@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.
Files changed (80) 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 +1396 -0
  18. package/dist/cli.js.map +16 -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 +429 -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 +106 -4
  48. package/project/artifacts.ts +277 -0
  49. package/project/cli.ts +305 -0
  50. package/project/index.ts +1 -0
  51. package/project/plant.ts +437 -0
  52. package/project/project.ts +312 -0
  53. package/project/server.ts +83 -0
  54. package/src/frontend/async.ts +1060 -0
  55. package/src/frontend/blocks.ts +419 -0
  56. package/src/frontend/configuration.ts +167 -0
  57. package/src/frontend/const-functions.ts +153 -0
  58. package/src/frontend/cooperative.ts +75 -0
  59. package/src/frontend/elaborate/capture.ts +138 -0
  60. package/src/frontend/elaborate/graph.ts +94 -0
  61. package/src/frontend/elaborate/index.ts +302 -0
  62. package/src/frontend/elaborate/layouts.ts +390 -0
  63. package/src/frontend/elaborate/model.ts +177 -0
  64. package/src/frontend/elaborate/specialize.ts +2280 -0
  65. package/src/frontend/exposure.ts +147 -0
  66. package/src/frontend/module-graph.ts +63 -0
  67. package/src/frontend/promises.ts +751 -0
  68. package/src/frontend/services.ts +435 -0
  69. package/src/frontend/types.ts +578 -0
  70. package/src/frontend/typescript.ts +2442 -0
  71. package/src/index.ts +7 -0
  72. package/stdlib/clock.ts +2 -0
  73. package/stdlib/math.ts +19 -0
  74. package/types/async.d.ts +81 -0
  75. package/types/clock.d.ts +4 -0
  76. package/types/config.d.ts +2 -0
  77. package/types/data-types.d.ts +31 -0
  78. package/types/globals.d.ts +40 -0
  79. package/types/index.d.ts +111 -0
  80. package/types/plc.d.ts +5 -0
