Skip to content

Commit b93d0ab

Browse files
authored
[red-knot] Add control flow for for loops (#13318)
1 parent e6b927a commit b93d0ab

File tree

2 files changed

+124
-10
lines changed

2 files changed

+124
-10
lines changed

crates/red_knot_python_semantic/src/semantic_index/builder.rs

+34-5
Original file line numberDiff line numberDiff line change
@@ -575,14 +575,23 @@ where
575575
self.flow_merge(pre_if);
576576
}
577577
}
578-
ast::Stmt::While(node) => {
579-
self.visit_expr(&node.test);
578+
ast::Stmt::While(ast::StmtWhile {
579+
test,
580+
body,
581+
orelse,
582+
range: _,
583+
}) => {
584+
self.visit_expr(test);
580585

581586
let pre_loop = self.flow_snapshot();
582587

583588
// Save aside any break states from an outer loop
584589
let saved_break_states = std::mem::take(&mut self.loop_break_states);
585-
self.visit_body(&node.body);
590+
591+
// TODO: definitions created inside the body should be fully visible
592+
// to other statements/expressions inside the body --Alex/Carl
593+
self.visit_body(body);
594+
586595
// Get the break states from the body of this loop, and restore the saved outer
587596
// ones.
588597
let break_states =
@@ -591,7 +600,7 @@ where
591600
// We may execute the `else` clause without ever executing the body, so merge in
592601
// the pre-loop state before visiting `else`.
593602
self.flow_merge(pre_loop);
594-
self.visit_body(&node.orelse);
603+
self.visit_body(orelse);
595604

596605
// Breaking out of a while loop bypasses the `else` clause, so merge in the break
597606
// states after visiting `else`.
@@ -625,15 +634,35 @@ where
625634
orelse,
626635
},
627636
) => {
628-
// TODO add control flow similar to `ast::Stmt::While` above
629637
self.add_standalone_expression(iter);
630638
self.visit_expr(iter);
639+
640+
let pre_loop = self.flow_snapshot();
641+
let saved_break_states = std::mem::take(&mut self.loop_break_states);
642+
631643
debug_assert!(self.current_assignment.is_none());
632644
self.current_assignment = Some(for_stmt.into());
633645
self.visit_expr(target);
634646
self.current_assignment = None;
647+
648+
// TODO: Definitions created by loop variables
649+
// (and definitions created inside the body)
650+
// are fully visible to other statements/expressions inside the body --Alex/Carl
635651
self.visit_body(body);
652+
653+
let break_states =
654+
std::mem::replace(&mut self.loop_break_states, saved_break_states);
655+
656+
// We may execute the `else` clause without ever executing the body, so merge in
657+
// the pre-loop state before visiting `else`.
658+
self.flow_merge(pre_loop);
636659
self.visit_body(orelse);
660+
661+
// Breaking out of a `for` loop bypasses the `else` clause, so merge in the break
662+
// states after visiting `else`.
663+
for break_state in break_states {
664+
self.flow_merge(break_state);
665+
}
637666
}
638667
ast::Stmt::Match(ast::StmtMatch {
639668
subject,

crates/red_knot_python_semantic/src/types/infer.rs

+90-5
Original file line numberDiff line numberDiff line change
@@ -4271,7 +4271,92 @@ mod tests {
42714271
",
42724272
)?;
42734273

4274-
assert_public_ty(&db, "src/a.py", "x", "int");
4274+
assert_public_ty(&db, "src/a.py", "x", "Unbound | int");
4275+
4276+
Ok(())
4277+
}
4278+
4279+
#[test]
4280+
fn for_loop_with_previous_definition() -> anyhow::Result<()> {
4281+
let mut db = setup_db();
4282+
4283+
db.write_dedented(
4284+
"src/a.py",
4285+
"
4286+
class IntIterator:
4287+
def __next__(self) -> int:
4288+
return 42
4289+
4290+
class IntIterable:
4291+
def __iter__(self) -> IntIterator:
4292+
return IntIterator()
4293+
4294+
x = 'foo'
4295+
4296+
for x in IntIterable():
4297+
pass
4298+
",
4299+
)?;
4300+
4301+
assert_public_ty(&db, "src/a.py", "x", r#"Literal["foo"] | int"#);
4302+
4303+
Ok(())
4304+
}
4305+
4306+
#[test]
4307+
fn for_loop_no_break() -> anyhow::Result<()> {
4308+
let mut db = setup_db();
4309+
4310+
db.write_dedented(
4311+
"src/a.py",
4312+
"
4313+
class IntIterator:
4314+
def __next__(self) -> int:
4315+
return 42
4316+
4317+
class IntIterable:
4318+
def __iter__(self) -> IntIterator:
4319+
return IntIterator()
4320+
4321+
for x in IntIterable():
4322+
pass
4323+
else:
4324+
x = 'foo'
4325+
",
4326+
)?;
4327+
4328+
// The `for` loop can never break, so the `else` clause will always be executed,
4329+
// meaning that the visible definition by the end of the scope is solely determined
4330+
// by the `else` clause
4331+
assert_public_ty(&db, "src/a.py", "x", r#"Literal["foo"]"#);
4332+
4333+
Ok(())
4334+
}
4335+
4336+
#[test]
4337+
fn for_loop_may_break() -> anyhow::Result<()> {
4338+
let mut db = setup_db();
4339+
4340+
db.write_dedented(
4341+
"src/a.py",
4342+
"
4343+
class IntIterator:
4344+
def __next__(self) -> int:
4345+
return 42
4346+
4347+
class IntIterable:
4348+
def __iter__(self) -> IntIterator:
4349+
return IntIterator()
4350+
4351+
for x in IntIterable():
4352+
if x > 5:
4353+
break
4354+
else:
4355+
x = 'foo'
4356+
",
4357+
)?;
4358+
4359+
assert_public_ty(&db, "src/a.py", "x", r#"int | Literal["foo"]"#);
42754360

42764361
Ok(())
42774362
}
@@ -4292,7 +4377,7 @@ mod tests {
42924377
",
42934378
)?;
42944379

4295-
assert_public_ty(&db, "src/a.py", "x", "int");
4380+
assert_public_ty(&db, "src/a.py", "x", "Unbound | int");
42964381

42974382
Ok(())
42984383
}
@@ -4320,7 +4405,7 @@ mod tests {
43204405
",
43214406
)?;
43224407

4323-
assert_scope_ty(&db, "src/a.py", &["foo"], "x", "Unknown");
4408+
assert_scope_ty(&db, "src/a.py", &["foo"], "x", "Unbound | Unknown");
43244409

43254410
Ok(())
43264411
}
@@ -4347,7 +4432,7 @@ mod tests {
43474432
)?;
43484433

43494434
// TODO(Alex) async iterables/iterators!
4350-
assert_scope_ty(&db, "src/a.py", &["foo"], "x", "Unknown");
4435+
assert_scope_ty(&db, "src/a.py", &["foo"], "x", "Unbound | Unknown");
43514436

43524437
Ok(())
43534438
}
@@ -4368,7 +4453,7 @@ mod tests {
43684453
&db,
43694454
"src/a.py",
43704455
"x",
4371-
r#"Literal[1] | Literal["a"] | Literal[b"foo"]"#,
4456+
r#"Unbound | Literal[1] | Literal["a"] | Literal[b"foo"]"#,
43724457
);
43734458

43744459
Ok(())

0 commit comments

Comments
 (0)