diff options
| author | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-07-05 12:42:44 -0700 |
|---|---|---|
| committer | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-07-05 12:42:44 -0700 |
| commit | 94b912b649150b9863a62096ba4740b2cc8ad21a (patch) | |
| tree | 1882236fc980fa5946d3089a2f2e4670f3a70580 /src/toys/tabloid/js/tabloid.js | |
| parent | ab0ddcaeb1fc37351d14b89100e07d3a343ba9e1 (diff) | |
| download | lizdotcoffee-94b912b649150b9863a62096ba4740b2cc8ad21a.tar.gz lizdotcoffee-94b912b649150b9863a62096ba4740b2cc8ad21a.zip | |
Factor
Diffstat (limited to 'src/toys/tabloid/js/tabloid.js')
| -rw-r--r-- | src/toys/tabloid/js/tabloid.js | 707 |
1 files changed, 0 insertions, 707 deletions
diff --git a/src/toys/tabloid/js/tabloid.js b/src/toys/tabloid/js/tabloid.js deleted file mode 100644 index 468ee9a..0000000 --- a/src/toys/tabloid/js/tabloid.js +++ /dev/null @@ -1,707 +0,0 @@ -/* 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(str.trim()); - 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; - } - wordifyString(endChar) { - let acc = ''; - acc += this.reader.readUntil(c => c == endChar); - while (acc.endsWith('\\') || !this.reader.hasNext()) { - acc = acc.substr(0, acc.length - 1); - this.reader.next(); // endChar - acc += endChar + 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'), - RumorHasIt: Symbol('RumorHasIt'), - WhatIf: Symbol('WhatIf'), - LiesBang: Symbol('LiesBang'), - EndOfStory: Symbol('EndOfStory'), - ExpertsClaim: Symbol('ExpertsClaim'), - ToBe: Symbol('ToBe'), - YouWontWantToMiss: Symbol('YouWontWantToMiss'), - LatestNewsOn: Symbol('LatestNewsOn'), - TotallyRight: Symbol('TotallyRight'), - CompletelyWrong: Symbol('CompletelyWrong'), - IsActually: Symbol('IsActually'), - And: Symbol('And'), - Or: Symbol('Or'), - Plus: Symbol('Plus'), - Minus: Symbol('Minus'), - Times: Symbol('Times'), - DividedBy: Symbol('DividedBy'), - Modulo: Symbol('Modulo'), - Beats: Symbol('Beats'), // > - SmallerThan: Symbol('SmallerThan'), // < - ShockingDevelopment: Symbol('ShockingDevelopment'), - PleaseLikeAndSubscribe: Symbol('PleaseLikeAndSubscribe'), -} - -const BINARY_OPS = [ - T.IsActually, - T.And, - T.Or, - T.Plus, - T.Minus, - T.Times, - T.DividedBy, - T.Modulo, - T.Beats, - T.SmallerThan, -]; - -export 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 'RUMOR': { - reader.expect('HAS'); - reader.expect('IT'); - tokens.push(T.RumorHasIt); - 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 'LATEST': { - reader.expect('NEWS'); - reader.expect('ON'); - tokens.push(T.LatestNewsOn); - 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 'PLUS': { - tokens.push(T.Plus); - break; - } - case 'MINUS': { - tokens.push(T.Minus); - break; - } - case 'TIMES': { - tokens.push(T.Times); - break; - } - case 'DIVIDED': { - reader.expect('BY'); - tokens.push(T.DividedBy); - 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 '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'), - BoolLiteral: Symbol('BoolLiteral'), - 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'), - InputExpr: Symbol('InputExpr'), -} - -export class Parser { - constructor(tokens) { - this.tokens = new Reader(tokens, []); - } - /** - * Atom - * Ident - * NumberLiteral - * StringLiteral - * BoolLiteral - * FnCall - * FnDecl - * ExprGroup - * - * Expression: - * (begins with atom) - * BinaryOp - * Atom - * (begins with keyword) - * IfExpr - * Assignment - * ReturnExpr - * ProgEndExpr - * PrintExpr - * InputExpr - * - */ - parse() { - const nodes = []; - while (this.tokens.hasNext()) { - nodes.push(this.expr()); - } - - if (nodes[nodes.length - 1].type !== N.ProgEndExpr) { - throw new Error('Parsing error: A Tabloid program MUST end with PLEASE LIKE AND SUBSCRIBE'); - } - - 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.TotallyRight) { - 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(), - } - } - } else if (next === T.RumorHasIt) { - // 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, - }; - } else if (next === T.LParen) { - // block, but guarded by parens, for binary exprs - const exprs = []; - while (this.tokens.hasNext() && this.tokens.peek() !== T.RParen) { - exprs.push(this.expr()); - } - this.tokens.expect(T.RParen); - 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(), - } - } else if (next === T.LatestNewsOn) { - // input expr - return { - type: N.InputExpr, - val: this.expr(), - } - } - - this.tokens.backstep(); - const atom = this.atom(); - if (BINARY_OPS.includes(this.tokens.peek())) { - // infix binary ops - 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); - const args = [this.expr()]; - while (this.tokens.peek() === T.Comma) { - this.tokens.next(); // comma - args.push(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; - } -} - -export class Environment { - constructor(runtime) { - /** - * Runtime contains the following functions: - * - print(s) - * - input(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: - case N.StringLiteral: - case N.BoolLiteral: - return node.val; - case N.FnDecl: { - const fnValue = { - node: node, - closure: { ...scope } - }; - scope[node.name] = fnValue; - return fnValue; - } - case N.FnCall: { - const fn = this.eval(node.fn); - const args = node.args.map(arg => this.eval(arg)); - - const calleeScope = {}; - fn.node.args.forEach((argName, i) => { - calleeScope[argName] = args[i]; - }); - - this.scopes.push(fn.closure); - this.scopes.push(calleeScope); - let rv; - try { - this.eval(fn.node.body); - } catch (maybeReturnErr) { - if (maybeReturnErr instanceof ReturnError) { - rv = maybeReturnErr.unwrap(); - } else { - throw maybeReturnErr; - } - } - this.scopes.pop(); - 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) { - case T.IsActually: - return left === right; - case T.And: - return left && right; - case T.Or: - return left || right; - case T.Plus: - return left + right; - case T.Minus: - return left - right; - case T.Times: - return left * right; - case T.DividedBy: - return left / right; - case T.Modulo: - return left % right; - case T.Beats: - return left > right; - case T.SmallerThan: - 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: { - if (!node.exprs.length) { - throw new Error('Runtime error: Empty expression group with no expressions'); - } - - let rv; - 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: { - let val = this.eval(node.val); - // shim for boolean to-string's - if (val === true) { - val = 'TOTALLY RIGHT'; - } else if (val === false) { - val = 'COMPLETELY WRONG'; - } - this.runtime.print(val); - return val; - } - case N.InputExpr: { - let val = this.eval(node.val); - // shim for boolean to-string's - if (val === true) { - val = 'TOTALLY RIGHT'; - } else if (val === false) { - val = 'COMPLETELY WRONG'; - } - return this.runtime.input(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)}`); - } - } -} |