@@ -0,0 +1,1063 @@
1
+ import type {
2
+ Constant,
3
+ CoreExpression,
4
+ CoreFunction,
5
+ CoreFunctionBlock,
6
+ CorePath,
7
+ CoreStatement,
8
+ EmitResult,
9
+ GeneratedObject,
10
+ SourceSpan,
11
+ Variable,
12
+ } from "@tsplc/compiler/ir";
13
+ import { defaultValue, instancedBlock, standardBlocks } from "@tsplc/compiler/ir";
14
+ import { Names, pascalCase } from "./names.ts";
15
+ import { NumericLowering } from "./numeric.ts";
16
+ import { type CodesysPlan, diagnostic, isCodesysPlan } from "./plan.ts";
17
+ import { storageLayout } from "./storage.ts";
18
+ import { accessedStorage } from "./task-access.ts";
19
+ import { emitHttp, httpMethods, httpCertificateVerifier, opcuaServiceCapacity } from "./http.ts";
20
+ import { CodesysTypes } from "./types.ts";
21
+ import type { Publication } from "@tsplc/config";
22
+
23
+ export interface SourceMapping {
24
+ path: string;
25
+ line: number;
26
+ span: SourceSpan;
27
+ }
28
+
29
+ // All symbol relationships were checked by normalization and planning. Failure
30
+ // here is an emitter invariant violation, rather than a recoverable source error.
31
+ function required<T>(value: T | undefined): T {
32
+ if (value === undefined) throw new Error("Missing verified CODESYS symbol.");
33
+ return value;
34
+ }
35
+
36
+ type CodesysBundle = EmitResult & {
37
+ /** Variables a generated device tree maps to their bit or byte channels. */
38
+ ioMappings: readonly { key: string; address: string; variable: string }[];
39
+ mappings: readonly SourceMapping[];
40
+ ioSymbols: Readonly<Record<string, string>>;
41
+ publication: Publication;
42
+ };
43
+
44
+ export function emitCodesys(plan: CodesysPlan): CodesysBundle {
45
+ if (!isCodesysPlan(plan)) {
46
+ return {
47
+ artifacts: [],
48
+ objects: [],
49
+ mappings: [],
50
+ ioSymbols: {},
51
+ publication: { production: [], development: [] },
52
+ ioMappings: [],
53
+ diagnostics: [diagnostic("Emission requires a validated CODESYS plan.")],
54
+ };
55
+ }
56
+ return new CodesysEmitter(plan).emit();
57
+ }
58
+
59
+ // A bundle owns target names and source mappings. Per-routine declarations and
60
+ // implementation lines stay local to emitRoutine, so they cannot leak between POUs.
61
+ class CodesysEmitter {
62
+ private readonly names = new Names();
63
+ private networkName: string | undefined;
64
+ private readonly rpcChannels: {
65
+ path: string;
66
+ request: string;
67
+ requestId: string;
68
+ reply: string;
69
+ replyId: string;
70
+ epoch: string;
71
+ }[] = [];
72
+ private readonly references = new Map<string, string>();
73
+ /** Each `io` binding: an AT wire copied to or from its logical target around the scan. */
74
+ private readonly channels: {
75
+ key: string;
76
+ root: string;
77
+ input: boolean;
78
+ target: string;
79
+ wire: string;
80
+ inverted?: boolean;
81
+ }[] = [];
82
+ private readonly ioMappings: { key: string; address: string; variable: string }[] = [];
83
+ private readonly objects: GeneratedObject[] = [];
84
+ private readonly mappings: SourceMapping[] = [];
85
+ private readonly mailboxLocks = new Map<string, string>();
86
+ private readonly blockFields = new Map<string, Map<string, string>>();
87
+ private readonly typeMap;
88
+ private readonly blocks;
89
+ private readonly blockNames;
90
+ private readonly functionNames = new Map<string, string>();
91
+ private readonly programRoutines = new Set<string>();
92
+ private readonly programResults = new Map<string, string>();
93
+ private readonly routineScopes = new Map<
94
+ string,
95
+ { names: Names; references: Map<string, string> }
96
+ >();
97
+ private readonly targetTypes: CodesysTypes;
98
+ private readonly numeric: NumericLowering;
99
+ private readonly layout;
100
+
101
+ constructor(private readonly plan: CodesysPlan) {
102
+ const { core, options } = plan;
103
+ const persistent = new Set(
104
+ core.variables.filter((item) => item.storage !== "constant").map((item) => item.id),
105
+ );
106
+ for (const fn of core.functions) {
107
+ if (!fn.owner && [...accessedStorage(fn, core)].some((id) => persistent.has(id)))
108
+ this.programRoutines.add(fn.id);
109
+ }
110
+ for (const name of Object.values(options.programNames ?? {})) this.names.reserve(name);
111
+
112
+ this.typeMap = new Map(core.types.map((type) => [type.id, type]));
113
+ this.blocks = new Map(core.blocks.map((block) => [block.id, block]));
114
+ this.blockNames = new Map(
115
+ core.blocks.map((block) => [
116
+ block.id,
117
+ block.standard
118
+ ? standardBlocks[block.standard].native
119
+ : this.names.allocateObject("FB", block.name),
120
+ ]),
121
+ );
122
+ for (const name of this.blockNames.values()) this.names.reserve(name);
123
+ this.targetTypes = new CodesysTypes(this.typeMap, this.names, this.blockNames);
124
+ this.numeric = new NumericLowering(this.typeMap, (label) => this.names.allocate(label));
125
+ this.layout = storageLayout(core);
126
+ this.objects.push(...this.targetTypes.objects);
127
+ }
128
+
129
+ emit(): CodesysBundle {
130
+ this.emitGlobals();
131
+ this.emitIo();
132
+ if (Object.values(this.plan.http).some((endpoint) => endpoint.transport !== "opcua")) {
133
+ this.networkName = this.names.allocateObject("GVL", "TsplcNetwork");
134
+ this.objects.push({
135
+ id: "runtime:httpNetwork",
136
+ kind: "globalList",
137
+ name: this.networkName,
138
+ // Blocks in other global lists take this in their initializers, so it must initialize first.
139
+ declaration: `{attribute 'qualified_only'}
140
+ {attribute 'global_init_slot' := '40000'}
141
+ VAR_GLOBAL
142
+ network:NBS.AsyncProperty := (usiTaskPrio:=15,udiTaskInterval:=1000,tnTaskName:='tsPLC HTTP');
143
+ END_VAR
144
+ `,
145
+ });
146
+ }
147
+
148
+ // Helpers must be discovered before allocating local names: an ST local must
149
+ // never shadow a helper, function, FB, or DUT that the implementation calls.
150
+ const { core } = this.plan;
151
+ const entries = new Set(core.programs.map((program) => program.entry));
152
+ for (const fn of core.functions.filter((fn) => !fn.owner)) {
153
+ const program = this.programRoutines.has(fn.id);
154
+ const label = program && entries.has(fn.id) ? `${fn.name}_body` : fn.name;
155
+ this.functionNames.set(fn.id, this.names.allocateObject(program ? "PRG" : "F", label));
156
+ }
157
+ for (const fn of core.functions) this.prepareStatements(fn.body);
158
+ for (const block of core.blocks) this.prepareStatements(block.body);
159
+ this.objects.push(...this.numeric.objects);
160
+ this.allocateBlockFields();
161
+
162
+ const routines = [...core.functions, ...core.blocks.filter((block) => !block.standard)];
163
+ for (const fn of routines) this.allocateRoutineScope(fn);
164
+ for (const fn of routines) this.emitRoutine(fn);
165
+ for (const program of core.programs) this.emitProgram(program);
166
+
167
+ const { objects, mappings } = this;
168
+ const paths = new Map<string, string>();
169
+ for (const object of objects) {
170
+ const path = this.objectPath(object);
171
+ paths.set(object.id, path);
172
+ object.sourcePath =
173
+ object.implementation === undefined ? `${path}.st` : `${path}/declaration.st`;
174
+ }
175
+ const artifacts = objects.flatMap((object) => {
176
+ const path = paths.get(object.id)!;
177
+ return object.implementation === undefined
178
+ ? [{ path: `${path}.st`, content: object.declaration }]
179
+ : [
180
+ { path: `${path}/declaration.st`, content: object.declaration },
181
+ { path: `${path}/implementation.st`, content: object.implementation },
182
+ ];
183
+ });
184
+ for (const mapping of mappings) {
185
+ const object = objects.find((object) => {
186
+ const key = object.kind === "method" ? object.id : object.name;
187
+ return mapping.path === `${key}.st` || mapping.path.startsWith(`${key}/`);
188
+ });
189
+ if (object) {
190
+ const key = object.kind === "method" ? object.id : object.name;
191
+ mapping.path = paths.get(object.id)! + mapping.path.slice(key.length);
192
+ }
193
+ }
194
+ const ioSymbols = Object.fromEntries([
195
+ ...this.channels.map((channel) => [channel.key, channel.target]),
196
+ ...this.ioMappings.map((mapping) => [mapping.key, mapping.variable]),
197
+ ]);
198
+ const { ioMappings } = this;
199
+ const logicalIo = this.plan.core.variables
200
+ .filter((variable) => variable.storage === "input" || variable.storage === "output")
201
+ .map((variable) => variable.id);
202
+ const rpcSymbols = [
203
+ ...new Set(
204
+ this.rpcChannels.flatMap((channel) =>
205
+ [channel.request, channel.requestId, channel.reply, channel.replyId, channel.epoch].map(
206
+ (path) => path.replace(/\[\d+\]$/, ""),
207
+ ),
208
+ ),
209
+ ),
210
+ ];
211
+ const published = (ids: readonly string[]) =>
212
+ [...ids, ...logicalIo].map((id) => required(this.references.get(id))).concat(rpcSymbols);
213
+ const publication = {
214
+ production: published(this.plan.publication.production),
215
+ development: published(this.plan.publication.development),
216
+ };
217
+ const exposures = Object.fromEntries(
218
+ core.variables
219
+ .filter((variable) => variable.exposure)
220
+ .map((variable) => {
221
+ const record = this.typeMap.get(variable.type);
222
+ if (record?.kind !== "record") throw new Error("Invalid exposure record.");
223
+ const root = required(this.references.get(variable.id));
224
+ const fields = Object.fromEntries(
225
+ record.fields
226
+ .filter((field) => field.name !== "contractVersion")
227
+ .map((field) => {
228
+ const type = this.typeMap.get(field.type);
229
+ const kind =
230
+ type?.kind === "bool" ? "boolean" : type?.kind === "string" ? "string" : "number";
231
+ return [
232
+ field.name,
233
+ { path: `${root}.${this.targetTypes.field(record.id, field.name)}`, kind },
234
+ ];
235
+ }),
236
+ );
237
+ return [
238
+ variable.exposure!.name,
239
+ {
240
+ version: variable.exposure!.version,
241
+ versionPath: `${root}.${this.targetTypes.field(record.id, "contractVersion")}`,
242
+ telemetry: fields,
243
+ },
244
+ ];
245
+ }),
246
+ );
247
+ artifacts.push(
248
+ { path: "exposures.json", content: JSON.stringify(exposures, null, 2) + "\n" },
249
+ {
250
+ path: "machines.ts",
251
+ content: `// Generated explicit read-only machine interfaces. Do not edit.\nexport const machines=${JSON.stringify(exposures, null, 2)} as const;\nexport const serviceChannels=${JSON.stringify(this.rpcChannels, null, 2)} as const;\n`,
252
+ },
253
+ );
254
+ return { artifacts, objects, mappings, ioSymbols, ioMappings, publication, diagnostics: [] };
255
+ }
256
+
257
+ private objectPath(object: GeneratedObject): string {
258
+ const { core } = this.plan;
259
+ if (object.kind === "method" && object.parent) {
260
+ const parent = required(this.objects.find((candidate) => candidate.id === object.parent));
261
+ return `${this.objectPath(parent)}/methods/${object.name}`;
262
+ }
263
+ if (object.kind === "method") {
264
+ const method = required(core.functions.find((fn) => fn.id === object.id));
265
+ const owner = required(this.objects.find((item) => item.id === method.owner?.block));
266
+ return `${this.objectPath(owner)}/methods/${object.name}`;
267
+ }
268
+ let source = object.id.split(":")[0] ?? "";
269
+ const program = core.programs.find((item) => item.id === object.id);
270
+ if (program) source = program.entry.split(":")[0] ?? "";
271
+ const group = this.layout.globalLists.find((item) => item.id === object.id);
272
+ if (group) {
273
+ // Variable IDs are relative to tsconfig, unlike their diagnostic spans.
274
+ source = group.variables[0]?.id.split(":")[0] ?? "";
275
+ }
276
+ if (
277
+ /^[\w./ -]+\.tsx?$/.test(source) &&
278
+ !source.split(/[\\/]/).includes("..") &&
279
+ !source.startsWith("/")
280
+ ) {
281
+ const segments = source.replaceAll("\\", "/").split("/").slice(0, -1);
282
+ if (segments[0] === "src") segments.shift();
283
+ // A DUT directory can also contain sibling input/output layouts. Keep
284
+ // that directory for every DUT, or its primary type would block the folder.
285
+ if (object.kind !== "dataType" && segments.at(-1) === object.name) segments.pop();
286
+ return [...segments, object.name].join("/");
287
+ }
288
+ return `${object.kind === "dataType" ? "Types" : object.id === "io" ? "IO" : "_Generated"}/${object.name}`;
289
+ }
290
+
291
+ private emitGlobals(): void {
292
+ const { options } = this.plan;
293
+ const { layout, names, references, mappings, targetTypes, typeMap, objects } = this;
294
+ const hasExports = (group: (typeof layout.globalLists)[number]) =>
295
+ group.variables.some((variable) => variable.exported && !variable.exposure);
296
+ for (const group of layout.globalLists) {
297
+ const splitByStorage = layout.globalLists.some(
298
+ (other) =>
299
+ other.module === group.module && other.storage !== group.storage && hasExports(other),
300
+ );
301
+ // Exported lists are the program's public interface; private storage stays generated.
302
+ const name = hasExports(group)
303
+ ? names.allocate(
304
+ pascalCase(splitByStorage ? `${group.module}-${group.storage}` : group.module),
305
+ )
306
+ : names.allocateObject("GVL", `${group.module}_${group.storage}`);
307
+ const published = new Set(this.plan.publication[options.buildTarget]);
308
+ const publish = group.variables.some((variable) => published.has(variable.id));
309
+ const fields = new Names();
310
+ for (const name of this.blockNames.values()) fields.reserve(name);
311
+ const lines = [
312
+ ...(publish ? ["{attribute 'linkalways'}"] : []),
313
+ "{attribute 'qualified_only'}",
314
+ group.storage === "constants"
315
+ ? "VAR_GLOBAL CONSTANT"
316
+ : group.storage === "retained"
317
+ ? "VAR_GLOBAL RETAIN"
318
+ : "VAR_GLOBAL",
319
+ ];
320
+ for (const item of group.variables) {
321
+ const field = fields.allocate(item.name);
322
+ references.set(item.id, `${name}.${field}`);
323
+ if (published.has(item.id))
324
+ lines.push(` {attribute 'symbol' := '${item.exposure ? "read" : "readwrite"}'}`);
325
+ mappings.push({ path: `${name}.st`, line: lines.length + 1, span: item.span });
326
+ let initial = "";
327
+ if (!instancedBlock(item.type, typeMap)) {
328
+ const value = item.initial ?? defaultValue(item.type, typeMap);
329
+ // CONSTANT declarations always keep their initializer. Mutable zero
330
+ // aggregates use the same implicit initialization as the original ST.
331
+ initial =
332
+ group.storage === "constants"
333
+ ? ` := ${targetTypes.literal(value, item.type)}`
334
+ : targetTypes.declarationInitializer(value, item.type);
335
+ }
336
+ lines.push(` ${field} : ${targetTypes.name(item.type)}${initial};`);
337
+ }
338
+ lines.push("END_VAR");
339
+ objects.push({
340
+ id: group.id,
341
+ kind: "globalList",
342
+ name,
343
+ declaration: `${lines.join("\n")}\n`,
344
+ });
345
+ }
346
+ }
347
+
348
+ private emitIo(): void {
349
+ const { core, options } = this.plan;
350
+ const { names, references, channels, ioMappings, mappings, targetTypes, typeMap, objects } =
351
+ this;
352
+ const ioName = names.allocate("GVL_IO");
353
+ const ioFields = new Names();
354
+ for (const name of this.blockNames.values()) ioFields.reserve(name);
355
+ const ioLines = ["{attribute 'linkalways'}", "{attribute 'qualified_only'}", "VAR_GLOBAL"];
356
+ for (const item of core.variables.filter(
357
+ (item) => item.storage === "input" || item.storage === "output",
358
+ )) {
359
+ const field = ioFields.allocate(item.name);
360
+ const initial = item.initial ?? defaultValue(item.type, typeMap);
361
+ const binding = required(options.io[item.name]);
362
+ references.set(item.id, `${ioName}.${field}`);
363
+ if (binding.mapped) {
364
+ ioMappings.push({
365
+ key: item.name,
366
+ address: binding.address,
367
+ variable: `${ioName}.${field}`,
368
+ });
369
+ mappings.push({ path: `${ioName}.st`, line: ioLines.length + 2, span: item.span });
370
+ ioLines.push(
371
+ " {attribute 'symbol' := 'read'}",
372
+ ` ${field} : ${targetTypes.name(item.type)} := ${targetTypes.literal(initial, item.type)};`,
373
+ );
374
+ continue;
375
+ }
376
+ const wire = ioFields.allocate(`${item.name}_physical`);
377
+ channels.push({
378
+ key: item.name,
379
+ root: item.id,
380
+ input: item.storage === "input",
381
+ target: `${ioName}.${field}`,
382
+ wire: `${ioName}.${wire}`,
383
+ inverted: binding.inverted,
384
+ });
385
+ mappings.push(
386
+ { path: `${ioName}.st`, line: ioLines.length + 1, span: item.span },
387
+ { path: `${ioName}.st`, line: ioLines.length + 3, span: item.span },
388
+ );
389
+ ioLines.push(
390
+ ` ${wire} AT ${binding.address} : ${targetTypes.name(item.type)};`,
391
+ // Publish the scan's logical value for monitoring. Physical wires stay
392
+ // private; external writes cannot alter the controller's IO snapshot.
393
+ " {attribute 'symbol' := 'read'}",
394
+ ` ${field} : ${targetTypes.name(item.type)} := ${targetTypes.literal(initial, item.type)};`,
395
+ );
396
+ }
397
+ for (const { key, root, segments, type } of this.plan.storageBindings) {
398
+ const { address, mapped, inverted } = required(options.io[key]);
399
+ const target = segments.reduce(
400
+ (path, segment) =>
401
+ segment.kind === "field"
402
+ ? `${path}.${targetTypes.field(segment.record, segment.name)}`
403
+ : `${path}[${segment.value}]`,
404
+ required(references.get(root.id)),
405
+ );
406
+ if (mapped) {
407
+ ioMappings.push({ key, address, variable: target });
408
+ continue;
409
+ }
410
+ const wire = ioFields.allocate(`${key.replace(/\W+/g, "_").replace(/_+$/, "")}_physical`);
411
+ // The wire takes the target's own type, so equal-width EDS types need no conversion.
412
+ ioLines.push(` ${wire} AT ${address} : ${targetTypes.name(type.id)};`);
413
+ channels.push({
414
+ key,
415
+ root: root.id,
416
+ input: address[1] === "I",
417
+ target,
418
+ wire: `${ioName}.${wire}`,
419
+ inverted,
420
+ });
421
+ }
422
+ ioLines.push("END_VAR");
423
+ // Mapped storage bindings need no IO list; mapped logical IO is declared here.
424
+ if (ioLines.length > 4)
425
+ objects.push({
426
+ id: "io",
427
+ kind: "globalList",
428
+ name: ioName,
429
+ declaration: `${ioLines.join("\n")}\n`,
430
+ });
431
+ }
432
+
433
+ private prepareStatements(items: readonly CoreStatement[]): void {
434
+ const { numeric } = this;
435
+ for (const item of items) {
436
+ if (item.kind === "assign") {
437
+ numeric.prepare(item.value);
438
+ item.path?.forEach((segment) => {
439
+ if (segment.kind === "index") numeric.prepare(segment.value);
440
+ });
441
+ }
442
+ if (item.kind === "if") {
443
+ numeric.prepare(item.condition);
444
+ this.prepareStatements(item.then);
445
+ this.prepareStatements(item.else);
446
+ }
447
+ if (item.kind === "switch") {
448
+ numeric.prepare(item.value);
449
+ for (const branch of item.cases) this.prepareStatements(branch.body);
450
+ this.prepareStatements(item.fallback);
451
+ }
452
+ if (item.kind === "for") {
453
+ if (item.dynamic) {
454
+ numeric.prepare(item.dynamic.start);
455
+ numeric.prepare(item.dynamic.limit);
456
+ }
457
+ this.prepareStatements(item.body);
458
+ }
459
+ if (item.kind === "return" && item.value) numeric.prepare(item.value);
460
+ if (item.kind === "invoke") {
461
+ if (item.index) numeric.prepare(item.index);
462
+ for (const value of item.arguments) numeric.prepare(value);
463
+ }
464
+ if (item.kind === "blockUpdate") {
465
+ if (item.index) numeric.prepare(item.index);
466
+ for (const value of Object.values(item.inputs)) numeric.prepare(value);
467
+ }
468
+ }
469
+ }
470
+
471
+ private allocateBlockFields(): void {
472
+ const { core } = this.plan;
473
+ const { objects, functionNames, blockNames, blockFields } = this;
474
+ for (const block of core.blocks) {
475
+ const fields = new Names(),
476
+ mapped = new Map<string, string>();
477
+ for (const object of objects) fields.reserve(object.name);
478
+ for (const objectName of [
479
+ ...core.functions.filter((fn) => !fn.owner).map((fn) => required(functionNames.get(fn.id))),
480
+ ...blockNames.values(),
481
+ ])
482
+ fields.reserve(objectName);
483
+ for (const method of core.functions.filter((fn) => fn.owner?.block === block.id))
484
+ functionNames.set(method.id, fields.allocate(method.name));
485
+ const native = block.standard && standardBlocks[block.standard];
486
+ for (const item of [...block.inputs, ...block.outputs, ...block.state])
487
+ mapped.set(
488
+ item.id,
489
+ native
490
+ ? required(
491
+ [...native.inputs, ...native.outputs].find(([name]) => name === item.name),
492
+ )[1]
493
+ : fields.allocate(item.name, true),
494
+ );
495
+ if (block.runtime?.kind === "mailbox")
496
+ this.mailboxLocks.set(block.id, fields.allocate("plc_mailbox_lock"));
497
+ blockFields.set(block.id, mapped);
498
+ }
499
+ }
500
+
501
+ private allocateRoutineScope(fn: CoreFunction | CoreFunctionBlock): void {
502
+ const block = "inputs" in fn ? fn : undefined;
503
+ const owner =
504
+ block ?? ("owner" in fn && fn.owner ? this.blocks.get(fn.owner.block) : undefined);
505
+ const names = new Names(),
506
+ references = new Map<string, string>();
507
+ for (const object of this.objects) names.reserve(object.name);
508
+ for (const name of [...this.functionNames.values(), ...this.blockNames.values()])
509
+ names.reserve(name);
510
+ if (this.programRoutines.has(fn.id) && "parameters" in fn && fn.result)
511
+ this.programResults.set(fn.id, names.allocate("plc_result"));
512
+ if (owner) {
513
+ for (const item of [...owner.outputs, ...owner.state]) {
514
+ const name = required(required(this.blockFields.get(owner.id)).get(item.id));
515
+ names.reserve(name);
516
+ references.set(item.id, name);
517
+ }
518
+ }
519
+ for (const item of "parameters" in fn ? fn.parameters : fn.inputs) {
520
+ const name = block
521
+ ? required(required(this.blockFields.get(block.id)).get(item.id))
522
+ : names.allocate(item.name);
523
+ names.reserve(name);
524
+ references.set(item.id, name);
525
+ }
526
+ for (const item of fn.locals) references.set(item.id, names.allocate(item.name));
527
+ this.routineScopes.set(fn.id, { names, references });
528
+ }
529
+
530
+ private routineHeader(
531
+ fn: CoreFunction | CoreFunctionBlock,
532
+ name: string,
533
+ ): { kind: GeneratedObject["kind"]; declaration: string } {
534
+ if ("inputs" in fn) return { kind: "functionBlock", declaration: `FUNCTION_BLOCK ${name}` };
535
+ if (fn.owner)
536
+ return {
537
+ kind: "method",
538
+ declaration: `METHOD ${fn.owner.access.toUpperCase()} ${name}${fn.result ? ` : ${this.targetTypes.name(fn.result)}` : ""}`,
539
+ };
540
+ if (this.programRoutines.has(fn.id)) return { kind: "program", declaration: `PROGRAM ${name}` };
541
+ return {
542
+ kind: "function",
543
+ declaration: `FUNCTION ${name} : ${fn.result ? this.targetTypes.name(fn.result) : "BOOL"}`,
544
+ };
545
+ }
546
+
547
+ private emitRoutine(fn: CoreFunction | CoreFunctionBlock): void {
548
+ const { core } = this.plan;
549
+ if ("inputs" in fn && (fn.runtime?.kind === "http" || fn.runtime?.kind === "rpc")) {
550
+ const endpoint = required(this.plan.http[fn.id]);
551
+ const rpcGlobal =
552
+ endpoint.transport === "opcua"
553
+ ? this.names.allocateObject("GVL", "TsplcRpc_" + required(this.blockNames.get(fn.id)))
554
+ : undefined;
555
+ if (rpcGlobal) {
556
+ this.objects.push({
557
+ id: fn.id + ":rpc",
558
+ kind: "globalList",
559
+ name: rpcGlobal,
560
+ declaration: `{attribute 'qualified_only'}
561
+ VAR_GLOBAL
562
+ {attribute 'symbol' := 'read'} requests:ARRAY[1..${opcuaServiceCapacity}] OF STRING(2048);
563
+ {attribute 'symbol' := 'read'} requestIds:ARRAY[1..${opcuaServiceCapacity}] OF STRING(64);
564
+ {attribute 'symbol' := 'readwrite'} replyIds:ARRAY[1..${opcuaServiceCapacity}] OF STRING(64);
565
+ {attribute 'symbol' := 'readwrite'} replies:ARRAY[1..${opcuaServiceCapacity}] OF STRING(2048);
566
+ {attribute 'symbol' := 'readwrite'} epochs:ARRAY[1..${opcuaServiceCapacity}] OF STRING(32);
567
+ inUse:ARRAY[1..${opcuaServiceCapacity}] OF __XWORD;
568
+ sequence:ARRAY[1..${opcuaServiceCapacity}] OF UDINT;
569
+ END_VAR`,
570
+ });
571
+ for (let slot = 1; slot <= opcuaServiceCapacity; slot++)
572
+ this.rpcChannels.push({
573
+ path: endpoint.path,
574
+ request: `${rpcGlobal}.requests[${slot}]`,
575
+ reply: `${rpcGlobal}.replies[${slot}]`,
576
+ requestId: `${rpcGlobal}.requestIds[${slot}]`,
577
+ replyId: `${rpcGlobal}.replyIds[${slot}]`,
578
+ epoch: `${rpcGlobal}.epochs[${slot}]`,
579
+ });
580
+ }
581
+ const generated = emitHttp(
582
+ fn,
583
+ required(this.blockNames.get(fn.id)),
584
+ required(this.blockFields.get(fn.id)),
585
+ this.typeMap,
586
+ this.targetTypes,
587
+ endpoint,
588
+ this.networkName ? `${this.networkName}.network` : "",
589
+ rpcGlobal,
590
+ );
591
+ this.objects.push(
592
+ generated,
593
+ ...httpMethods(generated.id, endpoint.transport),
594
+ ...httpCertificateVerifier(generated.id, generated.name, required(this.plan.http[fn.id])),
595
+ );
596
+ return;
597
+ }
598
+ const {
599
+ blockNames,
600
+ functionNames,
601
+ references,
602
+ objects,
603
+ mappings,
604
+ targetTypes,
605
+ typeMap,
606
+ blocks,
607
+ blockFields,
608
+ numeric,
609
+ layout,
610
+ } = this;
611
+ const block = "inputs" in fn ? fn : undefined;
612
+ const method = "owner" in fn ? fn.owner : undefined;
613
+ const program = this.programRoutines.has(fn.id);
614
+ const owner = method ? required(blocks.get(method.block)) : block;
615
+ const name = block ? required(blockNames.get(block.id)) : required(functionNames.get(fn.id));
616
+ const inputs = "parameters" in fn ? fn.parameters : fn.inputs;
617
+ const state = owner ? [...owner.outputs, ...owner.state] : [];
618
+ const mappingName = method ? fn.id : name;
619
+ const routineScope = required(this.routineScopes.get(fn.id));
620
+ const scope = new Map([...references, ...routineScope.references]),
621
+ locals = routineScope.names;
622
+ const declarations = fn.locals.map(
623
+ (item) => ` ${required(scope.get(item.id))} : ${targetTypes.name(item.type)};`,
624
+ );
625
+ const declarationSpans = fn.locals.map((item) => item.span);
626
+ const mutex =
627
+ method && owner?.runtime?.kind === "mailbox"
628
+ ? required(this.mailboxLocks.get(owner.id))
629
+ : undefined;
630
+ const acquired = mutex ? locals.allocate("plc_acquired") : undefined;
631
+ const unavailable = mutex && fn.result ? locals.allocate("plc_unavailable") : undefined;
632
+ if (acquired) {
633
+ declarations.push(` ${acquired} : BOOL;`);
634
+ declarationSpans.push(fn.span);
635
+ }
636
+ if (unavailable && fn.result) {
637
+ declarations.push(
638
+ ` ${unavailable} : ${targetTypes.name(fn.result)} := ${targetTypes.literal(defaultValue(fn.result, typeMap), fn.result)};`,
639
+ );
640
+ declarationSpans.push(fn.span);
641
+ }
642
+ // Count target lines as they are appended; diagnostics use these exact offsets.
643
+ const lines: string[] = [];
644
+ const write = (line: string, depth: number, source?: SourceSpan): void => {
645
+ lines.push(" ".repeat(depth) + line);
646
+ if (source)
647
+ mappings.push({
648
+ path: `${mappingName}/implementation.st`,
649
+ line: lines.length,
650
+ span: source,
651
+ });
652
+ };
653
+
654
+ // Only the holder releases, so a failed swap is spurious (see the acquire above); retry it.
655
+ const release = (lock: string, depth: number, source?: SourceSpan) => {
656
+ write(`WHILE NOT __COMPARE_AND_SWAP(ADR(${lock}), 1, 0) DO`, depth, source);
657
+ write(";", depth + 1);
658
+ write("END_WHILE;", depth);
659
+ };
660
+
661
+ const load = (id: string): string => {
662
+ const value = scope.get(id);
663
+ if (!value) throw new Error(`Missing verified storage ${id}.`);
664
+ return value;
665
+ };
666
+ const scopeTypes = new Map(
667
+ [...core.variables, ...inputs, ...state, ...fn.locals].map((variable) => [
668
+ variable.id,
669
+ variable.type,
670
+ ]),
671
+ );
672
+
673
+ // Block output records are a source abstraction; target FB fields are direct.
674
+ const place = (id: string, path: CorePath = []): string => {
675
+ let text = load(id),
676
+ typeId = required(scopeTypes.get(id));
677
+ let outputBlock: string | undefined;
678
+ for (const segment of path) {
679
+ const type = required(typeMap.get(typeId));
680
+ if (segment.kind === "field" && type.kind === "block" && segment.name === "out") {
681
+ outputBlock = type.block;
682
+ typeId = required(type.output);
683
+ continue;
684
+ }
685
+ if (segment.kind === "field" && outputBlock) {
686
+ const output = required(
687
+ required(blocks.get(outputBlock)).outputs.find((field) => field.name === segment.name),
688
+ );
689
+ text += `.${required(required(blockFields.get(outputBlock)).get(output.id))}`;
690
+ typeId = output.type;
691
+ outputBlock = undefined;
692
+ } else if (segment.kind === "field" && type.kind === "record") {
693
+ text += `.${targetTypes.field(typeId, segment.name)}`;
694
+ typeId = required(type.fields.find((field) => field.name === segment.name)).type;
695
+ } else if (segment.kind === "index" && type.kind === "array") {
696
+ // A strict enum converts explicitly to its DINT base, as HTTP JSON encoding does.
697
+ text +=
698
+ typeMap.get(segment.value.type)?.kind === "enum"
699
+ ? `[TO_DINT(${expression(segment.value)})]`
700
+ : `[${expression(segment.value)}]`;
701
+ typeId = type.element;
702
+ } else throw new Error("Invalid verified place");
703
+ }
704
+ return text;
705
+ };
706
+
707
+ const expression = (value: CoreExpression): string => {
708
+ if (value.kind === "constant") return targetTypes.literal(value.value, value.type);
709
+ if (value.kind === "load") return place(value.root, value.path);
710
+ if (value.kind === "borrow") return place(value.place.root, value.place.path);
711
+ if (value.kind === "convert" && typeMap.get(value.type)?.kind === "string")
712
+ return expression(value.operand);
713
+ if (value.kind === "unary" || value.kind === "convert")
714
+ return numeric.render(value, [expression(value.operand)]);
715
+ return numeric.render(value, [expression(value.left), expression(value.right)]);
716
+ };
717
+
718
+ // Aggregate constants are expanded into legal leaf assignments for ST.
719
+ const assignConstant = (
720
+ destination: string,
721
+ value: Constant,
722
+ id: string,
723
+ depth: number,
724
+ span: SourceSpan,
725
+ ): void => {
726
+ const type = required(typeMap.get(id));
727
+ if (type.kind === "record" && value.kind === "record")
728
+ type.fields.forEach((field) => {
729
+ assignConstant(
730
+ `${destination}.${targetTypes.field(id, field.name)}`,
731
+ required(value.fields[field.name]),
732
+ field.type,
733
+ depth,
734
+ span,
735
+ );
736
+ });
737
+ else if (type.kind === "array" && value.kind === "array")
738
+ value.items.forEach((item, index) => {
739
+ assignConstant(
740
+ `${destination}[${type.lowerBound + index}]`,
741
+ item,
742
+ type.element,
743
+ depth,
744
+ span,
745
+ );
746
+ });
747
+ else write(`${destination} := ${targetTypes.literal(value, id)};`, depth, span);
748
+ };
749
+
750
+ const body = (items: readonly CoreStatement[], depth: number): void => {
751
+ for (const item of items) {
752
+ switch (item.kind) {
753
+ case "assign":
754
+ if (item.value.kind === "constant")
755
+ assignConstant(
756
+ place(item.root, item.path),
757
+ item.value.value,
758
+ item.value.type,
759
+ depth,
760
+ item.span,
761
+ );
762
+ else
763
+ write(
764
+ `${place(item.root, item.path)} := ${expression(item.value)};`,
765
+ depth,
766
+ item.span,
767
+ );
768
+ break;
769
+ case "blockUpdate": {
770
+ const instance = instancedBlock(required(scopeTypes.get(item.instance)), typeMap);
771
+ if (!instance) throw new Error("Invalid verified block receiver");
772
+ const target = required(blocks.get(instance.block));
773
+ const arguments_ = Object.entries(item.inputs).map(([input, value]) => {
774
+ const formal = required(target.inputs.find((field) => field.name === input));
775
+ return `${required(required(blockFields.get(target.id)).get(formal.id))} := ${expression(value)}`;
776
+ });
777
+ const receiver = item.index
778
+ ? `${load(item.instance)}[${expression(item.index)}]`
779
+ : load(item.instance);
780
+ write(`${receiver}(${arguments_.join(", ")});`, depth, item.span);
781
+ break;
782
+ }
783
+ case "invoke": {
784
+ const receiver = item.instance
785
+ ? `${load(item.instance)}${item.index ? `[${expression(item.index)}]` : ""}.`
786
+ : "";
787
+ const called = required(core.functions.find((fn) => fn.id === item.function));
788
+ const programCall = this.programRoutines.has(called.id);
789
+ const arguments_ = item.arguments.map((value, index) => {
790
+ if (
791
+ !called.owner &&
792
+ !programCall &&
793
+ !called.parameters.some((parameter) => parameter.mode === "inOut")
794
+ )
795
+ return expression(value);
796
+ const parameter = required(called.parameters[index]);
797
+ const formal = required(
798
+ required(this.routineScopes.get(called.id)).references.get(parameter.id),
799
+ );
800
+ return `${formal} := ${expression(value)}`;
801
+ });
802
+ const call = `${receiver}${required(functionNames.get(item.function))}(${arguments_.join(", ")})`;
803
+ if (programCall) {
804
+ write(`${call};`, depth, item.span);
805
+ if (item.destination)
806
+ write(
807
+ `${load(item.destination)} := ${required(functionNames.get(called.id))}.${required(this.programResults.get(called.id))};`,
808
+ depth,
809
+ item.span,
810
+ );
811
+ break;
812
+ }
813
+ write(
814
+ item.destination ? `${load(item.destination)} := ${call};` : `${call};`,
815
+ depth,
816
+ item.span,
817
+ );
818
+ break;
819
+ }
820
+ case "if":
821
+ write(`IF ${expression(item.condition)} THEN`, depth, item.span);
822
+ body(item.then, depth + 1);
823
+ if (item.else.length) {
824
+ write("ELSE", depth, item.span);
825
+ body(item.else, depth + 1);
826
+ }
827
+ write("END_IF;", depth, item.span);
828
+ break;
829
+ case "switch": {
830
+ for (const [index, branch] of item.cases.entries()) {
831
+ const conditions = branch.values.map(
832
+ (value) =>
833
+ `(${expression(item.value)} = ${targetTypes.literal(value, item.value.type)})`,
834
+ );
835
+ write(`${index ? "ELSIF" : "IF"} ${conditions.join(" OR ")} THEN`, depth, item.span);
836
+ body(branch.body, depth + 1);
837
+ }
838
+ if (item.cases.length) {
839
+ if (item.fallback.length) write("ELSE", depth, item.span);
840
+ body(item.fallback, depth + 1);
841
+ write("END_IF;", depth, item.span);
842
+ } else body(item.fallback, depth);
843
+ break;
844
+ }
845
+ case "for": {
846
+ // Iterate with a DINT ordinal rather than overflowing the authored
847
+ // counter at a width boundary. Reconstruct its typed value each scan.
848
+ const counterType = scopeTypes.get(item.counter);
849
+ if (!counterType || item.start.kind !== "integer")
850
+ throw new Error("Invalid verified loop");
851
+ if (!item.maxIterations) {
852
+ if (item.counterScope === "existing")
853
+ write(
854
+ `${load(item.counter)} := ${item.dynamic ? expression(item.dynamic.start) : targetTypes.literal(item.start, counterType)};`,
855
+ depth,
856
+ item.span,
857
+ );
858
+ break;
859
+ }
860
+ const ordinal = locals.allocate("iteration");
861
+ declarations.push(` ${ordinal} : DINT;`);
862
+ declarationSpans.push(item.span);
863
+ const wide = counterType === "ULINT" ? "ULINT" : "LINT";
864
+ const ascending = BigInt(item.step) > 0n;
865
+ const step = BigInt(item.step) < 0n ? -BigInt(item.step) : BigInt(item.step);
866
+ let iterations = `DINT#${item.maxIterations}`;
867
+ let first = `${wide}#${item.start.value}`;
868
+ if (item.dynamic) {
869
+ first = `TO_${wide}(${expression(item.dynamic.start)})`;
870
+ const last = `TO_${wide}(${expression(item.dynamic.limit)})`;
871
+ const count = locals.allocate("iterationCount");
872
+ declarations.push(` ${count} : DINT;`);
873
+ declarationSpans.push(item.span);
874
+ const comparison = ascending ? "<" : ">";
875
+ const inclusive = item.dynamic.exclusive ? "" : "=";
876
+ const distance = ascending ? `${last} - ${first}` : `${first} - ${last}`;
877
+ const exclusive = item.dynamic.exclusive ? ` - ${wide}#1` : "";
878
+ write(`${count} := DINT#0;`, depth, item.span);
879
+ write(`IF ${first} ${comparison}${inclusive} ${last} THEN`, depth, item.span);
880
+ write(
881
+ `${count} := ${wide}_TO_DINT(((${distance}${exclusive}) / ${wide}#${step}) + ${wide}#1);`,
882
+ depth + 1,
883
+ item.span,
884
+ );
885
+ write("END_IF;", depth, item.span);
886
+ iterations = count;
887
+ }
888
+ write(
889
+ `FOR ${ordinal} := DINT#0 TO ${item.dynamic ? `${iterations} - DINT#1` : `DINT#${item.maxIterations - 1}`} DO`,
890
+ depth,
891
+ item.span,
892
+ );
893
+ const counterValue = `${load(item.counter)} := TO_${counterType}(${first} ${ascending ? "+" : "-"} (${wide}#${step} * DINT_TO_${wide}(${ordinal})));`;
894
+ write(counterValue, depth + 1, item.span);
895
+ body(item.body, depth + 1);
896
+ write("END_FOR;", depth, item.span);
897
+ // CODESYS keeps the ordinal at EXIT and advances it on normal
898
+ // completion/CONTINUE. Reconstructing here preserves that distinction.
899
+ if (item.counterScope === "existing") write(counterValue, depth, item.span);
900
+ break;
901
+ }
902
+ case "return":
903
+ if (block) {
904
+ if (item.value) {
905
+ const returned = expression(item.value);
906
+ for (const output of block.outputs)
907
+ write(
908
+ `${load(output.id)} := ${returned}.${targetTypes.field(required(block.result), output.name)};`,
909
+ depth,
910
+ item.span,
911
+ );
912
+ }
913
+ } else if (program) {
914
+ if (item.value)
915
+ write(
916
+ `${required(this.programResults.get(fn.id))} := ${expression(item.value)};`,
917
+ depth,
918
+ item.span,
919
+ );
920
+ } else if (!method || fn.result)
921
+ write(
922
+ `${name} := ${item.value ? expression(item.value) : "TRUE"};`,
923
+ depth,
924
+ item.span,
925
+ );
926
+ if (mutex) release(mutex, depth, item.span);
927
+ write("RETURN;", depth, item.span);
928
+ break;
929
+ case "break":
930
+ write("EXIT;", depth, item.span);
931
+ break;
932
+ case "continue":
933
+ write("CONTINUE;", depth, item.span);
934
+ break;
935
+ }
936
+ }
937
+ };
938
+
939
+ if (mutex && acquired) {
940
+ // ARM64 implements __COMPARE_AND_SWAP with load-linked/store-conditional, which can fail
941
+ // even when the word holds the expected value. Retry while the lock reads free.
942
+ write(`${acquired} := FALSE;`, 0);
943
+ write(`WHILE NOT ${acquired} AND ${mutex} = 0 DO`, 0);
944
+ write(`${acquired} := __COMPARE_AND_SWAP(ADR(${mutex}), 0, 1);`, 1);
945
+ write("END_WHILE;", 0);
946
+ write(`IF NOT ${acquired} THEN`, 0);
947
+ if (unavailable) write(`${name} := ${unavailable};`, 1);
948
+ write("RETURN;", 1);
949
+ write("END_IF;", 0);
950
+ }
951
+ body(fn.body, 0);
952
+ if (mutex) release(mutex, 0);
953
+ if (!block && !method && !program && !fn.result) write(`${name} := TRUE;`, 0);
954
+
955
+ const header = this.routineHeader(fn, name);
956
+ const declaration = [header.declaration];
957
+ mappings.push({ path: `${mappingName}/declaration.st`, line: 1, span: fn.span });
958
+ const section = (keyword: string, items: readonly Variable[]): void => {
959
+ if (!items.length) return;
960
+ declaration.push(keyword);
961
+ for (const item of items) {
962
+ mappings.push({
963
+ path: `${mappingName}/declaration.st`,
964
+ line: declaration.length + 1,
965
+ span: item.span,
966
+ });
967
+ const initial =
968
+ item.initial && !instancedBlock(item.type, typeMap)
969
+ ? targetTypes.declarationInitializer(item.initial, item.type)
970
+ : "";
971
+ declaration.push(` ${load(item.id)} : ${targetTypes.name(item.type)}${initial};`);
972
+ }
973
+ declaration.push("END_VAR");
974
+ };
975
+
976
+ section(
977
+ "VAR_INPUT",
978
+ inputs.filter((item) => item.mode !== "inOut"),
979
+ );
980
+ section(
981
+ "VAR_IN_OUT",
982
+ inputs.filter((item) => item.mode === "inOut"),
983
+ );
984
+ if (program && "parameters" in fn && fn.result) {
985
+ declaration.push(
986
+ "VAR_OUTPUT",
987
+ ` ${required(this.programResults.get(fn.id))} : ${targetTypes.name(fn.result)};`,
988
+ "END_VAR",
989
+ );
990
+ }
991
+ if (block) {
992
+ if (block.runtime?.kind === "mailbox")
993
+ declaration.push(
994
+ "VAR",
995
+ ` ${required(this.mailboxLocks.get(block.id))} : __XWORD;`,
996
+ "END_VAR",
997
+ );
998
+ section("VAR_OUTPUT", block.outputs);
999
+ section(
1000
+ "VAR",
1001
+ block.state.filter((item) => !layout.retainedVariables.has(item.id)),
1002
+ );
1003
+ section(
1004
+ "VAR RETAIN",
1005
+ block.state.filter((item) => layout.retainedVariables.has(item.id)),
1006
+ );
1007
+ }
1008
+ if (declarations.length) {
1009
+ declaration.push(block || program ? "VAR_TEMP" : "VAR");
1010
+ declarationSpans.forEach((span, index) => {
1011
+ mappings.push({
1012
+ path: `${mappingName}/declaration.st`,
1013
+ line: declaration.length + index + 1,
1014
+ span,
1015
+ });
1016
+ });
1017
+ declaration.push(...declarations, "END_VAR");
1018
+ }
1019
+
1020
+ objects.push({
1021
+ id: fn.id,
1022
+ kind: header.kind,
1023
+ name,
1024
+ declaration: `${declaration.join("\n")}\n`,
1025
+ implementation: `${lines.join("\n")}\n`,
1026
+ });
1027
+ }
1028
+
1029
+ private emitProgram(program: CodesysPlan["core"]["programs"][number]): void {
1030
+ const { core, options } = this.plan;
1031
+ const { names, functionNames, objects } = this;
1032
+ // Each wrapper snapshots physical inputs before the authored scan and commits
1033
+ // outputs afterwards. RETURN in an authored routine cannot skip that epilogue.
1034
+ const explicit =
1035
+ options.programNames && Object.hasOwn(options.programNames, program.name)
1036
+ ? options.programNames[program.name]
1037
+ : undefined;
1038
+ const name = explicit ?? names.allocateObject("PRG", program.name);
1039
+ const fn = required(core.functions.find((item) => item.id === program.entry));
1040
+ const used = accessedStorage(fn, core);
1041
+ const channels = this.channels.filter((channel) => used.has(channel.root));
1042
+ const stateful = this.programRoutines.has(program.entry);
1043
+ const call = `${functionNames.get(program.entry)}();`;
1044
+ const lines = [
1045
+ ...channels
1046
+ .filter((channel) => channel.input)
1047
+ .map(({ target, wire, inverted }) => `${target} := ${inverted ? "NOT " : ""}${wire};`),
1048
+ stateful ? call : `plc_discard := ${call}`,
1049
+ ...channels
1050
+ .filter((channel) => !channel.input)
1051
+ .map(({ target, wire }) => `${wire} := ${target};`),
1052
+ ];
1053
+ objects.push({
1054
+ id: program.id,
1055
+ kind: "program",
1056
+ name,
1057
+ declaration: stateful
1058
+ ? `PROGRAM ${name}\n`
1059
+ : `PROGRAM ${name}\nVAR\n plc_discard : BOOL;\nEND_VAR\n`,
1060
+ implementation: `${lines.join("\n")}\n`,
1061
+ });
1062
+ }
1063
+ }