Skip to content

[ty] Nominal Tagged Union Narrowing#24916

Merged
sharkdp merged 3 commits into
astral-sh:mainfrom
aholyoke:aholyoke/tagged-union-nominal-classes
Jun 1, 2026
Merged

[ty] Nominal Tagged Union Narrowing#24916
sharkdp merged 3 commits into
astral-sh:mainfrom
aholyoke:aholyoke/tagged-union-nominal-classes

Conversation

@aholyoke

Copy link
Copy Markdown
Contributor

Summary

Adds narrowing for tagged unions of nominal class instances based on literal attribute comparisons. Addresses part of astral-sh/ty#1479. Related to astral-sh/ty#2897.

This allows ty to narrow code such as x.tag == "a", "a" == x.tag, x.tag != "a", and match x.tag when x is a union of classes whose tag attributes are annotated with supported literal types.

  • non-literal tag arms are preserved during positive narrowing
  • impossible literal arms are removed
  • unsupported cases such as enum literals are left for future work

To keep the performance impact small, the new place tracking is gated by comparison operator. We only add base places for attribute/subscript expressions when the operator can produce a relevant narrowing constraint, which avoids extra Salsa narrowing queries for unrelated comparisons.

Test Plan

  • Updated md tests
  • Ran ecosystem analyzer on pydantic and prefect
  • Ran memory profiler

@astral-sh-bot astral-sh-bot Bot added the ty Multi-file analysis & type inference label Apr 29, 2026
@aholyoke

aholyoke commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

@ibraheemdev No rush, but can I get a review for this?

@MichaReiser

Copy link
Copy Markdown
Member

Thanks for working on this. It will probably be a few days before a review. Some of us are out this week, and all of us are traveling next week. But we try to get a look as soon as possible.

@ibraheemdev ibraheemdev 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 solid! Just a few comments.

Comment thread crates/ty_python_semantic/src/types/narrow.rs Outdated
Comment thread crates/ty_python_semantic/src/types/narrow.rs Outdated
Comment thread crates/ty_python_semantic/src/types/narrow.rs Outdated
@sharkdp sharkdp assigned sharkdp and unassigned ibraheemdev May 21, 2026
@sharkdp sharkdp force-pushed the aholyoke/tagged-union-nominal-classes branch 2 times, most recently from 8e2f3ef to 552db98 Compare May 22, 2026 09:02
@astral-sh-bot

astral-sh-bot Bot commented May 22, 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 91.94%. The percentage of expected errors that received a diagnostic held steady at 87.09%. The number of fully passing files held steady at 92/134.

@astral-sh-bot

astral-sh-bot Bot commented May 22, 2026

Copy link
Copy Markdown

Memory usage report

Summary

Project Old New Diff Outcome
prefect 709.59MB 712.49MB +0.41% (2.90MB)
sphinx 261.09MB 261.36MB +0.10% (276.04kB)
trio 109.16MB 109.31MB +0.14% (162.02kB)
flake8 43.95MB 43.97MB +0.05% (22.29kB)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
all_narrowing_constraints_for_expression 6.79MB 7.98MB +17.53% (1.19MB)
all_negative_narrowing_constraints_for_expression 6.62MB 7.81MB +17.88% (1.18MB)
infer_expression_types_impl 60.29MB 60.48MB +0.31% (193.84kB)
semantic_index 178.48MB 178.64MB +0.09% (163.24kB)
IntersectionType 898.60kB 970.91kB +8.05% (72.30kB)
place_table 806.31kB 865.20kB +7.30% (58.89kB)
infer_definition_types 88.42MB 88.44MB +0.02% (19.91kB)
infer_scope_types_impl 50.63MB 50.64MB +0.03% (13.61kB)
Type<'db>::class_member_with_policy_ 18.29MB 18.30MB +0.03% (6.55kB)
Type<'db>::member_lookup_with_policy_ 17.13MB 17.14MB +0.01% (2.26kB)
Type<'db>::class_member_with_policy_::interned_arguments 10.59MB 10.59MB +0.02% (2.23kB)
Type<'db>::member_lookup_with_policy_::interned_arguments 6.34MB 6.34MB +0.03% (2.23kB)
infer_expression_type_impl 8.22MB 8.23MB +0.03% (2.16kB)
StaticClassLiteral<'db>::implicit_attribute_inner_ 7.02MB 7.03MB +0.01% (836.00B)
UnionType 1.32MB 1.32MB +0.05% (672.00B)
... 7 more

