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/fjs/ci/proof.f.d.ts
CHANGED
package/fjs/ci/proof.f.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ci, main } from './module.f.js';
|
|
2
|
-
import { functionalscript } from './config/module.f.js';
|
|
2
|
+
import { functionalscript, node } from './config/module.f.js';
|
|
3
|
+
import { nodeNixJobs } from './node/module.f.js';
|
|
3
4
|
import { utf8, utf8ToString } from '../text/module.f.js';
|
|
4
5
|
import { empty as emptyVec, isVec } from '../types/bit_vec/module.f.js';
|
|
5
6
|
import { test, ubuntu, parseGitHubAction } from './common/module.f.js';
|
|
@@ -19,15 +20,22 @@ const makeState = (rust, packageJson) => ({
|
|
|
19
20
|
...(rust ? { 'Cargo.toml': [emptyVec] } : {}),
|
|
20
21
|
},
|
|
21
22
|
});
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
assert(typeof
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
const subDir = (dir, name) => {
|
|
24
|
+
const entity = dir[name];
|
|
25
|
+
assert(typeof entity === 'object' && !Array.isArray(entity), entity);
|
|
26
|
+
return entity;
|
|
27
|
+
};
|
|
28
|
+
const text = (dir, name) => {
|
|
29
|
+
const file = dir[name];
|
|
28
30
|
assert(!(!Array.isArray(file) || file.length === 0), file);
|
|
29
|
-
return
|
|
31
|
+
return utf8ToString(file[0]);
|
|
30
32
|
};
|
|
33
|
+
const path = (dir, names) => names.reduce(subDir, dir);
|
|
34
|
+
const workflow = (state) => {
|
|
35
|
+
const workflows = path(state.root, ['.github', 'workflows']);
|
|
36
|
+
return unwrap(parseGitHubAction(jsonParse(text(workflows, 'ci.yml'))));
|
|
37
|
+
};
|
|
38
|
+
const flake = (state, id) => text(path(state.root, ['nix', 'generated', id]), 'flake.nix');
|
|
31
39
|
const run = (rust, nodeExtra = () => []) => {
|
|
32
40
|
const [state, result] = virtual(makeState(rust))(ci({ nodeExtra }));
|
|
33
41
|
assertEq(result, 0);
|
|
@@ -75,7 +83,6 @@ export const proof = {
|
|
|
75
83
|
'node22',
|
|
76
84
|
'node24',
|
|
77
85
|
'node26',
|
|
78
|
-
'playwright',
|
|
79
86
|
]) {
|
|
80
87
|
assert(hasRunInJob(id, 'npm ci')(gha), `expected npm ci in ${id}`);
|
|
81
88
|
}
|
|
@@ -135,6 +142,37 @@ export const proof = {
|
|
|
135
142
|
assert(hasRun(`npm install -g functionalscript@${functionalscript}`)(gha), 'expected configured-version install');
|
|
136
143
|
},
|
|
137
144
|
},
|
|
145
|
+
nixFlakes: () => {
|
|
146
|
+
const [state, result] = virtual(makeState(false))(main());
|
|
147
|
+
assertEq(result, 0);
|
|
148
|
+
for (const { id, packages } of nodeNixJobs) {
|
|
149
|
+
const [nodePackage] = packages;
|
|
150
|
+
assert(flake(state, id).includes(`pkgs.${nodePackage}`), `expected ${nodePackage} in the ${id} flake`);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
nixFlakeJob: () => {
|
|
154
|
+
const gha = run(false);
|
|
155
|
+
const job = gha.jobs['nix-flakes'];
|
|
156
|
+
assert(job !== undefined, 'expected the temporary flake job');
|
|
157
|
+
assert(job.steps.some(step => step.uses?.startsWith('cachix/install-nix-action@') === true), 'expected a pinned Nix installer');
|
|
158
|
+
// Exactly one check per unmigrated flake: none goes unchecked, and no
|
|
159
|
+
// check outlives the flake it was written for. A migrated job checks its
|
|
160
|
+
// own flake by running through it, so it is not covered here.
|
|
161
|
+
assertEq(job.steps.filter(step => step.run !== undefined).length, nodeNixJobs.length);
|
|
162
|
+
for (const { id } of nodeNixJobs) {
|
|
163
|
+
assert(hasRunInJob('nix-flakes', `nix develop ./nix/generated/${id} --command node --version`)(gha), `expected the ${id} flake to be instantiated`);
|
|
164
|
+
}
|
|
165
|
+
// Since the flakes no longer assert their own version, this job is the
|
|
166
|
+
// only place the Nix runtime is tied to the version `setup-node` installs.
|
|
167
|
+
for (const version of [node.node22, node.node24, node.default]) {
|
|
168
|
+
assert(hasRunInJob('nix-flakes', `= v${version}`)(gha), `expected the flake job to check Node ${version}`);
|
|
169
|
+
}
|
|
170
|
+
// The canonical Node jobs keep their current runtime setup until they
|
|
171
|
+
// are migrated one at a time.
|
|
172
|
+
for (const { id } of nodeNixJobs) {
|
|
173
|
+
assert(!hasRunInJob(id, 'nix develop')(gha), `unexpected nix develop in ${id}`);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
138
176
|
ubuntu: () => {
|
|
139
177
|
const job = ubuntu([test({ run: 'echo hi' })]);
|
|
140
178
|
assert(job['runs-on'] !== undefined, 'expected runs-on');
|
package/fjs/dev/module.f.d.ts
CHANGED
|
@@ -42,4 +42,5 @@ export type LoadModuleOperations = Access | Import | All | Readdir;
|
|
|
42
42
|
export declare const loadModuleMap: (env: Env) => Effect<LoadModuleOperations, ModuleMap>;
|
|
43
43
|
export declare const proof: {
|
|
44
44
|
isSourceFile: () => void;
|
|
45
|
+
allFilesSkipsNodeModules: () => void;
|
|
45
46
|
};
|
package/fjs/dev/module.f.js
CHANGED
|
@@ -8,7 +8,8 @@ import { cmp as strCmp } from '../types/string/module.f.js';
|
|
|
8
8
|
import { unwrap } from '../types/result/module.f.js';
|
|
9
9
|
import { pure, step } from '../effects/module.f.js';
|
|
10
10
|
import { join, relativize, toPosix } from '../path/module.f.js';
|
|
11
|
-
import { assert } from '../asserts/module.f.js';
|
|
11
|
+
import { assert, assertEq } from '../asserts/module.f.js';
|
|
12
|
+
import { emptyState, virtual } from '../effects/node/virtual/module.f.js';
|
|
12
13
|
/**
|
|
13
14
|
* Returns `true` if the file should be loaded for proof discovery.
|
|
14
15
|
*
|
|
@@ -72,7 +73,7 @@ export const loadModuleMap = (env) => {
|
|
|
72
73
|
const s = initCwd === undefined ? '.' : toPosix(initCwd);
|
|
73
74
|
const prefix = s === '.' ? '' : s;
|
|
74
75
|
// TODO: there are multiple `all` effects here,
|
|
75
|
-
// we should consider
|
|
76
|
+
// we should consider optimizing them by ALIQ technique or something similar.
|
|
76
77
|
// For example, we should be able to write it like `allFiles(s).flatMap(loadFile)`,
|
|
77
78
|
// then an effect runner can batch all file loading operations together.
|
|
78
79
|
const x0 = step(allFiles(s, shouldLoad), files => all(...files.map(loadFile)));
|
|
@@ -90,4 +91,14 @@ export const proof = {
|
|
|
90
91
|
assert(!isSourceFile('readme.md'));
|
|
91
92
|
assert(!isSourceFile('module.json'));
|
|
92
93
|
},
|
|
94
|
+
allFilesSkipsNodeModules: () => {
|
|
95
|
+
// `node_modules` is skipped without descending into it, even though
|
|
96
|
+
// it contains a file that would otherwise match the predicate.
|
|
97
|
+
const root = {
|
|
98
|
+
'node_modules': { 'pkg.f.ts': [] },
|
|
99
|
+
'a.f.ts': [],
|
|
100
|
+
};
|
|
101
|
+
const [, result] = virtual({ ...emptyState, root })(allFiles('.', shouldLoad));
|
|
102
|
+
assertEq(result.join(','), './a.f.ts');
|
|
103
|
+
},
|
|
93
104
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronizes local development configuration generated from canonical repository files.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { type Effect } from '../../effects/module.f.ts';
|
|
7
|
+
import { type Mkdir, type NodeProgram, type ReadFile, type WriteFile } from '../../effects/node/module.f.ts';
|
|
8
|
+
/** Regenerates VS Code's local MCP configuration from the canonical Copilot configuration. */
|
|
9
|
+
export declare const syncMcp: () => Effect<Mkdir | ReadFile | WriteFile, void>;
|
|
10
|
+
/** Runs all local development configuration generators. */
|
|
11
|
+
export declare const main: NodeProgram;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronizes local development configuration generated from canonical repository files.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { history, historyStep, mapStep, step } from '../../effects/module.f.js';
|
|
7
|
+
import { mkdir, readUtf8File, writeUtf8File } from '../../effects/node/module.f.js';
|
|
8
|
+
import { unwrap } from '../../types/result/module.f.js';
|
|
9
|
+
const source = '.copilot/mcp.json';
|
|
10
|
+
const targetDirectory = '.vscode';
|
|
11
|
+
const target = '.vscode/mcp.json';
|
|
12
|
+
/** Regenerates VS Code's local MCP configuration from the canonical Copilot configuration. */
|
|
13
|
+
export const syncMcp = () => {
|
|
14
|
+
const sourceText = history(mapStep(readUtf8File(source), unwrap));
|
|
15
|
+
const targetDirectoryReady = historyStep(sourceText, () => mapStep(mkdir(targetDirectory, { recursive: true }), unwrap));
|
|
16
|
+
const targetWritten = step(targetDirectoryReady, ([, text]) => writeUtf8File(target, text));
|
|
17
|
+
return mapStep(targetWritten, unwrap);
|
|
18
|
+
};
|
|
19
|
+
/** Runs all local development configuration generators. */
|
|
20
|
+
export const main = () => mapStep(syncMcp(), () => 0);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proofs for local development configuration generation.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { assert, assertEq } from '../../asserts/module.f.js';
|
|
7
|
+
import { utf8 } from '../../text/module.f.js';
|
|
8
|
+
import { readUtf8File } from '../../effects/node/module.f.js';
|
|
9
|
+
import { defaultNodeProgramOptions, emptyState, virtual } from '../../effects/node/virtual/module.f.js';
|
|
10
|
+
import { main, syncMcp } from './module.f.js';
|
|
11
|
+
import { step } from '../../effects/module.f.js';
|
|
12
|
+
const mcp = '{"servers":{}}';
|
|
13
|
+
const initial = {
|
|
14
|
+
...emptyState,
|
|
15
|
+
root: {
|
|
16
|
+
'.copilot': {
|
|
17
|
+
'mcp.json': [utf8(mcp)],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
export const proof = {
|
|
22
|
+
syncMcp: () => {
|
|
23
|
+
const generatedMcp = step(syncMcp(), () => readUtf8File('.vscode/mcp.json'));
|
|
24
|
+
const [, [tag, result]] = virtual(initial)(generatedMcp);
|
|
25
|
+
assert(tag === 'ok', result);
|
|
26
|
+
assertEq(result, mcp);
|
|
27
|
+
},
|
|
28
|
+
main: () => {
|
|
29
|
+
const [, result] = virtual(initial)(main(defaultNodeProgramOptions));
|
|
30
|
+
assertEq(result, 0);
|
|
31
|
+
},
|
|
32
|
+
throw: {
|
|
33
|
+
missingSource: () => virtual(emptyState)(syncMcp()),
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -4,12 +4,59 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
import type { Primitive, Array, Unknown } from '../module.f.ts';
|
|
7
|
+
/**
|
|
8
|
+
* A parsed DJS module: its imported module specifiers, in source order, and
|
|
9
|
+
* its body.
|
|
10
|
+
*
|
|
11
|
+
* The specifier list indexes `['aref', i]`.
|
|
12
|
+
*/
|
|
7
13
|
export type AstModule = [readonly string[], AstBody];
|
|
14
|
+
/** A value in a module body: a primitive, a reference, an array, or an object. */
|
|
8
15
|
export type AstConst = Primitive | AstModuleRef | AstArray | AstObject;
|
|
16
|
+
/**
|
|
17
|
+
* A reference to a value defined outside this `AstConst`.
|
|
18
|
+
*
|
|
19
|
+
* - `['aref', i]` — the `i`-th argument of the body, i.e. the `i`-th imported
|
|
20
|
+
* module of the enclosing `AstModule`.
|
|
21
|
+
* - `['cref', i]` — the `i`-th entry of the enclosing `AstBody`.
|
|
22
|
+
*
|
|
23
|
+
* Both indices are absolute and zero-based, **not** offsets from the
|
|
24
|
+
* referencing entry: in the body `[a, b, ['cref', 0]]` the reference resolves
|
|
25
|
+
* to `a`, not to the nearest preceding entry `b`.
|
|
26
|
+
*
|
|
27
|
+
* A `cref` index must be smaller than the index of the entry holding it —
|
|
28
|
+
* `run` evaluates a body left to right, so a reference to the current or a
|
|
29
|
+
* later entry is unsatisfiable. It is not rejected: it resolves to the most
|
|
30
|
+
* recently evaluated entry instead.
|
|
31
|
+
*/
|
|
9
32
|
export type AstModuleRef = ['aref' | 'cref', number];
|
|
33
|
+
/** An array value; its elements are evaluated in order. */
|
|
10
34
|
export type AstArray = ['array', readonly AstConst[]];
|
|
35
|
+
/** An object value, keyed by property name. */
|
|
11
36
|
export type AstObject = {
|
|
12
37
|
readonly [k in string]?: AstConst;
|
|
13
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* The constants of a module body, in declaration order. The **last** entry is
|
|
41
|
+
* the value the module yields; the preceding entries exist to be named by
|
|
42
|
+
* `['cref', i]`.
|
|
43
|
+
*
|
|
44
|
+
* A body describes the function
|
|
45
|
+
*
|
|
46
|
+
* ```js
|
|
47
|
+
* (...args) => { const c0 = ...; const c1 = ...; return <last> }
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* where `args` are the imported modules.
|
|
51
|
+
*/
|
|
14
52
|
export type AstBody = readonly AstConst[];
|
|
53
|
+
/**
|
|
54
|
+
* Evaluates a module body against its imported modules and returns the value
|
|
55
|
+
* the module yields — the last entry of the body.
|
|
56
|
+
*
|
|
57
|
+
* Entries are evaluated left to right, so a `cref` resolves to an already
|
|
58
|
+
* evaluated entry. A reference is shared, not copied: two properties holding
|
|
59
|
+
* the same `['cref', i]` deserialize to the same object, which is what lets a
|
|
60
|
+
* DJS module denote a graph rather than a tree.
|
|
61
|
+
*/
|
|
15
62
|
export declare const run: (body: AstBody) => (args: Array) => Unknown;
|
package/fjs/djs/ast/module.f.js
CHANGED
|
@@ -42,6 +42,15 @@ const toDjs = state => ast => {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Evaluates a module body against its imported modules and returns the value
|
|
47
|
+
* the module yields — the last entry of the body.
|
|
48
|
+
*
|
|
49
|
+
* Entries are evaluated left to right, so a `cref` resolves to an already
|
|
50
|
+
* evaluated entry. A reference is shared, not copied: two properties holding
|
|
51
|
+
* the same `['cref', i]` deserialize to the same object, which is what lets a
|
|
52
|
+
* DJS module denote a graph rather than a tree.
|
|
53
|
+
*/
|
|
45
54
|
export const run = body => args => {
|
|
46
55
|
const state = fold(foldOp)({ body, args, consts: null })(body);
|
|
47
56
|
return last(null)(state.consts);
|
|
@@ -364,7 +364,7 @@ export const tokenizeString = s => {
|
|
|
364
364
|
}
|
|
365
365
|
const m = descentParser(jsGrammar());
|
|
366
366
|
const cpm = codePointsWithMetadata('')(cp);
|
|
367
|
-
const
|
|
367
|
+
const { ast, success: ok, idx: len } = m('', cpm);
|
|
368
368
|
if (!ok || len !== cp.length)
|
|
369
369
|
return 'error';
|
|
370
370
|
const flatTokens = toArray(getTokensFromAstRule(ast));
|
|
@@ -394,7 +394,7 @@ export const tokenizeJs = input => path => {
|
|
|
394
394
|
return [{ token: { kind: 'eof' }, metadata: initial }];
|
|
395
395
|
const m = descentParser(jsGrammar());
|
|
396
396
|
const cpm = codePointsWithMetadata(path)(cp);
|
|
397
|
-
const
|
|
397
|
+
const { ast, success: ok, idx: len } = m('', cpm);
|
|
398
398
|
const finalMetadata = fold(advanceMetadata)(initial)(cp);
|
|
399
399
|
if (!ok || len !== cp.length) {
|
|
400
400
|
const errorMetadata = len < cpm.length ? cpm[len][1] : finalMetadata;
|
|
@@ -8,13 +8,21 @@ import { sort } from '../../types/object/module.f.js';
|
|
|
8
8
|
// DjsTokenWithMetadata carries bigint fields (bf, bigint value) that JSON.stringify can't
|
|
9
9
|
// serialize — reuse the same djs stringifyAsTree serializer tokenizeString uses internally.
|
|
10
10
|
const stringify = stringifyAsTree(sort);
|
|
11
|
+
// 'line:column' of the error token `s` tokenizes to, or 'no error'. Collapsing the
|
|
12
|
+
// position to one string keeps an expectation readable as the caret a reader would
|
|
13
|
+
// draw, instead of two separate assertions per case.
|
|
14
|
+
const errorAt = (s) => {
|
|
15
|
+
const found = toArray(tokenizeJs(stringToList(s))('a.js'))
|
|
16
|
+
.find(t => t.token.kind === 'error');
|
|
17
|
+
return found === undefined ? 'no error' : `${found.metadata.line}:${found.metadata.column}`;
|
|
18
|
+
};
|
|
11
19
|
export const proof = {
|
|
12
20
|
isValid: [() => {
|
|
13
21
|
const m = descentParser(jsGrammar());
|
|
14
22
|
const expect = (s, expected) => {
|
|
15
23
|
const cp = toArray(stringToCodePointList(s));
|
|
16
24
|
const mr = descentParserCpOnly(m, '', cp);
|
|
17
|
-
const success = mr
|
|
25
|
+
const success = mr.success && mr.idx === cp.length;
|
|
18
26
|
assertEq(success, expected, JSON.stringify([s, mr]));
|
|
19
27
|
};
|
|
20
28
|
expect('"', false);
|
|
@@ -51,7 +59,7 @@ export const proof = {
|
|
|
51
59
|
const m = descentParser(jsGrammar());
|
|
52
60
|
const cp = toArray(stringToCodePointList('tr'));
|
|
53
61
|
const mr = descentParserCpOnly(m, '', cp);
|
|
54
|
-
const seq = mr
|
|
62
|
+
const seq = mr.ast.sequence[0];
|
|
55
63
|
assert(!(seq instanceof Array), JSON.stringify(mr));
|
|
56
64
|
assertEq(seq.tag, 'id', JSON.stringify(mr));
|
|
57
65
|
},
|
|
@@ -59,7 +67,7 @@ export const proof = {
|
|
|
59
67
|
const m = descentParser(jsGrammar());
|
|
60
68
|
const cp = toArray(stringToCodePointList('"tr"'));
|
|
61
69
|
const mr = descentParserCpOnly(m, '', cp);
|
|
62
|
-
const seq = mr
|
|
70
|
+
const seq = mr.ast.sequence[0];
|
|
63
71
|
assert(!(seq instanceof Array), JSON.stringify(mr));
|
|
64
72
|
assertEq(seq.tag, 'string', JSON.stringify(mr));
|
|
65
73
|
},
|
|
@@ -67,7 +75,7 @@ export const proof = {
|
|
|
67
75
|
const m = descentParser(jsGrammar());
|
|
68
76
|
const cp = toArray(stringToCodePointList('56.7e+5'));
|
|
69
77
|
const mr = descentParserCpOnly(m, '', cp);
|
|
70
|
-
const seq = mr
|
|
78
|
+
const seq = mr.ast.sequence[0];
|
|
71
79
|
assert(!(seq instanceof Array), JSON.stringify(mr));
|
|
72
80
|
assertEq(seq.tag, 'number', JSON.stringify(mr));
|
|
73
81
|
},
|
|
@@ -75,7 +83,7 @@ export const proof = {
|
|
|
75
83
|
const m = descentParser(jsGrammar());
|
|
76
84
|
const cp = toArray(stringToCodePointList('56n'));
|
|
77
85
|
const mr = descentParserCpOnly(m, '', cp);
|
|
78
|
-
const seq = mr
|
|
86
|
+
const seq = mr.ast.sequence[0];
|
|
79
87
|
assert(!(seq instanceof Array), JSON.stringify(mr));
|
|
80
88
|
assertEq(seq.tag, 'number', JSON.stringify(mr));
|
|
81
89
|
},
|
|
@@ -83,7 +91,7 @@ export const proof = {
|
|
|
83
91
|
const m = descentParser(jsGrammar());
|
|
84
92
|
const cp = toArray(stringToCodePointList('*'));
|
|
85
93
|
const mr = descentParserCpOnly(m, '', cp);
|
|
86
|
-
const seq = mr
|
|
94
|
+
const seq = mr.ast.sequence[0];
|
|
87
95
|
assert(!(seq instanceof Array), JSON.stringify(mr));
|
|
88
96
|
assertEq(seq.tag, '*', JSON.stringify(mr));
|
|
89
97
|
},
|
|
@@ -91,7 +99,7 @@ export const proof = {
|
|
|
91
99
|
const m = descentParser(jsGrammar());
|
|
92
100
|
const cp = toArray(stringToCodePointList('**'));
|
|
93
101
|
const mr = descentParserCpOnly(m, '', cp);
|
|
94
|
-
const seq = mr
|
|
102
|
+
const seq = mr.ast.sequence[0];
|
|
95
103
|
assert(!(seq instanceof Array), JSON.stringify(mr));
|
|
96
104
|
assertEq(seq.tag, '**', JSON.stringify(mr));
|
|
97
105
|
},
|
|
@@ -99,7 +107,7 @@ export const proof = {
|
|
|
99
107
|
const m = descentParser(jsGrammar());
|
|
100
108
|
const cp = toArray(stringToCodePointList('=>'));
|
|
101
109
|
const mr = descentParserCpOnly(m, '', cp);
|
|
102
|
-
const seq = mr
|
|
110
|
+
const seq = mr.ast.sequence[0];
|
|
103
111
|
assert(!(seq instanceof Array), JSON.stringify(mr));
|
|
104
112
|
assertEq(seq.tag, '=>', JSON.stringify(mr));
|
|
105
113
|
},
|
|
@@ -107,7 +115,7 @@ export const proof = {
|
|
|
107
115
|
const m = descentParser(jsGrammar());
|
|
108
116
|
const cp = toArray(stringToCodePointList('=='));
|
|
109
117
|
const mr = descentParserCpOnly(m, '', cp);
|
|
110
|
-
const seq = mr
|
|
118
|
+
const seq = mr.ast.sequence[0];
|
|
111
119
|
assert(!(seq instanceof Array), JSON.stringify(mr));
|
|
112
120
|
assert(!(seq.tag !== '=='), JSON.stringify(mr));
|
|
113
121
|
},
|
|
@@ -115,7 +123,7 @@ export const proof = {
|
|
|
115
123
|
const m = descentParser(jsGrammar());
|
|
116
124
|
const cp = toArray(stringToCodePointList('==='));
|
|
117
125
|
const mr = descentParserCpOnly(m, '', cp);
|
|
118
|
-
const seq = mr
|
|
126
|
+
const seq = mr.ast.sequence[0];
|
|
119
127
|
assert(!(seq instanceof Array), JSON.stringify(mr));
|
|
120
128
|
assert(!(seq.tag !== '==='), JSON.stringify(mr));
|
|
121
129
|
},
|
|
@@ -123,7 +131,7 @@ export const proof = {
|
|
|
123
131
|
const m = descentParser(jsGrammar());
|
|
124
132
|
const cp = toArray(stringToCodePointList('='));
|
|
125
133
|
const mr = descentParserCpOnly(m, '', cp);
|
|
126
|
-
const seq = mr
|
|
134
|
+
const seq = mr.ast.sequence[0];
|
|
127
135
|
assert(!(seq instanceof Array), JSON.stringify(mr));
|
|
128
136
|
assertEq(seq.tag, '=', JSON.stringify(mr));
|
|
129
137
|
},
|
|
@@ -131,7 +139,7 @@ export const proof = {
|
|
|
131
139
|
const m = descentParser(jsGrammar());
|
|
132
140
|
const cp = toArray(stringToCodePointList(' '));
|
|
133
141
|
const mr = descentParserCpOnly(m, '', cp);
|
|
134
|
-
const seq = mr
|
|
142
|
+
const seq = mr.ast.sequence[0];
|
|
135
143
|
assert(!(seq instanceof Array), JSON.stringify(mr));
|
|
136
144
|
assertEq(seq.tag, ' ', JSON.stringify(mr));
|
|
137
145
|
},
|
|
@@ -139,7 +147,7 @@ export const proof = {
|
|
|
139
147
|
const m = descentParser(jsGrammar());
|
|
140
148
|
const cp = toArray(stringToCodePointList('\n'));
|
|
141
149
|
const mr = descentParserCpOnly(m, '', cp);
|
|
142
|
-
const seq = mr
|
|
150
|
+
const seq = mr.ast.sequence[0];
|
|
143
151
|
assert(!(seq instanceof Array), JSON.stringify(mr));
|
|
144
152
|
assertEq(seq.tag, '\n', JSON.stringify(mr));
|
|
145
153
|
},
|
|
@@ -147,7 +155,7 @@ export const proof = {
|
|
|
147
155
|
const m = descentParser(jsGrammar());
|
|
148
156
|
const cp = toArray(stringToCodePointList('/\n'));
|
|
149
157
|
const mr = descentParserCpOnly(m, '', cp);
|
|
150
|
-
const seq = mr
|
|
158
|
+
const seq = mr.ast.sequence[0];
|
|
151
159
|
assert(!(seq instanceof Array), JSON.stringify(mr));
|
|
152
160
|
assertEq(seq.tag, '/', JSON.stringify(mr));
|
|
153
161
|
},
|
|
@@ -155,7 +163,7 @@ export const proof = {
|
|
|
155
163
|
const m = descentParser(jsGrammar());
|
|
156
164
|
const cp = toArray(stringToCodePointList('//\n'));
|
|
157
165
|
const mr = descentParserCpOnly(m, '', cp);
|
|
158
|
-
const seq = mr
|
|
166
|
+
const seq = mr.ast.sequence[0];
|
|
159
167
|
assert(!(seq instanceof Array), JSON.stringify(mr));
|
|
160
168
|
assertEq(seq.tag, 'comment', JSON.stringify(mr));
|
|
161
169
|
},
|
|
@@ -163,7 +171,7 @@ export const proof = {
|
|
|
163
171
|
const m = descentParser(jsGrammar());
|
|
164
172
|
const cp = toArray(stringToCodePointList('/*1*/'));
|
|
165
173
|
const mr = descentParserCpOnly(m, '', cp);
|
|
166
|
-
const seq = mr
|
|
174
|
+
const seq = mr.ast.sequence[0];
|
|
167
175
|
assert(!(seq instanceof Array), JSON.stringify(mr));
|
|
168
176
|
assertEq(seq.tag, 'comment', JSON.stringify(mr));
|
|
169
177
|
}
|
|
@@ -1056,6 +1064,52 @@ export const proof = {
|
|
|
1056
1064
|
assertEq(JSON.stringify(result), '[{"token":{"kind":"error","message":"invalid token"},"metadata":{"path":"a.js","line":1,"column":2}}]');
|
|
1057
1065
|
},
|
|
1058
1066
|
],
|
|
1067
|
+
// Where an error token is reported. The `tokenizer` group above checks that
|
|
1068
|
+
// these inputs are rejected at all, through the metadata-free
|
|
1069
|
+
// `tokenizeString`; here the same shapes go through `tokenizeJs` so the
|
|
1070
|
+
// position is pinned too.
|
|
1071
|
+
errorPosition: [
|
|
1072
|
+
() => {
|
|
1073
|
+
// input that tokenizes cleanly produces no error token at all —
|
|
1074
|
+
// without this the helper's own no-error branch never runs
|
|
1075
|
+
assertEq(errorAt('x'), 'no error');
|
|
1076
|
+
assertEq(errorAt('{ "a": 1 }'), 'no error');
|
|
1077
|
+
},
|
|
1078
|
+
() => {
|
|
1079
|
+
// a character no rule accepts: reported where it stands
|
|
1080
|
+
assertEq(errorAt('ᄑ'), '1:1');
|
|
1081
|
+
},
|
|
1082
|
+
() => {
|
|
1083
|
+
// after a good prefix, the position advances past it
|
|
1084
|
+
assertEq(errorAt('x @'), '1:3');
|
|
1085
|
+
},
|
|
1086
|
+
() => {
|
|
1087
|
+
// line as well as column, once newlines have been consumed
|
|
1088
|
+
assertEq(errorAt('a\nb\n@'), '3:1');
|
|
1089
|
+
assertEq(errorAt('x\n\n @y'), '3:3');
|
|
1090
|
+
},
|
|
1091
|
+
() => {
|
|
1092
|
+
// a number poisoned by a trailing character points at that character
|
|
1093
|
+
assertEq(errorAt('00'), '1:2');
|
|
1094
|
+
},
|
|
1095
|
+
() => {
|
|
1096
|
+
// a number cut short points just past the input
|
|
1097
|
+
assertEq(errorAt('1.'), '1:3');
|
|
1098
|
+
},
|
|
1099
|
+
() => {
|
|
1100
|
+
// Unterminated tokens anchor at the token's *start*, not where the
|
|
1101
|
+
// input ran out: the grammar matches them and tags them
|
|
1102
|
+
// 'unterminated', so this is the structural-error path, not a failed
|
|
1103
|
+
// match. See ./todo/error-position-range.md.
|
|
1104
|
+
assertEq(errorAt('"value'), '1:1');
|
|
1105
|
+
assertEq(errorAt('"a\nb"'), '1:1');
|
|
1106
|
+
},
|
|
1107
|
+
() => {
|
|
1108
|
+
// an unterminated block comment is the exception: its content is
|
|
1109
|
+
// consumed, so the report lands at the end of input
|
|
1110
|
+
assertEq(errorAt('/* c'), '1:5');
|
|
1111
|
+
},
|
|
1112
|
+
],
|
|
1059
1113
|
// DJS-level: keyword remapping and '-'-folding on top of tokenizeJs. Doesn't re-test
|
|
1060
1114
|
// JS-level token shapes already covered above/elsewhere in this file.
|
|
1061
1115
|
djsTokenize: [
|
|
@@ -379,6 +379,26 @@ export type MatchResult<O extends Operation, T, R> = readonly ['done', T] | read
|
|
|
379
379
|
* with the continuation. The one world-specific step — `await` for async
|
|
380
380
|
* runners, state threading for sync ones — is left to the caller, so every
|
|
381
381
|
* interpreter loop is this skeleton plus a single eliminator line.
|
|
382
|
+
*
|
|
383
|
+
* **The handler is looked up with `at`, never with `map[command]`.**
|
|
384
|
+
* `OperationMap<O, R>` pins `command` to `O[0]` at the type level, but a `Do`
|
|
385
|
+
* node's `command` is runtime data — it can reach an interpreter from a decoded
|
|
386
|
+
* payload or a deserialized continuation, where no type ever constrained it.
|
|
387
|
+
* `map` is an ordinary object, so a plain index read resolves an inherited name
|
|
388
|
+
* (`'constructor'`, `'toString'`, `'hasOwnProperty'`) to the `Object.prototype`
|
|
389
|
+
* member instead of `undefined`, and the line below would then call it with the
|
|
390
|
+
* node's payload: a value the type system promised was `(...payload) => R` turns
|
|
391
|
+
* out to be an arbitrary inherited function, chosen by the same input that
|
|
392
|
+
* supplies its arguments. `at` reads through `getOwnPropertyDescriptor`, which
|
|
393
|
+
* only ever sees own properties, so such a command yields `null` and never a
|
|
394
|
+
* callable.
|
|
395
|
+
*
|
|
396
|
+
* A `null` handler is an invariant violation, not an outcome: every `O1 extends
|
|
397
|
+
* O` the signature admits has its command in `map`, so reaching it means the
|
|
398
|
+
* node's `command` was never the `O1[0]` it claimed to be. It therefore throws
|
|
399
|
+
* (`assert`) rather than widening {@link MatchResult} with a variant no
|
|
400
|
+
* type-correct caller could ever observe — a runner cannot resume a command it
|
|
401
|
+
* has no handler for, so there is nothing for a recovery branch to do.
|
|
382
402
|
*/
|
|
383
403
|
export declare const match: <O extends Operation, R>(map: OperationMap<O, R>) => <O1 extends O, T>(e: Effect<O1, T>) => MatchResult<O1, T, R>;
|
|
384
404
|
export type ToAsyncOperationMap<O extends Operation> = {
|
package/fjs/effects/module.f.js
CHANGED
|
@@ -94,7 +94,9 @@
|
|
|
94
94
|
*
|
|
95
95
|
* @module
|
|
96
96
|
*/
|
|
97
|
+
import { assert } from '../asserts/module.f.js';
|
|
97
98
|
import { fold } from '../types/list/module.f.js';
|
|
99
|
+
import { at } from '../types/object/module.f.js';
|
|
98
100
|
export const pure = (v) => () => v;
|
|
99
101
|
/**
|
|
100
102
|
* Composes effects: run `e`, then continue with `f` applied to its result.
|
|
@@ -278,11 +280,33 @@ export const runPure = (e) => typeof e === 'function' ? [e()] : [];
|
|
|
278
280
|
* with the continuation. The one world-specific step — `await` for async
|
|
279
281
|
* runners, state threading for sync ones — is left to the caller, so every
|
|
280
282
|
* interpreter loop is this skeleton plus a single eliminator line.
|
|
283
|
+
*
|
|
284
|
+
* **The handler is looked up with `at`, never with `map[command]`.**
|
|
285
|
+
* `OperationMap<O, R>` pins `command` to `O[0]` at the type level, but a `Do`
|
|
286
|
+
* node's `command` is runtime data — it can reach an interpreter from a decoded
|
|
287
|
+
* payload or a deserialized continuation, where no type ever constrained it.
|
|
288
|
+
* `map` is an ordinary object, so a plain index read resolves an inherited name
|
|
289
|
+
* (`'constructor'`, `'toString'`, `'hasOwnProperty'`) to the `Object.prototype`
|
|
290
|
+
* member instead of `undefined`, and the line below would then call it with the
|
|
291
|
+
* node's payload: a value the type system promised was `(...payload) => R` turns
|
|
292
|
+
* out to be an arbitrary inherited function, chosen by the same input that
|
|
293
|
+
* supplies its arguments. `at` reads through `getOwnPropertyDescriptor`, which
|
|
294
|
+
* only ever sees own properties, so such a command yields `null` and never a
|
|
295
|
+
* callable.
|
|
296
|
+
*
|
|
297
|
+
* A `null` handler is an invariant violation, not an outcome: every `O1 extends
|
|
298
|
+
* O` the signature admits has its command in `map`, so reaching it means the
|
|
299
|
+
* node's `command` was never the `O1[0]` it claimed to be. It therefore throws
|
|
300
|
+
* (`assert`) rather than widening {@link MatchResult} with a variant no
|
|
301
|
+
* type-correct caller could ever observe — a runner cannot resume a command it
|
|
302
|
+
* has no handler for, so there is nothing for a recovery branch to do.
|
|
281
303
|
*/
|
|
282
304
|
export const match = (map) => (e) => {
|
|
283
305
|
if (typeof e === 'function') {
|
|
284
306
|
return ['done', e()];
|
|
285
307
|
}
|
|
286
308
|
const { command, payload, continuation } = e;
|
|
287
|
-
|
|
309
|
+
const handler = at(command)(map);
|
|
310
|
+
assert(handler !== null, command);
|
|
311
|
+
return ['cont', handler(...payload), continuation];
|
|
288
312
|
};
|
|
@@ -4,9 +4,9 @@ import { type NodeProgram } from './module.f.ts';
|
|
|
4
4
|
* resolving to its exit code **without** terminating the process.
|
|
5
5
|
*
|
|
6
6
|
* Use this when the caller must stay alive afterwards — e.g. when proofs are
|
|
7
|
-
* registered under an external test runner (Node `--test`, Bun,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* registered under an external test runner (Node `--test`, Bun, Deno) that owns
|
|
8
|
+
* the process lifecycle. For a standalone CLI entry point that should exit with
|
|
9
|
+
* the program's code, use {@link run} instead.
|
|
10
10
|
*/
|
|
11
11
|
export declare const runEffect: (p: NodeProgram) => Promise<number>;
|
|
12
12
|
/**
|