@fraczak/k 6.2.4 → 6.3.2
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/Value.mjs +50 -40
- package/a.klib +1 -1
- package/a.kvm +142676 -0
- package/a.wasm +0 -0
- package/backend-api.mjs +1 -1
- package/benchmark-results.html +532 -0
- package/benchmark-results.json +480 -0
- package/codecs/example-pipeline.mjs +7 -7
- package/codecs/ieee.mjs +8 -8
- package/codecs/int.mjs +6 -6
- package/codecs/json-codec.mjs +15 -15
- package/codecs/runtime/codec.mjs +13 -13
- package/codecs/runtime/prefix-codec.mjs +18 -18
- package/codecs/runtime/show-value.mjs +3 -3
- package/codecs/runtime/test-codec.mjs +7 -7
- package/codecs/runtime/typeFromValue.mjs +4 -4
- package/codecs/runtime/unicode-string.mjs +30 -30
- package/codecs/unit.mjs +4 -4
- package/index.mjs +2 -2
- package/kvm.mjs +86 -19
- package/package.json +4 -1
- package/repl.mjs +2 -2
- package/run.mjs +9 -9
- 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 +2 -0
- package/tests/code-derivation/codec-witness-pattern.mjs +4 -4
- package/tests/code-derivation/pattern-envelope.mjs +12 -12
- package/tests/integration.sh +7 -0
- package/tests/test-arithmetics.mjs +95 -0
- package/tests/test-deep-wire-value.mjs +4 -4
- package/tests/test-ieee-arithmetic.mjs +3 -3
- package/tests/test-k-object.mjs +10 -10
- package/tests/test-kvm.mjs +1 -1
- package/tests/test-repl.mjs +4 -4
- package/tests/test-structural-values.mjs +27 -0
- package/valueIO.mjs +1 -1
- package/valueParser.jison +9 -9
- package/valueParser.mjs +7 -7
package/kvm.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { argv, stdin, exit, stdout } from "node:process";
|
|
4
|
-
import {
|
|
4
|
+
import { Value, composePattern, withPattern, isProduct, isVariant } from "./Value.mjs";
|
|
5
5
|
import { exportPatternGraph } from "./codecs/runtime/codec.mjs";
|
|
6
6
|
import { patternToPropertyList } from "./codecs/runtime/pattern-json.mjs";
|
|
7
7
|
import {
|
|
@@ -17,6 +17,42 @@ import { decodeObject, loadLibrary } from "./object.mjs";
|
|
|
17
17
|
import codes from "./codes.mjs";
|
|
18
18
|
import { isMainEntrypoint } from "./codecs/runtime/cli-entry.mjs";
|
|
19
19
|
|
|
20
|
+
function isFile(filePath) {
|
|
21
|
+
try {
|
|
22
|
+
return fs.statSync(filePath).isFile();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error?.code === "ENOENT" || error?.code === "ENOTDIR") return false;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildExportPreamble(exports, libraries) {
|
|
30
|
+
if (exports.length === 0) return "";
|
|
31
|
+
const aliasMap = {};
|
|
32
|
+
for (const lib of libraries) {
|
|
33
|
+
for (const [name, hash] of Object.entries(lib.relAlias || {})) {
|
|
34
|
+
if (name !== "__main__") aliasMap[name] = hash;
|
|
35
|
+
}
|
|
36
|
+
for (const [hash, entry] of Object.entries(lib.meta || {})) {
|
|
37
|
+
if (entry?.type !== "rel") continue;
|
|
38
|
+
for (const origin of entry?.origins || []) {
|
|
39
|
+
if (origin?.name && origin.name !== "__main__") {
|
|
40
|
+
aliasMap[origin.name] = hash;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const lines = [];
|
|
46
|
+
for (const spec of exports) {
|
|
47
|
+
const [libName, localName] = spec.includes(":") ? spec.split(":", 2) : [spec, spec];
|
|
48
|
+
const hash = aliasMap[libName];
|
|
49
|
+
if (!hash) throw new Error(`--export: '${libName}' not found in loaded libraries`);
|
|
50
|
+
const body = hash.startsWith("@") ? hash.slice(1) : hash;
|
|
51
|
+
lines.push(`${localName} = @${body};`);
|
|
52
|
+
}
|
|
53
|
+
return lines.join("\n") + "\n";
|
|
54
|
+
}
|
|
55
|
+
|
|
20
56
|
class KVMBuilder {
|
|
21
57
|
constructor(typePatternGraph) {
|
|
22
58
|
this.typePatternGraph = typePatternGraph;
|
|
@@ -281,7 +317,7 @@ function executeInstruction(inst, registers, context) {
|
|
|
281
317
|
}
|
|
282
318
|
case "project_field": {
|
|
283
319
|
const val = registers.get(inst.src);
|
|
284
|
-
if (!(val
|
|
320
|
+
if (!isProduct(val)) return undefined;
|
|
285
321
|
const fieldVal = val.product[inst.label];
|
|
286
322
|
if (fieldVal === undefined) return undefined;
|
|
287
323
|
|
|
@@ -295,7 +331,7 @@ function executeInstruction(inst, registers, context) {
|
|
|
295
331
|
}
|
|
296
332
|
case "project_variant": {
|
|
297
333
|
const val = registers.get(inst.src);
|
|
298
|
-
if (!(val
|
|
334
|
+
if (!isVariant(val) || val.tag !== inst.tag) return undefined;
|
|
299
335
|
|
|
300
336
|
if (options.envelopeFree) {
|
|
301
337
|
registers.set(inst.dest, val.value);
|
|
@@ -308,11 +344,11 @@ function executeInstruction(inst, registers, context) {
|
|
|
308
344
|
case "make_variant": {
|
|
309
345
|
const val = registers.get(inst.src);
|
|
310
346
|
if (options.envelopeFree) {
|
|
311
|
-
registers.set(inst.dest,
|
|
347
|
+
registers.set(inst.dest, Value.variant(inst.tag, val));
|
|
312
348
|
} else {
|
|
313
349
|
registers.set(
|
|
314
350
|
inst.dest,
|
|
315
|
-
|
|
351
|
+
Value.variant(inst.tag, val, composePattern("open-union", [[inst.tag, val.pattern]]))
|
|
316
352
|
);
|
|
317
353
|
}
|
|
318
354
|
return { type: "continue" };
|
|
@@ -375,13 +411,13 @@ function executeInstruction(inst, registers, context) {
|
|
|
375
411
|
}
|
|
376
412
|
let productValue;
|
|
377
413
|
if (options.envelopeFree) {
|
|
378
|
-
productValue =
|
|
414
|
+
productValue = Value.product(result);
|
|
379
415
|
} else {
|
|
380
416
|
const staticOutputPattern = inst.pattern;
|
|
381
417
|
if (staticOutputPattern) {
|
|
382
|
-
productValue =
|
|
418
|
+
productValue = Value.product(result, staticOutputPattern);
|
|
383
419
|
} else {
|
|
384
|
-
productValue =
|
|
420
|
+
productValue = Value.product(result, composePattern("closed-product", patternEntries));
|
|
385
421
|
}
|
|
386
422
|
}
|
|
387
423
|
registers.set(inst.dest, productValue);
|
|
@@ -428,9 +464,10 @@ export default {
|
|
|
428
464
|
|
|
429
465
|
function usage() {
|
|
430
466
|
const prog = argv[1] || "kvm.mjs";
|
|
431
|
-
console.error(`Usage: node ${prog} [ options ] ( k-expr | -
|
|
467
|
+
console.error(`Usage: node ${prog} [ options ] ( k-expr | input-file ) [ input-file ]`);
|
|
432
468
|
console.error("Options:");
|
|
433
469
|
console.error(" --lib file Load one .klib dependency before compiling.");
|
|
470
|
+
console.error(" --export spec Export a library alias into scope. 'name' or 'libname:localname'. May be repeated.");
|
|
434
471
|
console.error(" --envelope-free Run the interpreter in envelope-free mode.");
|
|
435
472
|
console.error(" -h, --help Show this help.");
|
|
436
473
|
}
|
|
@@ -444,6 +481,7 @@ async function main() {
|
|
|
444
481
|
|
|
445
482
|
let envelopeFree = false;
|
|
446
483
|
const libraries = [];
|
|
484
|
+
const exports = [];
|
|
447
485
|
|
|
448
486
|
// Parse options
|
|
449
487
|
while (args.length > 0) {
|
|
@@ -457,22 +495,44 @@ async function main() {
|
|
|
457
495
|
if (libraries.length > 0) throw new Error("--lib may be specified at most once");
|
|
458
496
|
const libBuffer = fs.readFileSync(libPath);
|
|
459
497
|
libraries.push(loadLibrary(decodeObject(libBuffer)));
|
|
498
|
+
} else if (args[0] === "--export") {
|
|
499
|
+
args.shift();
|
|
500
|
+
const spec = args.shift();
|
|
501
|
+
if (!spec) throw new Error("--export requires a spec argument");
|
|
502
|
+
exports.push(spec);
|
|
503
|
+
} else if (args[0].startsWith("--")) {
|
|
504
|
+
throw new Error(`Unknown option: ${args[0]}`);
|
|
460
505
|
} else {
|
|
461
506
|
break;
|
|
462
507
|
}
|
|
463
508
|
}
|
|
464
509
|
|
|
465
|
-
|
|
510
|
+
const programInput = (function (arg) {
|
|
466
511
|
if (arg == null) {
|
|
467
512
|
throw new Error("Missing script argument");
|
|
468
513
|
}
|
|
469
514
|
if (arg === "-k") {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
515
|
+
throw new Error("-k is no longer supported; pass the source/object file path directly");
|
|
516
|
+
}
|
|
517
|
+
if (isFile(arg)) {
|
|
518
|
+
const buffer = fs.readFileSync(arg);
|
|
519
|
+
try {
|
|
520
|
+
const object = decodeObject(buffer);
|
|
521
|
+
if (object.main == null) {
|
|
522
|
+
throw new Error("Cannot run a .klib library without a main relation; load it with --lib.");
|
|
523
|
+
}
|
|
524
|
+
return { kind: "object", defs: { rels: object.rels }, main: object.main };
|
|
525
|
+
} catch (error) {
|
|
526
|
+
if (error.message === "Cannot run a .klib library without a main relation; load it with --lib.") {
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
kind: "source",
|
|
531
|
+
source: buildExportPreamble(exports, libraries) + buffer.toString("utf8")
|
|
532
|
+
};
|
|
533
|
+
}
|
|
475
534
|
}
|
|
535
|
+
return { kind: "source", source: buildExportPreamble(exports, libraries) + arg };
|
|
476
536
|
})(args.shift());
|
|
477
537
|
|
|
478
538
|
const inputStream = (function (arg) {
|
|
@@ -482,13 +542,20 @@ async function main() {
|
|
|
482
542
|
return fs.createReadStream(arg);
|
|
483
543
|
})(args.shift());
|
|
484
544
|
|
|
485
|
-
|
|
486
|
-
|
|
545
|
+
if (args.length > 0) {
|
|
546
|
+
throw new Error(`Unexpected argument: ${args[0]}`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const defs = programInput.kind === "object"
|
|
550
|
+
? programInput.defs
|
|
551
|
+
: annotate(programInput.source, { libraries });
|
|
552
|
+
const mainRelName = programInput.kind === "object" ? programInput.main : "__main__";
|
|
553
|
+
const mainRel = defs.rels[mainRelName];
|
|
487
554
|
if (!mainRel) {
|
|
488
|
-
throw new Error(
|
|
555
|
+
throw new Error(`No main relation (${mainRelName}) defined in script`);
|
|
489
556
|
}
|
|
490
557
|
|
|
491
|
-
const kvmFunc = lowerToKVM(mainRel,
|
|
558
|
+
const kvmFunc = lowerToKVM(mainRel, mainRelName);
|
|
492
559
|
|
|
493
560
|
const buffer = [];
|
|
494
561
|
inputStream.on("data", (data) => buffer.push(Buffer.isBuffer(data) ? data : Buffer.from(data)));
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fraczak/k",
|
|
3
|
-
"version": "6.2
|
|
3
|
+
"version": "6.3.2",
|
|
4
4
|
"description": "k — a language for composing first-order partial functions over algebraic data types (products and tagged unions); includes runtime, type derivation, REPL, and codec toolchain",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"prepare": "jiwson -m es6 parser.jison -o parser.mjs; jiwson -m es6 valueParser.jison -o valueParser.mjs; ",
|
|
8
8
|
"test": "node scripts/run-tests.mjs",
|
|
9
|
+
"benchmark": "node scripts/benchmark.mjs",
|
|
10
|
+
"benchmark:html": "node scripts/render-benchmark.mjs",
|
|
11
|
+
"hooks:install": "git config core.hooksPath scripts/git-hooks",
|
|
9
12
|
"perf:idempotency": "node scripts/benchmark-idempotency.mjs",
|
|
10
13
|
"deps:graph": "node scripts/generate-dependency-graph.mjs"
|
|
11
14
|
},
|
package/repl.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { argv, exit, stdin, stdout } from "node:process";
|
|
|
10
10
|
import { annotate, parse } from "./index.mjs";
|
|
11
11
|
import run from "./run.mjs";
|
|
12
12
|
import codes from "./codes.mjs";
|
|
13
|
-
import {
|
|
13
|
+
import { Value } from "./Value.mjs";
|
|
14
14
|
import { patterns2filters, prettyCode, prettyRel } from "./pretty.mjs";
|
|
15
15
|
import {
|
|
16
16
|
compileObject,
|
|
@@ -50,7 +50,7 @@ function cloneJSON(value) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function emptyValue() {
|
|
53
|
-
return
|
|
53
|
+
return Value.product({}, [["closed-product", []]]);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function createState() {
|
package/run.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "assert";
|
|
2
|
-
import {
|
|
2
|
+
import { Value, edgeSubpattern, composePattern, withPattern, isProduct, isVariant } from "./Value.mjs"
|
|
3
3
|
import { exportPatternGraph, intersectPropertyListPatterns } from "./codecs/runtime/codec.mjs";
|
|
4
4
|
import { patternToPropertyList } from "./codecs/runtime/pattern-json.mjs";
|
|
5
5
|
|
|
@@ -261,14 +261,14 @@ function compiledExp(findCode, exp, typePatternGraph) {
|
|
|
261
261
|
` - product envelope: ${patternPreview(dynamicOutputPattern)}`
|
|
262
262
|
);
|
|
263
263
|
}
|
|
264
|
-
return
|
|
264
|
+
return Value.product(result, pattern);
|
|
265
265
|
};
|
|
266
266
|
break;
|
|
267
267
|
}
|
|
268
268
|
case "vid":
|
|
269
269
|
fn = (value) => {
|
|
270
270
|
if (value === undefined) return undefined;
|
|
271
|
-
return
|
|
271
|
+
return Value.variant(exp.vid, value, composePattern("open-union", [[exp.vid, value.pattern]]));
|
|
272
272
|
};
|
|
273
273
|
break;
|
|
274
274
|
default:
|
|
@@ -375,13 +375,13 @@ function compiledConvergedExp(findCode, exp, typePatternGraph, options = {}) {
|
|
|
375
375
|
break;
|
|
376
376
|
case "dot":
|
|
377
377
|
fn = (value) => {
|
|
378
|
-
if (value === undefined || !(value
|
|
378
|
+
if (value === undefined || !isProduct(value)) return undefined;
|
|
379
379
|
return Object.hasOwn(value.product, exp.dot) ? value.product[exp.dot] : undefined;
|
|
380
380
|
};
|
|
381
381
|
break;
|
|
382
382
|
case "div":
|
|
383
383
|
fn = (value) => {
|
|
384
|
-
if (value === undefined || !(value
|
|
384
|
+
if (value === undefined || !isVariant(value) || value.tag !== exp.div) return undefined;
|
|
385
385
|
return value.value;
|
|
386
386
|
};
|
|
387
387
|
break;
|
|
@@ -419,12 +419,12 @@ function compiledConvergedExp(findCode, exp, typePatternGraph, options = {}) {
|
|
|
419
419
|
if (r === undefined) return undefined;
|
|
420
420
|
result[labels[i]] = r;
|
|
421
421
|
}
|
|
422
|
-
return
|
|
422
|
+
return Value.product(result);
|
|
423
423
|
};
|
|
424
424
|
break;
|
|
425
425
|
}
|
|
426
426
|
case "vid":
|
|
427
|
-
fn = (value) => value === undefined ? undefined :
|
|
427
|
+
fn = (value) => value === undefined ? undefined : Value.variant(exp.vid, value);
|
|
428
428
|
break;
|
|
429
429
|
default:
|
|
430
430
|
assert(false,`Unknown operation: '${exp.op}'`);
|
|
@@ -440,7 +440,7 @@ function verify(findCode, code, value) {
|
|
|
440
440
|
code = findCode(code);
|
|
441
441
|
switch (code.code) {
|
|
442
442
|
case "product":
|
|
443
|
-
if (!
|
|
443
|
+
if (!isProduct(value)) return false;
|
|
444
444
|
else {
|
|
445
445
|
const fields = Object.keys(value.product);
|
|
446
446
|
if (fields.length !== Object.keys(code.product).length) return false;
|
|
@@ -449,7 +449,7 @@ function verify(findCode, code, value) {
|
|
|
449
449
|
);
|
|
450
450
|
}
|
|
451
451
|
case "union":
|
|
452
|
-
if (!
|
|
452
|
+
if (!isProduct(value)) return false;
|
|
453
453
|
else {
|
|
454
454
|
const fields = Object.keys(value.product);
|
|
455
455
|
if (fields.length !== 1) return false;
|
|
@@ -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
|
+
});
|