From 91b7598b22f89319f64054daf42c950de3eb6451 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Wed, 7 Jan 2026 19:29:30 -0800 Subject: Adding some of my favorite toys --- src/toys/tabloid/js/main.js | 5 + src/toys/tabloid/js/playground.js | 120 +++++++ src/toys/tabloid/js/samples.js | 308 +++++++++++++++++ src/toys/tabloid/js/tabloid.js | 707 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 1140 insertions(+) create mode 100644 src/toys/tabloid/js/main.js create mode 100644 src/toys/tabloid/js/playground.js create mode 100644 src/toys/tabloid/js/samples.js create mode 100644 src/toys/tabloid/js/tabloid.js (limited to 'src/toys/tabloid/js') diff --git a/src/toys/tabloid/js/main.js b/src/toys/tabloid/js/main.js new file mode 100644 index 0000000..1f0b5dd --- /dev/null +++ b/src/toys/tabloid/js/main.js @@ -0,0 +1,5 @@ +import { TabloidPlayground } from "./playground.js"; + +document.addEventListener("DOMContentLoaded", () => { + new TabloidPlayground(); +}); diff --git a/src/toys/tabloid/js/playground.js b/src/toys/tabloid/js/playground.js new file mode 100644 index 0000000..d6ff71a --- /dev/null +++ b/src/toys/tabloid/js/playground.js @@ -0,0 +1,120 @@ +import { tokenize, Parser, Environment } from "./tabloid.js"; +import { SAMPLE_PROGRAMS } from "./samples.js"; + +class TabloidRunner { + run(code) { + const stdout = []; + const stderr = []; + + if (!code) { + stderr.push("No code to execute."); + return { stdout, stderr }; + } + + try { + const tokens = tokenize(code); + const nodes = new Parser(tokens).parse(); + const env = new Environment({ + print: (msg) => stdout.push(String(msg)), + input: (promptText) => window.prompt(promptText) ?? "" + }); + env.run(nodes); + } catch (error) { + stderr.push(error.message || error.toString()); + } + + return { stdout, stderr }; + } +} + +export class TabloidPlayground { + constructor() { + this.runner = new TabloidRunner(); + this.programLookup = new Map(SAMPLE_PROGRAMS.map((program) => [program.id, program])); + this.activeProgramId = this.defaultProgramId = SAMPLE_PROGRAMS[0].id; + + this.stdoutElement = document.getElementById("stdout-content"); + this.errorElement = document.getElementById("error-content"); + this.programSelect = document.getElementById("program-select"); + this.runButton = document.getElementById("run-button"); + this.clearButton = document.getElementById("clear-button"); + + this.editor = adelieEditor.init("#code-editor", { + language: "tabloid" + }); + + this.init(); + } + + init() { + this.populateProgramSelect(); + this.registerEvents(); + this.loadProgram(this.activeProgramId); + } + + populateProgramSelect() { + if (!this.programSelect) { + return; + } + + this.programSelect.innerHTML = ""; + SAMPLE_PROGRAMS.forEach((program) => { + const option = document.createElement("option"); + option.value = program.id; + option.textContent = program.label; + this.programSelect.appendChild(option); + }); + + this.programSelect.value = this.activeProgramId; + } + + registerEvents() { + this.runButton.addEventListener("click", () => this.runCode()); + this.clearButton.addEventListener("click", () => this.resetOutput()); + + this.programSelect.addEventListener("change", () => { + this.loadProgram(this.programSelect.value); + }); + + document.addEventListener("keydown", (event) => { + if (event.ctrlKey && event.key === "Enter") { + event.preventDefault(); + this.runCode(); + } + }); + } + + setEditorContent(content) { + const length = this.editor.state.doc.toString().length; + this.editor.dispatch({ + changes: { from: 0, to: length, insert: content } + }); + } + + loadProgram(programId = this.defaultProgramId) { + const selectedProgram = this.programLookup.get(programId); + if (!selectedProgram) { + return; + } + + this.activeProgramId = selectedProgram.id; + this.setEditorContent(selectedProgram.code); + this.resetOutput(); + + if (this.programSelect) { + this.programSelect.value = this.activeProgramId; + } + } + + resetOutput() { + this.stdoutElement.textContent = ""; + this.errorElement.textContent = ""; + } + + runCode() { + const code = this.editor.state.doc.toString(); + const result = this.runner.run(code); + this.stdoutElement.textContent = result.stdout.join("\n"); + this.errorElement.textContent = result.stderr.join("\n"); + } +} diff --git a/src/toys/tabloid/js/samples.js b/src/toys/tabloid/js/samples.js new file mode 100644 index 0000000..dc7bd58 --- /dev/null +++ b/src/toys/tabloid/js/samples.js @@ -0,0 +1,308 @@ +const CONS_SNIPPET = [ + "DISCOVER HOW TO cons WITH a, b", + "RUMOR HAS IT", + " DISCOVER HOW TO retrieve WITH is_first", + " RUMOR HAS IT", + " WHAT IF is_first IS ACTUALLY TOTALLY RIGHT", + " SHOCKING DEVELOPMENT a", + " LIES!", + " SHOCKING DEVELOPMENT b", + " END OF STORY", + " SHOCKING DEVELOPMENT retrieve", + "END OF STORY", +].join('\n'); + +const BINARY_INORDER_TRAVERSAL_PROGRAM = [ + CONS_SNIPPET, + '', + "DISCOVER HOW TO in_order_traverse WITH node, is_dual_ptr", + "RUMOR HAS IT", + " EXPERTS CLAIM left TO BE node OF TOTALLY RIGHT", + " EXPERTS CLAIM right TO BE node OF COMPLETELY WRONG", + '', + " WHAT IF is_dual_ptr IS ACTUALLY COMPLETELY WRONG", + " RUMOR HAS IT", + " YOU WON'T WANT TO MISS left", + " WHAT IF right IS ACTUALLY COMPLETELY WRONG", + " 1", + " LIES!", + " in_order_traverse OF right, TOTALLY RIGHT", + " END OF STORY", + " LIES!", + " RUMOR HAS IT", + " WHAT IF left IS ACTUALLY COMPLETELY WRONG", + " 1", + " LIES!", + " in_order_traverse OF left, COMPLETELY WRONG", + '', + " WHAT IF right IS ACTUALLY COMPLETELY WRONG", + " 1", + " LIES!", + " in_order_traverse OF right, COMPLETELY WRONG", + " END OF STORY", + "END OF STORY", + '', + "EXPERTS CLAIM l TO BE cons OF 1, COMPLETELY WRONG", + "EXPERTS CLAIM r TO BE cons OF 3, COMPLETELY WRONG", + "EXPERTS CLAIM root TO BE cons OF l, r", + "EXPERTS CLAIM head TO BE cons OF 2, root", + '', + "in_order_traverse OF head, COMPLETELY WRONG", + '', + "PLEASE LIKE AND SUBSCRIBE", +].join('\n'); + +const MERGE_SORT_PROGRAM = [ + CONS_SNIPPET, + '', + "DISCOVER HOW TO print WITH x", + "RUMOR HAS IT", + " YOU WON'T WANT TO MISS x", + "END OF STORY", + '', + "DISCOVER HOW TO map WITH fn, list", + "RUMOR HAS IT", + " WHAT IF list IS ACTUALLY COMPLETELY WRONG", + " SHOCKING DEVELOPMENT COMPLETELY WRONG", + " LIES!", + " RUMOR HAS IT", + " EXPERTS CLAIM car TO BE list OF TOTALLY RIGHT", + " EXPERTS CLAIM cdr TO BE list OF COMPLETELY WRONG", + " EXPERTS CLAIM new_car TO BE fn OF car", + " EXPERTS CLAIM rest_mapped TO BE map OF fn, cdr", + '', + " SHOCKING DEVELOPMENT cons OF new_car, rest_mapped", + " END OF STORY", + "END OF STORY", + '', + "DISCOVER HOW TO reduce WITH fn, list, accumulator", + "RUMOR HAS IT", + " WHAT IF list IS ACTUALLY COMPLETELY WRONG", + " SHOCKING DEVELOPMENT accumulator", + " LIES!", + " RUMOR HAS IT", + " EXPERTS CLAIM car TO BE list OF TOTALLY RIGHT", + " EXPERTS CLAIM cdr TO BE list OF COMPLETELY WRONG", + " EXPERTS CLAIM added_accumulator TO BE fn OF car, accumulator", + '', + " SHOCKING DEVELOPMENT reduce OF fn, cdr, added_accumulator", + " END OF STORY", + "END OF STORY", + '', + "DISCOVER HOW TO str_join_reducer WITH element, accumulator", + "RUMOR HAS IT", + " EXPERTS CLAIM added_comma TO BE element PLUS ', '", + " SHOCKING DEVELOPMENT added_comma PLUS accumulator", + "END OF STORY", + '', + "DISCOVER HOW TO join WITH list", + "RUMOR HAS IT", + " SHOCKING DEVELOPMENT reduce OF str_join_reducer, list, ''", + "END OF STORY", + '', + "DISCOVER HOW TO append WITH n, m", + "RUMOR HAS IT", + " WHAT IF n IS ACTUALLY COMPLETELY WRONG", + " SHOCKING DEVELOPMENT m", + " LIES!", + " RUMOR HAS IT", + " EXPERTS CLAIM car_n TO BE n OF TOTALLY RIGHT", + " EXPERTS CLAIM cdr_n TO BE n OF COMPLETELY WRONG", + " EXPERTS CLAIM appended TO BE append OF cdr_n, m", + '', + " SHOCKING DEVELOPMENT cons OF car_n, appended", + " END OF STORY", + "END OF STORY", + '', + "DISCOVER HOW TO reverse WITH l", + "RUMOR HAS IT", + " WHAT IF l IS ACTUALLY COMPLETELY WRONG", + " SHOCKING DEVELOPMENT COMPLETELY WRONG", + " LIES!", + " 1", + '', + " EXPERTS CLAIM car TO BE l OF TOTALLY RIGHT", + " EXPERTS CLAIM cdr TO BE l OF COMPLETELY WRONG", + " EXPERTS CLAIM reversed_cdr TO BE reverse OF cdr", + " EXPERTS CLAIM car_cons TO BE cons OF car, COMPLETELY WRONG", + '', + " SHOCKING DEVELOPMENT append OF reversed_cdr, car_cons", + "END OF STORY", + '', + "DISCOVER HOW TO merge WITH x, y", + "RUMOR HAS IT", + " WHAT IF x IS ACTUALLY COMPLETELY WRONG", + " SHOCKING DEVELOPMENT y", + " LIES!", + " 1", + '', + " WHAT IF y IS ACTUALLY COMPLETELY WRONG", + " RUMOR HAS IT", + " WHAT IF x IS ACTUALLY COMPLETELY WRONG", + " SHOCKING DEVELOPMENT COMPLETELY WRONG", + " LIES!", + " SHOCKING DEVELOPMENT x", + " END OF STORY", + " LIES!", + " 1", + '', + " EXPERTS CLAIM car_x TO BE x OF TOTALLY RIGHT", + " EXPERTS CLAIM car_y TO BE y OF TOTALLY RIGHT", + " EXPERTS CLAIM cdr_x TO BE x OF COMPLETELY WRONG", + " EXPERTS CLAIM cdr_y TO BE y OF COMPLETELY WRONG", + '', + " EXPERTS CLAIM x_gt_y TO BE car_x BEATS car_y", + '', + " WHAT IF x_gt_y IS ACTUALLY TOTALLY RIGHT", + " RUMOR HAS IT", + " EXPERTS CLAIM rest_x_merge_y TO BE merge OF cdr_x, y", + " SHOCKING DEVELOPMENT cons OF car_x, rest_x_merge_y", + " END OF STORY", + " LIES!", + " RUMOR HAS IT", + " EXPERTS CLAIM x_merge_rest_y TO BE merge OF x, cdr_y", + " SHOCKING DEVELOPMENT cons OF car_y, x_merge_rest_y", + " END OF STORY", + "END OF STORY", + '', + "DISCOVER HOW TO split_middle_helper WITH slow, fast, mid_to_head", + "RUMOR HAS IT", + " WHAT IF fast IS ACTUALLY COMPLETELY WRONG", + " RUMOR HAS IT", + " EXPERTS CLAIM head_to_mid TO BE reverse OF mid_to_head", + " SHOCKING DEVELOPMENT cons OF head_to_mid, slow", + " END OF STORY", + " LIES!", + " 1", + '', + " EXPERTS CLAIM fast_cdr TO BE fast OF COMPLETELY WRONG", + " EXPERTS CLAIM slow_car TO BE slow OF TOTALLY RIGHT", + " EXPERTS CLAIM slow_cdr TO BE slow OF COMPLETELY WRONG", + '', + " WHAT IF fast_cdr IS ACTUALLY COMPLETELY WRONG", + " RUMOR HAS IT", + " EXPERTS CLAIM mid_to_head_plus_slow TO BE cons OF slow_car, mid_to_head", + " EXPERTS CLAIM head_to_mid_plus_slow TO BE reverse OF mid_to_head_plus_slow", + '', + " SHOCKING DEVELOPMENT cons OF head_to_mid_plus_slow, slow_cdr", + " END OF STORY", + " LIES!", + " 1", + '', + " EXPERTS CLAIM fast_cddr TO BE fast_cdr OF COMPLETELY WRONG", + " EXPERTS CLAIM slow_car_mid_to_head TO BE cons OF slow_car, mid_to_head", + '', + " SHOCKING DEVELOPMENT split_middle_helper OF slow_cdr, fast_cddr, slow_car_mid_to_head", + "END OF STORY", + '', + "DISCOVER HOW TO split_middle WITH start", + "RUMOR HAS IT", + " EXPERTS CLAIM cdr TO BE start OF COMPLETELY WRONG", + '', + " SHOCKING DEVELOPMENT split_middle_helper OF start, cdr, COMPLETELY WRONG", + "END OF STORY", + '', + "DISCOVER HOW TO sort WITH root", + "RUMOR HAS IT", + " WHAT IF root IS ACTUALLY COMPLETELY WRONG", + " SHOCKING DEVELOPMENT root", + " LIES!", + " 1", + '', + " EXPERTS CLAIM root_cdr TO BE root OF COMPLETELY WRONG", + " WHAT IF root_cdr IS ACTUALLY COMPLETELY WRONG", + " SHOCKING DEVELOPMENT root", + " LIES!", + " 1", + '', + " EXPERTS CLAIM left_right_cons_cell TO BE split_middle OF root", + " EXPERTS CLAIM left TO BE left_right_cons_cell OF TOTALLY RIGHT", + " EXPERTS CLAIM right TO BE left_right_cons_cell OF COMPLETELY WRONG", + " EXPERTS CLAIM left_s TO BE sort OF left", + " EXPERTS CLAIM right_s TO BE sort OF right", + '', + " SHOCKING DEVELOPMENT merge OF left_s, right_s", + "END OF STORY", + '', + "EXPERTS CLAIM a_3 TO BE cons OF 3, COMPLETELY WRONG", + "EXPERTS CLAIM a_2 TO BE cons OF 1, a_3", + "EXPERTS CLAIM a_1 TO BE cons OF -2, a_2", + "EXPERTS CLAIM a_0 TO BE cons OF 5, a_1", + "EXPERTS CLAIM b_3 TO BE cons OF 2, a_0", + "EXPERTS CLAIM b_2 TO BE cons OF 7, b_3", + "EXPERTS CLAIM b_1 TO BE cons OF 3, b_2", + "EXPERTS CLAIM b_0 TO BE cons OF -1, b_1", + '', + "EXPERTS CLAIM b_sorted TO BE sort OF b_0", + '', + "YOU WON'T WANT TO MISS join OF b_sorted", + '', + "PLEASE LIKE AND SUBSCRIBE", +].join('\n'); + +export const SAMPLE_PROGRAMS = [ + { + id: 'fibonacci', + label: 'Fibonacci', + code: [ + "DISCOVER HOW TO fibonacci WITH a, b, n", + "RUMOR HAS IT", + " WHAT IF n SMALLER THAN 1", + " SHOCKING DEVELOPMENT b", + " LIES! RUMOR HAS IT", + " YOU WON'T WANT TO MISS b", + " SHOCKING DEVELOPMENT", + " fibonacci OF b, a PLUS b, n MINUS 1", + " END OF STORY", + "END OF STORY", + "", + "EXPERTS CLAIM limit TO BE 10", + "", + "fibonacci OF 0, 1, limit", + "", + "PLEASE LIKE AND SUBSCRIBE", + ].join('\n'), + }, + { + id: 'countdown', + label: 'Countdown', + code: [ + "EXPERTS CLAIM start TO BE 4", + "YOU WON'T WANT TO MISS 't minus 5...'", + "", + "DISCOVER HOW TO countdown WITH current", + "RUMOR HAS IT", + " WHAT IF current SMALLER THAN 1 RUMOR HAS IT", + " SHOCKING DEVELOPMENT 'Blastoff!'", + " END OF STORY", + " LIES! RUMOR HAS IT", + " YOU WON'T WANT TO MISS current", + " SHOCKING DEVELOPMENT countdown OF current MINUS 1", + " END OF STORY", + "END OF STORY", + "", + "YOU WON'T WANT TO MISS countdown OF start", + "", + "PLEASE LIKE AND SUBSCRIBE", + ].join('\n'), + }, + { + id: 'hello', + label: 'Hello!', + code: [ + "YOU WON'T WANT TO MISS ('Hello, ' PLUS (LATEST NEWS ON 'What is your name?')) PLUS '!'", + "", + "PLEASE LIKE AND SUBSCRIBE", + ].join('\n'), + }, + { + id: 'binary-inorder-traversal', + label: 'Binary tree in-order traversal', + code: BINARY_INORDER_TRAVERSAL_PROGRAM, + }, + { + id: 'merge-sort', + label: 'Merge sort linked list', + code: MERGE_SORT_PROGRAM, + }, +]; diff --git a/src/toys/tabloid/js/tabloid.js b/src/toys/tabloid/js/tabloid.js new file mode 100644 index 0000000..468ee9a --- /dev/null +++ b/src/toys/tabloid/js/tabloid.js @@ -0,0 +1,707 @@ +/* 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)}`); + } + } +} -- cgit v1.2.3-70-g09d2