Fix forward function references at module scope#263
Conversation
In CPython, names inside function bodies are resolved at call time, so `def foo(): return bar()` works even when `bar` is defined after `foo`. Monty's prepare phase takes a snapshot of the global name_map when processing each function def, but this snapshot only contained names processed so far — causing forward references to fail with NameError. Fix: add a pre-scan pass that collects all module-level names before the main prepare pass, ensuring the global_name_map is complete when any function body is prepared. Recurses into control flow blocks (if/for/while/try) since Python has no block scoping.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Replace hand-rolled AST traversal with the existing collect_scope_info_from_node helper. This: - Eliminates structural duplication (106 → 25 lines) - Fixes incorrect SubscriptAssign/SubscriptOpAssign registration - Adds walrus expression support for free - Avoids unnecessary to_string() allocation via contains_key(&str)
|
I created this issue (#183) and I've reviewed the approach. It looks clean and correct. The pre-scan reuses existing @davidhewitt would you be able to take a look please? |
|
Thanks for picking this up @wsenn! |
|
Thanks for the PR @wsenn and the ping @rsr5. I have taken a look and I think this solves part of the problem but not the full picture when interacting with builtins. e.g. cpython handles the following code fine, but neither monty with this patch nor without works properly. def call_sum():
# this will resolve as the builtin sum until the `def sum` later on
return sum([1, 2, 3])
# with this patch, monty fails on this assert with `NameError`
assert call_sum() == 6
def sum(*args):
return 42
# previously, monty would fail on this assert because it would still call the builtin `sum`
assert call_sum() == 42 |
|
I will explore possible solutions and either send review feedback to this PR, push to this PR, or push a new PR, whichever I think is easiest. |
|
I opened #469 to address this case with a more comprehensive set of adjustment. Thanks again both! |
Closes #183
Summary
def foo(): return bar()works even whenbaris defined afterfooname_mapwhen processing each function def, but the snapshot only contained names processed so far — causing forward references to fail withNameErrorApproach
Added a
pre_scan_module_namesmethod that reuses the existingcollect_scope_info_from_nodehelper to collect all names that will be assigned at module level, then pre-allocates namespace slots for each. This runs beforeprepare_nodes, ensuring theglobal_name_mapsnapshot passed to each function body is complete.By reusing
collect_scope_info_from_node, the implementation:SubscriptAssign/SubscriptOpAssign(which modify containers, not create new names)Test plan
function__ops.py: basic forward ref, chained forward refs, forward ref with arguments, forward ref to global variable, mutual recursionmath__module.pyunrelated to this change)ref-count-panictests pass with no leakscargo clippyclean