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,51 +1,248 @@
1
1
  /**
2
2
  * Core effect type constructors and combinators.
3
3
  *
4
- * Effect helpers are **step adapters**: functions that return a continuation
5
- * `(t: T) => Effect<Q, R>` meant to be passed into `.step`, never wrappers
6
- * that take the effect itself as an argument. `Effect` has no pipeline
7
- * operator to lean on and must not be extended with new methods, so
8
- * `.step(adapterA).step(adapterB)` is how helpers compose — flat,
9
- * left-to-right, in evaluation order. See {@link okStep} for an example.
4
+ * An `Effect<O, T>` **is** the raw value a `Pure` thunk (`() => T`) or a `Do`
5
+ * node (`{ command, payload, continuation }`). It is plain data with no methods.
6
+ * Composition is provided externally by {@link step}. The optional
7
+ * method-chaining wrapper lives in `fjs/effects/eff/module.f.ts`.
8
+ *
9
+ * **Three functions discriminate `Pure` from `Do`** — {@link step},
10
+ * {@link match}, and {@link runPure} — plus the node proof in
11
+ * `fjs/effects/proof.f.ts` that pins the representation on purpose. Everything
12
+ * else, interpreters included, goes through `match` or `runPure`. The count is
13
+ * the point: a `typeof e === 'function'` check appearing in a fifth place is a
14
+ * review flag, because the representation is only cheap to change while its
15
+ * readers stay enumerable.
16
+ *
17
+ * A `decode` function (`(e: Effect<O, T>) => Decoded<O, T>`) once funnelled all
18
+ * of that through a single `{ done, result }` / `{ done, command, payload,
19
+ * continuation }` record, so that exactly one function held the shape test. It
20
+ * has been removed. `Effect` is a function type unioned with an object type, so
21
+ * `typeof e === 'function'` is already a complete discriminant: `decode` bought
22
+ * no narrowing, it re-encoded that narrowing as a `done` flag to be re-narrowed
23
+ * one indirection later, and its `Decoded` record was declared in terms of the
24
+ * node it claimed to hide. The price was a second vocabulary every consumer had
25
+ * to learn for a shape it could already read. Reintroducing it would buy back
26
+ * the same nothing — and with {@link Do} now carrying named fields there is not
27
+ * even a positional layout left for it to insulate anyone from.
28
+ *
29
+ * Effect helpers come in two shapes. **Step adapters** return a continuation
30
+ * `(t: T) => Effect<Q, R>` meant to be passed into a step — see {@link okStep}.
31
+ * **Step variants** take the effect itself first, like {@link step} — see
32
+ * {@link historyStep}. {@link mapStep} is the variant for the end of a chain:
33
+ * a pure projection over an effect's result, which is a `step` that continues
34
+ * with no further effect.
35
+ *
36
+ * **Do not nest steps.** Bind each intermediate effect to its own name, so a
37
+ * sequence reads top-to-bottom in evaluation order:
38
+ *
39
+ * ```ts
40
+ * // avoid — reads inside-out, and gains a level of indentation per link
41
+ * step(a, x => step(f(x), y => step(g(y), z => h(z))))
42
+ *
43
+ * // prefer — flat, one name per link
44
+ * const x0 = step(a, f)
45
+ * const x1 = step(x0, g)
46
+ * return step(x1, h)
47
+ * ```
48
+ *
49
+ * **A step call that does not fit one line breaks after `(`, one argument per
50
+ * line.** This holds for every step variant — {@link step}, {@link mapStep},
51
+ * {@link historyStep}, {@link foldStep}, {@link forEachStep} — and it is the
52
+ * same rule as taking the effect first, written out at the call site. A step
53
+ * variant is this module's
54
+ * `do` notation: the arguments are a statement list in execution order, so each
55
+ * one gets a line and the sequence reads down the page. Packing the leading
56
+ * effect onto the `(` line and wrapping the rest beneath it hides which of them
57
+ * runs first. The closing `)` may sit on its own line or trail the last
58
+ * argument:
59
+ *
60
+ * ```ts
61
+ * return step(
62
+ * collectRead(cas.read(hash)),
63
+ * ([tag, value]) => pure(tag === 'error' ? null : decodeRevisionVec(value)))
64
+ * ```
65
+ *
66
+ * When a later link needs a value from an earlier one, that is not a reason to
67
+ * nest: a nested continuation only reaches back because it closes over the
68
+ * enclosing scope. {@link historyStep} carries the value forward instead, so
69
+ * the chain stays flat:
70
+ *
71
+ * ```ts
72
+ * // avoid — nested only so `h` can still see `x`
73
+ * step(a, x => step(f(x), y => h(x, y)))
74
+ *
75
+ * // prefer — the history tuple carries `x` forward alongside `y`
76
+ * const x0 = historyStep(history(a), f)
77
+ * return step(x0, ([y, x]) => h(x, y))
78
+ * ```
79
+ *
80
+ * Nesting is often forced by nothing more than a local declared inside a
81
+ * continuation that does not depend on it. Hoist such locals above the chain
82
+ * and the nesting usually dissolves on its own.
83
+ *
84
+ * That advice is for code *using* this module, and the combinators defined
85
+ * here are what make it followable. The nesting has to exist somewhere: a name
86
+ * cannot be bound to an effect that has not been produced yet, so `f(param)`
87
+ * cannot become a `const` until `e` resolves. {@link step} recurses into
88
+ * itself inside the continuation it rebuilds, {@link foldStep} composes one
89
+ * step per item, and {@link historyStep} runs `f` inside `e`'s continuation.
90
+ * Each writes that nesting down **once**, in one line, so that no caller ever
91
+ * writes it again — that is what a combinator here is *for*. Without
92
+ * {@link historyStep} the flat form would be unavailable the moment a later
93
+ * link needed an earlier link's value.
10
94
  *
11
95
  * @module
12
96
  */
