@fraczak/k 6.1.0 → 6.1.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/codecs/utf8.mjs CHANGED
@@ -1,7 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { stdin, stdout, argv, exit } from "node:process";
4
- import { encodeText, decodeText } from "./string-codec.mjs";
4
+ import { isMainEntrypoint } from "./runtime/cli-entry.mjs";
5
+ import {
6
+ STRING_PATTERN_PROPERTY_LIST,
7
+ encodeText,
8
+ decodeText,
9
+ textToStringValue,
10
+ stringValueToText
11
+ } from "./string-codec.mjs";
12
+
13
+ const name = "utf8";
14
+ const patterns = [STRING_PATTERN_PROPERTY_LIST];
15
+
16
+ function parse(text) {
17
+ return textToStringValue(text);
18
+ }
19
+
20
+ function print(value) {
21
+ return stringValueToText(value);
22
+ }
5
23
 
6
24
  function usage(stream = console.error) {
7
25
  stream(`Usage: ${argv[1]} --parse | --print`);
@@ -41,7 +59,11 @@ async function main() {
41
59
  }
42
60
  }
43
61
 
44
- main().catch(err => {
45
- console.error(err.message || String(err));
46
- exit(1);
47
- });
62
+ if (isMainEntrypoint(import.meta.url, argv[1])) {
63
+ main().catch(err => {
64
+ console.error(err.message || String(err));
65
+ exit(1);
66
+ });
67
+ }
68
+
69
+ export { name, patterns, parse, print };
package/ieee.klib CHANGED
Binary file
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@fraczak/k",
3
- "version": "6.1.0",
3
+ "version": "6.1.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
- "test": "node test.mjs; for f in Code-derivation-tests/*.mjs; do echo $f; node $f; done; node test-fingerprint.mjs; node test-hash-fuzz.mjs; node test-hash-normalization.mjs; node test-k-object.mjs; node test-repl.mjs; ./tests.sh",
8
+ "test": "node test.mjs; for f in Code-derivation-tests/*.mjs; do echo $f; node $f; done; node test-fingerprint.mjs; node test-hash-fuzz.mjs; node test-hash-normalization.mjs; node test-k-object.mjs; node test-repl.mjs; node test-ieee-arithmetic.mjs; ./tests.sh",
9
9
  "deps:graph": "node scripts/generate-dependency-graph.mjs"
10
10
  },
