Compare commits

...

2 Commits

Author SHA1 Message Date
Alex Waygood
c872fe4c08 generalize fast path more...? 2026-01-10 15:26:09 +00:00
Alex Waygood
f497167798 use an FxOrderSet in UnionType 2026-01-10 14:07:13 +00:00
9 changed files with 108 additions and 74 deletions

View File

@@ -1833,7 +1833,7 @@ impl<'db> Type<'db> {
///
/// This method may have false negatives, but it should not have false positives. It should be
/// a cheap shallow check, not an exhaustive recursive check.
fn subtyping_is_always_reflexive(self) -> bool {
const fn subtyping_is_always_reflexive(self) -> bool {
match self {
Type::Never
| Type::FunctionLiteral(..)
@@ -1854,6 +1854,7 @@ impl<'db> Type<'db> {
| Type::AlwaysFalsy
| Type::AlwaysTruthy
| Type::PropertyInstance(_)
| Type::TypeVar(_)
// might inherit `Any`, but subtyping is still reflexive
| Type::ClassLiteral(_)
=> true,
@@ -1865,7 +1866,6 @@ impl<'db> Type<'db> {
| Type::Union(_)
| Type::Intersection(_)
| Type::Callable(_)
| Type::TypeVar(_)
| Type::BoundSuper(_)
| Type::TypeIs(_)
| Type::TypeGuard(_)
@@ -11724,8 +11724,8 @@ pub(super) struct MetaclassCandidate<'db> {
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
pub struct UnionType<'db> {
/// The union type includes values in any of these types.
#[returns(deref)]
pub elements: Box<[Type<'db>]>,
#[returns(ref)]
pub elements: FxOrderSet<Type<'db>>,
/// Whether the value pointed to by this type is recursively defined.
/// If `Yes`, union literal widening is performed early.
recursively_defined: RecursivelyDefined,

View File

@@ -36,6 +36,8 @@
//! shares exactly the same possible super-types, and none of them are subtypes of each other
//! (unless exactly the same literal type), we can avoid many unnecessary redundancy checks.
use std::hash::BuildHasherDefault;
use crate::types::enums::{enum_member_literals, enum_metadata};
use crate::types::type_ordering::union_or_intersection_elements_ordering;
use crate::types::{
@@ -706,7 +708,10 @@ impl<'db> UnionBuilder<'db> {
}
pub(crate) fn try_build(self) -> Option<Type<'db>> {
let mut types = vec![];
let mut types: FxOrderSet<Type<'db>> = FxOrderSet::with_capacity_and_hasher(
self.elements.len(),
BuildHasherDefault::default(),
);
for element in self.elements {
match element {
UnionElement::IntLiterals(literals) => {
@@ -721,7 +726,9 @@ impl<'db> UnionBuilder<'db> {
UnionElement::EnumLiterals { literals, .. } => {
types.extend(literals.into_iter().map(Type::EnumLiteral));
}
UnionElement::Type(ty) => types.push(ty),
UnionElement::Type(ty) => {
types.insert(ty);
}
}
}
if self.order_elements {
@@ -730,11 +737,14 @@ impl<'db> UnionBuilder<'db> {
match types.len() {
0 => None,
1 => Some(types[0]),
_ => Some(Type::Union(UnionType::new(
self.db,
types.into_boxed_slice(),
self.recursively_defined,
))),
_ => {
types.shrink_to_fit();
Some(Type::Union(UnionType::new(
self.db,
types,
self.recursively_defined,
)))
}
}
}
}
@@ -847,7 +857,7 @@ impl<'db> IntersectionBuilder<'db> {
db,
enum_member_literals(db, instance.class_literal(db), None)
.expect("Calling `enum_member_literals` on an enum class")
.collect::<Box<[_]>>(),
.collect::<FxOrderSet<_>>(),
RecursivelyDefined::No,
)),
seen_aliases,
@@ -1412,6 +1422,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
mod tests {
use super::{IntersectionBuilder, Type, UnionBuilder, UnionType};
use crate::FxOrderSet;
use crate::db::tests::setup_db;
use crate::place::known_module_symbol;
use crate::types::enums::enum_member_literals;
@@ -1445,7 +1456,7 @@ mod tests {
let t1 = Type::IntLiteral(1);
let union = UnionType::from_elements(&db, [t0, t1]).expect_union();
assert_eq!(union.elements(&db), &[t0, t1]);
assert_eq!(union.elements(&db), &FxOrderSet::from_iter([t0, t1]));
}
#[test]

View File

@@ -4,10 +4,10 @@ use std::fmt::Display;
use itertools::{Either, Itertools};
use ruff_python_ast as ast;
use crate::Db;
use crate::types::KnownClass;
use crate::types::enums::{enum_member_literals, enum_metadata};
use crate::types::tuple::{Tuple, TupleType};
use crate::{Db, FxOrderSet};
use super::Type;
@@ -362,17 +362,17 @@ pub(crate) fn is_expandable_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool {
/// Expands a type into its possible subtypes, if applicable.
///
/// Returns [`None`] if the type cannot be expanded.
fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<FxOrderSet<Type<'db>>> {
// NOTE: Update `is_expandable_type` if this logic changes accordingly.
match ty {
Type::NominalInstance(instance) => {
let class = instance.class(db);
if class.is_known(db, KnownClass::Bool) {
return Some(vec![
return Some(FxOrderSet::from_iter([
Type::BooleanLiteral(true),
Type::BooleanLiteral(false),
]);
]));
}
// If the class is a fixed-length tuple subtype, we expand it to its elements.
@@ -390,7 +390,7 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
})
.multi_cartesian_product()
.map(|types| Type::tuple(TupleType::heterogeneous(db, types)))
.collect::<Vec<_>>();
.collect::<FxOrderSet<_>>();
if expanded.len() == 1 {
// There are no elements in the tuple type that can be expanded.
@@ -409,7 +409,7 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
None
}
Type::Union(union) => Some(union.elements(db).to_vec()),
Type::Union(union) => Some(union.elements(db).clone()),
// We don't handle `type[A | B]` here because it's already stored in the expanded form
// i.e., `type[A] | type[B]` which is handled by the `Type::Union` case.
_ => None,
@@ -418,6 +418,7 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
#[cfg(test)]
mod tests {
use crate::FxOrderSet;
use crate::db::tests::setup_db;
use crate::types::tuple::TupleType;
use crate::types::{KnownClass, Type, UnionType};
@@ -427,12 +428,12 @@ mod tests {
#[test]
fn expand_union_type() {
let db = setup_db();
let types = [
let types = FxOrderSet::from_iter([
KnownClass::Int.to_instance(&db),
KnownClass::Str.to_instance(&db),
KnownClass::Bytes.to_instance(&db),
];
let union_type = UnionType::from_elements(&db, types);
]);
let union_type = UnionType::from_elements(&db, &types);
let expanded = expand_type(&db, union_type).unwrap();
assert_eq!(expanded.len(), types.len());
assert_eq!(expanded, types);
@@ -443,7 +444,8 @@ mod tests {
let db = setup_db();
let bool_instance = KnownClass::Bool.to_instance(&db);
let expanded = expand_type(&db, bool_instance).unwrap();
let expected_types = [Type::BooleanLiteral(true), Type::BooleanLiteral(false)];
let expected_types =
FxOrderSet::from_iter([Type::BooleanLiteral(true), Type::BooleanLiteral(false)]);
assert_eq!(expanded.len(), expected_types.len());
assert_eq!(expanded, expected_types);
}
@@ -477,14 +479,14 @@ mod tests {
UnionType::from_elements(&db, [int_ty, str_ty, bytes_ty]),
],
);
let expected_types = [
let expected_types = FxOrderSet::from_iter([
Type::heterogeneous_tuple(&db, [true_ty, int_ty]),
Type::heterogeneous_tuple(&db, [true_ty, str_ty]),
Type::heterogeneous_tuple(&db, [true_ty, bytes_ty]),
Type::heterogeneous_tuple(&db, [false_ty, int_ty]),
Type::heterogeneous_tuple(&db, [false_ty, str_ty]),
Type::heterogeneous_tuple(&db, [false_ty, bytes_ty]),
];
]);
let expanded = expand_type(&db, tuple_type2).unwrap();
assert_eq!(expanded, expected_types);
@@ -498,12 +500,12 @@ mod tests {
str_ty,
],
);
let expected_types = [
let expected_types = FxOrderSet::from_iter([
Type::heterogeneous_tuple(&db, [true_ty, int_ty, str_ty, str_ty]),
Type::heterogeneous_tuple(&db, [true_ty, int_ty, bytes_ty, str_ty]),
Type::heterogeneous_tuple(&db, [false_ty, int_ty, str_ty, str_ty]),
Type::heterogeneous_tuple(&db, [false_ty, int_ty, bytes_ty, str_ty]),
];
]);
let expanded = expand_type(&db, tuple_type3).unwrap();
assert_eq!(expanded, expected_types);

View File

@@ -2784,18 +2784,25 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
// will allow us to error when `*args: P.args` is matched against, for example,
// `n: int` and correctly type check when `*args: P.args` is matched against
// `*args: P.args` (another ParamSpec).
match union.elements(db) {
[paramspec @ Type::TypeVar(typevar), other]
| [other, paramspec @ Type::TypeVar(typevar)]
if typevar.is_paramspec(db) && other.is_unknown() =>
{
VariadicArgumentType::ParamSpec(*paramspec)
}
_ => {
// TODO: Same todo comment as in the non-paramspec case below
VariadicArgumentType::Other(argument_type.iterate(db))
let elements = union.elements(db);
let mut paramspec = None;
if elements.len() == 2 {
match (elements[0], elements[1]) {
(Type::TypeVar(typevar), Type::Dynamic(_))
| (Type::Dynamic(_), Type::TypeVar(typevar))
if typevar.is_paramspec(db) =>
{
paramspec = Some(Type::TypeVar(typevar));
}
_ => {}
}
}
if let Some(paramspec) = paramspec {
VariadicArgumentType::ParamSpec(paramspec)
} else {
// TODO: Same todo comment as in the non-paramspec case below
VariadicArgumentType::Other(argument_type.iterate(db))
}
}
Some(paramspec @ Type::TypeVar(typevar)) if typevar.is_paramspec(db) => {
VariadicArgumentType::ParamSpec(paramspec)
@@ -2915,15 +2922,20 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
let value_type = match argument_type {
Some(argument_type @ Type::Union(union)) => {
// See the comment in `match_variadic` for why we special case this situation.
match union.elements(db) {
[paramspec @ Type::TypeVar(typevar), other]
| [other, paramspec @ Type::TypeVar(typevar)]
if typevar.is_paramspec(db) && other.is_unknown() =>
{
*paramspec
let elements = union.elements(db);
let mut paramspec = None;
if elements.len() == 2 {
match (elements[0], elements[1]) {
(Type::TypeVar(typevar), Type::Dynamic(_))
| (Type::Dynamic(_), Type::TypeVar(typevar))
if typevar.is_paramspec(db) =>
{
paramspec = Some(Type::TypeVar(typevar));
}
_ => {}
}
_ => dunder_getitem_return_type(argument_type),
}
paramspec.unwrap_or_else(|| dunder_getitem_return_type(argument_type))
}
Some(paramspec @ Type::TypeVar(typevar)) if typevar.is_paramspec(db) => paramspec,
Some(argument_type) => dunder_getitem_return_type(argument_type),
@@ -3572,15 +3584,20 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
let value_type = match argument_type {
Type::Union(union) => {
// See the comment in `match_variadic` for why we special case this situation.
match union.elements(self.db) {
[paramspec @ Type::TypeVar(typevar), other]
| [other, paramspec @ Type::TypeVar(typevar)]
if typevar.is_paramspec(self.db) && other.is_unknown() =>
{
Some(*paramspec)
let elements = union.elements(self.db);
let mut paramspec = None;
if elements.len() == 2 {
match (elements[0], elements[1]) {
(Type::TypeVar(typevar), Type::Dynamic(_))
| (Type::Dynamic(_), Type::TypeVar(typevar))
if typevar.is_paramspec(self.db) =>
{
paramspec = Some(Type::TypeVar(typevar));
}
_ => {}
}
_ => value_type_fallback(argument_type),
}
paramspec.or_else(|| value_type_fallback(argument_type))
}
Type::TypeVar(typevar) if typevar.is_paramspec(self.db) => Some(argument_type),
_ => value_type_fallback(argument_type),

View File

@@ -1,6 +1,7 @@
use std::collections::HashMap;
use crate::FxIndexSet;
use crate::FxOrderSet;
use crate::place::builtins_module_scope;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::definition::DefinitionKind;
@@ -228,8 +229,8 @@ pub fn definitions_for_attribute<'db>(
};
let tys = match lhs_ty {
Type::Union(union) => union.elements(model.db()).to_vec(),
_ => vec![lhs_ty],
Type::Union(union) => union.elements(model.db()).clone(),
_ => FxOrderSet::from_iter([lhs_ty]),
};
// Expand intersections for each subtype into their components

View File

@@ -7268,15 +7268,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.any(|overload| overload.signature.generic_context.is_some());
// If the type context is a union, attempt to narrow to a specific element.
let narrow_targets: &[_] = match call_expression_tcx.annotation {
let narrow_targets = match call_expression_tcx.annotation {
// TODO: We could theoretically attempt to narrow to every element of
// the power set of this union. However, this leads to an exponential
// explosion of inference attempts, and is rarely needed in practice.
//
// We only need to attempt narrowing on generic calls, otherwise the type
// context has no effect.
Some(Type::Union(union)) if has_generic_context => union.elements(db),
_ => &[],
Some(Type::Union(union)) if has_generic_context => {
Either::Left(union.elements(db).iter().copied())
}
_ => Either::Right(std::iter::empty()),
};
// We silence diagnostics until we successfully narrow to a specific type.
@@ -7346,10 +7348,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// Prefer the declared type of generic classes.
for narrowed_ty in narrow_targets
.iter()
.clone()
.filter(|ty| ty.class_specialization(db).is_some())
{
if let Some(result) = try_narrow(*narrowed_ty) {
if let Some(result) = try_narrow(narrowed_ty) {
return result;
}
}
@@ -7358,11 +7360,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
//
// TODO: We could also attempt an inference without type context, but this
// leads to similar performance issues.
for narrowed_ty in narrow_targets
.iter()
.filter(|ty| ty.class_specialization(db).is_none())
{
if let Some(result) = try_narrow(*narrowed_ty) {
for narrowed_ty in narrow_targets.filter(|ty| ty.class_specialization(db).is_none()) {
if let Some(result) = try_narrow(narrowed_ty) {
return result;
}
}

View File

@@ -467,23 +467,24 @@ impl<'db> Type<'db> {
//
// However, there is one exception to this general rule: for any given typevar `T`,
// `T` will always be a subtype of any union containing `T`.
(Type::TypeVar(bound_typevar), Type::Union(union))
if !bound_typevar.is_inferable(db, inferable)
(_, Type::Union(union))
if (!relation.is_subtyping() || self.subtyping_is_always_reflexive())
&& union.elements(db).contains(&self) =>
{
ConstraintSet::from(true)
}
// A similar rule applies in reverse to intersection types.
(Type::Intersection(intersection), Type::TypeVar(bound_typevar))
if !bound_typevar.is_inferable(db, inferable)
(Type::Intersection(intersection), _)
if (!relation.is_subtyping() || target.subtyping_is_always_reflexive())
&& intersection.positive(db).contains(&target) =>
{
ConstraintSet::from(true)
}
(Type::Intersection(intersection), Type::TypeVar(bound_typevar))
if !bound_typevar.is_inferable(db, inferable)
&& intersection.negative(db).contains(&target) =>
(Type::Intersection(intersection), _)
if (!relation.is_subtyping() || target.subtyping_is_always_reflexive())
&& intersection.negative(db).contains(&target)
&& !intersection.positive(db).iter().any(Type::is_dynamic) =>
{
ConstraintSet::from(false)
}

View File

@@ -1785,7 +1785,7 @@ impl<'db> Tuple<Type<'db>> {
// TODO: just grab this type from typeshed (it's a `sys._ReleaseLevel` type alias there)
let release_level_ty = {
let elements: Box<[Type<'db>]> = ["alpha", "beta", "candidate", "final"]
let elements: FxOrderSet<_> = ["alpha", "beta", "candidate", "final"]
.iter()
.map(|level| Type::string_literal(db, level))
.collect();

View File

@@ -1,5 +1,6 @@
use std::borrow::Cow;
use itertools::Either;
use ruff_db::parsed::ParsedModuleRef;
use rustc_hash::FxHashMap;
@@ -124,11 +125,13 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
// See <https://github.com/astral-sh/ruff/pull/20377#issuecomment-3401380305>
// for more discussion.
let unpack_types = match value_ty {
Type::Union(union_ty) => union_ty.elements(self.db()),
_ => std::slice::from_ref(&value_ty),
Type::Union(union_ty) => {
Either::Left(union_ty.elements(self.db()).iter().copied())
}
_ => Either::Right(std::iter::once(value_ty)),
};
for ty in unpack_types.iter().copied() {
for ty in unpack_types {
let tuple = ty.try_iterate(self.db()).unwrap_or_else(|err| {
err.report_diagnostic(&self.context, ty, value_expr);
Cow::Owned(TupleSpec::homogeneous(err.fallback_element_type(self.db())))