Skip to content

feat: IEC 61131-3 structure and array initialization (forum report), plus six initialization fixes - #205

Open
thiagoralves wants to merge 7 commits into
developmentfrom
feat/structure-initialization
Open

feat: IEC 61131-3 structure and array initialization (forum report), plus six initialization fixes#205
thiagoralves wants to merge 7 commits into
developmentfrom
feat/structure-initialization

Conversation

@thiagoralves

Copy link
Copy Markdown
Contributor

Closes the forum report "Initialization variable with user defined structure type". The user's syntax was correct IEC 61131-3 — the production simply wasn't implemented. Investigating it surfaced five more initialization defects, three of them silent wrong-value bugs.

Structure initialization (Annex B.1.4.3)

test_ai_var : analog_input := (min_ai_scale := 4.0, max_ai_scale := 22.0);

Previously error: Expected RParen, found :=. Now supported in every declaration position: file-level and CONFIGURATION VAR_GLOBAL, PROGRAM / FUNCTION_BLOCK / FUNCTION / METHOD variables, STRUCT element defaults, inside array literals, and for function block instances (t : TON := (PT := T#1s) — the same production the standard uses for fb_name_decl).

Lowering lives in one place, backend/struct-init-codegen.ts, shared by codegen.ts and type-codegen.ts:

strucpp::iec_struct_init<POINT>([](auto& v0) { v0.Y = 2.0; v0.X = 1.0; })

Not a braced aggregate initializer: elements may be written in any order and may be omitted, with an omitted element keeping the default from its own declaration — which C++17 cannot express (no designated initializers). The runtime helper default-constructs the value, applying each element's own default, and the lambda overwrites only what is named. Nested levels take their type from decltype(v0.MEMBER), so library types and inline array members need no metadata lookup.

Also implements the type-level default forms of Annex B.1.3.3 (Setpoint : REAL := 25.0;, Origin : Point := (x := 0.0);), previously a parse error even for elementary types. A single post-build pass copies the default onto declarations without their own initializer, so every downstream consumer sees an ordinary initializer; it re-runs on the merged unit so a TYPE and its uses can live in different files.

Array repetition (Annex B.1.4.3 array_initial_elements)

[10(0)], [3(1), 2(5)], [7, 4(2), 9], and the bracket-less := 2(3), 2(4). The repeated value is a full expression, so it may be a structure initializer or a nested array literal. Gated on an integer immediately followed by (, which is never an expression in ST, so a function call as an array element is unaffected. Counts above 65536 throw rather than truncating — expansion is linear in the count.

Nested array initializers, and 3D arrays

[[1, 2, 3], [4, 5, 6]] for a multi-dimensional array. Implemented in the runtime rather than by teaching codegen to detect ranks, so overload resolution handles it and the notation works in every scope automatically. Each level fills from its own lower bound, so a short inner list leaves the rest of that row at its default instead of shifting the next row up — the semantic difference from writing the values flat. Array1D also gained an element-typed list, so an array whose element is itself composite works (ARRAY[0..1] OF Row), and so does the nested-Array1D chain a 4+-dimensional array lowers to.

3D arrays were entirely unusable, not merely uninitialisable: IEC_ARRAY_3D had neither an initializer list nor at(), and codegen emits the bounds-checked at() for every subscript in a body. It is now at parity with 1D/2D.

Function block array invocation

units[0](step := 2.0) did not parse — the statement was taken as an assignment target, which then demanded :=. A new instanceCallStatement, gated on ( following the closing ] directly, claims exactly the element invocation and leaves arr[0].m(…) to the existing rules. FunctionCallExpression gains an optional instance; the FB type resolves through the array's element type, and input assignment, the call, VAR_IN_OUT copy-back and => capture all work against that expression unchanged, including variable and multi-dimensional indices.

Bugs found and fixed along the way

Each was pre-existing; the first three were silent.

  • PROGRAM variable composite initializers were dropped. arr : ARRAY[0..2] OF INT := [1,2,3] compiled clean and ran with a zero-filled array — the project model flattened initializers to a string via expressionToString, which had no case for array literals. It now carries the AST expression, which also removed the parallel string-lowering pass in getDefaultValue (based literals, digit separators, typed prefixes, time and calendar literals, strings), so declaration initializers and statement bodies can no longer disagree.
  • Array-literal defaults on a STRUCT element were dropped to {} with no diagnostic.
  • STRING/WSTRING literals in the type generator were emitted unescaped, so an embedded " closed the C++ string early. Latent until array defaults started emitting; OSCAT's HTML-entity tables hit it immediately. translateIECString moved to codegen-utils so a STRING literal lowers identically everywhere.
  • The debug pointer table indexed multi-dimensional arrays with chained [i][j], which Array2D/Array3D have no operator for — so any project with a multi-dimensional array and debug enabled failed to build (reported from an AVR build). A shared formatArrayElementAccess now owns the per-rank rule alongside formatArrayType.
  • File-level VAR_GLOBAL was invisible to VAR_EXTERNAL. Such a reference now validates, including the type check, and is dropped from the pointer-plumbing list — file-level globals are plain file-scope storage the body already reaches by name, and a GlobalVar<V>* member would shadow the very global being referenced. The CONFIGURATION path is unchanged.
  • A TYPE naming a function block (AccumGrid : ARRAY[…] OF Accum) emitted its alias before the POU forward declarations, so the element type was undeclared.

Two initializer forms change shape

Both still valid C++ with the same value, and now identical to what the statement path emits: INT#5 emits static_cast<IEC_INT>(5) instead of 5, and 1.5E3 emits 1500.0. The two assertions in codegen-var-initializers.test.ts that were pinned to the old string-lowering output are updated.

Verification

2132 tests passing (86 files), up from 2006 — ~126 new across parser/AST, the shared lowering, codegen, semantics, and g++ integration. Coverage thresholds pass; type-defaults.ts at 100%.

The integration tests run the binaries and assert values, not just compilation — the only way to catch out-of-order elements landing on the wrong member, omitted elements losing their defaults, or a repetition group filling the wrong slots.

Also validated end-to-end through the OpenPLC Editor on a real project (44 assertions covering every form, plus the AVR generated_debug.cpp compiled for atmega2560), since the program and debug-table paths fail independently.

Documented, not implemented

  • := [10()] — repetition with no value. No positional lowering in C++17, and neither matiec nor CODESYS accepts it.
  • A method call on an array element, units[0].M() — needs the expression grammar's method-call lookahead to accept a subscripted object.
  • No validation that an array initializer's shape or element count matches the declared dimensions, and none that a subscript count matches the rank. Ordinary mistakes there surface as C++ errors or silent truncation rather than ST diagnostics. Worth a follow-up.

🤖 Generated with Claude Code

thiagoralves and others added 6 commits August 10, 2026 11:01
… fixes

Reported on the forum: `v : myStruct := (a := 1.0, b := 2.0)` failed with
`Expected RParen, found :=`. The syntax is correct — IEC 61131-3 Annex B.1.4.3
`structure_initialization` — the production was simply not implemented anywhere
in the grammar. Two related defects surfaced while investigating it.

Structure initialization (IEC 61131-3 B.1.4.3, B.1.3.3)

Parser gains `structInitializer`, reached through `primaryExpression` and gated
on `( NAME :=` (which is never a valid parenthesised expression, so nothing else
is affected). Going through `primaryExpression` means element values are ordinary
expressions, so nesting and array literals compose without a second initializer
grammar. Supported in every declaration position: VAR_GLOBAL (file-level and
CONFIGURATION), PROGRAM / FUNCTION_BLOCK / FUNCTION / METHOD variables, STRUCT
element defaults, inside array literals, and for function block instances
(`t : TON := (PT := T#1s)`, the same production the standard uses for
`fb_name_decl`).

Lowering lives in one place, `backend/struct-init-codegen.ts`, shared by
`codegen.ts` and `type-codegen.ts`:

    strucpp::iec_struct_init<POINT>([](auto& v0) { v0.Y = 2.0; v0.X = 1.0; })

Elements may be written in any order and may be omitted, with an omitted element
keeping the default from its own declaration — which a braced aggregate
initializer cannot express in C++17 (no designated initializers). The new runtime
helper default-constructs the value, applying every element's own default, and
the lambda overwrites only the named elements. Nested levels take their type from
`decltype(v0.MEMBER)`, so library types and inline array members need no metadata
lookup.

Also implements the type-level default forms of B.1.3.3
(`Setpoint : REAL := 25.0;`, `Origin : Point := (x := 0.0);`) — previously a
parse error even for elementary types. A single post-build pass copies the
default onto declarations that have no initializer, so every downstream consumer
sees an ordinary initializer instead of each declaration path learning about type
defaults. It re-runs on the merged unit so a TYPE and its uses can live in
different files.

PROGRAM variable composite initializers were silently dropped

`arr : ARRAY[0..2] OF INT := [1, 2, 3]` in a PROGRAM compiled clean and ran with
a zero-filled array: the project model flattened initializers to strings via
`expressionToString`, which had no case for array literals. It now carries the
AST expression, so these initializers reach codegen and go through the one
expression emitter. That removes the parallel string-lowering pass in
`getDefaultValue` (based literals, digit separators, typed prefixes, time and
calendar literals, strings) — declaration initializers and statement bodies can
no longer disagree. `getDefaultValue` is now `getTypeDefaultValue`, covering only
the no-initializer case, and the two duplicated PROGRAM constructor loops
collapse into `projectVarInitializer`, shared with the file-scope globals.

Two initializers change shape as a result, both still valid C++ with the same
value and now identical to the statement path: `INT#5` emits
`static_cast<IEC_INT>(5)` instead of `5`, and `1.5E3` emits `1500.0`.

File-level VAR_GLOBAL was invisible to VAR_EXTERNAL

`VAR_EXTERNAL` resolution only consulted CONFIGURATION globals, so declaring a
file-level global (a GVL) that way failed with "no matching VAR_GLOBAL
declaration"; the same global was reachable if the declaration was omitted. Such
a reference now validates, including the type check, and is dropped from the
pointer-plumbing list: file-level globals are plain file-scope storage the body
already reaches by name, and a `GlobalVar<V>*` member would shadow the very
global being referenced. The CONFIGURATION path is unchanged. `collectFBExternals`
applies the same rule for function blocks and now serves both the header and
implementation paths, replacing a near-duplicate inline collector.

Tests: 79 new cases across parser/AST, codegen, the shared lowering, semantics,
and g++ integration. The integration tests run the binary and check the values,
since compiling alone cannot show that out-of-order elements land on the right
members or that omitted elements keep their defaults. Net effect on codegen.ts is
-10 lines despite the new feature.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
IEC 61131-3 Annex B.1.4.3 `array_initial_elements`:

    array_initial_elements ::= array_initial_element
                             | integer '(' [array_initial_element] ')'

`[10(0)]` stands for ten copies of `0`. Previously a parse error
(`Expected RBracket, found (`).

The parser gains `arrayInitialElements`, used by both `arrayLiteral` and
`initializerExpression`, so repetition works in the bracketed form and in the
bracket-less form OpenPLC emits (`:= 2(3), 2(4)`). The alternative is gated on an
integer immediately followed by `(`, which is never an expression — ST has no
implicit multiplication and only an identifier can be called — so a function call
as an array element (`[F(2), 3]`) is unaffected.

The AST builder expands repetition groups into plain element lists, so semantic
analysis, the project model and codegen need no support of their own; each repeat
gets its own expression node so per-element annotations cannot collide. The
repeated value is a full expression, so it may be a structure initializer
(`[2((x := 1.5, y := 2.5))]`) or a nested array literal.

Counts use IEC integer notation (`[16#4(7)]`), a zero count expands to nothing,
and a count above 65536 throws rather than truncating — expansion is linear in the
count, so a typo would otherwise exhaust memory, and silently dropping the tail
would leave wrong values in the array.

The optional-element form `[10()]` (ten copies of the element default) is not
accepted. It has no positional lowering in C++17 — a braced initializer list
cannot skip a slot, and mixing a value-initialised element into the list breaks
`initializer_list<U>` deduction — and neither matiec nor CODESYS accepts it
either. Documented under a new "Initialization gaps" table.

Two further initialization gaps found while auditing and documented there, not
fixed here: a nested array initializer for a multi-dimensional array
(`[[1, 2], [3, 4]]`), and any 3D array initializer, which fails to build because
the runtime `Array3D` has no initializer-list constructor.

Also registers the rules added by the previous commit (`structInitializer`,
`structElementInitializer`) in the parser error-message provider, so a syntax
error inside a structure initializer names the construct being parsed.

Tests: 16 parser/AST cases plus 4 g++ integration cases that run the binary and
check the slot values, including a repeated structure initializer, a 2D array, a
STRING array and a file-level VAR_GLOBAL. Regression cases cover a function call
as an element, scalar and arithmetic initialisers, and a CONSTANT still resolving
as an array dimension through the same rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ape their strings

Two defects in the type generator, both silent until now.

An array-literal default on a STRUCT element lost its values:

    TYPE Buf : STRUCT data : ARRAY[0..3] OF INT := [7, 8, 9, 10]; END_STRUCT; END_TYPE

emitted `Array1D<IEC_INT, 0, 3> DATA{}` — zero-filled, no diagnostic. The type
generator's expression emitter has no array-literal case, so the value fell
through to its `0` fallback and the "arrays can't be `= 0`" guard rewrote that as
`{}`. STRUCT element defaults now route through the shared
`generateInitializerValue`, which already handles array literals, structure
initializers and repetition groups and delegates everything else to
`expressionToCpp` — so the branch added alongside the structure-initializer work
disappears rather than growing a second case.

Emitting those values then exposed the second defect: the type generator wrote
STRING and WSTRING literal bodies verbatim, so an embedded `"` closed the C++
string early and a `$`-escape was never translated. It went unnoticed because
scalar STRING defaults are rare and array defaults were being dropped — OSCAT's
HTML-entity tables (`ARRAY[1..4] OF STRING` full of quotes) hit it immediately.
`translateIECString` moves from a private method on the expression emitter to
`codegen-utils`, so a STRING literal lowers identically in a statement, a
variable initialiser and a STRUCT element default. Its doc comment, which had
drifted onto the wrong function, moves with it.

Tests: five cases covering array-literal and repetition defaults on a STRUCT
element, the no-default case still value-initialising, and quote/`$T` escaping in
both a scalar and an array-literal element default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ug table

The debug pointer table emitted one subscript per dimension, so a 2D array came
out as `G_MATRIX.value[0][0]`. `Array2D` / `Array3D` take every index in a single
`operator()` call, so that has no matching operator and the generated
`generated_debug.cpp` fails to compile — reported from an AVR build:

    error: no match for 'operator[]' (operand types are
    'strucpp::IEC_ARRAY_2D<...>' and 'int')

Any project with a multi-dimensional array and debug enabled hit this; only 1D
arrays worked, which is why it went unnoticed.

`walkArrayDims` now collects the indices across all dimensions and renders the
access once at the innermost level through a new shared
`formatArrayElementAccess`, which sits next to `formatArrayType` because it has
to agree with the container that function picks per rank: `[i]` for `Array1D`,
`(i, j)` / `(i, j, k)` for `Array2D` / `Array3D`, and one subscript per dimension
again for 4+ dimensions (nested `Array1D`). The accessors stay unchecked rather
than `.at()` because only they are constexpr, which is what lets `&arr[i]` be a
constant expression — required for the table's PROGMEM placement on AVR.

The IEC display path in the debug map keeps its `[i][j]` form, which is what the
editor's debug UI shows.

Tests: 2D, 3D and 1D pointer expressions, no chained subscript in any pointer
expression, and the display paths unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…invocation

Three gaps found in the initialization audit.

3D arrays were unusable, not just uninitialisable

`IEC_ARRAY_3D` had neither an initializer-list constructor nor `at()`, so
`ARRAY[0..1,0..1,0..1] OF INT := [1, …]` failed to build — and so did any
subscript in a body, because codegen emits the bounds-checked `.at()`. It now
matches 1D/2D: flat row-major initializer list, `at()` in both const and mutable
form, iterators, and the `dimN_size` / `dimN_lower` / `dimN_upper` helpers. (The
dormant C++ runtime test already called `dim1_size()` on a 3D array, so that file
could not have compiled either; it can now.)

Nested array initializers

IEC 61131-3 Annex B.1.4.3 allows an `array_initialization` as an
`array_initial_element`, which is the natural way to write a multi-dimensional
initializer: `[[1, 2, 3], [4, 5, 6]]`. Implemented in the runtime rather than by
teaching codegen to detect ranks, so plain C++ overload resolution picks the right
constructor and the notation works in every scope — globals, PROGRAM/FB/FUNCTION
locals, STRUCT element defaults, and nested inside a structure initializer:

  - `Array2D` / `Array3D` gain row- and plane-nested initializer lists. Each level
    fills from its own lower bound, so a short inner list leaves the rest of that
    row at its default instead of shifting the next row up — the semantic
    difference from writing the same values flat.
  - `Array1D` gains an element-typed initializer list, so an array whose element
    is itself composite works too: `ARRAY[0..1] OF Row := [[1,2,3],[4,5,6]]`, and
    the nested-`Array1D` chain a 4+-dimensional array lowers to. The deducing
    template can't serve these — `U` has nothing to deduce from a braced element —
    and it still wins for a scalar list, so the two don't compete.

Codegen only had to stop descending the element type twice for a nested list: the
inner lists of a multi-dimensional array hold the *same* element type, and
descending produced `typename typename …::element_type::element_type`, which is
not valid C++ at all.

Function block array invocation

`units[0](step := 2.0)` did not parse — the statement was taken as an assignment
target, which then demanded `:=`. A new `instanceCallStatement`, gated on `(`
following the closing `]` directly, claims exactly the element invocation and
leaves `arr[0].m(…)` (which could equally be a method call on the element) to the
existing rules. `FunctionCallExpression` gains an optional `instance` expression;
`functionName` still carries the base variable name, so the declared type resolves
the usual way and the FB type comes from its array element type. Input assignment,
the call, VAR_IN_OUT copy-back and `=>` capture then all work against that
expression unchanged, including a variable or multi-dimensional index.

Declaring an FB array and reading a member already worked — an earlier report that
those were broken was a name collision in the test with the library FBs `RAMP` and
`RS`, not a defect.

Still not parsed, and now recorded: a *method* call on an array element
(`units[0].M()`), which needs the expression grammar's method-call lookahead to
accept a subscripted object.

Tests: 11 parser cases for element invocation (including a variable index, a 2D
index, a nested subscript, and inside a FOR loop) plus regression cases for
assignments and plain invocations; 8 g++ integration cases that run the binary and
check values — 3D init and element access, 2D/3D nesting, short rows keeping their
defaults, array-of-array-type nesting, structure initializers nested in a 2D
array, and per-element FB state after a scan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A TYPE may name a function block — `AccumGrid : ARRAY[0..1,0..1] OF Accum` emits
`using ACCUMGRID = Array2D<ACCUM, 0, 1, 0, 1>` — but the user-defined types block
was emitted before the POU forward declarations, so `ACCUM` was undeclared at that
point and the header failed to compile:

    error: use of undeclared identifier 'ACCUM'
    error: unknown type name 'ACCUMGRID'

The forward declarations now also precede the types block. An incomplete type is
enough for the alias, which instantiates nothing; the instantiation happens where
the alias is used as a member, well after the full definition. The existing
forward-declaration block stays where it was — repeating a class declaration is
legal — and both call the same helper so they can't drift.

Found while adding a named ARRAY-OF-function-block type to the end-to-end test
project, which is also where the inline form (`ARRAY[0..1,0..1] OF Accum` written
directly in a VAR block) can't be used: the OpenPLC editor's variable parser
rejects a comma inside the type, so a named type is the only way to declare a
multi-dimensional array there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three ordinary mistakes escaped the compiler. A nesting or rank error surfaced as
a C++ error against generated code, and an over-long initializer was silently
truncated by the runtime container's constructor — the array came out with values
missing and no diagnostic anywhere:

    ARRAY[0..2] OF INT := [1,2,3,4,5,6]      -> compiled, kept 1,2,3
    ARRAY[0..1,0..1] OF INT := [[1,2,3],[4,5,6]]  -> compiled, dropped 3 and 6
    ARRAY[0..1,0..1,0..2] OF INT := [[1,2,3],[4,5,6]]
                                             -> g++: no matching constructor
    p[0,0] on a 3-dimensional array          -> g++: no matching call to 'at'

Now reported against the source, with line and column:

    fmt.st:3:29: error: Initializer for 'A' has 6 values but the array holds 3.
                        The extra values would be discarded.
    fmt.st:7:8: error: 'M' has 2 dimensions but is indexed with 1 index.

Checks

  - **Over-long** — a flat list against the total element count, and each level of
    a nested list against its own dimension. Repetition groups are already
    expanded by then, so `[10(7)]` into a 3-element array is caught.
  - **Nesting** — depth must match the rank. A flat list at the outermost level
    still fills the whole array row-major (IEC allows it at any rank), but once
    nesting starts each level descends exactly one dimension: stopping early
    leaves dimensions unaccounted for and no container constructor matches.
    Nesting past the rank is caught too, as is mixing nested and flat entries.
    Nesting into an array whose element type is itself an array stays legal.
  - **Subscript count** — one index per dimension. Walks the ordered access chain
    rather than the flat `subscripts` list, because only the chain distinguishes
    `a[0][1]` (two steps into an array of arrays) from `a[0,1]` (one two-index
    step into a 2D array); the flat list reports 2 for both.

Deliberately conservative

Every check is skipped rather than guessed at when the shape isn't statically
known — variable-length `ARRAY[*]`, a non-constant bound, a type that doesn't
resolve, or a dereference in the access chain — so it can only add diagnostics for
definite mistakes. A scalar initializer on an array is also left alone: it is
meaningful on a STRUCT element (`data : ARRAY[…] OF INT := 0` value-initialises),
so rejecting it would flag working code.

Initializers are checked wherever a declaration can appear: file-level and
CONFIGURATION VAR_GLOBAL, PROGRAM / FUNCTION_BLOCK / FUNCTION / METHOD variables,
and STRUCT element defaults. Subscripts are checked in every POU body, against
the POU's own variables with globals as the fallback. One diagnostic per bad
declaration rather than one per row.

`evalIntConst` moves from `debug-table-gen` to `type-utils` alongside the new
shape resolver, so the two consumers share one implementation.

Tests: 31 cases split between rejected and accepted — the accepted half is the
point, since a false positive here would block valid code. Full suite 2163
passing, including the OSCAT and SoftMotion library builds, and the 44-assertion
end-to-end project still compiles clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thiagoralves

Copy link
Copy Markdown
Contributor Author

Added a seventh commit: feat(semantic): validate array initializer shape and subscript count.

This closes the follow-up flagged in the description — the three mistakes that previously escaped to g++ or truncated silently now produce ST diagnostics with line and column:

ARRAY[0..2] OF INT := [1,2,3,4,5,6]              -> compiled, silently kept 1,2,3
ARRAY[0..1,0..1] OF INT := [[1,2,3],[4,5,6]]     -> compiled, silently dropped 3 and 6
ARRAY[0..1,0..1,0..2] OF INT := [[1,2,3],[4,5,6]] -> g++: no matching constructor
p[0,0] on a 3-dimensional array                   -> g++: no matching call to 'at'

Now:

fmt.st:3:29: error: Initializer for 'A' has 6 values but the array holds 3. The extra values would be discarded.
fmt.st:7:8: error: 'M' has 2 dimensions but is indexed with 1 index.

The subscript check walks the ordered access chain rather than the flat subscripts list, because only the chain distinguishes a[0][1] (two steps into an array of arrays) from a[0,1] (one two-index step into a 2D array) — the flat list reports 2 for both.

Every check is skipped rather than guessed at when the shape isn't statically known (ARRAY[*], non-constant bounds, unresolved types, a dereference in the chain), so it can only add diagnostics for definite mistakes. 31 new tests, deliberately split between rejected and accepted cases — the accepted half is the point, since a false positive here would block valid code.

Suite: 2163 passing, no net new lint warnings.

Comment thread src/backend/codegen.ts
return undefined;
}
if (decl.initialValue) {
return this.generateInitializer(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping the string path loses precision on 64-bit integer initializers. projectVarInitializer now routes PROGRAM/VAR_GLOBAL initializers through generateInitializergenerateExpressionformatIntegerLiteral, whose plain-decimal branch is return String(value) where value is a JS number (parseIECInteger uses parseInt). The removed lowerNumericInitializer passed the raw digits through iecBaseToCppLiteral (which returns raw.replace(/_/g, "") for decimals), so it was exact.

PROGRAM Main VAR x : LINT := 9007199254740993; END_VAR

used to emit X(9007199254740993) and now emits X(9007199254740992) — a silently wrong constant. Either keep rawValue for plain decimals in formatIntegerLiteral (return raw.replace(/_/g, "")) or store integer literal values as bigint.

Comment thread src/backend/codegen.ts
// one appeared where no type is available (it is not an expression IEC
// allows in a statement), so value-initialise rather than emit code that
// would not compile.
return "{}";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

case "StructInitializerExpression": … return "{}" is a silent-wrong-value fallback, and it is reachable from real ST, not just from impossible positions. generateExpression's ArrayLiteralExpression case maps elements through generateExpression, so arr := [(x := 1.0), (x := 2.0)]; in a PROGRAM body emits ARR = {{}, {}}; — every element zeroed, no diagnostic. The same happens for a structure initializer passed as a named FB argument and for SETUP vars in test-main-gen.ts (emitExpression(decl.initialValue)). Since a structure initializer in a non-declaration position is not legal IEC anyway, emit a CompileError (or a codegen warning) here instead of silently value-initialising.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants