import { getById, h, writeError } from "/common.module.js"; import lzString from "/lz-string.module.js"; let definitions = {}; const form = getById("form"); const input = getById("input"); const log = getById("log"); form.onsubmit = async (event) => { event.preventDefault(); await submitForm(); }; const urlParams = new URLSearchParams(window.location.search); const queryInput = urlParams.get("in"); if (input.value.length === 0) { input.value = lzString.decompressFromEncodedURIComponent(queryInput); } async function submitForm() { console.log(input.value); resetLog(); try { definitions = {}; const stack = await evalString(input.value); console.log(stack); const path = window.location.pathname; const params = new URLSearchParams(window.location.search); const hash = window.location.hash; params.set("in", lzString.compressToEncodedURIComponent(input.value)); window.history.replaceState( {}, "", `${path}?${params.toString()}${hash}`, ); } catch (error) { writeError(error); console.log(error); } } function splitWords(input) { return input .trim() .split(/\s+/) .filter((i) => i); } /** @param {string} inputString */ function evalString(inputString) { let words = splitWords(inputString); return evalWords(words); } function effect2(f) { return (stack) => { if (stack.length <= 1) throw "stack underflow, need 2 values"; let [x, y, ...rest] = stack; return [f(y, x), ...rest]; }; } const plus = effect2((a, b) => a + b); const subtract = effect2((a, b) => a - b); const multiply = effect2((a, b) => a * b); const divide = effect2((a, b) => a / b); const equal = effect2((a, b) => a === b); const notEqual = effect2((a, b) => a !== b); const lessThan = effect2((a, b) => a < b); const greaterThan = effect2((a, b) => a > b); const lessOrEqual = effect2((a, b) => a <= b); const greaterOrEqual = effect2((a, b) => a >= b); /* Booleans in, booleans out: anything is a flag to 'if', but these words only ever leave a real one. */ const and = effect2((a, b) => Boolean(a) && Boolean(b)); const or = effect2((a, b) => Boolean(a) || Boolean(b)); async function evalWords(inputWords) { let words = inputWords; let stack = []; while (words.length > 0) { await writeLog(stack, words); if (words.length === 0) return stack; let [word, ...rest] = words; [stack, words] = evalWord(word, stack, rest); await new Promise((r) => setTimeout(r, 100)); } await writeLog(stack, words); return stack; } function evalWord(word, stack, rest) { switch (word) { case "+": return [plus(stack), rest]; case "-": return [subtract(stack), rest]; case "*": return [multiply(stack), rest]; case "/": return [divide(stack), rest]; case "=": return [equal(stack), rest]; case "<>": return [notEqual(stack), rest]; case "<": return [lessThan(stack), rest]; case ">": return [greaterThan(stack), rest]; case "<=": return [lessOrEqual(stack), rest]; case ">=": return [greaterOrEqual(stack), rest]; case "and": return [and(stack), rest]; case "or": return [or(stack), rest]; case "invert": return [invert(stack), rest]; case "dup": return [dup(stack), rest]; case "drop": return [drop(stack), rest]; case "swap": return [swap(stack), rest]; case "if": return branch(stack, rest); case "then": throw "'then' without a matching 'if'"; case ",,": return unquote(stack, rest); case ":": return [stack, define(rest)]; default: return parse(word, stack, rest); } } function unquote(stack, rest) { let [quote, ...restStack] = stack; if (typeof quote === "string" || quote instanceof String) { return [restStack, [...splitWords(quote), ...rest]]; } else { throw "not a string, only strings are unquoteable"; } } function branch(stack, words) { if (stack.length === 0) throw "stack underflow, nothing to test"; let [condition, ...restStack] = stack; let index = findThen(words); // The branch is taken: run the words in between, but drop the 'then' that // closes them, it has nothing left to do but clutter the log. if (condition) { let restWords = [...words.slice(0, index), ...words.slice(index + 1)]; return [restStack, restWords]; } return [restStack, words.slice(index + 1)]; } /** The index of the 'then' closing an 'if', skipping over any nested ones. */ function findThen(words) { let depth = 1; for (let index = 0; index < words.length; index++) { const word = words[index]; if (word === "if") { depth++; } else if (word === "then") { depth--; if (depth === 0) return index; } } throw "expected 'then', found end of program"; } function define(words) { if (words.length < 2) throw "missing definition after ':'"; let [ident, ...rest] = words; let index = 0; let definition = []; for (; index < rest.length; index++) { const word = rest[index]; if (word === ";") { break; } else if (rest.length - 1 === index) { throw "expected ';', found end of program"; } definition.push(word); } definitions[ident] = definition; return rest.slice(index + 1); } function dup(stack) { if (stack.length === 0) throw "stack underflow, nothing to duplicate"; let [x, ...rest] = stack; return [x, x, ...rest]; } function drop(stack) { if (stack.length === 0) throw "stack underflow, nothing to drop"; let [_, ...rest] = stack; return rest; } function swap(stack) { if (stack.length < 2) throw "stack underflow, not enough to swap"; let [x, y, ...rest] = stack; return [y, x, ...rest]; } function invert(stack) { if (stack.length === 0) throw "stack underflow, nothing to invert"; let [x, ...rest] = stack; return [!x, ...rest]; } function parse(word, stack, rest) { if (word.startsWith('"')) { return parseString(word, stack, rest); } if (word in definitions) { return [stack, [...definitions[word], ...rest]]; } if (word === "true" || word === "false") { return [[word === "true", ...stack], rest]; } let num = Number(word); if (isNaN(num)) { throw `word '${word}' not recognised`; } return [[num, ...stack], rest]; } function parseString(word, stack, rest) { if (word.length > 1 && word.endsWith('"')) { return [[word.slice(1, -1), ...stack], rest]; } let string = word.slice(1); let index = 0; for (; index < rest.length; index++) { const word = rest[index]; if (word.endsWith('"')) { string = string.concat(" ", word.slice(0, -1)); break; } else if (rest.length - 1 === index) { throw "expected word ending with '\"', found end of program"; } string = string.concat(" ", word); } return [[string, ...stack], rest.slice(index + 1)]; } function writeLog(stack, words) { return new Promise((resolve, _reject) => { let log_left = h("span", `[${stack.join(", ")}]`); let log_right = h("span", words.join(" ")); if (words.length === 0) { log_left.textContent += " <=="; } let log_row = h("div", {class: "stack-step"}, [log_left, log_right]); log.appendChild(log_row); resolve(); }); } function resetLog() { if (!log.hasChildNodes()) return; let summary = h("summary", log.firstChild.lastChild.textContent); let old_log = log.cloneNode(true); old_log.id = ""; let details = h("details", {class: "history"}, [summary, old_log]); log.insertAdjacentElement("afterend", details); log.replaceChildren(); //remove children, clear log }