@fraczak/k 6.3.2 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/DOCS/CODECS.md +258 -0
  2. package/DOCS/DICTIONARY.md +1172 -0
  3. package/DOCS/KIR_SPECIALIZATION_DESIGN.md +51 -46
  4. package/DOCS/KIR_V1.md +221 -0
  5. package/DOCS/KVM_EXECUTION_MODEL.md +56 -19
  6. package/DOCS/OBJECT_FILE_AND_PATTERN.md +2 -1
  7. package/DOCS/PROFILING_AND_TRACING.md +196 -0
  8. package/DOCS/REPL.md +76 -14
  9. package/DOCS/WEB_REPL_REQUIREMENTS.md +135 -0
  10. package/DOCS/dependency-graph.md +144 -0
  11. package/Examples/arithmetics.k +51 -48
  12. package/Examples/bnat.k +1 -1
  13. package/Examples/buda.k +44 -0
  14. package/Examples/ieee.k +300 -8671
  15. package/Examples/poly.k +67 -0
  16. package/README.md +68 -19
  17. package/augmentation.mjs +9 -7
  18. package/backend-api.mjs +11 -2
  19. package/backends/arm64/README.md +104 -0
  20. package/backends/arm64/bin/k-arm64-compile.mjs +101 -0
  21. package/backends/arm64/bin/k-arm64-run.mjs +58 -0
  22. package/backends/arm64/bin/k-arm64.mjs +88 -0
  23. package/backends/arm64/scripts/run-tests.mjs +41 -0
  24. package/backends/arm64/src/arm64.mjs +229 -0
  25. package/backends/arm64/src/cli.mjs +155 -0
  26. package/backends/arm64/src/kvm2arm64.mjs +906 -0
  27. package/backends/arm64/src/runtime.c +728 -0
  28. package/backends/arm64/tests/integration.sh +112 -0
  29. package/backends/arm64/tests/test-arm64.mjs +240 -0
  30. package/backends/llvm/README.md +135 -0
  31. package/backends/llvm/bin/k-llvm-build.mjs +71 -0
  32. package/backends/llvm/bin/k-llvm-compile.mjs +68 -0
  33. package/backends/llvm/bin/k-llvm-jit.mjs +160 -0
  34. package/backends/llvm/bin/k-llvm-run.mjs +90 -0
  35. package/backends/llvm/runtime/krt.c +1508 -0
  36. package/backends/llvm/runtime/krt.h +109 -0
  37. package/backends/llvm/scripts/conformance.mjs +76 -0
  38. package/backends/llvm/scripts/run-tests.mjs +28 -0
  39. package/backends/llvm/src/executable.mjs +562 -0
  40. package/backends/llvm/src/kvm2llvm.mjs +998 -0
  41. package/backends/llvm/src/llvm.mjs +1092 -0
  42. package/backends/llvm/tests/composition-driver.c +22 -0
  43. package/backends/llvm/tests/identity-driver.c +27 -0
  44. package/backends/llvm/tests/integration.sh +153 -0
  45. package/backends/llvm/tests/perf-support.mjs +735 -0
  46. package/backends/llvm/tests/product-driver.c +23 -0
  47. package/backends/llvm/tests/projection-driver.c +19 -0
  48. package/backends/llvm/tests/relation-driver.c +29 -0
  49. package/backends/llvm/tests/test-llvm.mjs +240 -0
  50. package/backends/llvm/tests/union-driver.c +31 -0
  51. package/backends/llvm/tests/variant-driver.c +19 -0
  52. package/backends/llvm/tests/variant-projection-driver.c +21 -0
  53. package/backends/wasm/DOCS/USE-WEB-ASSEMBLY.md +233 -0
  54. package/backends/wasm/README.md +97 -0
  55. package/backends/wasm/TECHNICAL-DEBT.md +60 -0
  56. package/backends/wasm/bin/k-wasm-compile.mjs +50 -0
  57. package/backends/wasm/bin/k-wasm-run.mjs +140 -0
  58. package/backends/wasm/bin/k-wasm.mjs +49 -0
  59. package/backends/wasm/demo_ieee.mjs +319 -0
  60. package/backends/wasm/runtime.wat +102 -0
  61. package/backends/wasm/scripts/run-tests.mjs +41 -0
  62. package/backends/wasm/src/cli.mjs +133 -0
  63. package/backends/wasm/src/kvm2wasm.mjs +661 -0
  64. package/backends/wasm/src/wasm.mjs +1010 -0
  65. package/backends/wasm/tests/integration.sh +182 -0
  66. package/backends/wasm/tests/perf-support.mjs +336 -0
  67. package/backends/wasm/tests/test-wasm.mjs +617 -0
  68. package/browser/repl-browser.mjs +1584 -0
  69. package/browser/shims/assert.mjs +19 -0
  70. package/browser/shims/buffer.mjs +224 -0
  71. package/browser/shims/crypto.mjs +103 -0
  72. package/browser/shims/fs.mjs +159 -0
  73. package/browser/shims/os.mjs +17 -0
  74. package/browser/shims/path.mjs +54 -0
  75. package/browser/shims/process.mjs +44 -0
  76. package/browser/shims/readline.mjs +11 -0
  77. package/browser/shims/url.mjs +12 -0
  78. package/browser/vfs-data.mjs +19 -0
  79. package/codecs/README.md +3 -0
  80. package/codecs/json.mjs +16 -2
  81. package/codecs/runtime/show-value.mjs +2 -2
  82. package/conformance/helper-reuse/case.json +8 -0
  83. package/conformance/helper-reuse/expected.kv +1 -0
  84. package/conformance/helper-reuse/input.kv +1 -0
  85. package/conformance/helper-reuse/program.k +2 -0
  86. package/conformance/identity/case.json +8 -0
  87. package/conformance/identity/expected.kv +1 -0
  88. package/conformance/identity/input.kv +1 -0
  89. package/conformance/identity/program.k +1 -0
  90. package/conformance/product-projection/case.json +8 -0
  91. package/conformance/product-projection/expected.kv +1 -0
  92. package/conformance/product-projection/input.kv +1 -0
  93. package/conformance/product-projection/program.k +1 -0
  94. package/conformance/run.mjs +149 -0
  95. package/conformance/variant-construction/case.json +8 -0
  96. package/conformance/variant-construction/expected.kv +1 -0
  97. package/conformance/variant-construction/input.kv +1 -0
  98. package/conformance/variant-construction/program.k +1 -0
  99. package/harness/adapters/arm64.mjs +105 -0
  100. package/harness/adapters/kvm.mjs +144 -0
  101. package/harness/adapters/llvm.mjs +118 -0
  102. package/harness/adapters/native.mjs +77 -0
  103. package/harness/adapters/wasm.mjs +169 -0
  104. package/harness/backend.mjs +95 -0
  105. package/harness/cli.mjs +130 -0
  106. package/harness/format.mjs +95 -0
  107. package/harness/index.mjs +25 -0
  108. package/harness/persistent.mjs +121 -0
  109. package/harness/registry.mjs +44 -0
  110. package/harness/suite.mjs +257 -0
  111. package/harness/suites/poly.mjs +156 -0
  112. package/harness/trace.mjs +50 -0
  113. package/harness/util.mjs +92 -0
  114. package/intrinsics.mjs +41 -0
  115. package/kir.mjs +399 -0
  116. package/kvm.mjs +585 -81
  117. package/objects/README.md +37 -3
  118. package/objects/compile.mjs +15 -22
  119. package/objects/inspect.mjs +96 -0
  120. package/objects/validate.mjs +415 -0
  121. package/package.json +51 -19
  122. package/repl-codecs.mjs +133 -20
  123. package/repl.mjs +408 -59
  124. package/run.mjs +2 -7
  125. package/scripts/benchmark.mjs +406 -22
  126. package/scripts/build-repl-html.mjs +1784 -0
  127. package/scripts/perf-ieee.mjs +586 -0
  128. package/scripts/perf-int.mjs +531 -0
  129. package/scripts/perf-poly.mjs +945 -0
  130. package/scripts/render-benchmark.mjs +12 -8
  131. package/scripts/run-tests.mjs +6 -0
  132. package/tests/integration.sh +19 -2
  133. package/tests/test-arithmetics.mjs +54 -0
  134. package/tests/test-ieee-arithmetic.mjs +6 -2
  135. package/tests/test-kir.mjs +85 -0
  136. package/tests/test-kvm-polymorphic.mjs +278 -0
  137. package/tests/test-kvm.mjs +25 -6
  138. package/tests/test-poly.mjs +129 -0
  139. package/tests/test-repl-html.mjs +303 -0
  140. package/tests/test-repl.mjs +175 -1
  141. package/tests/test-validate-object.mjs +40 -0
  142. package/a.klib +0 -1
  143. package/a.kvm +0 -142676
  144. package/a.wasm +0 -0
  145. package/benchmark-results.html +0 -532
  146. package/benchmark-results.json +0 -480
  147. package/k-vscode/.vscode/launch.json +0 -14
  148. package/k-vscode/README.md +0 -75
  149. package/k-vscode/extension.js +0 -273
  150. package/k-vscode/language-configuration.json +0 -42
  151. package/k-vscode/package.json +0 -79
  152. package/k-vscode/snippets/k.json +0 -92
  153. package/k-vscode/syntaxes/k.tmLanguage.json +0 -599
  154. package/k-vscode/test/background-ranges.test.js +0 -50
  155. package/k-vscode/themes/k-contrast-color-theme.json +0 -104
  156. package/scripts/git-hooks/pre-commit +0 -10
