Skip to content
Permalink
master
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
/* ***** 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;