sphinx

Name Old New Diff Outcome
all_narrowing_constraints_for_expression 2.60MB 2.69MB +3.43% (91.18kB)
all_negative_narrowing_constraints_for_expression 2.54MB 2.63MB +3.27% (85.29kB)
infer_expression_types_impl 21.93MB 21.97MB +0.17% (38.65kB)
semantic_index 58.84MB 58.87MB +0.05% (30.45kB)
IntersectionType 532.29kB 540.39kB +1.52% (8.10kB)
infer_definition_types 23.39MB 23.39MB +0.03% (7.84kB)
infer_scope_types_impl 13.42MB 13.43MB +0.04% (4.90kB)
infer_expression_type_impl 3.01MB 3.01MB +0.08% (2.60kB)
place_table 227.05kB 229.28kB +0.98% (2.23kB)
Type<'db>::member_lookup_with_policy_ 7.28MB 7.28MB +0.02% (1.18kB)
all_narrowing_constraints_for_pattern 0.00B 1.05kB +1.05kB (new)
all_negative_narrowing_constraints_for_pattern 0.00B 1008.00B +1008.00B (new)
Type<'db>::class_member_with_policy_ 7.94MB 7.94MB +0.01% (632.00B)
IntersectionType<'db>::from_two_elements_ 107.22kB 107.55kB +0.31% (340.00B)
Type<'db>::class_member_with_policy_::interned_arguments 4.21MB 4.21MB +0.00% (208.00B)
... 6 more

trio

Name Old New Diff Outcome
all_narrowing_constraints_for_expression 612.69kB 674.19kB +10.04% (61.50kB)
all_negative_narrowing_constraints_for_expression 591.79kB 646.68kB +9.27% (54.88kB)
semantic_index 28.32MB 28.33MB +0.06% (16.21kB)
infer_expression_types_impl 6.74MB 6.76MB +0.23% (16.11kB)
IntersectionType 130.19kB 136.27kB +4.67% (6.09kB)
place_table 77.23kB 79.16kB +2.50% (1.93kB)
infer_definition_types 7.40MB 7.40MB +0.02% (1.59kB)
infer_scope_types_impl 4.12MB 4.12MB +0.03% (1.30kB)
UnionType 144.23kB 145.47kB +0.86% (1.23kB)
infer_expression_type_impl 1.36MB 1.37MB +0.04% (520.00B)
is_redundant_with_impl::interned_arguments 222.84kB 223.01kB +0.08% (176.00B)
UnionType<'db>::from_two_elements_ 50.18kB 50.30kB +0.25% (128.00B)
loop_header_reachability 132.96kB 133.06kB +0.08% (104.00B)
is_redundant_with_impl 194.88kB 194.98kB +0.05% (96.00B)
StaticClassLiteral<'db>::implicit_attribute_inner_::interned_arguments 590.44kB 590.34kB -0.02% (96.00B)
... 8 more

flake8

