@procedures/compiler 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +21 -0
  3. package/README.md +254 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/checks.d.ts +24 -0
  6. package/dist/checks.d.ts.map +1 -0
  7. package/dist/checks.js +123 -0
  8. package/dist/checks.js.map +1 -0
  9. package/dist/codegen-closure.d.ts +19 -0
  10. package/dist/codegen-closure.d.ts.map +1 -0
  11. package/dist/codegen-closure.js +200 -0
  12. package/dist/codegen-closure.js.map +1 -0
  13. package/dist/codegen-text.d.ts +44 -0
  14. package/dist/codegen-text.d.ts.map +1 -0
  15. package/dist/codegen-text.js +360 -0
  16. package/dist/codegen-text.js.map +1 -0
  17. package/dist/diagnostics.d.ts +67 -0
  18. package/dist/diagnostics.d.ts.map +1 -0
  19. package/dist/diagnostics.js +38 -0
  20. package/dist/diagnostics.js.map +1 -0
  21. package/dist/effects.d.ts +63 -0
  22. package/dist/effects.d.ts.map +1 -0
  23. package/dist/effects.js +185 -0
  24. package/dist/effects.js.map +1 -0
  25. package/dist/explain.d.ts +58 -0
  26. package/dist/explain.d.ts.map +1 -0
  27. package/dist/explain.js +144 -0
  28. package/dist/explain.js.map +1 -0
  29. package/dist/facts.d.ts +66 -0
  30. package/dist/facts.d.ts.map +1 -0
  31. package/dist/facts.js +102 -0
  32. package/dist/facts.js.map +1 -0
  33. package/dist/fold.d.ts +27 -0
  34. package/dist/fold.d.ts.map +1 -0
  35. package/dist/fold.js +128 -0
  36. package/dist/fold.js.map +1 -0
  37. package/dist/index.d.ts +35 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +22 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/interceptor.d.ts +51 -0
  42. package/dist/interceptor.d.ts.map +1 -0
  43. package/dist/interceptor.js +116 -0
  44. package/dist/interceptor.js.map +1 -0
  45. package/dist/ir.d.ts +240 -0
  46. package/dist/ir.d.ts.map +1 -0
  47. package/dist/ir.js +74 -0
  48. package/dist/ir.js.map +1 -0
  49. package/dist/mermaid.d.ts +53 -0
  50. package/dist/mermaid.d.ts.map +1 -0
  51. package/dist/mermaid.js +423 -0
  52. package/dist/mermaid.js.map +1 -0
  53. package/dist/module.d.ts +20 -0
  54. package/dist/module.d.ts.map +1 -0
  55. package/dist/module.js +124 -0
  56. package/dist/module.js.map +1 -0
  57. package/dist/normalize.d.ts +43 -0
  58. package/dist/normalize.d.ts.map +1 -0
  59. package/dist/normalize.js +529 -0
  60. package/dist/normalize.js.map +1 -0
  61. package/dist/openrpc.d.ts +63 -0
  62. package/dist/openrpc.d.ts.map +1 -0
  63. package/dist/openrpc.js +57 -0
  64. package/dist/openrpc.js.map +1 -0
  65. package/dist/print.d.ts +8 -0
  66. package/dist/print.d.ts.map +1 -0
  67. package/dist/print.js +148 -0
  68. package/dist/print.js.map +1 -0
  69. package/dist/procedures/program/check.d.ts +14 -0
  70. package/dist/procedures/program/check.d.ts.map +1 -0
  71. package/dist/procedures/program/check.js +30 -0
  72. package/dist/procedures/program/check.js.map +1 -0
  73. package/dist/procedures/program/compile.d.ts +14 -0
  74. package/dist/procedures/program/compile.d.ts.map +1 -0
  75. package/dist/procedures/program/compile.js +35 -0
  76. package/dist/procedures/program/compile.js.map +1 -0
  77. package/dist/procedures/program/diagram.d.ts +14 -0
  78. package/dist/procedures/program/diagram.d.ts.map +1 -0
  79. package/dist/procedures/program/diagram.js +23 -0
  80. package/dist/procedures/program/diagram.js.map +1 -0
  81. package/dist/procedures/program/effects-output.d.ts +8 -0
  82. package/dist/procedures/program/effects-output.d.ts.map +1 -0
  83. package/dist/procedures/program/effects-output.js +13 -0
  84. package/dist/procedures/program/effects-output.js.map +1 -0
  85. package/dist/procedures/program/explain.d.ts +14 -0
  86. package/dist/procedures/program/explain.d.ts.map +1 -0
  87. package/dist/procedures/program/explain.js +43 -0
  88. package/dist/procedures/program/explain.js.map +1 -0
  89. package/dist/procedures/program/index.d.ts +5 -0
  90. package/dist/procedures/program/index.d.ts.map +1 -0
  91. package/dist/procedures/program/index.js +5 -0
  92. package/dist/procedures/program/index.js.map +1 -0
  93. package/dist/procedures/sdk/index.d.ts +3 -0
  94. package/dist/procedures/sdk/index.d.ts.map +1 -0
  95. package/dist/procedures/sdk/index.js +3 -0
  96. package/dist/procedures/sdk/index.js.map +1 -0
  97. package/dist/procedures/sdk/manifest.d.ts +11 -0
  98. package/dist/procedures/sdk/manifest.d.ts.map +1 -0
  99. package/dist/procedures/sdk/manifest.js +15 -0
  100. package/dist/procedures/sdk/manifest.js.map +1 -0
  101. package/dist/procedures/sdk/openrpc.d.ts +11 -0
  102. package/dist/procedures/sdk/openrpc.d.ts.map +1 -0
  103. package/dist/procedures/sdk/openrpc.js +13 -0
  104. package/dist/procedures/sdk/openrpc.js.map +1 -0
  105. package/dist/procedures/sdk/typescript.d.ts +11 -0
  106. package/dist/procedures/sdk/typescript.d.ts.map +1 -0
  107. package/dist/procedures/sdk/typescript.js +14 -0
  108. package/dist/procedures/sdk/typescript.js.map +1 -0
  109. package/dist/program.d.ts +84 -0
  110. package/dist/program.d.ts.map +1 -0
  111. package/dist/program.js +108 -0
  112. package/dist/program.js.map +1 -0
  113. package/dist/refs.d.ts +9 -0
  114. package/dist/refs.d.ts.map +1 -0
  115. package/dist/refs.js +9 -0
  116. package/dist/refs.js.map +1 -0
  117. package/dist/register.d.ts +6 -0
  118. package/dist/register.d.ts.map +1 -0
  119. package/dist/register.js +8 -0
  120. package/dist/register.js.map +1 -0
  121. package/dist/runtime.d.ts +68 -0
  122. package/dist/runtime.d.ts.map +1 -0
  123. package/dist/runtime.js +138 -0
  124. package/dist/runtime.js.map +1 -0
  125. package/dist/schema.d.ts +41 -0
  126. package/dist/schema.d.ts.map +1 -0
  127. package/dist/schema.js +325 -0
  128. package/dist/schema.js.map +1 -0
  129. package/dist/scopes.d.ts +25 -0
  130. package/dist/scopes.d.ts.map +1 -0
  131. package/dist/scopes.js +202 -0
  132. package/dist/scopes.js.map +1 -0
  133. package/dist/types.d.ts +129 -0
  134. package/dist/types.d.ts.map +1 -0
  135. package/dist/types.js +157 -0
  136. package/dist/types.js.map +1 -0
  137. package/dist/typescript-sdk.d.ts +34 -0
  138. package/dist/typescript-sdk.d.ts.map +1 -0
  139. package/dist/typescript-sdk.js +231 -0
  140. package/dist/typescript-sdk.js.map +1 -0
  141. package/package.json +83 -3
  142. package/src/checks.ts +128 -0
  143. package/src/codegen-closure.ts +224 -0
  144. package/src/codegen-text.ts +396 -0
  145. package/src/diagnostics.ts +98 -0
  146. package/src/effects.ts +231 -0
  147. package/src/explain.ts +194 -0
  148. package/src/facts.ts +161 -0
  149. package/src/fold.ts +136 -0
  150. package/src/index.ts +35 -0
  151. package/src/interceptor.ts +149 -0
  152. package/src/ir.ts +330 -0
  153. package/src/mermaid.ts +460 -0
  154. package/src/module.ts +171 -0
  155. package/src/normalize.ts +694 -0
  156. package/src/openrpc.ts +104 -0
  157. package/src/print.ts +143 -0
  158. package/src/procedures/program/check.ts +31 -0
  159. package/src/procedures/program/compile.ts +36 -0
  160. package/src/procedures/program/diagram.ts +25 -0
  161. package/src/procedures/program/effects-output.ts +16 -0
  162. package/src/procedures/program/explain.ts +46 -0
  163. package/src/procedures/program/index.ts +4 -0
  164. package/src/procedures/sdk/index.ts +2 -0
  165. package/src/procedures/sdk/manifest.ts +17 -0
  166. package/src/procedures/sdk/openrpc.ts +16 -0
  167. package/src/procedures/sdk/typescript.ts +17 -0
  168. package/src/program.ts +181 -0
  169. package/src/refs.ts +9 -0
  170. package/src/register.ts +10 -0
  171. package/src/runtime.ts +188 -0
  172. package/src/schema.ts +318 -0
  173. package/src/scopes.ts +196 -0
  174. package/src/types.ts +318 -0
  175. package/src/typescript-sdk.ts +282 -0
