functionalscript 0.38.0 → 0.40.0

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 (137) hide show
  1. package/README.md +1 -1
  2. package/fjs/bnf/descent/module.f.d.ts +43 -2
  3. package/fjs/bnf/descent/module.f.js +37 -12
  4. package/fjs/bnf/descent/proof.f.d.ts +1 -0
  5. package/fjs/bnf/descent/proof.f.js +73 -32
  6. package/fjs/bnf/ll1/module.f.js +2 -2
  7. package/fjs/bnf/token_symbol/module.f.d.ts +38 -0
  8. package/fjs/bnf/token_symbol/module.f.js +48 -0
  9. package/fjs/bnf/token_symbol/proof.f.d.ts +10 -0
  10. package/fjs/bnf/token_symbol/proof.f.js +37 -0
  11. package/fjs/cas/cli/module.f.js +6 -9
  12. package/fjs/cas/evo/module.f.d.ts +77 -13
  13. package/fjs/cas/evo/module.f.js +130 -18
  14. package/fjs/cas/evo/proof.f.d.ts +7 -0
  15. package/fjs/cas/evo/proof.f.js +120 -1
  16. package/fjs/cas/module.f.js +64 -37
  17. package/fjs/cas/proof.f.d.ts +3 -9
  18. package/fjs/cas/proof.f.js +94 -74
  19. package/fjs/ci/bun/module.f.js +3 -3
  20. package/fjs/ci/common/module.f.d.ts +1 -2
  21. package/fjs/ci/common/module.f.js +1 -5
  22. package/fjs/ci/config/module.f.d.ts +11 -8
  23. package/fjs/ci/config/module.f.js +24 -9
  24. package/fjs/ci/deno/module.f.js +3 -3
  25. package/fjs/ci/module.f.js +13 -7
  26. package/fjs/ci/nix/module.f.d.ts +61 -0
  27. package/fjs/ci/nix/module.f.js +92 -0
  28. package/fjs/ci/nix/proof.f.d.ts +23 -0
  29. package/fjs/ci/nix/proof.f.js +109 -0
  30. package/fjs/ci/node/module.f.d.ts +23 -1
  31. package/fjs/ci/node/module.f.js +58 -12
  32. package/fjs/ci/proof.f.d.ts +2 -0
  33. package/fjs/ci/proof.f.js +49 -9
  34. package/fjs/cli/module.f.js +2 -2
  35. package/fjs/common/monoid/module.f.d.ts +46 -2
  36. package/fjs/common/monoid/module.f.js +44 -0
  37. package/fjs/common/monoid/proof.f.d.ts +5 -0
  38. package/fjs/common/monoid/proof.f.js +27 -1
  39. package/fjs/dev/module.f.d.ts +1 -0
  40. package/fjs/dev/module.f.js +38 -27
  41. package/fjs/dev/update/module.f.d.ts +11 -0
  42. package/fjs/dev/update/module.f.js +20 -0
  43. package/fjs/dev/update/proof.f.d.ts +7 -0
  44. package/fjs/dev/update/proof.f.js +35 -0
  45. package/fjs/djs/ast/module.f.d.ts +47 -0
  46. package/fjs/djs/ast/module.f.js +9 -0
  47. package/fjs/djs/module.f.js +5 -8
  48. package/fjs/djs/parser/module.f.d.ts +3 -0
  49. package/fjs/djs/parser/module.f.js +17 -0
  50. package/fjs/djs/tokenizer/module.f.js +2 -2
  51. package/fjs/djs/tokenizer/proof.f.d.ts +1 -0
  52. package/fjs/djs/tokenizer/proof.f.js +70 -16
  53. package/fjs/djs/transpiler/module.f.js +6 -6
  54. package/fjs/effects/eff/module.f.d.ts +72 -0
  55. package/fjs/effects/eff/module.f.js +46 -0
  56. package/fjs/effects/eff/proof.f.d.ts +11 -0
  57. package/fjs/effects/eff/proof.f.js +58 -0
  58. package/fjs/effects/list/module.f.d.ts +11 -5
  59. package/fjs/effects/list/module.f.js +2 -1
  60. package/fjs/effects/memory/proof.f.js +7 -8
  61. package/fjs/effects/module.f.d.ts +334 -52
  62. package/fjs/effects/module.f.js +253 -45
  63. package/fjs/effects/node/memory/proof.js +3 -2
  64. package/fjs/effects/node/module.d.ts +3 -3
  65. package/fjs/effects/node/module.f.d.ts +18 -8
  66. package/fjs/effects/node/module.f.js +31 -11
  67. package/fjs/effects/node/module.js +12 -12
  68. package/fjs/effects/node/proof.f.d.ts +1 -0
  69. package/fjs/effects/node/proof.f.js +32 -11
  70. package/fjs/effects/node/virtual/module.f.js +1 -1
  71. package/fjs/effects/proof.f.d.ts +32 -6
  72. package/fjs/effects/proof.f.js +131 -39
  73. package/fjs/emergent_testing/all.test.js +2 -1
  74. package/fjs/emergent_testing/module.f.d.ts +3 -3
  75. package/fjs/emergent_testing/module.f.js +63 -49
  76. package/fjs/emergent_testing/proof.f.d.ts +4 -0
  77. package/fjs/emergent_testing/proof.f.js +54 -21
  78. package/fjs/emergent_testing/scenarios/thenable.pass.js +1 -1
  79. package/fjs/fsc/module.f.js +4 -4
  80. package/fjs/fsm/module.f.js +1 -1
  81. package/fjs/js/tokenizer/module.f.d.ts +1 -0
  82. package/fjs/js/tokenizer/module.f.js +13 -6
  83. package/fjs/{cas/mcp → mcp/cas}/module.f.d.ts +3 -25
  84. package/fjs/{cas/mcp → mcp/cas}/module.f.js +26 -64
  85. package/fjs/mcp/evo/module.f.d.ts +27 -0
  86. package/fjs/mcp/evo/module.f.js +94 -0
  87. package/fjs/{cas/evo/mcp → mcp/evo}/proof.f.d.ts +2 -0
  88. package/fjs/{cas/evo/mcp → mcp/evo}/proof.f.js +40 -9
  89. package/fjs/mcp/module.f.d.ts +54 -237
  90. package/fjs/mcp/module.f.js +55 -258
  91. package/fjs/mcp/proof.f.d.ts +43 -32
  92. package/fjs/mcp/proof.f.js +509 -200
  93. package/fjs/media/nix/module.f.d.ts +30 -0
  94. package/fjs/media/nix/module.f.js +166 -0
  95. package/fjs/media/nix/proof.f.d.ts +32 -0
  96. package/fjs/media/nix/proof.f.js +127 -0
  97. package/fjs/media/type/module.f.js +2 -2
  98. package/fjs/media/type/proof.f.js +7 -10
  99. package/fjs/module.f.js +5 -9
  100. package/fjs/protocol/json_rpc/module.f.d.ts +114 -0
  101. package/fjs/{media/json/rpc → protocol/json_rpc}/module.f.js +3 -3
  102. package/fjs/{media/json/rpc → protocol/json_rpc}/proof.f.js +3 -3
  103. package/fjs/protocol/mcp/module.f.d.ts +239 -0
  104. package/fjs/protocol/mcp/module.f.js +272 -0
  105. package/fjs/protocol/mcp/proof.f.d.ts +34 -0
  106. package/fjs/protocol/mcp/proof.f.js +208 -0
  107. package/fjs/{mcp → protocol/mcp}/stdio/module.f.d.ts +7 -7
  108. package/fjs/{mcp → protocol/mcp}/stdio/module.f.js +22 -22
  109. package/fjs/{mcp → protocol/mcp}/stdio/proof.f.js +9 -9
  110. package/fjs/types/bigint/module.f.js +4 -3
  111. package/fjs/types/bit_vec/module.f.js +5 -15
  112. package/fjs/types/btree/remove/module.f.d.ts +5 -0
  113. package/fjs/types/btree/remove/module.f.js +12 -0
  114. package/fjs/types/list/module.f.d.ts +21 -0
  115. package/fjs/types/list/module.f.js +17 -0
  116. package/fjs/types/list/proof.f.d.ts +1 -0
  117. package/fjs/types/list/proof.f.js +24 -1
  118. package/fjs/types/number/module.f.js +2 -1
  119. package/fjs/types/range_map/module.f.d.ts +14 -13
  120. package/fjs/types/range_map/module.f.js +18 -13
  121. package/fjs/types/range_map/proof.f.js +26 -39
  122. package/fjs/types/range_set/module.f.d.ts +5 -0
  123. package/fjs/types/range_set/module.f.js +16 -0
  124. package/fjs/types/range_set/proof.f.d.ts +1 -0
  125. package/fjs/types/range_set/proof.f.js +19 -0
  126. package/fjs/types/string/module.f.js +10 -2
  127. package/fjs/website/module.f.js +2 -3
  128. package/package.json +5 -5
  129. package/fjs/cas/evo/mcp/module.f.d.ts +0 -19
  130. package/fjs/cas/evo/mcp/module.f.js +0 -56
  131. package/fjs/cas/mcp/proof.f.d.ts +0 -45
  132. package/fjs/cas/mcp/proof.f.js +0 -545
  133. package/fjs/ci/playwright/module.f.d.ts +0 -2
  134. package/fjs/ci/playwright/module.f.js +0 -25
  135. package/fjs/media/json/rpc/module.f.d.ts +0 -114
  136. /package/fjs/{media/json/rpc → protocol/json_rpc}/proof.f.d.ts +0 -0
  137. /package/fjs/{mcp → protocol/mcp}/stdio/proof.f.d.ts +0 -0
