@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
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The source text: the IR to the text of an ES module (JavaScript or TypeScript).
|
|
3
|
+
*
|
|
4
|
+
* The text does the same steps as the closure code, with the functions of
|
|
5
|
+
* `@procedures/compiler/runtime`. Each literal goes into the text through `JSON.stringify` or
|
|
6
|
+
* through a number or a keyword, so a value of the program cannot change the code. Each key of
|
|
7
|
+
* an object is a computed key, so `__proto__` stays an own property.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { isPlainObject, type ProcedurePath } from "@procedures/client/browser";
|
|
11
|
+
import type { Binding, IRNode, ItemsSpec, Pointer, ProgramIR, ScopeSnapshot } from "./ir.js";
|
|
12
|
+
import { childrenOf, walk } from "./ir.js";
|
|
13
|
+
import type { Diagnostic } from "./diagnostics.js";
|
|
14
|
+
|
|
15
|
+
/** The options of the source text. */
|
|
16
|
+
export interface TextOptions {
|
|
17
|
+
/** `js` (the default) or `ts`: TypeScript adds the types that a strict configuration needs. */
|
|
18
|
+
readonly language?: "js" | "ts" | undefined;
|
|
19
|
+
/** The import specifier of the runtime. The default is `@procedures/compiler/runtime`. */
|
|
20
|
+
readonly runtime?: string | undefined;
|
|
21
|
+
/** The hash of the manifest that the code was compiled against. */
|
|
22
|
+
readonly registryHash: string;
|
|
23
|
+
/** The keys of the procedures that take raw input, for `client.exec(program, input)`. */
|
|
24
|
+
readonly rawKeys: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One line of the source map: a line of the module and the pointer of its part of the program. */
|
|
28
|
+
export interface SourceMapLine {
|
|
29
|
+
/** The line number in the module, from 1. */
|
|
30
|
+
readonly line: number;
|
|
31
|
+
readonly pointer: Pointer;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The source text of a program. */
|
|
35
|
+
export interface ProgramText {
|
|
36
|
+
/** The ES module: its default export is `async (ctx, input) => output`. */
|
|
37
|
+
readonly module: string;
|
|
38
|
+
/**
|
|
39
|
+
* The body of an async function with the parameters `rt` (the runtime), `ctx` and `input`, in
|
|
40
|
+
* JavaScript. A host that can evaluate code (a test, a playground) uses it.
|
|
41
|
+
*/
|
|
42
|
+
readonly body: string;
|
|
43
|
+
/** The lines of the module that start a part of the program. */
|
|
44
|
+
readonly sourceMap: SourceMapLine[];
|
|
45
|
+
/** The literals that have no source text. Then the text is not complete. */
|
|
46
|
+
readonly diagnostics: Diagnostic[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The deepest literal that the text writes: a deeper value is not serializable (no stack overflow). */
|
|
50
|
+
const MAX_LITERAL_DEPTH = 2000;
|
|
51
|
+
|
|
52
|
+
const LINE_SEPARATOR = String.fromCharCode(0x2028);
|
|
53
|
+
const PARAGRAPH_SEPARATOR = String.fromCharCode(0x2029);
|
|
54
|
+
const BACKSLASH = String.fromCharCode(0x5c);
|
|
55
|
+
|
|
56
|
+
/** A string literal: JSON, and the two line separators that a comment does not permit. */
|
|
57
|
+
function str(value: string): string {
|
|
58
|
+
return JSON.stringify(value).split(LINE_SEPARATOR).join(BACKSLASH + "u2028").split(PARAGRAPH_SEPARATOR).join(BACKSLASH + "u2029");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The key of a property in an object literal: a name, or a computed key. `__proto__` is always a
|
|
63
|
+
* computed key: as a name or a string, it sets the prototype and makes no property.
|
|
64
|
+
*/
|
|
65
|
+
function propertyKey(key: string): string {
|
|
66
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) && key !== "__proto__" ? key : `[${str(key)}]`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The comment that names the pointer of a line, for the source map. */
|
|
70
|
+
function pointerComment(pointer: Pointer): string {
|
|
71
|
+
return ` // ${str(pointer)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class Emitter {
|
|
75
|
+
readonly diagnostics: Diagnostic[] = [];
|
|
76
|
+
private temps = 0;
|
|
77
|
+
/** The bindings that a reference or a scope reads: only these get a variable. */
|
|
78
|
+
readonly used = new Set<number>();
|
|
79
|
+
usesContext = false;
|
|
80
|
+
usesRuntime = false;
|
|
81
|
+
|
|
82
|
+
constructor(private readonly ts: boolean) {}
|
|
83
|
+
|
|
84
|
+
temp(): string {
|
|
85
|
+
return `t${this.temps++}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
rt(name: string): string {
|
|
89
|
+
this.usesRuntime = true;
|
|
90
|
+
return `rt.${name}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ctx(): string {
|
|
94
|
+
this.usesContext = true;
|
|
95
|
+
return "ctx";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type(annotation: string): string {
|
|
99
|
+
return this.ts ? `: ${annotation}` : "";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The source of a literal value, or undefined when the value has none. */
|
|
103
|
+
literal(value: unknown, pointer: Pointer, seen: Set<object> = new Set()): string {
|
|
104
|
+
switch (typeof value) {
|
|
105
|
+
case "undefined":
|
|
106
|
+
return "undefined";
|
|
107
|
+
case "boolean":
|
|
108
|
+
return value ? "true" : "false";
|
|
109
|
+
case "number":
|
|
110
|
+
if (Number.isNaN(value)) return "NaN";
|
|
111
|
+
if (value === Infinity) return "Infinity";
|
|
112
|
+
if (value === -Infinity) return "-Infinity";
|
|
113
|
+
if (Object.is(value, -0)) return "-0";
|
|
114
|
+
return String(value);
|
|
115
|
+
case "bigint":
|
|
116
|
+
return `${value}n`;
|
|
117
|
+
case "string":
|
|
118
|
+
return str(value);
|
|
119
|
+
case "object": {
|
|
120
|
+
if (value === null) return "null";
|
|
121
|
+
if (seen.has(value)) return this.unserializable(pointer, "the value contains itself");
|
|
122
|
+
if (seen.size > MAX_LITERAL_DEPTH) return this.unserializable(pointer, `the value nests deeper than ${MAX_LITERAL_DEPTH} levels`);
|
|
123
|
+
seen.add(value);
|
|
124
|
+
try {
|
|
125
|
+
if (Array.isArray(value)) return `[${Array.from(value, (item) => this.literal(item, pointer, seen)).join(", ")}]`;
|
|
126
|
+
if (isPlainObject(value) && Object.getOwnPropertySymbols(value).length === 0) {
|
|
127
|
+
const entries = Object.entries(value);
|
|
128
|
+
if (entries.length === 0) return "{}";
|
|
129
|
+
return `{ ${entries.map(([key, item]) => `${propertyKey(key)}: ${this.literal(item, pointer, seen)}`).join(", ")} }`;
|
|
130
|
+
}
|
|
131
|
+
return this.unserializable(pointer, "the value is not plain data (a class instance or a runtime ref)");
|
|
132
|
+
} finally {
|
|
133
|
+
seen.delete(value);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
default:
|
|
137
|
+
return this.unserializable(pointer, `a ${typeof value} has no source text`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private unserializable(pointer: Pointer, why: string): string {
|
|
142
|
+
this.diagnostics.push({
|
|
143
|
+
severity: "warning",
|
|
144
|
+
code: "not-serializable",
|
|
145
|
+
message: `The source text cannot hold this literal: ${why}. The text has undefined in its place. The closure code is complete.`,
|
|
146
|
+
pointer,
|
|
147
|
+
});
|
|
148
|
+
return "undefined";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
path(path: ProcedurePath): string {
|
|
152
|
+
return `[${path.map((segment) => str(segment)).join(", ")}]`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
frames(snapshot: ScopeSnapshot): string {
|
|
156
|
+
if (!snapshot) return "undefined";
|
|
157
|
+
return `[${snapshot
|
|
158
|
+
.map((frame) => {
|
|
159
|
+
const names = frame.names.map(([name, binding]) => `[${str(name)}, ${this.binding(binding)}]`).join(", ");
|
|
160
|
+
return `{ names: [${names}]${frame.last ? `, last: [${this.binding(frame.last)}]` : ""} }`;
|
|
161
|
+
})
|
|
162
|
+
.join(", ")}]`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
binding(binding: Binding): string {
|
|
166
|
+
return `b${binding.id}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** An async block that gives a value: `(await (async () => { ... })())`. */
|
|
170
|
+
block(lines: string[], indent: string, pointer: Pointer, what: string): string {
|
|
171
|
+
const inner = lines.map((line) => `${indent} ${line}`).join("\n");
|
|
172
|
+
return `(await (async () => {${pointerComment(pointer)} ${what}\n${inner}\n${indent}})())`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
items(spec: ItemsSpec, indent: string): string {
|
|
176
|
+
if (spec.kind === "list") return `[${spec.items.map((item) => this.expr(item, indent)).join(", ")}]`;
|
|
177
|
+
return `${this.rt("items")}(${this.expr(spec.value, indent)})`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The expression of a node. `indent` is the indent of the line that holds it. */
|
|
181
|
+
expr(node: IRNode, indent: string): string {
|
|
182
|
+
const deeper = `${indent} `;
|
|
183
|
+
switch (node.kind) {
|
|
184
|
+
case "literal":
|
|
185
|
+
return this.literal(node.value, node.pointer);
|
|
186
|
+
case "object":
|
|
187
|
+
if (node.entries.length === 0) return "{}";
|
|
188
|
+
return `${this.rt("object")}([${node.entries.map((entry) => `[${str(entry.key)}, ${this.expr(entry.value, indent)}]`).join(", ")}])`;
|
|
189
|
+
case "array":
|
|
190
|
+
return `[${node.items.map((item) => this.expr(item, indent)).join(", ")}]`;
|
|
191
|
+
case "reference": {
|
|
192
|
+
if (node.binding) {
|
|
193
|
+
const variable = this.binding(node.binding);
|
|
194
|
+
return node.fields.length === 0 ? variable : `${this.rt("get")}(${variable}, [${node.fields.map(str).join(", ")}])`;
|
|
195
|
+
}
|
|
196
|
+
if (node.deferred) return this.literal(node.source, node.pointer);
|
|
197
|
+
return `${this.rt("unknownName")}(${str(node.text)})`;
|
|
198
|
+
}
|
|
199
|
+
case "call": {
|
|
200
|
+
const input = this.expr(node.input, indent);
|
|
201
|
+
const merged = Object.keys(node.extra).length > 0 ? `${this.rt("merge")}(${input}, ${this.literal(node.extra, node.pointer)})` : input;
|
|
202
|
+
return `(await ${this.rt("call")}(${this.ctx()}, ${this.path(node.path)}, ${merged}))`;
|
|
203
|
+
}
|
|
204
|
+
case "deferred":
|
|
205
|
+
return `${this.rt("deferred")}(${this.literal(node.source, node.pointer)}, ${this.expr(node.input, indent)})`;
|
|
206
|
+
case "dynamic": {
|
|
207
|
+
const input = this.expr(node.input, indent);
|
|
208
|
+
const merged = Object.keys(node.extra).length > 0 ? `${this.rt("merge")}(${input}, ${this.literal(node.extra, node.pointer)})` : input;
|
|
209
|
+
return `(await ${this.rt("callRaw")}(${this.ctx()}, ${this.path(node.path)}, ${merged}, ${this.frames(node.scope)}))`;
|
|
210
|
+
}
|
|
211
|
+
case "fail":
|
|
212
|
+
return `${this.rt("fail")}(${str(node.message)})`;
|
|
213
|
+
case "chain": {
|
|
214
|
+
const lines: string[] = [];
|
|
215
|
+
if (node.initialInput && node.initial) {
|
|
216
|
+
const initial = this.expr(node.initialInput, deeper);
|
|
217
|
+
lines.push(this.used.has(node.initial.id) ? `const ${this.binding(node.initial)} = ${initial};` : `void ${initial};`);
|
|
218
|
+
}
|
|
219
|
+
for (const step of node.steps) {
|
|
220
|
+
lines.push(`const ${this.binding(step.result)} = ${this.expr(step.node, deeper)};${pointerComment(step.pointer)}`);
|
|
221
|
+
}
|
|
222
|
+
const results = this.temp();
|
|
223
|
+
lines.push(`const ${results}${this.type("unknown[]")} = [${node.steps.map((step) => this.binding(step.result)).join(", ")}];`);
|
|
224
|
+
lines.push(`return { results: ${results}, final: ${results}[${results}.length - 1] };`);
|
|
225
|
+
return this.block(lines, indent, node.pointer, node.implicit ? "implicit chain" : "client.chain");
|
|
226
|
+
}
|
|
227
|
+
case "parallel": {
|
|
228
|
+
const tasks = node.tasks.map((task) => `async () => (${this.expr(task, deeper)})`);
|
|
229
|
+
const concurrency = node.concurrency === undefined ? "undefined" : String(node.concurrency);
|
|
230
|
+
return `(await ${this.rt("parallel")}([${tasks.join(", ")}], ${concurrency}, ${node.failFast ? "true" : "false"}))`;
|
|
231
|
+
}
|
|
232
|
+
case "conditional": {
|
|
233
|
+
const lines = [
|
|
234
|
+
`if (${this.rt("truthy")}(${this.expr(node.condition, deeper)})) return ${node.then ? this.expr(node.then, deeper) : "undefined"};`,
|
|
235
|
+
`return ${node.else ? this.expr(node.else, deeper) : "undefined"};`,
|
|
236
|
+
];
|
|
237
|
+
return this.block(lines, indent, node.pointer, "client.conditional");
|
|
238
|
+
}
|
|
239
|
+
case "try": {
|
|
240
|
+
const error = this.temp();
|
|
241
|
+
const tried = this.temp();
|
|
242
|
+
const lines = [`try {`, ` const ${tried} = ${this.expr(node.try, `${deeper} `)};`];
|
|
243
|
+
lines.push(` return { success: true, value: ${tried} };`);
|
|
244
|
+
lines.push(`} catch (${error}) {`);
|
|
245
|
+
lines.push(` const ${this.binding(node.error)} = ${this.rt("message")}(${error});`);
|
|
246
|
+
const caught = this.temp();
|
|
247
|
+
lines.push(` const ${caught} = ${this.expr(node.catch, `${deeper} `)};`);
|
|
248
|
+
lines.push(` return { success: false, value: ${caught}, error: ${this.binding(node.error)} };`);
|
|
249
|
+
lines.push(`}`);
|
|
250
|
+
return this.block(lines, indent, node.pointer, "client.tryCatch");
|
|
251
|
+
}
|
|
252
|
+
case "and":
|
|
253
|
+
case "or": {
|
|
254
|
+
const last = this.temp();
|
|
255
|
+
const lines = [`let ${last}${this.type("unknown")};`];
|
|
256
|
+
node.values.forEach((value, index) => {
|
|
257
|
+
lines.push(`${last} = ${this.expr(value, deeper)};${pointerComment(value.pointer)}`);
|
|
258
|
+
if (index < node.values.length - 1) lines.push(`if (${node.kind === "and" ? "!" : ""}${last}) return ${last};`);
|
|
259
|
+
});
|
|
260
|
+
lines.push(`return ${last};`);
|
|
261
|
+
return this.block(lines, indent, node.pointer, `client.${node.kind}`);
|
|
262
|
+
}
|
|
263
|
+
case "map": {
|
|
264
|
+
const values = this.temp();
|
|
265
|
+
const lines = [`const ${values}${this.type("unknown[]")} = ${this.items(node.items, deeper)};`];
|
|
266
|
+
if (!node.fn) {
|
|
267
|
+
lines.push(`return { results: ${values} };`);
|
|
268
|
+
} else {
|
|
269
|
+
const results = this.temp();
|
|
270
|
+
lines.push(`const ${results}${this.type("unknown[]")} = [];`);
|
|
271
|
+
lines.push(`for (const [${this.declare(node.index)}, ${this.declare(node.item)}] of ${values}.entries()) {`);
|
|
272
|
+
lines.push(` ${results}.push(${this.expr(node.fn, `${deeper} `)});${pointerComment(node.fn.pointer)}`);
|
|
273
|
+
lines.push(`}`);
|
|
274
|
+
lines.push(`return { results: ${results} };`);
|
|
275
|
+
}
|
|
276
|
+
return this.block(lines, indent, node.pointer, "client.map");
|
|
277
|
+
}
|
|
278
|
+
case "reduce": {
|
|
279
|
+
if (!node.fn || !node.items) return this.expr(node.initial, indent);
|
|
280
|
+
const values = this.temp();
|
|
281
|
+
const acc = this.temp();
|
|
282
|
+
const lines = [
|
|
283
|
+
`const ${values}${this.type("unknown[]")} = ${this.items(node.items, deeper)};`,
|
|
284
|
+
`let ${acc}${this.type("unknown")} = ${this.expr(node.initial, deeper)};`,
|
|
285
|
+
`for (const [${this.declare(node.index)}, ${this.declare(node.item)}] of ${values}.entries()) {`,
|
|
286
|
+
];
|
|
287
|
+
if (this.used.has(node.acc.id)) lines.push(` const ${this.binding(node.acc)} = ${acc};`);
|
|
288
|
+
lines.push(` ${acc} = ${this.expr(node.fn, `${deeper} `)};${pointerComment(node.fn.pointer)}`);
|
|
289
|
+
lines.push(`}`);
|
|
290
|
+
lines.push(`return ${acc};`);
|
|
291
|
+
return this.block(lines, indent, node.pointer, "client.reduce");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** The name of a binding in a declaration, or nothing when no reference reads it. */
|
|
297
|
+
private declare(binding: Binding): string {
|
|
298
|
+
return this.used.has(binding.id) ? this.binding(binding) : "";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** The bindings that the code reads: the resolved references and the scopes of the dynamic nodes. */
|
|
303
|
+
function usedBindings(root: IRNode): Set<number> {
|
|
304
|
+
const used = new Set<number>();
|
|
305
|
+
walk(root, (node) => {
|
|
306
|
+
if (node.kind === "reference" && node.binding) used.add(node.binding.id);
|
|
307
|
+
if (node.kind === "dynamic" && node.scope) {
|
|
308
|
+
for (const frame of node.scope) {
|
|
309
|
+
for (const [, binding] of frame.names) used.add(binding.id);
|
|
310
|
+
if (frame.last) used.add(frame.last.id);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
return used;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** The nesting above which the source text gets a warning. V8 refuses a text of approximately 1000 levels. */
|
|
318
|
+
const MAX_TEXT_DEPTH = 300;
|
|
319
|
+
|
|
320
|
+
/** The deepest nesting of the nodes. */
|
|
321
|
+
function depthOf(root: IRNode): number {
|
|
322
|
+
let deepest = 0;
|
|
323
|
+
const visit = (node: IRNode, depth: number): void => {
|
|
324
|
+
deepest = Math.max(deepest, depth);
|
|
325
|
+
for (const child of childrenOf(node)) visit(child, depth + 1);
|
|
326
|
+
};
|
|
327
|
+
visit(root, 1);
|
|
328
|
+
return deepest;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** This function writes the source text of a program. */
|
|
332
|
+
export function emitText(ir: ProgramIR, options: TextOptions): ProgramText {
|
|
333
|
+
const ts = options.language === "ts";
|
|
334
|
+
const runtime = options.runtime ?? "@procedures/compiler/runtime";
|
|
335
|
+
const emitter = new Emitter(ts);
|
|
336
|
+
for (const id of usedBindings(ir.root)) emitter.used.add(id);
|
|
337
|
+
const depth = depthOf(ir.root);
|
|
338
|
+
if (depth > MAX_TEXT_DEPTH) {
|
|
339
|
+
emitter.diagnostics.push({
|
|
340
|
+
severity: "warning",
|
|
341
|
+
code: "text-depth",
|
|
342
|
+
message: `The program nests ${depth} levels deep: a JavaScript parser can refuse a source text this deep. The closure code has no such limit.`,
|
|
343
|
+
pointer: ir.root.pointer,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const bodyLines: string[] = [];
|
|
348
|
+
/** The constants of the exec mode: the module has them one time, the function body has them in the body. */
|
|
349
|
+
const constants: string[] = [];
|
|
350
|
+
let inputUsed = false;
|
|
351
|
+
if (ir.mode === "exec") {
|
|
352
|
+
inputUsed = true;
|
|
353
|
+
constants.push(`const PROGRAM${emitter.type("unknown")} = ${emitter.literal(ir.program, "")};`);
|
|
354
|
+
constants.push(`const RAW_KEYS${emitter.type("string[]")} = [${options.rawKeys.map(str).join(", ")}];`);
|
|
355
|
+
bodyLines.push(`if (input !== undefined) return ${emitter.rt("execWithInput")}(${emitter.ctx()}, PROGRAM, input, RAW_KEYS);`);
|
|
356
|
+
} else if (ir.input && emitter.used.has(ir.input.id)) {
|
|
357
|
+
inputUsed = true;
|
|
358
|
+
bodyLines.push(`const ${emitter.binding(ir.input)} = input;`);
|
|
359
|
+
}
|
|
360
|
+
bodyLines.push(`return ${emitter.expr(ir.root, " ")};${pointerComment(ir.root.pointer)}`);
|
|
361
|
+
|
|
362
|
+
const body = bodyLines.map((line) => ` ${line}`).join("\n");
|
|
363
|
+
const ctxName = emitter.usesContext ? "ctx" : "_ctx";
|
|
364
|
+
const inputName = inputUsed ? "input" : "_input";
|
|
365
|
+
const header = [
|
|
366
|
+
"// Generated by @procedures/compiler from a program. Compile the program again after a change.",
|
|
367
|
+
`// The registry hash: ${options.registryHash}`,
|
|
368
|
+
...(emitter.usesRuntime ? [`import * as rt from ${str(runtime)};`] : []),
|
|
369
|
+
...(ts ? [`import type { ProgramContext } from ${str(runtime)};`] : []),
|
|
370
|
+
"",
|
|
371
|
+
`export const registryHash${ts ? ": string" : ""} = ${str(options.registryHash)};`,
|
|
372
|
+
...(constants.length > 0 ? ["", "// The program, for client.exec(program, input): the input changes the program, so the interpreter runs it", ...constants] : []),
|
|
373
|
+
"",
|
|
374
|
+
ts
|
|
375
|
+
? `export default async function program(${ctxName}: ProgramContext, ${inputName}?: unknown): Promise<unknown> {`
|
|
376
|
+
: `export default async function program(${ctxName}, ${inputName}) {`,
|
|
377
|
+
];
|
|
378
|
+
const moduleText = `${[...header, body, "}"].join("\n")}\n`;
|
|
379
|
+
|
|
380
|
+
// The function body for `new AsyncFunction("rt", "ctx", "input", body)`: always JavaScript
|
|
381
|
+
const jsEmitter = ts ? emitText(ir, { ...options, language: "js" }) : undefined;
|
|
382
|
+
const functionBody = jsEmitter ? jsEmitter.body : [...constants, ...bodyLines].join("\n");
|
|
383
|
+
|
|
384
|
+
const sourceMap: SourceMapLine[] = [];
|
|
385
|
+
moduleText.split("\n").forEach((line, index) => {
|
|
386
|
+
const match = /\/\/ ("(?:[^"\\]|\\.)*")(?: [a-zA-Z. ]*)?$/.exec(line);
|
|
387
|
+
if (match?.[1]) {
|
|
388
|
+
try {
|
|
389
|
+
sourceMap.push({ line: index + 1, pointer: JSON.parse(match[1]) as string });
|
|
390
|
+
} catch {
|
|
391
|
+
// Not a pointer comment
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
return { module: moduleText, body: functionBody, sourceMap, diagnostics: emitter.diagnostics };
|
|
396
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The diagnostics of the compiler: each problem that a pass finds, with the JSON pointer of its
|
|
3
|
+
* source in the program.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** The severity of a diagnostic. An error makes `ok` false: the host does not start the program. */
|
|
7
|
+
export type Severity = "error" | "warning" | "info";
|
|
8
|
+
|
|
9
|
+
/** The codes of the diagnostics. */
|
|
10
|
+
export type DiagnosticCode =
|
|
11
|
+
/** The program is not a call: `client.exec()` starts only a call. */
|
|
12
|
+
| "root-not-call"
|
|
13
|
+
/** A `$proc` path is empty, or it has a segment that is not a string. */
|
|
14
|
+
| "invalid-path"
|
|
15
|
+
/** No procedure has the path. */
|
|
16
|
+
| "unknown-procedure"
|
|
17
|
+
/** The host does not expose the procedure, so a data-driven procedure cannot call it. */
|
|
18
|
+
| "not-exposed"
|
|
19
|
+
/** The capability refuses the call: its effects, or a destructive procedure. */
|
|
20
|
+
| "capability-refused"
|
|
21
|
+
/** The compiler cannot see what a call runs, so it does not know its effects. */
|
|
22
|
+
| "effects-unknown"
|
|
23
|
+
/** A `$ref` reads a name that no scope has. */
|
|
24
|
+
| "unknown-name"
|
|
25
|
+
/** A `$ref` reads the name of a later step of a chain. */
|
|
26
|
+
| "use-before-definition"
|
|
27
|
+
/** A `$ref` is outside each scope, so it is data. */
|
|
28
|
+
| "unscoped-ref"
|
|
29
|
+
/** The input does not agree with the JSON Schema of the procedure. */
|
|
30
|
+
| "input-schema"
|
|
31
|
+
/** The input of a control-flow procedure does not agree with its schema. */
|
|
32
|
+
| "invalid-control-flow"
|
|
33
|
+
/** The `fn` of `client.map` or `client.reduce` is not one call. */
|
|
34
|
+
| "fn-not-call"
|
|
35
|
+
/** The calls nest deeper than the hydration limit of 10 levels. */
|
|
36
|
+
| "depth-limit"
|
|
37
|
+
/** Plain data nests deeper than 1000 levels. */
|
|
38
|
+
| "data-depth"
|
|
39
|
+
/** An input contains itself. */
|
|
40
|
+
| "cyclic-input"
|
|
41
|
+
/** A `$never` call is data in a position where a call usually runs. */
|
|
42
|
+
| "never-trap"
|
|
43
|
+
/** A call waits for its runner (`$parent` or a context name), so hydration keeps it as data. */
|
|
44
|
+
| "deferred-call"
|
|
45
|
+
/** A `$name` on a call that is not a chain step: no `$ref` can read it. */
|
|
46
|
+
| "name-not-bound"
|
|
47
|
+
/** Two steps of one chain have the same `$name`. */
|
|
48
|
+
| "duplicate-name"
|
|
49
|
+
/** Control flow adds `cwd` or `node` to an input that is not an object, so the input is lost. */
|
|
50
|
+
| "input-replaced"
|
|
51
|
+
/** An array of calls runs as one `client.chain`: its value is `{ results, final }`. */
|
|
52
|
+
| "implicit-chain"
|
|
53
|
+
/** A literal of the program has no source text (a function, a class instance). */
|
|
54
|
+
| "not-serializable"
|
|
55
|
+
/** The program nests so deep that a JavaScript parser can refuse its source text. */
|
|
56
|
+
| "text-depth";
|
|
57
|
+
|
|
58
|
+
/** One problem that the compiler found. */
|
|
59
|
+
export interface Diagnostic {
|
|
60
|
+
readonly severity: Severity;
|
|
61
|
+
readonly code: DiagnosticCode;
|
|
62
|
+
readonly message: string;
|
|
63
|
+
/** The JSON pointer (RFC 6901) of the part of the program. The empty string is the root. */
|
|
64
|
+
readonly pointer: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** True when the list has an error. */
|
|
68
|
+
export function hasErrors(diagnostics: readonly Diagnostic[]): boolean {
|
|
69
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The diagnostics in the order of their pointers, then the errors first. The same problem at the same place is one diagnostic. */
|
|
73
|
+
export function sortDiagnostics(diagnostics: readonly Diagnostic[]): Diagnostic[] {
|
|
74
|
+
const rank: Record<Severity, number> = { error: 0, warning: 1, info: 2 };
|
|
75
|
+
const seen = new Set<string>();
|
|
76
|
+
const unique = diagnostics.filter((diagnostic) => {
|
|
77
|
+
const key = `${diagnostic.pointer}\u0000${diagnostic.code}\u0000${diagnostic.message}`;
|
|
78
|
+
if (seen.has(key)) return false;
|
|
79
|
+
seen.add(key);
|
|
80
|
+
return true;
|
|
81
|
+
});
|
|
82
|
+
return unique.sort((a, b) => comparePointers(a.pointer, b.pointer) || rank[a.severity] - rank[b.severity]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The order of two pointers: the segments one by one, and a number segment as a number. */
|
|
86
|
+
function comparePointers(a: string, b: string): number {
|
|
87
|
+
const left = a.split("/");
|
|
88
|
+
const right = b.split("/");
|
|
89
|
+
for (let index = 0; index < Math.min(left.length, right.length); index++) {
|
|
90
|
+
const x = left[index]!;
|
|
91
|
+
const y = right[index]!;
|
|
92
|
+
if (x === y) continue;
|
|
93
|
+
const numbers = /^\d+$/.test(x) && /^\d+$/.test(y);
|
|
94
|
+
if (numbers) return Number(x) - Number(y);
|
|
95
|
+
return x < y ? -1 : 1;
|
|
96
|
+
}
|
|
97
|
+
return left.length - right.length;
|
|
98
|
+
}
|