Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, 1)

reveal_type(C().x) # revealed: Unknown | tuple[Divergent, Literal[1]]
```

## References

Some of the tests in the *Class and instance variables* section draw inspiration from
Expand Down
5 changes: 4 additions & 1 deletion crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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,
Expand Down
127 changes: 81 additions & 46 deletions crates/ty_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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`].
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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<ExpressionInference<'db>> {
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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -378,34 +387,61 @@ impl<'db> InferenceRegion<'db> {
}
}

#[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(ScopeId<'db>),
}

impl<'db> CycleRecovery<'db> {
fn merge(self, other: Option<CycleRecovery<'db>>) -> Self {
if let Some(other) = other {
match (self, other) {
// 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 {
self
}
}

fn fallback_type(self) -> Type<'db> {
match self {
Self::Initial => Type::Never,
Self::Divergent(scope) => Type::divergent(scope),
}
}
}

/// The inferred types for a scope region.
#[derive(Debug, Eq, PartialEq, salsa::Update, get_size2::GetSize)]
pub(crate) struct ScopeInference<'db> {
/// The types of every expression in this region.
expressions: FxHashMap<ExpressionNodeKey, Type<'db>>,

/// The extra data that is only present for few inference regions.
extra: Option<Box<ScopeInferenceExtra>>,
extra: Option<Box<ScopeInferenceExtra<'db>>>,
}

#[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<CycleRecovery<'db>>,

/// The diagnostics for this region.
diagnostics: TypeCheckDiagnostics,
}

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(),
Expand All @@ -431,14 +467,10 @@ impl<'db> ScopeInference<'db> {
.or_else(|| self.fallback_type())
}

fn is_cycle_callback(&self) -> bool {
fn fallback_type(&self) -> Option<Type<'db>> {
self.extra
.as_ref()
.is_some_and(|extra| extra.cycle_fallback)
}

fn fallback_type(&self) -> Option<Type<'db>> {
self.is_cycle_callback().then_some(Type::Never)
.and_then(|extra| extra.cycle_recovery.map(CycleRecovery::fallback_type))
}
}

Expand Down Expand Up @@ -471,10 +503,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<CycleRecovery<'db>>,

/// The definitions that are deferred.
deferred: Box<[Definition<'db>]>,
Expand All @@ -487,7 +517,7 @@ struct DefinitionInferenceExtra<'db> {
}

impl<'db> DefinitionInference<'db> {
fn cycle_fallback(scope: ScopeId<'db>) -> Self {
fn cycle_initial(scope: ScopeId<'db>) -> Self {
let _ = scope;

Self {
Expand All @@ -497,7 +527,7 @@ impl<'db> DefinitionInference<'db> {
#[cfg(debug_assertions)]
scope,
extra: Some(Box::new(DefinitionInferenceExtra {
cycle_fallback: true,
cycle_recovery: Some(CycleRecovery::Initial),
..DefinitionInferenceExtra::default()
})),
}
Expand Down Expand Up @@ -566,14 +596,10 @@ impl<'db> DefinitionInference<'db> {
self.declarations.iter().map(|(_, qualifiers)| *qualifiers)
}

fn is_cycle_callback(&self) -> bool {
fn fallback_type(&self) -> Option<Type<'db>> {
self.extra
.as_ref()
.is_some_and(|extra| extra.cycle_fallback)
}

fn fallback_type(&self) -> Option<Type<'db>> {
self.is_cycle_callback().then_some(Type::Never)
.and_then(|extra| extra.cycle_recovery.map(CycleRecovery::fallback_type))
}

pub(crate) fn undecorated_type(&self) -> Option<Type<'db>> {
Expand Down Expand Up @@ -605,21 +631,34 @@ 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<CycleRecovery<'db>>,

/// `true` if all places in this expression are definitely bound
all_definitely_bound: bool,
}

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(scope)),
all_definitely_bound: true,
..ExpressionInferenceExtra::default()
})),
Expand All @@ -645,14 +684,10 @@ impl<'db> ExpressionInference<'db> {
.unwrap_or_else(Type::unknown)
}

fn is_cycle_callback(&self) -> bool {
fn fallback_type(&self) -> Option<Type<'db>> {
self.extra
.as_ref()
.is_some_and(|extra| extra.cycle_fallback)
}

fn fallback_type(&self) -> Option<Type<'db>> {
self.is_cycle_callback().then_some(Type::Never)
.and_then(|extra| extra.cycle_recovery.map(CycleRecovery::fallback_type))
}

/// Returns true if all places in this expression are definitely bound.
Expand Down
Loading