Skip to content

[ty] Implement Duboc's TDD optimization for narrowing constraints#25787

Open
charliermarsh wants to merge 12 commits into
mainfrom
charlie/duboc-narrowing-constraints
Open

[ty] Implement Duboc's TDD optimization for narrowing constraints#25787
charliermarsh wants to merge 12 commits into
mainfrom
charlie/duboc-narrowing-constraints

Conversation

@charliermarsh

@charliermarsh charliermarsh commented Jun 9, 2026

Copy link
Copy Markdown
Member

Summary

This implements the alternative Doug outlined in his review of #25729: apply Duboc's TDD optimization directly to narrowing constraints.

For an if/elif chain like:

if is_a(value):
    pass
elif is_b(value):
    pass
elif is_c(value):
    pass
else:
    return

reveal_type(value)  # C | B | A

Each later branch is reached only after the earlier checks fail, so we record the paths as A, not A and B, and not A and not B and C. Once the successful branches meet, those earlier failed checks are redundant, but retaining them makes the formula grow quickly and makes every later load of value expensive.

Narrowing constraints previously reused the reachability TDD. Reachability uses its third branch for an ambiguous result in Kleene three-valued logic, while narrowing formulas are boolean and do not use that branch. This gives narrowing constraints their own graph and uses the third edge as a "don't care" edge, following the Duboc optimization already used for specialization constraints in #23881. For example, A or B can store A once on B's "don't care" edge instead of copying it into both outcomes of B. Projected narrowing now uses the same representation.

When merging an if/elif continuation, we remove preceding branch predicates from each later branch and use the smaller formula only when the resulting merge is equivalent to the original. This simplifies the example to A or B or C, while preserving exclusions when an earlier branch returns and does not reach the merge. Other control-flow merges retain the existing canonical representation to avoid unrelated revealed-type changes.

@astral-sh-bot

astral-sh-bot Bot commented Jun 9, 2026

Copy link
Copy Markdown

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 92.23%. The percentage of expected errors that received a diagnostic held steady at 87.42%. The number of fully passing files held steady at 92/134.

@astral-sh-bot

astral-sh-bot Bot commented Jun 9, 2026

Copy link
Copy Markdown

Memory usage report

Summary

Project Old New Diff Outcome
prefect 564.13MB 565.34MB +0.21% (1.21MB)
sphinx 207.56MB 207.87MB +0.15% (320.57kB)
trio 87.75MB 87.93MB +0.20% (184.05kB)
flake8 35.38MB 35.53MB +0.42% (150.85kB)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
semantic_index 129.49MB 130.69MB +0.93% (1.21MB)
all_narrowing_constraints_for_expression 13.21MB 13.22MB +0.03% (3.58kB)
infer_expression_types_impl 55.21MB 55.21MB +0.00% (520.00B)
UnionType<'db>::from_two_elements_::interned_arguments 613.94kB 613.51kB -0.07% (440.00B)
UnionType<'db>::from_two_elements_ 742.79kB 742.36kB -0.06% (436.00B)
UnionType 1.37MB 1.38MB +0.02% (272.00B)
is_redundant_with_impl::interned_arguments 2.37MB 2.37MB +0.01% (264.00B)
infer_definition_types 75.87MB 75.87MB -0.00% (228.00B)
IntersectionType 1012.11kB 1011.90kB -0.02% (216.00B)
infer_scope_types_impl 47.64MB 47.64MB -0.00% (120.00B)
member_lookup_with_policy_inner 13.53MB 13.53MB -0.00% (84.00B)
is_redundant_with_impl 1.99MB 1.99MB +0.00% (84.00B)
infer_expression_type_impl 390.32kB 390.26kB -0.02% (60.00B)
StaticClassLiteral<'db>::implicit_attribute_inner_ 1.01MB 1.01MB -0.01% (60.00B)
infer_unpack_types 1.01MB 1.01MB -0.00% (12.00B)

sphinx

