diff --git a/static/index.html b/static/index.html index a5235bd..9fa0e41 100644 --- a/static/index.html +++ b/static/index.html @@ -4,8 +4,8 @@ Tabloid: the clickbait headline programming language - - + + @@ -21,7 +21,7 @@ - - - + + + diff --git a/static/js/lang.js b/static/js/lang.js index 50fe098..d9330a9 100644 --- a/static/js/lang.js +++ b/static/js/lang.js @@ -41,7 +41,7 @@ class Reader { } } -/** +/** * Split into words for easier tokenization * with keywords. */ @@ -77,7 +77,7 @@ class Wordifier { // read until WS this.reader.backstep(); this.tokens.push(this.reader.readUntil(c => { - return !c.trim() || ['(', ')', ','].includes(c) + return !c.trim() || ['(', ')', ','].includes(c); })); } } @@ -103,6 +103,7 @@ const T = { RParen: Symbol('RParen'), Comma: Symbol('Comma'), DiscoverHowTo: Symbol('DiscoverHowTo'), + Breaking: Symbol('Breaking'), With: Symbol('With'), Of: Symbol('Of'), RumorHasIt: Symbol('RumorHasIt'), @@ -127,7 +128,7 @@ const T = { SmallerThan: Symbol('SmallerThan'), // < ShockingDevelopment: Symbol('ShockingDevelopment'), PleaseLikeAndSubscribe: Symbol('PleaseLikeAndSubscribe'), -} +}; const BINARY_OPS = [ T.IsActually, @@ -155,6 +156,10 @@ function tokenize(prog) { tokens.push(T.DiscoverHowTo); break; } + case 'BREAKING': { + tokens.push(T.Breaking); + break; + } case 'WITH': { tokens.push(T.With); break; @@ -364,24 +369,37 @@ class Parser { } throw new Error(`Parsing error: expected identifier, got ${ident.toString()}`); } + readFnArgs() { + let args = []; + if (this.tokens.peek(T.With)) { + this.tokens.next(); // with + // with args + args.push(this.expectIdentString()); + while (this.tokens.peek() === T.Comma) { + this.tokens.next(); // comma + args.push(this.expectIdentString()); + } + } + return args; + } 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); } @@ -390,37 +408,25 @@ class Parser { return { type: N.BoolLiteral, val: true, - } + }; } else if (next === T.CompletelyWrong) { return { type: N.BoolLiteral, val: false, - } + }; } 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(), - } - } + return { + type: N.FnDecl, + name: this.tokens.next(), + args: this.readFnArgs(), + body: this.expr(), + }; + } else if (next === T.Breaking) { + return { + type: N.FnDecl, + args: this.readFnArgs(), + body: this.expr(), + }; } else if (next === T.RumorHasIt) { // block const exprs = []; @@ -466,7 +472,7 @@ class Parser { cond: cond, ifBody: ifBody, elseBody: elseBody, - } + }; } else if (next === T.ExpertsClaim) { // assignment const name = this.expectIdentString(); @@ -476,30 +482,30 @@ class Parser { 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(), - } + }; } else if (next === T.LatestNewsOn) { // input expr return { type: N.InputExpr, val: this.expr(), - } + }; } this.tokens.backstep(); @@ -514,7 +520,7 @@ class Parser { op, left, right, - } + }; } return atom; @@ -530,7 +536,7 @@ class Parser { type: N.FnCall, fn: fnNode, args: args, - } + }; } } @@ -549,16 +555,46 @@ class ReturnError { } class Environment { - constructor(runtime) { - /** - * Runtime contains the following functions: - * - print(s) - * - input(s) - */ - this.runtime = runtime; - this.scopes = [{}]; // begin with global scope + constructor(parent) { + this.vars = Object.create(parent ? parent.vars : null); + this.parent = parent; + if (parent) { + /** + * Runtime contains the following functions: + * - print(s) + * - input(s) + */ + this.runtime = parent.runtime; + } + } + extend() { + return new Environment(this); + } + lookup(name) { + let scope = this; + while (scope) { + if (Object.prototype.hasOwnProperty.call(scope.vars, name)) + return scope; + scope = scope.parent; + } + } + get(name) { + if (name in this.vars) + return this.vars[name]; + throw new Error("Undefined variable " + name); + } + set(name, value) { + let scope = this.lookup(name); + if (!scope && this.parent) + throw new Error("Undefined variable " + name); + return (scope || this).vars[name] = value; + } + def(name, value) { + return this.vars[name] = value; } - run(nodes) { + + run(nodes, runtime) { + this.runtime = runtime; let rv; for (const node of nodes) { rv = this.eval(node); @@ -566,30 +602,33 @@ class Environment { return rv; } eval(node) { - const scope = this.scopes[this.scopes.length - 1]; - switch (node.type) { case N.NumberLiteral: case N.StringLiteral: case N.BoolLiteral: return node.val; case N.FnDecl: { - scope[node.name] = node; - return node; + let closure = { + env: this, + node: node + }; + if (node.name) { + this.def(node.name, closure); + } + return closure; } 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]; + const calleeEnv = fn.env.extend(); + fn.node.args.forEach((argName, i) => { + calleeEnv.def(argName, args[i]); }); - this.scopes.push(calleeScope); let rv; try { - this.eval(fn.body); + calleeEnv.eval(fn.node.body); } catch (maybeReturnErr) { if (maybeReturnErr instanceof ReturnError) { rv = maybeReturnErr.unwrap(); @@ -598,48 +637,37 @@ class Environment { 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}"`); + return this.get(node.val); } case N.Assignment: { - scope[node.name] = this.eval(node.val); - return scope[node.name]; + return this.set(node.name, this.eval(node.val)); } case N.BinaryOp: { - const left = this.eval(node.left); - const right = this.eval(node.right); switch (node.op) { case T.IsActually: - return left === right; + return this.eval(node.left) === this.eval(node.right); case T.And: - return left && right; + return this.eval(node.left) && this.eval(node.right); case T.Or: - return left || right; + return this.eval(node.left) || this.eval(node.right); case T.Plus: - return left + right; + return this.eval(node.left) + this.eval(node.right); case T.Minus: - return left - right; + return this.eval(node.left) - this.eval(node.right); case T.Times: - return left * right; + return this.eval(node.left) * this.eval(node.right); case T.DividedBy: - return left / right; + return this.eval(node.left) / this.eval(node.right); case T.Modulo: - return left % right; + return this.eval(node.left) % this.eval(node.right); case T.Beats: - return left > right; + return this.eval(node.left) > this.eval(node.right); case T.SmallerThan: - return left < right; + return this.eval(node.left) < this.eval(node.right); default: throw new Error(`Runtime error: Unknown binary op ${node.op.toString()}`); } diff --git a/static/js/main.js b/static/js/main.js index e14b8a6..e0e472d 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -66,7 +66,7 @@ class Editor extends Component { this.handleInput = evt => { this.prog = evt.target.value; this.render(); - } + }; this.handleKeydown = evt => { if (evt.key === 'Tab') { evt.preventDefault(); @@ -79,17 +79,17 @@ class Editor extends Component { evt.target.setSelectionRange(idx + 4, idx + 4); } } - } + }; this.setFactorial = () => { this.prog = PROG_FACTORIAL; this.output = this.errors = ''; this.render(); - } + }; this.setFibonacci= () => { this.prog = PROG_FIBONACCI; this.output = this.errors = ''; this.render(); - } + }; } eval() { this.output = ''; @@ -97,8 +97,10 @@ class Editor extends Component { try { const tokens = tokenize(this.prog); const nodes = new Parser(tokens).parse(); - const env = new Environment({ + const env = new Environment(); + env.run(nodes, { print: s => { + console.log(s); this.output += s.toString().toUpperCase() + '!\n'; this.render(); }, @@ -106,7 +108,6 @@ class Editor extends Component { return prompt(s); }, }); - env.run(nodes); } catch (e) { this.errors = e.toString(); }