Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
f660bc2
feat(frontend,backend): structure initialization + two composite-init…
thiagoralves Aug 10, 2026
bc50ec9
feat(frontend): array repetition initializer `[N(value)]`
thiagoralves Aug 10, 2026
c9257f6
fix(codegen): keep array-literal defaults on STRUCT elements, and esc…
thiagoralves Aug 10, 2026
97d82ad
fix(debug): index multi-dimensional arrays with operator() in the deb…
thiagoralves Aug 10, 2026
a78c5e3
feat: 3D arrays, nested array initializers, and function-block array …
thiagoralves Aug 10, 2026
cee7131
fix(codegen): forward-declare POU classes before the user-defined types
thiagoralves Aug 10, 2026
bd6f011
feat(semantic): validate array initializer shape and subscript count
thiagoralves Aug 10, 2026
070ebf8
fix(codegen,semantic): lower integer literals exactly, and reject mis…
thiagoralves Aug 14, 2026
585e5c6
fix(debug-table): mangle a member whose name matches its type, as cod…
thiagoralves Aug 15, 2026
143bacd
Merge pull request #205 from Autonomy-Logic/feat/structure-initializa…
thiagoralves Aug 17, 2026
67cb040
fix(backend): one member-mangling rule, applied everywhere a member i…
thiagoralves Aug 17, 2026
23c08be
Merge remote-tracking branch 'origin/development' into fix/debug-tabl…
thiagoralves Aug 17, 2026
8ad26a3
fix(codegen): mangle the PROGRAM constructor initializer list too
thiagoralves Aug 17, 2026
aa2ba1f
fix(codegen): mangle function-block parameters at the invocation site
thiagoralves Aug 17, 2026
beb0e3b
Merge pull request #213 from Autonomy-Logic/fix/debug-table-member-ma…
thiagoralves Aug 17, 2026
9c95b09
chore(release): bump version to 0.6.3
thiagoralves Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions docs/IEC_COMPLIANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ STruC++ implements the Structured Text (ST) language from IEC 61131-3. This docu
| TYPE ... END_TYPE | Supported | Type aliases |
| STRUCT ... END_STRUCT | Supported | With nested structs |
| Enumerations | Supported | With optional base type |
| Initialized type declarations | Supported | A type may carry its own default (`Setpoint : REAL := 25.0;`, `Origin : Point := (x := 0.0);`), inherited by every declaration of the type that has no initializer |
| ARRAY (1D) | Supported | Arbitrary bounds: ARRAY[1..10] OF INT |
| ARRAY (2D) | Supported | ARRAY[1..3, 1..4] OF REAL |
| ARRAY (3D) | Supported | ARRAY[1..3, 1..4, 1..5] OF INT |
| ARRAY OF function block | Supported | Declaration, member access, and element invocation (`units[i](step := 1.0)`). A *method* call on an element (`units[0].M()`) is not yet parsed |
| ARRAY[*] (VLA) | Supported | Variable-length array parameters |
| Subranges | Supported | Runtime validation |
| REF_TO | Supported | IEC reference type (explicit dereference) |
Expand Down Expand Up @@ -59,14 +61,24 @@ STruC++ implements the Structured Text (ST) language from IEC 61131-3. This docu
| VAR_INPUT | Supported | Input parameters |
| VAR_OUTPUT | Supported | Output parameters |
| VAR_IN_OUT | Supported | Pass-by-reference parameters |
| VAR_EXTERNAL | Supported | External references to VAR_GLOBAL |
| VAR_GLOBAL | Supported | Global variables |
| VAR_EXTERNAL | Supported | References either a CONFIGURATION or a file-level VAR_GLOBAL |
| VAR_GLOBAL | Supported | Global variables (CONFIGURATION-scoped or file-level) |
| CONSTANT | Supported | Compile-time constants |
| RETAIN | Supported | Tracked in retain variable table |
| NON_RETAIN | Supported | |
| AT %IX0.0 | Supported | Located variables (I/Q/M areas, X/B/W/D/L sizes) |
| Multiple names | Supported | `a, b, c : INT := 0;` |
| Initialization | Supported | `:= expression` |
| Array initialization | Supported | `:= [1, 2, 3]` and the bracket-less `:= 1, 2, 3`. Multi-dimensional arrays take either a flat row-major list or a nested one (`:= [[1, 2], [3, 4]]`), where each inner list fills one row from its own bound. Nesting depth and value count are validated against the declared dimensions |
| Array repetition | Supported | `:= [10(0)]`, `:= [3(1), 2(5)]`, `:= [7, 4(2), 9]`. The repeated value may be a structure initializer. Max count 65536 |
| Structure initialization | Supported | `:= (x := 1.0, y := 2.0)`; nested, in array literals, and for FB instances. Omitted elements keep their own declared default. Only valid as a declaration's initial value, as in the standard — one written inside a statement is rejected |
| STRUCT element defaults | Supported | Scalar, array-literal and structure-initializer defaults on a STRUCT element all carry their values |

