@fraczak/k 6.10.6 → 7.0.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 (83) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/CLAUDE.md +1 -1
  3. package/DOCS/BIG_PICTURE.md +156 -172
  4. package/DOCS/BINARY_CODEC_STATUS_AND_TODO.md +3 -3
  5. package/DOCS/CODEC_ARCHITECTURE.md +2 -2
  6. package/DOCS/DICTIONARY.md +243 -250
  7. package/DOCS/DIVERGENCE_DETECTION.md +0 -1
  8. package/DOCS/FILE_FORMATS.md +5 -5
  9. package/DOCS/KIR_SPECIALIZATION_DESIGN.md +1 -1
  10. package/DOCS/OBJECT_FILE_AND_PATTERN.md +12 -12
  11. package/DOCS/PATTERNS.md +17 -17
  12. package/DOCS/PATTERNS_AND_TYPED_VALUES.md +1 -1
  13. package/DOCS/REPL.md +9 -19
  14. package/DOCS/TYPE_DERIVATION.md +2 -8
  15. package/DOCS/WEB_REPL_REQUIREMENTS.md +8 -8
  16. package/DOCS/book/01-introduction.md +8 -11
  17. package/DOCS/book/02-syntax-and-values.md +19 -20
  18. package/DOCS/book/03-types-as-finite-automata.md +42 -43
  19. package/DOCS/book/04-partial-functions-and-composition.md +2 -3
  20. package/DOCS/book/05-typing-filters-and-normalization.md +44 -46
  21. package/DOCS/book/06-values-in-memory.md +10 -10
  22. package/DOCS/book/08-operational-semantics-and-execution.md +21 -26
  23. package/DOCS/book/09-compiler-architecture.md +11 -11
  24. package/DOCS/book/10-from-ast-to-intermediate-representation.md +6 -7
  25. package/DOCS/book/14-canonical-serialization.md +2 -2
  26. package/DOCS/book/appendix-b-incorporating-external-predefined-types-and-functions.md +17 -17
  27. package/DOCS/book.md +5 -5
  28. package/Examples/arithmetics.k +49 -153
  29. package/Examples/bnat.k +13 -13
  30. package/Examples/byte.k +2 -50
  31. package/Examples/ieee.k +408 -423
  32. package/Examples/karatsuba-mult.k +30 -34
  33. package/Examples/nat.k +5 -6
  34. package/README.md +54 -49
  35. package/TODO.md +13 -18
  36. package/backend-api.mjs +9 -1
  37. package/backends/llvm/scripts/run-tests.mjs +5 -0
  38. package/backends/llvm/src/kvm2llvm.mjs +2 -1
  39. package/backends/llvm/src/llvm.mjs +18 -25
  40. package/backends/llvm/tests/integration.sh +2 -2
  41. package/backends/llvm/tests/test-llvm.mjs +3 -3
  42. package/backends/llvm/tests/test-open-product-misalignment.mjs +140 -0
  43. package/backends/wasm/demo_ieee.mjs +2 -3
  44. package/backends/wasm/runtime.wat +157 -5
  45. package/backends/wasm/src/kvm2wasm.mjs +57 -34
  46. package/backends/wasm/src/wasm.mjs +38 -17
  47. package/backends/wasm/tests/perf-support.mjs +4 -4
  48. package/backends/wasm/tests/test-wasm.mjs +13 -14
  49. package/browser/vfs-data.mjs +12 -12
  50. package/codecs/BINARY_FORMAT.md +4 -4
  51. package/codecs/POLYMORPHIC_BINARY_FORMAT.md +4 -4
  52. package/codecs/README.md +6 -6
  53. package/codecs/ieee.mjs +15 -11
  54. package/codecs/int.mjs +9 -5
  55. package/codecs/string-codec.mjs +46 -47
  56. package/core.k +123 -95
  57. package/kir.mjs +213 -17
  58. package/object.mjs +0 -7
  59. package/package.json +5 -2
  60. package/parser.jison +2 -41
  61. package/parser.mjs +55 -95
  62. package/pretty.mjs +11 -4
  63. package/repl.mjs +8 -63
  64. package/scripts/build-repl-html.mjs +8 -38
  65. package/tests/code-derivation/blowup.mjs +2 -4
  66. package/tests/code-derivation/bnat.mjs +2 -3
  67. package/tests/code-derivation/dot.mjs +5 -11
  68. package/tests/code-derivation/nat.mjs +2 -2
  69. package/tests/code-derivation/not-not.mjs +6 -6
  70. package/tests/code-derivation/pattern-envelope.mjs +4 -6
  71. package/tests/code-derivation/product.mjs +3 -6
  72. package/tests/code-derivation/rlz.mjs +4 -5
  73. package/tests/test-codecs.mjs +1 -1
  74. package/tests/test-fingerprint.mjs +36 -41
  75. package/tests/test-hash-normalization.mjs +4 -6
  76. package/tests/test-ieee-arithmetic.mjs +6 -7
  77. package/tests/test-k-object.mjs +11 -15
  78. package/tests/test-kir.mjs +0 -1
  79. package/tests/test-kvm-polymorphic.mjs +1 -2
  80. package/tests/test-kvm.mjs +8 -11
  81. package/tests/test-repl-html.mjs +20 -2
  82. package/tests/test-repl.mjs +48 -53
  83. package/tests/test.mjs +2 -3
