functionalscript 0.38.0 → 0.39.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/fjs/bnf/token_symbol/module.f.d.ts +38 -0
- package/fjs/bnf/token_symbol/module.f.js +48 -0
- package/fjs/bnf/token_symbol/proof.f.d.ts +10 -0
- package/fjs/bnf/token_symbol/proof.f.js +37 -0
- package/fjs/cas/cli/module.f.js +6 -9
- package/fjs/cas/evo/mcp/module.f.d.ts +9 -1
- package/fjs/cas/evo/mcp/module.f.js +52 -14
- package/fjs/cas/evo/mcp/proof.f.d.ts +2 -0
- package/fjs/cas/evo/mcp/proof.f.js +33 -2
- package/fjs/cas/evo/module.f.d.ts +76 -12
- package/fjs/cas/evo/module.f.js +129 -17
- package/fjs/cas/evo/proof.f.d.ts +7 -0
- package/fjs/cas/evo/proof.f.js +119 -0
- package/fjs/cas/mcp/module.f.js +22 -20
- package/fjs/cas/mcp/proof.f.d.ts +1 -1
- package/fjs/cas/mcp/proof.f.js +9 -9
- package/fjs/cas/module.f.js +64 -37
- package/fjs/cas/proof.f.d.ts +3 -9
- package/fjs/cas/proof.f.js +94 -74
- package/fjs/ci/bun/module.f.js +3 -3
- package/fjs/ci/common/module.f.d.ts +1 -2
- package/fjs/ci/common/module.f.js +1 -5
- package/fjs/ci/config/module.f.d.ts +5 -5
- package/fjs/ci/config/module.f.js +5 -5
- package/fjs/ci/deno/module.f.js +3 -3
- package/fjs/ci/module.f.js +3 -4
- package/fjs/ci/node/module.f.js +11 -9
- package/fjs/ci/proof.f.js +2 -0
- package/fjs/cli/module.f.js +2 -2
- package/fjs/common/monoid/module.f.d.ts +46 -2
- package/fjs/common/monoid/module.f.js +44 -0
- package/fjs/common/monoid/proof.f.d.ts +5 -0
- package/fjs/common/monoid/proof.f.js +27 -1
- package/fjs/dev/module.f.js +25 -25
- package/fjs/djs/module.f.js +5 -8
- package/fjs/djs/parser/module.f.d.ts +3 -0
- package/fjs/djs/parser/module.f.js +17 -0
- package/fjs/djs/transpiler/module.f.js +6 -6
- package/fjs/effects/eff/module.f.d.ts +72 -0
- package/fjs/effects/eff/module.f.js +46 -0
- package/fjs/effects/eff/proof.f.d.ts +11 -0
- package/fjs/effects/eff/proof.f.js +58 -0
- package/fjs/effects/list/module.f.d.ts +11 -5
- package/fjs/effects/list/module.f.js +2 -1
- package/fjs/effects/memory/proof.f.js +7 -8
- package/fjs/effects/module.f.d.ts +334 -52
- package/fjs/effects/module.f.js +253 -45
- package/fjs/effects/node/memory/proof.js +3 -2
- package/fjs/effects/node/module.f.d.ts +1 -1
- package/fjs/effects/node/module.f.js +9 -11
- package/fjs/effects/node/proof.f.js +18 -10
- package/fjs/effects/proof.f.d.ts +32 -6
- package/fjs/effects/proof.f.js +131 -39
- package/fjs/emergent_testing/module.f.js +55 -39
- package/fjs/emergent_testing/proof.f.d.ts +4 -0
- package/fjs/emergent_testing/proof.f.js +53 -20
- package/fjs/mcp/module.f.js +8 -8
- package/fjs/mcp/proof.f.js +5 -4
- package/fjs/mcp/stdio/module.f.d.ts +2 -2
- package/fjs/mcp/stdio/module.f.js +12 -12
- package/fjs/media/type/module.f.js +2 -2
- package/fjs/media/type/proof.f.js +7 -10
- package/fjs/module.f.js +4 -8
- package/fjs/types/bigint/module.f.js +4 -3
- package/fjs/types/bit_vec/module.f.js +5 -15
- package/fjs/types/btree/remove/module.f.d.ts +5 -0
- package/fjs/types/btree/remove/module.f.js +12 -0
- package/fjs/types/list/module.f.d.ts +21 -0
- package/fjs/types/list/module.f.js +17 -0
- package/fjs/types/list/proof.f.d.ts +1 -0
- package/fjs/types/list/proof.f.js +24 -1
- package/fjs/types/number/module.f.js +2 -1
- package/fjs/types/string/module.f.js +10 -2
- package/fjs/website/module.f.js +2 -3
- package/package.json +3 -3
package/fjs/effects/proof.f.js
CHANGED
|
@@ -1,72 +1,93 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { step, do_, foldStep, forEachStep, mapStep, match, okStep, history, pure, runPure, historyStep } from './module.f.js';
|
|
2
2
|
import { error, ok } from '../types/result/module.f.js';
|
|
3
3
|
import { assert, assertEq } from '../asserts/module.f.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Asserts that `e` yields `expected` without performing a command. Exported so
|
|
6
|
+
* `eff/proof.f.ts` shares this definition instead of repeating it.
|
|
7
|
+
*
|
|
8
|
+
* Not `assertEq(runPure(e), [expected])`: `assertEq` compares with `===`, so a
|
|
9
|
+
* freshly allocated `[expected]` is never equal to the returned option. Assert
|
|
10
|
+
* the option's shape first, then compare the value inside it.
|
|
11
|
+
*/
|
|
12
|
+
export const assertPure = (e, expected) => {
|
|
13
|
+
const o = runPure(e);
|
|
14
|
+
assert(o.length === 1, e);
|
|
15
|
+
assertEq(o[0], expected);
|
|
8
16
|
};
|
|
9
17
|
const next = match({ add: (a, b) => a + b });
|
|
10
18
|
export const proof = {
|
|
11
|
-
lazy: {
|
|
12
|
-
value: () => {
|
|
13
|
-
assertPure(lazy(() => 42), 42);
|
|
14
|
-
},
|
|
15
|
-
deferred: () => {
|
|
16
|
-
// The thunk runs only when the effect is decoded, not when `lazy` is called.
|
|
17
|
-
let evaluated = false;
|
|
18
|
-
const e = lazy(() => { evaluated = true; return 7; });
|
|
19
|
-
assert(!(evaluated), 'lazy must not evaluate eagerly');
|
|
20
|
-
assertPure(e, 7);
|
|
21
|
-
assert(evaluated, 'decode must force the thunk');
|
|
22
|
-
},
|
|
23
|
-
step: () => {
|
|
24
|
-
const e = lazy(() => 5).step(v => pure(v * 2));
|
|
25
|
-
assertPure(e, 10);
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
19
|
foldStep: {
|
|
29
20
|
empty: () => {
|
|
30
|
-
const e = foldStep((
|
|
21
|
+
const e = foldStep(pure([]), 10, x => s => pure(s + x));
|
|
31
22
|
assertPure(e, 10);
|
|
32
23
|
},
|
|
33
24
|
threadsState: () => {
|
|
34
|
-
const e = foldStep((
|
|
25
|
+
const e = foldStep(pure([1, 2, 3, 4]), 0, x => s => pure(s + x));
|
|
35
26
|
assertPure(e, 10);
|
|
36
27
|
},
|
|
37
28
|
order: () => {
|
|
38
|
-
const e = foldStep((
|
|
29
|
+
const e = foldStep(pure(['a', 'b', 'c']), '', x => s => pure(s + x));
|
|
39
30
|
assertPure(e, 'abc');
|
|
40
31
|
},
|
|
41
32
|
},
|
|
42
33
|
forEachStep: {
|
|
43
34
|
empty: () => {
|
|
44
|
-
const e = forEachStep(() => pure(undefined))
|
|
35
|
+
const e = forEachStep(pure([]), () => pure(undefined));
|
|
45
36
|
assertPure(e, undefined);
|
|
46
37
|
},
|
|
47
38
|
runs: () => {
|
|
48
|
-
const e = forEachStep(
|
|
39
|
+
const e = forEachStep(pure([1, 2, 3]), () => pure(undefined));
|
|
49
40
|
assertPure(e, undefined);
|
|
50
41
|
},
|
|
51
42
|
},
|
|
52
43
|
okStep: {
|
|
53
44
|
ok: () => {
|
|
54
|
-
const e = pure(ok(5))
|
|
55
|
-
const
|
|
56
|
-
assert(
|
|
45
|
+
const e = step(pure(ok(5)), okStep((v) => pure(ok(v * 2))));
|
|
46
|
+
const o = runPure(e);
|
|
47
|
+
assert(o.length === 1, e);
|
|
48
|
+
const [r] = o;
|
|
49
|
+
assert(r[0] === 'ok', r);
|
|
50
|
+
assertEq(r[1], 10);
|
|
57
51
|
},
|
|
58
52
|
error: () => {
|
|
59
|
-
const e = pure(error('oops'))
|
|
60
|
-
const
|
|
61
|
-
assert(
|
|
53
|
+
const e = step(pure(error('oops')), okStep(v => pure(ok(v * 2))));
|
|
54
|
+
const o = runPure(e);
|
|
55
|
+
assert(o.length === 1, e);
|
|
56
|
+
const [r] = o;
|
|
57
|
+
assert(r[0] === 'error', r);
|
|
58
|
+
assertEq(r[1], 'oops');
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
runPure: {
|
|
62
|
+
pure: () => {
|
|
63
|
+
const o = runPure(pure(5));
|
|
64
|
+
assert(o.length === 1, o);
|
|
65
|
+
assertEq(o[0], 5);
|
|
66
|
+
},
|
|
67
|
+
// A pure `null` is `[null]`, never `[]`. Collapsing the two is what a
|
|
68
|
+
// `T | null` result would do, and why the option is tagged.
|
|
69
|
+
pureNull: () => {
|
|
70
|
+
const o = runPure(pure(null));
|
|
71
|
+
assertEq(o.length, 1, o);
|
|
72
|
+
assertEq(o[0], null);
|
|
73
|
+
},
|
|
74
|
+
do_: () => {
|
|
75
|
+
assertEq(runPure(do_('add')(2, 3)).length, 0);
|
|
62
76
|
},
|
|
63
77
|
},
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
// The one place a `Do` node is opened on purpose: `do_` builds the command,
|
|
79
|
+
// the payload it was called with, and a continuation that resumes with the
|
|
80
|
+
// command's output. Everything else goes through `step`, `match`, or
|
|
81
|
+
// `runPure`.
|
|
82
|
+
doNode: () => {
|
|
83
|
+
const e = do_('add')(2, 3);
|
|
84
|
+
assert(typeof e !== 'function', e);
|
|
85
|
+
const { command, payload, continuation } = e;
|
|
86
|
+
assertEq(command, 'add');
|
|
87
|
+
const [a, b] = payload;
|
|
88
|
+
assertEq(a, 2, payload);
|
|
89
|
+
assertEq(b, 3, payload);
|
|
90
|
+
assertPure(continuation(5), 5);
|
|
70
91
|
},
|
|
71
92
|
match: {
|
|
72
93
|
done: () => {
|
|
@@ -83,4 +104,75 @@ export const proof = {
|
|
|
83
104
|
assertEq(r2[1], 5);
|
|
84
105
|
},
|
|
85
106
|
},
|
|
107
|
+
step: {
|
|
108
|
+
pure: () => {
|
|
109
|
+
assertPure(step(pure(3), v => pure(v + 1)), 4);
|
|
110
|
+
},
|
|
111
|
+
chain: () => {
|
|
112
|
+
// Chains as step(step(e, f), g), raw effect in and out.
|
|
113
|
+
assertPure(step(step(pure(3), v => pure(v + 1)), v => pure(v * 2)), 8);
|
|
114
|
+
},
|
|
115
|
+
overDo: () => {
|
|
116
|
+
// Stepping a Do node preserves the command and threads the result
|
|
117
|
+
// through the rebuilt continuation.
|
|
118
|
+
const e = step(do_('add')(2, 3), r => pure(r * 10));
|
|
119
|
+
const r = next(e);
|
|
120
|
+
assert(r[0] === 'cont', r);
|
|
121
|
+
assertEq(r[1], 5);
|
|
122
|
+
assertPure(r[2](r[1]), 50);
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
mapStep: {
|
|
126
|
+
pure: () => {
|
|
127
|
+
assertPure(mapStep(pure(3), v => v + 1), 4);
|
|
128
|
+
},
|
|
129
|
+
constant: () => {
|
|
130
|
+
// The `() => v` shape a `constStep` would have covered.
|
|
131
|
+
assertPure(mapStep(pure('ignored'), () => 1), 1);
|
|
132
|
+
},
|
|
133
|
+
overDo: () => {
|
|
134
|
+
// A projection over a `Do` node keeps the command intact and is
|
|
135
|
+
// applied to the command's output when the continuation resumes.
|
|
136
|
+
const r = next(mapStep(do_('add')(2, 3), v => v * 10));
|
|
137
|
+
assert(r[0] === 'cont', r);
|
|
138
|
+
assertEq(r[1], 5);
|
|
139
|
+
assertPure(r[2](r[1]), 50);
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
historyStep: {
|
|
143
|
+
pure: () => {
|
|
144
|
+
const o = runPure(historyStep(history(pure(3)), v => pure(v * 2)));
|
|
145
|
+
assert(o.length === 1, o);
|
|
146
|
+
const [[result, param]] = o;
|
|
147
|
+
assertEq(param, 3);
|
|
148
|
+
assertEq(result, 6);
|
|
149
|
+
},
|
|
150
|
+
overDo: () => {
|
|
151
|
+
// The captured value survives a command boundary: the history is
|
|
152
|
+
// rebuilt inside the continuation rather than lost when `e` is a Do.
|
|
153
|
+
const c = next(historyStep(history(do_('add')(2, 3)), r => pure(r * 10)));
|
|
154
|
+
assert(c[0] === 'cont', c);
|
|
155
|
+
assertEq(c[1], 5);
|
|
156
|
+
const o = runPure(c[2](c[1]));
|
|
157
|
+
assert(o.length === 1, o);
|
|
158
|
+
const [[result, param]] = o;
|
|
159
|
+
assertEq(param, 5);
|
|
160
|
+
assertEq(result, 50);
|
|
161
|
+
},
|
|
162
|
+
chain: () => {
|
|
163
|
+
// `historyStep` takes a history and returns one, so link two is
|
|
164
|
+
// spelled exactly like link one. The tuple is newest first, so a
|
|
165
|
+
// destructuring reads reverse-chronologically.
|
|
166
|
+
const a = historyStep(history(pure(1)), x => pure(x + 1));
|
|
167
|
+
const b = historyStep(a, (result, param) => pure(result + param));
|
|
168
|
+
assertPure(step(b, ([z, y, x]) => pure(`${x}${y}${z}`)), '123');
|
|
169
|
+
},
|
|
170
|
+
fReceivesWholeHistory: () => {
|
|
171
|
+
// `f` is handed the whole history spread as arguments, not just the
|
|
172
|
+
// most recent value - which is what lets a later link reach back.
|
|
173
|
+
const a = historyStep(history(pure(1)), x => pure(x + 1));
|
|
174
|
+
const b = historyStep(a, (...p) => pure(p[1] * 100 + p[0]));
|
|
175
|
+
assertPure(step(b, ([result]) => pure(result)), 102);
|
|
176
|
+
},
|
|
177
|
+
},
|
|
86
178
|
};
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { reset, fgGreen, fgRed, bold, csiWrite } from '../text/sgr/module.f.js';
|
|
14
14
|
import { all, awaitIfPromise, sandbox, test } from '../effects/node/module.f.js';
|
|
15
|
-
import { pure } from '../effects/module.f.js';
|
|
15
|
+
import { history, historyStep, pure, step } from '../effects/module.f.js';
|
|
16
|
+
import { eff } from '../effects/eff/module.f.js';
|
|
16
17
|
import { loadModuleMap, shouldLoad } from '../dev/module.f.js';
|
|
17
18
|
import { invert } from '../types/result/module.f.js';
|
|
18
19
|
import { definedEntries } from '../types/object/module.f.js';
|
|
@@ -89,7 +90,7 @@ export const registerModule = (ctx, k, v, star) => {
|
|
|
89
90
|
// test is expected to throw, so no extra suffix is needed.
|
|
90
91
|
const base = fmtImport(k, path);
|
|
91
92
|
const name = throws ? base : `${base}${star}`;
|
|
92
|
-
return test(ctx, name, throws, (t) => awaitIfPromise(fn())
|
|
93
|
+
return test(ctx, name, throws, (t) => eff(awaitIfPromise(fn()))
|
|
93
94
|
.step(resolved => {
|
|
94
95
|
if (throws) {
|
|
95
96
|
return pure(undefined);
|
|
@@ -98,42 +99,49 @@ export const registerModule = (ctx, k, v, star) => {
|
|
|
98
99
|
if (sub.length === 0) {
|
|
99
100
|
return pure(undefined);
|
|
100
101
|
}
|
|
101
|
-
return all(...sub.map(e => registerOne(t, e)))
|
|
102
|
-
|
|
102
|
+
return eff(all(...sub.map(e => registerOne(t, e))))
|
|
103
|
+
.step(() => pure(undefined))
|
|
104
|
+
.value;
|
|
105
|
+
})
|
|
106
|
+
.value);
|
|
103
107
|
};
|
|
104
108
|
const tests = collectTests([], false, v);
|
|
105
109
|
if (tests.length === 0) {
|
|
106
110
|
return pure(undefined);
|
|
107
111
|
}
|
|
108
|
-
return all(...tests.map(e => registerOne(ctx, e)))
|
|
112
|
+
return eff(all(...tests.map(e => registerOne(ctx, e))))
|
|
113
|
+
.step(() => pure(undefined))
|
|
114
|
+
.value;
|
|
109
115
|
};
|
|
110
116
|
const mergeState = (a, b) => ({ time: a.time + b.time, pass: a.pass + b.pass, fail: a.fail + b.fail });
|
|
111
117
|
const zero = { time: 0, pass: 0, fail: 0 };
|
|
112
118
|
const runModule = ({ result, test }) => (k, v) => (ts) => {
|
|
113
|
-
const one = ([testPath, set]) =>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
// Walk return-value sub-tree; null marks the call boundary so
|
|
123
|
-
// paths render as e.g. `outer().inner`. throws resets to false.
|
|
124
|
-
return walk([...testPath, null], false, r)
|
|
125
|
-
.step(sub => pure(mergeState(addPass(duration)(zero), sub)));
|
|
119
|
+
const one = ([testPath, set]) => {
|
|
120
|
+
// The sandbox result is still needed after it has been reported, so the
|
|
121
|
+
// reporting call is captured rather than nested inside its own step.
|
|
122
|
+
const reported = historyStep(history(test(k, testPath, set)), sr => result(k, testPath, sr, set.throws));
|
|
123
|
+
return step(reported, ([, sr]) => {
|
|
124
|
+
const { result: [s, r], duration } = sr;
|
|
125
|
+
if (s !== 'ok') {
|
|
126
|
+
return pure(addFail(duration)(zero));
|
|
126
127
|
}
|
|
127
|
-
|
|
128
|
+
if (set.throws) {
|
|
129
|
+
return pure(addPass(duration)(zero));
|
|
130
|
+
}
|
|
131
|
+
// Walk return-value sub-tree; null marks the call boundary so
|
|
132
|
+
// paths render as e.g. `outer().inner`. throws resets to false.
|
|
133
|
+
return step(walk([...testPath, null], false, r), sub => pure(mergeState(addPass(duration)(zero), sub)));
|
|
128
134
|
});
|
|
129
|
-
}
|
|
135
|
+
};
|
|
130
136
|
const walk = (path, throws, v) => {
|
|
131
137
|
const effects = collectTests(path, throws, v).map(one);
|
|
132
|
-
return all(...effects)
|
|
133
|
-
.step(states => pure(states.reduce(mergeState, zero)))
|
|
138
|
+
return eff(all(...effects))
|
|
139
|
+
.step(states => pure(states.reduce(mergeState, zero)))
|
|
140
|
+
.value;
|
|
134
141
|
};
|
|
135
|
-
return walk([], false, v)
|
|
136
|
-
.step(delta => pure(mergeState(ts, delta)))
|
|
142
|
+
return eff(walk([], false, v))
|
|
143
|
+
.step(delta => pure(mergeState(ts, delta)))
|
|
144
|
+
.value;
|
|
137
145
|
};
|
|
138
146
|
const proofEntries = (moduleMap) => definedEntries(moduleMap)
|
|
139
147
|
.flatMap(([k, v]) => v.proof !== undefined ? [[k, v.proof]] : []);
|
|
@@ -145,17 +153,20 @@ const proofEntries = (moduleMap) => definedEntries(moduleMap)
|
|
|
145
153
|
export const runModuleMap = (reporter) => (moduleMap) => {
|
|
146
154
|
const { summary } = reporter;
|
|
147
155
|
const modules = proofEntries(moduleMap);
|
|
148
|
-
return all(...modules.map(([k, v]) => runModule(reporter)(k, v)(zero)))
|
|
156
|
+
return eff(all(...modules.map(([k, v]) => runModule(reporter)(k, v)(zero))))
|
|
149
157
|
.step(m => pure(m.reduce(mergeState, zero)))
|
|
150
|
-
.step(ts => summary(ts.pass, ts.fail, ts.time)
|
|
151
|
-
.step(() => pure(ts.fail !== 0 ? 1 : 0))
|
|
158
|
+
.step(ts => summary(ts.pass, ts.fail, ts.time))
|
|
159
|
+
.step((_, ts) => pure(ts.fail !== 0 ? 1 : 0))
|
|
160
|
+
.value;
|
|
152
161
|
};
|
|
153
162
|
/**
|
|
154
163
|
* Discovers all test modules via `loadModuleMap`, then runs them through
|
|
155
164
|
* `runModuleMap`. The composed effect is a `NodeProgram` entry point for the
|
|
156
165
|
* `fjs t` test runner.
|
|
157
166
|
*/
|
|
158
|
-
export const testAll = (reporter) => options => loadModuleMap(options.env)
|
|
167
|
+
export const testAll = (reporter) => options => eff(loadModuleMap(options.env))
|
|
168
|
+
.step(runModuleMap(reporter))
|
|
169
|
+
.value;
|
|
159
170
|
/**
|
|
160
171
|
* Registers all modules in `moduleMap` that export a `proof` property with
|
|
161
172
|
* `ctx`. Delegates to `registerModule` for each matching entry.
|
|
@@ -165,7 +176,9 @@ const registerModuleMap = (ctx, star) => (moduleMap) => {
|
|
|
165
176
|
if (modules.length === 0) {
|
|
166
177
|
return pure(undefined);
|
|
167
178
|
}
|
|
168
|
-
return all(...modules.map(([k, v]) => registerModule(ctx, k, v, star)))
|
|
179
|
+
return eff(all(...modules.map(([k, v]) => registerModule(ctx, k, v, star))))
|
|
180
|
+
.step(() => pure(undefined))
|
|
181
|
+
.value;
|
|
169
182
|
};
|
|
170
183
|
const isAlpha = (c) => (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c === '_' || c === '$';
|
|
171
184
|
const isDigit = (c) => c >= '0' && c <= '9';
|
|
@@ -219,8 +232,9 @@ export const ghEscape = (s) => s.replaceAll('%', '%25')
|
|
|
219
232
|
* Default `Reporter.test` implementation: sandboxes `fn` once and inverts the
|
|
220
233
|
* result when `throws` is `true` (caught error → pass, clean return → fail).
|
|
221
234
|
*/
|
|
222
|
-
export const defaultTest = (file, path, { fn, throws }) => sandbox(fn)
|
|
223
|
-
.step(r => pure(throws ? { ...r, result: invert(r.result) } : r))
|
|
235
|
+
export const defaultTest = (file, path, { fn, throws }) => eff(sandbox(fn))
|
|
236
|
+
.step(r => pure(throws ? { ...r, result: invert(r.result) } : r))
|
|
237
|
+
.value;
|
|
224
238
|
const fmtResultLine = (file, path, color, label, duration) => `${fmtImport(file, path)}: ${color}${label}${reset}, ${timeFormat(duration)}`;
|
|
225
239
|
/**
|
|
226
240
|
* The terminal/GitHub reporter used by `fjs t`. Output goes through
|
|
@@ -244,12 +258,14 @@ export const defaultReporter = (options) => {
|
|
|
244
258
|
? csiLog(fmtResultLine(file, path, fgGreen, 'ok', duration) + (throws ? ' # EXPECTED TO THROW' : ''))
|
|
245
259
|
: isGitHub
|
|
246
260
|
? csiError(`::error file=${file},line=1,title=${ghEscape(fmtImport(file, path))}::${ghEscape(String(v))}`)
|
|
247
|
-
: csiError(fmtResultLine(file, path, fgRed, 'error', duration))
|
|
248
|
-
.step(() => csiError(`${fgRed}${v}${reset}`))
|
|
261
|
+
: eff(csiError(fmtResultLine(file, path, fgRed, 'error', duration)))
|
|
262
|
+
.step(() => csiError(`${fgRed}${v}${reset}`))
|
|
263
|
+
.value,
|
|
249
264
|
summary: (pass, fail, time) => {
|
|
250
265
|
const fgFail = fail === 0 ? fgGreen : fgRed;
|
|
251
|
-
return csiLog(`${bold}Number of tests: pass: ${fgGreen}${pass}${reset}${bold}, fail: ${fgFail}${fail}${reset}${bold}, total: ${pass + fail}${reset}`)
|
|
252
|
-
.step(() => csiLog(`${bold}Time: ${timeFormat(time)}${reset}`))
|
|
266
|
+
return eff(csiLog(`${bold}Number of tests: pass: ${fgGreen}${pass}${reset}${bold}, fail: ${fgFail}${fail}${reset}${bold}, total: ${pass + fail}${reset}`))
|
|
267
|
+
.step(() => csiLog(`${bold}Time: ${timeFormat(time)}${reset}`))
|
|
268
|
+
.value;
|
|
253
269
|
},
|
|
254
270
|
test: defaultTest,
|
|
255
271
|
};
|
|
@@ -268,8 +284,8 @@ export const register = o => {
|
|
|
268
284
|
const ctx = o.engine === 'bun' ? o.bunTestContext :
|
|
269
285
|
o.engine === 'playwright' ? o.playwrightTestContext :
|
|
270
286
|
o.testContext;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
.step(
|
|
274
|
-
.
|
|
287
|
+
return eff(loadModuleMap(o.env))
|
|
288
|
+
.step(registerModuleMap(ctx, star))
|
|
289
|
+
.step(() => pure(0))
|
|
290
|
+
.value;
|
|
275
291
|
};
|
|
@@ -14,6 +14,8 @@ export declare const defaultReporterOutputLargeDuration: () => void;
|
|
|
14
14
|
export declare const defaultReporterFailOutput: () => void;
|
|
15
15
|
export declare const githubReporterOutput: () => void;
|
|
16
16
|
export declare const registerSuffixes: () => void;
|
|
17
|
+
export declare const registerThrowsWithoutThrowing: () => void;
|
|
18
|
+
export declare const registerEmptyProof: () => void;
|
|
17
19
|
export declare const helpers: {
|
|
18
20
|
isInteger: () => void;
|
|
19
21
|
isIdentifier: () => void;
|
|
@@ -45,6 +47,8 @@ export declare const proof: {
|
|
|
45
47
|
defaultReporterFailOutput: typeof defaultReporterFailOutput;
|
|
46
48
|
githubReporterOutput: typeof githubReporterOutput;
|
|
47
49
|
registerSuffixes: typeof registerSuffixes;
|
|
50
|
+
registerThrowsWithoutThrowing: typeof registerThrowsWithoutThrowing;
|
|
51
|
+
registerEmptyProof: typeof registerEmptyProof;
|
|
48
52
|
defaultReporterExpectedToThrow: typeof defaultReporterExpectedToThrow;
|
|
49
53
|
helpers: {
|
|
50
54
|
isInteger: () => void;
|
|
@@ -218,41 +218,72 @@ export const githubReporterOutput = () => {
|
|
|
218
218
|
assertEq(exit, 1);
|
|
219
219
|
assertEq(stderr, '::error file=./s.proof.f.ts,line=1,title=import("./s.proof.f.ts").proof["a%3Ab%2Cc%25d"]()::oops\n');
|
|
220
220
|
};
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
const registerNoopCtx = { test: (_n, _o, _f) => Promise.resolve() };
|
|
222
|
+
/**
|
|
223
|
+
* Builds a synchronous mock runner for `registerModule`'s `Test`/`All`/`Await`
|
|
224
|
+
* effect operations. Only the `test` op varies between call sites (whether it
|
|
225
|
+
* invokes the registered callback), so `all`/`await` are shared here.
|
|
226
|
+
*/
|
|
227
|
+
const makeRegisterRunner = (testOp) => {
|
|
224
228
|
let runner;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const [ns, r] = runner(st)(e);
|
|
233
|
-
st = ns;
|
|
234
|
-
rs.push(r);
|
|
235
|
-
}
|
|
236
|
-
return [st, rs];
|
|
237
|
-
},
|
|
238
|
-
await: p => (s) => [s, [p]],
|
|
229
|
+
runner = mockRun({
|
|
230
|
+
test: (ctx, name, xf, fn) => testOp(runner, ctx, name, xf, fn),
|
|
231
|
+
all: (...effects) => (s) => effects.reduce(([st, rs], e) => {
|
|
232
|
+
const [ns, r] = runner(st)(e);
|
|
233
|
+
return [ns, [...rs, r]];
|
|
234
|
+
}, [s, []]),
|
|
235
|
+
await: (p) => (s) => [s, [p]],
|
|
239
236
|
});
|
|
240
|
-
runner
|
|
237
|
+
return runner;
|
|
238
|
+
};
|
|
239
|
+
// registerModule appends ' ...' for inline runners (Bun/Playwright).
|
|
240
|
+
// This mock never invokes the registered callback; it only records names.
|
|
241
|
+
export const registerSuffixes = () => {
|
|
242
|
+
const runner = makeRegisterRunner((_runner, _ctx, name, _xf, _fn) => (s) => [[...s, name], undefined]);
|
|
241
243
|
const proof = {
|
|
242
244
|
ok: () => { },
|
|
243
245
|
throw: { a: () => { throw 'expected'; } },
|
|
244
246
|
};
|
|
245
247
|
// Node (star = ''): no suffixes
|
|
246
|
-
const [nodeNames] = runner([])(registerModule(
|
|
248
|
+
const [nodeNames] = runner([])(registerModule(registerNoopCtx, './a.f.ts', proof, ''));
|
|
247
249
|
assertEq(nodeNames.length, 2);
|
|
248
250
|
assert(nodeNames[0] === 'import("./a.f.ts").proof.ok()');
|
|
249
251
|
assertEq(nodeNames[1], 'import("./a.f.ts").proof.throw.a()');
|
|
250
252
|
// Bun/Playwright (star = ' ...'): ... on normal tests, path shows throw for throw-tests
|
|
251
|
-
const [inlineNames] = runner([])(registerModule(
|
|
253
|
+
const [inlineNames] = runner([])(registerModule(registerNoopCtx, './a.f.ts', proof, ' ...'));
|
|
252
254
|
assertEq(inlineNames.length, 2);
|
|
253
255
|
assert(inlineNames[0] === 'import("./a.f.ts").proof.ok() ...');
|
|
254
256
|
assertEq(inlineNames[1], 'import("./a.f.ts").proof.throw.a()');
|
|
255
257
|
};
|
|
258
|
+
// A `throw`-tagged test whose function completes without throwing (the
|
|
259
|
+
// external framework, not this module, is responsible for turning that into
|
|
260
|
+
// a failure via `expectFailure`). registerModule's own callback must still
|
|
261
|
+
// short-circuit before walking the returned value for sub-tests: it invokes
|
|
262
|
+
// the callback and returns without recursing, rather than treating the
|
|
263
|
+
// returned object as a sub-tree.
|
|
264
|
+
export const registerThrowsWithoutThrowing = () => {
|
|
265
|
+
// Unlike registerSuffixes' mock, this one actually invokes the registered
|
|
266
|
+
// callback so registerOne's inner `.step` body runs, and asserts the
|
|
267
|
+
// callback is registered with `expectFailure: true`.
|
|
268
|
+
const runner = makeRegisterRunner((runner, ctx, name, xf, fn) => (s) => {
|
|
269
|
+
assert(xf);
|
|
270
|
+
const [ns] = runner(s)(fn(ctx));
|
|
271
|
+
return [[...ns, name], undefined];
|
|
272
|
+
});
|
|
273
|
+
// Returns a sub-tree that would register more tests if it were walked.
|
|
274
|
+
const proof = { throw: { a: () => ({ sub: () => { } }) } };
|
|
275
|
+
const [names] = runner([])(registerModule(registerNoopCtx, './a.f.ts', proof, ''));
|
|
276
|
+
// Only the throw-test itself is registered; `sub` is never reached.
|
|
277
|
+
assertEq(names.length, 1);
|
|
278
|
+
assertEq(names[0], 'import("./a.f.ts").proof.throw.a()');
|
|
279
|
+
};
|
|
280
|
+
// registerModule with an empty proof object registers zero tests and
|
|
281
|
+
// returns without invoking the mock's `test` op at all.
|
|
282
|
+
export const registerEmptyProof = () => {
|
|
283
|
+
const runner = makeRegisterRunner((_runner, _ctx, name, _xf, _fn) => (s) => [[...s, name], undefined]);
|
|
284
|
+
const [names] = runner([])(registerModule(registerNoopCtx, './a.f.ts', {}, ''));
|
|
285
|
+
assertEq(names.length, 0);
|
|
286
|
+
};
|
|
256
287
|
// direct unit tests for the pure path-format helpers
|
|
257
288
|
export const helpers = {
|
|
258
289
|
isInteger: () => {
|
|
@@ -361,6 +392,8 @@ export const proof = {
|
|
|
361
392
|
defaultReporterFailOutput,
|
|
362
393
|
githubReporterOutput,
|
|
363
394
|
registerSuffixes,
|
|
395
|
+
registerThrowsWithoutThrowing,
|
|
396
|
+
registerEmptyProof,
|
|
364
397
|
defaultReporterExpectedToThrow,
|
|
365
398
|
helpers
|
|
366
399
|
};
|
package/fjs/mcp/module.f.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { boolean, string, option, array, record, or } from '../types/rtti/module.f.js';
|
|
17
17
|
import { unknown } from '../media/json/module.f.js';
|
|
18
|
-
import { pure } from '../effects/module.f.js';
|
|
18
|
+
import { pure, step } from '../effects/module.f.js';
|
|
19
19
|
import { read, write } from '../effects/memory/module.f.js';
|
|
20
20
|
import { decodeRequest, rpcError, invalidRequest, invalidParams, methodNotFound, jsonrpc, } from '../media/json/rpc/module.f.js';
|
|
21
21
|
import { validate } from '../types/rtti/validate/module.f.js';
|
|
@@ -211,8 +211,8 @@ export const mcpStep = ({ protocolVersion, capabilities, serverInfo, }) => (hand
|
|
|
211
211
|
// Malformed handshake — ignore it; the session stays gated.
|
|
212
212
|
return pure(null);
|
|
213
213
|
}
|
|
214
|
-
return read(stateKey)
|
|
215
|
-
? write(stateKey, ['initialized', true])
|
|
214
|
+
return step(read(stateKey), ([t]) => t === 'initializing'
|
|
215
|
+
? step(write(stateKey, ['initialized', true]), () => pure(null))
|
|
216
216
|
: pure(null));
|
|
217
217
|
}
|
|
218
218
|
return pure(null);
|
|
@@ -227,7 +227,7 @@ export const mcpStep = ({ protocolVersion, capabilities, serverInfo, }) => (hand
|
|
|
227
227
|
}
|
|
228
228
|
// `initialize` transitions uninitialized → initializing; reject if already done.
|
|
229
229
|
if (method === 'initialize') {
|
|
230
|
-
return read(stateKey)
|
|
230
|
+
return step(read(stateKey), ([t]) => {
|
|
231
231
|
if (t !== 'uninitialized') {
|
|
232
232
|
return pure(_errResponse(id)(invalidRequest));
|
|
233
233
|
}
|
|
@@ -240,11 +240,11 @@ export const mcpStep = ({ protocolVersion, capabilities, serverInfo, }) => (hand
|
|
|
240
240
|
capabilities,
|
|
241
241
|
serverInfo,
|
|
242
242
|
};
|
|
243
|
-
return write(stateKey, ['initializing'])
|
|
243
|
+
return step(write(stateKey, ['initializing']), () => pure(_okResponse(id)(result)));
|
|
244
244
|
});
|
|
245
245
|
}
|
|
246
246
|
// All other methods require fully initialized state — read it first.
|
|
247
|
-
return read(stateKey)
|
|
247
|
+
return step(read(stateKey), ([t]) => {
|
|
248
248
|
if (t !== 'initialized') {
|
|
249
249
|
return pure(_errResponse(id)(notInitialized));
|
|
250
250
|
}
|
|
@@ -256,7 +256,7 @@ export const mcpStep = ({ protocolVersion, capabilities, serverInfo, }) => (hand
|
|
|
256
256
|
const [t, pr] = validate(toolsListParams)(params === undefined ? {} : params);
|
|
257
257
|
return t === 'error'
|
|
258
258
|
? pure(_errResponse(id)(invalidParams))
|
|
259
|
-
: handlers.toolsList(pr)
|
|
259
|
+
: step(handlers.toolsList(pr), r => pure(_okResponse(id)(r)));
|
|
260
260
|
}
|
|
261
261
|
if (method === 'tools/call') {
|
|
262
262
|
if (capabilities.tools === undefined) {
|
|
@@ -265,7 +265,7 @@ export const mcpStep = ({ protocolVersion, capabilities, serverInfo, }) => (hand
|
|
|
265
265
|
const [t, pr] = validate(toolsCallParams)(params);
|
|
266
266
|
return t === 'error'
|
|
267
267
|
? pure(_errResponse(id)(invalidParams))
|
|
268
|
-
: handlers.toolsCall(pr)
|
|
268
|
+
: step(handlers.toolsCall(pr), r => pure(_okResponse(id)(r)));
|
|
269
269
|
}
|
|
270
270
|
return pure(_errResponse(id)(methodNotFound));
|
|
271
271
|
});
|
package/fjs/mcp/proof.f.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { assert, assertEq } from '../asserts/module.f.js';
|
|
2
|
-
import { pure } from '../effects/module.f.js';
|
|
2
|
+
import { pure, step } from '../effects/module.f.js';
|
|
3
|
+
import { eff } from '../effects/eff/module.f.js';
|
|
3
4
|
import { run } from '../effects/mock/module.f.js';
|
|
4
5
|
import { asBase, asNominal, create, read } from '../effects/memory/module.f.js';
|
|
5
6
|
import { uninitializedState, mcpStep, notInitialized, } from './module.f.js';
|
|
@@ -36,11 +37,11 @@ const runMem = (effect) => run(mock)(initial)(effect)[1];
|
|
|
36
37
|
// O flows through McpHandlers<never>, so we cast the widened type down to MemOp.
|
|
37
38
|
const asMemEffect = (e) => e;
|
|
38
39
|
// Run one step from uninitializedState, return [response, newState].
|
|
39
|
-
const step1 = (cfg) => (msg) => runMem(asMemEffect(create(uninitializedState)
|
|
40
|
+
const step1 = (cfg) => (msg) => runMem(asMemEffect(step(create(uninitializedState), key => step(mcpStep(cfg)(handlers)(key)(msg), resp => step(read(key), state => pure([resp, state]))))));
|
|
40
41
|
// Run initialize then a second step, return [response, newState] of the second.
|
|
41
|
-
const step2 = (cfg) => (msg1) => (msg2) => runMem(asMemEffect(create(uninitializedState).step(key => mcpStep(cfg)(handlers)(key)(msg1).step(() => mcpStep(cfg)(handlers)(key)(msg2).step(resp => read(key)
|
|
42
|
+
const step2 = (cfg) => (msg1) => (msg2) => runMem(asMemEffect(eff(create(uninitializedState)).step(key => eff(mcpStep(cfg)(handlers)(key)(msg1)).step(() => eff(mcpStep(cfg)(handlers)(key)(msg2)).step(resp => step(read(key), state => pure([resp, state]))).value).value).value));
|
|
42
43
|
// Run initialize, notifications/initialized, then a third step; return [response, newState] of the third.
|
|
43
|
-
const step3 = (cfg) => (msg1) => (msg2) => (msg3) => runMem(asMemEffect(create(uninitializedState).step(key => mcpStep(cfg)(handlers)(key)(msg1).step(() => mcpStep(cfg)(handlers)(key)(msg2).step(() => mcpStep(cfg)(handlers)(key)(msg3).step(resp => read(key).step(state => pure([resp, state]))))))));
|
|
44
|
+
const step3 = (cfg) => (msg1) => (msg2) => (msg3) => runMem(asMemEffect(eff(create(uninitializedState)).step(key => eff(mcpStep(cfg)(handlers)(key)(msg1)).step(() => eff(mcpStep(cfg)(handlers)(key)(msg2)).step(() => eff(mcpStep(cfg)(handlers)(key)(msg3)).step(resp => eff(read(key)).step(state => pure([resp, state])).value).value).value).value).value));
|
|
44
45
|
// ── Test messages ─────────────────────────────────────────────────────────────
|
|
45
46
|
const initMsg = { jsonrpc: '2.0', method: 'initialize', id: 1,
|
|
46
47
|
params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'client', version: '0.0.1' } } };
|
|
@@ -39,9 +39,9 @@ import { type Response } from '../../media/json/rpc/module.f.ts';
|
|
|
39
39
|
*/
|
|
40
40
|
export type Step<O extends Operation> = (value: Unknown) => Effect<O, Response | null>;
|
|
41
41
|
/**
|
|
42
|
-
* Drives the read-parse-dispatch-write loop for `
|
|
42
|
+
* Drives the read-parse-dispatch-write loop for `handler` over stdin/stdout.
|
|
43
43
|
*
|
|
44
44
|
* Recurses after each handled line; terminates (resolving to `void`) when
|
|
45
45
|
* `readLine` reports EOF.
|
|
46
46
|
*/
|
|
47
|
-
export declare const stdioTransport: <O extends Operation>(
|
|
47
|
+
export declare const stdioTransport: <O extends Operation>(handler: Step<O>) => Effect<Read | Write | O, void>;
|