Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/pyrefly_types/src/type_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
5 changes: 4 additions & 1 deletion pyrefly/lib/alt/narrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
)
}
}
Expand Down
11 changes: 11 additions & 0 deletions pyrefly/lib/binding/narrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
64 changes: 64 additions & 0 deletions pyrefly/lib/test/narrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
"#,
);
Loading