Skip to content

Merge development into main for v0.6.2 - #203

Merged
thiagoralves merged 4 commits into
mainfrom
development
Aug 4, 2026
Merged

Merge development into main for v0.6.2#203
thiagoralves merged 4 commits into
mainfrom
development

Conversation

@thiagoralves

Copy link
Copy Markdown
Contributor

Promotes developmentmain to release v0.6.2.

Contents

Only the located-globals work — nothing else is pending on development.

Commit
fb7980f Merge PR #202
a3a5ae4 Merge fix/located-address-validation
cf59629 fix(semantic): close three gaps in located-address validation
c5ccc7f feat(codegen): emit locatedGlobals[] so runtimes stop inferring scope

5 files, +661/−11.

What v0.6.2 delivers

locatedGlobals[] + C-linkage accessors. Codegen now states which locatedVars[] entries are CONFIGURATION VAR_GLOBAL ... AT by recording their canonical storage pointers, so a host runtime resolves config scope by pointer identity instead of inferring it from array position. The OpenPLC v4 runtime's inference was backwards — it assumed program-local entries came first — so one located variable declared in a POU made it conclude a project had zero located globals and silently stop syncing all of them. Reported on the forum as "%MX locations now invalid - Runtime v4".

Tightened located-address validation — three configurations that previously compiled into programs that looked fine and did not work now produce errors:

  • a location on anything other than VAR / VAR_GLOBAL
  • a located variable in a PROGRAM instantiated more than once
  • a POU-local location colliding with a located CONFIGURATION VAR_GLOBAL

Behaviour change for release notes

The validation rejects projects that previously compiled. Editor-authored projects should be unaffected (the editor already blocks located interface classes), but hand-written or imported ST may be. tests/fixtures/smart-traffic-light needed its located I/O moved from VAR_INPUT/VAR_OUTPUT to VAR.

Consumers waiting on this tag

Both editors pin v0.6.2, so the release must be cut as exactly v0.6.2 or their download URL 404s:

Verification

  • 2008 tests pass (79 files), 15 new.
  • End-to-end on a Raspberry Pi with the runtime PR: a Modbus coil write to a located %MX0.0 global reaches program logic and %MD0 is committed back; runtime reports 2 config-scope shared globals where it previously reported 0.
  • Backwards compatibility exercised both ways — an older runtime ignores the new symbols, and a newer runtime warns and runs with globals inert against pre-v0.6.2 generated code.

After merge

Tag v0.6.2 on the resulting merge commit. .github/workflows/release.yml triggers on v*, injects the version from the tag (npm version $VERSION), runs npm pack — whose prepack builds dist/browser-server.js — and attaches strucpp-0.6.2.tgz to the GitHub release, which is what the editors download.

🤖 Generated with Claude Code

thiagoralves and others added 4 commits August 3, 2026 16:33
locatedVars[] mixes two ownership classes: POU-local `VAR ... AT`, serviced
by the owning IEC task around run(), and CONFIGURATION `VAR_GLOBAL ... AT`,
owned by no task and therefore serviced by a host dispatcher at a quiescent
frame boundary. The descriptors carry no scope, and a runtime cannot recover
it -- globals are file-scope GlobalVar<V> singletons that appear nowhere in
the configuration object graph reachable via ConfigurationInstance.

So runtimes inferred the split from array position. The OpenPLC v4 runtime
assumed [program-local ... ][config globals ... ] and took the tail not
covered by any program's located_range() as the globals. We emit config
globals FIRST (emitFileScopeGlobals runs before the program headers), so the
program-local block is always the tail: one POU-local located variable made
the computed count collapse to zero and silently stopped servicing EVERY
located global -- %MX coils written over Modbus never reached the program,
and %IX/%QX/%MW globals stopped being read and committed. Reported on the
forum as "%MX locations now invalid - Runtime v4".

