@barnum/barnum 0.3.0 → 0.4.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 (109) hide show
  1. package/artifacts/linux-arm64/barnum +0 -0
  2. package/artifacts/linux-x64/barnum +0 -0
  3. package/artifacts/macos-arm64/barnum +0 -0
  4. package/artifacts/macos-x64/barnum +0 -0
  5. package/artifacts/win-x64/barnum.exe +0 -0
  6. package/dist/all.d.ts +41 -10
  7. package/dist/all.d.ts.map +1 -0
  8. package/dist/all.js +1 -1
  9. package/dist/ast.d.ts +199 -98
  10. package/dist/ast.d.ts.map +1 -0
  11. package/dist/ast.js +271 -233
  12. package/dist/bind.d.ts +9 -12
  13. package/dist/bind.d.ts.map +1 -0
  14. package/dist/bind.js +14 -51
  15. package/dist/builtins/array.d.ts +36 -0
  16. package/dist/builtins/array.d.ts.map +1 -0
  17. package/dist/builtins/array.js +93 -0
  18. package/dist/builtins/index.d.ts +6 -0
  19. package/dist/builtins/index.d.ts.map +1 -0
  20. package/dist/builtins/index.js +5 -0
  21. package/dist/builtins/scalar.d.ts +12 -0
  22. package/dist/builtins/scalar.d.ts.map +1 -0
  23. package/dist/builtins/scalar.js +41 -0
  24. package/dist/builtins/struct.d.ts +25 -0
  25. package/dist/builtins/struct.d.ts.map +1 -0
  26. package/dist/builtins/struct.js +67 -0
  27. package/dist/builtins/tagged-union.d.ts +54 -0
  28. package/dist/builtins/tagged-union.d.ts.map +1 -0
  29. package/dist/builtins/tagged-union.js +81 -0
  30. package/dist/builtins/with-resource.d.ts +23 -0
  31. package/dist/builtins/with-resource.d.ts.map +1 -0
  32. package/dist/builtins/with-resource.js +35 -0
  33. package/dist/chain.d.ts +1 -0
  34. package/dist/chain.d.ts.map +1 -0
  35. package/dist/chain.js +3 -3
  36. package/dist/effect-id.d.ts +1 -0
  37. package/dist/effect-id.d.ts.map +1 -0
  38. package/dist/handler.d.ts +7 -6
  39. package/dist/handler.d.ts.map +1 -0
  40. package/dist/handler.js +5 -21
  41. package/dist/index.d.ts +10 -6
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +4 -2
  44. package/dist/iterator.d.ts +32 -0
  45. package/dist/iterator.d.ts.map +1 -0
  46. package/dist/iterator.js +123 -0
  47. package/dist/option.d.ts +74 -0
  48. package/dist/option.d.ts.map +1 -0
  49. package/dist/option.js +141 -0
  50. package/dist/pipe.d.ts +11 -10
  51. package/dist/pipe.d.ts.map +1 -0
  52. package/dist/pipe.js +5 -4
  53. package/dist/race.d.ts +5 -4
  54. package/dist/race.d.ts.map +1 -0
  55. package/dist/race.js +17 -42
  56. package/dist/recursive.d.ts +9 -3
  57. package/dist/recursive.d.ts.map +1 -0
  58. package/dist/recursive.js +18 -13
  59. package/dist/result.d.ts +50 -0
  60. package/dist/result.d.ts.map +1 -0
  61. package/dist/result.js +117 -0
  62. package/dist/run.d.ts +9 -2
  63. package/dist/run.d.ts.map +1 -0
  64. package/dist/run.js +37 -20
  65. package/dist/runtime.d.ts +6 -0
  66. package/dist/runtime.d.ts.map +1 -0
  67. package/dist/runtime.js +7 -0
  68. package/dist/schema.d.ts +1 -0
  69. package/dist/schema.d.ts.map +1 -0
  70. package/dist/schemas.d.ts +5 -0
  71. package/dist/schemas.d.ts.map +1 -0
  72. package/dist/schemas.js +13 -0
  73. package/dist/try-catch.d.ts +2 -1
  74. package/dist/try-catch.d.ts.map +1 -0
  75. package/dist/try-catch.js +10 -9
  76. package/dist/values.d.ts +6 -0
  77. package/dist/values.d.ts.map +1 -0
  78. package/dist/values.js +12 -0
  79. package/dist/worker.d.ts +5 -1
  80. package/dist/worker.d.ts.map +1 -0
  81. package/dist/worker.js +15 -3
  82. package/package.json +8 -6
  83. package/src/all.ts +118 -74
  84. package/src/ast.ts +773 -350
  85. package/src/bind.ts +32 -62
  86. package/src/builtins/array.ts +121 -0
  87. package/src/builtins/index.ts +17 -0
  88. package/src/builtins/scalar.ts +49 -0
  89. package/src/builtins/struct.ts +111 -0
  90. package/src/builtins/tagged-union.ts +142 -0
  91. package/src/builtins/with-resource.ts +69 -0
  92. package/src/chain.ts +4 -4
  93. package/src/handler.ts +12 -28
  94. package/src/index.ts +24 -17
  95. package/src/iterator.ts +243 -0
  96. package/src/option.ts +199 -0
  97. package/src/pipe.ts +123 -78
  98. package/src/race.ts +41 -51
  99. package/src/recursive.ts +44 -27
  100. package/src/result.ts +168 -0
  101. package/src/run.ts +53 -25
  102. package/src/runtime.ts +16 -0
  103. package/src/schemas.ts +21 -0
  104. package/src/try-catch.ts +14 -10
  105. package/src/values.ts +21 -0
  106. package/src/worker.ts +17 -2
  107. package/dist/builtins.d.ts +0 -257
  108. package/dist/builtins.js +0 -600
  109. package/src/builtins.ts +0 -804
