Source: lib/vm.js

/*
 *  MIPS-Simulator : vm.js [Virtual Machine for Executing Instructions]
 *  Copyright (C)  2017  Progyan Bhattacharya, Bytes Club
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

"use strict";

const register     = require("./vm/register"),
      load         = require("./vm/load"),
      move         = require("./vm/move"),
      store        = require("./vm/store"),
      add          = require("./vm/add"),
      subtract     = require("./vm/subtract"),
      multiply     = require("./vm/multiply"),
      divide       = require("./vm/divide"),
      logicalAnd   = require("./vm/logical-and"),
      logicalOr    = require("./vm/logical-or"),
      logicalShift = require("./vm/logical-shift"),
      branch       = require("./vm/branch"),
      syscall      = require("./vm/syscall");

const regObject = (reg, register, i) => {
    let regObj = null;
    if (/\$r\d+/.test(reg)) {
        // Written in $r[n] format
        const index = Number(reg.slice(2));
        regObj = register.slice(index).shift();
    } else {
        regObj = register.find((v) => v.name === reg);
    }
    if (typeof regObj === "undefined" || regObj === null) {
        // Invalid register given
        console.error(`Fatal Error: Invalid register ${register} given,` +
            ` please check at the line number ${i}`);
        process.exit(4);
    }
    return regObj;
};

/** class: VM
 * @desc Virtual Machine for Simulator to Run
 * @param {FileStream}   stdin     - Standard Input for Virtual Machine
 * @param {FileStream}   stdout    - Standard Output for Virtual Machine
 * @prop  {Array}   register       - Register File for Virtual Machine
 * @prop  {Array}   AddressTable   - Address Lookup Table
 * @prop  {Array}   InstructionSet - Parse Tree Generated by Parse
 * @prop  {Number}  fenceSize      - Fence Register for Access Limitation
 * @prop  {Number}  ProgramCounter - Next Instruction Index
 * @prop  {boolean} shouldRun      - Execution State (Running | Halt)
 */
class VM extends Object {
    constructor({ stdin, stdout }) {
        super();

        // Member variables
        this.stdin              = typeof stdin !== "undefined" ?
                                  stdin :
                                  process.stdin;
        this.stdout             = typeof stdout !== "undefined" ?
                                  stdout :
                                  process.stdout;
        this.register           = register;
        this.AddressTable       = null;
        this.InstructionSet     = null;
        this.fenceSize          = 0;
        this.ProgramCounter     = -1;
        this.shouldRun          = true;
        this.run                = this.run.bind(this);
        this.execute            = this.execute.bind(this);
        this.load               = load.bind(this);
        this.move               = move.bind(this);
        this.loadOnRegister     = this.loadOnRegister.bind(this);
        this.valueFromRegister  = this.valueFromRegister.bind(this);
        this.encodeFromRegister = this.encodeFromRegister.bind(this);
        this.valueFromLabel     = this.valueFromLabel.bind(this);
        this.store              = store.bind(this);
        this.memory             = this.memory.bind(this);
        this.add                = add.bind(this);
        this.subtract           = subtract.bind(this);
        this.multiply           = multiply.bind(this);
        this.divide             = divide.bind(this);
        this.logicalAnd         = logicalAnd.bind(this);
        this.logicalOr          = logicalOr.bind(this);
        this.logicalShift       = logicalShift.bind(this);
        this.branch             = branch.bind(this);
        this.updatePC           = this.updatePC.bind(this);
        this.syscall            = syscall.bind(this);
    }

    /** method: run
     * @desc Invocation Method to ENtry POint of The Program
     * @param {Array} SymbolTable - Address Lookup Table
     * @param {Array} SyntaxTree  - Abstract Syntax Tree
     */
    run({ SymbolTable, SyntaxTree }) {
        // Load content in memory
        this.AddressTable = [].concat(SymbolTable);
        this.InstructionSet = [].concat(SyntaxTree);
        this.fenceSize = this.InstructionSet.length;
        const loader = SyntaxTree.shift();
        if (typeof loader !== "undefined" || loader !== null) {
            // Get entry point
            this.ProgramCounter = this.valueFromLabel("main");
            // Check if valid PC, or exit has called
            while (this.shouldRun && this.ProgramCounter >= 0 &&
                this.ProgramCounter < this.fenceSize) {
                this.execute();
            }
        }
    }

