@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 +14 -0
- package/backend-api.mjs +9 -1
- package/backends/llvm/scripts/run-tests.mjs +5 -0
- package/backends/llvm/src/kvm2llvm.mjs +2 -1
- package/backends/llvm/src/llvm.mjs +18 -25
- package/backends/llvm/tests/integration.sh +2 -2
- package/backends/llvm/tests/test-llvm.mjs +3 -3
- package/backends/llvm/tests/test-open-product-misalignment.mjs +140 -0
- package/backends/wasm/demo_ieee.mjs +2 -3
- package/backends/wasm/runtime.wat +157 -5
- package/backends/wasm/src/kvm2wasm.mjs +57 -34
- package/backends/wasm/src/wasm.mjs +38 -17
- package/backends/wasm/tests/perf-support.mjs +4 -4
- package/backends/wasm/tests/test-wasm.mjs +13 -14
- package/browser/vfs-data.mjs +1 -1
- package/kir.mjs +213 -17
- package/package.json +1 -1
- package/tests/test-repl-html.mjs +15 -0
- package/tests/test-repl.mjs +10 -0
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 {
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
|
1051
|
-
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
|
|
1059
|
-
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 '
|
|
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
|
-
|
|
243
|
-
foo =
|
|
242
|
+
list = ?< {} nil, { {} car, list cdr } cons >;
|
|
243
|
+
foo = list <
|
|
244
244
|
/nil |nil,
|
|
245
245
|
/cons { .car car, .cdr foo cdr } |cons
|
|
246
|
-
>
|
|
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
|
|
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,
|
|
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
|
-
|
|
417
|
+
i32.store
|
|
410
418
|
|
|
411
|
-
|
|
419
|
+
;; Value slot offset is 8 + 4*N + 4*i
|
|
412
420
|
i32.const 8
|
|
413
|
-
local.get $
|
|
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.
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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(`
|
|
211
|
-
lines.push(` i32.const ${
|
|
212
|
-
lines.push(`
|
|
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 ${
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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} (
|
|
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 ${
|
|
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}`);
|