diff --git a/marimo/_ast/visitor.py b/marimo/_ast/visitor.py index aadc2079e0a..e0016a8faf0 100644 --- a/marimo/_ast/visitor.py +++ b/marimo/_ast/visitor.py @@ -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) self.generic_visit(node) refs = self.ref_stack.pop() # The scope a level up from the one just investigated also is dependent diff --git a/tests/_runtime/test_runtime.py b/tests/_runtime/test_runtime.py index 71d56d43ae5..c564e795aae 100644 --- a/tests/_runtime/test_runtime.py +++ b/tests/_runtime/test_runtime.py @@ -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: