@procedures/compiler 0.0.0-stage → 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.
Files changed (175) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +21 -0
  3. package/README.md +254 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/checks.d.ts +24 -0
  6. package/dist/checks.d.ts.map +1 -0
  7. package/dist/checks.js +123 -0
  8. package/dist/checks.js.map +1 -0
  9. package/dist/codegen-closure.d.ts +19 -0
  10. package/dist/codegen-closure.d.ts.map +1 -0
  11. package/dist/codegen-closure.js +200 -0
  12. package/dist/codegen-closure.js.map +1 -0
  13. package/dist/codegen-text.d.ts +44 -0
  14. package/dist/codegen-text.d.ts.map +1 -0
  15. package/dist/codegen-text.js +360 -0
  16. package/dist/codegen-text.js.map +1 -0
  17. package/dist/diagnostics.d.ts +67 -0
  18. package/dist/diagnostics.d.ts.map +1 -0
  19. package/dist/diagnostics.js +38 -0
  20. package/dist/diagnostics.js.map +1 -0
  21. package/dist/effects.d.ts +63 -0
  22. package/dist/effects.d.ts.map +1 -0
  23. package/dist/effects.js +185 -0
  24. package/dist/effects.js.map +1 -0
  25. package/dist/explain.d.ts +58 -0
  26. package/dist/explain.d.ts.map +1 -0
  27. package/dist/explain.js +144 -0
  28. package/dist/explain.js.map +1 -0
  29. package/dist/facts.d.ts +66 -0
  30. package/dist/facts.d.ts.map +1 -0
  31. package/dist/facts.js +102 -0
  32. package/dist/facts.js.map +1 -0
  33. package/dist/fold.d.ts +27 -0
  34. package/dist/fold.d.ts.map +1 -0
  35. package/dist/fold.js +128 -0
  36. package/dist/fold.js.map +1 -0
  37. package/dist/index.d.ts +35 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +22 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/interceptor.d.ts +51 -0
  42. package/dist/interceptor.d.ts.map +1 -0
  43. package/dist/interceptor.js +116 -0
  44. package/dist/interceptor.js.map +1 -0
  45. package/dist/ir.d.ts +240 -0
  46. package/dist/ir.d.ts.map +1 -0
  47. package/dist/ir.js +74 -0
  48. package/dist/ir.js.map +1 -0
  49. package/dist/mermaid.d.ts +53 -0
  50. package/dist/mermaid.d.ts.map +1 -0
  51. package/dist/mermaid.js +423 -0
  52. package/dist/mermaid.js.map +1 -0
  53. package/dist/module.d.ts +20 -0
  54. package/dist/module.d.ts.map +1 -0
  55. package/dist/module.js +124 -0
  56. package/dist/module.js.map +1 -0
  57. package/dist/normalize.d.ts +43 -0
  58. package/dist/normalize.d.ts.map +1 -0
  59. package/dist/normalize.js +529 -0
  60. package/dist/normalize.js.map +1 -0
  61. package/dist/openrpc.d.ts +63 -0
  62. package/dist/openrpc.d.ts.map +1 -0
  63. package/dist/openrpc.js +57 -0
  64. package/dist/openrpc.js.map +1 -0
  65. package/dist/print.d.ts +8 -0
  66. package/dist/print.d.ts.map +1 -0
  67. package/dist/print.js +148 -0
  68. package/dist/print.js.map +1 -0
  69. package/dist/procedures/program/check.d.ts +14 -0
  70. package/dist/procedures/program/check.d.ts.map +1 -0
  71. package/dist/procedures/program/check.js +30 -0
  72. package/dist/procedures/program/check.js.map +1 -0
  73. package/dist/procedures/program/compile.d.ts +14 -0
  74. package/dist/procedures/program/compile.d.ts.map +1 -0
  75. package/dist/procedures/program/compile.js +35 -0
  76. package/dist/procedures/program/compile.js.map +1 -0
  77. package/dist/procedures/program/diagram.d.ts +14 -0
  78. package/dist/procedures/program/diagram.d.ts.map +1 -0
  79. package/dist/procedures/program/diagram.js +23 -0
  80. package/dist/procedures/program/diagram.js.map +1 -0
  81. package/dist/procedures/program/effects-output.d.ts +8 -0
  82. package/dist/procedures/program/effects-output.d.ts.map +1 -0
  83. package/dist/procedures/program/effects-output.js +13 -0
  84. package/dist/procedures/program/effects-output.js.map +1 -0
  85. package/dist/procedures/program/explain.d.ts +14 -0
  86. package/dist/procedures/program/explain.d.ts.map +1 -0
  87. package/dist/procedures/program/explain.js +43 -0
  88. package/dist/procedures/program/explain.js.map +1 -0
  89. package/dist/procedures/program/index.d.ts +5 -0
  90. package/dist/procedures/program/index.d.ts.map +1 -0
  91. package/dist/procedures/program/index.js +5 -0
  92. package/dist/procedures/program/index.js.map +1 -0
  93. package/dist/procedures/sdk/index.d.ts +3 -0
  94. package/dist/procedures/sdk/index.d.ts.map +1 -0
  95. package/dist/procedures/sdk/index.js +3 -0
  96. package/dist/procedures/sdk/index.js.map +1 -0
  97. package/dist/procedures/sdk/manifest.d.ts +11 -0
  98. package/dist/procedures/sdk/manifest.d.ts.map +1 -0
  99. package/dist/procedures/sdk/manifest.js +15 -0
  100. package/dist/procedures/sdk/manifest.js.map +1 -0
  101. package/dist/procedures/sdk/openrpc.d.ts +11 -0
  102. package/dist/procedures/sdk/openrpc.d.ts.map +1 -0
  103. package/dist/procedures/sdk/openrpc.js +13 -0
  104. package/dist/procedures/sdk/openrpc.js.map +1 -0
  105. package/dist/procedures/sdk/typescript.d.ts +11 -0
  106. package/dist/procedures/sdk/typescript.d.ts.map +1 -0
  107. package/dist/procedures/sdk/typescript.js +14 -0
  108. package/dist/procedures/sdk/typescript.js.map +1 -0
  109. package/dist/program.d.ts +84 -0
  110. package/dist/program.d.ts.map +1 -0
  111. package/dist/program.js +108 -0
  112. package/dist/program.js.map +1 -0
  113. package/dist/refs.d.ts +9 -0
  114. package/dist/refs.d.ts.map +1 -0
  115. package/dist/refs.js +9 -0
  116. package/dist/refs.js.map +1 -0
  117. package/dist/register.d.ts +6 -0
  118. package/dist/register.d.ts.map +1 -0
  119. package/dist/register.js +8 -0
  120. package/dist/register.js.map +1 -0
  121. package/dist/runtime.d.ts +68 -0
  122. package/dist/runtime.d.ts.map +1 -0
  123. package/dist/runtime.js +138 -0
  124. package/dist/runtime.js.map +1 -0
  125. package/dist/schema.d.ts +41 -0
  126. package/dist/schema.d.ts.map +1 -0
  127. package/dist/schema.js +325 -0
  128. package/dist/schema.js.map +1 -0
  129. package/dist/scopes.d.ts +25 -0
  130. package/dist/scopes.d.ts.map +1 -0
  131. package/dist/scopes.js +202 -0
  132. package/dist/scopes.js.map +1 -0
  133. package/dist/types.d.ts +129 -0
  134. package/dist/types.d.ts.map +1 -0
  135. package/dist/types.js +157 -0
  136. package/dist/types.js.map +1 -0
  137. package/dist/typescript-sdk.d.ts +34 -0
  138. package/dist/typescript-sdk.d.ts.map +1 -0
  139. package/dist/typescript-sdk.js +231 -0
  140. package/dist/typescript-sdk.js.map +1 -0
  141. package/package.json +83 -3
  142. package/src/checks.ts +128 -0
  143. package/src/codegen-closure.ts +224 -0
  144. package/src/codegen-text.ts +396 -0
  145. package/src/diagnostics.ts +98 -0
  146. package/src/effects.ts +231 -0
  147. package/src/explain.ts +194 -0
  148. package/src/facts.ts +161 -0
  149. package/src/fold.ts +136 -0
  150. package/src/index.ts +35 -0
  151. package/src/interceptor.ts +149 -0
  152. package/src/ir.ts +330 -0
  153. package/src/mermaid.ts +460 -0
  154. package/src/module.ts +171 -0
  155. package/src/normalize.ts +694 -0
  156. package/src/openrpc.ts +104 -0
  157. package/src/print.ts +143 -0
  158. package/src/procedures/program/check.ts +31 -0
  159. package/src/procedures/program/compile.ts +36 -0
  160. package/src/procedures/program/diagram.ts +25 -0
  161. package/src/procedures/program/effects-output.ts +16 -0
  162. package/src/procedures/program/explain.ts +46 -0
  163. package/src/procedures/program/index.ts +4 -0
  164. package/src/procedures/sdk/index.ts +2 -0
  165. package/src/procedures/sdk/manifest.ts +17 -0
  166. package/src/procedures/sdk/openrpc.ts +16 -0
  167. package/src/procedures/sdk/typescript.ts +17 -0
  168. package/src/program.ts +181 -0
  169. package/src/refs.ts +9 -0
  170. package/src/register.ts +10 -0
  171. package/src/runtime.ts +188 -0
  172. package/src/schema.ts +318 -0
  173. package/src/scopes.ts +196 -0
  174. package/src/types.ts +318 -0
  175. package/src/typescript-sdk.ts +282 -0
