Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/unknown kinds #13500

Closed
wants to merge 8 commits into from
201 changes: 153 additions & 48 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ fn symbol_ty_by_id<'db>(db: &'db dyn Db, scope: ScopeId<'db>, symbol: ScopedSymb
use_def.public_bindings(symbol),
use_def
.public_may_be_unbound(symbol)
.then_some(Type::Unknown),
.then_some(Type::Unknown(UnknownTypeKind::TypeError)),
))
} else {
None
Expand Down Expand Up @@ -239,7 +239,7 @@ pub enum Type<'db> {
Never,
/// unknown type (either no annotation, or some kind of type error)
/// equivalent to Any, or possibly to object in strict mode
Unknown,
Unknown(UnknownTypeKind),
/// name does not exist or is not bound to any value (this represents an error, but with some
/// leniency options it could be silently resolved to Unknown in some cases)
Unbound,
Expand Down Expand Up @@ -369,14 +369,24 @@ impl<'db> Type<'db> {
}
}

/// Apply a function to the type (if it is a non-union); or,
/// if it is a union, apply the function to each element of the union
/// and build a new union from the result
#[must_use]
pub fn replace_unbound_with(&self, db: &'db dyn Db, replacement: Type<'db>) -> Type<'db> {
pub fn recursive_transform(
self,
db: &'db dyn Db,
mut transform_fn: impl FnMut(Type<'db>) -> Type<'db>,
) -> Self {
match self {
Type::Unbound => replacement,
Type::Union(union) => {
union.map(db, |element| element.replace_unbound_with(db, replacement))
}
ty => *ty,
Type::Union(union) => union
.elements(db)
.iter()
.fold(UnionBuilder::new(db), |builder, ty| {
builder.add(transform_fn(*ty))
})
.build(),
ty => transform_fn(ty),
}
}

Expand All @@ -398,8 +408,8 @@ impl<'db> Type<'db> {
return true;
}
match (self, target) {
(Type::Unknown | Type::Any, _) => false,
(_, Type::Unknown | Type::Any) => false,
(Type::Unknown(_) | Type::Any, _) => false,
(_, Type::Unknown(_) | Type::Any) => false,
(Type::Never, _) => true,
(_, Type::Never) => false,
(Type::IntLiteral(_), Type::Instance(class))
Expand Down Expand Up @@ -434,8 +444,8 @@ impl<'db> Type<'db> {
/// [assignable to]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
match (self, target) {
(Type::Unknown | Type::Any, _) => true,
(_, Type::Unknown | Type::Any) => true,
(Type::Unknown(_) | Type::Any, _) => true,
(_, Type::Unknown(_) | Type::Any) => true,
(ty, Type::Union(union)) => union
.elements(db)
.iter()
Expand Down Expand Up @@ -471,52 +481,52 @@ impl<'db> Type<'db> {
Type::Any => Type::Any,
Type::Never => {
// TODO: attribute lookup on Never type
Type::Unknown
Type::Unknown(UnknownTypeKind::RedKnotLimitation)
}
Type::Unknown => Type::Unknown,
Type::Unknown(kind) => Type::Unknown(*kind),
Type::Unbound => Type::Unbound,
Type::None => {
// TODO: attribute lookup on None type
Type::Unknown
Type::Unknown(UnknownTypeKind::RedKnotLimitation)
}
Type::Function(_) | Type::RevealTypeFunction(_) => {
// TODO: attribute lookup on function type
Type::Unknown
Type::Unknown(UnknownTypeKind::RedKnotLimitation)
}
Type::Module(file) => global_symbol_ty(db, *file, name),
Type::Class(class) => class.class_member(db, name),
Type::Instance(_) => {
// TODO MRO? get_own_instance_member, get_instance_member
Type::Unknown
Type::Unknown(UnknownTypeKind::RedKnotLimitation)
}
Type::Union(union) => union.map(db, |element| element.member(db, name)),
Type::Intersection(_) => {
// TODO perform the get_member on each type in the intersection
// TODO return the intersection of those results
Type::Unknown
Type::Unknown(UnknownTypeKind::RedKnotLimitation)
}
Type::IntLiteral(_) => {
// TODO raise error
Type::Unknown
Type::Unknown(UnknownTypeKind::RedKnotLimitation)
}
Type::BooleanLiteral(_) => Type::Unknown,
Type::BooleanLiteral(_) => Type::Unknown(UnknownTypeKind::RedKnotLimitation),
Type::StringLiteral(_) => {
// TODO defer to `typing.LiteralString`/`builtins.str` methods
// from typeshed's stubs
Type::Unknown
Type::Unknown(UnknownTypeKind::RedKnotLimitation)
}
Type::LiteralString => {
// TODO defer to `typing.LiteralString`/`builtins.str` methods
// from typeshed's stubs
Type::Unknown
Type::Unknown(UnknownTypeKind::RedKnotLimitation)
}
Type::BytesLiteral(_) => {
// TODO defer to Type::Instance(<bytes from typeshed>).member
Type::Unknown
Type::Unknown(UnknownTypeKind::RedKnotLimitation)
}
Type::Tuple(_) => {
// TODO: implement tuple methods
Type::Unknown
Type::Unknown(UnknownTypeKind::RedKnotLimitation)
}
}
}
Expand All @@ -531,19 +541,23 @@ impl<'db> Type<'db> {
Type::Function(function_type) => CallOutcome::callable(function_type.return_type(db)),
Type::RevealTypeFunction(function_type) => CallOutcome::revealed(
function_type.return_type(db),
*arg_types.first().unwrap_or(&Type::Unknown),
*arg_types
.first()
.unwrap_or(&Type::Unknown(UnknownTypeKind::TypeError)),
),

// TODO annotated return type on `__new__` or metaclass `__call__`
Type::Class(class) => CallOutcome::callable(Type::Instance(class)),

// TODO: handle classes which implement the `__call__` protocol
Type::Instance(_instance_ty) => CallOutcome::callable(Type::Unknown),
Type::Instance(_instance_ty) => {
CallOutcome::callable(Type::Unknown(UnknownTypeKind::RedKnotLimitation))
}

// `Any` is callable, and its return type is also `Any`.
Type::Any => CallOutcome::callable(Type::Any),

Type::Unknown => CallOutcome::callable(Type::Unknown),
Type::Unknown(kind) => CallOutcome::callable(Type::Unknown(kind)),

Type::Union(union) => CallOutcome::union(
self,
Expand All @@ -555,7 +569,9 @@ impl<'db> Type<'db> {
),

// TODO: intersection types
Type::Intersection(_) => CallOutcome::callable(Type::Unknown),
Type::Intersection(_) => {
CallOutcome::callable(Type::Unknown(UnknownTypeKind::RedKnotLimitation))
}

_ => CallOutcome::not_callable(self),
}
Expand Down Expand Up @@ -622,13 +638,13 @@ impl<'db> Type<'db> {
pub fn to_instance(&self, db: &'db dyn Db) -> Type<'db> {
match self {
Type::Any => Type::Any,
Type::Unknown => Type::Unknown,
Type::Unbound => Type::Unknown,
Type::Unknown(kind) => Type::Unknown(*kind),
Type::Unbound => Type::Unknown(UnknownTypeKind::TypeError),
Type::Never => Type::Never,
Type::Class(class) => Type::Instance(*class),
Type::Union(union) => union.map(db, |element| element.to_instance(db)),
// TODO: we can probably do better here: --Alex
Type::Intersection(_) => Type::Unknown,
Type::Intersection(_) => Type::Unknown(UnknownTypeKind::RedKnotLimitation),
// TODO: calling `.to_instance()` on any of these should result in a diagnostic,
// since they already indicate that the object is an instance of some kind:
Type::BooleanLiteral(_)
Expand All @@ -641,7 +657,7 @@ impl<'db> Type<'db> {
| Type::StringLiteral(_)
| Type::Tuple(_)
| Type::LiteralString
| Type::None => Type::Unknown,
| Type::None => Type::Unknown(UnknownTypeKind::TypeError),
}
}

Expand All @@ -667,9 +683,9 @@ impl<'db> Type<'db> {
// TODO: `type[Any]`?
Type::Any => Type::Any,
// TODO: `type[Unknown]`?
Type::Unknown => Type::Unknown,
Type::Unknown(_) => Type::Unknown(UnknownTypeKind::RedKnotLimitation),
// TODO intersections
Type::Intersection(_) => Type::Unknown,
Type::Intersection(_) => Type::Unknown(UnknownTypeKind::RedKnotLimitation),
Type::Tuple(_) => builtins_symbol_ty(db, "tuple"),
}
}
Expand Down Expand Up @@ -750,7 +766,9 @@ impl<'db> CallOutcome<'db> {
match (acc, ty) {
(None, None) => None,
(None, Some(ty)) => Some(UnionBuilder::new(db).add(ty)),
(Some(builder), ty) => Some(builder.add(ty.unwrap_or(Type::Unknown))),
(Some(builder), ty) => Some(
builder.add(ty.unwrap_or(Type::Unknown(UnknownTypeKind::TypeError))),
),
}
})
.map(UnionBuilder::build),
Expand Down Expand Up @@ -786,7 +804,7 @@ impl<'db> CallOutcome<'db> {
not_callable_ty.display(db)
),
);
Type::Unknown
Type::Unknown(UnknownTypeKind::TypeError)
}
Self::Union {
outcomes,
Expand All @@ -799,7 +817,7 @@ impl<'db> CallOutcome<'db> {
let return_ty = match outcome {
Self::NotCallable { not_callable_ty } => {
not_callable.push(*not_callable_ty);
Type::Unknown
Type::Unknown(UnknownTypeKind::TypeError)
}
Self::RevealType {
return_ty,
Expand Down Expand Up @@ -867,12 +885,76 @@ impl<'db> IterationOutcome<'db> {
Self::Iterable { element_ty } => element_ty,
Self::NotIterable { not_iterable_ty } => {
inference_builder.not_iterable_diagnostic(iterable_node, not_iterable_ty);
Type::Unknown
Type::Unknown(UnknownTypeKind::TypeError)
}
}
}
}

// When it really matters to distinguish between different kinds of `Unknown` types
// (see `PartialEq` implementation), we use the representation as `u8`
#[derive(Debug, Clone, Copy, Eq)]
#[repr(u8)]
pub enum UnknownTypeKind {
/// Temporary variant that indicates that we *should*
/// be able to infer a type here in due course, but currently can't
RedKnotLimitation,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I like the name NotImplemented here


/// Invalid syntax in the node means we can't infer a type here
InvalidSyntax,

/// An `Unknown` type stemming from some kind of type error.
///
/// Examples:
/// - A function was called with argument(s) of incorrect type(s), leading
/// to a diagnostic being emitted and the expression being evaluated as `Unknown`.
/// - An expression was deemed to take place between two invalid types, e.g. `1 + "foo"`.
/// (This is really the same as the first example, since expressions ultimately desguar
/// to function calls, e.g. `1 + "foo"` desugars approximately to `type(1).__add__("foo")`.)
/// - An attribute or method was looked up on an instance of a type that doesn't have that
/// attribute or method.
TypeError,

/// The symbol was unannotated and the true type can't be reasonably inferred
UnannotatedSymbol,
Comment on lines +917 to +919
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure that this case exists, and I think we should limit the cases to those we actually use, not add more speculatively.


/// The type of symbols imported by imports that could not be resolved
UnresolvedImport,

/// A "second-order" Unknown type, that results from an interaction with
/// a "first-order" Unknown type. For example, if the type of `x` is `Unknown`,
/// the type of `x + 1` will also be `Unknown`
SecondOrder,
Comment on lines +924 to +927
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm also not convinced we need this kind; I think I'd rather propagate the original unknown kind in the x + 1 example.


/// Special kind for intersections that is equal to all other kinds. This allows to consider a
/// single `Type::Unknown(_)` in intersections. Should not be used in other contexts.
AllKinds,
Comment on lines +929 to +931
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm also not convinced we should have this; I think I'd prefer to just not simplify differing unknowns in intersections. I don't expect this case to arise often, because most intersections will come from narrowing constraints, and in most cases we'll just not return a narrowing constraint at all rather than return one with an unknown type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To be clear, does that mean that having potentially different unknown kinds inside an intersection is not a concern?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I don't think that's a problem.

}

impl PartialEq for UnknownTypeKind {
fn eq(&self, other: &Self) -> bool {
// The special kind `AllKinds` is equal to all other kinds
match (self, other) {
(UnknownTypeKind::AllKinds, _) | (_, UnknownTypeKind::AllKinds) => true,
_ => *self as u8 == *other as u8,
}
}
}

impl std::hash::Hash for UnknownTypeKind {
// Deriving `Hash` with an explicit implementation of `PartialEq` is flagged by clippy
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
(*self as u8).hash(state);
}
}

impl UnknownTypeKind {
/// Method to aggregate multiple `UnknownTypeKind`s when they interact in any context
pub(crate) fn union(self, _other: Self) -> Self {
Copy link
Contributor

@carljm carljm Sep 25, 2024

Choose a reason for hiding this comment

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

I think the kind of an unknown should just always be a bitflag, and then the behavior of union is clear. (It doesn't make sense to me to have an enum with a special variant which is a bitflag of other variants; that seems like complexity that doesn't buy anything useful.)

self
}
}

#[salsa::interned]
pub struct FunctionType<'db> {
/// name of the function at definition
Expand Down Expand Up @@ -914,13 +996,12 @@ impl<'db> FunctionType<'db> {
panic!("Function type definition must have `DefinitionKind::Function`")
};

// TODO if a function `bar` is decorated by `foo`,
// where `foo` is annotated as returning a type `X` that is a subtype of `Callable`,
// we need to infer the return type from `X`'s return annotation
// rather than from `bar`'s return annotation
// in order to determine the type that `bar` returns
// TODO if a function `bar` is decorated by `foo`, where `foo` is annotated as returning a
// type `X` that is a subtype of `Callable`, we need to infer the return type from `X`'s
// return annotation rather than from `bar`'s return annotation in order to determine the
// type that `bar` returns
if !function_stmt_node.decorator_list.is_empty() {
return Type::Unknown;
return Type::Unknown(UnknownTypeKind::RedKnotLimitation);
}

function_stmt_node
Expand All @@ -929,12 +1010,13 @@ impl<'db> FunctionType<'db> {
.map(|returns| {
if function_stmt_node.is_async {
// TODO: generic `types.CoroutineType`!
Type::Unknown
Type::Unknown(UnknownTypeKind::RedKnotLimitation)
} else {
definition_expression_ty(db, definition, returns.as_ref())
}
})
.unwrap_or(Type::Unknown)
// TODO: can't we figure out the return type from the function body?
.unwrap_or(Type::Unknown(UnknownTypeKind::RedKnotLimitation))
}
}

Expand Down Expand Up @@ -1075,7 +1157,9 @@ pub struct TupleType<'db> {

#[cfg(test)]
mod tests {
use super::{builtins_symbol_ty, BytesLiteralType, StringLiteralType, Type, UnionType};
use super::{
builtins_symbol_ty, BytesLiteralType, StringLiteralType, Type, UnionType, UnknownTypeKind,
};
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
Expand Down Expand Up @@ -1122,7 +1206,7 @@ mod tests {
fn into_type(self, db: &TestDb) -> Type<'_> {
match self {
Ty::Never => Type::Never,
Ty::Unknown => Type::Unknown,
Ty::Unknown => Type::Unknown(super::UnknownTypeKind::TypeError),
Ty::Any => Type::Any,
Ty::IntLiteral(n) => Type::IntLiteral(n),
Ty::StringLiteral(s) => {
Expand Down Expand Up @@ -1205,4 +1289,25 @@ mod tests {

assert!(from.into_type(&db).is_equivalent_to(&db, to.into_type(&db)));
}

#[test]
fn unknown_of_different_kinds_are_equal_to_all_kinds() {
let kind1 = UnknownTypeKind::TypeError;
let kind2 = UnknownTypeKind::InvalidSyntax;
let all_kinds = UnknownTypeKind::AllKinds;
assert_ne!(kind1, kind2);
assert_eq!(kind1, all_kinds);
assert_eq!(kind2, all_kinds);
}

#[test]
fn unknown_union_does_not_change_same_kinds() {
let uk0 = UnknownTypeKind::TypeError;
let uk1 = UnknownTypeKind::InvalidSyntax;
assert_ne!(uk0, uk1);
assert_eq!(uk0.union(uk0), uk0);
assert_eq!(uk1.union(uk1), uk1);
// TODO: unit test the outcome of != kind of unions once the correct implementation is
// decided
}
}
Loading
Loading