@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
@@ -0,0 +1,12 @@
1
+ export { type CodesysOptions, codesys, direct } from "./config.ts";
2
+ export { emitCodesys, type SourceMapping } from "./emit.ts";
3
+ export {
4
+ buildCodesys,
5
+ codesysBuildConfig,
6
+ type CodesysEngineeringBuild,
7
+ engineeringBuildSchema,
8
+ } from "./engineering.ts";
9
+ export { type CodesysPlan, planCodesys } from "./plan.ts";
10
+ export { diagnoseCodesys } from "./vendor.ts";
11
+ export { ioTargets } from "./io-paths.ts";
12
+ export { publicationSelectors } from "./publication.ts";
@@ -0,0 +1,103 @@
1
+ import type { PlcType, Variable, VerifiedCoreModule } from "@tsplc/compiler/ir";
2
+ import { type IoTarget, ioLayout } from "@tsplc/typegen/hardware";
3
+ import { moduleName } from "./storage.ts";
4
+
5
+ type StorageSegment =
6
+ { kind: "field"; record: string; name: string } | { kind: "index"; value: number };
7
+
8
+ /** An `io` key that names a field of authored module storage, e.g. `PlantIO.Motor.Output.Run`. */
9
+ export interface StorageBinding {
10
+ key: string;
11
+ root: Variable;
12
+ segments: readonly StorageSegment[];
13
+ type: PlcType;
14
+ }
15
+
16
+ const pathPattern = /^([\w$-]+)\.([A-Za-z_$][\w$]*)((?:\.[A-Za-z_$][\w$]*|\[-?\d+\])*)$/;
17
+
18
+ export const isStoragePath = (key: string): boolean => key.includes(".") || key.includes("[");
19
+
20
+ /** Resolve a storage path against verified types; the error explains the first unresolvable segment. */
21
+ export function resolveStoragePath(
22
+ core: VerifiedCoreModule,
23
+ key: string,
24
+ ): StorageBinding | { error: string } {
25
+ const match = pathPattern.exec(key);
26
+ if (!match) return { error: `${key}: expected Module.variable followed by .field or [index].` };
27
+ const [, module, name, rest] = match;
28
+ const roots = core.variables.filter(
29
+ (item) => item.storage === "module" && item.name === name && moduleName(item) === module,
30
+ );
31
+ if (roots.length !== 1)
32
+ return {
33
+ error: roots.length
34
+ ? `${key}: ${module}.${name} is declared by more than one module named ${module}.`
35
+ : `${key}: no mutable module variable ${module}.${name}.`,
36
+ };
37
+ const root = roots[0]!;
38
+ const types = new Map(core.types.map((type) => [type.id, type]));
39
+ let type = types.get(root.type)!;
40
+ const segments: StorageSegment[] = [];
41
+ for (const [, field, index] of rest!.matchAll(/\.([A-Za-z_$][\w$]*)|\[(-?\d+)\]/g)) {
42
+ if (field !== undefined) {
43
+ const member = type.kind === "record" ? type.fields.find((f) => f.name === field) : undefined;
44
+ if (!member) return { error: `${key}: no field ${field}.` };
45
+ segments.push({ kind: "field", record: type.id, name: field });
46
+ type = types.get(member.type)!;
47
+ } else {
48
+ const value = Number(index);
49
+ if (type.kind !== "array") return { error: `${key}: ${index} indexes a non-array value.` };
50
+ if (value < type.lowerBound || value >= type.lowerBound + type.length)
51
+ return {
52
+ error: `${key}: index ${value} is outside ${type.lowerBound}..${type.lowerBound + type.length - 1}.`,
53
+ };
54
+ segments.push({ kind: "index", value });
55
+ type = types.get(type.element)!;
56
+ }
57
+ }
58
+ return { key, root, segments, type };
59
+ }
60
+
61
+ /** IEC spelling of a bindable type, as typegen spells channels; undefined for records, enums, strings. */
62
+ export function iecSpelling(
63
+ type: PlcType,
64
+ types: ReadonlyMap<string, PlcType>,
65
+ ): string | undefined {
66
+ if (
67
+ type.kind === "bool" ||
68
+ type.kind === "integer" ||
69
+ type.kind === "bits" ||
70
+ type.kind === "float"
71
+ )
72
+ return type.id;
73
+ if (type.kind !== "array") return undefined;
74
+ const element = iecSpelling(types.get(type.element)!, types);
75
+ const upper = type.lowerBound + type.length - 1;
76
+ return element && !element.startsWith("ARRAY")
77
+ ? `ARRAY[${type.lowerBound}..${upper}] OF ${element}`
78
+ : undefined;
79
+ }
80
+
81
+ /** Every key an `io` mapping may use: logical IO names and paths into mutable module storage. */
82
+ export function ioTargets(core: VerifiedCoreModule): IoTarget[] {
83
+ const types = new Map(core.types.map((type) => [type.id, type]));
84
+ const targets: IoTarget[] = [];
85
+ const add = (key: string, type: PlcType) => {
86
+ const layout = ioLayout(iecSpelling(type, types) ?? "");
87
+ if (layout) targets.push({ key, layout });
88
+ };
89
+ const visit = (key: string, type: PlcType): void => {
90
+ add(key, type);
91
+ if (type.kind === "record")
92
+ for (const field of type.fields) visit(`${key}.${field.name}`, types.get(field.type)!);
93
+ // Typegen writes keys as template literal types, so any index matches here.
94
+ if (type.kind === "array") visit(`${key}[` + "${number}" + "]", types.get(type.element)!);
95
+ };
96
+ for (const variable of core.variables) {
97
+ if (variable.storage === "input" || variable.storage === "output")
98
+ add(variable.name, types.get(variable.type)!);
99
+ else if (variable.storage === "module")
100
+ visit(`${moduleName(variable)}.${variable.name}`, types.get(variable.type)!);
101
+ }
102
+ return targets;
103
+ }
@@ -0,0 +1,76 @@
1
+ // CODESYS declaration, statement, operator and elementary-type keywords.
2
+ // https://content.helpme-codesys.com/en/CODESYS%20Development%20System/_cds_keywords.html
3
+ const keywords = new Set(
4
+ `
5
+ ACTION END_ACTION PROGRAM END_PROGRAM FUNCTION END_FUNCTION FUNCTION_BLOCK END_FUNCTION_BLOCK
6
+ INTERFACE END_INTERFACE METHOD END_METHOD PROPERTY END_PROPERTY
7
+ TYPE END_TYPE STRUCT END_STRUCT UNION END_UNION ARRAY POINTER REFERENCE
8
+ VAR END_VAR VAR_INPUT VAR_OUTPUT VAR_IN_OUT VAR_TEMP VAR_GLOBAL VAR_EXTERNAL
9
+ VAR_ACCESS VAR_CONFIG VAR_STAT VAR_INST VAR_GENERIC
10
+ CONSTANT RETAIN NON_RETAIN PERSISTENT AT READ_ONLY READ_WRITE PARAMS
11
+ CONFIGURATION END_CONFIGURATION RESOURCE END_RESOURCE TASK WITH SINGLE INTERVAL PRIORITY
12
+ PRIVATE PUBLIC PROTECTED INTERNAL ABSTRACT FINAL OVERRIDE EXTENDS IMPLEMENTS THIS SUPER
13
+ IF THEN ELSIF ELSE END_IF CASE OF END_CASE FOR TO BY DO END_FOR
14
+ WHILE END_WHILE REPEAT UNTIL END_REPEAT RETURN EXIT CONTINUE JMP
15
+ TRUE FALSE BOOL BYTE WORD DWORD LWORD SINT USINT INT UINT DINT UDINT LINT ULINT
16
+ REAL LREAL TIME LTIME DATE LDATE DATE_AND_TIME LDATE_AND_TIME TIME_OF_DAY LTIME_OF_DAY
17
+ DT LDT TOD LTOD STRING WSTRING CHAR WCHAR
18
+ ANY ANY_BIT ANY_DATE ANY_DERIVED ANY_ELEMENTARY ANY_INT ANY_MAGNITUDE ANY_NUM
19
+ ANY_REAL ANY_STRING ANY_UNSIGNED
20
+ AND AND_THEN OR OR_ELSE XOR NOT MOD ADD SUB MUL DIV MOVE SEL MIN MAX LIMIT
21
+ ABS ACOS ASIN ATAN COS SIN TAN EXP EXPT LN LOG SQRT TRUNC SHL SHR ROL ROR
22
+ ADR SIZEOF CONCAT DELETE FIND INSERT LEFT LEN MID REPLACE RIGHT
23
+ `
24
+ .trim()
25
+ .split(/\s+/),
26
+ );
27
+ const types =
28
+ "(?:BOOL|BYTE|WORD|DWORD|LWORD|SINT|USINT|INT|UINT|DINT|UDINT|LINT|ULINT|REAL|LREAL|TIME|LTIME|DATE|LDATE|DATE_AND_TIME|LDATE_AND_TIME|TIME_OF_DAY|LTIME_OF_DAY|DT|LDT|TOD|LTOD|STRING|WSTRING|CHAR|WCHAR)";
29
+ const conversion = new RegExp(`^(?:${types}_TO_${types}|TO_${types}|TRUNC_${types})$`);
30
+ export function isIecIdentifier(name: string): boolean {
31
+ const upper = name.toUpperCase();
32
+ return (
33
+ /^[A-Za-z][A-Za-z0-9_]*$/.test(name) &&
34
+ !name.includes("__") &&
35
+ !keywords.has(upper) &&
36
+ !conversion.test(upper)
37
+ );
38
+ }
39
+
40
+ const identifierBody = (label: string): string =>
41
+ label
42
+ .replace(/[^A-Za-z0-9_]/g, "_")
43
+ .replace(/_+/g, "_")
44
+ .replace(/^_+|_+$/g, "") || "name";
45
+
46
+ /** Capitals inside a word are kept, so `PlantIO` stays as written while `plant-io` is `PlantIo`. */
47
+ export const pascalCase = (label: string): string =>
48
+ label
49
+ .split(/[^A-Za-z0-9]+/)
50
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
51
+ .join("");
52
+
53
+ export class Names {
54
+ private readonly used = new Set<string>();
55
+ allocateObject(prefix: "FB" | "F" | "PRG" | "ST" | "E" | "GVL", label: string): string {
56
+ const source = prefix === "F" ? label.replace(/^PRG_/, "") : label;
57
+ const kind = prefix === "F" && source.startsWith("FUN_") ? "FUN" : prefix;
58
+ const body = source.startsWith(`${kind}_`) ? source.slice(kind.length + 1) : source;
59
+ return this.allocate(`${kind}_${identifierBody(body)}`);
60
+ }
61
+
62
+ reserve(name: string): void {
63
+ this.used.add(name.toUpperCase());
64
+ // Qualified native types must keep their library prefix resolvable in the
65
+ // declaration scope, not merely reserve the complete dotted spelling.
66
+ const prefix = name.split(".")[0];
67
+ if (prefix) this.used.add(prefix.toUpperCase());
68
+ }
69
+ allocate(label: string, preserve = true): string {
70
+ const base = preserve && isIecIdentifier(label) ? label : `plc_${identifierBody(label)}`;
71
+ let name = base;
72
+ for (let suffix = 2; this.used.has(name.toUpperCase()); suffix++) name = `${base}_${suffix}`;
73
+ this.reserve(name);
74
+ return name;
75
+ }
76
+ }
@@ -0,0 +1,356 @@
1
+ import {
2
+ type Constant,
3
+ type CoreExpression,
4
+ type GeneratedObject,
5
+ integerRange,
6
+ floatMaximum,
7
+ integerType,
8
+ type PlcType,
9
+ } from "@tsplc/compiler/ir";
10
+
11
+ export function scalarLiteral(value: Constant, type: string): string {
12
+ if (value.kind === "bool") return value.value ? "TRUE" : "FALSE";
13
+ if (value.kind === "integer") return `${type}#${value.value}`;
14
+ if (value.kind === "float") {
15
+ // String(-0) loses the IEEE sign; keep it explicit in target float literals.
16
+ const text = Object.is(value.value, -0) ? "-0.0" : String(value.value).replace(/e/i, "E");
17
+ return `${type}#${text}`;
18
+ }
19
+ throw new Error("Expected verified scalar constant");
20
+ }
21
+
22
+ export class NumericLowering {
23
+ readonly objects: GeneratedObject[] = [];
24
+ private readonly helpers = new Map<string, string>();
25
+
26
+ constructor(
27
+ private readonly types: ReadonlyMap<string, PlcType>,
28
+ private readonly allocate: (label: string) => string,
29
+ ) {}
30
+
31
+ prepare(value: CoreExpression): void {
32
+ if (value.kind === "load") {
33
+ value.path?.forEach((segment) => {
34
+ if (segment.kind === "index") this.prepare(segment.value);
35
+ });
36
+ }
37
+ if (value.kind === "borrow") {
38
+ value.place.path.forEach((segment) => {
39
+ if (segment.kind === "index") this.prepare(segment.value);
40
+ });
41
+ }
42
+ if (value.kind === "binary") {
43
+ this.prepare(value.left);
44
+ this.prepare(value.right);
45
+ if (value.policy) this.arithmetic(value.type, value.operator, value.policy);
46
+ }
47
+ if (value.kind === "unary") this.prepare(value.operand);
48
+ if (value.kind === "convert") {
49
+ this.prepare(value.operand);
50
+ if (value.policy === "saturate") this.saturation(value.operand.type, value.type);
51
+ }
52
+ }
53
+
54
+ render(value: CoreExpression, operands: readonly string[]): string {
55
+ if (value.kind === "convert") {
56
+ return value.policy === "saturate"
57
+ ? `${this.saturation(value.operand.type, value.type)}(${operands[0]})`
58
+ : `TO_${value.type}(${operands[0]})`;
59
+ }
60
+ if (value.kind === "unary") {
61
+ // Avoid signed intermediate casts: TIME spans the complete UDINT range.
62
+ if (value.operator === "timeToMilliseconds") return `TIME_TO_UDINT(${operands[0]})`;
63
+ // Milliseconds since the runtime started; DINT wraps after 24.8 days, so subtract to compare.
64
+ if (value.operator === "clock") return "TO_DINT(TIME_TO_UDINT(TIME()))";
65
+ return value.operator === "not"
66
+ ? `(NOT ${operands[0]})`
67
+ : `TO_${value.type}(${value.operator === "negate" ? "-" : "NOT "}${operands[0]})`;
68
+ }
69
+ if (value.kind !== "binary") throw new Error("Expected numeric operation");
70
+ if (value.policy) {
71
+ return `${this.arithmetic(value.type, value.operator, value.policy)}(${operands.join(", ")})`;
72
+ }
73
+ if (value.operator === "shiftLeft" || value.operator === "shiftRight") {
74
+ const shift = value.operator === "shiftLeft" ? "SHL" : "SHR";
75
+ return `TO_${value.type}(${shift}(${operands.join(", ")}))`;
76
+ }
77
+ const tokens: Readonly<Record<string, string>> = {
78
+ add: "+",
79
+ subtract: "-",
80
+ multiply: "*",
81
+ divide: "/",
82
+ remainder: "MOD",
83
+ equal: "=",
84
+ notEqual: "<>",
85
+ less: "<",
86
+ lessEqual: "<=",
87
+ greater: ">",
88
+ greaterEqual: ">=",
89
+ bitAnd: "AND",
90
+ bitOr: "OR",
91
+ bitXor: "XOR",
92
+ };
93
+ const token = tokens[value.operator];
94
+ if (!token) throw new Error("Boolean short-circuit operation was not normalized");
95
+ const text =
96
+ value.operator === "remainder"
97
+ ? `(${operands[0]} - (${operands[0]} / ${operands[1]}) * ${operands[1]})`
98
+ : `(${operands[0]} ${token} ${operands[1]})`;
99
+ return value.type === "BOOL" ? text : `TO_${value.type}(${text})`;
100
+ }
101
+
102
+ private helper(
103
+ key: string,
104
+ type: string,
105
+ inputs: readonly [string, string][],
106
+ body: (name: string) => string,
107
+ locals = "",
108
+ ): string {
109
+ const existing = this.helpers.get(key);
110
+ if (existing) return existing;
111
+ const name = this.allocate(`numeric_${key}`);
112
+ this.helpers.set(key, name);
113
+ const parameters = inputs
114
+ .map(([input, inputType]) => ` ${input} : ${inputType};`)
115
+ .join("\n");
116
+ const variables = locals
117
+ ? `VAR
118
+ ${locals}
119
+ END_VAR
120
+ `
121
+ : "";
122
+ const declaration = `FUNCTION ${name} : ${type}
123
+ VAR_INPUT
124
+ ${parameters}
125
+ END_VAR
126
+ ${variables}`;
127
+ this.objects.push({
128
+ id: `numeric:${key}`,
129
+ kind: "function",
130
+ name,
131
+ declaration,
132
+ implementation: body(name),
133
+ });
134
+ return name;
135
+ }
136
+
137
+ private add64(): string {
138
+ // XOR forms the sum bits; each carry moves left until it exits the 64-bit word.
139
+ // This gives modulo 2^64 addition without relying on target integer overflow.
140
+ return this.helper(
141
+ "add64",
142
+ "LWORD",
143
+ [
144
+ ["a", "LWORD"],
145
+ ["b", "LWORD"],
146
+ ],
147
+ (name) => `x := a;
148
+ y := b;
149
+ FOR i := 1 TO 64 DO
150
+ carry := SHL(x AND y, 1);
151
+ x := x XOR y;
152
+ y := carry;
153
+ END_FOR;
154
+ ${name} := x;
155
+ `,
156
+ ` x, y, carry : LWORD;
157
+ i : UINT;`,
158
+ );
159
+ }
160
+
161
+ private wrap64(operator: string): string {
162
+ const add = this.add64();
163
+ if (operator === "add") return add;
164
+ if (operator === "subtract") {
165
+ // Two's complement negation and addition use the same modulo word operation.
166
+ return this.helper(
167
+ "subtract64",
168
+ "LWORD",
169
+ [
170
+ ["a", "LWORD"],
171
+ ["b", "LWORD"],
172
+ ],
173
+ (name) => `${name} := ${add}(a, ${add}(NOT b, LWORD#1));
174
+ `,
175
+ );
176
+ }
177
+ if (operator !== "multiply") throw new Error("Unsupported verified wrapping operation");
178
+ // Shift-and-add retains exactly the low 64 product bits.
179
+ return this.helper(
180
+ "multiply64",
181
+ "LWORD",
182
+ [
183
+ ["a", "LWORD"],
184
+ ["b", "LWORD"],
185
+ ],
186
+ (name) => `x := a;
187
+ y := b;
188
+ result := LWORD#0;
189
+ FOR i := 1 TO 64 DO
190
+ IF (y AND LWORD#1) <> LWORD#0 THEN
191
+ result := ${add}(result, x);
192
+ END_IF;
193
+ x := SHL(x, 1);
194
+ y := SHR(y, 1);
195
+ END_FOR;
196
+ ${name} := result;
197
+ `,
198
+ ` x, y, result : LWORD;
199
+ i : UINT;`,
200
+ );
201
+ }
202
+
203
+ private arithmetic(typeId: string, operator: string, policy: "wrap" | "saturate"): string {
204
+ const type = this.types.get(typeId);
205
+ if (type?.kind !== "integer") throw new Error("Expected verified integer arithmetic");
206
+ const token = operator === "add" ? "+" : operator === "subtract" ? "-" : "*";
207
+ const key = `${policy}_${operator}_${typeId}`;
208
+ if (policy === "wrap") {
209
+ if (type.bits === 64) {
210
+ const operation = this.wrap64(operator);
211
+ return this.helper(
212
+ key,
213
+ typeId,
214
+ [
215
+ ["a", typeId],
216
+ ["b", typeId],
217
+ ],
218
+ (
219
+ name,
220
+ ) => `${name} := LWORD_TO_${typeId}(${operation}(${typeId}_TO_LWORD(a), ${typeId}_TO_LWORD(b)));
221
+ `,
222
+ );
223
+ }
224
+ const wide = !type.signed && operator !== "subtract" ? "ULINT" : "LINT";
225
+ return this.helper(
226
+ key,
227
+ typeId,
228
+ [
229
+ ["a", typeId],
230
+ ["b", typeId],
231
+ ],
232
+ (
233
+ name,
234
+ ) => `${name} := ${wide}_TO_${typeId}(${typeId}_TO_${wide}(a) ${token} ${typeId}_TO_${wide}(b));
235
+ `,
236
+ );
237
+ }
238
+
239
+ const [minimum, maximum] = integerRange(type);
240
+ const min = `${typeId}#${minimum}`,
241
+ max = `${typeId}#${maximum}`,
242
+ zero = `${typeId}#0`;
243
+ return this.helper(
244
+ key,
245
+ typeId,
246
+ [
247
+ ["a", typeId],
248
+ ["b", typeId],
249
+ ],
250
+ (name) => {
251
+ const clamp = (
252
+ condition: string,
253
+ endpoint: string,
254
+ depth = "",
255
+ ) => `${depth}IF ${condition} THEN
256
+ ${depth} ${name} := ${endpoint};
257
+ ${depth} RETURN;
258
+ ${depth}END_IF;
259
+ `;
260
+ let body: string;
261
+ if (!type.signed) {
262
+ // Unsigned subtraction has only underflow; the other operations have only overflow.
263
+ if (operator === "add") body = clamp(`a > ${max} - b`, max);
264
+ else if (operator === "subtract") body = clamp("a < b", zero);
265
+ else
266
+ body = `IF b <> ${zero} THEN
267
+ ${clamp(`a > ${max} / b`, max, " ")}END_IF;
268
+ `;
269
+ } else if (operator === "add") {
270
+ body = `IF b > ${zero} THEN
271
+ ${clamp(`a > ${max} - b`, max, " ")}ELSIF b < ${zero} THEN
272
+ ${clamp(`a < ${min} - b`, min, " ")}END_IF;
273
+ `;
274
+ } else if (operator === "subtract") {
275
+ body = `IF b < ${zero} THEN
276
+ ${clamp(`a > ${max} + b`, max, " ")}ELSIF b > ${zero} THEN
277
+ ${clamp(`a < ${min} + b`, min, " ")}END_IF;
278
+ `;
279
+ } else {
280
+ // Handle zero first, then divide the endpoint before multiplying. Each sign
281
+ // quadrant tests representability without forming an overflowing product.
282
+ body =
283
+ clamp(`a = ${zero}`, zero) +
284
+ clamp(`b = ${zero}`, zero) +
285
+ `IF a > ${zero} THEN
286
+ IF b > ${zero} THEN
287
+ ${clamp(`a > ${max} / b`, max, " ")} ELSE
288
+ ${clamp(`b < ${min} / a`, min, " ")} END_IF;
289
+ ELSE
290
+ IF b > ${zero} THEN
291
+ ${clamp(`a < ${min} / b`, min, " ")} ELSE
292
+ ${clamp(`a < ${max} / b`, max, " ")} END_IF;
293
+ END_IF;
294
+ `;
295
+ }
296
+ return `${body}${name} := a ${token} b;
297
+ `;
298
+ },
299
+ );
300
+ }
301
+
302
+ private saturation(sourceId: string, targetId: string): string {
303
+ const source = this.types.get(sourceId),
304
+ target = this.types.get(targetId);
305
+ if (source?.kind === "float" && target?.kind === "float") {
306
+ // Widening cannot exceed the destination's finite range. Identity
307
+ // conversions are removed by normalization; neither needs a helper.
308
+ if (source.bits <= target.bits) return `TO_${targetId}`;
309
+ const maximum = floatMaximum(target);
310
+ const literal = (value: number, type: string): string =>
311
+ scalarLiteral({ kind: "float", value }, type);
312
+ return this.helper(
313
+ `saturate_${sourceId}_${targetId}`,
314
+ targetId,
315
+ [["value", sourceId]],
316
+ (name) => `IF value < ${literal(-maximum, sourceId)} THEN
317
+ ${name} := ${literal(-maximum, targetId)};
318
+ RETURN;
319
+ END_IF;
320
+ IF value > ${literal(maximum, sourceId)} THEN
321
+ ${name} := ${literal(maximum, targetId)};
322
+ RETURN;
323
+ END_IF;
324
+ ${name} := TO_${targetId}(value);
325
+ `,
326
+ );
327
+ }
328
+ if (!source || !target || !integerType(source) || !integerType(target)) {
329
+ throw new Error("Expected verified integer saturation");
330
+ }
331
+ const [fromMin, fromMax] = integerRange(source),
332
+ [toMin, toMax] = integerRange(target);
333
+ return this.helper(
334
+ `saturate_${sourceId}_${targetId}`,
335
+ targetId,
336
+ [["value", sourceId]],
337
+ (name) => {
338
+ let body = "";
339
+ if (fromMin < toMin)
340
+ body += `IF value < ${sourceId}#${toMin} THEN
341
+ ${name} := ${targetId}#${toMin};
342
+ RETURN;
343
+ END_IF;
344
+ `;
345
+ if (fromMax > toMax)
346
+ body += `IF value > ${sourceId}#${toMax} THEN
347
+ ${name} := ${targetId}#${toMax};
348
+ RETURN;
349
+ END_IF;
350
+ `;
351
+ return `${body}${name} := TO_${targetId}(value);
352
+ `;
353
+ },
354
+ );
355
+ }
356
+ }