@barnum/barnum 0.2.3 → 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 (115) 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/cli.cjs +33 -0
  7. package/dist/all.d.ts +43 -0
  8. package/dist/all.d.ts.map +1 -0
  9. package/dist/all.js +8 -0
  10. package/dist/ast.d.ts +476 -0
  11. package/dist/ast.d.ts.map +1 -0
  12. package/dist/ast.js +419 -0
  13. package/dist/bind.d.ts +59 -0
  14. package/dist/bind.d.ts.map +1 -0
  15. package/dist/bind.js +69 -0
  16. package/dist/builtins/array.d.ts +36 -0
  17. package/dist/builtins/array.d.ts.map +1 -0
  18. package/dist/builtins/array.js +93 -0
  19. package/dist/builtins/index.d.ts +6 -0
  20. package/dist/builtins/index.d.ts.map +1 -0
  21. package/dist/builtins/index.js +5 -0
  22. package/dist/builtins/scalar.d.ts +12 -0
  23. package/dist/builtins/scalar.d.ts.map +1 -0
  24. package/dist/builtins/scalar.js +41 -0
  25. package/dist/builtins/struct.d.ts +25 -0
  26. package/dist/builtins/struct.d.ts.map +1 -0
  27. package/dist/builtins/struct.js +67 -0
  28. package/dist/builtins/tagged-union.d.ts +54 -0
  29. package/dist/builtins/tagged-union.d.ts.map +1 -0
  30. package/dist/builtins/tagged-union.js +81 -0
  31. package/dist/builtins/with-resource.d.ts +23 -0
  32. package/dist/builtins/with-resource.d.ts.map +1 -0
  33. package/dist/builtins/with-resource.js +35 -0
  34. package/dist/chain.d.ts +3 -0
  35. package/dist/chain.d.ts.map +1 -0
  36. package/dist/chain.js +8 -0
  37. package/dist/effect-id.d.ts +15 -0
  38. package/dist/effect-id.d.ts.map +1 -0
  39. package/dist/effect-id.js +16 -0
  40. package/dist/handler.d.ts +51 -0
  41. package/dist/handler.d.ts.map +1 -0
  42. package/dist/handler.js +130 -0
  43. package/dist/index.d.ts +12 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +7 -0
  46. package/dist/iterator.d.ts +32 -0
  47. package/dist/iterator.d.ts.map +1 -0
  48. package/dist/iterator.js +123 -0
  49. package/dist/option.d.ts +74 -0
  50. package/dist/option.d.ts.map +1 -0
  51. package/dist/option.js +141 -0
  52. package/dist/pipe.d.ts +12 -0
  53. package/dist/pipe.d.ts.map +1 -0
  54. package/dist/pipe.js +12 -0
  55. package/dist/race.d.ts +54 -0
  56. package/dist/race.d.ts.map +1 -0
  57. package/dist/race.js +116 -0
  58. package/dist/recursive.d.ts +40 -0
  59. package/dist/recursive.d.ts.map +1 -0
  60. package/dist/recursive.js +58 -0
  61. package/dist/result.d.ts +50 -0
  62. package/dist/result.d.ts.map +1 -0
  63. package/dist/result.js +117 -0
  64. package/dist/run.d.ts +14 -0
  65. package/dist/run.d.ts.map +1 -0
  66. package/dist/run.js +160 -0
  67. package/dist/runtime.d.ts +6 -0
  68. package/dist/runtime.d.ts.map +1 -0
  69. package/dist/runtime.js +7 -0
  70. package/dist/schema.d.ts +9 -0
  71. package/dist/schema.d.ts.map +1 -0
  72. package/dist/schema.js +95 -0
  73. package/dist/schemas.d.ts +5 -0
  74. package/dist/schemas.d.ts.map +1 -0
  75. package/dist/schemas.js +13 -0
  76. package/dist/try-catch.d.ts +24 -0
  77. package/dist/try-catch.d.ts.map +1 -0
  78. package/dist/try-catch.js +37 -0
  79. package/dist/values.d.ts +6 -0
  80. package/dist/values.d.ts.map +1 -0
  81. package/dist/values.js +12 -0
  82. package/dist/worker.d.ts +15 -0
  83. package/dist/worker.d.ts.map +1 -0
  84. package/dist/worker.js +58 -0
  85. package/package.json +42 -16
  86. package/src/all.ts +133 -0
  87. package/src/ast.ts +1301 -0
  88. package/src/bind.ts +162 -0
  89. package/src/builtins/array.ts +121 -0
  90. package/src/builtins/index.ts +17 -0
  91. package/src/builtins/scalar.ts +49 -0
  92. package/src/builtins/struct.ts +111 -0
  93. package/src/builtins/tagged-union.ts +142 -0
  94. package/src/builtins/with-resource.ts +69 -0
  95. package/src/chain.ts +17 -0
  96. package/src/effect-id.ts +30 -0
  97. package/src/handler.ts +263 -0
  98. package/src/index.ts +37 -0
  99. package/src/iterator.ts +243 -0
  100. package/src/option.ts +199 -0
  101. package/src/pipe.ts +138 -0
  102. package/src/race.ts +173 -0
  103. package/src/recursive.ts +129 -0
  104. package/src/result.ts +168 -0
  105. package/src/run.ts +209 -0
  106. package/src/runtime.ts +16 -0
  107. package/src/schema.ts +118 -0
  108. package/src/schemas.ts +21 -0
  109. package/src/try-catch.ts +57 -0
  110. package/src/values.ts +21 -0
  111. package/src/worker.ts +71 -0
  112. package/README.md +0 -19
  113. package/barnum-config-schema.json +0 -408
  114. package/cli.js +0 -20
  115. package/index.js +0 -23
