Divergent is not equivalent to other dynamic types

This commit is contained in:
Shunsuke Shibayama 2025-09-04 01:44:10 +09:00
parent 7e418a75ee
commit aaa66f3e4d
3 changed files with 35 additions and 93 deletions

View file

@ -352,6 +352,15 @@ def call_divergent(x: int):
# TODO: it would be better to reveal `tuple[Divergent | None, int]`
reveal_type(call_divergent(1)) # revealed: Divergent
def tuple_obj(cond: bool):
if cond:
x = object()
else:
x = tuple_obj(cond)
return (x,)
reveal_type(tuple_obj(True)) # revealed: tuple[object]
def get_non_empty(node):
for child in node.children:
node = get_non_empty(child)

View file

@ -1905,6 +1905,10 @@ impl<'db> Type<'db> {
}
match (self, other) {
// The `Divergent` type is a special type that is not equivalent to other kinds of dynamic types,
// which prevents `Divergent` from being eliminated during union reduction.
(Type::Dynamic(_), Type::Dynamic(DynamicType::Divergent))
| (Type::Dynamic(DynamicType::Divergent), Type::Dynamic(_)) => C::unsatisfiable(db),
(Type::Dynamic(_), Type::Dynamic(_)) => C::always_satisfiable(db),
(Type::SubclassOf(first), Type::SubclassOf(second)) => {
@ -10797,65 +10801,41 @@ pub(crate) mod tests {
let div = Type::Dynamic(DynamicType::Divergent);
// The `Divergent` type must not be eliminated in union with other dynamic types,
// as this would prevent detection of divergent type inference using `Divergent`.
let union = UnionType::from_elements(&db, [Type::unknown(), div]);
assert_eq!(union.display(&db).to_string(), "Unknown | Divergent");
let union = UnionType::from_elements(&db, [div, Type::unknown()]);
assert_eq!(union.display(&db).to_string(), "Divergent");
assert_eq!(union.display(&db).to_string(), "Divergent | Unknown");
let union = UnionType::from_elements(&db, [div, Type::unknown(), todo_type!("1")]);
assert_eq!(union.display(&db).to_string(), "Divergent | Unknown");
assert!(div.is_equivalent_to(&db, div));
assert!(!div.is_equivalent_to(&db, Type::unknown()));
assert!(!Type::unknown().is_equivalent_to(&db, div));
// The `object` type has a good convergence property, that is, its union with all other types is `object`.
// (e.g. `object | tuple[Divergent] == object`, `object | tuple[object] == object`)
// So we can safely eliminate `Divergent`.
let union = UnionType::from_elements(&db, [div, KnownClass::Object.to_instance(&db)]);
assert_eq!(union.display(&db).to_string(), "object | Divergent");
assert_eq!(union.display(&db).to_string(), "object");
let union = UnionType::from_elements(&db, [KnownClass::Object.to_instance(&db), div]);
assert_eq!(union.display(&db).to_string(), "object | Divergent");
let union = UnionType::from_elements(
&db,
[
KnownClass::Object.to_instance(&db),
KnownClass::List.to_specialized_instance(&db, [div]),
],
);
assert_eq!(union.display(&db).to_string(), "object | list[Divergent]");
let union = UnionType::from_elements(
&db,
[
KnownClass::Object.to_instance(&db),
KnownClass::List.to_specialized_instance(&db, [div]),
KnownClass::Int.to_instance(&db),
],
);
assert_eq!(union.display(&db).to_string(), "object | list[Divergent]");
assert_eq!(union.display(&db).to_string(), "object");
// The same can be said about intersections for the `Never` type.
let intersection = IntersectionBuilder::new(&db)
.add_positive(Type::Never)
.add_positive(div)
.build();
assert_eq!(intersection.display(&db).to_string(), "Never & Divergent");
assert_eq!(intersection.display(&db).to_string(), "Never");
let intersection = IntersectionBuilder::new(&db)
.add_positive(div)
.add_positive(Type::Never)
.build();
assert_eq!(intersection.display(&db).to_string(), "Divergent & Never");
let intersection = IntersectionBuilder::new(&db)
.add_positive(KnownClass::List.to_specialized_instance(&db, [div]))
.add_positive(Type::Never)
.build();
assert_eq!(
intersection.display(&db).to_string(),
"list[Divergent] & Never"
);
let intersection = IntersectionBuilder::new(&db)
.add_positive(KnownClass::Int.to_instance(&db))
.add_positive(KnownClass::List.to_specialized_instance(&db, [div]))
.build();
assert_eq!(
intersection.display(&db).to_string(),
"int & list[Divergent]"
);
assert_eq!(intersection.display(&db).to_string(), "Never");
}
}

View file

@ -40,7 +40,7 @@
use crate::types::enums::{enum_member_literals, enum_metadata};
use crate::types::type_ordering::union_or_intersection_elements_ordering;
use crate::types::{
BytesLiteralType, DynamicType, IntersectionType, KnownClass, StringLiteralType, Type,
BytesLiteralType, IntersectionType, KnownClass, StringLiteralType, Type,
TypeVarBoundOrConstraints, UnionType,
};
use crate::{Db, FxOrderSet};
@ -241,22 +241,9 @@ impl<'db> UnionBuilder<'db> {
/// Collapse the union to a single type: `object`.
fn collapse_to_object(&mut self) {
let divergent = self.elements.iter().find_map(|elem| {
let UnionElement::Type(ty) = elem else {
return None;
};
if ty.has_divergent_type(self.db) {
Some(*ty)
} else {
None
}
});
self.elements.clear();
self.elements
.push(UnionElement::Type(Type::object(self.db)));
if let Some(divergent) = divergent {
self.elements.push(UnionElement::Type(divergent));
}
}
/// Adds a type to this union.
@ -465,15 +452,6 @@ impl<'db> UnionBuilder<'db> {
ty if ty.is_object(self.db) => {
self.collapse_to_object();
}
Type::Dynamic(DynamicType::Divergent) => {
if !self
.elements
.iter()
.any(|elem| matches!(elem, UnionElement::Type(elem) if ty == *elem))
{
self.elements.push(UnionElement::Type(ty));
}
}
_ => {
let bool_pair = if let Type::BooleanLiteral(b) = ty {
Some(Type::BooleanLiteral(!b))
@ -493,16 +471,6 @@ impl<'db> UnionBuilder<'db> {
Type::Never // won't be used
};
if ty.has_divergent_type(self.db)
&& !self
.elements
.iter()
.any(|elem| matches!(elem, UnionElement::Type(elem) if ty == *elem))
{
self.elements.push(UnionElement::Type(ty));
return;
}
for (index, element) in self.elements.iter_mut().enumerate() {
let element_type = match element.try_reduce(self.db, ty) {
ReduceResult::KeepIf(keep) => {
@ -896,9 +864,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
self.add_positive(db, Type::LiteralString);
self.add_negative(db, Type::string_literal(db, ""));
}
Type::Dynamic(DynamicType::Divergent) => {
self.positive.insert(new_positive);
}
_ => {
let known_instance = new_positive
.into_nominal_instance()
@ -967,9 +933,6 @@ impl<'db> InnerIntersectionBuilder<'db> {
let mut to_remove = SmallVec::<[usize; 1]>::new();
for (index, existing_positive) in self.positive.iter().enumerate() {
if new_positive.has_divergent_type(db) {
break;
}
// S & T = S if S <: T
if existing_positive.is_subtype_of(db, new_positive)
|| existing_positive.is_equivalent_to(db, new_positive)
@ -977,18 +940,11 @@ impl<'db> InnerIntersectionBuilder<'db> {
return;
}
// same rule, reverse order
if new_positive.is_subtype_of(db, *existing_positive)
&& !existing_positive.has_divergent_type(db)
{
if new_positive.is_subtype_of(db, *existing_positive) {
to_remove.push(index);
}
// A & B = Never if A and B are disjoint
if new_positive.is_disjoint_from(db, *existing_positive)
&& self
.positive
.iter()
.all(|existing_positive| !existing_positive.has_divergent_type(db))
{
if new_positive.is_disjoint_from(db, *existing_positive) {
*self = Self::default();
self.positive.insert(Type::Never);
return;
@ -1000,9 +956,6 @@ impl<'db> InnerIntersectionBuilder<'db> {
let mut to_remove = SmallVec::<[usize; 1]>::new();
for (index, existing_negative) in self.negative.iter().enumerate() {
if new_positive.has_divergent_type(db) {
break;
}
// S & ~T = Never if S <: T
if new_positive.is_subtype_of(db, *existing_negative) {
*self = Self::default();