package/package.json CHANGED
@@ -1,6 +1,86 @@
1
1
  {
2
+ "$schema": "https://json.schemastore.org/package",
2
3
  "name": "@procedures/compiler",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
4
+ "version": "0.3.0",
5
+ "description": "The compiler of procedure programs: it checks a JSON program before the first call (scopes, effects, schemas, capability) and writes it as a plain async function.",
6
+ "keywords": [
7
+ "compiler",
8
+ "procedures",
9
+ "static-analysis",
10
+ "effects",
11
+ "capabilities",
12
+ "codegen",
13
+ "workflow",
14
+ "dsl"
15
+ ],
16
+ "homepage": "https://mark1russell7.github.io/client/",
17
+ "bugs": {
18
+ "url": "https://github.com/mark1russell7/client/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/mark1russell7/client.git",
23
+ "directory": "packages/compiler"
24
+ },
25
+ "license": "MIT",
26
+ "author": "Mark Russell (https://github.com/mark1russell7)",
27
+ "type": "module",
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js"
34
+ },
35
+ "./module": {
36
+ "types": "./dist/module.d.ts",
37
+ "import": "./dist/module.js"
38
+ },
39
+ "./register": {
40
+ "types": "./dist/register.d.ts",
41
+ "import": "./dist/register.js"
42
+ },
43
+ "./runtime": {
44
+ "types": "./dist/runtime.d.ts",
45
+ "import": "./dist/runtime.js"
46
+ }
47
+ },
48
+ "files": [
49
+ "dist",
50
+ "src",
51
+ "!src/**/*.test.ts",
52
+ "!src/**/*.test.tsx",
53
+ "!src/**/__tests__",
54
+ "CHANGELOG.md"
55
+ ],
56
+ "sideEffects": [
57
+ "./dist/register.js"
58
+ ],
59
+ "devDependencies": {
60
+ "@mark1russell7/cue": "^1.0.0",
61
+ "fast-check": "^4.10.2",
62
+ "typescript": "^7.0.2",
63
+ "vitest": "^5.0.3"
64
+ },
65
+ "engines": {
66
+ "node": ">=22"
67
+ },
68
+ "publishConfig": {
69
+ "access": "public",
70
+ "provenance": true
71
+ },
72
+ "client": {
73
+ "procedures": "./dist/register.js"
74
+ },
75
+ "dependencies": {
76
+ "@procedures/client": "0.3.0",
77
+ "zod": "^4.6.5"
78
+ },
79
+ "scripts": {
80
+ "build": "tsc -b",
81
+ "typecheck": "tsc --noEmit",
82
+ "clean": "rm -rf dist .tsbuildinfo",
83
+ "test": "vitest run",
84
+ "test:watch": "vitest"
85
+ }
6
86
  }
