@fraczak/k 7.0.0 → 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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
6
  This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [7.0.1] — 2026-10-10 — Call-Graph Pattern Graph Specialization
9
+
10
+ ### Bug Fixes & Backend Alignments
11
+
12
+ - **Pattern Graph Specialization Across Call Graphs**:
13
+ - Implemented `specializeObjectRelations` in `kir.mjs` to propagate input
14
+ schemas across relation call graphs via monomorphization.
15
+ - Statically resolves field projection offsets for open product and
16
+ polymorphic helper relations without dynamic fallback (`@k_product_get_n`).
17
+ - Fixed WASM predicted field index calculation and aligned product header
18
+ memory layout (`tagId` at header slots).
19
+ - Resolved polymorphic list and open product projection alignment across
20
+ LLVM, WASM, and Web REPL.
21
+
8
22
  ## [7.0.0] — 2026-10-10 — Unified Schemas, Relational Contracts, and Pure Syntax
9
23
 
10
24
  ### Breaking Language & Syntax Changes
package/backend-api.mjs CHANGED
@@ -4,7 +4,15 @@ export { decodeWire, encodeToWire } from "./codecs/runtime/prefix-codec.mjs";
4
4
  export { exportPatternGraph, NODE_KIND } from "./codecs/runtime/codec.mjs";
5
5
  export { patternToPropertyList, propertyListToPattern } from "./codecs/runtime/pattern-json.mjs";
6
6
  export { Value, fromObject, isProduct, isVariant, isValue } from "./Value.mjs";
7
- export { KIR_FORMAT, KIR_VERSION, objectToKIRP, retypeObjectRelation, retypeObjectRelationForBackend } from "./kir.mjs";
7
+ export {
8
+ KIR_FORMAT,
9
+ KIR_VERSION,
10
+ objectToKIRP,
11
+ relationPatternPropertyList,
12
+ retypeObjectRelation,
13
+ retypeObjectRelationForBackend,
14
+ specializeObjectRelations
15
+ } from "./kir.mjs";
8
16
  export { compileLibrary, decodeObject, loadLibrary } from "./object.mjs";
9
17
  export { createState, evaluateInput } from "./repl.mjs";
10
18
  export { parse as parseFloat64, print as printFloat64 } from "./codecs/ieee.mjs";
