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> {