TouchDevelop/ast/typeChecker.ts

2182 строки
87 KiB
TypeScript
Исходник Ответственный История

Этот файл содержит невидимые символы Юникода!

Этот файл содержит невидимые символы Юникода, которые могут быть отображены не так, как показано ниже. Если это намеренно, можете спокойно проигнорировать это предупреждение. Используйте кнопку Экранировать, чтобы показать скрытые символы.

///<reference path='refs.ts'/>
// TODO events and async
// Next available error: TD211:
module TDev.AST
{
var innerAsyncWarning = false;
export class TypeResolver
extends NodeVisitor
{
constructor(public parent:TypeChecker)
{
super()
}
private fixupKind(n:AstNode, k:Kind)
{
if (k instanceof UnresolvedKind && this.parent.topApp) {
var k0 = this.parent.topApp.getDefinedKinds().filter(n => n.getName() == k.getName())[0]
if (k0) k = k0
}
if (k.getRoot() != k) {
var pk = <ParametricKind>k
var parms = pk.parameters.map(kk => this.fixupKind(n, kk))
if (parms.some((kk, i) => pk.parameters[i] != kk)) {
k = pk.createInstance(parms)
}
}
if (k.isError()) {
if (k instanceof LibraryRefAbstractKind || k.getRecord() instanceof LibraryRecordDef) {
if (k.parentLibrary().hasAbstractKind(k.getName())) {
k = k.parentLibrary().getAbstractKind(k.getName())
}
}
}
if (k.isError()) {
this.parent.errorCount++;
n.setError(lf("TD121: cannot find type {0}", k.toString()));
}
return k;
}
visitGlobalDef(node:GlobalDef)
{
node._kind = this.fixupKind(node, node.getKind());
}
visitAction(node:Action)
{
var fixupLocal = (a:ActionParameter) => {
var l = a.local;
l._kind = this.fixupKind(node, l.getKind());
}
if (node.modelParameter)
fixupLocal(node.modelParameter);
node.getInParameters().forEach(fixupLocal);
node.getOutParameters().forEach(fixupLocal);
}
visitBlock(b:Block)
{
this.visitChildren(b);
}
visitRecordDef(r:RecordDef)
{
this.visitChildren(r);
}
visitRecordField(node:RecordField)
{
node.setKind(this.fixupKind(node, node.dataKind))
}
visitApp(a:App)
{
this.visitChildren(a);
}
}
export enum ActionSection {
Normal,
Init,
Display,
Lambda,
}
export class TypeChecker
extends NodeVisitor
{
private typeResolver:TypeResolver;
private isTopExpr = false;
static lastStoreLocalsAt:ExprHolder;
constructor() {
super()
this.typeResolver = new TypeResolver(this);
}
static tcAction(a:Action, first:boolean, storeAt:ExprHolder = null)
{
var ctx = new TypeChecker();
ctx.topApp = Script;
ctx.storeLocalsAt = storeAt;
TypeChecker.lastStoreLocalsAt = storeAt;
if (first) {
TypeChecker.iterTokenUsage((t:TokenUsage) => { t.localCount = 0 });
ctx.countToUpdate = 1;
}
ctx.dispatch(a);
}
static tcFragment(e: ExprHolder)
{
var ctx = new TypeChecker();
ctx.topApp = Script;
ctx.storeLocalsAt = e;
TypeChecker.lastStoreLocalsAt = e;
var es = mkExprStmt(e);
ctx.dispatch(es);
}
static tcScript(a:App, ignoreLibErrors = false, isTop = false, depth = 0) : number
{
var ctx = new TypeChecker();
ctx.topApp = a;
a.imports = new AppImports();
var prev = Script;
ctx.storeLocalsAt = TypeChecker.lastStoreLocalsAt;
ctx.ignoreLibErrors = ignoreLibErrors;
try {
setGlobalScript(a);
ctx.typecheckLibraries(a);
// this may be too often
a.libNamespaceCache.recompute();
ctx.typeResolver.dispatch(a);
if (isTop) {
TypeChecker.iterTokenUsage((t:TokenUsage) => { t.globalCount = 0 });
ctx.countToUpdate = 2;
}
ctx.dispatch(a);
a.addFeatures(LanguageFeature.Refs); // refs is fixed after one round
if (ctx.numFixes > 0 && depth < 5) {
// retry
return TypeChecker.tcScript(a, ignoreLibErrors, isTop, depth + 1);
}
a.addFeatures(LanguageFeature.Current);
a.things.forEach((t) => {
if (ignoreLibErrors && t instanceof LibraryRef) {
(<LibraryRef>t)._hasErrors = false
return;
}
if (t.hasErrors()) ctx.errorCount++;
});
return ctx.errorCount;
} finally {
setGlobalScript(prev); // rebinds "data", "code" etc.
}
}
static tcApp(a:App) : number
{
return TypeChecker.tcScript(a, false, true);
}
static iterTokenUsage(f:(t:TokenUsage)=>void)
{
TDev.api.getKinds().forEach((k:Kind) => {
if (!!k.singleton) {
f(k.singleton.usage);
}
k.listProperties().forEach((p:IProperty) => {
f(p.getUsage());
});
});
TDev.api.getKind("code").singleton.usage.apiFreq = TDev.Script.actions().length > 0 ? 1.0 : 0.0;
TDev.api.getKind("data").singleton.usage.apiFreq = TDev.Script.variables().length > 0 ? 0.9 : 0.0;
TDev.api.getKind("art").singleton.usage.apiFreq = TDev.Script.resources().length > 0 ? 0.8 : 0.0;
api.core.allStmtUsage().forEach(f);
}
static resolveKind(k:Kind)
{
var ctx = new TypeChecker();
ctx.topApp = Script;
var a = new GlobalDef()
a._kind = k
ctx.typeResolver.dispatch(a);
return a._kind
}
public topApp:App;
private currentAction:Action;
private currentAnyAction:Stmt;
private seenAwait = false;
private numFixes = 0;
private nothingLocals:LocalDef[] = [];
private localScopes: LocalDef[][] = [[]];
private readOnlyLocals:LocalDef[] = [];
private invisibleLocals:LocalDef[] = [];
private writtenLocals:LocalDef[] = [];
private currLoop:Stmt;
private inAtomic = false;
private inShim = false;
private actionSection = ActionSection.Normal;
private pageInit:Block;
private pageDisplay:Block;
private saveFixes = 0;
private outLocals:LocalDef[] = [];
private reportedUnassigned = false;
private allLocals :LocalDef[] = [];
private recentErrors: string[] = [];
private lastStmt:Stmt;
public errorCount = 0;
public countToUpdate = 0;
private timestamp = 1;
private seenCurrent = false;
private missingLocals:LocalDef[] = [];
private errorLevel = 0;
private hintsToFlush:string[] = [];
private topInline:InlineActions;
public storeLocalsAt:ExprHolder = null;
public ignoreLibErrors = false;
private typeHint:Kind;
// TSBUG should be private
public markError(expr:Token, msg:string)
{
if (!!expr.getError()) return;
if (this.errorLevel > 0) return;
expr.setError(msg);
this.recentErrors.push(msg);
this.errorCount++;
var loc = (<Expr>expr).loc;
if (loc) {
var i = loc.beg;
var e = i + loc.len;
while (i < e) {
var t = loc.tokens[i]
if (!t.getError()) t.setError(msg);
i++;
}
}
}
private markHolderError(eh:ExprHolder, msg:string)
{
if (!!eh.getError()) return;
if (!msg) return;
if (this.errorLevel > 0) return;
this.errorCount++;
eh.setError(msg);
}
private setNodeError(n:AstNode, msg:string)
{
if (this.errorLevel == 0) {
this.errorCount++;
n.setError(msg)
}
}
private typeCheck(expr:Stmt)
{
expr.clearError();
expr._kind = null;
this.dispatch(expr);
}
private snapshotLocals()
{
var locs = [];
this.localScopes.forEach((l) => { locs.pushRange(l); });
return locs;
}
public tcTokens(toks:Token[])
{
var e = new ExprHolder();
e.tokens = toks
this.expect(e, null, "void");
}
private expectsMessage(whoExpects:string, tp:string)
{
switch (whoExpects) {
case "if": return lf("TD118: 'if' condition wants {1:a}", whoExpects, tp);
case "where": return lf("TD128: 'where' condition wants {1:a}", whoExpects, tp);
case "while": return lf("TD129: 'while' condition wants {1:a}", whoExpects, tp);
case "for": return lf("TD130: bound of 'for' wants {1:a}", whoExpects, tp);
case "optional": return lf("TD186: this optional parameter wants {1:a}", whoExpects, tp);
case "return": return lf("TD204: 'return' value wants {1:a}", whoExpects, tp);
default: Util.die()
}
}
private expect(expr:ExprHolder, tp:Kind, whoExpects:string)
{
this.hintsToFlush = [];
if (expr.tokens.length == 1) {
if (expr.isPlaceholder()) expr.tokens = [];
}
if (this.topApp.featureMissing(LanguageFeature.AllAsync) &&
expr.tokens.some(t => t.getOperator() == 'await'))
{
expr.tokens = expr.tokens.filter(t => t.getOperator() != 'await')
this.numFixes++;
}
expr.clearError();
expr.tokens.forEach((t, i) => {
if (!(t instanceof ThingRef)) return
var tt = <ThingRef>t
if (tt.forceLocal) return
if (!(expr.tokens[i + 1] instanceof PropertyRef)) return
var pp = <PropertyRef>expr.tokens[i + 1]
var key = tt.data + "->" + pp.data
if (AST.crossKindRenames.hasOwnProperty(key)) {
var m = /(.*)->(.*)/.exec(AST.crossKindRenames[key])
tt.data = m[1]
pp.data = m[2]
}
})
var parsed = ExprParser.parse1(expr.tokens);
parsed.clearError();
parsed._kind = null;
expr.parsed = parsed;
this.seenAwait = false;
this.recentErrors = [];
var parseErr:Token = expr.tokens.filter((n:Token) => n.getError() != null)[0];
var prevLocals = this.allLocals.length;
this.saveFixes = expr == this.storeLocalsAt ? 1 : 0;
if (!parseErr) {
this.isTopExpr = (whoExpects == "void");
this.dispatch(parsed);
expr.hasFix = this.saveFixes > 1
} else {
expr.hasFix = true
if (this.errorLevel == 0)
this.errorCount++;
this.errorLevel++;
this.dispatch(parsed);
this.errorLevel--;
}
this.saveFixes = 0
expr._kind = expr.parsed.getKind();
var seenAssign = false;
var prevTok = null
var tokErr:Token = null
expr.tokens.forEach((t) => {
if (t instanceof PropertyRef) {
var p = <PropertyRef>t;
if (!p.prop)
p.prop = p.getOrMakeProp();
if (prevTok && prevTok.getThing() instanceof LocalDef && prevTok.getThing().getName() == modelSymbol &&
AST.mkFakeCall(p).referencedRecordField())
p.skipArrow = true;
else if (p.skipArrow)
p.skipArrow = false;
} else if (t instanceof ThingRef) {
var tt = <ThingRef>t;
if (tt._lastTypechecker != this)
this.dispatch(tt);
} else if (t.getOperator() == ":=") {
seenAssign = true;
}
else if (t instanceof Literal) {
if (!t._kind) {
this.dispatch(t);
}
}
prevTok = t
if (!tokErr && t.getError() != null)
tokErr = t
})
if (whoExpects == "void") {
if (seenAssign && (this.allLocals.length == prevLocals ||
!this.allLocals.slice(prevLocals).some(l => l.isRegular)))
seenAssign = false;
expr.looksLikeVarDef = seenAssign;
}
var errNode:AstNode = parseErr || tokErr || expr.parsed;
this.markHolderError(expr, errNode.getError());
this.markHolderError(expr, this.recentErrors[0]);
expr.isAwait = this.seenAwait;
if (expr == this.storeLocalsAt) {
this.seenCurrent = true;
expr.locals = this.snapshotLocals();
} else {
expr.locals = null;
}
if (!Util.check(expr.getKind() != null, "expr type unset")) {
expr._kind = api.core.Unknown
}
if (!expr.getError() && tp != null && !expr.getKind().equals(tp)) {
var msg = this.expectsMessage(whoExpects, tp.toString())
if (expr.getKind() != this.core.Unknown)
msg += lf(", got {0:a} instead", expr.getKind().toString())
this.markHolderError(expr, msg);
}
if (!expr.getError() && parsed instanceof AST.Call) {
var call = <AST.Call> parsed;
if (tp == null && whoExpects == "void") {
var par = call.prop().parentKind
var act = call.anyCalledAction()
if (AST.legacyMode &&
par == this.core.Number &&
call.prop().getName() == "=")
expr.hint = lf("the '=' comparison has no effect here; did you mean assignment ':=' instead?");
else if (expr.getKind() != this.core.Nothing && expr.getKind() != this.core.Unknown && expr.getKind().getRoot() != this.core.Task)
// call.calledAction() == null)
// (par == core.Number || call.args.length == 0))
{
var exception = !!(call.prop().getFlags() & PropertyFlags.IgnoreReturnValue);
var k = expr.getKind()
var msg = ""
if (k.hasContext(KindContext.Parameter))
msg = lf("tap 'store in var' to store it in a variable or select a property on it");
else
msg = lf("now you can select a property on it");
if (call.prop() instanceof LibraryRef)
expr.hint =
lf("we have a library '{0}' here; {1}", call.prop().getName(), msg)
else if ((par.isBuiltin || call.args.length == 1) && !exception)
expr.hint =
lf("'{0}' returns a '{1}'; {2}",
call.prop().getName(), call.getKind(), msg)
else if (!exception) {
/*
switch (expr.getKind().getName()) {
case "Board":
case "Sprite Set":
case "Sprite":
case "Json Object":
case "Xml Object":
case "Json Builder":
case "Form Builder":
case "Web Request":
case "Web Response":
case "OAuth Response":
msgIt = storeIt; break;
}
*/
expr.hint =
lf("'{0}' returns a '{1}'; {2}",
call.prop().getName(), call.getKind(), msg)
}
}
else if (act && act.getOutParameters().length > 0)
expr.hint =
lf("'{0}' returns {1} values; you can use 'store in var' button to save them to locals",
call.prop().getName(), act.getOutParameters().length)
}
} else if (tp == null && whoExpects == "void") {
var k = expr.getKind();
if (k != this.core.Nothing && k != this.core.Unknown) {
if (k.singleton)
expr.hint =
lf("now you can select a property of {0}; it doesn't do anything by itself", k)
else
expr.hint =
lf("we have {0:a} here; did you want to do something with it?", k)
}
}
if (this.hintsToFlush.length > 0) {
var hh = this.hintsToFlush.join("\n")
if (expr.hint) expr.hint += "\n" + hh
else expr.hint = hh
this.hintsToFlush = [];
}
if (expr.assignmentInfo() && expr.assignmentInfo().fixContextError && expr.tokens[1].getOperator() == ":=") {
this.numFixes++;
this.nothingLocals.push(<LocalDef>expr.tokens[0].getThing());
expr.tokens.splice(0, 2);
}
if (this.nothingLocals.length > 0 && expr.tokens.length == 1 &&
this.nothingLocals.indexOf(<LocalDef>expr.tokens[0].getThing()) >= 0) {
expr.tokens = []
this.numFixes++;
}
if (this.topApp.featureMissing(LanguageFeature.Refs) &&
expr.tokens.some(t => !!t.tokenFix))
{
var t0 = expr.tokens.filter(t => t.tokenFix == ":=")[0]
if (t0) {
var idx = expr.tokens.indexOf(t0)
if (expr.tokens[idx + 1] && expr.tokens[idx + 1].getOperator() == "(") {
expr.tokens[idx] = mkOp(":=");
expr.tokens.splice(idx + 1, 1)
if (expr.tokens.peek().getOperator() == ")")
expr.tokens.pop()
}
}
expr.tokens = expr.tokens.filter(t => t.tokenFix != "delete")
this.numFixes++;
}
}
private declareLocal(v:LocalDef)
{
this.localScopes.peek().push(v);
this.allLocals.push(v);
v.lastUsedAt = this.timestamp++;
}
private lookupSymbol(t:ThingRef) : AST.Decl
{
var n = t.data;
for (var i = this.localScopes.length - 1; i >= 0; i--) {
var s = this.localScopes[i];
for (var j = s.length - 1; j >= 0; j--)
if (s[j].getName() === n)
return s[j];
}
if (n == "...") n = "$skip";
if (t.forceLocal) return undefined;
return TDev.api.getThing(n);
}
private scope(f : () => any)
{
this.localScopes.push(<LocalDef[]>[]); // STRBUG: this cast shouldn't be needed
var r = f();
this.localScopes.pop();
return r;
}
private conditionalScope(f: () => any)
{
var prevWritten = this.writtenLocals.slice(0);
var prevLoop = this.currLoop
try {
return this.scope(f);
} finally {
this.writtenLocals = prevWritten;
this.currLoop = prevLoop
}
}
private core = TDev.api.core;
///////////////////////////////////////////////////////////////////////////////////////////////
// Visitors
///////////////////////////////////////////////////////////////////////////////////////////////
public visitAstNode(node:AstNode)
{
Util.oops("typechecking " + node.nodeType() + " not implemented!");
}
public visitAnyIf(node:If)
{
this.updateStmtUsage(node);
this.expect(node.rawCondition, this.core.Boolean, "if");
var isComment = node.isTopCommentedOut()
if (isComment) {
var vis = new ClearErrors()
vis.dispatch(node.rawThenBody)
}
var prevWritten = this.writtenLocals.slice(0);
if (node.rawThenBody == this.pageInit)
this.actionSection = ActionSection.Init;
else if (node.rawThenBody == this.pageDisplay)
this.actionSection = ActionSection.Display;
if (isComment)
this.errorLevel++
this.typeCheck(node.rawThenBody);
node.rawThenBody.newlyWrittenLocals = this.writtenLocals.slice(prevWritten.length);
if (node.rawElseBody.stmts.length == 1 &&
node.rawElseBody.stmts[0] instanceof If) {
var ei = <If>node.rawElseBody.stmts[0]
ei.isElseIf = true;
node.setElse(Parser.emptyBlock())
var bl = node.parentBlock()
var idx = bl.stmts.indexOf(node)
bl.stmts.splice(idx + 1, 0, ei)
bl.newChild(ei)
}
if (node.rawElseBody.isBlockPlaceholder()) {
node.rawElseBody.newlyWrittenLocals = [];
this.typeCheck(node.rawElseBody);
} else {
this.writtenLocals = prevWritten.slice(0);
this.typeCheck(node.rawElseBody);
node.rawElseBody.newlyWrittenLocals = this.writtenLocals.slice(prevWritten.length);
}
if (isComment)
this.errorLevel--
this.writtenLocals = prevWritten;
}
public visitWhere(node:Where)
{
this.expect(node.condition, this.core.Boolean, 'where');
}
public visitWhile(node:While)
{
this.updateStmtUsage(node);
this.expect(node.condition, this.core.Boolean, 'while');
this.conditionalScope(() => {
this.currLoop = node;
this.typeCheck(node.body);
});
}
public visitBreakContinue(node:Call, tp:string)
{
node._kind = this.core.Nothing;
node.topAffectedStmt = this.currLoop
if (!this.currLoop)
this.markError(node, lf("TD200: '{0}' can be only used inside a loop", tp))
if (!node.args[0].isPlaceholder())
this.markError(node, lf("TD205: '{0}' cannot take arguments", tp))
this.expectExpr(node.args[0], null, tp)
}
public visitBreak(node:Call) { this.visitBreakContinue(node, "break") }
public visitContinue(node:Call) { this.visitBreakContinue(node, "continue") }
public visitShow(node:Call)
{
node._kind = this.core.Nothing;
this.expectExpr(node.args[0], null, "show")
node.topPostCall = null;
if (node.args[0].isPlaceholder()) {
this.markError(node, lf("TD207: we need something to show"))
} else {
var tp = node.args[0].getKind()
if (tp == api.core.Unknown) return
var show = tp.getProperty("post to wall")
if (!show)
this.markError(node, lf("TD201: we don't know how to display {0}", tp.toString()))
else
node.topPostCall = mkFakeCall(PropertyRef.mkProp(show), [node.args[0]])
}
}
public visitReturn(node:Call)
{
node._kind = this.core.Nothing;
node.topRetLocal = null;
var exp = null
if (!node.args[0].isPlaceholder()) {
if (this.outLocals.length == 0)
this.markError(node.args[0], lf("TD202: the function doesn't have output parameters; return with a value is not allowed"))
else if (this.outLocals.length > 1)
this.markError(node.args[0], lf("TD203: the function has more than one output parameter; return with a value is not allowed"))
else {
node.topRetLocal = this.outLocals[0]
exp = node.topRetLocal.getKind()
this.recordLocalWrite(node.topRetLocal)
}
} else {
if (this.outLocals.length == 1)
this.markError(node, lf("TD206: we need a value to return here"))
}
this.expectExpr(node.args[0], exp, "return")
node.topAffectedStmt = this.currentAnyAction;
this.checkAssignment(this.lastStmt)
}
public visitActionParameter(node:ActionParameter)
{
}
public visitBlock(node:Block)
{
this.scope(() => {
var ss = node.stmts;
var unreach = false
var reported = false
for (var i = 0; i < ss.length; ++i) {
this.typeCheck(ss[i])
if (unreach) {
ss[i].isUnreachable = true
if (!reported && !(ss[i] instanceof Comment)) {
reported = true
ss[i].addHint(lf("code after return, break or continue won't ever run"))
}
}
if (ss[i].isJump())
unreach = true
}
for (var i = 0; i < ss.length; ++i) {
if (ss[i] instanceof If) {
var si = <If>ss[i]
si.isElseIf = false;
var end = i + 1
while (isElseIf(ss[end])) {
end++;
if (!(<If>ss[end - 1]).rawElseBody.isBlockPlaceholder())
break;
}
si.branches = []
while (i < end) {
var innerIf = <If>ss[i++]
innerIf.parentIf = si;
si.branches.push({
condition: innerIf.rawCondition,
body: innerIf.rawThenBody
})
if (i == end) {
si.branches.push({
condition: null,
body: innerIf.rawElseBody
})
innerIf.displayElse = true;
} else {
innerIf.displayElse = false;
}
}
i--;
this.writtenLocals.pushRange(Util.intersectArraysVA(si.branches.map(b => b.body.newlyWrittenLocals)))
}
}
})
}
private updateStmtUsage(s:Stmt, tp:string = null)
{
this.lastStmt = s
if (this.countToUpdate != 0) {
var u = api.core.stmtUsage(tp || s.nodeType())
if (this.countToUpdate == 1)
u.localCount++;
else
u.globalCount++;
}
}
public visitFor(node:For)
{
this.updateStmtUsage(node);
this.expect(node.upperBound, this.core.Number, 'for');
node.boundLocal._kind = this.core.Number;
this.readOnlyLocals.push(node.boundLocal);
this.conditionalScope(() => {
this.currLoop = node;
this.declareLocal(node.boundLocal);
this.typeCheck(node.body);
});
}
public visitForeach(node:Foreach)
{
this.updateStmtUsage(node);
this.expect(node.collection, null, null);
var k = node.collection.getKind();
if (!k.isEnumerable() && !node.collection.getError()) {
if (k == this.core.Unknown) {
this.markHolderError(node.collection, lf("TD119: i need something to iterate on"));
} else {
this.markHolderError(node.collection, lf("TD120: i cannot iterate over {0}", k.toString()));
}
}
var ek: Kind;
if (k instanceof RecordDefKind)
ek = (<RecordDefKind>k).record.entryKind;
var atProp = k.getProperty("at");
if (!ek && !!atProp)
ek = atProp.getResult().getKind();
if (!!ek)
node.boundLocal._kind = ek;
this.readOnlyLocals.push(node.boundLocal);
this.conditionalScope(() => {
this.currLoop = node;
this.declareLocal(node.boundLocal);
this.typeCheck(node.conditions);
this.typeCheck(node.body);
});
}
public visitBox(node:Box)
{
this.updateStmtUsage(node);
this.typeCheck(node.body)
}
public visitComment(node:Comment)
{
this.updateStmtUsage(node);
}
private setOutLocals(locs:LocalDef[])
{
this.outLocals = locs
var isHiddenOut = locs.length == 1
locs.forEach(l => {
l.isHiddenOut = isHiddenOut;
l.isOut = true;
})
}
public visitAction(node:Action)
{
this.writtenLocals = [];
this.readOnlyLocals = [];
this.allLocals = [];
this.currentAction = node;
this.currentAnyAction = node;
node.clearError();
this.actionSection = ActionSection.Normal;
this.inAtomic = node.isAtomic;
this.inShim = this.topApp.entireShim || /{shim:.*}/.test(node.getDescription())
this.scope(() => {
// TODO in - read-only?
var prevErr = this.errorCount;
this.setOutLocals(node.getOutParameters().map((p) => p.local))
this.reportedUnassigned = false;
this.typeResolver.visitAction(node);
var fixupLocalInp = (a:ActionParameter) => {
this.declareLocal(a.local);
if (node.isPage())
this.readOnlyLocals.push(a.local)
}
if (node.modelParameter)
fixupLocalInp(node.modelParameter);
node.getInParameters().forEach(fixupLocalInp);
node.getOutParameters().forEach((a) => this.declareLocal(a.local))
if (node.isPage()) {
this.pageInit = node.getPageBlock(true)
this.pageDisplay = node.getPageBlock(false)
}
this.typeCheck(node.body);
if (node.isActionTypeDef()) {
var outp1 = node.getOutParameters()[1]
if (outp1) {
this.setNodeError(node, lf("TD171: function types support at most one output parameter; sorry"))
}
} else {
if (!this.reportedUnassigned)
this.checkAssignment(node);
}
node._hasErrors = this.errorCount > prevErr;
node.allLocals = this.allLocals;
});
if (// this.topApp.featureMissing(LanguageFeature.UnicodeModel) &&
node.modelParameter &&
node.modelParameter.local.getName() != modelSymbol)
{
node.modelParameter.local.setName(modelSymbol)
this.numFixes++;
}
if (this.missingLocals.length > 0 && this.storeLocalsAt && this.storeLocalsAt.locals) {
this.storeLocalsAt.locals.pushRange(this.missingLocals);
}
var inf = node.eventInfo;
if (inf != null) {
inf.disabled = false;
if (inf.type.globalKind != null && !inf.onVariable) {
var varName = node.getName().slice(inf.type.category.length);
var v = this.topApp.variables().filter((v:GlobalDef) => v.getName() == varName)[0];
if (v === undefined) {
node.setError(lf("TD122: i cannot find variable {0}", varName))
inf.disabled = true;
}
inf.onVariable = v;
}
if (!!inf.onVariable) {
var newName = inf.type.category + inf.onVariable.getName()
if (!Script.things.filter(t => t.getName() == newName)[0])
node.setName(newName);
if (node.getName() != newName)
inf.onVariable = null;
}
// these should never really happen - we do not allow users to edit the signature
if (node.hasOutParameters())
node.setError(lf("TD123: events cannot have out parameters"));
if (node.getInParameters().length != inf.type.inParameters.length)
node.setError(lf("TD124: wrong number of in parameters to an event"));
else {
var inParms = node.getInParameters();
for (var i = 0; i < inParms.length; ++i)
if (inParms[i].getKind() != inf.type.inParameters[i].getKind())
node.setError(lf("TD125: wrong type of parameter #{0}", i));
}
if (this.topApp.isLibrary && !this.ignoreLibErrors)
node.setError(lf("TD126: libraries cannot define global events"));
}
if (!node.isExternal) {
if (this.topApp.isCloud && node.isPage())
node.setError(lf("TD177: cloud libraries cannot define pages"));
if (this.topApp.isCloud && !node.isPrivate && node.isAtomic)
node.setError(lf("TD178: cloud libraries cannot define atomic functions"));
}
if (!!node.getError()) node._hasErrors = true;
node._errorsOK = undefined;
if (node.isCompilerTest()) {
node._errorsOK = runErrorChecker(node);
}
}
public visitLibraryRef(node:LibraryRef)
{
}
private persistentStorageError(whatr: AST.RecordDef, wherep: AST.RecordPersistence, where?: AST.RecordDef): string {
var error: string;
var wheres: string;
switch (wherep) {
case RecordPersistence.Cloud: wheres = lf("replicated "); break;
case RecordPersistence.Partial: wheres = lf("replicated "); break;
case RecordPersistence.Local: wheres = Script.isCloud ? lf("server-local ") :lf("locally persisted "); break;
}
wheres = wheres + (!where ? lf("variable") : (where.recordType === RecordType.Table ? lf("table") : lf("index")));
if (whatr.recordType === RecordType.Object) {
return lf("TD169: {0:a} cannot be persisted between script runs", whatr.toString());;
}
else {
Util.assert(whatr.recordType === RecordType.Table);
var whats: string;
switch (whatr.getRecordPersistence()) {
case RecordPersistence.Cloud: whats = lf("replicated table rows"); break;
case RecordPersistence.Local: whats = Script.isCloud ? lf("server-local table rows") : lf("locally persisted table rows"); break;
case RecordPersistence.Temporary: whats = lf("temporary table rows"); break;
case RecordPersistence.Partial: whats = lf("replicated table rows"); break;
}
return lf("TD166: cannot store {0} in a {1}",
whats, wheres);
}
}
visitGlobalDef(node: GlobalDef) {
node.clearError();
if (node.isResource) {
node.isTransient = true;
node.cloudEnabled = false;
}
this.typeResolver.visitGlobalDef(node);
if (node.getRecordPersistence() != RecordPersistence.Temporary &&
(node.getKind().getContexts() & KindContext.CloudField) == 0) {
this.setNodeError(node, lf("TD165: {0:a} cannot be saved between script runs", node.getKind().toString()))
if (this.topApp.featureMissing(LanguageFeature.LocalCloud)) {
node.isTransient = true;
node.cloudEnabled = false;
this.numFixes++;
}
}
if (node.getKind() instanceof RecordEntryKind) {
var rdef = (<RecordEntryKind>(node.getKind())).getRecord();
if (rdef.recordType == RecordType.Decorator) {
this.setNodeError(node, lf("TD175: must not store decorators in variables"));
}
else if (node.getRecordPersistence() > rdef.getRecordPersistence()) {
this.setNodeError(node, this.persistentStorageError(rdef, node.getRecordPersistence()));
if (!node.isTransient && this.topApp.featureMissing(LanguageFeature.LocalCloud)) {
node.isTransient = true;
node.cloudEnabled = false;
this.numFixes++;
}
}
}
}
private cyclefree(start,current: RecordDef) : boolean
{
return (current != start) &&
(!current._wasTypechecked || current.linkedtables.every((t: RecordDef) => this.cyclefree(start, t)));
}
public visitRecordField(node:RecordField)
{
node.clearError();
this.typeResolver.visitRecordField(node);
var pers = node.def().getRecordPersistence()
var cl = pers != RecordPersistence.Temporary;
var ctx = cl ? (node.isKey ? KindContext.IndexKey : KindContext.CloudField) :
(node.isKey ? KindContext.IndexKey : KindContext.GlobalVar);
var k = node.dataKind
var c = k.getContexts();
var newtype = node.def().recordType
if (node.def().recordType == RecordType.Decorator && ctx == KindContext.IndexKey)
ctx = KindContext.GcKey
// check if this is a valid row key for an index or table
if (!!(c & KindContext.RowKey) && (newtype == RecordType.Index || newtype == RecordType.Table)) {
var other = (<RecordEntryKind>node.dataKind).getRecord()
var otherpers = other.getRecordPersistence();
if (otherpers < pers && !(otherpers == RecordPersistence.Cloud && pers == RecordPersistence.Partial)) {
this.setNodeError(node, this.persistentStorageError(other, pers, node.def()));
this.errorCount++;
if (this.topApp.featureMissing(LanguageFeature.LocalCloud) &&
pers != RecordPersistence.Cloud &&
other.getRecordPersistence() != RecordPersistence.Cloud) {
node.def().persistent = false
other.persistent = false
this.numFixes++;
}
}
if (node.isKey && newtype == RecordType.Table) // check links
{
if (!this.cyclefree(node.def(), other)) {
this.setNodeError(node, lf("TD176: links must not be circular") )
}
else
node.def().linkedtables.push(other);
}
return;
}
if (!(c & ctx)) {
if (cl)
this.setNodeError(node, lf("TD167: {0:a} cannot be persisted between script runs", k.toString()));
else
this.setNodeError(node, lf("TD168: {0:a} cannot be used as a {1}", node.isKey ? lf("key") : lf("field"), k.toString()))
if (this.topApp.featureMissing(LanguageFeature.LocalCloud) && pers == RecordPersistence.Local) {
node.def().persistent = false;
this.numFixes++;
}
}
if (pers === RecordPersistence.Partial && node.isKey && node.isFirstChild()) {
if (node.dataKind.getName() !== "User") {
this.errorCount++;
node.setError("A partially replicated Index should have a User as first key");
}
}
}
public visitRecordDef(node:RecordDef)
{
node.isModel = false;
node.linkedtables = [];
this.visitChildren(node);
}
private typecheckLibraries(node:App)
{
node.libraries().forEach((l) => l.resolve());
node.libraries().forEach((l) => l.typeCheck());
}
public visitApp(node:App)
{
this.typecheckLibraries(node);
if (Cloud.isRestricted())
node.entireShim =
/#entireshim/i.test(node.comment) ||
node.actions().some(a => a.isPage()) ||
node.libraries().some(l => l.resolved && l.resolved.entireShim)
node.things.forEach((n:Decl) => {
var wt = n._wasTypechecked;
n._wasTypechecked = true;
this.dispatch(n);
if (!wt) n.notifyChange();
});
var usedNames = {}
node.things.forEach((n) => {
if (usedNames.hasOwnProperty(n.getName())) {
// name clash, need to fix
this.numFixes++;
n.setName(node.freshName(n.getName() + " " + this.numFixes))
} else {
usedNames[n.getName()] = n;
}
})
node.actions().forEach(a => {
var d = a.getModelDef()
if (d) d.isModel = true
})
}
public visitExprStmt(expr:ExprStmt)
{
this.updateStmtUsage(expr);
this.expect(expr.expr, null, "void");
if (expr.isVarDef())
this.updateStmtUsage(expr, "var");
}
private checkAssignment(node:Stmt)
{
var unassigned = this.outLocals.filter((v) => this.writtenLocals.indexOf(v) < 0);
if (unassigned.length > 0) {
this.reportedUnassigned = true;
node.addHint(
lf("return parameter{0:s} {1} may be unassigned before the function finishes",
unassigned.length,
unassigned.map((v) => "'" + v.getName() + "'").join(", ")))
}
}
private actionScope(k:Kind, f:()=>void)
{
this.scope(() => {
var prevReadOnly = this.readOnlyLocals;
var prevWritten = this.writtenLocals;
var prevSect = this.actionSection;
var prevAtomic = this.inAtomic;
var prevOut = this.outLocals;
var prevRep = this.reportedUnassigned;
var prevAct = this.currentAnyAction;
var prevLoop = this.currLoop;
var prevInvisibleLocals = this.invisibleLocals;
this.currLoop = null;
this.writtenLocals = [];
this.actionSection = ActionSection.Lambda;
this.inAtomic = k instanceof ActionKind && (<ActionKind>k).isAtomic();
this.readOnlyLocals = this.snapshotLocals();
if (Cloud.isRestricted())
this.invisibleLocals = this.snapshotLocals();
try {
f()
} finally {
this.writtenLocals = prevWritten;
this.readOnlyLocals = prevReadOnly;
this.inAtomic = prevAtomic;
this.actionSection = prevSect;
this.outLocals = prevOut;
this.currentAnyAction = prevAct;
this.reportedUnassigned = prevRep;
this.currLoop = prevLoop;
this.invisibleLocals = prevInvisibleLocals;
}
})
}
private typeCheckInlineAction(inl:InlineAction)
{
this.actionScope(inl.name.getKind(), () => {
this.currentAnyAction = inl;
inl.inParameters.forEach((d) => this.declareLocal(d));
inl.outParameters.forEach((d) => this.declareLocal(d));
this.setOutLocals(inl.outParameters.slice(0))
this.reportedUnassigned = false;
this.typeCheck(inl.body);
if (!this.reportedUnassigned)
this.checkAssignment(inl);
})
}
public visitInlineActions(inl:InlineActions)
{
this.updateStmtUsage(inl)
// cannot just use scope, as the next expression can introduce fresh variables
var names = inl.normalActions().map(a => a.name);
names.forEach((n) => {
this.declareLocal(n);
n.lambdaNameStatus = 2;
});
inl.actions.forEach((iab:InlineActionBase) => {
iab.clearError()
})
this.topInline = inl
this.expect(inl.expr, null, "void");
var p = this.localScopes.length - 1;
this.localScopes[p] = this.localScopes[p].filter((l) => names.indexOf(l) < 0);
var defined = (inl.expr.assignmentInfo() ? inl.expr.assignmentInfo().definedVars : null) || []
var prevScope = this.localScopes[p]
this.localScopes[p] = prevScope.filter(l => defined.indexOf(l) < 0)
var rename = (s:string) => s; // TODO
inl.actions.forEach((iab:InlineActionBase) => {
if (iab instanceof OptionalParameter) {
var op = <OptionalParameter>iab
this.expect(op.expr, op.recordField ? op.recordField.dataKind : null, "optional")
return
}
var ia = <InlineAction>iab
var coerced = false;
var coerce = (locs:LocalDef[], parms:PropertyParameter[]) =>
{
if (locs.length > parms.length) {
locs.splice(parms.length, locs.length - parms.length)
coerced = true;
} else if (parms.length > locs.length) {
coerced = true;
parms.slice(locs.length).forEach((pp) => {
locs.push(mkLocal(rename(pp.getName()), pp.getKind()))
});
}
locs.forEach((l, i) => {
var pp = parms[i];
if (pp.getKind() != l.getKind()) {
l.rename(rename(pp.getName()));
l._kind = pp.getKind();
coerced = true;
}
})
}
if (ia.isOptional && ia.recordField) {
var ak = <ActionKind>ia.recordField.dataKind
if (ak.isAction) {
coerce(ia.inParameters, ak.getInParameters())
coerce(ia.outParameters, ak.getOutParameters())
} else {
this.setNodeError(ia, lf("TD189: type of optional parameter '{0}' is not a function type", ia.getName()))
}
} else if (ia.name.lambdaNameStatus != 2 && ia.name.getKind().isAction) {
var ak = <ActionKind>ia.name.getKind();
coerce(ia.inParameters, ak.getInParameters())
coerce(ia.outParameters, ak.getOutParameters())
}
this.typeCheckInlineAction(ia);
});
this.localScopes[p] = prevScope
}
/////////////////////////////////////////////////
// Expression type-checking
/////////////////////////////////////////////////
public visitThingRef(t:ThingRef)
{
if (Util.startsWith(t.data, ThingRef.placeholderPrefix)) {
var kn = t.data.slice(ThingRef.placeholderPrefix.length);
var plName = null
var colon = kn.indexOf(':')
if (colon > 0) {
plName = kn.slice(colon + 1)
kn = kn.slice(0, colon)
}
var k = api.getKind(kn);
if (!k && /[_\[]/.test(kn)) {
k = Parser.parseType(kn)
if (k) k = TypeChecker.resolveKind(k)
if (k == api.core.Unknown) k = null
}
if (!!k) {
var pl = new PlaceholderDef();
pl.setName("");
pl._kind = k;
if (plName) pl.label = plName
t.def = pl
}
}
t._lastTypechecker = this;
if (t.def instanceof PlaceholderDef) {
if (!t.isEscapeDef())
this.markError(t, (<PlaceholderDef>t.def).longError());
} else {
if (!!t.def && !t.def.deleted && t.data != t.def.getName())
t.data = t.def.getName();
t.def = this.lookupSymbol(t);
if (!t.def && t.namespaceLibraryName()) {
if (!t.namespaceLibrary || t.namespaceLibrary.deleted)
t.namespaceLibrary = Script.libraries().filter(l => l.getName() == t.namespaceLibraryName())[0]
t.def = Script.libNamespaceCache.createSingleton(t.data)
}
if (!t.def) {
this.markError(t, lf("TD101: cannot find '{0}'", t.data));
var loc = mkLocal(t.data, this.core.Unknown);
t.def = loc
loc.isRegular = true
if (this.seenCurrent)
this.missingLocals.push(loc)
this.declareLocal(loc)
}
}
if (t.def instanceof LocalDef)
t.forceLocal = true;
else if (t.def instanceof SingletonDef)
t.forceLocal = false;
var l = <LocalDef>t.def;
if (l instanceof LocalDef && l.lambdaNameStatus) {
if (l.lambdaNameStatus == 1)
this.markError(t, lf("TD102: lambda reference '{0}' cannot be used more than once", l.getName()))
else {
l.lambdaNameStatus = 1;
if (this.typeHint && this.typeHint.isAction) {
l._kind = this.typeHint;
}
}
}
if (!this.inShim && this.invisibleLocals.indexOf(l) >= 0 && !Cloud.useEmbeddedGcc) {
this.markError(t, lf("TD208: inline functions cannot access locals from outside; try 'promote to data' on '{0}'", l.getName()))
}
t._kind = t.def.getKind();
if (t.def instanceof LocalDef) {
var l = <LocalDef> t.def;
if (!this.seenCurrent)
l.lastUsedAt = this.timestamp++;
} else if (this.countToUpdate != 0) {
if (t.def instanceof SingletonDef) {
var u = (<SingletonDef> t.def).usage;
if (this.countToUpdate == 1)
u.localCount++;
else
u.globalCount++;
}
}
}
public visitLiteral(l:Literal)
{
// Special, built-in type-checking for the literal that stands for a
// field name.
if (l instanceof FieldName) {
var mkKind = () => {
var mk = TDev.MultiplexRootProperty.md_make_kind();
mk.md_parametric("T");
var prop = TDev.MultiplexRootProperty
.md_make_prop(mk, 0, TDev.api.core.Unknown, ":", "Whatever", [], mk.getParameter(0));
return prop.getResult().getKind();
}
l._kind = mkKind();
} else switch (typeof l.data) {
case "number":
if (Cloud.isRestricted() && !this.inShim) {
if (Util.between(-0x80000000, l.data, l.possiblyNegative ? 0x80000000 : 0x7fffffff) != l.data) {
this.markError(l, lf("TD209: the number is outside of the allowed range (between {0} and {1})", -0x80000000, 0x7fffffff));
} else if (Math.round(l.data) != l.data) {
this.markError(l, lf("TD210: fractional numbers not allowed"));
}
}
l._kind = this.core.Number;
break;
case "string": l._kind = this.core.String; break;
case "boolean": l._kind = this.core.Boolean; break;
default: Util.die();
}
}
private typeCheckExpr(e:Expr)
{
e._kind = null;
// e.error = null;
this.dispatch(e);
Util.assert(e.getKind() != null);
}
private expectExpr(expr:Expr, tp:Kind, whoExpects:string, skipTypecheck = false)
{
if (!skipTypecheck) {
var prevHint = this.typeHint;
this.typeHint = tp;
this.typeCheckExpr(expr);
this.typeHint = prevHint;
}
if (!Util.check(expr.getKind() != null, "expr type unset2")) {
expr._kind = api.core.Unknown
}
if (tp != null && expr.getKind() !== tp) {
var code = "TD103: "
var suff = ""
if (tp.getRoot() == api.core.Ref && (expr.referencedData() || expr.referencedRecordField())) {
code = "TD164: "
suff = lf("; are you missing ◈ref?")
}
var msg = lf("'{0}' expects {1} here", whoExpects, tp.toString())
var k = expr.getKind();
if (k != this.core.Unknown)
msg += lf(", got {0}", k.toString())
this.markError(expr, code + msg + suff);
}
}
private handleAsync(t:Call, args:Expr[])
{
t._kind = this.core.Unknown; // will get overridden
if (args.length != 2) {
// cannot trigger this one?
this.markError(t, lf("TD104: syntax error in async"));
return;
}
// args[0] is 'this'
var arg = args[1]
if (arg instanceof Call)
(<Call>arg).runAsAsync = true;
this.expectExpr(arg, null, "async")
var calledProp = arg.getCalledProperty();
var isAsyncable = calledProp && !!(calledProp.getFlags() & PropertyFlags.Async)
if (calledProp == api.core.AsyncProp)
isAsyncable = false;
if (!isAsyncable) {
this.markError(t, lf("TD157: 'async' keyword needs a non-atomic API or function to call"))
return;
}
(<Call>arg).runAsAsync = true;
if (calledProp && calledProp.forwardsTo() instanceof Action) {
var act = <Action>calledProp.forwardsTo()
if (act.getOutParameters().length > 1)
this.markError(t, lf("TD170: cannot use 'async' on functions with more than one output parameter"))
}
t._kind = this.core.Task.createInstance([arg.getKind()])
}
private handleFun(t:Call, args:Expr[])
{
var ak:ActionKind = null
var resKind:Kind = null
if (this.typeHint && this.typeHint.isAction) {
ak = <ActionKind>this.typeHint
var outp = ak.getOutParameters()
if (outp.length != 1)
this.markError(t, lf("TD194: lambda expressions need to return exactly one value; function type '{0}' returns {1}", ak.getName(), outp.length))
else
resKind = outp[0].getKind()
} else {
this.markError(t, lf("TD195: lambda expressions can only be used as arguments of function type"))
}
this.actionScope(this.typeHint, () => {
if (ak) {
var synth = new InlineAction()
synth.name = mkLocal(Random.uniqueId(), ak)
this.declareLocal(synth.name)
var names = t.propRef.fromOp.getFunArgs() || []
synth.inParameters = ak.getInParameters().map((p, i) => mkLocal(names[i] || p.getName(), p.getKind()))
t.propRef.fromOp.funArgs = synth.inParameters
synth.outParameters = ak.getOutParameters().map(p => mkLocal(p.getName(), p.getKind()))
synth.inParameters.forEach(p => this.declareLocal(p))
synth.body = new CodeBlock()
synth.body.parent = synth
synth.parent = this.lastStmt.parent
var bb = Parser.emptyExprStmt()
synth.body.setChildren([bb])
var outp0 = synth.outParameters[0]
if (outp0) {
var resR = <ThingRef>mkThing(outp0.getName(), true)
resR.def = outp0
bb.expr.parsed = mkFakeCall(PropertyRef.mkProp(api.core.AssignmentProp), [
resR, args[1] ])
t.funAction = synth
}
}
this.expectExpr(args[1], resKind, "lambda expression")
});
t._kind = ak || this.core.Unknown
}
private recordLocalWrite(loc:LocalDef)
{
if (this.writtenLocals.indexOf(loc) < 0)
this.writtenLocals.push(loc);
}
private handleAssignment(t:Call, args:Expr[])
{
t._kind = this.core.Nothing;
if (args.length != 2) {
// cannot trigger this one?
this.markError(t, lf("TD104: syntax error in assignment"));
return;
}
Util.assert(args.length == 2);
var lhs = args[0].flatten(this.core.TupleProp);
var rhs = args[1];
this.isTopExpr = true;
this.typeCheckExpr(rhs);
var info = new AssignmentInfo();
t._assignmentInfo = info;
var sources = [AST.mkLocal("", rhs.getKind())];
var act = rhs.anyCalledAction()
if (act != null)
{
sources = act.getOutParameters().map((a:AST.ActionParameter) => a.local);
var missing = sources.length - lhs.length;
if (missing > 0) {
this.markError(t, lf("TD105: function '{0}' returns {1} more value{1:s}", act, missing));
info.missingArguments = missing;
}
}
info.targets = lhs;
info.sources = sources;
for (var i = 0; i < lhs.length; ++i) {
var trg = lhs[i];
var src = sources[i];
if (src == undefined) {
this.markError(trg, lf("TD106: not enough values returned to assign to {0}", trg));
continue;
}
var prevErr = this.errorCount;
if (trg.nodeType() === "thingRef") {
var tr = <ThingRef> trg;
tr._lastTypechecker = this;
if (!!tr.def) tr.data = tr.def.getName();
var name = tr.data;
var thing = this.lookupSymbol(tr);
if (!thing) {
var loc = mkLocal(name, src.getKind());
info.definedVars.push(loc);
thing = loc;
loc.isRegular = true
this.declareLocal(loc)
if (!src.getKind().hasContext(KindContext.Parameter)) {
if (src.getKind() == api.core.Nothing &&
this.topApp.featureMissing(LanguageFeature.ContextCheck))
info.fixContextError = true;
this.markError(tr, lf("TD155: '{0}' cannot be assigned to a local variable", src.getKind()))
}
} else {
var loc = <LocalDef>thing;
if (this.readOnlyLocals.indexOf(loc) >= 0) {
if (this.actionSection == ActionSection.Lambda)
this.markError(trg, lf("TD107: inline functions cannot assign to locals from outside like '{0}'", name));
else
this.markError(trg, lf("TD108: you cannot assign to the local variable '{0}'", name));
} else {
this.recordLocalWrite(loc)
}
}
this.typeCheckExpr(trg);
} else {
this.typeCheckExpr(trg);
var gd = trg.referencedData();
var rcf = trg.referencedRecordField();
var setter = trg.getLiftedSetter();
if (rcf == null && gd == null && setter == null) {
this.markError(trg, lf("TD109: cannot assign to this"));
continue;
}
if (gd && gd.readonly) {
this.markError(trg, lf("TD110: trying to assign to a read-only variable"));
} else if (rcf && rcf.isKey) {
this.markError(trg, lf("TD163: trying to assign to an index key"));
} else {
Util.assert(!!setter || trg.isRefValue());
}
(<Call>trg).autoGet = false;
}
if (src.getKind() != trg.getKind() && prevErr == this.errorCount)
this.markError(trg, lf("TD111: cannot assign from {0} to {1}", src.getKind(), trg.getKind()));
}
}
private lintJavaScript(js:string, isAsync:boolean)
{
var toks:string[] = Lexer.tokenize(js).map(l => {
switch (l.category) {
case TokenType.Op:
case TokenType.Id:
case TokenType.Keyword:
case TokenType.Label:
return l.data;
default:
return ""
}
}).filter(s => !!s)
var nextProtected = false
var hasResume = false
var hasUnprotected = 0
var hasOverprotected = 0
var brStack = []
var hasBrError = false
toks.forEach((t, i) => {
if ((t == "resume" || t == "checkAndResume") && toks[i + 1] == "(")
hasResume = true
if (t == "protect" && toks[i + 1] == "(")
nextProtected = true
if (t == "function") {
if (isAsync) {
if (!nextProtected)
hasUnprotected++
} else {
if (nextProtected)
hasOverprotected++
}
nextProtected = false
}
if (t == "(")
brStack.push(")")
if (t == "{")
brStack.push("}")
if (t == "[")
brStack.push("]")
if (t == ")" || t == "}" || t == "]") {
if (brStack.peek() != t) {
if (!hasBrError)
this.hintsToFlush.push(lf("JS hint: possible brace mismatch: got '{0}', expecting '{1}'", t, brStack.peek()))
hasBrError = true
} else brStack.pop()
}
})
if (!hasBrError && brStack.length > 0)
this.hintsToFlush.push(lf("JS hint: possible missing closing brace: '{0}'", brStack.peek()))
if (isAsync && !hasResume)
this.hintsToFlush.push(lf("JS hint: using 'javascript async' but no call to 'resume()'"))
if (!isAsync && hasResume)
this.hintsToFlush.push(lf("JS hint: using 'resume()' outside of 'javascript async'"))
if (hasUnprotected)
this.hintsToFlush.push(lf("JS hint: found function() without lib.protect(...) around it"))
if (hasOverprotected)
this.hintsToFlush.push(lf("JS hint: found function() with lib.protect(...) outside of `javascript async`"))
}
private checkStringLiteralArguments(t: Call) : (number) => boolean {
var propName = t.prop().getName();
if (!t.args.slice(1).every(a => a.getStringLiteral() != null)) {
this.markError(t, lf("TD179: arguments to `{0}` have to be string literals", propName))
return undefined;
}
var checkArgumentCount = (c: number) => {
if (t.args.length != c) {
this.markError(t, lf("TD181: wrong number of arguments to `{0}`", propName))
return false;
}
return true;
}
return checkArgumentCount;
}
private handleJavascriptImport(t:Call)
{
var checkArgumentCount = this.checkStringLiteralArguments(t);
if (!checkArgumentCount) return;
var propName = t.prop().getName();
switch (propName) {
case "javascript":
case "javascript async":
if (!checkArgumentCount(3)) return;
this.currentAction.getOutParameters().forEach(p => this.recordLocalWrite(p.local))
this.lintJavaScript(t.args[2].getStringLiteral(), /async/.test(t.prop().getName()))
break;
case "import":
if (!checkArgumentCount(4)) return;
var manager = t.args[1].getStringLiteral() || ""
var plugin = t.args[2].getStringLiteral()
var v = t.args[3].getStringLiteral()
if (v == null) v = "*";
switch (manager.trim().toLowerCase()) {
case "npm":
if (!this.topApp.canUseCapability(PlatformCapability.Npm))
this.unsupportedCapability(plugin, PlatformCapability.Npm);
this.topApp.imports.importNpm(this, t, plugin, v); break;
case "cordova":
if (!this.topApp.canUseCapability(PlatformCapability.Cordova))
this.unsupportedCapability(plugin, PlatformCapability.Cordova);
this.topApp.imports.importCordova(this, t, plugin, v); break;
case "bower":
this.topApp.imports.importBower(this, t, plugin, v); break;
case "client":
this.topApp.imports.importClientScript(this, t, plugin, v); break;
case "pip":
if (!this.topApp.canUseCapability(PlatformCapability.Npm))
this.unsupportedCapability(plugin, PlatformCapability.Npm);
this.topApp.imports.importPip(this, t, plugin, v); break;
case "touchdevelop": {
if (!/^\/?[a-z]{4,}$/.test(v)) this.markError(t, lf("TD190: version must be a published script id"));
else {
if (!this.topApp.canUseCapability(PlatformCapability.EditorOnly))
this.unsupportedCapability(plugin, PlatformCapability.EditorOnly);
this.topApp.imports.importTouchDevelop(this, t, plugin, v.replace(/^\/?/, ""));
}
break;
}
default: this.markError(t, lf("TD191: unknown package manager")); break;
}
break;
}
}
public visitCall(t:Call)
{
var args = t.args;
var prop = t.prop();
var wasTopExpr = this.isTopExpr
this.isTopExpr = false
if (this.inShim)
t.isShim = this.inShim;
t._assignmentInfo = null;
if (prop === this.core.AssignmentProp) {
this.handleAssignment(t, args);
return;
}
if (prop === this.core.AsyncProp) {
this.handleAsync(t, args);
return;
}
if (prop == this.core.FunProp) {
this.handleFun(t, args)
return
}
var topInline = this.topInline
this.topInline = null
var prevErr = this.errorCount;
this.typeCheckExpr(args[0]);
var k0 = args[0].getKind();
if (this.topApp.featureMissing(LanguageFeature.Refs) &&
args[0].referencedRecordField() &&
/^(get|set|test and set|confirmed|clear|add)$/.test(t.propRef.data))
{
this.numFixes++;
prop = null;
t.propRef.data = api.core.refPropPrefix + t.propRef.data
if (/^.(get)$/.test(t.propRef.data)) {
t.propRef.tokenFix = "delete";
} else if (/^.(set)$/.test(t.propRef.data)) {
t.propRef.tokenFix = ":=";
}
}
if (t.propRef.getText().slice(0,1) == api.core.refPropPrefix &&
args[0].allowRefUse()) {
var innerCall = <Call>args[0]
Util.assert(innerCall.autoGet)
innerCall._kind = k0 = api.core.Ref.createInstance([k0])
innerCall.autoGet = false;
}
if (prop) {
if (k0 != this.core.Unknown && prop.getInfixPriority() == 0 && prop.parentKind != k0)
prop = null; // rebind
}
if (prop && prop.deleted) {
var nn = prop.getName()
if (nn)
t.propRef.data = nn
prop = null;
}
if (!prop || prop instanceof UnresolvedProperty) {
prop = k0.getProperty(t.propRef.data);
if (!prop && this.topApp.featureMissing(LanguageFeature.UppercaseMultiplex)) {
if (k0 instanceof MultiplexKind) {
var otherOption = (s:string) => false
if (t.propRef.data[0] == recordSymbol) {
var libsuffix = "\u00A0" + t.propRef.data.slice(1)
otherOption = s => s.slice(-libsuffix.length) == libsuffix
}
prop = k0.listProperties().filter(p => p.getName().toLowerCase() == t.propRef.data || otherOption(p.getName()))[0]
} else if (k0.getName() == "Create" && t.propRef.data == "collection of") {
prop = k0.getProperty("Collection of")
}
}
if (!prop) {
var pref = k0.getName() + "->"
var autoRenameKey = pref + t.propRef.data
if (AST.propRenames.hasOwnProperty(autoRenameKey))
prop = k0.getProperty(AST.propRenames[autoRenameKey])
}
if (!prop && args[0] instanceof ThingRef) {
var nl = (<ThingRef>args[0]).namespaceLibrary
if (nl && !nl.deleted)
prop = nl.getKind().getProperty(t.propRef.data)
}
if (!prop) {
if (prevErr == this.errorCount)
this.markError(t, lf("TD112: i cannot find property '{0}' on {1}", t.propRef.data, k0));
prop = new UnresolvedProperty(k0, t.propRef.data);
}
t.propRef.prop = prop;
}
if (prop instanceof UnresolvedProperty) {
args.slice(1).forEach((p:Expr) => { this.typeCheckExpr(p); });
t._kind = this.core.Unknown;
return;
}
if (prop && prop.parentKind == this.core.App &&
/^(javascript|import)/.test(prop.getName())) {
this.handleJavascriptImport(t);
}
var imports = prop.getImports();
if (imports) imports.forEach(imp => {
switch (imp.manager) {
case "cordova": this.topApp.imports.importCordova(this, null, imp.name, imp.version); break;
case "npm": this.topApp.imports.importNpm(this, null, imp.name, imp.version); break;
default:
Util.assert(false, imp.manager + " not supported");
break;
}
});
t.autoGet = t.isRefValue();
var decl = prop.forwardsTo();
if (!!decl && decl.deleted) {
try {
Util.log("deleted decl name: " + decl.getName())
Util.log("deleted decl: " + decl.serialize())
} catch (e) {
}
Util.check(false, "deleted decl"); // should be handled above
decl.deleted = false;
// cannot trigger
// this.markError(t, "TD113: '{0}' was deleted", t.propRef.data);
}
var cacc = t.calledExtensionAction() || t.calledAction();
if (cacc) {
cacc.markUsed();
if (cacc.isPage()) {
t._assignmentInfo = new AssignmentInfo();
t._assignmentInfo.isPagePush = true;
}
}
if ((prop.getFlags() & PropertyFlags.Async) && !t.runAsAsync) {
if (this.inAtomic) {
if (this.actionSection == ActionSection.Init) {
// it's ok
} else if (this.actionSection == ActionSection.Display) {
var isRun = prop.getName() == "run" && prop.parentKind.isAction
if (cacc || isRun) {
// TODO: fix this warning to something usable
// this.hintsToFlush.push(Util.fmt("'{0}' may call non-atomic actions, which will fail at run time (search for 'atomic display' in the help)", prop.getName()));
} else {
this.markError(t, lf("TD172: '{0}' cannot be used in page display section (which is treated as 'atomic')", prop.getName()));
}
} else {
this.markError(t, lf("TD158: '{0}' cannot be used in functions marked with 'atomic'", prop.getName()));
}
}
this.seenAwait = true;
if (!wasTopExpr && innerAsyncWarning) {
this.hintsToFlush.push(lf("async call not on top-level"));
}
}
if (!prop.isImplementedAnywhere()) {
if (prop.getFlags() & PropertyFlags.IsObsolete)
this.hintsToFlush.push(lf("'{0}' is obsolete and not implemented", prop.getName()));
else
this.hintsToFlush.push(lf("'{0}' is currently not implemented", prop.getName()));
} else if (!Browser.isNodeJS && !this.topApp.canUseProperty(prop)) {
this.unsupportedCapability(prop.getName(), prop.getCapability());
} else if (prop.getFlags() & PropertyFlags.IsObsolete) {
this.hintsToFlush.push(lf("'{0}' is obsolete and should not be used", prop.getName()));
}
t._kind = prop.getResult().getKind();
var inP = prop.getParameters();
if (this.countToUpdate > 0)
if (this.countToUpdate == 1)
prop.getUsage().localCount++;
else
prop.getUsage().globalCount++;
if (topInline) (()=>{
var impl = topInline.implicitAction()
if (impl) {
var implIdx = -1
inP.forEach((p, i) => {
if (InlineActions.isImplicitActionKind(p.getKind()))
implIdx = i
})
if (implIdx < 0) {
this.setNodeError(topInline, lf("TD182: '{0}' doesn't take an implicit inline function", prop))
} else {
var tok = AST.mkThing(impl.getName(), true)
// this.dispatch(tok)
args.splice(implIdx, 0, tok)
}
}
})()
var optionsParm = OptionalParameter.optionsParameter(prop)
if (optionsParm && inP.length > args.length) (()=>{
var lk = optionsParm.getKind()
var rec = lk.getRecord()
var maker = new PlaceholderDef()
maker._kind = lk
maker.escapeDef = {
objectToMake: lk,
optionalConstructor: topInline,
isEmpty: true
}
var t = mkThing("_libobj_");
(<ThingRef>t).def = maker
args.push(t)
if (!topInline) return
var used:StringMap<boolean> = {}
topInline.optionalParameters().forEach(opt => {
var fld = lk.getProperty(opt.getName())
var stmt = fld ? fld.forwardsToStmt() : null
if (stmt instanceof AST.RecordField) {
if (used.hasOwnProperty(opt.getName()))
this.setNodeError(opt, lf("TD185: optional parameter '{0}' specified more than once", opt.getName()))
used[opt.getName()] = true
var rf = <AST.RecordField>stmt
opt.recordField = rf
maker.escapeDef.isEmpty = false
} else if (!opt.getName()) {
this.setNodeError(opt, lf("TD193: we need a name for optional parameter of '{0}' (from type '{1}')",
prop, lk))
} else {
this.setNodeError(opt, lf("TD183: '{0}' doesn't take optional parameter named '{1}' (in type '{2}')",
prop, opt.getName(), lk))
}
})
topInline = null
})()
if (topInline && topInline.optionalParameters().length > 0)
if (optionsParm)
this.markError(t, lf("TD188: '{0}' already has the optional parameters object passed explicitly", prop))
else
this.markError(t, lf("TD184: '{0}' doesn't take optional parameters", prop))
if (inP.length < args.length)
this.markError(t, lf("TD115: excessive parameter(s) supplied to {0}", prop));
else if (inP.length > args.length)
this.markError(t, lf("TD116: not enough parameters supplied to {0}", prop));
var concatOk = (e:Expr) => {
var k = e.getKind()
if (k == api.core.Unknown) return;
if (k == api.core.Nothing ||
k.singleton ||
k instanceof AST.LibraryRefKind)
this.markError(e, lf("TD117: cannot use this expression as argument to ||; are you missing parenthesis?"));
}
// args[0] is already typechecked; typechecking it again is exponential
if (prop == this.core.StringConcatProp) {
this.typeCheckExpr(args[1]);
concatOk(args[0]);
concatOk(args[1]);
if (this.saveFixes)
this.fixConcat(t)
return;
}
for (var i = 0; i < args.length; ++i) {
if (i >= inP.length) {
if (i > 0)
this.typeCheckExpr(args[i]);
} else {
var expK = inP[i].getKind();
if (i == 0 &&
args[i].getThing() instanceof SingletonDef &&
expK instanceof LibraryRefKind) {
(<ThingRef>args[i]).namespaceLibrary = (<LibraryRefKind>expK).lib;
(<ThingRef>args[i]).namespaceLibraryName();
} else {
this.expectExpr(args[i], expK, prop.getName(), i == 0);
}
args[i].languageHint = inP[i].languageHint
var str = inP[i].getStringValues()
if (str) {
var emap = (<any>str).enumMap
if (emap) {
var lit = args[i].getLiteral()
if (typeof lit == "string") {
var picMap = inP[i].getStringValueArtIds();
if (picMap) args[i].hintArtId = picMap[lit];
if (str.indexOf(lit) >= 0) {
args[i].enumVal = emap.hasOwnProperty(lit) ? emap[lit] : undefined
} else {
this.markError(args[i], lf("TD199: we didn't expect {0} here; try something like {1}",
JSON.stringify(lit),
str.map(s => JSON.stringify(s)).join(", ").slice(0, 100)))
}
} else {
this.markError(args[i], lf("TD198: we need an enum string here, something like {0}",
str.map(s => JSON.stringify(s)).join(", ").slice(0, 100)))
}
} else { // hints
var lit = args[i].getLiteral()
if (typeof lit == "string") {
var picMap = inP[i].getStringValueArtIds();
if (picMap) args[i].hintArtId = picMap[lit];
if(str.indexOf(lit) >= 0)
args[i].enumVal = lit;
}
}
}
if (/^bitmatrix|bitframe$/i.test(args[i].languageHint)) {
var lit = args[i].getLiteral();
if (!(typeof lit == "string")) {
this.markError(args[i], lf("TD179: we need a string here"));
}
}
}
}
if (this.saveFixes)
this.argumentFixes(t, inP)
}
private unsupportedCapability(name: string, cap: PlatformCapability) {
var missing = App.capabilityString(cap & ~(this.topApp.getPlatform()));
if (this.topApp.getPlatformRaw() & PlatformCapability.Current)
this.hintsToFlush.push(lf("your current device does not support '{0}'; to develop scripts for other devices, enable the '{1}' capability in the platform settings in the script properties pane",
name, missing));
else
this.hintsToFlush.push(lf("your current platform settings do not include the '{1}' capability required for '{0}'; you can change platform settings in the script properties pane",
name, missing));
if (this.currentAction)
this.currentAction.numUnsupported++
}
private cloneCall(t:Call, args:Expr[])
{
return mkFakeCall(t.propRef, args)
}
private fixConcat(t:Call)
{
var prop = t.args[1].getCalledProperty()
var c = <Call>t.args[1]
if (prop && prop.getResult().getKind() == api.core.Nothing && c.args.length == 1) {
t.savedFix = this.cloneCall(c, [this.cloneCall(t, [t.args[0], c.args[0]])])
this.saveFixes++
}
}
private argumentFixes(t:Call, inP:PropertyParameter[])
{
if (t.prop() == api.core.TupleProp) {
if (t.args[0].getKind() == api.core.String || t.args[1].getKind() == api.core.String) {
t.savedFix = mkFakeCall(PropertyRef.mkProp(api.core.StringConcatProp), t.args.slice(0))
this.saveFixes++
}
}
else if (inP.length > t.args.length) {
var args = t.args.slice(0)
var locs = this.snapshotLocals()
for (var i = args.length; i < inP.length; ++i) {
var toks = Fixer.findDefault(inP[i], locs)
var innExpr = ExprParser.parse1(toks)
this.typeCheckExpr(innExpr)
args.push(innExpr)
}
t.savedFix = this.cloneCall(t, args)
this.saveFixes++
}
}
}
class ClearErrors
extends NodeVisitor
{
public visitAstNode(n:AstNode)
{
n.clearError()
this.visitChildren(n)
}
}
}