[TSP] Found via differential testing comparing Pyright against Pyrefly running as an external type server over the Type Server Protocol (TSP). Surfaced downstream in Pylance: microsoft/pylance-release#8091
Summary
When converting a type[X] type to TSP, Pyrefly only marks the result INSTANTIABLE when the inner type is a class. For type[T] where T is a TypeVar (or any other non-class inner type), the type[...] wrapper is silently dropped: the TSP consumer receives a bare TypeVar, indistinguishable from a plain T.
Repro
from typing import TypeVar
T = TypeVar("T")
def f(object_type: type[T]) -> type[T]: ...
Hovering f over TSP renders (via Pylance):
(function) def f(object_type: T@f) -> T@f
Expected (Pyright built-in):
(function) def f(object_type: type[T@f]) -> type[T@f]
type[int] (a concrete class) renders correctly as type[int]; only type[TypeVar] (and similar non-class inner types, e.g. type[Self] for a user-defined Self) is affected.
Root cause
pyrefly/lib/tsp/type_conversion.rs, the PyreflyType::Type(inner) arm:
// --- type[X] wrapper ---
PyreflyType::Type(inner) => {
let inner_tsp = self.convert(inner);
// Return the inner type but mark it as instantiable
match inner_tsp {
TspType::Class(mut c) => {
c.flags = TypeFlags::INSTANTIABLE;
TspType::Class(c)
}
other => other, // <-- non-Class inner (e.g. a TypeVar) loses the type[...] wrapper
}
}
For a TspType::Var (and any non-Class variant), the other => other branch returns the inner type unchanged. Its flags were set to TypeFlags::NONE by make_typevar_declared (which hardcodes flags: TypeFlags::NONE), so the emitted type[T] is byte-for-byte identical to a plain T. The consumer has no signal to reconstruct the type[...] wrapper.
The TSP protocol supports this: TypeVarType extends the base Type and therefore carries a flags field, and TypeFlags::INSTANTIABLE (1 << 0) is a valid flag for it.
Suggested fix
In the other => other branch, set TypeFlags::INSTANTIABLE on the converted inner type's flags (as is already done for the TspType::Class case) so consumers can distinguish type[T] from T and reconstruct the wrapper.
Environment
- Pyrefly:
1.2.0-dev.1 (built from main; also reported by users on 1.1.1)
- OS: Windows
[TSP] Found via differential testing comparing Pyright against Pyrefly running as an external type server over the Type Server Protocol (TSP). Surfaced downstream in Pylance: microsoft/pylance-release#8091
Summary
When converting a
type[X]type to TSP, Pyrefly only marks the resultINSTANTIABLEwhen the inner type is a class. Fortype[T]whereTis aTypeVar(or any other non-class inner type), thetype[...]wrapper is silently dropped: the TSP consumer receives a bareTypeVar, indistinguishable from a plainT.Repro
Hovering
fover TSP renders (via Pylance):Expected (Pyright built-in):
type[int](a concrete class) renders correctly astype[int]; onlytype[TypeVar](and similar non-class inner types, e.g.type[Self]for a user-definedSelf) is affected.Root cause
pyrefly/lib/tsp/type_conversion.rs, thePyreflyType::Type(inner)arm:For a
TspType::Var(and any non-Classvariant), theother => otherbranch returns the inner type unchanged. Itsflagswere set toTypeFlags::NONEbymake_typevar_declared(which hardcodesflags: TypeFlags::NONE), so the emittedtype[T]is byte-for-byte identical to a plainT. The consumer has no signal to reconstruct thetype[...]wrapper.The TSP protocol supports this:
TypeVarTypeextends the baseTypeand therefore carries aflagsfield, andTypeFlags::INSTANTIABLE(1 << 0) is a valid flag for it.Suggested fix
In the
other => otherbranch, setTypeFlags::INSTANTIABLEon the converted inner type'sflags(as is already done for theTspType::Classcase) so consumers can distinguishtype[T]fromTand reconstruct the wrapper.Environment
1.2.0-dev.1(built frommain; also reported by users on1.1.1)