    /** method: execute
     * @desc Execution Module for Virtual Machine
     * @param {Array}  InstructionSet - Set of Instructions to be Executed
     * @param {Number} ProgramCounter - Index of Next Instruction
     */
    execute() {
        const { InstructionSet, ProgramCounter } = this;

        // Extract instruction at PC from set
        const key = InstructionSet.slice(ProgramCounter,
                    ProgramCounter + 1).shift();

        // Validity of instruction
        if (typeof key === "undefined" || key === null) {
            const counter = Number((ProgramCounter * 4) +
                            0x10000).toString(16).substr(-4);
            console.error("Undefined behaviour found at" +
                `instruction 0x${counter}`);
            process.exit(4);
        }

        // Increment PC by 1, point to next instrcution
        this.ProgramCounter = this.ProgramCounter + 1;

        // Execute current instrcution
        const { index, item } = key;
        switch (item.action) {
            case "Load":
                this.load(item, index);
                break;
            case "Move":
                this.move(item, index);
                break;
            case "Store":
                this.store(item, index);
                break;
            case "Add":
                this.add(item, index);
                break;
            case "Subtract":
                this.subtract(item, index);
                break;
            case "Multiplication":
                this.multiply(item, index);
                break;
            case "Division":
                this.divide(item, index);
                break;
            case "And":
                this.logicalAnd(item, index);
                break;
            case "Or":
                this.logicalOr(item, index);
                break;
            case "Shift":
                this.logicalShift(item, index);
                break;
            case "Branch":
                this.branch(item, index);
                break;
            case "OS":
                this.syscall(index);
                break;
            default:
                console.error("Fatal Error: Undefined action recieved for" +
                    `execution at line ${index}`);
                process.exit(4);
        }
    }

    /** method: loadOnRegister
     * @desc Load Data Into Register File
     * @param {string} register - Name of The Register
     * @param {any}    value    - Value to be Put
     * @param {Number} i        - Index of The Instruction
    */
    loadOnRegister(register, value, i) {
        const regObj = regObject(register, this.register, i);
        if (regObj.name === "$zero") {
            // $r0/$zero is a readonly register
            console.error("Fatal Error: Cannot overwrite zero register," +
                ` please check at the line number ${i}`);
            process.exit(4);
        }
        if (typeof value === "number") {
            // Integer value given
            if (isNaN(value)) {
                // Invalid value given
                console.error("Type Error: Type mismatch while storing into" +
                    ` register, expected integer at line ${i}`);
                process.exit(4);
            }
            // Update the value
            regObj.value = value;
        } else {
            // String value given
            regObj.value = value.data;
            regObj.encoding = value.encoding;
        }
    }

    /** method: valueFromRegister
     * @desc Fetch Data From Register File
     * @param {string} register - Name of The Register
     * @param {Number} i        - Index of The Instruction
    */
    valueFromRegister(register, i) {
        const regObj = regObject(register, this.register, i);
        if (! regObj.hasOwnProperty("value")) {
            // Un-initialized value received
            console.error("Value Error: Un-initialized value found" +
                ` in ${register}, please check at the line number ${i}`);
            process.exit(4);
        }
        if (typeof value === "number" && isNaN(regObj.value)) {
            // Invalid value found
            console.error(`Value Error: Invalid value found in ${register},` +
                ` please check at the line number ${i}`);
            process.exit(4);
        }
        // Return the value
        return regObj.value;
    }

    /** method: encodeFromRegister
     * @desc Fetch Encoding Information from Register File
     * @param {string} register - Name of The Register
     * @param {Number} i        - Index of The Instruction
    */
    encodeFromRegister(register, i) {
        const regObj = regObject(register, this.register, i);
        if (! regObj.hasOwnProperty("encoding")) {
            // Encoding information not found
            console.error("Type Error: Non string literal found in" +
                ` ${register}, please check at the line number ${i}`);
            process.exit(4);
        }
        // Return the encoding
        return regObj.encoding;
    }

    /** method: valueFromLabel
     * @desc Load Data From Main Memory
     * @param {string} label - Name of The Address Label
     * @param {Number} i     - Index of The Instruction
    */
    valueFromLabel(label, i) {
        const { AddressTable } = this;
        const id = AddressTable.find(item => item.name === label);
        if (typeof id === "undefined" || id === null) {
            if (label === "main") {
                // Entry point not found
                console.error("Fatal Error: Cannot find main entry point," +
                " failed to start execution");
                process.exit(4);
            }
            // Label (other than main) not found
            console.error(`Reference Error: Cannot find label ${label} at` +
                ` line ${i}`);
            process.exit(4);
        }
        // Default return
        return id.value;
    }

    /** method: memory
     * @desc Store Data Into Main Memory
     * @param {string} target - Name of The Taget Address
     * @param {any}    value  - Value to be Put
     * @param {Number} i     - Index of The Instruction
    */
    memory(target, value, i) {
        const entry = this.AddressTable.find(e => e.name === target);
        if (typeof entry !== "undefined" && (entry.readOnly === true ||
        entry.type === "Program Instructions")) {
            console.error("Error: Access to read only memory region." +
                ` Permission denied at ${i}`);
            process.exit(4);
        }
        this.AddressTable.push(
            {
                "name": target,
                "type": "Data Declaration",
                "value": {
                    "encoding": "ASCII",
                    "data": value
                }
            }
        );
    }

    /** method: updatePC
     * @desc Modify Program Counter in Case of Branching
     * @param {Number} address - Address of The Resolved Branch
    */
    updatePC(address) {
        this.ProgramCounter = address - 1;
    }
}

module.exports = VM;