Parametric type check - #47
Merged
Merged
Conversation
a parametric `is`-test against a protocol target (`x is A[int]`) was always an error: a protocol's instances never record which specialization they satisfy. but basedpython reifies class attribute annotations, so a protocol whose members are all data members can be checked structurally — each member's reified annotation against its specialized type, read at runtime with get_type_hints and an mro fallback. only a protocol with a method member (unrecoverable from an annotation) stays an error.
extend the parametric protocol check from `is` to the checked cast: a data-member protocol target (`x cast A[int]`) now validates the value's reified class annotations member by member via `_by_protocol_is`, rather than emitting a crashing `isinstance` against a subscripted protocol. a method-bearing protocol has no runtime residue, so it degrades to an unchecked `typing.cast` and reports `erased-cast-argument`.
extend the structural protocol check (for both `is` and `cast`) to method members: a method's parameters are validated contravariantly and its return covariantly against the value method's reified annotations, reading a parameter's inferred type from `type(default)` when it has no annotation. so `C() is A[int]` for `protocol A[T]: def f(self, other: T)` inspects `C.f`'s reified parameter type. only a member whose specialized type has no runtime spelling (a callable attribute) now leaves a protocol uncheckable.
`cast` had its own target-only plan (`runtime_check_plan`), which meant it could not see the value's type at all: a union target collapsed to one `isinstance` against a tuple, and a value typed by a reified type parameter warned `erased-cast-argument` even though `T == int` decides it exactly. both forms now build their runtime test with one `build_predicate`, over one `ParametricIsPlan`, differing only in `TargetPosition` (a cast target is a type expression, an `is`-rhs a value expression) and `ProbeStrictness` (an `is` must earn its `True` because it narrows; a cast only holds the value to arguments the runtime can see, keeping `[1, 2] cast list[int]` legal). - union arms are decomposed per kind, so a protocol or parameterized arm no longer produces a crashing `isinstance` - a reified type parameter compares its cell (`data cast? list[int]` on `data: list[T]` lowers to `T == int`); a parametric cast now reifies `T` - the four bespoke deep cast helpers collapse into `_checked_cast_pred` / `_try_cast_pred`; targets with no parametric claim keep the shallow form
`A[True]` specializes the member to `Literal[True]`, which had no runtime spelling — so the whole protocol fell back to `erased-type-check` even though the check is perfectly decidable. a literal member type now renders as a call to the structural check's own `_by_lit` helper, which rebuilds `typing.Literal[…]`. spelling it as a call keeps the member list import-free, and confining it to a protocol-specific speller keeps `_by_lit` out of the reified-call and constructor injections, where it is not in scope. `_by_proto_sub` learns literal subtyping, so variance stays exact: an invariant data member still rejects a `bool` annotation for `Literal[True]`, while a contravariant parameter accepts it (`bool` does accept `True`) and a covariant return follows `Literal[True] <: bool <: int`.
the cast now shares the parametric `is` engine, so a value typed by a reified type parameter verifies its argument exactly (`T == int`) and no longer warns. document that alongside the `__orig_class__` and protocol cases, and regenerate rules.md / ty.schema.json.
KotlinIsland
force-pushed
the
parametric-type-check
branch
from
July 23, 2026 06:24
d4c7e51 to
5106246
Compare
the probe assumed a subclass's type arguments line up positionally with its base's, so `class Odd[T](list[int])` reported `T` as list's argument: `Odd[str]() is list[str]` answered True even though `Odd` is a `list[int]`. a reordering base (`class Swap[A, B](dict[B, A])`) was wrong the same way. arguments are now resolved down the declared base chain, substituting the type parameters into each base, so a base that fixes, reorders or nests its arguments is followed faithfully. this subsumes both default patches: a bare class starts from its own pep 696 defaults and the same walk carries them through, so the special cases for own-identity and base-recorded parameters collapse into one mechanism. the positional rule survives only as a last resort, after resolution fails, for a builtin registered as a *virtual* subclass of an abc (`list` for `Sequence`) which has no base to walk. because it now runs on the resolved base rather than the original subclass, `Odd[str]` reaches it as `list[int]` and still answers `Sequence[int]` correctly. fix(parametric-is): resolve own-identity type-param defaults a type parameter that appears only in the class's own identity (`class A[T = Never]`) is recorded in no base, so the mro walk had nothing to resolve and the probe found no arguments at all. read it straight off `__type_params__` instead. this is precisely the case constructor reification cannot cover: it fills defaults by injecting `A[int]()`, but only when the default has a runtime spelling, so an unspellable one like `Never` leaves the constructor bare. the two mechanisms are complementary — injection covers own-identity parameters with spellable defaults, this covers the rest. restricted to `klass is origin`: a subclass's parameters have no positional relationship to a base's, so `class Odd[T = str](list[int])` must not report `str` as list's argument. fix(parametric-is): resolve pep 696 defaults in the runtime probe a class records its generic bases *unsubstituted* — `class L[T = Never] (list[T])` stores `list[T]`, never `list[Never]` — so the probe was comparing a bare TypeVar against the target and never matching. a parameter left at its default now resolves to that default, so `L() is list[Never]` answers True. the substitution is applied only when the value records no explicit `__orig_class__`: an `L[int]()` fixes `T` itself, and reading the class default over the top of that would report an argument the value never had (`L[str]() is list[int]` must stay False).
KotlinIsland
force-pushed
the
parametric-type-check
branch
from
July 23, 2026 06:24
5106246 to
1cb7601
Compare
the parametric-is runtime test probed with `type X[T] = T` — pep 695, which 3.12 accepts — but the pep 696 default assertions I added need native `[T = int]` syntax and `TypeVar.has_default()`, both 3.13. on a box whose `python3` is 3.12 (the linux CI runner) the probe passed, the test ran, and the program died on a SyntaxError. split the program by requirement rather than raising the bar for all of it: the protocol, method, literal, cast and base-chain coverage needs only pep 695 and keeps running on 3.12, while the defaults half is probed for pep 696 and skips when unavailable. `Odd`'s default was incidental to what it tests, so it drops one and stays in the 3.12 program. the probe's own SyntaxError is now swallowed — a rejected candidate should not print a scary traceback inside a passing run.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.