@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
package/dist/ir.js
ADDED
|
@@ -0,0 +1,1888 @@
|
|
|
1
|
+
// packages/compiler/ir/diagnostics.ts
|
|
2
|
+
function error(code, message, span) {
|
|
3
|
+
return { origin: "plc", code, severity: "error", message, ...span && { span } };
|
|
4
|
+
}
|
|
5
|
+
function hasErrors(diagnostics) {
|
|
6
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
7
|
+
}
|
|
8
|
+
function formatDiagnostic(diagnostic, source) {
|
|
9
|
+
const { span } = diagnostic;
|
|
10
|
+
const location = span ? `${span.file}:${span.line}:${span.column}: ` : "";
|
|
11
|
+
let result = `${location}${diagnostic.code}: ${diagnostic.message}`;
|
|
12
|
+
if (source !== undefined && span) {
|
|
13
|
+
const line = source.split(/\r?\n/)[span.line - 1];
|
|
14
|
+
if (line !== undefined) {
|
|
15
|
+
const width = Math.max(1, Math.min(span.end - span.start, line.length - span.column + 2));
|
|
16
|
+
result += `
|
|
17
|
+
${span.line} | ${line}
|
|
18
|
+
${" ".repeat(String(span.line).length + 3 + span.column - 1)}${"^".repeat(width)}`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
for (const related of diagnostic.related ?? []) {
|
|
22
|
+
result += `
|
|
23
|
+
${related.span.file}:${related.span.line}:${related.span.column}: ${related.message}`;
|
|
24
|
+
}
|
|
25
|
+
for (const note of diagnostic.notes ?? [])
|
|
26
|
+
result += `
|
|
27
|
+
${note}`;
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
// packages/compiler/ir/types.ts
|
|
31
|
+
var instancedBlock = (id, types) => {
|
|
32
|
+
const type = types.get(id);
|
|
33
|
+
const element = type?.kind === "array" ? types.get(type.element) : type;
|
|
34
|
+
return element?.kind === "block" ? element : undefined;
|
|
35
|
+
};
|
|
36
|
+
var primitiveTypes = [
|
|
37
|
+
{ id: "BOOL", kind: "bool" },
|
|
38
|
+
...[
|
|
39
|
+
["SINT", "USINT", "BYTE", 8],
|
|
40
|
+
["INT", "UINT", "WORD", 16],
|
|
41
|
+
["DINT", "UDINT", "DWORD", 32],
|
|
42
|
+
["LINT", "ULINT", "LWORD", 64]
|
|
43
|
+
].flatMap(([signed, unsigned, bitstring, bits]) => [
|
|
44
|
+
{ id: signed, kind: "integer", bits, signed: true },
|
|
45
|
+
{ id: unsigned, kind: "integer", bits, signed: false },
|
|
46
|
+
{ id: bitstring, kind: "bits", bits }
|
|
47
|
+
]),
|
|
48
|
+
{ id: "REAL", kind: "float", bits: 32 },
|
|
49
|
+
{ id: "LREAL", kind: "float", bits: 64 },
|
|
50
|
+
{ id: "TIME", kind: "duration" },
|
|
51
|
+
{ id: "DATE", kind: "calendar", domain: "date" },
|
|
52
|
+
{ id: "TIME_OF_DAY", kind: "calendar", domain: "timeOfDay" },
|
|
53
|
+
{ id: "DATE_AND_TIME", kind: "calendar", domain: "dateTime" }
|
|
54
|
+
];
|
|
55
|
+
function integerRange(type) {
|
|
56
|
+
const signed = type.kind === "integer" && type.signed;
|
|
57
|
+
const limit = 1n << BigInt(type.bits - (signed ? 1 : 0));
|
|
58
|
+
return signed ? [-limit, limit - 1n] : [0n, limit - 1n];
|
|
59
|
+
}
|
|
60
|
+
// packages/compiler/ir/model.ts
|
|
61
|
+
var comparisonOperators = new Set([
|
|
62
|
+
"equal",
|
|
63
|
+
"notEqual",
|
|
64
|
+
"less",
|
|
65
|
+
"lessEqual",
|
|
66
|
+
"greater",
|
|
67
|
+
"greaterEqual"
|
|
68
|
+
]);
|
|
69
|
+
var timer = (input, output) => ({
|
|
70
|
+
inputs: [
|
|
71
|
+
[input, "IN", "BOOL"],
|
|
72
|
+
["duration", "PT", "TIME"]
|
|
73
|
+
],
|
|
74
|
+
outputs: [
|
|
75
|
+
[output, "Q", "BOOL"],
|
|
76
|
+
["elapsed", "ET", "TIME"]
|
|
77
|
+
]
|
|
78
|
+
});
|
|
79
|
+
var standardBlocks = {
|
|
80
|
+
OnDelayTimer: { native: "TON", ...timer("enabled", "done") },
|
|
81
|
+
OffDelayTimer: { native: "TOF", ...timer("enabled", "active") },
|
|
82
|
+
PulseTimer: { native: "TP", ...timer("start", "active") },
|
|
83
|
+
RisingEdge: {
|
|
84
|
+
native: "R_TRIG",
|
|
85
|
+
inputs: [["signal", "CLK", "BOOL"]],
|
|
86
|
+
outputs: [["rose", "Q", "BOOL"]]
|
|
87
|
+
},
|
|
88
|
+
FallingEdge: {
|
|
89
|
+
native: "F_TRIG",
|
|
90
|
+
inputs: [["signal", "CLK", "BOOL"]],
|
|
91
|
+
outputs: [["fell", "Q", "BOOL"]]
|
|
92
|
+
},
|
|
93
|
+
Blinker: {
|
|
94
|
+
native: "Util.BLINK",
|
|
95
|
+
inputs: [
|
|
96
|
+
["enabled", "ENABLE", "BOOL"],
|
|
97
|
+
["offTime", "TIMELOW", "TIME"],
|
|
98
|
+
["onTime", "TIMEHIGH", "TIME"]
|
|
99
|
+
],
|
|
100
|
+
outputs: [["on", "OUT", "BOOL"]]
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
// packages/compiler/ir/core-model.ts
|
|
104
|
+
var verified = Symbol("verified core");
|
|
105
|
+
|
|
106
|
+
// packages/compiler/ir/scalars.ts
|
|
107
|
+
function integerType(type) {
|
|
108
|
+
return type.kind === "integer" || type.kind === "bits";
|
|
109
|
+
}
|
|
110
|
+
function floatMaximum(type) {
|
|
111
|
+
return type.bits === 32 ? Math.fround((2 - 2 ** -23) * 2 ** 127) : Number.MAX_VALUE;
|
|
112
|
+
}
|
|
113
|
+
function saturateFloat(value, type) {
|
|
114
|
+
const maximum = floatMaximum(type);
|
|
115
|
+
const clipped = value < -maximum ? -maximum : value > maximum ? maximum : value;
|
|
116
|
+
return type.bits === 32 ? Math.fround(clipped) : clipped;
|
|
117
|
+
}
|
|
118
|
+
function parseInteger(text) {
|
|
119
|
+
const clean = text.replaceAll("_", "").replace(/n$/, "");
|
|
120
|
+
const negative = clean.startsWith("-");
|
|
121
|
+
const unsigned = negative ? clean.slice(1) : clean;
|
|
122
|
+
if (/^0[xob]/i.test(unsigned))
|
|
123
|
+
return (negative ? -1n : 1n) * BigInt(unsigned);
|
|
124
|
+
const match = /^(\d+)(?:\.(\d*))?(?:e([+-]?\d+))?$/i.exec(unsigned);
|
|
125
|
+
if (!match)
|
|
126
|
+
throw new Error("Expected an exact integer literal.");
|
|
127
|
+
const fraction = match[2] ?? "";
|
|
128
|
+
const exponent = Number(match[3] ?? 0) - fraction.length;
|
|
129
|
+
if (!Number.isSafeInteger(exponent) || Math.abs(exponent) > 400)
|
|
130
|
+
throw new Error("Integer literal exponent is out of range.");
|
|
131
|
+
let value = BigInt(match[1] + fraction);
|
|
132
|
+
if (exponent >= 0)
|
|
133
|
+
value *= 10n ** BigInt(exponent);
|
|
134
|
+
else {
|
|
135
|
+
const divisor = 10n ** BigInt(-exponent);
|
|
136
|
+
if (value % divisor)
|
|
137
|
+
throw new Error("Integer literals cannot have a fractional part.");
|
|
138
|
+
value /= divisor;
|
|
139
|
+
}
|
|
140
|
+
return negative ? -value : value;
|
|
141
|
+
}
|
|
142
|
+
function constantFits(value, type) {
|
|
143
|
+
if (type.kind === "bool")
|
|
144
|
+
return value.kind === "bool" && typeof value.value === "boolean";
|
|
145
|
+
if (integerType(type)) {
|
|
146
|
+
if (value.kind !== "integer" || !/^(?:0|-?[1-9][0-9]*)$/.test(value.value))
|
|
147
|
+
return false;
|
|
148
|
+
const [minimum, maximum] = integerRange(type), number = BigInt(value.value);
|
|
149
|
+
return number >= minimum && number <= maximum;
|
|
150
|
+
}
|
|
151
|
+
if (type.kind === "float")
|
|
152
|
+
return value.kind === "float" && Number.isFinite(value.value) && (type.bits === 64 || Object.is(Math.fround(value.value), value.value));
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
function canAssign(source, destination, value) {
|
|
156
|
+
if (value) {
|
|
157
|
+
if (source.kind !== destination.kind || !constantFits(value, destination))
|
|
158
|
+
return false;
|
|
159
|
+
if (source.kind === "float" && destination.kind === "float" && source.bits > destination.bits && value.kind === "float")
|
|
160
|
+
return Object.is(Math.fround(value.value), value.value);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
if (source.id === destination.id)
|
|
164
|
+
return true;
|
|
165
|
+
if (source.kind === "integer" && destination.kind === "integer") {
|
|
166
|
+
const [fromMin, fromMax] = integerRange(source), [toMin, toMax] = integerRange(destination);
|
|
167
|
+
return fromMin >= toMin && fromMax <= toMax;
|
|
168
|
+
}
|
|
169
|
+
return source.kind === "float" && destination.kind === "float" && source.bits <= destination.bits;
|
|
170
|
+
}
|
|
171
|
+
function fold(expression, variables, types) {
|
|
172
|
+
if (expression.kind === "constant")
|
|
173
|
+
return expression.value;
|
|
174
|
+
if (expression.kind === "copy")
|
|
175
|
+
return fold(expression.operand, variables, types);
|
|
176
|
+
if (expression.kind === "length") {
|
|
177
|
+
const type = types?.get(expression.operand.type);
|
|
178
|
+
const receiver = expression.operand.kind === "load" ? variables.get(expression.operand.place.root) : undefined;
|
|
179
|
+
const pure = expression.operand.kind === "load" && expression.operand.place.path.every((segment) => segment.kind === "field") && Boolean(receiver?.initial || receiver?.storage === "input" || receiver?.storage === "output");
|
|
180
|
+
if (type?.kind === "array" && (pure || fold(expression.operand, variables, types)))
|
|
181
|
+
return { kind: "integer", value: String(type.length) };
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (expression.kind === "record") {
|
|
185
|
+
const fields = Object.create(null);
|
|
186
|
+
for (const [name, operand] of Object.entries(expression.fields)) {
|
|
187
|
+
const value = fold(operand, variables, types);
|
|
188
|
+
if (!value)
|
|
189
|
+
return;
|
|
190
|
+
fields[name] = value;
|
|
191
|
+
}
|
|
192
|
+
return { kind: "record", fields };
|
|
193
|
+
}
|
|
194
|
+
if (expression.kind === "array") {
|
|
195
|
+
const items = expression.items.map((item) => fold(item, variables, types));
|
|
196
|
+
return items.every((item) => item !== undefined) ? { kind: "array", items } : undefined;
|
|
197
|
+
}
|
|
198
|
+
if (expression.kind === "load" && !expression.place.path.length) {
|
|
199
|
+
const variable = variables.get(expression.place.root);
|
|
200
|
+
return variable?.storage === "constant" ? variable.initial : undefined;
|
|
201
|
+
}
|
|
202
|
+
if (expression.kind === "convert") {
|
|
203
|
+
const value = fold(expression.operand, variables, types);
|
|
204
|
+
const target = types?.get(expression.type);
|
|
205
|
+
if (value?.kind === "integer" && target) {
|
|
206
|
+
if (integerType(target))
|
|
207
|
+
return {
|
|
208
|
+
kind: "integer",
|
|
209
|
+
value: String(expression.policy === "exact" ? BigInt(value.value) : applyIntegerPolicy(BigInt(value.value), target, expression.policy))
|
|
210
|
+
};
|
|
211
|
+
if (target.kind === "float")
|
|
212
|
+
return {
|
|
213
|
+
kind: "float",
|
|
214
|
+
value: target.bits === 32 ? Math.fround(Number(value.value)) : Number(value.value)
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (value?.kind === "float" && target?.kind === "float")
|
|
218
|
+
return {
|
|
219
|
+
kind: "float",
|
|
220
|
+
value: expression.policy === "saturate" ? saturateFloat(value.value, target) : target.bits === 32 ? Math.fround(value.value) : value.value
|
|
221
|
+
};
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (expression.kind === "unary") {
|
|
225
|
+
const value = fold(expression.operand, variables, types);
|
|
226
|
+
if (value?.kind === "duration" && expression.operator === "timeToMilliseconds")
|
|
227
|
+
return { kind: "integer", value: String(BigInt(value.nanoseconds) / 1000000n) };
|
|
228
|
+
if (value?.kind === "bool" && expression.operator === "not")
|
|
229
|
+
return { kind: "bool", value: !value.value };
|
|
230
|
+
if (value?.kind === "integer" && expression.operator === "bitNot") {
|
|
231
|
+
const target = types?.get(expression.type);
|
|
232
|
+
if (target?.kind === "bits")
|
|
233
|
+
return {
|
|
234
|
+
kind: "integer",
|
|
235
|
+
value: String(applyIntegerPolicy(~BigInt(value.value), target, "wrap"))
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (value?.kind === "integer" && expression.operator === "negate")
|
|
239
|
+
return { kind: "integer", value: String(-BigInt(value.value)) };
|
|
240
|
+
if (value?.kind === "float" && expression.operator === "negate")
|
|
241
|
+
return { kind: "float", value: -value.value };
|
|
242
|
+
}
|
|
243
|
+
if (expression.kind === "binary") {
|
|
244
|
+
const left = fold(expression.left, variables, types), right = fold(expression.right, variables, types);
|
|
245
|
+
if (!left || !right || left.kind !== right.kind)
|
|
246
|
+
return;
|
|
247
|
+
if (left.kind === "bool" && right.kind === "bool") {
|
|
248
|
+
if (expression.operator === "and")
|
|
249
|
+
return { kind: "bool", value: left.value && right.value };
|
|
250
|
+
if (expression.operator === "or")
|
|
251
|
+
return { kind: "bool", value: left.value || right.value };
|
|
252
|
+
if (expression.operator === "equal")
|
|
253
|
+
return { kind: "bool", value: left.value === right.value };
|
|
254
|
+
if (expression.operator === "notEqual")
|
|
255
|
+
return { kind: "bool", value: left.value !== right.value };
|
|
256
|
+
}
|
|
257
|
+
if (left.kind === "integer" && right.kind === "integer" || left.kind === "float" && right.kind === "float" || left.kind === "duration" && right.kind === "duration" || left.kind === "calendar" && right.kind === "calendar" || left.kind === "string" && right.kind === "string") {
|
|
258
|
+
const a = left.kind === "integer" ? BigInt(left.value) : left.kind === "duration" ? BigInt(left.nanoseconds) : left.value;
|
|
259
|
+
const b = right.kind === "integer" ? BigInt(right.value) : right.kind === "duration" ? BigInt(right.nanoseconds) : right.value;
|
|
260
|
+
switch (expression.operator) {
|
|
261
|
+
case "equal":
|
|
262
|
+
return { kind: "bool", value: a === b };
|
|
263
|
+
case "notEqual":
|
|
264
|
+
return { kind: "bool", value: a !== b };
|
|
265
|
+
case "less":
|
|
266
|
+
return { kind: "bool", value: a < b };
|
|
267
|
+
case "lessEqual":
|
|
268
|
+
return { kind: "bool", value: a <= b };
|
|
269
|
+
case "greater":
|
|
270
|
+
return { kind: "bool", value: a > b };
|
|
271
|
+
case "greaterEqual":
|
|
272
|
+
return { kind: "bool", value: a >= b };
|
|
273
|
+
}
|
|
274
|
+
if (left.kind !== "integer" && left.kind !== "float")
|
|
275
|
+
return;
|
|
276
|
+
if (typeof a === "bigint" && typeof b === "bigint") {
|
|
277
|
+
let value;
|
|
278
|
+
switch (expression.operator) {
|
|
279
|
+
case "add":
|
|
280
|
+
value = a + b;
|
|
281
|
+
break;
|
|
282
|
+
case "subtract":
|
|
283
|
+
value = a - b;
|
|
284
|
+
break;
|
|
285
|
+
case "multiply":
|
|
286
|
+
value = a * b;
|
|
287
|
+
break;
|
|
288
|
+
case "divide":
|
|
289
|
+
if (b === 0n)
|
|
290
|
+
return;
|
|
291
|
+
value = a / b;
|
|
292
|
+
break;
|
|
293
|
+
case "remainder":
|
|
294
|
+
if (b === 0n)
|
|
295
|
+
return;
|
|
296
|
+
value = a % b;
|
|
297
|
+
break;
|
|
298
|
+
case "bitAnd":
|
|
299
|
+
value = a & b;
|
|
300
|
+
break;
|
|
301
|
+
case "bitOr":
|
|
302
|
+
value = a | b;
|
|
303
|
+
break;
|
|
304
|
+
case "bitXor":
|
|
305
|
+
value = a ^ b;
|
|
306
|
+
break;
|
|
307
|
+
case "shiftLeft":
|
|
308
|
+
if (b < 0n || b > 63n)
|
|
309
|
+
return;
|
|
310
|
+
value = a << b;
|
|
311
|
+
break;
|
|
312
|
+
case "shiftRight":
|
|
313
|
+
if (b < 0n || b > 63n)
|
|
314
|
+
return;
|
|
315
|
+
value = a >> b;
|
|
316
|
+
break;
|
|
317
|
+
default:
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const target = types?.get(expression.type);
|
|
321
|
+
if (target && integerType(target) && (expression.policy || target.kind === "bits"))
|
|
322
|
+
value = applyIntegerPolicy(value, target, expression.policy ?? "wrap");
|
|
323
|
+
return { kind: "integer", value: String(value) };
|
|
324
|
+
}
|
|
325
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
326
|
+
let value;
|
|
327
|
+
switch (expression.operator) {
|
|
328
|
+
case "add":
|
|
329
|
+
value = a + b;
|
|
330
|
+
break;
|
|
331
|
+
case "subtract":
|
|
332
|
+
value = a - b;
|
|
333
|
+
break;
|
|
334
|
+
case "multiply":
|
|
335
|
+
value = a * b;
|
|
336
|
+
break;
|
|
337
|
+
case "divide":
|
|
338
|
+
value = a / b;
|
|
339
|
+
break;
|
|
340
|
+
case "remainder":
|
|
341
|
+
value = a % b;
|
|
342
|
+
break;
|
|
343
|
+
default:
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
kind: "float",
|
|
348
|
+
value: types?.get(expression.type)?.kind === "float" && expression.type === "REAL" ? Math.fround(value) : value
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
function interval(expression, types, variables, ranges = new Map) {
|
|
356
|
+
if (expression.kind === "length") {
|
|
357
|
+
const array = types.get(expression.operand.type);
|
|
358
|
+
if (array?.kind === "array")
|
|
359
|
+
return [BigInt(array.length), BigInt(array.length)];
|
|
360
|
+
}
|
|
361
|
+
const value = fold(expression, variables, types);
|
|
362
|
+
if (value?.kind === "integer")
|
|
363
|
+
return [BigInt(value.value), BigInt(value.value)];
|
|
364
|
+
const type = types.get(expression.type);
|
|
365
|
+
if (!type || !integerType(type))
|
|
366
|
+
return;
|
|
367
|
+
if (expression.kind === "load")
|
|
368
|
+
return ranges.get(expression.place.root) ?? integerRange(type);
|
|
369
|
+
if (expression.kind === "convert")
|
|
370
|
+
return expression.policy === "exact" ? interval(expression.operand, types, variables, ranges) : integerRange(type);
|
|
371
|
+
if (expression.kind === "unary" && expression.operator === "negate") {
|
|
372
|
+
const operand = interval(expression.operand, types, variables, ranges);
|
|
373
|
+
return operand && [-operand[1], -operand[0]];
|
|
374
|
+
}
|
|
375
|
+
if (expression.kind === "binary" && !expression.policy) {
|
|
376
|
+
const a = interval(expression.left, types, variables, ranges), b = interval(expression.right, types, variables, ranges);
|
|
377
|
+
if (!a || !b)
|
|
378
|
+
return;
|
|
379
|
+
const [minimum, maximum] = integerRange(type);
|
|
380
|
+
const unwrapped = (low, high) => low < minimum || high > maximum ? [minimum, maximum] : [low, high];
|
|
381
|
+
switch (expression.operator) {
|
|
382
|
+
case "add":
|
|
383
|
+
return unwrapped(a[0] + b[0], a[1] + b[1]);
|
|
384
|
+
case "subtract":
|
|
385
|
+
return unwrapped(a[0] - b[1], a[1] - b[0]);
|
|
386
|
+
case "multiply": {
|
|
387
|
+
const values = [a[0] * b[0], a[0] * b[1], a[1] * b[0], a[1] * b[1]];
|
|
388
|
+
return unwrapped(values.reduce((x, y) => x < y ? x : y), values.reduce((x, y) => x > y ? x : y));
|
|
389
|
+
}
|
|
390
|
+
case "divide": {
|
|
391
|
+
if (b[0] <= 0n && b[1] >= 0n)
|
|
392
|
+
return;
|
|
393
|
+
const values = [a[0] / b[0], a[0] / b[1], a[1] / b[0], a[1] / b[1]];
|
|
394
|
+
return [values.reduce((x, y) => x < y ? x : y), values.reduce((x, y) => x > y ? x : y)];
|
|
395
|
+
}
|
|
396
|
+
case "remainder": {
|
|
397
|
+
if (b[0] <= 0n && b[1] >= 0n)
|
|
398
|
+
return;
|
|
399
|
+
const maximum = [b[0], b[1]].map((n) => n < 0n ? -n : n).reduce((x, y) => x > y ? x : y) - 1n;
|
|
400
|
+
return [a[0] < 0n ? -maximum : 0n, a[1] > 0n ? maximum : 0n];
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return integerRange(type);
|
|
405
|
+
}
|
|
406
|
+
function loopIterations(first, last, step) {
|
|
407
|
+
if (step > 0n)
|
|
408
|
+
return last >= first ? (last - first) / step + 1n : 0n;
|
|
409
|
+
return last <= first ? (first - last) / -step + 1n : 0n;
|
|
410
|
+
}
|
|
411
|
+
function loopBounds(start, limit, step, exclusive = false) {
|
|
412
|
+
const first = step > 0n ? start[0] : start[1];
|
|
413
|
+
const last = step > 0n ? limit[1] - (exclusive ? 1n : 0n) : limit[0] + (exclusive ? 1n : 0n);
|
|
414
|
+
const count = loopIterations(first, last, step);
|
|
415
|
+
const range = first <= last ? [first, last] : [last, first];
|
|
416
|
+
return { first, last, count, range };
|
|
417
|
+
}
|
|
418
|
+
function narrowRanges(condition, truth, types, variables, ranges) {
|
|
419
|
+
const narrowed = new Map(ranges);
|
|
420
|
+
if (condition.kind !== "binary")
|
|
421
|
+
return narrowed;
|
|
422
|
+
if (condition.operator === "and")
|
|
423
|
+
return truth ? narrowRanges(condition.right, truth, types, variables, narrowRanges(condition.left, truth, types, variables, ranges)) : narrowed;
|
|
424
|
+
if (condition.left.kind !== "load" || condition.left.place.path.length)
|
|
425
|
+
return narrowed;
|
|
426
|
+
const root = condition.left.place.root, variable = variables.get(root), type = types.get(condition.left.type), fixed = variable && (variable.storage === "parameter" && !variable.mode || variable.storage === "local" && !variable.mutable), current = ranges.get(root) ?? (fixed && type && integerType(type) ? integerRange(type) : undefined), bound = interval(condition.right, types, variables, ranges);
|
|
427
|
+
if (!current || !bound || bound[0] !== bound[1])
|
|
428
|
+
return narrowed;
|
|
429
|
+
const value = bound[0];
|
|
430
|
+
let lower = current[0], upper = current[1];
|
|
431
|
+
switch (condition.operator) {
|
|
432
|
+
case "less":
|
|
433
|
+
if (truth)
|
|
434
|
+
upper = value - 1n < upper ? value - 1n : upper;
|
|
435
|
+
else
|
|
436
|
+
lower = value > lower ? value : lower;
|
|
437
|
+
break;
|
|
438
|
+
case "lessEqual":
|
|
439
|
+
if (truth)
|
|
440
|
+
upper = value < upper ? value : upper;
|
|
441
|
+
else
|
|
442
|
+
lower = value + 1n > lower ? value + 1n : lower;
|
|
443
|
+
break;
|
|
444
|
+
case "greater":
|
|
445
|
+
if (truth)
|
|
446
|
+
lower = value + 1n > lower ? value + 1n : lower;
|
|
447
|
+
else
|
|
448
|
+
upper = value < upper ? value : upper;
|
|
449
|
+
break;
|
|
450
|
+
case "greaterEqual":
|
|
451
|
+
if (truth)
|
|
452
|
+
lower = value > lower ? value : lower;
|
|
453
|
+
else
|
|
454
|
+
upper = value - 1n < upper ? value - 1n : upper;
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
if (lower <= upper)
|
|
458
|
+
narrowed.set(root, [lower, upper]);
|
|
459
|
+
return narrowed;
|
|
460
|
+
}
|
|
461
|
+
function wrapsNatively(expression) {
|
|
462
|
+
return expression.kind === "binary" && !expression.policy && (expression.operator === "add" || expression.operator === "subtract" || expression.operator === "multiply");
|
|
463
|
+
}
|
|
464
|
+
var lowest = (a, b) => a < b ? a : b;
|
|
465
|
+
var highest = (a, b) => a > b ? a : b;
|
|
466
|
+
function valueRange(expression, types, variables, ranges, functions) {
|
|
467
|
+
if (expression.kind !== "call")
|
|
468
|
+
return interval(expression, types, variables, ranges);
|
|
469
|
+
const helper = functions.get(expression.function)?.bounds;
|
|
470
|
+
return helper && mathHelperRange(helper, expression.arguments.map((argument) => valueRange(argument, types, variables, ranges, functions)));
|
|
471
|
+
}
|
|
472
|
+
function mathHelperRange(helper, [first, second, third]) {
|
|
473
|
+
if (!first || !second)
|
|
474
|
+
return;
|
|
475
|
+
if (helper === "min")
|
|
476
|
+
return [lowest(first[0], second[0]), lowest(first[1], second[1])];
|
|
477
|
+
if (helper === "max")
|
|
478
|
+
return [highest(first[0], second[0]), highest(first[1], second[1])];
|
|
479
|
+
if (!third)
|
|
480
|
+
return;
|
|
481
|
+
return [lowest(second[0], third[0]), highest(second[1], third[1])];
|
|
482
|
+
}
|
|
483
|
+
function applyIntegerPolicy(value, type, policy) {
|
|
484
|
+
const [minimum, maximum] = integerRange(type);
|
|
485
|
+
if (policy === "saturate")
|
|
486
|
+
return value < minimum ? minimum : value > maximum ? maximum : value;
|
|
487
|
+
return type.kind === "integer" && type.signed ? BigInt.asIntN(type.bits, value) : BigInt.asUintN(type.bits, value);
|
|
488
|
+
}
|
|
489
|
+
function scalarIssue(expression, types, variables, ranges = new Map) {
|
|
490
|
+
const type = types.get(expression.type);
|
|
491
|
+
if (!type || !["bool", "integer", "bits", "float"].includes(type.kind))
|
|
492
|
+
return "Unknown scalar expression type.";
|
|
493
|
+
if (expression.kind === "constant")
|
|
494
|
+
return constantFits(expression.value, type) ? undefined : "Literal is outside its PLC type or has the wrong domain.";
|
|
495
|
+
if (expression.kind === "load" || expression.kind === "call")
|
|
496
|
+
return;
|
|
497
|
+
if ("operand" in expression && !types.has(expression.operand.type) || "left" in expression && (!types.has(expression.left.type) || !types.has(expression.right.type)))
|
|
498
|
+
return "Unknown operand type.";
|
|
499
|
+
const operand = (value) => types.get(value.type);
|
|
500
|
+
if (expression.kind === "convert") {
|
|
501
|
+
if (!["exact", "wrap", "saturate"].includes(expression.policy))
|
|
502
|
+
return "Unknown numeric conversion policy.";
|
|
503
|
+
const source = operand(expression.operand), value = fold(expression.operand, variables, types);
|
|
504
|
+
if (source.kind === "bool" || type.kind === "bool")
|
|
505
|
+
return "Numeric conversions cannot use BOOL.";
|
|
506
|
+
if (expression.policy === "exact") {
|
|
507
|
+
if (canAssign(source, type, value) || value && integerType(source) && integerType(type) && constantFits(value, type))
|
|
508
|
+
return;
|
|
509
|
+
if (integerType(source) && type.kind === "float") {
|
|
510
|
+
if (value?.kind === "integer") {
|
|
511
|
+
const converted = type.bits === 32 ? Math.fround(Number(value.value)) : Number(value.value);
|
|
512
|
+
if (Number.isFinite(converted) && Number.isInteger(converted) && BigInt(converted) === BigInt(value.value))
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const [minimum, maximum] = interval(expression.operand, types, variables, ranges) ?? integerRange(source);
|
|
516
|
+
const limit = 1n << BigInt(type.bits === 32 ? 24 : 53);
|
|
517
|
+
if (minimum >= -limit && maximum <= limit)
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (integerType(source) && integerType(type)) {
|
|
521
|
+
const [fromMin, fromMax] = interval(expression.operand, types, variables, ranges) ?? integerRange(source), [toMin, toMax] = integerRange(type);
|
|
522
|
+
if (fromMin >= toMin && fromMax <= toMax)
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
return "Exact conversion cannot be proved lossless.";
|
|
526
|
+
}
|
|
527
|
+
if (expression.policy === "saturate" && source.kind === "float" && type.kind === "float")
|
|
528
|
+
return;
|
|
529
|
+
return integerType(source) && integerType(type) ? undefined : expression.policy === "saturate" ? "Saturate conversions require two integer/bit-string operands or two floating operands." : "Wrap conversions require integer or bit-string operands.";
|
|
530
|
+
}
|
|
531
|
+
if (expression.kind === "unary") {
|
|
532
|
+
const source = operand(expression.operand);
|
|
533
|
+
if (expression.operator === "clock")
|
|
534
|
+
return type.id === "DINT" ? undefined : "now() returns DINT milliseconds.";
|
|
535
|
+
if (expression.operator === "timeToMilliseconds")
|
|
536
|
+
return source.id === "TIME" && source.kind === "duration" && type.id === "UDINT" ? undefined : "timeToMilliseconds requires TIME and returns UDINT.";
|
|
537
|
+
if (expression.operator === "not")
|
|
538
|
+
return type.kind === "bool" && source.kind === "bool" ? undefined : "Logical negation requires BOOL.";
|
|
539
|
+
if (expression.operator === "bitNot")
|
|
540
|
+
return type.kind === "bits" && source.id === type.id ? undefined : "Bitwise complement requires a bit string.";
|
|
541
|
+
if (source.id !== type.id || !(type.kind === "integer" || type.kind === "float"))
|
|
542
|
+
return "Numeric negation requires one arithmetic type.";
|
|
543
|
+
} else if (expression.kind === "binary") {
|
|
544
|
+
const left = operand(expression.left), right = operand(expression.right);
|
|
545
|
+
if (expression.operator === "and" || expression.operator === "or")
|
|
546
|
+
return type.kind === "bool" && left.kind === "bool" && right.kind === "bool" ? undefined : "Logical operators require BOOL.";
|
|
547
|
+
if (comparisonOperators.has(expression.operator))
|
|
548
|
+
return type.kind === "bool" && left.id === right.id && ["bool", "integer", "bits", "float"].includes(left.kind) && (left.kind !== "bool" || ["equal", "notEqual"].includes(expression.operator)) ? undefined : "Comparison operands need the same PLC domain.";
|
|
549
|
+
if (["bitAnd", "bitOr", "bitXor", "shiftLeft", "shiftRight"].includes(expression.operator)) {
|
|
550
|
+
if (type.kind !== "bits" || left.id !== type.id)
|
|
551
|
+
return "Bitwise operators require bit strings.";
|
|
552
|
+
if (expression.operator === "shiftLeft" || expression.operator === "shiftRight") {
|
|
553
|
+
const count = fold(expression.right, variables, types);
|
|
554
|
+
return count?.kind === "integer" && BigInt(count.value) >= 0n && BigInt(count.value) < BigInt(type.bits) ? undefined : "Shift count must be a compile-time integer within the bit width.";
|
|
555
|
+
}
|
|
556
|
+
return right.id === type.id ? undefined : "Bit-string widths differ.";
|
|
557
|
+
}
|
|
558
|
+
if (left.id !== type.id || right.id !== type.id || type.kind !== "integer" && type.kind !== "float")
|
|
559
|
+
return "Arithmetic operands need one integer or floating type.";
|
|
560
|
+
if (expression.policy)
|
|
561
|
+
return type.kind === "integer" && ["add", "subtract", "multiply"].includes(expression.operator) ? undefined : "Explicit arithmetic policies require integer addition, subtraction or multiplication.";
|
|
562
|
+
if ((expression.operator === "divide" || expression.operator === "remainder") && type.kind === "integer") {
|
|
563
|
+
const divisor = interval(expression.right, types, variables, ranges);
|
|
564
|
+
if (!divisor || divisor[0] <= 0n && divisor[1] >= 0n)
|
|
565
|
+
return "Division requires a proved nonzero divisor.";
|
|
566
|
+
const dividend = interval(expression.left, types, variables, ranges);
|
|
567
|
+
const [minimum] = integerRange(type);
|
|
568
|
+
if (type.signed && dividend && dividend[0] <= minimum && divisor[0] <= -1n && divisor[1] >= -1n)
|
|
569
|
+
return "Signed minimum divided by -1 cannot be represented.";
|
|
570
|
+
}
|
|
571
|
+
} else
|
|
572
|
+
return "Expression has not been reduced to a scalar operation.";
|
|
573
|
+
if (integerType(type)) {
|
|
574
|
+
const result = interval(expression, types, variables, ranges), [minimum, maximum] = integerRange(type);
|
|
575
|
+
if (!(wrapsNatively(expression) && !fold(expression, variables, types)) && (!result || result[0] < minimum || result[1] > maximum))
|
|
576
|
+
return "Arithmetic may overflow; widen the result or use an explicit wrap/saturate operation.";
|
|
577
|
+
} else if (type.kind === "float") {
|
|
578
|
+
const result = fold(expression, variables, types);
|
|
579
|
+
if (result)
|
|
580
|
+
return constantFits(result, type) ? undefined : "Floating result is not finite or representable.";
|
|
581
|
+
if (expression.kind === "binary") {
|
|
582
|
+
const lhs = fold(expression.left, variables, types);
|
|
583
|
+
const rhs = fold(expression.right, variables, types);
|
|
584
|
+
if (["add", "subtract"].includes(expression.operator) && (lhs?.kind === "float" || rhs?.kind === "float")) {
|
|
585
|
+
const maximum = floatMaximum(type);
|
|
586
|
+
const finite = [-maximum, maximum].every((endpoint) => {
|
|
587
|
+
const a = lhs?.kind === "float" ? lhs.value : endpoint;
|
|
588
|
+
const b = rhs?.kind === "float" ? rhs.value : endpoint;
|
|
589
|
+
const value = expression.operator === "add" ? a + b : a - b;
|
|
590
|
+
return Number.isFinite(type.bits === 32 ? Math.fround(value) : value);
|
|
591
|
+
});
|
|
592
|
+
if (finite)
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (rhs?.kind === "float" && rhs.value === 1 && ["multiply", "divide"].includes(expression.operator))
|
|
596
|
+
return;
|
|
597
|
+
} else if (expression.kind === "unary")
|
|
598
|
+
return;
|
|
599
|
+
return "Floating arithmetic must prove a finite result.";
|
|
600
|
+
}
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// packages/compiler/ir/domains.ts
|
|
605
|
+
function durationLiteral(text, scaleNanoseconds) {
|
|
606
|
+
const clean = text.replaceAll("_", "").replace(/^(-?)\./, (_, sign) => `${sign}0.`);
|
|
607
|
+
const match = /^(-?)(\d+)(?:\.(\d*))?(?:e([+-]?\d+))?$/i.exec(clean);
|
|
608
|
+
if (!match)
|
|
609
|
+
return;
|
|
610
|
+
const fraction = match[3] ?? "", exponent = Number(match[4] ?? 0) - fraction.length;
|
|
611
|
+
if (!Number.isSafeInteger(exponent) || Math.abs(exponent) > 400)
|
|
612
|
+
return;
|
|
613
|
+
let value = BigInt(match[2] + fraction) * scaleNanoseconds;
|
|
614
|
+
if (exponent >= 0)
|
|
615
|
+
value *= 10n ** BigInt(exponent);
|
|
616
|
+
else {
|
|
617
|
+
const divisor = 10n ** BigInt(-exponent);
|
|
618
|
+
if (value % divisor)
|
|
619
|
+
return;
|
|
620
|
+
value /= divisor;
|
|
621
|
+
}
|
|
622
|
+
if (match[1])
|
|
623
|
+
value = -value;
|
|
624
|
+
return validDuration(value) ? { kind: "duration", nanoseconds: String(value) } : undefined;
|
|
625
|
+
}
|
|
626
|
+
function validDuration(nanoseconds) {
|
|
627
|
+
return nanoseconds >= 0n && nanoseconds <= 4294967295000000n && nanoseconds % 1000000n === 0n;
|
|
628
|
+
}
|
|
629
|
+
function validDate(text) {
|
|
630
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
|
|
631
|
+
if (!match)
|
|
632
|
+
return false;
|
|
633
|
+
const year = Number(match[1]), month = Number(match[2]), day = Number(match[3]);
|
|
634
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
635
|
+
const length = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
|
|
636
|
+
return Boolean(length && day >= 1 && day <= length && text >= "1970-01-01" && text <= "2106-02-07");
|
|
637
|
+
}
|
|
638
|
+
function validClock(text, milliseconds) {
|
|
639
|
+
const match = (milliseconds ? /^(\d{2}):(\d{2}):(\d{2})\.(\d{3})$/ : /^(\d{2}):(\d{2}):(\d{2})$/).exec(text);
|
|
640
|
+
return Boolean(match && Number(match[1]) < 24 && Number(match[2]) < 60 && Number(match[3]) < 60);
|
|
641
|
+
}
|
|
642
|
+
function calendarLiteral(domain, text) {
|
|
643
|
+
if (domain === "date")
|
|
644
|
+
return validDate(text) ? { kind: "calendar", value: text } : undefined;
|
|
645
|
+
if (domain === "timeOfDay") {
|
|
646
|
+
const canonical = /^\d{2}:\d{2}:\d{2}$/.test(text) ? `${text}.000` : text;
|
|
647
|
+
return validClock(canonical, true) ? { kind: "calendar", value: canonical } : undefined;
|
|
648
|
+
}
|
|
649
|
+
return text.length === 19 && text[10] === "T" && validDate(text.slice(0, 10)) && validClock(text.slice(11), false) && text <= "2106-02-07T06:28:15" ? { kind: "calendar", value: text } : undefined;
|
|
650
|
+
}
|
|
651
|
+
function domainConstantFits(value, type) {
|
|
652
|
+
if (type.kind === "duration")
|
|
653
|
+
return value.kind === "duration" && /^(?:0|[1-9][0-9]*)$/.test(value.nanoseconds) && validDuration(BigInt(value.nanoseconds));
|
|
654
|
+
if (type.kind === "calendar" && value.kind === "calendar") {
|
|
655
|
+
const parsed = calendarLiteral(type.domain, value.value);
|
|
656
|
+
return parsed?.kind === "calendar" && parsed.value === value.value;
|
|
657
|
+
}
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
function domainIssue(value, types) {
|
|
661
|
+
const type = types.get(value.type);
|
|
662
|
+
if (value.kind === "constant")
|
|
663
|
+
return type && domainConstantFits(value.value, type) ? undefined : "Invalid TIME/calendar constant.";
|
|
664
|
+
if (value.kind === "load" || value.kind === "call" || value.kind === "conditional")
|
|
665
|
+
return;
|
|
666
|
+
if (value.kind === "binary" && comparisonOperators.has(value.operator)) {
|
|
667
|
+
const left = types.get(value.left.type);
|
|
668
|
+
return value.type === "BOOL" && left && ["duration", "calendar"].includes(left.kind) && value.left.type === value.right.type ? undefined : "Time comparisons require matching domains.";
|
|
669
|
+
}
|
|
670
|
+
return "TIME and calendar domains permit storage, typed copies and comparisons; implicit numeric arithmetic is unsupported.";
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// packages/compiler/ir/aggregates.ts
|
|
674
|
+
function aggregate(type) {
|
|
675
|
+
return type?.kind === "record" || type?.kind === "array";
|
|
676
|
+
}
|
|
677
|
+
function valueFits(value, type, types) {
|
|
678
|
+
if (type.kind === "duration" || type.kind === "calendar")
|
|
679
|
+
return domainConstantFits(value, type);
|
|
680
|
+
if (type.kind === "enum")
|
|
681
|
+
return value.kind === "integer" && type.members.some((member) => member.value === value.value);
|
|
682
|
+
if (type.kind === "string")
|
|
683
|
+
return value.kind === "string" && !value.value.includes("\x00") && (type.encoding === "utf8" ? new TextEncoder().encode(value.value).length <= type.capacity : value.value.length <= type.capacity);
|
|
684
|
+
if (type.kind === "array")
|
|
685
|
+
return value.kind === "array" && value.items.length === type.length && value.items.every((item) => valueFits(item, types.get(type.element), types));
|
|
686
|
+
if (type.kind === "record")
|
|
687
|
+
return value.kind === "record" && Object.keys(value.fields).length === type.fields.length && type.fields.every((field) => Object.hasOwn(value.fields, field.name) && valueFits(value.fields[field.name], types.get(field.type), types));
|
|
688
|
+
return constantFits(value, type);
|
|
689
|
+
}
|
|
690
|
+
function defaultValue(typeId, types) {
|
|
691
|
+
const type = types.get(typeId);
|
|
692
|
+
if (!type)
|
|
693
|
+
throw new Error("Unknown default-value type");
|
|
694
|
+
if (type.kind === "bool")
|
|
695
|
+
return { kind: "bool", value: false };
|
|
696
|
+
if (type.kind === "float")
|
|
697
|
+
return { kind: "float", value: 0 };
|
|
698
|
+
if (type.kind === "string")
|
|
699
|
+
return { kind: "string", value: "" };
|
|
700
|
+
if (type.kind === "enum")
|
|
701
|
+
return {
|
|
702
|
+
kind: "integer",
|
|
703
|
+
value: type.members.find((member) => member.value === "0")?.value ?? type.members[0].value
|
|
704
|
+
};
|
|
705
|
+
if (type.kind === "array")
|
|
706
|
+
return {
|
|
707
|
+
kind: "array",
|
|
708
|
+
items: Array.from({ length: type.length }, () => defaultValue(type.element, types))
|
|
709
|
+
};
|
|
710
|
+
if (type.kind === "record")
|
|
711
|
+
return {
|
|
712
|
+
kind: "record",
|
|
713
|
+
fields: Object.fromEntries(type.fields.map((field) => [field.name, defaultValue(field.type, types)]))
|
|
714
|
+
};
|
|
715
|
+
if (type.kind === "duration")
|
|
716
|
+
return { kind: "duration", nanoseconds: "0" };
|
|
717
|
+
if (type.kind === "calendar")
|
|
718
|
+
return {
|
|
719
|
+
kind: "calendar",
|
|
720
|
+
value: type.domain === "date" ? "1970-01-01" : type.domain === "timeOfDay" ? "00:00:00.000" : "1970-01-01T00:00:00"
|
|
721
|
+
};
|
|
722
|
+
if (type.kind === "integer" || type.kind === "bits")
|
|
723
|
+
return { kind: "integer", value: "0" };
|
|
724
|
+
throw new Error("Unsupported default-value type");
|
|
725
|
+
}
|
|
726
|
+
function placeType(place, types, variables, ranges = new Map, writable = false) {
|
|
727
|
+
const root = variables.get(place.root);
|
|
728
|
+
if (!root)
|
|
729
|
+
return { issue: "Unknown storage root." };
|
|
730
|
+
if (writable && (root.storage === "input" || root.storage === "constant" || root.storage === "blockOutput" || root.storage === "parameter" && root.mode !== "inOut" || !place.path.length && !root.mutable))
|
|
731
|
+
return { issue: "This storage is read-only." };
|
|
732
|
+
let id = root.type;
|
|
733
|
+
for (const segment of place.path) {
|
|
734
|
+
const type = types.get(id);
|
|
735
|
+
if (segment.kind === "field") {
|
|
736
|
+
if (type?.kind === "block" && segment.name === "out" && type.output) {
|
|
737
|
+
if (writable)
|
|
738
|
+
return { issue: "Function block output views are read-only." };
|
|
739
|
+
id = type.output;
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
if (type?.kind !== "record")
|
|
743
|
+
return { issue: "Field access requires a record." };
|
|
744
|
+
const field = type.fields.find((field) => field.name === segment.name);
|
|
745
|
+
if (!field)
|
|
746
|
+
return { issue: "Unknown record field." };
|
|
747
|
+
if (writable && field.readonly)
|
|
748
|
+
return { issue: "This field is read-only." };
|
|
749
|
+
id = field.type;
|
|
750
|
+
} else {
|
|
751
|
+
if (type?.kind !== "array")
|
|
752
|
+
return { issue: "Index access requires a fixed array." };
|
|
753
|
+
if (type.index && segment.value.type === type.index) {
|
|
754
|
+
id = type.element;
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
const indexType = types.get(segment.value.type), bounds = interval(segment.value, types, variables, ranges);
|
|
758
|
+
if (indexType?.kind !== "integer" || !bounds || bounds[0] < BigInt(type.lowerBound) || bounds[1] >= BigInt(type.lowerBound + type.length))
|
|
759
|
+
return { issue: "Array index must be proved within the declared bounds." };
|
|
760
|
+
id = type.element;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return { type: id };
|
|
764
|
+
}
|
|
765
|
+
function aggregateIssue(value, types) {
|
|
766
|
+
const type = types.get(value.type);
|
|
767
|
+
if (!type)
|
|
768
|
+
return "Unknown expression type.";
|
|
769
|
+
if (value.kind === "constant")
|
|
770
|
+
return valueFits(value.value, type, types) ? undefined : "Constant does not fit its closed PLC layout.";
|
|
771
|
+
if (value.kind === "record")
|
|
772
|
+
return type.kind === "record" && Object.keys(value.fields).length === type.fields.length && type.fields.every((field) => Object.hasOwn(value.fields, field.name) && value.fields[field.name]?.type === field.type) ? undefined : "Record expression differs from its layout.";
|
|
773
|
+
if (value.kind === "array")
|
|
774
|
+
return type.kind === "array" && value.items.length === type.length && value.items.every((item) => item.type === type.element) ? undefined : "Array expression differs from its layout.";
|
|
775
|
+
if (value.kind === "copy")
|
|
776
|
+
return aggregate(type) && value.operand.type === value.type ? undefined : "Copy requires one matching fixed aggregate.";
|
|
777
|
+
if (value.kind === "convert") {
|
|
778
|
+
const source = types.get(value.operand.type);
|
|
779
|
+
return type.kind === "string" && source?.kind === "string" && source.encoding === type.encoding && source.capacity <= type.capacity && value.policy === "exact" ? undefined : "Unsupported aggregate/domain conversion.";
|
|
780
|
+
}
|
|
781
|
+
if (value.kind === "load" || value.kind === "call" || value.kind === "conditional")
|
|
782
|
+
return;
|
|
783
|
+
if (value.kind === "binary" && comparisonOperators.has(value.operator)) {
|
|
784
|
+
const left = types.get(value.left.type);
|
|
785
|
+
return value.type === "BOOL" && left && ["enum", "string"].includes(left.kind) && value.left.type === value.right.type && (left.kind !== "enum" || ["equal", "notEqual"].includes(value.operator)) ? undefined : "Comparison requires compatible scalar domains; object identity is unsupported.";
|
|
786
|
+
}
|
|
787
|
+
return "Operation is unsupported for this PLC domain.";
|
|
788
|
+
}
|
|
789
|
+
function expressionIssue(value, types, variables, ranges) {
|
|
790
|
+
const kinds = [value.type, ...value.kind === "binary" ? [value.left.type] : []].map((id) => types.get(id)?.kind ?? "");
|
|
791
|
+
if (kinds.some((kind) => ["duration", "calendar"].includes(kind)))
|
|
792
|
+
return domainIssue(value, types);
|
|
793
|
+
if (["copy", "record", "array"].includes(value.kind) || kinds.some((kind) => ["record", "array", "enum", "string"].includes(kind)))
|
|
794
|
+
return aggregateIssue(value, types);
|
|
795
|
+
return scalarIssue(value, types, variables, ranges);
|
|
796
|
+
}
|
|
797
|
+
function layoutIssue(types) {
|
|
798
|
+
for (const type of types.values()) {
|
|
799
|
+
const canonical = primitiveTypes.find((item) => item.id === type.id);
|
|
800
|
+
if (canonical) {
|
|
801
|
+
if (type.kind !== canonical.kind || "bits" in canonical && (!("bits" in type) || type.bits !== canonical.bits) || "signed" in canonical && (!("signed" in type) || type.signed !== canonical.signed) || "domain" in canonical && (!("domain" in type) || type.domain !== canonical.domain))
|
|
802
|
+
return `Conflicting primitive type ${type.id}`;
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
if (type.kind === "record") {
|
|
806
|
+
if (!type.fields.length || new Set(type.fields.map((field) => field.name)).size !== type.fields.length || type.fields.some((field) => !field.name || typeof field.readonly !== "boolean" || field.mode && field.mode !== "inOut" || !types.has(field.type)))
|
|
807
|
+
return `Invalid closed record layout ${type.id}`;
|
|
808
|
+
} else if (type.kind === "array") {
|
|
809
|
+
if (!types.has(type.element) || !Number.isSafeInteger(type.length) || type.length < 1 || type.length > 1e6 || !Number.isInteger(type.lowerBound) || type.lowerBound < -2147483648 || type.lowerBound + type.length - 1 > 2147483647)
|
|
810
|
+
return `Invalid fixed array layout ${type.id}`;
|
|
811
|
+
if (type.index !== undefined) {
|
|
812
|
+
const index = types.get(type.index);
|
|
813
|
+
if (index?.kind !== "enum" || index.members.length !== type.length || index.members.some((member) => Number(member.value) < type.lowerBound || Number(member.value) >= type.lowerBound + type.length))
|
|
814
|
+
return `Invalid enum-keyed array layout ${type.id}`;
|
|
815
|
+
}
|
|
816
|
+
} else if (type.kind === "enum") {
|
|
817
|
+
if (type.base !== "DINT" || types.get(type.base)?.kind !== "integer" || !type.members.length || new Set(type.members.map((member) => member.name)).size !== type.members.length || new Set(type.members.map((member) => member.value)).size !== type.members.length || type.members.some((member) => !member.name || !constantFits({ kind: "integer", value: member.value }, types.get(type.base))))
|
|
818
|
+
return `Invalid enum layout ${type.id}`;
|
|
819
|
+
} else if (type.kind === "string") {
|
|
820
|
+
if (!Number.isInteger(type.capacity) || type.capacity < 1 || type.capacity > 65535 || !["utf8", "utf16"].includes(type.encoding))
|
|
821
|
+
return `Invalid bounded string layout ${type.id}`;
|
|
822
|
+
} else if (type.kind === "block") {
|
|
823
|
+
if (!type.block || type.output && types.get(type.output)?.kind !== "record")
|
|
824
|
+
return `Invalid block type ${type.id}`;
|
|
825
|
+
} else
|
|
826
|
+
return `Unsupported core type definition ${type.id}`;
|
|
827
|
+
}
|
|
828
|
+
const active = new Set, layouts = new Map;
|
|
829
|
+
const count = (id, depth) => {
|
|
830
|
+
if (active.has(id) || depth > 64)
|
|
831
|
+
throw new Error(`Recursive or excessively nested by-value layout ${id}`);
|
|
832
|
+
const cached = layouts.get(id);
|
|
833
|
+
if (cached) {
|
|
834
|
+
if (depth + cached.height > 64)
|
|
835
|
+
throw new Error(`Layout nesting exceeds 64 levels ${id}`);
|
|
836
|
+
return cached;
|
|
837
|
+
}
|
|
838
|
+
active.add(id);
|
|
839
|
+
const type = types.get(id);
|
|
840
|
+
const children = type.kind === "array" ? [count(type.element, depth + 1)] : type.kind === "record" ? type.fields.map((field) => count(field.type, depth + 1)) : [];
|
|
841
|
+
const size = type.kind === "array" ? type.length * children[0].size : type.kind === "record" ? children.reduce((sum, child) => sum + child.size, 0) : 1;
|
|
842
|
+
const height = children.length ? 1 + children.reduce((maximum, child) => Math.max(maximum, child.height), 0) : 0;
|
|
843
|
+
active.delete(id);
|
|
844
|
+
if (size > 1e6)
|
|
845
|
+
throw new Error(`Layout exceeds one million scalar elements ${id}`);
|
|
846
|
+
if (depth + height > 64)
|
|
847
|
+
throw new Error(`Layout nesting exceeds 64 levels ${id}`);
|
|
848
|
+
const layout = { size, height };
|
|
849
|
+
layouts.set(id, layout);
|
|
850
|
+
return layout;
|
|
851
|
+
};
|
|
852
|
+
try {
|
|
853
|
+
for (const id of types.keys())
|
|
854
|
+
count(id, 0);
|
|
855
|
+
} catch (cause) {
|
|
856
|
+
return cause instanceof Error ? cause.message : "Invalid layout";
|
|
857
|
+
}
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// packages/compiler/ir/normalization-error.ts
|
|
862
|
+
class InvalidCore extends Error {
|
|
863
|
+
diagnostic;
|
|
864
|
+
constructor(diagnostic) {
|
|
865
|
+
super(diagnostic.message);
|
|
866
|
+
this.diagnostic = diagnostic;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function reject(message, span) {
|
|
870
|
+
throw new InvalidCore(error("PLC6001", message, span));
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// packages/compiler/ir/declarations.ts
|
|
874
|
+
class ModuleDeclarations {
|
|
875
|
+
ids = new Set;
|
|
876
|
+
types = new Map;
|
|
877
|
+
globals = new Map;
|
|
878
|
+
blocks;
|
|
879
|
+
signatures;
|
|
880
|
+
constructor(module) {
|
|
881
|
+
for (const type of module.types) {
|
|
882
|
+
if (this.types.has(type.id))
|
|
883
|
+
reject(`Duplicate type ${type.id}.`);
|
|
884
|
+
this.types.set(type.id, type);
|
|
885
|
+
}
|
|
886
|
+
const invalidLayout = layoutIssue(this.types);
|
|
887
|
+
if (invalidLayout)
|
|
888
|
+
reject(invalidLayout);
|
|
889
|
+
if (this.types.get("BOOL")?.kind !== "bool")
|
|
890
|
+
reject("Missing canonical BOOL type.");
|
|
891
|
+
this.blocks = new Map(module.blocks.map((block) => [block.id, block]));
|
|
892
|
+
for (const item of module.variables) {
|
|
893
|
+
this.checkVariable(item, ["module", "instance", "input", "output", "constant"]);
|
|
894
|
+
this.globals.set(item.id, item);
|
|
895
|
+
}
|
|
896
|
+
this.signatures = new Map(module.functions.map((fn) => [fn.id, fn]));
|
|
897
|
+
for (const fn of module.functions) {
|
|
898
|
+
this.claimId(fn.id, fn.span);
|
|
899
|
+
if (fn.owner && (!this.blocks.has(fn.owner.block) || this.blocks.get(fn.owner.block)?.standard || !["public", "private", "protected"].includes(fn.owner.access)))
|
|
900
|
+
reject("Methods require a user block owner and a valid access modifier.", fn.span);
|
|
901
|
+
if (fn.result && !this.isValueType(fn.result))
|
|
902
|
+
reject("Unknown or non-value function result type.", fn.span);
|
|
903
|
+
for (const parameter of fn.parameters) {
|
|
904
|
+
this.checkVariable(parameter, ["parameter"], true);
|
|
905
|
+
if (parameter.mutable && !parameter.mode)
|
|
906
|
+
reject("Value parameters are readonly.", parameter.span);
|
|
907
|
+
}
|
|
908
|
+
for (const local of fn.locals)
|
|
909
|
+
this.checkVariable(local, ["local"]);
|
|
910
|
+
}
|
|
911
|
+
for (const block of module.blocks) {
|
|
912
|
+
this.claimId(block.id, block.span);
|
|
913
|
+
for (const input of block.inputs) {
|
|
914
|
+
this.checkVariable(input, ["parameter"], true);
|
|
915
|
+
if (!input.mode && input.mutable)
|
|
916
|
+
reject("Value block inputs are readonly.", input.span);
|
|
917
|
+
}
|
|
918
|
+
for (const output of block.outputs)
|
|
919
|
+
this.checkVariable(output, ["blockOutput"]);
|
|
920
|
+
for (const state of block.state)
|
|
921
|
+
this.checkVariable(state, ["field", "instance"]);
|
|
922
|
+
for (const local of block.locals)
|
|
923
|
+
this.checkVariable(local, ["local"]);
|
|
924
|
+
for (const ports of [block.inputs, block.outputs]) {
|
|
925
|
+
if (new Set(ports.map((variable) => variable.name)).size !== ports.length)
|
|
926
|
+
reject("Block input and output names must each be unique within their schema.", block.span);
|
|
927
|
+
}
|
|
928
|
+
const result = block.result ? this.types.get(block.result) : undefined;
|
|
929
|
+
if (block.outputs.length ? result?.kind !== "record" || !this.isValueType(result.id) || result.fields.length !== block.outputs.length || result.fields.some((field) => !block.outputs.some((output) => output.name === field.name && output.type === field.type)) : Boolean(block.result))
|
|
930
|
+
reject("Block output schema differs from its result record.", block.span);
|
|
931
|
+
if (block.standard) {
|
|
932
|
+
const expected = Object.hasOwn(standardBlocks, block.standard) ? standardBlocks[block.standard] : undefined;
|
|
933
|
+
const matches = (variables, schema) => variables.length === schema.length && schema.every(([name, , type]) => variables.some((variable) => variable.name === name && variable.type === type && !variable.mode && !variable.retain));
|
|
934
|
+
if (!expected || block.name !== block.standard || block.body.length || block.state.length || block.locals.length || !matches(block.inputs, expected.inputs) || !matches(block.outputs, expected.outputs) || block.outputs.some((output) => output.type === "BOOL" ? output.initial?.kind !== "bool" || output.initial.value !== false : output.initial?.kind !== "duration" || output.initial.nanoseconds !== "0"))
|
|
935
|
+
reject("Invalid canonical standard block definition.", block.span);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
for (const type of this.types.values())
|
|
939
|
+
if (type.kind === "block") {
|
|
940
|
+
const definition = this.blocks.get(type.block);
|
|
941
|
+
if (!definition || definition.result !== type.output)
|
|
942
|
+
reject("Block type differs from its definition.");
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
hasId(id) {
|
|
946
|
+
return this.ids.has(id);
|
|
947
|
+
}
|
|
948
|
+
claimId(id, span) {
|
|
949
|
+
if (this.ids.has(id))
|
|
950
|
+
reject(`Duplicate symbol ${id}.`, span);
|
|
951
|
+
this.ids.add(id);
|
|
952
|
+
}
|
|
953
|
+
isValueType(id, seen = new Set) {
|
|
954
|
+
if (seen.has(id))
|
|
955
|
+
return true;
|
|
956
|
+
seen.add(id);
|
|
957
|
+
const type = this.types.get(id);
|
|
958
|
+
return Boolean(type && type.kind !== "block" && (type.kind !== "record" || type.fields.every((field) => !field.mode && this.isValueType(field.type, seen))) && (type.kind !== "array" || this.isValueType(type.element, seen)));
|
|
959
|
+
}
|
|
960
|
+
checkVariable(item, storage, allowBorrow = false) {
|
|
961
|
+
this.claimId(item.id, item.span);
|
|
962
|
+
const type = this.types.get(item.type);
|
|
963
|
+
if (!type || !storage.includes(item.storage))
|
|
964
|
+
reject("Invalid storage type or class.", item.span);
|
|
965
|
+
if (item.mode && (item.mode !== "inOut" || !allowBorrow || !item.mutable || item.storage !== "parameter"))
|
|
966
|
+
reject("InOut belongs only on mutable parameters.", item.span);
|
|
967
|
+
const mutableState = item.mutable || type.kind === "array" || type.kind === "record" && type.fields.some((field) => !field.readonly);
|
|
968
|
+
if (item.retain && !(item.storage === "instance" || ["module", "field"].includes(item.storage) && mutableState))
|
|
969
|
+
reject("Only mutable static state or block instances may retain.", item.span);
|
|
970
|
+
if (item.storage === "instance") {
|
|
971
|
+
const instanced = instancedBlock(item.type, this.types);
|
|
972
|
+
if (!instanced || !this.blocks.has(instanced.block) || item.initial || item.mode)
|
|
973
|
+
reject("Instances require a known static block and cannot have value initializers.", item.span);
|
|
974
|
+
} else if (!this.isValueType(item.type) && !(allowBorrow && item.mode === "inOut" && type.kind === "block"))
|
|
975
|
+
reject("Block instances and reference schemas cannot be stored as values.", item.span);
|
|
976
|
+
if (item.storage === "parameter" && item.initial)
|
|
977
|
+
reject("Inputs are supplied at each invocation and cannot have stored initializers.", item.span);
|
|
978
|
+
if (item.initial && !valueFits(item.initial, type, this.types))
|
|
979
|
+
reject("Initializer has the wrong type or is out of range.", item.span);
|
|
980
|
+
if (["module", "field", "constant", "blockOutput"].includes(item.storage) && !item.initial)
|
|
981
|
+
reject("Persistent storage requires initialization.", item.span);
|
|
982
|
+
if (["input", "constant", "blockOutput"].includes(item.storage) && item.mutable)
|
|
983
|
+
reject("Readonly storage marked mutable.", item.span);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// packages/compiler/ir/allocation.ts
|
|
988
|
+
function checkStaticAllocation(module, declarations) {
|
|
989
|
+
const { types, blocks } = declarations;
|
|
990
|
+
const payloads = new Map;
|
|
991
|
+
const payload = (id) => {
|
|
992
|
+
const cached = payloads.get(id);
|
|
993
|
+
if (cached !== undefined)
|
|
994
|
+
return cached;
|
|
995
|
+
const type = types.get(id);
|
|
996
|
+
const size = type.kind === "array" ? type.length * payload(type.element) : type.kind === "record" ? type.fields.reduce((sum, field) => sum + payload(field.type), 0) : 1;
|
|
997
|
+
payloads.set(id, size);
|
|
998
|
+
return size;
|
|
999
|
+
};
|
|
1000
|
+
const instanceSize = (id) => {
|
|
1001
|
+
const type = types.get(id);
|
|
1002
|
+
return type.kind === "array" ? type.length * instanceSize(type.element) : allocation(type.kind === "block" ? type.block : "").size;
|
|
1003
|
+
};
|
|
1004
|
+
const allocations = new Map, allocating = new Set;
|
|
1005
|
+
const allocation = (id) => {
|
|
1006
|
+
if (allocating.size >= 64)
|
|
1007
|
+
reject("Static block allocation nesting exceeds 64 levels.", blocks.get(id)?.span);
|
|
1008
|
+
if (allocating.has(id))
|
|
1009
|
+
reject("Recursive static block allocation.", blocks.get(id)?.span);
|
|
1010
|
+
const cached = allocations.get(id);
|
|
1011
|
+
if (cached)
|
|
1012
|
+
return cached;
|
|
1013
|
+
allocating.add(id);
|
|
1014
|
+
const block = blocks.get(id);
|
|
1015
|
+
const children = block.state.filter((item) => item.storage === "instance").map((item) => ({
|
|
1016
|
+
height: allocation(instancedBlock(item.type, types).block).height,
|
|
1017
|
+
size: instanceSize(item.type)
|
|
1018
|
+
}));
|
|
1019
|
+
const own = [
|
|
1020
|
+
...block.inputs,
|
|
1021
|
+
...block.outputs,
|
|
1022
|
+
...block.state.filter((item) => item.storage !== "instance")
|
|
1023
|
+
].reduce((sum, item) => sum + (item.mode === "inOut" ? 1 : payload(item.type)), 1);
|
|
1024
|
+
const size = own + children.reduce((sum, child) => sum + child.size, 0), height = children.length ? 1 + Math.max(...children.map((child) => child.height)) : 1;
|
|
1025
|
+
if (size > 1e6 || height > 64)
|
|
1026
|
+
reject("Static block allocation exceeds its finite budget.", block.span);
|
|
1027
|
+
allocating.delete(id);
|
|
1028
|
+
const result = { size, height };
|
|
1029
|
+
allocations.set(id, result);
|
|
1030
|
+
return result;
|
|
1031
|
+
};
|
|
1032
|
+
for (const block of module.blocks)
|
|
1033
|
+
allocation(block.id);
|
|
1034
|
+
const staticPayload = module.variables.reduce((sum, variable) => {
|
|
1035
|
+
return sum + (instancedBlock(variable.type, types) ? instanceSize(variable.type) : payload(variable.type));
|
|
1036
|
+
}, 0);
|
|
1037
|
+
if (staticPayload > 1e6)
|
|
1038
|
+
reject("Static module storage exceeds one million scalar elements.");
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// packages/compiler/ir/routine.ts
|
|
1042
|
+
function join(branches, initialized) {
|
|
1043
|
+
const falling = branches.filter((branch) => branch.flow === "fallthrough");
|
|
1044
|
+
if (falling.length)
|
|
1045
|
+
return {
|
|
1046
|
+
flow: "fallthrough",
|
|
1047
|
+
initialized: new Set([...falling[0].initialized].filter((id) => falling.every((branch) => branch.initialized.has(id))))
|
|
1048
|
+
};
|
|
1049
|
+
const flow = branches[0].flow;
|
|
1050
|
+
return { flow: branches.every((branch) => branch.flow === flow) ? flow : "mixed", initialized };
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
class RoutineNormalizer {
|
|
1054
|
+
declarations;
|
|
1055
|
+
fn;
|
|
1056
|
+
block;
|
|
1057
|
+
owner;
|
|
1058
|
+
scope;
|
|
1059
|
+
sourceScope;
|
|
1060
|
+
locals;
|
|
1061
|
+
ranges = new Map;
|
|
1062
|
+
counters = new Set;
|
|
1063
|
+
callees = new Set;
|
|
1064
|
+
temporaryCount = 0;
|
|
1065
|
+
constructor(routine, declarations) {
|
|
1066
|
+
this.declarations = declarations;
|
|
1067
|
+
this.block = "inputs" in routine ? routine : undefined;
|
|
1068
|
+
this.owner = this.block ?? ("owner" in routine && routine.owner ? declarations.blocks.get(routine.owner.block) : undefined);
|
|
1069
|
+
const parameters = "parameters" in routine ? routine.parameters : routine.inputs;
|
|
1070
|
+
this.fn = {
|
|
1071
|
+
id: routine.id,
|
|
1072
|
+
name: routine.name,
|
|
1073
|
+
..."owner" in routine && routine.owner && { owner: routine.owner },
|
|
1074
|
+
parameters,
|
|
1075
|
+
locals: routine.locals,
|
|
1076
|
+
body: routine.body,
|
|
1077
|
+
span: routine.span,
|
|
1078
|
+
...routine.result && { result: routine.result }
|
|
1079
|
+
};
|
|
1080
|
+
this.scope = new Map(declarations.globals);
|
|
1081
|
+
for (const item of [
|
|
1082
|
+
...parameters,
|
|
1083
|
+
...this.owner ? [...this.owner.state, ...this.owner.outputs] : [],
|
|
1084
|
+
...routine.locals
|
|
1085
|
+
])
|
|
1086
|
+
this.scope.set(item.id, item);
|
|
1087
|
+
this.sourceScope = new Map(this.scope);
|
|
1088
|
+
this.locals = [...routine.locals];
|
|
1089
|
+
}
|
|
1090
|
+
normalize() {
|
|
1091
|
+
if (this.block?.standard)
|
|
1092
|
+
return { routine: { ...this.block, body: [] }, callees: this.callees };
|
|
1093
|
+
const normalized = this.body(this.fn.body, new Set(this.fn.parameters.map((item) => item.id)));
|
|
1094
|
+
if (this.fn.result && normalized.flow !== "return")
|
|
1095
|
+
reject("A typed helper must return on every path.", this.fn.span);
|
|
1096
|
+
const routine = this.block ? { ...this.block, locals: this.locals, body: normalized.body } : { ...this.fn, locals: this.locals, body: normalized.body };
|
|
1097
|
+
return { routine, callees: this.callees };
|
|
1098
|
+
}
|
|
1099
|
+
temporary(type, span) {
|
|
1100
|
+
let id;
|
|
1101
|
+
do {
|
|
1102
|
+
id = `${this.fn.id}:temporary:${this.temporaryCount++}`;
|
|
1103
|
+
} while (this.declarations.hasId(id));
|
|
1104
|
+
this.declarations.claimId(id);
|
|
1105
|
+
const item = {
|
|
1106
|
+
id,
|
|
1107
|
+
name: `temporary_${this.temporaryCount}`,
|
|
1108
|
+
type,
|
|
1109
|
+
storage: "local",
|
|
1110
|
+
mutable: true,
|
|
1111
|
+
retain: false,
|
|
1112
|
+
span
|
|
1113
|
+
};
|
|
1114
|
+
this.scope.set(id, item);
|
|
1115
|
+
this.locals.push(item);
|
|
1116
|
+
return item;
|
|
1117
|
+
}
|
|
1118
|
+
load(variable, span) {
|
|
1119
|
+
return { kind: "load", root: variable.id, type: variable.type, span };
|
|
1120
|
+
}
|
|
1121
|
+
verifyCall(id, arguments_, span, instance) {
|
|
1122
|
+
const called = this.declarations.signatures.get(id);
|
|
1123
|
+
if (!called || called.parameters.length !== arguments_.length)
|
|
1124
|
+
reject("Unknown helper or wrong argument count.", span);
|
|
1125
|
+
if (called.owner) {
|
|
1126
|
+
if (instance) {
|
|
1127
|
+
const variable = this.sourceScope.get(instance);
|
|
1128
|
+
const type = variable && instancedBlock(variable.type, this.declarations.types);
|
|
1129
|
+
if (variable?.storage !== "instance" && variable?.mode !== "inOut" || type?.block !== called.owner.block)
|
|
1130
|
+
reject("Method calls require a matching static block instance.", span);
|
|
1131
|
+
} else if (this.owner?.id !== called.owner.block)
|
|
1132
|
+
reject("An implicit method call requires its owning block scope.", span);
|
|
1133
|
+
if (called.owner.access !== "public" && this.owner?.id !== called.owner.block)
|
|
1134
|
+
reject("Private and protected methods require their owning block scope.", span);
|
|
1135
|
+
} else if (instance)
|
|
1136
|
+
reject("Ordinary functions cannot have an instance receiver.", span);
|
|
1137
|
+
this.callees.add(id);
|
|
1138
|
+
arguments_.forEach((argument, index) => {
|
|
1139
|
+
if (argument.type !== called.parameters[index]?.type)
|
|
1140
|
+
reject("Helper argument has the wrong PLC type.", argument.span);
|
|
1141
|
+
});
|
|
1142
|
+
return called;
|
|
1143
|
+
}
|
|
1144
|
+
expression(value, initialized, output, allowAggregateLoad = false) {
|
|
1145
|
+
const type = this.declarations.types.get(value.type);
|
|
1146
|
+
if (!type || !this.declarations.isValueType(value.type))
|
|
1147
|
+
reject("Unknown or non-value expression type.", value.span);
|
|
1148
|
+
if (value.kind === "borrow")
|
|
1149
|
+
reject("Borrowed places are only valid in block update arguments.", value.span);
|
|
1150
|
+
const common = { type: value.type, span: value.span };
|
|
1151
|
+
if (value.kind === "length") {
|
|
1152
|
+
const array = this.declarations.types.get(value.operand.type);
|
|
1153
|
+
if (array?.kind !== "array" || value.type !== "DINT")
|
|
1154
|
+
reject("Length requires a fixed array and DINT result.", value.span);
|
|
1155
|
+
if (value.operand.kind === "load") {
|
|
1156
|
+
const source = this.sourceScope.get(value.operand.place.root), checked = placeType(value.operand.place, this.declarations.types, this.sourceScope, this.ranges);
|
|
1157
|
+
if (!source || checked.issue || checked.type !== value.operand.type || source.storage === "local" && !initialized.has(source.id))
|
|
1158
|
+
reject(checked.issue ?? "Invalid array receiver.", value.span);
|
|
1159
|
+
this.normalizePath(value.operand.place, initialized, output);
|
|
1160
|
+
} else
|
|
1161
|
+
this.expression(value.operand, initialized, output, true);
|
|
1162
|
+
return {
|
|
1163
|
+
...common,
|
|
1164
|
+
kind: "constant",
|
|
1165
|
+
value: { kind: "integer", value: String(array.length) }
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
if (value.kind === "conditional") {
|
|
1169
|
+
if (value.condition.type !== "BOOL" || value.then.type !== value.type || value.else.type !== value.type)
|
|
1170
|
+
reject("Invalid conditional types.", value.span);
|
|
1171
|
+
const condition = this.expression(value.condition, initialized, output), result = this.temporary(value.type, value.span);
|
|
1172
|
+
const branch = (operand) => {
|
|
1173
|
+
const statements = [];
|
|
1174
|
+
const evaluated = this.expression(operand, new Set(initialized), statements);
|
|
1175
|
+
statements.push({ kind: "assign", root: result.id, value: evaluated, span: value.span });
|
|
1176
|
+
return statements;
|
|
1177
|
+
};
|
|
1178
|
+
output.push({
|
|
1179
|
+
kind: "if",
|
|
1180
|
+
condition,
|
|
1181
|
+
then: branch(value.then),
|
|
1182
|
+
else: branch(value.else),
|
|
1183
|
+
span: value.span
|
|
1184
|
+
});
|
|
1185
|
+
initialized.add(result.id);
|
|
1186
|
+
return this.load(result, value.span);
|
|
1187
|
+
}
|
|
1188
|
+
if (value.kind === "call") {
|
|
1189
|
+
const called = this.verifyCall(value.function, value.arguments, value.span, value.instance);
|
|
1190
|
+
if (called.result !== value.type)
|
|
1191
|
+
reject("Expression call requires a matching result type.", value.span);
|
|
1192
|
+
const index = value.instance && this.instanceIndex(value.instance, value.index, initialized, output, value.span);
|
|
1193
|
+
const arguments_ = this.arguments(called.parameters, value.arguments, initialized, output);
|
|
1194
|
+
const result = this.temporary(value.type, value.span);
|
|
1195
|
+
output.push({
|
|
1196
|
+
kind: "invoke",
|
|
1197
|
+
function: value.function,
|
|
1198
|
+
...value.instance && { instance: value.instance },
|
|
1199
|
+
...index && { index },
|
|
1200
|
+
arguments: arguments_,
|
|
1201
|
+
destination: result.id,
|
|
1202
|
+
span: value.span
|
|
1203
|
+
});
|
|
1204
|
+
initialized.add(result.id);
|
|
1205
|
+
return this.load(result, value.span);
|
|
1206
|
+
}
|
|
1207
|
+
const issue = expressionIssue(value, this.declarations.types, this.scope, this.ranges);
|
|
1208
|
+
if (issue)
|
|
1209
|
+
reject(issue, value.span);
|
|
1210
|
+
switch (value.kind) {
|
|
1211
|
+
case "copy":
|
|
1212
|
+
return this.expression(value.operand, initialized, output, true);
|
|
1213
|
+
case "record":
|
|
1214
|
+
case "array": {
|
|
1215
|
+
const result = this.temporary(value.type, value.span);
|
|
1216
|
+
if (value.kind === "record") {
|
|
1217
|
+
for (const [name, operand] of Object.entries(value.fields))
|
|
1218
|
+
output.push({
|
|
1219
|
+
kind: "assign",
|
|
1220
|
+
root: result.id,
|
|
1221
|
+
path: [{ kind: "field", name }],
|
|
1222
|
+
value: this.expression(operand, initialized, output),
|
|
1223
|
+
span: operand.span
|
|
1224
|
+
});
|
|
1225
|
+
} else {
|
|
1226
|
+
const array = this.declarations.types.get(value.type);
|
|
1227
|
+
if (array.kind !== "array")
|
|
1228
|
+
reject("Invalid array construction.", value.span);
|
|
1229
|
+
value.items.forEach((operand, index) => {
|
|
1230
|
+
output.push({
|
|
1231
|
+
kind: "assign",
|
|
1232
|
+
root: result.id,
|
|
1233
|
+
path: [
|
|
1234
|
+
{
|
|
1235
|
+
kind: "index",
|
|
1236
|
+
value: {
|
|
1237
|
+
kind: "constant",
|
|
1238
|
+
type: "DINT",
|
|
1239
|
+
value: { kind: "integer", value: String(array.lowerBound + index) },
|
|
1240
|
+
span: operand.span
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
],
|
|
1244
|
+
value: this.expression(operand, initialized, output),
|
|
1245
|
+
span: operand.span
|
|
1246
|
+
});
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
initialized.add(result.id);
|
|
1250
|
+
return this.load(result, value.span);
|
|
1251
|
+
}
|
|
1252
|
+
case "constant": {
|
|
1253
|
+
if (!aggregate(type))
|
|
1254
|
+
return { ...common, kind: "constant", value: value.value };
|
|
1255
|
+
const result = this.temporary(value.type, value.span);
|
|
1256
|
+
output.push({
|
|
1257
|
+
kind: "assign",
|
|
1258
|
+
root: result.id,
|
|
1259
|
+
value: { ...common, kind: "constant", value: value.value },
|
|
1260
|
+
span: value.span
|
|
1261
|
+
});
|
|
1262
|
+
initialized.add(result.id);
|
|
1263
|
+
return this.load(result, value.span);
|
|
1264
|
+
}
|
|
1265
|
+
case "load": {
|
|
1266
|
+
const source = this.sourceScope.get(value.place.root), checked = placeType(value.place, this.declarations.types, this.sourceScope, this.ranges);
|
|
1267
|
+
if (!source || checked.issue || checked.type !== value.type)
|
|
1268
|
+
reject(checked.issue ?? "Invalid PLC load.", value.span);
|
|
1269
|
+
if (aggregate(type) && !allowAggregateLoad)
|
|
1270
|
+
reject("Aggregate loads require explicit copy or a readonly value argument.", value.span);
|
|
1271
|
+
if (source.storage === "local" && !initialized.has(source.id))
|
|
1272
|
+
reject("Local read before initialization.", value.span);
|
|
1273
|
+
const first = value.place.path[0];
|
|
1274
|
+
if (source.storage === "instance" && value.place.path.length === 1 && first?.kind === "field" && first.name === "out" && type.kind === "record") {
|
|
1275
|
+
const result = this.temporary(value.type, value.span);
|
|
1276
|
+
for (const field of type.fields) {
|
|
1277
|
+
const selected = {
|
|
1278
|
+
kind: "load",
|
|
1279
|
+
type: field.type,
|
|
1280
|
+
place: {
|
|
1281
|
+
root: source.id,
|
|
1282
|
+
path: [...value.place.path, { kind: "field", name: field.name }]
|
|
1283
|
+
},
|
|
1284
|
+
span: value.span
|
|
1285
|
+
};
|
|
1286
|
+
const captured = this.expression(selected, initialized, output, true);
|
|
1287
|
+
output.push({
|
|
1288
|
+
kind: "assign",
|
|
1289
|
+
root: result.id,
|
|
1290
|
+
path: [{ kind: "field", name: field.name }],
|
|
1291
|
+
value: captured,
|
|
1292
|
+
span: value.span
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
initialized.add(result.id);
|
|
1296
|
+
return this.load(result, value.span);
|
|
1297
|
+
}
|
|
1298
|
+
const path = this.normalizePath(value.place, initialized, output);
|
|
1299
|
+
if (source.storage === "constant" && source.initial && !path.length)
|
|
1300
|
+
return { ...common, kind: "constant", value: source.initial };
|
|
1301
|
+
const result = this.temporary(value.type, value.span);
|
|
1302
|
+
output.push({
|
|
1303
|
+
kind: "assign",
|
|
1304
|
+
root: result.id,
|
|
1305
|
+
value: { ...common, kind: "load", root: source.id, ...path.length && { path } },
|
|
1306
|
+
span: value.span
|
|
1307
|
+
});
|
|
1308
|
+
const bound = interval(value, this.declarations.types, this.scope, this.ranges);
|
|
1309
|
+
if (bound)
|
|
1310
|
+
this.ranges.set(result.id, bound);
|
|
1311
|
+
initialized.add(result.id);
|
|
1312
|
+
return this.load(result, value.span);
|
|
1313
|
+
}
|
|
1314
|
+
case "unary": {
|
|
1315
|
+
const operand = this.expression(value.operand, initialized, output), constant = fold(value, this.scope, this.declarations.types);
|
|
1316
|
+
return constant ? { ...common, kind: "constant", value: constant } : { ...common, kind: "unary", operator: value.operator, operand };
|
|
1317
|
+
}
|
|
1318
|
+
case "convert": {
|
|
1319
|
+
const operand = this.expression(value.operand, initialized, output), constant = fold(value, this.scope, this.declarations.types);
|
|
1320
|
+
if (value.type === value.operand.type)
|
|
1321
|
+
return operand;
|
|
1322
|
+
return constant ? { ...common, kind: "constant", value: constant } : { ...common, kind: "convert", operand, policy: value.policy };
|
|
1323
|
+
}
|
|
1324
|
+
case "binary": {
|
|
1325
|
+
const left = this.expression(value.left, initialized, output);
|
|
1326
|
+
if (value.operator === "and" || value.operator === "or") {
|
|
1327
|
+
const result = this.temporary("BOOL", value.span), branch = [];
|
|
1328
|
+
output.push({ kind: "assign", root: result.id, value: left, span: value.span });
|
|
1329
|
+
const right = this.expression(value.right, new Set(initialized), branch);
|
|
1330
|
+
branch.push({ kind: "assign", root: result.id, value: right, span: value.span });
|
|
1331
|
+
const condition = value.operator === "and" ? this.load(result, value.span) : {
|
|
1332
|
+
kind: "unary",
|
|
1333
|
+
operator: "not",
|
|
1334
|
+
operand: this.load(result, value.span),
|
|
1335
|
+
type: "BOOL",
|
|
1336
|
+
span: value.span
|
|
1337
|
+
};
|
|
1338
|
+
output.push({
|
|
1339
|
+
kind: "if",
|
|
1340
|
+
condition,
|
|
1341
|
+
then: branch,
|
|
1342
|
+
else: [],
|
|
1343
|
+
span: value.span
|
|
1344
|
+
});
|
|
1345
|
+
initialized.add(result.id);
|
|
1346
|
+
return this.load(result, value.span);
|
|
1347
|
+
}
|
|
1348
|
+
const right = this.expression(value.right, initialized, output), constant = fold(value, this.scope, this.declarations.types);
|
|
1349
|
+
if (constant)
|
|
1350
|
+
return { ...common, kind: "constant", value: constant };
|
|
1351
|
+
return {
|
|
1352
|
+
...common,
|
|
1353
|
+
kind: "binary",
|
|
1354
|
+
operator: value.operator,
|
|
1355
|
+
left,
|
|
1356
|
+
right,
|
|
1357
|
+
...value.policy && { policy: value.policy }
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
normalizePath(place, initialized, output) {
|
|
1363
|
+
return place.path.map((segment) => segment.kind === "field" ? segment : { kind: "index", value: this.expression(segment.value, initialized, output) });
|
|
1364
|
+
}
|
|
1365
|
+
destination(place, type, initialized, span) {
|
|
1366
|
+
const target = this.sourceScope.get(place.root), initializing = target?.storage === "local" && !initialized.has(place.root) && !place.path.length;
|
|
1367
|
+
const checked = placeType(place, this.declarations.types, this.sourceScope, this.ranges, !initializing);
|
|
1368
|
+
if (!target || checked.issue || checked.type !== type || this.counters.has(place.root))
|
|
1369
|
+
reject(checked.issue ?? "Invalid assignment destination.", span);
|
|
1370
|
+
if (place.path.length && target.storage === "local" && !initialized.has(target.id))
|
|
1371
|
+
reject("Initialize the whole aggregate before assigning fields.", span);
|
|
1372
|
+
return target;
|
|
1373
|
+
}
|
|
1374
|
+
instanceIndex(instance, index, initialized, output, span) {
|
|
1375
|
+
if (!index)
|
|
1376
|
+
return;
|
|
1377
|
+
const checked = placeType({ root: instance, path: [{ kind: "index", value: index }] }, this.declarations.types, this.sourceScope, this.ranges);
|
|
1378
|
+
if (checked.issue)
|
|
1379
|
+
reject(checked.issue, span);
|
|
1380
|
+
return this.expression(index, initialized, output);
|
|
1381
|
+
}
|
|
1382
|
+
blockUpdate(item, initialized, output) {
|
|
1383
|
+
const instance = this.sourceScope.get(item.instance), type = instance && instancedBlock(instance.type, this.declarations.types);
|
|
1384
|
+
const called = type && this.declarations.blocks.get(type.block);
|
|
1385
|
+
if (instance?.storage !== "instance" && instance?.mode !== "inOut" || !called)
|
|
1386
|
+
reject("Updates require a statically allocated block instance.", item.span);
|
|
1387
|
+
const index = this.instanceIndex(item.instance, item.index, initialized, output, item.span);
|
|
1388
|
+
const keys = Object.keys(item.inputs);
|
|
1389
|
+
if (keys.length !== called.inputs.length || called.inputs.some((input) => !Object.hasOwn(item.inputs, input.name)))
|
|
1390
|
+
reject("Block update must supply every named input exactly once.", item.span);
|
|
1391
|
+
this.callees.add(called.id);
|
|
1392
|
+
const arguments_ = this.arguments(keys.map((name) => called.inputs.find((input) => input.name === name)), keys.map((name) => item.inputs[name]), initialized, output);
|
|
1393
|
+
const inputs = Object.fromEntries(keys.map((name, index) => [name, arguments_[index]]));
|
|
1394
|
+
output.push({
|
|
1395
|
+
kind: "blockUpdate",
|
|
1396
|
+
instance: instance.id,
|
|
1397
|
+
...index && { index },
|
|
1398
|
+
inputs,
|
|
1399
|
+
span: item.span
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
arguments(parameters, values, initialized, output) {
|
|
1403
|
+
const arguments_ = [];
|
|
1404
|
+
for (const [index, value] of values.entries()) {
|
|
1405
|
+
const input = parameters[index];
|
|
1406
|
+
if (value.type !== input.type)
|
|
1407
|
+
reject("Block input differs from its concrete PLC type.", value.span);
|
|
1408
|
+
const block = this.declarations.types.get(value.type)?.kind === "block";
|
|
1409
|
+
if (input.mode === "inOut" && value.kind !== "borrow") {
|
|
1410
|
+
if (block)
|
|
1411
|
+
reject("Pass a block instance by name.", value.span);
|
|
1412
|
+
const temporary = this.temporary(value.type, value.span);
|
|
1413
|
+
output.push({
|
|
1414
|
+
kind: "assign",
|
|
1415
|
+
root: temporary.id,
|
|
1416
|
+
value: this.expression(value, initialized, output),
|
|
1417
|
+
span: value.span
|
|
1418
|
+
});
|
|
1419
|
+
initialized.add(temporary.id);
|
|
1420
|
+
arguments_.push({
|
|
1421
|
+
kind: "borrow",
|
|
1422
|
+
type: value.type,
|
|
1423
|
+
place: { root: temporary.id, path: [] },
|
|
1424
|
+
span: value.span
|
|
1425
|
+
});
|
|
1426
|
+
} else if (input.mode === "inOut" && value.kind === "borrow") {
|
|
1427
|
+
const root = this.sourceScope.get(value.place.root), checked = placeType(value.place, this.declarations.types, this.sourceScope, this.ranges, !block);
|
|
1428
|
+
if (!root || checked.issue || checked.type !== value.type || !block && !this.declarations.isValueType(value.type) || this.counters.has(root.id) || root.storage === "local" && !initialized.has(root.id))
|
|
1429
|
+
reject(checked.issue ?? "Borrow requires initialized mutable value storage of the exact type.", value.span);
|
|
1430
|
+
arguments_.push({
|
|
1431
|
+
kind: "borrow",
|
|
1432
|
+
type: value.type,
|
|
1433
|
+
place: {
|
|
1434
|
+
root: value.place.root,
|
|
1435
|
+
path: this.normalizePath(value.place, initialized, output)
|
|
1436
|
+
},
|
|
1437
|
+
span: value.span
|
|
1438
|
+
});
|
|
1439
|
+
} else {
|
|
1440
|
+
if (value.kind === "borrow")
|
|
1441
|
+
reject("Readonly value inputs cannot receive borrowed references.", value.span);
|
|
1442
|
+
arguments_.push(this.expression(value, initialized, output, true));
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return arguments_;
|
|
1446
|
+
}
|
|
1447
|
+
body(statements, initialized, inLoop = false) {
|
|
1448
|
+
const output = [];
|
|
1449
|
+
let flow = "fallthrough";
|
|
1450
|
+
for (const item of statements) {
|
|
1451
|
+
if (flow !== "fallthrough")
|
|
1452
|
+
break;
|
|
1453
|
+
switch (item.kind) {
|
|
1454
|
+
case "assign": {
|
|
1455
|
+
this.destination(item.destination, item.value.type, initialized, item.span);
|
|
1456
|
+
const path = this.normalizePath(item.destination, initialized, output);
|
|
1457
|
+
const bound = !path.length && this.scope.get(item.destination.root)?.mutable === false && valueRange(item.value, this.declarations.types, this.scope, this.ranges, this.declarations.signatures);
|
|
1458
|
+
const value = this.expression(item.value, initialized, output);
|
|
1459
|
+
if (bound)
|
|
1460
|
+
this.ranges.set(item.destination.root, bound);
|
|
1461
|
+
if (!path.length)
|
|
1462
|
+
initialized.add(item.destination.root);
|
|
1463
|
+
output.push({
|
|
1464
|
+
kind: "assign",
|
|
1465
|
+
root: item.destination.root,
|
|
1466
|
+
...path.length && { path },
|
|
1467
|
+
value,
|
|
1468
|
+
span: item.span
|
|
1469
|
+
});
|
|
1470
|
+
break;
|
|
1471
|
+
}
|
|
1472
|
+
case "blockUpdate": {
|
|
1473
|
+
this.blockUpdate(item, initialized, output);
|
|
1474
|
+
break;
|
|
1475
|
+
}
|
|
1476
|
+
case "invoke": {
|
|
1477
|
+
const called = this.verifyCall(item.function, item.arguments, item.span, item.instance);
|
|
1478
|
+
if (item.destination) {
|
|
1479
|
+
if (item.destination.path.length || !called.result)
|
|
1480
|
+
reject("Invalid helper destination.", item.span);
|
|
1481
|
+
this.destination(item.destination, called.result, initialized, item.span);
|
|
1482
|
+
}
|
|
1483
|
+
const index = item.instance && this.instanceIndex(item.instance, item.index, initialized, output, item.span);
|
|
1484
|
+
const arguments_ = this.arguments(called.parameters, item.arguments, initialized, output);
|
|
1485
|
+
output.push({
|
|
1486
|
+
kind: "invoke",
|
|
1487
|
+
function: item.function,
|
|
1488
|
+
...item.instance && { instance: item.instance },
|
|
1489
|
+
...index && { index },
|
|
1490
|
+
arguments: arguments_,
|
|
1491
|
+
span: item.span,
|
|
1492
|
+
...item.destination && { destination: item.destination.root }
|
|
1493
|
+
});
|
|
1494
|
+
if (item.destination)
|
|
1495
|
+
initialized.add(item.destination.root);
|
|
1496
|
+
break;
|
|
1497
|
+
}
|
|
1498
|
+
case "if": {
|
|
1499
|
+
if (item.condition.type !== "BOOL")
|
|
1500
|
+
reject("Conditions require BOOL.", item.span);
|
|
1501
|
+
const condition = this.expression(item.condition, initialized, output);
|
|
1502
|
+
const originalRanges = new Map(this.ranges);
|
|
1503
|
+
const branch = (statements, truth) => {
|
|
1504
|
+
this.ranges.clear();
|
|
1505
|
+
for (const [root, range] of narrowRanges(item.condition, truth, this.declarations.types, this.sourceScope, originalRanges))
|
|
1506
|
+
this.ranges.set(root, range);
|
|
1507
|
+
try {
|
|
1508
|
+
return this.body(statements, new Set(initialized), inLoop);
|
|
1509
|
+
} finally {
|
|
1510
|
+
this.ranges.clear();
|
|
1511
|
+
for (const [root, range] of originalRanges)
|
|
1512
|
+
this.ranges.set(root, range);
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
const yes = branch(item.then, true), no = branch(item.else, false);
|
|
1516
|
+
({ flow, initialized } = join([yes, no], initialized));
|
|
1517
|
+
output.push({
|
|
1518
|
+
kind: "if",
|
|
1519
|
+
condition,
|
|
1520
|
+
then: yes.body,
|
|
1521
|
+
else: no.body,
|
|
1522
|
+
span: item.span
|
|
1523
|
+
});
|
|
1524
|
+
break;
|
|
1525
|
+
}
|
|
1526
|
+
case "switch": {
|
|
1527
|
+
const value = this.expression(item.value, initialized, output), seen = new Set;
|
|
1528
|
+
const branches = item.cases.map((branch) => {
|
|
1529
|
+
for (const constant of branch.values) {
|
|
1530
|
+
if (!valueFits(constant, this.declarations.types.get(item.value.type), this.declarations.types))
|
|
1531
|
+
reject("Invalid case constant.", item.span);
|
|
1532
|
+
const key = JSON.stringify(constant);
|
|
1533
|
+
if (seen.has(key))
|
|
1534
|
+
reject("Duplicate case constant.", item.span);
|
|
1535
|
+
seen.add(key);
|
|
1536
|
+
}
|
|
1537
|
+
return {
|
|
1538
|
+
values: branch.values,
|
|
1539
|
+
result: this.body(branch.body, new Set(initialized), inLoop)
|
|
1540
|
+
};
|
|
1541
|
+
});
|
|
1542
|
+
const fallback = this.body(item.fallback, new Set(initialized), inLoop);
|
|
1543
|
+
({ flow, initialized } = join([...branches.map((branch) => branch.result), fallback], initialized));
|
|
1544
|
+
output.push({
|
|
1545
|
+
kind: "switch",
|
|
1546
|
+
value,
|
|
1547
|
+
cases: branches.map((branch) => ({ values: branch.values, body: branch.result.body })),
|
|
1548
|
+
fallback: fallback.body,
|
|
1549
|
+
span: item.span
|
|
1550
|
+
});
|
|
1551
|
+
break;
|
|
1552
|
+
}
|
|
1553
|
+
case "for": {
|
|
1554
|
+
const counter = this.sourceScope.get(item.counter);
|
|
1555
|
+
const counterType = counter && this.declarations.types.get(counter.type);
|
|
1556
|
+
if (!counter || !(item.counterScope === "existing" ? ["local", "module", "field"].includes(counter.storage) : counter.storage === "local") || !counter.mutable || counterType?.kind !== "integer" || this.counters.has(counter.id) || item.start.type !== counter.type || item.limit.type !== counter.type || !/^(-?[1-9][0-9]*)$/.test(item.step))
|
|
1557
|
+
reject("Invalid bounded loop.", item.span);
|
|
1558
|
+
const start = fold(item.start, this.scope, this.declarations.types);
|
|
1559
|
+
const limit = fold(item.limit, this.scope, this.declarations.types);
|
|
1560
|
+
const startRange = interval(item.start, this.declarations.types, this.sourceScope, this.ranges);
|
|
1561
|
+
const limitRange = interval(item.limit, this.declarations.types, this.sourceScope, this.ranges);
|
|
1562
|
+
if (!startRange || !limitRange)
|
|
1563
|
+
reject("Loop bounds require finite integer ranges.", item.span);
|
|
1564
|
+
const step = BigInt(item.step);
|
|
1565
|
+
const proof = loopBounds(startRange, limitRange, step, item.exclusive);
|
|
1566
|
+
const dynamic = start?.kind !== "integer" || limit?.kind !== "integer";
|
|
1567
|
+
const { first, last, count } = proof;
|
|
1568
|
+
if (count > 1000000n || Number(count) !== item.maxIterations || !Number.isSafeInteger(item.maxIterations))
|
|
1569
|
+
reject("Loop iteration proof differs from its bounds.", item.span);
|
|
1570
|
+
if (item.counterScope === "existing") {
|
|
1571
|
+
const terminal = dynamic ? (step > 0n ? proof.range[1] : proof.range[0]) + step : first + count * step;
|
|
1572
|
+
const [minimum, maximum] = integerRange(counterType);
|
|
1573
|
+
if (count > 0n && (terminal < minimum || terminal > maximum))
|
|
1574
|
+
reject("The existing loop counter overflows on termination.", item.span);
|
|
1575
|
+
}
|
|
1576
|
+
const evaluatedStart = this.expression(item.start, initialized, output);
|
|
1577
|
+
const evaluatedLimit = this.expression(item.limit, initialized, output);
|
|
1578
|
+
initialized.add(counter.id);
|
|
1579
|
+
this.counters.add(counter.id);
|
|
1580
|
+
this.ranges.set(counter.id, proof.range);
|
|
1581
|
+
const nested = this.body(item.body, new Set(initialized), true);
|
|
1582
|
+
this.counters.delete(counter.id);
|
|
1583
|
+
this.ranges.delete(counter.id);
|
|
1584
|
+
if (item.counterScope !== "existing")
|
|
1585
|
+
initialized.delete(counter.id);
|
|
1586
|
+
output.push({
|
|
1587
|
+
kind: "for",
|
|
1588
|
+
counter: counter.id,
|
|
1589
|
+
...item.counterScope && { counterScope: item.counterScope },
|
|
1590
|
+
start: { kind: "integer", value: String(first) },
|
|
1591
|
+
limit: { kind: "integer", value: String(last) },
|
|
1592
|
+
...dynamic && {
|
|
1593
|
+
dynamic: {
|
|
1594
|
+
start: evaluatedStart,
|
|
1595
|
+
limit: evaluatedLimit,
|
|
1596
|
+
...item.exclusive && { exclusive: item.exclusive }
|
|
1597
|
+
}
|
|
1598
|
+
},
|
|
1599
|
+
step: item.step,
|
|
1600
|
+
maxIterations: item.maxIterations,
|
|
1601
|
+
body: nested.body,
|
|
1602
|
+
span: item.span
|
|
1603
|
+
});
|
|
1604
|
+
break;
|
|
1605
|
+
}
|
|
1606
|
+
case "return": {
|
|
1607
|
+
if (item.value?.type !== this.fn.result)
|
|
1608
|
+
reject("Return differs from the function signature.", item.span);
|
|
1609
|
+
const value = item.value && this.expression(item.value, initialized, output);
|
|
1610
|
+
output.push({ kind: "return", span: item.span, ...value && { value } });
|
|
1611
|
+
flow = "return";
|
|
1612
|
+
break;
|
|
1613
|
+
}
|
|
1614
|
+
case "break":
|
|
1615
|
+
case "continue":
|
|
1616
|
+
if (!inLoop)
|
|
1617
|
+
reject("Loop control appears outside a bounded loop.", item.span);
|
|
1618
|
+
output.push({ kind: item.kind, span: item.span });
|
|
1619
|
+
flow = "loop";
|
|
1620
|
+
break;
|
|
1621
|
+
default:
|
|
1622
|
+
reject("Unknown PLC statement.");
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
return { body: output, initialized, flow };
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// packages/compiler/ir/link.ts
|
|
1630
|
+
function linkModule(module, declarations, functions, normalizedBlocks, edges) {
|
|
1631
|
+
const { signatures, blocks, types } = declarations;
|
|
1632
|
+
const names = new Set, entries = new Set;
|
|
1633
|
+
for (const program of module.programs) {
|
|
1634
|
+
declarations.claimId(program.id);
|
|
1635
|
+
const entry = signatures.get(program.entry);
|
|
1636
|
+
if (!entry || entry.owner || entry.parameters.length || entry.result)
|
|
1637
|
+
reject("Program entry must be a parameterless void helper.");
|
|
1638
|
+
if (names.has(program.name))
|
|
1639
|
+
reject(`Duplicate program ${program.name}.`);
|
|
1640
|
+
names.add(program.name);
|
|
1641
|
+
entries.add(program.entry);
|
|
1642
|
+
}
|
|
1643
|
+
if (!module.programs.length)
|
|
1644
|
+
reject("At least one program is required.");
|
|
1645
|
+
const visited = new Set, active = new Set;
|
|
1646
|
+
const visit = (id) => {
|
|
1647
|
+
if (active.has(id))
|
|
1648
|
+
reject("Recursive scan calls are prohibited.", signatures.get(id)?.span);
|
|
1649
|
+
if (visited.has(id))
|
|
1650
|
+
return;
|
|
1651
|
+
active.add(id);
|
|
1652
|
+
for (const child of edges.get(id) ?? []) {
|
|
1653
|
+
if (entries.has(child))
|
|
1654
|
+
reject("Program entries cannot be called as ordinary helpers.", signatures.get(id)?.span);
|
|
1655
|
+
visit(child);
|
|
1656
|
+
}
|
|
1657
|
+
active.delete(id);
|
|
1658
|
+
visited.add(id);
|
|
1659
|
+
};
|
|
1660
|
+
for (const fn of [...functions, ...normalizedBlocks])
|
|
1661
|
+
visit(fn.id);
|
|
1662
|
+
const routines = new Map([...functions, ...normalizedBlocks].map((fn) => [fn.id, fn]));
|
|
1663
|
+
const variables = new Map([
|
|
1664
|
+
...module.variables,
|
|
1665
|
+
...functions.flatMap((fn) => [...fn.parameters, ...fn.locals]),
|
|
1666
|
+
...normalizedBlocks.flatMap((block) => [
|
|
1667
|
+
...block.inputs,
|
|
1668
|
+
...block.outputs,
|
|
1669
|
+
...block.state,
|
|
1670
|
+
...block.locals
|
|
1671
|
+
])
|
|
1672
|
+
].map((variable) => [variable.id, variable]));
|
|
1673
|
+
const calledRoutine = (item) => {
|
|
1674
|
+
if (item.kind === "invoke")
|
|
1675
|
+
return item.function;
|
|
1676
|
+
if (item.kind !== "blockUpdate")
|
|
1677
|
+
return;
|
|
1678
|
+
const variable = variables.get(item.instance);
|
|
1679
|
+
return variable && instancedBlock(variable.type, types)?.block;
|
|
1680
|
+
};
|
|
1681
|
+
const nestedBodies = (item) => {
|
|
1682
|
+
if (item.kind === "for")
|
|
1683
|
+
return [item.body];
|
|
1684
|
+
if (item.kind === "if")
|
|
1685
|
+
return [item.then, item.else];
|
|
1686
|
+
if (item.kind === "switch")
|
|
1687
|
+
return [...item.cases.map((branch) => branch.body), item.fallback];
|
|
1688
|
+
return [];
|
|
1689
|
+
};
|
|
1690
|
+
const writes = new Map;
|
|
1691
|
+
const routineWrites = (id) => {
|
|
1692
|
+
const cached = writes.get(id);
|
|
1693
|
+
if (cached)
|
|
1694
|
+
return cached;
|
|
1695
|
+
const result = new Set;
|
|
1696
|
+
const inspect = (body) => {
|
|
1697
|
+
for (const item of body) {
|
|
1698
|
+
if (item.kind === "assign")
|
|
1699
|
+
result.add(item.root);
|
|
1700
|
+
if (item.kind === "for")
|
|
1701
|
+
result.add(item.counter);
|
|
1702
|
+
if (item.kind === "invoke") {
|
|
1703
|
+
if (item.destination)
|
|
1704
|
+
result.add(item.destination);
|
|
1705
|
+
for (const argument of item.arguments)
|
|
1706
|
+
if (argument.kind === "borrow")
|
|
1707
|
+
result.add(argument.place.root);
|
|
1708
|
+
}
|
|
1709
|
+
if (item.kind === "blockUpdate") {
|
|
1710
|
+
for (const input of Object.values(item.inputs))
|
|
1711
|
+
if (input.kind === "borrow")
|
|
1712
|
+
result.add(input.place.root);
|
|
1713
|
+
}
|
|
1714
|
+
for (const nested of nestedBodies(item))
|
|
1715
|
+
inspect(nested);
|
|
1716
|
+
}
|
|
1717
|
+
};
|
|
1718
|
+
inspect(routines.get(id).body);
|
|
1719
|
+
for (const child of edges.get(id) ?? [])
|
|
1720
|
+
for (const root of routineWrites(child))
|
|
1721
|
+
result.add(root);
|
|
1722
|
+
writes.set(id, result);
|
|
1723
|
+
return result;
|
|
1724
|
+
};
|
|
1725
|
+
const checkCalls = (body, counters) => {
|
|
1726
|
+
for (const item of body) {
|
|
1727
|
+
const called = calledRoutine(item);
|
|
1728
|
+
if (called && [...counters].some((counter) => routineWrites(called).has(counter)))
|
|
1729
|
+
reject("A called routine may mutate an active loop counter.", item.span);
|
|
1730
|
+
const nestedCounters = item.kind === "for" ? new Set([...counters, item.counter]) : counters;
|
|
1731
|
+
for (const nested of nestedBodies(item))
|
|
1732
|
+
checkCalls(nested, nestedCounters);
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
for (const routine of routines.values())
|
|
1736
|
+
checkCalls(routine.body, new Set);
|
|
1737
|
+
const reachable = new Set;
|
|
1738
|
+
const link = (id) => {
|
|
1739
|
+
if (reachable.has(id))
|
|
1740
|
+
return;
|
|
1741
|
+
reachable.add(id);
|
|
1742
|
+
for (const child of edges.get(id) ?? [])
|
|
1743
|
+
link(child);
|
|
1744
|
+
const owner = signatures.get(id)?.owner;
|
|
1745
|
+
if (owner)
|
|
1746
|
+
link(owner.block);
|
|
1747
|
+
for (const method of functions.filter((fn) => fn.owner?.block === id))
|
|
1748
|
+
link(method.id);
|
|
1749
|
+
for (const instance of blocks.get(id)?.state.filter((variable) => variable.storage === "instance") ?? []) {
|
|
1750
|
+
const type = instancedBlock(instance.type, types);
|
|
1751
|
+
if (type)
|
|
1752
|
+
link(type.block);
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
for (const entry of entries)
|
|
1756
|
+
link(entry);
|
|
1757
|
+
for (const variable of module.variables) {
|
|
1758
|
+
const type = instancedBlock(variable.type, types);
|
|
1759
|
+
if (type)
|
|
1760
|
+
link(type.block);
|
|
1761
|
+
}
|
|
1762
|
+
const linkedFunctions = functions.filter((fn) => reachable.has(fn.id)), linkedBlocks = normalizedBlocks.filter((block) => reachable.has(block.id));
|
|
1763
|
+
const usedTypes = new Set;
|
|
1764
|
+
const useType = (id) => {
|
|
1765
|
+
if (usedTypes.has(id))
|
|
1766
|
+
return;
|
|
1767
|
+
usedTypes.add(id);
|
|
1768
|
+
const type = types.get(id);
|
|
1769
|
+
if (type.kind === "record") {
|
|
1770
|
+
for (const field of type.fields)
|
|
1771
|
+
useType(field.type);
|
|
1772
|
+
}
|
|
1773
|
+
if (type.kind === "array") {
|
|
1774
|
+
useType(type.element);
|
|
1775
|
+
if (type.index)
|
|
1776
|
+
useType(type.index);
|
|
1777
|
+
}
|
|
1778
|
+
if (type.kind === "enum")
|
|
1779
|
+
useType(type.base);
|
|
1780
|
+
if (type.kind === "block" && type.output)
|
|
1781
|
+
useType(type.output);
|
|
1782
|
+
};
|
|
1783
|
+
types.forEach((type) => {
|
|
1784
|
+
if (["bool", "integer", "bits", "float", "duration", "calendar"].includes(type.kind))
|
|
1785
|
+
useType(type.id);
|
|
1786
|
+
});
|
|
1787
|
+
for (const variable of module.variables)
|
|
1788
|
+
useType(variable.type);
|
|
1789
|
+
for (const fn of linkedFunctions) {
|
|
1790
|
+
if (fn.result)
|
|
1791
|
+
useType(fn.result);
|
|
1792
|
+
for (const variable of [...fn.parameters, ...fn.locals])
|
|
1793
|
+
useType(variable.type);
|
|
1794
|
+
}
|
|
1795
|
+
for (const block of linkedBlocks) {
|
|
1796
|
+
if (block.result)
|
|
1797
|
+
useType(block.result);
|
|
1798
|
+
for (const variable of [...block.inputs, ...block.outputs, ...block.state, ...block.locals]) {
|
|
1799
|
+
useType(variable.type);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
return {
|
|
1803
|
+
types: module.types.filter((type) => usedTypes.has(type.id)),
|
|
1804
|
+
variables: module.variables,
|
|
1805
|
+
functions: linkedFunctions,
|
|
1806
|
+
blocks: linkedBlocks,
|
|
1807
|
+
programs: module.programs
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// packages/compiler/ir/core.ts
|
|
1812
|
+
var verifiedModules = new WeakSet;
|
|
1813
|
+
function isVerifiedCore(value) {
|
|
1814
|
+
return typeof value === "object" && value !== null && verifiedModules.has(value);
|
|
1815
|
+
}
|
|
1816
|
+
function freeze(value) {
|
|
1817
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value))
|
|
1818
|
+
return;
|
|
1819
|
+
for (const child of Object.values(value))
|
|
1820
|
+
freeze(child);
|
|
1821
|
+
Object.freeze(value);
|
|
1822
|
+
}
|
|
1823
|
+
function normalizeModule(module) {
|
|
1824
|
+
try {
|
|
1825
|
+
const declarations = new ModuleDeclarations(module);
|
|
1826
|
+
checkStaticAllocation(module, declarations);
|
|
1827
|
+
const functions = [], blocks = [];
|
|
1828
|
+
const edges = new Map;
|
|
1829
|
+
for (const source of [...module.functions, ...module.blocks]) {
|
|
1830
|
+
const normalized = new RoutineNormalizer(source, declarations).normalize();
|
|
1831
|
+
edges.set(source.id, normalized.callees);
|
|
1832
|
+
if ("inputs" in normalized.routine)
|
|
1833
|
+
blocks.push(normalized.routine);
|
|
1834
|
+
else
|
|
1835
|
+
functions.push(normalized.routine);
|
|
1836
|
+
}
|
|
1837
|
+
const linked = linkModule(module, declarations, functions, blocks, edges);
|
|
1838
|
+
const core = { ...structuredClone(linked), [verified]: true };
|
|
1839
|
+
freeze(core);
|
|
1840
|
+
verifiedModules.add(core);
|
|
1841
|
+
return { core, diagnostics: [] };
|
|
1842
|
+
} catch (cause) {
|
|
1843
|
+
if (cause instanceof InvalidCore)
|
|
1844
|
+
return { diagnostics: [cause.diagnostic] };
|
|
1845
|
+
throw cause;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
export {
|
|
1849
|
+
aggregate,
|
|
1850
|
+
aggregateIssue,
|
|
1851
|
+
applyIntegerPolicy,
|
|
1852
|
+
calendarLiteral,
|
|
1853
|
+
canAssign,
|
|
1854
|
+
comparisonOperators,
|
|
1855
|
+
constantFits,
|
|
1856
|
+
defaultValue,
|
|
1857
|
+
domainConstantFits,
|
|
1858
|
+
domainIssue,
|
|
1859
|
+
durationLiteral,
|
|
1860
|
+
error,
|
|
1861
|
+
expressionIssue,
|
|
1862
|
+
floatMaximum,
|
|
1863
|
+
fold,
|
|
1864
|
+
formatDiagnostic,
|
|
1865
|
+
hasErrors,
|
|
1866
|
+
instancedBlock,
|
|
1867
|
+
integerRange,
|
|
1868
|
+
integerType,
|
|
1869
|
+
interval,
|
|
1870
|
+
isVerifiedCore,
|
|
1871
|
+
layoutIssue,
|
|
1872
|
+
loopBounds,
|
|
1873
|
+
loopIterations,
|
|
1874
|
+
narrowRanges,
|
|
1875
|
+
normalizeModule,
|
|
1876
|
+
parseInteger,
|
|
1877
|
+
placeType,
|
|
1878
|
+
primitiveTypes,
|
|
1879
|
+
saturateFloat,
|
|
1880
|
+
scalarIssue,
|
|
1881
|
+
standardBlocks,
|
|
1882
|
+
valueFits,
|
|
1883
|
+
valueRange,
|
|
1884
|
+
wrapsNatively
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1887
|
+
//# debugId=56F86269D01021CE64756E2164756E21
|
|
1888
|
+
//# sourceMappingURL=ir.js.map
|