@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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +16 -2
  3. package/backends/codesys/config.ts +54 -0
  4. package/backends/codesys/emit.ts +1063 -0
  5. package/backends/codesys/engineering.ts +88 -0
  6. package/backends/codesys/http.ts +598 -0
  7. package/backends/codesys/index.ts +12 -0
  8. package/backends/codesys/io-paths.ts +103 -0
  9. package/backends/codesys/names.ts +76 -0
  10. package/backends/codesys/numeric.ts +356 -0
  11. package/backends/codesys/plan.ts +512 -0
  12. package/backends/codesys/publication.ts +59 -0
  13. package/backends/codesys/storage.ts +63 -0
  14. package/backends/codesys/task-access.ts +118 -0
  15. package/backends/codesys/types.ts +178 -0
  16. package/backends/codesys/vendor.ts +108 -0
  17. package/dist/cli.js +1396 -0
  18. package/dist/cli.js.map +16 -0
  19. package/dist/clock.js +8 -0
  20. package/dist/clock.js.map +10 -0
  21. package/dist/codesys.js +2587 -0
  22. package/dist/codesys.js.map +22 -0
  23. package/dist/elaborate-capture.js +118 -0
  24. package/dist/elaborate-capture.js.map +10 -0
  25. package/dist/index.js +6859 -0
  26. package/dist/index.js.map +25 -0
  27. package/dist/ir.js +1888 -0
  28. package/dist/ir.js.map +22 -0
  29. package/dist/math.js +24 -0
  30. package/dist/math.js.map +10 -0
  31. package/dist/project.js +429 -0
  32. package/dist/project.js.map +11 -0
  33. package/ir/aggregates.ts +333 -0
  34. package/ir/allocation.ts +74 -0
  35. package/ir/core-model.ts +116 -0
  36. package/ir/core.ts +63 -0
  37. package/ir/declarations.ts +201 -0
  38. package/ir/diagnostics.ts +43 -0
  39. package/ir/domains.ts +96 -0
  40. package/ir/index.ts +7 -0
  41. package/ir/link.ts +176 -0
  42. package/ir/model.ts +268 -0
  43. package/ir/normalization-error.ts +10 -0
  44. package/ir/routine.ts +826 -0
  45. package/ir/scalars.ts +675 -0
  46. package/ir/types.ts +70 -0
  47. package/package.json +106 -4
  48. package/project/artifacts.ts +277 -0
  49. package/project/cli.ts +305 -0
  50. package/project/index.ts +1 -0
  51. package/project/plant.ts +437 -0
  52. package/project/project.ts +312 -0
  53. package/project/server.ts +83 -0
  54. package/src/frontend/async.ts +1060 -0
  55. package/src/frontend/blocks.ts +419 -0
  56. package/src/frontend/configuration.ts +167 -0
  57. package/src/frontend/const-functions.ts +153 -0
  58. package/src/frontend/cooperative.ts +75 -0
  59. package/src/frontend/elaborate/capture.ts +138 -0
  60. package/src/frontend/elaborate/graph.ts +94 -0
  61. package/src/frontend/elaborate/index.ts +302 -0
  62. package/src/frontend/elaborate/layouts.ts +390 -0
  63. package/src/frontend/elaborate/model.ts +177 -0
  64. package/src/frontend/elaborate/specialize.ts +2280 -0
  65. package/src/frontend/exposure.ts +147 -0
  66. package/src/frontend/module-graph.ts +63 -0
  67. package/src/frontend/promises.ts +751 -0
  68. package/src/frontend/services.ts +435 -0
  69. package/src/frontend/types.ts +578 -0
  70. package/src/frontend/typescript.ts +2442 -0
  71. package/src/index.ts +7 -0
  72. package/stdlib/clock.ts +2 -0
  73. package/stdlib/math.ts +19 -0
  74. package/types/async.d.ts +81 -0
  75. package/types/clock.d.ts +4 -0
  76. package/types/config.d.ts +2 -0
  77. package/types/data-types.d.ts +31 -0
  78. package/types/globals.d.ts +40 -0
  79. package/types/index.d.ts +111 -0
  80. package/types/plc.d.ts +5 -0