13
97
  import { fold } from '../types/list/module.f.js';
14
- export const pure = (v) => ({
15
- value: () => v,
16
- step: f => f(v)
17
- });
98
+ export const pure = (v) => () => v;
18
99
  /**
19
- * A lazy pure effect. Like {@link pure}, but takes a thunk and evaluates it on
20
- * demand instead of holding an already-computed value. Use this to produce the
21
- * next item of a list without instantiating the rest — the thunk runs only when
22
- * the effect is decoded (or stepped into).
100
+ * Composes effects: run `e`, then continue with `f` applied to its result.
101
+ * The data-first primitive raw `Effect` in, raw `Effect` out. Chains as
102
+ * `step(step(e, f), g)`.
103
+ *
104
+ * **`step` is not lazy.** It reads `e`'s shape immediately, so a `Pure` head is
105
+ * forced and `f` is called right there: `step(pure(v), f)` *is* `f(v)`,
106
+ * evaluated where the composition is written rather than where the effect is
107
+ * run. Only the `Do` case defers — the continuation rebuilt around `f` runs
108
+ * when a runner reaches that node.
109
+ *
110
+ * That is sound rather than an oversight, and it is sound only because of
111
+ * {@link Pure}'s contract: a `Pure` holds a value that has already been
112
+ * computed, so forcing it early observes nothing, repeats nothing, and can
113
+ * throw nothing. `step` never performs a `Do` node, which is where anything
114
+ * real lives. Break the contract — hide work behind the thunk — and merely
115
+ * composing a chain starts running the program.
116
+ *
117
+ * A composition cannot be suspended, and no combinator can fix that:
118
+ * `defer: (() => Effect<O, T>) => Effect<O, T>` cannot be written here, because
119
+ * the `Pure` / `Do` tag must be known before anything runs and the union has no
120
+ * third case meaning "not yet decided". That is inherent to the representation,
121
+ * not a gap in this module's API. A caller that needs to name a composition
122
+ * without performing it yet has to keep the ingredients and defer the `step`
123
+ * itself — `Eff` does exactly this, holding its history tuple as a thunk (`h`)
124
+ * precisely because composing it eagerly is the one thing it cannot take back.
23
125
  */
