-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[red-knot] Document public symbol type inference #15766
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
# Boundness and declaredness: public uses | ||
|
||
This document demonstrates how type-inference and diagnostics works for *public* uses of a symbol, | ||
This document demonstrates how type-inference and diagnostics work for *public* uses of a symbol, | ||
that is, a use of a symbol from another scope. If a symbol has a declared type in its local scope | ||
(e.g. `int`), we use that as the symbol's "public type" (the type of the symbol from the perspective | ||
of other scopes) even if there is a more precise local inferred type for the symbol (`Literal[1]`). | ||
|
@@ -34,20 +34,26 @@ In particular, we should raise errors in the "possibly-undeclared-and-unbound" a | |
### Declared and bound | ||
|
||
If a symbol has a declared type (`int`), we use that even if there is a more precise inferred type | ||
(`Literal[1]`), or a conflicting inferred type (`Literal[2]`): | ||
(`Literal[1]`), or a conflicting inferred type (`str` vs. `Literal[2]` below): | ||
|
||
```py path=mod.py | ||
x: int = 1 | ||
from typing import Any | ||
|
||
# error: [invalid-assignment] | ||
y: str = 2 | ||
def any() -> Any: ... | ||
|
||
a: int = 1 | ||
b: str = 2 # error: [invalid-assignment] | ||
c: Any = 3 | ||
d: int = any() | ||
``` | ||
|
||
```py | ||
from mod import x, y | ||
from mod import a, b, c, d | ||
|
||
reveal_type(x) # revealed: int | ||
reveal_type(y) # revealed: str | ||
reveal_type(a) # revealed: int | ||
reveal_type(b) # revealed: str | ||
reveal_type(c) # revealed: Any | ||
reveal_type(d) # revealed: int | ||
``` | ||
|
||
### Declared and possibly unbound | ||
|
@@ -56,21 +62,30 @@ If a symbol is declared and *possibly* unbound, we trust that other module and u | |
without raising an error. | ||
|
||
```py path=mod.py | ||
from typing import Any | ||
|
||
def any() -> Any: ... | ||
def flag() -> bool: ... | ||
|
||
x: int | ||
y: str | ||
a: int | ||
b: str | ||
c: Any | ||
d: int | ||
|
||
if flag: | ||
x = 1 | ||
# error: [invalid-assignment] | ||
y = 2 | ||
a = 1 | ||
b = 2 # error: [invalid-assignment] | ||
c = 3 | ||
d = any() | ||
``` | ||
|
||
```py | ||
from mod import x, y | ||
from mod import a, b, c, d | ||
|
||
reveal_type(x) # revealed: int | ||
reveal_type(y) # revealed: str | ||
reveal_type(a) # revealed: int | ||
reveal_type(b) # revealed: str | ||
reveal_type(c) # revealed: Any | ||
reveal_type(d) # revealed: int | ||
``` | ||
|
||
### Declared and unbound | ||
|
@@ -79,13 +94,17 @@ Similarly, if a symbol is declared but unbound, we do not raise an error. We tru | |
is available somehow and simply use the declared type. | ||
|
||
```py path=mod.py | ||
x: int | ||
from typing import Any | ||
|
||
a: int | ||
b: Any | ||
``` | ||
|
||
```py | ||
from mod import x | ||
from mod import a, b | ||
|
||
reveal_type(x) # revealed: int | ||
reveal_type(a) # revealed: int | ||
reveal_type(b) # revealed: Any | ||
``` | ||
|
||
## Possibly undeclared | ||
|
@@ -98,58 +117,61 @@ inferred types: | |
```py path=mod.py | ||
from typing import Any | ||
|
||
def any() -> Any: ... | ||
def flag() -> bool: ... | ||
|
||
x = 1 | ||
y = 2 | ||
z = 3 | ||
a = 1 | ||
b = 2 | ||
c = 3 | ||
d = any() | ||
if flag(): | ||
x: int | ||
y: Any | ||
# error: [invalid-declaration] | ||
z: str | ||
a: int | ||
b: Any | ||
c: str # error: [invalid-declaration] | ||
d: int | ||
``` | ||
|
||
```py | ||
from mod import x, y, z | ||
from mod import a, b, c, d | ||
|
||
reveal_type(x) # revealed: int | ||
reveal_type(y) # revealed: Literal[2] | Any | ||
reveal_type(z) # revealed: Literal[3] | Unknown | ||
reveal_type(a) # revealed: int | ||
reveal_type(b) # revealed: Literal[2] | Any | ||
reveal_type(c) # revealed: Literal[3] | Unknown | ||
reveal_type(d) # revealed: Any | int | ||
|
||
# External modifications of `x` that violate the declared type are not allowed: | ||
# External modifications of `a` that violate the declared type are not allowed: | ||
# error: [invalid-assignment] | ||
x = None | ||
a = None | ||
``` | ||
|
||
### Possibly undeclared and possibly unbound | ||
|
||
If a symbol is possibly undeclared and possibly unbound, we also use the union of the declared and | ||
inferred types. This case is interesting because the "possibly declared" definition might not be the | ||
same as the "possibly bound" definition (symbol `y`). Note that we raise a `possibly-unbound-import` | ||
error for both `x` and `y`: | ||
same as the "possibly bound" definition (symbol `b`). Note that we raise a `possibly-unbound-import` | ||
error for both `a` and `b`: | ||
|
||
```py path=mod.py | ||
def flag() -> bool: ... | ||
|
||
if flag(): | ||
x: Any = 1 | ||
y = 2 | ||
a: Any = 1 | ||
b = 2 | ||
else: | ||
y: str | ||
b: str | ||
``` | ||
|
||
```py | ||
# error: [possibly-unbound-import] | ||
# error: [possibly-unbound-import] | ||
from mod import x, y | ||
from mod import a, b | ||
|
||
reveal_type(x) # revealed: Literal[1] | Any | ||
reveal_type(y) # revealed: Literal[2] | str | ||
reveal_type(a) # revealed: Literal[1] | Any | ||
reveal_type(b) # revealed: Literal[2] | str | ||
|
||
# External modifications of `y` that violate the declared type are not allowed: | ||
# External modifications of `b` that violate the declared type are not allowed: | ||
# error: [invalid-assignment] | ||
y = None | ||
b = None | ||
``` | ||
|
||
### Possibly undeclared and unbound | ||
|
@@ -161,36 +183,46 @@ seems inconsistent when compared to the case just above. | |
def flag() -> bool: ... | ||
|
||
if flag(): | ||
x: int | ||
a: int | ||
``` | ||
|
||
```py | ||
# TODO: this should raise an error. Once we fix this, update the section description and the table | ||
# on top of this document. | ||
from mod import x | ||
from mod import a | ||
|
||
reveal_type(x) # revealed: int | ||
reveal_type(a) # revealed: int | ||
|
||
# External modifications to `x` that violate the declared type are not allowed: | ||
# External modifications to `a` that violate the declared type are not allowed: | ||
# error: [invalid-assignment] | ||
x = None | ||
a = None | ||
``` | ||
|
||
## Undeclared | ||
|
||
### Undeclared but bound | ||
|
||
If a symbols is undeclared, we use the union of `Unknown` with the inferred type. Note that we treat | ||
symbols that are undeclared differently from symbols that are explicitly declared as `Unknown`: | ||
|
||
```py path=mod.py | ||
x = 1 | ||
from knot_extensions import Unknown | ||
|
||
# Undeclared: | ||
a = 1 | ||
|
||
# Declared with `Unknown`: | ||
b: Unknown = 1 | ||
``` | ||
|
||
```py | ||
from mod import x | ||
from mod import a, b | ||
|
||
reveal_type(x) # revealed: Unknown | Literal[1] | ||
reveal_type(a) # revealed: Unknown | Literal[1] | ||
reveal_type(b) # revealed: Unknown | ||
Comment on lines
+221
to
+222
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This behavior is the main thing I wanted to document. |
||
|
||
# All external modifications of `x` are allowed: | ||
x = None | ||
# All external modifications of `a` are allowed: | ||
a = None | ||
``` | ||
|
||
### Undeclared and possibly unbound | ||
|
@@ -199,21 +231,25 @@ If a symbol is undeclared and *possibly* unbound, we currently do not raise an e | |
inconsistent when compared to the "possibly-undeclared-and-possibly-unbound" case. | ||
|
||
```py path=mod.py | ||
from knot_extensions import Unknown | ||
|
||
def flag() -> bool: ... | ||
|
||
if flag: | ||
x = 1 | ||
a = 1 | ||
b: Unknown = 1 | ||
``` | ||
|
||
```py | ||
# TODO: this should raise an error. Once we fix this, update the section description and the table | ||
# on top of this document. | ||
from mod import x | ||
from mod import a, b | ||
|
||
reveal_type(x) # revealed: Unknown | Literal[1] | ||
reveal_type(a) # revealed: Unknown | Literal[1] | ||
reveal_type(b) # revealed: Unknown | ||
|
||
# All external modifications of `x` are allowed: | ||
x = None | ||
# All external modifications of `a` are allowed: | ||
a = None | ||
``` | ||
|
||
### Undeclared and unbound | ||
|
@@ -222,15 +258,15 @@ If a symbol is undeclared *and* unbound, we infer `Unknown` and raise an error. | |
|
||
```py path=mod.py | ||
if False: | ||
x: int = 1 | ||
a: int = 1 | ||
``` | ||
|
||
```py | ||
# error: [unresolved-import] | ||
from mod import x | ||
from mod import a | ||
|
||
reveal_type(x) # revealed: Unknown | ||
reveal_type(a) # revealed: Unknown | ||
|
||
# Modifications allowed in this case: | ||
x = None | ||
a = None | ||
``` |
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think something that is noteworthy here is that the entire purpose of
Unknown
(as distinct fromAny
) is to distinguish a dynamic type that originates from an explicit annotation vs one that originates from lack of annotation. So the whole idea of "declared withUnknown
" is a contradiction. We have made it possible by introducingknot_extensions.Unknown
, but perhaps that is just confusing and was a mistake? It should really only be used in tests, but I would guess that in most cases we could structure the test so as to generate anUnknown
type via lack-of-annotation instead (e.g. an un-annotated function parameter).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I just realized that "declared with Unknown" is not a contradiction; it could occur anytime someone annotates with a name we can't resolve, or an invalid type expression.
Still not totally clear to me whether tests for this case are better written using
knot_extensions.Unknown
(which is clear and explicit, but not "realistic") or using an actual unknown name (which then requires suppressing the unknown-name diagnostic.) I can see good arguments both ways, so no strong feelings.