UNPKG

adder-script

Version:

Python like language to execute untrusted codes in browsers and Node.js.

331 lines (256 loc) 10.8 kB
"use strict"; /** * A global object persistent to the execution of a program, that contains stuff like pointer to the current interpreter, the code, * the global scope, sub scopes, etc. * * The context is one of the main internal objects / APIs that statements and expressions use inside. * * Author: Ronen Ness. * Since: 2016 */ // include jsface for classes var jsface = require("./../dependencies/jsface"), Class = jsface.Class, extend = jsface.extend; // include errors var Errors = require("./../errors"); // include scopes var Scope = require("./scope"); // require utils var Utils = require("./../utils"); // require variable type var Variable = require("./variable"); // Context class var Context = Class({ // context constructor. // @param interpreter - interpreter instance. // @param stackLimit - max stack depth (cause stack overflow if exceed this limit). // @param maxVarsInScope - maximum number of vars allowed per scope. constructor: function(interpreter, stackLimit, maxVarsInScope) { // set interpreter this._interpreter = interpreter; // create globals this._globals = new Scope(this, 0, null, maxVarsInScope); this._currScope = this._globals; // set flags this._stackLimit = stackLimit; this._maxVarsPerScope = maxVarsInScope; // readonly variables this._readonly = new Set(); // create stack this._stack = []; // push default root stack this.stackPush(); }, // return the type of the current scope getCurrBlockType: function() { return this._currScope._type; }, // clear the context stack until global (will end of at global scope) clearStack: function() { while (this._stack.length > 1) { this.stackPop(); } }, // reset context (clear stack and remove all user defined global vars) reset: function() { // clear stack this.clearStack(); // remove all user variables for (var key in this._globals._vars) { if (!this._readonly.has(key)) { this._globals.remove(key); } } }, // add to stack // @param type is the type of current scope - "function", "loop", ... stackPush: function(type) { // get scope var scope = this._stack.length === 0 ? this._globals : new Scope(this, this._stack.length, type, this._maxVarsPerScope); // create new stack entry this._stack.push({ scope: scope, // the scope itself breakAccess: type === "function", // does this scope break access to variables from previous scope? }); // check stack size if (this._stack.length > this._stackLimit) { throw new Errors.StackOverflow(this._stackLimit); } // set new current scope this._currScope = this._stack[this._stack.length - 1].scope; }, // pop from stack stackPop: function() { // get current scope and parent scope as default globals var thisScope = this._currScope; var parentScope = this._globals; // if its not first stack and current scope is not breaking if (this._stack.length > 1 && !this._stack[this._stack.length - 1].breakAccess) { // get this stack and previous stack parentScope = this._stack[this._stack.length - 2].scope; // iterate current scope vars and copy to upper scope for (var key in thisScope._vars) { parentScope.setVar(key, thisScope._vars[key]); } } // set return value for global scope parentScope.returnValue = thisScope.returnValue; // if variable existed in global scope, update it too for (var key in thisScope._vars) { if (this._globals._vars[key] !== undefined) { this._globals.setVar(key, thisScope._vars[key]); } } // pop stack this._stack.pop(); // if stack is empty (should never happen) raise exception if (this._stack.length === 0) { throw new Errors.RuntimeError("Invalid return statements at global block!"); } // set new current scope this._currScope = this._stack[this._stack.length - 1].scope; }, // remove variable remove: function(varName) { // make sure not readonly if (this._readonly.has(varName)) { throw new Errors.RuntimeError("Cannot delete readonly variable '" + varName + "'!"); } // first get all relevant scopes var scopes = this._getSharedScopes(); // now remove var from all scopes for (var i = 0; i < scopes.length; ++i) { scopes[i].remove(varName); } }, // return a list with all scopes that share the following variables (eg non-breaking scopes) + the global scope _getSharedScopes: function() { // the list of scopes to return, starting with current scope var ret = [this.getScope()]; // iterate from one scope above current until global (not including global) for (var i = this._stack.length - 2; i > 0; --i) { // add current scope var curr = this._stack[i]; ret.push(curr.scope); // note: we need to break AFTER access break, not before, as the scope of a new function has access break if (curr.breakAccess) { break; } } // add global and return if (this._stack.length > 0) {ret.push(this._globals);} return ret; }, // return local scope getScope: function() { return this._currScope; }, // get variable either from local or global scope. // @param key - variable name / key. // @param object - object to get variable from (if undefined will get from current scope). getVar: function(key, object) { // first, if we got an object to fetch variable from, use that object if (object !== undefined) { // this is important so if for example we have a function that return a list we can use it like this: // foo().someListFind(x)... if (!object.__isAdderObject) { object = Variable.makeAdderObjects(this, object); if (!object.isSimple) { object = object._value; } } // if got object to get from // try to get variable from object's api, and assert if not found var resultObj = object && object.api ? object.api[key] : undefined; if (resultObj === undefined) { throw new Errors.RuntimeError("Object '" + (object.name || object.type || object.identifier || typeof object) + "' has no attribute '" + key + "'."); } // return variable return resultObj; } // if we got here it means its not from an object, we need to search for the variable in scopes. // climb up from current scope until breaking and try to get the variable (not including index 0 which is global) var val; for (var i = this._stack.length-1; ((i > 0) && (val === undefined)); --i) { // try to get var from current scope val = this._stack[i].scope.getVar(key, object); // if this scope breaks access stop here if (this._stack[i].breakAccess) { break; } } // still not found? try global scope if (val === undefined) { val = this._globals.getVar(key, object); } // raise undefined var exception if (val === undefined) { throw new Errors.UndefinedVariable(key); } // return value return val; }, // return if a variable name exists. // @param key - variable name / key. // @param object - object to get variable from (if undefined will get from current scope). exist: function(key, object) { // first try local scope var val = this.getScope().getVar(key, object); // not defined? try global scope if (val === undefined) { val = this._globals.getVar(key, object); } // return if exist return val !== undefined; }, // set variable in current scope. // @param key - variable name / key. // @param val - value to set, must be of 'variable' type. // @param readonly - if true, cannot override this variable / object. // @param force - if true, will override variable even if exist and readonly. setVar: function(key, val, readonly, force) { // readonly vars can only be set at global scope if (readonly && this._stack.length > 1) { throw new Errors.RuntimeError("Cannot set readonly variable outside global scope. Variable key: '" + key + "'."); } // make sure its not readonly in global scope if (this._readonly.has(key) && !force) { throw new Errors.RuntimeError("Trying to override readonly variable or reserved word: '" + key + "'."); } // set readonly if (readonly) { this._readonly.add(key); } // set variable return this.getScope().setVar(key, val, readonly, force); }, // create a variable in this context createVariable: function(val, forceNew) { return Variable.makeVariables(this, val, forceNew); }, // return a list with all identifiers in current scope, including global scope getAllIdentifiers: function() { // get all scopes that share variables with current scope including global var scopes = this._getSharedScopes(); // create empty list for all identifiers var ret = []; // iterate over scopes and get identifiers for (var i = 0; i < scopes.length; ++i) { ret = ret.concat(scopes[i].getAllIdentifiers()); } // make unique by converting to a set and back into an array ret = Utils.toArray(Utils.toSet(ret)); // return final list return ret; }, }); // export the Context class module.exports = Context;