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.
- package/README.md +1 -1
- package/fjs/bnf/descent/module.f.d.ts +43 -2
- package/fjs/bnf/descent/module.f.js +37 -12
- package/fjs/bnf/descent/proof.f.d.ts +1 -0
- package/fjs/bnf/descent/proof.f.js +73 -32
- package/fjs/bnf/ll1/module.f.js +2 -2
- package/fjs/bnf/token_symbol/module.f.d.ts +38 -0
- package/fjs/bnf/token_symbol/module.f.js +48 -0
- package/fjs/bnf/token_symbol/proof.f.d.ts +10 -0
- package/fjs/bnf/token_symbol/proof.f.js +37 -0
- package/fjs/cas/cli/module.f.js +6 -9
- package/fjs/cas/evo/module.f.d.ts +77 -13
- package/fjs/cas/evo/module.f.js +130 -18
- package/fjs/cas/evo/proof.f.d.ts +7 -0
- package/fjs/cas/evo/proof.f.js +120 -1
- package/fjs/cas/module.f.js +64 -37
- package/fjs/cas/proof.f.d.ts +3 -9
- package/fjs/cas/proof.f.js +94 -74
- package/fjs/ci/bun/module.f.js +3 -3
- package/fjs/ci/common/module.f.d.ts +1 -2
- package/fjs/ci/common/module.f.js +1 -5
- package/fjs/ci/config/module.f.d.ts +11 -8
- package/fjs/ci/config/module.f.js +24 -9
- package/fjs/ci/deno/module.f.js +3 -3
- package/fjs/ci/module.f.js +13 -7
- package/fjs/ci/nix/module.f.d.ts +61 -0
- package/fjs/ci/nix/module.f.js +92 -0
- package/fjs/ci/nix/proof.f.d.ts +23 -0
- package/fjs/ci/nix/proof.f.js +109 -0
- package/fjs/ci/node/module.f.d.ts +23 -1
- package/fjs/ci/node/module.f.js +58 -12
- package/fjs/ci/proof.f.d.ts +2 -0
- package/fjs/ci/proof.f.js +49 -9
- package/fjs/cli/module.f.js +2 -2
- package/fjs/common/monoid/module.f.d.ts +46 -2
- package/fjs/common/monoid/module.f.js +44 -0
- package/fjs/common/monoid/proof.f.d.ts +5 -0
- package/fjs/common/monoid/proof.f.js +27 -1
- package/fjs/dev/module.f.d.ts +1 -0
- package/fjs/dev/module.f.js +38 -27
- package/fjs/dev/update/module.f.d.ts +11 -0
- package/fjs/dev/update/module.f.js +20 -0
- package/fjs/dev/update/proof.f.d.ts +7 -0
- package/fjs/dev/update/proof.f.js +35 -0
- package/fjs/djs/ast/module.f.d.ts +47 -0
- package/fjs/djs/ast/module.f.js +9 -0
- package/fjs/djs/module.f.js +5 -8
- package/fjs/djs/parser/module.f.d.ts +3 -0
- package/fjs/djs/parser/module.f.js +17 -0
- package/fjs/djs/tokenizer/module.f.js +2 -2
- package/fjs/djs/tokenizer/proof.f.d.ts +1 -0
- package/fjs/djs/tokenizer/proof.f.js +70 -16
- package/fjs/djs/transpiler/module.f.js +6 -6
- package/fjs/effects/eff/module.f.d.ts +72 -0
- package/fjs/effects/eff/module.f.js +46 -0
- package/fjs/effects/eff/proof.f.d.ts +11 -0
- package/fjs/effects/eff/proof.f.js +58 -0
- package/fjs/effects/list/module.f.d.ts +11 -5
- package/fjs/effects/list/module.f.js +2 -1
- package/fjs/effects/memory/proof.f.js +7 -8
- package/fjs/effects/module.f.d.ts +334 -52
- package/fjs/effects/module.f.js +253 -45
- package/fjs/effects/node/memory/proof.js +3 -2
- package/fjs/effects/node/module.d.ts +3 -3
- package/fjs/effects/node/module.f.d.ts +18 -8
- package/fjs/effects/node/module.f.js +31 -11
- package/fjs/effects/node/module.js +12 -12
- package/fjs/effects/node/proof.f.d.ts +1 -0
- package/fjs/effects/node/proof.f.js +32 -11
- package/fjs/effects/node/virtual/module.f.js +1 -1
- package/fjs/effects/proof.f.d.ts +32 -6
- package/fjs/effects/proof.f.js +131 -39
- package/fjs/emergent_testing/all.test.js +2 -1
- package/fjs/emergent_testing/module.f.d.ts +3 -3
- package/fjs/emergent_testing/module.f.js +63 -49
- package/fjs/emergent_testing/proof.f.d.ts +4 -0
- package/fjs/emergent_testing/proof.f.js +54 -21
- package/fjs/emergent_testing/scenarios/thenable.pass.js +1 -1
- package/fjs/fsc/module.f.js +4 -4
- package/fjs/fsm/module.f.js +1 -1
- package/fjs/js/tokenizer/module.f.d.ts +1 -0
- package/fjs/js/tokenizer/module.f.js +13 -6
- package/fjs/{cas/mcp → mcp/cas}/module.f.d.ts +3 -25
- package/fjs/{cas/mcp → mcp/cas}/module.f.js +26 -64
- package/fjs/mcp/evo/module.f.d.ts +27 -0
- package/fjs/mcp/evo/module.f.js +94 -0
- package/fjs/{cas/evo/mcp → mcp/evo}/proof.f.d.ts +2 -0
- package/fjs/{cas/evo/mcp → mcp/evo}/proof.f.js +40 -9
- package/fjs/mcp/module.f.d.ts +54 -237
- package/fjs/mcp/module.f.js +55 -258
- package/fjs/mcp/proof.f.d.ts +43 -32
- package/fjs/mcp/proof.f.js +509 -200
- package/fjs/media/nix/module.f.d.ts +30 -0
- package/fjs/media/nix/module.f.js +166 -0
- package/fjs/media/nix/proof.f.d.ts +32 -0
- package/fjs/media/nix/proof.f.js +127 -0
- package/fjs/media/type/module.f.js +2 -2
- package/fjs/media/type/proof.f.js +7 -10
- package/fjs/module.f.js +5 -9
- package/fjs/protocol/json_rpc/module.f.d.ts +114 -0
- package/fjs/{media/json/rpc → protocol/json_rpc}/module.f.js +3 -3
- package/fjs/{media/json/rpc → protocol/json_rpc}/proof.f.js +3 -3
- package/fjs/protocol/mcp/module.f.d.ts +239 -0
- package/fjs/protocol/mcp/module.f.js +272 -0
- package/fjs/protocol/mcp/proof.f.d.ts +34 -0
- package/fjs/protocol/mcp/proof.f.js +208 -0
- package/fjs/{mcp → protocol/mcp}/stdio/module.f.d.ts +7 -7
- package/fjs/{mcp → protocol/mcp}/stdio/module.f.js +22 -22
- package/fjs/{mcp → protocol/mcp}/stdio/proof.f.js +9 -9
- package/fjs/types/bigint/module.f.js +4 -3
- package/fjs/types/bit_vec/module.f.js +5 -15
- package/fjs/types/btree/remove/module.f.d.ts +5 -0
- package/fjs/types/btree/remove/module.f.js +12 -0
- package/fjs/types/list/module.f.d.ts +21 -0
- package/fjs/types/list/module.f.js +17 -0
- package/fjs/types/list/proof.f.d.ts +1 -0
- package/fjs/types/list/proof.f.js +24 -1
- package/fjs/types/number/module.f.js +2 -1
- package/fjs/types/range_map/module.f.d.ts +14 -13
- package/fjs/types/range_map/module.f.js +18 -13
- package/fjs/types/range_map/proof.f.js +26 -39
- package/fjs/types/range_set/module.f.d.ts +5 -0
- package/fjs/types/range_set/module.f.js +16 -0
- package/fjs/types/range_set/proof.f.d.ts +1 -0
- package/fjs/types/range_set/proof.f.js +19 -0
- package/fjs/types/string/module.f.js +10 -2
- package/fjs/website/module.f.js +2 -3
- package/package.json +5 -5
- package/fjs/cas/evo/mcp/module.f.d.ts +0 -19
- package/fjs/cas/evo/mcp/module.f.js +0 -56
- package/fjs/cas/mcp/proof.f.d.ts +0 -45
- package/fjs/cas/mcp/proof.f.js +0 -545
- package/fjs/ci/playwright/module.f.d.ts +0 -2
- package/fjs/ci/playwright/module.f.js +0 -25
- package/fjs/media/json/rpc/module.f.d.ts +0 -114
- /package/fjs/{media/json/rpc → protocol/json_rpc}/proof.f.d.ts +0 -0
- /package/fjs/{mcp → protocol/mcp}/stdio/proof.f.d.ts +0 -0
package/fjs/effects/module.f.js
CHANGED
|
@@ -1,51 +1,248 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core effect type constructors and combinators.
|
|
3
3
|
*
|
|
4
|
-
* Effect
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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 = (
|
|
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
|
|
46
|
-
* results. The `void` accumulator sibling of
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
|
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) => (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
13
|
-
assertEq(
|
|
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,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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
|
|
237
|
-
*
|
|
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)
|
|
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' | '
|
|
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)
|
|
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) =>
|
|
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)
|
|
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)
|
|
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)
|
|
147
|
+
* compose `mapStep(error(s), () => n)` directly.
|
|
150
148
|
*/
|
|
151
|
-
export const errorExit = (s) => error(s)
|
|
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
|
|
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
|
-
|
|
285
|
-
|
|
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,
|
|
293
|
-
*
|
|
294
|
-
*
|
|
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
|
/**
|