package/src/checks.ts ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The checks pass: the problems of each call before the first call starts.
3
+ *
4
+ * - An unknown path, or a path that the host does not expose.
5
+ * - A call that the capability refuses (`capabilityRefusal()` of the client, with the declared
6
+ * effects of the manifest), and a call whose program the compiler cannot see under a capability
7
+ * that limits the effects (fail closed).
8
+ * - An input that does not agree with the JSON Schema of the procedure.
9
+ *
10
+ * In the interpreter each call of a program goes through the admission: a call of the host, or a
11
+ * nested call of a data-driven procedure. So the pass examines each call of the IR.
12
+ */
13
+
14
+ import { capabilityRefusal, type AnyProcedure, type Capability, type Effects, type ProcedurePath } from "@procedures/client/browser";
15
+ import { isCallLike, type IRNode, walk } from "./ir.js";
16
+ import { isStringPath, type ProcedureFacts } from "./facts.js";
17
+ import { isUnknown, toEffects, type EffectsAnalysis } from "./effects.js";
18
+ import { checkShape, shapeOf, withExtra } from "./schema.js";
19
+ import type { Diagnostic } from "./diagnostics.js";
20
+
21
+ /** The options of the pass. */
22
+ export interface CheckPassOptions {
23
+ readonly capability?: Capability<unknown> | undefined;
24
+ }
25
+
26
+ /** A capability without its deadline: the static check does not know the time of the run. */
27
+ function withoutDeadline(capability: Capability<unknown> | undefined): Capability<unknown> | undefined {
28
+ if (!capability || capability.deadline === undefined) return capability;
29
+ const { deadline: _deadline, ...rest } = capability;
30
+ return rest;
31
+ }
32
+
33
+ /** True when the capability limits the reads or the writes, or refuses destructive procedures. */
34
+ function limitsEffects(capability: Capability<unknown> | undefined): boolean {
35
+ const permitted = capability?.effects;
36
+ if (!permitted) return false;
37
+ const limited = (permit: readonly string[] | "any" | undefined): boolean => permit !== undefined && permit !== "any";
38
+ return limited(permitted.reads) || limited(permitted.writes) || permitted.destructive === false;
39
+ }
40
+
41
+ /** A procedure that has only a path and effects, for `capabilityRefusal()`. */
42
+ function asProcedure(path: ProcedurePath, effects: Effects | undefined): AnyProcedure {
43
+ return { path, metadata: effects ? { effects } : {} } as unknown as AnyProcedure;
44
+ }
45
+
46
+ /** This function examines each call of the IR. */
47
+ export function checkCalls(root: IRNode, facts: ProcedureFacts, effects: EffectsAnalysis, options: CheckPassOptions = {}): Diagnostic[] {
48
+ const diagnostics: Diagnostic[] = [];
49
+ const capability = withoutDeadline(options.capability);
50
+ const limited = limitsEffects(capability);
51
+
52
+ const visit = (node: IRNode): void => {
53
+ if (node.kind === "deferred") {
54
+ if (isStringPath(node.path) && node.path.length > 0 && !facts.entry(node.path)) {
55
+ diagnostics.push({
56
+ severity: "warning",
57
+ code: "unknown-procedure",
58
+ message: `No procedure has the path ${node.key}. This call waits for its runner, and the runner cannot start it.`,
59
+ pointer: node.pointer,
60
+ });
61
+ }
62
+ return;
63
+ }
64
+ if (!isCallLike(node)) return;
65
+ if (node.kind === "dynamic" && node.model) walk(node.model, visit);
66
+ const path = node.path;
67
+ if (!isStringPath(path) || path.length === 0 || path.some((segment) => segment.length === 0)) return;
68
+ const entry = facts.entry(path);
69
+ if (!entry) {
70
+ diagnostics.push({
71
+ severity: "error",
72
+ code: "unknown-procedure",
73
+ message: `No procedure has the path ${node.key}: the host does not have it, or it does not expose it.`,
74
+ pointer: node.pointer,
75
+ });
76
+ return;
77
+ }
78
+ if (!facts.exposed(path)) {
79
+ diagnostics.push({
80
+ severity: "error",
81
+ code: "not-exposed",
82
+ message: `The host does not expose ${node.key}. A program runs its calls through data-driven procedures, and they can call only exposed procedures.`,
83
+ pointer: node.pointer,
84
+ });
85
+ }
86
+ if (capability) {
87
+ const refusal = capabilityRefusal(asProcedure(path, entry.effects), capability);
88
+ if (refusal) {
89
+ diagnostics.push({ severity: "error", code: "capability-refused", message: `The capability refuses this call (${refusal.reason}): ${refusal.message}.`, pointer: node.pointer });
90
+ }
91
+ }
92
+ if (node.kind === "call" || node.kind === "dynamic") {
93
+ const runs = effects.nested.get(node);
94
+ if (runs && isUnknown(runs)) {
95
+ diagnostics.push({
96
+ severity: limited ? "error" : "info",
97
+ code: "effects-unknown",
98
+ message: limited
99
+ ? `${node.key} runs a program that the compiler cannot see, and the capability limits the effects. The check fails closed: run program.check on a host that can see the program.`
100
+ : `${node.key} runs a program that the compiler cannot see, so the effects of the program are not known.`,
101
+ pointer: node.pointer,
102
+ });
103
+ } else if (runs && capability) {
104
+ const refusal = capabilityRefusal(asProcedure(path, toEffects(runs.may)), capability);
105
+ if (refusal) {
106
+ diagnostics.push({
107
+ severity: "error",
108
+ code: "capability-refused",
109
+ message: `The capability refuses a call that ${node.key} runs (${refusal.reason}): ${refusal.message.replace(node.key, `the program of ${node.key}`)}.`,
110
+ pointer: node.pointer,
111
+ });
112
+ }
113
+ }
114
+ if (!(node.kind === "dynamic" && (node.reason === "invalid-control-flow" || node.reason === "cyclic"))) {
115
+ for (const issue of checkShape(withExtra(shapeOf(node.input), node.extra), entry.input)) {
116
+ diagnostics.push({
117
+ severity: "error",
118
+ code: "input-schema",
119
+ message: `The input of ${node.key} does not agree with its schema: ${issue.path ? `${issue.path}: ` : ""}${issue.message}.`,
120
+ pointer: issue.pointer,
121
+ });
122
+ }
123
+ }
124
+ }
125
+ };
126
+ walk(root, visit);
127
+ return diagnostics;
128
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * The closure code: the IR to a JavaScript function made of closures. A host runs it without
3
+ * `eval` and without `new Function`.
4
+ *
5
+ * The function does the steps of the interpreter in the same order. Each call goes through
6
+ * `ctx.client.call`, so it keeps its admission, validation, deadline and interceptors. The
7
+ * control-flow procedures do not start: the function does their work in line. A dynamic node
8
+ * calls its procedure with the raw input, and the interpreter of the client runs it.
9
+ */
10
+
11
+ import type { IRNode, ItemsSpec, ProgramIR, ScopeSnapshot } from "./ir.js";
12
+ import {
13
+ call,
14
+ callRaw,
15
+ deferred,
16
+ execWithInput,
17
+ fail,
18
+ get,
19
+ items as itemsOf,
20
+ merge,
21
+ message,
22
+ object,
23
+ parallel,
24
+ truthy,
25
+ unknownName,
26
+ type CompiledProgram,
27
+ type ProgramContext,
28
+ type RuntimeFrame,
29
+ } from "./runtime.js";
30
+
31
+ /** The state of one run: the context and the value of each binding. */
32
+ interface State {
33
+ readonly ctx: ProgramContext;
34
+ readonly vars: unknown[];
35
+ }
36
+
37
+ type Run = (state: State) => Promise<unknown>;
38
+
39
+ /** The options of the closure code. */
40
+ export interface ClosureOptions {
41
+ /** The keys of the procedures that take raw input, for `client.exec(program, input)`. */
42
+ readonly rawKeys: readonly string[];
43
+ }
44
+
45
+ /** This function makes the closure code of a program. */
46
+ export function compileClosure(ir: ProgramIR, options: ClosureOptions): CompiledProgram {
47
+ const root = compile(ir.root);
48
+ const { mode, program, input: inputBinding, bindingCount } = ir;
49
+ const rawKeys = [...options.rawKeys];
50
+ return async (ctx: ProgramContext, input?: unknown): Promise<unknown> => {
51
+ if (mode === "exec" && input !== undefined) return execWithInput(ctx, program, input, rawKeys);
52
+ const state: State = { ctx, vars: new Array<unknown>(bindingCount) };
53
+ if (inputBinding) state.vars[inputBinding.id] = input;
54
+ return root(state);
55
+ };
56
+ }
57
+
58
+ /** The frames of a scope with the values of a run, for `withScope()`. */
59
+ function framesOf(snapshot: ScopeSnapshot, vars: readonly unknown[]): RuntimeFrame[] | undefined {
60
+ if (!snapshot) return undefined;
61
+ return snapshot.map((frame) => ({
62
+ names: frame.names.map(([name, binding]): readonly [string, unknown] => [name, vars[binding.id]]),
63
+ ...(frame.last ? { last: [vars[frame.last.id]] as const } : {}),
64
+ }));
65
+ }
66
+
67
+ function compileItems(spec: ItemsSpec): (state: State) => Promise<unknown[]> {
68
+ if (spec.kind === "list") {
69
+ const runs = spec.items.map(compile);
70
+ return async (state) => {
71
+ const values: unknown[] = [];
72
+ for (const run of runs) values.push(await run(state));
73
+ return values;
74
+ };
75
+ }
76
+ const run = compile(spec.value);
77
+ return async (state) => itemsOf(await run(state));
78
+ }
79
+
80
+ function compile(node: IRNode): Run {
81
+ switch (node.kind) {
82
+ case "literal": {
83
+ const value = node.value;
84
+ return async () => value;
85
+ }
86
+ case "object": {
87
+ const parts = node.entries.map((entry) => [entry.key, compile(entry.value)] as const);
88
+ return async (state) => {
89
+ const entries: Array<readonly [string, unknown]> = [];
90
+ for (const [key, run] of parts) entries.push([key, await run(state)]);
91
+ return object(entries);
92
+ };
93
+ }
94
+ case "array": {
95
+ const runs = node.items.map(compile);
96
+ return async (state) => {
97
+ const values: unknown[] = [];
98
+ for (const run of runs) values.push(await run(state));
99
+ return values;
100
+ };
101
+ }
102
+ case "reference": {
103
+ const { binding, fields, text, deferred: isDeferred, source } = node;
104
+ if (binding) {
105
+ const id = binding.id;
106
+ return fields.length === 0 ? async (state) => state.vars[id] : async (state) => get(state.vars[id], fields);
107
+ }
108
+ if (isDeferred) return async () => source;
109
+ return async () => unknownName(text);
110
+ }
111
+ case "call": {
112
+ const input = compile(node.input);
113
+ const { path, extra } = node;
114
+ return async (state) => call(state.ctx, path, merge(await input(state), extra));
115
+ }
116
+ case "deferred": {
117
+ const input = compile(node.input);
118
+ const source = node.source;
119
+ return async (state) => deferred(source, await input(state));
120
+ }
121
+ case "dynamic": {
122
+ const input = compile(node.input);
123
+ const { path, extra, scope } = node;
124
+ return async (state) => callRaw(state.ctx, path, merge(await input(state), extra), framesOf(scope, state.vars));
125
+ }
126
+ case "fail": {
127
+ const text = node.message;
128
+ return async () => fail(text);
129
+ }
130
+ case "chain": {
131
+ const initial = node.initialInput ? compile(node.initialInput) : undefined;
132
+ const initialId = node.initial?.id;
133
+ const steps = node.steps.map((step) => [step.result.id, compile(step.node)] as const);
134
+ return async (state) => {
135
+ if (initial && initialId !== undefined) state.vars[initialId] = await initial(state);
136
+ const results: unknown[] = [];
137
+ for (const [id, run] of steps) {
138
+ const result = await run(state);
139
+ state.vars[id] = result;
140
+ results.push(result);
141
+ }
142
+ return { results, final: results[results.length - 1] };
143
+ };
144
+ }
145
+ case "parallel": {
146
+ const tasks = node.tasks.map(compile);
147
+ const { concurrency, failFast } = node;
148
+ return async (state) => parallel(tasks.map((task) => () => task(state)), concurrency, failFast);
149
+ }
150
+ case "conditional": {
151
+ const condition = compile(node.condition);
152
+ const thenRun = node.then ? compile(node.then) : undefined;
153
+ const elseRun = node.else ? compile(node.else) : undefined;
154
+ return async (state) => {
155
+ const branch = truthy(await condition(state)) ? thenRun : elseRun;
156
+ return branch ? branch(state) : undefined;
157
+ };
158
+ }
159
+ case "try": {
160
+ const tried = compile(node.try);
161
+ const caught = compile(node.catch);
162
+ const errorId = node.error.id;
163
+ return async (state) => {
164
+ try {
165
+ const value = await tried(state);
166
+ return { success: true, value };
167
+ } catch (error) {
168
+ const text = message(error);
169
+ state.vars[errorId] = text;
170
+ const value = await caught(state);
171
+ return { success: false, value, error: text };
172
+ }
173
+ };
174
+ }
175
+ case "and":
176
+ case "or": {
177
+ const values = node.values.map(compile);
178
+ const stop = node.kind === "and" ? (value: unknown) => !value : (value: unknown) => Boolean(value);
179
+ return async (state) => {
180
+ let last: unknown = undefined;
181
+ for (const run of values) {
182
+ last = await run(state);
183
+ if (stop(last)) return last;
184
+ }
185
+ return last;
186
+ };
187
+ }
188
+ case "map": {
189
+ const list = compileItems(node.items);
190
+ const fn = node.fn ? compile(node.fn) : undefined;
191
+ const itemId = node.item.id;
192
+ const indexId = node.index.id;
193
+ return async (state) => {
194
+ const values = await list(state);
195
+ if (!fn) return { results: values };
196
+ const results: unknown[] = [];
197
+ for (const [index, item] of values.entries()) {
198
+ state.vars[itemId] = item;
199
+ state.vars[indexId] = index;
200
+ results.push(await fn(state));
201
+ }
202
+ return { results };
203
+ };
204
+ }
205
+ case "reduce": {
206
+ const initial = compile(node.initial);
207
+ if (!node.fn || !node.items) return initial;
208
+ const list = compileItems(node.items);
209
+ const fn = compile(node.fn);
210
+ const { acc: accBinding, item: itemBinding, index: indexBinding } = node;
211
+ return async (state) => {
212
+ const values = await list(state);
213
+ let acc = await initial(state);
214
+ for (const [index, item] of values.entries()) {
215
+ state.vars[accBinding.id] = acc;
216
+ state.vars[itemBinding.id] = item;
217
+ state.vars[indexBinding.id] = index;
218
+ acc = await fn(state);
219
+ }
220
+ return acc;
221
+ };
222
+ }
223
+ }
224
+ }