From b33cf5baba9e377c2c09a8eeca7a611cf75f44eb Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 9 Jan 2025 21:45:06 +0100 Subject: [PATCH] [red-knot] Move `UnionBuilder` tests to Markdown (#15374) ## Summary This moves almost all of our existing `UnionBuilder` tests to a Markdown-based test suite. I see how this could be a more controversial change, since these tests where written specifically for `UnionBuilder`, and by creating the union types using Python type expressions, we add an additional layer on top (parsing and inference of these expressions) that moves these tests away from clean unit tests more in the direction of integration tests. Also, there are probably a few implementation details of `UnionBuilder` hidden in the test assertions (e.g. order of union elements after simplifications). That said, I think we would like to see all those properties that are being tested here from *any* implementation of union types. And the Markdown tests come with the usual advantages: - More consice - Better readability - No re-compiliation when working on tests - Easier to add additional explanations and structure to the test suite This changeset adds a few additional tests, but keeps the logic of the existing tests except for a few minor modifications for consistency. --------- Co-authored-by: Alex Waygood Co-authored-by: T-256 <132141463+T-256@users.noreply.github.com> --- .../resources/mdtest/union_types.md | 125 ++++++++++++++++++ .../src/types/builder.rs | 103 +-------------- 2 files changed, 131 insertions(+), 97 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/union_types.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/union_types.md b/crates/red_knot_python_semantic/resources/mdtest/union_types.md new file mode 100644 index 0000000000000..b13dc527f9735 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/union_types.md @@ -0,0 +1,125 @@ +# Union types + +This test suite covers certain basic properties and simplification strategies for union types. + +## Basic unions + +```py +from typing import Literal + +def _(u1: int | str, u2: Literal[0] | Literal[1]) -> None: + reveal_type(u1) # revealed: int | str + reveal_type(u2) # revealed: Literal[0, 1] +``` + +## Duplicate elements are collapsed + +```py +def _(u1: int | int | str, u2: int | str | int) -> None: + reveal_type(u1) # revealed: int | str + reveal_type(u2) # revealed: int | str +``` + +## `Never` is removed + +`Never` is an empty set, a type with no inhabitants. Its presence in a union is always redundant, +and so we eagerly simplify it away. `NoReturn` is equivalent to `Never`. + +```py +from typing_extensions import Never, NoReturn + +def never(u1: int | Never, u2: int | Never | str) -> None: + reveal_type(u1) # revealed: int + reveal_type(u2) # revealed: int | str + +def noreturn(u1: int | NoReturn, u2: int | NoReturn | str) -> None: + reveal_type(u1) # revealed: int + reveal_type(u2) # revealed: int | str +``` + +## Flattening of nested unions + +```py +from typing import Literal + +def _( + u1: (int | str) | bytes, + u2: int | (str | bytes), + u3: int | (str | (bytes | complex)), +) -> None: + reveal_type(u1) # revealed: int | str | bytes + reveal_type(u2) # revealed: int | str | bytes + reveal_type(u3) # revealed: int | str | bytes | complex +``` + +## Simplification using subtyping + +The type `S | T` can be simplified to `T` if `S` is a subtype of `T`: + +```py +from typing_extensions import Literal, LiteralString + +def _( + u1: str | LiteralString, u2: LiteralString | str, u3: Literal["a"] | str | LiteralString, u4: str | bytes | LiteralString +) -> None: + reveal_type(u1) # revealed: str + reveal_type(u2) # revealed: str + reveal_type(u3) # revealed: str + reveal_type(u4) # revealed: str | bytes +``` + +## Boolean literals + +The union `Literal[True] | Literal[False]` is exactly equivalent to `bool`: + +```py +from typing import Literal + +def _( + u1: Literal[True, False], + u2: bool | Literal[True], + u3: Literal[True] | bool, + u4: Literal[True] | Literal[True, 17], + u5: Literal[True, False, True, 17], +) -> None: + reveal_type(u1) # revealed: bool + reveal_type(u2) # revealed: bool + reveal_type(u3) # revealed: bool + reveal_type(u4) # revealed: Literal[True, 17] + reveal_type(u5) # revealed: bool | Literal[17] +``` + +## Do not erase `Unknown` + +```py +from knot_extensions import Unknown + +def _(u1: Unknown | str, u2: str | Unknown) -> None: + reveal_type(u1) # revealed: Unknown | str + reveal_type(u2) # revealed: str | Unknown +``` + +## Collapse multiple `Unknown`s + +Since `Unknown` is a gradual type, it is not a subtype of anything, but multiple `Unknown`s in a +union are still redundant: + +```py +from knot_extensions import Unknown + +def _(u1: Unknown | Unknown | str, u2: Unknown | str | Unknown, u3: str | Unknown | Unknown) -> None: + reveal_type(u1) # revealed: Unknown | str + reveal_type(u2) # revealed: Unknown | str + reveal_type(u3) # revealed: str | Unknown +``` + +## Subsume multiple elements + +Simplifications still apply when `Unknown` is present. + +```py +from knot_extensions import Unknown + +def _(u1: str | Unknown | int | object): + reveal_type(u1) # revealed: Unknown | object +``` diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 8d5a559f865e6..33ac5e00257c5 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -396,119 +396,28 @@ mod tests { use test_case::test_case; #[test] - fn build_union() { - let db = setup_db(); - let t0 = Type::IntLiteral(0); - let t1 = Type::IntLiteral(1); - let union = UnionType::from_elements(&db, [t0, t1]).expect_union(); - - assert_eq!(union.elements(&db), &[t0, t1]); - } - - #[test] - fn build_union_single() { - let db = setup_db(); - let t0 = Type::IntLiteral(0); - let ty = UnionType::from_elements(&db, [t0]); - assert_eq!(ty, t0); - } - - #[test] - fn build_union_empty() { + fn build_union_no_elements() { let db = setup_db(); let ty = UnionBuilder::new(&db).build(); assert_eq!(ty, Type::Never); } #[test] - fn build_union_never() { + fn build_union_single_element() { let db = setup_db(); let t0 = Type::IntLiteral(0); - let ty = UnionType::from_elements(&db, [t0, Type::Never]); + let ty = UnionType::from_elements(&db, [t0]); assert_eq!(ty, t0); } #[test] - fn build_union_bool() { - let db = setup_db(); - let bool_instance_ty = KnownClass::Bool.to_instance(&db); - - let t0 = Type::BooleanLiteral(true); - let t1 = Type::BooleanLiteral(true); - let t2 = Type::BooleanLiteral(false); - let t3 = Type::IntLiteral(17); - - let union = UnionType::from_elements(&db, [t0, t1, t3]).expect_union(); - assert_eq!(union.elements(&db), &[t0, t3]); - - let union = UnionType::from_elements(&db, [t0, t1, t2, t3]).expect_union(); - assert_eq!(union.elements(&db), &[bool_instance_ty, t3]); - - let result_ty = UnionType::from_elements(&db, [bool_instance_ty, t0]); - assert_eq!(result_ty, bool_instance_ty); - - let result_ty = UnionType::from_elements(&db, [t0, bool_instance_ty]); - assert_eq!(result_ty, bool_instance_ty); - } - - #[test] - fn build_union_flatten() { + fn build_union_two_elements() { let db = setup_db(); let t0 = Type::IntLiteral(0); let t1 = Type::IntLiteral(1); - let t2 = Type::IntLiteral(2); - let u1 = UnionType::from_elements(&db, [t0, t1]); - let union = UnionType::from_elements(&db, [u1, t2]).expect_union(); - - assert_eq!(union.elements(&db), &[t0, t1, t2]); - } - - #[test] - fn build_union_simplify_subtype() { - let db = setup_db(); - let t0 = KnownClass::Str.to_instance(&db); - let t1 = Type::LiteralString; - let u0 = UnionType::from_elements(&db, [t0, t1]); - let u1 = UnionType::from_elements(&db, [t1, t0]); - - assert_eq!(u0, t0); - assert_eq!(u1, t0); - } - - #[test] - fn build_union_no_simplify_unknown() { - let db = setup_db(); - let t0 = KnownClass::Str.to_instance(&db); - let t1 = Type::Unknown; - let u0 = UnionType::from_elements(&db, [t0, t1]); - let u1 = UnionType::from_elements(&db, [t1, t0]); - - assert_eq!(u0.expect_union().elements(&db), &[t0, t1]); - assert_eq!(u1.expect_union().elements(&db), &[t1, t0]); - } - - #[test] - fn build_union_simplify_multiple_unknown() { - let db = setup_db(); - let t0 = KnownClass::Str.to_instance(&db); - let t1 = Type::Unknown; - - let u = UnionType::from_elements(&db, [t0, t1, t1]); - - assert_eq!(u.expect_union().elements(&db), &[t0, t1]); - } - - #[test] - fn build_union_subsume_multiple() { - let db = setup_db(); - let str_ty = KnownClass::Str.to_instance(&db); - let int_ty = KnownClass::Int.to_instance(&db); - let object_ty = KnownClass::Object.to_instance(&db); - let unknown_ty = Type::Unknown; - - let u0 = UnionType::from_elements(&db, [str_ty, unknown_ty, int_ty, object_ty]); + let union = UnionType::from_elements(&db, [t0, t1]).expect_union(); - assert_eq!(u0.expect_union().elements(&db), &[unknown_ty, object_ty]); + assert_eq!(union.elements(&db), &[t0, t1]); } impl<'db> IntersectionType<'db> {