Skip to content

[ty] Intern InferableTypeVars#24161

Merged
dcreager merged 1 commit intomainfrom
dcreager/intern-inferable
Mar 25, 2026
Merged

[ty] Intern InferableTypeVars#24161
dcreager merged 1 commit intomainfrom
dcreager/intern-inferable

Conversation

@dcreager
Copy link
Copy Markdown
Member

@dcreager dcreager commented Mar 24, 2026

Most importantly, this lets us use InferableTypeVars as the parameter to a salsa-tracked function, which, try as I might, I end up needing for the SpecializationBuilder refactoring.

Plus, the type was over-thought to begin with. By far most of these are created by querying a GenericContext, via a method that was already salsa-tracked. The main thing that we lose is that, when checking assignability of generic callables, we used to be able to "combine" the various inferable sets without having to actually construct a new hash set. Now we do have to do that. But, we can salsa-track the merge method, which should hopefully claw back some of that. Let's see what CI says!

@dcreager dcreager added the internal An internal refactor or improvement label Mar 24, 2026
@dcreager dcreager added the ty Multi-file analysis & type inference label Mar 24, 2026
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Mar 24, 2026

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 85.31%. The percentage of expected errors that received a diagnostic held steady at 78.79%. The number of fully passing files held steady at 65/133.

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Mar 24, 2026

Memory usage report

Summary

Project Old New Diff Outcome
prefect 703.66MB 704.70MB +0.15% (1.04MB)
sphinx 262.64MB 262.87MB +0.09% (237.95kB)
trio 115.82MB 115.94MB +0.10% (119.32kB)
flake8 48.13MB 48.16MB +0.06% (28.93kB)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
infer_expression_types_impl 55.84MB 56.08MB +0.43% (245.31kB)
infer_definition_types 90.50MB 90.71MB +0.23% (212.86kB)
InferableTypeVarsInner 0.00B 200.13kB +200.13kB (new)
infer_expression_type_impl 13.91MB 14.04MB +0.91% (128.94kB)
inferable_typevars_inner 152.04kB 69.17kB -54.51% (82.88kB)
StaticClassLiteral<'db>::implicit_attribute_inner_ 10.02MB 10.09MB +0.70% (71.73kB)
is_redundant_with_impl 5.45MB 5.50MB +0.94% (52.29kB)
Type<'db>::member_lookup_with_policy_ 16.23MB 16.28MB +0.27% (45.05kB)
InferableTypeVars<'db>::merge_::interned_arguments 0.00B 40.99kB +40.99kB (new)
InferableTypeVars<'db>::merge_ 0.00B 33.98kB +33.98kB (new)
all_narrowing_constraints_for_expression 7.39MB 7.42MB +0.41% (31.16kB)
infer_scope_types_impl 53.49MB 53.52MB +0.05% (29.33kB)
Type<'db>::class_member_with_policy_ 17.70MB 17.73MB +0.14% (25.41kB)
all_negative_narrowing_constraints_for_expression 2.79MB 2.81MB +0.53% (15.20kB)
GenericAlias<'db>::variance_of_ 569.36kB 573.88kB +0.79% (4.52kB)
... 15 more

sphinx

Name Old New Diff Outcome
InferableTypeVarsInner 0.00B 85.23kB +85.23kB (new)
infer_expression_types_impl 19.63MB 19.69MB +0.32% (63.59kB)
infer_definition_types 24.34MB 24.39MB +0.20% (50.55kB)
inferable_typevars_inner 73.53kB 35.08kB -52.29% (38.45kB)
is_redundant_with_impl 1.76MB 1.78MB +1.17% (21.16kB)
infer_scope_types_impl 15.50MB 15.51MB +0.11% (16.80kB)
InferableTypeVars<'db>::merge_::interned_arguments 0.00B 12.45kB +12.45kB (new)
InferableTypeVars<'db>::merge_ 0.00B 9.95kB +9.95kB (new)
infer_expression_type_impl 3.57MB 3.58MB +0.22% (8.09kB)
all_narrowing_constraints_for_expression 2.48MB 2.48MB +0.09% (2.30kB)
loop_header_reachability 514.95kB 517.10kB +0.42% (2.16kB)
infer_deferred_types 5.60MB 5.60MB +0.02% (1.24kB)
all_negative_narrowing_constraints_for_expression 1.08MB 1.08MB +0.10% (1.12kB)
Type<'db>::member_lookup_with_policy_ 6.51MB 6.51MB +0.01% (912.00B)
StaticClassLiteral<'db>::implicit_attribute_inner_ 2.40MB 2.40MB +0.02% (504.00B)
... 4 more

