@fraczak/k 7.1.0 → 8.0.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 (64) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/DOCS/BIG_PICTURE.md +54 -7
  3. package/DOCS/CODECS.md +47 -2
  4. package/DOCS/CODEC_ARCHITECTURE.md +14 -5
  5. package/DOCS/DICTIONARY.md +13 -4
  6. package/DOCS/FILE_FORMATS.md +3 -2
  7. package/DOCS/JSON_APPROACHES_COMPARISON.md +306 -0
  8. package/DOCS/NAMED_PATTERNS.md +317 -0
  9. package/DOCS/PATTERNS.md +59 -11
  10. package/DOCS/REPL.md +24 -5
  11. package/DOCS/WEB_REPL_REQUIREMENTS.md +4 -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-btrie.k +414 -0
  18. package/Examples/djson-ptrie.k +663 -0
  19. package/Examples/djson.k +323 -0
  20. package/Examples/esc.k +1 -1
  21. package/Examples/karatsuba-mult.k +16 -13
  22. package/Examples/list.k +8 -4
  23. package/Examples/nat.k +17 -11
  24. package/Examples/poly.k +11 -3
  25. package/Examples/vhdl_to_k.k +43 -31
  26. package/README.md +42 -15
  27. package/Value.mjs +28 -3
  28. package/backend-api.mjs +5 -1
  29. package/browser/repl-browser.mjs +6 -0
  30. package/browser/tree-view.mjs +82 -24
  31. package/browser/vfs-data.mjs +21 -12
  32. package/codecs/README.md +3 -0
  33. package/codecs/djson-btrie.mjs +560 -0
  34. package/codecs/djson-ptrie.mjs +587 -0
  35. package/codecs/djson.mjs +270 -0
  36. package/codecs/int.mjs +2 -1
  37. package/codecs/json.mjs +7 -2
  38. package/codecs/k-parse.mjs +7 -1
  39. package/codecs/k-print.mjs +4 -0
  40. package/codecs/runtime/codec-sdk.mjs +16 -2
  41. package/codecs/runtime/pattern-json.mjs +80 -1
  42. package/codecs/runtime/prefix-codec.mjs +629 -67
  43. package/codecs/runtime/show-value.mjs +93 -51
  44. package/codecs/runtime/unicode-string.mjs +29 -6
  45. package/codecs/show.mjs +4 -0
  46. package/codecs/utf16.mjs +2 -1
  47. package/core.k +79 -115
  48. package/index.mjs +3 -0
  49. package/named-patterns.mjs +267 -0
  50. package/object.mjs +31 -3
  51. package/package.json +5 -1
  52. package/parser.jison +68 -5
  53. package/parser.mjs +111 -60
  54. package/pretty.mjs +5 -0
  55. package/repl.mjs +245 -28
  56. package/scripts/build-repl-html.mjs +76 -5
  57. package/scripts/perf-djson.mjs +1142 -0
  58. package/scripts/run-tests.mjs +5 -0
  59. package/symbol-table.mjs +20 -2
  60. package/tests/bench-djson-tries.mjs +340 -0
  61. package/tests/test-codecs.mjs +211 -0
  62. package/tests/test-named-patterns.mjs +189 -0
  63. package/tests/test-repl-html.mjs +52 -4
  64. package/tests/test-repl.mjs +223 -0
