Skip to content
Merged
Show file tree
Hide file tree
Changes from 77 commits
Commits
Show all changes
117 commits
Select commit Hold shift + click to select a range
3be8257
Build context for generic classes
dcreager Mar 24, 2025
4c76cb5
Handle explicit specialization before outputting lints
dcreager Mar 24, 2025
8048604
Explicitly specialize classes
dcreager Mar 27, 2025
fe76d56
Track those structs
dcreager Mar 27, 2025
4346e7d
Add Specialization
dcreager Mar 27, 2025
2380ade
Add CallableType::Specialized
dcreager Mar 30, 2025
178ee89
Use union to hold typevar constraints
dcreager Mar 31, 2025
feb1ea9
Add Type::TypeVar variant
dcreager Mar 31, 2025
3c1ad79
Fix failing tests
dcreager Mar 31, 2025
59430b6
doc Type::TypeVar
dcreager Apr 1, 2025
77ffa80
More correct handling of final bounds/constraints
dcreager Apr 1, 2025
6f86720
use list[T] so generic funcs are callable even with Never
dcreager Apr 1, 2025
cf81967
lint
dcreager Apr 1, 2025
6615df1
Add (currently failing) narrowing tests
dcreager Apr 1, 2025
b2f5a2a
Typevars _can_ be fully static I guess
dcreager Apr 1, 2025
590680c
Simplify intersections with constrained typevars
dcreager Apr 1, 2025
e6b7d40
Merge branch 'main' into dcreager/typevar-type
dcreager Apr 1, 2025
debd60a
Fix tests
dcreager Apr 1, 2025
aa391fd
lint
dcreager Apr 1, 2025
e57e62e
Update crates/red_knot_python_semantic/src/types/type_ordering.rs
dcreager Apr 1, 2025
3df79cc
Clarify that typevar is subtype of object too
dcreager Apr 2, 2025
5b08e93
Clarify non-fully-static bounded typevars aren't subtypes
dcreager Apr 2, 2025
82e810f
Add more tests for constrained gradual typevars
dcreager Apr 2, 2025
a3d7253
Update crates/red_knot_python_semantic/src/types.rs
dcreager Apr 2, 2025
15682d5
Simplify intersections with constrained typevars w/o glossing into union
dcreager Apr 2, 2025
9e07efe
Simplify positive intersections too
dcreager Apr 2, 2025
fb63c22
Intersection of constraints is subtype of typevar
dcreager Apr 2, 2025
3459056
Better descriptions of intersections of constrained typevars
dcreager Apr 2, 2025
71d425e
Add multiple narrowing example
dcreager Apr 2, 2025
233e938
lint
dcreager Apr 2, 2025
9785202
Sort typevar constraints
dcreager Apr 2, 2025
c86af50
Remove moot todo
dcreager Apr 2, 2025
aa00895
Fold typevar match arms back into main match statement
dcreager Apr 3, 2025
d99d1d7
Remove moot comment
dcreager Apr 3, 2025
58f0995
Remove moot todo
dcreager Apr 3, 2025
42fd54a
Add more TODOs about OneOf connector
dcreager Apr 3, 2025
6bd69f1
add todos for unary/binary ops
dcreager Apr 3, 2025
1d6a917
Merge remote-tracking branch 'origin/main' into dcreager/typevar-type
dcreager Apr 3, 2025
37692f1
Merge remote-tracking branch 'origin/dcreager/typevar-type' into dcre…
dcreager Apr 3, 2025
3869dc6
Fix tests
dcreager Apr 3, 2025
0c1745b
Fix tests better
dcreager Apr 3, 2025
8bdd9e2
Support unary and binary ops
dcreager Apr 3, 2025
39244dd
Merge remote-tracking branch 'origin/main' into dcreager/typevar-type
dcreager Apr 3, 2025
5694b3d
Merge remote-tracking branch 'origin/main' into dcreager/special-class
dcreager Apr 3, 2025
123b920
Fix merge conflicts
dcreager Apr 3, 2025
9e1767f
Rename to SpecializedCallable{,Type}
dcreager Apr 3, 2025
23ac0b6
Merge branch 'dcreager/typevar-type' into dcreager/special-class
dcreager Apr 3, 2025
6b27947
Specialize types
dcreager Apr 3, 2025
669aa21
Merge remote-tracking branch 'origin/main' into dcreager/special-class
dcreager Apr 3, 2025
575998e
Fix tests in ide crate
dcreager Apr 3, 2025
f02fefa
Add GenericClass, NonGenericClass, and GenericAlias
dcreager Mar 30, 2025
cc3a3df
Add GenericAlias
dcreager Apr 4, 2025
18af8b6
Apply specializations
dcreager Apr 4, 2025
af52fd1
Use class literal for self member lookups
dcreager Apr 4, 2025
d3fd822
Display specializations and generic aliases
dcreager Apr 4, 2025
aa64990
Specialize generic base class in generic subclass
dcreager Apr 4, 2025
e41f889
Merge branch 'main' into dcreager/special-class
dcreager Apr 4, 2025
7eb7c28
Only specialize function literals, not all callables
dcreager Apr 7, 2025
adb4aba
Fix descriptor protocol tests
dcreager Apr 7, 2025
968e637
Merge branch 'main' into dcreager/special-class
dcreager Apr 7, 2025
fd7914a
Remove unused methods
dcreager Apr 7, 2025
7ebda98
clippy
dcreager Apr 7, 2025
6257e89
Apply specialization to signature in-place
dcreager Apr 7, 2025
78cd92a
typo
dcreager Apr 7, 2025
530e2bc
Fix test case
dcreager Apr 7, 2025
637fd48
lint
dcreager Apr 7, 2025
ec5a588
Defer class definitions with string literals in base classes
dcreager Apr 7, 2025
27cb208
fix lint fix
dcreager Apr 7, 2025
ea12548
skip failing test for now
dcreager Apr 7, 2025
c376dad
clippy and lint
dcreager Apr 7, 2025
311dc59
add xfail for specializing to union of constraints
dcreager Apr 7, 2025
5fc8425
specialize property types
dcreager Apr 7, 2025
7aaeb47
Don't specialize function twice
dcreager Apr 7, 2025
43554e2
fix docs
dcreager Apr 7, 2025
aec3392
Add (optional) generic context to overload signature
dcreager Apr 8, 2025
595ecae
Super-basic inference at call sites
dcreager Apr 8, 2025
58830fa
Add comment
dcreager Apr 8, 2025
0d656db
Merge remote-tracking branch 'origin/main' into dcreager/special-class
dcreager Apr 9, 2025
7c3405a
Better TODO fallback type
dcreager Apr 9, 2025
6593d90
Generic aliases are literals in type display
dcreager Apr 9, 2025
7ca6a60
Explain self_instance not being specialized
dcreager Apr 9, 2025
dea493e
Comment other non-specializations
dcreager Apr 9, 2025
048bb8b
Add xfail for generic method inside generic class
dcreager Apr 9, 2025
06859fa
More Python-like displays for specializations
dcreager Apr 9, 2025
d138405
Narrow type(generic) better
dcreager Apr 9, 2025
0997eca
Better todos
dcreager Apr 9, 2025
6ab1b90
Add TODO about property test data
dcreager Apr 9, 2025
bcb147f
lint
dcreager Apr 9, 2025
78f73fc
Merge branch 'dcreager/special-class' into dcreager/infer-function-calls
dcreager Apr 9, 2025
145a776
Merge branch 'main' into dcreager/infer-function-calls
dcreager Apr 9, 2025
3a7b638
Clean up union examples
dcreager Apr 9, 2025
cccc77d
Use with_self instead of prepend_synthetic
dcreager Apr 9, 2025
993c6e3
Use try_call_dunder_with_policy
dcreager Apr 9, 2025
2169282
Try to infer class specialization from constructor arguments
dcreager Apr 9, 2025
5a4bd28
TODO: constructor is getting specialized with unknown
dcreager Apr 9, 2025
756ce9a
Add uninitialized instance variant
dcreager Apr 10, 2025
1f67883
Create uninitialized instance when constructing
dcreager Apr 10, 2025
3cf9559
Merge branch 'main' into dcreager/infer-function-calls
dcreager Apr 10, 2025
beb64b8
clippy
dcreager Apr 10, 2025
c1e3470
Only extract specializations for generic classes
dcreager Apr 10, 2025
4bd8128
Use identity specialization for new/init
dcreager Apr 11, 2025
3734599
Revert new instance variant
dcreager Apr 11, 2025
836d02c
Remove unneeded helper method
dcreager Apr 11, 2025
0744540
Add comment explaining the identity specialization
dcreager Apr 11, 2025
701cfd1
Merge branch 'main' into dcreager/infer-function-calls
dcreager Apr 11, 2025
d3067b0
Revert unneeded policy change
dcreager Apr 11, 2025
4823f43
Remove TODOs
dcreager Apr 15, 2025
87f3f62
Add TODO about nested contexts
dcreager Apr 15, 2025
beed962
Add comment about potential class method inference
dcreager Apr 15, 2025
94bf3f6
Real function body
dcreager Apr 15, 2025
8f43e30
Remove unneeded special case lookup logic
dcreager Apr 16, 2025
f38b43e
Add tests for new and init
dcreager Apr 16, 2025
54fb8a2
Merge branch 'main' into dcreager/infer-function-calls
dcreager Apr 16, 2025
b852485
Merge remote-tracking branch 'origin/dcreager/infer-function-calls' i…
dcreager Apr 16, 2025
394c762
Add failing test case for generic __init__
dcreager Apr 16, 2025
347547b
Merge branch 'main' into dcreager/infer-function-calls
dcreager Apr 16, 2025
523ddcb
clean up known class detection
dcreager Apr 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 5 additions & 15 deletions crates/red_knot_python_semantic/resources/mdtest/call/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,29 +410,19 @@ def does_nothing[T](f: T) -> T:

