@tsplc/compiler 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +740 -0
- package/dist/cli.js.map +13 -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 +423 -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 +105 -4
- package/project/artifacts.ts +277 -0
- package/project/cli.ts +282 -0
- package/project/index.ts +1 -0
- package/project/project.ts +298 -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/ir/aggregates.ts
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import {
|
|
2
|
+
comparisonOperators,
|
|
3
|
+
type Constant,
|
|
4
|
+
type Expression,
|
|
5
|
+
type Variable,
|
|
6
|
+
type Place,
|
|
7
|
+
} from "./model.ts";
|
|
8
|
+
import { constantFits, interval, scalarIssue, type IntegerInterval } from "./scalars.ts";
|
|
9
|
+
import { domainConstantFits, domainIssue } from "./domains.ts";
|
|
10
|
+
import { primitiveTypes, type PlcType } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
export function aggregate(type: PlcType | undefined): boolean {
|
|
13
|
+
return type?.kind === "record" || type?.kind === "array";
|
|
14
|
+
}
|
|
15
|
+
export function valueFits(
|
|
16
|
+
value: Constant,
|
|
17
|
+
type: PlcType,
|
|
18
|
+
types: ReadonlyMap<string, PlcType>,
|
|
19
|
+
): boolean {
|
|
20
|
+
if (type.kind === "duration" || type.kind === "calendar") return domainConstantFits(value, type);
|
|
21
|
+
if (type.kind === "enum")
|
|
22
|
+
return value.kind === "integer" && type.members.some((member) => member.value === value.value);
|
|
23
|
+
if (type.kind === "string")
|
|
24
|
+
return (
|
|
25
|
+
value.kind === "string" &&
|
|
26
|
+
!value.value.includes("\0") &&
|
|
27
|
+
(type.encoding === "utf8"
|
|
28
|
+
? new TextEncoder().encode(value.value).length <= type.capacity
|
|
29
|
+
: value.value.length <= type.capacity)
|
|
30
|
+
);
|
|
31
|
+
if (type.kind === "array")
|
|
32
|
+
return (
|
|
33
|
+
value.kind === "array" &&
|
|
34
|
+
value.items.length === type.length &&
|
|
35
|
+
value.items.every((item) => valueFits(item, types.get(type.element)!, types))
|
|
36
|
+
);
|
|
37
|
+
if (type.kind === "record")
|
|
38
|
+
return (
|
|
39
|
+
value.kind === "record" &&
|
|
40
|
+
Object.keys(value.fields).length === type.fields.length &&
|
|
41
|
+
type.fields.every(
|
|
42
|
+
(field) =>
|
|
43
|
+
Object.hasOwn(value.fields, field.name) &&
|
|
44
|
+
valueFits(value.fields[field.name]!, types.get(field.type)!, types),
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
return constantFits(value, type);
|
|
48
|
+
}
|
|
49
|
+
export function defaultValue(typeId: string, types: ReadonlyMap<string, PlcType>): Constant {
|
|
50
|
+
const type = types.get(typeId);
|
|
51
|
+
if (!type) throw new Error("Unknown default-value type");
|
|
52
|
+
if (type.kind === "bool") return { kind: "bool", value: false };
|
|
53
|
+
if (type.kind === "float") return { kind: "float", value: 0 };
|
|
54
|
+
if (type.kind === "string") return { kind: "string", value: "" };
|
|
55
|
+
if (type.kind === "enum")
|
|
56
|
+
return {
|
|
57
|
+
kind: "integer",
|
|
58
|
+
value: type.members.find((member) => member.value === "0")?.value ?? type.members[0]!.value,
|
|
59
|
+
};
|
|
60
|
+
if (type.kind === "array")
|
|
61
|
+
return {
|
|
62
|
+
kind: "array",
|
|
63
|
+
items: Array.from({ length: type.length }, () => defaultValue(type.element, types)),
|
|
64
|
+
};
|
|
65
|
+
if (type.kind === "record")
|
|
66
|
+
return {
|
|
67
|
+
kind: "record",
|
|
68
|
+
fields: Object.fromEntries(
|
|
69
|
+
type.fields.map((field) => [field.name, defaultValue(field.type, types)]),
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
if (type.kind === "duration") return { kind: "duration", nanoseconds: "0" };
|
|
73
|
+
if (type.kind === "calendar")
|
|
74
|
+
return {
|
|
75
|
+
kind: "calendar",
|
|
76
|
+
value:
|
|
77
|
+
type.domain === "date"
|
|
78
|
+
? "1970-01-01"
|
|
79
|
+
: type.domain === "timeOfDay"
|
|
80
|
+
? "00:00:00.000"
|
|
81
|
+
: "1970-01-01T00:00:00",
|
|
82
|
+
};
|
|
83
|
+
if (type.kind === "integer" || type.kind === "bits") return { kind: "integer", value: "0" };
|
|
84
|
+
throw new Error("Unsupported default-value type");
|
|
85
|
+
}
|
|
86
|
+
export function placeType(
|
|
87
|
+
place: Place,
|
|
88
|
+
types: ReadonlyMap<string, PlcType>,
|
|
89
|
+
variables: ReadonlyMap<string, Variable>,
|
|
90
|
+
ranges: ReadonlyMap<string, IntegerInterval> = new Map(),
|
|
91
|
+
writable = false,
|
|
92
|
+
): { type?: string; issue?: string } {
|
|
93
|
+
const root = variables.get(place.root);
|
|
94
|
+
if (!root) return { issue: "Unknown storage root." };
|
|
95
|
+
if (
|
|
96
|
+
writable &&
|
|
97
|
+
(root.storage === "input" ||
|
|
98
|
+
root.storage === "constant" ||
|
|
99
|
+
root.storage === "blockOutput" ||
|
|
100
|
+
(root.storage === "parameter" && root.mode !== "inOut") ||
|
|
101
|
+
(!place.path.length && !root.mutable))
|
|
102
|
+
)
|
|
103
|
+
return { issue: "This storage is read-only." };
|
|
104
|
+
let id = root.type;
|
|
105
|
+
for (const segment of place.path) {
|
|
106
|
+
const type = types.get(id);
|
|
107
|
+
if (segment.kind === "field") {
|
|
108
|
+
if (type?.kind === "block" && segment.name === "out" && type.output) {
|
|
109
|
+
if (writable) return { issue: "Function block output views are read-only." };
|
|
110
|
+
id = type.output;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (type?.kind !== "record") return { issue: "Field access requires a record." };
|
|
114
|
+
const field = type.fields.find((field) => field.name === segment.name);
|
|
115
|
+
if (!field) return { issue: "Unknown record field." };
|
|
116
|
+
if (writable && field.readonly) return { issue: "This field is read-only." };
|
|
117
|
+
id = field.type;
|
|
118
|
+
} else {
|
|
119
|
+
if (type?.kind !== "array") return { issue: "Index access requires a fixed array." };
|
|
120
|
+
// Enum values are closed, and the layout check below proves its members are the bounds.
|
|
121
|
+
// An integer, such as a for…of counter, still needs its bounds proof.
|
|
122
|
+
if (type.index && segment.value.type === type.index) {
|
|
123
|
+
id = type.element;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const indexType = types.get(segment.value.type),
|
|
127
|
+
bounds = interval(segment.value, types, variables, ranges);
|
|
128
|
+
if (
|
|
129
|
+
indexType?.kind !== "integer" ||
|
|
130
|
+
!bounds ||
|
|
131
|
+
bounds[0] < BigInt(type.lowerBound) ||
|
|
132
|
+
bounds[1] >= BigInt(type.lowerBound + type.length)
|
|
133
|
+
)
|
|
134
|
+
return { issue: "Array index must be proved within the declared bounds." };
|
|
135
|
+
id = type.element;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { type: id };
|
|
139
|
+
}
|
|
140
|
+
export function aggregateIssue(
|
|
141
|
+
value: Expression,
|
|
142
|
+
types: ReadonlyMap<string, PlcType>,
|
|
143
|
+
): string | undefined {
|
|
144
|
+
const type = types.get(value.type);
|
|
145
|
+
if (!type) return "Unknown expression type.";
|
|
146
|
+
if (value.kind === "constant")
|
|
147
|
+
return valueFits(value.value, type, types)
|
|
148
|
+
? undefined
|
|
149
|
+
: "Constant does not fit its closed PLC layout.";
|
|
150
|
+
if (value.kind === "record")
|
|
151
|
+
return type.kind === "record" &&
|
|
152
|
+
Object.keys(value.fields).length === type.fields.length &&
|
|
153
|
+
type.fields.every(
|
|
154
|
+
(field) =>
|
|
155
|
+
Object.hasOwn(value.fields, field.name) && value.fields[field.name]?.type === field.type,
|
|
156
|
+
)
|
|
157
|
+
? undefined
|
|
158
|
+
: "Record expression differs from its layout.";
|
|
159
|
+
if (value.kind === "array")
|
|
160
|
+
return type.kind === "array" &&
|
|
161
|
+
value.items.length === type.length &&
|
|
162
|
+
value.items.every((item) => item.type === type.element)
|
|
163
|
+
? undefined
|
|
164
|
+
: "Array expression differs from its layout.";
|
|
165
|
+
if (value.kind === "copy")
|
|
166
|
+
return aggregate(type) && value.operand.type === value.type
|
|
167
|
+
? undefined
|
|
168
|
+
: "Copy requires one matching fixed aggregate.";
|
|
169
|
+
if (value.kind === "convert") {
|
|
170
|
+
const source = types.get(value.operand.type);
|
|
171
|
+
return type.kind === "string" &&
|
|
172
|
+
source?.kind === "string" &&
|
|
173
|
+
source.encoding === type.encoding &&
|
|
174
|
+
source.capacity <= type.capacity &&
|
|
175
|
+
value.policy === "exact"
|
|
176
|
+
? undefined
|
|
177
|
+
: "Unsupported aggregate/domain conversion.";
|
|
178
|
+
}
|
|
179
|
+
if (value.kind === "load" || value.kind === "call" || value.kind === "conditional")
|
|
180
|
+
return undefined;
|
|
181
|
+
if (value.kind === "binary" && comparisonOperators.has(value.operator)) {
|
|
182
|
+
const left = types.get(value.left.type);
|
|
183
|
+
return value.type === "BOOL" &&
|
|
184
|
+
left &&
|
|
185
|
+
["enum", "string"].includes(left.kind) &&
|
|
186
|
+
value.left.type === value.right.type &&
|
|
187
|
+
(left.kind !== "enum" || ["equal", "notEqual"].includes(value.operator))
|
|
188
|
+
? undefined
|
|
189
|
+
: "Comparison requires compatible scalar domains; object identity is unsupported.";
|
|
190
|
+
}
|
|
191
|
+
return "Operation is unsupported for this PLC domain.";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Route a reduced expression to the checker that owns its PLC domain. A binary
|
|
195
|
+
// operation belongs to its operand domain, e.g. a TIME comparison yields BOOL.
|
|
196
|
+
export function expressionIssue(
|
|
197
|
+
value: Expression,
|
|
198
|
+
types: ReadonlyMap<string, PlcType>,
|
|
199
|
+
variables: ReadonlyMap<string, Variable>,
|
|
200
|
+
ranges: ReadonlyMap<string, IntegerInterval>,
|
|
201
|
+
): string | undefined {
|
|
202
|
+
const kinds = [value.type, ...(value.kind === "binary" ? [value.left.type] : [])].map(
|
|
203
|
+
(id) => types.get(id)?.kind ?? "",
|
|
204
|
+
);
|
|
205
|
+
if (kinds.some((kind) => ["duration", "calendar"].includes(kind)))
|
|
206
|
+
return domainIssue(value, types);
|
|
207
|
+
if (
|
|
208
|
+
["copy", "record", "array"].includes(value.kind) ||
|
|
209
|
+
kinds.some((kind) => ["record", "array", "enum", "string"].includes(kind))
|
|
210
|
+
)
|
|
211
|
+
return aggregateIssue(value, types);
|
|
212
|
+
return scalarIssue(value, types, variables, ranges);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Validate all layout edges once before constants or places can traverse them.
|
|
216
|
+
export function layoutIssue(types: ReadonlyMap<string, PlcType>): string | undefined {
|
|
217
|
+
for (const type of types.values()) {
|
|
218
|
+
const canonical = primitiveTypes.find((item) => item.id === type.id);
|
|
219
|
+
if (canonical) {
|
|
220
|
+
if (
|
|
221
|
+
type.kind !== canonical.kind ||
|
|
222
|
+
("bits" in canonical && (!("bits" in type) || type.bits !== canonical.bits)) ||
|
|
223
|
+
("signed" in canonical && (!("signed" in type) || type.signed !== canonical.signed)) ||
|
|
224
|
+
("domain" in canonical && (!("domain" in type) || type.domain !== canonical.domain))
|
|
225
|
+
)
|
|
226
|
+
return `Conflicting primitive type ${type.id}`;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (type.kind === "record") {
|
|
230
|
+
if (
|
|
231
|
+
!type.fields.length ||
|
|
232
|
+
new Set(type.fields.map((field) => field.name)).size !== type.fields.length ||
|
|
233
|
+
type.fields.some(
|
|
234
|
+
(field) =>
|
|
235
|
+
!field.name ||
|
|
236
|
+
typeof field.readonly !== "boolean" ||
|
|
237
|
+
(field.mode && field.mode !== "inOut") ||
|
|
238
|
+
!types.has(field.type),
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
return `Invalid closed record layout ${type.id}`;
|
|
242
|
+
} else if (type.kind === "array") {
|
|
243
|
+
if (
|
|
244
|
+
!types.has(type.element) ||
|
|
245
|
+
!Number.isSafeInteger(type.length) ||
|
|
246
|
+
type.length < 1 ||
|
|
247
|
+
type.length > 1_000_000 ||
|
|
248
|
+
!Number.isInteger(type.lowerBound) ||
|
|
249
|
+
type.lowerBound < -2147483648 ||
|
|
250
|
+
type.lowerBound + type.length - 1 > 2147483647
|
|
251
|
+
)
|
|
252
|
+
return `Invalid fixed array layout ${type.id}`;
|
|
253
|
+
if (type.index !== undefined) {
|
|
254
|
+
const index = types.get(type.index);
|
|
255
|
+
if (
|
|
256
|
+
index?.kind !== "enum" ||
|
|
257
|
+
index.members.length !== type.length ||
|
|
258
|
+
index.members.some(
|
|
259
|
+
(member) =>
|
|
260
|
+
Number(member.value) < type.lowerBound ||
|
|
261
|
+
Number(member.value) >= type.lowerBound + type.length,
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
return `Invalid enum-keyed array layout ${type.id}`;
|
|
265
|
+
}
|
|
266
|
+
} else if (type.kind === "enum") {
|
|
267
|
+
if (
|
|
268
|
+
type.base !== "DINT" ||
|
|
269
|
+
types.get(type.base)?.kind !== "integer" ||
|
|
270
|
+
!type.members.length ||
|
|
271
|
+
new Set(type.members.map((member) => member.name)).size !== type.members.length ||
|
|
272
|
+
new Set(type.members.map((member) => member.value)).size !== type.members.length ||
|
|
273
|
+
type.members.some(
|
|
274
|
+
(member) =>
|
|
275
|
+
!member.name ||
|
|
276
|
+
!constantFits({ kind: "integer", value: member.value }, types.get(type.base)!),
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
return `Invalid enum layout ${type.id}`;
|
|
280
|
+
} else if (type.kind === "string") {
|
|
281
|
+
if (
|
|
282
|
+
!Number.isInteger(type.capacity) ||
|
|
283
|
+
type.capacity < 1 ||
|
|
284
|
+
type.capacity > 65535 ||
|
|
285
|
+
!["utf8", "utf16"].includes(type.encoding)
|
|
286
|
+
)
|
|
287
|
+
return `Invalid bounded string layout ${type.id}`;
|
|
288
|
+
} else if (type.kind === "block") {
|
|
289
|
+
if (!type.block || (type.output && types.get(type.output)?.kind !== "record"))
|
|
290
|
+
return `Invalid block type ${type.id}`;
|
|
291
|
+
} else return `Unsupported core type definition ${type.id}`;
|
|
292
|
+
}
|
|
293
|
+
const active = new Set<string>(),
|
|
294
|
+
layouts = new Map<string, { size: number; height: number }>();
|
|
295
|
+
const count = (id: string, depth: number): { size: number; height: number } => {
|
|
296
|
+
if (active.has(id) || depth > 64)
|
|
297
|
+
throw new Error(`Recursive or excessively nested by-value layout ${id}`);
|
|
298
|
+
const cached = layouts.get(id);
|
|
299
|
+
if (cached) {
|
|
300
|
+
if (depth + cached.height > 64) throw new Error(`Layout nesting exceeds 64 levels ${id}`);
|
|
301
|
+
return cached;
|
|
302
|
+
}
|
|
303
|
+
active.add(id);
|
|
304
|
+
const type = types.get(id)!;
|
|
305
|
+
const children =
|
|
306
|
+
type.kind === "array"
|
|
307
|
+
? [count(type.element, depth + 1)]
|
|
308
|
+
: type.kind === "record"
|
|
309
|
+
? type.fields.map((field) => count(field.type, depth + 1))
|
|
310
|
+
: [];
|
|
311
|
+
const size =
|
|
312
|
+
type.kind === "array"
|
|
313
|
+
? type.length * children[0]!.size
|
|
314
|
+
: type.kind === "record"
|
|
315
|
+
? children.reduce((sum, child) => sum + child.size, 0)
|
|
316
|
+
: 1;
|
|
317
|
+
const height = children.length
|
|
318
|
+
? 1 + children.reduce((maximum, child) => Math.max(maximum, child.height), 0)
|
|
319
|
+
: 0;
|
|
320
|
+
active.delete(id);
|
|
321
|
+
if (size > 1_000_000) throw new Error(`Layout exceeds one million scalar elements ${id}`);
|
|
322
|
+
if (depth + height > 64) throw new Error(`Layout nesting exceeds 64 levels ${id}`);
|
|
323
|
+
const layout = { size, height };
|
|
324
|
+
layouts.set(id, layout);
|
|
325
|
+
return layout;
|
|
326
|
+
};
|
|
327
|
+
try {
|
|
328
|
+
for (const id of types.keys()) count(id, 0);
|
|
329
|
+
} catch (cause) {
|
|
330
|
+
return cause instanceof Error ? cause.message : "Invalid layout";
|
|
331
|
+
}
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
package/ir/allocation.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { PlcModule } from "./model.ts";
|
|
2
|
+
import type { ModuleDeclarations } from "./declarations.ts";
|
|
3
|
+
import { reject } from "./normalization-error.ts";
|
|
4
|
+
import { instancedBlock } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export function checkStaticAllocation(module: PlcModule, declarations: ModuleDeclarations): void {
|
|
7
|
+
const { types, blocks } = declarations;
|
|
8
|
+
|
|
9
|
+
// Declaration admission closes these registries over every referenced type
|
|
10
|
+
// and block before allocation starts; a missing entry cannot be authored here.
|
|
11
|
+
// Static allocation is a separate graph from scan calls: even an unused child
|
|
12
|
+
// consumes memory, and a recursive child cannot be allocated by value.
|
|
13
|
+
const payloads = new Map<string, number>();
|
|
14
|
+
const payload = (id: string): number => {
|
|
15
|
+
const cached = payloads.get(id);
|
|
16
|
+
if (cached !== undefined) return cached;
|
|
17
|
+
const type = types.get(id)!;
|
|
18
|
+
const size =
|
|
19
|
+
type.kind === "array"
|
|
20
|
+
? type.length * payload(type.element)
|
|
21
|
+
: type.kind === "record"
|
|
22
|
+
? type.fields.reduce((sum, field) => sum + payload(field.type), 0)
|
|
23
|
+
: 1;
|
|
24
|
+
payloads.set(id, size);
|
|
25
|
+
return size;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const instanceSize = (id: string): number => {
|
|
29
|
+
const type = types.get(id)!;
|
|
30
|
+
return type.kind === "array"
|
|
31
|
+
? type.length * instanceSize(type.element)
|
|
32
|
+
: allocation(type.kind === "block" ? type.block : "").size;
|
|
33
|
+
};
|
|
34
|
+
const allocations = new Map<string, { size: number; height: number }>(),
|
|
35
|
+
allocating = new Set<string>();
|
|
36
|
+
const allocation = (id: string): { size: number; height: number } => {
|
|
37
|
+
if (allocating.size >= 64)
|
|
38
|
+
reject("Static block allocation nesting exceeds 64 levels.", blocks.get(id)?.span);
|
|
39
|
+
if (allocating.has(id)) reject("Recursive static block allocation.", blocks.get(id)?.span);
|
|
40
|
+
const cached = allocations.get(id);
|
|
41
|
+
if (cached) return cached;
|
|
42
|
+
allocating.add(id);
|
|
43
|
+
const block = blocks.get(id)!;
|
|
44
|
+
const children = block.state
|
|
45
|
+
.filter((item) => item.storage === "instance")
|
|
46
|
+
.map((item) => ({
|
|
47
|
+
height: allocation(instancedBlock(item.type, types)!.block).height,
|
|
48
|
+
size: instanceSize(item.type),
|
|
49
|
+
}));
|
|
50
|
+
const own = [
|
|
51
|
+
...block.inputs,
|
|
52
|
+
...block.outputs,
|
|
53
|
+
...block.state.filter((item) => item.storage !== "instance"),
|
|
54
|
+
].reduce((sum, item) => sum + (item.mode === "inOut" ? 1 : payload(item.type)), 1);
|
|
55
|
+
const size = own + children.reduce((sum, child) => sum + child.size, 0),
|
|
56
|
+
height = children.length ? 1 + Math.max(...children.map((child) => child.height)) : 1;
|
|
57
|
+
if (size > 1_000_000 || height > 64)
|
|
58
|
+
reject("Static block allocation exceeds its finite budget.", block.span);
|
|
59
|
+
allocating.delete(id);
|
|
60
|
+
const result = { size, height };
|
|
61
|
+
allocations.set(id, result);
|
|
62
|
+
return result;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
for (const block of module.blocks) allocation(block.id);
|
|
66
|
+
const staticPayload = module.variables.reduce((sum, variable) => {
|
|
67
|
+
return (
|
|
68
|
+
sum +
|
|
69
|
+
(instancedBlock(variable.type, types) ? instanceSize(variable.type) : payload(variable.type))
|
|
70
|
+
);
|
|
71
|
+
}, 0);
|
|
72
|
+
if (staticPayload > 1_000_000)
|
|
73
|
+
reject("Static module storage exceeds one million scalar elements.");
|
|
74
|
+
}
|
package/ir/core-model.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { SourceSpan } from "./diagnostics.ts";
|
|
2
|
+
import type {
|
|
3
|
+
Variable,
|
|
4
|
+
Constant,
|
|
5
|
+
BinaryOperator,
|
|
6
|
+
FunctionBlock,
|
|
7
|
+
PlcProgram,
|
|
8
|
+
MethodOwner,
|
|
9
|
+
} from "./model.ts";
|
|
10
|
+
import type { PlcType } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
// Core expressions contain no source-level calls. Their effects have already
|
|
13
|
+
// become ordered statements, and every place has passed storage/bounds checks.
|
|
14
|
+
export type CorePath = readonly (
|
|
15
|
+
| { readonly kind: "field"; readonly name: string }
|
|
16
|
+
| { readonly kind: "index"; readonly value: CoreExpression }
|
|
17
|
+
)[];
|
|
18
|
+
export interface CheckedPlace {
|
|
19
|
+
readonly root: string;
|
|
20
|
+
readonly path: CorePath;
|
|
21
|
+
}
|
|
22
|
+
export type CoreExpression = { readonly type: string; readonly span: SourceSpan } & (
|
|
23
|
+
| { readonly kind: "borrow"; readonly place: CheckedPlace }
|
|
24
|
+
| { readonly kind: "constant"; readonly value: Constant }
|
|
25
|
+
| { readonly kind: "load"; readonly root: string; readonly path?: CorePath }
|
|
26
|
+
| {
|
|
27
|
+
readonly kind: "unary";
|
|
28
|
+
readonly operator: "not" | "negate" | "bitNot" | "timeToMilliseconds" | "clock";
|
|
29
|
+
readonly operand: CoreExpression;
|
|
30
|
+
}
|
|
31
|
+
| {
|
|
32
|
+
readonly kind: "binary";
|
|
33
|
+
readonly operator: BinaryOperator;
|
|
34
|
+
readonly left: CoreExpression;
|
|
35
|
+
readonly right: CoreExpression;
|
|
36
|
+
readonly policy?: "wrap" | "saturate";
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
readonly kind: "convert";
|
|
40
|
+
readonly operand: CoreExpression;
|
|
41
|
+
readonly policy: "exact" | "wrap" | "saturate";
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
export type CoreStatement = { readonly span: SourceSpan } & (
|
|
45
|
+
| {
|
|
46
|
+
readonly kind: "assign";
|
|
47
|
+
readonly root: string;
|
|
48
|
+
readonly path?: CorePath;
|
|
49
|
+
readonly value: CoreExpression;
|
|
50
|
+
}
|
|
51
|
+
| {
|
|
52
|
+
readonly kind: "if";
|
|
53
|
+
readonly condition: CoreExpression;
|
|
54
|
+
readonly then: readonly CoreStatement[];
|
|
55
|
+
readonly else: readonly CoreStatement[];
|
|
56
|
+
}
|
|
57
|
+
| {
|
|
58
|
+
readonly kind: "switch";
|
|
59
|
+
readonly value: CoreExpression;
|
|
60
|
+
readonly cases: readonly { values: readonly Constant[]; body: readonly CoreStatement[] }[];
|
|
61
|
+
readonly fallback: readonly CoreStatement[];
|
|
62
|
+
}
|
|
63
|
+
| {
|
|
64
|
+
readonly kind: "for";
|
|
65
|
+
readonly counter: string;
|
|
66
|
+
readonly counterScope?: "existing";
|
|
67
|
+
readonly start: Constant;
|
|
68
|
+
readonly limit: Constant;
|
|
69
|
+
readonly step: string;
|
|
70
|
+
readonly dynamic?: {
|
|
71
|
+
readonly start: CoreExpression;
|
|
72
|
+
readonly limit: CoreExpression;
|
|
73
|
+
readonly exclusive?: true;
|
|
74
|
+
};
|
|
75
|
+
readonly maxIterations: number;
|
|
76
|
+
readonly body: readonly CoreStatement[];
|
|
77
|
+
}
|
|
78
|
+
| {
|
|
79
|
+
readonly kind: "invoke";
|
|
80
|
+
readonly function: string;
|
|
81
|
+
readonly instance?: string;
|
|
82
|
+
readonly index?: CoreExpression;
|
|
83
|
+
readonly arguments: readonly CoreExpression[];
|
|
84
|
+
readonly destination?: string;
|
|
85
|
+
}
|
|
86
|
+
| {
|
|
87
|
+
readonly kind: "blockUpdate";
|
|
88
|
+
readonly instance: string;
|
|
89
|
+
readonly index?: CoreExpression;
|
|
90
|
+
readonly inputs: Readonly<Record<string, CoreExpression>>;
|
|
91
|
+
}
|
|
92
|
+
| { readonly kind: "return"; readonly value?: CoreExpression }
|
|
93
|
+
| { readonly kind: "break" | "continue" }
|
|
94
|
+
);
|
|
95
|
+
export interface CoreFunction {
|
|
96
|
+
readonly owner?: MethodOwner;
|
|
97
|
+
readonly id: string;
|
|
98
|
+
readonly name: string;
|
|
99
|
+
readonly parameters: readonly Variable[];
|
|
100
|
+
readonly result?: string;
|
|
101
|
+
readonly locals: readonly Variable[];
|
|
102
|
+
readonly body: readonly CoreStatement[];
|
|
103
|
+
readonly span: SourceSpan;
|
|
104
|
+
}
|
|
105
|
+
export interface CoreFunctionBlock extends Readonly<Omit<FunctionBlock, "body">> {
|
|
106
|
+
readonly body: readonly CoreStatement[];
|
|
107
|
+
}
|
|
108
|
+
export const verified: unique symbol = Symbol("verified core");
|
|
109
|
+
export interface VerifiedCoreModule {
|
|
110
|
+
readonly [verified]: true;
|
|
111
|
+
readonly types: readonly PlcType[];
|
|
112
|
+
readonly variables: readonly Variable[];
|
|
113
|
+
readonly functions: readonly CoreFunction[];
|
|
114
|
+
readonly blocks: readonly CoreFunctionBlock[];
|
|
115
|
+
readonly programs: readonly PlcProgram[];
|
|
116
|
+
}
|
package/ir/core.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Diagnostic } from "./diagnostics.ts";
|
|
2
|
+
import type { PlcModule } from "./model.ts";
|
|
3
|
+
import {
|
|
4
|
+
verified,
|
|
5
|
+
type VerifiedCoreModule,
|
|
6
|
+
type CoreFunction,
|
|
7
|
+
type CoreFunctionBlock,
|
|
8
|
+
} from "./core-model.ts";
|
|
9
|
+
import { ModuleDeclarations } from "./declarations.ts";
|
|
10
|
+
import { checkStaticAllocation } from "./allocation.ts";
|
|
11
|
+
import { RoutineNormalizer } from "./routine.ts";
|
|
12
|
+
import { linkModule } from "./link.ts";
|
|
13
|
+
import { InvalidCore } from "./normalization-error.ts";
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
CorePath,
|
|
17
|
+
CheckedPlace,
|
|
18
|
+
CoreExpression,
|
|
19
|
+
CoreStatement,
|
|
20
|
+
CoreFunction,
|
|
21
|
+
CoreFunctionBlock,
|
|
22
|
+
VerifiedCoreModule,
|
|
23
|
+
} from "./core-model.ts";
|
|
24
|
+
|
|
25
|
+
const verifiedModules = new WeakSet<object>();
|
|
26
|
+
export function isVerifiedCore(value: unknown): value is VerifiedCoreModule {
|
|
27
|
+
return typeof value === "object" && value !== null && verifiedModules.has(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function freeze(value: unknown): void {
|
|
31
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value)) return;
|
|
32
|
+
for (const child of Object.values(value)) freeze(child);
|
|
33
|
+
Object.freeze(value);
|
|
34
|
+
}
|
|
35
|
+
// The module crosses the trust boundary only after admission, lowering and linking.
|
|
36
|
+
export function normalizeModule(module: PlcModule): {
|
|
37
|
+
core?: VerifiedCoreModule;
|
|
38
|
+
diagnostics: readonly Diagnostic[];
|
|
39
|
+
} {
|
|
40
|
+
try {
|
|
41
|
+
const declarations = new ModuleDeclarations(module);
|
|
42
|
+
checkStaticAllocation(module, declarations);
|
|
43
|
+
|
|
44
|
+
const functions: CoreFunction[] = [],
|
|
45
|
+
blocks: CoreFunctionBlock[] = [];
|
|
46
|
+
const edges = new Map<string, ReadonlySet<string>>();
|
|
47
|
+
for (const source of [...module.functions, ...module.blocks]) {
|
|
48
|
+
const normalized = new RoutineNormalizer(source, declarations).normalize();
|
|
49
|
+
edges.set(source.id, normalized.callees);
|
|
50
|
+
if ("inputs" in normalized.routine) blocks.push(normalized.routine);
|
|
51
|
+
else functions.push(normalized.routine);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const linked = linkModule(module, declarations, functions, blocks, edges);
|
|
55
|
+
const core: VerifiedCoreModule = { ...structuredClone(linked), [verified]: true };
|
|
56
|
+
freeze(core);
|
|
57
|
+
verifiedModules.add(core);
|
|
58
|
+
return { core, diagnostics: [] };
|
|
59
|
+
} catch (cause) {
|
|
60
|
+
if (cause instanceof InvalidCore) return { diagnostics: [cause.diagnostic] };
|
|
61
|
+
throw cause;
|
|
62
|
+
}
|
|
63
|
+
}
|