@tsplc/compiler 0.2.10 → 0.2.11

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.
@@ -11,13 +11,12 @@ import type {
11
11
  Variable,
12
12
  } from "@tsplc/compiler/ir";
13
13
  import { defaultValue, instancedBlock, standardBlocks } from "@tsplc/compiler/ir";
14
- import { Names, pascalCase } from "./names.ts";
14
+ import { planGlobalNames } from "./global-names.ts";
15
+ import { Names } from "./names.ts";
15
16
  import { NumericLowering } from "./numeric.ts";
16
17
  import { type CodesysPlan, diagnostic, isCodesysPlan } from "./plan.ts";
17
- import { storageLayout } from "./storage.ts";
18
18
  import { accessedStorage } from "./task-access.ts";
19
19
  import { emitHttp, httpMethods, httpCertificateVerifier, opcuaServiceCapacity } from "./http.ts";
20
- import { CodesysTypes } from "./types.ts";
21
20
  import type { Publication } from "@tsplc/config";
22
21
 
23
22
  export interface SourceMapping {
@@ -59,7 +58,7 @@ export function emitCodesys(plan: CodesysPlan): CodesysBundle {
59
58
  // A bundle owns target names and source mappings. Per-routine declarations and
60
59
  // implementation lines stay local to emitRoutine, so they cannot leak between POUs.
61
60
  class CodesysEmitter {
62
- private readonly names = new Names();
61
+ private readonly names;
63
62
  private networkName: string | undefined;
64
63
  private readonly rpcChannels: {
65
64
  path: string;
@@ -94,9 +93,10 @@ class CodesysEmitter {
94
93
  string,
95
94
  { names: Names; references: Map<string, string> }
96
95
  >();
97
- private readonly targetTypes: CodesysTypes;
96
+ private readonly targetTypes;
98
97
  private readonly numeric: NumericLowering;
99
- private readonly layout;
98
+ private readonly globals;
99
+ private readonly retainedVariables;
100
100
 
101
101
  constructor(private readonly plan: CodesysPlan) {
102
102
  const { core, options } = plan;
@@ -107,22 +107,18 @@ class CodesysEmitter {
107
107
  if (!fn.owner && [...accessedStorage(fn, core)].some((id) => persistent.has(id)))
108
108
  this.programRoutines.add(fn.id);
109
109
  }
110
- for (const name of Object.values(options.programNames ?? {})) this.names.reserve(name);
111
-
112
- this.typeMap = new Map(core.types.map((type) => [type.id, type]));
113
- this.blocks = new Map(core.blocks.map((block) => [block.id, block]));
114
- this.blockNames = new Map(
115
- core.blocks.map((block) => [
116
- block.id,
117
- block.standard
118
- ? standardBlocks[block.standard].native
119
- : this.names.allocateObject("FB", block.name),
120
- ]),
110
+ const { names, typeMap, blockNames, targetTypes, globals, retainedVariables } = planGlobalNames(
111
+ core,
112
+ options.programNames,
121
113
  );
122
- for (const name of this.blockNames.values()) this.names.reserve(name);
123
- this.targetTypes = new CodesysTypes(this.typeMap, this.names, this.blockNames);
114
+ this.names = names;
115
+ this.typeMap = typeMap;
116
+ this.blockNames = blockNames;
117
+ this.targetTypes = targetTypes;
118
+ this.globals = globals;
119
+ this.retainedVariables = retainedVariables;
120
+ this.blocks = new Map(core.blocks.map((block) => [block.id, block]));
124
121
  this.numeric = new NumericLowering(this.typeMap, (label) => this.names.allocate(label));
125
- this.layout = storageLayout(core);
126
122
  this.objects.push(...this.targetTypes.objects);
127
123
  }
128
124
 
@@ -268,7 +264,7 @@ END_VAR
268
264
  let source = object.id.split(":")[0] ?? "";
269
265
  const program = core.programs.find((item) => item.id === object.id);
270
266
  if (program) source = program.entry.split(":")[0] ?? "";
271
- const group = this.layout.globalLists.find((item) => item.id === object.id);
267
+ const group = this.globals.find((item) => item.id === object.id);
272
268
  if (group) {
273
269
  // Variable IDs are relative to tsconfig, unlike their diagnostic spans.
274
270
  source = group.variables[0]?.id.split(":")[0] ?? "";
@@ -290,24 +286,11 @@ END_VAR
290
286
 
291
287
  private emitGlobals(): void {
292
288
  const { options } = this.plan;
293
- const { layout, names, references, mappings, targetTypes, typeMap, objects } = this;
294
- const hasExports = (group: (typeof layout.globalLists)[number]) =>
295
- group.variables.some((variable) => variable.exported && !variable.exposure);
296
- for (const group of layout.globalLists) {
297
- const splitByStorage = layout.globalLists.some(
298
- (other) =>
299
- other.module === group.module && other.storage !== group.storage && hasExports(other),
300
- );
301
- // Exported lists are the program's public interface; private storage stays generated.
302
- const name = hasExports(group)
303
- ? names.allocate(
304
- pascalCase(splitByStorage ? `${group.module}-${group.storage}` : group.module),
305
- )
306
- : names.allocateObject("GVL", `${group.module}_${group.storage}`);
289
+ const { globals, references, mappings, targetTypes, typeMap, objects } = this;
290
+ for (const group of globals) {
291
+ const { name, fields } = group;
307
292
  const published = new Set(this.plan.publication[options.buildTarget]);
308
293
  const publish = group.variables.some((variable) => published.has(variable.id));
309
- const fields = new Names();
310
- for (const name of this.blockNames.values()) fields.reserve(name);
311
294
  const lines = [
312
295
  ...(publish ? ["{attribute 'linkalways'}"] : []),
313
296
  "{attribute 'qualified_only'}",
@@ -318,7 +301,7 @@ END_VAR
318
301
  : "VAR_GLOBAL",
319
302
  ];
320
303
  for (const item of group.variables) {
321
- const field = fields.allocate(item.name);
304
+ const field = required(fields.get(item.id));
322
305
  references.set(item.id, `${name}.${field}`);
323
306
  if (published.has(item.id))
324
307
  lines.push(` {attribute 'symbol' := '${item.exposure ? "read" : "readwrite"}'}`);
@@ -606,7 +589,7 @@ END_VAR`,
606
589
  blocks,
607
590
  blockFields,
608
591
  numeric,
609
- layout,
592
+ retainedVariables,
610
593
  } = this;
611
594
  const block = "inputs" in fn ? fn : undefined;
612
595
  const method = "owner" in fn ? fn.owner : undefined;
@@ -998,11 +981,11 @@ END_VAR`,
998
981
  section("VAR_OUTPUT", block.outputs);
999
982
  section(
1000
983
  "VAR",
1001
- block.state.filter((item) => !layout.retainedVariables.has(item.id)),
984
+ block.state.filter((item) => !retainedVariables.has(item.id)),
1002
985
  );
1003
986
  section(
1004
987
  "VAR RETAIN",
1005
- block.state.filter((item) => layout.retainedVariables.has(item.id)),
988
+ block.state.filter((item) => retainedVariables.has(item.id)),
1006
989
  );
1007
990
  }
1008
991
  if (declarations.length) {
@@ -0,0 +1,49 @@
1
+ import { standardBlocks, type VerifiedCoreModule } from "@tsplc/compiler/ir";
2
+ import { Names, pascalCase } from "./names.ts";
3
+ import { storageLayout } from "./storage.ts";
4
+ import { CodesysTypes } from "./types.ts";
5
+
6
+ export const planGlobalNames = (
7
+ core: VerifiedCoreModule,
8
+ programNames: Readonly<Record<string, string>> = {},
9
+ ) => {
10
+ const names = new Names();
11
+ for (const name of Object.values(programNames)) names.reserve(name);
12
+
13
+ const typeMap = new Map(core.types.map((type) => [type.id, type]));
14
+ const blockNames = new Map(
15
+ core.blocks.map((block) => [
16
+ block.id,
17
+ block.standard
18
+ ? standardBlocks[block.standard].native
19
+ : names.allocateObject("FB", block.name),
20
+ ]),
21
+ );
22
+ for (const name of blockNames.values()) names.reserve(name);
23
+ const targetTypes = new CodesysTypes(typeMap, names, blockNames);
24
+
25
+ const { globalLists, retainedVariables } = storageLayout(core);
26
+ const hasExports = (group: (typeof globalLists)[number]) =>
27
+ group.variables.some((variable) => variable.exported && !variable.exposure);
28
+ const globals = globalLists.map((group) => {
29
+ const splitByStorage = globalLists.some(
30
+ (other) =>
31
+ other.module === group.module && other.storage !== group.storage && hasExports(other),
32
+ );
33
+ // Exported lists are the program's public interface. Private storage stays generated.
34
+ const name = hasExports(group)
35
+ ? names.allocate(
36
+ pascalCase(splitByStorage ? `${group.module}-${group.storage}` : group.module),
37
+ )
38
+ : names.allocateObject("GVL", `${group.module}_${group.storage}`);
39
+ const fields = new Names();
40
+ for (const name of blockNames.values()) fields.reserve(name);
41
+ return {
42
+ ...group,
43
+ name,
44
+ fields: new Map(group.variables.map((item) => [item.id, fields.allocate(item.name)])),
45
+ };
46
+ });
47
+
48
+ return { names, typeMap, blockNames, targetTypes, globals, retainedVariables };
49
+ };
@@ -17,7 +17,7 @@ import { iecSpelling, isStoragePath, resolveStoragePath, type StorageBinding } f
17
17
  import { isIecIdentifier } from "./names.ts";
18
18
  import { storageLayout } from "./storage.ts";
19
19
  import { validateHttpLayout, resolveHttpEndpoint, type HttpEndpoint } from "./http.ts";
20
- import { accessedStorage } from "./task-access.ts";
20
+ import { accessedStorage, schedulePrograms, taskStorageOwnership } from "./task-access.ts";
21
21
  import { resolvePublication } from "./publication.ts";
22
22
  import type { Publication } from "@tsplc/config";
23
23
 
@@ -58,58 +58,12 @@ export function planCodesys(
58
58
  ),
59
59
  };
60
60
 
61
- const backgroundTasks = new Map<string, Deployment["tasks"][number]>();
62
- for (const program of core.programs) {
63
- if (!program.background) continue;
64
- const { priority, intervalNanoseconds } = program.background;
65
- const name = `TsplcBackground_${priority}_${BigInt(intervalNanoseconds) / 1000000n}`;
66
- const existing = backgroundTasks.get(name);
67
- backgroundTasks.set(name, {
68
- name,
69
- priority,
70
- intervalNanoseconds,
71
- programs: [...(existing?.programs ?? []), program.name],
72
- });
73
- }
74
- deployment = { tasks: [...deployment.tasks, ...backgroundTasks.values()] };
75
61
  const options = parsed.data;
76
62
  const diagnostics: Diagnostic[] = [];
77
63
  const report = (message: string, span?: SourceSpan): void => {
78
64
  diagnostics.push(diagnostic(message, span));
79
65
  };
80
- const callerTasks = deployment.tasks;
81
- for (const continuation of core.programs.filter((program) => program.continuation)) {
82
- const scope = continuation.continuation!;
83
- const root = core.variables.find(
84
- (variable) => variable.name === scope.instance && variable.span.file === scope.file,
85
- );
86
- if (!root) {
87
- report(`Missing continuation owner ${scope.instance}.`);
88
- continue;
89
- }
90
- const owners = callerTasks.filter((task) =>
91
- task.programs.some((name) => {
92
- const program = core.programs.find((program) => program.name === name);
93
- if (!program || program.background) return false;
94
- const fn = core.functions.find((fn) => fn.id === program.entry);
95
- return fn && accessedStorage(fn, core).has(root.id);
96
- }),
97
- );
98
- if (owners.length !== 1) {
99
- report(
100
- `Continuation owner ${scope.instance} must belong to exactly one caller task.`,
101
- root.span,
102
- );
103
- continue;
104
- }
105
- deployment = {
106
- tasks: deployment.tasks.map((task) =>
107
- task.name === owners[0]!.name
108
- ? { ...task, programs: [...task.programs, continuation.name] }
109
- : task,
110
- ),
111
- };
112
- }
66
+ deployment = { tasks: schedulePrograms(core, deployment.tasks, report) };
113
67
  const http = new Map<string, HttpEndpoint>();
114
68
  for (const block of core.blocks)
115
69
  if (block.runtime?.kind === "http" || block.runtime?.kind === "rpc") {
@@ -314,21 +268,10 @@ function ioBits(type: PlcType | undefined): number {
314
268
  }
315
269
 
316
270
  function validateTasks(core: VerifiedCoreModule, deployment: Deployment, report: Report): void {
317
- // One cyclic task owns each mutable storage root, including transitive calls
318
- // and borrowed globals. Sharing state within an ordered task remains valid.
319
271
  const programs = new Map(core.programs.map((program) => [program.name, program]));
320
272
  const assignment = new Map<string, string>();
321
273
  const taskNames = new Set<string>();
322
- const stateOwners = new Map<string, string>();
323
- const globals = new Map(core.variables.map((item) => [item.id, item]));
324
- const functions = new Map(core.functions.map((item) => [item.id, item]));
325
- const blockRuntimes = new Map(
326
- core.types.flatMap((type) =>
327
- type.kind === "block"
328
- ? [[type.id, core.blocks.find((block) => block.id === type.block)?.runtime?.kind] as const]
329
- : [],
330
- ),
331
- );
274
+ const checkOwnership = taskStorageOwnership(core, report);
332
275
  for (const task of deployment.tasks) {
333
276
  if (!isIecIdentifier(task.name)) report(`Invalid task name ${task.name}.`);
334
277
  if (taskNames.has(task.name.toUpperCase())) report(`Duplicate task name ${task.name}.`);
@@ -351,35 +294,7 @@ function validateTasks(core: VerifiedCoreModule, deployment: Deployment, report:
351
294
  }
352
295
  if (assignment.has(name)) report(`Program ${name} must belong to exactly one task.`);
353
296
  assignment.set(name, task.name);
354
- const fn = functions.get(program.entry);
355
- if (!fn) throw new Error("Verified core has a missing entry.");
356
- const host =
357
- program.background &&
358
- core.variables.find(
359
- (item) =>
360
- item.name === program.background?.worker &&
361
- item.span.file === fn.span.file &&
362
- blockRuntimes.get(item.type) === "backgroundHost",
363
- );
364
- for (const id of accessedStorage(fn, core, host ? host.id : undefined)) {
365
- const item = globals.get(id);
366
- if (!item || item.storage === "constant") continue;
367
- if (blockRuntimes.get(item.type) === "mailbox") continue;
368
- const ownsWorker =
369
- program.background?.worker === item.name && item.span.file === fn.span.file;
370
- if (program.background && (!ownsWorker || item.id === host?.id))
371
- report(
372
- `Background task ${program.name} accesses scan-owned storage ${item.name}; pass value snapshots through the mailbox.`,
373
- item.span,
374
- );
375
- const owner = stateOwners.get(id);
376
- if (owner && owner !== task.name)
377
- report(
378
- `Storage ${item.name} is shared across cyclic tasks ${owner} and ${task.name}.`,
379
- item.span,
380
- );
381
- stateOwners.set(id, task.name);
382
- }
297
+ checkOwnership(task.name, program);
383
298
  }
384
299
  }
385
300
  for (const name of programs.keys())
@@ -4,9 +4,86 @@ import type {
4
4
  CoreFunction,
5
5
  CorePath,
6
6
  CoreStatement,
7
+ PlcProgram,
8
+ SourceSpan,
9
+ Task,
7
10
  VerifiedCoreModule,
8
11
  } from "@tsplc/compiler/ir";
9
12
 
13
+ type Report = (message: string, span?: SourceSpan) => void;
14
+
15
+ export function schedulePrograms(
16
+ core: VerifiedCoreModule,
17
+ tasks: readonly Task[],
18
+ report: Report,
19
+ ): readonly Task[] {
20
+ const background = new Map<string, Task & { programs: string[] }>();
21
+ for (const program of core.programs) {
22
+ if (!program.background) continue;
23
+ const { priority, intervalNanoseconds } = program.background;
24
+ const name = `TsplcBackground_${priority}_${BigInt(intervalNanoseconds) / 1_000_000n}`;
25
+ const existing = background.get(name);
26
+ if (existing) {
27
+ existing.programs.push(program.name);
28
+ continue;
29
+ }
30
+ background.set(name, {
31
+ name,
32
+ priority,
33
+ intervalNanoseconds,
34
+ programs: [program.name],
35
+ });
36
+ }
37
+ return appendContinuations(core, [...tasks, ...background.values()], report);
38
+ }
39
+
40
+ /** The generated worker may enter its host, and only mailbox roots are shared between tasks. */
41
+ export function taskStorageOwnership(
42
+ core: VerifiedCoreModule,
43
+ report: Report,
44
+ ): (task: string, program: PlcProgram) => void {
45
+ const owners = new Map<string, string>();
46
+ const globals = new Map(core.variables.map((item) => [item.id, item]));
47
+ const functions = new Map(core.functions.map((item) => [item.id, item]));
48
+ const blocks = new Map(core.blocks.map((block) => [block.id, block.runtime?.kind]));
49
+ const runtimes = new Map(
50
+ core.types.flatMap((type) =>
51
+ type.kind === "block" ? [[type.id, blocks.get(type.block)] as const] : [],
52
+ ),
53
+ );
54
+
55
+ return (task, program) => {
56
+ const fn = functions.get(program.entry);
57
+ if (!fn) throw new Error("Verified core has a missing entry.");
58
+ const worker = program.background?.worker;
59
+ const host =
60
+ worker &&
61
+ core.variables.find(
62
+ (item) =>
63
+ item.name === worker &&
64
+ item.span.file === fn.span.file &&
65
+ runtimes.get(item.type) === "backgroundHost",
66
+ );
67
+ for (const id of accessedStorage(fn, core, host ? host.id : undefined)) {
68
+ const item = globals.get(id);
69
+ if (!item || item.storage === "constant" || runtimes.get(item.type) === "mailbox") continue;
70
+ const ownsWorker = worker === item.name && item.span.file === fn.span.file;
71
+ if (program.background && (!ownsWorker || (host && item.id === host.id)))
72
+ report(
73
+ `Background task ${program.name} accesses scan-owned storage ${item.name}; pass value snapshots through the mailbox.`,
74
+ item.span,
75
+ );
76
+ const owner = owners.get(id);
77
+ if (owner && owner !== task)
78
+ report(
79
+ `Storage ${item.name} is shared across cyclic tasks ${owner} and ${task}.`,
80
+ item.span,
81
+ );
82
+ owners.set(id, task);
83
+ }
84
+ };
85
+ }
86
+
10
87
  // Follow both helper calls and instance updates. Borrow addresses count as storage
11
88
  // access, so scheduling cannot accidentally give one mutable record two owners.
12
89
  export function accessedStorage(
@@ -116,3 +193,51 @@ export function accessedStorage(
116
193
  body(fn.body);
117
194
  return result;
118
195
  }
196
+
197
+ /** Detached continuations run after their caller programs in the same cyclic task. */
198
+ function appendContinuations(
199
+ core: VerifiedCoreModule,
200
+ tasks: readonly Task[],
201
+ report: Report,
202
+ ): readonly Task[] {
203
+ const continuations = core.programs.flatMap((program) =>
204
+ program.continuation ? [{ name: program.name, scope: program.continuation }] : [],
205
+ );
206
+ if (!continuations.length) return tasks;
207
+
208
+ const programs = new Map(core.programs.map((program) => [program.name, program]));
209
+ const functions = new Map(core.functions.map((routine) => [routine.id, routine]));
210
+ const placements = tasks.map((task): { task: Task; roots: Set<string>; additions: string[] } => {
211
+ const roots = new Set<string>();
212
+ for (const name of task.programs) {
213
+ const program = programs.get(name);
214
+ if (!program || program.background) continue;
215
+ const entry = functions.get(program.entry);
216
+ if (entry) for (const id of accessedStorage(entry, core)) roots.add(id);
217
+ }
218
+ return { task, roots, additions: [] };
219
+ });
220
+ for (const { name, scope } of continuations) {
221
+ const root = core.variables.find(
222
+ (variable) => variable.name === scope.instance && variable.span.file === scope.file,
223
+ );
224
+ if (!root) {
225
+ report(`Missing continuation owner ${scope.instance}.`);
226
+ continue;
227
+ }
228
+ const owners = placements.filter(({ roots }) => roots.has(root.id));
229
+ const [owner] = owners;
230
+ if (!owner || owners.length !== 1) {
231
+ report(
232
+ `Continuation owner ${scope.instance} must belong to exactly one caller task.`,
233
+ root.span,
234
+ );
235
+ continue;
236
+ }
237
+ owner.additions.push(name);
238
+ }
239
+ return placements.map(({ task, additions }) => ({
240
+ ...task,
241
+ programs: task.programs.concat(additions),
242
+ }));
243
+ }
package/dist/cli.js CHANGED
@@ -654,6 +654,13 @@ var askCredentials = async () => ({
654
654
  password: await askSecret("Controller password: ")
655
655
  });
656
656
  var progress = (message) => console.log(` ${message}`);
657
+ var timedProgress = () => {
658
+ const start = Date.now();
659
+ return (message) => {
660
+ const seconds = Math.round((Date.now() - start) / 1000);
661
+ progress(`${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")} ${message}`);
662
+ };
663
+ };
657
664
  var connect = () => {
658
665
  const environment = z.object({
659
666
  TSPLC_AGENT_URL: z.url({
@@ -761,9 +768,11 @@ var deploy = async (agent, file, loaded, flags) => {
761
768
  throw new Error("A login is open; log out before deploying: tsplc logout");
762
769
  const target = !status.target || flags.select ? await selectTarget(agent) : status.target;
763
770
  console.log(`Building the release for ${target.host}…`);
771
+ const building = timedProgress();
764
772
  const { release, diagnostics } = await prepareRelease(file, loaded, flags);
765
773
  if (!release)
766
774
  return { diagnostics };
775
+ building("Built");
767
776
  if (flags.download && !await confirm("A full download stops the running application while it loads. Continue?", flags))
768
777
  throw new Error("Cancelled.");
769
778
  const credentials = await askCredentials();
@@ -773,12 +782,14 @@ var deploy = async (agent, file, loaded, flags) => {
773
782
  initializeUser: flags["initialize-user"] ?? false
774
783
  };
775
784
  console.log(release.manifest.server ? "Deploying the server and the program…" : "Deploying the program…");
785
+ const deploying = timedProgress();
786
+ deploying("Uploading");
776
787
  try {
777
- return await agent.deploy(release, settings, progress);
788
+ return await agent.deploy(release, settings, deploying);
778
789
  } catch (error) {
779
790
  if (!(error instanceof AgentOperationError && error.downloadRequired) || !await confirm(`${error.message}. A full download stops the running application while it loads. Download now?`, flags))
780
791
  throw error;
781
- return agent.deploy(release, { ...settings, download: true }, progress);
792
+ return agent.deploy(release, { ...settings, download: true }, timedProgress());
782
793
  }
783
794
  };
784
795
  var ensureLogin = async (agent) => {
@@ -1383,5 +1394,5 @@ try {
1383
1394
  }
1384
1395
  }
1385
1396
 
1386
- //# debugId=4A2A7CEF65387D0264756E2164756E21
1397
+ //# debugId=3D22720924D3B22B64756E2164756E21
1387
1398
  //# sourceMappingURL=cli.js.map