class C:
@classmethod
# TODO: no error should be emitted here (needs support for generics)
# error: [invalid-argument-type]
@does_nothing
def f1(cls: type[C], x: int) -> str:
return "a"
# TODO: no error should be emitted here (needs support for generics)
# error: [invalid-argument-type]

@does_nothing
@classmethod
def f2(cls: type[C], x: int) -> str:
return "a"

# TODO: All of these should be `str` (and not emit an error), once we support generics

# error: [call-non-callable]
reveal_type(C.f1(1)) # revealed: Unknown
# error: [call-non-callable]
reveal_type(C().f1(1)) # revealed: Unknown

# error: [call-non-callable]
reveal_type(C.f2(1)) # revealed: Unknown
# error: [call-non-callable]
reveal_type(C().f2(1)) # revealed: Unknown
reveal_type(C.f1(1)) # revealed: str
reveal_type(C().f1(1)) # revealed: str
reveal_type(C.f2(1)) # revealed: str
reveal_type(C().f2(1)) # revealed: str
```

[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,13 @@ class C[T]: ...
A class that inherits from a generic class, and fills its type parameters with typevars, is generic:

```py
# TODO: no error
# error: [non-subscriptable]
class D[U](C[U]): ...
```

A class that inherits from a generic class, but fills its type parameters with concrete types, is
_not_ generic:

```py
# TODO: no error
# error: [non-subscriptable]
class E(C[int]): ...
```

Expand Down Expand Up @@ -57,33 +53,85 @@ class D(C[T]): ...

(Examples `E` and `F` from above do not have analogues in the legacy syntax.)

## Inferring generic class parameters
## Specializing generic classes explicitly

The type parameter can be specified explicitly:

```py
class C[T]:
x: T

