Skip to content

Commit

Permalink
[red-knot] Move UnionBuilder tests to Markdown (#15374)
Browse files Browse the repository at this point in the history
## 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 <[email protected]>
Co-authored-by: T-256 <[email protected]>
  • Loading branch information
3 people authored Jan 9, 2025
1 parent b0905c4 commit b33cf5b
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 97 deletions.
125 changes: 125 additions & 0 deletions crates/red_knot_python_semantic/resources/mdtest/union_types.md
Original file line number Diff line number Diff line change
@@ -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
```
103 changes: 6 additions & 97 deletions crates/red_knot_python_semantic/src/types/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down

0 comments on commit b33cf5b

Please sign in to comment.