### Initialization gaps

| Form | Notes |
|------|-------|
| Repetition with no value | `:= [10()]` (ten copies of the element default) — write `:= [10(0)]`, or omit the elements entirely. Matches matiec and CODESYS, which also require a value |

## Operators and Expressions

Expand All @@ -84,9 +96,10 @@ STruC++ implements the Structured Text (ST) language from IEC 61131-3. This docu
| Parentheses | `( )` | Supported |
| Function call | `name(args)` | Supported (positional + named) |
| Method call | `obj.method(args)` | Supported |
| Array access | `arr[i]`, `arr[i, j]` | Supported |
| Array access | `arr[i]`, `arr[i, j]` | Supported — the index count is validated against the declared rank |
| Field access | `struct.field` | Supported |
| Typed literals | `INT#5`, `DINT#42`, `REAL#3.14` | Supported |
| Integer literals | `9223372036854775807`, `16#FF`, `1_000` | Supported — the full 64-bit LINT/ULINT range is preserved exactly; a value wider than ULINT is rejected |
| NEW | `__NEW(type)`, `__NEW(type, size)` | Supported |
| DELETE | `__DELETE(ptr)` | Supported |

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "strucpp",
"version": "0.6.1",
"version": "0.6.3",
"description": "IEC 61131-3 Structured Text to C++ Compiler",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
17 changes: 17 additions & 0 deletions src/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
DrefExpression,
NewExpression,
ArrayLiteralExpression,
StructInitializerExpression,
StructElementInitializer,
AssertCall,
MockFunctionStatement,
MockVerifyCallCountStatement,
Expand Down Expand Up @@ -189,7 +191,7 @@
}

// Check scope filter
if (upperScope && nodeScope && nodeScope.toUpperCase() !== upperScope) {

Check warning on line 194 in src/ast-utils.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly

Check warning on line 194 in src/ast-utils.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly
// Still recurse into children — a matching POU may be nested inside
for (const child of getChildren(node)) {
visit(child, nodeScope);
Expand Down Expand Up @@ -325,7 +327,7 @@
column: number,
): boolean {
const span = node.sourceSpan;
if (!span || span.file !== file) return false;

Check warning on line 330 in src/ast-utils.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected object value in conditional. The condition is always true
if (line < span.startLine || line > span.endLine) return false;
if (line === span.startLine && column < span.startCol) return false;
if (line === span.endLine && column > span.endCol) return false;
Expand All @@ -344,6 +346,7 @@
"DrefExpression",
"NewExpression",
"ArrayLiteralExpression",
"StructInitializerExpression",
]);

function isExpression(node: ASTNode): boolean {
Expand Down Expand Up @@ -455,6 +458,7 @@
case "TypeDeclaration": {
const td = node as TypeDeclaration;
children.push(td.definition);
if (td.defaultValue) children.push(td.defaultValue);
break;
}

Expand Down Expand Up @@ -592,6 +596,7 @@

case "FunctionCallExpression": {
const fce = node as FunctionCallExpression;
if (fce.instance) children.push(fce.instance);
children.push(...fce.arguments);
break;
}
Expand Down Expand Up @@ -652,6 +657,18 @@
break;
}

case "StructInitializerExpression": {
const sie = node as StructInitializerExpression;
children.push(...sie.elements);
break;
}

case "StructElementInitializer": {
const sei = node as StructElementInitializer;
children.push(sei.value);
break;
}

