Blog/Components/Pages/Concat.razor.js 6.3 K · 244 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 numbers";
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 async function evalWords(inputWords) {
73 let words = inputWords;
74 let stack = [];
75
76 while (words.length > 0) {
77 await writeLog(stack, words);
78 if (words.length === 0) return stack;
79
80 let [word, ...rest] = words;
81 [stack, words] = evalWord(word, stack, rest);
82
83 await new Promise((r) => setTimeout(r, 100));
84 }
85 await writeLog(stack, words);
86 return stack;
87 }
88
89 function evalWord(word, stack, rest) {
90 switch (word) {
91 case "+":
92 return [plus(stack), rest];
93 case "-":
94 return [subtract(stack), rest];
95 case "*":
96 return [multiply(stack), rest];
97 case "/":
98 return [divide(stack), rest];
99 case "dup":
100 return [dup(stack), rest];
101 case "drop":
102 return [drop(stack), rest];
103 case "swap":
104 return [swap(stack), rest];
105 case "skip":
106 return skip(stack, rest);
107 case ",,":
108 return unquote(stack, rest);
109 case ":":
110 return [stack, define(rest)];
111 default:
112 return parse(word, stack, rest);
113 }
114 }
115
116 function unquote(stack, rest) {
117 let [quote, ...restStack] = stack;
118 if (typeof quote === "string" || quote instanceof String) {
119 return [restStack, [...splitWords(quote), ...rest]];
120 } else {
121 throw "not a string, only strings are unquoteable";
122 }
123 }
124
125 function skip(stack, words) {
126 if (stack.length === 0) throw "stack underflow, dont know how much to skip";
127 let [amount, ...restStack] = stack;
128
129 if (amount > words.length)
130 throw `program underflow, cant skip ${amount} words`;
131 if (amount <= 0) return [stack.slice(1), words]; // no skipping on <= 0
132
133 let restWords = words.slice(amount);
134
135 return [restStack, restWords];
136 }
137
138 function define(words) {
139 if (words.length < 2) throw "missing definition after ':'";
140 let [ident, ...rest] = words;
141 let index = 0;
142 let definition = [];
143
144 for (; index < rest.length; index++) {
145 const word = rest[index];
146 if (word === ";") {
147 break;
148 } else if (rest.length - 1 === index) {
149 throw "expected ';', found end of program";
150 }
151 definition.push(word);
152 }
153
154 definitions[ident] = definition;
155
156 return rest.slice(index + 1);
157 }
158
159 function dup(stack) {
160 if (stack.length === 0) throw "stack underflow, nothing to duplicate";
161 let [x, ...rest] = stack;
162 return [x, x, ...rest];
163 }
164
165 function drop(stack) {
166 if (stack.length === 0) throw "stack underflow, nothing to drop";
167 let [_, ...rest] = stack;
168 return rest;
169 }
170
171 function swap(stack) {
172 if (stack.length < 2) throw "stack underflow, not enough to swap";
173 let [x, y, ...rest] = stack;
174 return [y, x, ...rest];
175 }
176
177 function parse(word, stack, rest) {
178 if (word.startsWith('"')) {
179 return parseString(word, stack, rest);
180 }
181
182 if (word in definitions) {
183 return [stack, [...definitions[word], ...rest]];
184 }
185
186 let num = Number(word);
187 if (isNaN(num)) {
188 throw `word '${word}' not recognised`;
189 }
190
191 return [[num, ...stack], rest];
192 }
193
194 function parseString(word, stack, rest) {
195 if (word.length > 1 && word.endsWith('"')) {
196 return [[word.slice(1, -1), ...stack], rest];
197 }
198
199 let string = word.slice(1);
200 let index = 0;
201
202 for (; index < rest.length; index++) {
203 const word = rest[index];
204 if (word.endsWith('"')) {
205 string = string.concat(" ", word.slice(0, -1));
206 break;
207 } else if (rest.length - 1 === index) {
208 throw "expected word ending with '\"', found end of program";
209 }
210 string = string.concat(" ", word);
211 }
212
213 return [[string, ...stack], rest.slice(index + 1)];
214 }
215
216 function writeLog(stack, words) {
217 return new Promise((resolve, _reject) => {
218 let log_left = h("span", `[${stack.join(", ")}]`);
219 let log_right = h("span", words.join(" "));
220
221 if (words.length === 0) {
222 log_left.textContent += " <==";
223 }
224
225 let log_row = h("div", {class: "flex-spread"}, [log_left, log_right]);
226
227 log.appendChild(log_row);
228 resolve();
229 });
230 }
231
232 function resetLog() {
233 if (!log.hasChildNodes()) return;
234
235 let summary = h("summary", log.firstChild.lastChild.textContent);
236
237 let old_log = log.cloneNode(true);
238 old_log.id = "";
239
240 let details = h("details", {class: "history"}, [summary, old_log]);
241
242 log.insertAdjacentElement("afterend", details);
243 log.replaceChildren(); //remove children, clear log
244 }