summaryrefslogtreecommitdiff
path: root/src/toys/tabloid
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2026-01-07 19:29:30 -0800
committerElizabeth Hunt <me@liz.coffee>2026-01-07 19:29:30 -0800
commit91b7598b22f89319f64054daf42c950de3eb6451 (patch)
treeb337ad01c75e7ee88f287eda05522e72dd9a8dd5 /src/toys/tabloid
parent49012297ea792a69501b74d8d83bd4be44d177da (diff)
downloadlizdotcoffee-91b7598b22f89319f64054daf42c950de3eb6451.tar.gz
lizdotcoffee-91b7598b22f89319f64054daf42c950de3eb6451.zip
Adding some of my favorite toys
Diffstat (limited to 'src/toys/tabloid')
-rw-r--r--src/toys/tabloid/index.html53
-rw-r--r--src/toys/tabloid/js/main.js5
-rw-r--r--src/toys/tabloid/js/playground.js120
-rw-r--r--src/toys/tabloid/js/samples.js308
-rw-r--r--src/toys/tabloid/js/tabloid.js707
5 files changed, 1193 insertions, 0 deletions
diff --git a/src/toys/tabloid/index.html b/src/toys/tabloid/index.html
new file mode 100644
index 0000000..61ec937
--- /dev/null
+++ b/src/toys/tabloid/index.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link rel="icon" href="https://adelie.liz.coffee/img/favicon.ico" />
+ <link rel="stylesheet" href="https://adelie.liz.coffee/bundle.css">
+ <title>Tabloid</title>
+</head>
+<body>
+ <main>
+ <article>
+ <h3>Tabloid</h3>
+ <p>Forgotten how to write Tabloid since your schooldays? Here's <a target="_blank" href="https://github.com/thesephist/tabloid/blob/master/README.md#language-overview">a refresher</a>.</p>
+
+ <div>
+ <label for="program-select">Sample:</label>
+ <select id="program-select" aria-label="Sample Tabloid programs"></select>
+ </div>
+
+ <section class="mt-md">
+ <div
+ id="code-editor"
+ class="code-editor-container"
+ aria-label="Tabloid source code"
+ spellcheck="false"
+ autocomplete="off"
+ autocorrect="off"
+ ></div>
+ </section>
+
+ <div class="mt-sm">
+ <button id="run-button" class="primary">Run (Ctrl + Enter)</button>
+ <button id="clear-button">Clear Output</button>
+ </div>
+
+ <section class="mt-md">
+ <h4>stdout</h4>
+ <pre id="stdout-content" aria-live="polite"></pre>
+ </section>
+
+ <section class="mt-md">
+ <h4>stderr</h4>
+ <pre class="text-error" id="error-content" aria-live="polite"></pre>
+ </section>
+ </article>
+ </main>
+
+ <script src="https://adelie.liz.coffee/bundle.js"></script>
+ <script src="https://adelie.liz.coffee/adelie-editor.js"></script>
+ <script type="module" src="./js/main.js"></script>
+</body>
+</html>
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)}`);
+ }
+ }
+}