@tsplc/compiler 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +16 -2
  3. package/backends/codesys/config.ts +54 -0
  4. package/backends/codesys/emit.ts +1063 -0
  5. package/backends/codesys/engineering.ts +88 -0
  6. package/backends/codesys/http.ts +598 -0
  7. package/backends/codesys/index.ts +12 -0
  8. package/backends/codesys/io-paths.ts +103 -0
  9. package/backends/codesys/names.ts +76 -0
  10. package/backends/codesys/numeric.ts +356 -0
  11. package/backends/codesys/plan.ts +512 -0
  12. package/backends/codesys/publication.ts +59 -0
  13. package/backends/codesys/storage.ts +63 -0
  14. package/backends/codesys/task-access.ts +118 -0
  15. package/backends/codesys/types.ts +178 -0
  16. package/backends/codesys/vendor.ts +108 -0
  17. package/dist/cli.js +740 -0
  18. package/dist/cli.js.map +13 -0
  19. package/dist/clock.js +8 -0
  20. package/dist/clock.js.map +10 -0
  21. package/dist/codesys.js +2587 -0
  22. package/dist/codesys.js.map +22 -0
  23. package/dist/elaborate-capture.js +118 -0
  24. package/dist/elaborate-capture.js.map +10 -0
  25. package/dist/index.js +6859 -0
  26. package/dist/index.js.map +25 -0
  27. package/dist/ir.js +1888 -0
  28. package/dist/ir.js.map +22 -0
  29. package/dist/math.js +24 -0
  30. package/dist/math.js.map +10 -0
  31. package/dist/project.js +423 -0
  32. package/dist/project.js.map +11 -0
  33. package/ir/aggregates.ts +333 -0
  34. package/ir/allocation.ts +74 -0
  35. package/ir/core-model.ts +116 -0
  36. package/ir/core.ts +63 -0
  37. package/ir/declarations.ts +201 -0
  38. package/ir/diagnostics.ts +43 -0
  39. package/ir/domains.ts +96 -0
  40. package/ir/index.ts +7 -0
  41. package/ir/link.ts +176 -0
  42. package/ir/model.ts +268 -0
  43. package/ir/normalization-error.ts +10 -0
  44. package/ir/routine.ts +826 -0
  45. package/ir/scalars.ts +675 -0
  46. package/ir/types.ts +70 -0
  47. package/package.json +105 -4
  48. package/project/artifacts.ts +277 -0
  49. package/project/cli.ts +282 -0
  50. package/project/index.ts +1 -0
  51. package/project/project.ts +298 -0
  52. package/project/server.ts +83 -0
  53. package/src/frontend/async.ts +1060 -0
  54. package/src/frontend/blocks.ts +419 -0
  55. package/src/frontend/configuration.ts +167 -0
  56. package/src/frontend/const-functions.ts +153 -0
  57. package/src/frontend/cooperative.ts +75 -0
  58. package/src/frontend/elaborate/capture.ts +138 -0
  59. package/src/frontend/elaborate/graph.ts +94 -0
  60. package/src/frontend/elaborate/index.ts +302 -0
  61. package/src/frontend/elaborate/layouts.ts +390 -0
  62. package/src/frontend/elaborate/model.ts +177 -0
  63. package/src/frontend/elaborate/specialize.ts +2280 -0
  64. package/src/frontend/exposure.ts +147 -0
  65. package/src/frontend/module-graph.ts +63 -0
  66. package/src/frontend/promises.ts +751 -0
  67. package/src/frontend/services.ts +435 -0
  68. package/src/frontend/types.ts +578 -0
  69. package/src/frontend/typescript.ts +2442 -0
  70. package/src/index.ts +7 -0
  71. package/stdlib/clock.ts +2 -0
  72. package/stdlib/math.ts +19 -0
  73. package/types/async.d.ts +81 -0
  74. package/types/clock.d.ts +4 -0
  75. package/types/config.d.ts +2 -0
  76. package/types/data-types.d.ts +31 -0
  77. package/types/globals.d.ts +40 -0
  78. package/types/index.d.ts +111 -0
  79. package/types/plc.d.ts +5 -0
