Skip to content

fix: preserve order and scope of recursive groups in renameShadowedNames#147

Merged
Unisay merged 2 commits into
mainfrom
issue-133/rename-rec-groups
Jul 2, 2026
Merged

fix: preserve order and scope of recursive groups in renameShadowedNames#147
Unisay merged 2 commits into
mainfrom
issue-133/rename-rec-groups

Conversation

@Unisay

@Unisay Unisay commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Closes #133.

renameShadowedNamesInExpr mishandled 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 local RecursiveGroup came out with its members in reverse order. Member order is the initialization order computed by the laziness transform, so an eager member (for example the name = 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:

  • Three unit tests in the "renames shadowed names" block cover order preservation, self-references and forward references. All three failed on the old code.
  • A new golden module Golden.RecGroupOrder pins 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 a Lazy_record/record pair whose order is load-bearing. With the fix reverted, the generated Lua crashes with attempt to call local 'Lazy_record' (a nil value); with the fix it prints ok!.

The Golden.RecursiveBindings churn 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.

Unisay added 2 commits July 2, 2026 21:55
…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.
@Unisay Unisay requested a review from Copilot July 2, 2026 20:02
@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 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 RecursiveGroup handling in renameShadowedNamesInExpr to (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.

@Unisay Unisay merged commit 44bdbd3 into main Jul 2, 2026
3 checks passed
@Unisay Unisay deleted the issue-133/rename-rec-groups branch July 2, 2026 20:12
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.

renameShadowedNames miscompiles local recursive groups: member order reversed, member RHSs renamed in the wrong scope

2 participants