Source: brainfuckInterpreter.js

/**
 * @file
 * This file provides a class and a script to interpret a Brainfuck program.
 */

 /**
  * Class representing a Brainfuck Interpreter.
  */
class BrainfuckInterpreter {

    /**
     * Constructor for a Brainfuck Interpreter
     */
    constructor(font) {
        this.textFont = font;
        this.initInterpreter();
    }

    /**
     * Set the initial values for the interpreter fields.
     */
    initInterpreter() {
        this.memory = [];
        this.currentCellIdx = 0;
        this.commands = "";
        this.input = "";
        this.currentCommandIdx = 0;
        this.currentInputIdx = 0;
        this.loopStartIdxs = [];
        this.finished = false;
        this.keyPressedListenerActive = false;
        this.instructionBaseTime = 500;
        this.memory.push(this.createCell());
    }

    /**
     * Set the initial values necessary for the visualization.
     * 
     * @param {RunningMethodEnum} runningMethod - Selected runningMethod for the execution. See RunningMethodEnum in utils.js
     */
    initGUI(runningMethod) {
        document.getElementById("outputTextArea").value = "";
        this.speedRange = document.getElementById("speedRange");
        if (runningMethod == RunningMethodEnum.RUN || runningMethod == RunningMethodEnum.RUN_VISUALIZE) {
            this.keyPressedListenerActive = false;
            if (runningMethod == RunningMethodEnum.RUN_VISUALIZE) {
                this.drawMemory();
            }
        }
        else if (runningMethod == RunningMethodEnum.VISUALIZE) {
            this.keyPressedListenerActive = true;
            this.drawMemory();
        }
    }

    /**
     * Remove every element in the canvas.
     */
    clearCanvas() {
        while (visualizer.scene.children.length)
        {
            visualizer.scene.remove(visualizer.scene.children[0]);
        }
    }

    /**
     * Creates a new MemoryCell object.
     * 
     * @return MemoryCell a whole new MemoryCell object
     */
    createCell() {
        return new MemoryCell(0, this.textFont);
    }

    /**
     * Handler for the increment '>' command.
     */
    incrementPointer() {
        this.currentCellIdx++;
        while (this.currentCellIdx >= this.memory.length) {
            this.memory.push(this.createCell());
        }
    }

    /**
     * Handle for the decrement '<' command.
     */
    decrementPointer() {
        if (this.currentCellIdx > 0) {
            this.currentCellIdx--;
        }
    }

    /**
     * Handler for the increment value '+' command.
     */
    incrementValue() {
        if (this.memory[this.currentCellIdx].value < 255) {
            (this.memory[this.currentCellIdx].value)++;
        }
    }

    /**
     * Handler for the decrement value '-' command.
     */
    decrementValue() {
        if (this.memory[this.currentCellIdx].value > 0) {
            (this.memory[this.currentCellIdx].value)--;
        }
    }

    /**
     * Handler for the this.input ',' command.
     * Gets one this.input character from the user according to the current runningMethod.
     * 
     * @param {RunningMethodEnum} runningMethod - Selected runningMethod for the execution. See RunningMethodEnum in utils.js
     */
    inputCommand(runningMethod) {
        if (runningMethod == RunningMethodEnum.RUN || runningMethod == RunningMethodEnum.RUN_VISUALIZE) {
            if (this.currentInputIdx >= this.input.length) {
                alert("Error: Input out of bounds");
                // TODO: Stop execution
                return;
            }
            let inputByte = this.input[this.currentInputIdx];
            this.memory[this.currentCellIdx].value = inputByte.charCodeAt(0);
            this.currentInputIdx++;
        }
        else if (runningMethod == RunningMethodEnum.VISUALIZE) {
            let inputByte = prompt("Please enter a value between 0-255");
            if (inputByte >= 0 && inputByte <= 255) {
                this.memory[this.currentCellIdx].value = inputByte;
            }
            else {
                alert("Error: Inncorrect this.input value");
                // TODO: Stop execution
            }
        }
    }

    /**
     * Handler for the output '.' command.
     */
    outputCommand() {
        var value = this.memory[this.currentCellIdx].value;
        if (value >= 0 && value <= 255) {
            var valChar = String.fromCharCode(value);
            var outputTextBox = document.getElementById("outputTextArea");
            var outputSoFar = outputTextBox.value;
            var newOutput = outputSoFar + valChar;
            outputTextBox.value = newOutput;
            Materialize.updateTextFields();
            $('#outputTextArea').trigger('autoresize');
        }
    }

    /**
     * Handler for the start loop '[' command.
     */
    startLoop() {
        var value = this.memory[this.currentCellIdx].value;
        if (value != 0) {
            this.loopStartIdxs.push(new Loop(this.currentCommandIdx, true));
        }
        else {
            this.loopStartIdxs.push(new Loop(this.currentCommandIdx, false));
        }
    }

    /**
     * Handler for the end loop '[' command.
     */
    endLoop() {
        if (this.loopStartIdxs.length == 0) {
            // TODO: Handle syntax error
            return;
        }
        var value = this.memory[this.currentCellIdx].value;
        if (value != 0) {
            this.currentCommandIdx = this.loopStartIdxs[this.loopStartIdxs.length - 1].idx;
        }
        else {
            this.loopStartIdxs.pop();
        }
    }

