Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
77a62ae
return constraint sets directly in function comparison
dcreager Oct 1, 2025
296d485
pass inferable typevars explicitly
dcreager Oct 1, 2025
c7b9270
it's a start
dcreager Oct 1, 2025
f5720c4
separate type for inferable
dcreager Oct 1, 2025
9883c12
track inferable when infering
dcreager Oct 1, 2025
4ac1f29
function signature typevars are inferable
dcreager Oct 2, 2025
5ddfc1d
clippy
dcreager Oct 2, 2025
7d07b84
add display
dcreager Oct 2, 2025
720c704
track inferable during calls better
dcreager Oct 2, 2025
4a3e94e
use inferable here too
dcreager Oct 2, 2025
d890b41
more inferable
dcreager Oct 2, 2025
20f394c
fix incorrect xarray ecosystem results
dcreager Oct 3, 2025
c76ba5d
track heap size
dcreager Oct 3, 2025
88c388b
fix fuzz panics
dcreager Oct 3, 2025
5db9be8
absolutely consistent
dcreager Oct 7, 2025
53689ef
only infer inferable typevars
dcreager Oct 7, 2025
30f322c
include inferable in snapshot
dcreager Oct 7, 2025
e959369
skip bidi for parameters with typevars
dcreager Oct 10, 2025
998fcae
clippy!
dcreager Oct 10, 2025
5919ef2
Implement TypeVar identity refactoring
dcreager Oct 11, 2025
7cc7faa
clean up claudisms
dcreager Oct 11, 2025
0644e02
Refactor GenericContext to use GenericContextTypeVar struct
dcreager Oct 11, 2025
782214a
var -> variable
dcreager Oct 11, 2025
2ca6c1e
Remove unused 'original' field from TypeVarInstance
dcreager Oct 11, 2025
e6645bb
superfluous
dcreager Oct 11, 2025
6dab7e7
precommit
dcreager Oct 11, 2025
a4dc171
remove finished plan
dcreager Oct 11, 2025
e2f5b50
Merge branch 'dcreager/typevar-identity' into dcreager/non-non-inferable
dcreager Oct 11, 2025
625cee6
pre-commit
dcreager Oct 11, 2025
bbacf20
Move display method to BoundTypeVarIdentity
dcreager Oct 11, 2025
119bcb4
clippy
dcreager Oct 11, 2025
9adc068
Update ConstraintSet to use BoundTypeVarIdentity
dcreager Oct 11, 2025
7dc5144
clippy
dcreager Oct 11, 2025
f589ad0
pre-commit
dcreager Oct 11, 2025
ba69b80
Merge branch 'dcreager/typevar-identity' into dcreager/non-non-inferable
dcreager Oct 11, 2025
5d2738e
Merge branch 'main' into dcreager/typevar-identity
dcreager Oct 11, 2025
0392d9f
Merge branch 'dcreager/typevar-identity' into dcreager/non-non-inferable
dcreager Oct 11, 2025
88f3a21
merge conflicts
dcreager Oct 11, 2025
dd9abdf
missed one
dcreager Oct 11, 2025
8caf208
fix display
dcreager Oct 11, 2025
69a2b33
inferable in filter_disjoint
dcreager Oct 11, 2025
b2825f0
fix tests
dcreager Oct 11, 2025
375b9a3
Merge branch 'dcreager/typevar-identity' into dcreager/non-non-inferable
dcreager Oct 11, 2025
c836146
Merge remote-tracking branch 'origin/main' into dcreager/non-non-infe…
dcreager Oct 14, 2025
5a21bea
just the api parts
dcreager Oct 14, 2025
a2e7a9f
Merge branch 'dcreager/non-inferable-api' into dcreager/non-non-infer…
dcreager Oct 14, 2025
43da7c7
use existing method
dcreager Oct 14, 2025
68fc51c
Merge remote-tracking branch 'origin/main' into dcreager/non-non-infe…
dcreager Oct 15, 2025
642ed43
Merge remote-tracking branch 'origin/main' into dcreager/non-non-infe…
dcreager Oct 16, 2025
5d23e79
Update crates/ty_python_semantic/src/types.rs
dcreager Oct 16, 2025
26484e8
move is_inferable to typevar
dcreager Oct 16, 2025
c5c750c
default!
dcreager Oct 16, 2025
ff112ad
less inner
dcreager Oct 16, 2025
8524583
Merge remote-tracking branch 'origin/main' into dcreager/non-non-infe…
dcreager Oct 16, 2025
8dba99f
clippy
dcreager Oct 16, 2025
5c9aad2
Merge remote-tracking branch 'origin/main' into dcreager/non-non-infe…
dcreager Oct 16, 2025
0997627
tuple inferable
dcreager Oct 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/ty_ide/src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ impl<'db> Completion<'db> {
| Type::BytesLiteral(_) => CompletionKind::Value,
Type::EnumLiteral(_) => CompletionKind::Enum,
Type::ProtocolInstance(_) => CompletionKind::Interface,
Type::NonInferableTypeVar(_) | Type::TypeVar(_) => CompletionKind::TypeParameter,
Type::TypeVar(_) => CompletionKind::TypeParameter,
Type::Union(union) => union
.elements(db)
.iter()
Expand Down
4 changes: 1 addition & 3 deletions crates/ty_ide/src/semantic_tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,9 +336,7 @@ impl<'db> SemanticTokenVisitor<'db> {

match ty {
Type::ClassLiteral(_) => (SemanticTokenType::Class, modifiers),
Type::NonInferableTypeVar(_) | Type::TypeVar(_) => {
(SemanticTokenType::TypeParameter, modifiers)
}
Type::TypeVar(_) => (SemanticTokenType::TypeParameter, modifiers),
Type::FunctionLiteral(_) => {
// Check if this is a method based on current scope
if self.in_class_scope {
Expand Down
317 changes: 77 additions & 240 deletions crates/ty_python_semantic/src/types.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/ty_python_semantic/src/types/bound_super.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ impl<'db> BoundSuperType<'db> {
Type::TypeAlias(alias) => {
return delegate_with_error_mapped(alias.value_type(db), None);
}
Type::TypeVar(type_var) | Type::NonInferableTypeVar(type_var) => {
Type::TypeVar(type_var) => {
let type_var = type_var.typevar(db);
return match type_var.bound_or_constraints(db) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
Expand Down
3 changes: 1 addition & 2 deletions crates/ty_python_semantic/src/types/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1062,8 +1062,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
let mut positive_to_remove = SmallVec::<[usize; 1]>::new();

for (typevar_index, ty) in self.positive.iter().enumerate() {
let (Type::NonInferableTypeVar(bound_typevar) | Type::TypeVar(bound_typevar)) = ty
else {
let Type::TypeVar(bound_typevar) = ty else {
continue;
};
let Some(TypeVarBoundOrConstraints::Constraints(constraints)) =
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_python_semantic/src/types/call/bind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2589,7 +2589,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
return;
};

// TODO: Use the list of inferable typevars from the generic context of the callable.
self.inferable_typevars = generic_context.inferable_typevars(self.db);
let mut builder = SpecializationBuilder::new(self.db, self.inferable_typevars);

let parameters = self.signature.parameters();
Expand Down
11 changes: 3 additions & 8 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1627,15 +1627,10 @@ impl<'db> ClassLiteral<'db> {
})
}

/// Returns a specialization of this class where each typevar is mapped to itself. The second
/// parameter can be `Type::TypeVar` or `Type::NonInferableTypeVar`, depending on the use case.
pub(crate) fn identity_specialization(
self,
db: &'db dyn Db,
typevar_to_type: &impl Fn(BoundTypeVarInstance<'db>) -> Type<'db>,
) -> ClassType<'db> {
/// Returns a specialization of this class where each typevar is mapped to itself.
pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> ClassType<'db> {
self.apply_specialization(db, |generic_context| {
generic_context.identity_specialization(db, typevar_to_type)
generic_context.identity_specialization(db)
})
}

Expand Down
1 change: 0 additions & 1 deletion crates/ty_python_semantic/src/types/class_base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ impl<'db> ClassBase<'db> {
| Type::StringLiteral(_)
| Type::LiteralString
| Type::ModuleLiteral(_)
| Type::NonInferableTypeVar(_)
| Type::TypeVar(_)
| Type::BoundSuper(_)
| Type::ProtocolInstance(_)
Expand Down
4 changes: 1 addition & 3 deletions crates/ty_python_semantic/src/types/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -560,9 +560,7 @@ impl Display for DisplayRepresentation<'_> {
.display_with(self.db, self.settings.clone()),
literal_name = enum_literal.name(self.db)
),
Type::NonInferableTypeVar(bound_typevar) | Type::TypeVar(bound_typevar) => {
bound_typevar.identity(self.db).display(self.db).fmt(f)
}
Type::TypeVar(bound_typevar) => bound_typevar.identity(self.db).display(self.db).fmt(f),
Type::AlwaysTruthy => f.write_str("AlwaysTruthy"),
Type::AlwaysFalsy => f.write_str("AlwaysFalsy"),
Type::BoundSuper(bound_super) => {
Expand Down
31 changes: 3 additions & 28 deletions crates/ty_python_semantic/src/types/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,28 +365,12 @@ impl<'db> OverloadLiteral<'db> {
if function_node.is_async && !is_generator {
signature = signature.wrap_coroutine_return_type(db);
}
signature = signature.mark_typevars_inferable(db);

let pep695_ctx = function_node.type_params.as_ref().map(|type_params| {
GenericContext::from_type_params(db, index, self.definition(db), type_params)
});
let legacy_ctx = GenericContext::from_function_params(
db,
self.definition(db),
signature.parameters(),
signature.return_ty,
);
// We need to update `signature.generic_context` here,
// because type variables in `GenericContext::variables` are still non-inferable.
signature.generic_context =
GenericContext::merge_pep695_and_legacy(db, pep695_ctx, legacy_ctx);

signature
}

/// Typed internally-visible "raw" signature for this function.
/// That is, type variables in parameter types and the return type remain non-inferable,
/// and the return types of async functions are not wrapped in `CoroutineType[...]`.
/// That is, the return types of async functions are not wrapped in `CoroutineType[...]`.
///
/// ## Warning
///
Expand Down Expand Up @@ -1133,7 +1117,6 @@ fn is_instance_truthiness<'db>(
| Type::PropertyInstance(..)
| Type::AlwaysTruthy
| Type::AlwaysFalsy
| Type::NonInferableTypeVar(..)
| Type::TypeVar(..)
| Type::BoundSuper(..)
| Type::TypeIs(..)
Expand Down Expand Up @@ -1729,11 +1712,7 @@ impl KnownFunction {
}

KnownFunction::RangeConstraint => {
let [
Some(lower),
Some(Type::NonInferableTypeVar(typevar)),
Some(upper),
] = parameter_types
let [Some(lower), Some(Type::TypeVar(typevar)), Some(upper)] = parameter_types
else {
return;
};
Expand All @@ -1746,11 +1725,7 @@ impl KnownFunction {
}

KnownFunction::NegatedRangeConstraint => {
let [
Some(lower),
Some(Type::NonInferableTypeVar(typevar)),
Some(upper),
] = parameter_types
let [Some(lower), Some(Type::TypeVar(typevar)), Some(upper)] = parameter_types
else {
return;
};
Expand Down
167 changes: 147 additions & 20 deletions crates/ty_python_semantic/src/types/generics.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use std::marker::PhantomData;
use std::cell::RefCell;
use std::fmt::Display;

use itertools::Itertools;
use ruff_python_ast as ast;
use rustc_hash::FxHashMap;
use rustc_hash::{FxHashMap, FxHashSet};

use crate::semantic_index::definition::Definition;
use crate::semantic_index::scope::{FileScopeId, NodeWithScopeKind, ScopeId};
Expand All @@ -14,14 +15,16 @@ use crate::types::infer::infer_definition_types;
use crate::types::instance::{Protocol, ProtocolInstanceType};
use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type};
use crate::types::visitor::{NonAtomicType, TypeKind, TypeVisitor, walk_non_atomic_type};
use crate::types::{
ApplyTypeMappingVisitor, BoundTypeVarIdentity, BoundTypeVarInstance, ClassLiteral,
FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor,
KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, Type, TypeContext,
TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance,
TypeVarKind, TypeVarVariance, UnionType, binding_type, declaration_type,
walk_bound_type_var_type,
};
use crate::{Db, FxOrderMap, FxOrderSet};
use crate::{Db, FxIndexSet, FxOrderMap, FxOrderSet};

/// Returns an iterator of any generic context introduced by the given scope or any enclosing
/// scope.
Expand Down Expand Up @@ -106,7 +109,6 @@ pub(crate) fn typing_self<'db>(
scope_id: ScopeId,
typevar_binding_context: Option<Definition<'db>>,
class: ClassLiteral<'db>,
typevar_to_type: &impl Fn(BoundTypeVarInstance<'db>) -> Type<'db>,
) -> Option<Type<'db>> {
let index = semantic_index(db, scope_id.file(db));

Expand All @@ -118,7 +120,7 @@ pub(crate) fn typing_self<'db>(
);
let bounds = TypeVarBoundOrConstraints::UpperBound(Type::instance(
db,
class.identity_specialization(db, typevar_to_type),
class.identity_specialization(db),
));
let typevar = TypeVarInstance::new(
db,
Expand All @@ -138,17 +140,70 @@ pub(crate) fn typing_self<'db>(
typevar_binding_context,
typevar,
)
.map(typevar_to_type)
.map(Type::TypeVar)
}

#[derive(Clone, Copy, Debug)]
pub(crate) enum InferableTypeVars<'a, 'db> {
None,
// TODO: This variant isn't used, and only exists so that we can include the 'a and 'db in the
// type definition. They will be used soon when we start creating real InferableTypeVars
// instances.
#[expect(unused)]
Unused(PhantomData<&'a &'db ()>),
One(&'a FxHashSet<BoundTypeVarIdentity<'db>>),
Two(
&'a InferableTypeVars<'a, 'db>,
&'a InferableTypeVars<'a, 'db>,
),
}

impl<'db> BoundTypeVarInstance<'db> {
pub(crate) fn is_inferable(
self,
db: &'db dyn Db,
inferable: InferableTypeVars<'_, 'db>,
) -> bool {
match inferable {
InferableTypeVars::None => false,
InferableTypeVars::One(typevars) => typevars.contains(&self.identity(db)),
InferableTypeVars::Two(left, right) => {
self.is_inferable(db, *left) || self.is_inferable(db, *right)
}
}
}
}

impl<'a, 'db> InferableTypeVars<'a, 'db> {
pub(crate) fn merge(&'a self, other: Option<&'a InferableTypeVars<'a, 'db>>) -> Self {
match other {
Some(other) => InferableTypeVars::Two(self, other),
None => *self,
}
}

// Keep this around for debugging purposes
#[expect(dead_code)]
pub(crate) fn display(&self, db: &'db dyn Db) -> impl Display {
fn find_typevars<'db>(
result: &mut FxHashSet<BoundTypeVarIdentity<'db>>,
inferable: &InferableTypeVars<'_, 'db>,
) {
match inferable {
InferableTypeVars::None => {}
InferableTypeVars::One(typevars) => result.extend(typevars.iter().copied()),
InferableTypeVars::Two(left, right) => {
find_typevars(result, left);
find_typevars(result, right);
}
}
}

let mut typevars = FxHashSet::default();
find_typevars(&mut typevars, self);
format!(
"[{}]",
typevars
.into_iter()
.map(|identity| identity.display(db))
.format(", ")
)
}
}

#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize)]
Expand Down Expand Up @@ -255,6 +310,66 @@ impl<'db> GenericContext<'db> {
)
}

pub(crate) fn inferable_typevars(self, db: &'db dyn Db) -> InferableTypeVars<'db, 'db> {
#[derive(Default)]
struct CollectTypeVars<'db> {
typevars: RefCell<FxHashSet<BoundTypeVarIdentity<'db>>>,
seen_types: RefCell<FxIndexSet<NonAtomicType<'db>>>,
}

impl<'db> TypeVisitor<'db> for CollectTypeVars<'db> {
fn should_visit_lazy_type_attributes(&self) -> bool {
true
}

fn visit_bound_type_var_type(
&self,
db: &'db dyn Db,
bound_typevar: BoundTypeVarInstance<'db>,
) {
self.typevars
.borrow_mut()
.insert(bound_typevar.identity(db));
walk_bound_type_var_type(db, bound_typevar, self);
}

fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) {
match TypeKind::from(ty) {
TypeKind::Atomic => {}
TypeKind::NonAtomic(non_atomic_type) => {
if !self.seen_types.borrow_mut().insert(non_atomic_type) {
// If we have already seen this type, we can skip it.
return;
}
walk_non_atomic_type(db, non_atomic_type, self);
}
}
}
}

#[salsa::tracked(
returns(ref),
cycle_fn=inferable_typevars_cycle_recover,
cycle_initial=inferable_typevars_cycle_initial,
heap_size=ruff_memory_usage::heap_size,
)]
fn inferable_typevars_inner<'db>(
db: &'db dyn Db,
generic_context: GenericContext<'db>,
) -> FxHashSet<BoundTypeVarIdentity<'db>> {
let visitor = CollectTypeVars::default();
for bound_typevar in generic_context.variables(db) {
visitor.visit_bound_type_var_type(db, bound_typevar);
}
visitor.typevars.into_inner()
}

