@tsplc/compiler 0.2.9 → 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 +50 -48
- package/dist/cli.js.map +5 -5
- 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 +22 -9
- package/project/server.ts +43 -13
- 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
|
@@ -10,14 +10,11 @@ import ts from "typescript";
|
|
|
10
10
|
import { typeCheckOnly } from "@tsplc/compiler";
|
|
11
11
|
var resolveServerModule = (root, name) => name.startsWith(".") || name.startsWith("/") || existsSync(resolve(root, name)) ? resolve(root, name) : createRequire(join(root, "package.json")).resolve(name);
|
|
12
12
|
var kebabCase = (name) => name.replace(/([a-z0-9])([A-Z])|([A-Z])([A-Z][a-z])/g, "$1$3-$2$4").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
13
|
-
var
|
|
14
|
-
const
|
|
15
|
-
if (!
|
|
16
|
-
throw new Error("Configure server.runtime to build
|
|
17
|
-
const runtime = settings.runtime;
|
|
13
|
+
var checkedServerEntry = (config, root) => {
|
|
14
|
+
const runtime = config.server?.runtime;
|
|
15
|
+
if (!runtime)
|
|
16
|
+
throw new Error("Configure server.runtime to build the server.");
|
|
18
17
|
const entry = resolveServerModule(root, runtime.entry);
|
|
19
|
-
const windows = runtime.target ? runtime.target.includes("windows") : process.platform === "win32";
|
|
20
|
-
const outFile = resolve(root, "build/server", `${kebabCase(config.name)}-services${windows ? ".exe" : ""}`);
|
|
21
18
|
const tsconfig = ts.findConfigFile(dirname(entry), ts.sys.fileExists);
|
|
22
19
|
if (!tsconfig)
|
|
23
20
|
throw new Error("Server entry requires a tsconfig.json.");
|
|
@@ -39,6 +36,12 @@ var buildServer = async (config, root) => {
|
|
|
39
36
|
getNewLine: () => `
|
|
40
37
|
`
|
|
41
38
|
}));
|
|
39
|
+
return { entry, runtime };
|
|
40
|
+
};
|
|
41
|
+
var buildServer = async (config, root) => {
|
|
42
|
+
const { entry, runtime } = checkedServerEntry(config, root);
|
|
43
|
+
const windows = runtime.target ? runtime.target.includes("windows") : process.platform === "win32";
|
|
44
|
+
const outFile = resolve(root, "build/server", `${kebabCase(config.name)}-services${windows ? ".exe" : ""}`);
|
|
42
45
|
await mkdir(dirname(outFile), { recursive: true });
|
|
43
46
|
const staging = await mkdtemp(join(dirname(outFile), ".tsplc-server-"));
|
|
44
47
|
try {
|
|
@@ -531,14 +534,11 @@ import ts2 from "typescript";
|
|
|
531
534
|
import { typeCheckOnly as typeCheckOnly2 } from "@tsplc/compiler";
|
|
532
535
|
var resolveServerModule2 = (root, name) => name.startsWith(".") || name.startsWith("/") || existsSync2(resolve3(root, name)) ? resolve3(root, name) : createRequire2(join4(root, "package.json")).resolve(name);
|
|
533
536
|
var kebabCase2 = (name) => name.replace(/([a-z0-9])([A-Z])|([A-Z])([A-Z][a-z])/g, "$1$3-$2$4").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
534
|
-
var
|
|
535
|
-
const
|
|
536
|
-
if (!
|
|
537
|
-
throw new Error("Configure server.runtime to build
|
|
538
|
-
const runtime = settings.runtime;
|
|
537
|
+
var checkedServerEntry2 = (config, root) => {
|
|
538
|
+
const runtime = config.server?.runtime;
|
|
539
|
+
if (!runtime)
|
|
540
|
+
throw new Error("Configure server.runtime to build the server.");
|
|
539
541
|
const entry = resolveServerModule2(root, runtime.entry);
|
|
540
|
-
const windows = runtime.target ? runtime.target.includes("windows") : process.platform === "win32";
|
|
541
|
-
const outFile = resolve3(root, "build/server", `${kebabCase2(config.name)}-services${windows ? ".exe" : ""}`);
|
|
542
542
|
const tsconfig = ts2.findConfigFile(dirname4(entry), ts2.sys.fileExists);
|
|
543
543
|
if (!tsconfig)
|
|
544
544
|
throw new Error("Server entry requires a tsconfig.json.");
|
|
@@ -560,34 +560,25 @@ var buildServer2 = async (config, root) => {
|
|
|
560
560
|
getNewLine: () => `
|
|
561
561
|
`
|
|
562
562
|
}));
|
|
563
|
+
return { entry, runtime };
|
|
564
|
+
};
|
|
565
|
+
var bundleServer = async (config, root) => {
|
|
566
|
+
const { entry } = checkedServerEntry2(config, root);
|
|
567
|
+
const outFile = resolve3(root, "build/server", `${kebabCase2(config.name)}-services.js`);
|
|
568
|
+
const result = await Bun.build({
|
|
569
|
+
entrypoints: [entry],
|
|
570
|
+
target: "bun",
|
|
571
|
+
minify: true,
|
|
572
|
+
env: "disable"
|
|
573
|
+
});
|
|
574
|
+
if (!result.success)
|
|
575
|
+
throw new AggregateError(result.logs, "Server bundle build failed.");
|
|
576
|
+
const [script, ...assets] = result.outputs;
|
|
577
|
+
if (!script || assets.length)
|
|
578
|
+
throw new Error(`The server bundle needs files beside its script (${assets.map(({ path }) => path).join(", ")}), which deployments don't carry.`);
|
|
563
579
|
await mkdir4(dirname4(outFile), { recursive: true });
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const binary = join4(staging, "server");
|
|
567
|
-
const result = await Bun.build({
|
|
568
|
-
entrypoints: [entry],
|
|
569
|
-
env: "disable",
|
|
570
|
-
compile: {
|
|
571
|
-
outfile: binary,
|
|
572
|
-
...runtime.target ? { target: runtime.target } : {},
|
|
573
|
-
autoloadDotenv: false,
|
|
574
|
-
autoloadBunfig: false,
|
|
575
|
-
autoloadTsconfig: false,
|
|
576
|
-
autoloadPackageJson: false
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
if (!result.success)
|
|
580
|
-
throw new AggregateError(result.logs, "Server executable build failed.");
|
|
581
|
-
await rename3(windows ? `${binary}.exe` : binary, outFile);
|
|
582
|
-
return {
|
|
583
|
-
server: {
|
|
584
|
-
executable: outFile,
|
|
585
|
-
target: runtime.target ?? `bun-${process.platform}-${process.arch}`
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
|
-
} finally {
|
|
589
|
-
await rm4(staging, { recursive: true, force: true });
|
|
590
|
-
}
|
|
580
|
+
await Bun.write(outFile, script);
|
|
581
|
+
return outFile;
|
|
591
582
|
};
|
|
592
583
|
|
|
593
584
|
// packages/compiler/project/plant.ts
|
|
@@ -663,6 +654,13 @@ var askCredentials = async () => ({
|
|
|
663
654
|
password: await askSecret("Controller password: ")
|
|
664
655
|
});
|
|
665
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
|
+
};
|
|
666
664
|
var connect = () => {
|
|
667
665
|
const environment = z.object({
|
|
668
666
|
TSPLC_AGENT_URL: z.url({
|
|
@@ -732,16 +730,16 @@ var prepareRelease = async (file, loaded, flags) => {
|
|
|
732
730
|
{ path: "template.project", content: Bun.file(settings.template) },
|
|
733
731
|
...libraries.map((library) => ({ path: library.release, content: Bun.file(library.path) }))
|
|
734
732
|
];
|
|
735
|
-
for await (const source of new Bun.Glob("
|
|
733
|
+
for await (const source of new Bun.Glob("**/*.st").scan({ cwd: settings.sourceDir, dot: true }))
|
|
736
734
|
files.push({
|
|
737
735
|
path: `st/${portable(source)}`,
|
|
738
736
|
content: Bun.file(resolve4(settings.sourceDir, source))
|
|
739
737
|
});
|
|
740
738
|
let server;
|
|
741
739
|
if (config.server?.runtime) {
|
|
742
|
-
const
|
|
743
|
-
server = `server/${basename(
|
|
744
|
-
files.push({ path: server, content: Bun.file(
|
|
740
|
+
const script = await bundleServer(config, root);
|
|
741
|
+
server = `server/${basename(script)}`;
|
|
742
|
+
files.push({ path: server, content: Bun.file(script) });
|
|
745
743
|
}
|
|
746
744
|
const manifest = {
|
|
747
745
|
config: {
|
|
@@ -770,9 +768,11 @@ var deploy = async (agent, file, loaded, flags) => {
|
|
|
770
768
|
throw new Error("A login is open; log out before deploying: tsplc logout");
|
|
771
769
|
const target = !status.target || flags.select ? await selectTarget(agent) : status.target;
|
|
772
770
|
console.log(`Building the release for ${target.host}…`);
|
|
771
|
+
const building = timedProgress();
|
|
773
772
|
const { release, diagnostics } = await prepareRelease(file, loaded, flags);
|
|
774
773
|
if (!release)
|
|
775
774
|
return { diagnostics };
|
|
775
|
+
building("Built");
|
|
776
776
|
if (flags.download && !await confirm("A full download stops the running application while it loads. Continue?", flags))
|
|
777
777
|
throw new Error("Cancelled.");
|
|
778
778
|
const credentials = await askCredentials();
|
|
@@ -782,12 +782,14 @@ var deploy = async (agent, file, loaded, flags) => {
|
|
|
782
782
|
initializeUser: flags["initialize-user"] ?? false
|
|
783
783
|
};
|
|
784
784
|
console.log(release.manifest.server ? "Deploying the server and the program…" : "Deploying the program…");
|
|
785
|
+
const deploying = timedProgress();
|
|
786
|
+
deploying("Uploading");
|
|
785
787
|
try {
|
|
786
|
-
return await agent.deploy(release, settings,
|
|
788
|
+
return await agent.deploy(release, settings, deploying);
|
|
787
789
|
} catch (error) {
|
|
788
790
|
if (!(error instanceof AgentOperationError && error.downloadRequired) || !await confirm(`${error.message}. A full download stops the running application while it loads. Download now?`, flags))
|
|
789
791
|
throw error;
|
|
790
|
-
return agent.deploy(release, { ...settings, download: true },
|
|
792
|
+
return agent.deploy(release, { ...settings, download: true }, timedProgress());
|
|
791
793
|
}
|
|
792
794
|
};
|
|
793
795
|
var ensureLogin = async (agent) => {
|
|
@@ -1392,5 +1394,5 @@ try {
|
|
|
1392
1394
|
}
|
|
1393
1395
|
}
|
|
1394
1396
|
|
|
1395
|
-
//# debugId=
|
|
1397
|
+
//# debugId=3D22720924D3B22B64756E2164756E21
|
|
1396
1398
|
//# sourceMappingURL=cli.js.map
|