Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
022712a
Add failing test case
JukkaL Jun 20, 2026
618e153
Always borrow final attributes
JukkaL Jun 20, 2026
f587fbb
Add scope/duration to keep alives
JukkaL Jun 20, 2026
231ecf2
Actually fix keep alives to stay around long enough
JukkaL Jun 20, 2026
436c972
Add helper for transforming top-level expressions (with keep alive fl…
JukkaL Jun 20, 2026
4253768
Don't implicitly borrow top level attribute references
JukkaL Jun 20, 2026
9cf4a30
Test inheritance
JukkaL Jun 21, 2026
e075e2c
Add test case
JukkaL Jun 21, 2026
220313b
Fix expressions with mixed borrowing scopes
JukkaL Jun 21, 2026
49f6d90
WIP failing vec test case
JukkaL Jun 21, 2026
8ee2588
Handle vec borrowing properly
JukkaL Jun 30, 2026
b5dc263
Fix typos
JukkaL Jun 30, 2026
7584998
More general fix
JukkaL Jun 30, 2026
f2121cf
Don't borrow over walrus expressions
JukkaL Jul 1, 2026
32e1a46
Fix lambdas
JukkaL Jul 1, 2026
6674576
Tweaks
JukkaL Jul 1, 2026
400eb53
Fix conditional control flow in expressions
JukkaL Jul 1, 2026
f6d60d8
Fix comprehensions
JukkaL Jul 2, 2026
a71ba14
WIP test case
JukkaL Jul 2, 2026
aee6885
Add comprehension test cases
JukkaL Jul 2, 2026
a9304f2
Fix await, yield and yield from
JukkaL Jul 2, 2026
e6df3f2
Fix async comprehensions
JukkaL Jul 7, 2026
1691a9f
Refactor after rebase
JukkaL Jul 8, 2026
40527e6
Fix bug
JukkaL Jul 8, 2026
b32d34a
Update comment
JukkaL Jul 8, 2026
de28c6b
Update test
JukkaL Jul 8, 2026
00e3850
Fix more tests
JukkaL Jul 8, 2026
30de6e0
Refactor
JukkaL Jul 8, 2026
2be6c4b
Add codespell ignore work
JukkaL Jul 8, 2026
fb65dfa
Properly fix codespell
JukkaL Jul 8, 2026
883c250
Fix borrowing over casts
JukkaL Jul 9, 2026
8dd7527
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 9, 2026
082428c
Work around 32-bit platform test failure
JukkaL Jul 9, 2026
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ repos:
hooks:
- id: codespell
args:
- --ignore-words-list=HAX,Nam,ccompiler,ot,statics,whet,zar
- --ignore-words-list=HAX,Nam,ccompiler,keep-alives,ot,statics,whet,zar
exclude: ^(mypy/test/|mypy/typeshed/|mypyc/test-data/|test-data/).+$
- repo: https://github.com/rhysd/actionlint
rev: v1.7.7
Expand Down
8 changes: 8 additions & 0 deletions mypyc/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@
BITMAP_TYPE: Final = "uint32_t"
BITMAP_BITS: Final = 32

# Constant for keeping a (often borrowed) op alive for a short time (a few subexpressions,
# no arbitrary computation or memory allocations), until flush_keep_alives() is called.
KEEP_ALIVE_SHORT_LIVED: Final = 0
# Keep value alive longer, up to until the current top-level expression has been fully
# evaluated. This allows keeping alive values across function calls and arbitrary
# computation. Note that some expressions (e.g. lambdas), restrict the scope of borrowing.
KEEP_ALIVE_WHOLE_EXPRESSION: Final = 1

# Runtime C library files that are always included (some ops may bring
# extra dependencies via mypyc.ir.deps.SourceDep or mypyc.ir.deps.HeaderDep)
RUNTIME_C_FILES: Final = [
Expand Down
5 changes: 4 additions & 1 deletion mypyc/ir/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class to enable the new behavior. Sometimes adding a new abstract

from mypy_extensions import trait

from mypyc.common import PROPSET_PREFIX
from mypyc.common import KEEP_ALIVE_SHORT_LIVED, PROPSET_PREFIX
from mypyc.ir.deps import Dependency
from mypyc.ir.rtypes import (
RArray,
Expand Down Expand Up @@ -898,6 +898,7 @@ def __init__(
*,
borrow: bool = False,
allow_error_value: bool = False,
borrow_scope: int = KEEP_ALIVE_SHORT_LIVED,
) -> None:
super().__init__(line)
self.obj = obj
Expand All @@ -912,6 +913,8 @@ def __init__(
elif attr_type.error_overlap:
self.error_kind = ERR_MAGIC_OVERLAPPING
self.is_borrowed = borrow and attr_type.is_refcounted
# How long a borrowed result of this op stays valid (a KEEP_ALIVE_* constant).
self.borrow_scope = borrow_scope

def sources(self) -> list[Value]:
return [self.obj]
Expand Down
173 changes: 170 additions & 3 deletions mypyc/irbuild/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@
TYPE_VAR_KIND,
TYPE_VAR_TUPLE_KIND,
ArgKind,
AssignmentExpr,
AwaitExpr,
CallExpr,
Decorator,
DictionaryComprehension,
Expression,
FuncDef,
GeneratorExpr,
IndexExpr,
IntExpr,
LambdaExpr,
Lvalue,
MemberExpr,
MypyFile,
Expand All @@ -41,7 +46,10 @@
TypeInfo,
TypeParam,
Var,
YieldExpr,
YieldFromExpr,
)
from mypy.traverser import TraverserVisitor
from mypy.types import (
AnyType,
DeletedType,
Expand All @@ -63,6 +71,8 @@
EXT_SUFFIX,
GENERATOR_ATTRIBUTE_PREFIX,
IS_FREE_THREADED,
KEEP_ALIVE_SHORT_LIVED,
KEEP_ALIVE_WHOLE_EXPRESSION,
MODULE_PREFIX,
SELF_NAME,
TEMP_ATTR_NAME,
Expand All @@ -80,6 +90,7 @@
BasicBlock,
Branch,
Call,
Cast,
ComparisonOp,
GetAttr,
InitStatic,
Expand Down Expand Up @@ -284,6 +295,20 @@ def __init__(
self.imports: dict[str, None] = {}

self.can_borrow = False
self.expression_depth = 0
# Symbols (local vars) reassigned via a walrus expression within the current
# top-level expression. Used to avoid borrowing an attribute over the whole
# expression when the borrow root could be rebound (and thus freed) partway.
self.reassigned_in_expr: set[SymbolNode] = set()
# Whether the current top-level expression contains a suspension point
# (await, yield or yield from). A whole-expression borrow can't span such a
# point, since the borrowed value (and its root) live in registers that are
# not spilled into the generator environment across the suspend.
self.expr_has_suspend = False
# Saved expression state for enclosing functions (see enter()/leave()).
self.expression_depth_stack: list[int] = []
self.reassigned_in_expr_stack: list[set[SymbolNode]] = []
self.expr_has_suspend_stack: list[bool] = []

# When set, load_globals_dict uses this module instead of self.module_name.
# Used by generate_attr_defaults_init for cross-module inherited defaults.
Expand Down Expand Up @@ -315,6 +340,10 @@ def accept(self, node: Statement | Expression, *, can_borrow: bool = False) -> V
"""
with self.catch_errors(node.line):
if isinstance(node, Expression):
self.expression_depth += 1
if self.expression_depth == 1:
self.reassigned_in_expr = find_walrus_targets(node)
self.expr_has_suspend = expr_has_suspend(node)
old_can_borrow = self.can_borrow
self.can_borrow = can_borrow
try:
Expand All @@ -329,6 +358,11 @@ def accept(self, node: Statement | Expression, *, can_borrow: bool = False) -> V
self.can_borrow = old_can_borrow
if not can_borrow:
self.flush_keep_alives(node.line)
self.expression_depth -= 1
if self.expression_depth == 0:
self.flush_keep_alives(node.line, scope=KEEP_ALIVE_WHOLE_EXPRESSION)
self.reassigned_in_expr = set()
self.expr_has_suspend = False
return res
else:
try:
Expand All @@ -337,8 +371,8 @@ def accept(self, node: Statement | Expression, *, can_borrow: bool = False) -> V
pass
return None

def flush_keep_alives(self, line: int) -> None:
self.builder.flush_keep_alives(line)
def flush_keep_alives(self, line: int, *, scope: int = KEEP_ALIVE_SHORT_LIVED) -> None:
self.builder.flush_keep_alives(line, scope=scope)

# Pass through methods for the most common low-level builder ops, for convenience.

Expand Down Expand Up @@ -1192,7 +1226,9 @@ def is_synthetic_type(self, typ: TypeInfo) -> bool:
return typ.is_named_tuple or typ.is_newtype or typ.typeddict_type is not None

def get_final_ref(self, expr: MemberExpr) -> tuple[str, Var, bool] | None:
"""Check if `expr` is a final attribute.
"""Check if `expr` is a final class or module attribute.

Return False for instance attributes.

This needs to be done differently for class and module attributes to
correctly determine fully qualified name. Return a tuple that consists of
Expand Down Expand Up @@ -1341,6 +1377,15 @@ def enter(self, fn_info: FuncInfo | str = "", *, ret_type: RType = none_rprimiti
self.fn_info = fn_info
self.fn_infos.append(self.fn_info)
self.ret_types.append(ret_type)
# A function body is its own top-level expression context, even when the
# function (e.g. a lambda) is being generated in the middle of an outer
# expression. Save the outer expression state and start fresh.
self.expression_depth_stack.append(self.expression_depth)
self.reassigned_in_expr_stack.append(self.reassigned_in_expr)
self.expr_has_suspend_stack.append(self.expr_has_suspend)
self.expression_depth = 0
self.reassigned_in_expr = set()
self.expr_has_suspend = False
if fn_info.is_generator:
self.nonlocal_control.append(GeneratorNonlocalControl())
else:
Expand All @@ -1354,6 +1399,9 @@ def leave(self) -> tuple[list[Register], list[RuntimeArg], list[BasicBlock], RTy
ret_type = self.ret_types.pop()
fn_info = self.fn_infos.pop()
self.nonlocal_control.pop()
self.expression_depth = self.expression_depth_stack.pop()
self.reassigned_in_expr = self.reassigned_in_expr_stack.pop()
self.expr_has_suspend = self.expr_has_suspend_stack.pop()
self.builder = self.builders[-1]
self.fn_info = self.fn_infos[-1]
return builder.args, runtime_args, builder.blocks, ret_type, fn_info
Expand Down Expand Up @@ -1389,6 +1437,27 @@ def enter_scope(self, fn_info: FuncInfo) -> Iterator[None]:
self.builder = self.builders[-1]
self.fn_info = self.fn_infos[-1]

@contextmanager
def enter_borrow_scope(self, line: int) -> Iterator[None]:
"""Enter new borrow scope from which borrows can't leak to outer expressions.

This is a borrow region (see LowLevelIRBuilder.borrow_region) that also
resets the per-expression borrowing heuristic state, since the body forms
its own top-level expression context (e.g. a comprehension iteration or a
lambda body).
"""
old_expression_depth = self.expression_depth
old_reassigned_in_expr = self.reassigned_in_expr
old_expr_has_suspend = self.expr_has_suspend
self.expression_depth = 0
try:
with self.builder.borrow_region(line):
yield
finally:
self.expression_depth = old_expression_depth
self.reassigned_in_expr = old_reassigned_in_expr
self.expr_has_suspend = old_expr_has_suspend

@contextmanager
def enter_method(
self,
Expand Down Expand Up @@ -1581,6 +1650,32 @@ def is_final_native_attr_ref(self, expr: MemberExpr) -> bool:
return expr.name in ir.final_attributes
return False

def root_is_reassigned(self, v: Value) -> bool:
"""Is the root local variable a borrow chain 'v' reads from reassigned this expression?

A whole-expression borrow of an attribute keeps the borrow root alive only
via the register holding it. If that register belongs to a local variable
that is rebound (via a walrus assignment) during the same top-level
expression, the old value may be freed while the borrow is still live.
"""
if not self.reassigned_in_expr:
return False
# Peel borrowed links back to the root value the chain reads from.
while True:
if isinstance(v, GetAttr) and v.is_borrowed:
v = v.obj
elif isinstance(v, Cast) and v.is_borrowed:
v = v.src
else:
break
if not isinstance(v, Register):
return False
for symbol in self.reassigned_in_expr:
target = self.symtables[-1].get(symbol)
if isinstance(target, AssignmentTargetRegister) and target.register is v:
return True
return False

def mark_block_unreachable(self) -> None:
"""Mark statements in the innermost block being processed as unreachable.

Expand Down Expand Up @@ -1695,6 +1790,78 @@ def get_call_target_fullname(ref: RefExpr) -> str:
return ref.fullname


class WalrusTargetCollector(TraverserVisitor):
"""Collect the symbols assigned to by walrus expressions in a subtree."""

def __init__(self) -> None:
self.targets: set[SymbolNode] = set()

def visit_assignment_expr(self, o: AssignmentExpr) -> None:
if o.target.node is not None:
self.targets.add(o.target.node)
super().visit_assignment_expr(o)

def visit_lambda_expr(self, o: LambdaExpr) -> None:
# A lambda body forms its own expression context, so don't descend into it.
pass


def find_walrus_targets(expr: Expression) -> set[SymbolNode]:
"""Return the symbols reassigned via a walrus expression within 'expr'.

Walrus (':=') is the only way to rebind a variable in the middle of evaluating
an expression, so this is the complete set of in-expression reassignments.
"""
collector = WalrusTargetCollector()
expr.accept(collector)
return collector.targets


class SuspendDetector(TraverserVisitor):
"""Detect await/yield/yield from expressions in a subtree."""

def __init__(self) -> None:
self.found = False

def visit_await_expr(self, o: AwaitExpr) -> None:
self.found = True

def visit_yield_expr(self, o: YieldExpr) -> None:
self.found = True

def visit_yield_from_expr(self, o: YieldFromExpr) -> None:
self.found = True

def visit_generator_expr(self, o: GeneratorExpr) -> None:
# An 'async for' clause suspends via an implicit await on __anext__ that
# isn't represented as an AwaitExpr node in the AST (list/set comprehensions
# delegate to a GeneratorExpr, so they are covered here too).
if any(o.is_async):
self.found = True
super().visit_generator_expr(o)

def visit_dictionary_comprehension(self, o: DictionaryComprehension) -> None:
if any(o.is_async):
self.found = True
super().visit_dictionary_comprehension(o)

def visit_lambda_expr(self, o: LambdaExpr) -> None:
# A lambda body forms its own function (and suspension) context.
pass


def expr_has_suspend(expr: Expression) -> bool:
"""Does evaluating 'expr' involve a suspension point (await/yield/yield from)?

A whole-expression borrow can't safely span a suspension point, since the
borrowed value and its borrow root are held in registers that aren't spilled
into the generator environment across the suspend.
"""
detector = SuspendDetector()
expr.accept(detector)
return detector.found


def create_type_params(
builder: IRBuilder, typing_mod: Value, type_args: list[TypeParam], line: int
) -> list[Value]:
Expand Down
Loading
Loading