Enable TypeForm support by default#11412
Conversation
...rather than requiring the --enableExperimentalFeatures bit to be set
...by the current interence context or because EvalFlags.TypeFormArg is set.
Without this optimization, the call5.py test fails where it defines a
NamedTuple "X", [("a", int), ...]. Attempting to interpret the string "a"
as a forward reference looks up the name `a` (bound by a later for-loop),
and the resulting flow analysis recursively re-enters the Z(...) constructor,
causing a "Class definition for Z depends on itself" error.
|
@microsoft-github-policy-service agree |
There was a problem hiding this comment.
Nit: Misspelling in commit message: "current interence context" -> "current inference context"
This comment has been minimized.
This comment has been minimized.
|
There's a prettier error too:
Checking formatting... You should be able to run |
|
Squish change to -> Do not try to interpret a string as a TypeForm unless one is expected
Squish to -> Do not try to interpret a string as a TypeForm unless one is expected
Squish to -> Enable TypeForm support by default
I previously reported a performance issue with TypeForm support: #11008. This PR mostly fixes that large slowdown, but it seems like Pyright is still a little slower with TypeForm support enabled for that testcase. |
|
Analysis of mypy_primer output:
Analysis of new errors in diophantine.pyErrors at diophantine.py:175-183 are in the class DiophantineEquationType:
"""..."""
name: str
def __init__(self, equation, free_symbols=None): # LINE 175
self.equation = _sympify(equation).expand(force=True)
if free_symbols is not None:
self.free_symbols = free_symbols
else:
self.free_symbols = list(self.equation.free_symbols)
self.free_symbols.sort(key=default_sort_key) # LINE 182
@overload
def sympify(a: int, *, strict: bool = False) -> Integer: ... # type: ignore
@overload
def sympify(a: float, *, strict: bool = False) -> Float: ...
@overload
def sympify(a: Expr | complex, *, strict: bool = False) -> Expr: ...
@overload
def sympify(a: Tbasic, *, strict: bool = False) -> Tbasic: ...
@overload
def sympify(a: Any, *, strict: bool = False) -> Basic: ...
def sympify(a, locals=None, convert_xor=True, strict=False, rational=False,
evaluate=None): ...
For reference, the revealed types within diophantine.py:175-183 changed in the following ways: Analysis of new errors in euclidtools.pyAffected code: def dmp_cancel(...) -> tuple[dmp[Er], dmp[Er]] | tuple[Er, Er, dmp[Er], dmp[Er]]:
...
p = dmp_mul_ground(p, cp, u, K) # LINE 1973
q = dmp_mul_ground(q, cq, u, K) # LINE 1974
return p, q # LINE 1976One new error at line 1974 ( The two new errors at line 1974 are word-for-word identical to the two errors already at line 1973 (with q/cq substituted for p/cp). On main branch (Baseline) vs. this feature branch (TypeForm), the inputs to this section of code are identical: So p and q have identical types going in, yet:
Probable cause: A constraint-solver asymmetry in baseline.The only difference between the two calls is the second argument's type. cp is a 3-way union that includes RingElement* (the Er TypeVar's bound); cq is a 2-way union without it. Baseline pyright treats these very differently when solving for Er@dmp_mul_ground:
TypeForm enabling makes the solver flag both cases uniformly. The q value at TypeForm enabling makes the solver flag both cases uniformly. Thus the "new" error at line 1974 is not a regression. ✅ Summary of next steps
|
|
Holy moly. diophantine.py is complex. I think I'm going to disregard the remaining error deltas in sympy. sympy was already producing very many errors under pyright before this branch and now its producing very many but different errors after this branch. Not worth the investigation time: sympy was already broken at the start. I've just pushed up commits integrating the outstanding feedback. NextSo the next item (and possibly last) item I want to investigate is any performance regressions that this branch might introduce. I'll look at the psf/black codebase as issue #11008 did. |
|
GitHub cannot anchor PR review comments to unchanged lines in the diff. Falling back to a general PR comment for packages/pyright-internal/src/analyzer/typeEvaluator.ts:L5574. Copilot generated: Suggested fix: function expectedTypeWantsTypeForm(expectedType: Type): boolean {
return someSubtypes(expectedType, (subtype) =>
isClassInstance(subtype) && ClassType.isBuiltIn(subtype, 'TypeForm')
);
}🔍 Structurally confirmed: [verified] |
|
GitHub cannot anchor PR review comments to unchanged lines in the diff. Falling back to a general PR comment for packages/pyright-internal/src/analyzer/typeEvaluator.ts:L7659. Copilot generated:
[verified] |
|
GitHub cannot anchor PR review comments to unchanged lines in the diff. Falling back to a general PR comment for packages/pyright-internal/src/analyzer/typeEvaluator.ts:L990. Copilot generated: All 7 TypeForm test files (typeForm1–7) still explicitly set [verified] |
|
Update: The following item is still my next item for this PR:
Before I start the item, I will investigate/fix a performance regression on a similar PR in mypy. I expect I may be able to take learnings from that future fix and apply them to this PR. |
|
Update: I'm continuing to fix performance regressions in mypy. |
This reverts commit 7233033.
Adds a dev-facing tool, adapted from mypy's misc/perf_compare.py, that builds the production pyright CLI bundle at each of two or more commits and times repeated runs over a fixed corpus, reporting robust winsorized paired deltas vs a baseline. Supports wall or CPU-time metrics and single-file or project corpora. Cached bundles live beside the script under build/perfCompare/binaries/ (gitignored). Also adds a brief pointer in CONTRIBUTING.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
I profiled this branch's changes against two targets ("corpuses"):
and found no significant performance regression in either. Performance
Setup: Clone the corpus next to the pyright repo: psf/black's entire repo - branch base vs branch tip
Not significant (CI includes 0). psf/black's
|
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review feedback suggested short-circuiting cloneWithTypeForm with
`if (type.props?.typeForm === typeForm) return type;` (mirroring
cloneForCondition) to avoid allocations on the now-unconditional call
sites. After measuring, this is not worth doing: the no-op case occurs in
only ~0.2% of calls, so the guard would add a branch to the hot path while
saving almost no allocations. No code change is made.
The reference-equality check rarely holds because nearly every call site
passes a freshly-allocated value (e.g. convertToInstance(type)), which is a
new object every time. A value-equality check (isTypeSame) would catch more
cases but costs far more than the allocation it would save, so it is a net
loss on the hot path.
Supporting data (instrumented cloneWithTypeForm over the full
typeEvaluator jest suite, 1,162 tests):
cloneWithTypeForm calls : ~12,000
`=== typeForm` would fire: 23 (~0.2%)
of which both undefined: 22 (~0.18%)
non-undefined ref match: 1
So the general `=== typeForm` guard buys essentially nothing beyond the
both-undefined case, which is itself only ~0.18% of calls.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses review feedback: the TypeForm tests set enableExperimentalFeatures = true, so none validated this PR's core claim that TypeForm is enabled by default. Remove the flag from the existing TypeForm1-8 tests so they all exercise the default configuration and directly prove the isTypeFormSupported gate removal is effective. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s expected Fix prettier whitespace issue
|
@rchiodo I've added commits addressing all your outstanding feedback. The PR branch is now pretty messy, with a lot of commits that should ultimately be squashed. Would you mind if I reordered/squashed the commits to get a clean history? |
|
If you want to squash that's fine. When we merge the PR, we'll also squash everything so it doesn't really matter which. |
|
Diff from mypy_primer, showing the effect of this PR on open source code: sympy (https://github.com/sympy/sympy)
+ .../projects/sympy/sympy/polys/euclidtools.py:1974:24 - error: Argument of type "dmp[Er@dmp_cancel] | Unknown | dup[Unknown]" cannot be assigned to parameter "f" of type "dmp[Er@dmp_mul_ground]" in function "dmp_mul_ground"
+ Type "dmp[Er@dmp_cancel] | Unknown | dup[Unknown]" is not assignable to type "dmp[Er@dmp_mul_ground]"
+ "builtins.list" is not assignable to "builtins.list"
+ Type parameter "_T@list" is invariant, but "dmp" is not the same as "dmp"
+ Consider switching from "list" to "Sequence" which is covariant (reportArgumentType)
+ .../projects/sympy/sympy/polys/euclidtools.py:1974:34 - error: Argument of type "Domain[Er@dmp_cancel] | Ring[Unknown]" cannot be assigned to parameter "K" of type "Domain[Er@dmp_mul_ground]" in function "dmp_mul_ground"
+ Type "Domain[Er@dmp_cancel] | Ring[Unknown]" is not assignable to type "Domain[Er@dmp_mul_ground]"
+ "Domain[Er@dmp_cancel]" is not assignable to "Domain[Er@dmp_mul_ground]"
+ Type parameter "Er@Domain" is invariant, but "Er@dmp_cancel" is not the same as "Er@dmp_mul_ground" (reportArgumentType)
- .../projects/sympy/sympy/polys/euclidtools.py:1976:12 - error: Type "tuple[dmp[Er@dmp_cancel | Unknown | RingElement*], dmp[Er@dmp_cancel]]" is not assignable to return type "tuple[dmp[Er@dmp_cancel], dmp[Er@dmp_cancel]] | tuple[Er@dmp_cancel, Er@dmp_cancel, dmp[Er@dmp_cancel], dmp[Er@dmp_cancel]]"
+ .../projects/sympy/sympy/polys/euclidtools.py:1976:12 - error: Type "tuple[dmp[Er@dmp_cancel | Unknown | RingElement*], dmp[Er@dmp_cancel | Unknown]]" is not assignable to return type "tuple[dmp[Er@dmp_cancel], dmp[Er@dmp_cancel]] | tuple[Er@dmp_cancel, Er@dmp_cancel, dmp[Er@dmp_cancel], dmp[Er@dmp_cancel]]"
- Type "tuple[dmp[Er@dmp_cancel | Unknown | RingElement*], dmp[Er@dmp_cancel]]" is not assignable to type "tuple[dmp[Er@dmp_cancel], dmp[Er@dmp_cancel]] | tuple[Er@dmp_cancel, Er@dmp_cancel, dmp[Er@dmp_cancel], dmp[Er@dmp_cancel]]"
+ Type "tuple[dmp[Er@dmp_cancel | Unknown | RingElement*], dmp[Er@dmp_cancel | Unknown]]" is not assignable to type "tuple[dmp[Er@dmp_cancel], dmp[Er@dmp_cancel]] | tuple[Er@dmp_cancel, Er@dmp_cancel, dmp[Er@dmp_cancel], dmp[Er@dmp_cancel]]"
- "tuple[dmp[Er@dmp_cancel | Unknown | RingElement*], dmp[Er@dmp_cancel]]" is not assignable to "tuple[Er@dmp_cancel, Er@dmp_cancel, dmp[Er@dmp_cancel], dmp[Er@dmp_cancel]]"
+ "tuple[dmp[Er@dmp_cancel | Unknown | RingElement*], dmp[Er@dmp_cancel | Unknown]]" is not assignable to "tuple[Er@dmp_cancel, Er@dmp_cancel, dmp[Er@dmp_cancel], dmp[Er@dmp_cancel]]"
- "tuple[dmp[Er@dmp_cancel | Unknown | RingElement*], dmp[Er@dmp_cancel]]" is not assignable to "tuple[dmp[Er@dmp_cancel], dmp[Er@dmp_cancel]]"
+ "tuple[dmp[Er@dmp_cancel | Unknown | RingElement*], dmp[Er@dmp_cancel | Unknown]]" is not assignable to "tuple[dmp[Er@dmp_cancel], dmp[Er@dmp_cancel]]"
- .../projects/sympy/sympy/solvers/ode/hypergeometric.py:247:67 - error: Operator "**" not supported for types "Basic" and "Literal[2]" (reportOperatorIssue)
+ .../projects/sympy/sympy/solvers/ode/lie_group.py:619:61 - error: Operator "-" not supported for type "Unknown | Basic" (reportOperatorIssue)
- .../projects/sympy/sympy/solvers/ode/nonhomogeneous.py:468:28 - error: Argument of type "Expr | Unknown | None" cannot be assigned to parameter "expr" of type "Expr" in function "make_args"
+ .../projects/sympy/sympy/solvers/ode/nonhomogeneous.py:468:28 - error: Argument of type "Unknown | None" cannot be assigned to parameter "expr" of type "Expr" in function "make_args"
- Type "Expr | Unknown | None" is not assignable to type "Expr"
+ Type "Unknown | None" is not assignable to type "Expr"
- .../projects/sympy/sympy/solvers/ode/ode.py:1579:38 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)
- .../projects/sympy/sympy/solvers/ode/ode.py:1580:38 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)
- .../projects/sympy/sympy/solvers/ode/ode.py:1590:9 - error: No overloads for "update" match the provided arguments (reportCallIssue)
- .../projects/sympy/sympy/solvers/ode/ode.py:1590:12 - error: "update" is not a known attribute of "None" (reportOptionalMemberAccess)
- .../projects/sympy/sympy/solvers/ode/ode.py:1590:19 - error: Argument of type "Unknown | dict[Unknown, Unknown] | None" cannot be assigned to parameter "m" of type "Iterable[tuple[str, Unknown]]" in function "update"
- Type "Unknown | dict[Unknown, Unknown] | None" is not assignable to type "Iterable[tuple[str, Unknown]]"
- "None" is incompatible with protocol "Iterable[tuple[str, Unknown]]"
- "__iter__" is not present (reportArgumentType)
- .../projects/sympy/sympy/solvers/ode/ode.py:1597:43 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)
- .../projects/sympy/sympy/solvers/ode/ode.py:1597:64 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)
- .../projects/sympy/sympy/solvers/ode/ode.py:1603:9 - error: No overloads for "update" match the provided arguments (reportCallIssue)
- .../projects/sympy/sympy/solvers/ode/ode.py:1603:12 - error: "update" is not a known attribute of "None" (reportOptionalMemberAccess)
- .../projects/sympy/sympy/solvers/ode/ode.py:1603:19 - error: Argument of type "Unknown | dict[Unknown, Unknown] | None" cannot be assigned to parameter "m" of type "Iterable[tuple[str, Unknown]]" in function "update"
- Type "Unknown | dict[Unknown, Unknown] | None" is not assignable to type "Iterable[tuple[str, Unknown]]"
- "None" is incompatible with protocol "Iterable[tuple[str, Unknown]]"
- "__iter__" is not present (reportArgumentType)
- .../projects/sympy/sympy/solvers/ode/ode.py:1610:43 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)
- .../projects/sympy/sympy/solvers/ode/ode.py:1610:74 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)
- .../projects/sympy/sympy/solvers/ode/ode.py:1616:9 - error: No overloads for "update" match the provided arguments (reportCallIssue)
- .../projects/sympy/sympy/solvers/ode/ode.py:1616:12 - error: "update" is not a known attribute of "None" (reportOptionalMemberAccess)
- .../projects/sympy/sympy/solvers/ode/ode.py:1616:19 - error: Argument of type "Unknown | dict[Unknown, Unknown] | None" cannot be assigned to parameter "m" of type "Iterable[tuple[str, Unknown]]" in function "update"
- Type "Unknown | dict[Unknown, Unknown] | None" is not assignable to type "Iterable[tuple[str, Unknown]]"
- "None" is incompatible with protocol "Iterable[tuple[str, Unknown]]"
- "__iter__" is not present (reportArgumentType)
- .../projects/sympy/sympy/solvers/ode/ode.py:1623:49 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)
- .../projects/sympy/sympy/solvers/ode/ode.py:1623:70 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)
+ .../projects/sympy/sympy/solvers/ode/ode.py:1753:36 - error: Cannot access attribute "lhs" for class "Expr"
+ Attribute "lhs" is unknown (reportAttributeAccessIssue)
+ .../projects/sympy/sympy/solvers/ode/ode.py:1753:55 - error: Cannot access attribute "rhs" for class "Expr"
+ Attribute "rhs" is unknown (reportAttributeAccessIssue)
+ .../projects/sympy/sympy/solvers/ode/ode.py:1754:36 - error: Cannot access attribute "lhs" for class "Expr"
+ Attribute "lhs" is unknown (reportAttributeAccessIssue)
+ .../projects/sympy/sympy/solvers/ode/ode.py:1755:17 - error: No overloads for "__setitem__" match the provided arguments (reportCallIssue)
+ .../projects/sympy/sympy/solvers/ode/ode.py:1755:17 - error: Argument of type "Equality | BooleanFalse | BooleanTrue | Unknown | Expr" cannot be assigned to parameter "value" of type "Equality | BooleanFalse | BooleanTrue" in function "__setitem__"
+ Type "Equality | BooleanFalse | BooleanTrue | Unknown | Expr" is not assignable to type "Equality | BooleanFalse | BooleanTrue"
+ Type "Expr" is not assignable to type "Equality | BooleanFalse | BooleanTrue"
+ "Expr" is not assignable to "Equality"
+ "Expr" is not assignable to "BooleanFalse"
+ "Expr" is not assignable to "BooleanTrue" (reportArgumentType)
- .../projects/sympy/sympy/solvers/ode/ode.py:3442:7 - error: "update" is not a known attribute of "None" (reportOptionalMemberAccess)
- .../projects/sympy/sympy/solvers/ode/ode.py:3442:38 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)
- .../projects/sympy/sympy/solvers/ode/ode.py:3443:7 - error: "update" is not a known attribute of "None" (reportOptionalMemberAccess)
- .../projects/sympy/sympy/solvers/ode/ode.py:3443:38 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)
- .../projects/sympy/sympy/solvers/ode/ode.py:3444:14 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)
- .../projects/sympy/sympy/solvers/ode/ode.py:3445:14 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)
- .../projects/sympy/sympy/solvers/ode/ode.py:3446:14 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)
- .../projects/sympy/sympy/solvers/ode/ode.py:3458:50 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)
- .../projects/sympy/sympy/solvers/ode/ode.py:3459:50 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)
- .../projects/sympy/sympy/solvers/ode/ode.py:3460:50 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)
- .../projects/sympy/sympy/solvers/ode/ode.py:3500:5 - error: No overloads for "update" match the provided arguments (reportCallIssue)
- .../projects/sympy/sympy/solvers/ode/ode.py:3500:7 - error: "update" is not a known attribute of "None" (reportOptionalMemberAccess)
... (truncated 102 lines) ...
|
|
@davidfstr, thanks for the PR |
...rather than requiring the --enableExperimentalFeatures bit to be set
CONTEXT:
OPEN QUESTIONS: