@tsplc/compiler 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +16 -2
- package/backends/codesys/config.ts +54 -0
- package/backends/codesys/emit.ts +1063 -0
- package/backends/codesys/engineering.ts +88 -0
- package/backends/codesys/http.ts +598 -0
- package/backends/codesys/index.ts +12 -0
- package/backends/codesys/io-paths.ts +103 -0
- package/backends/codesys/names.ts +76 -0
- package/backends/codesys/numeric.ts +356 -0
- package/backends/codesys/plan.ts +512 -0
- package/backends/codesys/publication.ts +59 -0
- package/backends/codesys/storage.ts +63 -0
- package/backends/codesys/task-access.ts +118 -0
- package/backends/codesys/types.ts +178 -0
- package/backends/codesys/vendor.ts +108 -0
- package/dist/cli.js +740 -0
- package/dist/cli.js.map +13 -0
- package/dist/clock.js +8 -0
- package/dist/clock.js.map +10 -0
- package/dist/codesys.js +2587 -0
- package/dist/codesys.js.map +22 -0
- package/dist/elaborate-capture.js +118 -0
- package/dist/elaborate-capture.js.map +10 -0
- package/dist/index.js +6859 -0
- package/dist/index.js.map +25 -0
- package/dist/ir.js +1888 -0
- package/dist/ir.js.map +22 -0
- package/dist/math.js +24 -0
- package/dist/math.js.map +10 -0
- package/dist/project.js +423 -0
- package/dist/project.js.map +11 -0
- package/ir/aggregates.ts +333 -0
- package/ir/allocation.ts +74 -0
- package/ir/core-model.ts +116 -0
- package/ir/core.ts +63 -0
- package/ir/declarations.ts +201 -0
- package/ir/diagnostics.ts +43 -0
- package/ir/domains.ts +96 -0
- package/ir/index.ts +7 -0
- package/ir/link.ts +176 -0
- package/ir/model.ts +268 -0
- package/ir/normalization-error.ts +10 -0
- package/ir/routine.ts +826 -0
- package/ir/scalars.ts +675 -0
- package/ir/types.ts +70 -0
- package/package.json +105 -4
- package/project/artifacts.ts +277 -0
- package/project/cli.ts +282 -0
- package/project/index.ts +1 -0
- package/project/project.ts +298 -0
- package/project/server.ts +83 -0
- package/src/frontend/async.ts +1060 -0
- package/src/frontend/blocks.ts +419 -0
- package/src/frontend/configuration.ts +167 -0
- package/src/frontend/const-functions.ts +153 -0
- package/src/frontend/cooperative.ts +75 -0
- package/src/frontend/elaborate/capture.ts +138 -0
- package/src/frontend/elaborate/graph.ts +94 -0
- package/src/frontend/elaborate/index.ts +302 -0
- package/src/frontend/elaborate/layouts.ts +390 -0
- package/src/frontend/elaborate/model.ts +177 -0
- package/src/frontend/elaborate/specialize.ts +2280 -0
- package/src/frontend/exposure.ts +147 -0
- package/src/frontend/module-graph.ts +63 -0
- package/src/frontend/promises.ts +751 -0
- package/src/frontend/services.ts +435 -0
- package/src/frontend/types.ts +578 -0
- package/src/frontend/typescript.ts +2442 -0
- package/src/index.ts +7 -0
- package/stdlib/clock.ts +2 -0
- package/stdlib/math.ts +19 -0
- package/types/async.d.ts +81 -0
- package/types/clock.d.ts +4 -0
- package/types/config.d.ts +2 -0
- package/types/data-types.d.ts +31 -0
- package/types/globals.d.ts +40 -0
- package/types/index.d.ts +111 -0
- package/types/plc.d.ts +5 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { runtimeSources } from "./module-graph.ts";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { error, type Diagnostic } from "@tsplc/compiler/ir";
|
|
4
|
+
|
|
5
|
+
/** Normalize static arrow bindings so all routine forms use the same PLC analysis. */
|
|
6
|
+
export const lowerConstFunctions = (program: ts.Program) => {
|
|
7
|
+
const checker = program.getTypeChecker();
|
|
8
|
+
const printer = ts.createPrinter();
|
|
9
|
+
const sources = new Map<string, string>();
|
|
10
|
+
const diagnostics: Diagnostic[] = [];
|
|
11
|
+
|
|
12
|
+
for (const source of runtimeSources(program)) {
|
|
13
|
+
if (source.isDeclarationFile) continue;
|
|
14
|
+
const edits: { start: number; end: number; text: string }[] = [];
|
|
15
|
+
for (const statement of source.statements) {
|
|
16
|
+
const routines = ts.isClassDeclaration(statement)
|
|
17
|
+
? statement.members.filter(ts.isMethodDeclaration)
|
|
18
|
+
: [statement];
|
|
19
|
+
for (const routine of routines) {
|
|
20
|
+
if (
|
|
21
|
+
(!ts.isFunctionDeclaration(routine) && !ts.isMethodDeclaration(routine)) ||
|
|
22
|
+
routine.type ||
|
|
23
|
+
!routine.body ||
|
|
24
|
+
!routine.modifiers?.some((item) => item.kind === ts.SyntaxKind.AsyncKeyword)
|
|
25
|
+
)
|
|
26
|
+
continue;
|
|
27
|
+
const signature = checker.getSignatureFromDeclaration(routine);
|
|
28
|
+
const result =
|
|
29
|
+
signature && checker.getAwaitedType(checker.getReturnTypeOfSignature(signature));
|
|
30
|
+
if (result && result.flags & ts.TypeFlags.Void) {
|
|
31
|
+
const position = routine.body.getStart(source);
|
|
32
|
+
edits.push({ start: position, end: position, text: ":Promise<void> " });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const statement of source.statements) {
|
|
37
|
+
if (
|
|
38
|
+
!ts.isVariableStatement(statement) ||
|
|
39
|
+
!statement.declarationList.declarations.some(
|
|
40
|
+
(declaration) => declaration.initializer && ts.isArrowFunction(declaration.initializer),
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
continue;
|
|
44
|
+
|
|
45
|
+
const declarations: ts.Statement[] = [];
|
|
46
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
47
|
+
const arrow = declaration.initializer;
|
|
48
|
+
if (!arrow || !ts.isArrowFunction(arrow)) {
|
|
49
|
+
declarations.push(
|
|
50
|
+
ts.factory.updateVariableStatement(
|
|
51
|
+
statement,
|
|
52
|
+
statement.modifiers,
|
|
53
|
+
ts.factory.updateVariableDeclarationList(statement.declarationList, [declaration]),
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (
|
|
59
|
+
!(statement.declarationList.flags & ts.NodeFlags.Const) ||
|
|
60
|
+
!ts.isIdentifier(declaration.name)
|
|
61
|
+
) {
|
|
62
|
+
const start = declaration.getStart(source);
|
|
63
|
+
const position = source.getLineAndCharacterOfPosition(start);
|
|
64
|
+
diagnostics.push(
|
|
65
|
+
error(
|
|
66
|
+
"PLC1007",
|
|
67
|
+
"PLC routines require named const bindings; mutable function values are unsupported.",
|
|
68
|
+
{
|
|
69
|
+
file: source.fileName,
|
|
70
|
+
start,
|
|
71
|
+
end: declaration.end,
|
|
72
|
+
line: position.line + 1,
|
|
73
|
+
column: position.character + 1,
|
|
74
|
+
},
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const contextual =
|
|
81
|
+
declaration.type && ts.isFunctionTypeNode(declaration.type)
|
|
82
|
+
? declaration.type
|
|
83
|
+
: undefined;
|
|
84
|
+
const signature = checker.getTypeAtLocation(declaration.name).getCallSignatures()[0];
|
|
85
|
+
const returnsVoid =
|
|
86
|
+
signature &&
|
|
87
|
+
Boolean(checker.getReturnTypeOfSignature(signature).flags & ts.TypeFlags.Void);
|
|
88
|
+
const inferredResult =
|
|
89
|
+
signature && checker.getAwaitedType(checker.getReturnTypeOfSignature(signature));
|
|
90
|
+
const asyncVoid =
|
|
91
|
+
arrow.modifiers?.some((item) => item.kind === ts.SyntaxKind.AsyncKeyword) &&
|
|
92
|
+
inferredResult &&
|
|
93
|
+
inferredResult.flags & ts.TypeFlags.Void;
|
|
94
|
+
const inferredType = asyncVoid
|
|
95
|
+
? ts.factory.createTypeReferenceNode("Promise", [
|
|
96
|
+
ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword),
|
|
97
|
+
])
|
|
98
|
+
: undefined;
|
|
99
|
+
const result =
|
|
100
|
+
arrow.type ??
|
|
101
|
+
contextual?.type ??
|
|
102
|
+
(returnsVoid
|
|
103
|
+
? ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword)
|
|
104
|
+
: inferredType);
|
|
105
|
+
const parameters = arrow.parameters.map((parameter, index) =>
|
|
106
|
+
ts.factory.updateParameterDeclaration(
|
|
107
|
+
parameter,
|
|
108
|
+
parameter.modifiers,
|
|
109
|
+
parameter.dotDotDotToken,
|
|
110
|
+
parameter.name,
|
|
111
|
+
parameter.questionToken,
|
|
112
|
+
parameter.type ?? contextual?.parameters[index]?.type,
|
|
113
|
+
parameter.initializer,
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
const body = ts.isBlock(arrow.body)
|
|
117
|
+
? arrow.body
|
|
118
|
+
: ts.factory.createBlock(
|
|
119
|
+
[
|
|
120
|
+
result?.kind === ts.SyntaxKind.VoidKeyword
|
|
121
|
+
? ts.factory.createExpressionStatement(arrow.body)
|
|
122
|
+
: ts.factory.createReturnStatement(arrow.body),
|
|
123
|
+
],
|
|
124
|
+
true,
|
|
125
|
+
);
|
|
126
|
+
declarations.push(
|
|
127
|
+
ts.factory.createFunctionDeclaration(
|
|
128
|
+
[...(statement.modifiers ?? []), ...(arrow.modifiers ?? [])],
|
|
129
|
+
undefined,
|
|
130
|
+
declaration.name,
|
|
131
|
+
arrow.typeParameters ?? contextual?.typeParameters,
|
|
132
|
+
parameters,
|
|
133
|
+
result,
|
|
134
|
+
body,
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
edits.push({
|
|
139
|
+
start: statement.getStart(source),
|
|
140
|
+
end: statement.end,
|
|
141
|
+
text: declarations
|
|
142
|
+
.map((declaration) => printer.printNode(ts.EmitHint.Unspecified, declaration, source))
|
|
143
|
+
.join("\n"),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (!edits.length) continue;
|
|
147
|
+
let text = source.text;
|
|
148
|
+
for (const edit of edits.reverse())
|
|
149
|
+
text = text.slice(0, edit.start) + edit.text + text.slice(edit.end);
|
|
150
|
+
sources.set(source.fileName, text);
|
|
151
|
+
}
|
|
152
|
+
return { sources, diagnostics };
|
|
153
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
interface Parameter {
|
|
2
|
+
name: string;
|
|
3
|
+
type: string;
|
|
4
|
+
aggregate: boolean;
|
|
5
|
+
}
|
|
6
|
+
interface Worker {
|
|
7
|
+
start: string;
|
|
8
|
+
advance: string;
|
|
9
|
+
snapshot: string;
|
|
10
|
+
reset: string;
|
|
11
|
+
declaration: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A fixed call-site frame; callbacks and ordinary state stay in the caller task. */
|
|
15
|
+
export const cooperativeSite = (
|
|
16
|
+
name: string,
|
|
17
|
+
parameters: readonly Parameter[],
|
|
18
|
+
worker: Worker,
|
|
19
|
+
inline: boolean,
|
|
20
|
+
) => {
|
|
21
|
+
const prefix = inline ? name : "site";
|
|
22
|
+
const field = (label: string) => `${prefix}_${label}`;
|
|
23
|
+
const member = (label: string) => `this.${field(label)}`;
|
|
24
|
+
const method = (label: string) => (inline ? `${name}_${label}` : label);
|
|
25
|
+
const invoke = (label: string, args = "") => `this.${method(label)}(${args})`;
|
|
26
|
+
const copy = (value: string, aggregate: boolean) => (aggregate ? `copy(${value})` : value);
|
|
27
|
+
const faults = `${member("faults")}=tsplcAsyncWrapAdd<UDINT>(${member("faults")},1);${member("lastError")}=7;`;
|
|
28
|
+
const signature = parameters.map((parameter) => `${parameter.name}:${parameter.type}`).join(",");
|
|
29
|
+
const args = parameters.map((parameter) => parameter.name).join(",");
|
|
30
|
+
const queued = parameters.map((parameter) => member(`queued_${parameter.name}`)).join(",");
|
|
31
|
+
const fields = `private ${field("active")}:BOOL=false;private ${field("fresh")}:BOOL=false;private ${field("cancelled")}:BOOL=false;private ${field("queued")}:BOOL=false;private ${field("faults")}:UDINT=0;private ${field("lastError")}:UDINT=0;
|
|
32
|
+
${parameters.map((parameter) => `private ${field(`queued_${parameter.name}`)}:${parameter.type}=defaultValue<${parameter.type}>();`).join("\n")}
|
|
33
|
+
${worker.declaration}`;
|
|
34
|
+
const body = `${fields}
|
|
35
|
+
public ${method("begin")}(${signature}):void{const accepted:BOOL=${worker.start}(${args});if(!accepted){${faults}return;}${member("active")}=true;${member("fresh")}=true;${member("cancelled")}=false;${worker.advance}({cancel:false});}
|
|
36
|
+
public ${method("start")}(${signature}):void{if(${member("active")}){if(${member("cancelled")}&&!${member("queued")}){${parameters.map((parameter) => `${member(`queued_${parameter.name}`)}=${copy(parameter.name, parameter.aggregate)};`).join("")}${member("queued")}=true;}else{${faults}}return;}${invoke("begin", args)};}
|
|
37
|
+
public ${method("cancel")}():void{${member("cancelled")}=true;${member("queued")}=false;}
|
|
38
|
+
public ${method("poll")}():void{
|
|
39
|
+
if(${member("faults")}>0&&tsplcPromiseDiagnostics.report(${member("lastError")},${member("faults")}))${member("faults")}=0;
|
|
40
|
+
if(!${member("active")})return;
|
|
41
|
+
if(${member("fresh")}&&!${member("cancelled")}){${member("fresh")}=false;return;}
|
|
42
|
+
${worker.advance}({cancel:${member("cancelled")}});const state=${worker.snapshot}();if(!state.done)return;
|
|
43
|
+
if(!${member("cancelled")}&&state.error!==0&&state.error!==1){${member("faults")}=tsplcAsyncWrapAdd<UDINT>(${member("faults")},1);${member("lastError")}=state.error;}
|
|
44
|
+
${worker.reset}();${member("active")}=false;
|
|
45
|
+
if(${member("queued")}){${member("queued")}=false;${invoke("begin", queued)};}
|
|
46
|
+
}`;
|
|
47
|
+
return { body, start: method("start"), cancel: method("cancel"), poll: method("poll") };
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** A named task: the owner reads its state and result instead of attaching a callback. */
|
|
51
|
+
export const taskSite = (
|
|
52
|
+
name: string,
|
|
53
|
+
parameters: readonly Parameter[],
|
|
54
|
+
worker: Worker,
|
|
55
|
+
inline: boolean,
|
|
56
|
+
value: { type: string; aggregate: boolean },
|
|
57
|
+
) => {
|
|
58
|
+
const method = (label: string) => (inline ? `${name}_${label}` : label);
|
|
59
|
+
const cancelled = `this.${inline ? `${name}_cancelled` : "cancelled"}`;
|
|
60
|
+
const signature = parameters.map((parameter) => `${parameter.name}:${parameter.type}`).join(",");
|
|
61
|
+
const args = parameters.map((parameter) => parameter.name).join(",");
|
|
62
|
+
const result = value.aggregate ? "copy(state.result)" : "state.result";
|
|
63
|
+
const body = `private ${cancelled.slice(5)}:BOOL=false;
|
|
64
|
+
${worker.declaration}
|
|
65
|
+
public ${method("start")}(${signature}):BOOL{const accepted:BOOL=${worker.start}(${args});if(accepted){${cancelled}=false;${worker.advance}({cancel:false});}return accepted;}
|
|
66
|
+
public ${method("cancel")}():BOOL{const state=${worker.snapshot}();if(!state.busy&&!state.done)return false;${cancelled}=true;return true;}
|
|
67
|
+
public ${method("snapshot")}():{valid:BOOL;busy:BOOL;done:BOOL;cancelRequested:BOOL;error:UDINT;result:${value.type}}{const state=${worker.snapshot}();if(${cancelled}&&state.done)return {valid:true,busy:false,done:true,cancelRequested:false,error:1,result:defaultValue<${value.type}>()};return {valid:true,busy:state.busy,done:state.done,cancelRequested:${cancelled},error:state.error,result:${result}};}
|
|
68
|
+
public ${method("reset")}():BOOL{const state=${worker.snapshot}();if(state.busy)return false;${worker.reset}();${cancelled}=false;return true;}
|
|
69
|
+
public ${method("poll")}():void{${worker.advance}({cancel:${cancelled}});}`;
|
|
70
|
+
return {
|
|
71
|
+
body,
|
|
72
|
+
poll: method("poll"),
|
|
73
|
+
renames: new Map(["start", "cancel", "snapshot", "reset"].map((m) => [m, method(m)])),
|
|
74
|
+
};
|
|
75
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs a program's setup code in Bun and prints the object graph it builds: every module binding,
|
|
3
|
+
* and every object, array, class instance and function reachable from them. The compiler runs
|
|
4
|
+
* this in a subprocess (graph.ts), so setup code never shares the compiler's process.
|
|
5
|
+
*
|
|
6
|
+
* Usage: bun capture.ts <request.json>
|
|
7
|
+
*/
|
|
8
|
+
import { plugin } from "bun";
|
|
9
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
10
|
+
import type { CaptureRequest, CapturedGraph, CapturedValue, CapturedNode } from "./graph.ts";
|
|
11
|
+
|
|
12
|
+
const request: CaptureRequest = JSON.parse(readFileSync(process.argv[2]!, "utf8"));
|
|
13
|
+
// Bun loads modules by their real path; the compiler may name them through a symlink.
|
|
14
|
+
const real = new Map(request.files.map((file) => [realpathSync(file), file]));
|
|
15
|
+
const runtime = new Set(real.keys());
|
|
16
|
+
const exportName = "__tsplcCapture";
|
|
17
|
+
|
|
18
|
+
/** Top-level bindings a module declares, so captured state can be traced back to its source. */
|
|
19
|
+
const bindings = (source: string, names: CaptureRequest["bindings"][string]) =>
|
|
20
|
+
`${source}\n;export const ${exportName} = { values: { ${names.values
|
|
21
|
+
.map((name) => `${JSON.stringify(name)}: () => ${name}`)
|
|
22
|
+
.join(", ")} }, classes: { ${names.classes
|
|
23
|
+
.map((name) => `${JSON.stringify(name)}: ${name}`)
|
|
24
|
+
.join(", ")} }, functions: { ${names.functions
|
|
25
|
+
.map((name) => `${JSON.stringify(name)}: ${name}`)
|
|
26
|
+
.join(", ")} } };\n`;
|
|
27
|
+
|
|
28
|
+
const servicePath = Symbol("tsplc service path");
|
|
29
|
+
/** A service client: any property is a deeper procedure path, and its identity names that path. */
|
|
30
|
+
const service = (path: readonly string[]): unknown =>
|
|
31
|
+
new Proxy(function serviceProcedure() {}, {
|
|
32
|
+
get: (_, key) => (key === servicePath ? path.join(".") : service([...path, String(key)])),
|
|
33
|
+
apply: () => {
|
|
34
|
+
throw new Error(`Setup code called the PLC service ${path.join(".")}.`);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
plugin({
|
|
39
|
+
name: "tsplc-capture",
|
|
40
|
+
setup(build) {
|
|
41
|
+
build.onLoad(
|
|
42
|
+
{
|
|
43
|
+
filter: new RegExp(
|
|
44
|
+
`^(${[...runtime].map((file) => file.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})$`,
|
|
45
|
+
),
|
|
46
|
+
},
|
|
47
|
+
({ path }) => ({
|
|
48
|
+
contents: bindings(readFileSync(path, "utf8"), request.bindings[real.get(path)!]!),
|
|
49
|
+
loader: "ts",
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
// Setup runs before the first scan, so the clock reads 0 there.
|
|
53
|
+
build.module("@tsplc/compiler/clock", () => ({
|
|
54
|
+
contents: "export const now = () => 0;",
|
|
55
|
+
loader: "js",
|
|
56
|
+
}));
|
|
57
|
+
build.module("@tsplc/compiler/async", () => ({
|
|
58
|
+
contents: `export const createServiceClient = () => globalThis.__tsplcService([]);
|
|
59
|
+
const unsupported = (name) => () => { throw new Error(name + " cannot run during PLC setup."); };
|
|
60
|
+
export const sleep = unsupported("sleep"), fetch = unsupported("fetch"), cancelAsync = unsupported("cancelAsync");`,
|
|
61
|
+
loader: "js",
|
|
62
|
+
}));
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
(globalThis as { __tsplcService?: unknown }).__tsplcService = service;
|
|
66
|
+
|
|
67
|
+
const modules = new Map<string, Record<string, Record<string, unknown>>>();
|
|
68
|
+
for (const file of [request.entry, ...request.files.filter((file) => file !== request.entry)]) {
|
|
69
|
+
const loaded = (await import(realpathSync(file))) as Record<string, unknown>;
|
|
70
|
+
modules.set(file, loaded[exportName] as Record<string, Record<string, unknown>>);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const classes = new Map<unknown, { file: string; name: string }>();
|
|
74
|
+
const functions = new Map<unknown, { file: string; name: string }>();
|
|
75
|
+
for (const [file, registry] of modules) {
|
|
76
|
+
for (const [name, value] of Object.entries(registry.classes!)) classes.set(value, { file, name });
|
|
77
|
+
for (const [name, value] of Object.entries(registry.functions!))
|
|
78
|
+
functions.set(value, { file, name });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const nodes: CapturedNode[] = [];
|
|
82
|
+
const ids = new Map<object, number>();
|
|
83
|
+
const capture = (value: unknown, where: string): CapturedValue => {
|
|
84
|
+
if (value === undefined) return { kind: "undefined" };
|
|
85
|
+
if (value === null) throw new Error(`${where}: null has no PLC value; use undefined.`);
|
|
86
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "string")
|
|
87
|
+
return { kind: typeof value, value } as CapturedValue;
|
|
88
|
+
if (typeof value === "bigint") return { kind: "bigint", value: String(value) };
|
|
89
|
+
if (typeof value !== "object" && typeof value !== "function")
|
|
90
|
+
throw new Error(`${where}: a ${typeof value} has no PLC value.`);
|
|
91
|
+
const known = ids.get(value);
|
|
92
|
+
if (known !== undefined) return { kind: "node", id: known };
|
|
93
|
+
const id = nodes.length;
|
|
94
|
+
ids.set(value, id);
|
|
95
|
+
nodes.push({ kind: "object", fields: {} });
|
|
96
|
+
if (typeof value === "function") {
|
|
97
|
+
const path = (value as { [servicePath]?: string })[servicePath];
|
|
98
|
+
const named = functions.get(value) ?? classes.get(value);
|
|
99
|
+
if (path !== undefined) nodes[id] = { kind: "service", path };
|
|
100
|
+
else if (named) nodes[id] = { kind: "function", ...named };
|
|
101
|
+
else
|
|
102
|
+
throw new Error(
|
|
103
|
+
`${where}: setup stored a function that is not a module-level function, so the PLC can't call it.`,
|
|
104
|
+
);
|
|
105
|
+
return { kind: "node", id };
|
|
106
|
+
}
|
|
107
|
+
if (Array.isArray(value)) {
|
|
108
|
+
nodes[id] = {
|
|
109
|
+
kind: "array",
|
|
110
|
+
items: value.map((item, index) => capture(item, `${where}[${index}]`)),
|
|
111
|
+
};
|
|
112
|
+
return { kind: "node", id };
|
|
113
|
+
}
|
|
114
|
+
const prototype = Object.getPrototypeOf(value);
|
|
115
|
+
const owner =
|
|
116
|
+
prototype === Object.prototype || prototype === null ? undefined : prototype.constructor;
|
|
117
|
+
const registered = owner && classes.get(owner);
|
|
118
|
+
if (owner && !registered)
|
|
119
|
+
throw new Error(`${where}: a ${owner.name} has no PLC layout; use a class from the program.`);
|
|
120
|
+
const fields: Record<string, CapturedValue> = {};
|
|
121
|
+
for (const [key, field] of Object.entries(value)) fields[key] = capture(field, `${where}.${key}`);
|
|
122
|
+
nodes[id] = registered ? { kind: "instance", ...registered, fields } : { kind: "object", fields };
|
|
123
|
+
return { kind: "node", id };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const graph: CapturedGraph = { nodes, bindings: [] };
|
|
127
|
+
for (const [file, registry] of modules)
|
|
128
|
+
for (const [name, read] of Object.entries(registry.values!)) {
|
|
129
|
+
let value: unknown;
|
|
130
|
+
try {
|
|
131
|
+
value = (read as () => unknown)();
|
|
132
|
+
} catch {
|
|
133
|
+
continue; // A binding the module never initialized.
|
|
134
|
+
}
|
|
135
|
+
if (typeof value === "function" && (functions.has(value) || classes.has(value))) continue;
|
|
136
|
+
graph.bindings.push({ file, name, value: capture(value, name) });
|
|
137
|
+
}
|
|
138
|
+
process.stdout.write(JSON.stringify(graph));
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
/** What capture.ts needs: the program's runtime files and the bindings each declares. */
|
|
9
|
+
export interface CaptureRequest {
|
|
10
|
+
entry: string;
|
|
11
|
+
files: string[];
|
|
12
|
+
bindings: Record<string, { values: string[]; classes: string[]; functions: string[] }>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type CapturedValue =
|
|
16
|
+
| { kind: "undefined" }
|
|
17
|
+
| { kind: "number"; value: number }
|
|
18
|
+
| { kind: "boolean"; value: boolean }
|
|
19
|
+
| { kind: "string"; value: string }
|
|
20
|
+
| { kind: "bigint"; value: string }
|
|
21
|
+
| { kind: "node"; id: number };
|
|
22
|
+
|
|
23
|
+
export type CapturedNode =
|
|
24
|
+
| { kind: "object"; fields: Record<string, CapturedValue> }
|
|
25
|
+
| { kind: "instance"; file: string; name: string; fields: Record<string, CapturedValue> }
|
|
26
|
+
| { kind: "array"; items: CapturedValue[] }
|
|
27
|
+
| { kind: "function"; file: string; name: string }
|
|
28
|
+
| { kind: "service"; path: string };
|
|
29
|
+
|
|
30
|
+
export interface CapturedGraph {
|
|
31
|
+
nodes: CapturedNode[];
|
|
32
|
+
/** Every module-level value binding, in module evaluation order. */
|
|
33
|
+
bindings: { file: string; name: string; value: CapturedValue }[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const names = (source: ts.SourceFile) => {
|
|
37
|
+
const found: CaptureRequest["bindings"][string] = { values: [], classes: [], functions: [] };
|
|
38
|
+
const bind = (name: ts.BindingName): void => {
|
|
39
|
+
if (ts.isIdentifier(name)) found.values.push(name.text);
|
|
40
|
+
else
|
|
41
|
+
for (const element of name.elements) if (!ts.isOmittedExpression(element)) bind(element.name);
|
|
42
|
+
};
|
|
43
|
+
for (const statement of source.statements) {
|
|
44
|
+
if (ts.isVariableStatement(statement))
|
|
45
|
+
for (const declaration of statement.declarationList.declarations) bind(declaration.name);
|
|
46
|
+
else if (ts.isClassDeclaration(statement) && statement.name)
|
|
47
|
+
found.classes.push(statement.name.text);
|
|
48
|
+
else if (ts.isFunctionDeclaration(statement) && statement.name && statement.body)
|
|
49
|
+
found.functions.push(statement.name.text);
|
|
50
|
+
}
|
|
51
|
+
// Arrow functions bound to consts are functions too: a stored callback names its binding.
|
|
52
|
+
for (const statement of source.statements)
|
|
53
|
+
if (ts.isVariableStatement(statement))
|
|
54
|
+
for (const declaration of statement.declarationList.declarations)
|
|
55
|
+
if (
|
|
56
|
+
ts.isIdentifier(declaration.name) &&
|
|
57
|
+
declaration.initializer &&
|
|
58
|
+
(ts.isArrowFunction(declaration.initializer) ||
|
|
59
|
+
ts.isFunctionExpression(declaration.initializer))
|
|
60
|
+
) {
|
|
61
|
+
found.values = found.values.filter((name) => name !== declaration.name.getText());
|
|
62
|
+
found.functions.push(declaration.name.text);
|
|
63
|
+
}
|
|
64
|
+
return found;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Runs the program's setup in a Bun subprocess and returns the object graph it built. */
|
|
68
|
+
export const captureGraph = (entry: string, sources: readonly ts.SourceFile[]): CapturedGraph => {
|
|
69
|
+
const files = sources.filter((source) => !source.isDeclarationFile);
|
|
70
|
+
const request: CaptureRequest = {
|
|
71
|
+
entry,
|
|
72
|
+
files: files.map((source) => source.fileName),
|
|
73
|
+
bindings: Object.fromEntries(files.map((source) => [source.fileName, names(source)])),
|
|
74
|
+
};
|
|
75
|
+
const directory = mkdtempSync(join(tmpdir(), "tsplc-capture-"));
|
|
76
|
+
try {
|
|
77
|
+
const file = join(directory, "request.json");
|
|
78
|
+
writeFileSync(file, JSON.stringify(request));
|
|
79
|
+
// The source tree runs capture.ts; a built package ships it beside the bundle.
|
|
80
|
+
const script = ["./capture.ts", "./elaborate-capture.js"]
|
|
81
|
+
.map((path) => fileURLToPath(new URL(path, import.meta.url)))
|
|
82
|
+
.find((path) => existsSync(path));
|
|
83
|
+
if (!script) throw new Error("The elaboration capture script is missing from this build.");
|
|
84
|
+
const result = spawnSync(process.execPath, [script, file], {
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
87
|
+
});
|
|
88
|
+
if (result.status !== 0)
|
|
89
|
+
throw new Error(`Running the program's setup failed:\n${result.stderr.trim()}`);
|
|
90
|
+
return JSON.parse(result.stdout) as CapturedGraph;
|
|
91
|
+
} finally {
|
|
92
|
+
rmSync(directory, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
};
|