Equivalence

This commit is contained in:
David Peter 2025-06-05 15:48:09 +02:00
parent 969efae929
commit 0252ee6531
2 changed files with 116 additions and 0 deletions

View file

@ -23,6 +23,98 @@ def f() -> None:
reveal_type(x) # revealed: IntOrStr reveal_type(x) # revealed: IntOrStr
``` ```
## Type properties
### Equivalence
```py
from ty_extensions import static_assert, is_equivalent_to
type IntOrStr = int | str
type StrOrInt = str | int
static_assert(is_equivalent_to(IntOrStr, IntOrStr))
static_assert(is_equivalent_to(IntOrStr, StrOrInt))
type Rec1 = tuple[Rec1, int]
type Rec2 = tuple[Rec2, int]
type Other = tuple[Other, str]
static_assert(is_equivalent_to(Rec1, Rec2))
static_assert(not is_equivalent_to(Rec1, Other))
type Cycle1A = tuple[Cycle1B, int]
type Cycle1B = tuple[Cycle1A, str]
type Cycle2A = tuple[Cycle2B, int]
type Cycle2B = tuple[Cycle2A, str]
static_assert(is_equivalent_to(Cycle1A, Cycle2A))
static_assert(is_equivalent_to(Cycle1B, Cycle2B))
static_assert(not is_equivalent_to(Cycle1A, Cycle1B))
static_assert(not is_equivalent_to(Cycle1A, Cycle2B))
# type Cycle3A = tuple[Cycle3B] | None
# type Cycle3B = tuple[Cycle3A] | None
# static_assert(is_equivalent_to(Cycle3A, Cycle3A))
# static_assert(is_equivalent_to(Cycle3A, Cycle3B))
```
### Assignability
```py
type IntOrStr = int | str
x1: IntOrStr = 1
x2: IntOrStr = "1"
x3: IntOrStr | None = None
def _(int_or_str: IntOrStr) -> None:
# TODO: those should not be errors
x3: int | str = int_or_str # error: [invalid-assignment]
x4: int | str | None = int_or_str # error: [invalid-assignment]
x5: int | str | None = int_or_str or None # error: [invalid-assignment]
```
### Narrowing (intersections)
```py
class P: ...
class Q: ...
type EitherOr = P | Q
def _(x: EitherOr) -> None:
if isinstance(x, P):
reveal_type(x) # revealed: P
elif isinstance(x, Q):
reveal_type(x) # revealed: Q & ~P
else:
# TODO: This should be Never
reveal_type(x) # revealed: EitherOr & ~P & ~Q
```
### Fully static
```py
from typing import Any
from ty_extensions import static_assert, is_fully_static
type IntOrStr = int | str
type RecFullyStatic = int | tuple[RecFullyStatic]
static_assert(is_fully_static(IntOrStr))
static_assert(is_fully_static(RecFullyStatic))
type IntOrAny = int | Any
type RecNotFullyStatic = Any | tuple[RecNotFullyStatic]
static_assert(not is_fully_static(IntOrAny))
static_assert(not is_fully_static(RecNotFullyStatic))
```
## `__value__` attribute ## `__value__` attribute
```py ```py

View file

@ -275,6 +275,25 @@ fn is_fully_static_cycle_initial<'db>(_db: &'db dyn Db, _self: Type<'db>, _dummy
true true
} }
#[expect(clippy::trivially_copy_pass_by_ref)]
fn is_equivalent_to_cycle_recover<'db>(
_db: &'db dyn Db,
_value: &bool,
_count: u32,
_self: Type<'db>,
_other: Type<'db>,
) -> salsa::CycleRecoveryAction<bool> {
salsa::CycleRecoveryAction::Iterate
}
fn is_equivalent_to_cycle_initial<'db>(
_db: &'db dyn Db,
_self: Type<'db>,
_other: Type<'db>,
) -> bool {
true
}
/// Meta data for `Type::Todo`, which represents a known limitation in ty. /// Meta data for `Type::Todo`, which represents a known limitation in ty.
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
@ -1706,6 +1725,8 @@ impl<'db> Type<'db> {
/// This method returns `false` if either `self` or `other` is not fully static. /// This method returns `false` if either `self` or `other` is not fully static.
/// ///
/// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent /// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent
#[salsa::tracked(cycle_fn=is_equivalent_to_cycle_recover, cycle_initial=is_equivalent_to_cycle_initial)]
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool { pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool {
// TODO equivalent but not identical types: TypedDicts, Protocols, type aliases, etc. // TODO equivalent but not identical types: TypedDicts, Protocols, type aliases, etc.
@ -1735,6 +1756,9 @@ impl<'db> Type<'db> {
| (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => { | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => {
n.class.is_object(db) && protocol.normalized(db) == nominal n.class.is_object(db) && protocol.normalized(db) == nominal
} }
(Type::TypeAliasRef(left), right) => left.value_type(db).is_equivalent_to(db, right),
(left, Type::TypeAliasRef(right)) => left.is_equivalent_to(db, right.value_type(db)),
_ => self == other && self.is_fully_static(db) && other.is_fully_static(db), _ => self == other && self.is_fully_static(db) && other.is_fully_static(db),
} }
} }