24
- export const lazy = (t) => ({
25
- value: t,
26
- step: f => f(t())
27
- });
28
- export const doFull = (cmd, param, cont) => ({
29
- value: [cmd, param, cont],
30
- step: (f) => doFull(cmd, param, x => cont(x).step(f)),
31
- });
32
- export const do_ = (cmd) => (...param) => doFull(cmd, param, pure);
126
+ export const step = (e, f) => typeof e === 'function'
127
+ ? f(e())
128
+ : { ...e, continuation: x => step(e.continuation(x), f) };
33
129
  /**
34
- * Sequentially threads a state value through an effect for each item in `items`.
130
+ * Applies a pure function to an effect's result: the functor `map` of the
131
+ * effect monad, and a {@link step} whose continuation performs nothing further.
132
+ *
133
+ * Prefer it over the `step(e, t => pure(f(t)))` it abbreviates. The two are the
134
+ * same value, but they read as different things: a `step` announces another
135
+ * link in a sequence of effects, and a trailing pure projection is not one —
136
+ * it is where the sequence ends. Saying so in the combinator's name keeps the
137
+ * "one name per link" shape of a chain honest about how many effects it runs.
35
138
  *
36
- * Given `f: item => state => Effect<O, state>`, `init: S`, and `items: [x₀, x₁, …]`,
37
- * builds `f(x₀)(init).step(f(x₁)).step(f(x₂)).…` and yields a single
38
- * `Effect<O, S>` that produces the final state.
139
+ * **The operation set does not widen.** The result is `Effect<O, R>`, not
140
+ * `Effect<O | Q, R>`, because a pure projection issues no commands — nothing a
141
+ * runner has to know how to interpret is added by `f`. That is what separates
142
+ * this from `step`, beyond the shorter spelling.
143
+ *
144
+ * A constant variant (`constStep(e, v)`) is deliberately absent: `mapStep(e,
145
+ * () => v)` already reads clearly, and it keeps `v`'s evaluation inside the
146
+ * continuation where `step` puts it, rather than moving it to where the
147
+ * composition is written.
148
+ */
149
+ export const mapStep = (e, f) => step(e, t => pure(f(t)));
150
+ /**
151
+ * Like {@link step}, but keeps the values instead of discarding them: runs `e`
152
+ * to get the history `p`, continues with `f(...p)` to get `r`, and yields
153
+ * `[r, ...p]` — the same history with `r` prepended.
154
+ *
155
+ * This is what a chain of named intermediate effects cannot otherwise express.
156
+ * Each `step`'s continuation sees only the result of the effect it consumes, so
157
+ * a later link has no way to reach an earlier one. `historyStep` carries every
158
+ * earlier value forward, and the next destructuring names the parts:
159
+ *
160
+ * ```ts
161
+ * const b = historyStep(history(a), decodeRevisionBlob(cas))
162
+ * const c = step(b, ([revision, hash]) => ...)
163
+ * ```
164
+ *
165
+ * Chaining mimics an async function, one `await` per link — `const hash = ...`
166
+ * then `const revision = ...`, with both still reachable at the end. It takes a
167
+ * history and returns one, so it composes with itself to any depth; only the
168
+ * entry point needs {@link history}:
169
+ *
170
+ * ```ts
171
+ * const h0 = history(readHash(cas))
172
+ * const h1 = historyStep(h0, hash => decodeRevisionBlob(cas)(hash))
173
+ * const h2 = historyStep(h1, (revision, hash) => ...)
174
+ * ```
175
+ *
176
+ * **Newest first.** A position is distance back from the current link, not
177
+ * evaluation order, so a destructuring reads reverse-chronologically:
178
+ * `([z, y, x]) => ...` binds `x` earliest. Reaching further back costs an index
179
+ * rather than a traversal, but a long chain makes the positions hard to count.
180
+ * When that starts to hurt, collapse it into a record of named fields
181
+ * (`pure({ hash, revision } as const)`) and start a fresh history from there.
182
+ */
183
+ export const historyStep = (e,
184
+ // `Readonly<P>` is load-bearing: inferring `P` from a bare rest parameter
185
+ // yields a *mutable*, labelled tuple (`[next: string]`), which then rejects
186
+ // the `readonly` tuples every history is built from.
187
+ f) => step(e, param => step(f(...param), result => pure([result, ...param])));
188
+ /**
189
+ * Starts a history, lifting a plain result into a one-element tuple so that
190
+ * {@link historyStep} can extend it.
191
+ *
192
+ * Creating a history is the *only* thing this does — every later link goes
193
+ * through `historyStep`, which is what lets one combinator cover chains of any
194
+ * length. Fusing the two (a step that both starts and extends) is what makes
195
+ * chains stop composing: such a step nests its predecessor's tuple instead of
196
+ * flattening it, so link two would have to be spelled differently from link
197
+ * three.
198
+ */
199
+ export const history = (e) => step(e, v => pure([v]));
200
+ export const do_ = (command) => (...payload) => ({ command, payload, continuation: pure });
201
+ /**
202
+ * Sequentially threads a state value through an effect for each item produced by
203
+ * `items`.
204
+ *
205
+ * Given `f: item => state => Effect<Q, state>`, `init: S`, and an `items` that
206
+ * yields `[x₀, x₁, …]`, builds `step(step(f(x₀)(init), f(x₁)), f(x₂))…` and
207
+ * yields a single effect producing the final state.
39
208
  *
40
209
  * Sequential — each step depends on the previous state. Compare to `all`,
41
210
  * which fans out independent effects.
211
+ *
212
+ * **A step variant** (see the two shapes described in this module's header): the
213
+ * effect comes first, as in {@link step} and {@link historyStep}. `items` is an
214
+ * `Effect<O, List<T>>` rather than a bare `List<T>` because the list a caller
215
+ * folds over is normally *produced* by an effect — `cas.list()`, a `readdir`.
216
+ * Taking the plain list would force every such caller to open a continuation
217
+ * just to name the list (`step(cas.list(), foldStep(…))`), which is the nesting
218
+ * this module exists to keep out of call sites. A caller that already holds the
219
+ * list lifts it with `pure`, which costs a wrapper but no indentation.
220
+ *
221
+ * `O` and `Q` are separate on purpose: the operations needed to produce the list
222
+ * are rarely the ones the body performs, and the result unions them.
223
+ *
224
+ * **The argument order is deliberately not `fold`'s** from `fjs/types/list`, and
225
+ * the difference is what the two combinators are *for*. `fold` is a data
226
+ * pipeline: it is curried `f`-first because the list is the thing being threaded
227
+ * through, and nothing about it happens in time. A step variant is a sequencing
228
+ * construct — it is this module's `do` notation, and reading one top-to-bottom
229
+ * is reading the order the program executes in. The effect therefore has to come
230
+ * first, because it is what happens first. Currying `f` ahead of `items` would
231
+ * put the *body* of the loop above the thing it loops over, which is exactly the
232
+ * inversion `do` exists to remove.
233
+ *
234
+ * That is also why the whole family — `step`, `historyStep`, `foldStep`,
235
+ * `forEachStep` — takes its effect first and breaks one argument per line when
236
+ * it wraps (see this module's header): every such call is a statement list, and
237
+ * each line is one statement in execution order.
42
238
  */
