You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
547 lines
19 KiB
547 lines
19 KiB
// Type description parser
|
|
//
|
|
// Type description JSON files (such as ecma5.json and browser.json)
|
|
// are used to
|
|
//
|
|
// A) describe types that come from native code
|
|
//
|
|
// B) to cheaply load the types for big libraries, or libraries that
|
|
// can't be inferred well
|
|
|
|
(function(mod) {
|
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
|
return exports.init = mod;
|
|
if (typeof define == "function" && define.amd) // AMD
|
|
return define({init: mod});
|
|
tern.def = {init: mod};
|
|
})(function(exports, infer) {
|
|
"use strict";
|
|
|
|
function hop(obj, prop) {
|
|
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
}
|
|
|
|
var TypeParser = exports.TypeParser = function(spec, start, base, forceNew) {
|
|
this.pos = start || 0;
|
|
this.spec = spec;
|
|
this.base = base;
|
|
this.forceNew = forceNew;
|
|
};
|
|
TypeParser.prototype = {
|
|
eat: function(str) {
|
|
if (str.length == 1 ? this.spec.charAt(this.pos) == str : this.spec.indexOf(str, this.pos) == this.pos) {
|
|
this.pos += str.length;
|
|
return true;
|
|
}
|
|
},
|
|
word: function(re) {
|
|
var word = "", ch, re = re || /[\w$]/;
|
|
while ((ch = this.spec.charAt(this.pos)) && re.test(ch)) { word += ch; ++this.pos; }
|
|
return word;
|
|
},
|
|
error: function() {
|
|
throw new Error("Unrecognized type spec: " + this.spec + " (at " + this.pos + ")");
|
|
},
|
|
parseFnType: function(name, top) {
|
|
var args = [], names = [];
|
|
if (!this.eat(")")) for (var i = 0; ; ++i) {
|
|
var colon = this.spec.indexOf(": ", this.pos), argname;
|
|
if (colon != -1) {
|
|
argname = this.spec.slice(this.pos, colon);
|
|
if (/^[$\w?]+$/.test(argname))
|
|
this.pos = colon + 2;
|
|
else
|
|
argname = null;
|
|
}
|
|
names.push(argname);
|
|
args.push(this.parseType());
|
|
if (!this.eat(", ")) {
|
|
this.eat(")") || this.error();
|
|
break;
|
|
}
|
|
}
|
|
var retType, computeRet, computeRetStart, fn;
|
|
if (this.eat(" -> ")) {
|
|
if (top && this.spec.indexOf("!", this.pos) > -1) {
|
|
retType = infer.ANull;
|
|
computeRetStart = this.pos;
|
|
computeRet = this.parseRetType();
|
|
} else retType = this.parseType();
|
|
} else retType = infer.ANull;
|
|
if (top && (fn = this.base))
|
|
infer.Fn.call(this.base, name, infer.ANull, args, names, retType);
|
|
else
|
|
fn = new infer.Fn(name, infer.ANull, args, names, retType);
|
|
if (computeRet) fn.computeRet = computeRet;
|
|
if (computeRetStart != null) fn.computeRetSource = this.spec.slice(computeRetStart, this.pos);
|
|
return fn;
|
|
},
|
|
parseType: function(name, top) {
|
|
var type, union = false;
|
|
for (;;) {
|
|
var inner = this.parseTypeInner(name, top);
|
|
if (union) inner.propagate(union);
|
|
else type = inner;
|
|
if (!this.eat("|")) break;
|
|
if (!union) {
|
|
union = new infer.AVal;
|
|
type.propagate(union);
|
|
type = union;
|
|
}
|
|
}
|
|
return type;
|
|
},
|
|
parseTypeInner: function(name, top) {
|
|
if (this.eat("fn(")) {
|
|
return this.parseFnType(name, top);
|
|
} else if (this.eat("[")) {
|
|
var inner = this.parseType();
|
|
this.eat("]") || this.error();
|
|
if (top && this.base) {
|
|
infer.Arr.call(this.base, inner);
|
|
return this.base;
|
|
}
|
|
return new infer.Arr(inner);
|
|
} else if (this.eat("+")) {
|
|
var path = this.word(/[\w$<>\.!]/);
|
|
var base = parsePath(path + ".prototype");
|
|
if (!(base instanceof infer.Obj)) base = parsePath(path);
|
|
if (!(base instanceof infer.Obj)) return base;
|
|
if (top && this.forceNew) return new infer.Obj(base);
|
|
return infer.getInstance(base);
|
|
} else if (this.eat("?")) {
|
|
return infer.ANull;
|
|
} else {
|
|
return this.fromWord(this.word(/[\w$<>\.!`]/));
|
|
}
|
|
},
|
|
fromWord: function(spec) {
|
|
var cx = infer.cx();
|
|
switch (spec) {
|
|
case "number": return cx.num;
|
|
case "string": return cx.str;
|
|
case "bool": return cx.bool;
|
|
case "<top>": return cx.topScope;
|
|
}
|
|
if (cx.localDefs && spec in cx.localDefs) return cx.localDefs[spec];
|
|
return parsePath(spec);
|
|
},
|
|
parseBaseRetType: function() {
|
|
if (this.eat("[")) {
|
|
var inner = this.parseRetType();
|
|
this.eat("]") || this.error();
|
|
return function(self, args) { return new infer.Arr(inner(self, args)); };
|
|
} else if (this.eat("+")) {
|
|
var base = this.parseRetType();
|
|
var result = function(self, args) {
|
|
var proto = base(self, args);
|
|
if (proto instanceof infer.Fn && proto.hasProp("prototype"))
|
|
proto = proto.getProp("prototype").getObjType();
|
|
if (!(proto instanceof infer.Obj)) return proto;
|
|
return new infer.Obj(proto);
|
|
};
|
|
if (this.eat("[")) return this.parsePoly(result);
|
|
return result;
|
|
} else if (this.eat("!")) {
|
|
var arg = this.word(/\d/);
|
|
if (arg) {
|
|
arg = Number(arg);
|
|
return function(_self, args) {return args[arg] || infer.ANull;};
|
|
} else if (this.eat("this")) {
|
|
return function(self) {return self;};
|
|
} else if (this.eat("custom:")) {
|
|
var fname = this.word(/[\w$]/);
|
|
return customFunctions[fname] || function() { return infer.ANull; };
|
|
} else {
|
|
return this.fromWord("!" + arg + this.word(/[\w$<>\.!]/));
|
|
}
|
|
}
|
|
var t = this.parseType();
|
|
return function(){return t;};
|
|
},
|
|
extendRetType: function(base) {
|
|
var propName = this.word(/[\w<>$!]/) || this.error();
|
|
if (propName == "!ret") return function(self, args) {
|
|
var lhs = base(self, args);
|
|
if (lhs.retval) return lhs.retval;
|
|
var rv = new infer.AVal;
|
|
lhs.propagate(new infer.IsCallee(infer.ANull, [], null, rv));
|
|
return rv;
|
|
};
|
|
return function(self, args) {return base(self, args).getProp(propName);};
|
|
},
|
|
parsePoly: function(base) {
|
|
var propName = "<i>", match;
|
|
if (match = this.spec.slice(this.pos).match(/^\s*(\w+)\s*=\s*/)) {
|
|
propName = match[1];
|
|
this.pos += match[0].length;
|
|
}
|
|
var value = this.parseRetType();
|
|
if (!this.eat("]")) this.error();
|
|
return function(self, args) {
|
|
var instance = base(self, args);
|
|
if (instance instanceof infer.Obj)
|
|
value(self, args).propagate(instance.defProp(propName));
|
|
return instance;
|
|
};
|
|
},
|
|
parseRetType: function() {
|
|
var tp = this.parseBaseRetType();
|
|
while (this.eat(".")) tp = this.extendRetType(tp);
|
|
return tp;
|
|
}
|
|
};
|
|
|
|
function parseType(spec, name, base, forceNew) {
|
|
var type = new TypeParser(spec, null, base, forceNew).parseType(name, true);
|
|
if (/^fn\(/.test(spec)) for (var i = 0; i < type.args.length; ++i) (function(i) {
|
|
var arg = type.args[i];
|
|
if (arg instanceof infer.Fn && arg.args && arg.args.length) addEffect(type, function(_self, fArgs) {
|
|
var fArg = fArgs[i];
|
|
if (fArg) fArg.propagate(new infer.IsCallee(infer.cx().topScope, arg.args, null, infer.ANull));
|
|
});
|
|
})(i);
|
|
return type;
|
|
}
|
|
|
|
function addEffect(fn, handler, replaceRet) {
|
|
var oldCmp = fn.computeRet, rv = fn.retval;
|
|
fn.computeRet = function(self, args, argNodes) {
|
|
var handled = handler(self, args, argNodes);
|
|
var old = oldCmp ? oldCmp(self, args, argNodes) : rv;
|
|
return replaceRet ? handled : old;
|
|
};
|
|
}
|
|
|
|
var parseEffect = exports.parseEffect = function(effect, fn) {
|
|
var m;
|
|
if (effect.indexOf("propagate ") == 0) {
|
|
var p = new TypeParser(effect, 10);
|
|
var getOrigin = p.parseRetType();
|
|
if (!p.eat(" ")) p.error();
|
|
var getTarget = p.parseRetType();
|
|
addEffect(fn, function(self, args) {
|
|
getOrigin(self, args).propagate(getTarget(self, args));
|
|
});
|
|
} else if (effect.indexOf("call ") == 0) {
|
|
var andRet = effect.indexOf("and return ", 5) == 5;
|
|
var p = new TypeParser(effect, andRet ? 16 : 5);
|
|
var getCallee = p.parseRetType(), getSelf = null, getArgs = [];
|
|
if (p.eat(" this=")) getSelf = p.parseRetType();
|
|
while (p.eat(" ")) getArgs.push(p.parseRetType());
|
|
addEffect(fn, function(self, args) {
|
|
var callee = getCallee(self, args);
|
|
var slf = getSelf ? getSelf(self, args) : infer.ANull, as = [];
|
|
for (var i = 0; i < getArgs.length; ++i) as.push(getArgs[i](self, args));
|
|
var result = andRet ? new infer.AVal : infer.ANull;
|
|
callee.propagate(new infer.IsCallee(slf, as, null, result));
|
|
return result;
|
|
}, andRet);
|
|
} else if (m = effect.match(/^custom (\S+)\s*(.*)/)) {
|
|
var customFunc = customFunctions[m[1]];
|
|
if (customFunc) addEffect(fn, m[2] ? customFunc(m[2]) : customFunc);
|
|
} else if (effect.indexOf("copy ") == 0) {
|
|
var p = new TypeParser(effect, 5);
|
|
var getFrom = p.parseRetType();
|
|
p.eat(" ");
|
|
var getTo = p.parseRetType();
|
|
addEffect(fn, function(self, args) {
|
|
var from = getFrom(self, args), to = getTo(self, args);
|
|
from.forAllProps(function(prop, val, local) {
|
|
if (local && prop != "<i>")
|
|
to.propagate(new infer.PropHasSubset(prop, val));
|
|
});
|
|
});
|
|
} else {
|
|
throw new Error("Unknown effect type: " + effect);
|
|
}
|
|
};
|
|
|
|
var currentTopScope;
|
|
|
|
var parsePath = exports.parsePath = function(path, scope) {
|
|
var cx = infer.cx(), cached = cx.paths[path], origPath = path;
|
|
if (cached != null) return cached;
|
|
cx.paths[path] = infer.ANull;
|
|
|
|
var base = scope || currentTopScope || cx.topScope;
|
|
|
|
if (cx.localDefs) for (var name in cx.localDefs) {
|
|
if (path.indexOf(name) == 0) {
|
|
if (path == name) return cx.paths[path] = cx.localDefs[path];
|
|
if (path.charAt(name.length) == ".") {
|
|
base = cx.localDefs[name];
|
|
path = path.slice(name.length + 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
var parts = path.split(".");
|
|
for (var i = 0; i < parts.length && base != infer.ANull; ++i) {
|
|
var prop = parts[i];
|
|
if (prop.charAt(0) == "!") {
|
|
if (prop == "!proto") {
|
|
base = (base instanceof infer.Obj && base.proto) || infer.ANull;
|
|
} else {
|
|
var fn = base.getFunctionType();
|
|
if (!fn) {
|
|
base = infer.ANull;
|
|
} else if (prop == "!ret") {
|
|
base = fn.retval && fn.retval.getType(false) || infer.ANull;
|
|
} else {
|
|
var arg = fn.args && fn.args[Number(prop.slice(1))];
|
|
base = (arg && arg.getType(false)) || infer.ANull;
|
|
}
|
|
}
|
|
} else if (base instanceof infer.Obj) {
|
|
var propVal = (prop == "prototype" && base instanceof infer.Fn) ? base.getProp(prop) : base.props[prop];
|
|
if (!propVal || propVal.isEmpty())
|
|
base = infer.ANull;
|
|
else
|
|
base = propVal.types[0];
|
|
}
|
|
}
|
|
// Uncomment this to get feedback on your poorly written .json files
|
|
// if (base == infer.ANull) console.error("bad path: " + origPath + " (" + cx.curOrigin + ")");
|
|
cx.paths[origPath] = base == infer.ANull ? null : base;
|
|
return base;
|
|
};
|
|
|
|
function emptyObj(ctor) {
|
|
var empty = Object.create(ctor.prototype);
|
|
empty.props = Object.create(null);
|
|
empty.isShell = true;
|
|
return empty;
|
|
}
|
|
|
|
function isSimpleAnnotation(spec) {
|
|
if (!spec["!type"] || /^(fn\(|\[)/.test(spec["!type"])) return false;
|
|
for (var prop in spec)
|
|
if (prop != "!type" && prop != "!doc" && prop != "!url" && prop != "!span" && prop != "!data")
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
function passOne(base, spec, path) {
|
|
if (!base) {
|
|
var tp = spec["!type"];
|
|
if (tp) {
|
|
if (/^fn\(/.test(tp)) base = emptyObj(infer.Fn);
|
|
else if (tp.charAt(0) == "[") base = emptyObj(infer.Arr);
|
|
else throw new Error("Invalid !type spec: " + tp);
|
|
} else if (spec["!stdProto"]) {
|
|
base = infer.cx().protos[spec["!stdProto"]];
|
|
} else {
|
|
base = emptyObj(infer.Obj);
|
|
}
|
|
base.name = path;
|
|
}
|
|
|
|
for (var name in spec) if (hop(spec, name) && name.charCodeAt(0) != 33) {
|
|
var inner = spec[name];
|
|
if (typeof inner == "string" || isSimpleAnnotation(inner)) continue;
|
|
var prop = base.defProp(name);
|
|
passOne(prop.getObjType(), inner, path ? path + "." + name : name).propagate(prop);
|
|
}
|
|
return base;
|
|
}
|
|
|
|
function passTwo(base, spec, path) {
|
|
if (base.isShell) {
|
|
delete base.isShell;
|
|
var tp = spec["!type"];
|
|
if (tp) {
|
|
parseType(tp, path, base);
|
|
} else {
|
|
var proto = spec["!proto"] && parseType(spec["!proto"]);
|
|
infer.Obj.call(base, proto instanceof infer.Obj ? proto : true, path);
|
|
}
|
|
}
|
|
|
|
var effects = spec["!effects"];
|
|
if (effects && base instanceof infer.Fn) for (var i = 0; i < effects.length; ++i)
|
|
parseEffect(effects[i], base);
|
|
copyInfo(spec, base);
|
|
|
|
for (var name in spec) if (hop(spec, name) && name.charCodeAt(0) != 33) {
|
|
var inner = spec[name], known = base.defProp(name), innerPath = path ? path + "." + name : name;
|
|
if (typeof inner == "string") {
|
|
if (known.isEmpty()) parseType(inner, innerPath).propagate(known);
|
|
} else {
|
|
if (!isSimpleAnnotation(inner))
|
|
passTwo(known.getObjType(), inner, innerPath);
|
|
else if (known.isEmpty())
|
|
parseType(inner["!type"], innerPath, null, true).propagate(known);
|
|
else
|
|
continue;
|
|
if (inner["!doc"]) known.doc = inner["!doc"];
|
|
if (inner["!url"]) known.url = inner["!url"];
|
|
if (inner["!span"]) known.span = inner["!span"];
|
|
}
|
|
}
|
|
return base;
|
|
}
|
|
|
|
function copyInfo(spec, type) {
|
|
if (spec["!doc"]) type.doc = spec["!doc"];
|
|
if (spec["!url"]) type.url = spec["!url"];
|
|
if (spec["!span"]) type.span = spec["!span"];
|
|
if (spec["!data"]) type.metaData = spec["!data"];
|
|
}
|
|
|
|
function runPasses(type, arg) {
|
|
var parent = infer.cx().parent, pass = parent && parent.passes && parent.passes[type];
|
|
if (pass) for (var i = 0; i < pass.length; i++) pass[i](arg);
|
|
}
|
|
|
|
function doLoadEnvironment(data, scope) {
|
|
var cx = infer.cx();
|
|
|
|
infer.addOrigin(cx.curOrigin = data["!name"] || "env#" + cx.origins.length);
|
|
cx.localDefs = cx.definitions[cx.curOrigin] = Object.create(null);
|
|
|
|
runPasses("preLoadDef", data);
|
|
|
|
passOne(scope, data);
|
|
|
|
var def = data["!define"];
|
|
if (def) {
|
|
for (var name in def) {
|
|
var spec = def[name];
|
|
cx.localDefs[name] = typeof spec == "string" ? parsePath(spec) : passOne(null, spec, name);
|
|
}
|
|
for (var name in def) {
|
|
var spec = def[name];
|
|
if (typeof spec != "string") passTwo(cx.localDefs[name], def[name], name);
|
|
}
|
|
}
|
|
|
|
passTwo(scope, data);
|
|
|
|
runPasses("postLoadDef", data);
|
|
|
|
cx.curOrigin = cx.localDefs = null;
|
|
}
|
|
|
|
exports.load = function(data, scope) {
|
|
if (!scope) scope = infer.cx().topScope;
|
|
var oldScope = currentTopScope;
|
|
currentTopScope = scope;
|
|
try {
|
|
doLoadEnvironment(data, scope);
|
|
} finally {
|
|
currentTopScope = oldScope;
|
|
}
|
|
};
|
|
|
|
exports.parse = function(data, origin, path) {
|
|
var cx = infer.cx();
|
|
if (origin) {
|
|
cx.origin = origin;
|
|
cx.localDefs = cx.definitions[origin];
|
|
}
|
|
|
|
try {
|
|
if (typeof data == "string")
|
|
return parseType(data, path);
|
|
else
|
|
return passTwo(passOne(null, data, path), data, path);
|
|
} finally {
|
|
if (origin) cx.origin = cx.localDefs = null;
|
|
}
|
|
};
|
|
|
|
// Used to register custom logic for more involved effect or type
|
|
// computation.
|
|
var customFunctions = Object.create(null);
|
|
infer.registerFunction = function(name, f) { customFunctions[name] = f; };
|
|
|
|
var IsCreated = infer.constraint("created, target, spec", {
|
|
addType: function(tp) {
|
|
if (tp instanceof infer.Obj && this.created++ < 5) {
|
|
var derived = new infer.Obj(tp), spec = this.spec;
|
|
if (spec instanceof infer.AVal) spec = spec.getObjType(false);
|
|
if (spec instanceof infer.Obj) for (var prop in spec.props) {
|
|
var cur = spec.props[prop].types[0];
|
|
var p = derived.defProp(prop);
|
|
if (cur && cur instanceof infer.Obj && cur.props.value) {
|
|
var vtp = cur.props.value.getType(false);
|
|
if (vtp) p.addType(vtp);
|
|
}
|
|
}
|
|
this.target.addType(derived);
|
|
}
|
|
}
|
|
});
|
|
|
|
infer.registerFunction("Object_create", function(_self, args, argNodes) {
|
|
if (argNodes && argNodes.length && argNodes[0].type == "Literal" && argNodes[0].value == null)
|
|
return new infer.Obj();
|
|
|
|
var result = new infer.AVal;
|
|
if (args[0]) args[0].propagate(new IsCreated(0, result, args[1]));
|
|
return result;
|
|
});
|
|
|
|
var PropSpec = infer.constraint("target", {
|
|
addType: function(tp) {
|
|
if (!(tp instanceof infer.Obj)) return;
|
|
if (tp.hasProp("value"))
|
|
tp.getProp("value").propagate(this.target);
|
|
else if (tp.hasProp("get"))
|
|
tp.getProp("get").propagate(new infer.IsCallee(infer.ANull, [], null, this.target));
|
|
}
|
|
});
|
|
|
|
infer.registerFunction("Object_defineProperty", function(_self, args, argNodes) {
|
|
if (argNodes && argNodes.length >= 3 && argNodes[1].type == "Literal" &&
|
|
typeof argNodes[1].value == "string") {
|
|
var obj = args[0], connect = new infer.AVal;
|
|
obj.propagate(new infer.PropHasSubset(argNodes[1].value, connect, argNodes[1]));
|
|
args[2].propagate(new PropSpec(connect));
|
|
}
|
|
return infer.ANull;
|
|
});
|
|
|
|
var IsBound = infer.constraint("self, args, target", {
|
|
addType: function(tp) {
|
|
if (!(tp instanceof infer.Fn)) return;
|
|
this.target.addType(new infer.Fn(tp.name, tp.self, tp.args.slice(this.args.length),
|
|
tp.argNames.slice(this.args.length), tp.retval));
|
|
this.self.propagate(tp.self);
|
|
for (var i = 0; i < Math.min(tp.args.length, this.args.length); ++i)
|
|
this.args[i].propagate(tp.args[i]);
|
|
}
|
|
});
|
|
|
|
infer.registerFunction("Function_bind", function(self, args) {
|
|
if (!args.length) return infer.ANull;
|
|
var result = new infer.AVal;
|
|
self.propagate(new IsBound(args[0], args.slice(1), result));
|
|
return result;
|
|
});
|
|
|
|
infer.registerFunction("Array_ctor", function(_self, args) {
|
|
var arr = new infer.Arr;
|
|
if (args.length != 1 || !args[0].hasType(infer.cx().num)) {
|
|
var content = arr.getProp("<i>");
|
|
for (var i = 0; i < args.length; ++i) args[i].propagate(content);
|
|
}
|
|
return arr;
|
|
});
|
|
|
|
infer.registerFunction("Promise_ctor", function(_self, args, argNodes) {
|
|
if (args.length < 1) return infer.ANull;
|
|
var self = new infer.Obj(infer.cx().definitions.ecma6["Promise.prototype"]);
|
|
var valProp = self.defProp("value", argNodes && argNodes[0]);
|
|
var valArg = new infer.AVal;
|
|
valArg.propagate(valProp);
|
|
var exec = new infer.Fn("execute", infer.ANull, [valArg], ["value"], infer.ANull);
|
|
var reject = infer.cx().definitions.ecma6.promiseReject;
|
|
args[0].propagate(new infer.IsCallee(infer.ANull, [exec, reject], null, infer.ANull));
|
|
return self;
|
|
});
|
|
|
|
return exports;
|
|
});
|
|
|