@fraczak/k 6.8.0 → 6.10.1

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 (45) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/CLAUDE.md +1 -1
  3. package/DOCS/BIG_PICTURE.md +3 -5
  4. package/DOCS/CODECS.md +80 -181
  5. package/DOCS/CODEC_ARCHITECTURE.md +194 -0
  6. package/DOCS/REPL.md +69 -51
  7. package/Examples/arithmetics.k +1 -0
  8. package/Examples/poly.k +46 -0
  9. package/backends/llvm/runtime/krt.c +4 -0
  10. package/backends/llvm/src/kvm2llvm.mjs +2 -2
  11. package/backends/llvm/tests/test-llvm.mjs +19 -0
  12. package/backends/wasm/src/kvm2wasm.mjs +4 -0
  13. package/backends/wasm/tests/test-wasm.mjs +4 -0
  14. package/browser/repl-browser.mjs +131 -779
  15. package/browser/shims/process.mjs +7 -0
  16. package/browser/tree-view.mjs +5 -2
  17. package/browser/vfs-data.mjs +14 -14
  18. package/codecs/README.md +7 -0
  19. package/codecs/ieee.mjs +50 -75
  20. package/codecs/int.mjs +83 -62
  21. package/codecs/json-codec.mjs +5 -77
  22. package/codecs/json.mjs +12 -56
  23. package/codecs/k-parse.mjs +3 -19
  24. package/codecs/runtime/codec-sdk.mjs +94 -0
  25. package/codecs/runtime/pattern-k.mjs +45 -0
  26. package/codecs/string-codec.mjs +48 -74
  27. package/codecs/unit.mjs +13 -61
  28. package/codecs/utf16.mjs +13 -56
  29. package/codecs/utf8.mjs +11 -55
  30. package/package.json +1 -1
  31. package/patterns/from-k.mjs +2 -22
  32. package/repl-codecs.mjs +69 -196
  33. package/repl.mjs +177 -281
  34. package/scripts/build-repl-html.mjs +114 -385
  35. package/scripts/run-tests.mjs +1 -0
  36. package/tests/test-codecs.mjs +186 -0
  37. package/tests/test-poly.mjs +44 -0
  38. package/tests/test-repl-html.mjs +163 -68
  39. package/tests/test-repl.mjs +210 -156
  40. package/tests/test-structural-values.mjs +17 -0
  41. package/tests/test-tree-view.mjs +3 -0
  42. package/codecs/example-pipeline.mjs +0 -163
  43. package/codecs/runtime/ieee-pattern.mjs +0 -30
  44. package/codecs/runtime/test-codec.mjs +0 -89
  45. package/codecs/runtime/typeFromValue.mjs +0 -34
@@ -14,18 +14,25 @@ import {
14
14
  formatDuration,
15
15
  resolveInputTypeHash,
16
16
  resolveInputPattern,
17
- valueForPattern
17
+ valueForPattern,
18
+ codecNames,
19
+ ensureEnveloped,
20
+ patternFromFilter
18
21
  } from "../repl.mjs";
19
- import { Value, isProduct, isVariant } from "../Value.mjs";
22
+ import { Value, isProduct, isVariant, withPattern } from "../Value.mjs";
20
23
  import { encodeLibrary } from "../object.mjs";
21
24
  import { setVfsFile, getVfsFile, getAllVfsFiles } from "./shims/fs.mjs";
22
25
  import { createOutputEntryElement } from "./tree-view.mjs";
23
26
 
24
- // Make Value and helpers globally available for custom codecs
27
+ // Make Value and helpers globally available in browser console
25
28
  if (typeof window !== "undefined") {
26
- window.Value = Value;
27
- window.isProduct = isProduct;
28
- window.isVariant = isVariant;
29
+ Object.assign(window, {
30
+ Value,
31
+ isProduct,
32
+ isVariant,
33
+ withPattern,
34
+ patternFromFilter
35
+ });
29
36
  }
30
37
 
31
38
  // Global REPL state
@@ -60,18 +67,8 @@ let helpModal;
60
67
  let dropOverlay;
61
68
  let codecsModal;
62
69
  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 inputPopupCodecHint;
74
- let inputSamplesList;
70
+ let enabledCodecsGrid;
71
+
75
72
 
