@fraczak/k 6.3.1 → 6.3.3
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/REPL.md +15 -10
- package/a.klib +1 -1
- package/a.kvm +142676 -0
- package/a.wasm +0 -0
- package/benchmark-results.html +532 -0
- package/benchmark-results.json +480 -0
- package/codecs/json.mjs +16 -2
- package/kvm.mjs +78 -11
- package/package.json +4 -1
- package/repl-codecs.mjs +28 -14
- package/repl.mjs +74 -11
- package/scripts/benchmark.mjs +393 -0
- package/scripts/git-hooks/pre-commit +10 -0
- package/scripts/render-benchmark.mjs +343 -0
- package/scripts/run-tests.mjs +1 -0
- package/tests/integration.sh +7 -0
- package/tests/test-arithmetics.mjs +95 -0
- package/tests/test-ieee-arithmetic.mjs +2 -2
- package/tests/test-repl.mjs +32 -1
package/repl-codecs.mjs
CHANGED
|
@@ -7,11 +7,13 @@ import { deriveClosedPattern } from "./codecs/runtime/codec.mjs";
|
|
|
7
7
|
import { patternToPropertyList } from "./codecs/runtime/pattern-json.mjs";
|
|
8
8
|
import { finalize } from "./codes.mjs";
|
|
9
9
|
|
|
10
|
+
const UNIVERSAL_CODE = "*";
|
|
11
|
+
|
|
10
12
|
function isCanonicalCodeName(name) {
|
|
11
13
|
return typeof name === "string" && name.startsWith("@");
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
function
|
|
16
|
+
function closedPatternToCodeHash(pattern) {
|
|
15
17
|
if (!Array.isArray(pattern) || pattern.length === 0) {
|
|
16
18
|
return null;
|
|
17
19
|
}
|
|
@@ -23,7 +25,7 @@ function singletonPatternToCodeHash(pattern) {
|
|
|
23
25
|
return null;
|
|
24
26
|
}
|
|
25
27
|
if (!Array.isArray(edges)) {
|
|
26
|
-
throw new Error(`Invalid
|
|
28
|
+
throw new Error(`Invalid closed pattern node ${i}`);
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
const code = kind === "closed-product" ? "product" : "union";
|
|
@@ -31,7 +33,7 @@ function singletonPatternToCodeHash(pattern) {
|
|
|
31
33
|
code,
|
|
32
34
|
[code]: Object.fromEntries(edges.map(([label, target]) => {
|
|
33
35
|
if (!Number.isInteger(target) || target < 0 || target >= pattern.length) {
|
|
34
|
-
throw new Error(`Invalid
|
|
36
|
+
throw new Error(`Invalid closed pattern edge target ${target}`);
|
|
35
37
|
}
|
|
36
38
|
return [label, `C${target}`];
|
|
37
39
|
}))
|
|
@@ -64,17 +66,18 @@ function valueForCode(value, codeHash, findCode) {
|
|
|
64
66
|
function normalizeCodecModule(mod, fallbackName) {
|
|
65
67
|
const codec = mod.replCodec || mod.codec || mod.default || mod;
|
|
66
68
|
const name = codec.name || mod.name || fallbackName;
|
|
69
|
+
const universal = codec.universal === true;
|
|
67
70
|
const patterns = codec.patterns || (codec.pattern ? [codec.pattern] : null);
|
|
68
71
|
const codes = codec.codes || codec.codeHashes || (codec.code ? [codec.code] : null) ||
|
|
69
72
|
patterns?.map((pattern, index) => {
|
|
70
|
-
const codeHash =
|
|
71
|
-
if (!codeHash) throw new Error(`Codec '${name}' pattern ${index}
|
|
73
|
+
const codeHash = closedPatternToCodeHash(pattern);
|
|
74
|
+
if (!codeHash) throw new Error(`Codec '${name}' pattern ${index} must be a closed property-list pattern`);
|
|
72
75
|
return codeHash;
|
|
73
76
|
});
|
|
74
77
|
if (!name || typeof name !== "string") {
|
|
75
78
|
throw new Error("Codec module must export a string name");
|
|
76
79
|
}
|
|
77
|
-
if (!Array.isArray(codes) || codes.length === 0) {
|
|
80
|
+
if (!universal && (!Array.isArray(codes) || codes.length === 0)) {
|
|
78
81
|
throw new Error(`Codec '${name}' must export a non-empty codes array`);
|
|
79
82
|
}
|
|
80
83
|
if (codec.parse != null && typeof codec.parse !== "function") {
|
|
@@ -85,7 +88,8 @@ function normalizeCodecModule(mod, fallbackName) {
|
|
|
85
88
|
}
|
|
86
89
|
return {
|
|
87
90
|
name,
|
|
88
|
-
codes,
|
|
91
|
+
codes: universal ? [UNIVERSAL_CODE] : codes,
|
|
92
|
+
universal,
|
|
89
93
|
parse: codec.parse,
|
|
90
94
|
print: codec.print
|
|
91
95
|
};
|
|
@@ -100,13 +104,15 @@ function registerCodec(state, codec, source = null) {
|
|
|
100
104
|
const registered = [];
|
|
101
105
|
const store = codecStore(state);
|
|
102
106
|
for (const codeHash of codec.codes) {
|
|
103
|
-
|
|
107
|
+
const universal = codeHash === UNIVERSAL_CODE;
|
|
108
|
+
if (!universal && !isCanonicalCodeName(codeHash)) {
|
|
104
109
|
throw new Error(`Codec '${codec.name}' has non-canonical code '${codeHash}'`);
|
|
105
110
|
}
|
|
106
111
|
if (!store[codeHash]) store[codeHash] = [];
|
|
107
112
|
const entry = {
|
|
108
113
|
name: codec.name,
|
|
109
114
|
source,
|
|
115
|
+
universal,
|
|
110
116
|
parse: codec.parse,
|
|
111
117
|
print: codec.print
|
|
112
118
|
};
|
|
@@ -137,20 +143,27 @@ function listCodecs(state) {
|
|
|
137
143
|
.flatMap(([codeHash, codecs]) => codecs.map((codec) => ({ codeHash, ...codec })))
|
|
138
144
|
.sort((a, b) => a.name.localeCompare(b.name) || a.codeHash.localeCompare(b.codeHash));
|
|
139
145
|
if (entries.length === 0) return "(none)";
|
|
140
|
-
return entries.map((entry) => `${entry.name} ${entry.codeHash}${entry.source ? ` (${entry.source})` : ""}`).join("\n");
|
|
146
|
+
return entries.map((entry) => `${entry.name} ${entry.codeHash === UNIVERSAL_CODE ? "all" : entry.codeHash}${entry.source ? ` (${entry.source})` : ""}`).join("\n");
|
|
141
147
|
}
|
|
142
148
|
|
|
143
149
|
function codecNames(state, codeHash = null) {
|
|
144
150
|
const store = codecStore(state);
|
|
145
|
-
const codecs = codeHash
|
|
151
|
+
const codecs = codeHash
|
|
152
|
+
? [...(store[codeHash] || []), ...(store[UNIVERSAL_CODE] || [])]
|
|
153
|
+
: Object.values(store).flat();
|
|
146
154
|
return [...new Set(codecs.map((codec) => codec.name))].sort();
|
|
147
155
|
}
|
|
148
156
|
|
|
149
157
|
function resolveCodec(state, codeHash, codecName = null, capability = null) {
|
|
150
|
-
const
|
|
158
|
+
const store = codecStore(state);
|
|
159
|
+
const matchesCodec = (codec) =>
|
|
151
160
|
(codecName == null || codec.name === codecName) &&
|
|
152
|
-
(capability == null || typeof codec[capability] === "function")
|
|
153
|
-
);
|
|
161
|
+
(capability == null || typeof codec[capability] === "function");
|
|
162
|
+
const exactMatches = (store[codeHash] || []).filter(matchesCodec);
|
|
163
|
+
const universalMatches = (store[UNIVERSAL_CODE] || []).filter(matchesCodec);
|
|
164
|
+
const matches = codecName == null && exactMatches.length > 0
|
|
165
|
+
? exactMatches
|
|
166
|
+
: [...exactMatches, ...universalMatches];
|
|
154
167
|
if (matches.length === 0) {
|
|
155
168
|
const suffix = codecName ? ` named '${codecName}'` : "";
|
|
156
169
|
throw new Error(`No ${capability || "usable"} codec${suffix} for ${codeHash}`);
|
|
@@ -168,6 +181,7 @@ export {
|
|
|
168
181
|
loadCodecModule,
|
|
169
182
|
registerCodec,
|
|
170
183
|
resolveCodec,
|
|
171
|
-
|
|
184
|
+
closedPatternToCodeHash,
|
|
185
|
+
UNIVERSAL_CODE,
|
|
172
186
|
valueForCode
|
|
173
187
|
};
|
package/repl.mjs
CHANGED
|
@@ -29,7 +29,8 @@ import {
|
|
|
29
29
|
listCodecs,
|
|
30
30
|
loadCodecModule,
|
|
31
31
|
resolveCodec,
|
|
32
|
-
|
|
32
|
+
closedPatternToCodeHash,
|
|
33
|
+
UNIVERSAL_CODE,
|
|
33
34
|
valueForCode
|
|
34
35
|
} from "./repl-codecs.mjs";
|
|
35
36
|
|
|
@@ -43,6 +44,7 @@ const COMMAND_NAMES = [
|
|
|
43
44
|
];
|
|
44
45
|
const CODEC_COMMAND_NAMES = ["load", "list"];
|
|
45
46
|
const PATH_COMMANDS = new Set(["klib", "ko", "load"]);
|
|
47
|
+
const INPUT_TYPE_NAME = "__input__";
|
|
46
48
|
const initialCodes = codes.dump();
|
|
47
49
|
|
|
48
50
|
function cloneJSON(value) {
|
|
@@ -323,14 +325,19 @@ function printValue(value, state = null) {
|
|
|
323
325
|
const lines = [valueWithEnvelopeToK(value)];
|
|
324
326
|
if (!state || value === undefined) return lines[0];
|
|
325
327
|
|
|
326
|
-
const codeHash =
|
|
328
|
+
const codeHash = closedPatternToCodeHash(value.pattern);
|
|
327
329
|
if (!codeHash) return lines[0];
|
|
328
330
|
|
|
329
|
-
|
|
331
|
+
const codecs = [
|
|
332
|
+
...(state.codecs?.[codeHash] || []),
|
|
333
|
+
...(state.codecs?.[UNIVERSAL_CODE] || [])
|
|
334
|
+
];
|
|
335
|
+
for (const codec of codecs) {
|
|
330
336
|
if (typeof codec.print !== "function") continue;
|
|
331
337
|
try {
|
|
332
338
|
lines.push(`${codec.name}: ${formatCodecOutput(codec.print(value, { codeHash, state }))}`);
|
|
333
339
|
} catch (error) {
|
|
340
|
+
if (codec.universal) continue;
|
|
334
341
|
lines.push(`${codec.name}: <error: ${error.message || String(error)}>`);
|
|
335
342
|
}
|
|
336
343
|
}
|
|
@@ -609,6 +616,40 @@ function resolveTypeHash(state, rawName) {
|
|
|
609
616
|
return hash;
|
|
610
617
|
}
|
|
611
618
|
|
|
619
|
+
function isSimpleTypeReference(rawType) {
|
|
620
|
+
return /^\$?[a-zA-Z0-9_+-][a-zA-Z0-9_?!+-]*$/.test(rawType.trim());
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function resolveTypeExpressionHash(state, rawType) {
|
|
624
|
+
restoreCodes(state);
|
|
625
|
+
const preamble = aliasPreamble(state, INPUT_TYPE_NAME);
|
|
626
|
+
const source = [
|
|
627
|
+
preamble,
|
|
628
|
+
ensureSemicolon(`$ ${INPUT_TYPE_NAME} = ${rawType.trim()}`),
|
|
629
|
+
"()"
|
|
630
|
+
].filter(Boolean).join("\n");
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
const annotated = annotate(source, { libraries: [stateLibrary(state)] });
|
|
634
|
+
const hash = annotated.representatives[INPUT_TYPE_NAME];
|
|
635
|
+
if (!hash) throw new Error(":input type expression did not produce a code hash");
|
|
636
|
+
state.codes = codes.dump();
|
|
637
|
+
return hash;
|
|
638
|
+
} catch (error) {
|
|
639
|
+
restoreCodes(state);
|
|
640
|
+
throw remapError(error, preambleLineCount(preamble));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function resolveInputTypeHash(state, rawType) {
|
|
645
|
+
try {
|
|
646
|
+
return resolveTypeHash(state, rawType);
|
|
647
|
+
} catch (error) {
|
|
648
|
+
if (isSimpleTypeReference(rawType)) throw error;
|
|
649
|
+
}
|
|
650
|
+
return resolveTypeExpressionHash(state, rawType);
|
|
651
|
+
}
|
|
652
|
+
|
|
612
653
|
function completeInputCommand(line, state, argStart, arg) {
|
|
613
654
|
const match = arg.match(/^(\s*)(\S+)?(\s+)?(\S*)?$/);
|
|
614
655
|
if (!match) return [[], line];
|
|
@@ -900,7 +941,9 @@ async function loadCodec(input, state) {
|
|
|
900
941
|
const filePath = rest.join(" ");
|
|
901
942
|
if (!filePath) throw new Error(":codec load requires a file path");
|
|
902
943
|
const registered = await loadCodecModule(state, expandHome(filePath));
|
|
903
|
-
return registered.map(({ name, codeHash }) =>
|
|
944
|
+
return registered.map(({ name, codeHash }) =>
|
|
945
|
+
`loaded codec ${name} for ${codeHash === UNIVERSAL_CODE ? "all types" : codeHash}`
|
|
946
|
+
);
|
|
904
947
|
}
|
|
905
948
|
case "list":
|
|
906
949
|
if (rest.length > 0) throw new Error(":codec list does not accept arguments");
|
|
@@ -911,15 +954,28 @@ async function loadCodec(input, state) {
|
|
|
911
954
|
}
|
|
912
955
|
|
|
913
956
|
async function requestCodecInput(input, state) {
|
|
914
|
-
const
|
|
915
|
-
if (
|
|
916
|
-
throw new Error(":input requires a
|
|
957
|
+
const trimmed = input.trim();
|
|
958
|
+
if (!trimmed) {
|
|
959
|
+
throw new Error(":input requires a type and optional codec name");
|
|
917
960
|
}
|
|
918
961
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
962
|
+
let codeHash;
|
|
963
|
+
let codecName = null;
|
|
964
|
+
try {
|
|
965
|
+
codeHash = resolveInputTypeHash(state, trimmed);
|
|
966
|
+
} catch (wholeError) {
|
|
967
|
+
const split = trimmed.match(/^(.*\S)\s+([a-zA-Z0-9_+-][a-zA-Z0-9_?!+-]*)$/);
|
|
968
|
+
if (!split) throw wholeError;
|
|
969
|
+
try {
|
|
970
|
+
codeHash = resolveInputTypeHash(state, split[1]);
|
|
971
|
+
codecName = split[2];
|
|
972
|
+
} catch (typeError) {
|
|
973
|
+
throw typeError;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const codec = resolveCodec(state, codeHash, codecName, "parse");
|
|
978
|
+
state.pendingInput = { codeHash, codecName, promptName: codec.name };
|
|
923
979
|
return [`input ${codeHash}${codecName ? ` using ${codecName}` : ""}: enter value text`];
|
|
924
980
|
}
|
|
925
981
|
|
|
@@ -1097,6 +1153,11 @@ function helpText() {
|
|
|
1097
1153
|
].join("\n");
|
|
1098
1154
|
}
|
|
1099
1155
|
|
|
1156
|
+
function promptForState(state) {
|
|
1157
|
+
const promptName = state.pendingInput?.promptName;
|
|
1158
|
+
return promptName ? `${promptName}> ` : "> ";
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1100
1161
|
function cliUsage() {
|
|
1101
1162
|
console.log("Usage: k-repl");
|
|
1102
1163
|
console.log(" k-repl -h");
|
|
@@ -1151,6 +1212,7 @@ function startRepl() {
|
|
|
1151
1212
|
} catch (error) {
|
|
1152
1213
|
console.error(error.message || String(error));
|
|
1153
1214
|
}
|
|
1215
|
+
rl.setPrompt(promptForState(state));
|
|
1154
1216
|
if (!closed) rl.prompt();
|
|
1155
1217
|
return;
|
|
1156
1218
|
}
|
|
@@ -1223,6 +1285,7 @@ export {
|
|
|
1223
1285
|
lineHasExplicitContinuation,
|
|
1224
1286
|
lineTerminatesSnippet,
|
|
1225
1287
|
printValue,
|
|
1288
|
+
promptForState,
|
|
1226
1289
|
propertyListToFilter,
|
|
1227
1290
|
valueToK
|
|
1228
1291
|
};
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { performance } from "node:perf_hooks";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
codes,
|
|
13
|
+
createState,
|
|
14
|
+
evaluateInput,
|
|
15
|
+
executeKVM,
|
|
16
|
+
lowerToKVM,
|
|
17
|
+
parseFloat64,
|
|
18
|
+
run,
|
|
19
|
+
run_converged,
|
|
20
|
+
valueForCode,
|
|
21
|
+
Value
|
|
22
|
+
} from "../backend-api.mjs";
|
|
23
|
+
import { parse as parseIntValue } from "../codecs/int.mjs";
|
|
24
|
+
|
|
25
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
26
|
+
const root = path.resolve(path.dirname(scriptPath), "..");
|
|
27
|
+
const resultsPath = path.join(root, "benchmark-results.json");
|
|
28
|
+
|
|
29
|
+
const bigA = "1234567890".repeat(22);
|
|
30
|
+
const bigB = "9876543210".repeat(22);
|
|
31
|
+
const gcdFactor = 1234567890123456789012345678901234567890n;
|
|
32
|
+
const gcdA = (gcdFactor * 123456789012345678901234567890123456789012345678901n).toString();
|
|
33
|
+
const gcdB = (gcdFactor * 987654321098765432109876543210987654321098765432109n).toString();
|
|
34
|
+
|
|
35
|
+
const cases = [
|
|
36
|
+
{ name: "ieee.add", suite: "ieee", rel: "add", input: { x: "0.5", y: "0.25" } },
|
|
37
|
+
{ name: "ieee.sub", suite: "ieee", rel: "sub", input: { x: "2", y: "4" } },
|
|
38
|
+
{ name: "ieee.mul", suite: "ieee", rel: "mul", input: { x: "-2", y: "4" } },
|
|
39
|
+
{ name: "ieee.div", suite: "ieee", rel: "div", input: { x: "1", y: "2" } },
|
|
40
|
+
{ name: "int.times.220d", suite: "arithmetics", rel: "times", input: { shape: "int-pair", x: bigA, y: bigB } },
|
|
41
|
+
{ name: "int.gcd.90d", suite: "arithmetics", rel: "gcd", input: { shape: "bits-pair", x: gcdA, y: gcdB } },
|
|
42
|
+
{ name: "int.factorial.101", suite: "arithmetics", rel: "factorial", input: { shape: "int", x: "101" } },
|
|
43
|
+
{ name: "int.factorial.242", suite: "arithmetics", rel: "factorial", input: { shape: "int", x: "242" } },
|
|
44
|
+
{ name: "int.factorial.1174", suite: "arithmetics", rel: "factorial", input: { shape: "int", x: "1174" } }
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const modes = [
|
|
48
|
+
{ name: "Native JS (Envelope-Aware)", kind: "native-aware" },
|
|
49
|
+
{ name: "Native JS (Envelope-Free)", kind: "native-free" },
|
|
50
|
+
{ name: "kVM Interpreter (Env-Free)", kind: "kvm-free" }
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function csvEnv(name, fallback) {
|
|
54
|
+
return (process.env[name] || fallback)
|
|
55
|
+
.split(",")
|
|
56
|
+
.map(item => item.trim())
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function selectedCases() {
|
|
61
|
+
const selected = csvEnv("BENCHMARK_CASES", cases.map(testCase => testCase.name).join(","));
|
|
62
|
+
const byName = new Map(cases.map(testCase => [testCase.name, testCase]));
|
|
63
|
+
return selected.map((name) => {
|
|
64
|
+
const testCase = byName.get(name);
|
|
65
|
+
if (!testCase) throw new Error(`Unknown benchmark case '${name}'`);
|
|
66
|
+
return testCase;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function selectedModes() {
|
|
71
|
+
const selected = csvEnv("BENCHMARK_MODES", modes.map(mode => mode.name).join(","));
|
|
72
|
+
const byName = new Map(modes.map(mode => [mode.name, mode]));
|
|
73
|
+
return selected.map((name) => {
|
|
74
|
+
const mode = byName.get(name);
|
|
75
|
+
if (!mode) throw new Error(`Unknown benchmark mode '${name}'`);
|
|
76
|
+
return mode;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function mean(values) {
|
|
81
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function maxRssMiB(maxRSS) {
|
|
85
|
+
return process.platform === "darwin" ? maxRSS / 1024 / 1024 : maxRSS / 1024;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function digestValue(value) {
|
|
89
|
+
const json = JSON.stringify(value);
|
|
90
|
+
return {
|
|
91
|
+
digest: crypto.createHash("sha256").update(json).digest("hex").slice(0, 16),
|
|
92
|
+
bytes: Buffer.byteLength(json)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatMs(value) {
|
|
97
|
+
return value == null ? "-" : value.toFixed(2);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatMiB(value) {
|
|
101
|
+
return value == null ? "-" : value.toFixed(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function pad(value, width) {
|
|
105
|
+
const text = String(value);
|
|
106
|
+
return text.length >= width ? text : text + " ".repeat(width - text.length);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function prepareState(suite) {
|
|
110
|
+
const state = createState();
|
|
111
|
+
const source = suite === "ieee" ? "Examples/ieee.k" : "Examples/arithmetics.k";
|
|
112
|
+
await evaluateInput(`:load ${path.join(root, source)}`, state);
|
|
113
|
+
return state;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function ieeeInput(testCase, state) {
|
|
117
|
+
const float64Hash = state.typeAliases.float64;
|
|
118
|
+
const floatPairHash = state.typeAliases.float_pair;
|
|
119
|
+
const float64 = text => valueForCode(parseFloat64(text), float64Hash, codes.find);
|
|
120
|
+
return valueForCode(Value.product({
|
|
121
|
+
x: float64(testCase.input.x),
|
|
122
|
+
y: float64(testCase.input.y)
|
|
123
|
+
}), floatPairHash, codes.find);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function arithmeticInput(testCase) {
|
|
127
|
+
const { shape } = testCase.input;
|
|
128
|
+
if (shape === "int") {
|
|
129
|
+
return parseIntValue(testCase.input.x);
|
|
130
|
+
}
|
|
131
|
+
if (shape === "int-pair") {
|
|
132
|
+
return Value.product({
|
|
133
|
+
x: parseIntValue(testCase.input.x),
|
|
134
|
+
y: parseIntValue(testCase.input.y)
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (shape === "bits-pair") {
|
|
138
|
+
return Value.product({
|
|
139
|
+
x: parseIntValue(testCase.input.x).value,
|
|
140
|
+
y: parseIntValue(testCase.input.y).value
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
throw new Error(`Unknown arithmetic input shape '${shape}'`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function prepareRelation(testCase, state) {
|
|
147
|
+
const hash = state.relAliases[testCase.rel];
|
|
148
|
+
if (!hash) throw new Error(`Relation '${testCase.rel}' not found`);
|
|
149
|
+
const relDef = state.rels[hash];
|
|
150
|
+
if (!relDef) throw new Error(`Relation hash '${hash}' not found`);
|
|
151
|
+
return {
|
|
152
|
+
relDef,
|
|
153
|
+
kvmFunc: lowerToKVM(relDef, testCase.rel)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function runOnce(job, prepared) {
|
|
158
|
+
const { relDef, kvmFunc, inputVal, state } = prepared;
|
|
159
|
+
run.defs = state;
|
|
160
|
+
run_converged.defs = state;
|
|
161
|
+
|
|
162
|
+
if (job.mode.kind === "native-aware") {
|
|
163
|
+
return run(codes.find, relDef.def, inputVal, relDef.typePatternGraph);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (job.mode.kind === "native-free") {
|
|
167
|
+
return run_converged(codes.find, relDef.def, inputVal, relDef.typePatternGraph);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (job.mode.kind === "kvm-free") {
|
|
171
|
+
return executeKVM(kvmFunc, inputVal, {
|
|
172
|
+
rels: state.rels,
|
|
173
|
+
findCode: codes.find,
|
|
174
|
+
options: { envelopeFree: true }
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
throw new Error(`Unknown benchmark mode '${job.mode.name}'`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function workerMain() {
|
|
182
|
+
const job = JSON.parse(process.env.K_BENCHMARK_JOB);
|
|
183
|
+
const setupStartedAt = performance.now();
|
|
184
|
+
const state = await prepareState(job.case.suite);
|
|
185
|
+
const relation = prepareRelation(job.case, state);
|
|
186
|
+
const inputVal = job.case.suite === "ieee" ? ieeeInput(job.case, state) : arithmeticInput(job.case);
|
|
187
|
+
const prepared = { ...relation, inputVal, state };
|
|
188
|
+
const setupMs = performance.now() - setupStartedAt;
|
|
189
|
+
|
|
190
|
+
const samples = [];
|
|
191
|
+
let lastResult;
|
|
192
|
+
const runStartedHeap = process.memoryUsage().heapUsed;
|
|
193
|
+
for (let i = 0; i < job.samples; i++) {
|
|
194
|
+
const startedAt = performance.now();
|
|
195
|
+
lastResult = runOnce(job, prepared);
|
|
196
|
+
samples.push(performance.now() - startedAt);
|
|
197
|
+
if (lastResult === undefined) {
|
|
198
|
+
throw new Error("benchmark operation returned undefined");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const resultInfo = digestValue(lastResult);
|
|
203
|
+
const memory = process.memoryUsage();
|
|
204
|
+
const usage = process.resourceUsage();
|
|
205
|
+
process.stdout.write(JSON.stringify({
|
|
206
|
+
status: "ok",
|
|
207
|
+
mode: job.mode.name,
|
|
208
|
+
case: job.case.name,
|
|
209
|
+
samples,
|
|
210
|
+
avgMs: mean(samples),
|
|
211
|
+
minMs: Math.min(...samples),
|
|
212
|
+
maxMs: Math.max(...samples),
|
|
213
|
+
setupMs,
|
|
214
|
+
maxRssMiB: maxRssMiB(usage.maxRSS),
|
|
215
|
+
heapUsedMiB: memory.heapUsed / 1024 / 1024,
|
|
216
|
+
heapDeltaMiB: (memory.heapUsed - runStartedHeap) / 1024 / 1024,
|
|
217
|
+
resultDigest: resultInfo.digest,
|
|
218
|
+
resultBytes: resultInfo.bytes
|
|
219
|
+
}) + "\n");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function spawnJob(job, timeoutMs) {
|
|
223
|
+
return new Promise((resolve) => {
|
|
224
|
+
const child = spawn(process.execPath, [scriptPath, "--worker"], {
|
|
225
|
+
cwd: root,
|
|
226
|
+
env: {
|
|
227
|
+
...process.env,
|
|
228
|
+
K_BENCHMARK_WORKER: "1",
|
|
229
|
+
K_BENCHMARK_JOB: JSON.stringify(job)
|
|
230
|
+
},
|
|
231
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
let stdout = "";
|
|
235
|
+
let stderr = "";
|
|
236
|
+
let timedOut = false;
|
|
237
|
+
const timer = setTimeout(() => {
|
|
238
|
+
timedOut = true;
|
|
239
|
+
child.kill("SIGTERM");
|
|
240
|
+
setTimeout(() => {
|
|
241
|
+
if (!child.killed) child.kill("SIGKILL");
|
|
242
|
+
}, 1000).unref();
|
|
243
|
+
}, timeoutMs);
|
|
244
|
+
|
|
245
|
+
child.stdout.on("data", chunk => { stdout += chunk.toString("utf8"); });
|
|
246
|
+
child.stderr.on("data", chunk => { stderr += chunk.toString("utf8"); });
|
|
247
|
+
child.on("close", (code, signal) => {
|
|
248
|
+
clearTimeout(timer);
|
|
249
|
+
if (timedOut) {
|
|
250
|
+
resolve({ status: "timeout", mode: job.mode.name, case: job.case.name, timeoutMs });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (code !== 0) {
|
|
254
|
+
resolve({
|
|
255
|
+
status: "failed",
|
|
256
|
+
mode: job.mode.name,
|
|
257
|
+
case: job.case.name,
|
|
258
|
+
code,
|
|
259
|
+
signal,
|
|
260
|
+
stderr: stderr.trim() || stdout.trim()
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
resolve(JSON.parse(stdout.trim().split("\n").at(-1)));
|
|
266
|
+
} catch (error) {
|
|
267
|
+
resolve({
|
|
268
|
+
status: "failed",
|
|
269
|
+
mode: job.mode.name,
|
|
270
|
+
case: job.case.name,
|
|
271
|
+
stderr: `Could not parse worker output: ${error.message}\n${stdout}\n${stderr}`
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function runPool(jobs, concurrency, timeoutMs) {
|
|
279
|
+
const results = [];
|
|
280
|
+
let next = 0;
|
|
281
|
+
|
|
282
|
+
async function worker() {
|
|
283
|
+
while (next < jobs.length) {
|
|
284
|
+
const job = jobs[next++];
|
|
285
|
+
const result = await spawnJob(job, timeoutMs);
|
|
286
|
+
results.push(result);
|
|
287
|
+
const label = `${result.mode}/${result.case}`;
|
|
288
|
+
if (result.status === "ok") {
|
|
289
|
+
console.log(`[ok] ${label} ${formatMs(result.avgMs)} ms avg, ${formatMiB(result.maxRssMiB)} MiB max RSS`);
|
|
290
|
+
} else {
|
|
291
|
+
console.log(`[${result.status}] ${label}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await Promise.all(Array.from({ length: concurrency }, worker));
|
|
297
|
+
return results;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function resultDocument(results, metadata) {
|
|
301
|
+
const sorted = [...results].sort((a, b) => a.case.localeCompare(b.case) || a.mode.localeCompare(b.mode));
|
|
302
|
+
return {
|
|
303
|
+
generatedAt: new Date().toISOString(),
|
|
304
|
+
node: process.version,
|
|
305
|
+
platform: `${process.platform} ${process.arch}`,
|
|
306
|
+
cpuCount: os.availableParallelism(),
|
|
307
|
+
samples: metadata.samples,
|
|
308
|
+
timeoutMs: metadata.timeoutMs,
|
|
309
|
+
parallelJobs: metadata.concurrency,
|
|
310
|
+
results: sorted
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function writeResultsFile(document) {
|
|
315
|
+
fs.writeFileSync(resultsPath, JSON.stringify(document, null, 2) + "\n");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function printSummary(document) {
|
|
319
|
+
console.log("\n==================== K BENCHMARK SUMMARY ====================");
|
|
320
|
+
console.log(`samples/job: ${document.samples}`);
|
|
321
|
+
console.log(`timeout/job: ${(document.timeoutMs / 1000 / 60).toFixed(1)} min`);
|
|
322
|
+
console.log(`parallel jobs: ${document.parallelJobs}`);
|
|
323
|
+
console.log(`results file: ${path.relative(root, resultsPath)}`);
|
|
324
|
+
console.log("-------------------------------------------------------------");
|
|
325
|
+
console.log(`${pad("case", 20)} ${pad("mode", 24)} ${pad("status", 8)} ${pad("avg ms", 10)} ${pad("min", 9)} ${pad("max", 9)} ${pad("rss MiB", 9)} ${pad("heap MiB", 9)} digest`);
|
|
326
|
+
for (const result of document.results) {
|
|
327
|
+
console.log([
|
|
328
|
+
pad(result.case, 20),
|
|
329
|
+
pad(result.mode, 24),
|
|
330
|
+
pad(result.status, 8),
|
|
331
|
+
pad(formatMs(result.avgMs), 10),
|
|
332
|
+
pad(formatMs(result.minMs), 9),
|
|
333
|
+
pad(formatMs(result.maxMs), 9),
|
|
334
|
+
pad(formatMiB(result.maxRssMiB), 9),
|
|
335
|
+
pad(formatMiB(result.heapUsedMiB), 9),
|
|
336
|
+
result.resultDigest || ""
|
|
337
|
+
].join(" "));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const failures = document.results.filter(result => result.status !== "ok");
|
|
341
|
+
if (failures.length > 0) {
|
|
342
|
+
console.log("-------------------------------------------------------------");
|
|
343
|
+
console.log("Failures:");
|
|
344
|
+
for (const failure of failures) {
|
|
345
|
+
console.log(`- ${failure.mode}/${failure.case}: ${failure.status}`);
|
|
346
|
+
if (failure.stderr) console.log(failure.stderr.split("\n").slice(0, 12).map(line => ` ${line}`).join("\n"));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
console.log("=============================================================\n");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function main() {
|
|
353
|
+
if (process.env.K_BENCHMARK_WORKER === "1" || process.argv.includes("--worker")) {
|
|
354
|
+
await workerMain();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const selected = selectedCases();
|
|
359
|
+
const selectedBenchmarkModes = selectedModes();
|
|
360
|
+
const samples = process.env.BENCHMARK_SAMPLES ? Number(process.env.BENCHMARK_SAMPLES) : 3;
|
|
361
|
+
const timeoutMs = process.env.BENCHMARK_TIMEOUT_MS ? Number(process.env.BENCHMARK_TIMEOUT_MS) : 2 * 60 * 1000;
|
|
362
|
+
const requestedJobs = process.env.BENCHMARK_JOBS ? Number(process.env.BENCHMARK_JOBS) : os.availableParallelism();
|
|
363
|
+
const concurrency = Math.max(1, Math.min(requestedJobs, os.availableParallelism(), selected.length * selectedBenchmarkModes.length));
|
|
364
|
+
|
|
365
|
+
if (!Number.isInteger(samples) || samples <= 0) throw new Error("BENCHMARK_SAMPLES must be a positive integer");
|
|
366
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) throw new Error("BENCHMARK_TIMEOUT_MS must be a positive integer");
|
|
367
|
+
|
|
368
|
+
const jobs = [];
|
|
369
|
+
for (const testCase of selected) {
|
|
370
|
+
for (const mode of selectedBenchmarkModes) {
|
|
371
|
+
jobs.push({ case: testCase, mode, samples });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
console.log("==> k benchmark");
|
|
376
|
+
console.log(`cases: ${selected.map(testCase => testCase.name).join(", ")}`);
|
|
377
|
+
console.log(`modes: ${selectedBenchmarkModes.map(mode => mode.name).join(", ")}`);
|
|
378
|
+
console.log(`jobs: ${jobs.length}; parallelism: ${concurrency}; timeout/job: ${timeoutMs} ms`);
|
|
379
|
+
|
|
380
|
+
const results = await runPool(jobs, concurrency, timeoutMs);
|
|
381
|
+
const document = resultDocument(results, { samples, timeoutMs, concurrency });
|
|
382
|
+
writeResultsFile(document);
|
|
383
|
+
printSummary(document);
|
|
384
|
+
|
|
385
|
+
if (process.env.BENCHMARK_STRICT === "1" && results.some(result => result.status !== "ok")) {
|
|
386
|
+
process.exitCode = 1;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
main().catch((error) => {
|
|
391
|
+
console.error(error.stack || error.message || String(error));
|
|
392
|
+
process.exit(1);
|
|
393
|
+
});
|