@invinite-org/chartlang-compiler 1.0.1 → 1.1.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 +101 -0
- package/README.md +3 -0
- package/dist/analysis/extractCapabilities.d.ts +5 -1
- package/dist/analysis/extractCapabilities.d.ts.map +1 -1
- package/dist/analysis/extractCapabilities.js +6 -2
- package/dist/analysis/extractCapabilities.js.map +1 -1
- package/dist/analysis/extractDependencyGraph.d.ts +160 -0
- package/dist/analysis/extractDependencyGraph.d.ts.map +1 -0
- package/dist/analysis/extractDependencyGraph.js +690 -0
- package/dist/analysis/extractDependencyGraph.js.map +1 -0
- package/dist/analysis/extractInputs.d.ts +5 -1
- package/dist/analysis/extractInputs.d.ts.map +1 -1
- package/dist/analysis/extractInputs.js +6 -2
- package/dist/analysis/extractInputs.js.map +1 -1
- package/dist/analysis/extractMaxLookback.d.ts +6 -1
- package/dist/analysis/extractMaxLookback.d.ts.map +1 -1
- package/dist/analysis/extractMaxLookback.js +10 -5
- package/dist/analysis/extractMaxLookback.js.map +1 -1
- package/dist/analysis/forbiddenConstructs.d.ts.map +1 -1
- package/dist/analysis/forbiddenConstructs.js +3 -0
- package/dist/analysis/forbiddenConstructs.js.map +1 -1
- package/dist/analysis/index.d.ts +3 -1
- package/dist/analysis/index.d.ts.map +1 -1
- package/dist/analysis/index.js +1 -0
- package/dist/analysis/index.js.map +1 -1
- package/dist/analysis/structuralChecks.d.ts +64 -8
- package/dist/analysis/structuralChecks.d.ts.map +1 -1
- package/dist/analysis/structuralChecks.js +111 -22
- package/dist/analysis/structuralChecks.js.map +1 -1
- package/dist/api.d.ts +40 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +484 -35
- package/dist/api.js.map +1 -1
- package/dist/bundle.d.ts +90 -3
- package/dist/bundle.d.ts.map +1 -1
- package/dist/bundle.js +59 -5
- package/dist/bundle.js.map +1 -1
- package/dist/dependency/index.d.ts +3 -0
- package/dist/dependency/index.d.ts.map +1 -0
- package/dist/dependency/index.js +4 -0
- package/dist/dependency/index.js.map +1 -0
- package/dist/dependency/resolveProducer.d.ts +183 -0
- package/dist/dependency/resolveProducer.d.ts.map +1 -0
- package/dist/dependency/resolveProducer.js +256 -0
- package/dist/dependency/resolveProducer.js.map +1 -0
- package/dist/diagnostics.d.ts +6 -2
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +8 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +27 -0
- package/dist/manifest.js.map +1 -1
- package/dist/program.d.ts +1 -0
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +80 -4
- package/dist/program.js.map +1 -1
- package/dist/transformers/callsiteIdInjection.d.ts +2 -1
- package/dist/transformers/callsiteIdInjection.d.ts.map +1 -1
- package/dist/transformers/callsiteIdInjection.js +15 -0
- package/dist/transformers/callsiteIdInjection.js.map +1 -1
- package/dist/transformers/index.d.ts +2 -0
- package/dist/transformers/index.d.ts.map +1 -1
- package/dist/transformers/index.js +1 -0
- package/dist/transformers/index.js.map +1 -1
- package/dist/transformers/plotKindFromCallsite.d.ts +43 -0
- package/dist/transformers/plotKindFromCallsite.d.ts.map +1 -0
- package/dist/transformers/plotKindFromCallsite.js +103 -0
- package/dist/transformers/plotKindFromCallsite.js.map +1 -0
- package/dist/transformers/rewriteDependencyAccessors.d.ts +65 -0
- package/dist/transformers/rewriteDependencyAccessors.d.ts.map +1 -0
- package/dist/transformers/rewriteDependencyAccessors.js +204 -0
- package/dist/transformers/rewriteDependencyAccessors.js.map +1 -0
- package/dist/typesEmit.d.ts +14 -11
- package/dist/typesEmit.d.ts.map +1 -1
- package/dist/typesEmit.js +91 -7
- package/dist/typesEmit.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
// Copyright (c) 2026 Invinite. Licensed under the MIT License.
|
|
2
|
+
// See the LICENSE file in the repo root for full license text.
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { createDiagnostic } from "../diagnostics.js";
|
|
5
|
+
import { resolveCalleeName } from "../transformers/resolveCallee.js";
|
|
6
|
+
/**
|
|
7
|
+
* Read a JSON-clean literal from a TS expression — number, string,
|
|
8
|
+
* boolean, or null. Returns `undefined` for non-literal expressions
|
|
9
|
+
* (identifiers, computed access, calls, …).
|
|
10
|
+
*/
|
|
11
|
+
function readJsonLiteral(node) {
|
|
12
|
+
if (ts.isNumericLiteral(node))
|
|
13
|
+
return Number(node.text);
|
|
14
|
+
if (ts.isStringLiteral(node))
|
|
15
|
+
return node.text;
|
|
16
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword)
|
|
17
|
+
return true;
|
|
18
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword)
|
|
19
|
+
return false;
|
|
20
|
+
if (node.kind === ts.SyntaxKind.NullKeyword)
|
|
21
|
+
return null;
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Read the function body the `compute` property assignment of a
|
|
26
|
+
* `define*({ compute })` call literal points at. Returns `null` when
|
|
27
|
+
* the property is missing or non-function (treat as zero plot calls).
|
|
28
|
+
*
|
|
29
|
+
* Handles both arrow functions (`compute: () => {}`) and method
|
|
30
|
+
* shorthand (`compute() {}`).
|
|
31
|
+
*/
|
|
32
|
+
function readComputeBody(defineCall) {
|
|
33
|
+
const arg = defineCall.arguments[0];
|
|
34
|
+
/* v8 ignore next */
|
|
35
|
+
if (arg === undefined || !ts.isObjectLiteralExpression(arg))
|
|
36
|
+
return null;
|
|
37
|
+
const computeProperty = arg.properties.find((property) => {
|
|
38
|
+
const name = property.name;
|
|
39
|
+
return name !== undefined && ts.isIdentifier(name) && name.text === "compute";
|
|
40
|
+
});
|
|
41
|
+
if (computeProperty === undefined)
|
|
42
|
+
return null;
|
|
43
|
+
if (ts.isPropertyAssignment(computeProperty)) {
|
|
44
|
+
const initializer = computeProperty.initializer;
|
|
45
|
+
return ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)
|
|
46
|
+
? initializer.body
|
|
47
|
+
: null;
|
|
48
|
+
}
|
|
49
|
+
/* v8 ignore start */
|
|
50
|
+
if (ts.isMethodDeclaration(computeProperty)) {
|
|
51
|
+
return computeProperty.body ?? null;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/* v8 ignore stop */
|
|
56
|
+
/**
|
|
57
|
+
* Sweep D — extract producer outputs from `plot(value, { title })`
|
|
58
|
+
* calls inside the binding's compute body. Emits
|
|
59
|
+
* `duplicate-output-title` for clashes.
|
|
60
|
+
*/
|
|
61
|
+
function extractBindingOutputs(binding, sourceFile, checker, sourcePath, diagnostics) {
|
|
62
|
+
const body = readComputeBody(binding.defineCall);
|
|
63
|
+
const outputs = [];
|
|
64
|
+
const seenTitles = new Set();
|
|
65
|
+
let hasUntitledPlot = false;
|
|
66
|
+
/* v8 ignore next 3 */
|
|
67
|
+
if (body === null) {
|
|
68
|
+
return Object.freeze({ outputs: Object.freeze(outputs.slice()), hasUntitledPlot });
|
|
69
|
+
}
|
|
70
|
+
const visit = (node) => {
|
|
71
|
+
if (ts.isCallExpression(node)) {
|
|
72
|
+
const callee = resolveCalleeName(node, checker);
|
|
73
|
+
if (callee === "plot") {
|
|
74
|
+
const optsArg = node.arguments[1];
|
|
75
|
+
let title;
|
|
76
|
+
if (optsArg !== undefined && ts.isObjectLiteralExpression(optsArg)) {
|
|
77
|
+
for (const property of optsArg.properties) {
|
|
78
|
+
/* v8 ignore next */
|
|
79
|
+
if (!ts.isPropertyAssignment(property))
|
|
80
|
+
continue;
|
|
81
|
+
const propertyName = property.name;
|
|
82
|
+
if (!ts.isIdentifier(propertyName) || propertyName.text !== "title") {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const initializer = property.initializer;
|
|
86
|
+
/* v8 ignore next */
|
|
87
|
+
if (!ts.isStringLiteral(initializer))
|
|
88
|
+
continue;
|
|
89
|
+
title = initializer.text;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (title === undefined) {
|
|
93
|
+
hasUntitledPlot = true;
|
|
94
|
+
}
|
|
95
|
+
else if (seenTitles.has(title)) {
|
|
96
|
+
diagnostics.push(createDiagnostic({
|
|
97
|
+
severity: "error",
|
|
98
|
+
code: "duplicate-output-title",
|
|
99
|
+
message: `\`${binding.bindingName}\` declares two outputs titled "${title}". Titles must be unique within a script.`,
|
|
100
|
+
file: sourcePath,
|
|
101
|
+
node,
|
|
102
|
+
sourceFile,
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
seenTitles.add(title);
|
|
107
|
+
outputs.push(Object.freeze({ title, kind: "series-number" }));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
ts.forEachChild(node, visit);
|
|
112
|
+
};
|
|
113
|
+
ts.forEachChild(body, visit);
|
|
114
|
+
return Object.freeze({ outputs: Object.freeze(outputs.slice()), hasUntitledPlot });
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Sweep B — resolve a `<binding>.withInputs({...}).withInputs({...})`
|
|
118
|
+
* chain back to its rooted identifier and the merged effective-inputs
|
|
119
|
+
* map. Emits `dep-dynamic` / `dep-invalid-input-override` for invalid
|
|
120
|
+
* argument shapes.
|
|
121
|
+
*/
|
|
122
|
+
function resolveWithInputsChain(expression, sourceFile, sourcePath, diagnostics) {
|
|
123
|
+
const layers = [];
|
|
124
|
+
let current = expression;
|
|
125
|
+
while (ts.isCallExpression(current)) {
|
|
126
|
+
const callee = current.expression;
|
|
127
|
+
/* v8 ignore next 2 */
|
|
128
|
+
if (!ts.isPropertyAccessExpression(callee))
|
|
129
|
+
return null;
|
|
130
|
+
if (callee.name.text !== "withInputs")
|
|
131
|
+
return null;
|
|
132
|
+
const arg = current.arguments[0];
|
|
133
|
+
if (arg === undefined || !ts.isObjectLiteralExpression(arg)) {
|
|
134
|
+
diagnostics.push(createDiagnostic({
|
|
135
|
+
severity: "error",
|
|
136
|
+
code: "dep-dynamic",
|
|
137
|
+
message: "`.withInputs(...)` requires an object literal with literal-only property values.",
|
|
138
|
+
file: sourcePath,
|
|
139
|
+
node: arg ?? /* v8 ignore next */ current,
|
|
140
|
+
sourceFile,
|
|
141
|
+
}));
|
|
142
|
+
layers.unshift({ args: ts.factory.createObjectLiteralExpression([]), ok: false });
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
layers.unshift({ args: arg, ok: true });
|
|
146
|
+
}
|
|
147
|
+
current = callee.expression;
|
|
148
|
+
}
|
|
149
|
+
/* v8 ignore next */
|
|
150
|
+
if (!ts.isIdentifier(current))
|
|
151
|
+
return null;
|
|
152
|
+
const merged = {};
|
|
153
|
+
let ok = true;
|
|
154
|
+
for (const layer of layers) {
|
|
155
|
+
if (!layer.ok) {
|
|
156
|
+
ok = false;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
for (const property of layer.args.properties) {
|
|
160
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
161
|
+
diagnostics.push(createDiagnostic({
|
|
162
|
+
severity: "error",
|
|
163
|
+
code: "dep-dynamic",
|
|
164
|
+
message: "`.withInputs(...)` requires a plain object literal — no spread, shorthand, or computed keys.",
|
|
165
|
+
file: sourcePath,
|
|
166
|
+
node: property,
|
|
167
|
+
sourceFile,
|
|
168
|
+
}));
|
|
169
|
+
ok = false;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const key = property.name;
|
|
173
|
+
if (!ts.isIdentifier(key) && !ts.isStringLiteral(key)) {
|
|
174
|
+
diagnostics.push(createDiagnostic({
|
|
175
|
+
severity: "error",
|
|
176
|
+
code: "dep-dynamic",
|
|
177
|
+
message: "`.withInputs(...)` keys must be identifiers or string literals.",
|
|
178
|
+
file: sourcePath,
|
|
179
|
+
node: key,
|
|
180
|
+
sourceFile,
|
|
181
|
+
}));
|
|
182
|
+
ok = false;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const literal = readJsonLiteral(property.initializer);
|
|
186
|
+
if (literal === undefined) {
|
|
187
|
+
diagnostics.push(createDiagnostic({
|
|
188
|
+
severity: "error",
|
|
189
|
+
code: "dep-dynamic",
|
|
190
|
+
message: `\`.withInputs({ ${key.text}: ... })\` value must be a JSON literal (number, string, boolean, or null).`,
|
|
191
|
+
file: sourcePath,
|
|
192
|
+
node: property.initializer,
|
|
193
|
+
sourceFile,
|
|
194
|
+
}));
|
|
195
|
+
ok = false;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
merged[key.text] = literal;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return Object.freeze({
|
|
202
|
+
root: current,
|
|
203
|
+
inputs: Object.freeze({ ...merged }),
|
|
204
|
+
ok,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Validate an `effectiveInputs` map against the producer's `inputs`
|
|
209
|
+
* schema. Each producer-input descriptor is shaped like
|
|
210
|
+
* `{ kind: "int", defaultValue: 14, ... }`. Type-mismatch and unknown
|
|
211
|
+
* keys both emit `dep-invalid-input-override`.
|
|
212
|
+
*/
|
|
213
|
+
function validateInputOverrides(bindingName, inputs, producerInputs, node, sourceFile, sourcePath, diagnostics) {
|
|
214
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
215
|
+
const descriptor = producerInputs[key];
|
|
216
|
+
if (descriptor === undefined) {
|
|
217
|
+
diagnostics.push(createDiagnostic({
|
|
218
|
+
severity: "error",
|
|
219
|
+
code: "dep-invalid-input-override",
|
|
220
|
+
message: `\`${bindingName}\` overrides input "${key}" which the producer does not declare.`,
|
|
221
|
+
file: sourcePath,
|
|
222
|
+
node,
|
|
223
|
+
sourceFile,
|
|
224
|
+
}));
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const expectedKind = typeof descriptor === "object" && descriptor !== null && "kind" in descriptor
|
|
228
|
+
? descriptor.kind
|
|
229
|
+
: undefined;
|
|
230
|
+
if (typeof expectedKind === "string") {
|
|
231
|
+
const valueType = describeJsonValueKind(value);
|
|
232
|
+
const expectedType = expectedTypeForKind(expectedKind);
|
|
233
|
+
if (expectedType !== null && expectedType !== valueType) {
|
|
234
|
+
diagnostics.push(createDiagnostic({
|
|
235
|
+
severity: "error",
|
|
236
|
+
code: "dep-invalid-input-override",
|
|
237
|
+
message: `\`${bindingName}\` override for "${key}" expects ${expectedType} (producer declared \`${expectedKind}\`), received ${valueType}.`,
|
|
238
|
+
file: sourcePath,
|
|
239
|
+
node,
|
|
240
|
+
sourceFile,
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function describeJsonValueKind(value) {
|
|
247
|
+
if (value === null)
|
|
248
|
+
return "null";
|
|
249
|
+
/* v8 ignore next */
|
|
250
|
+
if (Array.isArray(value))
|
|
251
|
+
return "array";
|
|
252
|
+
return typeof value;
|
|
253
|
+
}
|
|
254
|
+
const KIND_TO_VALUE_TYPE = new Map([
|
|
255
|
+
["int", "number"],
|
|
256
|
+
["float", "number"],
|
|
257
|
+
["price", "number"],
|
|
258
|
+
["time", "number"],
|
|
259
|
+
["string", "string"],
|
|
260
|
+
["enum", "string"],
|
|
261
|
+
["color", "string"],
|
|
262
|
+
["symbol", "string"],
|
|
263
|
+
["interval", "string"],
|
|
264
|
+
["bool", "boolean"],
|
|
265
|
+
]);
|
|
266
|
+
function expectedTypeForKind(kind) {
|
|
267
|
+
return KIND_TO_VALUE_TYPE.get(kind) ?? null;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Walk back from a `<receiver>.output("title")` callee to a same-file
|
|
271
|
+
* binding identifier OR an imported identifier — whichever applies.
|
|
272
|
+
* Returns the underlying identifier or `null` when the receiver is a
|
|
273
|
+
* non-resolvable shape (computed access, call result, etc.).
|
|
274
|
+
*/
|
|
275
|
+
function resolveOutputReceiverRoot(receiver) {
|
|
276
|
+
if (ts.isIdentifier(receiver))
|
|
277
|
+
return receiver;
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
function originIsImport(symbol) {
|
|
281
|
+
const declarations = symbol.declarations ?? /* v8 ignore next */ [];
|
|
282
|
+
for (const declaration of declarations) {
|
|
283
|
+
if (ts.isImportClause(declaration)) {
|
|
284
|
+
const importDecl = declaration.parent;
|
|
285
|
+
const specifier = importDecl.moduleSpecifier;
|
|
286
|
+
/* v8 ignore next */
|
|
287
|
+
if (!ts.isStringLiteral(specifier))
|
|
288
|
+
continue;
|
|
289
|
+
return { moduleSpecifier: specifier.text, exportName: "default" };
|
|
290
|
+
}
|
|
291
|
+
if (ts.isImportSpecifier(declaration)) {
|
|
292
|
+
const importDecl = declaration.parent.parent.parent;
|
|
293
|
+
const specifier = importDecl.moduleSpecifier;
|
|
294
|
+
/* v8 ignore next */
|
|
295
|
+
if (!ts.isStringLiteral(specifier))
|
|
296
|
+
continue;
|
|
297
|
+
const exportName = declaration.propertyName?.text ?? declaration.name.text;
|
|
298
|
+
return { moduleSpecifier: specifier.text, exportName };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
function resolveBinding(identifier, checker, bindingsByName, resolveProducer) {
|
|
304
|
+
const sameFile = bindingsByName.get(identifier.text);
|
|
305
|
+
if (sameFile !== undefined) {
|
|
306
|
+
return Object.freeze({ kind: "same-file", binding: sameFile });
|
|
307
|
+
}
|
|
308
|
+
const symbol = checker.getSymbolAtLocation(identifier);
|
|
309
|
+
/* v8 ignore next */
|
|
310
|
+
if (symbol === undefined)
|
|
311
|
+
return null;
|
|
312
|
+
const importOrigin = originIsImport(symbol);
|
|
313
|
+
if (importOrigin === null)
|
|
314
|
+
return null;
|
|
315
|
+
const snapshot = resolveProducer(importOrigin.moduleSpecifier, importOrigin.exportName);
|
|
316
|
+
if (snapshot === null)
|
|
317
|
+
return null;
|
|
318
|
+
return Object.freeze({
|
|
319
|
+
kind: "cross-file",
|
|
320
|
+
sourcePath: importOrigin.moduleSpecifier,
|
|
321
|
+
exportName: importOrigin.exportName,
|
|
322
|
+
snapshot,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Run the dependency-graph analysis pass over a `.chart.ts` source
|
|
327
|
+
* file. Sweeps the AST four times — outputs, withInputs chains,
|
|
328
|
+
* consumer `.output(...)` calls, cycle detection — and returns a
|
|
329
|
+
* frozen `DepGraph` plus the diagnostics surfaced along the way.
|
|
330
|
+
*
|
|
331
|
+
* `resolveProducer` is a caller-supplied callback that the bundler
|
|
332
|
+
* (Task 3) wires to a recursive compile entry point. In Task 2 the
|
|
333
|
+
* pass works against `() => null` for cross-file edges; tests pass
|
|
334
|
+
* mocked snapshots instead.
|
|
335
|
+
*
|
|
336
|
+
* @since 0.7
|
|
337
|
+
* @stable
|
|
338
|
+
* @example
|
|
339
|
+
* // const graph = extractDependencyGraph(sf, checker, "demo.chart.ts", bindings, () => null);
|
|
340
|
+
* const fn: typeof extractDependencyGraph = extractDependencyGraph;
|
|
341
|
+
* void fn;
|
|
342
|
+
*/
|
|
343
|
+
export function extractDependencyGraph(sourceFile, checker, sourcePath, structuralBindings, resolveProducer) {
|
|
344
|
+
const diagnostics = [];
|
|
345
|
+
const bindingsByName = new Map();
|
|
346
|
+
for (const binding of structuralBindings) {
|
|
347
|
+
if (binding.exportKind === "default")
|
|
348
|
+
continue;
|
|
349
|
+
bindingsByName.set(binding.bindingName, binding);
|
|
350
|
+
}
|
|
351
|
+
const outputsByBinding = new Map();
|
|
352
|
+
for (const binding of structuralBindings) {
|
|
353
|
+
outputsByBinding.set(binding.bindingName, extractBindingOutputs(binding, sourceFile, checker, sourcePath, diagnostics));
|
|
354
|
+
}
|
|
355
|
+
const effectiveInputsByBinding = new Map();
|
|
356
|
+
for (const statement of sourceFile.statements) {
|
|
357
|
+
if (!ts.isVariableStatement(statement))
|
|
358
|
+
continue;
|
|
359
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
360
|
+
/* v8 ignore next */
|
|
361
|
+
if (!ts.isIdentifier(declaration.name))
|
|
362
|
+
continue;
|
|
363
|
+
const initializer = declaration.initializer;
|
|
364
|
+
if (initializer === undefined)
|
|
365
|
+
continue;
|
|
366
|
+
if (!ts.isCallExpression(initializer))
|
|
367
|
+
continue;
|
|
368
|
+
const callee = initializer.expression;
|
|
369
|
+
if (ts.isPropertyAccessExpression(callee) && callee.name.text === "withInputs") {
|
|
370
|
+
const chain = resolveWithInputsChain(initializer, sourceFile, sourcePath, diagnostics);
|
|
371
|
+
/* v8 ignore next */
|
|
372
|
+
if (chain === null)
|
|
373
|
+
continue;
|
|
374
|
+
const root = resolveBinding(chain.root, checker, bindingsByName, resolveProducer);
|
|
375
|
+
effectiveInputsByBinding.set(declaration.name.text, {
|
|
376
|
+
rootName: chain.root.text,
|
|
377
|
+
rootBinding: root,
|
|
378
|
+
inputs: chain.inputs,
|
|
379
|
+
});
|
|
380
|
+
if (root !== null && chain.ok) {
|
|
381
|
+
const producerInputs = root.kind === "cross-file" ? root.snapshot.inputs : {};
|
|
382
|
+
validateInputOverrides(declaration.name.text, chain.inputs, producerInputs, initializer, sourceFile, sourcePath, diagnostics);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const consumesByBinding = new Map();
|
|
388
|
+
for (const binding of structuralBindings) {
|
|
389
|
+
consumesByBinding.set(binding.bindingName, []);
|
|
390
|
+
}
|
|
391
|
+
for (const binding of structuralBindings) {
|
|
392
|
+
const body = readComputeBody(binding.defineCall);
|
|
393
|
+
if (body === null)
|
|
394
|
+
continue;
|
|
395
|
+
const consumerConsumes = consumesByBinding.get(binding.bindingName);
|
|
396
|
+
/* v8 ignore next */
|
|
397
|
+
if (consumerConsumes === undefined)
|
|
398
|
+
continue;
|
|
399
|
+
const seenLocalIds = new Set();
|
|
400
|
+
const visit = (node) => {
|
|
401
|
+
if (ts.isCallExpression(node) &&
|
|
402
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
403
|
+
node.expression.name.text === "output") {
|
|
404
|
+
const arg = node.arguments[0];
|
|
405
|
+
if (arg === undefined || !ts.isStringLiteral(arg)) {
|
|
406
|
+
diagnostics.push(createDiagnostic({
|
|
407
|
+
severity: "error",
|
|
408
|
+
code: "dep-dynamic",
|
|
409
|
+
message: "`<binding>.output(...)` requires a single string-literal argument.",
|
|
410
|
+
file: sourcePath,
|
|
411
|
+
node,
|
|
412
|
+
sourceFile,
|
|
413
|
+
}));
|
|
414
|
+
ts.forEachChild(node, visit);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const title = arg.text;
|
|
418
|
+
const root = resolveOutputReceiverRoot(node.expression.expression);
|
|
419
|
+
if (root === null) {
|
|
420
|
+
diagnostics.push(createDiagnostic({
|
|
421
|
+
severity: "error",
|
|
422
|
+
code: "dep-dynamic",
|
|
423
|
+
message: "`.output(...)` receiver must trace back to a `const`-bound `defineIndicator(...)` result.",
|
|
424
|
+
file: sourcePath,
|
|
425
|
+
node,
|
|
426
|
+
sourceFile,
|
|
427
|
+
}));
|
|
428
|
+
ts.forEachChild(node, visit);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const effective = effectiveInputsByBinding.get(root.text);
|
|
432
|
+
let resolvedRootName;
|
|
433
|
+
let resolvedRoot;
|
|
434
|
+
let effectiveInputs;
|
|
435
|
+
if (effective !== undefined) {
|
|
436
|
+
resolvedRootName = effective.rootName;
|
|
437
|
+
resolvedRoot = effective.rootBinding;
|
|
438
|
+
effectiveInputs = effective.inputs;
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
resolvedRootName = root.text;
|
|
442
|
+
resolvedRoot = resolveBinding(root, checker, bindingsByName, resolveProducer);
|
|
443
|
+
effectiveInputs = Object.freeze({});
|
|
444
|
+
}
|
|
445
|
+
if (resolvedRoot === null) {
|
|
446
|
+
diagnostics.push(createDiagnostic({
|
|
447
|
+
severity: "error",
|
|
448
|
+
code: "dep-dynamic",
|
|
449
|
+
message: `\`.output("${title}")\` receiver \`${root.text}\` does not resolve to a known same-file or imported indicator binding.`,
|
|
450
|
+
file: sourcePath,
|
|
451
|
+
node,
|
|
452
|
+
sourceFile,
|
|
453
|
+
}));
|
|
454
|
+
ts.forEachChild(node, visit);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const producerOutputs = resolvedRoot.kind === "same-file"
|
|
458
|
+
? outputsByBinding.get(resolvedRoot.binding.bindingName)
|
|
459
|
+
.outputs
|
|
460
|
+
: resolvedRoot.snapshot.outputs;
|
|
461
|
+
const producerHasUntitled = resolvedRoot.kind === "same-file" &&
|
|
462
|
+
outputsByBinding.get(resolvedRoot.binding.bindingName)
|
|
463
|
+
.hasUntitledPlot;
|
|
464
|
+
const titles = new Set(producerOutputs.map((o) => o.title));
|
|
465
|
+
if (!titles.has(title)) {
|
|
466
|
+
if (producerHasUntitled) {
|
|
467
|
+
diagnostics.push(createDiagnostic({
|
|
468
|
+
severity: "error",
|
|
469
|
+
code: "dep-output-not-titled",
|
|
470
|
+
message: `Producer \`${resolvedRootName}\` declares an untitled \`plot(...)\` but consumer references \`.output("${title}")\`. Title every \`plot\` the producer emits or remove the consumer reference.`,
|
|
471
|
+
file: sourcePath,
|
|
472
|
+
node,
|
|
473
|
+
sourceFile,
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
diagnostics.push(createDiagnostic({
|
|
478
|
+
severity: "error",
|
|
479
|
+
code: "dep-unknown-output",
|
|
480
|
+
message: `Producer \`${resolvedRootName}\` does not declare an output titled "${title}".`,
|
|
481
|
+
file: sourcePath,
|
|
482
|
+
node,
|
|
483
|
+
sourceFile,
|
|
484
|
+
}));
|
|
485
|
+
}
|
|
486
|
+
ts.forEachChild(node, visit);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const localId = effective !== undefined ? root.text : root.text;
|
|
490
|
+
if (!seenLocalIds.has(localId)) {
|
|
491
|
+
seenLocalIds.add(localId);
|
|
492
|
+
const producerRef = resolvedRoot.kind === "same-file"
|
|
493
|
+
? Object.freeze({
|
|
494
|
+
kind: "same-file",
|
|
495
|
+
bindingName: resolvedRoot.binding.bindingName,
|
|
496
|
+
})
|
|
497
|
+
: Object.freeze({
|
|
498
|
+
kind: "cross-file",
|
|
499
|
+
sourcePath: resolvedRoot.sourcePath,
|
|
500
|
+
exportName: resolvedRoot.exportName,
|
|
501
|
+
});
|
|
502
|
+
consumerConsumes.push({
|
|
503
|
+
localId,
|
|
504
|
+
producerRef,
|
|
505
|
+
outputs: producerOutputs,
|
|
506
|
+
effectiveInputs,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
ts.forEachChild(node, visit);
|
|
511
|
+
};
|
|
512
|
+
ts.forEachChild(body, visit);
|
|
513
|
+
}
|
|
514
|
+
const sameFileEdges = new Map();
|
|
515
|
+
for (const [bindingName, consumes] of consumesByBinding.entries()) {
|
|
516
|
+
const targets = new Set();
|
|
517
|
+
for (const entry of consumes) {
|
|
518
|
+
if (entry.producerRef.kind === "same-file") {
|
|
519
|
+
targets.add(entry.producerRef.bindingName);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
sameFileEdges.set(bindingName, targets);
|
|
523
|
+
}
|
|
524
|
+
const cycles = detectCycles(sameFileEdges);
|
|
525
|
+
for (const cycle of cycles) {
|
|
526
|
+
const path = `${cycle.join(" -> ")} -> ${cycle[0] ?? /* v8 ignore next */ ""}`;
|
|
527
|
+
for (const member of cycle) {
|
|
528
|
+
const binding = structuralBindings.find((b) => b.bindingName === member);
|
|
529
|
+
/* v8 ignore next */
|
|
530
|
+
if (binding === undefined)
|
|
531
|
+
continue;
|
|
532
|
+
diagnostics.push(createDiagnostic({
|
|
533
|
+
severity: "error",
|
|
534
|
+
code: "dep-cycle",
|
|
535
|
+
message: `Dependency cycle detected: ${path}`,
|
|
536
|
+
file: sourcePath,
|
|
537
|
+
node: binding.defineCall,
|
|
538
|
+
sourceFile,
|
|
539
|
+
}));
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const drawn = [];
|
|
543
|
+
const privateDeps = [];
|
|
544
|
+
// First emit any withInputs-derived alias bindings as synthetic
|
|
545
|
+
// private deps so the bundler + runtime can mount one DepRunner
|
|
546
|
+
// per unique alias. The alias has no `defineCall` of its own —
|
|
547
|
+
// its compute body lives on the producer the alias chains off.
|
|
548
|
+
for (const [aliasName, info] of effectiveInputsByBinding.entries()) {
|
|
549
|
+
/* v8 ignore start */
|
|
550
|
+
if (bindingsByName.has(aliasName))
|
|
551
|
+
continue;
|
|
552
|
+
if (info.rootBinding === null)
|
|
553
|
+
continue;
|
|
554
|
+
/* v8 ignore stop */
|
|
555
|
+
const producerRef = info.rootBinding.kind === "same-file"
|
|
556
|
+
? Object.freeze({
|
|
557
|
+
kind: "same-file",
|
|
558
|
+
bindingName: info.rootBinding.binding.bindingName,
|
|
559
|
+
})
|
|
560
|
+
: Object.freeze({
|
|
561
|
+
kind: "cross-file",
|
|
562
|
+
sourcePath: info.rootBinding.sourcePath,
|
|
563
|
+
exportName: info.rootBinding.exportName,
|
|
564
|
+
});
|
|
565
|
+
const aliasOutputs = info.rootBinding.kind === "same-file"
|
|
566
|
+
? outputsByBinding.get(info.rootBinding.binding.bindingName)
|
|
567
|
+
.outputs
|
|
568
|
+
: info.rootBinding.snapshot.outputs;
|
|
569
|
+
privateDeps.push(Object.freeze({
|
|
570
|
+
localId: aliasName,
|
|
571
|
+
producerRef,
|
|
572
|
+
effectiveInputs: info.inputs,
|
|
573
|
+
defineCall: null,
|
|
574
|
+
outputs: aliasOutputs,
|
|
575
|
+
consumes: Object.freeze([]),
|
|
576
|
+
}));
|
|
577
|
+
}
|
|
578
|
+
for (const binding of structuralBindings) {
|
|
579
|
+
const outputs = outputsByBinding.get(binding.bindingName).outputs;
|
|
580
|
+
const consumes = consumesByBinding.get(binding.bindingName).map((entry) => Object.freeze({
|
|
581
|
+
localId: entry.localId,
|
|
582
|
+
producerRef: entry.producerRef,
|
|
583
|
+
outputs: entry.outputs,
|
|
584
|
+
effectiveInputs: entry.effectiveInputs,
|
|
585
|
+
}));
|
|
586
|
+
const frozenConsumes = Object.freeze(consumes);
|
|
587
|
+
if (binding.exportKind === "private") {
|
|
588
|
+
// Private structural bindings are always `const X =
|
|
589
|
+
// defineIndicator(...)` direct calls — withInputs-derived
|
|
590
|
+
// aliases live in `effectiveInputsByBinding` and were
|
|
591
|
+
// already emitted as synthetic private deps above. Hence
|
|
592
|
+
// the alias-via-private-binding branch is structurally
|
|
593
|
+
// unreachable here.
|
|
594
|
+
const producerRef = Object.freeze({
|
|
595
|
+
kind: "same-file",
|
|
596
|
+
bindingName: binding.bindingName,
|
|
597
|
+
});
|
|
598
|
+
privateDeps.push(Object.freeze({
|
|
599
|
+
localId: binding.bindingName,
|
|
600
|
+
producerRef,
|
|
601
|
+
effectiveInputs: Object.freeze({}),
|
|
602
|
+
defineCall: binding.defineCall,
|
|
603
|
+
outputs,
|
|
604
|
+
consumes: frozenConsumes,
|
|
605
|
+
}));
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
drawn.push(Object.freeze({
|
|
609
|
+
exportName: binding.exportKind === "default" ? "default" : binding.bindingName,
|
|
610
|
+
bindingName: binding.bindingName,
|
|
611
|
+
defineCall: binding.defineCall,
|
|
612
|
+
outputs,
|
|
613
|
+
consumes: frozenConsumes,
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return Object.freeze({
|
|
618
|
+
drawn: Object.freeze(drawn.slice()),
|
|
619
|
+
privateDeps: Object.freeze(privateDeps.slice()),
|
|
620
|
+
diagnostics: Object.freeze(diagnostics.slice()),
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Colour-marking iterative DFS — returns every cycle found in the
|
|
625
|
+
* directed adjacency map. Each cycle is the ordered list of node ids
|
|
626
|
+
* along the back-edge. Used by `extractDependencyGraph` to surface
|
|
627
|
+
* `dep-cycle` diagnostics at every binding in the cycle.
|
|
628
|
+
*/
|
|
629
|
+
function detectCycles(edges) {
|
|
630
|
+
const WHITE = 0;
|
|
631
|
+
const GREY = 1;
|
|
632
|
+
const BLACK = 2;
|
|
633
|
+
const colour = new Map();
|
|
634
|
+
for (const key of edges.keys())
|
|
635
|
+
colour.set(key, WHITE);
|
|
636
|
+
const cycles = [];
|
|
637
|
+
const seenCycles = new Set();
|
|
638
|
+
const dfs = (start) => {
|
|
639
|
+
const stack = [];
|
|
640
|
+
const pathIndex = new Map();
|
|
641
|
+
const pathOrder = [];
|
|
642
|
+
const targets = edges.get(start) ?? /* v8 ignore next */ new Set();
|
|
643
|
+
colour.set(start, GREY);
|
|
644
|
+
pathIndex.set(start, 0);
|
|
645
|
+
pathOrder.push(start);
|
|
646
|
+
stack.push({ node: start, iter: targets[Symbol.iterator]() });
|
|
647
|
+
while (stack.length > 0) {
|
|
648
|
+
const top = stack[stack.length - 1];
|
|
649
|
+
/* v8 ignore next */
|
|
650
|
+
if (top === undefined)
|
|
651
|
+
break;
|
|
652
|
+
const next = top.iter.next();
|
|
653
|
+
if (next.done) {
|
|
654
|
+
colour.set(top.node, BLACK);
|
|
655
|
+
pathIndex.delete(top.node);
|
|
656
|
+
pathOrder.pop();
|
|
657
|
+
stack.pop();
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const child = next.value;
|
|
661
|
+
const childColour = colour.get(child) ?? /* v8 ignore next */ WHITE;
|
|
662
|
+
if (childColour === BLACK)
|
|
663
|
+
continue;
|
|
664
|
+
if (childColour === GREY) {
|
|
665
|
+
const startIdx = pathIndex.get(child);
|
|
666
|
+
/* v8 ignore next */
|
|
667
|
+
if (startIdx === undefined)
|
|
668
|
+
continue;
|
|
669
|
+
const cycle = pathOrder.slice(startIdx);
|
|
670
|
+
const sortedKey = [...cycle].sort().join("|");
|
|
671
|
+
if (!seenCycles.has(sortedKey)) {
|
|
672
|
+
seenCycles.add(sortedKey);
|
|
673
|
+
cycles.push(cycle);
|
|
674
|
+
}
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
colour.set(child, GREY);
|
|
678
|
+
pathIndex.set(child, pathOrder.length);
|
|
679
|
+
pathOrder.push(child);
|
|
680
|
+
const childTargets = edges.get(child) ?? /* v8 ignore next */ new Set();
|
|
681
|
+
stack.push({ node: child, iter: childTargets[Symbol.iterator]() });
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
for (const key of edges.keys()) {
|
|
685
|
+
if (colour.get(key) === WHITE)
|
|
686
|
+
dfs(key);
|
|
687
|
+
}
|
|
688
|
+
return cycles;
|
|
689
|
+
}
|
|
690
|
+
//# sourceMappingURL=extractDependencyGraph.js.map
|