functionalscript 0.39.0 → 0.41.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.
- package/README.md +1 -1
- package/fjs/bnf/descent/module.f.d.ts +43 -2
- package/fjs/bnf/descent/module.f.js +37 -12
- package/fjs/bnf/descent/proof.f.d.ts +1 -0
- package/fjs/bnf/descent/proof.f.js +73 -32
- package/fjs/bnf/ll1/module.f.js +2 -2
- package/fjs/cas/evo/module.f.d.ts +28 -8
- package/fjs/cas/evo/module.f.js +43 -10
- package/fjs/cas/evo/proof.f.d.ts +4 -0
- package/fjs/cas/evo/proof.f.js +82 -1
- package/fjs/ci/config/module.f.d.ts +10 -7
- package/fjs/ci/config/module.f.js +23 -8
- package/fjs/ci/module.f.js +12 -5
- package/fjs/ci/nix/module.f.d.ts +61 -0
- package/fjs/ci/nix/module.f.js +92 -0
- package/fjs/ci/nix/proof.f.d.ts +23 -0
- package/fjs/ci/nix/proof.f.js +109 -0
- package/fjs/ci/node/module.f.d.ts +23 -1
- package/fjs/ci/node/module.f.js +47 -3
- package/fjs/ci/node/proof.f.d.ts +3 -0
- package/fjs/ci/node/proof.f.js +17 -0
- package/fjs/ci/proof.f.d.ts +2 -0
- package/fjs/ci/proof.f.js +47 -9
- package/fjs/dev/module.f.d.ts +1 -0
- package/fjs/dev/module.f.js +13 -2
- package/fjs/dev/update/module.f.d.ts +11 -0
- package/fjs/dev/update/module.f.js +20 -0
- package/fjs/dev/update/proof.f.d.ts +7 -0
- package/fjs/dev/update/proof.f.js +35 -0
- package/fjs/djs/ast/module.f.d.ts +47 -0
- package/fjs/djs/ast/module.f.js +9 -0
- package/fjs/djs/tokenizer/module.f.js +2 -2
- package/fjs/djs/tokenizer/proof.f.d.ts +1 -0
- package/fjs/djs/tokenizer/proof.f.js +70 -16
- package/fjs/effects/module.f.d.ts +20 -0
- package/fjs/effects/module.f.js +25 -1
- package/fjs/effects/node/module.d.ts +3 -3
- package/fjs/effects/node/module.f.d.ts +17 -7
- package/fjs/effects/node/module.f.js +22 -0
- package/fjs/effects/node/module.js +12 -12
- package/fjs/effects/node/proof.f.d.ts +1 -0
- package/fjs/effects/node/proof.f.js +14 -1
- package/fjs/effects/node/virtual/module.f.js +1 -1
- package/fjs/effects/proof.f.d.ts +5 -0
- package/fjs/effects/proof.f.js +22 -0
- package/fjs/emergent_testing/all.test.js +2 -1
- package/fjs/emergent_testing/module.f.d.ts +3 -3
- package/fjs/emergent_testing/module.f.js +8 -10
- package/fjs/emergent_testing/proof.f.js +2 -2
- package/fjs/emergent_testing/scenarios/thenable.pass.js +1 -1
- package/fjs/fsc/module.f.js +4 -4
- package/fjs/fsm/module.f.js +1 -1
- package/fjs/js/tokenizer/module.f.d.ts +1 -0
- package/fjs/js/tokenizer/module.f.js +13 -6
- package/fjs/{cas/mcp → mcp/cas}/module.f.d.ts +3 -25
- package/fjs/{cas/mcp → mcp/cas}/module.f.js +18 -58
- package/fjs/mcp/evo/module.f.d.ts +32 -0
- package/fjs/{cas/evo/mcp → mcp/evo}/module.f.js +19 -14
- package/fjs/{cas/evo/mcp → mcp/evo}/proof.f.d.ts +1 -0
- package/fjs/{cas/evo/mcp → mcp/evo}/proof.f.js +23 -7
- package/fjs/mcp/module.f.d.ts +54 -237
- package/fjs/mcp/module.f.js +55 -258
- package/fjs/mcp/proof.f.d.ts +43 -32
- package/fjs/mcp/proof.f.js +508 -200
- package/fjs/media/nix/module.f.d.ts +30 -0
- package/fjs/media/nix/module.f.js +166 -0
- package/fjs/media/nix/proof.f.d.ts +32 -0
- package/fjs/media/nix/proof.f.js +127 -0
- package/fjs/module.f.js +1 -1
- package/fjs/protocol/json_rpc/module.f.d.ts +114 -0
- package/fjs/{media/json/rpc → protocol/json_rpc}/module.f.js +3 -3
- package/fjs/{media/json/rpc → protocol/json_rpc}/proof.f.js +3 -3
- package/fjs/protocol/mcp/module.f.d.ts +239 -0
- package/fjs/protocol/mcp/module.f.js +272 -0
- package/fjs/protocol/mcp/proof.f.d.ts +34 -0
- package/fjs/protocol/mcp/proof.f.js +208 -0
- package/fjs/{mcp → protocol/mcp}/stdio/module.f.d.ts +5 -5
- package/fjs/{mcp → protocol/mcp}/stdio/module.f.js +11 -11
- package/fjs/{mcp → protocol/mcp}/stdio/proof.f.js +9 -9
- package/fjs/types/range_map/module.f.d.ts +14 -13
- package/fjs/types/range_map/module.f.js +18 -13
- package/fjs/types/range_map/proof.f.js +26 -39
- package/fjs/types/range_set/module.f.d.ts +5 -0
- package/fjs/types/range_set/module.f.js +16 -0
- package/fjs/types/range_set/proof.f.d.ts +1 -0
- package/fjs/types/range_set/proof.f.js +19 -0
- package/package.json +5 -5
- package/fjs/cas/evo/mcp/module.f.d.ts +0 -27
- package/fjs/cas/mcp/proof.f.d.ts +0 -45
- package/fjs/cas/mcp/proof.f.js +0 -545
- package/fjs/ci/playwright/module.f.d.ts +0 -2
- package/fjs/ci/playwright/module.f.js +0 -25
- package/fjs/media/json/rpc/module.f.d.ts +0 -114
- /package/fjs/{media/json/rpc → protocol/json_rpc}/proof.f.d.ts +0 -0
- /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/
|
|
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
|
-
*
|
|
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
|
|
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) =>
|
|
33
|
-
const mrFail = (tag, sequence, 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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
106
|
-
const emptyResult = result
|
|
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 };
|
|
@@ -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 !== '
|
|
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, "",
|
|
97
|
+
const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('AF')));
|
|
94
98
|
const result = JSON.stringify(mr);
|
|
95
|
-
if (result !== '
|
|
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, "",
|
|
112
|
+
const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('A')));
|
|
103
113
|
const result = JSON.stringify(mr);
|
|
104
|
-
if (result !== '
|
|
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, "",
|
|
121
|
+
const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('@')));
|
|
112
122
|
const result = JSON.stringify(mr);
|
|
113
|
-
if (result !==
|
|
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, "",
|
|
130
|
+
const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('A')));
|
|
121
131
|
const result = JSON.stringify(mr);
|
|
122
|
-
if (result !==
|
|
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, "",
|
|
139
|
+
const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('@')));
|
|
130
140
|
const result = JSON.stringify(mr);
|
|
131
|
-
|
|
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 !== '
|
|
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, "",
|
|
160
|
+
const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('@')));
|
|
150
161
|
const result = JSON.stringify(mr);
|
|
151
|
-
if (result !== '
|
|
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
|
-
|
|
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, "",
|
|
180
|
+
const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('AB')));
|
|
168
181
|
const result = JSON.stringify(mr);
|
|
169
|
-
if (result !==
|
|
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, "",
|
|
189
|
+
const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('AC')));
|
|
177
190
|
const result = JSON.stringify(mr);
|
|
178
|
-
|
|
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, "",
|
|
204
|
+
const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('2')));
|
|
190
205
|
const result = JSON.stringify(mr);
|
|
191
|
-
if (result !==
|
|
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, "",
|
|
217
|
+
const mr = descentParserCpOnly(m, "", toArray(stringToCodePointList('-2')));
|
|
203
218
|
const result = JSON.stringify(mr);
|
|
204
|
-
if (result !==
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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("", [[
|
|
336
|
+
const mr = m("", [[cp1('-'), 'minus'], [cp1('2'), 'two']]);
|
|
321
337
|
const result = JSON.stringify(mr);
|
|
322
|
-
if (result !==
|
|
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
|
};
|
package/fjs/bnf/ll1/module.f.js
CHANGED
|
@@ -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(
|
|
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(
|
|
129
|
+
const dr = dispatchOp.get(rangeMap)(cp0);
|
|
130
130
|
if (dr === null) {
|
|
131
131
|
return emptyTag === undefined
|
|
132
132
|
? mrFail(emptyTag, [], cp)
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
* subject (see [`fjs/media/revision/README.md`](../../media/revision/README.md)).
|
|
18
18
|
* `Cache` therefore tracks, per subject, every revision hash seen and every
|
|
19
19
|
* hash referenced as somebody's parent; heads are the set difference between
|
|
20
|
-
* the two, computed at read time ({@link headsOf}).
|
|
20
|
+
* the two, computed at read time ({@link headsOf}). Alongside them it records
|
|
21
|
+
* which of the seen revisions are `archived`, so {@link Evo.list} can classify
|
|
22
|
+
* a subject as active or archived from its heads' flags
|
|
23
|
+
* ({@link subjectListed}) without touching the store. Storing both sets rather
|
|
21
24
|
* than a running head list is what makes folding revisions truly order
|
|
22
25
|
* independent: `cas.list()` (used by {@link buildCache} to scan an existing
|
|
23
26
|
* store) returns hashes in hash order, not revision ancestry, so a child can
|
|
@@ -97,14 +100,22 @@ export type RevisionData = {
|
|
|
97
100
|
readonly generation?: number | undefined;
|
|
98
101
|
};
|
|
99
102
|
/**
|
|
100
|
-
* Per-subject bookkeeping: every revision hash seen for the subject,
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* {@link headsOf} for how heads are
|
|
103
|
+
* Per-subject bookkeeping: every revision hash seen for the subject, every
|
|
104
|
+
* hash any of those revisions names as a parent, and which of the seen
|
|
105
|
+
* revisions are `archived`. See the module doc for why the first two sets are
|
|
106
|
+
* kept (rather than a running head list), {@link headsOf} for how heads are
|
|
107
|
+
* derived from them, and {@link subjectListed} for how `archived` classifies a
|
|
108
|
+
* subject once its heads are known.
|
|
109
|
+
*
|
|
110
|
+
* `archived` is keyed by revision hash, not by subject, for the same reason
|
|
111
|
+
* heads are computed at read time: which revisions are heads is only known
|
|
112
|
+
* once the whole store has been folded in, so a per-subject archived flag
|
|
113
|
+
* would have to be revised every time a later fold changes the head set.
|
|
104
114
|
*/
|
|
105
115
|
export type SubjectState = {
|
|
106
116
|
readonly hashes: readonly Hash[];
|
|
107
117
|
readonly parents: readonly Hash[];
|
|
118
|
+
readonly archived: readonly Hash[];
|
|
108
119
|
};
|
|
109
120
|
/** In-memory index: subject → its {@link SubjectState}. */
|
|
110
121
|
export type Cache = {
|
|
@@ -137,7 +148,7 @@ export declare const initEvo: <O extends Operation>(cas: Cas<O>) => Effect<O | M
|
|
|
137
148
|
* Folds `value` — bytes already written to a `Cas` at `hash` by some other
|
|
138
149
|
* caller — into the cache at `cacheKey` if it decodes as a `vnd.fjs.revision`
|
|
139
150
|
* ({@link decodeRevisionVec}); a no-op otherwise. `cas_add`/`evo_add`
|
|
140
|
-
* (`fjs/
|
|
151
|
+
* (`fjs/mcp`) are two ways to reach the same store — a plain `cas_add`
|
|
141
152
|
* call can store a revision blob without going through {@link addRevision},
|
|
142
153
|
* and this is what keeps the cache honest about it without rescanning the
|
|
143
154
|
* whole store.
|
|
@@ -182,8 +193,17 @@ export declare const addRevision: <O extends Operation>(cas: Cas<O>) => (cacheKe
|
|
|
182
193
|
export declare const readRevision: <O extends Operation>(cas: Cas<O>) => (hash: Hash) => Effect<O, Result<RevisionData, string>>;
|
|
183
194
|
/** The Evo API described in `fjs/cas/evo/README.md`, bound to a `Cas<O>` and its cache slot. */
|
|
184
195
|
export type Evo<O extends Operation> = {
|
|
185
|
-
/**
|
|
186
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Returns the subjects matching a status filter: the active ones by
|
|
198
|
+
* default, the archived ones when `archived` is `true`. A subject's status
|
|
199
|
+
* is derived from its current heads — see {@link subjectListed}, which
|
|
200
|
+
* also explains why a subject with no current heads is in neither result.
|
|
201
|
+
*
|
|
202
|
+
* There is deliberately no all-subjects mode: nothing needs one yet, and
|
|
203
|
+
* adding it later is a compatible extension of this parameter, while
|
|
204
|
+
* removing it would not be.
|
|
205
|
+
*/
|
|
206
|
+
readonly list: (archived?: true) => Effect<MemOp, readonly Subject[]>;
|
|
187
207
|
/** Returns the current head hashes of `subject` (empty if unknown). */
|
|
188
208
|
readonly head: (subject: Subject) => Effect<MemOp, readonly Hash[]>;
|
|
189
209
|
/** Adds a new head; see {@link addRevision}. */
|
package/fjs/cas/evo/module.f.js
CHANGED
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
* subject (see [`fjs/media/revision/README.md`](../../media/revision/README.md)).
|
|
18
18
|
* `Cache` therefore tracks, per subject, every revision hash seen and every
|
|
19
19
|
* hash referenced as somebody's parent; heads are the set difference between
|
|
20
|
-
* the two, computed at read time ({@link headsOf}).
|
|
20
|
+
* the two, computed at read time ({@link headsOf}). Alongside them it records
|
|
21
|
+
* which of the seen revisions are `archived`, so {@link Evo.list} can classify
|
|
22
|
+
* a subject as active or archived from its heads' flags
|
|
23
|
+
* ({@link subjectListed}) without touching the store. Storing both sets rather
|
|
21
24
|
* than a running head list is what makes folding revisions truly order
|
|
22
25
|
* independent: `cas.list()` (used by {@link buildCache} to scan an existing
|
|
23
26
|
* store) returns hashes in hash order, not revision ancestry, so a child can
|
|
@@ -58,7 +61,7 @@ import { isNotFound } from '../../effects/node/module.f.js';
|
|
|
58
61
|
export const emptyCache = { bySubject: {} };
|
|
59
62
|
/** Canonical JSON encoder for a `Revision` — key order carries no meaning for detection. */
|
|
60
63
|
const toJson = stringify(identity);
|
|
61
|
-
const emptySubjectState = { hashes: [], parents: [] };
|
|
64
|
+
const emptySubjectState = { hashes: [], parents: [], archived: [] };
|
|
62
65
|
/** Adds every item of `items` to `set` that isn't already there, preserving `set`'s existing order. */
|
|
63
66
|
const union = (set) => (items) => items.reduce((acc, h) => acc.includes(h) ? acc : [...acc, h], set);
|
|
64
67
|
/**
|
|
@@ -76,12 +79,37 @@ const union = (set) => (items) => items.reduce((acc, h) => acc.includes(h) ? acc
|
|
|
76
79
|
const canonicalHash = (h) => vecToCBase32(unwrap(cBase32ToVec(h)));
|
|
77
80
|
/** A subject's current heads: revision hashes seen that no other revision of the same subject names as a parent. */
|
|
78
81
|
const headsOf = (state) => state.hashes.filter(h => !state.parents.includes(h));
|
|
82
|
+
/**
|
|
83
|
+
* Whether a subject in `state` belongs in {@link Evo.list}'s result for the
|
|
84
|
+
* given `archived` filter — the subject-level status derived from its
|
|
85
|
+
* revision-level `archived` flags:
|
|
86
|
+
*
|
|
87
|
+
* - **active** — at least one current head is not archived. This is the
|
|
88
|
+
* default result set (`archived` omitted).
|
|
89
|
+
* - **archived** — the subject has at least one current head and every one of
|
|
90
|
+
* them is archived (`archived: true`).
|
|
91
|
+
*
|
|
92
|
+
* Concurrent heads can disagree, and the two rules resolve that the same way:
|
|
93
|
+
* one unarchived head keeps the whole subject active, because a subject is
|
|
94
|
+
* only done evolving when nothing left to build on remains. A subject with no
|
|
95
|
+
* current heads is neither active nor archived and appears in no result — the
|
|
96
|
+
* status is a statement about heads, and there is nothing to state. That case
|
|
97
|
+
* needs the explicit `heads.length` test only in the archived branch, since
|
|
98
|
+
* "every head is archived" is vacuously true of no heads at all.
|
|
99
|
+
*/
|
|
100
|
+
const subjectListed = (archived) => (state) => {
|
|
101
|
+
const heads = headsOf(state);
|
|
102
|
+
const unarchived = heads.filter(h => !state.archived.includes(h));
|
|
103
|
+
return archived === undefined
|
|
104
|
+
? unarchived.length !== 0
|
|
105
|
+
: heads.length !== 0 && unarchived.length === 0;
|
|
106
|
+
};
|
|
79
107
|
/**
|
|
80
108
|
* Folds one more stored revision into `cache`: `hash` joins its subject's
|
|
81
|
-
* `hashes` set,
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* incremental `add`.
|
|
109
|
+
* `hashes` set, `revision.parents` (canonicalized, see {@link canonicalHash})
|
|
110
|
+
* join its `parents` set, and `hash` also joins the `archived` set when the
|
|
111
|
+
* revision carries `archived: true`. Order independent (see the module doc) —
|
|
112
|
+
* used both for a full-store scan and for a single incremental `add`.
|
|
85
113
|
*
|
|
86
114
|
* Looks `revision.subject` up via {@link at} (own-property only), not plain
|
|
87
115
|
* bracket indexing: a subject is an arbitrary caller-supplied string
|
|
@@ -96,6 +124,7 @@ const addRevisionToCache = (hash, revision) => (cache) => {
|
|
|
96
124
|
const state = {
|
|
97
125
|
hashes: union(existing.hashes)([hash]),
|
|
98
126
|
parents: union(existing.parents)(revision.parents.map(canonicalHash)),
|
|
127
|
+
archived: union(existing.archived)(revision.archived === undefined ? [] : [hash]),
|
|
99
128
|
};
|
|
100
129
|
return { bySubject: { ...cache.bySubject, [revision.subject]: state } };
|
|
101
130
|
};
|
|
@@ -141,7 +170,7 @@ const foldIntoCache = (cacheKey) => (hash) => (revision) => eff(read(cacheKey))
|
|
|
141
170
|
* Folds `value` — bytes already written to a `Cas` at `hash` by some other
|
|
142
171
|
* caller — into the cache at `cacheKey` if it decodes as a `vnd.fjs.revision`
|
|
143
172
|
* ({@link decodeRevisionVec}); a no-op otherwise. `cas_add`/`evo_add`
|
|
144
|
-
* (`fjs/
|
|
173
|
+
* (`fjs/mcp`) are two ways to reach the same store — a plain `cas_add`
|
|
145
174
|
* call can store a revision blob without going through {@link addRevision},
|
|
146
175
|
* and this is what keeps the cache honest about it without rescanning the
|
|
147
176
|
* whole store.
|
|
@@ -407,9 +436,13 @@ export const readRevision = (cas) => (hash) => {
|
|
|
407
436
|
};
|
|
408
437
|
/** Builds the {@link Evo} API over `cas`, backed by the cache at `cacheKey` (see {@link initEvo}). */
|
|
409
438
|
export const evo = (cas) => (cacheKey) => ({
|
|
410
|
-
list:
|
|
411
|
-
|
|
412
|
-
|
|
439
|
+
list: archived => {
|
|
440
|
+
const listed = subjectListed(archived);
|
|
441
|
+
return eff(read(cacheKey))
|
|
442
|
+
.step(cache => pure(definedEntries(cache.bySubject)
|
|
443
|
+
.flatMap(([subject, state]) => listed(state) ? [subject] : [])))
|
|
444
|
+
.value;
|
|
445
|
+
},
|
|
413
446
|
head: subject => eff(read(cacheKey))
|
|
414
447
|
.step(cache => {
|
|
415
448
|
const state = at(subject)(cache.bySubject);
|
package/fjs/cas/evo/proof.f.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export declare const proof: {
|
|
|
9
9
|
buildCacheOrderIndependentWhenChildScannedBeforeParent: () => void;
|
|
10
10
|
buildCacheCanonicalizesNonCanonicalParentHashes: () => void;
|
|
11
11
|
addRevisionBuildsHeadsAcrossChainAndFork: () => void;
|
|
12
|
+
listPartitionsSubjectsByHeadArchivedFlag: () => void;
|
|
13
|
+
listTreatsDisagreeingHeadsAsActive: () => void;
|
|
14
|
+
listIgnoresArchivedRevisionsThatAreNoLongerHeads: () => void;
|
|
15
|
+
listExcludesSubjectWithNoCurrentHeads: () => void;
|
|
12
16
|
addRevisionIdempotentOnDuplicateContent: () => void;
|
|
13
17
|
addRevisionCanonicalizesParentSpellingBeforeSerializing: () => void;
|
|
14
18
|
addRevisionResolvesSubjectFromSingleParent: () => void;
|