Repository navigation
Release 0.6.9 - #243
Merged
Merged
Release 0.6.9#243
Conversation
…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
This reverts commit 73951a9.
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes
developmenttomainfor the 0.6.9 release. Taggingv0.6.9onmainafterwards 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 anAToperand, through one shared message — because an alias is not an address.Also in #242, from its review:
$'escapes a quote, sos : STRING := 'It$'s (*';no longer reports an unclosed block comment, and a genuinely unclosed(*further down keeps its own diagnostic;VAR_GLOBALwarns that the compiler cannot bind it rather than dropping the address in silence (only CONFIGURATION globals reachlocatedVars[]/locatedGlobals[]);#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) andtest:coverageall green locally — 2,435 tests across 97 files.🤖 Generated with Claude Code
https://claude.ai/code/session_01464qguGj42gWdmL2ZoK6Uh