// --- Test framework ---
case "AssertCall": {
const ac = node as AssertCall;
Expand Down
138 changes: 138 additions & 0 deletions src/backend/codegen-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* Shared utility functions for C++ code generation.
*/

import { exactIntegerLiteralValue } from "../literal-utils.js";

/**
* Convert an IEC 61131-3 based numeric string to a C++ literal string.
* Handles 16#FF → 0xFF, 8#77 → 077, 2#1010 → 0b1010, and plain decimals.
Expand All @@ -17,6 +19,44 @@ export function iecBaseToCppLiteral(raw: string): string {
return raw.replace(/_/g, "");
}

/** Largest value a C++ *decimal* literal can name without a suffix. */
const CPP_SIGNED_LITERAL_MAX = 9223372036854775807n;

/**
* Lower an IEC 61131-3 integer literal to a C++ integer literal.
*
* Based literals (16#FF, 8#77, 2#1010) keep their notation. Plain decimals are
* re-emitted from the *exact* value rather than from the parsed `number`, which
* matters twice over:
*
* - `number` rounds above 2^53, so a LINT/ULINT initializer such as
* `9007199254740993` would silently become ...992, and `ULINT` bounds would
* round past the type's range into a literal g++ rejects outright.
* - passing the raw digits straight through instead would make a leading zero
* an octal prefix in C++ (`0010` → 8, `008` → a compile error), so the
* digits are normalized rather than copied.
*
* A value above `CPP_SIGNED_LITERAL_MAX` gets a `ULL` suffix: a C++ decimal
* literal is only ever given a *signed* type (C++17 [lex.icon]/3), so without
* it `18446744073709551615` names no type at all.
*
* `value` is the pre-parsed fallback for a literal whose raw text is not a
* plain integer (synthesized nodes, for one).
*/
export function formatIntegerLiteral(rawValue: string, value: number): string {
const upper = rawValue.toUpperCase().replace(/_/g, "");
if (
upper.startsWith("16#") ||
upper.startsWith("8#") ||
upper.startsWith("2#")
) {
return iecBaseToCppLiteral(rawValue);
}
const exact = exactIntegerLiteralValue(rawValue);
if (exact === undefined) return String(value);
return exact > CPP_SIGNED_LITERAL_MAX ? `${exact}ULL` : exact.toString();
}

/**
* Format an array type string from element type and dimension bounds.
*
Expand Down Expand Up @@ -52,3 +92,101 @@ export function formatArrayType(
}
return result;
}

/**
* Append an unchecked element access for one full set of array indices,
* matching the container {@link formatArrayType} picked for that rank.
*
* `Array1D` subscripts with `operator[]`; `Array2D` / `Array3D` take all indices
* at once through `operator()`; 4+ dimensions are nested `Array1D`, so they
* subscript once per dimension. Getting this wrong doesn't just read the wrong
* element — `arr[i][j]` on an `Array2D` has no matching operator and fails to
* compile.
*
* Unchecked (rather than `.at()`) because these accessors are `constexpr`, which
* is what lets `&arr[i]` be a constant expression — required for the debug
* pointer table's PROGMEM placement on AVR.
*/
export function formatArrayElementAccess(
base: string,
indices: number[],
): string {
if (indices.length === 2 || indices.length === 3) {
return `${base}(${indices.join(", ")})`;
}
return base + indices.map((i) => `[${i}]`).join("");
}

/**
* Translate IEC 61131-3 `$`-escape sequences in a string literal's body to C++
* escape sequences, and escape what C++ needs escaped.
*
* Handles `$N`/`$n` (newline), `$L`/`$l` (line feed), `$R`/`$r` (CR), `$T`/`$t`
* (tab), `$P`/`$p` (form feed), `$$` (literal `$`), `$'` (single quote), `$XX`
* (hex byte) and `''` (doubled single quote), then escapes backslash and
* double-quote so the result is safe inside a C++ `"…"` literal.
*
* Shared by the expression emitter and the type generator: a STRING literal has
* to lower identically whether it appears in a statement, a variable
* initialiser, or a STRUCT element default.
*/
export function translateIECString(inner: string): string {
let result = "";
for (let i = 0; i < inner.length; i++) {
const ch = inner[i]!;
if (ch === "$" && i + 1 < inner.length) {
const next = inner[i + 1]!;
switch (next.toUpperCase()) {
case "N":
case "L":
result += "\\n";
i++;
break;
case "R":
result += "\\r";
i++;
break;
case "T":
result += "\\t";
i++;
break;
case "P":
result += "\\f";
i++;
break;
case "$":
result += "$";
i++;
break;
case "'":
result += "'";
i++;
break;
default:
// $XX hex escape: two hex digits
if (
i + 2 < inner.length &&
/^[0-9A-Fa-f]{2}$/.test(inner.substring(i + 1, i + 3))
) {
result += "\\x" + inner.substring(i + 1, i + 3);
i += 2;
} else {
// Unknown $-escape, pass through
result += "\\\\$";
}
break;
}
} else if (ch === "'" && i + 1 < inner.length && inner[i + 1] === "'") {
// ST doubled-quote → single quote
result += "'";
i++;
} else if (ch === "\\") {
result += "\\\\";
} else if (ch === '"') {
result += '\\"';
} else {
result += ch;
}
}
return result;
}
Loading
Loading