@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/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 singletonPatternToCodeHash(pattern) {
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 singleton pattern node ${i}`);
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 singleton pattern edge target ${target}`);
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 = singletonPatternToCodeHash(pattern);
71
- if (!codeHash) throw new Error(`Codec '${name}' pattern ${index} is not singleton`);
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
- if (!isCanonicalCodeName(codeHash)) {
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 ? (store[codeHash] || []) : Object.values(store).flat();
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 matches = (codecStore(state)[codeHash] || []).filter((codec) =>
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
- singletonPatternToCodeHash,
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
- singletonPatternToCodeHash,
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 = singletonPatternToCodeHash(value.pattern);
328
+ const codeHash = closedPatternToCodeHash(value.pattern);
327
329
  if (!codeHash) return lines[0];
328
330
 
329
- for (const codec of state.codecs?.[codeHash] || []) {
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 }) => `loaded codec ${name} for ${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 parts = input.trim().split(/\s+/).filter(Boolean);
915
- if (parts.length === 0 || parts.length > 2) {
916
- throw new Error(":input requires a singleton type and optional codec name");
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
- const codeHash = resolveTypeHash(state, parts[0]);
920
- const codecName = parts[1] || null;
921
- resolveCodec(state, codeHash, codecName, "parse");
922
- state.pendingInput = { codeHash, codecName };
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
+ });
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env sh
2
+ set -eu
3
+
4
+ if [ "${SKIP_BENCHMARK:-}" = "1" ]; then
5
+ echo "Skipping benchmark because SKIP_BENCHMARK=1"
6
+ exit 0
7
+ fi
8
+
9
+ npm run benchmark
10
+ git add benchmark-results.json