UNPKG

adder-script

Version:

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

1,566 lines (1,280 loc) 225 kB
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.AdderScript = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ "use strict"; /** * The main class that takes a raw code and compile it into a "bytecode" code ready for the interpreter. * The output of the compiler is the program AST (abstract syntax tree). * * Author: Ronen Ness. * Since: 2016 */ // include errors var Errors = require("./../errors"); // include jsface for classes var jsface = require("./../dependencies/jsface"), Class = jsface.Class, extend = jsface.extend; // include the lexer var Lexer = require("./lexer"); // include the parser var Parser = require("./parser"); // all token types var TokenTypes = require("./tokens"); // include default flags var defaultFlags = require("./default_flags"); // include errors var Errors = require("./../errors"); // the Compiler class - compile raw code into AST. var Compiler = Class({ // Compiler constructor // @param flags - compiler flags to set. for more info and defaults, see file default_flags.Js. constructor: function(flags) { // store compiler flags this._flags = flags || {}; // set default flags for (var key in defaultFlags) { if (this._flags[key] === undefined) { this._flags[key] = defaultFlags[key]; } } // how many spaces equal one tab this._spacesPerTab = ' '.repeat(this._flags.spacesNeededForBlockIndent); // create the lexer this._lexer = new Lexer(this._flags); }, // compile raw code into blocks of expressions // return value is a list of AST expressions and their corresponding line number ([ast, line]). // @param code - code to compile. Must be English only and use \n as line breaks, no \r\n. // @param flags - misc compilation flags: // fixLineBreaks: if true (default), will fix line breaks to be \n without \r. compile: function(code, flags) { // default flags flags = flags || {}; // trim code (right side trim only) code = code.replace(/\s+$/, ''); // remove illegal line breaks if (flags.fixLineBreaks || flags.fixLineBreaks === undefined) { code = code.replace(/\r\n/g, "\n").replace(/\r/g, ""); } // make sure there's no \r in code if (code.indexOf('\r') !== -1) { throw new Errors.SyntaxError("Illegal character found in code: '\\r'! Please use '\\n' only for line breaks.", 0); } // use the lexer to convert to tokens var tokens = this._lexer.parseExpression(code); // last block indent var lastBlockIndent = 0; // keep track on line index var lineIndex = 1; // return ast var ret = []; // iterate over lines and parse them for (var i = 0; i < tokens.length; ++i) { // if its a block indent change token if (tokens[i].t === TokenTypes.cblock) { // get current block indent var currBlockIndent = tokens[i].v; // check if need to create new block if (currBlockIndent > lastBlockIndent) { for (var k = currBlockIndent; k > lastBlockIndent; --k) { ret.push(["NEW_BLOCK", lineIndex]); } } // check if need to close current block else if (currBlockIndent < lastBlockIndent) { for (var k = currBlockIndent; k < lastBlockIndent; ++k) { ret.push(["END_BLOCK", lineIndex]); } } // store last block indent lastBlockIndent = currBlockIndent; continue; } // take chunk of tokens until break var j = i; var endToken = tokens[i]; while (endToken && endToken.t !== TokenTypes.lbreak) { endToken = tokens[++j]; } // if its line break (and not ';' for example), increase line index. if (endToken && endToken.v === "\n") { lineIndex++; } // get the tokens we handle now var currTokens = tokens.slice(i, j); // remove breaks from the end of tokens while (currTokens[currTokens.length-1] && currTokens[currTokens.length-1].t === TokenTypes.lbreak) { currTokens.pop(); } // set i to the end of the tokens we just processed i = j; // no tokens? skip if (currTokens.length === 0) continue; // compile current line var ast = Parser.parse(currTokens, lineIndex); if (ast && ast !== []) { ret.push([ast, lineIndex]); } } // return the parsed AST list return ret; }, }); // export the Compiler class module.exports = Compiler; },{"./../dependencies/jsface":24,"./../errors":28,"./default_flags":2,"./lexer":4,"./parser":5,"./tokens":6}],2:[function(require,module,exports){ "use strict"; module.exports = { spacesNeededForBlockIndent: 4, // how many spaces are needed to open a new scope / block. }; },{}],3:[function(require,module,exports){ module.exports = require("./compiler"); },{"./compiler":1}],4:[function(require,module,exports){ "use strict"; /** * The lexer takes a code string and convert it to a list of tokens. * * 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 console var Console = require("./../console"); // all arithmetic operators // note: its important to put the double operators (==, **, ..) before their singles version. sometimes we iterate and try to match first) var signOperators = ["+=", "-=", "*=", "/=", "|=", "&=", "%=", "**", "==", "!=", ">=", "<=", ">", "<", "+", "-", "*", "/", "|", "&", "%", "=", "."]; var wordsOperators = ["not in", "is not", "not", "in", "or", "and", "is"]; var operators = signOperators.concat(wordsOperators); // get default flags var defaultFlags = require("./default_flags"); // comment sign var commentPrefix = '#'; // get utils var Utils = require("./../utils"); // all token types var TokenTypes = require("./tokens"); // values that break between words // note: undefined is to identify out-of-string-range. var breakingSigns = [' ', '(', ')', '[', ']', undefined, ',', ':', ';', '\n', '\\']; breakingSigns = breakingSigns.concat(operators); breakingSigns = Utils.toSet(breakingSigns); // the Lexer class - convert code into tokens. var Lexer = Class({ // static stuff $static: { wordsOperators: wordsOperators, operators: operators, }, // Lexer constructor constructor: function(flags) { // store flags this._flags = flags || defaultFlags; }, // create a token instance /* token types: "p" // punctuation: commas, etc. "n" // numbers. "s" // strings. "v" // identifiers / variables / keywords. "o" // operators. "b" // line / command break. "_" // change in block index */ makeToken: function(type, val) { return {t: type, v: val}; }, // return if a character is a breaker, eg something that separate words etc isBreaker: function(expression, pos) { // first check comment if (this.isComment(expression, pos)) {return true;} // get character at position var c = expression[pos]; // check if space, undefined (means end of string) or operator return breakingSigns.has(c); }, // return if a character is a digit isNumber: function(c) { return (c >= '0' && c <= '9'); }, // return if opening string isOpeningString: function(c) { return c === '"' || c === "'"; }, // return if a character is a punctuation isPunc: function(c) { return c === "," || c === ":"; }, // return if a comment isComment: function(expression, start) { return expression[start] === commentPrefix; }, // read a whole number from starting pos until the end of the number // return [number, last_index] readNumber: function(expression, start) { // iterate until space var pos = start; var alreadyGotDot = false; while (expression[pos] === '.' || !this.isBreaker(expression, pos)) { // get current char var c = expression[pos]; // check if current char is a dot var isDot = c === '.'; // if we got non-digit (it means something like this: "4d41") its illegal expression. if (!this.isNumber(c) && !isDot) { throw new Errors.IllegalExpression(expression, "Invalid syntax (non-digits inside a number)", this.lineIndex); } // if its a dot: if (isDot) { // if already got dot in this expression its syntax error if (alreadyGotDot) { throw new Errors.IllegalExpression(expression, "Invalid syntax (multiple decimal marks in float)", this.lineIndex); } // set that already got dot alreadyGotDot = true; } // take next character pos++; } // return the number return [expression.substr(start, pos-start), pos-1]; }, // read the whole operator from string pos // return [operator, last_index] readOperator: function(expression, start) { // get current part that might contain the operator var currSeg = expression.substr(start, 10); // first check word operators for (var i = 0; i < wordsOperators.length; ++i) { // get current word operator var currOp = wordsOperators[i]; // check if match and if so return if (currSeg.indexOf(currOp + " ") === 0) { return [currOp, start + currOp.length - 1]; } } // now iterate over sign operators for (var i = 0; i < signOperators.length; ++i) { // get curr operator var curr = signOperators[i]; // check if operator match if (currSeg.substr(0, curr.length) === curr) { return [curr, start + curr.length - 1]; } } // if operator not found return null return null; }, // read the whole string from string pos // return [string, last_index] readString: function(expression, start) { // check if our quote sign is ' or " var quote = expression[start] === '"' ? '"' : "'"; // loop until finding the closing quotes (quotes without \ before them) var i = start; var lastC; var c; while (c = expression[++i]) { lastC = c; if (c === quote && lastC !== '\\') break; } // didn't find closing quotes? if (c === undefined) { throw new Errors.IllegalExpression(expression, "EOL while scanning string literal.", this.lineIndex); } // parse the string inside var val = expression.substr(start, i-start+1); return [val, i]; }, // read the whole punctuation from string pos // return [punctuation, last_index] readPunctuation: function(expression, start) { return [expression[start], start]; }, // read a comment until the end // unlike the other 'read' functions, this won't return the actual comment, just the ending position readComment: function(expression, start) { // iterate until end of string or until line break var pos = start; while (expression[pos] !== undefined && expression[pos] !== "\n") { pos++; } // return new position return ["", pos]; }, // read a word from string pos // return [word, last_index] readWord: function(expression, start) { // read characters until complete current word var pos = start; while (!this.isBreaker(expression, pos)) { pos++; } // get whole word var word = expression.substr(start, pos-start); // return word and position // take one char back so we won't skip the breaking character return [word, pos-1]; }, // convert string expression into list of tokens. parseExpression: function(expression) { // return list var ret = []; // current and last character parsed var lastC; var c; // count lines this.lineIndex = 1; // if true we need to skip next line break var skipNextLineBreak = false; // last block indent var lastBlockIndent = 0; // was last character a line break? var wasLineBreak = false; // indicating that last token was an inline block var inlineBlocks = 0; // count spaces we had in a row after line break var spacesInRow = 0; // iterate over all characters of expression for (var i = 0; i < expression.length; ++i) { // skip white spaces if (expression[i] === ' ') { if (wasLineBreak) spacesInRow++; continue; } if (expression[i] === '\t') { if (wasLineBreak) spacesInRow += this._flags.spacesNeededForBlockIndent; continue; } // if we got spaces after line break, calc block indent if (wasLineBreak) { // if this is break after inline block if (inlineBlocks > 0) { lastBlockIndent -= inlineBlocks; ret.push(this.makeToken(TokenTypes.cblock, lastBlockIndent)); inlineBlocks = 0; } // if its a regular block and line break wasn't ';' else if (lastC !== ';') { // get spaces needed for block indent var spacesForIndent = this._flags.spacesNeededForBlockIndent; // make sure current character is not line break, so we won't change blocks / validate indent for empty lines if (expression[i] !== '\n') { // check if spaces are not multiply indent spaces, but only if last token wasn't ';' (inline break) if ((spacesInRow % spacesForIndent) !== 0) { throw new Errors.SyntaxError("Bad block indent (spaces not multiply of " + this._flags.spacesNeededForBlockIndent + ")", this.lineIndex); } // calc current block indent and add block change token var blockIndent = spacesInRow / spacesForIndent; if (blockIndent !== lastBlockIndent) { ret.push(this.makeToken(TokenTypes.cblock, blockIndent)); lastBlockIndent = blockIndent; } } } // zero spaces count spacesInRow = 0; } // if its a comment - read it if (this.isComment(expression, i)) { // read comment var tokenData = this.readComment(expression, i); i = tokenData[1]; // add line break after the comment // but only if didn't reach the end if (expression[i]) { this.lineIndex++; wasLineBreak = true; ret.push(this.makeToken(TokenTypes.lbreak, '\n')); } // continue to next character continue; } // store last character and get current character lastC = c; var c = expression[i]; // special case - command break if (c === ';' || c === '\n') { // increase line count this.lineIndex++; // if should skip line break skip it if (skipNextLineBreak) { skipNextLineBreak = false; continue; } // do line break lastC = c; wasLineBreak = true; ret.push(this.makeToken(TokenTypes.lbreak, c)); continue; } // special case 2 - anti-line break, eg character that combine lines together else if (c === '\\') { if (expression[i+1] !== '\n') { throw new Errors.SyntaxError("Invalid character after \\ sign.", this.lineIndex); } skipNextLineBreak = true; continue; } // special case - if last character was ':', but we didn't get a new line, it means its an inline block if (lastC === ":") { // add break + open block ret.push(this.makeToken(TokenTypes.lbreak, ";")); lastBlockIndent++; ret.push(this.makeToken(TokenTypes.cblock, lastBlockIndent)); inlineBlocks++; } // not a line break wasLineBreak = false; // if we got an parenthesis parse it if (c === "(" || c === ")") { // add to tokens list ret.push(this.makeToken('o', c)); continue; } // if punctuation if (this.isPunc(c)) { // read punctuation var tokenData = this.readPunctuation(expression, i); var token = tokenData[0]; i = tokenData[1]; // add punctuation to tokens list ret.push(this.makeToken(TokenTypes.punctuation, token)); continue; } // if a number if (this.isNumber(c)) { // read punctuation var tokenData = this.readNumber(expression, i); var token = tokenData[0]; i = tokenData[1]; // add punctuation to tokens list ret.push(this.makeToken(TokenTypes.number, token)); continue; } // try to read an operator // with operators its a little different - we just try to read it and return null if not found var tokenData = this.readOperator(expression, i); if (tokenData) { // get token and new index var token = tokenData[0]; i = tokenData[1]; // add operator to tokens list ret.push(this.makeToken(TokenTypes.operator, token)); continue; } // if got string read it all until its closed if (this.isOpeningString(c)) { // read operator var tokenData = this.readString(expression, i); var token = tokenData[0]; i = tokenData[1]; // add operator to tokens list ret.push(this.makeToken(TokenTypes.string, token)); continue; } // if got here it means its a keyword, var-name, statement, etc.. // read word and add it var tokenData = this.readWord(expression, i); var token = tokenData[0]; i = tokenData[1]; // illegal token? if (token === "") { throw new Errors.IllegalExpression(expression, "Invalid or unexpected token '" + c + "'!", this.lineIndex); } // add operator to tokens list ret.push(this.makeToken(TokenTypes.identifier, token)); } // return parsed expression Console.debug("Lexer parsed", ret); return ret; }, }); // export the Lexer class module.exports = Lexer; },{"./../console":7,"./../dependencies/jsface":24,"./../errors":28,"./../utils":79,"./default_flags":2,"./tokens":6}],5:[function(require,module,exports){ "use strict"; /** * The parser takes tokens list and convert into a tree of expressions, optimized for evaluation. * To put it simple: it converts a list of tokens into an AST. * * Author: Ronen Ness. * Since: 2016 */ // include errors var Errors = require("./../errors"); // dictionary of symbols that are part of the AST var symbols = {}; // include general utils var Utils = require("./../utils"); // require some language defs var LanguageDefs = require("./../language/defs"); // set of operators that are words var wordOperators = Utils.toSet(Object.keys(LanguageDefs.keywords)); // get all token types var TokenTypes = require("./tokens"); // global scope that holds the current tokens and position we are parsing. // these are used internally in all the helper and parsing functions. var gscope = { i: 0, tokens: [], line: undefined, }; // function to define a new symbol type var defineSymbol = function (id, nud, lbp, led) { var sym = symbols[id] || {}; symbols[id] = { lbp: sym.lbp || lbp, nud: sym.nud || nud, led: sym.lef || led }; }; // define an infix symbol type var defineInfix = function (id, lbp, rbp, led) { rbp = rbp || lbp; defineSymbol(id, null, lbp, led || function (left) { return { type: id, left: left, right: parseExpression(rbp) }; }); }; // define a prefix symbol type var definePrefix = function (id, rbp) { defineSymbol(id, function () { return { type: id, right: parseExpression(rbp) }; }); }; // define comma and colon symbols defineSymbol(","); defineSymbol("blockopen", function (val) { return val; }); // define number symbol defineSymbol("number", function (number) { return number; }); // define string symbol defineSymbol("string", function (str) { return str; }); // define identifier symbol defineSymbol("identifier", function (name) { var token = currToken(); if (token && token.type === "(" && !wordOperators.has(name.value)) { var args = []; if (gscope.tokens[gscope.i + 1].v === ")") {popToken();} else { do { popToken(); if (!currToken()) { throw new Errors.SyntaxError("Missing closing parenthesis ')'"); } args.push(parseExpression(2)); } while (currToken() && currToken().type === ","); if (!currToken() || currToken().type !== ")") { throw new Errors.SyntaxError("Missing closing parenthesis ')'"); } } popToken(); return { type: "call", args: args, name: name.value }; } return name; }); // define the '(' symbol defineSymbol("(", function () { var value = parseExpression(2); if (currToken().type !== ")") new Errors.SyntaxError("Missing closing parenthesis!"); popToken(); return value; }); // define the ')' symbol defineSymbol(")"); definePrefix("-", 10); definePrefix("+", 10); definePrefix("not", 3); definePrefix("for", 10); defineInfix("*", 7); defineInfix("**", 9, 8); defineInfix("/", 7); defineInfix("+", 6); defineInfix("-", 6); defineInfix("%", 7); defineInfix("|", 4, 4); defineInfix("&", 5, 5); defineInfix("or", 3); defineInfix("and", 3); defineInfix("in", 4); defineInfix("not in", 4); defineInfix("is", 4); defineInfix("is not", 4); defineInfix("==", 4); defineInfix("!=", 4); defineInfix(">=", 4); defineInfix("<=", 4); defineInfix("<", 4); defineInfix(">", 4); defineInfix(".", 13); // assignment operator defineInfix("=", 1, 2, function (left) { if (left.type === "call") { for (var i = 0; i < left.args.length; i++) { if (left.args[i].type !== "identifier") throw new Errors.SyntaxError("Invalid argument name '" + left.args[i].value + "'!"); } return { type: "function", name: left.name, args: left.args, value: parseExpression(2) }; } else if (left.type === "identifier") { return { type: "assign", name: left.value, value: parseExpression(2) }; } else throw new Errors.SyntaxError("Can't assign to literal!"); }); // add all assignment+ operators var assignmentWith = ["+", "-", "*", "/", "|", "&", "%"]; for (var i = 0; i < assignmentWith.length; ++i) { (function(operator){ defineInfix(operator + "=", 1, 2, function (left) { if (left.type === "call") { for (var i = 0; i < left.args.length; i++) { if (left.args[i].type !== "identifier") throw new Errors.SyntaxError("Invalid argument name '" + left.args[i].value + "'!"); } return { type: "function", name: left.name, args: left.args, value: parseExpression(2) }; } else if (left.type === "identifier") { return { type: "assign+", op: operator, name: left.value, value: parseExpression(2) }; } else throw new Errors.SyntaxError("Can't assign to literal!"); }); })(assignmentWith[i]); } // convert a token to a symbol instance var tokenToSymbol = function (token) { // convert from token types to symbol types var type; switch (token.t) { case TokenTypes.operator: case TokenTypes.punctuation: if (token.v === ':') {type = 'blockopen';} else {type = token.v;} break; case TokenTypes.number: type = 'number'; break; case TokenTypes.identifier: type = 'identifier'; break; case TokenTypes.string: type = 'string'; break; default: throw new Errors.SyntaxError("Unknown token type '" + token.t + "'!"); } // create symbol and set its type and value var sym = Object.create(symbols[type]); sym.type = type; sym.value = token.v; // return the newly created symbol return sym; }; // start the actual parsing! // function to get current token var currToken = function () { return gscope.tokens[gscope.i] ? tokenToSymbol(gscope.tokens[gscope.i]) : null; }; // function to get current token and advance index to next token var popToken = function () { var ret = currToken(); gscope.i++; return ret; }; // parse an expression var parseExpression = function (rbp) { // default rbp if (rbp === undefined) {rbp = 0;} // get current token and advance to next var t = popToken(); if (t === null) {return;} // if current token is nud type if (!t.nud) {throw new Errors.SyntaxError("Unexpected token: " + t.type);} // get left operand var left = t.nud(t); // parse next gscope.tokens while (currToken() && rbp < currToken().lbp) { t = popToken(); if (!t.led) {throw new Errors.SyntaxError("Unexpected token: " + t.type);} left = t.led(left); } // return left operand return left; }; // take a list of tokens and generate a parsed tree // @param tokens - list of tokens, output of lexer. // @param line - is the line we are parsing (optional, used for exceptions etc) function parse(tokens, line) { // set global scope of current parsing - current tokens and index gscope.tokens = tokens; gscope.i = 0; gscope.line = line; try { // parse all tokens and return AST var parseTree = []; while (currToken()) { parseTree.push(parseExpression()); } return parseTree; } catch(e) { if (e.expectedError) { if (e.line === undefined) { e.message += " [at line: " + gscope.line + "]"; e.line = gscope.line; } throw e; } throw new Errors.SyntaxError("Unknown syntax error!", gscope.line); } } // export the parser functions module.exports = { parse: parse, }; },{"./../errors":28,"./../language/defs":34,"./../utils":79,"./tokens":6}],6:[function(require,module,exports){ module.exports = { punctuation: 'p', // punctuation: commas, etc. number: 'n', // numbers. string: 's', // strings. identifier: 'v', // identifiers / variables / keywords. operator: 'o', // operators. lbreak: 'b', // line / command break. cblock: '_', // change in block index } },{}],7:[function(require,module,exports){ "use strict"; /** * Override these functions to get debug data while using AdderScript. * * Author: Ronen Ness. * Since: 2016 */ module.exports = { // override these functions to get output log: function() {}, debug: function() {}, warn: function() {}, info: function() {}, // bind all functions to native javascript console bindToNativeConsole: function() { this.log = function() {console.log ("AdderScript.log>", arguments);}; this.debug = function() {console.debug ("AdderScript.debug>", arguments);}; this.warn = function() {console.warn ("AdderScript.warn>", arguments);}; this.info = function() {console.info ("AdderScript.info>", arguments);}; }, } },{}],8:[function(require,module,exports){ "use strict"; /** * The Block class represent a chunk of code, made of statements and sub-blocks to execute by order. * Do not confuse the Block with the Scope class; Scope is the runtime params, vars, registers, which are alive while executing the code, * while blocks are the structure of a compiled, loaded code, persistent and const between executions. * * 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"); // require the executable class var Executable = require("./executable"); // Block class var Block = Class(Executable, { // Block constructor // @param context - context of program currently executed. constructor: function(context) { // call base class Block.$super.call(this, context, null); // create executable (sub blocks and statements) queue this._executables = []; }, // execute the block statements. execute: function() { // get current scope var scope = this._context.getScope(); // iterate over executables (statements and sub blocks) var lastExecutable = null; for (var i = 0; i < this._executables.length; ++i) { // if "return" statement was called if (scope.calledReturn) { break; } // if "continue" or "break" statement was called (both raise the continue flag. else if (scope.calledContinue) { break; } // get current executable var curr = this._executables[i]; // do some tests on last executable block / statement if (lastExecutable) { // check if previous executable is a statement that cause a break if (lastExecutable.isBreakingBlock) { return; } // check if current executable is a block that we need to skip if (lastExecutable.skipFollowingBlock && curr.constructor === Block) { continue; } } // execute child block / statement this._context._interpreter.evalStatement(curr); lastExecutable = curr; } }, // add statement to block. addStatement: function(statement) { statement.setParentBlock(this, this._executables.length); this._executables.push(statement); this._lastStatement = statement; }, // add sub block to block. addBlock: function(block) { // get last executable to set its following block var lastExecutable = this._executables[this._executables.length-1]; if (lastExecutable === undefined) { throw new Errors.SyntaxError("Unexpected new block indent!"); } lastExecutable.setFollowingBlock(block); // set block parent block and add to executables list block.setParentBlock(this, this._executables.length); this._executables.push(block); }, // return all sub-blocks and statements. getChildren: function() { return this._executables; }, // return a debug representation of this block getDebugBlocksView: function(indent) { // default indent levels indent = indent || 1; // get indent spaces prefix var indentPrefix = ""; for (var i = 0; i < indent; ++i) {indentPrefix += ' ';} // return string var ret = "block:" + "\n"; // iterate over executables and print them for (var i = 0; i < this._executables.length; ++i) { // get current executable var curr = this._executables[i]; // if block if (curr.type === "block") { ret += indentPrefix + curr.getDebugBlocksView(indent + 1) + "\n"; } else { ret += indentPrefix + curr.getRepr() + "\n"; } } // add block closure ret += indentPrefix + "end_block" + "\n"; // return the result string return ret; }, // executable type type: "block", }); // export the scope class module.exports = Block; },{"./../dependencies/jsface":24,"./../errors":28,"./executable":12}],9:[function(require,module,exports){ "use strict"; /** * A builtin function that link between JavaScript code and a function accessible in the script execution. * Note: this is not the same as a function that the user script define in runtime. The built-in functions are pre-defined and * hard coded and are part of the language itself. * * With these we implement the most basic things like print, max, min, etc.. all the language built-ins. * * 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 variable class var Variable = require("./variable"); // include utils var Utils = require("./../utils"); // BuiltinFunction class // NOTE! when the function is executed, 'this' will always be the containing object. var BuiltinFunction = Class({ // built-in function constructor constructor: function() { }, // the actual function implementation. // @param args - list of arguments (evaluated values). __imp: function(args) { throw Errors.NotImplemented(); }, // execute this statement. // @param args - list of arguments (evaluated values) to send to the function. execute: function(args, obj) { // call function try { return this.__imp.apply(obj, args); } // catch errors catch (e) { if (e.expectedError){ throw e; } throw new Errors.InternalError("Exception in built-in function!", e); } }, // functions are built-in Adder objects __isAdderObject: true, // this must always be true (used internally) isFunction: true, // indicate that this object is abuilt-in function isBuiltinFunc: true, // indicate that this function is deterministic (eg for func(x) result will always be 'y'). deterministic: true, // how many args are required for this function // note: set to null for any number of args requiredArgs: 0, // number of optional params optionalArgs: 0, // convert to string toString: function() { return '<' + this.identifier + '>'; }, // convert to repr toRepr: function() { return this.toString(); }, // static stuff $static: { // create a new built-in function type from a plain function. // @param func - function that implements the built-in function logic (get list of variables as arguments). // @param mandatoryParams - integer, how many params must be provided to this function (null for any number of params). // @param optionalParams - integer, how many additional optional params can be provided to this function. // @param deterministic - indicate if this function is deterministic (default to true). create: function(func, mandatoryParams, optionalParams, deterministic) { // create and return the builtin function prototype var type = Class(BuiltinFunction, { __imp: func, requiredArgs: mandatoryParams || null, optionalArgs: optionalParams || null, deterministic: deterministic === undefined ? true : deterministic, }); return new type(); }, // if got only one argument and its a list or a set, return the list internal array. // else, just return arguments getArgumentsOrListContent: function(context, args) { // if we got only one argument.. if (args.length === 1) { // if its a list: if (args[0].type === "list") { return args[0]._value._list; } // else if its a set: else if (args[0].type === "set") { var ret = Utils.toArray(args[0]._value._set); return Variable.makeVariables(context, ret); } } // if not list / set one arg, return the arguments we got return args; }, }, }); // export the Executable class module.exports = BuiltinFunction; },{"./../dependencies/jsface":24,"./../errors":28,"./../utils":79,"./variable":22}],10:[function(require,module,exports){ "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 (