Skip to content

[ty] Track narrowing unions to prevent exponential blowup#22066

Closed
zanieb wants to merge 3 commits into
mainfrom
zb/narrow-track
Closed

[ty] Track narrowing unions to prevent exponential blowup#22066
zanieb wants to merge 3 commits into
mainfrom
zb/narrow-track

Conversation

@zanieb

@zanieb zanieb commented Dec 19, 2025

Copy link
Copy Markdown
Member

See astral-sh/ty#2026 (comment)

I'm mostly exploring this as a short-term way to resolve the exponential complexity of recursive analysis as described in the linked issue. It may or may not have longstanding value once we support discriminated (or "tagged") unions for typed dicts.

When inferring a call expression with a union type context, we try narrowing
to each element of the union. If nested calls have the same union as their
parameter type, this would lead to exponential blowup. By tracking which
unions we're already narrowing against, we skip redundant nested narrowing.

On a synthetic benchmark with nested `list_schema()` calls:
- 4-level nesting: >120s → 0.13s
@zanieb zanieb added performance Potential performance improvement ty Multi-file analysis & type inference labels Dec 19, 2025
@astral-sh-bot

astral-sh-bot Bot commented Dec 19, 2025

Copy link
Copy Markdown

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

@astral-sh-bot

astral-sh-bot Bot commented Dec 19, 2025

Copy link
Copy Markdown

mypy_primer results