    /**
     * Select the correct handler for the current command of the tape and execute it
     * according to the current runningMethod.
     * 
     * @param {char} command - Char indicating the current command. See the supportedCommands list in utils.js
     * @param {RunningMethodEnum} runningMethod - Selected runningMethod for the execution. See RunningMethodEnum in utils.js
     */
    runCommand(command, runningMethod) {
        if (supportedCommands.indexOf(command) == -1 && ignoredCommands.indexOf(command) == -1) {
            // TODO: Handle wrong command error
            alert("Invalid command " + command + " = " + command.charCodeAt(0) + " at idx: " + this.currentCommandIdx);
        }
        if (this.loopStartIdxs.length == 0 || this.loopStartIdxs[this.loopStartIdxs.length - 1].valid) {
            if (command == '>') {
                this.incrementPointer();
            }
            else if (command == '<') {
                this.decrementPointer();
            }
            else if (command == '+') {
                this.incrementValue();
            }
            else if (command == '-') {
                this.decrementValue();
            }
            else if (command == ',') {
                this.inputCommand(runningMethod);
            }
            else if (command == '.') {
                this.outputCommand();
            }
        }

        if (command == '[') {
            this.startLoop();
        }
        else if (command == ']') {
            this.endLoop();
        }
        if (runningMethod == RunningMethodEnum.RUN_VISUALIZE || runningMethod == RunningMethodEnum.VISUALIZE) {
            this.drawMemory();
        }
    }

    /**
     * Get one new command from the user according to the current runningMethod.
     * 
     * @param {RunningMethodEnum} runningMethod - Selected runningMethod for the execution. See RunningMethodEnum in utils.js
     */
    nextCommand(runningMethod) {
        if (this.finished) {
            return;
        }
        if (runningMethod == RunningMethodEnum.RUN || runningMethod == RunningMethodEnum.RUN_VISUALIZE) {
            if (this.currentCommandIdx >= this.commands.length) {
                this.finished = true;
                return;
            }
            let nextCommandChar = this.commands[this.currentCommandIdx];
            this.runCommand(nextCommandChar, runningMethod);
            this.currentCommandIdx++;
            if (runningMethod == RunningMethodEnum.RUN) {
                this.nextCommand(runningMethod);
            }
            if (runningMethod == RunningMethodEnum.RUN_VISUALIZE) {
                let interpreter = this;
                setTimeout(function(){
                    interpreter.nextCommand(runningMethod);
                }, this.instructionBaseTime / this.speedRange.value);
            }
        }
        else if (runningMethod == RunningMethodEnum.VISUALIZE) {
            if (this.currentCommandIdx >= this.commands.length) {
                return;
            }
            let nextCommandChar = this.commands[this.currentCommandIdx];
            this.runCommand(nextCommandChar, runningMethod);
            this.currentCommandIdx++;
            if (this.currentCommandIdx < this.commands.length) {
                let interpreter = this;
                setTimeout(function(){
                    interpreter.nextCommand(runningMethod);
                }, this.instructionBaseTime / this.speedRange.value);
            }
        }
    }

    /**
     * Run a Brainfuck program according to the selected runningMethod.
     * Init all the necessary elements for the interpreter and the canvas.
     * 
     * @param {RunningMethodEnum} runningMethod - Selected runningMethod for the execution. See RunningMethodEnum in utils.js
     */
    runCode(runningMethod) {
        this.clearCanvas();
        this.initInterpreter();
        this.initGUI(runningMethod);
        if (runningMethod == RunningMethodEnum.RUN || runningMethod == RunningMethodEnum.RUN_VISUALIZE) {
            this.keyPressedListenerActive = false;
            this.commands = document.getElementById("codeTextArea").value;
            this.input = document.getElementById("inputTextArea").value;
            if (runningMethod == RunningMethodEnum.RUN) {
                this.nextCommand(runningMethod);
            }
            else if (runningMethod == RunningMethodEnum.RUN_VISUALIZE) {
                let interpreter = this;
                setTimeout(function(){
                    interpreter.nextCommand(runningMethod);
                }, this.instructionBaseTime / this.speedRange.value);
            }
        }
        else if (runningMethod == RunningMethodEnum.VISUALIZE) {
            this.keyPressedListenerActive = true;
            this.commands = "";
        }
    }

    /**
     * Creates a string representation of the current state of the this.memory.
     * 
     * @return string A string representation of the this.memory
     */
    memoryToString() {
        s = "";
        s += "[";
        for(i = 0; i < this.memory.length; i++) {
            s += this.memory[i].value + (i == this.memory.length - 1 ? "" : ",");
        }
        s += "]";
        return s;
    }

    /**
     * Creates a string representation of the current state of the loops.
     * 
     * @return string A string representation of the loops
     */
    loopsToString() {
        s = "";
        s += "[";
        for(i = 0; i < this.loopStartIdxs.length; i++) {
            s += "(" + this.loopStartIdxs[i].idx + ", " + this.loopStartIdxs[i].valid + ")" + (i == this.loopStartIdxs.length - 1 ? "" : ",");
        }
        s += "]";
        return s;
    }

    /**
     * Draw the state of each this.memory cell in the canvas and renders it.
     */
    drawMemory() {
        for (var i = 0; i < this.memory.length; i++) {
            this.memory[i].draw(i, this.currentCellIdx, this.textFont);
        }
        visualizer.render();
    }

}