@fraczak/k 6.3.2 → 6.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DOCS/REPL.md CHANGED
@@ -47,7 +47,7 @@ complete.
47
47
  | `:rels` | List relation aliases |
48
48
  | `:codec load file` | Load an importable REPL codec module |
49
49
  | `:codec list` | List loaded REPL codecs |
50
- | `:input type [codec]` | Read the next line as codec input for a singleton type |
50
+ | `:input type [codec]` | Read the next line as codec input for a type |
51
51
  | `:load [--no-alias] file` | Load `.k` source or `.klib` into the current state |
52
52
  | `:klib file` | Export the active relation closure as a `.klib` library |
53
53
  | `:ko file expr` | Export a `.ko` executable with `expr` as main |
@@ -139,7 +139,7 @@ Tab completion covers:
139
139
  Type aliases also complete in `$name` position inside raw k input.
140
140
 
141
141
  For codec commands, completion covers `:codec load`, `:codec list`, file paths
142
- after `:codec load`, singleton type names after `:input`, and loaded codec names
142
+ after `:codec load`, type names after `:input`, and loaded codec names
143
143
  after the input type.
144
144
 
145
145
  ## Loading
@@ -158,8 +158,10 @@ unless `--no-alias` is used.
158
158
 
159
159
  ## Codecs
160
160
 
161
- REPL codecs are keyed by canonical singleton code hashes. Open filters are not
162
- used for codec dispatch.
161
+ REPL codecs are usually keyed by canonical code hashes. Patterns are not used
162
+ for codec dispatch. A codec may also export `universal = true` to make it
163
+ available for any type selected by `:input`; the parsed value is still validated
164
+ against the requested type before it enters the session.
163
165
 
164
166
  ### `:codec load file`
165
167
 
@@ -172,15 +174,18 @@ export function parse(text) { /* text -> Value */ }
172
174
  export function print(value) { /* Value -> text */ }
173
175
  ```
174
176
 
175
- A codec may export `patterns` instead of `codes`; each pattern must be a
176
- singleton closed property-list pattern, and its code hash is recalculated by the
177
- REPL.
177
+ A codec may export `patterns` instead of `codes`; each pattern must be a closed
178
+ property-list pattern that can be canonicalized to a code hash, and that hash is
179
+ recalculated by the REPL. A universal codec exports `universal = true` instead
180
+ of `codes` or `patterns`.
178
181
 
179
182
  ### `:input type [codec]`
180
183
 
181
- Resolves `type` to a canonical singleton code, selects the registered codec,
182
- and consumes the next line verbatim as codec input. The parsed value is validated
183
- against that code before becoming the current value.
184
+ Resolves `type` to a canonical code hash, selects the registered codec, and
185
+ consumes the next line verbatim as codec input. `type` may be a type alias, a
186
+ canonical code hash, or an inline type expression such as
187
+ `<{} true, {} false>`. The parsed value is validated against that type before
188
+ becoming the current value.
184
189
 
185
190
  ## Export
186
191
 
package/codecs/json.mjs CHANGED
@@ -5,6 +5,9 @@ import { decodeWire, encodeToWire } from "./runtime/prefix-codec.mjs";
5
5
  import { isMainEntrypoint } from "./runtime/cli-entry.mjs";
6
6
  import { fromJsonValue, toJsonValue, patternFromJsonValue } from "./json-codec.mjs";
7
7
 
8
+ const name = "json";
9
+ const universal = true;
10
+
8
11
  function usage(stream = console.error) {
9
12
  stream(`Usage: ${argv[1]} --parse | --print`);
10
13
  stream(" --parse Read JSON from stdin, write binary pattern+value stream.");
@@ -21,6 +24,14 @@ function readAll(stream) {
21
24
  });
22
25
  }
23
26
 
27
+ function parse(text) {
28
+ return fromJsonValue(JSON.parse(text));
29
+ }
30
+
31
+ function print(value) {
32
+ return JSON.stringify(toJsonValue(value));
33
+ }
34
+
24
35
  async function main() {
25
36
  const args = argv.slice(2);
26
37
  if (args.includes("-h") || args.includes("--help")) {
@@ -36,12 +47,13 @@ async function main() {
36
47
  const buf = await readAll(stdin);
37
48
 
38
49
  if (args[0] === "--parse") {
39
- const json = JSON.parse(buf.toString("utf8"));
50
+ const text = buf.toString("utf8");
51
+ const json = JSON.parse(text);
40
52
  const value = fromJsonValue(json);
41
53
  stdout.write(encodeToWire(value, patternFromJsonValue(json)));
42
54
  } else {
43
55
  const { value } = decodeWire(buf);
44
- stdout.write(`${JSON.stringify(toJsonValue(value))}\n`);
56
+ stdout.write(`${print(value)}\n`);
45
57
  }
46
58
  }
47
59
 
@@ -51,3 +63,5 @@ if (isMainEntrypoint(import.meta.url, argv[1])) {
51
63
  exit(1);
52
64
  });
53
65
  }
66
+
67
+ export { name, universal, parse, print };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fraczak/k",
3
- "version": "6.3.2",
3
+ "version": "6.3.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": {
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
  };
@@ -4,7 +4,7 @@ import { createState, evaluateInput } from "../repl.mjs";
4
4
  import { Value, isProduct } from "../Value.mjs";
5
5
  import codes from "../codes.mjs";
6
6
  import { parse as parseFloat64, print as printFloat64 } from "../codecs/ieee.mjs";
7
- import { singletonPatternToCodeHash, valueForCode } from "../repl-codecs.mjs";
7
+ import { closedPatternToCodeHash, valueForCode } from "../repl-codecs.mjs";
8
8
 
9
9
  const state = createState();
10
10
  await evaluateInput(":load Examples/ieee.k", state);
@@ -40,7 +40,7 @@ async function runProjectedResult(op, x, y) {
40
40
  }
41
41
 
42
42
  function assertFloatResult(value, expected, label) {
43
- assert.equal(singletonPatternToCodeHash(value.pattern), float64Hash, `${label}: result should keep float64 envelope`);
43
+ assert.equal(closedPatternToCodeHash(value.pattern), float64Hash, `${label}: result should keep float64 envelope`);
44
44
  assert.equal(printFloat64(value), expected, label);
45
45
  }
46
46
 
@@ -15,7 +15,8 @@ import {
15
15
  helpText,
16
16
  isMainEntrypoint,
17
17
  lineHasExplicitContinuation,
18
- lineTerminatesSnippet
18
+ lineTerminatesSnippet,
19
+ promptForState
19
20
  } from "../repl.mjs";
20
21
  import { decodeObject, objectToFunction } from "../object.mjs";
21
22
  import { Value } from "../Value.mjs";
@@ -194,6 +195,36 @@ assert.equal(output[0], `input ${utf8State.typeAliases.string} using utf8: enter
194
195
  output = await evaluateInput("hello", utf8State);