@@ -0,0 +1,140 @@
1
+ import assert from "node:assert/strict";
2
+ import { compileObjectBuffer, decodeObject } from "@fraczak/k/object.mjs";
3
+ import { compileObjectToLLVM } from "../src/llvm.mjs";
4
+ import { compileAndRunLLVM } from "../src/executable.mjs";
5
+ import { Value } from "@fraczak/k/Value.mjs";
6
+
7
+ console.log("==> LLVM Open Product Field Alignment Test");
8
+
9
+ // Test expression from user:
10
+ // len_ = ?X <{.list/nil if, .len then} .then,
11
+ // {.list/cons.cdr list, .len inc len} ?X len_ >;
12
+ const src = `
13
+ 0 = {}|0;
14
+ inc = |1;
15
+ len = {()list, 0 len} len_;
16
+ len_ = ?X <{.list/nil if, .len then} .then, {.list/cons.cdr list, .len inc len} ?X len_ >;
17
+ len
18
+ `;
19
+
20
+ // Input schema for list:
21
+ // cons is a closed product with fields 'car' (index 0) and
22
+ // 'cdr' (index 1).
23
+ const listPattern = [
24
+ ["closed-union", [["cons", 1], ["nil", 2]]],
25
+ ["closed-product", [["car", 3], ["cdr", 0]]],
26
+ ["closed-product", []],
27
+ ["any", []]
28
+ ];
29
+
30
+ const obj = decodeObject(
31
+ compileObjectBuffer(src, { source: "open-product-misalignment.k" })
32
+ );
33
+ const { llvm } = compileObjectToLLVM(obj, { inputPattern: listPattern });
34
+
35
+ // -----------------------------------------------------------------------
36
+ // Test 1: Static IR Field Index for .cdr
37
+ // -----------------------------------------------------------------------
38
+ // In cons payload { car, cdr }, 'car' is index 0 (byte offset 0) and
39
+ // 'cdr' is index 1 (byte offset 24 = 1 * K_FIELD_SIZE).
40
+ // The specialized LLVM IR for projecting .cdr MUST access byte offset 24.
41
+ // When misaligned, it accesses byte offset 0 (which is 'car').
42
+
43
+ const cdrLabelMatch = llvm.match(
44
+ /(@k_label_\d+)\s*=\s*private unnamed_addr constant \[4 x i8\] c"cdr\\00"/
45
+ );
46
+ assert.ok(cdrLabelMatch, "LLVM IR should define label constant for 'cdr'");
47
+ const cdrLabelGlobal = cdrLabelMatch[1];
48
+
49
+ const lines = llvm.split("\n");
50
+ let cdrFieldSlotOffset = null;
51
+ let cdrDynamicFallbackEmitted = false;
52
+
53
+ for (let i = 0; i < lines.length; i++) {
54
+ if (
55
+ lines[i].includes(cdrLabelGlobal) &&
56
+ lines[i].includes("getelementptr inbounds [4 x i8]")
57
+ ) {
58
+ const localLabelVar = lines[i].trim().split("=")[0].trim();
59
+ for (let j = i; j < Math.min(lines.length, i + 35); j++) {
60
+ if (
61
+ lines[j].includes("getelementptr inbounds i8, ptr %product_fields_ptr") &&
62
+ lines[j].includes(", i64 ")
63
+ ) {
64
+ const offsetMatch = lines[j].match(/, i64 (\d+)/);
65
+ if (offsetMatch) cdrFieldSlotOffset = Number(offsetMatch[1]);
66
+ }
67
+ if (
68
+ lines[j].includes("call ptr @k_product_get_n") &&
69
+ lines[j].includes(localLabelVar)
70
+ ) {
71
+ cdrDynamicFallbackEmitted = true;
72
+ }
73
+ }
74
+ if (cdrFieldSlotOffset !== null) break;
75
+ }
76
+ }
77
+
78
+ console.log(`- Detected .cdr field slot byte offset: ${cdrFieldSlotOffset} (expected 24)`);
79
+ console.log(`- Detected dynamic fallback (@k_product_get_n) emitted for .cdr: ${cdrDynamicFallbackEmitted}`);
80
+
81
+ // -----------------------------------------------------------------------
82
+ // Test 2: Execution without dynamic fallback
83
+ // -----------------------------------------------------------------------
84
+ // When dynamic fallback (@k_product_get_n) is removed from the emitted
85
+ // LLVM IR (forcing runtime to rely on the statically generated index),
86
+ // the misalignment causes .cdr projection to load 'car' (a leaf),
87
+ // causing the next iteration of len_ to fail because 'car' is not a list.
88
+ const llvmNoFallback = llvm.replace(
89
+ /call ptr @k_product_get_n\([^)]+\)/g,
90
+ "bitcast ptr null to ptr"
91
+ );
92
+
93
+ // Build test list: [1, 2] = cons(car: 1, cdr: cons(car: 2, cdr: nil))
94
+ const nil = Value.variant("nil", Value.product({}));
95
+ const cons2 = Value.variant("cons", Value.product({
96
+ car: Value.variant("1", Value.product({})),
97
+ cdr: nil
98
+ }));
99
+ const cons1 = Value.variant("cons", Value.product({
100
+ car: Value.variant("1", Value.product({})),
101
+ cdr: cons2
102
+ }));
103
+
104
+ let runtimeStatusNoFallback = null;
105
+ try {
106
+ const res = compileAndRunLLVM(llvmNoFallback, { input: cons1 });
107
+ runtimeStatusNoFallback = res.status;
108
+ } catch (err) {
109
+ runtimeStatusNoFallback = err.status ?? 2;
110
+ }
111
+
112
+ console.log(`- Runtime exit status without fallback: ${runtimeStatusNoFallback} (expected 0)`);
113
+
114
+ // -----------------------------------------------------------------------
115
+ // Assertions to expose misalignment
116
+ // -----------------------------------------------------------------------
117
+ const exposeMode = process.argv.includes("--assert");
118
+
119
+ if (exposeMode) {
120
+ assert.equal(
121
+ cdrFieldSlotOffset,
122
+ 24,
123
+ `Misalignment: .cdr projected at byte offset ${cdrFieldSlotOffset} (car) instead of 24 (cdr)`
124
+ );
125
+ assert.equal(
126
+ cdrDynamicFallbackEmitted,
127
+ false,
128
+ "Dynamic fallback: LLVM emits @k_product_get_n fallback to mask misalignment"
129
+ );
130
+ assert.equal(
131
+ runtimeStatusNoFallback,
132
+ 0,
133
+ `Runtime failure: status ${runtimeStatusNoFallback} when relying on static index`
134
+ );
135
+ } else {
136
+ console.log("\nSummary:");
137
+ console.log(` Offset misalignment: ${cdrFieldSlotOffset === 24 ? "ALIGNED" : "MISALIGNED (offset 0 instead of 24)"}`);
138
+ console.log(` Dynamic fallback used: ${cdrDynamicFallbackEmitted ? "YES (papers over bug at runtime)" : "NO"}`);
139
+ console.log(` Status without fallback: ${runtimeStatusNoFallback === 0 ? "OK" : "FAILS (status " + runtimeStatusNoFallback + ")"}`);
140
+ }
@@ -184,8 +184,7 @@ function readArenaValue(exports, ptr, pattern, patternNodeId) {
184
184
 
185
185
  for (let i = 0; i < N; i++) {
186
186
  const edge = patternNode.edges[i];
187
- const offsetVal = view.getUint32(ptr + 8 + 4 * i, true);
188
- const childPtr = view.getUint32(ptr + offsetVal, true);
187
+ const childPtr = view.getUint32(ptr + 8 + 4 * N + 4 * i, true);
189
188
  productObj[edge.label] = readArenaValue(exports, childPtr, pattern, edge.target);
190
189
  }
191
190
  return Value.product(productObj, pattern);
@@ -231,7 +230,7 @@ function writeValueToArena(value, pattern, patternNodeId) {
231
230
 
232
231
  for (let i = 0; i < N; i++) {
233
232
  const offsetVal = 8 + 4 * N + 4 * i;
234
- view.setUint32(ptr + 8 + 4 * i, offsetVal, true);
233
+ view.setUint32(ptr + 8 + 4 * i, getTagId(keys[i]), true);
235
234
  view.setUint32(ptr + offsetVal, childPtrs[i], true);
236
235
  }
237
236
  return ptr;
@@ -398,6 +398,14 @@
398
398
  i32.ge_u
399
399
  br_if $break_prod
400
400
 
401
+ ;; Copy field tagId at offset 8 + 4*i
402
+ local.get $dest
403
+ i32.const 8
404
+ local.get $i
405
+ i32.const 4
406
+ i32.mul
407
+ i32.add
408
+ i32.add
401
409
  local.get $curr
402
410
  i32.const 8
403
411
  local.get $i
@@ -406,17 +414,19 @@
406
414
  i32.add
407
415
  i32.add
408
416
  i32.load
409
- local.set $offsetVal
417
+ i32.store
410
418
 
411
- local.get $dest
419
+ ;; Value slot offset is 8 + 4*N + 4*i
412
420
  i32.const 8
413
- local.get $i
421
+ local.get $N
414
422
  i32.const 4
415
423
  i32.mul
416
424
  i32.add
425
+ local.get $i
426
+ i32.const 4
427
+ i32.mul
417
428
  i32.add
418
- local.get $offsetVal
419
- i32.store
429
+ local.set $offsetVal
420
430
 
421
431
  local.get $curr
422
432
  local.get $offsetVal
@@ -472,4 +482,146 @@
472
482
  i32.add
473
483
  global.set $arena_free
474
484
  )
485
+
486
+ ;; Lookup product field by tagId with predictedIndex hint
487
+ (func $product_get (export "product_get") (param $prod i32) (param $targetTagId i32) (param $predictedIndex i32) (result i32)
488
+ (local $size i32)
489
+ (local $N i32)
490
+ (local $i i32)
491
+
492
+ ;; 1. Null check
493
+ local.get $prod
494
+ i32.eqz
495
+ if
496
+ i32.const 0
497
+ return
498
+ end
499
+
500
+ ;; 2. Read totalSize at prod + 0
501
+ local.get $prod
502
+ i32.load offset=0
503
+ local.set $size
504
+
505
+ ;; 3. Validate product header (not a variant and size >= 8)
506
+ local.get $size
507
+ i32.const 12
508
+ i32.eq
509
+ if
510
+ i32.const 0
511
+ return
512
+ end
513
+ local.get $size
514
+ i32.const 8
515
+ i32.lt_u
516
+ if
517
+ i32.const 0
518
+ return
519
+ end
520
+
521
+ ;; 4. Read field count N at prod + 4
522
+ local.get $prod
523
+ i32.load offset=4
524
+ local.set $N
525
+
526
+ ;; 5. Validate size == 8 + 8 * N
527
+ local.get $size
528
+ i32.const 8
529
+ local.get $N
530
+ i32.const 8
531
+ i32.mul
532
+ i32.add
533
+ i32.ne
534
+ if
535
+ i32.const 0
536
+ return
537
+ end
538
+
539
+ ;; 6. Fast path: check predictedIndex if in range [0, N)
540
+ local.get $predictedIndex
541
+ i32.const 0
542
+ i32.ge_s
543
+ if
544
+ local.get $predictedIndex
545
+ local.get $N
546
+ i32.lt_u
547
+ if
548
+ ;; Load tagId at prod + 8 + 4 * predictedIndex
549
+ local.get $prod
550
+ i32.const 8
551
+ local.get $predictedIndex
552
+ i32.const 4
553
+ i32.mul
554
+ i32.add
555
+ i32.add
556
+ i32.load
557
+ local.get $targetTagId
558
+ i32.eq
559
+ if
560
+ ;; Match! Return value at prod + 8 + 4*N + 4*predictedIndex
561
+ local.get $prod
562
+ i32.const 8
563
+ local.get $N
564
+ i32.const 4
565
+ i32.mul
566
+ i32.add
567
+ local.get $predictedIndex
568
+ i32.const 4
569
+ i32.mul
570
+ i32.add
571
+ i32.add
572
+ i32.load
573
+ return
574
+ end
575
+ end
576
+ end
577
+
578
+ ;; 7. Scan path: check i = 0 .. N-1
579
+ i32.const 0
580
+ local.set $i
581
+ (block $break_scan
582
+ (loop $loop_scan
583
+ local.get $i
584
+ local.get $N
585
+ i32.ge_u
586
+ br_if $break_scan
587
+
588
+ ;; Load tagId at prod + 8 + 4 * i
589
+ local.get $prod
590
+ i32.const 8
591
+ local.get $i
592
+ i32.const 4
593
+ i32.mul
594
+ i32.add
595
+ i32.add
596
+ i32.load
597
+ local.get $targetTagId
598
+ i32.eq
599
+ if
600
+ ;; Match! Return value at prod + 8 + 4*N + 4*i
601
+ local.get $prod
602
+ i32.const 8
603
+ local.get $N
604
+ i32.const 4
605
+ i32.mul
606
+ i32.add
607
+ local.get $i
608
+ i32.const 4
609
+ i32.mul
610
+ i32.add
611
+ i32.add
612
+ i32.load
613
+ return
614
+ end
615
+
616
+ local.get $i
617
+ i32.const 1
618
+ i32.add
619
+ local.set $i
620
+ br $loop_scan
621
+ )
622
+ )
623
+
624
+ ;; 8. Not found: return 0
625
+ i32.const 0
626
+ )
475
627
  )
