diff options
Diffstat (limited to 'static/js/lang.js')
-rw-r--r-- | static/js/lang.js | 637 |
1 files changed, 637 insertions, 0 deletions
diff --git a/static/js/lang.js b/static/js/lang.js new file mode 100644 index 0000000..d5ed749 --- /dev/null +++ b/static/js/lang.js @@ -0,0 +1,637 @@ +/* Tabloid: the clickbait headline programming language */ + +/* tokenizer */ + +/** + * Reads in char or word chunks + */ +class Reader { + constructor(str, base = '') { + this.base = base; + this.i = 0; + this.str = str; + } + peek() { + return this.str[this.i]; + } + next() { + return this.str[this.i++]; + } + hasNext() { + return this.str[this.i] !== undefined; + } + backstep() { + this.i--; + } + readUntil(pred) { + let result = this.base.slice(); + while (this.hasNext() && !pred(this.peek())) { + result += this.next(); + } + return result; + } + dropWhitespace() { + this.readUntil(c => !!c.trim()); + } + expect(tok) { + const next = this.next(); + if (next !== tok) { + throw new Error(`Parsing error: expected ${tok}, got ${next}`); + } + } +} + +/** + * Split into words for easier tokenization + * with keywords. + */ +class Wordifier { + constructor(str) { + this.reader = new Reader(prog); + this.tokens = []; + } + wordify() { + if (this.tokens.length) return this.tokens; + + while (this.reader.hasNext()) { + const next = this.reader.next(); + switch (next) { + case '(': { + this.tokens.push('('); + break; + } + case ')': { + this.tokens.push(')'); + break; + } + case ',': { + this.tokens.push(','); + break; + } + case '"': + case "'": { + this.wordifyString(next); + break; + } + default: { + // read until WS + this.reader.backstep(); + this.tokens.push(this.reader.readUntil(c => { + return !c.trim() || ['(', ')', ','].includes(c) + })); + } + } + this.reader.dropWhitespace(); + } + return this.tokens.slice(1); + } + wordifyString(endChar) { + let acc = ''; + acc += this.reader.readUntil(c => c == endChar); + while (acc.endsWith('\\') || !this.reader.hasNext()) { + acc += this.reader.readUntil(c => c != endChar); + } + this.reader.next(); // throw away closing char + this.tokens.push('"' + acc); + } +} + +const T = { + LParen: Symbol('LParen'), + RParen: Symbol('RParen'), + Comma: Symbol('Comma'), + DiscoverHowTo: Symbol('DiscoverHowTo'), + With: Symbol('With'), + Of: Symbol('Of'), + WeSaid: Symbol('WeSaid'), + WhatIf: Symbol('WhatIf'), + LiesBang: Symbol('LiesBang'), + EndOfStory: Symbol('EndOfStory'), + ExpertsClaim: Symbol('ExpertsClaim'), + ToBe: Symbol('ToBe'), + YouWontWantToMiss: Symbol('YouWontWantToMiss'), + IsActually: Symbol('IsActually'), + And: Symbol('And'), + Or: Symbol('Or'), + Add: Symbol('Add'), + Subtract: Symbol('Subtract'), + Multiply: Symbol('Multiply'), + Divide: Symbol('Divide'), + Modulo: Symbol('Modulo'), + Beats: Symbol('Beats'), // > + SmallerThan: Symbol('SmallerThan'), // < + ShockingDevelopment: Symbol('ShockingDevelopment'), + PleaseLikeAndSubscribe: Symbol('PleaseLikeAndSubscribe'), + + // not implemented yet + StayTuned: Symbol('StayTuned'), + Unexpectedly: Symbol('Unexpectedly'), + TotallyRight: Symbol('TotallyRight'), + CompletelyWrong: Symbol('CompletelyWrong'), +} + +const BINARY_OPS = [ + T.IsActually, + T.And, + T.Or, + T.Add, + T.Subtract, + T.Multiply, + T.Divide, + T.Modulo, + T.Beats, + T.SmallerThan, +]; + +function tokenize(prog) { + const reader = new Reader(new Wordifier(prog).wordify(), []); + const tokens = []; + + while (reader.hasNext()) { + const next = reader.next(); + switch (next) { + case 'DISCOVER': { + reader.expect('HOW'); + reader.expect('TO'); + tokens.push(T.DiscoverHowTo); + break; + } + case 'WITH': { + tokens.push(T.With); + break; + } + case 'OF': { + tokens.push(T.Of); + break; + } + case 'WE': { + reader.expect('SAID'); + tokens.push(T.WeSaid); + break; + } + case 'WHAT': { + reader.expect('IF'); + tokens.push(T.WhatIf); + break; + } + case 'LIES!': { + tokens.push(T.LiesBang); + break; + } + case 'END': { + reader.expect('OF'); + reader.expect('STORY'); + tokens.push(T.EndOfStory); + break; + } + case 'EXPERTS': { + reader.expect('CLAIM'); + tokens.push(T.ExpertsClaim); + break; + } + case 'TO': { + reader.expect('BE'); + tokens.push(T.ToBe); + break; + } + case 'YOU': { + reader.expect('WON\'T'); + reader.expect('WANT'); + reader.expect('TO'); + reader.expect('MISS'); + tokens.push(T.YouWontWantToMiss); + break; + } + case 'IS': { + reader.expect('ACTUALLY'); + tokens.push(T.IsActually); + break; + } + case 'AND': { + tokens.push(T.And); + break; + } + case 'OR': { + tokens.push(T.Or); + break; + } + case 'ADD': { + tokens.push(T.Add); + break; + } + case 'SUBTRACT': { + tokens.push(T.Subtract); + break; + } + case 'MULTIPLY': { + tokens.push(T.Multiply); + break; + } + case 'DIVIDE': { + tokens.push(T.Divide); + break; + } + case 'MODULO': { + tokens.push(T.Modulo); + break; + } + case 'BEATS': { + tokens.push(T.Beats); + break; + } + case 'SMALLER': { + reader.expect('THAN'); + tokens.push(T.SmallerThan); + break; + } + case 'SHOCKING': { + reader.expect('DEVELOPMENT'); + tokens.push(T.ShockingDevelopment); + break; + } + case 'PLEASE': { + reader.expect('LIKE'); + reader.expect('AND'); + reader.expect('SUBSCRIBE'); + tokens.push(T.PleaseLikeAndSubscribe); + break; + } + case 'STAY': { + reader.expect('TUNED'); + tokens.push(T.StayTuned); + break; + } + case 'UNEXPECTEDLY': { + tokens.push(T.Unexpectedly); + break; + } + case 'TOTALLY': { + reader.expect('RIGHT'); + tokens.push(T.TotallyRight); + break; + } + case 'COMPLETELY': { + reader.expect('WRONG'); + tokens.push(T.CompletelyWrong); + break; + } + case '(': { + tokens.push(T.LParen); + break; + } + case ')': { + tokens.push(T.RParen); + break; + } + case ',': { + tokens.push(T.Comma); + break; + } + default: { + if (!isNaN(parseFloat(next))) { + // number literal + tokens.push(parseFloat(next)); + } else { + // string or varname + tokens.push(next); + } + } + } + } + return tokens; +} + +/* parser */ + +const N = { + NumberLiteral: Symbol('NumberLiteral'), + StringLiteral: Symbol('StringLiteral'), + FnDecl: Symbol('FnDecl'), + FnCall: Symbol('FnCall'), + Ident: Symbol('Ident'), + Assignment: Symbol('Assignment'), + BinaryOp: Symbol('BinaryOp'), + IfExpr: Symbol('IfExpr'), + ExprGroup: Symbol('ExprGroup'), + ReturnExpr: Symbol('ReturnExpr'), + ProgEndExpr: Symbol('ProgEndExpr'), + PrintExpr: Symbol('PrintExpr'), +} + +class Parser { + constructor(tokens) { + this.tokens = new Reader(tokens, []); + } + /** + * Atom + * Ident + * NumberLiteral + * StringLiteral + * FnCall + * FnDecl + * ExprGroup + * + * Expression: + * (begins with atom) + * BinaryOp + * Atom + * (begins with keyword) + * IfExpr + * Assignment + * ReturnExpr + * ProgEndExpr + * PrintExpr + * + */ + parse() { + const nodes = []; + while (this.tokens.hasNext()) { + nodes.push(this.expr()); + } + return nodes; + } + expectIdentString() { + const ident = this.tokens.next(); + if (typeof ident === 'string' && !ident.startsWith('"')) { + return ident; + } + throw new Error(`Parsing error: expected identifier, got ${ident.toString()}`); + } + atom() { + const next = this.tokens.next(); + if (typeof next === 'number') { + return { + type: N.NumberLiteral, + val: next, + } + } else if (typeof next === 'string') { + if (next.startsWith('"')) { + return { + type: N.StringLiteral, + val: next.substr(1), + } + } + const ident = { + type: N.Ident, + val: next, + } + if (this.tokens.peek() === T.Of) { + return this.fnCall(ident); + } + return ident; + } else if (next === T.DiscoverHowTo) { + // fn literal + const fnName = this.tokens.next(); + if (this.tokens.peek(T.With)) { + this.tokens.next(); // with + // with args + const args = [this.expectIdentString()]; + while (this.tokens.peek() === T.Comma) { + this.tokens.next(); // comma + args.push(this.expectIdentString()); + } + return { + type: N.FnDecl, + name: fnName, + args: args, + body: this.expr(), + } + } else { + return { + type: N.FnDecl, + name: fnName, + args: [], + body: this.expr(), + } + } + } else if (next === T.WeSaid) { + // block + const exprs = []; + while (this.tokens.hasNext() && this.tokens.peek() !== T.EndOfStory) { + exprs.push(this.expr()); + } + this.tokens.expect(T.EndOfStory); + return { + type: N.ExprGroup, + exprs: exprs, + }; + } + + throw new Error(`Parsing error: expected ident, literal, or block, got ${ + next.toString() + } before ${this.tokens.peek().toString()}`); + } + expr() { + const next = this.tokens.next(); + if (next === T.WhatIf) { + // if expr + const cond = this.expr(); + const ifBody = this.expr(); + + let elseBody = null; + if (this.tokens.peek() == T.LiesBang) { + this.tokens.next(); // LiesBang + elseBody = this.expr(); + } + return { + type: N.IfExpr, + cond: cond, + ifBody: ifBody, + elseBody: elseBody, + } + } else if (next === T.ExpertsClaim) { + // assignment + const name = this.expectIdentString(); + this.tokens.expect(T.ToBe); + const val = this.expr(); + return { + type: N.Assignment, + name, + val, + } + } else if (next === T.ShockingDevelopment) { + // return + return { + type: N.ReturnExpr, + val: this.expr(), + } + } else if (next === T.PleaseLikeAndSubscribe) { + // prog end + return { + type: N.ProgEndExpr, + } + } else if (next == T.YouWontWantToMiss) { + // print expr + return { + type: N.PrintExpr, + val: this.expr(), + } + } + + this.tokens.backstep(); + const atom = this.atom(); + if (BINARY_OPS.includes(this.tokens.peek())) { + // infix binary ops + // TODO: support operator precedence + const left = atom; + const op = this.tokens.next(); + const right = this.atom(); + return { + type: N.BinaryOp, + op, + left, + right, + } + } + + return atom; + } + fnCall(fnNode) { + this.tokens.expect(T.Of); + // TODO: support multiple arguments + const args = [this.expr()]; + return { + type: N.FnCall, + fn: fnNode, + args: args, + } + } +} + +/* executor (tree walk) */ + +/** + * Abused (slightly) to easily return values upstack + */ +class ReturnError { + constructor(value) { + this.value = value; + } + unwrap() { + return this.value; + } +} + +class Environment { + constructor(runtime) { + /** + * Runtime contains the following functions: + * - print(s) + */ + this.runtime = runtime; + this.scopes = [{}]; // begin with global scope + } + run(nodes) { + let rv; + for (const node of nodes) { + rv = this.eval(node); + } + return rv; + } + eval(node) { + const scope = this.scopes[this.scopes.length - 1]; + + switch (node.type) { + case N.NumberLiteral: { + return node.val; + } + case N.StringLiteral: { + return node.val; + } + case N.FnDecl: { + scope[node.name] = node; + return node; + } + case N.FnCall: { + const fn = this.eval(node.fn); + const args = node.args.map(arg => this.eval(arg)); + + const calleeScope = {}; + fn.args.forEach((argName, i) => { + calleeScope[argName] = args[i]; + }); + + this.scopes.push(calleeScope); + let rv; + try { + this.eval(fn.body); + } catch (maybeReturnErr) { + if (maybeReturnErr instanceof ReturnError) { + rv = maybeReturnErr.unwrap(); + } else { + // re-throw + throw maybeReturnErr; + } + } + this.scopes.pop(); + + return rv; + } + case N.Ident: { + let i = this.scopes.length - 1; + while (i >= 0) { + if (node.val in this.scopes[i]) { + return this.scopes[i][node.val]; + } + i --; + } + throw new Error(`Runtime error: Undefined variable "${node.val}"`); + } + case N.Assignment: { + scope[node.name] = this.eval(node.val); + return scope[node.name]; + } + case N.BinaryOp: { + const left = this.eval(node.left); + const right = this.eval(node.right); + switch (node.op) { + // TODO: other ops + case T.IsActually: + return left === right; + case T.Add: + return left + right; + case T.Subtract: + return left - right; + case T.Multiply: + return left * right; + default: + throw new Error(`Runtime error: Unknown binary op ${node.op.toString()}`); + } + } + case N.IfExpr: { + if (this.eval(node.cond)) { + return this.eval(node.ifBody); + } + if (node.elseBody != null) { + return this.eval(node.elseBody); + } + } + case N.ExprGroup: { + let rv = false; // TODO: make null value? make this illegal? + for (const expr of node.exprs) { + rv = this.eval(expr); + } + return rv; + } + case N.ReturnExpr: { + const rv = this.eval(node.val); + throw new ReturnError(rv); + } + case N.ProgEndExpr: { + // do nothing + break; + } + case N.PrintExpr: { + const val = this.eval(node.val); + Runtime.print(val); + return val; + } + default: + console.log(JSON.stringify(node, null, 2)); + throw new Error(`Runtime error: Unknown AST Node of type ${ + node.type.toString() + }:\n${JSON.stringify(node, null, 2)}`); + } + } +} + |