package/src/bind.ts ADDED
@@ -0,0 +1,162 @@
1
+ import {
2
+ type Action,
3
+ type ExtractInput,
4
+ type ExtractOutput,
5
+ type TypedAction,
6
+ toAction,
7
+ typedAction,
8
+ } from "./ast.js";
9
+ import { chain } from "./chain.js";
10
+ import { all } from "./all.js";
11
+ import { identity, drop, getIndex } from "./builtins/index.js";
12
+ import { allocateResumeHandlerId, type ResumeHandlerId } from "./effect-id.js";
13
+ import { pipe } from "./pipe.js";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // VarRef — typed reference to a bound value
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /**
20
+ * A typed reference to a bound value. Output is `TValue`.
21
+ *
22
+ * Use `.then()` (not `pipe()`) when chaining a VarRef into a generic
23
+ * action like `pick` or `getField` — pipe overloads can't infer
24
+ * the generic's type parameter from the VarRef's output.
25
+ */
26
+ export type VarRef<TValue> = TypedAction<any, TValue>;
27
+
28
+ function createVarRef<TValue>(
29
+ resumeHandlerId: ResumeHandlerId,
30
+ ): VarRef<TValue> {
31
+ return typedAction({
32
+ kind: "ResumePerform",
33
+ resume_handler_id: resumeHandlerId,
34
+ });
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // InferVarRefs — map bindings to VarRef types
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * Maps each binding's output type to a VarRef. TypeScript resolves
43
+ * ExtractOutput from each binding expression.
44
+ *
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
+ */
49
+ export type InferVarRefs<TBindings extends Action[]> = {
50
+ [K in keyof TBindings]: VarRef<ExtractOutput<TBindings[K]>>;
51
+ };
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // readVar — handler DAG for the nth binding
55
+ // ---------------------------------------------------------------------------
56
+
57
+ /**
58
+ * Returns an action that extracts the nth value from the ResumeHandle's
59
+ * state tuple and passes state through unchanged. When a ResumePerform
60
+ * fires, the engine calls the handler with `[payload, state]`. For bind,
61
+ * `state` (index 1) is the full All output tuple. The handler produces
62
+ * `[state[n], state]` — value is state[n], new_state is state (unchanged).
63
+ *
64
+ * Expanded AST: All(Chain(GetIndex(1).unwrap(), GetIndex(n).unwrap()), GetIndex(1).unwrap())
65
+ */
66
+ function readVar(n: number): Action {
67
+ return toAction(
68
+ all(
69
+ chain(toAction(getIndex(1).unwrap()), toAction(getIndex(n).unwrap())),
70
+ toAction(getIndex(1).unwrap()),
71
+ ),
72
+ );
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // bind — the user-facing function
77
+ // ---------------------------------------------------------------------------
78
+
79
+ /**
80
+ * Bind concurrent values as VarRefs available throughout the body.
81
+ *
82
+ * All bindings are actions (Pipeable) evaluated concurrently with the
83
+ * pipeline input. The body callback receives an array of VarRefs,
84
+ * one per binding.
85
+ *
86
+ * Compiles to:
87
+ * Chain(
88
+ * All(...bindings, Identity),
89
+ * ResumeHandle(r0, readVar(0),
90
+ * ResumeHandle(r1, readVar(1),
91
+ * Chain(GetIndex(N), body)
92
+ * )
93
+ * )
94
+ * )
95
+ */
96
+ /**
97
+ * Constraint for the body callback return type. Only requires the output
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.
100
+ */
101
+ type BodyResult<TOut> = Action & {
102
+ __out?: () => TOut;
103
+ };
104
+
105
+ export function bind<TBindings extends Action[], TOut>(
106
+ bindings: [...TBindings],
107
+ body: (vars: InferVarRefs<TBindings>) => BodyResult<TOut>,
108
+ ): TypedAction<ExtractInput<TBindings[number]>, TOut> {
109
+ // 1. Gensym one resumeHandlerId per binding.
110
+ const resumeHandlerIds = bindings.map(() => allocateResumeHandlerId());
111
+
112
+ // 2. Create VarRefs (ResumePerform nodes) for each binding.
113
+ const varRefs = resumeHandlerIds.map((id) => createVarRef(id));
114
+
115
+ // 3. Invoke the body callback with the VarRefs.
116
+ const bodyAction = toAction(body(varRefs as InferVarRefs<TBindings>));
117
+
118
+ // 4. Build nested Handles from inside out.
119
+ // Innermost: extract pipeline_input (last All element) → user body
120
+ const pipelineInputIndex = bindings.length;
121
+ let inner: Action = toAction(
122
+ chain(
123
+ toAction(getIndex(pipelineInputIndex).unwrap()),
124
+ toAction(bodyAction),
125
+ ),
126
+ );
127
+ for (let i = resumeHandlerIds.length - 1; i >= 0; i--) {
128
+ inner = {
129
+ kind: "ResumeHandle",
130
+ resume_handler_id: resumeHandlerIds[i],
131
+ handler: readVar(i),
132
+ body: inner,
133
+ };
134
+ }
135
+
136
+ // 5. All(...bindings, identity()) → nested Handles
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))));
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // bindInput — bind the pipeline input
146
+ // ---------------------------------------------------------------------------
147
+
148
+ /**
149
+ * Convenience wrapper for the common pattern of capturing the pipeline
150
+ * input as a VarRef. The body's pipeline input is `never` — the input
151
+ * is dropped, so the body must access it through the VarRef.
152
+ *
153
+ * Sugar for: `bind([identity()], ([input]) => pipe(drop, body(input)))`
154
+ *
155
+ * TOut defaults to `any` so callers can specify just TIn:
156
+ * bindInput<FileEntry>((entry) => ...)
157
+ */
158
+ export function bindInput<TIn, TOut = any>(
159
+ body: (input: VarRef<TIn>) => BodyResult<TOut>,
160
+ ): TypedAction<TIn, TOut> {
161
+ return bind([identity()], ([input]) => pipe(drop, body(input)));
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
+ }
package/src/chain.ts ADDED
@@ -0,0 +1,17 @@
1
+ import {
2
+ type Pipeable,
3
+ type TypedAction,
4
+ toAction,
5
+ typedAction,
6
+ } from "./ast.js";
7
+
8
+ export function chain<T1, T2, T3>(
9
+ first: Pipeable<T1, T2>,
10
+ rest: Pipeable<T2, T3>,
11
+ ): TypedAction<T1, T3> {
12
+ return typedAction<T1, T3>({
13
+ kind: "Chain",
14
+ first: toAction(first),
15
+ rest: toAction(rest),
16
+ });
17
+ }