package/ir/scalars.ts ADDED
@@ -0,0 +1,675 @@
1
+ import {
2
+ comparisonOperators,
3
+ type Constant,
4
+ type Expression,
5
+ type MathHelper,
6
+ type Variable,
7
+ } from "./model.ts";
8
+ import { integerRange, type PlcType } from "./types.ts";
9
+
10
+ export function integerType(type: PlcType): type is Extract<PlcType, { kind: "integer" | "bits" }> {
11
+ return type.kind === "integer" || type.kind === "bits";
12
+ }
13
+ export function floatMaximum(type: Extract<PlcType, { kind: "float" }>): number {
14
+ return type.bits === 32 ? Math.fround((2 - 2 ** -23) * 2 ** 127) : Number.MAX_VALUE;
15
+ }
16
+
17
+ export function saturateFloat(value: number, type: Extract<PlcType, { kind: "float" }>): number {
18
+ // Clamp before target rounding so a finite source cannot round to infinity.
19
+ // Comparisons leave signed zero intact; min/max substitution is unnecessary.
20
+ const maximum = floatMaximum(type);
21
+ const clipped = value < -maximum ? -maximum : value > maximum ? maximum : value;
22
+ return type.bits === 32 ? Math.fround(clipped) : clipped;
23
+ }
24
+
25
+ export function parseInteger(text: string): bigint {
26
+ const clean = text.replaceAll("_", "").replace(/n$/, "");
27
+ const negative = clean.startsWith("-");
28
+ const unsigned = negative ? clean.slice(1) : clean;
29
+ if (/^0[xob]/i.test(unsigned)) return (negative ? -1n : 1n) * BigInt(unsigned);
30
+ const match = /^(\d+)(?:\.(\d*))?(?:e([+-]?\d+))?$/i.exec(unsigned);
31
+ if (!match) throw new Error("Expected an exact integer literal.");
32
+ const fraction = match[2] ?? "";
33
+ const exponent = Number(match[3] ?? 0) - fraction.length;
34
+ if (!Number.isSafeInteger(exponent) || Math.abs(exponent) > 400)
35
+ throw new Error("Integer literal exponent is out of range.");
36
+ let value = BigInt(match[1]! + fraction);
37
+ if (exponent >= 0) value *= 10n ** BigInt(exponent);
38
+ else {
39
+ const divisor = 10n ** BigInt(-exponent);
40
+ if (value % divisor) throw new Error("Integer literals cannot have a fractional part.");
41
+ value /= divisor;
42
+ }
43
+ return negative ? -value : value;
44
+ }
45
+ export function constantFits(value: Constant, type: PlcType): boolean {
46
+ if (type.kind === "bool") return value.kind === "bool" && typeof value.value === "boolean";
47
+ if (integerType(type)) {
48
+ if (value.kind !== "integer" || !/^(?:0|-?[1-9][0-9]*)$/.test(value.value)) return false;
49
+ const [minimum, maximum] = integerRange(type),
50
+ number = BigInt(value.value);
51
+ return number >= minimum && number <= maximum;
52
+ }
53
+ if (type.kind === "float")
54
+ return (
55
+ value.kind === "float" &&
56
+ Number.isFinite(value.value) &&
57
+ (type.bits === 64 || Object.is(Math.fround(value.value), value.value))
58
+ );
59
+ return false;
60
+ }
61
+ export function canAssign(source: PlcType, destination: PlcType, value?: Constant): boolean {
62
+ if (value) {
63
+ if (source.kind !== destination.kind || !constantFits(value, destination)) return false;
64
+ if (
65
+ source.kind === "float" &&
66
+ destination.kind === "float" &&
67
+ source.bits > destination.bits &&
68
+ value.kind === "float"
69
+ )
70
+ return Object.is(Math.fround(value.value), value.value);
71
+ return true;
72
+ }
73
+ if (source.id === destination.id) return true;
74
+ if (source.kind === "integer" && destination.kind === "integer") {
75
+ const [fromMin, fromMax] = integerRange(source),
76
+ [toMin, toMax] = integerRange(destination);
77
+ return fromMin >= toMin && fromMax <= toMax;
78
+ }
79
+ return source.kind === "float" && destination.kind === "float" && source.bits <= destination.bits;
80
+ }
81
+ export function fold(
82
+ expression: Expression,
83
+ variables: ReadonlyMap<string, Variable>,
84
+ types?: ReadonlyMap<string, PlcType>,
85
+ ): Constant | undefined {
86
+ if (expression.kind === "constant") return expression.value;
87
+ if (expression.kind === "copy") return fold(expression.operand, variables, types);
88
+ if (expression.kind === "length") {
89
+ const type = types?.get(expression.operand.type);
90
+ const receiver =
91
+ expression.operand.kind === "load" ? variables.get(expression.operand.place.root) : undefined;
92
+ const pure =
93
+ expression.operand.kind === "load" &&
94
+ expression.operand.place.path.every((segment) => segment.kind === "field") &&
95
+ Boolean(receiver?.initial || receiver?.storage === "input" || receiver?.storage === "output");
96
+ if (type?.kind === "array" && (pure || fold(expression.operand, variables, types)))
97
+ return { kind: "integer", value: String(type.length) };
98
+ return undefined;
99
+ }
100
+ if (expression.kind === "record") {
101
+ const fields: Record<string, Constant> = Object.create(null);
102
+ for (const [name, operand] of Object.entries(expression.fields)) {
103
+ const value = fold(operand, variables, types);
104
+ if (!value) return undefined;
105
+ fields[name] = value;
106
+ }
107
+ return { kind: "record", fields };
108
+ }
109
+ if (expression.kind === "array") {
110
+ const items = expression.items.map((item) => fold(item, variables, types));
111
+ return items.every((item): item is Constant => item !== undefined)
112
+ ? { kind: "array", items }
113
+ : undefined;
114
+ }
115
+ if (expression.kind === "load" && !expression.place.path.length) {
116
+ const variable = variables.get(expression.place.root);
117
+ return variable?.storage === "constant" ? variable.initial : undefined;
118
+ }
119
+ if (expression.kind === "convert") {
120
+ const value = fold(expression.operand, variables, types);
121
+ const target = types?.get(expression.type);
122
+ if (value?.kind === "integer" && target) {
123
+ if (integerType(target))
124
+ return {
125
+ kind: "integer",
126
+ value: String(
127
+ expression.policy === "exact"
128
+ ? BigInt(value.value)
129
+ : applyIntegerPolicy(BigInt(value.value), target, expression.policy),
130
+ ),
131
+ };
132
+ if (target.kind === "float")
133
+ return {
134
+ kind: "float",
135
+ value: target.bits === 32 ? Math.fround(Number(value.value)) : Number(value.value),
136
+ };
137
+ }
138
+ if (value?.kind === "float" && target?.kind === "float")
139
+ return {
140
+ kind: "float",
141
+ value:
142
+ expression.policy === "saturate"
143
+ ? saturateFloat(value.value, target)
144
+ : target.bits === 32
145
+ ? Math.fround(value.value)
146
+ : value.value,
147
+ };
148
+ return undefined;
149
+ }
150
+ if (expression.kind === "unary") {
151
+ const value = fold(expression.operand, variables, types);
152
+ if (value?.kind === "duration" && expression.operator === "timeToMilliseconds")
153
+ return { kind: "integer", value: String(BigInt(value.nanoseconds) / 1_000_000n) };
154
+ if (value?.kind === "bool" && expression.operator === "not")
155
+ return { kind: "bool", value: !value.value };
156
+ if (value?.kind === "integer" && expression.operator === "bitNot") {
157
+ const target = types?.get(expression.type);
158
+ if (target?.kind === "bits")
159
+ return {
160
+ kind: "integer",
161
+ value: String(applyIntegerPolicy(~BigInt(value.value), target, "wrap")),
162
+ };
163
+ }
164
+ if (value?.kind === "integer" && expression.operator === "negate")
165
+ return { kind: "integer", value: String(-BigInt(value.value)) };
166
+ if (value?.kind === "float" && expression.operator === "negate")
167
+ return { kind: "float", value: -value.value };
168
+ }
169
+ if (expression.kind === "binary") {
170
+ const left = fold(expression.left, variables, types),
171
+ right = fold(expression.right, variables, types);
172
+ if (!left || !right || left.kind !== right.kind) return undefined;
173
+ if (left.kind === "bool" && right.kind === "bool") {
174
+ if (expression.operator === "and") return { kind: "bool", value: left.value && right.value };
175
+ if (expression.operator === "or") return { kind: "bool", value: left.value || right.value };
176
+ if (expression.operator === "equal")
177
+ return { kind: "bool", value: left.value === right.value };
178
+ if (expression.operator === "notEqual")
179
+ return { kind: "bool", value: left.value !== right.value };
180
+ }
181
+ if (
182
+ (left.kind === "integer" && right.kind === "integer") ||
183
+ (left.kind === "float" && right.kind === "float") ||
184
+ (left.kind === "duration" && right.kind === "duration") ||
185
+ (left.kind === "calendar" && right.kind === "calendar") ||
186
+ (left.kind === "string" && right.kind === "string")
187
+ ) {
188
+ const a =
189
+ left.kind === "integer"
190
+ ? BigInt(left.value)
191
+ : left.kind === "duration"
192
+ ? BigInt(left.nanoseconds)
193
+ : left.value;
194
+ const b =
195
+ right.kind === "integer"
196
+ ? BigInt(right.value)
197
+ : right.kind === "duration"
198
+ ? BigInt(right.nanoseconds)
199
+ : right.value;
200
+ switch (expression.operator) {
201
+ case "equal":
202
+ return { kind: "bool", value: a === b };
203
+ case "notEqual":
204
+ return { kind: "bool", value: a !== b };
205
+ case "less":
206
+ return { kind: "bool", value: a < b };
207
+ case "lessEqual":
208
+ return { kind: "bool", value: a <= b };
209
+ case "greater":
210
+ return { kind: "bool", value: a > b };
211
+ case "greaterEqual":
212
+ return { kind: "bool", value: a >= b };
213
+ }
214
+ if (left.kind !== "integer" && left.kind !== "float") return undefined;
215
+ if (typeof a === "bigint" && typeof b === "bigint") {
216
+ let value: bigint;
217
+ switch (expression.operator) {
218
+ case "add":
219
+ value = a + b;
220
+ break;
221
+ case "subtract":
222
+ value = a - b;
223
+ break;
224
+ case "multiply":
225
+ value = a * b;
226
+ break;
227
+ case "divide":
228
+ if (b === 0n) return undefined;
229
+ value = a / b;
230
+ break;
231
+ case "remainder":
232
+ if (b === 0n) return undefined;
233
+ value = a % b;
234
+ break;
235
+ case "bitAnd":
236
+ value = a & b;
237
+ break;
238
+ case "bitOr":
239
+ value = a | b;
240
+ break;
241
+ case "bitXor":
242
+ value = a ^ b;
243
+ break;
244
+ case "shiftLeft":
245
+ if (b < 0n || b > 63n) return undefined;
246
+ value = a << b;
247
+ break;
248
+ case "shiftRight":
249
+ if (b < 0n || b > 63n) return undefined;
250
+ value = a >> b;
251
+ break;
252
+ default:
253
+ return undefined;
254
+ }
255
+ const target = types?.get(expression.type);
256
+ if (target && integerType(target) && (expression.policy || target.kind === "bits"))
257
+ value = applyIntegerPolicy(value, target, expression.policy ?? "wrap");
258
+ return { kind: "integer", value: String(value) };
259
+ }
260
+ if (typeof a === "number" && typeof b === "number") {
261
+ let value: number;
262
+ switch (expression.operator) {
263
+ case "add":
264
+ value = a + b;
265
+ break;
266
+ case "subtract":
267
+ value = a - b;
268
+ break;
269
+ case "multiply":
270
+ value = a * b;
271
+ break;
272
+ case "divide":
273
+ value = a / b;
274
+ break;
275
+ case "remainder":
276
+ value = a % b;
277
+ break;
278
+ default:
279
+ return undefined;
280
+ }
281
+ return {
282
+ kind: "float",
283
+ value:
284
+ types?.get(expression.type)?.kind === "float" && expression.type === "REAL"
285
+ ? Math.fround(value)
286
+ : value,
287
+ };
288
+ }
289
+ }
290
+ }
291
+ return undefined;
292
+ }
293
+ export type IntegerInterval = readonly [bigint, bigint];
294
+ export function interval(
295
+ expression: Expression,
296
+ types: ReadonlyMap<string, PlcType>,
297
+ variables: ReadonlyMap<string, Variable>,
298
+ ranges: ReadonlyMap<string, IntegerInterval> = new Map(),
299
+ ): IntegerInterval | undefined {
300
+ if (expression.kind === "length") {
301
+ const array = types.get(expression.operand.type);
302
+ if (array?.kind === "array") return [BigInt(array.length), BigInt(array.length)];
303
+ }
304
+ const value = fold(expression, variables, types);
305
+ if (value?.kind === "integer") return [BigInt(value.value), BigInt(value.value)];
306
+ const type = types.get(expression.type);
307
+ if (!type || !integerType(type)) return undefined;
308
+ if (expression.kind === "load") return ranges.get(expression.place.root) ?? integerRange(type);
309
+ if (expression.kind === "convert")
310
+ return expression.policy === "exact"
311
+ ? interval(expression.operand, types, variables, ranges)
312
+ : integerRange(type);
313
+ if (expression.kind === "unary" && expression.operator === "negate") {
314
+ const operand = interval(expression.operand, types, variables, ranges);
315
+ return operand && [-operand[1], -operand[0]];
316
+ }
317
+ if (expression.kind === "binary" && !expression.policy) {
318
+ const a = interval(expression.left, types, variables, ranges),
319
+ b = interval(expression.right, types, variables, ranges);
320
+ if (!a || !b) return undefined;
321
+ const [minimum, maximum] = integerRange(type);
322
+ const unwrapped = (low: bigint, high: bigint): [bigint, bigint] =>
323
+ low < minimum || high > maximum ? [minimum, maximum] : [low, high];
324
+ switch (expression.operator) {
325
+ case "add":
326
+ return unwrapped(a[0] + b[0], a[1] + b[1]);
327
+ case "subtract":
328
+ return unwrapped(a[0] - b[1], a[1] - b[0]);
329
+ case "multiply": {
330
+ const values = [a[0] * b[0], a[0] * b[1], a[1] * b[0], a[1] * b[1]];
331
+ return unwrapped(
332
+ values.reduce((x, y) => (x < y ? x : y)),
333
+ values.reduce((x, y) => (x > y ? x : y)),
334
+ );
335
+ }
336
+ case "divide": {
337
+ if (b[0] <= 0n && b[1] >= 0n) return undefined;
338
+ const values = [a[0] / b[0], a[0] / b[1], a[1] / b[0], a[1] / b[1]];
339
+ return [values.reduce((x, y) => (x < y ? x : y)), values.reduce((x, y) => (x > y ? x : y))];
340
+ }
341
+ case "remainder": {
342
+ if (b[0] <= 0n && b[1] >= 0n) return undefined;
343
+ const maximum =
344
+ [b[0], b[1]].map((n) => (n < 0n ? -n : n)).reduce((x, y) => (x > y ? x : y)) - 1n;
345
+ return [a[0] < 0n ? -maximum : 0n, a[1] > 0n ? maximum : 0n];
346
+ }
347
+ }
348
+ }
349
+ return integerRange(type);
350
+ }
351
+ // FOR iterations from first to last inclusive; step is nonzero.
352
+ export function loopIterations(first: bigint, last: bigint, step: bigint): bigint {
353
+ if (step > 0n) return last >= first ? (last - first) / step + 1n : 0n;
354
+ return last <= first ? (first - last) / -step + 1n : 0n;
355
+ }
356
+ export function loopBounds(
357
+ start: IntegerInterval,
358
+ limit: IntegerInterval,
359
+ step: bigint,
360
+ exclusive = false,
361
+ ) {
362
+ const first = step > 0n ? start[0] : start[1];
363
+ const last = step > 0n ? limit[1] - (exclusive ? 1n : 0n) : limit[0] + (exclusive ? 1n : 0n);
364
+ const count = loopIterations(first, last, step);
365
+ const range: IntegerInterval = first <= last ? [first, last] : [last, first];
366
+ return { first, last, count, range };
367
+ }
368
+
369
+ // Only storage no statement can change carries branch refinements: loop counters,
370
+ // by-value parameters and const locals, so the proof survives through the branch.
371
+ export function narrowRanges(
372
+ condition: Expression,
373
+ truth: boolean,
374
+ types: ReadonlyMap<string, PlcType>,
375
+ variables: ReadonlyMap<string, Variable>,
376
+ ranges: ReadonlyMap<string, IntegerInterval>,
377
+ ): Map<string, IntegerInterval> {
378
+ const narrowed = new Map(ranges);
379
+ if (condition.kind !== "binary") return narrowed;
380
+ if (condition.operator === "and")
381
+ return truth
382
+ ? narrowRanges(
383
+ condition.right,
384
+ truth,
385
+ types,
386
+ variables,
387
+ narrowRanges(condition.left, truth, types, variables, ranges),
388
+ )
389
+ : narrowed;
390
+ if (condition.left.kind !== "load" || condition.left.place.path.length) return narrowed;
391
+ const root = condition.left.place.root,
392
+ variable = variables.get(root),
393
+ type = types.get(condition.left.type),
394
+ fixed =
395
+ variable &&
396
+ ((variable.storage === "parameter" && !variable.mode) ||
397
+ (variable.storage === "local" && !variable.mutable)),
398
+ current =
399
+ ranges.get(root) ?? (fixed && type && integerType(type) ? integerRange(type) : undefined),
400
+ bound = interval(condition.right, types, variables, ranges);
401
+ if (!current || !bound || bound[0] !== bound[1]) return narrowed;
402
+ const value = bound[0];
403
+ let lower = current[0],
404
+ upper = current[1];
405
+ switch (condition.operator) {
406
+ case "less":
407
+ if (truth) upper = value - 1n < upper ? value - 1n : upper;
408
+ else lower = value > lower ? value : lower;
409
+ break;
410
+ case "lessEqual":
411
+ if (truth) upper = value < upper ? value : upper;
412
+ else lower = value + 1n > lower ? value + 1n : lower;
413
+ break;
414
+ case "greater":
415
+ if (truth) lower = value + 1n > lower ? value + 1n : lower;
416
+ else upper = value < upper ? value : upper;
417
+ break;
418
+ case "greaterEqual":
419
+ if (truth) lower = value > lower ? value : lower;
420
+ else upper = value - 1n < upper ? value - 1n : upper;
421
+ break;
422
+ }
423
+ if (lower <= upper) narrowed.set(root, [lower, upper]);
424
+ return narrowed;
425
+ }
426
+
427
+ /** Plain integer +, - and * are native IEC arithmetic, which wraps on overflow as hand-written ST does. */
428
+ export function wrapsNatively(expression: {
429
+ kind: string;
430
+ operator?: string;
431
+ policy?: string;
432
+ }): boolean {
433
+ return (
434
+ expression.kind === "binary" &&
435
+ !expression.policy &&
436
+ (expression.operator === "add" ||
437
+ expression.operator === "subtract" ||
438
+ expression.operator === "multiply")
439
+ );
440
+ }
441
+
442
+ const lowest = (a: bigint, b: bigint): bigint => (a < b ? a : b);
443
+ const highest = (a: bigint, b: bigint): bigint => (a > b ? a : b);
444
+
445
+ /** A value's proved integer range; a math helper's result follows from its arguments. */
446
+ export function valueRange(
447
+ expression: Expression,
448
+ types: ReadonlyMap<string, PlcType>,
449
+ variables: ReadonlyMap<string, Variable>,
450
+ ranges: ReadonlyMap<string, IntegerInterval>,
451
+ functions: ReadonlyMap<string, { bounds?: MathHelper }>,
452
+ ): IntegerInterval | undefined {
453
+ if (expression.kind !== "call") return interval(expression, types, variables, ranges);
454
+ const helper = functions.get(expression.function)?.bounds;
455
+ return (
456
+ helper &&
457
+ mathHelperRange(
458
+ helper,
459
+ expression.arguments.map((argument) =>
460
+ valueRange(argument, types, variables, ranges, functions),
461
+ ),
462
+ )
463
+ );
464
+ }
465
+
466
+ function mathHelperRange(
467
+ helper: MathHelper,
468
+ [first, second, third]: readonly (IntegerInterval | undefined)[],
469
+ ): IntegerInterval | undefined {
470
+ if (!first || !second) return undefined;
471
+ if (helper === "min") return [lowest(first[0], second[0]), lowest(first[1], second[1])];
472
+ if (helper === "max") return [highest(first[0], second[0]), highest(first[1], second[1])];
473
+ // clamp(value, lower, upper) returns one of its three arguments, never outside lower..upper,
474
+ // and only lower or upper when the bounds are reversed.
475
+ if (!third) return undefined;
476
+ return [lowest(second[0], third[0]), highest(second[1], third[1])];
477
+ }
478
+
479
+ export function applyIntegerPolicy(
480
+ value: bigint,
481
+ type: Extract<PlcType, { kind: "integer" | "bits" }>,
482
+ policy: "wrap" | "saturate",
483
+ ): bigint {
484
+ const [minimum, maximum] = integerRange(type);
485
+ if (policy === "saturate") return value < minimum ? minimum : value > maximum ? maximum : value;
486
+ return type.kind === "integer" && type.signed
487
+ ? BigInt.asIntN(type.bits, value)
488
+ : BigInt.asUintN(type.bits, value);
489
+ }
490
+
491
+ export function scalarIssue(
492
+ expression: Expression,
493
+ types: ReadonlyMap<string, PlcType>,
494
+ variables: ReadonlyMap<string, Variable>,
495
+ ranges: ReadonlyMap<string, IntegerInterval> = new Map(),
496
+ ): string | undefined {
497
+ const type = types.get(expression.type);
498
+ if (!type || !["bool", "integer", "bits", "float"].includes(type.kind))
499
+ return "Unknown scalar expression type.";
500
+ if (expression.kind === "constant")
501
+ return constantFits(expression.value, type)
502
+ ? undefined
503
+ : "Literal is outside its PLC type or has the wrong domain.";
504
+ if (expression.kind === "load" || expression.kind === "call") return undefined;
505
+ if (
506
+ ("operand" in expression && !types.has(expression.operand.type)) ||
507
+ ("left" in expression &&
508
+ (!types.has(expression.left.type) || !types.has(expression.right.type)))
509
+ )
510
+ return "Unknown operand type.";
511
+ // Operand types were checked above.
512
+ const operand = (value: Expression): PlcType => types.get(value.type)!;
513
+ if (expression.kind === "convert") {
514
+ if (!["exact", "wrap", "saturate"].includes(expression.policy))
515
+ return "Unknown numeric conversion policy.";
516
+ const source = operand(expression.operand),
517
+ value = fold(expression.operand, variables, types);
518
+ if (source.kind === "bool" || type.kind === "bool")
519
+ return "Numeric conversions cannot use BOOL.";
520
+ if (expression.policy === "exact") {
521
+ if (
522
+ canAssign(source, type, value) ||
523
+ (value && integerType(source) && integerType(type) && constantFits(value, type))
524
+ )
525
+ return undefined;
526
+ if (integerType(source) && type.kind === "float") {
527
+ if (value?.kind === "integer") {
528
+ const converted =
529
+ type.bits === 32 ? Math.fround(Number(value.value)) : Number(value.value);
530
+ if (
531
+ Number.isFinite(converted) &&
532
+ Number.isInteger(converted) &&
533
+ BigInt(converted) === BigInt(value.value)
534
+ )
535
+ return undefined;
536
+ }
537
+ const [minimum, maximum] =
538
+ interval(expression.operand, types, variables, ranges) ?? integerRange(source);
539
+ const limit = 1n << BigInt(type.bits === 32 ? 24 : 53);
540
+ if (minimum >= -limit && maximum <= limit) return undefined;
541
+ }
542
+ if (integerType(source) && integerType(type)) {
543
+ const [fromMin, fromMax] =
544
+ interval(expression.operand, types, variables, ranges) ?? integerRange(source),
545
+ [toMin, toMax] = integerRange(type);
546
+ if (fromMin >= toMin && fromMax <= toMax) return undefined;
547
+ }
548
+ return "Exact conversion cannot be proved lossless.";
549
+ }
550
+ if (expression.policy === "saturate" && source.kind === "float" && type.kind === "float")
551
+ return undefined;
552
+ return integerType(source) && integerType(type)
553
+ ? undefined
554
+ : expression.policy === "saturate"
555
+ ? "Saturate conversions require two integer/bit-string operands or two floating operands."
556
+ : "Wrap conversions require integer or bit-string operands.";
557
+ }
558
+ if (expression.kind === "unary") {
559
+ const source = operand(expression.operand);
560
+ if (expression.operator === "clock")
561
+ return type.id === "DINT" ? undefined : "now() returns DINT milliseconds.";
562
+ if (expression.operator === "timeToMilliseconds")
563
+ return source.id === "TIME" && source.kind === "duration" && type.id === "UDINT"
564
+ ? undefined
565
+ : "timeToMilliseconds requires TIME and returns UDINT.";
566
+ if (expression.operator === "not")
567
+ return type.kind === "bool" && source.kind === "bool"
568
+ ? undefined
569
+ : "Logical negation requires BOOL.";
570
+ if (expression.operator === "bitNot")
571
+ return type.kind === "bits" && source.id === type.id
572
+ ? undefined
573
+ : "Bitwise complement requires a bit string.";
574
+ if (source.id !== type.id || !(type.kind === "integer" || type.kind === "float"))
575
+ return "Numeric negation requires one arithmetic type.";
576
+ } else if (expression.kind === "binary") {
577
+ const left = operand(expression.left),
578
+ right = operand(expression.right);
579
+ if (expression.operator === "and" || expression.operator === "or")
580
+ return type.kind === "bool" && left.kind === "bool" && right.kind === "bool"
581
+ ? undefined
582
+ : "Logical operators require BOOL.";
583
+ if (comparisonOperators.has(expression.operator))
584
+ return type.kind === "bool" &&
585
+ left.id === right.id &&
586
+ ["bool", "integer", "bits", "float"].includes(left.kind) &&
587
+ (left.kind !== "bool" || ["equal", "notEqual"].includes(expression.operator))
588
+ ? undefined
589
+ : "Comparison operands need the same PLC domain.";
590
+ if (["bitAnd", "bitOr", "bitXor", "shiftLeft", "shiftRight"].includes(expression.operator)) {
591
+ if (type.kind !== "bits" || left.id !== type.id)
592
+ return "Bitwise operators require bit strings.";
593
+ if (expression.operator === "shiftLeft" || expression.operator === "shiftRight") {
594
+ const count = fold(expression.right, variables, types);
595
+ return count?.kind === "integer" &&
596
+ BigInt(count.value) >= 0n &&
597
+ BigInt(count.value) < BigInt(type.bits)
598
+ ? undefined
599
+ : "Shift count must be a compile-time integer within the bit width.";
600
+ }
601
+ return right.id === type.id ? undefined : "Bit-string widths differ.";
602
+ }
603
+ if (
604
+ left.id !== type.id ||
605
+ right.id !== type.id ||
606
+ (type.kind !== "integer" && type.kind !== "float")
607
+ )
608
+ return "Arithmetic operands need one integer or floating type.";
609
+ if (expression.policy)
610
+ return type.kind === "integer" &&
611
+ ["add", "subtract", "multiply"].includes(expression.operator)
612
+ ? undefined
613
+ : "Explicit arithmetic policies require integer addition, subtraction or multiplication.";
614
+ if (
615
+ (expression.operator === "divide" || expression.operator === "remainder") &&
616
+ type.kind === "integer"
617
+ ) {
618
+ const divisor = interval(expression.right, types, variables, ranges);
619
+ if (!divisor || (divisor[0] <= 0n && divisor[1] >= 0n))
620
+ return "Division requires a proved nonzero divisor.";
621
+ const dividend = interval(expression.left, types, variables, ranges);
622
+ const [minimum] = integerRange(type);
623
+ if (
624
+ type.signed &&
625
+ dividend &&
626
+ dividend[0] <= minimum &&
627
+ divisor[0] <= -1n &&
628
+ divisor[1] >= -1n
629
+ )
630
+ return "Signed minimum divided by -1 cannot be represented.";
631
+ }
632
+ } else return "Expression has not been reduced to a scalar operation.";
633
+ if (integerType(type)) {
634
+ const result = interval(expression, types, variables, ranges),
635
+ [minimum, maximum] = integerRange(type);
636
+ if (
637
+ !(wrapsNatively(expression) && !fold(expression, variables, types)) &&
638
+ (!result || result[0] < minimum || result[1] > maximum)
639
+ )
640
+ return "Arithmetic may overflow; widen the result or use an explicit wrap/saturate operation.";
641
+ } else if (type.kind === "float") {
642
+ const result = fold(expression, variables, types);
643
+ if (result)
644
+ return constantFits(result, type)
645
+ ? undefined
646
+ : "Floating result is not finite or representable.";
647
+ if (expression.kind === "binary") {
648
+ const lhs = fold(expression.left, variables, types);
649
+ const rhs = fold(expression.right, variables, types);
650
+ if (
651
+ ["add", "subtract"].includes(expression.operator) &&
652
+ (lhs?.kind === "float" || rhs?.kind === "float")
653
+ ) {
654
+ // Addition/subtraction and nearest rounding are monotone. Checking both
655
+ // finite endpoints proves every finite input without changing its arithmetic.
656
+ const maximum = floatMaximum(type);
657
+ const finite = [-maximum, maximum].every((endpoint) => {
658
+ const a = lhs?.kind === "float" ? lhs.value : endpoint;
659
+ const b = rhs?.kind === "float" ? rhs.value : endpoint;
660
+ const value = expression.operator === "add" ? a + b : a - b;
661
+ return Number.isFinite(type.bits === 32 ? Math.fround(value) : value);
662
+ });
663
+ if (finite) return undefined;
664
+ }
665
+ if (
666
+ rhs?.kind === "float" &&
667
+ rhs.value === 1 &&
668
+ ["multiply", "divide"].includes(expression.operator)
669
+ )
670
+ return undefined;
671
+ } else if (expression.kind === "unary") return undefined;
672
+ return "Floating arithmetic must prove a finite result.";
673
+ }
674
+ return undefined;
675
+ }