@@ -108,22 +108,28 @@ export function lowerToWasm(relDef, name, options = {}) {
108
108
  const getPattern = (rName, inputMap = {}) => registerPatterns.get(mappedReg(rName, inputMap));
109
109
 
110
110
  const getProductEdges = (inst, inputMap) => {
111
- if (typePatternGraph) {
112
- const inputPatternId = inst.exp.patterns[0];
113
- const inputPatternNodeId = typePatternGraph.find(inputPatternId);
114
- const inputPropertyList = patternToPropertyList(exportPatternGraph(typePatternGraph, inputPatternNodeId));
115
- return inputPropertyList[0][1];
116
- }
117
-
118
111
  const inputPropertyList = getPattern(inst.src, inputMap);
119
- const inputRoot = inputPropertyList?.[0];
120
- if (!Array.isArray(inputRoot) || !Array.isArray(inputRoot[1])) {
121
- throw new Error(`Wasm compiler: cannot infer input pattern for field '${inst.label}'`);
112
+ if (Array.isArray(inputPropertyList) && inputPropertyList.length > 0) {
113
+ const inputRoot = inputPropertyList[0];
114
+ if ((inputRoot[0] === "open-product" || inputRoot[0] === "closed-product") && Array.isArray(inputRoot[1])) {
115
+ return inputRoot[1];
116
+ }
122
117
  }
123
- if (inputRoot[0] !== "open-product" && inputRoot[0] !== "closed-product") {
124
- throw new Error(`Wasm compiler: field '${inst.label}' requires a product input pattern`);
118
+
119
+ if (typePatternGraph && inst.exp?.patterns?.[0] != null) {
120
+ try {
121
+ const inputPatternId = inst.exp.patterns[0];
122
+ const inputPatternNodeId = typePatternGraph.find(inputPatternId);
123
+ const inputPropertyList = patternToPropertyList(exportPatternGraph(typePatternGraph, inputPatternNodeId));
124
+ if (Array.isArray(inputPropertyList?.[0]?.[1])) {
125
+ return inputPropertyList[0][1];
126
+ }
127
+ } catch {
128
+ // Ignore fallback errors
129
+ }
125
130
  }
126
- return inputRoot[1];
131
+
132
+ return null;
127
133
  };
128
134
 
129
135
  const registers = new Set();
@@ -205,14 +211,12 @@ export function lowerToWasm(relDef, name, options = {}) {
205
211
  const lines = [];
206
212
  for (const field of inputProductFields) {
207
213
  const fieldIndex = inputProductEdges.findIndex(([label]) => label === field.label);
214
+ const tagId = getTagId(field.label);
208
215
  lines.push(` ;; cache input field ${field.label} in $${field.local}`);
209
216
  lines.push(` local.get $in`);
210
- lines.push(` local.get $in`);
211
- lines.push(` i32.const ${8 + 4 * fieldIndex}`);
212
- lines.push(` i32.add`);
213
- lines.push(` i32.load`);
214
- lines.push(` i32.add`);
215
- lines.push(` i32.load`);
217
+ lines.push(` i32.const ${tagId}`);
218
+ lines.push(` i32.const ${fieldIndex}`);
219
+ lines.push(` call $product_get`);
216
220
  lines.push(` local.set $${field.local}`);
217
221
  }
218
222
  return lines.join("\n");
@@ -231,9 +235,10 @@ export function lowerToWasm(relDef, name, options = {}) {
231
235
  lines.push(` i32.const ${inputProductFields.length}`);
232
236
  lines.push(` i32.store offset=4`);
233
237
  for (const field of inputProductFields) {
238
+ const tagId = getTagId(field.label);
234
239
  const offsetVal = 8 + 4 * inputProductFields.length + 4 * field.index;
235
240
  lines.push(` local.get $${dest}`);
236
- lines.push(` i32.const ${offsetVal}`);
241
+ lines.push(` i32.const ${tagId}`);
237
242
  lines.push(` i32.store offset=${8 + 4 * field.index}`);
238
243
  lines.push(` local.get $${dest}`);
239
244
  lines.push(` local.get $${field.local}`);
@@ -314,14 +319,24 @@ export function lowerToWasm(relDef, name, options = {}) {
314
319
  break;
315
320
  }
316
321
 
317
- const edges = getProductEdges(inst, inputMap);
318
- const fieldIndex = edges.findIndex(([label]) => label === inst.label);
319
- if (fieldIndex === -1) {
320
- throw new Error(`Wasm compiler: field '${inst.label}' not found in input pattern`);
322
+ let predictedIndex = -1;
323
+ try {
324
+ const edges = getProductEdges(inst, inputMap);
325
+ if (Array.isArray(edges)) {
326
+ predictedIndex = edges.findIndex(([label]) => label === inst.label);
327
+ }
328
+ } catch {
329
+ predictedIndex = -1;
321
330
  }
331
+ const tagId = getTagId(inst.label);
322
332
 
323
- lines.push(` ;; project_field ${inst.label} (index ${fieldIndex}) from $${src} to $${dest}`);
333
+ lines.push(` ;; project_field ${inst.label} (tagId ${tagId}, hint ${predictedIndex}) from $${src} to $${dest}`);
324
334
  lines.push(` local.get $${src}`);
335
+ lines.push(` i32.const ${tagId}`);
336
+ lines.push(` i32.const ${predictedIndex}`);
337
+ lines.push(` call $product_get`);
338
+ lines.push(` local.set $${dest}`);
339
+ lines.push(` local.get $${dest}`);
325
340
  lines.push(` i32.eqz`);
326
341
  lines.push(` if`);
327
342
  if (failTarget) {
@@ -332,14 +347,6 @@ export function lowerToWasm(relDef, name, options = {}) {
332
347
  lines.push(` return`);
333
348
  }
334
349
  lines.push(` end`);
335
- lines.push(` local.get $${src}`);
336
- lines.push(` local.get $${src}`);
337
- lines.push(` i32.const ${8 + 4 * fieldIndex}`);
338
- lines.push(` i32.add`);
339
- lines.push(` i32.load`);
340
- lines.push(` i32.add`);
341
- lines.push(` i32.load`);
342
- lines.push(` local.set $${dest}`);
343
350
  setPattern(dest, inst.pattern);
344
351
  break;
345
352
  }
@@ -443,11 +450,13 @@ export function lowerToWasm(relDef, name, options = {}) {
443
450
  lines.push(` i32.store offset=4`);
444
451
 
445
452
  for (let i = 0; i < N; i++) {
453
+ const branch = sortedBranches[i];
454
+ const tagId = getTagId(branch.label);
446
455
  const offsetVal = 8 + 4 * N + 4 * i;
447
456
  const fieldTmp = `${dest}_f${i}`;
448
457
 
449
458
  lines.push(` local.get $${dest}`);
450
- lines.push(` i32.const ${offsetVal}`);
459
+ lines.push(` i32.const ${tagId}`);
451
460
  lines.push(` i32.store offset=${8 + 4 * i}`);
452
461
 
453
462
  lines.push(` local.get $${dest}`);
@@ -501,6 +510,20 @@ export function lowerToWasm(relDef, name, options = {}) {
501
510
  }
502
511
  lines.push(` end`);
503
512
 
513
+ lines.push(` local.get $${src}`);
514
+ lines.push(` i32.load offset=0`);
515
+ lines.push(` i32.const 12`);
516
+ lines.push(` i32.ne`);
517
+ lines.push(` if`);
518
+ if (failTarget) {
519
+ lines.push(` br ${failTarget}`);
520
+ } else {
521
+ lines.push(` i32.const 0`);
522
+ lines.push(` i32.const 0`);
523
+ lines.push(` return`);
524
+ }
525
+ lines.push(` end`);
526
+
504
527
  lines.push(` local.get $${src}`);
505
528
  lines.push(` i32.load offset=4`);
506
529
  lines.push(` i32.const ${tagId}`);
@@ -13,7 +13,8 @@ import {
13
13
  patternToPropertyList,
14
14
  propertyListToPattern,
15
15
  Value,
16
- specializeKVM
16
+ specializeKVM,
17
+ specializeObjectRelations
17
18
  } from "@fraczak/k/backend-api.mjs";
18
19
  import { intersectPropertyListPatterns } from "@fraczak/k/codecs/runtime/codec.mjs";
19
20
  import { propertyListToFilter } from "@fraczak/k/codecs/runtime/show-value.mjs";
@@ -304,7 +305,8 @@ function compileModule(mainRelName, defs) {
304
305
 
305
306
  kvmFunc.name = nameMap.get(name) || cleanName(name);
306
307
  cleanCallNames(kvmFunc.body, nameMap);
307
- wats.push(lowerToWasm(kvmFunc, kvmFunc.name));
308
+ const wasmText = lowerToWasm(kvmFunc, kvmFunc.name);
309
+ wats.push(wasmText);
308
310
  }
309
311
 
310
312
  return {
@@ -611,8 +613,10 @@ async function compileWasmArtifactFromObject(object, { entry = null, inputEnvelo
611
613
  if (!mainRel) throw new Error(`No main relation (${mainRelName}) defined in object`);
612
614
  const inputPattern = getPatternPropertyList(mainRel.typePatternGraph, mainRel.def.patterns[0]);
613
615
  const outputPattern = getPatternPropertyList(mainRel.typePatternGraph, mainRel.def.patterns[1]);
614
- if (classifyTyping(inputPattern, outputPattern).program === "polymorphic") {
615
- defs = annotateObjectWithInputFilter(object, mainRelName, inputPattern, inputEnvelopePattern);
616
+ const typing = classifyTyping(inputPattern, outputPattern);
617
+ if (typing.program === "polymorphic") {
618
+ const specialized = specializeObjectRelations(object, mainRelName, inputEnvelopePattern);
619
+ defs = { rels: specialized.rels, relAlias: specialized.relAlias, compileStats: specialized.compileStats };
616
620
  typingMode = "specialized";
617
621
  }
618
622
  }
@@ -635,7 +639,9 @@ async function compileWasmArtifact(
635
639
  const inputPattern = getPatternPropertyList(mainRel.typePatternGraph, mainRel.def.patterns[0]);
636
640
  const outputPattern = getPatternPropertyList(mainRel.typePatternGraph, mainRel.def.patterns[1]);
637
641
  if (classifyTyping(inputPattern, outputPattern).program === "polymorphic") {
638
- defs = annotateSourceWithInputFilter(fullSource, inputPattern, inputEnvelopePattern, options);
642
+ const obj = { format: "k-object", rels: defs.rels, relAlias: defs.relAlias, compileStats: defs.compileStats, main: "__main__" };
643
+ const specialized = specializeObjectRelations(obj, "__main__", inputEnvelopePattern);
644
+ defs = { rels: specialized.rels, relAlias: specialized.relAlias, compileStats: specialized.compileStats };
639
645
  typingMode = "specialized";
640
646
  }
641
647
  }
@@ -723,20 +729,18 @@ function readArenaValue(exports, ptr, pattern, patternNodeId, patternPropertyLis
723
729
 
724
730
  if (patternNode.kind === NODE_KIND.OPEN_PRODUCT || patternNode.kind === NODE_KIND.CLOSED_PRODUCT) {
725
731
  const N = view.getUint32(frame.ptr + 4, true);
726
- if (N !== patternNode.edges.length) {
727
- throw new Error(`Cannot decode product pointer ${frame.ptr}: arena field count ${N} does not match output pattern`);
728
- }
729
732
  const product = {};
730
733
  frame.assign(Value.product(product, patternForNode(patternPropertyList, frame.patternNodeId)));
731
734
  for (let i = N - 1; i >= 0; i--) {
732
- const edge = patternNode.edges[i];
733
- const offset = view.getUint32(frame.ptr + 8 + 4 * i, true);
734
- const childPtr = view.getUint32(frame.ptr + offset, true);
735
+ const tagId = view.getUint32(frame.ptr + 8 + 4 * i, true);
736
+ const label = tags.getTag(tagId) || patternNode.edges[i]?.label;
737
+ const edge = patternNode.edges.find((candidate) => candidate.label === label);
738
+ const childPtr = view.getUint32(frame.ptr + 8 + 4 * N + 4 * i, true);
735
739
  stack.push({
736
740
  ptr: wasmPtr(childPtr),
737
- patternNodeId: edge.target,
741
+ patternNodeId: edge?.target ?? null,
738
742
  assign(value) {
739
- product[edge.label] = value;
743
+ product[label] = value;
740
744
  }
741
745
  });
742
746
  }
@@ -787,8 +791,9 @@ function writeValueToArena(exports, value, pattern, patternNodeId, arenaValues,
787
791
  view.setUint32(frame.ptr, 8 + 8 * N, true);
788
792
  view.setUint32(frame.ptr + 4, N, true);
789
793
  for (let i = 0; i < N; i++) {
794
+ const tagId = tags.getId(frame.fields[i].label);
790
795
  const offset = 8 + 4 * N + 4 * i;
791
- view.setUint32(frame.ptr + 8 + 4 * i, offset, true);
796
+ view.setUint32(frame.ptr + 8 + 4 * i, tagId, true);
792
797
  view.setUint32(frame.ptr + offset, frame.childPtrs[i], true);
793
798
  }
794
799
  arenaValues.set(frame.ptr, valueWithPatternNode(frame.value, patternPropertyList, frame.patternNodeId));
@@ -820,6 +825,13 @@ function writeValueToArena(exports, value, pattern, patternNodeId, arenaValues,
820
825
  ? Object.keys(frame.value.product)
821
826
  .sort()
822
827
  .map((label) => ({ label, patternNodeId: frame.patternNodeId }))
828
+ : isOpenProduct
829
+ ? Object.keys(frame.value.product)
830
+ .sort()
831
+ .map((label) => {
832
+ const edge = patternNode.edges.find((e) => e.label === label);
833
+ return { label, patternNodeId: edge ? edge.target : null };
834
+ })
823
835
  : patternNode.edges.map((edge) => ({ label: edge.label, patternNodeId: edge.target }));
824
836
  const fieldLabels = new Set(fields.map(({ label }) => label));
825
837
  if (isClosedProduct) {
@@ -829,9 +841,17 @@ function writeValueToArena(exports, value, pattern, patternNodeId, arenaValues,
829
841
  }
830
842
  }
831
843
  }
832
- for (const { label } of fields) {
833
- if (!Object.hasOwn(frame.value.product, label)) {
834
- throw new Error(`Product field '${label}' is required by input pattern node ${frame.patternNodeId}`);
844
+ if (isOpenProduct) {
845
+ for (const edge of patternNode.edges) {
846
+ if (!Object.hasOwn(frame.value.product, edge.label)) {
847
+ throw new Error(`Product field '${edge.label}' is required by input pattern node ${frame.patternNodeId}`);
848
+ }
849
+ }
850
+ } else {
851
+ for (const { label } of fields) {
852
+ if (!Object.hasOwn(frame.value.product, label)) {
853
+ throw new Error(`Product field '${label}' is required by input pattern node ${frame.patternNodeId}`);
854
+ }
835
855
  }
836
856
  }
837
857
 
@@ -841,6 +861,7 @@ function writeValueToArena(exports, value, pattern, patternNodeId, arenaValues,
841
861
  finishProduct: true,
842
862
  value: frame.value,
843
863
  patternNodeId: frame.patternNodeId,
864
+ fields,
844
865
  ptr,
845
866
  childPtrs,
846
867
  assign: frame.assign
@@ -200,8 +200,7 @@ export function readArenaValue(exports, rootPtr, pattern, rootPatternNodeId, pat
200
200
 
201
201
  for (let i = N - 1; i >= 0; i--) {
202
202
  const edge = patternNode.edges[i];
203
- const offsetVal = view.getUint32(ptr + 8 + 4 * i, true);
204
- const childPtr = view.getUint32(ptr + offsetVal, true);
203
+ const childPtr = view.getUint32(ptr + 8 + 4 * N + 4 * i, true);
205
204
  const label = edge.label;
206
205
  stack.push({
207
206
  ptr: childPtr,
@@ -253,14 +252,14 @@ export function writeValueToArena(exports, rootValue, pattern, rootPatternNodeId
253
252
  const frame = stack.pop();
254
253
 
255
254
  if (frame.finishProduct) {
256
- const { ptr, totalSize, N, childPtrs, assign } = frame;
255
+ const { ptr, totalSize, N, keys, childPtrs, assign } = frame;
257
256
  const view = new DataView(exports.memory.buffer);
258
257
  view.setUint32(ptr, totalSize, true);
259
258
  view.setUint32(ptr + 4, N, true);
260
259
 
261
260
  for (let i = 0; i < N; i++) {
262
261
  const offsetVal = 8 + 4 * N + 4 * i;
263
- view.setUint32(ptr + 8 + 4 * i, offsetVal, true);
262
+ view.setUint32(ptr + 8 + 4 * i, getTagId(keys[i]), true);
264
263
  view.setUint32(ptr + offsetVal, childPtrs[i], true);
265
264
  }
266
265
  assign(ptr);
@@ -294,6 +293,7 @@ export function writeValueToArena(exports, rootValue, pattern, rootPatternNodeId
294
293
  ptr,
295
294
  totalSize,
296
295
  N,
296
+ keys,
297
297
  childPtrs,
298
298
  assign
299
299
  });