195
196
  assert.match(output[0], /utf8: hello/);
196
197
 
198
+ const jsonState = createState();
199
+ output = await evaluateInput(":type bool = <{} true, {} false>", jsonState);
200
+ assert.match(output[0], /^\$ bool = @/);
201
+ output = await evaluateInput(":codec load ./codecs/json.mjs", jsonState);
202
+ assert.equal(output[0], "loaded codec json for all types");
203
+ output = await evaluateInput(":codec list", jsonState);
204
+ assert.match(output[0], /^json all /);
205
+ completions = completeInput(":input bool j", jsonState)[0];
206
+ assert(completions.includes(":input bool json"));
207
+ output = await evaluateInput(":input bool", jsonState);
208
+ assert.equal(output[0], `input ${jsonState.typeAliases.bool}: enter value text`);
209
+ assert.equal(promptForState(jsonState), "json> ");
210
+ output = await evaluateInput("true", jsonState);
211
+ assert.match(output[0], /\{\}\|true \?</);
212
+ assert.match(output[0], /json: true/);
213
+ assert.equal(promptForState(jsonState), "> ");
214
+ output = await evaluateInput(":input <{}true,{}false>", jsonState);
215
+ assert.equal(output[0], `input ${jsonState.typeAliases.bool}: enter value text`);
216
+ assert.equal(promptForState(jsonState), "json> ");
217
+ output = await evaluateInput("false", jsonState);
218
+ assert.match(output[0], /\{\}\|false \?</);
219
+ assert.match(output[0], /json: false/);
220
+ assert.equal(promptForState(jsonState), "> ");
221
+ output = await evaluateInput(":input {bool left, bool right} json", jsonState);
222
+ assert.match(output[0], /^input @[^ ]+ using json: enter value text$/);
223
+ assert.equal(promptForState(jsonState), "json> ");
224
+ output = await evaluateInput('{"left":true,"right":false}', jsonState);
225
+ assert.match(output[0], /json: \{"left":true,"right":false\}/);
226
+ assert.equal(promptForState(jsonState), "> ");
227
+
197
228
  const replPath = fileURLToPath(new URL("../repl.mjs", import.meta.url));
198
229
  try {
199
230
  fs.rmSync(symlinkPath, { force: true });