Skip to content

Commit 27bf9ea

Browse files
yangdanny97facebook-github-bot
authored andcommitted
call ensure_expr before narrowing in comprehensions
Summary: fixes #350 Previously we calculated the narrowing, and then called ensure_expr on every sub-expression using a visitor. This meant that the narrows weren't being applied properly. We need to bind any names declared by walruses before we do the narrowing, which requires moving the ensure_expr calls into bind_comprehensions. Reviewed By: rchen152 Differential Revision: D75472199 fbshipit-source-id: 9865df98c9cf3bc84dd21e0e2e3d01a9181ae72b
1 parent 904ea10 commit 27bf9ea

File tree

2 files changed

+57
-21
lines changed

2 files changed

+57
-21
lines changed

pyrefly/lib/binding/expr.rs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -239,11 +239,13 @@ impl<'a> BindingsBuilder<'a> {
239239
fn bind_comprehensions(&mut self, range: TextRange, comprehensions: &mut [Comprehension]) {
240240
self.scopes.push(Scope::comprehension(range));
241241
for comp in comprehensions {
242+
self.ensure_expr(&mut comp.iter);
242243
self.scopes.add_lvalue_to_current_static(&comp.target);
243244
let make_binding =
244245
|ann| Binding::IterableValue(ann, comp.iter.clone(), IsAsync::new(comp.is_async));
245246
self.bind_target(&mut comp.target, &make_binding);
246-
for x in comp.ifs.iter() {
247+
for x in comp.ifs.iter_mut() {
248+
self.ensure_expr(x);
247249
let narrow_ops = NarrowOps::from_expr(self, Some(x));
248250
self.bind_narrow_ops(&narrow_ops, comp.range);
249251
}
@@ -360,7 +362,7 @@ impl<'a> BindingsBuilder<'a> {
360362

361363
/// Execute through the expr, ensuring every name has a binding.
362364
pub fn ensure_expr(&mut self, x: &mut Expr) {
363-
let new_scope = match x {
365+
match x {
364366
Expr::If(x) => {
365367
// Ternary operation. We treat it like an if/else statement.
366368
let base = self.scopes.clone_current_flow();
@@ -560,46 +562,48 @@ impl<'a> BindingsBuilder<'a> {
560562
{
561563
// Control flow doesn't proceed after sys.exit(), exit(), quit(), or os._exit().
562564
self.scopes.mark_flow_termination();
563-
false
564565
}
565566
Expr::Name(x) => {
566567
let name = Ast::expr_name_identifier(x.clone());
567568
let binding = self
568569
.lookup_name(&name.id, LookupKind::Regular)
569570
.map(Binding::Forward);
570571
self.ensure_name(&name, binding);
571-
false
572572
}
573573
Expr::ListComp(x) => {
574574
self.bind_comprehensions(x.range, &mut x.generators);
575-
true
575+
self.ensure_expr(&mut x.elt);
576+
self.scopes.pop();
577+
return;
576578
}
577579
Expr::SetComp(x) => {
578580
self.bind_comprehensions(x.range, &mut x.generators);
579-
true
581+
self.ensure_expr(&mut x.elt);
582+
self.scopes.pop();
583+
return;
580584
}
581585
Expr::DictComp(x) => {
582586
self.bind_comprehensions(x.range, &mut x.generators);
583-
true
587+
self.ensure_expr(&mut x.key);
588+
self.ensure_expr(&mut x.value);
589+
self.scopes.pop();
590+
return;
584591
}
585592
Expr::Generator(x) => {
586593
self.bind_comprehensions(x.range, &mut x.generators);
587-
true
594+
self.ensure_expr(&mut x.elt);
595+
self.scopes.pop();
596+
return;
588597
}
589598
Expr::Yield(x) => {
590599
self.record_yield(x.clone());
591-
false
592600
}
593601
Expr::YieldFrom(x) => {
594602
self.record_yield_from(x.clone());
595-
false
596603
}
597-
_ => false,
598-
};
599-
x.recurse_mut(&mut |x| self.ensure_expr(x));
600-
if new_scope {
601-
self.scopes.pop();
604+
_ => {}
602605
}
606+
x.recurse_mut(&mut |x| self.ensure_expr(x));
603607
}
604608

605609
/// Execute through the expr, ensuring every name has a binding.

pyrefly/lib/test/narrow.rs

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,42 @@ if (x := f()) is None:
412412
"#,
413413
);
414414

415+
testcase!(
416+
test_walrus_comprehension_if_simple,
417+
r#"
418+
from typing import assert_type
419+
def f(xs: list[int | None]):
420+
ys = [y111 for x in xs if (y111 := x)]
421+
assert_type(ys, list[int])
422+
"#,
423+
);
424+
425+
testcase!(
426+
test_walrus_comprehension_if_function,
427+
r#"
428+
from typing import assert_type
429+
def get_y(x: int | None) -> int | None:
430+
return x
431+
def f(xs: list[int | None]):
432+
ys = [y111 for x in xs if (y111 := get_y(x))]
433+
assert_type(ys, list[int])
434+
"#,
435+
);
436+
437+
testcase!(
438+
test_walrus_generator_if,
439+
r#"
440+
from typing import Sequence
441+
def foo(x: int) -> int | None:
442+
return (x + 5) if x % 2 else None
443+
foos: Sequence[int] = tuple(
444+
maybe_foo
445+
for x in range(10)
446+
if (maybe_foo := foo(x)) is not None
447+
)
448+
"#,
449+
);
450+
415451
testcase!(
416452
test_match_enum_fallback,
417453
r#"
@@ -866,20 +902,16 @@ def f(x: Any):
866902
);
867903

868904
testcase!(
869-
bug = "TODO(stroxler): We are binding narrows too early and miss errors in missing attributes that get narrowed.",
870905
test_listcomp_if_control_flow,
871906
r#"
872907
class C: pass
873908
class D(C): pass
874909
def accepts_d(x: D) -> None: pass
875910
def f(x: list[C], z: C):
876-
# if statement control flow works as expected, we get an error.
877911
if accepts_d(z) and isinstance(z, D): # E: Argument `C` is not assignable to parameter `x` with type `D`
878912
pass
879-
# But here we don't get an error, it appears the narrowed binding is applied too early.
880-
[y for y in x if (accepts_d(y) and isinstance(y, D))]
881-
# Here we don't get an error, for the same reason - we narrow too early, hiding the no-such-attribute error.
882-
[None for y in x if C.error]
913+
[y for y in x if (accepts_d(y) and isinstance(y, D))] # E: Argument `C` is not assignable to parameter `x` with type `D` in function `accepts_d`
914+
[None for y in x if C.error] # E: Class `C` has no class attribute `error`
883915
"#,
884916
);
885917

0 commit comments

Comments
 (0)