@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.
- package/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +254 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/checks.d.ts +24 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +123 -0
- package/dist/checks.js.map +1 -0
- package/dist/codegen-closure.d.ts +19 -0
- package/dist/codegen-closure.d.ts.map +1 -0
- package/dist/codegen-closure.js +200 -0
- package/dist/codegen-closure.js.map +1 -0
- package/dist/codegen-text.d.ts +44 -0
- package/dist/codegen-text.d.ts.map +1 -0
- package/dist/codegen-text.js +360 -0
- package/dist/codegen-text.js.map +1 -0
- package/dist/diagnostics.d.ts +67 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +38 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/effects.d.ts +63 -0
- package/dist/effects.d.ts.map +1 -0
- package/dist/effects.js +185 -0
- package/dist/effects.js.map +1 -0
- package/dist/explain.d.ts +58 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +144 -0
- package/dist/explain.js.map +1 -0
- package/dist/facts.d.ts +66 -0
- package/dist/facts.d.ts.map +1 -0
- package/dist/facts.js +102 -0
- package/dist/facts.js.map +1 -0
- package/dist/fold.d.ts +27 -0
- package/dist/fold.d.ts.map +1 -0
- package/dist/fold.js +128 -0
- package/dist/fold.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/interceptor.d.ts +51 -0
- package/dist/interceptor.d.ts.map +1 -0
- package/dist/interceptor.js +116 -0
- package/dist/interceptor.js.map +1 -0
- package/dist/ir.d.ts +240 -0
- package/dist/ir.d.ts.map +1 -0
- package/dist/ir.js +74 -0
- package/dist/ir.js.map +1 -0
- package/dist/mermaid.d.ts +53 -0
- package/dist/mermaid.d.ts.map +1 -0
- package/dist/mermaid.js +423 -0
- package/dist/mermaid.js.map +1 -0
- package/dist/module.d.ts +20 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +124 -0
- package/dist/module.js.map +1 -0
- package/dist/normalize.d.ts +43 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/normalize.js +529 -0
- package/dist/normalize.js.map +1 -0
- package/dist/openrpc.d.ts +63 -0
- package/dist/openrpc.d.ts.map +1 -0
- package/dist/openrpc.js +57 -0
- package/dist/openrpc.js.map +1 -0
- package/dist/print.d.ts +8 -0
- package/dist/print.d.ts.map +1 -0
- package/dist/print.js +148 -0
- package/dist/print.js.map +1 -0
- package/dist/procedures/program/check.d.ts +14 -0
- package/dist/procedures/program/check.d.ts.map +1 -0
- package/dist/procedures/program/check.js +30 -0
- package/dist/procedures/program/check.js.map +1 -0
- package/dist/procedures/program/compile.d.ts +14 -0
- package/dist/procedures/program/compile.d.ts.map +1 -0
- package/dist/procedures/program/compile.js +35 -0
- package/dist/procedures/program/compile.js.map +1 -0
- package/dist/procedures/program/diagram.d.ts +14 -0
- package/dist/procedures/program/diagram.d.ts.map +1 -0
- package/dist/procedures/program/diagram.js +23 -0
- package/dist/procedures/program/diagram.js.map +1 -0
- package/dist/procedures/program/effects-output.d.ts +8 -0
- package/dist/procedures/program/effects-output.d.ts.map +1 -0
- package/dist/procedures/program/effects-output.js +13 -0
- package/dist/procedures/program/effects-output.js.map +1 -0
- package/dist/procedures/program/explain.d.ts +14 -0
- package/dist/procedures/program/explain.d.ts.map +1 -0
- package/dist/procedures/program/explain.js +43 -0
- package/dist/procedures/program/explain.js.map +1 -0
- package/dist/procedures/program/index.d.ts +5 -0
- package/dist/procedures/program/index.d.ts.map +1 -0
- package/dist/procedures/program/index.js +5 -0
- package/dist/procedures/program/index.js.map +1 -0
- package/dist/procedures/sdk/index.d.ts +3 -0
- package/dist/procedures/sdk/index.d.ts.map +1 -0
- package/dist/procedures/sdk/index.js +3 -0
- package/dist/procedures/sdk/index.js.map +1 -0
- package/dist/procedures/sdk/manifest.d.ts +11 -0
- package/dist/procedures/sdk/manifest.d.ts.map +1 -0
- package/dist/procedures/sdk/manifest.js +15 -0
- package/dist/procedures/sdk/manifest.js.map +1 -0
- package/dist/procedures/sdk/openrpc.d.ts +11 -0
- package/dist/procedures/sdk/openrpc.d.ts.map +1 -0
- package/dist/procedures/sdk/openrpc.js +13 -0
- package/dist/procedures/sdk/openrpc.js.map +1 -0
- package/dist/procedures/sdk/typescript.d.ts +11 -0
- package/dist/procedures/sdk/typescript.d.ts.map +1 -0
- package/dist/procedures/sdk/typescript.js +14 -0
- package/dist/procedures/sdk/typescript.js.map +1 -0
- package/dist/program.d.ts +84 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +108 -0
- package/dist/program.js.map +1 -0
- package/dist/refs.d.ts +9 -0
- package/dist/refs.d.ts.map +1 -0
- package/dist/refs.js +9 -0
- package/dist/refs.js.map +1 -0
- package/dist/register.d.ts +6 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +8 -0
- package/dist/register.js.map +1 -0
- package/dist/runtime.d.ts +68 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +138 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts +41 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +325 -0
- package/dist/schema.js.map +1 -0
- package/dist/scopes.d.ts +25 -0
- package/dist/scopes.d.ts.map +1 -0
- package/dist/scopes.js +202 -0
- package/dist/scopes.js.map +1 -0
- package/dist/types.d.ts +129 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +157 -0
- package/dist/types.js.map +1 -0
- package/dist/typescript-sdk.d.ts +34 -0
- package/dist/typescript-sdk.d.ts.map +1 -0
- package/dist/typescript-sdk.js +231 -0
- package/dist/typescript-sdk.js.map +1 -0
- package/package.json +83 -3
- package/src/checks.ts +128 -0
- package/src/codegen-closure.ts +224 -0
- package/src/codegen-text.ts +396 -0
- package/src/diagnostics.ts +98 -0
- package/src/effects.ts +231 -0
- package/src/explain.ts +194 -0
- package/src/facts.ts +161 -0
- package/src/fold.ts +136 -0
- package/src/index.ts +35 -0
- package/src/interceptor.ts +149 -0
- package/src/ir.ts +330 -0
- package/src/mermaid.ts +460 -0
- package/src/module.ts +171 -0
- package/src/normalize.ts +694 -0
- package/src/openrpc.ts +104 -0
- package/src/print.ts +143 -0
- package/src/procedures/program/check.ts +31 -0
- package/src/procedures/program/compile.ts +36 -0
- package/src/procedures/program/diagram.ts +25 -0
- package/src/procedures/program/effects-output.ts +16 -0
- package/src/procedures/program/explain.ts +46 -0
- package/src/procedures/program/index.ts +4 -0
- package/src/procedures/sdk/index.ts +2 -0
- package/src/procedures/sdk/manifest.ts +17 -0
- package/src/procedures/sdk/openrpc.ts +16 -0
- package/src/procedures/sdk/typescript.ts +17 -0
- package/src/program.ts +181 -0
- package/src/refs.ts +9 -0
- package/src/register.ts +10 -0
- package/src/runtime.ts +188 -0
- package/src/schema.ts +318 -0
- package/src/scopes.ts +196 -0
- package/src/types.ts +318 -0
- package/src/typescript-sdk.ts +282 -0
package/src/normalize.ts
ADDED
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The normalize pass: the JSON of a program to the IR.
|
|
3
|
+
*
|
|
4
|
+
* The pass follows the interpreter of `@procedures/client` step by step:
|
|
5
|
+
*
|
|
6
|
+
* - `hydrateValue()` and `hydrateRef()` of `ref.ts` for a value and a call in an input.
|
|
7
|
+
* - `prepareRootCall()` for the root call (`client.exec()` and the body of a defined procedure).
|
|
8
|
+
* - `evaluateOperand()` and `callOperand()` of `core/operands.ts` for an operand of control flow.
|
|
9
|
+
* - The handlers of `core/index.ts` for each control-flow procedure.
|
|
10
|
+
*
|
|
11
|
+
* Each rule of the interpreter has its place here. The rules are the order of the keys, an
|
|
12
|
+
* implicit chain, the raw-input rule, `$when` and `$literal`. They also include a `$ref` with
|
|
13
|
+
* and without a scope, the hydration limits and the parent context (`cwd`, `node`) of an operand.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
getRefWhen,
|
|
18
|
+
isAnyProcedureRef,
|
|
19
|
+
isControlFlowPath,
|
|
20
|
+
isLiteral,
|
|
21
|
+
isPlainObject,
|
|
22
|
+
normalizeRef,
|
|
23
|
+
pathToKey,
|
|
24
|
+
shouldExecuteRef,
|
|
25
|
+
MAX_DATA_DEPTH,
|
|
26
|
+
PURE_EFFECTS,
|
|
27
|
+
WHEN_NEVER,
|
|
28
|
+
type AnyProcedureRef,
|
|
29
|
+
type ProcedureEntry,
|
|
30
|
+
type ProcedurePath,
|
|
31
|
+
} from "@procedures/client/browser";
|
|
32
|
+
import { childPointer, type Binding, type BindingKind, type CallPosition, type ChainStep, type DynamicReason, type IRNode, type ItemsSpec, type LiteralNode, type Pointer, type ProgramIR, type ReferenceNode } from "./ir.js";
|
|
33
|
+
import { isStringPath, type Definition, type ProcedureFacts } from "./facts.js";
|
|
34
|
+
import { isOutputRef } from "./refs.js";
|
|
35
|
+
import type { Diagnostic, DiagnosticCode, Severity } from "./diagnostics.js";
|
|
36
|
+
|
|
37
|
+
/** The default limit of nested calls in one hydration (`HydrateOptions.maxDepth` of the client). */
|
|
38
|
+
export const MAX_REF_DEPTH = 10;
|
|
39
|
+
|
|
40
|
+
/** The message of `client.exec()` for a program that is not a call. */
|
|
41
|
+
export const ROOT_NOT_CALL_MESSAGE = "exec() requires a ProcedureRef, $proc JSON, or procedure path array";
|
|
42
|
+
|
|
43
|
+
/** The objects on the path from the root of one hydration to a value (to find a cycle). */
|
|
44
|
+
interface Ancestors {
|
|
45
|
+
readonly value: object;
|
|
46
|
+
readonly up: Ancestors | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The state of one hydration, as `HydrateContext` of the client has it. */
|
|
50
|
+
interface Hydration {
|
|
51
|
+
/** True when the hydration has a scope: then a `$ref` reads it. */
|
|
52
|
+
readonly scoped: boolean;
|
|
53
|
+
readonly refDepth: number;
|
|
54
|
+
readonly skipImplicitChain: boolean;
|
|
55
|
+
readonly deferred: boolean;
|
|
56
|
+
readonly dataDepth: number;
|
|
57
|
+
readonly ancestors: Ancestors | undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const NO_EXTRA: Readonly<Record<string, unknown>> = Object.freeze({});
|
|
61
|
+
|
|
62
|
+
/** The options of the pass. */
|
|
63
|
+
export interface NormalizeOptions {
|
|
64
|
+
readonly mode: "exec" | "body";
|
|
65
|
+
/** The pointer of the program in a bigger document (the body of a definition). */
|
|
66
|
+
readonly base?: Pointer | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The result of the pass. */
|
|
70
|
+
export interface NormalizeResult {
|
|
71
|
+
readonly ir: ProgramIR;
|
|
72
|
+
readonly diagnostics: Diagnostic[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** This function makes the IR of a program. */
|
|
76
|
+
export function normalize(program: unknown, facts: ProcedureFacts, options: NormalizeOptions): NormalizeResult {
|
|
77
|
+
const normalizer = new Normalizer(facts);
|
|
78
|
+
const ir = normalizer.program(program, options.mode, options.base ?? "");
|
|
79
|
+
return { ir, diagnostics: normalizer.diagnostics };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function hasKeys(value: Readonly<Record<string, unknown>>): boolean {
|
|
83
|
+
return Object.keys(value).length > 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The input of an operand call with the parent context: `callOperand()` of the client. */
|
|
87
|
+
export function mergeExtra(input: unknown, extra: Readonly<Record<string, unknown>>): unknown {
|
|
88
|
+
if (!hasKeys(extra)) return input;
|
|
89
|
+
return { ...(typeof input === "object" && input !== null && !Array.isArray(input) ? input : {}), ...extra };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The parent context that a control-flow procedure gives to its operands: `parentContext()` of the client. */
|
|
93
|
+
function parentContext(input: Record<string, unknown>): Readonly<Record<string, unknown>> {
|
|
94
|
+
const cwd = input["cwd"];
|
|
95
|
+
const node = input["node"];
|
|
96
|
+
if (!cwd && !node) return NO_EXTRA;
|
|
97
|
+
return { ...(cwd ? { cwd } : {}), ...(node ? { node } : {}) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function typeName(value: unknown): string {
|
|
101
|
+
if (value === null) return "null";
|
|
102
|
+
if (Array.isArray(value)) return "array";
|
|
103
|
+
return typeof value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The text of a path for a message. */
|
|
107
|
+
function keyText(path: readonly unknown[]): string {
|
|
108
|
+
return path.map((segment) => String(segment)).join(".");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The options of an operand call. */
|
|
112
|
+
interface OperandOptions {
|
|
113
|
+
/** The value that `passThrough` gives: it replaces the input. */
|
|
114
|
+
readonly passed?: ReferenceNode | undefined;
|
|
115
|
+
/** True for a chain step: the chain binds its `$name`. */
|
|
116
|
+
readonly bindsName?: boolean | undefined;
|
|
117
|
+
/** The text of a `$never` trap: the meaning of a `$never` call at this place. */
|
|
118
|
+
readonly trap?: string | undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
class Normalizer {
|
|
122
|
+
readonly diagnostics: Diagnostic[] = [];
|
|
123
|
+
private bindingCount = 0;
|
|
124
|
+
/** The control-flow calls on the path (a call object that contains itself through its operands). */
|
|
125
|
+
private readonly flowStack = new Set<object>();
|
|
126
|
+
|
|
127
|
+
constructor(private readonly facts: ProcedureFacts) {}
|
|
128
|
+
|
|
129
|
+
program(program: unknown, mode: "exec" | "body", base: Pointer): ProgramIR {
|
|
130
|
+
if (mode === "body") {
|
|
131
|
+
const input = this.binding("input", "input", base);
|
|
132
|
+
const root = isAnyProcedureRef(program) ? this.rootCall(program, true, base) : this.hydrateRoot(program, true, base);
|
|
133
|
+
return { mode, root, input, bindingCount: this.bindingCount, program };
|
|
134
|
+
}
|
|
135
|
+
let root: IRNode;
|
|
136
|
+
if (isAnyProcedureRef(program)) {
|
|
137
|
+
root = this.rootCall(program, false, base);
|
|
138
|
+
} else if (Array.isArray(program) && program.every((segment) => typeof segment === "string")) {
|
|
139
|
+
// client.exec(path) starts the procedure with the input {}
|
|
140
|
+
root = this.rootCall({ $proc: program, input: {} }, false, base);
|
|
141
|
+
} else {
|
|
142
|
+
this.report("error", "root-not-call", "The program must be a call ({ $proc, input }): client.exec() starts only a call.", base);
|
|
143
|
+
root = { kind: "fail", pointer: base, message: ROOT_NOT_CALL_MESSAGE };
|
|
144
|
+
}
|
|
145
|
+
return { mode, root, input: undefined, bindingCount: this.bindingCount, program };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ===========================================================================
|
|
149
|
+
// Helpers
|
|
150
|
+
// ===========================================================================
|
|
151
|
+
|
|
152
|
+
private binding(kind: BindingKind, name: string | undefined, pointer: Pointer): Binding {
|
|
153
|
+
return { id: this.bindingCount++, kind, name, pointer };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private report(severity: Severity, code: DiagnosticCode, message: string, pointer: Pointer): void {
|
|
157
|
+
this.diagnostics.push({ severity, code, message, pointer });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private literal(value: unknown, origin: LiteralNode["origin"], pointer: Pointer): LiteralNode {
|
|
161
|
+
return { kind: "literal", value, origin, pointer };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private fail(code: DiagnosticCode, message: string, pointer: Pointer, explanation: string): IRNode {
|
|
165
|
+
this.report("error", code, explanation, pointer);
|
|
166
|
+
return { kind: "fail", message, pointer };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** This function reports a path that cannot name a procedure. */
|
|
170
|
+
private checkPath(path: readonly unknown[], pointer: Pointer): void {
|
|
171
|
+
if (path.length === 0) {
|
|
172
|
+
this.report("error", "invalid-path", "The $proc path is empty.", pointer);
|
|
173
|
+
} else if (!isStringPath(path) || path.some((segment) => segment.length === 0)) {
|
|
174
|
+
this.report("error", "invalid-path", `The $proc path ${JSON.stringify(path)} must have strings that are not empty.`, pointer);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private warnName(ref: AnyProcedureRef, pointer: Pointer, where: string): void {
|
|
179
|
+
const name = normalizeRef(ref).$name;
|
|
180
|
+
if (name !== undefined && name !== "") {
|
|
181
|
+
this.report(
|
|
182
|
+
"warning",
|
|
183
|
+
"name-not-bound",
|
|
184
|
+
`The $name "${String(name)}" names ${where}. Only a step of client.chain puts its $name in the scope, so no $ref can read this name.`,
|
|
185
|
+
pointer,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private synthetic(binding: Binding, pointer: Pointer): ReferenceNode {
|
|
191
|
+
return { kind: "reference", text: "$passed", name: "$passed", fields: [], deferred: false, source: undefined, synthetic: true, binding, pointer };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ===========================================================================
|
|
195
|
+
// The root call and the hydration (ref.ts)
|
|
196
|
+
// ===========================================================================
|
|
197
|
+
|
|
198
|
+
/** `prepareRootCall()`: the root call starts also with `$when`, and its input is a hydration root. */
|
|
199
|
+
private rootCall(ref: AnyProcedureRef, scoped: boolean, pointer: Pointer): IRNode {
|
|
200
|
+
const normalized = normalizeRef(ref);
|
|
201
|
+
const path = normalized.path as readonly unknown[] as ProcedurePath;
|
|
202
|
+
this.checkPath(path, pointer);
|
|
203
|
+
this.warnName(ref, pointer, "the root call");
|
|
204
|
+
if (this.facts.rawInput(path)) {
|
|
205
|
+
return this.rawCall(ref, path, normalized.input, "root", scoped, NO_EXTRA, pointer, false);
|
|
206
|
+
}
|
|
207
|
+
const input = this.hydrateRoot(normalized.input, scoped, childPointer(pointer, "input"));
|
|
208
|
+
return { kind: "call", path, key: keyText(path), position: "root", input, extra: NO_EXTRA, name: undefined, pointer };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** `hydrateInput()`: a new hydration, at depth 0, where an array of calls is an implicit chain. */
|
|
212
|
+
private hydrateRoot(value: unknown, scoped: boolean, pointer: Pointer): IRNode {
|
|
213
|
+
return this.value(value, { scoped, refDepth: 0, skipImplicitChain: false, deferred: false, dataDepth: 0, ancestors: undefined }, pointer);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** `hydrateValue()`. */
|
|
217
|
+
private value(value: unknown, h: Hydration, pointer: Pointer): IRNode {
|
|
218
|
+
if (value === null || typeof value !== "object") return this.literal(value, "data", pointer);
|
|
219
|
+
if (h.dataDepth > MAX_DATA_DEPTH) {
|
|
220
|
+
return this.fail(
|
|
221
|
+
"data-depth",
|
|
222
|
+
`Hydration failed: the input has more than ${MAX_DATA_DEPTH} levels of nesting`,
|
|
223
|
+
pointer,
|
|
224
|
+
`The data nests deeper than ${MAX_DATA_DEPTH} levels: the hydration stops here with an error.`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
for (let current = h.ancestors; current; current = current.up) {
|
|
228
|
+
if (current.value === value) {
|
|
229
|
+
return this.fail(
|
|
230
|
+
"cyclic-input",
|
|
231
|
+
"Hydration failed: the input is cyclic (an object contains itself)",
|
|
232
|
+
pointer,
|
|
233
|
+
"The input contains itself: the hydration stops here with an error.",
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (isLiteral(value)) return this.literal(value.$literal, "literal", pointer);
|
|
238
|
+
if (isOutputRef(value)) {
|
|
239
|
+
if (!h.scoped) {
|
|
240
|
+
this.report(
|
|
241
|
+
"warning",
|
|
242
|
+
"unscoped-ref",
|
|
243
|
+
`The $ref "${value.$ref}" is outside each scope, so it is data (for example a JSON Schema reference). A name can be read only in a chain step, map, reduce, the catch of tryCatch or a defined body. Write { "$literal": ... } for data.`,
|
|
244
|
+
pointer,
|
|
245
|
+
);
|
|
246
|
+
return this.literal(value, "unscoped-ref", pointer);
|
|
247
|
+
}
|
|
248
|
+
const [name = "", ...fields] = value.$ref.split(".");
|
|
249
|
+
return { kind: "reference", text: value.$ref, name, fields, deferred: h.deferred, source: value, synthetic: false, binding: undefined, pointer };
|
|
250
|
+
}
|
|
251
|
+
if (isAnyProcedureRef(value)) return this.ref(value, h, pointer);
|
|
252
|
+
|
|
253
|
+
const inner: Ancestors = { value, up: h.ancestors };
|
|
254
|
+
const child: Hydration = { ...h, ancestors: inner, dataDepth: h.dataDepth + 1 };
|
|
255
|
+
if (Array.isArray(value)) {
|
|
256
|
+
if (!h.skipImplicitChain && value.length > 0 && value.every((item) => isAnyProcedureRef(item))) {
|
|
257
|
+
if (value.every((item) => shouldExecuteRef(item as AnyProcedureRef, [], false))) {
|
|
258
|
+
return this.implicitChain(value, h, pointer);
|
|
259
|
+
}
|
|
260
|
+
const waiting = value.findIndex((item) => !shouldExecuteRef(item as AnyProcedureRef, [], false));
|
|
261
|
+
this.report(
|
|
262
|
+
"warning",
|
|
263
|
+
"never-trap",
|
|
264
|
+
`This array of calls is a list, not a chain: the call at ${childPointer(pointer, waiting)} waits ($when ${JSON.stringify(getRefWhen(value[waiting] as AnyProcedureRef))}). The other calls start as items of the list, without $name and $last.`,
|
|
265
|
+
pointer,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
const items = Array.from(value, (item: unknown, index) => this.value(item, child, childPointer(pointer, index)));
|
|
269
|
+
if (items.every((node, index) => node.kind === "literal" && node.value === value[index])) {
|
|
270
|
+
return this.literal(value, "data", pointer);
|
|
271
|
+
}
|
|
272
|
+
return { kind: "array", items, pointer };
|
|
273
|
+
}
|
|
274
|
+
if (!isPlainObject(value)) return this.literal(value, "data", pointer);
|
|
275
|
+
const entries = Object.entries(value).map(([key, item]) => ({ key, value: this.value(item, child, childPointer(pointer, key)), source: item }));
|
|
276
|
+
if (entries.every((entry) => entry.value.kind === "literal" && entry.value.value === entry.source)) {
|
|
277
|
+
return this.literal(value, "data", pointer);
|
|
278
|
+
}
|
|
279
|
+
return { kind: "object", entries: entries.map(({ key, value: node }) => ({ key, value: node })), pointer };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** `hydrateRef()`: a call in an input. */
|
|
283
|
+
private ref(value: AnyProcedureRef, h: Hydration, pointer: Pointer): IRNode {
|
|
284
|
+
const when = getRefWhen(value);
|
|
285
|
+
if (when === WHEN_NEVER) return this.literal(value, "never", pointer);
|
|
286
|
+
if (h.refDepth >= MAX_REF_DEPTH) {
|
|
287
|
+
return this.fail(
|
|
288
|
+
"depth-limit",
|
|
289
|
+
`Hydration depth exceeded maximum of ${MAX_REF_DEPTH}`,
|
|
290
|
+
pointer,
|
|
291
|
+
`Calls nest more than ${MAX_REF_DEPTH} levels in one input here: the hydration stops with an error. A chain step or another operand starts a new count.`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
const ref = normalizeRef(value);
|
|
295
|
+
const path = ref.path as readonly unknown[] as ProcedurePath;
|
|
296
|
+
this.checkPath(path, pointer);
|
|
297
|
+
const raw = this.facts.rawInput(path);
|
|
298
|
+
const inner: Ancestors = { value, up: h.ancestors };
|
|
299
|
+
const inputPointer = childPointer(pointer, "input");
|
|
300
|
+
if (!shouldExecuteRef(value, [], false)) {
|
|
301
|
+
this.report(
|
|
302
|
+
"info",
|
|
303
|
+
"deferred-call",
|
|
304
|
+
`This call waits for its runner ($when ${JSON.stringify(when)}): hydration keeps it as data for the procedure that gets it.`,
|
|
305
|
+
pointer,
|
|
306
|
+
);
|
|
307
|
+
if (raw) return this.literal(value, "deferred", pointer);
|
|
308
|
+
const input = this.value(ref.input, { ...h, skipImplicitChain: true, deferred: true, refDepth: h.refDepth + 1, ancestors: inner, dataDepth: h.dataDepth + 1 }, inputPointer);
|
|
309
|
+
if (input.kind === "literal" && input.value === ref.input) return this.literal(value, "deferred", pointer);
|
|
310
|
+
return { kind: "deferred", path, key: keyText(path), when, source: value as unknown as Record<string, unknown>, input, pointer };
|
|
311
|
+
}
|
|
312
|
+
this.warnName(value, pointer, "a call in an input");
|
|
313
|
+
if (raw) return this.rawCall(value, path, ref.input, "nested", h.scoped, NO_EXTRA, pointer, false);
|
|
314
|
+
const input = this.value(ref.input, { ...h, skipImplicitChain: true, deferred: false, refDepth: h.refDepth + 1, ancestors: inner, dataDepth: h.dataDepth + 1 }, inputPointer);
|
|
315
|
+
return { kind: "call", path, key: keyText(path), position: "nested", input, extra: NO_EXTRA, name: undefined, pointer };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** An array of calls that start during this hydration: `client.chain` gets it raw as its steps. */
|
|
319
|
+
private implicitChain(steps: unknown[], h: Hydration, pointer: Pointer): IRNode {
|
|
320
|
+
if (h.refDepth >= MAX_REF_DEPTH) {
|
|
321
|
+
return this.fail(
|
|
322
|
+
"depth-limit",
|
|
323
|
+
`Hydration depth exceeded maximum of ${MAX_REF_DEPTH}`,
|
|
324
|
+
pointer,
|
|
325
|
+
`Calls nest more than ${MAX_REF_DEPTH} levels in one input here: the hydration stops with an error.`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
this.report(
|
|
329
|
+
"info",
|
|
330
|
+
"implicit-chain",
|
|
331
|
+
"This array of calls runs as one client.chain: its value is { results, final }, and the steps can use $name and $last.",
|
|
332
|
+
pointer,
|
|
333
|
+
);
|
|
334
|
+
const ref = { $proc: ["client", "chain"], input: { steps } };
|
|
335
|
+
return this.rawCall(ref, ["client", "chain"], ref.input, "nested", h.scoped, NO_EXTRA, pointer, true);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ===========================================================================
|
|
339
|
+
// Operands (core/operands.ts)
|
|
340
|
+
// ===========================================================================
|
|
341
|
+
|
|
342
|
+
/** `evaluateOperand()`: a call starts as an operand call, and another value is a new hydration. */
|
|
343
|
+
private operand(value: unknown, scoped: boolean, extra: Readonly<Record<string, unknown>>, pointer: Pointer, trap?: string): IRNode {
|
|
344
|
+
if (isAnyProcedureRef(value)) return this.operandCall(value, scoped, extra, pointer, { trap });
|
|
345
|
+
return this.hydrateRoot(value, scoped, pointer);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** `callOperand()`: only `$never` holds the call back, and its input is a new hydration. */
|
|
349
|
+
private operandCall(ref: AnyProcedureRef, scoped: boolean, extra: Readonly<Record<string, unknown>>, pointer: Pointer, options: OperandOptions = {}): IRNode {
|
|
350
|
+
if (getRefWhen(ref) === WHEN_NEVER) {
|
|
351
|
+
if (options.trap) this.report("warning", "never-trap", options.trap, pointer);
|
|
352
|
+
return this.literal(ref, "never", pointer);
|
|
353
|
+
}
|
|
354
|
+
const normalized = normalizeRef(ref);
|
|
355
|
+
const path = normalized.path as readonly unknown[] as ProcedurePath;
|
|
356
|
+
this.checkPath(path, pointer);
|
|
357
|
+
if (!options.bindsName) this.warnName(ref, pointer, "an operand that is not a chain step");
|
|
358
|
+
const inputPointer = childPointer(pointer, "input");
|
|
359
|
+
if (this.facts.rawInput(path)) {
|
|
360
|
+
if (options.passed) {
|
|
361
|
+
// passThrough gives the step the input { $literal: <the value> }, also when the procedure takes it raw
|
|
362
|
+
const input: IRNode = { kind: "object", entries: [{ key: "$literal", value: options.passed }], pointer: inputPointer };
|
|
363
|
+
return this.dynamic(path, "operand", this.rawReason(path), input, extra, undefined, pointer);
|
|
364
|
+
}
|
|
365
|
+
return this.rawCall(ref, path, normalized.input, "operand", scoped, extra, pointer, false);
|
|
366
|
+
}
|
|
367
|
+
const input = options.passed ?? this.hydrateRoot(normalized.input, scoped, inputPointer);
|
|
368
|
+
if (hasKeys(extra) && input.kind === "literal" && (typeof input.value !== "object" || input.value === null || Array.isArray(input.value))) {
|
|
369
|
+
this.report(
|
|
370
|
+
"warning",
|
|
371
|
+
"input-replaced",
|
|
372
|
+
`Control flow adds ${Object.keys(extra).join(" and ")} to the input of this call. The input is ${typeName(input.value)}, not an object, so the call gets only { ${Object.keys(extra).join(", ")} }.`,
|
|
373
|
+
inputPointer,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
const name = typeof normalized.$name === "string" && normalized.$name !== "" ? normalized.$name : undefined;
|
|
377
|
+
return { kind: "call", path, key: keyText(path), position: "operand", input, extra, name, pointer };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private rawReason(path: ProcedurePath): DynamicReason {
|
|
381
|
+
return (this.facts.entry(path)?.tags ?? []).includes("runs-refs") || isControlFlowPath(path) ? "runs-refs" : "raw-input";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private dynamic(
|
|
385
|
+
path: ProcedurePath,
|
|
386
|
+
position: CallPosition,
|
|
387
|
+
reason: DynamicReason,
|
|
388
|
+
input: IRNode,
|
|
389
|
+
extra: Readonly<Record<string, unknown>>,
|
|
390
|
+
model: IRNode | undefined,
|
|
391
|
+
pointer: Pointer,
|
|
392
|
+
): IRNode {
|
|
393
|
+
return { kind: "dynamic", path, key: keyText(path), position, reason, input, extra, model, scope: undefined, pointer };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* A call whose procedure takes its input raw. The compiler models the core control-flow
|
|
398
|
+
* procedures. It calls the other procedures with the raw input (a dynamic node).
|
|
399
|
+
*/
|
|
400
|
+
private rawCall(
|
|
401
|
+
ref: unknown,
|
|
402
|
+
path: ProcedurePath,
|
|
403
|
+
rawInput: unknown,
|
|
404
|
+
position: CallPosition,
|
|
405
|
+
scoped: boolean,
|
|
406
|
+
extra: Readonly<Record<string, unknown>>,
|
|
407
|
+
pointer: Pointer,
|
|
408
|
+
implicit: boolean,
|
|
409
|
+
): IRNode {
|
|
410
|
+
const inputPointer = implicit ? pointer : childPointer(pointer, "input");
|
|
411
|
+
let reason: DynamicReason = this.rawReason(path);
|
|
412
|
+
if (isControlFlowPath(path)) {
|
|
413
|
+
if (!this.facts.core(path)) {
|
|
414
|
+
reason = "not-core";
|
|
415
|
+
} else if (typeof ref === "object" && ref !== null && this.flowStack.has(ref)) {
|
|
416
|
+
reason = "cyclic";
|
|
417
|
+
} else {
|
|
418
|
+
const effective = position === "operand" ? mergeExtra(rawInput, extra) : rawInput;
|
|
419
|
+
const at = (key: string): Pointer => (implicit && key === "steps" ? pointer : childPointer(inputPointer, key));
|
|
420
|
+
if (typeof ref === "object" && ref !== null) this.flowStack.add(ref);
|
|
421
|
+
try {
|
|
422
|
+
const node = this.controlFlow(path[1]!, effective, scoped, position, pointer, implicit, at);
|
|
423
|
+
if (node) return node;
|
|
424
|
+
} finally {
|
|
425
|
+
if (typeof ref === "object" && ref !== null) this.flowStack.delete(ref);
|
|
426
|
+
}
|
|
427
|
+
reason = "invalid-control-flow";
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const model = this.modelOf(path, mergeExtra(rawInput, position === "operand" ? extra : NO_EXTRA), scoped, pointer);
|
|
431
|
+
return this.dynamic(path, position, reason, this.literal(rawInput, "data", inputPointer), position === "operand" ? extra : NO_EXTRA, model, pointer);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** What `client.eval` runs, when its `procedure` is a literal path or call: one operand call in the scope of the caller. */
|
|
435
|
+
private modelOf(path: ProcedurePath, input: unknown, scoped: boolean, pointer: Pointer): IRNode | undefined {
|
|
436
|
+
if (keyText(path) !== "client.eval" || !this.facts.core(path) || !isPlainObject(input)) return undefined;
|
|
437
|
+
const procedure = input["procedure"];
|
|
438
|
+
const evalInput = input["input"];
|
|
439
|
+
const at = childPointer(pointer, "input");
|
|
440
|
+
if (Array.isArray(procedure) && procedure.every((segment) => typeof segment === "string")) {
|
|
441
|
+
return this.operandCall({ $proc: procedure, input: evalInput }, scoped, NO_EXTRA, at, { bindsName: true });
|
|
442
|
+
}
|
|
443
|
+
if (isAnyProcedureRef(procedure) && !isOutputRef(procedure)) {
|
|
444
|
+
const target = normalizeRef(procedure);
|
|
445
|
+
const refInput = target.input;
|
|
446
|
+
const finalInput =
|
|
447
|
+
evalInput === undefined ? refInput : isPlainObject(refInput) && isPlainObject(evalInput) ? { ...refInput, ...evalInput } : evalInput;
|
|
448
|
+
return this.operandCall({ $proc: target.path, input: finalInput }, scoped, NO_EXTRA, at, { bindsName: true });
|
|
449
|
+
}
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ===========================================================================
|
|
454
|
+
// The control-flow procedures (core/index.ts)
|
|
455
|
+
// ===========================================================================
|
|
456
|
+
|
|
457
|
+
private invalid(name: string, problem: string, pointer: Pointer): undefined {
|
|
458
|
+
this.report("error", "invalid-control-flow", `The input of client.${name} is not valid: ${problem}. The call fails with a validation error.`, pointer);
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private controlFlow(
|
|
463
|
+
name: string,
|
|
464
|
+
input: unknown,
|
|
465
|
+
scoped: boolean,
|
|
466
|
+
position: CallPosition,
|
|
467
|
+
pointer: Pointer,
|
|
468
|
+
implicit: boolean,
|
|
469
|
+
at: (key: string) => Pointer,
|
|
470
|
+
): IRNode | undefined {
|
|
471
|
+
if (!isPlainObject(input)) return this.invalid(name, `the input is ${typeName(input)}, not an object`, pointer);
|
|
472
|
+
const extra = parentContext(input);
|
|
473
|
+
const base = { path: ["client", name], key: `client.${name}`, position, pointer };
|
|
474
|
+
switch (name) {
|
|
475
|
+
case "chain":
|
|
476
|
+
return this.chain(input, extra, base, implicit, at);
|
|
477
|
+
case "parallel": {
|
|
478
|
+
const tasks = input["tasks"];
|
|
479
|
+
if (!Array.isArray(tasks)) return this.invalid(name, "tasks must be an array", pointer);
|
|
480
|
+
const concurrency = input["concurrency"];
|
|
481
|
+
if (concurrency !== undefined && !(typeof concurrency === "number" && Number.isSafeInteger(concurrency) && concurrency >= 1)) {
|
|
482
|
+
return this.invalid(name, "concurrency must be an integer of 1 or more", pointer);
|
|
483
|
+
}
|
|
484
|
+
const failFast = input["failFast"];
|
|
485
|
+
if (failFast !== undefined && typeof failFast !== "boolean") return this.invalid(name, "failFast must be a boolean", pointer);
|
|
486
|
+
const nodes = Array.from(tasks, (task: unknown, index) =>
|
|
487
|
+
this.operand(task, scoped, extra, childPointer(at("tasks"), index), "A $never task gives the call itself as its result: it does not start."),
|
|
488
|
+
);
|
|
489
|
+
return { kind: "parallel", ...base, tasks: nodes, concurrency: concurrency as number | undefined, failFast: failFast === true };
|
|
490
|
+
}
|
|
491
|
+
case "conditional": {
|
|
492
|
+
const condition = this.operand(input["condition"], scoped, extra, at("condition"), "A $never condition is the call object, so the condition is always true.");
|
|
493
|
+
const branch = (key: "then" | "else"): IRNode | undefined =>
|
|
494
|
+
input[key] === undefined ? undefined : this.operand(input[key], scoped, extra, at(key), `A $never ${key} branch gives the call itself: it does not start.`);
|
|
495
|
+
return { kind: "conditional", ...base, condition, then: branch("then"), else: branch("else") };
|
|
496
|
+
}
|
|
497
|
+
case "tryCatch": {
|
|
498
|
+
const tried = this.operand(input["try"], scoped, NO_EXTRA, at("try"), "A $never try operand gives the call itself: it does not start, so it never fails.");
|
|
499
|
+
const error = this.binding("error", "error", at("catch"));
|
|
500
|
+
const caught = this.operand(input["catch"], true, NO_EXTRA, at("catch"), "A $never catch operand gives the call itself: it does not start.");
|
|
501
|
+
return { kind: "try", ...base, try: tried, catch: caught, error };
|
|
502
|
+
}
|
|
503
|
+
case "and":
|
|
504
|
+
case "or": {
|
|
505
|
+
const values = input["values"];
|
|
506
|
+
if (!Array.isArray(values)) return this.invalid(name, "values must be an array", pointer);
|
|
507
|
+
const truth = name === "and" ? "truthy, so client.and goes on" : "truthy, so client.or stops there";
|
|
508
|
+
const nodes = Array.from(values, (value: unknown, index) =>
|
|
509
|
+
this.operand(value, scoped, extra, childPointer(at("values"), index), `A $never operand is the call object: it is ${truth}.`),
|
|
510
|
+
);
|
|
511
|
+
return { kind: name, ...base, values: nodes };
|
|
512
|
+
}
|
|
513
|
+
case "map":
|
|
514
|
+
return this.map(input, scoped, base, at);
|
|
515
|
+
case "reduce":
|
|
516
|
+
return this.reduce(input, scoped, base, at);
|
|
517
|
+
default:
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private chain(
|
|
523
|
+
input: Record<string, unknown>,
|
|
524
|
+
extra: Readonly<Record<string, unknown>>,
|
|
525
|
+
base: { path: ProcedurePath; key: string; position: CallPosition; pointer: Pointer },
|
|
526
|
+
implicit: boolean,
|
|
527
|
+
at: (key: string) => Pointer,
|
|
528
|
+
): IRNode | undefined {
|
|
529
|
+
const steps = input["steps"];
|
|
530
|
+
if (!Array.isArray(steps)) return this.invalid("chain", "steps must be an array", base.pointer);
|
|
531
|
+
const passThrough = input["passThrough"];
|
|
532
|
+
if (passThrough !== undefined && typeof passThrough !== "boolean") return this.invalid("chain", "passThrough must be a boolean", base.pointer);
|
|
533
|
+
const initial = passThrough ? this.binding("initial", undefined, at("initialInput")) : undefined;
|
|
534
|
+
const initialInput = passThrough ? this.hydrateRoot(input["initialInput"], true, at("initialInput")) : undefined;
|
|
535
|
+
const result: ChainStep[] = [];
|
|
536
|
+
const names = new Set<string>();
|
|
537
|
+
Array.from(steps).forEach((step: unknown, index) => {
|
|
538
|
+
const stepPointer = childPointer(at("steps"), index);
|
|
539
|
+
let node: IRNode;
|
|
540
|
+
let name: string | undefined;
|
|
541
|
+
if (isAnyProcedureRef(step)) {
|
|
542
|
+
const normalized = normalizeRef(step);
|
|
543
|
+
if (getRefWhen(step) === WHEN_NEVER) {
|
|
544
|
+
this.report("warning", "never-trap", "A $never step gives the call itself as its result: it does not start.", stepPointer);
|
|
545
|
+
node = this.literal(step, "never", stepPointer);
|
|
546
|
+
} else {
|
|
547
|
+
const usesPassed = passThrough === true && normalized.input === undefined;
|
|
548
|
+
const previous = index === 0 ? initial : result[index - 1]?.result;
|
|
549
|
+
const passed = usesPassed && previous ? this.synthetic(previous, childPointer(stepPointer, "input")) : undefined;
|
|
550
|
+
node = this.operandCall(step, true, extra, stepPointer, { passed, bindsName: true });
|
|
551
|
+
}
|
|
552
|
+
const stepName = normalized.$name as unknown;
|
|
553
|
+
if (typeof stepName === "string" && stepName !== "") {
|
|
554
|
+
name = stepName;
|
|
555
|
+
} else if (stepName) {
|
|
556
|
+
this.report("warning", "name-not-bound", `The $name ${JSON.stringify(stepName)} is not a string, so no $ref can read it.`, stepPointer);
|
|
557
|
+
}
|
|
558
|
+
} else {
|
|
559
|
+
node = this.hydrateRoot(step, true, stepPointer);
|
|
560
|
+
}
|
|
561
|
+
if (name !== undefined) {
|
|
562
|
+
if (names.has(name)) {
|
|
563
|
+
this.report("warning", "duplicate-name", `An earlier step has the $name "${name}" too. From this step, $ref "${name}" reads this result.`, stepPointer);
|
|
564
|
+
}
|
|
565
|
+
names.add(name);
|
|
566
|
+
}
|
|
567
|
+
result.push({ pointer: stepPointer, node, result: this.binding("step", name, stepPointer) });
|
|
568
|
+
});
|
|
569
|
+
return { kind: "chain", ...base, implicit, steps: result, passThrough: passThrough === true, initialInput, initial };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private items(value: unknown, scoped: boolean, pointer: Pointer): ItemsSpec {
|
|
573
|
+
if (Array.isArray(value)) {
|
|
574
|
+
return { kind: "list", items: Array.from(value, (item: unknown, index) => this.hydrateRoot(item, scoped, childPointer(pointer, index))) };
|
|
575
|
+
}
|
|
576
|
+
return { kind: "value", value: this.hydrateRoot(value, scoped, pointer) };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private map(
|
|
580
|
+
input: Record<string, unknown>,
|
|
581
|
+
scoped: boolean,
|
|
582
|
+
base: { path: ProcedurePath; key: string; position: CallPosition; pointer: Pointer },
|
|
583
|
+
at: (key: string) => Pointer,
|
|
584
|
+
): IRNode | undefined {
|
|
585
|
+
const as = input["as"];
|
|
586
|
+
if (as !== undefined && typeof as !== "string") return this.invalid("map", "as must be a string", base.pointer);
|
|
587
|
+
const fn = input["fn"];
|
|
588
|
+
if (fn !== undefined && !isAnyProcedureRef(fn)) {
|
|
589
|
+
return this.fail(
|
|
590
|
+
"fn-not-call",
|
|
591
|
+
"client.map: fn must be one call ({ $proc, input }), or absent",
|
|
592
|
+
at("fn"),
|
|
593
|
+
"The fn of client.map must be one call ({ $proc, input }): client.map fails with an error.",
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
const items = this.items(input["items"], scoped, at("items"));
|
|
597
|
+
const item = this.binding("item", as ?? "item", at("fn"));
|
|
598
|
+
const index = this.binding("index", "index", at("fn"));
|
|
599
|
+
const fnNode =
|
|
600
|
+
fn === undefined
|
|
601
|
+
? undefined
|
|
602
|
+
: this.operandCall(fn, true, NO_EXTRA, at("fn"), { trap: "A $never fn gives the call itself for each item: it does not start." });
|
|
603
|
+
return { kind: "map", ...base, items, fn: fnNode, item, index };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private reduce(
|
|
607
|
+
input: Record<string, unknown>,
|
|
608
|
+
scoped: boolean,
|
|
609
|
+
base: { path: ProcedurePath; key: string; position: CallPosition; pointer: Pointer },
|
|
610
|
+
at: (key: string) => Pointer,
|
|
611
|
+
): IRNode | undefined {
|
|
612
|
+
const fn = input["fn"];
|
|
613
|
+
if (fn !== undefined && !isAnyProcedureRef(fn)) {
|
|
614
|
+
return this.fail(
|
|
615
|
+
"fn-not-call",
|
|
616
|
+
"client.reduce: fn must be one call ({ $proc, input }), or absent",
|
|
617
|
+
at("fn"),
|
|
618
|
+
"The fn of client.reduce must be one call ({ $proc, input }): client.reduce fails with an error.",
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
const acc = this.binding("acc", "acc", at("fn"));
|
|
622
|
+
const item = this.binding("item", "item", at("fn"));
|
|
623
|
+
const index = this.binding("index", "index", at("fn"));
|
|
624
|
+
if (fn === undefined) {
|
|
625
|
+
// Without fn, the result is the hydrated accumulated value (or initial), and the items do not start
|
|
626
|
+
const accumulated = input["accumulated"];
|
|
627
|
+
const source = accumulated ?? input["initial"];
|
|
628
|
+
const initial = this.hydrateRoot(source, scoped, accumulated !== undefined && accumulated !== null ? at("accumulated") : at("initial"));
|
|
629
|
+
return { kind: "reduce", ...base, items: undefined, initial, fn: undefined, acc, item, index };
|
|
630
|
+
}
|
|
631
|
+
const items = this.items(input["items"], scoped, at("items"));
|
|
632
|
+
const initial = this.hydrateRoot(input["initial"], scoped, at("initial"));
|
|
633
|
+
const fnNode = this.operandCall(fn, true, NO_EXTRA, at("fn"), { trap: "A $never fn gives the call itself as the accumulator: it does not start." });
|
|
634
|
+
return { kind: "reduce", ...base, items, initial, fn: fnNode, acc, item, index };
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// =============================================================================
|
|
639
|
+
// The definitions of a program
|
|
640
|
+
// =============================================================================
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* This function finds each `procedure.define` call of a program with a literal path and a body
|
|
644
|
+
* that is a call. A later call of such a path is known to the compiler, and its body gives the
|
|
645
|
+
* effects of the call.
|
|
646
|
+
*/
|
|
647
|
+
export function findDefinitions(program: unknown): Map<string, Definition> {
|
|
648
|
+
const definitions = new Map<string, Definition>();
|
|
649
|
+
const visited = new Set<object>();
|
|
650
|
+
const visit = (value: unknown, pointer: Pointer, depth = 0): void => {
|
|
651
|
+
// A define deeper than 3000 levels never starts: hydration stops at 1000 levels of data in one operand
|
|
652
|
+
if (value === null || typeof value !== "object" || visited.has(value) || depth > 3000) return;
|
|
653
|
+
visited.add(value);
|
|
654
|
+
// A $literal and a $never call are data: a define in them does not start
|
|
655
|
+
if (isLiteral(value)) return;
|
|
656
|
+
if (isAnyProcedureRef(value) && getRefWhen(value) === WHEN_NEVER) return;
|
|
657
|
+
if (isAnyProcedureRef(value)) {
|
|
658
|
+
const ref = normalizeRef(value);
|
|
659
|
+
const input = ref.input;
|
|
660
|
+
if (ref.path.length === 2 && ref.path[0] === "procedure" && ref.path[1] === "define" && isPlainObject(input)) {
|
|
661
|
+
const path = input["path"];
|
|
662
|
+
const aggregation = input["aggregation"];
|
|
663
|
+
if (Array.isArray(path) && path.length > 0 && path.every((segment) => typeof segment === "string" && segment.length > 0) && isAnyProcedureRef(aggregation)) {
|
|
664
|
+
const metadata = isPlainObject(input["metadata"]) ? input["metadata"] : undefined;
|
|
665
|
+
const tags = metadata && Array.isArray(metadata["tags"]) && metadata["tags"].every((tag) => typeof tag === "string") ? (metadata["tags"] as string[]) : undefined;
|
|
666
|
+
const entry: ProcedureEntry = {
|
|
667
|
+
path: path as string[],
|
|
668
|
+
key: pathToKey(path as string[]),
|
|
669
|
+
...(typeof metadata?.["description"] === "string" ? { description: metadata["description"] } : {}),
|
|
670
|
+
...(tags ? { tags } : {}),
|
|
671
|
+
effects: PURE_EFFECTS,
|
|
672
|
+
input: {},
|
|
673
|
+
output: {},
|
|
674
|
+
dataDriven: true,
|
|
675
|
+
runtime: true,
|
|
676
|
+
hasHandler: true,
|
|
677
|
+
};
|
|
678
|
+
definitions.set(entry.key, { entry, body: aggregation, pointer: childPointer(childPointer(pointer, "input"), "aggregation") });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
visit(input, childPointer(pointer, "input"), depth + 1);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (Array.isArray(value)) {
|
|
685
|
+
value.forEach((item, index) => visit(item, childPointer(pointer, index), depth + 1));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (isPlainObject(value)) {
|
|
689
|
+
for (const [key, item] of Object.entries(value)) visit(item, childPointer(pointer, key), depth + 1);
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
visit(program, "");
|
|
693
|
+
return definitions;
|
|
694
|
+
}
|