# TODO: no error
# TODO: revealed: C[int]
# error: [non-subscriptable]
reveal_type(C[int]()) # revealed: C
reveal_type(C[int]()) # revealed: C[int]
```

The specialization must match the generic types:

```py
# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2"
reveal_type(C[int, int]()) # revealed: Unknown
```

If the type variable has an upper bound, the specialized type must satisfy that bound:

```py
class Bounded[T: int]: ...
class BoundedByUnion[T: int | str]: ...
class IntSubclass(int): ...

reveal_type(Bounded[int]()) # revealed: Bounded[int]
reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass]

# error: [invalid-argument-type] "Object of type `str` cannot be assigned to parameter 1 (`T`) of class `Bounded`; expected type `int`"
reveal_type(Bounded[str]()) # revealed: Unknown

# error: [invalid-argument-type] "Object of type `int | str` cannot be assigned to parameter 1 (`T`) of class `Bounded`; expected type `int`"
reveal_type(Bounded[int | str]()) # revealed: Unknown

reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int]
reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion[IntSubclass]
reveal_type(BoundedByUnion[str]()) # revealed: BoundedByUnion[str]
reveal_type(BoundedByUnion[int | str]()) # revealed: BoundedByUnion[int | str]
```

If the type variable is constrained, the specialized type must satisfy those constraints:

```py
class Constrained[T: (int, str)]: ...

