functionalscript 0.38.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +1 -1
  2. package/fjs/bnf/descent/module.f.d.ts +43 -2
  3. package/fjs/bnf/descent/module.f.js +37 -12
  4. package/fjs/bnf/descent/proof.f.d.ts +1 -0
  5. package/fjs/bnf/descent/proof.f.js +73 -32
  6. package/fjs/bnf/ll1/module.f.js +2 -2
  7. package/fjs/bnf/token_symbol/module.f.d.ts +38 -0
  8. package/fjs/bnf/token_symbol/module.f.js +48 -0
  9. package/fjs/bnf/token_symbol/proof.f.d.ts +10 -0
  10. package/fjs/bnf/token_symbol/proof.f.js +37 -0
  11. package/fjs/cas/cli/module.f.js +6 -9
  12. package/fjs/cas/evo/module.f.d.ts +77 -13
  13. package/fjs/cas/evo/module.f.js +130 -18
  14. package/fjs/cas/evo/proof.f.d.ts +7 -0
  15. package/fjs/cas/evo/proof.f.js +120 -1
  16. package/fjs/cas/module.f.js +64 -37
  17. package/fjs/cas/proof.f.d.ts +3 -9
  18. package/fjs/cas/proof.f.js +94 -74
  19. package/fjs/ci/bun/module.f.js +3 -3
  20. package/fjs/ci/common/module.f.d.ts +1 -2
  21. package/fjs/ci/common/module.f.js +1 -5
  22. package/fjs/ci/config/module.f.d.ts +11 -8
  23. package/fjs/ci/config/module.f.js +24 -9
  24. package/fjs/ci/deno/module.f.js +3 -3
  25. package/fjs/ci/module.f.js +13 -7
  26. package/fjs/ci/nix/module.f.d.ts +61 -0
  27. package/fjs/ci/nix/module.f.js +92 -0
  28. package/fjs/ci/nix/proof.f.d.ts +23 -0
  29. package/fjs/ci/nix/proof.f.js +109 -0
  30. package/fjs/ci/node/module.f.d.ts +23 -1
  31. package/fjs/ci/node/module.f.js +58 -12
  32. package/fjs/ci/proof.f.d.ts +2 -0
  33. package/fjs/ci/proof.f.js +49 -9
  34. package/fjs/cli/module.f.js +2 -2
  35. package/fjs/common/monoid/module.f.d.ts +46 -2
  36. package/fjs/common/monoid/module.f.js +44 -0
  37. package/fjs/common/monoid/proof.f.d.ts +5 -0
  38. package/fjs/common/monoid/proof.f.js +27 -1
  39. package/fjs/dev/module.f.d.ts +1 -0
  40. package/fjs/dev/module.f.js +38 -27
  41. package/fjs/dev/update/module.f.d.ts +11 -0
  42. package/fjs/dev/update/module.f.js +20 -0
  43. package/fjs/dev/update/proof.f.d.ts +7 -0
  44. package/fjs/dev/update/proof.f.js +35 -0
  45. package/fjs/djs/ast/module.f.d.ts +47 -0
  46. package/fjs/djs/ast/module.f.js +9 -0
  47. package/fjs/djs/module.f.js +5 -8
  48. package/fjs/djs/parser/module.f.d.ts +3 -0
  49. package/fjs/djs/parser/module.f.js +17 -0
  50. package/fjs/djs/tokenizer/module.f.js +2 -2
  51. package/fjs/djs/tokenizer/proof.f.d.ts +1 -0
  52. package/fjs/djs/tokenizer/proof.f.js +70 -16
  53. package/fjs/djs/transpiler/module.f.js +6 -6
  54. package/fjs/effects/eff/module.f.d.ts +72 -0
  55. package/fjs/effects/eff/module.f.js +46 -0
  56. package/fjs/effects/eff/proof.f.d.ts +11 -0
  57. package/fjs/effects/eff/proof.f.js +58 -0
  58. package/fjs/effects/list/module.f.d.ts +11 -5
  59. package/fjs/effects/list/module.f.js +2 -1
  60. package/fjs/effects/memory/proof.f.js +7 -8
  61. package/fjs/effects/module.f.d.ts +334 -52
  62. package/fjs/effects/module.f.js +253 -45
  63. package/fjs/effects/node/memory/proof.js +3 -2
  64. package/fjs/effects/node/module.d.ts +3 -3
  65. package/fjs/effects/node/module.f.d.ts +18 -8
  66. package/fjs/effects/node/module.f.js +31 -11
  67. package/fjs/effects/node/module.js +12 -12
  68. package/fjs/effects/node/proof.f.d.ts +1 -0
  69. package/fjs/effects/node/proof.f.js +32 -11
  70. package/fjs/effects/node/virtual/module.f.js +1 -1
  71. package/fjs/effects/proof.f.d.ts +32 -6
  72. package/fjs/effects/proof.f.js +131 -39
  73. package/fjs/emergent_testing/all.test.js +2 -1
  74. package/fjs/emergent_testing/module.f.d.ts +3 -3
  75. package/fjs/emergent_testing/module.f.js +63 -49
  76. package/fjs/emergent_testing/proof.f.d.ts +4 -0
  77. package/fjs/emergent_testing/proof.f.js +54 -21
  78. package/fjs/emergent_testing/scenarios/thenable.pass.js +1 -1
  79. package/fjs/fsc/module.f.js +4 -4
  80. package/fjs/fsm/module.f.js +1 -1
  81. package/fjs/js/tokenizer/module.f.d.ts +1 -0
  82. package/fjs/js/tokenizer/module.f.js +13 -6
  83. package/fjs/{cas/mcp → mcp/cas}/module.f.d.ts +3 -25
  84. package/fjs/{cas/mcp → mcp/cas}/module.f.js +26 -64
  85. package/fjs/mcp/evo/module.f.d.ts +27 -0
  86. package/fjs/mcp/evo/module.f.js +94 -0
  87. package/fjs/{cas/evo/mcp → mcp/evo}/proof.f.d.ts +2 -0
  88. package/fjs/{cas/evo/mcp → mcp/evo}/proof.f.js +40 -9
  89. package/fjs/mcp/module.f.d.ts +54 -237
  90. package/fjs/mcp/module.f.js +55 -258
  91. package/fjs/mcp/proof.f.d.ts +43 -32
  92. package/fjs/mcp/proof.f.js +509 -200
  93. package/fjs/media/nix/module.f.d.ts +30 -0
  94. package/fjs/media/nix/module.f.js +166 -0
  95. package/fjs/media/nix/proof.f.d.ts +32 -0
  96. package/fjs/media/nix/proof.f.js +127 -0
  97. package/fjs/media/type/module.f.js +2 -2
  98. package/fjs/media/type/proof.f.js +7 -10
  99. package/fjs/module.f.js +5 -9
  100. package/fjs/protocol/json_rpc/module.f.d.ts +114 -0
  101. package/fjs/{media/json/rpc → protocol/json_rpc}/module.f.js +3 -3
  102. package/fjs/{media/json/rpc → protocol/json_rpc}/proof.f.js +3 -3
  103. package/fjs/protocol/mcp/module.f.d.ts +239 -0
  104. package/fjs/protocol/mcp/module.f.js +272 -0
  105. package/fjs/protocol/mcp/proof.f.d.ts +34 -0
  106. package/fjs/protocol/mcp/proof.f.js +208 -0
  107. package/fjs/{mcp → protocol/mcp}/stdio/module.f.d.ts +7 -7
  108. package/fjs/{mcp → protocol/mcp}/stdio/module.f.js +22 -22
  109. package/fjs/{mcp → protocol/mcp}/stdio/proof.f.js +9 -9
  110. package/fjs/types/bigint/module.f.js +4 -3
  111. package/fjs/types/bit_vec/module.f.js +5 -15
  112. package/fjs/types/btree/remove/module.f.d.ts +5 -0
  113. package/fjs/types/btree/remove/module.f.js +12 -0
  114. package/fjs/types/list/module.f.d.ts +21 -0
  115. package/fjs/types/list/module.f.js +17 -0
  116. package/fjs/types/list/proof.f.d.ts +1 -0
  117. package/fjs/types/list/proof.f.js +24 -1
  118. package/fjs/types/number/module.f.js +2 -1
  119. package/fjs/types/range_map/module.f.d.ts +14 -13
  120. package/fjs/types/range_map/module.f.js +18 -13
  121. package/fjs/types/range_map/proof.f.js +26 -39
  122. package/fjs/types/range_set/module.f.d.ts +5 -0
  123. package/fjs/types/range_set/module.f.js +16 -0
  124. package/fjs/types/range_set/proof.f.d.ts +1 -0
  125. package/fjs/types/range_set/proof.f.js +19 -0
  126. package/fjs/types/string/module.f.js +10 -2
  127. package/fjs/website/module.f.js +2 -3
  128. package/package.json +5 -5
  129. package/fjs/cas/evo/mcp/module.f.d.ts +0 -19
  130. package/fjs/cas/evo/mcp/module.f.js +0 -56
  131. package/fjs/cas/mcp/proof.f.d.ts +0 -45
  132. package/fjs/cas/mcp/proof.f.js +0 -545
  133. package/fjs/ci/playwright/module.f.d.ts +0 -2
  134. package/fjs/ci/playwright/module.f.js +0 -25
  135. package/fjs/media/json/rpc/module.f.d.ts +0 -114
  136. /package/fjs/{media/json/rpc → protocol/json_rpc}/proof.f.d.ts +0 -0
  137. /package/fjs/{mcp → protocol/mcp}/stdio/proof.f.d.ts +0 -0