trio

Name Old New Diff Outcome
InferableTypeVarsInner 0.00B 76.55kB +76.55kB (new)
inferable_typevars_inner 62.12kB 29.23kB -52.95% (32.89kB)
infer_expression_types_impl 6.01MB 6.03MB +0.32% (19.92kB)
infer_definition_types 7.50MB 7.52MB +0.23% (17.60kB)
InferableTypeVars<'db>::merge_::interned_arguments 0.00B 13.64kB +13.64kB (new)
InferableTypeVars<'db>::merge_ 0.00B 10.93kB +10.93kB (new)
infer_scope_types_impl 4.76MB 4.76MB +0.10% (4.90kB)
infer_expression_type_impl 1.40MB 1.41MB +0.26% (3.73kB)
is_redundant_with_impl 468.45kB 469.91kB +0.31% (1.46kB)
all_narrowing_constraints_for_expression 626.01kB 626.95kB +0.15% (960.00B)
loop_header_reachability 136.56kB 137.41kB +0.62% (864.00B)
infer_deferred_types 2.36MB 2.36MB +0.02% (408.00B)
Type<'db>::member_lookup_with_policy_ 1.81MB 1.81MB +0.02% (360.00B)
is_equivalent_to_object_inner 32.34kB 32.60kB +0.80% (264.00B)
cached_protocol_interface 127.43kB 127.66kB +0.18% (240.00B)
... 6 more

flake8

Name Old New Diff Outcome
InferableTypeVarsInner 0.00B 26.07kB +26.07kB (new)
inferable_typevars_inner 23.00kB 11.00kB -52.17% (12.00kB)
infer_definition_types 1.94MB 1.95MB +0.20% (3.94kB)
infer_expression_types_impl 1.03MB 1.03MB +0.29% (3.07kB)
InferableTypeVars<'db>::merge_::interned_arguments 0.00B 2.53kB +2.53kB (new)
InferableTypeVars<'db>::merge_ 0.00B 1.97kB +1.97kB (new)
infer_scope_types_impl 996.02kB 997.08kB +0.11% (1.05kB)
infer_expression_type_impl 198.18kB 198.88kB +0.35% (720.00B)
is_redundant_with_impl 136.18kB 136.72kB +0.40% (552.00B)
loop_header_reachability 42.65kB 43.10kB +1.04% (456.00B)
all_narrowing_constraints_for_expression 102.12kB 102.34kB +0.21% (216.00B)
infer_unpack_types 47.73kB 47.85kB +0.25% (120.00B)
Type<'db>::member_lookup_with_policy_ 488.34kB 488.41kB +0.01% (72.00B)
is_equivalent_to_object_inner 11.41kB 11.46kB +0.41% (48.00B)
all_negative_narrowing_constraints_for_expression 45.35kB 45.39kB +0.10% (48.00B)
... 3 more

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Mar 24, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-await 40 0 0
invalid-return-type 1 0 0
Total 41 0 0

Changes in flaky projects detected. Raw diff output excludes flaky projects; see the HTML report for details.

Full report with detailed diff (timing results)

@carljm carljm removed their request for review March 24, 2026 22:11
Copy link
Copy Markdown
Contributor

@sharkdp sharkdp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The benchmark results are a mixed bag, but overall it seems to be performance neutral.