Name Old New Diff Outcome
semantic_index 43.45MB 43.76MB +0.71% (317.77kB)
all_narrowing_constraints_for_expression 4.12MB 4.13MB +0.27% (11.29kB)
IntersectionType 561.52kB 557.42kB -0.73% (4.10kB)
is_redundant_with_impl 919.62kB 916.44kB -0.35% (3.18kB)
is_redundant_with_impl::interned_arguments 1.14MB 1.14MB -0.23% (2.66kB)
infer_expression_types_impl 20.49MB 20.49MB +0.01% (2.66kB)
infer_scope_types_impl 12.55MB 12.54MB -0.01% (1.34kB)
UnionType<'db>::from_two_elements_::interned_arguments 265.89kB 265.03kB -0.32% (880.00B)
infer_expression_type_impl 279.56kB 280.29kB +0.26% (744.00B)
infer_definition_types 20.72MB 20.73MB +0.00% (696.00B)
UnionType<'db>::from_two_elements_ 292.57kB 291.94kB -0.21% (644.00B)
member_lookup_with_policy_inner 5.64MB 5.64MB +0.01% (432.00B)
UnionType 654.20kB 653.95kB -0.04% (256.00B)
infer_statement_types_impl 461.46kB 461.48kB +0.01% (24.00B)
StaticClassLiteral<'db>::implicit_attribute_inner_ 699.10kB 699.12kB +0.00% (24.00B)

trio

Name Old New Diff Outcome
semantic_index 21.15MB 21.33MB +0.85% (183.16kB)
all_narrowing_constraints_for_expression 1012.84kB 1014.40kB +0.15% (1.57kB)
infer_expression_types_impl 6.57MB 6.58MB +0.02% (1.43kB)
IntersectionType 144.58kB 143.96kB -0.43% (632.00B)
is_redundant_with_impl 182.21kB 181.79kB -0.23% (432.00B)
is_redundant_with_impl::interned_arguments 224.73kB 224.38kB -0.15% (352.00B)
UnionType<'db>::from_two_elements_ 43.93kB 43.62kB -0.69% (312.00B)
UnionType<'db>::from_two_elements_::interned_arguments 42.45kB 42.20kB -0.61% (264.00B)
infer_definition_types 6.61MB 6.61MB -0.00% (168.00B)
loop_header_reachability 126.60kB 126.59kB -0.01% (12.00B)
infer_scope_types_impl 3.78MB 3.78MB +0.00% (12.00B)

flake8

Name Old New Diff Outcome
semantic_index 9.72MB 9.86MB +1.52% (150.78kB)
infer_definition_types 1.57MB 1.57MB +0.00% (24.00B)
infer_statement_types_impl 39.98kB 40.00kB +0.06% (24.00B)
infer_expression_types_impl 974.41kB 974.44kB +0.00% (24.00B)

@astral-sh-bot

astral-sh-bot Bot commented Jun 9, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

Lint rule Added Removed Changed
unresolved-attribute 0 0 22
invalid-return-type 0 0 2
invalid-argument-type 0 0 1
invalid-assignment 0 0 1
Total 0 0 26

Large timing changes:

