@barnum/barnum 0.2.3 → 0.3.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/artifacts/linux-arm64/barnum +0 -0
- package/artifacts/linux-x64/barnum +0 -0
- package/artifacts/macos-arm64/barnum +0 -0
- package/artifacts/macos-x64/barnum +0 -0
- package/artifacts/win-x64/barnum.exe +0 -0
- package/cli.cjs +33 -0
- package/dist/all.d.ts +12 -0
- package/dist/all.js +8 -0
- package/dist/ast.d.ts +375 -0
- package/dist/ast.js +381 -0
- package/dist/bind.d.ts +62 -0
- package/dist/bind.js +106 -0
- package/dist/builtins.d.ts +257 -0
- package/dist/builtins.js +600 -0
- package/dist/chain.d.ts +2 -0
- package/dist/chain.js +8 -0
- package/dist/effect-id.d.ts +14 -0
- package/dist/effect-id.js +16 -0
- package/dist/handler.d.ts +50 -0
- package/dist/handler.js +146 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +5 -0
- package/dist/pipe.d.ts +11 -0
- package/dist/pipe.js +11 -0
- package/dist/race.d.ts +53 -0
- package/dist/race.js +141 -0
- package/dist/recursive.d.ts +34 -0
- package/dist/recursive.js +53 -0
- package/dist/run.d.ts +7 -0
- package/dist/run.js +143 -0
- package/dist/schema.d.ts +8 -0
- package/dist/schema.js +95 -0
- package/dist/try-catch.d.ts +23 -0
- package/dist/try-catch.js +36 -0
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +46 -0
- package/package.json +40 -16
- package/src/all.ts +89 -0
- package/src/ast.ts +878 -0
- package/src/bind.ts +192 -0
- package/src/builtins.ts +804 -0
- package/src/chain.ts +17 -0
- package/src/effect-id.ts +30 -0
- package/src/handler.ts +279 -0
- package/src/index.ts +30 -0
- package/src/pipe.ts +93 -0
- package/src/race.ts +183 -0
- package/src/recursive.ts +112 -0
- package/src/run.ts +181 -0
- package/src/schema.ts +118 -0
- package/src/try-catch.ts +53 -0
- package/src/worker.ts +56 -0
- package/README.md +0 -19
- package/barnum-config-schema.json +0 -408
- package/cli.js +0 -20
- package/index.js +0 -23
package/src/builtins.ts
ADDED
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Action,
|
|
3
|
+
type Option as OptionT,
|
|
4
|
+
type Pipeable,
|
|
5
|
+
type Result as ResultT,
|
|
6
|
+
type TaggedUnion,
|
|
7
|
+
type TypedAction,
|
|
8
|
+
typedAction,
|
|
9
|
+
} from "./ast.js";
|
|
10
|
+
import { chain } from "./chain.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Typed combinators for structural data transformations.
|
|
14
|
+
*
|
|
15
|
+
* All builtins emit `{ kind: "Builtin", builtin: { kind: ... } }` handler
|
|
16
|
+
* kinds. The Rust scheduler executes them inline (no subprocess).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Constant — produce a fixed value (takes no pipeline input)
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export function constant<TValue>(value: TValue): TypedAction<any, TValue> {
|
|
24
|
+
return typedAction({
|
|
25
|
+
kind: "Invoke",
|
|
26
|
+
handler: { kind: "Builtin", builtin: { kind: "Constant", value } },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Identity — pass input through unchanged
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export const identity: TypedAction<any, any> = typedAction({
|
|
35
|
+
kind: "Invoke",
|
|
36
|
+
handler: { kind: "Builtin", builtin: { kind: "Identity" } },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Drop — discard pipeline value
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
export const drop: TypedAction<any, never> = typedAction({
|
|
44
|
+
kind: "Invoke",
|
|
45
|
+
handler: { kind: "Builtin", builtin: { kind: "Drop" } },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Tag — wrap input as a tagged union variant
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Wrap input as a tagged union member. Requires the full variant map TDef
|
|
54
|
+
* so the output type carries __def for branch decomposition.
|
|
55
|
+
*
|
|
56
|
+
* Usage: tag<{ Ok: string; Err: number }, "Ok">("Ok")
|
|
57
|
+
* input: string → output: TaggedUnion<{ Ok: string; Err: number }>
|
|
58
|
+
*/
|
|
59
|
+
export function tag<
|
|
60
|
+
TDef extends Record<string, unknown>,
|
|
61
|
+
TKind extends keyof TDef & string,
|
|
62
|
+
>(kind: TKind): TypedAction<TDef[TKind], TaggedUnion<TDef>> {
|
|
63
|
+
return typedAction({
|
|
64
|
+
kind: "Invoke",
|
|
65
|
+
handler: { kind: "Builtin", builtin: { kind: "Tag", value: kind } },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Merge — merge a tuple of objects into a single object
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
+
type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (
|
|
75
|
+
x: infer I,
|
|
76
|
+
) => void
|
|
77
|
+
? I
|
|
78
|
+
: never;
|
|
79
|
+
|
|
80
|
+
export function merge<
|
|
81
|
+
TObjects extends Record<string, unknown>[],
|
|
82
|
+
>(): TypedAction<TObjects, UnionToIntersection<TObjects[number]>> {
|
|
83
|
+
return typedAction({
|
|
84
|
+
kind: "Invoke",
|
|
85
|
+
handler: { kind: "Builtin", builtin: { kind: "Merge" } },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Flatten — flatten a nested array one level
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
export function flatten<TElement>(): TypedAction<TElement[][], TElement[]> {
|
|
94
|
+
return typedAction({
|
|
95
|
+
kind: "Invoke",
|
|
96
|
+
handler: { kind: "Builtin", builtin: { kind: "Flatten" } },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// ExtractField — extract a single field from an object
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
export function extractField<
|
|
105
|
+
TObj extends Record<string, unknown>,
|
|
106
|
+
TField extends keyof TObj & string,
|
|
107
|
+
>(field: TField): TypedAction<TObj, TObj[TField]> {
|
|
108
|
+
return typedAction({
|
|
109
|
+
kind: "Invoke",
|
|
110
|
+
handler: {
|
|
111
|
+
kind: "Builtin",
|
|
112
|
+
builtin: { kind: "ExtractField", value: field },
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// ExtractIndex — extract a single element from an array by index
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
export function extractIndex<TTuple extends unknown[], TIndex extends number>(
|
|
122
|
+
index: TIndex,
|
|
123
|
+
): TypedAction<TTuple, TTuple[TIndex]> {
|
|
124
|
+
return typedAction({
|
|
125
|
+
kind: "Invoke",
|
|
126
|
+
handler: {
|
|
127
|
+
kind: "Builtin",
|
|
128
|
+
builtin: { kind: "ExtractIndex", value: index },
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Pick — select named fields from an object
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
export function pick<
|
|
138
|
+
TObj extends Record<string, unknown>,
|
|
139
|
+
TKeys extends (keyof TObj & string)[],
|
|
140
|
+
>(...keys: TKeys): TypedAction<TObj, Pick<TObj, TKeys[number]>> {
|
|
141
|
+
return typedAction({
|
|
142
|
+
kind: "Invoke",
|
|
143
|
+
handler: { kind: "Builtin", builtin: { kind: "Pick", value: keys } },
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// DropResult — run an action for side effects, discard its output
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
export function dropResult<TInput, TOutput>(
|
|
152
|
+
action: Pipeable<TInput, TOutput>,
|
|
153
|
+
): TypedAction<TInput, never> {
|
|
154
|
+
// Build AST directly — chain inference fails when drop's TValue
|
|
155
|
+
// isn't constrained by context (resolves to unknown ≠ TOutput).
|
|
156
|
+
return typedAction({
|
|
157
|
+
kind: "Chain",
|
|
158
|
+
first: action as Action,
|
|
159
|
+
rest: {
|
|
160
|
+
kind: "Invoke",
|
|
161
|
+
handler: { kind: "Builtin", builtin: { kind: "Drop" } },
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// WithResource — RAII-style create/action/dispose
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* RAII-style resource management combinator.
|
|
172
|
+
*
|
|
173
|
+
* Runs `create` to acquire a resource, then merges the resource with the
|
|
174
|
+
* original input into a flat object (`TResource & TIn`) for the action.
|
|
175
|
+
* After the action completes, `dispose` receives the resource for cleanup.
|
|
176
|
+
* The overall combinator returns the action's output.
|
|
177
|
+
*
|
|
178
|
+
* ```
|
|
179
|
+
* TIn → create → TResource
|
|
180
|
+
* → merge(TResource, TIn) → TResource & TIn
|
|
181
|
+
* → action(TResource & TIn) → TOut
|
|
182
|
+
* → dispose(TResource) → (discarded)
|
|
183
|
+
* → TOut
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export function withResource<
|
|
187
|
+
TIn extends Record<string, unknown>,
|
|
188
|
+
TResource extends Record<string, unknown>,
|
|
189
|
+
TOut,
|
|
190
|
+
TDisposeOut = unknown,
|
|
191
|
+
>({
|
|
192
|
+
create,
|
|
193
|
+
action,
|
|
194
|
+
dispose,
|
|
195
|
+
}: {
|
|
196
|
+
create: Pipeable<TIn, TResource>;
|
|
197
|
+
action: Pipeable<TResource & TIn, TOut>;
|
|
198
|
+
dispose: Pipeable<TResource, TDisposeOut>;
|
|
199
|
+
}): TypedAction<TIn, TOut> {
|
|
200
|
+
const mergeBuiltin: Action = {
|
|
201
|
+
kind: "Invoke",
|
|
202
|
+
handler: { kind: "Builtin", builtin: { kind: "Merge" } },
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Step 1: all(create, identity) → [TResource, TIn] → merge → TResource & TIn
|
|
206
|
+
const acquireAndMerge = chain(
|
|
207
|
+
typedAction<TIn, [TResource, TIn]>({
|
|
208
|
+
kind: "All",
|
|
209
|
+
actions: [create as Action, identity as Action],
|
|
210
|
+
}),
|
|
211
|
+
typedAction<[TResource, TIn], TResource & TIn>(mergeBuiltin),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Step 2: all(action, identity) → [TOut, TResource & TIn]
|
|
215
|
+
// Keep merged object so dispose can access resource fields.
|
|
216
|
+
const actionAndKeepMerged = typedAction<
|
|
217
|
+
TResource & TIn,
|
|
218
|
+
[TOut, TResource & TIn]
|
|
219
|
+
>({
|
|
220
|
+
kind: "All",
|
|
221
|
+
actions: [action as Action, identity as Action],
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Step 3: all(extractIndex(0), chain(extractIndex(1), dispose)) → [TOut, unknown]
|
|
225
|
+
const disposeAndKeepResult = typedAction<
|
|
226
|
+
[TOut, TResource & TIn],
|
|
227
|
+
[TOut, unknown]
|
|
228
|
+
>({
|
|
229
|
+
kind: "All",
|
|
230
|
+
actions: [
|
|
231
|
+
extractIndex<[TOut, TResource & TIn], 0>(0) as Action,
|
|
232
|
+
chain(
|
|
233
|
+
extractIndex<[TOut, TResource & TIn], 1>(1),
|
|
234
|
+
dispose as Pipeable<TResource & TIn, unknown>,
|
|
235
|
+
) as Action,
|
|
236
|
+
],
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Step 4: extractIndex(0) → TOut
|
|
240
|
+
return chain(
|
|
241
|
+
chain(chain(acquireAndMerge, actionAndKeepMerged), disposeAndKeepResult),
|
|
242
|
+
extractIndex<[TOut, unknown], 0>(0),
|
|
243
|
+
) as TypedAction<TIn, TOut>;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Augment — run a transform, merge its output back into the original input
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Run `action` on the input, then merge the action's output fields back
|
|
252
|
+
* into the original input object. The action must accept exactly `TInput`.
|
|
253
|
+
* Use `pick` inside the action's pipe if the inner handler needs a subset.
|
|
254
|
+
*
|
|
255
|
+
* Example:
|
|
256
|
+
* augment(pipe(pick("file"), migrate))
|
|
257
|
+
* // { file, outputPath } → { file, outputPath, content, migrated }
|
|
258
|
+
*/
|
|
259
|
+
export function augment<
|
|
260
|
+
TInput extends Record<string, unknown>,
|
|
261
|
+
TOutput extends Record<string, unknown>,
|
|
262
|
+
>(action: Pipeable<TInput, TOutput>): TypedAction<TInput, TInput & TOutput> {
|
|
263
|
+
// Build AST directly — chain inference fails because [TOutput, TInput]
|
|
264
|
+
// doesn't match merge()'s Record<string, unknown>[] with invariance.
|
|
265
|
+
return typedAction({
|
|
266
|
+
kind: "Chain",
|
|
267
|
+
first: {
|
|
268
|
+
kind: "All",
|
|
269
|
+
actions: [action as Action, identity as Action],
|
|
270
|
+
},
|
|
271
|
+
rest: {
|
|
272
|
+
kind: "Invoke",
|
|
273
|
+
handler: { kind: "Builtin", builtin: { kind: "Merge" } },
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// Tap — run an action for side effects, preserve original input
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Run `action` on the input for its side effects, then discard the action's
|
|
284
|
+
* output and return the original input unchanged. The action must accept
|
|
285
|
+
* exactly `TInput`. Use `pick` inside the action's pipe if the inner
|
|
286
|
+
* handler needs a subset.
|
|
287
|
+
*
|
|
288
|
+
* Constraint: input must be an object (uses augment internally, which
|
|
289
|
+
* relies on all + merge).
|
|
290
|
+
*
|
|
291
|
+
* Example:
|
|
292
|
+
* pipe(tap(pipe(pick("worktreePath", "description"), implement)), createPR)
|
|
293
|
+
*/
|
|
294
|
+
export function tap<TInput extends Record<string, unknown>>(
|
|
295
|
+
action: Pipeable<TInput, any>,
|
|
296
|
+
): TypedAction<TInput, TInput> {
|
|
297
|
+
// Build AST directly — internal plumbing (action → constant → augment)
|
|
298
|
+
// can't go through typed chain/augment with invariant phantom fields.
|
|
299
|
+
// tap: all(chain(action, constant({})), identity()) → merge
|
|
300
|
+
return typedAction({
|
|
301
|
+
kind: "Chain",
|
|
302
|
+
first: {
|
|
303
|
+
kind: "All",
|
|
304
|
+
actions: [
|
|
305
|
+
{
|
|
306
|
+
kind: "Chain",
|
|
307
|
+
first: action as Action,
|
|
308
|
+
rest: {
|
|
309
|
+
kind: "Invoke",
|
|
310
|
+
handler: {
|
|
311
|
+
kind: "Builtin",
|
|
312
|
+
builtin: { kind: "Constant", value: {} },
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
kind: "Invoke",
|
|
318
|
+
handler: { kind: "Builtin", builtin: { kind: "Identity" } },
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
},
|
|
322
|
+
rest: {
|
|
323
|
+
kind: "Invoke",
|
|
324
|
+
handler: { kind: "Builtin", builtin: { kind: "Merge" } },
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// Range — produce an integer array [start, start+1, ..., end-1]
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
export function range(start: number, end: number): TypedAction<any, number[]> {
|
|
334
|
+
const result: number[] = [];
|
|
335
|
+
for (let i = start; i < end; i++) {
|
|
336
|
+
result.push(i);
|
|
337
|
+
}
|
|
338
|
+
return typedAction({
|
|
339
|
+
kind: "Invoke",
|
|
340
|
+
handler: { kind: "Builtin", builtin: { kind: "Constant", value: result } },
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// Option namespace — combinators for Option<T> tagged unions
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
// Shared AST fragments for Option desugaring
|
|
349
|
+
const TAG_SOME: Action = {
|
|
350
|
+
kind: "Invoke",
|
|
351
|
+
handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Some" } },
|
|
352
|
+
};
|
|
353
|
+
const TAG_NONE: Action = {
|
|
354
|
+
kind: "Invoke",
|
|
355
|
+
handler: { kind: "Builtin", builtin: { kind: "Tag", value: "None" } },
|
|
356
|
+
};
|
|
357
|
+
const EXTRACT_VALUE: Action = {
|
|
358
|
+
kind: "Invoke",
|
|
359
|
+
handler: {
|
|
360
|
+
kind: "Builtin",
|
|
361
|
+
builtin: { kind: "ExtractField", value: "value" },
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
const DROP: Action = {
|
|
365
|
+
kind: "Invoke",
|
|
366
|
+
handler: { kind: "Builtin", builtin: { kind: "Drop" } },
|
|
367
|
+
};
|
|
368
|
+
const IDENTITY: Action = {
|
|
369
|
+
kind: "Invoke",
|
|
370
|
+
handler: { kind: "Builtin", builtin: { kind: "Identity" } },
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/** Wrap branch cases with ExtractField("value") auto-unwrapping. */
|
|
374
|
+
function optionBranch(someCaseBody: Action, noneCaseBody: Action): Action {
|
|
375
|
+
return {
|
|
376
|
+
kind: "Branch",
|
|
377
|
+
cases: {
|
|
378
|
+
Some: { kind: "Chain", first: EXTRACT_VALUE, rest: someCaseBody },
|
|
379
|
+
None: { kind: "Chain", first: EXTRACT_VALUE, rest: noneCaseBody },
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Option namespace. All combinators produce TypedAction AST nodes that
|
|
386
|
+
* desugar to branch + existing builtins, except collect which uses the
|
|
387
|
+
* CollectSome builtin.
|
|
388
|
+
*/
|
|
389
|
+
export const Option = {
|
|
390
|
+
/**
|
|
391
|
+
* Wrap a value as Some. `T → Option<T>`
|
|
392
|
+
*
|
|
393
|
+
* Equivalent to `tag<OptionDef<T>, "Some">("Some")`.
|
|
394
|
+
*/
|
|
395
|
+
some<T>(): TypedAction<T, OptionT<T>> {
|
|
396
|
+
return typedAction(TAG_SOME);
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Produce a None. `never → Option<T>`
|
|
401
|
+
*
|
|
402
|
+
* Chain after `.drop()` to discard the current value first.
|
|
403
|
+
* Equivalent to `tag<OptionDef<T>, "None">("None")`.
|
|
404
|
+
*/
|
|
405
|
+
none<T>(): TypedAction<never, OptionT<T>> {
|
|
406
|
+
return typedAction(TAG_NONE);
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Transform the Some value. `Option<T> → Option<U>`
|
|
411
|
+
*
|
|
412
|
+
* Desugars to: `branch({ Some: pipe(action, tag("Some")), None: tag("None") })`
|
|
413
|
+
*/
|
|
414
|
+
map<T, U>(action: Pipeable<T, U>): TypedAction<OptionT<T>, OptionT<U>> {
|
|
415
|
+
return typedAction(
|
|
416
|
+
optionBranch(
|
|
417
|
+
{ kind: "Chain", first: action as Action, rest: TAG_SOME },
|
|
418
|
+
TAG_NONE,
|
|
419
|
+
),
|
|
420
|
+
);
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Monadic bind (flatMap). If Some, pass the value to action which
|
|
425
|
+
* returns Option<U>. If None, stay None. `Option<T> → Option<U>`
|
|
426
|
+
*
|
|
427
|
+
* This is the most fundamental combinator — map, flatten, and filter
|
|
428
|
+
* are all derivable from andThen + constructors.
|
|
429
|
+
*
|
|
430
|
+
* Desugars to: `branch({ Some: action, None: tag("None") })`
|
|
431
|
+
*/
|
|
432
|
+
andThen<T, U>(
|
|
433
|
+
action: Pipeable<T, OptionT<U>>,
|
|
434
|
+
): TypedAction<OptionT<T>, OptionT<U>> {
|
|
435
|
+
return typedAction(optionBranch(action as Action, TAG_NONE));
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Extract the Some value or produce a default from an action.
|
|
440
|
+
* `Option<T> → T`
|
|
441
|
+
*
|
|
442
|
+
* The defaultAction takes no meaningful input (never) and must produce T.
|
|
443
|
+
* Use `Option.unwrapOr(constant("fallback"))`.
|
|
444
|
+
*
|
|
445
|
+
* The None branch drops its void payload before calling defaultAction,
|
|
446
|
+
* matching Rust's `unwrap_or_else(|| default)` where the closure takes
|
|
447
|
+
* no arguments.
|
|
448
|
+
*
|
|
449
|
+
* Desugars to: `branch({ Some: identity(), None: pipe(drop(), defaultAction) })`
|
|
450
|
+
*/
|
|
451
|
+
unwrapOr<T>(defaultAction: Pipeable<never, T>): TypedAction<OptionT<T>, T> {
|
|
452
|
+
return typedAction({
|
|
453
|
+
kind: "Branch",
|
|
454
|
+
cases: {
|
|
455
|
+
Some: { kind: "Chain", first: EXTRACT_VALUE, rest: IDENTITY },
|
|
456
|
+
None: {
|
|
457
|
+
kind: "Chain",
|
|
458
|
+
first: EXTRACT_VALUE,
|
|
459
|
+
rest: { kind: "Chain", first: DROP, rest: defaultAction as Action },
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Unwrap a nested Option. `Option<Option<T>> → Option<T>`
|
|
467
|
+
*
|
|
468
|
+
* Desugars to: `branch({ Some: identity(), None: tag("None") })`
|
|
469
|
+
*/
|
|
470
|
+
flatten<T>(): TypedAction<OptionT<OptionT<T>>, OptionT<T>> {
|
|
471
|
+
return typedAction(optionBranch(IDENTITY, TAG_NONE));
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Conditional keep. If Some, pass value to predicate which returns
|
|
476
|
+
* Option<T> (some() to keep, none() to discard). If None, stay None.
|
|
477
|
+
* `Option<T> → Option<T>`
|
|
478
|
+
*
|
|
479
|
+
* This has the same signature and desugaring as andThen with T=U.
|
|
480
|
+
* Named "filter" for readability when the intent is filtering.
|
|
481
|
+
*
|
|
482
|
+
* Desugars to: `branch({ Some: predicate, None: tag("None") })`
|
|
483
|
+
*/
|
|
484
|
+
filter<T>(
|
|
485
|
+
predicate: Pipeable<T, OptionT<T>>,
|
|
486
|
+
): TypedAction<OptionT<T>, OptionT<T>> {
|
|
487
|
+
return typedAction(optionBranch(predicate as Action, TAG_NONE));
|
|
488
|
+
},
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Collect Some values from an array, discarding Nones.
|
|
492
|
+
* `Option<T>[] → T[]`
|
|
493
|
+
*
|
|
494
|
+
* This is a builtin handler (CollectSome) — it can't be expressed
|
|
495
|
+
* as a composition of existing AST nodes because it requires
|
|
496
|
+
* array-level filtering logic.
|
|
497
|
+
*/
|
|
498
|
+
collect<T = any>(): TypedAction<OptionT<T>[], T[]> {
|
|
499
|
+
return typedAction({
|
|
500
|
+
kind: "Invoke",
|
|
501
|
+
handler: { kind: "Builtin", builtin: { kind: "CollectSome" } },
|
|
502
|
+
});
|
|
503
|
+
},
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Test if the value is Some. `Option<T> → boolean`
|
|
507
|
+
*
|
|
508
|
+
* Rarely useful — branch on Some/None directly instead.
|
|
509
|
+
*
|
|
510
|
+
* Desugars to: `branch({ Some: pipe(drop(), constant(true)), None: pipe(drop(), constant(false)) })`
|
|
511
|
+
*/
|
|
512
|
+
isSome<T>(): TypedAction<OptionT<T>, boolean> {
|
|
513
|
+
const constTrue: Action = {
|
|
514
|
+
kind: "Invoke",
|
|
515
|
+
handler: { kind: "Builtin", builtin: { kind: "Constant", value: true } },
|
|
516
|
+
};
|
|
517
|
+
const constFalse: Action = {
|
|
518
|
+
kind: "Invoke",
|
|
519
|
+
handler: { kind: "Builtin", builtin: { kind: "Constant", value: false } },
|
|
520
|
+
};
|
|
521
|
+
return typedAction(
|
|
522
|
+
optionBranch(
|
|
523
|
+
{ kind: "Chain", first: DROP, rest: constTrue },
|
|
524
|
+
{ kind: "Chain", first: DROP, rest: constFalse },
|
|
525
|
+
),
|
|
526
|
+
);
|
|
527
|
+
},
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Test if the value is None. `Option<T> → boolean`
|
|
531
|
+
*
|
|
532
|
+
* Rarely useful — branch on Some/None directly instead.
|
|
533
|
+
*
|
|
534
|
+
* Desugars to: `branch({ Some: pipe(drop(), constant(false)), None: pipe(drop(), constant(true)) })`
|
|
535
|
+
*/
|
|
536
|
+
isNone<T>(): TypedAction<OptionT<T>, boolean> {
|
|
537
|
+
const constTrue: Action = {
|
|
538
|
+
kind: "Invoke",
|
|
539
|
+
handler: { kind: "Builtin", builtin: { kind: "Constant", value: true } },
|
|
540
|
+
};
|
|
541
|
+
const constFalse: Action = {
|
|
542
|
+
kind: "Invoke",
|
|
543
|
+
handler: { kind: "Builtin", builtin: { kind: "Constant", value: false } },
|
|
544
|
+
};
|
|
545
|
+
return typedAction(
|
|
546
|
+
optionBranch(
|
|
547
|
+
{ kind: "Chain", first: DROP, rest: constFalse },
|
|
548
|
+
{ kind: "Chain", first: DROP, rest: constTrue },
|
|
549
|
+
),
|
|
550
|
+
);
|
|
551
|
+
},
|
|
552
|
+
} as const;
|
|
553
|
+
|
|
554
|
+
// ---------------------------------------------------------------------------
|
|
555
|
+
// Result namespace — combinators for Result<TValue, TError> tagged unions
|
|
556
|
+
// ---------------------------------------------------------------------------
|
|
557
|
+
|
|
558
|
+
// Shared AST fragments for Result desugaring
|
|
559
|
+
const TAG_OK: Action = {
|
|
560
|
+
kind: "Invoke",
|
|
561
|
+
handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Ok" } },
|
|
562
|
+
};
|
|
563
|
+
const TAG_ERR: Action = {
|
|
564
|
+
kind: "Invoke",
|
|
565
|
+
handler: { kind: "Builtin", builtin: { kind: "Tag", value: "Err" } },
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
/** Wrap branch cases with ExtractField("value") auto-unwrapping. */
|
|
569
|
+
function resultBranch(okCaseBody: Action, errCaseBody: Action): Action {
|
|
570
|
+
return {
|
|
571
|
+
kind: "Branch",
|
|
572
|
+
cases: {
|
|
573
|
+
Ok: { kind: "Chain", first: EXTRACT_VALUE, rest: okCaseBody },
|
|
574
|
+
Err: { kind: "Chain", first: EXTRACT_VALUE, rest: errCaseBody },
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Result namespace. All combinators produce TypedAction AST nodes that
|
|
581
|
+
* desugar to branch + existing builtins.
|
|
582
|
+
*/
|
|
583
|
+
export const Result = {
|
|
584
|
+
/**
|
|
585
|
+
* Wrap a value as Ok. `TValue → Result<TValue, TError>`
|
|
586
|
+
*/
|
|
587
|
+
ok<TValue, TError>(): TypedAction<TValue, ResultT<TValue, TError>> {
|
|
588
|
+
return typedAction(TAG_OK);
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Wrap a value as Err. `TError → Result<TValue, TError>`
|
|
593
|
+
*/
|
|
594
|
+
err<TValue, TError>(): TypedAction<TError, ResultT<TValue, TError>> {
|
|
595
|
+
return typedAction(TAG_ERR);
|
|
596
|
+
},
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Transform the Ok value. `Result<TValue, TError> → Result<TOut, TError>`
|
|
600
|
+
*
|
|
601
|
+
* Desugars to: `branch({ Ok: pipe(action, tag("Ok")), Err: tag("Err") })`
|
|
602
|
+
*/
|
|
603
|
+
map<TValue, TOut, TError>(
|
|
604
|
+
action: Pipeable<TValue, TOut>,
|
|
605
|
+
): TypedAction<ResultT<TValue, TError>, ResultT<TOut, TError>> {
|
|
606
|
+
return typedAction(
|
|
607
|
+
resultBranch(
|
|
608
|
+
{ kind: "Chain", first: action as Action, rest: TAG_OK },
|
|
609
|
+
TAG_ERR,
|
|
610
|
+
),
|
|
611
|
+
);
|
|
612
|
+
},
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Transform the Err value. `Result<TValue, TError> → Result<TValue, TErrorOut>`
|
|
616
|
+
*
|
|
617
|
+
* Desugars to: `branch({ Ok: tag("Ok"), Err: pipe(action, tag("Err")) })`
|
|
618
|
+
*/
|
|
619
|
+
mapErr<TValue, TError, TErrorOut>(
|
|
620
|
+
action: Pipeable<TError, TErrorOut>,
|
|
621
|
+
): TypedAction<ResultT<TValue, TError>, ResultT<TValue, TErrorOut>> {
|
|
622
|
+
return typedAction(
|
|
623
|
+
resultBranch(TAG_OK, {
|
|
624
|
+
kind: "Chain",
|
|
625
|
+
first: action as Action,
|
|
626
|
+
rest: TAG_ERR,
|
|
627
|
+
}),
|
|
628
|
+
);
|
|
629
|
+
},
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Monadic bind (flatMap) for Ok. If Ok, pass value to action which
|
|
633
|
+
* returns Result<TOut, TError>. If Err, propagate.
|
|
634
|
+
*
|
|
635
|
+
* Desugars to: `branch({ Ok: action, Err: tag("Err") })`
|
|
636
|
+
*/
|
|
637
|
+
andThen<TValue, TOut, TError>(
|
|
638
|
+
action: Pipeable<TValue, ResultT<TOut, TError>>,
|
|
639
|
+
): TypedAction<ResultT<TValue, TError>, ResultT<TOut, TError>> {
|
|
640
|
+
return typedAction(resultBranch(action as Action, TAG_ERR));
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Fallback on Err. If Ok, keep it. If Err, pass error to fallback
|
|
645
|
+
* which returns a new Result.
|
|
646
|
+
*
|
|
647
|
+
* Desugars to: `branch({ Ok: tag("Ok"), Err: fallback })`
|
|
648
|
+
*/
|
|
649
|
+
or<TValue, TError, TErrorOut>(
|
|
650
|
+
fallback: Pipeable<TError, ResultT<TValue, TErrorOut>>,
|
|
651
|
+
): TypedAction<ResultT<TValue, TError>, ResultT<TValue, TErrorOut>> {
|
|
652
|
+
return typedAction(resultBranch(TAG_OK, fallback as Action));
|
|
653
|
+
},
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Replace Ok value with another Result. If Ok, discard value and
|
|
657
|
+
* return other. If Err, propagate.
|
|
658
|
+
*
|
|
659
|
+
* Desugars to: `branch({ Ok: pipe(drop(), other), Err: tag("Err") })`
|
|
660
|
+
*/
|
|
661
|
+
and<TValue, TOut, TError>(
|
|
662
|
+
other: Pipeable<never, ResultT<TOut, TError>>,
|
|
663
|
+
): TypedAction<ResultT<TValue, TError>, ResultT<TOut, TError>> {
|
|
664
|
+
return typedAction(
|
|
665
|
+
resultBranch(
|
|
666
|
+
{ kind: "Chain", first: DROP, rest: other as Action },
|
|
667
|
+
TAG_ERR,
|
|
668
|
+
),
|
|
669
|
+
);
|
|
670
|
+
},
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Extract Ok or compute default from Err. `Result<TValue, TError> → TValue`
|
|
674
|
+
*
|
|
675
|
+
* Takes an action that receives the Err payload and produces a fallback.
|
|
676
|
+
* Uses covariant output checking so throw tokens (Out=never) are assignable
|
|
677
|
+
* when TValue is provided explicitly: `Result.unwrapOr<string, string>(throwError)`.
|
|
678
|
+
*
|
|
679
|
+
* For inference-free usage with throw tokens, prefer the postfix method:
|
|
680
|
+
* `handler.unwrapOr(throwError)` — the `this` constraint provides TValue.
|
|
681
|
+
*
|
|
682
|
+
* Desugars to: `branch({ Ok: identity(), Err: defaultAction })`
|
|
683
|
+
*/
|
|
684
|
+
unwrapOr<TValue, TError>(
|
|
685
|
+
defaultAction: Action & {
|
|
686
|
+
__in?: (input: TError) => void;
|
|
687
|
+
__out?: () => TValue;
|
|
688
|
+
},
|
|
689
|
+
): TypedAction<ResultT<TValue, TError>, TValue> {
|
|
690
|
+
return typedAction(resultBranch(IDENTITY, defaultAction as Action));
|
|
691
|
+
},
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Unwrap nested Result. `Result<Result<TValue, TError>, TError> → Result<TValue, TError>`
|
|
695
|
+
*
|
|
696
|
+
* Desugars to: `branch({ Ok: identity(), Err: tag("Err") })`
|
|
697
|
+
*/
|
|
698
|
+
flatten<TValue, TError>(): TypedAction<
|
|
699
|
+
ResultT<ResultT<TValue, TError>, TError>,
|
|
700
|
+
ResultT<TValue, TError>
|
|
701
|
+
> {
|
|
702
|
+
return typedAction(resultBranch(IDENTITY, TAG_ERR));
|
|
703
|
+
},
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Convert Ok to Some, Err to None. `Result<TValue, TError> → Option<TValue>`
|
|
707
|
+
*
|
|
708
|
+
* Desugars to: `branch({ Ok: tag("Some"), Err: pipe(drop(), tag("None")) })`
|
|
709
|
+
*/
|
|
710
|
+
toOption<TValue, TError>(): TypedAction<
|
|
711
|
+
ResultT<TValue, TError>,
|
|
712
|
+
OptionT<TValue>
|
|
713
|
+
> {
|
|
714
|
+
return typedAction(
|
|
715
|
+
resultBranch(TAG_SOME, { kind: "Chain", first: DROP, rest: TAG_NONE }),
|
|
716
|
+
);
|
|
717
|
+
},
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Convert Err to Some, Ok to None. `Result<TValue, TError> → Option<TError>`
|
|
721
|
+
*
|
|
722
|
+
* Desugars to: `branch({ Ok: pipe(drop(), tag("None")), Err: tag("Some") })`
|
|
723
|
+
*/
|
|
724
|
+
toOptionErr<TValue, TError>(): TypedAction<
|
|
725
|
+
ResultT<TValue, TError>,
|
|
726
|
+
OptionT<TError>
|
|
727
|
+
> {
|
|
728
|
+
return typedAction(
|
|
729
|
+
resultBranch({ kind: "Chain", first: DROP, rest: TAG_NONE }, TAG_SOME),
|
|
730
|
+
);
|
|
731
|
+
},
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Swap Result/Option nesting.
|
|
735
|
+
* `Result<Option<TValue>, TError> → Option<Result<TValue, TError>>`
|
|
736
|
+
*/
|
|
737
|
+
transpose<TValue, TError>(): TypedAction<
|
|
738
|
+
ResultT<OptionT<TValue>, TError>,
|
|
739
|
+
OptionT<ResultT<TValue, TError>>
|
|
740
|
+
> {
|
|
741
|
+
return typedAction(
|
|
742
|
+
resultBranch(
|
|
743
|
+
// Ok case: receives Option<TValue>, branch on Some/None
|
|
744
|
+
{
|
|
745
|
+
kind: "Branch",
|
|
746
|
+
cases: {
|
|
747
|
+
Some: {
|
|
748
|
+
kind: "Chain",
|
|
749
|
+
first: EXTRACT_VALUE,
|
|
750
|
+
rest: { kind: "Chain", first: TAG_OK, rest: TAG_SOME },
|
|
751
|
+
},
|
|
752
|
+
None: {
|
|
753
|
+
kind: "Chain",
|
|
754
|
+
first: EXTRACT_VALUE,
|
|
755
|
+
rest: { kind: "Chain", first: DROP, rest: TAG_NONE },
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
// Err case: receives TError, wrap as Result.err then Option.some
|
|
760
|
+
{ kind: "Chain", first: TAG_ERR, rest: TAG_SOME },
|
|
761
|
+
),
|
|
762
|
+
);
|
|
763
|
+
},
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Test if the value is Ok. `Result<TValue, TError> → boolean`
|
|
767
|
+
*/
|
|
768
|
+
isOk<TValue, TError>(): TypedAction<ResultT<TValue, TError>, boolean> {
|
|
769
|
+
const constTrue: Action = {
|
|
770
|
+
kind: "Invoke",
|
|
771
|
+
handler: { kind: "Builtin", builtin: { kind: "Constant", value: true } },
|
|
772
|
+
};
|
|
773
|
+
const constFalse: Action = {
|
|
774
|
+
kind: "Invoke",
|
|
775
|
+
handler: { kind: "Builtin", builtin: { kind: "Constant", value: false } },
|
|
776
|
+
};
|
|
777
|
+
return typedAction(
|
|
778
|
+
resultBranch(
|
|
779
|
+
{ kind: "Chain", first: DROP, rest: constTrue },
|
|
780
|
+
{ kind: "Chain", first: DROP, rest: constFalse },
|
|
781
|
+
),
|
|
782
|
+
);
|
|
783
|
+
},
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Test if the value is Err. `Result<TValue, TError> → boolean`
|
|
787
|
+
*/
|
|
788
|
+
isErr<TValue, TError>(): TypedAction<ResultT<TValue, TError>, boolean> {
|
|
789
|
+
const constTrue: Action = {
|
|
790
|
+
kind: "Invoke",
|
|
791
|
+
handler: { kind: "Builtin", builtin: { kind: "Constant", value: true } },
|
|
792
|
+
};
|
|
793
|
+
const constFalse: Action = {
|
|
794
|
+
kind: "Invoke",
|
|
795
|
+
handler: { kind: "Builtin", builtin: { kind: "Constant", value: false } },
|
|
796
|
+
};
|
|
797
|
+
return typedAction(
|
|
798
|
+
resultBranch(
|
|
799
|
+
{ kind: "Chain", first: DROP, rest: constFalse },
|
|
800
|
+
{ kind: "Chain", first: DROP, rest: constTrue },
|
|
801
|
+
),
|
|
802
|
+
);
|
|
803
|
+
},
|
|
804
|
+
} as const;
|