@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
package/ir/routine.ts
ADDED
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
import type { SourceSpan } from "./diagnostics.ts";
|
|
2
|
+
import type {
|
|
3
|
+
Variable,
|
|
4
|
+
Expression,
|
|
5
|
+
Statement,
|
|
6
|
+
Place,
|
|
7
|
+
PlcFunction,
|
|
8
|
+
FunctionBlock,
|
|
9
|
+
} from "./model.ts";
|
|
10
|
+
import type {
|
|
11
|
+
CoreExpression,
|
|
12
|
+
CoreStatement,
|
|
13
|
+
CorePath,
|
|
14
|
+
CoreFunction,
|
|
15
|
+
CoreFunctionBlock,
|
|
16
|
+
} from "./core-model.ts";
|
|
17
|
+
import {
|
|
18
|
+
fold,
|
|
19
|
+
interval,
|
|
20
|
+
loopBounds,
|
|
21
|
+
narrowRanges,
|
|
22
|
+
valueRange,
|
|
23
|
+
type IntegerInterval,
|
|
24
|
+
} from "./scalars.ts";
|
|
25
|
+
import { instancedBlock, integerRange } from "./types.ts";
|
|
26
|
+
import { aggregate, expressionIssue, placeType, valueFits } from "./aggregates.ts";
|
|
27
|
+
import { reject } from "./normalization-error.ts";
|
|
28
|
+
import type { ModuleDeclarations } from "./declarations.ts";
|
|
29
|
+
|
|
30
|
+
type Flow = "fallthrough" | "return" | "loop" | "mixed";
|
|
31
|
+
interface BodyResult {
|
|
32
|
+
body: CoreStatement[];
|
|
33
|
+
initialized: Set<string>;
|
|
34
|
+
flow: Flow;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Only paths that reach the next statement participate in a join. A local is
|
|
38
|
+
// definitely initialized when every such path initialized it; branches that
|
|
39
|
+
// return or leave the loop do not constrain it.
|
|
40
|
+
function join(
|
|
41
|
+
branches: readonly BodyResult[],
|
|
42
|
+
initialized: Set<string>,
|
|
43
|
+
): { flow: Flow; initialized: Set<string> } {
|
|
44
|
+
const falling = branches.filter((branch) => branch.flow === "fallthrough");
|
|
45
|
+
if (falling.length)
|
|
46
|
+
return {
|
|
47
|
+
flow: "fallthrough",
|
|
48
|
+
initialized: new Set(
|
|
49
|
+
[...falling[0]!.initialized].filter((id) =>
|
|
50
|
+
falling.every((branch) => branch.initialized.has(id)),
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
};
|
|
54
|
+
const flow = branches[0]!.flow;
|
|
55
|
+
return { flow: branches.every((branch) => branch.flow === flow) ? flow : "mixed", initialized };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// All mutable lowering state belongs to one invocation of one authored routine.
|
|
59
|
+
// sourceScope admits authored places; scope additionally contains captured temps.
|
|
60
|
+
export class RoutineNormalizer {
|
|
61
|
+
private readonly fn: PlcFunction;
|
|
62
|
+
|
|
63
|
+
private readonly block: FunctionBlock | undefined;
|
|
64
|
+
private readonly owner: FunctionBlock | undefined;
|
|
65
|
+
|
|
66
|
+
private readonly scope: Map<string, Variable>;
|
|
67
|
+
|
|
68
|
+
private readonly sourceScope: Map<string, Variable>;
|
|
69
|
+
|
|
70
|
+
private readonly locals: Variable[];
|
|
71
|
+
|
|
72
|
+
private readonly ranges = new Map<string, IntegerInterval>();
|
|
73
|
+
|
|
74
|
+
private readonly counters = new Set<string>();
|
|
75
|
+
|
|
76
|
+
private readonly callees = new Set<string>();
|
|
77
|
+
|
|
78
|
+
private temporaryCount = 0;
|
|
79
|
+
|
|
80
|
+
constructor(
|
|
81
|
+
routine: PlcFunction | FunctionBlock,
|
|
82
|
+
private readonly declarations: ModuleDeclarations,
|
|
83
|
+
) {
|
|
84
|
+
this.block = "inputs" in routine ? routine : undefined;
|
|
85
|
+
this.owner =
|
|
86
|
+
this.block ??
|
|
87
|
+
("owner" in routine && routine.owner
|
|
88
|
+
? declarations.blocks.get(routine.owner.block)
|
|
89
|
+
: undefined);
|
|
90
|
+
const parameters = "parameters" in routine ? routine.parameters : routine.inputs;
|
|
91
|
+
this.fn = {
|
|
92
|
+
id: routine.id,
|
|
93
|
+
name: routine.name,
|
|
94
|
+
...("owner" in routine && routine.owner && { owner: routine.owner }),
|
|
95
|
+
parameters,
|
|
96
|
+
locals: routine.locals,
|
|
97
|
+
body: routine.body,
|
|
98
|
+
span: routine.span,
|
|
99
|
+
...(routine.result && { result: routine.result }),
|
|
100
|
+
};
|
|
101
|
+
this.scope = new Map(declarations.globals);
|
|
102
|
+
for (const item of [
|
|
103
|
+
...parameters,
|
|
104
|
+
...(this.owner ? [...this.owner.state, ...this.owner.outputs] : []),
|
|
105
|
+
...routine.locals,
|
|
106
|
+
])
|
|
107
|
+
this.scope.set(item.id, item);
|
|
108
|
+
this.sourceScope = new Map(this.scope);
|
|
109
|
+
this.locals = [...routine.locals];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
normalize(): { routine: CoreFunction | CoreFunctionBlock; callees: ReadonlySet<string> } {
|
|
113
|
+
if (this.block?.standard)
|
|
114
|
+
return { routine: { ...this.block, body: [] }, callees: this.callees };
|
|
115
|
+
const normalized = this.body(this.fn.body, new Set(this.fn.parameters.map((item) => item.id)));
|
|
116
|
+
if (this.fn.result && normalized.flow !== "return")
|
|
117
|
+
reject("A typed helper must return on every path.", this.fn.span);
|
|
118
|
+
const routine = this.block
|
|
119
|
+
? { ...this.block, locals: this.locals, body: normalized.body }
|
|
120
|
+
: { ...this.fn, locals: this.locals, body: normalized.body };
|
|
121
|
+
return { routine, callees: this.callees };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private temporary(type: string, span: SourceSpan): Variable {
|
|
125
|
+
let id: string;
|
|
126
|
+
do {
|
|
127
|
+
id = `${this.fn.id}:temporary:${this.temporaryCount++}`;
|
|
128
|
+
} while (this.declarations.hasId(id));
|
|
129
|
+
this.declarations.claimId(id);
|
|
130
|
+
const item: Variable = {
|
|
131
|
+
id,
|
|
132
|
+
name: `temporary_${this.temporaryCount}`,
|
|
133
|
+
type,
|
|
134
|
+
storage: "local",
|
|
135
|
+
mutable: true,
|
|
136
|
+
retain: false,
|
|
137
|
+
span,
|
|
138
|
+
};
|
|
139
|
+
this.scope.set(id, item);
|
|
140
|
+
this.locals.push(item);
|
|
141
|
+
return item;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private load(variable: Variable, span: SourceSpan): CoreExpression {
|
|
145
|
+
return { kind: "load", root: variable.id, type: variable.type, span };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private verifyCall(
|
|
149
|
+
id: string,
|
|
150
|
+
arguments_: readonly Expression[],
|
|
151
|
+
span: SourceSpan,
|
|
152
|
+
instance?: string,
|
|
153
|
+
): PlcFunction {
|
|
154
|
+
const called = this.declarations.signatures.get(id);
|
|
155
|
+
if (!called || called.parameters.length !== arguments_.length)
|
|
156
|
+
reject("Unknown helper or wrong argument count.", span);
|
|
157
|
+
if (called.owner) {
|
|
158
|
+
if (instance) {
|
|
159
|
+
const variable = this.sourceScope.get(instance);
|
|
160
|
+
const type = variable && instancedBlock(variable.type, this.declarations.types);
|
|
161
|
+
if (
|
|
162
|
+
(variable?.storage !== "instance" && variable?.mode !== "inOut") ||
|
|
163
|
+
type?.block !== called.owner.block
|
|
164
|
+
)
|
|
165
|
+
reject("Method calls require a matching static block instance.", span);
|
|
166
|
+
} else if (this.owner?.id !== called.owner.block)
|
|
167
|
+
reject("An implicit method call requires its owning block scope.", span);
|
|
168
|
+
if (called.owner.access !== "public" && this.owner?.id !== called.owner.block)
|
|
169
|
+
reject("Private and protected methods require their owning block scope.", span);
|
|
170
|
+
} else if (instance) reject("Ordinary functions cannot have an instance receiver.", span);
|
|
171
|
+
this.callees.add(id);
|
|
172
|
+
arguments_.forEach((argument, index) => {
|
|
173
|
+
if (argument.type !== called.parameters[index]?.type)
|
|
174
|
+
reject("Helper argument has the wrong PLC type.", argument.span);
|
|
175
|
+
});
|
|
176
|
+
return called;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private expression(
|
|
180
|
+
value: Expression,
|
|
181
|
+
initialized: Set<string>,
|
|
182
|
+
output: CoreStatement[],
|
|
183
|
+
allowAggregateLoad = false,
|
|
184
|
+
): CoreExpression {
|
|
185
|
+
const type = this.declarations.types.get(value.type);
|
|
186
|
+
if (!type || !this.declarations.isValueType(value.type))
|
|
187
|
+
reject("Unknown or non-value expression type.", value.span);
|
|
188
|
+
if (value.kind === "borrow")
|
|
189
|
+
reject("Borrowed places are only valid in block update arguments.", value.span);
|
|
190
|
+
const common = { type: value.type, span: value.span };
|
|
191
|
+
if (value.kind === "length") {
|
|
192
|
+
const array = this.declarations.types.get(value.operand.type);
|
|
193
|
+
if (array?.kind !== "array" || value.type !== "DINT")
|
|
194
|
+
reject("Length requires a fixed array and DINT result.", value.span);
|
|
195
|
+
if (value.operand.kind === "load") {
|
|
196
|
+
const source = this.sourceScope.get(value.operand.place.root),
|
|
197
|
+
checked = placeType(
|
|
198
|
+
value.operand.place,
|
|
199
|
+
this.declarations.types,
|
|
200
|
+
this.sourceScope,
|
|
201
|
+
this.ranges,
|
|
202
|
+
);
|
|
203
|
+
if (
|
|
204
|
+
!source ||
|
|
205
|
+
checked.issue ||
|
|
206
|
+
checked.type !== value.operand.type ||
|
|
207
|
+
(source.storage === "local" && !initialized.has(source.id))
|
|
208
|
+
)
|
|
209
|
+
reject(checked.issue ?? "Invalid array receiver.", value.span);
|
|
210
|
+
this.normalizePath(value.operand.place, initialized, output);
|
|
211
|
+
} else this.expression(value.operand, initialized, output, true);
|
|
212
|
+
return {
|
|
213
|
+
...common,
|
|
214
|
+
kind: "constant",
|
|
215
|
+
value: { kind: "integer", value: String(array.length) },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (value.kind === "conditional") {
|
|
219
|
+
if (
|
|
220
|
+
value.condition.type !== "BOOL" ||
|
|
221
|
+
value.then.type !== value.type ||
|
|
222
|
+
value.else.type !== value.type
|
|
223
|
+
)
|
|
224
|
+
reject("Invalid conditional types.", value.span);
|
|
225
|
+
const condition = this.expression(value.condition, initialized, output),
|
|
226
|
+
result = this.temporary(value.type, value.span);
|
|
227
|
+
const branch = (operand: Expression): CoreStatement[] => {
|
|
228
|
+
const statements: CoreStatement[] = [];
|
|
229
|
+
const evaluated = this.expression(operand, new Set(initialized), statements);
|
|
230
|
+
statements.push({ kind: "assign", root: result.id, value: evaluated, span: value.span });
|
|
231
|
+
return statements;
|
|
232
|
+
};
|
|
233
|
+
output.push({
|
|
234
|
+
kind: "if",
|
|
235
|
+
condition,
|
|
236
|
+
// biome-ignore lint/suspicious/noThenProperty: PLC branch data is an array, not a thenable callback.
|
|
237
|
+
then: branch(value.then),
|
|
238
|
+
else: branch(value.else),
|
|
239
|
+
span: value.span,
|
|
240
|
+
});
|
|
241
|
+
initialized.add(result.id);
|
|
242
|
+
return this.load(result, value.span);
|
|
243
|
+
}
|
|
244
|
+
if (value.kind === "call") {
|
|
245
|
+
const called = this.verifyCall(value.function, value.arguments, value.span, value.instance);
|
|
246
|
+
if (called.result !== value.type)
|
|
247
|
+
reject("Expression call requires a matching result type.", value.span);
|
|
248
|
+
const index =
|
|
249
|
+
value.instance &&
|
|
250
|
+
this.instanceIndex(value.instance, value.index, initialized, output, value.span);
|
|
251
|
+
const arguments_ = this.arguments(called.parameters, value.arguments, initialized, output);
|
|
252
|
+
const result = this.temporary(value.type, value.span);
|
|
253
|
+
output.push({
|
|
254
|
+
kind: "invoke",
|
|
255
|
+
function: value.function,
|
|
256
|
+
...(value.instance && { instance: value.instance }),
|
|
257
|
+
...(index && { index }),
|
|
258
|
+
arguments: arguments_,
|
|
259
|
+
destination: result.id,
|
|
260
|
+
span: value.span,
|
|
261
|
+
});
|
|
262
|
+
initialized.add(result.id);
|
|
263
|
+
return this.load(result, value.span);
|
|
264
|
+
}
|
|
265
|
+
const issue = expressionIssue(value, this.declarations.types, this.scope, this.ranges);
|
|
266
|
+
if (issue) reject(issue, value.span);
|
|
267
|
+
switch (value.kind) {
|
|
268
|
+
case "copy":
|
|
269
|
+
return this.expression(value.operand, initialized, output, true);
|
|
270
|
+
case "record":
|
|
271
|
+
case "array": {
|
|
272
|
+
const result = this.temporary(value.type, value.span);
|
|
273
|
+
if (value.kind === "record") {
|
|
274
|
+
for (const [name, operand] of Object.entries(value.fields))
|
|
275
|
+
output.push({
|
|
276
|
+
kind: "assign",
|
|
277
|
+
root: result.id,
|
|
278
|
+
path: [{ kind: "field", name }],
|
|
279
|
+
value: this.expression(operand, initialized, output),
|
|
280
|
+
span: operand.span,
|
|
281
|
+
});
|
|
282
|
+
} else {
|
|
283
|
+
const array = this.declarations.types.get(value.type)!;
|
|
284
|
+
if (array.kind !== "array") reject("Invalid array construction.", value.span);
|
|
285
|
+
value.items.forEach((operand, index) => {
|
|
286
|
+
output.push({
|
|
287
|
+
kind: "assign",
|
|
288
|
+
root: result.id,
|
|
289
|
+
path: [
|
|
290
|
+
{
|
|
291
|
+
kind: "index",
|
|
292
|
+
value: {
|
|
293
|
+
kind: "constant",
|
|
294
|
+
type: "DINT",
|
|
295
|
+
value: { kind: "integer", value: String(array.lowerBound + index) },
|
|
296
|
+
span: operand.span,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
value: this.expression(operand, initialized, output),
|
|
301
|
+
span: operand.span,
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
initialized.add(result.id);
|
|
306
|
+
return this.load(result, value.span);
|
|
307
|
+
}
|
|
308
|
+
case "constant": {
|
|
309
|
+
if (!aggregate(type)) return { ...common, kind: "constant", value: value.value };
|
|
310
|
+
const result = this.temporary(value.type, value.span);
|
|
311
|
+
output.push({
|
|
312
|
+
kind: "assign",
|
|
313
|
+
root: result.id,
|
|
314
|
+
value: { ...common, kind: "constant", value: value.value },
|
|
315
|
+
span: value.span,
|
|
316
|
+
});
|
|
317
|
+
initialized.add(result.id);
|
|
318
|
+
return this.load(result, value.span);
|
|
319
|
+
}
|
|
320
|
+
case "load": {
|
|
321
|
+
const source = this.sourceScope.get(value.place.root),
|
|
322
|
+
checked = placeType(value.place, this.declarations.types, this.sourceScope, this.ranges);
|
|
323
|
+
if (!source || checked.issue || checked.type !== value.type)
|
|
324
|
+
reject(checked.issue ?? "Invalid PLC load.", value.span);
|
|
325
|
+
if (aggregate(type) && !allowAggregateLoad)
|
|
326
|
+
reject("Aggregate loads require explicit copy or a readonly value argument.", value.span);
|
|
327
|
+
if (source.storage === "local" && !initialized.has(source.id))
|
|
328
|
+
reject("Local read before initialization.", value.span);
|
|
329
|
+
// The virtual .out view is not a vendor record value. Snapshot its
|
|
330
|
+
// declared fields, rather than copying the whole FB instance.
|
|
331
|
+
const first = value.place.path[0];
|
|
332
|
+
if (
|
|
333
|
+
source.storage === "instance" &&
|
|
334
|
+
value.place.path.length === 1 &&
|
|
335
|
+
first?.kind === "field" &&
|
|
336
|
+
first.name === "out" &&
|
|
337
|
+
type.kind === "record"
|
|
338
|
+
) {
|
|
339
|
+
const result = this.temporary(value.type, value.span);
|
|
340
|
+
for (const field of type.fields) {
|
|
341
|
+
const selected: Expression = {
|
|
342
|
+
kind: "load",
|
|
343
|
+
type: field.type,
|
|
344
|
+
place: {
|
|
345
|
+
root: source.id,
|
|
346
|
+
path: [...value.place.path, { kind: "field", name: field.name }],
|
|
347
|
+
},
|
|
348
|
+
span: value.span,
|
|
349
|
+
};
|
|
350
|
+
const captured = this.expression(selected, initialized, output, true);
|
|
351
|
+
output.push({
|
|
352
|
+
kind: "assign",
|
|
353
|
+
root: result.id,
|
|
354
|
+
path: [{ kind: "field", name: field.name }],
|
|
355
|
+
value: captured,
|
|
356
|
+
span: value.span,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
initialized.add(result.id);
|
|
360
|
+
return this.load(result, value.span);
|
|
361
|
+
}
|
|
362
|
+
const path = this.normalizePath(value.place, initialized, output);
|
|
363
|
+
if (source.storage === "constant" && source.initial && !path.length)
|
|
364
|
+
return { ...common, kind: "constant", value: source.initial };
|
|
365
|
+
// Capture now, before a later operand can invoke an effectful helper.
|
|
366
|
+
const result = this.temporary(value.type, value.span);
|
|
367
|
+
output.push({
|
|
368
|
+
kind: "assign",
|
|
369
|
+
root: result.id,
|
|
370
|
+
value: { ...common, kind: "load", root: source.id, ...(path.length && { path }) },
|
|
371
|
+
span: value.span,
|
|
372
|
+
});
|
|
373
|
+
const bound = interval(value, this.declarations.types, this.scope, this.ranges);
|
|
374
|
+
if (bound) this.ranges.set(result.id, bound);
|
|
375
|
+
initialized.add(result.id);
|
|
376
|
+
return this.load(result, value.span);
|
|
377
|
+
}
|
|
378
|
+
case "unary": {
|
|
379
|
+
const operand = this.expression(value.operand, initialized, output),
|
|
380
|
+
constant = fold(value, this.scope, this.declarations.types);
|
|
381
|
+
return constant
|
|
382
|
+
? { ...common, kind: "constant", value: constant }
|
|
383
|
+
: { ...common, kind: "unary", operator: value.operator, operand };
|
|
384
|
+
}
|
|
385
|
+
case "convert": {
|
|
386
|
+
const operand = this.expression(value.operand, initialized, output),
|
|
387
|
+
constant = fold(value, this.scope, this.declarations.types);
|
|
388
|
+
if (value.type === value.operand.type) return operand;
|
|
389
|
+
return constant
|
|
390
|
+
? { ...common, kind: "constant", value: constant }
|
|
391
|
+
: { ...common, kind: "convert", operand, policy: value.policy };
|
|
392
|
+
}
|
|
393
|
+
case "binary": {
|
|
394
|
+
const left = this.expression(value.left, initialized, output);
|
|
395
|
+
if (value.operator === "and" || value.operator === "or") {
|
|
396
|
+
const result = this.temporary("BOOL", value.span),
|
|
397
|
+
branch: CoreStatement[] = [];
|
|
398
|
+
output.push({ kind: "assign", root: result.id, value: left, span: value.span });
|
|
399
|
+
const right = this.expression(value.right, new Set(initialized), branch);
|
|
400
|
+
branch.push({ kind: "assign", root: result.id, value: right, span: value.span });
|
|
401
|
+
const condition: CoreExpression =
|
|
402
|
+
value.operator === "and"
|
|
403
|
+
? this.load(result, value.span)
|
|
404
|
+
: {
|
|
405
|
+
kind: "unary",
|
|
406
|
+
operator: "not",
|
|
407
|
+
operand: this.load(result, value.span),
|
|
408
|
+
type: "BOOL",
|
|
409
|
+
span: value.span,
|
|
410
|
+
};
|
|
411
|
+
output.push({
|
|
412
|
+
kind: "if",
|
|
413
|
+
condition,
|
|
414
|
+
// biome-ignore lint/suspicious/noThenProperty: PLC branch data is an array, not a thenable callback.
|
|
415
|
+
then: branch,
|
|
416
|
+
else: [],
|
|
417
|
+
span: value.span,
|
|
418
|
+
});
|
|
419
|
+
initialized.add(result.id);
|
|
420
|
+
return this.load(result, value.span);
|
|
421
|
+
}
|
|
422
|
+
const right = this.expression(value.right, initialized, output),
|
|
423
|
+
constant = fold(value, this.scope, this.declarations.types);
|
|
424
|
+
if (constant) return { ...common, kind: "constant", value: constant };
|
|
425
|
+
return {
|
|
426
|
+
...common,
|
|
427
|
+
kind: "binary",
|
|
428
|
+
operator: value.operator,
|
|
429
|
+
left,
|
|
430
|
+
right,
|
|
431
|
+
...(value.policy && { policy: value.policy }),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private normalizePath(place: Place, initialized: Set<string>, output: CoreStatement[]): CorePath {
|
|
438
|
+
// An index is evaluated and captured once, before later arguments can change
|
|
439
|
+
// its source storage. The resulting path denotes that selected cell.
|
|
440
|
+
return place.path.map((segment) =>
|
|
441
|
+
segment.kind === "field"
|
|
442
|
+
? segment
|
|
443
|
+
: { kind: "index", value: this.expression(segment.value, initialized, output) },
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private destination(
|
|
448
|
+
place: Place,
|
|
449
|
+
type: string,
|
|
450
|
+
initialized: Set<string>,
|
|
451
|
+
span: SourceSpan,
|
|
452
|
+
): Variable {
|
|
453
|
+
const target = this.sourceScope.get(place.root),
|
|
454
|
+
initializing =
|
|
455
|
+
target?.storage === "local" && !initialized.has(place.root) && !place.path.length;
|
|
456
|
+
const checked = placeType(
|
|
457
|
+
place,
|
|
458
|
+
this.declarations.types,
|
|
459
|
+
this.sourceScope,
|
|
460
|
+
this.ranges,
|
|
461
|
+
!initializing,
|
|
462
|
+
);
|
|
463
|
+
if (!target || checked.issue || checked.type !== type || this.counters.has(place.root))
|
|
464
|
+
reject(checked.issue ?? "Invalid assignment destination.", span);
|
|
465
|
+
if (place.path.length && target.storage === "local" && !initialized.has(target.id))
|
|
466
|
+
reject("Initialize the whole aggregate before assigning fields.", span);
|
|
467
|
+
return target;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private instanceIndex(
|
|
471
|
+
instance: string,
|
|
472
|
+
index: Expression | undefined,
|
|
473
|
+
initialized: Set<string>,
|
|
474
|
+
output: CoreStatement[],
|
|
475
|
+
span: SourceSpan,
|
|
476
|
+
): CoreExpression | undefined {
|
|
477
|
+
if (!index) return undefined;
|
|
478
|
+
const checked = placeType(
|
|
479
|
+
{ root: instance, path: [{ kind: "index", value: index }] },
|
|
480
|
+
this.declarations.types,
|
|
481
|
+
this.sourceScope,
|
|
482
|
+
this.ranges,
|
|
483
|
+
);
|
|
484
|
+
if (checked.issue) reject(checked.issue, span);
|
|
485
|
+
return this.expression(index, initialized, output);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private blockUpdate(
|
|
489
|
+
item: Extract<Statement, { kind: "blockUpdate" }>,
|
|
490
|
+
initialized: Set<string>,
|
|
491
|
+
output: CoreStatement[],
|
|
492
|
+
): void {
|
|
493
|
+
const instance = this.sourceScope.get(item.instance),
|
|
494
|
+
type = instance && instancedBlock(instance.type, this.declarations.types);
|
|
495
|
+
const called = type && this.declarations.blocks.get(type.block);
|
|
496
|
+
if ((instance?.storage !== "instance" && instance?.mode !== "inOut") || !called)
|
|
497
|
+
reject("Updates require a statically allocated block instance.", item.span);
|
|
498
|
+
const index = this.instanceIndex(item.instance, item.index, initialized, output, item.span);
|
|
499
|
+
const keys = Object.keys(item.inputs);
|
|
500
|
+
if (
|
|
501
|
+
keys.length !== called.inputs.length ||
|
|
502
|
+
called.inputs.some((input) => !Object.hasOwn(item.inputs, input.name))
|
|
503
|
+
)
|
|
504
|
+
reject("Block update must supply every named input exactly once.", item.span);
|
|
505
|
+
this.callees.add(called.id);
|
|
506
|
+
const arguments_ = this.arguments(
|
|
507
|
+
keys.map((name) => called.inputs.find((input) => input.name === name)!),
|
|
508
|
+
keys.map((name) => item.inputs[name]!),
|
|
509
|
+
initialized,
|
|
510
|
+
output,
|
|
511
|
+
);
|
|
512
|
+
const inputs = Object.fromEntries(keys.map((name, index) => [name, arguments_[index]!]));
|
|
513
|
+
output.push({
|
|
514
|
+
kind: "blockUpdate",
|
|
515
|
+
instance: instance.id,
|
|
516
|
+
...(index && { index }),
|
|
517
|
+
inputs,
|
|
518
|
+
span: item.span,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private arguments(
|
|
523
|
+
parameters: readonly Variable[],
|
|
524
|
+
values: readonly Expression[],
|
|
525
|
+
initialized: Set<string>,
|
|
526
|
+
output: CoreStatement[],
|
|
527
|
+
): CoreExpression[] {
|
|
528
|
+
const arguments_: CoreExpression[] = [];
|
|
529
|
+
for (const [index, value] of values.entries()) {
|
|
530
|
+
const input = parameters[index]!;
|
|
531
|
+
if (value.type !== input.type)
|
|
532
|
+
reject("Block input differs from its concrete PLC type.", value.span);
|
|
533
|
+
const block = this.declarations.types.get(value.type)?.kind === "block";
|
|
534
|
+
if (input.mode === "inOut" && value.kind !== "borrow") {
|
|
535
|
+
// A value with no writable address reaches the callee through a temporary.
|
|
536
|
+
if (block) reject("Pass a block instance by name.", value.span);
|
|
537
|
+
const temporary = this.temporary(value.type, value.span);
|
|
538
|
+
output.push({
|
|
539
|
+
kind: "assign",
|
|
540
|
+
root: temporary.id,
|
|
541
|
+
value: this.expression(value, initialized, output),
|
|
542
|
+
span: value.span,
|
|
543
|
+
});
|
|
544
|
+
initialized.add(temporary.id);
|
|
545
|
+
arguments_.push({
|
|
546
|
+
kind: "borrow",
|
|
547
|
+
type: value.type,
|
|
548
|
+
place: { root: temporary.id, path: [] },
|
|
549
|
+
span: value.span,
|
|
550
|
+
});
|
|
551
|
+
} else if (input.mode === "inOut" && value.kind === "borrow") {
|
|
552
|
+
const root = this.sourceScope.get(value.place.root),
|
|
553
|
+
checked = placeType(
|
|
554
|
+
value.place,
|
|
555
|
+
this.declarations.types,
|
|
556
|
+
this.sourceScope,
|
|
557
|
+
this.ranges,
|
|
558
|
+
!block,
|
|
559
|
+
);
|
|
560
|
+
if (
|
|
561
|
+
!root ||
|
|
562
|
+
checked.issue ||
|
|
563
|
+
checked.type !== value.type ||
|
|
564
|
+
(!block && !this.declarations.isValueType(value.type)) ||
|
|
565
|
+
this.counters.has(root.id) ||
|
|
566
|
+
(root.storage === "local" && !initialized.has(root.id))
|
|
567
|
+
)
|
|
568
|
+
reject(
|
|
569
|
+
checked.issue ?? "Borrow requires initialized mutable value storage of the exact type.",
|
|
570
|
+
value.span,
|
|
571
|
+
);
|
|
572
|
+
arguments_.push({
|
|
573
|
+
kind: "borrow",
|
|
574
|
+
type: value.type,
|
|
575
|
+
place: {
|
|
576
|
+
root: value.place.root,
|
|
577
|
+
path: this.normalizePath(value.place, initialized, output),
|
|
578
|
+
},
|
|
579
|
+
span: value.span,
|
|
580
|
+
});
|
|
581
|
+
} else {
|
|
582
|
+
if (value.kind === "borrow")
|
|
583
|
+
reject("Readonly value inputs cannot receive borrowed references.", value.span);
|
|
584
|
+
arguments_.push(this.expression(value, initialized, output, true));
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return arguments_;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private body(
|
|
591
|
+
statements: readonly Statement[],
|
|
592
|
+
initialized: Set<string>,
|
|
593
|
+
inLoop = false,
|
|
594
|
+
): BodyResult {
|
|
595
|
+
const output: CoreStatement[] = [];
|
|
596
|
+
let flow: Flow = "fallthrough";
|
|
597
|
+
for (const item of statements) {
|
|
598
|
+
if (flow !== "fallthrough") break;
|
|
599
|
+
switch (item.kind) {
|
|
600
|
+
case "assign": {
|
|
601
|
+
this.destination(item.destination, item.value.type, initialized, item.span);
|
|
602
|
+
const path = this.normalizePath(item.destination, initialized, output);
|
|
603
|
+
// A const is assigned once, so its initializer's range holds wherever it is read.
|
|
604
|
+
const bound =
|
|
605
|
+
!path.length &&
|
|
606
|
+
this.scope.get(item.destination.root)?.mutable === false &&
|
|
607
|
+
valueRange(
|
|
608
|
+
item.value,
|
|
609
|
+
this.declarations.types,
|
|
610
|
+
this.scope,
|
|
611
|
+
this.ranges,
|
|
612
|
+
this.declarations.signatures,
|
|
613
|
+
);
|
|
614
|
+
const value = this.expression(item.value, initialized, output);
|
|
615
|
+
if (bound) this.ranges.set(item.destination.root, bound);
|
|
616
|
+
if (!path.length) initialized.add(item.destination.root);
|
|
617
|
+
output.push({
|
|
618
|
+
kind: "assign",
|
|
619
|
+
root: item.destination.root,
|
|
620
|
+
...(path.length && { path }),
|
|
621
|
+
value,
|
|
622
|
+
span: item.span,
|
|
623
|
+
});
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
case "blockUpdate": {
|
|
627
|
+
this.blockUpdate(item, initialized, output);
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
case "invoke": {
|
|
631
|
+
const called = this.verifyCall(item.function, item.arguments, item.span, item.instance);
|
|
632
|
+
if (item.destination) {
|
|
633
|
+
if (item.destination.path.length || !called.result)
|
|
634
|
+
reject("Invalid helper destination.", item.span);
|
|
635
|
+
this.destination(item.destination, called.result, initialized, item.span);
|
|
636
|
+
}
|
|
637
|
+
const index =
|
|
638
|
+
item.instance &&
|
|
639
|
+
this.instanceIndex(item.instance, item.index, initialized, output, item.span);
|
|
640
|
+
// Each argument snapshots its reads before the next argument can invoke
|
|
641
|
+
// a helper that mutates shared state.
|
|
642
|
+
const arguments_ = this.arguments(called.parameters, item.arguments, initialized, output);
|
|
643
|
+
output.push({
|
|
644
|
+
kind: "invoke",
|
|
645
|
+
function: item.function,
|
|
646
|
+
...(item.instance && { instance: item.instance }),
|
|
647
|
+
...(index && { index }),
|
|
648
|
+
arguments: arguments_,
|
|
649
|
+
span: item.span,
|
|
650
|
+
...(item.destination && { destination: item.destination.root }),
|
|
651
|
+
});
|
|
652
|
+
if (item.destination) initialized.add(item.destination.root);
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
case "if": {
|
|
656
|
+
if (item.condition.type !== "BOOL") reject("Conditions require BOOL.", item.span);
|
|
657
|
+
const condition = this.expression(item.condition, initialized, output);
|
|
658
|
+
const originalRanges = new Map(this.ranges);
|
|
659
|
+
const branch = (statements: readonly Statement[], truth: boolean) => {
|
|
660
|
+
this.ranges.clear();
|
|
661
|
+
for (const [root, range] of narrowRanges(
|
|
662
|
+
item.condition,
|
|
663
|
+
truth,
|
|
664
|
+
this.declarations.types,
|
|
665
|
+
this.sourceScope,
|
|
666
|
+
originalRanges,
|
|
667
|
+
))
|
|
668
|
+
this.ranges.set(root, range);
|
|
669
|
+
try {
|
|
670
|
+
return this.body(statements, new Set(initialized), inLoop);
|
|
671
|
+
} finally {
|
|
672
|
+
this.ranges.clear();
|
|
673
|
+
for (const [root, range] of originalRanges) this.ranges.set(root, range);
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
const yes = branch(item.then, true),
|
|
677
|
+
no = branch(item.else, false);
|
|
678
|
+
({ flow, initialized } = join([yes, no], initialized));
|
|
679
|
+
output.push({
|
|
680
|
+
kind: "if",
|
|
681
|
+
condition,
|
|
682
|
+
// biome-ignore lint/suspicious/noThenProperty: PLC branch data is an array, not a thenable callback.
|
|
683
|
+
then: yes.body,
|
|
684
|
+
else: no.body,
|
|
685
|
+
span: item.span,
|
|
686
|
+
});
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
case "switch": {
|
|
690
|
+
const value = this.expression(item.value, initialized, output),
|
|
691
|
+
seen = new Set<string>();
|
|
692
|
+
const branches = item.cases.map((branch) => {
|
|
693
|
+
for (const constant of branch.values) {
|
|
694
|
+
if (
|
|
695
|
+
!valueFits(
|
|
696
|
+
constant,
|
|
697
|
+
this.declarations.types.get(item.value.type)!,
|
|
698
|
+
this.declarations.types,
|
|
699
|
+
)
|
|
700
|
+
)
|
|
701
|
+
reject("Invalid case constant.", item.span);
|
|
702
|
+
const key = JSON.stringify(constant);
|
|
703
|
+
if (seen.has(key)) reject("Duplicate case constant.", item.span);
|
|
704
|
+
seen.add(key);
|
|
705
|
+
}
|
|
706
|
+
return {
|
|
707
|
+
values: branch.values,
|
|
708
|
+
result: this.body(branch.body, new Set(initialized), inLoop),
|
|
709
|
+
};
|
|
710
|
+
});
|
|
711
|
+
const fallback = this.body(item.fallback, new Set(initialized), inLoop);
|
|
712
|
+
({ flow, initialized } = join(
|
|
713
|
+
[...branches.map((branch) => branch.result), fallback],
|
|
714
|
+
initialized,
|
|
715
|
+
));
|
|
716
|
+
output.push({
|
|
717
|
+
kind: "switch",
|
|
718
|
+
value,
|
|
719
|
+
cases: branches.map((branch) => ({ values: branch.values, body: branch.result.body })),
|
|
720
|
+
fallback: fallback.body,
|
|
721
|
+
span: item.span,
|
|
722
|
+
});
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
case "for": {
|
|
726
|
+
const counter = this.sourceScope.get(item.counter);
|
|
727
|
+
const counterType = counter && this.declarations.types.get(counter.type);
|
|
728
|
+
if (
|
|
729
|
+
!counter ||
|
|
730
|
+
!(item.counterScope === "existing"
|
|
731
|
+
? ["local", "module", "field"].includes(counter.storage)
|
|
732
|
+
: counter.storage === "local") ||
|
|
733
|
+
!counter.mutable ||
|
|
734
|
+
counterType?.kind !== "integer" ||
|
|
735
|
+
this.counters.has(counter.id) ||
|
|
736
|
+
item.start.type !== counter.type ||
|
|
737
|
+
item.limit.type !== counter.type ||
|
|
738
|
+
!/^(-?[1-9][0-9]*)$/.test(item.step)
|
|
739
|
+
)
|
|
740
|
+
reject("Invalid bounded loop.", item.span);
|
|
741
|
+
const start = fold(item.start, this.scope, this.declarations.types);
|
|
742
|
+
const limit = fold(item.limit, this.scope, this.declarations.types);
|
|
743
|
+
const startRange = interval(
|
|
744
|
+
item.start,
|
|
745
|
+
this.declarations.types,
|
|
746
|
+
this.sourceScope,
|
|
747
|
+
this.ranges,
|
|
748
|
+
);
|
|
749
|
+
const limitRange = interval(
|
|
750
|
+
item.limit,
|
|
751
|
+
this.declarations.types,
|
|
752
|
+
this.sourceScope,
|
|
753
|
+
this.ranges,
|
|
754
|
+
);
|
|
755
|
+
if (!startRange || !limitRange)
|
|
756
|
+
reject("Loop bounds require finite integer ranges.", item.span);
|
|
757
|
+
const step = BigInt(item.step);
|
|
758
|
+
const proof = loopBounds(startRange, limitRange, step, item.exclusive);
|
|
759
|
+
const dynamic = start?.kind !== "integer" || limit?.kind !== "integer";
|
|
760
|
+
const first = proof.first,
|
|
761
|
+
last = proof.last,
|
|
762
|
+
count = proof.count;
|
|
763
|
+
if (
|
|
764
|
+
count > 1_000_000n ||
|
|
765
|
+
Number(count) !== item.maxIterations ||
|
|
766
|
+
!Number.isSafeInteger(item.maxIterations)
|
|
767
|
+
)
|
|
768
|
+
reject("Loop iteration proof differs from its bounds.", item.span);
|
|
769
|
+
if (item.counterScope === "existing") {
|
|
770
|
+
const terminal = dynamic
|
|
771
|
+
? (step > 0n ? proof.range[1] : proof.range[0]) + step
|
|
772
|
+
: first + count * step;
|
|
773
|
+
const [minimum, maximum] = integerRange(counterType);
|
|
774
|
+
if (count > 0n && (terminal < minimum || terminal > maximum))
|
|
775
|
+
reject("The existing loop counter overflows on termination.", item.span);
|
|
776
|
+
}
|
|
777
|
+
const evaluatedStart = this.expression(item.start, initialized, output);
|
|
778
|
+
const evaluatedLimit = this.expression(item.limit, initialized, output);
|
|
779
|
+
initialized.add(counter.id);
|
|
780
|
+
this.counters.add(counter.id);
|
|
781
|
+
this.ranges.set(counter.id, proof.range);
|
|
782
|
+
const nested = this.body(item.body, new Set(initialized), true);
|
|
783
|
+
this.counters.delete(counter.id);
|
|
784
|
+
this.ranges.delete(counter.id);
|
|
785
|
+
if (item.counterScope !== "existing") initialized.delete(counter.id);
|
|
786
|
+
output.push({
|
|
787
|
+
kind: "for",
|
|
788
|
+
counter: counter.id,
|
|
789
|
+
...(item.counterScope && { counterScope: item.counterScope }),
|
|
790
|
+
start: { kind: "integer", value: String(first) },
|
|
791
|
+
limit: { kind: "integer", value: String(last) },
|
|
792
|
+
...(dynamic && {
|
|
793
|
+
dynamic: {
|
|
794
|
+
start: evaluatedStart,
|
|
795
|
+
limit: evaluatedLimit,
|
|
796
|
+
...(item.exclusive && { exclusive: item.exclusive }),
|
|
797
|
+
},
|
|
798
|
+
}),
|
|
799
|
+
step: item.step,
|
|
800
|
+
maxIterations: item.maxIterations,
|
|
801
|
+
body: nested.body,
|
|
802
|
+
span: item.span,
|
|
803
|
+
});
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
case "return": {
|
|
807
|
+
if (item.value?.type !== this.fn.result)
|
|
808
|
+
reject("Return differs from the function signature.", item.span);
|
|
809
|
+
const value = item.value && this.expression(item.value, initialized, output);
|
|
810
|
+
output.push({ kind: "return", span: item.span, ...(value && { value }) });
|
|
811
|
+
flow = "return";
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
case "break":
|
|
815
|
+
case "continue":
|
|
816
|
+
if (!inLoop) reject("Loop control appears outside a bounded loop.", item.span);
|
|
817
|
+
output.push({ kind: item.kind, span: item.span });
|
|
818
|
+
flow = "loop";
|
|
819
|
+
break;
|
|
820
|
+
default:
|
|
821
|
+
reject("Unknown PLC statement.");
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return { body: output, initialized, flow };
|
|
825
|
+
}
|
|
826
|
+
}
|