@tsplc/compiler 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +16 -2
- package/backends/codesys/config.ts +54 -0
- package/backends/codesys/emit.ts +1063 -0
- package/backends/codesys/engineering.ts +88 -0
- package/backends/codesys/http.ts +598 -0
- package/backends/codesys/index.ts +12 -0
- package/backends/codesys/io-paths.ts +103 -0
- package/backends/codesys/names.ts +76 -0
- package/backends/codesys/numeric.ts +356 -0
- package/backends/codesys/plan.ts +512 -0
- package/backends/codesys/publication.ts +59 -0
- package/backends/codesys/storage.ts +63 -0
- package/backends/codesys/task-access.ts +118 -0
- package/backends/codesys/types.ts +178 -0
- package/backends/codesys/vendor.ts +108 -0
- package/dist/cli.js +740 -0
- package/dist/cli.js.map +13 -0
- package/dist/clock.js +8 -0
- package/dist/clock.js.map +10 -0
- package/dist/codesys.js +2587 -0
- package/dist/codesys.js.map +22 -0
- package/dist/elaborate-capture.js +118 -0
- package/dist/elaborate-capture.js.map +10 -0
- package/dist/index.js +6859 -0
- package/dist/index.js.map +25 -0
- package/dist/ir.js +1888 -0
- package/dist/ir.js.map +22 -0
- package/dist/math.js +24 -0
- package/dist/math.js.map +10 -0
- package/dist/project.js +423 -0
- package/dist/project.js.map +11 -0
- package/ir/aggregates.ts +333 -0
- package/ir/allocation.ts +74 -0
- package/ir/core-model.ts +116 -0
- package/ir/core.ts +63 -0
- package/ir/declarations.ts +201 -0
- package/ir/diagnostics.ts +43 -0
- package/ir/domains.ts +96 -0
- package/ir/index.ts +7 -0
- package/ir/link.ts +176 -0
- package/ir/model.ts +268 -0
- package/ir/normalization-error.ts +10 -0
- package/ir/routine.ts +826 -0
- package/ir/scalars.ts +675 -0
- package/ir/types.ts +70 -0
- package/package.json +105 -4
- package/project/artifacts.ts +277 -0
- package/project/cli.ts +282 -0
- package/project/index.ts +1 -0
- package/project/project.ts +298 -0
- package/project/server.ts +83 -0
- package/src/frontend/async.ts +1060 -0
- package/src/frontend/blocks.ts +419 -0
- package/src/frontend/configuration.ts +167 -0
- package/src/frontend/const-functions.ts +153 -0
- package/src/frontend/cooperative.ts +75 -0
- package/src/frontend/elaborate/capture.ts +138 -0
- package/src/frontend/elaborate/graph.ts +94 -0
- package/src/frontend/elaborate/index.ts +302 -0
- package/src/frontend/elaborate/layouts.ts +390 -0
- package/src/frontend/elaborate/model.ts +177 -0
- package/src/frontend/elaborate/specialize.ts +2280 -0
- package/src/frontend/exposure.ts +147 -0
- package/src/frontend/module-graph.ts +63 -0
- package/src/frontend/promises.ts +751 -0
- package/src/frontend/services.ts +435 -0
- package/src/frontend/types.ts +578 -0
- package/src/frontend/typescript.ts +2442 -0
- package/src/index.ts +7 -0
- package/stdlib/clock.ts +2 -0
- package/stdlib/math.ts +19 -0
- package/types/async.d.ts +81 -0
- package/types/clock.d.ts +4 -0
- package/types/config.d.ts +2 -0
- package/types/data-types.d.ts +31 -0
- package/types/globals.d.ts +40 -0
- package/types/index.d.ts +111 -0
- package/types/plc.d.ts +5 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
import { runtimeSources } from "./module-graph.ts";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { dirname, relative } from "node:path";
|
|
4
|
+
import { error, type Diagnostic } from "@tsplc/compiler/ir";
|
|
5
|
+
|
|
6
|
+
type Routine = ts.FunctionDeclaration | ts.MethodDeclaration;
|
|
7
|
+
type Handler = ts.ArrowFunction | ts.FunctionExpression | Routine;
|
|
8
|
+
interface Edit {
|
|
9
|
+
start: number;
|
|
10
|
+
end: number;
|
|
11
|
+
text: string;
|
|
12
|
+
}
|
|
13
|
+
interface Part {
|
|
14
|
+
kind: "then" | "catch";
|
|
15
|
+
handlers: readonly ts.Expression[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Normalize promise syntax into named coroutines; scheduling stays in async lowering. */
|
|
19
|
+
export const lowerPromises = (program: ts.Program, sdk: string) => {
|
|
20
|
+
const checker = program.getTypeChecker();
|
|
21
|
+
const sources = new Map<string, string>(),
|
|
22
|
+
diagnostics: Diagnostic[] = [];
|
|
23
|
+
const runtimeBlocks = new Map<string, { kind: "mailbox" }>();
|
|
24
|
+
const files = runtimeSources(program).filter((source) => !source.isDeclarationFile);
|
|
25
|
+
const additions = new Map<ts.SourceFile, string[]>(),
|
|
26
|
+
edits = new Map<ts.SourceFile, Edit[]>();
|
|
27
|
+
const methods = new Map<ts.ClassDeclaration, string[]>();
|
|
28
|
+
let sequence = 0;
|
|
29
|
+
const symbol = (node: ts.Node): ts.Symbol | undefined => {
|
|
30
|
+
let value = checker.getSymbolAtLocation(node);
|
|
31
|
+
while (value && value.flags & ts.SymbolFlags.Alias) value = checker.getAliasedSymbol(value);
|
|
32
|
+
return value;
|
|
33
|
+
};
|
|
34
|
+
const official = (node: ts.Node, name: string) => {
|
|
35
|
+
const value = symbol(node);
|
|
36
|
+
return (
|
|
37
|
+
value?.name === name &&
|
|
38
|
+
value.declarations?.some((declaration) =>
|
|
39
|
+
declaration.getSourceFile().fileName.startsWith(dirname(sdk) + "/"),
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
const diagnosticFor = (node: ts.Node, message: string) => {
|
|
44
|
+
const source = node.getSourceFile(),
|
|
45
|
+
start = node.getStart(source),
|
|
46
|
+
position = source.getLineAndCharacterOfPosition(start);
|
|
47
|
+
return error("PLC1012", message, {
|
|
48
|
+
file: source.fileName,
|
|
49
|
+
start,
|
|
50
|
+
end: node.end,
|
|
51
|
+
line: position.line + 1,
|
|
52
|
+
column: position.character + 1,
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
const fail: (node: ts.Node, message: string) => never = (node, message) => {
|
|
56
|
+
throw { promiseDiagnostic: diagnosticFor(node, message) };
|
|
57
|
+
};
|
|
58
|
+
const append = (source: ts.SourceFile, text: string) =>
|
|
59
|
+
additions.set(source, [...(additions.get(source) ?? []), text]);
|
|
60
|
+
const edit = (node: ts.Node, text: string) => {
|
|
61
|
+
const source = node.getSourceFile();
|
|
62
|
+
edits.set(source, [
|
|
63
|
+
...(edits.get(source) ?? []),
|
|
64
|
+
{ start: node.getStart(source), end: node.end, text },
|
|
65
|
+
]);
|
|
66
|
+
};
|
|
67
|
+
const routines = new Map<ts.Symbol, Routine>();
|
|
68
|
+
for (const source of files)
|
|
69
|
+
for (const statement of source.statements) {
|
|
70
|
+
const candidates = ts.isClassDeclaration(statement)
|
|
71
|
+
? statement.members.filter(ts.isMethodDeclaration)
|
|
72
|
+
: [statement];
|
|
73
|
+
for (const candidate of candidates)
|
|
74
|
+
if (
|
|
75
|
+
(ts.isFunctionDeclaration(candidate) || ts.isMethodDeclaration(candidate)) &&
|
|
76
|
+
candidate.name
|
|
77
|
+
)
|
|
78
|
+
routines.set(symbol(candidate.name)!, candidate);
|
|
79
|
+
}
|
|
80
|
+
const asyncRoutine = (node: ts.Node) => {
|
|
81
|
+
const fn = routines.get(symbol(node)!);
|
|
82
|
+
return fn?.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)
|
|
83
|
+
? fn
|
|
84
|
+
: undefined;
|
|
85
|
+
};
|
|
86
|
+
const job = (node: ts.Node) => {
|
|
87
|
+
const declaration = symbol(node)?.declarations?.find(
|
|
88
|
+
(value) => ts.isVariableDeclaration(value) || ts.isPropertyDeclaration(value),
|
|
89
|
+
);
|
|
90
|
+
if (
|
|
91
|
+
!declaration ||
|
|
92
|
+
(!ts.isVariableDeclaration(declaration) && !ts.isPropertyDeclaration(declaration))
|
|
93
|
+
)
|
|
94
|
+
return;
|
|
95
|
+
const init = declaration.initializer;
|
|
96
|
+
if (
|
|
97
|
+
!init ||
|
|
98
|
+
!ts.isCallExpression(init) ||
|
|
99
|
+
!ts.isPropertyAccessExpression(init.expression) ||
|
|
100
|
+
!official(init.expression.expression, "backgroundTasks") ||
|
|
101
|
+
init.expression.name.text !== "define"
|
|
102
|
+
)
|
|
103
|
+
return;
|
|
104
|
+
return asyncRoutine(init.arguments[0]!);
|
|
105
|
+
};
|
|
106
|
+
const operation = (call: ts.CallExpression) =>
|
|
107
|
+
asyncRoutine(
|
|
108
|
+
ts.isPropertyAccessExpression(call.expression) ? call.expression.name : call.expression,
|
|
109
|
+
) ??
|
|
110
|
+
job(ts.isPropertyAccessExpression(call.expression) ? call.expression.name : call.expression);
|
|
111
|
+
const isSdk = (call: ts.CallExpression, name: string) =>
|
|
112
|
+
official(
|
|
113
|
+
ts.isPropertyAccessExpression(call.expression) ? call.expression.name : call.expression,
|
|
114
|
+
name,
|
|
115
|
+
);
|
|
116
|
+
const split = (
|
|
117
|
+
expression: ts.Expression,
|
|
118
|
+
): { root: ts.CallExpression; parts: Part[] } | undefined => {
|
|
119
|
+
if (!ts.isCallExpression(expression)) return;
|
|
120
|
+
if (
|
|
121
|
+
ts.isPropertyAccessExpression(expression.expression) &&
|
|
122
|
+
["then", "catch"].includes(expression.expression.name.text)
|
|
123
|
+
) {
|
|
124
|
+
const prefix = split(expression.expression.expression);
|
|
125
|
+
if (!prefix) return;
|
|
126
|
+
return {
|
|
127
|
+
root: prefix.root,
|
|
128
|
+
parts: [
|
|
129
|
+
...prefix.parts,
|
|
130
|
+
{
|
|
131
|
+
kind: expression.expression.name.text === "then" ? "then" : "catch",
|
|
132
|
+
handlers: expression.arguments,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (operation(expression) || isSdk(expression, "sleep") || isSdk(expression, "fetch"))
|
|
138
|
+
return { root: expression, parts: [] };
|
|
139
|
+
};
|
|
140
|
+
const aliases = new Map<ts.Node, string>();
|
|
141
|
+
const typeImports = new Map<ts.SourceFile, Set<string>>();
|
|
142
|
+
const typeText = (node: ts.TypeNode, source: ts.SourceFile): string => {
|
|
143
|
+
if (node.kind === ts.SyntaxKind.VoidKeyword) return "void";
|
|
144
|
+
if (node.getSourceFile() === source) return node.getText(source);
|
|
145
|
+
let alias = aliases.get(node);
|
|
146
|
+
if (!alias) {
|
|
147
|
+
alias = `TsplcPromiseType${sequence++}`;
|
|
148
|
+
aliases.set(node, alias);
|
|
149
|
+
append(node.getSourceFile(), `export type ${alias}=${node.getText(node.getSourceFile())};`);
|
|
150
|
+
}
|
|
151
|
+
const path = relative(dirname(source.fileName), node.getSourceFile().fileName).replaceAll(
|
|
152
|
+
"\\",
|
|
153
|
+
"/",
|
|
154
|
+
);
|
|
155
|
+
const importedAliases = typeImports.get(source) ?? new Set<string>();
|
|
156
|
+
if (importedAliases.has(alias)) return alias;
|
|
157
|
+
importedAliases.add(alias);
|
|
158
|
+
typeImports.set(source, importedAliases);
|
|
159
|
+
append(
|
|
160
|
+
source,
|
|
161
|
+
`import type {${alias}} from ${JSON.stringify(path.startsWith(".") ? path : `./${path}`)};`,
|
|
162
|
+
);
|
|
163
|
+
return alias;
|
|
164
|
+
};
|
|
165
|
+
const expressionType = (node: ts.Expression, source: ts.SourceFile): string => {
|
|
166
|
+
if (
|
|
167
|
+
ts.isCallExpression(node) &&
|
|
168
|
+
node.typeArguments?.[0] &&
|
|
169
|
+
(official(node.expression, "defaultValue") ||
|
|
170
|
+
official(node.expression, "copy") ||
|
|
171
|
+
["exact", "wrap", "wrapAdd", "wrapMultiply", "saturate"].some((name) =>
|
|
172
|
+
official(node.expression, name),
|
|
173
|
+
))
|
|
174
|
+
)
|
|
175
|
+
return typeText(node.typeArguments[0], source);
|
|
176
|
+
const name = ts.isPropertyAccessExpression(node) ? node.name : node;
|
|
177
|
+
const declaration = symbol(name)?.declarations?.find(
|
|
178
|
+
(value) =>
|
|
179
|
+
(ts.isVariableDeclaration(value) ||
|
|
180
|
+
ts.isParameter(value) ||
|
|
181
|
+
ts.isPropertyDeclaration(value) ||
|
|
182
|
+
ts.isPropertySignature(value)) &&
|
|
183
|
+
value.type,
|
|
184
|
+
);
|
|
185
|
+
if (
|
|
186
|
+
declaration &&
|
|
187
|
+
(ts.isVariableDeclaration(declaration) ||
|
|
188
|
+
ts.isParameter(declaration) ||
|
|
189
|
+
ts.isPropertyDeclaration(declaration) ||
|
|
190
|
+
ts.isPropertySignature(declaration)) &&
|
|
191
|
+
declaration.type
|
|
192
|
+
)
|
|
193
|
+
return typeText(declaration.type, source);
|
|
194
|
+
const type = checker.getTypeAtLocation(node);
|
|
195
|
+
if (type.flags & ts.TypeFlags.BooleanLike) return "BOOL";
|
|
196
|
+
if (type.flags & ts.TypeFlags.Void) return "void";
|
|
197
|
+
if (type.symbol && type.symbol.name !== "__type" && type.flags & ts.TypeFlags.Object)
|
|
198
|
+
return checker.typeToString(
|
|
199
|
+
type,
|
|
200
|
+
node,
|
|
201
|
+
ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope,
|
|
202
|
+
);
|
|
203
|
+
fail(
|
|
204
|
+
node,
|
|
205
|
+
"Annotate this value with its concrete PLC type before using it in an async continuation.",
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
const isAggregate = (type: ts.Type | undefined) =>
|
|
209
|
+
Boolean(type && type.flags & ts.TypeFlags.Object);
|
|
210
|
+
const isPromise = (node: ts.Node) => {
|
|
211
|
+
const type = checker.getTypeAtLocation(node);
|
|
212
|
+
return checker.getAwaitedType(type) !== type;
|
|
213
|
+
};
|
|
214
|
+
const printer = ts.createPrinter();
|
|
215
|
+
const replaceNames = (node: ts.Node, names: ReadonlyMap<ts.Symbol, string>): string => {
|
|
216
|
+
const transformed = ts.transform(node, [
|
|
217
|
+
(context) => (root) => {
|
|
218
|
+
const visit = (value: ts.Node): ts.VisitResult<ts.Node> => {
|
|
219
|
+
if (
|
|
220
|
+
ts.isReturnStatement(value) &&
|
|
221
|
+
value.expression &&
|
|
222
|
+
!ts.isAwaitExpression(value.expression) &&
|
|
223
|
+
isPromise(value.expression)
|
|
224
|
+
) {
|
|
225
|
+
return ts.factory.updateReturnStatement(
|
|
226
|
+
value,
|
|
227
|
+
ts.factory.createAwaitExpression(
|
|
228
|
+
ts.visitNode(value.expression, visit) as ts.Expression,
|
|
229
|
+
),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (ts.isShorthandPropertyAssignment(value)) {
|
|
233
|
+
const key = checker.getShorthandAssignmentValueSymbol(value);
|
|
234
|
+
const name = key && names.get(key);
|
|
235
|
+
if (name)
|
|
236
|
+
return ts.factory.createPropertyAssignment(
|
|
237
|
+
value.name,
|
|
238
|
+
ts.factory.createIdentifier(name),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (
|
|
242
|
+
ts.isIdentifier(value) &&
|
|
243
|
+
!(ts.isPropertyAccessExpression(value.parent) && value.parent.name === value) &&
|
|
244
|
+
!(ts.isPropertyAssignment(value.parent) && value.parent.name === value)
|
|
245
|
+
) {
|
|
246
|
+
const name = names.get(symbol(value)!);
|
|
247
|
+
if (name) return ts.factory.createIdentifier(name);
|
|
248
|
+
}
|
|
249
|
+
return ts.visitEachChild(value, visit, context);
|
|
250
|
+
};
|
|
251
|
+
return ts.visitNode(root, visit) as typeof root;
|
|
252
|
+
},
|
|
253
|
+
]);
|
|
254
|
+
const value = printer.printNode(
|
|
255
|
+
ts.EmitHint.Unspecified,
|
|
256
|
+
transformed.transformed[0]!,
|
|
257
|
+
node.getSourceFile(),
|
|
258
|
+
);
|
|
259
|
+
transformed.dispose();
|
|
260
|
+
return value;
|
|
261
|
+
};
|
|
262
|
+
const hasBorrow = (node: ts.TypeNode, seen = new Set<ts.Symbol>()): boolean => {
|
|
263
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
264
|
+
if (official(node.typeName, "InOut")) return true;
|
|
265
|
+
const key = symbol(node.typeName);
|
|
266
|
+
if (key && !seen.has(key)) {
|
|
267
|
+
seen.add(key);
|
|
268
|
+
for (const declaration of key.declarations ?? []) {
|
|
269
|
+
if (
|
|
270
|
+
ts.isInterfaceDeclaration(declaration) &&
|
|
271
|
+
declaration.members.some(
|
|
272
|
+
(member) =>
|
|
273
|
+
ts.isPropertySignature(member) && member.type && hasBorrow(member.type, seen),
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
return true;
|
|
277
|
+
if (ts.isTypeAliasDeclaration(declaration) && hasBorrow(declaration.type, seen))
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return node.getChildren().some((child) => ts.isTypeNode(child) && hasBorrow(child, seen));
|
|
283
|
+
};
|
|
284
|
+
let diagnosticSource: ts.SourceFile | undefined;
|
|
285
|
+
const diag = () => {
|
|
286
|
+
if (!diagnosticSource) throw new Error("Missing async diagnostics owner");
|
|
287
|
+
return "tsplcPromiseDiagnostics";
|
|
288
|
+
};
|
|
289
|
+
const imported = new Set<ts.SourceFile>();
|
|
290
|
+
const ensureImports = (source: ts.SourceFile) => {
|
|
291
|
+
if (imported.has(source)) return;
|
|
292
|
+
imported.add(source);
|
|
293
|
+
append(
|
|
294
|
+
source,
|
|
295
|
+
`import {cooperativeTasks as tsplcCooperative,errorCode as tsplcErrorCode} from "@tsplc/compiler/async";import {wrapAdd as tsplcAsyncWrapAdd} from "@tsplc/compiler/types";`,
|
|
296
|
+
);
|
|
297
|
+
};
|
|
298
|
+
const scopes = new Map<ts.SourceFile | ts.ClassDeclaration, string[]>();
|
|
299
|
+
const make = (
|
|
300
|
+
expression: ts.Expression,
|
|
301
|
+
owner: ts.FunctionDeclaration | ts.MethodDeclaration,
|
|
302
|
+
detached: boolean,
|
|
303
|
+
): string => {
|
|
304
|
+
const parsed = split(expression);
|
|
305
|
+
if (!parsed) fail(expression, "Expected a statically resolved async call.");
|
|
306
|
+
const source = expression.getSourceFile();
|
|
307
|
+
const classOwner =
|
|
308
|
+
ts.isMethodDeclaration(owner) && ts.isClassDeclaration(owner.parent)
|
|
309
|
+
? owner.parent
|
|
310
|
+
: undefined;
|
|
311
|
+
ensureImports(source);
|
|
312
|
+
diagnosticSource ??= source;
|
|
313
|
+
const captures = new Map<ts.Symbol, { name: string; type: string; argument: string }>();
|
|
314
|
+
const collectCaptures = (handler: Handler) => {
|
|
315
|
+
const visit = (node: ts.Node): void => {
|
|
316
|
+
if (
|
|
317
|
+
ts.isIdentifier(node) &&
|
|
318
|
+
!(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) &&
|
|
319
|
+
!(ts.isPropertyAssignment(node.parent) && node.parent.name === node)
|
|
320
|
+
) {
|
|
321
|
+
const key = ts.isShorthandPropertyAssignment(node.parent)
|
|
322
|
+
? checker.getShorthandAssignmentValueSymbol(node.parent)
|
|
323
|
+
: symbol(node);
|
|
324
|
+
const declaration = key?.valueDeclaration;
|
|
325
|
+
if (
|
|
326
|
+
declaration &&
|
|
327
|
+
(ts.isVariableDeclaration(declaration) || ts.isParameter(declaration))
|
|
328
|
+
) {
|
|
329
|
+
let scope: ts.Node | undefined = declaration.parent;
|
|
330
|
+
while (scope && !ts.isFunctionLike(scope)) scope = scope.parent;
|
|
331
|
+
if (scope === owner && !captures.has(key!)) {
|
|
332
|
+
if (
|
|
333
|
+
ts.isVariableDeclaration(declaration) &&
|
|
334
|
+
(!ts.isVariableDeclarationList(declaration.parent) ||
|
|
335
|
+
!(declaration.parent.flags & ts.NodeFlags.Const))
|
|
336
|
+
)
|
|
337
|
+
fail(
|
|
338
|
+
node,
|
|
339
|
+
"Continuation captures must be const values; store mutable state on the controller instead.",
|
|
340
|
+
);
|
|
341
|
+
if (!declaration.type)
|
|
342
|
+
fail(node, "Annotate a captured value with its concrete PLC type.");
|
|
343
|
+
if (hasBorrow(declaration.type))
|
|
344
|
+
fail(
|
|
345
|
+
node,
|
|
346
|
+
"A continuation cannot capture borrowed InOut storage; capture values or use controller state.",
|
|
347
|
+
);
|
|
348
|
+
captures.set(key!, {
|
|
349
|
+
name: `capture${captures.size}`,
|
|
350
|
+
type: typeText(declaration.type, source),
|
|
351
|
+
argument: node.text,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
ts.forEachChild(node, visit);
|
|
357
|
+
};
|
|
358
|
+
visit(handler.body!);
|
|
359
|
+
};
|
|
360
|
+
for (const part of parsed.parts)
|
|
361
|
+
for (const value of part.handlers) {
|
|
362
|
+
if (
|
|
363
|
+
value.kind === ts.SyntaxKind.UndefinedKeyword ||
|
|
364
|
+
value.kind === ts.SyntaxKind.NullKeyword
|
|
365
|
+
)
|
|
366
|
+
continue;
|
|
367
|
+
if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) collectCaptures(value);
|
|
368
|
+
}
|
|
369
|
+
const captureNames = new Map([...captures].map(([key, value]) => [key, value.name]));
|
|
370
|
+
const root = parsed.root,
|
|
371
|
+
fn = operation(root);
|
|
372
|
+
let resultAggregate = isAggregate(checker.getAwaitedType(checker.getTypeAtLocation(root)));
|
|
373
|
+
const arguments_: { name: string; type: string; expression: string }[] = [];
|
|
374
|
+
let invocation: string, resultType: string;
|
|
375
|
+
if (fn) {
|
|
376
|
+
const node = fn.type;
|
|
377
|
+
if (!node || !ts.isTypeReferenceNode(node) || !node.typeArguments?.[0])
|
|
378
|
+
fail(fn, "Async calls need an explicit Promise<T> result.");
|
|
379
|
+
resultType = typeText(node.typeArguments[0], source);
|
|
380
|
+
fn.parameters.forEach((parameter, index) => {
|
|
381
|
+
if (!parameter.type || !root.arguments[index])
|
|
382
|
+
fail(root, "Async argument count differs from its concrete signature.");
|
|
383
|
+
arguments_.push({
|
|
384
|
+
name: `argument${index}`,
|
|
385
|
+
type: typeText(parameter.type, source),
|
|
386
|
+
expression: root.arguments[index]!.getText(source),
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
invocation = `${root.expression.getText(source)}(${arguments_.map((value) => value.name).join(",")})`;
|
|
390
|
+
} else if (isSdk(root, "sleep")) {
|
|
391
|
+
if (root.arguments.length !== 1) fail(root, "sleep requires one TIME argument.");
|
|
392
|
+
arguments_.push({
|
|
393
|
+
name: "argument0",
|
|
394
|
+
type: "TIME",
|
|
395
|
+
expression: root.arguments[0]!.getText(source),
|
|
396
|
+
});
|
|
397
|
+
invocation = `${root.expression.getText(source)}(argument0)`;
|
|
398
|
+
resultType = "void";
|
|
399
|
+
} else {
|
|
400
|
+
const response = root.typeArguments?.[0];
|
|
401
|
+
if (!response) fail(root, "fetch requires a typed response.");
|
|
402
|
+
resultType = typeText(response, source);
|
|
403
|
+
const options = root.arguments[1];
|
|
404
|
+
if (options && !ts.isObjectLiteralExpression(options))
|
|
405
|
+
fail(options, "fetch options must be an inline object.");
|
|
406
|
+
let optionText = "";
|
|
407
|
+
if (options && ts.isObjectLiteralExpression(options))
|
|
408
|
+
optionText = options.properties
|
|
409
|
+
.map((property) => {
|
|
410
|
+
if (!ts.isPropertyAssignment(property)) fail(property, "fetch requires named options.");
|
|
411
|
+
if (property.name.getText(source) !== "json") return property.getText(source);
|
|
412
|
+
arguments_.push({
|
|
413
|
+
name: "argument0",
|
|
414
|
+
type: expressionType(property.initializer, source),
|
|
415
|
+
expression: property.initializer.getText(source),
|
|
416
|
+
});
|
|
417
|
+
return "json:argument0";
|
|
418
|
+
})
|
|
419
|
+
.join(",");
|
|
420
|
+
invocation = `${root.expression.getText(source)}<${root.typeArguments!.map((type) => type.getText(source)).join(",")}>(${root.arguments[0]!.getText(source)}${options ? `,{${optionText}}` : ""})`;
|
|
421
|
+
}
|
|
422
|
+
const params = [
|
|
423
|
+
...arguments_,
|
|
424
|
+
...[...captures.values()].map((value) => ({
|
|
425
|
+
name: value.name,
|
|
426
|
+
type: value.type,
|
|
427
|
+
expression: value.argument,
|
|
428
|
+
})),
|
|
429
|
+
];
|
|
430
|
+
const signature = params.map((value) => `${value.name}:${value.type}`).join(","),
|
|
431
|
+
args = params.map((value) => value.name).join(",");
|
|
432
|
+
const addHelper = (
|
|
433
|
+
name: string,
|
|
434
|
+
body: string,
|
|
435
|
+
type: string,
|
|
436
|
+
extra?: { type: string; argument: string },
|
|
437
|
+
) => {
|
|
438
|
+
const text = `${classOwner ? "private " : "export "}async ${classOwner ? "" : "function "}${name}(${[signature, extra ? `promiseInput:${extra.type}` : ""].filter(Boolean).join(",")}):Promise<${type}>{${body}}`;
|
|
439
|
+
if (classOwner) methods.set(classOwner, [...(methods.get(classOwner) ?? []), text]);
|
|
440
|
+
else append(source, text);
|
|
441
|
+
return `${classOwner ? "this." : ""}${name}(${[args, extra?.argument ?? ""].filter(Boolean).join(",")})`;
|
|
442
|
+
};
|
|
443
|
+
const finish = (call: string, type: string, discard = false) =>
|
|
444
|
+
type === "void" || discard ? `await ${call};return;` : `return await ${call};`;
|
|
445
|
+
let prefix = addHelper(
|
|
446
|
+
`tsplcPromiseOperation${sequence++}`,
|
|
447
|
+
finish(invocation, resultType),
|
|
448
|
+
resultType,
|
|
449
|
+
);
|
|
450
|
+
const handler = (
|
|
451
|
+
expression: ts.Expression | undefined,
|
|
452
|
+
inputType: string,
|
|
453
|
+
input: string,
|
|
454
|
+
): { call: string; type: string; aggregate: boolean } | undefined => {
|
|
455
|
+
if (
|
|
456
|
+
!expression ||
|
|
457
|
+
expression.kind === ts.SyntaxKind.NullKeyword ||
|
|
458
|
+
(ts.isIdentifier(expression) && expression.text === "undefined")
|
|
459
|
+
)
|
|
460
|
+
return;
|
|
461
|
+
let declaration: Handler;
|
|
462
|
+
if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression))
|
|
463
|
+
declaration = expression;
|
|
464
|
+
else {
|
|
465
|
+
const named = routines.get(symbol(expression)!);
|
|
466
|
+
if (!named || !ts.isFunctionDeclaration(named))
|
|
467
|
+
fail(
|
|
468
|
+
expression,
|
|
469
|
+
"Use a named function or inline arrow; bind instance methods with an arrow.",
|
|
470
|
+
);
|
|
471
|
+
declaration = named;
|
|
472
|
+
}
|
|
473
|
+
const callbackSignature = checker.getSignatureFromDeclaration(declaration);
|
|
474
|
+
const aggregate = isAggregate(
|
|
475
|
+
callbackSignature &&
|
|
476
|
+
checker.getAwaitedType(checker.getReturnTypeOfSignature(callbackSignature)),
|
|
477
|
+
);
|
|
478
|
+
if (!declaration.body || declaration.parameters.length > 1)
|
|
479
|
+
fail(declaration, "Promise callbacks take at most one value.");
|
|
480
|
+
if (declaration.getSourceFile() !== source) {
|
|
481
|
+
const result = declaration.type;
|
|
482
|
+
if (!result) fail(declaration, "Annotate callback results with their concrete PLC type.");
|
|
483
|
+
let output = result;
|
|
484
|
+
if (
|
|
485
|
+
ts.isTypeReferenceNode(result) &&
|
|
486
|
+
result.typeName.getText(declaration.getSourceFile()) === "Promise"
|
|
487
|
+
)
|
|
488
|
+
output = result.typeArguments![0]!;
|
|
489
|
+
// Named callbacks retain their original module and access to its private state.
|
|
490
|
+
const callbackType = typeText(output, source);
|
|
491
|
+
const name = expression.getText(source);
|
|
492
|
+
if (declaration.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword))
|
|
493
|
+
return {
|
|
494
|
+
call: `${name}(${declaration.parameters.length ? input : ""})`,
|
|
495
|
+
type: callbackType,
|
|
496
|
+
aggregate,
|
|
497
|
+
};
|
|
498
|
+
return {
|
|
499
|
+
call: addHelper(
|
|
500
|
+
`tsplcPromiseNamed${sequence++}`,
|
|
501
|
+
callbackType === "void"
|
|
502
|
+
? `${name}(${declaration.parameters.length ? "promiseInput" : ""});return;`
|
|
503
|
+
: `return ${name}(${declaration.parameters.length ? "promiseInput" : ""});`,
|
|
504
|
+
callbackType,
|
|
505
|
+
declaration.parameters.length ? { type: inputType, argument: input } : undefined,
|
|
506
|
+
),
|
|
507
|
+
type: callbackType,
|
|
508
|
+
aggregate,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
const names = new Map(captureNames);
|
|
512
|
+
const parameter = declaration.parameters[0];
|
|
513
|
+
if (parameter) {
|
|
514
|
+
if (!ts.isIdentifier(parameter.name))
|
|
515
|
+
fail(parameter, "Promise callback parameters need names.");
|
|
516
|
+
names.set(symbol(parameter.name)!, "promiseValue");
|
|
517
|
+
}
|
|
518
|
+
let type: string;
|
|
519
|
+
if (declaration.type) {
|
|
520
|
+
const annotation = declaration.type;
|
|
521
|
+
type =
|
|
522
|
+
ts.isTypeReferenceNode(annotation) && annotation.typeName.getText(source) === "Promise"
|
|
523
|
+
? typeText(annotation.typeArguments![0]!, source)
|
|
524
|
+
: typeText(annotation, source);
|
|
525
|
+
} else {
|
|
526
|
+
const resolved = checker.getSignatureFromDeclaration(declaration);
|
|
527
|
+
const inferred =
|
|
528
|
+
resolved && checker.getAwaitedType(checker.getReturnTypeOfSignature(resolved));
|
|
529
|
+
if (inferred?.flags && inferred.flags & ts.TypeFlags.Void) type = "void";
|
|
530
|
+
else if (
|
|
531
|
+
!ts.isBlock(declaration.body) &&
|
|
532
|
+
ts.isIdentifier(declaration.body) &&
|
|
533
|
+
parameter &&
|
|
534
|
+
symbol(declaration.body) === symbol(parameter.name)
|
|
535
|
+
)
|
|
536
|
+
type = inputType;
|
|
537
|
+
else if (!ts.isBlock(declaration.body)) {
|
|
538
|
+
const returned = ts.isCallExpression(declaration.body)
|
|
539
|
+
? operation(declaration.body)
|
|
540
|
+
: undefined;
|
|
541
|
+
type =
|
|
542
|
+
returned?.type && ts.isTypeReferenceNode(returned.type)
|
|
543
|
+
? typeText(returned.type.typeArguments![0]!, source)
|
|
544
|
+
: expressionType(declaration.body, source);
|
|
545
|
+
} else
|
|
546
|
+
fail(declaration, "Annotate a callback that returns a value with its concrete PLC type.");
|
|
547
|
+
}
|
|
548
|
+
const name = `tsplcPromiseHandler${sequence++}`;
|
|
549
|
+
const body = replaceNames(declaration.body, names);
|
|
550
|
+
const code = ts.isBlock(declaration.body)
|
|
551
|
+
? body.slice(1, -1)
|
|
552
|
+
: type === "void"
|
|
553
|
+
? `${body};return;`
|
|
554
|
+
: `return ${isPromise(declaration.body) ? "await " : ""}${body};`;
|
|
555
|
+
const handlerParams = [
|
|
556
|
+
...(parameter && inputType !== "void" ? [`promiseValue:${inputType}`] : []),
|
|
557
|
+
...params.map((value) => `${value.name}:${value.type}`),
|
|
558
|
+
].join(",");
|
|
559
|
+
const text = `${classOwner ? "private " : "export "}async ${classOwner ? "" : "function "}${name}(${handlerParams}):Promise<${type}>{${code}}`;
|
|
560
|
+
if (classOwner) methods.set(classOwner, [...(methods.get(classOwner) ?? []), text]);
|
|
561
|
+
else append(source, text);
|
|
562
|
+
return {
|
|
563
|
+
call: `${classOwner ? "this." : ""}${name}(${[...(parameter && inputType !== "void" ? [input] : []), ...params.map((value) => value.name)].join(",")})`,
|
|
564
|
+
type,
|
|
565
|
+
aggregate,
|
|
566
|
+
};
|
|
567
|
+
};
|
|
568
|
+
for (const [index, part] of parsed.parts.entries()) {
|
|
569
|
+
const terminal = detached && index === parsed.parts.length - 1;
|
|
570
|
+
const fulfilled =
|
|
571
|
+
part.kind === "then" ? handler(part.handlers[0], resultType, "promiseResult") : undefined;
|
|
572
|
+
const rejected = handler(
|
|
573
|
+
part.handlers[part.kind === "catch" ? 0 : 1],
|
|
574
|
+
"UDINT",
|
|
575
|
+
"tsplcErrorCode(promiseError)",
|
|
576
|
+
);
|
|
577
|
+
if (!fulfilled && !rejected) continue;
|
|
578
|
+
const output = terminal ? "void" : (fulfilled?.type ?? resultType);
|
|
579
|
+
if (rejected && !terminal && rejected.type !== output)
|
|
580
|
+
fail(
|
|
581
|
+
expression,
|
|
582
|
+
"A recovery callback must return the same PLC type as the fulfilled branch.",
|
|
583
|
+
);
|
|
584
|
+
const success = fulfilled
|
|
585
|
+
? finish(fulfilled.call, fulfilled.type, terminal)
|
|
586
|
+
: resultType === "void" || terminal
|
|
587
|
+
? "return;"
|
|
588
|
+
: "return promiseResult;";
|
|
589
|
+
const load =
|
|
590
|
+
resultType === "void"
|
|
591
|
+
? `await ${prefix};`
|
|
592
|
+
: `const promiseResult:${resultType}=await ${prefix};`;
|
|
593
|
+
let body: string;
|
|
594
|
+
if (rejected && fulfilled) {
|
|
595
|
+
const stored =
|
|
596
|
+
resultType === "void"
|
|
597
|
+
? ""
|
|
598
|
+
: `let promiseResult:${resultType}=defaultValue<${resultType}>();`;
|
|
599
|
+
const attempt =
|
|
600
|
+
resultType === "void"
|
|
601
|
+
? `await ${prefix};`
|
|
602
|
+
: `const loaded:${resultType}=await ${prefix};promiseResult=${resultAggregate ? "copy(loaded)" : "loaded"};`;
|
|
603
|
+
body = `${stored}let succeeded:BOOL=false;let rejection:UDINT=0;try{${attempt}succeeded=true;}catch(promiseError){rejection=tsplcErrorCode(promiseError);}if(!succeeded){${finish(rejected.call.replace("tsplcErrorCode(promiseError)", "rejection"), rejected.type, terminal)}}${success}`;
|
|
604
|
+
} else if (rejected)
|
|
605
|
+
body = `try{${load}${success}}catch(promiseError){${finish(rejected.call, rejected.type, terminal)}}`;
|
|
606
|
+
else body = `${load}${success}`;
|
|
607
|
+
prefix = addHelper(`tsplcPromiseChain${sequence++}`, body, output);
|
|
608
|
+
resultType = output;
|
|
609
|
+
resultAggregate = terminal ? false : (fulfilled?.aggregate ?? resultAggregate);
|
|
610
|
+
}
|
|
611
|
+
if (!detached)
|
|
612
|
+
return prefix.replace(`(${args})`, `(${params.map((value) => value.expression).join(",")})`);
|
|
613
|
+
const site = `tsplcPromiseSite${sequence++}`;
|
|
614
|
+
const callable = prefix.slice(0, prefix.indexOf("("));
|
|
615
|
+
const declaration = `${classOwner ? "private " : "const "}${site}=tsplcCooperative.define(${callable});`;
|
|
616
|
+
if (classOwner) methods.set(classOwner, [...(methods.get(classOwner) ?? []), declaration]);
|
|
617
|
+
else append(source, declaration);
|
|
618
|
+
const scope = classOwner ?? source;
|
|
619
|
+
scopes.set(scope, [...(scopes.get(scope) ?? []), site]);
|
|
620
|
+
return `${classOwner ? "this." : ""}${site}.start(${params.map((value) => value.expression).join(",")});`;
|
|
621
|
+
};
|
|
622
|
+
const diagnosticsCalls: ts.CallExpression[] = [],
|
|
623
|
+
cancelCalls: {
|
|
624
|
+
node: ts.CallExpression;
|
|
625
|
+
owner: ts.FunctionDeclaration | ts.MethodDeclaration;
|
|
626
|
+
}[] = [];
|
|
627
|
+
for (const source of files) {
|
|
628
|
+
const visit = (node: ts.Node, owner?: ts.FunctionDeclaration | ts.MethodDeclaration): void => {
|
|
629
|
+
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
|
|
630
|
+
if (node.body) ts.forEachChild(node.body, (child) => visit(child, node));
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (!owner) {
|
|
634
|
+
ts.forEachChild(node, (child) => visit(child));
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (ts.isCallExpression(node) && official(node.expression, "asyncDiagnostics")) {
|
|
638
|
+
diagnosticSource ??= source;
|
|
639
|
+
diagnosticsCalls.push(node);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (ts.isCallExpression(node) && official(node.expression, "cancelAsync")) {
|
|
643
|
+
cancelCalls.push({ node, owner });
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const asynchronous = owner.modifiers?.some(
|
|
647
|
+
(modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword,
|
|
648
|
+
);
|
|
649
|
+
if (ts.isExpressionStatement(node)) {
|
|
650
|
+
const expression = ts.isVoidExpression(node.expression)
|
|
651
|
+
? node.expression.expression
|
|
652
|
+
: node.expression;
|
|
653
|
+
if (split(expression)) {
|
|
654
|
+
if (asynchronous) {
|
|
655
|
+
diagnostics.push(
|
|
656
|
+
diagnosticFor(
|
|
657
|
+
node,
|
|
658
|
+
"Await async work inside an async routine; detached calls belong in synchronous controllers.",
|
|
659
|
+
),
|
|
660
|
+
);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
try {
|
|
664
|
+
edit(node, make(expression, owner, true));
|
|
665
|
+
} catch (cause) {
|
|
666
|
+
if (typeof cause === "object" && cause !== null && "promiseDiagnostic" in cause)
|
|
667
|
+
diagnostics.push(cause.promiseDiagnostic as Diagnostic);
|
|
668
|
+
else throw cause;
|
|
669
|
+
}
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (ts.isCallExpression(node)) {
|
|
674
|
+
const chain = split(node);
|
|
675
|
+
if (chain?.parts.length) {
|
|
676
|
+
try {
|
|
677
|
+
edit(node, make(node, owner, false));
|
|
678
|
+
} catch (cause) {
|
|
679
|
+
if (typeof cause === "object" && cause !== null && "promiseDiagnostic" in cause)
|
|
680
|
+
diagnostics.push(cause.promiseDiagnostic as Diagnostic);
|
|
681
|
+
else throw cause;
|
|
682
|
+
}
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
ts.forEachChild(node, (child) => visit(child, owner));
|
|
687
|
+
};
|
|
688
|
+
visit(source);
|
|
689
|
+
}
|
|
690
|
+
for (const { node, owner } of cancelCalls) {
|
|
691
|
+
const scope =
|
|
692
|
+
ts.isMethodDeclaration(owner) && ts.isClassDeclaration(owner.parent)
|
|
693
|
+
? owner.parent
|
|
694
|
+
: owner.getSourceFile();
|
|
695
|
+
if (
|
|
696
|
+
node.arguments.length &&
|
|
697
|
+
(node.arguments.length !== 1 || node.arguments[0]!.kind !== ts.SyntaxKind.ThisKeyword)
|
|
698
|
+
)
|
|
699
|
+
diagnostics.push(
|
|
700
|
+
diagnosticFor(node, "cancelAsync takes this in a controller, or no argument in a module."),
|
|
701
|
+
);
|
|
702
|
+
const name = `tsplcCancelScope${sequence++}`;
|
|
703
|
+
const body = (scopes.get(scope) ?? [])
|
|
704
|
+
.map((site) => `${ts.isClassDeclaration(scope) ? "this." : ""}${site}.cancel();`)
|
|
705
|
+
.join("");
|
|
706
|
+
if (ts.isClassDeclaration(scope))
|
|
707
|
+
methods.set(scope, [...(methods.get(scope) ?? []), `private ${name}():void{${body}}`]);
|
|
708
|
+
else append(scope, `function ${name}():void{${body}}`);
|
|
709
|
+
edit(node, `${ts.isClassDeclaration(scope) ? "this." : ""}${name}()`);
|
|
710
|
+
}
|
|
711
|
+
if (diagnosticSource) {
|
|
712
|
+
const source = diagnosticSource;
|
|
713
|
+
ensureImports(source);
|
|
714
|
+
append(
|
|
715
|
+
source,
|
|
716
|
+
`export class TsplcPromiseDiagnostics extends FunctionBlock<{}>{private count:UDINT=0;private lastError:UDINT=0;public report(code:UDINT,count:UDINT):BOOL{this.count=tsplcAsyncWrapAdd<UDINT>(this.count,count);this.lastError=code;return true;}public snapshot():{valid:BOOL;count:UDINT;lastError:UDINT}{return {valid:true,count:this.count,lastError:this.lastError};}protected execute(input:{}):void{}}
|
|
717
|
+
export const tsplcPromiseDiagnostics=new TsplcPromiseDiagnostics();`,
|
|
718
|
+
);
|
|
719
|
+
runtimeBlocks.set(`${source.fileName}:TsplcPromiseDiagnostics`, { kind: "mailbox" });
|
|
720
|
+
for (const file of new Set([
|
|
721
|
+
...additions.keys(),
|
|
722
|
+
...diagnosticsCalls.map((node) => node.getSourceFile()),
|
|
723
|
+
])) {
|
|
724
|
+
if (file !== source) {
|
|
725
|
+
const path = relative(dirname(file.fileName), source.fileName).replaceAll("\\", "/");
|
|
726
|
+
append(
|
|
727
|
+
file,
|
|
728
|
+
`import {${diag()}} from ${JSON.stringify(path.startsWith(".") ? path : `./${path}`)};`,
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
for (const call of diagnosticsCalls) edit(call, `${diag()}.snapshot()`);
|
|
733
|
+
}
|
|
734
|
+
for (const [owner, texts] of methods) {
|
|
735
|
+
const source = owner.getSourceFile();
|
|
736
|
+
edits.set(source, [
|
|
737
|
+
...(edits.get(source) ?? []),
|
|
738
|
+
{ start: owner.end - 1, end: owner.end - 1, text: "\n" + texts.join("\n") },
|
|
739
|
+
]);
|
|
740
|
+
}
|
|
741
|
+
for (const source of files) {
|
|
742
|
+
const changes = edits.get(source) ?? [],
|
|
743
|
+
extra = additions.get(source) ?? [];
|
|
744
|
+
if (!changes.length && !extra.length) continue;
|
|
745
|
+
let text = source.text;
|
|
746
|
+
for (const change of changes.sort((left, right) => right.start - left.start))
|
|
747
|
+
text = text.slice(0, change.start) + change.text + text.slice(change.end);
|
|
748
|
+
sources.set(source.fileName, text + "\n" + extra.join("\n"));
|
|
749
|
+
}
|
|
750
|
+
return { sources, diagnostics, runtimeBlocks };
|
|
751
|
+
};
|