5050
5151from __future__ import annotations
5252
53+ import os
5354import re
55+ import time
5456from collections .abc import Callable , Collection , Iterable , Iterator
5557from contextlib import contextmanager
56- from typing import Any , Final , TypeAlias as _TypeAlias , TypeGuard , TypeVar , cast
58+ from typing import Any , Final , TextIO , TypeAlias as _TypeAlias , TypeGuard , TypeVar , cast
5759from typing_extensions import assert_never
5860
5961from mypy import errorcodes as codes , message_registry
320322T = TypeVar ("T" )
321323
322324
323- # Whether to print diagnostic information for failed full parses
324- # in SemanticAnalyzer.try_parse_as_type_expression().
325+ # Instrumentation: If non-None, every expression that reaches the expensive
326+ # full-parse block of SemanticAnalyzer.try_parse_as_type_expression()
327+ # is logged to a .tsv by log_typeform_full_parse().
325328#
326- # See also: misc/analyze_typeform_stats.py
327- DEBUG_TYPE_EXPRESSION_FULL_PARSE_FAILURES : Final = False
329+ # See also:
330+ # - misc/analyze_typeform_full_parse_profile.py
331+ # - misc/analyze_typeform_stats.py
332+ _TYPEFORM_PROFILE_FULL_PARSE_PATH : Final = os .environ .get ("MYPY_TYPEFORM_PROFILE_FULL_PARSE" )
333+ _typeform_full_parse_log_file : TextIO | None = None
334+
335+ # TSV column names for the full-parse profile log
336+ _TYPEFORM_PROFILE_FULL_PARSE_HEADER = "outcome\t kind\t subkind\t descriptor\t dur_ns\n "
328337
329338
330339FUTURE_IMPORTS : Final = {
@@ -8164,6 +8173,9 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None:
81648173 else :
81658174 assert_never (maybe_type_expr )
81668175
8176+ full_parse_t0 = (
8177+ time .perf_counter_ns () if _TYPEFORM_PROFILE_FULL_PARSE_PATH is not None else 0
8178+ )
81678179 with self .isolated_error_analysis ():
81688180 try :
81698181 t = self .expr_to_analyzed_type (maybe_type_expr )
@@ -8173,17 +8185,6 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None:
81738185 # Not a type expression
81748186 t = None
81758187
8176- if DEBUG_TYPE_EXPRESSION_FULL_PARSE_FAILURES and t is None :
8177- original_flushed_files = set (self .errors .flushed_files ) # save
8178- try :
8179- errors = self .errors .new_messages () # capture
8180- finally :
8181- self .errors .flushed_files = original_flushed_files # restore
8182-
8183- print (
8184- f"SA.try_parse_as_type_expression: Full parse failure: { maybe_type_expr } , errors={ errors !r} "
8185- )
8186-
81878188 # Count full parse attempts for profiling
81888189 if t is not None :
81898190 self .type_expression_full_parse_success_count += 1
@@ -8192,6 +8193,12 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None:
81928193
81938194 maybe_type_expr .as_type = t
81948195
8196+ if _TYPEFORM_PROFILE_FULL_PARSE_PATH is not None :
8197+ full_parse_t1 = time .perf_counter_ns ()
8198+ self .log_typeform_full_parse (
8199+ maybe_type_expr , t is not None , full_parse_t1 - full_parse_t0
8200+ )
8201+
81958202 @staticmethod
81968203 def var_is_typing_special_form (var : Var ) -> bool :
81978204 return var .fullname .startswith ("typing" ) and var .fullname in [
@@ -8208,6 +8215,92 @@ def var_is_typing_special_form(var: Var) -> bool:
82088215 "typing.Union" ,
82098216 ]
82108217
8218+ @staticmethod
8219+ def log_typeform_full_parse (expr : Expression , ok : bool , dur_ns : int ) -> None :
8220+ """Log one entry into the full-parse block of try_parse_as_type_expression.
8221+
8222+ Active only when the MYPY_TYPEFORM_PROFILE_FULL_PARSE environment variable
8223+ is set to a file path. Each mypy process (worker) writes to its own file
8224+ named "<path>.<pid>" to avoid contention; concatenating those files yields
8225+ the complete profile. Aggregate with misc/analyze_typeform_full_parse_profile.py.
8226+
8227+ Output is tab-separated with one row per full-parse attempt:
8228+
8229+ outcome "OK" if as_type was set, "FAIL" if the full parse rejected
8230+ the expression (either by raising TypeTranslationError or by
8231+ emitting errors during analysis).
8232+ kind AST node kind: StrExpr | IndexExpr | OpExpr | (other).
8233+ subkind For StrExpr: "ident", "dotident", or "other" (based on the
8234+ string's shape). For IndexExpr: "Name" or "Member" (base
8235+ kind). For OpExpr: always "|" (no other op reaches here).
8236+ descriptor Short, type-specific identifier for the expression:
8237+ StrExpr -> the string value, truncated to 80 chars
8238+ (with " (N)" suffix when truncated).
8239+ IndexExpr -> the full stringified expression (str(expr),
8240+ with tabs/newlines escaped).
8241+ OpExpr -> the full stringified expression (str(expr),
8242+ with tabs/newlines escaped).
8243+ dur_ns Wall-clock nanoseconds spent in the full-parse block for
8244+ this expression (measured around expr_to_analyzed_type
8245+ plus the surrounding isolated_error_analysis ctx).
8246+
8247+ The first line of each file is the column header (same as above).
8248+ """
8249+ global _typeform_full_parse_log_file
8250+ if _typeform_full_parse_log_file is None :
8251+ assert _TYPEFORM_PROFILE_FULL_PARSE_PATH is not None
8252+ _typeform_full_parse_log_file = open (
8253+ f"{ _TYPEFORM_PROFILE_FULL_PARSE_PATH } .{ os .getpid ()} " , "a" , buffering = 1
8254+ )
8255+ _typeform_full_parse_log_file .write (_TYPEFORM_PROFILE_FULL_PARSE_HEADER )
8256+ outcome = "OK" if ok else "FAIL"
8257+ if isinstance (expr , StrExpr ):
8258+ raw = expr .value
8259+ val = (
8260+ raw [:80 ]
8261+ .replace ("\\ " , "\\ \\ " )
8262+ .replace ("\t " , "\\ t" )
8263+ .replace ("\n " , "\\ n" )
8264+ .replace ("\r " , "\\ r" )
8265+ )
8266+ if len (raw ) > 80 :
8267+ val += f" ({ len (raw )} )"
8268+ if _IDENTIFIER_RE .fullmatch (raw ):
8269+ subkind = "ident"
8270+ elif _DOTTED_IDENTIFIER_RE .fullmatch (raw ):
8271+ subkind = "dotident"
8272+ else :
8273+ subkind = "other"
8274+ line = f"{ outcome } \t StrExpr\t { subkind } \t { val } \t { dur_ns } \n "
8275+ elif isinstance (expr , IndexExpr ):
8276+ base = expr .base
8277+ if isinstance (base , NameExpr ):
8278+ subkind = "Name"
8279+ elif isinstance (base , MemberExpr ):
8280+ subkind = "Member"
8281+ else :
8282+ subkind = type (base ).__name__
8283+ desc = (
8284+ str (expr )
8285+ .replace ("\\ " , "\\ \\ " )
8286+ .replace ("\t " , "\\ t" )
8287+ .replace ("\n " , "\\ n" )
8288+ .replace ("\r " , "\\ r" )
8289+ )
8290+ line = f"{ outcome } \t IndexExpr\t { subkind } \t { desc } \t { dur_ns } \n "
8291+ elif isinstance (expr , OpExpr ):
8292+ desc = (
8293+ str (expr )
8294+ .replace ("\\ " , "\\ \\ " )
8295+ .replace ("\t " , "\\ t" )
8296+ .replace ("\n " , "\\ n" )
8297+ .replace ("\r " , "\\ r" )
8298+ )
8299+ line = f"{ outcome } \t OpExpr\t |\t { desc } \t { dur_ns } \n "
8300+ else :
8301+ line = f"{ outcome } \t { type (expr ).__name__ } \t \t \t { dur_ns } \n "
8302+ _typeform_full_parse_log_file .write (line )
8303+
82118304 @contextmanager
82128305 def isolated_error_analysis (self ) -> Iterator [None ]:
82138306 """
0 commit comments