76
73
  function escapeHtml(str) {
77
74
  return String(str)
@@ -115,17 +112,7 @@ function ansiToHtml(str) {
115
112
 
116
113
  function countActiveCodecs() {
117
114
  const store = state.codecs || {};
118
- let count = 0;
119
- const seen = new Set();
120
- for (const list of Object.values(store)) {
121
- for (const c of list) {
122
- if (!seen.has(c.name)) {
123
- seen.add(c.name);
124
- count++;
125
- }
126
- }
127
- }
128
- return count;
115
+ return Object.keys(store).length;
129
116
  }
130
117
 
131
118
  function updateCodecsBadge() {
@@ -264,12 +251,6 @@ export async function executeCommand(input) {
264
251
  const trimmed = input.trim();
265
252
  if (!trimmed && !state.pendingInput) return;
266
253
 
267
- // Intercept :input without args -> open input popup
268
- if (trimmed === ":input") {
269
- openInputPopup();
270
- return;
271
- }
272
-
273
254
  // Add to history
274
255
  if (history.length === 0 || history[history.length - 1] !== input) {
275
256
  history.push(input);
@@ -321,14 +302,21 @@ export async function executeCommand(input) {
321
302
  // Re-create completer with updated state
322
303
  completer = createCompleter(state);
323
304
  updateCodecsBadge();
305
+ updateEngineUI();
324
306
  if (codecsModal && codecsModal.classList.contains("open")) {
325
307
  renderCodecsModal();
326
308
  }
309
+ }
327
310
 
328
- // If :input <type> [codec] was executed, state.pendingInput is now set!
329
- // Open the Input Popup immediately to prompt the user for the input string!
330
- if (errors.length === 0 && state.pendingInput && trimmed.startsWith(":input ")) {
331
- openInputPopup();
311
+ function updateEngineUI() {
312
+ const badge = document.getElementById("engine-badge");
313
+ if (!badge) return;
314
+ badge.textContent = state.engine === "js" ? "js-in-process" : "wasm-in-process";
315
+ badge.title = `Engine: ${state.engine} (click to toggle wasm/js)`;
316
+ if (state.engine === "js") {
317
+ badge.classList.add("js-mode");
318
+ } else {
319
+ badge.classList.remove("js-mode");
332
320
  }
333
321
  }
334
322
 
@@ -353,14 +341,7 @@ function handleAutocomplete() {
353
341
  }
354
342
 
355
343
  if (matches.length === 1) {
356
- hideAutocomplete();
357
- const tokenStart = lineBeforeCursor.lastIndexOf(" ") + 1;
358
- const match = matches[0];
359
- const beforeToken = text.slice(0, cursorPos - (lineBeforeCursor.length - tokenStart));
360
- const afterCursor = text.slice(cursorPos);
361
- inputEl.value = beforeToken + match + (afterCursor.startsWith(" ") ? "" : " ") + afterCursor;
362
- inputEl.selectionStart = inputEl.selectionEnd = beforeToken.length + match.length + 1;
363
- adjustInputHeight();
344
+ applyAutocomplete(matches[0]);
364
345
  return;
365
346
  }
366
347
 
@@ -402,11 +383,12 @@ function applyAutocomplete(match) {
402
383
  const text = inputEl.value;
403
384
  const cursorPos = inputEl.selectionStart;
404
385
  const lineBeforeCursor = text.slice(0, cursorPos).split("\n").pop();
405
- const tokenStart = Math.max(0, lineBeforeCursor.lastIndexOf(" ") + 1);
406
- const beforeToken = text.slice(0, cursorPos - (lineBeforeCursor.length - tokenStart));
386
+ const lineStart = cursorPos - lineBeforeCursor.length;
387
+ const beforeLine = text.slice(0, lineStart);
407
388
  const afterCursor = text.slice(cursorPos);
408
- inputEl.value = beforeToken + match + (afterCursor.startsWith(" ") ? "" : " ") + afterCursor;
409
- inputEl.selectionStart = inputEl.selectionEnd = beforeToken.length + match.length + 1;
389
+ const insertSpace = (match.endsWith("/") || afterCursor.startsWith(" ")) ? "" : " ";
390
+ inputEl.value = beforeLine + match + insertSpace + afterCursor;
391
+ inputEl.selectionStart = inputEl.selectionEnd = beforeLine.length + match.length + insertSpace.length;
410
392
  hideAutocomplete();
411
393
  adjustInputHeight();
412
394
  inputEl.focus();
@@ -566,98 +548,6 @@ function exportKlib() {
566
548
  // CODECS SUBSYSTEM UI & CONTROLS
567
549
  // ==========================================
568
550
 
569
- const CODEC_PRESETS = {
570
- yn: {
571
- name: "yn",
572
- type: "yes_no",
573
- universal: false,
574
- parse: `// Parse 'yes', 'no', 'true', 'false' into $yes_no (< {} yes, {} no >)
575
- const tag = text.trim().toLowerCase();
576
- if (tag === "yes" || tag === "true" || tag === "1") {
577
- return Value.variant("yes", Value.product({}));
578
- }
579
- if (tag === "no" || tag === "false" || tag === "0") {
580
- return Value.variant("no", Value.product({}));
581
- }
582
- throw new Error("Expected yes/no or true/false");`,
583
- print: `// Serialize yes_no value to YES / NO
584
- return value.tag === "yes" ? "YES" : "NO";`
585
- },
586
- hex: {
587
- name: "hex",
588
- type: "int",
589
- universal: false,
590
- parse: `// Parse decimal or hex (e.g. 0x2a, 42, -0x10) into $int
591
- let str = text.trim();
592
- let sign = "+";
593
- if (str.startsWith("-")) { sign = "-"; str = str.slice(1).trim(); }
594
- else if (str.startsWith("+")) { str = str.slice(1).trim(); }
595
-
596
- const num = str.startsWith("0x") || str.startsWith("0X") ? BigInt(str) : BigInt(str);
597
- if (num === 0n) sign = "+";
598
-
599
- const bitChars = num === 0n ? ["0"] : num.toString(2).split("");
600
- let v = Value.variant("_", Value.product({}));
601
- for (let i = bitChars.length - 1; i >= 0; i--) {
602
- v = Value.variant(bitChars[i], v);
603
- }
604
- return Value.variant(sign, v);`,
605
- print: `// Serialize $int to hexadecimal string (0x...)
606
- if (!isVariant(value) || (value.tag !== "+" && value.tag !== "-")) {
607
- throw new Error("Not a valid int value");
608
- }
609
- let bits = "";
610
- let node = value.value;
611
- while (isVariant(node) && node.tag !== "_") {
612
- bits += node.tag;
613
- node = node.value;
614
- }
615
- const n = bits === "" ? 0n : BigInt("0b" + bits);
616
- const hex = "0x" + n.toString(16).toUpperCase();
617
- return (value.tag === "-" && n !== 0n ? "-" : "") + hex;`
618
- },
619
- currency: {
620
- name: "currency",
621
- type: "int",
622
- universal: false,
623
- parse: `// Parse currency e.g. $42.50 or $100 into cents int
624
- const clean = text.trim().replace(/^\\$/, "").trim();
625
- const parts = clean.split(".");
626
- const dollars = BigInt(parts[0] || "0");
627
- const cents = BigInt((parts[1] || "0").padEnd(2, "0").slice(0, 2));
628
- const total = dollars * 100n + cents;
629
-
630
- const bitChars = total === 0n ? ["0"] : total.toString(2).split("");
631
- let v = Value.variant("_", Value.product({}));
632
- for (let i = bitChars.length - 1; i >= 0; i--) {
633
- v = Value.variant(bitChars[i], v);
634
- }
635
- return Value.variant("+", v);`,
636
- print: `// Format cents int as currency string ($X.XX)
637
- let bits = "";
638
- let node = value.value;
639
- while (isVariant(node) && node.tag !== "_") {
640
- bits += node.tag;
641
- node = node.value;
642
- }
643
- const n = bits === "" ? 0n : BigInt("0b" + bits);
644
- const dollars = n / 100n;
645
- const cents = (n % 100n).toString().padStart(2, "0");
646
- return "$" + dollars.toString() + "." + cents;`
647
- },
648
- blank: {
649
- name: "custom",
650
- type: "",
651
- universal: false,
652
- parse: `// Parse text string and return a K Value
653
- // Available: Value.product({ field: val }), Value.variant("tag", val)
654
- return Value.product({});`,
655
- print: `// Serialize K Value into string
656
- // value is a Value instance: inspect value.fields or value.tag & value.value
657
- return String(value);`
658
- }
659
- };
660
-
661
551
  function openCodecsModal() {
662
552
  if (!codecsModal) return;
663
553
  renderCodecsModal();
@@ -670,567 +560,105 @@ function closeCodecsModal() {
670
560
 
671
561
  function renderCodecsModal() {
672
562
  updateCodecsBadge();
563
+ const grid = enabledCodecsGrid || document.getElementById("enabled-codecs-grid");
564
+ if (!grid) return;
565
+ grid.innerHTML = "";
673
566
 
674
- // 1. Built-in codec buttons state
675
- const standardCodecs = ["int", "utf8", "json", "ieee"];
676
- for (const name of standardCodecs) {
677
- const btn = document.getElementById(`btn-toggle-codec-${name}`);
678
- const badge = document.getElementById(`codec-badge-${name}`);
679
- const isLoaded = isCodecRegistered(name);
680
- if (btn) {
681
- btn.textContent = isLoaded ? "Unload" : "Load";
682
- btn.className = isLoaded ? "btn btn-sm btn-secondary" : "btn btn-sm btn-primary";
683
- }
684
- if (badge) {
685
- badge.textContent = isLoaded ? "Active" : "Inactive";
686
- badge.className = isLoaded ? "codec-status-badge active" : "codec-status-badge";
687
- }
688
- }
689
-
690
- // 2. Populate target type dropdown
691
- if (codecTypeSelect) {
692
- const prev = codecTypeSelect.value;
693
- codecTypeSelect.innerHTML = `<option value="">-- Choose Type Alias or Code Hash --</option>`;
694
- const typeGroup = document.createElement("optgroup");
695
- typeGroup.label = "Available Type Aliases in State";
696
- const aliases = Object.entries(state.typeAliases || {}).sort(([a], [b]) => a.localeCompare(b));
697
- for (const [alias, hash] of aliases) {
698
- const opt = document.createElement("option");
699
- opt.value = alias;
700
- opt.textContent = `$ ${alias} (${hash.slice(0, 10)}...)`;
701
- typeGroup.appendChild(opt);
702
- }
703
- codecTypeSelect.appendChild(typeGroup);
704
-
705
- const universalOpt = document.createElement("option");
706
- universalOpt.value = "*";
707
- universalOpt.textContent = "* (Universal - all types)";
708
- codecTypeSelect.appendChild(universalOpt);
709
-
710
- if (prev) codecTypeSelect.value = prev;
711
- }
712
-
713
- // 3. Render active codecs table
714
- if (activeCodecsTbody) {
715
- activeCodecsTbody.innerHTML = "";
716
- const store = state.codecs || {};
717
- const entries = [];
718
- for (const [codeHash, list] of Object.entries(store)) {
719
- for (const c of list) {
720
- entries.push({ codeHash, ...c });
721
- }
722
- }
723
-
724
- if (entries.length === 0) {
725
- const tr = document.createElement("tr");
726
- 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>`;
727
- activeCodecsTbody.appendChild(tr);
728
- return;
729
- }
730
-
731
- // Invert type aliases for display
732
- const hashToAlias = {};
733
- for (const [name, hash] of Object.entries(state.typeAliases || {})) {
734
- hashToAlias[hash] = name;
735
- }
736
-
737
- for (const entry of entries) {
738
- const tr = document.createElement("tr");
739
-
740
- const nameTd = document.createElement("td");
741
- nameTd.className = "codec-cell-name";
742
- nameTd.textContent = entry.name;
743
- tr.appendChild(nameTd);
744
-
745
- const typeTd = document.createElement("td");
746
- typeTd.className = "codec-cell-type";
747
- if (entry.codeHash === "*") {
748
- typeTd.innerHTML = `<span class="badge-type universal">Universal (*)</span>`;
749
- } else {
750
- const alias = hashToAlias[entry.codeHash];
751
- typeTd.innerHTML = `<span class="badge-type">${alias ? `$${alias} ` : ""}<code title="${entry.codeHash}">${entry.codeHash.slice(0, 10)}...</code></span>`;
752
- }
753
- tr.appendChild(typeTd);
754
-
755
- const capTd = document.createElement("td");
756
- const caps = [];
757
- if (typeof entry.print === "function") caps.push(`<span class="cap-badge print">Serializer</span>`);
758
- if (typeof entry.parse === "function") caps.push(`<span class="cap-badge parse">Deserializer</span>`);
759
- capTd.innerHTML = caps.join(" ");
760
- tr.appendChild(capTd);
761
-
762
- const srcTd = document.createElement("td");
763
- srcTd.className = "codec-cell-src";
764
- srcTd.textContent = entry.source ? (entry.source === "built-in" ? "Built-in" : entry.source.split("/").pop()) : "Custom";
765
- tr.appendChild(srcTd);
766
-
767
- const actTd = document.createElement("td");
768
- const unloadBtn = document.createElement("button");
769
- unloadBtn.className = "btn btn-sm btn-danger";
770
- unloadBtn.textContent = "Unload";
771
- unloadBtn.onclick = () => {
772
- executeCommand(`:codec unload ${entry.name}`);
773
- renderCodecsModal();
774
- };
775
- actTd.appendChild(unloadBtn);
776
- tr.appendChild(actTd);
777
-
778
- activeCodecsTbody.appendChild(tr);
779
- }
780
- }
781
- }
782
-
783
- function isCodecRegistered(name) {
784
567
  const store = state.codecs || {};
785
- for (const list of Object.values(store)) {
786
- if (list.some(c => c.name === name)) return true;
787
- }
788
- return false;
789
- }
790
-
791
- function applyCodecPreset(presetKey) {
792
- const preset = CODEC_PRESETS[presetKey];
793
- if (!preset) return;
794
- const nameInput = document.getElementById("codec-custom-name");
795
- const parseArea = document.getElementById("codec-custom-parse");
796
- const printArea = document.getElementById("codec-custom-print");
797
-
798
- if (nameInput) nameInput.value = preset.name;
799
- if (parseArea) parseArea.value = preset.parse;
800
- if (printArea) printArea.value = preset.print;
801
- if (codecTypeSelect) {
802
- if (preset.type) {
803
- codecTypeSelect.value = preset.type;
804
- }
805
- }
806
- }
807
-
808
- function registerCustomCodecFromForm() {
809
- const nameInput = document.getElementById("codec-custom-name");
810
- const parseArea = document.getElementById("codec-custom-parse");
811
- const printArea = document.getElementById("codec-custom-print");
812
- const statusBox = document.getElementById("codec-studio-status");
813
-
814
- const name = nameInput?.value.trim();
815
- const rawType = codecTypeSelect?.value.trim() || "*";
816
- const parseCode = parseArea?.value.trim();
817
- const printCode = printArea?.value.trim();
818
-
819
- if (!name) {
820
- if (statusBox) statusBox.innerHTML = `<span class="line-error">Codec name is required.</span>`;
568
+ const entries = Object.values(store).sort((a, b) => a.name.localeCompare(b.name));
569
+
570
+ if (entries.length === 0) {
571
+ const empty = document.createElement("div");
572
+ empty.className = "codecs-empty-state";
573
+ empty.style.gridColumn = "1 / -1";
574
+ empty.style.padding = "32px 16px";
575
+ empty.style.textAlign = "center";
576
+ empty.style.color = "var(--text-muted)";
577
+ empty.style.background = "var(--bg-base)";
578
+ empty.style.border = "1px dashed var(--border-color)";
579
+ empty.style.borderRadius = "6px";
580
+ empty.innerHTML = `
581
+ <div style="font-size: 24px; margin-bottom: 8px;">📦</div>
582
+ <div style="font-size: 13px; font-weight: 600; color: var(--text-main); margin-bottom: 6px;">No codecs currently loaded</div>
583
+ <div style="font-size: 12px; line-height: 1.5;">Codecs are ordinary <code>.mjs</code> files.<br>Load a codec using <code>:codec load &lt;file.mjs&gt;</code> in the terminal, or select a codec file in <b>Files</b> and click <b>Load into REPL</b>.</div>
584
+ `;
585
+ grid.appendChild(empty);
821
586
  return;
822
587
  }
823
588
 
824
- try {
825
- let parseFn = null;
826
- let printFn = null;
827
-
828
- if (parseCode) {
829
- parseFn = new Function(
830
- "text", "context",
831
- `const { Value, isProduct, isVariant } = globalThis;\n${parseCode}`
832
- );
833
- }
834
-
835
- if (printCode) {
836
- printFn = new Function(
837
- "value", "context",
838
- `const { Value, isProduct, isVariant } = globalThis;\n${printCode}`
839
- );
840
- }
841
-
842
- let codeHash = "*";
843
- if (rawType !== "*") {
844
- codeHash = resolveInputTypeHash(state, rawType);
845
- }
846
-
847
- registerCodec(state, {
848
- name,
849
- codes: [codeHash],
850
- universal: codeHash === "*",
851
- parse: parseFn,
852
- print: printFn
853
- }, "<custom-studio>");
854
-
855
- if (statusBox) {
856
- statusBox.innerHTML = `<span class="line-output">✓ Successfully registered codec <b>${name}</b> for ${codeHash === "*" ? "all types" : codeHash}!</span>`;
857
- }
858
-
859
- appendSystemMessage(`⚙ Registered custom codec <b>${name}</b> (serializer/deserializer) for ${codeHash === "*" ? "all types" : codeHash}.`);
860
- renderCodecsModal();
861
- } catch (err) {
862
- if (statusBox) {
863
- statusBox.innerHTML = `<span class="line-error">Error: ${escapeHtml(err.message)}</span>`;
864
- }
865
- }
866
- }
867
-
868
- function testCustomParse() {
869
- const parseArea = document.getElementById("codec-custom-parse");
870
- const testInput = document.getElementById("codec-test-input");
871
- const statusBox = document.getElementById("codec-studio-status");
872
-
873
- try {
874
- const parseFn = new Function(
875
- "text", "context",
876
- `const { Value, isProduct, isVariant } = globalThis;\n${parseArea.value}`
877
- );
878
- const result = parseFn(testInput.value, { Value, isProduct, isVariant, state });
879
- if (statusBox) {
880
- statusBox.innerHTML = `<span class="line-output"><b>Parse Result:</b> ${escapeHtml(JSON.stringify(result, null, 2))}</span>`;
881
- }
882
- } catch (err) {
883
- if (statusBox) {
884
- statusBox.innerHTML = `<span class="line-error">Parse Error: ${escapeHtml(err.message)}</span>`;
885
- }
886
- }
887
- }
888
-
889
- function testCustomPrint() {
890
- const printArea = document.getElementById("codec-custom-print");
891
- const statusBox = document.getElementById("codec-studio-status");
892
-
893
- try {
894
- const printFn = new Function(
895
- "value", "context",
896
- `const { Value, isProduct, isVariant } = globalThis;\n${printArea.value}`
897
- );
898
- const result = printFn(state.value, { Value, isProduct, isVariant, state });
899
- if (statusBox) {
900
- statusBox.innerHTML = `<span class="line-output"><b>Print Output:</b> ${escapeHtml(String(result))}</span>`;
901
- }
902
- } catch (err) {
903
- if (statusBox) {
904
- statusBox.innerHTML = `<span class="line-error">Print Error: ${escapeHtml(err.message)}</span>`;
905
- }
906
- }
907
- }
908
-
909
- function saveCustomCodecToVfs() {
910
- const nameInput = document.getElementById("codec-custom-name");
911
- const parseArea = document.getElementById("codec-custom-parse");
912
- const printArea = document.getElementById("codec-custom-print");
913
- const statusBox = document.getElementById("codec-studio-status");
914
-
915
- const name = nameInput?.value.trim() || "custom";
916
- const rawType = codecTypeSelect?.value.trim() || "*";
917
- const codeHash = rawType === "*" ? "*" : state.typeAliases[rawType] || rawType;
918
- const fileName = `codecs/${name}-codec.mjs`;
919
-
920
- const fileContent = `// Custom K Codec: ${name}
921
- import { Value, isProduct, isVariant } from "../Value.mjs";
922
-
923
- export const name = ${JSON.stringify(name)};
924
- export const codes = [${JSON.stringify(codeHash)}];
925
- export const universal = ${codeHash === "*"};
926
-
927
- export function parse(text, context) {
928
- ${parseArea?.value || ""}
929
- }
930
-
931
- export function print(value, context) {
932
- ${printArea?.value || ""}
933
- }
934
- `;
935
-
936
- setVfsFile(fileName, fileContent);
937
- if (statusBox) {
938
- statusBox.innerHTML = `<span class="line-output">💾 Saved codec file to VFS as <b>${fileName}</b></span>`;
939
- }
940
- appendSystemMessage(`💾 Saved codec file to VFS: <b>${fileName}</b>. You can reload it anytime via <code>:codec load ${fileName}</code>.`);
941
- }
942
-
943
- // ==========================================
944
- // CODEC INPUT POPUP SUBSYSTEM (:input)
945
- // ==========================================
946
-
947
- const CODEC_SAMPLES = {
948
- int: ["0", "10", "42", "-15", "1000000"],
949
- utf8: ["hello", "hello world", "k repl", "abc"],
950
- json: ['{"x":12,"n":"Woj"}', '{"name":"Alice"}', 'true', '[1, 2, 3]'],
951
- ieee: ["0.0", "3.14159", "-2.718", "1e6"],
952
- yn: ["yes", "no", "true", "false"],
953
- hex: ["0x2A", "0xFF", "0x1000", "-0x10"],
954
- currency: ["$10.00", "$42.50", "$99.99"]
955
- };
956
-
957
- export function openInputPopup(preselectedType = null, preselectedCodec = null) {
958
- if (!inputPopupModal) return;
959
-
960
- populateInputPopupDropdowns(preselectedType, preselectedCodec);
961
-
962
- if (inputPopupText) {
963
- inputPopupText.value = "";
964
- }
965
- updateLiveValidation();
966
-
967
- inputPopupModal.classList.add("open");
968
- setTimeout(() => {
969
- if (inputPopupText) inputPopupText.focus();
970
- }, 50);
971
- }
972
-
973
- export function closeInputPopup() {
974
- if (inputPopupModal) {
975
- inputPopupModal.classList.remove("open");
976
- }
977
- if (inputEl) inputEl.focus();
978
- }
979
-
980
- export function cancelInputPopup() {
981
- if (state.pendingInput) {
982
- state.pendingInput = null;
983
- updatePrompt();
984
- appendSystemMessage("Input cancelled.");
985
- }
986
- closeInputPopup();
987
- }
988
-
989
- function renderInputSamples(codecName) {
990
- if (!inputSamplesList) return;
991
- inputSamplesList.innerHTML = "";
992
- const samples = CODEC_SAMPLES[codecName] || ["()"];
993
- for (const s of samples) {
994
- const btn = document.createElement("span");
995
- btn.className = "sample-pill";
996
- btn.textContent = s;
997
- btn.title = `Click to insert sample "${s}"`;
998
- btn.onclick = () => {
999
- if (inputPopupText) {
1000
- inputPopupText.value = s;
1001
- if (codecName === "json" && s === '{"x":12,"n":"Woj"}' && (!inputPopupType?.value || inputPopupType.value === "*")) {
1002
- inputPopupType.value = "{float64 x, string n}";
1003
- updateInputPopupCodecs();
1004
- }
1005
- inputPopupText.focus();
1006
- updateLiveValidation();
1007
- }
589
+ for (const entry of entries) {
590
+ const card = document.createElement("div");
591
+ card.className = "codec-card active";
592
+ card.id = `card-codec-${entry.name.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
593
+
594
+ const titleEl = document.createElement("div");
595
+ titleEl.className = "codec-card-title";
596
+ titleEl.innerHTML = `
597
+ <span class="codec-icon">📦</span>
598
+ <span class="codec-name">${escapeHtml(entry.name)}</span>
599
+ <span class="codec-status-badge active">Active</span>
600
+ `;
601
+ card.appendChild(titleEl);
602
+
603
+ let targetDesc = "Self-describing";
604
+ if (entry.name === "int.mjs") {
605
+ targetDesc = "Pattern Family: $ int & lists";
606
+ } else if (entry.name === "utf8.mjs") {
607
+ targetDesc = "$ string";
608
+ } else if (entry.name === "ieee.mjs") {
609
+ targetDesc = "$ float64";
610
+ } else if (entry.name === "unit.mjs") {
611
+ targetDesc = "{}";
612
+ } else if (entry.name === "json.mjs") {
613
+ targetDesc = "Self-describing JSON";
614
+ } else if (Array.isArray(entry.patterns) && entry.patterns.length > 0) {
615
+ targetDesc = `Pattern Family (${entry.patterns.length})`;
616
+ } else if (entry.pattern) {
617
+ targetDesc = "Pattern Recipe";
618
+ }
619
+
620
+ const typeDiv = document.createElement("div");
621
+ typeDiv.className = "codec-types";
622
+ typeDiv.innerHTML = `Target: <span class="badge-type">${escapeHtml(targetDesc)}</span>`;
623
+ card.appendChild(typeDiv);
624
+
625
+ const descDiv = document.createElement("div");
626
+ descDiv.className = "codec-desc";
627
+ const src = entry.source || entry.name;
628
+ descDiv.innerHTML = `Source: <code>${escapeHtml(src)}</code>`;
629
+ card.appendChild(descDiv);
630
+
631
+ const footer = document.createElement("div");
632
+ footer.className = "codec-card-footer";
633
+
634
+ const capsDiv = document.createElement("div");
635
+ capsDiv.className = "codec-caps";
636
+ const caps = [];
637
+ if (typeof entry.print === "function") caps.push('<span class="cap-badge print">Serializer</span>');
638
+ if (typeof entry.parse === "function") caps.push('<span class="cap-badge parse">Deserializer</span>');
639
+ capsDiv.innerHTML = caps.join(" ");
640
+ footer.appendChild(capsDiv);
641
+
642
+ const unloadBtn = document.createElement("button");
643
+ unloadBtn.className = "btn btn-sm btn-danger";
644
+ unloadBtn.textContent = "Unload";
645
+ unloadBtn.onclick = async () => {
646
+ await executeCommand(`:codec unload ${entry.name}`);
647
+ renderCodecsModal();
1008
648
  };
1009
- inputSamplesList.appendChild(btn);
1010
- }
1011
- }
649
+ footer.appendChild(unloadBtn);
1012
650
 
1013
- function updateInputPopupHint() {
1014
- if (!inputPopupHint) return;
1015
- const rawPattern = inputPopupType ? inputPopupType.value.trim() : "(...)";
1016
- const codecName = inputPopupCodec?.value || state.pendingInput?.codecName;
1017
- if (state.pendingInput) {
1018
- const displayPattern = rawPattern || state.pendingInput.rawExpr || "(...)";
1019
- inputPopupHint.innerHTML = `Enter input value for <b>${escapeHtml(displayPattern)}</b> using codec <b>${escapeHtml(codecName || "default")}</b>:`;
1020
- } else {
1021
- inputPopupHint.innerHTML = `Choose target pattern &amp; deserializer, then enter input string:`;
1022
- }
1023
-
1024
- if (inputPopupCodecHint) {
1025
- if (codecName === "json") {
1026
- inputPopupCodecHint.textContent = "Parameterized codec: deserializes JSON into the specified Target Pattern.";
1027
- } else {
1028
- inputPopupCodecHint.textContent = "";
1029
- }
651
+ card.appendChild(footer);
652
+ grid.appendChild(card);
1030
653
  }
1031
654
  }
1032
655
 
1033
- function updateInputPopupCodecs() {
1034
- if (!inputPopupCodec) return;
1035
- const rawPattern = inputPopupType ? inputPopupType.value.trim() : "(...)";
1036
- let codeHash = null;
1037
- if (rawPattern && rawPattern !== "*" && rawPattern !== "(...)") {
1038
- try {
1039
- const resolved = resolveInputPattern(state, rawPattern);
1040
- codeHash = resolved.codeHash;
1041
- } catch {
1042
- codeHash = null;
1043
- }
1044
- }
1045
- const prev = inputPopupCodec.value;
1046
-
1047
- inputPopupCodec.innerHTML = "";
1048
- const matchingCodecs = [];
656
+ function isCodecRegistered(name) {
1049
657
  const store = state.codecs || {};
1050
-
1051
- const candidates = [
1052
- ...(codeHash ? (store[codeHash] || []) : []),
1053
- ...(store["*"] || [])
1054
- ];
1055
-
1056
- const seen = new Set();
1057
- for (const c of candidates) {
1058
- if (typeof c.parse === "function" && !seen.has(c.name)) {
1059
- seen.add(c.name);
1060
- matchingCodecs.push(c.name);
1061
- }
1062
- }
1063
-
1064
- const bareName = rawPattern.replace(/^(\?|\$)\s*/, "").trim();
1065
- if (!seen.has("json")) {
1066
- matchingCodecs.push("json");
1067
- seen.add("json");
1068
- }
1069
- if ((bareName === "int" || (codeHash && codeHash === state.typeAliases?.int)) && !seen.has("int")) {
1070
- matchingCodecs.push("int");
1071
- seen.add("int");
1072
- }
1073
- if ((bareName === "string" || bareName === "utf8" || (codeHash && (codeHash === state.typeAliases?.string || codeHash === state.typeAliases?.utf8))) && !seen.has("utf8")) {
1074
- matchingCodecs.push("utf8");
1075
- seen.add("utf8");
1076
- }
1077
- if ((bareName === "float64" || bareName === "ieee" || (codeHash && codeHash === state.typeAliases?.float64)) && !seen.has("ieee")) {
1078
- matchingCodecs.push("ieee");
1079
- seen.add("ieee");
1080
- }
1081
- if ((bareName === "yes_no" || bareName === "bool" || (codeHash && codeHash === state.typeAliases?.yes_no)) && !seen.has("yn")) {
1082
- matchingCodecs.push("yn");
1083
- seen.add("yn");
1084
- }
1085
-
1086
- for (const name of matchingCodecs) {
1087
- const opt = document.createElement("option");
1088
- opt.value = name;
1089
- opt.textContent = name;
1090
- inputPopupCodec.appendChild(opt);
1091
- }
1092
-
1093
- if (prev && matchingCodecs.includes(prev)) {
1094
- inputPopupCodec.value = prev;
1095
- } else if (matchingCodecs.length > 0) {
1096
- inputPopupCodec.value = matchingCodecs[0];
1097
- }
1098
-
1099
- renderInputSamples(inputPopupCodec.value);
1100
- updateInputPopupHint();
1101
- updateLiveValidation();
658
+ return Boolean(store[name]);
1102
659
  }
1103
660
 
1104
- function populateInputPopupDropdowns(preselectedPattern = null, preselectedCodec = null) {
1105
- if (!inputPopupType) return;
1106
-
1107
- const datalist = document.getElementById("input-popup-type-list");
1108
- if (datalist) {
1109
- datalist.innerHTML = "";
1110
- const aliases = Object.entries(state.typeAliases || {}).sort(([a], [b]) => a.localeCompare(b));
1111
- for (const [alias, hash] of aliases) {
1112
- const opt = document.createElement("option");
1113
- opt.value = `$ ${alias}`;
1114
- opt.textContent = `$ ${alias} (${hash.slice(0, 10)}...)`;
1115
- datalist.appendChild(opt);
1116
- }
1117
- const universalOpt = document.createElement("option");
1118
- universalOpt.value = "(...)";
1119
- universalOpt.textContent = "(...) (Any document)";
1120
- datalist.appendChild(universalOpt);
1121
- }
1122
661
 
1123
- let prevPattern = preselectedPattern;
1124
- if (!prevPattern && state.pendingInput) {
1125
- prevPattern = state.pendingInput.rawExpr;
1126
- if (!prevPattern && state.pendingInput.codeHash) {
1127
- const foundAlias = Object.entries(state.typeAliases || {}).find(([, h]) => h === state.pendingInput.codeHash);
1128
- prevPattern = foundAlias ? `$ ${foundAlias[0]}` : state.pendingInput.codeHash;
1129
- }
1130
- }
1131
- if (!prevPattern) {
1132
- prevPattern = inputPopupType.value || "(...)";
1133
- }
1134
-
1135
- inputPopupType.value = prevPattern;
1136
- updateInputPopupCodecs();
1137
-
1138
- const targetCodec = preselectedCodec || state.pendingInput?.codecName;
1139
- if (targetCodec && inputPopupCodec) {
1140
- inputPopupCodec.value = targetCodec;
1141
- renderInputSamples(targetCodec);
1142
- }
1143
- }
1144
-
1145
- function updateLiveValidation() {
1146
- if (!inputPopupStatus) return;
1147
- const text = inputPopupText ? inputPopupText.value : "";
1148
- if (!text) {
1149
- inputPopupStatus.innerHTML = `<span style="color:var(--text-muted);font-size:11px;">Ready for input.</span>`;
1150
- return;
1151
- }
1152
-
1153
- const rawPattern = inputPopupType ? inputPopupType.value.trim() : "(...)";
1154
- const codecName = inputPopupCodec?.value;
1155
- if (!rawPattern && !codecName) {
1156
- inputPopupStatus.innerHTML = `<span class="line-warning" style="margin:0;padding:2px 6px;">Specify a target pattern or codec first.</span>`;
1157
- return;
1158
- }
1159
-
1160
- let pattern = null;
1161
- let codeHash = null;
1162
- if (rawPattern && rawPattern !== "*" && rawPattern !== "(...)") {
1163
- try {
1164
- const resolved = resolveInputPattern(state, rawPattern);
1165
- pattern = resolved.pattern;
1166
- codeHash = resolved.codeHash;
1167
- } catch (err) {
1168
- inputPopupStatus.innerHTML = `<span class="line-warning" style="margin:0;padding:2px 6px;">⚠ Pattern Error: ${escapeHtml(err.message || String(err))}</span>`;
1169
- return;
1170
- }
1171
- }
1172
-
1173
- try {
1174
- const codec = resolveCodec(state, codeHash, codecName || null, "parse");
1175
- const resolvedPattern = pattern || (codeHash && codeHash !== "*" ? codeHashToPattern(codeHash, (h) => state.codes?.[h]) : null);
1176
- const parsed = codec.parse(text, {
1177
- codeHash,
1178
- pattern: resolvedPattern,
1179
- state,
1180
- Value,
1181
- isProduct,
1182
- isVariant
1183
- });
1184
- const validated = valueForPattern(parsed, resolvedPattern, text, codec.name);
1185
- const repr = validated instanceof Value ? validated.toJSON() : (typeof validated === "object" ? JSON.stringify(validated) : String(validated));
1186
- inputPopupStatus.innerHTML = `<span class="line-output" style="margin:0;padding:2px 6px;">✓ Valid: <code>${escapeHtml(String(repr))}</code></span>`;
1187
- } catch (err) {
1188
- inputPopupStatus.innerHTML = `<span class="line-warning" style="margin:0;padding:2px 6px;">⚠ ${escapeHtml(err.message || String(err))}</span>`;
1189
- }
1190
- }
1191
-
1192
- export async function submitInputPopup() {
1193
- const text = inputPopupText?.value ?? "";
1194
- const rawPattern = inputPopupType ? inputPopupType.value.trim() : "(...)";
1195
- const codecName = inputPopupCodec?.value;
1196
-
1197
- if (!rawPattern && !codecName) {
1198
- if (inputPopupStatus) inputPopupStatus.innerHTML = `<span class="line-error">Please specify a target pattern or codec.</span>`;
1199
- return;
1200
- }
1201
-
1202
- let pattern = null;
1203
- let codeHash = null;
1204
- let rawExpr = rawPattern || "(...)";
1205
- if (rawPattern && rawPattern !== "*" && rawPattern !== "(...)") {
1206
- try {
1207
- const resolved = resolveInputPattern(state, rawPattern);
1208
- pattern = resolved.pattern;
1209
- codeHash = resolved.codeHash;
1210
- rawExpr = rawPattern;
1211
- } catch (err) {
1212
- if (inputPopupStatus) inputPopupStatus.innerHTML = `<span class="line-error">Pattern Error: ${escapeHtml(err.message || String(err))}</span>`;
1213
- return;
1214
- }
1215
- }
1216
-
1217
- try {
1218
- const codec = resolveCodec(state, codeHash, codecName || null, "parse");
1219
- state.pendingInput = {
1220
- pattern,
1221
- codeHash,
1222
- codecName: codec.name,
1223
- promptName: codec.name,
1224
- rawExpr: rawExpr || codec.name
1225
- };
1226
- } catch (err) {
1227
- if (inputPopupStatus) inputPopupStatus.innerHTML = `<span class="line-error">${escapeHtml(err.message || String(err))}</span>`;
1228
- return;
1229
- }
1230
-
1231
- closeInputPopup();
1232
- await executeCommand(text);
1233
- }
1234
662
 
1235
663
  // Initialization on DOMContentLoaded
1236
664
  export function initRepl() {
@@ -1248,17 +676,8 @@ export function initRepl() {
1248
676
  dropOverlay = document.getElementById("drop-overlay");
1249
677
  codecsModal = document.getElementById("codecs-modal");
1250
678
  codecsBadgeEl = document.getElementById("codecs-count-badge");
1251
- activeCodecsTbody = document.getElementById("active-codecs-tbody");
1252
- codecTypeSelect = document.getElementById("codec-custom-type");
1253
-
1254
- inputPopupModal = document.getElementById("input-popup-modal");
1255
- inputPopupType = document.getElementById("input-popup-type");
1256
- inputPopupCodec = document.getElementById("input-popup-codec");
1257
- inputPopupText = document.getElementById("input-popup-text");
1258
- inputPopupStatus = document.getElementById("input-popup-status");
1259
- inputPopupHint = document.getElementById("input-popup-hint");
1260
- inputPopupCodecHint = document.getElementById("input-popup-codec-hint");
1261
- inputSamplesList = document.getElementById("input-samples-list");
679
+ enabledCodecsGrid = document.getElementById("enabled-codecs-grid");
680
+
1262
681
 
1263
682
  updatePrompt();
1264
683
 
@@ -1459,78 +878,7 @@ export function initRepl() {
1459
878
  const closeCodecsBtn = document.getElementById("btn-close-codecs");
1460
879
  if (closeCodecsBtn) closeCodecsBtn.onclick = closeCodecsModal;
1461
880
 
1462
- // Toggle built-in codecs buttons
1463
- ["int", "utf8", "json", "ieee"].forEach(name => {
1464
- const btn = document.getElementById(`btn-toggle-codec-${name}`);
1465
- if (btn) {
1466
- btn.onclick = () => {
1467
- if (isCodecRegistered(name)) {
1468
- executeCommand(`:codec unload ${name}`);
1469
- } else {
1470
- executeCommand(`:codec load ${name}`);
1471
- }
1472
- renderCodecsModal();
1473
- };
1474
- }
1475
- });
1476
-
1477
- // Codec Studio controls
1478
- const presetSelect = document.getElementById("codec-preset-select");
1479
- if (presetSelect) {
1480
- presetSelect.addEventListener("change", () => {
1481
- if (presetSelect.value) applyCodecPreset(presetSelect.value);
1482
- });
1483
- }
1484
-
1485
- const btnRegisterCodec = document.getElementById("btn-register-codec");
1486
- if (btnRegisterCodec) btnRegisterCodec.onclick = registerCustomCodecFromForm;
1487
-
1488
- const btnTestParse = document.getElementById("btn-test-parse");
1489
- if (btnTestParse) btnTestParse.onclick = testCustomParse;
1490
-
1491
- const btnTestPrint = document.getElementById("btn-test-print");
1492
- if (btnTestPrint) btnTestPrint.onclick = testCustomPrint;
1493
-
1494
- const btnSaveCodecVfs = document.getElementById("btn-save-codec-vfs");
1495
- if (btnSaveCodecVfs) btnSaveCodecVfs.onclick = saveCustomCodecToVfs;
1496
-
1497
- // Codec Input Popup controls
1498
- const btnInputNavbar = document.getElementById("btn-input-popup");
1499
- if (btnInputNavbar) btnInputNavbar.onclick = () => openInputPopup();
1500
-
1501
- const btnCloseInput = document.getElementById("btn-close-input-popup");
1502
- if (btnCloseInput) btnCloseInput.onclick = cancelInputPopup;
1503
-
1504
- const btnCancelInput = document.getElementById("btn-cancel-input");
1505
- if (btnCancelInput) btnCancelInput.onclick = cancelInputPopup;
1506
881
 
1507
- const btnSubmitInput = document.getElementById("btn-submit-input");
1508
- if (btnSubmitInput) btnSubmitInput.onclick = submitInputPopup;
1509
-
1510
- if (inputPopupType) {
1511
- inputPopupType.addEventListener("change", updateInputPopupCodecs);
1512
- inputPopupType.addEventListener("input", updateInputPopupCodecs);
1513
- }
1514
-
1515
- if (inputPopupCodec) {
1516
- inputPopupCodec.addEventListener("change", () => {
1517
- renderInputSamples(inputPopupCodec.value);
1518
- updateLiveValidation();
1519
- });
1520
- }
1521
-
1522
- if (inputPopupText) {
1523
- inputPopupText.addEventListener("input", updateLiveValidation);
1524
- inputPopupText.addEventListener("keydown", (e) => {
1525
- if (e.key === "Enter" && (e.ctrlKey || e.metaKey || !e.shiftKey)) {
1526
- e.preventDefault();
1527
- submitInputPopup();
1528
- } else if (e.key === "Escape") {
1529
- e.preventDefault();
1530
- cancelInputPopup();
1531
- }
1532
- });
1533
- }
1534
882
 
1535
883
  // Export .klib button
1536
884
  const exportBtn = document.getElementById("btn-export");
@@ -1589,7 +937,6 @@ export function initRepl() {
1589
937
  if (e.target === vfsModal) closeVfsModal();
1590
938
  if (e.target === helpModal) helpModal.classList.remove("open");
1591
939
  if (e.target === codecsModal) closeCodecsModal();
1592
- if (e.target === inputPopupModal) cancelInputPopup();
1593
940
  });
1594
941
 
1595
942
  // Focus input when clicking anywhere in the input bar
@@ -1615,8 +962,15 @@ export function initRepl() {
1615
962
  }
1616
963
  });
1617
964
 
1618
- // Apply initial yn preset to custom form
1619
- applyCodecPreset("yn");
965
+ // Engine Badge toggle (wasm <-> js)
966
+ const engineBadge = document.getElementById("engine-badge");
967
+ if (engineBadge) {
968
+ engineBadge.onclick = () => {
969
+ const nextEngine = state.engine === "wasm" ? "js" : "wasm";
970
+ executeCommand(`:engine ${nextEngine}`);
971
+ };
972
+ }
973
+ updateEngineUI();
1620
974
 
1621
975
  inputEl.focus();
1622
976
  }
@@ -1634,14 +988,12 @@ if (typeof window !== "undefined") {
1634
988
  executeCommand,
1635
989
  initRepl,
1636
990
  getState: () => state,
991
+ getEngine: () => state.engine,
992
+ setEngine: (engine) => executeCommand(`:engine ${engine}`),
1637
993
  getVfsFile,
1638
994
  setVfsFile,
1639
995
  getAllVfsFiles,
1640
996
  openCodecsModal,
1641
- openInputPopup,
1642
- closeInputPopup,
1643
- submitInputPopup,
1644
- cancelInputPopup,
1645
997
  registerCodec,
1646
998
  unregisterCodec
1647
999
  };