Name Old New Diff Outcome
all_narrowing_constraints_for_expression 97.85kB 106.35kB +8.68% (8.50kB)
all_negative_narrowing_constraints_for_expression 96.00kB 103.20kB +7.49% (7.20kB)
infer_expression_types_impl 1.09MB 1.09MB +0.19% (2.13kB)
semantic_index 12.94MB 12.95MB +0.02% (2.10kB)
IntersectionType 48.79kB 50.27kB +3.03% (1.48kB)
infer_definition_types 1.78MB 1.78MB +0.02% (288.00B)
place_table 24.84kB 25.08kB +0.94% (240.00B)
infer_expression_type_impl 116.08kB 116.25kB +0.15% (180.00B)
StaticClassLiteral<'db>::implicit_attribute_inner_ 311.61kB 311.76kB +0.05% (156.00B)
Type<'db>::member_lookup_with_policy_ 573.89kB 573.92kB +0.00% (24.00B)
infer_scope_types_impl 868.18kB 868.20kB +0.00% (24.00B)

@astral-sh-bot

astral-sh-bot Bot commented May 22, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

Lint rule Added Removed Changed
unresolved-attribute 0 9 16
redundant-cast 4 0 0
invalid-argument-type 0 2 0
invalid-assignment 0 0 1
Total 4 11 17
Raw diff (32 changes)
core (https://github.com/home-assistant/core)
+ homeassistant/components/ssdp/common.py:18:41 warning[redundant-cast] Value is already of type `IPv6Address`
- homeassistant/components/anthropic/entity.py:1066:44 error[unresolved-attribute] Attribute `attachments` is not defined on `SystemContent`, `AssistantContent`, `ToolResultContent` in union `SystemContent | UserContent | AssistantContent | ToolResultContent`
- homeassistant/components/anthropic/entity.py:1080:53 error[unresolved-attribute] Attribute `attachments` is not defined on `SystemContent`, `AssistantContent`, `ToolResultContent` in union `SystemContent | UserContent | AssistantContent | ToolResultContent`
- homeassistant/components/conversation/chat_log.py:91:16 error[unresolved-attribute] Attribute `content` is not defined on `ToolResultContent` in union `SystemContent | UserContent | AssistantContent | ToolResultContent`
- homeassistant/components/conversation/chat_log.py:365:17 error[unresolved-attribute] Attribute `content` is not defined on `ToolResultContent` in union `SystemContent | UserContent | AssistantContent | ToolResultContent`
- homeassistant/components/conversation/chat_log.py:366:17 error[unresolved-attribute] Attribute `content` is not defined on `ToolResultContent` in union `SystemContent | UserContent | AssistantContent | ToolResultContent`
- homeassistant/components/google_generative_ai_conversation/entity.py:549:37 error[invalid-argument-type] Argument to bound method `list.append` is incorrect: Expected `ToolResultContent`, found `Content`
- homeassistant/components/google_generative_ai_conversation/entity.py:567:46 error[invalid-argument-type] Argument to function `_convert_content` is incorrect: Expected `UserContent | AssistantContent | SystemContent`, found `SystemContent | UserContent | AssistantContent | ToolResultContent`
- homeassistant/components/open_router/entity.py:271:44 error[unresolved-attribute] Attribute `attachments` is not defined on `SystemContent`, `AssistantContent`, `ToolResultContent` in union `SystemContent | UserContent | AssistantContent | ToolResultContent`
- homeassistant/components/open_router/entity.py:279:49 error[unresolved-attribute] Attribute `attachments` is not defined on `SystemContent`, `AssistantContent`, `ToolResultContent` in union `SystemContent | UserContent | AssistantContent | ToolResultContent`
- homeassistant/components/openai_conversation/entity.py:636:44 error[unresolved-attribute] Attribute `attachments` is not defined on `SystemContent`, `AssistantContent`, `ToolResultContent` in union `SystemContent | UserContent | AssistantContent | ToolResultContent`
- homeassistant/components/openai_conversation/entity.py:639:49 error[unresolved-attribute] Attribute `attachments` is not defined on `SystemContent`, `AssistantContent`, `ToolResultContent` in union `SystemContent | UserContent | AssistantContent | ToolResultContent`
+ homeassistant/components/shelly/utils.py:243:13 warning[redundant-cast] Value is already of type `IPv4Address`
+ homeassistant/components/ssdp/scanner.py:261:29 warning[redundant-cast] Value is already of type `IPv6Address`
+ homeassistant/components/ssdp/server.py:177:33 warning[redundant-cast] Value is already of type `IPv6Address`

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 `TestResult | None`
+ 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:1317:41 error[unresolved-attribute] Attribute `msg` is not defined on `None` in union `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:1318:64 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:1321:14 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:1323:41 error[unresolved-attribute] Attribute `step` 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:1324:41 error[unresolved-attribute] Attribute `msg` 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:1338:46 error[unresolved-attribute] Attribute `step` 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:76 error[unresolved-attribute] Attribute `msg` 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:1339:16 error[unresolved-attribute] Attribute `step` 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:55 error[unresolved-attribute] Attribute `mlog` 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:1343:37 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:1348: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:1349:37 error[unresolved-attribute] Attribute `stdo` 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:1351: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:1352:28 error[unresolved-attribute] Attribute `cicmds` 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:1354:33 error[unresolved-attribute] Attribute `stde` 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:1360:64 error[unresolved-attribute] Attribute `msg` 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`

Full report with detailed diff (timing results)

@AlexWaygood

Copy link
Copy Markdown
Member

might be interesting to compare this with #23116, which I believe was attempting the same thing

@sharkdp sharkdp force-pushed the aholyoke/tagged-union-nominal-classes branch from 552db98 to a61f79c Compare May 22, 2026 12:37
@aholyoke

Copy link
Copy Markdown
Contributor Author

@AlexWaygood Yeah, unfortunately I only noticed that PR after opening this. That one additionally covers is/is not with singletons and uses a stricter equality pre-check (won't narrow at all if any union member has a non-literal tag type). This PR preserves non-literal arms during positive narrowing and gates attribute place tracking by operator.

@sharkdp sharkdp force-pushed the aholyoke/tagged-union-nominal-classes branch from a61f79c to 01d2027 Compare June 1, 2026 11:32

@sharkdp sharkdp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks good, thank you.

I don't think the is/is not narrowing that the other PR has is as important, and this PR has other advantages (e.g. supports yolo conditions). If someone feels the is narrowing is important, then we can do that later.

@sharkdp sharkdp merged commit cc5a78a into astral-sh:main Jun 1, 2026
58 checks passed
@codspeed-hq

codspeed-hq Bot commented Jun 1, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 4.5%

⚡ 1 improved benchmark
✅ 64 untouched benchmarks
⏩ 60 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
WallTime pydantic 35.5 s 34 s +4.5%

Tip

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


Comparing aholyoke:aholyoke/tagged-union-nominal-classes (01d2027) with main (a6ad57f)

Open in CodSpeed

Footnotes

  1. 60 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.

carljm added a commit that referenced this pull request Jun 1, 2026
* main:
  [`pydocstyle`] Improve discoverability of rules enabled for each convention (#24973)
  [ty] Deduplicate retained use-def place states (#25450)
  [ty] reduce features of low-level crates depended on by `ty_python_semantic` (#25524)
  [ty] Fix narrowing enum literal unions by member identity (#25520)
  [ty] Test tagged union narrowing for named tuples (#25519)
  [ty] Disallow file-system access in `ty_python_core` (#25518)
  [ty] Nominal Tagged Union Narrowing (#24916)
  Commit `scripts/uv.lock` (#25517)
  Fix potential index out of range in `LineIndex` computation (#25492)
  [ty] Sync vendored typeshed stubs (#25514)
  [ty] Add disjointness for protocol method members (#25315)
  [ty] Use compact sets for more immutable fields (#25476)
  [ty] Derive `Default` for `FunctionDecoratorInference` (#25482)
  [ty] Ignore rejected assignments for synthesized bindings (#25340)
  [ty] Handle cycles in function decorator inference (#25475)
  docs: fix typo `bin/active` → `bin/activate` in tutorial (#25473)
  [ty] Narrow bound method overloads by receiver (#24707)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants