Skip to content

Commit

Permalink
[red-knot] support empty TypeInference with fallback type (#16510)
Browse files Browse the repository at this point in the history
This is split out of #14029, to
reduce the size of that PR, and to validate that this "fallback type"
support in `TypeInference` doesn't come with a performance cost. It also
improves the reliability and debuggability of our current (temporary)
cycle handling.

In order to recover from a cycle, we have to be able to construct a
"default" `TypeInference` where all expressions and definitions have
some "default" type. In our current cycle handling, this "default" type
is just unknown or a todo type. With fixpoint iteration, the "default"
type will be `Type::Never`, which is the "bottom" type that fixpoint
iteration starts from.

Since it would be costly (both in space and time) to actually enumerate
all expressions and definitions in a scope, just to insert the same
default type for all of them, instead we add an optional "missing type"
fallback to `TypeInference`, which (if set) is the fallback type for any
expression or definition which doesn't have an explicit type set.

With this change, cycles can no longer result in the dreaded "Missing
key" errors looking up the type of some expression.
  • Loading branch information
carljm authored Mar 5, 2025
1 parent 318f503 commit 114abc7
Showing 1 changed file with 45 additions and 20 deletions.
65 changes: 45 additions & 20 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,27 +111,14 @@ pub(crate) fn infer_scope_types<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Ty
TypeInferenceBuilder::new(db, InferenceRegion::Scope(scope), index).finish()
}

/// Cycle recovery for [`infer_definition_types()`]: for now, just [`Type::unknown`]
/// TODO fixpoint iteration
/// TODO temporary cycle recovery for [`infer_definition_types()`], pending fixpoint iteration
fn infer_definition_types_cycle_recovery<'db>(
db: &'db dyn Db,
_cycle: &salsa::Cycle,
input: Definition<'db>,
) -> TypeInference<'db> {
tracing::trace!("infer_definition_types_cycle_recovery");
let mut inference = TypeInference::empty(input.scope(db));
let category = input.kind(db).category(input.file(db).is_stub(db.upcast()));
if category.is_declaration() {
inference
.declarations
.insert(input, TypeAndQualifiers::unknown());
}
if category.is_binding() {
inference.bindings.insert(input, Type::unknown());
}
// TODO we don't fill in expression types for the cycle-participant definitions, which can
// later cause a panic when looking up an expression type.
inference
TypeInference::cycle_fallback(input.scope(db), todo_type!("cycle recovery"))
}

/// Infer all types for a [`Definition`] (including sub-expressions).
Expand Down Expand Up @@ -291,8 +278,13 @@ pub(crate) struct TypeInference<'db> {
/// The diagnostics for this region.
diagnostics: TypeCheckDiagnostics,

/// The scope belong to this region.
/// The scope this region is part of.
scope: ScopeId<'db>,

/// The fallback type for missing expressions/bindings/declarations.
///
/// This is used only when constructing a cycle-recovery `TypeInference`.
cycle_fallback_type: Option<Type<'db>>,
}

impl<'db> TypeInference<'db> {
Expand All @@ -304,26 +296,59 @@ impl<'db> TypeInference<'db> {
deferred: FxHashSet::default(),
diagnostics: TypeCheckDiagnostics::default(),
scope,
cycle_fallback_type: None,
}
}

fn cycle_fallback(scope: ScopeId<'db>, cycle_fallback_type: Type<'db>) -> Self {
Self {
expressions: FxHashMap::default(),
bindings: FxHashMap::default(),
declarations: FxHashMap::default(),
deferred: FxHashSet::default(),
diagnostics: TypeCheckDiagnostics::default(),
scope,
cycle_fallback_type: Some(cycle_fallback_type),
}
}

#[track_caller]
pub(crate) fn expression_type(&self, expression: ScopedExpressionId) -> Type<'db> {
self.expressions[&expression]
self.try_expression_type(expression).expect(
"expression should belong to this TypeInference region and
TypeInferenceBuilder should have inferred a type for it",
)
}

pub(crate) fn try_expression_type(&self, expression: ScopedExpressionId) -> Option<Type<'db>> {
self.expressions.get(&expression).copied()
self.expressions
.get(&expression)
.copied()
.or(self.cycle_fallback_type)
}

#[track_caller]
pub(crate) fn binding_type(&self, definition: Definition<'db>) -> Type<'db> {
self.bindings[&definition]
self.bindings
.get(&definition)
.copied()
.or(self.cycle_fallback_type)
.expect(
"definition should belong to this TypeInference region and
TypeInferenceBuilder should have inferred a type for it",
)
}

#[track_caller]
pub(crate) fn declaration_type(&self, definition: Definition<'db>) -> TypeAndQualifiers<'db> {
self.declarations[&definition]
self.declarations
.get(&definition)
.copied()
.or(self.cycle_fallback_type.map(Into::into))
.expect(
"definition should belong to this TypeInference region and
TypeInferenceBuilder should have inferred a type for it",
)
}

pub(crate) fn diagnostics(&self) -> &TypeCheckDiagnostics {
Expand Down

0 comments on commit 114abc7

Please sign in to comment.