@fraczak/k 6.2.1 → 6.2.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.
@@ -1,26 +1,50 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import fs from "node:fs";
4
+ import path from "node:path";
4
5
  import { argv, exit, stdin, stdout } from "node:process";
5
- import { compileObjectBuffer, decodeObject, loadLibrary } from "../object.mjs";
6
+ import {
7
+ compileObjectBuffer,
8
+ compileLibraryBuffer,
9
+ decodeObject,
10
+ loadLibrary
11
+ } from "../object.mjs";
12
+ import { lowerToKVM } from "../kvm.mjs";
13
+ import { annotate } from "../index.mjs";
6
14
 
7
15
  function helpText() {
8
16
  return [
9
- "Compile a k source file into an executable .ko object.",
10
- "The object records type-derivation convergence for each stored relation.",
17
+ "Compile a k source, .ko, or .klib into .ko, .klib, or .kvm output.",
18
+ "Output format is inferred from the output file extension, or from --format.",
11
19
  "",
12
- `Usage: ${argv[1]} [ --lib lib-file ]... [ k-file [ object-file ] ]`,
20
+ `Usage: ${argv[1]} [options] [source-snippet | input-file [output-file]]`,
13
21
  "",
14
22
  "Arguments:",
15
- " k-file Source .k file to compile. Reads UTF-8 source from stdin when omitted.",
16
- " object-file Output .ko path. Writes binary object data to stdout when omitted.",
23
+ " source-snippet Inline k source, in the same style as k.mjs.",
24
+ " input-file Source .k, .ko, or .klib file. Reads UTF-8 source from stdin when omitted.",
25
+ " output-file Output path (.ko, .klib, or .kvm). Writes to stdout when omitted.",
17
26
  "",
18
27
  "Options:",
19
- " --lib file Load a .klib dependency before compiling. May be repeated.",
20
- " -h, --help Show this help.",
28
+ " --lib file Load a .klib dependency before compiling. May be repeated.",
29
+ " --export spec Export a library alias into the source scope. May be repeated.",
30
+ " spec is 'name' or 'libname:localname'.",
31
+ " --format fmt Output format: ko, klib, or kvm. Overrides extension detection.",
32
+ " -h, --help Show this help.",
21
33
  "",
22
- "Example:",
23
- ` ${argv[1]} --lib core.klib program.k program.ko`
34
+ "Existing input paths are read as files. A non-existing input with .k, .ko,",
35
+ "or .klib extension is reported as a missing file; otherwise it is compiled",
36
+ "as inline k source.",
37
+ "",
38
+ "When input is .ko and output is .kvm, the object's relations are lowered",
39
+ "to kVM. A .klib input can only be copied to .klib output.",
40
+ "",
41
+ "Examples:",
42
+ ` ${argv[1]} 'x = |x; x x'`,
43
+ ` ${argv[1]} program.k program.ko`,
44
+ ` ${argv[1]} program.k program.klib`,
45
+ ` ${argv[1]} program.k program.kvm`,
46
+ ` ${argv[1]} program.ko program.kvm`,
47
+ ` ${argv[1]} --lib core.klib --export add --export mul:times program.k program.ko`
24
48
  ].join("\n");
25
49
  }
26
50
 
@@ -28,12 +52,100 @@ function usage(stream = console.error) {
28
52
  stream(helpText());
29
53
  }
30
54
 
