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