fix: preserve order and scope of recursive groups in renameShadowedNames#147
Merged
Conversation
…mes (#133) The RecursiveGroup branch of renameShadowedNamesInExpr had two defects: 1. The fold consed processed members onto an accumulator and never reversed the result, so every local recursive group came out with its members in reverse order. Member order is the initialization order computed by the laziness transform, so eager members (e.g. the `name = Lazy_name(0)` forcing bindings) could run before the members they read were assigned, producing nil at runtime. 2. Each member's RHS was renamed in the scope accumulated over the previous members only, while recursive-group scoping makes every member visible in every member's RHS, itself included (see Note [Sequential scoping of Let bindings]). When a group member shadowed an outer binder, self- and forward references kept the old name and silently rebound to the outer binder. The branch now brings all the members into scope first (avoiding the names bound anywhere in the group), then renames every RHS in the complete group scope, preserving member order. The Golden.RecGroupOrder test pins the runtime consequence: its lazy self-reference compiles to a Lazy_record/record pair whose order is load-bearing; with the old code the generated Lua crashed with "attempt to call local 'Lazy_record' (a nil value)". The Golden.RecursiveBindings churn is the reversal disappearing: member order now matches what the laziness transform emits.
There was a problem hiding this comment.
Pull request overview
This PR fixes renameShadowedNamesInExpr in the Haskell IR optimizer so that local RecursiveGroup let-bindings preserve member initialization order and correctly rename member RHSs in the full recursive scope, preventing runtime miscompilations (notably “attempt to call a nil value”) and broken recursion when names shadow outer binders.
Changes:
- Reworked
RecursiveGrouphandling inrenameShadowedNamesInExprto (1) bring all group members into scope first and (2) rename all RHSs using the complete group scope while preserving member order. - Added targeted unit tests for recursive-group member order, self-references, and forward references under shadowing.
- Added a new eval golden (
Golden.RecGroupOrder) to pin the runtime consequence; updated existing golden outputs where order reversal previously leaked through.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
lib/Language/PureScript/Backend/IR/Optimizer.hs |
Fixes recursive-group renaming by building full group scope first and preserving member order. |
test/Language/PureScript/Backend/IR/Optimizer/Spec.hs |
Adds regression unit tests covering order preservation and correct scoping for self/forward references. |
test/ps/src/Golden/RecGroupOrder/Test.purs |
New PureScript golden source to exercise order-sensitive recursive initialization. |
test/ps/output/Golden.RecGroupOrder.Test/corefn.json |
CoreFn artifact for the new golden module. |
test/ps/output/Golden.RecGroupOrder.Test/golden.ir |
Golden IR output for the new runtime regression case. |
test/ps/output/Golden.RecGroupOrder.Test/golden.lua |
Golden Lua output demonstrating correct Lazy_record/record initialization order. |
test/ps/output/Golden.RecGroupOrder.Test/eval/golden.txt |
Runtime oracle asserting the program prints ok!. |
test/ps/output/Golden.RecGroupOrder.Test/eval/.gitignore |
Ignores runtime-produced actual.txt for the eval golden. |
test/ps/output/Golden.RecursiveBindings.Test/golden.ir |
Updates existing golden IR to reflect corrected recursive-group member order. |
test/ps/output/Golden.RecursiveBindings.Test/golden.lua |
Updates existing golden Lua to reflect corrected recursive-group member order. |
changelog.d/20260702_120000_unisay_rename_rec_groups.md |
Changelog entry documenting the fix and its impact. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #133.
renameShadowedNamesInExprmishandled local recursive groups in two ways. The fold over group members consed each processed member onto an accumulator and never reversed the result, so every localRecursiveGroupcame out with its members in reverse order. Member order is the initialization order computed by the laziness transform, so an eager member (for example thename = Lazy_name(0)forcing binding of a runtime-lazy group) could run before the member it reads was assigned, and the generated Lua crashed with "attempt to call a nil value". The second defect: each member's RHS was renamed in the scope accumulated over the previous members only, while recursive-group scoping makes every member visible in every member's RHS, itself included (see Note [Sequential scoping of Let bindings]). A group member that shadowed an outer binder had its self- and forward references quietly rebound to that outer binder, severing the recursion.The fix processes each group in two passes: first it brings all the members into scope (avoiding names bound anywhere in the group), then it renames every RHS in the complete group scope. Member order is preserved.
Tests, written red first:
Golden.RecGroupOrderpins the runtime consequence. Its lazy self-reference (a reference under a lambda passed as a function argument, which the laziness analysis treats as potentially forced at initialization) compiles to aLazy_record/recordpair whose order is load-bearing. With the fix reverted, the generated Lua crashes withattempt to call local 'Lazy_record' (a nil value); with the fix it printsok!.The
Golden.RecursiveBindingschurn is the reversal disappearing: member order now matches what the laziness transform emits. Both members there are lambdas, so the old order happened to be harmless.