@fraczak/k 6.0.1 → 6.0.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.
Files changed (38) hide show
  1. package/AGENTS.md +35 -0
  2. package/CHANGELOG.md +48 -0
  3. package/CLAUDE.md +64 -0
  4. package/DOCS/BIG_PICTURE.md +4 -8
  5. package/DOCS/BINARY_CODEC_STATUS_AND_TODO.md +74 -0
  6. package/DOCS/CONVERGENCE.md +92 -0
  7. package/DOCS/DIVERGENCE_DETECTION.md +24 -0
  8. package/DOCS/Makefile +99 -0
  9. package/DOCS/OBJECT_FILE_AND_PATTERN.md +217 -0
  10. package/DOCS/PATTERNS.md +114 -0
  11. package/DOCS/PATTERNS_AND_TYPED_VALUES.md +665 -0
  12. package/DOCS/REPL.md +125 -125
  13. package/DOCS/TEXTUAL_VALUES.md +108 -0
  14. package/DOCS/TYPE_DERIVATION.md +230 -0
  15. package/DOCS/book/06-values-in-memory.md +1085 -0
  16. package/DOCS/book/07-the-partial-function-abi.md +143 -0
  17. package/DOCS/book/08-operational-semantics-and-execution.md +201 -0
  18. package/DOCS/book/09-compiler-architecture.md +160 -0
  19. package/DOCS/book/10-from-ast-to-intermediate-representation.md +167 -0
  20. package/DOCS/book/14-canonical-serialization.md +176 -0
  21. package/DOCS/book/appendix-b-incorporating-external-predefined-types-and-functions.md +155 -0
  22. package/DOCS/book/overview.txt +68 -0
  23. package/README.md +71 -27
  24. package/codecs/README.md +8 -0
  25. package/codecs/pandoc.css +71 -0
  26. package/codecs/runtime/test-codec.mjs +89 -0
  27. package/package.json +2 -2
  28. package/{codecs/k-pattern.mjs → patterns/from-k.mjs} +3 -3
  29. package/repl2.mjs +259 -50
  30. package/scripts/generate-dependency-graph.mjs +121 -0
  31. package/test-repl2.mjs +55 -8
  32. package/DOCS/book-combined.html +0 -3798
  33. package/DOCS/book-combined.md +0 -3745
  34. package/DOCS/book-combined.pdf +0 -0
  35. package/DOCS/book-combined.tex +0 -5682
  36. package/DOCS/book.html +0 -65
  37. package/DOCS/book.pdf +0 -0
  38. package/DOCS/book.tex +0 -150
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Test the binary codec with basic k types
4
+ */
5
+
6
+ import { deriveClosedPattern } from './codec.mjs';
7
+ import { encodeToWire, decodeWire } from './prefix-codec.mjs';
8
+ import { patternToPropertyList } from './pattern-json.mjs';
9
+ import { Product, Variant } from '../../Value.mjs';
10
+ import hash from '../../hash.mjs';
11
+
12
+ // Type repository
13
+ const types = {};
14
+
15
+ // Define unit type: {}
16
+ const unitTypeDef = { code: 'product', product: {} };
17
+ const unitTypeStr = '$C0={};';
18
+ const unitTypeName = hash(unitTypeStr);
19
+ types[unitTypeName] = unitTypeDef;
20
+
21
+ console.log('Unit type name:', unitTypeName);
22
+
23
+ // Define bool type: <{} false, {} true>
24
+ const boolTypeDef = {
25
+ code: 'union',
26
+ union: {
27
+ false: unitTypeName,
28
+ true: unitTypeName
29
+ }
30
+ };
31
+ const boolTypeStr = `$C0=<${unitTypeName}"false",${unitTypeName}"true">;`;
32
+ const boolTypeName = hash(boolTypeStr);
33
+ types[boolTypeName] = boolTypeDef;
34
+
35
+ console.log('Bool type name:', boolTypeName);
36
+
37
+ // Resolver function
38
+ function resolveType(typeName) {
39
+ if (!types[typeName]) {
40
+ throw new Error(`Unknown type: ${typeName}`);
41
+ }
42
+ return types[typeName];
43
+ }
44
+
45
+ // Test 1: Unit value
46
+ console.log('\n=== Test 1: Unit ===');
47
+ const unitValue = new Product({});
48
+ console.log('Encoding unit:', unitValue.toString());
49
+
50
+ const unitPattern = patternToPropertyList(deriveClosedPattern(unitTypeName, unitTypeDef, resolveType));
51
+ const unitEncoded = encodeToWire(unitValue, unitPattern);
52
+ console.log('Encoded bytes:', unitEncoded.length, 'bytes');
53
+ console.log('Hex:', unitEncoded.toString('hex'));
54
+
55
+ const unitDecoded = decodeWire(unitEncoded);
56
+ console.log('Decoded value:', unitDecoded.value.toString());
57
+ console.log('Match:', unitDecoded.value instanceof Product &&
58
+ Object.keys(unitDecoded.value.product).length === 0);
59
+
60
+ // Test 2: Bool false
61
+ console.log('\n=== Test 2: Bool (false) ===');
62
+ const falseValue = new Variant('false', new Product({}));
63
+ console.log('Encoding false:', falseValue.toString());
64
+
65
+ const boolPattern = patternToPropertyList(deriveClosedPattern(boolTypeName, boolTypeDef, resolveType));
66
+ const falseEncoded = encodeToWire(falseValue, boolPattern);
67
+ console.log('Encoded bytes:', falseEncoded.length, 'bytes');
68
+ console.log('Hex:', falseEncoded.toString('hex'));
69
+
70
+ const falseDecoded = decodeWire(falseEncoded);
71
+ console.log('Decoded value:', falseDecoded.value.toString());
72
+ console.log('Match:', falseDecoded.value instanceof Variant &&
73
+ falseDecoded.value.tag === 'false');
74
+
75
+ // Test 3: Bool true
76
+ console.log('\n=== Test 3: Bool (true) ===');
77
+ const trueValue = new Variant('true', new Product({}));
78
+ console.log('Encoding true:', trueValue.toString());
79
+
80
+ const trueEncoded = encodeToWire(trueValue, boolPattern);
81
+ console.log('Encoded bytes:', trueEncoded.length, 'bytes');
82
+ console.log('Hex:', trueEncoded.toString('hex'));
83
+
84
+ const trueDecoded = decodeWire(trueEncoded);
85
+ console.log('Decoded value:', trueDecoded.value.toString());
86
+ console.log('Match:', trueDecoded.value instanceof Variant &&
87
+ trueDecoded.value.tag === 'true');
88
+
89
+ console.log('\n=== All tests complete ===');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fraczak/k",
3
- "version": "6.0.1",
3
+ "version": "6.0.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": {
@@ -19,7 +19,7 @@
19
19
  "k-compile-object": "./objects/compile.mjs",
20
20
  "k-decompile-object": "./objects/decompile.mjs",
21
21
  "k-parse": "./codecs/k-parse.mjs",
22
- "k-pattern": "./codecs/k-pattern.mjs",
22
+ "k-pattern": "./patterns/from-k.mjs",
23
23
  "k-print": "./codecs/k-print.mjs",
24
24
  "k-repl": "./repl2.mjs",
25
25
  "k-repl2": "./repl2.mjs"
@@ -3,14 +3,14 @@
3
3
  import fs from "node:fs";
4
4
  import { argv, exit, stdin, stdout } from "node:process";
5
5
  import k from "../index.mjs";
6
- import { exportPatternGraph } from "./runtime/codec.mjs";
7
- import { patternToPropertyList } from "./runtime/pattern-json.mjs";
6
+ import { exportPatternGraph } from "../codecs/runtime/codec.mjs";
7
+ import { patternToPropertyList } from "../codecs/runtime/pattern-json.mjs";
8
8
 
9
9
  function usage(prog) {
10
10
  console.error(`Usage: ${prog} [script-or-file]`);
11
11
  console.error(" Read a k script from a file argument or stdin.");
12
12
  console.error(" The main expression must be a filter or a type name.");
13
- console.error(" Output is the canonical JSON pattern array.");
13
+ console.error(" Output is the canonical property-list pattern array.");
14
14
  console.error(" Example:");
15
15
  console.error(` cat pattern.k | ${prog}`);
16
16
  console.error(` ${prog} '$ bits = < {} _, bits 0, bits 1 >; ?< {} nil, { $bits car, X cdr } cons > = X'`);
package/repl2.mjs CHANGED
@@ -28,10 +28,10 @@ const TYPE_DEF_RE = /^\s*\$\s*([a-zA-Z0-9_+-][a-zA-Z0-9_?!+-]*)\s*=/;
28
28
  const REL_DEF_RE = /^\s*([a-zA-Z0-9_+-][a-zA-Z0-9_?!+-]*)\s*=/;
29
29
  const COMMAND_NAMES = [
30
30
  "help", "type", "code", "rel", "def", "run", "eval", "t", "d", "C",
31
- "codes", "rels", "val", "reset", "save", "klib", "ko", "export", "load",
31
+ "codes", "rels", "val", "reset", "klib", "ko", "load",
32
32
  "quit", "exit"
33
33
  ];
34
- const PATH_COMMANDS = new Set(["save", "klib", "ko", "export", "load"]);
34
+ const PATH_COMMANDS = new Set(["klib", "ko", "load"]);
35
35
  const initialCodes = codes.dump();
36
36
 
37
37
  function cloneJSON(value) {
@@ -88,12 +88,49 @@ function aliasPreamble(state, omitName = null) {
88
88
  return lines.filter(Boolean).join("\n");
89
89
  }
90
90
 
91
+ function preambleLineCount(preamble) {
92
+ if (!preamble) return 0;
93
+ return preamble.split("\n").length;
94
+ }
95
+
96
+ function remapLineNumber(line, offset) {
97
+ return Math.max(1, line - offset);
98
+ }
99
+
100
+ function remapDiagnosticMessage(message, lineOffset) {
101
+ if (!lineOffset) return message;
102
+
103
+ let rewritten = message.replace(
104
+ /\(lines (\d+):(\d+)\.\.\.(\d+):(\d+)\)/g,
105
+ (_, startLine, startCol, endLine, endCol) =>
106
+ `(lines ${remapLineNumber(Number(startLine), lineOffset)}:${startCol}...${remapLineNumber(Number(endLine), lineOffset)}:${endCol})`
107
+ );
108
+
109
+ rewritten = rewritten.replace(
110
+ /Parse error on line (\d+):/g,
111
+ (_, line) => `Parse error on line ${remapLineNumber(Number(line), lineOffset)}:`
112
+ );
113
+
114
+ return rewritten;
115
+ }
116
+
117
+ function remapError(error, lineOffset) {
118
+ if (!lineOffset || !error?.message) return error;
119
+ const remapped = new Error(remapDiagnosticMessage(error.message, lineOffset));
120
+ remapped.name = error.name;
121
+ if (error.stack) {
122
+ remapped.stack = error.stack.replace(error.message, remapped.message);
123
+ }
124
+ if ("cause" in error) remapped.cause = error.cause;
125
+ return remapped;
126
+ }
127
+
91
128
  function compileWithOptionalIdentity(source, options) {
92
129
  try {
93
130
  return hydrateObject(compileLibrary(source, options));
94
131
  } catch (error) {
95
132
  if (!/got 'EOF'|Expecting/.test(error.message)) throw error;
96
- return hydrateObject(compileLibrary(`${source}\n()`, options));
133
+ return hydrateObject(compileLibrary(`${ensureSemicolon(source)}\n()`, options));
97
134
  }
98
135
  }
99
136
 
@@ -102,7 +139,7 @@ function annotateWithOptionalIdentity(source, options) {
102
139
  return annotate(source, options);
103
140
  } catch (error) {
104
141
  if (!/got 'EOF'|Expecting/.test(error.message)) throw error;
105
- return annotate(`${source}\n()`, options);
142
+ return annotate(`${ensureSemicolon(source)}\n()`, options);
106
143
  }
107
144
  }
108
145
 
@@ -183,15 +220,18 @@ function savedLibrary(state) {
183
220
  function executableObject(state, mainExpression) {
184
221
  const main = (mainExpression || state.lastMain || "").trim();
185
222
  if (!main) {
186
- throw new Error(":ko requires a main expression unless one has already been evaluated or defined");
223
+ throw new Error(":ko file expr requires a main expression unless one has already been evaluated or defined");
187
224
  }
188
225
 
189
226
  restoreCodes(state);
227
+ const preamble = aliasPreamble(state);
190
228
  try {
191
- return compileObject([aliasPreamble(state), main].filter(Boolean).join("\n"), {
229
+ return compileObject([preamble, main].filter(Boolean).join("\n"), {
192
230
  source: "<repl>",
193
231
  libraries: [stateLibrary(state)]
194
232
  });
233
+ } catch (error) {
234
+ throw remapError(error, preambleLineCount(preamble));
195
235
  } finally {
196
236
  restoreCodes(state);
197
237
  }
@@ -272,6 +312,13 @@ function canonicalNames(state) {
272
312
  ])].filter((name) => name.startsWith("@")).sort();
273
313
  }
274
314
 
315
+ function aliasNames(state) {
316
+ return [...new Set([
317
+ ...Object.keys(state.typeAliases),
318
+ ...Object.keys(state.relAliases)
319
+ ])].sort();
320
+ }
321
+
275
322
  function expandHome(input) {
276
323
  if (input === "~") return os.homedir();
277
324
  if (input.startsWith("~/")) return path.join(os.homedir(), input.slice(2));
@@ -303,6 +350,19 @@ function completeCanonical(line, state) {
303
350
  return [matches, line];
304
351
  }
305
352
 
353
+ function completeIdentifier(line, state) {
354
+ const match = line.match(/^(.*?)(\$?[A-Za-z0-9_+-][A-Za-z0-9_?!+-]*)$/);
355
+ if (!match) return [[], line];
356
+ const [, prefix, partial] = match;
357
+ const isTypeToken = partial.startsWith("$");
358
+ const barePartial = isTypeToken ? partial.slice(1) : partial;
359
+ const names = isTypeToken ? Object.keys(state.typeAliases).sort() : aliasNames(state);
360
+ const matches = names
361
+ .filter((name) => name.startsWith(barePartial))
362
+ .map((name) => `${prefix}${isTypeToken ? "$" : ""}${name}`);
363
+ return [matches, line];
364
+ }
365
+
306
366
  function completePath(line, tokenStart) {
307
367
  const token = line.slice(tokenStart);
308
368
  const quote = token[0] === "\"" || token[0] === "'" ? token[0] : "";
@@ -359,6 +419,10 @@ function completeCommandArgument(line, state) {
359
419
  return completeCanonical(line, state);
360
420
  }
361
421
 
422
+ if (/\$?[A-Za-z0-9_+-][A-Za-z0-9_?!+-]*$/.test(line)) {
423
+ return completeIdentifier(line, state);
424
+ }
425
+
362
426
  return [[], line];
363
427
  }
364
428
 
@@ -369,6 +433,9 @@ function completeInput(line, state) {
369
433
  if (/@[A-Za-z0-9_?!+-]*$/.test(line)) {
370
434
  return completeCanonical(line, state);
371
435
  }
436
+ if (/\$?[A-Za-z0-9_+-][A-Za-z0-9_?!+-]*$/.test(line)) {
437
+ return completeIdentifier(line, state);
438
+ }
372
439
  return [[], line];
373
440
  }
374
441
 
@@ -389,6 +456,76 @@ function ensureSemicolon(source) {
389
456
  return source.trim().endsWith(";") ? source : `${source};`;
390
457
  }
391
458
 
459
+ function isEofParseError(error) {
460
+ return /got 'EOF'|Unexpected end of input/.test(error.message || "");
461
+ }
462
+
463
+ function lineForContinuation(line) {
464
+ return line.replace(/\\\s*$/, "");
465
+ }
466
+
467
+ function lineHasExplicitContinuation(line) {
468
+ return /\\\s*$/.test(line);
469
+ }
470
+
471
+ function lineTerminatesSnippet(line) {
472
+ return /;\s*$/.test(line);
473
+ }
474
+
475
+ function explicitSnippetTerminated(source) {
476
+ const lines = source.split("\n");
477
+ const lastLine = lines[lines.length - 1] || "";
478
+ return lineTerminatesSnippet(lastLine);
479
+ }
480
+
481
+ function analyzeRawSnippet(source) {
482
+ try {
483
+ const parsed = parse(source);
484
+ return { kind: "withMain", parsed };
485
+ } catch (error) {
486
+ if (!isEofParseError(error)) throw error;
487
+ }
488
+
489
+ return { kind: "incomplete", parsed: null };
490
+ }
491
+
492
+ function analyzeAcceptedSnippet(source, explicitTerminated = explicitSnippetTerminated(source)) {
493
+ try {
494
+ const parsed = parse(source);
495
+ return { kind: "withMain", parsed };
496
+ } catch (error) {
497
+ if (!explicitTerminated) {
498
+ if (isEofParseError(error)) return { kind: "incomplete", parsed: null };
499
+ throw error;
500
+ }
501
+ }
502
+
503
+ const parsed = parse(`${source}\n()`);
504
+ return { kind: "definitionsOnly", parsed };
505
+ }
506
+
507
+ function compileSnippetArtifacts(source, state, sourceName = "<repl>") {
508
+ restoreCodes(state);
509
+ const preamble = aliasPreamble(state);
510
+ const fullSource = [preamble, source].filter(Boolean).join("\n");
511
+ const options = {
512
+ source: sourceName,
513
+ libraries: [stateLibrary(state)]
514
+ };
515
+ try {
516
+ const lib = compileWithOptionalIdentity(fullSource, options);
517
+ const { relAlias, meta } = libraryOriginsFromSource(source, fullSource, lib, options);
518
+ const annotated = annotateWithOptionalIdentity(fullSource, options);
519
+ return {
520
+ annotated,
521
+ lib: { ...lib, relAlias, meta },
522
+ lineOffset: preambleLineCount(preamble)
523
+ };
524
+ } catch (error) {
525
+ throw remapError(error, preambleLineCount(preamble));
526
+ }
527
+ }
528
+
392
529
  async function defineType(input, state) {
393
530
  const typeMatch = input.match(TYPE_DEF_RE);
394
531
  if (!typeMatch) {
@@ -397,8 +534,14 @@ async function defineType(input, state) {
397
534
 
398
535
  const name = typeMatch[1];
399
536
  restoreCodes(state);
400
- const source = [aliasPreamble(state, name), ensureSemicolon(input), "()"].filter(Boolean).join("\n");
401
- const annotated = annotate(source, { libraries: [stateLibrary(state)] });
537
+ const preamble = aliasPreamble(state, name);
538
+ const source = [preamble, ensureSemicolon(input), "()"].filter(Boolean).join("\n");
539
+ let annotated;
540
+ try {
541
+ annotated = annotate(source, { libraries: [stateLibrary(state)] });
542
+ } catch (error) {
543
+ throw remapError(error, preambleLineCount(preamble));
544
+ }
402
545
  const hash = annotated.representatives[name];
403
546
  if (!hash) throw new Error(`Type definition did not produce an alias for '${name}'`);
404
547
  state.codes = codes.dump();
@@ -415,8 +558,14 @@ async function defineRelation(input, state) {
415
558
 
416
559
  const name = relMatch[1];
417
560
  restoreCodes(state);
418
- const source = [aliasPreamble(state, name), ensureSemicolon(input), name].filter(Boolean).join("\n");
419
- const lib = hydrateObject(compileLibrary(source, { source: "<repl>", libraries: [stateLibrary(state)] }));
561
+ const preamble = aliasPreamble(state, name);
562
+ const source = [preamble, ensureSemicolon(input), name].filter(Boolean).join("\n");
563
+ let lib;
564
+ try {
565
+ lib = hydrateObject(compileLibrary(source, { source: "<repl>", libraries: [stateLibrary(state)] }));
566
+ } catch (error) {
567
+ throw remapError(error, preambleLineCount(preamble));
568
+ }
420
569
  const hash = lib.relAlias?.[name];
421
570
  if (!hash || !lib.rels?.[hash]) throw new Error(`Relation definition did not produce an alias for '${name}'`);
422
571
  state.codes = codes.dump();
@@ -433,16 +582,52 @@ async function runExpression(input, state) {
433
582
  if (!expression) throw new Error(":run requires an expression");
434
583
 
435
584
  restoreCodes(state);
436
- const source = [aliasPreamble(state), expression].filter(Boolean).join("\n");
437
- const annotated = annotate(source, { libraries: [stateLibrary(state)] });
585
+ const preamble = aliasPreamble(state);
586
+ const source = [preamble, expression].filter(Boolean).join("\n");
587
+ let annotated;
588
+ try {
589
+ annotated = annotate(source, { libraries: [stateLibrary(state)] });
590
+ } catch (error) {
591
+ throw remapError(error, preambleLineCount(preamble));
592
+ }
438
593
  const mainRel = annotated.rels.__main__;
439
594
  run.defs = annotated;
440
- state.value = run(codes.find, mainRel.def, state.value, mainRel.typePatternGraph);
595
+ try {
596
+ state.value = run(codes.find, mainRel.def, state.value, mainRel.typePatternGraph);
597
+ } catch (error) {
598
+ throw remapError(error, preambleLineCount(preamble));
599
+ }
441
600
  state.lastMain = expression;
442
601
  restoreCodes(state);
443
602
  return [printValue(state.value)];
444
603
  }
445
604
 
605
+ async function runSnippet(input, state, options = {}) {
606
+ const snippet = input.trim();
607
+ if (!snippet) return [];
608
+
609
+ const explicitTerminated = options.explicitTerminated ?? explicitSnippetTerminated(input);
610
+ const analysis = analyzeAcceptedSnippet(snippet, explicitTerminated);
611
+ const { annotated, lib, lineOffset } = compileSnippetArtifacts(snippet, state);
612
+ mergeLibrary(state, lib, "<repl>");
613
+
614
+ if (analysis.kind === "definitionsOnly") {
615
+ restoreCodes(state);
616
+ return [];
617
+ }
618
+
619
+ const mainRel = annotated.rels.__main__;
620
+ run.defs = annotated;
621
+ try {
622
+ state.value = run(codes.find, mainRel.def, state.value, mainRel.typePatternGraph);
623
+ } catch (error) {
624
+ throw remapError(error, lineOffset);
625
+ }
626
+ state.lastMain = snippet;
627
+ restoreCodes(state);
628
+ return [printValue(state.value)];
629
+ }
630
+
446
631
  async function evaluateInput(input, state) {
447
632
  const line = input.trim();
448
633
  if (line === "" || line.startsWith("#") || line.startsWith("//") || line.startsWith("--")) return [];
@@ -451,9 +636,7 @@ async function evaluateInput(input, state) {
451
636
  return evaluateCommand(line, state);
452
637
  }
453
638
 
454
- if (input.match(TYPE_DEF_RE)) return defineType(input, state);
455
- if (input.match(REL_DEF_RE)) return defineRelation(input, state);
456
- return runExpression(input, state);
639
+ return runSnippet(input, state);
457
640
  }
458
641
 
459
642
  function parseCommand(line) {
@@ -503,37 +686,20 @@ async function evaluateCommand(line, state) {
503
686
  Object.assign(state, fresh);
504
687
  return ["reset"];
505
688
  }
506
- case "save": {
507
- if (!arg) throw new Error(`${usagePrefix}save requires a file path`);
508
- fs.writeFileSync(arg, encodeLibrary(savedLibrary(state)));
509
- return [`saved ${arg}`];
510
- }
511
689
  case "klib": {
512
690
  if (!arg) throw new Error(`${usagePrefix}klib requires a file path`);
513
691
  fs.writeFileSync(arg, encodeLibrary(savedLibrary(state)));
514
692
  return [`saved ${arg}`];
515
693
  }
516
694
  case "ko": {
517
- if (!arg) throw new Error(`${usagePrefix}ko requires a file path`);
695
+ if (!arg) throw new Error(`${usagePrefix}ko requires a file path and main expression`);
518
696
  const [path, ...mainParts] = arg.split(/\s+/);
519
- const object = executableObject(state, mainParts.join(" "));
520
- fs.writeFileSync(path, encodeObject(object));
521
- const main = (mainParts.join(" ") || state.lastMain).trim();
522
- return [`saved ${path} (${main})`];
523
- }
524
- case "export": {
525
- if (!arg) throw new Error(`${usagePrefix}export requires a file path`);
526
- const [path, ...mainParts] = arg.split(/\s+/);
527
- if (path.endsWith(".klib")) {
528
- fs.writeFileSync(path, encodeLibrary(savedLibrary(state)));
529
- return [`saved ${path}`];
530
- }
531
- if (!path.endsWith(".ko")) {
532
- throw new Error(":export writes .klib or .ko files; use :klib or :ko for explicit export");
697
+ if (mainParts.length === 0) {
698
+ throw new Error(":ko file expr requires a main expression");
533
699
  }
534
700
  const object = executableObject(state, mainParts.join(" "));
535
701
  fs.writeFileSync(path, encodeObject(object));
536
- const main = (mainParts.join(" ") || state.lastMain).trim();
702
+ const main = mainParts.join(" ").trim();
537
703
  return [`saved ${path} (${main})`];
538
704
  }
539
705
  case "load": {
@@ -544,14 +710,19 @@ async function evaluateCommand(line, state) {
544
710
  } else {
545
711
  restoreCodes(state);
546
712
  const source = fs.readFileSync(arg, "utf8");
547
- const fullSource = [aliasPreamble(state), source].filter(Boolean).join("\n");
713
+ const preamble = aliasPreamble(state);
714
+ const fullSource = [preamble, source].filter(Boolean).join("\n");
548
715
  const options = {
549
716
  source: arg,
550
717
  libraries: [stateLibrary(state)]
551
718
  };
552
- const lib = compileWithOptionalIdentity(fullSource, options);
553
- const { relAlias, meta } = libraryOriginsFromSource(source, fullSource, lib, options);
554
- mergeLibrary(state, { ...lib, relAlias, meta }, arg);
719
+ try {
720
+ const lib = compileWithOptionalIdentity(fullSource, options);
721
+ const { relAlias, meta } = libraryOriginsFromSource(source, fullSource, lib, options);
722
+ mergeLibrary(state, { ...lib, relAlias, meta }, arg);
723
+ } catch (error) {
724
+ throw remapError(error, preambleLineCount(preamble));
725
+ }
555
726
  }
556
727
  return [`loaded ${arg}`];
557
728
  }
@@ -589,14 +760,15 @@ function helpText() {
589
760
  ":codes list type aliases",
590
761
  ":rels list relation aliases",
591
762
  ":load file load .k source or .klib",
592
- ":klib file export state as .klib",
593
- ":ko file [expr] export executable .ko",
594
- ":export file [expr] export by extension: .klib or .ko",
763
+ ":klib file export state as a library",
764
+ ":ko file expr export executable .ko using expr as main",
595
765
  ":val print current value",
596
766
  ":reset clear state",
597
767
  ":help show this help",
598
768
  "",
599
- "Raw k definitions and expressions are still accepted as shorthand."
769
+ "Raw k input is compiled as a snippet on top of the current state.",
770
+ "A line ending with ';' plus only spaces closes the snippet.",
771
+ "Use '\\' to force continuation."
600
772
  ].join("\n");
601
773
  }
602
774
 
@@ -615,19 +787,50 @@ function startRepl() {
615
787
  console.log("k interpreter (.klib-backed). Type :help for commands.");
616
788
  rl.prompt();
617
789
  async function handleLine(line) {
618
- if (line.trim().endsWith("\\")) {
619
- buffer.push(line.replace(/\\\s*$/, ""));
790
+ if (lineHasExplicitContinuation(line)) {
791
+ buffer.push(lineForContinuation(line));
620
792
  rl.setPrompt(" ");
621
793
  if (!closed) rl.prompt();
622
794
  return;
623
795
  }
624
796
 
625
- const input = [...buffer, line].join("\n");
797
+ if (buffer.length === 0 && line.trim().startsWith(":")) {
798
+ rl.setPrompt("> ");
799
+ try {
800
+ for (const output of await evaluateInput(line, state)) {
801
+ console.log(output);
802
+ }
803
+ } catch (error) {
804
+ console.error(error.message || String(error));
805
+ }
806
+ if (!closed) rl.prompt();
807
+ return;
808
+ }
809
+
810
+ buffer.push(line);
811
+ const input = buffer.join("\n");
812
+ const explicitTerminated = lineTerminatesSnippet(line);
813
+ if (!explicitTerminated) {
814
+ try {
815
+ const analysis = analyzeRawSnippet(input);
816
+ if (analysis.kind !== "withMain") {
817
+ rl.setPrompt(" ");
818
+ if (!closed) rl.prompt();
819
+ return;
820
+ }
821
+ } catch (error) {
822
+ buffer.length = 0;
823
+ rl.setPrompt("> ");
824
+ console.error(error.message || String(error));
825
+ if (!closed) rl.prompt();
826
+ return;
827
+ }
828
+ }
829
+
626
830
  buffer.length = 0;
627
831
  rl.setPrompt("> ");
628
-
629
832
  try {
630
- for (const output of await evaluateInput(input, state)) {
833
+ for (const output of await runSnippet(input, state, { explicitTerminated })) {
631
834
  console.log(output);
632
835
  }
633
836
  } catch (error) {
@@ -649,13 +852,19 @@ if (isMainEntrypoint()) {
649
852
  }
650
853
 
651
854
  export {
855
+ aliasNames,
856
+ analyzeAcceptedSnippet,
857
+ analyzeRawSnippet,
652
858
  canonicalNames,
653
859
  completeInput,
654
860
  createCompleter,
655
861
  createState,
656
862
  evaluateInput,
863
+ explicitSnippetTerminated,
657
864
  helpText,
658
865
  isMainEntrypoint,
866
+ lineHasExplicitContinuation,
867
+ lineTerminatesSnippet,
659
868
  printValue,
660
869
  propertyListToFilter,
661
870
  valueToK