reveal_type(Constrained[int]()) # revealed: Constrained[int]

# TODO: error: [invalid-argument-type]
# TODO: revealed: Unknown
reveal_type(Constrained[IntSubclass]()) # revealed: Constrained[IntSubclass]

reveal_type(Constrained[str]()) # revealed: Constrained[str]

# TODO: error: [invalid-argument-type]
# TODO: revealed: Unknown
reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str]

# error: [invalid-argument-type] "Object of type `object` cannot be assigned to parameter 1 (`T`) of class `Constrained`; expected type `int | str`"
reveal_type(Constrained[object]()) # revealed: Unknown
```

## Inferring generic class parameters

We can infer the type parameter from a type context:

```py
class C[T]:
x: T

c: C[int] = C()
# TODO: revealed: C[int]
reveal_type(c) # revealed: C
reveal_type(c) # revealed: C[Unknown]
```

The typevars of a fully specialized generic class should no longer be visible:

```py
# TODO: revealed: int
reveal_type(c.x) # revealed: T
reveal_type(c.x) # revealed: Unknown
```

If the type parameter is not specified explicitly, and there are no constraints that let us infer a
Expand All @@ -92,15 +140,13 @@ specific type, we infer the typevar's default type:
```py
class D[T = int]: ...

# TODO: revealed: D[int]
reveal_type(D()) # revealed: D
reveal_type(D()) # revealed: D[int]
```

If a typevar does not provide a default, we use `Unknown`:

```py
# TODO: revealed: C[Unknown]
reveal_type(C()) # revealed: C
reveal_type(C()) # revealed: C[Unknown]
```

If the type of a constructor parameter is a class typevar, we can use that to infer the type
Expand All @@ -111,7 +157,7 @@ class E[T]:
def __init__(self, x: T) -> None: ...

# TODO: revealed: E[int] or E[Literal[1]]
reveal_type(E(1)) # revealed: E
reveal_type(E(1)) # revealed: E[Unknown]
```

The types inferred from a type context and from a constructor parameter must be consistent with each
Expand All @@ -131,17 +177,10 @@ propagate through:
class Base[T]:
x: T | None = None

# TODO: no error
# error: [non-subscriptable]
class Sub[U](Base[U]): ...

# TODO: no error
# TODO: revealed: int | None
# error: [non-subscriptable]
reveal_type(Base[int].x) # revealed: T | None
# TODO: revealed: int | None
# error: [non-subscriptable]
reveal_type(Sub[int].x) # revealed: T | None
reveal_type(Base[int].x) # revealed: int | None
reveal_type(Sub[int].x) # revealed: int | None
```

## Cyclic class definition
Expand All @@ -155,8 +194,6 @@ Here, `Sub` is not a generic class, since it fills its superclass's type paramet

```pyi
class Base[T]: ...
# TODO: no error
# error: [non-subscriptable]
class Sub(Base[Sub]): ...

reveal_type(Sub) # revealed: Literal[Sub]
Expand All @@ -168,9 +205,6 @@ A similar case can work in a non-stub file, if forward references are stringifie

```py
class Base[T]: ...

# TODO: no error
# error: [non-subscriptable]
class Sub(Base["Sub"]): ...

reveal_type(Sub) # revealed: Literal[Sub]
Expand All @@ -183,8 +217,6 @@ In a non-stub file, without stringified forward references, this raises a `NameE
```py
class Base[T]: ...

# TODO: the unresolved-reference error is correct, the non-subscriptable is not
# error: [non-subscriptable]
# error: [unresolved-reference]
class Sub(Base[Sub]): ...
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,25 +51,10 @@ the inferred type to e.g. `int`.
def f[T](x: T) -> T:
return x

# TODO: no error
# TODO: revealed: int or Literal[1]
# error: [invalid-argument-type]
reveal_type(f(1)) # revealed: T

# TODO: no error
# TODO: revealed: float
# error: [invalid-argument-type]
reveal_type(f(1.0)) # revealed: T

# TODO: no error
# TODO: revealed: bool or Literal[true]
# error: [invalid-argument-type]
reveal_type(f(True)) # revealed: T

# TODO: no error
# TODO: revealed: str or Literal["string"]
# error: [invalid-argument-type]
reveal_type(f("string")) # revealed: T
reveal_type(f(1)) # revealed: Literal[1]
reveal_type(f(1.0)) # revealed: float
reveal_type(f(True)) # revealed: Literal[True]
reveal_type(f("string")) # revealed: Literal["string"]
```

