"use strict"; class Ro_Token { constructor (type, value, asString, line) { this.type = type; this.value = value; this._asString = asString; this.line = line; } static _new (type, asString) { return new Ro_Token(type, null, asString); } /** * Create a copy of this token, with additional line information. * @param line The line of code. */ _ (line) { return new Ro_Token(this.type, this.value, this._asString, line); } eq (other) { return other && other.type === this.type; } toString () { if (this._asString) return this._asString; return this.toDebugString(); } toDebugString () { return `${this.type}${this.value ? ` :: ${this.value}` : ""}`; } static NUMBER (val, line) { return new Ro_Token(Ro_Token.TYP_NUMBER, val, null, line); } static IDENTIFIER (val, line) { return new Ro_Token(Ro_Token.TYP_IDENTIFIER, val, null, line); } static DYNAMIC (val, line) { return new Ro_Token(Ro_Token.TYP_DYNAMIC, val, null, line); } static SYMBOL () {} static UNPARSED (val, line) { return new Ro_Token(Ro_Token.TYP_UNPARSED, val, null, line); } } Ro_Token.TYP_NUMBER = "NUMBER"; Ro_Token.TYP_IDENTIFIER = "IDENTIFIER"; Ro_Token.TYP_DYNAMIC = "DYNAMIC"; Ro_Token.TYP_SYMBOL = "SYMBOL"; // Cannot be created by lexing, only parsing Ro_Token.TYP_UNPARSED = "UNPARSED"; // Only used when lexing in alternate modes Ro_Token.NEWLINE = Ro_Token._new("NEWLINE", "<newline>"); Ro_Token.INDENT = Ro_Token._new("INDENT", "<line_indent>"); Ro_Token.DEDENT = Ro_Token._new("DEDENT", "<line_dedent>"); Ro_Token.PAREN_OPEN = Ro_Token._new("PAREN_OPEN", "("); Ro_Token.PAREN_CLOSE = Ro_Token._new("PAREN_CLOSE", ")"); Ro_Token.IF = Ro_Token._new("IF", "if"); Ro_Token.ELSE = Ro_Token._new("ELSE", "else"); Ro_Token.ELIF = Ro_Token._new("ELIF", "elif"); Ro_Token.EQ = Ro_Token._new("EQ", "=="); Ro_Token.NE = Ro_Token._new("NE", "!="); Ro_Token.GT = Ro_Token._new("GT", ">"); Ro_Token.LT = Ro_Token._new("LT", "<"); Ro_Token.GTEQ = Ro_Token._new("GTEQ", ">="); Ro_Token.LTEQ = Ro_Token._new("LTEQ", "<="); Ro_Token.NOT = Ro_Token._new("NOT", "not"); Ro_Token.ADD = Ro_Token._new("ADD", "+"); Ro_Token.SUB = Ro_Token._new("SUB", "-"); Ro_Token.MULT = Ro_Token._new("MULT", "*"); Ro_Token.DIV = Ro_Token._new("DIV", "/"); Ro_Token.POW = Ro_Token._new("POW", "^"); Ro_Token.COLON = Ro_Token._new("COLON", ":"); Ro_Token.ASSIGN = Ro_Token._new("ASSIGN", "="); class Ro_Lexer { constructor () { this._indentStack = null; this._tokenStack = []; } /** * @param ipt Input text. * @param [opts] Options object. * @param [opts.isDynamicsOnly] If the lexer should only return dynamic tokens, and leave the rest of the input as * unparsed-type tokens. */ lex (ipt, opts) { opts = opts || {}; const lines = ipt .trimRight() .split("\n") .map(it => it.trimRight()) .filter(Boolean) .filter(it => !it.startsWith("#")) // remove comments ; this._indentStack = []; this._tokenStack = []; for (const l of lines) { this._lexLine(l, opts); this._tokenStack.push(Ro_Token.NEWLINE._(l)); // program should always end in a newline } [...new Array(this._indentStack.length)].forEach(() => this._tokenStack.push(Ro_Token.DEDENT)); return this._tokenStack; } _lexLine (l, opts) { opts = opts || {}; let indent = 0; let isBOL = true; let token = ""; let attribParenCount = 0; let parenCount = 0; let braceCount = 0; let mode = null; const outputToken = () => { if (token) { if (opts.isDynamicsOnly) { if (token.startsWith("@") || token.startsWith("(@")) this._tokenStack.push(Ro_Token.DYNAMIC(token, l)); else this._tokenStack.push(Ro_Token.UNPARSED(token, l)); } else { switch (token) { case "(": this._tokenStack.push(Ro_Token.PAREN_OPEN._(l)); break; case ")": this._tokenStack.push(Ro_Token.PAREN_CLOSE._(l)); break; case "if": this._tokenStack.push(Ro_Token.IF._(l)); break; case "else": this._tokenStack.push(Ro_Token.ELSE._(l)); break; case "elif": this._tokenStack.push(Ro_Token.ELIF._(l)); break; case "==": this._tokenStack.push(Ro_Token.EQ._(l)); break; case "!=": this._tokenStack.push(Ro_Token.NE._(l)); break; case ">": this._tokenStack.push(Ro_Token.GT._(l)); break; case "<": this._tokenStack.push(Ro_Token.LT._(l)); break; case ">=": this._tokenStack.push(Ro_Token.GTEQ._(l)); break; case "<=": this._tokenStack.push(Ro_Token.LTEQ._(l)); break; case "not": this._tokenStack.push(Ro_Token.NOT._(l)); break; case "+": case "--": this._tokenStack.push(Ro_Token.ADD._(l)); break; case "-": case "+-": case "-+": this._tokenStack.push(Ro_Token.SUB._(l)); break; case "*": this._tokenStack.push(Ro_Token.MULT._(l)); break; case "/": this._tokenStack.push(Ro_Token.DIV._(l)); break; case "^": this._tokenStack.push(Ro_Token.POW._(l)); break; case ":": this._tokenStack.push(Ro_Token.COLON._(l)); break; case "=": this._tokenStack.push(Ro_Token.ASSIGN._(l)); break; default: { if (token.startsWith("@") || token.startsWith("(@")) this._tokenStack.push(Ro_Token.DYNAMIC(token, l)); else if (Ro_Lexer._M_IDENT.test(token)) this._tokenStack.push(Ro_Token.IDENTIFIER(token, l)); else if (Ro_Lexer._M_NUMBER.test(token)) this._tokenStack.push(Ro_Token.NUMBER(token, l)); else throw new Error(`Syntax error: unexpected token ${token} (line ${l})`); } } } token = ""; } }; outer: for (let i = 0; i < l.length; ++i) { const c = l[i]; const d = l[i + 1]; // handle "beginning of line" case if (isBOL) { if (c === " " || c === "\t") indent++; else { isBOL = false; // If the indent has changed const lastIndent = this._indentStack.last(); if (lastIndent != null) { if (indent > lastIndent) { this._indentStack.push(indent); this._tokenStack.push(Ro_Token.INDENT._(l)); } else { // pop the stack until we find an opening indent that matches ours let nxtIndent; let dedentCount = 0; while (this._indentStack.length) { this._indentStack.pop(); dedentCount++; nxtIndent = this._indentStack.last() || 0; if (nxtIndent === indent) break; } if (nxtIndent === indent) { [...new Array(dedentCount)].forEach(() => this._tokenStack.push(Ro_Token.DEDENT._(l))); } else { // we broke the loop without finding an indent partner; this is a syntax error throw new Error(`Syntax error: no matching indent found for line ${l}`); } } } else { if (indent > 0) { this._indentStack.push(indent); this._tokenStack.push(Ro_Token.INDENT._(l)); } } } } // handle everything else switch (c) { case "#": { // comments if (opts.isDynamicsOnly) { token += c; break; } else break outer; } case " ": { if (attribParenCount) token += c; else if (opts.isDynamicsOnly) token += c; else outputToken(); break; } case ":": { if (attribParenCount) token += c; else if (opts.isDynamicsOnly) token += c; else { outputToken(); token = c; outputToken(); } break; } case "(": if (attribParenCount) { attribParenCount++; token += c; } else { if (d === "@") { // the start of a dynamic attribParenCount++; outputToken(); token += c; } else { // the start of some parentheses if (opts.isDynamicsOnly) token += c; else { parenCount++; outputToken(); token = "("; outputToken(); } } } break; case ")": if (attribParenCount) { attribParenCount--; token += c; if (!attribParenCount) outputToken(); } else if (opts.isDynamicsOnly) token += c; else { parenCount--; if (parenCount < 0) throw new Error(`Syntax error: closing ) without opening ( in line ${l}`); outputToken(); token = ")"; outputToken(); } break; case "{": { if (attribParenCount) token += c; else if (opts.isDynamicsOnly) token += c; else { braceCount++; outputToken(); token = "{"; outputToken(); } break; } case "}": { if (attribParenCount) token += c; else if (opts.isDynamicsOnly) token += c; else { braceCount--; if (braceCount < 0) throw new Error(`Syntax error: closing } without opening { in line ${l}`); outputToken(); token = "}"; outputToken(); } break; } default: { if (attribParenCount) token += c; else if (opts.isDynamicsOnly) { if (c === "@" && token.last() !== "(") { outputToken(); token = "@"; } else token += c; } else { if (Ro_Lexer._M_TEXT_CHAR.test(c)) { if (mode === "symbol") outputToken(); token += c; mode = "text"; } else if (Ro_Lexer._M_SYMBOL_CHAR.test(c)) { if (mode === "text") outputToken(); token += c; mode = "symbol"; } else throw new Error(`Syntax error: unexpected character ${c} in line ${l}`); } break; } } } // empty the stack of any remaining content outputToken(); } } Ro_Lexer._M_TEXT_CHAR = /[a-zA-Z0-9_@]/; Ro_Lexer._M_SYMBOL_CHAR = /[-+/*^=!:><]/; Ro_Lexer._M_NUMBER = /^\d+$/; Ro_Lexer._M_IDENT = /^[a-zA-Z]\w*$/; class Ro_Parser { constructor (lexed) { this._ixSym = -1; this._syms = lexed; this._sym = null; this._lastAccepted = null; } _nextSym () { const cur = this._syms[this._ixSym]; this._ixSym++; this._sym = this._syms[this._ixSym]; return cur; } _peek () { return this._syms[this._ixSym + 1]; } parse () { this._nextSym(); return this._block(); } _match (symbol) { if (this._sym == null) return false; if (symbol.type) symbol = symbol.type; // If it's a Ro_Token, convert it to its underlying type return this._sym.type === symbol; } _accept (symbol) { if (this._match(symbol)) { const out = this._sym; this._nextSym(); this._lastAccepted = out; return out; } return false; } _expect (symbol) { const accepted = this._accept(symbol); if (accepted) return accepted; if (this._sym) throw new Error(`Unexpected input: Expected ${symbol} but found ${this._sym} (line ${this._sym.line})`); else throw new Error(`Unexpected end of input: Expected ${symbol}`); } _factor () { if (this._accept(Ro_Token.TYP_IDENTIFIER)) return new Ro_Parser._Factor(this._lastAccepted); else if (this._accept(Ro_Token.TYP_NUMBER)) return new Ro_Parser._Factor(this._lastAccepted); else if (this._accept(Ro_Token.TYP_DYNAMIC)) return new Ro_Parser._Factor(this._lastAccepted); else if (this._accept(Ro_Token.PAREN_OPEN)) { const exp = this._expression(); this._expect(Ro_Token.PAREN_CLOSE); return new Ro_Parser._Factor(exp, {hasParens: true}); } else { if (this._sym) throw new Error(`Unexpected input: ${this._sym} (line ${this._sym.line})`); else throw new Error(`Unexpected end of input (line ${this._sym.line})`); } } _exponent () { const children = []; children.push(this._factor()); while (this._match(Ro_Token.POW)) { this._nextSym(); // don't bother pushing the "^" as we only deal with one operator type children.push(this._factor()); } return new Ro_Parser._Exponent(children); } _term () { const children = []; children.push(this._exponent()); while (this._match(Ro_Token.MULT) || this._match(Ro_Token.DIV)) { children.push(this._nextSym()); children.push(this._exponent()); } return new Ro_Parser._Term(children); } _expression () { const children = []; if (this._match(Ro_Token.ADD) || this._match(Ro_Token.SUB)) children.push(this._nextSym()); children.push(this._term()); while (this._match(Ro_Token.ADD) || this._match(Ro_Token.SUB)) { children.push(this._nextSym()); children.push(this._term()); } return new Ro_Parser._Expression(children); } _condition () { const children = []; if (this._match(Ro_Token.NOT)) children.push(this._nextSym()); children.push(this._expression()); // any expression can be evaluated as true/false, so the operator + second expression is optional if (this._match(Ro_Token.EQ) || this._match(Ro_Token.NE) || this._match(Ro_Token.GT) || this._match(Ro_Token.LT) || this._match(Ro_Token.GTEQ) || this._match(Ro_Token.LTEQ)) { children.push(this._nextSym()); children.push(this._expression()); } return new Ro_Parser._Condition(children); } _statement () { const children = []; if (this._match(Ro_Token.TYP_NUMBER) || this._match(Ro_Token.ADD) || this._match(Ro_Token.SUB) || this._match(Ro_Token.PAREN_OPEN)) { // e.g. `4` or `1 + 2` etc; all valid stage labels children.push(this._expression()); this._expect(Ro_Token.NEWLINE); } else if (this._match(Ro_Token.TYP_IDENTIFIER)) { if (this._peek() === Ro_Token.ASSIGN) { // a = 1 children.push(this._accept(Ro_Token.TYP_IDENTIFIER)); this._expect(Ro_Token.ASSIGN); children.push(this._expression()); } else { // a (a valid expression) children.push(this._expression()); } this._expect(Ro_Token.NEWLINE); } else if (this._accept(Ro_Token.IF)) { children.push(this._lastAccepted); children.push(this._condition()); this._expect(Ro_Token.COLON); if (this._accept(Ro_Token.NEWLINE)) { children.push(this._block()); } else { // one-liner if children.push(this._statement()); } while (this._accept(Ro_Token.ELIF)) { children.push(this._lastAccepted); children.push(this._condition()); this._expect(Ro_Token.COLON); if (this._accept(Ro_Token.NEWLINE)) { children.push(this._block()); } else { // one-liner elif children.push(this._statement()); } } if (this._accept(Ro_Token.ELSE)) { children.push(this._lastAccepted); this._expect(Ro_Token.COLON); if (this._accept(Ro_Token.NEWLINE)) { children.push(this._block()); } else { // one-liner else children.push(this._statement()); } } } else throw new Error(`Syntax error: ${this._sym} (line ${this._sym.line})`); return new Ro_Parser._Statement(children); } _block () { if (!this._syms.length) { // empty block return new Ro_Parser._Block([]); } else if (this._accept(Ro_Token.INDENT)) { const children = []; while (!this._match(Ro_Token.DEDENT)) { children.push(this._statement()); if (this._match(Ro_Token.INDENT)) children.push(this._block()); } this._expect(Ro_Token.DEDENT); return new Ro_Parser._Block(children); } else { // the root block const children = []; while (this._sym) { children.push(this._statement()); if (this._match(Ro_Token.INDENT)) children.push(this._block()); } return new Ro_Parser._Block(children); } } } Ro_Parser._AbstractSymbol = class { static _indent (str, depth) { return `${" ".repeat(depth * 2)}${str}`; } constructor () { this.type = Ro_Token.TYP_SYMBOL; } eq (symbol) { return symbol && this.type === symbol.type; } pEvl () { throw new Error("Unimplemented!"); } toString () { throw new Error("Unimplemented!"); } }; Ro_Parser._Factor = class extends Ro_Parser._AbstractSymbol { /** * @param node * @param [opts] * @param [opts.hasParens] */ constructor (node, opts) { super(); opts = opts || {}; this._node = node; this._hasParens = !!opts.hasParens; } pEvl (ctx, resolver) { switch (this._node.type) { case Ro_Token.TYP_IDENTIFIER: return {val: Number(ctx[this._node.value])}; case Ro_Token.TYP_NUMBER: return {val: Number(this._node.value)}; case Ro_Token.TYP_DYNAMIC: return Ro_Lang.pResolveDynamic(this._node, resolver); case Ro_Token.TYP_SYMBOL: return this._node.pEvl(ctx, resolver); default: throw new Error(`Unimplemented!`); } } toString (indent = 0) { let out; switch (this._node.type) { case Ro_Token.TYP_IDENTIFIER: out = this._node.value; break; case Ro_Token.TYP_NUMBER: out = this._node.value; break; case Ro_Token.TYP_DYNAMIC: out = `await Char_Lang.pResolveDynamic("${(this._node.value || "").replace(/"/g, `\\"`)}")`; break; case Ro_Token.TYP_SYMBOL: out = this._node.toString(indent); break; default: throw new Error(`Unimplemented!`); } return this._hasParens ? `(${out})` : out; } }; Ro_Parser._Exponent = class extends Ro_Parser._AbstractSymbol { constructor (nodes) { super(); this._nodes = nodes; } async pEvl (ctx, resolver) { // `3 ^ 3 ^ 2` is `3 ^ (3 ^ 2)`, not `(3 ^ 3) ^ 2` // i.e. unlike other operators, power is right-associative instead of left- const view = this._nodes.slice(); const out = await view.pop().pEvl(ctx, resolver); if (out.isCancelled) return out; let tmp = null; while (view.length) { tmp = await view.pop().pEvl(ctx, resolver); if (tmp.isCancelled) return tmp; out.val = tmp.val ** out.val; } return out; } toString (indent = 0) { const view = this._nodes.slice(); let out = view.pop().toString(indent); while (view.length) out = `${view.pop().toString(indent)} ** ${out}`; return out; } }; Ro_Parser._Term = class extends Ro_Parser._AbstractSymbol { constructor (nodes) { super(); this._nodes = nodes; } async pEvl (ctx, resolver) { const out = await this._nodes[0].pEvl(ctx, resolver); if (out.isCancelled) return out; let tmp; for (let i = 1; i < this._nodes.length; i += 2) { if (this._nodes[i].eq(Ro_Token.MULT)) { tmp = await this._nodes[i + 1].pEvl(ctx, resolver); if (tmp.isCancelled) return tmp; out.val *= tmp.val; } else if (this._nodes[i].eq(Ro_Token.DIV)) { tmp = await this._nodes[i + 1].pEvl(ctx, resolver); if (tmp.isCancelled) return tmp; out.val /= tmp.val; } else throw new Error(`Unimplemented!`); } return out; } toString (indent = 0) { let out = this._nodes[0].toString(indent); for (let i = 1; i < this._nodes.length; i += 2) { if (this._nodes[i].eq(Ro_Token.MULT)) out += ` * ${this._nodes[i + 1].toString(indent)}`; else if (this._nodes[i].eq(Ro_Token.DIV)) out += ` / ${this._nodes[i + 1].toString(indent)}`; else throw new Error(`Unimplemented!`); } return out; } }; Ro_Parser._Expression = class extends Ro_Parser._AbstractSymbol { constructor (nodes) { super(); this._nodes = nodes; } async pEvl (ctx, resolver) { const view = this._nodes.slice(); let isNeg = false; if (view[0].eq(Ro_Token.ADD) || view[0].eq(Ro_Token.SUB)) { isNeg = view.shift().eq(Ro_Token.SUB); } const out = await view[0].pEvl(ctx, resolver); if (out.isCancelled) return out; if (isNeg) out.val = -out.val; let tmp; for (let i = 1; i < view.length; i += 2) { if (view[i].eq(Ro_Token.ADD)) { tmp = await view[i + 1].pEvl(ctx, resolver); if (tmp.isCancelled) return tmp; out.val += tmp.val; } else if (view[i].eq(Ro_Token.SUB)) { tmp = await view[i + 1].pEvl(ctx, resolver); if (tmp.isCancelled) return tmp; out.val -= tmp.val; } else throw new Error(`Unimplemented!`); } return out; } toString (indent = 0) { let out = ""; const view = this._nodes.slice(); let isNeg = false; if (view[0].eq(Ro_Token.ADD) || view[0].eq(Ro_Token.SUB)) { isNeg = view.shift().eq(Ro_Token.SUB); if (isNeg) out += "-"; } out += view[0].toString(indent); for (let i = 1; i < view.length; i += 2) { if (view[i].eq(Ro_Token.ADD)) out += ` + ${view[i + 1].toString(indent)}`; else if (view[i].eq(Ro_Token.SUB)) out += ` - ${view[i + 1].toString(indent)}`; else throw new Error(`Unimplemented!`); } return out; } }; Ro_Parser._Condition = class extends Ro_Parser._AbstractSymbol { constructor (nodes) { super(); this._nodes = nodes; this._isNegated = this._nodes[0].eq(Ro_Token.NOT); this._cleanNodes = this._nodes.slice(); if (this._isNegated) this._cleanNodes.shift(); } async pEvl (ctx, resolver) { const out = {isCancelled: false, val: null}; if (this._cleanNodes.length === 3) { const [lhs, op, rhs] = this._cleanNodes; const resultLhs = await lhs.pEvl(ctx, resolver); if (resultLhs.isCancelled) return resultLhs; const resultRhs = await rhs.pEvl(ctx, resolver); if (resultRhs.isCancelled) return resultRhs; switch (op.type) { case Ro_Token.EQ.type: out.val = resultLhs.val === resultRhs.val; break; case Ro_Token.NE.type: out.val = resultLhs.val !== resultRhs.val; break; case Ro_Token.GT.type: out.val = resultLhs.val > resultRhs.val; break; case Ro_Token.LT.type: out.val = resultLhs.val < resultRhs.val; break; case Ro_Token.GTEQ.type: out.val = resultLhs.val >= resultRhs.val; break; case Ro_Token.LTEQ.type: out.val = resultLhs.val <= resultRhs.val; break; default: throw new Error(`Unimplemented!`); } } else if (this._cleanNodes.length === 1) { const resultSub = await this._cleanNodes[0].pEvl(ctx, resolver); if (resultSub.isCancelled) return resultSub; out.val = resultSub.val; } else throw new Error(`Invalid node count!`); if (this._isNegated) out.val = !out.val; return out; } toString (indent = 0) { let out = ""; if (this._cleanNodes.length === 3) { const [lhs, op, rhs] = this._nodes; const lhVal = lhs.toString(indent); const rhVal = rhs.toString(indent); switch (op.type) { case Ro_Token.EQ.type: out += `${lhVal} === ${rhVal}`; break; case Ro_Token.NE.type: out += `${lhVal} !== ${rhVal}`; break; case Ro_Token.GT.type: out += `${lhVal} > ${rhVal}`; break; case Ro_Token.LT.type: out += `${lhVal} < ${rhVal}`; break; case Ro_Token.GTEQ.type: out += `${lhVal} >= ${rhVal}`; break; case Ro_Token.LTEQ.type: out += `${lhVal} <= ${rhVal}`; break; default: throw new Error(`Unimplemented!`); } } else if (this._cleanNodes.length === 1) out += this._cleanNodes[0].toString(indent); else throw new Error(`Invalid node count!`); return this._isNegated ? `!${out}` : out; } }; Ro_Parser._Statement = class extends Ro_Parser._AbstractSymbol { constructor (nodes) { super(); this._nodes = nodes; } async pEvl (ctx, resolver) { switch (this._nodes[0].type) { case Ro_Token.TYP_SYMBOL: return this._nodes[0].pEvl(ctx, resolver); case Ro_Token.TYP_IDENTIFIER: { const [tokIdentifier, expression] = this._nodes; const result = await expression.pEvl(ctx, resolver); if (result.isCancelled) return result; ctx[tokIdentifier.value] = result.val; return result; } case Ro_Token.IF.type: return this._pEvl_pIfElse(ctx, resolver); default: throw new Error(`Unimplemented!`); } } async _pEvl_pIfElse (ctx, resolver) { const parts = []; for (let i = 0; i < this._nodes.length; ++i) { switch (this._nodes[i].type) { case Ro_Token.IF.type: case Ro_Token.ELIF.type: { parts.push({condition: this._nodes[i + 1], toEvl: this._nodes[i + 2]}); i += 2; break; } case Ro_Token.ELSE.type: { parts.push({toEvl: this._nodes[i + 1]}); i += 1; // (not really required since the "else" has to be the last block anyway) break; } } } // find the first of the if-elif-else where the condition is true // (or the condition is null, i.e. for the "else" case) for (const part of parts) { if (part.condition == null) { return part.toEvl.pEvl(ctx, resolver); } else { const result = await part.condition.pEvl(ctx, resolver); if (result.isCancelled) return result; if (result.val) return part.toEvl.pEvl(ctx, resolver); } } return {isCancelled: false, val: null}; } toString (indent = 0) { switch (this._nodes[0].type) { case Ro_Token.TYP_SYMBOL: return Ro_Parser._AbstractSymbol._indent(`return ${this._nodes[0].toString()}\n`, indent); case Ro_Token.TYP_IDENTIFIER: { const [tokIdentifier, expression] = this._nodes; return Ro_Parser._AbstractSymbol._indent(`var ${tokIdentifier.value} = ${expression.toString(indent)}\n`, indent); } case Ro_Token.IF.type: return this._toString_ifElse(indent); default: throw new Error(`Unimplemented!`); } } _toString_ifElse (indent) { let out = ""; for (let i = 0; i < this._nodes.length; ++i) { switch (this._nodes[i].type) { case Ro_Token.IF.type: case Ro_Token.ELIF.type: { out += `${this._nodes[i].eq(Ro_Token.IF) ? "if " : "else if "}`; out += `(${this._nodes[i + 1].toString(indent)}) {\n`; out += `${this._nodes[i + 2].toString(indent + 1)}`; out += `}\n`; i += 2; break; } case Ro_Token.ELSE.type: { out += `else {\n`; out += `${this._nodes[i + 1].toString(indent + 1)}`; out += `}\n`; i += 1; // (not really required since the "else" has to be the last block anyway) break; } } } return out; } }; Ro_Parser._Block = class extends Ro_Parser._AbstractSymbol { constructor (nodes) { super(); this._nodes = nodes; // a list of statements/blocks } async pEvl (ctx, resolver) { // go through our child statements/blocks, and return the first value we find for (const node of this._nodes) { const result = await node.pEvl(ctx, resolver); if (result.isCancelled) return result; if (result.val != null) return result; } return {isCancelled: false, val: null}; } toString (indent = 0) { return this._nodes.map(it => it.toString(indent)).join(""); } }; class Ro_Lang { /** * Validate a program. Returns an error string on error, or null otherwise. * @param ipt * @param resolver Dynamic resolver. */ static async pValidate (ipt, resolver) { // region Lexing const lexer = new Ro_Lexer(); let lexed; try { lexed = lexer.lex(ipt); } catch (e) { return e.message; } // endregion // region Dynamics for (const token of lexed) { if (token.type === Ro_Token.TYP_DYNAMIC) { try { await Ro_Lang.pResolveDynamic(token, resolver, {isValidateOnly: true}); } catch (e) { return e.message; } } } // endregion // region Parsing const parser = new Ro_Parser(lexed); try { parser.parse(); } catch (e) { return e.message; } // endregion return null; } static pRun (ipt, ctx, resolver) { const ctxCpy = MiscUtil.copyFast(ctx); const lexer = new Ro_Lexer(); const lexed = lexer.lex(ipt); const parser = new Ro_Parser(lexed); const parsed = parser.parse(); return parsed.pEvl(ctxCpy, resolver); } static async pResolveDynamics (ipt, resolver) { const lexer = new Ro_Lexer(); const lexed = lexer.lex(ipt, {isDynamicsOnly: true}); let out = ""; for (const tkn of lexed) { switch (tkn.type) { case Ro_Token.TYP_UNPARSED: out += tkn.value; break; case Ro_Token.TYP_DYNAMIC: out += (await this.pResolveDynamic(tkn, resolver)).val; break; case Ro_Token.NEWLINE.type: out += "\n"; break; default: throw new Error(`Unhandled token type: ${tkn.type}`); // should never occur } } return out; } static async pValidateDynamics (ipt, resolver) { const lexer = new Ro_Lexer(); const lexed = lexer.lex(ipt, {isDynamicsOnly: true}); for (const tkn of lexed) { if (tkn.type === Ro_Token.TYP_DYNAMIC) { const msgInvalid = await this.pResolveDynamic(tkn, resolver, {isValidateOnly: true}); if (msgInvalid) return msgInvalid; } } return null; } /** * @param token The dynamic Ro_Token to resolve. * @param resolver Dynamic name resolver. Should have a `.has()` method and a `.get()` method. * @param [opts] Options object * @param [opts.isValidateOnly] If the run should validate only, and avoid fetching data. */ static pResolveDynamic (token, resolver, opts) { opts = opts || {}; const getInvalidMessage = (type) => `Unknown property: ${type} (line ${token.line})`; const clean = token.value.replace(/^\(?@(.*?)\)?$/, "$1"); const [type, ...labelParts] = clean.split("|").map(it => it.trim()); while (labelParts.length && !labelParts.last()) labelParts.pop(); // pop empty strings from the end of the array switch (type) { case "user_int": return this._pResolveDynamic_getUserInt(token, labelParts, opts); case "user_bool": return this._pResolveDynamic_getUserBool(token, labelParts, opts); default: { if (opts.isValidateOnly) { if (resolver.has(type)) return null; else return getInvalidMessage(type); } else { if (resolver.has(type)) return {isCancelled: false, val: resolver.get(type)}; throw new Error(getInvalidMessage(type)); } } } } static async _pResolveDynamic_getUserInt (token, labelParts, opts) { opts = opts || {}; const out = {isCancelled: false, val: null}; if (labelParts.length <= 1) { if (opts.isValidateOnly) return; const nxtOpts = {int: true}; if (labelParts.length) nxtOpts.title = labelParts[0].trim(); const val = await InputUiUtil.pGetUserNumber(nxtOpts); if (val == null) out.isCancelled = true; else out.val = val; } else { // Format: ...|Window Title|1=Label One|2=Label Two|3|4|... const titlePart = labelParts[0].trim(); const choices = labelParts.slice(1).map(it => { const spl = it.split("=").map(it => it.trim()); const asNum = Number(spl[0]); if (isNaN(asNum)) throw new Error(`Syntax error: Option ${spl[0]} was not a number (line ${token.line})`); if (spl.length === 1) return {label: asNum, val: asNum}; else if (spl.length === 2) return {label: spl[1], val: asNum}; else throw new Error(`Syntax error: option ${it} was not formatted correctly (line ${token.line})`); }); if (opts.isValidateOnly) return; const ixOut = await InputUiUtil.pGetUserEnum({ fnDisplay: it => it.label, values: choices, title: titlePart, }); if (ixOut == null) out.isCancelled = true; else out.val = choices[ixOut].val; } return out; } static async _pResolveDynamic_getUserBool (token, labelParts, opts) { opts = opts || {}; const out = {isCancelled: false, val: null}; if (labelParts.length <= 1) { if (opts.isValidateOnly) return; const nxtOpts = {}; if (labelParts.length) nxtOpts.title = labelParts[0].trim(); const val = await InputUiUtil.pGetUserBoolean(nxtOpts); if (val == null) out.isCancelled = true; else out.val = val; } else if (labelParts.length === 3) { // Format: ...|Window title|True Label|False Label if (opts.isValidateOnly) return; const nxtOpts = { title: labelParts[0].trim(), textYes: labelParts[1].trim(), textNo: labelParts[2].trim(), }; const val = await InputUiUtil.pGetUserBoolean(nxtOpts); if (val == null) out.isCancelled = true; else out.val = val; } else { // Format: ...|Window title|true=Label One|false=Label Two|true|false|... const titlePart = labelParts[0].trim(); const choices = labelParts.slice(1).map(it => { const spl = it.split("=").map(it => it.trim()); const asBool = UiUtil.strToBool(spl[0], null); if (asBool == null) throw new Error(`Syntax error: Option ${spl[0]} was not a boolean (line ${token.line}`); if (spl.length === 1) return {label: asBool, val: asBool}; else if (spl.length === 2) return {label: spl[1], val: asBool}; else throw new Error(`Syntax error: option ${it} was not formatted correctly (line ${token.line})`); }); if (opts.isValidateOnly) return; const ixOut = await InputUiUtil.pGetUserEnum({ fnDisplay: it => it.label, values: choices, title: titlePart, }); if (ixOut == null) out.isCancelled = true; else out.val = choices[ixOut].val; } return out; } } export {Ro_Token, Ro_Lexer, Ro_Parser, Ro_Lang};