11
11
  "repository": {
@@ -0,0 +1,173 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ import { encodeToWire, decodeWire } from "./codecs/runtime/prefix-codec.mjs";
6
+ import { deriveClosedPattern } from "./codecs/runtime/codec.mjs";
7
+ import { patternToPropertyList } from "./codecs/runtime/pattern-json.mjs";
8
+ import { finalize } from "./codes.mjs";
9
+
10
+ function isCanonicalCodeName(name) {
11
+ return typeof name === "string" && name.startsWith("@");
12
+ }
13
+
14
+ function singletonPatternToCodeHash(pattern) {
15
+ if (!Array.isArray(pattern) || pattern.length === 0) {
16
+ return null;
17
+ }
18
+
19
+ const codeDefs = {};
20
+ for (let i = 0; i < pattern.length; i++) {
21
+ const [kind, edges] = pattern[i] || [];
22
+ if (kind !== "closed-product" && kind !== "closed-union") {
23
+ return null;
24
+ }
25
+ if (!Array.isArray(edges)) {
26
+ throw new Error(`Invalid singleton pattern node ${i}`);
27
+ }
28
+
29
+ const code = kind === "closed-product" ? "product" : "union";
30
+ codeDefs[`C${i}`] = {
31
+ code,
32
+ [code]: Object.fromEntries(edges.map(([label, target]) => {
33
+ if (!Number.isInteger(target) || target < 0 || target >= pattern.length) {
34
+ throw new Error(`Invalid singleton pattern edge target ${target}`);
35
+ }
36
+ return [label, `C${target}`];
37
+ }))
38
+ };
39
+ }
40
+
41
+ return finalize(codeDefs).representatives.C0 || null;
42
+ }
43
+
44
+ function codeHashToPattern(codeHash, findCode) {
45
+ const code = findCode(codeHash);
46
+ if (!code || code.code === "undefined") {
47
+ throw new Error(`Unknown type '${codeHash}'`);
48
+ }
49
+ const resolveType = (name) => {
50
+ const resolved = findCode(name);
51
+ if (!resolved || resolved.code === "undefined") {
52
+ throw new Error(`Unknown type '${name}'`);
53
+ }
54
+ return resolved;
55
+ };
56
+ return patternToPropertyList(deriveClosedPattern(codeHash, code, resolveType));
57
+ }
58
+
59
+ function valueForCode(value, codeHash, findCode) {
60
+ const pattern = codeHashToPattern(codeHash, findCode);
61
+ return decodeWire(encodeToWire(value, pattern)).value;
62
+ }
63
+
64
+ function normalizeCodecModule(mod, fallbackName) {
65
+ const codec = mod.replCodec || mod.codec || mod.default || mod;
66
+ const name = codec.name || mod.name || fallbackName;
67
+ const patterns = codec.patterns || (codec.pattern ? [codec.pattern] : null);
68
+ const codes = codec.codes || codec.codeHashes || (codec.code ? [codec.code] : null) ||
69
+ patterns?.map((pattern, index) => {
70
+ const codeHash = singletonPatternToCodeHash(pattern);
71
+ if (!codeHash) throw new Error(`Codec '${name}' pattern ${index} is not singleton`);
72
+ return codeHash;
73
+ });
74
+ if (!name || typeof name !== "string") {
75
+ throw new Error("Codec module must export a string name");
76
+ }
77
+ if (!Array.isArray(codes) || codes.length === 0) {
78
+ throw new Error(`Codec '${name}' must export a non-empty codes array`);
79
+ }
80
+ if (codec.parse != null && typeof codec.parse !== "function") {
81
+ throw new Error(`Codec '${name}' parse export must be a function`);
82
+ }
83
+ if (codec.print != null && typeof codec.print !== "function") {
84
+ throw new Error(`Codec '${name}' print export must be a function`);
85
+ }
86
+ return {
87
+ name,
88
+ codes,
89
+ parse: codec.parse,
90
+ print: codec.print
91
+ };
92
+ }
93
+
94
+ function codecStore(state) {
95
+ if (!state.codecs) state.codecs = {};
96
+ return state.codecs;
97
+ }
98
+
99
+ function registerCodec(state, codec, source = null) {
100
+ const registered = [];
101
+ const store = codecStore(state);
102
+ for (const codeHash of codec.codes) {
103
+ if (!isCanonicalCodeName(codeHash)) {
104
+ throw new Error(`Codec '${codec.name}' has non-canonical code '${codeHash}'`);
105
+ }
106
+ if (!store[codeHash]) store[codeHash] = [];
107
+ const entry = {
108
+ name: codec.name,
109
+ source,
110
+ parse: codec.parse,
111
+ print: codec.print
112
+ };
113
+ const existingIndex = store[codeHash].findIndex((existing) =>
114
+ existing.name === entry.name && existing.source === entry.source
115
+ );
116
+ if (existingIndex === -1) {
117
+ store[codeHash].push(entry);
118
+ } else {
119
+ store[codeHash][existingIndex] = entry;
120
+ }
121
+ registered.push({ codeHash, ...entry });
122
+ }
123
+ return registered;
124
+ }
125
+
126
+ async function loadCodecModule(state, filePath) {
127
+ const resolved = path.resolve(filePath);
128
+ const url = pathToFileURL(resolved);
129
+ url.searchParams.set("mtime", String(fs.statSync(resolved).mtimeMs));
130
+ const mod = await import(url.href);
131
+ const codec = normalizeCodecModule(mod, path.basename(resolved, path.extname(resolved)));
132
+ return registerCodec(state, codec, resolved);
133
+ }
134
+
135
+ function listCodecs(state) {
136
+ const entries = Object.entries(codecStore(state))
137
+ .flatMap(([codeHash, codecs]) => codecs.map((codec) => ({ codeHash, ...codec })))
138
+ .sort((a, b) => a.name.localeCompare(b.name) || a.codeHash.localeCompare(b.codeHash));
139
+ if (entries.length === 0) return "(none)";
140
+ return entries.map((entry) => `${entry.name} ${entry.codeHash}${entry.source ? ` (${entry.source})` : ""}`).join("\n");
141
+ }
142
+
143
+ function codecNames(state, codeHash = null) {
144
+ const store = codecStore(state);
145
+ const codecs = codeHash ? (store[codeHash] || []) : Object.values(store).flat();
146
+ return [...new Set(codecs.map((codec) => codec.name))].sort();
147
+ }
148
+
149
+ function resolveCodec(state, codeHash, codecName = null, capability = null) {
150
+ const matches = (codecStore(state)[codeHash] || []).filter((codec) =>
151
+ (codecName == null || codec.name === codecName) &&
152
+ (capability == null || typeof codec[capability] === "function")
153
+ );
154
+ if (matches.length === 0) {
155
+ const suffix = codecName ? ` named '${codecName}'` : "";
156
+ throw new Error(`No ${capability || "usable"} codec${suffix} for ${codeHash}`);
157
+ }
158
+ if (matches.length > 1 && codecName == null) {
159
+ throw new Error(`Multiple codecs for ${codeHash}; specify one of: ${matches.map((codec) => codec.name).join(", ")}`);
160
+ }
161
+ return matches[0];
162
+ }
163
+
164
+ export {
165
+ codecNames,
166
+ codeHashToPattern,
167
+ listCodecs,
168
+ loadCodecModule,
169
+ registerCodec,
170
+ resolveCodec,
171
+ singletonPatternToCodeHash,
172
+ valueForCode
173
+ };
package/repl.mjs CHANGED
@@ -23,15 +23,25 @@ import {
23
23
  prettyRelation
24
24
  } from "./object.mjs";
25
25
  import { propertyListToFilter, valueToK, valueWithEnvelopeToK } from "./codecs/runtime/show-value.mjs";
26
+ import {
27
+ codecNames,
28
+ codeHashToPattern,
29
+ listCodecs,
30
+ loadCodecModule,
31
+ resolveCodec,
32
+ singletonPatternToCodeHash,
33
+ valueForCode
34
+ } from "./repl-codecs.mjs";
26
35
 
27
36
  const NAME_RE = /^[a-zA-Z0-9_+-][a-zA-Z0-9_?!+-]*$/;
28
37
  const TYPE_DEF_RE = /^\s*\$\s*([a-zA-Z0-9_+-][a-zA-Z0-9_?!+-]*)\s*=/;
29
38
  const REL_DEF_RE = /^\s*([a-zA-Z0-9_+-][a-zA-Z0-9_?!+-]*)\s*=/;
30
39
  const COMMAND_NAMES = [
31
40
  "help", "type", "code", "rel", "def", "run", "eval", "t", "d", "C",
32
- "codes", "rels", "val", "reset", "klib", "ko", "load",
41
+ "codes", "rels", "val", "codec", "input", "reset", "klib", "ko", "load",
33
42
  "quit", "exit"
34
43
  ];
44
+ const CODEC_COMMAND_NAMES = ["load", "list"];
35
45
  const PATH_COMMANDS = new Set(["klib", "ko", "load"]);
36
46
  const initialCodes = codes.dump();
37
47
 
@@ -50,6 +60,8 @@ function createState() {
50
60
  rels: {},
51
61
  relAliases: {},
52
62
  typeAliases: {},
63
+ codecs: {},
64
+ pendingInput: null,
53
65
  meta: {},
54
66
  value: emptyValue(),
55
67
  lastMain: null
@@ -261,8 +273,35 @@ function listAliases(aliases) {
261
273
  return entries.map(([name, hash]) => `${name} = ${hash}`).join("\n");
262
274
  }
263
275
 
264
- function printValue(value = null) {
265
- return valueWithEnvelopeToK(value);
276
+ function formatCodecOutput(output) {
277
+ if (Buffer.isBuffer(output)) return output.toString("utf8");
278
+ return String(output);
279
+ }
280
+
281
+ function printValue(value, state = null) {
282
+ const lines = [valueWithEnvelopeToK(value)];
283
+ if (!state || value === undefined) return lines[0];
284
+
285
+ const codeHash = singletonPatternToCodeHash(value.pattern);
286
+ if (!codeHash) return lines[0];
287
+
288
+ for (const codec of state.codecs?.[codeHash] || []) {
289
+ if (typeof codec.print !== "function") continue;
290
+ try {
291
+ lines.push(`${codec.name}: ${formatCodecOutput(codec.print(value, { codeHash, state }))}`);
292
+ } catch (error) {
293
+ lines.push(`${codec.name}: <error: ${error.message || String(error)}>`);
294
+ }
295
+ }
296
+ return lines.join("\n");
297
+ }
298
+
299
+ function commitResult(state, result, lastMain) {
300
+ if (result !== undefined) {
301
+ state.value = result;
302
+ state.lastMain = lastMain;
303
+ }
304
+ return [printValue(result, state)];
266
305
  }
267
306
 
268
307
  function userDefinedNames(source) {
@@ -340,6 +379,13 @@ function canonicalNames(state) {
340
379
  ])].filter((name) => name.startsWith("@")).sort();
341
380
  }
342
381
 
382
+ function canonicalCodeNames(state) {
383
+ return [...new Set([
384
+ ...Object.keys(state.codes),
385
+ ...Object.values(state.typeAliases)
386
+ ])].filter((name) => name.startsWith("@")).sort();
387
+ }
388
+
343
389
  function aliasNames(state) {
344
390
  return [...new Set([
345
391
  ...Object.keys(state.typeAliases),
@@ -347,6 +393,17 @@ function aliasNames(state) {
347
393
  ])].sort();
348
394
  }
349
395
 
396
+ function displayAliases(state) {
397
+ const aliases = {};
398
+ for (const [name, hash] of [
399
+ ...Object.entries(state.typeAliases),
400
+ ...Object.entries(state.relAliases)
401
+ ].sort(([a], [b]) => a.localeCompare(b))) {
402
+ if (!aliases[hash]) aliases[hash] = name;
403
+ }
404
+ return aliases;
405
+ }
406
+
350
407
  function expandHome(input) {
351
408
  if (input === "~") return os.homedir();
352
409
  if (input.startsWith("~/")) return path.join(os.homedir(), input.slice(2));
@@ -378,6 +435,16 @@ function completeCanonical(line, state) {
378
435
  return [matches, line];
379
436
  }
380
437
 
438
+ function completeCanonicalCode(line, state) {
439
+ const match = line.match(/^(.*?)(@[A-Za-z0-9_?!+-]*)$/);
440
+ if (!match) return [[], line];
441
+ const [, prefix, partial] = match;
442
+ const matches = canonicalCodeNames(state)
443
+ .filter((name) => name.startsWith(partial))
444
+ .map((name) => `${prefix}${name}`);
445
+ return [matches, line];
446
+ }
447
+
381
448
  function completeIdentifier(line, state) {
382
449
  const match = line.match(/^(.*?)(\$?[A-Za-z0-9_+-][A-Za-z0-9_?!+-]*)$/);
383
450
  if (!match) return [[], line];
@@ -391,6 +458,19 @@ function completeIdentifier(line, state) {
391
458
  return [matches, line];
392
459
  }
393
460
 
461
+ function completeTypeIdentifier(line, state) {
462
+ const match = line.match(/^(.*?)(\$?[A-Za-z0-9_+-][A-Za-z0-9_?!+-]*)$/);
463
+ if (!match) return [[], line];
464
+ const [, prefix, partial] = match;
465
+ const isTypeToken = partial.startsWith("$");
466
+ const barePartial = isTypeToken ? partial.slice(1) : partial;
467
+ const matches = Object.keys(state.typeAliases)
468
+ .sort()
469
+ .filter((name) => name.startsWith(barePartial))
470
+ .map((name) => `${prefix}${isTypeToken ? "$" : ""}${name}`);
471
+ return [matches, line];
472
+ }
473
+
394
474
  function completePath(line, tokenStart) {
395
475
  const token = line.slice(tokenStart);
396
476
  const quote = token[0] === "\"" || token[0] === "'" ? token[0] : "";
@@ -449,6 +529,75 @@ function loadPathCompletionStart(argStart, arg) {
449
529
  return argStart + restStart + restLeadingWhitespace + completionTokenStart(rest.trimStart());
450
530
  }
451
531
 
532
+ function completeCommandWord(line, argStart, arg, words) {
533
+ const leadingWhitespace = arg.length - arg.trimStart().length;
534
+ const trimmed = arg.trimStart();
535
+ if (/\s/.test(trimmed)) return [[], line];
536
+ const prefix = line.slice(0, argStart + leadingWhitespace);
537
+ const matches = words
538
+ .filter((word) => word.startsWith(trimmed))
539
+ .map((word) => `${prefix}${word}`);
540
+ return [matches, line];
541
+ }
542
+
543
+ function codecLoadPathCompletionStart(argStart, arg) {
544
+ const match = arg.match(/^(\s*)load(?:\s+(.*))?$/);
545
+ if (!match) return null;
546
+ if (match[2] == null) return null;
547
+ const [, leading, rest] = match;
548
+ const restLeadingWhitespace = rest.length - rest.trimStart().length;
549
+ return argStart + leading.length + "load".length + 1 + restLeadingWhitespace + completionTokenStart(rest.trimStart());
550
+ }
551
+
552
+ function completeCodecCommand(line, argStart, arg) {
553
+ const trimmed = arg.trimStart();
554
+ if (trimmed === "" || !/\s/.test(trimmed)) {
555
+ return completeCommandWord(line, argStart, arg, CODEC_COMMAND_NAMES);
556
+ }
557
+
558
+ const tokenStart = codecLoadPathCompletionStart(argStart, arg);
559
+ if (tokenStart == null) return [[], line];
560
+ return completePath(line, tokenStart);
561
+ }
562
+
563
+ function resolveTypeHash(state, rawName) {
564
+ const token = rawName.trim();
565
+ const bareName = token.startsWith("$") ? token.slice(1) : token;
566
+ const hash = state.typeAliases[bareName] || (token.startsWith("@") ? token : null);
567
+ if (!hash || !(hash in state.codes)) throw new Error(`Unknown type '${rawName}'`);
568
+ return hash;
569
+ }
570
+
571
+ function completeInputCommand(line, state, argStart, arg) {
572
+ const match = arg.match(/^(\s*)(\S+)?(\s+)?(\S*)?$/);
573
+ if (!match) return [[], line];
574
+ const [, leading, typeToken = "", afterTypeWhitespace = "", codecPartial = ""] = match;
575
+
576
+ if (!typeToken || !afterTypeWhitespace) {
577
+ if (/@[A-Za-z0-9_?!+-]*$/.test(line)) return completeCanonicalCode(line, state);
578
+ return completeTypeIdentifier(line, state);
579
+ }
580
+
581
+ if (typeToken) {
582
+ const prefix = line.slice(0, argStart + leading.length + typeToken.length + afterTypeWhitespace.length);
583
+ const names = (() => {
584
+ try {
585
+ return codecNames(state, resolveTypeHash(state, typeToken));
586
+ } catch {
587
+ return codecNames(state);
588
+ }
589
+ })();
590
+ return [
591
+ names
592
+ .filter((name) => name.startsWith(codecPartial))
593
+ .map((name) => `${prefix}${name}`),
594
+ line
595
+ ];
596
+ }
597
+
598
+ return [[], line];
599
+ }
600
+
452
601
  function completeCommandArgument(line, state) {
453
602
  const body = line.slice(1);
454
603
  const firstSpace = body.search(/\s/);
@@ -458,6 +607,14 @@ function completeCommandArgument(line, state) {
458
607
  const argStart = 1 + firstSpace + 1;
459
608
  const arg = line.slice(argStart);
460
609
 
610
+ if (command === "codec") {
611
+ return completeCodecCommand(line, argStart, arg);
612
+ }
613
+
614
+ if (command === "input") {
615
+ return completeInputCommand(line, state, argStart, arg);
616
+ }
617
+
461
618
  if (PATH_COMMANDS.has(command)) {
462
619
  const tokenStart = command === "load"
463
620
  ? loadPathCompletionStart(argStart, arg)
@@ -659,14 +816,14 @@ async function runExpression(input, state) {
659
816
  }
660
817
  const mainRel = annotated.rels.__main__;
661
818
  run.defs = annotated;
819
+ let result;
662
820
  try {
663
- state.value = run(codes.find, mainRel.def, state.value, mainRel.typePatternGraph);
821
+ result = run(codes.find, mainRel.def, state.value, mainRel.typePatternGraph);
664
822
  } catch (error) {
665
823
  throw remapError(error, preambleLineCount(preamble));
666
824
  }
667
- state.lastMain = expression;
668
825
  restoreCodes(state);
669
- return [printValue(state.value)];
826
+ return commitResult(state, result, expression);
670
827
  }
671
828
 
672
829
  async function runSnippet(input, state, options = {}) {
@@ -685,17 +842,69 @@ async function runSnippet(input, state, options = {}) {
685
842
 
686
843
  const mainRel = annotated.rels.__main__;
687
844
  run.defs = annotated;
845
+ let result;
688
846
  try {
689
- state.value = run(codes.find, mainRel.def, state.value, mainRel.typePatternGraph);
847
+ result = run(codes.find, mainRel.def, state.value, mainRel.typePatternGraph);
690
848
  } catch (error) {
691
849
  throw remapError(error, lineOffset);
692
850
  }
693
- state.lastMain = snippet;
694
851
  restoreCodes(state);
695
- return [printValue(state.value)];
852
+ return commitResult(state, result, snippet);
853
+ }
854
+
855
+ async function loadCodec(input, state) {
856
+ const [subcommand, ...rest] = input.trim().split(/\s+/);
857
+ switch (subcommand) {
858
+ case "load": {
859
+ const filePath = rest.join(" ");
860
+ if (!filePath) throw new Error(":codec load requires a file path");
861
+ const registered = await loadCodecModule(state, expandHome(filePath));
862
+ return registered.map(({ name, codeHash }) => `loaded codec ${name} for ${codeHash}`);
863
+ }
864
+ case "list":
865
+ if (rest.length > 0) throw new Error(":codec list does not accept arguments");
866
+ return [listCodecs(state)];
867
+ default:
868
+ throw new Error(":codec requires 'load' or 'list'");
869
+ }
870
+ }
871
+
872
+ async function requestCodecInput(input, state) {
873
+ const parts = input.trim().split(/\s+/).filter(Boolean);
874
+ if (parts.length === 0 || parts.length > 2) {
875
+ throw new Error(":input requires a singleton type and optional codec name");
876
+ }
877
+
878
+ const codeHash = resolveTypeHash(state, parts[0]);
879
+ const codecName = parts[1] || null;
880
+ resolveCodec(state, codeHash, codecName, "parse");
881
+ state.pendingInput = { codeHash, codecName };
882
+ return [`input ${codeHash}${codecName ? ` using ${codecName}` : ""}: enter value text`];
883
+ }
884
+
885
+ async function consumeCodecInput(input, state) {
886
+ const pending = state.pendingInput;
887
+ if (!pending) return null;
888
+ state.pendingInput = null;
889
+
890
+ restoreCodes(state);
891
+ const codec = resolveCodec(state, pending.codeHash, pending.codecName, "parse");
892
+ const pattern = codeHashToPattern(pending.codeHash, codes.find);
893
+ const parsed = await codec.parse(input, {
894
+ codeHash: pending.codeHash,
895
+ pattern,
896
+ state
897
+ });
898
+ const value = valueForCode(parsed, pending.codeHash, codes.find);
899
+ state.value = value;
900
+ return [printValue(value, state)];
696
901
  }
697
902
 
698
903
  async function evaluateInput(input, state) {
904
+ if (state.pendingInput) {
905
+ return consumeCodecInput(input, state);
906
+ }
907
+
699
908
  const line = input.trim();
700
909
  if (line === "" || line.startsWith("#") || line.startsWith("//") || line.startsWith("--")) return [];
701
910
 
@@ -745,9 +954,13 @@ async function evaluateCommand(line, state) {
745
954
  return [listAliases(state.relAliases)];
746
955
  case "val":
747
956
  return [
748
- printValue(state.value),
957
+ printValue(state.value, state),
749
958
  JSON.stringify(state.value, null, 2)
750
959
  ];
960
+ case "codec":
961
+ return loadCodec(arg, state);
962
+ case "input":
963
+ return requestCodecInput(arg, state);
751
964
  case "reset": {
752
965
  const fresh = createState();
753
966
  Object.assign(state, fresh);
@@ -803,13 +1016,13 @@ async function evaluateCommand(line, state) {
803
1016
  if (!arg) throw new Error(`${usagePrefix}d requires a relation name`);
804
1017
  const { hash, rel } = resolveRel(state, arg);
805
1018
  if (!rel) throw new Error(`Unknown relation '${arg}'`);
806
- return [`${arg} = ${prettyRelation(rel)}; -- ${hash}`];
1019
+ return [`${arg} = ${prettyRelation(rel, displayAliases(state), state.relAliases)}; -- ${hash}`];
807
1020
  }
808
1021
  case "C": {
809
1022
  if (!arg) throw new Error(`${usagePrefix}C requires a type name`);
810
1023
  const hash = state.typeAliases[arg] || (arg.startsWith("@") ? arg : null);
811
1024
  if (!hash || !(hash in state.codes)) throw new Error(`Unknown type '${arg}'`);
812
- return [`$ ${arg} = ${prettyCode({}, codes.find, codes.find(hash))}; -- ${hash}`];
1025
+ return [`$ ${arg} = ${prettyCode(displayAliases(state), codes.find, codes.find(hash))}; -- ${hash}`];
813
1026
  }
814
1027
  default:
815
1028
  throw new Error(`Unknown command ':${command}'. Try :help`);
@@ -826,6 +1039,9 @@ function helpText() {
826
1039
  ":type name show type definition",
827
1040
  ":codes list type aliases",
828
1041
  ":rels list relation aliases",
1042
+ ":codec load file load a REPL codec module",
1043
+ ":codec list list loaded codecs",
1044
+ ":input type [codec] read next line as codec input",
829
1045
  ":load [--no-alias] file",
830
1046
  " load .k source or .klib",
831
1047
  ":klib file export state as a library",
@@ -865,6 +1081,19 @@ function startRepl() {
865
1081
  console.log("k interpreter (.klib-backed). Type :help for commands.");
866
1082
  rl.prompt();
867
1083
  async function handleLine(line) {
1084
+ if (state.pendingInput && buffer.length === 0) {
1085
+ rl.setPrompt("> ");
1086
+ try {
1087
+ for (const output of await consumeCodecInput(line, state)) {
1088
+ console.log(output);
1089
+ }
1090
+ } catch (error) {
1091
+ console.error(error.message || String(error));
1092
+ }
1093
+ if (!closed) rl.prompt();
1094
+ return;
1095
+ }
1096
+
868
1097
  if (lineHasExplicitContinuation(line)) {
869
1098
  buffer.push(lineForContinuation(line));
870
1099
  rl.setPrompt(" ");
package/run.mjs CHANGED
@@ -122,6 +122,20 @@ function constrainWithPattern(value, constraint, exp) {
122
122
  return withPattern(value, pattern);
123
123
  }
124
124
 
125
+ function projectionPattern(staticOutputPattern, valuePattern, label, exp) {
126
+ const dynamicOutputPattern = edgeSubpattern(valuePattern, label);
127
+ const pattern = intersectPatterns(staticOutputPattern, dynamicOutputPattern);
128
+ if (!pattern && staticOutputPattern && dynamicOutputPattern) {
129
+ throw new TypeError(
130
+ `Type Error in '${exp.op}'${expLocation(exp)}\n` +
131
+ ` - Projected value envelope does not intersect expression output pattern.\n` +
132
+ ` - output pattern: ${patternPreview(staticOutputPattern)}\n` +
133
+ ` - projected envelope: ${patternPreview(dynamicOutputPattern)}`
134
+ );
135
+ }
136
+ return pattern;
137
+ }
138
+
125
139
  function compiledExp(findCode, exp, typePatternGraph) {
126
140
  const cached = exp._compiledRun;
127
141
  if (cached?.findCode === findCode && cached?.typePatternGraph === typePatternGraph && cached?.defs === run.defs) {
@@ -188,13 +202,13 @@ function compiledExp(findCode, exp, typePatternGraph) {
188
202
  case "dot":
189
203
  fn = (value) => {
190
204
  if (value === undefined) return undefined;
191
- return withPattern(value.product[exp.dot], staticOutputPattern || edgeSubpattern(value.pattern, exp.dot));
205
+ return withPattern(value.product[exp.dot], projectionPattern(staticOutputPattern, value.pattern, exp.dot, exp));
192
206
  };
193
207
  break;
194
208
  case "div":
195
209
  fn = (value) => {
196
210
  if (value === undefined || value.tag !== exp.div) return undefined;
197
- return withPattern(value.value, staticOutputPattern || edgeSubpattern(value.pattern, exp.div));
211
+ return withPattern(value.value, projectionPattern(staticOutputPattern, value.pattern, exp.div, exp));
198
212
  };
199
213
  break;
200
214
  case "comp": {