Skip to content

Release 0.6.9 - #243

Merged
thiagoralves merged 13 commits into
mainfrom
development
Sep 22, 2026
Merged

thiagoralves merged 13 commits into
mainfrom
development

Conversation

@thiagoralves

Copy link
Copy Markdown
Contributor

Promotes development to main for the 0.6.9 release. Tagging v0.6.9 on main afterwards kicks the Build & Release workflow.

What is in it

#242 — AT <identifier>, for the OpenPLC Editor's I/O aliases. The editor binds a variable to a symbolic channel name and resolves it to a real address before asking for a compile. The grammar now accepts that operand so the editor can read its own declarations with this parser instead of maintaining a second one, and the AST carries the spans (addressSpan, nameSpans, the inline ARRAY type) an editor needs to write a declaration back without re-serialising it. A compile still refuses an unresolved alias — in all three places a declaration can hold an AT operand, through one shared message — because an alias is not an address.

Also in #242, from its review:

  • the comment scanner knows $' escapes a quote, so s : STRING := 'It$'s (*'; no longer reports an unclosed block comment, and a genuinely unclosed (* further down keeps its own diagnostic;
  • a string literal is not code when scanning for unclosed comments;
  • a located top-level VAR_GLOBAL warns that the compiler cannot bind it rather than dropping the address in silence (only CONFIGURATION globals reach locatedVars[] / locatedGlobals[]);
  • the CJS parser bundle ships again — the editor's Jest suites load the parser through it, since the ESM chain does not survive Jest's transform.

#241 — CTUD rising-edge guard on the typed counter variants.

Why now

openplc-editor#1130 and openplc-web#772 (DOPE-650) pin this release and cannot merge without it: their declaration parser is this one.

Checks

typecheck, format:check, lint (0 errors) and test:coverage all green locally — 2,435 tests across 97 files.

🤖 Generated with Claude Code

https://claude.ai/code/session_01464qguGj42gWdmL2ZoK6Uh

thiagoralves and others added 13 commits September 21, 2026 11:57
…CU/CD edges

Reported as #217: every down-counting input was edge-detected with
F_TRIG while every up-counting input used R_TRIG, so a down-count landed
when the input pulse ended instead of when it began, one pulse late. The
asymmetry sat inside a single block, CTUD declaring CU_T : R_TRIG next
to CD_T : F_TRIG.

CD_T becomes R_TRIG in all ten down-counters: CTD and CTUD plus their
DINT, LINT, UDINT and ULINT variants. IEC 61131-3 declares both counting
inputs R_EDGE, and CODESYS documents both as "Rising edge", so the two
agree and this is simply a bug.

Making CD rising-edge exposed a second defect in CTUD. It ran its two
counts as independent sequential IFs, so a scan where CU and CD both
rise incremented and then decremented. Mid-range that cancels out and is
invisible; at CV = PV it is not, because the increment is blocked by the
CV < PV guard while the decrement still fires, dropping CV to PV-1. This
was nearly unreachable while CD was falling-edge and is routine once both
inputs are rising-edge, so it ships here. CTUD now uses the standard's
IF NOT (CU AND CD) guard, leaving CV unchanged anywhere in the range.

The counting range is deliberately left as it was. The standard guards
on PVmin and PVmax, the limits of the counter's own type, but CODESYS
stops counting up at PV, which it documents as the "upper limit for
incrementing", and counts down only "as long as CV is greater than 0".
CODESYS gets that range by typing CV and PV as WORD, a deviation from
IEC it documents; we keep the IEC types and express the same range with
the existing PV and 0 guards. Where the standard and CODESYS diverge,
we match CODESYS.

Tests: the two that encoded the falling-edge behaviour are rewritten,
one down to its title and a comment about F_TRIG's memory bit. Six new
tests cover the eight typed variants, CU/CD symmetry, the CV > 0 guard
against underflow on signed and unsigned, the PV cap, LD winning over a
coincident CD edge, and the simultaneous-edge guard both mid-range and
at CV = PV. Reverting CD_T to F_TRIG fails five of the eleven tests;
dropping the CTUD guard fails one.

Refs RTOP-290, closes #217

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The guard was only pinned on the base CTUD. The typed-variant test holds
CU TRUE while CD rises, so CU_T.Q is never set on the same scan and no
coincident edge is ever produced. Mutation proved the gap: reverting
CTUD_DINT alone to the old sequential IFs left all eleven tests passing.

The new test drives each of the four typed variants through a coincident
edge twice, once mid-range and once at CV = PV. The second is the
discriminating case: without the guard the increment is blocked by
CV < PV while the decrement still fires, so CV falls to PV-1.

Mutation results now, one block at a time, rest of the file untouched:

  CD_T : R_TRIG -> F_TRIG     caught in all 10 down-counters
  CTUD guard -> sequential    caught in all 5 CTUD blocks

Refs RTOP-290

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

The 12-line header added with the CODESYS alignment repeated
docs/IEC_COMPLIANCE.md almost verbatim. Two copies of the same rationale
drift apart, and every sibling file in the directory carries a single
one-line header. Replaced with a pointer.

Refs RTOP-290

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WachxZyp81hRPqNUv8kwmM
…ter-rising-edge

fix(std-fb): count on the rising edge of CD, and ignore simultaneous CU/CD edges (RTOP-290)
IEC only allows a `%` location after `AT`. The OpenPLC Editor also lets
a user bind a variable to an I/O *alias* — a symbolic name for a
channel, which the editor resolves to a real address before asking for
a compile.

Because this parser refused that form, the editor had to keep a second,
hand-written parser alive just to read its own declarations, and the
two drifted: the editor accepted declarations the compiler rejected
(`a, b : INT`, `a : BOOL; b : INT;`) and rejected ones it accepted
(`url : STRING := 'http://x'`, where a naive comment pass ate the rest
of the line). Accepting the alias here is what lets that second parser
be deleted, so there is one definition of a declaration and it is the
compiler's (DOPE-650).

Both declaration orderings take it, since both already took the direct
form:

    m AT Motor_Start : BOOL;
    m : BOOL AT Motor_Start;

`addressKind` distinguishes `"direct"` from `"alias"` so a consumer can
tell them apart without re-lexing the operand.

Parsing it is deliberately not compiling it. `analyze` still refuses an
address that is not a well-formed `%` location — an alias that reaches
a real compile was meant to be resolved and was not. The message says
that instead of "Invalid address format", which pointed at the spelling
of something spelled correctly.

Also adds `addressSpan` and `nameSpans`. The lexer folds identifier
case, so `address` and `names` are not what the user typed; an editor
that writes declarations back to disk needs the spans to keep
`Motor_Start` from becoming `MOTOR_START` on the next save. One span
per name, so a multi-name declaration can be split apart faithfully.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01464qguGj42gWdmL2ZoK6Uh
Two things the OpenPLC Editor needs to parse its declarations with this
compiler instead of with a parser of its own (DOPE-650).

**The inline ARRAY span was the declaration's.**
`buildInlineArrayTypeReference` took `nodeToSourceSpan(parentNode)`, so
`a : ARRAY [0..3] OF INT;` reported a type spanning the whole line. A
caller slicing the source by that span got the declaration back where it
asked for the type. Every other TypeReference spans just the type, so
this was inconsistent with itself as well as wrong. It now spans the
array type node, and `parentNode` is gone from the signature since
nothing else used it.

**`dist/parser-bundle.cjs`.** `dist/index.js` is ESM and reaches
chevrotain, which reaches `lodash-es` and `@chevrotain/utils` — both ESM
behind `exports` maps. A consumer running Jest on the default CommonJS
resolver cannot load that chain: widening `transformIgnorePatterns` gets
as far as a resolution failure inside chevrotain's own package exports,
and no Jest configuration fixes it.

The editor is exactly that consumer, and its unit suite has to import
the real parser — mocking it would defeat the point of the change, which
was to stop the editor having a second opinion about what a declaration
is. esbuild inlines the chain into one self-contained file (~1 MB),
built in `prepack` alongside the browser-server bundle and shipped by
the existing `files: ["dist/"]`.

It is a build artifact for loaders that cannot follow ESM. Everything
that can should keep importing `strucpp` normally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01464qguGj42gWdmL2ZoK6Uh
…omments

`findUnclosedBlockComment` walked raw characters. It skipped `//`
comments, for the obvious reason that their contents are not code, but
not string literals — so `s : STRING := '(*';`, a valid IEC
declaration, opened a block comment that never closed and the whole
compilation unit was rejected with "Unclosed block comment" pointing at
a line with no comment on it.

Both parse and compile refused it, so this was not a diagnostic-only
problem: a program a user could legitimately write did not build.

Now skips `'…'` and `"…"` the same way, honouring IEC's doubled-quote
escape. Verified parsing AND compiling for a literal holding `(*`, `*)`,
`http://` (whose `//` is not a comment either), a semicolon, and an
escaped quote. A genuinely unclosed comment is still reported.

Found while pointing the OpenPLC Editor at this parser (DOPE-650): the
editor's own hand-written scanner had the identical bug, which is a
reasonable argument that scanning for comments without knowing what a
string is will keep producing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01464qguGj42gWdmL2ZoK6Uh
It was added so the OpenPLC Editor's Jest suite could load this package, whose
ESM chain Jest could not resolve. The editor resolves it through Jest's own
resolver now — chevrotain's scoped packages just needed mapping to their entry
points — so the bundle has no consumer, and shipping a megabyte of duplicated
parser in the tarball for none is worse than not shipping it.

The two parser fixes in this PR stay: they are what the editor actually needs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01464qguGj42gWdmL2ZoK6Uh
Three findings from the review of this PR.

**The alias message reached one of the three places a declaration can hold
an `AT` operand.** It was taught to `buildVarBlockSymbols`, so a POU-local
`VAR` said what was wrong. A `CONFIGURATION VAR_GLOBAL` — which is where the
editor puts every I/O binding it emits, so it is the path a user actually
hits — still said `Invalid address format: MOTOR_START`, sending them to look
for a typo in a name that is spelled correctly. The wording now lives in one
helper, `unusableAddressMessage`, that all three call.

**A top-level `VAR_GLOBAL` said nothing at all.** That loop never looked at
`decl.address`, so an unresolved alias compiled clean and left the variable
unlocated — the silent failure the alias diagnostic exists to prevent, and
newly reachable because this branch accepts an identifier wherever it accepts
an address. A well-formed `%` address there is no better off: only
CONFIGURATION globals reach `locatedVars[]`/`locatedGlobals[]`, and codegen
emits a top-level global as plain `inline` storage, so the address was
dropped just as quietly. Both are now named. Binding the second one is not a
message fix — it means wiring a scope the runtime contract does not cover —
so it is refused with the one instruction that resolves it: move it to
CONFIGURATION VAR_GLOBAL.

**`findUnclosedBlockComment` did not know `$` escapes a quote.** The token it
guards, `StringLiteral`, accepts `$'`; the scanner counted only a doubled
quote, ended the string at the `$'`, and paired every quote after that the
wrong way. `s : STRING := 'It$'s (*';` was still rejected with "Unclosed
block comment" — the very error this scanner was added to stop — and a
genuinely unclosed `(*` further down could land inside a phantom string and
lose its diagnostic entirely: it surfaced as "Expected `END_PROGRAM`,
found `(`". The scanner now skips the character after a `$`, counting a
line break if that is what it escaped.

`src/version-build.ts` comes along regenerated: it is written from
package.json by the build, and the committed copy still said 0.6.4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01464qguGj42gWdmL2ZoK6Uh
The address is still ignored — only CONFIGURATION globals reach
`locatedVars[]` / `locatedGlobals[]` — and it is still said out loud. But a
program that declares one is otherwise compilable, and refusing it fails
sources that passed before this branch, over a shortcoming of the compiler
rather than a mistake in the source. So it warns, says the address is
ignored, and names the move that would have it located.

An unresolved alias stays an error: there is no address to ignore.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01464qguGj42gWdmL2ZoK6Uh
…entifier-in-at

feat(parser): accept an identifier as the AT operand (OpenPLC alias)
Carries #242: `AT <identifier>` for the OpenPLC Editor's I/O aliases, the
alias diagnostic in all three scopes a declaration can hold an `AT` operand,
and the comment scanner's `$`-escape handling. The editor's DOPE-650 pair
pins this release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01464qguGj42gWdmL2ZoK6Uh
@thiagoralves
thiagoralves merged commit 62e7736 into main Sep 22, 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.

1 participant