[ty] Homogeneous and mixed tuples (#18600)
Some checks are pending
CI / cargo fuzz build (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

We already had support for homogeneous tuples (`tuple[int, ...]`). This
PR extends this to also support mixed tuples (`tuple[str, str,
*tuple[int, ...], str str]`).

A mixed tuple consists of a fixed-length (possibly empty) prefix and
suffix, and a variable-length portion in the middle. Every element of
the variable-length portion must be of the same type. A homogeneous
tuple is then just a mixed tuple with an empty prefix and suffix.

The new data representation uses different Rust types for a fixed-length
(aka heterogeneous) tuple. Another option would have been to use the
`VariableLengthTuple` representation for all tuples, and to wrap the
"variable + suffix" portion in an `Option`. I don't think that would
simplify the method implementations much, though, since we would still
have a 2×2 case analysis for most of them.

One wrinkle is that the definition of the `tuple` class in the typeshed
has a single typevar, and canonically represents a homogeneous tuple.
When getting the class of a tuple instance, that means that we have to
summarize our detailed mixed tuple type information into its
"homogeneous supertype". (We were already doing this for heterogeneous
types.)

A similar thing happens when concatenating two mixed tuples: the
variable-length portion and suffix of the LHS, and the prefix and
variable-length portion of the RHS, all get unioned into the
variable-length portion of the result. The LHS prefix and RHS suffix
carry through unchanged.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Douglas Creager 2025-06-20 18:23:54 -04:00 committed by GitHub
parent d9266284df
commit ea812d0813
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 2432 additions and 758 deletions

View file

@ -51,6 +51,7 @@ use crate::types::infer::infer_unpack_types;
use crate::types::mro::{Mro, MroError, MroIterator};
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
use crate::types::signatures::{Parameter, ParameterForm, Parameters};
use crate::types::tuple::{TupleSpec, TupleType};
pub use crate::util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
use crate::{Db, FxOrderSet, Module, Program};
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass};
@ -78,6 +79,7 @@ mod slots;
mod special_form;
mod string_annotation;
mod subclass_of;
mod tuple;
mod type_ordering;
mod unpacker;
@ -543,8 +545,10 @@ pub enum Type<'db> {
LiteralString,
/// A bytes literal
BytesLiteral(BytesLiteralType<'db>),
/// A heterogeneous tuple type, with elements of the given types in source order.
// TODO: Support variable length homogeneous tuple type like `tuple[int, ...]`.
/// An instance of the builtin `tuple` class.
/// TODO: Consider removing this in favor of `NominalInstance`. This is currently stored as a
/// separate variant partly for historical reasons, and partly to allow us to easily
/// distinguish tuples since they occur so often.
Tuple(TupleType<'db>),
/// An instance of a typevar in a generic class or function. When the generic class or function
/// is specialized, we will replace this typevar with its specialization.
@ -720,13 +724,7 @@ impl<'db> Type<'db> {
.map(|ty| ty.materialize(db, variance.flip())),
)
.build(),
Type::Tuple(tuple_type) => TupleType::from_elements(
db,
tuple_type
.elements(db)
.iter()
.map(|ty| ty.materialize(db, variance)),
),
Type::Tuple(tuple_type) => Type::tuple(db, tuple_type.materialize(db, variance)),
Type::TypeVar(type_var) => Type::TypeVar(type_var.materialize(db, variance)),
Type::TypeIs(type_is) => {
type_is.with_type(db, type_is.return_type(db).materialize(db, variance))
@ -770,8 +768,8 @@ impl<'db> Type<'db> {
Self::Tuple(tuple) => TupleType::from_elements(
db,
tuple
.elements(db)
.iter()
.tuple(db)
.all_elements()
.map(|ty| ty.replace_self_reference(db, class)),
),
@ -881,8 +879,8 @@ impl<'db> Type<'db> {
}
Self::Tuple(tuple) => tuple
.elements(db)
.iter()
.tuple(db)
.all_elements()
.any(|ty| ty.any_over_type(db, type_fn)),
Self::Union(union) => union
@ -1076,13 +1074,6 @@ impl<'db> Type<'db> {
.expect("Expected a Type::IntLiteral variant")
}
pub const fn into_tuple(self) -> Option<TupleType<'db>> {
match self {
Type::Tuple(tuple_type) => Some(tuple_type),
_ => None,
}
}
pub const fn is_boolean_literal(&self) -> bool {
matches!(self, Type::BooleanLiteral(..))
}
@ -1141,7 +1132,7 @@ impl<'db> Type<'db> {
match self {
Type::Union(union) => Type::Union(union.normalized(db)),
Type::Intersection(intersection) => Type::Intersection(intersection.normalized(db)),
Type::Tuple(tuple) => Type::Tuple(tuple.normalized(db)),
Type::Tuple(tuple) => Type::tuple(db, tuple.normalized(db)),
Type::Callable(callable) => Type::Callable(callable.normalized(db)),
Type::ProtocolInstance(protocol) => protocol.normalized(db),
Type::NominalInstance(instance) => Type::NominalInstance(instance.normalized(db)),
@ -1441,27 +1432,23 @@ impl<'db> Type<'db> {
false
}
// A fully static heterogeneous tuple type `A` is a subtype of a fully static heterogeneous tuple type `B`
// iff the two tuple types have the same number of elements and each element-type in `A` is a subtype
// of the element-type at the same index in `B`. (Now say that 5 times fast.)
//
// For example: `tuple[bool, bool]` is a subtype of `tuple[int, int]`,
// but `tuple[bool, bool, bool]` is not a subtype of `tuple[int, int]`
(Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => {
let self_elements = self_tuple.elements(db);
let target_elements = target_tuple.elements(db);
self_elements.len() == target_elements.len()
&& self_elements.iter().zip(target_elements).all(
|(self_element, target_element)| {
self_element.has_relation_to(db, *target_element, relation)
},
)
self_tuple.has_relation_to(db, target_tuple, relation)
}
// `tuple[A, B, C]` is a subtype of `tuple[A | B | C, ...]`
(Type::Tuple(tuple), _) => tuple
.homogeneous_supertype(db)
.has_relation_to(db, target, relation),
(Type::Tuple(self_tuple), Type::NominalInstance(target_instance)) => {
self_tuple.to_class_type(db).is_some_and(|self_class| {
self_class.has_relation_to(db, target_instance.class, relation)
})
}
(Type::NominalInstance(self_instance), Type::Tuple(target_tuple)) => {
target_tuple.to_class_type(db).is_some_and(|target_class| {
self_instance
.class
.has_relation_to(db, target_class, relation)
})
}
(Type::Tuple(_), _) => false,
(Type::BoundSuper(_), Type::BoundSuper(_)) => relation.are_equivalent(db, self, target),
(Type::BoundSuper(_), _) => KnownClass::Super
@ -1961,14 +1948,15 @@ impl<'db> Type<'db> {
!known_instance.is_instance_of(db, instance.class)
}
(
known_instance_ty @ (Type::SpecialForm(_) | Type::KnownInstance(_)),
Type::Tuple(tuple),
)
| (
Type::Tuple(tuple),
known_instance_ty @ (Type::SpecialForm(_) | Type::KnownInstance(_)),
) => known_instance_ty.is_disjoint_from(db, tuple.homogeneous_supertype(db)),
(Type::SpecialForm(special_form), Type::Tuple(tuple))
| (Type::Tuple(tuple), Type::SpecialForm(special_form)) => tuple
.to_class_type(db)
.is_some_and(|tuple_class| !special_form.is_instance_of(db, tuple_class)),
(Type::KnownInstance(known_instance), Type::Tuple(tuple))
| (Type::Tuple(tuple), Type::KnownInstance(known_instance)) => tuple
.to_class_type(db)
.is_some_and(|tuple_class| !known_instance.is_instance_of(db, tuple_class)),
(Type::BooleanLiteral(..) | Type::TypeIs(_), Type::NominalInstance(instance))
| (Type::NominalInstance(instance), Type::BooleanLiteral(..) | Type::TypeIs(_)) => {
@ -2113,18 +2101,14 @@ impl<'db> Type<'db> {
}
(Type::Tuple(tuple), Type::Tuple(other_tuple)) => {
let self_elements = tuple.elements(db);
let other_elements = other_tuple.elements(db);
self_elements.len() != other_elements.len()
|| self_elements
.iter()
.zip(other_elements)
.any(|(e1, e2)| e1.is_disjoint_from(db, *e2))
tuple.is_disjoint_from(db, other_tuple)
}
(Type::Tuple(tuple), instance @ Type::NominalInstance(_))
| (instance @ Type::NominalInstance(_), Type::Tuple(tuple)) => {
instance.is_disjoint_from(db, tuple.homogeneous_supertype(db))
(Type::Tuple(tuple), Type::NominalInstance(instance))
| (Type::NominalInstance(instance), Type::Tuple(tuple)) => {
tuple.to_class_type(db).is_some_and(|tuple_class| {
instance.is_disjoint_from_nominal_instance_of_class(db, tuple_class)
})
}
(Type::PropertyInstance(_), other) | (other, Type::PropertyInstance(_)) => {
@ -2203,10 +2187,7 @@ impl<'db> Type<'db> {
// containing gradual forms such as `tuple[Any, ...]`.
// Conversely, make sure to return `true` for homogeneous tuples such as
// `tuple[int, ...]`, once we add support for them.
Type::Tuple(tuple) => tuple
.elements(db)
.iter()
.all(|elem| elem.is_fully_static(db)),
Type::Tuple(tuple) => tuple.is_fully_static(db),
Type::Callable(callable) => callable.is_fully_static(db),
Type::TypeIs(type_is) => type_is.return_type(db).is_fully_static(db),
}
@ -2379,11 +2360,7 @@ impl<'db> Type<'db> {
false
}
Type::Tuple(tuple) => tuple
.elements(db)
.iter()
.all(|elem| elem.is_single_valued(db)),
Type::Tuple(tuple) => tuple.is_single_valued(db),
Type::NominalInstance(instance) => instance.is_single_valued(db),
Type::BoundSuper(_) => {
@ -2629,7 +2606,10 @@ impl<'db> Type<'db> {
KnownClass::Str.to_instance(db).instance_member(db, name)
}
Type::BytesLiteral(_) => KnownClass::Bytes.to_instance(db).instance_member(db, name),
Type::Tuple(tuple) => tuple.homogeneous_supertype(db).instance_member(db, name),
Type::Tuple(tuple) => tuple
.to_class_type(db)
.map(|class| class.instance_member(db, name))
.unwrap_or(Place::Unbound.into()),
Type::AlwaysTruthy | Type::AlwaysFalsy => Type::object(db).instance_member(db, name),
Type::ModuleLiteral(_) => KnownClass::ModuleType
@ -3474,7 +3454,7 @@ impl<'db> Type<'db> {
Type::BooleanLiteral(bool) => Truthiness::from(*bool),
Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()),
Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()),
Type::Tuple(items) => Truthiness::from(!items.elements(db).is_empty()),
Type::Tuple(tuple) => Truthiness::from(!tuple.tuple(db).is_empty()),
};
Ok(truthiness)
@ -3505,7 +3485,11 @@ impl<'db> Type<'db> {
let usize_len = match self {
Type::BytesLiteral(bytes) => Some(bytes.python_len(db)),
Type::StringLiteral(string) => Some(string.python_len(db)),
Type::Tuple(tuple) => Some(tuple.len(db)),
Type::Tuple(tuple) => match tuple.tuple(db) {
TupleSpec::Fixed(tuple) => Some(tuple.len()),
TupleSpec::Variable(_) => None,
},
_ => None,
};
@ -3705,10 +3689,7 @@ impl<'db> Type<'db> {
db,
[
KnownClass::Str.to_instance(db),
KnownClass::Tuple.to_specialized_instance(
db,
[KnownClass::Str.to_instance(db)],
),
TupleType::homogeneous(db, KnownClass::Str.to_instance(db)),
],
)),
Parameter::positional_only(Some(Name::new_static("start")))
@ -4022,10 +4003,10 @@ impl<'db> Type<'db> {
Parameter::positional_only(Some(Name::new_static("name")))
.with_annotated_type(str_instance),
Parameter::positional_only(Some(Name::new_static("bases")))
.with_annotated_type(
KnownClass::Tuple
.to_specialized_instance(db, [type_instance]),
),
.with_annotated_type(TupleType::homogeneous(
db,
type_instance,
)),
Parameter::positional_only(Some(Name::new_static("dict")))
.with_annotated_type(
KnownClass::Dict.to_specialized_instance(
@ -4173,16 +4154,16 @@ impl<'db> Type<'db> {
.with_annotated_type(Type::any())
.type_form(),
Parameter::keyword_only(Name::new_static("type_params"))
.with_annotated_type(KnownClass::Tuple.to_specialized_instance(
.with_annotated_type(TupleType::homogeneous(
db,
[UnionType::from_elements(
UnionType::from_elements(
db,
[
KnownClass::TypeVar.to_instance(db),
KnownClass::ParamSpec.to_instance(db),
KnownClass::TypeVarTuple.to_instance(db),
],
)],
),
))
.with_default_type(TupleType::empty(db)),
]),
@ -4476,7 +4457,10 @@ impl<'db> Type<'db> {
/// ```
fn try_iterate(self, db: &'db dyn Db) -> Result<Type<'db>, IterationError<'db>> {
if let Type::Tuple(tuple_type) = self {
return Ok(UnionType::from_elements(db, tuple_type.elements(db)));
return Ok(UnionType::from_elements(
db,
tuple_type.tuple(db).all_elements(),
));
}
if let Type::GenericAlias(alias) = self {
@ -4639,20 +4623,20 @@ impl<'db> Type<'db> {
// have the class's typevars still in the method signature when we attempt to call it. To
// do this, we instead use the _identity_ specialization, which maps each of the class's
// generic typevars to itself.
let (generic_origin, generic_context, self_type) = match self {
Type::ClassLiteral(class) => match class.generic_context(db) {
Some(generic_context) => {
let specialization = generic_context.identity_specialization(db);
(
let (generic_origin, generic_context, self_type) =
match self {
Type::ClassLiteral(class) => match class.generic_context(db) {
Some(generic_context) => (
Some(class),
Some(generic_context),
Type::GenericAlias(GenericAlias::new(db, class, specialization)),
)
}
Type::from(class.apply_specialization(db, |_| {
generic_context.identity_specialization(db)
})),
),
_ => (None, None, self),
},
_ => (None, None, self),
},
_ => (None, None, self),
};
};
// As of now we do not model custom `__call__` on meta-classes, so the code below
// only deals with interplay between `__new__` and `__init__` methods.
@ -4775,11 +4759,7 @@ impl<'db> Type<'db> {
.map(|specialization| {
Type::instance(
db,
ClassType::Generic(GenericAlias::new(
db,
generic_origin,
specialization,
)),
generic_origin.apply_specialization(db, |_| specialization),
)
})
.unwrap_or(instance_ty);
@ -4966,7 +4946,7 @@ impl<'db> Type<'db> {
// We treat `typing.Type` exactly the same as `builtins.type`:
SpecialFormType::Type => Ok(KnownClass::Type.to_instance(db)),
SpecialFormType::Tuple => Ok(KnownClass::Tuple.to_instance(db)),
SpecialFormType::Tuple => Ok(TupleType::homogeneous(db, Type::unknown())),
// Legacy `typing` aliases
SpecialFormType::List => Ok(KnownClass::List.to_instance(db)),
@ -5189,7 +5169,10 @@ impl<'db> Type<'db> {
}
Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Type.to_instance(db),
Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db),
Type::Tuple(_) => KnownClass::Tuple.to_class_literal(db),
Type::Tuple(tuple) => tuple
.to_class_type(db)
.map(Type::from)
.unwrap_or_else(Type::unknown),
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
None => KnownClass::Type.to_instance(db),
@ -5343,12 +5326,7 @@ impl<'db> Type<'db> {
}
builder.build()
}
Type::Tuple(tuple) => TupleType::from_elements(
db,
tuple
.iter(db)
.map(|ty| ty.apply_type_mapping(db, type_mapping)),
),
Type::Tuple(tuple) => Type::Tuple(tuple.apply_type_mapping(db, type_mapping)),
Type::TypeIs(type_is) => type_is.with_type(db, type_is.return_type(db).apply_type_mapping(db, type_mapping)),
@ -5439,10 +5417,9 @@ impl<'db> Type<'db> {
negative.find_legacy_typevars(db, typevars);
}
}
Type::Tuple(tuple) => {
for element in tuple.iter(db) {
element.find_legacy_typevars(db, typevars);
}
tuple.find_legacy_typevars(db, typevars);
}
Type::GenericAlias(alias) => {
@ -8134,89 +8111,6 @@ impl<'db> BytesLiteralType<'db> {
}
}
/// # Ordering
/// Ordering is based on the tuple's salsa-assigned id and not on its elements.
/// The id may change between runs, or when the tuple was garbage collected and recreated.
#[salsa::interned(debug)]
#[derive(PartialOrd, Ord)]
pub struct TupleType<'db> {
#[returns(deref)]
elements: Box<[Type<'db>]>,
}
impl<'db> TupleType<'db> {
fn homogeneous_supertype(self, db: &'db dyn Db) -> Type<'db> {
KnownClass::Tuple
.to_specialized_instance(db, [UnionType::from_elements(db, self.elements(db))])
}
pub(crate) fn empty(db: &'db dyn Db) -> Type<'db> {
Type::Tuple(TupleType::new(db, Box::<[Type<'db>]>::from([])))
}
pub(crate) fn from_elements<T: Into<Type<'db>>>(
db: &'db dyn Db,
types: impl IntoIterator<Item = T>,
) -> Type<'db> {
let mut elements = vec![];
for ty in types {
let ty = ty.into();
if ty.is_never() {
return Type::Never;
}
elements.push(ty);
}
Type::Tuple(Self::new(db, elements.into_boxed_slice()))
}
/// Return a normalized version of `self`.
///
/// See [`Type::normalized`] for more details.
#[must_use]
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
let elements: Box<[Type<'db>]> = self
.elements(db)
.iter()
.map(|ty| ty.normalized(db))
.collect();
TupleType::new(db, elements)
}
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
let self_elements = self.elements(db);
let other_elements = other.elements(db);
self_elements.len() == other_elements.len()
&& self_elements
.iter()
.zip(other_elements)
.all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty))
}
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
let self_elements = self.elements(db);
let other_elements = other.elements(db);
self_elements.len() == other_elements.len()
&& self_elements
.iter()
.zip(other_elements)
.all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty))
}
pub fn get(&self, db: &'db dyn Db, index: usize) -> Option<Type<'db>> {
self.elements(db).get(index).copied()
}
pub fn len(&self, db: &'db dyn Db) -> usize {
self.elements(db).len()
}
pub fn iter(&self, db: &'db dyn Db) -> impl Iterator<Item = Type<'db>> + 'db + '_ {
self.elements(db).iter().copied()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum BoundSuperError<'db> {
InvalidPivotClassType {

View file

@ -4,7 +4,8 @@ use std::ops::{Deref, DerefMut};
use itertools::{Either, Itertools};
use crate::Db;
use crate::types::{KnownClass, TupleType};
use crate::types::KnownClass;
use crate::types::tuple::{TupleSpec, TupleType};
use super::Type;
@ -210,11 +211,15 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
Type::BooleanLiteral(false),
])
}
Type::Tuple(tuple) => {
Type::Tuple(tuple_type) => {
// Note: This should only account for tuples of known length, i.e., `tuple[bool, ...]`
// should not be expanded here.
let tuple = tuple_type.tuple(db);
if !matches!(tuple, TupleSpec::Fixed(_)) {
return None;
}
let expanded = tuple
.iter(db)
.all_elements()
.map(|element| {
if let Some(expanded) = expand_type(db, element) {
Either::Left(expanded.into_iter())
@ -242,7 +247,8 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
#[cfg(test)]
mod tests {
use crate::db::tests::setup_db;
use crate::types::{KnownClass, TupleType, Type, UnionType};
use crate::types::tuple::TupleType;
use crate::types::{KnownClass, Type, UnionType};
use super::expand_type;
@ -308,7 +314,6 @@ mod tests {
TupleType::from_elements(&db, [false_ty, bytes_ty]),
];
let expanded = expand_type(&db, tuple_type2).unwrap();
assert_eq!(expanded.len(), expected_types.len());
assert_eq!(expanded, expected_types);
// Mixed set of elements where some can be expanded while others cannot be.
@ -328,7 +333,16 @@ mod tests {
TupleType::from_elements(&db, [false_ty, int_ty, bytes_ty, str_ty]),
];
let expanded = expand_type(&db, tuple_type3).unwrap();
assert_eq!(expanded.len(), expected_types.len());
assert_eq!(expanded, expected_types);
// Variable-length tuples are not expanded.
let variable_length_tuple = TupleType::mixed(
&db,
[bool_ty],
int_ty,
[UnionType::from_elements(&db, [str_ty, bytes_ty]), str_ty],
);
let expanded = expand_type(&db, variable_length_tuple);
assert!(expanded.is_none());
}
}

View file

@ -27,9 +27,10 @@ use crate::types::function::{
};
use crate::types::generics::{Specialization, SpecializationBuilder, SpecializationError};
use crate::types::signatures::{Parameter, ParameterForm};
use crate::types::tuple::TupleType;
use crate::types::{
BoundMethodType, ClassLiteral, DataclassParams, KnownClass, KnownInstanceType,
MethodWrapperKind, PropertyInstanceType, SpecialFormType, TupleType, TypeMapping, UnionType,
MethodWrapperKind, PropertyInstanceType, SpecialFormType, TypeMapping, UnionType,
WrapperDescriptorKind, ide_support, todo_type,
};
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic};

View file

@ -12,6 +12,7 @@ use crate::semantic_index::definition::{Definition, DefinitionState};
use crate::types::function::{DataclassTransformerParams, KnownFunction};
use crate::types::generics::{GenericContext, Specialization};
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
use crate::types::tuple::TupleType;
use crate::types::{
CallableType, DataclassParams, KnownInstanceType, TypeMapping, TypeRelation, TypeVarInstance,
};
@ -30,8 +31,8 @@ use crate::{
place_table, semantic_index, use_def_map,
},
types::{
CallArgumentTypes, CallError, CallErrorKind, MetaclassCandidate, TupleType, UnionBuilder,
UnionType, definition_expression_type,
CallArgumentTypes, CallError, CallErrorKind, MetaclassCandidate, UnionBuilder, UnionType,
definition_expression_type,
},
};
use indexmap::IndexSet;
@ -203,6 +204,8 @@ impl<'db> GenericAlias<'db> {
db: &'db dyn Db,
typevars: &mut FxOrderSet<TypeVarInstance<'db>>,
) {
// A tuple's specialization will include all of its element types, so we don't need to also
// look in `self.tuple`.
self.specialization(db).find_legacy_typevars(db, typevars);
}
}
@ -761,58 +764,46 @@ impl<'db> ClassLiteral<'db> {
index.expect_single_definition(body_scope.node(db).expect_class(&module))
}
pub(crate) fn apply_specialization(
self,
db: &'db dyn Db,
f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>,
) -> ClassType<'db> {
match self.generic_context(db) {
None => ClassType::NonGeneric(self),
Some(generic_context) => {
let specialization = f(generic_context);
ClassType::Generic(GenericAlias::new(db, self, specialization))
}
}
}
pub(crate) fn apply_optional_specialization(
self,
db: &'db dyn Db,
specialization: Option<Specialization<'db>>,
) -> ClassType<'db> {
match (self.generic_context(db), specialization) {
(None, _) => ClassType::NonGeneric(self),
(Some(generic_context), None) => {
let specialization = generic_context.default_specialization(db);
ClassType::Generic(GenericAlias::new(db, self, specialization))
}
(Some(_), Some(specialization)) => {
ClassType::Generic(GenericAlias::new(db, self, specialization))
}
}
self.apply_specialization(db, |generic_context| {
specialization.unwrap_or_else(|| generic_context.default_specialization(db))
})
}
/// Returns the default specialization of this class. For non-generic classes, the class is
/// returned unchanged. For a non-specialized generic class, we return a generic alias that
/// applies the default specialization to the class's typevars.
pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> {
match self.generic_context(db) {
None => ClassType::NonGeneric(self),
Some(generic_context) => {
let specialization = generic_context.default_specialization(db);
ClassType::Generic(GenericAlias::new(db, self, specialization))
}
}
}
/// Returns a specialization of this class with a `@Todo`-type
pub(crate) fn todo_specialization(self, db: &'db dyn Db, todo: &'static str) -> ClassType<'db> {
match self.generic_context(db) {
None => ClassType::NonGeneric(self),
Some(generic_context) => {
let specialization = generic_context.todo_specialization(db, todo);
ClassType::Generic(GenericAlias::new(db, self, specialization))
}
}
self.apply_specialization(db, |generic_context| {
generic_context.default_specialization(db)
})
}
/// Returns the unknown specialization of this class. For non-generic classes, the class is
/// returned unchanged. For a non-specialized generic class, we return a generic alias that
/// maps each of the class's typevars to `Unknown`.
pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> {
match self.generic_context(db) {
None => ClassType::NonGeneric(self),
Some(generic_context) => {
let specialization = generic_context.unknown_specialization(db);
ClassType::Generic(GenericAlias::new(db, self, specialization))
}
}
self.apply_specialization(db, |generic_context| {
generic_context.unknown_specialization(db)
})
}
/// Return an iterator over the inferred types of this class's *explicit* bases.
@ -2448,22 +2439,20 @@ impl<'db> KnownClass {
.unwrap_or_else(Type::unknown)
}
/// Lookup a [`KnownClass`] in typeshed and return a [`Type`]
/// representing all possible instances of the generic class with a specialization.
/// Lookup a generic [`KnownClass`] in typeshed and return a [`Type`]
/// representing a specialization of that class.
///
/// If the class cannot be found in typeshed, or if you provide a specialization with the wrong
/// number of types, a debug-level log message will be emitted stating this.
pub(crate) fn to_specialized_instance(
pub(crate) fn to_specialized_class_type(
self,
db: &'db dyn Db,
specialization: impl IntoIterator<Item = Type<'db>>,
) -> Type<'db> {
) -> Option<ClassType<'db>> {
let Type::ClassLiteral(class_literal) = self.to_class_literal(db) else {
return Type::unknown();
};
let Some(generic_context) = class_literal.generic_context(db) else {
return Type::instance(db, ClassType::NonGeneric(class_literal));
return None;
};
let generic_context = class_literal.generic_context(db)?;
let types = specialization.into_iter().collect::<Box<[_]>>();
if types.len() != generic_context.len(db) {
@ -2477,21 +2466,32 @@ impl<'db> KnownClass {
self.display(db)
);
}
return Type::instance(db, class_literal.default_specialization(db));
return Some(class_literal.default_specialization(db));
}
let specialization = generic_context.specialize(db, types);
Type::instance(
db,
ClassType::Generic(GenericAlias::new(db, class_literal, specialization)),
)
Some(class_literal.apply_specialization(db, |_| generic_context.specialize(db, types)))
}
/// Lookup a [`KnownClass`] in typeshed and return a [`Type`]
/// representing all possible instances of the generic class with a specialization.
///
/// If the class cannot be found in typeshed, or if you provide a specialization with the wrong
/// number of types, a debug-level log message will be emitted stating this.
pub(crate) fn to_specialized_instance(
self,
db: &'db dyn Db,
specialization: impl IntoIterator<Item = Type<'db>>,
) -> Type<'db> {
self.to_specialized_class_type(db, specialization)
.and_then(|class_type| Type::from(class_type).to_instance(db))
.unwrap_or_else(Type::unknown)
}
/// Attempt to lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal.
///
/// Return an error if the symbol cannot be found in the expected typeshed module,
/// or if the symbol is not a class definition, or if the symbol is possibly unbound.
pub(crate) fn try_to_class_literal(
fn try_to_class_literal_without_logging(
self,
db: &'db dyn Db,
) -> Result<ClassLiteral<'db>, KnownClassLookupError<'db>> {
@ -2511,14 +2511,13 @@ impl<'db> KnownClass {
/// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal.
///
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
pub(crate) fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> {
pub(crate) fn try_to_class_literal(self, db: &'db dyn Db) -> Option<ClassLiteral<'db>> {
// a cache of the `KnownClass`es that we have already failed to lookup in typeshed
// (and therefore that we've already logged a warning for)
static MESSAGES: LazyLock<Mutex<FxHashSet<KnownClass>>> = LazyLock::new(Mutex::default);
self.try_to_class_literal(db)
.map(Type::ClassLiteral)
.unwrap_or_else(|lookup_error| {
self.try_to_class_literal_without_logging(db)
.or_else(|lookup_error| {
if MESSAGES.lock().unwrap().insert(self) {
if matches!(
lookup_error,
@ -2535,12 +2534,22 @@ impl<'db> KnownClass {
match lookup_error {
KnownClassLookupError::ClassPossiblyUnbound { class_literal, .. } => {
class_literal.into()
Ok(class_literal)
}
KnownClassLookupError::ClassNotFound { .. }
| KnownClassLookupError::SymbolNotAClass { .. } => Type::unknown(),
| KnownClassLookupError::SymbolNotAClass { .. } => Err(()),
}
})
.ok()
}
/// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal.
///
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
pub(crate) fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> {
self.try_to_class_literal(db)
.map(Type::ClassLiteral)
.unwrap_or_else(Type::unknown)
}
/// Lookup a [`KnownClass`] in typeshed and return a [`Type`]
@ -2557,7 +2566,7 @@ impl<'db> KnownClass {
/// Return `true` if this symbol can be resolved to a class definition `class` in typeshed,
/// *and* `class` is a subclass of `other`.
pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
self.try_to_class_literal(db)
self.try_to_class_literal_without_logging(db)
.is_ok_and(|class| class.is_subclass_of(db, None, other))
}

View file

@ -68,6 +68,7 @@ impl<'db> ClassBase<'db> {
if literal.is_known(db, KnownClass::Any) {
Some(Self::Dynamic(DynamicType::Any))
} else if literal.is_known(db, KnownClass::NamedTuple) {
// TODO: Figure out the tuple spec for the named tuple
Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db))
} else {
Some(Self::Class(literal.default_specialization(db)))

View file

@ -14,6 +14,7 @@ use crate::types::string_annotation::{
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
RAW_STRING_TYPE_ANNOTATION,
};
use crate::types::tuple::TupleType;
use crate::types::{SpecialFormType, Type, protocol_class::ProtocolClassLiteral};
use crate::{Db, Module, ModuleName, Program, declare_lint};
use itertools::Itertools;
@ -1606,7 +1607,7 @@ pub(super) fn report_index_out_of_bounds(
kind: &'static str,
node: AnyNodeRef,
tuple_ty: Type,
length: usize,
length: impl std::fmt::Display,
index: i64,
) {
let Some(builder) = context.report_lint(&INDEX_OUT_OF_BOUNDS, node) else {
@ -2120,7 +2121,7 @@ pub(crate) fn report_invalid_or_unsupported_base(
return;
}
let tuple_of_types = KnownClass::Tuple.to_specialized_instance(db, [instance_of_type]);
let tuple_of_types = TupleType::homogeneous(db, instance_of_type);
let explain_mro_entries = |diagnostic: &mut LintDiagnosticGuard| {
diagnostic.info(

View file

@ -10,6 +10,7 @@ use crate::types::class::{ClassLiteral, ClassType, GenericAlias};
use crate::types::function::{FunctionType, OverloadLiteral};
use crate::types::generics::{GenericContext, Specialization};
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
use crate::types::tuple::TupleSpec;
use crate::types::{
CallableType, IntersectionType, KnownClass, MethodWrapperKind, Protocol, StringLiteralType,
SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance, UnionType,
@ -190,16 +191,7 @@ impl Display for DisplayRepresentation<'_> {
escape.bytes_repr(TripleQuotes::No).write(f)
}
Type::Tuple(tuple) => {
f.write_str("tuple[")?;
let elements = tuple.elements(self.db);
if elements.is_empty() {
f.write_str("()")?;
} else {
elements.display(self.db).fmt(f)?;
}
f.write_str("]")
}
Type::Tuple(specialization) => specialization.tuple(self.db).display(self.db).fmt(f),
Type::TypeVar(typevar) => f.write_str(typevar.name(self.db)),
Type::AlwaysTruthy => f.write_str("AlwaysTruthy"),
Type::AlwaysFalsy => f.write_str("AlwaysFalsy"),
@ -224,6 +216,67 @@ impl Display for DisplayRepresentation<'_> {
}
}
impl<'db> TupleSpec<'db> {
pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayTuple<'db> {
DisplayTuple { tuple: self, db }
}
}
pub(crate) struct DisplayTuple<'db> {
tuple: &'db TupleSpec<'db>,
db: &'db dyn Db,
}
impl Display for DisplayTuple<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("tuple[")?;
match self.tuple {
TupleSpec::Fixed(tuple) => {
let elements = tuple.elements_slice();
if elements.is_empty() {
f.write_str("()")?;
} else {
elements.display(self.db).fmt(f)?;
}
}
// Decoder key for which snippets of text need to be included depending on whether
// the tuple contains a prefix and/or suffix:
//
// tuple[ yyy, ... ]
// tuple[xxx, *tuple[yyy, ...] ]
// tuple[xxx, *tuple[yyy, ...], zzz]
// tuple[ *tuple[yyy, ...], zzz]
// PPPPPPPPPPPP P
// SSSSSSS SSSSSS
//
// (Anything that appears above only a P is included only if there's a prefix; anything
// above only an S is included only if there's a suffix; anything about both a P and an
// S is included if there is either a prefix or a suffix. The initial `tuple[` and
// trailing `]` are printed elsewhere. The `yyy, ...` is printed no matter what.)
TupleSpec::Variable(tuple) => {
if !tuple.prefix.is_empty() {
tuple.prefix.display(self.db).fmt(f)?;
f.write_str(", ")?;
}
if !tuple.prefix.is_empty() || !tuple.suffix.is_empty() {
f.write_str("*tuple[")?;
}
tuple.variable.display(self.db).fmt(f)?;
f.write_str(", ...")?;
if !tuple.prefix.is_empty() || !tuple.suffix.is_empty() {
f.write_str("]")?;
}
if !tuple.suffix.is_empty() {
f.write_str(", ")?;
tuple.suffix.display(self.db).fmt(f)?;
}
}
}
f.write_str("]")
}
}
impl<'db> OverloadLiteral<'db> {
// Not currently used, but useful for debugging.
#[expect(dead_code)]
@ -307,15 +360,19 @@ pub(crate) struct DisplayGenericAlias<'db> {
impl Display for DisplayGenericAlias<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"{origin}{specialization}",
origin = self.origin.name(self.db),
specialization = self.specialization.display_short(
self.db,
TupleSpecialization::from_class(self.db, self.origin)
),
)
if self.origin.is_known(self.db, KnownClass::Tuple) {
self.specialization.tuple(self.db).display(self.db).fmt(f)
} else {
write!(
f,
"{origin}{specialization}",
origin = self.origin.name(self.db),
specialization = self.specialization.display_short(
self.db,
TupleSpecialization::from_class(self.db, self.origin)
),
)
}
}
}

View file

@ -8,9 +8,10 @@ use crate::types::class::ClassType;
use crate::types::class_base::ClassBase;
use crate::types::instance::{NominalInstanceType, Protocol, ProtocolInstanceType};
use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::{
KnownInstanceType, Type, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance,
TypeVarVariance, UnionType, declaration_type, todo_type,
TypeVarVariance, UnionType, declaration_type,
};
use crate::{Db, FxOrderSet};
@ -143,20 +144,6 @@ impl<'db> GenericContext<'db> {
self.specialize_partial(db, &vec![None; self.variables(db).len()])
}
#[allow(unused_variables)] // Only unused in release builds
pub(crate) fn todo_specialization(
self,
db: &'db dyn Db,
todo: &'static str,
) -> Specialization<'db> {
let types = self
.variables(db)
.iter()
.map(|typevar| typevar.default_ty(db).unwrap_or(todo_type!(todo)))
.collect();
self.specialize(db, types)
}
pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> Specialization<'db> {
let types = self
.variables(db)
@ -185,7 +172,17 @@ impl<'db> GenericContext<'db> {
types: Box<[Type<'db>]>,
) -> Specialization<'db> {
assert!(self.variables(db).len() == types.len());
Specialization::new(db, self, types)
Specialization::new(db, self, types, None)
}
/// Creates a specialization of this generic context for the `tuple` class.
pub(crate) fn specialize_tuple(
self,
db: &'db dyn Db,
tuple: TupleType<'db>,
) -> Specialization<'db> {
let element_type = UnionType::from_elements(db, tuple.tuple(db).all_elements());
Specialization::new(db, self, Box::from([element_type]), Some(tuple))
}
/// Creates a specialization of this generic context. Panics if the length of `types` does not
@ -230,7 +227,7 @@ impl<'db> GenericContext<'db> {
expanded[idx] = default;
}
Specialization::new(db, self, expanded.into_boxed_slice())
Specialization::new(db, self, expanded.into_boxed_slice(), None)
}
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
@ -273,9 +270,24 @@ pub struct Specialization<'db> {
pub(crate) generic_context: GenericContext<'db>,
#[returns(deref)]
pub(crate) types: Box<[Type<'db>]>,
/// For specializations of `tuple`, we also store more detailed information about the tuple's
/// elements, above what the class's (single) typevar can represent.
tuple_inner: Option<TupleType<'db>>,
}
impl<'db> Specialization<'db> {
/// Returns the tuple spec for a specialization of the `tuple` class.
pub(crate) fn tuple(self, db: &'db dyn Db) -> &'db TupleSpec<'db> {
if let Some(tuple) = self.tuple_inner(db).map(|tuple_type| tuple_type.tuple(db)) {
return tuple;
}
if let [element_type] = self.types(db) {
return TupleType::new(db, TupleSpec::homogeneous(*element_type)).tuple(db);
}
TupleType::new(db, TupleSpec::homogeneous(Type::unknown())).tuple(db)
}
/// Returns the type that a typevar is mapped to, or None if the typevar isn't part of this
/// mapping.
pub(crate) fn get(self, db: &'db dyn Db, typevar: TypeVarInstance<'db>) -> Option<Type<'db>> {
@ -313,7 +325,10 @@ impl<'db> Specialization<'db> {
.iter()
.map(|ty| ty.apply_type_mapping(db, type_mapping))
.collect();
Specialization::new(db, self.generic_context(db), types)
let tuple_inner = self
.tuple_inner(db)
.map(|tuple| tuple.apply_type_mapping(db, type_mapping));
Specialization::new(db, self.generic_context(db), types, tuple_inner)
}
/// Applies an optional specialization to this specialization.
@ -350,12 +365,14 @@ impl<'db> Specialization<'db> {
_ => UnionType::from_elements(db, [self_type, other_type]),
})
.collect();
Specialization::new(db, self.generic_context(db), types)
// TODO: Combine the tuple specs too
Specialization::new(db, self.generic_context(db), types, None)
}
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
let types: Box<[_]> = self.types(db).iter().map(|ty| ty.normalized(db)).collect();
Self::new(db, self.generic_context(db), types)
let tuple_inner = self.tuple_inner(db).map(|tuple| tuple.normalized(db));
Self::new(db, self.generic_context(db), types, tuple_inner)
}
pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
@ -374,7 +391,11 @@ impl<'db> Specialization<'db> {
vartype.materialize(db, variance)
})
.collect();
Specialization::new(db, self.generic_context(db), types)
let tuple_inner = self.tuple_inner(db).map(|tuple| {
// Tuples are immutable, so tuple element types are always in covariant position.
tuple.materialize(db, variance)
});
Specialization::new(db, self.generic_context(db), types, tuple_inner)
}
pub(crate) fn has_relation_to(
@ -388,6 +409,11 @@ impl<'db> Specialization<'db> {
return false;
}
if let (Some(self_tuple), Some(other_tuple)) = (self.tuple_inner(db), other.tuple_inner(db))
{
return self_tuple.has_relation_to(db, other_tuple, relation);
}
for ((typevar, self_type), other_type) in (generic_context.variables(db).into_iter())
.zip(self.types(db))
.zip(other.types(db))
@ -570,7 +596,8 @@ impl<'db> SpecializationBuilder<'db> {
.unwrap_or(variable.default_ty(self.db).unwrap_or(Type::unknown()))
})
.collect();
Specialization::new(self.db, generic_context, types)
// TODO Infer the tuple spec for a tuple type
Specialization::new(self.db, generic_context, types, None)
}
fn add_type_mapping(&mut self, typevar: TypeVarInstance<'db>, ty: Type<'db>) {
@ -641,14 +668,19 @@ impl<'db> SpecializationBuilder<'db> {
}
(Type::Tuple(formal_tuple), Type::Tuple(actual_tuple)) => {
let formal_elements = formal_tuple.elements(self.db);
let actual_elements = actual_tuple.elements(self.db);
if formal_elements.len() == actual_elements.len() {
for (formal_element, actual_element) in
formal_elements.iter().zip(actual_elements)
{
self.infer(*formal_element, *actual_element)?;
let formal_tuple = formal_tuple.tuple(self.db);
let actual_tuple = actual_tuple.tuple(self.db);
match (formal_tuple, actual_tuple) {
(TupleSpec::Fixed(formal_tuple), TupleSpec::Fixed(actual_tuple)) => {
if formal_tuple.len() == actual_tuple.len() {
for (formal_element, actual_element) in formal_tuple.elements().zip(actual_tuple.elements()) {
self.infer(formal_element, actual_element)?;
}
}
}
// TODO: Infer specializations of variable-length tuples
(TupleSpec::Variable(_), _) | (_, TupleSpec::Variable(_)) => {}
}
}

View file

@ -117,11 +117,16 @@ impl AllMembers {
| Type::KnownInstance(_)
| Type::TypeVar(_)
| Type::BoundSuper(_)
| Type::TypeIs(_) => {
if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db) {
| Type::TypeIs(_) => match ty.to_meta_type(db) {
Type::ClassLiteral(class_literal) => {
self.extend_with_class_members(db, class_literal);
}
}
Type::GenericAlias(generic_alias) => {
let class_literal = generic_alias.origin(db);
self.extend_with_class_members(db, class_literal);
}
_ => {}
},
Type::ModuleLiteral(literal) => {
self.extend_with_type(db, KnownClass::ModuleType.to_instance(db));

View file

@ -95,15 +95,16 @@ use crate::types::function::{
use crate::types::generics::GenericContext;
use crate::types::mro::MroErrorKind;
use crate::types::signatures::{CallableSignature, Signature};
use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
BareTypeAliasType, CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams,
DynamicType, GenericAlias, IntersectionBuilder, IntersectionType, KnownClass,
KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate,
PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType, StringLiteralType,
SubclassOfType, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
TypeArrayDisplay, TypeIsType, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance,
TypeVarKind, TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type,
DynamicType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType,
LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter,
ParameterForm, Parameters, SpecialFormType, StringLiteralType, SubclassOfType, Truthiness,
Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeIsType, TypeQualifiers,
TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypeVarVariance, UnionBuilder,
UnionType, binding_type, todo_type,
};
use crate::unpack::{Unpack, UnpackPosition};
use crate::util::subscript::{PyIndex, PySlice};
@ -2443,7 +2444,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
todo_type!("PEP 646")
} else {
let annotated_type = self.file_expression_type(annotation);
KnownClass::Tuple.to_specialized_instance(self.db(), [annotated_type])
TupleType::homogeneous(self.db(), annotated_type)
};
self.add_declaration_with_binding(
@ -2455,7 +2456,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_binding(
parameter.into(),
definition,
KnownClass::Tuple.to_specialized_instance(self.db(), [Type::unknown()]),
TupleType::homogeneous(self.db(), Type::unknown()),
);
}
}
@ -2832,7 +2833,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// it will actually be the type of the generic parameters to `BaseExceptionGroup` or `ExceptionGroup`.
let symbol_ty = if let Type::Tuple(tuple) = node_ty {
let mut builder = UnionBuilder::new(self.db());
for element in tuple.elements(self.db()).iter().copied() {
for element in tuple.tuple(self.db()).all_elements() {
builder = builder.add(
if element.is_assignable_to(self.db(), type_base_exception) {
element.to_instance(self.db()).expect(
@ -2855,7 +2856,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
)
} else if node_ty.is_assignable_to(
self.db(),
KnownClass::Tuple.to_specialized_instance(self.db(), [type_base_exception]),
TupleType::homogeneous(self.db(), type_base_exception),
) {
extract_tuple_specialization(self.db(), node_ty)
.unwrap_or_else(|| KnownClass::BaseException.to_instance(self.db()))
@ -2865,7 +2866,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.db(),
[
type_base_exception,
KnownClass::Tuple.to_specialized_instance(self.db(), [type_base_exception]),
TupleType::homogeneous(self.db(), type_base_exception),
],
),
) {
@ -3698,9 +3699,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ast::Expr::List(ast::ExprList { elts, .. })
| ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => {
let mut assigned_tys = match assigned_ty {
Some(Type::Tuple(tuple)) => {
Either::Left(tuple.elements(self.db()).iter().copied())
}
Some(Type::Tuple(tuple)) => Either::Left(tuple.tuple(self.db()).all_elements()),
Some(_) | None => Either::Right(std::iter::empty()),
};
@ -6940,6 +6939,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
(Type::BooleanLiteral(b1), Type::BooleanLiteral(b2), ast::Operator::BitXor) => {
Some(Type::BooleanLiteral(b1 ^ b2))
}
(Type::BooleanLiteral(b1), Type::BooleanLiteral(_) | Type::IntLiteral(_), op) => self
.infer_binary_expression_type(
node,
@ -6956,19 +6956,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Type::IntLiteral(i64::from(b2)),
op,
),
(Type::Tuple(lhs), Type::Tuple(rhs), ast::Operator::Add) => {
// Note: this only works on heterogeneous tuples.
let lhs_elements = lhs.elements(self.db());
let rhs_elements = rhs.elements(self.db());
Some(TupleType::from_elements(
(Type::Tuple(lhs), Type::Tuple(rhs), ast::Operator::Add) => Some(Type::tuple(
self.db(),
TupleType::new(
self.db(),
lhs_elements
.iter()
.copied()
.chain(rhs_elements.iter().copied()),
))
}
lhs.tuple(self.db()).concat(self.db(), rhs.tuple(self.db())),
),
)),
// We've handled all of the special cases that we support for literals, so we need to
// fall back on looking for dunder methods on one of the operand types.
@ -7425,19 +7420,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// tuples.
//
// Ref: https://github.com/astral-sh/ruff/pull/18251#discussion_r2115909311
if tuple.len(self.db()) > 1 << 12 {
let (minimum_length, _) = tuple.tuple(self.db()).size_hint();
if minimum_length > 1 << 12 {
return None;
}
let mut definitely_true = false;
let mut definitely_false = true;
for element in tuple.elements(self.db()) {
for element in tuple.tuple(self.db()).all_elements() {
if element.is_string_literal() {
if literal == *element {
if literal == element {
definitely_true = true;
definitely_false = false;
}
} else if !literal.is_disjoint_from(self.db(), *element) {
} else if !literal.is_disjoint_from(self.db(), element) {
definitely_false = false;
}
}
@ -7697,12 +7693,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
)
}
(Type::Tuple(lhs), Type::Tuple(rhs)) => {
// Note: This only works on heterogeneous tuple types.
let lhs_elements = lhs.elements(self.db());
let rhs_elements = rhs.elements(self.db());
let lhs_tuple = lhs.tuple(self.db());
let rhs_tuple = rhs.tuple(self.db());
let mut tuple_rich_comparison =
|op| self.infer_tuple_rich_comparison(lhs_elements, op, rhs_elements, range);
|op| self.infer_tuple_rich_comparison(lhs_tuple, op, rhs_tuple, range);
match op {
ast::CmpOp::Eq => tuple_rich_comparison(RichCompareOperator::Eq),
@ -7712,14 +7707,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ast::CmpOp::Gt => tuple_rich_comparison(RichCompareOperator::Gt),
ast::CmpOp::GtE => tuple_rich_comparison(RichCompareOperator::Ge),
ast::CmpOp::In | ast::CmpOp::NotIn => {
let mut eq_count = 0usize;
let mut not_eq_count = 0usize;
let mut any_eq = false;
let mut any_ambiguous = false;
for ty in rhs_elements {
for ty in rhs_tuple.all_elements() {
let eq_result = self.infer_binary_type_comparison(
Type::Tuple(lhs),
ast::CmpOp::Eq,
*ty,
ty,
range,
).expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`");
@ -7729,16 +7724,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// for different union variants. Instead, this is just for us to
// evaluate a possibly truthy value to `false` or `true`.
ty => match ty.bool(self.db()) {
Truthiness::AlwaysTrue => eq_count += 1,
Truthiness::AlwaysFalse => not_eq_count += 1,
Truthiness::Ambiguous => (),
Truthiness::AlwaysTrue => any_eq = true,
Truthiness::AlwaysFalse => (),
Truthiness::Ambiguous => any_ambiguous = true,
},
}
}
if eq_count >= 1 {
if any_eq {
Ok(Type::BooleanLiteral(op.is_in()))
} else if not_eq_count == rhs_elements.len() {
} else if !any_ambiguous {
Ok(Type::BooleanLiteral(op.is_not_in()))
} else {
Ok(KnownClass::Bool.to_instance(self.db()))
@ -7914,13 +7909,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
/// see `<https://github.com/python/cpython/blob/9d6366b60d01305fc5e45100e0cd13e358aa397d/Objects/tupleobject.c#L637>`
fn infer_tuple_rich_comparison(
&mut self,
left: &[Type<'db>],
left: &TupleSpec<'db>,
op: RichCompareOperator,
right: &[Type<'db>],
right: &TupleSpec<'db>,
range: TextRange,
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
let left_iter = left.iter().copied();
let right_iter = right.iter().copied();
// If either tuple is variable length, we can make no assumptions about the relative
// lengths of the tuples, and therefore neither about how they compare lexicographically.
// TODO: Consider comparing the prefixes of the tuples, since that could give a comparison
// result regardless of how long the variable-length tuple is.
let (TupleSpec::Fixed(left), TupleSpec::Fixed(right)) = (left, right) else {
return Ok(Type::unknown());
};
let left_iter = left.elements();
let right_iter = right.elements();
let mut builder = UnionBuilder::new(self.db());
@ -8052,11 +8055,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// special cases, too.
if let Type::ClassLiteral(class) = value_ty {
if class.is_known(self.db(), KnownClass::Tuple) {
self.infer_expression(slice);
// TODO heterogeneous and homogeneous tuples in value expressions
return Type::from(
class.todo_specialization(self.db(), "Generic tuple specializations"),
);
return self
.infer_tuple_type_expression(slice)
.to_meta_type(self.db());
}
if let Some(generic_context) = class.generic_context(self.db()) {
return self.infer_explicit_class_specialization(
@ -8067,6 +8068,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
);
}
}
if let Type::SpecialForm(SpecialFormType::Tuple) = value_ty {
return self
.infer_tuple_type_expression(slice)
.to_meta_type(self.db());
}
let slice_ty = self.infer_expression(slice);
let result_ty = self.infer_subscript_expression_types(value, value_ty, slice_ty);
@ -8113,9 +8119,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.matching_overloads()
.next()
.expect("valid bindings should have matching overload");
let specialization =
generic_context.specialize_partial(self.db(), overload.parameter_types());
Type::from(GenericAlias::new(self.db(), generic_class, specialization))
Type::from(generic_class.apply_specialization(self.db(), |_| {
generic_context.specialize_partial(self.db(), overload.parameter_types())
}))
}
fn infer_subscript_expression_types(
@ -8137,18 +8143,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// Ex) Given `("a", "b", "c", "d")[1]`, return `"b"`
(Type::Tuple(tuple_ty), Type::IntLiteral(int), _) if i32::try_from(int).is_ok() => {
let elements = tuple_ty.elements(self.db());
elements
.iter()
.py_index(i32::try_from(int).expect("checked in branch arm"))
.copied()
let tuple = tuple_ty.tuple(self.db());
tuple
.py_index(
self.db(),
i32::try_from(int).expect("checked in branch arm"),
)
.unwrap_or_else(|_| {
report_index_out_of_bounds(
&self.context,
"tuple",
value_node.into(),
value_ty,
elements.len(),
tuple.display_minimum_length(),
int,
);
Type::unknown()
@ -8156,9 +8163,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
// Ex) Given `("a", 1, Null)[0:2]`, return `("a", 1)`
(Type::Tuple(tuple_ty), _, Some(SliceLiteral { start, stop, step })) => {
let elements = tuple_ty.elements(self.db());
let TupleSpec::Fixed(tuple) = tuple_ty.tuple(self.db()) else {
return todo_type!("slice into variable-length tuple");
};
if let Ok(new_elements) = elements.py_slice(start, stop, step) {
if let Ok(new_elements) = tuple.py_slice(self.db(), start, stop, step) {
TupleType::from_elements(self.db(), new_elements)
} else {
report_slice_step_size_zero(&self.context, value_node.into());
@ -8170,9 +8179,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if i32::try_from(int).is_ok() =>
{
let literal_value = literal_ty.value(self.db());
literal_value
.chars()
.py_index(i32::try_from(int).expect("checked in branch arm"))
(&mut literal_value.chars())
.py_index(
self.db(),
i32::try_from(int).expect("checked in branch arm"),
)
.map(|ch| Type::string_literal(self.db(), &ch.to_string()))
.unwrap_or_else(|_| {
report_index_out_of_bounds(
@ -8192,7 +8203,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let chars: Vec<_> = literal_value.chars().collect();
if let Ok(new_chars) = chars.py_slice(start, stop, step) {
if let Ok(new_chars) = chars.py_slice(self.db(), start, stop, step) {
let literal: String = new_chars.collect();
Type::string_literal(self.db(), &literal)
} else {
@ -8206,8 +8217,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
{
let literal_value = literal_ty.value(self.db());
literal_value
.iter()
.py_index(i32::try_from(int).expect("checked in branch arm"))
.py_index(
self.db(),
i32::try_from(int).expect("checked in branch arm"),
)
.map(|byte| Type::IntLiteral((*byte).into()))
.unwrap_or_else(|_| {
report_index_out_of_bounds(
@ -8225,7 +8238,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
(Type::BytesLiteral(literal_ty), _, Some(SliceLiteral { start, stop, step })) => {
let literal_value = literal_ty.value(self.db());
if let Ok(new_bytes) = literal_value.py_slice(start, stop, step) {
if let Ok(new_bytes) = literal_value.py_slice(self.db(), start, stop, step) {
let new_bytes: Vec<u8> = new_bytes.copied().collect();
Type::bytes_literal(self.db(), &new_bytes)
} else {
@ -8243,14 +8256,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
value_ty,
Type::IntLiteral(i64::from(bool)),
),
(Type::SpecialForm(SpecialFormType::Protocol), Type::Tuple(typevars), _) => self
.legacy_generic_class_context(
(Type::SpecialForm(SpecialFormType::Protocol), Type::Tuple(typevars), _) => {
let TupleSpec::Fixed(typevars) = typevars.tuple(self.db()) else {
// TODO: emit a diagnostic
return Type::unknown();
};
self.legacy_generic_class_context(
value_node,
typevars.elements(self.db()),
typevars.elements_slice(),
LegacyGenericBase::Protocol,
)
.map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(context)))
.unwrap_or_else(Type::unknown),
.unwrap_or_else(Type::unknown)
}
(Type::SpecialForm(SpecialFormType::Protocol), typevar, _) => self
.legacy_generic_class_context(
value_node,
@ -8263,14 +8281,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// TODO: emit a diagnostic
todo_type!("doubly-specialized typing.Protocol")
}
(Type::SpecialForm(SpecialFormType::Generic), Type::Tuple(typevars), _) => self
.legacy_generic_class_context(
(Type::SpecialForm(SpecialFormType::Generic), Type::Tuple(typevars), _) => {
let TupleSpec::Fixed(typevars) = typevars.tuple(self.db()) else {
// TODO: emit a diagnostic
return Type::unknown();
};
self.legacy_generic_class_context(
value_node,
typevars.elements(self.db()),
typevars.elements_slice(),
LegacyGenericBase::Generic,
)
.map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(context)))
.unwrap_or_else(Type::unknown),
.unwrap_or_else(Type::unknown)
}
(Type::SpecialForm(SpecialFormType::Generic), typevar, _) => self
.legacy_generic_class_context(
value_node,
@ -9167,10 +9190,23 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
todo_type!("ellipsis literal in type expression")
}
ast::Expr::Starred(starred) => {
self.infer_starred_expression(starred);
todo_type!("PEP 646")
}
ast::Expr::Starred(starred) => self.infer_starred_type_expression(starred),
}
}
fn infer_starred_type_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> {
let ast::ExprStarred {
range: _,
node_index: _,
value,
ctx: _,
} = starred;
let starred_type = self.infer_type_expression(value);
if let Type::Tuple(_) = starred_type {
starred_type
} else {
todo_type!("PEP 646")
}
}
@ -9225,7 +9261,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
match element {
ast::Expr::Starred(_) => true,
ast::Expr::Starred(_) => !matches!(element_ty, Type::Tuple(_)),
ast::Expr::Subscript(ast::ExprSubscript { value, .. }) => {
let value_ty = if builder.deferred_state.in_string_annotation() {
// Using `.expression_type` does not work in string annotations, because
@ -9246,13 +9282,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
ast::Expr::Tuple(elements) => {
if let [element, ellipsis @ ast::Expr::EllipsisLiteral(_)] = &*elements.elts {
self.infer_expression(ellipsis);
let result = KnownClass::Tuple
.to_specialized_instance(self.db(), [self.infer_type_expression(element)]);
let result =
TupleType::homogeneous(self.db(), self.infer_type_expression(element));
self.store_expression_type(tuple_slice, result);
return result;
}
let mut element_types = Vec::with_capacity(elements.len());
let mut element_types = TupleSpec::with_capacity(elements.len());
// Whether to infer `Todo` for the whole tuple
// (see docstring for `element_could_alter_type_of_whole_tuple`)
@ -9262,13 +9298,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
let element_ty = self.infer_type_expression(element);
return_todo |=
element_could_alter_type_of_whole_tuple(element, element_ty, self);
element_types.push(element_ty);
if let ast::Expr::Starred(_) = element {
if let Type::Tuple(inner_tuple) = element_ty {
element_types =
element_types.concat(self.db(), inner_tuple.tuple(self.db()));
} else {
// TODO: emit a diagnostic
}
} else {
element_types.push(element_ty);
}
}
let ty = if return_todo {
todo_type!("PEP 646")
} else {
TupleType::from_elements(self.db(), element_types)
Type::tuple(self.db(), TupleType::new(self.db(), element_types))
};
// Here, we store the type for the inner `int, str` tuple-expression,

View file

@ -5,6 +5,7 @@ use std::marker::PhantomData;
use super::protocol_class::ProtocolInterface;
use super::{ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance};
use crate::place::{Boundness, Place, PlaceAndQualifiers};
use crate::types::tuple::TupleType;
use crate::types::{ClassLiteral, DynamicType, TypeMapping, TypeRelation, TypeVarInstance};
use crate::{Db, FxOrderSet};
@ -12,12 +13,18 @@ pub(super) use synthesized_protocol::SynthesizedProtocolType;
impl<'db> Type<'db> {
pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self {
if class.is_known(db, KnownClass::Any) {
Self::Dynamic(DynamicType::Any)
} else if class.class_literal(db).0.is_protocol(db) {
Self::ProtocolInstance(ProtocolInstanceType::from_class(class))
} else {
Self::NominalInstance(NominalInstanceType::from_class(class))
match (class, class.known(db)) {
(_, Some(KnownClass::Any)) => Self::Dynamic(DynamicType::Any),
(ClassType::NonGeneric(_), Some(KnownClass::Tuple)) => {
TupleType::homogeneous(db, Type::unknown())
}
(ClassType::Generic(alias), Some(KnownClass::Tuple)) => {
Self::tuple(db, TupleType::new(db, alias.specialization(db).tuple(db)))
}
_ if class.class_literal(db).0.is_protocol(db) => {
Self::ProtocolInstance(ProtocolInstanceType::from_class(class))
}
_ => Self::NominalInstance(NominalInstanceType::from_class(class)),
}
}
@ -98,11 +105,24 @@ impl<'db> NominalInstanceType<'db> {
}
pub(super) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
if self.class.is_final(db) && !self.class.is_subclass_of(db, other.class) {
self.is_disjoint_from_nominal_instance_of_class(db, other.class)
}
// Note that this method only exists so that we can check disjointness between nominal
// instances of `tuple` and some other class. Tuples are currently represented by the
// `Type::Tuple` variant, not `Type::NominalInstance`. We have a TODO to try to remove the
// dedicated `Tuple` variant in favor of `NominalInstance`; if we can do that, then we won't
// need this method, and its logic can be subsumed into `is_disjoint_from`.
pub(super) fn is_disjoint_from_nominal_instance_of_class(
self,
db: &'db dyn Db,
other_class: ClassType,
) -> bool {
if self.class.is_final(db) && !self.class.is_subclass_of(db, other_class) {
return true;
}
if other.class.is_final(db) && !other.class.is_subclass_of(db, self.class) {
if other_class.is_final(db) && !other_class.is_subclass_of(db, self.class) {
return true;
}
@ -116,7 +136,7 @@ impl<'db> NominalInstanceType<'db> {
if self_metaclass == type_type {
return false;
}
let other_metaclass = other.class.metaclass_instance_type(db);
let other_metaclass = other_class.metaclass_instance_type(db);
if other_metaclass == type_type {
return false;
}

View file

@ -175,8 +175,8 @@ impl ClassInfoConstraintFunction {
match classinfo {
Type::Tuple(tuple) => {
let mut builder = UnionBuilder::new(db);
for element in tuple.elements(db) {
builder = builder.add(self.generate_constraint(db, *element)?);
for element in tuple.tuple(db).all_elements() {
builder = builder.add(self.generate_constraint(db, element)?);
}
Some(builder.build())
}
@ -540,7 +540,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
match rhs_ty {
Type::Tuple(rhs_tuple) => Some(UnionType::from_elements(
self.db,
rhs_tuple.elements(self.db),
rhs_tuple.tuple(self.db).all_elements(),
)),
Type::StringLiteral(string_literal) => Some(UnionType::from_elements(

View file

@ -1,8 +1,9 @@
use crate::db::tests::TestDb;
use crate::place::{builtins_symbol, known_module_symbol};
use crate::types::tuple::TupleType;
use crate::types::{
BoundMethodType, CallableType, IntersectionBuilder, KnownClass, Parameter, Parameters,
Signature, SpecialFormType, SubclassOfType, TupleType, Type, UnionType,
Signature, SpecialFormType, SubclassOfType, Type, UnionType,
};
use crate::{Db, KnownModule};
use hashbrown::HashSet;

View file

@ -36,7 +36,7 @@ impl SlotsKind {
match slots_ty {
// __slots__ = ("a", "b")
Type::Tuple(tuple) => {
if tuple.elements(db).is_empty() {
if tuple.tuple(db).is_empty() {
Self::Empty
} else {
Self::NotEmpty

View file

@ -0,0 +1,868 @@
//! Types describing fixed- and variable-length tuples.
//!
//! At runtime, a Python tuple is a fixed-length immutable list of values. There is no restriction
//! on the types of the elements of a tuple value. In the type system, we want to model both
//! "heterogeneous" tuples that have elements of a fixed sequence of specific types, and
//! "homogeneous" tuples that have an unknown number of elements of the same single type. And in
//! fact, we want to model tuples that are a combination of the two ("mixed" tuples), with a
//! heterogeneous prefix and/or suffix, and a homogeneous portion of unknown length in between
//! those.
//!
//! The description of which elements can appear in a `tuple` is called a [`TupleSpec`]. Other
//! things besides `tuple` instances can be described by a tuple spec — for instance, the targets
//! of an unpacking assignment. A `tuple` specialization that includes `Never` as one of its
//! fixed-length elements cannot be instantiated. We reduce the entire `tuple` type down to
//! `Never`. The same is not true of tuple specs in general. (That means that it is [`TupleType`]
//! that adds that "collapse `Never`" behavior, whereas [`TupleSpec`] allows you to add any element
//! types, including `Never`.)
use itertools::Either;
use crate::types::class::{ClassType, KnownClass};
use crate::types::{Type, TypeMapping, TypeRelation, TypeVarInstance, TypeVarVariance, UnionType};
use crate::util::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError};
use crate::{Db, FxOrderSet};
/// # Ordering
/// Ordering is based on the tuple's salsa-assigned id and not on its elements.
/// The id may change between runs, or when the tuple was garbage collected and recreated.
#[salsa::interned(debug)]
#[derive(PartialOrd, Ord)]
pub struct TupleType<'db> {
#[returns(ref)]
pub(crate) tuple: TupleSpec<'db>,
}
impl<'db> Type<'db> {
pub(crate) fn tuple(db: &'db dyn Db, tuple: TupleType<'db>) -> Self {
// If a fixed-length (i.e., mandatory) element of the tuple is `Never`, then it's not
// possible to instantiate the tuple as a whole. (This is not true of the variable-length
// portion of the tuple, since it can contain no elements.)
if tuple.tuple(db).fixed_elements().any(|ty| ty.is_never()) {
return Type::Never;
}
Self::Tuple(tuple)
}
}
impl<'db> TupleType<'db> {
pub(crate) fn empty(db: &'db dyn Db) -> Type<'db> {
Type::tuple(
db,
TupleType::new(db, TupleSpec::from(FixedLengthTupleSpec::empty())),
)
}
pub(crate) fn from_elements(
db: &'db dyn Db,
types: impl IntoIterator<Item = impl Into<Type<'db>>>,
) -> Type<'db> {
Type::tuple(
db,
TupleType::new(
db,
TupleSpec::from(FixedLengthTupleSpec::from_elements(types)),
),
)
}
#[cfg(test)]
pub(crate) fn mixed(
db: &'db dyn Db,
prefix: impl IntoIterator<Item = impl Into<Type<'db>>>,
variable: Type<'db>,
suffix: impl IntoIterator<Item = impl Into<Type<'db>>>,
) -> Type<'db> {
Type::tuple(
db,
TupleType::new(
db,
TupleSpec::from(VariableLengthTupleSpec::mixed(prefix, variable, suffix)),
),
)
}
pub(crate) fn homogeneous(db: &'db dyn Db, element: Type<'db>) -> Type<'db> {
Type::tuple(db, TupleType::new(db, TupleSpec::homogeneous(element)))
}
pub(crate) fn to_class_type(self, db: &'db dyn Db) -> Option<ClassType<'db>> {
KnownClass::Tuple
.try_to_class_literal(db)
.and_then(|class_literal| match class_literal.generic_context(db) {
None => Some(ClassType::NonGeneric(class_literal)),
Some(generic_context) if generic_context.variables(db).len() != 1 => None,
Some(generic_context) => Some(
class_literal
.apply_specialization(db, |_| generic_context.specialize_tuple(db, self)),
),
})
}
/// Return a normalized version of `self`.
///
/// See [`Type::normalized`] for more details.
#[must_use]
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
TupleType::new(db, self.tuple(db).normalized(db))
}
pub(crate) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
TupleType::new(db, self.tuple(db).materialize(db, variance))
}
pub(crate) fn apply_type_mapping<'a>(
self,
db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>,
) -> Self {
TupleType::new(db, self.tuple(db).apply_type_mapping(db, type_mapping))
}
pub(crate) fn find_legacy_typevars(
self,
db: &'db dyn Db,
typevars: &mut FxOrderSet<TypeVarInstance<'db>>,
) {
self.tuple(db).find_legacy_typevars(db, typevars);
}
pub(crate) fn has_relation_to(
self,
db: &'db dyn Db,
other: Self,
relation: TypeRelation,
) -> bool {
self.tuple(db)
.has_relation_to(db, other.tuple(db), relation)
}
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.tuple(db).is_equivalent_to(db, other.tuple(db))
}
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.tuple(db).is_gradual_equivalent_to(db, other.tuple(db))
}
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
self.tuple(db).is_disjoint_from(db, other.tuple(db))
}
pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool {
self.tuple(db).is_fully_static(db)
}
pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool {
self.tuple(db).is_single_valued(db)
}
}
/// A fixed-length tuple spec.
///
/// Tuple specs are used for more than just `tuple` instances, so they allow `Never` to appear as a
/// fixed-length element type. [`TupleType`] adds that additional invariant (since a tuple that
/// must contain an element that can't be instantiated, can't be instantiated itself).
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, salsa::Update)]
pub struct FixedLengthTupleSpec<'db>(Vec<Type<'db>>);
impl<'db> FixedLengthTupleSpec<'db> {
pub(crate) fn empty() -> Self {
Self::default()
}
pub(crate) fn with_capacity(capacity: usize) -> Self {
Self(Vec::with_capacity(capacity))
}
pub(crate) fn from_elements(elements: impl IntoIterator<Item = impl Into<Type<'db>>>) -> Self {
Self(elements.into_iter().map(Into::into).collect())
}
pub(crate) fn elements_slice(&self) -> &[Type<'db>] {
&self.0
}
pub(crate) fn elements(&self) -> impl Iterator<Item = Type<'db>> + '_ {
self.0.iter().copied()
}
/// Returns the length of this tuple.
pub(crate) fn len(&self) -> usize {
self.0.len()
}
fn is_empty(&self) -> bool {
self.0.is_empty()
}
fn concat(&self, other: &TupleSpec<'db>) -> TupleSpec<'db> {
match other {
TupleSpec::Fixed(other) => {
let mut elements = Vec::with_capacity(self.0.len() + other.0.len());
elements.extend_from_slice(&self.0);
elements.extend_from_slice(&other.0);
TupleSpec::Fixed(FixedLengthTupleSpec(elements))
}
TupleSpec::Variable(other) => {
let mut prefix = Vec::with_capacity(self.0.len() + other.prefix.len());
prefix.extend_from_slice(&self.0);
prefix.extend_from_slice(&other.prefix);
TupleSpec::Variable(VariableLengthTupleSpec {
prefix,
variable: other.variable,
suffix: other.suffix.clone(),
})
}
}
}
pub(crate) fn push(&mut self, element: Type<'db>) {
self.0.push(element);
}
pub(crate) fn extend_from_slice(&mut self, elements: &[Type<'db>]) {
self.0.extend_from_slice(elements);
}
#[must_use]
fn normalized(&self, db: &'db dyn Db) -> Self {
Self(self.0.iter().map(|ty| ty.normalized(db)).collect())
}
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
Self(
self.0
.iter()
.map(|ty| ty.materialize(db, variance))
.collect(),
)
}
fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
Self(
self.0
.iter()
.map(|ty| ty.apply_type_mapping(db, type_mapping))
.collect(),
)
}
fn find_legacy_typevars(
&self,
db: &'db dyn Db,
typevars: &mut FxOrderSet<TypeVarInstance<'db>>,
) {
for ty in &self.0 {
ty.find_legacy_typevars(db, typevars);
}
}
fn has_relation_to(
&self,
db: &'db dyn Db,
other: &TupleSpec<'db>,
relation: TypeRelation,
) -> bool {
match other {
TupleSpec::Fixed(other) => {
self.0.len() == other.0.len()
&& (self.0.iter())
.zip(&other.0)
.all(|(self_ty, other_ty)| self_ty.has_relation_to(db, *other_ty, relation))
}
TupleSpec::Variable(other) => {
// This tuple must have enough elements to match up with the other tuple's prefix
// and suffix, and each of those elements must pairwise satisfy the relation.
let mut self_iter = self.0.iter();
for other_ty in &other.prefix {
let Some(self_ty) = self_iter.next() else {
return false;
};
if !self_ty.has_relation_to(db, *other_ty, relation) {
return false;
}
}
for other_ty in other.suffix.iter().rev() {
let Some(self_ty) = self_iter.next_back() else {
return false;
};
if !self_ty.has_relation_to(db, *other_ty, relation) {
return false;
}
}
// In addition, any remaining elements in this tuple must satisfy the
// variable-length portion of the other tuple.
self_iter.all(|self_ty| self_ty.has_relation_to(db, other.variable, relation))
}
}
}
fn is_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool {
self.0.len() == other.0.len()
&& (self.0.iter())
.zip(&other.0)
.all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty))
}
fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool {
self.0.len() == other.0.len()
&& (self.0.iter())
.zip(&other.0)
.all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty))
}
fn is_disjoint_from(&self, db: &'db dyn Db, other: &Self) -> bool {
self.0.len() != other.0.len()
|| (self.0.iter())
.zip(&other.0)
.any(|(self_ty, other_ty)| self_ty.is_disjoint_from(db, *other_ty))
}
fn is_fully_static(&self, db: &'db dyn Db) -> bool {
self.0.iter().all(|ty| ty.is_fully_static(db))
}
fn is_single_valued(&self, db: &'db dyn Db) -> bool {
self.0.iter().all(|ty| ty.is_single_valued(db))
}
}
impl<'db> PyIndex<'db> for &FixedLengthTupleSpec<'db> {
type Item = Type<'db>;
fn py_index(self, db: &'db dyn Db, index: i32) -> Result<Self::Item, OutOfBoundsError> {
self.0.as_slice().py_index(db, index).copied()
}
}
impl<'db> PySlice<'db> for FixedLengthTupleSpec<'db> {
type Item = Type<'db>;
fn py_slice(
&'db self,
db: &'db dyn Db,
start: Option<i32>,
stop: Option<i32>,
step: Option<i32>,
) -> Result<impl Iterator<Item = &'db Self::Item>, StepSizeZeroError> {
self.0.py_slice(db, start, stop, step)
}
}
/// A variable-length tuple spec.
///
/// The tuple spec can contain a fixed-length heterogeneous prefix and/or suffix. All of the
/// elements of the variable-length portion must be of the same type.
///
/// Tuple specs are used for more than just `tuple` instances, so they allow `Never` to appear as a
/// fixed-length element type. [`TupleType`] adds that additional invariant (since a tuple that
/// must contain an element that can't be instantiated, can't be instantiated itself).
#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
pub struct VariableLengthTupleSpec<'db> {
pub(crate) prefix: Vec<Type<'db>>,
pub(crate) variable: Type<'db>,
pub(crate) suffix: Vec<Type<'db>>,
}
impl<'db> VariableLengthTupleSpec<'db> {
/// Creates a new tuple spec containing zero or more elements of a given type, with no prefix
/// or suffix.
fn homogeneous(ty: Type<'db>) -> Self {
Self {
prefix: vec![],
variable: ty,
suffix: vec![],
}
}
#[cfg(test)]
fn mixed(
prefix: impl IntoIterator<Item = impl Into<Type<'db>>>,
variable: Type<'db>,
suffix: impl IntoIterator<Item = impl Into<Type<'db>>>,
) -> Self {
Self {
prefix: prefix.into_iter().map(Into::into).collect(),
variable,
suffix: suffix.into_iter().map(Into::into).collect(),
}
}
fn fixed_elements(&self) -> impl Iterator<Item = Type<'db>> + '_ {
(self.prefix.iter().copied()).chain(self.suffix.iter().copied())
}
fn all_elements(&self) -> impl Iterator<Item = Type<'db>> + '_ {
(self.prefix.iter().copied())
.chain(std::iter::once(self.variable))
.chain(self.suffix.iter().copied())
}
/// Returns the minimum length of this tuple.
pub(crate) fn minimum_length(&self) -> usize {
self.prefix.len() + self.suffix.len()
}
fn concat(&self, db: &'db dyn Db, other: &TupleSpec<'db>) -> TupleSpec<'db> {
match other {
TupleSpec::Fixed(other) => {
let mut suffix = Vec::with_capacity(self.suffix.len() + other.0.len());
suffix.extend_from_slice(&self.suffix);
suffix.extend_from_slice(&other.0);
TupleSpec::Variable(VariableLengthTupleSpec {
prefix: self.prefix.clone(),
variable: self.variable,
suffix,
})
}
TupleSpec::Variable(other) => {
let variable = UnionType::from_elements(
db,
(self.suffix.iter().copied())
.chain([self.variable, other.variable])
.chain(other.prefix.iter().copied()),
);
TupleSpec::Variable(VariableLengthTupleSpec {
prefix: self.prefix.clone(),
variable,
suffix: other.suffix.clone(),
})
}
}
}
fn push(&mut self, element: Type<'db>) {
self.suffix.push(element);
}
#[must_use]
fn normalized(&self, db: &'db dyn Db) -> Self {
Self {
prefix: self.prefix.iter().map(|ty| ty.normalized(db)).collect(),
variable: self.variable.normalized(db),
suffix: self.suffix.iter().map(|ty| ty.normalized(db)).collect(),
}
}
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
Self {
prefix: self
.prefix
.iter()
.map(|ty| ty.materialize(db, variance))
.collect(),
variable: self.variable.materialize(db, variance),
suffix: self
.suffix
.iter()
.map(|ty| ty.materialize(db, variance))
.collect(),
}
}
fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
Self {
prefix: self
.prefix
.iter()
.map(|ty| ty.apply_type_mapping(db, type_mapping))
.collect(),
variable: self.variable.apply_type_mapping(db, type_mapping),
suffix: self
.suffix
.iter()
.map(|ty| ty.apply_type_mapping(db, type_mapping))
.collect(),
}
}
fn find_legacy_typevars(
&self,
db: &'db dyn Db,
typevars: &mut FxOrderSet<TypeVarInstance<'db>>,
) {
for ty in &self.prefix {
ty.find_legacy_typevars(db, typevars);
}
self.variable.find_legacy_typevars(db, typevars);
for ty in &self.suffix {
ty.find_legacy_typevars(db, typevars);
}
}
fn has_relation_to(
&self,
db: &'db dyn Db,
other: &TupleSpec<'db>,
relation: TypeRelation,
) -> bool {
match other {
TupleSpec::Fixed(other) => {
// The `...` length specifier of a variable-length tuple type is interpreted
// differently depending on the type of the variable-length elements.
//
// It typically represents the _union_ of all possible lengths. That means that a
// variable-length tuple type is not a subtype of _any_ fixed-length tuple type.
//
// However, as a special case, if the variable-length portion of the tuple is `Any`
// (or any other dynamic type), then the `...` is the _gradual choice_ of all
// possible lengths. This means that `tuple[Any, ...]` can match any tuple of any
// length.
if relation == TypeRelation::Subtyping || !matches!(self.variable, Type::Dynamic(_))
{
return false;
}
// In addition, the other tuple must have enough elements to match up with this
// tuple's prefix and suffix, and each of those elements must pairwise satisfy the
// relation.
let mut other_iter = other.0.iter();
for self_ty in &self.prefix {
let Some(other_ty) = other_iter.next() else {
return false;
};
if !self_ty.has_relation_to(db, *other_ty, relation) {
return false;
}
}
for self_ty in self.suffix.iter().rev() {
let Some(other_ty) = other_iter.next_back() else {
return false;
};
if !self_ty.has_relation_to(db, *other_ty, relation) {
return false;
}
}
true
}
TupleSpec::Variable(other) => {
// The overlapping parts of the prefixes and suffixes must satisfy the relation.
let mut self_prefix = self.prefix.iter();
let mut other_prefix = other.prefix.iter();
let prefixes_match = (&mut self_prefix)
.zip(&mut other_prefix)
.all(|(self_ty, other_ty)| self_ty.has_relation_to(db, *other_ty, relation));
if !prefixes_match {
return false;
}
let mut self_suffix = self.suffix.iter().rev();
let mut other_suffix = other.suffix.iter().rev();
let suffixes_match = (&mut self_suffix)
.zip(&mut other_suffix)
.all(|(self_ty, other_ty)| self_ty.has_relation_to(db, *other_ty, relation));
if !suffixes_match {
return false;
}
// Any remaining parts of either prefix or suffix must satisfy the relation with
// the other tuple's variable-length portion.
let prefix_matches_variable = self_prefix
.all(|self_ty| self_ty.has_relation_to(db, other.variable, relation));
if !prefix_matches_variable {
return false;
}
let prefix_matches_variable = other_prefix
.all(|other_ty| self.variable.has_relation_to(db, *other_ty, relation));
if !prefix_matches_variable {
return false;
}
let suffix_matches_variable = self_suffix
.all(|self_ty| self_ty.has_relation_to(db, other.variable, relation));
if !suffix_matches_variable {
return false;
}
let suffix_matches_variable = other_suffix
.all(|other_ty| self.variable.has_relation_to(db, *other_ty, relation));
if !suffix_matches_variable {
return false;
}
// And lastly, the variable-length portions must satisfy the relation.
self.variable.has_relation_to(db, other.variable, relation)
}
}
}
fn is_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool {
self.prefix.len() == other.prefix.len()
&& self.suffix.len() == other.suffix.len()
&& self.variable.is_equivalent_to(db, other.variable)
&& (self.prefix.iter())
.zip(&other.prefix)
.all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty))
&& (self.suffix.iter())
.zip(&other.suffix)
.all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty))
}
fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool {
self.prefix.len() == other.prefix.len()
&& self.suffix.len() == other.suffix.len()
&& self.variable.is_gradual_equivalent_to(db, other.variable)
&& (self.prefix.iter())
.zip(&other.prefix)
.all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty))
&& (self.suffix.iter())
.zip(&other.suffix)
.all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty))
}
fn is_fully_static(&self, db: &'db dyn Db) -> bool {
self.variable.is_fully_static(db)
&& self.prefix.iter().all(|ty| ty.is_fully_static(db))
&& self.suffix.iter().all(|ty| ty.is_fully_static(db))
}
}
impl<'db> PyIndex<'db> for &VariableLengthTupleSpec<'db> {
type Item = Type<'db>;
fn py_index(self, db: &'db dyn Db, index: i32) -> Result<Self::Item, OutOfBoundsError> {
match Nth::from_index(index) {
Nth::FromStart(index) => {
if let Some(element) = self.prefix.get(index) {
// index is small enough that it lands in the prefix of the tuple.
return Ok(*element);
}
// index is large enough that it lands past the prefix. The tuple can always be
// large enough that it lands in the variable-length portion. It might also be
// small enough to land in the suffix.
let index_past_prefix = index - self.prefix.len() + 1;
Ok(UnionType::from_elements(
db,
std::iter::once(self.variable)
.chain(self.suffix.iter().copied().take(index_past_prefix)),
))
}
Nth::FromEnd(index_from_end) => {
if index_from_end < self.suffix.len() {
// index is small enough that it lands in the suffix of the tuple.
return Ok(self.suffix[self.suffix.len() - index_from_end - 1]);
}
// index is large enough that it lands past the suffix. The tuple can always be
// large enough that it lands in the variable-length portion. It might also be
// small enough to land in the prefix.
let index_past_suffix = index_from_end - self.suffix.len() + 1;
Ok(UnionType::from_elements(
db,
(self.prefix.iter().rev().copied())
.take(index_past_suffix)
.rev()
.chain(std::iter::once(self.variable)),
))
}
}
}
}
/// A tuple spec that might be fixed- or variable-length.
///
/// Tuple specs are used for more than just `tuple` instances, so they allow `Never` to appear as a
/// fixed-length element type. [`TupleType`] adds that additional invariant (since a tuple that
/// must contain an element that can't be instantiated, can't be instantiated itself).
#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
pub enum TupleSpec<'db> {
Fixed(FixedLengthTupleSpec<'db>),
Variable(VariableLengthTupleSpec<'db>),
}
impl<'db> TupleSpec<'db> {
pub(crate) fn with_capacity(capacity: usize) -> Self {
TupleSpec::Fixed(FixedLengthTupleSpec::with_capacity(capacity))
}
pub(crate) fn homogeneous(element: Type<'db>) -> Self {
TupleSpec::from(VariableLengthTupleSpec::homogeneous(element))
}
/// Returns an iterator of all of the fixed-length element types of this tuple.
pub(crate) fn fixed_elements(&self) -> impl Iterator<Item = Type<'db>> + '_ {
match self {
TupleSpec::Fixed(tuple) => Either::Left(tuple.elements()),
TupleSpec::Variable(tuple) => Either::Right(tuple.fixed_elements()),
}
}
/// Returns an iterator of all of the element types of this tuple. Does not deduplicate the
/// elements, and does not distinguish between fixed- and variable-length elements.
pub(crate) fn all_elements(&self) -> impl Iterator<Item = Type<'db>> + '_ {
match self {
TupleSpec::Fixed(tuple) => Either::Left(tuple.elements()),
TupleSpec::Variable(tuple) => Either::Right(tuple.all_elements()),
}
}
pub(crate) fn display_minimum_length(&self) -> String {
match self {
TupleSpec::Fixed(tuple) => tuple.len().to_string(),
TupleSpec::Variable(tuple) => format!("at least {}", tuple.minimum_length()),
}
}
/// Returns the minimum and maximum length of this tuple. (The maximum length will be `None`
/// for a tuple with a variable-length portion.)
pub(crate) fn size_hint(&self) -> (usize, Option<usize>) {
match self {
TupleSpec::Fixed(tuple) => {
let len = tuple.len();
(len, Some(len))
}
TupleSpec::Variable(tuple) => (tuple.minimum_length(), None),
}
}
pub(crate) fn is_empty(&self) -> bool {
match self {
TupleSpec::Fixed(tuple) => tuple.is_empty(),
TupleSpec::Variable(_) => false,
}
}
/// Concatenates another tuple to the end of this tuple, returning a new tuple.
pub(crate) fn concat(&self, db: &'db dyn Db, other: &Self) -> Self {
match self {
TupleSpec::Fixed(tuple) => tuple.concat(other),
TupleSpec::Variable(tuple) => tuple.concat(db, other),
}
}
pub(crate) fn push(&mut self, element: Type<'db>) {
match self {
TupleSpec::Fixed(tuple) => tuple.push(element),
TupleSpec::Variable(tuple) => tuple.push(element),
}
}
fn normalized(&self, db: &'db dyn Db) -> Self {
match self {
TupleSpec::Fixed(tuple) => TupleSpec::Fixed(tuple.normalized(db)),
TupleSpec::Variable(tuple) => TupleSpec::Variable(tuple.normalized(db)),
}
}
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
match self {
TupleSpec::Fixed(tuple) => TupleSpec::Fixed(tuple.materialize(db, variance)),
TupleSpec::Variable(tuple) => TupleSpec::Variable(tuple.materialize(db, variance)),
}
}
fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
match self {
TupleSpec::Fixed(tuple) => TupleSpec::Fixed(tuple.apply_type_mapping(db, type_mapping)),
TupleSpec::Variable(tuple) => {
TupleSpec::Variable(tuple.apply_type_mapping(db, type_mapping))
}
}
}
fn find_legacy_typevars(
&self,
db: &'db dyn Db,
typevars: &mut FxOrderSet<TypeVarInstance<'db>>,
) {
match self {
TupleSpec::Fixed(tuple) => tuple.find_legacy_typevars(db, typevars),
TupleSpec::Variable(tuple) => tuple.find_legacy_typevars(db, typevars),
}
}
fn has_relation_to(&self, db: &'db dyn Db, other: &Self, relation: TypeRelation) -> bool {
match self {
TupleSpec::Fixed(self_tuple) => self_tuple.has_relation_to(db, other, relation),
TupleSpec::Variable(self_tuple) => self_tuple.has_relation_to(db, other, relation),
}
}
fn is_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool {
match (self, other) {
(TupleSpec::Fixed(self_tuple), TupleSpec::Fixed(other_tuple)) => {
self_tuple.is_equivalent_to(db, other_tuple)
}
(TupleSpec::Variable(self_tuple), TupleSpec::Variable(other_tuple)) => {
self_tuple.is_equivalent_to(db, other_tuple)
}
(TupleSpec::Fixed(_), TupleSpec::Variable(_))
| (TupleSpec::Variable(_), TupleSpec::Fixed(_)) => false,
}
}
fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &TupleSpec<'db>) -> bool {
match (self, other) {
(TupleSpec::Fixed(self_tuple), TupleSpec::Fixed(other_tuple)) => {
self_tuple.is_gradual_equivalent_to(db, other_tuple)
}
(TupleSpec::Variable(self_tuple), TupleSpec::Variable(other_tuple)) => {
self_tuple.is_gradual_equivalent_to(db, other_tuple)
}
(TupleSpec::Fixed(_), TupleSpec::Variable(_))
| (TupleSpec::Variable(_), TupleSpec::Fixed(_)) => false,
}
}
fn is_disjoint_from(&self, db: &'db dyn Db, other: &Self) -> bool {
match (self, other) {
(TupleSpec::Fixed(self_tuple), TupleSpec::Fixed(other_tuple)) => {
self_tuple.is_disjoint_from(db, other_tuple)
}
// Two pure homogeneous tuples `tuple[A, ...]` and `tuple[B, ...]` can never be
// disjoint even if A and B are disjoint, because `tuple[()]` would be assignable to
// both.
// TODO: Consider checking for disjointness between the tuples' prefixes and suffixes.
(TupleSpec::Variable(_), TupleSpec::Variable(_)) => false,
// TODO: Consider checking for disjointness between the fixed-length tuple and the
// variable-length tuple's prefix/suffix.
(TupleSpec::Fixed(_), TupleSpec::Variable(_))
| (TupleSpec::Variable(_), TupleSpec::Fixed(_)) => false,
}
}
fn is_fully_static(&self, db: &'db dyn Db) -> bool {
match self {
TupleSpec::Fixed(tuple) => tuple.is_fully_static(db),
TupleSpec::Variable(tuple) => tuple.is_fully_static(db),
}
}
fn is_single_valued(&self, db: &'db dyn Db) -> bool {
match self {
TupleSpec::Fixed(tuple) => tuple.is_single_valued(db),
TupleSpec::Variable(_) => false,
}
}
}
impl<'db> From<FixedLengthTupleSpec<'db>> for TupleSpec<'db> {
fn from(tuple: FixedLengthTupleSpec<'db>) -> Self {
TupleSpec::Fixed(tuple)
}
}
impl<'db> From<VariableLengthTupleSpec<'db>> for TupleSpec<'db> {
fn from(tuple: VariableLengthTupleSpec<'db>) -> Self {
TupleSpec::Variable(tuple)
}
}
impl<'db> PyIndex<'db> for &TupleSpec<'db> {
type Item = Type<'db>;
fn py_index(self, db: &'db dyn Db, index: i32) -> Result<Self::Item, OutOfBoundsError> {
match self {
TupleSpec::Fixed(tuple) => tuple.py_index(db, index),
TupleSpec::Variable(tuple) => tuple.py_index(db, index),
}
}
}

View file

@ -9,12 +9,13 @@ use ruff_python_ast::{self as ast, AnyNodeRef};
use crate::Db;
use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId};
use crate::semantic_index::place::ScopeId;
use crate::types::{Type, TypeCheckDiagnostics, infer_expression_types};
use crate::types::tuple::{FixedLengthTupleSpec, TupleSpec, TupleType};
use crate::types::{Type, TypeCheckDiagnostics, infer_expression_types, todo_type};
use crate::unpack::{UnpackKind, UnpackValue};
use super::context::InferContext;
use super::diagnostic::INVALID_ASSIGNMENT;
use super::{KnownClass, TupleType, UnionType};
use super::{KnownClass, UnionType};
/// Unpacks the value expression type to their respective targets.
pub(crate) struct Unpacker<'db, 'ast> {
@ -152,53 +153,55 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
_ => ty,
};
if let Some(tuple_ty) = ty.into_tuple() {
let tuple_ty_elements =
self.tuple_ty_elements(target, elts, tuple_ty, value_expr);
if let Type::Tuple(tuple_ty) = ty {
let tuple = self.tuple_ty_elements(target, elts, tuple_ty, value_expr);
let length_mismatch =
match elts.len().cmp(&tuple_ty_elements.len()) {
Ordering::Less => {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
{
let mut diag =
builder.into_diagnostic("Too many values to unpack");
diag.set_primary_message(format_args!(
"Expected {}",
elts.len(),
));
diag.annotate(self.context.secondary(value_expr).message(
format_args!("Got {}", tuple_ty_elements.len()),
));
}
true
let length_mismatch = match elts.len().cmp(&tuple.len()) {
Ordering::Less => {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
{
let mut diag =
builder.into_diagnostic("Too many values to unpack");
diag.set_primary_message(format_args!(
"Expected {}",
elts.len(),
));
diag.annotate(
self.context
.secondary(value_expr)
.message(format_args!("Got {}", tuple.len())),
);
}
Ordering::Greater => {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
{
let mut diag =
builder.into_diagnostic("Not enough values to unpack");
diag.set_primary_message(format_args!(
"Expected {}",
elts.len(),
));
diag.annotate(self.context.secondary(value_expr).message(
format_args!("Got {}", tuple_ty_elements.len()),
));
}
true
true
}
Ordering::Greater => {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
{
let mut diag =
builder.into_diagnostic("Not enough values to unpack");
diag.set_primary_message(format_args!(
"Expected {}",
elts.len(),
));
diag.annotate(
self.context
.secondary(value_expr)
.message(format_args!("Got {}", tuple.len())),
);
}
Ordering::Equal => false,
};
true
}
Ordering::Equal => false,
};
for (index, ty) in tuple_ty_elements.iter().enumerate() {
for (index, ty) in tuple.elements().enumerate() {
if let Some(element_types) = target_types.get_mut(index) {
if length_mismatch {
element_types.push(Type::unknown());
} else {
element_types.push(*ty);
element_types.push(ty);
}
}
}
@ -248,24 +251,36 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
targets: &[ast::Expr],
tuple_ty: TupleType<'db>,
value_expr: AnyNodeRef<'_>,
) -> Cow<'_, [Type<'db>]> {
) -> Cow<'_, FixedLengthTupleSpec<'db>> {
let TupleSpec::Fixed(tuple) = tuple_ty.tuple(self.db()) else {
let todo = todo_type!("Unpack variable-length tuple");
return Cow::Owned(FixedLengthTupleSpec::from_elements(targets.iter().map(
|target| {
if target.is_starred_expr() {
KnownClass::List.to_specialized_instance(self.db(), [todo])
} else {
todo
}
},
)));
};
// If there is a starred expression, it will consume all of the types at that location.
let Some(starred_index) = targets.iter().position(ast::Expr::is_starred_expr) else {
// Otherwise, the types will be unpacked 1-1 to the targets.
return Cow::Borrowed(tuple_ty.elements(self.db()));
return Cow::Borrowed(tuple);
};
if tuple_ty.len(self.db()) >= targets.len() - 1 {
if tuple.len() >= targets.len() - 1 {
// This branch is only taken when there are enough elements in the tuple type to
// combine for the starred expression. So, the arithmetic and indexing operations are
// safe to perform.
let mut element_types = Vec::with_capacity(targets.len());
let mut element_types = FixedLengthTupleSpec::with_capacity(targets.len());
let tuple_elements = tuple.elements_slice();
// Insert all the elements before the starred expression.
element_types.extend_from_slice(
// SAFETY: Safe because of the length check above.
&tuple_ty.elements(self.db())[..starred_index],
);
// SAFETY: Safe because of the length check above.
element_types.extend_from_slice(&tuple_elements[..starred_index]);
// The number of target expressions that are remaining after the starred expression.
// For example, in `(a, *b, c, d) = ...`, the index of starred element `b` is 1 and the
@ -276,11 +291,10 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
// expression, in an exclusive manner. For example, in `(a, *b, c) = (1, 2, 3, 4)`, the
// starred expression `b` will consume the elements `Literal[2]` and `Literal[3]` and
// the index value would be 3.
let starred_end_index = tuple_ty.len(self.db()) - remaining;
let starred_end_index = tuple.len() - remaining;
// SAFETY: Safe because of the length check above.
let starred_element_types =
&tuple_ty.elements(self.db())[starred_index..starred_end_index];
let starred_element_types = &tuple_elements[starred_index..starred_end_index];
element_types.push(KnownClass::List.to_specialized_instance(
self.db(),
@ -292,10 +306,8 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
));
// Insert the types remaining that aren't consumed by the starred expression.
element_types.extend_from_slice(
// SAFETY: Safe because of the length check above.
&tuple_ty.elements(self.db())[starred_end_index..],
);
// SAFETY: Safe because of the length check above.
element_types.extend_from_slice(&tuple_elements[starred_end_index..]);
Cow::Owned(element_types)
} else {
@ -305,22 +317,19 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
diag.annotate(
self.context
.secondary(value_expr)
.message(format_args!("Got {}", tuple_ty.len(self.db()))),
.message(format_args!("Got {}", tuple.len())),
);
}
Cow::Owned(
targets
.iter()
.map(|target| {
if target.is_starred_expr() {
KnownClass::List.to_specialized_instance(self.db(), [Type::unknown()])
} else {
Type::unknown()
}
})
.collect(),
)
Cow::Owned(FixedLengthTupleSpec::from_elements(targets.iter().map(
|target| {
if target.is_starred_expr() {
KnownClass::List.to_specialized_instance(self.db(), [Type::unknown()])
} else {
Type::unknown()
}
},
)))
}
}

View file

@ -4,13 +4,15 @@
use itertools::Either;
use crate::Db;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct OutOfBoundsError;
pub(crate) trait PyIndex {
type Item;
pub(crate) trait PyIndex<'db> {
type Item: 'db;
fn py_index(&mut self, index: i32) -> Result<Self::Item, OutOfBoundsError>;
fn py_index(self, db: &'db dyn Db, index: i32) -> Result<Self::Item, OutOfBoundsError>;
}
fn from_nonnegative_i32(index: i32) -> usize {
@ -39,13 +41,13 @@ enum Position {
AfterEnd,
}
enum Nth {
pub(crate) enum Nth {
FromStart(usize),
FromEnd(usize),
}
impl Nth {
fn from_index(index: i32) -> Self {
pub(crate) fn from_index(index: i32) -> Self {
if index >= 0 {
Nth::FromStart(from_nonnegative_i32(index))
} else {
@ -75,13 +77,26 @@ impl Nth {
}
}
impl<I, T> PyIndex for T
impl<'db, T> PyIndex<'db> for &'db [T] {
type Item = &'db T;
fn py_index(self, _db: &'db dyn Db, index: i32) -> Result<&'db T, OutOfBoundsError> {
match Nth::from_index(index) {
Nth::FromStart(nth) => self.get(nth).ok_or(OutOfBoundsError),
Nth::FromEnd(nth_rev) => (self.len().checked_sub(nth_rev + 1))
.map(|idx| &self[idx])
.ok_or(OutOfBoundsError),
}
}
}
impl<'db, I: 'db, T> PyIndex<'db> for &mut T
where
T: DoubleEndedIterator<Item = I>,
{
type Item = I;
fn py_index(&mut self, index: i32) -> Result<I, OutOfBoundsError> {
fn py_index(self, _db: &'db dyn Db, index: i32) -> Result<I, OutOfBoundsError> {
match Nth::from_index(index) {
Nth::FromStart(nth) => self.nth(nth).ok_or(OutOfBoundsError),
Nth::FromEnd(nth_rev) => self.nth_back(nth_rev).ok_or(OutOfBoundsError),
@ -92,32 +107,28 @@ where
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct StepSizeZeroError;
pub(crate) trait PySlice {
type Item;
pub(crate) trait PySlice<'db> {
type Item: 'db;
fn py_slice(
&self,
&'db self,
db: &'db dyn Db,
start: Option<i32>,
stop: Option<i32>,
step: Option<i32>,
) -> Result<
Either<impl Iterator<Item = &Self::Item>, impl Iterator<Item = &Self::Item>>,
StepSizeZeroError,
>;
) -> Result<impl Iterator<Item = &'db Self::Item>, StepSizeZeroError>;
}
impl<T> PySlice for [T] {
impl<'db, T: 'db> PySlice<'db> for [T] {
type Item = T;
fn py_slice(
&self,
&'db self,
_db: &'db dyn Db,
start: Option<i32>,
stop: Option<i32>,
step_int: Option<i32>,
) -> Result<
Either<impl Iterator<Item = &Self::Item>, impl Iterator<Item = &Self::Item>>,
StepSizeZeroError,
> {
) -> Result<impl Iterator<Item = &'db Self::Item>, StepSizeZeroError> {
let step_int = step_int.unwrap_or(1);
if step_int == 0 {
return Err(StepSizeZeroError);
@ -194,6 +205,8 @@ impl<T> PySlice for [T] {
#[cfg(test)]
#[expect(clippy::redundant_clone)]
mod tests {
use crate::Db;
use crate::db::tests::setup_db;
use crate::util::subscript::{OutOfBoundsError, StepSizeZeroError};
use super::{PyIndex, PySlice};
@ -201,302 +214,387 @@ mod tests {
#[test]
fn py_index_empty() {
let db = setup_db();
let iter = std::iter::empty::<char>();
assert_eq!(iter.clone().py_index(0), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(1), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(-1), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(i32::MIN), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(i32::MAX), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, 0), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, 1), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, -1), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, i32::MIN), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, i32::MAX), Err(OutOfBoundsError));
}
#[test]
fn py_index_single_element() {
let db = setup_db();
let iter = ['a'].into_iter();
assert_eq!(iter.clone().py_index(0), Ok('a'));
assert_eq!(iter.clone().py_index(1), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(-1), Ok('a'));
assert_eq!(iter.clone().py_index(-2), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, 0), Ok('a'));
assert_eq!(iter.clone().py_index(&db, 1), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, -1), Ok('a'));
assert_eq!(iter.clone().py_index(&db, -2), Err(OutOfBoundsError));
}
#[test]
fn py_index_more_elements() {
let db = setup_db();
let iter = ['a', 'b', 'c', 'd', 'e'].into_iter();
assert_eq!(iter.clone().py_index(0), Ok('a'));
assert_eq!(iter.clone().py_index(1), Ok('b'));
assert_eq!(iter.clone().py_index(4), Ok('e'));
assert_eq!(iter.clone().py_index(5), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, 0), Ok('a'));
assert_eq!(iter.clone().py_index(&db, 1), Ok('b'));
assert_eq!(iter.clone().py_index(&db, 4), Ok('e'));
assert_eq!(iter.clone().py_index(&db, 5), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(-1), Ok('e'));
assert_eq!(iter.clone().py_index(-2), Ok('d'));
assert_eq!(iter.clone().py_index(-5), Ok('a'));
assert_eq!(iter.clone().py_index(-6), Err(OutOfBoundsError));
assert_eq!(iter.clone().py_index(&db, -1), Ok('e'));
assert_eq!(iter.clone().py_index(&db, -2), Ok('d'));
assert_eq!(iter.clone().py_index(&db, -5), Ok('a'));
assert_eq!(iter.clone().py_index(&db, -6), Err(OutOfBoundsError));
}
#[test]
fn py_index_uses_full_index_range() {
let db = setup_db();
let iter = 0..=u32::MAX;
// u32::MAX - |i32::MIN| + 1 = 2^32 - 1 - 2^31 + 1 = 2^31
assert_eq!(iter.clone().py_index(i32::MIN), Ok(2u32.pow(31)));
assert_eq!(iter.clone().py_index(-2), Ok(u32::MAX - 2 + 1));
assert_eq!(iter.clone().py_index(-1), Ok(u32::MAX - 1 + 1));
assert_eq!(iter.clone().py_index(&db, i32::MIN), Ok(2u32.pow(31)));
assert_eq!(iter.clone().py_index(&db, -2), Ok(u32::MAX - 2 + 1));
assert_eq!(iter.clone().py_index(&db, -1), Ok(u32::MAX - 1 + 1));
assert_eq!(iter.clone().py_index(0), Ok(0));
assert_eq!(iter.clone().py_index(1), Ok(1));
assert_eq!(iter.clone().py_index(i32::MAX), Ok(i32::MAX as u32));
assert_eq!(iter.clone().py_index(&db, 0), Ok(0));
assert_eq!(iter.clone().py_index(&db, 1), Ok(1));
assert_eq!(iter.clone().py_index(&db, i32::MAX), Ok(i32::MAX as u32));
}
#[track_caller]
fn assert_eq_slice<const N: usize, const M: usize>(
db: &dyn Db,
input: &[char; N],
start: Option<i32>,
stop: Option<i32>,
step: Option<i32>,
expected: &[char; M],
) {
assert_equal(input.py_slice(start, stop, step).unwrap(), expected.iter());
assert_equal(
input.py_slice(db, start, stop, step).unwrap(),
expected.iter(),
);
}
#[test]
fn py_slice_empty_input() {
let db = setup_db();
let input = [];
assert_eq_slice(&input, None, None, None, &[]);
assert_eq_slice(&input, Some(0), None, None, &[]);
assert_eq_slice(&input, None, Some(0), None, &[]);
assert_eq_slice(&input, Some(0), Some(0), None, &[]);
assert_eq_slice(&input, Some(-5), Some(-5), None, &[]);
assert_eq_slice(&input, None, None, Some(-1), &[]);
assert_eq_slice(&input, None, None, Some(2), &[]);
assert_eq_slice(&db, &input, None, None, None, &[]);
assert_eq_slice(&db, &input, Some(0), None, None, &[]);
assert_eq_slice(&db, &input, None, Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(0), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(-5), Some(-5), None, &[]);
assert_eq_slice(&db, &input, None, None, Some(-1), &[]);
assert_eq_slice(&db, &input, None, None, Some(2), &[]);
}
#[test]
fn py_slice_single_element_input() {
let db = setup_db();
let input = ['a'];
assert_eq_slice(&input, None, None, None, &['a']);
assert_eq_slice(&db, &input, None, None, None, &['a']);
assert_eq_slice(&input, Some(0), None, None, &['a']);
assert_eq_slice(&input, None, Some(0), None, &[]);
assert_eq_slice(&input, Some(0), Some(0), None, &[]);
assert_eq_slice(&input, Some(0), Some(1), None, &['a']);
assert_eq_slice(&input, Some(0), Some(2), None, &['a']);
assert_eq_slice(&db, &input, Some(0), None, None, &['a']);
assert_eq_slice(&db, &input, None, Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(0), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(0), Some(1), None, &['a']);
assert_eq_slice(&db, &input, Some(0), Some(2), None, &['a']);
assert_eq_slice(&input, Some(-1), None, None, &['a']);
assert_eq_slice(&input, Some(-1), Some(-1), None, &[]);
assert_eq_slice(&input, Some(-1), Some(0), None, &[]);
assert_eq_slice(&input, Some(-1), Some(1), None, &['a']);
assert_eq_slice(&input, Some(-1), Some(2), None, &['a']);
assert_eq_slice(&input, None, Some(-1), None, &[]);
assert_eq_slice(&db, &input, Some(-1), None, None, &['a']);
assert_eq_slice(&db, &input, Some(-1), Some(-1), None, &[]);
assert_eq_slice(&db, &input, Some(-1), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(-1), Some(1), None, &['a']);
assert_eq_slice(&db, &input, Some(-1), Some(2), None, &['a']);
assert_eq_slice(&db, &input, None, Some(-1), None, &[]);
assert_eq_slice(&input, Some(-2), None, None, &['a']);
assert_eq_slice(&input, Some(-2), Some(-1), None, &[]);
assert_eq_slice(&input, Some(-2), Some(0), None, &[]);
assert_eq_slice(&input, Some(-2), Some(1), None, &['a']);
assert_eq_slice(&input, Some(-2), Some(2), None, &['a']);
assert_eq_slice(&db, &input, Some(-2), None, None, &['a']);
assert_eq_slice(&db, &input, Some(-2), Some(-1), None, &[]);
assert_eq_slice(&db, &input, Some(-2), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(-2), Some(1), None, &['a']);
assert_eq_slice(&db, &input, Some(-2), Some(2), None, &['a']);
}
#[test]
fn py_slice_nonnegative_indices() {
let db = setup_db();
let input = ['a', 'b', 'c', 'd', 'e'];
assert_eq_slice(&input, None, Some(0), None, &[]);
assert_eq_slice(&input, None, Some(1), None, &['a']);
assert_eq_slice(&input, None, Some(4), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&input, None, Some(5), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, None, Some(6), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, None, None, None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, None, Some(0), None, &[]);
assert_eq_slice(&db, &input, None, Some(1), None, &['a']);
assert_eq_slice(&db, &input, None, Some(4), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&db, &input, None, Some(5), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, None, Some(6), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, None, None, None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(0), Some(0), None, &[]);
assert_eq_slice(&input, Some(0), Some(1), None, &['a']);
assert_eq_slice(&input, Some(0), Some(4), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&input, Some(0), Some(5), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(0), Some(6), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(0), None, None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, Some(0), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(0), Some(1), None, &['a']);
assert_eq_slice(&db, &input, Some(0), Some(4), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(
&db,
&input,
Some(0),
Some(5),
None,
&['a', 'b', 'c', 'd', 'e'],
);
assert_eq_slice(
&db,
&input,
Some(0),
Some(6),
None,
&['a', 'b', 'c', 'd', 'e'],
);
assert_eq_slice(&db, &input, Some(0), None, None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(1), Some(0), None, &[]);
assert_eq_slice(&input, Some(1), Some(1), None, &[]);
assert_eq_slice(&input, Some(1), Some(2), None, &['b']);
assert_eq_slice(&input, Some(1), Some(4), None, &['b', 'c', 'd']);
assert_eq_slice(&input, Some(1), Some(5), None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(1), Some(6), None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(1), None, None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, Some(1), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(1), Some(1), None, &[]);
assert_eq_slice(&db, &input, Some(1), Some(2), None, &['b']);
assert_eq_slice(&db, &input, Some(1), Some(4), None, &['b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(1), Some(5), None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, Some(1), Some(6), None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, Some(1), None, None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(4), Some(0), None, &[]);
assert_eq_slice(&input, Some(4), Some(4), None, &[]);
assert_eq_slice(&input, Some(4), Some(5), None, &['e']);
assert_eq_slice(&input, Some(4), Some(6), None, &['e']);
assert_eq_slice(&input, Some(4), None, None, &['e']);
assert_eq_slice(&db, &input, Some(4), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(4), Some(4), None, &[]);
assert_eq_slice(&db, &input, Some(4), Some(5), None, &['e']);
assert_eq_slice(&db, &input, Some(4), Some(6), None, &['e']);
assert_eq_slice(&db, &input, Some(4), None, None, &['e']);
assert_eq_slice(&input, Some(5), Some(0), None, &[]);
assert_eq_slice(&input, Some(5), Some(5), None, &[]);
assert_eq_slice(&input, Some(5), Some(6), None, &[]);
assert_eq_slice(&input, Some(5), None, None, &[]);
assert_eq_slice(&db, &input, Some(5), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(5), Some(5), None, &[]);
assert_eq_slice(&db, &input, Some(5), Some(6), None, &[]);
assert_eq_slice(&db, &input, Some(5), None, None, &[]);
assert_eq_slice(&input, Some(6), Some(0), None, &[]);
assert_eq_slice(&input, Some(6), Some(6), None, &[]);
assert_eq_slice(&input, Some(6), None, None, &[]);
assert_eq_slice(&db, &input, Some(6), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(6), Some(6), None, &[]);
assert_eq_slice(&db, &input, Some(6), None, None, &[]);
}
#[test]
fn py_slice_negative_indices() {
let db = setup_db();
let input = ['a', 'b', 'c', 'd', 'e'];
assert_eq_slice(&input, Some(-6), None, None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-6), Some(-1), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&input, Some(-6), Some(-4), None, &['a']);
assert_eq_slice(&input, Some(-6), Some(-5), None, &[]);
assert_eq_slice(&input, Some(-6), Some(-6), None, &[]);
assert_eq_slice(&input, Some(-6), Some(-10), None, &[]);
assert_eq_slice(
&db,
&input,
Some(-6),
None,
None,
&['a', 'b', 'c', 'd', 'e'],
);
assert_eq_slice(&db, &input, Some(-6), Some(-1), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(-6), Some(-4), None, &['a']);
assert_eq_slice(&db, &input, Some(-6), Some(-5), None, &[]);
assert_eq_slice(&db, &input, Some(-6), Some(-6), None, &[]);
assert_eq_slice(&db, &input, Some(-6), Some(-10), None, &[]);
assert_eq_slice(&input, Some(-5), None, None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-5), Some(-1), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&input, Some(-5), Some(-4), None, &['a']);
assert_eq_slice(&input, Some(-5), Some(-5), None, &[]);
assert_eq_slice(&input, Some(-5), Some(-6), None, &[]);
assert_eq_slice(&input, Some(-5), Some(-10), None, &[]);
assert_eq_slice(
&db,
&input,
Some(-5),
None,
None,
&['a', 'b', 'c', 'd', 'e'],
);
assert_eq_slice(&db, &input, Some(-5), Some(-1), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(-5), Some(-4), None, &['a']);
assert_eq_slice(&db, &input, Some(-5), Some(-5), None, &[]);
assert_eq_slice(&db, &input, Some(-5), Some(-6), None, &[]);
assert_eq_slice(&db, &input, Some(-5), Some(-10), None, &[]);
assert_eq_slice(&input, Some(-4), None, None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-4), Some(-1), None, &['b', 'c', 'd']);
assert_eq_slice(&input, Some(-4), Some(-3), None, &['b']);
assert_eq_slice(&input, Some(-4), Some(-4), None, &[]);
assert_eq_slice(&input, Some(-4), Some(-10), None, &[]);
assert_eq_slice(&db, &input, Some(-4), None, None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, Some(-4), Some(-1), None, &['b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(-4), Some(-3), None, &['b']);
assert_eq_slice(&db, &input, Some(-4), Some(-4), None, &[]);
assert_eq_slice(&db, &input, Some(-4), Some(-10), None, &[]);
assert_eq_slice(&input, Some(-1), None, None, &['e']);
assert_eq_slice(&input, Some(-1), Some(-1), None, &[]);
assert_eq_slice(&input, Some(-1), Some(-10), None, &[]);
assert_eq_slice(&db, &input, Some(-1), None, None, &['e']);
assert_eq_slice(&db, &input, Some(-1), Some(-1), None, &[]);
assert_eq_slice(&db, &input, Some(-1), Some(-10), None, &[]);
assert_eq_slice(&input, None, Some(-1), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&input, None, Some(-4), None, &['a']);
assert_eq_slice(&input, None, Some(-5), None, &[]);
assert_eq_slice(&input, None, Some(-6), None, &[]);
assert_eq_slice(&db, &input, None, Some(-1), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&db, &input, None, Some(-4), None, &['a']);
assert_eq_slice(&db, &input, None, Some(-5), None, &[]);
assert_eq_slice(&db, &input, None, Some(-6), None, &[]);
}
#[test]
fn py_slice_mixed_positive_negative_indices() {
let db = setup_db();
let input = ['a', 'b', 'c', 'd', 'e'];
assert_eq_slice(&input, Some(0), Some(-1), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&input, Some(1), Some(-1), None, &['b', 'c', 'd']);
assert_eq_slice(&input, Some(3), Some(-1), None, &['d']);
assert_eq_slice(&input, Some(4), Some(-1), None, &[]);
assert_eq_slice(&input, Some(5), Some(-1), None, &[]);
assert_eq_slice(&db, &input, Some(0), Some(-1), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(1), Some(-1), None, &['b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(3), Some(-1), None, &['d']);
assert_eq_slice(&db, &input, Some(4), Some(-1), None, &[]);
assert_eq_slice(&db, &input, Some(5), Some(-1), None, &[]);
assert_eq_slice(&input, Some(0), Some(-4), None, &['a']);
assert_eq_slice(&input, Some(1), Some(-4), None, &[]);
assert_eq_slice(&input, Some(3), Some(-4), None, &[]);
assert_eq_slice(&db, &input, Some(0), Some(-4), None, &['a']);
assert_eq_slice(&db, &input, Some(1), Some(-4), None, &[]);
assert_eq_slice(&db, &input, Some(3), Some(-4), None, &[]);
assert_eq_slice(&input, Some(0), Some(-5), None, &[]);
assert_eq_slice(&input, Some(1), Some(-5), None, &[]);
assert_eq_slice(&input, Some(3), Some(-5), None, &[]);
assert_eq_slice(&db, &input, Some(0), Some(-5), None, &[]);
assert_eq_slice(&db, &input, Some(1), Some(-5), None, &[]);
assert_eq_slice(&db, &input, Some(3), Some(-5), None, &[]);
assert_eq_slice(&input, Some(0), Some(-6), None, &[]);
assert_eq_slice(&input, Some(1), Some(-6), None, &[]);
assert_eq_slice(&db, &input, Some(0), Some(-6), None, &[]);
assert_eq_slice(&db, &input, Some(1), Some(-6), None, &[]);
assert_eq_slice(&input, Some(-6), Some(6), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-6), Some(5), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-6), Some(4), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&input, Some(-6), Some(1), None, &['a']);
assert_eq_slice(&input, Some(-6), Some(0), None, &[]);
assert_eq_slice(
&db,
&input,
Some(-6),
Some(6),
None,
&['a', 'b', 'c', 'd', 'e'],
);
assert_eq_slice(
&db,
&input,
Some(-6),
Some(5),
None,
&['a', 'b', 'c', 'd', 'e'],
);
assert_eq_slice(&db, &input, Some(-6), Some(4), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(-6), Some(1), None, &['a']);
assert_eq_slice(&db, &input, Some(-6), Some(0), None, &[]);
assert_eq_slice(&input, Some(-5), Some(6), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-5), Some(5), None, &['a', 'b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-5), Some(4), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&input, Some(-5), Some(1), None, &['a']);
assert_eq_slice(&input, Some(-5), Some(0), None, &[]);
assert_eq_slice(
&db,
&input,
Some(-5),
Some(6),
None,
&['a', 'b', 'c', 'd', 'e'],
);
assert_eq_slice(
&db,
&input,
Some(-5),
Some(5),
None,
&['a', 'b', 'c', 'd', 'e'],
);
assert_eq_slice(&db, &input, Some(-5), Some(4), None, &['a', 'b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(-5), Some(1), None, &['a']);
assert_eq_slice(&db, &input, Some(-5), Some(0), None, &[]);
assert_eq_slice(&input, Some(-4), Some(6), None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-4), Some(5), None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&input, Some(-4), Some(4), None, &['b', 'c', 'd']);
assert_eq_slice(&input, Some(-4), Some(2), None, &['b']);
assert_eq_slice(&input, Some(-4), Some(1), None, &[]);
assert_eq_slice(&input, Some(-4), Some(0), None, &[]);
assert_eq_slice(&db, &input, Some(-4), Some(6), None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, Some(-4), Some(5), None, &['b', 'c', 'd', 'e']);
assert_eq_slice(&db, &input, Some(-4), Some(4), None, &['b', 'c', 'd']);
assert_eq_slice(&db, &input, Some(-4), Some(2), None, &['b']);
assert_eq_slice(&db, &input, Some(-4), Some(1), None, &[]);
assert_eq_slice(&db, &input, Some(-4), Some(0), None, &[]);
assert_eq_slice(&input, Some(-1), Some(6), None, &['e']);
assert_eq_slice(&input, Some(-1), Some(5), None, &['e']);
assert_eq_slice(&input, Some(-1), Some(4), None, &[]);
assert_eq_slice(&input, Some(-1), Some(1), None, &[]);
assert_eq_slice(&db, &input, Some(-1), Some(6), None, &['e']);
assert_eq_slice(&db, &input, Some(-1), Some(5), None, &['e']);
assert_eq_slice(&db, &input, Some(-1), Some(4), None, &[]);
assert_eq_slice(&db, &input, Some(-1), Some(1), None, &[]);
}
#[test]
fn py_slice_step_forward() {
let db = setup_db();
// indices: 0 1 2 3 4 5 6
let input = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
// Step size zero is invalid:
assert!(matches!(
input.py_slice(None, None, Some(0)),
input.py_slice(&db, None, None, Some(0)),
Err(StepSizeZeroError)
));
assert!(matches!(
input.py_slice(Some(0), Some(5), Some(0)),
input.py_slice(&db, Some(0), Some(5), Some(0)),
Err(StepSizeZeroError)
));
assert!(matches!(
input.py_slice(Some(0), Some(0), Some(0)),
input.py_slice(&db, Some(0), Some(0), Some(0)),
Err(StepSizeZeroError)
));
assert_eq_slice(&input, Some(0), Some(8), Some(2), &['a', 'c', 'e', 'g']);
assert_eq_slice(&input, Some(0), Some(7), Some(2), &['a', 'c', 'e', 'g']);
assert_eq_slice(&input, Some(0), Some(6), Some(2), &['a', 'c', 'e']);
assert_eq_slice(&input, Some(0), Some(5), Some(2), &['a', 'c', 'e']);
assert_eq_slice(&input, Some(0), Some(4), Some(2), &['a', 'c']);
assert_eq_slice(&input, Some(0), Some(3), Some(2), &['a', 'c']);
assert_eq_slice(&input, Some(0), Some(2), Some(2), &['a']);
assert_eq_slice(&input, Some(0), Some(1), Some(2), &['a']);
assert_eq_slice(&input, Some(0), Some(0), Some(2), &[]);
assert_eq_slice(&input, Some(1), Some(5), Some(2), &['b', 'd']);
assert_eq_slice(
&db,
&input,
Some(0),
Some(8),
Some(2),
&['a', 'c', 'e', 'g'],
);
assert_eq_slice(
&db,
&input,
Some(0),
Some(7),
Some(2),
&['a', 'c', 'e', 'g'],
);
assert_eq_slice(&db, &input, Some(0), Some(6), Some(2), &['a', 'c', 'e']);
assert_eq_slice(&db, &input, Some(0), Some(5), Some(2), &['a', 'c', 'e']);
assert_eq_slice(&db, &input, Some(0), Some(4), Some(2), &['a', 'c']);
assert_eq_slice(&db, &input, Some(0), Some(3), Some(2), &['a', 'c']);
assert_eq_slice(&db, &input, Some(0), Some(2), Some(2), &['a']);
assert_eq_slice(&db, &input, Some(0), Some(1), Some(2), &['a']);
assert_eq_slice(&db, &input, Some(0), Some(0), Some(2), &[]);
assert_eq_slice(&db, &input, Some(1), Some(5), Some(2), &['b', 'd']);
assert_eq_slice(&input, Some(0), Some(7), Some(3), &['a', 'd', 'g']);
assert_eq_slice(&input, Some(0), Some(6), Some(3), &['a', 'd']);
assert_eq_slice(&db, &input, Some(0), Some(7), Some(3), &['a', 'd', 'g']);
assert_eq_slice(&db, &input, Some(0), Some(6), Some(3), &['a', 'd']);
assert_eq_slice(&input, Some(0), None, Some(10), &['a']);
assert_eq_slice(&db, &input, Some(0), None, Some(10), &['a']);
}
#[test]
fn py_slice_step_backward() {
let db = setup_db();
// indices: 0 1 2 3 4 5 6
let input = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
assert_eq_slice(&input, Some(7), Some(0), Some(-2), &['g', 'e', 'c']);
assert_eq_slice(&input, Some(6), Some(0), Some(-2), &['g', 'e', 'c']);
assert_eq_slice(&input, Some(5), Some(0), Some(-2), &['f', 'd', 'b']);
assert_eq_slice(&input, Some(4), Some(0), Some(-2), &['e', 'c']);
assert_eq_slice(&input, Some(3), Some(0), Some(-2), &['d', 'b']);
assert_eq_slice(&input, Some(2), Some(0), Some(-2), &['c']);
assert_eq_slice(&input, Some(1), Some(0), Some(-2), &['b']);
assert_eq_slice(&input, Some(0), Some(0), Some(-2), &[]);
assert_eq_slice(&db, &input, Some(7), Some(0), Some(-2), &['g', 'e', 'c']);
assert_eq_slice(&db, &input, Some(6), Some(0), Some(-2), &['g', 'e', 'c']);
assert_eq_slice(&db, &input, Some(5), Some(0), Some(-2), &['f', 'd', 'b']);
assert_eq_slice(&db, &input, Some(4), Some(0), Some(-2), &['e', 'c']);
assert_eq_slice(&db, &input, Some(3), Some(0), Some(-2), &['d', 'b']);
assert_eq_slice(&db, &input, Some(2), Some(0), Some(-2), &['c']);
assert_eq_slice(&db, &input, Some(1), Some(0), Some(-2), &['b']);
assert_eq_slice(&db, &input, Some(0), Some(0), Some(-2), &[]);
assert_eq_slice(&input, Some(7), None, Some(-2), &['g', 'e', 'c', 'a']);
assert_eq_slice(&input, None, None, Some(-2), &['g', 'e', 'c', 'a']);
assert_eq_slice(&input, None, Some(0), Some(-2), &['g', 'e', 'c']);
assert_eq_slice(&db, &input, Some(7), None, Some(-2), &['g', 'e', 'c', 'a']);
assert_eq_slice(&db, &input, None, None, Some(-2), &['g', 'e', 'c', 'a']);
assert_eq_slice(&db, &input, None, Some(0), Some(-2), &['g', 'e', 'c']);
assert_eq_slice(&input, Some(5), Some(1), Some(-2), &['f', 'd']);
assert_eq_slice(&input, Some(5), Some(2), Some(-2), &['f', 'd']);
assert_eq_slice(&input, Some(5), Some(3), Some(-2), &['f']);
assert_eq_slice(&input, Some(5), Some(4), Some(-2), &['f']);
assert_eq_slice(&input, Some(5), Some(5), Some(-2), &[]);
assert_eq_slice(&db, &input, Some(5), Some(1), Some(-2), &['f', 'd']);
assert_eq_slice(&db, &input, Some(5), Some(2), Some(-2), &['f', 'd']);
assert_eq_slice(&db, &input, Some(5), Some(3), Some(-2), &['f']);
assert_eq_slice(&db, &input, Some(5), Some(4), Some(-2), &['f']);
assert_eq_slice(&db, &input, Some(5), Some(5), Some(-2), &[]);
assert_eq_slice(&input, Some(6), None, Some(-3), &['g', 'd', 'a']);
assert_eq_slice(&input, Some(6), Some(0), Some(-3), &['g', 'd']);
assert_eq_slice(&db, &input, Some(6), None, Some(-3), &['g', 'd', 'a']);
assert_eq_slice(&db, &input, Some(6), Some(0), Some(-3), &['g', 'd']);
assert_eq_slice(&input, Some(7), None, Some(-10), &['g']);
assert_eq_slice(&db, &input, Some(7), None, Some(-10), &['g']);
assert_eq_slice(&input, Some(-6), Some(-9), Some(-1), &['b', 'a']);
assert_eq_slice(&input, Some(-6), Some(-8), Some(-1), &['b', 'a']);
assert_eq_slice(&input, Some(-6), Some(-7), Some(-1), &['b']);
assert_eq_slice(&input, Some(-6), Some(-6), Some(-1), &[]);
assert_eq_slice(&db, &input, Some(-6), Some(-9), Some(-1), &['b', 'a']);
assert_eq_slice(&db, &input, Some(-6), Some(-8), Some(-1), &['b', 'a']);
assert_eq_slice(&db, &input, Some(-6), Some(-7), Some(-1), &['b']);
assert_eq_slice(&db, &input, Some(-6), Some(-6), Some(-1), &[]);
assert_eq_slice(&input, Some(-7), Some(-9), Some(-1), &['a']);
assert_eq_slice(&db, &input, Some(-7), Some(-9), Some(-1), &['a']);
assert_eq_slice(&input, Some(-8), Some(-9), Some(-1), &[]);
assert_eq_slice(&input, Some(-9), Some(-9), Some(-1), &[]);
assert_eq_slice(&db, &input, Some(-8), Some(-9), Some(-1), &[]);
assert_eq_slice(&db, &input, Some(-9), Some(-9), Some(-1), &[]);
assert_eq_slice(&input, Some(-6), Some(-2), Some(-1), &[]);
assert_eq_slice(&input, Some(-9), Some(-6), Some(-1), &[]);
assert_eq_slice(&db, &input, Some(-6), Some(-2), Some(-1), &[]);
assert_eq_slice(&db, &input, Some(-9), Some(-6), Some(-1), &[]);
}
}