summaryrefslogtreecommitdiff
path: root/html
diff options
context:
space:
mode:
Diffstat (limited to 'html')
-rw-r--r--html/assets/bellevue.jpegbin0 -> 377844 bytes
-rw-r--r--html/assets/bread.jpegbin0 -> 206615 bytes
-rw-r--r--html/assets/critter.jpegbin0 -> 181598 bytes
-rw-r--r--html/assets/euler.pngbin0 -> 297917 bytes
-rw-r--r--html/assets/fourier.pngbin0 -> 461309 bytes
-rw-r--r--html/assets/godel.pngbin0 -> 466399 bytes
-rw-r--r--html/assets/gus.jpegbin0 -> 148667 bytes
-rw-r--r--html/assets/julia.pngbin0 -> 263809 bytes
-rw-r--r--html/assets/lambda.pngbin0 -> 179682 bytes
-rw-r--r--html/assets/penguin.gifbin0 -> 53409 bytes
-rw-r--r--html/assets/petting_basil.mp4bin0 -> 1816422 bytes
-rw-r--r--html/assets/scooters_on_ceiling.jpegbin0 -> 493630 bytes
-rw-r--r--html/assets/tabloid.pngbin0 -> 254486 bytes
-rw-r--r--html/assets/turing.pngbin0 -> 133163 bytes
-rw-r--r--html/index.html216
-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
47 files changed, 5296 insertions, 0 deletions
diff --git a/html/assets/bellevue.jpeg b/html/assets/bellevue.jpeg
new file mode 100644
index 0000000..0c68197
--- /dev/null
+++ b/html/assets/bellevue.jpeg
Binary files differ
diff --git a/html/assets/bread.jpeg b/html/assets/bread.jpeg
new file mode 100644
index 0000000..e9f4021
--- /dev/null
+++ b/html/assets/bread.jpeg
Binary files differ
diff --git a/html/assets/critter.jpeg b/html/assets/critter.jpeg
new file mode 100644
index 0000000..168dde4
--- /dev/null
+++ b/html/assets/critter.jpeg
Binary files differ
diff --git a/html/assets/euler.png b/html/assets/euler.png
new file mode 100644
index 0000000..9ae56f3
--- /dev/null
+++ b/html/assets/euler.png
Binary files differ
diff --git a/html/assets/fourier.png b/html/assets/fourier.png
new file mode 100644
index 0000000..0e7f221
--- /dev/null
+++ b/html/assets/fourier.png
Binary files differ
diff --git a/html/assets/godel.png b/html/assets/godel.png
new file mode 100644
index 0000000..22c3f45
--- /dev/null
+++ b/html/assets/godel.png
Binary files differ
diff --git a/html/assets/gus.jpeg b/html/assets/gus.jpeg
new file mode 100644
index 0000000..c75d559
--- /dev/null
+++ b/html/assets/gus.jpeg
Binary files differ
diff --git a/html/assets/julia.png b/html/assets/julia.png
new file mode 100644
index 0000000..1d259b5
--- /dev/null
+++ b/html/assets/julia.png
Binary files differ
diff --git a/html/assets/lambda.png b/html/assets/lambda.png
new file mode 100644
index 0000000..4f07e8c
--- /dev/null
+++ b/html/assets/lambda.png
Binary files differ
diff --git a/html/assets/penguin.gif b/html/assets/penguin.gif
new file mode 100644
index 0000000..48bd0e2
--- /dev/null
+++ b/html/assets/penguin.gif
Binary files differ
diff --git a/html/assets/petting_basil.mp4 b/html/assets/petting_basil.mp4
new file mode 100644
index 0000000..0ed34ba
--- /dev/null
+++ b/html/assets/petting_basil.mp4
Binary files differ
diff --git a/html/assets/scooters_on_ceiling.jpeg b/html/assets/scooters_on_ceiling.jpeg
new file mode 100644
index 0000000..2b1e856
--- /dev/null
+++ b/html/assets/scooters_on_ceiling.jpeg
Binary files differ
diff --git a/html/assets/tabloid.png b/html/assets/tabloid.png
new file mode 100644
index 0000000..5fd489a
--- /dev/null
+++ b/html/assets/tabloid.png
Binary files differ
diff --git a/html/assets/turing.png b/html/assets/turing.png
new file mode 100644
index 0000000..51ae5a5
--- /dev/null
+++ b/html/assets/turing.png
Binary files differ
diff --git a/html/index.html b/html/index.html
new file mode 100644
index 0000000..6199372
--- /dev/null
+++ b/html/index.html
@@ -0,0 +1,216 @@
+<!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" />
+<title>liz.coffee</title>
+<style>
+ @font-face {
+ font-family: 'Latin Modern';
+ font-style: normal; font-weight: normal; font-display: swap;
+ src: url('https://cdn.jsdelivr.net/gh/vincentdoerig/latex-css@1.10.0/fonts/LM-regular.woff2') format('woff2');
+ }
+ @font-face {
+ font-family: 'Latin Modern';
+ font-style: italic; font-weight: normal; font-display: swap;
+ src: url('https://cdn.jsdelivr.net/gh/vincentdoerig/latex-css@1.10.0/fonts/LM-italic.woff2') format('woff2');
+ }
+ @font-face {
+ font-family: 'Latin Modern';
+ font-style: normal; font-weight: bold; font-display: swap;
+ src: url('https://cdn.jsdelivr.net/gh/vincentdoerig/latex-css@1.10.0/fonts/LM-bold.woff2') format('woff2');
+ }
+ @font-face {
+ font-family: 'Latin Modern';
+ font-style: italic; font-weight: bold; font-display: swap;
+ src: url('https://cdn.jsdelivr.net/gh/vincentdoerig/latex-css@1.10.0/fonts/LM-bold-italic.woff2') format('woff2');
+ }
+
+ :root {
+ --ink: #1a1a1a;
+ --link: #003399;
+ --stain: 150, 62, 22;
+ }
+
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+
+ html { background: #54575c; }
+
+ body {
+ font-family: 'Latin Modern', 'Computer Modern Serif', 'CMU Serif', Georgia, serif;
+ color: var(--ink);
+ font-size: 15.5px;
+ line-height: 1.45;
+ padding: 2.5rem 0.75rem 4rem;
+ }
+
+ .page {
+ position: relative;
+ max-width: 51rem;
+ margin: 0 auto;
+ background: #fffdf7;
+ box-shadow: 0 0 4px rgba(0,0,0,.45), 0 10px 30px rgba(0,0,0,.35);
+ padding: 4.5rem 5.5rem 3.5rem;
+ overflow: hidden;
+ }
+ @media (max-width: 700px) {
+ .page { padding: 3rem 1.4rem 2.5rem; }
+ .arxiv { display: none; }
+ }
+
+ .arxiv {
+ position: absolute;
+ left: 1.1rem; top: 24rem;
+ transform: rotate(180deg);
+ writing-mode: vertical-rl;
+ font-family: 'Latin Modern', Georgia, serif;
+ font-size: .95rem;
+ letter-spacing: .02em;
+ color: #333;
+ }
+
+ .stain { position: absolute; border-radius: 50%; pointer-events: none; }
+ .stain.ring {
+ border: 9px solid rgba(var(--stain), .13);
+ box-shadow:
+ inset 0 0 6px 2px rgba(var(--stain), .10),
+ 0 0 5px 1px rgba(var(--stain), .07),
+ inset 8px 4px 12px rgba(var(--stain), .08);
+ filter: blur(.4px);
+ }
+ .stain.blotch {
+ background: radial-gradient(ellipse at 40% 40%,
+ rgba(var(--stain), .16) 0%,
+ rgba(var(--stain), .08) 45%,
+ rgba(var(--stain), .13) 62%,
+ rgba(var(--stain), 0) 72%);
+ filter: blur(.6px);
+ }
+ .stain.drop { background: rgba(var(--stain), .11); filter: blur(.5px); }
+
+ #stain1 { width: 190px; height: 178px; top: -48px; right: -55px; transform: rotate(12deg); }
+ #stain1b { width: 145px; height: 140px; top: -22px; right: -30px; }
+ #stain2 { width: 120px; height: 112px; top: 44rem; left: -58px; transform: rotate(-9deg); border-width: 12px; }
+ #stain3 { width: 16px; height: 13px; top: 46.5rem; left: 70px; }
+ #stain3b { width: 8px; height: 8px; top: 45.5rem; left: 92px; }
+ #stain4 { width: 230px; height: 215px; bottom: 6rem; right: -80px; transform: rotate(31deg); }
+ #stain5 { width: 11px; height: 10px; bottom: 16rem; right: 130px; }
+
+ .center { text-align: center; }
+ h1 {
+ font-size: 1.55rem; font-weight: normal;
+ text-align: center;
+ margin: 1.2rem auto 1.6rem;
+ max-width: 36rem;
+ line-height: 1.3;
+ }
+ .authorblock { text-align: center; margin-bottom: .4rem; }
+ .authorblock .author { font-size: 1.1rem; }
+ .authorblock .affil { font-style: italic; font-size: .95rem; margin-top: .15rem; }
+ .date { text-align: center; font-size: .95rem; margin: .8rem 0 1.6rem; }
+ tt, .tt { font-family: 'CMU Typewriter Text', ui-monospace, 'Cascadia Mono', Menlo, monospace; font-size: .88em; }
+
+ .navline { text-align: center; margin-bottom: 2.2rem; }
+ .navline a { margin: 0 .35rem; }
+
+ a { color: var(--link); text-decoration: none; }
+ a:hover { text-decoration: underline; }
+
+ .abstract { max-width: 38rem; margin: 0 auto 2rem; }
+ .abstract .head { text-align: center; font-weight: bold; font-size: .95rem; margin-bottom: .5rem; }
+ .abstract p { font-size: .92rem; text-align: justify; hyphens: auto; }
+
+ .epigraph {
+ max-width: 22rem; margin: 0 0 2rem auto;
+ font-style: italic; font-size: .92rem; text-align: right;
+ }
+ .epigraph .src { font-style: normal; font-size: .88rem; }
+
+ h2 {
+ font-size: 1.15rem; font-weight: bold;
+ margin: 1.9rem 0 .8rem;
+ }
+ h2 .no { margin-right: 1em; }
+
+ p { text-align: justify; hyphens: auto; margin-bottom: .75rem; }
+ p.indent { text-indent: 1.6em; margin-bottom: 0; }
+
+</style>
+</head>
+<body>
+
+<div class="page">
+ <div class="stain blotch" id="stain1"></div>
+ <div class="stain ring" id="stain1b"></div>
+ <div class="stain ring" id="stain2"></div>
+ <div class="stain drop" id="stain3"></div>
+ <div class="stain drop" id="stain3b"></div>
+ <div class="stain ring" id="stain4"></div>
+ <div class="stain drop" id="stain5"></div>
+
+ <div class="arxiv">arXiv:2607.0404v3&nbsp;&nbsp;[cs.DOG]&nbsp;&nbsp;<span id="arxiv-date">5 Jul 2026</span></div>
+
+ <div class="authorblock">
+ <div class="author">Elizabeth / Alexander Hunt (she/he)</div>
+ <div class="affil">Department of Penguins</div>
+ <div><a class="tt" href="mailto:me@liz.coffee">me@liz.coffee</a></div>
+ </div>
+ <div class="date" id="today">July 4, 2026</div>
+
+ <div class="navline tt">
+ [<a href="https://code.liz.coffee">code</a>] ·
+ [<a href="https://linkedin.com/in/emprespresso">linkedin</a>] ·
+ [<a href="https://instagram.com/emprespresso">instagram</a>] ·
+ [<a href="https://x.com/emprespresso">twitter</a>]
+ </div>
+
+ <div class="abstract">
+ <div class="head">Abstract</div>
+ <p>We introduce the author and establish some of her fundamental properties. If it's a proof you're looking for, well, the sourdough did that.</p>
+ <div class="center"><img src="assets/bread.jpeg" width="400"><br><em>Fig. 1 - The bread, proven. Also, baked.</em></div>
+ </div>
+
+ <h2>1. Introduction</h2>
+ <p>
+ The author is a programmer at a Big Ol' Tech™ company in Seattle. She was previously at USU studying CS and Math, thankfully escaping from BYU-I at 16.
+ </p>
+ <p class="indent">She enjoys recreational programming and studying outside of work; some of her interests are in functional programming, compilers/PLs, game programming, overengineering her homelab, and low level fun. She hacks on some LISP, drives Emacs, and is generally fond of free software (from 2021 to 2023 she was the president of the <a href="https://linux.usu.edu">Free Software and Linux Club</a>).</p>
+ <p class="indent">
+ When not computing, she runs, bikes, watches anime, rides trains, bakes bread, daydreams about going back to school. On rare occassions when nostalgic, she is heard practicing violin. She is overly fascinated by penguins and visits them when possible.
+ </p>
+ <p class="indent">
+ The author is also the advisor to PhD (PHuckin-cuteass Dog) candidate Gus, whose current work involves proving something about squirrels.
+ <br><br>
+ <div class="center"><img src="assets/gus.jpeg" height="400"><br><em>Fig. 2 - PhD Candidate.</em></div>
+ </p>
+
+ <!--
+ <h2>2. Webtoys</h2>
+ <p>Some hacky little toys left around.</p>
+
+ <ul style="margin-left: 2rem">
+ <li><a href="/toys/tabloid">Tabloid IDE (now including closures!)</a></li>
+ <li><a href="/toys/turing">Turing Machine</a></li>
+ <li><a href="https://the-abstraction-engine.pages.dev">The Abstraction Engine</a></li>
+ <li><a href="/toys/godel">Godel Number Explorer</a></li>
+ <li><a href="/toys/julia">Julia Set Viewer (gpu.js accelerated)</a></li>
+ <li><a href="/toys/fourier">Discrete Fourier Transform Playground</a></li>
+ </ul>
+ -->
+ <footer style="display: flex; align-items: center; justify-content: center;">
+ <img width="100" src="/assets/penguin.gif">
+ <a href="https://liz.coffee"><img src="https://adelie.liz.coffee/img/blinkies/liz--coffee.gif" width="150" height="20"></a>
+ </footer>
+</div>
+
+<script>
+ const d = new Date();
+ const long = d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
+ document.getElementById('today').textContent = long;
+ document.getElementById('arxiv-date').textContent =
+ d.getDate() + ' ' + d.toLocaleDateString('en-US', { month: 'short' }) + ' ' + d.getFullYear();
+</script>
+
+</body>
+</html>
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"));
+ }
+}