Skip to content

fix(lowering): resolve array ctor bounds through the index - #1897

Open
ghaith wants to merge 2 commits into
masterfrom
PRG-4730
Open

fix(lowering): resolve array ctor bounds through the index#1897
ghaith wants to merge 2 commits into
masterfrom
PRG-4730

Conversation

@ghaith

@ghaith ghaith commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Attribution: this branch, its tests and this description were produced by Claude
(Claude Code) working in my repository, not written directly by me. I have reviewed the
change, but please review it as machine-authored work. The commits carry a
Co-Authored-By: Claude trailer.

Fixes PRG-4730. Regression from #1831.

Problem

An array whose bound names a constant of the declaring POU fails to compile, when the element
type is one that needs construction:

PROGRAM mainProg
VAR CONSTANT five : DINT := 5; END_VAR
VAR arr : ARRAY[1..five] OF STRING[63]; END_VAR
END_PROGRAM
error: cannot generate call statement for ReferenceExpr {
    kind: Member(Identifier { name: "DINT_GREATER" }), base: None }
error occurred while generating initialization code for type '__mainProg_arr'

Moving the constant to a global makes it compile.

Cause

#1831 added a per-element construction loop to generated array constructors, and built the
loop bounds by copying the bound expression out of the declaration:

create_internal_binary_expression(counter.clone(), Operator::Greater, end.as_ref().clone(), ..)

The constructor (__mainProg_arr__ctor) is a POU of its own, so five, a member of
mainProg, does not resolve inside it. The operand is left without a type, the resolver's
first branch requires both operands to be numerical, and the comparison falls through to the
compare-via-function path, which annotates a call to a DINT_GREATER that does not exist. A
global constant worked only because a global is in scope everywhere.

Only element types that need construction were affected, since a scalar array generates no
loop at all — which is why this went unnoticed.

Fix

Take the bounds from the index, which holds them already const-evaluated, and emit literals.
The generated loop is now store i32 1 / icmp sgt i32 .., 5: a native comparison, no call.

An undetermined bound is rejected explicitly rather than through as_int_value, which reports
it as the pointer size and would otherwise produce a loop silently ranging over the wrong
bounds.

Failure surface

case before after
ARRAY[1..five] OF STRING[63] crash ok
ARRAY[1..five] OF <struct> crash ok
ARRAY[1..five, 1..five] OF STRING[10] crash ok
ARRAY[1..five] OF DINT ok ok
STRING[five] ok ok

Tests

  • 6 lowering tests reading the bounds back out of the generated constructor: local constant,
    global constant, an expression over a local constant ([two..two*3] resolves to 2..6),
    per-dimension bounds of a 2D array, and a guard that a scalar array gets no loop.
  • 1 codegen test asserting the native comparison and the absence of DINT_GREATER, which
    guards the reported symptom directly.
  • 2 lit tests for the runtime side, checking the last element of every array so that a bound
    resolved to the wrong value is caught, not only one that fails to compile.
  • 2 codegen tests pinning strings sized by a POU-local constant (STRING[five],
    WSTRING[five], STRING[five * 2 + 1], ARRAY[1..count] OF STRING[len]) against the same
    declaration written with literals. These pass without the fix as well; string sizing was
    never affected. Only the global form was covered before.

The 7 tests in the first three groups were confirmed red against the pre-fix code, then green.

Note the global-constant lowering test also fails before the fix: bounds previously arrived as
identifiers even when they happened to resolve, so the fix tightens both paths.

Ran locally on Windows: 2555 + 98 lib, 170 lowering, 372 correctness, 43 integration, all
green, fmt and clippy clean. The lit tests were not run locally (lit needs Python and
that suite runs on Linux in CI); they get their first run here.

🤖 Generated with Claude Code

ghaith and others added 2 commits August 27, 2026 17:11
Problem: An array whose bound names a constant of the declaring POU
(`ARRAY[1..five]`) failed to compile when its element type needs construction,
a sized string or a struct for example:

    error: cannot generate call statement for ReferenceExpr {
        kind: Member(Identifier { name: "DINT_GREATER" }), base: None }
    error occurred while generating initialization code for type '__mainProg_arr'

The generated element construction loop copied the bound expression out of the
declaration. That constructor is a POU of its own, so a name that belongs to the
declaring POU does not resolve inside it, the operand was left without a type,
and the comparison was then annotated as a call to a `DINT_GREATER` function
that does not exist. The same bound worked as a global constant, because a global
is in scope everywhere.

Solution: Take the bounds from the index, which holds them already
const-evaluated, and emit them as literals. An undetermined bound is rejected
explicitly rather than through `as_int_value`, which reports it as the pointer
size and would otherwise produce a loop over the wrong range.

Regression from #1831, which introduced the element construction loop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A constant declared in a POU sizes a string exactly like the literal it stands
for, on its own (`STRING[five]`, `WSTRING[five]`), inside an expression
(`STRING[five * 2 + 1]`), and as both the bound and the element length of an
array (`ARRAY[1..count] OF STRING[len]`).

The tests compare the generated field layout against the same declaration
written with literals, rather than snapshotting IR, so they assert the sizes
themselves. Only the global form was covered before, by
`variable_length_strings_using_constants_can_be_created`.

These pass without the array constructor fix as well: string sizing was never
affected by it. They are here because the local and the global form turned out
to be handled differently in a neighbouring position, and this pins the one that
was already correct.

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

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown

Build Artifacts

🪟 Windows

Artifact Link Size
stdlib.lib Download 5.3 MB
stdlib.dll Download 0.3 MB
plc.exe Download 38.3 MB

From workflow run

🐧 Linux

Artifact Link Size
deb-aarch64 Download 31.9 MB
plc-aarch64 Download 43.4 MB
deb-x86_64 Download 40.8 MB
schema Download 0.0 MB
stdlib Download 39.4 MB
plc-x86_64 Download 43.5 MB

From workflow run

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.

1 participant