31
- async function readStdinText() {
55
+ async function readStdinBytes() {
32
56
  const chunks = [];
33
57
  for await (const chunk of stdin) {
34
58
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
35
59
  }
36
- return Buffer.concat(chunks).toString("utf8");
60
+ return Buffer.concat(chunks);
61
+ }
62
+
63
+ function inferFormat(outputPath) {
64
+ if (!outputPath) return "ko";
65
+ const ext = path.extname(outputPath).toLowerCase();
66
+ if (ext === ".klib") return "klib";
67
+ if (ext === ".kvm") return "kvm";
68
+ return "ko";
69
+ }
70
+
71
+ function inferInputType(inputPath) {
72
+ if (!inputPath) return "k";
73
+ const ext = path.extname(inputPath).toLowerCase();
74
+ if (ext === ".ko") return "ko";
75
+ if (ext === ".klib") return "klib";
76
+ return "k";
77
+ }
78
+
79
+ function resolveInput(inputArg) {
80
+ if (inputArg == null) {
81
+ return { kind: "stdin", type: "k", sourceName: "<stdin>" };
82
+ }
83
+
84
+ const ext = path.extname(inputArg).toLowerCase();
85
+ if (fs.existsSync(inputArg)) {
86
+ return {
87
+ kind: "file",
88
+ type: inferInputType(inputArg),
89
+ path: inputArg,
90
+ sourceName: inputArg
91
+ };
92
+ }
93
+
94
+ if (ext === ".k" || ext === ".ko" || ext === ".klib") {
95
+ throw new Error(`Input file not found: ${inputArg}`);
96
+ }
97
+
98
+ return {
99
+ kind: "inline",
100
+ type: "k",
101
+ source: inputArg,
102
+ sourceName: "<inline>"
103
+ };
104
+ }
105
+
106
+ function buildExportPreamble(exports, libraries) {
107
+ if (exports.length === 0) return "";
108
+ // Build a combined alias map from all libraries: name -> @hash
109
+ const aliasMap = {};
110
+ for (const lib of libraries) {
111
+ for (const [name, hash] of Object.entries(lib.relAlias || {})) {
112
+ if (name !== "__main__") aliasMap[name] = hash;
113
+ }
114
+ for (const [hash, entry] of Object.entries(lib.meta || {})) {
115
+ if (entry?.type !== "rel") continue;
116
+ for (const origin of entry?.origins || []) {
117
+ if (origin?.name && origin.name !== "__main__") {
118
+ aliasMap[origin.name] = hash;
119
+ }
120
+ }
121
+ }
122
+ }
123
+ const lines = [];
124
+ for (const spec of exports) {
125
+ const [libName, localName] = spec.includes(":") ? spec.split(":", 2) : [spec, spec];
126
+ const hash = aliasMap[libName];
127
+ if (!hash) throw new Error(`--export: '${libName}' not found in loaded libraries`);
128
+ const body = hash.startsWith("@") ? hash.slice(1) : hash;
129
+ lines.push(`${localName} = @${body};`);
130
+ }
131
+ return lines.join("\n") + "\n";
132
+ }
133
+
134
+ function compileToKVM(source, options) {
135
+ const defs = annotate(source, options);
136
+ const kvmProgram = {};
137
+ for (const [name, relDef] of Object.entries(defs.rels)) {
138
+ kvmProgram[name] = lowerToKVM(relDef, name);
139
+ }
140
+ return JSON.stringify(kvmProgram, null, 2) + "\n";
141
+ }
142
+
143
+ function objectToKVM(object) {
144
+ const kvmProgram = {};
145
+ for (const [name, relDef] of Object.entries(object.rels)) {
146
+ kvmProgram[name] = lowerToKVM(relDef, name);
147
+ }
148
+ return JSON.stringify(kvmProgram, null, 2) + "\n";
37
149
  }
38
150
 
39
151
  try {
@@ -44,24 +156,101 @@ try {
44
156
  }
45
157
 
46
158
  const libraries = [];
47
- while (args.length > 0 && args[0] === "--lib") {
48
- args.shift();
49
- const libPath = args.shift();
50
- if (!libPath) throw new Error("--lib requires a file argument");
51
- libraries.push(loadLibrary(decodeObject(fs.readFileSync(libPath))));
159
+ let format = null;
160
+ const exports = [];
161
+
162
+ while (args.length > 0) {
163
+ if (args[0] === "--") {
164
+ args.shift();
165
+ break;
166
+ }
167
+ if (args[0] === "--lib") {
168
+ args.shift();
169
+ const libPath = args.shift();
170
+ if (!libPath) throw new Error("--lib requires a file argument");
171
+ libraries.push(loadLibrary(decodeObject(fs.readFileSync(libPath))));
172
+ } else if (args[0] === "--export") {
173
+ args.shift();
174
+ const spec = args.shift();
175
+ if (!spec) throw new Error("--export requires a spec argument (name or libname:localname)");
176
+ exports.push(spec);
177
+ } else if (args[0] === "--format") {
178
+ args.shift();
179
+ format = args.shift();
180
+ if (!["ko", "klib", "kvm"].includes(format))
181
+ throw new Error(`Unknown format: ${format}. Use ko, klib, or kvm.`);
182
+ } else if (args[0].startsWith("--")) {
183
+ throw new Error(`Unknown option: ${args[0]}`);
184
+ } else {
185
+ break;
186
+ }
187
+ }
188
+
189
+ const inputArg = args.shift() || null;
190
+ const outputPath = args.shift() || null;
191
+ if (args.length > 0) {
192
+ throw new Error(`Unexpected argument: ${args[0]}`);
193
+ }
194
+
195
+ const outputFormat = format || inferFormat(outputPath);
196
+ const input = resolveInput(inputArg);
197
+ const inputType = input.type;
198
+
199
+ let output;
200
+
201
+ if (inputType === "ko" || inputType === "klib") {
202
+ const buffer = input.kind === "stdin" ? await readStdinBytes() : fs.readFileSync(input.path);
203
+ const object = decodeObject(buffer);
204
+ if (inputType === "klib") {
205
+ libraries.push(loadLibrary(object));
206
+ }
207
+
208
+ if (outputFormat === "kvm") {
209
+ if (inputType === "klib") {
210
+ throw new Error("Cannot produce .kvm from .klib alone; use a .ko or .k input.");
211
+ }
212
+ output = objectToKVM(object);
213
+ } else if (outputFormat === "ko") {
214
+ if (inputType === "ko") {
215
+ // already a .ko, just copy
216
+ output = buffer;
217
+ } else {
218
+ throw new Error("Cannot produce .ko from .klib without a main expression; provide .k source instead.");
219
+ }
220
+ } else if (outputFormat === "klib") {
221
+ if (inputType === "klib") {
222
+ output = buffer;
223
+ } else {
224
+ throw new Error("Cannot produce .klib from .ko; provide .k source instead.");
225
+ }
226
+ }
227
+ } else {
228
+ // Input is .k source
229
+ const rawSource = input.kind === "stdin"
230
+ ? (await readStdinBytes()).toString("utf8")
231
+ : input.kind === "inline"
232
+ ? input.source
233
+ : fs.readFileSync(input.path, "utf8");
234
+ const preamble = buildExportPreamble(exports, libraries);
235
+ const source = preamble + rawSource;
236
+ const opts = { source: input.sourceName, libraries };
237
+
238
+ if (outputFormat === "ko") {
239
+ output = compileObjectBuffer(source, opts);
240
+ } else if (outputFormat === "klib") {
241
+ output = compileLibraryBuffer(source, opts);
242
+ } else if (outputFormat === "kvm") {
243
+ output = compileToKVM(source, opts);
244
+ }
52
245
  }
53
246
 
54
- const sourcePath = args.shift();
55
- const outputPath = args.shift();
56
- const source = sourcePath == null ? await readStdinText() : fs.readFileSync(sourcePath, "utf8");
57
- const objectBuffer = compileObjectBuffer(source, { source: sourcePath || "<stdin>", libraries });
58
247
  if (outputPath == null) {
59
- stdout.write(objectBuffer);
248
+ stdout.write(output);
60
249
  } else {
61
- fs.writeFileSync(outputPath, objectBuffer);
250
+ fs.writeFileSync(outputPath, output);
62
251
  }
63
252
  } catch (error) {
64
- console.error(error);
253
+ console.error(error.message || String(error));
65
254
  usage();
66
255
  exit(1);
67
256
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fraczak/k",
3
- "version": "6.2.1",
3
+ "version": "6.2.3",
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": {
@@ -18,7 +18,6 @@
18
18
  "bin": {
19
19
  "k": "./k.mjs",
20
20
  "k-compile": "./objects/compile.mjs",
21
- "k-compile-lib": "./objects/compile-lib.mjs",
22
21
  "k-decompile": "./objects/decompile.mjs",
23
22
  "k-extract-aliases": "./objects/extract-aliases.mjs",
24
23
  "k-ieee": "./codecs/ieee.mjs",
@@ -44,7 +44,7 @@ function formatBytes(value) {
44
44
  return `${value} B`;
45
45
  }
46
46
 
47
- console.log(`compile-lib/decompile round-trip benchmark (${iterations} iterations)`);
47
+ console.log(`compile/decompile .klib round-trip benchmark (${iterations} iterations)`);
48
48
  console.log("example, median, min, max, object");
49
49
 
50
50
  for (const example of benchmarkExamples) {
@@ -54,9 +54,9 @@ for (const example of benchmarkExamples) {
54
54
  for (let i = 0; i < iterations; i++) {
55
55
  const out = mkdtempSync(join(tmpdir(), `k-idempotency-bench-${example.replaceAll("/", "_")}-`));
56
56
  const t0 = performance.now();
57
- run(["objects/compile-lib.mjs", example, join(out, "1.klib")]);
57
+ run(["objects/compile.mjs", example, join(out, "1.klib")]);
58
58
  run(["objects/decompile.mjs", join(out, "1.klib"), join(out, "1.k")]);
59
- run(["objects/compile-lib.mjs", join(out, "1.k"), join(out, "2.klib")]);
59
+ run(["objects/compile.mjs", join(out, "1.k"), join(out, "2.klib")]);
60
60
  run(["objects/decompile.mjs", join(out, "2.klib"), join(out, "2.k")]);
61
61
  times.push(performance.now() - t0);
62
62
  size = statSync(join(out, "1.klib")).size;
@@ -18,6 +18,7 @@ const tests = [
18
18
  ["tests/test-fingerprint.mjs", node, ["tests/test-fingerprint.mjs"]],
19
19
  ["tests/test-hash-fuzz.mjs", node, ["tests/test-hash-fuzz.mjs"]],
20
20
  ["tests/test-hash-normalization.mjs", node, ["tests/test-hash-normalization.mjs"]],
21
+ ["tests/test-deep-wire-value.mjs", node, ["tests/test-deep-wire-value.mjs"]],
21
22
  ["tests/test-k-object.mjs", node, ["tests/test-k-object.mjs"]],
22
23
  ["tests/test-repl.mjs", node, ["tests/test-repl.mjs"]],
23
24
  ["tests/test-kvm.mjs", node, ["tests/test-kvm.mjs"]],
@@ -8,6 +8,23 @@ echo "Running './tests/integration.sh' ..."
8
8
  TMP_DIR=`mktemp -d`
9
9
  trap 'rm -rf "$TMP_DIR"' EXIT
10
10
 
11
+ ./objects/compile.mjs 'x = |x; x x' "$TMP_DIR/inline.ko"
12
+ ./objects/decompile.mjs "$TMP_DIR/inline.ko" | grep -q '^----- main -----$'
13
+ printf '()' | ./objects/compile.mjs > "$TMP_DIR/stdin.ko"
14
+ ./objects/decompile.mjs "$TMP_DIR/stdin.ko" | grep -q '^----- main -----$'
15
+ ./objects/compile.mjs --format kvm '()' | grep -q '"__main__"'
16
+ ./objects/compile.mjs "$TMP_DIR/stdin.ko" "$TMP_DIR/stdin.kvm"
17
+ grep -q '"__main__"' "$TMP_DIR/stdin.kvm"
18
+ ./objects/compile.mjs 'id = ();' "$TMP_DIR/export.klib"
19
+ ./objects/compile.mjs --lib "$TMP_DIR/export.klib" --export id:alias 'alias' "$TMP_DIR/export.ko"
20
+ ./objects/decompile.mjs "$TMP_DIR/export.ko" | grep -q '^----- main -----$'
21
+ node ./codecs/unit.mjs --parse | ./k.mjs --lib "$TMP_DIR/export.klib" --export id:alias 'alias' | node ./codecs/unit.mjs --print | grep -qx '{}'
22
+ if ./objects/compile.mjs "$TMP_DIR/missing.k" > "$TMP_DIR/missing.out" 2> "$TMP_DIR/missing.err"; then
23
+ echo "expected missing .k input to fail" >&2
24
+ exit 1
25
+ fi
26
+ grep -q 'Input file not found:' "$TMP_DIR/missing.err"
27
+
11
28
  echo '["zebara", "ela", "kupa", ala, owca]' | node ./codecs/k-parse.mjs | ./k.mjs '{.1 0,.3 1}' | node ./codecs/k-print.mjs
12
29
  echo
13
30
  echo '-21' | node ./codecs/int.mjs --parse | node ./codecs/int.mjs --print | grep -qx -- '-21'
@@ -16,7 +33,7 @@ node ./codecs/unit.mjs --parse | node ./codecs/unit.mjs --print | grep -qx '{}'
16
33
  echo
17
34
  printf 'A🙂\nBé~\t' | ./codecs/utf8.mjs --parse | ./k.mjs -k Examples/byte.k |./codecs/utf8.mjs --print
18
35
  echo
19
- ./objects/compile-lib.mjs Examples/ieee.k "$TMP_DIR/ieee.klib"
36
+ ./objects/compile.mjs Examples/ieee.k "$TMP_DIR/ieee.klib"
20
37
  MUL=`./objects/extract-aliases.mjs "$TMP_DIR/ieee.klib" | grep '^mul = @' | sed 's/^mul = \(@[^;]*\);.*$/\1/'`
21
38
  DIV=`./objects/extract-aliases.mjs "$TMP_DIR/ieee.klib" | grep '^div = @' | sed 's/^div = \(@[^;]*\);.*$/\1/'`
22
39
  ARG=0.12
@@ -0,0 +1,29 @@
1
+ import assert from "node:assert/strict";
2
+ import { Product, Variant } from "../Value.mjs";
3
+ import { decodeWire, encodeToWire } from "../codecs/runtime/prefix-codec.mjs";
4
+
5
+ const depth = 12000;
6
+ const pattern = [
7
+ ["closed-union", [["+", 1]]],
8
+ ["closed-union", [["_", 2], ["1", 1]]],
9
+ ["closed-product", []]
10
+ ];
11
+
12
+ let bits = new Variant("_", new Product({}));
13
+ for (let i = 0; i < depth; i++) {
14
+ bits = new Variant("1", bits);
15
+ }
16
+
17
+ const wire = encodeToWire(new Variant("+", bits), pattern);
18
+ const decoded = decodeWire(wire).value;
19
+ assert.equal(decoded.tag, "+");
20
+
21
+ bits = decoded.value;
22
+ for (let i = 0; i < depth; i++) {
23
+ assert.equal(bits.tag, "1");
24
+ bits = bits.value;
25
+ }
26
+ assert.equal(bits.tag, "_");
27
+ assert.deepEqual(bits.value.product, {});
28
+
29
+ console.log("deep wire value: ok");
@@ -1,67 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from "node:fs";
4
- import { argv, exit, stdin, stdout } from "node:process";
5
- import { compileLibraryBuffer, decodeObject, loadLibrary } from "../object.mjs";
6
-
7
- function helpText() {
8
- return [
9
- "Compile a k source file into a plain-JSON .klib library object.",
10
- "The library records type-derivation convergence for each stored relation.",
11
- "",
12
- `Usage: ${argv[1]} [ --lib lib-file ]... [ k-file [ lib-file ] ]`,
13
- "",
14
- "Arguments:",
15
- " k-file Source .k file to compile. Reads UTF-8 source from stdin when omitted.",
16
- " lib-file Output .klib path. Writes plain JSON library data to stdout when omitted.",
17
- "",
18
- "Options:",
19
- " --lib file Load a .klib dependency before compiling. May be repeated.",
20
- " -h, --help Show this help.",
21
- "",
22
- "Example:",
23
- ` ${argv[1]} --lib core.klib library.k library.klib`
24
- ].join("\n");
25
- }
26
-
27
- function usage(stream = console.error) {
28
- stream(helpText());
29
- }
30
-
31
- async function readStdinText() {
32
- const chunks = [];
33
- for await (const chunk of stdin) {
34
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
35
- }
36
- return Buffer.concat(chunks).toString("utf8");
37
- }
38
-
39
- try {
40
- const args = argv.slice(2);
41
- if (args.includes("-h") || args.includes("--help")) {
42
- usage(console.log);
43
- exit(0);
44
- }
45
-
46
- const libraries = [];
47
- while (args.length > 0 && args[0] === "--lib") {
48
- args.shift();
49
- const libPath = args.shift();
50
- if (!libPath) throw new Error("--lib requires a file argument");
51
- libraries.push(loadLibrary(decodeObject(fs.readFileSync(libPath))));
52
- }
53
-
54
- const sourcePath = args.shift();
55
- const outputPath = args.shift();
56
- const source = sourcePath == null ? await readStdinText() : fs.readFileSync(sourcePath, "utf8");
57
- const objectBuffer = compileLibraryBuffer(source, { source: sourcePath || "<stdin>", libraries });
58
- if (outputPath == null) {
59
- stdout.write(objectBuffer);
60
- } else {
61
- fs.writeFileSync(outputPath, objectBuffer);
62
- }
63
- } catch (error) {
64
- console.error(error);
65
- usage();
66
- exit(1);
67
- }