State the scope instead of leaving it to be guessed. locatedGlobals[] holds
the canonical storage pointer of each located VAR_GLOBAL -- the same
raw_ptr() value written into locatedVars[].pointer -- so a runtime resolves
the config-scope entries by pointer identity, with no dependence on array
order. Located variables are always elementary types, so every entry is a
scalar IECVar and raw_ptr() is available. Populated in the configuration
constructor because raw_ptr() is not a constant expression, and to keep it
beside the locatedVars[] population it must agree with.

The extern "C" accessors are emitted here rather than left to a host
runtime's shim. That is what makes the change backwards-compatible in both
directions: the OpenPLC runtime rebuilds its shim against each upload's
generated header, so a shim referencing these symbols would fail to COMPILE
against an older project, whereas an accessor emitted beside its own array
always matches. An older runtime never resolves the symbols and is
bit-for-bit unaffected; a newer runtime probes with dlsym and skips located
globals when they are absent.

Guarded by STRUCPP_THREADED throughout: freestanding targets bind every
located variable directly and have no dispatcher, so they pay nothing --
verified that a non-threaded build emits no locatedGlobals symbol at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
validateLocatedVariables() rejected duplicate addresses across POUs, but
three configurations slipped through and each produced silent misbehaviour
in the runtime rather than a diagnostic.

1. Configuration VAR_GLOBALs were invisible to every located-variable rule.
   They live in ast.configurations[].varBlocks, while buildVarBlockSymbols
   only walks POU scopes (program / function / functionBlock) via
   ast.programs et al, so a located global was never recorded. A POU-local
   `VAR ... AT %MX0.0` alongside a `VAR_GLOBAL ... AT %MX0.0` therefore
   compiled, and the two entries resolve to one image slot -- serviced by
   two different paths, the owning task and the dispatcher at the quiescent
   frame boundary. Collect them (deduped by name, since codegen emits one
   file-scope singleton per name and a global repeated across
   configurations is one canonical global) so they take part in the
   duplicate, type-compatibility and bit-range rules.

2. A located variable in a PROGRAM instantiated more than once was
   accepted. Codegen allocates one locatedVars[] slot per *declaration*,
   and the pointer is assigned in the POU constructor `this`-relative --
   the class cannot name its own instance -- so the last instance
   constructed wins and the other instances' copies are never serviced.
   Verified: two instances of one POU report the same located_range() and
   share one slot, leaving one instance's %IX3.0 orphaned. Same reasoning
   as the existing FUNCTION_BLOCK restriction: a physical address belongs
   to one point of hardware. IEC 61131-3 permits multiple program
   instances and answers per-instance addressing with a partly specified
   location (`AT %I*`) resolved by VAR_CONFIG, which is not supported
   here, so the fully specified form must be rejected.

3. Only VAR and VAR_GLOBAL may now carry a location. Interface sections
   describe a call contract, not hardware, and a located interface
   declaration hands the runtime two owners for one image slot -- a hazard
   invisible from the generated code. VAR_EXTERNAL is the sharpest case:
   it references storage a VAR_GLOBAL owns, codegen emits it as
   `GlobalVar<T>*` and collects located vars from local declarations only,
   so the address is silently dropped while duplicating the global's
   claim. Matches the editor, which rejects the same set at edit and load
   time (DISALLOWED_LOCATION_CLASSES, GitHub issue #904).

   tests/fixtures/smart-traffic-light declared its located I/O in
   VAR_INPUT / VAR_OUTPUT; moved to VAR, which is where it belonged.

addressKey() is deliberately left matching on exact (area, size, byte, bit).
The image is not flat memory: each size class has its own array in the
runtime (bool_memory[][], int_memory[], dint_memory[], lint_memory[]) and
byte_index indexes that array, so %MW0 and %MD0 name unrelated storage
rather than overlapping bytes. Documented on the function so it is not
"fixed" into a range check later.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(codegen): emit locatedGlobals[] and tighten located-address validation
@thiagoralves
thiagoralves merged commit e48bc70 into main Aug 4, 2026
6 checks passed
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