Blog/Components/Pages/Concat.razor.js 8.7 K · 323 lines · raw · history

1 import { getById, h, writeError } from "/common.module.js";
2 import lzString from "/lz-string.module.js";
3
4 let definitions = {};
5
6 const form = getById("form");
7 const input = getById("input");
8 const instant = getById("instant");
9 const log = getById("log");
10
11 form.onsubmit = async (event) => {
12 event.preventDefault();
13 await submitForm();
14 };
15
16 const urlParams = new URLSearchParams(window.location.search);
17 const queryInput = urlParams.get("in");
18
19 if (input.value.length === 0) {
20 input.value = lzString.decompressFromEncodedURIComponent(queryInput);
21 }
22
23 async function submitForm() {
24 console.log(input.value);
25 resetLog();
26 try {
27 definitions = {};
28 const stack = await evalString(input.value);
29 console.log(stack);
30
31 const path = window.location.pathname;
32 const params = new URLSearchParams(window.location.search);
33 const hash = window.location.hash;
34
35 params.set("in", lzString.compressToEncodedURIComponent(input.value));
36 window.history.replaceState(
37 {},
38 "",
39 `${path}?${params.toString()}${hash}`,
40 );
41 } catch (error) {
42 writeError(error);
43 console.log(error);
44 } finally {
45 if (instant.checked) foldSteps();
46 }
47 }
48
49 /**
50 * A run with no pauses between the steps arrives all at once, so everything
51 * between the program and what it ended on is folded away.
52 */
53 function foldSteps() {
54 let rows = [...log.children];
55 if (rows.length <= 2) return;
56
57 let steps = rows.slice(1, -1);
58 let label = steps.length === 1 ? "1 step" : `${steps.length} steps`;
59 let fold = h("details", {class: "steps"}, [h("summary", label), ...steps]);
60
61 log.insertBefore(fold, rows.at(-1));
62 }
63
64 function splitWords(input) {
65 return input
66 .trim()
67 .split(/\s+/)
68 .filter((i) => i);
69 }
70
71 /** @param {string} inputString */
72 function evalString(inputString) {
73 let words = splitWords(inputString);
74 return evalWords(words);
75 }
76
77 function effect2(f) {
78 return (stack) => {
79 if (stack.length <= 1) throw "stack underflow, need 2 values";
80 let [x, y, ...rest] = stack;
81 return [f(y, x), ...rest];
82 };
83 }
84
85 const plus = effect2((a, b) => a + b);
86 const subtract = effect2((a, b) => a - b);
87 const multiply = effect2((a, b) => a * b);
88 const divide = effect2((a, b) => a / b);
89
90 const equal = effect2((a, b) => a === b);
91 const notEqual = effect2((a, b) => a !== b);
92 const lessThan = effect2((a, b) => a < b);
93 const greaterThan = effect2((a, b) => a > b);
94 const lessOrEqual = effect2((a, b) => a <= b);
95 const greaterOrEqual = effect2((a, b) => a >= b);
96
97 /* Booleans in, booleans out: anything is a flag to 'if', but these words only
98 ever leave a real one. */
99 const and = effect2((a, b) => Boolean(a) && Boolean(b));
100 const or = effect2((a, b) => Boolean(a) || Boolean(b));
101
102 async function evalWords(inputWords) {
103 let words = inputWords;
104 let stack = [];
105
106 while (words.length > 0) {
107 await writeLog(stack, words);
108 if (words.length === 0) return stack;
109
110 let [word, ...rest] = words;
111 [stack, words] = evalWord(word, stack, rest);
112
113 if (!instant.checked) await new Promise((r) => setTimeout(r, 100));
114 }
115 await writeLog(stack, words);
116 return stack;
117 }
118
119 function evalWord(word, stack, rest) {
120 switch (word) {
121 case "+":
122 return [plus(stack), rest];
123 case "-":
124 return [subtract(stack), rest];
125 case "*":
126 return [multiply(stack), rest];
127 case "/":
128 return [divide(stack), rest];
129 case "=":
130 return [equal(stack), rest];
131 case "<>":
132 return [notEqual(stack), rest];
133 case "<":
134 return [lessThan(stack), rest];
135 case ">":
136 return [greaterThan(stack), rest];
137 case "<=":
138 return [lessOrEqual(stack), rest];
139 case ">=":
140 return [greaterOrEqual(stack), rest];
141 case "and":
142 return [and(stack), rest];
143 case "or":
144 return [or(stack), rest];
145 case "invert":
146 return [invert(stack), rest];
147 case "dup":
148 return [dup(stack), rest];
149 case "drop":
150 return [drop(stack), rest];
151 case "swap":
152 return [swap(stack), rest];
153 case "if":
154 return branch(stack, rest);
155 case "then":
156 throw "'then' without a matching 'if'";
157 case ",,":
158 return unquote(stack, rest);
159 case ":":
160 return [stack, define(rest)];
161 default:
162 return parse(word, stack, rest);
163 }
164 }
165
166 function unquote(stack, rest) {
167 let [quote, ...restStack] = stack;
168 if (typeof quote === "string" || quote instanceof String) {
169 return [restStack, [...splitWords(quote), ...rest]];
170 } else {
171 throw "not a string, only strings are unquoteable";
172 }
173 }
174
175 function branch(stack, words) {
176 if (stack.length === 0) throw "stack underflow, nothing to test";
177 let [condition, ...restStack] = stack;
178 let index = findThen(words);
179
180 // The branch is taken: run the words in between, but drop the 'then' that
181 // closes them, it has nothing left to do but clutter the log.
182 if (condition) {
183 let restWords = [...words.slice(0, index), ...words.slice(index + 1)];
184 return [restStack, restWords];
185 }
186
187 return [restStack, words.slice(index + 1)];
188 }
189
190 /** The index of the 'then' closing an 'if', skipping over any nested ones. */
191 function findThen(words) {
192 let depth = 1;
193
194 for (let index = 0; index < words.length; index++) {
195 const word = words[index];
196 if (word === "if") {
197 depth++;
198 } else if (word === "then") {
199 depth--;
200 if (depth === 0) return index;
201 }
202 }
203
204 throw "expected 'then', found end of program";
205 }
206
207 function define(words) {
208 if (words.length < 2) throw "missing definition after ':'";
209 let [ident, ...rest] = words;
210 let index = 0;
211 let definition = [];
212
213 for (; index < rest.length; index++) {
214 const word = rest[index];
215 if (word === ";") {
216 break;
217 } else if (rest.length - 1 === index) {
218 throw "expected ';', found end of program";
219 }
220 definition.push(word);
221 }
222
223 definitions[ident] = definition;
224
225 return rest.slice(index + 1);
226 }
227
228 function dup(stack) {
229 if (stack.length === 0) throw "stack underflow, nothing to duplicate";
230 let [x, ...rest] = stack;
231 return [x, x, ...rest];
232 }
233
234 function drop(stack) {
235 if (stack.length === 0) throw "stack underflow, nothing to drop";
236 let [_, ...rest] = stack;
237 return rest;
238 }
239
240 function swap(stack) {
241 if (stack.length < 2) throw "stack underflow, not enough to swap";
242 let [x, y, ...rest] = stack;
243 return [y, x, ...rest];
244 }
245
246 function invert(stack) {
247 if (stack.length === 0) throw "stack underflow, nothing to invert";
248 let [x, ...rest] = stack;
249 return [!x, ...rest];
250 }
251
252 function parse(word, stack, rest) {
253 if (word.startsWith('"')) {
254 return parseString(word, stack, rest);
255 }
256
257 if (word in definitions) {
258 return [stack, [...definitions[word], ...rest]];
259 }
260
261 if (word === "true" || word === "false") {
262 return [[word === "true", ...stack], rest];
263 }
264
265 let num = Number(word);
266 if (isNaN(num)) {
267 throw `word '${word}' not recognised`;
268 }
269
270 return [[num, ...stack], rest];
271 }
272
273 function parseString(word, stack, rest) {
274 if (word.length > 1 && word.endsWith('"')) {
275 return [[word.slice(1, -1), ...stack], rest];
276 }
277
278 let string = word.slice(1);
279 let index = 0;
280
281 for (; index < rest.length; index++) {
282 const word = rest[index];
283 if (word.endsWith('"')) {
284 string = string.concat(" ", word.slice(0, -1));
285 break;
286 } else if (rest.length - 1 === index) {
287 throw "expected word ending with '\"', found end of program";
288 }
289 string = string.concat(" ", word);
290 }
291
292 return [[string, ...stack], rest.slice(index + 1)];
293 }
294
295 function writeLog(stack, words) {
296 return new Promise((resolve, _reject) => {
297 let log_left = h("span", `[${stack.join(", ")}]`);
298 let log_right = h("span", words.join(" "));
299
300 if (words.length === 0) {
301 log_left.textContent += " <==";
302 }
303
304 let log_row = h("div", {class: "stack-step"}, [log_left, log_right]);
305
306 log.appendChild(log_row);
307 resolve();
308 });
309 }
310
311 function resetLog() {
312 if (!log.hasChildNodes()) return;
313
314 let summary = h("summary", log.firstChild.lastChild.textContent);
315
316 let old_log = log.cloneNode(true);
317 old_log.id = "";
318
319 let details = h("details", {class: "history"}, [summary, old_log]);
320
321 log.insertAdjacentElement("afterend", details);
322 log.replaceChildren(); //remove children, clear log
323 }