@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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +16 -2
  3. package/backends/codesys/config.ts +54 -0
  4. package/backends/codesys/emit.ts +1063 -0
  5. package/backends/codesys/engineering.ts +88 -0
  6. package/backends/codesys/http.ts +598 -0
  7. package/backends/codesys/index.ts +12 -0
  8. package/backends/codesys/io-paths.ts +103 -0
  9. package/backends/codesys/names.ts +76 -0
  10. package/backends/codesys/numeric.ts +356 -0
  11. package/backends/codesys/plan.ts +512 -0
  12. package/backends/codesys/publication.ts +59 -0
  13. package/backends/codesys/storage.ts +63 -0
  14. package/backends/codesys/task-access.ts +118 -0
  15. package/backends/codesys/types.ts +178 -0
  16. package/backends/codesys/vendor.ts +108 -0
  17. package/dist/cli.js +740 -0
  18. package/dist/cli.js.map +13 -0
  19. package/dist/clock.js +8 -0
  20. package/dist/clock.js.map +10 -0
  21. package/dist/codesys.js +2587 -0
  22. package/dist/codesys.js.map +22 -0
  23. package/dist/elaborate-capture.js +118 -0
  24. package/dist/elaborate-capture.js.map +10 -0
  25. package/dist/index.js +6859 -0
  26. package/dist/index.js.map +25 -0
  27. package/dist/ir.js +1888 -0
  28. package/dist/ir.js.map +22 -0
  29. package/dist/math.js +24 -0
  30. package/dist/math.js.map +10 -0
  31. package/dist/project.js +423 -0
  32. package/dist/project.js.map +11 -0
  33. package/ir/aggregates.ts +333 -0
  34. package/ir/allocation.ts +74 -0
  35. package/ir/core-model.ts +116 -0
  36. package/ir/core.ts +63 -0
  37. package/ir/declarations.ts +201 -0
  38. package/ir/diagnostics.ts +43 -0
  39. package/ir/domains.ts +96 -0
  40. package/ir/index.ts +7 -0
  41. package/ir/link.ts +176 -0
  42. package/ir/model.ts +268 -0
  43. package/ir/normalization-error.ts +10 -0
  44. package/ir/routine.ts +826 -0
  45. package/ir/scalars.ts +675 -0
  46. package/ir/types.ts +70 -0
  47. package/package.json +105 -4
  48. package/project/artifacts.ts +277 -0
  49. package/project/cli.ts +282 -0
  50. package/project/index.ts +1 -0
  51. package/project/project.ts +298 -0
  52. package/project/server.ts +83 -0
  53. package/src/frontend/async.ts +1060 -0
  54. package/src/frontend/blocks.ts +419 -0
  55. package/src/frontend/configuration.ts +167 -0
  56. package/src/frontend/const-functions.ts +153 -0
  57. package/src/frontend/cooperative.ts +75 -0
  58. package/src/frontend/elaborate/capture.ts +138 -0
  59. package/src/frontend/elaborate/graph.ts +94 -0
  60. package/src/frontend/elaborate/index.ts +302 -0
  61. package/src/frontend/elaborate/layouts.ts +390 -0
  62. package/src/frontend/elaborate/model.ts +177 -0
  63. package/src/frontend/elaborate/specialize.ts +2280 -0
  64. package/src/frontend/exposure.ts +147 -0
  65. package/src/frontend/module-graph.ts +63 -0
  66. package/src/frontend/promises.ts +751 -0
  67. package/src/frontend/services.ts +435 -0
  68. package/src/frontend/types.ts +578 -0
  69. package/src/frontend/typescript.ts +2442 -0
  70. package/src/index.ts +7 -0
  71. package/stdlib/clock.ts +2 -0
  72. package/stdlib/math.ts +19 -0
  73. package/types/async.d.ts +81 -0
  74. package/types/clock.d.ts +4 -0
  75. package/types/config.d.ts +2 -0
  76. package/types/data-types.d.ts +31 -0
  77. package/types/globals.d.ts +40 -0
  78. package/types/index.d.ts +111 -0
  79. package/types/plc.d.ts +5 -0
