@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.
- package/backends/codesys/emit.ts +24 -41
- package/backends/codesys/global-names.ts +49 -0
- package/backends/codesys/plan.ts +4 -89
- package/backends/codesys/task-access.ts +125 -0
- package/dist/cli.js +14 -3
- package/dist/cli.js.map +3 -3
- package/dist/codesys.js +360 -307
- package/dist/codesys.js.map +9 -8
- package/dist/index.js +23 -7
- package/dist/index.js.map +4 -4
- package/dist/ir.js +52 -2
- package/dist/ir.js.map +4 -4
- package/ir/declarations.ts +12 -5
- package/ir/model.ts +40 -1
- package/package.json +6 -6
- package/project/plant.ts +15 -2
- package/src/frontend/async.ts +9 -4
- package/src/frontend/typescript.ts +21 -7
- package/types/globals.d.ts +6 -0
- package/types/index.d.ts +37 -1
package/backends/codesys/emit.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
96
|
+
private readonly targetTypes;
|
|
98
97
|
private readonly numeric: NumericLowering;
|
|
99
|
-
private readonly
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
123
|
-
this.
|
|
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.
|
|
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 {
|
|
294
|
-
const
|
|
295
|
-
|
|
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.
|
|
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
|
-
|
|
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) => !
|
|
984
|
+
block.state.filter((item) => !retainedVariables.has(item.id)),
|
|
1002
985
|
);
|
|
1003
986
|
section(
|
|
1004
987
|
"VAR RETAIN",
|
|
1005
|
-
block.state.filter((item) =>
|
|
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
|
+
};
|
package/backends/codesys/plan.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 },
|
|
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=
|
|
1397
|
+
//# debugId=3D22720924D3B22B64756E2164756E21
|
|
1387
1398
|
//# sourceMappingURL=cli.js.map
|