@@ -24,6 +24,11 @@ if (process.argv.includes("-h") || process.argv.includes("--help")) {
24
24
 
25
25
  const tests = [
26
26
  ["tests/test-llvm.mjs", node, ["tests/test-llvm.mjs"]],
27
+ [
28
+ "tests/test-open-product-misalignment.mjs",
29
+ node,
30
+ ["tests/test-open-product-misalignment.mjs", "--assert"]
31
+ ],
27
32
  ["tests/integration.sh", path.join(root, "tests/integration.sh"), []],
28
33
  ["scripts/conformance.mjs", node, ["scripts/conformance.mjs"]]
29
34
  ];
@@ -678,7 +678,7 @@ function lowerKVMFunction(kvmFunc, symbol, funcName, moduleCtx, linkage = "", op
678
678
  const label = ctx.labelRef(inst.label);
679
679
  let val;
680
680
  if (edgeIndex >= 0 && ctx.runtimeMode !== "compact") {
681
- val = lowerProductGetAt(ctx, input, edgeIndex, label);
681
+ val = lowerProductGetAt(ctx, input, edgeIndex, null);
682
682
  } else {
683
683
  val = ctx.tempName("field");
684
684
  ctx.lines.push(` ${val} = call ptr @k_product_get_n(ptr ${input}, ptr ${label.pointer}, i64 ${label.length})`);
@@ -831,6 +831,7 @@ function lowerKVMFunction(kvmFunc, symbol, funcName, moduleCtx, linkage = "", op
831
831
  }
832
832
 
833
833
  regValues.set(cleanReg(inst.dest), product);
834
+ if (inst.pattern) regPatterns.set(cleanReg(inst.dest), inst.pattern);
834
835
  break;
835
836
  }
836
837
  case "union": {
@@ -1011,8 +1011,7 @@ export function emitLLVMModule(input, options = {}) {
1011
1011
  export function compileObjectToLLVM(object, options = {}) {
1012
1012
  const inputPattern = options.inputPattern ? readPattern(options.inputPattern) : null;
1013
1013
  const relation = options.relation || object.main;
1014
-
1015
- let retypedKir = null;
1014
+ let targetObject = object;
1016
1015
  let entryInputPattern = inputPattern;
1017
1016
  let outputPattern = null;
1018
1017
 
@@ -1020,43 +1019,37 @@ export function compileObjectToLLVM(object, options = {}) {
1020
1019
  const retyped = retypeObjectRelationForBackend(object, relation, inputPattern, {
1021
1020
  source: options.source || "<k-llvm>"
1022
1021
  });
1023
- retypedKir = retyped.kir;
1022
+ targetObject = retyped.retypedObject;
1024
1023
  entryInputPattern = retyped.inputPattern;
1025
1024
  outputPattern = retyped.outputPattern;
1026
- } else {
1027
- retypedKir = objectToKIRP(object);
1028
1025
  }
1029
1026
 
1030
- let kvmArtifact = objectToKVMArtifact(object, relation, null, options);
1031
- let specializedKvm = kvmArtifact;
1032
- if (inputPattern) {
1033
- try {
1034
- specializedKvm = specializeKVM(kvmArtifact, inputPattern);
1035
- } catch {
1036
- specializedKvm = {
1037
- ...kvmArtifact,
1038
- functions: lowerKIRToKVM(retypedKir),
1039
- entry: relation
1040
- };
1041
- }
1042
- }
1043
-
1044
- const entry = specializedKvm.entry || relation;
1045
- const functions = specializedKvm.functions;
1027
+ const retypedKir = objectToKIRP(targetObject);
1028
+ const functions = lowerKIRToKVM(retypedKir);
1029
+ const entry = relation || targetObject.main || "__main__";
1030
+
1031
+ const specializedKvm = {
1032
+ format: "k-vm",
1033
+ version: 1,
1034
+ entry,
1035
+ functions,
1036
+ inputPattern: entryInputPattern,
1037
+ outputPattern
1038
+ };
1046
1039
 
1047
1040
  const llvm = compileKVMModuleToLLVM(entry, functions, {
1048
1041
  ...options,
1049
1042
  relation,
1050
- inputPattern: entryInputPattern || specializedKvm.inputPattern,
1051
- outputPattern: outputPattern || specializedKvm.outputPattern
1043
+ inputPattern: entryInputPattern,
1044
+ outputPattern
1052
1045
  });
1053
1046
 
1054
1047
  return {
1055
1048
  kir: retypedKir,
1056
1049
  kvm: specializedKvm,
1057
1050
  relation,
1058
- inputPattern: entryInputPattern || specializedKvm.inputPattern,
1059
- outputPattern: outputPattern || specializedKvm.outputPattern,
1051
+ inputPattern: entryInputPattern,
1052
+ outputPattern,
1060
1053
  llvm
1061
1054
  };
1062
1055
  }
@@ -112,7 +112,7 @@ node --input-type=module -e "import { stdout } from 'node:process'; import { enc
112
112
  | node --input-type=module -e "import { stdin } from 'node:process'; import { decodeWire } from '../../codecs/runtime/prefix-codec.mjs'; const chunks = []; stdin.on('data', (chunk) => chunks.push(chunk)); stdin.on('end', () => console.log(JSON.stringify(decodeWire(Buffer.concat(chunks)).pattern)));" \
113
113
  | grep -Fqx '[["open-union",[["left",1],["right",1]]],["closed-product",[]]]'
114
114
 
115
- node ./bin/k-llvm-build.mjs '$bits = < {} _, bits 0, bits 1 >; $int = < bits "+", bits "-" >; $int' -o "$TMP_DIR/id-int-exe"
115
+ node ./bin/k-llvm-build.mjs '?< <bits 0, bits 1, {} _>=bits "+", bits "-">' -o "$TMP_DIR/id-int-exe"
116
116
  printf '2' \
117
117
  | node ../../codecs/int.mjs --parse \
118
118
  | "$TMP_DIR/id-int-exe" \
@@ -121,7 +121,7 @@ printf '2' \
121
121
 
122
122
  node --input-type=module -e "import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; import { encodeToWire, decodeWire } from '../../codecs/runtime/prefix-codec.mjs'; import { parse, INT_PATTERN } from '../../codecs/int.mjs'; const input = encodeToWire(parse('2'), INT_PATTERN); const header = Buffer.alloc(4); header.writeUInt32BE(input.length); const child = spawn(process.argv[1], ['--server']); const chunks = []; const stderr = []; child.stdout.on('data', (chunk) => chunks.push(chunk)); child.stderr.on('data', (chunk) => stderr.push(chunk)); child.stdin.end(Buffer.concat([header, input])); const status = await new Promise((resolve, reject) => { child.on('error', reject); child.on('close', resolve); }); assert.equal(status, 0, Buffer.concat(stderr).toString('utf8')); const output = Buffer.concat(chunks); const length = output.readUInt32BE(0); assert.equal(output.length, 4 + length); assert.deepEqual(decodeWire(output.subarray(4)).value.toJSON(), parse('2').toJSON());" "$TMP_DIR/id-int-exe"
123
123
 
124
- node ./bin/k-llvm-build.mjs --lib ../../Examples/arithmetics.k --export times 's = {()x, ()y} times; s' -o "$TMP_DIR/square-exe"
124
+ node ./bin/k-llvm-build.mjs --lib ../../Examples/arithmetics.k --export times 's = ?< <bits 0, bits 1, {} _>=bits "+", bits "-" > {()x, ()y} times; s' -o "$TMP_DIR/square-exe"
125
125
  printf '987654321' \
126
126
  | node ../../codecs/int.mjs --parse \
127
127
  | "$TMP_DIR/square-exe" \
@@ -239,11 +239,11 @@ assert.doesNotMatch(nestedUnionLLVM, /call void @k_product_set_at\(ptr %product\
239
239
  assert.doesNotMatch(nestedUnionLLVM, /ptr false/);
240
240
 
241
241
  const productRecurseObject = decodeObject(compileObjectBuffer(`
242
- $ list = < {} nil, { {} car, list cdr } cons >;
243
- foo = $list <
242
+ list = ?< {} nil, { {} car, list cdr } cons >;
243
+ foo = list <
244
244
  /nil |nil,
245
245
  /cons { .car car, .cdr foo cdr } |cons
246
- > $list;
246
+ > list;
247
247
  foo
248
248
  `, { source: "llvm-product-recurse.k" }));
249
249
  const { llvm: productRecurseLLVM } = compileObjectToLLVM(productRecurseObject, {
@@ -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}`);