@@ -0,0 +1,1584 @@
1
+ import {
2
+ createState,
3
+ evaluateInput,
4
+ createCompleter,
5
+ promptForState,
6
+ helpText,
7
+ analyzeRawSnippet,
8
+ lineTerminatesSnippet,
9
+ lineHasExplicitContinuation,
10
+ savedLibrary,
11
+ listCodecs,
12
+ loadCodecModule,
13
+ registerCodec,
14
+ unregisterCodec,
15
+ resolveCodec,
16
+ codeHashToPattern,
17
+ BUILTIN_CODECS,
18
+ formatDuration
19
+ } from "../repl.mjs";
20
+ import { Value, isProduct, isVariant } from "../Value.mjs";
21
+ import { encodeLibrary } from "../object.mjs";
22
+ import { setVfsFile, getVfsFile, getAllVfsFiles } from "./shims/fs.mjs";
23
+
24
+ // Make Value and helpers globally available for custom codecs
25
+ if (typeof window !== "undefined") {
26
+ window.Value = Value;
27
+ window.isProduct = isProduct;
28
+ window.isVariant = isVariant;
29
+ }
30
+
31
+ // Global REPL state
32
+ let state = createState();
33
+ let completer = createCompleter(state);
34
+ let isEvaluating = false;
35
+
36
+ // Command history
37
+ const HISTORY_KEY = "k_repl_history";
38
+ let history = [];
39
+ try {
40
+ const saved = localStorage.getItem(HISTORY_KEY);
41
+ if (saved) history = JSON.parse(saved);
42
+ } catch {
43
+ history = [];
44
+ }
45
+ let historyIndex = history.length;
46
+ let currentDraft = "";
47
+
48
+ // DOM Elements
49
+ let outputEl;
50
+ let inputEl;
51
+ let promptEl;
52
+ let statusDot;
53
+ let statusText;
54
+ let autocompleteEl;
55
+ let vfsModal;
56
+ let vfsListEl;
57
+ let vfsPreviewEl;
58
+ let vfsPreviewNameEl;
59
+ let helpModal;
60
+ let dropOverlay;
61
+ let codecsModal;
62
+ let codecsBadgeEl;
63
+ let activeCodecsTbody;
64
+ let codecTypeSelect;
65
+
66
+ // Input Popup Modal Elements
67
+ let inputPopupModal;
68
+ let inputPopupType;
69
+ let inputPopupCodec;
70
+ let inputPopupText;
71
+ let inputPopupStatus;
72
+ let inputPopupHint;
73
+ let inputSamplesList;
74
+
75
+ function escapeHtml(str) {
76
+ return String(str)
77
+ .replace(/&/g, "&")
78
+ .replace(/</g, "&lt;")
79
+ .replace(/>/g, "&gt;")
80
+ .replace(/"/g, "&quot;")
81
+ .replace(/'/g, "&#039;");
82
+ }
83
+
84
+ function ansiToHtml(str) {
85
+ let inSpan = false;
86
+ const escaped = escapeHtml(str);
87
+ const formatted = escaped.replace(/\x1b\[([0-9;]+)m/g, (_, code) => {
88
+ const codes = code.split(";");
89
+ let style = "";
90
+ for (const c of codes) {
91
+ if (c === "0") {
92
+ if (inSpan) {
93
+ inSpan = false;
94
+ return "</span>";
95
+ }
96
+ return "";
97
+ }
98
+ if (c === "1") style += "font-weight:600;";
99
+ if (c === "31" || c === "91") style += "color:var(--color-red);";
100
+ if (c === "32" || c === "92") style += "color:var(--color-green);";
101
+ if (c === "33" || c === "93") style += "color:var(--color-yellow);";
102
+ if (c === "34" || c === "94") style += "color:var(--color-blue);";
103
+ if (c === "35" || c === "95") style += "color:var(--color-purple);";
104
+ if (c === "36" || c === "96") style += "color:var(--color-cyan);";
105
+ }
106
+ if (style) {
107
+ inSpan = true;
108
+ return `<span style="${style}">`;
109
+ }
110
+ return "";
111
+ });
112
+ return inSpan ? formatted + "</span>" : formatted;
113
+ }
114
+
115
+ function countActiveCodecs() {
116
+ const store = state.codecs || {};
117
+ let count = 0;
118
+ const seen = new Set();
119
+ for (const list of Object.values(store)) {
120
+ for (const c of list) {
121
+ if (!seen.has(c.name)) {
122
+ seen.add(c.name);
123
+ count++;
124
+ }
125
+ }
126
+ }
127
+ return count;
128
+ }
129
+
130
+ function updateCodecsBadge() {
131
+ if (!codecsBadgeEl) return;
132
+ const count = countActiveCodecs();
133
+ codecsBadgeEl.textContent = String(count);
134
+ codecsBadgeEl.className = count > 0 ? "badge-count active" : "badge-count";
135
+ }
136
+
137
+ function updatePrompt() {
138
+ const prompt = promptForState(state);
139
+ if (promptEl) promptEl.textContent = prompt;
140
+ if (inputEl) {
141
+ inputEl.placeholder = state.pendingInput
142
+ ? `Enter value for ${state.pendingInput.promptName || "input"}...`
143
+ : "Enter K expression or command (:help, :codecs)...";
144
+ }
145
+ updateCodecsBadge();
146
+ }
147
+
148
+ function setStatus(busy, text = "Ready") {
149
+ isEvaluating = busy;
150
+ if (statusDot) {
151
+ statusDot.className = busy ? "status-dot busy" : "status-dot ready";
152
+ }
153
+ if (statusText) {
154
+ statusText.textContent = busy ? "Running..." : text;
155
+ }
156
+ }
157
+
158
+ function appendEntry({ prompt, command, outputs = [], errors = [], warnings = [], duration = null, timing = null }) {
159
+ if (!outputEl) return;
160
+
161
+ const entry = document.createElement("div");
162
+ entry.className = "terminal-entry";
163
+
164
+ if (command !== undefined && command !== null) {
165
+ const header = document.createElement("div");
166
+ header.className = "entry-header";
167
+
168
+ const promptSpan = document.createElement("span");
169
+ promptSpan.className = "entry-prompt";
170
+ promptSpan.textContent = prompt || "> ";
171
+ header.appendChild(promptSpan);
172
+
173
+ const cmdSpan = document.createElement("span");
174
+ cmdSpan.className = "entry-command";
175
+ cmdSpan.textContent = command;
176
+ header.appendChild(cmdSpan);
177
+
178
+ if (timing && (timing.compileMs > 0 || timing.executeMs > 0)) {
179
+ const metaSpan = document.createElement("span");
180
+ metaSpan.className = "entry-meta";
181
+ metaSpan.textContent = `comp: ${formatDuration(timing.compileMs)} | exec: ${formatDuration(timing.executeMs)}`;
182
+ metaSpan.title = `Compilation: ${timing.compileMs.toFixed(1)}ms\nExecution: ${timing.executeMs.toFixed(1)}ms\nTotal: ${(duration ?? timing.totalMs).toFixed(1)}ms`;
183
+ header.appendChild(metaSpan);
184
+ } else if (duration !== null) {
185
+ const metaSpan = document.createElement("span");
186
+ metaSpan.className = "entry-meta";
187
+ metaSpan.textContent = formatDuration(duration);
188
+ metaSpan.title = `Total: ${duration.toFixed(1)}ms`;
189
+ header.appendChild(metaSpan);
190
+ }
191
+
192
+ const copyBtn = document.createElement("button");
193
+ copyBtn.className = "btn-copy-entry";
194
+ copyBtn.title = "Copy command";
195
+ copyBtn.textContent = "📋";
196
+ copyBtn.onclick = () => {
197
+ navigator.clipboard.writeText(command);
198
+ copyBtn.textContent = "✓";
199
+ setTimeout(() => { copyBtn.textContent = "📋"; }, 1500);
200
+ };
201
+ header.appendChild(copyBtn);
202
+
203
+ entry.appendChild(header);
204
+ }
205
+
206
+ // Warnings
207
+ for (const warn of warnings) {
208
+ const warnDiv = document.createElement("div");
209
+ warnDiv.className = "entry-line line-warning";
210
+ warnDiv.innerHTML = ansiToHtml(warn);
211
+ entry.appendChild(warnDiv);
212
+ }
213
+
214
+ // Errors
215
+ for (const err of errors) {
216
+ const errDiv = document.createElement("div");
217
+ errDiv.className = "entry-line line-error";
218
+ errDiv.innerHTML = ansiToHtml(err);
219
+ entry.appendChild(errDiv);
220
+ }
221
+
222
+ // Outputs
223
+ for (const out of outputs) {
224
+ const outDiv = document.createElement("div");
225
+ outDiv.className = "entry-line line-output";
226
+ outDiv.innerHTML = ansiToHtml(out);
227
+ entry.appendChild(outDiv);
228
+ }
229
+
230
+ outputEl.appendChild(entry);
231
+ outputEl.scrollTop = outputEl.scrollHeight;
232
+ }
233
+
234
+ function appendSystemMessage(msg, isError = false, isHtml = false) {
235
+ if (!outputEl) return;
236
+ const entry = document.createElement("div");
237
+ entry.className = `terminal-entry system-msg ${isError ? "line-error" : ""}`;
238
+ if (isHtml || msg.trim().startsWith("<")) {
239
+ entry.innerHTML = msg;
240
+ } else {
241
+ entry.innerHTML = ansiToHtml(msg);
242
+ }
243
+ outputEl.appendChild(entry);
244
+ outputEl.scrollTop = outputEl.scrollHeight;
245
+ }
246
+
247
+ export async function executeCommand(input) {
248
+ const trimmed = input.trim();
249
+ if (!trimmed && !state.pendingInput) return;
250
+
251
+ // Intercept :input without args -> open input popup
252
+ if (trimmed === ":input") {
253
+ openInputPopup();
254
+ return;
255
+ }
256
+
257
+ // Add to history
258
+ if (history.length === 0 || history[history.length - 1] !== input) {
259
+ history.push(input);
260
+ if (history.length > 200) history.shift();
261
+ try {
262
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
263
+ } catch {}
264
+ }
265
+ historyIndex = history.length;
266
+ currentDraft = "";
267
+
268
+ const activePrompt = promptForState(state);
269
+ setStatus(true);
270
+
271
+ const warnings = [];
272
+ const origWarn = console.warn;
273
+ console.warn = (...args) => {
274
+ warnings.push(args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" "));
275
+ origWarn(...args);
276
+ };
277
+
278
+ const t0 = performance.now();
279
+ let outputs = [];
280
+ let errors = [];
281
+
282
+ try {
283
+ outputs = await evaluateInput(input, state);
284
+ } catch (err) {
285
+ errors.push(err.message || String(err));
286
+ } finally {
287
+ console.warn = origWarn;
288
+ }
289
+
290
+ const duration = performance.now() - t0;
291
+ setStatus(false);
292
+ updatePrompt();
293
+
294
+ appendEntry({
295
+ prompt: activePrompt,
296
+ command: input,
297
+ outputs,
298
+ errors,
299
+ warnings,
300
+ duration,
301
+ timing: state.lastTiming
302
+ });
303
+
304
+ // Re-create completer with updated state
305
+ completer = createCompleter(state);
306
+ updateCodecsBadge();
307
+ if (codecsModal && codecsModal.classList.contains("open")) {
308
+ renderCodecsModal();
309
+ }
310
+
311
+ // If :input <type> [codec] was executed, state.pendingInput is now set!
312
+ // Open the Input Popup immediately to prompt the user for the input string!
313
+ if (errors.length === 0 && state.pendingInput && trimmed.startsWith(":input ")) {
314
+ openInputPopup();
315
+ }
316
+ }
317
+
318
+ function adjustInputHeight() {
319
+ if (!inputEl) return;
320
+ inputEl.style.height = "auto";
321
+ const newHeight = Math.min(Math.max(inputEl.scrollHeight, 28), 240);
322
+ inputEl.style.height = `${newHeight}px`;
323
+ }
324
+
325
+ // Autocomplete logic
326
+ function handleAutocomplete() {
327
+ if (!inputEl || !autocompleteEl) return;
328
+ const text = inputEl.value;
329
+ const cursorPos = inputEl.selectionStart;
330
+ const lineBeforeCursor = text.slice(0, cursorPos).split("\n").pop();
331
+
332
+ const [matches, original] = completer(lineBeforeCursor);
333
+ if (!matches || matches.length === 0) {
334
+ hideAutocomplete();
335
+ return;
336
+ }
337
+
338
+ if (matches.length === 1) {
339
+ hideAutocomplete();
340
+ const tokenStart = lineBeforeCursor.lastIndexOf(" ") + 1;
341
+ const match = matches[0];
342
+ const beforeToken = text.slice(0, cursorPos - (lineBeforeCursor.length - tokenStart));
343
+ const afterCursor = text.slice(cursorPos);
344
+ inputEl.value = beforeToken + match + (afterCursor.startsWith(" ") ? "" : " ") + afterCursor;
345
+ inputEl.selectionStart = inputEl.selectionEnd = beforeToken.length + match.length + 1;
346
+ adjustInputHeight();
347
+ return;
348
+ }
349
+
350
+ // Multiple matches: show dropdown popup
351
+ showAutocompletePopup(matches, lineBeforeCursor);
352
+ }
353
+
354
+ let activeMatchIndex = -1;
355
+ let currentMatches = [];
356
+
357
+ function showAutocompletePopup(matches, line) {
358
+ currentMatches = matches;
359
+ activeMatchIndex = 0;
360
+ autocompleteEl.innerHTML = "";
361
+ matches.forEach((m, idx) => {
362
+ const item = document.createElement("div");
363
+ item.className = `autocomplete-item ${idx === 0 ? "active" : ""}`;
364
+ item.textContent = m;
365
+ item.onmousedown = (e) => {
366
+ e.preventDefault();
367
+ applyAutocomplete(m);
368
+ };
369
+ autocompleteEl.appendChild(item);
370
+ });
371
+ autocompleteEl.classList.add("visible");
372
+ }
373
+
374
+ function hideAutocomplete() {
375
+ if (autocompleteEl) {
376
+ autocompleteEl.classList.remove("visible");
377
+ autocompleteEl.innerHTML = "";
378
+ }
379
+ currentMatches = [];
380
+ activeMatchIndex = -1;
381
+ }
382
+
383
+ function applyAutocomplete(match) {
384
+ if (!inputEl) return;
385
+ const text = inputEl.value;
386
+ const cursorPos = inputEl.selectionStart;
387
+ const lineBeforeCursor = text.slice(0, cursorPos).split("\n").pop();
388
+ const tokenStart = Math.max(0, lineBeforeCursor.lastIndexOf(" ") + 1);
389
+ const beforeToken = text.slice(0, cursorPos - (lineBeforeCursor.length - tokenStart));
390
+ const afterCursor = text.slice(cursorPos);
391
+ inputEl.value = beforeToken + match + (afterCursor.startsWith(" ") ? "" : " ") + afterCursor;
392
+ inputEl.selectionStart = inputEl.selectionEnd = beforeToken.length + match.length + 1;
393
+ hideAutocomplete();
394
+ adjustInputHeight();
395
+ inputEl.focus();
396
+ }
397
+
398
+ // History navigation
399
+ function navigateHistory(direction) {
400
+ if (history.length === 0) return;
401
+ if (historyIndex === history.length) {
402
+ currentDraft = inputEl.value;
403
+ }
404
+ if (direction === "up") {
405
+ if (historyIndex > 0) {
406
+ historyIndex--;
407
+ inputEl.value = history[historyIndex];
408
+ }
409
+ } else if (direction === "down") {
410
+ if (historyIndex < history.length - 1) {
411
+ historyIndex++;
412
+ inputEl.value = history[historyIndex];
413
+ } else {
414
+ historyIndex = history.length;
415
+ inputEl.value = currentDraft;
416
+ }
417
+ }
418
+ adjustInputHeight();
419
+ inputEl.selectionStart = inputEl.selectionEnd = inputEl.value.length;
420
+ }
421
+
422
+ // VFS File Explorer Modal
423
+ function openVfsModal() {
424
+ if (!vfsModal) return;
425
+ renderVfsFileList();
426
+ vfsModal.classList.add("open");
427
+ }
428
+
429
+ function closeVfsModal() {
430
+ if (vfsModal) vfsModal.classList.remove("open");
431
+ }
432
+
433
+ function renderVfsFileList() {
434
+ if (!vfsListEl) return;
435
+ vfsListEl.innerHTML = "";
436
+ const files = getAllVfsFiles().sort();
437
+
438
+ for (const filename of files) {
439
+ const item = document.createElement("div");
440
+ item.className = "vfs-list-item";
441
+
442
+ const nameSpan = document.createElement("span");
443
+ nameSpan.className = "vfs-file-name";
444
+ nameSpan.textContent = filename;
445
+ item.appendChild(nameSpan);
446
+
447
+ const data = getVfsFile(filename);
448
+ const size = typeof data === "string" ? new TextEncoder().encode(data).length : (data?.length || 0);
449
+ const sizeSpan = document.createElement("span");
450
+ sizeSpan.className = "vfs-file-size";
451
+ sizeSpan.textContent = `${(size / 1024).toFixed(1)} KB`;
452
+ item.appendChild(sizeSpan);
453
+
454
+ item.onclick = () => showVfsFilePreview(filename);
455
+ vfsListEl.appendChild(item);
456
+ }
457
+
458
+ if (files.length > 0) {
459
+ showVfsFilePreview(files[0]);
460
+ }
461
+ }
462
+
463
+ let currentPreviewFile = null;
464
+ function showVfsFilePreview(filename) {
465
+ currentPreviewFile = filename;
466
+ if (!vfsPreviewEl || !vfsPreviewNameEl) return;
467
+ vfsPreviewNameEl.textContent = filename;
468
+ const content = getVfsFile(filename);
469
+ if (typeof content === "string") {
470
+ vfsPreviewEl.textContent = content;
471
+ } else if (content instanceof Uint8Array || (typeof Buffer !== "undefined" && Buffer.isBuffer(content))) {
472
+ vfsPreviewEl.textContent = `<binary data, ${content.length} bytes>`;
473
+ } else {
474
+ vfsPreviewEl.textContent = String(content || "");
475
+ }
476
+ }
477
+
478
+ function loadPreviewedFile() {
479
+ if (!currentPreviewFile) return;
480
+ closeVfsModal();
481
+ if (currentPreviewFile.endsWith(".mjs") || currentPreviewFile.startsWith("codecs/")) {
482
+ executeCommand(`:codec load ${currentPreviewFile}`);
483
+ } else {
484
+ executeCommand(`:load ${currentPreviewFile}`);
485
+ }
486
+ }
487
+
488
+ function downloadPreviewedFile() {
489
+ if (!currentPreviewFile) return;
490
+ const content = getVfsFile(currentPreviewFile);
491
+ const blob = typeof content === "string"
492
+ ? new Blob([content], { type: "text/plain;charset=utf-8" })
493
+ : new Blob([content], { type: "application/octet-stream" });
494
+ const url = URL.createObjectURL(blob);
495
+ const a = document.createElement("a");
496
+ a.href = url;
497
+ a.download = currentPreviewFile.split("/").pop();
498
+ a.click();
499
+ URL.revokeObjectURL(url);
500
+ }
501
+
502
+ // File Upload Handler
503
+ function handleFileUpload(file) {
504
+ const reader = new FileReader();
505
+ const isBinary = file.name.endsWith(".klib") || file.name.endsWith(".ko") || file.name.endsWith(".wasm");
506
+
507
+ reader.onload = (e) => {
508
+ let data;
509
+ if (isBinary) {
510
+ data = new Uint8Array(e.target.result);
511
+ } else {
512
+ data = e.target.result;
513
+ }
514
+ setVfsFile(file.name, data);
515
+ appendSystemMessage(`📁 Uploaded <b>${escapeHtml(file.name)}</b> into VFS (${(data.length / 1024).toFixed(1)} KB).`);
516
+ if (file.name.endsWith("-codec.mjs") || file.name.endsWith("-codec.js") || file.name.includes("codec")) {
517
+ executeCommand(`:codec load ${file.name}`);
518
+ } else {
519
+ executeCommand(`:load ${file.name}`);
520
+ }
521
+ };
522
+
523
+ if (isBinary) {
524
+ reader.readAsArrayBuffer(file);
525
+ } else {
526
+ reader.readAsText(file);
527
+ }
528
+ }
529
+
530
+ // Export State as .klib
531
+ function exportKlib() {
532
+ try {
533
+ const lib = savedLibrary(state);
534
+ const encoded = encodeLibrary(lib);
535
+ const blob = new Blob([encoded], { type: "application/octet-stream" });
536
+ const url = URL.createObjectURL(blob);
537
+ const a = document.createElement("a");
538
+ a.href = url;
539
+ a.download = "session.klib";
540
+ a.click();
541
+ URL.revokeObjectURL(url);
542
+ appendSystemMessage("💾 Exported current session state to <b>session.klib</b>.");
543
+ } catch (err) {
544
+ appendSystemMessage(`Failed to export .klib: ${err.message}`, true);
545
+ }
546
+ }
547
+
548
+ // ==========================================
549
+ // CODECS SUBSYSTEM UI & CONTROLS
550
+ // ==========================================
551
+
552
+ const CODEC_PRESETS = {
553
+ yn: {
554
+ name: "yn",
555
+ type: "bool",
556
+ universal: false,
557
+ parse: `// Parse 'yes', 'no', 'true', 'false' into $bool
558
+ const tag = text.trim().toLowerCase();
559
+ if (tag === "yes" || tag === "true" || tag === "1") {
560
+ return Value.variant("true", Value.product({}));
561
+ }
562
+ if (tag === "no" || tag === "false" || tag === "0") {
563
+ return Value.variant("false", Value.product({}));
564
+ }
565
+ throw new Error("Expected yes/no or true/false");`,
566
+ print: `// Serialize boolean value to YES / NO
567
+ return value.tag === "true" ? "YES" : "NO";`
568
+ },
569
+ hex: {
570
+ name: "hex",
571
+ type: "int",
572
+ universal: false,
573
+ parse: `// Parse decimal or hex (e.g. 0x2a, 42, -0x10) into $int
574
+ let str = text.trim();
575
+ let sign = "+";
576
+ if (str.startsWith("-")) { sign = "-"; str = str.slice(1).trim(); }
577
+ else if (str.startsWith("+")) { str = str.slice(1).trim(); }
578
+
579
+ const num = str.startsWith("0x") || str.startsWith("0X") ? BigInt(str) : BigInt(str);
580
+ if (num === 0n) sign = "+";
581
+
582
+ const bitChars = num === 0n ? ["0"] : num.toString(2).split("");
583
+ let v = Value.variant("_", Value.product({}));
584
+ for (let i = bitChars.length - 1; i >= 0; i--) {
585
+ v = Value.variant(bitChars[i], v);
586
+ }
587
+ return Value.variant(sign, v);`,
588
+ print: `// Serialize $int to hexadecimal string (0x...)
589
+ if (!isVariant(value) || (value.tag !== "+" && value.tag !== "-")) {
590
+ throw new Error("Not a valid int value");
591
+ }
592
+ let bits = "";
593
+ let node = value.value;
594
+ while (isVariant(node) && node.tag !== "_") {
595
+ bits += node.tag;
596
+ node = node.value;
597
+ }
598
+ const n = bits === "" ? 0n : BigInt("0b" + bits);
599
+ const hex = "0x" + n.toString(16).toUpperCase();
600
+ return (value.tag === "-" && n !== 0n ? "-" : "") + hex;`
601
+ },
602
+ currency: {
603
+ name: "currency",
604
+ type: "int",
605
+ universal: false,
606
+ parse: `// Parse currency e.g. $42.50 or $100 into cents int
607
+ const clean = text.trim().replace(/^\\$/, "").trim();
608
+ const parts = clean.split(".");
609
+ const dollars = BigInt(parts[0] || "0");
610
+ const cents = BigInt((parts[1] || "0").padEnd(2, "0").slice(0, 2));
611
+ const total = dollars * 100n + cents;
612
+
613
+ const bitChars = total === 0n ? ["0"] : total.toString(2).split("");
614
+ let v = Value.variant("_", Value.product({}));
615
+ for (let i = bitChars.length - 1; i >= 0; i--) {
616
+ v = Value.variant(bitChars[i], v);
617
+ }
618
+ return Value.variant("+", v);`,
619
+ print: `// Format cents int as currency string ($X.XX)
620
+ let bits = "";
621
+ let node = value.value;
622
+ while (isVariant(node) && node.tag !== "_") {
623
+ bits += node.tag;
624
+ node = node.value;
625
+ }
626
+ const n = bits === "" ? 0n : BigInt("0b" + bits);
627
+ const dollars = n / 100n;
628
+ const cents = (n % 100n).toString().padStart(2, "0");
629
+ return "$" + dollars.toString() + "." + cents;`
630
+ },
631
+ blank: {
632
+ name: "custom",
633
+ type: "",
634
+ universal: false,
635
+ parse: `// Parse text string and return a K Value
636
+ // Available: Value.product({ field: val }), Value.variant("tag", val)
637
+ return Value.product({});`,
638
+ print: `// Serialize K Value into string or Buffer
639
+ return JSON.stringify(value);`
640
+ }
641
+ };
642
+
643
+ function openCodecsModal() {
644
+ if (!codecsModal) return;
645
+ renderCodecsModal();
646
+ codecsModal.classList.add("open");
647
+ }
648
+
649
+ function closeCodecsModal() {
650
+ if (codecsModal) codecsModal.classList.remove("open");
651
+ }
652
+
653
+ function renderCodecsModal() {
654
+ updateCodecsBadge();
655
+
656
+ // 1. Built-in codec buttons state
657
+ const standardCodecs = ["int", "utf8", "json", "ieee"];
658
+ for (const name of standardCodecs) {
659
+ const btn = document.getElementById(`btn-toggle-codec-${name}`);
660
+ const badge = document.getElementById(`codec-badge-${name}`);
661
+ const isLoaded = isCodecRegistered(name);
662
+ if (btn) {
663
+ btn.textContent = isLoaded ? "Unload" : "Load";
664
+ btn.className = isLoaded ? "btn btn-sm btn-secondary" : "btn btn-sm btn-primary";
665
+ }
666
+ if (badge) {
667
+ badge.textContent = isLoaded ? "Active" : "Inactive";
668
+ badge.className = isLoaded ? "codec-status-badge active" : "codec-status-badge";
669
+ }
670
+ }
671
+
672
+ // 2. Populate target type dropdown
673
+ if (codecTypeSelect) {
674
+ const prev = codecTypeSelect.value;
675
+ codecTypeSelect.innerHTML = `<option value="">-- Choose Type Alias or Code Hash --</option>`;
676
+ const typeGroup = document.createElement("optgroup");
677
+ typeGroup.label = "Available Type Aliases in State";
678
+ const aliases = Object.entries(state.typeAliases || {}).sort(([a], [b]) => a.localeCompare(b));
679
+ for (const [alias, hash] of aliases) {
680
+ const opt = document.createElement("option");
681
+ opt.value = alias;
682
+ opt.textContent = `$${alias} (${hash.slice(0, 10)}...)`;
683
+ typeGroup.appendChild(opt);
684
+ }
685
+ codecTypeSelect.appendChild(typeGroup);
686
+
687
+ const universalOpt = document.createElement("option");
688
+ universalOpt.value = "*";
689
+ universalOpt.textContent = "* (Universal - all types)";
690
+ codecTypeSelect.appendChild(universalOpt);
691
+
692
+ if (prev) codecTypeSelect.value = prev;
693
+ }
694
+
695
+ // 3. Render active codecs table
696
+ if (activeCodecsTbody) {
697
+ activeCodecsTbody.innerHTML = "";
698
+ const store = state.codecs || {};
699
+ const entries = [];
700
+ for (const [codeHash, list] of Object.entries(store)) {
701
+ for (const c of list) {
702
+ entries.push({ codeHash, ...c });
703
+ }
704
+ }
705
+
706
+ if (entries.length === 0) {
707
+ const tr = document.createElement("tr");
708
+ tr.innerHTML = `<td colspan="5" style="text-align:center;color:var(--text-muted);padding:16px;">No codecs currently loaded. Load a built-in codec or register a custom serializer/deserializer below.</td>`;
709
+ activeCodecsTbody.appendChild(tr);
710
+ return;
711
+ }
712
+
713
+ // Invert type aliases for display
714
+ const hashToAlias = {};
715
+ for (const [name, hash] of Object.entries(state.typeAliases || {})) {
716
+ hashToAlias[hash] = name;
717
+ }
718
+
719
+ for (const entry of entries) {
720
+ const tr = document.createElement("tr");
721
+
722
+ const nameTd = document.createElement("td");
723
+ nameTd.className = "codec-cell-name";
724
+ nameTd.textContent = entry.name;
725
+ tr.appendChild(nameTd);
726
+
727
+ const typeTd = document.createElement("td");
728
+ typeTd.className = "codec-cell-type";
729
+ if (entry.codeHash === "*") {
730
+ typeTd.innerHTML = `<span class="badge-type universal">Universal (*)</span>`;
731
+ } else {
732
+ const alias = hashToAlias[entry.codeHash];
733
+ typeTd.innerHTML = `<span class="badge-type">${alias ? `$${alias} ` : ""}<code title="${entry.codeHash}">${entry.codeHash.slice(0, 10)}...</code></span>`;
734
+ }
735
+ tr.appendChild(typeTd);
736
+
737
+ const capTd = document.createElement("td");
738
+ const caps = [];
739
+ if (typeof entry.print === "function") caps.push(`<span class="cap-badge print">Serializer</span>`);
740
+ if (typeof entry.parse === "function") caps.push(`<span class="cap-badge parse">Deserializer</span>`);
741
+ capTd.innerHTML = caps.join(" ");
742
+ tr.appendChild(capTd);
743
+
744
+ const srcTd = document.createElement("td");
745
+ srcTd.className = "codec-cell-src";
746
+ srcTd.textContent = entry.source ? (entry.source === "built-in" ? "Built-in" : entry.source.split("/").pop()) : "Custom";
747
+ tr.appendChild(srcTd);
748
+
749
+ const actTd = document.createElement("td");
750
+ const unloadBtn = document.createElement("button");
751
+ unloadBtn.className = "btn btn-sm btn-danger";
752
+ unloadBtn.textContent = "Unload";
753
+ unloadBtn.onclick = () => {
754
+ executeCommand(`:codec unload ${entry.name}`);
755
+ renderCodecsModal();
756
+ };
757
+ actTd.appendChild(unloadBtn);
758
+ tr.appendChild(actTd);
759
+
760
+ activeCodecsTbody.appendChild(tr);
761
+ }
762
+ }
763
+ }
764
+
765
+ function isCodecRegistered(name) {
766
+ const store = state.codecs || {};
767
+ for (const list of Object.values(store)) {
768
+ if (list.some(c => c.name === name)) return true;
769
+ }
770
+ return false;
771
+ }
772
+
773
+ function applyCodecPreset(presetKey) {
774
+ const preset = CODEC_PRESETS[presetKey];
775
+ if (!preset) return;
776
+ const nameInput = document.getElementById("codec-custom-name");
777
+ const parseArea = document.getElementById("codec-custom-parse");
778
+ const printArea = document.getElementById("codec-custom-print");
779
+
780
+ if (nameInput) nameInput.value = preset.name;
781
+ if (parseArea) parseArea.value = preset.parse;
782
+ if (printArea) printArea.value = preset.print;
783
+ if (codecTypeSelect) {
784
+ if (preset.type) {
785
+ codecTypeSelect.value = preset.type;
786
+ }
787
+ }
788
+ }
789
+
790
+ function registerCustomCodecFromForm() {
791
+ const nameInput = document.getElementById("codec-custom-name");
792
+ const parseArea = document.getElementById("codec-custom-parse");
793
+ const printArea = document.getElementById("codec-custom-print");
794
+ const statusBox = document.getElementById("codec-studio-status");
795
+
796
+ const name = nameInput?.value.trim();
797
+ const rawType = codecTypeSelect?.value.trim() || "*";
798
+ const parseCode = parseArea?.value.trim();
799
+ const printCode = printArea?.value.trim();
800
+
801
+ if (!name) {
802
+ if (statusBox) statusBox.innerHTML = `<span class="line-error">Codec name is required.</span>`;
803
+ return;
804
+ }
805
+
806
+ try {
807
+ let parseFn = null;
808
+ let printFn = null;
809
+
810
+ if (parseCode) {
811
+ parseFn = new Function(
812
+ "text", "context",
813
+ `const { Value, isProduct, isVariant } = globalThis;\n${parseCode}`
814
+ );
815
+ }
816
+
817
+ if (printCode) {
818
+ printFn = new Function(
819
+ "value", "context",
820
+ `const { Value, isProduct, isVariant } = globalThis;\n${printCode}`
821
+ );
822
+ }
823
+
824
+ let codeHash = rawType === "*" ? "*" : state.typeAliases[rawType] || rawType;
825
+ if (!codeHash.startsWith("@") && codeHash !== "*") {
826
+ throw new Error(`Cannot resolve type '${rawType}' to a canonical code hash. Define the type first (e.g. :type ${rawType} = ...).`);
827
+ }
828
+
829
+ registerCodec(state, {
830
+ name,
831
+ codes: [codeHash],
832
+ universal: codeHash === "*",
833
+ parse: parseFn,
834
+ print: printFn
835
+ }, "<custom-studio>");
836
+
837
+ if (statusBox) {
838
+ statusBox.innerHTML = `<span class="line-output">✓ Successfully registered codec <b>${name}</b> for ${codeHash === "*" ? "all types" : codeHash}!</span>`;
839
+ }
840
+
841
+ appendSystemMessage(`⚙ Registered custom codec <b>${name}</b> (serializer/deserializer) for ${codeHash === "*" ? "all types" : codeHash}.`);
842
+ renderCodecsModal();
843
+ } catch (err) {
844
+ if (statusBox) {
845
+ statusBox.innerHTML = `<span class="line-error">Error: ${escapeHtml(err.message)}</span>`;
846
+ }
847
+ }
848
+ }
849
+
850
+ function testCustomParse() {
851
+ const parseArea = document.getElementById("codec-custom-parse");
852
+ const testInput = document.getElementById("codec-test-input");
853
+ const statusBox = document.getElementById("codec-studio-status");
854
+
855
+ try {
856
+ const parseFn = new Function(
857
+ "text", "context",
858
+ `const { Value, isProduct, isVariant } = globalThis;\n${parseArea.value}`
859
+ );
860
+ const result = parseFn(testInput.value, { Value, isProduct, isVariant, state });
861
+ if (statusBox) {
862
+ statusBox.innerHTML = `<span class="line-output"><b>Parse Result:</b> ${escapeHtml(JSON.stringify(result, null, 2))}</span>`;
863
+ }
864
+ } catch (err) {
865
+ if (statusBox) {
866
+ statusBox.innerHTML = `<span class="line-error">Parse Error: ${escapeHtml(err.message)}</span>`;
867
+ }
868
+ }
869
+ }
870
+
871
+ function testCustomPrint() {
872
+ const printArea = document.getElementById("codec-custom-print");
873
+ const statusBox = document.getElementById("codec-studio-status");
874
+
875
+ try {
876
+ const printFn = new Function(
877
+ "value", "context",
878
+ `const { Value, isProduct, isVariant } = globalThis;\n${printArea.value}`
879
+ );
880
+ const result = printFn(state.value, { Value, isProduct, isVariant, state });
881
+ if (statusBox) {
882
+ statusBox.innerHTML = `<span class="line-output"><b>Print Output:</b> ${escapeHtml(String(result))}</span>`;
883
+ }
884
+ } catch (err) {
885
+ if (statusBox) {
886
+ statusBox.innerHTML = `<span class="line-error">Print Error: ${escapeHtml(err.message)}</span>`;
887
+ }
888
+ }
889
+ }
890
+
891
+ function saveCustomCodecToVfs() {
892
+ const nameInput = document.getElementById("codec-custom-name");
893
+ const parseArea = document.getElementById("codec-custom-parse");
894
+ const printArea = document.getElementById("codec-custom-print");
895
+ const statusBox = document.getElementById("codec-studio-status");
896
+
897
+ const name = nameInput?.value.trim() || "custom";
898
+ const rawType = codecTypeSelect?.value.trim() || "*";
899
+ const codeHash = rawType === "*" ? "*" : state.typeAliases[rawType] || rawType;
900
+ const fileName = `codecs/${name}-codec.mjs`;
901
+
902
+ const fileContent = `// Custom K Codec: ${name}
903
+ import { Value, isProduct, isVariant } from "../Value.mjs";
904
+
905
+ export const name = ${JSON.stringify(name)};
906
+ export const codes = [${JSON.stringify(codeHash)}];
907
+ export const universal = ${codeHash === "*"};
908
+
909
+ export function parse(text, context) {
910
+ ${parseArea?.value || ""}
911
+ }
912
+
913
+ export function print(value, context) {
914
+ ${printArea?.value || ""}
915
+ }
916
+ `;
917
+
918
+ setVfsFile(fileName, fileContent);
919
+ if (statusBox) {
920
+ statusBox.innerHTML = `<span class="line-output">💾 Saved codec file to VFS as <b>${fileName}</b></span>`;
921
+ }
922
+ appendSystemMessage(`💾 Saved codec file to VFS: <b>${fileName}</b>. You can reload it anytime via <code>:codec load ${fileName}</code>.`);
923
+ }
924
+
925
+ // ==========================================
926
+ // CODEC INPUT POPUP SUBSYSTEM (:input)
927
+ // ==========================================
928
+
929
+ const CODEC_SAMPLES = {
930
+ int: ["0", "10", "42", "-15", "1000000"],
931
+ utf8: ["hello", "hello world", "k repl", "λ"],
932
+ json: ['{"name":"Alice"}', '{"x":10,"y":20}', 'true', '[1, 2, 3]'],
933
+ ieee: ["0.0", "3.14159", "-2.718", "1e6"],
934
+ yn: ["yes", "no", "true", "false"],
935
+ hex: ["0x2A", "0xFF", "0x1000", "-0x10"],
936
+ currency: ["$10.00", "$42.50", "$99.99"]
937
+ };
938
+
939
+ export function openInputPopup(preselectedType = null, preselectedCodec = null) {
940
+ if (!inputPopupModal) return;
941
+
942
+ populateInputPopupDropdowns(preselectedType, preselectedCodec);
943
+
944
+ if (inputPopupText) {
945
+ inputPopupText.value = "";
946
+ }
947
+ updateLiveValidation();
948
+
949
+ inputPopupModal.classList.add("open");
950
+ setTimeout(() => {
951
+ if (inputPopupText) inputPopupText.focus();
952
+ }, 50);
953
+ }
954
+
955
+ export function closeInputPopup() {
956
+ if (inputPopupModal) {
957
+ inputPopupModal.classList.remove("open");
958
+ }
959
+ if (inputEl) inputEl.focus();
960
+ }
961
+
962
+ export function cancelInputPopup() {
963
+ if (state.pendingInput) {
964
+ state.pendingInput = null;
965
+ updatePrompt();
966
+ appendSystemMessage("Input cancelled.");
967
+ }
968
+ closeInputPopup();
969
+ }
970
+
971
+ function renderInputSamples(codecName) {
972
+ if (!inputSamplesList) return;
973
+ inputSamplesList.innerHTML = "";
974
+ const samples = CODEC_SAMPLES[codecName] || ["()"];
975
+ for (const s of samples) {
976
+ const btn = document.createElement("span");
977
+ btn.className = "sample-pill";
978
+ btn.textContent = s;
979
+ btn.title = `Click to insert sample "${s}"`;
980
+ btn.onclick = () => {
981
+ if (inputPopupText) {
982
+ inputPopupText.value = s;
983
+ inputPopupText.focus();
984
+ updateLiveValidation();
985
+ }
986
+ };
987
+ inputSamplesList.appendChild(btn);
988
+ }
989
+ }
990
+
991
+ function updateInputPopupHint() {
992
+ if (!inputPopupHint) return;
993
+ if (state.pendingInput) {
994
+ const rawType = inputPopupType?.value || state.pendingInput.codeHash;
995
+ const codecName = inputPopupCodec?.value || state.pendingInput.codecName;
996
+ inputPopupHint.innerHTML = `Enter input value for <b>$${escapeHtml(rawType)}</b> using codec <b>${escapeHtml(codecName || "default")}</b>:`;
997
+ } else {
998
+ inputPopupHint.innerHTML = `Choose target type &amp; deserializer, then enter input string:`;
999
+ }
1000
+ }
1001
+
1002
+ function updateInputPopupCodecs() {
1003
+ if (!inputPopupCodec) return;
1004
+ const rawType = inputPopupType?.value || "*";
1005
+ const codeHash = rawType === "*" ? "*" : state.typeAliases?.[rawType] || rawType;
1006
+ const prev = inputPopupCodec.value;
1007
+
1008
+ inputPopupCodec.innerHTML = "";
1009
+ const matchingCodecs = [];
1010
+ const store = state.codecs || {};
1011
+
1012
+ const candidates = [
1013
+ ...(store[codeHash] || []),
1014
+ ...(codeHash !== "*" ? (store["*"] || []) : [])
1015
+ ];
1016
+
1017
+ const seen = new Set();
1018
+ for (const c of candidates) {
1019
+ if (typeof c.parse === "function" && !seen.has(c.name)) {
1020
+ seen.add(c.name);
1021
+ matchingCodecs.push(c.name);
1022
+ }
1023
+ }
1024
+
1025
+ if (matchingCodecs.length === 0) {
1026
+ if (rawType === "int") matchingCodecs.push("int");
1027
+ if (rawType === "bool") matchingCodecs.push("yn");
1028
+ if (rawType === "string") matchingCodecs.push("utf8");
1029
+ matchingCodecs.push("json");
1030
+ }
1031
+
1032
+ for (const name of matchingCodecs) {
1033
+ const opt = document.createElement("option");
1034
+ opt.value = name;
1035
+ opt.textContent = name;
1036
+ inputPopupCodec.appendChild(opt);
1037
+ }
1038
+
1039
+ if (prev && matchingCodecs.includes(prev)) {
1040
+ inputPopupCodec.value = prev;
1041
+ } else if (matchingCodecs.length > 0) {
1042
+ inputPopupCodec.value = matchingCodecs[0];
1043
+ }
1044
+
1045
+ renderInputSamples(inputPopupCodec.value);
1046
+ updateInputPopupHint();
1047
+ updateLiveValidation();
1048
+ }
1049
+
1050
+ function populateInputPopupDropdowns(preselectedType = null, preselectedCodec = null) {
1051
+ if (!inputPopupType) return;
1052
+
1053
+ let prevType = preselectedType;
1054
+ if (!prevType && state.pendingInput) {
1055
+ const foundAlias = Object.entries(state.typeAliases || {}).find(([, h]) => h === state.pendingInput.codeHash);
1056
+ prevType = foundAlias ? foundAlias[0] : state.pendingInput.codeHash;
1057
+ }
1058
+ if (!prevType) {
1059
+ prevType = inputPopupType.value;
1060
+ }
1061
+
1062
+ inputPopupType.innerHTML = "";
1063
+
1064
+ const aliases = Object.entries(state.typeAliases || {}).sort(([a], [b]) => a.localeCompare(b));
1065
+ for (const [alias, hash] of aliases) {
1066
+ const opt = document.createElement("option");
1067
+ opt.value = alias;
1068
+ opt.textContent = `$${alias} (${hash.slice(0, 10)}...)`;
1069
+ inputPopupType.appendChild(opt);
1070
+ }
1071
+
1072
+ const universalOpt = document.createElement("option");
1073
+ universalOpt.value = "*";
1074
+ universalOpt.textContent = "* (Universal / Any)";
1075
+ inputPopupType.appendChild(universalOpt);
1076
+
1077
+ if (prevType && (aliases.some(([a]) => a === prevType) || prevType === "*")) {
1078
+ inputPopupType.value = prevType;
1079
+ } else if (aliases.length > 0) {
1080
+ inputPopupType.value = aliases[0][0];
1081
+ }
1082
+
1083
+ updateInputPopupCodecs();
1084
+
1085
+ const targetCodec = preselectedCodec || state.pendingInput?.codecName;
1086
+ if (targetCodec && inputPopupCodec) {
1087
+ inputPopupCodec.value = targetCodec;
1088
+ renderInputSamples(targetCodec);
1089
+ }
1090
+ }
1091
+
1092
+ function updateLiveValidation() {
1093
+ if (!inputPopupStatus) return;
1094
+ const text = inputPopupText ? inputPopupText.value : "";
1095
+ if (!text) {
1096
+ inputPopupStatus.innerHTML = `<span style="color:var(--text-muted);font-size:11px;">Ready for input.</span>`;
1097
+ return;
1098
+ }
1099
+
1100
+ const rawType = inputPopupType?.value;
1101
+ const codecName = inputPopupCodec?.value;
1102
+ if (!rawType) {
1103
+ inputPopupStatus.innerHTML = `<span class="line-warning" style="margin:0;padding:2px 6px;">Select a target type first.</span>`;
1104
+ return;
1105
+ }
1106
+
1107
+ const codeHash = rawType === "*" ? "*" : state.typeAliases?.[rawType] || rawType;
1108
+ try {
1109
+ const codec = resolveCodec(state, codeHash, codecName || null, "parse");
1110
+ const pattern = codeHashToPattern(codeHash, (h) => state.codes?.[h]);
1111
+ const parsed = codec.parse(text, {
1112
+ codeHash,
1113
+ pattern,
1114
+ state,
1115
+ Value,
1116
+ isProduct,
1117
+ isVariant
1118
+ });
1119
+ const repr = parsed instanceof Value ? parsed.toJSON() : (typeof parsed === "object" ? JSON.stringify(parsed) : String(parsed));
1120
+ inputPopupStatus.innerHTML = `<span class="line-output" style="margin:0;padding:2px 6px;">✓ Valid: <code>${escapeHtml(String(repr))}</code></span>`;
1121
+ } catch (err) {
1122
+ inputPopupStatus.innerHTML = `<span class="line-warning" style="margin:0;padding:2px 6px;">⚠ ${escapeHtml(err.message || String(err))}</span>`;
1123
+ }
1124
+ }
1125
+
1126
+ export async function submitInputPopup() {
1127
+ const text = inputPopupText?.value ?? "";
1128
+
1129
+ // If state.pendingInput is not set, configure it from the popup selections
1130
+ if (!state.pendingInput) {
1131
+ const rawType = inputPopupType?.value;
1132
+ const codecName = inputPopupCodec?.value;
1133
+ if (!rawType) {
1134
+ if (inputPopupStatus) inputPopupStatus.innerHTML = `<span class="line-error">Please select a target type.</span>`;
1135
+ return;
1136
+ }
1137
+ const codeHash = rawType === "*" ? "*" : state.typeAliases?.[rawType] || rawType;
1138
+ try {
1139
+ const codec = resolveCodec(state, codeHash, codecName || null, "parse");
1140
+ state.pendingInput = { codeHash, codecName: codec.name, promptName: codec.name };
1141
+ } catch (err) {
1142
+ if (inputPopupStatus) inputPopupStatus.innerHTML = `<span class="line-error">${escapeHtml(err.message)}</span>`;
1143
+ return;
1144
+ }
1145
+ }
1146
+
1147
+ closeInputPopup();
1148
+ await executeCommand(text);
1149
+ }
1150
+
1151
+ // Initialization on DOMContentLoaded
1152
+ export function initRepl() {
1153
+ outputEl = document.getElementById("terminal-output");
1154
+ inputEl = document.getElementById("repl-input");
1155
+ promptEl = document.getElementById("prompt-label");
1156
+ statusDot = document.getElementById("status-dot");
1157
+ statusText = document.getElementById("status-text");
1158
+ autocompleteEl = document.getElementById("autocomplete-popup");
1159
+ vfsModal = document.getElementById("vfs-modal");
1160
+ vfsListEl = document.getElementById("vfs-files-list");
1161
+ vfsPreviewEl = document.getElementById("vfs-preview-content");
1162
+ vfsPreviewNameEl = document.getElementById("vfs-preview-name");
1163
+ helpModal = document.getElementById("help-modal");
1164
+ dropOverlay = document.getElementById("drop-overlay");
1165
+ codecsModal = document.getElementById("codecs-modal");
1166
+ codecsBadgeEl = document.getElementById("codecs-count-badge");
1167
+ activeCodecsTbody = document.getElementById("active-codecs-tbody");
1168
+ codecTypeSelect = document.getElementById("codec-custom-type");
1169
+
1170
+ inputPopupModal = document.getElementById("input-popup-modal");
1171
+ inputPopupType = document.getElementById("input-popup-type");
1172
+ inputPopupCodec = document.getElementById("input-popup-codec");
1173
+ inputPopupText = document.getElementById("input-popup-text");
1174
+ inputPopupStatus = document.getElementById("input-popup-status");
1175
+ inputPopupHint = document.getElementById("input-popup-hint");
1176
+ inputSamplesList = document.getElementById("input-samples-list");
1177
+
1178
+ updatePrompt();
1179
+
1180
+ // Welcome message
1181
+ appendSystemMessage(`
1182
+ <div class="welcome-banner">
1183
+ <div class="welcome-title">λ k interactive repl</div>
1184
+ <div class="welcome-desc">First-order partial functions over algebraic data types &bull; WebAssembly execution engine &bull; Custom Serializers &amp; Deserializers (:codecs)</div>
1185
+ <div class="welcome-tips">
1186
+ <span>💡 Try: <a href="javascript:void(0)" class="quick-link" data-code=":load core.k">:load core.k</a></span>
1187
+ <span>• <a href="javascript:void(0)" class="quick-link" data-code=":codec load int">:codec load int</a></span>
1188
+ <span>• <a href="javascript:void(0)" class="quick-link" data-code="10 int">10 int</a></span>
1189
+ <span>• <a href="javascript:void(0)" class="quick-link" data-code="{10 int x, 5 int y} plus">{10 int x, 5 int y} plus</a></span>
1190
+ <span>• <a href="javascript:void(0)" class="quick-link" data-code=":codecs">:codecs</a></span>
1191
+ <span>• <a href="javascript:void(0)" class="quick-link" data-code=":help">:help</a></span>
1192
+ </div>
1193
+ </div>
1194
+ `, false, true);
1195
+
1196
+ // Delegate quick links
1197
+ document.addEventListener("click", (e) => {
1198
+ const link = e.target.closest(".quick-link");
1199
+ if (link && link.dataset.code) {
1200
+ e.preventDefault();
1201
+ inputEl.value = link.dataset.code;
1202
+ adjustInputHeight();
1203
+ executeCommand(link.dataset.code);
1204
+ }
1205
+ });
1206
+
1207
+ // Input events
1208
+ inputEl.addEventListener("input", () => {
1209
+ adjustInputHeight();
1210
+ hideAutocomplete();
1211
+ });
1212
+
1213
+ inputEl.addEventListener("keydown", (e) => {
1214
+ // Autocomplete popup navigation
1215
+ if (autocompleteEl && autocompleteEl.classList.contains("visible")) {
1216
+ if (e.key === "ArrowDown") {
1217
+ e.preventDefault();
1218
+ activeMatchIndex = (activeMatchIndex + 1) % currentMatches.length;
1219
+ Array.from(autocompleteEl.children).forEach((child, i) => {
1220
+ child.classList.toggle("active", i === activeMatchIndex);
1221
+ });
1222
+ return;
1223
+ }
1224
+ if (e.key === "ArrowUp") {
1225
+ e.preventDefault();
1226
+ activeMatchIndex = (activeMatchIndex - 1 + currentMatches.length) % currentMatches.length;
1227
+ Array.from(autocompleteEl.children).forEach((child, i) => {
1228
+ child.classList.toggle("active", i === activeMatchIndex);
1229
+ });
1230
+ return;
1231
+ }
1232
+ if (e.key === "Enter" || e.key === "Tab") {
1233
+ e.preventDefault();
1234
+ if (activeMatchIndex >= 0 && activeMatchIndex < currentMatches.length) {
1235
+ applyAutocomplete(currentMatches[activeMatchIndex]);
1236
+ }
1237
+ return;
1238
+ }
1239
+ if (e.key === "Escape") {
1240
+ hideAutocomplete();
1241
+ return;
1242
+ }
1243
+ }
1244
+
1245
+ // Tab for completion
1246
+ if (e.key === "Tab") {
1247
+ e.preventDefault();
1248
+ handleAutocomplete();
1249
+ return;
1250
+ }
1251
+
1252
+ // Enter to run
1253
+ if (e.key === "Enter") {
1254
+ if (e.shiftKey) {
1255
+ // Shift+Enter: let default newline happen
1256
+ setTimeout(adjustInputHeight, 0);
1257
+ return;
1258
+ }
1259
+
1260
+ if (e.ctrlKey || e.metaKey) {
1261
+ // Force run
1262
+ e.preventDefault();
1263
+ const code = inputEl.value;
1264
+ inputEl.value = "";
1265
+ adjustInputHeight();
1266
+ executeCommand(code);
1267
+ return;
1268
+ }
1269
+
1270
+ // Normal Enter
1271
+ const val = inputEl.value;
1272
+ // Check if explicit continuation with \
1273
+ if (lineHasExplicitContinuation(val)) {
1274
+ setTimeout(adjustInputHeight, 0);
1275
+ return;
1276
+ }
1277
+
1278
+ // Check if incomplete snippet
1279
+ if (!val.trim().startsWith(":") && !lineTerminatesSnippet(val)) {
1280
+ try {
1281
+ const analysis = analyzeRawSnippet(val);
1282
+ if (analysis.kind === "incomplete") {
1283
+ // Let it wrap to next line
1284
+ setTimeout(adjustInputHeight, 0);
1285
+ return;
1286
+ }
1287
+ } catch {
1288
+ // Syntax error or complete, let executeCommand display error
1289
+ }
1290
+ }
1291
+
1292
+ e.preventDefault();
1293
+ const code = inputEl.value;
1294
+ inputEl.value = "";
1295
+ adjustInputHeight();
1296
+ executeCommand(code);
1297
+ return;
1298
+ }
1299
+
1300
+ // Up / Down history
1301
+ if (e.key === "ArrowUp") {
1302
+ const isFirstLine = inputEl.value.slice(0, inputEl.selectionStart).indexOf("\n") === -1;
1303
+ if (isFirstLine) {
1304
+ e.preventDefault();
1305
+ navigateHistory("up");
1306
+ return;
1307
+ }
1308
+ }
1309
+ if (e.key === "ArrowDown") {
1310
+ const isLastLine = inputEl.value.slice(inputEl.selectionEnd).indexOf("\n") === -1;
1311
+ if (isLastLine) {
1312
+ e.preventDefault();
1313
+ navigateHistory("down");
1314
+ return;
1315
+ }
1316
+ }
1317
+
1318
+ // Ctrl+L: Clear
1319
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "l") {
1320
+ e.preventDefault();
1321
+ clearOutput();
1322
+ return;
1323
+ }
1324
+
1325
+ // Escape: clear input
1326
+ if (e.key === "Escape") {
1327
+ inputEl.value = "";
1328
+ adjustInputHeight();
1329
+ return;
1330
+ }
1331
+ });
1332
+
1333
+ // Run button
1334
+ const runBtn = document.getElementById("btn-run");
1335
+ if (runBtn) {
1336
+ runBtn.onclick = () => {
1337
+ const code = inputEl.value;
1338
+ if (code.trim()) {
1339
+ inputEl.value = "";
1340
+ adjustInputHeight();
1341
+ executeCommand(code);
1342
+ }
1343
+ inputEl.focus();
1344
+ };
1345
+ }
1346
+
1347
+ // Clear button
1348
+ const clearBtn = document.getElementById("btn-clear");
1349
+ if (clearBtn) clearBtn.onclick = clearOutput;
1350
+
1351
+ // Reset button
1352
+ const resetBtn = document.getElementById("btn-reset");
1353
+ if (resetBtn) {
1354
+ resetBtn.onclick = () => {
1355
+ executeCommand(":reset");
1356
+ };
1357
+ }
1358
+
1359
+ // Help button
1360
+ const helpBtn = document.getElementById("btn-help");
1361
+ if (helpBtn) {
1362
+ helpBtn.onclick = () => {
1363
+ if (helpModal) helpModal.classList.add("open");
1364
+ };
1365
+ }
1366
+
1367
+ const closeHelpBtn = document.getElementById("btn-close-help");
1368
+ if (closeHelpBtn) {
1369
+ closeHelpBtn.onclick = () => {
1370
+ if (helpModal) helpModal.classList.remove("open");
1371
+ };
1372
+ }
1373
+
1374
+ // VFS Explorer button
1375
+ const vfsBtn = document.getElementById("btn-vfs");
1376
+ if (vfsBtn) vfsBtn.onclick = openVfsModal;
1377
+
1378
+ const closeVfsBtn = document.getElementById("btn-close-vfs");
1379
+ if (closeVfsBtn) closeVfsBtn.onclick = closeVfsModal;
1380
+
1381
+ const loadVfsFileBtn = document.getElementById("btn-vfs-load");
1382
+ if (loadVfsFileBtn) loadVfsFileBtn.onclick = loadPreviewedFile;
1383
+
1384
+ const downloadVfsFileBtn = document.getElementById("btn-vfs-download");
1385
+ if (downloadVfsFileBtn) downloadVfsFileBtn.onclick = downloadPreviewedFile;
1386
+
1387
+ // Codecs Subsystem button & modal
1388
+ const codecsBtn = document.getElementById("btn-codecs");
1389
+ if (codecsBtn) codecsBtn.onclick = openCodecsModal;
1390
+
1391
+ const closeCodecsBtn = document.getElementById("btn-close-codecs");
1392
+ if (closeCodecsBtn) closeCodecsBtn.onclick = closeCodecsModal;
1393
+
1394
+ // Toggle built-in codecs buttons
1395
+ ["int", "utf8", "json", "ieee"].forEach(name => {
1396
+ const btn = document.getElementById(`btn-toggle-codec-${name}`);
1397
+ if (btn) {
1398
+ btn.onclick = () => {
1399
+ if (isCodecRegistered(name)) {
1400
+ executeCommand(`:codec unload ${name}`);
1401
+ } else {
1402
+ executeCommand(`:codec load ${name}`);
1403
+ }
1404
+ renderCodecsModal();
1405
+ };
1406
+ }
1407
+ });
1408
+
1409
+ // Codec Studio controls
1410
+ const presetSelect = document.getElementById("codec-preset-select");
1411
+ if (presetSelect) {
1412
+ presetSelect.addEventListener("change", () => {
1413
+ if (presetSelect.value) applyCodecPreset(presetSelect.value);
1414
+ });
1415
+ }
1416
+
1417
+ const btnRegisterCodec = document.getElementById("btn-register-codec");
1418
+ if (btnRegisterCodec) btnRegisterCodec.onclick = registerCustomCodecFromForm;
1419
+
1420
+ const btnTestParse = document.getElementById("btn-test-parse");
1421
+ if (btnTestParse) btnTestParse.onclick = testCustomParse;
1422
+
1423
+ const btnTestPrint = document.getElementById("btn-test-print");
1424
+ if (btnTestPrint) btnTestPrint.onclick = testCustomPrint;
1425
+
1426
+ const btnSaveCodecVfs = document.getElementById("btn-save-codec-vfs");
1427
+ if (btnSaveCodecVfs) btnSaveCodecVfs.onclick = saveCustomCodecToVfs;
1428
+
1429
+ // Codec Input Popup controls
1430
+ const btnInputNavbar = document.getElementById("btn-input-popup");
1431
+ if (btnInputNavbar) btnInputNavbar.onclick = () => openInputPopup();
1432
+
1433
+ const btnCloseInput = document.getElementById("btn-close-input-popup");
1434
+ if (btnCloseInput) btnCloseInput.onclick = cancelInputPopup;
1435
+
1436
+ const btnCancelInput = document.getElementById("btn-cancel-input");
1437
+ if (btnCancelInput) btnCancelInput.onclick = cancelInputPopup;
1438
+
1439
+ const btnSubmitInput = document.getElementById("btn-submit-input");
1440
+ if (btnSubmitInput) btnSubmitInput.onclick = submitInputPopup;
1441
+
1442
+ if (inputPopupType) {
1443
+ inputPopupType.addEventListener("change", updateInputPopupCodecs);
1444
+ }
1445
+
1446
+ if (inputPopupCodec) {
1447
+ inputPopupCodec.addEventListener("change", () => {
1448
+ renderInputSamples(inputPopupCodec.value);
1449
+ updateLiveValidation();
1450
+ });
1451
+ }
1452
+
1453
+ if (inputPopupText) {
1454
+ inputPopupText.addEventListener("input", updateLiveValidation);
1455
+ inputPopupText.addEventListener("keydown", (e) => {
1456
+ if (e.key === "Enter" && (e.ctrlKey || e.metaKey || !e.shiftKey)) {
1457
+ e.preventDefault();
1458
+ submitInputPopup();
1459
+ } else if (e.key === "Escape") {
1460
+ e.preventDefault();
1461
+ cancelInputPopup();
1462
+ }
1463
+ });
1464
+ }
1465
+
1466
+ // Export .klib button
1467
+ const exportBtn = document.getElementById("btn-export");
1468
+ if (exportBtn) exportBtn.onclick = exportKlib;
1469
+
1470
+ // Example selector
1471
+ const exampleSelect = document.getElementById("example-select");
1472
+ const loadExampleBtn = document.getElementById("btn-load-example");
1473
+ if (exampleSelect && loadExampleBtn) {
1474
+ loadExampleBtn.onclick = () => {
1475
+ const val = exampleSelect.value;
1476
+ if (!val) return;
1477
+ if (val.startsWith("expr:")) {
1478
+ const expr = val.slice("expr:".length);
1479
+ inputEl.value = expr;
1480
+ adjustInputHeight();
1481
+ executeCommand(expr);
1482
+ } else {
1483
+ executeCommand(`:load ${val}`);
1484
+ }
1485
+ inputEl.focus();
1486
+ };
1487
+ }
1488
+
1489
+ // File Upload
1490
+ const fileInput = document.getElementById("file-upload");
1491
+ if (fileInput) {
1492
+ fileInput.addEventListener("change", (e) => {
1493
+ const file = e.target.files?.[0];
1494
+ if (file) handleFileUpload(file);
1495
+ fileInput.value = "";
1496
+ });
1497
+ }
1498
+
1499
+ // Drag and drop onto window
1500
+ window.addEventListener("dragover", (e) => {
1501
+ e.preventDefault();
1502
+ if (dropOverlay) dropOverlay.classList.add("visible");
1503
+ });
1504
+
1505
+ window.addEventListener("dragleave", (e) => {
1506
+ if (e.target === dropOverlay || !e.relatedTarget) {
1507
+ if (dropOverlay) dropOverlay.classList.remove("visible");
1508
+ }
1509
+ });
1510
+
1511
+ window.addEventListener("drop", (e) => {
1512
+ e.preventDefault();
1513
+ if (dropOverlay) dropOverlay.classList.remove("visible");
1514
+ const file = e.dataTransfer?.files?.[0];
1515
+ if (file) handleFileUpload(file);
1516
+ });
1517
+
1518
+ // Modal close when clicking backdrop
1519
+ window.addEventListener("click", (e) => {
1520
+ if (e.target === vfsModal) closeVfsModal();
1521
+ if (e.target === helpModal) helpModal.classList.remove("open");
1522
+ if (e.target === codecsModal) closeCodecsModal();
1523
+ if (e.target === inputPopupModal) cancelInputPopup();
1524
+ });
1525
+
1526
+ // Focus input when clicking anywhere in the input bar
1527
+ const inputBar = document.querySelector(".terminal-input-bar");
1528
+ if (inputBar) {
1529
+ inputBar.addEventListener("click", (e) => {
1530
+ if (inputEl && !e.target.closest("button, select, input, textarea")) {
1531
+ inputEl.focus();
1532
+ }
1533
+ });
1534
+ }
1535
+
1536
+ // Focus input when typing printable characters outside modals and inputs
1537
+ window.addEventListener("keydown", (e) => {
1538
+ if (e.target.matches("input, textarea, select") || document.querySelector(".modal.open")) {
1539
+ return;
1540
+ }
1541
+ if (e.ctrlKey || e.metaKey || e.altKey) {
1542
+ return;
1543
+ }
1544
+ if (e.key.length === 1 && inputEl) {
1545
+ inputEl.focus();
1546
+ }
1547
+ });
1548
+
1549
+ // Apply initial yn preset to custom form
1550
+ applyCodecPreset("yn");
1551
+
1552
+ inputEl.focus();
1553
+ }
1554
+
1555
+ function clearOutput() {
1556
+ if (outputEl) {
1557
+ outputEl.innerHTML = "";
1558
+ appendSystemMessage("<span style='color:var(--color-muted)'>Terminal cleared.</span>");
1559
+ }
1560
+ }
1561
+
1562
+ // Auto-run if in browser
1563
+ if (typeof window !== "undefined") {
1564
+ window.kRepl = {
1565
+ executeCommand,
1566
+ initRepl,
1567
+ getState: () => state,
1568
+ getVfsFile,
1569
+ setVfsFile,
1570
+ getAllVfsFiles,
1571
+ openCodecsModal,
1572
+ openInputPopup,
1573
+ closeInputPopup,
1574
+ submitInputPopup,
1575
+ cancelInputPopup,
1576
+ registerCodec,
1577
+ unregisterCodec
1578
+ };
1579
+ if (document.readyState !== "loading") {
1580
+ initRepl();
1581
+ } else {
1582
+ document.addEventListener("DOMContentLoaded", initRepl);
1583
+ }
1584
+ }