Skip to content

Make unzip of tuples inferable and validate its input - #859

Merged
devmotion merged 2 commits into
mainfrom
dmw/unzip-tuple-inference
Oct 30, 2026
Merged

devmotion merged 2 commits into
mainfrom
dmw/unzip-tuple-inference

Conversation

@devmotion

@devmotion devmotion commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

unzip(xs::Tuple) can't be inferred: ntuple(i -> map(Get(i), xs), length(x1)) builds Get(i) from a runtime i, so the components are inferred with Any elements:

julia> Base.infer_return_type(ChainRules.unzip, Tuple{NTuple{3,Tuple{Float64,Float64}}})
Tuple{Tuple{Any, Any, Any}, Tuple{Any, Any, Any}}

This becomes visible with JuliaDiff/ChainRulesCore.jl#724, where the cotangent of a tuple-valued broadcast stays a Tuple instead of being collected into a Vector. unzip_broadcast then takes its tuple branch, and test_rrule(copy∘broadcasted, BT1, atan, Tuple(rand(3)), Tuple(rand(3))) fails its inference check (the only failure in the ChainRules integration job of that PR).

This PR dispatches on Tuple{Vararg{NTuple{N,Any}}}, a non-empty tuple of tuples of equal length N, and forwards to the existing unzip(xs, Val(N)) method, as the array path already does. After this change:

julia> Base.infer_return_type(ChainRules.unzip, Tuple{NTuple{3,Tuple{Float64,Float64}}})
Tuple{Tuple{Float64, Float64, Float64}, Tuple{Float64, Float64, Float64}}

Invalid inputs now throw descriptive errors instead of failing in first, throwing a BoundsError, or silently dropping elements:

input before after
() ArgumentError: tuple must be non-empty ArgumentError: unzip cannot determine the number of components of an empty tuple
((1, 2), 3) BoundsError ArgumentError: unzip only accepts arrays or tuples of tuples
((1, 2), [3, 4]) ((1, 3), (2, 4)) ArgumentError: unzip only accepts arrays or tuples of tuples
((1, 2), (3,)) BoundsError DimensionMismatch: unzip requires tuples of equal length, got lengths (2, 1)
((1,), (2, 3)) ((1, 2),) DimensionMismatch: unzip requires tuples of equal length, got lengths (1, 2)

Inside ChainRules, all elements come from the same pullback, so they are always tuples of the same length. The empty-tuple broadcast test (atan over () and ()) does not reach unzip.

This supersedes the unzip part of #858. The _bc_cotangent part of #858 is no longer needed with JuliaDiff/ChainRulesCore.jl#724. It is independent of #857; both bump the version to 1.74.9, so whichever is merged second needs a trivial rebase.

Tests

@inferred tests for valid inputs, and tests for each of the invalid inputs above. Locally on Julia 1.13:

The full test suite was not run locally.

🤖 Generated with Claude Code

devmotion and others added 2 commits October 30, 2026 11:22
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.63%. Comparing base (7fb6cb1) to head (0ba0b12).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #859   +/-   ##
=======================================
  Coverage   96.63%   96.63%           
=======================================
  Files          28       28           
  Lines        3596     3598    +2     
=======================================
+ Hits         3475     3477    +2     
  Misses        121      121           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@devmotion
devmotion merged commit a441bd5 into main Oct 30, 2026
11 checks passed
@devmotion
devmotion deleted the dmw/unzip-tuple-inference branch October 30, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant