@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,2280 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { Layouts, type LiteralEnum } from "./layouts.ts";
|
|
3
|
+
import type { CapturedValue } from "./graph.ts";
|
|
4
|
+
import type { Model, ModelNode, Root, Step } from "./model.ts";
|
|
5
|
+
|
|
6
|
+
type FunctionLike =
|
|
7
|
+
| ts.FunctionDeclaration
|
|
8
|
+
| ts.MethodDeclaration
|
|
9
|
+
| ts.ArrowFunction
|
|
10
|
+
| ts.FunctionExpression
|
|
11
|
+
| ts.GetAccessorDeclaration;
|
|
12
|
+
|
|
13
|
+
/** What an expression is while specializing: known now, or code the PLC runs. */
|
|
14
|
+
export type Value =
|
|
15
|
+
/** An object setup built: a class instance, record, array, function or service. */
|
|
16
|
+
| { kind: "static"; node: ModelNode }
|
|
17
|
+
/** A literal or folded primitive. `undefined` is a value too. */
|
|
18
|
+
| { kind: "const"; value: number | boolean | string | undefined | bigint }
|
|
19
|
+
/** An array or record built in scan code from known parts, used before it needs storage. */
|
|
20
|
+
| { kind: "virtual"; items?: Value[]; fields?: Map<string, Value> }
|
|
21
|
+
/** A function value: a module function, method of a known instance, or a local arrow. */
|
|
22
|
+
| { kind: "function"; declaration: FunctionLike; self?: ModelNode; env?: Env }
|
|
23
|
+
/** Code the PLC runs. `declared` is the storage's declared type, before narrowing. */
|
|
24
|
+
| {
|
|
25
|
+
kind: "code";
|
|
26
|
+
text: string;
|
|
27
|
+
declared?: ts.Type;
|
|
28
|
+
place?: boolean;
|
|
29
|
+
iec?: string;
|
|
30
|
+
/** A module global another module stores into through its setter; an import is read-only. */
|
|
31
|
+
setter?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type Env = Map<ts.Symbol, Value>;
|
|
35
|
+
|
|
36
|
+
const constant = (value: number | boolean | string | undefined | bigint): Value => ({
|
|
37
|
+
kind: "const",
|
|
38
|
+
value,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export class SpecializeError extends Error {
|
|
42
|
+
constructor(
|
|
43
|
+
readonly node: ts.Node,
|
|
44
|
+
message: string,
|
|
45
|
+
) {
|
|
46
|
+
super(message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Indented lines of one generated routine, with the local names it has used. */
|
|
51
|
+
class Emitter {
|
|
52
|
+
readonly lines: string[] = [];
|
|
53
|
+
private depth = 1;
|
|
54
|
+
private readonly used = new Set<string>();
|
|
55
|
+
private temporaries = 0;
|
|
56
|
+
|
|
57
|
+
line(text: string): void {
|
|
58
|
+
this.lines.push(`${" ".repeat(this.depth)}${text}`);
|
|
59
|
+
}
|
|
60
|
+
open(text: string): void {
|
|
61
|
+
this.line(text);
|
|
62
|
+
this.depth++;
|
|
63
|
+
}
|
|
64
|
+
close(text = "}"): void {
|
|
65
|
+
this.depth--;
|
|
66
|
+
this.line(text);
|
|
67
|
+
}
|
|
68
|
+
local(name: string): string {
|
|
69
|
+
let candidate = name;
|
|
70
|
+
for (let index = 2; this.used.has(candidate); index++) candidate = `${name}_${index}`;
|
|
71
|
+
this.used.add(candidate);
|
|
72
|
+
return candidate;
|
|
73
|
+
}
|
|
74
|
+
temporary(): string {
|
|
75
|
+
return this.local(`tsplcTemp${++this.temporaries}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface Frame {
|
|
80
|
+
emitter: Emitter;
|
|
81
|
+
env: Env;
|
|
82
|
+
self?: ModelNode;
|
|
83
|
+
/** The routine's declared return type, for literal and optional results. */
|
|
84
|
+
returns?: ts.Type;
|
|
85
|
+
async: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Evaluates the program's scan code against the setup graph. Everything setup decided (which
|
|
90
|
+
* instance `this` is, what a field refers to, array lengths, config never written in scan code)
|
|
91
|
+
* resolves at compile time; what is left becomes tsPLC dialect code over the stored state.
|
|
92
|
+
*/
|
|
93
|
+
export class Specializer {
|
|
94
|
+
readonly layouts: Layouts;
|
|
95
|
+
readonly routines: string[] = [];
|
|
96
|
+
/** Modules generated code reads through a namespace import, by import name. */
|
|
97
|
+
readonly namespaces = new Map<string, string>();
|
|
98
|
+
/** Value imports from kept modules, by file. */
|
|
99
|
+
readonly imports = new Map<string, Set<string>>();
|
|
100
|
+
private readonly specialized = new Map<string, string>();
|
|
101
|
+
private readonly routineNames = new Set<string>();
|
|
102
|
+
private readonly assigned = new Set<ts.Symbol>();
|
|
103
|
+
private readonly storedRoots = new Set<Root>();
|
|
104
|
+
|
|
105
|
+
constructor(
|
|
106
|
+
readonly model: Model,
|
|
107
|
+
readonly checker: ts.TypeChecker,
|
|
108
|
+
readonly sources: readonly ts.SourceFile[],
|
|
109
|
+
readonly kept: (file: string) => boolean,
|
|
110
|
+
readonly rootModule: (root: Root) => string,
|
|
111
|
+
) {
|
|
112
|
+
this.layouts = new Layouts(
|
|
113
|
+
model,
|
|
114
|
+
checker,
|
|
115
|
+
kept,
|
|
116
|
+
sources.filter((source) => kept(source.fileName)),
|
|
117
|
+
);
|
|
118
|
+
for (const source of sources) this.collectAssignments(source);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---- Which fields scan code writes ----
|
|
122
|
+
|
|
123
|
+
/** Fields scan code writes are state; the rest keep what setup gave them. */
|
|
124
|
+
private collectAssignments(node: ts.Node, inConstructor = false): void {
|
|
125
|
+
if (ts.isConstructorDeclaration(node)) inConstructor = true;
|
|
126
|
+
if (!inConstructor) {
|
|
127
|
+
const write = (target: ts.Expression): void => {
|
|
128
|
+
while (ts.isParenthesizedExpression(target) || ts.isNonNullExpression(target))
|
|
129
|
+
target = target.expression;
|
|
130
|
+
if (ts.isPropertyAccessExpression(target)) this.markAssigned(target.name);
|
|
131
|
+
else if (ts.isElementAccessExpression(target)) {
|
|
132
|
+
let base: ts.Expression = target.expression;
|
|
133
|
+
while (ts.isElementAccessExpression(base)) base = base.expression;
|
|
134
|
+
if (ts.isPropertyAccessExpression(base)) this.markAssigned(base.name);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
if (ts.isBinaryExpression(node) && isAssignment(node.operatorToken.kind)) write(node.left);
|
|
138
|
+
if (
|
|
139
|
+
(ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
|
|
140
|
+
(node.operator === ts.SyntaxKind.PlusPlusToken ||
|
|
141
|
+
node.operator === ts.SyntaxKind.MinusMinusToken)
|
|
142
|
+
)
|
|
143
|
+
write(node.operand);
|
|
144
|
+
}
|
|
145
|
+
ts.forEachChild(node, (child) => this.collectAssignments(child, inConstructor));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private markAssigned(name: ts.MemberName): void {
|
|
149
|
+
const symbol = this.checker.getSymbolAtLocation(name);
|
|
150
|
+
if (!symbol) return;
|
|
151
|
+
this.assigned.add(symbol);
|
|
152
|
+
// A property of a union is a synthetic symbol over each member's own property.
|
|
153
|
+
for (const declaration of symbol.declarations ?? []) {
|
|
154
|
+
const own = (declaration as { symbol?: ts.Symbol }).symbol;
|
|
155
|
+
if (own) this.assigned.add(own);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The declaration symbol of a captured field, to ask whether scan code writes it. */
|
|
160
|
+
private fieldSymbol(holder: ModelNode, name: string): ts.Symbol | undefined {
|
|
161
|
+
if (holder.captured.kind === "instance") {
|
|
162
|
+
const member = this.model.member(this.model.classOf(holder), name);
|
|
163
|
+
return member && this.checker.getSymbolAtLocation(member.name);
|
|
164
|
+
}
|
|
165
|
+
if (!holder.type) return undefined;
|
|
166
|
+
const variant = this.model.variant(holder.type, holder.captured);
|
|
167
|
+
return variant.getProperty(name) ?? this.checker.getPropertyOfType(variant, name);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Whether a field holds state the PLC stores, rather than a value fixed at setup. */
|
|
171
|
+
stored(holder: ModelNode, name: string): boolean {
|
|
172
|
+
if (holder.owner?.root.exported) return true;
|
|
173
|
+
return this.written(holder, name);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Whether scan code assigns the field itself. */
|
|
177
|
+
private written(holder: ModelNode, name: string): boolean {
|
|
178
|
+
const symbol = this.fieldSymbol(holder, name);
|
|
179
|
+
if (!symbol) return false;
|
|
180
|
+
if (this.assigned.has(symbol)) return true;
|
|
181
|
+
// A union's property is written through any member's declaration of it.
|
|
182
|
+
return (symbol.declarations ?? []).some((declaration) => {
|
|
183
|
+
const own = (declaration as { symbol?: ts.Symbol }).symbol;
|
|
184
|
+
return own !== undefined && this.assigned.has(own);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Array elements are stored when the field holding the array is, or its root is exported. */
|
|
189
|
+
private storedArray(array: ModelNode): boolean {
|
|
190
|
+
const owner = array.owner;
|
|
191
|
+
if (!owner) return false;
|
|
192
|
+
if (owner.root.exported) return true;
|
|
193
|
+
const last = owner.steps.at(-1);
|
|
194
|
+
if (!last) return false;
|
|
195
|
+
const holder = this.at(owner.root, owner.steps.slice(0, -1));
|
|
196
|
+
return last.kind === "field" && holder !== undefined && this.stored(holder, last.name);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The node at a path from a root, following captured values. */
|
|
200
|
+
private at(root: Root, steps: readonly Step[]): ModelNode | undefined {
|
|
201
|
+
let value: CapturedValue | undefined = this.model.rootValues.get(root);
|
|
202
|
+
for (const step of steps) {
|
|
203
|
+
const node = value && this.model.node(value);
|
|
204
|
+
if (!node) return undefined;
|
|
205
|
+
const captured = node.captured;
|
|
206
|
+
value =
|
|
207
|
+
step.kind === "index"
|
|
208
|
+
? captured.kind === "array"
|
|
209
|
+
? captured.items[step.index]
|
|
210
|
+
: undefined
|
|
211
|
+
: captured.kind === "object" || captured.kind === "instance"
|
|
212
|
+
? captured.fields[step.name]
|
|
213
|
+
: undefined;
|
|
214
|
+
}
|
|
215
|
+
return value && this.model.node(value);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---- Storage ----
|
|
219
|
+
|
|
220
|
+
/** Dialect text for a stored node's place. */
|
|
221
|
+
pathText(node: ModelNode): string {
|
|
222
|
+
const owner = node.owner;
|
|
223
|
+
if (!owner) throw new Error("An object with no owner has no storage.");
|
|
224
|
+
this.storedRoots.add(owner.root);
|
|
225
|
+
const namespace = this.namespace(owner.root);
|
|
226
|
+
let text = this.flattened(owner.root) ? namespace : `${namespace}.${owner.root.name}`;
|
|
227
|
+
owner.steps.forEach((step, index) => {
|
|
228
|
+
const holder = this.at(owner.root, owner.steps.slice(0, index));
|
|
229
|
+
if (step.kind === "index") text += `[${step.index}]`;
|
|
230
|
+
else if (/^\d+$/.test(step.name) && holder?.type)
|
|
231
|
+
text += `[${this.keyText(holder.type, step.name)}]`;
|
|
232
|
+
else text += `.${step.name}`;
|
|
233
|
+
});
|
|
234
|
+
return text;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The enum member naming a key of a `Record<Enum, T>`. */
|
|
238
|
+
private keyText(type: ts.Type, key: string): string {
|
|
239
|
+
const keyType = type.aliasSymbol?.name === "Record" ? type.aliasTypeArguments?.[0] : undefined;
|
|
240
|
+
const member = keyType && this.enumMemberName(keyType, Number(key));
|
|
241
|
+
return member ? `${this.layouts.text(keyType!)}.${member}` : key;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** A record or instance root becomes its own global list, one global per field. */
|
|
245
|
+
flattened(root: Root): boolean {
|
|
246
|
+
const node = this.model.node(this.model.rootValues.get(root)!);
|
|
247
|
+
return node?.captured.kind === "object" || node?.captured.kind === "instance";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private namespace(root: Root): string {
|
|
251
|
+
const name = pascal(root.name);
|
|
252
|
+
this.namespaces.set(name, this.rootModule(root));
|
|
253
|
+
return name;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Stores a root whether or not scan code reads it, as exported data must be. */
|
|
257
|
+
use(root: Root): void {
|
|
258
|
+
this.storedRoots.add(root);
|
|
259
|
+
this.namespace(root);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Roots generated code reached, which need storage declarations. */
|
|
263
|
+
get roots(): ReadonlySet<Root> {
|
|
264
|
+
return this.storedRoots;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Whether anything inside a node is state the PLC stores. */
|
|
268
|
+
private hasState(node: ModelNode): boolean {
|
|
269
|
+
const captured = node.captured;
|
|
270
|
+
if (node.owner?.root.exported) return true;
|
|
271
|
+
if (captured.kind === "instance") return this.instanceFields(node).length > 0;
|
|
272
|
+
if (captured.kind === "array")
|
|
273
|
+
return (
|
|
274
|
+
this.storedArray(node) ||
|
|
275
|
+
captured.items.some((item) => {
|
|
276
|
+
const inner = this.model.node(item);
|
|
277
|
+
return inner !== undefined && isChild(inner, node) && this.hasState(inner);
|
|
278
|
+
})
|
|
279
|
+
);
|
|
280
|
+
if (captured.kind === "object")
|
|
281
|
+
return Object.entries(captured.fields).some(([name, value]) => {
|
|
282
|
+
if (this.stored(node, name)) return true;
|
|
283
|
+
const inner = this.model.node(value);
|
|
284
|
+
return inner !== undefined && isChild(inner, node, name) && this.hasState(inner);
|
|
285
|
+
});
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The stored layout of a node, or undefined when nothing in it is stored. Records and arrays
|
|
291
|
+
* keep their declared type; an instance stores a record of the fields that hold its state.
|
|
292
|
+
*/
|
|
293
|
+
storageType(node: ModelNode): string | undefined {
|
|
294
|
+
const captured = node.captured;
|
|
295
|
+
if (!this.hasState(node)) return undefined;
|
|
296
|
+
if (captured.kind === "instance") {
|
|
297
|
+
const fields = this.instanceFields(node);
|
|
298
|
+
return this.layoutName(
|
|
299
|
+
`${captured.name}State`,
|
|
300
|
+
fields.map(([field, text]) => `${field}: ${text};`).join(" "),
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
if (captured.kind === "array") {
|
|
304
|
+
const first = captured.items[0] && this.model.node(captured.items[0]);
|
|
305
|
+
if (first?.captured.kind === "instance") {
|
|
306
|
+
const states = captured.items.map((item) => this.storageType(this.model.node(item)!));
|
|
307
|
+
if (new Set(states).size !== 1)
|
|
308
|
+
throw new Error("Instances in one array must hold the same kinds of state.");
|
|
309
|
+
return `FixedArray<${states[0]}, ${captured.items.length}>`;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (!node.type) throw new Error("Stored state needs a declared type.");
|
|
313
|
+
return this.layouts.text(node.type, { kind: "node", id: node.id });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Records stored as a whole: anything in them is state. */
|
|
317
|
+
private storedWhole(node: ModelNode): boolean {
|
|
318
|
+
return node.captured.kind !== "instance" && this.hasState(node);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** An instance's stored fields: what scan code writes, and owned objects holding state. */
|
|
322
|
+
instanceFields(node: ModelNode): [string, string][] {
|
|
323
|
+
const captured = node.captured;
|
|
324
|
+
if (captured.kind !== "instance") return [];
|
|
325
|
+
const declaration = this.model.classOf(node);
|
|
326
|
+
return this.fieldNames(declaration).flatMap((name): [string, string][] => {
|
|
327
|
+
const value = captured.fields[name] ?? { kind: "undefined" };
|
|
328
|
+
const inner = this.model.node(value);
|
|
329
|
+
const member = this.model.member(declaration, name);
|
|
330
|
+
const type = member && this.checker.getTypeAtLocation(member.name);
|
|
331
|
+
if (inner) {
|
|
332
|
+
if (inner.captured.kind === "function" || inner.captured.kind === "service") return [];
|
|
333
|
+
if (!isChild(inner, node, name)) {
|
|
334
|
+
if (!this.stored(node, name)) return [];
|
|
335
|
+
// A stored field holds its own copy of the record setup gave it.
|
|
336
|
+
return type ? [[name, this.layouts.text(type, value)]] : [];
|
|
337
|
+
}
|
|
338
|
+
if (inner.captured.kind !== "instance" && this.stored(node, name) && type)
|
|
339
|
+
return [[name, this.layouts.text(type, value)]];
|
|
340
|
+
const text = this.storageType(inner);
|
|
341
|
+
return text ? [[name, text]] : [];
|
|
342
|
+
}
|
|
343
|
+
if (!this.stored(node, name)) return [];
|
|
344
|
+
if (!type) throw new Error(`${captured.name}.${name} has no declared type.`);
|
|
345
|
+
return [[name, this.layouts.declaredText(member, type, value)]];
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** A class's fields in declaration order: properties and constructor parameter properties. */
|
|
350
|
+
private fieldNames(declaration: ts.ClassDeclaration): string[] {
|
|
351
|
+
const names: string[] = [];
|
|
352
|
+
for (const member of declaration.members) {
|
|
353
|
+
if (ts.isPropertyDeclaration(member)) names.push(member.name.getText());
|
|
354
|
+
if (ts.isConstructorDeclaration(member))
|
|
355
|
+
for (const parameter of member.parameters)
|
|
356
|
+
if (ts.isParameterPropertyDeclaration(parameter, member))
|
|
357
|
+
names.push(parameter.name.getText());
|
|
358
|
+
}
|
|
359
|
+
return names;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private readonly layoutNames = new Map<string, string>();
|
|
363
|
+
private layoutName(base: string, body: string): string {
|
|
364
|
+
const known = this.layoutNames.get(body);
|
|
365
|
+
if (known) return known;
|
|
366
|
+
let name = base;
|
|
367
|
+
for (
|
|
368
|
+
let index = 2;
|
|
369
|
+
[...this.layoutNames.values()].includes(name) || this.layouts.declarations.has(name);
|
|
370
|
+
index++
|
|
371
|
+
)
|
|
372
|
+
name = `${base}_${index}`;
|
|
373
|
+
this.layoutNames.set(body, name);
|
|
374
|
+
this.layouts.declarations.set(name, `export interface ${name} { ${body} }`);
|
|
375
|
+
return name;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private fieldInitializer(
|
|
379
|
+
holder: ModelNode,
|
|
380
|
+
name: string,
|
|
381
|
+
value: CapturedValue,
|
|
382
|
+
type: string,
|
|
383
|
+
): string {
|
|
384
|
+
const declaredType =
|
|
385
|
+
holder.captured.kind === "instance"
|
|
386
|
+
? (() => {
|
|
387
|
+
const member = this.model.member(this.model.classOf(holder), name);
|
|
388
|
+
return member && this.checker.getTypeAtLocation(member.name);
|
|
389
|
+
})()
|
|
390
|
+
: holder.type && this.model.fieldType(holder.type, name, holder.captured);
|
|
391
|
+
if (!declaredType) throw new Error(`${name} has no declared type.`);
|
|
392
|
+
return this.valueText(value, declaredType, type);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** A captured value as dialect text of its declared type. */
|
|
396
|
+
valueText(value: CapturedValue, declared: ts.Type, layout?: string): string {
|
|
397
|
+
const node = this.model.node(value);
|
|
398
|
+
const literal = this.layouts.literalEnum(declared);
|
|
399
|
+
if (literal)
|
|
400
|
+
return this.layouts.member(literal, value.kind === "string" ? value.value : undefined);
|
|
401
|
+
const optional = this.isOptionalRecord(declared);
|
|
402
|
+
if (optional) {
|
|
403
|
+
const inner = this.checker.getNonNullableType(declared);
|
|
404
|
+
const name = layout ?? this.layouts.text(declared, value);
|
|
405
|
+
return value.kind === "undefined"
|
|
406
|
+
? `defaultValue<${name}>()`
|
|
407
|
+
: `{ has: true, value: ${this.valueText(value, inner)} }`;
|
|
408
|
+
}
|
|
409
|
+
if (this.layouts.enumDeclaration(declared) && value.kind === "number") {
|
|
410
|
+
const enumName = this.layouts.text(declared);
|
|
411
|
+
const member = this.enumMemberName(declared, value.value);
|
|
412
|
+
return member ? `${enumName}.${member}` : String(value.value);
|
|
413
|
+
}
|
|
414
|
+
if (!node) return this.literal(value);
|
|
415
|
+
if (node.captured.kind === "array") {
|
|
416
|
+
const element = this.model.elementType(declared);
|
|
417
|
+
const items = node.captured.items;
|
|
418
|
+
if (!items.length) return `defaultValue<${layout ?? this.layouts.text(declared, value)}>()`;
|
|
419
|
+
return `[${items.map((item) => (element ? this.valueText(item, element) : this.literal(item))).join(", ")}]`;
|
|
420
|
+
}
|
|
421
|
+
if (node.captured.kind === "object") return this.recordInitializer(node, declared);
|
|
422
|
+
if (node.captured.kind === "instance") {
|
|
423
|
+
const text = this.storageType(node);
|
|
424
|
+
return text ? this.instanceInitializer(node) : "{}";
|
|
425
|
+
}
|
|
426
|
+
throw new Error("A function has no stored value.");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
instanceInitializer(node: ModelNode): string {
|
|
430
|
+
return `{ ${this.instanceFields(node)
|
|
431
|
+
.map(([name, layout]) => `${name}: ${this.instanceFieldInitializer(node, name, layout)}`)
|
|
432
|
+
.join(", ")} }`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** The initial value of one of an instance's stored fields. */
|
|
436
|
+
instanceFieldInitializer(node: ModelNode, name: string, layout: string): string {
|
|
437
|
+
const captured = node.captured;
|
|
438
|
+
if (captured.kind !== "instance") throw new Error("Not an instance.");
|
|
439
|
+
const value = captured.fields[name] ?? { kind: "undefined" as const };
|
|
440
|
+
const inner = this.model.node(value);
|
|
441
|
+
if (inner?.captured.kind === "instance") return this.instanceInitializer(inner);
|
|
442
|
+
if (
|
|
443
|
+
inner?.captured.kind === "array" &&
|
|
444
|
+
inner.captured.items.some((item) => this.model.node(item)?.captured.kind === "instance")
|
|
445
|
+
)
|
|
446
|
+
return `[${inner.captured.items.map((item) => this.instanceInitializer(this.model.node(item)!)).join(", ")}]`;
|
|
447
|
+
return this.fieldInitializer(node, name, value, layout);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private recordInitializer(node: ModelNode, declared: ts.Type): string {
|
|
451
|
+
const captured = node.captured;
|
|
452
|
+
if (captured.kind !== "object") throw new Error("Not a record.");
|
|
453
|
+
const tagged = this.layouts.tagged(declared);
|
|
454
|
+
const variant = this.model.variant(declared, captured);
|
|
455
|
+
const enumRecord = declared.aliasSymbol?.name === "Record";
|
|
456
|
+
const properties = tagged
|
|
457
|
+
? this.taggedFields(tagged)
|
|
458
|
+
: this.checker
|
|
459
|
+
.getPropertiesOfType(enumRecord ? declared : variant)
|
|
460
|
+
.map((property) => property.name);
|
|
461
|
+
const parts = properties.map((name) => {
|
|
462
|
+
const field = captured.fields[name];
|
|
463
|
+
const fieldType =
|
|
464
|
+
tagged && name === tagged.tag
|
|
465
|
+
? undefined
|
|
466
|
+
: this.model.fieldType(enumRecord ? declared : variant, name, captured);
|
|
467
|
+
if (tagged && name === tagged.tag) {
|
|
468
|
+
const tag = this.layouts.literalEnum(this.tagType(declared, tagged))!;
|
|
469
|
+
return `${name}: ${this.layouts.member(tag, field?.kind === "string" ? field.value : undefined)}`;
|
|
470
|
+
}
|
|
471
|
+
const memberType = fieldType ?? this.taggedFieldType(declared, tagged!, name);
|
|
472
|
+
if (!field) return `${name}: defaultValue<${this.layouts.text(memberType)}>()`;
|
|
473
|
+
return `${enumRecord ? `[${name}]` : name}: ${this.valueText(field, memberType)}`;
|
|
474
|
+
});
|
|
475
|
+
if (enumRecord) {
|
|
476
|
+
const [keyType, valueType] = declared.aliasTypeArguments as [ts.Type, ts.Type];
|
|
477
|
+
const literal = this.layouts.literalEnum(keyType);
|
|
478
|
+
const entries = properties.map((name, index) => {
|
|
479
|
+
const key = literal
|
|
480
|
+
? this.layouts.member(literal, name)
|
|
481
|
+
: `${this.layouts.text(keyType)}.${this.enumMemberName(keyType, Number(name))}`;
|
|
482
|
+
return `[${key}]: ${parts[index]!.slice(parts[index]!.indexOf(": ") + 2)}`;
|
|
483
|
+
});
|
|
484
|
+
// An enum that is also optional elsewhere has an Undefined member the table must cover.
|
|
485
|
+
if (literal?.optional)
|
|
486
|
+
entries.unshift(
|
|
487
|
+
`[${this.layouts.member(literal, undefined)}]: ${this.defaultOf(valueType)}`,
|
|
488
|
+
);
|
|
489
|
+
return `{ ${entries.join(", ")} }`;
|
|
490
|
+
}
|
|
491
|
+
return `{ ${parts.join(", ")} }`;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private taggedFields(tagged: { tag: string; variants: ts.Type[] }): string[] {
|
|
495
|
+
const names = [tagged.tag];
|
|
496
|
+
for (const variant of tagged.variants)
|
|
497
|
+
for (const property of variant.getProperties())
|
|
498
|
+
if (!names.includes(property.name)) names.push(property.name);
|
|
499
|
+
return names;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private taggedFieldType(
|
|
503
|
+
declared: ts.Type,
|
|
504
|
+
tagged: { tag: string; variants: ts.Type[] },
|
|
505
|
+
name: string,
|
|
506
|
+
): ts.Type {
|
|
507
|
+
for (const variant of tagged.variants) {
|
|
508
|
+
const property = variant.getProperty(name);
|
|
509
|
+
if (property) return this.checker.getTypeOfSymbol(property);
|
|
510
|
+
}
|
|
511
|
+
throw new Error(`${name} is in no variant of ${this.checker.typeToString(declared)}.`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private tagType(declared: ts.Type, tagged: { tag: string; variants: ts.Type[] }): ts.Type {
|
|
515
|
+
this.layouts.text(declared);
|
|
516
|
+
const property = this.checker.getPropertyOfType(declared, tagged.tag);
|
|
517
|
+
return property ? this.checker.getTypeOfSymbol(property) : declared;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private enumMemberName(type: ts.Type, value: number): string | undefined {
|
|
521
|
+
return this.layouts
|
|
522
|
+
.enumDeclaration(type)
|
|
523
|
+
?.members.find((member) => this.checker.getConstantValue(member) === value)
|
|
524
|
+
?.name.getText();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private literal(value: CapturedValue): string {
|
|
528
|
+
switch (value.kind) {
|
|
529
|
+
case "number":
|
|
530
|
+
return String(value.value);
|
|
531
|
+
case "boolean":
|
|
532
|
+
return String(value.value);
|
|
533
|
+
case "string":
|
|
534
|
+
return JSON.stringify(value.value);
|
|
535
|
+
case "bigint":
|
|
536
|
+
return `${value.value}n`;
|
|
537
|
+
default:
|
|
538
|
+
throw new Error(`A captured ${value.kind} has no literal.`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private isOptionalRecord(type: ts.Type): boolean {
|
|
543
|
+
if (!type.isUnion() || !type.types.some((part) => part.flags & ts.TypeFlags.Undefined))
|
|
544
|
+
return false;
|
|
545
|
+
return !this.layouts.literalEnum(type);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ---- Routines ----
|
|
549
|
+
|
|
550
|
+
/** Specializes the entry function, returning its body. */
|
|
551
|
+
entry(declaration: ts.FunctionDeclaration | ts.ArrowFunction): string[] {
|
|
552
|
+
const frame: Frame = {
|
|
553
|
+
emitter: new Emitter(),
|
|
554
|
+
env: new Map(),
|
|
555
|
+
returns: undefined,
|
|
556
|
+
async: false,
|
|
557
|
+
};
|
|
558
|
+
this.body(declaration.body!, frame);
|
|
559
|
+
return frame.emitter.lines;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** The name of `declaration` specialized for `self` and these known arguments. */
|
|
563
|
+
routine(
|
|
564
|
+
declaration: FunctionLike,
|
|
565
|
+
self: ModelNode | undefined,
|
|
566
|
+
args: Value[],
|
|
567
|
+
closure?: Env,
|
|
568
|
+
): {
|
|
569
|
+
name: string;
|
|
570
|
+
args: string[];
|
|
571
|
+
returns: ts.Type | undefined;
|
|
572
|
+
} {
|
|
573
|
+
const signature = this.checker.getSignatureFromDeclaration(declaration);
|
|
574
|
+
const returnType = signature && this.checker.getReturnTypeOfSignature(signature);
|
|
575
|
+
const isAsync = Boolean(
|
|
576
|
+
ts.canHaveModifiers(declaration) &&
|
|
577
|
+
ts
|
|
578
|
+
.getModifiers(declaration)
|
|
579
|
+
?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword),
|
|
580
|
+
);
|
|
581
|
+
const frame: Frame = {
|
|
582
|
+
emitter: new Emitter(),
|
|
583
|
+
env: new Map(closure),
|
|
584
|
+
self,
|
|
585
|
+
returns: returnType && (isAsync ? this.checker.getAwaitedType(returnType) : returnType),
|
|
586
|
+
async: isAsync,
|
|
587
|
+
};
|
|
588
|
+
const parameters: string[] = [];
|
|
589
|
+
const runtimeArgs: string[] = [];
|
|
590
|
+
const keys: string[] = [];
|
|
591
|
+
const parameterList = ts.isGetAccessorDeclaration(declaration) ? [] : declaration.parameters;
|
|
592
|
+
parameterList.forEach((parameter, index) => {
|
|
593
|
+
const arg = args[index] ?? constant(undefined);
|
|
594
|
+
const declared = this.checker.getTypeAtLocation(parameter);
|
|
595
|
+
this.bindParameter(parameter.name, arg, declared, frame, parameters, runtimeArgs, keys);
|
|
596
|
+
});
|
|
597
|
+
// Captured runtime locals become parameters of the specialized routine.
|
|
598
|
+
for (const [symbol, value] of closure ?? [])
|
|
599
|
+
if (value.kind === "code") {
|
|
600
|
+
const name = frame.emitter.local(symbol.name);
|
|
601
|
+
const type = value.declared ? this.layouts.text(value.declared) : "number";
|
|
602
|
+
parameters.push(`${name}: ${type}`);
|
|
603
|
+
runtimeArgs.push(value.text);
|
|
604
|
+
frame.env.set(symbol, { kind: "code", text: name, declared: value.declared, place: true });
|
|
605
|
+
keys.push(`${symbol.name}=code`);
|
|
606
|
+
}
|
|
607
|
+
const key = `${declarationKey(declaration)}|${self?.id ?? ""}|${keys.join(",")}`;
|
|
608
|
+
const known = this.specialized.get(key);
|
|
609
|
+
if (known) return { name: known, args: runtimeArgs, returns: frame.returns };
|
|
610
|
+
const name = this.routineName(declaration, self);
|
|
611
|
+
this.specialized.set(key, name);
|
|
612
|
+
if (ts.isArrowFunction(declaration) && !ts.isBlock(declaration.body)) {
|
|
613
|
+
const result = this.expression(declaration.body, frame, frame.returns);
|
|
614
|
+
frame.emitter.line(`return ${this.code(result, frame.returns, frame)};`);
|
|
615
|
+
} else this.body(declaration.body!, frame);
|
|
616
|
+
const resultText =
|
|
617
|
+
frame.returns && !(frame.returns.flags & ts.TypeFlags.Void)
|
|
618
|
+
? this.layouts.text(frame.returns)
|
|
619
|
+
: "void";
|
|
620
|
+
const head = `${frame.async ? "async " : ""}function ${name}(${parameters.join(", ")}): ${
|
|
621
|
+
frame.async ? `Promise<${resultText}>` : resultText
|
|
622
|
+
} {`;
|
|
623
|
+
const where = self?.owner
|
|
624
|
+
? ` /* ${self.owner.root.name}${self.owner.steps.map(stepText).join("")} */`
|
|
625
|
+
: "";
|
|
626
|
+
this.routines.push([`${head}${where}`, ...frame.emitter.lines, "}"].join("\n"));
|
|
627
|
+
return { name, args: runtimeArgs, returns: frame.returns };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
private routineName(declaration: FunctionLike, self: ModelNode | undefined): string {
|
|
631
|
+
const own =
|
|
632
|
+
declaration.name && ts.isIdentifier(declaration.name) ? declaration.name.text : undefined;
|
|
633
|
+
const variable =
|
|
634
|
+
!own &&
|
|
635
|
+
declaration.parent &&
|
|
636
|
+
ts.isVariableDeclaration(declaration.parent) &&
|
|
637
|
+
ts.isIdentifier(declaration.parent.name)
|
|
638
|
+
? declaration.parent.name.text
|
|
639
|
+
: undefined;
|
|
640
|
+
const owner = self?.captured.kind === "instance" ? self.captured.name : undefined;
|
|
641
|
+
const base = [owner, own ?? variable ?? "callback"].filter(Boolean).join("_");
|
|
642
|
+
let name = base;
|
|
643
|
+
for (let index = 2; this.routineNames.has(name); index++) name = `${base}_${index}`;
|
|
644
|
+
this.routineNames.add(name);
|
|
645
|
+
return name;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private bindParameter(
|
|
649
|
+
name: ts.BindingName,
|
|
650
|
+
arg: Value,
|
|
651
|
+
declared: ts.Type,
|
|
652
|
+
frame: Frame,
|
|
653
|
+
parameters: string[],
|
|
654
|
+
runtimeArgs: string[],
|
|
655
|
+
keys: string[],
|
|
656
|
+
): void {
|
|
657
|
+
if (ts.isObjectBindingPattern(name)) {
|
|
658
|
+
for (const element of name.elements) {
|
|
659
|
+
const field = (element.propertyName ?? element.name).getText();
|
|
660
|
+
const inner = this.member(arg, field, element, frame);
|
|
661
|
+
this.bindParameter(
|
|
662
|
+
element.name,
|
|
663
|
+
inner,
|
|
664
|
+
this.checker.getTypeAtLocation(element),
|
|
665
|
+
frame,
|
|
666
|
+
parameters,
|
|
667
|
+
runtimeArgs,
|
|
668
|
+
keys,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (ts.isArrayBindingPattern(name))
|
|
674
|
+
throw new SpecializeError(name, "Array patterns are not supported in parameters.");
|
|
675
|
+
const symbol = this.checker.getSymbolAtLocation(name)!;
|
|
676
|
+
if (arg.kind === "code" || (arg.kind === "virtual" && !this.isKnownShape(arg))) {
|
|
677
|
+
const local = frame.emitter.local(name.text);
|
|
678
|
+
const iec = this.layouts.iecName(name.parent);
|
|
679
|
+
parameters.push(`${local}: ${iec ?? this.layouts.text(declared)}`);
|
|
680
|
+
runtimeArgs.push(this.assignmentText(arg, declared, frame, iec));
|
|
681
|
+
frame.env.set(symbol, { kind: "code", text: local, declared, place: true });
|
|
682
|
+
keys.push(`${name.text}=code`);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (
|
|
686
|
+
arg.kind === "const" &&
|
|
687
|
+
!(declared.flags & ts.TypeFlags.Undefined) &&
|
|
688
|
+
typeof arg.value !== "undefined"
|
|
689
|
+
) {
|
|
690
|
+
// Runtime primitives stay parameters, so one routine serves every value.
|
|
691
|
+
const local = frame.emitter.local(name.text);
|
|
692
|
+
parameters.push(`${local}: ${this.layouts.text(declared)}`);
|
|
693
|
+
runtimeArgs.push(this.code(arg, declared, frame));
|
|
694
|
+
frame.env.set(symbol, { kind: "code", text: local, declared, place: true });
|
|
695
|
+
keys.push(`${name.text}=code`);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
frame.env.set(symbol, arg);
|
|
699
|
+
keys.push(`${name.text}=${valueKey(arg)}`);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
private isKnownShape(value: Value): boolean {
|
|
703
|
+
return (
|
|
704
|
+
value.kind === "virtual" &&
|
|
705
|
+
[...(value.fields?.values() ?? []), ...(value.items ?? [])].some(
|
|
706
|
+
(inner) => inner.kind === "static" || inner.kind === "function",
|
|
707
|
+
)
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ---- Statements ----
|
|
712
|
+
|
|
713
|
+
private body(node: ts.ConciseBody, frame: Frame): void {
|
|
714
|
+
if (ts.isBlock(node)) for (const statement of node.statements) this.statement(statement, frame);
|
|
715
|
+
else frame.emitter.line(`${this.code(this.expression(node, frame), undefined, frame)};`);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
private statement(node: ts.Statement, frame: Frame): void {
|
|
719
|
+
const { emitter } = frame;
|
|
720
|
+
if (ts.isBlock(node)) {
|
|
721
|
+
emitter.open("{");
|
|
722
|
+
for (const statement of node.statements) this.statement(statement, frame);
|
|
723
|
+
emitter.close();
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (ts.isVariableStatement(node)) {
|
|
727
|
+
const mutable = !(node.declarationList.flags & ts.NodeFlags.Const);
|
|
728
|
+
for (const declaration of node.declarationList.declarations)
|
|
729
|
+
this.declare(declaration, mutable, frame);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (ts.isExpressionStatement(node)) {
|
|
733
|
+
this.effect(node.expression, frame);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (ts.isIfStatement(node)) {
|
|
737
|
+
const condition = this.expression(node.expression, frame);
|
|
738
|
+
if (
|
|
739
|
+
condition.kind === "const" ||
|
|
740
|
+
condition.kind === "static" ||
|
|
741
|
+
condition.kind === "function"
|
|
742
|
+
) {
|
|
743
|
+
const truthy = condition.kind === "const" ? Boolean(condition.value) : true;
|
|
744
|
+
if (truthy) this.statement(node.thenStatement, frame);
|
|
745
|
+
else if (node.elseStatement) this.statement(node.elseStatement, frame);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
emitter.open(`if (${this.condition(condition, node.expression, frame)}) {`);
|
|
749
|
+
this.inner(node.thenStatement, frame);
|
|
750
|
+
if (node.elseStatement) {
|
|
751
|
+
emitter.close("} else {");
|
|
752
|
+
emitter.open("");
|
|
753
|
+
emitter.lines.pop();
|
|
754
|
+
this.inner(node.elseStatement, frame);
|
|
755
|
+
}
|
|
756
|
+
emitter.close();
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (ts.isReturnStatement(node)) {
|
|
760
|
+
if (!node.expression) {
|
|
761
|
+
emitter.line("return;");
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (ts.isSatisfiesExpression(node.expression) && this.isNever(node.expression.type)) {
|
|
765
|
+
emitter.line(`return ${this.defaultOf(frame.returns)};`);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const value = this.expression(node.expression, frame, frame.returns);
|
|
769
|
+
emitter.line(`return ${this.code(value, frame.returns, frame)};`);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (ts.isForOfStatement(node)) {
|
|
773
|
+
this.forOf(node, frame);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (ts.isSwitchStatement(node)) {
|
|
777
|
+
this.switch(node, frame);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
if (ts.isForStatement(node) || ts.isWhileStatement(node)) {
|
|
781
|
+
throw new SpecializeError(
|
|
782
|
+
node,
|
|
783
|
+
"Use for…of over an array; counted and while loops are not lowered yet.",
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
if (ts.isTryStatement(node)) {
|
|
787
|
+
emitter.open("try {");
|
|
788
|
+
for (const statement of node.tryBlock.statements) this.statement(statement, frame);
|
|
789
|
+
if (node.catchClause) {
|
|
790
|
+
emitter.close(
|
|
791
|
+
`} catch${node.catchClause.variableDeclaration ? ` (${node.catchClause.variableDeclaration.name.getText()})` : ""} {`,
|
|
792
|
+
);
|
|
793
|
+
emitter.open("");
|
|
794
|
+
emitter.lines.pop();
|
|
795
|
+
for (const statement of node.catchClause.block.statements) this.statement(statement, frame);
|
|
796
|
+
}
|
|
797
|
+
emitter.close();
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (ts.isBreakStatement(node) || ts.isContinueStatement(node)) {
|
|
801
|
+
emitter.line(node.getText());
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (ts.isEmptyStatement(node)) return;
|
|
805
|
+
throw new SpecializeError(node, `${ts.SyntaxKind[node.kind]} is not supported in scan code.`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** A branch body: blocks keep their statements without another brace level. */
|
|
809
|
+
private inner(node: ts.Statement, frame: Frame): void {
|
|
810
|
+
if (ts.isBlock(node)) for (const statement of node.statements) this.statement(statement, frame);
|
|
811
|
+
else this.statement(node, frame);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
private isNever(type: ts.TypeNode): boolean {
|
|
815
|
+
return type.kind === ts.SyntaxKind.NeverKeyword;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
private defaultOf(type: ts.Type | undefined): string {
|
|
819
|
+
if (!type || type.flags & ts.TypeFlags.Void) return "";
|
|
820
|
+
if (type.flags & ts.TypeFlags.BooleanLike) return "false";
|
|
821
|
+
if (type.flags & ts.TypeFlags.NumberLike) return "0";
|
|
822
|
+
return `defaultValue<${this.layouts.text(type)}>()`;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
private declare(declaration: ts.VariableDeclaration, mutable: boolean, frame: Frame): void {
|
|
826
|
+
const initializer = declaration.initializer;
|
|
827
|
+
if (!initializer)
|
|
828
|
+
throw new SpecializeError(declaration, "Initialize locals where they are declared.");
|
|
829
|
+
const value = this.expression(
|
|
830
|
+
initializer,
|
|
831
|
+
frame,
|
|
832
|
+
declaration.type && this.checker.getTypeAtLocation(declaration.type),
|
|
833
|
+
);
|
|
834
|
+
this.bind(
|
|
835
|
+
declaration.name,
|
|
836
|
+
value,
|
|
837
|
+
mutable,
|
|
838
|
+
frame,
|
|
839
|
+
this.checker.getTypeAtLocation(declaration.name),
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/** Binds a local, or each name of a destructuring pattern, to a value. */
|
|
844
|
+
private bind(
|
|
845
|
+
name: ts.BindingName,
|
|
846
|
+
value: Value,
|
|
847
|
+
mutable: boolean,
|
|
848
|
+
frame: Frame,
|
|
849
|
+
declared: ts.Type,
|
|
850
|
+
): void {
|
|
851
|
+
if (!ts.isIdentifier(name) && value.kind === "code" && !value.place)
|
|
852
|
+
value = this.temporaryOf(value, frame);
|
|
853
|
+
if (ts.isObjectBindingPattern(name)) {
|
|
854
|
+
for (const element of name.elements) {
|
|
855
|
+
const field = (element.propertyName ?? element.name).getText();
|
|
856
|
+
const inner = this.member(value, field, element, frame);
|
|
857
|
+
this.bind(
|
|
858
|
+
element.name,
|
|
859
|
+
inner,
|
|
860
|
+
mutable,
|
|
861
|
+
frame,
|
|
862
|
+
this.checker.getTypeAtLocation(element.name),
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (ts.isArrayBindingPattern(name)) {
|
|
868
|
+
name.elements.forEach((element, index) => {
|
|
869
|
+
if (ts.isOmittedExpression(element)) return;
|
|
870
|
+
const inner = this.element(value, constant(index), element, frame);
|
|
871
|
+
this.bind(
|
|
872
|
+
element.name,
|
|
873
|
+
inner,
|
|
874
|
+
mutable,
|
|
875
|
+
frame,
|
|
876
|
+
this.checker.getTypeAtLocation(element.name),
|
|
877
|
+
);
|
|
878
|
+
});
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const symbol = this.checker.getSymbolAtLocation(name)!;
|
|
882
|
+
if (!mutable && value.kind !== "code") {
|
|
883
|
+
frame.env.set(symbol, value);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (!mutable && value.kind === "code" && value.place) {
|
|
887
|
+
// A const naming a place is an alias, as an object binding is in TypeScript.
|
|
888
|
+
if (this.isAggregate(declared)) {
|
|
889
|
+
frame.env.set(symbol, value);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const local = frame.emitter.local(name.text);
|
|
894
|
+
const type = value.kind === "code" && value.iec ? value.iec : this.localType(declared);
|
|
895
|
+
frame.emitter.line(
|
|
896
|
+
`${mutable ? "let" : "const"} ${local}: ${type} = ${this.code(value, declared, frame)};`,
|
|
897
|
+
);
|
|
898
|
+
frame.env.set(symbol, {
|
|
899
|
+
kind: "code",
|
|
900
|
+
text: local,
|
|
901
|
+
declared,
|
|
902
|
+
place: true,
|
|
903
|
+
iec: value.kind === "code" ? value.iec : undefined,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/** A local's dialect type: a literal union keeps its enum; a lone literal widens to its base type. */
|
|
908
|
+
private localType(declared: ts.Type): string {
|
|
909
|
+
if (this.layouts.literalEnum(declared)) return this.layouts.text(declared);
|
|
910
|
+
return this.layouts.text(this.checker.getBaseTypeOfLiteralType(declared));
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
private isAggregate(type: ts.Type): boolean {
|
|
914
|
+
return Boolean(type.flags & ts.TypeFlags.Object) && !this.layouts.literalEnum(type);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
private forOf(node: ts.ForOfStatement, frame: Frame): void {
|
|
918
|
+
const iterable = this.expression(node.expression, frame);
|
|
919
|
+
const items = this.items(iterable);
|
|
920
|
+
const declaration = node.initializer;
|
|
921
|
+
if (!ts.isVariableDeclarationList(declaration) || declaration.declarations.length !== 1)
|
|
922
|
+
throw new SpecializeError(node.initializer, "Declare one const in for…of.");
|
|
923
|
+
const name = declaration.declarations[0]!.name;
|
|
924
|
+
const declared = this.checker.getTypeAtLocation(name);
|
|
925
|
+
if (items) {
|
|
926
|
+
for (const item of items) {
|
|
927
|
+
frame.emitter.open("{");
|
|
928
|
+
this.bind(name, item, false, frame, declared);
|
|
929
|
+
this.inner(node.statement, frame);
|
|
930
|
+
frame.emitter.close();
|
|
931
|
+
}
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (iterable.kind !== "code")
|
|
935
|
+
throw new SpecializeError(node.expression, "for…of needs an array.");
|
|
936
|
+
if (!ts.isIdentifier(name))
|
|
937
|
+
throw new SpecializeError(name, "Destructure a runtime element inside the loop.");
|
|
938
|
+
const local = frame.emitter.local(name.text);
|
|
939
|
+
frame.emitter.open(`for (const ${local} of ${iterable.text}) {`);
|
|
940
|
+
frame.env.set(this.checker.getSymbolAtLocation(name)!, {
|
|
941
|
+
kind: "code",
|
|
942
|
+
text: local,
|
|
943
|
+
declared,
|
|
944
|
+
place: true,
|
|
945
|
+
});
|
|
946
|
+
this.inner(node.statement, frame);
|
|
947
|
+
frame.emitter.close();
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* The elements of an array setup built or scan code assembled from known parts; undefined for
|
|
952
|
+
* a runtime array. Known arrays unroll, so each element's identity stays known.
|
|
953
|
+
*/
|
|
954
|
+
private items(value: Value): Value[] | undefined {
|
|
955
|
+
if (value.kind === "virtual" && value.items) return value.items;
|
|
956
|
+
if (value.kind !== "static") return undefined;
|
|
957
|
+
const array = value.node;
|
|
958
|
+
const captured = array.captured;
|
|
959
|
+
if (captured.kind !== "array") return undefined;
|
|
960
|
+
return captured.items.map((item, index) =>
|
|
961
|
+
this.fromCaptured(item, array, { kind: "index", index }),
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
private switch(node: ts.SwitchStatement, frame: Frame): void {
|
|
966
|
+
const subject = this.expression(node.expression, frame);
|
|
967
|
+
const declared = subject.kind === "code" ? subject.declared : undefined;
|
|
968
|
+
if (subject.kind === "const") {
|
|
969
|
+
let matched = false;
|
|
970
|
+
for (const clause of node.caseBlock.clauses) {
|
|
971
|
+
if (!matched) {
|
|
972
|
+
if (ts.isDefaultClause(clause)) matched = true;
|
|
973
|
+
else {
|
|
974
|
+
const label = this.expression(clause.expression, frame);
|
|
975
|
+
matched = label.kind === "const" && label.value === subject.value;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (!matched) continue;
|
|
979
|
+
for (const statement of clause.statements) {
|
|
980
|
+
if (ts.isBreakStatement(statement)) return;
|
|
981
|
+
this.statement(statement, frame);
|
|
982
|
+
}
|
|
983
|
+
if (clause.statements.some((statement) => ts.isReturnStatement(statement))) return;
|
|
984
|
+
}
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
frame.emitter.open(`switch (${this.code(subject, declared, frame)}) {`);
|
|
988
|
+
for (const clause of node.caseBlock.clauses) {
|
|
989
|
+
if (ts.isDefaultClause(clause)) {
|
|
990
|
+
const unreachable = clause.statements.some(
|
|
991
|
+
(statement) =>
|
|
992
|
+
ts.isReturnStatement(statement) &&
|
|
993
|
+
statement.expression &&
|
|
994
|
+
ts.isSatisfiesExpression(statement.expression) &&
|
|
995
|
+
this.isNever(statement.expression.type),
|
|
996
|
+
);
|
|
997
|
+
frame.emitter.open("default: {");
|
|
998
|
+
if (unreachable)
|
|
999
|
+
frame.emitter.line(
|
|
1000
|
+
frame.returns && !(frame.returns.flags & ts.TypeFlags.Void)
|
|
1001
|
+
? `return ${this.defaultOf(frame.returns)};`
|
|
1002
|
+
: "break;",
|
|
1003
|
+
);
|
|
1004
|
+
else for (const statement of clause.statements) this.statement(statement, frame);
|
|
1005
|
+
frame.emitter.close();
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
const label = this.expression(clause.expression, frame, declared);
|
|
1009
|
+
frame.emitter.open(`case ${this.code(label, declared, frame)}: {`);
|
|
1010
|
+
for (const statement of clause.statements) this.statement(statement, frame);
|
|
1011
|
+
frame.emitter.close();
|
|
1012
|
+
}
|
|
1013
|
+
frame.emitter.close();
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/** An expression evaluated for its effect. */
|
|
1017
|
+
private effect(node: ts.Expression, frame: Frame): void {
|
|
1018
|
+
if (ts.isVoidExpression(node)) {
|
|
1019
|
+
const value = this.expression(node.expression, frame);
|
|
1020
|
+
if (value.kind === "code") frame.emitter.line(`void ${value.text};`);
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
if (ts.isBinaryExpression(node) && isAssignment(node.operatorToken.kind)) {
|
|
1024
|
+
this.assign(node, frame);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
if (
|
|
1028
|
+
(ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
|
|
1029
|
+
(node.operator === ts.SyntaxKind.PlusPlusToken ||
|
|
1030
|
+
node.operator === ts.SyntaxKind.MinusMinusToken)
|
|
1031
|
+
) {
|
|
1032
|
+
const target = this.expression(node.operand, frame);
|
|
1033
|
+
const text = this.placeText(target, node.operand);
|
|
1034
|
+
frame.emitter.line(
|
|
1035
|
+
this.store(
|
|
1036
|
+
target,
|
|
1037
|
+
`${text} ${node.operator === ts.SyntaxKind.PlusPlusToken ? "+" : "-"} 1`,
|
|
1038
|
+
),
|
|
1039
|
+
);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
const value = this.expression(node, frame);
|
|
1043
|
+
if (value.kind === "code" && value.text.includes("(")) frame.emitter.line(`${value.text};`);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
private assign(node: ts.BinaryExpression, frame: Frame): void {
|
|
1047
|
+
const target = this.expression(node.left, frame);
|
|
1048
|
+
const text = this.placeText(target, node.left);
|
|
1049
|
+
const declared = target.kind === "code" ? target.declared : undefined;
|
|
1050
|
+
const operator = node.operatorToken.kind;
|
|
1051
|
+
if (operator === ts.SyntaxKind.QuestionQuestionEqualsToken) {
|
|
1052
|
+
const value = this.expression(
|
|
1053
|
+
node.right,
|
|
1054
|
+
frame,
|
|
1055
|
+
declared && this.checker.getNonNullableType(declared),
|
|
1056
|
+
);
|
|
1057
|
+
frame.emitter.open(`if (${this.isUndefinedText(text, declared)}) {`);
|
|
1058
|
+
frame.emitter.line(
|
|
1059
|
+
this.store(
|
|
1060
|
+
target,
|
|
1061
|
+
this.wrapDefined(
|
|
1062
|
+
this.code(value, declared && this.checker.getNonNullableType(declared), frame),
|
|
1063
|
+
declared,
|
|
1064
|
+
),
|
|
1065
|
+
),
|
|
1066
|
+
);
|
|
1067
|
+
frame.emitter.close();
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
const value = this.expression(node.right, frame, declared);
|
|
1071
|
+
if (operator === ts.SyntaxKind.EqualsToken) {
|
|
1072
|
+
frame.emitter.line(
|
|
1073
|
+
this.store(
|
|
1074
|
+
target,
|
|
1075
|
+
this.assignmentText(
|
|
1076
|
+
value,
|
|
1077
|
+
declared,
|
|
1078
|
+
frame,
|
|
1079
|
+
target.kind === "code" ? target.iec : undefined,
|
|
1080
|
+
),
|
|
1081
|
+
),
|
|
1082
|
+
);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
const binary = compoundOperator(operator);
|
|
1086
|
+
if (!binary)
|
|
1087
|
+
throw new SpecializeError(node, `${node.operatorToken.getText()} is not supported.`);
|
|
1088
|
+
frame.emitter.line(
|
|
1089
|
+
this.store(target, `${text} ${binary} ${this.code(value, declared, frame)}`),
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/** A statement storing `text` into a place, through its module's setter when it has one. */
|
|
1094
|
+
private store(target: Value, text: string): string {
|
|
1095
|
+
if (target.kind !== "code") throw new Error("Not a place.");
|
|
1096
|
+
return target.setter ? `${target.setter}(${text});` : `${target.text} = ${text};`;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/** The text stored into a place of `declared` type, wrapping an optional's value. */
|
|
1100
|
+
private assignmentText(
|
|
1101
|
+
value: Value,
|
|
1102
|
+
declared: ts.Type | undefined,
|
|
1103
|
+
frame: Frame,
|
|
1104
|
+
iec?: string,
|
|
1105
|
+
): string {
|
|
1106
|
+
const target = iec ?? (declared && declared.flags & ts.TypeFlags.Number ? "DINT" : undefined);
|
|
1107
|
+
if (target && value.kind === "code" && needsConversion(value.iec ?? "DINT", target)) {
|
|
1108
|
+
// A TypeScript number holds any of these; in the PLC a value converts as an IEC assignment does.
|
|
1109
|
+
this.typeImports.add("wrap");
|
|
1110
|
+
return `wrap<${target}>(${this.code(value, undefined, frame)})`;
|
|
1111
|
+
}
|
|
1112
|
+
if (declared && this.isOptionalRecord(declared)) {
|
|
1113
|
+
if (value.kind === "const" && value.value === undefined)
|
|
1114
|
+
return `defaultValue<${this.layouts.text(declared)}>()`;
|
|
1115
|
+
if (value.kind === "code" && value.declared && this.isOptionalRecord(value.declared))
|
|
1116
|
+
return value.text;
|
|
1117
|
+
return this.wrapDefined(
|
|
1118
|
+
this.code(value, this.checker.getNonNullableType(declared), frame),
|
|
1119
|
+
declared,
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
return this.code(value, declared, frame);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
private wrapDefined(text: string, declared: ts.Type | undefined): string {
|
|
1126
|
+
return declared && this.isOptionalRecord(declared) ? `{ has: true, value: ${text} }` : text;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
private isUndefinedText(text: string, declared: ts.Type | undefined): string {
|
|
1130
|
+
if (declared && this.isOptionalRecord(declared)) return `!${text}.has`;
|
|
1131
|
+
const literal = declared && this.layouts.literalEnum(declared);
|
|
1132
|
+
if (literal) return `${text} === ${this.layouts.member(literal, undefined)}`;
|
|
1133
|
+
throw new Error("Only optional values can be undefined.");
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
private placeText(value: Value, node: ts.Node): string {
|
|
1137
|
+
if (value.kind === "code" && value.place) return value.text;
|
|
1138
|
+
throw new SpecializeError(node, "Assign to stored state: a field scan code owns, or a let.");
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// ---- Expressions ----
|
|
1142
|
+
|
|
1143
|
+
expression(node: ts.Expression, frame: Frame, expected?: ts.Type): Value {
|
|
1144
|
+
if (
|
|
1145
|
+
ts.isParenthesizedExpression(node) ||
|
|
1146
|
+
ts.isNonNullExpression(node) ||
|
|
1147
|
+
ts.isAsExpression(node) ||
|
|
1148
|
+
ts.isSatisfiesExpression(node)
|
|
1149
|
+
)
|
|
1150
|
+
return this.expression(node.expression, frame, expected);
|
|
1151
|
+
if (ts.isNumericLiteral(node)) return constant(Number(node.text));
|
|
1152
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
1153
|
+
return constant(node.text);
|
|
1154
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return constant(true);
|
|
1155
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return constant(false);
|
|
1156
|
+
if (ts.isBigIntLiteral(node)) return constant(BigInt(node.text.slice(0, -1)));
|
|
1157
|
+
if (node.kind === ts.SyntaxKind.ThisKeyword) {
|
|
1158
|
+
if (!frame.self) throw new SpecializeError(node, "`this` outside a method.");
|
|
1159
|
+
return { kind: "static", node: frame.self };
|
|
1160
|
+
}
|
|
1161
|
+
if (ts.isIdentifier(node)) return this.identifier(node, frame);
|
|
1162
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
1163
|
+
const object = this.expression(node.expression, frame);
|
|
1164
|
+
if (node.questionDotToken && object.kind === "const" && object.value === undefined)
|
|
1165
|
+
return constant(undefined);
|
|
1166
|
+
return this.narrowed(this.member(object, node.name.text, node, frame), node);
|
|
1167
|
+
}
|
|
1168
|
+
if (ts.isElementAccessExpression(node)) {
|
|
1169
|
+
const object = this.expression(node.expression, frame);
|
|
1170
|
+
const index = this.expression(node.argumentExpression, frame);
|
|
1171
|
+
return this.element(object, index, node, frame);
|
|
1172
|
+
}
|
|
1173
|
+
if (ts.isCallExpression(node)) return this.call(node, frame, expected);
|
|
1174
|
+
if (ts.isAwaitExpression(node)) {
|
|
1175
|
+
const value = this.expression(node.expression, frame);
|
|
1176
|
+
return {
|
|
1177
|
+
kind: "code",
|
|
1178
|
+
text: `await ${this.code(value, undefined, frame)}`,
|
|
1179
|
+
declared: this.checker.getTypeAtLocation(node),
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
if (ts.isPrefixUnaryExpression(node)) return this.unary(node, frame);
|
|
1183
|
+
if (ts.isBinaryExpression(node)) return this.binary(node, frame, expected);
|
|
1184
|
+
if (ts.isConditionalExpression(node)) {
|
|
1185
|
+
const condition = this.expression(node.condition, frame);
|
|
1186
|
+
if (condition.kind === "const")
|
|
1187
|
+
return this.expression(condition.value ? node.whenTrue : node.whenFalse, frame, expected);
|
|
1188
|
+
const declared = expected ?? this.checker.getTypeAtLocation(node);
|
|
1189
|
+
const whenTrue = this.expression(node.whenTrue, frame, declared);
|
|
1190
|
+
const whenFalse = this.expression(node.whenFalse, frame, declared);
|
|
1191
|
+
return {
|
|
1192
|
+
kind: "code",
|
|
1193
|
+
text: `(${this.condition(condition, node.condition, frame)} ? ${this.code(whenTrue, declared, frame)} : ${this.code(whenFalse, declared, frame)})`,
|
|
1194
|
+
declared,
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node))
|
|
1198
|
+
return { kind: "function", declaration: node, self: frame.self, env: frame.env };
|
|
1199
|
+
if (ts.isObjectLiteralExpression(node)) return this.objectLiteral(node, frame);
|
|
1200
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
1201
|
+
const items: Value[] = [];
|
|
1202
|
+
for (const element of node.elements) {
|
|
1203
|
+
if (ts.isSpreadElement(element)) {
|
|
1204
|
+
const spread = this.items(this.expression(element.expression, frame));
|
|
1205
|
+
if (!spread) throw new SpecializeError(element, "Spread an array setup built.");
|
|
1206
|
+
items.push(...spread);
|
|
1207
|
+
} else items.push(this.expression(element, frame));
|
|
1208
|
+
}
|
|
1209
|
+
return { kind: "virtual", items };
|
|
1210
|
+
}
|
|
1211
|
+
if (ts.isTemplateExpression(node))
|
|
1212
|
+
throw new SpecializeError(node, "Template strings are not supported in scan code.");
|
|
1213
|
+
throw new SpecializeError(node, `${ts.SyntaxKind[node.kind]} is not supported in scan code.`);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
private identifier(node: ts.Identifier, frame: Frame): Value {
|
|
1217
|
+
if (node.text === "undefined") return constant(undefined);
|
|
1218
|
+
let symbol = this.checker.getSymbolAtLocation(node);
|
|
1219
|
+
if (!symbol) throw new SpecializeError(node, `${node.text} is not declared.`);
|
|
1220
|
+
const local = frame.env.get(symbol);
|
|
1221
|
+
if (local) return this.narrowed(local, node);
|
|
1222
|
+
while (symbol.flags & ts.SymbolFlags.Alias) symbol = this.checker.getAliasedSymbol(symbol);
|
|
1223
|
+
const own = frame.env.get(symbol);
|
|
1224
|
+
if (own) return own;
|
|
1225
|
+
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
1226
|
+
if (!declaration) throw new SpecializeError(node, `${node.text} has no declaration.`);
|
|
1227
|
+
if (ts.isFunctionDeclaration(declaration)) return { kind: "function", declaration };
|
|
1228
|
+
if (ts.isVariableDeclaration(declaration)) {
|
|
1229
|
+
const initializer = declaration.initializer;
|
|
1230
|
+
if (initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)))
|
|
1231
|
+
return { kind: "function", declaration: initializer };
|
|
1232
|
+
const root = this.model.roots.find((item) => item.declaration === declaration);
|
|
1233
|
+
if (root) return this.rootValue(root, node);
|
|
1234
|
+
if (
|
|
1235
|
+
ts.isVariableStatement(declaration.parent.parent) &&
|
|
1236
|
+
declaration.parent.parent.parent &&
|
|
1237
|
+
ts.isSourceFile(declaration.parent.parent.parent)
|
|
1238
|
+
) {
|
|
1239
|
+
if (initializer) return this.expression(initializer, { ...frame, env: new Map() });
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (ts.isEnumMember(declaration))
|
|
1243
|
+
return constant(this.checker.getConstantValue(declaration) as number);
|
|
1244
|
+
throw new SpecializeError(node, `${node.text} is not available in scan code.`);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
private rootValue(root: Root, at: ts.Node): Value {
|
|
1248
|
+
const value = this.model.rootValues.get(root)!;
|
|
1249
|
+
const node = this.model.node(value);
|
|
1250
|
+
if (node) {
|
|
1251
|
+
if (this.kept(root.file) && node.captured.kind !== "service") return this.keptRoot(root);
|
|
1252
|
+
return { kind: "static", node };
|
|
1253
|
+
}
|
|
1254
|
+
if (this.kept(root.file) || root.declaration.parent.flags & ts.NodeFlags.Const)
|
|
1255
|
+
return this.capturedConstant(value, at);
|
|
1256
|
+
// A module `let` of a plain module is state in its own global list.
|
|
1257
|
+
const namespace = this.namespace(root);
|
|
1258
|
+
this.storedRoots.add(root);
|
|
1259
|
+
return {
|
|
1260
|
+
kind: "code",
|
|
1261
|
+
text: `${namespace}.${root.name}`,
|
|
1262
|
+
declared: this.checker.getTypeAtLocation(root.declaration.name),
|
|
1263
|
+
place: true,
|
|
1264
|
+
iec: this.layouts.iecName(root.declaration),
|
|
1265
|
+
setter: `${namespace}.${setterName(root.name)}`,
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/** A binding in a module the program keeps, read by its own name. */
|
|
1270
|
+
private keptRoot(root: Root): Value {
|
|
1271
|
+
const names = this.imports.get(root.file) ?? new Set<string>();
|
|
1272
|
+
names.add(root.name);
|
|
1273
|
+
this.imports.set(root.file, names);
|
|
1274
|
+
return {
|
|
1275
|
+
kind: "code",
|
|
1276
|
+
text: root.name,
|
|
1277
|
+
declared: this.checker.getTypeAtLocation(root.declaration.name),
|
|
1278
|
+
place: !(root.declaration.parent.flags & ts.NodeFlags.Const),
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
private capturedConstant(value: CapturedValue, at: ts.Node): Value {
|
|
1283
|
+
if (value.kind === "node") throw new SpecializeError(at, "Not a primitive.");
|
|
1284
|
+
return constant(
|
|
1285
|
+
value.kind === "undefined"
|
|
1286
|
+
? undefined
|
|
1287
|
+
: value.kind === "bigint"
|
|
1288
|
+
? BigInt(value.value)
|
|
1289
|
+
: value.value,
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/** A known node or primitive read from a captured object at one step. */
|
|
1294
|
+
private fromCaptured(value: CapturedValue, holder: ModelNode, step: Step): Value {
|
|
1295
|
+
const node = this.model.node(value);
|
|
1296
|
+
if (node) {
|
|
1297
|
+
// A field scan code assigns as a whole is a place, holding its own copy of what setup gave it.
|
|
1298
|
+
if (
|
|
1299
|
+
(node.captured.kind === "object" || node.captured.kind === "array") &&
|
|
1300
|
+
step.kind === "field" &&
|
|
1301
|
+
this.written(holder, step.name)
|
|
1302
|
+
)
|
|
1303
|
+
return this.storedField(holder, step);
|
|
1304
|
+
return { kind: "static", node };
|
|
1305
|
+
}
|
|
1306
|
+
const stored =
|
|
1307
|
+
step.kind === "field" ? this.stored(holder, step.name) : this.storedArray(holder);
|
|
1308
|
+
if (stored) return this.storedField(holder, step);
|
|
1309
|
+
return this.capturedConstant(value, holder.type?.symbol?.valueDeclaration ?? this.sources[0]!);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/** The IEC width a captured field's declaration names. */
|
|
1313
|
+
private fieldIec(holder: ModelNode, name: string): string | undefined {
|
|
1314
|
+
if (holder.captured.kind === "instance")
|
|
1315
|
+
return this.layouts.iecName(this.model.member(this.model.classOf(holder), name));
|
|
1316
|
+
return this.layouts.iecName(this.fieldSymbol(holder, name)?.valueDeclaration);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/** The IEC width of an array's elements, from the declaration holding the array. */
|
|
1320
|
+
private elementIec(array: ModelNode): string | undefined {
|
|
1321
|
+
const owner = array.owner;
|
|
1322
|
+
const last = owner?.steps.at(-1);
|
|
1323
|
+
if (!owner || last?.kind !== "field") return undefined;
|
|
1324
|
+
const holder = this.at(owner.root, owner.steps.slice(0, -1));
|
|
1325
|
+
const declaration =
|
|
1326
|
+
holder &&
|
|
1327
|
+
(holder.captured.kind === "instance"
|
|
1328
|
+
? this.model.member(this.model.classOf(holder), last.name)
|
|
1329
|
+
: this.fieldSymbol(holder, last.name)?.valueDeclaration);
|
|
1330
|
+
const node = declaration && (declaration as { type?: ts.TypeNode }).type;
|
|
1331
|
+
const element =
|
|
1332
|
+
node && ts.isArrayTypeNode(node)
|
|
1333
|
+
? node.elementType
|
|
1334
|
+
: node && ts.isTypeReferenceNode(node)
|
|
1335
|
+
? node.typeArguments?.[0]
|
|
1336
|
+
: undefined;
|
|
1337
|
+
return element ? this.layouts.iecName({ type: element } as unknown as ts.Node) : undefined;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
private storedField(holder: ModelNode, step: Step): Value {
|
|
1341
|
+
const declared =
|
|
1342
|
+
step.kind === "index"
|
|
1343
|
+
? holder.type && this.model.elementType(holder.type)
|
|
1344
|
+
: holder.captured.kind === "instance"
|
|
1345
|
+
? (() => {
|
|
1346
|
+
const member = this.model.member(this.model.classOf(holder), step.name);
|
|
1347
|
+
return member && this.checker.getTypeAtLocation(member.name);
|
|
1348
|
+
})()
|
|
1349
|
+
: holder.type && this.model.fieldType(holder.type, step.name, holder.captured);
|
|
1350
|
+
const global =
|
|
1351
|
+
step.kind === "field" &&
|
|
1352
|
+
holder.owner?.steps.length === 0 &&
|
|
1353
|
+
this.flattened(holder.owner.root);
|
|
1354
|
+
return {
|
|
1355
|
+
kind: "code",
|
|
1356
|
+
text: `${this.pathText(holder)}${step.kind === "index" ? `[${step.index}]` : `.${step.name}`}`,
|
|
1357
|
+
declared,
|
|
1358
|
+
place: true,
|
|
1359
|
+
iec: step.kind === "field" ? this.fieldIec(holder, step.name) : this.elementIec(holder),
|
|
1360
|
+
...(global && { setter: `${this.pathText(holder)}.${setterName(step.name)}` }),
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
member(object: Value, name: string, at: ts.Node, frame: Frame): Value {
|
|
1365
|
+
if (object.kind === "static") {
|
|
1366
|
+
const node = object.node;
|
|
1367
|
+
const captured = node.captured;
|
|
1368
|
+
if (captured.kind === "service")
|
|
1369
|
+
return {
|
|
1370
|
+
kind: "static",
|
|
1371
|
+
node: this.serviceNode(captured.path ? `${captured.path}.${name}` : name),
|
|
1372
|
+
};
|
|
1373
|
+
if (captured.kind === "instance") {
|
|
1374
|
+
const declaration = this.model.classOf(node);
|
|
1375
|
+
const accessor = declaration.members.find(
|
|
1376
|
+
(member): member is ts.GetAccessorDeclaration =>
|
|
1377
|
+
ts.isGetAccessorDeclaration(member) && member.name.getText() === name,
|
|
1378
|
+
);
|
|
1379
|
+
if (accessor)
|
|
1380
|
+
return this.invoke(
|
|
1381
|
+
{ kind: "function", declaration: accessor, self: node },
|
|
1382
|
+
[],
|
|
1383
|
+
at,
|
|
1384
|
+
frame,
|
|
1385
|
+
);
|
|
1386
|
+
const method = declaration.members.find(
|
|
1387
|
+
(member): member is ts.MethodDeclaration =>
|
|
1388
|
+
ts.isMethodDeclaration(member) && member.name.getText() === name,
|
|
1389
|
+
);
|
|
1390
|
+
if (method) return { kind: "function", declaration: method, self: node };
|
|
1391
|
+
}
|
|
1392
|
+
if (captured.kind === "array" && name === "length") return constant(captured.items.length);
|
|
1393
|
+
if (captured.kind === "object" || captured.kind === "instance") {
|
|
1394
|
+
const field = captured.fields[name];
|
|
1395
|
+
if (!field) {
|
|
1396
|
+
if (captured.kind === "object") return constant(undefined);
|
|
1397
|
+
throw new SpecializeError(at, `${name} is not set during setup.`);
|
|
1398
|
+
}
|
|
1399
|
+
return this.fromCaptured(field, node, { kind: "field", name });
|
|
1400
|
+
}
|
|
1401
|
+
throw new SpecializeError(at, `${name} cannot be read from this value.`);
|
|
1402
|
+
}
|
|
1403
|
+
if (object.kind === "virtual") {
|
|
1404
|
+
if (name === "length" && object.items) return constant(object.items.length);
|
|
1405
|
+
const field = object.fields?.get(name);
|
|
1406
|
+
if (field) return field;
|
|
1407
|
+
throw new SpecializeError(at, `${name} is not set.`);
|
|
1408
|
+
}
|
|
1409
|
+
if (object.kind === "code") {
|
|
1410
|
+
const declared = object.declared && this.fieldDeclared(object.declared, name, at);
|
|
1411
|
+
const property =
|
|
1412
|
+
object.declared &&
|
|
1413
|
+
this.checker.getPropertyOfType(this.checker.getNonNullableType(object.declared), name);
|
|
1414
|
+
return {
|
|
1415
|
+
kind: "code",
|
|
1416
|
+
text: `${object.text}.${name}`,
|
|
1417
|
+
declared,
|
|
1418
|
+
place: object.place,
|
|
1419
|
+
iec: this.layouts.iecName(property?.valueDeclaration),
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
if (object.kind === "const" && object.value === undefined)
|
|
1423
|
+
throw new SpecializeError(at, `Reading ${name} of undefined.`);
|
|
1424
|
+
throw new SpecializeError(at, `${name} cannot be read from this value.`);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/** An optional's value where TypeScript has narrowed out undefined, as `x` reads after `x !== undefined`. */
|
|
1428
|
+
private narrowed(value: Value, at: ts.Node): Value {
|
|
1429
|
+
if (value.kind !== "code" || !value.declared || !this.isOptionalRecord(value.declared))
|
|
1430
|
+
return value;
|
|
1431
|
+
const type = this.checker.getTypeAtLocation(at);
|
|
1432
|
+
if (type.isUnion() && type.types.some((part) => part.flags & ts.TypeFlags.Undefined))
|
|
1433
|
+
return value;
|
|
1434
|
+
if (type.flags & ts.TypeFlags.Undefined) return value;
|
|
1435
|
+
return {
|
|
1436
|
+
kind: "code",
|
|
1437
|
+
text: `${value.text}.value`,
|
|
1438
|
+
declared: this.checker.getNonNullableType(value.declared),
|
|
1439
|
+
place: value.place,
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/** The declared type of a field of a runtime record, through optionals and tagged unions. */
|
|
1444
|
+
private fieldDeclared(type: ts.Type, name: string, at: ts.Node): ts.Type | undefined {
|
|
1445
|
+
const defined = this.checker.getNonNullableType(type);
|
|
1446
|
+
const tagged = this.layouts.tagged(defined);
|
|
1447
|
+
if (tagged)
|
|
1448
|
+
return name === tagged.tag
|
|
1449
|
+
? this.tagType(defined, tagged)
|
|
1450
|
+
: this.taggedFieldType(defined, tagged, name);
|
|
1451
|
+
const property = this.checker.getPropertyOfType(defined, name);
|
|
1452
|
+
if (!property)
|
|
1453
|
+
throw new SpecializeError(
|
|
1454
|
+
at,
|
|
1455
|
+
`${name} is not a field of ${this.checker.typeToString(type)}.`,
|
|
1456
|
+
);
|
|
1457
|
+
return this.checker.getTypeOfSymbol(property);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
private readonly services = new Map<string, ModelNode>();
|
|
1461
|
+
private serviceNode(path: string): ModelNode {
|
|
1462
|
+
const known = this.services.get(path);
|
|
1463
|
+
if (known) return known;
|
|
1464
|
+
const node: ModelNode = { id: -1 - this.services.size, captured: { kind: "service", path } };
|
|
1465
|
+
this.services.set(path, node);
|
|
1466
|
+
return node;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
element(object: Value, index: Value, at: ts.Node, frame: Frame): Value {
|
|
1470
|
+
if (index.kind === "const" && typeof index.value === "number") {
|
|
1471
|
+
if (object.kind === "virtual" && object.items) {
|
|
1472
|
+
const item = object.items[index.value];
|
|
1473
|
+
if (!item) throw new SpecializeError(at, `Index ${index.value} is past the end.`);
|
|
1474
|
+
return item;
|
|
1475
|
+
}
|
|
1476
|
+
if (object.kind === "static" && object.node.captured.kind === "array") {
|
|
1477
|
+
const item = object.node.captured.items[index.value];
|
|
1478
|
+
if (!item) return constant(undefined);
|
|
1479
|
+
return this.fromCaptured(item, object.node, { kind: "index", index: index.value });
|
|
1480
|
+
}
|
|
1481
|
+
if (object.kind === "static" && object.node.captured.kind === "object") {
|
|
1482
|
+
const field = object.node.captured.fields[String(index.value)];
|
|
1483
|
+
if (!field) return constant(undefined);
|
|
1484
|
+
return this.fromCaptured(field, object.node, { kind: "field", name: String(index.value) });
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
if (object.kind === "static") {
|
|
1488
|
+
const node = object.node;
|
|
1489
|
+
// A runtime index into stored data, or into a fixed table of plain values.
|
|
1490
|
+
const declared =
|
|
1491
|
+
node.type &&
|
|
1492
|
+
(node.captured.kind === "array"
|
|
1493
|
+
? this.model.elementType(node.type)
|
|
1494
|
+
: this.recordElement(node.type));
|
|
1495
|
+
const text = this.storedNode(node) ? this.pathText(node) : this.table(node);
|
|
1496
|
+
return {
|
|
1497
|
+
kind: "code",
|
|
1498
|
+
text: `${text}[${this.code(index, undefined, frame)}]`,
|
|
1499
|
+
declared,
|
|
1500
|
+
place: true,
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
if (object.kind === "code") {
|
|
1504
|
+
const declared =
|
|
1505
|
+
object.declared &&
|
|
1506
|
+
(this.model.elementType(object.declared) ?? this.recordElement(object.declared));
|
|
1507
|
+
return {
|
|
1508
|
+
kind: "code",
|
|
1509
|
+
text: `${object.text}[${this.code(index, undefined, frame)}]`,
|
|
1510
|
+
declared,
|
|
1511
|
+
place: object.place,
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
if (object.kind === "virtual" && object.items) {
|
|
1515
|
+
const text = this.materialize(object, at, frame);
|
|
1516
|
+
return {
|
|
1517
|
+
kind: "code",
|
|
1518
|
+
text: `${text}[${this.code(index, undefined, frame)}]`,
|
|
1519
|
+
declared: this.checker.getTypeAtLocation(at),
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
throw new SpecializeError(at, "This value cannot be indexed.");
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
private recordElement(type: ts.Type): ts.Type | undefined {
|
|
1526
|
+
return type.aliasSymbol?.name === "Record" ? type.aliasTypeArguments?.[1] : undefined;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
private storedNode(node: ModelNode): boolean {
|
|
1530
|
+
return node.captured.kind === "array" ? this.storedArray(node) : this.storedWhole(node);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
private readonly tables = new Map<number, string>();
|
|
1534
|
+
readonly tableDeclarations: string[] = [];
|
|
1535
|
+
/** A fixed table setup built, emitted once for scan code that indexes it at runtime. */
|
|
1536
|
+
private table(node: ModelNode): string {
|
|
1537
|
+
const known = this.tables.get(node.id);
|
|
1538
|
+
if (known) return known;
|
|
1539
|
+
if (!node.type) throw new Error("A table needs a declared type.");
|
|
1540
|
+
const name = `tsplcTable${this.tables.size + 1}`;
|
|
1541
|
+
this.tables.set(node.id, name);
|
|
1542
|
+
const type = this.layouts.text(node.type, { kind: "node", id: node.id });
|
|
1543
|
+
this.tableDeclarations.push(
|
|
1544
|
+
`const ${name}: ${type} = ${this.valueText({ kind: "node", id: node.id }, node.type, type)};`,
|
|
1545
|
+
);
|
|
1546
|
+
return name;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/** Stores a scan-built array in a local, for indexing at runtime. */
|
|
1550
|
+
private materialize(value: Value, at: ts.Node, frame: Frame): string {
|
|
1551
|
+
const type = this.checker.getTypeAtLocation(at);
|
|
1552
|
+
const element = this.model.elementType(type);
|
|
1553
|
+
if (!element || value.kind !== "virtual" || !value.items)
|
|
1554
|
+
throw new SpecializeError(at, "Not an array.");
|
|
1555
|
+
const local = frame.emitter.temporary();
|
|
1556
|
+
frame.emitter.line(
|
|
1557
|
+
`const ${local}: FixedArray<${this.layouts.text(element)}, ${Math.max(value.items.length, 1)}> = [${value.items
|
|
1558
|
+
.map((item) => this.code(item, element, frame))
|
|
1559
|
+
.join(", ")}];`,
|
|
1560
|
+
);
|
|
1561
|
+
return local;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
private objectLiteral(node: ts.ObjectLiteralExpression, frame: Frame): Value {
|
|
1565
|
+
const fields = new Map<string, Value>();
|
|
1566
|
+
for (const property of node.properties) {
|
|
1567
|
+
if (ts.isPropertyAssignment(property))
|
|
1568
|
+
fields.set(
|
|
1569
|
+
property.name.getText(),
|
|
1570
|
+
this.expression(
|
|
1571
|
+
property.initializer,
|
|
1572
|
+
frame,
|
|
1573
|
+
this.checker.getContextualType(property.initializer),
|
|
1574
|
+
),
|
|
1575
|
+
);
|
|
1576
|
+
else if (ts.isShorthandPropertyAssignment(property)) {
|
|
1577
|
+
const symbol = this.checker.getShorthandAssignmentValueSymbol(property);
|
|
1578
|
+
const local = symbol && frame.env.get(symbol);
|
|
1579
|
+
fields.set(property.name.text, local ?? this.expression(property.name, frame));
|
|
1580
|
+
} else throw new SpecializeError(property, "Write each field of a record.");
|
|
1581
|
+
}
|
|
1582
|
+
return { kind: "virtual", fields };
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
private unary(node: ts.PrefixUnaryExpression, frame: Frame): Value {
|
|
1586
|
+
const operand = this.expression(node.operand, frame);
|
|
1587
|
+
switch (node.operator) {
|
|
1588
|
+
case ts.SyntaxKind.ExclamationToken:
|
|
1589
|
+
if (operand.kind === "const") return constant(!operand.value);
|
|
1590
|
+
if (operand.kind === "static" || operand.kind === "function") return constant(false);
|
|
1591
|
+
return {
|
|
1592
|
+
kind: "code",
|
|
1593
|
+
text: `!${this.condition(operand, node.operand, frame)}`,
|
|
1594
|
+
declared: this.checker.getBooleanType(),
|
|
1595
|
+
};
|
|
1596
|
+
case ts.SyntaxKind.MinusToken:
|
|
1597
|
+
if (operand.kind === "const" && typeof operand.value === "number")
|
|
1598
|
+
return constant(-operand.value);
|
|
1599
|
+
return {
|
|
1600
|
+
kind: "code",
|
|
1601
|
+
text: `-${this.code(operand, undefined, frame)}`,
|
|
1602
|
+
declared: this.checker.getTypeAtLocation(node),
|
|
1603
|
+
};
|
|
1604
|
+
case ts.SyntaxKind.PlusToken:
|
|
1605
|
+
return operand;
|
|
1606
|
+
default:
|
|
1607
|
+
throw new SpecializeError(node, `${ts.tokenToString(node.operator)} is not supported.`);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
private binary(node: ts.BinaryExpression, frame: Frame, expected?: ts.Type): Value {
|
|
1612
|
+
const kind = node.operatorToken.kind;
|
|
1613
|
+
if (kind === ts.SyntaxKind.AmpersandAmpersandToken || kind === ts.SyntaxKind.BarBarToken) {
|
|
1614
|
+
const left = this.expression(node.left, frame);
|
|
1615
|
+
const and = kind === ts.SyntaxKind.AmpersandAmpersandToken;
|
|
1616
|
+
if (left.kind === "const") {
|
|
1617
|
+
if (Boolean(left.value) !== and) return left;
|
|
1618
|
+
return this.expression(node.right, frame, expected);
|
|
1619
|
+
}
|
|
1620
|
+
if (left.kind === "static" || left.kind === "function")
|
|
1621
|
+
return and ? this.expression(node.right, frame, expected) : left;
|
|
1622
|
+
const right = this.expression(node.right, frame, expected);
|
|
1623
|
+
if (right.kind === "const" && typeof right.value === "boolean") {
|
|
1624
|
+
if (right.value === and)
|
|
1625
|
+
return {
|
|
1626
|
+
kind: "code",
|
|
1627
|
+
text: this.condition(left, node.left, frame),
|
|
1628
|
+
declared: this.checker.getBooleanType(),
|
|
1629
|
+
};
|
|
1630
|
+
return constant(!and);
|
|
1631
|
+
}
|
|
1632
|
+
return {
|
|
1633
|
+
kind: "code",
|
|
1634
|
+
text: `(${this.condition(left, node.left, frame)} ${and ? "&&" : "||"} ${this.condition(right, node.right, frame)})`,
|
|
1635
|
+
declared: this.checker.getBooleanType(),
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
if (kind === ts.SyntaxKind.QuestionQuestionToken) {
|
|
1639
|
+
const left = this.expression(node.left, frame);
|
|
1640
|
+
if (left.kind === "const")
|
|
1641
|
+
return left.value === undefined ? this.expression(node.right, frame, expected) : left;
|
|
1642
|
+
if (left.kind === "static") return left;
|
|
1643
|
+
const declared = left.kind === "code" ? left.declared : undefined;
|
|
1644
|
+
const right = this.expression(
|
|
1645
|
+
node.right,
|
|
1646
|
+
frame,
|
|
1647
|
+
declared && this.checker.getNonNullableType(declared),
|
|
1648
|
+
);
|
|
1649
|
+
return {
|
|
1650
|
+
kind: "code",
|
|
1651
|
+
text: `(${this.isUndefinedText(this.code(left, declared, frame), declared)} ? ${this.code(right, declared && this.checker.getNonNullableType(declared), frame)} : ${this.code(left, undefined, frame)}${declared && this.isOptionalRecord(declared) ? ".value" : ""})`,
|
|
1652
|
+
declared: declared && this.checker.getNonNullableType(declared),
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
if (isAssignment(kind)) throw new SpecializeError(node, "Assign in a statement of its own.");
|
|
1656
|
+
const equality = [
|
|
1657
|
+
ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
1658
|
+
ts.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
1659
|
+
ts.SyntaxKind.EqualsEqualsToken,
|
|
1660
|
+
ts.SyntaxKind.ExclamationEqualsToken,
|
|
1661
|
+
].includes(kind);
|
|
1662
|
+
const left = this.expression(node.left, frame);
|
|
1663
|
+
const right = this.expression(
|
|
1664
|
+
node.right,
|
|
1665
|
+
frame,
|
|
1666
|
+
left.kind === "code" ? left.declared : undefined,
|
|
1667
|
+
);
|
|
1668
|
+
const negate =
|
|
1669
|
+
kind === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
|
1670
|
+
kind === ts.SyntaxKind.ExclamationEqualsToken;
|
|
1671
|
+
if (equality) {
|
|
1672
|
+
const folded = this.equal(left, right);
|
|
1673
|
+
if (folded !== undefined) return constant(negate ? !folded : folded);
|
|
1674
|
+
const [code, other] = left.kind === "code" ? [left, right] : [right as Value, left];
|
|
1675
|
+
if (code.kind === "code" && other.kind === "const" && other.value === undefined) {
|
|
1676
|
+
const test = this.isUndefinedText(code.text, code.declared);
|
|
1677
|
+
return {
|
|
1678
|
+
kind: "code",
|
|
1679
|
+
text: negate ? `!(${test})` : test,
|
|
1680
|
+
declared: this.checker.getBooleanType(),
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
const declared = code.kind === "code" ? code.declared : undefined;
|
|
1684
|
+
const [leftText, rightText] = this.comparable(left, right, declared, frame);
|
|
1685
|
+
const text = `${leftText} ${negate ? "!==" : "==="} ${rightText}`;
|
|
1686
|
+
return { kind: "code", text: `(${text})`, declared: this.checker.getBooleanType() };
|
|
1687
|
+
}
|
|
1688
|
+
if (
|
|
1689
|
+
left.kind === "const" &&
|
|
1690
|
+
right.kind === "const" &&
|
|
1691
|
+
typeof left.value === "number" &&
|
|
1692
|
+
typeof right.value === "number"
|
|
1693
|
+
) {
|
|
1694
|
+
const folded = foldNumber(kind, left.value, right.value);
|
|
1695
|
+
if (folded !== undefined) return constant(folded);
|
|
1696
|
+
}
|
|
1697
|
+
const operator = binaryToken(kind);
|
|
1698
|
+
if (!operator)
|
|
1699
|
+
throw new SpecializeError(node, `${node.operatorToken.getText()} is not supported.`);
|
|
1700
|
+
const declared = this.checker.getTypeAtLocation(node);
|
|
1701
|
+
const [leftText, rightText] = ["<", "<=", ">", ">="].includes(operator)
|
|
1702
|
+
? this.comparable(left, right, undefined, frame)
|
|
1703
|
+
: [this.code(left, undefined, frame), this.code(right, undefined, frame)];
|
|
1704
|
+
return { kind: "code", text: `(${leftText} ${operator} ${rightText})`, declared };
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
/**
|
|
1708
|
+
* Both sides of a comparison as text. Integers of different signedness, which TypeScript
|
|
1709
|
+
* compares as numbers, both widen to LINT so the PLC compares them exactly.
|
|
1710
|
+
*/
|
|
1711
|
+
private comparable(
|
|
1712
|
+
left: Value,
|
|
1713
|
+
right: Value,
|
|
1714
|
+
declared: ts.Type | undefined,
|
|
1715
|
+
frame: Frame,
|
|
1716
|
+
): [string, string] {
|
|
1717
|
+
const leftText = this.code(left, declared, frame),
|
|
1718
|
+
rightText = this.code(right, declared, frame);
|
|
1719
|
+
if (left.kind !== "code" || right.kind !== "code") return [leftText, rightText];
|
|
1720
|
+
const from = left.iec ?? "DINT",
|
|
1721
|
+
to = right.iec ?? "DINT";
|
|
1722
|
+
if (!needsConversion(from, to) || !needsConversion(to, from)) return [leftText, rightText];
|
|
1723
|
+
this.typeImports.add("wrap");
|
|
1724
|
+
return [`wrap<LINT>(${leftText})`, `wrap<LINT>(${rightText})`];
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/** Equality known now: two known objects, or a known object against undefined. */
|
|
1728
|
+
private equal(left: Value, right: Value): boolean | undefined {
|
|
1729
|
+
if (left.kind === "const" && right.kind === "const") return left.value === right.value;
|
|
1730
|
+
if (left.kind === "static" && right.kind === "static") return left.node === right.node;
|
|
1731
|
+
if (
|
|
1732
|
+
(left.kind === "static" && right.kind === "const") ||
|
|
1733
|
+
(left.kind === "const" && right.kind === "static")
|
|
1734
|
+
)
|
|
1735
|
+
return false;
|
|
1736
|
+
return undefined;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
/** A boolean condition, including JavaScript truthiness of optionals and enums. */
|
|
1740
|
+
private condition(value: Value, at: ts.Node, frame: Frame): string {
|
|
1741
|
+
if (value.kind === "code" && value.declared) {
|
|
1742
|
+
const declared = value.declared;
|
|
1743
|
+
if (this.isOptionalRecord(declared) && !ts.isPrefixUnaryExpression(at)) {
|
|
1744
|
+
const type = this.checker.getTypeAtLocation(at);
|
|
1745
|
+
if (!(type.flags & ts.TypeFlags.BooleanLike)) return `${value.text}.has`;
|
|
1746
|
+
}
|
|
1747
|
+
const literal = this.layouts.literalEnum(declared);
|
|
1748
|
+
if (literal?.optional) return `${value.text} !== ${this.layouts.member(literal, undefined)}`;
|
|
1749
|
+
}
|
|
1750
|
+
return this.code(value, undefined, frame);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// ---- Calls ----
|
|
1754
|
+
|
|
1755
|
+
private call(node: ts.CallExpression, frame: Frame, expected?: ts.Type): Value {
|
|
1756
|
+
const callee = node.expression;
|
|
1757
|
+
// Array methods and other calls the language provides.
|
|
1758
|
+
if (ts.isPropertyAccessExpression(callee) || ts.isPropertyAccessChain(callee)) {
|
|
1759
|
+
const name = callee.name.text;
|
|
1760
|
+
if (ts.isIdentifier(callee.expression) && callee.expression.text === "Math")
|
|
1761
|
+
return this.math(name, node, frame);
|
|
1762
|
+
if (
|
|
1763
|
+
ts.isIdentifier(callee.expression) &&
|
|
1764
|
+
callee.expression.text === "Object" &&
|
|
1765
|
+
name === "values"
|
|
1766
|
+
) {
|
|
1767
|
+
const object = this.expression(node.arguments[0]!, frame);
|
|
1768
|
+
if (object.kind === "static" && object.node.captured.kind === "object")
|
|
1769
|
+
return {
|
|
1770
|
+
kind: "virtual",
|
|
1771
|
+
items: Object.entries(object.node.captured.fields).map(([field, value]) =>
|
|
1772
|
+
this.fromCaptured(value, object.node, { kind: "field", name: field }),
|
|
1773
|
+
),
|
|
1774
|
+
};
|
|
1775
|
+
throw new SpecializeError(node, "Object.values needs a record setup built.");
|
|
1776
|
+
}
|
|
1777
|
+
const receiver = this.expression(callee.expression, frame);
|
|
1778
|
+
const items = this.items(receiver);
|
|
1779
|
+
if (items && isArrayMethod(name)) return this.arrayMethod(name, items, node, frame);
|
|
1780
|
+
if (receiver.kind === "static" && receiver.node.captured.kind === "service") {
|
|
1781
|
+
const path = receiver.node.captured.path ? `${receiver.node.captured.path}.${name}` : name;
|
|
1782
|
+
return this.serviceCall(path, node, frame);
|
|
1783
|
+
}
|
|
1784
|
+
const method = this.member(receiver, name, callee, frame);
|
|
1785
|
+
if (method.kind === "static" && method.node.captured.kind === "service")
|
|
1786
|
+
return this.serviceCall(method.node.captured.path, node, frame);
|
|
1787
|
+
return this.invoke(
|
|
1788
|
+
method,
|
|
1789
|
+
node.arguments.map((argument, index) => this.argument(argument, method, index, frame)),
|
|
1790
|
+
node,
|
|
1791
|
+
frame,
|
|
1792
|
+
expected,
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
const target = this.expression(callee, frame);
|
|
1796
|
+
if (target.kind === "static" && target.node.captured.kind === "service")
|
|
1797
|
+
return this.serviceCall(target.node.captured.path, node, frame);
|
|
1798
|
+
if (target.kind === "static" && target.node.captured.kind === "function") {
|
|
1799
|
+
const fn = this.functionDeclaration(target.node.captured.file, target.node.captured.name);
|
|
1800
|
+
return this.invoke(
|
|
1801
|
+
{ kind: "function", declaration: fn },
|
|
1802
|
+
node.arguments.map((argument, index) =>
|
|
1803
|
+
this.argument(argument, { kind: "function", declaration: fn }, index, frame),
|
|
1804
|
+
),
|
|
1805
|
+
node,
|
|
1806
|
+
frame,
|
|
1807
|
+
expected,
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
if (ts.isIdentifier(callee) && this.sdkCall(callee)) return this.sdk(callee.text, node, frame);
|
|
1811
|
+
return this.invoke(
|
|
1812
|
+
target,
|
|
1813
|
+
node.arguments.map((argument, index) => this.argument(argument, target, index, frame)),
|
|
1814
|
+
node,
|
|
1815
|
+
frame,
|
|
1816
|
+
expected,
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
private argument(node: ts.Expression, callee: Value, index: number, frame: Frame): Value {
|
|
1821
|
+
let expected: ts.Type | undefined;
|
|
1822
|
+
if (callee.kind === "function" && !ts.isGetAccessorDeclaration(callee.declaration)) {
|
|
1823
|
+
const parameter = callee.declaration.parameters[index];
|
|
1824
|
+
expected = parameter && this.checker.getTypeAtLocation(parameter);
|
|
1825
|
+
}
|
|
1826
|
+
return this.expression(node, frame, expected);
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
private functionDeclaration(file: string, name: string): FunctionLike {
|
|
1830
|
+
const source = this.sources.find((item) => item.fileName === file);
|
|
1831
|
+
for (const statement of source?.statements ?? []) {
|
|
1832
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === name) return statement;
|
|
1833
|
+
if (ts.isVariableStatement(statement))
|
|
1834
|
+
for (const declaration of statement.declarationList.declarations)
|
|
1835
|
+
if (
|
|
1836
|
+
ts.isIdentifier(declaration.name) &&
|
|
1837
|
+
declaration.name.text === name &&
|
|
1838
|
+
declaration.initializer &&
|
|
1839
|
+
(ts.isArrowFunction(declaration.initializer) ||
|
|
1840
|
+
ts.isFunctionExpression(declaration.initializer))
|
|
1841
|
+
)
|
|
1842
|
+
return declaration.initializer;
|
|
1843
|
+
}
|
|
1844
|
+
throw new Error(`${file}: no function ${name}.`);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
/** SDK functions scan code calls, such as the PLC clock. */
|
|
1848
|
+
private sdkCall(callee: ts.Identifier): boolean {
|
|
1849
|
+
let symbol = this.checker.getSymbolAtLocation(callee);
|
|
1850
|
+
while (symbol && symbol.flags & ts.SymbolFlags.Alias)
|
|
1851
|
+
symbol = this.checker.getAliasedSymbol(symbol);
|
|
1852
|
+
const file = symbol?.declarations?.[0]?.getSourceFile().fileName ?? "";
|
|
1853
|
+
return file.includes("/@tsplc/compiler/") || file.includes("/packages/compiler/");
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
private sdk(name: string, node: ts.CallExpression, frame: Frame): Value {
|
|
1857
|
+
const args = node.arguments.map((argument) =>
|
|
1858
|
+
this.code(this.expression(argument, frame), undefined, frame),
|
|
1859
|
+
);
|
|
1860
|
+
this.sdkImports.add(name);
|
|
1861
|
+
return {
|
|
1862
|
+
kind: "code",
|
|
1863
|
+
text: `${name}(${args.join(", ")})`,
|
|
1864
|
+
declared: this.checker.getTypeAtLocation(node),
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
readonly sdkImports = new Set<string>();
|
|
1869
|
+
/** Conversions generated code imports from `@tsplc/compiler/types`. */
|
|
1870
|
+
readonly typeImports = new Set<string>();
|
|
1871
|
+
|
|
1872
|
+
private math(name: string, node: ts.CallExpression, frame: Frame): Value {
|
|
1873
|
+
const args = node.arguments.map((argument) => this.expression(argument, frame));
|
|
1874
|
+
if (args.every((arg) => arg.kind === "const" && typeof arg.value === "number")) {
|
|
1875
|
+
const values = args.map((arg) => (arg as { value: number }).value);
|
|
1876
|
+
const folded = (Math as unknown as Record<string, (...items: number[]) => number>)[name]?.(
|
|
1877
|
+
...values,
|
|
1878
|
+
);
|
|
1879
|
+
if (typeof folded === "number") return constant(folded);
|
|
1880
|
+
}
|
|
1881
|
+
const texts = args.map((arg) => this.code(arg, undefined, frame));
|
|
1882
|
+
const declared = this.checker.getTypeAtLocation(node);
|
|
1883
|
+
switch (name) {
|
|
1884
|
+
case "abs": {
|
|
1885
|
+
const local = frame.emitter.temporary();
|
|
1886
|
+
frame.emitter.line(`const ${local}: number = ${texts[0]};`);
|
|
1887
|
+
return { kind: "code", text: `(${local} < 0 ? 0 - ${local} : ${local})`, declared };
|
|
1888
|
+
}
|
|
1889
|
+
case "min":
|
|
1890
|
+
case "max":
|
|
1891
|
+
if (texts.length !== 2) throw new SpecializeError(node, `Math.${name} takes two values.`);
|
|
1892
|
+
return {
|
|
1893
|
+
kind: "code",
|
|
1894
|
+
text: `(${texts[0]} ${name === "min" ? "<" : ">"} ${texts[1]} ? ${texts[0]} : ${texts[1]})`,
|
|
1895
|
+
declared,
|
|
1896
|
+
};
|
|
1897
|
+
default:
|
|
1898
|
+
throw new SpecializeError(node, `Math.${name} is not supported.`);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
private serviceCall(path: string, node: ts.CallExpression, frame: Frame): Value {
|
|
1903
|
+
const client = this.model.roots.find((root) => {
|
|
1904
|
+
const value = this.model.node(this.model.rootValues.get(root)!);
|
|
1905
|
+
return value?.captured.kind === "service" && value.captured.path === "";
|
|
1906
|
+
});
|
|
1907
|
+
if (!client)
|
|
1908
|
+
throw new SpecializeError(node, "Create the service client in a module of its own.");
|
|
1909
|
+
const names = this.imports.get(client.file) ?? new Set<string>();
|
|
1910
|
+
names.add(client.name);
|
|
1911
|
+
this.imports.set(client.file, names);
|
|
1912
|
+
const args = node.arguments.map((argument) =>
|
|
1913
|
+
this.code(
|
|
1914
|
+
this.expression(argument, frame, this.checker.getContextualType(argument)),
|
|
1915
|
+
this.checker.getContextualType(argument),
|
|
1916
|
+
frame,
|
|
1917
|
+
),
|
|
1918
|
+
);
|
|
1919
|
+
return {
|
|
1920
|
+
kind: "code",
|
|
1921
|
+
text: `${client.name}.${path}(${args.join(", ")})`,
|
|
1922
|
+
declared: this.checker.getTypeAtLocation(node),
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
/** Calls a function value: specializes it, or inlines a one-expression arrow. */
|
|
1927
|
+
private invoke(
|
|
1928
|
+
target: Value,
|
|
1929
|
+
args: Value[],
|
|
1930
|
+
at: ts.Node,
|
|
1931
|
+
frame: Frame,
|
|
1932
|
+
expected?: ts.Type,
|
|
1933
|
+
): Value {
|
|
1934
|
+
if (target.kind !== "function") throw new SpecializeError(at, "Only functions can be called.");
|
|
1935
|
+
const declaration = target.declaration;
|
|
1936
|
+
if (
|
|
1937
|
+
(ts.isArrowFunction(declaration) || ts.isFunctionExpression(declaration)) &&
|
|
1938
|
+
target.env &&
|
|
1939
|
+
ts.isArrowFunction(declaration) &&
|
|
1940
|
+
!ts.isBlock(declaration.body)
|
|
1941
|
+
) {
|
|
1942
|
+
// A callback with one expression inlines with its arguments bound.
|
|
1943
|
+
const inline: Frame = { ...frame, env: new Map(target.env), self: target.self ?? frame.self };
|
|
1944
|
+
declaration.parameters.forEach((parameter, index) => {
|
|
1945
|
+
const arg = args[index] ?? constant(undefined);
|
|
1946
|
+
const value = arg.kind === "code" && !arg.place ? this.temporaryOf(arg, frame) : arg;
|
|
1947
|
+
this.bind(parameter.name, value, false, inline, this.checker.getTypeAtLocation(parameter));
|
|
1948
|
+
});
|
|
1949
|
+
return this.expression(declaration.body, inline, expected);
|
|
1950
|
+
}
|
|
1951
|
+
const called = this.routine(declaration, target.self, args, target.env);
|
|
1952
|
+
const text = `${called.name}(${called.args.join(", ")})`;
|
|
1953
|
+
const returns = called.returns;
|
|
1954
|
+
if (!returns || returns.flags & ts.TypeFlags.Void) {
|
|
1955
|
+
if (ts.isExpressionStatement(at.parent)) return { kind: "code", text, declared: returns };
|
|
1956
|
+
frame.emitter.line(`${text};`);
|
|
1957
|
+
return constant(undefined);
|
|
1958
|
+
}
|
|
1959
|
+
return { kind: "code", text, declared: returns };
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
/** A runtime value held in a const, so inlining doesn't evaluate it twice. */
|
|
1963
|
+
private temporaryOf(value: Value & { kind: "code" }, frame: Frame): Value {
|
|
1964
|
+
if (/^[\w.[\]]+$/.test(value.text)) return value;
|
|
1965
|
+
const local = frame.emitter.temporary();
|
|
1966
|
+
const type = value.declared ? this.localType(value.declared) : "number";
|
|
1967
|
+
frame.emitter.line(`const ${local}: ${type} = ${value.text};`);
|
|
1968
|
+
return { kind: "code", text: local, declared: value.declared, place: true };
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
private arrayMethod(name: string, items: Value[], node: ts.CallExpression, frame: Frame): Value {
|
|
1972
|
+
const callback = node.arguments[0] ? this.expression(node.arguments[0], frame) : undefined;
|
|
1973
|
+
const apply = (args: Value[]): Value => {
|
|
1974
|
+
if (!callback) throw new SpecializeError(node, `${name} needs a callback.`);
|
|
1975
|
+
return this.invoke(callback, args, node, frame);
|
|
1976
|
+
};
|
|
1977
|
+
switch (name) {
|
|
1978
|
+
case "entries":
|
|
1979
|
+
return {
|
|
1980
|
+
kind: "virtual",
|
|
1981
|
+
items: items.map((item, index) => ({ kind: "virtual", items: [constant(index), item] })),
|
|
1982
|
+
};
|
|
1983
|
+
case "slice": {
|
|
1984
|
+
const [start, end] = node.arguments.map((argument) => this.expression(argument, frame));
|
|
1985
|
+
if ((start && start.kind !== "const") || (end && end.kind !== "const"))
|
|
1986
|
+
throw new SpecializeError(node, "slice needs constant bounds.");
|
|
1987
|
+
return {
|
|
1988
|
+
kind: "virtual",
|
|
1989
|
+
items: items.slice(start?.value as number | undefined, end?.value as number | undefined),
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
case "map":
|
|
1993
|
+
return {
|
|
1994
|
+
kind: "virtual",
|
|
1995
|
+
items: items.map((item, index) => this.settle(apply([item, constant(index)]), frame)),
|
|
1996
|
+
};
|
|
1997
|
+
case "filter": {
|
|
1998
|
+
const kept: Value[] = [];
|
|
1999
|
+
items.forEach((item, index) => {
|
|
2000
|
+
const test = apply([item, constant(index)]);
|
|
2001
|
+
if (test.kind !== "const")
|
|
2002
|
+
throw new SpecializeError(node, "filter must decide from what setup built.");
|
|
2003
|
+
if (test.value) kept.push(item);
|
|
2004
|
+
});
|
|
2005
|
+
return { kind: "virtual", items: kept };
|
|
2006
|
+
}
|
|
2007
|
+
case "forEach":
|
|
2008
|
+
items.forEach((item, index) => {
|
|
2009
|
+
const result = apply([item, constant(index)]);
|
|
2010
|
+
if (result.kind === "code") frame.emitter.line(`${result.text};`);
|
|
2011
|
+
});
|
|
2012
|
+
return constant(undefined);
|
|
2013
|
+
case "some":
|
|
2014
|
+
case "every": {
|
|
2015
|
+
const some = name === "some";
|
|
2016
|
+
const tests = items.map((item, index) => apply([item, constant(index)]));
|
|
2017
|
+
const runtime: string[] = [];
|
|
2018
|
+
for (const test of tests) {
|
|
2019
|
+
if (test.kind === "const") {
|
|
2020
|
+
if (Boolean(test.value) === some) return constant(some);
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
runtime.push(this.condition(test, node.arguments[0] ?? node, frame));
|
|
2024
|
+
}
|
|
2025
|
+
if (!runtime.length) return constant(!some);
|
|
2026
|
+
return {
|
|
2027
|
+
kind: "code",
|
|
2028
|
+
text: `(${runtime.join(some ? " || " : " && ")})`,
|
|
2029
|
+
declared: this.checker.getBooleanType(),
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
case "reduce":
|
|
2033
|
+
case "reduceRight": {
|
|
2034
|
+
const order = name === "reduce" ? items : [...items].reverse();
|
|
2035
|
+
let accumulator = node.arguments[1]
|
|
2036
|
+
? this.expression(node.arguments[1], frame)
|
|
2037
|
+
: order.shift()!;
|
|
2038
|
+
for (const item of order) accumulator = this.settle(apply([accumulator, item]), frame);
|
|
2039
|
+
return accumulator;
|
|
2040
|
+
}
|
|
2041
|
+
case "find": {
|
|
2042
|
+
for (const [index, item] of items.entries()) {
|
|
2043
|
+
const test = apply([item, constant(index)]);
|
|
2044
|
+
if (test.kind !== "const")
|
|
2045
|
+
throw new SpecializeError(node, "find must decide from what setup built.");
|
|
2046
|
+
if (test.value) return item;
|
|
2047
|
+
}
|
|
2048
|
+
return constant(undefined);
|
|
2049
|
+
}
|
|
2050
|
+
case "includes": {
|
|
2051
|
+
const target = this.expression(node.arguments[0]!, frame);
|
|
2052
|
+
const tests = items.map((item) => this.equal(item, target));
|
|
2053
|
+
if (tests.some((test) => test === true)) return constant(true);
|
|
2054
|
+
if (tests.every((test) => test === false)) return constant(false);
|
|
2055
|
+
throw new SpecializeError(node, "includes must decide from what setup built.");
|
|
2056
|
+
}
|
|
2057
|
+
default:
|
|
2058
|
+
throw new SpecializeError(node, `${name} is not supported.`);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
/** Keeps a runtime result in a local, so later code reads it rather than repeating it. */
|
|
2063
|
+
private settle(value: Value, frame: Frame): Value {
|
|
2064
|
+
if (value.kind !== "code" || /^[\w.[\]]+$/.test(value.text)) return value;
|
|
2065
|
+
const local = frame.emitter.temporary();
|
|
2066
|
+
frame.emitter.line(
|
|
2067
|
+
`const ${local}: ${value.declared ? this.localType(value.declared) : "boolean"} = ${value.text};`,
|
|
2068
|
+
);
|
|
2069
|
+
return { kind: "code", text: local, declared: value.declared, place: true };
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
// ---- Values as code ----
|
|
2073
|
+
|
|
2074
|
+
/** Dialect text for a value, in a place of `expected` type when known. */
|
|
2075
|
+
code(value: Value, expected: ts.Type | undefined, frame: Frame): string {
|
|
2076
|
+
switch (value.kind) {
|
|
2077
|
+
case "code":
|
|
2078
|
+
if (
|
|
2079
|
+
expected &&
|
|
2080
|
+
value.declared &&
|
|
2081
|
+
this.isOptionalRecord(value.declared) &&
|
|
2082
|
+
!this.isOptionalRecord(expected)
|
|
2083
|
+
)
|
|
2084
|
+
return `${value.text}.value`;
|
|
2085
|
+
return value.text;
|
|
2086
|
+
case "const": {
|
|
2087
|
+
const literal = expected && this.layouts.literalEnum(expected);
|
|
2088
|
+
if (literal && (typeof value.value === "string" || value.value === undefined))
|
|
2089
|
+
return this.layouts.member(literal, value.value as string | undefined);
|
|
2090
|
+
if (expected && this.isOptionalRecord(expected) && value.value === undefined)
|
|
2091
|
+
return `defaultValue<${this.layouts.text(expected)}>()`;
|
|
2092
|
+
if (expected && this.layouts.enumDeclaration(expected) && typeof value.value === "number") {
|
|
2093
|
+
const member = this.enumMemberName(expected, value.value);
|
|
2094
|
+
if (member) return `${this.layouts.text(expected)}.${member}`;
|
|
2095
|
+
}
|
|
2096
|
+
if (value.value === undefined) throw new Error("undefined has no PLC value here.");
|
|
2097
|
+
if (typeof value.value === "string") return JSON.stringify(value.value);
|
|
2098
|
+
if (typeof value.value === "bigint") return `${value.value}n`;
|
|
2099
|
+
return String(value.value);
|
|
2100
|
+
}
|
|
2101
|
+
case "static": {
|
|
2102
|
+
const node = value.node;
|
|
2103
|
+
if (this.storedNode(node)) return this.pathText(node);
|
|
2104
|
+
if (node.type) return this.valueText({ kind: "node", id: node.id }, expected ?? node.type);
|
|
2105
|
+
throw new Error("This object has no PLC value.");
|
|
2106
|
+
}
|
|
2107
|
+
case "virtual": {
|
|
2108
|
+
if (value.fields) {
|
|
2109
|
+
const type = expected && this.checker.getNonNullableType(expected);
|
|
2110
|
+
if (!type) throw new Error("A record literal needs a declared type.");
|
|
2111
|
+
return this.recordLiteral(value.fields, type, frame);
|
|
2112
|
+
}
|
|
2113
|
+
if (value.items) {
|
|
2114
|
+
const element = expected && this.model.elementType(expected);
|
|
2115
|
+
return `[${value.items.map((item) => this.code(item, element, frame)).join(", ")}]`;
|
|
2116
|
+
}
|
|
2117
|
+
throw new Error("Empty value.");
|
|
2118
|
+
}
|
|
2119
|
+
case "function":
|
|
2120
|
+
throw new Error("A function has no PLC value; call it.");
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
/** A record literal of `type`, every field written; a tagged union fills the other variants' fields. */
|
|
2125
|
+
private recordLiteral(fields: Map<string, Value>, type: ts.Type, frame: Frame): string {
|
|
2126
|
+
const tagged = this.layouts.tagged(type);
|
|
2127
|
+
const names = tagged
|
|
2128
|
+
? this.taggedFields(tagged)
|
|
2129
|
+
: this.checker.getPropertiesOfType(type).map((property) => property.name);
|
|
2130
|
+
return `{ ${names
|
|
2131
|
+
.map((name) => {
|
|
2132
|
+
const fieldType = tagged
|
|
2133
|
+
? name === tagged.tag
|
|
2134
|
+
? this.tagType(type, tagged)
|
|
2135
|
+
: this.taggedFieldType(type, tagged, name)
|
|
2136
|
+
: this.checker.getTypeOfSymbol(this.checker.getPropertyOfType(type, name)!);
|
|
2137
|
+
const value = fields.get(name);
|
|
2138
|
+
if (!value) return `${name}: ${this.defaultOf(fieldType)}`;
|
|
2139
|
+
const property = this.checker.getPropertyOfType(type, name);
|
|
2140
|
+
return `${name}: ${this.assignmentText(value, fieldType, frame, this.layouts.iecName(property?.valueDeclaration))}`;
|
|
2141
|
+
})
|
|
2142
|
+
.join(", ")} }`;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
/** IEC integer and bit-string widths: [bits, signed, bit string]. */
|
|
2147
|
+
const integerWidths: Readonly<Record<string, readonly [number, boolean, boolean]>> = {
|
|
2148
|
+
SINT: [8, true, false],
|
|
2149
|
+
INT: [16, true, false],
|
|
2150
|
+
DINT: [32, true, false],
|
|
2151
|
+
LINT: [64, true, false],
|
|
2152
|
+
USINT: [8, false, false],
|
|
2153
|
+
UINT: [16, false, false],
|
|
2154
|
+
UDINT: [32, false, false],
|
|
2155
|
+
ULINT: [64, false, false],
|
|
2156
|
+
BYTE: [8, false, true],
|
|
2157
|
+
WORD: [16, false, true],
|
|
2158
|
+
DWORD: [32, false, true],
|
|
2159
|
+
LWORD: [64, false, true],
|
|
2160
|
+
};
|
|
2161
|
+
|
|
2162
|
+
/** Whether IEC needs an explicit conversion from one integer type to another; widening is implicit. */
|
|
2163
|
+
const needsConversion = (from: string, to: string): boolean => {
|
|
2164
|
+
const source = integerWidths[from],
|
|
2165
|
+
target = integerWidths[to];
|
|
2166
|
+
if (!source || !target || from === to) return false;
|
|
2167
|
+
if (source[2] || target[2]) return true;
|
|
2168
|
+
if (source[1] === target[1]) return source[0] > target[0];
|
|
2169
|
+
return source[1] || source[0] >= target[0];
|
|
2170
|
+
};
|
|
2171
|
+
|
|
2172
|
+
const isChild = (node: ModelNode, holder: ModelNode, field?: string): boolean => {
|
|
2173
|
+
const owner = node.owner;
|
|
2174
|
+
const parent = holder.owner;
|
|
2175
|
+
if (!owner || !parent || owner.root !== parent.root) return false;
|
|
2176
|
+
if (owner.steps.length !== parent.steps.length + 1) return false;
|
|
2177
|
+
if (!parent.steps.every((step, index) => stepText(step) === stepText(owner.steps[index]!)))
|
|
2178
|
+
return false;
|
|
2179
|
+
const last = owner.steps.at(-1)!;
|
|
2180
|
+
return field === undefined || (last.kind === "field" && last.name === field);
|
|
2181
|
+
};
|
|
2182
|
+
|
|
2183
|
+
/** The function a storage module exports to set one of its globals. */
|
|
2184
|
+
export const setterName = (name: string): string => `tsplcSet_${name}`;
|
|
2185
|
+
|
|
2186
|
+
const stepText = (step: Step): string =>
|
|
2187
|
+
step.kind === "index" ? `[${step.index}]` : `.${step.name}`;
|
|
2188
|
+
|
|
2189
|
+
const declarationKey = (declaration: ts.Node): string =>
|
|
2190
|
+
`${declaration.getSourceFile().fileName}:${declaration.pos}`;
|
|
2191
|
+
|
|
2192
|
+
const valueKey = (value: Value): string => {
|
|
2193
|
+
switch (value.kind) {
|
|
2194
|
+
case "static":
|
|
2195
|
+
return `#${value.node.id}`;
|
|
2196
|
+
case "const":
|
|
2197
|
+
return `=${String(value.value)}`;
|
|
2198
|
+
case "function":
|
|
2199
|
+
return `fn:${declarationKey(value.declaration)}:${value.self?.id ?? ""}`;
|
|
2200
|
+
case "virtual":
|
|
2201
|
+
return value.items
|
|
2202
|
+
? `[${value.items.map(valueKey).join(",")}]`
|
|
2203
|
+
: `{${[...(value.fields ?? [])].map(([name, inner]) => `${name}:${valueKey(inner)}`).join(",")}}`;
|
|
2204
|
+
case "code":
|
|
2205
|
+
return "code";
|
|
2206
|
+
}
|
|
2207
|
+
};
|
|
2208
|
+
|
|
2209
|
+
const isAssignment = (kind: ts.SyntaxKind): boolean =>
|
|
2210
|
+
kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
|
|
2211
|
+
|
|
2212
|
+
const compoundOperator = (kind: ts.SyntaxKind): string | undefined =>
|
|
2213
|
+
({
|
|
2214
|
+
[ts.SyntaxKind.PlusEqualsToken]: "+",
|
|
2215
|
+
[ts.SyntaxKind.MinusEqualsToken]: "-",
|
|
2216
|
+
[ts.SyntaxKind.AsteriskEqualsToken]: "*",
|
|
2217
|
+
[ts.SyntaxKind.BarEqualsToken]: "|",
|
|
2218
|
+
[ts.SyntaxKind.AmpersandEqualsToken]: "&",
|
|
2219
|
+
})[kind as number];
|
|
2220
|
+
|
|
2221
|
+
const binaryToken = (kind: ts.SyntaxKind): string | undefined =>
|
|
2222
|
+
({
|
|
2223
|
+
[ts.SyntaxKind.PlusToken]: "+",
|
|
2224
|
+
[ts.SyntaxKind.MinusToken]: "-",
|
|
2225
|
+
[ts.SyntaxKind.AsteriskToken]: "*",
|
|
2226
|
+
[ts.SyntaxKind.LessThanToken]: "<",
|
|
2227
|
+
[ts.SyntaxKind.LessThanEqualsToken]: "<=",
|
|
2228
|
+
[ts.SyntaxKind.GreaterThanToken]: ">",
|
|
2229
|
+
[ts.SyntaxKind.GreaterThanEqualsToken]: ">=",
|
|
2230
|
+
[ts.SyntaxKind.AmpersandToken]: "&",
|
|
2231
|
+
[ts.SyntaxKind.BarToken]: "|",
|
|
2232
|
+
[ts.SyntaxKind.CaretToken]: "^",
|
|
2233
|
+
[ts.SyntaxKind.LessThanLessThanToken]: "<<",
|
|
2234
|
+
[ts.SyntaxKind.GreaterThanGreaterThanToken]: ">>",
|
|
2235
|
+
})[kind as number];
|
|
2236
|
+
|
|
2237
|
+
const foldNumber = (
|
|
2238
|
+
kind: ts.SyntaxKind,
|
|
2239
|
+
left: number,
|
|
2240
|
+
right: number,
|
|
2241
|
+
): number | boolean | undefined => {
|
|
2242
|
+
switch (kind) {
|
|
2243
|
+
case ts.SyntaxKind.PlusToken:
|
|
2244
|
+
return left + right;
|
|
2245
|
+
case ts.SyntaxKind.MinusToken:
|
|
2246
|
+
return left - right;
|
|
2247
|
+
case ts.SyntaxKind.AsteriskToken:
|
|
2248
|
+
return left * right;
|
|
2249
|
+
case ts.SyntaxKind.LessThanToken:
|
|
2250
|
+
return left < right;
|
|
2251
|
+
case ts.SyntaxKind.LessThanEqualsToken:
|
|
2252
|
+
return left <= right;
|
|
2253
|
+
case ts.SyntaxKind.GreaterThanToken:
|
|
2254
|
+
return left > right;
|
|
2255
|
+
case ts.SyntaxKind.GreaterThanEqualsToken:
|
|
2256
|
+
return left >= right;
|
|
2257
|
+
default:
|
|
2258
|
+
return undefined;
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
2261
|
+
|
|
2262
|
+
const isArrayMethod = (name: string): boolean =>
|
|
2263
|
+
[
|
|
2264
|
+
"entries",
|
|
2265
|
+
"slice",
|
|
2266
|
+
"map",
|
|
2267
|
+
"filter",
|
|
2268
|
+
"forEach",
|
|
2269
|
+
"some",
|
|
2270
|
+
"every",
|
|
2271
|
+
"reduce",
|
|
2272
|
+
"reduceRight",
|
|
2273
|
+
"find",
|
|
2274
|
+
"includes",
|
|
2275
|
+
].includes(name);
|
|
2276
|
+
|
|
2277
|
+
const pascal = (text: string): string =>
|
|
2278
|
+
text.replace(/(^|[^A-Za-z0-9]+)([A-Za-z0-9])/g, (_, __, letter: string) => letter.toUpperCase());
|
|
2279
|
+
|
|
2280
|
+
export type { LiteralEnum };
|