// This ensures that salsa caches the FxHashSet, not the InferableTypeVars that wraps it.
// (That way InferableTypeVars can contain references, and doesn't need to impl
// salsa::Update.)
InferableTypeVars::One(inferable_typevars_inner(db, self))
}

pub(crate) fn variables(
self,
db: &'db dyn Db,
Expand Down Expand Up @@ -410,14 +525,8 @@ impl<'db> GenericContext<'db> {
}

/// Returns a specialization of this generic context where each typevar is mapped to itself.
/// The second parameter can be `Type::TypeVar` or `Type::NonInferableTypeVar`, depending on
/// the use case.
pub(crate) fn identity_specialization(
self,
db: &'db dyn Db,
typevar_to_type: &impl Fn(BoundTypeVarInstance<'db>) -> Type<'db>,
) -> Specialization<'db> {
let types = self.variables(db).map(typevar_to_type).collect();
pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> Specialization<'db> {
let types = self.variables(db).map(Type::TypeVar).collect();
self.specialize(db, types)
}

Expand Down Expand Up @@ -543,6 +652,22 @@ impl<'db> GenericContext<'db> {
}
}

fn inferable_typevars_cycle_recover<'db>(
_db: &'db dyn Db,
_value: &FxHashSet<BoundTypeVarIdentity<'db>>,
_count: u32,
_self: GenericContext<'db>,
) -> salsa::CycleRecoveryAction<FxHashSet<BoundTypeVarIdentity<'db>>> {
salsa::CycleRecoveryAction::Iterate
}

fn inferable_typevars_cycle_initial<'db>(
_db: &'db dyn Db,
_self: GenericContext<'db>,
) -> FxHashSet<BoundTypeVarIdentity<'db>> {
FxHashSet::default()
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(super) enum LegacyGenericBase {
Generic,
Expand Down Expand Up @@ -1357,7 +1482,9 @@ impl<'db> SpecializationBuilder<'db> {
}
}

(Type::TypeVar(bound_typevar), ty) | (ty, Type::TypeVar(bound_typevar)) => {
(Type::TypeVar(bound_typevar), ty) | (ty, Type::TypeVar(bound_typevar))
if bound_typevar.is_inferable(self.db, self.inferable) =>
{
match bound_typevar.typevar(self.db).bound_or_constraints(self.db) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
if !ty
Expand Down
1 change: 0 additions & 1 deletion crates/ty_python_semantic/src/types/ide_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ impl<'db> AllMembers<'db> {
| Type::ProtocolInstance(_)
| Type::SpecialForm(_)
| Type::KnownInstance(_)
| Type::NonInferableTypeVar(_)
| Type::TypeVar(_)
| Type::BoundSuper(_)
| Type::TypeIs(_) => match ty.to_meta_type(db) {
Expand Down
Loading
Loading