Skip to content

fix: unshift dropped Let binders and resolve body scope per let* in IR DCE#148

Merged
Unisay merged 1 commit into
mainfrom
issue-134/dce-let-scope
Jul 2, 2026
Merged

fix: unshift dropped Let binders and resolve body scope per let* in IR DCE#148
Unisay merged 1 commit into
mainfrom
issue-134/dce-let-scope

Conversation

@Unisay

@Unisay Unisay commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Closes #134.

Two defects in the Let handling of Language.PureScript.Backend.IR.DCE, both of the De Bruijn bookkeeping class of #37 and #56.

The first: dropping a dead binding from a Let removed a slot from that name's De Bruijn namespace without lowering the references that skipped over it, unlike the Abs case (fixed in #56). A dangling reference either crashed the Lua codegen with UnexpectedRefBound or silently resolved to the wrong binder. The Let case of dceAnnotatedExp now walks the groupings left to right and, for every dropped binder, unshifts the rest of the Let (later grouping RHSs and the body). Rather than re-implementing the scope threading, the fix reuses the unshift traversal by wrapping the remainder back into a Let, so the bookkeeping follows Note [Sequential scoping of Let bindings] by construction. Dead recursive-group members get the same treatment.

The second: the scope the Let body was resolved against in adjacencyListForExpr was built with a foldr, putting the first binding of a name at index 0 where the let* convention requires the last. Reachability thus marked the wrong binding live among same-name siblings. The body now uses the same scope that adjacencyListForGrouping threads through the groupings one line below.

Four regression tests were written red first: body scope resolution among same-name siblings, unshift of the body after dropping a dead shadowing binder, unshift of later sibling RHSs, and unshift after dropping a dead recursive-group member.

Both defects are latent on the current corpus, and the fix produces no golden churn: same-name Let siblings cannot be produced from source today, and the dangling-reference path needs an inliner-created shadowing binder whose uses later die. An end-to-end golden can follow once a source-level reproduction for that path is pinned down, as noted in the issue.

…R DCE (#134)

Two defects in the Let handling of IR dead code elimination, both of
the De Bruijn bookkeeping class of #37/#56:

1. Dropping a dead binding from a Let removed a slot from that name's
   De Bruijn namespace without lowering the references that skipped
   over it, unlike the Abs case (fixed in #56). A dangling reference
   either crashed the Lua codegen with UnexpectedRefBound or silently
   resolved to the wrong binder. The Let case of dceAnnotatedExp now
   walks the groupings left to right and, for every dropped binder,
   unshifts the rest of the Let (later grouping RHSs and the body).
   The unshift traversal is reused by wrapping the remainder back into
   a Let, so the scope threading follows
   Note [Sequential scoping of Let bindings] instead of being
   re-implemented.

2. The scope the Let body was resolved against in adjacencyListForExpr
   was built with a foldr, putting the *first* binding of a name at
   index 0 where the let* convention requires the *last*. Reachability
   thus marked the wrong binding live among same-name siblings. The
   body now uses the same scope that adjacencyListForGrouping threads
   through the groupings.

Both defects are latent on the current corpus (no golden churn):
same-name Let siblings cannot be produced from source today, and the
dangling-reference path needs an inliner-created shadowing binder
whose uses later die. Regression tests pin the semantics at the DCE
level; an end-to-end golden can follow once a source-level
reproduction for the second path is pinned down.
@Unisay Unisay requested a review from Copilot July 2, 2026 22:13
@Unisay Unisay self-assigned this Jul 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes two De Bruijn index bookkeeping defects in the IR dead-code-elimination pass (Language.PureScript.Backend.IR.DCE) specifically for Let bindings, aligning both reachability analysis and binder dropping behavior with Note [Sequential scoping of Let bindings]. It adds focused regression tests to prevent reintroducing the same class of Let scoping/unshift bugs implicated in #134 (and previously #56-style failures).

Changes:

  • Fix DCE’s Let rewriting to unshift later RHSs/body when a dead Let binder (or recursive-group member) is dropped, preventing dangling/wrong binder references.
  • Fix reachability graph construction so Let bodies resolve same-name binders innermost-first (let*) by using the scope threaded through groupings (instead of a reversed foldr-built scope).
  • Add four regression tests covering body-scope resolution and unshift behavior for dropped binders (including recursive groups), plus a changelog fragment.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
lib/Language/PureScript/Backend/IR/DCE.hs Corrects Let DCE binder dropping (with tail unshift) and fixes body-scope construction for reachability to match let* semantics.
test/Language/PureScript/Backend/IR/DCE/Spec.hs Adds targeted regression tests for Let body scope ordering and unshift-after-drop behavior (standalone + recursive group).
changelog.d/20260702_130000_unisay_dce_let_scope.md Documents the two Let-handling fixes and their user-visible impact (crash vs silent misbinding).

@Unisay Unisay merged commit 91e2f0d into main Jul 2, 2026
3 checks passed
@Unisay Unisay deleted the issue-134/dce-let-scope branch July 2, 2026 22:20
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.

IR DCE mishandles Let bindings: no unshift after dropping a dead shadowing binder, body scope resolved in reverse

2 participants