## Inferring “deep” generic parameter types
Expand All @@ -82,7 +67,7 @@ def f[T](x: list[T]) -> T:
return x[0]

# TODO: revealed: float
reveal_type(f([1.0, 2.0])) # revealed: T
reveal_type(f([1.0, 2.0])) # revealed: Unknown
```

## Typevar constraints
Expand Down Expand Up @@ -162,61 +147,39 @@ parameters simultaneously.
def two_params[T](x: T, y: T) -> T:
return x

# TODO: no error
# TODO: revealed: str
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not actually convinced there's a strong rationale for this TODO. I don't think there is any function body for two_params that would type-check and allow it to return anything other than "a" or "b". Same for a number of similar TODOs below.

But I could be wrong; not asking to have these TODOs removed in this PR, just putting it out there that I'm not convinced :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if revealed type of return value of function most be Literal["a"] | Literal["b"], then it's incorrect type checking, since there is only one type var: T.

in other words, it should get Literal["a"] only when calling by two_params("a", "a"). but when calling two_params("a", "b"), both of "a" and "b" are different shape of literal strings and it should show error. but if revealed type became str, the call of two_params("a", "b") is acceptable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree. There's nothing special about a union type in a set-theoretic type system. Literal["a", "b"] is a well-defined single type, just as much as str is, and the single typevar T can bind to the union type just as well as it can bind to any other type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and the single typevar T can bind to the union type just as well as it can bind to any other type.

IIUC, T would be Literal["a", "b"] when call it by reveal_type(two_params("a", "b")).
It now makes sense to me, thanks for clarification.

What I was thinking is that when first parameter passed ("a"), then T typevar locks to Literal["a"] and by second parameter which is Literal["b"] it will invalidate the T. Actually, I had rust backgrounding of generics in my mind:

fn two_params<T>(a:T, b:T) -> T {
    a
}

fn main() {
    two_params("a", 1);  // error[E0308]: mismatched types
}

BTW, it's valid in Python generics.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I was thinking is that when first parameter passed ("a"), then T typevar locks to Literal["a"] and by second parameter which is Literal["b"] it will invalidate the T.

This is a good way to think about how the implementation works here, and how it will need to grow to support some of the other mdtests that still have TODOs.

Right now, the only "solving" that we do is to see that the param is a typevar, and the argument is "some type", and add to the pending specialization that the typevar maps to the type. But if we've already seen that typevar in a different parameter, instead of replacing the previous type, or requiring the previous and new types to be the same (as you thought might be the case), we merge them together into a union. This is the only "unification" that the implementation in this PR does.

This first example in this section is one that this PR doesn't addres, where we'll need to not just blindly union everything together — the type annotation is meant to be an actual restriction that we should enforce.

Copy link
Contributor

@carljm carljm Apr 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think this is a very useful example, thank you! But (to rephrase what @dcreager already mentioned), no Python type checker actually supports the general principle that if two_params(T1, T2) is OK, therefore f = curry_two_params(T1); f(T2) should also be OK. In both mypy and pyright, two_params("a", 1) is fine (T is solved to int | str), but f = curry_two_params("a"); f(1) is a type error (f is (str) -> str).

In other words, there's no difference in principle here, just a difference in semi-arbitrary widening heuristics, that makes the same issue appear at a different level of type granularity.

Perhaps the mypy/pyright approach is the best one available, in practice! But it would certainly be satisfying if we can find a more general answer that doesn't depend on heuristics.

One approach to solve this kind of issue is unification of a constraint system across the entire scope. But this is quite hard to reconcile with flow typing.

Copy link
Contributor

@carljm carljm Apr 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is another form of the same problem:

def f(x: T) -> list[T]:
    return [x]

list_of_what = f(1)
reveal_type(list_of_what)  # mypy/pyright say `list[int]`
list_of_what.append(2)  # mypy/pyright are ok with this
list_of_what.append("foo")  # but not this, which is sort of arbitrary

The common thread in the examples is that a type is returned from a generic function in which a type parameter appears in an invariant/contravariant position.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this another place where we'd lean on OneOf? The type of list_of_what (and maybe the type of a literal in general?) would be list[OneOf[Literal[1], int]]. i.e. OneOf might be shaping up to be the way that we handle "cross-expression" constraints in general.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think it's plausible that we can leverage gradual typing here, though I'm not sure we need OneOf in this case, I think "union with Unknown" suffices. (Using OneOf still requires us to make the arbitrary judgment that you aren't supposed to add strings to this list.) The union Unknown | Literal[1] expresses the gradual type "some type at least as large as Literal[1] but possibly larger", which has the nice property that if you ever try to use it somewhere you can't use an integer, it'll error -- but it'll allow things other than Literal[1] to be assigned to it.

I think at least two things we would need to make this approach work would be narrowing such that if you have a list l of type list[Unknown | Literal[1]] and you do l.append("foo"), we subsequently consider it to be of type list[Unknown | Literal[1] | Literal["foo"].

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know I said in the first comment here that I wasn't actually saying we should remove all these TODOs asking for wider types... but I kind of think we should? Unless someone wants to argue that we definitely want this change. Having TODOs around suggests to a contributor that a PR to widen all these types would be welcomed, when I'm not sure it would/should be.

Removed the TODOs. If we decide to make this change, test failures will tell us all the places that need to be updated.

# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(two_params("a", "b")) # revealed: T
reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"]

# TODO: no error
# TODO: revealed: str | int
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(two_params("a", 1)) # revealed: T
reveal_type(two_params("a", 1)) # revealed: Literal["a", 1]
```

