@procedures/compiler 0.0.0-stage → 0.2.3

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 (174) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +254 -2
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/checks.d.ts +24 -0
  5. package/dist/checks.d.ts.map +1 -0
  6. package/dist/checks.js +123 -0
  7. package/dist/checks.js.map +1 -0
  8. package/dist/codegen-closure.d.ts +19 -0
  9. package/dist/codegen-closure.d.ts.map +1 -0
  10. package/dist/codegen-closure.js +200 -0
  11. package/dist/codegen-closure.js.map +1 -0
  12. package/dist/codegen-text.d.ts +44 -0
  13. package/dist/codegen-text.d.ts.map +1 -0
  14. package/dist/codegen-text.js +360 -0
  15. package/dist/codegen-text.js.map +1 -0
  16. package/dist/diagnostics.d.ts +67 -0
  17. package/dist/diagnostics.d.ts.map +1 -0
  18. package/dist/diagnostics.js +38 -0
  19. package/dist/diagnostics.js.map +1 -0
  20. package/dist/effects.d.ts +63 -0
  21. package/dist/effects.d.ts.map +1 -0
  22. package/dist/effects.js +185 -0
  23. package/dist/effects.js.map +1 -0
  24. package/dist/explain.d.ts +58 -0
  25. package/dist/explain.d.ts.map +1 -0
  26. package/dist/explain.js +144 -0
  27. package/dist/explain.js.map +1 -0
  28. package/dist/facts.d.ts +66 -0
  29. package/dist/facts.d.ts.map +1 -0
  30. package/dist/facts.js +102 -0
  31. package/dist/facts.js.map +1 -0
  32. package/dist/fold.d.ts +27 -0
  33. package/dist/fold.d.ts.map +1 -0
  34. package/dist/fold.js +128 -0
  35. package/dist/fold.js.map +1 -0
  36. package/dist/index.d.ts +35 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +22 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/interceptor.d.ts +51 -0
  41. package/dist/interceptor.d.ts.map +1 -0
  42. package/dist/interceptor.js +116 -0
  43. package/dist/interceptor.js.map +1 -0
  44. package/dist/ir.d.ts +240 -0
  45. package/dist/ir.d.ts.map +1 -0
  46. package/dist/ir.js +74 -0
  47. package/dist/ir.js.map +1 -0
  48. package/dist/mermaid.d.ts +53 -0
  49. package/dist/mermaid.d.ts.map +1 -0
  50. package/dist/mermaid.js +423 -0
  51. package/dist/mermaid.js.map +1 -0
  52. package/dist/module.d.ts +20 -0
  53. package/dist/module.d.ts.map +1 -0
  54. package/dist/module.js +124 -0
  55. package/dist/module.js.map +1 -0
  56. package/dist/normalize.d.ts +43 -0
  57. package/dist/normalize.d.ts.map +1 -0
  58. package/dist/normalize.js +529 -0
  59. package/dist/normalize.js.map +1 -0
  60. package/dist/openrpc.d.ts +63 -0
  61. package/dist/openrpc.d.ts.map +1 -0
  62. package/dist/openrpc.js +57 -0
  63. package/dist/openrpc.js.map +1 -0
  64. package/dist/print.d.ts +8 -0
  65. package/dist/print.d.ts.map +1 -0
  66. package/dist/print.js +148 -0
  67. package/dist/print.js.map +1 -0
  68. package/dist/procedures/program/check.d.ts +14 -0
  69. package/dist/procedures/program/check.d.ts.map +1 -0
  70. package/dist/procedures/program/check.js +30 -0
  71. package/dist/procedures/program/check.js.map +1 -0
  72. package/dist/procedures/program/compile.d.ts +14 -0
  73. package/dist/procedures/program/compile.d.ts.map +1 -0
  74. package/dist/procedures/program/compile.js +35 -0
  75. package/dist/procedures/program/compile.js.map +1 -0
  76. package/dist/procedures/program/diagram.d.ts +14 -0
  77. package/dist/procedures/program/diagram.d.ts.map +1 -0
  78. package/dist/procedures/program/diagram.js +23 -0
  79. package/dist/procedures/program/diagram.js.map +1 -0
  80. package/dist/procedures/program/effects-output.d.ts +8 -0
  81. package/dist/procedures/program/effects-output.d.ts.map +1 -0
  82. package/dist/procedures/program/effects-output.js +13 -0
  83. package/dist/procedures/program/effects-output.js.map +1 -0
  84. package/dist/procedures/program/explain.d.ts +14 -0
  85. package/dist/procedures/program/explain.d.ts.map +1 -0
  86. package/dist/procedures/program/explain.js +43 -0
  87. package/dist/procedures/program/explain.js.map +1 -0
  88. package/dist/procedures/program/index.d.ts +5 -0
  89. package/dist/procedures/program/index.d.ts.map +1 -0
  90. package/dist/procedures/program/index.js +5 -0
  91. package/dist/procedures/program/index.js.map +1 -0
  92. package/dist/procedures/sdk/index.d.ts +3 -0
  93. package/dist/procedures/sdk/index.d.ts.map +1 -0
  94. package/dist/procedures/sdk/index.js +3 -0
  95. package/dist/procedures/sdk/index.js.map +1 -0
  96. package/dist/procedures/sdk/manifest.d.ts +11 -0
  97. package/dist/procedures/sdk/manifest.d.ts.map +1 -0
  98. package/dist/procedures/sdk/manifest.js +15 -0
  99. package/dist/procedures/sdk/manifest.js.map +1 -0
  100. package/dist/procedures/sdk/openrpc.d.ts +11 -0
  101. package/dist/procedures/sdk/openrpc.d.ts.map +1 -0
  102. package/dist/procedures/sdk/openrpc.js +13 -0
  103. package/dist/procedures/sdk/openrpc.js.map +1 -0
  104. package/dist/procedures/sdk/typescript.d.ts +11 -0
  105. package/dist/procedures/sdk/typescript.d.ts.map +1 -0
  106. package/dist/procedures/sdk/typescript.js +14 -0
  107. package/dist/procedures/sdk/typescript.js.map +1 -0
  108. package/dist/program.d.ts +84 -0
  109. package/dist/program.d.ts.map +1 -0
  110. package/dist/program.js +108 -0
  111. package/dist/program.js.map +1 -0
  112. package/dist/refs.d.ts +9 -0
  113. package/dist/refs.d.ts.map +1 -0
  114. package/dist/refs.js +9 -0
  115. package/dist/refs.js.map +1 -0
  116. package/dist/register.d.ts +6 -0
  117. package/dist/register.d.ts.map +1 -0
  118. package/dist/register.js +8 -0
  119. package/dist/register.js.map +1 -0
  120. package/dist/runtime.d.ts +68 -0
  121. package/dist/runtime.d.ts.map +1 -0
  122. package/dist/runtime.js +138 -0
  123. package/dist/runtime.js.map +1 -0
  124. package/dist/schema.d.ts +41 -0
  125. package/dist/schema.d.ts.map +1 -0
  126. package/dist/schema.js +325 -0
  127. package/dist/schema.js.map +1 -0
  128. package/dist/scopes.d.ts +25 -0
  129. package/dist/scopes.d.ts.map +1 -0
  130. package/dist/scopes.js +202 -0
  131. package/dist/scopes.js.map +1 -0
  132. package/dist/types.d.ts +129 -0
  133. package/dist/types.d.ts.map +1 -0
  134. package/dist/types.js +157 -0
  135. package/dist/types.js.map +1 -0
  136. package/dist/typescript-sdk.d.ts +34 -0
  137. package/dist/typescript-sdk.d.ts.map +1 -0
  138. package/dist/typescript-sdk.js +231 -0
  139. package/dist/typescript-sdk.js.map +1 -0
  140. package/package.json +83 -3
  141. package/src/checks.ts +128 -0
  142. package/src/codegen-closure.ts +224 -0
  143. package/src/codegen-text.ts +396 -0
  144. package/src/diagnostics.ts +98 -0
  145. package/src/effects.ts +231 -0
  146. package/src/explain.ts +194 -0
  147. package/src/facts.ts +161 -0
  148. package/src/fold.ts +136 -0
  149. package/src/index.ts +35 -0
  150. package/src/interceptor.ts +149 -0
  151. package/src/ir.ts +330 -0
  152. package/src/mermaid.ts +460 -0
  153. package/src/module.ts +171 -0
  154. package/src/normalize.ts +694 -0
  155. package/src/openrpc.ts +104 -0
  156. package/src/print.ts +143 -0
  157. package/src/procedures/program/check.ts +31 -0
  158. package/src/procedures/program/compile.ts +36 -0
  159. package/src/procedures/program/diagram.ts +25 -0
  160. package/src/procedures/program/effects-output.ts +16 -0
  161. package/src/procedures/program/explain.ts +46 -0
  162. package/src/procedures/program/index.ts +4 -0
  163. package/src/procedures/sdk/index.ts +2 -0
  164. package/src/procedures/sdk/manifest.ts +17 -0
  165. package/src/procedures/sdk/openrpc.ts +16 -0
  166. package/src/procedures/sdk/typescript.ts +17 -0
  167. package/src/program.ts +181 -0
  168. package/src/refs.ts +9 -0
  169. package/src/register.ts +10 -0
  170. package/src/runtime.ts +188 -0
  171. package/src/schema.ts +318 -0
  172. package/src/scopes.ts +196 -0
  173. package/src/types.ts +318 -0
  174. package/src/typescript-sdk.ts +282 -0
