summaryrefslogtreecommitdiff
path: root/src/toys/godel/js/ui.js
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2026-01-07 19:29:30 -0800
committerElizabeth Hunt <me@liz.coffee>2026-01-07 19:29:30 -0800
commit91b7598b22f89319f64054daf42c950de3eb6451 (patch)
treeb337ad01c75e7ee88f287eda05522e72dd9a8dd5 /src/toys/godel/js/ui.js
parent49012297ea792a69501b74d8d83bd4be44d177da (diff)
downloadlizdotcoffee-91b7598b22f89319f64054daf42c950de3eb6451.tar.gz
lizdotcoffee-91b7598b22f89319f64054daf42c950de3eb6451.zip
Adding some of my favorite toys
Diffstat (limited to 'src/toys/godel/js/ui.js')
-rw-r--r--src/toys/godel/js/ui.js228
1 files changed, 228 insertions, 0 deletions
diff --git a/src/toys/godel/js/ui.js b/src/toys/godel/js/ui.js
new file mode 100644
index 0000000..2952140
--- /dev/null
+++ b/src/toys/godel/js/ui.js
@@ -0,0 +1,228 @@
+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",
+ `Compiled ${godelSequence.length} instruction${godelSequence.length === 1 ? "" : "s"}.`,
+ "success"
+ );
+ this.elements.computeNumberBtn.disabled = godelSequence.length === 0;
+ } catch (error) {
+ this.latestSequence = [];
+ this.renderSequence([]);
+ this.elements.computeNumberBtn.disabled = true;
+ this.setStatus("compile", error.message || "Failed to compile program", "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] || ""}`;
+ }
+}