@procedures/compiler 0.25.0 → 0.26.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 +38 -0
- package/README.md +3 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/ir.d.ts +2 -0
- package/dist/ir.d.ts.map +1 -1
- package/dist/ir.js.map +1 -1
- package/dist/names.d.ts +27 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +83 -0
- package/dist/names.js.map +1 -0
- package/dist/normalize.js +2 -2
- package/dist/normalize.js.map +1 -1
- package/dist/program.js +4 -4
- package/dist/program.js.map +1 -1
- package/dist/scopes.d.ts +20 -14
- package/dist/scopes.d.ts.map +1 -1
- package/dist/scopes.js +32 -48
- package/dist/scopes.js.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +3 -1
- package/src/ir.ts +2 -0
- package/src/names.ts +105 -0
- package/src/normalize.ts +2 -2
- package/src/program.ts +4 -4
- package/src/scopes.ts +34 -51
package/src/names.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The names pass: the static kit of coeval checks the names of a program.
|
|
3
|
+
*
|
|
4
|
+
* The client changes the program into a run document of coeval (`desugarProgram()`, with the
|
|
5
|
+
* operands of the control-flow procedures as documents). The kinds of the document tell which
|
|
6
|
+
* names they bind for which parts: the steps of a chain, `last`, `item`, `index`, `acc` and
|
|
7
|
+
* `error`. Then `check()` of `@mark1russell7/coeval` finds each reference whose name no kind
|
|
8
|
+
* around it binds. This pass changes each issue into a diagnostic with the JSON pointer of the
|
|
9
|
+
* `$ref` in the program.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { check, walk, type DocVisit, type Key } from "@mark1russell7/coeval";
|
|
13
|
+
import {
|
|
14
|
+
PROGRAM_WORLD,
|
|
15
|
+
desugarInput,
|
|
16
|
+
desugarProgram,
|
|
17
|
+
isAnyProcedureRef,
|
|
18
|
+
nameOfKey,
|
|
19
|
+
originOf,
|
|
20
|
+
programMachine,
|
|
21
|
+
type ProcedurePath,
|
|
22
|
+
} from "@procedures/client/browser";
|
|
23
|
+
import type { Diagnostic } from "./diagnostics.js";
|
|
24
|
+
|
|
25
|
+
/** The options of the names pass. */
|
|
26
|
+
export interface NameOptions {
|
|
27
|
+
/** `exec`: the program operates as `client.exec()` operates it. `body`: as the body of a defined procedure, with the name `input`. */
|
|
28
|
+
readonly mode: "exec" | "body";
|
|
29
|
+
/** The pointer of the program in a bigger document (the body of a definition). */
|
|
30
|
+
readonly base?: string | undefined;
|
|
31
|
+
/** The raw-input rule of the procedures. The default knows the control-flow procedures. */
|
|
32
|
+
readonly rawInput?: ((path: ProcedurePath) => boolean) | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The name of the result of the step before, in the kernel. The `$proc` form writes it as `$last`. */
|
|
36
|
+
const LAST = "last";
|
|
37
|
+
|
|
38
|
+
/** The `$proc` text of a name of the kernel. */
|
|
39
|
+
function procName(key: string): string {
|
|
40
|
+
return key === LAST ? "$last" : nameOfKey(key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function placeKey(place: readonly Key[]): string {
|
|
44
|
+
return place.map(String).join("\u0000");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* This function gives the diagnostics of the names of a program. It uses `check()` of coeval on
|
|
49
|
+
* the run document of the program.
|
|
50
|
+
*/
|
|
51
|
+
export function nameDiagnostics(program: unknown, options: NameOptions): Diagnostic[] {
|
|
52
|
+
const body = options.mode === "body";
|
|
53
|
+
const settings = { rawInput: options.rawInput, scoped: body, expand: true };
|
|
54
|
+
const doc = isAnyProcedureRef(program) ? desugarProgram(program, settings) : body ? desugarInput(program, settings) : undefined;
|
|
55
|
+
if (!doc) return [];
|
|
56
|
+
const names = body ? ["input"] : [];
|
|
57
|
+
// The visit of each document, by its place. A part that is the whole value of its document has the
|
|
58
|
+
// place of its document: the deeper visit comes later, and it stays.
|
|
59
|
+
const visits = new Map<string, DocVisit>();
|
|
60
|
+
walk(PROGRAM_WORLD, doc, (visit) => void visits.set(placeKey(visit.place), visit), { names });
|
|
61
|
+
const issues = check(programMachine({ rawInput: options.rawInput ?? (() => false) }), doc, { names });
|
|
62
|
+
const diagnostics: Diagnostic[] = [];
|
|
63
|
+
for (const issue of issues) {
|
|
64
|
+
if (issue.code !== "#NAME?") continue;
|
|
65
|
+
const visit = visits.get(placeKey(issue.place));
|
|
66
|
+
const origin = visit ? originOf(visit.doc) : undefined;
|
|
67
|
+
const source = origin?.source as { readonly $ref?: unknown } | undefined;
|
|
68
|
+
if (!visit || !origin || typeof source?.$ref !== "string") continue;
|
|
69
|
+
const text = source.$ref;
|
|
70
|
+
const name = text.split(".")[0] ?? "";
|
|
71
|
+
const key = (visit.doc.value as readonly unknown[])[1];
|
|
72
|
+
const pointer = `${options.base ?? ""}${origin.pointer}`;
|
|
73
|
+
diagnostics.push(diagnosticOf(origin.deferred === true, visit, String(key), name, text, pointer));
|
|
74
|
+
}
|
|
75
|
+
return diagnostics;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** This function makes the diagnostic of one name that no kind binds. */
|
|
79
|
+
function diagnosticOf(deferred: boolean, visit: DocVisit, key: string, name: string, text: string, pointer: string): Diagnostic {
|
|
80
|
+
if (deferred) {
|
|
81
|
+
return {
|
|
82
|
+
severity: "info",
|
|
83
|
+
code: "unknown-name",
|
|
84
|
+
message: `No scope has the name "${name}" here. In the input of a deferred call, the $ref stays data for the runner.`,
|
|
85
|
+
pointer,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// The result of the step before is not the name of a step: the first step of a chain has no $last
|
|
89
|
+
if (key !== LAST && visit.later.has(key)) {
|
|
90
|
+
return {
|
|
91
|
+
severity: "error",
|
|
92
|
+
code: "use-before-definition",
|
|
93
|
+
message: `The $ref "${text}" reads "${name}" before the step that makes it: a step reads only the names of the steps before it.`,
|
|
94
|
+
pointer,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const known = [...new Set([...visit.names].map(procName))];
|
|
98
|
+
const here = known.length > 0 ? ` The names here: ${known.join(", ")}.` : " No name is in the scope here.";
|
|
99
|
+
return {
|
|
100
|
+
severity: "error",
|
|
101
|
+
code: "unknown-name",
|
|
102
|
+
message: `Unknown $ref name "${name}" in "${text}".${here}${name === "$last" ? " The first step of a chain has no $last." : ""}`,
|
|
103
|
+
pointer,
|
|
104
|
+
};
|
|
105
|
+
}
|
package/src/normalize.ts
CHANGED
|
@@ -130,7 +130,7 @@ class Normalizer {
|
|
|
130
130
|
if (mode === "body") {
|
|
131
131
|
const input = this.binding("input", "input", base);
|
|
132
132
|
const root = isAnyProcedureRef(program) ? this.rootCall(program, true, base) : this.hydrateRoot(program, true, base);
|
|
133
|
-
return { mode, root, input, bindingCount: this.bindingCount, program };
|
|
133
|
+
return { mode, root, input, bindingCount: this.bindingCount, program, base };
|
|
134
134
|
}
|
|
135
135
|
let root: IRNode;
|
|
136
136
|
if (isAnyProcedureRef(program)) {
|
|
@@ -142,7 +142,7 @@ class Normalizer {
|
|
|
142
142
|
this.report("error", "root-not-call", "The program must be a call ({ $proc, input }): client.exec() starts only a call.", base);
|
|
143
143
|
root = { kind: "fail", pointer: base, message: ROOT_NOT_CALL_MESSAGE };
|
|
144
144
|
}
|
|
145
|
-
return { mode, root, input: undefined, bindingCount: this.bindingCount, program };
|
|
145
|
+
return { mode, root, input: undefined, bindingCount: this.bindingCount, program, base };
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
// ===========================================================================
|
package/src/program.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { pathToKey, type Capability, type Effects, type ProcedureManifest, type
|
|
|
10
10
|
import type { ProgramIR } from "./ir.js";
|
|
11
11
|
import { manifestFacts, registryFacts, withDefinitions, type ProcedureFacts, type RegistryLike } from "./facts.js";
|
|
12
12
|
import { findDefinitions, normalize } from "./normalize.js";
|
|
13
|
-
import { resolveScopes } from "./scopes.js";
|
|
13
|
+
import { bindScopes, resolveScopes } from "./scopes.js";
|
|
14
14
|
import { inferEffects, toEffects, type EffectSummary } from "./effects.js";
|
|
15
15
|
import { checkCalls } from "./checks.js";
|
|
16
16
|
import { hasErrors, sortDiagnostics, type Diagnostic } from "./diagnostics.js";
|
|
@@ -112,7 +112,7 @@ function analyze(program: unknown, options: CheckOptions): Analysis {
|
|
|
112
112
|
if (body === undefined) return undefined;
|
|
113
113
|
bodies.set(key, "pending");
|
|
114
114
|
const { ir } = normalize(body, facts, { mode: "body" });
|
|
115
|
-
|
|
115
|
+
bindScopes(ir);
|
|
116
116
|
const summary = inferEffects(ir.root, facts, bodyEffects).program;
|
|
117
117
|
bodies.set(key, summary);
|
|
118
118
|
return summary;
|
|
@@ -121,12 +121,12 @@ function analyze(program: unknown, options: CheckOptions): Analysis {
|
|
|
121
121
|
// The bodies that the program defines are a part of the program: their problems have pointers in it
|
|
122
122
|
for (const definition of definitions.values()) {
|
|
123
123
|
const body = normalize(definition.body, facts, { mode: "body", base: definition.pointer });
|
|
124
|
-
diagnostics.push(...body.diagnostics, ...resolveScopes(body.ir));
|
|
124
|
+
diagnostics.push(...body.diagnostics, ...resolveScopes(body.ir, facts));
|
|
125
125
|
diagnostics.push(...checkCalls(body.ir.root, facts, inferEffects(body.ir.root, facts, bodyEffects)));
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
const result = normalize(program, facts, { mode: options.mode ?? "exec" });
|
|
129
|
-
diagnostics.push(...result.diagnostics, ...resolveScopes(result.ir));
|
|
129
|
+
diagnostics.push(...result.diagnostics, ...resolveScopes(result.ir, facts));
|
|
130
130
|
const effects = inferEffects(result.ir.root, facts, bodyEffects);
|
|
131
131
|
diagnostics.push(...checkCalls(result.ir.root, facts, effects, { capability: options.capability }));
|
|
132
132
|
return { facts, ir: result.ir, diagnostics, program: effects.program };
|
package/src/scopes.ts
CHANGED
|
@@ -1,37 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The scopes pass: each `$ref` gets the binding that the interpreter reads.
|
|
2
|
+
* The scopes pass: each `$ref` gets the binding that the interpreter reads, for the generated code.
|
|
3
3
|
*
|
|
4
|
-
* The pass follows
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* name
|
|
4
|
+
* The pass follows the names of the kernel. The innermost frame with the name gives the value.
|
|
5
|
+
* `$last` is the last result of the innermost frame that has one. A chain frame gets a name after
|
|
6
|
+
* its step: a step reads only the names of the steps before it.
|
|
7
|
+
*
|
|
8
|
+
* The rules of the names (an unknown name, a use before the definition) run through `check()` of
|
|
9
|
+
* coeval on the run document of the program (`names.ts`). This pass gives their diagnostics too.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import { isPlainObject } from "@procedures/client/browser";
|
|
12
13
|
import { isOutputRef } from "./refs.js";
|
|
13
14
|
import { childPointer, type Binding, type IRNode, type ItemsSpec, type Pointer, type ProgramIR, type ReferenceNode, type ScopeFrame, type ScopeSnapshot } from "./ir.js";
|
|
14
15
|
import type { Diagnostic } from "./diagnostics.js";
|
|
16
|
+
import type { ProcedureFacts } from "./facts.js";
|
|
17
|
+
import { nameDiagnostics } from "./names.js";
|
|
15
18
|
|
|
16
19
|
/** A frame while the pass walks: the names grow as the steps of a chain pass. */
|
|
17
20
|
interface Frame {
|
|
18
21
|
readonly names: Map<string, Binding>;
|
|
19
22
|
last: Binding | undefined;
|
|
20
|
-
/** The names of this step and the later steps of a chain, to find a use before the definition. */
|
|
21
|
-
later: ReadonlySet<string>;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
type Env = readonly Frame[] | undefined;
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
|
-
* This function resolves the references of the IR and gives the diagnostics of the names.
|
|
28
|
+
* This function resolves the references of the IR, and gives the diagnostics of the names. The
|
|
29
|
+
* static kit of coeval finds the problems of the names (`nameDiagnostics()`). Give the facts of
|
|
30
|
+
* the procedures for their raw-input rule. The default knows the control-flow procedures.
|
|
28
31
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* matters.
|
|
32
|
+
* The order of the diagnostics (D26): first the warnings for the raw input, then the diagnostics
|
|
33
|
+
* of the names. Before 0.26.0, the diagnostics came in the order of the program tree. Sort the
|
|
34
|
+
* diagnostics by `pointer` when the order matters.
|
|
33
35
|
*/
|
|
34
|
-
export function resolveScopes(ir: ProgramIR): Diagnostic[] {
|
|
36
|
+
export function resolveScopes(ir: ProgramIR, facts?: Pick<ProcedureFacts, "rawInput">): Diagnostic[] {
|
|
37
|
+
const diagnostics = bindScopes(ir);
|
|
38
|
+
const rawInput = facts ? (path: readonly string[]) => facts.rawInput(path as string[]) : undefined;
|
|
39
|
+
diagnostics.push(...nameDiagnostics(ir.program, { mode: ir.mode, base: ir.base, rawInput }));
|
|
40
|
+
return diagnostics;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* This function resolves the references of the IR only. It gives the warnings for the raw input
|
|
45
|
+
* of a procedure that runs refs, and no diagnostics of the names.
|
|
46
|
+
*/
|
|
47
|
+
export function bindScopes(ir: ProgramIR): Diagnostic[] {
|
|
35
48
|
const diagnostics: Diagnostic[] = [];
|
|
36
49
|
const root: Env = ir.input ? [frameOf([["input", ir.input]])] : undefined;
|
|
37
50
|
visit(ir.root, root, diagnostics);
|
|
@@ -39,7 +52,7 @@ export function resolveScopes(ir: ProgramIR): Diagnostic[] {
|
|
|
39
52
|
}
|
|
40
53
|
|
|
41
54
|
function frameOf(names: ReadonlyArray<readonly [string, Binding]>): Frame {
|
|
42
|
-
return { names: new Map(names), last: undefined
|
|
55
|
+
return { names: new Map(names), last: undefined };
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
/** The static scope at a point: a copy of the frames, as `withScope()` gives them to a raw input. */
|
|
@@ -63,37 +76,10 @@ function lookup(env: Env, name: string): Binding | undefined {
|
|
|
63
76
|
return undefined;
|
|
64
77
|
}
|
|
65
78
|
|
|
66
|
-
function
|
|
79
|
+
/** This function gives a reference the binding of its name. `names.ts` reports a name that no frame has. */
|
|
80
|
+
function resolve(node: ReferenceNode, env: Env): void {
|
|
67
81
|
if (node.synthetic) return;
|
|
68
82
|
node.binding = lookup(env, node.name);
|
|
69
|
-
if (node.binding) return;
|
|
70
|
-
if (node.deferred) {
|
|
71
|
-
diagnostics.push({
|
|
72
|
-
severity: "info",
|
|
73
|
-
code: "unknown-name",
|
|
74
|
-
message: `No scope has the name "${node.name}" here. In the input of a deferred call, the $ref stays data for the runner.`,
|
|
75
|
-
pointer: node.pointer,
|
|
76
|
-
});
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
const later = env?.some((frame) => frame.later.has(node.name)) ?? false;
|
|
80
|
-
const names = [...new Set((env ?? []).flatMap((frame) => [...frame.names.keys(), ...(frame.last ? ["$last"] : [])]))];
|
|
81
|
-
const known = names.length > 0 ? ` The names here: ${names.join(", ")}.` : " No name is in the scope here.";
|
|
82
|
-
diagnostics.push(
|
|
83
|
-
later
|
|
84
|
-
? {
|
|
85
|
-
severity: "error",
|
|
86
|
-
code: "use-before-definition",
|
|
87
|
-
message: `The $ref "${node.text}" reads "${node.name}" before the step that makes it: a step reads only the names of the steps before it.`,
|
|
88
|
-
pointer: node.pointer,
|
|
89
|
-
}
|
|
90
|
-
: {
|
|
91
|
-
severity: "error",
|
|
92
|
-
code: "unknown-name",
|
|
93
|
-
message: `Unknown $ref name "${node.name}" in "${node.text}".${known}${node.name === "$last" ? " The first step of a chain has no $last." : ""}`,
|
|
94
|
-
pointer: node.pointer,
|
|
95
|
-
},
|
|
96
|
-
);
|
|
97
83
|
}
|
|
98
84
|
|
|
99
85
|
function visitItems(items: ItemsSpec, env: Env, diagnostics: Diagnostic[]): void {
|
|
@@ -107,7 +93,7 @@ function visit(node: IRNode, env: Env, diagnostics: Diagnostic[]): void {
|
|
|
107
93
|
case "fail":
|
|
108
94
|
return;
|
|
109
95
|
case "reference":
|
|
110
|
-
resolve(node, env
|
|
96
|
+
resolve(node, env);
|
|
111
97
|
return;
|
|
112
98
|
case "object":
|
|
113
99
|
for (const entry of node.entries) visit(entry.value, env, diagnostics);
|
|
@@ -127,17 +113,14 @@ function visit(node: IRNode, env: Env, diagnostics: Diagnostic[]): void {
|
|
|
127
113
|
else if (node.reason === "runs-refs" && node.key !== "procedure.define") scanRaw(node.input, env, diagnostics);
|
|
128
114
|
return;
|
|
129
115
|
case "chain": {
|
|
130
|
-
const
|
|
131
|
-
const frame: Frame = { names: new Map(), last: undefined, later: new Set() };
|
|
116
|
+
const frame: Frame = { names: new Map(), last: undefined };
|
|
132
117
|
const inner: Env = [...(env ?? []), frame];
|
|
133
118
|
if (node.initialInput) visit(node.initialInput, inner, diagnostics);
|
|
134
|
-
node.steps
|
|
135
|
-
frame.later = new Set(names.slice(index).filter((name): name is string => name !== undefined));
|
|
119
|
+
for (const step of node.steps) {
|
|
136
120
|
visit(step.node, inner, diagnostics);
|
|
137
121
|
if (step.result.name !== undefined) frame.names.set(step.result.name, step.result);
|
|
138
122
|
frame.last = step.result;
|
|
139
|
-
}
|
|
140
|
-
frame.later = new Set();
|
|
123
|
+
}
|
|
141
124
|
return;
|
|
142
125
|
}
|
|
143
126
|
case "parallel":
|