Skip to content

lowering: close the local-binder module-alias shadowing residual - #18

Merged
simontreanor merged 1 commit into
mainfrom
fix-local-binder-shadowing
Jul 27, 2026
Merged

lowering: close the local-binder module-alias shadowing residual#18
simontreanor merged 1 commit into
mainfrom
fix-local-binder-shadowing

Conversation

@simontreanor

Copy link
Copy Markdown
Owner

Closes the residual from the module-alias shadowing fix: any binder named like an imported module alias - parameter, block let anywhere in the function (Python hoists locals, so even one after the qualified call), lambda parameter, match-pattern capture (function-level or module-level), or native-CE binder (let! ids becomes a case Ok(ids): capture inside the generated def) - now forces import ids as _pf_ids, with every qualified reference routed through the alias.

Design note: the check is a whole-module binder walk (collect_binders, which enters nested scopes, unlike scan_scope) feeding one binder_names set. That is a deliberate overapproximation: one collision anywhere in the module costs only the alias spelling in the emitted imports, and it keeps the check fully independent of the scope machinery the fold pass and closure capture rely on. The first attempt threaded richer frames through fn_local_stack and the fold pass''s P8 rebind check immediately became conservative enough to reject the network-rail dedupLegs shape - the pinned differential test caught it, and the whole-module design avoids that class of interaction entirely.

Tests: one per-shape regression test covering all six binder kinds end-to-end through Python, plus the existing collided/uncollided tests. Full suite green; clippy clean. ROADMAP entry closed.

Any binder named like an imported module alias now forces the aliased
import (import ids as _pf_ids): parameters, block lets anywhere in a
function (Python hoists locals, so position does not matter), lambda
parameters, match-pattern captures at any level including module level,
and native-CE binders (a `let! ids` lowers to a `case Ok(ids):` capture
inside the generated def).

Design: a whole-module binder walk (`collect_binders`, entering nested
scopes unlike `scan_scope`) feeds one `binder_names` set consulted by
`py_module_ref`. Deliberately an overapproximation - one collision
anywhere costs only the alias spelling - which keeps the check
independent of the scope machinery the fold pass (P8 rebind check) and
closure capture rely on. A first attempt that enriched `fn_local_stack`
frames instead made the fold pass conservatively reject the dedupLegs
shape; the pinned test caught it.

ROADMAP: the module-alias shadowing entry is now fully closed.
@simontreanor
simontreanor merged commit 46bd8a9 into main Jul 27, 2026
11 checks passed
@simontreanor
simontreanor deleted the fix-local-binder-shadowing branch July 27, 2026 10:10
simontreanor added a commit that referenced this pull request Jul 31, 2026
Two user-visible additions since 0.2.0 -- the `input` prelude builtin
(`string ->{io} string`, #19) and caller-supplied `extern` keyword slots
(`kw = ...`, #21) -- plus five fixes, four of them from the first program
written in Pyfun in anger:

* imported schemes reused the exporting module's type-variable ids, silently
  blocking generalization in the importing module and cascading to its
  consumers (#26 / #27)
* a recursive function of arity >= 2 could not perform any effect -- the shape
  of every loop in a language with no `while` (#24 / #28)
* a module that only matched an imported `Option` emitted `None_` without
  importing it (#25 / #29)
* user identifiers collided with Python keywords, builtins the emitter calls,
  and modules an `extern` imports (#23 / #30)
* the local-binder module-alias shadowing residual (#18)
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