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.
Files changed (75) hide show
  1. package/fjs/bnf/token_symbol/module.f.d.ts +38 -0
  2. package/fjs/bnf/token_symbol/module.f.js +48 -0
  3. package/fjs/bnf/token_symbol/proof.f.d.ts +10 -0
  4. package/fjs/bnf/token_symbol/proof.f.js +37 -0
  5. package/fjs/cas/cli/module.f.js +6 -9
  6. package/fjs/cas/evo/mcp/module.f.d.ts +9 -1
  7. package/fjs/cas/evo/mcp/module.f.js +52 -14
  8. package/fjs/cas/evo/mcp/proof.f.d.ts +2 -0
  9. package/fjs/cas/evo/mcp/proof.f.js +33 -2
  10. package/fjs/cas/evo/module.f.d.ts +76 -12
  11. package/fjs/cas/evo/module.f.js +129 -17
  12. package/fjs/cas/evo/proof.f.d.ts +7 -0
  13. package/fjs/cas/evo/proof.f.js +119 -0
  14. package/fjs/cas/mcp/module.f.js +22 -20
  15. package/fjs/cas/mcp/proof.f.d.ts +1 -1
  16. package/fjs/cas/mcp/proof.f.js +9 -9
  17. package/fjs/cas/module.f.js +64 -37
  18. package/fjs/cas/proof.f.d.ts +3 -9
  19. package/fjs/cas/proof.f.js +94 -74
  20. package/fjs/ci/bun/module.f.js +3 -3
  21. package/fjs/ci/common/module.f.d.ts +1 -2
  22. package/fjs/ci/common/module.f.js +1 -5
  23. package/fjs/ci/config/module.f.d.ts +5 -5
  24. package/fjs/ci/config/module.f.js +5 -5
  25. package/fjs/ci/deno/module.f.js +3 -3
  26. package/fjs/ci/module.f.js +3 -4
  27. package/fjs/ci/node/module.f.js +11 -9
  28. package/fjs/ci/proof.f.js +2 -0
  29. package/fjs/cli/module.f.js +2 -2
  30. package/fjs/common/monoid/module.f.d.ts +46 -2
  31. package/fjs/common/monoid/module.f.js +44 -0
  32. package/fjs/common/monoid/proof.f.d.ts +5 -0
  33. package/fjs/common/monoid/proof.f.js +27 -1
  34. package/fjs/dev/module.f.js +25 -25
  35. package/fjs/djs/module.f.js +5 -8
  36. package/fjs/djs/parser/module.f.d.ts +3 -0
  37. package/fjs/djs/parser/module.f.js +17 -0
  38. package/fjs/djs/transpiler/module.f.js +6 -6
  39. package/fjs/effects/eff/module.f.d.ts +72 -0
  40. package/fjs/effects/eff/module.f.js +46 -0
  41. package/fjs/effects/eff/proof.f.d.ts +11 -0
  42. package/fjs/effects/eff/proof.f.js +58 -0
  43. package/fjs/effects/list/module.f.d.ts +11 -5
  44. package/fjs/effects/list/module.f.js +2 -1
  45. package/fjs/effects/memory/proof.f.js +7 -8
  46. package/fjs/effects/module.f.d.ts +334 -52
  47. package/fjs/effects/module.f.js +253 -45
  48. package/fjs/effects/node/memory/proof.js +3 -2
  49. package/fjs/effects/node/module.f.d.ts +1 -1
  50. package/fjs/effects/node/module.f.js +9 -11
  51. package/fjs/effects/node/proof.f.js +18 -10
  52. package/fjs/effects/proof.f.d.ts +32 -6
  53. package/fjs/effects/proof.f.js +131 -39
  54. package/fjs/emergent_testing/module.f.js +55 -39
  55. package/fjs/emergent_testing/proof.f.d.ts +4 -0
  56. package/fjs/emergent_testing/proof.f.js +53 -20
  57. package/fjs/mcp/module.f.js +8 -8
  58. package/fjs/mcp/proof.f.js +5 -4
  59. package/fjs/mcp/stdio/module.f.d.ts +2 -2
  60. package/fjs/mcp/stdio/module.f.js +12 -12
  61. package/fjs/media/type/module.f.js +2 -2
  62. package/fjs/media/type/proof.f.js +7 -10
  63. package/fjs/module.f.js +4 -8
  64. package/fjs/types/bigint/module.f.js +4 -3
  65. package/fjs/types/bit_vec/module.f.js +5 -15
  66. package/fjs/types/btree/remove/module.f.d.ts +5 -0
  67. package/fjs/types/btree/remove/module.f.js +12 -0
  68. package/fjs/types/list/module.f.d.ts +21 -0
  69. package/fjs/types/list/module.f.js +17 -0
  70. package/fjs/types/list/proof.f.d.ts +1 -0
  71. package/fjs/types/list/proof.f.js +24 -1
  72. package/fjs/types/number/module.f.js +2 -1
  73. package/fjs/types/string/module.f.js +10 -2
  74. package/fjs/website/module.f.js +2 -3
  75. package/package.json +3 -3
