@fraczak/k 6.3.3 → 6.5.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.
- package/CHANGELOG.md +17 -0
- package/DOCS/CODECS.md +258 -0
- package/DOCS/DICTIONARY.md +1172 -0
- package/DOCS/KIR_SPECIALIZATION_DESIGN.md +51 -46
- package/DOCS/KIR_V1.md +221 -0
- package/DOCS/KVM_EXECUTION_MODEL.md +56 -19
- package/DOCS/OBJECT_FILE_AND_PATTERN.md +2 -1
- package/DOCS/PROFILING_AND_TRACING.md +196 -0
- package/DOCS/REPL.md +61 -4
- package/DOCS/WEB_REPL_REQUIREMENTS.md +135 -0
- package/DOCS/dependency-graph.md +144 -0
- package/Examples/arithmetics.k +108 -82
- package/Examples/bnat.k +1 -1
- package/Examples/buda.k +44 -0
- package/Examples/ieee.k +300 -8671
- package/Examples/poly.k +67 -0
- package/README.md +68 -19
- package/Value.mjs +8 -2
- package/augmentation.mjs +9 -7
- package/backend-api.mjs +11 -2
- package/backends/arm64/README.md +104 -0
- package/backends/arm64/bin/k-arm64-compile.mjs +101 -0
- package/backends/arm64/bin/k-arm64-run.mjs +58 -0
- package/backends/arm64/bin/k-arm64.mjs +88 -0
- package/backends/arm64/scripts/run-tests.mjs +41 -0
- package/backends/arm64/src/arm64.mjs +229 -0
- package/backends/arm64/src/cli.mjs +155 -0
- package/backends/arm64/src/kvm2arm64.mjs +906 -0
- package/backends/arm64/src/runtime.c +728 -0
- package/backends/arm64/tests/integration.sh +112 -0
- package/backends/arm64/tests/test-arm64.mjs +240 -0
- package/backends/llvm/README.md +135 -0
- package/backends/llvm/bin/k-llvm-build.mjs +71 -0
- package/backends/llvm/bin/k-llvm-compile.mjs +68 -0
- package/backends/llvm/bin/k-llvm-jit.mjs +160 -0
- package/backends/llvm/bin/k-llvm-run.mjs +90 -0
- package/backends/llvm/runtime/krt.c +1508 -0
- package/backends/llvm/runtime/krt.h +109 -0
- package/backends/llvm/scripts/conformance.mjs +76 -0
- package/backends/llvm/scripts/run-tests.mjs +28 -0
- package/backends/llvm/src/executable.mjs +562 -0
- package/backends/llvm/src/kvm2llvm.mjs +998 -0
- package/backends/llvm/src/llvm.mjs +1092 -0
- package/backends/llvm/tests/composition-driver.c +22 -0
- package/backends/llvm/tests/identity-driver.c +27 -0
- package/backends/llvm/tests/integration.sh +153 -0
- package/backends/llvm/tests/perf-support.mjs +735 -0
- package/backends/llvm/tests/product-driver.c +23 -0
- package/backends/llvm/tests/projection-driver.c +19 -0
- package/backends/llvm/tests/relation-driver.c +29 -0
- package/backends/llvm/tests/test-llvm.mjs +240 -0
- package/backends/llvm/tests/union-driver.c +31 -0
- package/backends/llvm/tests/variant-driver.c +19 -0
- package/backends/llvm/tests/variant-projection-driver.c +21 -0
- package/backends/wasm/DOCS/USE-WEB-ASSEMBLY.md +233 -0
- package/backends/wasm/README.md +97 -0
- package/backends/wasm/TECHNICAL-DEBT.md +58 -0
- package/backends/wasm/bin/k-wasm-compile.mjs +50 -0
- package/backends/wasm/bin/k-wasm-run.mjs +140 -0
- package/backends/wasm/bin/k-wasm.mjs +49 -0
- package/backends/wasm/demo_ieee.mjs +319 -0
- package/backends/wasm/runtime.wat +475 -0
- package/backends/wasm/scripts/run-tests.mjs +41 -0
- package/backends/wasm/src/cli.mjs +133 -0
- package/backends/wasm/src/kvm2wasm.mjs +697 -0
- package/backends/wasm/src/wasm.mjs +1054 -0
- package/backends/wasm/tests/integration.sh +182 -0
- package/backends/wasm/tests/perf-support.mjs +336 -0
- package/backends/wasm/tests/test-wasm.mjs +625 -0
- package/browser/repl-browser.mjs +1584 -0
- package/browser/shims/assert.mjs +19 -0
- package/browser/shims/buffer.mjs +224 -0
- package/browser/shims/crypto.mjs +103 -0
- package/browser/shims/fs.mjs +159 -0
- package/browser/shims/os.mjs +17 -0
- package/browser/shims/path.mjs +54 -0
- package/browser/shims/process.mjs +44 -0
- package/browser/shims/readline.mjs +11 -0
- package/browser/shims/url.mjs +12 -0
- package/browser/vfs-data.mjs +19 -0
- package/codecs/README.md +3 -0
- package/codecs/runtime/show-value.mjs +12 -3
- package/conformance/helper-reuse/case.json +8 -0
- package/conformance/helper-reuse/expected.kv +1 -0
- package/conformance/helper-reuse/input.kv +1 -0
- package/conformance/helper-reuse/program.k +2 -0
- package/conformance/identity/case.json +8 -0
- package/conformance/identity/expected.kv +1 -0
- package/conformance/identity/input.kv +1 -0
- package/conformance/identity/program.k +1 -0
- package/conformance/product-projection/case.json +8 -0
- package/conformance/product-projection/expected.kv +1 -0
- package/conformance/product-projection/input.kv +1 -0
- package/conformance/product-projection/program.k +1 -0
- package/conformance/run.mjs +149 -0
- package/conformance/variant-construction/case.json +8 -0
- package/conformance/variant-construction/expected.kv +1 -0
- package/conformance/variant-construction/input.kv +1 -0
- package/conformance/variant-construction/program.k +1 -0
- package/harness/adapters/arm64.mjs +105 -0
- package/harness/adapters/kvm.mjs +144 -0
- package/harness/adapters/llvm.mjs +118 -0
- package/harness/adapters/native.mjs +77 -0
- package/harness/adapters/wasm.mjs +169 -0
- package/harness/backend.mjs +95 -0
- package/harness/cli.mjs +130 -0
- package/harness/format.mjs +95 -0
- package/harness/index.mjs +25 -0
- package/harness/persistent.mjs +121 -0
- package/harness/registry.mjs +44 -0
- package/harness/suite.mjs +257 -0
- package/harness/suites/poly.mjs +156 -0
- package/harness/trace.mjs +50 -0
- package/harness/util.mjs +92 -0
- package/intrinsics.mjs +41 -0
- package/kir.mjs +399 -0
- package/kvm.mjs +585 -81
- package/objects/README.md +37 -3
- package/objects/compile.mjs +15 -22
- package/objects/inspect.mjs +96 -0
- package/objects/validate.mjs +415 -0
- package/package.json +51 -19
- package/repl-codecs.mjs +105 -6
- package/repl.mjs +335 -49
- package/run.mjs +2 -7
- package/scripts/benchmark.mjs +406 -22
- package/scripts/build-repl-html.mjs +1784 -0
- package/scripts/perf-ieee.mjs +586 -0
- package/scripts/perf-int.mjs +531 -0
- package/scripts/perf-poly.mjs +945 -0
- package/scripts/render-benchmark.mjs +12 -8
- package/scripts/run-tests.mjs +6 -0
- package/tests/integration.sh +19 -2
- package/tests/test-arithmetics.mjs +54 -0
- package/tests/test-ieee-arithmetic.mjs +4 -0
- package/tests/test-kir.mjs +85 -0
- package/tests/test-kvm-polymorphic.mjs +278 -0
- package/tests/test-kvm.mjs +25 -6
- package/tests/test-poly.mjs +129 -0
- package/tests/test-repl-html.mjs +303 -0
- package/tests/test-repl.mjs +144 -1
- package/tests/test-validate-object.mjs +40 -0
- package/a.klib +0 -1
- package/a.kvm +0 -142676
- package/a.wasm +0 -0
- package/benchmark-results.html +0 -532
- package/benchmark-results.json +0 -480
- package/k-vscode/.vscode/launch.json +0 -14
- package/k-vscode/README.md +0 -75
- package/k-vscode/extension.js +0 -273
- package/k-vscode/language-configuration.json +0 -42
- package/k-vscode/package.json +0 -79
- package/k-vscode/snippets/k.json +0 -92
- package/k-vscode/syntaxes/k.tmLanguage.json +0 -599
- package/k-vscode/test/background-ranges.test.js +0 -50
- package/k-vscode/themes/k-contrast-color-theme.json +0 -104
- 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, "<")
|
|
79
|
+
.replace(/>/g, ">")
|
|
80
|
+
.replace(/"/g, """)
|
|
81
|
+
.replace(/'/g, "'");
|
|
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 & 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 • WebAssembly execution engine • Custom Serializers & 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
|
+
}
|