Skip to content

fix(lsp): resolve alias locations and let strucpp own array resolution - #966

Merged
thiagoralves merged 2 commits into
developmentfrom
bugfix/lsp-alias-location-resolution
Jul 28, 2026
Merged

fix(lsp): resolve alias locations and let strucpp own array resolution#966
thiagoralves merged 2 commits into
developmentfrom
bugfix/lsp-alias-location-resolution

Conversation

@thiagoralves

@thiagoralves thiagoralves commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Editor half of the shared-surface fix. Mirrors openplc-web#629 byte-for-byte and requires strucpp v0.6.1 (STruCpp#200).

Two LSP defects that both broke autocomplete and painted graphical boxes red.

1. Alias-bound locations broke the whole VAR block

A variable's location holds either a producer alias name or a literal %addr. The LSP documents (POU stubs, graphical scope-query docs, resource-globals CONFIGURATION) serialized it verbatim, emitting AT label2 — not valid IEC ST. strucpp abandons the entire VAR block on it:

Expected `DirectAddress`, found identifier `LABEL2`.
Unexpected identifier `LABEL3` after `;` while parsing a statement.
Symbol 'LABEL2' already defined in scope 'MAIN'

Every symbol after the first alias-bound variable fell out of scope — autocomplete offered at most one candidate, unrelated variables read as undefined, their boxes painted red.

The codebase already holds the invariant that the compiler never sees aliases (getCompileReadyProjectData). The LSP is a third consumer that must not either; it was missed. Now resolved at the LSP serialization boundary only: literal passes through verbatim, live alias becomes its address, orphaned alias drops the AT clause.

generateIecVariablesToString is deliberately unchanged — persistence and the round-trippable variables text view must stay verbatim or alias bindings would be destroyed on save. The store keeps the alias-name form throughout: table, text view and saved file all still show label2, never %IW0.

projectActions.getAliasIndex() exposes the index memoized on producer-state identity (the LSP reconciles on every POU mutation — rebuilding the address registry per keystroke would be far too costly). A subscription on it re-publishes when recalculateIecAddresses moves an address without touching any POU.

Resolution only rewrites text within a declaration line, so bodyLineOffset and the pouvars:// diagnostics mirror are unaffected.

2. Arrays were erased by strucpp's completion surface

strucpp published the internal __INLINE_ARRAY_BOOL as a variable's type, discarding the element type and bounds. So someArray never matched a BOOL box, and someArray[0] resolved to nothing.

