@tsplc/compiler 0.0.0-stage → 0.2.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 +1396 -0
- package/dist/cli.js.map +16 -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 +429 -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 +106 -4
- package/project/artifacts.ts +277 -0
- package/project/cli.ts +305 -0
- package/project/index.ts +1 -0
- package/project/plant.ts +437 -0
- package/project/project.ts +312 -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,12 @@
|
|
|
1
|
+
export { type CodesysOptions, codesys, direct } from "./config.ts";
|
|
2
|
+
export { emitCodesys, type SourceMapping } from "./emit.ts";
|
|
3
|
+
export {
|
|
4
|
+
buildCodesys,
|
|
5
|
+
codesysBuildConfig,
|
|
6
|
+
type CodesysEngineeringBuild,
|
|
7
|
+
engineeringBuildSchema,
|
|
8
|
+
} from "./engineering.ts";
|
|
9
|
+
export { type CodesysPlan, planCodesys } from "./plan.ts";
|
|
10
|
+
export { diagnoseCodesys } from "./vendor.ts";
|
|
11
|
+
export { ioTargets } from "./io-paths.ts";
|
|
12
|
+
export { publicationSelectors } from "./publication.ts";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { PlcType, Variable, VerifiedCoreModule } from "@tsplc/compiler/ir";
|
|
2
|
+
import { type IoTarget, ioLayout } from "@tsplc/typegen/hardware";
|
|
3
|
+
import { moduleName } from "./storage.ts";
|
|
4
|
+
|
|
5
|
+
type StorageSegment =
|
|
6
|
+
{ kind: "field"; record: string; name: string } | { kind: "index"; value: number };
|
|
7
|
+
|
|
8
|
+
/** An `io` key that names a field of authored module storage, e.g. `PlantIO.Motor.Output.Run`. */
|
|
9
|
+
export interface StorageBinding {
|
|
10
|
+
key: string;
|
|
11
|
+
root: Variable;
|
|
12
|
+
segments: readonly StorageSegment[];
|
|
13
|
+
type: PlcType;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const pathPattern = /^([\w$-]+)\.([A-Za-z_$][\w$]*)((?:\.[A-Za-z_$][\w$]*|\[-?\d+\])*)$/;
|
|
17
|
+
|
|
18
|
+
export const isStoragePath = (key: string): boolean => key.includes(".") || key.includes("[");
|
|
19
|
+
|
|
20
|
+
/** Resolve a storage path against verified types; the error explains the first unresolvable segment. */
|
|
21
|
+
export function resolveStoragePath(
|
|
22
|
+
core: VerifiedCoreModule,
|
|
23
|
+
key: string,
|
|
24
|
+
): StorageBinding | { error: string } {
|
|
25
|
+
const match = pathPattern.exec(key);
|
|
26
|
+
if (!match) return { error: `${key}: expected Module.variable followed by .field or [index].` };
|
|
27
|
+
const [, module, name, rest] = match;
|
|
28
|
+
const roots = core.variables.filter(
|
|
29
|
+
(item) => item.storage === "module" && item.name === name && moduleName(item) === module,
|
|
30
|
+
);
|
|
31
|
+
if (roots.length !== 1)
|
|
32
|
+
return {
|
|
33
|
+
error: roots.length
|
|
34
|
+
? `${key}: ${module}.${name} is declared by more than one module named ${module}.`
|
|
35
|
+
: `${key}: no mutable module variable ${module}.${name}.`,
|
|
36
|
+
};
|
|
37
|
+
const root = roots[0]!;
|
|
38
|
+
const types = new Map(core.types.map((type) => [type.id, type]));
|
|
39
|
+
let type = types.get(root.type)!;
|
|
40
|
+
const segments: StorageSegment[] = [];
|
|
41
|
+
for (const [, field, index] of rest!.matchAll(/\.([A-Za-z_$][\w$]*)|\[(-?\d+)\]/g)) {
|
|
42
|
+
if (field !== undefined) {
|
|
43
|
+
const member = type.kind === "record" ? type.fields.find((f) => f.name === field) : undefined;
|
|
44
|
+
if (!member) return { error: `${key}: no field ${field}.` };
|
|
45
|
+
segments.push({ kind: "field", record: type.id, name: field });
|
|
46
|
+
type = types.get(member.type)!;
|
|
47
|
+
} else {
|
|
48
|
+
const value = Number(index);
|
|
49
|
+
if (type.kind !== "array") return { error: `${key}: ${index} indexes a non-array value.` };
|
|
50
|
+
if (value < type.lowerBound || value >= type.lowerBound + type.length)
|
|
51
|
+
return {
|
|
52
|
+
error: `${key}: index ${value} is outside ${type.lowerBound}..${type.lowerBound + type.length - 1}.`,
|
|
53
|
+
};
|
|
54
|
+
segments.push({ kind: "index", value });
|
|
55
|
+
type = types.get(type.element)!;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { key, root, segments, type };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** IEC spelling of a bindable type, as typegen spells channels; undefined for records, enums, strings. */
|
|
62
|
+
export function iecSpelling(
|
|
63
|
+
type: PlcType,
|
|
64
|
+
types: ReadonlyMap<string, PlcType>,
|
|
65
|
+
): string | undefined {
|
|
66
|
+
if (
|
|
67
|
+
type.kind === "bool" ||
|
|
68
|
+
type.kind === "integer" ||
|
|
69
|
+
type.kind === "bits" ||
|
|
70
|
+
type.kind === "float"
|
|
71
|
+
)
|
|
72
|
+
return type.id;
|
|
73
|
+
if (type.kind !== "array") return undefined;
|
|
74
|
+
const element = iecSpelling(types.get(type.element)!, types);
|
|
75
|
+
const upper = type.lowerBound + type.length - 1;
|
|
76
|
+
return element && !element.startsWith("ARRAY")
|
|
77
|
+
? `ARRAY[${type.lowerBound}..${upper}] OF ${element}`
|
|
78
|
+
: undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Every key an `io` mapping may use: logical IO names and paths into mutable module storage. */
|
|
82
|
+
export function ioTargets(core: VerifiedCoreModule): IoTarget[] {
|
|
83
|
+
const types = new Map(core.types.map((type) => [type.id, type]));
|
|
84
|
+
const targets: IoTarget[] = [];
|
|
85
|
+
const add = (key: string, type: PlcType) => {
|
|
86
|
+
const layout = ioLayout(iecSpelling(type, types) ?? "");
|
|
87
|
+
if (layout) targets.push({ key, layout });
|
|
88
|
+
};
|
|
89
|
+
const visit = (key: string, type: PlcType): void => {
|
|
90
|
+
add(key, type);
|
|
91
|
+
if (type.kind === "record")
|
|
92
|
+
for (const field of type.fields) visit(`${key}.${field.name}`, types.get(field.type)!);
|
|
93
|
+
// Typegen writes keys as template literal types, so any index matches here.
|
|
94
|
+
if (type.kind === "array") visit(`${key}[` + "${number}" + "]", types.get(type.element)!);
|
|
95
|
+
};
|
|
96
|
+
for (const variable of core.variables) {
|
|
97
|
+
if (variable.storage === "input" || variable.storage === "output")
|
|
98
|
+
add(variable.name, types.get(variable.type)!);
|
|
99
|
+
else if (variable.storage === "module")
|
|
100
|
+
visit(`${moduleName(variable)}.${variable.name}`, types.get(variable.type)!);
|
|
101
|
+
}
|
|
102
|
+
return targets;
|
|
103
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// CODESYS declaration, statement, operator and elementary-type keywords.
|
|
2
|
+
// https://content.helpme-codesys.com/en/CODESYS%20Development%20System/_cds_keywords.html
|
|
3
|
+
const keywords = new Set(
|
|
4
|
+
`
|
|
5
|
+
ACTION END_ACTION PROGRAM END_PROGRAM FUNCTION END_FUNCTION FUNCTION_BLOCK END_FUNCTION_BLOCK
|
|
6
|
+
INTERFACE END_INTERFACE METHOD END_METHOD PROPERTY END_PROPERTY
|
|
7
|
+
TYPE END_TYPE STRUCT END_STRUCT UNION END_UNION ARRAY POINTER REFERENCE
|
|
8
|
+
VAR END_VAR VAR_INPUT VAR_OUTPUT VAR_IN_OUT VAR_TEMP VAR_GLOBAL VAR_EXTERNAL
|
|
9
|
+
VAR_ACCESS VAR_CONFIG VAR_STAT VAR_INST VAR_GENERIC
|
|
10
|
+
CONSTANT RETAIN NON_RETAIN PERSISTENT AT READ_ONLY READ_WRITE PARAMS
|
|
11
|
+
CONFIGURATION END_CONFIGURATION RESOURCE END_RESOURCE TASK WITH SINGLE INTERVAL PRIORITY
|
|
12
|
+
PRIVATE PUBLIC PROTECTED INTERNAL ABSTRACT FINAL OVERRIDE EXTENDS IMPLEMENTS THIS SUPER
|
|
13
|
+
IF THEN ELSIF ELSE END_IF CASE OF END_CASE FOR TO BY DO END_FOR
|
|
14
|
+
WHILE END_WHILE REPEAT UNTIL END_REPEAT RETURN EXIT CONTINUE JMP
|
|
15
|
+
TRUE FALSE BOOL BYTE WORD DWORD LWORD SINT USINT INT UINT DINT UDINT LINT ULINT
|
|
16
|
+
REAL LREAL TIME LTIME DATE LDATE DATE_AND_TIME LDATE_AND_TIME TIME_OF_DAY LTIME_OF_DAY
|
|
17
|
+
DT LDT TOD LTOD STRING WSTRING CHAR WCHAR
|
|
18
|
+
ANY ANY_BIT ANY_DATE ANY_DERIVED ANY_ELEMENTARY ANY_INT ANY_MAGNITUDE ANY_NUM
|
|
19
|
+
ANY_REAL ANY_STRING ANY_UNSIGNED
|
|
20
|
+
AND AND_THEN OR OR_ELSE XOR NOT MOD ADD SUB MUL DIV MOVE SEL MIN MAX LIMIT
|
|
21
|
+
ABS ACOS ASIN ATAN COS SIN TAN EXP EXPT LN LOG SQRT TRUNC SHL SHR ROL ROR
|
|
22
|
+
ADR SIZEOF CONCAT DELETE FIND INSERT LEFT LEN MID REPLACE RIGHT
|
|
23
|
+
`
|
|
24
|
+
.trim()
|
|
25
|
+
.split(/\s+/),
|
|
26
|
+
);
|
|
27
|
+
const types =
|
|
28
|
+
"(?:BOOL|BYTE|WORD|DWORD|LWORD|SINT|USINT|INT|UINT|DINT|UDINT|LINT|ULINT|REAL|LREAL|TIME|LTIME|DATE|LDATE|DATE_AND_TIME|LDATE_AND_TIME|TIME_OF_DAY|LTIME_OF_DAY|DT|LDT|TOD|LTOD|STRING|WSTRING|CHAR|WCHAR)";
|
|
29
|
+
const conversion = new RegExp(`^(?:${types}_TO_${types}|TO_${types}|TRUNC_${types})$`);
|
|
30
|
+
export function isIecIdentifier(name: string): boolean {
|
|
31
|
+
const upper = name.toUpperCase();
|
|
32
|
+
return (
|
|
33
|
+
/^[A-Za-z][A-Za-z0-9_]*$/.test(name) &&
|
|
34
|
+
!name.includes("__") &&
|
|
35
|
+
!keywords.has(upper) &&
|
|
36
|
+
!conversion.test(upper)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const identifierBody = (label: string): string =>
|
|
41
|
+
label
|
|
42
|
+
.replace(/[^A-Za-z0-9_]/g, "_")
|
|
43
|
+
.replace(/_+/g, "_")
|
|
44
|
+
.replace(/^_+|_+$/g, "") || "name";
|
|
45
|
+
|
|
46
|
+
/** Capitals inside a word are kept, so `PlantIO` stays as written while `plant-io` is `PlantIo`. */
|
|
47
|
+
export const pascalCase = (label: string): string =>
|
|
48
|
+
label
|
|
49
|
+
.split(/[^A-Za-z0-9]+/)
|
|
50
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
51
|
+
.join("");
|
|
52
|
+
|
|
53
|
+
export class Names {
|
|
54
|
+
private readonly used = new Set<string>();
|
|
55
|
+
allocateObject(prefix: "FB" | "F" | "PRG" | "ST" | "E" | "GVL", label: string): string {
|
|
56
|
+
const source = prefix === "F" ? label.replace(/^PRG_/, "") : label;
|
|
57
|
+
const kind = prefix === "F" && source.startsWith("FUN_") ? "FUN" : prefix;
|
|
58
|
+
const body = source.startsWith(`${kind}_`) ? source.slice(kind.length + 1) : source;
|
|
59
|
+
return this.allocate(`${kind}_${identifierBody(body)}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
reserve(name: string): void {
|
|
63
|
+
this.used.add(name.toUpperCase());
|
|
64
|
+
// Qualified native types must keep their library prefix resolvable in the
|
|
65
|
+
// declaration scope, not merely reserve the complete dotted spelling.
|
|
66
|
+
const prefix = name.split(".")[0];
|
|
67
|
+
if (prefix) this.used.add(prefix.toUpperCase());
|
|
68
|
+
}
|
|
69
|
+
allocate(label: string, preserve = true): string {
|
|
70
|
+
const base = preserve && isIecIdentifier(label) ? label : `plc_${identifierBody(label)}`;
|
|
71
|
+
let name = base;
|
|
72
|
+
for (let suffix = 2; this.used.has(name.toUpperCase()); suffix++) name = `${base}_${suffix}`;
|
|
73
|
+
this.reserve(name);
|
|
74
|
+
return name;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Constant,
|
|
3
|
+
type CoreExpression,
|
|
4
|
+
type GeneratedObject,
|
|
5
|
+
integerRange,
|
|
6
|
+
floatMaximum,
|
|
7
|
+
integerType,
|
|
8
|
+
type PlcType,
|
|
9
|
+
} from "@tsplc/compiler/ir";
|
|
10
|
+
|
|
11
|
+
export function scalarLiteral(value: Constant, type: string): string {
|
|
12
|
+
if (value.kind === "bool") return value.value ? "TRUE" : "FALSE";
|
|
13
|
+
if (value.kind === "integer") return `${type}#${value.value}`;
|
|
14
|
+
if (value.kind === "float") {
|
|
15
|
+
// String(-0) loses the IEEE sign; keep it explicit in target float literals.
|
|
16
|
+
const text = Object.is(value.value, -0) ? "-0.0" : String(value.value).replace(/e/i, "E");
|
|
17
|
+
return `${type}#${text}`;
|
|
18
|
+
}
|
|
19
|
+
throw new Error("Expected verified scalar constant");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class NumericLowering {
|
|
23
|
+
readonly objects: GeneratedObject[] = [];
|
|
24
|
+
private readonly helpers = new Map<string, string>();
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
private readonly types: ReadonlyMap<string, PlcType>,
|
|
28
|
+
private readonly allocate: (label: string) => string,
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
prepare(value: CoreExpression): void {
|
|
32
|
+
if (value.kind === "load") {
|
|
33
|
+
value.path?.forEach((segment) => {
|
|
34
|
+
if (segment.kind === "index") this.prepare(segment.value);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (value.kind === "borrow") {
|
|
38
|
+
value.place.path.forEach((segment) => {
|
|
39
|
+
if (segment.kind === "index") this.prepare(segment.value);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (value.kind === "binary") {
|
|
43
|
+
this.prepare(value.left);
|
|
44
|
+
this.prepare(value.right);
|
|
45
|
+
if (value.policy) this.arithmetic(value.type, value.operator, value.policy);
|
|
46
|
+
}
|
|
47
|
+
if (value.kind === "unary") this.prepare(value.operand);
|
|
48
|
+
if (value.kind === "convert") {
|
|
49
|
+
this.prepare(value.operand);
|
|
50
|
+
if (value.policy === "saturate") this.saturation(value.operand.type, value.type);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
render(value: CoreExpression, operands: readonly string[]): string {
|
|
55
|
+
if (value.kind === "convert") {
|
|
56
|
+
return value.policy === "saturate"
|
|
57
|
+
? `${this.saturation(value.operand.type, value.type)}(${operands[0]})`
|
|
58
|
+
: `TO_${value.type}(${operands[0]})`;
|
|
59
|
+
}
|
|
60
|
+
if (value.kind === "unary") {
|
|
61
|
+
// Avoid signed intermediate casts: TIME spans the complete UDINT range.
|
|
62
|
+
if (value.operator === "timeToMilliseconds") return `TIME_TO_UDINT(${operands[0]})`;
|
|
63
|
+
// Milliseconds since the runtime started; DINT wraps after 24.8 days, so subtract to compare.
|
|
64
|
+
if (value.operator === "clock") return "TO_DINT(TIME_TO_UDINT(TIME()))";
|
|
65
|
+
return value.operator === "not"
|
|
66
|
+
? `(NOT ${operands[0]})`
|
|
67
|
+
: `TO_${value.type}(${value.operator === "negate" ? "-" : "NOT "}${operands[0]})`;
|
|
68
|
+
}
|
|
69
|
+
if (value.kind !== "binary") throw new Error("Expected numeric operation");
|
|
70
|
+
if (value.policy) {
|
|
71
|
+
return `${this.arithmetic(value.type, value.operator, value.policy)}(${operands.join(", ")})`;
|
|
72
|
+
}
|
|
73
|
+
if (value.operator === "shiftLeft" || value.operator === "shiftRight") {
|
|
74
|
+
const shift = value.operator === "shiftLeft" ? "SHL" : "SHR";
|
|
75
|
+
return `TO_${value.type}(${shift}(${operands.join(", ")}))`;
|
|
76
|
+
}
|
|
77
|
+
const tokens: Readonly<Record<string, string>> = {
|
|
78
|
+
add: "+",
|
|
79
|
+
subtract: "-",
|
|
80
|
+
multiply: "*",
|
|
81
|
+
divide: "/",
|
|
82
|
+
remainder: "MOD",
|
|
83
|
+
equal: "=",
|
|
84
|
+
notEqual: "<>",
|
|
85
|
+
less: "<",
|
|
86
|
+
lessEqual: "<=",
|
|
87
|
+
greater: ">",
|
|
88
|
+
greaterEqual: ">=",
|
|
89
|
+
bitAnd: "AND",
|
|
90
|
+
bitOr: "OR",
|
|
91
|
+
bitXor: "XOR",
|
|
92
|
+
};
|
|
93
|
+
const token = tokens[value.operator];
|
|
94
|
+
if (!token) throw new Error("Boolean short-circuit operation was not normalized");
|
|
95
|
+
const text =
|
|
96
|
+
value.operator === "remainder"
|
|
97
|
+
? `(${operands[0]} - (${operands[0]} / ${operands[1]}) * ${operands[1]})`
|
|
98
|
+
: `(${operands[0]} ${token} ${operands[1]})`;
|
|
99
|
+
return value.type === "BOOL" ? text : `TO_${value.type}(${text})`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private helper(
|
|
103
|
+
key: string,
|
|
104
|
+
type: string,
|
|
105
|
+
inputs: readonly [string, string][],
|
|
106
|
+
body: (name: string) => string,
|
|
107
|
+
locals = "",
|
|
108
|
+
): string {
|
|
109
|
+
const existing = this.helpers.get(key);
|
|
110
|
+
if (existing) return existing;
|
|
111
|
+
const name = this.allocate(`numeric_${key}`);
|
|
112
|
+
this.helpers.set(key, name);
|
|
113
|
+
const parameters = inputs
|
|
114
|
+
.map(([input, inputType]) => ` ${input} : ${inputType};`)
|
|
115
|
+
.join("\n");
|
|
116
|
+
const variables = locals
|
|
117
|
+
? `VAR
|
|
118
|
+
${locals}
|
|
119
|
+
END_VAR
|
|
120
|
+
`
|
|
121
|
+
: "";
|
|
122
|
+
const declaration = `FUNCTION ${name} : ${type}
|
|
123
|
+
VAR_INPUT
|
|
124
|
+
${parameters}
|
|
125
|
+
END_VAR
|
|
126
|
+
${variables}`;
|
|
127
|
+
this.objects.push({
|
|
128
|
+
id: `numeric:${key}`,
|
|
129
|
+
kind: "function",
|
|
130
|
+
name,
|
|
131
|
+
declaration,
|
|
132
|
+
implementation: body(name),
|
|
133
|
+
});
|
|
134
|
+
return name;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private add64(): string {
|
|
138
|
+
// XOR forms the sum bits; each carry moves left until it exits the 64-bit word.
|
|
139
|
+
// This gives modulo 2^64 addition without relying on target integer overflow.
|
|
140
|
+
return this.helper(
|
|
141
|
+
"add64",
|
|
142
|
+
"LWORD",
|
|
143
|
+
[
|
|
144
|
+
["a", "LWORD"],
|
|
145
|
+
["b", "LWORD"],
|
|
146
|
+
],
|
|
147
|
+
(name) => `x := a;
|
|
148
|
+
y := b;
|
|
149
|
+
FOR i := 1 TO 64 DO
|
|
150
|
+
carry := SHL(x AND y, 1);
|
|
151
|
+
x := x XOR y;
|
|
152
|
+
y := carry;
|
|
153
|
+
END_FOR;
|
|
154
|
+
${name} := x;
|
|
155
|
+
`,
|
|
156
|
+
` x, y, carry : LWORD;
|
|
157
|
+
i : UINT;`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private wrap64(operator: string): string {
|
|
162
|
+
const add = this.add64();
|
|
163
|
+
if (operator === "add") return add;
|
|
164
|
+
if (operator === "subtract") {
|
|
165
|
+
// Two's complement negation and addition use the same modulo word operation.
|
|
166
|
+
return this.helper(
|
|
167
|
+
"subtract64",
|
|
168
|
+
"LWORD",
|
|
169
|
+
[
|
|
170
|
+
["a", "LWORD"],
|
|
171
|
+
["b", "LWORD"],
|
|
172
|
+
],
|
|
173
|
+
(name) => `${name} := ${add}(a, ${add}(NOT b, LWORD#1));
|
|
174
|
+
`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (operator !== "multiply") throw new Error("Unsupported verified wrapping operation");
|
|
178
|
+
// Shift-and-add retains exactly the low 64 product bits.
|
|
179
|
+
return this.helper(
|
|
180
|
+
"multiply64",
|
|
181
|
+
"LWORD",
|
|
182
|
+
[
|
|
183
|
+
["a", "LWORD"],
|
|
184
|
+
["b", "LWORD"],
|
|
185
|
+
],
|
|
186
|
+
(name) => `x := a;
|
|
187
|
+
y := b;
|
|
188
|
+
result := LWORD#0;
|
|
189
|
+
FOR i := 1 TO 64 DO
|
|
190
|
+
IF (y AND LWORD#1) <> LWORD#0 THEN
|
|
191
|
+
result := ${add}(result, x);
|
|
192
|
+
END_IF;
|
|
193
|
+
x := SHL(x, 1);
|
|
194
|
+
y := SHR(y, 1);
|
|
195
|
+
END_FOR;
|
|
196
|
+
${name} := result;
|
|
197
|
+
`,
|
|
198
|
+
` x, y, result : LWORD;
|
|
199
|
+
i : UINT;`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private arithmetic(typeId: string, operator: string, policy: "wrap" | "saturate"): string {
|
|
204
|
+
const type = this.types.get(typeId);
|
|
205
|
+
if (type?.kind !== "integer") throw new Error("Expected verified integer arithmetic");
|
|
206
|
+
const token = operator === "add" ? "+" : operator === "subtract" ? "-" : "*";
|
|
207
|
+
const key = `${policy}_${operator}_${typeId}`;
|
|
208
|
+
if (policy === "wrap") {
|
|
209
|
+
if (type.bits === 64) {
|
|
210
|
+
const operation = this.wrap64(operator);
|
|
211
|
+
return this.helper(
|
|
212
|
+
key,
|
|
213
|
+
typeId,
|
|
214
|
+
[
|
|
215
|
+
["a", typeId],
|
|
216
|
+
["b", typeId],
|
|
217
|
+
],
|
|
218
|
+
(
|
|
219
|
+
name,
|
|
220
|
+
) => `${name} := LWORD_TO_${typeId}(${operation}(${typeId}_TO_LWORD(a), ${typeId}_TO_LWORD(b)));
|
|
221
|
+
`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const wide = !type.signed && operator !== "subtract" ? "ULINT" : "LINT";
|
|
225
|
+
return this.helper(
|
|
226
|
+
key,
|
|
227
|
+
typeId,
|
|
228
|
+
[
|
|
229
|
+
["a", typeId],
|
|
230
|
+
["b", typeId],
|
|
231
|
+
],
|
|
232
|
+
(
|
|
233
|
+
name,
|
|
234
|
+
) => `${name} := ${wide}_TO_${typeId}(${typeId}_TO_${wide}(a) ${token} ${typeId}_TO_${wide}(b));
|
|
235
|
+
`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const [minimum, maximum] = integerRange(type);
|
|
240
|
+
const min = `${typeId}#${minimum}`,
|
|
241
|
+
max = `${typeId}#${maximum}`,
|
|
242
|
+
zero = `${typeId}#0`;
|
|
243
|
+
return this.helper(
|
|
244
|
+
key,
|
|
245
|
+
typeId,
|
|
246
|
+
[
|
|
247
|
+
["a", typeId],
|
|
248
|
+
["b", typeId],
|
|
249
|
+
],
|
|
250
|
+
(name) => {
|
|
251
|
+
const clamp = (
|
|
252
|
+
condition: string,
|
|
253
|
+
endpoint: string,
|
|
254
|
+
depth = "",
|
|
255
|
+
) => `${depth}IF ${condition} THEN
|
|
256
|
+
${depth} ${name} := ${endpoint};
|
|
257
|
+
${depth} RETURN;
|
|
258
|
+
${depth}END_IF;
|
|
259
|
+
`;
|
|
260
|
+
let body: string;
|
|
261
|
+
if (!type.signed) {
|
|
262
|
+
// Unsigned subtraction has only underflow; the other operations have only overflow.
|
|
263
|
+
if (operator === "add") body = clamp(`a > ${max} - b`, max);
|
|
264
|
+
else if (operator === "subtract") body = clamp("a < b", zero);
|
|
265
|
+
else
|
|
266
|
+
body = `IF b <> ${zero} THEN
|
|
267
|
+
${clamp(`a > ${max} / b`, max, " ")}END_IF;
|
|
268
|
+
`;
|
|
269
|
+
} else if (operator === "add") {
|
|
270
|
+
body = `IF b > ${zero} THEN
|
|
271
|
+
${clamp(`a > ${max} - b`, max, " ")}ELSIF b < ${zero} THEN
|
|
272
|
+
${clamp(`a < ${min} - b`, min, " ")}END_IF;
|
|
273
|
+
`;
|
|
274
|
+
} else if (operator === "subtract") {
|
|
275
|
+
body = `IF b < ${zero} THEN
|
|
276
|
+
${clamp(`a > ${max} + b`, max, " ")}ELSIF b > ${zero} THEN
|
|
277
|
+
${clamp(`a < ${min} + b`, min, " ")}END_IF;
|
|
278
|
+
`;
|
|
279
|
+
} else {
|
|
280
|
+
// Handle zero first, then divide the endpoint before multiplying. Each sign
|
|
281
|
+
// quadrant tests representability without forming an overflowing product.
|
|
282
|
+
body =
|
|
283
|
+
clamp(`a = ${zero}`, zero) +
|
|
284
|
+
clamp(`b = ${zero}`, zero) +
|
|
285
|
+
`IF a > ${zero} THEN
|
|
286
|
+
IF b > ${zero} THEN
|
|
287
|
+
${clamp(`a > ${max} / b`, max, " ")} ELSE
|
|
288
|
+
${clamp(`b < ${min} / a`, min, " ")} END_IF;
|
|
289
|
+
ELSE
|
|
290
|
+
IF b > ${zero} THEN
|
|
291
|
+
${clamp(`a < ${min} / b`, min, " ")} ELSE
|
|
292
|
+
${clamp(`a < ${max} / b`, max, " ")} END_IF;
|
|
293
|
+
END_IF;
|
|
294
|
+
`;
|
|
295
|
+
}
|
|
296
|
+
return `${body}${name} := a ${token} b;
|
|
297
|
+
`;
|
|
298
|
+
},
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private saturation(sourceId: string, targetId: string): string {
|
|
303
|
+
const source = this.types.get(sourceId),
|
|
304
|
+
target = this.types.get(targetId);
|
|
305
|
+
if (source?.kind === "float" && target?.kind === "float") {
|
|
306
|
+
// Widening cannot exceed the destination's finite range. Identity
|
|
307
|
+
// conversions are removed by normalization; neither needs a helper.
|
|
308
|
+
if (source.bits <= target.bits) return `TO_${targetId}`;
|
|
309
|
+
const maximum = floatMaximum(target);
|
|
310
|
+
const literal = (value: number, type: string): string =>
|
|
311
|
+
scalarLiteral({ kind: "float", value }, type);
|
|
312
|
+
return this.helper(
|
|
313
|
+
`saturate_${sourceId}_${targetId}`,
|
|
314
|
+
targetId,
|
|
315
|
+
[["value", sourceId]],
|
|
316
|
+
(name) => `IF value < ${literal(-maximum, sourceId)} THEN
|
|
317
|
+
${name} := ${literal(-maximum, targetId)};
|
|
318
|
+
RETURN;
|
|
319
|
+
END_IF;
|
|
320
|
+
IF value > ${literal(maximum, sourceId)} THEN
|
|
321
|
+
${name} := ${literal(maximum, targetId)};
|
|
322
|
+
RETURN;
|
|
323
|
+
END_IF;
|
|
324
|
+
${name} := TO_${targetId}(value);
|
|
325
|
+
`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
if (!source || !target || !integerType(source) || !integerType(target)) {
|
|
329
|
+
throw new Error("Expected verified integer saturation");
|
|
330
|
+
}
|
|
331
|
+
const [fromMin, fromMax] = integerRange(source),
|
|
332
|
+
[toMin, toMax] = integerRange(target);
|
|
333
|
+
return this.helper(
|
|
334
|
+
`saturate_${sourceId}_${targetId}`,
|
|
335
|
+
targetId,
|
|
336
|
+
[["value", sourceId]],
|
|
337
|
+
(name) => {
|
|
338
|
+
let body = "";
|
|
339
|
+
if (fromMin < toMin)
|
|
340
|
+
body += `IF value < ${sourceId}#${toMin} THEN
|
|
341
|
+
${name} := ${targetId}#${toMin};
|
|
342
|
+
RETURN;
|
|
343
|
+
END_IF;
|
|
344
|
+
`;
|
|
345
|
+
if (fromMax > toMax)
|
|
346
|
+
body += `IF value > ${sourceId}#${toMax} THEN
|
|
347
|
+
${name} := ${targetId}#${toMax};
|
|
348
|
+
RETURN;
|
|
349
|
+
END_IF;
|
|
350
|
+
`;
|
|
351
|
+
return `${body}${name} := TO_${targetId}(value);
|
|
352
|
+
`;
|
|
353
|
+
},
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|