From 99f9abed08a78edc3639564aaa8e101206524c60 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Wed, 11 Feb 2026 10:02:42 -0500 Subject: [PATCH 1/4] Fix rrule for broadcasted over empty Tuple{} When broadcasting over an empty tuple, `Broadcast.combine_eltypes` returns `Union{}`. Since `Union{} <: Number` is true, the code entered `may_bc_derivatives` which tried to construct `Tuple{Union{}, ...}` and errored. Fix by treating `Union{}` as a trivial non-differentiable case alongside `Bool`. Fixes #830 Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.6 --- src/rulesets/Base/broadcast.jl | 2 +- test/rulesets/Base/broadcast.jl | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/rulesets/Base/broadcast.jl b/src/rulesets/Base/broadcast.jl index d1610ce24..92328b515 100644 --- a/src/rulesets/Base/broadcast.jl +++ b/src/rulesets/Base/broadcast.jl @@ -28,7 +28,7 @@ end function rrule(cfg::RCR, ::typeof(broadcasted), ::BroadcastStyle, f::F, args::Vararg{Any,N}) where {F,N} T = Broadcast.combine_eltypes(f, args) - if T === Bool # TODO use nondifftype here + if T === Bool || T === Union{} # TODO use nondifftype here # 1: Trivial case: non-differentiable output, e.g. `x .> 0` @debug("split broadcasting trivial", f, T) bc_trivial_back(_) = (TRI_NO..., ntuple(Returns(ZeroTangent()), length(args))...) diff --git a/test/rulesets/Base/broadcast.jl b/test/rulesets/Base/broadcast.jl index d2993774d..e30ddcc42 100644 --- a/test/rulesets/Base/broadcast.jl +++ b/test/rulesets/Base/broadcast.jl @@ -177,6 +177,15 @@ BT1 = Broadcast.BroadcastStyle(Tuple) end @testset "bugs" begin + @testset "broadcast over empty tuple" begin # https://github.com/JuliaDiff/ChainRules.jl/issues/830 + y, bk = rrule(CFG, copy∘broadcasted, BT1, isone, ()) + @test y == () + @test all(d -> d isa AbstractZero, bk(())) + + y2, bk2 = rrule(CFG, copy∘broadcasted, BT1, sin, ()) + @test y2 == () + @test all(d -> d isa AbstractZero, bk2(())) + end @testset "unbroadcast with NTuple" begin # https://github.com/JuliaDiff/ChainRules.jl/pull/661 @test ChainRules.unbroadcast((1, 2, [3]), [4, 5, [6]]) isa Tangent # earlier, NTuple demanded same type @test ChainRules.unbroadcast(broadcasted(-, (1, 2), 3), (4, 5)) == (4, 5) # earlier, called ndims(::Tuple) From 3e5b9c4303450958b9fa9c5a7c722d7085a3e39d Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Wed, 11 Feb 2026 11:52:21 -0500 Subject: [PATCH 2/4] Update test/rulesets/Base/broadcast.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Müller-Widmann --- test/rulesets/Base/broadcast.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/rulesets/Base/broadcast.jl b/test/rulesets/Base/broadcast.jl index e30ddcc42..d31880367 100644 --- a/test/rulesets/Base/broadcast.jl +++ b/test/rulesets/Base/broadcast.jl @@ -180,7 +180,7 @@ BT1 = Broadcast.BroadcastStyle(Tuple) @testset "broadcast over empty tuple" begin # https://github.com/JuliaDiff/ChainRules.jl/issues/830 y, bk = rrule(CFG, copy∘broadcasted, BT1, isone, ()) @test y == () - @test all(d -> d isa AbstractZero, bk(())) + @test bk(Tangent{Tuple{}}()) == (NoTangent(), NoTangent(), NoTangent(), ZeroTangent()) y2, bk2 = rrule(CFG, copy∘broadcasted, BT1, sin, ()) @test y2 == () From 5b5d269a34f9735d2aa4e6b620a35664b9709339 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Wed, 11 Feb 2026 11:52:28 -0500 Subject: [PATCH 3/4] Update test/rulesets/Base/broadcast.jl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Müller-Widmann --- test/rulesets/Base/broadcast.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/rulesets/Base/broadcast.jl b/test/rulesets/Base/broadcast.jl index d31880367..cd9a463c2 100644 --- a/test/rulesets/Base/broadcast.jl +++ b/test/rulesets/Base/broadcast.jl @@ -184,7 +184,7 @@ BT1 = Broadcast.BroadcastStyle(Tuple) y2, bk2 = rrule(CFG, copy∘broadcasted, BT1, sin, ()) @test y2 == () - @test all(d -> d isa AbstractZero, bk2(())) + @test bk(Tangent{Tuple{}}()) == (NoTangent(), NoTangent(), NoTangent(), ZeroTangent()) end @testset "unbroadcast with NTuple" begin # https://github.com/JuliaDiff/ChainRules.jl/pull/661 @test ChainRules.unbroadcast((1, 2, [3]), [4, 5, [6]]) isa Tangent # earlier, NTuple demanded same type From 4069462e3147808f9540a5181d7a48deb7a3b8a9 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Wed, 11 Feb 2026 11:56:57 -0500 Subject: [PATCH 4/4] Add multi-argument test for broadcast over empty tuples Test atan.((), ()) to cover the multi-arg path through the trivial broadcast rrule when eltype is Union{}. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.6 --- test/rulesets/Base/broadcast.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/rulesets/Base/broadcast.jl b/test/rulesets/Base/broadcast.jl index cd9a463c2..de7516bfb 100644 --- a/test/rulesets/Base/broadcast.jl +++ b/test/rulesets/Base/broadcast.jl @@ -184,7 +184,12 @@ BT1 = Broadcast.BroadcastStyle(Tuple) y2, bk2 = rrule(CFG, copy∘broadcasted, BT1, sin, ()) @test y2 == () - @test bk(Tangent{Tuple{}}()) == (NoTangent(), NoTangent(), NoTangent(), ZeroTangent()) + @test bk2(Tangent{Tuple{}}()) == (NoTangent(), NoTangent(), NoTangent(), ZeroTangent()) + + # Multi-argument case + y3, bk3 = rrule(CFG, copy∘broadcasted, BT1, atan, (), ()) + @test y3 == () + @test bk3(Tangent{Tuple{}}()) == (NoTangent(), NoTangent(), NoTangent(), ZeroTangent(), ZeroTangent()) end @testset "unbroadcast with NTuple" begin # https://github.com/JuliaDiff/ChainRules.jl/pull/661 @test ChainRules.unbroadcast((1, 2, [3]), [4, 5, [6]]) isa Tangent # earlier, NTuple demanded same type