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