@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/option.ts ADDED
@@ -0,0 +1,199 @@
1
+ import {
2
+ type Option as OptionT,
3
+ type OptionDef,
4
+ type Pipeable,
5
+ type Result as ResultT,
6
+ type TypedAction,
7
+ toAction,
8
+ typedAction,
9
+ branch,
10
+ } from "./ast.js";
11
+ import { chain } from "./chain.js";
12
+ import {
13
+ constant,
14
+ drop,
15
+ getIndex,
16
+ identity,
17
+ panic,
18
+ splitFirst,
19
+ splitLast,
20
+ tag,
21
+ } from "./builtins/index.js";
22
+ import { Result } from "./result.js";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Option namespace — combinators for Option<T> tagged unions
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /**
29
+ * Option namespace. All combinators produce TypedAction AST nodes that
30
+ * desugar to branch + existing builtins, except collect which uses the
31
+ * CollectSome builtin.
32
+ */
33
+ export const Option = {
34
+ /** Tag combinator: wrap value as `Option.Some`. `T → Option<T>` */
35
+ some<T>(): TypedAction<T, OptionT<T>> {
36
+ return tag<"Option", OptionDef<T>, "Some">("Some", "Option");
37
+ },
38
+ /** Tag combinator: wrap value as `Option.None`. `void → Option<T>` */
39
+ none<T>(): TypedAction<void, OptionT<T>> {
40
+ return tag<"Option", OptionDef<T>, "None">("None", "Option");
41
+ },
42
+
43
+ /** Transform the Some value. `Option<T> → Option<U>` */
44
+ map<T, U>(action: Pipeable<T, U>): TypedAction<OptionT<T>, OptionT<U>> {
45
+ return branch({
46
+ Some: chain(action, Option.some<U>()),
47
+ None: Option.none<U>(),
48
+ }) as TypedAction<OptionT<T>, OptionT<U>>;
49
+ },
50
+
51
+ /**
52
+ * Monadic bind (flatMap). If Some, pass the value to action which
53
+ * returns Option<U>. If None, stay None. `Option<T> → Option<U>`
54
+ */
55
+ andThen<T, U>(
56
+ action: Pipeable<T, OptionT<U>>,
57
+ ): TypedAction<OptionT<T>, OptionT<U>> {
58
+ return branch({
59
+ Some: action,
60
+ None: Option.none<U>(),
61
+ }) as TypedAction<OptionT<T>, OptionT<U>>;
62
+ },
63
+
64
+ /**
65
+ * Extract the Some value or panic. `Option<T> → T`
66
+ *
67
+ * Panics (fatal, not caught by tryCatch) if the value is None.
68
+ */
69
+ unwrap<T>(): TypedAction<OptionT<T>, T> {
70
+ return branch({
71
+ Some: identity<T>(),
72
+ None: panic("called unwrap on None"),
73
+ }) as TypedAction<OptionT<T>, T>;
74
+ },
75
+
76
+ /**
77
+ * Extract the Some value or produce a default from an action.
78
+ * `Option<T> → T`
79
+ */
80
+ unwrapOr<T>(defaultAction: Pipeable<void, T>): TypedAction<OptionT<T>, T> {
81
+ return branch({
82
+ Some: identity<T>(),
83
+ None: defaultAction,
84
+ }) as TypedAction<OptionT<T>, T>;
85
+ },
86
+
87
+ /**
88
+ * Conditional keep. If Some, pass value to predicate which returns
89
+ * Option<T>. If None, stay None. `Option<T> → Option<T>`
90
+ */
91
+ filter<T>(
92
+ predicate: Pipeable<T, OptionT<T>>,
93
+ ): TypedAction<OptionT<T>, OptionT<T>> {
94
+ return branch({
95
+ Some: predicate,
96
+ None: Option.none<T>(),
97
+ }) as TypedAction<OptionT<T>, OptionT<T>>;
98
+ },
99
+
100
+ /**
101
+ * Collect Some values from an array, discarding Nones.
102
+ * `Option<T>[] → T[]`
103
+ */
104
+ collect<T = any>(): TypedAction<OptionT<T>[], T[]> {
105
+ return typedAction({
106
+ kind: "Invoke",
107
+ handler: { kind: "Builtin", builtin: { kind: "CollectSome" } },
108
+ });
109
+ },
110
+
111
+ /**
112
+ * Test if the value is Some. `Option<T> → boolean`
113
+ */
114
+ isSome<T>(): TypedAction<OptionT<T>, boolean> {
115
+ return branch({
116
+ Some: constant<boolean>(true),
117
+ None: constant<boolean>(false),
118
+ }) as TypedAction<OptionT<T>, boolean>;
119
+ },
120
+
121
+ /**
122
+ * Test if the value is None. `Option<T> → boolean`
123
+ */
124
+ isNone<T>(): TypedAction<OptionT<T>, boolean> {
125
+ return branch({
126
+ Some: constant<boolean>(false),
127
+ None: constant<boolean>(true),
128
+ }) as TypedAction<OptionT<T>, boolean>;
129
+ },
130
+
131
+ /**
132
+ * Swap Option/Result nesting.
133
+ * `Option<Result<TValue, TError>> → Result<Option<TValue>, TError>`
134
+ *
135
+ * - Some(Ok(t)) → Ok(Some(t))
136
+ * - Some(Err(e)) → Err(e)
137
+ * - None → Ok(None)
138
+ */
139
+ transpose<TValue, TError>(): TypedAction<
140
+ OptionT<ResultT<TValue, TError>>,
141
+ ResultT<OptionT<TValue>, TError>
142
+ > {
143
+ return branch({
144
+ Some: branch({
145
+ Ok: chain(Option.some<TValue>(), Result.ok<OptionT<TValue>, TError>()),
146
+ Err: Result.err<OptionT<TValue>, TError>(),
147
+ }),
148
+ None: chain(
149
+ chain(drop, Option.none<TValue>()),
150
+ Result.ok<OptionT<TValue>, TError>(),
151
+ ),
152
+ }) as TypedAction<
153
+ OptionT<ResultT<TValue, TError>>,
154
+ ResultT<OptionT<TValue>, TError>
155
+ >;
156
+ },
157
+ } as const;
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // First — extract the first element of an array as Option<TElement>
161
+ // ---------------------------------------------------------------------------
162
+
163
+ /**
164
+ * Extract the first element of an array.
165
+ * `readonly TElement[] → Option<TElement>`
166
+ *
167
+ * Composes `splitFirst` (which returns `Option<[TElement, TElement[]]>`)
168
+ * with `Option.map(getIndex(0))` to extract just the element.
169
+ */
170
+ export function first<TElement>(): TypedAction<
171
+ readonly TElement[],
172
+ OptionT<TElement>
173
+ > {
174
+ return chain(
175
+ toAction(splitFirst()),
176
+ toAction(Option.map(toAction(getIndex(0).unwrap()))),
177
+ ) as TypedAction<readonly TElement[], OptionT<TElement>>;
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Last — extract the last element of an array as Option<TElement>
182
+ // ---------------------------------------------------------------------------
183
+
184
+ /**
185
+ * Extract the last element of an array.
186
+ * `readonly TElement[] → Option<TElement>`
187
+ *
188
+ * Composes `splitLast` (which returns `Option<[TElement[], TElement]>`)
189
+ * with `Option.map(getIndex(1))` to extract just the element.
190
+ */
191
+ export function last<TElement>(): TypedAction<
192
+ readonly TElement[],
193
+ OptionT<TElement>
194
+ > {
195
+ return chain(
196
+ toAction(splitLast()),
197
+ toAction(Option.map(toAction(getIndex(1).unwrap()))),
198
+ ) as TypedAction<readonly TElement[], OptionT<TElement>>;
199
+ }
package/src/pipe.ts ADDED
@@ -0,0 +1,138 @@
1
+ import {
2
+ type Action,
3
+ type PipeIn,
4
+ type Pipeable,
5
+ type TypedAction,
6
+ toAction,
7
+ } from "./ast.js";
8
+ import { chain } from "./chain.js";
9
+ import { identity } from "./builtins/index.js";
10
+
11
+ export function pipe<TStep1, TStep2>(
12
+ a1: Pipeable<TStep1, TStep2>,
13
+ ): TypedAction<PipeIn<TStep1>, TStep2>;
14
+ export function pipe<TStep1, TStep2, TStep3>(
15
+ a1: Pipeable<TStep1, TStep2>,
16
+ a2: Pipeable<TStep2, TStep3>,
17
+ ): TypedAction<PipeIn<TStep1>, TStep3>;
18
+ export function pipe<TStep1, TStep2, TStep3, TStep4>(
19
+ a1: Pipeable<TStep1, TStep2>,
20
+ a2: Pipeable<TStep2, TStep3>,
21
+ a3: Pipeable<TStep3, TStep4>,
22
+ ): TypedAction<PipeIn<TStep1>, TStep4>;
23
+ export function pipe<TStep1, TStep2, TStep3, TStep4, TStep5>(
24
+ a1: Pipeable<TStep1, TStep2>,
25
+ a2: Pipeable<TStep2, TStep3>,
26
+ a3: Pipeable<TStep3, TStep4>,
27
+ a4: Pipeable<TStep4, TStep5>,
28
+ ): TypedAction<PipeIn<TStep1>, TStep5>;
29
+ export function pipe<TStep1, TStep2, TStep3, TStep4, TStep5, TStep6>(
30
+ a1: Pipeable<TStep1, TStep2>,
31
+ a2: Pipeable<TStep2, TStep3>,
32
+ a3: Pipeable<TStep3, TStep4>,
33
+ a4: Pipeable<TStep4, TStep5>,
34
+ a5: Pipeable<TStep5, TStep6>,
35
+ ): TypedAction<PipeIn<TStep1>, TStep6>;
36
+ export function pipe<TStep1, TStep2, TStep3, TStep4, TStep5, TStep6, TStep7>(
37
+ a1: Pipeable<TStep1, TStep2>,
38
+ a2: Pipeable<TStep2, TStep3>,
39
+ a3: Pipeable<TStep3, TStep4>,
40
+ a4: Pipeable<TStep4, TStep5>,
41
+ a5: Pipeable<TStep5, TStep6>,
42
+ a6: Pipeable<TStep6, TStep7>,
43
+ ): TypedAction<PipeIn<TStep1>, TStep7>;
44
+ export function pipe<
45
+ TStep1,
46
+ TStep2,
47
+ TStep3,
48
+ TStep4,
49
+ TStep5,
50
+ TStep6,
51
+ TStep7,
52
+ TStep8,
53
+ >(
54
+ a1: Pipeable<TStep1, TStep2>,
55
+ a2: Pipeable<TStep2, TStep3>,
56
+ a3: Pipeable<TStep3, TStep4>,
57
+ a4: Pipeable<TStep4, TStep5>,
58
+ a5: Pipeable<TStep5, TStep6>,
59
+ a6: Pipeable<TStep6, TStep7>,
60
+ a7: Pipeable<TStep7, TStep8>,
61
+ ): TypedAction<PipeIn<TStep1>, TStep8>;
62
+ export function pipe<
63
+ TStep1,
64
+ TStep2,
65
+ TStep3,
66
+ TStep4,
67
+ TStep5,
68
+ TStep6,
69
+ TStep7,
70
+ TStep8,
71
+ TStep9,
72
+ >(
73
+ a1: Pipeable<TStep1, TStep2>,
74
+ a2: Pipeable<TStep2, TStep3>,
75
+ a3: Pipeable<TStep3, TStep4>,
76
+ a4: Pipeable<TStep4, TStep5>,
77
+ a5: Pipeable<TStep5, TStep6>,
78
+ a6: Pipeable<TStep6, TStep7>,
79
+ a7: Pipeable<TStep7, TStep8>,
80
+ a8: Pipeable<TStep8, TStep9>,
81
+ ): TypedAction<PipeIn<TStep1>, TStep9>;
82
+ export function pipe<
83
+ TStep1,
84
+ TStep2,
85
+ TStep3,
86
+ TStep4,
87
+ TStep5,
88
+ TStep6,
89
+ TStep7,
90
+ TStep8,
91
+ TStep9,
92
+ TStep10,
93
+ >(
94
+ a1: Pipeable<TStep1, TStep2>,
95
+ a2: Pipeable<TStep2, TStep3>,
96
+ a3: Pipeable<TStep3, TStep4>,
97
+ a4: Pipeable<TStep4, TStep5>,
98
+ a5: Pipeable<TStep5, TStep6>,
99
+ a6: Pipeable<TStep6, TStep7>,
100
+ a7: Pipeable<TStep7, TStep8>,
101
+ a8: Pipeable<TStep8, TStep9>,
102
+ a9: Pipeable<TStep9, TStep10>,
103
+ ): TypedAction<PipeIn<TStep1>, TStep10>;
104
+ export function pipe<
105
+ TStep1,
106
+ TStep2,
107
+ TStep3,
108
+ TStep4,
109
+ TStep5,
110
+ TStep6,
111
+ TStep7,
112
+ TStep8,
113
+ TStep9,
114
+ TStep10,
115
+ TStep11,
116
+ >(
117
+ a1: Pipeable<TStep1, TStep2>,
118
+ a2: Pipeable<TStep2, TStep3>,
119
+ a3: Pipeable<TStep3, TStep4>,
120
+ a4: Pipeable<TStep4, TStep5>,
121
+ a5: Pipeable<TStep5, TStep6>,
122
+ a6: Pipeable<TStep6, TStep7>,
123
+ a7: Pipeable<TStep7, TStep8>,
124
+ a8: Pipeable<TStep8, TStep9>,
125
+ a9: Pipeable<TStep9, TStep10>,
126
+ a10: Pipeable<TStep10, TStep11>,
127
+ ): TypedAction<PipeIn<TStep1>, TStep11>;
128
+ export function pipe(...actions: Action[]): Action {
129
+ if (actions.length === 0) {
130
+ return identity();
131
+ }
132
+ if (actions.length === 1) {
133
+ return actions[0];
134
+ }
135
+ return actions.reduceRight((rest, first) =>
136
+ toAction(chain(toAction(first), toAction(rest))),
137
+ );
138
+ }
package/src/race.ts ADDED
@@ -0,0 +1,173 @@
1
+ import {
2
+ type Action,
3
+ type Pipeable,
4
+ type Result as ResultT,
5
+ type TypedAction,
6
+ toAction,
7
+ typedAction,
8
+ buildRestartBranchAction,
9
+ } from "./ast.js";
10
+ import { chain } from "./chain.js";
11
+ import { identity, tag } from "./builtins/index.js";
12
+ import { Result } from "./result.js";
13
+ import {
14
+ allocateRestartHandlerId,
15
+ type RestartHandlerId,
16
+ } from "./effect-id.js";
17
+
18
+ /**
19
+ * `Chain(Tag("Break"), RestartPerform(id))` — shared by race branches.
20
+ * The winning branch tags its result as Break, then performs. The handler
21
+ * restarts the body; Branch takes the Break arm (identity), `RestartHandle` exits.
22
+ */
23
+ function breakPerform(restartHandlerId: RestartHandlerId): Action {
24
+ return toAction(
25
+ chain(toAction(tag("Break", "LoopResult")), {
26
+ kind: "RestartPerform",
27
+ restart_handler_id: restartHandlerId,
28
+ }),
29
+ );
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // race — first branch to complete wins, losers cancelled
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /**
37
+ * Run multiple actions concurrently. The first to complete wins; losers
38
+ * are cancelled during `RestartHandle` frame teardown.
39
+ *
40
+ * All branches must have the same input and output type (since either
41
+ * could win).
42
+ *
43
+ * Compiled form (restart+Branch, same substrate as loop/earlyReturn):
44
+ * `Chain(Tag("Continue"),`
45
+ * `RestartHandle(id, GetIndex(0),`
46
+ * `Branch({`
47
+ * `Continue: All(Chain(a, breakPerform), Chain(b, breakPerform), ...),`
48
+ * `Break: identity,`
49
+ * `})))`
50
+ *
51
+ * First branch to complete tags Break → `RestartPerform` → handler restarts →
52
+ * Branch takes Break arm → identity → `RestartHandle` exits with winner's value.
53
+ */
54
+ export function race<TIn, TOut>(
55
+ ...actions: Pipeable<TIn, TOut>[]
56
+ ): TypedAction<TIn, TOut> {
57
+ const restartHandlerId = allocateRestartHandlerId();
58
+ const perform = breakPerform(restartHandlerId);
59
+
60
+ const branches = actions.map((action) =>
61
+ toAction(chain(toAction(action), toAction(perform))),
62
+ );
63
+
64
+ const allAction: Action = { kind: "All", actions: branches };
65
+
66
+ return typedAction(
67
+ buildRestartBranchAction(restartHandlerId, allAction, toAction(identity())),
68
+ );
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // sleep — Rust builtin that delays for a fixed duration (passthrough)
73
+ // ---------------------------------------------------------------------------
74
+
75
+ /**
76
+ * Sleep for a fixed duration, ignoring input and returning void.
77
+ *
78
+ * `ms` is baked into the AST at construction time. Executed by the Rust
79
+ * scheduler via `tokio::time::sleep` — no subprocess spawned.
80
+ *
81
+ * To preserve data across a sleep, use `bindInput`.
82
+ */
83
+ export function sleep(ms: number): TypedAction<any, void> {
84
+ return typedAction<any, void>({
85
+ kind: "Invoke",
86
+ handler: { kind: "Builtin", builtin: { kind: "Sleep", ms } },
87
+ });
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // dynamicSleep — TypeScript handler for withTimeout (takes ms as input)
92
+ // ---------------------------------------------------------------------------
93
+
94
+ /** The raw Invoke node for the dynamic sleep handler. */
95
+ const DYNAMIC_SLEEP_INVOKE: Action = {
96
+ kind: "Invoke",
97
+ handler: {
98
+ kind: "TypeScript",
99
+ module: import.meta.url,
100
+ func: "dynamicSleep",
101
+ },
102
+ };
103
+
104
+ /**
105
+ * @internal TypeScript handler that takes ms as pipeline input and returns
106
+ * void after the timer fires. Used by `withTimeout` where the duration
107
+ * comes from a runtime pipeline, not a build-time constant.
108
+ */
109
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
110
+ export function dynamicSleep(): void {}
111
+ Object.defineProperty(dynamicSleep, "__definition", {
112
+ value: {
113
+ handle: ({ value }: { value: number }) =>
114
+ new Promise<void>((resolve) => setTimeout(resolve, value)),
115
+ },
116
+ enumerable: false,
117
+ });
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // withTimeout — race body against sleep, return Result
121
+ // ---------------------------------------------------------------------------
122
+
123
+ /**
124
+ * Race the body against a sleep timer. Returns `Result<TOut, void>`:
125
+ * - Ok(value) if the body completed first
126
+ * - Err(void) if the timeout fired first
127
+ *
128
+ * The `ms` parameter is an AST node that evaluates to the timeout duration
129
+ * in milliseconds. Use `constant(5000)` for a fixed timeout, or any action
130
+ * that computes a duration from the pipeline input.
131
+ *
132
+ * Built as raw AST rather than through `race()` because each branch wraps
133
+ * its result differently (Ok vs Err) before the Break+Perform. `race()`
134
+ * requires homogeneous output types, but withTimeout needs heterogeneous
135
+ * tagging.
136
+ *
137
+ * Same restart+Branch substrate as race: each branch tags Break after
138
+ * wrapping its result as Ok or Err.
139
+ */
140
+ export function withTimeout<TIn, TOut>(
141
+ ms: Pipeable<TIn, number>,
142
+ body: Pipeable<TIn, TOut>,
143
+ ): TypedAction<TIn, ResultT<TOut, void>> {
144
+ const restartHandlerId = allocateRestartHandlerId();
145
+ const perform = breakPerform(restartHandlerId);
146
+
147
+ // Branch 1: body → Tag("Ok") → Break → RestartPerform
148
+ const bodyBranch = toAction(
149
+ chain(
150
+ toAction(chain(toAction(body), toAction(Result.ok()))),
151
+ toAction(perform),
152
+ ),
153
+ );
154
+
155
+ // Branch 2: ms → sleep() → Tag("Err") → Break → RestartPerform
156
+ const sleepBranch = toAction(
157
+ chain(
158
+ toAction(
159
+ chain(
160
+ toAction(chain(toAction(ms), toAction(DYNAMIC_SLEEP_INVOKE))),
161
+ toAction(Result.err()),
162
+ ),
163
+ ),
164
+ toAction(perform),
165
+ ),
166
+ );
167
+
168
+ const allAction: Action = { kind: "All", actions: [bodyBranch, sleepBranch] };
169
+
170
+ return typedAction(
171
+ buildRestartBranchAction(restartHandlerId, allAction, toAction(identity())),
172
+ );
173
+ }
@@ -0,0 +1,129 @@
1
+ import {
2
+ type Action,
3
+ type Pipeable,
4
+ type TypedAction,
5
+ toAction,
6
+ typedAction,
7
+ branch,
8
+ } from "./ast.js";
9
+ import { all } from "./all.js";
10
+ import { chain } from "./chain.js";
11
+ import {
12
+ constant,
13
+ identity,
14
+ getField,
15
+ getIndex,
16
+ tag,
17
+ } from "./builtins/index.js";
18
+ import { allocateResumeHandlerId } from "./effect-id.js";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Types
22
+ // ---------------------------------------------------------------------------
23
+
24
+ type FunctionDef = [input: unknown, output: unknown];
25
+
26
+ type FunctionRefs<TDefs extends FunctionDef[]> = {
27
+ [K in keyof TDefs]: TypedAction<TDefs[K][0], TDefs[K][1]>;
28
+ };
29
+
30
+ /**
31
+ * Constraint for the entry-point callback return type. Only requires the
32
+ * output phantom field — omits __in and __in_co so that actions with
33
+ * any input type (e.g. pipelines starting from a call token) are assignable.
34
+ */
35
+ type BodyResult<TOut> = Action & {
36
+ __out?: () => TOut;
37
+ };
38
+
39
+ const UNUSED_STATE = null;
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // defineRecursiveFunctions
43
+ // ---------------------------------------------------------------------------
44
+
45
+ /**
46
+ * Define mutually recursive functions that can call each other.
47
+ *
48
+ * The type parameter is an array of [In, Out] tuples — one per function.
49
+ * TypeScript can't infer these from circular definitions, so they must be
50
+ * explicit.
51
+ *
52
+ * Returns a curried combinator: the first callback defines function bodies,
53
+ * the second receives the same call tokens and returns the workflow entry
54
+ * point.
55
+ *
56
+ * Desugars to a ResumeHandle with a Branch-based handler. Each call token
57
+ * is Chain(Tag("CallN"), ResumePerform(id)). The handler dispatches to the
58
+ * correct function body by tag. The caller's pipeline is preserved as a
59
+ * ResumePerformFrame across each call.
60
+ *
61
+ * **Known limitation:** concurrent calls to the same function do not work
62
+ * as expected. `all(f(x), f(x))` will NOT call `f` twice — both branches
63
+ * perform on the same ResumeHandle, and the second perform will not
64
+ * execute independently. Use sequential calls (chain/then) instead of
65
+ * concurrent calls (all) when calling recursive functions multiple times.
66
+ */
67
+ export function defineRecursiveFunctions<TDefs extends FunctionDef[]>(
68
+ bodiesFn: (...fns: FunctionRefs<TDefs>) => {
69
+ [K in keyof TDefs]: Pipeable<TDefs[K][0], TDefs[K][1]>;
70
+ },
71
+ ): <TOut>(
72
+ entryFn: (...fns: FunctionRefs<TDefs>) => BodyResult<TOut>,
73
+ ) => TypedAction<any, TOut> {
74
+ const resumeHandlerId = allocateResumeHandlerId();
75
+
76
+ const resumePerform: Action = {
77
+ kind: "ResumePerform",
78
+ resume_handler_id: resumeHandlerId,
79
+ };
80
+
81
+ // Call tokens: Chain(Tag("CallN"), ResumePerform(resumeHandlerId))
82
+ const fnCount = bodiesFn.length;
83
+ const callTokens = Array.from({ length: fnCount }, (_, i) =>
84
+ typedAction(
85
+ toAction(
86
+ chain(
87
+ toAction(tag(`Call${i}`, "RecursiveDispatch")),
88
+ toAction(resumePerform),
89
+ ),
90
+ ),
91
+ ),
92
+ );
93
+
94
+ // Get function body ASTs
95
+ const bodyActions = (
96
+ bodiesFn(...(callTokens as FunctionRefs<TDefs>)) as Pipeable[]
97
+ ).map(toAction);
98
+
99
+ // Branch cases: CallN → GetField("value") → bodyN
100
+ const cases: Record<string, Action> = {};
101
+ for (let i = 0; i < bodyActions.length; i++) {
102
+ cases[`Call${i}`] = toAction(
103
+ chain(toAction(getField("value")), toAction(bodyActions[i])),
104
+ );
105
+ }
106
+
107
+ // Return curried entry-point combinator
108
+ return <TOut>(entryFn: (...fns: FunctionRefs<TDefs>) => BodyResult<TOut>) => {
109
+ const userBody = toAction(entryFn(...(callTokens as FunctionRefs<TDefs>)));
110
+
111
+ return typedAction<any, TOut>(
112
+ toAction(
113
+ chain(toAction(all(identity(), constant(UNUSED_STATE))), {
114
+ kind: "ResumeHandle",
115
+ resume_handler_id: resumeHandlerId,
116
+ body: toAction(
117
+ chain(toAction(getIndex(0).unwrap()), toAction(userBody)),
118
+ ),
119
+ handler: toAction(
120
+ all(
121
+ chain(toAction(getIndex(0).unwrap()), toAction(branch(cases))),
122
+ constant(UNUSED_STATE),
123
+ ),
124
+ ),
125
+ }),
126
+ ),
127
+ );
128
+ };
129
+ }