package/src/bind.ts CHANGED
@@ -3,9 +3,12 @@ import {
3
3
  type ExtractInput,
4
4
  type ExtractOutput,
5
5
  type TypedAction,
6
+ toAction,
6
7
  typedAction,
7
8
  } from "./ast.js";
8
- import { identity, drop } from "./builtins.js";
9
+ import { chain } from "./chain.js";
10
+ import { all } from "./all.js";
11
+ import { identity, drop, getIndex } from "./builtins/index.js";
9
12
  import { allocateResumeHandlerId, type ResumeHandlerId } from "./effect-id.js";
10
13
  import { pipe } from "./pipe.js";
11
14
 
@@ -17,10 +20,10 @@ import { pipe } from "./pipe.js";
17
20
  * A typed reference to a bound value. Output is `TValue`.
18
21
  *
19
22
  * Use `.then()` (not `pipe()`) when chaining a VarRef into a generic
20
- * action like `pick` or `extractField` — pipe overloads can't infer
23
+ * action like `pick` or `getField` — pipe overloads can't infer
21
24
  * the generic's type parameter from the VarRef's output.
22
25
  */
23
- export type VarRef<TValue> = TypedAction<never, TValue>;
26
+ export type VarRef<TValue> = TypedAction<any, TValue>;
24
27
 
