@fraczak/k 6.3.2 → 6.4.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.
- package/DOCS/CODECS.md +258 -0
- package/DOCS/DICTIONARY.md +1172 -0
- package/DOCS/KIR_SPECIALIZATION_DESIGN.md +51 -46
- package/DOCS/KIR_V1.md +221 -0
- package/DOCS/KVM_EXECUTION_MODEL.md +56 -19
- package/DOCS/OBJECT_FILE_AND_PATTERN.md +2 -1
- package/DOCS/PROFILING_AND_TRACING.md +196 -0
- package/DOCS/REPL.md +76 -14
- package/DOCS/WEB_REPL_REQUIREMENTS.md +135 -0
- package/DOCS/dependency-graph.md +144 -0
- package/Examples/arithmetics.k +51 -48
- package/Examples/bnat.k +1 -1
- package/Examples/buda.k +44 -0
- package/Examples/ieee.k +300 -8671
- package/Examples/poly.k +67 -0
- package/README.md +68 -19
- package/augmentation.mjs +9 -7
- package/backend-api.mjs +11 -2
- package/backends/arm64/README.md +104 -0
- package/backends/arm64/bin/k-arm64-compile.mjs +101 -0
- package/backends/arm64/bin/k-arm64-run.mjs +58 -0
- package/backends/arm64/bin/k-arm64.mjs +88 -0
- package/backends/arm64/scripts/run-tests.mjs +41 -0
- package/backends/arm64/src/arm64.mjs +229 -0
- package/backends/arm64/src/cli.mjs +155 -0
- package/backends/arm64/src/kvm2arm64.mjs +906 -0
- package/backends/arm64/src/runtime.c +728 -0
- package/backends/arm64/tests/integration.sh +112 -0
- package/backends/arm64/tests/test-arm64.mjs +240 -0
- package/backends/llvm/README.md +135 -0
- package/backends/llvm/bin/k-llvm-build.mjs +71 -0
- package/backends/llvm/bin/k-llvm-compile.mjs +68 -0
- package/backends/llvm/bin/k-llvm-jit.mjs +160 -0
- package/backends/llvm/bin/k-llvm-run.mjs +90 -0
- package/backends/llvm/runtime/krt.c +1508 -0
- package/backends/llvm/runtime/krt.h +109 -0
- package/backends/llvm/scripts/conformance.mjs +76 -0
- package/backends/llvm/scripts/run-tests.mjs +28 -0
- package/backends/llvm/src/executable.mjs +562 -0
- package/backends/llvm/src/kvm2llvm.mjs +998 -0
- package/backends/llvm/src/llvm.mjs +1092 -0
- package/backends/llvm/tests/composition-driver.c +22 -0
- package/backends/llvm/tests/identity-driver.c +27 -0
- package/backends/llvm/tests/integration.sh +153 -0
- package/backends/llvm/tests/perf-support.mjs +735 -0
- package/backends/llvm/tests/product-driver.c +23 -0
- package/backends/llvm/tests/projection-driver.c +19 -0
- package/backends/llvm/tests/relation-driver.c +29 -0
- package/backends/llvm/tests/test-llvm.mjs +240 -0
- package/backends/llvm/tests/union-driver.c +31 -0
- package/backends/llvm/tests/variant-driver.c +19 -0
- package/backends/llvm/tests/variant-projection-driver.c +21 -0
- package/backends/wasm/DOCS/USE-WEB-ASSEMBLY.md +233 -0
- package/backends/wasm/README.md +97 -0
- package/backends/wasm/TECHNICAL-DEBT.md +60 -0
- package/backends/wasm/bin/k-wasm-compile.mjs +50 -0
- package/backends/wasm/bin/k-wasm-run.mjs +140 -0
- package/backends/wasm/bin/k-wasm.mjs +49 -0
- package/backends/wasm/demo_ieee.mjs +319 -0
- package/backends/wasm/runtime.wat +102 -0
- package/backends/wasm/scripts/run-tests.mjs +41 -0
- package/backends/wasm/src/cli.mjs +133 -0
- package/backends/wasm/src/kvm2wasm.mjs +661 -0
- package/backends/wasm/src/wasm.mjs +1010 -0
- package/backends/wasm/tests/integration.sh +182 -0
- package/backends/wasm/tests/perf-support.mjs +336 -0
- package/backends/wasm/tests/test-wasm.mjs +617 -0
- package/browser/repl-browser.mjs +1584 -0
- package/browser/shims/assert.mjs +19 -0
- package/browser/shims/buffer.mjs +224 -0
- package/browser/shims/crypto.mjs +103 -0
- package/browser/shims/fs.mjs +159 -0
- package/browser/shims/os.mjs +17 -0
- package/browser/shims/path.mjs +54 -0
- package/browser/shims/process.mjs +44 -0
- package/browser/shims/readline.mjs +11 -0
- package/browser/shims/url.mjs +12 -0
- package/browser/vfs-data.mjs +19 -0
- package/codecs/README.md +3 -0
- package/codecs/json.mjs +16 -2
- package/codecs/runtime/show-value.mjs +2 -2
- package/conformance/helper-reuse/case.json +8 -0
- package/conformance/helper-reuse/expected.kv +1 -0
- package/conformance/helper-reuse/input.kv +1 -0
- package/conformance/helper-reuse/program.k +2 -0
- package/conformance/identity/case.json +8 -0
- package/conformance/identity/expected.kv +1 -0
- package/conformance/identity/input.kv +1 -0
- package/conformance/identity/program.k +1 -0
- package/conformance/product-projection/case.json +8 -0
- package/conformance/product-projection/expected.kv +1 -0
- package/conformance/product-projection/input.kv +1 -0
- package/conformance/product-projection/program.k +1 -0
- package/conformance/run.mjs +149 -0
- package/conformance/variant-construction/case.json +8 -0
- package/conformance/variant-construction/expected.kv +1 -0
- package/conformance/variant-construction/input.kv +1 -0
- package/conformance/variant-construction/program.k +1 -0
- package/harness/adapters/arm64.mjs +105 -0
- package/harness/adapters/kvm.mjs +144 -0
- package/harness/adapters/llvm.mjs +118 -0
- package/harness/adapters/native.mjs +77 -0
- package/harness/adapters/wasm.mjs +169 -0
- package/harness/backend.mjs +95 -0
- package/harness/cli.mjs +130 -0
- package/harness/format.mjs +95 -0
- package/harness/index.mjs +25 -0
- package/harness/persistent.mjs +121 -0
- package/harness/registry.mjs +44 -0
- package/harness/suite.mjs +257 -0
- package/harness/suites/poly.mjs +156 -0
- package/harness/trace.mjs +50 -0
- package/harness/util.mjs +92 -0
- package/intrinsics.mjs +41 -0
- package/kir.mjs +399 -0
- package/kvm.mjs +585 -81
- package/objects/README.md +37 -3
- package/objects/compile.mjs +15 -22
- package/objects/inspect.mjs +96 -0
- package/objects/validate.mjs +415 -0
- package/package.json +51 -19
- package/repl-codecs.mjs +133 -20
- package/repl.mjs +408 -59
- package/run.mjs +2 -7
- package/scripts/benchmark.mjs +406 -22
- package/scripts/build-repl-html.mjs +1784 -0
- package/scripts/perf-ieee.mjs +586 -0
- package/scripts/perf-int.mjs +531 -0
- package/scripts/perf-poly.mjs +945 -0
- package/scripts/render-benchmark.mjs +12 -8
- package/scripts/run-tests.mjs +6 -0
- package/tests/integration.sh +19 -2
- package/tests/test-arithmetics.mjs +54 -0
- package/tests/test-ieee-arithmetic.mjs +6 -2
- package/tests/test-kir.mjs +85 -0
- package/tests/test-kvm-polymorphic.mjs +278 -0
- package/tests/test-kvm.mjs +25 -6
- package/tests/test-poly.mjs +129 -0
- package/tests/test-repl-html.mjs +303 -0
- package/tests/test-repl.mjs +175 -1
- package/tests/test-validate-object.mjs +40 -0
- package/a.klib +0 -1
- package/a.kvm +0 -142676
- package/a.wasm +0 -0
- package/benchmark-results.html +0 -532
- package/benchmark-results.json +0 -480
- package/k-vscode/.vscode/launch.json +0 -14
- package/k-vscode/README.md +0 -75
- package/k-vscode/extension.js +0 -273
- package/k-vscode/language-configuration.json +0 -42
- package/k-vscode/package.json +0 -79
- package/k-vscode/snippets/k.json +0 -92
- package/k-vscode/syntaxes/k.tmLanguage.json +0 -599
- package/k-vscode/test/background-ranges.test.js +0 -50
- package/k-vscode/themes/k-contrast-color-theme.json +0 -104
- package/scripts/git-hooks/pre-commit +0 -10
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import wabtFactory from "wabt";
|
|
4
|
+
|
|
5
|
+
console.log("==> Initializing WebAssembly TDD Harness");
|
|
6
|
+
|
|
7
|
+
let wabtInstance;
|
|
8
|
+
try {
|
|
9
|
+
wabtInstance = await wabtFactory();
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error("Failed to load WABT:", error);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Helper to compile WAT string to a binary Uint8Array
|
|
16
|
+
export function compileWat(filename, watText) {
|
|
17
|
+
const watModule = wabtInstance.parseWat(filename, watText, {
|
|
18
|
+
mutable_globals: true,
|
|
19
|
+
sat_float_to_int: true,
|
|
20
|
+
sign_extension: true,
|
|
21
|
+
multi_value: true,
|
|
22
|
+
bulk_memory: true,
|
|
23
|
+
reference_types: true
|
|
24
|
+
});
|
|
25
|
+
watModule.resolveNames();
|
|
26
|
+
watModule.validate();
|
|
27
|
+
const binary = watModule.toBinary({
|
|
28
|
+
log: false,
|
|
29
|
+
canonicalize_lebs: true,
|
|
30
|
+
relocatable: false,
|
|
31
|
+
write_debug_names: true
|
|
32
|
+
});
|
|
33
|
+
return binary.buffer;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Helper to instantiate compiled WASM buffer with standard imports
|
|
37
|
+
export async function instantiateWasm(binaryBuffer, imports = {}) {
|
|
38
|
+
const module = await WebAssembly.compile(binaryBuffer);
|
|
39
|
+
const instance = await WebAssembly.instantiate(module, imports);
|
|
40
|
+
return instance.exports;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log("==> Running smoke test: Basic WAT Compilation & Execution");
|
|
44
|
+
{
|
|
45
|
+
const smokeWat = `
|
|
46
|
+
(module
|
|
47
|
+
(func (export "add") (param $x i32) (param $y i32) (result i32)
|
|
48
|
+
local.get $x
|
|
49
|
+
local.get $y
|
|
50
|
+
i32.add
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
const binary = compileWat("smoke.wat", smokeWat);
|
|
56
|
+
const exports = await instantiateWasm(binary);
|
|
57
|
+
|
|
58
|
+
const res = exports.add(40, 2);
|
|
59
|
+
console.log(`Add output: 40 + 2 = ${res}`);
|
|
60
|
+
assert.equal(res, 42, "Smoke test: 40 + 2 must equal 42");
|
|
61
|
+
console.log("Smoke test passed!");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log("==> Running runtime.wat Core Allocator Tests");
|
|
65
|
+
{
|
|
66
|
+
const runtimeWat = fs.readFileSync("runtime.wat", "utf8");
|
|
67
|
+
const binary = compileWat("runtime.wat", runtimeWat);
|
|
68
|
+
const runtimeExports = await instantiateWasm(binary);
|
|
69
|
+
|
|
70
|
+
// Test 1: First allocation starts at 1024 (aligned)
|
|
71
|
+
const ptr1 = runtimeExports.alloc(10);
|
|
72
|
+
assert.equal(ptr1, 1024, "Allocation 1 must start at 1024");
|
|
73
|
+
assert.equal(ptr1 % 8, 0, "Allocation 1 must be 8-byte aligned");
|
|
74
|
+
|
|
75
|
+
// Test 2: Second allocation must account for 10 bytes aligned to 16 bytes
|
|
76
|
+
const ptr2 = runtimeExports.alloc(12);
|
|
77
|
+
assert.equal(ptr2, 1040, "Allocation 2 must start at 1040");
|
|
78
|
+
assert.equal(ptr2 % 8, 0, "Allocation 2 must be 8-byte aligned");
|
|
79
|
+
|
|
80
|
+
// Test 3: Resetting to a mark must make temporary arena space reusable
|
|
81
|
+
const mark = runtimeExports.arena_mark();
|
|
82
|
+
assert.equal(mark, 1056, "Arena mark must point to the next free byte");
|
|
83
|
+
assert.equal(runtimeExports.alloc(32), mark, "Temporary allocation must start at the mark");
|
|
84
|
+
runtimeExports.arena_reset(mark);
|
|
85
|
+
assert.equal(runtimeExports.alloc(32), mark, "Reset arena space must be reused");
|
|
86
|
+
|
|
87
|
+
// Test 4: Large allocation causing memory growth beyond 64KB (page 1 limit)
|
|
88
|
+
const ptr3 = runtimeExports.alloc(70000);
|
|
89
|
+
assert.ok(ptr3 > 1040, "Large allocation must return a valid pointer");
|
|
90
|
+
assert.equal(ptr3 % 8, 0, "Large allocation must be 8-byte aligned");
|
|
91
|
+
console.log("Core Allocator tests passed successfully!");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
import { lowerToWasm } from "../src/kvm2wasm.mjs";
|
|
95
|
+
|
|
96
|
+
console.log("==> Running Compiler Scaffolding Tests (id, fail, return)");
|
|
97
|
+
{
|
|
98
|
+
const runtimeWat = fs.readFileSync("runtime.wat", "utf8");
|
|
99
|
+
|
|
100
|
+
// Compile Identity function: %v1 = %in; return %v1;
|
|
101
|
+
const kvmIdentity = {
|
|
102
|
+
body: [
|
|
103
|
+
{ op: "id", dest: "%v1", src: "%in" },
|
|
104
|
+
{ op: "return", src: "%v1" }
|
|
105
|
+
]
|
|
106
|
+
};
|
|
107
|
+
const identityWat = lowerToWasm(kvmIdentity, "test_identity");
|
|
108
|
+
const fullIdentityWat = runtimeWat.trim().slice(0, -1) + "\n" + identityWat + "\n)";
|
|
109
|
+
const binaryIdentity = compileWat("identity.wat", fullIdentityWat);
|
|
110
|
+
const exportsIdentity = await instantiateWasm(binaryIdentity);
|
|
111
|
+
|
|
112
|
+
// Invoke identity: input value 1234 should return (1234, 1) -> success
|
|
113
|
+
const resIdentity = exportsIdentity.test_identity(1234);
|
|
114
|
+
// WebAssembly multi-value returns are returned as array in some JS environments, or directly if supported.
|
|
115
|
+
// Note: Node 16+ supports multi-value return as array.
|
|
116
|
+
assert.deepEqual(resIdentity, [1234, 1], "Identity test: must return [1234, 1]");
|
|
117
|
+
|
|
118
|
+
// Compile Failure function: fail;
|
|
119
|
+
const kvmFailure = {
|
|
120
|
+
body: [
|
|
121
|
+
{ op: "fail" }
|
|
122
|
+
]
|
|
123
|
+
};
|
|
124
|
+
const failureWat = lowerToWasm(kvmFailure, "test_failure");
|
|
125
|
+
const fullFailureWat = runtimeWat.trim().slice(0, -1) + "\n" + failureWat + "\n)";
|
|
126
|
+
const binaryFailure = compileWat("failure.wat", fullFailureWat);
|
|
127
|
+
const exportsFailure = await instantiateWasm(binaryFailure);
|
|
128
|
+
|
|
129
|
+
// Invoke failure: input value 1234 should return (0, 0) -> failure
|
|
130
|
+
const resFailure = exportsFailure.test_failure(1234);
|
|
131
|
+
assert.deepEqual(resFailure, [0, 0], "Failure test: must return [0, 0]");
|
|
132
|
+
|
|
133
|
+
console.log("Compiler Scaffolding tests passed successfully!");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
import { annotate, objectToKVMArtifact } from "@fraczak/k/backend-api.mjs";
|
|
137
|
+
import { compileObjectBuffer, decodeObject } from "@fraczak/k/object.mjs";
|
|
138
|
+
import { compileWasmArtifactFromKVM, metadataFromModule } from "../src/wasm.mjs";
|
|
139
|
+
import { getTagId } from "../src/kvm2wasm.mjs";
|
|
140
|
+
|
|
141
|
+
console.log("==> Running Wrapped kVM Artifact Test");
|
|
142
|
+
{
|
|
143
|
+
const object = decodeObject(compileObjectBuffer("()", { source: "wasm-kvm-artifact.k" }));
|
|
144
|
+
const artifact = objectToKVMArtifact(object, "__main__", [["closed-product", []]]);
|
|
145
|
+
assert.equal(artifact.layer, "KVM");
|
|
146
|
+
assert.equal(artifact.kir.layer, "KIR-P");
|
|
147
|
+
assert(!("instanceKey" in artifact));
|
|
148
|
+
const binary = await compileWasmArtifactFromKVM(artifact);
|
|
149
|
+
const metadata = metadataFromModule(await WebAssembly.compile(binary));
|
|
150
|
+
assert.equal(metadata.entry, "rel___main__");
|
|
151
|
+
assert.equal(metadata.relation, "__main__");
|
|
152
|
+
assert(!("instanceKey" in metadata));
|
|
153
|
+
assert.deepEqual(metadata.inputPattern, artifact.inputPattern);
|
|
154
|
+
assert.deepEqual(metadata.outputPattern, artifact.outputPattern);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log("==> Running Product Operations Integration Tests");
|
|
158
|
+
{
|
|
159
|
+
const runtimeWat = fs.readFileSync("runtime.wat", "utf8");
|
|
160
|
+
|
|
161
|
+
// Compile a relation that creates a product { .x x, .y y } and another that projects .y
|
|
162
|
+
const productDefs = annotate(`
|
|
163
|
+
make_prod = { .x x, .y y };
|
|
164
|
+
proj_y = make_prod .y;
|
|
165
|
+
()
|
|
166
|
+
`, { convergence: { strategy: "auto" } });
|
|
167
|
+
|
|
168
|
+
const makeProdWat = lowerToWasm(productDefs.rels.make_prod, "make_prod");
|
|
169
|
+
const projYWat = lowerToWasm(productDefs.rels.proj_y, "proj_y");
|
|
170
|
+
|
|
171
|
+
const fullWat = runtimeWat.trim().slice(0, -1) + "\n" + makeProdWat + "\n" + projYWat + "\n)";
|
|
172
|
+
const binary = compileWat("product_tests.wat", fullWat);
|
|
173
|
+
const exports = await instantiateWasm(binary);
|
|
174
|
+
|
|
175
|
+
// Helper to write a product to the arena
|
|
176
|
+
function writeProductToArena(fieldsMap) {
|
|
177
|
+
const keys = Object.keys(fieldsMap).sort();
|
|
178
|
+
const N = keys.length;
|
|
179
|
+
const totalSize = 8 + 8 * N;
|
|
180
|
+
const ptr = exports.alloc(totalSize);
|
|
181
|
+
const view = new DataView(exports.memory.buffer);
|
|
182
|
+
|
|
183
|
+
view.setUint32(ptr, totalSize, true);
|
|
184
|
+
view.setUint32(ptr + 4, N, true);
|
|
185
|
+
|
|
186
|
+
for (let i = 0; i < N; i++) {
|
|
187
|
+
const offsetVal = 8 + 4 * N + 4 * i;
|
|
188
|
+
const val = fieldsMap[keys[i]];
|
|
189
|
+
view.setUint32(ptr + 8 + 4 * i, offsetVal, true);
|
|
190
|
+
view.setUint32(ptr + offsetVal, val, true);
|
|
191
|
+
}
|
|
192
|
+
return ptr;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 1. Test manual product creation & compiled projection
|
|
196
|
+
const ptrIn = writeProductToArena({ x: 101, y: 202 });
|
|
197
|
+
const resProj = exports.proj_y(ptrIn);
|
|
198
|
+
assert.deepEqual(resProj, [202, 1], "Projection of y should return [202, 1]");
|
|
199
|
+
|
|
200
|
+
// 2. Test compiled product creation & compiled projection
|
|
201
|
+
// Passing ptrIn to make_prod should construct a new product using make_prod's logic
|
|
202
|
+
const resMake = exports.make_prod(ptrIn);
|
|
203
|
+
assert.equal(resMake[1], 1, "make_prod should succeed");
|
|
204
|
+
const ptrOut = resMake[0];
|
|
205
|
+
|
|
206
|
+
// Inspect memory of ptrOut
|
|
207
|
+
const view = new DataView(exports.memory.buffer);
|
|
208
|
+
assert.equal(view.getUint32(ptrOut, true), 24, "Product total size should be 24");
|
|
209
|
+
assert.equal(view.getUint32(ptrOut + 4, true), 2, "Product field count should be 2");
|
|
210
|
+
assert.equal(view.getUint32(ptrOut + 8, true), 16, "Offset of first field (x) should be 16");
|
|
211
|
+
assert.equal(view.getUint32(ptrOut + 12, true), 20, "Offset of second field (y) should be 20");
|
|
212
|
+
|
|
213
|
+
// The first field value should be the projection of x from ptrIn (101)
|
|
214
|
+
const valXPtr = view.getUint32(ptrOut + 16, true);
|
|
215
|
+
assert.equal(valXPtr, 101, "First field value should be 101");
|
|
216
|
+
// The second field value should be the projection of y from ptrIn (202)
|
|
217
|
+
const valYPtr = view.getUint32(ptrOut + 20, true);
|
|
218
|
+
assert.equal(valYPtr, 202, "Second field value should be 202");
|
|
219
|
+
|
|
220
|
+
console.log("Product Operations integration tests passed successfully!");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
console.log("==> Running Variant Operations Integration Tests");
|
|
224
|
+
{
|
|
225
|
+
const runtimeWat = fs.readFileSync("runtime.wat", "utf8");
|
|
226
|
+
|
|
227
|
+
const variantDefs = annotate(`
|
|
228
|
+
make_foo = |foo;
|
|
229
|
+
proj_foo = /foo;
|
|
230
|
+
proj_bar = /bar;
|
|
231
|
+
()
|
|
232
|
+
`, { convergence: { strategy: "auto" } });
|
|
233
|
+
|
|
234
|
+
const makeFooWat = lowerToWasm(variantDefs.rels.make_foo, "make_foo");
|
|
235
|
+
const projFooWat = lowerToWasm(variantDefs.rels.proj_foo, "proj_foo");
|
|
236
|
+
const projBarWat = lowerToWasm(variantDefs.rels.proj_bar, "proj_bar");
|
|
237
|
+
|
|
238
|
+
const fullWat = runtimeWat.trim().slice(0, -1) + "\n" +
|
|
239
|
+
makeFooWat + "\n" +
|
|
240
|
+
projFooWat + "\n" +
|
|
241
|
+
projBarWat + "\n)";
|
|
242
|
+
const binary = compileWat("variant_tests.wat", fullWat);
|
|
243
|
+
const exports = await instantiateWasm(binary);
|
|
244
|
+
|
|
245
|
+
// 1. Test compilation of variant tag creation
|
|
246
|
+
const resMake = exports.make_foo(999);
|
|
247
|
+
assert.equal(resMake[1], 1, "make_foo should succeed");
|
|
248
|
+
const ptrVar = resMake[0];
|
|
249
|
+
|
|
250
|
+
const view = new DataView(exports.memory.buffer);
|
|
251
|
+
assert.equal(view.getUint32(ptrVar, true), 12, "Variant total size should be 12");
|
|
252
|
+
assert.equal(view.getUint32(ptrVar + 4, true), getTagId("foo"), "Variant tagId should match 'foo'");
|
|
253
|
+
assert.equal(view.getUint32(ptrVar + 8, true), 999, "Variant payload should be 999");
|
|
254
|
+
|
|
255
|
+
// 2. Test variant projection success
|
|
256
|
+
const resProjSuccess = exports.proj_foo(ptrVar);
|
|
257
|
+
assert.deepEqual(resProjSuccess, [999, 1], "Projecting 'foo' from variant 'foo' should return [999, 1]");
|
|
258
|
+
|
|
259
|
+
// 3. Test variant projection failure
|
|
260
|
+
const resProjFail = exports.proj_bar(ptrVar);
|
|
261
|
+
assert.deepEqual(resProjFail, [0, 0], "Projecting 'bar' from variant 'foo' should fail and return [0, 0]");
|
|
262
|
+
|
|
263
|
+
console.log("Variant Operations integration tests passed successfully!");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
console.log("==> Running Union & Choice Integration Tests");
|
|
267
|
+
{
|
|
268
|
+
const runtimeWat = fs.readFileSync("runtime.wat", "utf8");
|
|
269
|
+
|
|
270
|
+
const unionDefs = annotate(`
|
|
271
|
+
choose = < /x, /y >;
|
|
272
|
+
()
|
|
273
|
+
`, { convergence: { strategy: "auto" } });
|
|
274
|
+
|
|
275
|
+
const chooseWat = lowerToWasm(unionDefs.rels.choose, "choose");
|
|
276
|
+
const chooseWithRollbackWat = lowerToWasm({
|
|
277
|
+
body: [
|
|
278
|
+
{
|
|
279
|
+
op: "union",
|
|
280
|
+
dest: "%v0",
|
|
281
|
+
src: "%in",
|
|
282
|
+
branches: [
|
|
283
|
+
{
|
|
284
|
+
body: [
|
|
285
|
+
{ op: "make_variant", dest: "%v1", tag: "temporary", src: "%in" },
|
|
286
|
+
{ op: "fail" }
|
|
287
|
+
]
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
body: [
|
|
291
|
+
{ op: "id", dest: "%v2", src: "%in" },
|
|
292
|
+
{ op: "return", src: "%v2" }
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
]
|
|
296
|
+
},
|
|
297
|
+
{ op: "return", src: "%v0" }
|
|
298
|
+
]
|
|
299
|
+
}, "choose_with_rollback");
|
|
300
|
+
const fullWat = runtimeWat.trim().slice(0, -1) + "\n" + chooseWat + "\n" + chooseWithRollbackWat + "\n)";
|
|
301
|
+
const binary = compileWat("union_tests.wat", fullWat);
|
|
302
|
+
const exports = await instantiateWasm(binary);
|
|
303
|
+
|
|
304
|
+
// Helper to write a variant to the arena
|
|
305
|
+
function writeVariantToArena(tag, payloadVal) {
|
|
306
|
+
const tagId = getTagId(tag);
|
|
307
|
+
const ptr = exports.alloc(12);
|
|
308
|
+
const view = new DataView(exports.memory.buffer);
|
|
309
|
+
view.setUint32(ptr, 12, true);
|
|
310
|
+
view.setUint32(ptr + 4, tagId, true);
|
|
311
|
+
view.setUint32(ptr + 8, payloadVal, true);
|
|
312
|
+
return ptr;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 1. Pass variant with tag 'x': should match branch 1 and succeed
|
|
316
|
+
const ptrX = writeVariantToArena("x", 777);
|
|
317
|
+
const resX = exports.choose(ptrX);
|
|
318
|
+
assert.deepEqual(resX, [777, 1], "choose with tag 'x' should return [777, 1]");
|
|
319
|
+
|
|
320
|
+
// 2. Pass variant with tag 'y': should fail branch 1, match branch 2, and succeed
|
|
321
|
+
const ptrY = writeVariantToArena("y", 888);
|
|
322
|
+
const resY = exports.choose(ptrY);
|
|
323
|
+
assert.deepEqual(resY, [888, 1], "choose with tag 'y' should return [888, 1]");
|
|
324
|
+
|
|
325
|
+
// 3. Pass variant with tag 'z': should fail both branches and return failure
|
|
326
|
+
const ptrZ = writeVariantToArena("z", 999);
|
|
327
|
+
const resZ = exports.choose(ptrZ);
|
|
328
|
+
assert.deepEqual(resZ, [0, 0], "choose with tag 'z' should return [0, 0]");
|
|
329
|
+
|
|
330
|
+
// 4. Failed branches must release scratch allocations before trying the next choice
|
|
331
|
+
const mark = exports.arena_mark();
|
|
332
|
+
assert.deepEqual(exports.choose_with_rollback(999), [999, 1], "Fallback branch should return the input");
|
|
333
|
+
assert.equal(exports.arena_mark(), mark, "Failed union branch allocations must be reclaimed");
|
|
334
|
+
|
|
335
|
+
console.log("Union & Choice integration tests passed successfully!");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
import {
|
|
339
|
+
exportPatternGraph,
|
|
340
|
+
fromObject,
|
|
341
|
+
isProduct,
|
|
342
|
+
isVariant,
|
|
343
|
+
NODE_KIND,
|
|
344
|
+
patternToPropertyList,
|
|
345
|
+
propertyListToPattern,
|
|
346
|
+
Value
|
|
347
|
+
} from "@fraczak/k/backend-api.mjs";
|
|
348
|
+
import { getTagFromId } from "../src/kvm2wasm.mjs";
|
|
349
|
+
import { readArenaValue, wasmPtr, writeValueToArena } from "../src/wasm.mjs";
|
|
350
|
+
|
|
351
|
+
console.log("==> Running Stack-Safe Arena Serialization Tests");
|
|
352
|
+
{
|
|
353
|
+
assert.equal(wasmPtr(-2061758672), 2233208624, "Exported Wasm i32 pointers must be treated as unsigned offsets");
|
|
354
|
+
|
|
355
|
+
const depth = 12000;
|
|
356
|
+
const memory = { buffer: new ArrayBuffer(1024 + 8 + 16 * (depth + 2)) };
|
|
357
|
+
const view = new DataView(memory.buffer);
|
|
358
|
+
const propertyList = [
|
|
359
|
+
["closed-union", [["+", 1]]],
|
|
360
|
+
["closed-union", [["_", 2], ["1", 1]]],
|
|
361
|
+
["closed-product", []]
|
|
362
|
+
];
|
|
363
|
+
const pattern = {
|
|
364
|
+
nodes: [
|
|
365
|
+
{ kind: NODE_KIND.CLOSED_UNION, edges: [{ label: "+", target: 1 }] },
|
|
366
|
+
{ kind: NODE_KIND.CLOSED_UNION, edges: [{ label: "_", target: 2 }, { label: "1", target: 1 }] },
|
|
367
|
+
{ kind: NODE_KIND.CLOSED_PRODUCT, edges: [] }
|
|
368
|
+
]
|
|
369
|
+
};
|
|
370
|
+
const tagNames = new Map([[1, "+"], [2, "_"], [3, "1"]]);
|
|
371
|
+
const tagIds = new Map([["+", 1], ["_", 2], ["1", 3]]);
|
|
372
|
+
const tags = {
|
|
373
|
+
getId: (tag) => tagIds.get(tag) ?? null,
|
|
374
|
+
getTag: (id) => tagNames.get(id) ?? null
|
|
375
|
+
};
|
|
376
|
+
let free = 1024;
|
|
377
|
+
|
|
378
|
+
function writeVariant(tagId, payloadPtr) {
|
|
379
|
+
const ptr = free;
|
|
380
|
+
free += 12;
|
|
381
|
+
view.setUint32(ptr, 12, true);
|
|
382
|
+
view.setUint32(ptr + 4, tagId, true);
|
|
383
|
+
view.setUint32(ptr + 8, payloadPtr, true);
|
|
384
|
+
return ptr;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const unitPtr = free;
|
|
388
|
+
free += 8;
|
|
389
|
+
view.setUint32(unitPtr, 8, true);
|
|
390
|
+
view.setUint32(unitPtr + 4, 0, true);
|
|
391
|
+
|
|
392
|
+
let ptr = writeVariant(2, unitPtr);
|
|
393
|
+
for (let i = 0; i < depth; i++) {
|
|
394
|
+
ptr = writeVariant(3, ptr);
|
|
395
|
+
}
|
|
396
|
+
ptr = writeVariant(1, ptr);
|
|
397
|
+
|
|
398
|
+
function assertDeepValue(output) {
|
|
399
|
+
assert.equal(output.tag, "+");
|
|
400
|
+
let bits = output.value;
|
|
401
|
+
for (let i = 0; i < depth; i++) {
|
|
402
|
+
assert.equal(bits.tag, "1");
|
|
403
|
+
bits = bits.value;
|
|
404
|
+
}
|
|
405
|
+
assert.equal(bits.tag, "_");
|
|
406
|
+
assert.deepEqual(bits.value.product, {});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
assertDeepValue(readArenaValue({ memory }, ptr, pattern, 0, propertyList, new Map(), tags));
|
|
410
|
+
|
|
411
|
+
let input = Value.variant("_", Value.product({}));
|
|
412
|
+
for (let i = 0; i < depth; i++) {
|
|
413
|
+
input = Value.variant("1", input);
|
|
414
|
+
}
|
|
415
|
+
input = Value.variant("+", input);
|
|
416
|
+
|
|
417
|
+
free = 1024;
|
|
418
|
+
const exports = {
|
|
419
|
+
memory,
|
|
420
|
+
alloc(size) {
|
|
421
|
+
const ptr = free;
|
|
422
|
+
free += (size + 7) & -8;
|
|
423
|
+
return ptr;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
const arenaValues = new Map();
|
|
427
|
+
ptr = writeValueToArena(exports, input, pattern, 0, arenaValues, tags);
|
|
428
|
+
const output = readArenaValue(exports, ptr, pattern, 0, propertyList, arenaValues, tags);
|
|
429
|
+
assertDeepValue(output);
|
|
430
|
+
console.log("Stack-safe arena serialization tests passed successfully!");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
console.log("==> Running End-to-End Peano Addition & Serialization Tests");
|
|
434
|
+
{
|
|
435
|
+
const runtimeWat = fs.readFileSync("runtime.wat", "utf8");
|
|
436
|
+
|
|
437
|
+
const script = `
|
|
438
|
+
$ nat = < {} 0, nat +1 >;
|
|
439
|
+
0 = {} | 0 $ nat;
|
|
440
|
+
inc = | +1 $ nat;
|
|
441
|
+
dec = $ nat / +1;
|
|
442
|
+
add = $ { nat x, nat y } <
|
|
443
|
+
{ . x dec x, . y inc y } add,
|
|
444
|
+
. y
|
|
445
|
+
>;
|
|
446
|
+
()
|
|
447
|
+
`;
|
|
448
|
+
|
|
449
|
+
const defs = annotate(script, { convergence: { strategy: "auto" } });
|
|
450
|
+
|
|
451
|
+
// Compile relations to WAT
|
|
452
|
+
const incWat = lowerToWasm(defs.rels.inc, "inc");
|
|
453
|
+
const decWat = lowerToWasm(defs.rels.dec, "dec");
|
|
454
|
+
const addWat = lowerToWasm(defs.rels.add, "add");
|
|
455
|
+
assert.match(addWat, /\bbr \$tail_loop\b/, "Recursive add should be lowered as a tail jump");
|
|
456
|
+
assert.doesNotMatch(addWat, /\bcall \$add\b/, "Recursive add should not emit a Wasm self-call");
|
|
457
|
+
|
|
458
|
+
// Combine and compile
|
|
459
|
+
const fullWat = runtimeWat.trim().slice(0, -1) + "\n" +
|
|
460
|
+
incWat + "\n" +
|
|
461
|
+
decWat + "\n" +
|
|
462
|
+
addWat + "\n)";
|
|
463
|
+
const binary = compileWat("add_tests.wat", fullWat);
|
|
464
|
+
const exports = await instantiateWasm(binary);
|
|
465
|
+
|
|
466
|
+
// JS reader function for Wasm Execution Arena
|
|
467
|
+
function readArenaValue(ptr, pattern, patternNodeId) {
|
|
468
|
+
const patternNode = pattern.nodes[patternNodeId];
|
|
469
|
+
const view = new DataView(exports.memory.buffer);
|
|
470
|
+
|
|
471
|
+
if (patternNode.kind === 1 || patternNode.kind === 3) { // OPEN_PRODUCT or CLOSED_PRODUCT
|
|
472
|
+
const size = view.getUint32(ptr, true);
|
|
473
|
+
const N = view.getUint32(ptr + 4, true);
|
|
474
|
+
const productObj = {};
|
|
475
|
+
|
|
476
|
+
for (let i = 0; i < N; i++) {
|
|
477
|
+
const edge = patternNode.edges[i];
|
|
478
|
+
const offsetVal = view.getUint32(ptr + 8 + 4 * i, true);
|
|
479
|
+
const childPtr = view.getUint32(ptr + offsetVal, true);
|
|
480
|
+
productObj[edge.label] = readArenaValue(childPtr, pattern, edge.target);
|
|
481
|
+
}
|
|
482
|
+
return Value.product(productObj, pattern);
|
|
483
|
+
} else if (patternNode.kind === 2 || patternNode.kind === 4) { // OPEN_UNION or CLOSED_UNION
|
|
484
|
+
const size = view.getUint32(ptr, true);
|
|
485
|
+
const tagId = view.getUint32(ptr + 4, true);
|
|
486
|
+
const payloadPtr = view.getUint32(ptr + 8, true);
|
|
487
|
+
|
|
488
|
+
const tag = getTagFromId(tagId);
|
|
489
|
+
const edge = patternNode.edges.find(e => e.label === tag);
|
|
490
|
+
if (!edge) {
|
|
491
|
+
throw new Error(`Variant tag '${tag}' not found in pattern edges`);
|
|
492
|
+
}
|
|
493
|
+
const payloadVal = readArenaValue(payloadPtr, pattern, edge.target);
|
|
494
|
+
return Value.variant(tag, payloadVal, pattern);
|
|
495
|
+
}
|
|
496
|
+
throw new Error(`Unsupported pattern kind: ${patternNode.kind}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// JS writer function for Wasm Execution Arena
|
|
500
|
+
function writeValueToArena(value, pattern, patternNodeId) {
|
|
501
|
+
const patternNode = pattern.nodes[patternNodeId];
|
|
502
|
+
|
|
503
|
+
if (isProduct(value)) {
|
|
504
|
+
const keys = Object.keys(value.product).sort();
|
|
505
|
+
const N = keys.length;
|
|
506
|
+
const totalSize = 8 + 8 * N;
|
|
507
|
+
|
|
508
|
+
// Evaluate and allocate all children first
|
|
509
|
+
const childPtrs = [];
|
|
510
|
+
for (let i = 0; i < N; i++) {
|
|
511
|
+
const label = keys[i];
|
|
512
|
+
const edge = patternNode.edges.find(e => e.label === label);
|
|
513
|
+
const childPtr = writeValueToArena(value.product[label], pattern, edge.target);
|
|
514
|
+
childPtrs.push(childPtr);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const ptr = exports.alloc(totalSize);
|
|
518
|
+
const view = new DataView(exports.memory.buffer);
|
|
519
|
+
|
|
520
|
+
view.setUint32(ptr, totalSize, true);
|
|
521
|
+
view.setUint32(ptr + 4, N, true);
|
|
522
|
+
|
|
523
|
+
for (let i = 0; i < N; i++) {
|
|
524
|
+
const offsetVal = 8 + 4 * N + 4 * i;
|
|
525
|
+
view.setUint32(ptr + 8 + 4 * i, offsetVal, true);
|
|
526
|
+
view.setUint32(ptr + offsetVal, childPtrs[i], true);
|
|
527
|
+
}
|
|
528
|
+
return ptr;
|
|
529
|
+
} else if (isVariant(value)) {
|
|
530
|
+
const tagId = getTagId(value.tag);
|
|
531
|
+
const edge = patternNode.edges.find(e => e.label === value.tag);
|
|
532
|
+
const childPtr = writeValueToArena(value.value, pattern, edge.target);
|
|
533
|
+
|
|
534
|
+
const ptr = exports.alloc(12);
|
|
535
|
+
const view = new DataView(exports.memory.buffer);
|
|
536
|
+
|
|
537
|
+
view.setUint32(ptr, 12, true);
|
|
538
|
+
view.setUint32(ptr + 4, tagId, true);
|
|
539
|
+
view.setUint32(ptr + 8, childPtr, true);
|
|
540
|
+
return ptr;
|
|
541
|
+
}
|
|
542
|
+
throw new Error(`Unsupported value type: ${value}`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function writeUnitProductToArena() {
|
|
546
|
+
const ptr = exports.alloc(8);
|
|
547
|
+
const view = new DataView(exports.memory.buffer);
|
|
548
|
+
view.setUint32(ptr, 8, true);
|
|
549
|
+
view.setUint32(ptr + 4, 0, true);
|
|
550
|
+
return ptr;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function writeVariantPtrToArena(tag, payloadPtr) {
|
|
554
|
+
const ptr = exports.alloc(12);
|
|
555
|
+
const view = new DataView(exports.memory.buffer);
|
|
556
|
+
view.setUint32(ptr, 12, true);
|
|
557
|
+
view.setUint32(ptr + 4, getTagId(tag), true);
|
|
558
|
+
view.setUint32(ptr + 8, payloadPtr, true);
|
|
559
|
+
return ptr;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function writeNatPtrToArena(n) {
|
|
563
|
+
let ptr = writeVariantPtrToArena("0", writeUnitProductToArena());
|
|
564
|
+
for (let i = 0; i < n; i++) {
|
|
565
|
+
ptr = writeVariantPtrToArena("+1", ptr);
|
|
566
|
+
}
|
|
567
|
+
return ptr;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function writeAddInputPtrToArena(xPtr, yPtr) {
|
|
571
|
+
const ptr = exports.alloc(24);
|
|
572
|
+
const view = new DataView(exports.memory.buffer);
|
|
573
|
+
view.setUint32(ptr, 24, true);
|
|
574
|
+
view.setUint32(ptr + 4, 2, true);
|
|
575
|
+
view.setUint32(ptr + 8, 16, true);
|
|
576
|
+
view.setUint32(ptr + 12, 20, true);
|
|
577
|
+
view.setUint32(ptr + 16, xPtr, true);
|
|
578
|
+
view.setUint32(ptr + 20, yPtr, true);
|
|
579
|
+
return ptr;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Helper to extract property list pattern for a pattern ID in graph
|
|
583
|
+
function getPattern(graph, patternId) {
|
|
584
|
+
const nodeId = graph.find(patternId);
|
|
585
|
+
return propertyListToPattern(patternToPropertyList(exportPatternGraph(graph, nodeId)));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Prepare input values: 2 + 1 = 3
|
|
589
|
+
const inputData = {
|
|
590
|
+
x: { "+1": { "+1": { "0": {} } } }, // 2
|
|
591
|
+
y: { "+1": { "0": {} } } // 1
|
|
592
|
+
};
|
|
593
|
+
const valInput = fromObject(inputData);
|
|
594
|
+
|
|
595
|
+
const graph = defs.rels.add.typePatternGraph;
|
|
596
|
+
const inputPattern = getPattern(graph, defs.rels.add.def.patterns[0]);
|
|
597
|
+
const outputPattern = getPattern(graph, defs.rels.add.def.patterns[1]);
|
|
598
|
+
|
|
599
|
+
const ptrIn = writeValueToArena(valInput, inputPattern, 0);
|
|
600
|
+
|
|
601
|
+
// Call compiled recursive Peano addition
|
|
602
|
+
const res = exports.add(ptrIn);
|
|
603
|
+
assert.equal(res[1], 1, "Peano addition must succeed");
|
|
604
|
+
|
|
605
|
+
const valOutput = readArenaValue(res[0], outputPattern, 0);
|
|
606
|
+
|
|
607
|
+
// Assert equivalence with reference math
|
|
608
|
+
assert.deepEqual(valOutput.toJSON(), { "+1": { "+1": { "+1": "0" } } }, "Addition of 2 and 1 must equal 3");
|
|
609
|
+
|
|
610
|
+
const deepInputPtr = writeAddInputPtrToArena(writeNatPtrToArena(5000), writeNatPtrToArena(0));
|
|
611
|
+
const deepRes = exports.add(deepInputPtr);
|
|
612
|
+
assert.equal(deepRes[1], 1, "Tail-recursive Peano addition must handle deep inputs without overflowing the Wasm call stack");
|
|
613
|
+
|
|
614
|
+
console.log("End-to-End Peano Addition & Serialization tests passed successfully!");
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
console.log("==> Harness ready.");
|