@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/mermaid.ts
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Mermaid flowchart of a program (roadmap R23).
|
|
3
|
+
*
|
|
4
|
+
* The diagram comes from the IR, so it shows what the interpreter does:
|
|
5
|
+
*
|
|
6
|
+
* - A solid arrow is the order of the interpreter. A call in an input starts before its call.
|
|
7
|
+
* - A chain is a subgraph with its steps in order. A parallel call has a fork and a join.
|
|
8
|
+
* - A conditional call is a decision with a `then` arrow and an `else` arrow.
|
|
9
|
+
* - A `client.tryCatch` call has a `try` subgraph and an `error` arrow to its catch part.
|
|
10
|
+
* - A map or a reduce has a loop node: an arrow to `fn` for each item, and an arrow back.
|
|
11
|
+
* - The operands of `client.and` and `client.or` are in order, with a stop arrow from each operand.
|
|
12
|
+
* - A dotted arrow goes from the part that binds a name to each `$ref` that reads the name.
|
|
13
|
+
* - A dynamic call and a deferred call have their own class and a second line of text.
|
|
14
|
+
*
|
|
15
|
+
* Each label goes through `mermaidText()`, so no text of a program can break the syntax.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { isAnyProcedureRef, normalizeRef } from "@procedures/client/browser";
|
|
19
|
+
import type { ChainNode, ConditionalNode, DynamicReason, IRNode, ItemsSpec, LogicNode, MapNode, ParallelNode, ProgramIR, ReduceNode, ReferenceNode, TryNode } from "./ir.js";
|
|
20
|
+
import type { Diagnostic } from "./diagnostics.js";
|
|
21
|
+
import { checkProgram, type CheckOptions } from "./program.js";
|
|
22
|
+
|
|
23
|
+
/** The options of `toMermaid()`. */
|
|
24
|
+
export interface MermaidOptions {
|
|
25
|
+
/** The direction of the flowchart: `TD` (from the top down, the default) or `LR` (from left to right). */
|
|
26
|
+
readonly direction?: "TD" | "LR" | undefined;
|
|
27
|
+
/** The diagnostics of the check. A node with an error at its pointer gets the class `error`. */
|
|
28
|
+
readonly diagnostics?: readonly Diagnostic[] | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The characters that a label keeps as they are. Each other character becomes a Mermaid entity code. */
|
|
32
|
+
const SAFE_CHARACTER = /^[A-Za-z0-9 .,_\-/+=?!'()]$/;
|
|
33
|
+
|
|
34
|
+
/** The default limit of the characters of one line of a label. */
|
|
35
|
+
const LINE_LIMIT = 60;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* This function makes a text safe for a quoted Mermaid label. It keeps letters, digits, spaces
|
|
39
|
+
* and some punctuation. It writes each other character as an entity code (`#34;` for a quote).
|
|
40
|
+
* Thus quotes, brackets, `<`, `#`, line breaks and the Unicode line separators cannot break the
|
|
41
|
+
* syntax. A text longer than `limit` characters ends with three dots.
|
|
42
|
+
*/
|
|
43
|
+
export function mermaidText(text: string, limit: number = LINE_LIMIT): string {
|
|
44
|
+
const characters = Array.from(text);
|
|
45
|
+
const kept = characters.length > limit ? [...characters.slice(0, Math.max(0, limit - 3)), ".", ".", "."] : characters;
|
|
46
|
+
return kept.map((character) => (SAFE_CHARACTER.test(character) ? character : `#${character.codePointAt(0)};`)).join("");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A part of the diagram: the node where the part starts and the node that gives its value. */
|
|
50
|
+
interface Fragment {
|
|
51
|
+
readonly first: string;
|
|
52
|
+
readonly last: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type Shape = "box" | "subroutine" | "round" | "stadium" | "decision" | "hexagon" | "circle";
|
|
56
|
+
|
|
57
|
+
const SHAPES: Record<Shape, readonly [string, string]> = {
|
|
58
|
+
box: ["[", "]"],
|
|
59
|
+
subroutine: ["[[", "]]"],
|
|
60
|
+
round: ["(", ")"],
|
|
61
|
+
stadium: ["([", "])"],
|
|
62
|
+
decision: ["{", "}"],
|
|
63
|
+
hexagon: ["{{", "}}"],
|
|
64
|
+
circle: ["((", "))"],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type NodeClass = "call" | "control" | "dynamic" | "deferred" | "value" | "boundary" | "fail" | "error";
|
|
68
|
+
|
|
69
|
+
/** The styles of the classes, in the order of the `classDef` lines. */
|
|
70
|
+
const CLASS_STYLES: ReadonlyArray<readonly [NodeClass, string]> = [
|
|
71
|
+
["call", "fill:#e7f0ff,stroke:#2f5fb3,color:#10213f"],
|
|
72
|
+
["control", "fill:#f1ebff,stroke:#6741d9,color:#1f1147"],
|
|
73
|
+
["dynamic", "fill:#fff1e0,stroke:#d9480f,stroke-width:2px,color:#3d1a00"],
|
|
74
|
+
["deferred", "fill:#f4f4f5,stroke:#71717a,stroke-dasharray:5 4,color:#27272a"],
|
|
75
|
+
["value", "fill:#f8fafc,stroke:#94a3b8,color:#1e293b"],
|
|
76
|
+
["boundary", "fill:#ecfdf3,stroke:#2f9e44,color:#0b3d1c"],
|
|
77
|
+
["fail", "fill:#fff5f5,stroke:#e03131,color:#5c0a0a"],
|
|
78
|
+
["error", "stroke:#e03131,stroke-width:3px"],
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const REASONS: Record<DynamicReason, string> = {
|
|
82
|
+
"runs-refs": "it runs the refs of its input",
|
|
83
|
+
"raw-input": "it takes its input as data",
|
|
84
|
+
"invalid-control-flow": "the input is not valid",
|
|
85
|
+
"not-core": "not the core procedure",
|
|
86
|
+
cyclic: "the call contains itself",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** The text of a JSON value for a label. */
|
|
90
|
+
function jsonText(value: unknown): string {
|
|
91
|
+
try {
|
|
92
|
+
return JSON.stringify(value) ?? String(value);
|
|
93
|
+
} catch {
|
|
94
|
+
return "a value that is not JSON";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The key of a call object that the IR keeps as data (a `$never` call, a deferred call). */
|
|
99
|
+
function callKey(value: unknown): string {
|
|
100
|
+
if (!isAnyProcedureRef(value)) return "a call";
|
|
101
|
+
const path = normalizeRef(value).path as readonly unknown[];
|
|
102
|
+
return path.map((segment) => String(segment)).join(".");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The text of a data node in a label. */
|
|
106
|
+
function valueText(node: IRNode): string {
|
|
107
|
+
switch (node.kind) {
|
|
108
|
+
case "literal":
|
|
109
|
+
return node.origin === "literal" ? `literal ${jsonText(node.value)}` : jsonText(node.value);
|
|
110
|
+
case "object":
|
|
111
|
+
return `object (${node.entries.map((entry) => entry.key).join(", ")})`;
|
|
112
|
+
case "array":
|
|
113
|
+
return `array (${node.items.length})`;
|
|
114
|
+
case "reference":
|
|
115
|
+
return `$ref ${node.text}`;
|
|
116
|
+
default:
|
|
117
|
+
return node.kind;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The diagram while the drawer makes it. */
|
|
122
|
+
class Drawer {
|
|
123
|
+
private readonly body: string[] = [];
|
|
124
|
+
private readonly edges: string[] = [];
|
|
125
|
+
private readonly classes = new Map<NodeClass, string[]>();
|
|
126
|
+
/** The node that gives the value of each binding, by the id of the binding. */
|
|
127
|
+
private readonly sources = new Map<number, string>();
|
|
128
|
+
/** Each `$ref` and the node that reads it. */
|
|
129
|
+
private readonly uses: Array<{ readonly ref: ReferenceNode; readonly target: string }> = [];
|
|
130
|
+
private readonly errors: ReadonlySet<string>;
|
|
131
|
+
private nodeCount = 0;
|
|
132
|
+
private groupCount = 0;
|
|
133
|
+
private depth = 1;
|
|
134
|
+
|
|
135
|
+
constructor(diagnostics: readonly Diagnostic[]) {
|
|
136
|
+
this.errors = new Set(diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.pointer));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private indent(): string {
|
|
140
|
+
return " ".repeat(this.depth);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private addClass(name: NodeClass, id: string): void {
|
|
144
|
+
const ids = this.classes.get(name);
|
|
145
|
+
if (ids) ids.push(id);
|
|
146
|
+
else this.classes.set(name, [id]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** This function adds a node with the lines of its label, and gives its id. */
|
|
150
|
+
private node(shape: Shape, lines: readonly string[], name: NodeClass, pointer?: string): string {
|
|
151
|
+
const id = `n${this.nodeCount++}`;
|
|
152
|
+
const [open, close] = SHAPES[shape];
|
|
153
|
+
this.body.push(`${this.indent()}${id}${open}"${lines.map((line) => mermaidText(line)).join("<br/>")}"${close}`);
|
|
154
|
+
this.addClass(name, id);
|
|
155
|
+
if (pointer !== undefined && this.errors.has(pointer)) this.addClass("error", id);
|
|
156
|
+
return id;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** This function draws a part in a subgraph, and gives the part and the id of the subgraph. */
|
|
160
|
+
private group(title: string, draw: () => Fragment): { readonly fragment: Fragment; readonly id: string } {
|
|
161
|
+
const id = `s${this.groupCount++}`;
|
|
162
|
+
this.body.push(`${this.indent()}subgraph ${id}["${mermaidText(title)}"]`);
|
|
163
|
+
this.depth++;
|
|
164
|
+
const fragment = draw();
|
|
165
|
+
this.depth--;
|
|
166
|
+
this.body.push(`${this.indent()}end`);
|
|
167
|
+
return { fragment, id };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A solid arrow is the order of the interpreter. A dotted arrow carries data. */
|
|
171
|
+
private edge(from: string, to: string, label?: string, kind: "flow" | "data" = "flow"): void {
|
|
172
|
+
const arrow = kind === "flow" ? "-->" : "-.->";
|
|
173
|
+
this.edges.push(label === undefined ? ` ${from} ${arrow} ${to}` : ` ${from} ${arrow}|"${mermaidText(label)}"| ${to}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private sequence(parts: readonly Fragment[]): Fragment {
|
|
177
|
+
for (let index = 1; index < parts.length; index++) this.edge(parts[index - 1]!.last, parts[index]!.first);
|
|
178
|
+
return { first: parts[0]!.first, last: parts[parts.length - 1]!.last };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private single(id: string): Fragment {
|
|
182
|
+
return { first: id, last: id };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* This function finds the parts of an input. Each call is a part, in the order of the
|
|
187
|
+
* interpreter. Each `$ref` goes to the node that reads the input.
|
|
188
|
+
*/
|
|
189
|
+
private collect(node: IRNode, parts: Fragment[], refs: ReferenceNode[]): void {
|
|
190
|
+
switch (node.kind) {
|
|
191
|
+
case "literal":
|
|
192
|
+
if (node.origin === "never" || node.origin === "deferred") parts.push(this.flow(node));
|
|
193
|
+
return;
|
|
194
|
+
case "object":
|
|
195
|
+
for (const entry of node.entries) this.collect(entry.value, parts, refs);
|
|
196
|
+
return;
|
|
197
|
+
case "array":
|
|
198
|
+
for (const item of node.items) this.collect(item, parts, refs);
|
|
199
|
+
return;
|
|
200
|
+
case "reference":
|
|
201
|
+
refs.push(node);
|
|
202
|
+
return;
|
|
203
|
+
default:
|
|
204
|
+
parts.push(this.flow(node));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** This function draws the calls of an input, then the node that reads the input. */
|
|
209
|
+
private consumer(input: IRNode | undefined, make: () => string): Fragment {
|
|
210
|
+
const parts: Fragment[] = [];
|
|
211
|
+
const refs: ReferenceNode[] = [];
|
|
212
|
+
if (input) this.collect(input, parts, refs);
|
|
213
|
+
const id = make();
|
|
214
|
+
for (const ref of refs) this.uses.push({ ref, target: id });
|
|
215
|
+
return this.sequence([...parts, this.single(id)]);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** This function draws data in the place of an operand: its calls, then a value node. */
|
|
219
|
+
private value(node: IRNode, label: string = valueText(node)): Fragment {
|
|
220
|
+
return this.consumer(node, () => this.node("round", [label], "value", node.pointer));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** This function draws one node of the IR and gives its part of the diagram. */
|
|
224
|
+
flow(node: IRNode): Fragment {
|
|
225
|
+
switch (node.kind) {
|
|
226
|
+
case "call":
|
|
227
|
+
return this.consumer(node.input, () => this.node("box", node.name === undefined ? [node.key] : [node.key, `$name ${node.name}`], "call", node.pointer));
|
|
228
|
+
case "dynamic": {
|
|
229
|
+
const fragment = this.consumer(node.input, () => this.node("subroutine", [node.key, `dynamic (${REASONS[node.reason]})`], "dynamic", node.pointer));
|
|
230
|
+
if (node.model) {
|
|
231
|
+
const model = node.model;
|
|
232
|
+
const runs = this.group("runs", () => this.flow(model));
|
|
233
|
+
this.edge(fragment.last, runs.fragment.first, "runs", "data");
|
|
234
|
+
}
|
|
235
|
+
return fragment;
|
|
236
|
+
}
|
|
237
|
+
case "deferred":
|
|
238
|
+
return this.consumer(node.input, () => this.node("box", [node.key, `deferred (waits for ${node.when})`], "deferred", node.pointer));
|
|
239
|
+
case "fail":
|
|
240
|
+
return this.single(this.node("box", ["fail", node.message], "fail", node.pointer));
|
|
241
|
+
case "literal":
|
|
242
|
+
if (node.origin === "never") return this.single(this.node("box", [callKey(node.value), "data ($when $never)"], "deferred", node.pointer));
|
|
243
|
+
if (node.origin === "deferred") return this.single(this.node("box", [callKey(node.value), "deferred (data for its runner)"], "deferred", node.pointer));
|
|
244
|
+
return this.value(node);
|
|
245
|
+
case "object":
|
|
246
|
+
case "array":
|
|
247
|
+
case "reference":
|
|
248
|
+
return this.value(node);
|
|
249
|
+
case "chain":
|
|
250
|
+
return this.chain(node);
|
|
251
|
+
case "parallel":
|
|
252
|
+
return this.parallel(node);
|
|
253
|
+
case "conditional":
|
|
254
|
+
return this.conditional(node);
|
|
255
|
+
case "try":
|
|
256
|
+
return this.tryCatch(node);
|
|
257
|
+
case "and":
|
|
258
|
+
case "or":
|
|
259
|
+
return this.logic(node);
|
|
260
|
+
case "map":
|
|
261
|
+
return this.map(node);
|
|
262
|
+
case "reduce":
|
|
263
|
+
return this.reduce(node);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private chain(node: ChainNode): Fragment {
|
|
268
|
+
const title = node.implicit ? "implicit chain" : node.passThrough ? `${node.key} (passThrough)` : node.key;
|
|
269
|
+
return this.group(title, () => {
|
|
270
|
+
const parts: Fragment[] = [];
|
|
271
|
+
if (node.initialInput) {
|
|
272
|
+
const initial = this.flow(node.initialInput);
|
|
273
|
+
if (node.initial) this.sources.set(node.initial.id, initial.last);
|
|
274
|
+
parts.push(initial);
|
|
275
|
+
}
|
|
276
|
+
for (const step of node.steps) {
|
|
277
|
+
const part = this.flow(step.node);
|
|
278
|
+
this.sources.set(step.result.id, part.last);
|
|
279
|
+
parts.push(part);
|
|
280
|
+
}
|
|
281
|
+
if (parts.length === 0) parts.push(this.single(this.node("round", ["no steps"], "value", node.pointer)));
|
|
282
|
+
return this.sequence(parts);
|
|
283
|
+
}).fragment;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private parallel(node: ParallelNode): Fragment {
|
|
287
|
+
const details = [...(node.concurrency !== undefined ? [`concurrency ${node.concurrency}`] : []), ...(node.failFast ? ["failFast"] : [])];
|
|
288
|
+
const fork = this.node("hexagon", [node.key, ...details], "control", node.pointer);
|
|
289
|
+
const tasks = node.tasks.map((task) => this.flow(task));
|
|
290
|
+
const join = this.node("circle", ["join"], "control");
|
|
291
|
+
for (const task of tasks) {
|
|
292
|
+
this.edge(fork, task.first);
|
|
293
|
+
this.edge(task.last, join);
|
|
294
|
+
}
|
|
295
|
+
if (tasks.length === 0) this.edge(fork, join);
|
|
296
|
+
return { first: fork, last: join };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private conditional(node: ConditionalNode): Fragment {
|
|
300
|
+
const condition = this.flow(node.condition);
|
|
301
|
+
const decision = this.node("decision", [node.key], "control", node.pointer);
|
|
302
|
+
this.edge(condition.last, decision);
|
|
303
|
+
const branches = (["then", "else"] as const).map((label) => {
|
|
304
|
+
const branch = node[label];
|
|
305
|
+
return { label, part: branch ? this.flow(branch) : undefined };
|
|
306
|
+
});
|
|
307
|
+
const join = this.node("circle", ["join"], "control");
|
|
308
|
+
for (const { label, part } of branches) {
|
|
309
|
+
if (part) {
|
|
310
|
+
this.edge(decision, part.first, label);
|
|
311
|
+
this.edge(part.last, join);
|
|
312
|
+
} else {
|
|
313
|
+
this.edge(decision, join, `${label} (nothing)`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return { first: condition.first, last: join };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private tryCatch(node: TryNode): Fragment {
|
|
320
|
+
const header = this.node("hexagon", [node.key], "control", node.pointer);
|
|
321
|
+
const tried = this.group("try", () => this.flow(node.try));
|
|
322
|
+
this.sources.set(node.error.id, tried.id);
|
|
323
|
+
const caught = this.flow(node.catch);
|
|
324
|
+
const join = this.node("circle", ["join"], "control");
|
|
325
|
+
this.edge(header, tried.fragment.first);
|
|
326
|
+
this.edge(tried.fragment.last, join, "ok");
|
|
327
|
+
this.edge(tried.id, caught.first, "error");
|
|
328
|
+
this.edge(caught.last, join);
|
|
329
|
+
return { first: header, last: join };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private logic(node: LogicNode): Fragment {
|
|
333
|
+
const header = this.node("hexagon", [node.key], "control", node.pointer);
|
|
334
|
+
const values = node.values.map((value) => this.flow(value));
|
|
335
|
+
const join = this.node("circle", ["join"], "control");
|
|
336
|
+
const next = node.kind === "and" ? "true" : "false";
|
|
337
|
+
const stop = node.kind === "and" ? "false (stop)" : "true (stop)";
|
|
338
|
+
if (values.length === 0) this.edge(header, join);
|
|
339
|
+
else this.edge(header, values[0]!.first);
|
|
340
|
+
values.forEach((value, index) => {
|
|
341
|
+
const following = values[index + 1];
|
|
342
|
+
if (following) {
|
|
343
|
+
this.edge(value.last, following.first, next);
|
|
344
|
+
this.edge(value.last, join, stop);
|
|
345
|
+
} else {
|
|
346
|
+
this.edge(value.last, join);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
return { first: header, last: join };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** The items of a map or a reduce: the calls of a list, then one node for the list. */
|
|
353
|
+
private items(spec: ItemsSpec, pointer: string): Fragment {
|
|
354
|
+
if (spec.kind === "value") return this.flow(spec.value);
|
|
355
|
+
const parts: Fragment[] = [];
|
|
356
|
+
const refs: ReferenceNode[] = [];
|
|
357
|
+
for (const item of spec.items) this.collect(item, parts, refs);
|
|
358
|
+
const id = this.node("round", [`items (${spec.items.length})`], "value", `${pointer}/input/items`);
|
|
359
|
+
for (const ref of refs) this.uses.push({ ref, target: id });
|
|
360
|
+
return this.sequence([...parts, this.single(id)]);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** The loop of a map or a reduce: an arrow to `fn` for each item, an arrow back, and a join. */
|
|
364
|
+
private loop(loop: string, fn: IRNode | undefined): string {
|
|
365
|
+
if (fn) {
|
|
366
|
+
const part = this.flow(fn);
|
|
367
|
+
this.edge(loop, part.first, "each item");
|
|
368
|
+
this.edge(part.last, loop, "next item");
|
|
369
|
+
}
|
|
370
|
+
const join = this.node("circle", ["join"], "control");
|
|
371
|
+
this.edge(loop, join, "done");
|
|
372
|
+
return join;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private map(node: MapNode): Fragment {
|
|
376
|
+
const items = this.items(node.items, node.pointer);
|
|
377
|
+
const loop = this.node("hexagon", [node.key, `each item (${node.item.name ?? "item"}, ${node.index.name ?? "index"})`], "control", node.pointer);
|
|
378
|
+
this.sources.set(node.item.id, loop);
|
|
379
|
+
this.sources.set(node.index.id, loop);
|
|
380
|
+
this.edge(items.last, loop);
|
|
381
|
+
return { first: items.first, last: this.loop(loop, node.fn) };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private reduce(node: ReduceNode): Fragment {
|
|
385
|
+
if (!node.fn || !node.items) {
|
|
386
|
+
// Without fn or items, the result is the initial value, and the items do not start
|
|
387
|
+
const initial = this.flow(node.initial);
|
|
388
|
+
const result = this.node("hexagon", [node.key, "no fn (the initial value)"], "control", node.pointer);
|
|
389
|
+
this.edge(initial.last, result);
|
|
390
|
+
return { first: initial.first, last: result };
|
|
391
|
+
}
|
|
392
|
+
const items = this.items(node.items, node.pointer);
|
|
393
|
+
const initial = this.flow(node.initial);
|
|
394
|
+
this.edge(items.last, initial.first);
|
|
395
|
+
const loop = this.node("hexagon", [node.key, "each item (acc, item, index)"], "control", node.pointer);
|
|
396
|
+
for (const binding of [node.acc, node.item, node.index]) this.sources.set(binding.id, loop);
|
|
397
|
+
this.edge(initial.last, loop);
|
|
398
|
+
return { first: items.first, last: this.loop(loop, node.fn) };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** This function draws the program and gives the text of the diagram. */
|
|
402
|
+
draw(ir: ProgramIR, direction: "TD" | "LR"): string {
|
|
403
|
+
const start = this.node("stadium", [ir.mode === "body" ? "input" : "start"], "boundary");
|
|
404
|
+
if (ir.input) this.sources.set(ir.input.id, start);
|
|
405
|
+
const root = this.flow(ir.root);
|
|
406
|
+
const result = this.node("stadium", ["result"], "boundary");
|
|
407
|
+
this.edge(start, root.first);
|
|
408
|
+
this.edge(root.last, result);
|
|
409
|
+
// The dotted arrows of the names: from the part that binds a name to each part that reads it
|
|
410
|
+
for (const { ref, target } of this.uses) {
|
|
411
|
+
if (ref.binding) {
|
|
412
|
+
const source = this.sources.get(ref.binding.id);
|
|
413
|
+
if (source !== undefined) this.edge(source, target, ref.synthetic ? "passThrough" : ref.text, "data");
|
|
414
|
+
} else if (!ref.deferred) {
|
|
415
|
+
const unknown = this.node("round", [`unknown name (${ref.text})`], "fail", ref.pointer);
|
|
416
|
+
this.edge(unknown, target, ref.text, "data");
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const styles = CLASS_STYLES.filter(([name]) => this.classes.has(name));
|
|
420
|
+
return [
|
|
421
|
+
`flowchart ${direction}`,
|
|
422
|
+
...this.body,
|
|
423
|
+
...this.edges,
|
|
424
|
+
...styles.map(([name, style]) => ` classDef ${name} ${style}`),
|
|
425
|
+
...styles.map(([name]) => ` class ${this.classes.get(name)!.join(",")} ${name}`),
|
|
426
|
+
"",
|
|
427
|
+
].join("\n");
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* This function gives a Mermaid flowchart of the IR of a program. The flowchart shows the order
|
|
433
|
+
* of the calls, the control flow and the names. The text has the same lines for the same IR.
|
|
434
|
+
*/
|
|
435
|
+
export function toMermaid(ir: ProgramIR, options: MermaidOptions = {}): string {
|
|
436
|
+
return new Drawer(options.diagnostics ?? []).draw(ir, options.direction ?? "TD");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** The result of `diagramProgram()`. */
|
|
440
|
+
export interface DiagramResult {
|
|
441
|
+
/** True when no diagnostic is an error. */
|
|
442
|
+
readonly ok: boolean;
|
|
443
|
+
/** The text of the Mermaid flowchart. */
|
|
444
|
+
readonly mermaid: string;
|
|
445
|
+
/** The diagnostics of the check, in the order of the program. */
|
|
446
|
+
readonly diagnostics: Diagnostic[];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* This function checks a program and gives the Mermaid flowchart of its IR. A node with an error
|
|
451
|
+
* gets the class `error`. No call of the program starts.
|
|
452
|
+
*/
|
|
453
|
+
export function diagramProgram(program: unknown, options: CheckOptions & Pick<MermaidOptions, "direction">): DiagramResult {
|
|
454
|
+
const result = checkProgram(program, options);
|
|
455
|
+
return {
|
|
456
|
+
ok: result.ok,
|
|
457
|
+
mermaid: toMermaid(result.ir, { direction: options.direction, diagnostics: result.diagnostics }),
|
|
458
|
+
diagnostics: result.diagnostics,
|
|
459
|
+
};
|
|
460
|
+
}
|
package/src/module.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The procedures of @procedures/compiler as data. Importing this file registers nothing.
|
|
3
|
+
*
|
|
4
|
+
* Scaffold a procedure with: mark procedure new <name> --path packages/compiler
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
createProcedure,
|
|
9
|
+
defineModule,
|
|
10
|
+
registerModules,
|
|
11
|
+
zodAdapter,
|
|
12
|
+
PROCEDURE_REGISTRY,
|
|
13
|
+
RAW_INPUT_TAG,
|
|
14
|
+
REGISTRY_READ_EFFECTS,
|
|
15
|
+
type AnyProcedure,
|
|
16
|
+
type ProcedureContext,
|
|
17
|
+
type ProcedureModule,
|
|
18
|
+
type ProcedureRegistry,
|
|
19
|
+
} from "@procedures/client/browser";
|
|
20
|
+
import { programCheck } from "./procedures/program/check.js";
|
|
21
|
+
import { programCompile } from "./procedures/program/compile.js";
|
|
22
|
+
import { programDiagram } from "./procedures/program/diagram.js";
|
|
23
|
+
import { programExplain } from "./procedures/program/explain.js";
|
|
24
|
+
import { sdkOpenrpc } from "./procedures/sdk/openrpc.js";
|
|
25
|
+
import { sdkTypescript } from "./procedures/sdk/typescript.js";
|
|
26
|
+
import {
|
|
27
|
+
ProgramCheckInputSchema,
|
|
28
|
+
ProgramCheckOutputSchema,
|
|
29
|
+
ProgramCompileInputSchema,
|
|
30
|
+
ProgramCompileOutputSchema,
|
|
31
|
+
ProgramDiagramInputSchema,
|
|
32
|
+
ProgramDiagramOutputSchema,
|
|
33
|
+
ProgramExplainInputSchema,
|
|
34
|
+
ProgramExplainOutputSchema,
|
|
35
|
+
SdkOpenrpcInputSchema,
|
|
36
|
+
SdkOpenrpcOutputSchema,
|
|
37
|
+
SdkTypescriptInputSchema,
|
|
38
|
+
SdkTypescriptOutputSchema,
|
|
39
|
+
type ProgramCheckInput,
|
|
40
|
+
type ProgramCheckOutput,
|
|
41
|
+
type ProgramCompileInput,
|
|
42
|
+
type ProgramCompileOutput,
|
|
43
|
+
type ProgramDiagramInput,
|
|
44
|
+
type ProgramDiagramOutput,
|
|
45
|
+
type ProgramExplainInput,
|
|
46
|
+
type ProgramExplainOutput,
|
|
47
|
+
type SdkOpenrpcInput,
|
|
48
|
+
type SdkOpenrpcOutput,
|
|
49
|
+
type SdkTypescriptInput,
|
|
50
|
+
type SdkTypescriptOutput,
|
|
51
|
+
} from "./types.js";
|
|
52
|
+
|
|
53
|
+
// The program is data for these procedures: the raw-input tag keeps hydration from starting it.
|
|
54
|
+
// They read the registry and change nothing.
|
|
55
|
+
const programCheckProcedure = createProcedure()
|
|
56
|
+
.path(["program", "check"])
|
|
57
|
+
.input(zodAdapter<ProgramCheckInput>(ProgramCheckInputSchema))
|
|
58
|
+
.output(zodAdapter<ProgramCheckOutput>(ProgramCheckOutputSchema))
|
|
59
|
+
.meta({
|
|
60
|
+
description: "Check a program before it runs: names, procedures, inputs, the capability and the effects",
|
|
61
|
+
tags: ["compiler", RAW_INPUT_TAG],
|
|
62
|
+
effects: REGISTRY_READ_EFFECTS,
|
|
63
|
+
args: [],
|
|
64
|
+
shorts: {},
|
|
65
|
+
output: "json",
|
|
66
|
+
})
|
|
67
|
+
.handler(async (input: ProgramCheckInput, ctx: ProcedureContext): Promise<ProgramCheckOutput> => programCheck(input, ctx))
|
|
68
|
+
.build();
|
|
69
|
+
|
|
70
|
+
const programCompileProcedure = createProcedure()
|
|
71
|
+
.path(["program", "compile"])
|
|
72
|
+
.input(zodAdapter<ProgramCompileInput>(ProgramCompileInputSchema))
|
|
73
|
+
.output(zodAdapter<ProgramCompileOutput>(ProgramCompileOutputSchema))
|
|
74
|
+
.meta({
|
|
75
|
+
description: "Compile a program to a JavaScript function that makes the same calls in the same order",
|
|
76
|
+
tags: ["compiler", RAW_INPUT_TAG],
|
|
77
|
+
effects: REGISTRY_READ_EFFECTS,
|
|
78
|
+
args: [],
|
|
79
|
+
shorts: {},
|
|
80
|
+
output: "json",
|
|
81
|
+
})
|
|
82
|
+
.handler(async (input: ProgramCompileInput, ctx: ProcedureContext): Promise<ProgramCompileOutput> => programCompile(input, ctx))
|
|
83
|
+
.build();
|
|
84
|
+
|
|
85
|
+
const programDiagramProcedure = createProcedure()
|
|
86
|
+
.path(["program", "diagram"])
|
|
87
|
+
.input(zodAdapter<ProgramDiagramInput>(ProgramDiagramInputSchema))
|
|
88
|
+
.output(zodAdapter<ProgramDiagramOutput>(ProgramDiagramOutputSchema))
|
|
89
|
+
.meta({
|
|
90
|
+
description: "Draw a program as a Mermaid flowchart: the order of the calls, the control flow and the names",
|
|
91
|
+
tags: ["compiler", RAW_INPUT_TAG],
|
|
92
|
+
effects: REGISTRY_READ_EFFECTS,
|
|
93
|
+
args: [],
|
|
94
|
+
shorts: {},
|
|
95
|
+
output: "json",
|
|
96
|
+
})
|
|
97
|
+
.handler(async (input: ProgramDiagramInput, ctx: ProcedureContext): Promise<ProgramDiagramOutput> => programDiagram(input, ctx))
|
|
98
|
+
.build();
|
|
99
|
+
|
|
100
|
+
const programExplainProcedure = createProcedure()
|
|
101
|
+
.path(["program", "explain"])
|
|
102
|
+
.input(zodAdapter<ProgramExplainInput>(ProgramExplainInputSchema))
|
|
103
|
+
.output(zodAdapter<ProgramExplainOutput>(ProgramExplainOutputSchema))
|
|
104
|
+
.meta({
|
|
105
|
+
description: "Explain a program before it runs: its effects, each call with the answer of the capability, and the problems",
|
|
106
|
+
tags: ["compiler", RAW_INPUT_TAG],
|
|
107
|
+
effects: REGISTRY_READ_EFFECTS,
|
|
108
|
+
args: [],
|
|
109
|
+
shorts: {},
|
|
110
|
+
output: "json",
|
|
111
|
+
})
|
|
112
|
+
.handler(async (input: ProgramExplainInput, ctx: ProcedureContext): Promise<ProgramExplainOutput> => programExplain(input, ctx))
|
|
113
|
+
.build();
|
|
114
|
+
|
|
115
|
+
// The SDK procedures read the manifest of the caller's registry, as procedure.manifest does.
|
|
116
|
+
// The namespace sdk is not a tool namespace of the MCP server.
|
|
117
|
+
const sdkOpenrpcProcedure = createProcedure()
|
|
118
|
+
.path(["sdk", "openrpc"])
|
|
119
|
+
.input(zodAdapter<SdkOpenrpcInput>(SdkOpenrpcInputSchema))
|
|
120
|
+
.output(zodAdapter<SdkOpenrpcOutput>(SdkOpenrpcOutputSchema))
|
|
121
|
+
.meta({
|
|
122
|
+
description: "Describe the exposed procedures as an OpenRPC 1.3 document",
|
|
123
|
+
tags: ["compiler", "sdk"],
|
|
124
|
+
effects: REGISTRY_READ_EFFECTS,
|
|
125
|
+
args: [],
|
|
126
|
+
shorts: { namespace: "n" },
|
|
127
|
+
output: "json",
|
|
128
|
+
})
|
|
129
|
+
.handler(async (input: SdkOpenrpcInput, ctx: ProcedureContext): Promise<SdkOpenrpcOutput> => sdkOpenrpc(input, ctx))
|
|
130
|
+
.build();
|
|
131
|
+
|
|
132
|
+
const sdkTypescriptProcedure = createProcedure()
|
|
133
|
+
.path(["sdk", "typescript"])
|
|
134
|
+
.input(zodAdapter<SdkTypescriptInput>(SdkTypescriptInputSchema))
|
|
135
|
+
.output(zodAdapter<SdkTypescriptOutput>(SdkTypescriptOutputSchema))
|
|
136
|
+
.meta({
|
|
137
|
+
description: "Write a typed TypeScript SDK for the exposed procedures",
|
|
138
|
+
tags: ["compiler", "sdk"],
|
|
139
|
+
effects: REGISTRY_READ_EFFECTS,
|
|
140
|
+
args: [],
|
|
141
|
+
shorts: { namespace: "n" },
|
|
142
|
+
output: "json",
|
|
143
|
+
})
|
|
144
|
+
.handler(async (input: SdkTypescriptInput, ctx: ProcedureContext): Promise<SdkTypescriptOutput> => sdkTypescript(input, ctx))
|
|
145
|
+
.build();
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The module of this package, for `createRegistry()`: `program.check`, `program.compile`,
|
|
149
|
+
* `program.diagram`, `program.explain`, `sdk.openrpc` and `sdk.typescript`.
|
|
150
|
+
*/
|
|
151
|
+
export const compilerModule: ProcedureModule = defineModule("@procedures/compiler", [
|
|
152
|
+
programCheckProcedure as AnyProcedure,
|
|
153
|
+
programCompileProcedure as AnyProcedure,
|
|
154
|
+
programDiagramProcedure as AnyProcedure,
|
|
155
|
+
programExplainProcedure as AnyProcedure,
|
|
156
|
+
sdkOpenrpcProcedure as AnyProcedure,
|
|
157
|
+
sdkTypescriptProcedure as AnyProcedure,
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
/** The same module under the name of its namespace. */
|
|
161
|
+
export const programModule: ProcedureModule = compilerModule;
|
|
162
|
+
|
|
163
|
+
export default compilerModule;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* This function registers the procedures of @procedures/compiler in a registry. The default is the
|
|
167
|
+
* global registry.
|
|
168
|
+
*/
|
|
169
|
+
export function registerCompilerProcedures(registry: ProcedureRegistry = PROCEDURE_REGISTRY): void {
|
|
170
|
+
registerModules(registry, [compilerModule]);
|
|
171
|
+
}
|