@@ -1,72 +1,93 @@
1
- import { decode, do_, foldStep, forEachStep, lazy, match, okStep, pure } from './module.f.js';
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
- const assertPure = (e, expected) => {
5
- const d = decode(e);
6
- assert(d.done, e.value);
7
- assertEq(d.result, expected);
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((x) => (s) => pure(s + x))(10)([]);
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((x) => (s) => pure(s + x))(0)([1, 2, 3, 4]);
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((x) => (s) => pure(s + x))('')(['a', 'b', 'c']);
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(() => pure(undefined))([1, 2, 3]);
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)).step(okStep((v) => pure(ok(v * 2))));
55
- const d = decode(e);
56
- assert(!(!d.done || d.result[0] !== 'ok' || d.result[1] !== 10), e.value);
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')).step(okStep(v => pure(ok(v * 2))));
60
- const d = decode(e);
61
- assert(!(!d.done || d.result[0] !== 'error' || d.result[1] !== 'oops'), e.value);
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
- decode: () => {
65
- const d = decode(do_('add')(2, 3));
66
- assert(!(d.done), d);
67
- assertEq(d.command, 'add');
68
- assert(!(d.payload[0] !== 2 || d.payload[1] !== 3), d.payload);
69
- assertPure(d.continuation(5), 5);
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))).step(() => pure(undefined));
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))).step(() => pure(undefined));
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]) => test(k, testPath, set)
114
- .step(sr => {
115
- const { result: [s, r], duration } = sr;
116
- return result(k, testPath, sr, set.throws)
117
- .step(() => {
118
- if (s === 'ok') {
119
- if (set.throws) {
120
- return pure(addPass(duration)(zero));
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
- return pure(addFail(duration)(zero));
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).step(runModuleMap(reporter));
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))).step(() => pure(undefined));
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
- const r = registerModuleMap(ctx, star);
272
- return loadModuleMap(o.env)
273
- .step(r)
274
- .step(() => pure(0));
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
- // registerModule appends ' ...' for inline runners (Bun/Playwright).
222
- // Uses a minimal synchronous mock for the Test/All/Await effect operations.
223
- export const registerSuffixes = () => {
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
- const noopCtx = { test: (_n, _o, _f) => Promise.resolve() };
226
- const makeRunner = () => mockRun({
227
- test: (_ctx, name, _xf, _fn) => (s) => [[...s, name], undefined],
228
- all: (...effects) => (s) => {
229
- let st = s;
230
- const rs = [];
231
- for (const e of effects) {
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 = makeRunner();
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(noopCtx, './a.f.ts', proof, ''));
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(noopCtx, './a.f.ts', proof, ' ...'));
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
  };
@@ -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).step(([t]) => t === 'initializing'
215
- ? write(stateKey, ['initialized', true]).step(() => pure(null))
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).step(([t]) => {
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']).step(() => pure(_okResponse(id)(result)));
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).step(([t]) => {
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).step(r => pure(_okResponse(id)(r)));
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).step(r => pure(_okResponse(id)(r)));
268
+ : step(handlers.toolsCall(pr), r => pure(_okResponse(id)(r)));
269
269
  }
270
270
  return pure(_errResponse(id)(methodNotFound));
271
271
  });
@@ -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).step(key => mcpStep(cfg)(handlers)(key)(msg).step(resp => read(key).step(state => pure([resp, state]))))));
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).step(state => pure([resp, state])))))));
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 `step` over stdin/stdout.
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>(step: Step<O>) => Effect<Read | Write | O, void>;
47
+ export declare const stdioTransport: <O extends Operation>(handler: Step<O>) => Effect<Read | Write | O, void>;