Skip to content

Commit 1107984

Browse files
authored
[mypyc] Fix for loop iteration when the loop body reassigns the iterable (#22137)
Fixes mypyc/mypyc#1233 `for` loops over lists, tuples, strings, bytes, dicts and `range()` (also inside `enumerate()`, `zip()` and `reversed()`) read the iterable, or the end of the range, on every iteration. When it was a local variable, the loop read the variable's register, so assigning to the variable in the loop body changed the iteration: `for i in range(n): n = 0` ran once, `for x in a: a = [9]` stopped after the first item, and a loop over a tuple read past the end of the new tuple, which could segfault. Now, if the loop target or body assigns to the local variable that holds the iterable or the end of the range, its value is copied to a new register before the loop. A small AST visitor finds these assignments: plain, augmented and starred assignments, `:=`, `del`, `for` targets, `with ... as`, `except ... as` and `match` capture patterns. Other loops generate the same IR as before. Copying unconditionally would add a copy to every loop over a local variable and change the IR of 23 existing test cases. The comprehension form, `[x for x in t if (t := (9,))]`, is fixed separately by #22133.
1 parent 0cdcbfd commit 1107984

4 files changed

Lines changed: 424 additions & 18 deletions

File tree

‎mypyc/irbuild/for_helpers.py‎

Lines changed: 153 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,37 @@
77

88
from __future__ import annotations
99

10-
from collections.abc import Callable, Iterator
10+
from collections.abc import Callable, Iterator, Set as AbstractSet
1111
from contextlib import contextmanager
1212
from typing import ClassVar, cast
1313

1414
from mypy.nodes import (
1515
ARG_POS,
16+
AssignmentExpr,
17+
AssignmentStmt,
1618
CallExpr,
19+
DelStmt,
1720
DictionaryComprehension,
1821
Expression,
22+
ForStmt,
1923
GeneratorExpr,
2024
ListExpr,
2125
Lvalue,
2226
MemberExpr,
2327
NameExpr,
28+
OperatorAssignmentStmt,
2429
RefExpr,
2530
SetExpr,
2631
StarExpr,
32+
SymbolNode,
33+
TryStmt,
2734
TupleExpr,
2835
TypeAlias,
2936
Var,
37+
WithStmt,
3038
)
39+
from mypy.patterns import AsPattern, MappingPattern, StarredPattern
40+
from mypy.traverser import TraverserVisitor
3141
from mypy.types import LiteralType, TupleType, get_proper_type, get_proper_types
3242
from mypyc.common import GENERATOR_HELPER_NAME
3343
from mypyc.ir.ops import (
@@ -73,7 +83,7 @@
7383
from mypyc.irbuild.constant_fold import constant_fold_expr
7484
from mypyc.irbuild.context import FuncInfo
7585
from mypyc.irbuild.env_class import add_vars_to_env, finalize_env_class, setup_env_class
76-
from mypyc.irbuild.targets import AssignmentTargetTuple
86+
from mypyc.irbuild.targets import AssignmentTargetRegister, AssignmentTargetTuple
7787
from mypyc.irbuild.vec import vec_append, vec_create, vec_get_item_unsafe, vec_init_item_unsafe
7888
from mypyc.primitives.dict_ops import (
7989
dict_check_size_op,
@@ -110,6 +120,7 @@ def for_loop_helper(
110120
is_async: bool,
111121
line: int,
112122
after_init: GenFunc | None = None,
123+
reassigned: AbstractSet[SymbolNode] = frozenset(),
113124
) -> None:
114125
"""Generate IR for a loop.
115126
@@ -120,6 +131,8 @@ def for_loop_helper(
120131
else_insts: a function that generates the else block instructions
121132
after_init: a function that generates instructions to run after the iterable has
122133
been evaluated (and iter() called on it) but before the first iteration
134+
reassigned: local variables that the loop index or body may assign to
135+
(see find_loop_assignments)
123136
"""
124137
# Body of the loop
125138
body_block = BasicBlock()
@@ -134,7 +147,14 @@ def for_loop_helper(
134147
normal_loop_exit = else_block if else_insts is not None else exit_block
135148

136149
for_gen = make_for_loop_generator(
137-
builder, index, expr, body_block, normal_loop_exit, line, is_async=is_async
150+
builder,
151+
index,
152+
expr,
153+
body_block,
154+
normal_loop_exit,
155+
line,
156+
is_async=is_async,
157+
reassigned=reassigned,
138158
)
139159
if after_init is not None:
140160
after_init()
@@ -584,6 +604,95 @@ def is_range_ref(expr: RefExpr) -> bool:
584604
)
585605

586606

607+
def find_loop_assignments(s: ForStmt) -> set[SymbolNode]:
608+
"""Return the variables that the index or the body of a for loop may assign to."""
609+
collector = AssignmentCollector()
610+
collector.add_lvalue(s.index)
611+
s.body.accept(collector)
612+
return collector.symbols
613+
614+
615+
class AssignmentCollector(TraverserVisitor):
616+
"""Collect the symbols of the variables assigned to (or deleted) in a subtree."""
617+
618+
def __init__(self) -> None:
619+
super().__init__()
620+
self.symbols: set[SymbolNode] = set()
621+
622+
def add_lvalue(self, lvalue: Expression | None) -> None:
623+
if isinstance(lvalue, NameExpr):
624+
if lvalue.node is not None:
625+
self.symbols.add(lvalue.node)
626+
elif isinstance(lvalue, (TupleExpr, ListExpr)):
627+
for item in lvalue.items:
628+
self.add_lvalue(item)
629+
elif isinstance(lvalue, StarExpr):
630+
self.add_lvalue(lvalue.expr)
631+
632+
def visit_assignment_stmt(self, o: AssignmentStmt) -> None:
633+
for lvalue in o.lvalues:
634+
self.add_lvalue(lvalue)
635+
super().visit_assignment_stmt(o)
636+
637+
def visit_operator_assignment_stmt(self, o: OperatorAssignmentStmt) -> None:
638+
self.add_lvalue(o.lvalue)
639+
super().visit_operator_assignment_stmt(o)
640+
641+
def visit_assignment_expr(self, o: AssignmentExpr) -> None:
642+
self.add_lvalue(o.target)
643+
super().visit_assignment_expr(o)
644+
645+
def visit_for_stmt(self, o: ForStmt) -> None:
646+
self.add_lvalue(o.index)
647+
super().visit_for_stmt(o)
648+
649+
def visit_with_stmt(self, o: WithStmt) -> None:
650+
for target in o.target:
651+
self.add_lvalue(target)
652+
super().visit_with_stmt(o)
653+
654+
def visit_try_stmt(self, o: TryStmt) -> None:
655+
for var in o.vars:
656+
self.add_lvalue(var)
657+
super().visit_try_stmt(o)
658+
659+
def visit_del_stmt(self, o: DelStmt) -> None:
660+
self.add_lvalue(o.expr)
661+
super().visit_del_stmt(o)
662+
663+
def visit_as_pattern(self, o: AsPattern) -> None:
664+
self.add_lvalue(o.name)
665+
super().visit_as_pattern(o)
666+
667+
def visit_starred_pattern(self, o: StarredPattern) -> None:
668+
self.add_lvalue(o.capture)
669+
super().visit_starred_pattern(o)
670+
671+
def visit_mapping_pattern(self, o: MappingPattern) -> None:
672+
self.add_lvalue(o.rest)
673+
super().visit_mapping_pattern(o)
674+
675+
676+
def accept_loop_state(
677+
builder: IRBuilder, expr: Expression, reassigned: AbstractSet[SymbolNode]
678+
) -> Value:
679+
"""Evaluate the object to iterate over, or the end of a range.
680+
681+
Specialized loops read this value again on every iteration. If it's the register
682+
of a local variable that the loop may assign to, copy it to a new register, since
683+
the assignment must not affect the iteration (as in "for x in a: a = []").
684+
"""
685+
value = builder.accept(expr)
686+
if isinstance(value, Register):
687+
for symbol in reassigned:
688+
target = builder.symtables[-1].get(symbol)
689+
if isinstance(target, AssignmentTargetRegister) and target.register is value:
690+
copy = Register(value.type)
691+
builder.assign(copy, value, expr.line)
692+
return copy
693+
return value
694+
695+
587696
def make_for_loop_generator(
588697
builder: IRBuilder,
589698
index: Lvalue,
@@ -593,10 +702,14 @@ def make_for_loop_generator(
593702
line: int,
594703
is_async: bool = False,
595704
nested: bool = False,
705+
reassigned: AbstractSet[SymbolNode] = frozenset(),
596706
) -> ForGenerator:
597707
"""Return helper object for generating a for loop over an iterable.
598708
599709
If "nested" is True, this is a nested iterator such as "e" in "enumerate(e)".
710+
711+
"reassigned" contains the local variables that the loop may assign to
712+
(see accept_loop_state).
600713
"""
601714

602715
# Do an async loop if needed. async is always generic
@@ -611,7 +724,7 @@ def make_for_loop_generator(
611724
rtyp = builder.node_type(expr)
612725
if is_sequence_rprimitive(rtyp) or isinstance(rtyp, RVec):
613726
# Special case "for x in <seq>" for concrete sequence types.
614-
expr_reg = builder.accept(expr)
727+
expr_reg = accept_loop_state(builder, expr, reassigned)
615728
target_type = builder.get_sequence_type(expr)
616729

617730
for_list = ForSequence(builder, index, body_block, loop_exit, line, nested)
@@ -620,7 +733,7 @@ def make_for_loop_generator(
620733

621734
if is_dict_rprimitive(rtyp):
622735
# Special case "for k in <dict>".
623-
expr_reg = builder.accept(expr)
736+
expr_reg = accept_loop_state(builder, expr, reassigned)
624737
target_type = builder.get_dict_key_type(expr)
625738

626739
for_dict = ForDictionaryKeys(builder, index, body_block, loop_exit, line, nested)
@@ -642,10 +755,10 @@ def make_for_loop_generator(
642755
# direction of comparison to do.
643756
if len(expr.args) == 1:
644757
start_reg: Value = Integer(0)
645-
end_reg = builder.accept(expr.args[0])
758+
end_reg = accept_loop_state(builder, expr.args[0], reassigned)
646759
else:
647760
start_reg = builder.accept(expr.args[0])
648-
end_reg = builder.accept(expr.args[1])
761+
end_reg = accept_loop_state(builder, expr.args[1], reassigned)
649762
if len(expr.args) == 3:
650763
step = builder.extract_int(expr.args[2])
651764
assert step is not None
@@ -669,7 +782,7 @@ def make_for_loop_generator(
669782
lvalue1 = index.items[0]
670783
lvalue2 = index.items[1]
671784
for_enumerate = ForEnumerate(builder, index, body_block, loop_exit, line, nested)
672-
for_enumerate.init(lvalue1, lvalue2, expr.args[0])
785+
for_enumerate.init(lvalue1, lvalue2, expr.args[0], reassigned)
673786
return for_enumerate
674787

675788
elif (
@@ -681,7 +794,7 @@ def make_for_loop_generator(
681794
):
682795
# Special case "for x, y in zip(a, b)".
683796
for_zip = ForZip(builder, index, body_block, loop_exit, line, nested)
684-
for_zip.init(index.items, expr.args)
797+
for_zip.init(index.items, expr.args, reassigned)
685798
return for_zip
686799

687800
if (
@@ -691,7 +804,7 @@ def make_for_loop_generator(
691804
and is_sequence_rprimitive(builder.node_type(expr.args[0]))
692805
):
693806
# Special case "for x in reversed(<list>)".
694-
expr_reg = builder.accept(expr.args[0])
807+
expr_reg = accept_loop_state(builder, expr.args[0], reassigned)
695808
target_type = builder.get_sequence_type(expr)
696809

697810
for_list = ForSequence(builder, index, body_block, loop_exit, line, nested)
@@ -701,7 +814,7 @@ def make_for_loop_generator(
701814
# Special cases for dictionary iterator methods, like dict.items().
702815
rtype = builder.node_type(expr.callee.expr)
703816
if is_dict_rprimitive(rtype) and expr.callee.name in ("keys", "values", "items"):
704-
expr_reg = builder.accept(expr.callee.expr)
817+
expr_reg = accept_loop_state(builder, expr.callee.expr, reassigned)
705818
for_dict_type: type[ForGenerator] | None = None
706819
if expr.callee.name == "keys":
707820
target_type = builder.get_dict_key_type(expr.callee.expr)
@@ -1362,15 +1475,28 @@ def need_cleanup(self) -> bool:
13621475
# redundant cleanup block, but that's okay.
13631476
return True
13641477

1365-
def init(self, index1: Lvalue, index2: Lvalue, expr: Expression) -> None:
1478+
def init(
1479+
self,
1480+
index1: Lvalue,
1481+
index2: Lvalue,
1482+
expr: Expression,
1483+
reassigned: AbstractSet[SymbolNode] = frozenset(),
1484+
) -> None:
13661485
# Count from 0 to infinity (for the index lvalue).
13671486
self.index_gen = ForInfiniteCounter(
13681487
self.builder, index1, self.body_block, self.loop_exit, self.line, nested=True
13691488
)
13701489
self.index_gen.init()
13711490
# Iterate over the actual iterable.
13721491
self.main_gen = make_for_loop_generator(
1373-
self.builder, index2, expr, self.body_block, self.loop_exit, self.line, nested=True
1492+
self.builder,
1493+
index2,
1494+
expr,
1495+
self.body_block,
1496+
self.loop_exit,
1497+
self.line,
1498+
nested=True,
1499+
reassigned=reassigned,
13741500
)
13751501

13761502
def gen_condition(self) -> None:
@@ -1398,15 +1524,27 @@ def need_cleanup(self) -> bool:
13981524
# redundant cleanup block, but that's okay.
13991525
return True
14001526

1401-
def init(self, indexes: list[Lvalue], exprs: list[Expression]) -> None:
1527+
def init(
1528+
self,
1529+
indexes: list[Lvalue],
1530+
exprs: list[Expression],
1531+
reassigned: AbstractSet[SymbolNode] = frozenset(),
1532+
) -> None:
14021533
assert len(indexes) == len(exprs)
14031534
# Condition check will require multiple basic blocks, since there will be
14041535
# multiple conditions to check.
14051536
self.cond_blocks = [BasicBlock() for _ in range(len(indexes) - 1)] + [self.body_block]
14061537
self.gens: list[ForGenerator] = []
14071538
for index, expr, next_block in zip(indexes, exprs, self.cond_blocks):
14081539
gen = make_for_loop_generator(
1409-
self.builder, index, expr, next_block, self.loop_exit, self.line, nested=True
1540+
self.builder,
1541+
index,
1542+
expr,
1543+
next_block,
1544+
self.loop_exit,
1545+
self.line,
1546+
nested=True,
1547+
reassigned=reassigned,
14101548
)
14111549
self.gens.append(gen)
14121550

‎mypyc/irbuild/statement.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@
9898
find_walrus_targets,
9999
int_borrow_friendly_op,
100100
)
101-
from mypyc.irbuild.for_helpers import for_loop_helper
101+
from mypyc.irbuild.for_helpers import find_loop_assignments, for_loop_helper
102102
from mypyc.irbuild.generator import add_raise_exception_blocks_to_generator_class
103103
from mypyc.irbuild.nonlocalcontrol import (
104104
ExceptNonlocalControl,
@@ -661,7 +661,14 @@ def else_block() -> None:
661661
builder.accept(s.else_body)
662662

663663
for_loop_helper(
664-
builder, s.index, s.expr, body, else_block if s.else_body else None, s.is_async, s.line
664+
builder,
665+
s.index,
666+
s.expr,
667+
body,
668+
else_block if s.else_body else None,
669+
s.is_async,
670+
s.line,
671+
reassigned=find_loop_assignments(s),
665672
)
666673

667674

‎mypyc/test-data/irbuild-statements.test‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,6 +1172,37 @@ L7:
11721172
L8:
11731173
return 1
11741174

1175+
[case testForLoopReassignIterable]
1176+
def f(a: tuple[int, ...], b: tuple[int, ...]) -> None:
1177+
for x in a:
1178+
a = b
1179+
[out]
1180+
def f(a, b):
1181+
a, b, r0 :: tuple
1182+
r1, r2 :: native_int
1183+
r3 :: bit
1184+
r4 :: object
1185+
r5, x :: int
1186+
r6 :: native_int
1187+
L0:
1188+
r0 = a
1189+
r1 = var_object_size r0
1190+
r2 = 0
1191+
L1:
1192+
r3 = r2 < r1 :: signed
1193+
if r3 goto L2 else goto L4 :: bool
1194+
L2:
1195+
r4 = CPySequenceTuple_GetItemUnsafe(r0, r2)
1196+
r5 = unbox(int, r4)
1197+
x = r5
1198+
a = b
1199+
L3:
1200+
r6 = r2 + 1
1201+
r2 = r6
1202+
goto L1
1203+
L4:
1204+
return 1
1205+
11751206
[case testConditionalFunctionDefinition]
11761207
if int():
11771208
def foo() -> int:

0 commit comments

Comments
 (0)