@@ -0,0 +1,1142 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { performance } from "node:perf_hooks";
6
+
7
+ import {
8
+ codes,
9
+ decodeWire,
10
+ encodeToWire,
11
+ executeKVM
12
+ } from "../backend-api.mjs";
13
+ import { createState, evaluateInput } from "../repl.mjs";
14
+ import * as jsonCodec from "../codecs/json.mjs";
15
+ import * as djsonCodec from "../codecs/djson.mjs";
16
+ import btrieCodec, {
17
+ bitPathToIndex,
18
+ indexToBitPath
19
+ } from "../codecs/djson-btrie.mjs";
20
+ import ptrieCodec from "../codecs/djson-ptrie.mjs";
21
+ import {
22
+ compileWasmArtifactFromObject,
23
+ instantiateWasmArtifact,
24
+ writeValueToArena
25
+ } from "../backends/wasm/src/wasm.mjs";
26
+ import {
27
+ compileCaseExecutable,
28
+ prepareRelation,
29
+ runExecutable,
30
+ runExecutableMainBench,
31
+ wireInput
32
+ } from "../backends/llvm/tests/perf-support.mjs";
33
+
34
+ function printHelp() {
35
+ console.log(`Usage: node scripts/perf-djson.mjs [options]
36
+
37
+ Dynamic JSON Codecs & Schemas Benchmark Suite.
38
+ Compares the three universal dynamic JSON implementations:
39
+ - djson (sorted cons-list objects & cons-list arrays)
40
+ - djson-btrie (uncompressed binary bit-trie objects & arrays)
41
+ - djson-ptrie (Patricia/Radix trie objects & arrays, trie-array style)
42
+ and contrasts codec wire sizes against the schema-specializing json codec.
43
+
44
+ Part 1 benchmarks codec operations (parse, encodeToWire, decodeWire,
45
+ print, and wire payload sizes) across 5 value categories:
46
+ 1. Shared-prefix objects
47
+ 2. Varied-key objects
48
+ 3. Short-key objects
49
+ 4. Homogeneous number arrays
50
+ 5. Mixed nested documents (arrays of objects)
51
+
52
+ Part 2 benchmarks K schema relations (dot, has_key?, nth, obj_size,
53
+ list_size, remove, put) across kVM, WebAssembly, PKIR (Wasm), and
54
+ LLVM -O3 native binaries.
55
+
56
+ Options:
57
+ --sizes <csv> Comma-separated sizes N (default: 10,100,500)
58
+ --calls <n> Calls per relation benchmark (default: 1000)
59
+ --codec-iters <n> Iterations for codec timing (default: 100)
60
+ --backends <csv> Backends to run: kvm,wasm,pkir,llvm (default: all)
61
+ --codecs-only Run only Part 1 (Codec & Wire Format benchmark)
62
+ --schemas-only Run only Part 2 (Schema Relation Execution benchmark)
63
+ --json Print machine-readable JSON report at the end
64
+ -h, --help Show this help message and exit
65
+
66
+ Environment Variables:
67
+ SIZES Comma-separated sizes (e.g. SIZES=10,100,500)
68
+ CALLS Number of calls per relation case (default: 1000)
69
+ CODEC_ITERS Iterations for codec microbenchmarks (default: 100)
70
+ BACKENDS Target backends (e.g. BACKENDS=wasm,llvm)`);
71
+ }
72
+
73
+ if (process.argv.includes("-h") || process.argv.includes("--help")) {
74
+ printHelp();
75
+ process.exit(0);
76
+ }
77
+
78
+ function getArgValue(flag) {
79
+ const idx = process.argv.indexOf(flag);
80
+ if (idx !== -1 && idx + 1 < process.argv.length) {
81
+ return process.argv[idx + 1];
82
+ }
83
+ return null;
84
+ }
85
+
86
+ const sizesRaw =
87
+ getArgValue("--sizes") || process.env.SIZES || "10,100,500";
88
+ const SIZES = sizesRaw
89
+ .split(",")
90
+ .map((s) => Number.parseInt(s.trim(), 10))
91
+ .filter((n) => Number.isFinite(n) && n > 0);
92
+
93
+ const CALLS = Number.parseInt(
94
+ getArgValue("--calls") || process.env.CALLS || "1000",
95
+ 10
96
+ );
97
+ const CODEC_ITERS = Number.parseInt(
98
+ getArgValue("--codec-iters") || process.env.CODEC_ITERS || "100",
99
+ 10
100
+ );
101
+
102
+ const backendsRaw = (
103
+ getArgValue("--backends") ||
104
+ process.env.BACKENDS ||
105
+ "kvm,wasm,pkir,llvm"
106
+ ).toLowerCase();
107
+ const BACKENDS = new Set(
108
+ backendsRaw.split(",").map((s) => s.trim()).filter(Boolean)
109
+ );
110
+
111
+ const runCodecsPart = !process.argv.includes("--schemas-only");
112
+ const runSchemasPart = !process.argv.includes("--codecs-only");
113
+ const emitJson = process.argv.includes("--json");
114
+
115
+ // -------------------------------------------------------------------
116
+ // Dataset Generators
117
+ // -------------------------------------------------------------------
118
+
119
+ const WORDS = [
120
+ "account", "balance", "category", "database", "endpoint",
121
+ "filename", "geometry", "hostname", "identity", "journal",
122
+ "keyboard", "language", "metadata", "network", "operator",
123
+ "password", "quantity", "resource", "security", "template",
124
+ "username", "validate", "workflow", "x509cert", "yearbook", "zoneinfo"
125
+ ];
126
+
127
+ function makeSharedPrefixObject(n) {
128
+ const obj = {};
129
+ for (let i = 0; i < n; i++) {
130
+ const k = `field_prefix_${String(i).padStart(4, "0")}`;
131
+ obj[k] = i * 10;
132
+ }
133
+ return obj;
134
+ }
135
+
136
+ function makeVariedKeyObject(n) {
137
+ const obj = {};
138
+ for (let i = 0; i < n; i++) {
139
+ const w1 = WORDS[i % WORDS.length];
140
+ const w2 = WORDS[Math.floor(i / WORDS.length) % WORDS.length];
141
+ const k = `${w1}_${i}_${w2}_attr`;
142
+ obj[k] = i * 10;
143
+ }
144
+ return obj;
145
+ }
146
+
147
+ function makeShortKeyObject(n) {
148
+ const obj = {};
149
+ for (let i = 0; i < n; i++) {
150
+ obj[`k${i}`] = i * 10;
151
+ }
152
+ return obj;
153
+ }
154
+
155
+ function makeNumberArray(n) {
156
+ return Array.from({ length: n }, (_, i) => i * 3);
157
+ }
158
+
159
+ function makeMixedNestedDoc(n) {
160
+ const arr = [];
161
+ for (let i = 0; i < n; i++) {
162
+ arr.push({
163
+ id: i,
164
+ username: `user_${String(i).padStart(4, "0")}`,
165
+ active: (i & 1) === 0,
166
+ score: i * 1.5,
167
+ meta: {
168
+ role: i % 3 === 0 ? "admin" : "member",
169
+ region: WORDS[i % 5]
170
+ }
171
+ });
172
+ }
173
+ return arr;
174
+ }
175
+
176
+ function indexToKLiteral(idx) {
177
+ const bits = indexToBitPath(idx);
178
+ const tags = bits.slice().reverse().map((b) => `|${b}`).join("");
179
+ return `({}|_${tags})`;
180
+ }
181
+
182
+ function unfoldNatValue(val) {
183
+ const bits = [];
184
+ let cur = val;
185
+ while (cur && typeof cur.tag === "string") {
186
+ if (cur.tag === "_") break;
187
+ bits.push(cur.tag === "1" ? 1 : 0);
188
+ cur = cur.value;
189
+ }
190
+ return bitPathToIndex(bits);
191
+ }
192
+
193
+ // -------------------------------------------------------------------
194
+ // Part 1: Codec & Wire Representation Benchmark
195
+ // -------------------------------------------------------------------
196
+
197
+ const CODECS = [
198
+ {
199
+ name: "djson",
200
+ desc: "Sorted cons-list",
201
+ codec: djsonCodec,
202
+ pattern: djsonCodec.DJSON_PATTERN
203
+ },
204
+ {
205
+ name: "djson-btrie",
206
+ desc: "Binary bit-trie",
207
+ codec: btrieCodec,
208
+ pattern: btrieCodec.pattern
209
+ },
210
+ {
211
+ name: "djson-ptrie",
212
+ desc: "Patricia/Radix trie",
213
+ codec: ptrieCodec,
214
+ pattern: ptrieCodec.pattern
215
+ },
216
+ {
217
+ name: "json (spec)",
218
+ desc: "Schema-specializing",
219
+ codec: jsonCodec,
220
+ pattern: null
221
+ }
222
+ ];
223
+
224
+ const WORKLOADS = [
225
+ {
226
+ id: "shared_obj",
227
+ title: "1A. Shared-Prefix Objects",
228
+ build: makeSharedPrefixObject
229
+ },
230
+ {
231
+ id: "varied_obj",
232
+ title: "1B. Varied-Key Objects",
233
+ build: makeVariedKeyObject
234
+ },
235
+ {
236
+ id: "short_obj",
237
+ title: "1C. Short-Key Objects",
238
+ build: makeShortKeyObject
239
+ },
240
+ {
241
+ id: "num_array",
242
+ title: "1D. Homogeneous Number Arrays",
243
+ build: makeNumberArray
244
+ },
245
+ {
246
+ id: "mixed_doc",
247
+ title: "1E. Mixed Nested Documents (Array of Objects)",
248
+ build: makeMixedNestedDoc
249
+ }
250
+ ];
251
+
252
+ function benchCodecOnJson(codecEntry, rawJsonObj, iters) {
253
+ const rawStr = JSON.stringify(rawJsonObj);
254
+ const jsonBytes = Buffer.byteLength(rawStr, "utf8");
255
+ const { codec, pattern: fixedPattern } = codecEntry;
256
+
257
+ // Warmup & correctness check
258
+ const kVal = codec.parse(rawStr);
259
+ const envPattern = fixedPattern || kVal.pattern;
260
+ const wireStr = encodeToWire(kVal, envPattern);
261
+ const decoded = decodeWire(wireStr);
262
+ const printedStr = codec.print(decoded.value);
263
+ assert.deepStrictEqual(
264
+ JSON.parse(printedStr),
265
+ rawJsonObj,
266
+ `${codecEntry.name} round-trip mismatch`
267
+ );
268
+
269
+ const wireBytes = wireStr.length;
270
+ const patNodes = envPattern ? envPattern.length : 0;
271
+
272
+ // Scale iterations down for large payloads to keep runs fast
273
+ const effectiveIters = Math.max(
274
+ 5,
275
+ Math.min(iters, Math.floor((iters * 2000) / Math.max(200, jsonBytes)))
276
+ );
277
+
278
+ const tParse0 = performance.now();
279
+ let lastVal = kVal;
280
+ for (let i = 0; i < effectiveIters; i++) {
281
+ lastVal = codec.parse(rawStr);
282
+ }
283
+ const parseUs = ((performance.now() - tParse0) * 1000) / effectiveIters;
284
+
285
+ const tEnc0 = performance.now();
286
+ let lastWire = wireStr;
287
+ for (let i = 0; i < effectiveIters; i++) {
288
+ lastWire = encodeToWire(lastVal, envPattern);
289
+ }
290
+ const encodeUs = ((performance.now() - tEnc0) * 1000) / effectiveIters;
291
+
292
+ const tDec0 = performance.now();
293
+ let lastDec = decoded;
294
+ for (let i = 0; i < effectiveIters; i++) {
295
+ lastDec = decodeWire(lastWire);
296
+ }
297
+ const decodeUs = ((performance.now() - tDec0) * 1000) / effectiveIters;
298
+
299
+ const tPrint0 = performance.now();
300
+ let lastStr = printedStr;
301
+ for (let i = 0; i < effectiveIters; i++) {
302
+ lastStr = codec.print(lastDec.value);
303
+ }
304
+ const printUs = ((performance.now() - tPrint0) * 1000) / effectiveIters;
305
+ assert.strictEqual(lastStr.length, printedStr.length);
306
+
307
+ return {
308
+ codec: codecEntry.name,
309
+ jsonBytes,
310
+ patNodes,
311
+ wireBytes,
312
+ wireRatio: wireBytes / jsonBytes,
313
+ parseUs,
314
+ encodeUs,
315
+ decodeUs,
316
+ printUs,
317
+ totalUs: parseUs + encodeUs + decodeUs + printUs
318
+ };
319
+ }
320
+
321
+ function runPart1CodecBenchmarks() {
322
+ const results = [];
323
+ const bar = "=".repeat(74);
324
+ console.log(bar);
325
+ console.log("PART 1: CODEC & WIRE FORMAT BENCHMARK");
326
+ console.log(bar);
327
+
328
+ for (const wl of WORKLOADS) {
329
+ console.log(`\n### ${wl.title}`);
330
+ for (const n of SIZES) {
331
+ const data = wl.build(n);
332
+ const rawBytes = Buffer.byteLength(JSON.stringify(data), "utf8");
333
+ console.log(`\n--- Size N = ${n} (Raw JSON: ${rawBytes} B) ---`);
334
+ console.log(
335
+ "Codec | PatNodes | Wire (B) | Wire/JSON | Parse(µs) " +
336
+ "| Enc(µs) | Dec(µs) | Print(µs) | Total(µs)"
337
+ );
338
+ console.log(
339
+ "-------------+----------+----------+-----------+-----------" +
340
+ "+---------+---------+-----------+----------"
341
+ );
342
+
343
+ for (const c of CODECS) {
344
+ const r = benchCodecOnJson(c, data, CODEC_ITERS);
345
+ results.push({ workload: wl.id, size: n, ...r });
346
+ console.log(
347
+ `${c.name.padEnd(12)} | ${String(r.patNodes).padStart(8)} | ` +
348
+ `${String(r.wireBytes).padStart(8)} | ` +
349
+ `${(r.wireRatio.toFixed(2) + "x").padStart(9)} | ` +
350
+ `${r.parseUs.toFixed(1).padStart(9)} | ` +
351
+ `${r.encodeUs.toFixed(1).padStart(7)} | ` +
352
+ `${r.decodeUs.toFixed(1).padStart(7)} | ` +
353
+ `${r.printUs.toFixed(1).padStart(9)} | ` +
354
+ `${r.totalUs.toFixed(1).padStart(9)}`
355
+ );
356
+ }
357
+ }
358
+ }
359
+ return results;
360
+ }
361
+
362
+ // -------------------------------------------------------------------
363
+ // Part 2: Schema Relation Execution Benchmark
364
+ // -------------------------------------------------------------------
365
+
366
+ const SCHEMAS = [
367
+ {
368
+ name: "djson",
369
+ kFile: "Examples/djson.k",
370
+ codec: djsonCodec,
371
+ pattern: djsonCodec.DJSON_PATTERN,
372
+ fst: "/list /cons .car",
373
+ snd: "/list /cons .cdr /cons .car",
374
+ trd: "/list /cons .cdr /cons .cdr /cons .car"
375
+ },
376
+ {
377
+ name: "djson-btrie",
378
+ kFile: "Examples/djson-btrie.k",
379
+ codec: btrieCodec,
380
+ pattern: btrieCodec.pattern,
381
+ fst: "{ 0 index, /list list } nth",
382
+ snd: "{ 1 index, /list list } nth",
383
+ trd: "{ 0 |1 index, /list list } nth"
384
+ },
385
+ {
386
+ name: "djson-ptrie",
387
+ kFile: "Examples/djson-ptrie.k",
388
+ codec: ptrieCodec,
389
+ pattern: ptrieCodec.pattern,
390
+ fst: "{ 0 index, /list list } nth",
391
+ snd: "{ 1 index, /list list } nth",
392
+ trd: "{ 0 |1 index, /list list } nth"
393
+ }
394
+ ];
395
+
396
+ async function buildSchemaState(schema, extraDefs = "") {
397
+ const st = createState({ engine: "wasm" });
398
+ await evaluateInput(`:load ${schema.kFile}`, st);
399
+ const stdDefs = `
400
+ fst = ${schema.fst};
401
+ snd = ${schema.snd};
402
+ trd = ${schema.trd};
403
+ bench_dot = json? { fst /string key, snd /object object } dot json?;
404
+ bench_has_key = json? <
405
+ {
406
+ { fst /string key, snd /object object } has_key? if,
407
+ true then
408
+ } .then,
409
+ false
410
+ > json?;
411
+ bench_obj_size = json? /object obj_size;
412
+ bench_list_size = json? /list list_size;
413
+ bench_remove = json?
414
+ { fst /string key, snd /object object } remove object;
415
+ bench_put = json?
416
+ { fst /string key, snd val, trd /object object } put object;
417
+ ${extraDefs}
418
+ `;
419
+ await evaluateInput(stdDefs, st);
420
+ return st;
421
+ }
422
+
423
+ async function compileRelationRunners(
424
+ state,
425
+ schema,
426
+ relName,
427
+ sampleInput,
428
+ cacheDir
429
+ ) {
430
+ const prepared = prepareRelation(state, relName);
431
+ const wire = wireInput(sampleInput, schema.pattern);
432
+
433
+ let wasmArtifact = null;
434
+ let wasmRunner = null;
435
+ let wasmCompileMs = 0;
436
+ if (BACKENDS.has("wasm")) {
437
+ const t0 = performance.now();
438
+ wasmArtifact = await compileWasmArtifactFromObject(prepared.object, {
439
+ relation: prepared.relationName,
440
+ inputEnvelopePattern: wire.inputPattern,
441
+ usePKIR: false
442
+ });
443
+ wasmRunner = await instantiateWasmArtifact(wasmArtifact);
444
+ wasmCompileMs = performance.now() - t0;
445
+ }
446
+
447
+ let pkirArtifact = null;
448
+ let pkirRunner = null;
449
+ let pkirCompileMs = 0;
450
+ if (BACKENDS.has("pkir")) {
451
+ const t0 = performance.now();
452
+ pkirArtifact = await compileWasmArtifactFromObject(prepared.object, {
453
+ relation: prepared.relationName,
454
+ inputEnvelopePattern: wire.inputPattern,
455
+ usePKIR: true
456
+ });
457
+ pkirRunner = await instantiateWasmArtifact(pkirArtifact);
458
+ pkirCompileMs = performance.now() - t0;
459
+ }
460
+
461
+ let llvmExePath = null;
462
+ let llvmCompileMs = 0;
463
+ if (BACKENDS.has("llvm")) {
464
+ const t0 = performance.now();
465
+ llvmExePath = compileCaseExecutable({
466
+ object: prepared.object,
467
+ relationName: prepared.relationName,
468
+ relHash: prepared.relHash,
469
+ inputPattern: wire.inputPattern,
470
+ cacheDir,
471
+ sourceLabel: `${schema.name}_${relName}`,
472
+ runtimeMode: "fast",
473
+ clangOpt: "-O3",
474
+ usePKIR: false
475
+ });
476
+ llvmCompileMs = performance.now() - t0;
477
+ assert(
478
+ typeof llvmExePath === "string" && llvmExePath.length > 0,
479
+ `LLVM compilation failed for ${schema.name}:${relName}`
480
+ );
481
+ }
482
+
483
+ return {
484
+ state,
485
+ schema,
486
+ relName,
487
+ prepared,
488
+ wasmBytes: wasmArtifact ? wasmArtifact.length : 0,
489
+ pkirBytes: pkirArtifact ? pkirArtifact.length : 0,
490
+ wasmRunner,
491
+ pkirRunner,
492
+ llvmExePath,
493
+ wasmCompileMs,
494
+ pkirCompileMs,
495
+ llvmCompileMs
496
+ };
497
+ }
498
+
499
+ function benchWasmRunner(runner, kInput, verifyFn, calls) {
500
+ const outVal = runner.executeValue(kInput);
501
+ assert(outVal !== undefined, "Wasm execution returned undefined");
502
+ verifyFn(outVal, "Wasm");
503
+
504
+ const ptrIn = writeValueToArena(
505
+ runner.exports,
506
+ kInput,
507
+ runner.inputPattern,
508
+ 0,
509
+ new Map(),
510
+ runner.tags,
511
+ runner.metadata.inputPattern
512
+ );
513
+ const mark = runner.exports.arena_mark();
514
+ const entryFn = runner.exports[runner.metadata.entry];
515
+
516
+ // Warmup and measure single call latency
517
+ runner.exports.arena_reset(mark);
518
+ const tWarm0 = performance.now();
519
+ entryFn(ptrIn);
520
+ const singleMs = Math.max(0.001, performance.now() - tWarm0);
521
+
522
+ // Target ~50ms total execution time for timing loop
523
+ const effectiveCalls = Math.max(
524
+ 10,
525
+ Math.min(calls, Math.floor(50 / singleMs))
526
+ );
527
+
528
+ const t0 = performance.now();
529
+ for (let i = 0; i < effectiveCalls; i++) {
530
+ runner.exports.arena_reset(mark);
531
+ const resPtr = entryFn(ptrIn);
532
+ if (resPtr < 0) throw new Error("Wasm entryFn failed");
533
+ }
534
+ return ((performance.now() - t0) * 1e6) / effectiveCalls;
535
+ }
536
+
537
+ async function executeRelationBench(compiled, kInput, verifyFn, calls) {
538
+ const {
539
+ state,
540
+ schema,
541
+ prepared,
542
+ wasmRunner,
543
+ pkirRunner,
544
+ llvmExePath
545
+ } = compiled;
546
+ const wire = wireInput(kInput, schema.pattern);
547
+ const wireBytes = wire.inputWire.length;
548
+
549
+ let kvmNs = null;
550
+ if (BACKENDS.has("kvm")) {
551
+ const ctx = {
552
+ rels: state.rels,
553
+ codes: state.codes,
554
+ findCode: (h) =>
555
+ codes.find(h) ||
556
+ state.codes[String(h).replace(/^@/, "")] ||
557
+ state.codes[h],
558
+ options: { envelopeFree: true }
559
+ };
560
+ const outVal = executeKVM(prepared.kvmFunc, kInput, ctx);
561
+ assert(outVal !== undefined, "kVM returned undefined");
562
+ verifyFn(outVal, "kVM");
563
+
564
+ const kvmCalls = Math.max(5, Math.min(200, Math.floor(calls / 5)));
565
+ for (let i = 0; i < 3; i++) {
566
+ executeKVM(prepared.kvmFunc, kInput, ctx);
567
+ }
568
+ const t0 = performance.now();
569
+ for (let i = 0; i < kvmCalls; i++) {
570
+ executeKVM(prepared.kvmFunc, kInput, ctx);
571
+ }
572
+ kvmNs = ((performance.now() - t0) * 1e6) / kvmCalls;
573
+ }
574
+
575
+ let wasmNs = null;
576
+ if (wasmRunner) {
577
+ wasmNs = benchWasmRunner(wasmRunner, kInput, verifyFn, calls);
578
+ }
579
+
580
+ let pkirNs = null;
581
+ if (pkirRunner) {
582
+ pkirNs = benchWasmRunner(pkirRunner, kInput, verifyFn, calls);
583
+ }
584
+
585
+ let llvmNs = null;
586
+ if (llvmExePath) {
587
+ const llvmOutWire = await runExecutable(llvmExePath, wire.inputWire);
588
+ const llvmDecoded = decodeWire(llvmOutWire);
589
+ verifyFn(llvmDecoded.value, "LLVM");
590
+
591
+ const llvmCalls = Math.max(
592
+ 10,
593
+ Math.min(calls, Math.floor(100_000_000 / (wasmNs || 50_000)))
594
+ );
595
+ const llvmBench = await runExecutableMainBench(
596
+ llvmExePath,
597
+ wire.inputWire,
598
+ llvmCalls
599
+ );
600
+ llvmNs = llvmBench.perCallNs;
601
+ }
602
+
603
+ return {
604
+ wireBytes,
605
+ wasmBytes: compiled.wasmBytes,
606
+ pkirBytes: compiled.pkirBytes,
607
+ wasmCompileMs: compiled.wasmCompileMs,
608
+ llvmCompileMs: compiled.llvmCompileMs,
609
+ kvmNs,
610
+ wasmNs,
611
+ pkirNs,
612
+ llvmNs
613
+ };
614
+ }
615
+
616
+ function formatNs(ns) {
617
+ if (ns === null || ns === undefined) return " n/a";
618
+ if (ns >= 1e6) return `${(ns / 1e6).toFixed(2).padStart(6)}ms`;
619
+ if (ns >= 1e4) return `${(ns / 1e3).toFixed(1).padStart(6)}µs`;
620
+ return `${ns.toFixed(0).padStart(6)}ns`;
621
+ }
622
+
623
+ function formatSpeedup(baseNs, curNs) {
624
+ if (!baseNs || !curNs) return " n/a";
625
+ const ratio = baseNs / curNs;
626
+ return `${ratio.toFixed(1).padStart(5)}x`;
627
+ }
628
+
629
+ function printRelationTableHeader() {
630
+ console.log(
631
+ "Schema | Wire (B) | Wasm (B) | kVM | Wasm | " +
632
+ "PKIR | LLVM -O3 | Wasm Spd | LLVM Spd"
633
+ );
634
+ console.log(
635
+ "-------------+----------+----------+----------+----------+---" +
636
+ "-------+----------+----------+---------"
637
+ );
638
+ }
639
+
640
+ function printRelationRow(schemaName, res, baseRes) {
641
+ const wasmSpd = baseRes
642
+ ? formatSpeedup(baseRes.wasmNs, res.wasmNs)
643
+ : " 1.0x";
644
+ const llvmSpd = baseRes
645
+ ? formatSpeedup(baseRes.llvmNs, res.llvmNs)
646
+ : " 1.0x";
647
+ console.log(
648
+ `${schemaName.padEnd(12)} | ${String(res.wireBytes).padStart(8)} | ` +
649
+ `${String(res.wasmBytes).padStart(8)} | ` +
650
+ `${formatNs(res.kvmNs)} | ` +
651
+ `${formatNs(res.wasmNs)} | ` +
652
+ `${formatNs(res.pkirNs)} | ` +
653
+ `${formatNs(res.llvmNs)} | ` +
654
+ `${wasmSpd.padStart(8)} | ` +
655
+ `${llvmSpd.padStart(8)}`
656
+ );
657
+ }
658
+
659
+ async function runPart2SchemaBenchmarks() {
660
+ const cacheDir = fs.mkdtempSync(
661
+ path.join(os.tmpdir(), "k-perf-djson-")
662
+ );
663
+ const results = [];
664
+
665
+ try {
666
+ const bar = "=".repeat(74);
667
+ console.log(`\n${bar}`);
668
+ console.log("PART 2: SCHEMA RELATION EXECUTION BENCHMARK");
669
+ console.log(bar);
670
+
671
+ // Build extra nth definitions for each size N
672
+ const nthTargets = new Map();
673
+ for (const n of SIZES) {
674
+ nthTargets.set(`nth_first_${n}`, 0);
675
+ nthTargets.set(`nth_mid_${n}`, Math.floor(n / 2));
676
+ nthTargets.set(`nth_last_${n}`, n - 1);
677
+ }
678
+ const extraNthDefs = Array.from(nthTargets.entries())
679
+ .map(
680
+ ([relName, idx]) =>
681
+ `${relName} = json? { ` +
682
+ `${indexToKLiteral(idx)} index, /list list } nth json?;`
683
+ )
684
+ .join("\n");
685
+
686
+ console.log(
687
+ "\n==> Compiling schema relations (Wasm, PKIR, LLVM -O3)..."
688
+ );
689
+ const compiledBySchema = new Map();
690
+ const samplePair = ["alpha", { alpha: 1, beta: 2 }];
691
+ const sampleTriple = ["gamma", 99, { alpha: 1, beta: 2 }];
692
+ const sampleObj = { alpha: 1, beta: 2 };
693
+ const sampleArr = [10, 20, 30];
694
+
695
+ for (const s of SCHEMAS) {
696
+ const st = await buildSchemaState(s, extraNthDefs);
697
+ const map = new Map();
698
+ map.set(
699
+ "bench_dot",
700
+ await compileRelationRunners(
701
+ st,
702
+ s,
703
+ "bench_dot",
704
+ s.codec.parse(JSON.stringify(samplePair)),
705
+ cacheDir
706
+ )
707
+ );
708
+ map.set(
709
+ "bench_has_key",
710
+ await compileRelationRunners(
711
+ st,
712
+ s,
713
+ "bench_has_key",
714
+ s.codec.parse(JSON.stringify(samplePair)),
715
+ cacheDir
716
+ )
717
+ );
718
+ map.set(
719
+ "bench_obj_size",
720
+ await compileRelationRunners(
721
+ st,
722
+ s,
723
+ "bench_obj_size",
724
+ s.codec.parse(JSON.stringify(sampleObj)),
725
+ cacheDir
726
+ )
727
+ );
728
+ map.set(
729
+ "bench_list_size",
730
+ await compileRelationRunners(
731
+ st,
732
+ s,
733
+ "bench_list_size",
734
+ s.codec.parse(JSON.stringify(sampleArr)),
735
+ cacheDir
736
+ )
737
+ );
738
+ map.set(
739
+ "bench_remove",
740
+ await compileRelationRunners(
741
+ st,
742
+ s,
743
+ "bench_remove",
744
+ s.codec.parse(JSON.stringify(samplePair)),
745
+ cacheDir
746
+ )
747
+ );
748
+ map.set(
749
+ "bench_put",
750
+ await compileRelationRunners(
751
+ st,
752
+ s,
753
+ "bench_put",
754
+ s.codec.parse(JSON.stringify(sampleTriple)),
755
+ cacheDir
756
+ )
757
+ );
758
+
759
+ for (const n of SIZES) {
760
+ const arrN = makeNumberArray(n);
761
+ const kArrN = s.codec.parse(JSON.stringify(arrN));
762
+ for (const pos of ["first", "mid", "last"]) {
763
+ const relName = `nth_${pos}_${n}`;
764
+ map.set(
765
+ relName,
766
+ await compileRelationRunners(st, s, relName, kArrN, cacheDir)
767
+ );
768
+ }
769
+ }
770
+ compiledBySchema.set(s.name, map);
771
+ }
772
+
773
+ // ---------------------------------------------------------------
774
+ // 2A. Object Lookup (`dot`) — Shared-Prefix Keys
775
+ // ---------------------------------------------------------------
776
+ console.log("\n### 2A. Object Lookup (`dot`) — Shared-Prefix Keys");
777
+ for (const n of SIZES) {
778
+ const obj = makeSharedPrefixObject(n);
779
+ const keys = Object.keys(obj).sort();
780
+ const queries = [
781
+ ["first", keys[0]],
782
+ ["mid", keys[Math.floor(n / 2)]],
783
+ ["last", keys[n - 1]]
784
+ ];
785
+ for (const [posLabel, targetKey] of queries) {
786
+ const expectedVal = obj[targetKey];
787
+ console.log(
788
+ `\n--- Shared-Prefix Object N = ${n}, ` +
789
+ `key = ${posLabel} ('${targetKey}') ---`
790
+ );
791
+ printRelationTableHeader();
792
+ let baseRes = null;
793
+ for (const s of SCHEMAS) {
794
+ const compiled = compiledBySchema.get(s.name).get("bench_dot");
795
+ const kInput = s.codec.parse(JSON.stringify([targetKey, obj]));
796
+ const res = await executeRelationBench(
797
+ compiled,
798
+ kInput,
799
+ (val, backend) => {
800
+ assert.strictEqual(
801
+ JSON.parse(s.codec.print(val)),
802
+ expectedVal,
803
+ `${s.name} (${backend}) dot mismatch`
804
+ );
805
+ },
806
+ CALLS
807
+ );
808
+ if (!baseRes) baseRes = res;
809
+ printRelationRow(s.name, res, baseRes);
810
+ results.push({
811
+ section: "dot_shared",
812
+ size: n,
813
+ position: posLabel,
814
+ schema: s.name,
815
+ ...res
816
+ });
817
+ }
818
+ }
819
+ }
820
+
821
+ // ---------------------------------------------------------------
822
+ // 2B. Object Lookup (`dot`) — Varied Keys
823
+ // ---------------------------------------------------------------
824
+ console.log("\n### 2B. Object Lookup (`dot`) — Varied Keys");
825
+ for (const n of SIZES) {
826
+ const obj = makeVariedKeyObject(n);
827
+ const keys = Object.keys(obj).sort();
828
+ const queries = [
829
+ ["first", keys[0]],
830
+ ["mid", keys[Math.floor(n / 2)]],
831
+ ["last", keys[n - 1]]
832
+ ];
833
+ for (const [posLabel, targetKey] of queries) {
834
+ const expectedVal = obj[targetKey];
835
+ console.log(
836
+ `\n--- Varied-Key Object N = ${n}, ` +
837
+ `key = ${posLabel} ('${targetKey}') ---`
838
+ );
839
+ printRelationTableHeader();
840
+ let baseRes = null;
841
+ for (const s of SCHEMAS) {
842
+ const compiled = compiledBySchema.get(s.name).get("bench_dot");
843
+ const kInput = s.codec.parse(JSON.stringify([targetKey, obj]));
844
+ const res = await executeRelationBench(
845
+ compiled,
846
+ kInput,
847
+ (val, backend) => {
848
+ assert.strictEqual(
849
+ JSON.parse(s.codec.print(val)),
850
+ expectedVal,
851
+ `${s.name} (${backend}) dot mismatch`
852
+ );
853
+ },
854
+ CALLS
855
+ );
856
+ if (!baseRes) baseRes = res;
857
+ printRelationRow(s.name, res, baseRes);
858
+ results.push({
859
+ section: "dot_varied",
860
+ size: n,
861
+ position: posLabel,
862
+ schema: s.name,
863
+ ...res
864
+ });
865
+ }
866
+ }
867
+ }
868
+
869
+ // ---------------------------------------------------------------
870
+ // 2C. Key Membership Filter (`has_key?`) — Hit vs Miss
871
+ // ---------------------------------------------------------------
872
+ console.log(
873
+ "\n### 2C. Object Membership (`has_key?`) — Hit (last) vs Miss"
874
+ );
875
+ for (const n of SIZES) {
876
+ const obj = makeVariedKeyObject(n);
877
+ const keys = Object.keys(obj).sort();
878
+ const cases = [
879
+ ["hit", keys[n - 1], true],
880
+ ["miss", "zzzz_nonexistent_key", false]
881
+ ];
882
+ for (const [caseLabel, queryKey, expectedBool] of cases) {
883
+ console.log(
884
+ `\n--- Varied-Key Object N = ${n}, ` +
885
+ `case = ${caseLabel} ('${queryKey}') ---`
886
+ );
887
+ printRelationTableHeader();
888
+ let baseRes = null;
889
+ for (const s of SCHEMAS) {
890
+ const compiled = compiledBySchema
891
+ .get(s.name)
892
+ .get("bench_has_key");
893
+ const kInput = s.codec.parse(JSON.stringify([queryKey, obj]));
894
+ const res = await executeRelationBench(
895
+ compiled,
896
+ kInput,
897
+ (val, backend) => {
898
+ assert.strictEqual(
899
+ JSON.parse(s.codec.print(val)),
900
+ expectedBool,
901
+ `${s.name} (${backend}) has_key? mismatch`
902
+ );
903
+ },
904
+ CALLS
905
+ );
906
+ if (!baseRes) baseRes = res;
907
+ printRelationRow(s.name, res, baseRes);
908
+ results.push({
909
+ section: "has_key",
910
+ size: n,
911
+ position: caseLabel,
912
+ schema: s.name,
913
+ ...res
914
+ });
915
+ }
916
+ }
917
+ }
918
+
919
+ // ---------------------------------------------------------------
920
+ // 2D. Array Element Lookup (`nth`) — First, Middle, Last Index
921
+ // ---------------------------------------------------------------
922
+ console.log(
923
+ "\n### 2D. Array Element Lookup (`nth`) — First, Mid, Last Index"
924
+ );
925
+ for (const n of SIZES) {
926
+ const arr = makeNumberArray(n);
927
+ const positions = [
928
+ ["first", 0],
929
+ ["mid", Math.floor(n / 2)],
930
+ ["last", n - 1]
931
+ ];
932
+ for (const [posLabel, targetIdx] of positions) {
933
+ const expectedVal = arr[targetIdx];
934
+ const relName = `nth_${posLabel}_${n}`;
935
+ console.log(
936
+ `\n--- Array N = ${n}, index = ${posLabel} (${targetIdx}) ---`
937
+ );
938
+ printRelationTableHeader();
939
+ let baseRes = null;
940
+ for (const s of SCHEMAS) {
941
+ const compiled = compiledBySchema.get(s.name).get(relName);
942
+ const kInput = s.codec.parse(JSON.stringify(arr));
943
+ const res = await executeRelationBench(
944
+ compiled,
945
+ kInput,
946
+ (val, backend) => {
947
+ assert.strictEqual(
948
+ JSON.parse(s.codec.print(val)),
949
+ expectedVal,
950
+ `${s.name} (${backend}) nth mismatch`
951
+ );
952
+ },
953
+ CALLS
954
+ );
955
+ if (!baseRes) baseRes = res;
956
+ printRelationRow(s.name, res, baseRes);
957
+ results.push({
958
+ section: "nth",
959
+ size: n,
960
+ position: posLabel,
961
+ schema: s.name,
962
+ ...res
963
+ });
964
+ }
965
+ }
966
+ }
967
+
968
+ // ---------------------------------------------------------------
969
+ // 2E. Subtree Cardinality Queries (`obj_size` and `list_size`)
970
+ // ---------------------------------------------------------------
971
+ console.log(
972
+ "\n### 2E. Cardinality Queries (`obj_size` & `list_size`: " +
973
+ "O(N) vs O(1) in djson-ptrie)"
974
+ );
975
+ for (const n of SIZES) {
976
+ const obj = makeVariedKeyObject(n);
977
+ console.log(`\n--- Object Size Query (obj_size) for N = ${n} ---`);
978
+ printRelationTableHeader();
979
+ let baseObjRes = null;
980
+ for (const s of SCHEMAS) {
981
+ const compiled = compiledBySchema
982
+ .get(s.name)
983
+ .get("bench_obj_size");
984
+ const kInput = s.codec.parse(JSON.stringify(obj));
985
+ const res = await executeRelationBench(
986
+ compiled,
987
+ kInput,
988
+ (val, backend) => {
989
+ assert.strictEqual(
990
+ unfoldNatValue(val),
991
+ n,
992
+ `${s.name} (${backend}) obj_size mismatch`
993
+ );
994
+ },
995
+ CALLS
996
+ );
997
+ if (!baseObjRes) baseObjRes = res;
998
+ printRelationRow(s.name, res, baseObjRes);
999
+ results.push({
1000
+ section: "obj_size",
1001
+ size: n,
1002
+ schema: s.name,
1003
+ ...res
1004
+ });
1005
+ }
1006
+
1007
+ const arr = makeNumberArray(n);
1008
+ console.log(`\n--- Array Size Query (list_size) for N = ${n} ---`);
1009
+ printRelationTableHeader();
1010
+ let baseListRes = null;
1011
+ for (const s of SCHEMAS) {
1012
+ const compiled = compiledBySchema
1013
+ .get(s.name)
1014
+ .get("bench_list_size");
1015
+ const kInput = s.codec.parse(JSON.stringify(arr));
1016
+ const res = await executeRelationBench(
1017
+ compiled,
1018
+ kInput,
1019
+ (val, backend) => {
1020
+ assert.strictEqual(
1021
+ unfoldNatValue(val),
1022
+ n,
1023
+ `${s.name} (${backend}) list_size mismatch`
1024
+ );
1025
+ },
1026
+ CALLS
1027
+ );
1028
+ if (!baseListRes) baseListRes = res;
1029
+ printRelationRow(s.name, res, baseListRes);
1030
+ results.push({
1031
+ section: "list_size",
1032
+ size: n,
1033
+ schema: s.name,
1034
+ ...res
1035
+ });
1036
+ }
1037
+ }
1038
+
1039
+ // ---------------------------------------------------------------
1040
+ // 2F. Object Key Removal (`remove`) and Insertion (`put`)
1041
+ // ---------------------------------------------------------------
1042
+ console.log("\n### 2F. Object Update Operations (`remove` & `put`)");
1043
+ for (const n of SIZES) {
1044
+ const obj = makeVariedKeyObject(n);
1045
+ const keys = Object.keys(obj).sort();
1046
+ const targetKey = keys[n - 1];
1047
+ const expectedAfterRem = { ...obj };
1048
+ delete expectedAfterRem[targetKey];
1049
+
1050
+ console.log(
1051
+ `\n--- Object Key Removal (remove '${targetKey}') ` +
1052
+ `for N = ${n} ---`
1053
+ );
1054
+ printRelationTableHeader();
1055
+ let baseRemRes = null;
1056
+ for (const s of SCHEMAS) {
1057
+ const compiled = compiledBySchema
1058
+ .get(s.name)
1059
+ .get("bench_remove");
1060
+ const kInput = s.codec.parse(JSON.stringify([targetKey, obj]));
1061
+ const res = await executeRelationBench(
1062
+ compiled,
1063
+ kInput,
1064
+ (val, backend) => {
1065
+ assert.deepStrictEqual(
1066
+ JSON.parse(s.codec.print(val)),
1067
+ expectedAfterRem,
1068
+ `${s.name} (${backend}) remove mismatch`
1069
+ );
1070
+ },
1071
+ CALLS
1072
+ );
1073
+ if (!baseRemRes) baseRemRes = res;
1074
+ printRelationRow(s.name, res, baseRemRes);
1075
+ results.push({
1076
+ section: "remove",
1077
+ size: n,
1078
+ schema: s.name,
1079
+ ...res
1080
+ });
1081
+ }
1082
+
1083
+ const newKey = " middle_inserted_key_attr";
1084
+ const newVal = 777;
1085
+ const expectedAfterPut = { ...obj, [newKey]: newVal };
1086
+ console.log(
1087
+ `\n--- Object Key Insertion (put '${newKey}') for N = ${n} ---`
1088
+ );
1089
+ printRelationTableHeader();
1090
+ let basePutRes = null;
1091
+ for (const s of SCHEMAS) {
1092
+ const compiled = compiledBySchema
1093
+ .get(s.name)
1094
+ .get("bench_put");
1095
+ const kInput = s.codec.parse(
1096
+ JSON.stringify([newKey, newVal, obj])
1097
+ );
1098
+ const res = await executeRelationBench(
1099
+ compiled,
1100
+ kInput,
1101
+ (val, backend) => {
1102
+ assert.deepStrictEqual(
1103
+ JSON.parse(s.codec.print(val)),
1104
+ expectedAfterPut,
1105
+ `${s.name} (${backend}) put mismatch`
1106
+ );
1107
+ },
1108
+ CALLS
1109
+ );
1110
+ if (!basePutRes) basePutRes = res;
1111
+ printRelationRow(s.name, res, basePutRes);
1112
+ results.push({
1113
+ section: "put",
1114
+ size: n,
1115
+ schema: s.name,
1116
+ ...res
1117
+ });
1118
+ }
1119
+ }
1120
+ } finally {
1121
+ fs.rmSync(cacheDir, { recursive: true, force: true });
1122
+ }
1123
+
1124
+ return results;
1125
+ }
1126
+
1127
+ // -------------------------------------------------------------------
1128
+ // Main Runner
1129
+ // -------------------------------------------------------------------
1130
+
1131
+ const summaryReport = {};
1132
+ if (runCodecsPart) {
1133
+ summaryReport.codecs = runPart1CodecBenchmarks();
1134
+ }
1135
+ if (runSchemasPart) {
1136
+ summaryReport.schemas = await runPart2SchemaBenchmarks();
1137
+ }
1138
+
1139
+ if (emitJson) {
1140
+ console.log("\n=== JSON REPORT ===");
1141
+ console.log(JSON.stringify(summaryReport, null, 2));
1142
+ }