@tsplc/compiler 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +16 -2
- package/backends/codesys/config.ts +54 -0
- package/backends/codesys/emit.ts +1063 -0
- package/backends/codesys/engineering.ts +88 -0
- package/backends/codesys/http.ts +598 -0
- package/backends/codesys/index.ts +12 -0
- package/backends/codesys/io-paths.ts +103 -0
- package/backends/codesys/names.ts +76 -0
- package/backends/codesys/numeric.ts +356 -0
- package/backends/codesys/plan.ts +512 -0
- package/backends/codesys/publication.ts +59 -0
- package/backends/codesys/storage.ts +63 -0
- package/backends/codesys/task-access.ts +118 -0
- package/backends/codesys/types.ts +178 -0
- package/backends/codesys/vendor.ts +108 -0
- package/dist/cli.js +1396 -0
- package/dist/cli.js.map +16 -0
- package/dist/clock.js +8 -0
- package/dist/clock.js.map +10 -0
- package/dist/codesys.js +2587 -0
- package/dist/codesys.js.map +22 -0
- package/dist/elaborate-capture.js +118 -0
- package/dist/elaborate-capture.js.map +10 -0
- package/dist/index.js +6859 -0
- package/dist/index.js.map +25 -0
- package/dist/ir.js +1888 -0
- package/dist/ir.js.map +22 -0
- package/dist/math.js +24 -0
- package/dist/math.js.map +10 -0
- package/dist/project.js +429 -0
- package/dist/project.js.map +11 -0
- package/ir/aggregates.ts +333 -0
- package/ir/allocation.ts +74 -0
- package/ir/core-model.ts +116 -0
- package/ir/core.ts +63 -0
- package/ir/declarations.ts +201 -0
- package/ir/diagnostics.ts +43 -0
- package/ir/domains.ts +96 -0
- package/ir/index.ts +7 -0
- package/ir/link.ts +176 -0
- package/ir/model.ts +268 -0
- package/ir/normalization-error.ts +10 -0
- package/ir/routine.ts +826 -0
- package/ir/scalars.ts +675 -0
- package/ir/types.ts +70 -0
- package/package.json +106 -4
- package/project/artifacts.ts +277 -0
- package/project/cli.ts +305 -0
- package/project/index.ts +1 -0
- package/project/plant.ts +437 -0
- package/project/project.ts +312 -0
- package/project/server.ts +83 -0
- package/src/frontend/async.ts +1060 -0
- package/src/frontend/blocks.ts +419 -0
- package/src/frontend/configuration.ts +167 -0
- package/src/frontend/const-functions.ts +153 -0
- package/src/frontend/cooperative.ts +75 -0
- package/src/frontend/elaborate/capture.ts +138 -0
- package/src/frontend/elaborate/graph.ts +94 -0
- package/src/frontend/elaborate/index.ts +302 -0
- package/src/frontend/elaborate/layouts.ts +390 -0
- package/src/frontend/elaborate/model.ts +177 -0
- package/src/frontend/elaborate/specialize.ts +2280 -0
- package/src/frontend/exposure.ts +147 -0
- package/src/frontend/module-graph.ts +63 -0
- package/src/frontend/promises.ts +751 -0
- package/src/frontend/services.ts +435 -0
- package/src/frontend/types.ts +578 -0
- package/src/frontend/typescript.ts +2442 -0
- package/src/index.ts +7 -0
- package/stdlib/clock.ts +2 -0
- package/stdlib/math.ts +19 -0
- package/types/async.d.ts +81 -0
- package/types/clock.d.ts +4 -0
- package/types/config.d.ts +2 -0
- package/types/data-types.d.ts +31 -0
- package/types/globals.d.ts +40 -0
- package/types/index.d.ts +111 -0
- package/types/plc.d.ts +5 -0
|
@@ -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
|
+
}
|