fix(transpiler): resolve polymorphic TO_<TYPE> conversions in JSON→ST emitter - #854
Conversation
…o concrete output types
Reported by user on the desktop editor compiling a project that
worked on web. STruC++ rejected the program with:
main.st:15:30: error: Undefined type 'ANY' in PROGRAM 'MAIN'
15 | _TMP_TO_UINT874561_OUT : ANY;
`ANY` is a generic placeholder in IEC 61131-3 — valid as a function
argument type but never as a variable declaration type. STruC++ is
correct to reject it. The bug is in the editor's JSON→ST
transpiler (PR #843, recently landed): it was leaving synthetic
output temps for polymorphic IEC type-conversion functions
(`TO_INT`, `TO_UINT`, `TO_REAL`, …) at `ANY` instead of resolving
them to their concrete destination types.
The web editor still compiles this project because it uses xml2st
in a worker (PR #482), not the new JSON→ST transpiler — different
compiler, different code path. Once the web side also migrates to
JSON→ST, this fix needs to be mirrored.
## Root cause
`generateGraphicalPou` (emit/pou-graphical.ts:71) resolves `ANY` in
synthetic vars by:
1. Looking for a user-defined project function with that name →
use its `interface.returnType`.
2. Looking the name up in `data/std_block_catalog.json` via
`resolveBlockType` → use the formal output port's type.
3. (Was missing) Fallback: leave as `ANY`.
The catalog has entries for every *source-typed* conversion
(`BOOL_TO_UINT`, `INT_TO_UINT`, `REAL_TO_UINT`, …) — but NOT for the
polymorphic-input shortcuts (`TO_UINT`, `TO_INT`, …). IEC 61131-3
defines `TO_<TYPE>(in: ANY) → <TYPE>` as a generic conversion whose
output type is fixed by the function name, with the input type
inferred from the connected source. The catalog's enumerate-every-
combination approach doesn't capture this family; resolveBlockType
returns null, the fallback kept `ANY`, strucpp rejected.
## Fix
Adds a third resolution case after the catalog lookup fails: if
`originBlockTypeName` matches `^TO_([A-Z]+)$` and the suffix is one
of the 21 known IEC destination types (BOOL, INT, UINT, REAL, DINT,
LREAL, BYTE, WORD, DWORD, LWORD, SINT, USINT, LINT, ULINT, UDINT,
BCD, TIME, DATE, TOD, DT, STRING), use the suffix as the output
type. Otherwise the existing fallback keeps `ANY` and strucpp
reports the clear "Undefined type" error like before — no regression
on truly-unknown names.
The 21-entry allowlist is hard-coded in pou-graphical.ts with a
comment pinning it to the catalog's `*_TO_<X>` set; a comment notes
that future catalog additions need to be reflected here too. This
is intentionally more visible than deriving the set at runtime —
the allowlist serves as documentation of which polymorphic
conversions the transpiler claims to support.
## Why not a deeper fix
A more principled solution would have the walker carry the block's
own declared output type (which IS present in the project JSON —
the FBD node's `variant.variables` lists OUT as `UINT` for this
exact case) all the way through to synthetic-var emit, replacing
the catalog round-trip entirely. That's a walker-side refactor
(walker/ld.ts:687-700 currently only forwards typeName +
numericId + formal parameter name). Out of scope for a targeted
bug fix.
## Verification
`tsc --noEmit` clean. No existing unit tests in the transpiler
subtree — verification is via the user retesting the project that
hit the bug (`/Users/thiagoralves/Downloads/Blink Demo Multilanguage`,
which has two `TO_UINT` blocks at numericId 874561 and 9788614).
Expected result: ST compiles cleanly, both temps now declared as
`UINT` instead of `ANY`.
## Shared-surface mirror
This file is part of the byte-identical shared surface between
openplc-editor and openplc-web. A matching commit on the web side
is required before either branch is merged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughThis PR extends graphical POU emission to properly type synthetic variables created from IEC 61131-3 polymorphic ChangesTO_ Polymorphic Conversion Type Resolution
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
CI Format Check rejected the compact 3-per-line layout. `prettier --write` reflows the 21-element Set to one-per-line. No semantic change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e call names (Autonomy-Logic#944) Blocks like TO_INT/TO_UINT/TO_REAL are placed on the FBD/LD canvas with the generic IEC 61131-3 conversion shorthand as their type name, but that shorthand is not itself a valid ST function -- only the fully qualified <SRC>_TO_<DST> family exists (std_block_catalog.json never has a bare TO_INT entry). The JSON->ST walker emitted the shorthand verbatim as the call name, producing e.g. `TO_INT(O_R)` for a REAL source, which matiec/strucpp reject as an undefined function -- the exact "';' missing at the end of statement" parse error reported in Autonomy-Logic#944 for `TO_INT(O_R)`. PR Autonomy-Logic#854 already fixed the sibling defect where the synthesized output temp's *declared type* stayed ANY for these blocks, but explicitly left the call-name resolution as future work ("computeConnectionTypes port"). This closes that specific gap: emitFunctionCall (walker/ld.ts) now resolves the block's single wired input to its declared type (from a new project/POU variable-type index built in pou-graphical.ts) and substitutes the concrete <SRC>_TO_<DST> name when the catalog has a matching entry, falling back to the original shorthand otherwise so an unresolvable case still surfaces the same "undefined function" error instead of a different wrong one. Scope: this fixes the in-process JSON->ST transpiler (backend/shared/transpilers/st-transpiler/), which is currently opt-in via OPENPLC_USE_NEW_TRANSPILER and is the path PR Autonomy-Logic#854 already targeted. The default legacy pipeline (XmlGenerator -> external xml2st subprocess) has the identical defect inherited from the original Python generator (xml2st's own PLCGenerator.py also passes the block's raw type name through unresolved) and needs a separate fix -- old-editor/fbd-xml.ts has no project-wide variable-type context available at its call site today, so resolving it there needs its own plumbing change. Flagging that as follow-up scope rather than bundling a larger, untested change into this fix. Verification: - New tests reproduce the exact issue Autonomy-Logic#944 scenario (REAL O_R -> TO_INT -> INT output) at both the walker level (ld.test.ts) and the full generateGraphicalPou level (pou-graphical.test.ts); each fails against the pre-fix code (bare TO_INT(O_R)) and passes with the fix (REAL_TO_INT(O_R)) -- confirmed by running both ways. - Added coverage for: unconnected input, unknown source type, no matching catalog entry, non-conversion blocks (ADD-style), sibling conversions (TO_DINT from BOOL), multi-input blocks, and global vs. local variable shadowing. - Full suite: 243 suites / 5216 tests, 0 failures, 0 regressions. - tsc --noEmit and eslint clean on all changed files. Could not run the Electron GUI or the external matiec/strucpp compilers in this environment to compile-verify the generated ST end-to-end; verification is at the transpiler-output level (the generated ST text matches the exact fully-qualified call the catalog and STruC++'s own test suite confirm is valid, e.g. REAL_TO_INT). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
User-reported regression: a project that compiles fine on the web editor failed on the desktop editor with:
The bug is in the new JSON→ST transpiler from PR #843 — synthetic output temps for polymorphic IEC type-conversion functions (
TO_INT,TO_UINT,TO_REAL, …) were left atANYbecause the catalog only enumerates source-specific variants (BOOL_TO_UINT,INT_TO_UINT, …), never the polymorphic shortcuts. STruC++ correctly rejectsANYas a variable declaration type — it's a generic placeholder, not a concrete type.Web compiled the same project because it's still on xml2st in a worker (different compiler, different code path).
Fix
Single file (
src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts), 27 lines added. After the catalog lookup fails, the resolver now matches^TO_([A-Z]+)$against the block name and, if the suffix is one of the 21 known IEC destination types (BOOL/INT/UINT/REAL/DINT/LREAL/BYTE/WORD/DWORD/LWORD/SINT/USINT/LINT/ULINT/UDINT/BCD/TIME/DATE/TOD/DT/STRING), uses the suffix as the output type. Unknown block names still fall through toANYso STruC++ produces the same clear error for truly-unknown blocks — no regression on the unhappy path.Why this is essentially a no-op today
With PR #853 just merged, the editor ships with
OPENPLC_USE_NEW_TRANSPILERdefaulting to OFF — the new JSON→ST transpiler is only exercised when an operator explicitly opts in. Production projects still flow through legacy xml2st. This fix lands the moment-someone-flips-the-toggle behavior we want, without changing default compilation.Test plan
tsc --noEmitcleanOPENPLC_USE_NEW_TRANSPILER=1, compile~/Downloads/Blink Demo Multilanguage(twoTO_UINTblocks at numericId874561and9788614) — should now emit: UINTinstead of: ANYand pass STruC++Shared-surface mirror
emit/pou-graphical.tsis part of the byte-identical shared surface. A matching commit on openplc-web is required (will be opened in tandem so the sync check passes).🤖 Generated with Claude Code
Summary by CodeRabbit