functionalscript 0.39.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.
- 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 +1 -1
- package/fjs/cas/evo/module.f.js +1 -1
- package/fjs/cas/evo/proof.f.js +1 -1
- package/fjs/ci/config/module.f.d.ts +7 -4
- package/fjs/ci/config/module.f.js +20 -5
- 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/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/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/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 +27 -0
- package/fjs/{cas/evo/mcp → mcp/evo}/module.f.js +9 -9
- package/fjs/{cas/evo/mcp → mcp/evo}/proof.f.js +7 -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/{cas/evo/mcp → mcp/evo}/proof.f.d.ts +0 -0
- /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)
|
|
@@ -137,7 +137,7 @@ export declare const initEvo: <O extends Operation>(cas: Cas<O>) => Effect<O | M
|
|
|
137
137
|
* Folds `value` — bytes already written to a `Cas` at `hash` by some other
|
|
138
138
|
* caller — into the cache at `cacheKey` if it decodes as a `vnd.fjs.revision`
|
|
139
139
|
* ({@link decodeRevisionVec}); a no-op otherwise. `cas_add`/`evo_add`
|
|
140
|
-
* (`fjs/
|
|
140
|
+
* (`fjs/mcp`) are two ways to reach the same store — a plain `cas_add`
|
|
141
141
|
* call can store a revision blob without going through {@link addRevision},
|
|
142
142
|
* and this is what keeps the cache honest about it without rescanning the
|
|
143
143
|
* whole store.
|
package/fjs/cas/evo/module.f.js
CHANGED
|
@@ -141,7 +141,7 @@ const foldIntoCache = (cacheKey) => (hash) => (revision) => eff(read(cacheKey))
|
|
|
141
141
|
* Folds `value` — bytes already written to a `Cas` at `hash` by some other
|
|
142
142
|
* caller — into the cache at `cacheKey` if it decodes as a `vnd.fjs.revision`
|
|
143
143
|
* ({@link decodeRevisionVec}); a no-op otherwise. `cas_add`/`evo_add`
|
|
144
|
-
* (`fjs/
|
|
144
|
+
* (`fjs/mcp`) are two ways to reach the same store — a plain `cas_add`
|
|
145
145
|
* call can store a revision blob without going through {@link addRevision},
|
|
146
146
|
* and this is what keeps the cache honest about it without rescanning the
|
|
147
147
|
* whole store.
|
package/fjs/cas/evo/proof.f.js
CHANGED
|
@@ -518,7 +518,7 @@ export const proof = {
|
|
|
518
518
|
// A raw CAS write (e.g. `cas_add`) of valid revision content is folded
|
|
519
519
|
// into the cache exactly as `addRevision` would, without going through
|
|
520
520
|
// `evo.add` — this is what keeps `cas_add` and `evo_add` writes to the
|
|
521
|
-
// same store consistent (see `fjs/
|
|
521
|
+
// same store consistent (see `fjs/mcp`).
|
|
522
522
|
syncRevisionFoldsValidRevisionIntoCache: () => {
|
|
523
523
|
const c = fileCas(sha256)(home);
|
|
524
524
|
const [state0, cacheKey] = virtual(emptyState)(initEvo(c));
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralized version pins and OS images used by the CI generator: runner
|
|
3
|
-
* images, tool versions (Bun, Deno,
|
|
4
|
-
* TSGO).
|
|
3
|
+
* images, tool versions (Bun, Deno, Rust, Node, Wasmtime, Wasmer, TSGO).
|
|
5
4
|
*
|
|
6
5
|
* @module
|
|
7
6
|
*/
|
|
@@ -22,12 +21,15 @@ export declare const images: {
|
|
|
22
21
|
export declare const functionalscript: '0.38.0';
|
|
23
22
|
export declare const bun = "1.3.14";
|
|
24
23
|
export declare const deno = "2.9.4";
|
|
25
|
-
export declare const playwright = "1.62.0";
|
|
26
24
|
export declare const node: {
|
|
27
|
-
readonly default: '26.5.
|
|
25
|
+
readonly default: '26.5.1';
|
|
28
26
|
readonly node22: '22.23.1';
|
|
29
27
|
readonly node24: '24.18.0';
|
|
30
28
|
};
|
|
29
|
+
export declare const nixpkgs: {
|
|
30
|
+
readonly ref: 'nixos-26.05';
|
|
31
|
+
readonly commit: '21ea275a7c46aef9d4d6ddc962e6d562e9d94183';
|
|
32
|
+
};
|
|
31
33
|
export declare const wasmtime = "47.0.2";
|
|
32
34
|
export declare const wasmer = "7.2.1";
|
|
33
35
|
export declare const actions: {
|
|
@@ -38,5 +40,6 @@ export declare const actions: {
|
|
|
38
40
|
readonly 'oven-sh/setup-bun': 'v2.2.0';
|
|
39
41
|
readonly 'bytecodealliance/actions/wasmtime/setup': 'v1.1.3';
|
|
40
42
|
readonly 'wasmerio/setup-wasmer': 'v3.1';
|
|
43
|
+
readonly 'cachix/install-nix-action': 'v31.11.0';
|
|
41
44
|
readonly 'dtolnay/rust-toolchain': '1.97.1';
|
|
42
45
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralized version pins and OS images used by the CI generator: runner
|
|
3
|
-
* images, tool versions (Bun, Deno,
|
|
4
|
-
* TSGO).
|
|
3
|
+
* images, tool versions (Bun, Deno, Rust, Node, Wasmtime, Wasmer, TSGO).
|
|
5
4
|
*
|
|
6
5
|
* @module
|
|
7
6
|
*/
|
|
@@ -29,14 +28,27 @@ export const functionalscript = '0.38.0';
|
|
|
29
28
|
export const bun = '1.3.14';
|
|
30
29
|
// https://deno.com/
|
|
31
30
|
export const deno = '2.9.4';
|
|
32
|
-
//
|
|
33
|
-
|
|
31
|
+
// The Node versions the pinned Nixpkgs snapshot below provides — read from
|
|
32
|
+
// `pkgs/development/web/nodejs/v{22,24,26}.nix` at that commit. Every runtime
|
|
33
|
+
// uses these: `setup-node` on the GitHub-hosted runners and the generated
|
|
34
|
+
// flakes on the Nix jobs, which assert the version they actually get. Nixpkgs
|
|
35
|
+
// usually trails nodejs.org, so bump the snapshot first and copy the versions
|
|
36
|
+
// it offers rather than the latest release.
|
|
34
37
|
// https://nodejs.org/en/download
|
|
35
38
|
export const node = {
|
|
36
|
-
default: '26.5.
|
|
39
|
+
default: '26.5.1',
|
|
37
40
|
node22: '22.23.1',
|
|
38
41
|
node24: '24.18.0',
|
|
39
42
|
};
|
|
43
|
+
// Official Nixpkgs snapshot used by the generated CI flakes. `ref` is the
|
|
44
|
+
// stable channel the commit is accepted from; `commit` is the exact revision
|
|
45
|
+
// every generated `flake.nix` pins. The Node versions above come from this
|
|
46
|
+
// snapshot, so the two move together.
|
|
47
|
+
// https://channels.nixos.org/nixos-26.05/git-revision
|
|
48
|
+
export const nixpkgs = {
|
|
49
|
+
ref: 'nixos-26.05',
|
|
50
|
+
commit: '21ea275a7c46aef9d4d6ddc962e6d562e9d94183',
|
|
51
|
+
};
|
|
40
52
|
// https://github.com/bytecodealliance/wasmtime/releases
|
|
41
53
|
export const wasmtime = '47.0.2';
|
|
42
54
|
// https://github.com/wasmerio/wasmer/releases
|
|
@@ -60,6 +72,9 @@ export const actions = {
|
|
|
60
72
|
'bytecodealliance/actions/wasmtime/setup': 'v1.1.3',
|
|
61
73
|
// https://github.com/wasmerio/setup-wasmer
|
|
62
74
|
'wasmerio/setup-wasmer': 'v3.1',
|
|
75
|
+
// https://github.com/marketplace/actions/install-nix
|
|
76
|
+
// Enables the `nix-command` and `flakes` experimental features by default.
|
|
77
|
+
'cachix/install-nix-action': 'v31.11.0',
|
|
63
78
|
// https://rust-lang.org/ - value is Rust version, not action version
|
|
64
79
|
'dtolnay/rust-toolchain': '1.97.1',
|
|
65
80
|
};
|
package/fjs/ci/module.f.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { mapStep, step } from '../effects/module.f.js';
|
|
7
7
|
import { access, writeUtf8File } from '../effects/node/module.f.js';
|
|
8
8
|
import { functionalscript, images } from './config/module.f.js';
|
|
9
9
|
import { architecture, os, toSteps, ubuntuArm } from './common/module.f.js';
|
|
10
10
|
import { rustPlatformSteps, rustWasmSteps } from './rust/module.f.js';
|
|
11
|
-
import { nodeMainSteps, nodeVersionJobs } from './node/module.f.js';
|
|
12
|
-
import {
|
|
11
|
+
import { nodeMainSteps, nodeNixJobs, nodeNixVersionSteps, nodeVersionJobs } from './node/module.f.js';
|
|
12
|
+
import { nixFlakes, nixInstall } from './nix/module.f.js';
|
|
13
13
|
import { bunSteps } from './bun/module.f.js';
|
|
14
14
|
import { denoSteps } from './deno/module.f.js';
|
|
15
15
|
const job = (rust, nodeExtra) => (o) => (a) => {
|
|
@@ -22,12 +22,17 @@ const job = (rust, nodeExtra) => (o) => (a) => {
|
|
|
22
22
|
];
|
|
23
23
|
return [id, { 'runs-on': image, steps: toSteps(result) }];
|
|
24
24
|
};
|
|
25
|
+
// Every generated flake, across all job families that own one.
|
|
26
|
+
const nixJobs = nodeNixJobs;
|
|
27
|
+
// Temporary: proves the not-yet-migrated flakes still evaluate. Removed once
|
|
28
|
+
// the canonical Node jobs check their own flake by running through it.
|
|
29
|
+
const nixFlakeJob = ubuntuArm([nixInstall, ...nodeNixVersionSteps]);
|
|
25
30
|
const canonicalJobs = (rust) => ({
|
|
26
31
|
...(rust ? { wasm: ubuntuArm(rustWasmSteps) } : {}),
|
|
27
32
|
deno: ubuntuArm(denoSteps(functionalscript)),
|
|
28
33
|
bun: ubuntuArm(bunSteps(functionalscript)),
|
|
29
34
|
...nodeVersionJobs(functionalscript),
|
|
30
|
-
|
|
35
|
+
'nix-flakes': nixFlakeJob,
|
|
31
36
|
});
|
|
32
37
|
export const ci = ({ nodeExtra }) => step(access('Cargo.toml'), result => {
|
|
33
38
|
const rust = result[0] === 'ok';
|
|
@@ -46,6 +51,8 @@ export const ci = ({ nodeExtra }) => step(access('Cargo.toml'), result => {
|
|
|
46
51
|
},
|
|
47
52
|
jobs,
|
|
48
53
|
};
|
|
49
|
-
|
|
54
|
+
const workflowWritten = writeUtf8File('.github/workflows/ci.yml', JSON.stringify(gha, null, ' '));
|
|
55
|
+
const flakesWritten = step(workflowWritten, () => nixFlakes(nixJobs));
|
|
56
|
+
return mapStep(flakesWritten, () => 0);
|
|
50
57
|
});
|
|
51
58
|
export const main = () => ci({ nodeExtra: () => [] });
|