@@ -1,25 +1,46 @@
1
1
  import { empty, isVec, uint, vec, vec8 } from "../../types/bit_vec/module.f.js";
2
2
  import { utf8, utf8ToString } from "../../text/module.f.js";
3
- import { decode, pure } from "../module.f.js";
4
- import { both, fetch, mkdir, now, readdir, readFile, readUtf8File, rm, sandbox, writeFile, writeUtf8File, rename, readBytes, randomInt, writeFromStream } from "./module.f.js";
3
+ import { match, pure, step } from "../module.f.js";
4
+ import { both, fetch, mkdir, now, readdir, readFile, readUtf8File, rm, sandbox, writeFile, writeUtf8File, rename, readBytes, randomInt, writeFromStream, usesInlineTestContext, versionLessThan } from "./module.f.js";
5
5
  import { create as memCreate, read as memRead, write as memWrite } from "../memory/module.f.js";
6
6
  import { empty as listEmpty, nonEmpty as listNonEmpty } from "../list/module.f.js";
7
7
  import { emptyState, virtual } from "./virtual/module.f.js";
8
8
  import { assert, assertEq, assertNotNullish } from '../../asserts/module.f.js';
9
+ import { ok } from '../../types/result/module.f.js';
10
+ // Answers the one command the `map` proof below drives. Routing the loop
11
+ // through `match` keeps the `Pure`/`Do` layout out of this module: the map key
12
+ // is the command assertion, and `MatchResult` types the continuation.
13
+ const readHello = match({
14
+ readFile: path => {
15
+ assertEq(path, 'hello');
16
+ return ok(vec8(0x15n));
17
+ },
18
+ });
9
19
  export const proof = {
20
+ externalTestContext: () => {
21
+ assert(usesInlineTestContext('node', 'v22.20.0'));
22
+ assert(usesInlineTestContext('node', '25.99.99'));
23
+ assert(!usesInlineTestContext('node', '26.0.0'));
24
+ assert(!usesInlineTestContext('node', '26.1.0'));
25
+ assert(!usesInlineTestContext('node'));
26
+ assert(usesInlineTestContext('bun'));
27
+ assert(!usesInlineTestContext('deno', '22.0.0'));
28
+ assert(versionLessThan('25.99.99', '26.0.0'));
29
+ assert(versionLessThan('26.0.0', '26.1.0'));
30
+ assert(versionLessThan('26.1.0', '26.1.1'));
31
+ assert(!versionLessThan('26.1.1', '26.1.1'));
32
+ },
10
33
  map: () => {
11
- const e = readFile('hello').step(([k, v]) => {
34
+ const e = step(readFile('hello'), ([k, v]) => {
12
35
  assert(k !== 'error', v);
13
36
  return pure(uint(v) * 2n);
14
37
  });
15
38
  //
16
- let d = decode(e);
17
- while (!d.done) {
18
- assertEq(d.command, 'readFile');
19
- assert(d.payload[0] === 'hello', d.payload);
20
- d = decode(d.continuation(['ok', vec8(0x15n)]));
39
+ let r = readHello(e);
40
+ while (r[0] === 'cont') {
41
+ r = readHello(r[2](r[1]));
21
42
  }
22
- assertEq(d.result, 0x2an);
43
+ assertEq(r[1], 0x2an);
23
44
  },
24
45
  fetch: () => {
25
46
  const [_, [t, result]] = virtual({
@@ -282,12 +303,12 @@ export const proof = {
282
303
  },
283
304
  memory: {
284
305
  createAndRead: () => {
285
- const effect = memCreate(42).step(key => memRead(key));
306
+ const effect = step(memCreate(42), key => memRead(key));
286
307
  const [_, value] = virtual(emptyState)(effect);
287
308
  assertEq(value, 42);
288
309
  },
289
310
  createAndWrite: () => {
290
- const effect = memCreate(1).step(key => memWrite(key, 99).step(() => memRead(key)));
311
+ const effect = step(memCreate(1), key => step(memWrite(key, 99), () => memRead(key)));
291
312
  const [_, value] = virtual(emptyState)(effect);
292
313
  assertEq(value, 99);
293
314
  },
@@ -435,6 +435,6 @@ export const defaultNodeProgramOptions = {
435
435
  std: { stdout: { isTTY: false }, stderr: { isTTY: false } },
436
436
  testContext,
437
437
  bunTestContext: testContext,
438
- playwrightTestContext: testContext,
439
438
  engine: 'node',
439
+ inlineTestContext: false,
440
440
  };
@@ -1,9 +1,14 @@
1
+ import { type Effect, type Operation } from './module.f.ts';
2
+ /**
3
+ * Asserts that `e` yields `expected` without performing a command. Exported so
4
+ * `eff/proof.f.ts` shares this definition instead of repeating it.
5
+ *
6
+ * Not `assertEq(runPure(e), [expected])`: `assertEq` compares with `===`, so a
7
+ * freshly allocated `[expected]` is never equal to the returned option. Assert
8
+ * the option's shape first, then compare the value inside it.
9
+ */
10
+ export declare const assertPure: <O extends Operation, T>(e: Effect<O, T>, expected: T) => void;
1
11
  export declare const proof: {
2
- lazy: {
3
- value: () => void;
4
- deferred: () => void;
5
- step: () => void;
6
- };
7
12
  foldStep: {
8
13
  empty: () => void;
9
14
  threadsState: () => void;
@@ -17,9 +22,30 @@ export declare const proof: {
17
22
  ok: () => void;
18
23
  error: () => void;
19
24
  };
20
- decode: () => void;
25
+ runPure: {
26
+ pure: () => void;
27
+ pureNull: () => void;
28
+ do_: () => void;
29
+ };
30
+ doNode: () => void;
21
31
  match: {
22
32
  done: () => void;
23
33
  cont: () => void;
24
34
  };
35
+ step: {
36
+ pure: () => void;
37
+ chain: () => void;
38
+ overDo: () => void;
39
+ };
40
+ mapStep: {
41
+ pure: () => void;
42
+ constant: () => void;
43
+ overDo: () => void;
44
+ };
45
+ historyStep: {
46
+ pure: () => void;
47
+ overDo: () => void;
48
+ chain: () => void;
49
+ fReceivesWholeHistory: () => void;
50
+ };
25
51
  };
@@ -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
  };
@@ -1,4 +1,5 @@
1
1
  import { runEffect } from '../effects/node/module.js';
2
2
  import { register } from './module.f.js';
3
- // we need `await` for Playwright.
3
+ // Top-level `await`: every proof must be registered before the runner starts
4
+ // collecting tests.
4
5
  await runEffect(register);
@@ -62,8 +62,8 @@ export type Reporter<O extends Operation> = {
62
62
  * Unlike `runModule`, which sandboxes only the leaf function, `registerModule`
63
63
  * lets the external framework own scheduling: each registered test callback
64
64
  * calls `fn`, then recursively registers any sub-trees returned by the function.
65
- * This is the correct model for Node `--test`, Bun, and Playwright, where tests
66
- * must be declared upfront and the framework drives execution.
65
+ * This is the correct model for Node `--test`, Bun, and Deno, where tests must
66
+ * be declared upfront and the framework drives execution.
67
67
  */
68
68
  export declare const registerModule: (ctx: TestContext, k: string, v: unknown, star: string) => Effect<Test | All | Await, void>;
69
69
  /**
@@ -130,7 +130,7 @@ export declare const defaultReporter: (options: NodeProgramOptions) => Reporter<
130
130
  /** The `fjs t` entry point: runs all tests using `defaultReporter`. */
131
131
  export declare const main: NodeProgram;
132
132
  /**
133
- * Entry point for external test frameworks (Node `--test`, Bun, Playwright).
133
+ * Entry point for external test frameworks (Node `--test`, Bun, Deno).
134
134
  *
135
135
  * Discovers test modules via `loadModuleMap`, then registers each with the
136
136
  * framework-appropriate `TestContext` selected from `NodeProgramOptions`
@@ -5,14 +5,15 @@
5
5
  * - `runModule` / `Reporter<O>` — self-hosted Effects runner used by `fjs t`;
6
6
  * sandboxes each leaf call individually and accumulates `TestState`.
7
7
  * - `registerModule` / `TestContext` — registers tests with an external
8
- * framework (Node `--test`, Bun, Playwright) at import time; the framework
9
- * owns scheduling and pass/fail counting.
8
+ * framework (Node `--test`, Bun, Deno) at import time; the framework owns
9
+ * scheduling and pass/fail counting.
10
10
  *
11
11
  * @module
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';
@@ -78,18 +79,18 @@ export const collectTests = (path, throws, v) => {
78
79
  * Unlike `runModule`, which sandboxes only the leaf function, `registerModule`
79
80
  * lets the external framework own scheduling: each registered test callback
80
81
  * calls `fn`, then recursively registers any sub-trees returned by the function.
81
- * This is the correct model for Node `--test`, Bun, and Playwright, where tests
82
- * must be declared upfront and the framework drives execution.
82
+ * This is the correct model for Node `--test`, Bun, and Deno, where tests must
83
+ * be declared upfront and the framework drives execution.
83
84
  */
84
85
  export const registerModule = (ctx, k, v, star) => {
85
86
  const registerOne = (ctx, [path, { fn, throws }]) => {
86
- // ' *' (non-empty only for Bun/Playwright) signals that all sub-tests run
87
+ // ' *' (non-empty only for Bun) signals that all sub-tests run
87
88
  // inline inside this single registration. Not appended to throw-tests since
88
89
  // those never produce sub-tests. The path already contains '.throw' when a
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
  };
@@ -257,19 +273,17 @@ export const defaultReporter = (options) => {
257
273
  /** The `fjs t` entry point: runs all tests using `defaultReporter`. */
258
274
  export const main = options => testAll(defaultReporter(options))(options);
259
275
  /**
260
- * Entry point for external test frameworks (Node `--test`, Bun, Playwright).
276
+ * Entry point for external test frameworks (Node `--test`, Bun, Deno).
261
277
  *
262
278
  * Discovers test modules via `loadModuleMap`, then registers each with the
263
279
  * framework-appropriate `TestContext` selected from `NodeProgramOptions`
264
280
  * based on the detected `engine`.
265
281
  */
266
282
  export const register = o => {
267
- const star = o.engine === 'bun' || o.engine === 'playwright' ? ' ...' : '';
268
- const ctx = o.engine === 'bun' ? o.bunTestContext :
269
- o.engine === 'playwright' ? o.playwrightTestContext :
270
- o.testContext;
271
- const r = registerModuleMap(ctx, star);
272
- return loadModuleMap(o.env)
273
- .step(r)
274
- .step(() => pure(0));
283
+ const star = o.inlineTestContext ? ' ...' : '';
284
+ const ctx = o.engine === 'bun' ? o.bunTestContext : o.testContext;
285
+ return eff(loadModuleMap(o.env))
286
+ .step(registerModuleMap(ctx, star))
287
+ .step(() => pure(0))
288
+ .value;
275
289
  };
@@ -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;