feat(located): support ARRAY at a physical address - #229
Conversation
`HR_myData AT %MW60 : ARRAY [0..66] OF WORD` was rejected. The reason
given upstream was that MatIEC could not express a located non-elementary
type (openplc-editor#565) -- but that constraint left with MatIEC, and
nothing in this compiler stands in its way: the descriptor table is flat,
one {area, size, index, pointer} row per slot, so an array is N rows
rather than a new mechanism.
Analyzer: validate the ELEMENT type against the address size class, and
detect duplicates by slot-range overlap instead of address equality -- an
array occupies slotCount consecutive slots, so `x AT %MW60 : ARRAY[0..66]
OF WORD` collides with a plain `y AT %MW61 : WORD` even though the two
addresses differ. Reject the array shapes with no linear layout
(multi-dimensional, variable-length) with their own sentence, so none of
them falls through to the type-compatibility error and reports the
internal `__INLINE_ARRAY_<T>` spelling at the user.
Codegen: expand a located array into one descriptor per element, walking
the address forward by one slot each time (bit addresses continue across
the byte boundary).
Also fix the pointer initialiser, which located each descriptor by
variable NAME. With N descriptors sharing one name that resolved every
element to slot 0: element 0 bound N times, elements 1..N-1 left null.
Walk the array by position instead -- the index is right there.
Verified end to end: the issue's own declaration compiles to 67
descriptors over %MW60..%MW126, all 67 bound to distinct storage, and a
write through one descriptor lands in that element alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…llisions Editor half of openplc-editor#565. The compiler half is Autonomy-Logic/STruCpp#229. **Accept the declaration.** `HR_myData AT %MW60 : ARRAY [0..66] OF WORD` was refused: location validation switched on the variable's own type, and an array fell through to the default branch. It now validates the ELEMENT type against the address class, which is what actually has to fit -- 67 consecutive WORD slots, each a WORD. The refusal came from the MatIEC era, and that constraint left with MatIEC. **Detect the collisions that come with it.** An array is a contiguous area, so `arr AT %QX0.0 : ARRAY [0..9] OF BOOL` runs through `%QX1.1` and conflicts with a plain `flag AT %QX0.6` -- two different address strings, one piece of storage. `checkIfLocationExists` compared locations for string equality, which was sufficient while every variable claimed exactly one slot and silently wrong the moment one could claim more. It now compares slot RANGES, via a new `slotRangesOverlap` beside `parseAddress`, reusing the existing `getArrayTotalElements` for the span. Two properties preserved deliberately: ranges only collide within the same class (`%MW0` and `%MD0` index different runtime arrays and still never overlap), and a location that is not a literal `%…` is an alias name, where the test stays exact equality. **Auto-increment follows.** The next-free-location scan tested set membership against exact strings, so it could stop one slot inside a neighbouring array, or place a new array on top of an existing scalar. It now advances until the candidate's whole span is clear, and steps by the element type -- an array's own `type.value` is the "ARRAY [...] OF T" text, which `incrementLocationByOne` matches against nothing and would have bailed on the first pass. Also fixes `variableLocationValidationErrorMessage` returning `''` for any type with no address class, which surfaced as a bare "Please make sure that the location is valid." with no reason attached. The bundled compiler is still pinned to strucpp v0.6.4, which rejects a located array. STruCpp#229 has to be released and `binary-versions.json` bumped before this reaches users, or the editor accepts a declaration the build then refuses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thiagoralves
left a comment
There was a problem hiding this comment.
Reviewed together with openplc-editor#1080 and openplc-web#730 (which is a byte-identical mirror of #1080). Built this branch merged with development, ran the full suite (1021 passed, 0 failed), compiled real ST projects with g++ -fsyntax-only, and took the compiler end-to-end through the editor onto hardware (SLM-RP4 @ 192.168.2.4, runtime v4.2.0).
Verified working: inline located arrays, %QX bit arrays crossing byte boundaries, negative lower bounds, and CONFIGURATION VAR_GLOBAL arrays all generate correct per-element descriptors with distinct pointers. The end-to-end editor build produced 19 correct descriptors (10 HR + 8 Coils + 1 Counter), each bound to a distinct element. The findIndex → positional-index fix in generateLocatedVarPointerInit is correct, and locatedGlobals[] indexing stays consistent with generateLocatedGlobalsDefinition's filter. Rejection messages for multi-dimensional, VLA, empty-bounds and element-type mismatches are accurate and no longer leak __INLINE_ARRAY_* / __VLA_*.
One regression found — see the inline comment.
The stated merge order (release this PR, bump binary-versions.json, then merge the editor PRs) is correct and still necessary: the currently pinned v0.6.6 rejects located arrays.
| // Inline `ARRAY [a..b] OF T`: the AST builder resolves the bounds onto | ||
| // the TypeReference. A named ARRAY type carries none here and is | ||
| // handled by the model path, which resolves the alias first. | ||
| decl.type.arrayDimensions, |
There was a problem hiding this comment.
HIGH — regression: named ARRAY types reach codegen and fail there.
collectLocatedVar passes decl.type.arrayDimensions, which is undefined for a named ARRAY type, while the analyzer's resolveLocatedShape resolves named types through resolveArrayShapeByName. The two disagree.
TYPE Buf : ARRAY [0..9] OF WORD; END_TYPE
VAR_GLOBAL HR AT %MW60 : Buf; END_VAR
This passes semantics and correctly reserves 10 slots (a second variable at %MW65 is properly rejected), but codegen emits one descriptor and calls HR.raw_ptr() on an array:
error: no member named 'raw_ptr' in 'strucpp::IEC_ARRAY_1D<strucpp::IECVar<unsigned short>, strucpp::ArrayBounds<0, 9>>'
On development the analyzer rejected this cleanly with Type 'Buf' is not compatible with address size 'W'. It now fails in the C++ backend leaking internal type names — the exact failure mode this PR set out to remove.
The PR's own test accepts the same shape spelled as a named ARRAY type only asserts compile(source).success and has no CONFIGURATION, so codegen's located path is never exercised. Suggest resolving the named type here the same way resolveLocatedShape does, and extending that test with a CONFIGURATION block.
Hardware validation (SLM-RP4 + P1AM-100)Followed up the review with end-to-end runs on real hardware, using this PR's compiler injected into openplc-editor#1080. Summary: the located-array feature works correctly on both targets, and the named-ARRAY-type defect is confirmed as a regression that reaches the device. ✅ Located arrays work end-to-endProject with Each element lands on its own register/bit, the array claims exactly its 10 registers and no more, and the bit array is placed correctly within the byte. The same shape ( ❌ The named-ARRAY-type regression reaches the deviceSame project with LocatedVar locatedVars[10] = { // ← 10, should be 19
{ LocatedArea::Output, LocatedSize::Word, 0, 0, ... }, // HR AT %QW0 ← ONE descriptor for 10 elements
...
Confirmed as a regression against the currently pinned release — v0.6.6 rejects the identical source cleanly at the ST level: So the PR turns a precise, actionable ST diagnostic into a C++ template error surfaced from the target's build log — the exact failure mode it set out to remove. Worth fixing before release, since |
Regression found in review. The AST builder writes bounds onto the
TypeReference only for an INLINE `ARRAY [a..b] OF T`; a named type carries
none. `collectLocatedVar` read `decl.type.arrayDimensions` directly, while the
analyzer's `resolveLocatedShape` resolves named types through
`resolveArrayShapeByName` -- so the two passes disagreed:
TYPE Buf : ARRAY [0..9] OF WORD; END_TYPE
VAR_GLOBAL HR AT %MW60 : Buf; END_VAR
Semantics accepted it and correctly reserved ten slots (a second variable at
%MW65 was properly rejected), then codegen emitted ONE descriptor and called
`.raw_ptr()` on the array itself:
error: no member named 'raw_ptr' in 'IEC_ARRAY_1D<IECVar<unsigned short>, ArrayBounds<0, 9>>'
Worse than the state before this branch, where the analyzer refused the
declaration cleanly -- it failed in the C++ backend leaking internal type
names, which is the failure mode this branch set out to remove.
Codegen now resolves the shape by name when the declaration carries no bounds,
mirroring what the analyzer already does.
The existing test missed this because it asserted only `compile(source).success`
with no CONFIGURATION, so codegen's located path never ran. Replaced with one
that emits the path and checks what comes out: ten descriptors over
%MW60-%MW69, ten distinct pointers, and no `.raw_ptr()` on the aggregate.
Verified the generated C++ now passes `g++ -fsyntax-only`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks — the named-ARRAY regression is real and it was mine. Fixed in Cause, as you diagnosed: the AST builder writes bounds onto the Codegen now resolves the shape by name when the declaration carries no bounds, through the same On the weak test — you were right, and that is why it passed. Asserting Verified after the fix:
Worth flagging: my verification here is One consequence to note: the compiler now accepts a named ARRAY type at a location, and the editor still refuses it (a Also corrected the pin in openplc-editor#1080: it said v0.6.4, it is v0.6.6 as you wrote. My local |
Re-tested on hardware after b2d5a1a — regression is fixed ✅Rebuilt the branch (merged with The named-ARRAY-type case now behaves exactly like the inline one. Same project as before, LocatedVar locatedVars[19] = { // was 10
{ LocatedArea::Output, LocatedSize::Word, 0, 0, ... }, // HR[0] AT %QW0
{ LocatedArea::Output, LocatedSize::Word, 1, 0, ... }, // HR[1] AT %QW1
... // HR[9] AT %QW9
locatedVars[0].pointer = HR.value[0].raw_ptr(); // was HR.value.raw_ptr()Flashed to the SLM-RP4 and read back over Modbus TCP — holding registers 0..9 = 1000..1009, registers 10..19 untouched, register 20 the incrementing counter, coils 80..87 = No new regressions
Rejection paths still clean and precise, with no internal type names leaking: The overlap message is correct through a named type too ( Probed the new
|
Concurrency review + multi-threaded hardware testChecked whether located arrays respect the threading model, then ran a genuinely multi-task project on an SLM-RP4 (runtime v4.2.0). Verdict: the feature respects every existing concurrency boundary. No new shared mutable state, no new races. One limit is worth documenting explicitly, plus two pre-existing scaling notes. Why it's safe: an array is N independent located scalarsBoth halves of the protection apply per element, unchanged: Program side — every element access on a located global goes through the per-global mutex. From the generated code: for (I = 0; I <= 9; I++) {
auto __gwv_0 = FASTTICK->read();
FASTARR->with_lock([&](auto* __glk){ (*__glk).at(I) = __gwv_0; });
}The value is computed outside the lock and only the store is inside, so no lock is ever nested with another global's — the deadlock-freedom argument in Dispatcher side — Three more things I checked specifically because arrays could have broken them:
Hardware testTwo tasks, deliberately near-equal rates for maximum interleaving:
Forcing also behaves under contention: forcing The one limit worth stating in the docsPer-access validity, not whole-array atomicity. This is the documented Two pre-existing scaling notes (not regressions — flagging because arrays make them easy to hit)
Neither blocks this PR. |
Resolves the compiler half of openplc-editor#565 ("Runtime compile error when using array type of Holding Registers").
{area, size, index, pointer}row per slot, so an array is N rows rather than a new mechanism.HR AT %MW60 : ARRAY [0..66] OF WORDnow expands to 67 descriptors over%MW60..%MW126.slotCountconsecutive slots, sox AT %MW60 : ARRAY [0..66] OF WORDcollides with a plainy AT %MW61 : WORDeven though the two addresses differ. Comparing addresses for equality — all that was needed while every declaration took one slot — would have let the second variable silently share storage with an element of the first. Kept per size class:%MW0and%MD0index different runtime arrays and still do not collide.findIndexmatching on variable name. With N descriptors sharing one name that resolves every element to slot 0 — element 0 bound N times, elements 1..N-1 left null. Now walks the array by position; the index is right there.Rejections that stay, with their own message
Multi-dimensional, variable-length (
ARRAY [*]), and non-constant bounds have no linear run of addresses to occupy. Each is refused with a sentence saying so, rather than falling through to the type-compatibility error — which is what leaked the internal__INLINE_ARRAY_WORD/__VLA_1D_WORDspelling at the user in the first place. A mismatched element type now readsArray element type 'BOOL' is not compatible with address size 'W', naming the half of the declaration that is actually wrong.Verification
tests/semantic/located-variables.test.ts: 73 green, 15 of them new (element-type validation, namedARRAYTYPE, range collisions, the rejected shapes, bit arrays crossing a byte boundary, one descriptor per element with distinct pointers).tests/semantic+tests/backend+tests/integration),tsc --noEmitclean, no new lint warnings (111 before and after).distinct=67is the line that pins thefindIndexfix — before it, all 67 descriptors pointed at element 0.Downstream
The editor accepts a located array in its variables table only once it bundles a release containing this. The paired editor PR carries that UI change already, so this needs to be released and the editor's
binary-versions.jsonpin bumped before that PR merges — otherwise the editor accepts the declaration and the bundled v0.6.4 rejects it at compile time.🤖 Generated with Claude Code