Comment on lines +210 to +213
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, get_size2::GetSize, salsa::Update)]
pub(crate) enum InferableTypeVars<'db> {
None,
One(&'a FxHashSet<BoundTypeVarIdentity<'db>>),
Two(
&'a InferableTypeVars<'a, 'db>,
&'a InferableTypeVars<'a, 'db>,
),
Some(InferableTypeVarsInner<'db>),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the main benefit of this layer that we don't need a db when constructing an empty set via ::None? Or are you worried that an interned empty set would be more costly performance-wise?

Details
diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
index 7cea85b668..7baa46014b 100644
--- a/crates/ty_python_semantic/src/types.rs
+++ b/crates/ty_python_semantic/src/types.rs
@@ -3487,7 +3487,7 @@ impl<'db> Type<'db> {
     fn bindings(self, db: &'db dyn Db) -> Bindings<'db> {
         match self {
             Type::Callable(callable) => {
-                CallableBinding::from_overloads(self, callable.signatures(db).iter().cloned())
+                CallableBinding::from_overloads(db, self, callable.signatures(db).iter().cloned())
                     .into()
             }
 
@@ -3506,23 +3506,24 @@ impl<'db> Type<'db> {
 
             Type::BoundMethod(bound_method) => {
                 let signature = bound_method.function(db).signature(db);
-                CallableBinding::from_overloads(self, signature.overloads.iter().cloned())
+                CallableBinding::from_overloads(db, self, signature.overloads.iter().cloned())
                     .with_bound_type(bound_method.self_instance(db))
                     .into()
             }
 
             Type::KnownBoundMethod(method) => {
-                CallableBinding::from_overloads(self, method.signatures(db)).into()
+                CallableBinding::from_overloads(db, self, method.signatures(db)).into()
             }
 
             Type::WrapperDescriptor(wrapper_descriptor) => {
-                CallableBinding::from_overloads(self, wrapper_descriptor.signatures(db)).into()
+                CallableBinding::from_overloads(db, self, wrapper_descriptor.signatures(db)).into()
             }
 
             // TODO: We should probably also check the original return type of the function
             // that was decorated with `@dataclass_transform`, to see if it is consistent with
             // with what we configure here.
             Type::DataclassTransformer(_) => Binding::single(
+                db,
                 self,
                 Signature::new(
                     Parameters::new(
@@ -3542,6 +3543,7 @@ impl<'db> Type<'db> {
                     | KnownFunction::IsSubtypeOf
                     | KnownFunction::IsDisjointFrom,
                 ) => Binding::single(
+                    db,
                     self,
                     Signature::new(
                         Parameters::new(
@@ -3562,6 +3564,7 @@ impl<'db> Type<'db> {
 
                 Some(KnownFunction::IsSingleton | KnownFunction::IsSingleValued) => {
                     Binding::single(
+                        db,
                         self,
                         Signature::new(
                             Parameters::new(
@@ -3584,6 +3587,7 @@ impl<'db> Type<'db> {
                     );
 
                     Binding::single(
+                        db,
                         self,
                         Signature::new_generic(
                             Some(GenericContext::from_typevar_instances(db, [val_ty])),
@@ -3605,6 +3609,7 @@ impl<'db> Type<'db> {
 
                 Some(KnownFunction::AssertNever) => {
                     Binding::single(
+                        db,
                         self,
                         Signature::new(
                             Parameters::new(
@@ -3623,6 +3628,7 @@ impl<'db> Type<'db> {
                 }
 
                 Some(KnownFunction::Cast) => Binding::single(
+                    db,
                     self,
                     Signature::new(
                         Parameters::new(
@@ -3642,6 +3648,7 @@ impl<'db> Type<'db> {
 
                 Some(KnownFunction::Dataclass) => {
                     CallableBinding::from_overloads(
+                        db,
                         self,
                         [
                             // def dataclass(cls: None, /) -> Callable[[type[_T]], type[_T]]: ...
@@ -3721,6 +3728,7 @@ impl<'db> Type<'db> {
                 }
 
                 _ => CallableBinding::from_overloads(
+                    db,
                     self,
                     function_type.signature(db).overloads.iter().cloned(),
                 )
@@ -3736,7 +3744,8 @@ impl<'db> Type<'db> {
 
             Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
                 SubclassOfInner::Dynamic(dynamic_type) => {
-                    Binding::single(self, Signature::dynamic(Type::Dynamic(dynamic_type))).into()
+                    Binding::single(db, self, Signature::dynamic(Type::Dynamic(dynamic_type)))
+                        .into()
                 }
                 SubclassOfInner::Class(class) => self.constructor_bindings(db, class),
                 SubclassOfInner::TypeVar(tvar) => {
@@ -3761,6 +3770,7 @@ impl<'db> Type<'db> {
 
             Type::SpecialForm(SpecialFormType::TypedDict) => {
                 Binding::single(
+                    db,
                     self,
                     Signature::new(
                         Parameters::new(
@@ -3818,7 +3828,7 @@ impl<'db> Type<'db> {
             // Dynamic types are callable, and the return type is the same dynamic type. Similarly,
             // `Never` is always callable and returns `Never`.
             Type::Dynamic(_) | Type::Never => {
-                Binding::single(self, Signature::dynamic(self)).into()
+                Binding::single(db, self, Signature::dynamic(self)).into()
             }
 
             // Note that this correctly returns `None` if none of the union elements are callable.
@@ -3852,7 +3862,7 @@ impl<'db> Type<'db> {
                 let returns = IntersectionType::from_two_elements(db, typevar_meta, Type::any());
                 let signature =
                     Signature::new_generic(Some(context), Parameters::new(db, parameters), returns);
-                Binding::single(self, signature).into()
+                Binding::single(db, self, signature).into()
             }
 
             // TODO: some `SpecialForm`s are callable (e.g. TypedDicts)
@@ -3866,6 +3876,7 @@ impl<'db> Type<'db> {
             },
 
             Type::KnownInstance(KnownInstanceType::NewType(newtype)) => Binding::single(
+                db,
                 self,
                 Signature::new(
                     Parameters::new(
@@ -3910,6 +3921,7 @@ impl<'db> Type<'db> {
                 // ```
                 Some(
                     Binding::single(
+                        db,
                         self,
                         Signature::new(
                             Parameters::new(
@@ -3935,6 +3947,7 @@ impl<'db> Type<'db> {
                 // ```
                 Some(
                     CallableBinding::from_overloads(
+                        db,
                         self,
                         [
                             Signature::new(
@@ -3987,13 +4000,17 @@ impl<'db> Type<'db> {
                 //    def __new__(cls) -> Self: ...
                 // ```
                 Some(
-                    Binding::single(self, Signature::new(Parameters::empty(), Type::object()))
-                        .into(),
+                    Binding::single(
+                        db,
+                        self,
+                        Signature::new(Parameters::empty(), Type::object()),
+                    )
+                    .into(),
                 )
             }
 
             KnownClass::Enum => {
-                Some(Binding::single(self, Signature::todo("functional `Enum` syntax")).into())
+                Some(Binding::single(db, self, Signature::todo("functional `Enum` syntax")).into())
             }
 
             KnownClass::Super => {
@@ -4008,6 +4025,7 @@ impl<'db> Type<'db> {
                 // ```
                 Some(
                     CallableBinding::from_overloads(
+                        db,
                         self,
                         [
                             Signature::new(
@@ -4051,6 +4069,7 @@ impl<'db> Type<'db> {
                 // ```
                 Some(
                     Binding::single(
+                        db,
                         self,
                         Signature::new(
                             Parameters::new(
@@ -4091,6 +4110,7 @@ impl<'db> Type<'db> {
                 // ```
                 Some(
                     Binding::single(
+                        db,
                         self,
                         Signature::new(
                             Parameters::new(
@@ -4151,6 +4171,7 @@ impl<'db> Type<'db> {
 
                 Some(
                     Binding::single(
+                        db,
                         self,
                         Signature::new(
                             Parameters::new(
@@ -4209,6 +4230,7 @@ impl<'db> Type<'db> {
                 // ```
                 Some(
                     CallableBinding::from_overloads(
+                        db,
                         self,
                         [
                             Signature::new(Parameters::empty(), Type::empty_tuple(db)),
@@ -4279,6 +4301,7 @@ impl<'db> Type<'db> {
         let fallback_bindings = || {
             let return_type = self.to_instance(db).unwrap_or(Type::unknown());
             Binding::single(
+                db,
                 self,
                 Signature::new_generic(
                     class_generic_context,
@@ -4449,6 +4472,7 @@ impl<'db> Type<'db> {
                         // to `object`. Keep analysis going and surface the missing-implicit-call
                         // lint via the builder.
                         let mut bindings: Bindings<'db> = Binding::single(
+                            db,
                             self_type,
                             Signature::new(Parameters::gradual_form(), constructor_instance_ty),
                         )
diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs
index 472a1a7469..6211d5a2d3 100644
--- a/crates/ty_python_semantic/src/types/call/bind.rs
+++ b/crates/ty_python_semantic/src/types/call/bind.rs
@@ -1197,7 +1197,7 @@ impl<'db> Bindings<'db> {
                                         db,
                                         *ty_b,
                                         constraints,
-                                        InferableTypeVars::None,
+                                        InferableTypeVars::none(db),
                                     )
                                 });
                                 let tracked = InternedConstraintSet::new(db, result);
@@ -1215,7 +1215,7 @@ impl<'db> Bindings<'db> {
                                         db,
                                         *ty_b,
                                         constraints,
-                                        InferableTypeVars::None,
+                                        InferableTypeVars::none(db),
                                     )
                                 });
                                 let tracked = InternedConstraintSet::new(db, result);
@@ -1233,7 +1233,7 @@ impl<'db> Bindings<'db> {
                                         db,
                                         *ty_b,
                                         constraints,
-                                        InferableTypeVars::None,
+                                        InferableTypeVars::none(db),
                                     )
                                 });
                                 let tracked = InternedConstraintSet::new(db, result);
@@ -1844,7 +1844,7 @@ impl<'db> Bindings<'db> {
                                 *ty_b,
                                 constraints.load(db, tracked.constraints(db)),
                                 constraints,
-                                InferableTypeVars::None,
+                                InferableTypeVars::none(db),
                             )
                         });
                         let tracked = InternedConstraintSet::new(db, result);
@@ -1882,7 +1882,7 @@ impl<'db> Bindings<'db> {
                         let extract_inferable = |instance: &NominalInstanceType<'db>| {
                             if instance.has_known_class(db, KnownClass::NoneType) {
                                 // Caller explicitly passed None, so no typevars are inferable.
-                                return Some(InferableTypeVars::None);
+                                return Some(InferableTypeVars::none(db));
                             }
                             let typevars: Option<FxOrderSet<_>> = instance
                                 .tuple_spec(db)?
@@ -1897,7 +1897,7 @@ impl<'db> Bindings<'db> {
 
                         let inferable = match overload.parameter_types() {
                             // Caller did not provide argument, so no typevars are inferable.
-                            [None] => InferableTypeVars::None,
+                            [None] => InferableTypeVars::none(db),
                             [Some(Type::NominalInstance(instance))] => {
                                 match extract_inferable(instance) {
                                     Some(inferable) => inferable,
@@ -2090,12 +2090,13 @@ pub(crate) struct CallableBinding<'db> {
 
 impl<'db> CallableBinding<'db> {
     pub(crate) fn from_overloads(
+        db: &'db dyn Db,
         signature_type: Type<'db>,
         overloads: impl IntoIterator<Item = Signature<'db>>,
     ) -> Self {
         let overloads = overloads
             .into_iter()
-            .map(|signature| Binding::single(signature_type, signature))
+            .map(|signature| Binding::single(db, signature_type, signature))
             .collect();
         Self {
             callable_type: signature_type,
@@ -2383,7 +2384,7 @@ impl<'db> CallableBinding<'db> {
                 // https://github.com/astral-sh/ty/issues/735 for more details.
                 for overload in &mut self.overloads {
                     // Clear the state of all overloads before re-evaluating from step 1
-                    overload.reset();
+                    overload.reset(db);
                     overload.match_parameters(db, expanded_arguments, &mut argument_forms);
                 }
 
@@ -3706,7 +3707,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
             call_expression_tcx,
             return_ty,
             errors,
-            inferable_typevars: InferableTypeVars::None,
+            inferable_typevars: InferableTypeVars::none(db),
             specialization: None,
             constraint_set_errors: vec![false; arguments.len()],
         }
@@ -4245,8 +4246,11 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
         };
 
         // Create Bindings with all overloads and perform full overload resolution
-        let callable_binding =
-            CallableBinding::from_overloads(self.signature_type, signatures.iter().cloned());
+        let callable_binding = CallableBinding::from_overloads(
+            self.db,
+            self.signature_type,
+            signatures.iter().cloned(),
+        );
         let bindings = match Bindings::from(callable_binding)
             .match_parameters(self.db, &sub_arguments)
             .check_types(
@@ -4522,14 +4526,18 @@ pub(crate) struct Binding<'db> {
 }
 
 impl<'db> Binding<'db> {
-    pub(crate) fn single(signature_type: Type<'db>, signature: Signature<'db>) -> Binding<'db> {
+    pub(crate) fn single(
+        db: &'db dyn Db,
+        signature_type: Type<'db>,
+        signature: Signature<'db>,
+    ) -> Binding<'db> {
         Binding {
             signature,
             callable_type: signature_type,
             signature_type,
             constructor_instance_type: None,
             return_ty: Type::unknown(),
-            inferable_typevars: InferableTypeVars::None,
+            inferable_typevars: InferableTypeVars::none(db),
             specialization: None,
             argument_matches: Box::from([]),
             variadic_argument_matched_to_variadic_parameter: false,
@@ -4764,9 +4772,9 @@ impl<'db> Binding<'db> {
     }
 
     /// Resets the state of this binding to its initial state.
-    fn reset(&mut self) {
+    fn reset(&mut self, db: &'db dyn Db) {
         self.return_ty = Type::unknown();
-        self.inferable_typevars = InferableTypeVars::None;
+        self.inferable_typevars = InferableTypeVars::none(db);
         self.specialization = None;
         self.argument_matches = Box::from([]);
         self.parameter_tys = Box::from([]);
diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs
index 785048eb92..669326d700 100644
--- a/crates/ty_python_semantic/src/types/class.rs
+++ b/crates/ty_python_semantic/src/types/class.rs
@@ -1052,7 +1052,7 @@ impl<'db> ClassType<'db> {
         let disjointness_visitor = IsDisjointVisitor::default(&constraints);
         let checker = TypeRelationChecker::subtyping(
             &constraints,
-            InferableTypeVars::None,
+            InferableTypeVars::none(db),
             &relation_visitor,
             &disjointness_visitor,
         );
@@ -1104,7 +1104,7 @@ impl<'db> ClassType<'db> {
                                 db,
                                 other_alias.specialization(db),
                                 constraints,
-                                InferableTypeVars::None,
+                                InferableTypeVars::none(db),
                             )
                             .is_always_satisfied(db)
                 }
@@ -1175,7 +1175,7 @@ impl<'db> ClassType<'db> {
                 db,
                 other_metaclass_instance,
                 constraints,
-                InferableTypeVars::None,
+                InferableTypeVars::none(db),
             )
             .is_always_satisfied(db)
         {
diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs
index 4df1ebcd2e..834a1bc815 100644
--- a/crates/ty_python_semantic/src/types/generics.rs
+++ b/crates/ty_python_semantic/src/types/generics.rs
@@ -3,7 +3,7 @@ use std::cell::{Cell, RefCell};
 use std::collections::hash_map::Entry;
 use std::fmt::Display;
 
-use itertools::{Either, Itertools};
+use itertools::Itertools;
 use ruff_python_ast as ast;
 use rustc_hash::{FxHashMap, FxHashSet};
 
@@ -207,39 +207,31 @@ pub(crate) fn typing_self<'db>(
     )
 }
 
-#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, get_size2::GetSize, salsa::Update)]
-pub(crate) enum InferableTypeVars<'db> {
-    None,
-    Some(InferableTypeVarsInner<'db>),
+#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
+pub(crate) struct InferableTypeVars<'db> {
+    #[returns(ref)]
+    inferable: FxOrderSet<BoundTypeVarIdentity<'db>>,
 }
 
+// The Salsa heap is tracked separately.
+impl get_size2::GetSize for InferableTypeVars<'_> {}
+
 impl<'db> InferableTypeVars<'db> {
+    pub(crate) fn none(db: &'db dyn Db) -> Self {
+        Self::new(db, FxOrderSet::default())
+    }
+
     pub(crate) fn from_typevars(
         db: &'db dyn Db,
         typevars: FxOrderSet<BoundTypeVarIdentity<'db>>,
     ) -> Self {
-        if typevars.is_empty() {
-            return InferableTypeVars::None;
-        }
-        Self::Some(InferableTypeVarsInner::new_internal(db, typevars))
+        Self::new(db, typevars)
     }
 }
 
-#[salsa::interned(debug, constructor=new_internal, heap_size=ruff_memory_usage::heap_size)]
-pub(crate) struct InferableTypeVarsInner<'db> {
-    #[returns(ref)]
-    inferable: FxOrderSet<BoundTypeVarIdentity<'db>>,
-}
-
-// The Salsa heap is tracked separately.
-impl get_size2::GetSize for InferableTypeVarsInner<'_> {}
-
 impl<'db> BoundTypeVarIdentity<'db> {
     pub(crate) fn is_inferable(self, db: &'db dyn Db, inferable: InferableTypeVars<'db>) -> bool {
-        match inferable {
-            InferableTypeVars::None => false,
-            InferableTypeVars::Some(inner) => inner.inferable(db).contains(&self),
-        }
+        inferable.inferable(db).contains(&self)
     }
 }
 
@@ -253,13 +245,8 @@ impl<'db> BoundTypeVarInstance<'db> {
 impl<'db> InferableTypeVars<'db> {
     #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
     pub(crate) fn merge(self, db: &'db dyn Db, other: Self) -> Self {
-        match (self, other) {
-            (InferableTypeVars::None, other) | (other, InferableTypeVars::None) => other,
-            (InferableTypeVars::Some(self_inner), InferableTypeVars::Some(other_inner)) => {
-                let merged = self_inner.inferable(db) | other_inner.inferable(db);
-                Self::Some(InferableTypeVarsInner::new_internal(db, merged))
-            }
-        }
+        let merged = self.inferable(db) | other.inferable(db);
+        Self::new(db, merged)
     }
 
     // This is not an IntoIterator implementation because I have no desire to try to name the
@@ -268,10 +255,7 @@ impl<'db> InferableTypeVars<'db> {
         self,
         db: &'db dyn Db,
     ) -> impl Iterator<Item = BoundTypeVarIdentity<'db>> + 'db {
-        match self {
-            InferableTypeVars::None => Either::Left(std::iter::empty()),
-            InferableTypeVars::Some(inner) => Either::Right(inner.inferable(db).iter().copied()),
-        }
+        self.inferable(db).iter().copied()
     }
 
     // Keep this around for debugging purposes
@@ -419,7 +403,7 @@ impl<'db> GenericContext<'db> {
         }
 
         #[salsa::tracked(
-            cycle_initial=|_, _, _| InferableTypeVars::None,
+            cycle_initial=|db, _, _| InferableTypeVars::none(db),
             heap_size=ruff_memory_usage::heap_size,
         )]
         fn inferable_typevars_inner<'db>(
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index bddc2487ec..679f7dda91 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -4984,7 +4984,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
                     .signature
                     .generic_context
                     .map(|generic_context| generic_context.inferable_typevars(db))
-                    .unwrap_or(InferableTypeVars::None);
+                    .unwrap_or_else(|| InferableTypeVars::none(self.db()));
 
                 !overload
                     .constructor_instance_type
@@ -5664,7 +5664,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
                 .try_to_class_literal(self.db())
                 .and_then(|class| class.generic_context(self.db()))
                 .map(|generic_context| generic_context.inferable_typevars(self.db()))
-                .unwrap_or(InferableTypeVars::None);
+                .unwrap_or_else(|| InferableTypeVars::none(self.db()));
             annotation.filter_disjoint_elements(
                 self.db(),
                 Type::homogeneous_tuple(self.db(), Type::unknown()),
diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs
index d07f4ef240..017152ba28 100644
--- a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs
@@ -636,7 +636,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
                                     db,
                                     bound,
                                     &constraints,
-                                    InferableTypeVars::None,
+                                    InferableTypeVars::none(db),
                                 )
                                 .is_never_satisfied(db)
                             {
@@ -669,7 +669,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
                                     db,
                                     typevar_constraints.as_type(db),
                                     &constraints,
-                                    InferableTypeVars::None,
+                                    InferableTypeVars::none(db),
                                 )
                                 .is_never_satisfied(db)
                             {
diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs
index d0e6881002..9f19e485be 100644
--- a/crates/ty_python_semantic/src/types/instance.rs
+++ b/crates/ty_python_semantic/src/types/instance.rs
@@ -717,7 +717,7 @@ impl<'db> ProtocolInstanceType<'db> {
             let disjointness_visitor = IsDisjointVisitor::default(&constraints);
             let checker = TypeRelationChecker::subtyping(
                 &constraints,
-                InferableTypeVars::None,
+                InferableTypeVars::none(db),
                 &relation_visitor,
                 &disjointness_visitor,
             );
diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs
index 565a592637..7b356302ef 100644
--- a/crates/ty_python_semantic/src/types/relation.rs
+++ b/crates/ty_python_semantic/src/types/relation.rs
@@ -293,7 +293,7 @@ impl<'db> Type<'db> {
     /// See [`TypeRelation::Subtyping`] for more details.
     pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool {
         let constraints = ConstraintSetBuilder::new();
-        self.when_subtype_of(db, target, &constraints, InferableTypeVars::None)
+        self.when_subtype_of(db, target, &constraints, InferableTypeVars::none(db))
             .is_always_satisfied(db)
     }
 
@@ -335,7 +335,7 @@ impl<'db> Type<'db> {
     /// See `TypeRelation::Assignability` for more details.
     pub fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
         let constraints = ConstraintSetBuilder::new();
-        self.when_assignable_to(db, target, &constraints, InferableTypeVars::None)
+        self.when_assignable_to(db, target, &constraints, InferableTypeVars::none(db))
             .is_always_satisfied(db)
     }
 
@@ -376,7 +376,7 @@ impl<'db> Type<'db> {
             db,
             target,
             constraints,
-            InferableTypeVars::None,
+            InferableTypeVars::none(db),
             TypeRelation::ConstraintSetAssignability,
         )
     }
@@ -396,7 +396,7 @@ impl<'db> Type<'db> {
                     db,
                     other,
                     &ConstraintSetBuilder::new(),
-                    InferableTypeVars::None,
+                    InferableTypeVars::none(db),
                     TypeRelation::Redundancy { pure: false },
                 )
                 .is_always_satisfied(db)
@@ -477,7 +477,7 @@ impl<'db> Type<'db> {
     /// `false` answers in some cases.
     pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool {
         let constraints = ConstraintSetBuilder::new();
-        self.when_disjoint_from(db, other, &constraints, InferableTypeVars::None)
+        self.when_disjoint_from(db, other, &constraints, InferableTypeVars::none(db))
             .is_always_satisfied(db)
     }
 
@@ -556,13 +556,14 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> {
     }
 
     pub(super) fn constraint_set_assignability(
+        db: &'db dyn Db,
         constraints: &'c ConstraintSetBuilder<'db>,
         relation_visitor: &'a HasRelationToVisitor<'db, 'c>,
         disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>,
     ) -> Self {
         Self {
             constraints,
-            inferable: InferableTypeVars::None,
+            inferable: InferableTypeVars::none(db),
             relation: TypeRelation::ConstraintSetAssignability,
             given: ConstraintSet::from_bool(constraints, false),
             relation_visitor,
@@ -1540,12 +1541,12 @@ pub(super) struct EquivalenceChecker<'a, 'c, 'db> {
 }
 
 impl<'c, 'db> EquivalenceChecker<'_, 'c, 'db> {
-    fn as_relation_checker(&self) -> TypeRelationChecker<'_, 'c, 'db> {
+    fn as_relation_checker(&self, db: &'db dyn Db) -> TypeRelationChecker<'_, 'c, 'db> {
         TypeRelationChecker {
             relation: TypeRelation::Redundancy { pure: true },
             constraints: self.constraints,
             given: self.given,
-            inferable: InferableTypeVars::None,
+            inferable: InferableTypeVars::none(db),
             relation_visitor: self.relation_visitor,
             disjointness_visitor: self.disjointness_visitor,
         }
@@ -1565,7 +1566,7 @@ impl<'c, 'db> EquivalenceChecker<'_, 'c, 'db> {
         left: Type<'db>,
         right: Type<'db>,
     ) -> ConstraintSet<'db, 'c> {
-        let relation_checker = self.as_relation_checker();
+        let relation_checker = self.as_relation_checker(db);
         relation_checker
             .check_type_pair(db, left, right)
             .and(db, self.constraints, || {
diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs
index d5b40ae6a5..4a086eb162 100644
--- a/crates/ty_python_semantic/src/types/signatures.rs
+++ b/crates/ty_python_semantic/src/types/signatures.rs
@@ -323,6 +323,7 @@ impl<'db> CallableSignature<'db> {
         let relation_visitor = HasRelationToVisitor::default(constraints);
         let disjointness_visitor = IsDisjointVisitor::default(constraints);
         let checker = TypeRelationChecker::constraint_set_assignability(
+            db,
             constraints,
             &relation_visitor,
             &disjointness_visitor,
@@ -718,7 +719,7 @@ impl<'db> Signature<'db> {
     fn inferable_typevars(&self, db: &'db dyn Db) -> InferableTypeVars<'db> {
         match self.generic_context {
             Some(generic_context) => generic_context.inferable_typevars(db),
-            None => InferableTypeVars::None,
+            None => InferableTypeVars::none(db),
         }
     }
 
@@ -781,6 +782,7 @@ impl<'db> Signature<'db> {
         let relation_visitor = HasRelationToVisitor::default(constraints);
         let disjointness_visitor = IsDisjointVisitor::default(constraints);
         let checker = TypeRelationChecker::constraint_set_assignability(
+            db,
             constraints,
             &relation_visitor,
             &disjointness_visitor,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was partly to avoid the diff churn changing all of the ::None callsites, but more importantly, there are enough of them that I did want to avoid having to hit the global intern hash table to get the ID of the empty set.

@dcreager dcreager merged commit 7d38e1b into main Mar 25, 2026
49 checks passed
@dcreager dcreager deleted the dcreager/intern-inferable branch March 25, 2026 17:27
carljm added a commit that referenced this pull request Mar 25, 2026
* main:
  [ty] make `test-case` a dev-dependency (#24187)
  [ty] implement cycle normalization for more types to prevent too-many-cycle panics (#24061)
  [ty] Silence all diagnostics in unreachable code (#24179)
  [ty] Intern `InferableTypeVars` (#24161)
  Implement unnecessary-if (RUF050) (#24114)
  Recognize `Self` annotation and `self` assignment in SLF001 (#24144)
  Bump the npm version before publish (#24178)
  [ty] Disallow Self in metaclass and static methods (#23231)
  Use trusted publishing for NPM packages (#24171)
  [ty] Respect non-explicitly defined dataclass params (#24170)
  Add RUF072: warn when using  operator on an f-string (#24162)
  [ty] Check return type of generator functions (#24026)
  Implement useless-finally (RUF-072) (#24165)
  [ty] Add test for a dataclass with a default field converter (#24169)
  [ty] Dataclass field converters (#23088)
  [flake8-bandit] Treat sys.executable as trusted input in S603 (#24106)
  [ty] Add support for `typing.Concatenate` (#23689)
  `ASYNC115`: autofix to use full qualified `anyio.lowlevel` import (#24166)
  [ty] Disallow read-only fields in TypedDict updates (#24128)
  Speed up diagnostic rendering (#24146)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

internal An internal refactor or improvement ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants