Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions marimo/_ast/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,13 @@ def _visit_and_get_refs(
unbounded_refs |= set(self.ref_stack[-1])

# Process the function body
if isinstance(node, (ast.AsyncFunctionDef, ast.FunctionDef)):
# A function body can refer to the function's binding in the
# enclosing scope. Register the name before visiting the body so
# a nested private function's self-references are not mistaken
# for cell-local references. Variable metadata is attached after
# the body has been visited and its references are known.
self.block_stack[-1].defs.add(node.name)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: When a function is declared directly in a class, this line marks the method name as defined in the class block, hiding unqualified references to that name inside the method. Python method bodies do not resolve bare names through the class namespace, so preserve the reference unless the function is nested in a lexical function scope rather than directly in a class.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At marimo/_ast/visitor.py, line 606:

<comment>When a function is declared directly in a class, this line marks the method name as defined in the class block, hiding unqualified references to that name inside the method. Python method bodies do not resolve bare names through the class namespace, so preserve the reference unless the function is nested in a lexical function scope rather than directly in a class.</comment>

<file context>
@@ -597,6 +597,13 @@ def _visit_and_get_refs(
+            # a nested private function's self-references are not mistaken
+            # for cell-local references. Variable metadata is attached after
+            # the body has been visited and its references are known.
+            self.block_stack[-1].defs.add(node.name)
         self.generic_visit(node)
         refs = self.ref_stack.pop()
</file context>

self.generic_visit(node)
Comment on lines 599 to 607
refs = self.ref_stack.pop()
# The scope a level up from the one just investigated also is dependent
Expand Down
51 changes: 51 additions & 0 deletions tests/_runtime/test_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -1953,6 +1953,57 @@ def _recurse(n):
assert cell.exception is None
assert k.globals["result"] == 3

async def test_nested_private_recursive_function(
self, any_kernel: Kernel, exec_req: ExecReqProvider
) -> None:
"""Regression test for #10675."""
k = any_kernel
await k.run(
[
er := exec_req.get(
"""
def _sum(xs):
def _recursive_sum(xs):
if not xs:
return 0
return xs[0] + _recursive_sum(xs[1:])

return _recursive_sum(xs)
result = _sum([1, 2, 3, 4, 5])
"""
)
]
)
cell = k.graph.cells[er.cell_id]
assert cell.exception is None
assert k.globals["result"] == 15

async def test_nested_public_recursive_function(
self, any_kernel: Kernel, exec_req: ExecReqProvider
) -> None:
k = any_kernel
await k.run(
[
exec_req.get(
"""
def sum_values(xs):
def _recursive_sum(xs):
if not xs:
return 0
return xs[0] + _recursive_sum(xs[1:])

return _recursive_sum(xs)
"""
)
]
)
await k.run(
[er := exec_req.get("result = sum_values([1, 2, 3, 4, 5])")]
)
cell = k.graph.cells[er.cell_id]
assert cell.exception is None
assert k.globals["result"] == 15

async def test_private_recursive_function_local_shadow(
self, any_kernel: Kernel, exec_req: ExecReqProvider
) -> None:
Expand Down
Loading