From 367b9d555683636d4da591e0a875f626123c43d9 Mon Sep 17 00:00:00 2001 From: Jake VanderPlas Date: Fri, 16 Oct 2026 11:34:29 -0700 Subject: [PATCH] fix https://github.com/facebook/pyrefly/issues/5255 --- crates/pyrefly_types/src/type_info.rs | 2 +- pyrefly/lib/alt/narrow.rs | 5 ++- pyrefly/lib/binding/narrow.rs | 11 +++++ pyrefly/lib/test/narrow.rs | 64 +++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/crates/pyrefly_types/src/type_info.rs b/crates/pyrefly_types/src/type_info.rs index d533bc2240..83ff5ee097 100644 --- a/crates/pyrefly_types/src/type_info.rs +++ b/crates/pyrefly_types/src/type_info.rs @@ -939,7 +939,7 @@ fn join_types_impl( && types.iter().any(|t| t.is_any()) { base_ty - } else if is_narrow { + } else if is_narrow && !base_ty.is_any() { // Check for the case where `base_ty` is directly in the merge before doing // a subset check. We do this to avoid the possibility of pinning a `Var` to // itself inside the join (which at one point caused a stack overflow). diff --git a/pyrefly/lib/alt/narrow.rs b/pyrefly/lib/alt/narrow.rs index 0bf807fb64..cc085e0bdd 100644 --- a/pyrefly/lib/alt/narrow.rs +++ b/pyrefly/lib/alt/narrow.rs @@ -2081,6 +2081,9 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { range: TextRange, errors: &ErrorCollector, ) -> TypeInfo { + if op.is_placeholder() { + return type_info.clone(); + } match op { NarrowOp::Atomic( subject, @@ -2319,7 +2322,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { branches, &|tys| self.unions(tys), &|got, want| self.is_subset_eq(got, want), - JoinStyle::SimpleMerge, + JoinStyle::NarrowOf(type_info), ) } } diff --git a/pyrefly/lib/binding/narrow.rs b/pyrefly/lib/binding/narrow.rs index 7d494d21f0..38005b8c30 100644 --- a/pyrefly/lib/binding/narrow.rs +++ b/pyrefly/lib/binding/narrow.rs @@ -539,6 +539,17 @@ impl NarrowOp { } } + /// Whether this operation imposes no narrowing constraint (for example, an `Or` + /// where at least one branch is an unconstrained `Placeholder`). + pub fn is_placeholder(&self) -> bool { + match self { + Self::Atomic(_, AtomicNarrowOp::Placeholder) => true, + Self::Atomic(..) => false, + Self::And(ops) => ops.iter().all(Self::is_placeholder), + Self::Or(ops) => ops.iter().any(Self::is_placeholder), + } + } + fn and(&mut self, other: Self) { match self { Self::And(ops) => ops.push(other), diff --git a/pyrefly/lib/test/narrow.rs b/pyrefly/lib/test/narrow.rs index ec5f853402..67925c7fba 100644 --- a/pyrefly/lib/test/narrow.rs +++ b/pyrefly/lib/test/narrow.rs @@ -4648,3 +4648,67 @@ def test_dict(x: dict[str, object] | dict[Never, object]): "#, ); + +// https://github.com/facebook/pyrefly/issues/5255 +testcase!( + test_narrow_or_placeholder_gradual, + r#" +from typing import Any, assert_type + +class A: + val: int | None + +class B(A): + pass + +def nested_flag_and(x, flag: bool) -> int: + flag = flag and x is not None + if x is not None: + if flag: + return 0 + else: + assert_type(x, Any) + return x - 1 + return 0 + +def get_vmap_pattern(ref_dim, idx_is_batched: bool) -> int: + will_add_int_batcher = ref_dim is not None and idx_is_batched + if ref_dim is not None: + if will_add_int_batcher: + return 0 + else: + assert_type(ref_dim, Any) + return ref_dim - 1 + return 0 + +def swap_vmap_pattern(ref_dim, val_dim, indexers: bool) -> int: + ref_is_batched = ref_dim is not None + val_is_batched = val_dim is not None + if not ref_is_batched: + raise ValueError() + if not indexers: + if ref_is_batched and not val_is_batched: + return 0 + else: + assert_type(ref_dim, Any) + return ref_dim - 1 + return 0 + +def non_literal_eq_branch(x, y: int) -> None: + if x is not None and x != y: + pass + else: + assert_type(x, Any) + +def preserve_facets_and_subtypes(x: A, flag: bool) -> None: + if x.val is not None: + if x is not None and flag: + pass + else: + assert_type(x.val, int) + if not isinstance(x, B) and flag: + pass + else: + assert_type(x, A) + "#, +);