43
- export const foldStep = (f) => (init) => (items) => fold(item => acc => acc.step(f(item)))(pure(init))(items);
239
+ export const foldStep = (items, init, f) => step(items, fold(item => acc => step(acc, f(item)))(pure(init)));
44
240
  /**
45
- * Sequentially runs `f(item)` for each item in `items`, discarding intermediate
46
- * results. The `void` accumulator sibling of `foldStep`.
241
+ * Sequentially runs `f(item)` for each item produced by `items`, discarding
242
+ * intermediate results. The `void` accumulator sibling of {@link foldStep}, and
243
+ * a step variant on the same grounds.
47
244
  */
48
- export const forEachStep = (f) => (items) => foldStep((item) => () => f(item))(undefined)(items);
245
+ export const forEachStep = (items, f) => foldStep(items, undefined, (item) => () => f(item));
49
246
  /**
50
247
  * A step adapter for the `error` short-circuit: `error` → pass it through
51
248
  * unchanged as `pure`, `ok` → continue with `f`. Collapses the hand-written
@@ -54,17 +251,27 @@ export const forEachStep = (f) => (items) => foldStep((item) => () => f(item))(u
54
251
  */
55
252
  export const okStep = (f) => (r) => r[0] === 'error' ? pure(r) : f(r[1]);
