@fraczak/k 7.2.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/DOCS/BIG_PICTURE.md +54 -7
  3. package/DOCS/CODECS.md +21 -13
  4. package/DOCS/CODEC_ARCHITECTURE.md +1 -1
  5. package/DOCS/DICTIONARY.md +13 -4
  6. package/DOCS/FILE_FORMATS.md +3 -2
  7. package/DOCS/JSON_APPROACHES_COMPARISON.md +302 -0
  8. package/DOCS/NAMED_PATTERNS.md +317 -0
  9. package/DOCS/PATTERNS.md +59 -11
  10. package/DOCS/REPL.md +13 -2
  11. package/DOCS/WEB_REPL_REQUIREMENTS.md +3 -2
  12. package/Examples/arithmetics.k +22 -17
  13. package/Examples/bnat.k +21 -11
  14. package/Examples/buda.k +37 -20
  15. package/Examples/byte.k +3 -1
  16. package/Examples/code.k +38 -39
  17. package/Examples/djson.k +789 -0
  18. package/Examples/esc.k +1 -1
  19. package/Examples/karatsuba-mult.k +16 -13
  20. package/Examples/list.k +8 -4
  21. package/Examples/nat.k +17 -11
  22. package/Examples/poly.k +11 -3
  23. package/Examples/vhdl_to_k.k +43 -31
  24. package/README.md +40 -16
  25. package/browser/repl-browser.mjs +2 -0
  26. package/browser/tree-view.mjs +35 -16
  27. package/browser/vfs-data.mjs +10 -8
  28. package/codecs/README.md +1 -1
  29. package/codecs/djson.mjs +375 -84
  30. package/core.k +79 -115
  31. package/index.mjs +3 -0
  32. package/named-patterns.mjs +267 -0
  33. package/object.mjs +31 -3
  34. package/package.json +1 -1
  35. package/parser.jison +68 -5
  36. package/parser.mjs +111 -60
  37. package/pretty.mjs +5 -0
  38. package/repl.mjs +188 -22
  39. package/scripts/build-repl-html.mjs +58 -4
  40. package/scripts/run-tests.mjs +10 -0
  41. package/symbol-table.mjs +20 -2
  42. package/tests/test-codecs.mjs +30 -17
  43. package/tests/test-djson-methods.mjs +285 -0
  44. package/tests/test-named-patterns.mjs +189 -0
  45. package/tests/test-repl-html.mjs +47 -4
  46. package/tests/test-repl.mjs +149 -0
package/repl.mjs CHANGED
@@ -21,7 +21,7 @@ if (typeof globalThis !== "undefined") {
21
21
  globalThis.isVariant = isVariant;
22
22
  globalThis.patternFromFilter = patternFromFilter;
23
23
  }