package/README.md CHANGED
@@ -60,7 +60,7 @@ The CAS is also exposed as an [MCP](https://modelcontextprotocol.io/) server so
60
60
  claude mcp add cas -- npx functionalscript m
61
61
  ```
62
62
 
63
- See [`fjs/cas/mcp/README.md`](fjs/cas/mcp/README.md) for details on the `cas_add`, `cas_get`, and `cas_list` tools.
63
+ See [`fjs/mcp/README.md`](fjs/mcp/README.md) for details on the `cas_add`, `cas_get`, and `cas_list` tools.
64
64
 
65
65
  ## Vision
66
66
 
@@ -7,9 +7,14 @@
7
7
  * AST ({@link AstRuleMeta}). Nullability (which rule can match empty input) is
8
8
  * computed once by {@link emptyTagMap} in `fjs/bnf/data`.
9
9
  *
10
+ * A failed result also carries a {@link DescentFailure}: the furthest position a
11
+ * terminal was rejected at, which — unlike the result's own index — never
12
+ * rewinds and is what diagnostics should be built from.
13
+ *
10
14
  * @module
11
15
  */
12
16
  import { type CodePoint } from '../../text/utf16/module.f.ts';
17
+ import { type TerminalRange } from '../module.f.ts';
13
18
  import { type Rule as FRule } from '../module.f.ts';
14
19
  export type AstTag = string | true | undefined;
15
20
  /**
@@ -17,9 +22,45 @@ export type AstTag = string | true | undefined;
17
22
  */
18
23
  export type DescentMatchRule<T> = (name: string, tag: AstTag, s: readonly CodePointMeta<T>[], idx: number) => DescentMatchResult<T>;
19
24
  /**
20
- * Result tuple of a descent match operation: AST node, success flag, and next index.
25
+ * Where a match ran out of road, for diagnostics.
26
+ *
27
+ * `idx` is the furthest position any terminal was tried at and rejected, and
28
+ * `expected` holds the terminals that would have allowed progress there, in the
29
+ * order the grammar tried them and without repeats.
30
+ *
31
+ * Unlike a failed result's own index, this never rewinds: a failing sequence
32
+ * item rewinds the result to the sequence's start, while the furthest failure is
33
+ * a high-water mark over the whole match — including branches the grammar
34
+ * backtracked out of. That is what makes "expected X or Y at N" possible.
35
+ *
36
+ * `idx` is `0` with an empty `expected` when the match failed without ever
37
+ * rejecting a terminal, as an empty variant does.
21
38
  */
22
- export type DescentMatchResult<T> = readonly [AstRuleMeta<T>, boolean, number];
39
+ export type DescentFailure = {
40
+ readonly idx: number;
41
+ readonly expected: readonly TerminalRange[];
42
+ };
43
+ /**
44
+ * Result of a descent match operation.
45
+ *
46
+ * `failure` is present exactly when `success` is `false`: a successful match has
47
+ * nothing to diagnose, and its `idx` already says where matching stopped. Note
48
+ * the consequence for a match that succeeds *without consuming all input* —
49
+ * `idx` still locates the position it stopped at, but the terminals that would
50
+ * have let it continue are not reported.
51
+ *
52
+ * On failure `idx` has rewound to the start of the enclosing sequence and
53
+ * locates nothing; read `failure.idx` instead.
54
+ *
55
+ * The same type describes a match in progress, where `failure` is likewise
56
+ * absent until the match ends.
57
+ */
58
+ export type DescentMatchResult<T> = {
59
+ readonly ast: AstRuleMeta<T>;
60
+ readonly success: boolean;
61
+ readonly idx: number;
62
+ readonly failure?: DescentFailure;
63
+ };
23
64
  /**
24
65
  * Entry-point recursive descent matcher.
25
66
  */
@@ -7,6 +7,10 @@
7
7
  * AST ({@link AstRuleMeta}). Nullability (which rule can match empty input) is
8
8
  * computed once by {@link emptyTagMap} in `fjs/bnf/data`.
9
9
  *
10
+ * A failed result also carries a {@link DescentFailure}: the furthest position a
11
+ * terminal was rejected at, which — unlike the result's own index — never
12
+ * rewinds and is what diagnostics should be built from.
13
+ *
10
14
  * @module
11
15
  */
12
16
  import {} from '../../text/utf16/module.f.js';
@@ -15,6 +19,20 @@ import { contains as rangeContains } from '../../types/range/module.f.js';
15
19
  import { definedEntries } from '../../types/object/module.f.js';
16
20
  import { emptyTagMap, toData } from '../data/module.f.js';
17
21
  import {} from '../module.f.js';
22
+ /**
23
+ * Folds one rejected terminal into the furthest-failure record: further along
24
+ * replaces, the same position accumulates (ignoring repeats), earlier is
25
+ * discarded.
26
+ */
27
+ const recordFailure = (failure, idx, terminal) => {
28
+ if (idx > failure.idx) {
29
+ return { idx, expected: [terminal] };
30
+ }
31
+ if (idx < failure.idx || failure.expected.includes(terminal)) {
32
+ return failure;
33
+ }
34
+ return { idx, expected: [...failure.expected, terminal] };
35
+ };
18
36
  /**
19
37
  * Creates a recursive descent parser that preserves metadata for each consumed
20
38
  * code point.
@@ -29,11 +47,14 @@ export const descentParser = (fr) => {
29
47
  // grammar recursion depth — right-recursive rules (e.g. repeat0Plus chains) no longer
30
48
  // overflow on long input (see the longInput proof group).
31
49
  const f = (name, tag, cp, idx) => {
32
- const mrSuccess = (tag, sequence, idx) => [{ tag, sequence }, true, idx];
33
- const mrFail = (tag, sequence, idx) => [{ tag, sequence }, false, idx];
50
+ const mrSuccess = (tag, sequence, idx) => ({ ast: { tag, sequence }, success: true, idx });
51
+ const mrFail = (tag, sequence, idx) => ({ ast: { tag, sequence }, success: false, idx });
34
52
  let stack = null;
35
53
  let task = { name, tag, idx };
36
54
  let result = mrFail(undefined, [], idx);
55
+ // High-water mark across the whole match, so it survives the rewinds a
56
+ // failing sequence item does to `result`.
57
+ let furthest = { idx: 0, expected: [] };
37
58
  while (true) {
38
59
  if (task !== null) {
39
60
  const { name, tag, idx } = task;
@@ -43,14 +64,17 @@ export const descentParser = (fr) => {
43
64
  // later `task` assignments that `name`'s narrowing depends on.
44
65
  const rule = data[0][name];
45
66
  if (typeof rule === 'number') {
46
- const emptyTag = emptyTags[name];
47
- if (idx >= cp.length) {
48
- result = emptyTag === undefined ? mrFail(emptyTag, [], idx) : mrSuccess(emptyTag, [], idx);
67
+ // No nullable case: `emptyTagOf` in `bnf/data` returns `undefined`
68
+ // for every terminal, so `emptyTags[name]` here is always
69
+ // `undefined` and a terminal either consumes one symbol or fails.
70
+ if (idx < cp.length && rangeContains(...rangeDecode(rule))(cp[idx][0])) {
71
+ result = mrSuccess(tag, [cp[idx]], idx + 1);
49
72
  }
50
73
  else {
51
- const cpi = cp[idx];
52
- const range = rangeDecode(rule);
53
- result = rangeContains(...range)(cpi[0]) ? mrSuccess(tag, [cpi], idx + 1) : mrFail(emptyTag, [], idx);
74
+ // The only place a terminal is rejected, so the only place
75
+ // the furthest failure can advance.
76
+ furthest = recordFailure(furthest, idx, rule);
77
+ result = mrFail(undefined, [], idx);
54
78
  }
55
79
  }
56
80
  else if (rule instanceof Array) {
@@ -78,12 +102,13 @@ export const descentParser = (fr) => {
78
102
  continue;
79
103
  }
80
104
  if (stack === null) {
81
- return result;
105
+ // A success has nothing to diagnose; only a failure carries it.
106
+ return result.success ? result : { ...result, failure: furthest };
82
107
  }
83
108
  const frame = stack.top;
84
109
  stack = stack.rest;
85
110
  if (frame.kind === 'seq') {
86
- const [astRule, success, nidx] = result;
111
+ const { ast: astRule, success, idx: nidx } = result;
87
112
  if (success === false) {
88
113
  result = mrFail(frame.tag, [], frame.startIdx);
89
114
  }
@@ -102,8 +127,8 @@ export const descentParser = (fr) => {
102
127
  else {
103
128
  // success that consumed input wins immediately: the frame stays popped and
104
129
  // `result` propagates to the frame below, matching the recursive `return m`.
105
- if (!(result[1] && frame.idx !== result[2])) {
106
- const emptyResult = result[1] ? result : frame.emptyResult;
130
+ if (!(result.success && frame.idx !== result.idx)) {
131
+ const emptyResult = result.success ? result : frame.emptyResult;
107
132
  const entryIndex = frame.entryIndex + 1;
108
133
  if (entryIndex < frame.entries.length) {
109
134
  stack = { top: { ...frame, entryIndex, emptyResult }, rest: stack };
@@ -3,4 +3,5 @@ export declare const proof: {
3
3
  descentParser: (() => void)[];
4
4
  longInput: (() => void)[];
5
5
  descentParserWithMeta: (() => void)[];
6
+ furthestFailure: (() => void)[];
6
7
  };
@@ -4,8 +4,12 @@ import { commaJoin0Plus, option, range, repeat0Plus, set } from '../module.f.js'
4
4
  import { deterministic } from '../testlib.f.js';
5
5
  import { emptyTagMap, toData } from '../data/module.f.js';
6
6
  import { descentParser } from './module.f.js';
7
- import { assertEq } from '../../asserts/module.f.js';
7
+ import { assertEq, assertNotNullish } from '../../asserts/module.f.js';
8
8
  const mapCodePoint = (cp) => [cp, undefined];
9
+ // The code point of a one-character string, for expectations that would
10
+ // otherwise spell it as a bare number. Goes through the module's own
11
+ // conversion, the same one that builds the parser's input.
12
+ const cp1 = (s) => toArray(stringToCodePointList(s))[0];
9
13
  const descentParserCpOnly = (m, name, cp) => {
10
14
  const cpm = toArray(map(mapCodePoint)(cp));
11
15
  return m(name, cpm);
@@ -83,52 +87,59 @@ export const proof = {
83
87
  const m = descentParser(emptyRule);
84
88
  const mr = m("", []);
85
89
  const result = JSON.stringify(mr);
86
- if (result !== '[{"sequence":[]},true,0]') {
90
+ if (result !== '{"ast":{"sequence":[]},"success":true,"idx":0}') {
87
91
  throw result;
88
92
  }
89
93
  },
90
94
  () => {
91
95
  const emptyRule = '';
92
96
  const m = descentParser(emptyRule);
93
- const mr = descentParserCpOnly(m, "", [65, 70]);
97
+ const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('AF')));
94
98
  const result = JSON.stringify(mr);
95
- if (result !== '[{"sequence":[]},true,0]') {
99
+ if (result !== '{"ast":{"sequence":[]},"success":true,"idx":0}') {
96
100
  throw result;
97
101
  }
98
102
  },
99
103
  () => {
104
+ // Literal code point on purpose. Elsewhere both sides of the
105
+ // assertion come from `stringToCodePointList` — the input is built
106
+ // with it and the expectation interpolates `cp1`, which calls it
107
+ // too — so a change to the conversion would move both sides
108
+ // together and the test would still pass. Pinning `A` to 65 here
109
+ // covers the conversion itself.
100
110
  const terminalRangeRule = range('AF');
101
111
  const m = descentParser(terminalRangeRule);
102
- const mr = descentParserCpOnly(m, "", [65]);
112
+ const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('A')));
103
113
  const result = JSON.stringify(mr);
104
- if (result !== '[{"sequence":[[65,null]]},true,1]') {
114
+ if (result !== '{"ast":{"sequence":[[65,null]]},"success":true,"idx":1}') {
105
115
  throw result;
106
116
  }
107
117
  },
108
118
  () => {
109
119
  const terminalRangeRule = range('AF');
110
120
  const m = descentParser(terminalRangeRule);
111
- const mr = descentParserCpOnly(m, "", [64]);
121
+ const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('@')));
112
122
  const result = JSON.stringify(mr);
113
- if (result !== '[{"sequence":[]},false,0]') {
123
+ if (result !== `{"ast":{"sequence":[]},"success":false,"idx":0,"failure":{"idx":0,"expected":[${range('AF')}]}}`) {
114
124
  throw result;
115
125
  }
116
126
  },
117
127
  () => {
118
128
  const variantRule = { 'a': range('AA'), 'b': range('BB') };
119
129
  const m = descentParser(variantRule);
120
- const mr = descentParserCpOnly(m, "", [65]);
130
+ const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('A')));
121
131
  const result = JSON.stringify(mr);
122
- if (result !== '[{"tag":"a","sequence":[[65,null]]},true,1]') {
132
+ if (result !== `{"ast":{"tag":"a","sequence":[[${cp1('A')},null]]},"success":true,"idx":1}`) {
123
133
  throw result;
124
134
  }
125
135
  },
126
136
  () => {
127
137
  const variantRule = { 'a': range('AA'), 'b': range('BB') };
128
138
  const m = descentParser(variantRule);
129
- const mr = descentParserCpOnly(m, "", [64]);
139
+ const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('@')));
130
140
  const result = JSON.stringify(mr);
131
- if (result !== '[{"sequence":[]},false,0]') {
141
+ // Both branches were rejected at 0, so both terminals are expected there.
142
+ if (result !== `{"ast":{"sequence":[]},"success":false,"idx":0,"failure":{"idx":0,"expected":[${range('AA')},${range('BB')}]}}`) {
132
143
  throw result;
133
144
  }
134
145
  },
@@ -138,7 +149,7 @@ export const proof = {
138
149
  const m = descentParser(variantRule);
139
150
  const mr = m("", []);
140
151
  const result = JSON.stringify(mr);
141
- if (result !== '[{"tag":"e","sequence":[]},true,0]') {
152
+ if (result !== '{"ast":{"tag":"e","sequence":[]},"success":true,"idx":0}') {
142
153
  throw result;
143
154
  }
144
155
  },
@@ -146,9 +157,9 @@ export const proof = {
146
157
  const emptyRule = '';
147
158
  const variantRule = { 'e': emptyRule, 'a': range('AA') };
148
159
  const m = descentParser(variantRule);
149
- const mr = descentParserCpOnly(m, "", [64]);
160
+ const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('@')));
150
161
  const result = JSON.stringify(mr);
151
- if (result !== '[{"tag":"e","sequence":[]},true,0]') {
162
+ if (result !== '{"ast":{"tag":"e","sequence":[]},"success":true,"idx":0}') {
152
163
  throw result;
153
164
  }
154
165
  },
@@ -157,25 +168,29 @@ export const proof = {
157
168
  const m = descentParser(emptyVariantRule);
158
169
  const mr = m("", []);
159
170
  const result = JSON.stringify(mr);
160
- if (result !== '[{"sequence":[]},false,0]') {
171
+ // A variant with no branches fails without ever trying a terminal,
172
+ // so there is nothing to expect.
173
+ if (result !== '{"ast":{"sequence":[]},"success":false,"idx":0,"failure":{"idx":0,"expected":[]}}') {
161
174
  throw result;
162
175
  }
163
176
  },
164
177
  () => {
165
178
  const stringRule = 'AB';
166
179
  const m = descentParser(stringRule);
167
- const mr = descentParserCpOnly(m, "", [65, 66]);
180
+ const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('AB')));
168
181
  const result = JSON.stringify(mr);
169
- if (result !== '[{"sequence":[{"sequence":[[65,null]]},{"sequence":[[66,null]]}]},true,2]') {
182
+ if (result !== `{"ast":{"sequence":[{"sequence":[[${cp1('A')},null]]},{"sequence":[[${cp1('B')},null]]}]},"success":true,"idx":2}`) {
170
183
  throw result;
171
184
  }
172
185
  },
173
186
  () => {
174
187
  const stringRule = 'AB';
175
188
  const m = descentParser(stringRule);
176
- const mr = descentParserCpOnly(m, "", [65, 67]);
189
+ const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('AC')));
177
190
  const result = JSON.stringify(mr);
178
- if (result !== '[{"sequence":[]},false,0]') {
191
+ // The result index rewound to the sequence's start, but the furthest
192
+ // failure kept the position where 'B' was actually rejected.
193
+ if (result !== `{"ast":{"sequence":[]},"success":false,"idx":0,"failure":{"idx":1,"expected":[${range('BB')}]}}`) {
179
194
  throw result;
180
195
  }
181
196
  },
@@ -186,9 +201,9 @@ export const proof = {
186
201
  const digitRule = range('09');
187
202
  const numberRule = [optionalMinusRule, digitRule];
188
203
  const m = descentParser(numberRule);
189
- const mr = descentParserCpOnly(m, "", [50]);
204
+ const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('2')));
190
205
  const result = JSON.stringify(mr);
191
- if (result !== '[{"sequence":[{"tag":"none","sequence":[]},{"sequence":[[50,null]]}]},true,1]') {
206
+ if (result !== `{"ast":{"sequence":[{"tag":"none","sequence":[]},{"sequence":[[${cp1('2')},null]]}]},"success":true,"idx":1}`) {
192
207
  throw result;
193
208
  }
194
209
  },
@@ -199,9 +214,9 @@ export const proof = {
199
214
  const digitRule = range('09');
200
215
  const numberRule = [optionalMinusRule, digitRule];
201
216
  const m = descentParser(numberRule);
202
- const mr = descentParserCpOnly(m, "", [45, 50]);
217
+ const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('-2')));
203
218
  const result = JSON.stringify(mr);
204
- if (result !== '[{"sequence":[{"tag":"minus","sequence":[[45,null]]},{"sequence":[[50,null]]}]},true,2]') {
219
+ if (result !== `{"ast":{"sequence":[{"tag":"minus","sequence":[[${cp1('-')},null]]},{"sequence":[[${cp1('2')},null]]}]},"success":true,"idx":2}`) {
205
220
  throw result;
206
221
  }
207
222
  },
@@ -214,7 +229,8 @@ export const proof = {
214
229
  const m = descentParser(numberRule);
215
230
  const mr = m("", []);
216
231
  const result = JSON.stringify(mr);
217
- if (result !== '[{"sequence":[]},false,0]') {
232
+ // Past the end: '-' and then the digit range were both rejected at 0.
233
+ if (result !== `{"ast":{"sequence":[]},"success":false,"idx":0,"failure":{"idx":0,"expected":[${range('--')},${range('09')}]}}`) {
218
234
  throw result;
219
235
  }
220
236
  },
@@ -223,7 +239,7 @@ export const proof = {
223
239
  const expect = (s, expected) => {
224
240
  const cp = toArray(stringToCodePointList(s));
225
241
  const mr = descentParserCpOnly(m, '', cp);
226
- const success = mr[1] && mr[2] === cp.length;
242
+ const success = mr.success && mr.idx === cp.length;
227
243
  assertEq(success, expected, mr);
228
244
  };
229
245
  expect('a', true);
@@ -243,7 +259,7 @@ export const proof = {
243
259
  const expect = (s, expected) => {
244
260
  const cp = toArray(stringToCodePointList(s));
245
261
  const mr = descentParserCpOnly(m, 'value', cp);
246
- const success = mr[1] && mr[2] === cp.length;
262
+ const success = mr.success && mr.idx === cp.length;
247
263
  assertEq(success, expected, mr);
248
264
  };
249
265
  expect('', false);
@@ -257,7 +273,7 @@ export const proof = {
257
273
  const expect = (s, expected) => {
258
274
  const cp = toArray(stringToCodePointList(s));
259
275
  const mr = descentParserCpOnly(m, '', cp);
260
- const success = mr[1] && mr[2] === cp.length;
276
+ const success = mr.success && mr.idx === cp.length;
261
277
  assertEq(success, expected, mr);
262
278
  };
263
279
  expect(' true ', true);
@@ -293,7 +309,7 @@ export const proof = {
293
309
  const name = toData(rule)[1];
294
310
  const m = descentParser(rule);
295
311
  const cp = toArray(stringToCodePointList(' '.repeat(10000)));
296
- const [, ok, idx] = descentParserCpOnly(m, name, cp);
312
+ const { success: ok, idx } = descentParserCpOnly(m, name, cp);
297
313
  assertEq(ok, true);
298
314
  assertEq(idx, 10000);
299
315
  },
@@ -304,7 +320,7 @@ export const proof = {
304
320
  const m = descentParser(deterministic());
305
321
  const n = 5000;
306
322
  const cp = toArray(stringToCodePointList('['.repeat(n) + ']'.repeat(n)));
307
- const [, ok, idx] = descentParserCpOnly(m, '', cp);
323
+ const { success: ok, idx } = descentParserCpOnly(m, '', cp);
308
324
  assertEq(ok, true);
309
325
  assertEq(idx, n * 2);
310
326
  },
@@ -317,11 +333,36 @@ export const proof = {
317
333
  const digitRule = range('09');
318
334
  const numberRule = [optionalMinusRule, digitRule];
319
335
  const m = descentParser(numberRule);
320
- const mr = m("", [[45, 'minus'], [50, 'two']]);
336
+ const mr = m("", [[cp1('-'), 'minus'], [cp1('2'), 'two']]);
321
337
  const result = JSON.stringify(mr);
322
- if (result !== '[{"sequence":[{"tag":"minus","sequence":[[45,"minus"]]},{"sequence":[[50,"two"]]}]},true,2]') {
338
+ if (result !== `{"ast":{"sequence":[{"tag":"minus","sequence":[[${cp1('-')},"minus"]]},{"sequence":[[${cp1('2')},"two"]]}]},"success":true,"idx":2}`) {
323
339
  throw result;
324
340
  }
325
341
  },
326
342
  ],
343
+ furthestFailure: [
344
+ () => {
345
+ // A branch rejected *before* the high-water mark must not pull it back:
346
+ // `x` gets to index 1 before failing, then `y` fails at 0.
347
+ const m = descentParser({ x: ['A', 'B'], y: 'B' });
348
+ const { success: ok, idx, failure } = descentParserCpOnly(m, '', toArray(stringToCodePointList('AC')));
349
+ assertEq(ok, false);
350
+ assertEq(idx, 0);
351
+ // A failed match always carries a failure; that is the contract.
352
+ const f = assertNotNullish(failure);
353
+ assertEq(f.idx, 1);
354
+ assertEq(f.expected.length, 1);
355
+ assertEq(f.expected[0], range('BB'));
356
+ },
357
+ () => {
358
+ // The same terminal rejected at the same index by two branches is
359
+ // expected once, not twice.
360
+ const m = descentParser({ x: ['A', 'B'], y: ['A', 'B', 'C'] });
361
+ const { failure } = descentParserCpOnly(m, '', toArray(stringToCodePointList('AC')));
362
+ const f = assertNotNullish(failure);
363
+ assertEq(f.idx, 1);
364
+ assertEq(f.expected.length, 1);
365
+ assertEq(f.expected[0], range('BB'));
366
+ },
367
+ ],
327
368
  };
@@ -55,7 +55,7 @@ export const dispatchMap = (ruleSet) => {
55
55
  const rule = ruleSet[name];
56
56
  if (typeof rule === 'number') {
57
57
  const range = rangeDecode(rule);
58
- const dispatch = dispatchOp.fromRange(range)({ tag: undefined, rules: [] });
58
+ const dispatch = dispatchOp.fromRange({ tag: undefined, rules: [] })(range);
59
59
  const dr = { emptyTag: undefined, rangeMap: dispatch };
60
60
  return { ...dm, [name]: dr };
61
61
  }
@@ -126,7 +126,7 @@ export const parserRuleSet = (ruleSet) => {
126
126
  return mrSuccess(emptyTag, [], emptyTag === undefined ? null : cp);
127
127
  }
128
128
  const [cp0] = cp;
129
- const dr = dispatchOp.get(cp0)(rangeMap);
129
+ const dr = dispatchOp.get(rangeMap)(cp0);
130
130
  if (dr === null) {
131
131
  return emptyTag === undefined
132
132
  ? mrFail(emptyTag, [], cp)
@@ -0,0 +1,38 @@
1
+ import { type Nullable } from '../../types/nullable/module.f.ts';
2
+ /**
3
+ * How many names one encoding holds: every symbol from {@link start} up to but
4
+ * not including `eof` (`0xFFFFFF`, the top of the 24-bit symbol space).
5
+ */
6
+ export declare const capacity: number;
7
+ /**
8
+ * A bidirectional map between a fixed alphabet of token names and the symbol
9
+ * range reserved for them.
10
+ */
11
+ export type Encoding<T extends string> = {
12
+ /**
13
+ * The input symbol standing for `name`.
14
+ *
15
+ * The result is a bare symbol, the form a tokenizer emits. Wrap it in
16
+ * `oneEncode` to use it as a terminal of a grammar rule — a symbol and a
17
+ * `TerminalRange` are both plain numbers, so passing one where the other
18
+ * belongs is not a type error.
19
+ */
20
+ readonly encode: (name: T) => number;
21
+ /**
22
+ * The name a symbol stands for, or `null` when the symbol belongs to no
23
+ * registered name — a code point, `eof`, or a symbol past the end of the
24
+ * alphabet.
25
+ */
26
+ readonly decode: (symbol: number) => Nullable<T>;
27
+ };
28
+ /**
29
+ * Builds an encoding over the complete list of token names.
30
+ *
31
+ * A name takes the symbol at its index in `names`, so the list is append-only:
32
+ * inserting or reordering names changes the symbols of everything after the
33
+ * edit.
34
+ *
35
+ * @throws When `names` holds more than {@link capacity} entries, or when a name
36
+ * repeats — a repeated name has no single symbol to decode back to.
37
+ */
38
+ export declare const encoding: <T extends string>(names: readonly T[]) => Encoding<T>;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Encoding of multi-character token names as single BNF input symbols.
3
+ *
4
+ * A parser built on top of a tokenizer consumes one input symbol per token, so
5
+ * multi-character operators (`>>>=`) and keywords (`instanceof`) each need a
6
+ * symbol of their own. Names are registered as one fixed alphabet and get a
7
+ * symbol from their position in it, above the Unicode range and below `eof`.
8
+ *
9
+ * @module
10
+ */
11
+ import { assert } from '../../asserts/module.f.js';
12
+ import { fromUndefined } from '../../types/nullable/module.f.js';
13
+ import { eof, rangeDecode, unicodeRange } from '../module.f.js';
14
+ const [, unicodeLast] = rangeDecode(unicodeRange);
15
+ const [eofSymbol] = rangeDecode(eof);
16
+ /**
17
+ * The symbol of the first registered name: `0x110000`, one past the last
18
+ * Unicode scalar value, so a token symbol can never be mistaken for a code
19
+ * point of the layer below.
20
+ */
21
+ const start = unicodeLast + 1;
22
+ /**
23
+ * How many names one encoding holds: every symbol from {@link start} up to but
24
+ * not including `eof` (`0xFFFFFF`, the top of the 24-bit symbol space).
25
+ */
26
+ export const capacity = eofSymbol - start;
27
+ /**
28
+ * Builds an encoding over the complete list of token names.
29
+ *
30
+ * A name takes the symbol at its index in `names`, so the list is append-only:
31
+ * inserting or reordering names changes the symbols of everything after the
32
+ * edit.
33
+ *
34
+ * @throws When `names` holds more than {@link capacity} entries, or when a name
35
+ * repeats — a repeated name has no single symbol to decode back to.
36
+ */
37
+ export const encoding = (names) => {
38
+ assert(names.length <= capacity, ['too many token names', names.length]);
39
+ assert(new Set(names).size === names.length, ['duplicate token name', names]);
40
+ return {
41
+ encode: name => {
42
+ const index = names.indexOf(name);
43
+ assert(index !== -1, ['unregistered token name', name]);
44
+ return start + index;
45
+ },
46
+ decode: symbol => fromUndefined(names[symbol - start]),
47
+ };
48
+ };
@@ -0,0 +1,10 @@
1
+ export declare const proof: {
2
+ encode: () => void;
3
+ decode: (() => void)[];
4
+ roundTrip: () => void;
5
+ throw: {
6
+ duplicateName: () => void;
7
+ unregisteredName: () => void;
8
+ tooManyNames: () => void;
9
+ };
10
+ };
@@ -0,0 +1,37 @@
1
+ import { assertEq } from '../../asserts/module.f.js';
2
+ import { capacity, encoding } from './module.f.js';
3
+ const names = ['>>', '>>>=', 'instanceof'];
4
+ export const proof = {
5
+ encode: () => {
6
+ const { encode } = encoding(names);
7
+ assertEq(encode('>>'), 0x110000);
8
+ assertEq(encode('instanceof'), 0x110002);
9
+ },
10
+ decode: [
11
+ () => {
12
+ const { decode } = encoding(names);
13
+ assertEq(decode(0x110000), '>>');
14
+ assertEq(decode(0x110002), 'instanceof');
15
+ },
16
+ () => {
17
+ const { decode } = encoding(names);
18
+ // past the end of the alphabet, a code point, and `eof`
19
+ assertEq(decode(0x110003), null);
20
+ assertEq(decode(0x10FFFF), null);
21
+ assertEq(decode(0xFFFFFF), null);
22
+ },
23
+ ],
24
+ roundTrip: () => {
25
+ const { encode, decode } = encoding(names);
26
+ for (const name of names) {
27
+ assertEq(decode(encode(name)), name);
28
+ }
29
+ },
30
+ throw: {
31
+ duplicateName: () => { encoding(['a', 'b', 'a']); },
32
+ unregisteredName: () => { encoding(['a']).encode('b'); },
33
+ // `capacity` names fit, so one more is the smallest list that doesn't.
34
+ // The array is sparse, so this costs a length, not the strings.
35
+ tooManyNames: () => { encoding(new Array(capacity + 1)); },
36
+ },
37
+ };
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { sha256 } from '../../crypto/sha2/module.f.js';
7
7
  import { cBase32ToVec, vecToCBase32 } from '../../basen/cbase32/module.f.js';
8
- import { forEachStep, pure } from '../../effects/module.f.js';
8
+ import { forEachStep, pure, step } from '../../effects/module.f.js';
9
9
  import { errorExit, log, writeFromStream } from '../../effects/node/module.f.js';
10
10
  import { dispatch } from '../../cli/module.f.js';
11
11
  import {} from '../../effects/memory/module.f.js';
@@ -19,10 +19,9 @@ export const commands = [
19
19
  return errorExit("'cas add' expects one parameter");
20
20
  }
21
21
  const c = fileCas(sha256)(home);
22
- return casAddFile(c)(path)
23
- .step(hashResult => hashResult[0] === 'error'
22
+ return step(casAddFile(c)(path), hashResult => hashResult[0] === 'error'
24
23
  ? pure(1)
25
- : log(vecToCBase32(hashResult[1])).step(() => pure(0)));
24
+ : step(log(vecToCBase32(hashResult[1])), () => pure(0)));
26
25
  },
27
26
  },
28
27
  {
@@ -38,8 +37,7 @@ export const commands = [
38
37
  }
39
38
  const c = fileCas(sha256)(home);
40
39
  const x = c.read(hash);
41
- return writeFromStream(path, x)
42
- .step(([r, v]) => r === 'error' ? errorExit(`e: ` + String(v)) : pure(0));
40
+ return step(writeFromStream(path, x), ([r, v]) => r === 'error' ? errorExit(`e: ` + String(v)) : pure(0));
43
41
  },
44
42
  },
45
43
  {
@@ -47,9 +45,8 @@ export const commands = [
47
45
  description: 'List all stored content hashes',
48
46
  handler: ({ home }) => {
49
47
  const c = fileCas(sha256)(home);
50
- return c.list()
51
- .step(forEachStep(j => log(vecToCBase32(j))))
52
- .step(() => pure(0));
48
+ const x0 = forEachStep(c.list(), j => log(vecToCBase32(j)));
49
+ return step(x0, () => pure(0));
53
50
  },
54
51
  },
55
52
  ];