Permalink
Cannot retrieve contributors at this time
4374 lines (3935 sloc)
137 KB
| /* ***** BEGIN LICENSE BLOCK ***** | |
| * Version: MPL 1.1/GPL 2.0/LGPL 2.1 | |
| * | |
| * The contents of this file are subject to the Mozilla Public License Version | |
| * 1.1 (the "License"); you may not use this file except in compliance with | |
| * the License. You may obtain a copy of the License at | |
| * http://www.mozilla.org/MPL/ | |
| * | |
| * Software distributed under the License is distributed on an 'AS IS' basis, | |
| * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License | |
| * for the specific language governing rights and limitations under the | |
| * License. | |
| * | |
| * The Original Code is Bespin. | |
| * | |
| * The Initial Developer of the Original Code is | |
| * Dimitris Vardoulakis <dimvar@gmail.com> | |
| * Portions created by the Initial Developer are Copyright (C) 2010 | |
| * the Initial Developer. All Rights Reserved. | |
| * | |
| * Contributor(s): | |
| * Dimitris Vardoulakis <dimvar@gmail.com> | |
| * Patrick Walton <pcwalton@mozilla.com> | |
| * | |
| * Alternatively, the contents of this file may be used under the terms of | |
| * either the GNU General Public License Version 2 or later (the "GPL"), or | |
| * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), | |
| * in which case the provisions of the GPL or the LGPL are applicable instead | |
| * of those above. If you wish to allow use of your version of this file only | |
| * under the terms of either the GPL or the LGPL, and not to allow others to | |
| * use your version of this file under the terms of the MPL, indicate your | |
| * decision by deleting the provisions above and replace them with the notice | |
| * and other provisions required by the GPL or the LGPL. If you do not delete | |
| * the provisions above, a recipient may use your version of this file under | |
| * the terms of any one of the MPL, the GPL or the LGPL. | |
| * | |
| * ***** END LICENSE BLOCK ***** */ | |
| /* | |
| * Narcissus - JS implemented in JS. | |
| * | |
| * Control-flow analysis to infer types. The output is in ctags format. | |
| */ | |
| /* (Possible) TODOs: | |
| * - now all objs are in heap. If it's too imprecise, treat them as heap vars. | |
| * Create on stack & heap, and if heap changes when u need the obj then join. | |
| * - representation of Aobj: in the common case, an abstract obj has one proto | |
| * and one constructor. Specialize for this case. | |
| */ | |
| /* | |
| * Semantics of: function foo (args) body: | |
| * It's not the same as: var foo = function foo (args) body | |
| * If it appears in a script then it's hoisted at the top, so it's in funDecls | |
| * If it appears in a block then it's visible after it's appearance, in the | |
| * whole rest of the script!! | |
| * {foo(); {function foo() {print("foo");}}; foo();} | |
| * The 1st call to foo throws, but if you remove it the 2nd call succeeds. | |
| */ | |
| /* (POSSIBLY) UNSOUND ASSUMPTIONS: | |
| * - Won't iterate loops to fixpt. | |
| * - Return undefined not tracked, eg (if sth return 12;) always returns number. | |
| * - If the prototype property of a function object foo is accessed in a weird | |
| * way, eg foo["proto" + "type"] the analysis won't figure it out. | |
| * - when popping from an array, I do nothing. This is hard to make sound. | |
| */ | |
| //////////////////////////////////////////////////////////////////////////////// | |
| ///////////////////////////// UTILITIES ///////////////////////////////////// | |
| //////////////////////////////////////////////////////////////////////////////// | |
| if (!Array.prototype.forEach) | |
| Array.prototype.forEach = function(fun) { | |
| for (var i = 0, len = this.length; i < len; i++) | |
| /* if (i in this) */ fun(this[i], i, this); | |
| }; | |
| // search for an elm in the array that satisfies pred | |
| Array.prototype.member = function(pred) { | |
| for (var i = 0, len = this.length; i < len; i++) | |
| /* if (i in this) */ if (pred(this[i])) return this[i]; | |
| return false; | |
| }; | |
| Array.prototype.memq = function(sth) { | |
| for (var i = 0, len = this.length; i < len; i++) | |
| /* if (i in this) */ if (sth === this[i]) return this[i]; | |
| return false; | |
| }; | |
| // starting at index, remove all elms that satisfy the pred in linear time. | |
| Array.prototype.rmElmAfterIndex = function(pred, index) { | |
| if (index >= this.length) return; | |
| for (var i = index, j = index, len = this.length; i < len; i++) | |
| if (!pred(this[i])) this[j++] = this[i]; | |
| this.length = j; | |
| }; | |
| // remove all duplicates from array (keep first occurence of each elm) | |
| // pred determines the equality of elms | |
| Array.prototype.rmDups = function(pred) { | |
| var i = 0, self = this; | |
| while (i < (this.length - 1)) { | |
| this.rmElmAfterIndex(function(elm) { return pred(elm, self[i]); }, i+1); | |
| i++; | |
| } | |
| }; | |
| // compare two arrays for structural equality | |
| function arrayeq(eq, a1, a2) { | |
| var len = a1.length, i; | |
| if (len !== a2.length) return false; | |
| for (i=0; i<len; i++) if (!eq(a1[i], a2[i])) return false; | |
| return true; | |
| } | |
| function buildArray(size, elm) { | |
| var a = new Array(size); | |
| for (var i=0; i<size; i++) a[i] = elm; | |
| return a; | |
| } | |
| function buildString(size, elm) { | |
| return buildArray(size, elm).join(""); | |
| } | |
| // merge two sorted arrays of numbers into a sorted new array, remove dups! | |
| function arraymerge(a1, a2) { | |
| var i=0, j=0, len1 = a1.length, len2 = a2.length, a = new Array(); | |
| while (true) { | |
| if (i === len1) { | |
| for (; j < len2; j++) a.push(a2[j]); | |
| return a; | |
| } | |
| if (j === len2) { | |
| for (; i<len1; i++) a.push(a1[i]); | |
| return a; | |
| } | |
| var diff = a1[i] - a2[j]; | |
| if (diff < 0) | |
| a.push(a1[i++]); | |
| else if (diff > 0) | |
| a.push(a2[j++]); | |
| else | |
| i++; | |
| } | |
| } | |
| const UNHANDLED_CONSTRUCT = 0; | |
| const CFA_ERROR = 1; | |
| // number, string -> Error | |
| // returns an Error w/ a "code" property, so that DrJS can classify errors | |
| function errorWithCode(code, msg) { | |
| var e = new Error(msg); | |
| e.code = code; | |
| return e; | |
| } | |
| //////////////////////////////////////////////////////////////////////////////// | |
| //////////////////// PREPARE AST FOR FLOW ANALYSIS /////////////////////// | |
| //////////////////////////////////////////////////////////////////////////////// | |
| var jsparse = require('../../narcissus/lib/parser'); | |
| var Node = jsparse.Node; | |
| const DECLARED_FORM = jsparse.DECLARED_FORM; | |
| const STATEMENT_FORM = jsparse.STATEMENT_FORM; | |
| eval(require('../../narcissus/lib/definitions').consts); | |
| var print = console.log; | |
| var fs = require('fs'); | |
| function printf(fd, s) { fs.writeSync(fd, s, null, encoding='utf8'); } | |
| // count is used to generate a unique ID for each node in the AST. | |
| var count; | |
| function newCount() { return ++count; } | |
| // Use token_count to get fresh IDs for new non-terminals | |
| var token_count = (require('../../narcissus/lib/definitions').tokens).length; | |
| const DOT_PROTO = token_count++; | |
| const ARGUMENTS = token_count++; | |
| const PLUS_ASSIGN = token_count++; | |
| // Instead of a flat long case dispatch, arities create a tree-like dispatch. | |
| // Nodes are grouped in arities by how we recur down their structure. | |
| const NULLARY = [NULL, THIS, TRUE, FALSE, IDENTIFIER, NUMBER, STRING, REGEXP]; | |
| const UNARY = [DELETE, VOID, TYPEOF, NOT, BITWISE_NOT, UNARY_PLUS, UNARY_MINUS, | |
| NEW, INCREMENT, DECREMENT, DOT_PROTO, ARGUMENTS]; | |
| const BINARY = [BITWISE_OR, BITWISE_XOR, BITWISE_AND, EQ, NE, STRICT_EQ, | |
| STRICT_NE, LT, LE, GE, GT, INSTANCEOF, LSH, RSH, URSH, PLUS, | |
| MINUS, MUL, DIV, MOD, AND, OR, ASSIGN, INDEX, IN, DOT, | |
| PLUS_ASSIGN]; | |
| const N_ARY = [COMMA, ARRAY_INIT]; | |
| // expr node -> stm node | |
| function semiNode(n) { | |
| var sn = new Node(n.tokenizer, {type:SEMICOLON}); | |
| sn.expression = n; | |
| return sn; | |
| } | |
| // tokenizer, string -> identifier node | |
| function idNode(t, name) { | |
| var n = new Node(t, {type:IDENTIFIER}); | |
| n.name = n.value = name; | |
| return n; | |
| } | |
| var astSize; // calculated for statistics | |
| // node, optional string -> node | |
| // Does some cleanup on the input expression node in-place. | |
| // funName is used to name some anonymous funs using heuristics from the context | |
| function fixExp(n, funName) { | |
| var nt = n.type, ch = n.children; | |
| astSize++; | |
| function fixe(n, i, ch) { ch[i] = fixExp(n); } | |
| if (NULLARY.memq(nt)) { | |
| if (nt === IDENTIFIER) n.name = n.value; | |
| else if (nt === STRING) n.value += "-"; | |
| } | |
| else if (UNARY.memq(nt)) { | |
| if (nt === UNARY_MINUS && ch[0].type === NUMBER) { | |
| n.type = NUMBER; | |
| n.value = -ch[0].value; | |
| n.children = []; | |
| return n; | |
| } | |
| if (nt === NEW) { // unify NEW and NEW_WITH_ARGS | |
| n.type = NEW_WITH_ARGS; | |
| ch.push(new Node(n.tokenizer, {type:LIST, children:[]})); | |
| } | |
| ch[0] = fixExp(ch[0]); | |
| } | |
| else if (BINARY.memq(nt)) { | |
| if (nt === INDEX) { | |
| ch.forEach(fixe); | |
| if (ch[1].type === NUMBER) { | |
| ch[1].type = STRING; | |
| ch[1].value += "-"; | |
| } | |
| return n; | |
| } | |
| else if (nt === DOT) { | |
| var ch1 = ch[1]; | |
| // jsparse makes the 1st child of DOTs an ID b/c that's what is parsed. | |
| // For an evaluator it makes more sense to make the 1st child a string, | |
| // b/c functions that recur on DOTs won't try to look it up as a name. | |
| ch1.type = STRING; | |
| delete ch1.name; | |
| // DOT_PROTO has no new semantic meaning, it's an optimization so that we | |
| // dont check at every property access if the property name is "prototype" | |
| if (ch1.value === "prototype") n.type = DOT_PROTO; | |
| } | |
| else if (nt === ASSIGN && ch[0].assignOp === PLUS) | |
| n.type = PLUS_ASSIGN; | |
| else if (nt === ASSIGN && !ch[0].assignOp) { | |
| var n0 = ch[0]; | |
| ch[0] = fixExp(n0); | |
| if (n0.type === IDENTIFIER) | |
| funName = n0.name; | |
| else if (n0.type === DOT) { | |
| var fname = n0.children[1].value; | |
| funName = fname.substring(0, fname.length - 1); | |
| } | |
| ch[1] = fixExp(ch[1], funName); | |
| return n; | |
| } | |
| ch.forEach(fixe); | |
| } | |
| else if (nt === HOOK || N_ARY.memq(nt)) { | |
| // Uncomment this if Narcissus produces empty COMMA nodes. | |
| if (nt === COMMA && ch.length === 0) | |
| ch.push(new Node(n.tokenizer, {type:TRUE})); | |
| if (nt === ARRAY_INIT) { | |
| ch.forEach(function(kid, i, ch) { | |
| if (kid === null) ch[i] = idNode(n.tokenizer, "undefined"); | |
| }); | |
| } | |
| ch.forEach(fixe); | |
| } | |
| else if (nt === CALL || nt === NEW_WITH_ARGS) { | |
| ch[0] = fixExp(ch[0]); | |
| ch[1].children.forEach(fixe); | |
| } | |
| else if (nt === FUNCTION) { | |
| fixFun(n, funName); | |
| } | |
| else if (nt === OBJECT_INIT) { | |
| ch.forEach(function(prop) { | |
| if (prop.type === GETTER || prop.type === SETTER) { | |
| // FIXME: for now, just don't crash on getters/setters | |
| prop.children = [new Node(n.tokenizer, | |
| { type : STRING, value : prop.name + "-" }), | |
| new Node(n.tokenizer, {type:TRUE})]; | |
| } | |
| else { | |
| var pch = prop.children, pch0 = pch[0], pname = pch0.value; | |
| pch0.type = STRING; | |
| delete pch0.name; | |
| pch0.value += "-"; | |
| pch[1] = fixExp(pch[1], pname); | |
| } | |
| }); | |
| } | |
| else if (nt === LET_BLOCK) { | |
| n.varDecls.forEach(function(vd, i, vds) { | |
| vd.name = vd.value; | |
| vd.initializer && (vd.initializer = fixExp(vd.initializer, vd.value)); | |
| }); | |
| n.expression = fixExp(n.expression); | |
| } | |
| else if (nt === YIELD) { // FIXME: for now, just don't crash on yield | |
| n.type = TRUE; | |
| delete n.value | |
| } | |
| return n; | |
| } | |
| // node, optional string -> void | |
| function fixFun(n, funName) { | |
| var t = n.tokenizer; | |
| // replace name w/ a property fname which is an IDENTIFIER node. | |
| n.fname = idNode(t, n.name || funName); | |
| delete n.name; | |
| // turn the formals to nodes, I'll want to attach stuff to them later. | |
| n.params.forEach(function(p, i, ps) { ps[i] = idNode(t, p); }); | |
| // don't tag builtin funs, they never have RETURN when used as constructors. | |
| n.hasReturn = fixStm(n.body); | |
| } | |
| // Array of id nodes, tokenizer -> comma node | |
| // Convert variable initializations (coming from VAR,CONST,LET) to assignments. | |
| function initsToAssigns(inits, t) { | |
| var n, vdecl, a, i, len, ch; | |
| n = new Node(t, {type:COMMA}); | |
| ch = n.children; | |
| for (i = 0, len = inits.length; i < len; i++) { | |
| vdecl = inits[i]; | |
| if (vdecl.initializer) { | |
| a = new Node(vdecl.tokenizer, {type:ASSIGN}); | |
| a.children = [idNode(vdecl.tokenizer, vdecl.name), vdecl.initializer]; | |
| ch.push(a); | |
| } | |
| } | |
| // if no initialization values, push dummy node to avoid empty comma node. | |
| ch.length || ch.push(new Node(t, {type:TRUE})); | |
| return n; | |
| } | |
| // node, optional script node -> boolean | |
| // returns true iff n contains RETURN directly (not RETURNs of inner functions). | |
| function fixStm(n, parentScript) { | |
| var i, j, n2, ans1, ans2, ch = n.children; | |
| astSize++; | |
| switch (n.type) { | |
| case SCRIPT: | |
| case BLOCK: | |
| var ans1 = false, parscr = (n.type === SCRIPT) ? n : parentScript; | |
| i=0; | |
| while (i < ch.length) { | |
| n2 = ch[i]; | |
| switch (n2.type) { | |
| case VAR: | |
| case CONST: | |
| ch.splice(i++, 1, | |
| semiNode(fixExp(initsToAssigns(n2.children, n2.tokenizer)))); | |
| break; | |
| case SWITCH: | |
| if (n2.cases.length === 0) {// switch w/out branches becomes semi node | |
| n2.discriminant = fixExp(n2.discriminant); | |
| ch[i] = semiNode(n2.discriminant); | |
| } | |
| else | |
| ans1 = fixStm(n2, parscr) || ans1; | |
| i++; | |
| break; | |
| case FUNCTION: //rm functions from Script bodies, they're in funDecls | |
| fixFun(n2); | |
| // To handle funs in blocks, unsoundly add them to funDecls | |
| if (n2.functionForm === STATEMENT_FORM) parscr.funDecls.push(n2); | |
| ch.splice(i, 1); | |
| // The code below is for when we don't handle funs in blocks. | |
| // if (n2.functionForm === DECLARED_FORM) | |
| // ch.splice(i, 1); | |
| // else | |
| // ++i; | |
| break; | |
| case SEMICOLON: // remove empty SEMICOLON nodes | |
| if (n2.expression == null) { | |
| ch.splice(i, 1); | |
| break; | |
| } // o/w fall thru to fix the node | |
| default: | |
| ans1 = fixStm(n2, parscr) || ans1; | |
| i++; | |
| break; | |
| } | |
| } | |
| return ans1; | |
| case LET: // let definitions | |
| n.children.forEach(function(vd) { | |
| vd.name = vd.value; | |
| vd.initializer && (vd.initializer = fixExp(vd.initializer)); | |
| }); | |
| return false; | |
| case LET_BLOCK: | |
| n.varDecls.forEach(function(vd) { | |
| vd.name = vd.value; | |
| vd.initializer && (vd.initializer = fixExp(vd.initializer)); | |
| }); | |
| if (n.expression) { // let-exp in stm context | |
| n.expression = fixExp(n.expression); | |
| n.block = semiNode(n.expression); | |
| delete n.expression; | |
| return false; | |
| } | |
| else // let-stm | |
| return fixStm(n.block, parentScript); | |
| case BREAK: case CONTINUE: case DEBUGGER: | |
| n.type = BLOCK; | |
| n.varDecls = []; | |
| n.children = []; | |
| return false; | |
| case SEMICOLON: | |
| if (!n.expression) | |
| n.expression = new Node(n.tokenizer, {type:TRUE}); | |
| else | |
| n.expression = fixExp(n.expression); //n.expression can't be null | |
| return false; | |
| case IF: | |
| n.condition = fixExp(n.condition); | |
| ans1 = fixStm(n.thenPart, parentScript); | |
| return (n.elsePart && fixStm(n.elsePart, parentScript)) || ans1; | |
| case SWITCH: | |
| n.discriminant = fixExp(n.discriminant); | |
| ans1 = false; | |
| n.cases.forEach( function(branch) { | |
| branch.caseLabel && (branch.caseLabel = fixExp(branch.caseLabel)); | |
| ans2 = fixStm(branch.statements, parentScript); | |
| ans1 = ans1 || ans2; | |
| }); | |
| return ans1; | |
| case FOR: | |
| n2 = n.setup; | |
| if (n2) { | |
| if (n2.type === VAR || n2.type === CONST) | |
| n.setup = fixExp(initsToAssigns(n2.children, n2.tokenizer)); | |
| else | |
| n.setup = fixExp(n2); | |
| } | |
| n.condition && (n.condition = fixExp(n.condition)); | |
| n.update && (n.update = fixExp(n.update)); | |
| return fixStm(n.body, parentScript); | |
| case FOR_IN: | |
| n.iterator = fixExp(n.iterator); | |
| n.object = fixExp(n.object); | |
| if (n.body.type !== BLOCK) { | |
| var fibody = new Node(n.tokenizer, {type:BLOCK}); | |
| fibody.children = [n.body]; | |
| n.body = fibody; | |
| } | |
| return fixStm(n.body, parentScript); | |
| case WHILE: | |
| case DO: | |
| n.condition = fixExp(n.condition); | |
| return fixStm(n.body, parentScript); | |
| case TRY: // turn the varName of each catch-clause to a node called exvar | |
| ans1 = fixStm(n.tryBlock, parentScript); | |
| n.catchClauses.forEach(function(clause) { | |
| clause.exvar = idNode(clause.tokenizer, clause.varName); | |
| clause.guard && (clause.guard = fixExp(clause.guard)); | |
| ans2 = fixStm(clause.block, parentScript); | |
| ans1 = ans1 || ans2; | |
| }); | |
| return (n.finallyBlock && fixStm(n.finallyBlock, parentScript)) || ans1; | |
| case THROW: | |
| n.exception = fixExp(n.exception); | |
| return false; | |
| case RETURN: | |
| if (n.value === undefined) | |
| n.value = idNode(n.tokenizer, "undefined"); | |
| else | |
| n.value = fixExp(n.value); | |
| return true; | |
| case VAR: case CONST: // very rare to not appear in a block or script. | |
| n.type = SEMICOLON; | |
| n.expression = fixExp(initsToAssigns(n.children, n.tokenizer)); | |
| ch = []; | |
| return false; | |
| case FUNCTION: | |
| n2 = new Node(n.tokenizer, {type:FUNCTION}); | |
| n2.name = n.name; delete n.name; | |
| n2.params = n.params; delete n.params; | |
| n2.functionForm = n.functionForm; delete n.functionForm; | |
| n2.body = n.body; delete n.body; | |
| fixFun(n2); | |
| // To handle funs not in scripts, unsoundly add them to funDecls | |
| parentScript.funDecls.push(n2); | |
| n.type = SEMICOLON; | |
| n.expression = new Node(n.tokenizer, {type:TRUE}); | |
| return false; | |
| case WITH: | |
| n.object = fixExp(n.object); | |
| return fixStm(n.body, parentScript); | |
| case LABEL: //replace LABEL nodes by their statement (forget labels) | |
| n.type = BLOCK; | |
| n.varDecls = []; | |
| n.children = [n.statement]; | |
| delete n.statement; | |
| return fixStm(n.children[0], parentScript); | |
| default: | |
| throw errorWithCode(UNHANDLED_CONSTRUCT, | |
| "fixStm: " + n.type + ", line " + n.lineno); | |
| } | |
| } | |
| // Invariants of the AST after fixStm: | |
| // - no NEW nodes, they became NEW_WITH_ARGS | |
| // - the formals of functions are nodes, not strings | |
| // - functions have a property fname which is an IDENTIFIER node, name deleted | |
| // - no VAR and CONST nodes, they've become semicolon comma nodes. | |
| // - no BREAK and CONTINUE nodes. | |
| // Unfortunately, this isn't independent of exceptions. | |
| // If a finally-block breaks or continues, the exception isn't propagated. | |
| // I will falsely propagate it (still sound, just more approximate). | |
| // - no LABEL nodes | |
| // - function nodes only in blocks, not in scripts | |
| // - no empty SEMICOLON nodes | |
| // - no switches w/out branches | |
| // - each catch clause has a property exvar which is an IDENTIFIER node | |
| // - all returns have .value (the ones that didn't, got an undefined) | |
| // - the lhs of a property initializer of an OBJECT_INIT is always a string | |
| // - the property names in DOT and OBJECT_INIT end with a dash. | |
| // - there is no DOT whose 2nd arg is "prototype", they've become NODE_PROTOs. | |
| // - the second kid of DOT is always a STRING, not an IDENTIFIER. | |
| // - value of a NUMBER can be negative (UNARY_MINUS of constant became NUMBER). | |
| // - the operator += has its own non-terminal, PLUS_ASSIGN. | |
| // - each function node has a property hasReturn to show if it uses RETURN. | |
| // - there is no INDEX whose 2nd arg has type NUMBER, they've become STRINGs. | |
| // - The body of a LET_BLOCK in statement context is always a statement. | |
| // - FUNCTIONs in BLOCKs are added to funDecls of enclosing SCRIPT. | |
| // - Array literals don't have holes (null children) in them. | |
| // - The body of FOR_IN is always a BLOCK. | |
| function walkASTgenerator() { | |
| var jt = new Array(token_count); // jump table | |
| var o = {}; // dummy object for the calls to apply | |
| function recur(n) { return jt[n.type].apply(o, arguments); } | |
| function override(tt, fun) { jt[tt] = fun; } | |
| function _nokids() {} | |
| NULLARY.forEach(function(tt) { jt[tt] = _nokids; }); | |
| function _haskids() { | |
| var args = arguments, ch = args[0].children; | |
| ch.forEach(function(kid) { | |
| args[0] = kid; | |
| recur.apply(o, args); | |
| }); | |
| } | |
| [HOOK].concat(N_ARY, UNARY, BINARY).forEach(function(tt) {jt[tt]=_haskids;}); | |
| jt[CALL] = jt[NEW_WITH_ARGS] = function() { | |
| var args = arguments, n = args[0], ch = n.children; | |
| args[0] = ch[0]; | |
| recur.apply(o, args); | |
| ch[1].children.forEach(function(kid) { | |
| args[0] = kid; | |
| recur.apply(o, args); | |
| }); | |
| }; | |
| jt[OBJECT_INIT] = function() { | |
| var args = arguments; | |
| args[0].children.forEach(function(kid) { | |
| var ch = kid.children; | |
| args[0] = ch[0]; | |
| recur.apply(o, args); | |
| args[0] = ch[1]; | |
| recur.apply(o, args); | |
| }); | |
| }; | |
| jt[FUNCTION] = function() { | |
| arguments[0] = arguments[0].body; | |
| recur.apply(o, arguments); | |
| }; | |
| jt[SCRIPT] = function() { | |
| var args = arguments, n = args[0]; | |
| n.funDecls.forEach(function(fd) { | |
| args[0] = fd; | |
| recur.apply(o, args); | |
| }); | |
| n.children.forEach(function(kid) { | |
| args[0] = kid; | |
| recur.apply(o, args); | |
| }); | |
| }; | |
| jt[BLOCK] = function() { | |
| var args = arguments; | |
| args[0].children.forEach(function(kid) { | |
| args[0] = kid; | |
| recur.apply(o, args); | |
| }); | |
| }; | |
| jt[SEMICOLON] = function() { | |
| arguments[0] = arguments[0].expression; | |
| recur.apply(o, arguments); | |
| }; | |
| jt[IF] = function() { | |
| var n = arguments[0]; | |
| arguments[0] = n.condition; | |
| recur.apply(o, arguments); | |
| arguments[0] = n.thenPart; | |
| recur.apply(o, arguments); | |
| if (n.elsePart) { | |
| arguments[0] = n.elsePart; | |
| recur.apply(o, arguments); | |
| } | |
| }; | |
| jt[SWITCH] = function() { | |
| var args = arguments, n = args[0]; | |
| args[0] = n.discriminant; | |
| recur.apply(o, args); | |
| n.cases.forEach(function(branch) { | |
| if (branch.caseLabel) { | |
| args[0] = branch.caseLabel; | |
| recur.apply(o, args); | |
| } | |
| args[0] = branch.statements; | |
| recur.apply(o, args); | |
| }); | |
| }; | |
| jt[FOR] = function() { | |
| var n = arguments[0]; | |
| if (n.setup) { | |
| arguments[0] = n.setup; | |
| recur.apply(o, arguments); | |
| } | |
| if (n.condition) { | |
| arguments[0] = n.condition; | |
| recur.apply(o, arguments); | |
| } | |
| if (n.update) { | |
| arguments[0] = n.update; | |
| recur.apply(o, arguments); | |
| } | |
| arguments[0] = n.body; | |
| recur.apply(o, arguments); | |
| }; | |
| jt[FOR_IN] = function() { | |
| var n = arguments[0]; | |
| arguments[0] = n.iterator; | |
| recur.apply(o, arguments); | |
| arguments[0] = n.object; | |
| recur.apply(o, arguments); | |
| arguments[0] = n.body; | |
| recur.apply(o, arguments); | |
| }; | |
| jt[WHILE] = jt[DO] = function() { | |
| var n = arguments[0]; | |
| arguments[0] = n.condition; | |
| recur.apply(o, arguments); | |
| arguments[0] = n.body; | |
| recur.apply(o, arguments); | |
| }; | |
| jt[TRY] = function() { | |
| var args = arguments, n = args[0]; | |
| args[0] = n.tryBlock; | |
| recur.apply(o, args); | |
| n.catchClauses.forEach(function(clause) { | |
| if (clause.guard) { | |
| args[0] = clause.guard; | |
| recur.apply(o, args); | |
| } | |
| args[0] = clause.block; | |
| recur.apply(o, args); | |
| }); | |
| if (n.finallyBlock) { | |
| args[0] = n.finallyBlock; | |
| recur.apply(o, args); | |
| } | |
| }; | |
| jt[THROW] = function() { | |
| arguments[0] = arguments[0].exception; | |
| recur.apply(o, arguments); | |
| }; | |
| jt[RETURN] = function() { | |
| var n = arguments[0]; | |
| if (n.value) { | |
| arguments[0] = n.value; | |
| recur.apply(o, arguments); | |
| } | |
| }; | |
| jt[WITH] = function() { | |
| var n = arguments[0]; | |
| arguments[0] = n.object; | |
| recur.apply(o, arguments); | |
| arguments[0] = n.body; | |
| recur.apply(o, arguments); | |
| }; | |
| jt[LET_BLOCK] = function() { | |
| var args = arguments, n = args[0]; | |
| n.varDecls.forEach(function(vd) { | |
| (args[0] = vd.initializer) && recur.apply(o, args); | |
| }); | |
| (args[0] = n.expression) || (args[0] = n.block); | |
| recur.apply(o, args); | |
| }; | |
| jt[LET] = function() { | |
| var args = arguments; | |
| args[0].children.forEach(function(vd) { | |
| (args[0] = vd.initializer) && recur.apply(o, args); | |
| }); | |
| }; | |
| return { walkAST: recur, override: override}; | |
| } | |
| // node -> void | |
| // Adds an "addr" property to nodes, which is a number unique for each node. | |
| var labelAST, labelAST_override; | |
| (function() { | |
| var walkerobj = walkASTgenerator(), override = walkerobj.override; | |
| labelAST = walkerobj.walkAST; | |
| labelAST_override = override; | |
| override(ARRAY_INIT, function(n) { | |
| n.addr = newCount(); | |
| n.children.forEach(labelAST); | |
| }); | |
| override(NEW_WITH_ARGS, function(n) { | |
| n.addr = newCount(); | |
| labelAST(n.children[0]); | |
| n.children[1].children.forEach(labelAST); | |
| }); | |
| override(REGEXP, function(n) { n.addr = newCount(); }); | |
| override(OBJECT_INIT, function(n) { | |
| n.addr = newCount(); | |
| n.children.forEach(function(prop) { | |
| labelAST(prop.children[0]); | |
| labelAST(prop.children[1]); | |
| }); | |
| }); | |
| override(TRY, function(n) { | |
| labelAST(n.tryBlock); | |
| n.catchClauses.forEach(function(c) { | |
| c.exvar.addr = newCount(); | |
| c.guard && labelAST(c.guard); | |
| labelAST(c.block); | |
| }); | |
| n.finallyBlock && labelAST(n.finallyBlock); | |
| }); | |
| override(SCRIPT, function(n) { | |
| n.varDecls.forEach(function(vd) {vd.addr = newCount();}); | |
| n.funDecls.forEach(labelAST); | |
| n.children.forEach(labelAST); | |
| }); | |
| override(FUNCTION, function _function(n) { | |
| n.addr = newCount(); | |
| n.defaultProtoAddr = newCount(); | |
| n.fname.addr = newCount(); | |
| n.params.forEach(function(p) { p.addr = newCount(); }); | |
| labelAST(n.body); | |
| }); | |
| })(); | |
| // Node, number, Array of id nodes -> void | |
| // WITH node, address of the fresh variable, bound variables | |
| // After desugarWith is executed, there are no WITHs in the AST. | |
| var desugarWith; | |
| // FIXME: the desugaring handles nested WITHs incorrectly. | |
| (function () { | |
| var walkerobj = walkASTgenerator(), override = walkerobj.override; | |
| desugarWith = walkerobj.walkAST; | |
| function withIdNode(t, addr) { | |
| var n = idNode(t, "internalVar: with"); | |
| n.addr = addr; | |
| return n; | |
| } | |
| override(SCRIPT, function(n, withAddr, boundvars) { | |
| n.funDecls.forEach(function(fd) { desugarWith(fd, withAddr, boundvars); }); | |
| var blen = boundvars.length; | |
| Array.prototype.push.apply(boundvars, n.varDecls); | |
| n.children.forEach(function(stm) { | |
| desugarWith(stm, withAddr, boundvars, n.varDecls); | |
| }); | |
| boundvars.splice(blen, boundvars.length); | |
| }); | |
| override(FUNCTION, function(n, withAddr, boundvars) { | |
| var blen = boundvars.length; | |
| Array.prototype.push.apply(boundvars, n.params); | |
| desugarWith(n.body, withAddr, boundvars); | |
| boundvars.splice(blen, boundvars.length); | |
| }); | |
| // Turn to a block with two statements. | |
| // The 4th argument is the varDecls of the innermost enclosing SCRIPT, so that | |
| // the fresh variable of the WITH can be added there. | |
| override(WITH, function(n, withAddr, boundvars, vardecls) { | |
| var newaddr = newCount(), t = n.tokenizer; | |
| var freshvar = withIdNode(t, newaddr), assgn; | |
| vardecls.push(freshvar); | |
| assgn = new Node(t, {type:ASSIGN}); | |
| assgn.children = [freshvar, n.object]; | |
| desugarWith(n.body, newaddr, [], vardecls); | |
| n.type = BLOCK; | |
| n.varDecls = []; | |
| n.children = [semiNode(assgn), n.body]; | |
| delete n.object; | |
| delete n.body; | |
| }); | |
| // Add the CATCH variable to the bound variables | |
| override(TRY, function(n, withAddr, boundvars, vardecls) { | |
| desugarWith(n.tryBlock, withAddr, boundvars, vardecls); | |
| n.catchClauses.forEach(function(clause) { | |
| boundvars.push(clause.exvar); | |
| clause.guard && desugarWith(clause.guard, withAddr, boundvars); | |
| desugarWith(clause.block, withAddr, boundvars, vardecls); | |
| boundvars.pop(); | |
| }); | |
| n.finallyBlock && desugarWith(n.finallyBlock,withAddr,boundvars,vardecls); | |
| }); | |
| // May change n to an IF node | |
| override(FOR_IN, function(n, withAddr, boundvars, vardecls) { | |
| var it = n.iterator, it, obj = n.object, b = n.body; | |
| if (it.type === IDENTIFIER) | |
| // (Temporary) Copy to avoid sharing introduced by desugaring. | |
| n.iterator = it = idNode(it.tokenizer, it.value); | |
| desugarWith(obj, withAddr, boundvars); | |
| desugarWith(b, withAddr, boundvars, vardecls); | |
| if (it.type !== IDENTIFIER) { | |
| desugarWith(it, withAddr, boundvars); | |
| return; | |
| } | |
| if (_makeHook(n, it, withAddr, boundvars)) { | |
| var t = n.tokenizer, ch = n.children; | |
| n.type = IF; | |
| n.children = []; | |
| n.condition = ch[0]; | |
| var forinn = new Node(t, {type:FOR_IN}); | |
| forinn.iterator = ch[1]; | |
| forinn.object = obj; | |
| forinn.body = b; | |
| n.thenPart = forinn; | |
| forinn = new Node(t, {type:FOR_IN}); | |
| forinn.iterator = ch[2]; | |
| forinn.object = obj; | |
| // The body b must be cloned o/w markConts will overwrite the conts of the | |
| // true branch when it processes the false branch. | |
| forinn.body = b; | |
| n.elsePart = forinn; | |
| } | |
| }); | |
| // Change the 1st arg to a HOOK node. | |
| // n may be an ID node, but it doesn't appear in varDecls b/c it's an rvalue, | |
| // it doesn't come from a VAR, so it can be mutated safely. | |
| // Returns true iff it edits the node. | |
| function _makeHook(n, idn, withAddr, boundvars) { | |
| var t = idn.tokenizer, name = idn.name; | |
| if (!withAddr) return; | |
| if (boundvars.member(function(bv) { return bv.name === name; })) return; | |
| var inn = new Node(t, {type : IN}); | |
| inn.children = [new Node(t, {type : STRING, value : name + "-"}), | |
| withIdNode(t, withAddr)]; | |
| var dotn = new Node(t, {type: DOT}); | |
| dotn.children = [withIdNode(t, withAddr), | |
| new Node(t, {type:STRING, value : name + "-"})]; | |
| n.type = HOOK; | |
| n.children = [inn, dotn, idNode(t, name)]; | |
| return true; | |
| } | |
| override(IDENTIFIER, function(n, withAddr, boundvars) { | |
| _makeHook(n, n, withAddr, boundvars); | |
| }); | |
| // May change n to a HOOK node | |
| function _assign(n, withAddr, boundvars) { | |
| var t = n.tokenizer, type = n.type, lval = n.children[0], | |
| aop = lval.assignOp, rval = n.children[1]; | |
| desugarWith(rval, withAddr, boundvars); | |
| if (lval.type !== IDENTIFIER) { | |
| desugarWith(lval, withAddr, boundvars); | |
| return; | |
| } | |
| if (_makeHook(n, lval, withAddr, boundvars)) { | |
| var branch = n.children[1], assgn = new Node(t, {type:type}); | |
| branch.assignOp = aop; | |
| assgn.children = [branch, rval]; | |
| n.children[1] = assgn; | |
| branch = n.children[2], assgn = new Node(t, {type:type}); | |
| branch.assignOp = aop; | |
| assgn.children = [branch, rval]; | |
| n.children[2] = assgn; | |
| } | |
| } | |
| override(ASSIGN, _assign); | |
| override(PLUS_ASSIGN, _assign); | |
| })(); | |
| // Node, Map from strings to id nodes, Array of id nodes -> void | |
| // Rename variables according to the input substitution map | |
| var subst; | |
| (function () { | |
| var walkerobj = walkASTgenerator(), override = walkerobj.override; | |
| subst = walkerobj.walkAST; | |
| // The only substitution happens here. All other cases are binders, so they | |
| // update boundvars to prevent erroneous substitutions. | |
| override(IDENTIFIER, function(n, smap, boundvars) { | |
| var vname = n.value; | |
| if (boundvars.member(function(bv) { return bv.value === vname; })) return; | |
| var newvar = smap[vname]; | |
| if (newvar) { | |
| n.name = n.value = newvar.value; | |
| n.addr = newvar.addr; | |
| } | |
| }); | |
| override(FUNCTION, function(n, smap, boundvars) { | |
| var blen = boundvars.length; | |
| boundvars.push(n.fname); | |
| Array.prototype.push.apply(boundvars, n.params); | |
| subst(n.body, smap, boundvars); | |
| boundvars.splice(blen, boundvars.length); | |
| }); | |
| function _block(vds, fds, stms, smap, boundvars) { | |
| var blen = boundvars.length; | |
| Array.prototype.push.apply(boundvars, vds); | |
| if (fds) { | |
| fds.forEach(function(fd) { boundvars.push(fd.fname); }); | |
| fds.forEach(function(fd) { subst(fd, smap, boundvars); }); | |
| } | |
| stms.forEach(function(stm) { subst(stm, smap, boundvars); }); | |
| boundvars.splice(blen, boundvars.length); | |
| } | |
| override(SCRIPT, function(n, smap, boundvars) { | |
| _block(n.varDecls, n.funDecls, n.children, smap, boundvars); | |
| }); | |
| override(BLOCK, function(n, smap, boundvars) { | |
| _block(n.varDecls, undefined, n.children, smap, boundvars); | |
| }); | |
| override(FOR, function(n, smap, boundvars) { | |
| var blen = boundvars.length; | |
| n.varDecls && Array.prototype.push.apply(boundvars, n.varDecls); | |
| n.setup && subst(n.setup, smap, boundvars); | |
| n.condition && subst(n.condition, smap, boundvars); | |
| n.update && subst(n.update, smap, boundvars); | |
| subst(n.body, smap, boundvars); | |
| boundvars.splice(blen, boundvars.length); | |
| }); | |
| override(LET_BLOCK, function(n, smap, boundvars) { | |
| var vds = n.varDecls, stms; | |
| if (n.expression) | |
| stms = [semiNode(n.expression)]; | |
| else { | |
| vds.concat(n.block.varDecls); | |
| stms = n.block.children; | |
| } | |
| _block(vds, undefined, stms, smap, boundvars); | |
| }); | |
| override(TRY, function(n, smap, boundvars) { | |
| subst(n.tryBlock, smap, boundvars); | |
| n.catchClauses.forEach(function(clause) { | |
| boundvars.push(clause.exvar); | |
| clause.guard && subst(clause.guard, smap, boundvars); | |
| subst(clause.block, smap, boundvars); | |
| boundvars.pop(); | |
| }); | |
| n.finallyBlock && subst(n.finallyBlock, smap, boundvars); | |
| }); | |
| })(); | |
| // Must happen after desugaring of WITH, o/w when I create fresh vars for LETs, | |
| // I may subst erroneously in the body of a WITH. | |
| // After desugarLet is executed, there are no LETs or LET_BLOCKs in the AST. | |
| var desugarLet; | |
| (function () { | |
| var walkerobj = walkASTgenerator(), override = walkerobj.override; | |
| desugarLet = walkerobj.walkAST; | |
| // Array of id nodes, Array of id nodes, tokenizer -> | |
| // { smap : Substitution map, comman : comma node } | |
| function letInitsToAssigns(vds, scriptVds, t) { | |
| var smap = {}, newaddr, freshvar, comman; | |
| for (var i = 0, len = vds.length; i < len; i++) { | |
| newaddr = newCount(); | |
| // New vars must have different names for tagVarRefs to work. | |
| freshvar = idNode(t, "internalVar: let" + newaddr); | |
| freshvar.addr = newaddr; | |
| smap[vds[i].value] = freshvar; | |
| scriptVds.push(freshvar); | |
| } | |
| comman = initsToAssigns(vds, t); | |
| subst(comman, smap, []); | |
| desugarLet(comman, scriptVds); | |
| return {smap : smap, comman : comman}; | |
| } | |
| override(LET_BLOCK, function(n, scriptVds) { | |
| var tmp = letInitsToAssigns(n.varDecls, scriptVds, n.tokenizer), | |
| smap = tmp.smap, | |
| comman = tmp.comman; | |
| delete n.variables; | |
| var body; | |
| if (body = n.expression) { | |
| subst(body, smap, []); | |
| desugarLet(body, scriptVds); | |
| n.type = COMMA; | |
| n.children = comman.children; | |
| n.children.push(body); | |
| delete n.expression; | |
| } | |
| else if (body = n.block) { | |
| subst(body, smap, []); | |
| desugarLet(body, scriptVds, body); | |
| n.type = BLOCK; | |
| n.children = body.children; | |
| n.children.unshift(semiNode(comman)); | |
| delete n.block; | |
| n.varDecls = []; | |
| } | |
| }); | |
| override(BLOCK, function(n, scriptVds) { | |
| n.children.forEach(function(stm) { desugarLet(stm, scriptVds, n); }); | |
| }); | |
| override(FOR, function(n, scriptVds) { | |
| n.setup && desugarLet(n.setup, scriptVds, n); // LET can appear in setup | |
| n.condition && desugarLet(n.condition, scriptVds); | |
| n.update && desugarLet(n.update, scriptVds); | |
| desugarLet(n.body, scriptVds); | |
| }); | |
| // LET definitions can appear in SCRIPTs or BLOCKs | |
| override(LET, function(n, scriptVds, innerBlock) { | |
| var tmp = letInitsToAssigns(n.children, scriptVds, n.tokenizer), | |
| smap = tmp.smap, | |
| comman = tmp.comman; | |
| // Call subst on the sub-nodes of innerBlock directly, to avoid binding vars | |
| // that must be substituted. | |
| if (innerBlock.type === FOR) { | |
| subst(innerBlock.setup, smap, []); | |
| innerBlock.condition && subst(innerBlock.condition, smap, []); | |
| innerBlock.update && subst(innerBlock.update, smap, []); | |
| subst(innerBlock.body, smap, []); | |
| n.type = COMMA; | |
| n.children = comman.children; | |
| } | |
| else { | |
| innerBlock.children.forEach(function(stm) { subst(stm, smap, []); }); | |
| n.type = SEMICOLON; | |
| n.expression = comman; | |
| delete n.children; | |
| } | |
| }); | |
| override(SCRIPT, function(n) { | |
| var vds = n.varDecls; | |
| n.funDecls.forEach(desugarLet); | |
| n.children.forEach(function(stm) { desugarLet(stm, vds, n); }); | |
| }); | |
| })(); | |
| const STACK = 0, HEAP = 1; | |
| // helper function for the IDENTIFIER case of tagVarRefs | |
| function tagVarRefsId(classifyEvents) { | |
| return function(n, innerscope, otherscopes) { | |
| var boundvar, varname = n.name; | |
| // search var in innermost scope | |
| for (var i = innerscope.length - 1; i >= 0; i--) { | |
| boundvar = innerscope[i]; | |
| if (boundvar.name === varname) { | |
| //print("stack ref: " + varname); | |
| n.kind = STACK; | |
| // if boundvar is a heap var and some of its heap refs get mutated, | |
| // we may need to update bindings in frames during the cfa. | |
| n.addr = boundvar.addr; | |
| return; | |
| } | |
| } | |
| // search var in other scopes | |
| for (var i = otherscopes.length - 1; i >= 0; i--) { | |
| boundvar = otherscopes[i]; | |
| if (boundvar.name === varname) { | |
| // print("heap ref: " + varname); | |
| n.kind = boundvar.kind = HEAP; | |
| n.addr = boundvar.addr; | |
| flags[boundvar.addr] = true; | |
| return; | |
| } | |
| } | |
| // var has no binder in the program | |
| if (commonJSmode && varname === "exports") { | |
| n.kind = HEAP; | |
| n.addr = exports_object_av_addr; | |
| var p = arguments[3]; // exported property name passed as extra arg | |
| if (p && p.type === STRING) | |
| exports_object.lines[p.value.slice(0, -1)] = p.lineno; | |
| return; | |
| } | |
| //print("global: " + varname + " :: " + n.lineno); | |
| n.type = DOT; | |
| var nthis = idNode({lineno: n.lineno}, "internalVar: global object"); | |
| nthis.kind = HEAP; | |
| if (classifyEvents && varname === "addEventListener") | |
| nthis.addr = chrome_obj_av_addr; | |
| else | |
| nthis.addr = global_object_av_addr; | |
| n.children = [nthis, new Node({}, {type:STRING, value:n.name + "-"})]; | |
| }; | |
| } | |
| // node, array of id nodes, array of id nodes -> void | |
| // Classify variable references as either stack or heap references. | |
| var tagVarRefs, tagVarRefs_override; | |
| (function() { | |
| var walkerobj = walkASTgenerator(), override = walkerobj.override; | |
| tagVarRefs = walkerobj.walkAST; | |
| tagVarRefs_override = override; | |
| override(DOT, function(n, innerscope, otherscopes) { | |
| var ch = n.children; | |
| var n0 = ch[0]; | |
| // don't classify property names | |
| if (commonJSmode && n0.type === IDENTIFIER && n0.name === "exports") | |
| tagVarRefs(n0, innerscope, otherscopes, ch[1]); | |
| else | |
| tagVarRefs(n0, innerscope, otherscopes); | |
| }); | |
| override(INDEX, function(n, innerscope, otherscopes) { | |
| var ch = n.children, n0 = ch[0], shadowed = false; | |
| // use new non-terminal only if "arguments" refers to the arguments array | |
| if (n0.type === IDENTIFIER && n0.name === "arguments") { | |
| for (var i = innerscope.length - 1; i >= 0; i--) | |
| if (innerscope[i].name === "arguments") { | |
| shadowed = true; | |
| break; | |
| } | |
| if (!shadowed) { | |
| n.type = ARGUMENTS; | |
| n.arguments = innerscope; //hack: innerscope is params (maybe extended) | |
| ch[0] = ch[1]; | |
| ch.splice(1, 1); | |
| // undo the changes made for INDEX nodes only in fixExp | |
| if (ch[0].type === STRING && propIsNumeric(ch[0].value)) { | |
| ch[0].type = NUMBER; | |
| ch[0].value = ch[0].value.slice(0, -1) - 0; | |
| } | |
| } | |
| } | |
| ch.forEach(function(kid) { tagVarRefs(kid, innerscope, otherscopes); }); | |
| }); | |
| override(IDENTIFIER, tagVarRefsId(false)); | |
| override(FUNCTION, function(n, innerscope, otherscopes) { | |
| var fn = n.fname, len, params = n.params; | |
| len = otherscopes.length; | |
| // extend otherscopes | |
| Array.prototype.push.apply(otherscopes, innerscope); | |
| // fun name is visible in the body & not a heap ref, add it to scope | |
| params.push(fn); | |
| tagVarRefs(n.body, params, otherscopes); | |
| params.pop(); | |
| if (fn.kind !== HEAP) fn.kind = STACK; | |
| params.forEach(function(p) {if (p.kind !== HEAP) p.kind=STACK;}); | |
| // trim otherscopes | |
| otherscopes.splice(len, innerscope.length); | |
| }); | |
| override(SCRIPT, function(n, innerscope, otherscopes) { | |
| var i, j, len, vdecl, vdecls = n.varDecls, fdecl, fdecls = n.funDecls; | |
| // extend inner scope | |
| j = innerscope.length; | |
| Array.prototype.push.apply(innerscope, vdecls); | |
| fdecls.forEach(function(fd) { innerscope.push(fd.fname); }); | |
| // tag refs in fun decls | |
| fdecls.forEach(function(fd) { tagVarRefs(fd, innerscope, otherscopes); }); | |
| // tag the var refs in the body | |
| var as = arguments; | |
| n.children.forEach(function(stm) {tagVarRefs(stm,innerscope,otherscopes);}); | |
| // tag formals | |
| vdecls.forEach(function(vd) { | |
| // for toplevel vars, assigning flags causes the Aval`s to be recorded | |
| // in the heap. After the analysis, we use that for ctags. | |
| if (as[3] === "toplevel") flags[vd.addr] = true; | |
| if (vd.kind !== HEAP) vd.kind = STACK; | |
| }); | |
| fdecls.forEach(function(fd) { if (fd.kind !== HEAP) fd.kind = STACK; }); | |
| //trim inner scope | |
| innerscope.splice(j, vdecls.length + fdecls.length); | |
| }); | |
| override(TRY, function(n, innerscope, otherscopes) { | |
| tagVarRefs(n.tryBlock, innerscope, otherscopes); | |
| n.catchClauses.forEach(function(clause) { | |
| var xv = clause.exvar; | |
| innerscope.push(xv); | |
| clause.guard && | |
| tagVarRefs(clause.guard, innerscope, otherscopes); | |
| tagVarRefs(clause.block, innerscope, otherscopes); | |
| innerscope.pop(); | |
| if (xv.kind !== HEAP) xv.kind = STACK; | |
| }); | |
| n.finallyBlock && tagVarRefs(n.finallyBlock, innerscope, otherscopes); | |
| }); | |
| })(); | |
| // node, node, node -> void | |
| // For every node N in the AST, add refs from N to the node that is normally | |
| // exec'd after N and to the node that is exec'd if N throws an exception. | |
| var markConts; | |
| (function() { | |
| var walkerobj = walkASTgenerator(), override = walkerobj.override; | |
| markConts = walkerobj.walkAST; | |
| function _fun(n) { markConts(n.body, undefined, undefined); } | |
| override(FUNCTION, _fun); | |
| function _block(n, kreg, kexc) { | |
| var ch = n.children, i, len; | |
| n.kexc = kexc; | |
| len = ch.length; | |
| if (len === 0) | |
| n.kreg = kreg; | |
| else { | |
| n.kreg = ch[0]; | |
| len--; | |
| for (i=0; i < len; i++) markConts(ch[i], ch[i+1], kexc); | |
| markConts(ch[len], kreg, kexc); | |
| } | |
| } | |
| override(BLOCK, _block); | |
| override(SCRIPT, function(n, kreg, kexc) { | |
| n.funDecls.forEach(_fun); | |
| _block(n, kreg, kexc); | |
| }); | |
| override(SEMICOLON, function(n, kreg, kexc) { | |
| n.kreg = kreg; | |
| n.kexc = kexc; | |
| markConts(n.expression); | |
| }); | |
| // normally, return & throw don't use their kreg. But this analysis allows | |
| // more permissive control flow, to be faster. | |
| override(THROW, function(n, kreg, kexc) { | |
| n.kreg = kreg; | |
| n.kexc = kexc; | |
| markConts(n.exception); | |
| }); | |
| override(RETURN, function(n, kreg, kexc) { | |
| n.kreg = kreg; | |
| n.kexc = kexc; | |
| markConts(n.value); | |
| }); | |
| override(IF, function(n, kreg, kexc) { | |
| var thenp = n.thenPart, elsep = n.elsePart, condStm; | |
| condStm = semiNode(n.condition); | |
| n.kreg = condStm; // first run the test | |
| n.kexc = kexc; | |
| markConts(condStm, thenp, kexc); // ignore result & run the true branch | |
| markConts(thenp, elsep || kreg, kexc); // then run the false branch | |
| elsep && markConts(elsep, kreg, kexc); | |
| }); | |
| function markContsCase(n, kreg, kexc) { | |
| var clabel = n.caseLabel, clabelStm, stms = n.statements; | |
| n.kexc = kexc; | |
| if (clabel) { | |
| clabelStm = semiNode(clabel); | |
| n.kreg = clabelStm; | |
| markConts(clabelStm, stms, kexc); | |
| } | |
| else n.kreg = stms; | |
| markConts(stms, kreg, kexc); | |
| } | |
| override(SWITCH, function(n, kreg, kexc) { | |
| var cases = n.cases, discStm, i, len; | |
| discStm = semiNode(n.discriminant); | |
| n.kreg = discStm; // first run the discriminant, then all branches in order | |
| n.kexc = kexc; | |
| markConts(discStm, cases[0], kexc); | |
| for (i = 0, len = cases.length - 1; i < len; i++) //no empty switch, len >=0 | |
| markContsCase(cases[i], cases[i+1], kexc); | |
| markContsCase(cases[len], kreg, kexc); | |
| }); | |
| override(FOR, function(n, kreg, kexc) { | |
| var body = n.body; | |
| n.kexc = kexc; | |
| // Do setup, condition, body & update once. | |
| var setup = n.setup, setupStm, condition = n.condition, condStm; | |
| var update = n.update, updStm; | |
| n.kexc = kexc; | |
| if (!setup && !condition) | |
| n.kreg = body; | |
| else if (setup && !condition) { | |
| setupStm = semiNode(setup); | |
| n.kreg = setupStm; | |
| markConts(setupStm, body, kexc); | |
| } | |
| else {// condition exists | |
| condStm = semiNode(condition); | |
| markConts(condStm, body, kexc); | |
| if (setup) { | |
| setupStm = semiNode(setup); | |
| n.kreg = setupStm; | |
| markConts(setupStm, condStm, kexc); | |
| } | |
| else n.kreg = condStm; | |
| } | |
| if (update) { | |
| updStm = semiNode(update); | |
| markConts(body, updStm, kexc); | |
| markConts(updStm, kreg, kexc); | |
| } | |
| else markConts(body, kreg, kexc); | |
| }); | |
| override(FOR_IN, function(n, kreg, kexc) { | |
| var body = n.body; | |
| n.kexc = kexc; | |
| n.kreg = body; | |
| markConts(body, kreg, kexc); | |
| }); | |
| override(WHILE, function(n, kreg, kexc) { | |
| var condStm = semiNode(n.condition), body = n.body; | |
| n.kreg = condStm; | |
| n.kexc = kexc; | |
| markConts(condStm, body, kexc); | |
| markConts(body, kreg, kexc); | |
| }); | |
| override(DO, function(n, kreg, kexc) { | |
| var condStm = semiNode(n.condition), body = n.body; | |
| n.kreg = body; | |
| n.kexc = kexc; | |
| markConts(body, condStm, kexc); | |
| markConts(condStm, kreg, kexc); | |
| }); | |
| function markContsCatch(n, knocatch, kreg, kexc) { | |
| var guard = n.guard, guardStm, block = n.block; | |
| if (guard) {// Mozilla catch | |
| // The guard is of the form (var if expr). | |
| // If expr is truthy, the catch body is run w/ var bound to the exception. | |
| // If expr is falsy, we go to the next block (another catch or finally). | |
| // If the guard or the body throw, the next catches (if any) can't handle | |
| // the exception, we go to the finally block (if any) directly. | |
| markConts(guard); | |
| guardStm = semiNode(guard); | |
| n.kreg = guardStm; | |
| guardStm.kcatch = block; // this catch handles the exception | |
| guardStm.knocatch = knocatch; // this catch doesn't handle the exception | |
| guardStm.kexc = kexc; // the guard throws a new exception | |
| } | |
| markConts(block, kreg, kexc); | |
| } | |
| override(TRY, function(n, kreg, kexc) { | |
| var fin = n.finallyBlock, clause, clauses=n.catchClauses, knocatch, i, len; | |
| // process back-to-front to avoid if-madness | |
| if (fin) { | |
| markConts(fin, kreg, kexc); | |
| knocatch = kexc = kreg = fin; // TRY & CATCHes go to fin no matter what | |
| } | |
| for (len = clauses.length, i = len-1; i>=0; i--) { | |
| clause = clauses[i]; | |
| markContsCatch(clause, knocatch, kreg, kexc); | |
| knocatch = clause; | |
| } | |
| markConts(n.tryBlock, kreg, knocatch || kexc); | |
| n.kreg = n.tryBlock; | |
| }); | |
| })(); | |
| //////////////////////////////////////////////////////////////////////////////// | |
| //////////////////////////// CFA2 code ///////////////////////////////////// | |
| //////////////////////////////////////////////////////////////////////////////// | |
| // abstract objects and abstract values are different!!! | |
| var heap; | |
| // modified[addr] is a timestamp that shows the last time heap[addr] was updated | |
| var modified; | |
| var timestamp; | |
| // If i is the addr of a var, flags[i] is true if the var is a heap var. | |
| var flags; | |
| var exports_object; | |
| var exports_object_av_addr; | |
| var commonJSmode; | |
| var timedout = false, timeout = 120; // stop after 2 minutes if you're not done | |
| // A summary contains a function node (fn), an array of abstract values (args), | |
| // a timestamp (ts) and abstract values (res) and (err). It means: when we call | |
| // fn w/ args and the heap's timestamp is ts, the result is res and if fn can | |
| // throw an exception, the value thrown is err. | |
| // summaries: a map from addresses of fun nodes to triples {ts, insouts, type}, | |
| // where ts is a timestamp, insouts is an array of args-result pairs, | |
| // and type is the join of all args-result pairs. | |
| var summaries; | |
| // pending contains info that exists in the runtime stack. For each pending call | |
| // to evalFun, pending contains an object {args, ts} where args is the arguments | |
| // of the call and ts is the timestamp at the time of the call. | |
| var pending; | |
| // core JS functions also use pending but differently. | |
| // when initGlobals is called, count has its final value (core objs are in heap) | |
| // FIXME, I'm violating the invariant in "function cfa2". Change it? | |
| function initGlobals() { | |
| big_ts = 1000; // used only when debugging | |
| timestamp = 0; | |
| heap = new Array(count); // reserve heap space, don't grow it gradually | |
| modified = buildArray(count, timestamp); | |
| summaries = {}; // We use {} instead of an Array b/c it's sparse. | |
| pending = {}; | |
| flags = {}; | |
| exports_object = {lines : {}}; | |
| } | |
| // string -> void | |
| // works only in NodeJS | |
| function dumpHeap(filename) { | |
| var fd = fs.openSync(filename, "w", mode=0777); | |
| for (var i = 0, l = heap.length; i < l; i++) | |
| printf(fd, "[" + i + "]\n" + (heap[i] ? heap[i].toString(2) : "") + "\n"); | |
| fs.closeSync(fd); | |
| } | |
| // non-empty array of strings -> void | |
| function normalizeUnionType(types) { | |
| // any is a supertype of all types | |
| if (types.memq("any")) { | |
| types[0] = "any"; | |
| types.length = 1; | |
| } | |
| else | |
| types.rmDups(function(e1, e2) {return e1 === e2;}); | |
| } | |
| // Constructor for abstract properties | |
| // Takes an object w/ the property's attributes | |
| // Don't call from outside the abstract-values module, use addProp instead. | |
| function Aprop(attribs){ | |
| this.aval = attribs.aval; | |
| // writable, enumerable and configurable default to true | |
| this.write = ("write" in attribs) ? attribs.write : true; | |
| this.enum = ("enum" in attribs) ? attribs.enum : true; | |
| this.config = ("config" in attribs) ? attribs.config : true; | |
| } | |
| // optional number -> string | |
| Aprop.prototype.toString = function(indent) { | |
| return this.aval.toString(indent); | |
| }; | |
| // An abstract object o1 is represented as a JS object o2. | |
| // A property of o1 named p has the name p- in o2. | |
| // A special property p of o1 has the name -p in o2. | |
| // Special properties: -number, -string, -proto, -fun, -addr | |
| // -addr: the address of o1 in the heap | |
| // -proto: the address of o1's prototype in the heap | |
| // -fun: may point to a function node | |
| // -number: may contain an Aval (join of properties w/ "numeric" names) | |
| // -string: may contain an Aval (join of all properties) | |
| function Aobj(specialProps) { | |
| for (var p in specialProps) this["-" + p] = specialProps[p]; | |
| heap[specialProps.addr] = this; // put new object in heap | |
| } | |
| Aobj.prototype = new Array(); | |
| // Takes an optional array for cycle detection. | |
| Aobj.prototype.toType = function(seenObjs) { | |
| var self = this; | |
| if (seenObjs) { | |
| if (seenObjs.memq(self)) | |
| return "any"; | |
| else | |
| seenObjs.push(self); | |
| } | |
| else | |
| seenObjs = [self]; | |
| if (this["-fun"]) return funToType(this["-fun"], seenObjs); | |
| var c = this.getProp("constructor-"); | |
| var types = []; | |
| if (c === undefined) return "Global Object"; | |
| c.forEachObj(function(o) {if (o["-fun"]) types.push(o["-fun"].fname.name);}); | |
| if (types.length === 0) | |
| throw errorWithCode(CFA_ERROR, "Didn't find a name for constructor"); | |
| normalizeUnionType(types); | |
| if (types.length === 1) { | |
| if (types[0] === "Array") return this.toArrayType(seenObjs); | |
| return types[0]; | |
| } | |
| return ("<" + types.join(" | ") + ">"); | |
| }; | |
| // void -> boolean | |
| Aobj.prototype.isArray = function() { | |
| var c = this.getProp("constructor-"), cobj; | |
| if (c === undefined) return false; | |
| if (c.objs.length !== 1) return false; | |
| cobj = heap[c.objs[0]]; | |
| return cobj["-fun"] && cobj["-fun"].fname.name === "Array"; | |
| } | |
| // For homogeneous arrays, include the type of their elms. | |
| // Takes an optional array for cycle detection. | |
| Aobj.prototype.toArrayType = function(seenObjs) { | |
| var elmtype = BOTTOM, self = this; | |
| this.forEachOwnProp(function(p) { | |
| if (propIsNumeric(p)) elmtype = avjoin(elmtype, self.getOwnExactProp(p)); | |
| }); | |
| elmtype = elmtype.toType(seenObjs); | |
| if (/\|/.test(elmtype) || elmtype === "any") | |
| return "Array"; | |
| else | |
| return "Array[" + elmtype + "]"; | |
| }; | |
| // void -> function node | |
| Aobj.prototype.getFun = function() { return this["-fun"]; }; | |
| // Aval -> void | |
| Aobj.prototype.updateProto = function(av) { | |
| if (this["-proto"]) { | |
| if (!avlt(av, this["-proto"])) { | |
| this["-proto"] = avjoin(this["-proto"], av); | |
| //print("++ts: 1"); | |
| ++timestamp; | |
| } | |
| return; | |
| } | |
| this["-proto"] = av; | |
| //print("++ts: 2"); | |
| ++timestamp; | |
| }; | |
| // string -> boolean | |
| function propIsNumeric(p) { | |
| return p === "-number" || (/-$/.test(p) && !isNaN(p.slice(0, -1))); | |
| } | |
| // act: Aobj, optional Array[string] -> {val : A, stop : boolean} | |
| // combine: Array[A] -> A | |
| function foldProtoChain(o, act, combine) { | |
| function recur(o, seenObjs, seenProps) { | |
| var v = act(o, seenProps), val = v.val; | |
| if (v.stop) return val; // stop going up the chain | |
| if (!o["-proto"]) return val; // reached the end of the prototype chain | |
| if (seenObjs.memq(o)) | |
| return val; | |
| else | |
| seenObjs.push(o); | |
| var a = [], solen = seenObjs.length, splen = seenProps.length; | |
| o["-proto"].forEachObj(function(proto) { | |
| a.push(recur(proto, seenObjs, seenProps)); | |
| seenObjs.splice(solen, seenObjs.length); | |
| seenProps.splice(splen, seenProps.length); | |
| }); | |
| a.push(val); // The current level's result is last, 'combine' pops to get it | |
| return combine(a); | |
| } | |
| return recur(o, [], []); | |
| } | |
| // string -> Aval or undefined | |
| // doesn't look in prototype chain | |
| Aobj.prototype.getOwnExactProp = function(pname) { | |
| return this[pname] && this[pname].aval; | |
| }; | |
| // string -> Aval or undefined | |
| // doesn't look in prototype chain | |
| // pname is not "-number" or "-string" | |
| Aobj.prototype.getOwnProp = function(pname) { | |
| if (this.hasOwnProperty(pname)) | |
| return this[pname].aval; | |
| if (this.numPropsMerged && propIsNumeric(pname)) | |
| return this["-number"].aval; | |
| if (this.strPropsMerged) | |
| return this["-string"].aval; | |
| }; | |
| // string -> Aval or undefined | |
| // May include "-number" and "-string" in its result | |
| Aobj.prototype.getProp = function(pname) { | |
| return getProp(this, pname, true); | |
| }; | |
| // Aobj, string, boolean -> Aval or undefined | |
| // pname is not "-number" or "-string" | |
| // Looks in prototype chain. Returns undefined iff the property doesn't exist in | |
| // *all* paths up the prototype chain o/w it returns an Aval. | |
| // function getProp(o, pname, lax) { | |
| // function act(o) { | |
| // if (o.hasOwnProperty(pname)) | |
| // return {val : o[pname].aval, stop : true}; | |
| // if (lax && o.numPropsMerged) { | |
| // if (propIsNumeric(pname)) | |
| // return {val : o["-number"].aval, stop : true}; | |
| // if (o.strPropsMerged) | |
| // return {val : o["-string"].aval, stop : false}; | |
| // } | |
| // return { stop : false }; | |
| // } | |
| // function combine(avs) { | |
| // var notfoundinsomechain = false, val = avs.pop(); | |
| // avs.forEach(function(av) { | |
| // if (!av) | |
| // notfoundinsomechain = true; | |
| // else | |
| // val = maybeavjoin(val, av); | |
| // }); | |
| // return val && (notfoundinsomechain ? avjoin(AUNDEF, val) : val); | |
| // } | |
| // return foldProtoChain(o, act, combine); | |
| // } | |
| function getProp(o, pname, lax) { | |
| var levels = 0, av; | |
| while (o !== undefined && levels <= 2) { | |
| if (o.hasOwnProperty(pname)) | |
| return o[pname].aval; | |
| if (lax && o.numPropsMerged) { | |
| if (propIsNumeric(pname)) | |
| return o["-number"].aval; | |
| if (o.strPropsMerged) | |
| av = maybeavjoin(av, o["-string"].aval); | |
| } | |
| levels++; | |
| o = o["-proto"] && heap[o["-proto"].objs[0]]; | |
| } | |
| return av; | |
| } | |
| Aobj.prototype.getNumProps = function() { | |
| this.mergeNumProps(); | |
| return this["-number"].aval; | |
| }; | |
| // Aobj.prototype.getStrProps = function() { | |
| // function act(o, seenProps) { | |
| // if (o.strPropsMerged) | |
| // return {val : o["-string"].aval, stop : false}; | |
| // var val = BOTTOM; | |
| // o.forEachOwnProp(function(pname, pval) { | |
| // if (pval.enum && !seenProps.memq(pname)) { | |
| // val = avjoin(val, o[pname].aval); | |
| // (pname !== "-number") && seenProps.push(pname); | |
| // } | |
| // }); | |
| // return {val : val, stop : false}; | |
| // } | |
| // function combine(avs) { | |
| // var val = BOTTOM; | |
| // avs.forEach(function(av) { val = avjoin(val, av); }); | |
| // return val; | |
| // } | |
| // return foldProtoChain(this, act, combine); | |
| // }; | |
| Aobj.prototype.getStrProps = function() { | |
| var levels = 0, av = BOTTOM, seenProps = [], o = this; | |
| while (o !== undefined && levels <= 2) { | |
| if (o.strPropsMerged) | |
| av = avjoin(av, o["-string"].aval); | |
| else | |
| o.forEachOwnProp(function(pname, pval) { | |
| if (pval.enum && !seenProps.memq(pname)) { | |
| av = avjoin(av, o[pname].aval); | |
| (pname !== "-number") && seenProps.push(pname); | |
| } | |
| }); | |
| levels++; | |
| o = o["-proto"] && heap[o["-proto"].objs[0]]; | |
| } | |
| return av; | |
| }; | |
| // string, Object -> void | |
| // attribs.aval is the property's value | |
| // The property shouldn't be already present, it'll be overwritten | |
| Aobj.prototype.addProp = function(prop, attribs) { | |
| this[prop] = new Aprop(attribs); | |
| }; | |
| // string, Aval -> void | |
| Aobj.prototype.updateExactProp = function(pname, newv) { | |
| if (this.hasOwnProperty(pname)) { | |
| var p = this[pname]; | |
| if (!avlt(newv, p.aval)) { | |
| p.aval = avjoin(p.aval, newv); | |
| //print("++ts: 3"); | |
| ++timestamp; | |
| } | |
| return; | |
| } | |
| this[pname] = new Aprop({aval : newv}); | |
| //print("++ts: 4"); | |
| ++timestamp; | |
| }; | |
| // string, Aval -> void | |
| // pname is not "-number" or "-string" | |
| Aobj.prototype.updateProp = function(pname, newv) { | |
| function upd(pname, pval, newv) { | |
| if (!avlt(newv, pval.aval)) { | |
| pval.aval = avjoin(pval.aval, newv); | |
| //print("++ts: 5"); | |
| ++timestamp; | |
| } | |
| } | |
| if (this.hasOwnProperty(pname)) | |
| // either it's not enumerable, or it doesn't interfere with "-number" | |
| upd(pname, this[pname], newv); | |
| else {// the new property is enumerable | |
| if (this.strPropsMerged) | |
| upd("-string", this["-string"], newv); | |
| if (this.numPropsMerged && propIsNumeric(pname)) | |
| upd("-number", this["-number"], newv); | |
| else if (!this.strPropsMerged) { | |
| this[pname] = new Aprop({aval : newv}); | |
| //print("++ts: 6 " + pname); | |
| ++timestamp; | |
| } | |
| } | |
| }; | |
| // Aval -> void | |
| Aobj.prototype.updateNumProps = function(newv) { | |
| this.mergeNumProps(); | |
| this.updateExactProp("-number", newv); | |
| }; | |
| // Aval -> void | |
| Aobj.prototype.updateStrProps = function(newv) { | |
| this.mergeStrProps(); | |
| this.updateExactProp("-number", newv); | |
| this.updateExactProp("-string", newv); | |
| }; | |
| // merge all numeric properties of the object to one generic number property | |
| Aobj.prototype.mergeNumProps = function() { | |
| if (this.numPropsMerged) return; | |
| var av = BOTTOM, self = this; | |
| this.forEachOwnProp(function(p) { | |
| if (propIsNumeric(p)) { | |
| av = avjoin(av, self[p].aval); | |
| delete self[p]; // don't keep a mix of merged and unmerged properties. | |
| } | |
| }); | |
| this.updateExactProp("-number", av); // this bumps timestamp, don't bump again | |
| this.numPropsMerged = true; | |
| }; | |
| Aobj.prototype.mergeStrProps = function() { | |
| if (this.strPropsMerged) return; | |
| if (!this.numPropsMerged) this.mergeNumProps(); | |
| var av = BOTTOM, self = this; | |
| this.forEachOwnProp(function(pname, pval) { | |
| if (pval.enum) { | |
| av = avjoin(av, pval.aval); | |
| if (pname !== "-number") delete self[pname]; | |
| } | |
| }); | |
| this.updateExactProp("-string", av); // this bumps timestamp, don't bump again | |
| this.strPropsMerged = true; | |
| }; | |
| Aobj.prototype.forEachOwnProp = function(f) { | |
| for (var p in this) | |
| if (this[p] instanceof Aprop) | |
| f(p, this[p]); // f may ignore its second argument | |
| }; | |
| // Aobj.prototype.forEachEnumProp = function(f) { | |
| // function act(o, seenProps) { | |
| // for (var p in o) { | |
| // var pval = o[p]; | |
| // if ((pval instanceof Aprop) && pval.enum | |
| // && !seenProps.memq(p)) { | |
| // f(p, pval.aval); // f may ignore its second argument | |
| // seenProps.push(p); | |
| // } | |
| // } | |
| // return { stop : false }; | |
| // } | |
| // foldProtoChain(this, act, function() {}); | |
| // }; | |
| Aobj.prototype.forEachEnumProp = function(f) { | |
| var levels = 0, av = BOTTOM, seenProps = [], o = this; | |
| while (o !== undefined && levels <= 2) { | |
| for (var p in o) { | |
| var pval = o[p]; | |
| if ((pval instanceof Aprop) && pval.enum && !seenProps.memq(p)) { | |
| f(p, pval.aval); // f may ignore its second argument | |
| seenProps.push(p); | |
| } | |
| } | |
| levels++; | |
| o = o["-proto"] && heap[o["-proto"].objs[0]]; | |
| } | |
| return av; | |
| }; | |
| // optional number -> string | |
| // Returns a multiline string, each line starts with indent (or more) spaces | |
| Aobj.prototype.toString = function(indent) { | |
| indent = indent || 0; | |
| var i1 = buildString(indent, " "), i2 = i1 + " "; | |
| var s = i1 + "<proto>:\n"; | |
| s += (this["-proto"] ? this["-proto"].toString(indent + 2) : (i2 + "??\n")); | |
| if (this["-fun"]) { | |
| s += i1 + "<function>:\n" + i2 + this["-fun"].fname.name + | |
| (this["-fun"].lineno ? ("@" + this["-fun"].lineno) : "") + "\n"; | |
| } | |
| var self = this; | |
| this.forEachOwnProp(function(p) { | |
| var pname = p; | |
| if (p !== "-number" && p !== "-string") | |
| pname = p.slice(0, -1); | |
| s += i1 + pname + ":\n" + self[p].toString(indent + 2); | |
| }); | |
| return s; | |
| }; | |
| // An abstract value is an obj w/ 2 properties: base is a number whose bits | |
| // encode the base values, objs is an array of sorted heap addresses that | |
| // denotes a set of objects. | |
| const BOTTOM = makeBaseAval(0), ANUM = makeBaseAval(1), ASTR = makeBaseAval(2); | |
| const ATRU = makeBaseAval(4), AFALS = makeBaseAval(8), ABOOL = makeBaseAval(12); | |
| const AUNDEF = makeBaseAval(16), ANULL = makeBaseAval(32); | |
| const RESTARGS = -1; | |
| const INVALID_TIMESTAMP = -1; | |
| // constructor for abstract values. Should only be called from the wrappers. | |
| function Aval() {} | |
| // number -> Aval | |
| function makeBaseAval(base) { | |
| var v = new Aval(); | |
| v.base = base; | |
| v.objs = []; | |
| return v; | |
| } | |
| // number -> Aval | |
| // When creating an abs. value, it can contain at most one object | |
| function makeObjAval(objaddr) { | |
| var v = new Aval(); | |
| v.base = 0; | |
| v.objs = [objaddr]; | |
| return v; | |
| } | |
| // string -> Aval | |
| function makeStrLitAval(s) { | |
| var v = new Aval(); | |
| v.base = 2; | |
| v.objs = []; | |
| v.str = s; | |
| return v; | |
| } | |
| // used by parts of the code that don't know the representation of Aval | |
| Aval.prototype.getBase = function() { return makeBaseAval(this.base); }; | |
| // void -> string or undefined | |
| Aval.prototype.getStrLit = function() { return this.str; }; | |
| // Merge ATRU and AFALS to one generic boolean in the base b | |
| function mergeBoolsInBase(b) { | |
| if (b & 8) b |= 4; | |
| b = ((b & 48) >> 1) | (b & 7); | |
| return b; | |
| } | |
| // Takes an optional array for cycle detection. | |
| Aval.prototype.toType = function(seenObjs) { | |
| var i = 1, base = mergeBoolsInBase(this.base), types = []; | |
| var basetypes = {1 : "number", 2 : "string", 4 : "boolean", | |
| 8 : "undefined", 16 : "null"}; | |
| while (i <= 16) { | |
| if ((base & i) === i) types.push(basetypes[i]); | |
| i *= 2; | |
| } | |
| // If uncommented, tags show string constants where possible. | |
| // if ((base & 2) && (this.str !== undefined)) { | |
| // types.rmElmAfterIndex(function(s) {return s === "string";}, 0); | |
| // types.push("\"" + this.str + "\""); | |
| // } | |
| seenObjs || (seenObjs = []); | |
| var slen = seenObjs.length; | |
| this.forEachObj(function (o) { | |
| types.push(o.toType(seenObjs)); | |
| seenObjs.splice(slen, seenObjs.length); | |
| }); | |
| if (types.length === 0) return "any"; | |
| normalizeUnionType(types); | |
| if (types.length === 1) return types[0]; | |
| return ("<" + types.join(" | ") + ">"); | |
| }; | |
| // void -> Aval | |
| // Used when scalars need to be converted to objects | |
| Aval.prototype.baseToObj = function() { | |
| var base = this.base; | |
| if (base & 15 === 0) return this; | |
| var av = makeBaseAval(0); | |
| av.objs = this.objs; | |
| if (base & 2) av = avjoin(av, generic_string_av); | |
| if (base & 1) av = avjoin(av, generic_number_av); | |
| if (base & 12) av = avjoin(av, generic_boolean_av); | |
| return av; | |
| }; | |
| // fun takes an Aobj | |
| Aval.prototype.forEachObj = function(fun) { | |
| var objaddrs = this.objs; | |
| if (objaddrs.length === 1) // make common case faster | |
| fun(heap[objaddrs[0]]); | |
| else | |
| objaddrs.forEach(function(addr) { fun(heap[addr]); }); | |
| }; | |
| // Like forEachObj but fun returns a boolean; if it's true, we stop. | |
| Aval.prototype.forEachObjWhile = function(fun) { | |
| var objaddrs = this.objs, len = objaddrs.length; | |
| if (len === 1) // make common case faster | |
| fun(heap[objaddrs[0]]); | |
| else { | |
| var i = 0, cont = false; | |
| while (!cont && i < len) | |
| cont = fun(heap[objaddrs[i++]]); | |
| } | |
| }; | |
| // string -> Aval | |
| Aval.prototype.getProp = function(pname) { | |
| var av = BOTTOM; | |
| this.forEachObj(function(o) { av = avjoin(av, o.getProp(pname) || AUNDEF); }); | |
| return av; | |
| }; | |
| // string, Aval -> void | |
| Aval.prototype.updateProp = function(pname, av) { | |
| this.forEachObj(function(o) { o.updateProp(pname, av); }); | |
| }; | |
| // array of Aval, node -> Ans | |
| // Call each function with args. args[0] is what THIS is bound to. | |
| // FIXME: record error if rator contains base vals and non-functions | |
| Aval.prototype.callFun = function(args, callNode) { | |
| var retval = BOTTOM, errval, ans, debugCalls = 0; | |
| this.baseToObj().forEachObj(function(o) { | |
| var clos = o.getFun(); | |
| if (!clos) return; | |
| debugCalls++; | |
| ans = evalFun(clos, args, false, callNode); | |
| retval = avjoin(retval, ans.v); | |
| errval = maybeavjoin(errval, ans.err); | |
| }); | |
| // var ratorNode = callNode && callNode.children[0]; | |
| // if (!debugCalls) { | |
| // var funName, ln = ratorNode.lineno; | |
| // switch (ratorNode.type) { | |
| // case IDENTIFIER: | |
| // funName = ratorNode.name; | |
| // break; | |
| // case FUNCTION: | |
| // funName = ratorNode.fname.name; | |
| // break; | |
| // case DOT: | |
| // if (ratorNode.children[1].type === STRING) { | |
| // funName = ratorNode.children[1].value.slice(0, -1); | |
| // break; | |
| // } | |
| // // fall thru | |
| // default: | |
| // funName = "??"; | |
| // break; | |
| // } | |
| // if (args[0] === global_object_av) | |
| // print("unknown function: " + funName + ", line " + (ln || "??")); | |
| // else | |
| // print("unknown method: " + funName + ", line " + (ln || "??")); | |
| // } | |
| return new Ans(retval, undefined, errval); | |
| }; | |
| Aval.prototype.hasNum = function() { return this.base & 1; }; | |
| Aval.prototype.hasStr = function() { return this.base & 2; }; | |
| Aval.prototype.hasObjs = function() { return this.objs.length > 0; }; | |
| // returns true if it can guarantee that the Aval is falsy. | |
| Aval.prototype.isFalsy = function() { | |
| var base = this.base; | |
| return this.objs.length === 0 && base !== 0 && | |
| (base % 8 === 0 || (base === 2 && this.str === "-")); | |
| }; | |
| // returns true if it can guarantee that the Aval is truthy. | |
| Aval.prototype.isTruthy = function() { | |
| var base = this.base; | |
| return (this.objs.length === 0 && ((base === 2 && this.str !== "-") || | |
| base === 4)); | |
| }; | |
| // optional number -> string | |
| Aval.prototype.toString = function(indent) { | |
| var i1 = buildString(indent || 0, " "); | |
| var i = 1, base = mergeBoolsInBase(this.base), types = []; | |
| var basetypes = {1 : "number", 2 : "string", 4 : "boolean", | |
| 8 : "undefined", 16 : "null"}; | |
| while (i <= 16) { | |
| if ((base & i) === i) types.push(basetypes[i]); | |
| i *= 2; | |
| } | |
| return (i1 + "Base: " + types.join(", ") + "\n" + | |
| i1 + "Objects: " + this.objs.join(", ") + "\n"); | |
| }; | |
| // Aval, Aval -> Aval | |
| function avjoin(v1, v2) { | |
| var os1 = v1.objs, os1l = os1.length, os2 = v2.objs, os2l = os2.length; | |
| var b1 = v1.base, b2 = v2.base, av = makeBaseAval(b1 | b2); | |
| if (av.base & 2) { | |
| if (b1 & 2) { | |
| if (!(b2 & 2) || v1.str === v2.str) | |
| av.str = v1.str; | |
| } | |
| else // (b2 & 2) is truthy | |
| av.str = v2.str; | |
| } | |
| if (os1l === 0) | |
| av.objs = os2; // need a copy of os2 here? I think not. | |
| else if (os2l === 0) | |
| av.objs = os1; // need a copy of os1 here? I think not. | |
| else if (os1 === os2) | |
| av.objs = os1; | |
| else if (os1l === os2l) { | |
| for (var i = 0; i < os1l; i++) if (os1[i] !== os2[i]) break; | |
| if (i === os1l) { | |
| av.objs = v2.objs = os1; | |
| return av; | |
| } | |
| else | |
| av.objs = arraymerge(os1, os2); | |
| } | |
| else // merge the two arrays | |
| av.objs = arraymerge(os1, os2); | |
| return av; | |
| } | |
| // Aval or undefined, Aval or undefined -> Aval or undefined | |
| function maybeavjoin(v1, v2) { | |
| if (!v1) return v2; | |
| if (!v2) return v1; | |
| return avjoin(v1, v2); | |
| } | |
| // Aval, Aval -> boolean | |
| // compares abstract values for equality | |
| function aveq(v1, v2) { | |
| if (v1.base !== v2.base) return false; | |
| if (v1.str !== v2.str) return false; | |
| var os1 = v1.objs, os2 = v2.objs, len = os1.length, i; | |
| if (len !== os2.length) return false; | |
| if (os1 === os2) return true; | |
| for (i=0; i<len; i++) if (os1[i] !== os2[i]) return false; | |
| // os2 = os1; // Some extra sharing is possible. | |
| return true; | |
| } | |
| // Aval, Aval -> boolean | |
| // returns true if v1 is less than v2 | |
| function avlt(v1, v2) { | |
| var b1 = v1.base, b2 = v2.base; | |
| if (b1 > (b1 & b2)) return false; | |
| if ((b1 & 2) && ("str" in v2) && v2.str !== v1.str) | |
| return false; | |
| var os1 = v1.objs, os1l = os1.length, os2 = v2.objs, os2l = os2.length; | |
| if (os1l === 0 || os1 === os2) return true; | |
| if (os1l > os2l) return false; | |
| for (var i = 0, j = 0; i < os1l; i++) { | |
| while (os2[j] < os1[i]) j++; | |
| if (j === os2l || os1[i] !== os2[j]) | |
| return false; // there's an elm is os1 that's not in os2 | |
| } | |
| return true; | |
| } | |
| // function node -> Aval | |
| // If the program doesn't set a function's prototype property, create default. | |
| function makeDefaultProto(n) { | |
| var paddr = n.defaultProtoAddr; | |
| var o = new Aobj({ addr: paddr, proto: object_prototype_av }); | |
| o["constructor-"] = new Aprop({aval: makeObjAval(n.addr), enum: false}); | |
| return makeObjAval(paddr); | |
| } | |
| // heap address, Aval -> void | |
| function updateHeapAv(addr, newv) { | |
| var oldv = heap[addr]; //oldv shouldn't be undefined | |
| if (!avlt(newv, oldv)) { | |
| heap[addr] = avjoin(oldv, newv); | |
| //print("++ts: 7"); | |
| modified[addr] = ++timestamp; | |
| } | |
| } | |
| // abstract plus | |
| function aplus(v1, v2) { | |
| if (v1.objs.length !== 0 || v2.objs.length !== 0) | |
| return makeBaseAval(3); | |
| var base = ((v1.base | v2.base) & 2); // base is 0 or 2 | |
| if ((v1.base & 61) !== 0 && (v2.base & 61) !== 0) base |= 1; | |
| return makeBaseAval(base); | |
| } | |
| // Invariant: the following code should know nothing about the representation | |
| // of abstract values. | |
| //////////////////////////////////////////////////////////////////////////////// | |
| ///////////////////// CORE AND CLIENT-SIDE OBJECTS ////////////////////////// | |
| //////////////////////////////////////////////////////////////////////////////// | |
| var global_object_av; | |
| var global_object_av_addr; | |
| var object_prototype_av; | |
| var function_prototype_av; | |
| var array_constructor; | |
| var regexp_constructor; | |
| // Used to automatically convert base values to objs and call methods on them | |
| var generic_number_av; | |
| var generic_string_av; | |
| var generic_boolean_av; | |
| // string, function, number -> function node | |
| function funToNode(name, code, arity) { | |
| var n = new Node({}, {type:FUNCTION}); | |
| n.fname = idNode({}, name); | |
| n.builtin = true; | |
| n.addr = newCount(); | |
| pending[count] = 0; | |
| // built-in funs have no params property but they have an arity property | |
| // instead. It's only used by the apply method. | |
| n.arity = arity; | |
| n.body = code; | |
| return n; | |
| } | |
| // Aobj, string, function, number -> void | |
| function attachMethod(o, mname, mcode, arity) { | |
| var n = funToNode(mname, mcode, arity), addr = n.addr; | |
| var fobj = new Aobj({ addr: addr, | |
| proto: function_prototype_av, | |
| fun: n }); | |
| o.addProp(mname + "-", { aval: makeObjAval(addr), enum : false }); | |
| return fobj; | |
| } | |
| // create the JS core objects in heap & fill in core | |
| function initCoreObjs() { | |
| function toStr(args) { return new Ans(ASTR); } | |
| function toNum(args) { return new Ans(ANUM); } | |
| function toBool(args) { return new Ans(ABOOL); } | |
| function toThis(args) { return new Ans(args[0]); } | |
| // Global object | |
| var go = new Aobj({ addr: newCount() }), goav = makeObjAval(count); | |
| global_object_av = heap[newCount()] = goav; | |
| global_object_av_addr = count; | |
| // global identifiers and methods | |
| go.addProp("Infinity-", {aval:ANUM, write:false, enum:false, config:false}); | |
| go.addProp("NaN-", {aval:ANUM, write:false, enum:false, config:false}); | |
| go.addProp("undefined-",{aval:AUNDEF, write:false, enum:false, config:false}); | |
| // Object.prototype | |
| var op = new Aobj({ addr: newCount() }), opav = makeObjAval(count); | |
| object_prototype_av = opav; | |
| // Object.__proto__ (same as Function.prototype) | |
| var o_p = new Aobj({ addr: newCount(), proto: opav }); | |
| var o_pav = makeObjAval(count); | |
| function_prototype_av = o_pav; | |
| // Function.prototype.prototype | |
| var fpp = new Aobj({ addr: newCount(), proto: opav }); | |
| o_p.addProp("prototype-",{aval:makeObjAval(count), enum:false, config:false}); | |
| fpp.addProp("constructor-", {aval : o_pav, enum : false}); | |
| // Object | |
| var _Object = (function () { | |
| // This object is used when Object is called w/out new. | |
| // In reality the behavior doesn't change. In CFA, when there is no new, | |
| // it's better to bind THIS to nonewav instead of the global object. | |
| new Aobj({ addr: newCount(), proto: opav }); | |
| var nonewav = makeObjAval(count); | |
| return function (args, withNew) { | |
| var retval = withNew ? args[0] : nonewav; | |
| var arg = args[1]; | |
| if (!arg) { | |
| retval.forEachObj(function (o) { o.updateProto(opav); }); | |
| return new Ans(retval); | |
| } | |
| else { | |
| // throw errorWithCode(CFA_ERROR, "call a suitable constructor, " + | |
| // "hasn't been defined yet. FIXME"); | |
| retval.forEachObj(function (o) { o.updateProto(opav); }); | |
| return new Ans(retval); | |
| } | |
| }; | |
| })(); | |
| // Object is a heap var that will contain an Aval that points to o | |
| var o = attachMethod(go, "Object", _Object, 0), oav = makeObjAval(count); | |
| o.addProp("prototype-", {aval:opav, write:false, enum:false, config:false}); | |
| op.addProp("constructor-", {aval: oav, enum: false}); | |
| // Function | |
| var f = new Aobj({addr: newCount(), proto: o_pav}), fav = makeObjAval(count); | |
| go.addProp("Function-",{aval : fav, enum : false}); | |
| f.addProp("prototype-", {aval:o_pav, write:false, enum:false, config:false}); | |
| o_p.addProp("constructor-", {aval : fav, enum : false}); | |
| // Methods are attached here because o_pav must be defined already. | |
| attachMethod(go, "isFinite", toBool, 1); | |
| attachMethod(go, "isNaN", toBool, 1); | |
| attachMethod(go, "parseInt", toNum, 1); | |
| attachMethod(op, "hasOwnProperty", toBool, 1); | |
| attachMethod(op, "toString", toStr, 0); | |
| attachMethod(op, "valueOf", toThis, 0); | |
| attachMethod(o_p, "toString", toStr, 0); | |
| attachMethod(o_p, "call", | |
| function(args, withNew, cn) { | |
| var f = args.shift(); | |
| args[0] || args.unshift(global_object_av); | |
| return f.callFun(args, cn); | |
| }, 0); | |
| attachMethod(o_p, "apply", | |
| function(args, withNew, cn) { | |
| var recv = args[1] || global_object_av, a2 = args[2], rands, | |
| av, maxArity = 0, restargs, i, ans, retval = BOTTOM, errval; | |
| // We can't compute the arguments once for all functions that | |
| // may be applied. The functions may have different arity which | |
| // impacts what goes to the restargs for each function. | |
| args[0].forEachObj(function(o) { | |
| var clos = o.getFun(), pslen, i; | |
| if (!clos) return; | |
| if (clos.builtin) | |
| pslen = clos.arity; | |
| else | |
| pslen = clos.params.length; | |
| // compute arguments | |
| restargs = BOTTOM; | |
| rands = buildArray(pslen, BOTTOM); | |
| if (a2) { // a2 is the array passed at the call to apply. | |
| a2.forEachObj(function(o) { | |
| if (o.numPropsMerged) { | |
| av = o.getNumProps(); | |
| restargs = avjoin(restargs, av); | |
| for (i = 0; i < pslen; i++) | |
| rands[i] = avjoin(rands[i], av); | |
| } | |
| else { | |
| for (i = 0; i < pslen; i++) { | |
| av = o.getOwnExactProp(i + "-") || AUNDEF; | |
| rands[i] = avjoin(rands[i], av); | |
| } | |
| while (true) { // search for extra arguments | |
| av = o.getOwnExactProp(i++ + "-"); | |
| // to find the end of the array, we must see that | |
| // an elm *definitely* doesn't exist, different | |
| // from AUNDEF | |
| if (!av) break; | |
| restargs = avjoin(restargs, av); | |
| } | |
| } | |
| }); | |
| } | |
| else { | |
| rands = buildArray(pslen, BOTTOM); | |
| } | |
| // do function call | |
| rands.unshift(recv); | |
| rands.push(restargs); | |
| ans = evalFun(clos, rands, false, cn); | |
| retval = avjoin(retval, ans.v); | |
| errval = maybeavjoin(errval, ans.err); | |
| }); | |
| return new Ans(retval, undefined, errval); | |
| }, 2); | |
| (function () { | |
| // Array.prototype | |
| var ap = new Aobj({ addr: newCount(), proto: opav }); | |
| var apav = makeObjAval(count); | |
| function putelms(args) { | |
| var i, len = args.length; | |
| args[0].forEachObj(function (o) { | |
| for (i = 1; i < len; i++) o.updateNumProps(args[i]); | |
| }); | |
| return new Ans(ANUM); | |
| } | |
| function getelms(args) { | |
| var av = BOTTOM; | |
| args[0].forEachObj(function (o) { av = avjoin(av, o.getNumProps()); }); | |
| return new Ans(av); | |
| } | |
| attachMethod(ap, "concat", | |
| // lose precision by not creating a new array | |
| function(args) { | |
| var thisarr = args[0], av = BOTTOM; | |
| // if arg is base, join it, if it's array join its elms | |
| for (var i = 1, l = args.length; i < l; i++) { | |
| var avarg = args[i]; | |
| av = avjoin(av, avarg.getBase()); | |
| avarg.forEachObj(function(o) { | |
| if (o.isArray()) av = avjoin(av, o.getNumProps()); | |
| }); | |
| thisarr.forEachObj(function(o) { o.updateNumProps(av); }); | |
| } | |
| return new Ans(thisarr); | |
| }, 0); | |
| attachMethod(ap, "join", toStr, 1); | |
| attachMethod(ap, "pop", getelms, 0); | |
| attachMethod(ap, "push", putelms, 0); | |
| attachMethod(ap, "slice", toThis, 2); | |
| attachMethod(ap, "sort", toThis, 1); | |
| attachMethod(ap, "splice", toThis, 0); | |
| attachMethod(ap, "shift", getelms, 0); | |
| attachMethod(ap, "toString", toStr, 0); | |
| attachMethod(ap, "unshift", putelms, 0); | |
| // Array | |
| var _Array = (function () { | |
| // This object is used when Array is called w/out new | |
| new Aobj({ addr: newCount(), proto: apav }); | |
| var nonewav = makeObjAval(count); | |
| return function(args, withNew) { | |
| var retval = withNew ? args[0] : nonewav; | |
| var arglen = args.length; | |
| retval.forEachObj(function (o) { | |
| o.updateProto(apav); | |
| if (o.getOwnExactProp("length-")) | |
| o.updateProp("length-", ANUM); | |
| else | |
| o.addProp("length-", {aval : ANUM, enum : false}); | |
| }); | |
| if (arglen <= 2) // new Array(), new Array(size) | |
| ; | |
| else { // new Array(elm1, ... , elmN) | |
| retval.forEachObj(function (o) { | |
| for (var i = 1; i < arglen; i++) | |
| o.updateProp((i - 1) + "-", args[i]); | |
| }); | |
| } | |
| return new Ans(retval); | |
| }; | |
| })(); | |
| array_constructor = _Array; | |
| var a = attachMethod(go, "Array", _Array, 0), aav = makeObjAval(count); | |
| a.addProp("prototype-", {aval:apav, write:false, enum:false, config:false}); | |
| ap.addProp("constructor-", {aval : aav, enum : false}); | |
| })(); | |
| (function () { | |
| // Number.prototype | |
| var np = new Aobj({ addr: newCount(), proto: opav }); | |
| var npav = makeObjAval(count); | |
| attachMethod(np, "toString", toStr, 0); | |
| attachMethod(np, "valueOf", toNum, 0); | |
| // create generic number object | |
| new Aobj({ addr: newCount(), proto: npav}); | |
| generic_number_av = makeObjAval(count); | |
| // Number | |
| function _Number(args, withNew) { | |
| if (withNew) { | |
| args[0].forEachObj(function (o) { o.updateProto(npav); }); | |
| return new Ans(args[0]); | |
| } | |
| return new Ans(ANUM); | |
| } | |
| var n = attachMethod(go, "Number", _Number, 0), nav = makeObjAval(count); | |
| n.addProp("prototype-", {aval:npav, write:false, enum:false, config:false}); | |
| np.addProp("constructor-", {aval : nav, enum : false}); | |
| })(); | |
| (function () { | |
| // String.prototype | |
| var sp = new Aobj({ addr: newCount(), proto: opav }); | |
| var spav = makeObjAval(count); | |
| attachMethod(sp, "charAt", toStr, 1); | |
| attachMethod(sp, "charCodeAt", toNum, 1); | |
| attachMethod(sp, "indexOf", toNum, 2); | |
| attachMethod(sp, "lastIndexOf", toNum, 2); | |
| // all Arrays returned by calls to match are merged in one | |
| var omatch = new Aobj({ addr: newCount() }); | |
| var omatchav = avjoin(ANULL, makeObjAval(count)); | |
| array_constructor([omatchav], true); | |
| omatch.updateNumProps(ASTR); | |
| omatch.addProp("index-", {aval : ANUM}); | |
| omatch.addProp("input-", {aval : ASTR}); | |
| attachMethod(sp, "match", function(args) { return new Ans(omatchav); }, 1); | |
| attachMethod(sp, "replace", toStr, 2); | |
| attachMethod(sp, "slice", toStr, 2); | |
| attachMethod(sp, "substr", toStr, 2); | |
| attachMethod(sp, "substring", toStr, 2); | |
| attachMethod(sp, "toLowerCase", toStr, 0); | |
| attachMethod(sp, "toString", toStr, 0); | |
| attachMethod(sp, "toUpperCase", toStr, 0); | |
| // all Arrays returned by calls to split are merged in one | |
| var osplit = new Aobj({ addr: newCount() }); | |
| var osplitav = makeObjAval(count); | |
| array_constructor([osplitav], true); | |
| osplit.updateNumProps(ASTR); | |
| attachMethod(sp, "split", function(args) { | |
| return new Ans(osplitav); | |
| }, 2); | |
| attachMethod(sp, "valueOf", toStr, 0); | |
| // create generic string object | |
| new Aobj({ addr: newCount(), proto: spav }); | |
| generic_string_av = makeObjAval(count); | |
| // String | |
| function _String(args, withNew) { | |
| if (withNew) { | |
| args[0].forEachObj(function (o) { o.updateProto(spav); }); | |
| return new Ans(args[0]); | |
| } | |
| return new Ans(ASTR); | |
| } | |
| var s = attachMethod(go, "String", _String, 1), sav = makeObjAval(count); | |
| s.addProp("prototype-", {aval:spav, write:false, enum:false, config:false}); | |
| sp.addProp("constructor-", {aval : sav, enum : false}); | |
| attachMethod(s, "fromCharCode", toStr, 0); | |
| })(); | |
| (function () { | |
| // Error.prototype | |
| var ep = new Aobj({ addr: newCount(), proto: opav }); | |
| var epav = makeObjAval(count); | |
| attachMethod(ep, "toString", toStr, 0); | |
| // Error | |
| function _Error(args) { | |
| args[0].forEachObj(function (o) { | |
| o.updateProto(epav); | |
| o.updateProp("message-", args[1] || ASTR); | |
| }); | |
| return new Ans(args[0]); | |
| } | |
| var e = attachMethod(go, "Error", _Error, 1), eav = makeObjAval(count); | |
| e.addProp("prototype-", {aval:epav, write:false, enum:false, config:false}); | |
| ep.addProp("constructor-", {aval : eav, enum : false}); | |
| ep.addProp("name-", {aval : ASTR, enum : false}); | |
| // SyntaxError.prototype | |
| var sep = new Aobj({ addr: newCount(), proto: epav }); | |
| var sepav = makeObjAval(count); | |
| // SyntaxError | |
| function _SyntaxError(args) { | |
| args[0].forEachObj(function (o) { | |
| o.updateProto(sepav); | |
| o.addProp("message-", {aval : ASTR}); | |
| }); | |
| return new Ans(args[0]); | |
| } | |
| var se = attachMethod(go, "SyntaxError", _SyntaxError, 1); | |
| var seav = makeObjAval(count); | |
| se.addProp("prototype-",{aval:sepav, write:false, enum:false,config:false}); | |
| sep.addProp("constructor-", {aval : seav, enum : false}); | |
| sep.addProp("name-", {aval : ASTR}); | |
| })(); | |
| (function () { | |
| // RegExp.prototype | |
| var rp = new Aobj({ addr: newCount(), proto: opav }); | |
| var rpav = makeObjAval(count); | |
| // all Arrays returned by calls to exec are merged in one | |
| var oexec = new Aobj({ addr: newCount() }); | |
| var oexecav = avjoin(ANULL, makeObjAval(count)); | |
| array_constructor([oexecav], true); | |
| oexec.updateNumProps(ASTR); | |
| oexec.addProp("index-", {aval : ANUM}); | |
| oexec.addProp("input-", {aval : ASTR}); | |
| attachMethod(rp, "exec", function(args) { return new Ans(oexecav); }, 1); | |
| attachMethod(rp, "test", toBool, 1); | |
| // RegExp | |
| function _RegExp(args) { | |
| args[0].forEachObj(function (o) { | |
| o.updateProto(rpav); | |
| o.addProp("global-",{aval:ABOOL, write:false, enum:false,config:false}); | |
| o.addProp("ignoreCase-", | |
| {aval:ABOOL, write:false, enum:false, config:false}); | |
| o.addProp("lastIndex-", {aval : ANUM, enum : false, config : false}); | |
| o.addProp("multiline-", | |
| {aval:ABOOL, write:false, enum:false, config:false}); | |
| o.addProp("source-",{aval:ASTR, write:false, enum:false, config:false}); | |
| }); | |
| return new Ans(args[0]); | |
| } | |
| regexp_constructor = _RegExp; | |
| var r = attachMethod(go, "RegExp", _RegExp, 2), rav = makeObjAval(count); | |
| r.addProp("prototype-", {aval:rpav, write:false, enum:false, config:false}); | |
| rp.addProp("constructor-", {aval : rav, enum : false}); | |
| })(); | |
| (function () { | |
| // Date.prototype | |
| var dp = new Aobj({ addr: newCount(), proto: opav }); | |
| var dpav = makeObjAval(count); | |
| attachMethod(dp, "getDate", toNum, 0); | |
| attachMethod(dp, "getDay", toNum, 0); | |
| attachMethod(dp, "getFullYear", toNum, 0); | |
| attachMethod(dp, "getHours", toNum, 0); | |
| attachMethod(dp, "getMilliseconds", toNum, 0); | |
| attachMethod(dp, "getMinutes", toNum, 0); | |
| attachMethod(dp, "getMonth", toNum, 0); | |
| attachMethod(dp, "getSeconds", toNum, 0); | |
| attachMethod(dp, "getTime", toNum, 0); | |
| attachMethod(dp, "getTimezoneOffset", toNum, 0); | |
| attachMethod(dp, "getYear", toNum, 0); | |
| attachMethod(dp, "setTime", toNum, 1); | |
| attachMethod(dp, "toString", toStr, 0); | |
| attachMethod(dp, "valueOf", toNum, 0); | |
| // Date | |
| function _Date(args, withNew) { | |
| if (withNew) { | |
| args[0].forEachObj(function (o) { o.updateProto(dpav); }); | |
| return new Ans(args[0]); | |
| } | |
| return new Ans(ASTR); | |
| } | |
| var d = attachMethod(go, "Date", _Date, 0), dav = makeObjAval(count); | |
| d.addProp("prototype-", {aval:dpav, write:false, enum:false, config:false}); | |
| dp.addProp("constructor-", {aval : dav, enum : false}); | |
| })(); | |
| (function () { | |
| // Math | |
| var m = new Aobj({ addr: newCount(), proto: opav }); | |
| var mav = makeObjAval(count); | |
| go.addProp("Math-", {aval : mav, enum : false}); | |
| m.addProp("constructor-", {aval : oav, enum : false}); | |
| m.addProp("E-", {aval : ANUM, write : false, enum : false, config : false}); | |
| m.addProp("LN10-",{aval : ANUM, write : false, enum : false, config:false}); | |
| m.addProp("LN2-", {aval : ANUM, write : false, enum : false, config:false}); | |
| m.addProp("LOG10E-", {aval : ANUM, write:false, enum:false, config:false}); | |
| m.addProp("LOG2E-", {aval : ANUM, write:false, enum:false, config:false}); | |
| m.addProp("PI-", {aval : ANUM, write : false, enum : false, config :false}); | |
| m.addProp("SQRT1_2-", {aval : ANUM, write:false, enum:false, config:false}); | |
| m.addProp("SQRT2-",{aval:ANUM, write:false, enum:false, config:false}); | |
| attachMethod(m, "abs", toNum, 1); | |
| attachMethod(m, "acos", toNum, 1); | |
| attachMethod(m, "asin", toNum, 1); | |
| attachMethod(m, "atan", toNum, 1); | |
| attachMethod(m, "atan2", toNum, 1); | |
| attachMethod(m, "ceil", toNum, 1); | |
| attachMethod(m, "cos", toNum, 1); | |
| attachMethod(m, "exp", toNum, 1); | |
| attachMethod(m, "floor", toNum, 1); | |
| attachMethod(m, "log", toNum, 1); | |
| attachMethod(m, "max", toNum, 0); | |
| attachMethod(m, "min", toNum, 0); | |
| attachMethod(m, "pow", toNum, 2); | |
| attachMethod(m, "random", toNum, 0); | |
| attachMethod(m, "round", toNum, 1); | |
| attachMethod(m, "sin", toNum, 1); | |
| attachMethod(m, "sqrt", toNum, 1); | |
| attachMethod(m, "tan", toNum, 1); | |
| })(); | |
| (function () { | |
| // Boolean.prototype | |
| var bp = new Aobj({ addr: newCount(), proto: opav }); | |
| var bpav = makeObjAval(count); | |
| attachMethod(bp, "toString", toStr, 0); | |
| attachMethod(bp, "valueOf", toBool, 0); | |
| // create generic boolean object | |
| new Aobj({ addr: newCount(), proto: bpav }); | |
| generic_boolean_av = makeObjAval(count); | |
| // Boolean | |
| function _Boolean(args, withNew) { | |
| if (withNew) { | |
| args[0].forEachObj(function (o) { o.updateProto(bpav); }); | |
| return new Ans(args[0]); | |
| } | |
| return new Ans(ABOOL); | |
| } | |
| var b = attachMethod(go, "Boolean", _Boolean, 1), bav = makeObjAval(count); | |
| b.addProp("prototype-", {aval:bpav, write:false, enum:false, config:false}); | |
| bp.addProp("constructor-", {aval : bav, enum : false}); | |
| })(); | |
| } | |
| //////////////////////////////////////////////////////////////////////////////// | |
| ////////////////////////// EVALUATION PREAMBLE ///////////////////////////// | |
| //////////////////////////////////////////////////////////////////////////////// | |
| // frame, identifier node, Aval -> void | |
| function frameSet(fr, param, val) { | |
| fr[param.addr] = [val, timestamp]; // record when param was bound to val | |
| } | |
| // frame, identifier node -> Aval | |
| function frameGet(fr, param) { | |
| var pa = param.addr, binding = fr[pa]; | |
| if (binding[1] < modified[pa]) { | |
| // if binding changed in heap, change it in frame to be sound | |
| binding[0] = avjoin(binding[0], heap[pa]); | |
| binding[1] = timestamp; | |
| } | |
| return binding[0]; | |
| } | |
| // fun. node, array of Aval, timestamp -> [Aval, Aval] or false | |
| function searchSummary(n, args, ts) { | |
| var n_summaries = summaries[n.addr], insouts, summary; | |
| if (n_summaries.ts < ts) return false; | |
| insouts = n_summaries.insouts; | |
| // Start from the end to find the elm that was pushed last | |
| for (var i = insouts.length - 1; i >= 0; i--) { | |
| summary = insouts[i]; | |
| // If no widening, turn avlt to aveq in the next line. | |
| if (arrayeq(avlt, args, summary[0])) return summary.slice(-2); | |
| } | |
| return false; | |
| } | |
| // function node -> boolean | |
| // check if any summary exists for this function node | |
| function existsSummary(n) { | |
| return summaries[n.addr].ts !== INVALID_TIMESTAMP; | |
| } | |
| // fun. node, array of Aval, Aval, Aval or undefined, timestamp -> void | |
| function addSummary(n, args, retval, errval, ts) { | |
| var addr = n.addr, summary = summaries[addr]; | |
| if (summary.ts === ts) | |
| summary.insouts.push([args, retval, errval]); | |
| else if (summary.ts < ts) { // discard summaries for old timestamps. | |
| summary.ts = ts; | |
| summary.insouts = [[args, retval, errval]]; | |
| } | |
| // join new summary w/ earlier ones. | |
| var insjoin = summary.type[0]; | |
| for (var i = 0, len = insjoin.length; i < len; i++) | |
| insjoin[i] = avjoin(insjoin[i], args[i] || AUNDEF/*arg mismatch*/); | |
| summary.type[1] = avjoin(summary.type[1], retval); | |
| summary.type[2] = maybeavjoin(summary.type[2], errval); | |
| } | |
| function showSummaries() { | |
| for (var addr in summaries) { | |
| var f = heap[addr].getFun(); | |
| //print(f.fname.name + ": " + funToType(f)); | |
| } | |
| } | |
| // function node, array of Aval -> number or undefined | |
| // How evalFun interprets the return value of searchPending | |
| // Zero: new frame on the stack | |
| // Positive number: throw to clear the stack b/c of timestamp increase | |
| // Negative number: throw to clear the stack during recursion | |
| // undefined: return w/out throwing during recursion | |
| function searchPending(n, args) { | |
| var bucket = pending[n.addr], len = bucket.length, i; | |
| // We use the number of pending calls to n to clear the stack. | |
| if (len === 0) return 0; | |
| if (bucket[0].ts < timestamp) return len; | |
| // Invariant: no two sets of args are related in \sqsubseteq. | |
| for (i = 0; i < len; i++) { | |
| // No need to keep going, a more general frame is pending. | |
| if (arrayeq(avlt, args, bucket[i].args)) | |
| return; | |
| // The deeper frame can be widened, throw to it. | |
| else if (arrayeq(avlt, bucket[i].args, args)) | |
| return i - len; | |
| } | |
| return 0; | |
| } | |
| // function node, {args, timestamp} -> void | |
| function addPending(n, elm) { pending[n.addr].push(elm); } | |
| // function node -> {args, timestamp} | |
| function rmPending(n) { | |
| // // Uncomment when debugging | |
| // var elm = pending[n.addr].pop(); | |
| // if (!elm) throw errorWithCode(CFA_ERROR, "Remove from empty pending."); | |
| // return elm; | |
| return pending[n.addr].pop(); | |
| } | |
| // evalExp & friends use Ans to return tuples | |
| function Ans(v, fr, err) { | |
| this.v = v; // evalExp puts abstract values here, evalStm puts statements | |
| this.fr = fr; // frame | |
| err && (this.err = err); // Aval for exceptions thrown | |
| } | |
| // Initialize the heap for each fun decl, var decl and heap var. | |
| // Because of this function, we never get undefined by reading from heap. | |
| // Must be run after initGlobals and after initCoreObjs. | |
| // Most Aobj`s that aren't core are created here. | |
| var initDeclsInHeap, initDeclsInHeap_override; | |
| (function() { | |
| var walkerobj = walkASTgenerator(), override = walkerobj.override; | |
| initDeclsInHeap = walkerobj.walkAST; | |
| initDeclsInHeap_override = override; | |
| override(REGEXP, function(n) { | |
| new Aobj({ addr: n.addr }); | |
| regexp_constructor([makeObjAval(n.addr)]); | |
| }); | |
| // FIXME?: when array elms have the same type, they can be prematurely merged | |
| // to help the speed of the algo e.g. in 3d-cube | |
| override(ARRAY_INIT, function(n) { | |
| new Aobj({ addr: n.addr }); | |
| array_constructor([makeObjAval(n.addr)], true); | |
| n.children.forEach(initDeclsInHeap); | |
| }); | |
| override(OBJECT_INIT, function(n) { | |
| new Aobj({ addr: n.addr, proto: object_prototype_av }); | |
| n.children.forEach(function(prop) { | |
| initDeclsInHeap(prop.children[0]); | |
| initDeclsInHeap(prop.children[1]); | |
| }); | |
| }); | |
| override(NEW_WITH_ARGS, function(n) { | |
| new Aobj({ addr: n.addr }); | |
| initDeclsInHeap(n.children[0]); | |
| n.children[1].children.forEach(initDeclsInHeap); | |
| }); | |
| override(TRY, function(n) { | |
| initDeclsInHeap(n.tryBlock); | |
| n.catchClauses.forEach(function(c) { | |
| if (c.exvar.kind === HEAP) heap[c.exvar.addr] = BOTTOM; | |
| c.guard && initDeclsInHeap(c.guard); | |
| initDeclsInHeap(c.block); | |
| }); | |
| n.finallyBlock && initDeclsInHeap(n.finallyBlock); | |
| }); | |
| function _function(n) { | |
| var objaddr = n.addr, fn = n.fname, | |
| obj = new Aobj({ addr: objaddr, fun: n, proto: function_prototype_av }); | |
| obj.addProp("prototype-", {aval:BOTTOM, enum:false}); | |
| if (fn.kind === HEAP) | |
| heap[fn.addr] = makeObjAval(objaddr); | |
| n.params.forEach(function(p) {if (p.kind === HEAP) heap[p.addr] = BOTTOM;}); | |
| flags[objaddr] = n; | |
| // initialize summaries and pending | |
| summaries[objaddr] = { | |
| ts: INVALID_TIMESTAMP, | |
| insouts: [], | |
| type: [buildArray(n.params.length + 1, BOTTOM), BOTTOM]//arg 0 is for THIS | |
| }; | |
| pending[objaddr] = []; | |
| initDeclsInHeap(n.body); | |
| } | |
| override(FUNCTION, _function); | |
| override(SCRIPT, function(n) { | |
| n.funDecls.forEach(_function); | |
| n.varDecls.forEach(function(vd){if (flags[vd.addr]) heap[vd.addr]=BOTTOM;}); | |
| n.children.forEach(initDeclsInHeap); | |
| }); | |
| })(); | |
| // void -> Aval | |
| // Used to analyze functions that aren't called | |
| function makeGenericObj() { | |
| new Aobj({ addr: newCount(), proto: object_prototype_av }); | |
| return makeObjAval(count); | |
| } | |
| //////////////////////////////////////////////////////////////////////////////// | |
| ////////////////////////// EVALUATION FUNCTIONS //////////////////////////// | |
| //////////////////////////////////////////////////////////////////////////////// | |
| // function for evaluating lvalues | |
| // node, Ans, optional Aval -> Ans | |
| // use n to get an lvalue, do the assignment and return the rvalue | |
| var evalLval; | |
| (function() { | |
| var walkerobj = walkASTgenerator(), override = walkerobj.override; | |
| evalLval = walkerobj.walkAST; | |
| function _stackref(n, ans, oldlval) { | |
| if (n.assignOp) { | |
| if (n.assignOp === PLUS) | |
| ans.v = aplus(ans.v, oldlval); | |
| else | |
| ans.v = ANUM; | |
| } | |
| var newav = avjoin(frameGet(ans.fr, n), ans.v); | |
| frameSet(ans.fr, n, newav); | |
| // if n is a heap var, update heap so its heap refs get the correct Aval. | |
| if (flags[n.addr]) updateHeapAv(n.addr, newav); | |
| return ans; | |
| } | |
| function _heapref(n, ans, oldlval) { | |
| if (n.assignOp) { | |
| if (n.assignOp === PLUS) | |
| ans.v = aplus(ans.v, oldlval); | |
| else | |
| ans.v = ANUM; | |
| } | |
| updateHeapAv(n.addr, ans.v); | |
| return ans; | |
| } | |
| override(IDENTIFIER, function(n, ans, oldlval) { | |
| if (n.kind === STACK) | |
| return _stackref(n, ans, oldlval); | |
| else | |
| return _heapref(n, ans, oldlval); | |
| }); | |
| override(INDEX, function(n, ans, oldlval) { | |
| var rval = ans.v, fr = ans.fr, errval, ch = n.children; | |
| if (n.assignOp) { | |
| if (n.assignOp === PLUS) | |
| rval = aplus(rval, oldlval); | |
| else | |
| rval = ANUM; | |
| } | |
| var prop = ch[1]; | |
| var ansobj = evalExp(ch[0], fr), avobj = ansobj.v; | |
| fr = ansobj.fr; | |
| errval = ansobj.err; | |
| // Unsound: ignore everything the index can eval to except numbers & strings | |
| if (prop.type === STRING) | |
| avobj.updateProp(prop.value, rval); | |
| else { | |
| var ansprop = evalExp(prop, fr), avprop = ansprop.v; | |
| fr = ansprop.fr; | |
| errval = maybeavjoin(errval, ansprop.err); | |
| if (avprop.hasNum()) | |
| avobj.forEachObj(function(o) { o.updateNumProps(rval); }); | |
| if (avprop.hasStr()) { | |
| var slit = avprop.getStrLit(); | |
| if (slit) | |
| avobj.updateProp(slit, rval); | |
| else | |
| avobj.forEachObj(function(o) { o.updateStrProps(rval); }); | |
| } | |
| } | |
| return new Ans(rval, fr, maybeavjoin(errval, ans.err)); | |
| }); | |
| function _dot(n, ans, oldlval) { | |
| if (n.assignOp) { | |
| if (n.assignOp === PLUS) | |
| ans.v = aplus(ans.v, oldlval); | |
| else | |
| ans.v = ANUM; | |
| } | |
| var ch = n.children, ans2 = evalExp(ch[0], ans.fr); | |
| ans2.v.updateProp(ch[1].value, ans.v); | |
| ans.fr = ans2.fr; | |
| ans.err = maybeavjoin(ans.err, ans2.err); | |
| return ans; | |
| } | |
| override(DOT, _dot); | |
| override(DOT_PROTO, _dot); | |
| override(ARGUMENTS, function(n, ans, oldlval) { | |
| // FIXME: handle assignment to the arguments array | |
| return ans; | |
| }); | |
| // in extremely rare cases, you can see a CALL as the lhs of an assignment. | |
| override(CALL, function(n, ans, oldlval) { | |
| return ans; | |
| }); | |
| })(); | |
| // function for evaluating expressions | |
| // node, frame -> Ans | |
| var evalExp, evalExp_override; | |
| (function() { | |
| var walkerobj = walkASTgenerator(), override = walkerobj.override; | |
| evalExp = walkerobj.walkAST; | |
| evalExp_override = override; | |
| function _stackref(n, fr) { return new Ans(frameGet(fr, n), fr); } | |
| function _heapref(n, fr) { return new Ans(heap[n.addr], fr); } | |
| override(IDENTIFIER, function(n, fr) { | |
| if (n.kind === STACK) | |
| return _stackref(n, fr); | |
| else | |
| return _heapref(n, fr); | |
| }); | |
| override(NUMBER, function(n, fr) { return new Ans(ANUM, fr); }); | |
| override(STRING, | |
| function(n, fr) { return new Ans(makeStrLitAval(n.value), fr); }); | |
| override(TRUE, function(n, fr) { return new Ans(ATRU, fr); }); | |
| override(FALSE, function(n, fr) { return new Ans(AFALS, fr); }); | |
| override(NULL, function(n, fr) { return new Ans(ANULL, fr); }); | |
| override(REGEXP, function(n, fr){ return new Ans(makeObjAval(n.addr), fr); }); | |
| override(THIS, function(n, fr) { return new Ans(fr.thisav, fr); }); | |
| function _unary2num(n, fr) { | |
| var ans = evalExp(n.children[0], fr); | |
| ans.v = ANUM; | |
| return ans; | |
| } | |
| [UNARY_PLUS, UNARY_MINUS, INCREMENT, DECREMENT, BITWISE_NOT].forEach( | |
| function(tt) { override(tt, _unary2num); }); | |
| override(NOT, function(n, fr) { | |
| var ans = evalExp(n.children[0], fr), av = ans.v; | |
| if (av.isTruthy()) | |
| ans.v = AFALS; | |
| else if (av.isFalsy()) | |
| ans.v = ATRU; | |
| else | |
| ans.v = ABOOL; | |
| return ans; | |
| }); | |
| override(TYPEOF, function(n, fr) { | |
| var ans = evalExp(n.children[0], fr); | |
| ans.v = ASTR; | |
| return ans; | |
| }); | |
| override(VOID, function(n, fr) { | |
| var ans = evalExp(n.children[0], fr); | |
| ans.v = AUNDEF; | |
| return ans; | |
| }); | |
| override(DELETE, function(n, fr) { // unsound: I'm not deleting anything | |
| var ans = evalExp(n.children[0], fr); | |
| ans.v = ABOOL; | |
| return ans; | |
| }); | |
| override(IN, function(n, fr) { | |
| var ans1 = evalExp(n.children[0], fr); | |
| var ans2 = evalExp(n.children[1], ans1.fr); | |
| ans2.err = maybeavjoin(ans1.err, ans2.err); | |
| var pname = ans1.v.getStrLit(), ans2v = ans2.v; | |
| if (!ans2v.hasObjs()) | |
| ans2.v = AFALS; | |
| else if (!pname) | |
| ans2.v = ABOOL; | |
| else { | |
| var av = BOTTOM; | |
| ans2v.forEachObj(function(o) { | |
| if (!o.getProp(pname)) | |
| av = avjoin(av, AFALS); | |
| else | |
| av = avjoin(av, ATRU); | |
| }); | |
| ans2.v = av; | |
| } | |
| return ans2; | |
| }); | |
| function _binary2bool(n, fr) { | |
| var ans1 = evalExp(n.children[0], fr); | |
| var ans2 = evalExp(n.children[1], ans1.fr); | |
| ans2.v = ABOOL; | |
| ans2.err = maybeavjoin(ans1.err, ans2.err); | |
| return ans2; | |
| } | |
| [EQ, NE, STRICT_EQ, STRICT_NE, LT, LE, GE, GT, INSTANCEOF].forEach( | |
| function(tt) { override(tt, _binary2bool); }); | |
| function _andor(pred1, pred2) { | |
| return function(n, fr) { | |
| var ans1 = evalExp(n.children[0], fr), av = ans1.v; | |
| if (pred1.call(av)) return ans1; | |
| var ans2 = evalExp(n.children[1], ans1.fr); | |
| ans2.err = maybeavjoin(ans1.err, ans2.err); | |
| if (!pred2.call(av)) ans2.v = avjoin(av, ans2.v); | |
| return ans2; | |
| } | |
| } | |
| override(AND, _andor(Aval.prototype.isFalsy, Aval.prototype.isTruthy)); | |
| override(OR, _andor(Aval.prototype.isTruthy, Aval.prototype.isFalsy)); | |
| override(PLUS, function(n, fr) { | |
| var ans1 = evalExp(n.children[0], fr); | |
| var ans2 = evalExp(n.children[1], ans1.fr); | |
| ans2.v = aplus(ans1.v, ans2.v); | |
| ans2.err = maybeavjoin(ans1.err, ans2.err); | |
| return ans2; | |
| }); | |
| function _binary2num(n, fr) { | |
| var ans1 = evalExp(n.children[0], fr); | |
| var ans2 = evalExp(n.children[1], ans1.fr); | |
| ans2.v = ANUM; | |
| ans2.err = maybeavjoin(ans1.err, ans2.err); | |
| return ans2; | |
| } | |
| [MINUS, MUL, DIV, MOD, BITWISE_OR, BITWISE_XOR, BITWISE_AND, | |
| LSH, RSH, URSH].forEach(function(tt) { override(tt, _binary2num); }); | |
| override(PLUS_ASSIGN, function(n, fr) { | |
| var ch = n.children; | |
| // recomputing ch[0] for += is better than checking every lhs in evalLval | |
| var ans = evalExp(ch[0], fr); | |
| return evalLval(ch[0], evalExp(ch[1], fr), ans.v); | |
| }); | |
| override(ASSIGN, function(n, fr) { | |
| return evalLval(n.children[0], evalExp(n.children[1], fr)); | |
| }); | |
| override(HOOK, function(n, fr) { | |
| var ch = n.children; | |
| var ans = evalExp(ch[0], fr), test = ans.v, av = BOTTOM, err = ans.err; | |
| if (!test.isFalsy()) { | |
| ans = evalExp(ch[1], ans.fr); | |
| av = avjoin(av, ans.v); | |
| err = maybeavjoin(err, ans.err); | |
| } | |
| if (!test.isTruthy()) { | |
| ans = evalExp(ch[2], ans.fr); | |
| av = avjoin(av, ans.v); | |
| err = maybeavjoin(err, ans.err); | |
| } | |
| return new Ans(av, ans.fr, err); | |
| }); | |
| override(FUNCTION, | |
| function(n, fr) { return new Ans(makeObjAval(n.addr), fr); }); | |
| override(COMMA, function(n, fr) { | |
| var ans, av, errval; | |
| n.children.forEach(function(exp) { | |
| ans = evalExp(exp, fr); | |
| av = ans.v; // keep last one | |
| fr = ans.fr; | |
| errval = maybeavjoin(errval, ans.err); | |
| }); | |
| ans.v = av; | |
| ans.err = errval; | |
| return ans; | |
| }); | |
| override(OBJECT_INIT, function(n, fr) { | |
| var ans, errval, objaddr = n.addr, newobj = heap[objaddr]; | |
| n.children.forEach(function(pinit) { | |
| ans = evalExp(pinit.children[1], fr); | |
| fr = ans.fr; | |
| newobj.updateProp(pinit.children[0].value, ans.v); | |
| errval = maybeavjoin(errval, ans.err); | |
| }); | |
| return new Ans(makeObjAval(objaddr), fr, errval); | |
| }); | |
| override(ARRAY_INIT, function(n, fr) { | |
| var ans, errval, arrayaddr = n.addr, newarray = heap[arrayaddr]; | |
| n.children.forEach(function(elm, i) { | |
| ans = evalExp(elm, fr); | |
| fr = ans.fr; | |
| newarray.updateProp(i + "-", ans.v); | |
| errval = maybeavjoin(errval, ans.err); | |
| }); | |
| return new Ans(makeObjAval(arrayaddr), fr, errval); | |
| }); | |
| override(DOT_PROTO, function(n, fr) { | |
| var ans = evalExp(n.children[0], fr), ans2, av = BOTTOM, | |
| av2, errval = ans.err; | |
| // FIXME: record error if ans.v contains base values | |
| ans.v.forEachObj(function(o) { | |
| var clos = o.getFun(), proto; | |
| if (!clos) { // if o isn't a function, this is just a property access | |
| av2 = o.getProp("prototype-"); | |
| av = avjoin(av, av2 || AUNDEF); | |
| } | |
| else { | |
| proto = o.getProp("prototype-"); | |
| if (!aveq(BOTTOM, proto)) | |
| av = avjoin(av, proto); | |
| else {// create default prototype and return it | |
| proto = makeDefaultProto(clos); | |
| o.updateProp("prototype-", proto); | |
| av = avjoin(av, proto); | |
| } | |
| } | |
| }); | |
| ans2 = new Ans(av, ans.fr, errval); | |
| ans2.thisav = ans.v; // used by method calls | |
| return ans2; | |
| }); | |
| override(INDEX, function(n, fr) { | |
| var ansobj = evalExp(n.children[0], fr), avobj = ansobj.v.baseToObj(), | |
| prop = n.children[1], errval = ansobj.err, av , ans; | |
| fr = ansobj.fr; | |
| // If [] notation is used with a constant, try to be precise. | |
| // Unsound: ignore everything the index can eval to except numbers & strings | |
| if (prop.type === STRING) | |
| av = avobj.getProp(prop.value); | |
| else { | |
| var ansprop = evalExp(prop, fr), avprop = ansprop.v; | |
| fr = ansprop.fr; | |
| errval = maybeavjoin(errval, ansprop.err); | |
| av = BOTTOM; | |
| if (avprop.hasNum()) | |
| avobj.forEachObj(function(o) { av = avjoin(av, o.getNumProps()); }); | |
| if (avprop.hasStr()) { | |
| var slit = avprop.getStrLit(); | |
| if (slit) | |
| av = avjoin(av, avobj.getProp(slit)); | |
| else | |
| avobj.forEachObj(function(o) { av = avjoin(av, o.getStrProps()); }); | |
| } | |
| } | |
| ans = new Ans(av, fr, errval); | |
| ans.thisav = avobj; | |
| return ans; | |
| }); | |
| override(DOT, function(n, fr) { | |
| var ans = evalExp(n.children[0], fr), avobj = ans.v.baseToObj(); | |
| ans.thisav = avobj; // used by method calls | |
| ans.v = avobj.getProp(n.children[1].value); | |
| return ans; | |
| }); | |
| override(CALL, function(n, fr) { | |
| // To see if the analysis reaches some program point in all.js, add some | |
| // call: e10sDebug(some_msg) and uncomment the following code. | |
| // if (n.children[0].value === "e10sDebug") { | |
| // print(n.children[1].children[0].value); | |
| // } | |
| var ans = evalExp(n.children[0], fr), ans1, errval, rands = []; | |
| rands.push(ans.thisav ? ans.thisav : global_object_av); | |
| fr = ans.fr; | |
| errval = ans.err; | |
| // evaluate arguments | |
| n.children[1].children.forEach(function(rand) { | |
| ans1 = evalExp(rand, fr); | |
| rands.push(ans1.v); | |
| fr = ans1.fr; | |
| errval = maybeavjoin(errval, ans1.err); | |
| }); | |
| // call each function that can flow to the operator position | |
| ans = ans.v.callFun(rands, n); | |
| ans.fr = fr; | |
| ans.err = maybeavjoin(errval, ans.err); | |
| return ans; | |
| }); | |
| override(NEW_WITH_ARGS, function(n, fr) { | |
| var ch = n.children, rands = [], retval = BOTTOM; | |
| var ans = evalExp(ch[0], fr), ans1, errval; | |
| var objaddr = n.addr, thisobj = heap[objaddr]; | |
| rands.push(makeObjAval(objaddr)); | |
| fr = ans.fr; | |
| errval = ans.err; | |
| // evaluate arguments | |
| ch[1].children.forEach(function(rand) { | |
| ans1 = evalExp(rand, fr); | |
| rands.push(ans1.v); | |
| fr = ans1.fr; | |
| errval = maybeavjoin(errval, ans1.err); | |
| }); | |
| // FIXME: record error if rator contains base vals and non-functions | |
| ans.v.baseToObj().forEachObj(function(o) { | |
| var clos = o.getFun(), proto; | |
| if (!clos) return; | |
| proto = o.getProp("prototype-"); | |
| if (aveq(BOTTOM, proto)) { | |
| // create default prototype & use it | |
| proto = makeDefaultProto(clos); | |
| o.updateProp("prototype-", proto); | |
| } | |
| thisobj.updateProto(proto); | |
| // if a fun is called both w/ and w/out new, assume it's a constructor | |
| clos.withNew = true; | |
| ans = evalFun(clos, rands, true, n); | |
| if (clos.hasReturn) // constructor uses return | |
| retval = avjoin(retval, ans.v); | |
| else // constructor doesn't use return | |
| retval = avjoin(retval, rands[0]); | |
| errval = maybeavjoin(errval, ans.err); | |
| }); | |
| return new Ans(retval, fr, errval); | |
| }); | |
| override(ARGUMENTS, function(n, fr) { | |
| var index = n.children[0], ps = n.arguments; | |
| var restargs = fr[RESTARGS] || BOTTOM, ans, av, errval; | |
| if (index.type === NUMBER) { | |
| var iv = index.value; | |
| if (iv < 0) | |
| av = AUNDEF; | |
| else if (iv < ps.length) | |
| av = frameGet(fr, ps[iv]); | |
| else | |
| av = restargs; // unsound: not checking if iv > #args | |
| } | |
| else { | |
| ans = evalExp(index, fr); | |
| fr = ans.fr; | |
| errval = ans.err; | |
| av = BOTTOM; | |
| // when we don't know the index, we return the join of all args | |
| ps.forEach(function(p) { av = avjoin(av, frameGet(fr, p)); }); | |
| av = avjoin(av, restargs); | |
| } | |
| return new Ans(av, fr, errval); | |
| }); | |
| })(); | |
| // function for evaluating statements | |
| // node, frame -> Ans | |
| // Evaluate the statement and find which statement should be executed next. | |
| var evalStm; | |
| (function() { | |
| var walkerobj = walkASTgenerator(), override = walkerobj.override; | |
| evalStm = walkerobj.walkAST; | |
| override(SEMICOLON, function(n, fr) { | |
| var ans = evalExp(n.expression, fr); | |
| ans.v = n.kreg; | |
| return ans; | |
| }); | |
| function _next(n, fr) { return new Ans(n.kreg, fr); } | |
| [BLOCK, CASE, DEFAULT, DO, FINALLY, FOR, IF, SWITCH, TRY, WHILE].forEach( | |
| function(tt) { override(tt, _next); }); | |
| override(FOR_IN, function(n, fr) { | |
| // For most kinds of iterators at FOR/IN we have to be conservative | |
| // (e.g. DOTs or INDEXes). Without flow sensitivity, we even have to be | |
| // conservative for stack refs that have been initialized, we can't forget | |
| // their current value. We can only be precise when the iterator is a stack | |
| // reference and the variable is BOTTOM in the frame. | |
| var ans = evalExp(n.object, fr), errval, av; | |
| var it = n.iterator, b = n.body; | |
| if (it.type === IDENTIFIER && | |
| it.kind === STACK && | |
| aveq(BOTTOM, frameGet(fr, it))) { | |
| av = ans.v; | |
| errval = ans.err; | |
| av.forEachObj(function(o) { | |
| o.forEachEnumProp(function(p) { | |
| // wipe the value of it from the previous iteration | |
| frameSet(fr, it, makeStrLitAval(p)); | |
| if (flags[it.addr]) updateHeapAv(it.addr, makeStrLitAval(p)); | |
| ans = evalStm(b, fr); | |
| errval = maybeavjoin(errval, ans.err); | |
| }); | |
| }); | |
| ans.v = b.kreg; | |
| ans.err = errval; | |
| } | |
| else { | |
| av = BOTTOM; | |
| ans.v.forEachObj(function(o) { | |
| o.forEachEnumProp(function(p) { | |
| if (propIsNumeric(p)) | |
| av = avjoin(av, ANUM); | |
| else | |
| av = avjoin(av, ASTR); | |
| }); | |
| }); | |
| ans.v = av; | |
| ans = evalLval(n.iterator, ans); | |
| ans.v = b; | |
| } | |
| return ans; | |
| }); | |
| override(CATCH, function(n, fr) { return new Ans(n.block, fr); }); | |
| override(THROW, function(n, fr) { | |
| var ans = evalExp(n.exception, fr); | |
| ans.err = maybeavjoin(ans.err, ans.v); | |
| ans.v = n.kreg; | |
| return ans; | |
| }); | |
| })(); | |
| var big_ts; | |
| // StackCleaner inherits from Error and is not used to signal errors, but to | |
| // manage the size of the runtime stack. | |
| // function node, number, optional array of Aval | |
| function StackCleaner(fn, howmany, args) { | |
| this.fn = fn; | |
| this.howmany = howmany; | |
| if (args) this.args = args; | |
| } | |
| StackCleaner.prototype = new Error(); | |
| // function node, array of Aval, boolean, optional call node -> Ans w/out fr | |
| // Arg 4 is the node that caused the function call (if there is one). | |
| function evalFun(fn, args, withNew, cn) { | |
| var ans, n, params, fr, w, script = fn.body, pelm1; | |
| var retval = BOTTOM, errval = BOTTOM; | |
| // stm node (exception continuation), av (exception value) -> void | |
| function stmThrows(n, errav) { | |
| if (n) { | |
| if (n.type === CATCH) { | |
| var exvar = n.exvar; | |
| if (exvar.kind === HEAP) | |
| heap[exvar.addr] = avjoin(errav, heap[exvar.addr]); | |
| if (fr[exvar.addr]) // revealing the representation of frame here. | |
| frameSet(fr, exvar, avjoin(errav, frameGet(fr, exvar))); | |
| else | |
| frameSet(fr, exvar, errav); | |
| } | |
| w.push(n); | |
| } | |
| else | |
| errval = avjoin(errval, errav); | |
| } | |
| if (process.uptime() > timeout) throw new Error("timeout"); | |
| // if (timestamp > big_ts) { | |
| // print("big ts: " + timestamp); | |
| // dumpHeap("heapdump" + timestamp + ".txt"); | |
| // big_ts += 1000; | |
| // if (big_ts > 100000) throw new Error("foobar"); | |
| // } | |
| // treat built-in functions specially | |
| if (fn.builtin) { | |
| // return fn.body(args, withNew, cn); | |
| // If there's an input for which built-ins cause stack overflow, uncomment. | |
| var addr = fn.addr; | |
| if (pending[addr] > 1) { | |
| return new Ans(BOTTOM, undefined, BOTTOM); | |
| } | |
| ++pending[addr]; | |
| try { | |
| ans = fn.body(args, withNew, cn); | |
| --pending[addr]; | |
| return ans; | |
| } | |
| catch (e) { | |
| if (e instanceof StackCleaner) --pending[addr]; | |
| throw e; | |
| } | |
| } | |
| var tsAtStart; | |
| var result = searchSummary(fn, args, timestamp); | |
| if (result) return new Ans(result[0], undefined, result[1]); | |
| while(true) { | |
| try { | |
| tsAtStart = timestamp; | |
| // pending & exceptions prevent the runtime stack from growing too much. | |
| var pelm2 = searchPending(fn, args); | |
| if (pelm2 === 0) { | |
| pelm1 = {args : args, ts : timestamp}; | |
| addPending(fn, pelm1); | |
| } | |
| else if (pelm2 === undefined) { | |
| // If a call eventually leads to itself, stop analyzing & return BOTTOM. | |
| // Add a summary that describes the least solution. | |
| addSummary(fn, args, BOTTOM, BOTTOM, tsAtStart); | |
| return new Ans(BOTTOM, undefined, BOTTOM); | |
| } | |
| else if (pelm2 > 0) { | |
| // There are pending calls that are obsolete because their timestamp is | |
| // old. Discard frames to not grow the stack too much. | |
| throw new StackCleaner(fn, pelm2); | |
| } | |
| else /* if (pelm2 < 0) */ { | |
| throw new StackCleaner(fn, -pelm2, args); | |
| } | |
| w = []; | |
| fr = {}; | |
| params = fn.params; | |
| frameSet(fr, fn.fname, makeObjAval(fn.addr)); | |
| // args[0] is always the obj that THIS is bound to. | |
| // THIS never has a heap ref, so its entry in the frame is special. | |
| fr.thisav = args[0]; | |
| // Bind formals to actuals | |
| for (var i = 0, len = params.length; i < len; i++) { | |
| var param = params[i], arg = args[i+1] || AUNDEF;//maybe #args < #params | |
| if (param.kind === HEAP) updateHeapAv(param.addr, arg); | |
| frameSet(fr, param, arg); | |
| } | |
| var argslen = args.length; | |
| if ((++i) < argslen) { // handling of extra arguments | |
| var restargs = BOTTOM; | |
| for (; i<argslen; i++) restargs = avjoin(restargs, args[i]); | |
| fr[RESTARGS] = restargs; // special entry in the frame. | |
| } | |
| // bind a non-init`d var to bottom, not undefined. | |
| script.varDecls.forEach(function(vd) { frameSet(fr, vd, BOTTOM); }); | |
| // bind the fun names in the frame. | |
| script.funDecls.forEach(function(fd) { | |
| frameSet(fr, fd.fname, makeObjAval(fd.addr)); | |
| }); | |
| w.push(script.kreg); | |
| while (w.length !== 0) { | |
| n = w.pop(); | |
| if (n === undefined) continue; | |
| if (n.type === RETURN) { | |
| ans = evalExp(n.value, fr); | |
| // fr is passed to exprs/stms & mutated, no need to join(fr, ans.fr) | |
| fr = ans.fr; | |
| retval = avjoin(retval, ans.v); | |
| w.push(n.kreg); | |
| if (ans.err) stmThrows(n.kexc, ans.err); | |
| } | |
| else { | |
| ans = evalStm(n, fr); | |
| fr = ans.fr; | |
| w.push(ans.v); | |
| if (ans.err) stmThrows(n.kexc, ans.err); | |
| } | |
| } | |
| rmPending(fn); | |
| if (!fn.hasReturn) retval = AUNDEF; | |
| // Find if a summary has been added during recursion. | |
| result = searchSummary(fn, args, tsAtStart); | |
| if (!result || (avlt(retval, result[0]) && avlt(errval, result[1]))) { | |
| // Either fn isn't recursive, or the fixpt computation has finished. | |
| if (!result) addSummary(fn, args, retval, errval, tsAtStart); | |
| return new Ans(retval, undefined, errval); | |
| } | |
| else { | |
| retval = avjoin(result[0], retval); | |
| errval = avjoin(result[1], errval); | |
| // The result changed the last summary; update summary and keep going. | |
| addSummary(fn, args, retval, errval, tsAtStart); | |
| } | |
| } | |
| catch (e) { | |
| if (!(e instanceof StackCleaner)) { | |
| // analysis error, irrelevant to the stack-handling code | |
| throw e; | |
| } | |
| if (!pelm1) throw e; | |
| rmPending(fn); | |
| if (e.fn !== fn) throw e; | |
| if (e.howmany !== 1) { | |
| e.howmany--; | |
| throw e; | |
| } | |
| if (e.args) args = e.args; | |
| } | |
| } | |
| } | |
| // maybe merge with evalFun at some point | |
| function evalToplevel(tl) { | |
| var w /* workset */, fr, n, ans; | |
| w = []; | |
| fr = {}; | |
| initDeclsInHeap(tl); | |
| fr.thisav = global_object_av; | |
| // bind a non-init`d var to bottom, different from assigning undefined to it. | |
| tl.varDecls.forEach(function(vd) { frameSet(fr, vd, BOTTOM); }); | |
| // bind the fun names in the frame. | |
| tl.funDecls.forEach(function(fd) { | |
| frameSet(fr, fd.fname, makeObjAval(fd.addr)); | |
| }); | |
| // evaluate the stms of the toplevel in order | |
| w.push(tl.kreg); | |
| while (w.length !== 0) { | |
| n = w.pop(); | |
| if (n === undefined) continue; // end of toplevel reached | |
| if (n.type === RETURN) | |
| ; // record error, return in toplevel | |
| else { | |
| ans = evalStm(n, fr); | |
| fr = ans.fr; | |
| w.push(ans.v); | |
| // FIXME: handle toplevel uncaught exception | |
| } | |
| } | |
| //print("call uncalled functions"); | |
| // each function w/out a summary is called with unknown arguments | |
| for (var addr in summaries) { | |
| var f = heap[addr].getFun(); | |
| if (!existsSummary(f)) { | |
| var any_args = buildArray(f.params.length, BOTTOM); | |
| any_args.unshift(makeGenericObj()); | |
| evalFun(f, any_args, false); | |
| } | |
| } | |
| //showSummaries(); | |
| } | |
| // initGlobals and initCoreObjs are difficult to override. The next 2 vars help | |
| // clients of the analysis add stuff to happen during initialization | |
| var initOtherGlobals, initOtherObjs; | |
| // consumes the ast returned by jsparse.parse | |
| function cfa2(ast) { | |
| count = 0; | |
| astSize = 0; | |
| initGlobals(); | |
| initOtherGlobals && initOtherGlobals(); | |
| //print("fixStm start"); | |
| fixStm(ast); | |
| //print("fixStm succeeded"); | |
| initCoreObjs(); | |
| initOtherObjs && initOtherObjs(); | |
| //print("initObjs done"); | |
| if (commonJSmode) { // create the exports object | |
| var e = new Aobj({ addr: newCount() }), eav = makeObjAval(count); | |
| heap[newCount()] = eav; | |
| exports_object_av_addr = count; | |
| exports_object.obj = e; | |
| } | |
| labelAST(ast); | |
| //print("labelStm done"); | |
| desugarWith(ast, undefined, []); | |
| //print("desugarWith done"); | |
| desugarLet(ast); | |
| tagVarRefs(ast, [], [], "toplevel"); | |
| //print("tagrefsStm done"); | |
| markConts(ast, undefined, undefined); | |
| //print("markconts done"); | |
| try { | |
| //print("Done with preamble. Analysis starting."); | |
| evalToplevel(ast); | |
| //print("after cfa2"); | |
| // print("AST size: " + astSize); | |
| // print("ts: " + timestamp); | |
| // dumpHeap("heapdump.txt"); | |
| } | |
| catch (e) { | |
| if (e.message !== "timeout") { | |
| print(e.message); | |
| console.trace(); | |
| if (! ("code" in e)) e.code = CFA_ERROR; | |
| throw e; | |
| } | |
| else | |
| timedout = true; | |
| } | |
| } | |
| // function node -> string | |
| function funToType(n, seenObjs) { | |
| if (n.builtin) | |
| return "function"; // FIXME: tag built-in nodes w/ their types | |
| if (seenObjs) { | |
| if (seenObjs.memq(n)) | |
| return "any"; | |
| else | |
| seenObjs.push(n); | |
| } | |
| else { | |
| seenObjs = [n]; | |
| } | |
| var addr = n.addr, summary = summaries[addr]; | |
| if (summary.ts === INVALID_TIMESTAMP) // the function was never called | |
| return "function"; | |
| var insjoin = summary.type[0], instypes = [], outtype, slen = seenObjs.length; | |
| for (var i = 1, len = insjoin.length; i < len; i++) { | |
| instypes[i - 1] = insjoin[i].toType(seenObjs); | |
| // each argument must see the same seenObjs, the initial one. | |
| seenObjs.splice(slen, seenObjs.length); | |
| } | |
| if (n.withNew && !n.hasReturn) { | |
| outtype = n.fname.name; | |
| // If a fun is called both w/ and w/out new, assume it's a constructor. | |
| // If a constructor is called w/out new, THIS is bound to the global obj. | |
| // In this case, the result type must contain void. | |
| var thisObjType = insjoin[0].toType(seenObjs); | |
| if (/Global Object/.test(thisObjType)) | |
| outtype = "<void | " + outtype + ">"; | |
| } | |
| else | |
| outtype = summary.type[1].toType(seenObjs); | |
| if (outtype === "undefined") outtype = "void"; | |
| return (outtype + " function(" + instypes.join(", ") +")"); | |
| } | |
| // node, string, Array of string, cmd-line options -> Array of ctags | |
| function getTags(ast, pathtofile, lines, options) { | |
| const REGEX_ESCAPES = { "\n": "\\n", "\r": "\\r", "\t": "\\t" }; | |
| var tags = []; | |
| function regexify(str) { | |
| function subst(ch) { | |
| return (ch in REGEX_ESCAPES) ? REGEX_ESCAPES[ch] : "\\" + ch; | |
| } | |
| str || (str = ""); | |
| return "/^" + str.replace(/[\\/$\n\r\t]/g, subst) + "$/"; | |
| } | |
| if (options.commonJS) commonJSmode = true; | |
| // print(pathtofile); | |
| cfa2(ast); | |
| // print("Flow analysis done. Generating tags"); | |
| if (exports_object.obj) { | |
| var eo = exports_object.obj; | |
| eo.forEachOwnProp(function (p) { | |
| var av = eo.getOwnExactProp(p); | |
| var tag = {}; | |
| tag.name = /-$/.test(p) ? p.slice(0, -1) : p.slice(1); | |
| tag.tagfile = pathtofile; | |
| tag.addr = regexify(lines[exports_object.lines[p] - 1]); | |
| var type = av.toType(); | |
| if (/(^<.*> function)|(^[^<>\|]*function)/.test(type)) | |
| tag.kind = "f"; | |
| else | |
| tag.kind = "v"; | |
| tag.type = type; | |
| tag.module = options.module; | |
| tag.lineno = exports_object.lines[p]; | |
| tags.push(tag); | |
| }); | |
| } | |
| for (var addr in summaries) { | |
| var f = heap[addr].getFun(); | |
| tags.push({ name : f.fname.name || "%anonymous_function", | |
| tagfile : pathtofile, | |
| addr : regexify(lines[f.lineno - 1]), | |
| kind : "f", | |
| type : funToType(f), | |
| lineno : f.lineno.toString(), | |
| sortno : f.lineno.toString() | |
| }); | |
| } | |
| ast.varDecls.forEach(function(vd) { | |
| tags.push({ name : vd.name, | |
| tagfile : pathtofile, | |
| addr : regexify(lines[vd.lineno - 1]), | |
| kind : "v", | |
| type : heap[vd.addr].toType(), | |
| lineno : vd.lineno.toString(), | |
| sortno : vd.lineno.toString() | |
| }); | |
| }); | |
| return tags; | |
| } | |
| // node -> boolean | |
| // hacky test suite. Look in run-tests.js | |
| function runtest(ast) { | |
| cfa2(ast); | |
| // find test's addr at the toplevel | |
| var testaddr, fds = ast.funDecls; | |
| for (var i = 0, len = fds.length; i < len; i++) | |
| if (fds[i].fname.name === "test") { | |
| testaddr = fds[i].addr; | |
| break; | |
| } | |
| if (testaddr === undefined) throw errorWithCode(CFA_ERROR, "Malformed test"); | |
| var type = summaries[testaddr].type; | |
| // print(type[0][1]); | |
| // print(type[1]); | |
| return aveq(type[0][1], type[1]); | |
| } | |
| exports.cfa2 = cfa2; | |
| exports.runtest = runtest; | |
| exports.getTags = getTags; | |
| //////////////////////////////////////////////////////////////////////////////// | |
| ////////////// DATA DEFINITIONS FOR THE AST RETURNED BY JSPARSE //////////// | |
| //////////////////////////////////////////////////////////////////////////////// | |
| function walkExp(n) { | |
| switch (n.type){ | |
| //nullary | |
| case NULL: | |
| case THIS: | |
| case TRUE: | |
| case FALSE: | |
| break; | |
| case IDENTIFIER: | |
| case NUMBER: | |
| case STRING: | |
| case REGEXP: | |
| // n.value | |
| break; | |
| //unary | |
| case DELETE: | |
| case VOID: | |
| case TYPEOF: | |
| case NOT: | |
| case BITWISE_NOT: | |
| case UNARY_PLUS: case UNARY_MINUS: | |
| case NEW: | |
| walkExp(n.children[0]); | |
| break; | |
| case INCREMENT: case DECREMENT: | |
| // n.postfix is true or undefined | |
| walkExp(n.children[0]); | |
| break; | |
| //binary | |
| case CALL: | |
| case NEW_WITH_ARGS: | |
| walkExp(n.children[0]); | |
| //n[1].type === LIST | |
| n.children[1].children.forEach(walkExp); | |
| break; | |
| case IN: | |
| walkExp(n.children[0]); // an exp which must eval to string | |
| walkExp(n.children[1]); // an exp which must eval to obj | |
| break; | |
| case DOT: | |
| walkExp(n.children[0]); | |
| walkExp(n.children[1]); // must be IDENTIFIER | |
| break; | |
| case BITWISE_OR: case BITWISE_XOR: case BITWISE_AND: | |
| case EQ: case NE: case STRICT_EQ: case STRICT_NE: | |
| case LT: case LE: case GE: case GT: | |
| case INSTANCEOF: | |
| case LSH: case RSH: case URSH: | |
| case PLUS: case MINUS: case MUL: case DIV: case MOD: | |
| case AND: case OR: | |
| case ASSIGN: // n[0].assignOp shows which op-and-assign operator we have here | |
| case INDEX: // property indexing | |
| walkExp(n.children[0]); | |
| walkExp(n.children[1]); | |
| break; | |
| //ternary | |
| case HOOK: | |
| walkExp(n.children[0]); | |
| walkExp(n.children[1]); | |
| walkExp(n.children[2]); | |
| break; | |
| //variable arity | |
| case COMMA: | |
| case ARRAY_INIT: // array literal | |
| n.children.forEach(walkExp); | |
| break; | |
| case OBJECT_INIT: | |
| n.children.forEach(function(prop) { // prop.type === PROPERTY_INIT | |
| walkExp(prop.children[0]); // identifier, number or string | |
| walkExp(prop.children[1]); | |
| }); | |
| break; | |
| //other | |
| case FUNCTION: | |
| // n.name is a string | |
| // n.params is an array of strings | |
| // n.functionForm === EXPRESSED_FORM | |
| walkStm(n.body); | |
| break; | |
| } | |
| } | |
| function walkStm(n) { | |
| switch (n.type) { | |
| case SCRIPT: | |
| case BLOCK: | |
| n.children.forEach(walkStm); | |
| break; | |
| case FUNCTION: | |
| // n.name is a string | |
| // n.params is an array of strings | |
| // n.functionForm === DECLARED_FORM or STATEMENT_FORM | |
| // STATEMENT_FORM is for funs declared in inner blocks, like IF branches | |
| // It doesn't extend the funDecls of the script, bad! | |
| walkStm(n.body); | |
| break; | |
| case SEMICOLON: | |
| n.expression && walkExp(n.expression); | |
| break; | |
| case IF: | |
| walkExp(n.condition); | |
| walkStm(n.thenPart); | |
| n.elsePart && walkStm(n.elsePart); | |
| break; | |
| case SWITCH: | |
| walkExp(n.discriminant); | |
| // a switch w/out branches is legal, n.cases is [] | |
| n.cases.forEach(function(branch) { | |
| branch.caseLabel && walkExp(branch.caseLabel); | |
| // if the branch has no stms, branch.statements is an empty block | |
| walkStm(branch.statements); | |
| }); | |
| break; | |
| case FOR: | |
| if (n.setup) { | |
| if (n.setup.type === VAR || n.setup.type === CONST) | |
| walkStm(n.setup); | |
| else walkExp(n.setup); | |
| } | |
| n.condition && walkExp(n.condition); | |
| n.update && walkExp(n.update); | |
| walkStm(n.body); | |
| break; | |
| case FOR_IN: | |
| // n.varDecl may be used when there is a LET at the head of the for/in loop. | |
| walkExp(n.iterator); | |
| walkExp(n.object); | |
| walkStm(n.body); | |
| break; | |
| case WHILE: | |
| case DO: | |
| walkExp(n.condition); | |
| walkStm(n.body); | |
| break; | |
| case BREAK: | |
| case CONTINUE: | |
| // do nothing: n.label is just a name, n.target points back to ancestor | |
| break; | |
| case TRY: | |
| walkStm(n.tryBlock); | |
| n.catchClauses.forEach(function(clause) { // clause.varName is a string | |
| clause.guard && walkExp(clause.guard); | |
| walkStm(clause.block); | |
| }); | |
| n.finallyBlock && walkStm(n.finallyBlock); | |
| break; | |
| case THROW: | |
| walkExp(n.exception); | |
| break; | |
| case RETURN: | |
| n.value && walkExp(n.value); | |
| break; | |
| case WITH: | |
| walkExp(n.object); | |
| walkStm(n.body); | |
| break; | |
| case LABEL: | |
| // n.label is a string | |
| walkStm(n.statement); | |
| break; | |
| case VAR: | |
| case CONST: // variable or constant declaration | |
| // vd.name is a string | |
| // vd.readOnly is true for constants, false for variables | |
| n.children.forEach(function(vd) { walkExp(vd.initializer); }); | |
| break; | |
| } | |
| return n; | |
| } | |
| //////////////////////////////////////////////////////////////////////////////// | |
| //////////// EVENT CLASSIFICATION FOR FIREFOX ADDONS ///////////////////// | |
| //////////////////////////////////////////////////////////////////////////////// | |
| var e10sResults, chrome_obj_av_addr, Chrome, Content; | |
| (function() { | |
| // Initializing functions are overriden here | |
| initOtherGlobals = function() { | |
| // make separate constructors for chrome and content objs, so that we can | |
| // distinguish them w/ instanceof | |
| Chrome = function(specialProps) { Aobj.call(this, specialProps); } | |
| Chrome.prototype = new Aobj({ addr: newCount() }); | |
| Content = function(specialProps) { Aobj.call(this, specialProps); } | |
| Content.prototype = new Aobj({ addr: newCount() }); | |
| e10sResults = {}; | |
| }; | |
| initOtherObjs = initDOMObjs; | |
| tagVarRefs_override(IDENTIFIER, tagVarRefsId(true)); | |
| // Must be called *after* the desugarings. | |
| function initDeclsInHeap_e10s(n) { | |
| // "Attach listener" have one more property called status: | |
| // status is an Array of four strings, describing the listener: | |
| // event name: some specific name or unknown | |
| // attached on: chrome, content or any | |
| // originates from: chrome, content or any | |
| // flagged: safe or unsafe | |
| n.addr = newCount(); | |
| e10sResults[count] = { | |
| lineno : n.lineno, | |
| analyzed : false, | |
| kind : (n.type === DOT && n.children[1].value === "addEventListener-") ? | |
| "Attach listener" : "Touch content" | |
| }; | |
| n.children.forEach(initDeclsInHeap); | |
| } | |
| initDeclsInHeap_override(DOT, initDeclsInHeap_e10s); | |
| initDeclsInHeap_override(INDEX, initDeclsInHeap_e10s); | |
| })(); | |
| function initDOMObjs() { | |
| function toThis(args) { return new Ans(args[0]); } | |
| // the whole DOM tree is modeled by 2 chrome and 1 content object | |
| var chr = new Chrome({ addr: newCount() }), chrav = makeObjAval(count); | |
| chrome_obj_av_addr = newCount(); | |
| heap[chrome_obj_av_addr] = chrav; | |
| var chr2 = new Chrome({ addr: newCount() }), chr2av = makeObjAval(count); | |
| var con = new Content({ addr: newCount() }), conav = makeObjAval(count); | |
| chr.addProp("content-", { aval: chr2av, enum: false }); | |
| chr.addProp("contentDocument-", { aval: conav, enum: false }); | |
| chr.addProp("contentWindow-", { aval: chr2av, enum: false }); | |
| chr.addProp("defaultView-", { aval: conav, enum: false }); | |
| chr2.addProp("document-", { aval: conav, enum: false }); | |
| con.addProp("documentElement-", { aval: conav, enum: false }); | |
| chr.addProp("opener-", { aval: chrav, enum: false }); | |
| con.addProp("opener-", { aval: conav, enum: false }); | |
| chr.addProp("selectedBrowser-", { aval: chrav, enum: false }); | |
| ["firstChild-", "lastChild-", "nextSibling-", "top-"].forEach( | |
| function(pname) { | |
| chr.addProp(pname, { aval: chrav, enum: false }); | |
| chr2.addProp(pname, { aval: chr2av, enum: false }); | |
| con.addProp(pname, { aval: conav, enum: false }); | |
| } | |
| ); | |
| chr.addProp("parentNode-", { aval: chrav, enum: false }); | |
| chr2.addProp("parentNode-", { aval: chrav, enum: false }); | |
| con.addProp("parentNode-", { aval: conav, enum: false }); | |
| attachMethod(chr, "getBrowser", toThis, 0); | |
| ["getElementById", "appendChild", "removeChild", "createElement"].forEach( | |
| function(fname) { | |
| attachMethod(chr, fname, toThis, 1); | |
| attachMethod(con, fname, toThis, 1); | |
| } | |
| ); | |
| // chrarr is for functions that normally return a Node list of chrome elms | |
| var chrarr = new Aobj({ addr: newCount() }), chrarrav = makeObjAval(count); | |
| array_constructor([chrarrav], true); | |
| chrarr.updateNumProps(chrav); | |
| // conarr is for functions that normally return a Node list of content elms | |
| var conarr = new Aobj({ addr: newCount() }), conarrav = makeObjAval(count); | |
| array_constructor([conarrav], true); | |
| conarr.updateNumProps(conav); | |
| function toNodeList(args) { | |
| var av = BOTTOM, both = 0; | |
| args[0].forEachObjWhile(function(o) { | |
| if (o instanceof Chrome) { | |
| av = avjoin(av, chrarrav); | |
| both |= 1; | |
| } | |
| if (o instanceof Content) { | |
| av = avjoin(av, conarrav); | |
| both |= 2; | |
| } | |
| return both === 3; | |
| }); | |
| return new Ans(av); | |
| } | |
| chr.addProp("childNodes-", { aval: chrarrav, enum: false }); | |
| chr2.addProp("childNodes-", { aval: chrarrav, enum: false }); | |
| con.addProp("childNodes-", { aval: conarrav, enum: false }); | |
| [["querySelectorAll", 1], ["getElementsByClassName", 1], | |
| ["getElementsByAttribute", 2], ["getElementsByTagName", 1]].forEach( | |
| function(pair) { | |
| var p0 = pair[0], p1 = pair[1]; | |
| attachMethod(chr, p0, toNodeList, p1); | |
| attachMethod(chr2, p0, toNodeList, p1); | |
| attachMethod(con, p0, toNodeList, p1); | |
| } | |
| ); | |
| global_object_av.forEachObj(function(go) { | |
| go.addProp("content-", { aval: chr2av, enum: false }); | |
| go.addProp("contentWindow-", { aval: chr2av, enum: false }); | |
| go.addProp("document-", { aval: chrav, enum: false }); | |
| go.addProp("gBrowser-", { aval: chrav, enum: false }); | |
| go.addProp("opener-", { aval: chrav, enum: false }); | |
| go.addProp("window-", { aval: chrav, enum: false }); | |
| }); | |
| function aEL(args, withNew, callNode) { | |
| // oldst can be undefined | |
| function evtjoin(oldst, newst) { | |
| if (oldst) { | |
| if (oldst[0] !== newst[0]) newst[0] = "unknown-"; | |
| if (oldst[1] !== newst[1]) newst[1] = "any"; | |
| if (oldst[2] !== newst[2]) newst[2] = "any"; | |
| if (oldst[3] !== newst[3]) newst[3] = "unsafe"; | |
| } | |
| return newst; | |
| } | |
| var evt = (args[1] && args[1].getStrLit()) || "unknown-"; | |
| var ratorNode = callNode.children[0], raddr=ratorNode.addr, ec, evtkind, st; | |
| if (!ratorNode) return new Ans(BOTTOM); | |
| ec = e10sResults[raddr]; | |
| if (ec) | |
| ec.analyzed = true; | |
| else { | |
| if (!raddr) raddr = ratorNode.addr = newCount(); | |
| ec = e10sResults[raddr] = {lineno : ratorNode.lineno, | |
| analyzed : true, | |
| kind : "Attach listener", | |
| status : undefined}; | |
| } | |
| evtkind = eventKinds[evt.slice(0,-1)]; | |
| if (evtkind === XUL) | |
| st = [evt, "chrome", "chrome", "safe"]; | |
| else if (evtkind === undefined && !args[4] && evt !== "unknown-") { | |
| // listener for custom event that can't come from content | |
| st = [evt, "chrome", "chrome", "safe"]; | |
| } | |
| else { | |
| var numobjs = 0; | |
| args[0].forEachObj(function(o) { | |
| ++numobjs; | |
| if (o instanceof Chrome) { | |
| var st2 = [evt, "chrome", undefined, undefined]; | |
| if (evtkind === NO_BUBBLE) { | |
| st2[2] = "chrome"; | |
| st2[3] = "safe"; | |
| } | |
| else { | |
| st2[2] = "any"; | |
| st2[3] = "unsafe" | |
| } | |
| st = evtjoin(st, st2); | |
| } | |
| else if (o instanceof Content) { | |
| st = evtjoin(st, [evt, "content", "content", "unsafe"]); | |
| } | |
| else | |
| st = evtjoin(st, [evt, "any", "any", "unsafe"]); | |
| }); | |
| if (numobjs === 0) st = [evt, "any", "any", "unsafe"]; | |
| } | |
| ec.status = evtjoin(ec.status, st); | |
| return new Ans(BOTTOM); | |
| } | |
| var aEL_node = funToNode("addEventListener-", aEL, 3); | |
| new Aobj({ addr: count, | |
| proto : function_prototype_av, | |
| fun : aEL_node }); | |
| var aELav = makeObjAval(count); | |
| // Node, Aval, Aval -> Aval | |
| function evalExp_e10s(n, recv, av) { | |
| var recvchrome = false, recvcon = false; | |
| recv.forEachObjWhile(function(o) { | |
| if (o instanceof Chrome) | |
| recvchrome = true; | |
| else if (o instanceof Content) | |
| recvcon = true; | |
| return recvchrome && recvcon; | |
| }); | |
| if (recvchrome && !recvcon) | |
| av.forEachObjWhile(function(o) { | |
| if (o instanceof Content) { | |
| var ec = e10sResults[n.addr]; | |
| ec.analyzed = true; | |
| ec.kind = "Touch content"; | |
| return true; | |
| } | |
| }); | |
| //hideous hack for properties of chrome & content elms we don't know about | |
| if (aveq(av, AUNDEF) && (recvchrome || recvcon)) | |
| return recv; | |
| else | |
| return av; | |
| } | |
| // replace evalExp/DOT with the following function | |
| evalExp_override(DOT, function(n, fr) { | |
| var ch = n.children; | |
| var ans = evalExp(ch[0], fr), avobj = ans.v.baseToObj(); | |
| ans.thisav = avobj; // used by method calls | |
| if (ch[1].value === "addEventListener-") | |
| ans.v = aELav; | |
| else | |
| ans.v = avobj.getProp(ch[1].value); | |
| ans.v = evalExp_e10s(n, avobj, ans.v); | |
| return ans; | |
| }); | |
| evalExp_override(INDEX, function(n, fr) { | |
| var ansobj = evalExp(n.children[0], fr), avobj = ansobj.v.baseToObj(); | |
| var prop = n.children[1], errval = ansobj.err, av , ans; | |
| fr = ansobj.fr; | |
| if (prop.type === STRING) | |
| av = avobj.getProp(prop.value); | |
| else { | |
| var ansprop = evalExp(prop, fr), avprop = ansprop.v; | |
| fr = ansprop.fr; | |
| errval = maybeavjoin(errval, ansprop.err); | |
| av = BOTTOM; | |
| if (avprop.hasNum()) | |
| avobj.forEachObj(function(o) { av = avjoin(av, o.getNumProps()); }); | |
| if (avprop.hasStr()) { | |
| var slit = avprop.getStrLit(); | |
| if (slit) | |
| av = avjoin(av, avobj.getProp(slit)); | |
| else | |
| avobj.forEachObj(function(o) { av = avjoin(av, o.getStrProps()); }); | |
| } | |
| } | |
| ans = new Ans(evalExp_e10s(n, avobj, av), fr, errval); | |
| ans.thisav = avobj; | |
| return ans; | |
| }); | |
| } | |
| const XUL = 0, BUBBLE = 1, NO_BUBBLE = 2; | |
| var eventKinds = { | |
| abort : BUBBLE, | |
| blur : NO_BUBBLE, | |
| broadcast : XUL, | |
| change : BUBBLE, | |
| CheckboxStateChange : XUL, | |
| click : BUBBLE, | |
| close : XUL, | |
| command : XUL, | |
| commandupdate : XUL, | |
| contextmenu : XUL, | |
| dblclick : BUBBLE, | |
| DOMActivate : BUBBLE, | |
| DOMAttrModified : BUBBLE, | |
| DOMCharacterDataModified : BUBBLE, | |
| DOMContentLoaded : BUBBLE, | |
| DOMFocusIn : BUBBLE, | |
| DOMFocusOut : BUBBLE, | |
| DOMMenuItemActive : XUL, | |
| DOMMenuItemInactive : XUL, | |
| DOMMouseScroll : XUL, | |
| DOMNodeInserted : BUBBLE, | |
| DOMNodeInsertedIntoDocument : NO_BUBBLE, | |
| DOMNodeRemoved : BUBBLE, | |
| DOMNodeRemovedFromDocument : NO_BUBBLE, | |
| DOMSubtreeModified : BUBBLE, | |
| dragdrop : XUL, | |
| dragenter : XUL, | |
| dragexit : XUL, | |
| draggesture : XUL, | |
| dragover : XUL, | |
| error : BUBBLE, | |
| focus : NO_BUBBLE, | |
| input : XUL, | |
| keydown : BUBBLE, | |
| keypress : BUBBLE, | |
| keyup : BUBBLE, | |
| load : NO_BUBBLE, | |
| mousedown : BUBBLE, | |
| mousemove : BUBBLE, | |
| mouseout : BUBBLE, | |
| mouseover : BUBBLE, | |
| mouseup : BUBBLE, | |
| overflow : XUL, | |
| overflowchanged : XUL, | |
| pagehide: BUBBLE, | |
| pageshow: BUBBLE, | |
| popuphidden : XUL, | |
| popuphiding : XUL, | |
| popupshowing : XUL, | |
| popupshown : XUL, | |
| RadioStateChange : XUL, | |
| reset : BUBBLE, | |
| resize : BUBBLE, | |
| scroll : BUBBLE, | |
| select : BUBBLE, | |
| submit : BUBBLE, | |
| TabClose : XUL, | |
| TabOpen : XUL, | |
| TabSelect : XUL, | |
| underflow : XUL, | |
| unload : NO_BUBBLE | |
| // ViewChanged event from greasemonkey? | |
| }; | |
| function analyze_addon(ast, tmout) { | |
| print("Doing CFA"); | |
| timeout = tmout; | |
| cfa2(ast); | |
| // Addresses are irrelevant outside jscfa, convert results to array. | |
| var rs = []; | |
| for (var addr in e10sResults) { | |
| var r = e10sResults[addr]; | |
| if (r.analyzed) rs.push(r); | |
| } | |
| rs.astSize = astSize; | |
| rs.timedout = timedout; | |
| print("Done with CFA: " + timestamp); | |
| return rs; | |
| } | |
| exports.analyze_addon = analyze_addon; |