25
28
  function createVarRef<TValue>(
26
29
  resumeHandlerId: ResumeHandlerId,
@@ -39,12 +42,9 @@ function createVarRef<TValue>(
39
42
  * Maps each binding's output type to a VarRef. TypeScript resolves
40
43
  * ExtractOutput from each binding expression.
41
44
  *
42
- * Constraint is `Action[]` (not `Pipeable<any, any>[]`) because
43
- * `TypedAction<never, X>` (e.g. from `constant()`) fails the invariant
44
- * `__in` check against `Pipeable<any, any>` on the 9-variant
45
- * Action union. Using raw `Action[]` avoids the phantom field
46
- * assignability issue while `ExtractOutput` still extracts the correct
47
- * output type from the phantom fields on the concrete types.
45
+ * Constraint is `Action[]` (not `Pipeable<any, any>[]`) so that
46
+ * `ExtractOutput` extracts the correct output type from the phantom
47
+ * fields on the concrete types without fighting invariant `__in` checks.
48
48
  */
49
49
  export type InferVarRefs<TBindings extends Action[]> = {
50
50
  [K in keyof TBindings]: VarRef<ExtractOutput<TBindings[K]>>;
@@ -61,38 +61,15 @@ export type InferVarRefs<TBindings extends Action[]> = {
61
61
  * `state` (index 1) is the full All output tuple. The handler produces
62
62
  * `[state[n], state]` — value is state[n], new_state is state (unchanged).
63
63
  *
64
- * Expanded AST: All(Chain(ExtractIndex(1), ExtractIndex(n)), ExtractIndex(1))
64
+ * Expanded AST: All(Chain(GetIndex(1).unwrap(), GetIndex(n).unwrap()), GetIndex(1).unwrap())
65
65
  */
66
66
  function readVar(n: number): Action {
67
- return {
68
- kind: "All",
69
- actions: [
70
- {
71
- kind: "Chain",
72
- first: {
73
- kind: "Invoke",
74
- handler: {
75
- kind: "Builtin",
76
- builtin: { kind: "ExtractIndex", value: 1 },
77
- },
78
- },
79
- rest: {
80
- kind: "Invoke",
81
- handler: {
82
- kind: "Builtin",
83
- builtin: { kind: "ExtractIndex", value: n },
84
- },
85
- },
86
- },
87
- {
88
- kind: "Invoke",
89
- handler: {
90
- kind: "Builtin",
91
- builtin: { kind: "ExtractIndex", value: 1 },
92
- },
93
- },
94
- ],
95
- };
67
+ return toAction(
68
+ all(
69
+ chain(toAction(getIndex(1).unwrap()), toAction(getIndex(n).unwrap())),
70
+ toAction(getIndex(1).unwrap()),
71
+ ),
72
+ );
96
73
  }
97
74
 
98
75
  // ---------------------------------------------------------------------------
@@ -111,19 +88,18 @@ function readVar(n: number): Action {
111
88
  * All(...bindings, Identity),
112
89
  * ResumeHandle(r0, readVar(0),
113
90
  * ResumeHandle(r1, readVar(1),
114
- * Chain(ExtractIndex(N), body)
91
+ * Chain(GetIndex(N), body)
115
92
  * )
116
93
  * )
117
94
  * )
118
95
  */
119
96
  /**
120
97
  * Constraint for the body callback return type. Only requires the output
121
- * phantom fields — omits `__in` and `__in_co` so that body actions with
122
- * `In = never` (e.g. pipelines starting from a VarRef) are assignable.
98
+ * phantom field — omits `__in` and `__in_co` so that body actions with
99
+ * any input type (e.g. pipelines starting from a VarRef) are assignable.
123
100
  */
124
101
  type BodyResult<TOut> = Action & {
125
102
  __out?: () => TOut;
126
- __out_contra?: (output: TOut) => void;
127
103
  };
128
104
 
129
105
  export function bind<TBindings extends Action[], TOut>(
@@ -137,22 +113,17 @@ export function bind<TBindings extends Action[], TOut>(
137
113
  const varRefs = resumeHandlerIds.map((id) => createVarRef(id));
138
114
 
139
115
  // 3. Invoke the body callback with the VarRefs.
140
- const bodyAction = body(varRefs as InferVarRefs<TBindings>) as Action;
116
+ const bodyAction = toAction(body(varRefs as InferVarRefs<TBindings>));
141
117
 
142
118
  // 4. Build nested Handles from inside out.
143
119
  // Innermost: extract pipeline_input (last All element) → user body
144
120
  const pipelineInputIndex = bindings.length;
145
- let inner: Action = {
146
- kind: "Chain",
147
- first: {
148
- kind: "Invoke",
149
- handler: {
150
- kind: "Builtin",
151
- builtin: { kind: "ExtractIndex", value: pipelineInputIndex },
152
- },
153
- },
154
- rest: bodyAction,
155
- };
121
+ let inner: Action = toAction(
122
+ chain(
123
+ toAction(getIndex(pipelineInputIndex).unwrap()),
124
+ toAction(bodyAction),
125
+ ),
126
+ );
156
127
  for (let i = resumeHandlerIds.length - 1; i >= 0; i--) {
157
128
  inner = {
158
129
  kind: "ResumeHandle",
@@ -163,12 +134,11 @@ export function bind<TBindings extends Action[], TOut>(
163
134
  }
164
135
 
165
136
  // 5. All(...bindings, identity()) → nested Handles
166
- const allActions = [...bindings.map((b) => b as Action), identity as Action];
167
- return typedAction({
168
- kind: "Chain",
169
- first: { kind: "All", actions: allActions },
170
- rest: inner,
171
- });
137
+ const allAction: Action = {
138
+ kind: "All",
139
+ actions: [...bindings.map((b) => toAction(b)), toAction(identity())],
140
+ };
141
+ return typedAction(toAction(chain(toAction(allAction), toAction(inner))));
172
142
  }
173
143
 
174
144
  // ---------------------------------------------------------------------------
@@ -188,5 +158,5 @@ export function bind<TBindings extends Action[], TOut>(
188
158
  export function bindInput<TIn, TOut = any>(
189
159
  body: (input: VarRef<TIn>) => BodyResult<TOut>,
190
160
  ): TypedAction<TIn, TOut> {
191
- return bind([identity], ([input]) => pipe(drop, body(input)));
161
+ return bind([identity()], ([input]) => pipe(drop, body(input)));
192
162
  }
@@ -0,0 +1,121 @@
1
+ import {
2
+ type Option as OptionT,
3
+ type TypedAction,
4
+ typedAction,
5
+ } from "../ast.js";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // GetIndex — extract a single element from an array by index
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export function getIndex<TTuple extends unknown[], TIndex extends number>(
12
+ index: TIndex,
13
+ ): TypedAction<TTuple, OptionT<TTuple[TIndex]>> {
14
+ return typedAction({
15
+ kind: "Invoke",
16
+ handler: {
17
+ kind: "Builtin",
18
+ builtin: { kind: "GetIndex", index },
19
+ },
20
+ });
21
+ }
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Flatten — flatten a nested array one level
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export function flatten<TElement>(): TypedAction<TElement[][], TElement[]> {
28
+ return typedAction({
29
+ kind: "Invoke",
30
+ handler: { kind: "Builtin", builtin: { kind: "Flatten" } },
31
+ });
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // SplitFirst — head/tail decomposition of an array
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /**
39
+ * Deconstruct an array into its first element and the remaining elements.
40
+ * `TElement[] → Option<[TElement, TElement[]]>`
41
+ *
42
+ * Returns `Some([first, rest])` for non-empty arrays, `None` for empty arrays.
43
+ * This is the array equivalent of cons/uncons — enables recursive iteration
44
+ * patterns via `loop` + `splitFirst` + `branch`.
45
+ *
46
+ * This is a builtin (SplitFirst) because it requires array-length branching
47
+ * that can't be composed from existing AST nodes.
48
+ */
49
+ export function splitFirst<TElement>(): TypedAction<
50
+ TElement[],
51
+ OptionT<[TElement, TElement[]]>
52
+ > {
53
+ return typedAction({
54
+ kind: "Invoke",
55
+ handler: { kind: "Builtin", builtin: { kind: "SplitFirst" } },
56
+ });
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // SplitLast — init/last decomposition of an array
61
+ // ---------------------------------------------------------------------------
62
+
63
+ /**
64
+ * Deconstruct an array into the leading elements and the last element.
65
+ * `TElement[] → Option<[TElement[], TElement]>`
66
+ *
67
+ * Returns `Some([init, last])` for non-empty arrays, `None` for empty arrays.
68
+ * Mirror of `splitFirst` — enables processing from the tail end.
69
+ *
70
+ * This is a builtin (SplitLast) because it requires array-length branching
71
+ * that can't be composed from existing AST nodes.
72
+ */
73
+ export function splitLast<TElement>(): TypedAction<
74
+ TElement[],
75
+ OptionT<[TElement[], TElement]>
76
+ > {
77
+ return typedAction({
78
+ kind: "Invoke",
79
+ handler: { kind: "Builtin", builtin: { kind: "SplitLast" } },
80
+ });
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Slice — extract a sub-array from start to end
85
+ // ---------------------------------------------------------------------------
86
+
87
+ /**
88
+ * Slice an array from `start` (inclusive) to `end` (exclusive).
89
+ * `T[] → T[]`
90
+ *
91
+ * Both indices are clamped to array length. If `end` is omitted, slices
92
+ * to the end of the array. Returns empty array if `start >= end`.
93
+ */
94
+ export function slice<TElement>(
95
+ start: number,
96
+ end?: number,
97
+ ): TypedAction<TElement[], TElement[]> {
98
+ const builtin: { kind: "Slice"; start: number; end?: number } =
99
+ end === undefined
100
+ ? { kind: "Slice", start }
101
+ : { kind: "Slice", start, end };
102
+ return typedAction({
103
+ kind: "Invoke",
104
+ handler: { kind: "Builtin", builtin },
105
+ });
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Range — produce an integer array [start, start+1, ..., end-1]
110
+ // ---------------------------------------------------------------------------
111
+
112
+ export function range(start: number, end: number): TypedAction<any, number[]> {
113
+ const result: number[] = [];
114
+ for (let i = start; i < end; i++) {
115
+ result.push(i);
116
+ }
117
+ return typedAction({
118
+ kind: "Invoke",
119
+ handler: { kind: "Builtin", builtin: { kind: "Constant", value: result } },
120
+ });
121
+ }
@@ -0,0 +1,17 @@
1
+ export { constant, identity, drop, panic } from "./scalar.js";
2
+ export { getField, wrapInField, merge, pick, allObject } from "./struct.js";
3
+ export {
4
+ getIndex,
5
+ flatten,
6
+ splitFirst,
7
+ splitLast,
8
+ slice,
9
+ range,
10
+ } from "./array.js";
11
+ export {
12
+ tag,
13
+ extractPrefix,
14
+ asOption,
15
+ taggedUnionSchema,
16
+ } from "./tagged-union.js";
17
+ export { withResource } from "./with-resource.js";
@@ -0,0 +1,49 @@
1
+ import { type TypedAction, typedAction } from "../ast.js";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Constant — produce a fixed value (takes no pipeline input)
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export function constant<TValue>(value: TValue): TypedAction<any, TValue> {
8
+ return typedAction({
9
+ kind: "Invoke",
10
+ handler: { kind: "Builtin", builtin: { kind: "Constant", value } },
11
+ });
12
+ }
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Identity — pass input through unchanged
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export function identity<TValue = any>(): TypedAction<TValue, TValue> {
19
+ return typedAction({
20
+ kind: "Invoke",
21
+ handler: { kind: "Builtin", builtin: { kind: "Identity" } },
22
+ });
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Drop — discard pipeline value
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export const drop: TypedAction<any, void> = typedAction({
30
+ kind: "Invoke",
31
+ handler: { kind: "Builtin", builtin: { kind: "Drop" } },
32
+ });
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Panic — halt execution with an error message
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /**
39
+ * Halt execution with a fatal error. Not caught by tryCatch.
40
+ * Analogous to Rust's `panic!`.
41
+ *
42
+ * Output type is `never` — a panic never produces a value.
43
+ */
44
+ export function panic(message: string): TypedAction<any, never> {
45
+ return typedAction({
46
+ kind: "Invoke",
47
+ handler: { kind: "Builtin", builtin: { kind: "Panic", message } },
48
+ });
49
+ }
@@ -0,0 +1,111 @@
1
+ import {
2
+ type Action,
3
+ type ExtractOutput,
4
+ type MergeTuple,
5
+ type Pipeable,
6
+ type TypedAction,
7
+ toAction,
8
+ typedAction,
9
+ } from "../ast.js";
10
+ import { chain } from "../chain.js";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // GetField — extract a single field from an object
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export function getField<
17
+ TObj extends Record<string, unknown>,
18
+ TField extends keyof TObj & string,
19
+ >(field: TField): TypedAction<TObj, TObj[TField]> {
20
+ return typedAction({
21
+ kind: "Invoke",
22
+ handler: {
23
+ kind: "Builtin",
24
+ builtin: { kind: "GetField", field },
25
+ },
26
+ });
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // WrapInField — wrap input as { <field>: <input> }
31
+ // ---------------------------------------------------------------------------
32
+
33
+ export function wrapInField<TField extends string, TValue>(
34
+ field: TField,
35
+ ): TypedAction<TValue, Record<TField, TValue>> {
36
+ return typedAction({
37
+ kind: "Invoke",
38
+ handler: {
39
+ kind: "Builtin",
40
+ builtin: { kind: "WrapInField", field },
41
+ },
42
+ });
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Merge — merge a tuple of objects into a single object
47
+ // ---------------------------------------------------------------------------
48
+
49
+ export function merge<TTuple extends Record<string, unknown>[]>(): TypedAction<
50
+ TTuple,
51
+ MergeTuple<TTuple>
52
+ > {
53
+ return typedAction({
54
+ kind: "Invoke",
55
+ handler: { kind: "Builtin", builtin: { kind: "Merge" } },
56
+ });
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Pick — select named fields from an object
61
+ // ---------------------------------------------------------------------------
62
+
63
+ export function pick<
64
+ TObj extends Record<string, unknown>,
65
+ TKeys extends (keyof TObj & string)[],
66
+ >(...keys: TKeys): TypedAction<TObj, Pick<TObj, TKeys[number]>> {
67
+ const actions = keys.map((key) =>
68
+ toAction(chain(toAction(getField(key)), toAction(wrapInField(key)))),
69
+ );
70
+ const allAction: Action = { kind: "All", actions };
71
+ return chain(toAction(allAction), toAction(merge())) as TypedAction<
72
+ TObj,
73
+ Pick<TObj, TKeys[number]>
74
+ >;
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // AllObject — run named actions concurrently, collect into an object
79
+ // ---------------------------------------------------------------------------
80
+
81
+ /**
82
+ * Run named actions concurrently on the same input, collecting results
83
+ * into an object with matching keys.
84
+ *
85
+ * ```ts
86
+ * allObject({
87
+ * files: listFiles,
88
+ * config: loadConfig,
89
+ * })
90
+ * // TIn → { files: string[], config: Config }
91
+ * ```
92
+ *
93
+ * Each action receives the pipeline input. Results are wrapped in
94
+ * `{ key: value }` via `wrapInField`, run concurrently via `All`,
95
+ * then merged into a single object.
96
+ */
97
+ export function allObject<TActions extends Record<string, Pipeable<any, any>>>(
98
+ actions: TActions,
99
+ ): TypedAction<
100
+ any,
101
+ { [K in keyof TActions & string]: ExtractOutput<TActions[K]> }
102
+ > {
103
+ const wrapped = Object.entries(actions).map(([key, action]) =>
104
+ toAction(chain(action, wrapInField(key))),
105
+ );
106
+ const allAction: Action = { kind: "All", actions: wrapped };
107
+ return chain(toAction(allAction), toAction(merge())) as TypedAction<
108
+ any,
109
+ { [K in keyof TActions & string]: ExtractOutput<TActions[K]> }
110
+ >;
111
+ }
@@ -0,0 +1,142 @@
1
+ import {
2
+ type Option,
3
+ type TaggedUnion,
4
+ type TypedAction,
5
+ toAction,
6
+ typedAction,
7
+ } from "../ast.js";
8
+ import { chain } from "../chain.js";
9
+ import { all } from "../all.js";
10
+ import { constant } from "./scalar.js";
11
+ import { wrapInField, merge } from "./struct.js";
12
+ import { z } from "zod";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Tag — wrap input as a tagged union variant
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /**
19
+ * Wrap input as a tagged union member. Requires the full variant map TDef
20
+ * so the output type carries __def for branch decomposition.
21
+ *
22
+ * Usage: tag<{ Ok: string; Err: number }, "Ok">("Ok")
23
+ * input: string → output: TaggedUnion<{ Ok: string; Err: number }>
24
+ */
25
+ export function tag<
26
+ TEnumName extends string,
27
+ TDef extends Record<string, unknown>,
28
+ TKind extends keyof TDef & string,
29
+ >(
30
+ kind: TKind,
31
+ enumName: TEnumName,
32
+ ): TypedAction<TDef[TKind], TaggedUnion<TEnumName, TDef>> {
33
+ const namespacedKind = `${enumName}.${kind}`;
34
+ return chain(
35
+ toAction(
36
+ all(
37
+ chain(
38
+ toAction(constant(namespacedKind)),
39
+ toAction(wrapInField("kind")),
40
+ ),
41
+ wrapInField("value"),
42
+ ),
43
+ ),
44
+ toAction(merge()),
45
+ ) as TypedAction<TDef[TKind], TaggedUnion<TEnumName, TDef>>;
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // ExtractPrefix — extract enum prefix from tagged value kind
50
+ // ---------------------------------------------------------------------------
51
+
52
+ /**
53
+ * Extract the enum prefix from a tagged value's `kind` field.
54
+ *
55
+ * Input: `{ kind: "Result.Ok", value: 42 }`
56
+ * Output: `{ kind: "Result", value: { kind: "Result.Ok", value: 42 } }`
57
+ *
58
+ * If `kind` contains no `'.'`, the entire kind string becomes the prefix.
59
+ * Used internally by `branchFamily` for two-level dispatch.
60
+ */
61
+ export function extractPrefix(): TypedAction {
62
+ return typedAction({
63
+ kind: "Invoke",
64
+ handler: { kind: "Builtin", builtin: { kind: "ExtractPrefix" } },
65
+ });
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // AsOption — convert boolean to Option<void>
70
+ // ---------------------------------------------------------------------------
71
+
72
+ /**
73
+ * Convert a boolean to `Option<void>`.
74
+ *
75
+ * `true` → `{ kind: "Option.Some", value: null }`
76
+ * `false` → `{ kind: "Option.None", value: null }`
77
+ */
78
+ export function asOption(): TypedAction<boolean, Option<void>> {
79
+ return typedAction({
80
+ kind: "Invoke",
81
+ handler: { kind: "Builtin", builtin: { kind: "AsOption" } },
82
+ });
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // TaggedUnion Zod schema constructor
87
+ // ---------------------------------------------------------------------------
88
+
89
+ /**
90
+ * Reverse of VoidToNull: maps `null` back to `void` in the def so that
91
+ * `taggedUnionSchema({ Clean: z.null() })` produces the same phantom __def
92
+ * as `TaggedUnion<{ Clean: void }>`.
93
+ */
94
+ type NullToVoid<TDef> = {
95
+ [K in keyof TDef]: TDef[K] extends null ? void : TDef[K];
96
+ };
97
+
98
+ /**
99
+ * Build a Zod schema for a `TaggedUnion<TEnumName, TDef>` — a discriminated
100
+ * union of `{ kind: "EnumName.Variant"; value: V }` objects.
101
+ *
102
+ * Each key in `cases` becomes a variant with a namespaced kind string.
103
+ * Use `z.null()` for void variants.
104
+ *
105
+ * ```ts
106
+ * const schema = taggedUnionSchema("ClassifyResult", {
107
+ * HasErrors: z.array(TypeErrorValidator),
108
+ * Clean: z.null(),
109
+ * });
110
+ * ```
111
+ */
112
+ export function taggedUnionSchema<
113
+ TEnumName extends string,
114
+ TDef extends Record<string, z.ZodTypeAny>,
115
+ >(
116
+ enumName: TEnumName,
117
+ cases: TDef,
118
+ ): z.ZodType<
119
+ TaggedUnion<
120
+ TEnumName,
121
+ NullToVoid<{ [K in keyof TDef & string]: z.infer<TDef[K]> }>
122
+ >
123
+ > {
124
+ type Out = TaggedUnion<
125
+ TEnumName,
126
+ NullToVoid<{ [K in keyof TDef & string]: z.infer<TDef[K]> }>
127
+ >;
128
+ const variants = Object.entries(cases).map(([kind, valueSchema]) =>
129
+ z.object({ kind: z.literal(`${enumName}.${kind}`), value: valueSchema }),
130
+ );
131
+ if (variants.length === 0) {
132
+ return z.never() as z.ZodType<Out>;
133
+ }
134
+ if (variants.length === 1) {
135
+ return variants[0] as z.ZodType<Out>;
136
+ }
137
+ return z.discriminatedUnion("kind", [
138
+ variants[0],
139
+ variants[1],
140
+ ...variants.slice(2),
141
+ ]) as z.ZodType<Out>;
142
+ }
@@ -0,0 +1,69 @@
1
+ import { type Pipeable, type TypedAction, toAction } from "../ast.js";
2
+ import { chain } from "../chain.js";
3
+ import { all } from "../all.js";
4
+ import { identity } from "./scalar.js";
5
+ import { merge } from "./struct.js";
6
+ import { getIndex } from "./array.js";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // WithResource — RAII-style create/action/dispose
10
+ // ---------------------------------------------------------------------------
11
+
12
+ /**
13
+ * RAII-style resource management combinator.
14
+ *
15
+ * Runs `create` to acquire a resource, then merges the resource with the
16
+ * original input into a flat object (`TResource & TIn`) for the action.
17
+ * After the action completes, `dispose` receives the resource for cleanup.
18
+ * The overall combinator returns the action's output.
19
+ *
20
+ * ```
21
+ * TIn → create → TResource
22
+ * → merge(TResource, TIn) → TResource & TIn
23
+ * → action(TResource & TIn) → TOut
24
+ * → dispose(TResource) → (discarded)
25
+ * → TOut
26
+ * ```
27
+ */
28
+ export function withResource<
29
+ TIn extends Record<string, unknown>,
30
+ TResource extends Record<string, unknown>,
31
+ TOut,
32
+ TDisposeOut = unknown,
33
+ >({
34
+ create,
35
+ action,
36
+ dispose,
37
+ }: {
38
+ create: Pipeable<TIn, TResource>;
39
+ action: Pipeable<TResource & TIn, TOut>;
40
+ dispose: Pipeable<TResource, TDisposeOut>;
41
+ }): TypedAction<TIn, TOut> {
42
+ // Step 1: all(create, identity) → [TResource, TIn] → merge → TResource & TIn
43
+ const acquireAndMerge = chain(
44
+ toAction(all(create, identity())),
45
+ toAction(merge()),
46
+ );
47
+
48
+ // Step 2: all(action, identity) → [TOut, TResource & TIn]
49
+ const actionAndKeepMerged = all(toAction(action), toAction(identity()));
50
+
51
+ // Step 3: all(getIndex(0).unwrap(), chain(getIndex(1).unwrap(), dispose)) → [TOut, unknown]
52
+ const disposeAndKeepResult = all(
53
+ toAction(getIndex(0).unwrap()),
54
+ chain(toAction(getIndex(1).unwrap()), toAction(dispose)),
55
+ );
56
+
57
+ // Step 4: getIndex(0).unwrap() → TOut
58
+ return chain(
59
+ toAction(
60
+ chain(
61
+ toAction(
62
+ chain(toAction(acquireAndMerge), toAction(actionAndKeepMerged)),
63
+ ),
64
+ toAction(disposeAndKeepResult),
65
+ ),
66
+ ),
67
+ toAction(getIndex(0).unwrap()),
68
+ ) as TypedAction<TIn, TOut>;
69
+ }