@tsplc/compiler 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +16 -2
- package/backends/codesys/config.ts +54 -0
- package/backends/codesys/emit.ts +1063 -0
- package/backends/codesys/engineering.ts +88 -0
- package/backends/codesys/http.ts +598 -0
- package/backends/codesys/index.ts +12 -0
- package/backends/codesys/io-paths.ts +103 -0
- package/backends/codesys/names.ts +76 -0
- package/backends/codesys/numeric.ts +356 -0
- package/backends/codesys/plan.ts +512 -0
- package/backends/codesys/publication.ts +59 -0
- package/backends/codesys/storage.ts +63 -0
- package/backends/codesys/task-access.ts +118 -0
- package/backends/codesys/types.ts +178 -0
- package/backends/codesys/vendor.ts +108 -0
- package/dist/cli.js +1396 -0
- package/dist/cli.js.map +16 -0
- package/dist/clock.js +8 -0
- package/dist/clock.js.map +10 -0
- package/dist/codesys.js +2587 -0
- package/dist/codesys.js.map +22 -0
- package/dist/elaborate-capture.js +118 -0
- package/dist/elaborate-capture.js.map +10 -0
- package/dist/index.js +6859 -0
- package/dist/index.js.map +25 -0
- package/dist/ir.js +1888 -0
- package/dist/ir.js.map +22 -0
- package/dist/math.js +24 -0
- package/dist/math.js.map +10 -0
- package/dist/project.js +429 -0
- package/dist/project.js.map +11 -0
- package/ir/aggregates.ts +333 -0
- package/ir/allocation.ts +74 -0
- package/ir/core-model.ts +116 -0
- package/ir/core.ts +63 -0
- package/ir/declarations.ts +201 -0
- package/ir/diagnostics.ts +43 -0
- package/ir/domains.ts +96 -0
- package/ir/index.ts +7 -0
- package/ir/link.ts +176 -0
- package/ir/model.ts +268 -0
- package/ir/normalization-error.ts +10 -0
- package/ir/routine.ts +826 -0
- package/ir/scalars.ts +675 -0
- package/ir/types.ts +70 -0
- package/package.json +106 -4
- package/project/artifacts.ts +277 -0
- package/project/cli.ts +305 -0
- package/project/index.ts +1 -0
- package/project/plant.ts +437 -0
- package/project/project.ts +312 -0
- package/project/server.ts +83 -0
- package/src/frontend/async.ts +1060 -0
- package/src/frontend/blocks.ts +419 -0
- package/src/frontend/configuration.ts +167 -0
- package/src/frontend/const-functions.ts +153 -0
- package/src/frontend/cooperative.ts +75 -0
- package/src/frontend/elaborate/capture.ts +138 -0
- package/src/frontend/elaborate/graph.ts +94 -0
- package/src/frontend/elaborate/index.ts +302 -0
- package/src/frontend/elaborate/layouts.ts +390 -0
- package/src/frontend/elaborate/model.ts +177 -0
- package/src/frontend/elaborate/specialize.ts +2280 -0
- package/src/frontend/exposure.ts +147 -0
- package/src/frontend/module-graph.ts +63 -0
- package/src/frontend/promises.ts +751 -0
- package/src/frontend/services.ts +435 -0
- package/src/frontend/types.ts +578 -0
- package/src/frontend/typescript.ts +2442 -0
- package/src/index.ts +7 -0
- package/stdlib/clock.ts +2 -0
- package/stdlib/math.ts +19 -0
- package/types/async.d.ts +81 -0
- package/types/clock.d.ts +4 -0
- package/types/config.d.ts +2 -0
- package/types/data-types.d.ts +31 -0
- package/types/globals.d.ts +40 -0
- package/types/index.d.ts +111 -0
- package/types/plc.d.ts +5 -0
package/dist/codesys.js
ADDED
|
@@ -0,0 +1,2587 @@
|
|
|
1
|
+
// packages/compiler/backends/codesys/config.ts
|
|
2
|
+
import {
|
|
3
|
+
httpOptionsSchema,
|
|
4
|
+
buildTargetSchema,
|
|
5
|
+
publicationSchema
|
|
6
|
+
} from "@tsplc/config";
|
|
7
|
+
import { z as z2 } from "zod";
|
|
8
|
+
|
|
9
|
+
// packages/compiler/backends/codesys/engineering.ts
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { buildProject, configSchema, resolveConfig } from "@tsplc/build";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
var engineeringBuildSchema = configSchema.omit({
|
|
14
|
+
sourceDir: true,
|
|
15
|
+
entry: true,
|
|
16
|
+
outDir: true,
|
|
17
|
+
task: true,
|
|
18
|
+
tasks: true
|
|
19
|
+
});
|
|
20
|
+
var engineeringSchema = z.strictObject({
|
|
21
|
+
kind: z.literal("v3.5"),
|
|
22
|
+
build: engineeringBuildSchema.optional()
|
|
23
|
+
});
|
|
24
|
+
function codesysBuildConfig(plan, bundle, paths) {
|
|
25
|
+
const settings = plan.options.engineering.build;
|
|
26
|
+
if (!settings)
|
|
27
|
+
throw new Error("CODESYS build requires engineering.build settings (codesys installation and template). Configure them or use --emit-only.");
|
|
28
|
+
const programs = new Map(plan.core.programs.map((program) => {
|
|
29
|
+
const matches = bundle.objects.filter((object) => object.id === program.id && object.kind === "program");
|
|
30
|
+
const generated = matches[0];
|
|
31
|
+
if (matches.length !== 1 || !generated)
|
|
32
|
+
throw new Error(`Generated bundle must contain one PROGRAM object for ${program.name}.`);
|
|
33
|
+
return [program.name, generated];
|
|
34
|
+
}));
|
|
35
|
+
const tasks = plan.deployment.tasks.map((task) => ({
|
|
36
|
+
...task,
|
|
37
|
+
programs: task.programs.map((name) => {
|
|
38
|
+
const program = programs.get(name);
|
|
39
|
+
if (!program)
|
|
40
|
+
throw new Error(`Task ${task.name} references a missing generated PROGRAM ${name}.`);
|
|
41
|
+
return program.name;
|
|
42
|
+
})
|
|
43
|
+
}));
|
|
44
|
+
const first = plan.core.programs[0], entry = first && programs.get(first.name);
|
|
45
|
+
if (!entry)
|
|
46
|
+
throw new Error("CODESYS build requires a generated PROGRAM entry.");
|
|
47
|
+
const sourceDir = resolve(paths.sourceDir);
|
|
48
|
+
return resolveConfig({
|
|
49
|
+
...settings,
|
|
50
|
+
libraryReferences: [
|
|
51
|
+
...settings.libraryReferences ?? [],
|
|
52
|
+
...Object.values(plan.http).some((endpoint) => endpoint.transport !== "opcua") ? ["Net Base Services, 4.0.0.0 (CODESYS)"] : [],
|
|
53
|
+
...Object.values(plan.http).some((endpoint) => endpoint.tls) ? ["CmpX509Cert Implementation, 3.5.22.0 (System)"] : []
|
|
54
|
+
],
|
|
55
|
+
...paths.libraries ? { libraries: paths.libraries } : {},
|
|
56
|
+
...paths.deviceTree ? { deviceTree: paths.deviceTree } : {},
|
|
57
|
+
...paths.controller ? { controller: paths.controller } : {},
|
|
58
|
+
sourceDir,
|
|
59
|
+
entry: resolve(sourceDir, entry.sourcePath ?? `${entry.name}/declaration.st`),
|
|
60
|
+
outDir: resolve(paths.outDir),
|
|
61
|
+
tasks
|
|
62
|
+
}, paths.configFile);
|
|
63
|
+
}
|
|
64
|
+
async function buildCodesys(...arguments_) {
|
|
65
|
+
return buildProject(codesysBuildConfig(...arguments_));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// packages/compiler/backends/codesys/names.ts
|
|
69
|
+
var keywords = new Set(`
|
|
70
|
+
ACTION END_ACTION PROGRAM END_PROGRAM FUNCTION END_FUNCTION FUNCTION_BLOCK END_FUNCTION_BLOCK
|
|
71
|
+
INTERFACE END_INTERFACE METHOD END_METHOD PROPERTY END_PROPERTY
|
|
72
|
+
TYPE END_TYPE STRUCT END_STRUCT UNION END_UNION ARRAY POINTER REFERENCE
|
|
73
|
+
VAR END_VAR VAR_INPUT VAR_OUTPUT VAR_IN_OUT VAR_TEMP VAR_GLOBAL VAR_EXTERNAL
|
|
74
|
+
VAR_ACCESS VAR_CONFIG VAR_STAT VAR_INST VAR_GENERIC
|
|
75
|
+
CONSTANT RETAIN NON_RETAIN PERSISTENT AT READ_ONLY READ_WRITE PARAMS
|
|
76
|
+
CONFIGURATION END_CONFIGURATION RESOURCE END_RESOURCE TASK WITH SINGLE INTERVAL PRIORITY
|
|
77
|
+
PRIVATE PUBLIC PROTECTED INTERNAL ABSTRACT FINAL OVERRIDE EXTENDS IMPLEMENTS THIS SUPER
|
|
78
|
+
IF THEN ELSIF ELSE END_IF CASE OF END_CASE FOR TO BY DO END_FOR
|
|
79
|
+
WHILE END_WHILE REPEAT UNTIL END_REPEAT RETURN EXIT CONTINUE JMP
|
|
80
|
+
TRUE FALSE BOOL BYTE WORD DWORD LWORD SINT USINT INT UINT DINT UDINT LINT ULINT
|
|
81
|
+
REAL LREAL TIME LTIME DATE LDATE DATE_AND_TIME LDATE_AND_TIME TIME_OF_DAY LTIME_OF_DAY
|
|
82
|
+
DT LDT TOD LTOD STRING WSTRING CHAR WCHAR
|
|
83
|
+
ANY ANY_BIT ANY_DATE ANY_DERIVED ANY_ELEMENTARY ANY_INT ANY_MAGNITUDE ANY_NUM
|
|
84
|
+
ANY_REAL ANY_STRING ANY_UNSIGNED
|
|
85
|
+
AND AND_THEN OR OR_ELSE XOR NOT MOD ADD SUB MUL DIV MOVE SEL MIN MAX LIMIT
|
|
86
|
+
ABS ACOS ASIN ATAN COS SIN TAN EXP EXPT LN LOG SQRT TRUNC SHL SHR ROL ROR
|
|
87
|
+
ADR SIZEOF CONCAT DELETE FIND INSERT LEFT LEN MID REPLACE RIGHT
|
|
88
|
+
`.trim().split(/\s+/));
|
|
89
|
+
var types = "(?:BOOL|BYTE|WORD|DWORD|LWORD|SINT|USINT|INT|UINT|DINT|UDINT|LINT|ULINT|REAL|LREAL|TIME|LTIME|DATE|LDATE|DATE_AND_TIME|LDATE_AND_TIME|TIME_OF_DAY|LTIME_OF_DAY|DT|LDT|TOD|LTOD|STRING|WSTRING|CHAR|WCHAR)";
|
|
90
|
+
var conversion = new RegExp(`^(?:${types}_TO_${types}|TO_${types}|TRUNC_${types})$`);
|
|
91
|
+
function isIecIdentifier(name) {
|
|
92
|
+
const upper = name.toUpperCase();
|
|
93
|
+
return /^[A-Za-z][A-Za-z0-9_]*$/.test(name) && !name.includes("__") && !keywords.has(upper) && !conversion.test(upper);
|
|
94
|
+
}
|
|
95
|
+
var identifierBody = (label) => label.replace(/[^A-Za-z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "") || "name";
|
|
96
|
+
var pascalCase = (label) => label.split(/[^A-Za-z0-9]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
97
|
+
|
|
98
|
+
class Names {
|
|
99
|
+
used = new Set;
|
|
100
|
+
allocateObject(prefix, label) {
|
|
101
|
+
const source = prefix === "F" ? label.replace(/^PRG_/, "") : label;
|
|
102
|
+
const kind = prefix === "F" && source.startsWith("FUN_") ? "FUN" : prefix;
|
|
103
|
+
const body = source.startsWith(`${kind}_`) ? source.slice(kind.length + 1) : source;
|
|
104
|
+
return this.allocate(`${kind}_${identifierBody(body)}`);
|
|
105
|
+
}
|
|
106
|
+
reserve(name) {
|
|
107
|
+
this.used.add(name.toUpperCase());
|
|
108
|
+
const prefix = name.split(".")[0];
|
|
109
|
+
if (prefix)
|
|
110
|
+
this.used.add(prefix.toUpperCase());
|
|
111
|
+
}
|
|
112
|
+
allocate(label, preserve = true) {
|
|
113
|
+
const base = preserve && isIecIdentifier(label) ? label : `plc_${identifierBody(label)}`;
|
|
114
|
+
let name = base;
|
|
115
|
+
for (let suffix = 2;this.used.has(name.toUpperCase()); suffix++)
|
|
116
|
+
name = `${base}_${suffix}`;
|
|
117
|
+
this.reserve(name);
|
|
118
|
+
return name;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// packages/compiler/backends/codesys/config.ts
|
|
123
|
+
var addressSchema = z2.object({
|
|
124
|
+
kind: z2.literal("direct"),
|
|
125
|
+
address: z2.string().regex(/^%(?:[IQ]X[0-9]+\.[0-7]|[IQ][BWD][0-9]+)$/),
|
|
126
|
+
type: z2.string().optional(),
|
|
127
|
+
mapped: z2.boolean().optional(),
|
|
128
|
+
inverted: z2.boolean().optional(),
|
|
129
|
+
shared: z2.boolean().optional()
|
|
130
|
+
}).strict();
|
|
131
|
+
var iecNames = z2.record(z2.string().min(1), z2.string().refine(isIecIdentifier, "Use a valid, non-reserved IEC identifier.")).optional();
|
|
132
|
+
var optionsSchema = z2.object({
|
|
133
|
+
engineering: engineeringSchema,
|
|
134
|
+
http: httpOptionsSchema,
|
|
135
|
+
io: z2.record(z2.string().min(1), addressSchema),
|
|
136
|
+
publish: publicationSchema.default({ production: [], development: [] }),
|
|
137
|
+
buildTarget: buildTargetSchema.default("production"),
|
|
138
|
+
programNames: iecNames
|
|
139
|
+
}).strict();
|
|
140
|
+
function codesys(options) {
|
|
141
|
+
return { kind: "codesys", options };
|
|
142
|
+
}
|
|
143
|
+
function direct(address) {
|
|
144
|
+
return { kind: "direct", address };
|
|
145
|
+
}
|
|
146
|
+
// packages/compiler/backends/codesys/emit.ts
|
|
147
|
+
import { defaultValue, instancedBlock as instancedBlock5, standardBlocks } from "@tsplc/compiler/ir";
|
|
148
|
+
|
|
149
|
+
// packages/compiler/backends/codesys/numeric.ts
|
|
150
|
+
import {
|
|
151
|
+
integerRange,
|
|
152
|
+
floatMaximum,
|
|
153
|
+
integerType
|
|
154
|
+
} from "@tsplc/compiler/ir";
|
|
155
|
+
function scalarLiteral(value, type) {
|
|
156
|
+
if (value.kind === "bool")
|
|
157
|
+
return value.value ? "TRUE" : "FALSE";
|
|
158
|
+
if (value.kind === "integer")
|
|
159
|
+
return `${type}#${value.value}`;
|
|
160
|
+
if (value.kind === "float") {
|
|
161
|
+
const text = Object.is(value.value, -0) ? "-0.0" : String(value.value).replace(/e/i, "E");
|
|
162
|
+
return `${type}#${text}`;
|
|
163
|
+
}
|
|
164
|
+
throw new Error("Expected verified scalar constant");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
class NumericLowering {
|
|
168
|
+
types;
|
|
169
|
+
allocate;
|
|
170
|
+
objects = [];
|
|
171
|
+
helpers = new Map;
|
|
172
|
+
constructor(types, allocate) {
|
|
173
|
+
this.types = types;
|
|
174
|
+
this.allocate = allocate;
|
|
175
|
+
}
|
|
176
|
+
prepare(value) {
|
|
177
|
+
if (value.kind === "load") {
|
|
178
|
+
value.path?.forEach((segment) => {
|
|
179
|
+
if (segment.kind === "index")
|
|
180
|
+
this.prepare(segment.value);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (value.kind === "borrow") {
|
|
184
|
+
value.place.path.forEach((segment) => {
|
|
185
|
+
if (segment.kind === "index")
|
|
186
|
+
this.prepare(segment.value);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (value.kind === "binary") {
|
|
190
|
+
this.prepare(value.left);
|
|
191
|
+
this.prepare(value.right);
|
|
192
|
+
if (value.policy)
|
|
193
|
+
this.arithmetic(value.type, value.operator, value.policy);
|
|
194
|
+
}
|
|
195
|
+
if (value.kind === "unary")
|
|
196
|
+
this.prepare(value.operand);
|
|
197
|
+
if (value.kind === "convert") {
|
|
198
|
+
this.prepare(value.operand);
|
|
199
|
+
if (value.policy === "saturate")
|
|
200
|
+
this.saturation(value.operand.type, value.type);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
render(value, operands) {
|
|
204
|
+
if (value.kind === "convert") {
|
|
205
|
+
return value.policy === "saturate" ? `${this.saturation(value.operand.type, value.type)}(${operands[0]})` : `TO_${value.type}(${operands[0]})`;
|
|
206
|
+
}
|
|
207
|
+
if (value.kind === "unary") {
|
|
208
|
+
if (value.operator === "timeToMilliseconds")
|
|
209
|
+
return `TIME_TO_UDINT(${operands[0]})`;
|
|
210
|
+
if (value.operator === "clock")
|
|
211
|
+
return "TO_DINT(TIME_TO_UDINT(TIME()))";
|
|
212
|
+
return value.operator === "not" ? `(NOT ${operands[0]})` : `TO_${value.type}(${value.operator === "negate" ? "-" : "NOT "}${operands[0]})`;
|
|
213
|
+
}
|
|
214
|
+
if (value.kind !== "binary")
|
|
215
|
+
throw new Error("Expected numeric operation");
|
|
216
|
+
if (value.policy) {
|
|
217
|
+
return `${this.arithmetic(value.type, value.operator, value.policy)}(${operands.join(", ")})`;
|
|
218
|
+
}
|
|
219
|
+
if (value.operator === "shiftLeft" || value.operator === "shiftRight") {
|
|
220
|
+
const shift = value.operator === "shiftLeft" ? "SHL" : "SHR";
|
|
221
|
+
return `TO_${value.type}(${shift}(${operands.join(", ")}))`;
|
|
222
|
+
}
|
|
223
|
+
const tokens = {
|
|
224
|
+
add: "+",
|
|
225
|
+
subtract: "-",
|
|
226
|
+
multiply: "*",
|
|
227
|
+
divide: "/",
|
|
228
|
+
remainder: "MOD",
|
|
229
|
+
equal: "=",
|
|
230
|
+
notEqual: "<>",
|
|
231
|
+
less: "<",
|
|
232
|
+
lessEqual: "<=",
|
|
233
|
+
greater: ">",
|
|
234
|
+
greaterEqual: ">=",
|
|
235
|
+
bitAnd: "AND",
|
|
236
|
+
bitOr: "OR",
|
|
237
|
+
bitXor: "XOR"
|
|
238
|
+
};
|
|
239
|
+
const token = tokens[value.operator];
|
|
240
|
+
if (!token)
|
|
241
|
+
throw new Error("Boolean short-circuit operation was not normalized");
|
|
242
|
+
const text = value.operator === "remainder" ? `(${operands[0]} - (${operands[0]} / ${operands[1]}) * ${operands[1]})` : `(${operands[0]} ${token} ${operands[1]})`;
|
|
243
|
+
return value.type === "BOOL" ? text : `TO_${value.type}(${text})`;
|
|
244
|
+
}
|
|
245
|
+
helper(key, type, inputs, body, locals = "") {
|
|
246
|
+
const existing = this.helpers.get(key);
|
|
247
|
+
if (existing)
|
|
248
|
+
return existing;
|
|
249
|
+
const name = this.allocate(`numeric_${key}`);
|
|
250
|
+
this.helpers.set(key, name);
|
|
251
|
+
const parameters = inputs.map(([input, inputType]) => ` ${input} : ${inputType};`).join(`
|
|
252
|
+
`);
|
|
253
|
+
const variables = locals ? `VAR
|
|
254
|
+
${locals}
|
|
255
|
+
END_VAR
|
|
256
|
+
` : "";
|
|
257
|
+
const declaration = `FUNCTION ${name} : ${type}
|
|
258
|
+
VAR_INPUT
|
|
259
|
+
${parameters}
|
|
260
|
+
END_VAR
|
|
261
|
+
${variables}`;
|
|
262
|
+
this.objects.push({
|
|
263
|
+
id: `numeric:${key}`,
|
|
264
|
+
kind: "function",
|
|
265
|
+
name,
|
|
266
|
+
declaration,
|
|
267
|
+
implementation: body(name)
|
|
268
|
+
});
|
|
269
|
+
return name;
|
|
270
|
+
}
|
|
271
|
+
add64() {
|
|
272
|
+
return this.helper("add64", "LWORD", [
|
|
273
|
+
["a", "LWORD"],
|
|
274
|
+
["b", "LWORD"]
|
|
275
|
+
], (name) => `x := a;
|
|
276
|
+
y := b;
|
|
277
|
+
FOR i := 1 TO 64 DO
|
|
278
|
+
carry := SHL(x AND y, 1);
|
|
279
|
+
x := x XOR y;
|
|
280
|
+
y := carry;
|
|
281
|
+
END_FOR;
|
|
282
|
+
${name} := x;
|
|
283
|
+
`, ` x, y, carry : LWORD;
|
|
284
|
+
i : UINT;`);
|
|
285
|
+
}
|
|
286
|
+
wrap64(operator) {
|
|
287
|
+
const add = this.add64();
|
|
288
|
+
if (operator === "add")
|
|
289
|
+
return add;
|
|
290
|
+
if (operator === "subtract") {
|
|
291
|
+
return this.helper("subtract64", "LWORD", [
|
|
292
|
+
["a", "LWORD"],
|
|
293
|
+
["b", "LWORD"]
|
|
294
|
+
], (name) => `${name} := ${add}(a, ${add}(NOT b, LWORD#1));
|
|
295
|
+
`);
|
|
296
|
+
}
|
|
297
|
+
if (operator !== "multiply")
|
|
298
|
+
throw new Error("Unsupported verified wrapping operation");
|
|
299
|
+
return this.helper("multiply64", "LWORD", [
|
|
300
|
+
["a", "LWORD"],
|
|
301
|
+
["b", "LWORD"]
|
|
302
|
+
], (name) => `x := a;
|
|
303
|
+
y := b;
|
|
304
|
+
result := LWORD#0;
|
|
305
|
+
FOR i := 1 TO 64 DO
|
|
306
|
+
IF (y AND LWORD#1) <> LWORD#0 THEN
|
|
307
|
+
result := ${add}(result, x);
|
|
308
|
+
END_IF;
|
|
309
|
+
x := SHL(x, 1);
|
|
310
|
+
y := SHR(y, 1);
|
|
311
|
+
END_FOR;
|
|
312
|
+
${name} := result;
|
|
313
|
+
`, ` x, y, result : LWORD;
|
|
314
|
+
i : UINT;`);
|
|
315
|
+
}
|
|
316
|
+
arithmetic(typeId, operator, policy) {
|
|
317
|
+
const type = this.types.get(typeId);
|
|
318
|
+
if (type?.kind !== "integer")
|
|
319
|
+
throw new Error("Expected verified integer arithmetic");
|
|
320
|
+
const token = operator === "add" ? "+" : operator === "subtract" ? "-" : "*";
|
|
321
|
+
const key = `${policy}_${operator}_${typeId}`;
|
|
322
|
+
if (policy === "wrap") {
|
|
323
|
+
if (type.bits === 64) {
|
|
324
|
+
const operation = this.wrap64(operator);
|
|
325
|
+
return this.helper(key, typeId, [
|
|
326
|
+
["a", typeId],
|
|
327
|
+
["b", typeId]
|
|
328
|
+
], (name) => `${name} := LWORD_TO_${typeId}(${operation}(${typeId}_TO_LWORD(a), ${typeId}_TO_LWORD(b)));
|
|
329
|
+
`);
|
|
330
|
+
}
|
|
331
|
+
const wide = !type.signed && operator !== "subtract" ? "ULINT" : "LINT";
|
|
332
|
+
return this.helper(key, typeId, [
|
|
333
|
+
["a", typeId],
|
|
334
|
+
["b", typeId]
|
|
335
|
+
], (name) => `${name} := ${wide}_TO_${typeId}(${typeId}_TO_${wide}(a) ${token} ${typeId}_TO_${wide}(b));
|
|
336
|
+
`);
|
|
337
|
+
}
|
|
338
|
+
const [minimum, maximum] = integerRange(type);
|
|
339
|
+
const min = `${typeId}#${minimum}`, max = `${typeId}#${maximum}`, zero = `${typeId}#0`;
|
|
340
|
+
return this.helper(key, typeId, [
|
|
341
|
+
["a", typeId],
|
|
342
|
+
["b", typeId]
|
|
343
|
+
], (name) => {
|
|
344
|
+
const clamp = (condition, endpoint, depth = "") => `${depth}IF ${condition} THEN
|
|
345
|
+
${depth} ${name} := ${endpoint};
|
|
346
|
+
${depth} RETURN;
|
|
347
|
+
${depth}END_IF;
|
|
348
|
+
`;
|
|
349
|
+
let body;
|
|
350
|
+
if (!type.signed) {
|
|
351
|
+
if (operator === "add")
|
|
352
|
+
body = clamp(`a > ${max} - b`, max);
|
|
353
|
+
else if (operator === "subtract")
|
|
354
|
+
body = clamp("a < b", zero);
|
|
355
|
+
else
|
|
356
|
+
body = `IF b <> ${zero} THEN
|
|
357
|
+
${clamp(`a > ${max} / b`, max, " ")}END_IF;
|
|
358
|
+
`;
|
|
359
|
+
} else if (operator === "add") {
|
|
360
|
+
body = `IF b > ${zero} THEN
|
|
361
|
+
${clamp(`a > ${max} - b`, max, " ")}ELSIF b < ${zero} THEN
|
|
362
|
+
${clamp(`a < ${min} - b`, min, " ")}END_IF;
|
|
363
|
+
`;
|
|
364
|
+
} else if (operator === "subtract") {
|
|
365
|
+
body = `IF b < ${zero} THEN
|
|
366
|
+
${clamp(`a > ${max} + b`, max, " ")}ELSIF b > ${zero} THEN
|
|
367
|
+
${clamp(`a < ${min} + b`, min, " ")}END_IF;
|
|
368
|
+
`;
|
|
369
|
+
} else {
|
|
370
|
+
body = clamp(`a = ${zero}`, zero) + clamp(`b = ${zero}`, zero) + `IF a > ${zero} THEN
|
|
371
|
+
IF b > ${zero} THEN
|
|
372
|
+
${clamp(`a > ${max} / b`, max, " ")} ELSE
|
|
373
|
+
${clamp(`b < ${min} / a`, min, " ")} END_IF;
|
|
374
|
+
ELSE
|
|
375
|
+
IF b > ${zero} THEN
|
|
376
|
+
${clamp(`a < ${min} / b`, min, " ")} ELSE
|
|
377
|
+
${clamp(`a < ${max} / b`, max, " ")} END_IF;
|
|
378
|
+
END_IF;
|
|
379
|
+
`;
|
|
380
|
+
}
|
|
381
|
+
return `${body}${name} := a ${token} b;
|
|
382
|
+
`;
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
saturation(sourceId, targetId) {
|
|
386
|
+
const source = this.types.get(sourceId), target = this.types.get(targetId);
|
|
387
|
+
if (source?.kind === "float" && target?.kind === "float") {
|
|
388
|
+
if (source.bits <= target.bits)
|
|
389
|
+
return `TO_${targetId}`;
|
|
390
|
+
const maximum = floatMaximum(target);
|
|
391
|
+
const literal = (value, type) => scalarLiteral({ kind: "float", value }, type);
|
|
392
|
+
return this.helper(`saturate_${sourceId}_${targetId}`, targetId, [["value", sourceId]], (name) => `IF value < ${literal(-maximum, sourceId)} THEN
|
|
393
|
+
${name} := ${literal(-maximum, targetId)};
|
|
394
|
+
RETURN;
|
|
395
|
+
END_IF;
|
|
396
|
+
IF value > ${literal(maximum, sourceId)} THEN
|
|
397
|
+
${name} := ${literal(maximum, targetId)};
|
|
398
|
+
RETURN;
|
|
399
|
+
END_IF;
|
|
400
|
+
${name} := TO_${targetId}(value);
|
|
401
|
+
`);
|
|
402
|
+
}
|
|
403
|
+
if (!source || !target || !integerType(source) || !integerType(target)) {
|
|
404
|
+
throw new Error("Expected verified integer saturation");
|
|
405
|
+
}
|
|
406
|
+
const [fromMin, fromMax] = integerRange(source), [toMin, toMax] = integerRange(target);
|
|
407
|
+
return this.helper(`saturate_${sourceId}_${targetId}`, targetId, [["value", sourceId]], (name) => {
|
|
408
|
+
let body = "";
|
|
409
|
+
if (fromMin < toMin)
|
|
410
|
+
body += `IF value < ${sourceId}#${toMin} THEN
|
|
411
|
+
${name} := ${targetId}#${toMin};
|
|
412
|
+
RETURN;
|
|
413
|
+
END_IF;
|
|
414
|
+
`;
|
|
415
|
+
if (fromMax > toMax)
|
|
416
|
+
body += `IF value > ${sourceId}#${toMax} THEN
|
|
417
|
+
${name} := ${targetId}#${toMax};
|
|
418
|
+
RETURN;
|
|
419
|
+
END_IF;
|
|
420
|
+
`;
|
|
421
|
+
return `${body}${name} := TO_${targetId}(value);
|
|
422
|
+
`;
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// packages/compiler/backends/codesys/plan.ts
|
|
428
|
+
import {
|
|
429
|
+
instancedBlock as instancedBlock3,
|
|
430
|
+
isVerifiedCore
|
|
431
|
+
} from "@tsplc/compiler/ir";
|
|
432
|
+
import { ioLayout as ioLayout2 } from "@tsplc/typegen/hardware";
|
|
433
|
+
|
|
434
|
+
// packages/compiler/backends/codesys/io-paths.ts
|
|
435
|
+
import { ioLayout } from "@tsplc/typegen/hardware";
|
|
436
|
+
|
|
437
|
+
// packages/compiler/backends/codesys/storage.ts
|
|
438
|
+
import { basename } from "node:path";
|
|
439
|
+
import { instancedBlock } from "@tsplc/compiler/ir";
|
|
440
|
+
var moduleName = (variable) => basename(variable.span.file).replace(/\.[^.]+$/, "");
|
|
441
|
+
function storageLayout(core) {
|
|
442
|
+
const types = new Map(core.types.map((type) => [type.id, type]));
|
|
443
|
+
const blocks = new Map(core.blocks.map((block) => [block.id, block]));
|
|
444
|
+
const retention = new Map;
|
|
445
|
+
const blockRetained = (id) => {
|
|
446
|
+
const cached = retention.get(id);
|
|
447
|
+
if (cached !== undefined)
|
|
448
|
+
return cached;
|
|
449
|
+
const block = blocks.get(id);
|
|
450
|
+
if (!block)
|
|
451
|
+
throw new Error("Missing verified block during retention planning.");
|
|
452
|
+
const retained = block.state.some((field) => {
|
|
453
|
+
const type = instancedBlock(field.type, types);
|
|
454
|
+
return field.retain || type !== undefined && blockRetained(type.block);
|
|
455
|
+
});
|
|
456
|
+
retention.set(id, retained);
|
|
457
|
+
return retained;
|
|
458
|
+
};
|
|
459
|
+
const retainedBlocks = new Set(core.blocks.filter((block) => blockRetained(block.id)).map((block) => block.id));
|
|
460
|
+
const retainedVariables = new Set([...core.variables, ...core.blocks.flatMap((block) => block.state)].filter((variable) => {
|
|
461
|
+
const type = instancedBlock(variable.type, types);
|
|
462
|
+
return variable.retain || type !== undefined && retainedBlocks.has(type.block);
|
|
463
|
+
}).map((variable) => variable.id));
|
|
464
|
+
const groups = new Map;
|
|
465
|
+
for (const variable of core.variables) {
|
|
466
|
+
if (variable.storage === "input" || variable.storage === "output")
|
|
467
|
+
continue;
|
|
468
|
+
const storage = variable.storage === "constant" ? "constants" : retainedVariables.has(variable.id) ? "retained" : "state";
|
|
469
|
+
const id = `${variable.span.file}:${storage}`;
|
|
470
|
+
let group = groups.get(id);
|
|
471
|
+
if (!group) {
|
|
472
|
+
group = { id, module: moduleName(variable), storage, variables: [] };
|
|
473
|
+
groups.set(id, group);
|
|
474
|
+
}
|
|
475
|
+
group.variables.push(variable);
|
|
476
|
+
}
|
|
477
|
+
return { retainedBlocks, retainedVariables, globalLists: [...groups.values()] };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// packages/compiler/backends/codesys/io-paths.ts
|
|
481
|
+
var pathPattern = /^([\w$-]+)\.([A-Za-z_$][\w$]*)((?:\.[A-Za-z_$][\w$]*|\[-?\d+\])*)$/;
|
|
482
|
+
var isStoragePath = (key) => key.includes(".") || key.includes("[");
|
|
483
|
+
function resolveStoragePath(core, key) {
|
|
484
|
+
const match = pathPattern.exec(key);
|
|
485
|
+
if (!match)
|
|
486
|
+
return { error: `${key}: expected Module.variable followed by .field or [index].` };
|
|
487
|
+
const [, module, name, rest] = match;
|
|
488
|
+
const roots = core.variables.filter((item) => item.storage === "module" && item.name === name && moduleName(item) === module);
|
|
489
|
+
if (roots.length !== 1)
|
|
490
|
+
return {
|
|
491
|
+
error: roots.length ? `${key}: ${module}.${name} is declared by more than one module named ${module}.` : `${key}: no mutable module variable ${module}.${name}.`
|
|
492
|
+
};
|
|
493
|
+
const root = roots[0];
|
|
494
|
+
const types = new Map(core.types.map((type) => [type.id, type]));
|
|
495
|
+
let type = types.get(root.type);
|
|
496
|
+
const segments = [];
|
|
497
|
+
for (const [, field, index] of rest.matchAll(/\.([A-Za-z_$][\w$]*)|\[(-?\d+)\]/g)) {
|
|
498
|
+
if (field !== undefined) {
|
|
499
|
+
const member = type.kind === "record" ? type.fields.find((f) => f.name === field) : undefined;
|
|
500
|
+
if (!member)
|
|
501
|
+
return { error: `${key}: no field ${field}.` };
|
|
502
|
+
segments.push({ kind: "field", record: type.id, name: field });
|
|
503
|
+
type = types.get(member.type);
|
|
504
|
+
} else {
|
|
505
|
+
const value = Number(index);
|
|
506
|
+
if (type.kind !== "array")
|
|
507
|
+
return { error: `${key}: ${index} indexes a non-array value.` };
|
|
508
|
+
if (value < type.lowerBound || value >= type.lowerBound + type.length)
|
|
509
|
+
return {
|
|
510
|
+
error: `${key}: index ${value} is outside ${type.lowerBound}..${type.lowerBound + type.length - 1}.`
|
|
511
|
+
};
|
|
512
|
+
segments.push({ kind: "index", value });
|
|
513
|
+
type = types.get(type.element);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return { key, root, segments, type };
|
|
517
|
+
}
|
|
518
|
+
function iecSpelling(type, types) {
|
|
519
|
+
if (type.kind === "bool" || type.kind === "integer" || type.kind === "bits" || type.kind === "float")
|
|
520
|
+
return type.id;
|
|
521
|
+
if (type.kind !== "array")
|
|
522
|
+
return;
|
|
523
|
+
const element = iecSpelling(types.get(type.element), types);
|
|
524
|
+
const upper = type.lowerBound + type.length - 1;
|
|
525
|
+
return element && !element.startsWith("ARRAY") ? `ARRAY[${type.lowerBound}..${upper}] OF ${element}` : undefined;
|
|
526
|
+
}
|
|
527
|
+
function ioTargets(core) {
|
|
528
|
+
const types = new Map(core.types.map((type) => [type.id, type]));
|
|
529
|
+
const targets = [];
|
|
530
|
+
const add = (key, type) => {
|
|
531
|
+
const layout = ioLayout(iecSpelling(type, types) ?? "");
|
|
532
|
+
if (layout)
|
|
533
|
+
targets.push({ key, layout });
|
|
534
|
+
};
|
|
535
|
+
const visit = (key, type) => {
|
|
536
|
+
add(key, type);
|
|
537
|
+
if (type.kind === "record")
|
|
538
|
+
for (const field of type.fields)
|
|
539
|
+
visit(`${key}.${field.name}`, types.get(field.type));
|
|
540
|
+
if (type.kind === "array")
|
|
541
|
+
visit(`${key}[` + "${number}" + "]", types.get(type.element));
|
|
542
|
+
};
|
|
543
|
+
for (const variable of core.variables) {
|
|
544
|
+
if (variable.storage === "input" || variable.storage === "output")
|
|
545
|
+
add(variable.name, types.get(variable.type));
|
|
546
|
+
else if (variable.storage === "module")
|
|
547
|
+
visit(`${moduleName(variable)}.${variable.name}`, types.get(variable.type));
|
|
548
|
+
}
|
|
549
|
+
return targets;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// packages/compiler/backends/codesys/http.ts
|
|
553
|
+
var opcuaServiceCapacity = 16;
|
|
554
|
+
var resolveHttpEndpoint = (value, input) => {
|
|
555
|
+
const url = new URL(value);
|
|
556
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash || url.hostname.includes(":"))
|
|
557
|
+
throw new Error("fetch requires an HTTP(S) URL with an IPv4 address or DNS hostname, without credentials or fragment.");
|
|
558
|
+
if ((url.pathname + url.search).length > 2048 || url.hostname.length > 253 || Number(url.port || (url.protocol === "https:" ? 443 : 80)) === 0)
|
|
559
|
+
throw new Error("HTTP URL exceeds the native host/path limits or has an invalid port.");
|
|
560
|
+
let token = "";
|
|
561
|
+
const origins = new Set;
|
|
562
|
+
for (const credential of input.credentials) {
|
|
563
|
+
const configured = process.env[credential.originEnv], secret = process.env[credential.bearerTokenEnv];
|
|
564
|
+
if (!configured || !secret)
|
|
565
|
+
throw new Error(`HTTP credentials require ${credential.originEnv} and ${credential.bearerTokenEnv}.`);
|
|
566
|
+
const origin = new URL(configured);
|
|
567
|
+
if (!["http:", "https:"].includes(origin.protocol) || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash)
|
|
568
|
+
throw new Error(`HTTP credential origin ${credential.originEnv} must contain only a scheme, host and port.`);
|
|
569
|
+
if (origins.has(origin.origin))
|
|
570
|
+
throw new Error("HTTP credential origins must be unique.");
|
|
571
|
+
origins.add(origin.origin);
|
|
572
|
+
if (/[\r\n\0]/.test(secret) || secret.length > 512)
|
|
573
|
+
throw new Error(`Invalid bearer token in ${credential.bearerTokenEnv}.`);
|
|
574
|
+
if (origin.origin === url.origin)
|
|
575
|
+
token = secret;
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
host: url.hostname,
|
|
579
|
+
port: Number(url.port || (url.protocol === "https:" ? 443 : 80)),
|
|
580
|
+
authority: url.host,
|
|
581
|
+
path: url.pathname + url.search,
|
|
582
|
+
token,
|
|
583
|
+
timeoutMs: input.timeoutMs,
|
|
584
|
+
tls: url.protocol === "https:"
|
|
585
|
+
};
|
|
586
|
+
};
|
|
587
|
+
var quote = (text) => "'" + text.replaceAll("$", "$$").replaceAll("'", "$'").replaceAll("\r", "$R").replaceAll(`
|
|
588
|
+
`, "$N") + "'";
|
|
589
|
+
var httpLayouts = (block, types) => {
|
|
590
|
+
const request = block.inputs.find((v) => v.name === "request"), response = block.outputs.find((v) => v.name === "result");
|
|
591
|
+
if (!request || !response)
|
|
592
|
+
throw new Error("Missing HTTP record ports.");
|
|
593
|
+
const requestLayout = types.get(request.type), responseLayout = types.get(response.type);
|
|
594
|
+
if (requestLayout?.kind !== "record" || responseLayout?.kind !== "record" || responseLayout.fields.length === 0 || responseLayout.fields.length > 31)
|
|
595
|
+
throw new Error("HTTP requires closed request/response records with 1..31 response fields.");
|
|
596
|
+
const scalarSize = (id) => {
|
|
597
|
+
const type = types.get(id);
|
|
598
|
+
if (type?.kind === "string" && type.encoding === "utf8" && type.capacity <= 2048)
|
|
599
|
+
return type.capacity * 6 + 2;
|
|
600
|
+
if (type?.kind === "bool")
|
|
601
|
+
return 5;
|
|
602
|
+
if (type?.kind === "enum")
|
|
603
|
+
return Math.max(...type.members.map((member) => member.value.length));
|
|
604
|
+
if ((type?.kind === "integer" || type?.kind === "bits") && type.bits <= 32) {
|
|
605
|
+
const signed = type.kind === "integer" && type.signed;
|
|
606
|
+
return String(signed ? -(2 ** (type.bits - 1)) : 2 ** type.bits - 1).length;
|
|
607
|
+
}
|
|
608
|
+
throw new Error("HTTP JSON supports bounded UTF-8 strings, BOOL, enums, and integers up to 32 bits.");
|
|
609
|
+
};
|
|
610
|
+
const recordSize = (record, response) => 2 + record.fields.reduce((size, field) => {
|
|
611
|
+
const type = types.get(field.type);
|
|
612
|
+
const valueSize = type?.kind === "array" && !response ? 2 + type.length * (scalarSize(type.element) + 1) : scalarSize(field.type);
|
|
613
|
+
return size + field.name.length + 4 + valueSize;
|
|
614
|
+
}, 0);
|
|
615
|
+
if (recordSize(requestLayout, false) > 2048 || recordSize(responseLayout, true) > 2048)
|
|
616
|
+
throw new Error("The worst-case HTTP JSON payload exceeds the native 2048-byte buffer; reduce record/string/array bounds.");
|
|
617
|
+
return { request, response, requestLayout, responseLayout };
|
|
618
|
+
};
|
|
619
|
+
var validateHttpLayout = (block, types) => {
|
|
620
|
+
httpLayouts(block, types);
|
|
621
|
+
};
|
|
622
|
+
var emitHttp = (block, name, fields, types, target, endpoint, network, rpcGlobal) => {
|
|
623
|
+
const port = (label) => {
|
|
624
|
+
const variable = [...block.inputs, ...block.outputs].find((v) => v.name === label);
|
|
625
|
+
if (!variable)
|
|
626
|
+
throw new Error("Missing HTTP port " + label);
|
|
627
|
+
return fields.get(variable.id);
|
|
628
|
+
};
|
|
629
|
+
const { request, response, requestLayout, responseLayout } = httpLayouts(block, types);
|
|
630
|
+
const encode = (typeId, value) => {
|
|
631
|
+
const type = types.get(typeId);
|
|
632
|
+
if (type.kind === "array")
|
|
633
|
+
return Array.from({ length: type.length }, (_, i) => `${i ? "Append(',');" : ""}${encode(type.element, `${value}[${i + type.lowerBound}]`)}`).join(`
|
|
634
|
+
`);
|
|
635
|
+
if (type.kind === "string" && type.encoding === "utf8" && type.capacity <= 2048)
|
|
636
|
+
return `WriteString(${value});`;
|
|
637
|
+
if (type.kind === "bool")
|
|
638
|
+
return `IF ${value} THEN Append('true'); ELSE Append('false'); END_IF;`;
|
|
639
|
+
if (type.kind === "enum")
|
|
640
|
+
return `Append(DINT_TO_STRING(TO_DINT(${value})));`;
|
|
641
|
+
if (type.kind === "bits" && type.bits <= 32)
|
|
642
|
+
return `Append(UDINT_TO_STRING(TO_UDINT(${value})));`;
|
|
643
|
+
if (type.kind === "integer" && type.bits <= 32)
|
|
644
|
+
return `Append(${target.name(typeId)}_TO_STRING(${value}));`;
|
|
645
|
+
throw new Error("HTTP JSON supports bounded UTF-8 strings, BOOL, enums, integers up to 32 bits and fixed arrays.");
|
|
646
|
+
};
|
|
647
|
+
const requestCode = requestLayout.fields.map((field, i) => {
|
|
648
|
+
const type = types.get(field.type), value = `${port("request")}.${target.field(request.type, field.name)}`;
|
|
649
|
+
return `Append(${quote((i ? "," : "") + '"' + field.name + '":')});${type.kind === "array" ? "Append('[');" : ""}${encode(field.type, value)}${type.kind === "array" ? "Append(']');" : ""}`;
|
|
650
|
+
}).join(`
|
|
651
|
+
`);
|
|
652
|
+
const responseCode = responseLayout.fields.map((field, i) => {
|
|
653
|
+
const type = types.get(field.type), value = `${port("result")}.${target.field(response.type, field.name)}`;
|
|
654
|
+
let decode;
|
|
655
|
+
if (type.kind === "string" && type.encoding === "utf8" && type.capacity <= 2048)
|
|
656
|
+
decode = `ReadString(); IF ByteLength(token) > ${type.capacity} THEN malformed := TRUE; ELSE ${value} := token; END_IF;`;
|
|
657
|
+
else if (type.kind === "bool")
|
|
658
|
+
decode = `IF MatchLiteral('true') THEN ${value}:=TRUE; ELSIF MatchLiteral('false') THEN ${value}:=FALSE; ELSE malformed:=TRUE;END_IF;`;
|
|
659
|
+
else if ((type.kind === "integer" || type.kind === "bits") && type.bits <= 32 || type.kind === "enum") {
|
|
660
|
+
const signed = type.kind === "enum" || type.kind === "integer" && type.signed, bits = type.kind === "enum" ? 32 : type.bits;
|
|
661
|
+
const min = signed ? -(2 ** (bits - 1)) : 0, max = signed ? 2 ** (bits - 1) - 1 : 2 ** bits - 1;
|
|
662
|
+
decode = `ReadInteger(); IF numberValue < ${min} OR numberValue > ${max} THEN malformed:=TRUE; ELSE `;
|
|
663
|
+
if (type.kind === "enum")
|
|
664
|
+
decode += `CASE numberValue OF ${type.members.map((m) => `${m.value}: ${value}:=${target.name(field.type)}.${target.field(field.type, m.name)};`).join(" ")} ELSE malformed:=TRUE; END_CASE;`;
|
|
665
|
+
else
|
|
666
|
+
decode += `${value}:=TO_${target.name(field.type)}(numberValue);`;
|
|
667
|
+
decode += " END_IF;";
|
|
668
|
+
} else
|
|
669
|
+
throw new Error("HTTP responses currently require flat scalar records.");
|
|
670
|
+
return `${i === 0 ? "IF" : "ELSIF"} part = ${quote(field.name)} THEN IF (seen AND SHL(DWORD#1,${i})) <> 0 THEN malformed:=TRUE; END_IF; seen:=seen OR SHL(DWORD#1,${i}); ${decode}`;
|
|
671
|
+
}).join(`
|
|
672
|
+
`);
|
|
673
|
+
const inputs = block.inputs.map((v) => `${fields.get(v.id)} : ${target.name(v.type)};`).join(`
|
|
674
|
+
`);
|
|
675
|
+
const outputs = block.outputs.map((v) => `${fields.get(v.id)} : ${target.name(v.type)};`).join(`
|
|
676
|
+
`);
|
|
677
|
+
const decodeResponse = `cursor:=1;malformed:=FALSE;seen:=0;
|
|
678
|
+
SkipWhitespace();Expect(123);SkipWhitespace();
|
|
679
|
+
WHILE cursor<=TO_UINT(ByteLength(payload)) AND NOT malformed DO
|
|
680
|
+
IF payload[cursor-1]=125 THEN cursor:=cursor+1;EXIT;END_IF;
|
|
681
|
+
ReadString();part:=token;SkipWhitespace();Expect(58);SkipWhitespace();
|
|
682
|
+
${responseCode} ELSE malformed:=TRUE;END_IF;
|
|
683
|
+
SkipWhitespace();
|
|
684
|
+
IF payload[cursor-1]=44 THEN cursor:=cursor+1;SkipWhitespace();IF payload[cursor-1]=125 THEN malformed:=TRUE;END_IF;
|
|
685
|
+
ELSIF payload[cursor-1]<>125 THEN malformed:=TRUE;END_IF;
|
|
686
|
+
END_WHILE;
|
|
687
|
+
SkipWhitespace();
|
|
688
|
+
IF malformed OR cursor<>TO_UINT(ByteLength(payload))+1 OR seen<>DWORD#${2 ** responseLayout.fields.length - 1} THEN ${port("error")}:=4;END_IF;`;
|
|
689
|
+
if (endpoint.transport === "opcua") {
|
|
690
|
+
if (!rpcGlobal)
|
|
691
|
+
throw new Error("Missing OPC UA RPC global.");
|
|
692
|
+
const channel = (field, index = "slotIndex") => `${rpcGlobal}.${field}[${index}]`;
|
|
693
|
+
return {
|
|
694
|
+
id: block.id,
|
|
695
|
+
kind: "functionBlock",
|
|
696
|
+
name,
|
|
697
|
+
declaration: `FUNCTION_BLOCK ${name}
|
|
698
|
+
VAR_INPUT ${inputs} END_VAR
|
|
699
|
+
VAR_OUTPUT ${outputs} END_VAR
|
|
700
|
+
VAR
|
|
701
|
+
timeout:TON;stage:UINT;slotIndex:UINT;candidate:UINT;
|
|
702
|
+
identity:STRING(64);epoch:STRING(32);prefix:STRING(80);
|
|
703
|
+
payload:STRING(2048);token:STRING(2048);part:STRING(2048);
|
|
704
|
+
cursor:UINT;malformed:BOOL;seen:DWORD;numberValue:LINT;tokenUsed:UINT;
|
|
705
|
+
END_VAR`,
|
|
706
|
+
implementation: `IF NOT ${port("start")} OR ${port("cancel")} THEN
|
|
707
|
+
IF slotIndex>0 THEN ${channel("requestIds")}:='';${channel("requests")}:='';${channel("inUse")}:=0;END_IF;
|
|
708
|
+
slotIndex:=0;stage:=0;timeout(IN:=FALSE);${port("busy")}:=FALSE;${port("done")}:=FALSE;${port("error")}:=0;RETURN;
|
|
709
|
+
END_IF;
|
|
710
|
+
IF stage=0 THEN
|
|
711
|
+
FOR candidate:=1 TO ${opcuaServiceCapacity} DO
|
|
712
|
+
(* __COMPARE_AND_SWAP can fail spuriously on ARM64; retry while the slot reads free. *)
|
|
713
|
+
WHILE slotIndex=0 AND ${channel("epochs", "candidate")}<>'' AND ${channel("inUse", "candidate")}=0 DO
|
|
714
|
+
IF __COMPARE_AND_SWAP(ADR(${channel("inUse", "candidate")}),0,1) THEN slotIndex:=candidate;END_IF;
|
|
715
|
+
END_WHILE;
|
|
716
|
+
IF slotIndex>0 THEN EXIT;END_IF;
|
|
717
|
+
END_FOR;
|
|
718
|
+
IF slotIndex=0 THEN ${port("error")}:=7;stage:=90;
|
|
719
|
+
ELSE
|
|
720
|
+
${channel("sequence")}:=${channel("sequence")}+1;
|
|
721
|
+
IF ${channel("sequence")}=0 THEN ${channel("epochs")}:='';${port("error")}:=5;stage:=90;
|
|
722
|
+
ELSE
|
|
723
|
+
epoch:=${channel("epochs")};identity:=CONCAT(CONCAT(epoch,':'),UDINT_TO_STRING(${channel("sequence")}));
|
|
724
|
+
prefix:=CONCAT(identity,'|');payload:='';malformed:=FALSE;
|
|
725
|
+
Append('{');${requestCode}Append('}');
|
|
726
|
+
IF malformed THEN ${port("error")}:=3;stage:=90;
|
|
727
|
+
ELSIF ByteLength(payload)+LEN(prefix)>2048 THEN ${port("error")}:=3;stage:=90;
|
|
728
|
+
ELSE
|
|
729
|
+
FOR candidate:=0 TO LEN(prefix)-1 DO ${channel("requests")}[candidate]:=prefix[candidate];END_FOR;
|
|
730
|
+
FOR candidate:=0 TO ByteLength(payload)-1 DO ${channel("requests")}[candidate+LEN(prefix)]:=payload[candidate];END_FOR;
|
|
731
|
+
${channel("requests")}[ByteLength(payload)+LEN(prefix)]:=0;${channel("requestIds")}:=identity;stage:=1;
|
|
732
|
+
END_IF;
|
|
733
|
+
END_IF;
|
|
734
|
+
END_IF;
|
|
735
|
+
END_IF;
|
|
736
|
+
timeout(IN:=stage<90,PT:=T#${endpoint.timeoutMs}ms);
|
|
737
|
+
IF timeout.Q THEN ${port("error")}:=2;stage:=90;END_IF;
|
|
738
|
+
IF stage=1 THEN
|
|
739
|
+
IF ${channel("epochs")}<>epoch THEN ${port("error")}:=5;stage:=90;
|
|
740
|
+
ELSIF ${channel("replyIds")}=identity AND_THEN LEFT(${channel("replies")},LEN(prefix))=prefix THEN
|
|
741
|
+
payload:='';cursor:=LEN(prefix);
|
|
742
|
+
IF ByteLength(${channel("replies")})<cursor+4 THEN ${port("error")}:=4;
|
|
743
|
+
ELSIF ${channel("replies")}[cursor]<>48 OR ${channel("replies")}[cursor+1]<>124 THEN ${port("error")}:=5;
|
|
744
|
+
ELSE
|
|
745
|
+
FOR candidate:=cursor+2 TO ByteLength(${channel("replies")})-1 DO payload[candidate-cursor-2]:=${channel("replies")}[candidate];END_FOR;
|
|
746
|
+
payload[ByteLength(${channel("replies")})-cursor-2]:=0;
|
|
747
|
+
${decodeResponse}
|
|
748
|
+
END_IF;
|
|
749
|
+
stage:=90;
|
|
750
|
+
END_IF;
|
|
751
|
+
END_IF;
|
|
752
|
+
${port("busy")}:=stage<90;
|
|
753
|
+
IF stage=90 THEN
|
|
754
|
+
IF slotIndex>0 THEN ${channel("requestIds")}:='';${channel("requests")}:='';${channel("inUse")}:=0;slotIndex:=0;END_IF;
|
|
755
|
+
timeout(IN:=FALSE);${port("busy")}:=FALSE;${port("done")}:=TRUE;
|
|
756
|
+
END_IF;`
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
return {
|
|
760
|
+
id: block.id,
|
|
761
|
+
kind: "functionBlock",
|
|
762
|
+
name,
|
|
763
|
+
declaration: `FUNCTION_BLOCK ${name}
|
|
764
|
+
VAR_INPUT
|
|
765
|
+
${inputs}
|
|
766
|
+
END_VAR
|
|
767
|
+
VAR_OUTPUT
|
|
768
|
+
${outputs}
|
|
769
|
+
END_VAR
|
|
770
|
+
VAR
|
|
771
|
+
remoteAddress : NBS.IPv4Address;
|
|
772
|
+
remoteIP : NBS.IIPAddress := remoteAddress;
|
|
773
|
+
serverHost : STRING(255) := ${quote(endpoint.host)};
|
|
774
|
+
resolver : NBS.ResolveHostname := (itfAsyncProperty:=${network});
|
|
775
|
+
${endpoint.tls ? `certificateVerifier : ${name}_CertificateVerifier;
|
|
776
|
+
tlsContext : NBS.TLSContext := (itfCertVerifer:=certificateVerifier,sUseCaseName:='tsPLC HTTPS',ePurpose:=NBS.PURPOSE.CLIENT_SIDE,sTLSVersion:='1.2',sHostname:=${quote(endpoint.host)},udiVerificationMode:=2);` : ""}
|
|
777
|
+
client : NBS.TCP_Client := (itfAsyncProperty:=${network},itfIPAddress:=remoteAddress,uiPort:=${endpoint.port},udiTimeOut:=${endpoint.timeoutMs * 1000}${endpoint.tls ? ",itfTLSContext:=tlsContext" : ""});
|
|
778
|
+
writer : NBS.TCP_Write;
|
|
779
|
+
reader : NBS.TCP_Read;
|
|
780
|
+
timeout : TON;
|
|
781
|
+
stage : UINT;
|
|
782
|
+
wire : STRING(8192);
|
|
783
|
+
received : STRING(8192);
|
|
784
|
+
chunk : ARRAY[0..8191] OF BYTE;
|
|
785
|
+
payload : STRING(2048);
|
|
786
|
+
token : STRING(2048);
|
|
787
|
+
part : STRING(2048);
|
|
788
|
+
used : UINT;
|
|
789
|
+
headerEnd : UINT;
|
|
790
|
+
contentLength : DINT;
|
|
791
|
+
status : UINT;
|
|
792
|
+
cursor : UINT;
|
|
793
|
+
malformed : BOOL;
|
|
794
|
+
seen : DWORD;
|
|
795
|
+
numberValue : LINT;
|
|
796
|
+
wireUsed : UINT;
|
|
797
|
+
tokenUsed : UINT;
|
|
798
|
+
index : UINT;
|
|
799
|
+
END_VAR
|
|
800
|
+
`,
|
|
801
|
+
implementation: `IF NOT ${port("start")} OR ${port("cancel")} THEN
|
|
802
|
+
resolver(xExecute:=FALSE,itfIPAddress:=remoteIP);client(xEnable:=FALSE); writer(xExecute:=FALSE); reader(xEnable:=FALSE); timeout(IN:=FALSE);
|
|
803
|
+
stage:=0; ${port("busy")}:=FALSE; ${port("done")}:=FALSE; ${port("error")}:=0;
|
|
804
|
+
RETURN;
|
|
805
|
+
END_IF;
|
|
806
|
+
IF stage=0 THEN
|
|
807
|
+
payload:=''; malformed:=FALSE; used:=0; received:='';
|
|
808
|
+
${block.runtime?.kind === "http" && block.runtime.json ? `Append('{'); ${requestCode} Append('}');` : ""}
|
|
809
|
+
IF malformed THEN ${port("error")}:=3; stage:=90;
|
|
810
|
+
ELSE
|
|
811
|
+
wire:='';wireUsed:=0;
|
|
812
|
+
AppendWire(${quote((block.runtime?.kind === "http" ? block.runtime.method : "GET") + " " + endpoint.path + ` HTTP/1.1\r
|
|
813
|
+
Host: ` + endpoint.authority + `\r
|
|
814
|
+
` + (endpoint.token ? "Authorization: Bearer " + endpoint.token + `\r
|
|
815
|
+
` : "") + (block.runtime?.kind === "http" && block.runtime.json ? `Content-Type: application/json\r
|
|
816
|
+
` : "") + `Accept: application/json\r
|
|
817
|
+
Connection: close\r
|
|
818
|
+
Content-Length: `)});
|
|
819
|
+
AppendWire(UINT_TO_STRING(ByteLength(payload)));AppendWire('$R$N$R$N');AppendWire(payload); stage:=10;
|
|
820
|
+
END_IF;
|
|
821
|
+
END_IF;
|
|
822
|
+
timeout(IN:=stage<90,PT:=T#${endpoint.timeoutMs}ms);
|
|
823
|
+
IF timeout.Q THEN ${port("error")}:=2; stage:=90; END_IF;
|
|
824
|
+
${port("busy")}:=stage<90;
|
|
825
|
+
IF stage=10 THEN
|
|
826
|
+
resolver(xExecute:=TRUE,sHostname:=serverHost,itfIPAddress:=remoteIP);
|
|
827
|
+
IF resolver.xError THEN ${port("error")}:=5;stage:=90;ELSIF resolver.xDone THEN stage:=1;END_IF;
|
|
828
|
+
END_IF;
|
|
829
|
+
IF stage=1 THEN
|
|
830
|
+
client(xEnable:=TRUE);
|
|
831
|
+
IF client.xError THEN ${port("error")}:=${endpoint.tls ? 6 : 5};stage:=90; ELSIF client.xActive THEN stage:=2;END_IF;
|
|
832
|
+
END_IF;
|
|
833
|
+
IF stage=2 THEN
|
|
834
|
+
writer(xExecute:=TRUE,itfConnection:=client.itfConnection,pData:=ADR(wire),udiSize:=UINT_TO_UDINT(wireUsed));
|
|
835
|
+
IF writer.xError THEN ${port("error")}:=5;stage:=90;ELSIF writer.xDone THEN stage:=3;END_IF;
|
|
836
|
+
END_IF;
|
|
837
|
+
IF stage=3 THEN
|
|
838
|
+
IF used>=8191 THEN ${port("error")}:=3;stage:=90;
|
|
839
|
+
ELSE
|
|
840
|
+
reader(xEnable:=TRUE,itfConnection:=client.itfConnection,pData:=ADR(chunk),udiSize:=8191);
|
|
841
|
+
IF reader.xError THEN ${port("error")}:=5;stage:=90;
|
|
842
|
+
ELSIF reader.xReady AND reader.udiCount>0 THEN
|
|
843
|
+
IF TO_UDINT(used)+reader.udiCount>8191 THEN ${port("error")}:=3;stage:=90;
|
|
844
|
+
ELSE
|
|
845
|
+
FOR index:=0 TO TO_UINT(reader.udiCount)-1 DO received[used+index]:=chunk[index];END_FOR;
|
|
846
|
+
used:=used+TO_UINT(reader.udiCount);received[used]:=0;
|
|
847
|
+
FindHeaders();
|
|
848
|
+
IF headerEnd=0 AND used>4096 THEN ${port("error")}:=3;stage:=90;END_IF;
|
|
849
|
+
IF headerEnd>0 THEN
|
|
850
|
+
IF headerEnd>4096 THEN ${port("error")}:=3;stage:=90;
|
|
851
|
+
ELSE
|
|
852
|
+
ReadHeaders();
|
|
853
|
+
IF malformed OR contentLength<0 OR contentLength>2048 THEN ${port("error")}:=3;stage:=90;
|
|
854
|
+
ELSIF TO_DINT(used)>=TO_DINT(headerEnd)+3+contentLength THEN
|
|
855
|
+
IF status<200 OR status>=300 THEN ${port("error")}:=10000+status;stage:=90;
|
|
856
|
+
ELSE
|
|
857
|
+
payload:='';IF contentLength>0 THEN FOR index:=0 TO TO_UINT(contentLength)-1 DO payload[index]:=received[headerEnd+3+index]; END_FOR;END_IF;payload[TO_UINT(contentLength)]:=0; ${decodeResponse}
|
|
858
|
+
stage:=90;
|
|
859
|
+
END_IF;
|
|
860
|
+
END_IF;
|
|
861
|
+
END_IF;
|
|
862
|
+
END_IF;
|
|
863
|
+
END_IF;
|
|
864
|
+
END_IF;
|
|
865
|
+
END_IF;
|
|
866
|
+
END_IF;
|
|
867
|
+
IF stage=90 THEN
|
|
868
|
+
resolver(xExecute:=FALSE,itfIPAddress:=remoteIP);client(xEnable:=FALSE);writer(xExecute:=FALSE);reader(xEnable:=FALSE);timeout(IN:=FALSE);
|
|
869
|
+
${port("busy")}:=FALSE;${port("done")}:=TRUE;
|
|
870
|
+
END_IF;
|
|
871
|
+
`
|
|
872
|
+
};
|
|
873
|
+
};
|
|
874
|
+
var httpCertificateVerifier = (owner, name, endpoint) => {
|
|
875
|
+
if (!endpoint.tls)
|
|
876
|
+
return [];
|
|
877
|
+
const id = `${owner}:certificateVerifier`;
|
|
878
|
+
const ipv4 = /^\d+\.\d+\.\d+\.\d+$/.test(endpoint.host);
|
|
879
|
+
return [
|
|
880
|
+
{
|
|
881
|
+
id,
|
|
882
|
+
kind: "functionBlock",
|
|
883
|
+
name: `${name}_CertificateVerifier`,
|
|
884
|
+
declaration: `FUNCTION_BLOCK ${name}_CertificateVerifier IMPLEMENTS NBS.ICertificateVerifier
|
|
885
|
+
VAR
|
|
886
|
+
hostname:STRING(255):=${quote(endpoint.host)};
|
|
887
|
+
${ipv4 ? `address:ARRAY[0..3] OF BYTE := [${endpoint.host.split(".").join(",")}];` : ""}
|
|
888
|
+
END_VAR`,
|
|
889
|
+
implementation: ""
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
id: `${id}:VerifyCertificate`,
|
|
893
|
+
parent: id,
|
|
894
|
+
kind: "method",
|
|
895
|
+
name: "VerifyCertificate",
|
|
896
|
+
declaration: `METHOD PUBLIC VerifyCertificate : UDINT
|
|
897
|
+
VAR_INPUT hCert:POINTER TO BYTE;eCurState:UDINT;END_VAR
|
|
898
|
+
VAR matched:BOOL;result:UDINT;END_VAR`,
|
|
899
|
+
implementation: `VerifyCertificate:=eCurState;
|
|
900
|
+
IF eCurState<>0 THEN RETURN;END_IF;
|
|
901
|
+
matched:=CmpX509Cert_Implementation.${ipv4 ? "X509CertCheckIP(hCert,ADR(address),0,ADR(result))" : "X509CertCheckHost(hCert,ADR(hostname),4,ADR(result))"};
|
|
902
|
+
IF result<>0 THEN VerifyCertificate:=result;ELSIF NOT matched THEN VerifyCertificate:=1;END_IF;`
|
|
903
|
+
}
|
|
904
|
+
];
|
|
905
|
+
};
|
|
906
|
+
var httpMethods = (owner, transport = "http") => {
|
|
907
|
+
const method = (name, declaration, implementation, result) => ({
|
|
908
|
+
id: `${owner}:${name}`,
|
|
909
|
+
parent: owner,
|
|
910
|
+
kind: "method",
|
|
911
|
+
name,
|
|
912
|
+
declaration: `METHOD PRIVATE ${name}${result ? ` : ${result}` : ""}
|
|
913
|
+
${declaration}`,
|
|
914
|
+
implementation
|
|
915
|
+
});
|
|
916
|
+
return [
|
|
917
|
+
method("ByteLength", `VAR_INPUT value:STRING(8192);END_VAR
|
|
918
|
+
VAR i:UINT;END_VAR`, "ByteLength:=0;FOR i:=0 TO 8191 DO IF value[i]=0 THEN ByteLength:=i;RETURN;END_IF;END_FOR;ByteLength:=8192;", "UINT"),
|
|
919
|
+
method("Append", `VAR_INPUT value : STRING(2048); END_VAR
|
|
920
|
+
VAR offset:UINT;i:UINT;END_VAR`, `offset:=TO_UINT(ByteLength(payload));IF offset+TO_UINT(ByteLength(value))>2048 THEN malformed:=TRUE; ELSE IF ByteLength(value)>0 THEN FOR i:=0 TO TO_UINT(ByteLength(value))-1 DO payload[offset+i]:=value[i];END_FOR;END_IF;payload[offset+TO_UINT(ByteLength(value))]:=0;END_IF;`),
|
|
921
|
+
method("AppendWire", `VAR_INPUT value:STRING(8192);END_VAR
|
|
922
|
+
VAR i:UINT;END_VAR`, `IF wireUsed+TO_UINT(ByteLength(value))>8191 THEN malformed:=TRUE;RETURN;END_IF;IF ByteLength(value)>0 THEN FOR i:=0 TO TO_UINT(ByteLength(value))-1 DO wire[wireUsed+i]:=value[i];END_FOR;END_IF;wireUsed:=wireUsed+TO_UINT(ByteLength(value));wire[wireUsed]:=0;`),
|
|
923
|
+
method("FindHeaders", "VAR i:UINT;END_VAR", `headerEnd:=0;IF used<4 THEN RETURN;END_IF;
|
|
924
|
+
FOR i:=0 TO used-4 DO IF received[i]=13 AND received[i+1]=10 AND received[i+2]=13 AND received[i+3]=10 THEN headerEnd:=i+1;RETURN;END_IF;END_FOR;`),
|
|
925
|
+
method("MatchLiteral", `VAR_INPUT value:STRING(8);END_VAR
|
|
926
|
+
VAR i:UINT;size:UINT;END_VAR`, `MatchLiteral:=FALSE;size:=TO_UINT(ByteLength(value));IF cursor+size-1>TO_UINT(ByteLength(payload)) THEN RETURN;END_IF;
|
|
927
|
+
FOR i:=0 TO size-1 DO IF payload[cursor-1+i]<>value[i] THEN RETURN;END_IF;END_FOR;cursor:=cursor+size;MatchLiteral:=TRUE;`, "BOOL"),
|
|
928
|
+
method("ReadHeaders", `VAR lineStart:UINT;lineEnd:UINT;i:UINT;j:UINT;key:STRING(64);octet:BYTE;found:BOOL;digits:UINT;END_VAR`, `malformed:=FALSE;found:=FALSE;contentLength:=-1;
|
|
929
|
+
IF LEFT(received,9)<>'HTTP/1.1 ' AND LEFT(received,9)<>'HTTP/1.0 ' THEN malformed:=TRUE;RETURN;END_IF;
|
|
930
|
+
status:=0;FOR i:=9 TO 11 DO IF received[i]<48 OR received[i]>57 THEN malformed:=TRUE;RETURN;END_IF;status:=status*10+TO_UINT(received[i]-48);END_FOR;
|
|
931
|
+
lineStart:=1;
|
|
932
|
+
WHILE lineStart<headerEnd DO
|
|
933
|
+
lineEnd:=lineStart;WHILE lineEnd<headerEnd AND received[lineEnd-1]<>13 DO lineEnd:=lineEnd+1;END_WHILE;
|
|
934
|
+
key:='';j:=0;i:=lineStart;
|
|
935
|
+
WHILE i<lineEnd AND received[i-1]<>58 AND j<63 DO octet:=received[i-1];IF octet>=65 AND octet<=90 THEN octet:=octet+32;END_IF;key[j]:=octet;j:=j+1;i:=i+1;END_WHILE;key[j]:=0;
|
|
936
|
+
IF key='transfer-encoding' THEN malformed:=TRUE;RETURN;END_IF;
|
|
937
|
+
IF key='content-length' THEN
|
|
938
|
+
IF found THEN malformed:=TRUE;RETURN;END_IF;found:=TRUE;contentLength:=0;digits:=0;i:=i+1;
|
|
939
|
+
WHILE i<lineEnd AND received[i-1]=32 DO i:=i+1;END_WHILE;
|
|
940
|
+
WHILE i<lineEnd DO octet:=received[i-1];IF octet<48 OR octet>57 OR digits>=5 THEN malformed:=TRUE;RETURN;END_IF;contentLength:=contentLength*10+TO_DINT(octet-48);digits:=digits+1;i:=i+1;END_WHILE;
|
|
941
|
+
IF digits=0 THEN malformed:=TRUE;RETURN;END_IF;
|
|
942
|
+
END_IF;
|
|
943
|
+
lineStart:=lineEnd+2;
|
|
944
|
+
END_WHILE;`),
|
|
945
|
+
method("WriteString", `VAR_INPUT value : STRING(2048); END_VAR
|
|
946
|
+
VAR i:UINT; one:STRING(1); hex:STRING(16):='0123456789abcdef'; END_VAR`, `Append('"');IF ByteLength(value)>0 THEN FOR i:=0 TO ByteLength(value)-1 DO
|
|
947
|
+
one[0]:=value[i];one[1]:=0;
|
|
948
|
+
IF value[i]=34 OR value[i]=92 THEN Append('\\');Append(one);
|
|
949
|
+
ELSIF value[i]<32 THEN Append('\\u00');one[0]:=hex[SHR(value[i],4)];Append(one);one[0]:=hex[value[i] AND 15];Append(one);
|
|
950
|
+
ELSE Append(one);END_IF;
|
|
951
|
+
END_FOR;END_IF;Append('"');`),
|
|
952
|
+
method("SkipWhitespace", "", `WHILE cursor<=TO_UINT(ByteLength(payload)) DO IF payload[cursor-1]<>32 AND payload[cursor-1]<>9 AND payload[cursor-1]<>10 AND payload[cursor-1]<>13 THEN EXIT;END_IF;cursor:=cursor+1;END_WHILE;`),
|
|
953
|
+
method("Expect", "VAR_INPUT value:BYTE; END_VAR", `IF cursor>TO_UINT(ByteLength(payload)) OR_ELSE payload[cursor-1]<>value THEN malformed:=TRUE;ELSE cursor:=cursor+1;END_IF;`),
|
|
954
|
+
method("TokenByte", "VAR_INPUT value:BYTE;END_VAR", `IF tokenUsed>=2048 OR value=0 THEN malformed:=TRUE;RETURN;END_IF;token[tokenUsed]:=value;tokenUsed:=tokenUsed+1;token[tokenUsed]:=0;`),
|
|
955
|
+
method("ReadHex", "VAR value:UDINT;i:UINT;octet:BYTE;END_VAR", `numberValue:=0;FOR i:=1 TO 4 DO
|
|
956
|
+
IF cursor>TO_UINT(ByteLength(payload)) THEN malformed:=TRUE;RETURN;END_IF;
|
|
957
|
+
octet:=payload[cursor-1];cursor:=cursor+1;
|
|
958
|
+
IF octet>=48 AND octet<=57 THEN value:=TO_UDINT(octet-48);ELSIF octet>=65 AND octet<=70 THEN value:=TO_UDINT(octet-55);ELSIF octet>=97 AND octet<=102 THEN value:=TO_UDINT(octet-87);ELSE malformed:=TRUE;RETURN;END_IF;
|
|
959
|
+
numberValue:=numberValue*16+TO_LINT(value);END_FOR;`),
|
|
960
|
+
method("ReadString", `VAR octet:BYTE;point:UDINT;high:UDINT;END_VAR`, `token:='';tokenUsed:=0;Expect(34);
|
|
961
|
+
WHILE cursor<=TO_UINT(ByteLength(payload)) AND NOT malformed DO
|
|
962
|
+
octet:=payload[cursor-1];cursor:=cursor+1;
|
|
963
|
+
IF octet=34 THEN ValidateUtf8();RETURN;END_IF;
|
|
964
|
+
IF octet=92 THEN
|
|
965
|
+
IF cursor>TO_UINT(ByteLength(payload)) THEN malformed:=TRUE;RETURN;END_IF;
|
|
966
|
+
octet:=payload[cursor-1];cursor:=cursor+1;
|
|
967
|
+
CASE octet OF
|
|
968
|
+
34,47,92: TokenByte(octet);
|
|
969
|
+
98:TokenByte(8);102:TokenByte(12);110:TokenByte(10);114:TokenByte(13);116:TokenByte(9);
|
|
970
|
+
117:
|
|
971
|
+
ReadHex();point:=TO_UDINT(numberValue);
|
|
972
|
+
IF point>=55296 AND point<=56319 THEN
|
|
973
|
+
high:=point;Expect(92);Expect(117);ReadHex();
|
|
974
|
+
IF numberValue<56320 OR numberValue>57343 THEN malformed:=TRUE;RETURN;END_IF;
|
|
975
|
+
point:=65536+(high-55296)*1024+TO_UDINT(numberValue)-56320;
|
|
976
|
+
ELSIF point>=56320 AND point<=57343 THEN malformed:=TRUE;RETURN;END_IF;
|
|
977
|
+
IF point<128 THEN TokenByte(TO_BYTE(point));
|
|
978
|
+
ELSIF point<2048 THEN TokenByte(TO_BYTE(192+SHR(point,6)));TokenByte(TO_BYTE(128+(point AND 63)));
|
|
979
|
+
ELSIF point<65536 THEN TokenByte(TO_BYTE(224+SHR(point,12)));TokenByte(TO_BYTE(128+(SHR(point,6) AND 63)));TokenByte(TO_BYTE(128+(point AND 63)));
|
|
980
|
+
ELSE TokenByte(TO_BYTE(240+SHR(point,18)));TokenByte(TO_BYTE(128+(SHR(point,12) AND 63)));TokenByte(TO_BYTE(128+(SHR(point,6) AND 63)));TokenByte(TO_BYTE(128+(point AND 63)));END_IF;
|
|
981
|
+
ELSE malformed:=TRUE;RETURN;
|
|
982
|
+
END_CASE;
|
|
983
|
+
ELSIF octet<32 THEN malformed:=TRUE;RETURN;ELSE TokenByte(octet);END_IF;
|
|
984
|
+
END_WHILE;malformed:=TRUE;`),
|
|
985
|
+
method("ValidateUtf8", "VAR i:UINT;j:UINT;count:UINT;first:BYTE;next:BYTE;END_VAR", `i:=0;
|
|
986
|
+
WHILE i<tokenUsed AND NOT malformed DO
|
|
987
|
+
first:=token[i];
|
|
988
|
+
IF first<128 THEN i:=i+1;
|
|
989
|
+
ELSE
|
|
990
|
+
IF first>=194 AND first<=223 THEN count:=1;ELSIF first>=224 AND first<=239 THEN count:=2;ELSIF first>=240 AND first<=244 THEN count:=3;ELSE malformed:=TRUE;RETURN;END_IF;
|
|
991
|
+
IF i+count>=tokenUsed THEN malformed:=TRUE;RETURN;END_IF;
|
|
992
|
+
next:=token[i+1];
|
|
993
|
+
IF (first=224 AND next<160) OR (first=237 AND next>=160) OR (first=240 AND next<144) OR (first=244 AND next>=144) THEN malformed:=TRUE;RETURN;END_IF;
|
|
994
|
+
FOR j:=1 TO count DO IF token[i+j]<128 OR token[i+j]>191 THEN malformed:=TRUE;RETURN;END_IF;END_FOR;
|
|
995
|
+
i:=i+count+1;
|
|
996
|
+
END_IF;
|
|
997
|
+
END_WHILE;`),
|
|
998
|
+
method("ReadInteger", "VAR negative:BOOL;digits:UINT;octet:BYTE;END_VAR", `numberValue:=0;negative:=FALSE;digits:=0;
|
|
999
|
+
IF payload[cursor-1]=45 THEN negative:=TRUE;cursor:=cursor+1;END_IF;
|
|
1000
|
+
WHILE cursor<=TO_UINT(ByteLength(payload)) DO
|
|
1001
|
+
octet:=payload[cursor-1];IF octet<48 OR octet>57 THEN EXIT;END_IF;
|
|
1002
|
+
IF digits>=10 THEN malformed:=TRUE;RETURN;END_IF;
|
|
1003
|
+
IF digits>0 AND numberValue=0 THEN malformed:=TRUE;RETURN;END_IF;numberValue:=numberValue*10+TO_LINT(octet-48);digits:=digits+1;cursor:=cursor+1;
|
|
1004
|
+
END_WHILE;
|
|
1005
|
+
IF digits=0 THEN malformed:=TRUE;END_IF;IF negative THEN numberValue:=-numberValue;END_IF;`)
|
|
1006
|
+
].filter((item) => transport === "http" || !["AppendWire", "FindHeaders", "ReadHeaders"].includes(item.name));
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
// packages/compiler/backends/codesys/task-access.ts
|
|
1010
|
+
import { instancedBlock as instancedBlock2 } from "@tsplc/compiler/ir";
|
|
1011
|
+
function accessedStorage(fn, core, scheduledHost) {
|
|
1012
|
+
const scheduledCall = fn.body.find((item) => item.kind === "invoke" && item.instance === scheduledHost);
|
|
1013
|
+
const result = new Set, seen = new Set;
|
|
1014
|
+
const functions = new Map(core.functions.map((item) => [item.id, item]));
|
|
1015
|
+
const blocks = new Map(core.blocks.map((item) => [item.id, item]));
|
|
1016
|
+
const types = new Map(core.types.map((item) => [item.id, item]));
|
|
1017
|
+
const variables = new Map([
|
|
1018
|
+
...core.variables,
|
|
1019
|
+
...core.functions.flatMap((item) => [...item.parameters, ...item.locals]),
|
|
1020
|
+
...core.blocks.flatMap((item) => [
|
|
1021
|
+
...item.inputs,
|
|
1022
|
+
...item.outputs,
|
|
1023
|
+
...item.state,
|
|
1024
|
+
...item.locals
|
|
1025
|
+
])
|
|
1026
|
+
].map((item) => [item.id, item]));
|
|
1027
|
+
const path = (segments = []) => {
|
|
1028
|
+
segments.forEach((segment) => {
|
|
1029
|
+
if (segment.kind === "index")
|
|
1030
|
+
expression(segment.value);
|
|
1031
|
+
});
|
|
1032
|
+
};
|
|
1033
|
+
const expression = (value) => {
|
|
1034
|
+
if (value.kind === "load") {
|
|
1035
|
+
result.add(value.root);
|
|
1036
|
+
path(value.path);
|
|
1037
|
+
}
|
|
1038
|
+
if (value.kind === "borrow") {
|
|
1039
|
+
result.add(value.place.root);
|
|
1040
|
+
path(value.place.path);
|
|
1041
|
+
}
|
|
1042
|
+
if (value.kind === "unary" || value.kind === "convert")
|
|
1043
|
+
expression(value.operand);
|
|
1044
|
+
if (value.kind === "binary") {
|
|
1045
|
+
expression(value.left);
|
|
1046
|
+
expression(value.right);
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
const body = (items) => {
|
|
1050
|
+
for (const item of items) {
|
|
1051
|
+
if (item.kind === "assign") {
|
|
1052
|
+
result.add(item.root);
|
|
1053
|
+
path(item.path);
|
|
1054
|
+
expression(item.value);
|
|
1055
|
+
}
|
|
1056
|
+
if (item.kind === "if") {
|
|
1057
|
+
expression(item.condition);
|
|
1058
|
+
body(item.then);
|
|
1059
|
+
body(item.else);
|
|
1060
|
+
}
|
|
1061
|
+
if (item.kind === "switch") {
|
|
1062
|
+
expression(item.value);
|
|
1063
|
+
for (const branch of item.cases)
|
|
1064
|
+
body(branch.body);
|
|
1065
|
+
body(item.fallback);
|
|
1066
|
+
}
|
|
1067
|
+
if (item.kind === "for") {
|
|
1068
|
+
if (item.dynamic) {
|
|
1069
|
+
expression(item.dynamic.start);
|
|
1070
|
+
expression(item.dynamic.limit);
|
|
1071
|
+
}
|
|
1072
|
+
result.add(item.counter);
|
|
1073
|
+
body(item.body);
|
|
1074
|
+
}
|
|
1075
|
+
if (item.kind === "return" && item.value)
|
|
1076
|
+
expression(item.value);
|
|
1077
|
+
if (item.kind === "invoke") {
|
|
1078
|
+
if (item.instance && item !== scheduledCall)
|
|
1079
|
+
result.add(item.instance);
|
|
1080
|
+
if (item.index)
|
|
1081
|
+
expression(item.index);
|
|
1082
|
+
item.arguments.forEach(expression);
|
|
1083
|
+
if (item.destination)
|
|
1084
|
+
result.add(item.destination);
|
|
1085
|
+
const called = functions.get(item.function);
|
|
1086
|
+
if (called && !seen.has(called.id)) {
|
|
1087
|
+
seen.add(called.id);
|
|
1088
|
+
body(called.body);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (item.kind === "blockUpdate") {
|
|
1092
|
+
result.add(item.instance);
|
|
1093
|
+
if (item.index)
|
|
1094
|
+
expression(item.index);
|
|
1095
|
+
Object.values(item.inputs).forEach(expression);
|
|
1096
|
+
const variable = variables.get(item.instance);
|
|
1097
|
+
if (!variable)
|
|
1098
|
+
throw new Error("Missing verified instance during task planning.");
|
|
1099
|
+
const type = instancedBlock2(variable.type, types);
|
|
1100
|
+
const block = type && blocks.get(type.block);
|
|
1101
|
+
if (block && !seen.has(block.id)) {
|
|
1102
|
+
seen.add(block.id);
|
|
1103
|
+
body(block.body);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
seen.add(fn.id);
|
|
1109
|
+
body(fn.body);
|
|
1110
|
+
return result;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// packages/compiler/backends/codesys/publication.ts
|
|
1114
|
+
var globals = (core) => core.variables.filter((variable) => variable.storage === "module" && variable.exported);
|
|
1115
|
+
var publicationSelectors = (core) => {
|
|
1116
|
+
const variables = globals(core);
|
|
1117
|
+
return [
|
|
1118
|
+
...new Set(variables.flatMap((variable) => [
|
|
1119
|
+
moduleName(variable),
|
|
1120
|
+
`${moduleName(variable)}.${variable.name}`
|
|
1121
|
+
]))
|
|
1122
|
+
].sort();
|
|
1123
|
+
};
|
|
1124
|
+
var resolvePublication = (core, policy) => {
|
|
1125
|
+
const variables = globals(core);
|
|
1126
|
+
const errors = [];
|
|
1127
|
+
const select = (selectors) => {
|
|
1128
|
+
const selected = new Set(variables.filter((variable) => variable.exposure).map((variable) => variable.id));
|
|
1129
|
+
const seen = new Set;
|
|
1130
|
+
for (const selector of selectors) {
|
|
1131
|
+
if (seen.has(selector))
|
|
1132
|
+
errors.push(`Duplicate publication selector ${selector}.`);
|
|
1133
|
+
seen.add(selector);
|
|
1134
|
+
const candidates = variables.filter((variable) => selector === moduleName(variable) || selector === `${moduleName(variable)}.${variable.name}`);
|
|
1135
|
+
if (!candidates.length) {
|
|
1136
|
+
errors.push(`Unknown publication selector ${selector}; select an exported mutable global or its module.`);
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
if (new Set(candidates.map((variable) => variable.span.file)).size !== 1) {
|
|
1140
|
+
errors.push(`Ambiguous publication selector ${selector}.`);
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
for (const variable of candidates)
|
|
1144
|
+
selected.add(variable.id);
|
|
1145
|
+
}
|
|
1146
|
+
return variables.filter((variable) => selected.has(variable.id)).map((variable) => variable.id);
|
|
1147
|
+
};
|
|
1148
|
+
const publication = {
|
|
1149
|
+
production: select(policy.production),
|
|
1150
|
+
development: select(policy.development)
|
|
1151
|
+
};
|
|
1152
|
+
return { publication, errors };
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
// packages/compiler/backends/codesys/plan.ts
|
|
1156
|
+
var planned = Symbol("CODESYS plan");
|
|
1157
|
+
var plans = new WeakSet;
|
|
1158
|
+
function diagnostic(message, span) {
|
|
1159
|
+
return { origin: "backend", code: "PLC7001", severity: "error", message, ...span && { span } };
|
|
1160
|
+
}
|
|
1161
|
+
function isCodesysPlan(plan) {
|
|
1162
|
+
return plans.has(plan);
|
|
1163
|
+
}
|
|
1164
|
+
function planCodesys(core, deployment, value) {
|
|
1165
|
+
if (!isVerifiedCore(core))
|
|
1166
|
+
return { diagnostics: [diagnostic("CODESYS requires a verified, normalized core module.")] };
|
|
1167
|
+
const parsed = optionsSchema.safeParse(value);
|
|
1168
|
+
if (!parsed.success)
|
|
1169
|
+
return {
|
|
1170
|
+
diagnostics: parsed.error.issues.map((issue) => diagnostic(`${issue.path.join(".")}: ${issue.message}`))
|
|
1171
|
+
};
|
|
1172
|
+
const backgroundTasks = new Map;
|
|
1173
|
+
for (const program of core.programs) {
|
|
1174
|
+
if (!program.background)
|
|
1175
|
+
continue;
|
|
1176
|
+
const { priority, intervalNanoseconds } = program.background;
|
|
1177
|
+
const name = `TsplcBackground_${priority}_${BigInt(intervalNanoseconds) / 1000000n}`;
|
|
1178
|
+
const existing = backgroundTasks.get(name);
|
|
1179
|
+
backgroundTasks.set(name, {
|
|
1180
|
+
name,
|
|
1181
|
+
priority,
|
|
1182
|
+
intervalNanoseconds,
|
|
1183
|
+
programs: [...existing?.programs ?? [], program.name]
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
deployment = { tasks: [...deployment.tasks, ...backgroundTasks.values()] };
|
|
1187
|
+
const options = parsed.data;
|
|
1188
|
+
const diagnostics = [];
|
|
1189
|
+
const report = (message, span) => {
|
|
1190
|
+
diagnostics.push(diagnostic(message, span));
|
|
1191
|
+
};
|
|
1192
|
+
const callerTasks = deployment.tasks;
|
|
1193
|
+
for (const continuation of core.programs.filter((program) => program.continuation)) {
|
|
1194
|
+
const scope = continuation.continuation;
|
|
1195
|
+
const root = core.variables.find((variable) => variable.name === scope.instance && variable.span.file === scope.file);
|
|
1196
|
+
if (!root) {
|
|
1197
|
+
report(`Missing continuation owner ${scope.instance}.`);
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
const owners = callerTasks.filter((task) => task.programs.some((name) => {
|
|
1201
|
+
const program = core.programs.find((program) => program.name === name);
|
|
1202
|
+
if (!program || program.background)
|
|
1203
|
+
return false;
|
|
1204
|
+
const fn = core.functions.find((fn) => fn.id === program.entry);
|
|
1205
|
+
return fn && accessedStorage(fn, core).has(root.id);
|
|
1206
|
+
}));
|
|
1207
|
+
if (owners.length !== 1) {
|
|
1208
|
+
report(`Continuation owner ${scope.instance} must belong to exactly one caller task.`, root.span);
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
deployment = {
|
|
1212
|
+
tasks: deployment.tasks.map((task) => task.name === owners[0].name ? { ...task, programs: [...task.programs, continuation.name] } : task)
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
const http = new Map;
|
|
1216
|
+
for (const block of core.blocks)
|
|
1217
|
+
if (block.runtime?.kind === "http" || block.runtime?.kind === "rpc") {
|
|
1218
|
+
try {
|
|
1219
|
+
validateHttpLayout(block, new Map(core.types.map((type) => [type.id, type])));
|
|
1220
|
+
http.set(block.id, block.runtime.kind === "rpc" ? {
|
|
1221
|
+
transport: "opcua",
|
|
1222
|
+
host: "",
|
|
1223
|
+
port: 0,
|
|
1224
|
+
authority: "",
|
|
1225
|
+
path: block.runtime.path,
|
|
1226
|
+
token: "",
|
|
1227
|
+
timeoutMs: options.http.timeoutMs,
|
|
1228
|
+
tls: false
|
|
1229
|
+
} : resolveHttpEndpoint(block.runtime.url, options.http));
|
|
1230
|
+
} catch (cause) {
|
|
1231
|
+
report(cause instanceof Error ? cause.message : String(cause), block.span);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
const storageBindings = validateIo(core, options, report);
|
|
1235
|
+
validateTasks(core, deployment, report);
|
|
1236
|
+
validateStorage(core, options, report);
|
|
1237
|
+
validateConstants(core, report);
|
|
1238
|
+
const { publication, errors } = resolvePublication(core, options.publish);
|
|
1239
|
+
for (const error of errors)
|
|
1240
|
+
report(error);
|
|
1241
|
+
if (diagnostics.length)
|
|
1242
|
+
return { diagnostics };
|
|
1243
|
+
const plan = Object.freeze({
|
|
1244
|
+
core,
|
|
1245
|
+
deployment: structuredClone(deployment),
|
|
1246
|
+
options: structuredClone(options),
|
|
1247
|
+
storageBindings,
|
|
1248
|
+
publication,
|
|
1249
|
+
http: Object.fromEntries(http),
|
|
1250
|
+
[planned]: true
|
|
1251
|
+
});
|
|
1252
|
+
const freeze = (value) => {
|
|
1253
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value))
|
|
1254
|
+
return;
|
|
1255
|
+
for (const child of Object.values(value))
|
|
1256
|
+
freeze(child);
|
|
1257
|
+
Object.freeze(value);
|
|
1258
|
+
};
|
|
1259
|
+
freeze(plan.deployment);
|
|
1260
|
+
freeze(plan.options);
|
|
1261
|
+
freeze(plan.storageBindings);
|
|
1262
|
+
freeze(plan.publication);
|
|
1263
|
+
plans.add(plan);
|
|
1264
|
+
return { plan, diagnostics: [] };
|
|
1265
|
+
}
|
|
1266
|
+
function validateIo(core, options, report) {
|
|
1267
|
+
const types = new Map(core.types.map((type) => [type.id, type]));
|
|
1268
|
+
const targets = [];
|
|
1269
|
+
const names = new Set;
|
|
1270
|
+
for (const item of core.variables.filter((item) => item.storage === "input" || item.storage === "output")) {
|
|
1271
|
+
if (names.has(item.name))
|
|
1272
|
+
report(`Ambiguous I/O name ${item.name}; logical I/O names must be unique.`, item.span);
|
|
1273
|
+
names.add(item.name);
|
|
1274
|
+
const binding = Object.hasOwn(options.io, item.name) ? options.io[item.name] : undefined;
|
|
1275
|
+
if (!binding) {
|
|
1276
|
+
report(`Missing I/O binding for ${item.name}.`, item.span);
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
targets.push({
|
|
1280
|
+
name: item.name,
|
|
1281
|
+
binding,
|
|
1282
|
+
type: types.get(item.type),
|
|
1283
|
+
region: item.storage === "input" ? "I" : "Q",
|
|
1284
|
+
span: item.span
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
const scheduled = new Set;
|
|
1288
|
+
for (const program of core.programs) {
|
|
1289
|
+
const fn = core.functions.find((item) => item.id === program.entry);
|
|
1290
|
+
for (const id of accessedStorage(fn, core))
|
|
1291
|
+
scheduled.add(id);
|
|
1292
|
+
}
|
|
1293
|
+
const storageBindings = [];
|
|
1294
|
+
for (const [name, binding] of Object.entries(options.io)) {
|
|
1295
|
+
if (names.has(name))
|
|
1296
|
+
continue;
|
|
1297
|
+
if (!isStoragePath(name)) {
|
|
1298
|
+
report(`Unknown I/O binding ${name}.`);
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
const resolved = resolveStoragePath(core, name);
|
|
1302
|
+
if ("error" in resolved) {
|
|
1303
|
+
report(resolved.error);
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
const overlapping = storageBindings.find((other) => nested(other, resolved));
|
|
1307
|
+
if (overlapping)
|
|
1308
|
+
report(`${name} overlaps the binding ${overlapping.key}.`);
|
|
1309
|
+
if (!scheduled.has(resolved.root.id))
|
|
1310
|
+
report(`${name}: no scheduled program uses ${resolved.root.name}, so it would never be copied.`);
|
|
1311
|
+
storageBindings.push(resolved);
|
|
1312
|
+
targets.push({
|
|
1313
|
+
name,
|
|
1314
|
+
binding,
|
|
1315
|
+
type: resolved.type,
|
|
1316
|
+
region: binding.address[1] === "I" ? "I" : "Q"
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
const addresses = [];
|
|
1320
|
+
for (const { name, binding, type, region, span } of targets) {
|
|
1321
|
+
const array = type.kind === "array" ? type : undefined;
|
|
1322
|
+
const element = array ? types.get(array.element) : type;
|
|
1323
|
+
const layout = ioLayout2(iecSpelling(type, types) ?? "");
|
|
1324
|
+
if (binding.type && (!layout || ioLayout2(binding.type) !== layout))
|
|
1325
|
+
report(`I/O type mismatch for ${name}: ${type.id} cannot bind to ${binding.type}.`, span);
|
|
1326
|
+
if (!binding.address.startsWith(`%${region}`))
|
|
1327
|
+
report(`Wrong address region for ${name}: expected %${region}X.`, span);
|
|
1328
|
+
const match = /^%([IQ])([XBWD])([0-9]+)(?:\.([0-7]))?$/.exec(binding.address);
|
|
1329
|
+
if (!match)
|
|
1330
|
+
continue;
|
|
1331
|
+
if (region === "I" && element?.kind === "float")
|
|
1332
|
+
report("Floating logical inputs require an explicit runtime nonfinite policy, which is not supported yet.", span);
|
|
1333
|
+
if (array && element?.kind === "bool")
|
|
1334
|
+
report(`BOOL arrays are not bit-packed in CODESYS I/O; bind each element of ${name} separately.`, span);
|
|
1335
|
+
const size = match[2] === "X" ? 1 : match[2] === "B" ? 8 : match[2] === "W" ? 16 : 32;
|
|
1336
|
+
if (ioBits(element) !== size)
|
|
1337
|
+
report(`Address size does not match ${name}:${type.id}.`, span);
|
|
1338
|
+
const unit = size === 1 ? 8n : BigInt(size);
|
|
1339
|
+
const offset = BigInt(match[3]);
|
|
1340
|
+
const start = offset * unit + BigInt(match[4] ?? 0), end = start + BigInt(size) * BigInt(array?.length ?? 1);
|
|
1341
|
+
if (binding.inverted && (region !== "I" || element?.kind !== "bool" || array))
|
|
1342
|
+
report(`Only a BOOL input can be inverted: ${name}.`, span);
|
|
1343
|
+
if (binding.shared && region !== "I")
|
|
1344
|
+
report(`Only an input can be shared: ${name}.`, span);
|
|
1345
|
+
const collision = addresses.find((address) => address.region === match[1] && address.start < end && start < address.end && !(binding.shared && address.shared && address.start === start && address.end === end));
|
|
1346
|
+
if (collision)
|
|
1347
|
+
report(`Overlapping I/O address ${binding.address} with ${collision.name}.`, span);
|
|
1348
|
+
addresses.push({ region: match[1], start, end, name, shared: binding.shared });
|
|
1349
|
+
if (offset > 0x7fffffffn)
|
|
1350
|
+
report(`I/O offset is too large: ${binding.address}.`, span);
|
|
1351
|
+
}
|
|
1352
|
+
return storageBindings;
|
|
1353
|
+
}
|
|
1354
|
+
function nested(left, right) {
|
|
1355
|
+
return left.root.id === right.root.id && left.segments.every((a, index) => {
|
|
1356
|
+
const b = right.segments[index];
|
|
1357
|
+
if (!b)
|
|
1358
|
+
return true;
|
|
1359
|
+
return a.kind === "field" ? b.kind === "field" && a.name === b.name : b.kind === "index" && a.value === b.value;
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
function ioBits(type) {
|
|
1363
|
+
if (type?.kind === "bool")
|
|
1364
|
+
return 1;
|
|
1365
|
+
return type && "bits" in type ? type.bits : 0;
|
|
1366
|
+
}
|
|
1367
|
+
function validateTasks(core, deployment, report) {
|
|
1368
|
+
const programs = new Map(core.programs.map((program) => [program.name, program]));
|
|
1369
|
+
const assignment = new Map;
|
|
1370
|
+
const taskNames = new Set;
|
|
1371
|
+
const stateOwners = new Map;
|
|
1372
|
+
const globals = new Map(core.variables.map((item) => [item.id, item]));
|
|
1373
|
+
const functions = new Map(core.functions.map((item) => [item.id, item]));
|
|
1374
|
+
const blockRuntimes = new Map(core.types.flatMap((type) => type.kind === "block" ? [[type.id, core.blocks.find((block) => block.id === type.block)?.runtime?.kind]] : []));
|
|
1375
|
+
for (const task of deployment.tasks) {
|
|
1376
|
+
if (!isIecIdentifier(task.name))
|
|
1377
|
+
report(`Invalid task name ${task.name}.`);
|
|
1378
|
+
if (taskNames.has(task.name.toUpperCase()))
|
|
1379
|
+
report(`Duplicate task name ${task.name}.`);
|
|
1380
|
+
taskNames.add(task.name.toUpperCase());
|
|
1381
|
+
if (!Number.isInteger(task.priority) || task.priority < 0 || task.priority > 31)
|
|
1382
|
+
report(`Task ${task.name} priority must be 0..31.`);
|
|
1383
|
+
if (!/^[1-9][0-9]*$/.test(task.intervalNanoseconds))
|
|
1384
|
+
report(`Invalid task interval for ${task.name}.`);
|
|
1385
|
+
else {
|
|
1386
|
+
const ns = BigInt(task.intervalNanoseconds);
|
|
1387
|
+
if (ns % 1000000n || ns > 4294967295000000n)
|
|
1388
|
+
report(`Task ${task.name} requires an exact positive TIME interval in milliseconds.`);
|
|
1389
|
+
}
|
|
1390
|
+
if (!task.programs.length)
|
|
1391
|
+
report(`Task ${task.name} has no programs.`);
|
|
1392
|
+
for (const name of task.programs) {
|
|
1393
|
+
const program = programs.get(name);
|
|
1394
|
+
if (!program) {
|
|
1395
|
+
report(`Unknown task program ${name}.`);
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
if (assignment.has(name))
|
|
1399
|
+
report(`Program ${name} must belong to exactly one task.`);
|
|
1400
|
+
assignment.set(name, task.name);
|
|
1401
|
+
const fn = functions.get(program.entry);
|
|
1402
|
+
if (!fn)
|
|
1403
|
+
throw new Error("Verified core has a missing entry.");
|
|
1404
|
+
const host = program.background && core.variables.find((item) => item.name === program.background?.worker && item.span.file === fn.span.file && blockRuntimes.get(item.type) === "backgroundHost");
|
|
1405
|
+
for (const id of accessedStorage(fn, core, host ? host.id : undefined)) {
|
|
1406
|
+
const item = globals.get(id);
|
|
1407
|
+
if (!item || item.storage === "constant")
|
|
1408
|
+
continue;
|
|
1409
|
+
if (blockRuntimes.get(item.type) === "mailbox")
|
|
1410
|
+
continue;
|
|
1411
|
+
const ownsWorker = program.background?.worker === item.name && item.span.file === fn.span.file;
|
|
1412
|
+
if (program.background && (!ownsWorker || item.id === host?.id))
|
|
1413
|
+
report(`Background task ${program.name} accesses scan-owned storage ${item.name}; pass value snapshots through the mailbox.`, item.span);
|
|
1414
|
+
const owner = stateOwners.get(id);
|
|
1415
|
+
if (owner && owner !== task.name)
|
|
1416
|
+
report(`Storage ${item.name} is shared across cyclic tasks ${owner} and ${task.name}.`, item.span);
|
|
1417
|
+
stateOwners.set(id, task.name);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
for (const name of programs.keys())
|
|
1422
|
+
if (!assignment.has(name))
|
|
1423
|
+
report(`Program ${name} has no cyclic task.`);
|
|
1424
|
+
}
|
|
1425
|
+
function validateStorage(core, options, report) {
|
|
1426
|
+
const programs = new Map(core.programs.map((program) => [program.name, program]));
|
|
1427
|
+
if (core.blocks.some((block) => block.standard === "Blinker")) {
|
|
1428
|
+
for (const target of Object.values(options.programNames ?? {})) {
|
|
1429
|
+
if (target.toUpperCase() === "UTIL")
|
|
1430
|
+
report(`IEC object name ${target} shadows the Util library namespace required by Blinker.`);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
const explicitNames = new Set;
|
|
1434
|
+
for (const [name, target] of Object.entries(options.programNames ?? {})) {
|
|
1435
|
+
if (!programs.has(name))
|
|
1436
|
+
report(`Unknown program name override ${name}.`);
|
|
1437
|
+
if (explicitNames.has(target.toUpperCase()))
|
|
1438
|
+
report(`Duplicate IEC program name ${target}.`);
|
|
1439
|
+
explicitNames.add(target.toUpperCase());
|
|
1440
|
+
}
|
|
1441
|
+
const types = new Map(core.types.map((type) => [type.id, type]));
|
|
1442
|
+
const blocks = new Map(core.blocks.map((block) => [block.id, block]));
|
|
1443
|
+
const blockHasInOut = (id) => {
|
|
1444
|
+
const block = blocks.get(id);
|
|
1445
|
+
return block.inputs.some((input) => input.mode === "inOut") || block.state.some((field) => {
|
|
1446
|
+
const type = instancedBlock3(field.type, types);
|
|
1447
|
+
return type !== undefined && blockHasInOut(type.block);
|
|
1448
|
+
});
|
|
1449
|
+
};
|
|
1450
|
+
const layout = storageLayout(core);
|
|
1451
|
+
for (const block of core.blocks)
|
|
1452
|
+
if (layout.retainedBlocks.has(block.id) && blockHasInOut(block.id))
|
|
1453
|
+
report("Retained function blocks cannot contain direct or nested InOut references.", block.span);
|
|
1454
|
+
for (const variable of core.variables) {
|
|
1455
|
+
const type = instancedBlock3(variable.type, types);
|
|
1456
|
+
if (variable.retain && type && blockHasInOut(type.block))
|
|
1457
|
+
report("Retained function blocks cannot contain direct or nested InOut references.", variable.span);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
function validateConstants(core, report) {
|
|
1461
|
+
const types = new Map(core.types.map((type) => [type.id, type]));
|
|
1462
|
+
const constant = (value, id, span) => {
|
|
1463
|
+
const type = types.get(id);
|
|
1464
|
+
if (type.kind === "string" && value.kind === "string") {
|
|
1465
|
+
if (type.encoding === "utf8" && [...value.value].some((character) => character.charCodeAt(0) === 0 || character.charCodeAt(0) > 127))
|
|
1466
|
+
report("Non-ASCII STRING literals require a verified project-wide UTF8 setting; this profile supports ASCII literals.", span);
|
|
1467
|
+
if (type.encoding === "utf16" && /[\uD800-\uDFFF]/.test(value.value))
|
|
1468
|
+
report("CODESYS WSTRING uses UCS-2 and cannot represent surrogate code units.", span);
|
|
1469
|
+
}
|
|
1470
|
+
if (type.kind === "array" && value.kind === "array")
|
|
1471
|
+
for (const item of value.items)
|
|
1472
|
+
constant(item, type.element, span);
|
|
1473
|
+
if (type.kind === "record" && value.kind === "record")
|
|
1474
|
+
for (const field of type.fields)
|
|
1475
|
+
constant(value.fields[field.name], field.type, span);
|
|
1476
|
+
};
|
|
1477
|
+
const constantsInPath = (path = []) => {
|
|
1478
|
+
for (const segment of path)
|
|
1479
|
+
if (segment.kind === "index")
|
|
1480
|
+
constantsInExpression(segment.value);
|
|
1481
|
+
};
|
|
1482
|
+
const constantsInExpression = (value) => {
|
|
1483
|
+
if (value.kind === "constant")
|
|
1484
|
+
constant(value.value, value.type, value.span);
|
|
1485
|
+
if (value.kind === "load")
|
|
1486
|
+
constantsInPath(value.path);
|
|
1487
|
+
if (value.kind === "borrow")
|
|
1488
|
+
constantsInPath(value.place.path);
|
|
1489
|
+
if (value.kind === "unary" || value.kind === "convert")
|
|
1490
|
+
constantsInExpression(value.operand);
|
|
1491
|
+
if (value.kind === "binary") {
|
|
1492
|
+
constantsInExpression(value.left);
|
|
1493
|
+
constantsInExpression(value.right);
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
const constantsInBody = (items) => {
|
|
1497
|
+
for (const item of items) {
|
|
1498
|
+
if (item.kind === "assign") {
|
|
1499
|
+
constantsInExpression(item.value);
|
|
1500
|
+
constantsInPath(item.path);
|
|
1501
|
+
}
|
|
1502
|
+
if (item.kind === "if") {
|
|
1503
|
+
constantsInExpression(item.condition);
|
|
1504
|
+
constantsInBody(item.then);
|
|
1505
|
+
constantsInBody(item.else);
|
|
1506
|
+
}
|
|
1507
|
+
if (item.kind === "switch") {
|
|
1508
|
+
constantsInExpression(item.value);
|
|
1509
|
+
item.cases.forEach((branch) => {
|
|
1510
|
+
for (const value of branch.values)
|
|
1511
|
+
constant(value, item.value.type, item.span);
|
|
1512
|
+
constantsInBody(branch.body);
|
|
1513
|
+
});
|
|
1514
|
+
constantsInBody(item.fallback);
|
|
1515
|
+
}
|
|
1516
|
+
if (item.kind === "for")
|
|
1517
|
+
constantsInBody(item.body);
|
|
1518
|
+
if (item.kind === "invoke") {
|
|
1519
|
+
if (item.index)
|
|
1520
|
+
constantsInExpression(item.index);
|
|
1521
|
+
item.arguments.forEach(constantsInExpression);
|
|
1522
|
+
}
|
|
1523
|
+
if (item.kind === "blockUpdate") {
|
|
1524
|
+
if (item.index)
|
|
1525
|
+
constantsInExpression(item.index);
|
|
1526
|
+
Object.values(item.inputs).forEach(constantsInExpression);
|
|
1527
|
+
}
|
|
1528
|
+
if (item.kind === "return" && item.value)
|
|
1529
|
+
constantsInExpression(item.value);
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
core.variables.forEach((variable) => {
|
|
1533
|
+
if (variable.initial)
|
|
1534
|
+
constant(variable.initial, variable.type, variable.span);
|
|
1535
|
+
});
|
|
1536
|
+
for (const fn of core.functions)
|
|
1537
|
+
constantsInBody(fn.body);
|
|
1538
|
+
core.blocks.forEach((block) => {
|
|
1539
|
+
[...block.outputs, ...block.state, ...block.locals].forEach((variable) => {
|
|
1540
|
+
if (variable.initial)
|
|
1541
|
+
constant(variable.initial, variable.type, variable.span);
|
|
1542
|
+
});
|
|
1543
|
+
constantsInBody(block.body);
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// packages/compiler/backends/codesys/types.ts
|
|
1548
|
+
import { instancedBlock as instancedBlock4 } from "@tsplc/compiler/ir";
|
|
1549
|
+
class CodesysTypes {
|
|
1550
|
+
types;
|
|
1551
|
+
objects = [];
|
|
1552
|
+
names = new Map;
|
|
1553
|
+
fields = new Map;
|
|
1554
|
+
constructor(types, names, blockNames) {
|
|
1555
|
+
this.types = types;
|
|
1556
|
+
for (const type of types.values()) {
|
|
1557
|
+
if (type.kind === "block") {
|
|
1558
|
+
const name = blockNames.get(type.block);
|
|
1559
|
+
if (!name)
|
|
1560
|
+
throw new Error("Missing verified block name");
|
|
1561
|
+
this.names.set(type.id, name);
|
|
1562
|
+
}
|
|
1563
|
+
if (["record", "array", "enum", "string"].includes(type.kind)) {
|
|
1564
|
+
const label = type.id.split(":").at(-1);
|
|
1565
|
+
if (type.kind === "record" || type.kind === "enum") {
|
|
1566
|
+
const prefix = type.kind === "record" ? "ST" : "E";
|
|
1567
|
+
this.names.set(type.id, names.allocateObject(prefix, label));
|
|
1568
|
+
} else
|
|
1569
|
+
this.names.set(type.id, names.allocate(`T_${label}`));
|
|
1570
|
+
}
|
|
1571
|
+
if (type.kind === "record" || type.kind === "enum") {
|
|
1572
|
+
const names = new Names, fields = new Map;
|
|
1573
|
+
for (const member of type.kind === "record" ? type.fields : type.members) {
|
|
1574
|
+
fields.set(member.name, names.allocate(member.name, true));
|
|
1575
|
+
}
|
|
1576
|
+
this.fields.set(type.id, fields);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
for (const type of types.values())
|
|
1580
|
+
if (type.kind === "array" && instancedBlock4(type.id, types))
|
|
1581
|
+
this.names.set(type.id, `ARRAY[${type.lowerBound}..${type.lowerBound + type.length - 1}] OF ${this.name(type.element)}`);
|
|
1582
|
+
const emitted = new Set;
|
|
1583
|
+
const emit = (type) => {
|
|
1584
|
+
if (instancedBlock4(type.id, types) || !this.names.has(type.id) || emitted.has(type.id))
|
|
1585
|
+
return;
|
|
1586
|
+
if (type.kind === "array")
|
|
1587
|
+
emit(this.type(type.element));
|
|
1588
|
+
if (type.kind === "record") {
|
|
1589
|
+
for (const field of type.fields)
|
|
1590
|
+
emit(this.type(field.type));
|
|
1591
|
+
}
|
|
1592
|
+
emitted.add(type.id);
|
|
1593
|
+
const name = this.name(type.id);
|
|
1594
|
+
let declaration;
|
|
1595
|
+
if (type.kind === "record") {
|
|
1596
|
+
const fields = type.fields.map((field) => ` ${this.field(type.id, field.name)} : ${this.name(field.type)};`).join(`
|
|
1597
|
+
`);
|
|
1598
|
+
declaration = `TYPE ${name} :
|
|
1599
|
+
STRUCT
|
|
1600
|
+
${fields}
|
|
1601
|
+
END_STRUCT
|
|
1602
|
+
END_TYPE
|
|
1603
|
+
`;
|
|
1604
|
+
} else if (type.kind === "array") {
|
|
1605
|
+
declaration = `TYPE ${name} : ARRAY[${type.lowerBound}..${type.lowerBound + type.length - 1}] OF ${this.name(type.element)};
|
|
1606
|
+
END_TYPE
|
|
1607
|
+
`;
|
|
1608
|
+
} else if (type.kind === "string") {
|
|
1609
|
+
declaration = `TYPE ${name} : ${type.encoding === "utf8" ? "STRING" : "WSTRING"}(${type.capacity});
|
|
1610
|
+
END_TYPE
|
|
1611
|
+
`;
|
|
1612
|
+
} else if (type.kind === "enum") {
|
|
1613
|
+
const members = type.members.map((member) => ` ${this.field(type.id, member.name)} := ${member.value}`).join(`,
|
|
1614
|
+
`);
|
|
1615
|
+
declaration = `{attribute 'qualified_only'}
|
|
1616
|
+
{attribute 'strict'}
|
|
1617
|
+
TYPE ${name} : (
|
|
1618
|
+
${members}
|
|
1619
|
+
) DINT;
|
|
1620
|
+
END_TYPE
|
|
1621
|
+
`;
|
|
1622
|
+
} else
|
|
1623
|
+
throw new Error("Unsupported verified data type");
|
|
1624
|
+
this.objects.push({ id: type.id, kind: "dataType", name, declaration });
|
|
1625
|
+
};
|
|
1626
|
+
for (const type of types.values())
|
|
1627
|
+
emit(type);
|
|
1628
|
+
}
|
|
1629
|
+
type(id) {
|
|
1630
|
+
const type = this.types.get(id);
|
|
1631
|
+
if (!type)
|
|
1632
|
+
throw new Error("Missing verified type");
|
|
1633
|
+
return type;
|
|
1634
|
+
}
|
|
1635
|
+
name(id) {
|
|
1636
|
+
return this.names.get(id) ?? id;
|
|
1637
|
+
}
|
|
1638
|
+
field(id, name) {
|
|
1639
|
+
const field = this.fields.get(id)?.get(name);
|
|
1640
|
+
if (!field)
|
|
1641
|
+
throw new Error("Missing verified field");
|
|
1642
|
+
return field;
|
|
1643
|
+
}
|
|
1644
|
+
declarationInitializer(value, id) {
|
|
1645
|
+
const type = this.type(id);
|
|
1646
|
+
if ((type.kind === "record" || type.kind === "array") && this.isZero(value))
|
|
1647
|
+
return "";
|
|
1648
|
+
return ` := ${this.literal(value, id)}`;
|
|
1649
|
+
}
|
|
1650
|
+
isZero(value) {
|
|
1651
|
+
if (value.kind === "bool")
|
|
1652
|
+
return !value.value;
|
|
1653
|
+
if (value.kind === "integer")
|
|
1654
|
+
return value.value === "0";
|
|
1655
|
+
if (value.kind === "float")
|
|
1656
|
+
return Object.is(value.value, 0);
|
|
1657
|
+
if (value.kind === "string")
|
|
1658
|
+
return value.value === "";
|
|
1659
|
+
if (value.kind === "array")
|
|
1660
|
+
return value.items.every((item) => this.isZero(item));
|
|
1661
|
+
if (value.kind === "record")
|
|
1662
|
+
return Object.values(value.fields).every((item) => this.isZero(item));
|
|
1663
|
+
return false;
|
|
1664
|
+
}
|
|
1665
|
+
literal(value, id) {
|
|
1666
|
+
const type = this.type(id);
|
|
1667
|
+
if (type.kind === "enum" && value.kind === "integer") {
|
|
1668
|
+
const member = type.members.find((member) => member.value === value.value);
|
|
1669
|
+
if (!member)
|
|
1670
|
+
throw new Error("Missing verified enum member");
|
|
1671
|
+
return `${this.name(id)}.${this.field(id, member.name)}`;
|
|
1672
|
+
}
|
|
1673
|
+
if (type.kind === "string" && value.kind === "string") {
|
|
1674
|
+
const wide = type.encoding === "utf16", quote = wide ? '"' : "'";
|
|
1675
|
+
let text = "";
|
|
1676
|
+
for (const char of value.value) {
|
|
1677
|
+
const code = char.charCodeAt(0);
|
|
1678
|
+
if (char === "$")
|
|
1679
|
+
text += "$$";
|
|
1680
|
+
else if (char === quote)
|
|
1681
|
+
text += `$${char}`;
|
|
1682
|
+
else if (code < 32 || code === 127)
|
|
1683
|
+
text += `$${code.toString(16).toUpperCase().padStart(wide ? 4 : 2, "0")}`;
|
|
1684
|
+
else
|
|
1685
|
+
text += char;
|
|
1686
|
+
}
|
|
1687
|
+
return `${quote}${text}${quote}`;
|
|
1688
|
+
}
|
|
1689
|
+
if (type.kind === "array" && value.kind === "array") {
|
|
1690
|
+
return `[${value.items.map((item) => this.literal(item, type.element)).join(", ")}]`;
|
|
1691
|
+
}
|
|
1692
|
+
if (type.kind === "record" && value.kind === "record") {
|
|
1693
|
+
const fields = type.fields.map((field) => {
|
|
1694
|
+
const initial = value.fields[field.name];
|
|
1695
|
+
if (!initial)
|
|
1696
|
+
throw new Error("Missing verified record initializer");
|
|
1697
|
+
return `${this.field(id, field.name)} := ${this.literal(initial, field.type)}`;
|
|
1698
|
+
});
|
|
1699
|
+
return `(${fields.join(", ")})`;
|
|
1700
|
+
}
|
|
1701
|
+
if (type.kind === "duration" && value.kind === "duration") {
|
|
1702
|
+
return `T#${BigInt(value.nanoseconds) / 1000000n}ms`;
|
|
1703
|
+
}
|
|
1704
|
+
if (type.kind === "calendar" && value.kind === "calendar") {
|
|
1705
|
+
if (type.domain === "date")
|
|
1706
|
+
return `D#${value.value}`;
|
|
1707
|
+
if (type.domain === "timeOfDay")
|
|
1708
|
+
return `TOD#${value.value}`;
|
|
1709
|
+
return `DT#${value.value.replace("T", "-")}`;
|
|
1710
|
+
}
|
|
1711
|
+
return scalarLiteral(value, id);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// packages/compiler/backends/codesys/emit.ts
|
|
1716
|
+
function required(value) {
|
|
1717
|
+
if (value === undefined)
|
|
1718
|
+
throw new Error("Missing verified CODESYS symbol.");
|
|
1719
|
+
return value;
|
|
1720
|
+
}
|
|
1721
|
+
function emitCodesys(plan) {
|
|
1722
|
+
if (!isCodesysPlan(plan)) {
|
|
1723
|
+
return {
|
|
1724
|
+
artifacts: [],
|
|
1725
|
+
objects: [],
|
|
1726
|
+
mappings: [],
|
|
1727
|
+
ioSymbols: {},
|
|
1728
|
+
publication: { production: [], development: [] },
|
|
1729
|
+
ioMappings: [],
|
|
1730
|
+
diagnostics: [diagnostic("Emission requires a validated CODESYS plan.")]
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
return new CodesysEmitter(plan).emit();
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
class CodesysEmitter {
|
|
1737
|
+
plan;
|
|
1738
|
+
names = new Names;
|
|
1739
|
+
networkName;
|
|
1740
|
+
rpcChannels = [];
|
|
1741
|
+
references = new Map;
|
|
1742
|
+
channels = [];
|
|
1743
|
+
ioMappings = [];
|
|
1744
|
+
objects = [];
|
|
1745
|
+
mappings = [];
|
|
1746
|
+
mailboxLocks = new Map;
|
|
1747
|
+
blockFields = new Map;
|
|
1748
|
+
typeMap;
|
|
1749
|
+
blocks;
|
|
1750
|
+
blockNames;
|
|
1751
|
+
functionNames = new Map;
|
|
1752
|
+
programRoutines = new Set;
|
|
1753
|
+
programResults = new Map;
|
|
1754
|
+
routineScopes = new Map;
|
|
1755
|
+
targetTypes;
|
|
1756
|
+
numeric;
|
|
1757
|
+
layout;
|
|
1758
|
+
constructor(plan) {
|
|
1759
|
+
this.plan = plan;
|
|
1760
|
+
const { core, options } = plan;
|
|
1761
|
+
const persistent = new Set(core.variables.filter((item) => item.storage !== "constant").map((item) => item.id));
|
|
1762
|
+
for (const fn of core.functions) {
|
|
1763
|
+
if (!fn.owner && [...accessedStorage(fn, core)].some((id) => persistent.has(id)))
|
|
1764
|
+
this.programRoutines.add(fn.id);
|
|
1765
|
+
}
|
|
1766
|
+
for (const name of Object.values(options.programNames ?? {}))
|
|
1767
|
+
this.names.reserve(name);
|
|
1768
|
+
this.typeMap = new Map(core.types.map((type) => [type.id, type]));
|
|
1769
|
+
this.blocks = new Map(core.blocks.map((block) => [block.id, block]));
|
|
1770
|
+
this.blockNames = new Map(core.blocks.map((block) => [
|
|
1771
|
+
block.id,
|
|
1772
|
+
block.standard ? standardBlocks[block.standard].native : this.names.allocateObject("FB", block.name)
|
|
1773
|
+
]));
|
|
1774
|
+
for (const name of this.blockNames.values())
|
|
1775
|
+
this.names.reserve(name);
|
|
1776
|
+
this.targetTypes = new CodesysTypes(this.typeMap, this.names, this.blockNames);
|
|
1777
|
+
this.numeric = new NumericLowering(this.typeMap, (label) => this.names.allocate(label));
|
|
1778
|
+
this.layout = storageLayout(core);
|
|
1779
|
+
this.objects.push(...this.targetTypes.objects);
|
|
1780
|
+
}
|
|
1781
|
+
emit() {
|
|
1782
|
+
this.emitGlobals();
|
|
1783
|
+
this.emitIo();
|
|
1784
|
+
if (Object.values(this.plan.http).some((endpoint) => endpoint.transport !== "opcua")) {
|
|
1785
|
+
this.networkName = this.names.allocateObject("GVL", "TsplcNetwork");
|
|
1786
|
+
this.objects.push({
|
|
1787
|
+
id: "runtime:httpNetwork",
|
|
1788
|
+
kind: "globalList",
|
|
1789
|
+
name: this.networkName,
|
|
1790
|
+
declaration: `{attribute 'qualified_only'}
|
|
1791
|
+
{attribute 'global_init_slot' := '40000'}
|
|
1792
|
+
VAR_GLOBAL
|
|
1793
|
+
network:NBS.AsyncProperty := (usiTaskPrio:=15,udiTaskInterval:=1000,tnTaskName:='tsPLC HTTP');
|
|
1794
|
+
END_VAR
|
|
1795
|
+
`
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
const { core } = this.plan;
|
|
1799
|
+
const entries = new Set(core.programs.map((program) => program.entry));
|
|
1800
|
+
for (const fn of core.functions.filter((fn) => !fn.owner)) {
|
|
1801
|
+
const program = this.programRoutines.has(fn.id);
|
|
1802
|
+
const label = program && entries.has(fn.id) ? `${fn.name}_body` : fn.name;
|
|
1803
|
+
this.functionNames.set(fn.id, this.names.allocateObject(program ? "PRG" : "F", label));
|
|
1804
|
+
}
|
|
1805
|
+
for (const fn of core.functions)
|
|
1806
|
+
this.prepareStatements(fn.body);
|
|
1807
|
+
for (const block of core.blocks)
|
|
1808
|
+
this.prepareStatements(block.body);
|
|
1809
|
+
this.objects.push(...this.numeric.objects);
|
|
1810
|
+
this.allocateBlockFields();
|
|
1811
|
+
const routines = [...core.functions, ...core.blocks.filter((block) => !block.standard)];
|
|
1812
|
+
for (const fn of routines)
|
|
1813
|
+
this.allocateRoutineScope(fn);
|
|
1814
|
+
for (const fn of routines)
|
|
1815
|
+
this.emitRoutine(fn);
|
|
1816
|
+
for (const program of core.programs)
|
|
1817
|
+
this.emitProgram(program);
|
|
1818
|
+
const { objects, mappings } = this;
|
|
1819
|
+
const paths = new Map;
|
|
1820
|
+
for (const object of objects) {
|
|
1821
|
+
const path = this.objectPath(object);
|
|
1822
|
+
paths.set(object.id, path);
|
|
1823
|
+
object.sourcePath = object.implementation === undefined ? `${path}.st` : `${path}/declaration.st`;
|
|
1824
|
+
}
|
|
1825
|
+
const artifacts = objects.flatMap((object) => {
|
|
1826
|
+
const path = paths.get(object.id);
|
|
1827
|
+
return object.implementation === undefined ? [{ path: `${path}.st`, content: object.declaration }] : [
|
|
1828
|
+
{ path: `${path}/declaration.st`, content: object.declaration },
|
|
1829
|
+
{ path: `${path}/implementation.st`, content: object.implementation }
|
|
1830
|
+
];
|
|
1831
|
+
});
|
|
1832
|
+
for (const mapping of mappings) {
|
|
1833
|
+
const object = objects.find((object) => {
|
|
1834
|
+
const key = object.kind === "method" ? object.id : object.name;
|
|
1835
|
+
return mapping.path === `${key}.st` || mapping.path.startsWith(`${key}/`);
|
|
1836
|
+
});
|
|
1837
|
+
if (object) {
|
|
1838
|
+
const key = object.kind === "method" ? object.id : object.name;
|
|
1839
|
+
mapping.path = paths.get(object.id) + mapping.path.slice(key.length);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
const ioSymbols = Object.fromEntries([
|
|
1843
|
+
...this.channels.map((channel) => [channel.key, channel.target]),
|
|
1844
|
+
...this.ioMappings.map((mapping) => [mapping.key, mapping.variable])
|
|
1845
|
+
]);
|
|
1846
|
+
const { ioMappings } = this;
|
|
1847
|
+
const logicalIo = this.plan.core.variables.filter((variable) => variable.storage === "input" || variable.storage === "output").map((variable) => variable.id);
|
|
1848
|
+
const rpcSymbols = [
|
|
1849
|
+
...new Set(this.rpcChannels.flatMap((channel) => [channel.request, channel.requestId, channel.reply, channel.replyId, channel.epoch].map((path) => path.replace(/\[\d+\]$/, ""))))
|
|
1850
|
+
];
|
|
1851
|
+
const published = (ids) => [...ids, ...logicalIo].map((id) => required(this.references.get(id))).concat(rpcSymbols);
|
|
1852
|
+
const publication = {
|
|
1853
|
+
production: published(this.plan.publication.production),
|
|
1854
|
+
development: published(this.plan.publication.development)
|
|
1855
|
+
};
|
|
1856
|
+
const exposures = Object.fromEntries(core.variables.filter((variable) => variable.exposure).map((variable) => {
|
|
1857
|
+
const record = this.typeMap.get(variable.type);
|
|
1858
|
+
if (record?.kind !== "record")
|
|
1859
|
+
throw new Error("Invalid exposure record.");
|
|
1860
|
+
const root = required(this.references.get(variable.id));
|
|
1861
|
+
const fields = Object.fromEntries(record.fields.filter((field) => field.name !== "contractVersion").map((field) => {
|
|
1862
|
+
const type = this.typeMap.get(field.type);
|
|
1863
|
+
const kind = type?.kind === "bool" ? "boolean" : type?.kind === "string" ? "string" : "number";
|
|
1864
|
+
return [
|
|
1865
|
+
field.name,
|
|
1866
|
+
{ path: `${root}.${this.targetTypes.field(record.id, field.name)}`, kind }
|
|
1867
|
+
];
|
|
1868
|
+
}));
|
|
1869
|
+
return [
|
|
1870
|
+
variable.exposure.name,
|
|
1871
|
+
{
|
|
1872
|
+
version: variable.exposure.version,
|
|
1873
|
+
versionPath: `${root}.${this.targetTypes.field(record.id, "contractVersion")}`,
|
|
1874
|
+
telemetry: fields
|
|
1875
|
+
}
|
|
1876
|
+
];
|
|
1877
|
+
}));
|
|
1878
|
+
artifacts.push({ path: "exposures.json", content: JSON.stringify(exposures, null, 2) + `
|
|
1879
|
+
` }, {
|
|
1880
|
+
path: "machines.ts",
|
|
1881
|
+
content: `// Generated explicit read-only machine interfaces. Do not edit.
|
|
1882
|
+
export const machines=${JSON.stringify(exposures, null, 2)} as const;
|
|
1883
|
+
export const serviceChannels=${JSON.stringify(this.rpcChannels, null, 2)} as const;
|
|
1884
|
+
`
|
|
1885
|
+
});
|
|
1886
|
+
return { artifacts, objects, mappings, ioSymbols, ioMappings, publication, diagnostics: [] };
|
|
1887
|
+
}
|
|
1888
|
+
objectPath(object) {
|
|
1889
|
+
const { core } = this.plan;
|
|
1890
|
+
if (object.kind === "method" && object.parent) {
|
|
1891
|
+
const parent = required(this.objects.find((candidate) => candidate.id === object.parent));
|
|
1892
|
+
return `${this.objectPath(parent)}/methods/${object.name}`;
|
|
1893
|
+
}
|
|
1894
|
+
if (object.kind === "method") {
|
|
1895
|
+
const method = required(core.functions.find((fn) => fn.id === object.id));
|
|
1896
|
+
const owner = required(this.objects.find((item) => item.id === method.owner?.block));
|
|
1897
|
+
return `${this.objectPath(owner)}/methods/${object.name}`;
|
|
1898
|
+
}
|
|
1899
|
+
let source = object.id.split(":")[0] ?? "";
|
|
1900
|
+
const program = core.programs.find((item) => item.id === object.id);
|
|
1901
|
+
if (program)
|
|
1902
|
+
source = program.entry.split(":")[0] ?? "";
|
|
1903
|
+
const group = this.layout.globalLists.find((item) => item.id === object.id);
|
|
1904
|
+
if (group) {
|
|
1905
|
+
source = group.variables[0]?.id.split(":")[0] ?? "";
|
|
1906
|
+
}
|
|
1907
|
+
if (/^[\w./ -]+\.tsx?$/.test(source) && !source.split(/[\\/]/).includes("..") && !source.startsWith("/")) {
|
|
1908
|
+
const segments = source.replaceAll("\\", "/").split("/").slice(0, -1);
|
|
1909
|
+
if (segments[0] === "src")
|
|
1910
|
+
segments.shift();
|
|
1911
|
+
if (object.kind !== "dataType" && segments.at(-1) === object.name)
|
|
1912
|
+
segments.pop();
|
|
1913
|
+
return [...segments, object.name].join("/");
|
|
1914
|
+
}
|
|
1915
|
+
return `${object.kind === "dataType" ? "Types" : object.id === "io" ? "IO" : "_Generated"}/${object.name}`;
|
|
1916
|
+
}
|
|
1917
|
+
emitGlobals() {
|
|
1918
|
+
const { options } = this.plan;
|
|
1919
|
+
const { layout, names, references, mappings, targetTypes, typeMap, objects } = this;
|
|
1920
|
+
const hasExports = (group) => group.variables.some((variable) => variable.exported && !variable.exposure);
|
|
1921
|
+
for (const group of layout.globalLists) {
|
|
1922
|
+
const splitByStorage = layout.globalLists.some((other) => other.module === group.module && other.storage !== group.storage && hasExports(other));
|
|
1923
|
+
const name = hasExports(group) ? names.allocate(pascalCase(splitByStorage ? `${group.module}-${group.storage}` : group.module)) : names.allocateObject("GVL", `${group.module}_${group.storage}`);
|
|
1924
|
+
const published = new Set(this.plan.publication[options.buildTarget]);
|
|
1925
|
+
const publish = group.variables.some((variable) => published.has(variable.id));
|
|
1926
|
+
const fields = new Names;
|
|
1927
|
+
for (const name of this.blockNames.values())
|
|
1928
|
+
fields.reserve(name);
|
|
1929
|
+
const lines = [
|
|
1930
|
+
...publish ? ["{attribute 'linkalways'}"] : [],
|
|
1931
|
+
"{attribute 'qualified_only'}",
|
|
1932
|
+
group.storage === "constants" ? "VAR_GLOBAL CONSTANT" : group.storage === "retained" ? "VAR_GLOBAL RETAIN" : "VAR_GLOBAL"
|
|
1933
|
+
];
|
|
1934
|
+
for (const item of group.variables) {
|
|
1935
|
+
const field = fields.allocate(item.name);
|
|
1936
|
+
references.set(item.id, `${name}.${field}`);
|
|
1937
|
+
if (published.has(item.id))
|
|
1938
|
+
lines.push(` {attribute 'symbol' := '${item.exposure ? "read" : "readwrite"}'}`);
|
|
1939
|
+
mappings.push({ path: `${name}.st`, line: lines.length + 1, span: item.span });
|
|
1940
|
+
let initial = "";
|
|
1941
|
+
if (!instancedBlock5(item.type, typeMap)) {
|
|
1942
|
+
const value = item.initial ?? defaultValue(item.type, typeMap);
|
|
1943
|
+
initial = group.storage === "constants" ? ` := ${targetTypes.literal(value, item.type)}` : targetTypes.declarationInitializer(value, item.type);
|
|
1944
|
+
}
|
|
1945
|
+
lines.push(` ${field} : ${targetTypes.name(item.type)}${initial};`);
|
|
1946
|
+
}
|
|
1947
|
+
lines.push("END_VAR");
|
|
1948
|
+
objects.push({
|
|
1949
|
+
id: group.id,
|
|
1950
|
+
kind: "globalList",
|
|
1951
|
+
name,
|
|
1952
|
+
declaration: `${lines.join(`
|
|
1953
|
+
`)}
|
|
1954
|
+
`
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
emitIo() {
|
|
1959
|
+
const { core, options } = this.plan;
|
|
1960
|
+
const { names, references, channels, ioMappings, mappings, targetTypes, typeMap, objects } = this;
|
|
1961
|
+
const ioName = names.allocate("GVL_IO");
|
|
1962
|
+
const ioFields = new Names;
|
|
1963
|
+
for (const name of this.blockNames.values())
|
|
1964
|
+
ioFields.reserve(name);
|
|
1965
|
+
const ioLines = ["{attribute 'linkalways'}", "{attribute 'qualified_only'}", "VAR_GLOBAL"];
|
|
1966
|
+
for (const item of core.variables.filter((item) => item.storage === "input" || item.storage === "output")) {
|
|
1967
|
+
const field = ioFields.allocate(item.name);
|
|
1968
|
+
const initial = item.initial ?? defaultValue(item.type, typeMap);
|
|
1969
|
+
const binding = required(options.io[item.name]);
|
|
1970
|
+
references.set(item.id, `${ioName}.${field}`);
|
|
1971
|
+
if (binding.mapped) {
|
|
1972
|
+
ioMappings.push({
|
|
1973
|
+
key: item.name,
|
|
1974
|
+
address: binding.address,
|
|
1975
|
+
variable: `${ioName}.${field}`
|
|
1976
|
+
});
|
|
1977
|
+
mappings.push({ path: `${ioName}.st`, line: ioLines.length + 2, span: item.span });
|
|
1978
|
+
ioLines.push(" {attribute 'symbol' := 'read'}", ` ${field} : ${targetTypes.name(item.type)} := ${targetTypes.literal(initial, item.type)};`);
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1981
|
+
const wire = ioFields.allocate(`${item.name}_physical`);
|
|
1982
|
+
channels.push({
|
|
1983
|
+
key: item.name,
|
|
1984
|
+
root: item.id,
|
|
1985
|
+
input: item.storage === "input",
|
|
1986
|
+
target: `${ioName}.${field}`,
|
|
1987
|
+
wire: `${ioName}.${wire}`,
|
|
1988
|
+
inverted: binding.inverted
|
|
1989
|
+
});
|
|
1990
|
+
mappings.push({ path: `${ioName}.st`, line: ioLines.length + 1, span: item.span }, { path: `${ioName}.st`, line: ioLines.length + 3, span: item.span });
|
|
1991
|
+
ioLines.push(` ${wire} AT ${binding.address} : ${targetTypes.name(item.type)};`, " {attribute 'symbol' := 'read'}", ` ${field} : ${targetTypes.name(item.type)} := ${targetTypes.literal(initial, item.type)};`);
|
|
1992
|
+
}
|
|
1993
|
+
for (const { key, root, segments, type } of this.plan.storageBindings) {
|
|
1994
|
+
const { address, mapped, inverted } = required(options.io[key]);
|
|
1995
|
+
const target = segments.reduce((path, segment) => segment.kind === "field" ? `${path}.${targetTypes.field(segment.record, segment.name)}` : `${path}[${segment.value}]`, required(references.get(root.id)));
|
|
1996
|
+
if (mapped) {
|
|
1997
|
+
ioMappings.push({ key, address, variable: target });
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
2000
|
+
const wire = ioFields.allocate(`${key.replace(/\W+/g, "_").replace(/_+$/, "")}_physical`);
|
|
2001
|
+
ioLines.push(` ${wire} AT ${address} : ${targetTypes.name(type.id)};`);
|
|
2002
|
+
channels.push({
|
|
2003
|
+
key,
|
|
2004
|
+
root: root.id,
|
|
2005
|
+
input: address[1] === "I",
|
|
2006
|
+
target,
|
|
2007
|
+
wire: `${ioName}.${wire}`,
|
|
2008
|
+
inverted
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
ioLines.push("END_VAR");
|
|
2012
|
+
if (ioLines.length > 4)
|
|
2013
|
+
objects.push({
|
|
2014
|
+
id: "io",
|
|
2015
|
+
kind: "globalList",
|
|
2016
|
+
name: ioName,
|
|
2017
|
+
declaration: `${ioLines.join(`
|
|
2018
|
+
`)}
|
|
2019
|
+
`
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
prepareStatements(items) {
|
|
2023
|
+
const { numeric } = this;
|
|
2024
|
+
for (const item of items) {
|
|
2025
|
+
if (item.kind === "assign") {
|
|
2026
|
+
numeric.prepare(item.value);
|
|
2027
|
+
item.path?.forEach((segment) => {
|
|
2028
|
+
if (segment.kind === "index")
|
|
2029
|
+
numeric.prepare(segment.value);
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
if (item.kind === "if") {
|
|
2033
|
+
numeric.prepare(item.condition);
|
|
2034
|
+
this.prepareStatements(item.then);
|
|
2035
|
+
this.prepareStatements(item.else);
|
|
2036
|
+
}
|
|
2037
|
+
if (item.kind === "switch") {
|
|
2038
|
+
numeric.prepare(item.value);
|
|
2039
|
+
for (const branch of item.cases)
|
|
2040
|
+
this.prepareStatements(branch.body);
|
|
2041
|
+
this.prepareStatements(item.fallback);
|
|
2042
|
+
}
|
|
2043
|
+
if (item.kind === "for") {
|
|
2044
|
+
if (item.dynamic) {
|
|
2045
|
+
numeric.prepare(item.dynamic.start);
|
|
2046
|
+
numeric.prepare(item.dynamic.limit);
|
|
2047
|
+
}
|
|
2048
|
+
this.prepareStatements(item.body);
|
|
2049
|
+
}
|
|
2050
|
+
if (item.kind === "return" && item.value)
|
|
2051
|
+
numeric.prepare(item.value);
|
|
2052
|
+
if (item.kind === "invoke") {
|
|
2053
|
+
if (item.index)
|
|
2054
|
+
numeric.prepare(item.index);
|
|
2055
|
+
for (const value of item.arguments)
|
|
2056
|
+
numeric.prepare(value);
|
|
2057
|
+
}
|
|
2058
|
+
if (item.kind === "blockUpdate") {
|
|
2059
|
+
if (item.index)
|
|
2060
|
+
numeric.prepare(item.index);
|
|
2061
|
+
for (const value of Object.values(item.inputs))
|
|
2062
|
+
numeric.prepare(value);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
allocateBlockFields() {
|
|
2067
|
+
const { core } = this.plan;
|
|
2068
|
+
const { objects, functionNames, blockNames, blockFields } = this;
|
|
2069
|
+
for (const block of core.blocks) {
|
|
2070
|
+
const fields = new Names, mapped = new Map;
|
|
2071
|
+
for (const object of objects)
|
|
2072
|
+
fields.reserve(object.name);
|
|
2073
|
+
for (const objectName of [
|
|
2074
|
+
...core.functions.filter((fn) => !fn.owner).map((fn) => required(functionNames.get(fn.id))),
|
|
2075
|
+
...blockNames.values()
|
|
2076
|
+
])
|
|
2077
|
+
fields.reserve(objectName);
|
|
2078
|
+
for (const method of core.functions.filter((fn) => fn.owner?.block === block.id))
|
|
2079
|
+
functionNames.set(method.id, fields.allocate(method.name));
|
|
2080
|
+
const native = block.standard && standardBlocks[block.standard];
|
|
2081
|
+
for (const item of [...block.inputs, ...block.outputs, ...block.state])
|
|
2082
|
+
mapped.set(item.id, native ? required([...native.inputs, ...native.outputs].find(([name]) => name === item.name))[1] : fields.allocate(item.name, true));
|
|
2083
|
+
if (block.runtime?.kind === "mailbox")
|
|
2084
|
+
this.mailboxLocks.set(block.id, fields.allocate("plc_mailbox_lock"));
|
|
2085
|
+
blockFields.set(block.id, mapped);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
allocateRoutineScope(fn) {
|
|
2089
|
+
const block = "inputs" in fn ? fn : undefined;
|
|
2090
|
+
const owner = block ?? ("owner" in fn && fn.owner ? this.blocks.get(fn.owner.block) : undefined);
|
|
2091
|
+
const names = new Names, references = new Map;
|
|
2092
|
+
for (const object of this.objects)
|
|
2093
|
+
names.reserve(object.name);
|
|
2094
|
+
for (const name of [...this.functionNames.values(), ...this.blockNames.values()])
|
|
2095
|
+
names.reserve(name);
|
|
2096
|
+
if (this.programRoutines.has(fn.id) && "parameters" in fn && fn.result)
|
|
2097
|
+
this.programResults.set(fn.id, names.allocate("plc_result"));
|
|
2098
|
+
if (owner) {
|
|
2099
|
+
for (const item of [...owner.outputs, ...owner.state]) {
|
|
2100
|
+
const name = required(required(this.blockFields.get(owner.id)).get(item.id));
|
|
2101
|
+
names.reserve(name);
|
|
2102
|
+
references.set(item.id, name);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
for (const item of "parameters" in fn ? fn.parameters : fn.inputs) {
|
|
2106
|
+
const name = block ? required(required(this.blockFields.get(block.id)).get(item.id)) : names.allocate(item.name);
|
|
2107
|
+
names.reserve(name);
|
|
2108
|
+
references.set(item.id, name);
|
|
2109
|
+
}
|
|
2110
|
+
for (const item of fn.locals)
|
|
2111
|
+
references.set(item.id, names.allocate(item.name));
|
|
2112
|
+
this.routineScopes.set(fn.id, { names, references });
|
|
2113
|
+
}
|
|
2114
|
+
routineHeader(fn, name) {
|
|
2115
|
+
if ("inputs" in fn)
|
|
2116
|
+
return { kind: "functionBlock", declaration: `FUNCTION_BLOCK ${name}` };
|
|
2117
|
+
if (fn.owner)
|
|
2118
|
+
return {
|
|
2119
|
+
kind: "method",
|
|
2120
|
+
declaration: `METHOD ${fn.owner.access.toUpperCase()} ${name}${fn.result ? ` : ${this.targetTypes.name(fn.result)}` : ""}`
|
|
2121
|
+
};
|
|
2122
|
+
if (this.programRoutines.has(fn.id))
|
|
2123
|
+
return { kind: "program", declaration: `PROGRAM ${name}` };
|
|
2124
|
+
return {
|
|
2125
|
+
kind: "function",
|
|
2126
|
+
declaration: `FUNCTION ${name} : ${fn.result ? this.targetTypes.name(fn.result) : "BOOL"}`
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
emitRoutine(fn) {
|
|
2130
|
+
const { core } = this.plan;
|
|
2131
|
+
if ("inputs" in fn && (fn.runtime?.kind === "http" || fn.runtime?.kind === "rpc")) {
|
|
2132
|
+
const endpoint = required(this.plan.http[fn.id]);
|
|
2133
|
+
const rpcGlobal = endpoint.transport === "opcua" ? this.names.allocateObject("GVL", "TsplcRpc_" + required(this.blockNames.get(fn.id))) : undefined;
|
|
2134
|
+
if (rpcGlobal) {
|
|
2135
|
+
this.objects.push({
|
|
2136
|
+
id: fn.id + ":rpc",
|
|
2137
|
+
kind: "globalList",
|
|
2138
|
+
name: rpcGlobal,
|
|
2139
|
+
declaration: `{attribute 'qualified_only'}
|
|
2140
|
+
VAR_GLOBAL
|
|
2141
|
+
{attribute 'symbol' := 'read'} requests:ARRAY[1..${opcuaServiceCapacity}] OF STRING(2048);
|
|
2142
|
+
{attribute 'symbol' := 'read'} requestIds:ARRAY[1..${opcuaServiceCapacity}] OF STRING(64);
|
|
2143
|
+
{attribute 'symbol' := 'readwrite'} replyIds:ARRAY[1..${opcuaServiceCapacity}] OF STRING(64);
|
|
2144
|
+
{attribute 'symbol' := 'readwrite'} replies:ARRAY[1..${opcuaServiceCapacity}] OF STRING(2048);
|
|
2145
|
+
{attribute 'symbol' := 'readwrite'} epochs:ARRAY[1..${opcuaServiceCapacity}] OF STRING(32);
|
|
2146
|
+
inUse:ARRAY[1..${opcuaServiceCapacity}] OF __XWORD;
|
|
2147
|
+
sequence:ARRAY[1..${opcuaServiceCapacity}] OF UDINT;
|
|
2148
|
+
END_VAR`
|
|
2149
|
+
});
|
|
2150
|
+
for (let slot = 1;slot <= opcuaServiceCapacity; slot++)
|
|
2151
|
+
this.rpcChannels.push({
|
|
2152
|
+
path: endpoint.path,
|
|
2153
|
+
request: `${rpcGlobal}.requests[${slot}]`,
|
|
2154
|
+
reply: `${rpcGlobal}.replies[${slot}]`,
|
|
2155
|
+
requestId: `${rpcGlobal}.requestIds[${slot}]`,
|
|
2156
|
+
replyId: `${rpcGlobal}.replyIds[${slot}]`,
|
|
2157
|
+
epoch: `${rpcGlobal}.epochs[${slot}]`
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
const generated = emitHttp(fn, required(this.blockNames.get(fn.id)), required(this.blockFields.get(fn.id)), this.typeMap, this.targetTypes, endpoint, this.networkName ? `${this.networkName}.network` : "", rpcGlobal);
|
|
2161
|
+
this.objects.push(generated, ...httpMethods(generated.id, endpoint.transport), ...httpCertificateVerifier(generated.id, generated.name, required(this.plan.http[fn.id])));
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
const {
|
|
2165
|
+
blockNames,
|
|
2166
|
+
functionNames,
|
|
2167
|
+
references,
|
|
2168
|
+
objects,
|
|
2169
|
+
mappings,
|
|
2170
|
+
targetTypes,
|
|
2171
|
+
typeMap,
|
|
2172
|
+
blocks,
|
|
2173
|
+
blockFields,
|
|
2174
|
+
numeric,
|
|
2175
|
+
layout
|
|
2176
|
+
} = this;
|
|
2177
|
+
const block = "inputs" in fn ? fn : undefined;
|
|
2178
|
+
const method = "owner" in fn ? fn.owner : undefined;
|
|
2179
|
+
const program = this.programRoutines.has(fn.id);
|
|
2180
|
+
const owner = method ? required(blocks.get(method.block)) : block;
|
|
2181
|
+
const name = block ? required(blockNames.get(block.id)) : required(functionNames.get(fn.id));
|
|
2182
|
+
const inputs = "parameters" in fn ? fn.parameters : fn.inputs;
|
|
2183
|
+
const state = owner ? [...owner.outputs, ...owner.state] : [];
|
|
2184
|
+
const mappingName = method ? fn.id : name;
|
|
2185
|
+
const routineScope = required(this.routineScopes.get(fn.id));
|
|
2186
|
+
const scope = new Map([...references, ...routineScope.references]), locals = routineScope.names;
|
|
2187
|
+
const declarations = fn.locals.map((item) => ` ${required(scope.get(item.id))} : ${targetTypes.name(item.type)};`);
|
|
2188
|
+
const declarationSpans = fn.locals.map((item) => item.span);
|
|
2189
|
+
const mutex = method && owner?.runtime?.kind === "mailbox" ? required(this.mailboxLocks.get(owner.id)) : undefined;
|
|
2190
|
+
const acquired = mutex ? locals.allocate("plc_acquired") : undefined;
|
|
2191
|
+
const unavailable = mutex && fn.result ? locals.allocate("plc_unavailable") : undefined;
|
|
2192
|
+
if (acquired) {
|
|
2193
|
+
declarations.push(` ${acquired} : BOOL;`);
|
|
2194
|
+
declarationSpans.push(fn.span);
|
|
2195
|
+
}
|
|
2196
|
+
if (unavailable && fn.result) {
|
|
2197
|
+
declarations.push(` ${unavailable} : ${targetTypes.name(fn.result)} := ${targetTypes.literal(defaultValue(fn.result, typeMap), fn.result)};`);
|
|
2198
|
+
declarationSpans.push(fn.span);
|
|
2199
|
+
}
|
|
2200
|
+
const lines = [];
|
|
2201
|
+
const write = (line, depth, source) => {
|
|
2202
|
+
lines.push(" ".repeat(depth) + line);
|
|
2203
|
+
if (source)
|
|
2204
|
+
mappings.push({
|
|
2205
|
+
path: `${mappingName}/implementation.st`,
|
|
2206
|
+
line: lines.length,
|
|
2207
|
+
span: source
|
|
2208
|
+
});
|
|
2209
|
+
};
|
|
2210
|
+
const release = (lock, depth, source) => {
|
|
2211
|
+
write(`WHILE NOT __COMPARE_AND_SWAP(ADR(${lock}), 1, 0) DO`, depth, source);
|
|
2212
|
+
write(";", depth + 1);
|
|
2213
|
+
write("END_WHILE;", depth);
|
|
2214
|
+
};
|
|
2215
|
+
const load = (id) => {
|
|
2216
|
+
const value = scope.get(id);
|
|
2217
|
+
if (!value)
|
|
2218
|
+
throw new Error(`Missing verified storage ${id}.`);
|
|
2219
|
+
return value;
|
|
2220
|
+
};
|
|
2221
|
+
const scopeTypes = new Map([...core.variables, ...inputs, ...state, ...fn.locals].map((variable) => [
|
|
2222
|
+
variable.id,
|
|
2223
|
+
variable.type
|
|
2224
|
+
]));
|
|
2225
|
+
const place = (id, path = []) => {
|
|
2226
|
+
let text = load(id), typeId = required(scopeTypes.get(id));
|
|
2227
|
+
let outputBlock;
|
|
2228
|
+
for (const segment of path) {
|
|
2229
|
+
const type = required(typeMap.get(typeId));
|
|
2230
|
+
if (segment.kind === "field" && type.kind === "block" && segment.name === "out") {
|
|
2231
|
+
outputBlock = type.block;
|
|
2232
|
+
typeId = required(type.output);
|
|
2233
|
+
continue;
|
|
2234
|
+
}
|
|
2235
|
+
if (segment.kind === "field" && outputBlock) {
|
|
2236
|
+
const output = required(required(blocks.get(outputBlock)).outputs.find((field) => field.name === segment.name));
|
|
2237
|
+
text += `.${required(required(blockFields.get(outputBlock)).get(output.id))}`;
|
|
2238
|
+
typeId = output.type;
|
|
2239
|
+
outputBlock = undefined;
|
|
2240
|
+
} else if (segment.kind === "field" && type.kind === "record") {
|
|
2241
|
+
text += `.${targetTypes.field(typeId, segment.name)}`;
|
|
2242
|
+
typeId = required(type.fields.find((field) => field.name === segment.name)).type;
|
|
2243
|
+
} else if (segment.kind === "index" && type.kind === "array") {
|
|
2244
|
+
text += typeMap.get(segment.value.type)?.kind === "enum" ? `[TO_DINT(${expression(segment.value)})]` : `[${expression(segment.value)}]`;
|
|
2245
|
+
typeId = type.element;
|
|
2246
|
+
} else
|
|
2247
|
+
throw new Error("Invalid verified place");
|
|
2248
|
+
}
|
|
2249
|
+
return text;
|
|
2250
|
+
};
|
|
2251
|
+
const expression = (value) => {
|
|
2252
|
+
if (value.kind === "constant")
|
|
2253
|
+
return targetTypes.literal(value.value, value.type);
|
|
2254
|
+
if (value.kind === "load")
|
|
2255
|
+
return place(value.root, value.path);
|
|
2256
|
+
if (value.kind === "borrow")
|
|
2257
|
+
return place(value.place.root, value.place.path);
|
|
2258
|
+
if (value.kind === "convert" && typeMap.get(value.type)?.kind === "string")
|
|
2259
|
+
return expression(value.operand);
|
|
2260
|
+
if (value.kind === "unary" || value.kind === "convert")
|
|
2261
|
+
return numeric.render(value, [expression(value.operand)]);
|
|
2262
|
+
return numeric.render(value, [expression(value.left), expression(value.right)]);
|
|
2263
|
+
};
|
|
2264
|
+
const assignConstant = (destination, value, id, depth, span) => {
|
|
2265
|
+
const type = required(typeMap.get(id));
|
|
2266
|
+
if (type.kind === "record" && value.kind === "record")
|
|
2267
|
+
type.fields.forEach((field) => {
|
|
2268
|
+
assignConstant(`${destination}.${targetTypes.field(id, field.name)}`, required(value.fields[field.name]), field.type, depth, span);
|
|
2269
|
+
});
|
|
2270
|
+
else if (type.kind === "array" && value.kind === "array")
|
|
2271
|
+
value.items.forEach((item, index) => {
|
|
2272
|
+
assignConstant(`${destination}[${type.lowerBound + index}]`, item, type.element, depth, span);
|
|
2273
|
+
});
|
|
2274
|
+
else
|
|
2275
|
+
write(`${destination} := ${targetTypes.literal(value, id)};`, depth, span);
|
|
2276
|
+
};
|
|
2277
|
+
const body = (items, depth) => {
|
|
2278
|
+
for (const item of items) {
|
|
2279
|
+
switch (item.kind) {
|
|
2280
|
+
case "assign":
|
|
2281
|
+
if (item.value.kind === "constant")
|
|
2282
|
+
assignConstant(place(item.root, item.path), item.value.value, item.value.type, depth, item.span);
|
|
2283
|
+
else
|
|
2284
|
+
write(`${place(item.root, item.path)} := ${expression(item.value)};`, depth, item.span);
|
|
2285
|
+
break;
|
|
2286
|
+
case "blockUpdate": {
|
|
2287
|
+
const instance = instancedBlock5(required(scopeTypes.get(item.instance)), typeMap);
|
|
2288
|
+
if (!instance)
|
|
2289
|
+
throw new Error("Invalid verified block receiver");
|
|
2290
|
+
const target = required(blocks.get(instance.block));
|
|
2291
|
+
const arguments_ = Object.entries(item.inputs).map(([input, value]) => {
|
|
2292
|
+
const formal = required(target.inputs.find((field) => field.name === input));
|
|
2293
|
+
return `${required(required(blockFields.get(target.id)).get(formal.id))} := ${expression(value)}`;
|
|
2294
|
+
});
|
|
2295
|
+
const receiver = item.index ? `${load(item.instance)}[${expression(item.index)}]` : load(item.instance);
|
|
2296
|
+
write(`${receiver}(${arguments_.join(", ")});`, depth, item.span);
|
|
2297
|
+
break;
|
|
2298
|
+
}
|
|
2299
|
+
case "invoke": {
|
|
2300
|
+
const receiver = item.instance ? `${load(item.instance)}${item.index ? `[${expression(item.index)}]` : ""}.` : "";
|
|
2301
|
+
const called = required(core.functions.find((fn) => fn.id === item.function));
|
|
2302
|
+
const programCall = this.programRoutines.has(called.id);
|
|
2303
|
+
const arguments_ = item.arguments.map((value, index) => {
|
|
2304
|
+
if (!called.owner && !programCall && !called.parameters.some((parameter) => parameter.mode === "inOut"))
|
|
2305
|
+
return expression(value);
|
|
2306
|
+
const parameter = required(called.parameters[index]);
|
|
2307
|
+
const formal = required(required(this.routineScopes.get(called.id)).references.get(parameter.id));
|
|
2308
|
+
return `${formal} := ${expression(value)}`;
|
|
2309
|
+
});
|
|
2310
|
+
const call = `${receiver}${required(functionNames.get(item.function))}(${arguments_.join(", ")})`;
|
|
2311
|
+
if (programCall) {
|
|
2312
|
+
write(`${call};`, depth, item.span);
|
|
2313
|
+
if (item.destination)
|
|
2314
|
+
write(`${load(item.destination)} := ${required(functionNames.get(called.id))}.${required(this.programResults.get(called.id))};`, depth, item.span);
|
|
2315
|
+
break;
|
|
2316
|
+
}
|
|
2317
|
+
write(item.destination ? `${load(item.destination)} := ${call};` : `${call};`, depth, item.span);
|
|
2318
|
+
break;
|
|
2319
|
+
}
|
|
2320
|
+
case "if":
|
|
2321
|
+
write(`IF ${expression(item.condition)} THEN`, depth, item.span);
|
|
2322
|
+
body(item.then, depth + 1);
|
|
2323
|
+
if (item.else.length) {
|
|
2324
|
+
write("ELSE", depth, item.span);
|
|
2325
|
+
body(item.else, depth + 1);
|
|
2326
|
+
}
|
|
2327
|
+
write("END_IF;", depth, item.span);
|
|
2328
|
+
break;
|
|
2329
|
+
case "switch": {
|
|
2330
|
+
for (const [index, branch] of item.cases.entries()) {
|
|
2331
|
+
const conditions = branch.values.map((value) => `(${expression(item.value)} = ${targetTypes.literal(value, item.value.type)})`);
|
|
2332
|
+
write(`${index ? "ELSIF" : "IF"} ${conditions.join(" OR ")} THEN`, depth, item.span);
|
|
2333
|
+
body(branch.body, depth + 1);
|
|
2334
|
+
}
|
|
2335
|
+
if (item.cases.length) {
|
|
2336
|
+
if (item.fallback.length)
|
|
2337
|
+
write("ELSE", depth, item.span);
|
|
2338
|
+
body(item.fallback, depth + 1);
|
|
2339
|
+
write("END_IF;", depth, item.span);
|
|
2340
|
+
} else
|
|
2341
|
+
body(item.fallback, depth);
|
|
2342
|
+
break;
|
|
2343
|
+
}
|
|
2344
|
+
case "for": {
|
|
2345
|
+
const counterType = scopeTypes.get(item.counter);
|
|
2346
|
+
if (!counterType || item.start.kind !== "integer")
|
|
2347
|
+
throw new Error("Invalid verified loop");
|
|
2348
|
+
if (!item.maxIterations) {
|
|
2349
|
+
if (item.counterScope === "existing")
|
|
2350
|
+
write(`${load(item.counter)} := ${item.dynamic ? expression(item.dynamic.start) : targetTypes.literal(item.start, counterType)};`, depth, item.span);
|
|
2351
|
+
break;
|
|
2352
|
+
}
|
|
2353
|
+
const ordinal = locals.allocate("iteration");
|
|
2354
|
+
declarations.push(` ${ordinal} : DINT;`);
|
|
2355
|
+
declarationSpans.push(item.span);
|
|
2356
|
+
const wide = counterType === "ULINT" ? "ULINT" : "LINT";
|
|
2357
|
+
const ascending = BigInt(item.step) > 0n;
|
|
2358
|
+
const step = BigInt(item.step) < 0n ? -BigInt(item.step) : BigInt(item.step);
|
|
2359
|
+
let iterations = `DINT#${item.maxIterations}`;
|
|
2360
|
+
let first = `${wide}#${item.start.value}`;
|
|
2361
|
+
if (item.dynamic) {
|
|
2362
|
+
first = `TO_${wide}(${expression(item.dynamic.start)})`;
|
|
2363
|
+
const last = `TO_${wide}(${expression(item.dynamic.limit)})`;
|
|
2364
|
+
const count = locals.allocate("iterationCount");
|
|
2365
|
+
declarations.push(` ${count} : DINT;`);
|
|
2366
|
+
declarationSpans.push(item.span);
|
|
2367
|
+
const comparison = ascending ? "<" : ">";
|
|
2368
|
+
const inclusive = item.dynamic.exclusive ? "" : "=";
|
|
2369
|
+
const distance = ascending ? `${last} - ${first}` : `${first} - ${last}`;
|
|
2370
|
+
const exclusive = item.dynamic.exclusive ? ` - ${wide}#1` : "";
|
|
2371
|
+
write(`${count} := DINT#0;`, depth, item.span);
|
|
2372
|
+
write(`IF ${first} ${comparison}${inclusive} ${last} THEN`, depth, item.span);
|
|
2373
|
+
write(`${count} := ${wide}_TO_DINT(((${distance}${exclusive}) / ${wide}#${step}) + ${wide}#1);`, depth + 1, item.span);
|
|
2374
|
+
write("END_IF;", depth, item.span);
|
|
2375
|
+
iterations = count;
|
|
2376
|
+
}
|
|
2377
|
+
write(`FOR ${ordinal} := DINT#0 TO ${item.dynamic ? `${iterations} - DINT#1` : `DINT#${item.maxIterations - 1}`} DO`, depth, item.span);
|
|
2378
|
+
const counterValue = `${load(item.counter)} := TO_${counterType}(${first} ${ascending ? "+" : "-"} (${wide}#${step} * DINT_TO_${wide}(${ordinal})));`;
|
|
2379
|
+
write(counterValue, depth + 1, item.span);
|
|
2380
|
+
body(item.body, depth + 1);
|
|
2381
|
+
write("END_FOR;", depth, item.span);
|
|
2382
|
+
if (item.counterScope === "existing")
|
|
2383
|
+
write(counterValue, depth, item.span);
|
|
2384
|
+
break;
|
|
2385
|
+
}
|
|
2386
|
+
case "return":
|
|
2387
|
+
if (block) {
|
|
2388
|
+
if (item.value) {
|
|
2389
|
+
const returned = expression(item.value);
|
|
2390
|
+
for (const output of block.outputs)
|
|
2391
|
+
write(`${load(output.id)} := ${returned}.${targetTypes.field(required(block.result), output.name)};`, depth, item.span);
|
|
2392
|
+
}
|
|
2393
|
+
} else if (program) {
|
|
2394
|
+
if (item.value)
|
|
2395
|
+
write(`${required(this.programResults.get(fn.id))} := ${expression(item.value)};`, depth, item.span);
|
|
2396
|
+
} else if (!method || fn.result)
|
|
2397
|
+
write(`${name} := ${item.value ? expression(item.value) : "TRUE"};`, depth, item.span);
|
|
2398
|
+
if (mutex)
|
|
2399
|
+
release(mutex, depth, item.span);
|
|
2400
|
+
write("RETURN;", depth, item.span);
|
|
2401
|
+
break;
|
|
2402
|
+
case "break":
|
|
2403
|
+
write("EXIT;", depth, item.span);
|
|
2404
|
+
break;
|
|
2405
|
+
case "continue":
|
|
2406
|
+
write("CONTINUE;", depth, item.span);
|
|
2407
|
+
break;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
2411
|
+
if (mutex && acquired) {
|
|
2412
|
+
write(`${acquired} := FALSE;`, 0);
|
|
2413
|
+
write(`WHILE NOT ${acquired} AND ${mutex} = 0 DO`, 0);
|
|
2414
|
+
write(`${acquired} := __COMPARE_AND_SWAP(ADR(${mutex}), 0, 1);`, 1);
|
|
2415
|
+
write("END_WHILE;", 0);
|
|
2416
|
+
write(`IF NOT ${acquired} THEN`, 0);
|
|
2417
|
+
if (unavailable)
|
|
2418
|
+
write(`${name} := ${unavailable};`, 1);
|
|
2419
|
+
write("RETURN;", 1);
|
|
2420
|
+
write("END_IF;", 0);
|
|
2421
|
+
}
|
|
2422
|
+
body(fn.body, 0);
|
|
2423
|
+
if (mutex)
|
|
2424
|
+
release(mutex, 0);
|
|
2425
|
+
if (!block && !method && !program && !fn.result)
|
|
2426
|
+
write(`${name} := TRUE;`, 0);
|
|
2427
|
+
const header = this.routineHeader(fn, name);
|
|
2428
|
+
const declaration = [header.declaration];
|
|
2429
|
+
mappings.push({ path: `${mappingName}/declaration.st`, line: 1, span: fn.span });
|
|
2430
|
+
const section = (keyword, items) => {
|
|
2431
|
+
if (!items.length)
|
|
2432
|
+
return;
|
|
2433
|
+
declaration.push(keyword);
|
|
2434
|
+
for (const item of items) {
|
|
2435
|
+
mappings.push({
|
|
2436
|
+
path: `${mappingName}/declaration.st`,
|
|
2437
|
+
line: declaration.length + 1,
|
|
2438
|
+
span: item.span
|
|
2439
|
+
});
|
|
2440
|
+
const initial = item.initial && !instancedBlock5(item.type, typeMap) ? targetTypes.declarationInitializer(item.initial, item.type) : "";
|
|
2441
|
+
declaration.push(` ${load(item.id)} : ${targetTypes.name(item.type)}${initial};`);
|
|
2442
|
+
}
|
|
2443
|
+
declaration.push("END_VAR");
|
|
2444
|
+
};
|
|
2445
|
+
section("VAR_INPUT", inputs.filter((item) => item.mode !== "inOut"));
|
|
2446
|
+
section("VAR_IN_OUT", inputs.filter((item) => item.mode === "inOut"));
|
|
2447
|
+
if (program && "parameters" in fn && fn.result) {
|
|
2448
|
+
declaration.push("VAR_OUTPUT", ` ${required(this.programResults.get(fn.id))} : ${targetTypes.name(fn.result)};`, "END_VAR");
|
|
2449
|
+
}
|
|
2450
|
+
if (block) {
|
|
2451
|
+
if (block.runtime?.kind === "mailbox")
|
|
2452
|
+
declaration.push("VAR", ` ${required(this.mailboxLocks.get(block.id))} : __XWORD;`, "END_VAR");
|
|
2453
|
+
section("VAR_OUTPUT", block.outputs);
|
|
2454
|
+
section("VAR", block.state.filter((item) => !layout.retainedVariables.has(item.id)));
|
|
2455
|
+
section("VAR RETAIN", block.state.filter((item) => layout.retainedVariables.has(item.id)));
|
|
2456
|
+
}
|
|
2457
|
+
if (declarations.length) {
|
|
2458
|
+
declaration.push(block || program ? "VAR_TEMP" : "VAR");
|
|
2459
|
+
declarationSpans.forEach((span, index) => {
|
|
2460
|
+
mappings.push({
|
|
2461
|
+
path: `${mappingName}/declaration.st`,
|
|
2462
|
+
line: declaration.length + index + 1,
|
|
2463
|
+
span
|
|
2464
|
+
});
|
|
2465
|
+
});
|
|
2466
|
+
declaration.push(...declarations, "END_VAR");
|
|
2467
|
+
}
|
|
2468
|
+
objects.push({
|
|
2469
|
+
id: fn.id,
|
|
2470
|
+
kind: header.kind,
|
|
2471
|
+
name,
|
|
2472
|
+
declaration: `${declaration.join(`
|
|
2473
|
+
`)}
|
|
2474
|
+
`,
|
|
2475
|
+
implementation: `${lines.join(`
|
|
2476
|
+
`)}
|
|
2477
|
+
`
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
emitProgram(program) {
|
|
2481
|
+
const { core, options } = this.plan;
|
|
2482
|
+
const { names, functionNames, objects } = this;
|
|
2483
|
+
const explicit = options.programNames && Object.hasOwn(options.programNames, program.name) ? options.programNames[program.name] : undefined;
|
|
2484
|
+
const name = explicit ?? names.allocateObject("PRG", program.name);
|
|
2485
|
+
const fn = required(core.functions.find((item) => item.id === program.entry));
|
|
2486
|
+
const used = accessedStorage(fn, core);
|
|
2487
|
+
const channels = this.channels.filter((channel) => used.has(channel.root));
|
|
2488
|
+
const stateful = this.programRoutines.has(program.entry);
|
|
2489
|
+
const call = `${functionNames.get(program.entry)}();`;
|
|
2490
|
+
const lines = [
|
|
2491
|
+
...channels.filter((channel) => channel.input).map(({ target, wire, inverted }) => `${target} := ${inverted ? "NOT " : ""}${wire};`),
|
|
2492
|
+
stateful ? call : `plc_discard := ${call}`,
|
|
2493
|
+
...channels.filter((channel) => !channel.input).map(({ target, wire }) => `${wire} := ${target};`)
|
|
2494
|
+
];
|
|
2495
|
+
objects.push({
|
|
2496
|
+
id: program.id,
|
|
2497
|
+
kind: "program",
|
|
2498
|
+
name,
|
|
2499
|
+
declaration: stateful ? `PROGRAM ${name}
|
|
2500
|
+
` : `PROGRAM ${name}
|
|
2501
|
+
VAR
|
|
2502
|
+
plc_discard : BOOL;
|
|
2503
|
+
END_VAR
|
|
2504
|
+
`,
|
|
2505
|
+
implementation: `${lines.join(`
|
|
2506
|
+
`)}
|
|
2507
|
+
`
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
// packages/compiler/backends/codesys/vendor.ts
|
|
2512
|
+
import { CodesysCompilerError } from "@tsplc/build";
|
|
2513
|
+
function diagnoseCodesys(cause, bundle) {
|
|
2514
|
+
const notes = [];
|
|
2515
|
+
let current = cause;
|
|
2516
|
+
while (current instanceof Error) {
|
|
2517
|
+
if (current instanceof CodesysCompilerError && current.diagnostics.length) {
|
|
2518
|
+
return current.diagnostics.map((item) => {
|
|
2519
|
+
const location = item.positionText && /^Line ([1-9][0-9]*)(?:, Column ([1-9][0-9]*))? \((Decl|Impl)\)$/.exec(item.positionText);
|
|
2520
|
+
const identity = item.object;
|
|
2521
|
+
let objectPath;
|
|
2522
|
+
if (identity && identity.path.at(-1) === identity.name) {
|
|
2523
|
+
const relative = identity.applicationPath;
|
|
2524
|
+
if (Array.isArray(relative) && relative.length > 0 && relative.every((part) => typeof part === "string") && identity.isApplicationChild === (relative.length === 1) && identity.path.length > relative.length && identity.path.slice(-relative.length).every((part, index) => part === relative[index])) {
|
|
2525
|
+
objectPath = relative.join("/");
|
|
2526
|
+
} else if (relative === undefined && identity.isApplicationChild === true && identity.path.length >= 2) {
|
|
2527
|
+
objectPath = identity.name;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
const objects = objectPath ? bundle.objects.flatMap((object) => {
|
|
2531
|
+
const declaration = object.sourcePath ?? (object.implementation === undefined ? `${object.name}.st` : `${object.name}/declaration.st`);
|
|
2532
|
+
const nativePath = (object.kind === "method" ? declaration.replace(/\/methods\/([^/]+\/declaration\.st)$/, "/$1") : declaration).replace(object.implementation === undefined ? /\.st$/ : /\/declaration\.st$/, "");
|
|
2533
|
+
return object.name === identity?.name && nativePath === objectPath ? [{ object, declaration }] : [];
|
|
2534
|
+
}) : [];
|
|
2535
|
+
const match = objects.length === 1 ? objects[0] : undefined;
|
|
2536
|
+
const section = location?.[3] === "Decl" ? "declaration" : "implementation";
|
|
2537
|
+
const path = section === "declaration" ? match?.declaration : match?.object.implementation !== undefined ? match.declaration.replace(/declaration\.st$/, "implementation.st") : undefined;
|
|
2538
|
+
const mappings = location && path ? bundle.mappings.filter((mapping) => mapping.path === path && mapping.line === Number(location[1])) : [];
|
|
2539
|
+
const mapping = mappings.length === 1 ? mappings[0] : undefined;
|
|
2540
|
+
return {
|
|
2541
|
+
origin: "vendor",
|
|
2542
|
+
code: "PLC8001",
|
|
2543
|
+
severity: "error",
|
|
2544
|
+
message: item.text,
|
|
2545
|
+
...mapping && { span: mapping.span },
|
|
2546
|
+
notes: [
|
|
2547
|
+
...notes,
|
|
2548
|
+
...location && path ? [
|
|
2549
|
+
"Generated ST: " + path + ":" + location[1] + (location[2] ? `:${location[2]}` : "")
|
|
2550
|
+
] : [],
|
|
2551
|
+
`CODESYS diagnostic: ${JSON.stringify(item)}`
|
|
2552
|
+
]
|
|
2553
|
+
};
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
notes.push(current.message);
|
|
2557
|
+
if (current.cause === undefined)
|
|
2558
|
+
break;
|
|
2559
|
+
current = current.cause;
|
|
2560
|
+
if (!(current instanceof Error))
|
|
2561
|
+
notes.push(String(current));
|
|
2562
|
+
}
|
|
2563
|
+
return [
|
|
2564
|
+
{
|
|
2565
|
+
origin: "vendor",
|
|
2566
|
+
code: "PLC8001",
|
|
2567
|
+
severity: "error",
|
|
2568
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
2569
|
+
...notes.length > 1 && { notes: notes.slice(1) }
|
|
2570
|
+
}
|
|
2571
|
+
];
|
|
2572
|
+
}
|
|
2573
|
+
export {
|
|
2574
|
+
buildCodesys,
|
|
2575
|
+
codesys,
|
|
2576
|
+
codesysBuildConfig,
|
|
2577
|
+
diagnoseCodesys,
|
|
2578
|
+
direct,
|
|
2579
|
+
emitCodesys,
|
|
2580
|
+
engineeringBuildSchema,
|
|
2581
|
+
ioTargets,
|
|
2582
|
+
planCodesys,
|
|
2583
|
+
publicationSelectors
|
|
2584
|
+
};
|
|
2585
|
+
|
|
2586
|
+
//# debugId=0F83E9CDABD60BCF64756E2164756E21
|
|
2587
|
+
//# sourceMappingURL=codesys.js.map
|