56
253
  /**
57
- * Decodes an effect's next step: a pure result, or a command to perform.
254
+ * Runs an effect that reaches its value without performing a command: `[t]` for
255
+ * a {@link Pure}, empty for a {@link Do}. Forces the thunk in the `Pure` case,
256
+ * which {@link Pure}'s contract makes free of consequence.
257
+ *
258
+ * The eliminator for callers that expect no operations at all — the other side
259
+ * of {@link match}, which is for callers that intend to perform them.
260
+ *
261
+ * **The result is tagged on purpose.** Returning `T | null` would collapse two
262
+ * distinct outcomes whenever `T` itself admits `null`: `runPure(pure(null))` and
263
+ * `runPure(someDo)` would both be `null`, so a caller asserting `null` would
264
+ * accept an effect that unexpectedly stopped at a command — exactly the case
265
+ * this exists to rule out. `Option<T>` keeps them apart: `[null]` is a pure
266
+ * `null`, `[]` is a `Do`.
58
267
  *
59
- * This is the only function that knows how `Value` is laid out (a thunk
60
- * `() => T` for `Pure`, a `[command, payload, continuation]` tuple for `Do`).
61
- * Interpreters and proofs must go through `decode` (or `match`) instead of
62
- * inspecting the value, so the representation can change without touching
63
- * them.
268
+ * `O` stays generic rather than narrowing to `Effect<never, T>`. `Effect` is
269
+ * covariant in `O`, so `Effect<never, T>` is assignable to `Effect<O, T>` and
270
+ * not the reverse a continuation's result is always the wider type and would
271
+ * be rejected. `Do<never, T>` is uninhabited besides, which would make the empty
272
+ * case unreachable without a cast.
64
273
  */
65
- export const decode = ({ value }) => typeof value === 'function'
66
- ? { done: true, result: value() }
67
- : { done: false, command: value[0], payload: value[1], continuation: value[2] };
274
+ export const runPure = (e) => typeof e === 'function' ? [e()] : [];
68
275
  /**
69
276
  * Decodes an effect's next step and dispatches its command to `map`,
70
277
  * returning either the final result or the operation's output `R` paired
@@ -72,9 +279,10 @@ export const decode = ({ value }) => typeof value === 'function'
72
279
  * runners, state threading for sync ones — is left to the caller, so every
73
280
  * interpreter loop is this skeleton plus a single eliminator line.
74
281
  */