Changes were detected when running on open source projects
prefect (https://github.com/PrefectHQ/prefect)
- src/integrations/prefect-dbt/prefect_dbt/core/settings.py:94:28: error[invalid-assignment] Object of type `dict[str, Any] | T@resolve_block_document_references | str | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
+ src/integrations/prefect-dbt/prefect_dbt/core/settings.py:94:28: error[invalid-assignment] Object of type `dict[str, Any] | T@resolve_block_document_references | int | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
- src/prefect/cli/deploy/_core.py:86:21: error[invalid-assignment] Object of type `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
+ src/prefect/cli/deploy/_core.py:86:21: error[invalid-assignment] Object of type `T@resolve_block_document_references | int | dict[str, Any] | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
- src/prefect/deployments/steps/core.py:137:38: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements`
+ src/prefect/deployments/steps/core.py:137:38: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | int | dict[str, Any] | ... omitted 4 union elements`
- src/prefect/utilities/templating.py:320:13: error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements` on object of type `dict[str, Any]`
+ src/prefect/utilities/templating.py:320:13: error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `T@resolve_block_document_references | int | dict[str, Any] | ... omitted 4 union elements` on object of type `dict[str, Any]`
- src/prefect/utilities/templating.py:323:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_block_document_references | dict[str, Any]`, found `list[T@resolve_block_document_references | dict[str, Any] | str | ... omitted 5 union elements]`
+ src/prefect/utilities/templating.py:323:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_block_document_references | dict[str, Any]`, found `list[T@resolve_block_document_references | int | dict[str, Any] | ... omitted 5 union elements]`
- src/prefect/workers/base.py:228:13: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements`
+ src/prefect/workers/base.py:228:13: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | int | dict[str, Any] | ... omitted 4 union elements`

scikit-build-core (https://github.com/scikit-build/scikit-build-core)
+ src/scikit_build_core/build/wheel.py:98:20: error[no-matching-overload] No overload of bound method `__init__` matches arguments
- Found 42 diagnostics
+ Found 43 diagnostics

jax (https://github.com/google/jax)
+ jax/_src/tree_util.py:302:31: error[invalid-argument-type] Argument to bound method `register_node` is incorrect: Expected `(Hashable, Iterable[object], /) -> T@register_pytree_node`, found `(_AuxData@register_pytree_node, _Children@register_pytree_node, /) -> T@register_pytree_node`
+ jax/_src/tree_util.py:305:31: error[invalid-argument-type] Argument to bound method `register_node` is incorrect: Expected `(Hashable, Iterable[object], /) -> T@register_pytree_node`, found `(_AuxData@register_pytree_node, _Children@register_pytree_node, /) -> T@register_pytree_node`
+ jax/_src/tree_util.py:308:31: error[invalid-argument-type] Argument to bound method `register_node` is incorrect: Expected `(Hashable, Iterable[object], /) -> T@register_pytree_node`, found `(_AuxData@register_pytree_node, _Children@register_pytree_node, /) -> T@register_pytree_node`
- Found 2802 diagnostics
+ Found 2805 diagnostics

static-frame (https://github.com/static-frame/static-frame)
- static_frame/core/series.py:772:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Series[Any, Any], TVDtype@Series]`, found `InterGetItemILocReduces[Series[Any, Any] | Top[Index[Any]] | TypeBlocks | ... omitted 6 union elements, generic[object]]`
+ static_frame/core/series.py:772:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Series[Any, Any], TVDtype@Series]`, found `InterGetItemILocReduces[Series[Any, Any], generic[object]]`

pandas-stubs (https://github.com/pandas-dev/pandas-stubs)
- pandas-stubs/_typing.pyi:1223:16: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 5086 diagnostics
+ Found 5085 diagnostics

No memory usage changes detected ✅

@codspeed-hq

codspeed-hq Bot commented Dec 19, 2025

Copy link
Copy Markdown

CodSpeed Performance Report

Merging #22066 will improve performance by 72.68%

Comparing zb/narrow-track (fd525e8) with main (1a18ada)

Summary

⚡ 2 improvements
✅ 20 untouched
⏩ 30 skipped1

Benchmarks breakdown

Mode Benchmark BASE HEAD Efficiency
WallTime medium[colour-science] 107.1 s 100.7 s +6.35%
WallTime large[pydantic] 105.9 s 61.3 s +72.68%

Footnotes

  1. 30 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@zanieb

zanieb commented Dec 19, 2025

Copy link
Copy Markdown
Member Author

Hm is this a fix for some false positives?

pandas-stubs (https://github.com/pandas-dev/pandas-stubs)
- tests/frame/test_groupby.py:228:15: error[type-assertion-failure] Type `Series[Any]` does not match asserted type `Series[str | bytes | int | ... omitted 12 union elements]`
- tests/frame/test_groupby.py:624:15: error[type-assertion-failure] Type `Series[Any]` does not match asserted type `Series[str | bytes | int | ... omitted 12 union elements]`
- Found 5085 diagnostics
+ Found 5083 diagnostics

@dcreager dcreager left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks fine to me, esp with an eye towards landing a quick fix and iterating on that as follow-on work.

@ibraheemdev mentioned in discord:

I think we need something slightly different than [this] PR though, we should be propagating the narrowed type context through the nested calls instead of the entire union, but not ignoring it completely

It looks like with the latest commit, you are propagating the narrowed type (though not the narrowed type context) along with the union. I'm not sure whether that's sufficient, but if not, I'm happy to address that as a follow-on.

Comment thread crates/ty_python_semantic/src/types/infer/builder.rs Outdated
Comment thread crates/ty_python_semantic/src/types/infer/builder.rs Outdated
@zanieb

zanieb commented Dec 19, 2025

Copy link
Copy Markdown
Member Author

Thanks for giving this a look!

It looks like with the latest commit, you are propagating the narrowed type (though not the narrowed type context) along with the union. I'm not sure whether that's sufficient, but if not, I'm happy to address that as a follow-on.

Ah interesting. I missed the nuance there.

@dcreager

Copy link
Copy Markdown
Member

Ah interesting. I missed the nuance there.

It might be the case that the narrowed type context will always end up being the narrowed type — that's the point of the bidi type context after all. But I'm not certain whether there are cases where the narrowed type will still be narrower than the narrowed type context, and if so, whether that's important. I would defer to @ibraheemdev on that question.

@ibraheemdev

Copy link
Copy Markdown
Member

As discussed on Discord, I don't think this is correct. We already correctly propagate the narrowed type context during inference, this PR essentially argues that if a child call has a union as type context which we are currently narrowing as part of a parent call, the child call will narrow to the same element as the parent. It's very rare to have nested generic calls where this matters in the first place, but that heuristic is not necessarily true, e.g.,

def f(x: list[int | None] | list[str | None]) -> str:
    return ""

def g[T](x: T) -> list[T]:
    return [x]

# regression: error: Expected `list[int | None] | list[str | None]`, found `list[str | None | int]`
f(g(f(g(1))))

The performance improvement here seems to be in cases where didn't need to narrow in the first place, which #22102 should address.

@zanieb

zanieb commented Dec 20, 2025

Copy link
Copy Markdown
Member Author

Thank you!

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.

3 participants