Project Old Time New Time Change
isort 0.18s 0.31s +74%
Raw diff (26 changes)
meson (https://github.com/mesonbuild/meson)
- run_project_tests.py:1314:13 error[invalid-assignment] Object of type `str` is not assignable to attribute `msg` on type `Unknown | TestResult | None`
+ run_project_tests.py:1314:13 error[invalid-assignment] Object of type `str` is not assignable to attribute `msg` on type `TestResult | None`
- run_project_tests.py:1317:41 error[unresolved-attribute] Attribute `msg` is not defined on `None` in union `Unknown | TestResult | None`
+ run_project_tests.py:1317:41 error[unresolved-attribute] Attribute `msg` is not defined on `None` in union `TestResult | None`
- run_project_tests.py:1318:64 error[unresolved-attribute] Attribute `msg` is not defined on `None` in union `Unknown | TestResult | None`
+ run_project_tests.py:1318:64 error[unresolved-attribute] Attribute `msg` is not defined on `None` in union `TestResult | None`
- run_project_tests.py:1321:14 error[unresolved-attribute] Attribute `msg` is not defined on `None` in union `Unknown | TestResult | None`
+ run_project_tests.py:1321:14 error[unresolved-attribute] Attribute `msg` is not defined on `None` in union `TestResult | None`
- run_project_tests.py:1323:41 error[unresolved-attribute] Attribute `step` is not defined on `None` in union `Unknown | TestResult | None`
+ run_project_tests.py:1323:41 error[unresolved-attribute] Attribute `step` is not defined on `None` in union `TestResult | None`
- run_project_tests.py:1324:41 error[unresolved-attribute] Attribute `msg` is not defined on `None` in union `Unknown | TestResult | None`
+ run_project_tests.py:1324:41 error[unresolved-attribute] Attribute `msg` is not defined on `None` in union `TestResult | None`
- run_project_tests.py:1338:46 error[unresolved-attribute] Attribute `step` is not defined on `None` in union `Unknown | TestResult | None`
+ run_project_tests.py:1338:46 error[unresolved-attribute] Attribute `step` is not defined on `None` in union `TestResult | None`
- run_project_tests.py:1338:76 error[unresolved-attribute] Attribute `msg` is not defined on `None` in union `Unknown | TestResult | None`
+ run_project_tests.py:1338:76 error[unresolved-attribute] Attribute `msg` is not defined on `None` in union `TestResult | None`
- run_project_tests.py:1339:16 error[unresolved-attribute] Attribute `step` is not defined on `None` in union `Unknown | TestResult | None`
+ run_project_tests.py:1339:16 error[unresolved-attribute] Attribute `step` is not defined on `None` in union `TestResult | None`
- run_project_tests.py:1339:55 error[unresolved-attribute] Attribute `mlog` is not defined on `None` in union `Unknown | TestResult | None`
+ run_project_tests.py:1339:55 error[unresolved-attribute] Attribute `mlog` is not defined on `None` in union `TestResult | None`
- run_project_tests.py:1343:37 error[unresolved-attribute] Attribute `mlog` is not defined on `None` in union `Unknown | TestResult | None`
+ run_project_tests.py:1343:37 error[unresolved-attribute] Attribute `mlog` is not defined on `None` in union `TestResult | None`
- run_project_tests.py:1348:37 error[unresolved-attribute] Attribute `mlog` is not defined on `None` in union `Unknown | TestResult | None`
+ run_project_tests.py:1348:37 error[unresolved-attribute] Attribute `mlog` is not defined on `None` in union `TestResult | None`
- run_project_tests.py:1349:37 error[unresolved-attribute] Attribute `stdo` is not defined on `None` in union `Unknown | TestResult | None`
+ run_project_tests.py:1349:37 error[unresolved-attribute] Attribute `stdo` is not defined on `None` in union `TestResult | None`
- run_project_tests.py:1351:37 error[unresolved-attribute] Attribute `stdo` is not defined on `None` in union `Unknown | TestResult | None`
+ run_project_tests.py:1351:37 error[unresolved-attribute] Attribute `stdo` is not defined on `None` in union `TestResult | None`
- run_project_tests.py:1352:28 error[unresolved-attribute] Attribute `cicmds` is not defined on `None` in union `Unknown | TestResult | None`
+ run_project_tests.py:1352:28 error[unresolved-attribute] Attribute `cicmds` is not defined on `None` in union `TestResult | None`
- run_project_tests.py:1354:33 error[unresolved-attribute] Attribute `stde` is not defined on `None` in union `Unknown | TestResult | None`
+ run_project_tests.py:1354:33 error[unresolved-attribute] Attribute `stde` is not defined on `None` in union `TestResult | None`
- run_project_tests.py:1360:64 error[unresolved-attribute] Attribute `msg` is not defined on `None` in union `Unknown | TestResult | None`
+ run_project_tests.py:1360:64 error[unresolved-attribute] Attribute `msg` is not defined on `None` in union `TestResult | None`

pandas (https://github.com/pandas-dev/pandas)
- pandas/core/groupby/grouper.py:643:26 error[unresolved-attribute] Attribute `categories` is not defined on `Index`, `ndarray[tuple[Any, ...], dtype[Any]]` in union `(Unknown & Series) | Index | (Unknown & ExtensionArray) | ... omitted 4 union elements`
+ pandas/core/groupby/grouper.py:643:26 error[unresolved-attribute] Attribute `categories` is not defined on `Index`, `ndarray[tuple[Any, ...], dtype[Any]]` in union `(Unknown & ndarray[tuple[object, ...], dtype[object]]) | (Unknown & Series) | Index | ... omitted 4 union elements`
- pandas/core/groupby/grouper.py:646:46 error[unresolved-attribute] Attribute `codes` is not defined on `Index`, `ndarray[tuple[Any, ...], dtype[Any]]` in union `(Unknown & Series) | Index | (Unknown & ExtensionArray) | ... omitted 4 union elements`
+ pandas/core/groupby/grouper.py:646:46 error[unresolved-attribute] Attribute `codes` is not defined on `Index`, `ndarray[tuple[Any, ...], dtype[Any]]` in union `(Unknown & ndarray[tuple[object, ...], dtype[object]]) | (Unknown & Series) | Index | ... omitted 4 union elements`
- pandas/core/groupby/grouper.py:655:27 error[unresolved-attribute] Attribute `isna` is not defined on `ndarray[tuple[Any, ...], dtype[Any]]` in union `(Unknown & Series) | Index | (Unknown & ExtensionArray) | ... omitted 4 union elements`
+ pandas/core/groupby/grouper.py:655:27 error[unresolved-attribute] Attribute `isna` is not defined on `ndarray[tuple[Any, ...], dtype[Any]]` in union `(Unknown & ndarray[tuple[object, ...], dtype[object]]) | (Unknown & Series) | Index | ... omitted 4 union elements`
- pandas/core/groupby/grouper.py:665:59 error[unresolved-attribute] Attribute `codes` is not defined on `Index`, `ndarray[tuple[Any, ...], dtype[Any]]` in union `(Unknown & Series) | Index | (Unknown & ExtensionArray) | ... omitted 4 union elements`
+ pandas/core/groupby/grouper.py:665:59 error[unresolved-attribute] Attribute `codes` is not defined on `Index`, `ndarray[tuple[Any, ...], dtype[Any]]` in union `(Unknown & ndarray[tuple[object, ...], dtype[object]]) | (Unknown & Series) | Index | ... omitted 4 union elements`
- pandas/core/groupby/grouper.py:669:62 error[unresolved-attribute] Attribute `ordered` is not defined on `Index`, `ndarray[tuple[Any, ...], dtype[Any]]` in union `(Unknown & Series) | Index | (Unknown & ExtensionArray) | ... omitted 4 union elements`
+ pandas/core/groupby/grouper.py:669:62 error[unresolved-attribute] Attribute `ordered` is not defined on `Index`, `ndarray[tuple[Any, ...], dtype[Any]]` in union `(Unknown & ndarray[tuple[object, ...], dtype[object]]) | (Unknown & Series) | Index | ... omitted 4 union elements`
- pandas/core/groupby/grouper.py:671:21 error[unresolved-attribute] Attribute `codes` is not defined on `Index`, `ndarray[tuple[Any, ...], dtype[Any]]` in union `(Unknown & Series) | Index | (Unknown & ExtensionArray) | ... omitted 4 union elements`
+ pandas/core/groupby/grouper.py:671:21 error[unresolved-attribute] Attribute `codes` is not defined on `Index`, `ndarray[tuple[Any, ...], dtype[Any]]` in union `(Unknown & ndarray[tuple[object, ...], dtype[object]]) | (Unknown & Series) | Index | ... omitted 4 union elements`
- pandas/io/excel/_base.py:1406:16 error[invalid-return-type] Return type does not match returned value: expected `tuple[int | float | str | date, str | None]`, found `tuple[(Unknown & date) | int | float | Decimal | str, None | str]`
+ pandas/io/excel/_base.py:1406:16 error[invalid-return-type] Return type does not match returned value: expected `tuple[int | float | str | date, str | None]`, found `tuple[(Unknown & date & ~datetime) | (Unknown & datetime) | int | ... omitted 3 union elements, None | str]`
- pandas/tests/extension/base/groupby.py:32:41 error[invalid-argument-type] Argument to function `assert_extension_array_equal` is incorrect: Expected `ExtensionArray`, found `(Unknown & Series) | Index | (Unknown & ExtensionArray) | ... omitted 4 union elements`
+ pandas/tests/extension/base/groupby.py:32:41 error[invalid-argument-type] Argument to function `assert_extension_array_equal` is incorrect: Expected `ExtensionArray`, found `(Unknown & ndarray[tuple[object, ...], dtype[object]]) | (Unknown & Series) | Index | ... omitted 4 union elements`

pydantic (https://github.com/pydantic/pydantic)
- pydantic/v1/utils.py:613:16 error[invalid-return-type] Return type does not match returned value: expected `Mapping[int | str, Any]`, found `(AbstractSet[int | str] & Top[Mapping[Unknown, object]]) | (Mapping[int | str, Any] & AbstractSet[object]) | (Mapping[int | str, Any] & ~AbstractSet[object]) | dict[int | str, EllipsisType]`
+ pydantic/v1/utils.py:613:16 error[invalid-return-type] Return type does not match returned value: expected `Mapping[int | str, Any]`, found `(AbstractSet[int | str] & Top[Mapping[Unknown, object]]) | Mapping[int | str, Any] | dict[int | str, EllipsisType]`

Full report with detailed diff (timing results)

@codspeed-hq

codspeed-hq Bot commented Jun 9, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 39.92%

⚡ 1 improved benchmark
✅ 126 untouched benchmarks
🆕 6 new benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation ty_micro[literal_equality_fallthrough_guarded_any] 180.5 ms 129 ms +39.92%
🆕 Simulation ty_micro[typeis_elif_narrowing] N/A 173.3 ms N/A
🆕 Simulation ty_micro[large_isinstance_narrowing_across_calls] N/A 201.1 ms N/A
🆕 Simulation ty_micro[large_isinstance_narrowing_mixed_calls] N/A 195.2 ms N/A
🆕 Memory ty_micro[typeis_elif_narrowing] N/A 14.1 MB N/A
🆕 Memory ty_micro[large_isinstance_narrowing_across_calls] N/A 14.8 MB N/A
🆕 Memory ty_micro[large_isinstance_narrowing_mixed_calls] N/A 14.7 MB N/A

Tip

Curious why this is faster? Use the CodSpeed MCP and ask your agent.


Comparing charlie/duboc-narrowing-constraints (d774951) with main (8b79528)

Open in CodSpeed

@charliermarsh charliermarsh force-pushed the charlie/duboc-narrowing-constraints branch 2 times, most recently from 89c4940 to e41f7c9 Compare June 9, 2026 21:49
@charliermarsh charliermarsh changed the title [ty] Keep narrowing constraints compact across branches [ty] Implement Duboc's TDD optimization for narrowing constraints Jun 9, 2026
@charliermarsh charliermarsh force-pushed the charlie/duboc-narrowing-constraints branch 2 times, most recently from c1e3e6f to c106298 Compare June 10, 2026 02:08
@charliermarsh charliermarsh marked this pull request as ready for review June 10, 2026 03:47
@charliermarsh charliermarsh added performance Potential performance improvement ty Multi-file analysis & type inference labels Jun 10, 2026
@charliermarsh

Copy link
Copy Markdown
Member Author

I think we may just want to eat the isort slowdown for now. I couldn't find any low-hanging fruit, and we already know that isort suffers from pathological performance around narrowing that we need to resolve (it's the motivation for our existing caps). We could probably avoid the slowdown by further adjusting our caps, but I'd rather just proceed with this and investigate that separately.

@charliermarsh

Copy link
Copy Markdown
Member Author

Separately, I've confirmed that this solves the PyTorch performance problems to the same degree as the prior PR (e.g., 70% faster or more vs. main).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Potential performance improvement ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants