Skip to content

[mypyc] Fix AttributeError in async try/finally with mixed return paths #19361

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 2, 2025
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
21 changes: 21 additions & 0 deletions mypyc/codegen/emitfunc.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,9 @@ def get_attr_expr(self, obj: str, op: GetAttr | SetAttr, decl_cl: ClassIR) -> st
return f"({cast}{obj})->{self.emitter.attr(op.attr)}"

def visit_get_attr(self, op: GetAttr) -> None:
if op.allow_null:
self.get_attr_with_allow_null(op)
return
dest = self.reg(op)
obj = self.reg(op.obj)
rtype = op.class_type
Expand Down Expand Up @@ -426,6 +429,24 @@ def visit_get_attr(self, op: GetAttr) -> None:
elif not always_defined:
self.emitter.emit_line("}")

def get_attr_with_allow_null(self, op: GetAttr) -> None:
"""Handle GetAttr with allow_null=True which allows NULL without raising AttributeError."""
dest = self.reg(op)
obj = self.reg(op.obj)
rtype = op.class_type
cl = rtype.class_ir
attr_rtype, decl_cl = cl.attr_details(op.attr)

# Direct struct access without NULL check
attr_expr = self.get_attr_expr(obj, op, decl_cl)
self.emitter.emit_line(f"{dest} = {attr_expr};")

# Only emit inc_ref if not NULL
if attr_rtype.is_refcounted and not op.is_borrowed:
self.emitter.emit_line(f"if ({dest} != NULL) {{")
self.emitter.emit_inc_ref(dest, attr_rtype)
self.emitter.emit_line("}")

def next_branch(self) -> Branch | None:
if self.op_index + 1 < len(self.ops):
next_op = self.ops[self.op_index + 1]
Expand Down
9 changes: 7 additions & 2 deletions mypyc/ir/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -777,15 +777,20 @@ class GetAttr(RegisterOp):

error_kind = ERR_MAGIC

def __init__(self, obj: Value, attr: str, line: int, *, borrow: bool = False) -> None:
def __init__(
self, obj: Value, attr: str, line: int, *, borrow: bool = False, allow_null: bool = False
) -> None:
super().__init__(line)
self.obj = obj
self.attr = attr
self.allow_null = allow_null
assert isinstance(obj.type, RInstance), "Attribute access not supported: %s" % obj.type
self.class_type = obj.type
attr_type = obj.type.attr_type(attr)
self.type = attr_type
if attr_type.error_overlap:
if allow_null:
self.error_kind = ERR_NEVER
elif attr_type.error_overlap:
self.error_kind = ERR_MAGIC_OVERLAPPING
self.is_borrowed = borrow and attr_type.is_refcounted

Expand Down
9 changes: 9 additions & 0 deletions mypyc/irbuild/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,15 @@ def read(

assert False, "Unsupported lvalue: %r" % target

def read_nullable_attr(self, obj: Value, attr: str, line: int = -1) -> Value:
"""Read an attribute that might be NULL without raising AttributeError.

This is used for reading spill targets in try/finally blocks where NULL
indicates the non-return path was taken.
"""
assert isinstance(obj.type, RInstance) and obj.type.class_ir.is_ext_class
return self.add(GetAttr(obj, attr, line, allow_null=True))

def assign(self, target: Register | AssignmentTarget, rvalue_reg: Value, line: int) -> None:
if isinstance(target, Register):
self.add(Assign(target, self.coerce_rvalue(rvalue_reg, target.type, line)))
Expand Down
10 changes: 8 additions & 2 deletions mypyc/irbuild/statement.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
YieldExpr,
YieldFromExpr,
)
from mypyc.common import TEMP_ATTR_NAME
from mypyc.ir.ops import (
NAMESPACE_MODULE,
NO_TRACEBACK_LINE_NO,
Expand Down Expand Up @@ -653,10 +654,15 @@ def try_finally_resolve_control(
if ret_reg:
builder.activate_block(rest)
return_block, rest = BasicBlock(), BasicBlock()
builder.add(Branch(builder.read(ret_reg), rest, return_block, Branch.IS_ERROR))
# For spill targets in try/finally, use nullable read to avoid AttributeError
if isinstance(ret_reg, AssignmentTargetAttr) and ret_reg.attr.startswith(TEMP_ATTR_NAME):
ret_val = builder.read_nullable_attr(ret_reg.obj, ret_reg.attr, -1)
else:
ret_val = builder.read(ret_reg)
builder.add(Branch(ret_val, rest, return_block, Branch.IS_ERROR))

builder.activate_block(return_block)
builder.nonlocal_control[-1].gen_return(builder, builder.read(ret_reg), -1)
builder.nonlocal_control[-1].gen_return(builder, ret_val, -1)

# TODO: handle break/continue
builder.activate_block(rest)
Expand Down
Loading