diff --git a/typechecker/src/checker.rs b/typechecker/src/checker.rs index a1c64e39..ea0dcacb 100644 --- a/typechecker/src/checker.rs +++ b/typechecker/src/checker.rs @@ -688,4 +688,8 @@ mod tests { annotations_coroutine, "test_data/inputs/conformance_tests/annotations_coroutine.py" ); + type_eval_test!( + specialtypes_none, + "test_data/inputs/conformance_tests/specialtypes_none.py" + ); } diff --git a/typechecker/src/type_evaluator.rs b/typechecker/src/type_evaluator.rs index f90bcafa..bc87a736 100755 --- a/typechecker/src/type_evaluator.rs +++ b/typechecker/src/type_evaluator.rs @@ -166,6 +166,7 @@ impl<'a> TypeEvaluator<'a> { c.details.clone(), vec![final_elm_type], true, + vec![], ))) } ast::Expression::Tuple(t) => { @@ -181,6 +182,7 @@ impl<'a> TypeEvaluator<'a> { c.details.clone(), vec![elm_type], true, + vec![], ))) } ast::Expression::Dict(d) => { @@ -196,6 +198,7 @@ impl<'a> TypeEvaluator<'a> { c.details.clone(), vec![key_type, value_type], true, + vec![], ))) } ast::Expression::Set(s) => { @@ -211,6 +214,7 @@ impl<'a> TypeEvaluator<'a> { class_type, vec![elm_type], true, + vec![], ))) } ast::Expression::BoolOp(_) => Ok(self.get_builtin_type("bool").expect("typeshed")), @@ -312,16 +316,43 @@ impl<'a> TypeEvaluator<'a> { } }; match value_type { - PythonType::Class(c) => Ok(self - .lookup_on_class(symbol_table, &c, &a.attr) - .expect("attribute not found on type")), + PythonType::Class(ref c) => { + let attribute_on_c = self.lookup_on_class(symbol_table, &c, &a.attr); + if let Some(attribute_on_c) = attribute_on_c { + Ok(attribute_on_c) + } else { + let bases = self.get_base_classes(value_type); + for base in bases { + let base_class = base.expect_class(); + let attribute_on_base = + self.lookup_on_class(symbol_table, &base_class, &a.attr); + if let Some(attribute_on_base) = attribute_on_base { + return Ok(attribute_on_base); + } + } + + Ok(PythonType::Unknown) + } + } PythonType::Module(module) => { log::debug!("module: {:?}", module); let module_sym_table = self.imported_symbol_tables.get(&module.module_id).unwrap(); self.get_name_type(&a.attr, None, &module_sym_table, Some(0)) } - _ => Ok(PythonType::Unknown), + _ => { + let bases = self.get_base_classes(value_type); + for base in bases { + let base_class = base.expect_class(); + let attribute_on_base = + self.lookup_on_class(symbol_table, &base_class, &a.attr); + if let Some(attribute_on_base) = attribute_on_base { + return Ok(attribute_on_base); + } + } + + Ok(PythonType::Unknown) + } } } ast::Expression::BinOp(b) => Ok(self.bin_op_result_type( @@ -447,6 +478,7 @@ impl<'a> TypeEvaluator<'a> { class_type.details.clone(), type_parameters, true, + vec![], )) } // Illegal type annotation? Trying to subscript a non class type @@ -547,6 +579,7 @@ impl<'a> TypeEvaluator<'a> { class_symbol, vec![], false, + vec![], ))) } else { Ok(var_type) @@ -739,11 +772,14 @@ impl<'a> TypeEvaluator<'a> { }; let mut class_def_type_parameters = vec![]; + let mut base_classes = vec![]; for base in bases { let base_type = self.get_type(base, Some(symbol_table), None); let Ok(PythonType::Class(c)) = base_type else { continue; }; + // TODO: Maybe unnecessary clone + base_classes.push(c.clone()); let Some(possible_type_parameter) = base.as_subscript() else { class_def_type_parameters.extend(c.type_parameters); continue; @@ -794,6 +830,7 @@ impl<'a> TypeEvaluator<'a> { class_symbol.clone(), class_def_type_parameters, false, + base_classes, ))) } } @@ -1168,4 +1205,19 @@ impl<'a> TypeEvaluator<'a> { true, ))) } + + fn get_base_classes(&self, python_type: PythonType) -> Vec { + let mut super_classes = vec![]; + if let PythonType::Class(c) = python_type { + super_classes.push(PythonType::Class(c.clone())); + for super_class in &c.base_classes { + super_classes.push(PythonType::Class(super_class.clone())); + } + } + let object_class = self.get_builtin_type("object").expect("object not found"); + + super_classes.push(object_class.clone()); + + super_classes + } } diff --git a/typechecker/src/types.rs b/typechecker/src/types.rs index 59a223f4..29b1c138 100644 --- a/typechecker/src/types.rs +++ b/typechecker/src/types.rs @@ -71,6 +71,9 @@ pub enum TypeFlags { Instantiable, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NoneType {} + #[derive(Debug, Clone, PartialEq)] pub struct Any { pub source: AnySource, @@ -144,6 +147,8 @@ pub struct ClassType { // 1. If the type is inferred from annotation of a parameter or variable that is an instance // 2. If the type is inferred from a class node then it's an instance pub is_instance: bool, + // What types are allowed as base classes? + pub base_classes: Vec, } impl ClassType { @@ -151,11 +156,13 @@ impl ClassType { details: symbol_table::Class, type_parameters: Vec, is_instance: bool, + base_classes: Vec, ) -> Self { Self { details, type_parameters, is_instance, + base_classes, } } diff --git a/typechecker/test_data/inputs/conformance_tests/specialtypes_none.py b/typechecker/test_data/inputs/conformance_tests/specialtypes_none.py new file mode 100644 index 00000000..ac4dbffa --- /dev/null +++ b/typechecker/test_data/inputs/conformance_tests/specialtypes_none.py @@ -0,0 +1,41 @@ +""" +Tests the handling of builtins.None in a type annotation. +""" + +# Specification: https://typing.readthedocs.io/en/latest/spec/special-types.html#none + +from types import NoneType +from typing import Hashable, Iterable, assert_type + + +# > When used in a type hint, the expression None is considered equivalent to type(None). + + +def func1(val1: None) -> None: + assert_type(val1, None) + t1: None = None + return None # OK + + +func1(None) # OK +func1(type(None)) # E + +# None is hashable +none1: Hashable = None # OK + +# None is not iterable +none2: Iterable = None # E: not iterable + + +None.__class__ # OK +None.__doc__ # OK +None.__eq__(0) # OK + + +def func2(val1: type[None]): + assert_type(val1, type[None]) + + +func2(None.__class__) # OK +func2(type(None)) # OK +func2(None) # E: not compatible diff --git a/typechecker/test_data/output/enderpy_python_type_checker__checker__tests__specialtypes_none.snap b/typechecker/test_data/output/enderpy_python_type_checker__checker__tests__specialtypes_none.snap new file mode 100644 index 00000000..b012f1c9 --- /dev/null +++ b/typechecker/test_data/output/enderpy_python_type_checker__checker__tests__specialtypes_none.snap @@ -0,0 +1,162 @@ +--- +source: typechecker/src/checker.rs +description: "1: \"\"\"\n2: Tests the handling of builtins.None in a type annotation.\n3: \"\"\"\n4: \n5: # Specification: https://typing.readthedocs.io/en/latest/spec/special-types.html#none\n6: \n7: from types import NoneType\n8: from typing import Hashable, Iterable, assert_type\n9: \n10: \n11: # > When used in a type hint, the expression None is considered equivalent to type(None).\n12: \n13: \n14: def func1(val1: None) -> None:\n15: assert_type(val1, None)\n16: t1: None = None\n17: return None # OK\n18: \n19: \n20: func1(None) # OK\n21: func1(type(None)) # E\n22: \n23: # None is hashable\n24: none1: Hashable = None # OK\n25: \n26: # None is not iterable\n27: none2: Iterable = None # E: not iterable\n28: \n29: \n30: None.__class__ # OK\n31: None.__doc__ # OK\n32: None.__eq__(0) # OK\n33: \n34: \n35: def func2(val1: type[None]):\n36: assert_type(val1, type[None])\n37: \n38: \n39: func2(None.__class__) # OK\n40: func2(type(None)) # OK\n41: func2(None) # E: not compatible\n" +expression: result +--- +Line 1: """ +Tests the handling of builtins.None in a type annotation. +""" + +Expr types in the line --->: + """ +Tests the handling of builtins.None in a type annotation. +""" => (class) str + +--- +Line 5: from types import NoneType + +Expr types in the line --->: + types => Module + NoneType => (class) NoneType + +--- +Line 6: from typing import Hashable, Iterable, assert_type + +Expr types in the line --->: + typing => Module + Hashable => (class) Hashable + Iterable => (class) typing.Iterable[TypeVar[_T_co, ]] + assert_type => (function) Callable[[TypeVar[_T, ], (class) object], TypeVar[_T, ]] + +--- +Line 12: def func1(val1: None) -> None: + +Expr types in the line --->: + func1 => (function) Callable[[None], None] + val1: None => None + None => None + None => None + +--- +Line 13: assert_type(val1, None) + +Expr types in the line --->: + assert_type => (function) Callable[[TypeVar[_T, ], (class) object], TypeVar[_T, ]] + assert_type(val1, None) => TypeVar[_T, ] + val1 => None + None => None + +--- +Line 14: t1: None = None + +Expr types in the line --->: + t1 => None + None => None + +--- +Line 15: return None # OK + +Expr types in the line --->: + None => None + +--- +Line 18: func1(None) # OK + +Expr types in the line --->: + func1 => (function) Callable[[None], None] + func1(None) => None + None => None + +--- +Line 19: func1(type(None)) # E + +Expr types in the line --->: + func1 => (function) Callable[[None], None] + func1(type(None)) => None + type => (class) type + type(None) => (class) type + None => None + +--- +Line 22: none1: Hashable = None # OK + +Expr types in the line --->: + none1 => (class) Hashable + None => None + +--- +Line 25: none2: Iterable = None # E: not iterable + +Expr types in the line --->: + none2 => (class) typing.Iterable[TypeVar[_T_co, ]] + None => None + +--- +Line 28: None.__class__ # OK + +Expr types in the line --->: + None => None + None.__class__ => (function) Callable[[Unknown], (class) builtins.type[(class) Self]] + +--- +Line 29: None.__doc__ # OK + +Expr types in the line --->: + None => None + None.__doc__ => Union[(class) str, None] + +--- +Line 30: None.__eq__(0) # OK + +Expr types in the line --->: + None.__eq__ => (function) Callable[[Unknown, (class) object], (class) bool] + None.__eq__(0) => (class) bool + 0 => (class) int + +--- +Line 33: def func2(val1: type[None]): + +Expr types in the line --->: + func2 => (function) Callable[[(class) builtins.type[None]], Unknown] + val1: type[None] => (class) builtins.type[None] + type[None] => (class) type + +--- +Line 34: assert_type(val1, type[None]) + +Expr types in the line --->: + assert_type => (function) Callable[[TypeVar[_T, ], (class) object], TypeVar[_T, ]] + assert_type(val1, type[None]) => TypeVar[_T, ] + val1 => (class) builtins.type[None] + type => (class) type + type[None] => (class) type + None => None + +--- +Line 37: func2(None.__class__) # OK + +Expr types in the line --->: + func2 => (function) Callable[[(class) builtins.type[None]], Unknown] + func2(None.__class__) => Unknown + None => None + None.__class__ => (function) Callable[[Unknown], (class) builtins.type[(class) Self]] + +--- +Line 38: func2(type(None)) # OK + +Expr types in the line --->: + func2 => (function) Callable[[(class) builtins.type[None]], Unknown] + func2(type(None)) => Unknown + type => (class) type + type(None) => (class) type + None => None + +--- +Line 39: func2(None) # E: not compatible + +Expr types in the line --->: + func2 => (function) Callable[[(class) builtins.type[None]], Unknown] + func2(None) => Unknown + None => None + +---