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,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());
@@ -247,7 +247,7 @@ export type NodeEffect<T> = Effect<NodeOp, T>;
247
247
  /**
248
248
  * Writes an error line to `stderr` and yields exit code `1`. The canonical
249
249
  * "fail with a message" program for a `NodeProgram`. For non-`1` exit codes,
250
- * compose `error(s).step(() => pure(n))` directly.
250
+ * compose `mapStep(error(s), () => n)` directly.
251
251
  */
252
252
  export declare const errorExit: (s: string) => Effect<Write, number>;
253
253
  export type NodeOperationMap = ToAsyncOperationMap<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,11 @@ 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);
@@ -1,25 +1,33 @@
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";
3
+ import { match, pure, step } from "../module.f.js";
4
4
  import { both, fetch, mkdir, now, readdir, readFile, readUtf8File, rm, sandbox, writeFile, writeUtf8File, rename, readBytes, randomInt, writeFromStream } 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 = {
10
20
  map: () => {
11
- const e = readFile('hello').step(([k, v]) => {
21
+ const e = step(readFile('hello'), ([k, v]) => {
12
22
  assert(k !== 'error', v);
13
23
  return pure(uint(v) * 2n);
14
24
  });
15
25
  //
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)]));
26
+ let r = readHello(e);
27
+ while (r[0] === 'cont') {
28
+ r = readHello(r[2](r[1]));
21
29
  }
22
- assertEq(d.result, 0x2an);
30
+ assertEq(r[1], 0x2an);
23
31
  },
24
32
  fetch: () => {
25
33
  const [_, [t, result]] = virtual({
@@ -282,12 +290,12 @@ export const proof = {
282
290
  },
283
291
  memory: {
284
292
  createAndRead: () => {
285
- const effect = memCreate(42).step(key => memRead(key));
293
+ const effect = step(memCreate(42), key => memRead(key));
286
294
  const [_, value] = virtual(emptyState)(effect);
287
295
  assertEq(value, 42);
288
296
  },
289
297
  createAndWrite: () => {
290
- const effect = memCreate(1).step(key => memWrite(key, 99).step(() => memRead(key)));
298
+ const effect = step(memCreate(1), key => step(memWrite(key, 99), () => memRead(key)));
291
299
  const [_, value] = virtual(emptyState)(effect);
292
300
  assertEq(value, 99);
293
301
  },
@@ -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
  };