75
- export const match = (map) => (effect) => {
76
- const d = decode(effect);
77
- return d.done
78
- ? ['done', d.result]
79
- : ['cont', map[d.command](...d.payload), d.continuation];
282
+ export const match = (map) => (e) => {
283
+ if (typeof e === 'function') {
284
+ return ['done', e()];
285
+ }
286
+ const { command, payload, continuation } = e;
287
+ return ['cont', map[command](...payload), continuation];
80
288
  };
@@ -7,10 +7,11 @@ import { asyncRun } from '../../module.js';
7
7
  import { asNominal, create, read, write, } from '../../memory/module.f.js';
8
8
  import { memoryOperationMap, run } from './module.js';
9
9
  import { assert, assertEq } from '../../../asserts/module.f.js';
10
+ import { step } from '../../module.f.js';
10
11
  export const proof = {
11
12
  nodeInterpreter: async () => {
12
- const result = await run(create(1).step(key => write(key, 2).step(() => read(key))));
13
- assertEq(result, 2);
13
+ const x = step(create(1), key => step(write(key, 2), () => read(key)));
14
+ assertEq(await run(x), 2);
14
15
  },
15
16
  reusedOperationMapPersists: async () => {
16
17
  const runner = asyncRun(memoryOperationMap());
@@ -4,9 +4,9 @@ import { type NodeProgram } from './module.f.ts';
4
4
  * resolving to its exit code **without** terminating the process.
5
5
  *
6
6
  * Use this when the caller must stay alive afterwards — e.g. when proofs are
7
- * registered under an external test runner (Node `--test`, Bun, Playwright)
8
- * that owns the process lifecycle. For a standalone CLI entry point that should
9
- * exit with the program's code, use {@link run} instead.
7
+ * registered under an external test runner (Node `--test`, Bun, Deno) that owns
8
+ * the process lifecycle. For a standalone CLI entry point that should exit with
9
+ * the program's code, use {@link run} instead.
10
10
  */
11
11
  export declare const runEffect: (p: NodeProgram) => Promise<number>;
12
12
  /**
@@ -224,8 +224,7 @@ export type Await = readonly ['await', (p: unknown) => readonly [unknown]];
224
224
  export declare const awaitIfPromise: (p: unknown) => Effect<Await, unknown>;
225
225
  /**
226
226
  * Signature of a framework test-registration function (e.g. `nodeTest.test`,
227
- * `bunTest.test`, `pwTest`). Returns `Promise<void>` so async sub-tests can
228
- * be awaited.
227
+ * `bunTest.test`). Returns `Promise<void>` so async sub-tests can be awaited.
229
228
  */
230
229
  export type TestFn = (name: string, options: {
231
230
  readonly expectFailure: boolean;
@@ -233,8 +232,8 @@ export type TestFn = (name: string, options: {
233
232
  /**
234
233
  * A thin wrapper around a framework's `test` function. Passed through
235
234
  * `registerModule` so nested test registration uses the appropriate context
236
- * (e.g. `inlineContext` on Bun and Playwright, which do not support nested
237
- * `test()` calls inside a callback).
235
+ * (e.g. `inlineContext` on Bun, which does not support nested `test()` calls
236
+ * inside a callback).
238
237
  */
239
238
  export type TestContext = {
240
239
  readonly test: TestFn;
@@ -247,7 +246,7 @@ export type NodeEffect<T> = Effect<NodeOp, T>;
247
246
  /**
248
247
  * Writes an error line to `stderr` and yields exit code `1`. The canonical
249
248
  * "fail with a message" program for a `NodeProgram`. For non-`1` exit codes,
250
- * compose `error(s).step(() => pure(n))` directly.
249
+ * compose `mapStep(error(s), () => n)` directly.
251
250
  */
252
251
  export declare const errorExit: (s: string) => Effect<Write, number>;
253
252
  export type NodeOperationMap = ToAsyncOperationMap<NodeOp>;
@@ -258,7 +257,16 @@ export type Env = {
258
257
  readonly [k: string]: string | undefined;
259
258
  };
260
259
  /** Identifies the JavaScript runtime detected at startup. */
261
- export type Engine = 'node' | 'bun' | 'playwright';
260
+ export type Engine = 'node' | 'bun' | 'deno';
261
+ /** Compares semantic versions numerically by major, minor, then patch. */
262
+ export declare const versionLessThan: (version: string, minimum: string) => boolean;
263
+ /**
264
+ * Reports whether an external runner needs FunctionalScript's flattened test
265
+ * registration strategy. Node uses the native `expectFailure` option only
266
+ * from the Node 26 baseline; Deno is deliberately exempt from this Node-only
267
+ * version check.
268
+ */
269
+ export declare const usesInlineTestContext: (engine: Engine, nodeVersion?: string) => boolean;
262
270
  /**
263
271
  * Runtime options passed to every `NodeProgram`.
264
272
  *
@@ -269,8 +277,9 @@ export type Engine = 'node' | 'bun' | 'playwright';
269
277
  * - `testContext`: Node `--test` context; used by `register` on Node.
270
278
  * - `bunTestContext`: Bun-compatible context that flattens nested tests inline,
271
279
  * working around Bun's lack of nested `test()` support.
272
- * - `playwrightTestContext`: Playwright context using the same inline strategy.
273
280
  * - `engine`: runtime detected at startup; controls which context `register` selects.
281
+ * - `nodeVersion`: detected Node version; absent for other and virtual runtimes.
282
+ * - `inlineTestContext`: whether the selected context flattens nested tests.
274
283
  */
275
284
  export type NodeProgramOptions = {
276
285
  readonly args: readonly string[];
@@ -283,8 +292,9 @@ export type NodeProgramOptions = {
283
292
  };
284
293
  readonly testContext: TestContext;
285
294
  readonly bunTestContext: TestContext;
286
- readonly playwrightTestContext: TestContext;
287
295
  readonly engine: Engine;
296
+ readonly nodeVersion?: string;
297
+ readonly inlineTestContext: boolean;
288
298
  };
289
299
  export type Program<O extends Operation> = (options: NodeProgramOptions) => Effect<O, number>;
290
300
  export type NodeProgram = Program<NodeOp>;
@@ -14,7 +14,7 @@ import { codePointListToString } from '../../text/utf16/module.f.js';
14
14
  import { reverse } from '../../types/list/module.f.js';
15
15
  import { length } from '../../types/bit_vec/module.f.js';
16
16
  import { ok, error as resultError, mapOk } from '../../types/result/module.f.js';
17
- import { do_, okStep, pure } from '../module.f.js';
17
+ import { do_, mapStep, okStep, pure, step } from '../module.f.js';
18
18
  /**
19
19
  * True if `e` is a "file or directory does not exist" (`ENOENT`) error.
20
20
  *
@@ -43,7 +43,7 @@ export const readFile = do_('readFile');
43
43
  * on errors (e.g. convert them into domain-specific errors) or `unwrap` at the
44
44
  * call site.
45
45
  */
46
- export const readUtf8File = (path) => readFile(path).step(r => pure(mapOk(utf8ToString)(r)));
46
+ export const readUtf8File = (path) => mapStep(readFile(path), mapOk(utf8ToString));
47
47
  export const readdir = do_('readdir');
48
48
  export const writeFile = do_('writeFile');
49
49
  /** Writes a string to `path` as UTF-8 bytes. */
@@ -57,7 +57,7 @@ export const access = do_('access');
57
57
  export const createExclusive = do_('createExclusive');
58
58
  export const writeBytes = do_('writeBytes');
59
59
  const writeLoop = (path) => {
60
- const f = (offset, e) => e.step(r => {
60
+ const f = (offset, e) => step(e, r => {
61
61
  if (r === undefined) {
62
62
  return pure(ok(undefined));
63
63
  }
@@ -69,13 +69,11 @@ const writeLoop = (path) => {
69
69
  if ((lenV & 7n) !== 0n) {
70
70
  return pure(resultError('invalid buffer size'));
71
71
  }
72
- return writeBytes(path, offset, v)
73
- .step(okStep(() => f(offset + Number(lenV >> 3n), tail)));
72
+ return step(writeBytes(path, offset, v), okStep(() => f(offset + Number(lenV >> 3n), tail)));
74
73
  });
75
74
  return f;
76
75
  };
77
- export const writeFromStream = (path, e) => createExclusive(path)
78
- .step(okStep(() => writeLoop(path)(0, e)));
76
+ export const writeFromStream = (path, e) => step(createExclusive(path), okStep(() => writeLoop(path)(0, e)));
79
77
  export const stat = do_('stat');
80
78
  export const createServer = do_('createServer');
81
79
  export const listen = do_('listen');
@@ -113,7 +111,7 @@ const lf = 0x0a;
113
111
  * rather than the O(n²) of copying a growing array on every byte.
114
112
  */
115
113
  export const readLine = (stream) => {
116
- const loop = (acc) => read(stream).step(b => b === null
114
+ const loop = (acc) => step(read(stream), b => b === null
117
115
  ? pure(acc === null ? null : utf8ListToString(reverse(acc)))
118
116
  : b === lf
119
117
  ? pure(utf8ListToString(reverse(acc)))
@@ -141,11 +139,33 @@ export const now = do_('now');
141
139
  */
142
140
  export const sandbox = do_('sandbox');
143
141
  const awaitPromise = do_('await');
144
- export const awaitIfPromise = (p) => awaitPromise(p).step(([x]) => pure(x));
142
+ export const awaitIfPromise = (p) => mapStep(awaitPromise(p), ([x]) => x);
145
143
  export const test = do_('test');
146
144
  /**
147
145
  * Writes an error line to `stderr` and yields exit code `1`. The canonical
148
146
  * "fail with a message" program for a `NodeProgram`. For non-`1` exit codes,
149
- * compose `error(s).step(() => pure(n))` directly.
147
+ * compose `mapStep(error(s), () => n)` directly.
150
148
  */
151
- export const errorExit = (s) => error(s).step(() => pure(1));
149
+ export const errorExit = (s) => mapStep(error(s), () => 1);
150
+ const versionParts = (version) => version.replace(/^v/, '').split('.').map(Number);
151
+ /** Compares semantic versions numerically by major, minor, then patch. */
152
+ export const versionLessThan = (version, minimum) => {
153
+ const [major = 0, minor = 0, patch = 0] = versionParts(version);
154
+ const [minMajor = 0, minMinor = 0, minPatch = 0] = versionParts(minimum);
155
+ return major < minMajor || major === minMajor && (minor < minMinor || minor === minMinor && patch < minPatch);
156
+ };
157
+ /**
158
+ * Reports whether an external runner needs FunctionalScript's flattened test
159
+ * registration strategy. Node uses the native `expectFailure` option only
160
+ * from the Node 26 baseline; Deno is deliberately exempt from this Node-only
161
+ * version check.
162
+ */
163
+ export const usesInlineTestContext = (engine, nodeVersion) => {
164
+ if (engine === 'bun') {
165
+ return true;
166
+ }
167
+ if (engine !== 'node' || nodeVersion === undefined) {
168
+ return false;
169
+ }
170
+ return versionLessThan(nodeVersion, '26.0.0');
171
+ };
@@ -32,7 +32,7 @@ import { concat, normalize, toPosix } from '../../path/module.f.js';
32
32
  import {} from '../module.f.js';
33
33
  import { asyncRun } from '../module.js';
34
34
  import { memoryOperationMap } from './memory/module.js';
35
- import {} from './module.f.js';
35
+ import { usesInlineTestContext, } from './module.f.js';
36
36
  import { asBase, asNominal } from '../../types/nominal/module.f.js';
37
37
  import { error, ok } from '../../types/result/module.f.js';
38
38
  import { asyncTryCatch } from '../../types/result/module.js';
@@ -250,10 +250,6 @@ const runNodeEffect = asyncRun({
250
250
  read: readStdinByte,
251
251
  test: async (ctx, name, expectFailure, test) => ctx.test(name, { expectFailure }, async (t) => runNodeEffect(test(t))),
252
252
  });
253
- const isPlaywright = 'PLAYWRIGHT_TEST' in (process?.env ?? {});
254
- const pwTest = isPlaywright
255
- ? (await import('@playwright/test')).test
256
- : undefined;
257
253
  const inlineTest = async (name, { expectFailure }, fn) => {
258
254
  if (expectFailure) {
259
255
  try {
@@ -273,25 +269,29 @@ const wrapInlineTest = (register) => ({
273
269
  test: (name, opts, fn) => register(name, () => inlineTest(name, opts, fn))
274
270
  });
275
271
  const bunTestContext = wrapInlineTest(testContext.test);
276
- const playwrightTestContext = wrapInlineTest(pwTest);
272
+ const engine = 'Bun' in globalThis ? 'bun' :
273
+ 'Deno' in globalThis ? 'deno' : 'node';
274
+ const nodeVersion = engine === 'node' ? process.version : undefined;
275
+ const inlineTestContext = usesInlineTestContext(engine, nodeVersion);
277
276
  const options = {
278
277
  args: process.argv.slice(2),
279
278
  env: process.env,
280
279
  home: toPosix(os.homedir()),
281
280
  std: { stdout: process.stdout, stderr: process.stderr },
282
- testContext,
281
+ testContext: inlineTestContext ? wrapInlineTest(testContext.test) : testContext,
283
282
  bunTestContext,
284
- playwrightTestContext,
285
- engine: isPlaywright ? 'playwright' : 'Bun' in globalThis ? 'bun' : 'node',
283
+ engine,
284
+ ...(nodeVersion === undefined ? {} : { nodeVersion }),
285
+ inlineTestContext,
286
286
  };
287
287
  /**
288
288
  * Runs a `NodeProgram` against the real Node globals and process arguments,
289
289
  * resolving to its exit code **without** terminating the process.
290
290
  *
291
291
  * Use this when the caller must stay alive afterwards — e.g. when proofs are
292
- * registered under an external test runner (Node `--test`, Bun, Playwright)
293
- * that owns the process lifecycle. For a standalone CLI entry point that should
294
- * exit with the program's code, use {@link run} instead.
292
+ * registered under an external test runner (Node `--test`, Bun, Deno) that owns
293
+ * the process lifecycle. For a standalone CLI entry point that should exit with
294
+ * the program's code, use {@link run} instead.
295
295
  */
296
296
  export const runEffect = program => runNodeEffect(program(options));
297
297
  /**
@@ -1,4 +1,5 @@
1
1
  export declare const proof: {
2
+ externalTestContext: () => void;
2
3
  map: () => void;
3
4
  fetch: () => void;
4
5
  mkdir: {