summaryrefslogtreecommitdiff
path: root/html/toys
diff options
context:
space:
mode:
Diffstat (limited to 'html/toys')
-rw-r--r--html/toys/euler-golf/css/styles.css81
-rw-r--r--html/toys/euler-golf/index.html80
-rw-r--r--html/toys/euler-golf/js/controls.js33
-rw-r--r--html/toys/euler-golf/js/cx.js308
-rw-r--r--html/toys/euler-golf/js/game.js275
-rw-r--r--html/toys/euler-golf/js/json-ds.js19
-rw-r--r--html/toys/euler-golf/js/modal-vanilla.min.js1
-rw-r--r--html/toys/euler-golf/js/sol.js44
-rw-r--r--html/toys/fourier/index.html46
-rw-r--r--html/toys/fourier/js/script.js294
-rw-r--r--html/toys/godel/index.html51
-rw-r--r--html/toys/godel/js/compiler.js226
-rw-r--r--html/toys/godel/js/godel-worker.js42
-rw-r--r--html/toys/godel/js/main.js5
-rw-r--r--html/toys/godel/js/parser.js1059
-rw-r--r--html/toys/godel/js/ui.js224
-rw-r--r--html/toys/index.html0
-rw-r--r--html/toys/julia/index.html51
-rw-r--r--html/toys/julia/julia.js143
-rw-r--r--html/toys/tabloid/index.html53
-rw-r--r--html/toys/tabloid/js/main.js5
-rw-r--r--html/toys/tabloid/js/playground.js120
-rw-r--r--html/toys/tabloid/js/samples.js308
-rw-r--r--html/toys/tabloid/js/tabloid.js707
-rw-r--r--html/toys/turing/css/styles.css66
-rw-r--r--html/toys/turing/index.html42
-rw-r--r--html/toys/turing/js/machine.js75
-rw-r--r--html/toys/turing/js/main.js4
-rw-r--r--html/toys/turing/js/parser.js141
-rw-r--r--html/toys/turing/js/samples.js88
-rw-r--r--html/toys/turing/js/tape.js103
-rw-r--r--html/toys/turing/js/ui.js386
32 files changed, 5080 insertions, 0 deletions
diff --git a/html/toys/euler-golf/css/styles.css b/html/toys/euler-golf/css/styles.css
new file mode 100644
index 0000000..219012d
--- /dev/null
+++ b/html/toys/euler-golf/css/styles.css
@@ -0,0 +1,81 @@
+body {
+ margin: 0;
+ padding: 0;
+ overflow: scroll;
+ font-family: Lucida Console, Lucida Sans Typewriter, monaco,
+ Bitstream Vera Sans Mono, monospace;
+ width: 100vw;
+ height: 100vh;
+ background: rgb(238, 174, 202);
+ background: radial-gradient(
+ circle,
+ rgba(238, 174, 202, 1) 0%,
+ rgba(148, 187, 233, 1) 100%
+ );
+}
+
+.canvas {
+ padding: 0;
+ margin: auto;
+ display: block;
+ border: 1px solid black;
+
+ width: 100vw;
+ height: 100vw;
+}
+
+button {
+ border-radius: 5px;
+ padding: 5px;
+ cursor: pointer;
+ margin-left: 5px;
+}
+
+.controls {
+ cursor: pointer;
+ padding: 12px;
+ position: fixed;
+ bottom: 0;
+ right: 0;
+ background-color: rgba(255, 255, 255, 0.8);
+ border: 1px solid white;
+ border-radius: 8px;
+ margin-right: 6px;
+ margin-bottom: 6px;
+}
+
+.buttons {
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+}
+
+.modal {
+ display: flex;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+
+ width: 80vw;
+ max-width: 500px;
+ min-height: 200px;
+
+ padding: 12px;
+
+ background-color: rgba(255, 255, 255, 0.8);
+ border: 1px solid black;
+ border-radius: 15px;
+}
+
+.modal-body {
+ display: flex;
+ justify-content: center;
+}
+
+.slider {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: 3px;
+}
diff --git a/html/toys/euler-golf/index.html b/html/toys/euler-golf/index.html
new file mode 100644
index 0000000..8164823
--- /dev/null
+++ b/html/toys/euler-golf/index.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Euler Golf 2</title>
+ <link rel="stylesheet" type="text/css" href="css/styles.css" />
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <canvas id="canvas"></canvas>
+
+ <div class="controls" id="controls-container">
+ <div id="controls" style="display: none">
+ <div class="buttons">
+ <button id="reset">Reset</button>
+ <button id="solve">Solve</button>
+ <button id="directions">Directions</button>
+ </div>
+ <div class="slider">
+ <label>Gap</label>
+ <input type="range" min="15" max="80" id="gap" />
+ </div>
+ </div>
+ <span id="expand-show">↑↑</span>
+ </div>
+
+ <div
+ id="directions-modal"
+ class="modal"
+ style="display: none"
+ tabindex="-1"
+ role="dialog"
+ >
+ <button
+ type="button"
+ class="close"
+ data-dismiss="modal"
+ aria-label="Close"
+ >
+ <span aria-hidden="true">X</span>
+ </button>
+
+ <div class="modal-body">
+ <div style="margin: 0; display: inline-block">
+ <h1 style="text-align: center">Euler Golf 2</h1>
+ <p>
+ Use the left and right arrow keys as navigation & hover over the
+ bottom right corner for controls.
+ </p>
+ <p>Rules</p>
+ <ul>
+ <li>
+ Every move consists of a 90 degree rotation around your last
+ position.
+ </li>
+ <li>You begin at the point one unit right from the center.</li>
+ <li>
+ The inital point that you rotate around is the origin (blue).
+ </li>
+ <li>You must navigate to the target point (white).</li>
+ </ul>
+ <p>
+ Initial game by
+ <a href="https://kylehovey.github.io/EulerGolf/">speleo</a>,
+ reimplemented & solved by
+ <a href="https://github.com/Simponic">simponic</a>.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <script src="js/modal-vanilla.min.js"></script>
+
+ <script src="js/cx.js"></script>
+ <script src="js/json-ds.js"></script>
+ <script src="js/sol.js"></script>
+
+ <script src="js/game.js"></script>
+ <script src="js/controls.js"></script>
+ </body>
+</html>
diff --git a/html/toys/euler-golf/js/controls.js b/html/toys/euler-golf/js/controls.js
new file mode 100644
index 0000000..7f606cc
--- /dev/null
+++ b/html/toys/euler-golf/js/controls.js
@@ -0,0 +1,33 @@
+document
+ .getElementById("controls-container")
+ .addEventListener("mouseover", () => {
+ document.getElementById("controls").style.display = "block";
+ document.getElementById("expand-show").style.display = "none";
+ });
+
+document
+ .getElementById("controls-container")
+ .addEventListener("mouseout", () => {
+ document.getElementById("controls").style.display = "none";
+ document.getElementById("expand-show").style.display = "inline";
+ });
+
+document.getElementById("reset").addEventListener("click", () => {
+ state = reset_state(state);
+
+ state.target = rand_target(state.rows, state.cols);
+});
+
+document.getElementById("solve").addEventListener("click", () => {
+ if (!cx.eq(state.path.at(-2), new cx(0, 0))) state = reset_state(state);
+
+ state.solution = sol(state.target);
+});
+
+document
+ .getElementById("directions")
+ .addEventListener("click", () => directions_modal.show());
+
+document.getElementById("gap").addEventListener("input", function () {
+ state.changes.gap = Number(this.value);
+});
diff --git a/html/toys/euler-golf/js/cx.js b/html/toys/euler-golf/js/cx.js
new file mode 100644
index 0000000..371415d
--- /dev/null
+++ b/html/toys/euler-golf/js/cx.js
@@ -0,0 +1,308 @@
+// http://www.russellcottrell.com/fractalsEtc/cx.js
+
+class cx {
+ static degrees(d) {
+ cx._RD = d ? Math.PI / 180 : 1;
+ }
+ // Math.PI/180 for degrees, 1 for radians
+ // applies to i/o (constructor, get/set arg, and toString etc.)
+
+ constructor(x, y, polar) {
+ if (!polar) {
+ this.re = x;
+ this.im = y;
+ } else {
+ y *= cx._RD; // may be radians or degrees
+ this.re = x * Math.cos(y);
+ this.im = x * Math.sin(y);
+ }
+ }
+
+ get abs() {
+ return Math.sqrt(this.re * this.re + this.im * this.im);
+ }
+
+ set abs(r) {
+ var theta = this._arg;
+ this.re = r * Math.cos(theta);
+ this.im = r * Math.sin(theta);
+ }
+
+ get arg() {
+ // returns radians or degrees, non-negative
+ return (
+ ((Math.atan2(this.im, this.re) + 2 * Math.PI) % (2 * Math.PI)) / cx._RD
+ );
+ }
+
+ set arg(theta) {
+ // may be radians or degrees
+ var r = this.abs;
+ this.re = r * Math.cos(theta * cx._RD);
+ this.im = r * Math.sin(theta * cx._RD);
+ }
+
+ get _arg() {
+ // internal; returns radians
+ return Math.atan2(this.im, this.re);
+ }
+
+ static get i() {
+ return new cx(0, 1);
+ }
+
+ static set i(x) {
+ throw new Error("i is read-only");
+ }
+
+ toString(polar) {
+ if (!polar)
+ return (
+ this.re.toString() +
+ (this.im >= 0 ? " + " : " - ") +
+ Math.abs(this.im).toString() +
+ "i"
+ );
+ else return this.abs.toString() + " cis " + this.arg.toString();
+ }
+
+ toPrecision(n, polar) {
+ if (!polar)
+ return (
+ this.re.toPrecision(n) +
+ (this.im >= 0 ? " + " : " - ") +
+ Math.abs(this.im).toPrecision(n) +
+ "i"
+ );
+ else return this.abs.toPrecision(n) + " cis " + this.arg.toPrecision(n);
+ }
+
+ toPrecis(n, polar) {
+ // trims trailing zeros
+ if (!polar)
+ return (
+ parseFloat(this.re.toPrecision(n)).toString() +
+ (this.im >= 0 ? " + " : " - ") +
+ parseFloat(Math.abs(this.im).toPrecision(n)).toString() +
+ "i"
+ );
+ else
+ return (
+ parseFloat(this.abs.toPrecision(n)).toString() +
+ " cis " +
+ parseFloat(this.arg.toPrecision(n)).toString()
+ );
+ }
+
+ toFixed(n, polar) {
+ if (!polar)
+ return (
+ this.re.toFixed(n) +
+ (this.im >= 0 ? " + " : " - ") +
+ Math.abs(this.im).toFixed(n) +
+ "i"
+ );
+ else return this.abs.toFixed(n) + " cis " + this.arg.toFixed(n);
+ }
+
+ toExponential(n, polar) {
+ if (!polar)
+ return (
+ this.re.toExponential(n) +
+ (this.im >= 0 ? " + " : " - ") +
+ Math.abs(this.im).toExponential(n) +
+ "i"
+ );
+ else return this.abs.toExponential(n) + " cis " + this.arg.toExponential(n);
+ }
+
+ static getReals(c, d) {
+ // when c or d may be simple or complex
+ var x, y, u, v;
+ if (c instanceof cx) {
+ x = c.re;
+ y = c.im;
+ } else {
+ x = c;
+ y = 0;
+ }
+ if (d instanceof cx) {
+ u = d.re;
+ v = d.im;
+ } else {
+ u = d;
+ v = 0;
+ }
+ return [x, y, u, v];
+ }
+
+ static conj(c) {
+ return new cx(c.re, -c.im);
+ }
+
+ static neg(c) {
+ return new cx(-c.re, -c.im);
+ }
+
+ static add(c, d) {
+ var a = cx.getReals(c, d);
+ var x = a[0];
+ var y = a[1];
+ var u = a[2];
+ var v = a[3];
+ return new cx(x + u, y + v);
+ }
+
+ static sub(c, d) {
+ var a = cx.getReals(c, d);
+ var x = a[0];
+ var y = a[1];
+ var u = a[2];
+ var v = a[3];
+ return new cx(x - u, y - v);
+ }
+
+ static mult(c, d) {
+ var a = cx.getReals(c, d);
+ var x = a[0];
+ var y = a[1];
+ var u = a[2];
+ var v = a[3];
+ return new cx(x * u - y * v, x * v + y * u);
+ }
+
+ static div(c, d) {
+ var a = cx.getReals(c, d);
+ var x = a[0];
+ var y = a[1];
+ var u = a[2];
+ var v = a[3];
+ return new cx(
+ (x * u + y * v) / (u * u + v * v),
+ (y * u - x * v) / (u * u + v * v)
+ );
+ }
+
+ static pow(c, int) {
+ if (Number.isInteger(int) && int >= 0) {
+ var r = Math.pow(c.abs, int);
+ var theta = int * c._arg;
+ return new cx(r * Math.cos(theta), r * Math.sin(theta));
+ } else return NaN;
+ }
+
+ static root(c, int, k) {
+ if (!k) k = 0;
+ if (
+ Number.isInteger(int) &&
+ int >= 2 &&
+ Number.isInteger(k) &&
+ k >= 0 &&
+ k < int
+ ) {
+ var r = Math.pow(c.abs, 1 / int);
+ var theta = (c._arg + 2 * k * Math.PI) / int;
+ return new cx(r * Math.cos(theta), r * Math.sin(theta));
+ } else return NaN;
+ }
+
+ static log(c) {
+ return new cx(Math.log(c.abs), c._arg);
+ }
+
+ static exp(c) {
+ var r = Math.exp(c.re);
+ var theta = c.im;
+ return new cx(r * Math.cos(theta), r * Math.sin(theta));
+ }
+
+ static sin(c) {
+ var a = c.re;
+ var b = c.im;
+ return new cx(Math.sin(a) * Math.cosh(b), Math.cos(a) * Math.sinh(b));
+ }
+
+ static cos(c) {
+ var a = c.re;
+ var b = c.im;
+ return new cx(Math.cos(a) * Math.cosh(b), -Math.sin(a) * Math.sinh(b));
+ }
+
+ static tan(c) {
+ return cx.div(cx.sin(c), cx.cos(c));
+ }
+
+ static asin(c, k) {
+ if (!k) k = 0;
+ var ic = cx.mult(cx.i, c);
+ var c2 = cx.pow(c, 2);
+ return cx.mult(
+ cx.neg(cx.i),
+ cx.log(cx.add(ic, cx.root(cx.sub(1, c2), 2, k)))
+ );
+ }
+
+ static acos(c, k) {
+ if (!k) k = 0;
+ var c2 = cx.pow(c, 2);
+ return cx.mult(
+ cx.neg(cx.i),
+ cx.log(cx.add(c, cx.mult(cx.i, cx.root(cx.sub(1, c2), 2, k))))
+ );
+ }
+
+ static atan(c) {
+ return cx.mult(
+ cx.div(cx.i, 2),
+ cx.log(cx.div(cx.add(cx.i, c), cx.sub(cx.i, c)))
+ );
+ }
+
+ static sinh(c) {
+ var a = c.re;
+ var b = c.im;
+ return new cx(Math.sinh(a) * Math.cos(b), Math.cosh(a) * Math.sin(b));
+ }
+
+ static cosh(c) {
+ var a = c.re;
+ var b = c.im;
+ return new cx(Math.cosh(a) * Math.cos(b), Math.sinh(a) * Math.sin(b));
+ }
+
+ static tanh(c) {
+ return cx.div(cx.sinh(c), cx.cosh(c));
+ }
+
+ static asinh(c, k) {
+ if (!k) k = 0;
+ var c2 = cx.pow(c, 2);
+ return cx.log(cx.add(c, cx.root(cx.add(c2, 1), 2, k)));
+ }
+
+ static acosh(c, k) {
+ if (!k) k = 0;
+ var c2 = cx.pow(c, 2);
+ return cx.log(cx.add(c, cx.root(cx.sub(c2, 1), 2, k)));
+ }
+
+ static atanh(c) {
+ return cx.mult(cx.div(1, 2), cx.log(cx.div(cx.add(1, c), cx.sub(1, c))));
+ }
+
+ static copy(c) {
+ return new cx(c.re, c.im);
+ }
+
+ static eq(c, d, epsilon) {
+ if (!epsilon) {
+ if (c.re == d.re && c.im == d.im) return true;
+ } else {
+ if (Math.abs(c.re - d.re) < epsilon && Math.abs(c.im - d.im) < epsilon)
+ return true;
+ }
+ return false;
+ }
+}
+
+cx.degrees(true); // need to call this
diff --git a/html/toys/euler-golf/js/game.js b/html/toys/euler-golf/js/game.js
new file mode 100644
index 0000000..5e123a0
--- /dev/null
+++ b/html/toys/euler-golf/js/game.js
@@ -0,0 +1,275 @@
+const DEFAULTS = {
+ max_rows: 80,
+ max_cols: 80,
+ min_gap: 30,
+ angle_multiplier: 10e-4,
+};
+
+const CANVAS = document.getElementById("canvas");
+
+let state = {
+ grid_padding: 30,
+ gap: DEFAULTS.min_gap,
+ canvas: CANVAS,
+ ctx: CANVAS.getContext("2d"),
+ last_render: 0,
+ keys: {},
+ changes: {},
+};
+
+// Rendering
+CanvasRenderingContext2D.prototype.circle = function (x, y, r, color) {
+ this.beginPath();
+ this.arc(x, y, r, 0, Math.PI * 2);
+ this.fillStyle = color;
+ this.fill();
+ this.closePath();
+};
+
+CanvasRenderingContext2D.prototype.line = function (
+ { x_pos: x1, y_pos: y1 },
+ { x_pos: x2, y_pos: y2 },
+ width,
+ color,
+ cap = "round"
+) {
+ this.lineWidth = width;
+ this.strokeStyle = color;
+ this.lineCap = cap;
+
+ this.beginPath();
+ this.moveTo(x1, y1);
+ this.lineTo(x2, y2);
+ this.stroke();
+ this.closePath();
+};
+
+CanvasRenderingContext2D.prototype.draw_cartesian_path = function (
+ grid_spec,
+ cartesian_path,
+ width = 2,
+ color = "#fff"
+) {
+ const path = cartesian_path.map((coord) => grid_to_canvas(coord, grid_spec));
+ path.slice(1).forEach((coord, i) => {
+ this.line(path[i], coord, width, color);
+ });
+};
+
+CanvasRenderingContext2D.prototype.do_grid = function (
+ rows,
+ cols,
+ draw_at_grid_pos = (ctx, x, y) => ctx.circle(x, y, 10, "#44ff44")
+) {
+ for (let y = 0; y < rows; y++) {
+ for (let x = 0; x < cols; x++) {
+ draw_at_grid_pos(this, x, y);
+ }
+ }
+};
+
+CanvasRenderingContext2D.prototype.cartesian_grid = function (
+ rows,
+ cols,
+ grid_spec,
+ circle_spec_at_coords = (_x, _y) => ({ radius: 5, color: "#000" })
+) {
+ this.do_grid(rows, cols, (ctx, x, y) => {
+ const { x_pos, y_pos } = grid_to_canvas({ x, y }, grid_spec);
+ const { radius, color } = circle_spec_at_coords(x, y);
+
+ ctx.circle(x_pos, y_pos, radius, color);
+ });
+};
+
+// Utilities
+const move = (prev, curr, c) => cx.add(prev, cx.mult(c, cx.sub(curr, prev)));
+
+const rand_between = (min, max) =>
+ Math.floor(Math.random() * (max - min + 1)) + min;
+
+const rand_target = (rows, cols) => {
+ const r = Math.floor((rows - 1) / 2);
+ const c = Math.floor((cols - 1) / 2);
+ const res = new cx(rand_between(-c, c), rand_between(-r, r));
+ if (!sol(res)) return rand_target(rows, cols);
+
+ return res;
+};
+
+const calculate_grid_spec = ({ rows, cols, width, height, grid_padding }) => {
+ const dx = (width - 2 * grid_padding) / cols;
+ const dy = (height - 2 * grid_padding) / rows;
+
+ return {
+ dx,
+ dy,
+ start_x: grid_padding + dx / 2,
+ start_y: grid_padding + dy / 2,
+ };
+};
+
+const grid_to_canvas = ({ x, y }, { dx, dy, start_x, start_y }) => ({
+ x_pos: x * dx + start_x,
+ y_pos: y * dy + start_y,
+});
+
+const complex_to_grid = (c, rows, cols) => {
+ const { re, im } = c;
+ return {
+ x: re + Math.floor(cols / 2),
+ y: Math.floor(rows / 2) - im,
+ };
+};
+
+// Game loop
+
+const maybe_add_state_angle_move = ({ angle } = state) => {
+ if (angle.im <= -1 || angle.im >= 1) {
+ angle.im = angle.im <= -1 ? -1 : 1;
+ state.path.push(move(state.path.at(-2), state.path.at(-1), angle));
+ state.angle = new cx(0, 0);
+ }
+ return state;
+};
+
+const handle_input = (state, dt) => {
+ if (state.keys.ArrowLeft) {
+ state.angle.im += DEFAULTS.angle_multiplier * dt;
+ } else if (state.keys.ArrowRight) {
+ state.angle.im -= DEFAULTS.angle_multiplier * dt;
+ }
+ state = maybe_add_state_angle_move(state);
+};
+
+const render = ({ width, height, ctx, rows, cols, target, gap } = state) => {
+ ctx.clearRect(0, 0, width, height);
+ ctx.fillStyle = "rgba(0, 0, 0, 0)";
+ ctx.fillRect(0, 0, width, height);
+
+ const grid_spec = calculate_grid_spec(state);
+
+ const curr = state.path.at(-1);
+ const prev = state.path.at(-2);
+
+ const v_diff = cx.sub(curr, prev);
+ const theta = (state.angle.im * Math.PI) / 2;
+
+ const angle_re = Math.cos(theta) * v_diff.re - Math.sin(theta) * v_diff.im;
+ const angle_im = Math.sin(theta) * v_diff.re + Math.cos(theta) * v_diff.im;
+
+ ctx.draw_cartesian_path(grid_spec, [
+ ...state.path.map((c) => complex_to_grid(c, rows, cols)),
+ complex_to_grid(cx.add(new cx(angle_re, angle_im), prev), rows, cols),
+ ]);
+
+ if (!(state.angle.im == state.angle.re && state.angle.re == 0)) {
+ // Draw path to next player's target
+ const [a, b] = [
+ curr,
+ move(prev, curr, new cx(0, state.angle.im < 0 ? -1 : 1)),
+ ].map((c) => grid_to_canvas(complex_to_grid(c, rows, cols), grid_spec));
+
+ ctx.line(a, b, 6, "rgba(127, 127, 127, 0.3)");
+ }
+
+ const grid_target = complex_to_grid(target, rows, cols);
+ ctx.cartesian_grid(rows, cols, grid_spec, (x, y) => {
+ if (x == Math.floor(cols / 2) && y == Math.floor(rows / 2)) {
+ return {
+ radius: 7,
+ color: "#2f9c94",
+ };
+ } else if (x == grid_target.x && y == grid_target.y) {
+ return {
+ radius: 8,
+ color: "#fff",
+ };
+ } else {
+ return {
+ radius: 3,
+ color: `rgb(${255 * (x / cols)}, 100, 100)`, // todo: animate with last_render
+ };
+ }
+ });
+
+ // Render gap value in slider
+ document.getElementById("gap").value = gap;
+};
+
+const loop = (now) => {
+ const dt = now - state.last_render;
+ state.changes.last_render = now;
+
+ if (Object.keys(state.changes).length > 0) {
+ state = { ...state, ...state.changes };
+
+ if (state.changes.width || state.changes.height || state.changes.gap) {
+ state.rows = Math.floor(state.height / state.gap);
+ state.cols = Math.floor(state.width / state.gap);
+ }
+
+ state.changes = {};
+ }
+
+ if (!state.target) state.target = rand_target(state.rows, state.cols);
+
+ if (!state.solution) {
+ handle_input(state, dt);
+ } else {
+ if (!state?.solution.length) {
+ delete state.solution;
+ } else {
+ state.angle.im +=
+ (state.solution[0] === "-" ? 1 : -1) * DEFAULTS.angle_multiplier * dt;
+
+ state = maybe_add_state_angle_move(state);
+
+ if (cx.eq(state.angle, new cx(0, 0))) state.solution.shift();
+ }
+ }
+ render(state);
+ requestAnimationFrame(loop);
+};
+
+const reset_state = ({ rows, cols } = state) => ({
+ ...state,
+ solution: null,
+ path: [new cx(0, 0), new cx(1, 0)],
+ angle: new cx(0, 0),
+});
+
+// DOM
+const directions_modal = new Modal({
+ el: document.getElementById("directions-modal"),
+});
+
+const on_resize = () => {
+ CANVAS.width = document.body.clientWidth;
+ CANVAS.height = document.body.clientHeight;
+ state.changes.width = CANVAS.width;
+ state.changes.height = CANVAS.height;
+};
+
+const on_keyup = (e) => {
+ delete state.keys[e.key];
+};
+
+const on_keydown = (e) => {
+ state.keys[e.key] = true;
+};
+
+window.addEventListener("resize", on_resize);
+window.addEventListener("keydown", on_keydown);
+window.addEventListener("keyup", on_keyup);
+
+// main
+on_resize();
+state = reset_state(state);
+
+if (!sessionStorage.getItem("seen-instructions")) {
+ directions_modal.show();
+ sessionStorage.setItem("seen-instructions", true);
+}
+
+requestAnimationFrame(loop);
diff --git a/html/toys/euler-golf/js/json-ds.js b/html/toys/euler-golf/js/json-ds.js
new file mode 100644
index 0000000..dc7e88e
--- /dev/null
+++ b/html/toys/euler-golf/js/json-ds.js
@@ -0,0 +1,19 @@
+class JSONSet {
+ items = new Set();
+
+ constructor(initial) {
+ if (Array.isArray(initial)) {
+ initial.map((x) => this.apply_set_function("add", x));
+ } else {
+ this.apply_set_function("add", initial);
+ }
+
+ ["add", "has", "remove"].forEach(
+ (f_name) => (this[f_name] = (x) => this.apply_set_function(f_name, x))
+ );
+ }
+
+ apply_set_function(f_name, x) {
+ return this.items[f_name](JSON.stringify(x));
+ }
+}
diff --git a/html/toys/euler-golf/js/modal-vanilla.min.js b/html/toys/euler-golf/js/modal-vanilla.min.js
new file mode 100644
index 0000000..0d314c7
--- /dev/null
+++ b/html/toys/euler-golf/js/modal-vanilla.min.js
@@ -0,0 +1 @@
+var Modal=function(e){function t(i){if(n[i])return n[i].exports;var o=n[i]={i:i,l:!1,exports:{}};return e[i].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,i){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:i})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=0)}([function(e,t,n){e.exports=n(1).default},function(e,t,n){"use strict";function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function r(e){for(var t in e)Array.isArray(e[t])?e[t].forEach(function(e){r(e)}):null!==e[t]&&"object"===p(e[t])&&Object.freeze(e[t]);return Object.freeze(e)}function a(){return(65536*(1+Math.random())|0).toString(16)+(65536*(1+Math.random())|0).toString(16)}function l(e,t,n){var i=e.data||{};if(void 0===n){if(e.data&&e.data[t])return e.data[t];var o=e.getAttribute("data-"+t);return void 0!==o?o:null}return i[t]=n,e.data=i,e}function d(e,t){return e.nodeName?e:(e=e.replace(/(\t|\n$)/g,""),_||(_=document.createElement("div")),_.innerHTML="",_.innerHTML=e,!0===t?_.childNodes:_.childNodes[0])}function c(){var e=void 0,t=void 0,n=void 0,i=document.createElement("div");return v(i.style,{visibility:"hidden",width:"100px"}),document.body.appendChild(i),n=i.offsetWidth,i.style.overflow="scroll",e=document.createElement("div"),e.style.width="100%",i.appendChild(e),t=n-e.offsetWidth,document.body.removeChild(i),t}function h(e){for(var t=[e];e.parentNode;)e=e.parentNode,t.push(e);return t}Object.defineProperty(t,"__esModule",{value:!0});var u=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),v=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},p="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},f=n(2),m=function(e){return e&&e.__esModule?e:{default:e}}(f),_=null,y=Object.freeze({el:null,animate:!0,animateClass:"fade",animateInClass:"show",appendTo:"body",backdrop:!0,keyboard:!0,title:!1,header:!0,content:!1,footer:!0,buttons:null,headerClose:!0,construct:!1,transition:300,backdropTransition:150}),b=r({dialog:[{text:"Cancel",value:!1,attr:{class:"btn btn-default","data-dismiss":"modal"}},{text:"OK",value:!0,attr:{class:"btn btn-primary","data-dismiss":"modal"}}],alert:[{text:"OK",attr:{class:"btn btn-primary","data-dismiss":"modal"}}],confirm:[{text:"Cancel",value:!1,attr:{class:"btn btn-default","data-dismiss":"modal"}},{text:"OK",value:!0,attr:{class:"btn btn-primary","data-dismiss":"modal"}}]}),g={container:'<div class="modal"></div>',dialog:'<div class="modal-dialog"></div>',content:'<div class="modal-content"></div>',header:'<div class="modal-header"></div>',headerClose:'<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>',body:'<div class="modal-body"></div>',footer:'<div class="modal-footer"></div>',backdrop:'<div class="modal-backdrop"></div>'},k=function(e){function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};i(this,t);var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this));if(n.id=a(),n.el=null,n._html={},n._events={},n._visible=!1,n._pointerInContent=!1,n._options=v({},t.options,e),n._templates=v({},t.templates,e.templates||{}),n._html.appendTo=document.querySelector(n._options.appendTo),n._scrollbarWidth=c(),null===n._options.buttons&&(n._options.buttons=t.buttons.dialog),n._options.el){var s=n._options.el;if("string"==typeof n._options.el&&!(s=document.querySelector(n._options.el)))throw new Error("Selector: DOM Element "+n._options.el+" not found.");l(s,"modal",n),n.el=s}else n._options.construct=!0;return n._options.construct?n._render():n._mapDom(),n}return s(t,e),u(t,null,[{key:"alert",value:function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return new t(v({},y,{title:e,content:!1,construct:!0,headerClose:!1,buttons:t.buttons.alert},n))}},{key:"confirm",value:function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return new t(v({},y,{title:e,content:!1,construct:!0,headerClose:!1,buttons:t.buttons.confirm},n))}},{key:"templates",set:function(e){this._baseTemplates=e},get:function(){return v({},g,t._baseTemplates||{})}},{key:"buttons",set:function(e){this._baseButtons=e},get:function(){return v({},b,t._baseButtons||{})}},{key:"options",set:function(e){this._baseOptions=e},get:function(){return v({},y,t._baseOptions||{})}},{key:"version",get:function(){return"0.12.0"}}]),u(t,[{key:"_render",value:function(){var e=this._html,t=this._options,n=this._templates,i=!!t.animate&&t.animateClass;return e.container=d(n.container),e.dialog=d(n.dialog),e.content=d(n.content),e.header=d(n.header),e.headerClose=d(n.headerClose),e.body=d(n.body),e.footer=d(n.footer),i&&e.container.classList.add(i),this._setHeader(),this._setContent(),this._setFooter(),this.el=e.container,e.dialog.appendChild(e.content),e.container.appendChild(e.dialog),this}},{key:"_mapDom",value:function(){var e=this._html,t=this._options;return this.el.classList.contains(t.animateClass)&&(t.animate=!0),e.container=this.el,e.dialog=this.el.querySelector(".modal-dialog"),e.content=this.el.querySelector(".modal-content"),e.header=this.el.querySelector(".modal-header"),e.headerClose=this.el.querySelector(".modal-header .close"),e.body=this.el.querySelector(".modal-body"),e.footer=this.el.querySelector(".modal-footer"),this._setHeader(),this._setContent(),this._setFooter(),this}},{key:"_setHeader",value:function(){var e=this._html,t=this._options;t.header&&e.header&&(t.title.nodeName?e.header.innerHTML=t.title.outerHTML:"string"==typeof t.title&&(e.header.innerHTML='<h4 class="modal-title">'+t.title+"</h4>"),null===this.el&&e.headerClose&&t.headerClose&&e.header.appendChild(e.headerClose),t.construct&&e.content.appendChild(e.header))}},{key:"_setContent",value:function(){var e=this._html,t=this._options;t.content&&e.body&&("string"==typeof t.content?e.body.innerHTML=t.content:e.body.innerHTML=t.content.outerHTML,t.construct&&e.content.appendChild(e.body))}},{key:"_setFooter",value:function(){var e=this._html,t=this._options;t.footer&&e.footer&&(t.footer.nodeName?e.footer.ineerHTML=t.footer.outerHTML:"string"==typeof t.footer?e.footer.innerHTML=t.footer:e.footer.children.length||t.buttons.forEach(function(t){var n=document.createElement("button");l(n,"button",t),n.innerHTML=t.text,n.setAttribute("type","button");for(var i in t.attr)n.setAttribute(i,t.attr[i]);e.footer.appendChild(n)}),t.construct&&e.content.appendChild(e.footer))}},{key:"_setEvents",value:function(){var e=(this._options,this._html);this._events.keydownHandler=this._handleKeydownEvent.bind(this),document.body.addEventListener("keydown",this._events.keydownHandler),this._events.mousedownHandler=this._handleMousedownEvent.bind(this),e.container.addEventListener("mousedown",this._events.mousedownHandler),this._events.clickHandler=this._handleClickEvent.bind(this),e.container.addEventListener("click",this._events.clickHandler),this._events.resizeHandler=this._handleResizeEvent.bind(this),window.addEventListener("resize",this._events.resizeHandler)}},{key:"_handleMousedownEvent",value:function(e){var t=this;this._pointerInContent=!1,h(e.target).every(function(e){return!e.classList||!e.classList.contains("modal-content")||(t._pointerInContent=!0,!1)})}},{key:"_handleClickEvent",value:function(e){var t=this;h(e.target).every(function(n){return!("HTML"===n.tagName||!0!==t._options.backdrop&&n.classList.contains("modal")||n.classList.contains("modal-content")||("modal"===n.getAttribute("data-dismiss")?(t.emit("dismiss",t,e,l(e.target,"button")),t.hide(),1):!t._pointerInContent&&n.classList.contains("modal")&&(t.emit("dismiss",t,e,null),t.hide(),1)))}),this._pointerInContent=!1}},{key:"_handleKeydownEvent",value:function(e){27===e.which&&this._options.keyboard&&(this.emit("dismiss",this,e,null),this.hide())}},{key:"_handleResizeEvent",value:function(e){this._resize()}},{key:"show",value:function(){var e=this,t=this._options,n=this._html;return this.emit("show",this),this._checkScrollbar(),this._setScrollbar(),document.body.classList.add("modal-open"),t.construct&&n.appendTo.appendChild(n.container),n.container.style.display="block",n.container.scrollTop=0,!1!==t.backdrop?(this.once("showBackdrop",function(){e._setEvents(),t.animate&&n.container.offsetWidth,n.container.classList.add(t.animateInClass),setTimeout(function(){e._visible=!0,e.emit("shown",e)},t.transition)}),this._backdrop()):(this._setEvents(),t.animate&&n.container.offsetWidth,n.container.classList.add(t.animateInClass),setTimeout(function(){e._visible=!0,e.emit("shown",e)},t.transition)),this._resize(),this}},{key:"toggle",value:function(){this._visible?this.hide():this.show()}},{key:"_resize",value:function(){var e=this._html.container.scrollHeight>document.documentElement.clientHeight;this._html.container.style.paddingLeft=!this.bodyIsOverflowing&&e?this._scrollbarWidth+"px":"",this._html.container.style.paddingRight=this.bodyIsOverflowing&&!e?this._scrollbarWidth+"px":""}},{key:"_backdrop",value:function(){var e=this,t=this._html,n=this._templates,i=this._options,o=!!i.animate&&i.animateClass;t.backdrop=d(n.backdrop),o&&t.backdrop.classList.add(o),t.appendTo.appendChild(t.backdrop),o&&t.backdrop.offsetWidth,t.backdrop.classList.add(i.animateInClass),setTimeout(function(){e.emit("showBackdrop",e)},this._options.backdropTransition)}},{key:"hide",value:function(){var e=this,t=this._html,n=this._options,i=t.container.classList;if(this.emit("hide",this),i.remove(n.animateInClass),n.backdrop){t.backdrop.classList.remove(n.animateInClass)}return this._removeEvents(),setTimeout(function(){document.body.classList.remove("modal-open"),document.body.style.paddingRight=e.originalBodyPad},n.backdropTransition),setTimeout(function(){n.backdrop&&t.backdrop.parentNode.removeChild(t.backdrop),t.container.style.display="none",n.construct&&t.container.parentNode.removeChild(t.container),e._visible=!1,e.emit("hidden",e)},n.transition),this}},{key:"_removeEvents",value:function(){this._events.keydownHandler&&document.body.removeEventListener("keydown",this._events.keydownHandler),this._html.container.removeEventListener("mousedown",this._events.mousedownHandler),this._html.container.removeEventListener("click",this._events.clickHandler),window.removeEventListener("resize",this._events.resizeHandler)}},{key:"_checkScrollbar",value:function(){this.bodyIsOverflowing=document.body.clientWidth<window.innerWidth}},{key:"_setScrollbar",value:function(){if(this.originalBodyPad=document.body.style.paddingRight||"",this.bodyIsOverflowing){var e=parseInt(this.originalBodyPad||0,10);document.body.style.paddingRight=e+this._scrollbarWidth+"px"}}}]),t}(m.default);t.default=k},function(e,t){function n(){this._events=this._events||{},this._maxListeners=this._maxListeners||void 0}function i(e){return"function"==typeof e}function o(e){return"number"==typeof e}function s(e){return"object"==typeof e&&null!==e}function r(e){return void 0===e}e.exports=n,n.EventEmitter=n,n.prototype._events=void 0,n.prototype._maxListeners=void 0,n.defaultMaxListeners=10,n.prototype.setMaxListeners=function(e){if(!o(e)||e<0||isNaN(e))throw TypeError("n must be a positive number");return this._maxListeners=e,this},n.prototype.emit=function(e){var t,n,o,a,l,d;if(this._events||(this._events={}),"error"===e&&(!this._events.error||s(this._events.error)&&!this._events.error.length)){if((t=arguments[1])instanceof Error)throw t;var c=new Error('Uncaught, unspecified "error" event. ('+t+")");throw c.context=t,c}if(n=this._events[e],r(n))return!1;if(i(n))switch(arguments.length){case 1:n.call(this);break;case 2:n.call(this,arguments[1]);break;case 3:n.call(this,arguments[1],arguments[2]);break;default:a=Array.prototype.slice.call(arguments,1),n.apply(this,a)}else if(s(n))for(a=Array.prototype.slice.call(arguments,1),d=n.slice(),o=d.length,l=0;l<o;l++)d[l].apply(this,a);return!0},n.prototype.addListener=function(e,t){var o;if(!i(t))throw TypeError("listener must be a function");return this._events||(this._events={}),this._events.newListener&&this.emit("newListener",e,i(t.listener)?t.listener:t),this._events[e]?s(this._events[e])?this._events[e].push(t):this._events[e]=[this._events[e],t]:this._events[e]=t,s(this._events[e])&&!this._events[e].warned&&(o=r(this._maxListeners)?n.defaultMaxListeners:this._maxListeners)&&o>0&&this._events[e].length>o&&(this._events[e].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[e].length),"function"==typeof console.trace&&console.trace()),this},n.prototype.on=n.prototype.addListener,n.prototype.once=function(e,t){function n(){this.removeListener(e,n),o||(o=!0,t.apply(this,arguments))}if(!i(t))throw TypeError("listener must be a function");var o=!1;return n.listener=t,this.on(e,n),this},n.prototype.removeListener=function(e,t){var n,o,r,a;if(!i(t))throw TypeError("listener must be a function");if(!this._events||!this._events[e])return this;if(n=this._events[e],r=n.length,o=-1,n===t||i(n.listener)&&n.listener===t)delete this._events[e],this._events.removeListener&&this.emit("removeListener",e,t);else if(s(n)){for(a=r;a-- >0;)if(n[a]===t||n[a].listener&&n[a].listener===t){o=a;break}if(o<0)return this;1===n.length?(n.length=0,delete this._events[e]):n.splice(o,1),this._events.removeListener&&this.emit("removeListener",e,t)}return this},n.prototype.removeAllListeners=function(e){var t,n;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[e]&&delete this._events[e],this;if(0===arguments.length){for(t in this._events)"removeListener"!==t&&this.removeAllListeners(t);return this.removeAllListeners("removeListener"),this._events={},this}if(n=this._events[e],i(n))this.removeListener(e,n);else if(n)for(;n.length;)this.removeListener(e,n[n.length-1]);return delete this._events[e],this},n.prototype.listeners=function(e){return this._events&&this._events[e]?i(this._events[e])?[this._events[e]]:this._events[e].slice():[]},n.prototype.listenerCount=function(e){if(this._events){var t=this._events[e];if(i(t))return 1;if(t)return t.length}return 0},n.listenerCount=function(e,t){return e.listenerCount(t)}}]);
diff --git a/html/toys/euler-golf/js/sol.js b/html/toys/euler-golf/js/sol.js
new file mode 100644
index 0000000..b4e527f
--- /dev/null
+++ b/html/toys/euler-golf/js/sol.js
@@ -0,0 +1,44 @@
+const DEPTH = 15;
+
+const DIRECTION = {
+ 0: new cx(0, 1),
+ 1: new cx(0, -1),
+};
+
+const construct_moves = (curr, prev) =>
+ Object.keys(DIRECTION).map((x) => move(curr, prev, DIRECTION[x]));
+
+const backtrack = (local_index, depth) =>
+ local_index
+ .toString(2)
+ .padStart(depth, "0")
+ .split("")
+ .map((direction) => (Number(direction) ? "+" : "-"));
+
+const sol = (target, start_from = new cx(0, 0), start_to = new cx(1, 0)) => {
+ const next_moves = construct_moves(start_from, start_to);
+ const solved_in_first_move = next_moves.findIndex((move) =>
+ cx.eq(move, target)
+ );
+ if (solved_in_first_move != -1) return backtrack(solved_in_first_move, 1);
+
+ let moves = [start_to, ...next_moves];
+ let curr_depth = 2;
+ while (curr_depth < DEPTH) {
+ for (let i = 0; i < Math.pow(2, curr_depth); i++) {
+ const direction = DIRECTION[Number(i.toString(2).at(-1))];
+ // Current element is at i >> 1 + the offset for the previous group (which is
+ // the sum of the geometric series 2**n until curr_depth - 1)
+ const current_i = (i >> 1) + (1 - Math.pow(2, curr_depth - 1)) / (1 - 2);
+ const previous_i = (i >> 2) + (1 - Math.pow(2, curr_depth - 2)) / (1 - 2);
+
+ const new_move = move(moves[previous_i], moves[current_i], direction);
+
+ moves.push(new_move);
+ if (cx.eq(new_move, target)) return backtrack(i, curr_depth);
+ }
+ curr_depth++;
+ }
+
+ return null;
+};
diff --git a/html/toys/fourier/index.html b/html/toys/fourier/index.html
new file mode 100644
index 0000000..ccb8f48
--- /dev/null
+++ b/html/toys/fourier/index.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Simponic's FT Visualizer</title>
+ <style>
+ body {
+ margin: 0;
+ height: 100vh;
+ width: 100vw;
+ padding: 0;
+ }
+
+ #calculator {
+ height: 50vh;
+ width: 100%;
+ }
+
+ .canvas-holder {
+ height: 50vh;
+ width: 100%;
+ }
+
+ .container {
+ display: flex;
+ flex-direction: column;
+ margin-left: auto;
+ margin-right: auto;
+ max-width: 1300px;
+ min-height: 100%;
+ padding-left: 2rem;
+ padding-right: 2rem;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="container">
+ <div id="calculator"></div>
+ <div class="canvas-holder">
+ <canvas id="canvas"></canvas>
+ </div>
+ </div>
+
+ <script src="https://www.desmos.com/api/v1.7/calculator.js?apiKey=dcb31709b452b1cf9dc26972add0fda6"></script>
+ <script src="js/script.js"></script>
+ </body>
+</html>
diff --git a/html/toys/fourier/js/script.js b/html/toys/fourier/js/script.js
new file mode 100644
index 0000000..8d5af31
--- /dev/null
+++ b/html/toys/fourier/js/script.js
@@ -0,0 +1,294 @@
+const RENDER_TYPE = {
+ LATEX: 1,
+ FUNC: 2,
+};
+const THRESHOLD = 1e-12;
+const FONT = "Courier New";
+const FONT_HEIGHT_PX = 24;
+const DX = 4;
+
+const canvas = document.getElementById("canvas");
+const ctx = canvas.getContext("2d");
+let state = {};
+const initializeState = () => {
+ const xLabels = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sept",
+ "Oct",
+ "Nov",
+ "Dec",
+ "Jan",
+ ];
+ const yLabels = ["Great", "Good", "Meh", "Bad", "Horrible"];
+ return {
+ width: canvas.parentElement.clientWidth,
+ height: canvas.parentElement.clientHeight,
+ xLabels,
+ yLabels,
+ yLabelPadding: 12,
+ heights: Array(xLabels.length * DX - 1).fill(0),
+ };
+};
+
+const dft = (heights, render = RENDER_TYPE.LATEX, threshold = THRESHOLD) => {
+ const n = heights.length;
+ return Array(n)
+ .fill()
+ .map((x, w) => {
+ const rate = -2 * Math.PI * w;
+ const s = heights.reduce(
+ (a, x, i) => ({
+ re: a.re + x * Math.cos((rate * i) / n),
+ im: a.im + x * Math.sin((rate * i) / n),
+ }),
+ { re: 0, im: 0 }
+ );
+
+ Object.entries(s).forEach(
+ ([key, value]) =>
+ (s[key] = ((Math.abs(value) < threshold ? 0 : 1) * value) / n)
+ );
+
+ const amp = Math.sqrt(s.re * s.re + s.im * s.im);
+ const phase = Math.atan2(s.im, s.re);
+
+ switch (render) {
+ case RENDER_TYPE.LATEX:
+ return `${amp}\\cos\\left(${w}\\frac{${
+ 2 * DX
+ }\\pi}{${n}}x+${phase}\\right)`;
+ case RENDER_TYPE.FUNC:
+ return (t) => amp * Math.cos(w * t + phase);
+ }
+ });
+};
+
+const resizeCanvas = ({ width, height }) => {
+ canvas.width = width;
+ canvas.style.width = width;
+ canvas.height = height;
+ canvas.style.height = height;
+};
+
+const loop = () => {
+ const stateChanges = Object.keys(state.diff);
+ if (stateChanges.length > 0) {
+ state = { ...state, ...state.diff };
+ if (
+ state.diff.width ||
+ state.diff.height ||
+ state.diff.xLabels ||
+ state.diff.yLabels
+ ) {
+ resizeCanvas(state.diff);
+ ctx.font = `${FONT_HEIGHT_PX}px ${FONT}`;
+ state.maxYLabelWidth = state.yLabels.reduce(
+ (a, label) => Math.max(ctx.measureText(label).width, a),
+ -Infinity
+ );
+
+ state.gridBoxWidth =
+ state.width - state.maxYLabelWidth - state.yLabelPadding;
+ state.gridBoxHeight = state.height - 2.5 * FONT_HEIGHT_PX; // 2.5 to include bottom part of tall letters ("g", "y", etc.)
+
+ state.topLeftGridPos = {
+ x: state.maxYLabelWidth + state.yLabelPadding,
+ y: FONT_HEIGHT_PX,
+ };
+
+ state.bottomRightGridPos = {
+ x: state.topLeftGridPos.x + state.gridBoxWidth,
+ y: state.topLeftGridPos.y + state.gridBoxHeight,
+ };
+ }
+ if (state.diff.heights) drawDesmos();
+ draw(state);
+ state.diff = {};
+ }
+
+ requestAnimationFrame(loop);
+};
+
+const drawLine = (pos1, pos2) => {
+ ctx.beginPath();
+ ctx.moveTo(pos1.x, pos1.y);
+ ctx.lineTo(pos2.x, pos2.y);
+ ctx.stroke();
+};
+
+const drawDividers = (
+ xDividers,
+ yDividers,
+ topLeftGridPos,
+ bottomRightGridPos,
+ yLabelPadding
+) => {
+ ctx.font = `${FONT_HEIGHT_PX}px ${FONT}`;
+ xDividers.forEach(({ label, position }) => {
+ ctx.fillText(label, position.x - ctx.measureText(label).width, position.y);
+ drawLine(
+ { ...position, y: topLeftGridPos.y },
+ { ...position, y: bottomRightGridPos.y }
+ );
+ });
+ yDividers.forEach(({ label, position }) => {
+ ctx.fillText(
+ label,
+ topLeftGridPos.x - yLabelPadding - ctx.measureText(label).width,
+ position.y
+ );
+ drawLine(
+ { ...position, x: topLeftGridPos.x },
+ { ...position, x: bottomRightGridPos.x }
+ );
+ });
+};
+
+const draw = ({
+ heights,
+ gridBoxWidth,
+ gridBoxHeight,
+ topLeftGridPos,
+ bottomRightGridPos,
+ maxYLabelWidth,
+ xLabels,
+ yLabels,
+ width,
+ height,
+}) => {
+ ctx.clearRect(0, 0, width, height);
+
+ const xDividers = xLabels.map((label, i) => ({
+ label,
+ position: {
+ x: topLeftGridPos.x + (gridBoxWidth / (xLabels.length - 1)) * i,
+ y: bottomRightGridPos.y + FONT_HEIGHT_PX,
+ },
+ }));
+
+ const yDividers = yLabels.map((label, i) => ({
+ label,
+ position: {
+ x: 0,
+ y: topLeftGridPos.y + (gridBoxHeight / (yLabels.length - 1)) * i,
+ },
+ }));
+
+ drawDividers(xDividers, yDividers, topLeftGridPos, bottomRightGridPos, 12);
+
+ const dx = gridBoxWidth / (DX * (xLabels.length - 1));
+ const prevStrokeStyle = ctx.strokeStyle;
+ ctx.strokeStyle = "red";
+ for (let i = 0; i < heights.length; ++i) {
+ const x = dx * i + topLeftGridPos.x;
+ drawLine(
+ { x, y: (gridBoxHeight / 2) * (1 - heights[i]) + topLeftGridPos.y },
+ {
+ x: x + dx,
+ y: (gridBoxHeight / 2) * (1 - heights[i + 1]) + topLeftGridPos.y,
+ }
+ );
+ }
+ ctx.strokeStyle = prevStrokeStyle;
+};
+
+const calculator = Desmos.GraphingCalculator(
+ document.getElementById("calculator"),
+ {
+ expressionsCollapsed: true,
+ autosize: true,
+ }
+);
+calculator.setMathBounds({
+ left: -0.8,
+ right: 12,
+ bottom: -3,
+ top: 3,
+});
+
+const drawDesmos = () => {
+ const equations = dft(state.heights);
+
+ calculator.setExpression({
+ id: `graph-total`,
+ latex: equations.map((_x, i) => `y_{${i}}`).join(" + "),
+ });
+ equations.forEach((x, i) =>
+ calculator.setExpression({
+ id: `graph${i}`,
+ latex: `y_{${i}}=${x}`,
+ hidden: true,
+ })
+ );
+};
+
+let isDown = false;
+canvas.addEventListener(
+ "mousedown",
+ (e) => {
+ e.preventDefault();
+ isDown = true;
+ },
+ true
+);
+
+canvas.addEventListener(
+ "mouseup",
+ (e) => {
+ e.preventDefault();
+ isDown = false;
+ },
+ true
+);
+
+canvas.addEventListener(
+ "mousemove",
+ (e) => {
+ e.preventDefault();
+ if (isDown) {
+ const rect = canvas.getBoundingClientRect();
+ const [x, y] = [e.clientX - rect.left, e.clientY - rect.top];
+
+ const {
+ topLeftGridPos,
+ bottomRightGridPos,
+ gridBoxWidth,
+ gridBoxHeight,
+ heights,
+ xLabels,
+ } = state;
+ const delta = gridBoxWidth / (DX * (xLabels.length - 1));
+
+ const bin = Math.min(
+ Math.round(Math.max(x - topLeftGridPos.x, 0) / delta),
+ heights.length - 1
+ );
+ heights[bin] = Math.min(
+ Math.max(1 - (2 * (y - topLeftGridPos.y)) / gridBoxHeight, -1),
+ 1
+ );
+ state.diff.heights = heights;
+ }
+ },
+ true
+);
+
+window.addEventListener("resize", () => {
+ state.diff = {
+ ...state.diff,
+ width: canvas.parentElement.clientWidth,
+ height: canvas.parentElement.clientHeight,
+ };
+});
+
+(() => {
+ state.diff = initializeState();
+ window.requestAnimationFrame(loop);
+})();
diff --git a/html/toys/godel/index.html b/html/toys/godel/index.html
new file mode 100644
index 0000000..50a680a
--- /dev/null
+++ b/html/toys/godel/index.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link rel="stylesheet" href="https://adelie.liz.coffee/bundle.css">
+ <title>Gödel Numbering</title>
+</head>
+<body>
+ <main>
+ <article>
+ <h3>Gödel Numbering</h3>
+ <section>
+ <div>
+ <h4>L Source</h4>
+ <div id="source-editor" class="code-editor-container mb-sm" aria-label="L source code"></div>
+ <div>
+ <button id="compile-btn" class="primary">Compile</button>
+ <button id="copy-btn">Copy State Link</button>
+ </div>
+ <span id="compile-status" class="status-text muted"></span>
+ </div>
+ </section>
+
+ <section class="mt-sm">
+ <div>
+ <h4>"Compiled" JS</h4>
+ <div id="compiled-editor" class="code-editor-container mb-sm" aria-label="Compiled JavaScript"></div>
+ <div class="button-group">
+ <button id="eval-btn">Eval</button>
+ </div>
+ <span id="eval-status" class="status-text muted"></span>
+ </div>
+ </section>
+
+ <section class="mt-sm">
+ <h4>Gödel Sequence</h4>
+ <pre id="godel-sequence">Compile to view the Gödel sequence.</pre>
+ <div>
+ <button id="compute-godel-btn">Compute Gödel Number</button>
+ </div>
+ <pre id="godel-number"></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/html/toys/godel/js/compiler.js b/html/toys/godel/js/compiler.js
new file mode 100644
index 0000000..d15c0f4
--- /dev/null
+++ b/html/toys/godel/js/compiler.js
@@ -0,0 +1,226 @@
+const INDENT_SIZE = 2;
+
+class CodeBuilder {
+ constructor(indentSize = INDENT_SIZE) {
+ this.indentSize = indentSize;
+ this.indentLevel = 0;
+ this.parts = [];
+ }
+
+ addLine(line = "") {
+ const indent = " ".repeat(this.indentLevel * this.indentSize);
+ this.parts.push(line ? `${indent}${line}` : "");
+ }
+
+ open(line) {
+ if (line) {
+ this.addLine(line);
+ }
+ this.indentLevel += 1;
+ }
+
+ close(line) {
+ this.indentLevel = Math.max(0, this.indentLevel - 1);
+ if (line) {
+ this.addLine(line);
+ }
+ }
+
+ toString() {
+ return this.parts.join("\n");
+ }
+}
+
+const compileGoto = (gotoNode, builder) => {
+ builder.addLine(`this.followGoto("${gotoNode.label.symbol}");`);
+ builder.addLine("return;");
+};
+
+const compileConditional = (conditionalNode, builder) => {
+ const variable = conditionalNode.variable.symbol;
+ builder.addLine(`if (this.get("${variable}") !== 0) {`);
+ builder.open();
+ compileGoto(conditionalNode.goto, builder);
+ builder.close("}");
+};
+
+const compileAssignment = (assignmentNode, builder) => {
+ const variable = assignmentNode.variable.symbol;
+ const expr = assignmentNode.expr || {};
+
+ if (expr.opr === "+") {
+ builder.addLine(`this.addOne("${variable}");`);
+ } else if (expr.opr === "-") {
+ builder.addLine(`this.subtractOne("${variable}");`);
+ } else {
+ builder.addLine("// noop");
+ }
+};
+
+const compileInstruction = (instruction, builder) => {
+ if (instruction.goto) {
+ compileGoto(instruction.goto, builder);
+ return;
+ }
+
+ if (instruction.conditional) {
+ compileConditional(instruction.conditional, builder);
+ } else if (instruction.assignment) {
+ compileAssignment(instruction.assignment, builder);
+ }
+
+ builder.addLine("this.instructionPointer++;");
+};
+
+const emitMethod = (builder, signature, bodyFn) => {
+ builder.addLine(`${signature} {`);
+ builder.open();
+ bodyFn();
+ builder.close("}");
+ builder.addLine("");
+};
+
+const emitConstructor = (builder, ast, godelSequence, methodCatalog) => {
+ emitMethod(builder, "constructor()", () => {
+ builder.addLine("this.variables = new Map();");
+ builder.addLine("this.labelInstructions = new Map();");
+ builder.addLine("this.instructions = new Map();");
+ builder.addLine("this.instructions.set(0, () => this.main());");
+ builder.addLine("this.instructionPointer = 0;");
+ builder.addLine('this.variables.set("Y", 0);');
+ builder.addLine(`this.finalInstruction = ${ast.instructions.length + 1};`);
+ builder.addLine('this.labelInstructions.set("E1", this.finalInstruction);');
+ builder.addLine("");
+ builder.addLine("// instruction bindings");
+
+ ast.instructions.forEach((entry, index) => {
+ const instructionNode = entry.instruction;
+ const instructionIdx = index + 1;
+ godelSequence.push(entry.godel);
+
+ if (instructionNode.label) {
+ const labelName = instructionNode.label.symbol;
+ builder.addLine(
+ `this.instructions.set(${instructionIdx}, () => this.${labelName}());`
+ );
+ builder.addLine(
+ `this.labelInstructions.set("${labelName}", ${instructionIdx});`
+ );
+ methodCatalog.push({
+ name: labelName,
+ index: instructionIdx,
+ node: instructionNode.instruction
+ });
+ } else {
+ const methodName = `instruction${instructionIdx}`;
+ builder.addLine(
+ `this.instructions.set(${instructionIdx}, () => this.${methodName}());`
+ );
+ methodCatalog.push({
+ name: methodName,
+ index: instructionIdx,
+ node: instructionNode
+ });
+ }
+ });
+ });
+};
+
+const emitRuntimeHelpers = (builder, instructionCount) => {
+ emitMethod(builder, "get(variable)", () => {
+ builder.addLine("if (!this.variables.has(variable)) {");
+ builder.open();
+ builder.addLine("this.variables.set(variable, 0);");
+ builder.close("}");
+ builder.addLine("return this.variables.get(variable);");
+ });
+
+ emitMethod(builder, "addOne(variable)", () => {
+ builder.addLine("const val = this.get(variable);");
+ builder.addLine("this.variables.set(variable, val + 1);");
+ });
+
+ emitMethod(builder, "subtractOne(variable)", () => {
+ builder.addLine("const val = this.get(variable);");
+ builder.addLine("this.variables.set(variable, val - 1);");
+ });
+
+ emitMethod(builder, "followGoto(label)", () => {
+ builder.addLine("this.instructionPointer = this.labelInstructions.get(label);");
+ });
+
+ emitMethod(builder, "step()", () => {
+ builder.addLine("if (!this.isCompleted()) {");
+ builder.open();
+ builder.addLine("const procedure = this.instructions.get(this.instructionPointer);");
+ builder.addLine("procedure();");
+ builder.close("}");
+ builder.addLine("return this.instructionPointer;");
+ });
+
+ emitMethod(builder, "isCompleted()", () => {
+ builder.addLine("return this.instructionPointer === this.finalInstruction;");
+ });
+
+ emitMethod(builder, "getResult()", () => {
+ builder.addLine('return this.variables.get("Y");');
+ });
+
+ emitMethod(builder, "run(maxIter = 500_000)", () => {
+ builder.addLine("let iter = 0;");
+ builder.addLine("while (!this.isCompleted() && ++iter < maxIter) {");
+ builder.open();
+ builder.addLine("this.step();");
+ builder.close("}");
+ builder.addLine("if (iter < maxIter) {");
+ builder.open();
+ builder.addLine("return this.getResult();");
+ builder.close("}");
+ builder.addLine(
+ 'throw new Error("Program exceeded iteration limit. Try optimizing your instructions or increasing the cap.");'
+ );
+ });
+
+ emitMethod(builder, "main()", () => {
+ if (instructionCount > 0) {
+ builder.addLine("this.instructionPointer = 1;");
+ } else {
+ builder.addLine("this.instructionPointer = this.finalInstruction;");
+ }
+ });
+};
+
+const emitInstructionMethods = (builder, catalog) => {
+ catalog.forEach((entry) => {
+ emitMethod(builder, `${entry.name}()`, () => {
+ builder.addLine(`this.instructionPointer = ${entry.index};`);
+ compileInstruction(entry.node, builder);
+ });
+ });
+};
+
+export const compileProgram = (ast) => {
+ const builder = new CodeBuilder();
+ const godelSequence = [];
+ const methodCatalog = [];
+
+ builder.addLine("class Program {");
+ builder.open();
+ emitConstructor(builder, ast, godelSequence, methodCatalog);
+ emitRuntimeHelpers(builder, ast.instructions.length);
+ emitInstructionMethods(builder, methodCatalog);
+ builder.close("}");
+ builder.addLine("");
+ builder.addLine("// bootstrap");
+ builder.addLine("const program = new Program();");
+ builder.addLine('// program.variables.set("X1", 2);');
+ builder.addLine('// program.variables.set("X2", 3);');
+ builder.addLine("program.run();");
+ builder.addLine("console.log(program.variables);");
+ builder.addLine("program.getResult();");
+
+ return {
+ js: builder.toString(),
+ godelSequence
+ };
+};
diff --git a/html/toys/godel/js/godel-worker.js b/html/toys/godel/js/godel-worker.js
new file mode 100644
index 0000000..1ba9da4
--- /dev/null
+++ b/html/toys/godel/js/godel-worker.js
@@ -0,0 +1,42 @@
+const isPrime = (n) => {
+ if (n < 2) {
+ return false;
+ }
+ if (n === 2) {
+ return true;
+ }
+ if (n % 2 === 0) {
+ return false;
+ }
+ const limit = Math.floor(Math.sqrt(n));
+ for (let i = 3; i <= limit; i += 2) {
+ if (n % i === 0) {
+ return false;
+ }
+ }
+ return true;
+};
+
+const primes = [2];
+const primeAt = (index) => {
+ while (primes.length < index) {
+ let candidate = primes[primes.length - 1] + 1;
+ while (!isPrime(candidate)) {
+ candidate += 1;
+ }
+ primes.push(candidate);
+ }
+ return primes[index - 1];
+};
+
+const computeGodelNumber = (sequence) => {
+ return sequence.reduce((acc, exponent, idx) => {
+ const prime = BigInt(primeAt(idx + 1));
+ return acc * prime ** BigInt(exponent);
+ }, BigInt(1)) - BigInt(1);
+};
+
+self.addEventListener("message", (event) => {
+ const result = computeGodelNumber(event.data);
+ self.postMessage(result.toString());
+});
diff --git a/html/toys/godel/js/main.js b/html/toys/godel/js/main.js
new file mode 100644
index 0000000..b752206
--- /dev/null
+++ b/html/toys/godel/js/main.js
@@ -0,0 +1,5 @@
+import { GodelPlayground } from "./ui.js";
+
+document.addEventListener("DOMContentLoaded", () => {
+ new GodelPlayground();
+});
diff --git a/html/toys/godel/js/parser.js b/html/toys/godel/js/parser.js
new file mode 100644
index 0000000..a9436b6
--- /dev/null
+++ b/html/toys/godel/js/parser.js
@@ -0,0 +1,1059 @@
+const parser = /*
+ * Generated by PEG.js 0.10.0.
+ *
+ * http://pegjs.org/
+ */
+(function() {
+ "use strict";
+
+ function peg$subclass(child, parent) {
+ function ctor() { this.constructor = child; }
+ ctor.prototype = parent.prototype;
+ child.prototype = new ctor();
+ }
+
+ function peg$SyntaxError(message, expected, found, location) {
+ this.message = message;
+ this.expected = expected;
+ this.found = found;
+ this.location = location;
+ this.name = "SyntaxError";
+
+ if (typeof Error.captureStackTrace === "function") {
+ Error.captureStackTrace(this, peg$SyntaxError);
+ }
+ }
+
+ peg$subclass(peg$SyntaxError, Error);
+
+ peg$SyntaxError.buildMessage = function(expected, found) {
+ var DESCRIBE_EXPECTATION_FNS = {
+ literal: function(expectation) {
+ return "\"" + literalEscape(expectation.text) + "\"";
+ },
+
+ "class": function(expectation) {
+ var escapedParts = "",
+ i;
+
+ for (i = 0; i < expectation.parts.length; i++) {
+ escapedParts += expectation.parts[i] instanceof Array
+ ? classEscape(expectation.parts[i][0]) + "-" + classEscape(expectation.parts[i][1])
+ : classEscape(expectation.parts[i]);
+ }
+
+ return "[" + (expectation.inverted ? "^" : "") + escapedParts + "]";
+ },
+
+ any: function(expectation) {
+ return "any character";
+ },
+
+ end: function(expectation) {
+ return "end of input";
+ },
+
+ other: function(expectation) {
+ return expectation.description;
+ }
+ };
+
+ function hex(ch) {
+ return ch.charCodeAt(0).toString(16).toUpperCase();
+ }
+
+ function literalEscape(s) {
+ return s
+ .replace(/\\/g, '\\\\')
+ .replace(/"/g, '\\"')
+ .replace(/\0/g, '\\0')
+ .replace(/\t/g, '\\t')
+ .replace(/\n/g, '\\n')
+ .replace(/\r/g, '\\r')
+ .replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); })
+ .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); });
+ }
+
+ function classEscape(s) {
+ return s
+ .replace(/\\/g, '\\\\')
+ .replace(/\]/g, '\\]')
+ .replace(/\^/g, '\\^')
+ .replace(/-/g, '\\-')
+ .replace(/\0/g, '\\0')
+ .replace(/\t/g, '\\t')
+ .replace(/\n/g, '\\n')
+ .replace(/\r/g, '\\r')
+ .replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); })
+ .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); });
+ }
+
+ function describeExpectation(expectation) {
+ return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation);
+ }
+
+ function describeExpected(expected) {
+ var descriptions = new Array(expected.length),
+ i, j;
+
+ for (i = 0; i < expected.length; i++) {
+ descriptions[i] = describeExpectation(expected[i]);
+ }
+
+ descriptions.sort();
+
+ if (descriptions.length > 0) {
+ for (i = 1, j = 1; i < descriptions.length; i++) {
+ if (descriptions[i - 1] !== descriptions[i]) {
+ descriptions[j] = descriptions[i];
+ j++;
+ }
+ }
+ descriptions.length = j;
+ }
+
+ switch (descriptions.length) {
+ case 1:
+ return descriptions[0];
+
+ case 2:
+ return descriptions[0] + " or " + descriptions[1];
+
+ default:
+ return descriptions.slice(0, -1).join(", ")
+ + ", or "
+ + descriptions[descriptions.length - 1];
+ }
+ }
+
+ function describeFound(found) {
+ return found ? "\"" + literalEscape(found) + "\"" : "end of input";
+ }
+
+ return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found.";
+ };
+
+ function peg$parse(input, options) {
+ options = options !== void 0 ? options : {};
+
+ var peg$FAILED = {},
+
+ peg$startRuleFunctions = { Program: peg$parseProgram },
+ peg$startRuleFunction = peg$parseProgram,
+
+ peg$c0 = /^[\n]/,
+ peg$c1 = peg$classExpectation(["\n"], false, false),
+ peg$c2 = function(lines) {
+ return { instructions: lines.filter((line) => typeof line !== "string" || line.trim() != "") };
+ },
+ peg$c3 = function(instruction) {
+ let x = 0;
+ let y = 0;
+ if (instruction.label) {
+ x = instruction.label.godel;
+ y = instruction.instruction.godel;
+ } else {
+ y = instruction.godel;
+ }
+ return { instruction, godel: ((2 ** x) * ((2 * y) + 1) - 1) };
+ },
+ peg$c4 = function(label, instruction) {
+ return { label, instruction };
+ },
+ peg$c5 = "[",
+ peg$c6 = peg$literalExpectation("[", false),
+ peg$c7 = "]",
+ peg$c8 = peg$literalExpectation("]", false),
+ peg$c9 = function(label) {
+ return label;
+ },
+ peg$c10 = function(conditional) { return { conditional, godel: conditional.godel }; },
+ peg$c11 = function(assignment) { return { assignment, godel: assignment.godel }; },
+ peg$c12 = function(goto) { return { goto, godel: goto.godel }; },
+ peg$c13 = function(label) {
+ return { label, godel: label.godel + 2 };
+ },
+ peg$c14 = "IF",
+ peg$c15 = peg$literalExpectation("IF", false),
+ peg$c16 = "!=",
+ peg$c17 = peg$literalExpectation("!=", false),
+ peg$c18 = "0",
+ peg$c19 = peg$literalExpectation("0", false),
+ peg$c20 = function(variable, goto) {
+ const y = variable.godel - 1;
+ const x = goto.godel;
+ return { variable, goto, godel: ((2 ** x) * ((2 * y) + 1) - 1) };
+ },
+ peg$c21 = "<-",
+ peg$c22 = peg$literalExpectation("<-", false),
+ peg$c23 = function(variable, expr) {
+ if (expr.left.symbol != variable.symbol) {
+ error("left hand variable must match right hand");
+ }
+ const x = expr.instructionNumber;
+ const y = variable.godel - 1;
+ return { variable, expr, godel: ((2 ** x) * ((2 * y) + 1) - 1) };
+ },
+ peg$c24 = "1",
+ peg$c25 = peg$literalExpectation("1", false),
+ peg$c26 = function(left, opr) {
+ const instructionNumber = { "+" : 1, "-" : 2 }[opr];
+ return { left, opr, instructionNumber };
+ },
+ peg$c27 = function(left) {
+ return { left, instructionNumber: 0 };
+ },
+ peg$c28 = "Y",
+ peg$c29 = peg$literalExpectation("Y", false),
+ peg$c30 = function(symbol) { return { symbol, godel: 1 }; },
+ peg$c31 = "X",
+ peg$c32 = peg$literalExpectation("X", false),
+ peg$c33 = "Z",
+ peg$c34 = peg$literalExpectation("Z", false),
+ peg$c35 = function(symbol, ind) {
+ const index = parseInt(ind);
+ const order = ["X", "Z"];
+ const godel = index * order.length + order.indexOf(symbol);
+ return { symbol: symbol + ind, godel };
+ },
+ peg$c36 = "GOTO",
+ peg$c37 = peg$literalExpectation("GOTO", false),
+ peg$c38 = "+",
+ peg$c39 = peg$literalExpectation("+", false),
+ peg$c40 = "-",
+ peg$c41 = peg$literalExpectation("-", false),
+ peg$c42 = /^[A-E]/,
+ peg$c43 = peg$classExpectation([["A", "E"]], false, false),
+ peg$c44 = function(symbol, ind) {
+ const index = parseInt(ind);
+ const godel = (symbol.charCodeAt(0) - "A".charCodeAt(0) + 1) + 5*(index-1);
+ return { symbol: symbol + ind, godel };
+ },
+ peg$c45 = peg$otherExpectation("integer"),
+ peg$c46 = /^[0-9]/,
+ peg$c47 = peg$classExpectation([["0", "9"]], false, false),
+ peg$c48 = function() { return parseInt(text(), 10); },
+ peg$c49 = peg$otherExpectation("whitespace"),
+ peg$c50 = /^[ \t]/,
+ peg$c51 = peg$classExpectation([" ", "\t"], false, false),
+ peg$c52 = function() { },
+
+ peg$currPos = 0,
+ peg$savedPos = 0,
+ peg$posDetailsCache = [{ line: 1, column: 1 }],
+ peg$maxFailPos = 0,
+ peg$maxFailExpected = [],
+ peg$silentFails = 0,
+
+ peg$result;
+
+ if ("startRule" in options) {
+ if (!(options.startRule in peg$startRuleFunctions)) {
+ throw new Error("Can't start parsing from rule \"" + options.startRule + "\".");
+ }
+
+ peg$startRuleFunction = peg$startRuleFunctions[options.startRule];
+ }
+
+ function text() {
+ return input.substring(peg$savedPos, peg$currPos);
+ }
+
+ function location() {
+ return peg$computeLocation(peg$savedPos, peg$currPos);
+ }
+
+ function expected(description, location) {
+ location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos)
+
+ throw peg$buildStructuredError(
+ [peg$otherExpectation(description)],
+ input.substring(peg$savedPos, peg$currPos),
+ location
+ );
+ }
+
+ function error(message, location) {
+ location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos)
+
+ throw peg$buildSimpleError(message, location);
+ }
+
+ function peg$literalExpectation(text, ignoreCase) {
+ return { type: "literal", text: text, ignoreCase: ignoreCase };
+ }
+
+ function peg$classExpectation(parts, inverted, ignoreCase) {
+ return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase };
+ }
+
+ function peg$anyExpectation() {
+ return { type: "any" };
+ }
+
+ function peg$endExpectation() {
+ return { type: "end" };
+ }
+
+ function peg$otherExpectation(description) {
+ return { type: "other", description: description };
+ }
+
+ function peg$computePosDetails(pos) {
+ var details = peg$posDetailsCache[pos], p;
+
+ if (details) {
+ return details;
+ } else {
+ p = pos - 1;
+ while (!peg$posDetailsCache[p]) {
+ p--;
+ }
+
+ details = peg$posDetailsCache[p];
+ details = {
+ line: details.line,
+ column: details.column
+ };
+
+ while (p < pos) {
+ if (input.charCodeAt(p) === 10) {
+ details.line++;
+ details.column = 1;
+ } else {
+ details.column++;
+ }
+
+ p++;
+ }
+
+ peg$posDetailsCache[pos] = details;
+ return details;
+ }
+ }
+
+ function peg$computeLocation(startPos, endPos) {
+ var startPosDetails = peg$computePosDetails(startPos),
+ endPosDetails = peg$computePosDetails(endPos);
+
+ return {
+ start: {
+ offset: startPos,
+ line: startPosDetails.line,
+ column: startPosDetails.column
+ },
+ end: {
+ offset: endPos,
+ line: endPosDetails.line,
+ column: endPosDetails.column
+ }
+ };
+ }
+
+ function peg$fail(expected) {
+ if (peg$currPos < peg$maxFailPos) { return; }
+
+ if (peg$currPos > peg$maxFailPos) {
+ peg$maxFailPos = peg$currPos;
+ peg$maxFailExpected = [];
+ }
+
+ peg$maxFailExpected.push(expected);
+ }
+
+ function peg$buildSimpleError(message, location) {
+ return new peg$SyntaxError(message, null, null, location);
+ }
+
+ function peg$buildStructuredError(expected, found, location) {
+ return new peg$SyntaxError(
+ peg$SyntaxError.buildMessage(expected, found),
+ expected,
+ found,
+ location
+ );
+ }
+
+ function peg$parseProgram() {
+ var s0, s1, s2;
+
+ s0 = peg$currPos;
+ s1 = [];
+ s2 = peg$parseProgramInstruction();
+ if (s2 === peg$FAILED) {
+ s2 = peg$parse_();
+ if (s2 === peg$FAILED) {
+ if (peg$c0.test(input.charAt(peg$currPos))) {
+ s2 = input.charAt(peg$currPos);
+ peg$currPos++;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c1); }
+ }
+ }
+ }
+ while (s2 !== peg$FAILED) {
+ s1.push(s2);
+ s2 = peg$parseProgramInstruction();
+ if (s2 === peg$FAILED) {
+ s2 = peg$parse_();
+ if (s2 === peg$FAILED) {
+ if (peg$c0.test(input.charAt(peg$currPos))) {
+ s2 = input.charAt(peg$currPos);
+ peg$currPos++;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c1); }
+ }
+ }
+ }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c2(s1);
+ }
+ s0 = s1;
+
+ return s0;
+ }
+
+ function peg$parseProgramInstruction() {
+ var s0, s1, s2, s3, s4;
+
+ s0 = peg$currPos;
+ s1 = peg$parse_();
+ if (s1 === peg$FAILED) {
+ s1 = null;
+ }
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parseLabeledInstruction();
+ if (s2 === peg$FAILED) {
+ s2 = peg$parseInstruction();
+ }
+ if (s2 !== peg$FAILED) {
+ s3 = peg$parse_();
+ if (s3 === peg$FAILED) {
+ s3 = null;
+ }
+ if (s3 !== peg$FAILED) {
+ if (peg$c0.test(input.charAt(peg$currPos))) {
+ s4 = input.charAt(peg$currPos);
+ peg$currPos++;
+ } else {
+ s4 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c1); }
+ }
+ if (s4 === peg$FAILED) {
+ s4 = null;
+ }
+ if (s4 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c3(s2);
+ s0 = s1;
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseLabeledInstruction() {
+ var s0, s1, s2, s3;
+
+ s0 = peg$currPos;
+ s1 = peg$parseLabel();
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parse_();
+ if (s2 !== peg$FAILED) {
+ s3 = peg$parseInstruction();
+ if (s3 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c4(s1, s3);
+ s0 = s1;
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseLabel() {
+ var s0, s1, s2, s3, s4, s5;
+
+ s0 = peg$currPos;
+ if (input.charCodeAt(peg$currPos) === 91) {
+ s1 = peg$c5;
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c6); }
+ }
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parse_();
+ if (s2 === peg$FAILED) {
+ s2 = null;
+ }
+ if (s2 !== peg$FAILED) {
+ s3 = peg$parseLABEL_V();
+ if (s3 !== peg$FAILED) {
+ s4 = peg$parse_();
+ if (s4 === peg$FAILED) {
+ s4 = null;
+ }
+ if (s4 !== peg$FAILED) {
+ if (input.charCodeAt(peg$currPos) === 93) {
+ s5 = peg$c7;
+ peg$currPos++;
+ } else {
+ s5 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c8); }
+ }
+ if (s5 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c9(s3);
+ s0 = s1;
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseInstruction() {
+ var s0, s1;
+
+ s0 = peg$currPos;
+ s1 = peg$parseConditional();
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c10(s1);
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ s1 = peg$parseAssignment();
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c11(s1);
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ s1 = peg$parseGoto();
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c12(s1);
+ }
+ s0 = s1;
+ }
+ }
+
+ return s0;
+ }
+
+ function peg$parseGoto() {
+ var s0, s1, s2, s3;
+
+ s0 = peg$currPos;
+ s1 = peg$parseGOTO();
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parse_();
+ if (s2 !== peg$FAILED) {
+ s3 = peg$parseLABEL_V();
+ if (s3 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c13(s3);
+ s0 = s1;
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseConditional() {
+ var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9;
+
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 2) === peg$c14) {
+ s1 = peg$c14;
+ peg$currPos += 2;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c15); }
+ }
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parse_();
+ if (s2 !== peg$FAILED) {
+ s3 = peg$parseVAR();
+ if (s3 !== peg$FAILED) {
+ s4 = peg$parse_();
+ if (s4 === peg$FAILED) {
+ s4 = null;
+ }
+ if (s4 !== peg$FAILED) {
+ if (input.substr(peg$currPos, 2) === peg$c16) {
+ s5 = peg$c16;
+ peg$currPos += 2;
+ } else {
+ s5 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c17); }
+ }
+ if (s5 !== peg$FAILED) {
+ s6 = peg$parse_();
+ if (s6 === peg$FAILED) {
+ s6 = null;
+ }
+ if (s6 !== peg$FAILED) {
+ if (input.charCodeAt(peg$currPos) === 48) {
+ s7 = peg$c18;
+ peg$currPos++;
+ } else {
+ s7 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c19); }
+ }
+ if (s7 !== peg$FAILED) {
+ s8 = peg$parse_();
+ if (s8 !== peg$FAILED) {
+ s9 = peg$parseGoto();
+ if (s9 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c20(s3, s9);
+ s0 = s1;
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseAssignment() {
+ var s0, s1, s2, s3, s4, s5;
+
+ s0 = peg$currPos;
+ s1 = peg$parseVAR();
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parse_();
+ if (s2 !== peg$FAILED) {
+ if (input.substr(peg$currPos, 2) === peg$c21) {
+ s3 = peg$c21;
+ peg$currPos += 2;
+ } else {
+ s3 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c22); }
+ }
+ if (s3 !== peg$FAILED) {
+ s4 = peg$parse_();
+ if (s4 !== peg$FAILED) {
+ s5 = peg$parseExpression();
+ if (s5 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c23(s1, s5);
+ s0 = s1;
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseExpression() {
+ var s0, s1, s2, s3, s4, s5;
+
+ s0 = peg$currPos;
+ s1 = peg$parseVAR();
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parse_();
+ if (s2 !== peg$FAILED) {
+ s3 = peg$parseOPERATION();
+ if (s3 !== peg$FAILED) {
+ s4 = peg$parse_();
+ if (s4 !== peg$FAILED) {
+ if (input.charCodeAt(peg$currPos) === 49) {
+ s5 = peg$c24;
+ peg$currPos++;
+ } else {
+ s5 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c25); }
+ }
+ if (s5 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c26(s1, s3);
+ s0 = s1;
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ s1 = peg$parseVAR();
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c27(s1);
+ }
+ s0 = s1;
+ }
+
+ return s0;
+ }
+
+ function peg$parseVAR() {
+ var s0, s1, s2, s3;
+
+ s0 = peg$currPos;
+ if (input.charCodeAt(peg$currPos) === 89) {
+ s1 = peg$c28;
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c29); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c30(s1);
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.charCodeAt(peg$currPos) === 88) {
+ s1 = peg$c31;
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c32); }
+ }
+ if (s1 === peg$FAILED) {
+ if (input.charCodeAt(peg$currPos) === 90) {
+ s1 = peg$c33;
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c34); }
+ }
+ }
+ if (s1 !== peg$FAILED) {
+ s2 = [];
+ s3 = peg$parseInteger();
+ if (s3 !== peg$FAILED) {
+ while (s3 !== peg$FAILED) {
+ s2.push(s3);
+ s3 = peg$parseInteger();
+ }
+ } else {
+ s2 = peg$FAILED;
+ }
+ if (s2 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c35(s1, s2);
+ s0 = s1;
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ }
+
+ return s0;
+ }
+
+ function peg$parseGOTO() {
+ var s0;
+
+ if (input.substr(peg$currPos, 4) === peg$c36) {
+ s0 = peg$c36;
+ peg$currPos += 4;
+ } else {
+ s0 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c37); }
+ }
+
+ return s0;
+ }
+
+ function peg$parseOPERATION() {
+ var s0;
+
+ if (input.charCodeAt(peg$currPos) === 43) {
+ s0 = peg$c38;
+ peg$currPos++;
+ } else {
+ s0 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c39); }
+ }
+ if (s0 === peg$FAILED) {
+ if (input.charCodeAt(peg$currPos) === 45) {
+ s0 = peg$c40;
+ peg$currPos++;
+ } else {
+ s0 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c41); }
+ }
+ }
+
+ return s0;
+ }
+
+ function peg$parseLABEL_V() {
+ var s0, s1, s2, s3;
+
+ s0 = peg$currPos;
+ if (peg$c42.test(input.charAt(peg$currPos))) {
+ s1 = input.charAt(peg$currPos);
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c43); }
+ }
+ if (s1 !== peg$FAILED) {
+ s2 = [];
+ s3 = peg$parseInteger();
+ if (s3 !== peg$FAILED) {
+ while (s3 !== peg$FAILED) {
+ s2.push(s3);
+ s3 = peg$parseInteger();
+ }
+ } else {
+ s2 = peg$FAILED;
+ }
+ if (s2 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c44(s1, s2);
+ s0 = s1;
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseInteger() {
+ var s0, s1, s2;
+
+ peg$silentFails++;
+ s0 = peg$currPos;
+ s1 = [];
+ if (peg$c46.test(input.charAt(peg$currPos))) {
+ s2 = input.charAt(peg$currPos);
+ peg$currPos++;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c47); }
+ }
+ if (s2 !== peg$FAILED) {
+ while (s2 !== peg$FAILED) {
+ s1.push(s2);
+ if (peg$c46.test(input.charAt(peg$currPos))) {
+ s2 = input.charAt(peg$currPos);
+ peg$currPos++;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c47); }
+ }
+ }
+ } else {
+ s1 = peg$FAILED;
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c48();
+ }
+ s0 = s1;
+ peg$silentFails--;
+ if (s0 === peg$FAILED) {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c45); }
+ }
+
+ return s0;
+ }
+
+ function peg$parse_() {
+ var s0, s1, s2;
+
+ peg$silentFails++;
+ s0 = peg$currPos;
+ s1 = [];
+ if (peg$c50.test(input.charAt(peg$currPos))) {
+ s2 = input.charAt(peg$currPos);
+ peg$currPos++;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c51); }
+ }
+ if (s2 !== peg$FAILED) {
+ while (s2 !== peg$FAILED) {
+ s1.push(s2);
+ if (peg$c50.test(input.charAt(peg$currPos))) {
+ s2 = input.charAt(peg$currPos);
+ peg$currPos++;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c51); }
+ }
+ }
+ } else {
+ s1 = peg$FAILED;
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c52();
+ }
+ s0 = s1;
+ peg$silentFails--;
+ if (s0 === peg$FAILED) {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$c49); }
+ }
+
+ return s0;
+ }
+
+ peg$result = peg$startRuleFunction();
+
+ if (peg$result !== peg$FAILED && peg$currPos === input.length) {
+ return peg$result;
+ } else {
+ if (peg$result !== peg$FAILED && peg$currPos < input.length) {
+ peg$fail(peg$endExpectation());
+ }
+
+ throw peg$buildStructuredError(
+ peg$maxFailExpected,
+ peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null,
+ peg$maxFailPos < input.length
+ ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1)
+ : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)
+ );
+ }
+ }
+
+ return {
+ SyntaxError: peg$SyntaxError,
+ parse: peg$parse
+ };
+})();
+
+export default parser;
+export const SyntaxError = parser.SyntaxError;
diff --git a/html/toys/godel/js/ui.js b/html/toys/godel/js/ui.js
new file mode 100644
index 0000000..97e5209
--- /dev/null
+++ b/html/toys/godel/js/ui.js
@@ -0,0 +1,224 @@
+import parser from "./parser.js";
+import { compileProgram } from "./compiler.js";
+
+const SAMPLE_PROGRAM = `// THIS PROGRAM COMPUTES X1 + X2
+
+// Y <- X1
+[ A1 ] IF X1 != 0 GOTO A2
+ GOTO B1
+[ A2 ] X1 <- X1 - 1
+ Y <- Y + 1
+ GOTO A1
+
+// Z1 <- X2
+[ B1 ] IF X2 != 0 GOTO B2
+ GOTO C1
+[ B2 ] X2 <- X2 - 1
+ Z1 <- Z1 + 1
+ GOTO B1
+
+// Y <- Y + Z1
+[ C1 ] IF Z1 != 0 GOTO C2
+ GOTO E1
+[ C2 ] Z1 <- Z1 - 1
+ Y <- Y + 1
+ GOTO C1`;
+
+const STATUS_VARIANTS = {
+ success: "text-success",
+ error: "text-error",
+ info: "muted"
+};
+
+export class GodelPlayground {
+ constructor() {
+ this.elements = {
+ compileBtn: document.getElementById("compile-btn"),
+ copyBtn: document.getElementById("copy-btn"),
+ evalBtn: document.getElementById("eval-btn"),
+ computeNumberBtn: document.getElementById("compute-godel-btn"),
+ compileStatus: document.getElementById("compile-status"),
+ evalStatus: document.getElementById("eval-status"),
+ godelSequence: document.getElementById("godel-sequence"),
+ godelNumber: document.getElementById("godel-number")
+ };
+
+ this.sourceEditor = null;
+ this.compiledEditor = null;
+ this.worker = null;
+ this.latestSequence = [];
+
+ this.init();
+ }
+
+ init() {
+ this.setupEditors();
+ this.bindEvents();
+ this.hydrateFromParams();
+ if (!this.getSource().trim()) {
+ this.setSource(SAMPLE_PROGRAM);
+ }
+ this.compileSource();
+ }
+
+ setupEditors() {
+ this.sourceEditor = adelieEditor.init("#source-editor", {
+ language: "javascript"
+ });
+ this.compiledEditor = adelieEditor.init("#compiled-editor", {
+ language: "javascript"
+ });
+ this.setSource(SAMPLE_PROGRAM);
+ }
+
+ bindEvents() {
+ this.elements.compileBtn.addEventListener("click", () => this.compileSource());
+ this.elements.evalBtn.addEventListener("click", () => this.evaluateCompiled());
+ this.elements.copyBtn.addEventListener("click", () => this.copyShareLink());
+ this.elements.computeNumberBtn.addEventListener("click", () => this.computeGodelNumber());
+
+ document.addEventListener("keydown", (event) => {
+ if (event.ctrlKey && event.key === "Enter") {
+ event.preventDefault();
+ this.compileSource();
+ }
+ });
+ }
+
+ hydrateFromParams() {
+ const params = new URLSearchParams(window.location.search);
+ const encoded = params.get("instructions");
+ if (encoded) {
+ try {
+ const decoded = atob(encoded);
+ this.setSource(decoded);
+ } catch (error) {
+ console.warn("Failed to decode instructions from URL", error);
+ }
+ }
+ }
+
+ getSource() {
+ return this.sourceEditor.state.doc.toString();
+ }
+
+ setSource(content) {
+ const docLength = this.sourceEditor.state.doc.toString().length;
+ this.sourceEditor.dispatch({
+ changes: { from: 0, to: docLength, insert: content }
+ });
+ }
+
+ getCompiled() {
+ return this.compiledEditor.state.doc.toString();
+ }
+
+ setCompiled(content) {
+ const docLength = this.compiledEditor.state.doc.toString().length;
+ this.compiledEditor.dispatch({
+ changes: { from: 0, to: docLength, insert: content }
+ });
+ }
+
+ prepareSource(source) {
+ return source.replace(/\/\/.*$/gm, "").trim();
+ }
+
+ compileSource() {
+ const raw = this.getSource();
+ const prepared = this.prepareSource(raw);
+
+ if (!prepared) {
+ this.setStatus("compile", "Provide some source to compile", "error");
+ return;
+ }
+
+ try {
+ const ast = parser.parse(prepared);
+ const { js, godelSequence } = compileProgram(ast);
+ this.latestSequence = godelSequence;
+ this.setCompiled(js);
+ this.renderSequence(godelSequence);
+ this.setStatus("compile", "Successful compilation", "success");
+ this.elements.computeNumberBtn.disabled = godelSequence.length === 0;
+ } catch (error) {
+ this.latestSequence = [];
+ this.renderSequence([]);
+ this.elements.computeNumberBtn.disabled = true;
+ this.setStatus("compile", error.message || "Error compiling", "error");
+ }
+ }
+
+ evaluateCompiled() {
+ const js = this.getCompiled();
+ if (!js.trim()) {
+ this.setStatus("eval", "Compile a program first", "error");
+ return;
+ }
+
+ try {
+ const result = (0, eval)(js);
+ this.setStatus(
+ "eval",
+ `Result: ${typeof result === "undefined" ? "(no return)" : result}`,
+ "success"
+ );
+ } catch (error) {
+ this.setStatus("eval", error.message || "Failed to evaluate program", "error");
+ }
+ }
+
+ renderSequence(sequence) {
+ if (!sequence.length) {
+ this.elements.godelSequence.textContent = "Compile to view the Gödel sequence.";
+ this.elements.godelNumber.textContent = "";
+ return;
+ }
+ this.elements.godelSequence.textContent = `[${sequence.join(", ")}]`;
+ this.elements.godelNumber.textContent = "";
+ }
+
+ copyShareLink() {
+ const data = btoa(this.getSource());
+ const url = `${window.location.href.split("?")[0]}?instructions=${data}`;
+
+ navigator.clipboard.writeText(url)
+ .then(() => alert("Shareable link copied to clipboard"))
+ .catch(() => alert("Failed to copy link"));
+ }
+
+ computeGodelNumber() {
+ if (!this.latestSequence.length) {
+ this.setStatus("compile", "Compile a program to produce its Gödel sequence", "error");
+ return;
+ }
+
+ this.elements.godelNumber.textContent = "Working...";
+ this.elements.computeNumberBtn.disabled = true;
+
+ const worker = this.getWorker();
+ worker.onmessage = (event) => {
+ this.elements.godelNumber.textContent = event.data;
+ this.elements.computeNumberBtn.disabled = false;
+ };
+ worker.onerror = () => {
+ this.elements.godelNumber.textContent = "Failed to compute Gödel number";
+ this.elements.computeNumberBtn.disabled = false;
+ };
+ worker.postMessage(this.latestSequence);
+ }
+
+ getWorker() {
+ if (!this.worker) {
+ const workerUrl = new URL("./godel-worker.js", import.meta.url);
+ this.worker = new Worker(workerUrl, { type: "module" });
+ }
+ return this.worker;
+ }
+
+ setStatus(kind, message, variant = "info") {
+ const element = kind === "compile" ? this.elements.compileStatus : this.elements.evalStatus;
+ element.textContent = message;
+ element.className = `status-text ${STATUS_VARIANTS[variant] || ""}`;
+ }
+}
diff --git a/html/toys/index.html b/html/toys/index.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/html/toys/index.html
diff --git a/html/toys/julia/index.html b/html/toys/julia/index.html
new file mode 100644
index 0000000..9d9de51
--- /dev/null
+++ b/html/toys/julia/index.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <style>
+ body {
+ height: 100vh;
+ width: 100vw;
+ margin: 0;
+ }
+ .bottomRight {
+ padding: 12px;
+ position: fixed;
+ bottom: 0;
+ right: 0;
+ background-color: rgba(255,255,255,0.5);
+ border: 1px solid white;
+ border-radius: 8px;
+ margin-right: 6px;
+ margin-bottom: 6px;
+ }
+ .verticalSlider
+ {
+ writing-mode: bt-lr; /* IE */
+ -webkit-appearance: slider-vertical; /* Chromium */
+ width: 8px;
+ padding: 0 5px;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="canvasHolder">
+
+ </div>
+ <div class="bottomRight">
+ <input orient="vertical" type="range" min="-1000" max="1000" value="0" id="imaginarySlider" class="verticalSlider">
+ <input type="range" min="-1000" max="1000" value="0" id="realSlider">
+ <br>
+ imaginary: <input id="imag-val" size="10" type="number" step="0.001"></input>
+ <button id="animate-imag" onclick="startAnim('animate-imag', 'ci')">Animate</button>
+ <br>
+ real: <input id="real-val" size="10" type="number" step="0.001"></input>
+ <button id="animate-real" onclick="startAnim('animate-real', 'cr')">Animate</button>
+ <br>
+ powered by <a href="https://github.com/gpujs/gpu.js">gpu.js</a>
+ </div>
+
+ <script src="https://cdn.jsdelivr.net/npm/gpu.js@latest/dist/gpu-browser.min.js"></script>
+ <script src="julia.js"></script>
+ </body>
+</html>
diff --git a/html/toys/julia/julia.js b/html/toys/julia/julia.js
new file mode 100644
index 0000000..0744e6f
--- /dev/null
+++ b/html/toys/julia/julia.js
@@ -0,0 +1,143 @@
+const MIN_ZOOM = 0.0001;
+const MAX_ZOOM = 4;
+const C_THRESHOLD = Math.sqrt(2)/2;
+const SLIDER_DIV = 2000*C_THRESHOLD;
+
+function initGPU() {
+ try {
+ return new window.GPU.GPU();
+ } catch (e) {
+ return new GPU();
+ }
+}
+const gpu = initGPU();
+const buildRender = (width, height) => gpu.createKernel(function (maxIterations, cr, ci, centerX, centerY, zoom, colorMultipliers) {
+ let zx = (this.output.x / this.output.y) * (centerX + (4 * this.thread.x / this.output.x - 2) * (zoom / 4));
+ let zy = centerY + (4 * this.thread.y / this.output.y - 2) * (zoom / 4);
+ let iterations = 0;
+ for (let i = 0; i < maxIterations; i++) {
+ const xtemp = zx * zx - zy * zy + cr;
+ zy = 2 * zx * zy + ci;
+ zx = xtemp;
+ if (zx * zx + zy * zy > 4) {
+ iterations = i;
+ break;
+ }
+ }
+ if (iterations == 0 || iterations == maxIterations) {
+ this.color(0, 0, 0);
+ } else {
+ this.color(colorMultipliers[0] * iterations, colorMultipliers[1] * iterations, colorMultipliers[2] * iterations);
+ }
+}, { output: [width, height], graphical: true });
+
+const canvasHolder = document.getElementById('canvasHolder');
+let render; // The GPU kernel built from buildRender
+let state = {
+ colorMultipliers: [0.01 * Math.random() + 0.015, 0.03 * Math.random() + 0.007, 0.02 * Math.random() + 0.010],
+ changes: {
+ centerX: 0,
+ centerY: 0,
+ zoom: 3,
+ cr: parseFloat(document.getElementById('realSlider').value) / SLIDER_DIV,
+ ci: parseFloat(document.getElementById('imaginarySlider').value) / SLIDER_DIV,
+ maxIterations: 1000,
+ width: document.body.clientWidth,
+ height: document.body.clientHeight,
+ },
+};
+
+const doRender = (renderF, state) => {
+ // gpu.js doesn't support JS objects as kernel parameters - https://github.com/gpujs/gpu.js/issues/245
+ renderF(state.maxIterations, state.cr, state.ci, state.centerX, state.centerY, state.zoom, state.colorMultipliers);
+};
+
+const loop = () => {
+ const stateChanges = Object.keys(state.changes);
+ if (stateChanges.length > 0) {
+ state = {...state, ...state.changes};
+ if (state.changes.width || state.changes.height) {
+ render = buildRender(state.width, state.height);
+ canvasHolder.appendChild(render.canvas);
+ }
+ if (typeof state.changes.ci !== 'undefined') {
+ document.getElementById('imag-val').value = state.ci.toFixed(4);
+ document.getElementById('imaginarySlider').value = state.changes.ci * SLIDER_DIV;
+ }
+ if (typeof state.changes.cr !== 'undefined') {
+ document.getElementById('real-val').value = state.cr.toFixed(4);
+ document.getElementById('realSlider').value = state.changes.cr * SLIDER_DIV;
+ }
+ state.changes = {};
+ doRender(render, state);
+ }
+
+ requestAnimationFrame(loop);
+};
+
+loop();
+
+// UI Code
+
+const startAnim = (sliderId, complexComponentName='ci') => {
+ const restart = (interval) => {
+ clearInterval(interval);
+ document.getElementById(sliderId).innerHTML = 'Animate';
+ document.getElementById(sliderId).onclick = ()=>startAnim(sliderId, complexComponentName);
+ return;
+ };
+ const start = setInterval(() => {
+ if (state[complexComponentName] >= C_THRESHOLD) {
+ restart(start);
+ }
+
+ state.changes[complexComponentName] = state[complexComponentName] + 0.001;
+ }, 1000/60);
+ document.getElementById(sliderId).innerHTML = 'Stop';
+ document.getElementById(sliderId).onclick = ()=>restart(start);
+};
+
+document.getElementById('imaginarySlider').oninput = function() {
+ state.changes.ci = parseFloat(this.value) / SLIDER_DIV;
+};
+document.getElementById('realSlider').oninput = function() {
+ state.changes.cr = parseFloat(this.value) / SLIDER_DIV;
+};
+
+document.getElementById('imag-val').addEventListener('change', function (e) {
+ state.changes.ci = parseFloat(this.value);
+});
+document.getElementById('real-val').addEventListener('change', function (e) {
+ state.changes.cr = parseFloat(this.value);
+});
+
+canvasHolder.addEventListener('wheel', (e) => {
+ e.preventDefault();
+ state.changes.zoom = Math.min(Math.max(state.zoom + e.deltaY * 0.001 * state.zoom, MIN_ZOOM), MAX_ZOOM);
+});
+
+let isDown = false;
+canvasHolder.addEventListener('mousedown', (e) => {
+ e.preventDefault();
+ isDown = true;
+}, true);
+
+canvasHolder.addEventListener('mouseup', (e) => {
+ e.preventDefault();
+ isDown = false;
+}, true);
+
+canvasHolder.addEventListener('mousemove', (e) => {
+ e.preventDefault();
+ if (isDown) {
+ let deltaX = -e.movementX * state.zoom / document.body.clientWidth;
+ let deltaY = e.movementY * state.zoom / document.body.clientHeight;
+ state.changes.centerX = state.centerX + deltaX;
+ state.changes.centerY = state.centerY + deltaY;
+ }
+}, true);
+
+window.addEventListener('resize', () => {
+ state.changes.width = document.body.clientWidth;
+ state.changes.height = document.body.clientHeight;
+});
diff --git a/html/toys/tabloid/index.html b/html/toys/tabloid/index.html
new file mode 100644
index 0000000..61ec937
--- /dev/null
+++ b/html/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/html/toys/tabloid/js/main.js b/html/toys/tabloid/js/main.js
new file mode 100644
index 0000000..1f0b5dd
--- /dev/null
+++ b/html/toys/tabloid/js/main.js
@@ -0,0 +1,5 @@
+import { TabloidPlayground } from "./playground.js";
+
+document.addEventListener("DOMContentLoaded", () => {
+ new TabloidPlayground();
+});
diff --git a/html/toys/tabloid/js/playground.js b/html/toys/tabloid/js/playground.js
new file mode 100644
index 0000000..d6ff71a
--- /dev/null
+++ b/html/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/html/toys/tabloid/js/samples.js b/html/toys/tabloid/js/samples.js
new file mode 100644
index 0000000..dc7bd58
--- /dev/null
+++ b/html/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/html/toys/tabloid/js/tabloid.js b/html/toys/tabloid/js/tabloid.js
new file mode 100644
index 0000000..468ee9a
--- /dev/null
+++ b/html/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)}`);
+ }
+ }
+}
diff --git a/html/toys/turing/css/styles.css b/html/toys/turing/css/styles.css
new file mode 100644
index 0000000..2407cb4
--- /dev/null
+++ b/html/toys/turing/css/styles.css
@@ -0,0 +1,66 @@
+.tape {
+ display: flex;
+ gap: 0.25rem;
+ padding: var(--space-md);
+ overflow-x: auto;
+ background: var(--surface-alt);
+ border: var(--border-width) solid var(--border);
+ box-shadow: inset 1px 1px 0 var(--border-light),
+ inset -1px -1px 0 var(--border-dark);
+ min-height: 5rem;
+}
+
+.cell {
+ position: relative;
+ width: 3.25rem;
+ padding: var(--space-xs);
+ background: var(--surface);
+ border: 2px solid var(--border-dark);
+ box-shadow: inset 1px 1px 0 var(--border-light),
+ inset -1px -1px 0 var(--border-dark);
+ flex-shrink: 0;
+}
+
+.cell input {
+ width: 100%;
+ text-align: center;
+ border: none;
+ background: transparent;
+ font-family: var(--font-mono);
+ color: var(--fg);
+ font-size: 0.875rem;
+ padding: 0.25rem;
+}
+
+.cell input:focus {
+ outline: 1px dotted var(--fg);
+ outline-offset: 2px;
+}
+
+.cell.active {
+ border-color: var(--primary);
+ background: color-mix(in srgb, var(--primary) 15%, var(--surface));
+}
+
+.cell.active::after {
+ content: '▲';
+ position: absolute;
+ bottom: -1.5rem;
+ left: 50%;
+ transform: translateX(-50%);
+ color: var(--primary);
+ font-size: 1rem;
+ line-height: 1;
+}
+
+#state-text {
+ font-family: var(--font-mono);
+ font-weight: 700;
+ margin-top: var(--space-md);
+}
+
+.controls {
+ display: flex;
+ gap: var(--space-sm);
+ flex-wrap: wrap;
+}
diff --git a/html/toys/turing/index.html b/html/toys/turing/index.html
new file mode 100644
index 0000000..838c5bf
--- /dev/null
+++ b/html/toys/turing/index.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link rel="stylesheet" href="https://adelie.liz.coffee/bundle.css">
+ <link rel="stylesheet" href="./css/styles.css">
+ <title>Turing Machine</title>
+</head>
+<body>
+ <main>
+ <article>
+ <h3>Turing Machine</h3>
+
+ <p id="state-text">State: _, Step: 0</p>
+
+ <div id="tape" class="tape mt-md"></div>
+
+ <div class="controls mt-sm">
+ <button id="run-btn" class="primary">Run (Ctrl + Enter)</button>
+ <button id="step-btn">Step</button>
+ <button id="reset-btn">Reset</button>
+ <button id="copy-btn">Copy State</button>
+ </div>
+
+ <section class="mt-lg">
+ <div class="mt-sm">
+ <label for="program-select">Example:</label>
+ <select id="program-select"></select>
+ </div>
+
+ <div id="code-editor" class="code-editor-container mt-sm"></div>
+ </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/html/toys/turing/js/machine.js b/html/toys/turing/js/machine.js
new file mode 100644
index 0000000..6af4be6
--- /dev/null
+++ b/html/toys/turing/js/machine.js
@@ -0,0 +1,75 @@
+export class TuringMachine {
+ constructor({
+ tape,
+ rules,
+ startState,
+ acceptStates = [],
+ rejectStates = []
+ }) {
+ this.tape = tape;
+ this.rules = rules;
+ this.state = startState;
+ this.acceptStates = new Set(acceptStates);
+ this.rejectStates = new Set(rejectStates);
+ this.iteration = 0;
+ }
+
+ step() {
+ if (this.isHalted()) {
+ return false;
+ }
+
+ const currentSymbol = this.tape.readHead();
+ const ruleKey = this.getRuleKey(this.state, currentSymbol);
+ if (!this.rules.has(ruleKey)) {
+ return false;
+ }
+
+ const { nextState, writeSymbol, direction } = this.rules.get(ruleKey);
+ this.tape.writeHead(writeSymbol);
+
+ if (direction === "R") {
+ this.tape.moveRight();
+ } else if (direction === "L") {
+ this.tape.moveLeft();
+ }
+
+ this.state = nextState;
+ this.iteration += 1;
+ return !this.isHalted();
+ }
+
+ canStep() {
+ if (this.isHalted()) {
+ return false;
+ }
+
+ const currentSymbol = this.tape.readHead();
+ const ruleKey = this.getRuleKey(this.state, currentSymbol);
+ return this.rules.has(ruleKey);
+ }
+
+ getRuleKey(state, symbol) {
+ return `${state}:${symbol}`;
+ }
+
+ isAccepting() {
+ return this.acceptStates.has(this.state);
+ }
+
+ isRejecting() {
+ return this.rejectStates.has(this.state);
+ }
+
+ isHalted() {
+ return this.isAccepting() || this.isRejecting();
+ }
+
+ getStateStatus() {
+ return `State: ${this.state}, Step: ${this.iteration}`;
+ }
+
+ getState() {
+ return this.state;
+ }
+}
diff --git a/html/toys/turing/js/main.js b/html/toys/turing/js/main.js
new file mode 100644
index 0000000..c9f37fc
--- /dev/null
+++ b/html/toys/turing/js/main.js
@@ -0,0 +1,4 @@
+import { TuringMachineUI } from "./ui.js";
+document.addEventListener("DOMContentLoaded", (event) => {
+ new TuringMachineUI();
+});
diff --git a/html/toys/turing/js/parser.js b/html/toys/turing/js/parser.js
new file mode 100644
index 0000000..dd65613
--- /dev/null
+++ b/html/toys/turing/js/parser.js
@@ -0,0 +1,141 @@
+export function parseInstructionSet(code) {
+ const lines = code.split("\n");
+ const instructions = [];
+ const config = {
+ startState: null,
+ acceptStates: new Set(),
+ rejectStates: new Set()
+ };
+
+ lines.forEach((line, lineIndex) => {
+ const withoutComments = line.replace(/\/\/.*$/, "").trim();
+ if (!withoutComments) {
+ return;
+ }
+
+ if (withoutComments.startsWith("#")) {
+ applyDirective(withoutComments.slice(1).trim(), config, lineIndex + 1);
+ return;
+ }
+
+ const parts = withoutComments.split(/\s+/).filter(Boolean);
+ if (parts.length !== 5) {
+ throw new Error(`Invalid instruction on line ${lineIndex + 1}: expected 5 parts, received ${parts.length}`);
+ }
+
+ const [fromState, readSymbol, writeSymbol, direction, toState] = parts;
+ if (!config.startState) {
+ config.startState = fromState;
+ }
+
+ instructions.push({ fromState, readSymbol, writeSymbol, direction, toState, line: lineIndex + 1 });
+ });
+
+ if (!instructions.length) {
+ throw new Error("No instructions provided");
+ }
+
+ const { acceptStates, rejectStates } = deriveHaltingStates(instructions, config);
+ const rules = buildRuleMap(instructions);
+
+ return {
+ rules,
+ startState: config.startState ?? instructions[0].fromState,
+ acceptStates,
+ rejectStates
+ };
+}
+
+function applyDirective(directiveLine, config, lineNumber) {
+ if (!directiveLine) {
+ return;
+ }
+
+ const [keyword, ...values] = directiveLine.split(/\s+/).filter(Boolean);
+ if (!keyword) {
+ return;
+ }
+
+ switch (keyword.toLowerCase()) {
+ case "start": {
+ if (values.length !== 1) {
+ throw new Error(`#start on line ${lineNumber} must provide exactly one state`);
+ }
+ config.startState = values[0];
+ break;
+ }
+ case "accept":
+ case "accepts":
+ case "accepting": {
+ if (!values.length) {
+ throw new Error(`#${keyword} on line ${lineNumber} must include at least one state`);
+ }
+ values.forEach((value) => config.acceptStates.add(value));
+ break;
+ }
+ case "reject":
+ case "rejects":
+ case "rejecting": {
+ if (!values.length) {
+ throw new Error(`#${keyword} on line ${lineNumber} must include at least one state`);
+ }
+ values.forEach((value) => config.rejectStates.add(value));
+ break;
+ }
+ default:
+ throw new Error(`Unknown directive '#${keyword}' on line ${lineNumber}`);
+ }
+}
+
+function deriveHaltingStates(instructions, config) {
+ const fromStates = new Set();
+ const toStates = new Set();
+ const allStates = new Set();
+
+ instructions.forEach(({ fromState, toState }) => {
+ fromStates.add(fromState);
+ toStates.add(toState);
+ allStates.add(fromState);
+ allStates.add(toState);
+ });
+
+ // Remove any overlap so rejects always win
+ config.rejectStates.forEach((state) => config.acceptStates.delete(state));
+
+ if (!config.acceptStates.size) {
+ for (const state of allStates) {
+ if (!fromStates.has(state) && toStates.has(state) && !config.rejectStates.has(state)) {
+ config.acceptStates.add(state);
+ }
+ }
+ }
+
+ return {
+ acceptStates: Array.from(config.acceptStates),
+ rejectStates: Array.from(config.rejectStates)
+ };
+}
+
+function buildRuleMap(instructions) {
+ const rules = new Map();
+
+ instructions.forEach(({ fromState, readSymbol, writeSymbol, direction, toState, line }) => {
+ const dir = direction.toUpperCase();
+ if (!["L", "R", "S"].includes(dir)) {
+ throw new Error(`Invalid direction '${direction}' on line ${line}. Use L, R, or S.`);
+ }
+
+ const key = `${fromState}:${readSymbol}`;
+ if (rules.has(key)) {
+ throw new Error(`Duplicate rule for state '${fromState}' reading '${readSymbol}' (line ${line})`);
+ }
+
+ rules.set(key, {
+ nextState: toState,
+ writeSymbol,
+ direction: dir
+ });
+ });
+
+ return rules;
+}
diff --git a/html/toys/turing/js/samples.js b/html/toys/turing/js/samples.js
new file mode 100644
index 0000000..0998ee4
--- /dev/null
+++ b/html/toys/turing/js/samples.js
@@ -0,0 +1,88 @@
+// Example programs with initial tape states
+export const EXAMPLE_PROGRAMS = [
+ {
+ name: "Replace two B's",
+ code: `#start q0
+#accept acc
+#reject rej
+
+q0 B 1 R q1
+q1 1 1 R q1
+q1 B 1 R acc`,
+ initialTape: ""
+ },
+ {
+ name: "Binary equality checker",
+ code: `// https://stackoverflow.com/questions/59045832
+
+#start q0
+#accept acc
+#reject rej
+
+q0 0 X R q1
+q0 1 X R q2
+q0 = = R q7
+q1 0 0 R q1
+q1 1 1 R q1
+q1 = = R q3
+q2 0 0 R q2
+q2 1 1 R q2
+q2 = = R q4
+q3 X X R q3
+q3 0 X L q5
+q3 1 1 L rej
+q3 B B L rej
+q4 X X R q4
+q4 0 0 L rej
+q4 B B L rej
+q4 1 X L q5
+q5 X X L q5
+q5 = = L q6
+q6 0 0 L q6
+q6 1 1 L q6
+q6 X X R q0
+q7 X X R q7
+q7 B B L q8
+q7 0 0 L rej
+q7 1 1 L rej
+q8 X X L q8
+q8 0 0 L q8
+q8 1 1 L q8
+q8 = = L acc`,
+ initialTape: "1011=1011"
+ },
+ {
+ name: "Binary addition",
+ code: `// https://stackoverflow.com/questions/59045832
+
+#start q0
+#accept acc
+#reject rej
+
+q0 B B R q0
+q0 0 0 R q0
+q0 1 1 R q0
+q0 + + R q1
+q1 0 0 R q1
+q1 1 1 R q1
+q1 B B L q2
+q2 0 1 L q2
+q2 1 0 L q3
+q2 + + R q5
+q3 0 0 L q3
+q3 1 1 L q3
+q3 + + L q4
+q4 0 1 R q0
+q4 1 0 L q4
+q4 B 1 R q0
+q5 1 B R q5
+q5 B B R q6
+q6 B B L q6
+q6 + B L q7
+q7 0 0 L q7
+q7 1 1 L q7
+q7 B B R acc
+`,
+ initialTape: "101+110"
+ }
+];
diff --git a/html/toys/turing/js/tape.js b/html/toys/turing/js/tape.js
new file mode 100644
index 0000000..fd05366
--- /dev/null
+++ b/html/toys/turing/js/tape.js
@@ -0,0 +1,103 @@
+const ESCAPE_REGEX = /[.*+?^${}()|[\]\\]/g;
+
+function escapeForRegex(value) {
+ return value.replace(ESCAPE_REGEX, "\\$&");
+}
+
+export class Tape {
+ constructor({
+ initialContent = "",
+ blankSymbol = "B",
+ minLength = 50,
+ padding = 40
+ } = {}) {
+ this.blankSymbol = blankSymbol;
+ this.minLength = minLength;
+ this.padding = padding;
+ this.reset(initialContent);
+ }
+
+ reset(initialContent = "") {
+ const targetLength = Math.max(this.minLength, initialContent.length + this.padding);
+ this.cells = Array(targetLength).fill(this.blankSymbol);
+ const startOffset = Math.floor((targetLength - initialContent.length) / 2);
+ for (let i = 0; i < initialContent.length; i++) {
+ this.cells[startOffset + i] = initialContent[i];
+ }
+ this.headIndex = startOffset;
+ }
+
+ get length() {
+ return this.cells.length;
+ }
+
+ getHeadIndex() {
+ return this.headIndex;
+ }
+
+ readHead() {
+ return this.getCell(this.headIndex);
+ }
+
+ writeHead(symbol) {
+ this.cells[this.headIndex] = symbol || this.blankSymbol;
+ }
+
+ readAt(index) {
+ return this.getCell(index);
+ }
+
+ writeAt(index, symbol) {
+ if (index < 0) {
+ throw new Error("Cannot write to a negative tape index");
+ }
+ this.ensureRightCapacity(index);
+ this.cells[index] = symbol || this.blankSymbol;
+ }
+
+ setHead(index) {
+ if (index < 0) {
+ throw new Error("Head index cannot be negative");
+ }
+ this.ensureRightCapacity(index);
+ this.headIndex = index;
+ }
+
+ moveLeft() {
+ if (this.headIndex === 0) {
+ this.cells.unshift(this.blankSymbol);
+ } else {
+ this.headIndex -= 1;
+ return;
+ }
+ }
+
+ moveRight() {
+ this.headIndex += 1;
+ if (this.headIndex >= this.cells.length) {
+ this.cells.push(this.blankSymbol);
+ }
+ }
+
+ getCell(index) {
+ if (index < 0 || index >= this.cells.length) {
+ return this.blankSymbol;
+ }
+ return this.cells[index];
+ }
+
+ ensureRightCapacity(index) {
+ while (index >= this.cells.length) {
+ this.cells.push(this.blankSymbol);
+ }
+ }
+
+ getContents({ trimTrailing = true } = {}) {
+ let snapshot = this.cells.join("");
+ if (trimTrailing) {
+ const regex = new RegExp(`${escapeForRegex(this.blankSymbol)}+$`, "g");
+ snapshot = snapshot.replace(regex, "");
+ }
+ return snapshot;
+ }
+}
diff --git a/html/toys/turing/js/ui.js b/html/toys/turing/js/ui.js
new file mode 100644
index 0000000..ae01a4b
--- /dev/null
+++ b/html/toys/turing/js/ui.js
@@ -0,0 +1,386 @@
+import { Tape } from "./tape.js";
+import { TuringMachine } from "./machine.js";
+import { parseInstructionSet } from "./parser.js";
+import { EXAMPLE_PROGRAMS } from "./samples.js";
+
+const SCROLL_THRESHOLD = 3;
+
+export class TuringMachineUI {
+ constructor() {
+ this.machine = null;
+ this.editor = null;
+ this.intervalId = null;
+ this.isRunning = false;
+ this.initialTapeSize = 50;
+ this.blankSymbol = "B";
+ this.currentProgramIndex = 0;
+ this.loadedFromURL = false;
+ this.urlTapeState = "";
+ this.lastScrollPosition = 0;
+ this.renderedTapeLength = 0;
+ this.simulationInterval = 200;
+
+ this.elements = {
+ tape: document.getElementById("tape"),
+ stateText: document.getElementById("state-text"),
+ runBtn: document.getElementById("run-btn"),
+ stepBtn: document.getElementById("step-btn"),
+ resetBtn: document.getElementById("reset-btn"),
+ copyBtn: document.getElementById("copy-btn"),
+ programSelect: document.getElementById("program-select")
+ };
+
+ this.init();
+ }
+
+ init() {
+ this.setupEditor();
+ this.populateProgramSelect();
+ this.setupEventListeners();
+ this.loadFromURL();
+ }
+
+ setupEditor() {
+ this.editor = adelieEditor.init("#code-editor", {
+ language: "javascript"
+ });
+
+ this.editor.dom.addEventListener("input", () => {
+ this.machine = null;
+ });
+ }
+
+ populateProgramSelect() {
+ if (!this.elements.programSelect) {
+ return;
+ }
+
+ this.elements.programSelect.innerHTML = "";
+ EXAMPLE_PROGRAMS.forEach((program, index) => {
+ const option = document.createElement("option");
+ option.value = index.toString();
+ option.textContent = program.name;
+ this.elements.programSelect.appendChild(option);
+ });
+ }
+
+ setupEventListeners() {
+ this.elements.runBtn.addEventListener("click", () => this.toggleRun());
+ this.elements.stepBtn.addEventListener("click", () => this.step());
+ this.elements.resetBtn.addEventListener("click", () => this.reset());
+ this.elements.copyBtn.addEventListener("click", () => this.copyState());
+
+ this.elements.programSelect.addEventListener("change", (event) => {
+ this.loadProgram(parseInt(event.target.value, 10));
+ });
+
+ document.addEventListener("keydown", (event) => {
+ if (event.ctrlKey && event.key === "Enter") {
+ event.preventDefault();
+ this.reset();
+ setTimeout(() => this.run(), 0);
+ }
+ });
+
+ this.elements.tape.addEventListener("input", (event) => this.handleTapeInput(event));
+ this.elements.tape.addEventListener("focusin", () => {
+ if (this.isRunning) {
+ this.pause();
+ }
+ });
+ }
+
+ handleTapeInput(event) {
+ if (!this.machine) {
+ return;
+ }
+ const target = event.target;
+ if (!(target instanceof HTMLInputElement)) {
+ return;
+ }
+
+ const parentCell = target.closest(".cell");
+ if (!parentCell) {
+ return;
+ }
+
+ const index = Number(parentCell.dataset.index);
+ if (Number.isNaN(index)) {
+ return;
+ }
+
+ const sanitized = (target.value || this.blankSymbol).slice(0, 1);
+ target.value = sanitized;
+ this.machine.tape.writeAt(index, sanitized || this.blankSymbol);
+ }
+
+ loadFromURL() {
+ const urlParams = new URLSearchParams(window.location.search);
+ const startState = urlParams.get("start") ?? "";
+ const instructions = urlParams.get("instructions");
+
+ if (!instructions) {
+ this.loadProgram(0);
+ return;
+ }
+
+ try {
+ const code = atob(instructions);
+ this.setEditorContent(code);
+ this.loadedFromURL = true;
+ this.urlTapeState = startState;
+ this.compile(this.urlTapeState);
+ } catch (error) {
+ console.error("Failed to load from URL", error);
+ this.loadedFromURL = false;
+ this.loadProgram(0);
+ }
+ }
+
+ setEditorContent(content) {
+ const length = this.editor.state.doc.toString().length;
+ this.editor.dispatch({
+ changes: { from: 0, to: length, insert: content }
+ });
+ }
+
+ getEditorContent() {
+ return this.editor.state.doc.toString();
+ }
+
+ loadProgram(index = 0) {
+ const program = EXAMPLE_PROGRAMS[index];
+ if (!program) {
+ return;
+ }
+
+ this.loadedFromURL = false;
+ this.currentProgramIndex = index;
+ this.elements.programSelect.value = index.toString();
+ this.setEditorContent(program.code);
+
+ try {
+ this.compile(program.initialTape);
+ } catch (error) {
+ this.elements.stateText.innerHTML = `<span class="text-error">Error: ${error.message}</span>`;
+ }
+ }
+
+ compile(initialTape = "") {
+ this.pause();
+
+ const code = this.getEditorContent();
+ const instructionSet = parseInstructionSet(code);
+ const tapeSeed = initialTape || EXAMPLE_PROGRAMS[this.currentProgramIndex]?.initialTape || "";
+
+ const tape = new Tape({
+ initialContent: tapeSeed,
+ blankSymbol: this.blankSymbol,
+ minLength: Math.max(this.initialTapeSize, tapeSeed.length + 40)
+ });
+
+ this.machine = new TuringMachine({
+ tape,
+ rules: instructionSet.rules,
+ startState: instructionSet.startState,
+ acceptStates: instructionSet.acceptStates,
+ rejectStates: instructionSet.rejectStates
+ });
+
+ this.renderedTapeLength = 0;
+ this.lastScrollPosition = tape.getHeadIndex();
+ this.updateStateDisplay();
+ this.updateTape(true);
+ }
+
+ reset() {
+ this.pause();
+
+ if (this.loadedFromURL) {
+ try {
+ this.compile(this.urlTapeState);
+ return;
+ } catch (error) {
+ this.elements.stateText.innerHTML = `<span class="text-error">Error: ${error.message}</span>`;
+ }
+ }
+
+ this.loadProgram(this.currentProgramIndex);
+ }
+
+ step() {
+ if (!this.machine) {
+ try {
+ this.compile();
+ } catch (error) {
+ this.elements.stateText.innerHTML = `<span class="text-error">Error: ${error.message}</span>`;
+ return;
+ }
+ }
+
+ const canContinue = this.machine.step();
+ this.updateStateDisplay();
+ this.updateTape();
+
+ if (!canContinue) {
+ this.pause();
+ }
+ }
+
+ toggleRun() {
+ if (this.isRunning) {
+ this.pause();
+ } else {
+ this.run();
+ }
+ }
+
+ run() {
+ if (this.isRunning) {
+ return;
+ }
+
+ if (!this.machine) {
+ try {
+ this.compile();
+ } catch (error) {
+ this.elements.stateText.innerHTML = `<span class="text-error">Error: ${error.message}</span>`;
+ return;
+ }
+ }
+
+ this.isRunning = true;
+ this.elements.runBtn.textContent = "⏸ Pause";
+ this.elements.runBtn.classList.remove("primary");
+
+ this.intervalId = setInterval(() => {
+ const canContinue = this.machine.step();
+ this.updateStateDisplay();
+ this.updateTape();
+ if (!canContinue) {
+ this.pause();
+ }
+ }, this.simulationInterval);
+ }
+
+ pause() {
+ if (!this.isRunning) {
+ return;
+ }
+
+ this.isRunning = false;
+ clearInterval(this.intervalId);
+ this.intervalId = null;
+
+ this.elements.runBtn.textContent = "▶ Run (Ctrl + Enter)";
+ this.elements.runBtn.classList.add("primary");
+ }
+
+ updateStateDisplay() {
+ if (!this.machine) {
+ this.elements.stateText.textContent = "State: _, Step: 0";
+ return;
+ }
+
+ const status = this.machine.getStateStatus();
+
+ if (!this.machine.canStep()) {
+ if (this.machine.isAccepting()) {
+ this.elements.stateText.innerHTML = `<span class="text-success">Accept(${status})</span>`;
+ } else if (this.machine.isRejecting()) {
+ this.elements.stateText.innerHTML = `<span class="text-error">Reject(${status})</span>`;
+ } else {
+ this.elements.stateText.innerHTML = `<span class="text-error">Halt(${status})</span>`;
+ }
+ return;
+ }
+
+ this.elements.stateText.textContent = status;
+ }
+
+ updateTape(forceRender = false) {
+ if (!this.machine) {
+ return;
+ }
+
+ const tape = this.machine.tape;
+ if (forceRender || this.renderedTapeLength !== tape.length) {
+ this.renderTape();
+ }
+
+ const cells = this.elements.tape.querySelectorAll(".cell");
+ const headIndex = tape.getHeadIndex();
+
+ cells.forEach((cell, index) => {
+ cell.dataset.index = index.toString();
+ const input = cell.querySelector("input");
+ const value = tape.getCell(index);
+ if (input.value !== value) {
+ input.value = value;
+ }
+
+ if (index === headIndex) {
+ cell.classList.add("active");
+ this.maybeScrollIntoView(cell, headIndex, forceRender);
+ } else {
+ cell.classList.remove("active");
+ }
+ });
+ }
+
+ renderTape() {
+ const fragment = document.createDocumentFragment();
+ const tape = this.machine.tape;
+
+ for (let i = 0; i < tape.length; i++) {
+ fragment.appendChild(this.createCell(i, tape.getCell(i)));
+ }
+
+ this.elements.tape.innerHTML = "";
+ this.elements.tape.appendChild(fragment);
+ this.renderedTapeLength = tape.length;
+ }
+
+ createCell(index, value) {
+ const cell = document.createElement("div");
+ cell.classList.add("cell");
+ cell.dataset.index = index.toString();
+
+ const input = document.createElement("input");
+ input.type = "text";
+ input.maxLength = 1;
+ input.value = value;
+
+ cell.appendChild(input);
+ return cell;
+ }
+
+ maybeScrollIntoView(cell, headIndex, forceImmediate = false) {
+ if (forceImmediate) {
+ cell.scrollIntoView({ behavior: "auto", block: "nearest", inline: "center" });
+ this.lastScrollPosition = headIndex;
+ return;
+ }
+
+ if (Math.abs(headIndex - this.lastScrollPosition) < SCROLL_THRESHOLD) {
+ return;
+ }
+
+ cell.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
+ this.lastScrollPosition = headIndex;
+ }
+
+ copyState() {
+ if (!this.machine) {
+ return;
+ }
+
+ const tapeState = this.machine.tape.getContents();
+ const instructions = btoa(this.getEditorContent());
+ const url = `${window.location.href.split("?")[0]}?start=${tapeState}&instructions=${instructions}`;
+
+ navigator.clipboard.writeText(url)
+ .then(() => alert("State copied to clipboard!"))
+ .catch(() => alert("Failed to copy to clipboard"));
+ }
+}