package/src/effects.ts ADDED
@@ -0,0 +1,231 @@
1
+ /**
2
+ * The effects pass: the effects of the whole program, from the bottom up.
3
+ *
4
+ * - `may` is the upper bound: the effects of each call that can start. A sequence and the
5
+ * branches give the union. A call that does not declare its effects, or whose program the
6
+ * compiler cannot see, makes the effects unknown: the check fails closed.
7
+ * - `must` is the lower bound: the effects of each run in which no call fails. A sequence gives
8
+ * the union. Two branches give the condition and the intersection of the branches. A part
9
+ * that can start zero times (a later operand of `and`, the `fn` of `map`) gives nothing.
10
+ */
11
+
12
+ import { isDestructive, isReadOnly, type Effects, type ProcedureEntry, type ProcedurePath } from "@procedures/client/browser";
13
+ import type { CallNode, DynamicNode, IRNode, ItemsSpec, Pointer } from "./ir.js";
14
+ import type { ProcedureFacts } from "./facts.js";
15
+
16
+ /** The upper bound of the effects. An undefined list: the compiler does not know it. */
17
+ export interface MayEffects {
18
+ readonly reads: ReadonlySet<string> | undefined;
19
+ readonly writes: ReadonlySet<string> | undefined;
20
+ /** True: a call can delete or overwrite data. */
21
+ readonly destructive: boolean;
22
+ /** True: each call that writes is idempotent. */
23
+ readonly idempotent: boolean;
24
+ }
25
+
26
+ /** The lower bound of the effects. */
27
+ export interface MustEffects {
28
+ readonly reads: ReadonlySet<string>;
29
+ readonly writes: ReadonlySet<string>;
30
+ }
31
+
32
+ /** The effects of a node. */
33
+ export interface EffectSummary {
34
+ readonly may: MayEffects;
35
+ readonly must: MustEffects;
36
+ }
37
+
38
+ const EMPTY_SET: ReadonlySet<string> = new Set();
39
+
40
+ /** The effects of a part that does nothing. */
41
+ export const NO_EFFECTS: EffectSummary = {
42
+ may: { reads: EMPTY_SET, writes: EMPTY_SET, destructive: false, idempotent: true },
43
+ must: { reads: EMPTY_SET, writes: EMPTY_SET },
44
+ };
45
+
46
+ /** The effects of a part that the compiler does not know. */
47
+ export const UNKNOWN_EFFECTS: EffectSummary = {
48
+ may: { reads: undefined, writes: undefined, destructive: true, idempotent: false },
49
+ must: { reads: EMPTY_SET, writes: EMPTY_SET },
50
+ };
51
+
52
+ function union(a: ReadonlySet<string> | undefined, b: ReadonlySet<string> | undefined): ReadonlySet<string> | undefined {
53
+ if (!a || !b) return undefined;
54
+ if (a.size === 0) return b;
55
+ if (b.size === 0) return a;
56
+ return new Set([...a, ...b]);
57
+ }
58
+
59
+ function unionMust(a: ReadonlySet<string>, b: ReadonlySet<string>): ReadonlySet<string> {
60
+ return union(a, b) ?? EMPTY_SET;
61
+ }
62
+
63
+ /** The effects of two parts that both start, one after the other. */
64
+ export function sequence(...parts: EffectSummary[]): EffectSummary {
65
+ return parts.reduce(
66
+ (a, b) => ({
67
+ may: {
68
+ reads: union(a.may.reads, b.may.reads),
69
+ writes: union(a.may.writes, b.may.writes),
70
+ destructive: a.may.destructive || b.may.destructive,
71
+ idempotent: a.may.idempotent && b.may.idempotent,
72
+ },
73
+ must: { reads: unionMust(a.must.reads, b.must.reads), writes: unionMust(a.must.writes, b.must.writes) },
74
+ }),
75
+ NO_EFFECTS,
76
+ );
77
+ }
78
+
79
+ /** The effects of a part that can start zero times. */
80
+ export function optional(part: EffectSummary): EffectSummary {
81
+ return { may: part.may, must: NO_EFFECTS.must };
82
+ }
83
+
84
+ /** The effects of one of two parts. */
85
+ export function either(a: EffectSummary, b: EffectSummary): EffectSummary {
86
+ const intersect = (x: ReadonlySet<string>, y: ReadonlySet<string>): ReadonlySet<string> => new Set([...x].filter((name) => y.has(name)));
87
+ return {
88
+ may: sequence(a, b).may,
89
+ must: { reads: intersect(a.must.reads, b.must.reads), writes: intersect(a.must.writes, b.must.writes) },
90
+ };
91
+ }
92
+
93
+ /** The effects that an entry declares. Absent effects, or an absent list, are unknown. */
94
+ export function declaredEffects(entry: ProcedureEntry | undefined): EffectSummary {
95
+ const effects = entry?.effects;
96
+ if (!effects) return UNKNOWN_EFFECTS;
97
+ return {
98
+ may: {
99
+ reads: effects.reads ? new Set(effects.reads) : undefined,
100
+ writes: effects.writes ? new Set(effects.writes) : undefined,
101
+ destructive: isDestructive(effects),
102
+ idempotent: isReadOnly(effects) || effects.idempotent === true,
103
+ },
104
+ must: { reads: new Set(effects.reads ?? []), writes: new Set(effects.writes ?? []) },
105
+ };
106
+ }
107
+
108
+ /** True when the effects are not fully known. */
109
+ export function isUnknown(summary: EffectSummary): boolean {
110
+ return summary.may.reads === undefined || summary.may.writes === undefined;
111
+ }
112
+
113
+ /** The effects as the `Effects` of a manifest: an unknown list is absent. */
114
+ export function toEffects(may: MayEffects): Effects {
115
+ return {
116
+ ...(may.reads ? { reads: [...may.reads].sort() } : {}),
117
+ ...(may.writes ? { writes: [...may.writes].sort() } : {}),
118
+ destructive: may.destructive,
119
+ idempotent: may.idempotent,
120
+ };
121
+ }
122
+
123
+ /** The result of the pass. */
124
+ export interface EffectsAnalysis {
125
+ readonly program: EffectSummary;
126
+ /** The effects of each node. */
127
+ readonly byNode: ReadonlyMap<IRNode, EffectSummary>;
128
+ /** The effects of the program that each data-driven call runs (its body), when the compiler knows them. */
129
+ readonly nested: ReadonlyMap<IRNode, EffectSummary>;
130
+ /** The pointers of the calls whose effects the compiler does not know. */
131
+ readonly unknownAt: readonly Pointer[];
132
+ }
133
+
134
+ /** The effects of the body of a defined procedure, or undefined when the compiler cannot see it. */
135
+ export type BodyEffects = (path: ProcedurePath) => EffectSummary | undefined;
136
+
137
+ /** This function infers the effects of each node and of the program. */
138
+ export function inferEffects(root: IRNode, facts: ProcedureFacts, bodyEffects: BodyEffects): EffectsAnalysis {
139
+ const byNode = new Map<IRNode, EffectSummary>();
140
+ const nested = new Map<IRNode, EffectSummary>();
141
+ const unknownAt: Pointer[] = [];
142
+
143
+ const items = (spec: ItemsSpec): { summary: EffectSummary; nonEmpty: boolean } =>
144
+ spec.kind === "list"
145
+ ? { summary: sequence(...spec.items.map(infer)), nonEmpty: spec.items.length > 0 }
146
+ : { summary: infer(spec.value), nonEmpty: false };
147
+
148
+ /** The effects of what a data-driven procedure runs. */
149
+ const inner = (node: CallNode | DynamicNode, entry: ProcedureEntry | undefined): EffectSummary => {
150
+ if (node.kind === "dynamic") {
151
+ if (node.model) return infer(node.model);
152
+ if (node.reason === "invalid-control-flow") return NO_EFFECTS;
153
+ if (node.reason === "raw-input" && entry && !entry.dataDriven) return NO_EFFECTS;
154
+ // procedure.define runs nothing now: the body runs when the new procedure starts
155
+ if (node.key === "procedure.define" && entry?.dataDriven) return NO_EFFECTS;
156
+ }
157
+ if (entry && !entry.dataDriven) return NO_EFFECTS;
158
+ if (entry) {
159
+ const body = bodyEffects(node.path);
160
+ if (body) return body;
161
+ }
162
+ return UNKNOWN_EFFECTS;
163
+ };
164
+
165
+ const infer = (node: IRNode): EffectSummary => {
166
+ let summary: EffectSummary;
167
+ switch (node.kind) {
168
+ case "literal":
169
+ case "reference":
170
+ case "fail":
171
+ summary = NO_EFFECTS;
172
+ break;
173
+ case "object":
174
+ summary = sequence(...node.entries.map((entry) => infer(entry.value)));
175
+ break;
176
+ case "array":
177
+ summary = sequence(...node.items.map(infer));
178
+ break;
179
+ case "deferred":
180
+ summary = infer(node.input);
181
+ break;
182
+ case "call":
183
+ case "dynamic": {
184
+ const entry = facts.entry(node.path);
185
+ const runs = inner(node, entry);
186
+ if (runs !== NO_EFFECTS) nested.set(node, runs);
187
+ summary = sequence(infer(node.input), declaredEffects(entry), runs);
188
+ if (isUnknown(declaredEffects(entry)) || isUnknown(runs)) unknownAt.push(node.pointer);
189
+ break;
190
+ }
191
+ case "chain":
192
+ summary = sequence(declaredEffects(facts.entry(node.path)), ...(node.initialInput ? [infer(node.initialInput)] : []), ...node.steps.map((step) => infer(step.node)));
193
+ break;
194
+ case "parallel":
195
+ summary = sequence(declaredEffects(facts.entry(node.path)), ...node.tasks.map(infer));
196
+ break;
197
+ case "conditional": {
198
+ const thenEffects = node.then ? infer(node.then) : NO_EFFECTS;
199
+ const elseEffects = node.else ? infer(node.else) : NO_EFFECTS;
200
+ summary = sequence(declaredEffects(facts.entry(node.path)), infer(node.condition), either(thenEffects, elseEffects));
201
+ break;
202
+ }
203
+ case "try":
204
+ summary = sequence(declaredEffects(facts.entry(node.path)), infer(node.try), optional(infer(node.catch)));
205
+ break;
206
+ case "and":
207
+ case "or": {
208
+ const [first, ...rest] = node.values.map(infer);
209
+ summary = sequence(declaredEffects(facts.entry(node.path)), ...(first ? [first] : []), ...rest.map(optional));
210
+ break;
211
+ }
212
+ case "map": {
213
+ const { summary: itemEffects, nonEmpty } = items(node.items);
214
+ const fn = node.fn ? infer(node.fn) : NO_EFFECTS;
215
+ summary = sequence(declaredEffects(facts.entry(node.path)), itemEffects, nonEmpty ? fn : optional(fn));
216
+ break;
217
+ }
218
+ case "reduce": {
219
+ const itemPart = node.items ? items(node.items) : { summary: NO_EFFECTS, nonEmpty: false };
220
+ const fn = node.fn ? infer(node.fn) : NO_EFFECTS;
221
+ summary = sequence(declaredEffects(facts.entry(node.path)), itemPart.summary, infer(node.initial), itemPart.nonEmpty ? fn : optional(fn));
222
+ break;
223
+ }
224
+ }
225
+ byNode.set(node, summary);
226
+ return summary;
227
+ };
228
+
229
+ const program = infer(root);
230
+ return { program, byNode, nested, unknownAt };
231
+ }
package/src/explain.ts ADDED
@@ -0,0 +1,194 @@
1
+ /**
2
+ * The explanation of a program (roadmap R23): the check and `explainInvocation()` of the client
3
+ * in one result.
4
+ *
5
+ * The result has the effects of the program, each call with its declared effects, the answer of
6
+ * the capability for the call, and the diagnostics. A short text in plain words tells the same.
7
+ * An application can show the text to a user before the user starts the program.
8
+ */
9
+
10
+ import {
11
+ explainInvocation,
12
+ type AnyProcedure,
13
+ type Capability,
14
+ type CapabilityRefusalReason,
15
+ type Effects,
16
+ type ProcedurePath,
17
+ } from "@procedures/client/browser";
18
+ import { isCallLike, walk, type IRNode, type Pointer } from "./ir.js";
19
+ import { isStringPath, withDefinitions, type ProcedureFacts } from "./facts.js";
20
+ import { findDefinitions } from "./normalize.js";
21
+ import { baseFacts, checkProgram, type CheckOptions, type MustEffects } from "./program.js";
22
+ import type { Diagnostic } from "./diagnostics.js";
23
+
24
+ /** The kind of a call: a call of a procedure, a control-flow call or a dynamic call. */
25
+ export type CallKind = "call" | "control" | "dynamic";
26
+
27
+ /** The explanation of one call of a program. */
28
+ export interface CallExplanation {
29
+ /** The key of the procedure: its path with dots. */
30
+ readonly key: string;
31
+ readonly path: ProcedurePath;
32
+ /** The JSON pointer of the call in the program. */
33
+ readonly pointer: Pointer;
34
+ readonly kind: CallKind;
35
+ /** The effects that the procedure declares. Absent: the procedure does not declare its effects. */
36
+ readonly effects?: Effects | undefined;
37
+ /** True when the host has a procedure at the path. */
38
+ readonly known: boolean;
39
+ /** True when the host has the procedure and its expose rule lets a program call it. */
40
+ readonly exposed: boolean;
41
+ /** True when the capability permits the call (`explainInvocation()` of the client). */
42
+ readonly permitted: boolean;
43
+ /** The reason of a refusal, or `not-found` for a path without a procedure. */
44
+ readonly reason?: CapabilityRefusalReason | "not-found" | undefined;
45
+ /** The answer of `explainInvocation()` in words. */
46
+ readonly message: string;
47
+ }
48
+
49
+ /** The result of `explainProgram()`. */
50
+ export interface ProgramExplanation {
51
+ /** True when no diagnostic is an error. A host starts the program only then. */
52
+ readonly ok: boolean;
53
+ /** The effects that a run can have (the upper bound). An absent `reads` or `writes`: not known. */
54
+ readonly effects: Effects;
55
+ /** The effects of each run in which no call fails (the lower bound). */
56
+ readonly mustEffects: MustEffects;
57
+ /** The calls of the program, in the order of the tree: a call comes before the calls of its input. */
58
+ readonly calls: CallExplanation[];
59
+ /** The problems that the check found, in the order of the program. */
60
+ readonly diagnostics: Diagnostic[];
61
+ /** The hash of the manifest that the check used. */
62
+ readonly registryHash: string;
63
+ /** The explanation in short sentences of plain words. */
64
+ readonly summary: string;
65
+ }
66
+
67
+ /** A capability without its deadline: the static check does not know the time of the run. */
68
+ function withoutDeadline(capability: Capability<unknown> | undefined): Capability<unknown> | undefined {
69
+ if (!capability || capability.deadline === undefined) return capability;
70
+ const { deadline: _deadline, ...rest } = capability;
71
+ return rest;
72
+ }
73
+
74
+ /** A registry for `explainInvocation()` that has a procedure with the declared effects for each known path. */
75
+ function registryOf(facts: ProcedureFacts): { get(path: ProcedurePath): AnyProcedure | undefined } {
76
+ return {
77
+ get: (path) => {
78
+ const entry = facts.entry(path);
79
+ return entry ? ({ path, metadata: entry.effects ? { effects: entry.effects } : {} } as unknown as AnyProcedure) : undefined;
80
+ },
81
+ };
82
+ }
83
+
84
+ function kindOf(node: IRNode): CallKind {
85
+ return node.kind === "call" ? "call" : node.kind === "dynamic" ? "dynamic" : "control";
86
+ }
87
+
88
+ /** A list in words: "a", "a and b", "a, b and c". */
89
+ function listText(items: readonly string[]): string {
90
+ if (items.length <= 1) return items.join("");
91
+ return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
92
+ }
93
+
94
+ function count(value: number, noun: string): string {
95
+ return `${value} ${noun}${value === 1 ? "" : "s"}`;
96
+ }
97
+
98
+ /** Each call of a list with its place, at most three, and the number of the others. */
99
+ function callsText(calls: readonly CallExplanation[]): string {
100
+ const shown = calls.slice(0, 3).map((call) => `${call.key} at ${call.pointer === "" ? "the root" : call.pointer}`);
101
+ return calls.length > 3 ? `${shown.join(", ")} and ${calls.length - 3} more` : listText(shown);
102
+ }
103
+
104
+ /** The sentences of the effects. */
105
+ function effectsText(effects: Effects, must: MustEffects): string[] {
106
+ const sentences: string[] = [];
107
+ const names = (list: readonly string[]): string => (list.length === 0 ? "nothing" : listText(list));
108
+ if (effects.reads && effects.writes) {
109
+ const reads = effects.reads.length === 0 ? "reads nothing" : `can read ${names(effects.reads)}`;
110
+ const writes = effects.writes.length === 0 ? "changes nothing" : `can change ${names(effects.writes)}`;
111
+ sentences.push(`The program ${reads} and ${writes}.`);
112
+ } else if (effects.reads) {
113
+ sentences.push(`The program ${effects.reads.length === 0 ? "reads nothing" : `can read ${names(effects.reads)}`}. The compiler does not know what it changes.`);
114
+ } else if (effects.writes) {
115
+ sentences.push(`The compiler does not know what the program reads. It ${effects.writes.length === 0 ? "changes nothing" : `can change ${names(effects.writes)}`}.`);
116
+ } else {
117
+ sentences.push("The compiler does not know the effects of the program: a call does not declare them, or it runs a program that the compiler cannot see.");
118
+ }
119
+ if (effects.writes && effects.writes.length > 0 && effects.destructive) sentences.push("A call can delete or overwrite data.");
120
+ if (must.reads.length > 0 || must.writes.length > 0) {
121
+ const parts = [...(must.reads.length > 0 ? [`reads ${names(must.reads)}`] : []), ...(must.writes.length > 0 ? [`changes ${names(must.writes)}`] : [])];
122
+ sentences.push(`Each run in which no call fails ${parts.join(" and ")}.`);
123
+ }
124
+ return sentences;
125
+ }
126
+
127
+ /** The text of the explanation, in short sentences. */
128
+ function summaryOf(explanation: Omit<ProgramExplanation, "summary">): string {
129
+ const { calls, diagnostics } = explanation;
130
+ const sentences: string[] = [];
131
+ const keys = new Set(calls.map((call) => call.key));
132
+ sentences.push(`The program has ${count(calls.length, "call")} of ${count(keys.size, "procedure")}.`);
133
+ sentences.push(...effectsText(explanation.effects, explanation.mustEffects));
134
+ const unknown = calls.filter((call) => !call.known);
135
+ if (unknown.length > 0) sentences.push(`The host has no procedure for ${count(unknown.length, "call")}: ${callsText(unknown)}.`);
136
+ const hidden = calls.filter((call) => call.known && !call.exposed);
137
+ if (hidden.length > 0) sentences.push(`The host does not expose ${count(hidden.length, "call")}: ${callsText(hidden)}.`);
138
+ const refused = calls.filter((call) => call.known && !call.permitted);
139
+ if (refused.length > 0) sentences.push(`The capability refuses ${count(refused.length, "call")}: ${callsText(refused)}.`);
140
+ const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
141
+ const warnings = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
142
+ sentences.push(
143
+ errors === 0
144
+ ? "The check found no error, so the host can start the program."
145
+ : `The check found ${count(errors, "error")}, so the host does not start the program.`,
146
+ );
147
+ if (warnings > 0) sentences.push(`The check also found ${count(warnings, "warning")}.`);
148
+ return sentences.join(" ");
149
+ }
150
+
151
+ /**
152
+ * This function explains a program before it starts. It checks the program, and it asks
153
+ * `explainInvocation()` of the client about each call. No call of the program starts.
154
+ */
155
+ export function explainProgram(program: unknown, options: CheckOptions): ProgramExplanation {
156
+ const base = baseFacts(options);
157
+ const facts = withDefinitions(base, findDefinitions(program));
158
+ const result = checkProgram(program, { ...options, facts: base });
159
+ const capability = withoutDeadline(options.capability);
160
+ const registry = registryOf(facts);
161
+ const calls: CallExplanation[] = [];
162
+ const visit = (node: IRNode): void => {
163
+ if (!isCallLike(node)) return;
164
+ const path = node.path;
165
+ if (isStringPath(path) && path.length > 0 && path.every((segment) => segment.length > 0)) {
166
+ const entry = facts.entry(path);
167
+ const answer = explainInvocation(path, capability, registry);
168
+ calls.push({
169
+ key: node.key,
170
+ path: [...path],
171
+ pointer: node.pointer,
172
+ kind: kindOf(node),
173
+ ...(entry?.effects ? { effects: entry.effects } : {}),
174
+ known: entry !== undefined,
175
+ exposed: entry !== undefined && facts.exposed(path),
176
+ permitted: answer.permitted,
177
+ ...(answer.reason !== undefined ? { reason: answer.reason } : {}),
178
+ message: answer.message,
179
+ });
180
+ }
181
+ // The program that a dynamic call runs, when the compiler can see it (client.eval of a literal path)
182
+ if (node.kind === "dynamic" && node.model) walk(node.model, visit);
183
+ };
184
+ walk(result.ir.root, visit);
185
+ const explanation: Omit<ProgramExplanation, "summary"> = {
186
+ ok: result.ok,
187
+ effects: result.effects,
188
+ mustEffects: result.mustEffects,
189
+ calls,
190
+ diagnostics: result.diagnostics,
191
+ registryHash: result.registryHash,
192
+ };
193
+ return { ...explanation, summary: summaryOf(explanation) };
194
+ }
package/src/facts.ts ADDED
@@ -0,0 +1,161 @@
1
+ /**
2
+ * The facts about procedures that the compiler reads. They are the manifest entry of a path,
3
+ * the expose rule, the raw-input rule and the body of a defined procedure.
4
+ *
5
+ * A browser has only a manifest (`procedure.manifest`, roadmap R17). A host has the registry.
6
+ * The two functions of this module give the same facts from each source, so the passes do not
7
+ * know the source.
8
+ */
9
+
10
+ import {
11
+ allCoreProcedures,
12
+ describeProcedureEntry,
13
+ describeRegistry,
14
+ hashOf,
15
+ isControlFlowPath,
16
+ pathToKey,
17
+ runtimeProgramOf,
18
+ RAW_INPUT_TAG,
19
+ RUNS_REFS_TAG,
20
+ type AnyProcedure,
21
+ type ProcedureEntry,
22
+ type ProcedureManifest,
23
+ type ProcedurePath,
24
+ } from "@procedures/client/browser";
25
+
26
+ /** The procedure facts of the compiler. */
27
+ export interface ProcedureFacts {
28
+ /** The hash of the manifest that the host gives (`procedure.manifest`). */
29
+ readonly hash: string;
30
+ /** The keys of the procedures that take raw input, without the control-flow procedures. */
31
+ readonly rawKeys: readonly string[];
32
+ /** The manifest entry of a path, or undefined when the source does not have the path. */
33
+ entry(path: ProcedurePath): ProcedureEntry | undefined;
34
+ /** True when the host lets a data-driven procedure call the path. */
35
+ exposed(path: ProcedurePath): boolean;
36
+ /** True when hydration gives the procedure its input raw (`rawInputRule()` of the client). */
37
+ rawInput(path: ProcedurePath): boolean;
38
+ /** The program of a procedure that `procedure.define` made, when the source can see it. */
39
+ body(path: ProcedurePath): unknown;
40
+ /** The core procedure at the path, when the entry describes exactly that procedure. */
41
+ core(path: ProcedurePath): AnyProcedure | undefined;
42
+ }
43
+
44
+ /** A registry, as the compiler reads it. */
45
+ export interface RegistryLike {
46
+ get(path: ProcedurePath): AnyProcedure | undefined;
47
+ getAll(): AnyProcedure[];
48
+ }
49
+
50
+ /** The options of the fact sources. */
51
+ export interface FactsOptions {
52
+ /** The expose rule of the host. The default exposes each procedure. */
53
+ readonly expose?: ((path: ProcedurePath) => boolean) | undefined;
54
+ }
55
+
56
+ /** True when an entry takes its input raw: the rule of `takesRawInput()`, from the manifest. */
57
+ export function entryTakesRawInput(entry: ProcedureEntry): boolean {
58
+ const tags = entry.tags ?? [];
59
+ return isControlFlowPath(entry.path) || tags.includes(RUNS_REFS_TAG) || tags.includes(RAW_INPUT_TAG);
60
+ }
61
+
62
+ /** True when the path elements are strings: only such a path can name a procedure. */
63
+ export function isStringPath(path: readonly unknown[]): path is ProcedurePath {
64
+ return path.every((segment) => typeof segment === "string");
65
+ }
66
+
67
+ let coreDescriptions: Map<string, { procedure: AnyProcedure; hash: string }> | undefined;
68
+
69
+ /** The core procedures by key, with the hash of their manifest entries. */
70
+ function coreByKey(): Map<string, { procedure: AnyProcedure; hash: string }> {
71
+ if (!coreDescriptions) {
72
+ coreDescriptions = new Map(
73
+ allCoreProcedures.map((procedure) => [pathToKey(procedure.path), { procedure, hash: hashOf(describeProcedureEntry(procedure)) }]),
74
+ );
75
+ }
76
+ return coreDescriptions;
77
+ }
78
+
79
+ /**
80
+ * This function gives the core procedure that an entry describes. The entry must be the same as
81
+ * the description of the core procedure: a host can register a different procedure at a core path.
82
+ */
83
+ export function coreProcedureOf(entry: ProcedureEntry | undefined): AnyProcedure | undefined {
84
+ if (!entry) return undefined;
85
+ const core = coreByKey().get(entry.key);
86
+ return core && core.hash === hashOf(entry) ? core.procedure : undefined;
87
+ }
88
+
89
+ /** The facts of a manifest. The manifest has only the procedures that its host exposes. */
90
+ export function manifestFacts(manifest: ProcedureManifest, options: FactsOptions = {}): ProcedureFacts {
91
+ const byKey = new Map(manifest.procedures.map((entry) => [entry.key, entry]));
92
+ const expose = options.expose;
93
+ const rawKeys = manifest.procedures.filter((entry) => entryTakesRawInput(entry) && !isControlFlowPath(entry.path)).map((entry) => entry.key);
94
+ // The key of a path, as the registry finds it: the segments joined with dots, also for a segment that is not a string
95
+ const lookup = (path: ProcedurePath): ProcedureEntry | undefined => byKey.get(pathToKey(path));
96
+ return {
97
+ hash: manifest.hash,
98
+ rawKeys,
99
+ entry: lookup,
100
+ exposed: (path) => (expose ? expose(path) : true),
101
+ rawInput: (path) => {
102
+ const entry = lookup(path);
103
+ return entry ? entryTakesRawInput(entry) : isControlFlowPath(path);
104
+ },
105
+ body: () => undefined,
106
+ core: (path) => coreProcedureOf(lookup(path)),
107
+ };
108
+ }
109
+
110
+ /**
111
+ * The facts of a registry. The entries include the procedures that the host does not expose, so
112
+ * the check can tell "not exposed" from "unknown". The hash is the hash of `procedure.manifest` on
113
+ * the host: the manifest of the exposed procedures.
114
+ */
115
+ export function registryFacts(registry: RegistryLike, options: FactsOptions = {}): ProcedureFacts {
116
+ const manifest = describeRegistry(registry, { withoutHandler: true });
117
+ const expose = options.expose;
118
+ const exposedEntries = manifest.procedures.filter((entry) => entry.hasHandler && (!expose || expose(entry.path)));
119
+ const facts = manifestFacts({ ...manifest, hash: hashOf(exposedEntries) }, options);
120
+ return {
121
+ ...facts,
122
+ body: (path) => {
123
+ if (!isStringPath(path)) return undefined;
124
+ const procedure = registry.get(path);
125
+ return procedure ? runtimeProgramOf(procedure) : undefined;
126
+ },
127
+ };
128
+ }
129
+
130
+ /**
131
+ * The facts with the procedures that the program defines (`procedure.define` with a literal path
132
+ * and body). A later call of such a path is known, and its body gives its effects.
133
+ */
134
+ export function withDefinitions(facts: ProcedureFacts, definitions: ReadonlyMap<string, Definition>): ProcedureFacts {
135
+ if (definitions.size === 0) return facts;
136
+ const defined = (path: ProcedurePath): Definition | undefined => (isStringPath(path) ? definitions.get(pathToKey(path)) : undefined);
137
+ const definedRaw = [...definitions.values()].filter((definition) => entryTakesRawInput(definition.entry)).map((definition) => definition.entry.key);
138
+ return {
139
+ hash: facts.hash,
140
+ rawKeys: [...new Set([...facts.rawKeys, ...definedRaw])],
141
+ entry: (path) => defined(path)?.entry ?? facts.entry(path),
142
+ exposed: (path) => facts.exposed(path),
143
+ rawInput: (path) => {
144
+ const definition = defined(path);
145
+ return definition ? entryTakesRawInput(definition.entry) : facts.rawInput(path);
146
+ },
147
+ body: (path) => {
148
+ const definition = defined(path);
149
+ return definition ? definition.body : facts.body(path);
150
+ },
151
+ core: (path) => (defined(path) ? undefined : facts.core(path)),
152
+ };
153
+ }
154
+
155
+ /** A procedure that the program defines with `procedure.define`. */
156
+ export interface Definition {
157
+ readonly entry: ProcedureEntry;
158
+ readonly body: unknown;
159
+ /** The pointer of the aggregation in the program. */
160
+ readonly pointer: string;
161
+ }