2020 TYPE_VAR_KIND ,
2121 TYPE_VAR_TUPLE_KIND ,
2222 ArgKind ,
23+ AssignmentExpr ,
24+ AwaitExpr ,
2325 CallExpr ,
2426 Decorator ,
27+ DictionaryComprehension ,
2528 Expression ,
2629 FuncDef ,
30+ GeneratorExpr ,
2731 IndexExpr ,
2832 IntExpr ,
33+ LambdaExpr ,
2934 Lvalue ,
3035 MemberExpr ,
3136 MypyFile ,
4146 TypeInfo ,
4247 TypeParam ,
4348 Var ,
49+ YieldExpr ,
50+ YieldFromExpr ,
4451)
52+ from mypy .traverser import TraverserVisitor
4553from mypy .types import (
4654 AnyType ,
4755 DeletedType ,
6371 EXT_SUFFIX ,
6472 GENERATOR_ATTRIBUTE_PREFIX ,
6573 IS_FREE_THREADED ,
74+ KEEP_ALIVE_SHORT_LIVED ,
75+ KEEP_ALIVE_WHOLE_EXPRESSION ,
6676 MODULE_PREFIX ,
6777 SELF_NAME ,
6878 TEMP_ATTR_NAME ,
8090 BasicBlock ,
8191 Branch ,
8292 Call ,
93+ Cast ,
8394 ComparisonOp ,
8495 GetAttr ,
8596 InitStatic ,
@@ -284,6 +295,20 @@ def __init__(
284295 self .imports : dict [str , None ] = {}
285296
286297 self .can_borrow = False
298+ self .expression_depth = 0
299+ # Symbols (local vars) reassigned via a walrus expression within the current
300+ # top-level expression. Used to avoid borrowing an attribute over the whole
301+ # expression when the borrow root could be rebound (and thus freed) partway.
302+ self .reassigned_in_expr : set [SymbolNode ] = set ()
303+ # Whether the current top-level expression contains a suspension point
304+ # (await, yield or yield from). A whole-expression borrow can't span such a
305+ # point, since the borrowed value (and its root) live in registers that are
306+ # not spilled into the generator environment across the suspend.
307+ self .expr_has_suspend = False
308+ # Saved expression state for enclosing functions (see enter()/leave()).
309+ self .expression_depth_stack : list [int ] = []
310+ self .reassigned_in_expr_stack : list [set [SymbolNode ]] = []
311+ self .expr_has_suspend_stack : list [bool ] = []
287312
288313 # When set, load_globals_dict uses this module instead of self.module_name.
289314 # Used by generate_attr_defaults_init for cross-module inherited defaults.
@@ -315,6 +340,10 @@ def accept(self, node: Statement | Expression, *, can_borrow: bool = False) -> V
315340 """
316341 with self .catch_errors (node .line ):
317342 if isinstance (node , Expression ):
343+ self .expression_depth += 1
344+ if self .expression_depth == 1 :
345+ self .reassigned_in_expr = find_walrus_targets (node )
346+ self .expr_has_suspend = expr_has_suspend (node )
318347 old_can_borrow = self .can_borrow
319348 self .can_borrow = can_borrow
320349 try :
@@ -329,6 +358,11 @@ def accept(self, node: Statement | Expression, *, can_borrow: bool = False) -> V
329358 self .can_borrow = old_can_borrow
330359 if not can_borrow :
331360 self .flush_keep_alives (node .line )
361+ self .expression_depth -= 1
362+ if self .expression_depth == 0 :
363+ self .flush_keep_alives (node .line , scope = KEEP_ALIVE_WHOLE_EXPRESSION )
364+ self .reassigned_in_expr = set ()
365+ self .expr_has_suspend = False
332366 return res
333367 else :
334368 try :
@@ -337,8 +371,8 @@ def accept(self, node: Statement | Expression, *, can_borrow: bool = False) -> V
337371 pass
338372 return None
339373
340- def flush_keep_alives (self , line : int ) -> None :
341- self .builder .flush_keep_alives (line )
374+ def flush_keep_alives (self , line : int , * , scope : int = KEEP_ALIVE_SHORT_LIVED ) -> None :
375+ self .builder .flush_keep_alives (line , scope = scope )
342376
343377 # Pass through methods for the most common low-level builder ops, for convenience.
344378
@@ -1192,7 +1226,9 @@ def is_synthetic_type(self, typ: TypeInfo) -> bool:
11921226 return typ .is_named_tuple or typ .is_newtype or typ .typeddict_type is not None
11931227
11941228 def get_final_ref (self , expr : MemberExpr ) -> tuple [str , Var , bool ] | None :
1195- """Check if `expr` is a final attribute.
1229+ """Check if `expr` is a final class or module attribute.
1230+
1231+ Return False for instance attributes.
11961232
11971233 This needs to be done differently for class and module attributes to
11981234 correctly determine fully qualified name. Return a tuple that consists of
@@ -1341,6 +1377,15 @@ def enter(self, fn_info: FuncInfo | str = "", *, ret_type: RType = none_rprimiti
13411377 self .fn_info = fn_info
13421378 self .fn_infos .append (self .fn_info )
13431379 self .ret_types .append (ret_type )
1380+ # A function body is its own top-level expression context, even when the
1381+ # function (e.g. a lambda) is being generated in the middle of an outer
1382+ # expression. Save the outer expression state and start fresh.
1383+ self .expression_depth_stack .append (self .expression_depth )
1384+ self .reassigned_in_expr_stack .append (self .reassigned_in_expr )
1385+ self .expr_has_suspend_stack .append (self .expr_has_suspend )
1386+ self .expression_depth = 0
1387+ self .reassigned_in_expr = set ()
1388+ self .expr_has_suspend = False
13441389 if fn_info .is_generator :
13451390 self .nonlocal_control .append (GeneratorNonlocalControl ())
13461391 else :
@@ -1354,6 +1399,9 @@ def leave(self) -> tuple[list[Register], list[RuntimeArg], list[BasicBlock], RTy
13541399 ret_type = self .ret_types .pop ()
13551400 fn_info = self .fn_infos .pop ()
13561401 self .nonlocal_control .pop ()
1402+ self .expression_depth = self .expression_depth_stack .pop ()
1403+ self .reassigned_in_expr = self .reassigned_in_expr_stack .pop ()
1404+ self .expr_has_suspend = self .expr_has_suspend_stack .pop ()
13571405 self .builder = self .builders [- 1 ]
13581406 self .fn_info = self .fn_infos [- 1 ]
13591407 return builder .args , runtime_args , builder .blocks , ret_type , fn_info
@@ -1389,6 +1437,27 @@ def enter_scope(self, fn_info: FuncInfo) -> Iterator[None]:
13891437 self .builder = self .builders [- 1 ]
13901438 self .fn_info = self .fn_infos [- 1 ]
13911439
1440+ @contextmanager
1441+ def enter_borrow_scope (self , line : int ) -> Iterator [None ]:
1442+ """Enter new borrow scope from which borrows can't leak to outer expressions.
1443+
1444+ This is a borrow region (see LowLevelIRBuilder.borrow_region) that also
1445+ resets the per-expression borrowing heuristic state, since the body forms
1446+ its own top-level expression context (e.g. a comprehension iteration or a
1447+ lambda body).
1448+ """
1449+ old_expression_depth = self .expression_depth
1450+ old_reassigned_in_expr = self .reassigned_in_expr
1451+ old_expr_has_suspend = self .expr_has_suspend
1452+ self .expression_depth = 0
1453+ try :
1454+ with self .builder .borrow_region (line ):
1455+ yield
1456+ finally :
1457+ self .expression_depth = old_expression_depth
1458+ self .reassigned_in_expr = old_reassigned_in_expr
1459+ self .expr_has_suspend = old_expr_has_suspend
1460+
13921461 @contextmanager
13931462 def enter_method (
13941463 self ,
@@ -1581,6 +1650,32 @@ def is_final_native_attr_ref(self, expr: MemberExpr) -> bool:
15811650 return expr .name in ir .final_attributes
15821651 return False
15831652
1653+ def root_is_reassigned (self , v : Value ) -> bool :
1654+ """Is the root local variable a borrow chain 'v' reads from reassigned this expression?
1655+
1656+ A whole-expression borrow of an attribute keeps the borrow root alive only
1657+ via the register holding it. If that register belongs to a local variable
1658+ that is rebound (via a walrus assignment) during the same top-level
1659+ expression, the old value may be freed while the borrow is still live.
1660+ """
1661+ if not self .reassigned_in_expr :
1662+ return False
1663+ # Peel borrowed links back to the root value the chain reads from.
1664+ while True :
1665+ if isinstance (v , GetAttr ) and v .is_borrowed :
1666+ v = v .obj
1667+ elif isinstance (v , Cast ) and v .is_borrowed :
1668+ v = v .src
1669+ else :
1670+ break
1671+ if not isinstance (v , Register ):
1672+ return False
1673+ for symbol in self .reassigned_in_expr :
1674+ target = self .symtables [- 1 ].get (symbol )
1675+ if isinstance (target , AssignmentTargetRegister ) and target .register is v :
1676+ return True
1677+ return False
1678+
15841679 def mark_block_unreachable (self ) -> None :
15851680 """Mark statements in the innermost block being processed as unreachable.
15861681
@@ -1695,6 +1790,78 @@ def get_call_target_fullname(ref: RefExpr) -> str:
16951790 return ref .fullname
16961791
16971792
1793+ class WalrusTargetCollector (TraverserVisitor ):
1794+ """Collect the symbols assigned to by walrus expressions in a subtree."""
1795+
1796+ def __init__ (self ) -> None :
1797+ self .targets : set [SymbolNode ] = set ()
1798+
1799+ def visit_assignment_expr (self , o : AssignmentExpr ) -> None :
1800+ if o .target .node is not None :
1801+ self .targets .add (o .target .node )
1802+ super ().visit_assignment_expr (o )
1803+
1804+ def visit_lambda_expr (self , o : LambdaExpr ) -> None :
1805+ # A lambda body forms its own expression context, so don't descend into it.
1806+ pass
1807+
1808+
1809+ def find_walrus_targets (expr : Expression ) -> set [SymbolNode ]:
1810+ """Return the symbols reassigned via a walrus expression within 'expr'.
1811+
1812+ Walrus (':=') is the only way to rebind a variable in the middle of evaluating
1813+ an expression, so this is the complete set of in-expression reassignments.
1814+ """
1815+ collector = WalrusTargetCollector ()
1816+ expr .accept (collector )
1817+ return collector .targets
1818+
1819+
1820+ class SuspendDetector (TraverserVisitor ):
1821+ """Detect await/yield/yield from expressions in a subtree."""
1822+
1823+ def __init__ (self ) -> None :
1824+ self .found = False
1825+
1826+ def visit_await_expr (self , o : AwaitExpr ) -> None :
1827+ self .found = True
1828+
1829+ def visit_yield_expr (self , o : YieldExpr ) -> None :
1830+ self .found = True
1831+
1832+ def visit_yield_from_expr (self , o : YieldFromExpr ) -> None :
1833+ self .found = True
1834+
1835+ def visit_generator_expr (self , o : GeneratorExpr ) -> None :
1836+ # An 'async for' clause suspends via an implicit await on __anext__ that
1837+ # isn't represented as an AwaitExpr node in the AST (list/set comprehensions
1838+ # delegate to a GeneratorExpr, so they are covered here too).
1839+ if any (o .is_async ):
1840+ self .found = True
1841+ super ().visit_generator_expr (o )
1842+
1843+ def visit_dictionary_comprehension (self , o : DictionaryComprehension ) -> None :
1844+ if any (o .is_async ):
1845+ self .found = True
1846+ super ().visit_dictionary_comprehension (o )
1847+
1848+ def visit_lambda_expr (self , o : LambdaExpr ) -> None :
1849+ # A lambda body forms its own function (and suspension) context.
1850+ pass
1851+
1852+
1853+ def expr_has_suspend (expr : Expression ) -> bool :
1854+ """Does evaluating 'expr' involve a suspension point (await/yield/yield from)?
1855+
1856+ A whole-expression borrow can't safely span a suspension point, since the
1857+ borrowed value and its borrow root are held in registers that aren't spilled
1858+ into the generator environment across the suspend.
1859+ """
1860+ detector = SuspendDetector ()
1861+ expr .accept (detector )
1862+ return detector .found
1863+
1864+
16981865def create_type_params (
16991866 builder : IRBuilder , typing_mod : Value , type_args : list [TypeParam ], line : int
17001867) -> list [Value ]:
0 commit comments