Skip to content

Commit 14781dc

Browse files
committed
[mypyc] Fix for loop iteration when the loop body reassigns the iterable
1 parent 30099ec commit 14781dc

4 files changed

Lines changed: 427 additions & 17 deletions

File tree

‎mypyc/irbuild/for_helpers.py‎

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

88
from __future__ import annotations
99

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

1313
from mypy.nodes import (
1414
ARG_POS,
15+
AssignmentExpr,
16+
AssignmentStmt,
1517
CallExpr,
18+
DelStmt,
1619
DictionaryComprehension,
1720
Expression,
21+
ForStmt,
1822
GeneratorExpr,
1923
ListExpr,
2024
Lvalue,
2125
MemberExpr,
2226
NameExpr,
27+
OperatorAssignmentStmt,
2328
RefExpr,
2429
SetExpr,
2530
StarExpr,
31+
SymbolNode,
32+
TryStmt,
2633
TupleExpr,
2734
TypeAlias,
2835
Var,
36+
WithStmt,
2937
)
38+
from mypy.patterns import AsPattern, MappingPattern, StarredPattern
39+
from mypy.traverser import TraverserVisitor
3040
from mypy.types import LiteralType, TupleType, get_proper_type, get_proper_types
3141
from mypyc.common import GENERATOR_HELPER_NAME, IS_FREE_THREADED
3242
from mypyc.ir.ops import (
@@ -70,7 +80,7 @@
7080
)
7181
from mypyc.irbuild.builder import IRBuilder
7282
from mypyc.irbuild.constant_fold import constant_fold_expr
73-
from mypyc.irbuild.targets import AssignmentTargetTuple
83+
from mypyc.irbuild.targets import AssignmentTargetRegister, AssignmentTargetTuple
7484
from mypyc.irbuild.vec import vec_append, vec_create, vec_get_item_unsafe, vec_init_item_unsafe
7585
from mypyc.primitives.dict_ops import (
7686
dict_check_size_op,
@@ -106,6 +116,7 @@ def for_loop_helper(
106116
else_insts: GenFunc | None,
107117
is_async: bool,
108118
line: int,
119+
reassigned: AbstractSet[SymbolNode] = frozenset(),
109120
) -> None:
110121
"""Generate IR for a loop.
111122
@@ -114,6 +125,8 @@ def for_loop_helper(
114125
expr: the expression to iterate over
115126
body_insts: a function that generates the body of the loop
116127
else_insts: a function that generates the else block instructions
128+
reassigned: local variables that the loop index or body may assign to
129+
(see find_loop_assignments)
117130
"""
118131
# Body of the loop
119132
body_block = BasicBlock()
@@ -128,7 +141,14 @@ def for_loop_helper(
128141
normal_loop_exit = else_block if else_insts is not None else exit_block
129142

130143
for_gen = make_for_loop_generator(
131-
builder, index, expr, body_block, normal_loop_exit, line, is_async=is_async
144+
builder,
145+
index,
146+
expr,
147+
body_block,
148+
normal_loop_exit,
149+
line,
150+
is_async=is_async,
151+
reassigned=reassigned,
132152
)
133153

134154
builder.push_loop_stack(step_block, exit_block)
@@ -479,6 +499,95 @@ def is_range_ref(expr: RefExpr) -> bool:
479499
)
480500

481501

502+
def find_loop_assignments(s: ForStmt) -> set[SymbolNode]:
503+
"""Return the variables that the index or the body of a for loop may assign to."""
504+
collector = AssignmentCollector()
505+
collector.add_lvalue(s.index)
506+
s.body.accept(collector)
507+
return collector.symbols
508+
509+
510+
class AssignmentCollector(TraverserVisitor):
511+
"""Collect the symbols of the variables assigned to (or deleted) in a subtree."""
512+
513+
def __init__(self) -> None:
514+
super().__init__()
515+
self.symbols: set[SymbolNode] = set()
516+
517+
def add_lvalue(self, lvalue: Expression | None) -> None:
518+
if isinstance(lvalue, NameExpr):
519+
if lvalue.node is not None:
520+
self.symbols.add(lvalue.node)
521+
elif isinstance(lvalue, (TupleExpr, ListExpr)):
522+
for item in lvalue.items:
523+
self.add_lvalue(item)
524+
elif isinstance(lvalue, StarExpr):
525+
self.add_lvalue(lvalue.expr)
526+
527+
def visit_assignment_stmt(self, o: AssignmentStmt) -> None:
528+
for lvalue in o.lvalues:
529+
self.add_lvalue(lvalue)
530+
super().visit_assignment_stmt(o)
531+
532+
def visit_operator_assignment_stmt(self, o: OperatorAssignmentStmt) -> None:
533+
self.add_lvalue(o.lvalue)
534+
super().visit_operator_assignment_stmt(o)
535+
536+
def visit_assignment_expr(self, o: AssignmentExpr) -> None:
537+
self.add_lvalue(o.target)
538+
super().visit_assignment_expr(o)
539+
540+
def visit_for_stmt(self, o: ForStmt) -> None:
541+
self.add_lvalue(o.index)
542+
super().visit_for_stmt(o)
543+
544+
def visit_with_stmt(self, o: WithStmt) -> None:
545+
for target in o.target:
546+
self.add_lvalue(target)
547+
super().visit_with_stmt(o)
548+
549+
def visit_try_stmt(self, o: TryStmt) -> None:
550+
for var in o.vars:
551+
self.add_lvalue(var)
552+
super().visit_try_stmt(o)
553+
554+
def visit_del_stmt(self, o: DelStmt) -> None:
555+
self.add_lvalue(o.expr)
556+
super().visit_del_stmt(o)
557+
558+
def visit_as_pattern(self, o: AsPattern) -> None:
559+
self.add_lvalue(o.name)
560+
super().visit_as_pattern(o)
561+
562+
def visit_starred_pattern(self, o: StarredPattern) -> None:
563+
self.add_lvalue(o.capture)
564+
super().visit_starred_pattern(o)
565+
566+
def visit_mapping_pattern(self, o: MappingPattern) -> None:
567+
self.add_lvalue(o.rest)
568+
super().visit_mapping_pattern(o)
569+
570+
571+
def accept_loop_state(
572+
builder: IRBuilder, expr: Expression, reassigned: AbstractSet[SymbolNode]
573+
) -> Value:
574+
"""Evaluate the object to iterate over, or the end of a range.
575+
576+
Specialized loops read this value again on every iteration. If it's the register
577+
of a local variable that the loop may assign to, copy it to a new register, since
578+
the assignment must not affect the iteration (as in "for x in a: a = []").
579+
"""
580+
value = builder.accept(expr)
581+
if isinstance(value, Register):
582+
for symbol in reassigned:
583+
target = builder.symtables[-1].get(symbol)
584+
if isinstance(target, AssignmentTargetRegister) and target.register is value:
585+
copy = Register(value.type)
586+
builder.assign(copy, value, expr.line)
587+
return copy
588+
return value
589+
590+
482591
def make_for_loop_generator(
483592
builder: IRBuilder,
484593
index: Lvalue,
@@ -488,10 +597,14 @@ def make_for_loop_generator(
488597
line: int,
489598
is_async: bool = False,
490599
nested: bool = False,
600+
reassigned: AbstractSet[SymbolNode] = frozenset(),
491601
) -> ForGenerator:
492602
"""Return helper object for generating a for loop over an iterable.
493603
494604
If "nested" is True, this is a nested iterator such as "e" in "enumerate(e)".
605+
606+
"reassigned" contains the local variables that the loop may assign to
607+
(see accept_loop_state).
495608
"""
496609

497610
# Do an async loop if needed. async is always generic
@@ -506,7 +619,7 @@ def make_for_loop_generator(
506619
rtyp = builder.node_type(expr)
507620
if is_sequence_rprimitive(rtyp) or isinstance(rtyp, RVec):
508621
# Special case "for x in <seq>" for concrete sequence types.
509-
expr_reg = builder.accept(expr)
622+
expr_reg = accept_loop_state(builder, expr, reassigned)
510623
target_type = builder.get_sequence_type(expr)
511624

512625
for_list = ForSequence(builder, index, body_block, loop_exit, line, nested)
@@ -515,7 +628,7 @@ def make_for_loop_generator(
515628

516629
if is_dict_rprimitive(rtyp):
517630
# Special case "for k in <dict>".
518-
expr_reg = builder.accept(expr)
631+
expr_reg = accept_loop_state(builder, expr, reassigned)
519632
target_type = builder.get_dict_key_type(expr)
520633

521634
for_dict = ForDictionaryKeys(builder, index, body_block, loop_exit, line, nested)
@@ -537,10 +650,10 @@ def make_for_loop_generator(
537650
# direction of comparison to do.
538651
if len(expr.args) == 1:
539652
start_reg: Value = Integer(0)
540-
end_reg = builder.accept(expr.args[0])
653+
end_reg = accept_loop_state(builder, expr.args[0], reassigned)
541654
else:
542655
start_reg = builder.accept(expr.args[0])
543-
end_reg = builder.accept(expr.args[1])
656+
end_reg = accept_loop_state(builder, expr.args[1], reassigned)
544657
if len(expr.args) == 3:
545658
step = builder.extract_int(expr.args[2])
546659
assert step is not None
@@ -564,7 +677,7 @@ def make_for_loop_generator(
564677
lvalue1 = index.items[0]
565678
lvalue2 = index.items[1]
566679
for_enumerate = ForEnumerate(builder, index, body_block, loop_exit, line, nested)
567-
for_enumerate.init(lvalue1, lvalue2, expr.args[0])
680+
for_enumerate.init(lvalue1, lvalue2, expr.args[0], reassigned)
568681
return for_enumerate
569682

570683
elif (
@@ -576,7 +689,7 @@ def make_for_loop_generator(
576689
):
577690
# Special case "for x, y in zip(a, b)".
578691
for_zip = ForZip(builder, index, body_block, loop_exit, line, nested)
579-
for_zip.init(index.items, expr.args)
692+
for_zip.init(index.items, expr.args, reassigned)
580693
return for_zip
581694

582695
if (
@@ -586,7 +699,7 @@ def make_for_loop_generator(
586699
and is_sequence_rprimitive(builder.node_type(expr.args[0]))
587700
):
588701
# Special case "for x in reversed(<list>)".
589-
expr_reg = builder.accept(expr.args[0])
702+
expr_reg = accept_loop_state(builder, expr.args[0], reassigned)
590703
target_type = builder.get_sequence_type(expr)
591704

592705
for_list = ForSequence(builder, index, body_block, loop_exit, line, nested)
@@ -596,7 +709,7 @@ def make_for_loop_generator(
596709
# Special cases for dictionary iterator methods, like dict.items().
597710
rtype = builder.node_type(expr.callee.expr)
598711
if is_dict_rprimitive(rtype) and expr.callee.name in ("keys", "values", "items"):
599-
expr_reg = builder.accept(expr.callee.expr)
712+
expr_reg = accept_loop_state(builder, expr.callee.expr, reassigned)
600713
for_dict_type: type[ForGenerator] | None = None
601714
if expr.callee.name == "keys":
602715
target_type = builder.get_dict_key_type(expr.callee.expr)
@@ -1257,15 +1370,28 @@ def need_cleanup(self) -> bool:
12571370
# redundant cleanup block, but that's okay.
12581371
return True
12591372

1260-
def init(self, index1: Lvalue, index2: Lvalue, expr: Expression) -> None:
1373+
def init(
1374+
self,
1375+
index1: Lvalue,
1376+
index2: Lvalue,
1377+
expr: Expression,
1378+
reassigned: AbstractSet[SymbolNode] = frozenset(),
1379+
) -> None:
12611380
# Count from 0 to infinity (for the index lvalue).
12621381
self.index_gen = ForInfiniteCounter(
12631382
self.builder, index1, self.body_block, self.loop_exit, self.line, nested=True
12641383
)
12651384
self.index_gen.init()
12661385
# Iterate over the actual iterable.
12671386
self.main_gen = make_for_loop_generator(
1268-
self.builder, index2, expr, self.body_block, self.loop_exit, self.line, nested=True
1387+
self.builder,
1388+
index2,
1389+
expr,
1390+
self.body_block,
1391+
self.loop_exit,
1392+
self.line,
1393+
nested=True,
1394+
reassigned=reassigned,
12691395
)
12701396

12711397
def gen_condition(self) -> None:
@@ -1293,15 +1419,27 @@ def need_cleanup(self) -> bool:
12931419
# redundant cleanup block, but that's okay.
12941420
return True
12951421

1296-
def init(self, indexes: list[Lvalue], exprs: list[Expression]) -> None:
1422+
def init(
1423+
self,
1424+
indexes: list[Lvalue],
1425+
exprs: list[Expression],
1426+
reassigned: AbstractSet[SymbolNode] = frozenset(),
1427+
) -> None:
12971428
assert len(indexes) == len(exprs)
12981429
# Condition check will require multiple basic blocks, since there will be
12991430
# multiple conditions to check.
13001431
self.cond_blocks = [BasicBlock() for _ in range(len(indexes) - 1)] + [self.body_block]
13011432
self.gens: list[ForGenerator] = []
13021433
for index, expr, next_block in zip(indexes, exprs, self.cond_blocks):
13031434
gen = make_for_loop_generator(
1304-
self.builder, index, expr, next_block, self.loop_exit, self.line, nested=True
1435+
self.builder,
1436+
index,
1437+
expr,
1438+
next_block,
1439+
self.loop_exit,
1440+
self.line,
1441+
nested=True,
1442+
reassigned=reassigned,
13051443
)
13061444
self.gens.append(gen)
13071445

‎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)