Fixed in the compiler (STruCpp#200). Here, the client-side workarounds go away: resolveScopeExpressionType no longer strips subscripts and regex-parses an element type out of the detail string — arr[3] matches an LSP symbol by label like any other expression. The old arrayElementType() looked for an ARRAY [..] OF <type> rendering strucpp never actually emitted.

Deferring to the LSP also makes bounds authoritative: arr[99] isn't a symbol, so it resolves unknown and the box is flagged. The subscript heuristic accepted any index.

expandArrayVariable / expandArrayVariables — a second source of truth for index enumeration, with no caller since the graphical boxes moved to LSP-backed completion — removed with their tests. resolveArrayVariableByName and friends stay for now: they serve the synchronous render path in the ladder/FBD node utilities, which would need to become async to consume LSP types. Worth a follow-up.

Verification

Behaviour was verified end-to-end in openplc-web (shared UI layer and shared LSP code — see #629 for the walkthrough). Here: tsc clean apart from the 4 pre-existing @xyflow OnNodeDrag errors, validate:arch passes, and the mirrored jest suites pass (array-variable-utils 31, project-slice 287, pou-signature-serializer + resource-globals-serializer + project-sync 51).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Alias-based variable locations now resolve to the correct IEC addresses in editor diagnostics, scope queries, and generated declarations.
    • Changes to device or I/O alias mappings now refresh related editor information automatically, preventing stale addresses.
    • Array element types are resolved more accurately; invalid or out-of-range indexes report as unknown instead of being inferred incorrectly.
  • Chores

    • Updated the strucpp binary version from v0.6.0 to v0.6.1.

thiagoralves and others added 2 commits July 27, 2026 16:13
…ucpp

A variable's `location` holds EITHER a producer alias name OR a literal
`%addr`. The LSP documents (POU signature stubs, graphical scope-query
docs, the resource-globals CONFIGURATION) were serializing that field
verbatim, emitting `AT label2` — not valid IEC ST. strucpp abandons the
whole VAR block on it, so every symbol declared after the first
alias-bound variable falls out of the POU's scope: LD/FBD autocomplete
offers at most one candidate, unrelated variables read as undefined, and
their boxes paint red.

The codebase already holds the invariant that the compiler never sees
aliases (`getCompileReadyProjectData`). The LSP is a third consumer that
must not either — it was missed. Resolve at the LSP serialization
boundary only: a literal passes through verbatim, a live alias becomes
its address, an orphaned alias drops the `AT` clause.

Resolution only rewrites text within a declaration line, so
`bodyLineOffset` and the `pouvars://` diagnostics mirror are unaffected.

The store keeps the alias-name form throughout — the variables table,
its text view, and the saved file all still show `label2`, never `%IW0`.
`generateIecVariablesToString` is deliberately unchanged: persistence and
the round-trippable text view must stay verbatim or alias bindings would
be destroyed on save.

`projectActions.getAliasIndex()` exposes the index, memoized on
producer-state identity — the LSP reconciles on every POU mutation, and
rebuilding the address registry per keystroke would be far too costly.
A subscription on that index re-publishes the documents when a
producer-only change (`recalculateIecAddresses`) moves an address
without touching any POU.

Mirrors openplc-web byte-for-byte (shared surface).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
strucpp 0.6.1 stops publishing the internal `__INLINE_ARRAY_<T>` name on
its LSP surface and lists each in-bounds array element as its own symbol
typed as the element type. That is everything the graphical editors
needed, and it removes the reason the editor was deriving array facts
itself.

`resolveScopeExpressionType` no longer strips subscripts and regex-parses
an element type out of the detail string: `arr[3]` now matches an LSP
symbol by label like any other expression. The old `arrayElementType()`
looked for an `ARRAY [..] OF <type>` rendering that strucpp never actually
emitted, which is why an array-element box always painted red.

Deferring to the LSP also makes array bounds authoritative — `arr[99]`
isn't a symbol, so it resolves `unknown` and the box is correctly flagged.
The subscript-stripping heuristic accepted any index.

With completion candidates now coming from the compiler, the local
index-enumeration helpers (`expandArrayVariable` / `expandArrayVariables`)
are a second source of truth for something strucpp owns, and have had no
caller since the graphical boxes moved to LSP-backed completion. Removed
with their tests.

`resolveArrayVariableByName` and friends stay for now: they serve the
synchronous render path in the ladder/FBD node utilities, which would need
to become async to consume LSP types. Tracked separately.

Requires strucpp v0.6.1 (binary-versions.json).
Mirrors openplc-web byte-for-byte (shared surface).

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

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR adds memoized alias-to-address resolution across project state and ST-LSP serialization, updates graphical scope typing to trust LSP completions, removes frontend array expansion helpers, extends coverage, and updates the recorded strucpp binary version.

Changes

Alias resolution pipeline

Layer / File(s) Summary
Memoized alias index contract and cache
src/frontend/store/slices/project/*, src/frontend/store/__tests__/project-slice.test.ts
The project store exposes a memoized alias-to-IEC-address index and reuses it across unchanged producer state.
Alias-aware ST serialization
src/frontend/utils/PLC/*serializer.ts, src/frontend/utils/PLC/__tests__/*serializer.test.ts
POU signatures, scope-query documents, and resource globals resolve aliases to literal IEC addresses before emitting ST.
LSP alias propagation and resynchronization
src/frontend/services/st-lsp/*, src/frontend/services/st-lsp/__tests__/project-sync.test.ts
Scope queries and warm-up documents receive the alias index, while producer alias changes trigger resource and POU reconciliation.

Scope and array resolution

Layer / File(s) Summary
Completion-based scope type resolution
src/frontend/services/graphical-scope.ts
Scope typing matches expression segments directly against LSP candidates instead of parsing array subscripts client-side.
Indexed-element utility simplification
src/frontend/utils/PLC/array-variable-utils.ts, src/frontend/utils/PLC/__tests__/array-variable-utils.test.ts
Autocomplete array-expansion helpers and their tests are removed; parsing, validation, and single-element resolution remain.

Binary version metadata

Layer / File(s) Summary
strucpp version metadata
binary-versions.json
The recorded strucpp version changes from v0.6.0 to v0.6.1.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant projectActions
  participant attachProjectSync
  participant STSerializer
  projectActions->>attachProjectSync: provide alias index
  attachProjectSync->>STSerializer: serialize POU or resource globals
  STSerializer-->>attachProjectSync: emit literal IEC addresses
  projectActions->>attachProjectSync: notify alias index change
  attachProjectSync->>STSerializer: reserialize affected documents
Loading

Possibly related PRs

Suggested reviewers: dcoutinho1328

Poem

A rabbit mapped aliases bright,
And turned %IW0 into light.
Arrays thinned, scopes learned to see,
ST stubs hopped synchronously.
strucpp wore a newer coat—
“v0.6.1!” I softly wrote.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the two main changes: alias location resolution and delegating array resolution to strucpp.
Description check ✅ Passed The description thoroughly explains the alias and array fixes plus verification, but it omits the template's References and DOD checklist sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/lsp-alias-location-resolution

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/store/slices/project/slice.ts`:
- Around line 298-346: Move aliasIndexCache out of module scope and into the
per-store closure used by createProjectSlice, ensuring getMemoizedAliasIndex
accesses that store-local cache. Preserve the existing identity checks and
rebuild behavior, while preventing alias-index reuse between separate project
stores.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2535369c-c197-4940-8017-cdde509a79a4

📥 Commits

Reviewing files that changed from the base of the PR and between 89998a1 and cfa6479.

📒 Files selected for processing (14)
  • binary-versions.json
  • src/frontend/services/graphical-scope.ts
  • src/frontend/services/st-lsp/__tests__/project-sync.test.ts
  • src/frontend/services/st-lsp/index.ts
  • src/frontend/services/st-lsp/project-sync.ts
  • src/frontend/store/__tests__/project-slice.test.ts
  • src/frontend/store/slices/project/slice.ts
  • src/frontend/store/slices/project/types.ts
  • src/frontend/utils/PLC/__tests__/array-variable-utils.test.ts
  • src/frontend/utils/PLC/__tests__/pou-signature-serializer.test.ts
  • src/frontend/utils/PLC/__tests__/resource-globals-serializer.test.ts
  • src/frontend/utils/PLC/array-variable-utils.ts
  • src/frontend/utils/PLC/pou-signature-serializer.ts
  • src/frontend/utils/PLC/resource-globals-serializer.ts
💤 Files with no reviewable changes (2)
  • src/frontend/utils/PLC/array-variable-utils.ts
  • src/frontend/utils/PLC/tests/array-variable-utils.test.ts

Comment on lines +298 to +346
/**
* Single-entry memo for the `alias → address` index.
*
* The LSP asks for this index on every project reconcile — which fires on
* every POU mutation, i.e. every keystroke in an ST editor — while
* `buildIecRegistry` does a full migrate + reallocate over every producer.
* The index depends only on producer state (pins, VPP entries, remote
* devices, active board, alias memory), none of which a POU edit touches, so
* an identity-keyed cache turns the hot path into a handful of `===` checks.
*
* Zustand/Immer guarantee reference stability when a slice didn't change, so
* identity comparison is sound; when any input's identity moves, the next
* call rebuilds and replaces the entry.
*/
interface AliasIndexCache {
/** Raw `pinsByBoard[board]` — NOT defaulted to `[]`, which would mint a new
* array identity on every call and defeat the cache. */
pins: ProjectSliceRoot['deviceDefinitions']['pinMapping']['pinsByBoard'][string] | undefined
vendorScreenData: ProjectSliceRoot['deviceDefinitions']['configuration']['vendorScreenData']
remoteDevices: ProjectSliceRoot['project']['data']['remoteDevices']
board: ProjectSliceRoot['deviceDefinitions']['configuration']['deviceBoard']
aliasMemory: ProjectSliceRoot['iecAliasMemory']
index: ReadonlyMap<string, string>
}
let aliasIndexCache: AliasIndexCache | null = null

function getMemoizedAliasIndex(live: ProjectSliceRoot): ReadonlyMap<string, string> {
const board = live.deviceDefinitions.configuration.deviceBoard
const pins = live.deviceDefinitions.pinMapping.pinsByBoard[board]
const vendorScreenData = live.deviceDefinitions.configuration.vendorScreenData
const remoteDevices = live.project.data.remoteDevices
const aliasMemory = live.iecAliasMemory

if (
aliasIndexCache &&
aliasIndexCache.pins === pins &&
aliasIndexCache.vendorScreenData === vendorScreenData &&
aliasIndexCache.remoteDevices === remoteDevices &&
aliasIndexCache.board === board &&
aliasIndexCache.aliasMemory === aliasMemory
) {
return aliasIndexCache.index
}

const index = buildAliasIndex(buildIecRegistry(live))
aliasIndexCache = { pins, vendorScreenData, remoteDevices, board, aliasMemory, index }
return index
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm device-slice defaults for the alias-cache keys are always freshly
# created per slice instantiation (not shared module-level constants), and
# check whether project-slice.test.ts creates a fresh store per test.
fd -e ts -i device -p src/frontend/store/slices
rg -n "pinsByBoard|vendorScreenData" src/frontend/store/slices/device -A3 -B3
rg -n "beforeEach" src/frontend/store/__tests__/project-slice.test.ts -A3 | head -40

Repository: Autonomy-Logic/openplc-editor

Length of output: 15624


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== project slice alias cache area =="
sed -n '260,360p' src/frontend/store/slices/project/slice.ts

echo
echo "== module-level createProjectSlice / getState references around getMemoizedAliasIndex =="
rg -n "function makeStore|function createProjectSlice|const createProjectSlice|export .*ProjectSlice|getState|getMemoizedAliasIndex|aliasIndexCache|buildAliasIndex|buildIecRegistry" src/frontend/store/slices/project/slice.ts -A2 -B2

echo
echo "== tests makeStore definitions/usages =="
sed -n '330,370p' src/frontend/store/__tests__/project-slice.test.ts
rg -n "function makeStore|const makeStore|makeStore\\(" src/frontend/store/__tests__/project-slice.test.ts src/frontend/store -A2 -B2 | head -120

echo
echo "== project slice exports outline =="
ast-grep outline src/frontend/store/slices/project/slice.ts --match getMemoizedAliasIndex --view expanded || true

Repository: Autonomy-Logic/openplc-editor

Length of output: 26889


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== makeStore implementations using project slice and shared data in tests =="
cat -n src/frontend/store/__tests__/project-slice.test.ts | sed -n '1,140p'
cat -n src/frontend/store/__tests__/flow-writeback.test.ts | sed -n '1,90p'
cat -n src/frontend/store/__tests__/alias-location-on-board-load.test.ts | sed -n '1,80p'

echo
echo "== project slice slice factory and getAliasIndex / addIecAliases locations =="
cat -n src/frontend/store/slices/project/slice.ts | sed -n '1650,1705p'
sed -n '195,290p' src/frontend/store/slices/project/slice.ts
sed -n '504,514p' src/frontend/store/slices/project/slice.ts

Repository: Autonomy-Logic/openplc-editor

Length of output: 20609


Move the alias index cache out of module scope.

aliasIndexCache is a shared mutable slot for every createProjectSlice(...) store created in the process. Separate stores can easily share remoteDevices or other producer-state references, causing one store’s alias→address map to be reused by another when resolving compile-ready data. Keep the cache inside the per-store closure or key it through the stable getState reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/store/slices/project/slice.ts` around lines 298 - 346, Move
aliasIndexCache out of module scope and into the per-store closure used by
createProjectSlice, ensuring getMemoizedAliasIndex accesses that store-local
cache. Preserve the existing identity checks and rebuild behavior, while
preventing alias-index reuse between separate project stores.

@thiagoralves
thiagoralves merged commit bc975e9 into development Jul 28, 2026
19 of 20 checks passed
@thiagoralves
thiagoralves deleted the bugfix/lsp-alias-location-resolution branch July 28, 2026 03:52
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