From cfa673411cbfb49996c17152735e830a37f0ba46 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 10 Sep 2025 07:59:04 -0700 Subject: [PATCH 1/3] [ty] use Type::Divergent to avoid panic in infinitely-nested-tuple implicit attribute --- Cargo.lock | 6 +- Cargo.toml | 2 +- .../resources/mdtest/attributes.md | 10 ++ crates/ty_python_semantic/src/types.rs | 5 +- crates/ty_python_semantic/src/types/infer.rs | 151 ++++++++++++------ .../src/types/infer/builder.rs | 76 ++++++--- .../ty_python_semantic/src/types/unpacker.rs | 2 +- fuzz/Cargo.toml | 2 +- 8 files changed, 172 insertions(+), 82 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6870a54d3d5d..a55720800ff64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3430,7 +3430,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa" version = "0.23.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9" +source = "git+https://github.com/salsa-rs/salsa.git?rev=3713cd7eb30821c0c086591832dd6f59f2af7fe7#3713cd7eb30821c0c086591832dd6f59f2af7fe7" dependencies = [ "boxcar", "compact_str", @@ -3454,12 +3454,12 @@ dependencies = [ [[package]] name = "salsa-macro-rules" version = "0.23.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9" +source = "git+https://github.com/salsa-rs/salsa.git?rev=3713cd7eb30821c0c086591832dd6f59f2af7fe7#3713cd7eb30821c0c086591832dd6f59f2af7fe7" [[package]] name = "salsa-macros" version = "0.23.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9" +source = "git+https://github.com/salsa-rs/salsa.git?rev=3713cd7eb30821c0c086591832dd6f59f2af7fe7#3713cd7eb30821c0c086591832dd6f59f2af7fe7" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 6c924ada3d5ff..e18858b6880e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,7 +143,7 @@ regex-automata = { version = "0.4.9" } rustc-hash = { version = "2.0.0" } rustc-stable-hash = { version = "0.1.2" } # When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml` -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "a3ffa22cb26756473d56f867aedec3fd907c4dd9", default-features = false, features = [ +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "3713cd7eb30821c0c086591832dd6f59f2af7fe7", default-features = false, features = [ "compact_str", "macros", "salsa_unstable", diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 7b6fb78d7e9cb..ddc87a7ef7d8a 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2419,6 +2419,16 @@ reveal_type(Answer.NO.value) # revealed: Any reveal_type(Answer.__members__) # revealed: MappingProxyType[str, Unknown] ``` +## Divergent inferred implicit instance attribute types + +```py +class C: + def f(self, other: "C"): + self.x = (other.x,) + +reveal_type((C().x,)) # revealed: tuple[Unknown | Divergent] +``` + ## References Some of the tests in the *Class and instance variables* section draw inspiration from diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 7126af975a1a1..62bbac60512b5 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -762,6 +762,10 @@ impl<'db> Type<'db> { Self::Dynamic(DynamicType::Unknown) } + pub(crate) fn divergent(scope: ScopeId<'db>) -> Self { + Self::Dynamic(DynamicType::Divergent(DivergentType { scope })) + } + pub(crate) fn object(db: &'db dyn Db) -> Self { KnownClass::Object.to_instance(db) } @@ -6516,7 +6520,6 @@ impl<'db> Type<'db> { } } - #[allow(unused)] pub(super) fn has_divergent_type(self, db: &'db dyn Db, div: Type<'db>) -> bool { any_over_type(db, self, &|ty| match ty { Type::Dynamic(DynamicType::Divergent(_)) => ty == div, diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 770bb2373670c..df251700f8579 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -85,7 +85,7 @@ fn scope_cycle_recover<'db>( } fn scope_cycle_initial<'db>(_db: &'db dyn Db, scope: ScopeId<'db>) -> ScopeInference<'db> { - ScopeInference::cycle_fallback(scope) + ScopeInference::cycle_initial(scope) } /// Infer all types for a [`Definition`] (including sub-expressions). @@ -123,7 +123,7 @@ fn definition_cycle_initial<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> DefinitionInference<'db> { - DefinitionInference::cycle_fallback(definition.scope(db)) + DefinitionInference::cycle_initial(definition.scope(db)) } /// Infer types for all deferred type expressions in a [`Definition`]. @@ -164,7 +164,7 @@ fn deferred_cycle_initial<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> DefinitionInference<'db> { - DefinitionInference::cycle_fallback(definition.scope(db)) + DefinitionInference::cycle_initial(definition.scope(db)) } /// Infer all types for an [`Expression`] (including sub-expressions). @@ -192,20 +192,29 @@ pub(crate) fn infer_expression_types<'db>( .finish_expression() } +/// How many fixpoint iterations to allow before falling back to Divergent type. +const ITERATIONS_BEFORE_FALLBACK: u32 = 10; + fn expression_cycle_recover<'db>( - _db: &'db dyn Db, + db: &'db dyn Db, _value: &ExpressionInference<'db>, - _count: u32, - _expression: Expression<'db>, + count: u32, + expression: Expression<'db>, ) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate + if count == ITERATIONS_BEFORE_FALLBACK { + salsa::CycleRecoveryAction::Fallback(ExpressionInference::cycle_fallback( + expression.scope(db), + )) + } else { + salsa::CycleRecoveryAction::Iterate + } } fn expression_cycle_initial<'db>( db: &'db dyn Db, expression: Expression<'db>, ) -> ExpressionInference<'db> { - ExpressionInference::cycle_fallback(expression.scope(db)) + ExpressionInference::cycle_initial(expression.scope(db)) } /// Infers the type of an `expression` that is guaranteed to be in the same file as the calling query. @@ -324,7 +333,7 @@ fn unpack_cycle_recover<'db>( } fn unpack_cycle_initial<'db>(_db: &'db dyn Db, _unpack: Unpack<'db>) -> UnpackResult<'db> { - UnpackResult::cycle_fallback(Type::Never) + UnpackResult::cycle_initial(Type::Never) } /// Returns the type of the nearest enclosing class for the given scope. @@ -378,6 +387,34 @@ impl<'db> InferenceRegion<'db> { } } +#[derive(Debug, Clone, Copy, Eq, PartialEq, get_size2::GetSize)] +enum CycleRecovery { + /// An initial-value for fixpoint iteration; all types are `Type::Never`. + Initial, + /// A divergence-fallback value for fixpoint iteration; all types are `Divergent`. + Divergent, +} + +impl CycleRecovery { + fn merge(self, other: Option) -> CycleRecovery { + if let Some(other) = other { + match (self, other) { + (Self::Divergent, _) | (_, Self::Divergent) => Self::Divergent, + _ => Self::Initial, + } + } else { + self + } + } + + fn fallback_type<'db>(self, scope_fn: impl FnOnce() -> ScopeId<'db>) -> Type<'db> { + match self { + Self::Initial => Type::Never, + Self::Divergent => Type::divergent(scope_fn()), + } + } +} + /// The inferred types for a scope region. #[derive(Debug, Eq, PartialEq, salsa::Update, get_size2::GetSize)] pub(crate) struct ScopeInference<'db> { @@ -385,27 +422,28 @@ pub(crate) struct ScopeInference<'db> { expressions: FxHashMap>, /// The extra data that is only present for few inference regions. - extra: Option>, + extra: Option>>, } #[derive(Debug, Eq, PartialEq, get_size2::GetSize, salsa::Update, Default)] -struct ScopeInferenceExtra { - /// The fallback type for missing expressions/bindings/declarations. - /// - /// This is used only when constructing a cycle-recovery `TypeInference`. - cycle_fallback: bool, +struct ScopeInferenceExtra<'db> { + /// Is this a cycle-recovery inference result, and if so, what kind? + cycle_recovery: Option, /// The diagnostics for this region. diagnostics: TypeCheckDiagnostics, + + /// The scope for which this is an inference result, if we are a divergent cycle recovery. + scope: Option>, } impl<'db> ScopeInference<'db> { - fn cycle_fallback(scope: ScopeId<'db>) -> Self { + fn cycle_initial(scope: ScopeId<'db>) -> Self { let _ = scope; Self { extra: Some(Box::new(ScopeInferenceExtra { - cycle_fallback: true, + cycle_recovery: Some(CycleRecovery::Initial), ..ScopeInferenceExtra::default() })), expressions: FxHashMap::default(), @@ -431,14 +469,13 @@ impl<'db> ScopeInference<'db> { .or_else(|| self.fallback_type()) } - fn is_cycle_callback(&self) -> bool { - self.extra - .as_ref() - .is_some_and(|extra| extra.cycle_fallback) - } - fn fallback_type(&self) -> Option> { - self.is_cycle_callback().then_some(Type::Never) + self.extra.as_ref().and_then(|extra| { + extra.cycle_recovery.map(|recovery| { + recovery + .fallback_type(|| extra.scope.expect("Divergent inference should have scope")) + }) + }) } } @@ -471,10 +508,8 @@ pub(crate) struct DefinitionInference<'db> { #[derive(Debug, Eq, PartialEq, get_size2::GetSize, salsa::Update, Default)] struct DefinitionInferenceExtra<'db> { - /// The fallback type for missing expressions/bindings/declarations. - /// - /// This is used only when constructing a cycle-recovery `TypeInference`. - cycle_fallback: bool, + /// Is this a cycle-recovery inference result, and if so, what kind? + cycle_recovery: Option, /// The definitions that are deferred. deferred: Box<[Definition<'db>]>, @@ -484,10 +519,13 @@ struct DefinitionInferenceExtra<'db> { /// For function definitions, the undecorated type of the function. undecorated_type: Option>, + + /// The scope this region is part of, if we are a divergent cycle recovery. + scope: Option>, } impl<'db> DefinitionInference<'db> { - fn cycle_fallback(scope: ScopeId<'db>) -> Self { + fn cycle_initial(scope: ScopeId<'db>) -> Self { let _ = scope; Self { @@ -497,7 +535,7 @@ impl<'db> DefinitionInference<'db> { #[cfg(debug_assertions)] scope, extra: Some(Box::new(DefinitionInferenceExtra { - cycle_fallback: true, + cycle_recovery: Some(CycleRecovery::Initial), ..DefinitionInferenceExtra::default() })), } @@ -566,14 +604,13 @@ impl<'db> DefinitionInference<'db> { self.declarations.iter().map(|(_, qualifiers)| *qualifiers) } - fn is_cycle_callback(&self) -> bool { - self.extra - .as_ref() - .is_some_and(|extra| extra.cycle_fallback) - } - fn fallback_type(&self) -> Option> { - self.is_cycle_callback().then_some(Type::Never) + self.extra.as_ref().and_then(|extra| { + extra.cycle_recovery.map(|recovery| { + recovery + .fallback_type(|| extra.scope.expect("Divergent inference should have scope")) + }) + }) } pub(crate) fn undecorated_type(&self) -> Option> { @@ -605,22 +642,39 @@ struct ExpressionInferenceExtra<'db> { /// The diagnostics for this region. diagnostics: TypeCheckDiagnostics, - /// `true` if this region is part of a cycle-recovery `TypeInference`. - /// - /// Falls back to `Type::Never` if an expression is missing. - cycle_fallback: bool, + /// Is this a cycle recovery inference result, and if so, what kind? + cycle_recovery: Option, /// `true` if all places in this expression are definitely bound all_definitely_bound: bool, + + /// The scope this region is part of (if we are a Divergent cycle recovery.) + scope: Option>, } impl<'db> ExpressionInference<'db> { + fn cycle_initial(scope: ScopeId<'db>) -> Self { + let _ = scope; + Self { + extra: Some(Box::new(ExpressionInferenceExtra { + cycle_recovery: Some(CycleRecovery::Initial), + all_definitely_bound: true, + ..ExpressionInferenceExtra::default() + })), + expressions: FxHashMap::default(), + + #[cfg(debug_assertions)] + scope, + } + } + fn cycle_fallback(scope: ScopeId<'db>) -> Self { let _ = scope; Self { extra: Some(Box::new(ExpressionInferenceExtra { - cycle_fallback: true, + cycle_recovery: Some(CycleRecovery::Divergent), all_definitely_bound: true, + scope: Some(scope), ..ExpressionInferenceExtra::default() })), expressions: FxHashMap::default(), @@ -645,14 +699,13 @@ impl<'db> ExpressionInference<'db> { .unwrap_or_else(Type::unknown) } - fn is_cycle_callback(&self) -> bool { - self.extra - .as_ref() - .is_some_and(|extra| extra.cycle_fallback) - } - fn fallback_type(&self) -> Option> { - self.is_cycle_callback().then_some(Type::Never) + self.extra.as_ref().and_then(|extra| { + extra.cycle_recovery.map(|recovery| { + recovery + .fallback_type(|| extra.scope.expect("Divergent inference should have scope")) + }) + }) } /// Returns true if all places in this expression are definitely bound. diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 878b07d7df2ed..962df7cf15a9e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -9,10 +9,10 @@ use ruff_text_size::{Ranged, TextRange}; use rustc_hash::{FxHashMap, FxHashSet}; use super::{ - DefinitionInference, DefinitionInferenceExtra, ExpressionInference, ExpressionInferenceExtra, - InferenceRegion, ScopeInference, ScopeInferenceExtra, infer_deferred_types, - infer_definition_types, infer_expression_types, infer_same_file_expression_type, - infer_scope_types, infer_unpack_types, + CycleRecovery, DefinitionInference, DefinitionInferenceExtra, ExpressionInference, + ExpressionInferenceExtra, InferenceRegion, ScopeInference, ScopeInferenceExtra, + infer_deferred_types, infer_definition_types, infer_expression_types, + infer_same_file_expression_type, infer_scope_types, infer_unpack_types, }; use crate::module_name::{ModuleName, ModuleNameResolutionError}; use crate::module_resolver::{ @@ -251,10 +251,8 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> { /// For function definitions, the undecorated type of the function. undecorated_type: Option>, - /// The fallback type for missing expressions/bindings/declarations. - /// - /// This is used only when constructing a cycle-recovery `TypeInference`. - cycle_fallback: bool, + /// Did we merge in a sub-region with a cycle-recovery fallback, and if so, what kind? + cycle_recovery: Option, /// `true` if all places in this expression are definitely bound all_definitely_bound: bool, @@ -290,11 +288,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { typevar_binding_context: None, deferred: VecSet::default(), undecorated_type: None, - cycle_fallback: false, + cycle_recovery: None, all_definitely_bound: true, } } + fn extend_cycle_recovery(&mut self, other_recovery: Option) { + match &mut self.cycle_recovery { + Some(recovery) => *recovery = recovery.merge(other_recovery), + recovery @ None => *recovery = other_recovery, + } + } + + fn fallback_type(&self) -> Option> { + self.cycle_recovery + .map(|recovery| recovery.fallback_type(|| self.scope)) + } + fn extend_definition(&mut self, inference: &DefinitionInference<'db>) { #[cfg(debug_assertions)] assert_eq!(self.scope, inference.scope); @@ -307,7 +317,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } if let Some(extra) = &inference.extra { - self.cycle_fallback |= extra.cycle_fallback; + self.extend_cycle_recovery(extra.cycle_recovery); self.context.extend(&extra.diagnostics); self.deferred.extend(extra.deferred.iter().copied()); } @@ -325,7 +335,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(extra) = &inference.extra { self.context.extend(&extra.diagnostics); - self.cycle_fallback |= extra.cycle_fallback; + self.extend_cycle_recovery(extra.cycle_recovery); if !matches!(self.region, InferenceRegion::Scope(..)) { self.bindings.extend(extra.bindings.iter().copied()); @@ -399,7 +409,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.expressions .get(&expr.into()) .copied() - .or(self.cycle_fallback.then_some(Type::Never)) + .or(self.fallback_type()) } /// Get the type of an expression from any scope in the same file. @@ -5189,12 +5199,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { parenthesized: _, } = tuple; - // Collecting all elements is necessary to infer all sub-expressions even if some - // element types are `Never` (which leads `from_elements` to return early without - // consuming the whole iterator). - let element_types: Vec<_> = elts.iter().map(|elt| self.infer_expression(elt)).collect(); + let divergent_marker = Type::divergent(self.scope()); + let mut divergent = None; + let mut element_types = Vec::with_capacity(elts.len()); + for element in elts { + let element_type = self.infer_expression(element); + if element_type.has_divergent_type(self.db(), divergent_marker) { + divergent = Some(element_type); + break; + } + element_types.push(element_type); + } - Type::heterogeneous_tuple(self.db(), element_types) + if let Some(divergent) = divergent { + divergent + } else { + Type::heterogeneous_tuple(self.db(), element_types) + } } fn infer_list_expression(&mut self, list: &ast::ExprList) -> Type<'db> { @@ -8808,7 +8829,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { bindings, declarations, deferred, - cycle_fallback, + cycle_recovery, all_definitely_bound, // Ignored; only relevant to definition regions @@ -8836,7 +8857,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); let extra = - (cycle_fallback || !bindings.is_empty() || !diagnostics.is_empty() || !all_definitely_bound).then(|| { + (cycle_recovery.is_some() || !bindings.is_empty() || !diagnostics.is_empty() || !all_definitely_bound).then(|| { if bindings.len() > 20 { tracing::debug!( "Inferred expression region `{:?}` contains {} bindings. Lookups by linear scan might be slow.", @@ -8848,8 +8869,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Box::new(ExpressionInferenceExtra { bindings: bindings.into_boxed_slice(), diagnostics, - cycle_fallback, + cycle_recovery, all_definitely_bound, + scope: Some(scope), }) }); @@ -8873,7 +8895,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { bindings, declarations, deferred, - cycle_fallback, + cycle_recovery, undecorated_type, all_definitely_bound: _, // builder only state @@ -8889,15 +8911,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let diagnostics = context.finish(); let extra = (!diagnostics.is_empty() - || cycle_fallback + || cycle_recovery.is_some() || undecorated_type.is_some() || !deferred.is_empty()) .then(|| { Box::new(DefinitionInferenceExtra { - cycle_fallback, + cycle_recovery, deferred: deferred.into_boxed_slice(), diagnostics, undecorated_type, + scope: Some(scope), }) }); @@ -8936,7 +8959,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { context, mut expressions, scope, - cycle_fallback, + cycle_recovery, // Ignored, because scope types are never extended into other scopes. deferred: _, @@ -8959,10 +8982,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let _ = scope; let diagnostics = context.finish(); - let extra = (!diagnostics.is_empty() || cycle_fallback).then(|| { + let extra = (!diagnostics.is_empty() || cycle_recovery.is_some()).then(|| { Box::new(ScopeInferenceExtra { - cycle_fallback, + cycle_recovery, diagnostics, + scope: Some(scope), }) }); diff --git a/crates/ty_python_semantic/src/types/unpacker.rs b/crates/ty_python_semantic/src/types/unpacker.rs index 65bec0ef4f097..7197d57e03939 100644 --- a/crates/ty_python_semantic/src/types/unpacker.rs +++ b/crates/ty_python_semantic/src/types/unpacker.rs @@ -224,7 +224,7 @@ impl<'db> UnpackResult<'db> { &self.diagnostics } - pub(crate) fn cycle_fallback(cycle_fallback_type: Type<'db>) -> Self { + pub(crate) fn cycle_initial(cycle_fallback_type: Type<'db>) -> Self { Self { targets: FxHashMap::default(), diagnostics: TypeCheckDiagnostics::default(), diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 4fa0f0d77a37a..dce7fb92212d5 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -30,7 +30,7 @@ ty_python_semantic = { path = "../crates/ty_python_semantic" } ty_vendored = { path = "../crates/ty_vendored" } libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false } -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "a3ffa22cb26756473d56f867aedec3fd907c4dd9", default-features = false, features = [ +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "3713cd7eb30821c0c086591832dd6f59f2af7fe7", default-features = false, features = [ "compact_str", "macros", "salsa_unstable", From 2689da0a9de2a4141bd1fac2de3c873c5b312882 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 10 Sep 2025 22:03:51 -0700 Subject: [PATCH 2/3] improved approach --- .../resources/mdtest/attributes.md | 4 ++-- .../src/types/infer/builder.rs | 23 ++++++++----------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index ddc87a7ef7d8a..1ef154ee3600d 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2424,9 +2424,9 @@ reveal_type(Answer.__members__) # revealed: MappingProxyType[str, Unknown] ```py class C: def f(self, other: "C"): - self.x = (other.x,) + self.x = (other.x, 1) -reveal_type((C().x,)) # revealed: tuple[Unknown | Divergent] +reveal_type(C().x) # revealed: Unknown | tuple[Divergent, Literal[1]] ``` ## References diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 962df7cf15a9e..b1e6fd3b4f3af 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -5199,23 +5199,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { parenthesized: _, } = tuple; - let divergent_marker = Type::divergent(self.scope()); - let mut divergent = None; - let mut element_types = Vec::with_capacity(elts.len()); - for element in elts { + let db = self.db(); + let divergent = Type::divergent(self.scope()); + let element_types = elts.iter().map(|element| { let element_type = self.infer_expression(element); - if element_type.has_divergent_type(self.db(), divergent_marker) { - divergent = Some(element_type); - break; + if element_type.has_divergent_type(self.db(), divergent) { + divergent + } else { + element_type } - element_types.push(element_type); - } + }); - if let Some(divergent) = divergent { - divergent - } else { - Type::heterogeneous_tuple(self.db(), element_types) - } + Type::heterogeneous_tuple(db, element_types) } fn infer_list_expression(&mut self, list: &ast::ExprList) -> Type<'db> { From 1f8073d141bbc8d5fbece9b7377442ad88555659 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 11 Sep 2025 06:12:16 -0700 Subject: [PATCH 3/3] keep the scope inside CycleRecovery::Divergent --- crates/ty_python_semantic/src/types/infer.rs | 62 +++++++------------ .../src/types/infer/builder.rs | 10 +-- 2 files changed, 25 insertions(+), 47 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index df251700f8579..a13f36e35add6 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -387,19 +387,20 @@ impl<'db> InferenceRegion<'db> { } } -#[derive(Debug, Clone, Copy, Eq, PartialEq, get_size2::GetSize)] -enum CycleRecovery { +#[derive(Debug, Clone, Copy, Eq, PartialEq, get_size2::GetSize, salsa::Update)] +enum CycleRecovery<'db> { /// An initial-value for fixpoint iteration; all types are `Type::Never`. Initial, /// A divergence-fallback value for fixpoint iteration; all types are `Divergent`. - Divergent, + Divergent(ScopeId<'db>), } -impl CycleRecovery { - fn merge(self, other: Option) -> CycleRecovery { +impl<'db> CycleRecovery<'db> { + fn merge(self, other: Option>) -> Self { if let Some(other) = other { match (self, other) { - (Self::Divergent, _) | (_, Self::Divergent) => Self::Divergent, + // It's important here that we keep the scope of `self` if merging two `Divergent`. + (Self::Divergent(scope), _) | (_, Self::Divergent(scope)) => Self::Divergent(scope), _ => Self::Initial, } } else { @@ -407,10 +408,10 @@ impl CycleRecovery { } } - fn fallback_type<'db>(self, scope_fn: impl FnOnce() -> ScopeId<'db>) -> Type<'db> { + fn fallback_type(self) -> Type<'db> { match self { Self::Initial => Type::Never, - Self::Divergent => Type::divergent(scope_fn()), + Self::Divergent(scope) => Type::divergent(scope), } } } @@ -428,13 +429,10 @@ pub(crate) struct ScopeInference<'db> { #[derive(Debug, Eq, PartialEq, get_size2::GetSize, salsa::Update, Default)] struct ScopeInferenceExtra<'db> { /// Is this a cycle-recovery inference result, and if so, what kind? - cycle_recovery: Option, + cycle_recovery: Option>, /// The diagnostics for this region. diagnostics: TypeCheckDiagnostics, - - /// The scope for which this is an inference result, if we are a divergent cycle recovery. - scope: Option>, } impl<'db> ScopeInference<'db> { @@ -470,12 +468,9 @@ impl<'db> ScopeInference<'db> { } fn fallback_type(&self) -> Option> { - self.extra.as_ref().and_then(|extra| { - extra.cycle_recovery.map(|recovery| { - recovery - .fallback_type(|| extra.scope.expect("Divergent inference should have scope")) - }) - }) + self.extra + .as_ref() + .and_then(|extra| extra.cycle_recovery.map(CycleRecovery::fallback_type)) } } @@ -509,7 +504,7 @@ pub(crate) struct DefinitionInference<'db> { #[derive(Debug, Eq, PartialEq, get_size2::GetSize, salsa::Update, Default)] struct DefinitionInferenceExtra<'db> { /// Is this a cycle-recovery inference result, and if so, what kind? - cycle_recovery: Option, + cycle_recovery: Option>, /// The definitions that are deferred. deferred: Box<[Definition<'db>]>, @@ -519,9 +514,6 @@ struct DefinitionInferenceExtra<'db> { /// For function definitions, the undecorated type of the function. undecorated_type: Option>, - - /// The scope this region is part of, if we are a divergent cycle recovery. - scope: Option>, } impl<'db> DefinitionInference<'db> { @@ -605,12 +597,9 @@ impl<'db> DefinitionInference<'db> { } fn fallback_type(&self) -> Option> { - self.extra.as_ref().and_then(|extra| { - extra.cycle_recovery.map(|recovery| { - recovery - .fallback_type(|| extra.scope.expect("Divergent inference should have scope")) - }) - }) + self.extra + .as_ref() + .and_then(|extra| extra.cycle_recovery.map(CycleRecovery::fallback_type)) } pub(crate) fn undecorated_type(&self) -> Option> { @@ -643,13 +632,10 @@ struct ExpressionInferenceExtra<'db> { diagnostics: TypeCheckDiagnostics, /// Is this a cycle recovery inference result, and if so, what kind? - cycle_recovery: Option, + cycle_recovery: Option>, /// `true` if all places in this expression are definitely bound all_definitely_bound: bool, - - /// The scope this region is part of (if we are a Divergent cycle recovery.) - scope: Option>, } impl<'db> ExpressionInference<'db> { @@ -672,9 +658,8 @@ impl<'db> ExpressionInference<'db> { let _ = scope; Self { extra: Some(Box::new(ExpressionInferenceExtra { - cycle_recovery: Some(CycleRecovery::Divergent), + cycle_recovery: Some(CycleRecovery::Divergent(scope)), all_definitely_bound: true, - scope: Some(scope), ..ExpressionInferenceExtra::default() })), expressions: FxHashMap::default(), @@ -700,12 +685,9 @@ impl<'db> ExpressionInference<'db> { } fn fallback_type(&self) -> Option> { - self.extra.as_ref().and_then(|extra| { - extra.cycle_recovery.map(|recovery| { - recovery - .fallback_type(|| extra.scope.expect("Divergent inference should have scope")) - }) - }) + self.extra + .as_ref() + .and_then(|extra| extra.cycle_recovery.map(CycleRecovery::fallback_type)) } /// Returns true if all places in this expression are definitely bound. diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index b1e6fd3b4f3af..1264aa21fdf0d 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -252,7 +252,7 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> { undecorated_type: Option>, /// Did we merge in a sub-region with a cycle-recovery fallback, and if so, what kind? - cycle_recovery: Option, + cycle_recovery: Option>, /// `true` if all places in this expression are definitely bound all_definitely_bound: bool, @@ -293,7 +293,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - fn extend_cycle_recovery(&mut self, other_recovery: Option) { + fn extend_cycle_recovery(&mut self, other_recovery: Option>) { match &mut self.cycle_recovery { Some(recovery) => *recovery = recovery.merge(other_recovery), recovery @ None => *recovery = other_recovery, @@ -301,8 +301,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn fallback_type(&self) -> Option> { - self.cycle_recovery - .map(|recovery| recovery.fallback_type(|| self.scope)) + self.cycle_recovery.map(CycleRecovery::fallback_type) } fn extend_definition(&mut self, inference: &DefinitionInference<'db>) { @@ -8866,7 +8865,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { diagnostics, cycle_recovery, all_definitely_bound, - scope: Some(scope), }) }); @@ -8915,7 +8913,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { deferred: deferred.into_boxed_slice(), diagnostics, undecorated_type, - scope: Some(scope), }) }); @@ -8981,7 +8978,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Box::new(ScopeInferenceExtra { cycle_recovery, diagnostics, - scope: Some(scope), }) });