@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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { SourceSpan } from "./diagnostics.ts";
|
|
2
|
+
import {
|
|
3
|
+
standardBlocks,
|
|
4
|
+
type PlcModule,
|
|
5
|
+
type Variable,
|
|
6
|
+
type FunctionBlock,
|
|
7
|
+
type PlcFunction,
|
|
8
|
+
} from "./model.ts";
|
|
9
|
+
import { instancedBlock, type PlcType } from "./types.ts";
|
|
10
|
+
import { layoutIssue, valueFits } from "./aggregates.ts";
|
|
11
|
+
import { reject } from "./normalization-error.ts";
|
|
12
|
+
|
|
13
|
+
// This index owns the module namespace and storage declarations. Routine
|
|
14
|
+
// lowering may claim temporary identifiers, but cannot weaken admission rules.
|
|
15
|
+
export class ModuleDeclarations {
|
|
16
|
+
private readonly ids = new Set<string>();
|
|
17
|
+
readonly types = new Map<string, PlcType>();
|
|
18
|
+
readonly globals = new Map<string, Variable>();
|
|
19
|
+
readonly blocks: Map<string, FunctionBlock>;
|
|
20
|
+
readonly signatures: Map<string, PlcFunction>;
|
|
21
|
+
|
|
22
|
+
constructor(module: PlcModule) {
|
|
23
|
+
for (const type of module.types) {
|
|
24
|
+
if (this.types.has(type.id)) reject(`Duplicate type ${type.id}.`);
|
|
25
|
+
this.types.set(type.id, type);
|
|
26
|
+
}
|
|
27
|
+
const invalidLayout = layoutIssue(this.types);
|
|
28
|
+
if (invalidLayout) reject(invalidLayout);
|
|
29
|
+
if (this.types.get("BOOL")?.kind !== "bool") reject("Missing canonical BOOL type.");
|
|
30
|
+
|
|
31
|
+
this.blocks = new Map(module.blocks.map((block) => [block.id, block]));
|
|
32
|
+
for (const item of module.variables) {
|
|
33
|
+
this.checkVariable(item, ["module", "instance", "input", "output", "constant"]);
|
|
34
|
+
this.globals.set(item.id, item);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.signatures = new Map(module.functions.map((fn) => [fn.id, fn]));
|
|
38
|
+
for (const fn of module.functions) {
|
|
39
|
+
this.claimId(fn.id, fn.span);
|
|
40
|
+
if (
|
|
41
|
+
fn.owner &&
|
|
42
|
+
(!this.blocks.has(fn.owner.block) ||
|
|
43
|
+
this.blocks.get(fn.owner.block)?.standard ||
|
|
44
|
+
!["public", "private", "protected"].includes(fn.owner.access))
|
|
45
|
+
)
|
|
46
|
+
reject("Methods require a user block owner and a valid access modifier.", fn.span);
|
|
47
|
+
if (fn.result && !this.isValueType(fn.result))
|
|
48
|
+
reject("Unknown or non-value function result type.", fn.span);
|
|
49
|
+
for (const parameter of fn.parameters) {
|
|
50
|
+
this.checkVariable(parameter, ["parameter"], true);
|
|
51
|
+
if (parameter.mutable && !parameter.mode)
|
|
52
|
+
reject("Value parameters are readonly.", parameter.span);
|
|
53
|
+
}
|
|
54
|
+
for (const local of fn.locals) this.checkVariable(local, ["local"]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const block of module.blocks) {
|
|
58
|
+
this.claimId(block.id, block.span);
|
|
59
|
+
for (const input of block.inputs) {
|
|
60
|
+
this.checkVariable(input, ["parameter"], true);
|
|
61
|
+
if (!input.mode && input.mutable) reject("Value block inputs are readonly.", input.span);
|
|
62
|
+
}
|
|
63
|
+
for (const output of block.outputs) this.checkVariable(output, ["blockOutput"]);
|
|
64
|
+
for (const state of block.state) this.checkVariable(state, ["field", "instance"]);
|
|
65
|
+
for (const local of block.locals) this.checkVariable(local, ["local"]);
|
|
66
|
+
for (const ports of [block.inputs, block.outputs]) {
|
|
67
|
+
if (new Set(ports.map((variable) => variable.name)).size !== ports.length)
|
|
68
|
+
reject(
|
|
69
|
+
"Block input and output names must each be unique within their schema.",
|
|
70
|
+
block.span,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const result = block.result ? this.types.get(block.result) : undefined;
|
|
74
|
+
if (
|
|
75
|
+
block.outputs.length
|
|
76
|
+
? result?.kind !== "record" ||
|
|
77
|
+
!this.isValueType(result.id) ||
|
|
78
|
+
result.fields.length !== block.outputs.length ||
|
|
79
|
+
result.fields.some(
|
|
80
|
+
(field) =>
|
|
81
|
+
!block.outputs.some(
|
|
82
|
+
(output) => output.name === field.name && output.type === field.type,
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
: Boolean(block.result)
|
|
86
|
+
)
|
|
87
|
+
reject("Block output schema differs from its result record.", block.span);
|
|
88
|
+
if (block.standard) {
|
|
89
|
+
const expected = Object.hasOwn(standardBlocks, block.standard)
|
|
90
|
+
? standardBlocks[block.standard]
|
|
91
|
+
: undefined;
|
|
92
|
+
const matches = (
|
|
93
|
+
variables: readonly Variable[],
|
|
94
|
+
schema: readonly (readonly [string, string, string])[],
|
|
95
|
+
): boolean =>
|
|
96
|
+
variables.length === schema.length &&
|
|
97
|
+
schema.every(([name, , type]) =>
|
|
98
|
+
variables.some(
|
|
99
|
+
(variable) =>
|
|
100
|
+
variable.name === name &&
|
|
101
|
+
variable.type === type &&
|
|
102
|
+
!variable.mode &&
|
|
103
|
+
!variable.retain,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
if (
|
|
107
|
+
!expected ||
|
|
108
|
+
block.name !== block.standard ||
|
|
109
|
+
block.body.length ||
|
|
110
|
+
block.state.length ||
|
|
111
|
+
block.locals.length ||
|
|
112
|
+
!matches(block.inputs, expected.inputs) ||
|
|
113
|
+
!matches(block.outputs, expected.outputs) ||
|
|
114
|
+
block.outputs.some((output) =>
|
|
115
|
+
output.type === "BOOL"
|
|
116
|
+
? output.initial?.kind !== "bool" || output.initial.value !== false
|
|
117
|
+
: output.initial?.kind !== "duration" || output.initial.nanoseconds !== "0",
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
reject("Invalid canonical standard block definition.", block.span);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const type of this.types.values())
|
|
125
|
+
if (type.kind === "block") {
|
|
126
|
+
const definition = this.blocks.get(type.block);
|
|
127
|
+
if (!definition || definition.result !== type.output)
|
|
128
|
+
reject("Block type differs from its definition.");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
hasId(id: string): boolean {
|
|
133
|
+
return this.ids.has(id);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public claimId(id: string, span?: SourceSpan): void {
|
|
137
|
+
if (this.ids.has(id)) reject(`Duplicate symbol ${id}.`, span);
|
|
138
|
+
this.ids.add(id);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public isValueType(id: string, seen = new Set<string>()): boolean {
|
|
142
|
+
if (seen.has(id)) return true;
|
|
143
|
+
seen.add(id);
|
|
144
|
+
const type = this.types.get(id);
|
|
145
|
+
return Boolean(
|
|
146
|
+
type &&
|
|
147
|
+
type.kind !== "block" &&
|
|
148
|
+
(type.kind !== "record" ||
|
|
149
|
+
type.fields.every((field) => !field.mode && this.isValueType(field.type, seen))) &&
|
|
150
|
+
(type.kind !== "array" || this.isValueType(type.element, seen)),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private checkVariable(
|
|
155
|
+
item: Variable,
|
|
156
|
+
storage: readonly Variable["storage"][],
|
|
157
|
+
allowBorrow = false,
|
|
158
|
+
): void {
|
|
159
|
+
this.claimId(item.id, item.span);
|
|
160
|
+
const type = this.types.get(item.type);
|
|
161
|
+
if (!type || !storage.includes(item.storage))
|
|
162
|
+
reject("Invalid storage type or class.", item.span);
|
|
163
|
+
if (
|
|
164
|
+
item.mode &&
|
|
165
|
+
(item.mode !== "inOut" || !allowBorrow || !item.mutable || item.storage !== "parameter")
|
|
166
|
+
)
|
|
167
|
+
reject("InOut belongs only on mutable parameters.", item.span);
|
|
168
|
+
const mutableState =
|
|
169
|
+
item.mutable ||
|
|
170
|
+
type.kind === "array" ||
|
|
171
|
+
(type.kind === "record" && type.fields.some((field) => !field.readonly));
|
|
172
|
+
if (
|
|
173
|
+
item.retain &&
|
|
174
|
+
!(item.storage === "instance" || (["module", "field"].includes(item.storage) && mutableState))
|
|
175
|
+
)
|
|
176
|
+
reject("Only mutable static state or block instances may retain.", item.span);
|
|
177
|
+
if (item.storage === "instance") {
|
|
178
|
+
const instanced = instancedBlock(item.type, this.types);
|
|
179
|
+
if (!instanced || !this.blocks.has(instanced.block) || item.initial || item.mode)
|
|
180
|
+
reject(
|
|
181
|
+
"Instances require a known static block and cannot have value initializers.",
|
|
182
|
+
item.span,
|
|
183
|
+
);
|
|
184
|
+
} else if (
|
|
185
|
+
!this.isValueType(item.type) &&
|
|
186
|
+
!(allowBorrow && item.mode === "inOut" && type.kind === "block")
|
|
187
|
+
)
|
|
188
|
+
reject("Block instances and reference schemas cannot be stored as values.", item.span);
|
|
189
|
+
if (item.storage === "parameter" && item.initial)
|
|
190
|
+
reject(
|
|
191
|
+
"Inputs are supplied at each invocation and cannot have stored initializers.",
|
|
192
|
+
item.span,
|
|
193
|
+
);
|
|
194
|
+
if (item.initial && !valueFits(item.initial, type, this.types))
|
|
195
|
+
reject("Initializer has the wrong type or is out of range.", item.span);
|
|
196
|
+
if (["module", "field", "constant", "blockOutput"].includes(item.storage) && !item.initial)
|
|
197
|
+
reject("Persistent storage requires initialization.", item.span);
|
|
198
|
+
if (["input", "constant", "blockOutput"].includes(item.storage) && item.mutable)
|
|
199
|
+
reject("Readonly storage marked mutable.", item.span);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface SourceSpan {
|
|
2
|
+
file: string;
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
line: number;
|
|
6
|
+
column: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Diagnostic {
|
|
10
|
+
origin: "typescript" | "plc" | "config" | "backend" | "vendor";
|
|
11
|
+
code: string;
|
|
12
|
+
severity: "error" | "warning";
|
|
13
|
+
message: string;
|
|
14
|
+
span?: SourceSpan;
|
|
15
|
+
notes?: readonly string[];
|
|
16
|
+
related?: readonly { span: SourceSpan; message: string }[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function error(code: string, message: string, span?: SourceSpan): Diagnostic {
|
|
20
|
+
return { origin: "plc", code, severity: "error", message, ...(span && { span }) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function hasErrors(diagnostics: readonly Diagnostic[]): boolean {
|
|
24
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatDiagnostic(diagnostic: Diagnostic, source?: string): string {
|
|
28
|
+
const { span } = diagnostic;
|
|
29
|
+
const location = span ? `${span.file}:${span.line}:${span.column}: ` : "";
|
|
30
|
+
let result = `${location}${diagnostic.code}: ${diagnostic.message}`;
|
|
31
|
+
if (source !== undefined && span) {
|
|
32
|
+
const line = source.split(/\r?\n/)[span.line - 1];
|
|
33
|
+
if (line !== undefined) {
|
|
34
|
+
const width = Math.max(1, Math.min(span.end - span.start, line.length - span.column + 2));
|
|
35
|
+
result += `\n${span.line} | ${line}\n${" ".repeat(String(span.line).length + 3 + span.column - 1)}${"^".repeat(width)}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const related of diagnostic.related ?? []) {
|
|
39
|
+
result += `\n ${related.span.file}:${related.span.line}:${related.span.column}: ${related.message}`;
|
|
40
|
+
}
|
|
41
|
+
for (const note of diagnostic.notes ?? []) result += `\n ${note}`;
|
|
42
|
+
return result;
|
|
43
|
+
}
|
package/ir/domains.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { comparisonOperators, type Constant, type Expression } from "./model.ts";
|
|
2
|
+
import type { PlcType } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export function durationLiteral(text: string, scaleNanoseconds: bigint): Constant | undefined {
|
|
5
|
+
const clean = text.replaceAll("_", "").replace(/^(-?)\./, (_, sign: string) => `${sign}0.`);
|
|
6
|
+
const match = /^(-?)(\d+)(?:\.(\d*))?(?:e([+-]?\d+))?$/i.exec(clean);
|
|
7
|
+
if (!match) return undefined;
|
|
8
|
+
const fraction = match[3] ?? "",
|
|
9
|
+
exponent = Number(match[4] ?? 0) - fraction.length;
|
|
10
|
+
if (!Number.isSafeInteger(exponent) || Math.abs(exponent) > 400) return undefined;
|
|
11
|
+
let value = BigInt(match[2]! + fraction) * scaleNanoseconds;
|
|
12
|
+
if (exponent >= 0) value *= 10n ** BigInt(exponent);
|
|
13
|
+
else {
|
|
14
|
+
const divisor = 10n ** BigInt(-exponent);
|
|
15
|
+
if (value % divisor) return undefined;
|
|
16
|
+
value /= divisor;
|
|
17
|
+
}
|
|
18
|
+
if (match[1]) value = -value;
|
|
19
|
+
return validDuration(value) ? { kind: "duration", nanoseconds: String(value) } : undefined;
|
|
20
|
+
}
|
|
21
|
+
// TIME stores unsigned 32-bit milliseconds.
|
|
22
|
+
function validDuration(nanoseconds: bigint): boolean {
|
|
23
|
+
return (
|
|
24
|
+
nanoseconds >= 0n && nanoseconds <= 4_294_967_295_000_000n && nanoseconds % 1_000_000n === 0n
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
function validDate(text: string): boolean {
|
|
28
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
|
|
29
|
+
if (!match) return false;
|
|
30
|
+
const year = Number(match[1]),
|
|
31
|
+
month = Number(match[2]),
|
|
32
|
+
day = Number(match[3]);
|
|
33
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
34
|
+
const length = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
|
|
35
|
+
return Boolean(
|
|
36
|
+
length && day >= 1 && day <= length && text >= "1970-01-01" && text <= "2106-02-07",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
function validClock(text: string, milliseconds: boolean): boolean {
|
|
40
|
+
const match = (
|
|
41
|
+
milliseconds ? /^(\d{2}):(\d{2}):(\d{2})\.(\d{3})$/ : /^(\d{2}):(\d{2}):(\d{2})$/
|
|
42
|
+
).exec(text);
|
|
43
|
+
return Boolean(match && Number(match[1]) < 24 && Number(match[2]) < 60 && Number(match[3]) < 60);
|
|
44
|
+
}
|
|
45
|
+
export function calendarLiteral(
|
|
46
|
+
domain: Extract<PlcType, { kind: "calendar" }>["domain"],
|
|
47
|
+
text: string,
|
|
48
|
+
): Constant | undefined {
|
|
49
|
+
if (domain === "date") return validDate(text) ? { kind: "calendar", value: text } : undefined;
|
|
50
|
+
if (domain === "timeOfDay") {
|
|
51
|
+
const canonical = /^\d{2}:\d{2}:\d{2}$/.test(text) ? `${text}.000` : text;
|
|
52
|
+
return validClock(canonical, true) ? { kind: "calendar", value: canonical } : undefined;
|
|
53
|
+
}
|
|
54
|
+
return text.length === 19 &&
|
|
55
|
+
text[10] === "T" &&
|
|
56
|
+
validDate(text.slice(0, 10)) &&
|
|
57
|
+
validClock(text.slice(11), false) &&
|
|
58
|
+
text <= "2106-02-07T06:28:15"
|
|
59
|
+
? { kind: "calendar", value: text }
|
|
60
|
+
: undefined;
|
|
61
|
+
}
|
|
62
|
+
export function domainConstantFits(value: Constant, type: PlcType): boolean {
|
|
63
|
+
if (type.kind === "duration")
|
|
64
|
+
return (
|
|
65
|
+
value.kind === "duration" &&
|
|
66
|
+
/^(?:0|[1-9][0-9]*)$/.test(value.nanoseconds) &&
|
|
67
|
+
validDuration(BigInt(value.nanoseconds))
|
|
68
|
+
);
|
|
69
|
+
if (type.kind === "calendar" && value.kind === "calendar") {
|
|
70
|
+
const parsed = calendarLiteral(type.domain, value.value);
|
|
71
|
+
return parsed?.kind === "calendar" && parsed.value === value.value;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
export function domainIssue(
|
|
76
|
+
value: Expression,
|
|
77
|
+
types: ReadonlyMap<string, PlcType>,
|
|
78
|
+
): string | undefined {
|
|
79
|
+
const type = types.get(value.type);
|
|
80
|
+
if (value.kind === "constant")
|
|
81
|
+
return type && domainConstantFits(value.value, type)
|
|
82
|
+
? undefined
|
|
83
|
+
: "Invalid TIME/calendar constant.";
|
|
84
|
+
if (value.kind === "load" || value.kind === "call" || value.kind === "conditional")
|
|
85
|
+
return undefined;
|
|
86
|
+
if (value.kind === "binary" && comparisonOperators.has(value.operator)) {
|
|
87
|
+
const left = types.get(value.left.type);
|
|
88
|
+
return value.type === "BOOL" &&
|
|
89
|
+
left &&
|
|
90
|
+
["duration", "calendar"].includes(left.kind) &&
|
|
91
|
+
value.left.type === value.right.type
|
|
92
|
+
? undefined
|
|
93
|
+
: "Time comparisons require matching domains.";
|
|
94
|
+
}
|
|
95
|
+
return "TIME and calendar domains permit storage, typed copies and comparisons; implicit numeric arithmetic is unsupported.";
|
|
96
|
+
}
|
package/ir/index.ts
ADDED
package/ir/link.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { PlcModule } from "./model.ts";
|
|
2
|
+
import type { CoreFunction, CoreFunctionBlock, CoreStatement } from "./core-model.ts";
|
|
3
|
+
import type { ModuleDeclarations } from "./declarations.ts";
|
|
4
|
+
import { reject } from "./normalization-error.ts";
|
|
5
|
+
import { instancedBlock } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
// Validate the complete authored call graph before pruning unreachable code.
|
|
8
|
+
// Static instances keep their child layouts even when a child has no scan call.
|
|
9
|
+
export function linkModule(
|
|
10
|
+
module: PlcModule,
|
|
11
|
+
declarations: ModuleDeclarations,
|
|
12
|
+
functions: CoreFunction[],
|
|
13
|
+
normalizedBlocks: CoreFunctionBlock[],
|
|
14
|
+
edges: ReadonlyMap<string, ReadonlySet<string>>,
|
|
15
|
+
) {
|
|
16
|
+
const { signatures, blocks, types } = declarations;
|
|
17
|
+
const names = new Set<string>(),
|
|
18
|
+
entries = new Set<string>();
|
|
19
|
+
for (const program of module.programs) {
|
|
20
|
+
declarations.claimId(program.id);
|
|
21
|
+
const entry = signatures.get(program.entry);
|
|
22
|
+
if (!entry || entry.owner || entry.parameters.length || entry.result)
|
|
23
|
+
reject("Program entry must be a parameterless void helper.");
|
|
24
|
+
if (names.has(program.name)) reject(`Duplicate program ${program.name}.`);
|
|
25
|
+
names.add(program.name);
|
|
26
|
+
entries.add(program.entry);
|
|
27
|
+
}
|
|
28
|
+
if (!module.programs.length) reject("At least one program is required.");
|
|
29
|
+
|
|
30
|
+
// Authored execution cycles are invalid even in unreachable routines. Static
|
|
31
|
+
// allocation has its own cycle check because it does not depend on scan calls.
|
|
32
|
+
const visited = new Set<string>(),
|
|
33
|
+
active = new Set<string>();
|
|
34
|
+
const visit = (id: string): void => {
|
|
35
|
+
if (active.has(id)) reject("Recursive scan calls are prohibited.", signatures.get(id)?.span);
|
|
36
|
+
if (visited.has(id)) return;
|
|
37
|
+
active.add(id);
|
|
38
|
+
for (const child of edges.get(id) ?? []) {
|
|
39
|
+
if (entries.has(child))
|
|
40
|
+
reject("Program entries cannot be called as ordinary helpers.", signatures.get(id)?.span);
|
|
41
|
+
visit(child);
|
|
42
|
+
}
|
|
43
|
+
active.delete(id);
|
|
44
|
+
visited.add(id);
|
|
45
|
+
};
|
|
46
|
+
for (const fn of [...functions, ...normalizedBlocks]) visit(fn.id);
|
|
47
|
+
|
|
48
|
+
// A bounded loop can only reconstruct its counter if called helpers leave
|
|
49
|
+
// that storage alone. Include transitive calls, not just direct assignments.
|
|
50
|
+
const routines = new Map([...functions, ...normalizedBlocks].map((fn) => [fn.id, fn]));
|
|
51
|
+
const variables = new Map(
|
|
52
|
+
[
|
|
53
|
+
...module.variables,
|
|
54
|
+
...functions.flatMap((fn) => [...fn.parameters, ...fn.locals]),
|
|
55
|
+
...normalizedBlocks.flatMap((block) => [
|
|
56
|
+
...block.inputs,
|
|
57
|
+
...block.outputs,
|
|
58
|
+
...block.state,
|
|
59
|
+
...block.locals,
|
|
60
|
+
]),
|
|
61
|
+
].map((variable) => [variable.id, variable]),
|
|
62
|
+
);
|
|
63
|
+
const calledRoutine = (item: CoreStatement): string | undefined => {
|
|
64
|
+
if (item.kind === "invoke") return item.function;
|
|
65
|
+
if (item.kind !== "blockUpdate") return undefined;
|
|
66
|
+
const variable = variables.get(item.instance);
|
|
67
|
+
return variable && instancedBlock(variable.type, types)?.block;
|
|
68
|
+
};
|
|
69
|
+
const nestedBodies = (item: CoreStatement): readonly (readonly CoreStatement[])[] => {
|
|
70
|
+
if (item.kind === "for") return [item.body];
|
|
71
|
+
if (item.kind === "if") return [item.then, item.else];
|
|
72
|
+
if (item.kind === "switch") return [...item.cases.map((branch) => branch.body), item.fallback];
|
|
73
|
+
return [];
|
|
74
|
+
};
|
|
75
|
+
const writes = new Map<string, Set<string>>();
|
|
76
|
+
const routineWrites = (id: string): Set<string> => {
|
|
77
|
+
const cached = writes.get(id);
|
|
78
|
+
if (cached) return cached;
|
|
79
|
+
const result = new Set<string>();
|
|
80
|
+
const inspect = (body: readonly CoreStatement[]) => {
|
|
81
|
+
for (const item of body) {
|
|
82
|
+
if (item.kind === "assign") result.add(item.root);
|
|
83
|
+
if (item.kind === "for") result.add(item.counter);
|
|
84
|
+
if (item.kind === "invoke") {
|
|
85
|
+
if (item.destination) result.add(item.destination);
|
|
86
|
+
for (const argument of item.arguments)
|
|
87
|
+
if (argument.kind === "borrow") result.add(argument.place.root);
|
|
88
|
+
}
|
|
89
|
+
if (item.kind === "blockUpdate")
|
|
90
|
+
for (const input of Object.values(item.inputs))
|
|
91
|
+
if (input.kind === "borrow") result.add(input.place.root);
|
|
92
|
+
for (const nested of nestedBodies(item)) inspect(nested);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
inspect(routines.get(id)!.body);
|
|
96
|
+
for (const child of edges.get(id) ?? [])
|
|
97
|
+
for (const root of routineWrites(child)) result.add(root);
|
|
98
|
+
writes.set(id, result);
|
|
99
|
+
return result;
|
|
100
|
+
};
|
|
101
|
+
const checkCalls = (body: readonly CoreStatement[], counters: ReadonlySet<string>) => {
|
|
102
|
+
for (const item of body) {
|
|
103
|
+
const called = calledRoutine(item);
|
|
104
|
+
if (called && [...counters].some((counter) => routineWrites(called).has(counter)))
|
|
105
|
+
reject("A called routine may mutate an active loop counter.", item.span);
|
|
106
|
+
const nestedCounters = item.kind === "for" ? new Set([...counters, item.counter]) : counters;
|
|
107
|
+
for (const nested of nestedBodies(item)) checkCalls(nested, nestedCounters);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
for (const routine of routines.values()) checkCalls(routine.body, new Set());
|
|
111
|
+
|
|
112
|
+
// Only after the complete graph is valid may emission discard unused helpers.
|
|
113
|
+
const reachable = new Set<string>();
|
|
114
|
+
const link = (id: string): void => {
|
|
115
|
+
if (reachable.has(id)) return;
|
|
116
|
+
reachable.add(id);
|
|
117
|
+
for (const child of edges.get(id) ?? []) link(child);
|
|
118
|
+
const owner = signatures.get(id)?.owner;
|
|
119
|
+
if (owner) link(owner.block);
|
|
120
|
+
for (const method of functions.filter((fn) => fn.owner?.block === id)) link(method.id);
|
|
121
|
+
for (const instance of blocks
|
|
122
|
+
.get(id)
|
|
123
|
+
?.state.filter((variable) => variable.storage === "instance") ?? []) {
|
|
124
|
+
const type = instancedBlock(instance.type, types);
|
|
125
|
+
if (type) link(type.block);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
for (const entry of entries) link(entry);
|
|
129
|
+
for (const variable of module.variables) {
|
|
130
|
+
const type = instancedBlock(variable.type, types);
|
|
131
|
+
if (type) link(type.block);
|
|
132
|
+
}
|
|
133
|
+
const linkedFunctions = functions.filter((fn) => reachable.has(fn.id)),
|
|
134
|
+
linkedBlocks = normalizedBlocks.filter((block) => reachable.has(block.id));
|
|
135
|
+
|
|
136
|
+
// Admission has already validated every referenced type; these lookups are
|
|
137
|
+
// closed over the declaration registry, including dependencies kept by DUTs.
|
|
138
|
+
const usedTypes = new Set<string>();
|
|
139
|
+
const useType = (id: string): void => {
|
|
140
|
+
if (usedTypes.has(id)) return;
|
|
141
|
+
usedTypes.add(id);
|
|
142
|
+
const type = types.get(id)!;
|
|
143
|
+
if (type.kind === "record") {
|
|
144
|
+
for (const field of type.fields) useType(field.type);
|
|
145
|
+
}
|
|
146
|
+
if (type.kind === "array") {
|
|
147
|
+
useType(type.element);
|
|
148
|
+
if (type.index) useType(type.index);
|
|
149
|
+
}
|
|
150
|
+
if (type.kind === "enum") useType(type.base);
|
|
151
|
+
if (type.kind === "block" && type.output) useType(type.output);
|
|
152
|
+
};
|
|
153
|
+
// Primitive definitions also support backend-generated numeric helpers.
|
|
154
|
+
types.forEach((type) => {
|
|
155
|
+
if (["bool", "integer", "bits", "float", "duration", "calendar"].includes(type.kind))
|
|
156
|
+
useType(type.id);
|
|
157
|
+
});
|
|
158
|
+
for (const variable of module.variables) useType(variable.type);
|
|
159
|
+
for (const fn of linkedFunctions) {
|
|
160
|
+
if (fn.result) useType(fn.result);
|
|
161
|
+
for (const variable of [...fn.parameters, ...fn.locals]) useType(variable.type);
|
|
162
|
+
}
|
|
163
|
+
for (const block of linkedBlocks) {
|
|
164
|
+
if (block.result) useType(block.result);
|
|
165
|
+
for (const variable of [...block.inputs, ...block.outputs, ...block.state, ...block.locals]) {
|
|
166
|
+
useType(variable.type);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
types: module.types.filter((type) => usedTypes.has(type.id)),
|
|
171
|
+
variables: module.variables,
|
|
172
|
+
functions: linkedFunctions,
|
|
173
|
+
blocks: linkedBlocks,
|
|
174
|
+
programs: module.programs,
|
|
175
|
+
};
|
|
176
|
+
}
|