@tsplc/compiler 0.0.0-stage → 0.2.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 +1396 -0
- package/dist/cli.js.map +16 -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 +429 -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 +106 -4
- package/project/artifacts.ts +277 -0
- package/project/cli.ts +305 -0
- package/project/index.ts +1 -0
- package/project/plant.ts +437 -0
- package/project/project.ts +312 -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,1060 @@
|
|
|
1
|
+
import { runtimeSources } from "./module-graph.ts";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { dirname, resolve, relative } from "node:path";
|
|
4
|
+
import { cooperativeSite, taskSite } from "./cooperative.ts";
|
|
5
|
+
import { staticString } from "./configuration.ts";
|
|
6
|
+
import { error, type Diagnostic, type FunctionBlock, type PlcProgram } from "@tsplc/compiler/ir";
|
|
7
|
+
|
|
8
|
+
export interface AsyncLowering {
|
|
9
|
+
sources: Map<string, string>;
|
|
10
|
+
blocks: Map<string, NonNullable<FunctionBlock["runtime"]>>;
|
|
11
|
+
programs: Record<string, { module: string; export: string }>;
|
|
12
|
+
schedules: Map<string, NonNullable<PlcProgram["background"]>>;
|
|
13
|
+
diagnostics: Diagnostic[];
|
|
14
|
+
continuations: Map<string, NonNullable<PlcProgram["continuation"]>>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const copyValue = (value: string, aggregate: boolean) => (aggregate ? `copy(${value})` : value);
|
|
18
|
+
|
|
19
|
+
/** Lower suspension into ordinary typed blocks before the PLC semantic pass. */
|
|
20
|
+
export const lowerAsync = (program: ts.Program, sdk: string): AsyncLowering => {
|
|
21
|
+
const result: AsyncLowering = {
|
|
22
|
+
sources: new Map(),
|
|
23
|
+
blocks: new Map(),
|
|
24
|
+
programs: {},
|
|
25
|
+
schedules: new Map(),
|
|
26
|
+
diagnostics: [],
|
|
27
|
+
continuations: new Map(),
|
|
28
|
+
};
|
|
29
|
+
const checker = program.getTypeChecker();
|
|
30
|
+
const symbol = (node: ts.Node): ts.Symbol | undefined => {
|
|
31
|
+
let s = checker.getSymbolAtLocation(node);
|
|
32
|
+
while (s && s.flags & ts.SymbolFlags.Alias) s = checker.getAliasedSymbol(s);
|
|
33
|
+
return s;
|
|
34
|
+
};
|
|
35
|
+
const official = (node: ts.Node, name: string) => {
|
|
36
|
+
const s = symbol(node);
|
|
37
|
+
return (
|
|
38
|
+
s?.name === name &&
|
|
39
|
+
s.declarations?.some((d) => d.getSourceFile().fileName.startsWith(dirname(sdk) + "/"))
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
const diagnosticFor = (node: ts.Node, message: string): Diagnostic => {
|
|
43
|
+
const source = node.getSourceFile();
|
|
44
|
+
const start = node.getStart(source);
|
|
45
|
+
const position = source.getLineAndCharacterOfPosition(start);
|
|
46
|
+
return error("PLC1010", message, {
|
|
47
|
+
file: source.fileName,
|
|
48
|
+
start,
|
|
49
|
+
end: node.end,
|
|
50
|
+
line: position.line + 1,
|
|
51
|
+
column: position.character + 1,
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
const fail: (node: ts.Node, message: string) => never = (node, message) => {
|
|
55
|
+
throw { asyncDiagnostic: diagnosticFor(node, message) };
|
|
56
|
+
};
|
|
57
|
+
type Routine = ts.FunctionDeclaration | ts.MethodDeclaration;
|
|
58
|
+
const functions = new Map<ts.Symbol, Routine>();
|
|
59
|
+
const methods = new Map<
|
|
60
|
+
ts.Symbol,
|
|
61
|
+
{ start: string; reset: string; snapshot: string; advance: string }
|
|
62
|
+
>();
|
|
63
|
+
for (const source of runtimeSources(program)) {
|
|
64
|
+
if (source.isDeclarationFile) continue;
|
|
65
|
+
for (const statement of source.statements) {
|
|
66
|
+
const routines = ts.isClassDeclaration(statement)
|
|
67
|
+
? statement.members.filter(ts.isMethodDeclaration)
|
|
68
|
+
: [statement];
|
|
69
|
+
for (const fn of routines) {
|
|
70
|
+
if (
|
|
71
|
+
(!ts.isFunctionDeclaration(fn) && !ts.isMethodDeclaration(fn)) ||
|
|
72
|
+
!fn.name ||
|
|
73
|
+
!fn.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)
|
|
74
|
+
)
|
|
75
|
+
continue;
|
|
76
|
+
functions.set(symbol(fn.name)!, fn);
|
|
77
|
+
if (ts.isMethodDeclaration(fn)) {
|
|
78
|
+
const prefix = `tsplcAsync${fn.name.getText(source)}${fn.getStart(source)}`;
|
|
79
|
+
methods.set(symbol(fn.name)!, {
|
|
80
|
+
start: prefix + "Start",
|
|
81
|
+
reset: prefix + "Reset",
|
|
82
|
+
snapshot: prefix + "Snapshot",
|
|
83
|
+
advance: prefix + "Advance",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!functions.size) return result;
|
|
90
|
+
const aliasPrefix = (fn: Routine) =>
|
|
91
|
+
`TsplcAsync${fn.name!.getText(fn.getSourceFile())}${fn.getStart(fn.getSourceFile())}`;
|
|
92
|
+
const printer = ts.createPrinter({ removeComments: false });
|
|
93
|
+
let sequence = 0;
|
|
94
|
+
const ownedContinuations = new Map<ts.Symbol, string[]>();
|
|
95
|
+
const ownedJobs = new Map<
|
|
96
|
+
ts.Symbol,
|
|
97
|
+
{ method: string; schedule: NonNullable<PlcProgram["background"]> }[]
|
|
98
|
+
>();
|
|
99
|
+
for (const source of runtimeSources(program)) {
|
|
100
|
+
if (source.isDeclarationFile) continue;
|
|
101
|
+
try {
|
|
102
|
+
const replacements: { start: number; end: number; text: string }[] = [],
|
|
103
|
+
extra: string[] = [];
|
|
104
|
+
const text = (node: ts.Node) => node.getText(source);
|
|
105
|
+
for (const fn of functions.values()) {
|
|
106
|
+
if (fn.getSourceFile() !== source) continue;
|
|
107
|
+
const method = methods.get(symbol(fn.name!)!);
|
|
108
|
+
if (!fn.name || !functions.has(symbol(fn.name)!)) continue;
|
|
109
|
+
if (
|
|
110
|
+
!fn.body ||
|
|
111
|
+
fn.typeParameters?.length ||
|
|
112
|
+
!fn.type ||
|
|
113
|
+
!ts.isTypeReferenceNode(fn.type) ||
|
|
114
|
+
fn.type.typeName.getText(source) !== "Promise" ||
|
|
115
|
+
fn.type.typeArguments?.length !== 1
|
|
116
|
+
)
|
|
117
|
+
fail(
|
|
118
|
+
fn,
|
|
119
|
+
"Async functions require concrete parameters and an explicit Promise<T> result.",
|
|
120
|
+
);
|
|
121
|
+
const returnsVoid = fn.type.typeArguments[0]!.kind === ts.SyntaxKind.VoidKeyword;
|
|
122
|
+
const returnType = returnsVoid ? "BOOL" : text(fn.type.typeArguments[0]!);
|
|
123
|
+
const resultAggregate = Boolean(
|
|
124
|
+
checker.getTypeFromTypeNode(fn.type.typeArguments[0]!).flags & ts.TypeFlags.Object,
|
|
125
|
+
);
|
|
126
|
+
const fields = new Map<ts.Symbol, { name: string; type: string; aggregate: boolean }>();
|
|
127
|
+
const parameters = fn.parameters.map((p, index) => {
|
|
128
|
+
if (
|
|
129
|
+
!ts.isIdentifier(p.name) ||
|
|
130
|
+
!p.type ||
|
|
131
|
+
p.initializer ||
|
|
132
|
+
p.questionToken ||
|
|
133
|
+
p.dotDotDotToken
|
|
134
|
+
)
|
|
135
|
+
fail(p, "Async parameters require explicit concrete value types.");
|
|
136
|
+
if (/\bInOut\b/.test(text(p.type)))
|
|
137
|
+
fail(p, "InOut cannot cross an async suspension; pass a value snapshot.");
|
|
138
|
+
fields.set(symbol(p.name)!, {
|
|
139
|
+
name: `argument${index}`,
|
|
140
|
+
type: text(p.type),
|
|
141
|
+
aggregate: Boolean(checker.getTypeFromTypeNode(p.type).flags & ts.TypeFlags.Object),
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
name: p.name.text,
|
|
145
|
+
type: text(p.type),
|
|
146
|
+
aggregate: Boolean(checker.getTypeFromTypeNode(p.type).flags & ts.TypeFlags.Object),
|
|
147
|
+
field: `argument${index}`,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
const containsAwait = (node: ts.Node): boolean =>
|
|
151
|
+
ts.isAwaitExpression(node) || node.getChildren(source).some(containsAwait);
|
|
152
|
+
const collect = (node: ts.Node) => {
|
|
153
|
+
if (ts.isForStatement(node)) {
|
|
154
|
+
if (containsAwait(node))
|
|
155
|
+
fail(node, "Await inside loops is not supported yet; use bounded async calls.");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (ts.isFunctionLike(node) && node !== fn)
|
|
159
|
+
fail(node, "Closures and nested functions are not supported in async PLC routines.");
|
|
160
|
+
if (ts.isCatchClause(node)) {
|
|
161
|
+
const declaration = node.variableDeclaration;
|
|
162
|
+
if (declaration) {
|
|
163
|
+
if (!ts.isIdentifier(declaration.name))
|
|
164
|
+
fail(declaration, "Catch requires a named error code or no binding.");
|
|
165
|
+
fields.set(symbol(declaration.name)!, {
|
|
166
|
+
name: `caught${fields.size}`,
|
|
167
|
+
type: "UDINT",
|
|
168
|
+
aggregate: false,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
collect(node.block);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (ts.isVariableDeclaration(node)) {
|
|
175
|
+
if (!ts.isIdentifier(node.name) || !node.initializer)
|
|
176
|
+
fail(node, "Async locals need names and initializers.");
|
|
177
|
+
let type = node.type ? text(node.type) : undefined;
|
|
178
|
+
if (!type) {
|
|
179
|
+
if (
|
|
180
|
+
node.initializer.kind === ts.SyntaxKind.TrueKeyword ||
|
|
181
|
+
node.initializer.kind === ts.SyntaxKind.FalseKeyword
|
|
182
|
+
)
|
|
183
|
+
type = "BOOL";
|
|
184
|
+
else if (
|
|
185
|
+
ts.isAwaitExpression(node.initializer) &&
|
|
186
|
+
ts.isCallExpression(node.initializer.expression)
|
|
187
|
+
) {
|
|
188
|
+
const call = node.initializer.expression;
|
|
189
|
+
const target = ts.isPropertyAccessExpression(call.expression)
|
|
190
|
+
? call.expression.name
|
|
191
|
+
: call.expression;
|
|
192
|
+
const called = functions.get(symbol(target)!);
|
|
193
|
+
if (called?.type && ts.isTypeReferenceNode(called.type)) {
|
|
194
|
+
type = called.type.typeArguments![0]!.getText(called.getSourceFile());
|
|
195
|
+
if (called.getSourceFile() !== source) {
|
|
196
|
+
type = `${aliasPrefix(called)}Result`;
|
|
197
|
+
const path = relative(
|
|
198
|
+
dirname(source.fileName),
|
|
199
|
+
called.getSourceFile().fileName,
|
|
200
|
+
).replaceAll("\\", "/");
|
|
201
|
+
const statement = `import type {${type}} from ${JSON.stringify(path.startsWith(".") ? path : `./${path}`)};`;
|
|
202
|
+
if (!extra.includes(statement)) extra.push(statement);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (!type) fail(node, "Annotate async locals with their concrete PLC type.");
|
|
207
|
+
}
|
|
208
|
+
fields.set(symbol(node.name)!, {
|
|
209
|
+
name: `local${fields.size}`,
|
|
210
|
+
type,
|
|
211
|
+
aggregate: Boolean(checker.getTypeAtLocation(node).flags & ts.TypeFlags.Object),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
ts.forEachChild(node, collect);
|
|
215
|
+
};
|
|
216
|
+
collect(fn.body);
|
|
217
|
+
const rewrite = (node: ts.Node): string => {
|
|
218
|
+
const transformed = ts.transform(node, [
|
|
219
|
+
(context) => (root) => {
|
|
220
|
+
const visit = (n: ts.Node): ts.VisitResult<ts.Node> => {
|
|
221
|
+
if (
|
|
222
|
+
method &&
|
|
223
|
+
ts.isPropertyAccessExpression(n) &&
|
|
224
|
+
n.expression.kind === ts.SyntaxKind.ThisKeyword
|
|
225
|
+
) {
|
|
226
|
+
return ts.factory.createElementAccessExpression(
|
|
227
|
+
ts.factory.createThis(),
|
|
228
|
+
ts.factory.createStringLiteral(n.name.text),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
if (ts.isIdentifier(n)) {
|
|
232
|
+
const field = fields.get(symbol(n)!);
|
|
233
|
+
if (
|
|
234
|
+
field &&
|
|
235
|
+
!(ts.isPropertyAccessExpression(n.parent) && n.parent.name === n) &&
|
|
236
|
+
!(ts.isPropertyAssignment(n.parent) && n.parent.name === n)
|
|
237
|
+
)
|
|
238
|
+
return ts.factory.createPropertyAccessExpression(
|
|
239
|
+
ts.factory.createThis(),
|
|
240
|
+
field.name,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
if (ts.isShorthandPropertyAssignment(n)) {
|
|
244
|
+
const value = checker.getShorthandAssignmentValueSymbol(n),
|
|
245
|
+
field = value && fields.get(value);
|
|
246
|
+
if (field)
|
|
247
|
+
return ts.factory.createPropertyAssignment(
|
|
248
|
+
n.name,
|
|
249
|
+
ts.factory.createPropertyAccessExpression(
|
|
250
|
+
ts.factory.createThis(),
|
|
251
|
+
field.name,
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return ts.visitEachChild(n, visit, context);
|
|
256
|
+
};
|
|
257
|
+
return ts.visitNode(root, visit) as typeof root;
|
|
258
|
+
},
|
|
259
|
+
]);
|
|
260
|
+
const value = printer.printNode(
|
|
261
|
+
ts.EmitHint.Unspecified,
|
|
262
|
+
transformed.transformed[0]!,
|
|
263
|
+
source,
|
|
264
|
+
);
|
|
265
|
+
transformed.dispose();
|
|
266
|
+
return value;
|
|
267
|
+
};
|
|
268
|
+
const nodes = new Map<number, string>();
|
|
269
|
+
const yielding = new Set<number>();
|
|
270
|
+
let nextId = 1;
|
|
271
|
+
const state = (body: string, yields = false) => {
|
|
272
|
+
const id = nextId++;
|
|
273
|
+
nodes.set(id, body);
|
|
274
|
+
if (yields) yielding.add(id);
|
|
275
|
+
return id;
|
|
276
|
+
};
|
|
277
|
+
const children: string[] = [],
|
|
278
|
+
resets: string[] = [],
|
|
279
|
+
cancels: string[] = [];
|
|
280
|
+
const finish = (value: string) =>
|
|
281
|
+
`this.value = ${copyValue(value, resultAggregate)}; this.busy = false; this.done = true; this.step = 0;`;
|
|
282
|
+
const failure = (error: string) =>
|
|
283
|
+
`this.error = ${error}; if(this.error === 0)this.error=4; this.busy = false; this.done = true; this.step = 0;`;
|
|
284
|
+
const awaitState = (
|
|
285
|
+
awaited: ts.AwaitExpression | ts.CallExpression,
|
|
286
|
+
target: string | undefined,
|
|
287
|
+
next: number,
|
|
288
|
+
reject: (error: string, cancellable?: boolean) => string,
|
|
289
|
+
): number => {
|
|
290
|
+
const expression = ts.isAwaitExpression(awaited) ? awaited.expression : awaited;
|
|
291
|
+
if (!ts.isCallExpression(expression))
|
|
292
|
+
fail(awaited, "Await requires a tsPLC operation or a statically named async function.");
|
|
293
|
+
const call = expression,
|
|
294
|
+
name = ts.isPropertyAccessExpression(call.expression)
|
|
295
|
+
? call.expression.name
|
|
296
|
+
: call.expression;
|
|
297
|
+
const child = `child${children.length}`;
|
|
298
|
+
if (official(name, "sleep")) {
|
|
299
|
+
if (call.arguments.length !== 1 || call.typeArguments?.length || target)
|
|
300
|
+
fail(call, "sleep takes one TIME and returns void.");
|
|
301
|
+
children.push(`private ${child} = new OnDelayTimer();`);
|
|
302
|
+
resets.push(`this.${child}.update({enabled:false,duration:ms(0)});`);
|
|
303
|
+
const poll = state(
|
|
304
|
+
`this.${child}.update({enabled:true,duration:${rewrite(call.arguments[0]!)}}); if(this.${child}.out.done) { this.step = ${next}; }`,
|
|
305
|
+
);
|
|
306
|
+
return state(
|
|
307
|
+
`this.${child}.update({enabled:true,duration:${rewrite(call.arguments[0]!)}});this.step=${poll};`,
|
|
308
|
+
true,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (official(name, "fetch") || official(name, "opcuaCall")) {
|
|
312
|
+
const rpc = official(name, "opcuaCall");
|
|
313
|
+
if (
|
|
314
|
+
call.arguments.length < 1 ||
|
|
315
|
+
call.arguments.length > 2 ||
|
|
316
|
+
!call.typeArguments?.length ||
|
|
317
|
+
call.typeArguments.length > 2
|
|
318
|
+
)
|
|
319
|
+
fail(
|
|
320
|
+
call,
|
|
321
|
+
"fetch<Response, Request?> requires a URL and optional { method, json } options.",
|
|
322
|
+
);
|
|
323
|
+
const url = staticString(checker, call.arguments[0]!);
|
|
324
|
+
if (url === undefined)
|
|
325
|
+
fail(
|
|
326
|
+
call.arguments[0]!,
|
|
327
|
+
"fetch URL must be a literal or compile-time const string; configuration properties must be readonly.",
|
|
328
|
+
);
|
|
329
|
+
const options = rpc ? undefined : call.arguments[1];
|
|
330
|
+
if (options && !ts.isObjectLiteralExpression(options))
|
|
331
|
+
fail(options, "fetch options must be an inline { method, json } object.");
|
|
332
|
+
let method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" = rpc ? "POST" : "GET";
|
|
333
|
+
let json: ts.Expression | undefined = rpc ? call.arguments[1] : undefined;
|
|
334
|
+
if (options && ts.isObjectLiteralExpression(options))
|
|
335
|
+
for (const property of options.properties) {
|
|
336
|
+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name))
|
|
337
|
+
fail(property, "fetch options require named properties.");
|
|
338
|
+
if (property.name.text === "method") {
|
|
339
|
+
const value = staticString(checker, property.initializer);
|
|
340
|
+
if (
|
|
341
|
+
value !== "GET" &&
|
|
342
|
+
value !== "POST" &&
|
|
343
|
+
value !== "PUT" &&
|
|
344
|
+
value !== "PATCH" &&
|
|
345
|
+
value !== "DELETE"
|
|
346
|
+
)
|
|
347
|
+
fail(property, "Unsupported fetch method.");
|
|
348
|
+
method = value;
|
|
349
|
+
} else if (property.name.text === "json") json = property.initializer;
|
|
350
|
+
else fail(property, "Unsupported fetch option.");
|
|
351
|
+
}
|
|
352
|
+
if (method === "GET" && json) fail(call, "GET requests cannot contain json.");
|
|
353
|
+
const responseType = text(call.typeArguments[0]!),
|
|
354
|
+
httpName = `Tsplc${rpc ? "Rpc" : "Http"}${sequence++}`;
|
|
355
|
+
let requestType = `${httpName}Request`;
|
|
356
|
+
if (!json) extra.push(`interface ${requestType} {reserved:BOOL}`);
|
|
357
|
+
if (json) {
|
|
358
|
+
if (call.typeArguments[1]) requestType = text(call.typeArguments[1]);
|
|
359
|
+
else {
|
|
360
|
+
const layout = checker.getTypeAtLocation(json);
|
|
361
|
+
if (!layout.aliasSymbol && (!layout.symbol || layout.symbol.name === "__type"))
|
|
362
|
+
fail(
|
|
363
|
+
json,
|
|
364
|
+
"Annotate json with a named PLC record or supply the Request type argument.",
|
|
365
|
+
);
|
|
366
|
+
requestType = checker.typeToString(
|
|
367
|
+
layout,
|
|
368
|
+
json,
|
|
369
|
+
ts.TypeFormatFlags.NoTruncation |
|
|
370
|
+
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
extra.push(`interface ${httpName}Input {start:BOOL;cancel:BOOL;request:${requestType}}
|
|
375
|
+
interface ${httpName}Output {busy:BOOL;done:BOOL;error:UDINT;result:${responseType}}
|
|
376
|
+
class ${httpName} extends FunctionBlock<${httpName}Input,${httpName}Output> { protected execute(input:${httpName}Input):${httpName}Output {return {busy:false,done:false,error:0,result:defaultValue<${responseType}>()};} }`);
|
|
377
|
+
result.blocks.set(
|
|
378
|
+
`${source.fileName}:${httpName}`,
|
|
379
|
+
rpc
|
|
380
|
+
? { kind: "rpc", path: url }
|
|
381
|
+
: {
|
|
382
|
+
kind: "http",
|
|
383
|
+
url,
|
|
384
|
+
method,
|
|
385
|
+
json: Boolean(json),
|
|
386
|
+
},
|
|
387
|
+
);
|
|
388
|
+
children.push(`private ${child} = new ${httpName}();`);
|
|
389
|
+
resets.push(
|
|
390
|
+
`this.${child}.update({start:false,cancel:false,request:defaultValue<${requestType}>()});`,
|
|
391
|
+
);
|
|
392
|
+
cancels.push(
|
|
393
|
+
`this.${child}.update({start:false,cancel:true,request:defaultValue<${requestType}>()});`,
|
|
394
|
+
);
|
|
395
|
+
return state(
|
|
396
|
+
`this.${child}.update({start:true,cancel:false,request:${json ? rewrite(json) : `defaultValue<${requestType}>()`}}); if(this.${child}.out.done) { if(this.${child}.out.error !== 0) {${reject(`this.${child}.out.error`, true)}} else {${target ? `${target} = ${copyValue(`this.${child}.out.result`, Boolean(checker.getAwaitedType(checker.getTypeAtLocation(call))!.flags & ts.TypeFlags.Object))};` : ""}this.step = ${next};} }`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
const jobDeclaration = symbol(name)?.declarations?.find(
|
|
400
|
+
(node) => ts.isVariableDeclaration(node) || ts.isPropertyDeclaration(node),
|
|
401
|
+
);
|
|
402
|
+
const jobInitializer =
|
|
403
|
+
jobDeclaration &&
|
|
404
|
+
(ts.isVariableDeclaration(jobDeclaration) || ts.isPropertyDeclaration(jobDeclaration))
|
|
405
|
+
? jobDeclaration.initializer
|
|
406
|
+
: undefined;
|
|
407
|
+
if (
|
|
408
|
+
jobInitializer &&
|
|
409
|
+
ts.isCallExpression(jobInitializer) &&
|
|
410
|
+
ts.isPropertyAccessExpression(jobInitializer.expression) &&
|
|
411
|
+
official(jobInitializer.expression.expression, "backgroundTasks") &&
|
|
412
|
+
jobInitializer.expression.name.text === "define"
|
|
413
|
+
) {
|
|
414
|
+
const receiver = rewrite(call.expression),
|
|
415
|
+
borrow = `borrow${children.length}`;
|
|
416
|
+
children.push(`private ${borrow}:BOOL=false;`);
|
|
417
|
+
resets.push(`this.${borrow}=false;`);
|
|
418
|
+
cancels.push(
|
|
419
|
+
`if(this.${borrow}){${receiver}.cancel();const borrowed=${receiver}.snapshot();if(!borrowed.valid||borrowed.busy)return;if(!${receiver}.reset())return;this.${borrow}=false;}`,
|
|
420
|
+
);
|
|
421
|
+
const poll = state(
|
|
422
|
+
`const borrowed=${receiver}.snapshot();if(borrowed.valid&&borrowed.done){if(${receiver}.reset()){this.${borrow}=false;if(borrowed.error!==0){${reject("borrowed.error", true)}}else{${target ? `${target}=${copyValue("borrowed.result", Boolean(checker.getAwaitedType(checker.getTypeAtLocation(call))!.flags & ts.TypeFlags.Object))};` : ""}this.step=${next};}}}`,
|
|
423
|
+
);
|
|
424
|
+
return state(
|
|
425
|
+
`const available=${receiver}.snapshot();if(!available.valid)return;if(available.busy||available.done){${reject("7")}}else{const accepted:BOOL=${receiver}.start(${call.arguments.map(rewrite).join(",")});if(accepted){this.${borrow}=true;this.step=${poll};}}`,
|
|
426
|
+
true,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const called = functions.get(symbol(name)!);
|
|
430
|
+
if (!called) fail(call, "Await requires a known async helper or SDK operation.");
|
|
431
|
+
const calledMethod = methods.get(symbol(name)!);
|
|
432
|
+
if (calledMethod) {
|
|
433
|
+
if (!ts.isPropertyAccessExpression(call.expression))
|
|
434
|
+
fail(call, "Async methods require a static receiver.");
|
|
435
|
+
const receiver = rewrite(call.expression.expression);
|
|
436
|
+
const borrow = `methodBorrow${children.length}`;
|
|
437
|
+
children.push(`private ${borrow}:BOOL=false;`);
|
|
438
|
+
resets.push(
|
|
439
|
+
`if(this.${borrow}){${receiver}.${calledMethod.reset}();this.${borrow}=false;}`,
|
|
440
|
+
);
|
|
441
|
+
cancels.push(
|
|
442
|
+
`if(this.${borrow}){${receiver}.${calledMethod.advance}({cancel:true});const cancellingChild=${receiver}.${calledMethod.snapshot}();if(cancellingChild.busy)return;${receiver}.${calledMethod.reset}();this.${borrow}=false;}`,
|
|
443
|
+
);
|
|
444
|
+
const poll = state(
|
|
445
|
+
`${receiver}.${calledMethod.advance}({cancel:false});const childState=${receiver}.${calledMethod.snapshot}();if(childState.done){${receiver}.${calledMethod.reset}();this.${borrow}=false;if(childState.error!==0){${reject("childState.error", true)}}else{${target ? `${target}=${copyValue("childState.result", Boolean(checker.getAwaitedType(checker.getTypeAtLocation(call))!.flags & ts.TypeFlags.Object))};` : ""}this.step=${next};}}`,
|
|
446
|
+
);
|
|
447
|
+
return state(
|
|
448
|
+
`const accepted:BOOL=${receiver}.${calledMethod.start}(${call.arguments.map(rewrite).join(",")});if(!accepted){${reject("7")}}else{this.${borrow}=true;${receiver}.${calledMethod.advance}({cancel:false});this.step=${poll};}`,
|
|
449
|
+
true,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
if (!ts.isIdentifier(call.expression))
|
|
453
|
+
fail(call, "Async helpers require a static function.");
|
|
454
|
+
children.push(`private ${child} = new ${text(call.expression)}();`);
|
|
455
|
+
resets.push(`this.${child}.reset();`);
|
|
456
|
+
cancels.push(
|
|
457
|
+
`this.${child}.update({cancel:true});const cancellingChild=this.${child}.snapshot();if(cancellingChild.busy)return;`,
|
|
458
|
+
);
|
|
459
|
+
const poll = state(
|
|
460
|
+
`this.${child}.update({cancel:false}); const childState = this.${child}.snapshot(); if(childState.done) {if(childState.error !== 0) {${reject("childState.error", true)}} else {${target ? `${target} = ${copyValue("childState.result", Boolean(checker.getAwaitedType(checker.getTypeAtLocation(call))!.flags & ts.TypeFlags.Object))};` : ""}this.step = ${next};} }`,
|
|
461
|
+
);
|
|
462
|
+
return state(
|
|
463
|
+
`const accepted:BOOL = this.${child}.start(${call.arguments.map(rewrite).join(",")}); if(accepted){this.${child}.update({cancel:false});this.step = ${poll};}`,
|
|
464
|
+
true,
|
|
465
|
+
);
|
|
466
|
+
};
|
|
467
|
+
const block = (
|
|
468
|
+
statements: readonly ts.Statement[],
|
|
469
|
+
next: number,
|
|
470
|
+
reject: (error: string, cancellable?: boolean) => string = failure,
|
|
471
|
+
): number => {
|
|
472
|
+
let entry = next;
|
|
473
|
+
for (const stmt of [...statements].reverse()) {
|
|
474
|
+
if (ts.isBlock(stmt)) {
|
|
475
|
+
entry = block(stmt.statements, entry, reject);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (ts.isIfStatement(stmt)) {
|
|
479
|
+
const yes = block([stmt.thenStatement], entry, reject),
|
|
480
|
+
no = stmt.elseStatement ? block([stmt.elseStatement], entry, reject) : entry;
|
|
481
|
+
entry = state(
|
|
482
|
+
`if(${rewrite(stmt.expression)}) this.step = ${yes}; else this.step = ${no};`,
|
|
483
|
+
);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (ts.isTryStatement(stmt)) {
|
|
487
|
+
if (!stmt.catchClause || stmt.finallyBlock)
|
|
488
|
+
fail(stmt, "Async try requires catch; finally is not supported yet.");
|
|
489
|
+
const caught = block(stmt.catchClause.block.statements, entry, reject);
|
|
490
|
+
const declaration = stmt.catchClause.variableDeclaration;
|
|
491
|
+
const field = declaration && fields.get(symbol(declaration.name)!);
|
|
492
|
+
entry = block(stmt.tryBlock.statements, entry, (error, cancellable) => {
|
|
493
|
+
const recover = `${field ? `this.${field.name} = ${error};` : ""}this.step = ${caught};`;
|
|
494
|
+
return cancellable
|
|
495
|
+
? `if(${error} === 1) {${failure(error)}} else {${recover}}`
|
|
496
|
+
: recover;
|
|
497
|
+
});
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (ts.isThrowStatement(stmt)) {
|
|
501
|
+
if (
|
|
502
|
+
!checker.isTypeAssignableTo(
|
|
503
|
+
checker.getTypeAtLocation(stmt.expression),
|
|
504
|
+
checker.getNumberType(),
|
|
505
|
+
)
|
|
506
|
+
)
|
|
507
|
+
fail(stmt, "Async throw requires a numeric error code.");
|
|
508
|
+
entry = state(reject(rewrite(stmt.expression)));
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (ts.isReturnStatement(stmt)) {
|
|
512
|
+
if (!stmt.expression) {
|
|
513
|
+
if (!returnsVoid) fail(stmt, "Async routines must return a value.");
|
|
514
|
+
entry = state(finish("true"));
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
const adopted =
|
|
518
|
+
ts.isCallExpression(stmt.expression) &&
|
|
519
|
+
checker.getAwaitedType(checker.getTypeAtLocation(stmt.expression)) !==
|
|
520
|
+
checker.getTypeAtLocation(stmt.expression)
|
|
521
|
+
? stmt.expression
|
|
522
|
+
: undefined;
|
|
523
|
+
const suspension = ts.isAwaitExpression(stmt.expression) ? stmt.expression : adopted;
|
|
524
|
+
entry = suspension
|
|
525
|
+
? awaitState(
|
|
526
|
+
suspension,
|
|
527
|
+
returnsVoid ? undefined : "this.value",
|
|
528
|
+
state(finish(returnsVoid ? "true" : "this.value")),
|
|
529
|
+
reject,
|
|
530
|
+
)
|
|
531
|
+
: state(finish(rewrite(stmt.expression)));
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (ts.isVariableStatement(stmt)) {
|
|
535
|
+
for (const dec of [...stmt.declarationList.declarations].reverse()) {
|
|
536
|
+
const field = fields.get(symbol(dec.name)!);
|
|
537
|
+
if (!field || !dec.initializer) fail(dec, "Invalid async local.");
|
|
538
|
+
const target = `this.${field.name}`;
|
|
539
|
+
entry = ts.isAwaitExpression(dec.initializer)
|
|
540
|
+
? awaitState(dec.initializer, target, entry, reject)
|
|
541
|
+
: state(
|
|
542
|
+
`${target} = ${copyValue(rewrite(dec.initializer), field.aggregate)}; this.step = ${entry};`,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (ts.isExpressionStatement(stmt) && ts.isAwaitExpression(stmt.expression)) {
|
|
548
|
+
entry = awaitState(stmt.expression, undefined, entry, reject);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (containsAwait(stmt))
|
|
552
|
+
fail(
|
|
553
|
+
stmt,
|
|
554
|
+
"This suspension position is not supported; await into a named local or return.",
|
|
555
|
+
);
|
|
556
|
+
if (ts.isWhileStatement(stmt) || ts.isDoStatement(stmt))
|
|
557
|
+
fail(stmt, "Unbounded loops are not supported in async routines.");
|
|
558
|
+
entry = state(`${rewrite(stmt)} this.step = ${entry};`);
|
|
559
|
+
}
|
|
560
|
+
return entry;
|
|
561
|
+
};
|
|
562
|
+
const entry = block(fn.body.statements, state(returnsVoid ? finish("true") : failure("4")));
|
|
563
|
+
const exported = fn.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
|
|
564
|
+
? "export "
|
|
565
|
+
: "";
|
|
566
|
+
const aliases = `export type ${aliasPrefix(fn)}Result = ${returnType};\n${parameters.map((p, i) => `export type ${aliasPrefix(fn)}Argument${i} = ${p.type};`).join("\n")}`;
|
|
567
|
+
const generated = `${aliases}\n${exported}class ${fn.name!.getText(source)} extends FunctionBlock<{cancel:BOOL}> {
|
|
568
|
+
private step:UINT=0; private cancelling:BOOL=false; private busy:BOOL=false; private done:BOOL=false; private error:UDINT=0; private value:${returnType}=defaultValue<${returnType}>();
|
|
569
|
+
${[...fields.values()].map((f) => `private ${f.name}:${f.type}=defaultValue<${f.type}>();`).join("\n")}
|
|
570
|
+
${children.join("\n")}
|
|
571
|
+
start(${parameters.map((p) => `${p.name}:${p.type}`).join(",")}):BOOL {if(this.busy || this.done)return false; ${parameters.map((p) => `this.${p.field} = ${copyValue(p.name, p.aggregate)};`).join(" ")} ${resets.join(" ")} this.value = defaultValue<${returnType}>(); this.error=0;this.cancelling=false;this.busy=true;this.done=false;this.step=${entry};return true;}
|
|
572
|
+
reset():void {${resets.join(" ")} this.busy=false;this.cancelling=false;this.done=false;this.error=0;this.step=0;}
|
|
573
|
+
snapshot():{busy:BOOL;done:BOOL;error:UDINT;result:${returnType}} {return {busy:this.busy,done:this.done,error:this.error,result:${copyValue("this.value", resultAggregate)}};}
|
|
574
|
+
protected execute(input:{cancel:BOOL}):void {if(!this.busy)return;if(input.cancel)this.cancelling=true;if(this.cancelling){${cancels.map((cancel) => `{${cancel}}`).join(" ")}${failure("1")}return;}for(let transition:DINT=0;transition<${nodes.size + 1};transition++){const previous:UINT=this.step;switch(this.step){${[...nodes].map(([id, body]) => `case ${id}: {${body}} break;`).join("\n")} }if(!this.busy||this.step===previous${[...yielding].map((id) => `||previous===${id}`).join("")})return;}}
|
|
575
|
+
|
|
576
|
+
}`;
|
|
577
|
+
if (!method)
|
|
578
|
+
replacements.push({ start: fn.getStart(source), end: fn.end, text: generated });
|
|
579
|
+
else {
|
|
580
|
+
const parsed = ts.createSourceFile(
|
|
581
|
+
source.fileName,
|
|
582
|
+
generated,
|
|
583
|
+
ts.ScriptTarget.Latest,
|
|
584
|
+
true,
|
|
585
|
+
);
|
|
586
|
+
const worker = parsed.statements.find(ts.isClassDeclaration)!;
|
|
587
|
+
const names = new Map(
|
|
588
|
+
worker.members.map((member) => [
|
|
589
|
+
member.name!.getText(parsed),
|
|
590
|
+
`tsplcAsync${fn.getStart(source)}_${member.name!.getText(parsed)}`,
|
|
591
|
+
]),
|
|
592
|
+
);
|
|
593
|
+
names.set("start", method.start);
|
|
594
|
+
names.set("reset", method.reset);
|
|
595
|
+
names.set("snapshot", method.snapshot);
|
|
596
|
+
names.set("execute", method.advance);
|
|
597
|
+
const transformed = ts.transform(worker, [
|
|
598
|
+
(context) => (root) => {
|
|
599
|
+
const visit = (node: ts.Node): ts.VisitResult<ts.Node> => {
|
|
600
|
+
if (
|
|
601
|
+
ts.isElementAccessExpression(node) &&
|
|
602
|
+
node.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
603
|
+
ts.isStringLiteral(node.argumentExpression)
|
|
604
|
+
)
|
|
605
|
+
return ts.factory.createPropertyAccessExpression(
|
|
606
|
+
ts.factory.createThis(),
|
|
607
|
+
node.argumentExpression.text,
|
|
608
|
+
);
|
|
609
|
+
if (
|
|
610
|
+
ts.isPropertyAccessExpression(node) &&
|
|
611
|
+
node.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
612
|
+
names.has(node.name.text)
|
|
613
|
+
)
|
|
614
|
+
return ts.factory.createPropertyAccessExpression(
|
|
615
|
+
ts.factory.createThis(),
|
|
616
|
+
names.get(node.name.text)!,
|
|
617
|
+
);
|
|
618
|
+
if (
|
|
619
|
+
(ts.isPropertyDeclaration(node) || ts.isMethodDeclaration(node)) &&
|
|
620
|
+
ts.isIdentifier(node.name) &&
|
|
621
|
+
names.has(node.name.text)
|
|
622
|
+
) {
|
|
623
|
+
const updated = ts.visitEachChild(node, visit, context);
|
|
624
|
+
const name = ts.factory.createIdentifier(names.get(node.name.text)!);
|
|
625
|
+
if (ts.isPropertyDeclaration(updated))
|
|
626
|
+
return ts.factory.updatePropertyDeclaration(
|
|
627
|
+
updated,
|
|
628
|
+
updated.modifiers,
|
|
629
|
+
name,
|
|
630
|
+
updated.questionToken ?? updated.exclamationToken,
|
|
631
|
+
updated.type,
|
|
632
|
+
updated.initializer,
|
|
633
|
+
);
|
|
634
|
+
if (ts.isMethodDeclaration(updated))
|
|
635
|
+
return ts.factory.updateMethodDeclaration(
|
|
636
|
+
updated,
|
|
637
|
+
[ts.factory.createModifier(ts.SyntaxKind.PublicKeyword)],
|
|
638
|
+
updated.asteriskToken,
|
|
639
|
+
name,
|
|
640
|
+
updated.questionToken,
|
|
641
|
+
updated.typeParameters,
|
|
642
|
+
updated.parameters,
|
|
643
|
+
updated.type,
|
|
644
|
+
updated.body,
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
return ts.visitEachChild(node, visit, context);
|
|
648
|
+
};
|
|
649
|
+
return ts.visitNode(root, visit) as typeof root;
|
|
650
|
+
},
|
|
651
|
+
]);
|
|
652
|
+
const generatedClass = transformed.transformed[0]!;
|
|
653
|
+
replacements.push({
|
|
654
|
+
start: fn.getStart(source),
|
|
655
|
+
end: fn.end,
|
|
656
|
+
text: generatedClass.members
|
|
657
|
+
.map((member) => printer.printNode(ts.EmitHint.Unspecified, member, parsed))
|
|
658
|
+
.join("\n"),
|
|
659
|
+
});
|
|
660
|
+
transformed.dispose();
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const workTypes = (fn: Routine, dec: ts.VariableDeclaration | ts.PropertyDeclaration) => {
|
|
664
|
+
const reference = fn.type as ts.TypeReferenceNode;
|
|
665
|
+
const returnType =
|
|
666
|
+
reference.typeArguments![0]!.kind === ts.SyntaxKind.VoidKeyword
|
|
667
|
+
? "BOOL"
|
|
668
|
+
: reference.typeArguments![0]!.getText(fn.getSourceFile());
|
|
669
|
+
// Types in the job module must be visible: use the original inferred Promise result annotation at the definition boundary.
|
|
670
|
+
const signature = checker.getSignatureFromDeclaration(fn)!;
|
|
671
|
+
const promised = checker.getAwaitedType(checker.getReturnTypeOfSignature(signature))!;
|
|
672
|
+
const resultType =
|
|
673
|
+
promised.flags & ts.TypeFlags.Void
|
|
674
|
+
? "BOOL"
|
|
675
|
+
: checker.typeToString(
|
|
676
|
+
promised,
|
|
677
|
+
dec,
|
|
678
|
+
ts.TypeFormatFlags.NoTruncation |
|
|
679
|
+
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope,
|
|
680
|
+
);
|
|
681
|
+
const params = fn.parameters.map((p, i) => ({
|
|
682
|
+
field: `argument${i}`,
|
|
683
|
+
name: `argument${i}`,
|
|
684
|
+
aggregate: Boolean(checker.getTypeAtLocation(p).flags & ts.TypeFlags.Object),
|
|
685
|
+
type: checker.typeToString(
|
|
686
|
+
checker.getTypeAtLocation(p),
|
|
687
|
+
dec,
|
|
688
|
+
ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope,
|
|
689
|
+
),
|
|
690
|
+
}));
|
|
691
|
+
// Preserve PLC widths erased by the TS checker when declarations share the module.
|
|
692
|
+
for (let i = 0; i < params.length; i++)
|
|
693
|
+
if (fn.getSourceFile() === source)
|
|
694
|
+
params[i]!.type = fn.parameters[i]!.type!.getText(source);
|
|
695
|
+
let valueType = fn.getSourceFile() === source ? returnType : resultType;
|
|
696
|
+
if (fn.getSourceFile() !== source) {
|
|
697
|
+
const prefix = aliasPrefix(fn);
|
|
698
|
+
valueType = `${prefix}Result`;
|
|
699
|
+
for (let i = 0; i < params.length; i++) params[i]!.type = `${prefix}Argument${i}`;
|
|
700
|
+
const path = relative(dirname(source.fileName), fn.getSourceFile().fileName).replaceAll(
|
|
701
|
+
"\\",
|
|
702
|
+
"/",
|
|
703
|
+
);
|
|
704
|
+
extra.push(
|
|
705
|
+
`import type {${valueType},${params.map((p) => p.type).join(",")}} from ${JSON.stringify(path.startsWith(".") ? path : `./${path}`)};`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
const valueAggregate = Boolean(promised.flags & ts.TypeFlags.Object);
|
|
709
|
+
return { params, valueType, valueAggregate };
|
|
710
|
+
};
|
|
711
|
+
const definitions = source.statements.flatMap<
|
|
712
|
+
ts.VariableDeclaration | ts.PropertyDeclaration
|
|
713
|
+
>((stmt) => {
|
|
714
|
+
if (ts.isVariableStatement(stmt)) return [...stmt.declarationList.declarations];
|
|
715
|
+
if (ts.isClassDeclaration(stmt)) return stmt.members.filter(ts.isPropertyDeclaration);
|
|
716
|
+
return [];
|
|
717
|
+
});
|
|
718
|
+
for (const dec of definitions) {
|
|
719
|
+
const owner = ts.isPropertyDeclaration(dec) ? dec.parent : undefined;
|
|
720
|
+
const init = dec.initializer;
|
|
721
|
+
if (
|
|
722
|
+
!init ||
|
|
723
|
+
!ts.isCallExpression(init) ||
|
|
724
|
+
!ts.isPropertyAccessExpression(init.expression) ||
|
|
725
|
+
(!official(init.expression.expression, "backgroundTasks") &&
|
|
726
|
+
!official(init.expression.expression, "cooperativeTasks") &&
|
|
727
|
+
!official(init.expression.expression, "tasks")) ||
|
|
728
|
+
init.expression.name.text !== "define"
|
|
729
|
+
)
|
|
730
|
+
continue;
|
|
731
|
+
if (official(init.expression.expression, "tasks")) {
|
|
732
|
+
const fn = functions.get(symbol(init.arguments[0] ?? init)!);
|
|
733
|
+
if (!ts.isIdentifier(dec.name) || init.arguments.length !== 1 || !fn?.type)
|
|
734
|
+
fail(init, "tasks.define requires one named async function with a return type.");
|
|
735
|
+
const { params, valueType, valueAggregate } = workTypes(fn, dec);
|
|
736
|
+
const worker = `tsplcTaskWorker${sequence++}`;
|
|
737
|
+
const receiver = `this.${worker}`;
|
|
738
|
+
const site = taskSite(
|
|
739
|
+
dec.name.text,
|
|
740
|
+
params,
|
|
741
|
+
{
|
|
742
|
+
start: `${receiver}.start`,
|
|
743
|
+
advance: `${receiver}.update`,
|
|
744
|
+
snapshot: `${receiver}.snapshot`,
|
|
745
|
+
reset: `${receiver}.reset`,
|
|
746
|
+
declaration: `private ${worker}=new ${init.arguments[0]!.getText(source)}();`,
|
|
747
|
+
},
|
|
748
|
+
Boolean(owner),
|
|
749
|
+
{ type: valueType, aggregate: valueAggregate },
|
|
750
|
+
);
|
|
751
|
+
if (owner) {
|
|
752
|
+
if (!owner.name) fail(owner, "Task owners require a named block.");
|
|
753
|
+
replacements.push({ start: dec.getStart(source), end: dec.end, text: site.body });
|
|
754
|
+
ownedContinuations.set(symbol(owner.name)!, [
|
|
755
|
+
...(ownedContinuations.get(symbol(owner.name)!) ?? []),
|
|
756
|
+
site.poll,
|
|
757
|
+
]);
|
|
758
|
+
const visit = (node: ts.Node): void => {
|
|
759
|
+
if (
|
|
760
|
+
ts.isCallExpression(node) &&
|
|
761
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
762
|
+
ts.isPropertyAccessExpression(node.expression.expression) &&
|
|
763
|
+
node.expression.expression.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
764
|
+
symbol(node.expression.expression.name) === symbol(dec.name)
|
|
765
|
+
) {
|
|
766
|
+
const renamed = site.renames.get(node.expression.name.text);
|
|
767
|
+
if (renamed)
|
|
768
|
+
replacements.push({
|
|
769
|
+
start: node.expression.getStart(source),
|
|
770
|
+
end: node.expression.end,
|
|
771
|
+
text: `this.${renamed}`,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
ts.forEachChild(node, visit);
|
|
775
|
+
};
|
|
776
|
+
visit(owner);
|
|
777
|
+
} else {
|
|
778
|
+
const name = `TsplcTaskSite${sequence++}`,
|
|
779
|
+
entry = `tsplcContinuation${sequence++}`;
|
|
780
|
+
extra.push(`class ${name} extends FunctionBlock<{}>{${site.body}protected execute(input:{}):void{}}
|
|
781
|
+
export function ${entry}():void{${dec.name.text}.poll();}`);
|
|
782
|
+
replacements.push({
|
|
783
|
+
start: init.getStart(source),
|
|
784
|
+
end: init.end,
|
|
785
|
+
text: `new ${name}()`,
|
|
786
|
+
});
|
|
787
|
+
result.programs[entry] = { module: resolve(source.fileName), export: entry };
|
|
788
|
+
result.continuations.set(entry, { instance: dec.name.text, file: source.fileName });
|
|
789
|
+
}
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
const cooperative = official(init.expression.expression, "cooperativeTasks");
|
|
793
|
+
if (cooperative) {
|
|
794
|
+
if (!ts.isIdentifier(dec.name) || init.arguments.length !== 1)
|
|
795
|
+
fail(init, "Invalid compiler-owned continuation slot.");
|
|
796
|
+
const workNode = init.arguments[0]!;
|
|
797
|
+
const work = functions.get(
|
|
798
|
+
symbol(ts.isPropertyAccessExpression(workNode) ? workNode.name : workNode)!,
|
|
799
|
+
);
|
|
800
|
+
if (!work) fail(workNode, "A continuation requires a named async routine.");
|
|
801
|
+
const params = work.parameters.map((parameter, index) => ({
|
|
802
|
+
name: `argument${index}`,
|
|
803
|
+
type: parameter.type!.getText(work.getSourceFile()),
|
|
804
|
+
aggregate: Boolean(checker.getTypeAtLocation(parameter).flags & ts.TypeFlags.Object),
|
|
805
|
+
}));
|
|
806
|
+
const workMethod = methods.get(symbol(work.name!)!);
|
|
807
|
+
const worker = `tsplcCooperativeWorker${sequence++}`;
|
|
808
|
+
const receiver = workMethod ? "this" : `this.${worker}`;
|
|
809
|
+
const site = cooperativeSite(
|
|
810
|
+
dec.name.text,
|
|
811
|
+
params,
|
|
812
|
+
{
|
|
813
|
+
start: `${receiver}.${workMethod?.start ?? "start"}`,
|
|
814
|
+
advance: `${receiver}.${workMethod?.advance ?? "update"}`,
|
|
815
|
+
snapshot: `${receiver}.${workMethod?.snapshot ?? "snapshot"}`,
|
|
816
|
+
reset: `${receiver}.${workMethod?.reset ?? "reset"}`,
|
|
817
|
+
declaration: workMethod ? "" : `private ${worker}=new ${workNode.getText(source)}();`,
|
|
818
|
+
},
|
|
819
|
+
Boolean(owner),
|
|
820
|
+
);
|
|
821
|
+
if (owner) {
|
|
822
|
+
if (!owner.name) fail(owner, "Continuation owners require a named block.");
|
|
823
|
+
replacements.push({ start: dec.getStart(source), end: dec.end, text: site.body });
|
|
824
|
+
ownedContinuations.set(symbol(owner.name)!, [
|
|
825
|
+
...(ownedContinuations.get(symbol(owner.name)!) ?? []),
|
|
826
|
+
site.poll,
|
|
827
|
+
]);
|
|
828
|
+
const visit = (node: ts.Node): void => {
|
|
829
|
+
if (
|
|
830
|
+
ts.isCallExpression(node) &&
|
|
831
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
832
|
+
ts.isPropertyAccessExpression(node.expression.expression) &&
|
|
833
|
+
node.expression.expression.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
834
|
+
symbol(node.expression.expression.name) === symbol(dec.name)
|
|
835
|
+
) {
|
|
836
|
+
const renamed =
|
|
837
|
+
node.expression.name.text === "start"
|
|
838
|
+
? site.start
|
|
839
|
+
: node.expression.name.text === "cancel"
|
|
840
|
+
? site.cancel
|
|
841
|
+
: undefined;
|
|
842
|
+
if (renamed)
|
|
843
|
+
replacements.push({
|
|
844
|
+
start: node.expression.getStart(source),
|
|
845
|
+
end: node.expression.end,
|
|
846
|
+
text: `this.${renamed}`,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
ts.forEachChild(node, visit);
|
|
850
|
+
};
|
|
851
|
+
visit(owner);
|
|
852
|
+
} else {
|
|
853
|
+
const name = `TsplcCooperativeSite${sequence++}`,
|
|
854
|
+
entry = `tsplcContinuation${sequence++}`;
|
|
855
|
+
extra.push(`class ${name} extends FunctionBlock<{}>{${site.body}protected execute(input:{}):void{}}
|
|
856
|
+
export function ${entry}():void{${dec.name.text}.poll();}`);
|
|
857
|
+
replacements.push({
|
|
858
|
+
start: init.getStart(source),
|
|
859
|
+
end: init.end,
|
|
860
|
+
text: `new ${name}()`,
|
|
861
|
+
});
|
|
862
|
+
result.programs[entry] = { module: resolve(source.fileName), export: entry };
|
|
863
|
+
result.continuations.set(entry, { instance: dec.name.text, file: source.fileName });
|
|
864
|
+
}
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
if (
|
|
868
|
+
!ts.isIdentifier(dec.name) ||
|
|
869
|
+
init.arguments.length !== 2 ||
|
|
870
|
+
!ts.isObjectLiteralExpression(init.arguments[1]!)
|
|
871
|
+
)
|
|
872
|
+
fail(
|
|
873
|
+
dec,
|
|
874
|
+
"Define background tasks at module scope or as function-block fields with a named async function and scheduling options.",
|
|
875
|
+
);
|
|
876
|
+
const fn = functions.get(symbol(init.arguments[0]!)!);
|
|
877
|
+
if (!fn?.type || !ts.isTypeReferenceNode(fn.type))
|
|
878
|
+
fail(init, "backgroundTasks.define requires a named async function.");
|
|
879
|
+
const { params, valueType, valueAggregate } = workTypes(fn, dec);
|
|
880
|
+
const mailbox = `TsplcMailbox${sequence++}`,
|
|
881
|
+
worker = `tsplcWorker${sequence++}`,
|
|
882
|
+
entry = `tsplcBackground${sequence++}`;
|
|
883
|
+
const requestType = `{ready:BOOL;${params.map((p) => `${p.field}:${p.type}`).join(";")}}`;
|
|
884
|
+
const snapshotType = `{valid:BOOL;busy:BOOL;done:BOOL;cancelRequested:BOOL;error:UDINT;result:${valueType}}`;
|
|
885
|
+
const emptySnapshot = `{valid:true,busy:this.pending || this.active,done:this.completed,cancelRequested:this.cancelRequested,error:this.failure,result:${copyValue("this.value", valueAggregate)}}`;
|
|
886
|
+
extra.push(`class ${mailbox} extends FunctionBlock<{}> {
|
|
887
|
+
private pending:BOOL=false;private active:BOOL=false;private completed:BOOL=false;private cancelRequested:BOOL=false;private failure:UDINT=0;private value:${valueType}=defaultValue<${valueType}>();
|
|
888
|
+
${params.map((p) => `private ${p.field}:${p.type}=defaultValue<${p.type}>();`).join("\n")}
|
|
889
|
+
start(${params.map((p) => `${p.name}:${p.type}`).join(",")}):BOOL {if(this.pending || this.active || this.completed)return false;${params.map((p) => `this.${p.field}=${copyValue(p.name, p.aggregate)};`).join("")}this.pending=true;return true;}
|
|
890
|
+
take():${requestType} {const ready:BOOL=this.pending; if(ready){this.pending=false;this.active=true;}return {ready,${params.map((p) => `${p.field}:${copyValue(`this.${p.field}`, p.aggregate)}`).join(",")}};}
|
|
891
|
+
publish(value:${valueType},error:UDINT):BOOL {if(!this.active)return false;if(this.cancelRequested){this.value=defaultValue<${valueType}>();this.failure=1;}else{this.value=${copyValue("value", valueAggregate)};this.failure=error;}this.active=false;this.completed=true;this.cancelRequested=false;return true;}
|
|
892
|
+
snapshot():${snapshotType} {return ${emptySnapshot};}
|
|
893
|
+
cancel():BOOL {if(this.pending){this.pending=false;this.completed=true;this.failure=1;return true;}if(this.active){this.cancelRequested=true;return true;}return false;}
|
|
894
|
+
reset():BOOL {if(this.pending || this.active)return false;this.completed=false;this.failure=0;this.cancelRequested=false;this.value=defaultValue<${valueType}>();return true;}
|
|
895
|
+
protected execute(input:{}):void {}
|
|
896
|
+
}
|
|
897
|
+
`);
|
|
898
|
+
const receiver = owner ? `this.${dec.name.text}` : dec.name.text;
|
|
899
|
+
const workerReceiver = owner ? `this.${worker}` : worker;
|
|
900
|
+
const run = `const request=${receiver}.take();if(request.ready){const accepted:BOOL=${workerReceiver}.start(${params.map((p) => `request.${p.field}`).join(",")});}const control=${receiver}.snapshot();if(!control.valid)return;${workerReceiver}.update({cancel:control.cancelRequested});const state=${workerReceiver}.snapshot();if(state.done){const published:BOOL=${receiver}.publish(state.result,state.error);if(published)${workerReceiver}.reset();}`;
|
|
901
|
+
if (owner) {
|
|
902
|
+
if (!owner.name) fail(owner, "Background job owners need a named function block.");
|
|
903
|
+
replacements.push({
|
|
904
|
+
start: owner.end - 1,
|
|
905
|
+
end: owner.end - 1,
|
|
906
|
+
text: `private ${worker}=new ${text(init.arguments[0]!)}();public ${entry}():void{${run}}`,
|
|
907
|
+
});
|
|
908
|
+
result.blocks.set(`${source.fileName}:${owner.name.text}`, { kind: "backgroundHost" });
|
|
909
|
+
} else
|
|
910
|
+
extra.push(
|
|
911
|
+
`const ${worker}=new ${text(init.arguments[0]!)}();export function ${entry}():void{${run}}`,
|
|
912
|
+
);
|
|
913
|
+
result.blocks.set(`${source.fileName}:${mailbox}`, { kind: "mailbox" });
|
|
914
|
+
const opts = new Map(
|
|
915
|
+
init.arguments[1]!.properties.filter(ts.isPropertyAssignment).map((p) => [
|
|
916
|
+
p.name.getText(source),
|
|
917
|
+
p.initializer,
|
|
918
|
+
]),
|
|
919
|
+
);
|
|
920
|
+
const interval = opts.get("interval"),
|
|
921
|
+
priority = opts.get("priority");
|
|
922
|
+
if (
|
|
923
|
+
!interval ||
|
|
924
|
+
!ts.isCallExpression(interval) ||
|
|
925
|
+
interval.arguments.length !== 1 ||
|
|
926
|
+
!ts.isNumericLiteral(interval.arguments[0]!) ||
|
|
927
|
+
!priority ||
|
|
928
|
+
!ts.isNumericLiteral(priority) ||
|
|
929
|
+
!["ms", "seconds"].some((n) => official(interval.expression, n))
|
|
930
|
+
)
|
|
931
|
+
fail(init, "Background schedules require a literal ms/seconds interval and priority.");
|
|
932
|
+
const milliseconds =
|
|
933
|
+
Number(interval.arguments[0]!.text) *
|
|
934
|
+
(official(interval.expression, "seconds") ? 1000 : 1);
|
|
935
|
+
if (
|
|
936
|
+
!Number.isSafeInteger(milliseconds) ||
|
|
937
|
+
milliseconds <= 0 ||
|
|
938
|
+
milliseconds > 4294967295 ||
|
|
939
|
+
Number(priority.text) < 1 ||
|
|
940
|
+
Number(priority.text) > 31
|
|
941
|
+
)
|
|
942
|
+
fail(init, "Background interval must be positive and priority must be 1..31.");
|
|
943
|
+
const schedule = {
|
|
944
|
+
intervalNanoseconds: String(BigInt(milliseconds) * 1000000n),
|
|
945
|
+
priority: Number(priority.text),
|
|
946
|
+
mailbox: dec.name.text,
|
|
947
|
+
worker,
|
|
948
|
+
};
|
|
949
|
+
if (owner) {
|
|
950
|
+
const key = symbol(owner.name!)!;
|
|
951
|
+
ownedJobs.set(key, [...(ownedJobs.get(key) ?? []), { method: entry, schedule }]);
|
|
952
|
+
} else {
|
|
953
|
+
result.programs[entry] = { module: resolve(source.fileName), export: entry };
|
|
954
|
+
result.schedules.set(entry, schedule);
|
|
955
|
+
}
|
|
956
|
+
replacements.push({
|
|
957
|
+
start: init.getStart(source),
|
|
958
|
+
end: init.end,
|
|
959
|
+
text: `new ${mailbox}()`,
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
if (replacements.length) {
|
|
963
|
+
let changed = source.text;
|
|
964
|
+
for (const edit of replacements.sort((a, b) => b.start - a.start))
|
|
965
|
+
changed = changed.slice(0, edit.start) + edit.text + changed.slice(edit.end);
|
|
966
|
+
const generated = ts.createSourceFile(
|
|
967
|
+
source.fileName,
|
|
968
|
+
extra.join("\n"),
|
|
969
|
+
ts.ScriptTarget.Latest,
|
|
970
|
+
true,
|
|
971
|
+
);
|
|
972
|
+
const before = generated.statements
|
|
973
|
+
.filter(ts.isClassDeclaration)
|
|
974
|
+
.map((n) => n.getText(generated))
|
|
975
|
+
.join("\n");
|
|
976
|
+
const after = generated.statements
|
|
977
|
+
.filter((n) => !ts.isClassDeclaration(n))
|
|
978
|
+
.map((n) => n.getText(generated))
|
|
979
|
+
.join("\n");
|
|
980
|
+
result.sources.set(source.fileName, before + "\n" + changed + "\n" + after);
|
|
981
|
+
}
|
|
982
|
+
} catch (cause) {
|
|
983
|
+
if (typeof cause === "object" && cause !== null && "asyncDiagnostic" in cause)
|
|
984
|
+
result.diagnostics.push(cause.asyncDiagnostic as Diagnostic);
|
|
985
|
+
else throw cause;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
for (const source of runtimeSources(program)) {
|
|
989
|
+
if (source.isDeclarationFile) continue;
|
|
990
|
+
const entries: string[] = [];
|
|
991
|
+
// `new Block()` allocates one instance; `defaultValue<FixedArray<Block, N, Lower>>()` one per element.
|
|
992
|
+
const allocated = (node: ts.Node) => {
|
|
993
|
+
if (ts.isNewExpression(node)) return { block: node.expression };
|
|
994
|
+
const type = ts.isCallExpression(node) ? node.typeArguments?.[0] : undefined;
|
|
995
|
+
const [element, length, lower] =
|
|
996
|
+
type && ts.isTypeReferenceNode(type) && type.typeName.getText(source) === "FixedArray"
|
|
997
|
+
? (type.typeArguments ?? [])
|
|
998
|
+
: [];
|
|
999
|
+
if (!element || !ts.isTypeReferenceNode(element) || !length) return undefined;
|
|
1000
|
+
// A literal, or a table's `(typeof table)["length"]`.
|
|
1001
|
+
const integer = (node: ts.TypeNode) => {
|
|
1002
|
+
const type = checker.getTypeFromTypeNode(node);
|
|
1003
|
+
return type.isNumberLiteral() ? type.value : NaN;
|
|
1004
|
+
};
|
|
1005
|
+
const first = lower ? integer(lower) : 0;
|
|
1006
|
+
return { block: element.typeName, first, last: first + integer(length) - 1 };
|
|
1007
|
+
};
|
|
1008
|
+
const visit = (node: ts.Node): void => {
|
|
1009
|
+
const instance = allocated(node);
|
|
1010
|
+
if (instance) {
|
|
1011
|
+
const jobs = ownedJobs.get(symbol(instance.block)!);
|
|
1012
|
+
const polls = ownedContinuations.get(symbol(instance.block)!);
|
|
1013
|
+
if (jobs || polls) {
|
|
1014
|
+
const declaration = node.parent;
|
|
1015
|
+
if (
|
|
1016
|
+
!ts.isVariableDeclaration(declaration) ||
|
|
1017
|
+
!ts.isIdentifier(declaration.name) ||
|
|
1018
|
+
!ts.isVariableDeclarationList(declaration.parent) ||
|
|
1019
|
+
!ts.isVariableStatement(declaration.parent.parent) ||
|
|
1020
|
+
!ts.isSourceFile(declaration.parent.parent.parent) ||
|
|
1021
|
+
(jobs && instance.first !== undefined)
|
|
1022
|
+
) {
|
|
1023
|
+
result.diagnostics.push(
|
|
1024
|
+
diagnosticFor(
|
|
1025
|
+
node,
|
|
1026
|
+
"Function blocks with background jobs require module-level named instances.",
|
|
1027
|
+
),
|
|
1028
|
+
);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const name = declaration.name.text;
|
|
1032
|
+
for (const poll of polls ?? []) {
|
|
1033
|
+
const entry = `tsplcContinuation${sequence++}`;
|
|
1034
|
+
entries.push(
|
|
1035
|
+
instance.first === undefined
|
|
1036
|
+
? `export function ${entry}():void{${name}.${poll}();}`
|
|
1037
|
+
: `export function ${entry}():void{for(let element:DINT=${instance.first};element<=${instance.last};element++){${name}[element].${poll}();}}`,
|
|
1038
|
+
);
|
|
1039
|
+
result.programs[entry] = { module: resolve(source.fileName), export: entry };
|
|
1040
|
+
result.continuations.set(entry, { instance: name, file: source.fileName });
|
|
1041
|
+
}
|
|
1042
|
+
for (const job of jobs ?? []) {
|
|
1043
|
+
const entry = `tsplcBackground${sequence++}`;
|
|
1044
|
+
entries.push(`export function ${entry}():void{${name}.${job.method}();}`);
|
|
1045
|
+
result.programs[entry] = { module: resolve(source.fileName), export: entry };
|
|
1046
|
+
result.schedules.set(entry, { ...job.schedule, worker: name });
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
ts.forEachChild(node, visit);
|
|
1051
|
+
};
|
|
1052
|
+
visit(source);
|
|
1053
|
+
if (entries.length)
|
|
1054
|
+
result.sources.set(
|
|
1055
|
+
source.fileName,
|
|
1056
|
+
(result.sources.get(source.fileName) ?? source.text) + "\n" + entries.join("\n"),
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
return result;
|
|
1060
|
+
};
|