```py
def param_with_union[T](x: T | int, y: T) -> T:
return y

# TODO: no error
# TODO: revealed: str
# error: [invalid-argument-type]
reveal_type(param_with_union(1, "a")) # revealed: T
reveal_type(param_with_union(1, "a")) # revealed: Literal["a"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case is a bit subtle, I had to actually try to write a body for param_with_union in which we could return 1 from this call, before realizing it's not possible because T | int is not assignable to T, so before we can return x we have to exclude int from its type, meaning this call can't return 1.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Trying to write that function also highlighted a bug that currently we don't think T & ~int is assignable to T, but it should be.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some extra tests that exercise the logic I actually care about here (namely, that we try to infer the smallest specialization that satisfies all the constraints). I went with a function with signature (T | None) -> T. I think there's technically no way to correctly instantiate that signature, so for now I'm accepting the [invalid-return-type] error. Once we support list[T], I can make it return that.


# TODO: no error
# TODO: revealed: str
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(param_with_union("a", "a")) # revealed: T
reveal_type(param_with_union("a", "a")) # revealed: Literal["a"]

# TODO: no error
# TODO: revealed: int
# error: [invalid-argument-type]
reveal_type(param_with_union(1, 1)) # revealed: T
reveal_type(param_with_union(1, 1)) # revealed: Literal[1]

# TODO: no error
# TODO: revealed: str | int
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(param_with_union("a", 1)) # revealed: T
reveal_type(param_with_union("a", 1)) # revealed: Literal["a", 1]
```

```py
def tuple_param[T, S](x: T | S, y: tuple[T, S]) -> tuple[T, S]:
return y

# TODO: no error
# TODO: revealed: tuple[str, int]
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[T, S]
reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]

# TODO: no error
# TODO: revealed: tuple[str, int]
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[T, S]
reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]
```

## Inferring nested generic function calls
Expand All @@ -231,15 +194,9 @@ def f[T](x: T) -> tuple[T, int]:
def g[T](x: T) -> T | None:
return x

# TODO: no error
# TODO: revealed: tuple[str | None, int]
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(f(g("a"))) # revealed: tuple[T, int]
reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int]

# TODO: no error
# TODO: revealed: tuple[str, int] | None
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(g(f("a"))) # revealed: T | None
reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None
```
Loading
Loading