-
-
Notifications
You must be signed in to change notification settings - Fork 3k
New semantic analyzer: Support multiple passes over functions. #6280
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
Changes from 21 commits
d915d4a
0e44eb4
272bfee
e01e70e
b98838c
6c119c1
d8b1e02
7b32240
5482ddc
fe24b18
062f771
8d9656a
cb6023b
c632464
36f992f
1163340
89db035
9e279d3
8df4445
4c6dbd4
37a209d
e47f748
1579600
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -125,15 +125,6 @@ | |
'builtins.bytearray': 'builtins.str', | ||
}) | ||
|
||
# When analyzing a function, should we analyze the whole function in one go, or | ||
# should we only perform one phase of the analysis? The latter is used for | ||
# nested functions. In the first phase we add the function to the symbol table | ||
# but don't process body. In the second phase we process function body. This | ||
# way we can have mutually recursive nested functions. | ||
FUNCTION_BOTH_PHASES = 0 # type: Final # Everything in one go | ||
FUNCTION_FIRST_PHASE_POSTPONE_SECOND = 1 # type: Final # Add to symbol table but postpone body | ||
FUNCTION_SECOND_PHASE = 2 # type: Final # Only analyze body | ||
|
||
# Map from the full name of a missing definition to the test fixture (under | ||
# test-data/unit/fixtures/) that provides the definition. This is used for | ||
# generating better error messages when running mypy tests only. | ||
|
@@ -193,14 +184,6 @@ class NewSemanticAnalyzer(NodeVisitor[None], | |
# Stack of functions being analyzed | ||
function_stack = None # type: List[FuncItem] | ||
|
||
# Status of postponing analysis of nested function bodies. By using this we | ||
# can have mutually recursive nested functions. Values are FUNCTION_x | ||
# constants. Note that separate phasea are not used for methods. | ||
postpone_nested_functions_stack = None # type: List[int] | ||
# Postponed functions collected if | ||
# postpone_nested_functions_stack[-1] == FUNCTION_FIRST_PHASE_POSTPONE_SECOND. | ||
postponed_functions_stack = None # type: List[List[Node]] | ||
|
||
loop_depth = 0 # Depth of breakable loops | ||
cur_mod_id = '' # Current module id (or None) (phase 2) | ||
is_stub_file = False # Are we analyzing a stub file? | ||
|
@@ -228,6 +211,8 @@ def __init__(self, | |
errors: Report analysis errors using this instance | ||
""" | ||
self.locals = [None] | ||
# Saved namespaces from previous passes. | ||
self.saved_locals = {} # type: Dict[FuncItem, SymbolTable] | ||
self.imports = set() | ||
self.type = None | ||
self.type_stack = [] | ||
|
@@ -243,8 +228,6 @@ def __init__(self, | |
# missing name in these namespaces, we need to defer the current analysis target, | ||
# since it's possible that the name will be there once the namespace is complete. | ||
self.incomplete_namespaces = incomplete_namespaces | ||
self.postpone_nested_functions_stack = [FUNCTION_BOTH_PHASES] | ||
self.postponed_functions_stack = [] | ||
self.all_exports = [] # type: List[str] | ||
# Map from module id to list of explicitly exported names (i.e. names in __all__). | ||
self.export_map = {} # type: Dict[str, List[str]] | ||
|
@@ -451,67 +434,56 @@ def add_func_to_symbol_table(self, func: Union[FuncDef, OverloadedFuncDef]) -> N | |
self.add_symbol(func.name(), func, func) | ||
|
||
def _visit_func_def(self, defn: FuncDef) -> None: | ||
phase_info = self.postpone_nested_functions_stack[-1] | ||
if phase_info != FUNCTION_SECOND_PHASE: | ||
self.function_stack.append(defn) | ||
# First phase of analysis for function. | ||
if not defn._fullname: | ||
defn._fullname = self.qualified_name(defn.name()) | ||
if defn.type: | ||
assert isinstance(defn.type, CallableType) | ||
self.update_function_type_variables(defn.type, defn) | ||
self.function_stack.pop() | ||
self.function_stack.append(defn) | ||
|
||
defn.is_conditional = self.block_depth[-1] > 0 | ||
if defn.type: | ||
assert isinstance(defn.type, CallableType) | ||
self.update_function_type_variables(defn.type, defn) | ||
self.function_stack.pop() | ||
|
||
if self.is_class_scope(): | ||
# Method definition | ||
assert self.type is not None | ||
defn.info = self.type | ||
if defn.type is not None and defn.name() in ('__init__', '__init_subclass__'): | ||
assert isinstance(defn.type, CallableType) | ||
if isinstance(defn.type.ret_type, AnyType): | ||
defn.type = defn.type.copy_modified(ret_type=NoneTyp()) | ||
self.prepare_method_signature(defn, self.type) | ||
|
||
# Analyze function signature and initializers in the first phase | ||
# (at least this mirrors what happens at runtime). | ||
with self.tvar_scope_frame(self.tvar_scope.method_frame()): | ||
if defn.type: | ||
self.check_classvar_in_signature(defn.type) | ||
defn.is_conditional = self.block_depth[-1] > 0 | ||
|
||
if self.is_class_scope(): | ||
# Method definition | ||
assert self.type is not None | ||
defn.info = self.type | ||
if defn.type is not None and defn.name() in ('__init__', '__init_subclass__'): | ||
assert isinstance(defn.type, CallableType) | ||
if isinstance(defn.type.ret_type, AnyType): | ||
defn.type = defn.type.copy_modified(ret_type=NoneTyp()) | ||
self.prepare_method_signature(defn, self.type) | ||
|
||
# Analyze function signature and initializers first. | ||
with self.tvar_scope_frame(self.tvar_scope.method_frame()): | ||
if defn.type: | ||
self.check_classvar_in_signature(defn.type) | ||
assert isinstance(defn.type, CallableType) | ||
# Signature must be analyzed in the surrounding scope so that | ||
# class-level imported names and type variables are in scope. | ||
analyzer = self.type_analyzer() | ||
defn.type = analyzer.visit_callable_type(defn.type, nested=False) | ||
self.add_type_alias_deps(analyzer.aliases_used) | ||
self.check_function_signature(defn) | ||
if isinstance(defn, FuncDef): | ||
assert isinstance(defn.type, CallableType) | ||
# Signature must be analyzed in the surrounding scope so that | ||
# class-level imported names and type variables are in scope. | ||
analyzer = self.type_analyzer() | ||
defn.type = analyzer.visit_callable_type(defn.type, nested=False) | ||
self.add_type_alias_deps(analyzer.aliases_used) | ||
self.check_function_signature(defn) | ||
if isinstance(defn, FuncDef): | ||
assert isinstance(defn.type, CallableType) | ||
defn.type = set_callable_name(defn.type, defn) | ||
for arg in defn.arguments: | ||
if arg.initializer: | ||
arg.initializer.accept(self) | ||
|
||
if phase_info == FUNCTION_FIRST_PHASE_POSTPONE_SECOND: | ||
# Postpone this function (for the second phase). | ||
self.postponed_functions_stack[-1].append(defn) | ||
return | ||
if phase_info != FUNCTION_FIRST_PHASE_POSTPONE_SECOND: | ||
# Second phase of analysis for function. | ||
self.analyze_function(defn) | ||
if defn.is_coroutine and isinstance(defn.type, CallableType): | ||
if defn.is_async_generator: | ||
# Async generator types are handled elsewhere | ||
pass | ||
else: | ||
# A coroutine defined as `async def foo(...) -> T: ...` | ||
# has external return type `Coroutine[Any, Any, T]`. | ||
any_type = AnyType(TypeOfAny.special_form) | ||
ret_type = self.named_type_or_none('typing.Coroutine', | ||
[any_type, any_type, defn.type.ret_type]) | ||
assert ret_type is not None, "Internal error: typing.Coroutine not found" | ||
defn.type = defn.type.copy_modified(ret_type=ret_type) | ||
defn.type = set_callable_name(defn.type, defn) | ||
for arg in defn.arguments: | ||
if arg.initializer: | ||
arg.initializer.accept(self) | ||
|
||
self.analyze_function(defn) | ||
if defn.is_coroutine and isinstance(defn.type, CallableType): | ||
if defn.is_async_generator: | ||
# Async generator types are handled elsewhere | ||
pass | ||
else: | ||
# A coroutine defined as `async def foo(...) -> T: ...` | ||
# has external return type `Coroutine[Any, Any, T]`. | ||
any_type = AnyType(TypeOfAny.special_form) | ||
ret_type = self.named_type_or_none('typing.Coroutine', | ||
[any_type, any_type, defn.type.ret_type]) | ||
assert ret_type is not None, "Internal error: typing.Coroutine not found" | ||
defn.type = defn.type.copy_modified(ret_type=ret_type) | ||
|
||
def prepare_method_signature(self, func: FuncDef, info: TypeInfo) -> None: | ||
"""Check basic signature validity and tweak annotation of self/cls argument.""" | ||
|
@@ -630,6 +602,9 @@ def analyze_overload_sigs_and_impl( | |
types = [] | ||
non_overload_indexes = [] | ||
impl = None # type: Optional[OverloadPart] | ||
if defn.impl: | ||
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. Ew the way this works is ugly (the existing logic, not this change in particular). Add TODO item about not modifying the |
||
# We are visiting this second time. | ||
defn.items.append(defn.impl) | ||
for i, item in enumerate(defn.items): | ||
if i != 0: | ||
# Assume that the first item was already visited | ||
|
@@ -780,7 +755,7 @@ def analyze_function(self, defn: FuncItem) -> None: | |
a = self.type_analyzer() | ||
a.bind_function_type_variables(cast(CallableType, defn.type), defn) | ||
self.function_stack.append(defn) | ||
self.enter() | ||
self.enter(defn) | ||
for arg in defn.arguments: | ||
self.add_local(arg.variable, defn) | ||
|
||
|
@@ -790,18 +765,7 @@ def analyze_function(self, defn: FuncItem) -> None: | |
if is_method and not defn.is_static and not defn.is_class and defn.arguments: | ||
defn.arguments[0].variable.is_self = True | ||
|
||
# First analyze body of the function but ignore nested functions. | ||
self.postpone_nested_functions_stack.append(FUNCTION_FIRST_PHASE_POSTPONE_SECOND) | ||
self.postponed_functions_stack.append([]) | ||
defn.body.accept(self) | ||
|
||
# Analyze nested functions (if any) as a second phase. | ||
self.postpone_nested_functions_stack[-1] = FUNCTION_SECOND_PHASE | ||
for postponed in self.postponed_functions_stack[-1]: | ||
postponed.accept(self) | ||
self.postpone_nested_functions_stack.pop() | ||
self.postponed_functions_stack.pop() | ||
|
||
self.leave() | ||
self.function_stack.pop() | ||
|
||
|
@@ -977,12 +941,10 @@ def enter_class(self, info: TypeInfo) -> None: | |
self.type_stack.append(self.type) | ||
self.locals.append(None) # Add class scope | ||
self.block_depth.append(-1) # The class body increments this to 0 | ||
self.postpone_nested_functions_stack.append(FUNCTION_BOTH_PHASES) | ||
self.type = info | ||
|
||
def leave_class(self) -> None: | ||
""" Restore analyzer state. """ | ||
self.postpone_nested_functions_stack.pop() | ||
self.block_depth.pop() | ||
self.locals.pop() | ||
self.type = self.type_stack.pop() | ||
|
@@ -3797,8 +3759,12 @@ def qualified_name(self, n: str) -> str: | |
base = self.cur_mod_id | ||
return base + '.' + n | ||
|
||
def enter(self) -> None: | ||
self.locals.append(SymbolTable()) | ||
def enter(self, function: Optional[FuncItem] = None) -> None: | ||
if function: | ||
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. Add docstring and describe the argument. Also discuss when it's okay to leave it out. |
||
names = self.saved_locals.setdefault(function, SymbolTable()) | ||
else: | ||
names = SymbolTable() | ||
self.locals.append(names) | ||
self.global_decls.append(set()) | ||
self.nonlocal_decls.append(set()) | ||
# -1 since entering block will increment this to 0. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,7 +53,6 @@ def semantic_analysis_for_scc(graph: 'Graph', scc: List[str]) -> None: | |
|
||
def process_top_levels(graph: 'Graph', scc: List[str]) -> None: | ||
# Process top levels until everything has been bound. | ||
# TODO: Limit the number of iterations | ||
|
||
# Initialize ASTs and symbol tables. | ||
for id in scc: | ||
|
@@ -92,13 +91,29 @@ def process_functions(graph: 'Graph', scc: List[str]) -> None: | |
for module in scc: | ||
tree = graph[module].tree | ||
assert tree is not None | ||
analyzer = graph[module].manager.new_semantic_analyzer | ||
symtable = tree.names | ||
targets = get_all_leaf_targets(symtable, module, None) | ||
for target, node, active_type in targets: | ||
deferred, incomplete = semantic_analyze_target(target, graph[module], node, | ||
active_type) | ||
assert not deferred # There can't be cross-function forward refs | ||
assert not incomplete # Ditto | ||
iteration = 0 | ||
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. It would be better to move the new logic to a separate function. I think that |
||
# We need one more pass after incomplete is False. | ||
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. Similar to above, it's probably better to avoid the term 'pass' here. Also consider renaming the |
||
more_passes = incomplete = True | ||
# Start in the incomplete state (no missing names will be reported on first pass). | ||
# Note that we use module name, since functions don't create qualified names. | ||
deferred = [module] | ||
analyzer.incomplete_namespaces.add(module) | ||
while deferred and more_passes: | ||
iteration += 1 | ||
if not incomplete or iteration == MAX_ITERATIONS: | ||
# OK, this is one last pass, now missing names will be reported. | ||
more_passes = False | ||
analyzer.incomplete_namespaces.discard(module) | ||
deferred, incomplete = semantic_analyze_target(module, graph[module], node, | ||
active_type) | ||
|
||
# After semantic analysis is done, discard local namespaces | ||
# to avoid memory hoarding. | ||
analyzer.saved_locals.clear() | ||
|
||
|
||
TargetInfo = Tuple[str, Union[MypyFile, FuncDef, OverloadedFuncDef, Decorator], Optional[TypeInfo]] | ||
|
@@ -124,7 +139,6 @@ def semantic_analyze_target(target: str, | |
state: 'State', | ||
node: Union[MypyFile, FuncDef, OverloadedFuncDef, Decorator], | ||
active_type: Optional[TypeInfo]) -> Tuple[List[str], bool]: | ||
# TODO: Support refreshing function targets (currently only works for module top levels) | ||
tree = state.tree | ||
assert tree is not None | ||
analyzer = state.manager.new_semantic_analyzer | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -150,6 +150,8 @@ def run_case_once(self, testcase: DataDrivenTestCase, | |
options.show_traceback = True | ||
if 'optional' in testcase.file: | ||
options.strict_optional = True | ||
if 'newsemanal' in testcase.file: | ||
options.new_semantic_analyzer = True | ||
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 is a good idea! |
||
if incremental_step and options.incremental: | ||
# Don't overwrite # flags: --no-incremental in incremental test cases | ||
options.incremental = True | ||
|
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.
A more detailed explanation would be nice here. Also, avoid the use of the term 'pass' since this means a different thing in the old semantic analyzer (maybe 'iteration'?).