From 175d067250e0f906d60043529b690d4663fc609a Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 12 Sep 2024 14:15:25 -0400 Subject: [PATCH] [red-knot] add initial Type::is_equivalent_to and Type::is_assignable_to (#13332) These are quite incomplete, but I needed to start stubbing them out in order to build and test declared-types. Allowing unused for now, until they are used later in the declared-types PR. --------- Co-authored-by: Alex Waygood --- Cargo.lock | 1 + crates/red_knot_python_semantic/Cargo.toml | 1 + crates/red_knot_python_semantic/src/types.rs | 151 +++++++++++++++++++ 3 files changed, 153 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 0b513d1e4f25c..c125563ae52fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1936,6 +1936,7 @@ dependencies = [ "smallvec", "static_assertions", "tempfile", + "test-case", "thiserror", "tracing", "walkdir", diff --git a/crates/red_knot_python_semantic/Cargo.toml b/crates/red_knot_python_semantic/Cargo.toml index d0619955434ac..862f6f268967e 100644 --- a/crates/red_knot_python_semantic/Cargo.toml +++ b/crates/red_knot_python_semantic/Cargo.toml @@ -33,6 +33,7 @@ rustc-hash = { workspace = true } hashbrown = { workspace = true } smallvec = { workspace = true } static_assertions = { workspace = true } +test-case = { workspace = true } [build-dependencies] path-slash = { workspace = true } diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 093dd205ecb0d..d37b3c9ce7b08 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2,6 +2,7 @@ use infer::TypeInferenceBuilder; use ruff_db::files::File; use ruff_python_ast as ast; +use crate::module_resolver::file_to_module; use crate::semantic_index::ast_ids::HasScopedAstId; use crate::semantic_index::definition::{Definition, DefinitionKind}; use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId}; @@ -296,6 +297,46 @@ impl<'db> Type<'db> { } } + /// Return true if this type is [assignable to] type `target`. + /// + /// [assignable to]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation + #[allow(unused)] + pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool { + if self.is_equivalent_to(db, target) { + return true; + } + match (self, target) { + (Type::Unknown | Type::Any | Type::Never, _) => true, + (_, Type::Unknown | Type::Any) => true, + (Type::IntLiteral(_), Type::Instance(class)) + if class.is_stdlib_symbol(db, "builtins", "int") => + { + true + } + (Type::StringLiteral(_), Type::LiteralString) => true, + (Type::StringLiteral(_) | Type::LiteralString, Type::Instance(class)) + if class.is_stdlib_symbol(db, "builtins", "str") => + { + true + } + (Type::BytesLiteral(_), Type::Instance(class)) + if class.is_stdlib_symbol(db, "builtins", "bytes") => + { + true + } + // TODO + _ => false, + } + } + + /// Return true if this type is equivalent to type `other`. + #[allow(unused)] + pub(crate) fn is_equivalent_to(self, _db: &'db dyn Db, other: Type<'db>) -> bool { + // TODO equivalent but not identical structural types, differently-ordered unions and + // intersections, other cases? + self == other + } + /// Resolve a member access of a type. /// /// For example, if `foo` is `Type::Instance()`, @@ -588,6 +629,15 @@ pub struct ClassType<'db> { } impl<'db> ClassType<'db> { + /// Return true if this class is a standard library type with given module name and name. + #[allow(unused)] + pub(crate) fn is_stdlib_symbol(self, db: &'db dyn Db, module_name: &str, name: &str) -> bool { + name == self.name(db) + && file_to_module(db, self.body_scope(db).file(db)).is_some_and(|module| { + module.search_path().is_standard_library() && module.name() == module_name + }) + } + /// Return an iterator over the types of this class's bases. /// /// # Panics: @@ -702,3 +752,104 @@ pub struct TupleType<'db> { #[return_ref] elements: Box<[Type<'db>]>, } + +#[cfg(test)] +mod tests { + use super::{builtins_symbol_ty, BytesLiteralType, StringLiteralType, Type, UnionType}; + use crate::db::tests::TestDb; + use crate::program::{Program, SearchPathSettings}; + use crate::python_version::PythonVersion; + use crate::ProgramSettings; + use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; + use test_case::test_case; + + fn setup_db() -> TestDb { + let db = TestDb::new(); + + let src_root = SystemPathBuf::from("/src"); + db.memory_file_system() + .create_directory_all(&src_root) + .unwrap(); + + Program::from_settings( + &db, + &ProgramSettings { + target_version: PythonVersion::default(), + search_paths: SearchPathSettings::new(src_root), + }, + ) + .expect("Valid search path settings"); + + db + } + + /// A test representation of a type that can be transformed unambiguously into a real Type, + /// given a db. + #[derive(Debug)] + enum Ty { + Never, + Unknown, + Any, + IntLiteral(i64), + StringLiteral(&'static str), + LiteralString, + BytesLiteral(&'static str), + BuiltinInstance(&'static str), + Union(Vec), + } + + impl Ty { + fn into_type(self, db: &TestDb) -> Type<'_> { + match self { + Ty::Never => Type::Never, + Ty::Unknown => Type::Unknown, + Ty::Any => Type::Any, + Ty::IntLiteral(n) => Type::IntLiteral(n), + Ty::StringLiteral(s) => { + Type::StringLiteral(StringLiteralType::new(db, (*s).into())) + } + Ty::LiteralString => Type::LiteralString, + Ty::BytesLiteral(s) => { + Type::BytesLiteral(BytesLiteralType::new(db, s.as_bytes().into())) + } + Ty::BuiltinInstance(s) => builtins_symbol_ty(db, s).to_instance(db), + Ty::Union(tys) => { + UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db))) + } + } + } + } + + #[test_case(Ty::Unknown, Ty::IntLiteral(1))] + #[test_case(Ty::Any, Ty::IntLiteral(1))] + #[test_case(Ty::Never, Ty::IntLiteral(1))] + #[test_case(Ty::IntLiteral(1), Ty::Unknown)] + #[test_case(Ty::IntLiteral(1), Ty::Any)] + #[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("int"))] + #[test_case(Ty::StringLiteral("foo"), Ty::BuiltinInstance("str"))] + #[test_case(Ty::StringLiteral("foo"), Ty::LiteralString)] + #[test_case(Ty::LiteralString, Ty::BuiltinInstance("str"))] + #[test_case(Ty::BytesLiteral("foo"), Ty::BuiltinInstance("bytes"))] + fn is_assignable_to(from: Ty, to: Ty) { + let db = setup_db(); + assert!(from.into_type(&db).is_assignable_to(&db, to.into_type(&db))); + } + + #[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("str"))] + #[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str"))] + #[test_case(Ty::BuiltinInstance("int"), Ty::IntLiteral(1))] + fn is_not_assignable_to(from: Ty, to: Ty) { + let db = setup_db(); + assert!(!from.into_type(&db).is_assignable_to(&db, to.into_type(&db))); + } + + #[test_case( + Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), + Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]) + )] + fn is_equivalent_to(from: Ty, to: Ty) { + let db = setup_db(); + + assert!(from.into_type(&db).is_equivalent_to(&db, to.into_type(&db))); + } +}