24
- import { prettyCode } from "./pretty.mjs";
24
+ import { prettyCode, prettyFilter } from "./pretty.mjs";
25
25
  import {
26
26
  compileObject,
27
27
  compileLibrary,
@@ -32,7 +32,11 @@ import {
32
32
  loadLibrary,
33
33
  prettyRelation
34
34
  } from "./object.mjs";
35
- import { propertyListToFilter, valueToK, valueWithEnvelopeToK } from "./codecs/runtime/show-value.mjs";
35
+ import {
36
+ propertyListToFilter,
37
+ valueToK,
38
+ valueWithEnvelopeToK
39
+ } from "./codecs/runtime/show-value.mjs";
36
40
  import {
37
41
  codecNames,
38
42
  codeHashToPattern,
@@ -48,7 +52,7 @@ import {
48
52
 
49
53
  const NAME_RE = /^[a-zA-Z0-9_+-][a-zA-Z0-9_?!+-]*$/;
50
54
  const COMMAND_NAMES = [
51
- "help", "rel",
55
+ "help", "rel", "pattern", "pat", "patterns", "pats",
52
56
  "codecs", "rels", "codec", "input", "reset", "klib", "ko", "load",
53
57
  "engine", "wasm", "pkir", "js", "limit",
54
58
  "quit", "exit"
@@ -87,6 +91,7 @@ function createState(options = {}) {
87
91
  rels: {},
88
92
  relAliases: {},
89
93
  typeAliases: {},
94
+ patterns: {},
90
95
  codecs: {},
91
96
  loadedFiles: new Set(),
92
97
  pendingInput: null,
@@ -106,6 +111,7 @@ function stateLibrary(state) {
106
111
  codes: state.codes,
107
112
  rels: state.rels,
108
113
  relAlias: state.relAliases,
114
+ patterns: state.patterns,
109
115
  meta: state.meta,
110
116
  main: null
111
117
  };
@@ -227,6 +233,11 @@ function mergeLibrary(state, lib, source = "<load>", options = {}) {
227
233
  const loadAliases = options.loadAliases ?? true;
228
234
  state.codes = { ...state.codes, ...(lib.codes || {}) };
229
235
  state.rels = { ...state.rels, ...(lib.rels || {}) };
236
+ state.patterns = {
237
+ ...state.patterns,
238
+ ...(lib.patterns || lib.sourceDefs?.patterns ||
239
+ lib.defs?.patterns || {})
240
+ };
230
241
  mergeMeta(state, lib.meta);
231
242
 
232
243
  if (loadAliases) {
@@ -310,6 +321,7 @@ function savedLibrary(state) {
310
321
  rels,
311
322
  relAlias: state.relAliases,
312
323
  compileStats: { sccs: [], sccCount: 0 },
324
+ patterns: state.patterns || {},
313
325
  meta: Object.fromEntries(
314
326
  Object.entries(meta).filter(([hash]) => storedHashes.has(hash))
315
327
  ),
@@ -320,7 +332,10 @@ function savedLibrary(state) {
320
332
  function executableObject(state, mainExpression) {
321
333
  const main = (mainExpression || state.lastMain || "").trim();
322
334
  if (!main) {
323
- throw new Error(":ko file expr requires a main expression unless one has already been evaluated or defined");
335
+ throw new Error(
336
+ ":ko file expr requires a main expression unless one has " +
337
+ "already been evaluated or defined"
338
+ );
324
339
  }
325
340
 
326
341
  restoreCodes(state);
@@ -328,7 +343,8 @@ function executableObject(state, mainExpression) {
328
343
  try {
329
344
  return compileObject([preamble, main].filter(Boolean).join("\n"), {
330
345
  source: "<repl>",
331
- libraries: [stateLibrary(state)]
346
+ libraries: [stateLibrary(state)],
347
+ patterns: state.patterns
332
348
  });
333
349
  } catch (error) {
334
350
  throw remapError(error, preambleLineCount(preamble));
@@ -343,11 +359,72 @@ function resolveRel(state, name) {
343
359
  }
344
360
 
345
361
  function listAliases(aliases) {
346
- const entries = Object.entries(aliases).sort(([a], [b]) => a.localeCompare(b));
362
+ const entries = Object.entries(aliases).sort(([a], [b]) =>
363
+ a.localeCompare(b)
364
+ );
347
365
  if (entries.length === 0) return "(none)";
348
366
  return entries.map(([name, hash]) => `${name} = ${hash}`).join("\n");
349
367
  }
350
368
 
369
+ function resolvePatternCanonicalName(state, name, pat) {
370
+ if (!state || !pat) return null;
371
+ if (pat._canonical) return pat._canonical;
372
+ const params = pat.params || [];
373
+ const source = `? ${name}[${params.join(", ")}]`;
374
+ try {
375
+ restoreCodes(state);
376
+ const annotated = annotate(source, {
377
+ libraries: [stateLibrary(state)],
378
+ patterns: state.patterns
379
+ });
380
+ const mainRel = annotated.rels.__main__;
381
+ if (
382
+ mainRel?.typePatternGraph &&
383
+ mainRel?.def?.patterns?.[0] != null
384
+ ) {
385
+ const rootId = mainRel.typePatternGraph.find(
386
+ mainRel.def.patterns[0]
387
+ );
388
+ const propList = patternToPropertyList(
389
+ exportPatternGraph(mainRel.typePatternGraph, rootId)
390
+ );
391
+ const codeHash = closedPatternToCodeHash(propList);
392
+ if (codeHash) {
393
+ state.codes = codes.dump();
394
+ pat._canonical = codeHash;
395
+ return codeHash;
396
+ }
397
+ }
398
+ const relHash = annotated.relAlias?.__main__ || null;
399
+ if (relHash) {
400
+ pat._canonical = relHash;
401
+ return relHash;
402
+ }
403
+ } catch {
404
+ restoreCodes(state);
405
+ return null;
406
+ }
407
+ return null;
408
+ }
409
+
410
+ function prettyPattern(name, pat, state = null) {
411
+ const params = (pat?.params || []).join(", ");
412
+ const filterStr = pat?.filter ? prettyFilter(pat.filter) : "()";
413
+ const canonical = resolvePatternCanonicalName(state, name, pat);
414
+ const suffix = canonical ? ` -- ${canonical}` : "";
415
+ return `${name}[${params}] = ${filterStr};${suffix}`;
416
+ }
417
+
418
+ function listPatterns(patterns) {
419
+ const entries = Object.entries(patterns || {}).sort(([a], [b]) =>
420
+ a.localeCompare(b)
421
+ );
422
+ if (entries.length === 0) return "(none)";
423
+ return entries
424
+ .map(([name, pat]) => `${name}[${(pat?.params || []).join(", ")}]`)
425
+ .join("\n");
426
+ }
427
+
351
428
  function formatCodecOutput(output) {
352
429
  if (typeof Buffer !== "undefined" && Buffer.isBuffer(output)) return output.toString("utf8");
353
430
  if (typeof Uint8Array !== "undefined" && output instanceof Uint8Array) return new TextDecoder().decode(output);
@@ -644,9 +721,16 @@ function resolveTypeExpressionHash(state, rawType) {
644
721
  ].filter(Boolean).join("\n");
645
722
 
646
723
  try {
647
- const annotated = annotate(source, { libraries: [stateLibrary(state)] });
724
+ const annotated = annotate(source, {
725
+ libraries: [stateLibrary(state)],
726
+ patterns: state.patterns
727
+ });
648
728
  const hash = annotated.representatives[INPUT_TYPE_NAME];
649
- if (!hash) throw new Error(":input type expression did not produce a code hash");
729
+ if (!hash) {
730
+ throw new Error(
731
+ ":input type expression did not produce a code hash"
732
+ );
733
+ }
650
734
  state.codes = codes.dump();
651
735
  return hash;
652
736
  } catch (error) {
@@ -672,7 +756,10 @@ function resolvePatternExpression(state, patternExpr) {
672
756
  patternExpr
673
757
  ].filter(Boolean).join("\n");
674
758
  try {
675
- const annotated = annotate(source, { libraries: [stateLibrary(state)] });
759
+ const annotated = annotate(source, {
760
+ libraries: [stateLibrary(state)],
761
+ patterns: state.patterns
762
+ });
676
763
  const mainRel = annotated.rels.__main__;
677
764
  if (!mainRel || !mainRel.typePatternGraph) {
678
765
  throw new Error(`Invalid pattern expression '${patternExpr}'`);
@@ -771,6 +858,25 @@ function completeCommandArgument(line, state) {
771
858
  const argStart = 1 + firstSpace + 1;
772
859
  const arg = line.slice(argStart);
773
860
 
861
+ if (command === "pattern" || command === "pat") {
862
+ const trimmed = arg.trimStart();
863
+ const leadingWhitespace = arg.length - trimmed.length;
864
+ const prefix = line.slice(0, argStart + leadingWhitespace);
865
+ const names = Object.keys(state.patterns || {}).sort();
866
+ const matches = names
867
+ .filter((name) => name.startsWith(trimmed))
868
+ .map((name) => `${prefix}${name}`);
869
+ if (trimmed.startsWith("@")) {
870
+ for (const [name, p] of Object.entries(state.patterns || {})) {
871
+ const hash = resolvePatternCanonicalName(state, name, p);
872
+ if (hash && hash.startsWith(trimmed)) {
873
+ matches.push(`${prefix}${hash}`);
874
+ }
875
+ }
876
+ }
877
+ return [matches, line];
878
+ }
879
+
774
880
  if (command === "codec" || command === "codecs") {
775
881
  return completeCodecCommand(line, argStart, arg, state);
776
882
  }
@@ -877,7 +983,10 @@ function resolveKFilePath(filePath) {
877
983
  if (fs.existsSync(strippedEx)) return strippedEx;
878
984
  } catch {}
879
985
  try {
880
- const fromRepoStripped = path.resolve(path.dirname(fileURLToPath(import.meta.url)), strippedEx);
986
+ const fromRepoStripped = path.resolve(
987
+ path.dirname(fileURLToPath(import.meta.url)),
988
+ strippedEx
989
+ );
881
990
  if (fs.existsSync(fromRepoStripped)) return fromRepoStripped;
882
991
  } catch {}
883
992
  return filePath;
@@ -908,7 +1017,8 @@ function loadSourceOrKlib(state, targetPath, options = {}) {
908
1017
  const fullSource = [preamble, source].filter(Boolean).join("\n");
909
1018
  const compileOptions = {
910
1019
  source: targetPath,
911
- libraries: [stateLibrary(state)]
1020
+ libraries: [stateLibrary(state)],
1021
+ patterns: state.patterns
912
1022
  };
913
1023
  try {
914
1024
  const lib = compileWithOptionalIdentity(fullSource, compileOptions);
@@ -972,15 +1082,29 @@ function compileSnippetArtifacts(source, state, sourceName = "<repl>") {
972
1082
  const fullSource = [preamble, source].filter(Boolean).join("\n");
973
1083
  const options = {
974
1084
  source: sourceName,
975
- libraries: [stateLibrary(state)]
1085
+ libraries: [stateLibrary(state)],
1086
+ patterns: state.patterns
976
1087
  };
977
1088
  try {
978
1089
  const lib = compileWithOptionalIdentity(fullSource, options);
979
- const { relAlias, meta } = libraryOriginsFromSource(source, fullSource, lib, options);
1090
+ const { relAlias, meta } = libraryOriginsFromSource(
1091
+ source,
1092
+ fullSource,
1093
+ lib,
1094
+ options
1095
+ );
980
1096
  const annotated = annotateWithOptionalIdentity(fullSource, options);
981
1097
  return {
982
1098
  annotated,
983
- lib: { ...lib, relAlias, meta },
1099
+ lib: {
1100
+ ...lib,
1101
+ relAlias,
1102
+ meta,
1103
+ patterns: {
1104
+ ...(state.patterns || {}),
1105
+ ...(annotated.sourceDefs?.patterns || {})
1106
+ }
1107
+ },
984
1108
  lineOffset: preambleLineCount(preamble)
985
1109
  };
986
1110
  } catch (error) {
@@ -1233,14 +1357,49 @@ async function evaluateCommand(line, state) {
1233
1357
  case "rel": {
1234
1358
  if (!arg) throw new Error(`${usagePrefix}rel requires a relation name`);
1235
1359
  if (arg.includes("=")) {
1236
- throw new Error("Relation definitions use syntax: name = relExpr; Use :rel <name> to show a relation definition");
1360
+ throw new Error(
1361
+ "Relation definitions use syntax: name = relExpr; " +
1362
+ "Use :rel <name> to show a relation definition"
1363
+ );
1237
1364
  }
1238
1365
  const { hash, rel } = resolveRel(state, arg);
1239
1366
  if (!rel) throw new Error(`Unknown relation '${arg}'`);
1240
- return [`${arg} = ${prettyRelation(rel, displayAliases(state), state.relAliases)}; -- ${hash}`];
1367
+ const prettyRelStr = prettyRelation(
1368
+ rel,
1369
+ displayAliases(state),
1370
+ state.relAliases
1371
+ );
1372
+ return [`${arg} = ${prettyRelStr}; -- ${hash}`];
1241
1373
  }
1242
1374
  case "rels":
1243
1375
  return [listAliases(state.relAliases)];
1376
+ case "pattern":
1377
+ case "pat": {
1378
+ if (!arg) {
1379
+ throw new Error(`${usagePrefix}pattern requires a pattern name`);
1380
+ }
1381
+ const rawName = arg.trim();
1382
+ let patternName = rawName.replace(/\[.*\]$/, "").trim();
1383
+ let pat = state.patterns?.[patternName];
1384
+ if (!pat && patternName.startsWith("@")) {
1385
+ for (const [name, p] of Object.entries(state.patterns || {})) {
1386
+ if (
1387
+ resolvePatternCanonicalName(state, name, p) === patternName
1388
+ ) {
1389
+ patternName = name;
1390
+ pat = p;
1391
+ break;
1392
+ }
1393
+ }
1394
+ }
1395
+ if (!pat) {
1396
+ throw new Error(`Unknown pattern '${rawName}'`);
1397
+ }
1398
+ return [prettyPattern(patternName, pat, state)];
1399
+ }
1400
+ case "patterns":
1401
+ case "pats":
1402
+ return [listPatterns(state.patterns)];
1244
1403
  case "codec":
1245
1404
  case "codecs":
1246
1405
  return loadCodec(arg, state);
@@ -1333,19 +1492,23 @@ function helpText() {
1333
1492
  ":engine [wasm|pkir|js] display or switch evaluation engine",
1334
1493
  ":rel name show relation definition",
1335
1494
  ":rels list relation aliases",
1495
+ ":pattern name show named pattern definition (alias :pat)",
1496
+ ":patterns list defined named patterns (alias :pats)",
1336
1497
  ":limit [num|none] set maximum characters for value output",
1337
- ":codec load file load a codec module from file (e.g. :codec load codecs/int.mjs)",
1498
+ ":codec load file load a codec module from file",
1338
1499
  ":codec unload name unload a registered codec",
1339
1500
  ":codec list list loaded codecs (or simply :codecs)",
1340
- ":input <codec.mjs> [text] parse input using specified codec",
1341
- ":load [--no-alias] file",
1342
- " load .k source or .klib",
1501
+ ":input <codec> [txt] parse input using specified codec",
1502
+ ":load [--no-alias] f load .k source or .klib",
1343
1503
  ":klib file export state as a library",
1344
1504
  ":ko file expr export executable .ko using expr as main",
1345
1505
  ":reset clear state",
1346
1506
  ":help show this help",
1347
1507
  "",
1348
- "Raw k input is compiled as a snippet on top of the current state.",
1508
+ "Raw k input:",
1509
+ " name = expr; define relation alias",
1510
+ " name[P1, ...] = filter; define named pattern template",
1511
+ " ? name[...] evaluate named pattern filter",
1349
1512
  "A line ending with ';' plus only spaces closes the snippet.",
1350
1513
  "Use '\\' to force continuation."
1351
1514
  ].join("\n");
@@ -1553,5 +1716,8 @@ export {
1553
1716
  formatDuration,
1554
1717
  codecNames,
1555
1718
  ensureEnveloped,
1556
- patternFromFilter
1719
+ patternFromFilter,
1720
+ prettyPattern,
1721
+ resolvePatternCanonicalName,
1722
+ listPatterns
1557
1723
  };
@@ -78,8 +78,14 @@ if (fs.existsSync(coreKPath)) {
78
78
  vfsFiles["core.k"] = fs.readFileSync(coreKPath, "utf8");
79
79
  }
80
80
 
81
- // Add verified k-code files: arithmetics.k, ieee.k, poly.k, karatsuba-mult.k
82
- const verifiedExamples = ["arithmetics.k", "ieee.k", "poly.k", "karatsuba-mult.k"];
81
+ // Add verified k-code files
82
+ const verifiedExamples = [
83
+ "arithmetics.k",
84
+ "ieee.k",
85
+ "poly.k",
86
+ "karatsuba-mult.k",
87
+ "djson.k"
88
+ ];
83
89
  const examplesDir = path.join(projectRoot, "Examples");
84
90
  for (const filename of verifiedExamples) {
85
91
  const fullPath = path.join(examplesDir, filename);
@@ -645,6 +651,12 @@ const htmlContent = `<!DOCTYPE html>
645
651
  text-overflow: ellipsis;
646
652
  white-space: nowrap;
647
653
  flex: 1;
654
+ min-width: 0;
655
+ }
656
+
657
+ .k-btn-copy {
658
+ flex-shrink: 0;
659
+ margin-left: auto;
648
660
  }
649
661
 
650
662
  .k-tree-section-body {
@@ -1681,6 +1693,7 @@ const htmlContent = `<!DOCTYPE html>
1681
1693
  <option value="ieee.k">ieee.k</option>
1682
1694
  <option value="poly.k">poly.k</option>
1683
1695
  <option value="karatsuba-mult.k">karatsuba-mult.k</option>
1696
+ <option value="djson.k">djson.k</option>
1684
1697
  </optgroup>
1685
1698
  <optgroup label="Quick Expressions">
1686
1699
  <option value="expr:{} | ok">{} | ok</option>
@@ -1700,9 +1713,11 @@ const htmlContent = `<!DOCTYPE html>
1700
1713
  <option value="expr::codec load int.mjs">:codec load int.mjs</option>
1701
1714
  <option value="expr::codec load utf8.mjs">:codec load utf8.mjs</option>
1702
1715
  <option value="expr::codec load json.mjs">:codec load json.mjs</option>
1716
+ <option value="expr::codec load djson.mjs">:codec load djson.mjs</option>
1703
1717
  <option value="expr::codec load ieee.mjs">:codec load ieee.mjs</option>
1704
1718
  <option value="expr::codec load unit.mjs">:codec load unit.mjs</option>
1705
1719
  <option value="expr::input json.mjs {&quot;a&quot;:12,&quot;b&quot;:&quot;hello&quot;}">:input json.mjs {"a":12,"b":"hello"}</option>
1720
+ <option value="expr::input djson.mjs {&quot;a&quot;:12,&quot;b&quot;:&quot;hello&quot;}">:input djson.mjs {"a":12,"b":"hello"}</option>
1706
1721
  <option value="expr::input int.mjs [0,1,2]">:input int.mjs [0,1,2]</option>
1707
1722
  </optgroup>
1708
1723
  </select>
@@ -1837,6 +1852,16 @@ const htmlContent = `<!DOCTYPE html>
1837
1852
  <td><code>name = relExpr;</code></td>
1838
1853
  <td><b>Relation Alias:</b> Defines a local alias for a relation expression.<br><i>e.g.</i> <code>swap = { . y x, . x y };</code></td>
1839
1854
  </tr>
1855
+ <tr>
1856
+ <td><code>name[P1, ..., Pn] = filter;</code></td>
1857
+ <td>
1858
+ <b>Named Pattern Template:</b> Defines a reusable
1859
+ pattern template.<br>
1860
+ <i>e.g.</i><br>
1861
+ <code>bit[] = &lt; {} 0, {} 1 &gt;;</code> or<br>
1862
+ <code>list[X] = &lt; {} nil, ... &gt; = Y;</code>
1863
+ </td>
1864
+ </tr>
1840
1865
  </tbody>
1841
1866
  </table>
1842
1867
 
@@ -1872,8 +1897,15 @@ const htmlContent = `<!DOCTYPE html>
1872
1897
  <td><b>Variant Constructor:</b> Wraps document into variant <code>tag</code></td>
1873
1898
  </tr>
1874
1899
  <tr>
1875
- <td><code>? filterExpr</code></td>
1876
- <td><b>Pattern Filter:</b> Asserts input matches pattern; acts as partial identity and inductive termination witness (<i>e.g.</i> <code>?&lt; {} true, {} false &gt;</code>)</td>
1900
+ <td>
1901
+ <code>? filterExpr</code> /
1902
+ <code>? name[...]</code>
1903
+ </td>
1904
+ <td>
1905
+ <b>Pattern Filter:</b> Asserts input matches pattern;
1906
+ acts as partial identity and inductive witness
1907
+ (<i>e.g.</i> <code>? list[X]</code>)
1908
+ </td>
1877
1909
  </tr>
1878
1910
  <tr>
1879
1911
  <td><code>name</code> / <code>@hash</code></td>
@@ -1888,6 +1920,20 @@ const htmlContent = `<!DOCTYPE html>
1888
1920
  <table class="help-table">
1889
1921
  <tr><td><code>:engine [wasm|pkir|js]</code></td><td>Display or switch evaluation engine (<code>wasm</code>, <code>pkir</code>, or <code>js</code>)</td></tr>
1890
1922
  <tr><td><code>:load &lt;file&gt;</code></td><td>Load a <code>.k</code> source or <code>.klib</code> library into current state</td></tr>
1923
+ <tr>
1924
+ <td><code>:pattern &lt;name&gt;</code></td>
1925
+ <td>
1926
+ Display definition of named pattern
1927
+ <code>&lt;name&gt;</code> (alias <code>:pat</code>)
1928
+ </td>
1929
+ </tr>
1930
+ <tr>
1931
+ <td><code>:patterns</code></td>
1932
+ <td>
1933
+ List defined named pattern templates
1934
+ (alias <code>:pats</code>)
1935
+ </td>
1936
+ </tr>
1891
1937
  <tr><td><code>:rel &lt;name&gt;</code></td><td>Display definition of relation <code>&lt;name&gt;</code></td></tr>
1892
1938
  <tr><td><code>:rels</code></td><td>List all relation aliases</td></tr>
1893
1939
  <tr><td><code>:codecs</code></td><td>List all loaded format codecs</td></tr>
@@ -1908,6 +1954,14 @@ const htmlContent = `<!DOCTYPE html>
1908
1954
  <li><code>not = &lt; / true | false, / false | true &gt;;</code> &mdash; Polymorphic negation via branch projections and ordered choice</li>
1909
1955
  <li><code>{} | true not</code> &mdash; Compose constant relation <code>{} | true</code> with <code>not</code> (evaluates to <code>{} | false</code>)</li>
1910
1956
  <li><code>bool? = ?&lt; {} true, {} false &gt;;</code> &mdash; Define a boolean pattern filter relation</li>
1957
+ <li>
1958
+ <code>bit[] = &lt; {} 0, {} 1 &gt;;</code>
1959
+ &mdash; Define a 0-arity bit pattern
1960
+ </li>
1961
+ <li>
1962
+ <code>? list[bool[]]</code>
1963
+ &mdash; Instantiated pattern filter matching booleans
1964
+ </li>
1911
1965
  <li><code>not = bool? ?X &lt; / true | false, {} | true &gt; ?X;</code> &mdash; Filtered boolean negation: guarded by <code>bool?</code>, defaulting to <code>{} | true</code></li>
1912
1966
  <li><code>swap = { . y x, . x y };</code> &mdash; Define a product field-swapping relation</li>
1913
1967
  <li><code>{ {} | ok x, {} | nil y } swap</code> &mdash; Construct a product and pass it through <code>swap</code></li>
@@ -32,12 +32,22 @@ const derivationTests = fs.readdirSync(path.join(root, "tests/code-derivation"))
32
32
 
33
33
  const tests = [
34
34
  ["tests/test.mjs", node, ["tests/test.mjs"]],
35
+ [
36
+ "tests/test-named-patterns.mjs",
37
+ node,
38
+ ["tests/test-named-patterns.mjs"]
39
+ ],
35
40
  ...derivationTests,
36
41
  ["tests/test-fingerprint.mjs", node, ["tests/test-fingerprint.mjs"]],
37
42
  ["tests/test-hash-fuzz.mjs", node, ["tests/test-hash-fuzz.mjs"]],
38
43
  ["tests/test-hash-normalization.mjs", node, ["tests/test-hash-normalization.mjs"]],
39
44
  ["tests/test-structural-values.mjs", node, ["tests/test-structural-values.mjs"]],
40
45
  ["tests/test-codecs.mjs", node, ["tests/test-codecs.mjs"]],
46
+ [
47
+ "tests/test-djson-methods.mjs",
48
+ node,
49
+ ["tests/test-djson-methods.mjs"]
50
+ ],
41
51
  ["tests/test-deep-wire-value.mjs", node, ["tests/test-deep-wire-value.mjs"]],
42
52
  ["tests/test-k-object.mjs", node, ["tests/test-k-object.mjs"]],
43
53
  ["tests/test-kir.mjs", node, ["tests/test-kir.mjs"]],
package/symbol-table.mjs CHANGED
@@ -93,6 +93,7 @@ function union(rels) {
93
93
  class SymbolTable {
94
94
  constructor() {
95
95
  this.rels = {}; // name -> {def: rel_exp}
96
+ this.patterns = {}; // name -> {params: string[], filter: filter_ast}
96
97
  this.codes = {
97
98
  [unitCode]: {
98
99
  code: "product",
@@ -109,12 +110,29 @@ class SymbolTable {
109
110
  }
110
111
 
111
112
  add_rel(name, rel) {
112
- if (this.rels[name] != undefined)
113
- console.error(`SymbolTable: rel '${name}' is redefined (lines ${rel.start?.line}:${rel.start?.column}...${rel.end?.line}:${rel.end?.column})`);
113
+ if (this.rels[name] != undefined) {
114
+ console.error(
115
+ `SymbolTable: rel '${name}' is redefined ` +
116
+ `(lines ${rel.start?.line}:${rel.start?.column}...` +
117
+ `${rel.end?.line}:${rel.end?.column})`
118
+ );
119
+ }
114
120
 
115
121
  this.rels[name] = {def: rel};
116
122
  }
117
123
 
124
+ add_pattern(name, params, filter) {
125
+ if (this.patterns[name] != undefined) {
126
+ console.error(
127
+ `SymbolTable: pattern '${name}' is redefined ` +
128
+ `(lines ${filter.start?.line}:${filter.start?.column}...` +
129
+ `${filter.end?.line}:${filter.end?.column})`
130
+ );
131
+ }
132
+
133
+ this.patterns[name] = { params, filter };
134
+ }
135
+
118
136
  add_code(name, code) {
119
137
  this.codes[name] = code;
120
138
  }
@@ -314,11 +314,11 @@ console.log("8. Verifying fast string decoding & bounded rendering...");
314
314
  );
315
315
  }
316
316
 
317
- // 9. DJSON Singleton Pattern Invariant & Round-Trip
318
- console.log("9. Verifying DJSON singleton pattern invariant...");
317
+ // 9. DJSON Singleton Pattern Invariants & Round-Trip
318
+ console.log("9. Verifying DJSON singleton pattern invariants...");
319
319
  {
320
320
  assert(Array.isArray(DjsonCodec.DJSON_PATTERN));
321
- assert.equal(DjsonCodec.DJSON_PATTERN.length, 29);
321
+ assert.equal(DjsonCodec.DJSON_PATTERN.length, 27);
322
322
 
323
323
  const samples = [
324
324
  null,
@@ -332,21 +332,34 @@ console.log("9. Verifying DJSON singleton pattern invariant...");
332
332
  { key: "value", nested: [1, 2, { deep: true }] }
333
333
  ];
334
334
 
335
- for (const sample of samples) {
336
- const jsonStr = JSON.stringify(sample);
337
- const enveloped = DjsonCodec.parse(jsonStr);
338
- assert.strictEqual(
339
- enveloped.pattern,
340
- DjsonCodec.DJSON_PATTERN,
341
- "Every DJSON value must share the exact singleton pattern"
342
- );
343
- const wire = SDK.encodeToWire(enveloped, enveloped.pattern);
344
- const decoded = SDK.decodeWire(wire);
345
- const restored = DjsonCodec.print(decoded.value);
335
+ for (const [label, codec, pat, relPath] of [
336
+ ["djson", DjsonCodec, DjsonCodec.DJSON_PATTERN, "codecs/djson.mjs"]
337
+ ]) {
338
+ for (const sample of samples) {
339
+ const jsonStr = JSON.stringify(sample);
340
+ const enveloped = codec.parse(jsonStr);
341
+ assert.strictEqual(
342
+ enveloped.pattern,
343
+ pat,
344
+ `Every ${label} value must share the exact singleton pattern`
345
+ );
346
+ const wire = SDK.encodeToWire(enveloped, enveloped.pattern);
347
+ const decoded = SDK.decodeWire(wire);
348
+ const restored = codec.print(decoded.value);
349
+ assert.deepEqual(
350
+ JSON.parse(restored),
351
+ sample,
352
+ `${label} round-trip mismatch for: ${jsonStr}`
353
+ );
354
+ }
355
+ const cliSample = JSON.stringify({ a: 1, b: [true, "ok"] });
356
+ const parseRes = runCLI(relPath, ["--parse"], cliSample);
357
+ assert.equal(parseRes.status, 0);
358
+ const printRes = runCLI(relPath, ["--print"], parseRes.stdout);
359
+ assert.equal(printRes.status, 0);
346
360
  assert.deepEqual(
347
- JSON.parse(restored),
348
- sample,
349
- `DJSON round-trip mismatch for: ${jsonStr}`
361
+ JSON.parse(printRes.stdout.toString("utf8")),
362
+ JSON.parse(cliSample)
350
363
  );
351
364
  }
352
365
  }