@tsplc/compiler 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +16 -2
- package/backends/codesys/config.ts +54 -0
- package/backends/codesys/emit.ts +1063 -0
- package/backends/codesys/engineering.ts +88 -0
- package/backends/codesys/http.ts +598 -0
- package/backends/codesys/index.ts +12 -0
- package/backends/codesys/io-paths.ts +103 -0
- package/backends/codesys/names.ts +76 -0
- package/backends/codesys/numeric.ts +356 -0
- package/backends/codesys/plan.ts +512 -0
- package/backends/codesys/publication.ts +59 -0
- package/backends/codesys/storage.ts +63 -0
- package/backends/codesys/task-access.ts +118 -0
- package/backends/codesys/types.ts +178 -0
- package/backends/codesys/vendor.ts +108 -0
- package/dist/cli.js +1396 -0
- package/dist/cli.js.map +16 -0
- package/dist/clock.js +8 -0
- package/dist/clock.js.map +10 -0
- package/dist/codesys.js +2587 -0
- package/dist/codesys.js.map +22 -0
- package/dist/elaborate-capture.js +118 -0
- package/dist/elaborate-capture.js.map +10 -0
- package/dist/index.js +6859 -0
- package/dist/index.js.map +25 -0
- package/dist/ir.js +1888 -0
- package/dist/ir.js.map +22 -0
- package/dist/math.js +24 -0
- package/dist/math.js.map +10 -0
- package/dist/project.js +429 -0
- package/dist/project.js.map +11 -0
- package/ir/aggregates.ts +333 -0
- package/ir/allocation.ts +74 -0
- package/ir/core-model.ts +116 -0
- package/ir/core.ts +63 -0
- package/ir/declarations.ts +201 -0
- package/ir/diagnostics.ts +43 -0
- package/ir/domains.ts +96 -0
- package/ir/index.ts +7 -0
- package/ir/link.ts +176 -0
- package/ir/model.ts +268 -0
- package/ir/normalization-error.ts +10 -0
- package/ir/routine.ts +826 -0
- package/ir/scalars.ts +675 -0
- package/ir/types.ts +70 -0
- package/package.json +106 -4
- package/project/artifacts.ts +277 -0
- package/project/cli.ts +305 -0
- package/project/index.ts +1 -0
- package/project/plant.ts +437 -0
- package/project/project.ts +312 -0
- package/project/server.ts +83 -0
- package/src/frontend/async.ts +1060 -0
- package/src/frontend/blocks.ts +419 -0
- package/src/frontend/configuration.ts +167 -0
- package/src/frontend/const-functions.ts +153 -0
- package/src/frontend/cooperative.ts +75 -0
- package/src/frontend/elaborate/capture.ts +138 -0
- package/src/frontend/elaborate/graph.ts +94 -0
- package/src/frontend/elaborate/index.ts +302 -0
- package/src/frontend/elaborate/layouts.ts +390 -0
- package/src/frontend/elaborate/model.ts +177 -0
- package/src/frontend/elaborate/specialize.ts +2280 -0
- package/src/frontend/exposure.ts +147 -0
- package/src/frontend/module-graph.ts +63 -0
- package/src/frontend/promises.ts +751 -0
- package/src/frontend/services.ts +435 -0
- package/src/frontend/types.ts +578 -0
- package/src/frontend/typescript.ts +2442 -0
- package/src/index.ts +7 -0
- package/stdlib/clock.ts +2 -0
- package/stdlib/math.ts +19 -0
- package/types/async.d.ts +81 -0
- package/types/clock.d.ts +4 -0
- package/types/config.d.ts +2 -0
- package/types/data-types.d.ts +31 -0
- package/types/globals.d.ts +40 -0
- package/types/index.d.ts +111 -0
- package/types/plc.d.ts +5 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { dirname, relative, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
instancedBlock,
|
|
5
|
+
primitiveTypes,
|
|
6
|
+
parseInteger,
|
|
7
|
+
type PlcType,
|
|
8
|
+
type Field,
|
|
9
|
+
type MathHelper,
|
|
10
|
+
} from "@tsplc/compiler/ir";
|
|
11
|
+
|
|
12
|
+
export interface ResolvedType {
|
|
13
|
+
type: string;
|
|
14
|
+
io?: "input" | "output";
|
|
15
|
+
mode?: "inOut";
|
|
16
|
+
}
|
|
17
|
+
export type TypeArgument =
|
|
18
|
+
{ kind: "type"; value: ResolvedType } | { kind: "integer"; value: number };
|
|
19
|
+
export type TypeEnvironment = ReadonlyMap<ts.Symbol, TypeArgument>;
|
|
20
|
+
export type Fail = (node: ts.Node, code: string, message: string) => never;
|
|
21
|
+
|
|
22
|
+
// The PLC type of an unannotated numeric literal: `number` means DINT unless it is fractional.
|
|
23
|
+
export const numericLiteralType = (text: string) =>
|
|
24
|
+
text.endsWith("n") ? "LINT" : /[.e]/i.test(text) && !/^[-]?0[xob]/i.test(text) ? "LREAL" : "DINT";
|
|
25
|
+
|
|
26
|
+
// The statically named callee of `name(...)` or `namespace.name(...)`.
|
|
27
|
+
export function calleeName(node: ts.Expression): ts.MemberName | undefined {
|
|
28
|
+
if (ts.isIdentifier(node)) return node;
|
|
29
|
+
return ts.isPropertyAccessExpression(node) ? node.name : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// This is the sole owner of TypeScript annotation substitution and PLC layouts.
|
|
33
|
+
// The checker resolves declarations; it never supplies erased numeric widths.
|
|
34
|
+
// Service records are the exception: `iec.*` schemas carry their widths as literal types.
|
|
35
|
+
export class TypeResolver {
|
|
36
|
+
readonly types = new Map<string, PlcType>(primitiveTypes.map((type) => [type.id, type]));
|
|
37
|
+
readonly blockTypes = new Map<ts.Symbol, string>();
|
|
38
|
+
readonly standardTypes = new Map<string, string>();
|
|
39
|
+
readonly unions = new Set<string>();
|
|
40
|
+
private readonly active = new Set<string>();
|
|
41
|
+
constructor(
|
|
42
|
+
private readonly checker: ts.TypeChecker,
|
|
43
|
+
private readonly root: string,
|
|
44
|
+
private readonly sdk: string,
|
|
45
|
+
private readonly fail: Fail,
|
|
46
|
+
/** The service client's struct for a type naming a called procedure's record. */
|
|
47
|
+
private readonly serviceRecord?: (type: ts.Type) => ts.Symbol | undefined,
|
|
48
|
+
) {}
|
|
49
|
+
symbol(node: ts.Node): ts.Symbol {
|
|
50
|
+
// An object shorthand has a property symbol and a separate value binding.
|
|
51
|
+
// PLC loads and intrinsic provenance must follow the value, including imports.
|
|
52
|
+
let symbol =
|
|
53
|
+
ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node
|
|
54
|
+
? this.checker.getShorthandAssignmentValueSymbol(node.parent)
|
|
55
|
+
: this.checker.getSymbolAtLocation(node);
|
|
56
|
+
if (!symbol) this.fail(node, "PLC1002", "This name has no PLC declaration.");
|
|
57
|
+
while (symbol.flags & ts.SymbolFlags.Alias) symbol = this.checker.getAliasedSymbol(symbol);
|
|
58
|
+
return symbol;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Declared by TypeScript's default library, such as `Record` or `Object`. */
|
|
62
|
+
standardLibrary(symbol: ts.Symbol): boolean {
|
|
63
|
+
return Boolean(
|
|
64
|
+
symbol.declarations?.every(
|
|
65
|
+
(declaration) =>
|
|
66
|
+
dirname(declaration.getSourceFile().fileName) === dirname(ts.getDefaultLibFilePath({})),
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
sdkName(node: ts.Node): string | undefined {
|
|
72
|
+
const symbol = this.symbol(node);
|
|
73
|
+
return symbol.declarations?.some((declaration) =>
|
|
74
|
+
declaration.getSourceFile().fileName.startsWith(`${dirname(this.sdk)}/`),
|
|
75
|
+
)
|
|
76
|
+
? symbol.getName()
|
|
77
|
+
: undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `min`, `max` or `clamp` from the SDK's `@tsplc/compiler/math`. */
|
|
81
|
+
mathHelper(node: ts.Node, name: string): MathHelper | undefined {
|
|
82
|
+
return resolve(node.getSourceFile().fileName) ===
|
|
83
|
+
resolve(dirname(this.sdk), "../stdlib/math.ts") &&
|
|
84
|
+
(name === "min" || name === "max" || name === "clamp")
|
|
85
|
+
? name
|
|
86
|
+
: undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
id(node: ts.Node, name: string): string {
|
|
90
|
+
return `${relative(this.root, node.getSourceFile().fileName).replaceAll("\\", "/")}:${node.getStart()}:${name}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
argument(node: ts.TypeNode, environment: TypeEnvironment): TypeArgument {
|
|
94
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
95
|
+
const bound = environment.get(this.symbol(node.typeName));
|
|
96
|
+
if (bound && !node.typeArguments?.length) return bound;
|
|
97
|
+
}
|
|
98
|
+
// `(typeof table)["length"]` or `typeof table.length` has the literal type of an `as const` table.
|
|
99
|
+
const queried =
|
|
100
|
+
ts.isIndexedAccessTypeNode(node) || ts.isTypeQueryNode(node)
|
|
101
|
+
? this.checker.getTypeFromTypeNode(node)
|
|
102
|
+
: undefined;
|
|
103
|
+
const text = queried?.isNumberLiteral()
|
|
104
|
+
? String(queried.value)
|
|
105
|
+
: ts.isLiteralTypeNode(node) &&
|
|
106
|
+
(ts.isNumericLiteral(node.literal) || ts.isPrefixUnaryExpression(node.literal))
|
|
107
|
+
? node.literal.getText()
|
|
108
|
+
: undefined;
|
|
109
|
+
if (text !== undefined) {
|
|
110
|
+
let value: bigint;
|
|
111
|
+
try {
|
|
112
|
+
value = parseInteger(text);
|
|
113
|
+
} catch {
|
|
114
|
+
this.fail(node, "PLC2001", "Layout arguments must be exact integer literals.");
|
|
115
|
+
}
|
|
116
|
+
if (value < -2147483648n || value > 2147483647n)
|
|
117
|
+
this.fail(node, "PLC2001", "Layout argument is outside signed DINT range.");
|
|
118
|
+
return { kind: "integer", value: Number(value) };
|
|
119
|
+
}
|
|
120
|
+
return { kind: "type", value: this.resolve(node, environment) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
bindings(
|
|
124
|
+
parameters: readonly ts.TypeParameterDeclaration[] | undefined,
|
|
125
|
+
arguments_: readonly ts.TypeNode[] | undefined,
|
|
126
|
+
outer: TypeEnvironment,
|
|
127
|
+
node: ts.Node,
|
|
128
|
+
): Map<ts.Symbol, TypeArgument> {
|
|
129
|
+
const result = new Map(outer),
|
|
130
|
+
params = parameters ?? [],
|
|
131
|
+
args = arguments_ ?? [];
|
|
132
|
+
if (args.length > params.length)
|
|
133
|
+
this.fail(node, "PLC2001", "Too many concrete type arguments.");
|
|
134
|
+
params.forEach((parameter, index) => {
|
|
135
|
+
const argument = args[index] ?? parameter.default;
|
|
136
|
+
if (!argument)
|
|
137
|
+
this.fail(
|
|
138
|
+
node,
|
|
139
|
+
"PLC2001",
|
|
140
|
+
"Generic PLC declarations require explicit concrete type arguments.",
|
|
141
|
+
);
|
|
142
|
+
result.set(this.symbol(parameter.name), this.argument(argument, result));
|
|
143
|
+
});
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
resolve(node: ts.TypeNode, environment: TypeEnvironment = new Map()): ResolvedType {
|
|
148
|
+
if (node.kind === ts.SyntaxKind.BooleanKeyword) return { type: "BOOL" };
|
|
149
|
+
if (node.kind === ts.SyntaxKind.NumberKeyword) return { type: "DINT" };
|
|
150
|
+
if (node.kind === ts.SyntaxKind.StringKeyword) {
|
|
151
|
+
const id = "STRING<80>";
|
|
152
|
+
this.types.set(id, { id, kind: "string", capacity: 80, encoding: "utf8" });
|
|
153
|
+
return { type: id };
|
|
154
|
+
}
|
|
155
|
+
if (ts.isParenthesizedTypeNode(node)) return this.resolve(node.type, environment);
|
|
156
|
+
if (ts.isTypeLiteralNode(node)) {
|
|
157
|
+
// Environment keys are admitted, declared type parameters with concrete bindings.
|
|
158
|
+
const arguments_ = [...environment]
|
|
159
|
+
.map(
|
|
160
|
+
([symbol, value]) =>
|
|
161
|
+
`${this.id(symbol.declarations![0]!, symbol.name)}=${this.key(value)}`,
|
|
162
|
+
)
|
|
163
|
+
.sort()
|
|
164
|
+
.join(",");
|
|
165
|
+
return this.record(
|
|
166
|
+
node,
|
|
167
|
+
`${this.id(node, "record")}${arguments_ ? `<${arguments_}>` : ""}`,
|
|
168
|
+
node.members,
|
|
169
|
+
environment,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (ts.isUnionTypeNode(node)) return this.union(node, this.id(node, "Union"), environment);
|
|
173
|
+
if (ts.isIndexedAccessTypeNode(node)) {
|
|
174
|
+
const record = this.serviceRecord?.(this.checker.getTypeFromTypeNode(node));
|
|
175
|
+
if (record) return this.declaration(record, undefined, environment, node);
|
|
176
|
+
}
|
|
177
|
+
if (!ts.isTypeReferenceNode(node))
|
|
178
|
+
this.fail(
|
|
179
|
+
node,
|
|
180
|
+
"PLC2001",
|
|
181
|
+
"Use an explicit supported PLC type; optional and dynamic layouts are unsupported.",
|
|
182
|
+
);
|
|
183
|
+
const symbol = this.symbol(node.typeName),
|
|
184
|
+
bound = environment.get(symbol);
|
|
185
|
+
if (bound) {
|
|
186
|
+
if (bound.kind !== "type" || node.typeArguments?.length)
|
|
187
|
+
this.fail(node, "PLC2001", "An integer layout argument is not a storage type.");
|
|
188
|
+
return bound.value;
|
|
189
|
+
}
|
|
190
|
+
const official = this.sdkName(node.typeName);
|
|
191
|
+
const block =
|
|
192
|
+
this.blockTypes.get(symbol) ?? (official ? this.standardTypes.get(official) : undefined);
|
|
193
|
+
if (block) {
|
|
194
|
+
if (node.typeArguments?.length)
|
|
195
|
+
this.fail(
|
|
196
|
+
node,
|
|
197
|
+
"PLC2001",
|
|
198
|
+
"Function block classes are concrete and take no type arguments.",
|
|
199
|
+
);
|
|
200
|
+
return { type: block };
|
|
201
|
+
}
|
|
202
|
+
if (official === "InOut") {
|
|
203
|
+
if (node.typeArguments?.length !== 1)
|
|
204
|
+
this.fail(node, "PLC2001", "InOut requires one concrete value type.");
|
|
205
|
+
const inner = this.resolve(node.typeArguments[0]!, environment);
|
|
206
|
+
if (inner.io || !this.isValue(inner))
|
|
207
|
+
this.fail(node, "PLC2001", "InOut cannot nest or borrow a block instance.");
|
|
208
|
+
return { type: inner.type, mode: "inOut" };
|
|
209
|
+
}
|
|
210
|
+
if (official && this.types.has(official)) {
|
|
211
|
+
if (node.typeArguments?.length)
|
|
212
|
+
this.fail(node, "PLC2001", "Scalar types take no type arguments.");
|
|
213
|
+
return { type: official };
|
|
214
|
+
}
|
|
215
|
+
if (official === "Input" || official === "Output") {
|
|
216
|
+
if (node.typeArguments?.length !== 1)
|
|
217
|
+
this.fail(node, "PLC2001", "I/O types need exactly one PLC type argument.");
|
|
218
|
+
const inner = this.resolve(node.typeArguments[0]!, environment);
|
|
219
|
+
if (inner.io || inner.mode || this.hasReferences(inner.type))
|
|
220
|
+
this.fail(node, "PLC2001", "I/O type wrappers cannot be nested.");
|
|
221
|
+
return { type: inner.type, io: official === "Input" ? "input" : "output" };
|
|
222
|
+
}
|
|
223
|
+
if (official === "FixedArray") {
|
|
224
|
+
const args = node.typeArguments;
|
|
225
|
+
if (!args || args.length < 2 || args.length > 3)
|
|
226
|
+
this.fail(node, "PLC2001", "FixedArray needs element, length and optional lower bound.");
|
|
227
|
+
return this.fixedArray(
|
|
228
|
+
node,
|
|
229
|
+
this.resolve(args[0]!, environment),
|
|
230
|
+
this.argument(args[1]!, environment),
|
|
231
|
+
args[2] ? this.argument(args[2], environment) : { kind: "integer", value: 0 },
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
// TypeScript's own `Record`; its keys are a contiguous enum.
|
|
235
|
+
if (symbol.name === "Record" && this.standardLibrary(symbol)) {
|
|
236
|
+
const [key, value] = node.typeArguments ?? [];
|
|
237
|
+
const index =
|
|
238
|
+
key && ts.isTypeReferenceNode(key)
|
|
239
|
+
? this.types.get(this.resolve(key, environment).type)
|
|
240
|
+
: undefined;
|
|
241
|
+
if (index?.kind !== "enum")
|
|
242
|
+
this.fail(
|
|
243
|
+
node,
|
|
244
|
+
"PLC2001",
|
|
245
|
+
"Record keys must be a numeric PLC enum; dynamic index signatures are unsupported.",
|
|
246
|
+
);
|
|
247
|
+
const values = index.members.map((member) => Number(member.value));
|
|
248
|
+
const lower = Math.min(...values);
|
|
249
|
+
if (Math.max(...values) - lower + 1 !== values.length)
|
|
250
|
+
this.fail(node, "PLC2001", "Record<Enum, T> needs enum member values without gaps.");
|
|
251
|
+
const element = this.resolve(value!, environment);
|
|
252
|
+
if (element.io || !this.isValue(element))
|
|
253
|
+
this.fail(node, "PLC2001", "Enum-keyed records need a plain value element.");
|
|
254
|
+
const id = `array<${element.type},${values.length},${lower},${index.id}>`;
|
|
255
|
+
this.types.set(id, {
|
|
256
|
+
id,
|
|
257
|
+
kind: "array",
|
|
258
|
+
element: element.type,
|
|
259
|
+
length: values.length,
|
|
260
|
+
lowerBound: lower,
|
|
261
|
+
index: index.id,
|
|
262
|
+
});
|
|
263
|
+
this.checkSize(id, node);
|
|
264
|
+
return { type: id };
|
|
265
|
+
}
|
|
266
|
+
if (official === "STRING" || official === "WSTRING") {
|
|
267
|
+
if (node.typeArguments?.length !== 1)
|
|
268
|
+
this.fail(node, "PLC2001", "Bounded strings require one capacity argument.");
|
|
269
|
+
const capacity = this.argument(node.typeArguments[0]!, environment);
|
|
270
|
+
if (capacity.kind !== "integer" || capacity.value < 1 || capacity.value > 65535)
|
|
271
|
+
this.fail(node, "PLC2001", "String capacity must be 1..65535.");
|
|
272
|
+
const id = `${official}<${capacity.value}>`;
|
|
273
|
+
this.types.set(id, {
|
|
274
|
+
id,
|
|
275
|
+
kind: "string",
|
|
276
|
+
capacity: capacity.value,
|
|
277
|
+
encoding: official === "STRING" ? "utf8" : "utf16",
|
|
278
|
+
});
|
|
279
|
+
return { type: id };
|
|
280
|
+
}
|
|
281
|
+
return this.declaration(symbol, node.typeArguments, environment, node);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
fixedArray(
|
|
285
|
+
node: ts.Node,
|
|
286
|
+
element: ResolvedType,
|
|
287
|
+
length: TypeArgument,
|
|
288
|
+
lower: TypeArgument,
|
|
289
|
+
): ResolvedType {
|
|
290
|
+
if (
|
|
291
|
+
element.io ||
|
|
292
|
+
!(this.isValue(element) || this.types.get(element.type)?.kind === "block") ||
|
|
293
|
+
length.kind !== "integer" ||
|
|
294
|
+
lower.kind !== "integer" ||
|
|
295
|
+
length.value < 1 ||
|
|
296
|
+
length.value > 1_000_000 ||
|
|
297
|
+
lower.value + length.value - 1 > 2147483647
|
|
298
|
+
)
|
|
299
|
+
this.fail(
|
|
300
|
+
node,
|
|
301
|
+
"PLC2001",
|
|
302
|
+
"Fixed arrays need a supported element, positive bounded length and signed DINT bounds.",
|
|
303
|
+
);
|
|
304
|
+
const id = `array<${element.type},${length.value},${lower.value}>`;
|
|
305
|
+
this.types.set(id, {
|
|
306
|
+
id,
|
|
307
|
+
kind: "array",
|
|
308
|
+
element: element.type,
|
|
309
|
+
length: length.value,
|
|
310
|
+
lowerBound: lower.value,
|
|
311
|
+
});
|
|
312
|
+
this.checkSize(id, node);
|
|
313
|
+
return { type: id };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private declaration(
|
|
317
|
+
symbol: ts.Symbol,
|
|
318
|
+
arguments_: readonly ts.TypeNode[] | undefined,
|
|
319
|
+
environment: TypeEnvironment,
|
|
320
|
+
node: ts.Node,
|
|
321
|
+
): ResolvedType {
|
|
322
|
+
const declarations = symbol.declarations ?? [];
|
|
323
|
+
if (declarations.filter(ts.isInterfaceDeclaration).length > 1)
|
|
324
|
+
this.fail(
|
|
325
|
+
node,
|
|
326
|
+
"PLC2001",
|
|
327
|
+
"Interface declaration merging is unsupported; use one closed layout declaration.",
|
|
328
|
+
);
|
|
329
|
+
const declaration = declarations.find(
|
|
330
|
+
(item) =>
|
|
331
|
+
ts.isTypeAliasDeclaration(item) ||
|
|
332
|
+
ts.isInterfaceDeclaration(item) ||
|
|
333
|
+
ts.isEnumDeclaration(item),
|
|
334
|
+
);
|
|
335
|
+
if (!declaration)
|
|
336
|
+
this.fail(node, "PLC2001", "This type is not implemented by the PLC frontend.");
|
|
337
|
+
if (ts.isEnumDeclaration(declaration)) {
|
|
338
|
+
if (arguments_?.length) this.fail(node, "PLC2001", "Enums take no type arguments.");
|
|
339
|
+
return this.enumeration(declaration);
|
|
340
|
+
}
|
|
341
|
+
const bindings = this.bindings(declaration.typeParameters, arguments_, environment, node);
|
|
342
|
+
const suffix = (declaration.typeParameters ?? [])
|
|
343
|
+
.map((parameter) => this.key(bindings.get(this.symbol(parameter.name))!))
|
|
344
|
+
.join(",");
|
|
345
|
+
const id = `${this.id(declaration, declaration.name.text)}${suffix ? `<${suffix}>` : ""}`;
|
|
346
|
+
if (this.active.has(id))
|
|
347
|
+
this.fail(node, "PLC2001", "Recursive by-value PLC layouts are unsupported.");
|
|
348
|
+
if (this.types.has(id)) return { type: id };
|
|
349
|
+
if (this.active.size >= 64)
|
|
350
|
+
this.fail(
|
|
351
|
+
node,
|
|
352
|
+
"PLC2001",
|
|
353
|
+
"Type substitution exceeds 64 levels; recursive expansion is unsupported.",
|
|
354
|
+
);
|
|
355
|
+
this.active.add(id);
|
|
356
|
+
try {
|
|
357
|
+
if (ts.isTypeAliasDeclaration(declaration)) {
|
|
358
|
+
if (ts.isTypeLiteralNode(declaration.type))
|
|
359
|
+
return this.record(declaration, id, declaration.type.members, bindings);
|
|
360
|
+
if (ts.isUnionTypeNode(declaration.type)) return this.union(declaration.type, id, bindings);
|
|
361
|
+
return this.resolve(declaration.type, bindings);
|
|
362
|
+
}
|
|
363
|
+
const inherited: Field[] = [];
|
|
364
|
+
for (const heritage of declaration.heritageClauses ?? []) {
|
|
365
|
+
for (const base of heritage.types) {
|
|
366
|
+
const baseSymbol = this.symbol(base.expression);
|
|
367
|
+
const baseType = this.declaration(baseSymbol, base.typeArguments, bindings, base);
|
|
368
|
+
const layout = this.types.get(baseType.type);
|
|
369
|
+
if (layout?.kind !== "record")
|
|
370
|
+
this.fail(base, "PLC2001", "Inherited layout must be a record.");
|
|
371
|
+
inherited.push(...layout.fields);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return this.record(declaration, id, declaration.members, bindings, inherited);
|
|
375
|
+
} finally {
|
|
376
|
+
this.active.delete(id);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Specialization identity includes I/O and InOut modes: equal value widths alone
|
|
380
|
+
// must not merge a readonly value parameter with a mutable borrowed input.
|
|
381
|
+
key(argument: TypeArgument): string {
|
|
382
|
+
if (argument.kind === "integer") return String(argument.value);
|
|
383
|
+
const { type, io, mode } = argument.value;
|
|
384
|
+
return `${type}${io ? `:${io}` : ""}${mode ? `:${mode}` : ""}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* A discriminated union is one fixed layout: a `kind` enum plus the fields of every variant.
|
|
389
|
+
* TypeScript's narrowing guards which fields a branch can read; a literal's unset fields are zero.
|
|
390
|
+
*/
|
|
391
|
+
private union(node: ts.UnionTypeNode, id: string, environment: TypeEnvironment): ResolvedType {
|
|
392
|
+
const fields = new Map<string, Field>();
|
|
393
|
+
const kinds: string[] = [];
|
|
394
|
+
for (const variant of node.types) {
|
|
395
|
+
const members = ts.isTypeLiteralNode(variant) ? variant.members : undefined;
|
|
396
|
+
const tag = members?.find(
|
|
397
|
+
(member) => ts.isPropertySignature(member) && member.name.getText() === "kind",
|
|
398
|
+
);
|
|
399
|
+
const literal =
|
|
400
|
+
tag && ts.isPropertySignature(tag) && tag.type && ts.isLiteralTypeNode(tag.type)
|
|
401
|
+
? tag.type.literal
|
|
402
|
+
: undefined;
|
|
403
|
+
if (
|
|
404
|
+
!members ||
|
|
405
|
+
!literal ||
|
|
406
|
+
!ts.isStringLiteral(literal) ||
|
|
407
|
+
!/^[A-Za-z]\w*$/.test(literal.text)
|
|
408
|
+
)
|
|
409
|
+
this.fail(
|
|
410
|
+
variant,
|
|
411
|
+
"PLC2001",
|
|
412
|
+
'Union variants are object types with an identifier string literal `kind`, such as { kind: "idle" }.',
|
|
413
|
+
);
|
|
414
|
+
if (kinds.includes(literal.text)) this.fail(variant, "PLC2001", "Duplicate union kind.");
|
|
415
|
+
kinds.push(literal.text);
|
|
416
|
+
for (const field of this.fields(
|
|
417
|
+
members.filter((member) => member !== tag),
|
|
418
|
+
environment,
|
|
419
|
+
)) {
|
|
420
|
+
const existing = fields.get(field.name);
|
|
421
|
+
if (existing && existing.type !== field.type)
|
|
422
|
+
this.fail(
|
|
423
|
+
variant,
|
|
424
|
+
"PLC2001",
|
|
425
|
+
"Union variants may share a field only with the same type.",
|
|
426
|
+
);
|
|
427
|
+
fields.set(field.name, field);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const kindId = `${id}Kind`;
|
|
431
|
+
this.types.set(kindId, {
|
|
432
|
+
id: kindId,
|
|
433
|
+
kind: "enum",
|
|
434
|
+
base: "DINT",
|
|
435
|
+
members: kinds.map((name, value) => ({ name, value: String(value) })),
|
|
436
|
+
});
|
|
437
|
+
this.types.set(id, {
|
|
438
|
+
id,
|
|
439
|
+
kind: "record",
|
|
440
|
+
fields: [{ name: "kind", type: kindId, readonly: false }, ...fields.values()],
|
|
441
|
+
});
|
|
442
|
+
this.unions.add(id);
|
|
443
|
+
this.checkSize(id, node);
|
|
444
|
+
return { type: id };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private record(
|
|
448
|
+
node: ts.Node,
|
|
449
|
+
id: string,
|
|
450
|
+
members: readonly ts.TypeElement[],
|
|
451
|
+
environment: TypeEnvironment,
|
|
452
|
+
inherited: readonly Field[] = [],
|
|
453
|
+
): ResolvedType {
|
|
454
|
+
const fields = this.fields(members, environment, inherited);
|
|
455
|
+
if (!fields.length) this.fail(node, "PLC2001", "PLC records require at least one field.");
|
|
456
|
+
this.types.set(id, { id, kind: "record", fields });
|
|
457
|
+
this.checkSize(id, node);
|
|
458
|
+
return { type: id };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private fields(
|
|
462
|
+
members: readonly ts.TypeElement[],
|
|
463
|
+
environment: TypeEnvironment,
|
|
464
|
+
inherited: readonly Field[] = [],
|
|
465
|
+
): Field[] {
|
|
466
|
+
const fields = [...inherited],
|
|
467
|
+
names = new Set(fields.map((field) => field.name));
|
|
468
|
+
for (const member of members) {
|
|
469
|
+
if (
|
|
470
|
+
!ts.isPropertySignature(member) ||
|
|
471
|
+
!member.type ||
|
|
472
|
+
member.questionToken ||
|
|
473
|
+
!member.name ||
|
|
474
|
+
!ts.isIdentifier(member.name)
|
|
475
|
+
)
|
|
476
|
+
this.fail(
|
|
477
|
+
member,
|
|
478
|
+
"PLC2001",
|
|
479
|
+
"PLC records have named, required, explicitly typed fields; methods and index signatures are unsupported.",
|
|
480
|
+
);
|
|
481
|
+
if (names.has(member.name.text)) this.fail(member, "PLC2001", "Duplicate record field.");
|
|
482
|
+
const type = this.resolve(member.type, environment);
|
|
483
|
+
if (instancedBlock(type.type, this.types))
|
|
484
|
+
this.fail(
|
|
485
|
+
member,
|
|
486
|
+
"PLC2001",
|
|
487
|
+
"Block instances belong in module or private block fields, not value records.",
|
|
488
|
+
);
|
|
489
|
+
if (type.io)
|
|
490
|
+
this.fail(
|
|
491
|
+
member,
|
|
492
|
+
"PLC2001",
|
|
493
|
+
"Logical I/O wrappers belong on ambient bindings, not fields.",
|
|
494
|
+
);
|
|
495
|
+
fields.push({
|
|
496
|
+
name: member.name.text,
|
|
497
|
+
type: type.type,
|
|
498
|
+
...(type.mode && { mode: type.mode }),
|
|
499
|
+
readonly: Boolean(
|
|
500
|
+
member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ReadonlyKeyword),
|
|
501
|
+
),
|
|
502
|
+
});
|
|
503
|
+
names.add(member.name.text);
|
|
504
|
+
}
|
|
505
|
+
return fields;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
inputFields(node: ts.TypeNode): readonly Field[] {
|
|
509
|
+
if (ts.isTypeLiteralNode(node) && !node.members.length) return [];
|
|
510
|
+
const resolved = this.resolve(node),
|
|
511
|
+
type = this.types.get(resolved.type);
|
|
512
|
+
if (resolved.io || resolved.mode || type?.kind !== "record")
|
|
513
|
+
this.fail(node, "PLC2001", "Block inputs require one closed record schema.");
|
|
514
|
+
if (type.fields.some((field) => this.hasReferences(field.type)))
|
|
515
|
+
this.fail(node, "PLC2001", "InOut belongs directly on input fields, not nested records.");
|
|
516
|
+
return type.fields;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// A plain value layout: no InOut borrow, nested borrow schema or block instance.
|
|
520
|
+
isValue(resolved: ResolvedType): boolean {
|
|
521
|
+
return (
|
|
522
|
+
!resolved.mode &&
|
|
523
|
+
!this.hasReferences(resolved.type) &&
|
|
524
|
+
!instancedBlock(resolved.type, this.types)
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
hasReferences(id: string, seen = new Set<string>()): boolean {
|
|
529
|
+
if (seen.has(id)) return false;
|
|
530
|
+
seen.add(id);
|
|
531
|
+
const type = this.types.get(id);
|
|
532
|
+
return type?.kind === "record"
|
|
533
|
+
? type.fields.some((field) => field.mode || this.hasReferences(field.type, seen))
|
|
534
|
+
: type?.kind === "array"
|
|
535
|
+
? this.hasReferences(type.element, seen)
|
|
536
|
+
: false;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
enumeration(node: ts.EnumDeclaration): ResolvedType {
|
|
540
|
+
const id = this.id(node, node.name.text);
|
|
541
|
+
if (this.types.has(id)) return { type: id };
|
|
542
|
+
if (!node.members.length) this.fail(node, "PLC2001", "PLC enums cannot be empty.");
|
|
543
|
+
const members = node.members.map((member) => {
|
|
544
|
+
if (!ts.isIdentifier(member.name))
|
|
545
|
+
this.fail(member, "PLC2001", "Enum members require identifier names.");
|
|
546
|
+
const value = this.checker.getConstantValue(member);
|
|
547
|
+
if (
|
|
548
|
+
typeof value !== "number" ||
|
|
549
|
+
!Number.isInteger(value) ||
|
|
550
|
+
value < -2147483648 ||
|
|
551
|
+
value > 2147483647
|
|
552
|
+
)
|
|
553
|
+
this.fail(member, "PLC2001", "PLC enum values must be compile-time DINT integers.");
|
|
554
|
+
return { name: member.name.text, value: String(value) };
|
|
555
|
+
});
|
|
556
|
+
if (new Set(members.map((member) => member.value)).size !== members.length)
|
|
557
|
+
this.fail(node, "PLC2001", "PLC enum members require distinct values.");
|
|
558
|
+
this.types.set(id, { id, kind: "enum", base: "DINT", members });
|
|
559
|
+
return { type: id };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private checkSize(id: string, node: ts.Node): void {
|
|
563
|
+
const count = (typeId: string, depth: number): number => {
|
|
564
|
+
if (depth > 64) this.fail(node, "PLC2001", "PLC layout nesting exceeds 64 levels.");
|
|
565
|
+
const type = this.types.get(typeId)!;
|
|
566
|
+
const size =
|
|
567
|
+
type.kind === "array"
|
|
568
|
+
? type.length * count(type.element, depth + 1)
|
|
569
|
+
: type.kind === "record"
|
|
570
|
+
? type.fields.reduce((total, field) => total + count(field.type, depth + 1), 0)
|
|
571
|
+
: 1;
|
|
572
|
+
if (size > 1_000_000)
|
|
573
|
+
this.fail(node, "PLC2001", "PLC layout exceeds one million scalar elements.");
|
|
574
|
+
return size;
|
|
575
|
+
};
|
|
576
|
+
count(id, 0);
|
|
577
|
+
}
|
|
578
|
+
}
|