@@ -0,0 +1,419 @@
1
+ import ts from "typescript";
2
+ import {
3
+ aggregate,
4
+ defaultValue,
5
+ instancedBlock,
6
+ standardBlocks,
7
+ type StandardBlock,
8
+ type FunctionBlock,
9
+ type Variable,
10
+ type SourceSpan,
11
+ type PlcType,
12
+ } from "@tsplc/compiler/ir";
13
+ import { calleeName, numericLiteralType, type Fail, type TypeResolver } from "./types.ts";
14
+
15
+ export interface BlockSource {
16
+ block: FunctionBlock;
17
+ method: ts.MethodDeclaration;
18
+ methods: readonly ts.MethodDeclaration[];
19
+ inputSymbol: ts.Symbol;
20
+ inputBindings: ReadonlyMap<string, Variable>;
21
+ fieldBindings: ReadonlyMap<ts.Symbol, Variable>;
22
+ initializers: ReadonlyMap<Variable, ts.Expression>;
23
+ }
24
+ export function storageInitializer(
25
+ node: ts.Expression,
26
+ types: TypeResolver,
27
+ fail: Fail,
28
+ ): { value: ts.Expression; retain: boolean } {
29
+ if (!ts.isCallExpression(node)) return { value: node, retain: false };
30
+ const callee = calleeName(node.expression);
31
+ if (!callee || types.sdkName(callee) !== "retain") return { value: node, retain: false };
32
+ if (node.arguments.length !== 1 || (node.typeArguments?.length ?? 0) > 1)
33
+ fail(node, "PLC3004", "retain requires one static storage initializer.");
34
+ return { value: node.arguments[0]!, retain: true };
35
+ }
36
+
37
+ export class BlockResolver {
38
+ readonly definitions = new Map<string, FunctionBlock>();
39
+ readonly sources = new Map<ts.Symbol, BlockSource>();
40
+ private readonly declarations = new Map<ts.Symbol, ts.ClassDeclaration>();
41
+ private readonly active = new Set<ts.Symbol>();
42
+ constructor(
43
+ private readonly types: TypeResolver,
44
+ private readonly origin: (node: ts.Node) => SourceSpan,
45
+ private readonly fail: Fail,
46
+ sdkSpan: SourceSpan,
47
+ ) {
48
+ for (const name of Object.keys(standardBlocks) as StandardBlock[]) {
49
+ const schema = standardBlocks[name],
50
+ id = `stdlib:${name}`,
51
+ output = `${id}:out`;
52
+ const variable = (label: string, type: string, storage: Variable["storage"]): Variable => ({
53
+ id: `${id}:${label}`,
54
+ name: label,
55
+ type,
56
+ storage,
57
+ mutable: false,
58
+ retain: false,
59
+ span: sdkSpan,
60
+ ...(storage === "blockOutput" && { initial: defaultValue(type, types.types) }),
61
+ });
62
+ const block: FunctionBlock = {
63
+ id,
64
+ name,
65
+ inputs: schema.inputs.map(([label, , type]) => variable(label, type, "parameter")),
66
+ outputs: schema.outputs.map(([label, , type]) => variable(label, type, "blockOutput")),
67
+ state: [],
68
+ locals: [],
69
+ body: [],
70
+ result: output,
71
+ standard: name,
72
+ span: sdkSpan,
73
+ };
74
+ this.definitions.set(id, block);
75
+ types.types.set(output, {
76
+ id: output,
77
+ kind: "record",
78
+ fields: block.outputs.map((field) => ({
79
+ name: field.name,
80
+ type: field.type,
81
+ readonly: true,
82
+ })),
83
+ });
84
+ const type: PlcType = { id: `block:${id}`, kind: "block", block: id, output };
85
+ types.types.set(type.id, type);
86
+ types.standardTypes.set(name, type.id);
87
+ }
88
+ }
89
+
90
+ index(nodes: readonly ts.ClassDeclaration[]): void {
91
+ for (const node of nodes) {
92
+ if (!node.name) this.fail(node, "PLC1001", "Function block classes need names.");
93
+ this.declarations.set(this.types.symbol(node.name), node);
94
+ }
95
+ }
96
+
97
+ instanceType(node: ts.NewExpression): string {
98
+ const name = calleeName(node.expression);
99
+ if (node.arguments?.length || node.typeArguments?.length || !name)
100
+ this.fail(
101
+ node,
102
+ "PLC1007",
103
+ "Block instances use a concrete zero-argument constructor at static storage declarations.",
104
+ );
105
+ const official = this.types.sdkName(name),
106
+ standard = official && this.types.standardTypes.get(official);
107
+ if (standard) return standard;
108
+ const symbol = this.types.symbol(name),
109
+ declaration = this.declarations.get(symbol);
110
+ if (!declaration)
111
+ this.fail(node, "PLC1007", "Only recognized static function block allocation is supported.");
112
+ this.collect(declaration);
113
+ // Successful collection registers this class identity before publishing its source.
114
+ return this.types.blockTypes.get(symbol)!;
115
+ }
116
+
117
+ /**
118
+ * `FixedArray<Block, N>` storage allocated with `defaultValue`; its elements are static
119
+ * instances. The allocation names the type, so an annotation is optional but must agree.
120
+ */
121
+ instanceArrayType(annotation: ts.TypeNode | undefined, value: ts.Expression): string | undefined {
122
+ const blockArray = (type: string) =>
123
+ this.types.types.get(type)?.kind === "array" && instancedBlock(type, this.types.types)
124
+ ? type
125
+ : undefined;
126
+ const declared = annotation && this.types.resolve(annotation).type;
127
+ if (declared && !blockArray(declared)) return undefined;
128
+ const callee = ts.isCallExpression(value) && calleeName(value.expression);
129
+ const allocation =
130
+ callee && this.types.sdkName(callee) === "defaultValue" && value.typeArguments?.length === 1
131
+ ? this.types.resolve(value.typeArguments[0]!).type
132
+ : undefined;
133
+ if (!declared) return allocation && blockArray(allocation);
134
+ if (allocation !== declared)
135
+ this.fail(
136
+ value,
137
+ "PLC1007",
138
+ "Block arrays allocate with defaultValue<FixedArray<Block, N>>() of their declared type.",
139
+ );
140
+ return declared;
141
+ }
142
+
143
+ collect(node: ts.ClassDeclaration): void {
144
+ if (!node.name) this.fail(node, "PLC1001", "Function block classes need names.");
145
+ const symbol = this.types.symbol(node.name);
146
+ if (this.sources.has(symbol)) return;
147
+ if (this.active.has(symbol) || this.active.size >= 64)
148
+ this.fail(node, "PLC1007", "Recursive or excessively nested block instance layout.");
149
+ if (
150
+ node.typeParameters?.length ||
151
+ node.modifiers?.some(
152
+ (modifier) =>
153
+ modifier.kind === ts.SyntaxKind.Decorator ||
154
+ modifier.kind === ts.SyntaxKind.AbstractKeyword,
155
+ )
156
+ )
157
+ this.fail(
158
+ node,
159
+ "PLC1001",
160
+ "Block classes are concrete; generic classes and decorators are unsupported.",
161
+ );
162
+ const heritage = node.heritageClauses;
163
+ if (
164
+ heritage?.length !== 1 ||
165
+ heritage[0]?.token !== ts.SyntaxKind.ExtendsKeyword ||
166
+ heritage[0].types.length !== 1
167
+ )
168
+ this.fail(
169
+ node,
170
+ "PLC1001",
171
+ "Function block classes extend the canonical FunctionBlock marker only.",
172
+ );
173
+ const base = heritage[0].types[0]!;
174
+ if (
175
+ this.types.sdkName(base.expression) !== "FunctionBlock" ||
176
+ !base.typeArguments ||
177
+ base.typeArguments.length < 1 ||
178
+ base.typeArguments.length > 2
179
+ )
180
+ this.fail(base, "PLC1001", "Extend FunctionBlock<Inputs, Outputs> with concrete schemas.");
181
+ // Register identity before nested fields; the active set rejects recursive allocation.
182
+ this.active.add(symbol);
183
+ const id = this.types.id(node, node.name.text),
184
+ typeId = `block:${id}`;
185
+ this.types.types.set(typeId, { id: typeId, kind: "block", block: id });
186
+ this.types.blockTypes.set(symbol, typeId);
187
+ try {
188
+ const inputFields = this.types.inputFields(base.typeArguments[0]!);
189
+ let result: string | undefined;
190
+ const outputNode = base.typeArguments[1];
191
+ if (outputNode && !(ts.isTypeLiteralNode(outputNode) && !outputNode.members.length)) {
192
+ const resolved = this.types.resolve(outputNode),
193
+ layout = this.types.types.get(resolved.type);
194
+ if (
195
+ resolved.io ||
196
+ resolved.mode ||
197
+ this.types.hasReferences(resolved.type) ||
198
+ layout?.kind !== "record"
199
+ )
200
+ this.fail(outputNode, "PLC2001", "Block outputs require a closed value record.");
201
+ result = resolved.type;
202
+ }
203
+ const fieldBindings = new Map<ts.Symbol, Variable>(),
204
+ initializers = new Map<Variable, ts.Expression>(),
205
+ state: Variable[] = [];
206
+ let method: ts.MethodDeclaration | undefined;
207
+ const methods: ts.MethodDeclaration[] = [];
208
+ for (const member of node.members) {
209
+ if (ts.isMethodDeclaration(member)) {
210
+ if (ts.isIdentifier(member.name) && member.name.text !== "execute") {
211
+ if (
212
+ !member.body ||
213
+ member.asteriskToken ||
214
+ member.questionToken ||
215
+ member.typeParameters?.length ||
216
+ ["update", "out"].includes(member.name.text) ||
217
+ member.modifiers?.some(
218
+ (modifier) =>
219
+ ![
220
+ ts.SyntaxKind.PublicKeyword,
221
+ ts.SyntaxKind.PrivateKeyword,
222
+ ts.SyntaxKind.ProtectedKeyword,
223
+ ].includes(modifier.kind),
224
+ )
225
+ )
226
+ this.fail(
227
+ member,
228
+ "PLC1001",
229
+ "Block methods require concrete instance implementations without generics or overloads.",
230
+ );
231
+ methods.push(member);
232
+ continue;
233
+ }
234
+ if (
235
+ method ||
236
+ !ts.isIdentifier(member.name) ||
237
+ member.name.text !== "execute" ||
238
+ !member.body ||
239
+ member.asteriskToken ||
240
+ member.modifiers?.some(
241
+ (modifier) =>
242
+ ![ts.SyntaxKind.ProtectedKeyword, ts.SyntaxKind.OverrideKeyword].includes(
243
+ modifier.kind,
244
+ ),
245
+ ) ||
246
+ !member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ProtectedKeyword)
247
+ )
248
+ this.fail(member, "PLC1001", "A block implements one protected execute method.");
249
+ method = member;
250
+ continue;
251
+ }
252
+ const field = this.collectStateField(member, id);
253
+ state.push(field.variable);
254
+ fieldBindings.set(field.symbol, field.variable);
255
+ if (field.initializer) initializers.set(field.variable, field.initializer);
256
+ }
257
+ if (method?.parameters.length !== 1 || method.typeParameters?.length)
258
+ this.fail(node, "PLC1001", "Block execute requires one concrete input record parameter.");
259
+ const parameter = method.parameters[0]!;
260
+ if (
261
+ !ts.isIdentifier(parameter.name) ||
262
+ !parameter.type ||
263
+ parameter.initializer ||
264
+ parameter.questionToken ||
265
+ parameter.dotDotDotToken
266
+ )
267
+ this.fail(
268
+ parameter,
269
+ "PLC2001",
270
+ "Block execute requires one named, explicitly typed input parameter.",
271
+ );
272
+ const methodFields = this.types.inputFields(parameter.type);
273
+ if (JSON.stringify(methodFields) !== JSON.stringify(inputFields))
274
+ this.fail(
275
+ parameter,
276
+ "PLC2001",
277
+ "execute input schema differs from its FunctionBlock marker.",
278
+ );
279
+ if (
280
+ !method.type ||
281
+ (result
282
+ ? this.types.resolve(method.type).type !== result
283
+ : method.type.kind !== ts.SyntaxKind.VoidKeyword)
284
+ )
285
+ this.fail(
286
+ method,
287
+ "PLC2001",
288
+ "execute return annotation must match the complete output record, or void when there are no outputs.",
289
+ );
290
+ const inputs = inputFields.map((field) => {
291
+ // Records and arrays pass by reference, as objects do in TypeScript.
292
+ const mode =
293
+ field.mode ?? (aggregate(this.types.types.get(field.type)) ? "inOut" : undefined);
294
+ return {
295
+ id: `${id}:input:${field.name}`,
296
+ name: field.name,
297
+ type: field.type,
298
+ storage: "parameter" as const,
299
+ mutable: mode === "inOut",
300
+ retain: false,
301
+ span: this.origin(parameter),
302
+ ...(mode && { mode }),
303
+ };
304
+ });
305
+ const outputType = result && this.types.types.get(result),
306
+ outputs: Variable[] =
307
+ outputType && outputType.kind === "record"
308
+ ? outputType.fields.map((field) => ({
309
+ id: `${id}:output:${field.name}`,
310
+ name: field.name,
311
+ type: field.type,
312
+ storage: "blockOutput",
313
+ mutable: false,
314
+ retain: false,
315
+ initial: defaultValue(field.type, this.types.types),
316
+ span: this.origin(method!),
317
+ }))
318
+ : [];
319
+ const block: FunctionBlock = {
320
+ id,
321
+ name: node.name.text,
322
+ inputs,
323
+ outputs,
324
+ state,
325
+ locals: [],
326
+ body: [],
327
+ span: this.origin(node),
328
+ ...(result && { result }),
329
+ };
330
+ this.definitions.set(id, block);
331
+ this.types.types.set(typeId, {
332
+ id: typeId,
333
+ kind: "block",
334
+ block: id,
335
+ ...(result && { output: result }),
336
+ });
337
+ this.sources.set(symbol, {
338
+ block,
339
+ method,
340
+ methods,
341
+ inputSymbol: this.types.symbol(parameter.name),
342
+ inputBindings: new Map(inputs.map((variable) => [variable.name, variable])),
343
+ fieldBindings,
344
+ initializers,
345
+ });
346
+ } finally {
347
+ this.active.delete(symbol);
348
+ }
349
+ }
350
+ // Private state fields own storage admission, constructor allocation and initializer capture.
351
+ // The enclosing collection keeps schema/execute validation separate from this field policy.
352
+ private collectStateField(
353
+ member: ts.ClassElement,
354
+ id: string,
355
+ ): {
356
+ variable: Variable;
357
+ symbol: ts.Symbol;
358
+ initializer?: ts.Expression;
359
+ } {
360
+ if (
361
+ !ts.isPropertyDeclaration(member) ||
362
+ !ts.isIdentifier(member.name) ||
363
+ !member.initializer ||
364
+ member.questionToken ||
365
+ member.exclamationToken ||
366
+ !member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.PrivateKeyword) ||
367
+ member.modifiers.some(
368
+ (modifier) =>
369
+ ![ts.SyntaxKind.PrivateKeyword, ts.SyntaxKind.ReadonlyKeyword].includes(modifier.kind),
370
+ )
371
+ )
372
+ this.fail(
373
+ member,
374
+ "PLC1001",
375
+ "Block state uses private initialized instance fields; constructors, static fields and decorators are unsupported.",
376
+ );
377
+ const initial = storageInitializer(member.initializer, this.types, this.fail);
378
+ const instanceArray = ts.isNewExpression(initial.value)
379
+ ? undefined
380
+ : this.instanceArrayType(member.type, initial.value);
381
+ let type: string;
382
+ if (ts.isNewExpression(initial.value)) type = this.instanceType(initial.value);
383
+ else if (instanceArray) type = instanceArray;
384
+ else if (member.type) {
385
+ const annotation = this.types.resolve(member.type);
386
+ if (annotation.io || annotation.mode || this.types.hasReferences(annotation.type))
387
+ this.fail(member, "PLC2001", "Borrow and I/O wrappers cannot be stored in block state.");
388
+ type = annotation.type;
389
+ } else if (
390
+ initial.value.kind === ts.SyntaxKind.TrueKeyword ||
391
+ initial.value.kind === ts.SyntaxKind.FalseKeyword
392
+ )
393
+ type = "BOOL";
394
+ else if (/^-?\d/.test(initial.value.getText()))
395
+ type = numericLiteralType(initial.value.getText());
396
+ else this.fail(member, "PLC2001", "Annotate private numeric and aggregate state explicitly.");
397
+ if (member.type && this.types.resolve(member.type).type !== type)
398
+ this.fail(member, "PLC2001", "Instance annotation differs from its constructor.");
399
+ const instance = Boolean(instancedBlock(type, this.types.types));
400
+ if (this.types.types.get(type)?.kind === "block" && !ts.isNewExpression(initial.value))
401
+ this.fail(member, "PLC1007", "Block instances require static constructor initialization.");
402
+ const variable: Variable = {
403
+ id: `${id}:field:${member.name.text}`,
404
+ name: member.name.text,
405
+ type,
406
+ storage: instance ? "instance" : "field",
407
+ mutable:
408
+ !instance &&
409
+ !member.modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ReadonlyKeyword),
410
+ retain: initial.retain,
411
+ span: this.origin(member),
412
+ };
413
+ return {
414
+ variable,
415
+ symbol: this.types.symbol(member.name),
416
+ ...(!instance && { initializer: initial.value }),
417
+ };
418
+ }
419
+ }
@@ -0,0 +1,167 @@
1
+ import { runtimeSources } from "./module-graph.ts";
2
+ import ts from "typescript";
3
+ import { dirname } from "node:path";
4
+ import { error, type Diagnostic } from "@tsplc/compiler/ir";
5
+
6
+ const symbolAt = (checker: ts.TypeChecker, node: ts.Node) => {
7
+ let symbol = checker.getSymbolAtLocation(node);
8
+ while (symbol && symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
9
+ return symbol;
10
+ };
11
+
12
+ export const lowerEnvironment = (program: ts.Program, sdk: string) => {
13
+ const checker = program.getTypeChecker();
14
+ const sources = new Map<string, string>();
15
+ const diagnostics: Diagnostic[] = [];
16
+ for (const source of runtimeSources(program)) {
17
+ if (source.isDeclarationFile) continue;
18
+ const edits: { start: number; end: number; text: string }[] = [];
19
+ const visit = (node: ts.Node): void => {
20
+ if (ts.isCallExpression(node)) {
21
+ const symbol = symbolAt(checker, node.expression);
22
+ if (
23
+ symbol?.name === "env" &&
24
+ symbol.declarations?.some((declaration) =>
25
+ declaration.getSourceFile().fileName.startsWith(dirname(sdk) + "/"),
26
+ )
27
+ ) {
28
+ const name = node.arguments[0];
29
+ let owner: ts.Node | undefined = node.parent;
30
+ while (owner && !ts.isVariableDeclaration(owner) && !ts.isFunctionLike(owner))
31
+ owner = owner.parent;
32
+ const moduleConst =
33
+ owner &&
34
+ ts.isVariableDeclaration(owner) &&
35
+ ts.isVariableDeclarationList(owner.parent) &&
36
+ Boolean(owner.parent.flags & ts.NodeFlags.Const) &&
37
+ ts.isVariableStatement(owner.parent.parent) &&
38
+ ts.isSourceFile(owner.parent.parent.parent);
39
+ const value = name && ts.isStringLiteral(name) ? process.env[name.text] : undefined;
40
+ let message: string | undefined;
41
+ if (
42
+ !moduleConst ||
43
+ node.arguments.length !== 1 ||
44
+ !name ||
45
+ !ts.isStringLiteral(name) ||
46
+ node.typeArguments?.length !== 1
47
+ )
48
+ message =
49
+ "env<STRING<N>> requires a literal environment name in module-level const configuration.";
50
+ else if (value === undefined || value.length === 0)
51
+ message = `Required environment value ${name.text} is missing.`;
52
+ else if (value.includes("\0"))
53
+ message = `Environment value ${name.text} contains an unsupported NUL byte.`;
54
+ if (message) {
55
+ const start = node.getStart(source),
56
+ position = source.getLineAndCharacterOfPosition(start);
57
+ diagnostics.push(
58
+ error("PLC1011", message, {
59
+ file: source.fileName,
60
+ start,
61
+ end: node.end,
62
+ line: position.line + 1,
63
+ column: position.character + 1,
64
+ }),
65
+ );
66
+ } else if (value !== undefined)
67
+ edits.push({
68
+ start: node.getStart(source),
69
+ end: node.end,
70
+ text: JSON.stringify(value),
71
+ });
72
+ return;
73
+ }
74
+ }
75
+ ts.forEachChild(node, visit);
76
+ };
77
+ visit(source);
78
+ if (!edits.length) continue;
79
+ let text = source.text;
80
+ for (const edit of edits.sort((a, b) => b.start - a.start))
81
+ text = text.slice(0, edit.start) + edit.text + text.slice(edit.end);
82
+ sources.set(source.fileName, text);
83
+ }
84
+ return { sources, diagnostics };
85
+ };
86
+
87
+ /** URL expressions are compile-time values, never reads from mutable PLC state. */
88
+ export const staticString = (checker: ts.TypeChecker, root: ts.Expression): string | undefined => {
89
+ const active = new Set<ts.Node>();
90
+ const valueNode = (node: ts.Node): ts.Node | undefined => {
91
+ if (ts.isParenthesizedExpression(node)) return node.expression;
92
+ if (ts.isIdentifier(node)) {
93
+ const declaration = symbolAt(checker, node)?.declarations?.find(ts.isVariableDeclaration);
94
+ if (
95
+ declaration &&
96
+ ts.isVariableDeclarationList(declaration.parent) &&
97
+ declaration.parent.flags & ts.NodeFlags.Const
98
+ )
99
+ return declaration.initializer;
100
+ }
101
+ if (ts.isPropertyAccessExpression(node)) {
102
+ const field = symbolAt(checker, node.name);
103
+ if (
104
+ !field?.declarations?.some(
105
+ (declaration) =>
106
+ ts.canHaveModifiers(declaration) &&
107
+ ts
108
+ .getModifiers(declaration)
109
+ ?.some((modifier) => modifier.kind === ts.SyntaxKind.ReadonlyKeyword),
110
+ )
111
+ )
112
+ return;
113
+ const object = resolve(node.expression);
114
+ if (object && ts.isObjectLiteralExpression(object)) {
115
+ for (const property of object.properties) {
116
+ if (
117
+ ts.isPropertyAssignment(property) &&
118
+ (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) &&
119
+ property.name.text === node.name.text
120
+ )
121
+ return property.initializer;
122
+ }
123
+ }
124
+ return;
125
+ }
126
+ return node;
127
+ };
128
+ const resolve = (node: ts.Node): ts.Node | undefined => {
129
+ if (active.has(node)) return;
130
+ active.add(node);
131
+ try {
132
+ const next = valueNode(node);
133
+ return next && next !== node ? resolve(next) : next;
134
+ } finally {
135
+ active.delete(node);
136
+ }
137
+ };
138
+ const read = (node: ts.Node): string | undefined => {
139
+ const resolved = resolve(node);
140
+ if (!resolved || active.has(resolved)) return;
141
+ active.add(resolved);
142
+ try {
143
+ if (ts.isStringLiteral(resolved) || ts.isNoSubstitutionTemplateLiteral(resolved))
144
+ return resolved.text;
145
+ if (ts.isTemplateExpression(resolved)) {
146
+ let value = resolved.head.text;
147
+ for (const part of resolved.templateSpans) {
148
+ const item = read(part.expression);
149
+ if (item === undefined) return;
150
+ value += item + part.literal.text;
151
+ }
152
+ return value;
153
+ }
154
+ if (
155
+ ts.isBinaryExpression(resolved) &&
156
+ resolved.operatorToken.kind === ts.SyntaxKind.PlusToken
157
+ ) {
158
+ const left = read(resolved.left),
159
+ right = read(resolved.right);
160
+ if (left !== undefined && right !== undefined) return left + right;
161
+ }
162
+ } finally {
163
+ active.delete(resolved);
164
+ }
165
+ };
166
+ return read(root);
167
+ };