adder-script
Version:
Python like language to execute untrusted codes in browsers and Node.js.
1,566 lines (1,280 loc) • 225 kB
JavaScript
(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 (