/// module TDev { export module AST { export class Parser { private errors:ParseError[]; private tokens:LexToken[]; private tokenPos:number; public stmtList:Stmt[]; private declList:Decl[]; private currentAction:Action; public currentApp:App; private libRefs:any = {}; private currentLabel:string[] = []; // use peek/pop() to get the label constructor() { this.declarationCallbacks = { "action": this.parseAction, "event": this.parseAction, "meta": this.parseMetaDecl, "var": GlobalDef.parse, "table": RecordDef.parse }; this.statementCallbacks = { "meta": this.parseMetaStmt, "skip": this.parseSkip, "for": Parser.stmtCtor(For), "foreach": Parser.stmtCtor(Foreach), "while": Parser.stmtCtor(While), "if": Parser.stmtCtor(If), "else": this.parseElseIf, "do": this.parseDo, "where": this.parseWhere, "var": this.parseField, }; } private declarationCallbacks:any; private statementCallbacks:any; static stmtCtor(f:new()=>any) { return (p:Parser) => { var r = new f(); p.stmtList.push(r); r.parseFrom(p); } } // TSBUG should be private public errorLoc(loc:LexToken, msg:string, ...args:any[]) { debugger; this.errors.push(new ParseError(loc, Util.fmt_va(msg, args))); } public error(msg:string, ...args:any[]) { debugger; this.errors.push(new ParseError(this.curr(), Util.fmt_va(msg, args))); } public getLibraryRef(name:string):LibraryRef { if (name == "this") return this.currentApp.thisLibRef; var existing = this.currentApp && this.currentApp.libraries().filter((l) => l.getName() == name)[0]; if (existing) return existing; if (this.libRefs.hasOwnProperty(name)) return this.libRefs[name]; var l = new LibraryRef(); this.libRefs[name] = l; l.setName(name); this.declList.push(l); return l; } public declareLibrary(l:LibraryRef) { var idx = this.declList.indexOf(l); this.declList.splice(idx, 1); this.declList.push(l); } public consumeLabel() { var r = this.currentLabel this.currentLabel = [] return r } public featureMissing(f:LanguageFeature) { if (!this.currentApp) return false return this.currentApp.featureMissing(f) } public lookahead(n:number) { var t = this.tokens[this.tokenPos + n] if (!t) return this.tokens[this.tokens.length - 1] return t; } public curr() { return this.tokens[this.tokenPos]; } public got(t:TokenType) { return this.curr().category == t; } public gotOp(s:string) { return this.got(TokenType.Op) && this.curr().data == s; } public gotKw(s:string) { return this.got(TokenType.Keyword) && this.curr().data == s; } public gotId(s:string) { return this.got(TokenType.Id) && this.curr().data == s; } public tokenize(s:string) { this.errors = []; this.stmtList = []; this.declList = []; this.currentAction = null; this.currentApp = null; var toks = Lexer.tokenize(s); toks.forEach((l:LexToken) => { if (l.category == TokenType.Error) this.errorLoc(l, l.data); }); this.tokenPos = 0; this.tokens = toks.filter((l:LexToken) => l.category != TokenType.Error); } static parseEndpoint(text:string, f : (p:Parser) => any, errs:string[], script:App) : any { var p = new Parser(); p.tokenize(text); p.currentApp = script || Script; var r = f(p); if (errs) errs.pushRange(p.errors.map((e) => e.toString())); else p.handleErrors(); return r; } static parseType(text:string, script:App = null) : Kind { return Parser.parseEndpoint(text, (p) => p.parseType(), null, script); } static parseExprHolder(text:string, script:App = null) : ExprHolder { return Parser.parseEndpoint(text, (p) => p.parseExpr(), null, script); } static parseStmt(text:string, script:App = null) : Stmt { return Parser.parseEndpoint(text, (p) => p.parseOneStmt(), null, script); } static parseDecl(text:string, script:App = null) : Decl { return Parser.parseEndpoint(text, (p) => p.parseOneDecl(), null, script); } static parseDecls(text:string, script:App = null) : Decl[] { return Parser.parseEndpoint(text, (p) => p.parseManyDecls(), null, script); } static parseScript(text:string, errs:string[] = null) : App { return Parser.parseEndpoint(text, (p) => p.parseApp(), errs, null); } public handleErrors() { var errors = this.errors.map((e:ParseError) => e.toString()).join("\n"); if (errors != "") { Util.log("Parse error: " + errors); debugger; } } public gotKey(s:string) { var t = this.curr(); if (t.data == s) { this.shift(); if (this.gotOp("=") || this.gotOp(":=")) this.shift(); return true; } return false; } private trySkipId(s:string) { if (this.gotId(s)) { this.shift(); return true; } else return false; } private gotTerminator() { switch (this.curr().category) { case TokenType.Keyword: case TokenType.EOF: case TokenType.Label: return true; case TokenType.Op: switch (this.curr().data) { case ";": case "{": case "}": return true; } } return false; } public shift() { var r = this.curr(); if (r.category != TokenType.EOF) this.tokenPos++; return r; } public skipOp(s:string) { if (this.gotOp(s)) this.shift(); else this.error("expecting operator {0}, got {1}", s, this.curr()); } public skipKw(s:string) { if (this.gotKw(s)) this.shift(); else this.error("expecting keyword {0}, got {1}", s, this.curr()); } public parseString() { if (!this.got(TokenType.String)) { this.error("expecting string, got {0}", this.curr()); this.shift(); return ""; } else return this.shift().data; } public parseBool() { return this.parseId() == "true"; } public parseLibRef() { if (this.gotOp(libSymbol)) { this.shift(); return this.getLibraryRef(this.parseId("this")); } else { this.error("expecting library reference, got {0}", this.curr()); return this.getLibraryRef("this"); } } public parseId(defl = "?") { if (!this.got(TokenType.Id)) { this.error("expecting identifier, got {0}", this.curr()); this.shift(); return defl; } else { return this.shift().data; } } public parseType() { if (this.gotOp(libSymbol)) { var lib = this.parseLibRef(); this.skipOp("\u2192"); // -> if (this.got(TokenType.Id)) return lib.getAbstractKind(this.parseId()); } var forceRecord = false if (this.gotOp("*")) { this.shift(); forceRecord = true } var n = this.parseId(); var k = forceRecord ? new UnresolvedKind(n) : api.getKind(n); if (!k && / field$/.test(n)) { n = n.slice(0, n.length - 6); if (/^Collection of /.test(n)) n = n.slice(14) + " Collection"; k = api.getKind(n); } var collKind:Kind = null; switch (n) { case "Appointment Collection": collKind = api.getKind("Appointment"); break; case "Contact Collection": collKind = api.getKind("Contact"); break; case "Device Collection": collKind = api.getKind("Device"); break; case "Link Collection": collKind = api.getKind("Link"); break; case "Location Collection": collKind = api.getKind("Location"); break; case "Media Link Collection": collKind = api.getKind("Media Link"); break; case "Media Player Collection": collKind = api.getKind("Media Player"); break; case "Media Server Collection": collKind = api.getKind("Media Server"); break; case "Message Collection": collKind = api.getKind("Message"); break; case "Number Collection": collKind = api.core.Number; break; case "Page Collection": collKind = api.getKind("Page"); break; case "Place Collection": collKind = api.getKind("Place"); break; case "Printer Collection": collKind = api.getKind("Printer"); break; case "String Collection": collKind = api.core.String; break; } if (collKind) k = api.core.Collection.createInstance([collKind]) if (!k) { if (/ Collection$/.test(n)) k = api.core.Collection.createInstance([new UnresolvedKind(n.slice(0, n.length - 11))]) else k = new UnresolvedKind(n) } if (n == "Unknown") k = api.core.String; if (!k) { this.error("unknown type {0}", n); k = api.core.Unknown; } if (this.gotOp("[")) { this.shift(); var parms = []; while (true) { var t = this.parseType() parms.push(t) if (this.gotOp("]")) { this.shift(); break; } else if (this.gotOp(",")) { this.shift(); continue; } else { this.error("bad type syntax"); break; } } if (k instanceof ParametricKind) { var pk = k; var numF = pk.parameters.length if (parms.length > numF) { this.error("too many type arguments"); parms = parms.slice(0, numF) } else if (parms.length < numF) { this.error("too few type arguments"); while (parms.length < numF) parms.push(api.core.Nothing) } k = pk.createInstance(parms); } else { this.error("{0} is not parametric type", k.getName()) } } return k; } public parseTypeAnnotation() { this.skipOp(":"); return this.parseType(); } public addDecl(s:Decl) { this.declList.push(s); } private addStmt(s:Stmt) { this.stmtList.push(s); } private parseElseIf() { this.shiftLabel(); this.skipKw("if") var r = new If() this.stmtList.push(r) r.isElseIf = true; r.parseFrom(this) } private parseParameters(lbl:string,sep:string, out = false) { var parens = false; var res:ActionParameter[] = []; if (this.gotOp("(")) { this.shift(); parens = true; } var index = 0; while (true) { if (parens && this.gotOp(")")) { this.shift(); break; } this.shiftLabel(); var lbl2 = this.consumeLabel() if (this.gotTerminator()) break; var n = this.parseId(); var t = this.parseTypeAnnotation(); var loc = mkLocal(n, t); var ap = new ActionParameter(loc, out); ap.setStableName(lbl2) index++; res.push(ap); if (this.gotOp(",")) this.shift(); } return res; } // TODO - lbl should always be provided (check events.ts) public parseActionHeader(lbl:string = "") { var isType = false while (this.got(TokenType.Op) && !this.gotTerminator()) { if (this.gotOp("type")) { isType = true } else { this.error("expecting identifier, got {0}", this.curr()); } this.shift(); } var res = { name: this.parseId(), inParameters: [], outParameters: [], isType: isType, } res.inParameters = this.parseParameters(lbl,"_3"); if (this.gotKw("returns")) { this.shift(); res.outParameters = this.parseParameters(lbl,"_4", true); } return res; } private parseTokenSequenceStmt() { var eh = this.parseTokenSequence(); if (eh != null) this.addStmt(mkExprStmt(eh, this)); } static emptyExpr() { var eh = new ExprHolder(); eh.tokens = []; eh.locals = []; eh.parsed = mkPlaceholderThingRef() return eh; } public parseExpr() { var eh = this.parseTokenSequence(); if (eh == null) { eh = Parser.emptyExpr(); } return eh; } private parseTokenSequence() { var toks:Token[] = []; var t:LexToken; var add = (tp:string, json = undefined) => { if (json === undefined) json = {}; json.type = tp; if (json.data === undefined) json.data = t.data; toks.push(mkTok(json)) } while (true) { if (this.gotTerminator()) break; t = this.curr(); switch (t.category) { case TokenType.Op: if (t.data == "...") { add("thingRef", { data: "$skip" }); } else if (t.data == "$") { this.shift(); var pos = this.tokenPos; var id = this.parseId(); this.tokenPos = pos; // there is Shift() down the line add("thingRef", { data: id, forceLocal: true }); } else if (t.data == "[") { var annot:string[] = [] this.shift() while (this.curr().category == TokenType.Id) { annot.push(this.curr().data) this.shift() } this.skipOp("]") this.tokenPos--; // shift() down the line if (annot[0] == "lib") { var l = toks.peek() if (l instanceof ThingRef) { l._namespaceLibraryName = annot[1] } } } else { add("operator"); } break; case TokenType.Id: if (toks.peek() instanceof Operator) { var pt = ( toks.peek()).data; if (pt == "→") toks[toks.length - 1] = mkPropRef(t.data); else if (pt == "♻") { toks[toks.length - 1] = mkThing(pt); add("propertyRef"); } else { add("thingRef"); } } else { add("thingRef"); } break; case TokenType.String: add("literal", { kind: "String" }); break; case TokenType.Comment: break; case TokenType.Keyword: case TokenType.EOF: Util.die(); break; default: Util.die(); break; } this.shift(); } if (Cloud.isRestricted()) { // rewrite `basic->plot image(s)` into `basic->show leds(s, 400)` toks.forEach((t, i) => { if (t instanceof PropertyRef && t.getText() == "plot image" && toks[i - 1] instanceof ThingRef && toks[i - 1].getText() == "basic" && toks[i + 3] && toks[i + 1].getOperator() == "(" && toks[i + 2].getStringLiteral() != null && toks[i + 3].getOperator() == ")") { (t).data = "show leds" toks.splice(i + 3, 0, mkOp(","), mkOp("4"), mkOp("0"), mkOp("0")) } }) } if (toks.length > 0) { var r = new ExprHolder(); // avoid stack overflow; /bygua is a real script using more than 200 tokens in a line... if (toks.length > 300) toks = toks.slice(0, 300); r.tokens = toks; return r; } else { return null; } } static emptyBlock() { var r = new CodeBlock(); r.setChildren([Parser.emptyExprStmt()]); return r; } public parseBlock() : CodeBlock { var nesting = 0; var loc0 = this.curr(); var elseStack:ElseEntry[] = []; var mkCodeBlock = (stmts:Stmt[]) => { var r = new CodeBlock(); if (stmts.length == 0) stmts.push(r.emptyStmt()); r.setChildren(stmts); return r; }; var unwind1 = () => { var br = elseStack.pop(); var curr = this.stmtList this.stmtList = br.stmtList if (br.ifStmt) { if (curr[0] instanceof If && curr.slice(1).every(isElseIf)) { (curr[0]).isElseIf = true; this.stmtList.pushRange(curr) } else { br.ifStmt.setElse(mkCodeBlock(curr)); } } else if (elseStack.length == 0) { return mkCodeBlock(curr); } else { this.stmtList.pushRange(curr); } return null; }; var unwind = () => { while (true) { var r = unwind1() if (r) return r; } }; var pushBrace = (ifStmt:If = null) => { elseStack.push({ stmtList: this.stmtList, ifStmt: ifStmt, }) this.stmtList = [] }; if (this.gotOp("{")) this.shift(); else this.error("expecting {{ at the beginning of the block, got {0}", this.curr()); pushBrace(); while (true) { var t = this.curr(); switch (t.category) { case TokenType.Op: switch (t.data) { case "}": this.shift(); var r = unwind1(); if (r) return r; break; case "{": this.shift(); pushBrace(); break; case ";": this.shift(); break; default: this.parseStmtCore(); break; } break; case TokenType.EOF: this.errorLoc(loc0, "unclosed {{ (got end of file)"); return unwind(); default: if (this.gotKw("else") && this.stmtList.peek() instanceof If) { this.shift(); if (this.gotOp("{")) { this.shift(); pushBrace(this.stmtList.peek()); } } else if (this.parseStmtCore()) { this.errorLoc(loc0, "unclosed {{ (got next declaration)"); return unwind(); } break; } } } private parseStmtCore() { this.shiftLabel(); var t = this.curr(); switch (t.category) { case TokenType.Keyword: var a = this.statementCallbacks[t.data]; if (!a) { if (!!this.declarationCallbacks[t.data]) { return true; } else { this.error("keyword {0} is unexpected here (or reserved for future use)", t); this.shift(); } } else { this.shift(); a.call(this, this); } break; case TokenType.Comment: var c = new Comment(); c.setStableName(this.consumeLabel()); c.text = t.data.trim(); this.addStmt(c); this.shift(); break; case TokenType.Op: case TokenType.Id: case TokenType.String: this.parseTokenSequenceStmt(); break; default: Util.die(); break; } return false; } public parseOneStmt() { Util.assert(this.tokenPos == 0); this.stmtList = []; if (this.gotOp("{")) return this.parseBlock(); this.parseStmtCore(); if (this.stmtList.length != 1) this.error("expecting a single statement"); return this.stmtList[0]; } public shiftLabel() { var arr = []; while (this.got(TokenType.Label)) { arr.push(this.curr().data); this.shift(); } this.currentLabel = arr; if(this.currentLabel.length==1 && !(this.currentLabel[0])) this.currentLabel = []; var res = (this.currentLabel.length > 0); //if(res) console.log(">>>>>>>>> shifted label: \""+this.currentLabel+"\""); return res; } public parseBraced(a : () => void) { if (!this.gotOp("{")) return; var nesting = 0; while (true) { this.shiftLabel(); if (this.gotOp("{")) { nesting++; this.shift(); continue; } else if (this.gotOp("}")) { nesting--; this.shift(); if (nesting == 0) break; else continue; } else if (this.got(TokenType.EOF)) { break; } var prevPos = this.tokenPos; a(); if (prevPos == this.tokenPos) this.shift(); // error? } } private skipBlock() { return this.parseBraced(() => {}); } private parseDeclCore() { this.shiftLabel(); // TODO - what if there is no label? if (this.got(TokenType.Keyword)) { var a = this.declarationCallbacks[this.curr().data]; if (a) { this.shift(); a.call(this, this); return true; } } return false; } public parseOneDecl() { var decls = this.parseManyDecls(); if (decls.length != 1) this.error("expecting a single declaration"); return decls[0]; } public parseManyDecls() { Util.assert(this.tokenPos == 0); this.declList = []; this.parseDeclCore(); return this.declList; } public parseApp() { Util.assert(this.tokenPos == 0); this.currentApp = new App(this); var printedError = false; var description = ""; while (true) { if (this.got(TokenType.EOF)) break; if (this.parseDeclCore()) { printedError = false; continue; } if (this.got(TokenType.Comment)) { description += this.curr().data.trim() + "\n"; this.shift(); printedError = false; continue; } if (!printedError) this.error("expecting declaration, got {0}", this.curr()); printedError = true; this.shift(); } var app = this.currentApp; app.comment = description.trim(); this.declList.forEach((d) => { if (d instanceof LibraryRef) { var l = d; if (!l.isDeclared) { l.deleted = true; return; } } app.addDecl(d) }); app.parsingFinished(); this.currentApp = null; return app; } private parseAction() { var prevTok = this.tokens[this.tokenPos - 1]; var lbl = this.consumeLabel(); var hd = this.parseActionHeader(lbl.peek()); this.currentAction = new Action(); this.currentAction.setStableName(lbl); this.addDecl(this.currentAction); this.currentAction.header.inParameters.setChildren(hd.inParameters); this.currentAction.header.outParameters.setChildren(hd.outParameters); this.currentAction.setName(hd.name); this.currentAction.body = this.parseBlock(); this.currentAction._isActionTypeDef = hd.isType if (this.currentAction.isPage() && !this.currentAction.getPageBlock(false)) { // pages need to have a specific structure; bring it up to date var bl = Parser.parseStmt("{ if box->is_init then { } if true then { } }"); (bl.children()[1]).rawThenBody.setChildren(this.currentAction.body.children()); this.currentAction.body = bl; } this.currentAction.body.parent = this.currentAction.header; if (prevTok.data == "event") api.eventMgr.setInfo(this.currentAction, this); if (this.currentAction.isPage()) { this.currentAction.isAtomic = true; var parms = this.currentAction.header.inParameters; var parm0 = parms.stmts[0]; if (parm0 && parms.stmts[0] && (parm0.getKind().isExtensionEnabled() || parm0.getKind() instanceof UnresolvedKind) && ((this.featureMissing(LanguageFeature.UnicodeModel) && (parm0.getName() == "model" || parm0.getName() == "page data")) || parm0.getName() == modelSymbol || parm0.getName() == oldModelSymbol )) { parms.setChildren(parms.stmts.slice(1)); this.currentAction.modelParameter = parm0; } } this.currentAction = null; } private parseMetaDecl() { var tag = ""; if (this.got(TokenType.Keyword)) tag = this.shift().data; else tag = this.parseId(); // currently, ignore "recent" if (tag == "import") { LibraryRef.parse(this); return; } if (this.gotOp("{")) this.skipBlock(); else { var v = this.shift().data; if (this.currentApp) this.currentApp.setMeta(tag, v); this.skipOp(";"); } } private parseMetaStmt() { var tag = ""; if (this.got(TokenType.Keyword)) tag = this.shift().data; else tag = this.parseId(); // currently, ignore "recent" and "guid" if (tag == "private") { if (this.currentAction) this.currentAction.isPrivate = true; this.skipOp(";"); } else if (tag == "page") { if (this.currentAction) this.currentAction._isPage = true; this.skipOp(";"); } else if (tag == "offloaded") { // obsolete this.skipOp(";"); } else if (tag == "async") { if (this.currentAction) this.currentAction.isAtomic = false; this.skipOp(";"); } else if (tag == "sync") { if (this.currentAction) this.currentAction.isAtomic = true; this.skipOp(";"); } else if (tag == "test") { if (this.currentAction) this.currentAction._isTest = true; this.skipOp(";"); } else if (tag == "offline") { if (this.currentAction) this.currentAction.isOffline = true; this.skipOp(";"); } else if (tag == "query") { if (this.currentAction) this.currentAction.isQuery = true; this.skipOp(";"); } else { if (this.gotOp("{")) this.skipBlock(); else { this.shift(); this.skipOp(";"); } } } static emptyCondition() { var e = Parser.emptyExpr(); e.tokens.push(AST.mkThing("true")); return mkWhere(e); } static emptyExprStmt(p:Parser = null) { return mkExprStmt(Parser.emptyExpr(), p); } private parseSkip() { if (this.gotOp(";")) this.shift(); this.addStmt(Parser.emptyExprStmt(this)); } private parseDo() { var id = this.parseId(); var node; if (id == "box") node = Box; else { this.error("expecting 'box' here"); return; } (Parser.stmtCtor(node))(this); } private parseWhere() { var len = this.stmtList.length; var stmt: Stmt; var eq = this.lookahead(1) if (eq.category == TokenType.Op && eq.data == ":=") { var opt = new OptionalParameter() opt.parseFrom(this) stmt = opt } else { var inl = new InlineAction(); inl.parseFrom(this); stmt = inl } if (len == 0 || !(this.stmtList[len - 1] instanceof ExprStmt)) { this.error("'where' should follow an expression"); } else { var prev = this.stmtList[len - 1]; var inls = prev; if (!(prev instanceof InlineActions)) { inls = new InlineActions(); inls.setStableName(prev.getStableName()); inls.expr = (prev).expr; inls.parent = prev.parent; this.stmtList[len - 1] = inls; } inls.actions.push(stmt); } } private parseField() { // the "var" keyword has already been [shift]'d var name = this.parseId(); this.skipOp(":"); var kind = this.parseType(); this.skipOp(";"); var description = ""; if (this.got(TokenType.Comment)) { description = this.curr().data; this.shift(); } // [isKey] will be set properly by the code in [pasteCode] this.stmtList.push(new RecordField(name, kind, false, description)); } } interface ElseEntry { stmtList: Stmt[]; ifStmt: If; } export class ParseError { constructor(public loc:LexToken, private msg:string) { } public toString() { var beg = this.loc.inputPos - 30; if (beg < 0) beg = 0; var desc = this.loc.input.slice(beg, this.loc.inputPos) + "<*>" + this.loc.input.slice(this.loc.inputPos, beg + 60); return this.msg + " at >>>" + desc + "<<<"; } } } }