feat(ast,resolve): make if/else branch bodies real namespaces - #28
Conversation
Add ast.IfBranchNode so each branch of an if action is an element that can own a scope. Declarations in a branch body are members of the branch: they resolve inside it, do not escape to the enclosing behavior or the sibling branch, and are body-local (excluded from recursive imports and the REPL scope-tree search), matching loop bodies. Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
Original prompt from Devin Bot
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Summary
Declarations inside an
if/elsebranch body were registered nowhere: the AST had no per-branch node, so nothing could own a scope for them (docs/SPEC_COMPLIANCE.md❌ row). This adds that node and makes each branch a real, body-local namespace.The node
Both branch bodies were parsed by two copies of the same loop; they are now one
parseIfBranch(kind, start, closeMsg). The then branch's span starts at its{, the else branch's at theelsekeyword, so an editor position inside a branch maps to the branch.The node carries syntax only — no semantic data — and stays immutable. Membership it owns: everything its body declares. The scope is created by
symbols/builder.go:i.e. exactly the shape
WhileLoopActionNodealready uses — an anonymous child scope keyed by the node, found again via the existingchildScope(scope, node)helpers inresolve/document.go,symbols/bodyscopes.goandlsp/walk.go. Soif c { action a; } else { action a; }declares two distinctas, and neither is a member of the enclosing behavior. The condition keeps resolving in the enclosing scope, since it is evaluated before either branch is entered.Body-local: deliberate exclusion from recursive search
The branch scopes are marked
markBodyLocal(), soimport P::**(resolve/unqualified.golookupInSubtree) and the REPL%evalscope-tree search (repl/meta.golookupInScopeTree) both skip them — a branch-local name is not a member of the namespace being imported. Chosen for parity with loop bodies and body-expression parameters, and pinned by extending the two existing tests (TestImportRecursiveSkipsBodyLocalNames,TestLookupInScopeTreeSkipsBodyLocalNames) withthenLocal/elseLocal.Lowering / control flow
Unaffected.
internal/core/lowerdoes not consumeIfActionNodetoday (there is noiflowering; the guarded-succession shorthandif x then t;parses to aControlFlowEdge, a path this PR does not touch). Branch bodies were never walked byToActionGraph, so wrapping them in a node removes nothing; wheniflowering lands,Branches()is the entry point and the branch node is the natural owner of the branch's subgraph. Succession lowering inside a branch body is likewise unchanged — the members are the same nodes, just reachable through the branch.LSP (same PR, per the resolver-change rule)
lsp/walk.go's reference walker now descends into each branch through the branch's scope. No symbol is synthesized for a branch (the scope is anonymous, like a loop's), so there is noNameSpan/DocNameto stamp; the branch's members are ordinary symbols and already carry both.internal/lsp/if_branch_test.gocovers hover, go-to-definition and rename from both a declaration and a use, on a model that declaresbrakein the then branch, in the else branch, and in the enclosing package — definition from each use lands on that branch's own declaration, and rename touches only that branch.ast.Dumplearned both nodes so the golden fixtureparse/action_if_branch_body.goldenlocks the parse structure instead of printing(*ast.IfActionNode).Verification
CI was not used for this PR (CI is down for this repo); every gate below was run locally on this branch.
Targeted suites for the tiers this PR touches:
OMG training corpus gate (not run by CI; corpus fetched with
./scripts/download-training-examples.sh):Unchanged from the
main@97b5edd baseline — no drift,training_examples_expected.txtuntouched.docs/SPEC_COMPLIANCE.md's ❌ row is now ✅ with the implementation/test mapping and the recursive-import decision recorded.Link to Devin session: https://nasa-jpl-demo.devinenterprise.com/sessions/2dc484815bec4dae9051904854c9ad3c
Requested by: @HuiJun