From 0838ddda3124dfd1496009a2659ef7e429c5f72d Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Mon, 8 Jun 2026 13:36:18 -0400 Subject: [PATCH 1/2] fix(transpiler): resolve polymorphic TO_ conversion functions to concrete output types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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_(in: ANY) → ` 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_` 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) --- .../st-transpiler/emit/pou-graphical.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts b/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts index 5f68bd18c..a167781c2 100644 --- a/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts +++ b/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts @@ -26,6 +26,20 @@ interface InterfaceEntry { vars: TranspileVariable[] } +/** + * Destination types of the IEC 61131-3 polymorphic conversion family + * (`TO_BOOL`, `TO_INT`, `TO_UINT`, …). Hard-coded here rather than + * derived at runtime from the catalog so any future addition is visible + * in code review. Kept in sync with `data/std_block_catalog.json` — any + * `_TO_` entry in the catalog implies `TO_` is a valid + * polymorphic conversion target. + */ +const TO_CONVERSION_TARGETS: ReadonlySet = new Set([ + 'BCD', 'BOOL', 'BYTE', 'DATE', 'DINT', 'DT', 'DWORD', 'INT', + 'LINT', 'LREAL', 'LWORD', 'REAL', 'SINT', 'STRING', 'TIME', + 'TOD', 'UDINT', 'UINT', 'ULINT', 'USINT', 'WORD', +]) + /* ─────────────────────────── public entry ───────────────────────────────── */ /** @@ -68,6 +82,15 @@ export function generateGraphicalPou(pou: TranspilePou, project: TranspileProjec // …) collapse to `BOOL`, which matches the corpus where these // operators are always Boolean rung logic. A future // computeConnectionTypes port will narrow these properly. + // 3. Polymorphic IEC 61131-3 type-conversion functions of the + // form `TO_` (TO_INT, TO_UINT, TO_REAL, …) — the + // catalog enumerates the source-specific variants + // (`BOOL_TO_UINT`, `INT_TO_UINT`, …) but NOT the generic + // `TO_` family, so resolveBlockType returns null for + // them. Without this case the synthetic var stayed at + // `ANY` and strucpp rejected the program with + // "Undefined type 'ANY' in PROGRAM" — fixed here by reading + // the destination type directly from the function name. const resolvedSyntheticVars = emitted.syntheticVars.map((sv) => { if (sv.type !== 'ANY' || sv.originBlockTypeName === undefined) return sv const referenced = project.pous.find((p) => p.name === sv.originBlockTypeName) @@ -82,6 +105,10 @@ export function generateGraphicalPou(pou: TranspilePou, project: TranspileProjec return { ...sv, type: collapsed } } } + const polymorphicMatch = sv.originBlockTypeName.match(/^TO_([A-Z]+)$/) + if (polymorphicMatch && TO_CONVERSION_TARGETS.has(polymorphicMatch[1])) { + return { ...sv, type: polymorphicMatch[1] } + } return sv }) From 87d5ba58accaf8e40b679a4bd37ef40b55b3ef44 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Mon, 8 Jun 2026 15:23:35 -0400 Subject: [PATCH 2/2] style(transpiler): prettier reflow of TO_CONVERSION_TARGETS allowlist 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) --- .../st-transpiler/emit/pou-graphical.ts | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts b/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts index a167781c2..0540334e7 100644 --- a/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts +++ b/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts @@ -35,9 +35,27 @@ interface InterfaceEntry { * polymorphic conversion target. */ const TO_CONVERSION_TARGETS: ReadonlySet = new Set([ - 'BCD', 'BOOL', 'BYTE', 'DATE', 'DINT', 'DT', 'DWORD', 'INT', - 'LINT', 'LREAL', 'LWORD', 'REAL', 'SINT', 'STRING', 'TIME', - 'TOD', 'UDINT', 'UINT', 'ULINT', 'USINT', 'WORD', + 'BCD', + 'BOOL', + 'BYTE', + 'DATE', + 'DINT', + 'DT', + 'DWORD', + 'INT', + 'LINT', + 'LREAL', + 'LWORD', + 'REAL', + 'SINT', + 'STRING', + 'TIME', + 'TOD', + 'UDINT', + 'UINT', + 'ULINT', + 'USINT', + 'WORD', ]) /* ─────────────────────────── public entry ───────────────────────────────── */