@@ -0,0 +1,118 @@
1
+ import { instancedBlock } from "@tsplc/compiler/ir";
2
+ import type {
3
+ CoreExpression,
4
+ CoreFunction,
5
+ CorePath,
6
+ CoreStatement,
7
+ VerifiedCoreModule,
8
+ } from "@tsplc/compiler/ir";
9
+
10
+ // Follow both helper calls and instance updates. Borrow addresses count as storage
11
+ // access, so scheduling cannot accidentally give one mutable record two owners.
12
+ export function accessedStorage(
13
+ fn: CoreFunction,
14
+ core: VerifiedCoreModule,
15
+ scheduledHost?: string,
16
+ ): Set<string> {
17
+ // Only the generated entry's worker call may borrow its host. Ordinary calls
18
+ // reached through the async body still count as access to scan-owned storage.
19
+ const scheduledCall = fn.body.find(
20
+ (item) => item.kind === "invoke" && item.instance === scheduledHost,
21
+ );
22
+ const result = new Set<string>(),
23
+ seen = new Set<string>();
24
+ const functions = new Map(core.functions.map((item) => [item.id, item]));
25
+ const blocks = new Map(core.blocks.map((item) => [item.id, item]));
26
+ const types = new Map(core.types.map((item) => [item.id, item]));
27
+ const variables = new Map(
28
+ [
29
+ ...core.variables,
30
+ ...core.functions.flatMap((item) => [...item.parameters, ...item.locals]),
31
+ ...core.blocks.flatMap((item) => [
32
+ ...item.inputs,
33
+ ...item.outputs,
34
+ ...item.state,
35
+ ...item.locals,
36
+ ]),
37
+ ].map((item) => [item.id, item]),
38
+ );
39
+
40
+ const path = (segments: CorePath = []): void => {
41
+ segments.forEach((segment) => {
42
+ if (segment.kind === "index") expression(segment.value);
43
+ });
44
+ };
45
+
46
+ const expression = (value: CoreExpression): void => {
47
+ if (value.kind === "load") {
48
+ result.add(value.root);
49
+ path(value.path);
50
+ }
51
+ if (value.kind === "borrow") {
52
+ result.add(value.place.root);
53
+ path(value.place.path);
54
+ }
55
+ if (value.kind === "unary" || value.kind === "convert") expression(value.operand);
56
+ if (value.kind === "binary") {
57
+ expression(value.left);
58
+ expression(value.right);
59
+ }
60
+ };
61
+
62
+ const body = (items: readonly CoreStatement[]): void => {
63
+ for (const item of items) {
64
+ if (item.kind === "assign") {
65
+ result.add(item.root);
66
+ path(item.path);
67
+ expression(item.value);
68
+ }
69
+ if (item.kind === "if") {
70
+ expression(item.condition);
71
+ body(item.then);
72
+ body(item.else);
73
+ }
74
+ if (item.kind === "switch") {
75
+ expression(item.value);
76
+ for (const branch of item.cases) body(branch.body);
77
+ body(item.fallback);
78
+ }
79
+ if (item.kind === "for") {
80
+ if (item.dynamic) {
81
+ expression(item.dynamic.start);
82
+ expression(item.dynamic.limit);
83
+ }
84
+ result.add(item.counter);
85
+ body(item.body);
86
+ }
87
+ if (item.kind === "return" && item.value) expression(item.value);
88
+ if (item.kind === "invoke") {
89
+ if (item.instance && item !== scheduledCall) result.add(item.instance);
90
+ if (item.index) expression(item.index);
91
+ item.arguments.forEach(expression);
92
+ if (item.destination) result.add(item.destination);
93
+ const called = functions.get(item.function);
94
+ if (called && !seen.has(called.id)) {
95
+ seen.add(called.id);
96
+ body(called.body);
97
+ }
98
+ }
99
+ if (item.kind === "blockUpdate") {
100
+ result.add(item.instance);
101
+ if (item.index) expression(item.index);
102
+ Object.values(item.inputs).forEach(expression);
103
+ const variable = variables.get(item.instance);
104
+ if (!variable) throw new Error("Missing verified instance during task planning.");
105
+ const type = instancedBlock(variable.type, types);
106
+ const block = type && blocks.get(type.block);
107
+ if (block && !seen.has(block.id)) {
108
+ seen.add(block.id);
109
+ body(block.body);
110
+ }
111
+ }
112
+ }
113
+ };
114
+
115
+ seen.add(fn.id);
116
+ body(fn.body);
117
+ return result;
118
+ }
@@ -0,0 +1,178 @@
1
+ import { instancedBlock } from "@tsplc/compiler/ir";
2
+ import type { Constant, GeneratedObject, PlcType } from "@tsplc/compiler/ir";
3
+ import { Names } from "./names.ts";
4
+ import { scalarLiteral } from "./numeric.ts";
5
+
6
+ export class CodesysTypes {
7
+ readonly objects: GeneratedObject[] = [];
8
+ private readonly names = new Map<string, string>();
9
+ private readonly fields = new Map<string, Map<string, string>>();
10
+
11
+ constructor(
12
+ private readonly types: ReadonlyMap<string, PlcType>,
13
+ names: Names,
14
+ blockNames: ReadonlyMap<string, string>,
15
+ ) {
16
+ // Register every type and member name before following layout dependencies.
17
+ for (const type of types.values()) {
18
+ if (type.kind === "block") {
19
+ const name = blockNames.get(type.block);
20
+ if (!name) throw new Error("Missing verified block name");
21
+ this.names.set(type.id, name);
22
+ }
23
+ if (["record", "array", "enum", "string"].includes(type.kind)) {
24
+ const label = type.id.split(":").at(-1)!;
25
+ if (type.kind === "record" || type.kind === "enum") {
26
+ const prefix = type.kind === "record" ? "ST" : "E";
27
+ this.names.set(type.id, names.allocateObject(prefix, label));
28
+ } else this.names.set(type.id, names.allocate(`T_${label}`));
29
+ }
30
+ if (type.kind === "record" || type.kind === "enum") {
31
+ const names = new Names(),
32
+ fields = new Map<string, string>();
33
+ for (const member of type.kind === "record" ? type.fields : type.members) {
34
+ fields.set(member.name, names.allocate(member.name, true));
35
+ }
36
+ this.fields.set(type.id, fields);
37
+ }
38
+ }
39
+
40
+ // A block array is declared in place: a function block cannot be a data type's element.
41
+ for (const type of types.values())
42
+ if (type.kind === "array" && instancedBlock(type.id, types))
43
+ this.names.set(
44
+ type.id,
45
+ `ARRAY[${type.lowerBound}..${type.lowerBound + type.length - 1}] OF ${this.name(type.element)}`,
46
+ );
47
+
48
+ // Verified layouts are acyclic. Emit child DUTs first, then their containing DUT.
49
+ const emitted = new Set<string>();
50
+ const emit = (type: PlcType): void => {
51
+ if (instancedBlock(type.id, types) || !this.names.has(type.id) || emitted.has(type.id))
52
+ return;
53
+ if (type.kind === "array") emit(this.type(type.element));
54
+ if (type.kind === "record") {
55
+ for (const field of type.fields) emit(this.type(field.type));
56
+ }
57
+ emitted.add(type.id);
58
+ const name = this.name(type.id);
59
+ let declaration: string;
60
+ if (type.kind === "record") {
61
+ const fields = type.fields
62
+ .map((field) => ` ${this.field(type.id, field.name)} : ${this.name(field.type)};`)
63
+ .join("\n");
64
+ declaration = `TYPE ${name} :
65
+ STRUCT
66
+ ${fields}
67
+ END_STRUCT
68
+ END_TYPE
69
+ `;
70
+ } else if (type.kind === "array") {
71
+ declaration = `TYPE ${name} : ARRAY[${type.lowerBound}..${type.lowerBound + type.length - 1}] OF ${this.name(type.element)};
72
+ END_TYPE
73
+ `;
74
+ } else if (type.kind === "string") {
75
+ declaration = `TYPE ${name} : ${type.encoding === "utf8" ? "STRING" : "WSTRING"}(${type.capacity});
76
+ END_TYPE
77
+ `;
78
+ } else if (type.kind === "enum") {
79
+ const members = type.members
80
+ .map((member) => ` ${this.field(type.id, member.name)} := ${member.value}`)
81
+ .join(",\n");
82
+ declaration = `{attribute 'qualified_only'}
83
+ {attribute 'strict'}
84
+ TYPE ${name} : (
85
+ ${members}
86
+ ) DINT;
87
+ END_TYPE
88
+ `;
89
+ } else throw new Error("Unsupported verified data type");
90
+ this.objects.push({ id: type.id, kind: "dataType", name, declaration });
91
+ };
92
+ for (const type of types.values()) emit(type);
93
+ }
94
+
95
+ private type(id: string): PlcType {
96
+ const type = this.types.get(id);
97
+ if (!type) throw new Error("Missing verified type");
98
+ return type;
99
+ }
100
+
101
+ name(id: string): string {
102
+ return this.names.get(id) ?? id;
103
+ }
104
+
105
+ field(id: string, name: string): string {
106
+ const field = this.fields.get(id)?.get(name);
107
+ if (!field) throw new Error("Missing verified field");
108
+ return field;
109
+ }
110
+
111
+ declarationInitializer(value: Constant, id: string): string {
112
+ const type = this.type(id);
113
+ // Our DUT fields have no custom initializers. CODESYS initializes their
114
+ // numbers/BOOL/strings to zero/false/empty, including nested arrays/records.
115
+ // Keep explicit nonzero enum members and signed floating zero: omission must
116
+ // not change the initial value selected by PLC semantics.
117
+ if ((type.kind === "record" || type.kind === "array") && this.isZero(value)) return "";
118
+ return ` := ${this.literal(value, id)}`;
119
+ }
120
+
121
+ private isZero(value: Constant): boolean {
122
+ if (value.kind === "bool") return !value.value;
123
+ if (value.kind === "integer") return value.value === "0";
124
+ if (value.kind === "float") return Object.is(value.value, 0);
125
+ if (value.kind === "string") return value.value === "";
126
+ if (value.kind === "array") return value.items.every((item) => this.isZero(item));
127
+ if (value.kind === "record")
128
+ return Object.values(value.fields).every((item) => this.isZero(item));
129
+ // Time/calendar values remain explicit without relying on target defaults.
130
+ return false;
131
+ }
132
+
133
+ literal(value: Constant, id: string): string {
134
+ const type = this.type(id);
135
+ if (type.kind === "enum" && value.kind === "integer") {
136
+ const member = type.members.find((member) => member.value === value.value);
137
+ if (!member) throw new Error("Missing verified enum member");
138
+ return `${this.name(id)}.${this.field(id, member.name)}`;
139
+ }
140
+ if (type.kind === "string" && value.kind === "string") {
141
+ const wide = type.encoding === "utf16",
142
+ quote = wide ? '"' : "'";
143
+ let text = "";
144
+ for (const char of value.value) {
145
+ const code = char.charCodeAt(0);
146
+ if (char === "$") text += "$$";
147
+ else if (char === quote) text += `$${char}`;
148
+ else if (code < 32 || code === 127)
149
+ text += `$${code
150
+ .toString(16)
151
+ .toUpperCase()
152
+ .padStart(wide ? 4 : 2, "0")}`;
153
+ else text += char;
154
+ }
155
+ return `${quote}${text}${quote}`;
156
+ }
157
+ if (type.kind === "array" && value.kind === "array") {
158
+ return `[${value.items.map((item) => this.literal(item, type.element)).join(", ")}]`;
159
+ }
160
+ if (type.kind === "record" && value.kind === "record") {
161
+ const fields = type.fields.map((field) => {
162
+ const initial = value.fields[field.name];
163
+ if (!initial) throw new Error("Missing verified record initializer");
164
+ return `${this.field(id, field.name)} := ${this.literal(initial, field.type)}`;
165
+ });
166
+ return `(${fields.join(", ")})`;
167
+ }
168
+ if (type.kind === "duration" && value.kind === "duration") {
169
+ return `T#${BigInt(value.nanoseconds) / 1_000_000n}ms`;
170
+ }
171
+ if (type.kind === "calendar" && value.kind === "calendar") {
172
+ if (type.domain === "date") return `D#${value.value}`;
173
+ if (type.domain === "timeOfDay") return `TOD#${value.value}`;
174
+ return `DT#${value.value.replace("T", "-")}`;
175
+ }
176
+ return scalarLiteral(value, id);
177
+ }
178
+ }
@@ -0,0 +1,108 @@
1
+ import type { Diagnostic, EmitResult } from "@tsplc/compiler/ir";
2
+ import { CodesysCompilerError } from "@tsplc/build";
3
+ import type { SourceMapping } from "./emit.ts";
4
+
5
+ type MappedBundle = Pick<EmitResult, "objects"> & { mappings: readonly SourceMapping[] };
6
+
7
+ /** Map verified vendor locations; preserve raw context for everything else. */
8
+ export function diagnoseCodesys(cause: unknown, bundle: MappedBundle): readonly Diagnostic[] {
9
+ const notes: string[] = [];
10
+ let current = cause;
11
+ while (current instanceof Error) {
12
+ if (current instanceof CodesysCompilerError && current.diagnostics.length) {
13
+ return current.diagnostics.map((item) => {
14
+ // Observed in CODESYS 3.5 SP22. position itself is an opaque vendor value.
15
+ const location =
16
+ item.positionText &&
17
+ /^Line ([1-9][0-9]*)(?:, Column ([1-9][0-9]*))? \((Decl|Impl)\)$/.exec(item.positionText);
18
+ const identity = item.object;
19
+ let objectPath: string | undefined;
20
+ if (identity && identity.path.at(-1) === identity.name) {
21
+ const relative = identity.applicationPath;
22
+ if (
23
+ Array.isArray(relative) &&
24
+ relative.length > 0 &&
25
+ relative.every((part) => typeof part === "string") &&
26
+ identity.isApplicationChild === (relative.length === 1) &&
27
+ identity.path.length > relative.length &&
28
+ identity.path.slice(-relative.length).every((part, index) => part === relative[index])
29
+ ) {
30
+ objectPath = relative.join("/");
31
+ } else if (
32
+ relative === undefined &&
33
+ identity.isApplicationChild === true &&
34
+ identity.path.length >= 2
35
+ ) {
36
+ // A verified immediate Application parent proves the root identity.
37
+ objectPath = identity.name;
38
+ }
39
+ }
40
+ const objects = objectPath
41
+ ? bundle.objects.flatMap((object) => {
42
+ const declaration =
43
+ object.sourcePath ??
44
+ (object.implementation === undefined
45
+ ? `${object.name}.st`
46
+ : `${object.name}/declaration.st`);
47
+ const nativePath = (
48
+ object.kind === "method"
49
+ ? declaration.replace(/\/methods\/([^/]+\/declaration\.st)$/, "/$1")
50
+ : declaration
51
+ ).replace(object.implementation === undefined ? /\.st$/ : /\/declaration\.st$/, "");
52
+ return object.name === identity?.name && nativePath === objectPath
53
+ ? [{ object, declaration }]
54
+ : [];
55
+ })
56
+ : [];
57
+ const match = objects.length === 1 ? objects[0] : undefined;
58
+ const section = location?.[3] === "Decl" ? "declaration" : "implementation";
59
+ const path =
60
+ section === "declaration"
61
+ ? match?.declaration
62
+ : match?.object.implementation !== undefined
63
+ ? match.declaration.replace(/declaration\.st$/, "implementation.st")
64
+ : undefined;
65
+ const mappings =
66
+ location && path
67
+ ? bundle.mappings.filter(
68
+ (mapping) => mapping.path === path && mapping.line === Number(location[1]),
69
+ )
70
+ : [];
71
+ const mapping = mappings.length === 1 ? mappings[0] : undefined;
72
+ return {
73
+ origin: "vendor",
74
+ code: "PLC8001",
75
+ severity: "error",
76
+ message: item.text,
77
+ ...(mapping && { span: mapping.span }),
78
+ notes: [
79
+ ...notes,
80
+ ...(location && path
81
+ ? [
82
+ "Generated ST: " +
83
+ path +
84
+ ":" +
85
+ location[1] +
86
+ (location[2] ? `:${location[2]}` : ""),
87
+ ]
88
+ : []),
89
+ `CODESYS diagnostic: ${JSON.stringify(item)}`,
90
+ ],
91
+ };
92
+ });
93
+ }
94
+ notes.push(current.message);
95
+ if (current.cause === undefined) break;
96
+ current = current.cause;
97
+ if (!(current instanceof Error)) notes.push(String(current));
98
+ }
99
+ return [
100
+ {
101
+ origin: "vendor",
102
+ code: "PLC8001",
103
+ severity: "error",
104
+ message: cause instanceof Error ? cause.message : String(cause),
105
+ ...(notes.length > 1 && { notes: notes.slice(1) }),
106
+ },
107
+ ];
108
+ }