diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md index c1c887c3755d6..4fde8fae0b156 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md @@ -33,7 +33,7 @@ typevar can only specialize to a type that is a supertype of the lower bound, an upper bound. ```py -from typing import Any, final, Never, Sequence +from typing import Any, Callable, final, Never, Sequence from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -86,6 +86,13 @@ def _[T]() -> None: static_assert(not ConstraintSet.range(Base, T, Unrelated)) ``` +Ordinary TypeVar bounds compare whole callables, so incompatible returns make a range unsatisfiable. + +```py +def callable_returns[T]() -> None: + static_assert(ConstraintSet.range(Callable[[int], int], T, Callable[[int], str]) == ConstraintSet.never()) +``` + When the lower and upper bounds are the same type, `equality` requires the typevar to specialize to that specific type. @@ -131,6 +138,16 @@ def _[T]() -> None: static_assert(ConstraintSet.lower_bound(int, T) == expected) ``` +Ordinary TypeVar bounds retain callable returns. + +```py +from typing import Callable + +def callable_bound[T]() -> None: + constraints = ConstraintSet.lower_bound(Callable[[int], int], T) + static_assert(constraints != ConstraintSet.lower_bound(Callable[[int], str], T)) +``` + ### Upper bound An upper-bound constraint requires the type variable to be a subtype of its bound without providing @@ -145,6 +162,16 @@ def _[T]() -> None: static_assert(ConstraintSet.upper_bound(T, int) == expected) ``` +Upper bounds likewise retain ordinary callable returns. + +```py +from typing import Callable + +def callable_bound[T]() -> None: + constraints = ConstraintSet.upper_bound(T, Callable[[int], int]) + static_assert(constraints != ConstraintSet.upper_bound(T, Callable[[int], str])) +``` + Unlike an explicit two-sided range, an upper-bound constraint does not supply `Never` as lower-bound inference evidence. @@ -176,6 +203,16 @@ def _[T]() -> None: reveal_type(equality.solutions_for(T, inferable=tuple[T])) ``` +Equality of ordinary types also includes callable returns. + +```py +from typing import Callable + +def callable_bound[T]() -> None: + constraints = ConstraintSet.equality(T, Callable[[int], int]) + static_assert(constraints != ConstraintSet.equality(T, Callable[[int], str])) +``` + ### Negated range A _negated range_ constraint is the opposite of a range constraint: it requires the typevar to _not_ @@ -1082,6 +1119,26 @@ def same_typevar[T](): static_assert(constraints == expected) ``` +Constraining a ParamSpec with itself leaves every parameter list possible. + +```pyi +from typing import Callable +from ty_extensions import Bottom, Top + +def same_paramspec[**P]() -> None: + constraints = ConstraintSet.upper_bound(P, P) + expected = ConstraintSet.range(Bottom[Callable[..., Never]], P, Top[Callable[..., object]]) + static_assert(constraints == expected) + + constraints = ConstraintSet.lower_bound(P, P) + expected = ConstraintSet.range(Bottom[Callable[..., Never]], P, Top[Callable[..., object]]) + static_assert(constraints == expected) + + constraints = ConstraintSet.equality(P, P) + expected = ConstraintSet.range(Bottom[Callable[..., Never]], P, Top[Callable[..., object]]) + static_assert(constraints == expected) +``` + ## Existential quantification Existential quantification removes the listed typevars from a constraint set. Any constraints that @@ -1167,7 +1224,7 @@ out all of the different kinds of constraints described above. Here we just test exists, and provides more detail than otherwise. ```py -from ty_extensions._internal import ConstraintSet +from ty_extensions._internal import ConstraintSet, RegularCallableTypeOf class Super: ... class Base(Super): ... @@ -1181,3 +1238,483 @@ def _[T]() -> None: # revealed: ConstraintSet[(Sub ≤ T@_ ≤ Super)] reveal_type(ConstraintSet.range(Sub, T, Super).with_detailed_display()) ``` + +Explicit bottom and top parameter-list bounds are shown in the constraint. + +```py +from typing import Callable, Never +from ty_extensions import Bottom, Top + +def explicit_bounds[**P]() -> None: + lower = ConstraintSet.range(Bottom[Callable[..., Never]], P, Callable[[int], int]) + # revealed: ConstraintSet[((*args: object, **kwargs: object) ≤ P@explicit_bounds ≤ (int, /))] + reveal_type(lower.with_detailed_display()) + upper = ConstraintSet.range(Callable[[int], int], P, Top[Callable[..., object]]) + # revealed: ConstraintSet[((int, /) ≤ P@explicit_bounds ≤ Top[(...)])] + reveal_type(upper.with_detailed_display()) +``` + +ParamSpec bounds display the full parameter list without the callable return type. + +```py +def complete(value: int, /, text: str = "", *args: float, flag: bool = False, **kwargs: bytes) -> int: + return 0 + +def signature[**P]() -> None: + constraints = ConstraintSet.range(RegularCallableTypeOf[complete], P, RegularCallableTypeOf[complete]) + # revealed: ConstraintSet[(P@signature = (value: int, /, text: str = "", *args: float, flag: bool = False, **kwargs: bytes))] + reveal_type(constraints.with_detailed_display()) +``` + +Generic callable bounds keep their own ParamSpec binder. + +```py +def callback[**Q](*args: Q.args, **kwargs: Q.kwargs) -> None: ... +def generic_signature[**P]() -> None: + constraints = ConstraintSet.range(RegularCallableTypeOf[callback], P, RegularCallableTypeOf[callback]) + # revealed: ConstraintSet[(P@generic_signature = (**Q@callback))] + reveal_type(constraints.with_detailed_display()) +``` + +## ParamSpec + +A ParamSpec constraint describes parameter lists; callable returns are ignored. + +### Construction + +Legacy ParamSpecs work with every constructor. + +```py +from typing import Callable, ParamSpec +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet, is_constraint_set_assignable_to + +P = ParamSpec("P") + +def legacy_range(callback: Callable[P, None]) -> None: + constraints = ConstraintSet.range(Callable[[int, str], None], P, Callable[[int, str], None]) + reveal_type(constraints) # revealed: ConstraintSet[bool] + different_returns = ConstraintSet.range(Callable[[int, str], int], P, Callable[[int, str], str]) + static_assert(constraints == different_returns) + +def legacy_lower_bound(callback: Callable[P, None]) -> None: + expected = is_constraint_set_assignable_to(Callable[[int, str], None], Callable[P, None]) + static_assert(ConstraintSet.lower_bound(Callable[[int, str], int], P) == expected) + +def legacy_upper_bound(callback: Callable[P, None]) -> None: + expected = is_constraint_set_assignable_to(Callable[P, None], Callable[[int, str], None]) + static_assert(ConstraintSet.upper_bound(P, Callable[[int, str], str]) == expected) + +def legacy_equality(callback: Callable[P, None]) -> None: + equality = ConstraintSet.equality(P, Callable[[int, str], bytes]) + static_assert(equality == ConstraintSet.range(Callable[[int, str], None], P, Callable[[int, str], None])) +``` + +An empty parameter list is an exact bound, distinct from a one-parameter list. + +```py +def empty[**P]() -> None: + constraints = ConstraintSet.range(Callable[[], None], P, Callable[[], None]) + reveal_type(constraints) # revealed: ConstraintSet[bool] + static_assert(constraints != ConstraintSet.range(Callable[[int], None], P, Callable[[int], None])) +``` + +An alias of a known constructor retains its ParamSpec argument rules. + +```py +def aliased_constructor[**P]() -> None: + equals = ConstraintSet.equality + constraints = equals(P, Callable[[int], None]) + static_assert(constraints == ConstraintSet.range(Callable[[int], None], P, Callable[[int], None])) +``` + +### Callable aliases + +Specialized callable aliases have the same bounds as their expanded parameter lists. + +```py +from typing import Callable, Concatenate +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +type Callback[**Q, R] = Callable[Q, R] + +def aliases[**P]() -> None: + constraints = ConstraintSet.range(Callback[[int, str, bool], int], P, Callback[[int, str, bool], str]) + reveal_type(constraints) # revealed: ConstraintSet[bool] + expected = ConstraintSet.range(Callable[[int, str, bool], None], P, Callable[[int, str, bool], None]) + static_assert(constraints == expected) +``` + +Fully specializing a `Concatenate` alias preserves every prefix parameter and the concrete tail. + +```py +type Prefixed[**Q, R] = Callable[Concatenate[int, str, Q], R] + +def concatenate[**P]() -> None: + constraints = ConstraintSet.range(Prefixed[[bool], int], P, Prefixed[[bool], str]) + reveal_type(constraints) # revealed: ConstraintSet[bool] + expected = ConstraintSet.range(Callable[[int, str, bool], None], P, Callable[[int, str, bool], None]) + static_assert(constraints == expected) +``` + +### Two-sided bounds + +A callable accepting `Super` and a consumer passing a `Sub` give `(Super, /) ≤ P ≤ (Sub, /)`. + +```py +from typing import Callable, final +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +class Super: ... +class Base(Super): ... +class Sub(Base): ... + +@final +class Unrelated: ... + +def two_sided[**P]() -> None: + constraints = ConstraintSet.range(Callable[[Super], None], P, Callable[[Sub], None]) + reveal_type(constraints) # revealed: ConstraintSet[bool] + lower = ConstraintSet.lower_bound(Callable[[Super], int], P) + upper = ConstraintSet.upper_bound(P, Callable[[Sub], str]) + static_assert(constraints == (lower & upper)) + static_assert(constraints != lower) + static_assert(constraints != upper) +``` + +Inverted or incomparable bounds are unsatisfiable. + +```py +def incompatible[**P]() -> None: + inverted = ConstraintSet.range(Callable[[Sub], None], P, Callable[[Super], None]) + static_assert(inverted == ConstraintSet.never()) + incomparable = ConstraintSet.range(Callable[[Base], None], P, Callable[[Unrelated], None]) + static_assert(incomparable == ConstraintSet.never()) +``` + +Individually satisfiable lower and upper bounds can have an empty intersection. + +```py +def incompatible_intersection[**P]() -> None: + lower = ConstraintSet.lower_bound(Callable[[Sub], None], P) + upper = ConstraintSet.upper_bound(P, Callable[[Super], None]) + static_assert((lower & upper) == ConstraintSet.never()) +``` + +### Symbolic bounds + +Two ParamSpecs can be constrained to the same parameter list, in either order. + +```py +from typing import Any, Callable +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet, is_constraint_set_assignable_to + +def equality[**P, **Q]() -> None: + constraints = ConstraintSet.equality(P, Q) + expected = is_constraint_set_assignable_to(Callable[P, Any], Callable[Q, Any]) + static_assert(constraints == expected) + static_assert(ConstraintSet.equality(Q, P) == constraints) +``` + +Each endpoint is retained when a symbolic lower bound is combined with a concrete upper bound. + +```py +def symbolic_lower[**P, **Q]() -> None: + constraints = ConstraintSet.range(Q, P, Callable[[int], None]) + lower = ConstraintSet.lower_bound(Q, P) + upper = ConstraintSet.upper_bound(P, Callable[[int], None]) + static_assert(constraints == (lower & upper)) + static_assert(constraints != lower) + static_assert(constraints != upper) +``` + +Symbolic upper bounds likewise retain their concrete lower bound. + +```py +def symbolic_upper[**P, **Q]() -> None: + constraints = ConstraintSet.range(Callable[[int], None], P, Q) + lower = ConstraintSet.lower_bound(Callable[[int], None], P) + upper = ConstraintSet.upper_bound(P, Q) + static_assert(constraints == (lower & upper)) + static_assert(constraints != lower) + static_assert(constraints != upper) +``` + +Three ParamSpecs form a two-sided range. + +```py +def symbolic_range[**P, **Q, **R]() -> None: + constraints = ConstraintSet.range(Q, P, R) + lower = ConstraintSet.lower_bound(Q, P) + upper = ConstraintSet.upper_bound(P, R) + static_assert(constraints == (lower & upper)) + static_assert(constraints != lower) + static_assert(constraints != upper) +``` + +### Symbolic callable bounds + +An unprefixed callable bound describes the same parameter list as its bare ParamSpec. + +```py +from typing import Any, Callable, Concatenate +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet, is_constraint_set_assignable_to + +def unprefixed[**P, **Q]() -> None: + constraints = ConstraintSet.range(Callable[Q, int], P, Callable[Q, str]) + static_assert(constraints == ConstraintSet.range(Q, P, Q)) +``` + +A `Concatenate` bound preserves its prefix and symbolic tail while erasing the return. + +```py +def prefixed[**P, **Q]() -> None: + constraints = ConstraintSet.range(Callable[Concatenate[int, Q], int], P, Callable[Concatenate[int, Q], str]) + expected = is_constraint_set_assignable_to(Callable[Concatenate[int, Q], int], Callable[P, Any]) + expected &= is_constraint_set_assignable_to(Callable[P, Any], Callable[Concatenate[int, Q], str]) + static_assert(constraints == expected) + static_assert(constraints != ConstraintSet.range(Q, P, Q)) + different_prefix = ConstraintSet.range(Callable[Concatenate[str, Q], None], P, Callable[Concatenate[str, Q], None]) + static_assert(constraints != different_prefix) +``` + +### Signature preservation + +Named parameters accept positional-only calls; the reverse range is invalid. + +```pyi +from typing import Callable +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet, RegularCallableTypeOf + +def named(value: int) -> None: ... +def positional_only[**P]() -> None: + constraints = ConstraintSet.range(RegularCallableTypeOf[named], P, Callable[[int], None]) + reveal_type(constraints) # revealed: ConstraintSet[bool] + reverse = ConstraintSet.range(Callable[[int], None], P, RegularCallableTypeOf[named]) + static_assert(reverse == ConstraintSet.never()) +``` + +Named parameters also accept keyword-only calls; the reverse range is invalid. + +```pyi +def keyword(*, value: int) -> None: ... +def keyword_only[**P]() -> None: + constraints = ConstraintSet.range(RegularCallableTypeOf[named], P, RegularCallableTypeOf[keyword]) + reveal_type(constraints) # revealed: ConstraintSet[bool] + reverse = ConstraintSet.range(RegularCallableTypeOf[keyword], P, RegularCallableTypeOf[named]) + static_assert(reverse == ConstraintSet.never()) +``` + +An optional parameter accepts every call to a required parameter, but not the reverse. + +```pyi +def optional(value: int = ...) -> None: ... +def defaults[**P]() -> None: + constraints = ConstraintSet.range(RegularCallableTypeOf[optional], P, RegularCallableTypeOf[named]) + reveal_type(constraints) # revealed: ConstraintSet[bool] + reverse = ConstraintSet.range(RegularCallableTypeOf[named], P, RegularCallableTypeOf[optional]) + static_assert(reverse == ConstraintSet.never()) +``` + +Variadic positional parameters accept fixed positional lists, but not the reverse. + +```pyi +def args(*args: int) -> None: ... +def positional_variadics[**P]() -> None: + constraints = ConstraintSet.range(RegularCallableTypeOf[args], P, Callable[[int, int], None]) + reveal_type(constraints) # revealed: ConstraintSet[bool] + reverse = ConstraintSet.range(Callable[[int, int], None], P, RegularCallableTypeOf[args]) + static_assert(reverse == ConstraintSet.never()) +``` + +Variadic keyword parameters likewise accept a fixed keyword-only parameter, but not the reverse. + +```pyi +def kwargs(**kwargs: int) -> None: ... +def keyword_variadics[**P]() -> None: + constraints = ConstraintSet.range(RegularCallableTypeOf[kwargs], P, RegularCallableTypeOf[keyword]) + reveal_type(constraints) # revealed: ConstraintSet[bool] + reverse = ConstraintSet.range(RegularCallableTypeOf[keyword], P, RegularCallableTypeOf[kwargs]) + static_assert(reverse == ConstraintSet.never()) +``` + +### Overloaded bounds + +Return types are erased in every overload, without keeping only the first or last parameter list. + +```pyi +from typing import Callable, overload +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet, RegularCallableTypeOf + +@overload +def overloaded(value: int, /) -> int: ... +@overload +def overloaded(*, value: str) -> str: ... +@overload +def swapped_returns(value: int, /) -> str: ... +@overload +def swapped_returns(*, value: str) -> int: ... +def keyword(*, value: str) -> None: ... +def overloads[**P]() -> None: + constraints = ConstraintSet.range(RegularCallableTypeOf[overloaded], P, RegularCallableTypeOf[swapped_returns]) + reveal_type(constraints) # revealed: ConstraintSet[bool] + static_assert(constraints == ConstraintSet.range(RegularCallableTypeOf[overloaded], P, RegularCallableTypeOf[overloaded])) + static_assert(constraints != ConstraintSet.range(Callable[[int], None], P, Callable[[int], None])) + static_assert(constraints != ConstraintSet.range(RegularCallableTypeOf[keyword], P, RegularCallableTypeOf[keyword])) +``` + +An overloaded lower bound can satisfy a single signature; an overloaded upper bound requires both. + +```pyi +def asymmetric[**P]() -> None: + constraints = ConstraintSet.range(RegularCallableTypeOf[overloaded], P, Callable[[int], None]) + reveal_type(constraints) # revealed: ConstraintSet[bool] + reverse = ConstraintSet.range(Callable[[int], None], P, RegularCallableTypeOf[overloaded]) + static_assert(reverse == ConstraintSet.never()) +``` + +The string overload accepts only a keyword argument, not a positional argument. + +```pyi +def parameter_kinds[**P]() -> None: + constraints = ConstraintSet.range(RegularCallableTypeOf[overloaded], P, RegularCallableTypeOf[keyword]) + reveal_type(constraints) # revealed: ConstraintSet[bool] + positional = ConstraintSet.range(RegularCallableTypeOf[overloaded], P, Callable[[str], None]) + static_assert(positional == ConstraintSet.never()) +``` + +### Gradual parameter lists + +Ellipsis describes gradual parameters; `Any` annotates one required positional-only parameter. + +```py +from typing import Any, Callable +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +def gradual[**P]() -> None: + ellipsis = ConstraintSet.range(Callable[..., int], P, Callable[..., str]) + reveal_type(ellipsis.with_detailed_display()) # revealed: ConstraintSet[(P@gradual = (...))] + any_parameter = ConstraintSet.range(Callable[[Any], int], P, Callable[[Any], str]) + reveal_type(any_parameter.with_detailed_display()) # revealed: ConstraintSet[(P@gradual = (Any, /))] +``` + +Only ellipsis is compatible with an empty parameter list in either bound. + +```py +def empty[**P]() -> None: + reveal_type(ConstraintSet.range(Callable[..., None], P, Callable[[], None])) # revealed: ConstraintSet[bool] + reveal_type(ConstraintSet.range(Callable[[], None], P, Callable[..., None])) # revealed: ConstraintSet[bool] + static_assert(ConstraintSet.range(Callable[[Any], None], P, Callable[[], None]) == ConstraintSet.never()) + static_assert(ConstraintSet.range(Callable[[], None], P, Callable[[Any], None]) == ConstraintSet.never()) +``` + +### Missing bounds + +A missing lower bound is equivalent to the bottom signature, which accepts all arguments. + +```py +from typing import Callable, Never +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import ConstraintSet + +def missing_lower_bound[**P]() -> None: + constraints = ConstraintSet.upper_bound(P, Callable[[int], int]) + expected = ConstraintSet.range(Bottom[Callable[..., Never]], P, Callable[[int], int]) + static_assert(constraints == expected) +``` + +A missing upper bound is equivalent to the top signature, which accepts no calls. + +```py +def missing_upper_bound[**P]() -> None: + constraints = ConstraintSet.lower_bound(Callable[[int], int], P) + expected = ConstraintSet.range(Callable[[int], int], P, Top[Callable[..., object]]) + static_assert(constraints == expected) +``` + +Omitted bounds stay absent; explicit `...` bounds remain visible. + +```py +def missing_bounds[**P]() -> None: + # revealed: ConstraintSet[((int, /) ≤ P@missing_bounds)] + reveal_type(ConstraintSet.lower_bound(Callable[[int], None], P).with_detailed_display()) + # revealed: ConstraintSet[((int, /) ≤ P@missing_bounds ≤ (...))] + reveal_type(ConstraintSet.range(Callable[[int], None], P, Callable[..., None]).with_detailed_display()) + # revealed: ConstraintSet[(P@missing_bounds ≤ (int, /))] + reveal_type(ConstraintSet.upper_bound(P, Callable[[int], None]).with_detailed_display()) + # revealed: ConstraintSet[((...) ≤ P@missing_bounds ≤ (int, /))] + reveal_type(ConstraintSet.range(Callable[..., None], P, Callable[[int], None]).with_detailed_display()) +``` + +### Invalid forms and preservation controls + +An ordinary type is not a parameter list, so it makes a ParamSpec constraint unsatisfiable. + +```py +from typing import Callable, Never, TypeVarTuple +from typing_extensions import TypeForm +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +def invalid_bounds[**P]() -> None: + static_assert(ConstraintSet.range(int, P, Callable[[int], None]) == ConstraintSet.never()) + static_assert(ConstraintSet.range(Callable[[int], None], P, int) == ConstraintSet.never()) + static_assert(ConstraintSet.lower_bound(int, P) == ConstraintSet.never()) + static_assert(ConstraintSet.upper_bound(P, object) == ConstraintSet.never()) + static_assert(ConstraintSet.equality(P, Never) == ConstraintSet.never()) +``` + +An ordinary TypeVar or ParamSpec component is not a complete parameter list. + +```py +def invalid_typevar_bounds[**P, **Q, T]() -> None: + static_assert(ConstraintSet.range(T, P, Callable[[int], None]) == ConstraintSet.never()) + static_assert(ConstraintSet.range(Callable[[int], None], P, Q.args) == ConstraintSet.never()) + static_assert(ConstraintSet.range(Q.kwargs, P, Callable[[int], None]) == ConstraintSet.never()) +``` + +Bare TypeVarTuples remain invalid bounds. + +```py +def typevartuple_bounds[**P, *Us]() -> None: + ConstraintSet.range(Us, P, Callable[[int], None]) # error: [invalid-type-form] "TypeVarTuple `Us`" + ConstraintSet.range(Callable[[int], None], P, Us) # error: [invalid-type-form] "TypeVarTuple `Us`" +``` + +Allowing ParamSpecs in a constructor does not affect subsequent unrelated TypeForm calls. + +```py +def accepts_type_form(form: TypeForm[object]) -> None: ... +def invalid_forms[**P]() -> None: + ConstraintSet.equality(P, Callable[[int], None]) + accepts_type_form(P) # error: [invalid-type-form] +``` + +ParamSpec components keep their ordinary bounds. + +```py +def components[**P]() -> None: + args = ConstraintSet.range(tuple[int], P.args, tuple[object, ...]) + kwargs = ConstraintSet.range(dict[str, object], P.kwargs, dict[str, object]) + reveal_type(args) # revealed: ConstraintSet[bool] + reveal_type(kwargs) # revealed: ConstraintSet[bool] +``` + +Bare TypeVarTuples remain invalid subjects. + +```py +Ts = TypeVarTuple("Ts") + +def legacy_typevartuple_subject(value: tuple[*Ts]) -> None: + ConstraintSet.range(Callable[[int], None], Ts, Callable[[int], None]) # error: [invalid-type-form] + +def typevartuple_subject[*Us]() -> None: + ConstraintSet.range(Callable[[int], None], Us, Callable[[int], None]) # error: [invalid-type-form] +``` diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 93bbea724afd2..d0cb4caffc00a 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -233,6 +233,35 @@ fn inferable_typevars_from_tuple<'db>( typevars.map(|typevars| TypeVarSet::from_typevars(db, typevars)) } +/// Converts a bound from an internal `ConstraintSet` constructor to its solver representation. +/// A bare `ParamSpec` requires parameter lists; ordinary typevars and `ParamSpec` components keep +/// their type bounds. `None` indicates an invalid supplied bound, not an omitted endpoint. +fn normalize_constraint_bound<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarInstance<'db>, + bound: Type<'db>, +) -> Option> { + let bound = bound.project_type_form(db, env); + if !typevar.is_paramspec(db) || typevar.paramspec_attr(db).is_some() { + return Some(bound); + } + match bound.resolve_type_alias(db) { + Type::Callable(callable) + if let [signature] = callable.signatures(db).overloads.as_slice() + && signature.generic_context.is_none() + && let Some(paramspec) = signature.parameters().as_paramspec() => + { + Some(Type::TypeVar(paramspec)) + } + Type::Callable(callable) => Some(Type::Callable(callable.into_paramspec_value(db))), + Type::TypeVar(bound) if bound.is_paramspec(db) && bound.paramspec_attr(db).is_none() => { + Some(Type::TypeVar(bound)) + } + _ => None, + } +} + /// Priority levels for call errors in intersection types. /// Higher values indicate more specific errors that should take precedence. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -2819,13 +2848,16 @@ impl<'db> Bindings<'db> { let [Some(lower), Some(typevar)] = overload.parameter_types() else { return; }; - let lower = lower.project_type_form(db, env); let typevar = typevar.project_type_form(db, env); let Type::TypeVar(typevar) = typevar else { return; }; let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { + let Some(lower) = normalize_constraint_bound(db, env, typevar, *lower) + else { + return ConstraintSet::from_bool(constraints, false); + }; ConstraintSet::constrain_typevar_lower_bound( db, env, @@ -2845,12 +2877,15 @@ impl<'db> Bindings<'db> { return; }; let typevar = typevar.project_type_form(db, env); - let upper = upper.project_type_form(db, env); let Type::TypeVar(typevar) = typevar else { return; }; let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { + let Some(upper) = normalize_constraint_bound(db, env, typevar, *upper) + else { + return ConstraintSet::from_bool(constraints, false); + }; ConstraintSet::constrain_typevar_upper_bound( db, env, @@ -2870,12 +2905,15 @@ impl<'db> Bindings<'db> { return; }; let typevar = typevar.project_type_form(db, env); - let value = value.project_type_form(db, env); let Type::TypeVar(typevar) = typevar else { return; }; let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { + let Some(value) = normalize_constraint_bound(db, env, typevar, *value) + else { + return ConstraintSet::from_bool(constraints, false); + }; ConstraintSet::constrain_typevar( db, env, @@ -2896,14 +2934,18 @@ impl<'db> Bindings<'db> { else { return; }; - let lower = lower.project_type_form(db, env); let typevar = typevar.project_type_form(db, env); - let upper = upper.project_type_form(db, env); let Type::TypeVar(typevar) = typevar else { return; }; let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { + let (Some(lower), Some(upper)) = ( + normalize_constraint_bound(db, env, typevar, *lower), + normalize_constraint_bound(db, env, typevar, *upper), + ) else { + return ConstraintSet::from_bool(constraints, false); + }; ConstraintSet::constrain_typevar( db, env, diff --git a/crates/ty_python_semantic/src/types/callable.rs b/crates/ty_python_semantic/src/types/callable.rs index e2d6a031383a0..e923f247a67bd 100644 --- a/crates/ty_python_semantic/src/types/callable.rs +++ b/crates/ty_python_semantic/src/types/callable.rs @@ -486,6 +486,21 @@ impl<'db> CallableType<'db> { CallableType::new(db, self.signatures(db), CallableTypeKind::Regular) } + /// Retain every parameter signature and its generic context, but erase return types + /// that do not participate in a `ParamSpec` specialization. + pub(crate) fn into_paramspec_value(self, db: &'db dyn Db) -> CallableType<'db> { + CallableType::new( + db, + CallableSignature::from_overloads( + self.signatures(db) + .iter() + .cloned() + .map(|signature| signature.with_return_type(Type::unknown())), + ), + CallableTypeKind::ParamSpecValue, + ) + } + /// Returns the reduced callable produced by partially applying selected overloads. pub(crate) fn partially_apply( db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index a7371e5b399bc..1d54304b9731a 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -111,7 +111,7 @@ use crate::types::visitor::{ TypeCollector, TypeKind, TypeVisitor, walk_non_atomic_type, walk_type_with_recursion_guard, }; use crate::types::{ - ApplyTypeMappingVisitor, BoundTypeVarInstance, IntersectionType, Type, TypeContext, + ApplyTypeMappingVisitor, BoundTypeVarInstance, IntersectionType, Parameters, Type, TypeContext, TypeMapping, TypePair, TypeVarBoundOrConstraints, TypeVarVariance, UnionType, }; use crate::{Db, FxIndexMap, FxIndexSet, FxOrderSet, ProgramEnvironment}; @@ -1851,20 +1851,36 @@ impl<'db> Constraint<'db> { /// Returns the effective lower endpoint with its provenance. /// - /// An absent endpoint defaults to `Validity(Never)`. Explicit `Evidence(Never)` is returned - /// unchanged. - fn lower_bound(self) -> ConstraintBound<'db> { + /// An absent endpoint defaults to `Validity(Never)`, or a validity bound for the bottom + /// parameter list of a bare `ParamSpec`. Explicit `Evidence(Never)` is returned unchanged. + fn lower_bound(self, db: &'db dyn Db) -> ConstraintBound<'db> { self.stored_lower_bound() - .unwrap_or_else(ConstraintBound::missing_lower) + .unwrap_or_else(|| ConstraintBound::Validity(self.default_lower_bound(db))) } /// Returns the effective upper endpoint with its provenance. /// - /// An absent endpoint defaults to `Validity(object)`. Explicit `Evidence(object)` is returned - /// unchanged. - fn upper_bound(self) -> ConstraintBound<'db> { + /// An absent endpoint defaults to `Validity(object)`, or a validity bound for the top + /// parameter list of a bare `ParamSpec`. Explicit `Evidence(object)` is returned unchanged. + fn upper_bound(self, db: &'db dyn Db) -> ConstraintBound<'db> { self.stored_upper_bound() - .unwrap_or_else(ConstraintBound::missing_upper) + .unwrap_or_else(|| ConstraintBound::Validity(self.default_upper_bound(db))) + } + + fn default_lower_bound(self, db: &'db dyn Db) -> Type<'db> { + if self.typevar.is_paramspec(db) && self.typevar.paramspec_attr(db).is_none() { + Type::paramspec_value_callable(db, Parameters::bottom()) + } else { + Type::Never + } + } + + fn default_upper_bound(self, db: &'db dyn Db) -> Type<'db> { + if self.typevar.is_paramspec(db) && self.typevar.paramspec_attr(db).is_none() { + Type::paramspec_value_callable(db, Parameters::top()) + } else { + Type::object() + } } /// Returns the stored lower endpoint with its provenance, or `None` if absent. @@ -1909,8 +1925,9 @@ impl<'db> Constraint<'db> { /// A bound derived only from validity remains validity. Any derivation that also depends on /// evidence is itself evidence. /// -/// Every type is a supertype of `Never` and a subtype of `object`, so `Validity(Never)` represents -/// an absent lower bound and `Validity(object)` represents an absent upper bound. +/// Missing endpoints are stored as `None`. [`Constraint`] supplies validity defaults appropriate +/// for its typevar: `Never`/`object` for ordinary types, and bottom/top parameter lists for bare +/// `ParamSpec`s. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)] enum ConstraintBound<'db> { Validity(Type<'db>), @@ -2008,7 +2025,7 @@ impl<'db> ConstraintBound<'db> { /// /// Missing bounds are stored as `None`, making equality and hashing cheaper for this common case. /// Ordinary validity identities (`Never`/`object`) are canonicalized to absence. The owning -/// [`Constraint`] supplies effective defaults for missing endpoints. +/// [`Constraint`] supplies effective defaults appropriate for its typevar. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)] struct ConstraintBounds<'db> { lower: Option>, @@ -2484,8 +2501,8 @@ impl<'db> Constraint<'db> { // `upper`. We use an existential check here ("is there *some* assignment where // `lower ≤ upper`?") rather than a universal check, because the bounds may mention // typevars — e.g., `Sequence[int] ≤ A ≤ Sequence[T]` is satisfiable when `int ≤ T`. - let effective_lower = constraint.lower_bound().ty(); - let effective_upper = constraint.upper_bound().ty(); + let effective_lower = constraint.lower_bound(db).ty(); + let effective_upper = constraint.upper_bound(db).ty(); if lower.is_some() && upper.is_some() { let when = effective_lower.when_constraint_set_assignable_to_owned(db, env, effective_upper); @@ -2666,10 +2683,10 @@ impl ConstraintId { { return false; } - let other_lower = other_constraint.lower_bound().ty(); - let self_lower = self_constraint.lower_bound().ty(); - let self_upper = self_constraint.upper_bound().ty(); - let other_upper = other_constraint.upper_bound().ty(); + let self_lower = self_constraint.lower_bound(db).ty(); + let self_upper = self_constraint.upper_bound(db).ty(); + let other_lower = other_constraint.lower_bound(db).ty(); + let other_upper = other_constraint.upper_bound(db).ty(); other_lower.is_constraint_set_assignable_to(db, env, self_lower) && self_upper.is_constraint_set_assignable_to(db, env, other_upper) } @@ -5073,8 +5090,8 @@ impl ConstraintAssignment { std::fmt::from_fn(move |f| { let constraint_data = storage.constraint_data(self.constraint()); - // Render supplied bounds, using the ordinary identities below only for the - // shorthand for omitted endpoints. + // Render supplied bounds, not the synthetic parameter-list defaults. The ordinary + // identities below retain the existing shorthand for omitted endpoints. let lower = constraint_data .stored_lower_bound() .map_or(Type::Never, ConstraintBound::ty); diff --git a/crates/ty_python_semantic/src/types/constraints/sequents.rs b/crates/ty_python_semantic/src/types/constraints/sequents.rs index 54a3bf54fc29b..70a9cc228c069 100644 --- a/crates/ty_python_semantic/src/types/constraints/sequents.rs +++ b/crates/ty_python_semantic/src/types/constraints/sequents.rs @@ -194,8 +194,8 @@ impl SequentMap { ) { // If the post constraint is unsatisfiable, then the antecedents contradict each other. let post_data = storage.constraint_data(post); - let post_lower = post_data.lower_bound().ty(); - let post_upper = post_data.upper_bound().ty(); + let post_lower = post_data.lower_bound(db).ty(); + let post_upper = post_data.upper_bound(db).ty(); let (when, source_order) = storage.load( db, env, @@ -233,15 +233,22 @@ impl SequentMap { ) { // If this constraint binds its typevar to `Never ≤ T ≤ object`, then the typevar can take // on any type, and the constraint is always satisfied. + // For a ParamSpec, the bottom and top parameter lists likewise allow every specialization. + // Record this fact without discarding the supplied bounds as inference evidence. + // Some internal producers still use the ordinary identities for ParamSpecs. let constraint_data = storage.constraint_data(constraint); - let lower = constraint_data.lower_bound().ty(); - let upper = constraint_data.upper_bound().ty(); - if constraint_data + let lower = constraint_data.lower_bound(db).ty(); + let upper = constraint_data.upper_bound(db).ty(); + if (constraint_data .stored_lower_bound() .is_none_or(|bound| bound.ty().is_never()) && constraint_data .stored_upper_bound() - .is_none_or(|bound| bound.ty().is_object()) + .is_none_or(|bound| bound.ty().is_object())) + || (constraint_data.typevar.is_paramspec(db) + && constraint_data.typevar.paramspec_attr(db).is_none() + && lower.is_equivalent_to(db, env, constraint_data.default_lower_bound(db)) + && upper.is_equivalent_to(db, env, constraint_data.default_upper_bound(db))) { self.add_single_tautology(constraint); return; @@ -282,7 +289,8 @@ impl SequentMap { // implication. (That is, this check directly encodes `(L ≤ T ≤ U) → (L ≤ U)` as an // implication.) - // Missing endpoints add no relation to derive. Keep defaults out of stored evidence. + // Missing endpoints add no relation to derive. In particular, do not turn a synthetic + // ParamSpec default into stored evidence through a lazy comparison with another typevar. if constraint_data.stored_lower_bound().is_none() || constraint_data.stored_upper_bound().is_none() || lower.is_never() @@ -478,8 +486,8 @@ impl SequentMap { let bound_upper_bound = bound_constraint_data.stored_upper_bound(); // A pivot can equal a missing endpoint's identity, such as an alias of Never. That // comparison is useful even though there is no stored bound to copy. - let effective_bound_lower = bound_constraint_data.lower_bound(); - let effective_bound_upper = bound_constraint_data.upper_bound(); + let effective_bound_lower = bound_constraint_data.lower_bound(db); + let effective_bound_upper = bound_constraint_data.upper_bound(db); // Transitive pivots require subtyping; classes with dynamic bases can be assignable to // unrelated types without being subtypes. @@ -708,8 +716,8 @@ impl SequentMap { let constrained_data = storage.constraint_data(constrained_constraint); let constrained_typevar = constrained_data.typevar; let constrained_identity = constrained_typevar.identity(db); - let constrained_lower_bound = constrained_data.lower_bound(); - let constrained_upper_bound = constrained_data.upper_bound(); + let constrained_lower_bound = constrained_data.lower_bound(db); + let constrained_upper_bound = constrained_data.upper_bound(db); let constrained_lower = constrained_lower_bound.ty(); let constrained_upper = constrained_upper_bound.ty(); @@ -928,13 +936,13 @@ impl SequentMap { |bound_constraint: ConstraintId, constrained_constraint: ConstraintId| { let bound_data = storage.constraint_data(bound_constraint); let bound_typevar = bound_data.typevar; - let bound_lower_bound = bound_data.lower_bound(); - let bound_upper_bound = bound_data.upper_bound(); + let bound_lower_bound = bound_data.lower_bound(db); + let bound_upper_bound = bound_data.upper_bound(db); let bound_lower = bound_lower_bound.ty(); let constrained_data = storage.constraint_data(constrained_constraint); let constrained_typevar = constrained_data.typevar; - let constrained_lower_bound = constrained_data.lower_bound(); - let constrained_upper_bound = constrained_data.upper_bound(); + let constrained_lower_bound = constrained_data.lower_bound(db); + let constrained_upper_bound = constrained_data.upper_bound(db); let constrained_lower = constrained_lower_bound.ty(); let constrained_upper = constrained_upper_bound.ty(); diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 49e13caaa790c..0f95337acd475 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -118,14 +118,14 @@ use crate::types::unpacker::{UnpackResult, fixed_sequence_elements}; use crate::types::{ BindingContext, BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType, CallableTypes, ClassType, DynamicType, GeneratorTypeMode, InferenceFlags, - InternedConstraintSet, InternedType, IntersectionBuilder, IntersectionType, KnownClass, - KnownInstanceType, KnownUnion, LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, - ParamSpecAttrKind, Parameter, Parameters, ProgramEnvironment, SentinelInstance, Signature, - SpecialFormType, SubclassOfType, Type, TypeAliasType, TypeAndQualifiers, TypeContext, - TypeQualifiers, TypeVarBoundOrConstraints, TypeVarKind, TypeVarVariance, TypingModule, - UnionAccumulator, UnionBuilder, UnionType, any_over_type, binding_type, - extract_fixed_length_iterable_element_types, infer_complete_scope_types, infer_scope_types, - is_discarded_dict_key_assignment, todo_type, + InternedConstraintSet, InternedType, IntersectionBuilder, IntersectionType, + KnownBoundMethodType, KnownClass, KnownInstanceType, KnownUnion, LiteralValueType, + LiteralValueTypeKind, MemberLookupPolicy, ParamSpecAttrKind, Parameter, Parameters, + ProgramEnvironment, SentinelInstance, Signature, SpecialFormType, SubclassOfType, Type, + TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, + TypeVarKind, TypeVarVariance, TypingModule, UnionAccumulator, UnionBuilder, UnionType, + any_over_type, binding_type, extract_fixed_length_iterable_element_types, + infer_complete_scope_types, infer_scope_types, is_discarded_dict_key_assignment, todo_type, }; use crate::{AnalysisSettings, Db, DisplaySettings, FxIndexSet, FxOrderSet, SemanticModel}; use ty_python_core::definition::{ @@ -9402,6 +9402,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &bindings, ); + // The internal constraint constructors accept ParamSpecs as well as ordinary types. + let previously_allowed_paramspec = matches!( + callable_type, + Type::KnownBoundMethod( + KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality + | KnownBoundMethodType::ConstraintSetRange + ) + ) + .then(|| { + self.context + .inference_flags + .replace(InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, true) + }); + let bindings_result = self.infer_and_check_argument_types( ArgumentsIter::from_ast(arguments), &mut call_arguments, @@ -9410,6 +9426,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { call_expression_tcx, ); + if let Some(previously_allowed_paramspec) = previously_allowed_paramspec { + self.context.inference_flags.set( + InferenceFlags::ALLOW_PARAMSPEC_TYPE_EXPR, + previously_allowed_paramspec, + ); + } + let mut bindings = match bindings_result { Ok(()) => bindings, Err(_) => { diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index b093ca923b346..2a0729fcfda09 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -4825,7 +4825,7 @@ impl<'db> Parameters<'db> { /// Return parameters that represents `(*args: object, **kwargs: object)`, the bottom signature /// (accepts any call, so subtype of all other signatures.) - fn bottom() -> Self { + pub(crate) fn bottom() -> Self { Self::new( [ Parameter::variadic(Name::new_static("args")).with_annotated_type(Type::object()),