package/src/fold.ts ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The fold pass (optional): a pure core call with a literal input becomes its value.
3
+ *
4
+ * The pass folds a call only when all of these conditions are true:
5
+ *
6
+ * - The manifest entry is the description of a core procedure of `@procedures/client`. Thus the
7
+ * host has the same code.
8
+ * - Its effects are `reads: []` and `writes: []`.
9
+ * - It does not take raw input, and it is not data-driven.
10
+ * - Its value is a primitive.
11
+ *
12
+ * The pass starts the procedure here with `invokeProcedure()`, so the validation and the output
13
+ * schema apply. A call that fails stays a call: the run gives its error.
14
+ *
15
+ * A folded call does not start at run time: an interceptor, a trace and the admission do not see
16
+ * it. A host that needs each call in its trace does not fold.
17
+ */
18
+
19
+ import { invokeProcedure, isReadOnly, outputValue, effectsOf } from "@procedures/client/browser";
20
+ import type { ChainStep, IRNode, ItemsSpec, LiteralNode } from "./ir.js";
21
+ import type { ProcedureFacts } from "./facts.js";
22
+ import { mergeExtra } from "./normalize.js";
23
+
24
+ /** The value of a node when it is fully known before the run. */
25
+ function staticValue(node: IRNode): { known: true; value: unknown } | { known: false } {
26
+ switch (node.kind) {
27
+ case "literal":
28
+ return { known: true, value: node.value };
29
+ case "object": {
30
+ const entries: Array<[string, unknown]> = [];
31
+ for (const entry of node.entries) {
32
+ const value = staticValue(entry.value);
33
+ if (!value.known) return value;
34
+ entries.push([entry.key, value.value]);
35
+ }
36
+ return { known: true, value: Object.fromEntries(entries) };
37
+ }
38
+ case "array": {
39
+ const values: unknown[] = [];
40
+ for (const item of node.items) {
41
+ const value = staticValue(item);
42
+ if (!value.known) return value;
43
+ values.push(value.value);
44
+ }
45
+ return { known: true, value: values };
46
+ }
47
+ default:
48
+ return { known: false };
49
+ }
50
+ }
51
+
52
+ function isPrimitive(value: unknown): boolean {
53
+ return value === null || (typeof value !== "object" && typeof value !== "function" && typeof value !== "symbol");
54
+ }
55
+
56
+ /** The result of the pass: the new tree and the pointers of the folded calls. */
57
+ export interface FoldResult {
58
+ readonly root: IRNode;
59
+ readonly folded: readonly string[];
60
+ }
61
+
62
+ /** This function folds the pure core calls with literal inputs. */
63
+ export async function foldProgram(root: IRNode, facts: ProcedureFacts): Promise<FoldResult> {
64
+ const folded: string[] = [];
65
+
66
+ const items = async (spec: ItemsSpec): Promise<ItemsSpec> =>
67
+ spec.kind === "list" ? { kind: "list", items: await Promise.all(spec.items.map(fold)) } : { kind: "value", value: await fold(spec.value) };
68
+
69
+ const tryFold = async (node: Extract<IRNode, { kind: "call" }>): Promise<IRNode> => {
70
+ const input = staticValue(node.input);
71
+ if (!input.known) return node;
72
+ const entry = facts.entry(node.path);
73
+ const procedure = facts.core(node.path);
74
+ if (!entry || !procedure || entry.dataDriven || facts.rawInput(node.path)) return node;
75
+ const effects = effectsOf(procedure);
76
+ if (!isReadOnly(effects) || (effects?.reads ?? ["unknown"]).length > 0) return node;
77
+ try {
78
+ const value = await outputValue(await invokeProcedure(procedure, mergeExtra(input.value, node.extra)), node.path);
79
+ if (!isPrimitive(value)) return node;
80
+ folded.push(node.pointer);
81
+ const literal: LiteralNode = { kind: "literal", value, origin: "fold", pointer: node.pointer };
82
+ return literal;
83
+ } catch {
84
+ return node;
85
+ }
86
+ };
87
+
88
+ const fold = async (node: IRNode): Promise<IRNode> => {
89
+ switch (node.kind) {
90
+ case "literal":
91
+ case "reference":
92
+ case "fail":
93
+ return node;
94
+ case "object":
95
+ return { ...node, entries: await Promise.all(node.entries.map(async (entry) => ({ key: entry.key, value: await fold(entry.value) }))) };
96
+ case "array":
97
+ return { ...node, items: await Promise.all(node.items.map(fold)) };
98
+ case "call":
99
+ return tryFold({ ...node, input: await fold(node.input) });
100
+ case "deferred":
101
+ return { ...node, input: await fold(node.input) };
102
+ case "dynamic":
103
+ return node;
104
+ case "chain": {
105
+ const steps: ChainStep[] = [];
106
+ for (const step of node.steps) steps.push({ ...step, node: await fold(step.node) });
107
+ return { ...node, steps, initialInput: node.initialInput ? await fold(node.initialInput) : undefined };
108
+ }
109
+ case "parallel":
110
+ return { ...node, tasks: await Promise.all(node.tasks.map(fold)) };
111
+ case "conditional":
112
+ return {
113
+ ...node,
114
+ condition: await fold(node.condition),
115
+ then: node.then ? await fold(node.then) : undefined,
116
+ else: node.else ? await fold(node.else) : undefined,
117
+ };
118
+ case "try":
119
+ return { ...node, try: await fold(node.try), catch: await fold(node.catch) };
120
+ case "and":
121
+ case "or":
122
+ return { ...node, values: await Promise.all(node.values.map(fold)) };
123
+ case "map":
124
+ return { ...node, items: await items(node.items), fn: node.fn ? await fold(node.fn) : undefined };
125
+ case "reduce":
126
+ return {
127
+ ...node,
128
+ items: node.items ? await items(node.items) : undefined,
129
+ initial: await fold(node.initial),
130
+ fn: node.fn ? await fold(node.fn) : undefined,
131
+ };
132
+ }
133
+ };
134
+
135
+ return { root: await fold(root), folded };
136
+ }
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ // The root registers nothing: import "<package>/register", or give the module to createRegistry()
2
+ export * from "./module.js";
3
+ export * from "./types.js";
4
+ export { checkProgram, compileProgram } from "./program.js";
5
+ export { programCheckInterceptor } from "./interceptor.js";
6
+ export { toMermaid, diagramProgram, mermaidText } from "./mermaid.js";
7
+ export type { MermaidOptions, DiagramResult } from "./mermaid.js";
8
+ export { explainProgram } from "./explain.js";
9
+ export type { CallExplanation, CallKind, ProgramExplanation } from "./explain.js";
10
+ export { toOpenRpc, OPENRPC_VERSION } from "./openrpc.js";
11
+ export type { OpenRpcDocument, OpenRpcMethod, OpenRpcContentDescriptor, OpenRpcOptions } from "./openrpc.js";
12
+ export { toTypeScriptSdk, schemaToType } from "./typescript-sdk.js";
13
+ export type { TypeScriptSdkOptions } from "./typescript-sdk.js";
14
+ export type { ProgramCheckOptions, ObservableRegistry } from "./interceptor.js";
15
+ export type { CheckOptions, CheckResult, CompileOptions, CompileResult, MustEffects } from "./program.js";
16
+ export { manifestFacts, registryFacts, withDefinitions, coreProcedureOf, entryTakesRawInput } from "./facts.js";
17
+ export type { ProcedureFacts, RegistryLike, FactsOptions, Definition } from "./facts.js";
18
+ export { normalize, findDefinitions, MAX_REF_DEPTH } from "./normalize.js";
19
+ export type { NormalizeOptions, NormalizeResult } from "./normalize.js";
20
+ export { resolveScopes } from "./scopes.js";
21
+ export { inferEffects, toEffects, NO_EFFECTS, UNKNOWN_EFFECTS } from "./effects.js";
22
+ export type { EffectSummary, EffectsAnalysis, MayEffects, MustEffects as MustEffectSets, BodyEffects } from "./effects.js";
23
+ export { checkCalls } from "./checks.js";
24
+ export { checkShape, shapeOf } from "./schema.js";
25
+ export type { Shape, SchemaIssue } from "./schema.js";
26
+ export { foldProgram } from "./fold.js";
27
+ export { compileClosure } from "./codegen-closure.js";
28
+ export { emitText } from "./codegen-text.js";
29
+ export type { ProgramText, SourceMapLine, TextOptions } from "./codegen-text.js";
30
+ export { printIR } from "./print.js";
31
+ export { hasErrors, sortDiagnostics } from "./diagnostics.js";
32
+ export type { Diagnostic, DiagnosticCode, Severity } from "./diagnostics.js";
33
+ export { childPointer, childrenOf, isCallLike, isControl, walk } from "./ir.js";
34
+ export type * from "./ir.js";
35
+ export type { CompiledProgram, ProgramContext, RuntimeFrame } from "./runtime.js";
@@ -0,0 +1,149 @@
1
+ /**
2
+ * The check before the run, as an interceptor of a host (roadmap R22).
3
+ *
4
+ * A host calls a data-driven procedure (`client.chain`, `client.map`, a procedure that
5
+ * `procedure.define` made) with a program in its input. Without the check, the interpreter finds
6
+ * a problem only when it arrives there: step 1 can write a file before step 3 meets a refusal.
7
+ * The interceptor checks the whole program before the first step. When the check finds an error,
8
+ * the host refuses the call, and no step starts.
9
+ *
10
+ * The check is conservative. It refuses a call that a branch can make, also a branch that does
11
+ * not run. A `client.tryCatch` around a refused call does not help: the program does not start.
12
+ *
13
+ * A call of `procedure.define` gets the check of its body. The host refuses a body with an
14
+ * error, and gives the effects of the body to `procedure.define`, which keeps them as
15
+ * `inferredEffects`. Then the tool of a defined procedure has the right hints.
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const server = new ProcedureServer({
20
+ * registry,
21
+ * expose,
22
+ * interceptors: [programCheckInterceptor({ registry, expose })],
23
+ * });
24
+ * ```
25
+ */
26
+
27
+ import {
28
+ INFERRED_EFFECTS_METADATA_KEY,
29
+ InvocationError,
30
+ effectsValue,
31
+ isDataDriven,
32
+ pathToKey,
33
+ type AnyProcedure,
34
+ type CapabilityRefusalReason,
35
+ type Interceptor,
36
+ type Invocation,
37
+ type InvocationErrorCode,
38
+ type ProcedurePath,
39
+ } from "@procedures/client/browser";
40
+ import { registryFacts, type ProcedureFacts, type RegistryLike } from "./facts.js";
41
+ import { checkProgram, type CheckResult } from "./program.js";
42
+ import type { Diagnostic } from "./diagnostics.js";
43
+
44
+ /** A registry that tells when its procedures change, so the interceptor can make its facts again. */
45
+ export interface ObservableRegistry extends RegistryLike {
46
+ on?(event: "register" | "unregister", listener: (procedure: AnyProcedure) => void): void;
47
+ }
48
+
49
+ /** The options of `programCheckInterceptor()`. */
50
+ export interface ProgramCheckOptions {
51
+ /** The registry of the host. */
52
+ readonly registry: ObservableRegistry;
53
+ /** The expose rule of the host. The default exposes each procedure. */
54
+ readonly expose?: ((path: ProcedurePath) => boolean) | undefined;
55
+ /** The diagnostics that refuse a program. The default refuses each error. */
56
+ readonly refuses?: ((diagnostic: Diagnostic) => boolean) | undefined;
57
+ /** This function gets each result of the check (for logs and tests). */
58
+ readonly onCheck?: ((invocation: Invocation, result: CheckResult) => void) | undefined;
59
+ }
60
+
61
+ /** The error code of the interpreter for the same problem, so a client sees one code. */
62
+ function codeOf(diagnostic: Diagnostic): InvocationErrorCode {
63
+ switch (diagnostic.code) {
64
+ case "not-exposed":
65
+ return "NOT_EXPOSED";
66
+ case "unknown-procedure":
67
+ return "NOT_FOUND";
68
+ case "capability-refused":
69
+ case "effects-unknown":
70
+ return "FORBIDDEN";
71
+ default:
72
+ return "VALIDATION_ERROR";
73
+ }
74
+ }
75
+
76
+ const REASONS: readonly CapabilityRefusalReason[] = ["effects-undeclared", "effects-exceeded", "clearance", "deadline"];
77
+
78
+ /** The reason of a `FORBIDDEN` refusal: the diagnostic names it in its message. */
79
+ function reasonOf(diagnostic: Diagnostic): CapabilityRefusalReason | undefined {
80
+ if (diagnostic.code === "effects-unknown") return "effects-undeclared";
81
+ if (diagnostic.code !== "capability-refused") return undefined;
82
+ return REASONS.find((reason) => diagnostic.message.includes(`(${reason})`));
83
+ }
84
+
85
+ /**
86
+ * The place of a problem for the caller. The check examines `{ $proc, input }`, and the caller
87
+ * sent only the input, so a pointer into the input loses its `/input` part.
88
+ */
89
+ function placeOf(pointer: string): string {
90
+ if (pointer === "" || pointer === "/$proc") return "the call";
91
+ if (pointer === "/input") return "the input";
92
+ if (pointer.startsWith("/input/")) return `${pointer.slice("/input".length)} of the input`;
93
+ return pointer;
94
+ }
95
+
96
+ /** The text of a refusal: the first problem with its place, and the number of the others. */
97
+ function messageOf(refused: readonly Diagnostic[]): string {
98
+ const first = refused[0]!;
99
+ const where = placeOf(first.pointer);
100
+ const more = refused.length > 1 ? ` The check found ${refused.length - 1} more problem${refused.length > 2 ? "s" : ""}.` : "";
101
+ return `The check before the run refused the program, and no step started. ${first.code} at ${where}: ${first.message}${more}`;
102
+ }
103
+
104
+ /**
105
+ * This function gives an interceptor that checks the program of each call of the host to a
106
+ * data-driven procedure, before the procedure starts. A nested call is a part of a program that
107
+ * the check examined already, so the interceptor does not check it again.
108
+ */
109
+ export function programCheckInterceptor(options: ProgramCheckOptions): Interceptor {
110
+ const refuses = options.refuses ?? ((diagnostic: Diagnostic) => diagnostic.severity === "error");
111
+ // The facts of the registry cost a description of each procedure: keep them until it changes
112
+ let facts: ProcedureFacts | undefined;
113
+ const reset = (): void => {
114
+ facts = undefined;
115
+ };
116
+ options.registry.on?.("register", reset);
117
+ options.registry.on?.("unregister", reset);
118
+
119
+ return async (invocation, next) => {
120
+ if (pathToKey(invocation.path) === "procedure.define") {
121
+ const aggregation = (invocation.input as { aggregation?: unknown } | null | undefined)?.aggregation;
122
+ if (aggregation === undefined) return next();
123
+ facts ??= registryFacts(options.registry, { expose: options.expose });
124
+ const body = checkProgram(aggregation, { facts, expose: options.expose, mode: "body" });
125
+ options.onCheck?.(invocation, body);
126
+ // A nested definition is a part of a program that the check examined already
127
+ const refused = invocation.depth === 0 ? body.diagnostics.filter(refuses) : [];
128
+ if (refused.length > 0) {
129
+ const placed = refused.map((diagnostic) => ({ ...diagnostic, pointer: `/input/aggregation${diagnostic.pointer}` }));
130
+ throw new InvocationError(codeOf(placed[0]!), messageOf(placed), invocation.path, reasonOf(placed[0]!));
131
+ }
132
+ const effects = effectsValue(body.effects);
133
+ return effects ? next({ metadata: { ...invocation.metadata, [INFERRED_EFFECTS_METADATA_KEY]: effects } }) : next();
134
+ }
135
+ if (invocation.depth !== 0 || !isDataDriven(invocation.procedure)) return next();
136
+ facts ??= registryFacts(options.registry, { expose: options.expose });
137
+ const result = checkProgram(
138
+ { $proc: invocation.path, input: invocation.input },
139
+ { facts, expose: options.expose, capability: invocation.capability },
140
+ );
141
+ options.onCheck?.(invocation, result);
142
+ const refused = result.diagnostics.filter(refuses);
143
+ if (refused.length > 0) {
144
+ const first = refused[0]!;
145
+ throw new InvocationError(codeOf(first), messageOf(refused), invocation.path, reasonOf(first));
146
+ }
147
+ return next();
148
+ };
149
+ }
package/src/ir.ts ADDED
@@ -0,0 +1,330 @@
1
+ /**
2
+ * The intermediate representation (IR) of a program.
3
+ *
4
+ * The normalize pass makes the IR from the JSON of a program. Each node is one step of the
5
+ * interpreter: `hydrateInput()` or a control-flow procedure of `@procedures/client`. The code
6
+ * generation follows the IR in the order of the interpreter. Each node keeps the JSON pointer
7
+ * (RFC 6901) of its source in the program.
8
+ */
9
+
10
+ import type { ProcedurePath } from "@procedures/client/browser";
11
+
12
+ /** A JSON pointer (RFC 6901) into the program. The empty string is the root. */
13
+ export type Pointer = string;
14
+
15
+ /** This function gives the pointer of a child: the key is escaped as RFC 6901 tells. */
16
+ export function childPointer(pointer: Pointer, key: string | number): Pointer {
17
+ return `${pointer}/${String(key).replace(/~/g, "~0").replace(/\//g, "~1")}`;
18
+ }
19
+
20
+ /** The kinds of a binding: the source of a name in a scope. */
21
+ export type BindingKind = "step" | "initial" | "item" | "index" | "acc" | "error" | "input";
22
+
23
+ /**
24
+ * A binding is a value that a scope gives to a name. Examples are the result of a chain step,
25
+ * the item of `client.map` and the error of `client.tryCatch`. Each binding gets one variable.
26
+ */
27
+ export interface Binding {
28
+ /** The number of the binding, unique in the program. */
29
+ readonly id: number;
30
+ readonly kind: BindingKind;
31
+ /** The name in the scope. A step without `$name` has no name: only `$last` reads it. */
32
+ readonly name: string | undefined;
33
+ /** The pointer of the source of the binding. */
34
+ readonly pointer: Pointer;
35
+ }
36
+
37
+ /** One frame of a static scope, as the interpreter has it at one point of the program. */
38
+ export interface ScopeFrame {
39
+ /** The names of the frame and their bindings, in the order of `Map.set`. */
40
+ readonly names: ReadonlyArray<readonly [string, Binding]>;
41
+ /** The binding that `$last` reads in this frame, or undefined when the frame has none. */
42
+ readonly last: Binding | undefined;
43
+ }
44
+
45
+ /** A static scope: the frames from the outermost to the innermost. Undefined: no scope. */
46
+ export type ScopeSnapshot = readonly ScopeFrame[] | undefined;
47
+
48
+ /** Where a call is: the root call, an operand of control flow, or a call in an input. */
49
+ export type CallPosition = "root" | "operand" | "nested";
50
+
51
+ interface NodeBase {
52
+ readonly pointer: Pointer;
53
+ }
54
+
55
+ /** A value that the program gives as it is. */
56
+ export interface LiteralNode extends NodeBase {
57
+ readonly kind: "literal";
58
+ /** The value. The closure code gives this object itself, as the interpreter does. */
59
+ readonly value: unknown;
60
+ /**
61
+ * The source of the value. It is plain data, a `$literal`, a `$never` call or a `$ref` outside
62
+ * each scope. It can also be a deferred call of a procedure that takes raw input, or a folded call.
63
+ */
64
+ readonly origin: "data" | "literal" | "never" | "deferred" | "unscoped-ref" | "fold";
65
+ }
66
+
67
+ /** A plain object with values that the interpreter computes. The entries keep the order of `Object.entries`. */
68
+ export interface ObjectNode extends NodeBase {
69
+ readonly kind: "object";
70
+ readonly entries: ReadonlyArray<{ readonly key: string; readonly value: IRNode }>;
71
+ }
72
+
73
+ /** An array with items that the interpreter computes, from the first item to the last item. */
74
+ export interface ArrayNode extends NodeBase {
75
+ readonly kind: "array";
76
+ readonly items: readonly IRNode[];
77
+ }
78
+
79
+ /** A `$ref`: it reads a name of the scope and optional fields. */
80
+ export interface ReferenceNode extends NodeBase {
81
+ readonly kind: "reference";
82
+ /** The text of the `$ref`: a name and fields, with dots. */
83
+ readonly text: string;
84
+ readonly name: string;
85
+ readonly fields: readonly string[];
86
+ /** True in the input of a deferred call: there, an unknown name stays data for the runner. */
87
+ readonly deferred: boolean;
88
+ /** The `$ref` object of the program. A deferred input gives it for an unknown name. */
89
+ readonly source: unknown;
90
+ /** True for a reference that the compiler made (the value that `passThrough` gives). */
91
+ readonly synthetic: boolean;
92
+ /** The binding of the name. The scopes pass sets it. Undefined: no scope has the name. */
93
+ binding: Binding | undefined;
94
+ }
95
+
96
+ /** A call through the client. Its input is hydrated before the call. */
97
+ export interface CallNode extends NodeBase {
98
+ readonly kind: "call";
99
+ readonly path: ProcedurePath;
100
+ readonly key: string;
101
+ readonly position: CallPosition;
102
+ readonly input: IRNode;
103
+ /** The parent context (`cwd`, `node`) that control flow adds to the input of an operand call. */
104
+ readonly extra: Readonly<Record<string, unknown>>;
105
+ /** The `$name` of the call. Only a chain step binds it. */
106
+ readonly name: string | undefined;
107
+ }
108
+
109
+ /** A call that waits for its runner (`$parent` or a context name): it is data, with its input hydrated. */
110
+ export interface DeferredNode extends NodeBase {
111
+ readonly kind: "deferred";
112
+ readonly path: ProcedurePath;
113
+ readonly key: string;
114
+ readonly when: string;
115
+ /** The call object of the program. */
116
+ readonly source: Readonly<Record<string, unknown>>;
117
+ readonly input: IRNode;
118
+ }
119
+
120
+ /** The reasons for a dynamic node. */
121
+ export type DynamicReason =
122
+ /** The procedure runs the refs of its input (the `runs-refs` tag): `client.eval`, `dag.traverse`, `procedure.define`. */
123
+ | "runs-refs"
124
+ /** The procedure takes its input as data (the `raw-input` tag): `client.export`. */
125
+ | "raw-input"
126
+ /** The input of a control-flow procedure does not agree with its schema: the call gives the validation error. */
127
+ | "invalid-control-flow"
128
+ /** The registry does not have the core control-flow procedure at the path. */
129
+ | "not-core"
130
+ /** A control-flow call contains itself. */
131
+ | "cyclic";
132
+
133
+ /**
134
+ * A call whose procedure gets its input raw, and whose operands the compiler does not model.
135
+ * The compiled code calls the procedure through the client with the raw input and the scope:
136
+ * the interpreter of the client runs it.
137
+ */
138
+ export interface DynamicNode extends NodeBase {
139
+ readonly kind: "dynamic";
140
+ readonly path: ProcedurePath;
141
+ readonly key: string;
142
+ readonly position: CallPosition;
143
+ readonly reason: DynamicReason;
144
+ /** The raw input: a literal, or a value that the compiled code computes (`passThrough`). */
145
+ readonly input: IRNode;
146
+ readonly extra: Readonly<Record<string, unknown>>;
147
+ /** What the procedure runs, when the compiler can see it (`client.eval` of a literal path). Only the checks read it. */
148
+ readonly model: IRNode | undefined;
149
+ /** The scope that the call gets (`withScope`). The scopes pass sets it. */
150
+ scope: ScopeSnapshot;
151
+ }
152
+
153
+ /** A step of the interpreter that throws: a hydration limit, or a `fn` that is not a call. */
154
+ export interface FailNode extends NodeBase {
155
+ readonly kind: "fail";
156
+ readonly message: string;
157
+ }
158
+
159
+ interface ControlBase extends NodeBase {
160
+ readonly path: ProcedurePath;
161
+ readonly key: string;
162
+ readonly position: CallPosition;
163
+ }
164
+
165
+ /** One step of a chain. */
166
+ export interface ChainStep {
167
+ readonly pointer: Pointer;
168
+ readonly node: IRNode;
169
+ /** The result of the step: `$last` and `passThrough` read it, and `$name` names it. */
170
+ readonly result: Binding;
171
+ }
172
+
173
+ /** `client.chain`, or an array of calls that runs as one chain (an implicit chain). */
174
+ export interface ChainNode extends ControlBase {
175
+ readonly kind: "chain";
176
+ readonly implicit: boolean;
177
+ readonly steps: readonly ChainStep[];
178
+ readonly passThrough: boolean;
179
+ /** The hydrated `initialInput` when `passThrough` is true. */
180
+ readonly initialInput: IRNode | undefined;
181
+ readonly initial: Binding | undefined;
182
+ }
183
+
184
+ /** `client.parallel`. */
185
+ export interface ParallelNode extends ControlBase {
186
+ readonly kind: "parallel";
187
+ readonly tasks: readonly IRNode[];
188
+ readonly concurrency: number | undefined;
189
+ readonly failFast: boolean;
190
+ }
191
+
192
+ /** `client.conditional`. An absent branch is undefined: the result is undefined, and nothing runs. */
193
+ export interface ConditionalNode extends ControlBase {
194
+ readonly kind: "conditional";
195
+ readonly condition: IRNode;
196
+ readonly then: IRNode | undefined;
197
+ readonly else: IRNode | undefined;
198
+ }
199
+
200
+ /** `client.tryCatch`. */
201
+ export interface TryNode extends ControlBase {
202
+ readonly kind: "try";
203
+ readonly try: IRNode;
204
+ readonly catch: IRNode;
205
+ readonly error: Binding;
206
+ }
207
+
208
+ /** `client.and` and `client.or`. */
209
+ export interface LogicNode extends ControlBase {
210
+ readonly kind: "and" | "or";
211
+ readonly values: readonly IRNode[];
212
+ }
213
+
214
+ /** The `items` of `client.map` and `client.reduce`: an array whose items are hydrated one by one, or one value. */
215
+ export type ItemsSpec =
216
+ | { readonly kind: "list"; readonly items: readonly IRNode[] }
217
+ | { readonly kind: "value"; readonly value: IRNode };
218
+
219
+ /** `client.map`. */
220
+ export interface MapNode extends ControlBase {
221
+ readonly kind: "map";
222
+ readonly items: ItemsSpec;
223
+ readonly fn: IRNode | undefined;
224
+ readonly item: Binding;
225
+ readonly index: Binding;
226
+ }
227
+
228
+ /** `client.reduce`. Without `fn`, the result is the hydrated `initial` (or `accumulated`), and the items do not run. */
229
+ export interface ReduceNode extends ControlBase {
230
+ readonly kind: "reduce";
231
+ readonly items: ItemsSpec | undefined;
232
+ readonly initial: IRNode;
233
+ readonly fn: IRNode | undefined;
234
+ readonly acc: Binding;
235
+ readonly item: Binding;
236
+ readonly index: Binding;
237
+ }
238
+
239
+ /** The nodes of the control flow. */
240
+ export type ControlNode = ChainNode | ParallelNode | ConditionalNode | TryNode | LogicNode | MapNode | ReduceNode;
241
+
242
+ /** A node of the IR. */
243
+ export type IRNode =
244
+ | LiteralNode
245
+ | ObjectNode
246
+ | ArrayNode
247
+ | ReferenceNode
248
+ | CallNode
249
+ | DeferredNode
250
+ | DynamicNode
251
+ | FailNode
252
+ | ControlNode;
253
+
254
+ /** The IR of a program. */
255
+ export interface ProgramIR {
256
+ /** `exec`: the program runs as `client.exec()` runs it. `body`: as the body of a defined procedure. */
257
+ readonly mode: "exec" | "body";
258
+ readonly root: IRNode;
259
+ /** The binding of `input` in the body mode. */
260
+ readonly input: Binding | undefined;
261
+ /** The number of bindings: the ids go from 0 to this number minus one. */
262
+ readonly bindingCount: number;
263
+ /** The program, as the source of the IR. */
264
+ readonly program: unknown;
265
+ }
266
+
267
+ /** True for the nodes that call a procedure (a control-flow node is a call in the interpreter). */
268
+ export function isCallLike(node: IRNode): node is CallNode | DynamicNode | ControlNode {
269
+ return node.kind === "call" || node.kind === "dynamic" || isControl(node);
270
+ }
271
+
272
+ /** True for a control-flow node. */
273
+ export function isControl(node: IRNode): node is ControlNode {
274
+ switch (node.kind) {
275
+ case "chain":
276
+ case "parallel":
277
+ case "conditional":
278
+ case "try":
279
+ case "and":
280
+ case "or":
281
+ case "map":
282
+ case "reduce":
283
+ return true;
284
+ default:
285
+ return false;
286
+ }
287
+ }
288
+
289
+ /** The child nodes of a node, in the order of the interpreter. A dynamic node gives its input, not its model. */
290
+ export function childrenOf(node: IRNode): IRNode[] {
291
+ switch (node.kind) {
292
+ case "literal":
293
+ case "reference":
294
+ case "fail":
295
+ return [];
296
+ case "object":
297
+ return node.entries.map((entry) => entry.value);
298
+ case "array":
299
+ return [...node.items];
300
+ case "call":
301
+ case "deferred":
302
+ case "dynamic":
303
+ return [node.input];
304
+ case "chain":
305
+ return [...(node.initialInput ? [node.initialInput] : []), ...node.steps.map((step) => step.node)];
306
+ case "parallel":
307
+ return [...node.tasks];
308
+ case "conditional":
309
+ return [node.condition, ...(node.then ? [node.then] : []), ...(node.else ? [node.else] : [])];
310
+ case "try":
311
+ return [node.try, node.catch];
312
+ case "and":
313
+ case "or":
314
+ return [...node.values];
315
+ case "map":
316
+ return [...itemsChildren(node.items), ...(node.fn ? [node.fn] : [])];
317
+ case "reduce":
318
+ return [...(node.items ? itemsChildren(node.items) : []), node.initial, ...(node.fn ? [node.fn] : [])];
319
+ }
320
+ }
321
+
322
+ function itemsChildren(items: ItemsSpec): IRNode[] {
323
+ return items.kind === "list" ? [...items.items] : [items.value];
324
+ }
325
+
326
+ /** This function calls `visit` for each node of the tree, parents before children. */
327
+ export function walk(node: IRNode, visit: (node: IRNode) => void): void {
328
+ visit(node);
329
+ for (const child of childrenOf(node)) walk(child, visit);
330
+ }