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
318320T = TypeVar ("T" )
319321
320322
321- # Whether to print diagnostic information for failed full parses
322- # in SemanticAnalyzer.try_parse_as_type_expression().
323+ # Instrumentation: If non-None, every expression that reaches the expensive
324+ # full-parse block of SemanticAnalyzer.try_parse_as_type_expression()
325+ # is logged to a .tsv by log_typeform_full_parse().
323326#
324- # See also: misc/analyze_typeform_stats.py
325- DEBUG_TYPE_EXPRESSION_FULL_PARSE_FAILURES : Final = False
327+ # See also:
328+ # - misc/analyze_typeform_full_parse_profile.py
329+ # - misc/analyze_typeform_stats.py
330+ _TYPEFORM_PROFILE_FULL_PARSE_PATH : Final = os .environ .get ("MYPY_TYPEFORM_PROFILE_FULL_PARSE" )
331+ _typeform_full_parse_log_file : TextIO | None = None
332+
333+ # TSV column names for the full-parse profile log
334+ _TYPEFORM_PROFILE_FULL_PARSE_HEADER = "outcome\t kind\t subkind\t descriptor\t dur_ns\n "
326335
327336
328337FUTURE_IMPORTS : Final = {
@@ -8124,6 +8133,9 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None:
81248133 else :
81258134 assert_never (maybe_type_expr )
81268135
8136+ full_parse_t0 = (
8137+ time .perf_counter_ns () if _TYPEFORM_PROFILE_FULL_PARSE_PATH is not None else 0
8138+ )
81278139 with self .isolated_error_analysis ():
81288140 try :
81298141 t = self .expr_to_analyzed_type (maybe_type_expr )
@@ -8133,17 +8145,6 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None:
81338145 # Not a type expression
81348146 t = None
81358147
8136- if DEBUG_TYPE_EXPRESSION_FULL_PARSE_FAILURES and t is None :
8137- original_flushed_files = set (self .errors .flushed_files ) # save
8138- try :
8139- errors = self .errors .new_messages () # capture
8140- finally :
8141- self .errors .flushed_files = original_flushed_files # restore
8142-
8143- print (
8144- f"SA.try_parse_as_type_expression: Full parse failure: { maybe_type_expr } , errors={ errors !r} "
8145- )
8146-
81478148 # Count full parse attempts for profiling
81488149 if t is not None :
81498150 self .type_expression_full_parse_success_count += 1
@@ -8152,6 +8153,12 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> None:
81528153
81538154 maybe_type_expr .as_type = t
81548155
8156+ if _TYPEFORM_PROFILE_FULL_PARSE_PATH is not None :
8157+ full_parse_t1 = time .perf_counter_ns ()
8158+ self .log_typeform_full_parse (
8159+ maybe_type_expr , t is not None , full_parse_t1 - full_parse_t0
8160+ )
8161+
81558162 @staticmethod
81568163 def var_is_typing_special_form (var : Var ) -> bool :
81578164 return var .fullname .startswith ("typing" ) and var .fullname in [
@@ -8168,6 +8175,92 @@ def var_is_typing_special_form(var: Var) -> bool:
81688175 "typing.Union" ,
81698176 ]
81708177
8178+ @staticmethod
8179+ def log_typeform_full_parse (expr : Expression , ok : bool , dur_ns : int ) -> None :
8180+ """Log one entry into the full-parse block of try_parse_as_type_expression.
8181+
8182+ Active only when the MYPY_TYPEFORM_PROFILE_FULL_PARSE environment variable
8183+ is set to a file path. Each mypy process (worker) writes to its own file
8184+ named "<path>.<pid>" to avoid contention; concatenating those files yields
8185+ the complete profile. Aggregate with misc/analyze_typeform_full_parse_profile.py.
8186+
8187+ Output is tab-separated with one row per full-parse attempt:
8188+
8189+ outcome "OK" if as_type was set, "FAIL" if the full parse rejected
8190+ the expression (either by raising TypeTranslationError or by
8191+ emitting errors during analysis).
8192+ kind AST node kind: StrExpr | IndexExpr | OpExpr | (other).
8193+ subkind For StrExpr: "ident", "dotident", or "other" (based on the
8194+ string's shape). For IndexExpr: "Name" or "Member" (base
8195+ kind). For OpExpr: always "|" (no other op reaches here).
8196+ descriptor Short, type-specific identifier for the expression:
8197+ StrExpr -> the string value, truncated to 80 chars
8198+ (with " (N)" suffix when truncated).
8199+ IndexExpr -> the full stringified expression (str(expr),
8200+ with tabs/newlines escaped).
8201+ OpExpr -> the full stringified expression (str(expr),
8202+ with tabs/newlines escaped).
8203+ dur_ns Wall-clock nanoseconds spent in the full-parse block for
8204+ this expression (measured around expr_to_analyzed_type
8205+ plus the surrounding isolated_error_analysis ctx).
8206+
8207+ The first line of each file is the column header (same as above).
8208+ """
8209+ global _typeform_full_parse_log_file
8210+ if _typeform_full_parse_log_file is None :
8211+ assert _TYPEFORM_PROFILE_FULL_PARSE_PATH is not None
8212+ _typeform_full_parse_log_file = open (
8213+ f"{ _TYPEFORM_PROFILE_FULL_PARSE_PATH } .{ os .getpid ()} " , "a" , buffering = 1
8214+ )
8215+ _typeform_full_parse_log_file .write (_TYPEFORM_PROFILE_FULL_PARSE_HEADER )
8216+ outcome = "OK" if ok else "FAIL"
8217+ if isinstance (expr , StrExpr ):
8218+ raw = expr .value
8219+ val = (
8220+ raw [:80 ]
8221+ .replace ("\\ " , "\\ \\ " )
8222+ .replace ("\t " , "\\ t" )
8223+ .replace ("\n " , "\\ n" )
8224+ .replace ("\r " , "\\ r" )
8225+ )
8226+ if len (raw ) > 80 :
8227+ val += f" ({ len (raw )} )"
8228+ if _IDENTIFIER_RE .fullmatch (raw ):
8229+ subkind = "ident"
8230+ elif _DOTTED_IDENTIFIER_RE .fullmatch (raw ):
8231+ subkind = "dotident"
8232+ else :
8233+ subkind = "other"
8234+ line = f"{ outcome } \t StrExpr\t { subkind } \t { val } \t { dur_ns } \n "
8235+ elif isinstance (expr , IndexExpr ):
8236+ base = expr .base
8237+ if isinstance (base , NameExpr ):
8238+ subkind = "Name"
8239+ elif isinstance (base , MemberExpr ):
8240+ subkind = "Member"
8241+ else :
8242+ subkind = type (base ).__name__
8243+ desc = (
8244+ str (expr )
8245+ .replace ("\\ " , "\\ \\ " )
8246+ .replace ("\t " , "\\ t" )
8247+ .replace ("\n " , "\\ n" )
8248+ .replace ("\r " , "\\ r" )
8249+ )
8250+ line = f"{ outcome } \t IndexExpr\t { subkind } \t { desc } \t { dur_ns } \n "
8251+ elif isinstance (expr , OpExpr ):
8252+ desc = (
8253+ str (expr )
8254+ .replace ("\\ " , "\\ \\ " )
8255+ .replace ("\t " , "\\ t" )
8256+ .replace ("\n " , "\\ n" )
8257+ .replace ("\r " , "\\ r" )
8258+ )
8259+ line = f"{ outcome } \t OpExpr\t |\t { desc } \t { dur_ns } \n "
8260+ else :
8261+ line = f"{ outcome } \t { type (expr ).__name__ } \t \t \t { dur_ns } \n "
8262+ _typeform_full_parse_log_file .write (line )
8263+
81718264 @contextmanager
81728265 def isolated_error_analysis (self ) -> Iterator [None ]:
81738266 """
0 commit comments