From f79e3669ca13448cdbc11ca932c41946ccf5762e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Wed, 16 Sep 2026 12:21:20 +0200 Subject: [PATCH 1/5] Realias `result` on the gradient path (fixes #269) `DiffResults` requires the object returned by a mutating call to be used, and ReverseDiff returned the argument binding instead. Besides violating that contract (#251), it miscompiles on Julia 1.8 - 1.11.4: the optimizer forwards a stale pre-call read of `result`'s fields across the call that writes them, so `DiffResults.value(result)` still holds the value the result was constructed with. The underlying bug is JuliaLang/julia#62812, fixed by JuliaLang/julia#57201 in Julia 1.11.5 but never backported to the 1.10 LTS. Rebinding has to happen at every level of the chain - `gradients.jl` alone and `tape.jl` alone both still return the stale value. Co-Authored-By: Claude Opus 5 (1M context) --- src/api/gradients.jl | 8 ++++---- src/api/tape.jl | 2 +- test/api/GradientTests.jl | 21 +++++++++++++++++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/api/gradients.jl b/src/api/gradients.jl index ed92cd4d..fe616651 100644 --- a/src/api/gradients.jl +++ b/src/api/gradients.jl @@ -21,7 +21,7 @@ call. function gradient(f, input, cfg::GradientConfig = GradientConfig(input)) tape = GradientTape(f, input, cfg) result = construct_result(input_hook(tape)) - seeded_reverse_pass!(result, tape) + result = seeded_reverse_pass!(result, tape) empty!(cfg.tape) return result end @@ -39,7 +39,7 @@ in it as well. """ function gradient!(result, f, input, cfg::GradientConfig = GradientConfig(input)) tape = GradientTape(f, input, cfg) - seeded_reverse_pass!(result, tape) + result = seeded_reverse_pass!(result, tape) empty!(cfg.tape) return result end @@ -60,7 +60,7 @@ of `f` w.r.t. `input[i].` """ function gradient!(tape::Union{GradientTape,CompiledGradient}, input) result = construct_result(input_hook(tape)) - gradient!(result, tape, input) + result = gradient!(result, tape, input) return result end @@ -77,6 +77,6 @@ in it as well. """ function gradient!(result, tape::Union{GradientTape,CompiledGradient}, input) seeded_forward_pass!(tape, input) - seeded_reverse_pass!(result, tape) + result = seeded_reverse_pass!(result, tape) return result end diff --git a/src/api/tape.jl b/src/api/tape.jl index 2f033ee6..1be1f973 100644 --- a/src/api/tape.jl +++ b/src/api/tape.jl @@ -44,7 +44,7 @@ function seeded_forward_pass!(t::AbstractTape, input) end function seeded_reverse_pass!(result, t::AbstractTape) - seeded_reverse_pass!(result, output_hook(t), input_hook(t), t) + result = seeded_reverse_pass!(result, output_hook(t), input_hook(t), t) return result end diff --git a/test/api/GradientTests.jl b/test/api/GradientTests.jl index f05868c9..5814828e 100644 --- a/test/api/GradientTests.jl +++ b/test/api/GradientTests.jl @@ -261,4 +261,25 @@ end end end +############################################################################################ + +# Top level, not inside the `@testset`: closures would change the inlining decisions. +f269(x) = sum(abs2, x) + +function value_and_gradient269!(grad, tape, x) + result = DiffResults.MutableDiffResult(zero(eltype(x)), (grad,)) + result = ReverseDiff.gradient!(result, tape, x) + return DiffResults.value(result), DiffResults.gradient(result) +end + +nested269!(grad, tape, x) = (y = value_and_gradient269!(grad, tape, x)[1]; (y, grad)) + +@testset "primal value survives inlining into a caller (#269)" begin + x = [3.0, 5.0] + tape = ReverseDiff.GradientTape(f269, x) + for t in (tape, ReverseDiff.compile(tape)) + @test nested269!(similar(x), t, x) == (34.0, [6.0, 10.0]) + end +end + end # module From 0da149f4a370d0ef2dc608538e6d741ac88e0a57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Thu, 17 Sep 2026 00:04:30 +0200 Subject: [PATCH 2/5] Realias `result` on the Jacobian, Hessian and tuple paths (fixes #251) `DiffResults` requires the object returned by a mutating call to be used. For an `ImmutableDiffResult`, which is what `DiffResults.GradientResult(x)` returns for a `StaticArray`, `value!` cannot mutate and returns a new struct, so discarding it drops the primal value. Only the wrapper is immutable, so the derivatives still come out correct and only `DiffResults.value` goes stale. The rebind has to be unbroken. `utils.jl` is the load-bearing layer: its leaf `extract_result!`/`extract_result_value!` methods already realias, but every caller discarded the result. Two changes beyond the mechanical rebind: The `Tuple` methods looped over `result[i]`, which is not assignable, so a returned struct had nowhere to go. They now `map` over the tuple, matching `construct_result(input::Tuple)`. Since `map` over tuples truncates to the shorter argument, the two methods taking both a result and an input tuple constrain them to a common `NTuple{N,Any}`, so a mismatched arity is a dispatch error rather than a silent truncation. `hessian!(result::DiffResult, tape, input)` builds an inner `DiffResult` holding the gradient in its value slot. `MutableDiffResult` copies into the shared buffer so it propagates, `ImmutableDiffResult` rebuilds the struct instead, so the gradient was dropped. It now reads the gradient back off the inner result. Allocations are byte-identical across all 26 gradient/Jacobian/Hessian entry points measured A/B, array and tuple, and inference stays concrete. Co-Authored-By: Claude Opus 5 (1M context) --- src/api/hessians.jl | 12 +++++---- src/api/jacobians.jl | 8 +++--- src/api/utils.jl | 30 +++++++---------------- test/api/GradientTests.jl | 51 ++++++++++++++++++++++++++++++++++++++- test/api/HessianTests.jl | 26 +++++++++++++++++++- test/api/JacobianTests.jl | 23 +++++++++++++++++- 6 files changed, 117 insertions(+), 33 deletions(-) diff --git a/src/api/hessians.jl b/src/api/hessians.jl index e0107cb9..6fb9175e 100644 --- a/src/api/hessians.jl +++ b/src/api/hessians.jl @@ -38,7 +38,7 @@ If `result` is a `DiffResults.DiffResult`, the primal value `f(input)` and the g """ function hessian!(result, f, input::AbstractArray, cfg::HessianConfig = HessianConfig(input)) ∇f = x -> gradient(f, x, cfg.gradient_config) - jacobian!(result, ∇f, input, cfg.jacobian_config) + result = jacobian!(result, ∇f, input, cfg.jacobian_config) return result end @@ -46,7 +46,7 @@ function hessian!(result::DiffResult, f, input::AbstractArray, cfg::HessianConfig = HessianConfig(result, input)) ∇f! = (y, x) -> begin gradient_result = DiffResult(zero(eltype(y)), y) - gradient!(gradient_result, f, x, cfg.gradient_config) + gradient_result = gradient!(gradient_result, f, x, cfg.gradient_config) result = DiffResults.value!(result, value(DiffResults.value(gradient_result))) return y end @@ -68,7 +68,7 @@ return the Hessian `H(f)(input)`. """ function hessian!(tape::Union{HessianTape,CompiledHessian}, input::AbstractArray) result = construct_result(output_hook(tape), input_hook(tape)) - hessian!(result, tape, input) + result = hessian!(result, tape, input) return result end @@ -85,13 +85,15 @@ If `result` is a `DiffResults.DiffResult`, the primal value `f(input)` and the g """ function hessian!(result::AbstractArray, tape::Union{HessianTape,CompiledHessian}, input::AbstractArray) seeded_forward_pass!(tape, input) - seeded_reverse_pass!(result, tape) + result = seeded_reverse_pass!(result, tape) return result end function hessian!(result::DiffResult, tape::Union{HessianTape,CompiledHessian}, input::AbstractArray) seeded_forward_pass!(tape, input) - seeded_reverse_pass!(DiffResult(DiffResults.gradient(result), DiffResults.hessian(result)), tape) + inner = DiffResult(DiffResults.gradient(result), DiffResults.hessian(result)) + inner = seeded_reverse_pass!(inner, tape) + result = DiffResults.gradient!(result, DiffResults.value(inner)) result = DiffResults.value!(result, func_hook(tape)(input)) return result end diff --git a/src/api/jacobians.jl b/src/api/jacobians.jl index 132957af..800727a7 100644 --- a/src/api/jacobians.jl +++ b/src/api/jacobians.jl @@ -41,7 +41,7 @@ in it as well. function jacobian!(result, f, input, cfg::JacobianConfig = JacobianConfig(input)) tape = JacobianTape(f, input, cfg) isa(input, TrackedArray) && empty!(input.tape) - jacobian!(result, tape, input) + result = jacobian!(result, tape, input) empty!(tape.tape) return result end @@ -74,7 +74,7 @@ form `f!(output::AbstractArray{<:Real}, input::AbstractArray{<:Real}...)`. function jacobian!(result, f!, output, input, cfg::JacobianConfig = JacobianConfig(output, input)) tape = JacobianTape(f!, output, input, cfg) isa(input, TrackedArray) && empty!(input.tape) - jacobian!(result, tape, input) + result = jacobian!(result, tape, input) extract_result_value!(output, output_hook(tape)) empty!(tape.tape) return result @@ -103,7 +103,7 @@ new `output` values into the tape. """ function jacobian!(tape::Union{JacobianTape,CompiledJacobian}, input) result = construct_result(output_hook(tape), input_hook(tape)) - jacobian!(result, tape, input) + result = jacobian!(result, tape, input) return result end @@ -119,7 +119,7 @@ case the primal value of the target function will be stored in it as well. """ function jacobian!(result, tape::Union{JacobianTape,CompiledJacobian}, input) seeded_forward_pass!(tape, input) - seeded_reverse_pass!(result, tape) + result = seeded_reverse_pass!(result, tape) return result end diff --git a/src/api/utils.jl b/src/api/utils.jl index a2469b0e..6540fe09 100644 --- a/src/api/utils.jl +++ b/src/api/utils.jl @@ -29,12 +29,12 @@ function seeded_reverse_pass!(result, output::TrackedReal, input, tape) unseed!(input) seed!(output) reverse_pass!(tape) - extract_result!(result, output, input) + result = extract_result!(result, output, input) return result end function seeded_reverse_pass!(result, output::Number, input, tape) - extract_result!(result, output) + result = extract_result!(result, output) return result end @@ -59,33 +59,24 @@ end function seeded_reverse_pass!(result::DiffResult, output::AbstractArray, input::TrackedArray, tape) seeded_reverse_pass!(DiffResults.jacobian(result), output, input, tape) - extract_result_value!(result, output) + result = extract_result_value!(result, output) return result end -function seeded_reverse_pass!(result::Tuple, output::AbstractArray, input::Tuple, tape) - for i in eachindex(result) - seeded_reverse_pass!(result[i], output, input[i], tape) - end - return result +function seeded_reverse_pass!(result::NTuple{N,Any}, output::AbstractArray, input::NTuple{N,Any}, tape) where {N} + return map((r, i) -> seeded_reverse_pass!(r, output, i, tape), result, input) end ##################### # result extraction # ##################### -function extract_result!(result::Tuple, output, input::Tuple) - for i in eachindex(result) - extract_result!(result[i], output, input[i]) - end - return result +function extract_result!(result::NTuple{N,Any}, output, input::NTuple{N,Any}) where {N} + return map((r, i) -> extract_result!(r, output, i), result, input) end function extract_result!(result::Tuple, output) - for i in eachindex(result) - extract_result!(result[i], output) - end - return result + return map(r -> extract_result!(r, output), result) end function extract_result!(result::AbstractArray, output::TrackedReal, input::TrackedArray) @@ -111,10 +102,7 @@ function extract_result!(result::DiffResult, output::Number) end function extract_result_value!(result::Tuple, output) - for i in eachindex(result) - extract_result_value!(result[i], output) - end - return result + return map(r -> extract_result_value!(r, output), result) end function extract_result_value!(result::DiffResult, output::AbstractArray) diff --git a/test/api/GradientTests.jl b/test/api/GradientTests.jl index 5814828e..92f2c93f 100644 --- a/test/api/GradientTests.jl +++ b/test/api/GradientTests.jl @@ -1,6 +1,6 @@ module GradientTests -using DiffTests, ForwardDiff, ReverseDiff, Test, LinearAlgebra +using DiffTests, ForwardDiff, ReverseDiff, StaticArrays, Test, LinearAlgebra include(joinpath(dirname(@__FILE__), "../utils.jl")) @@ -282,4 +282,53 @@ nested269!(grad, tape, x) = (y = value_and_gradient269!(grad, tape, x)[1]; (y, g end end +############################################################################################ + +f251(x) = sum(abs2, x) + +# An `MVector` gives an `ImmutableDiffResult` with a writable gradient buffer; an `SVector` +# buffer cannot be written to at all. +@testset "primal value of an immutable result (#251)" begin + x = MVector{2}(3.0, 5.0) + value, grad = 34.0, [6.0, 10.0] + + # `GradientResult` aliases its argument as the gradient buffer, so pass a copy. + result = ReverseDiff.gradient!(DiffResults.GradientResult(MVector(x)), f251, x) + @test result isa DiffResults.ImmutableDiffResult + @test DiffResults.value(result) == value + @test DiffResults.gradient(result) == grad + + tape = ReverseDiff.GradientTape(f251, x) + for t in (tape, ReverseDiff.compile(tape)) + result = ReverseDiff.gradient!(DiffResults.GradientResult(MVector(x)), t, x) + @test DiffResults.value(result) == value + @test DiffResults.gradient(result) == grad + end +end + +g251(x, y) = sum(abs2, x) + sum(abs2, y) + +@testset "primal value of immutable results in a tuple (#251)" begin + x, y = MVector{2}(3.0, 5.0), MVector{2}(2.0, 4.0) + value, grads = 54.0, ([6.0, 10.0], [4.0, 8.0]) + + result = (DiffResults.GradientResult(MVector(x)), DiffResults.GradientResult(MVector(y))) + result = ReverseDiff.gradient!(result, g251, (x, y)) + @test all(r -> r isa DiffResults.ImmutableDiffResult, result) + @test map(DiffResults.value, result) == (value, value) + @test map(DiffResults.gradient, result) == grads + + tape = ReverseDiff.GradientTape(g251, (x, y)) + for t in (tape, ReverseDiff.compile(tape)) + result = (DiffResults.GradientResult(MVector(x)), DiffResults.GradientResult(MVector(y))) + result = ReverseDiff.gradient!(result, t, (x, y)) + @test map(DiffResults.value, result) == (value, value) + @test map(DiffResults.gradient, result) == grads + end + + # a result tuple that does not match the input tuple is rejected by dispatch + @test_throws MethodError ReverseDiff.gradient!((MVector(x),), g251, (x, y)) + @test_throws MethodError ReverseDiff.gradient!((MVector(x), MVector(y), MVector(x)), g251, (x, y)) +end + end # module diff --git a/test/api/HessianTests.jl b/test/api/HessianTests.jl index f489403e..c1c49a0a 100644 --- a/test/api/HessianTests.jl +++ b/test/api/HessianTests.jl @@ -1,6 +1,6 @@ module HessianTests -using DiffTests, ForwardDiff, ReverseDiff, Test +using DiffTests, ForwardDiff, ReverseDiff, StaticArrays, Test include(joinpath(dirname(@__FILE__), "../utils.jl")) @@ -106,4 +106,28 @@ end @test ReverseDiff.hessian(fa, x) == [12.0 0.0; 0.0 18.0] end +############################################################################################ + +f251(x) = sum(abs2, x) + +@testset "primal value of an immutable result (#251)" begin + x = MVector{2}(3.0, 5.0) + value, grad, hess = 34.0, [6.0, 10.0], [2.0 0.0; 0.0 2.0] + + result = DiffResults.HessianResult(MVector(x)) + result = ReverseDiff.hessian!(result, f251, x, ReverseDiff.HessianConfig(result, x)) + @test result isa DiffResults.ImmutableDiffResult + @test DiffResults.value(result) == value + @test DiffResults.gradient(result) == grad + @test DiffResults.hessian(result) == hess + + tape = ReverseDiff.HessianTape(f251, x) + for t in (tape, ReverseDiff.compile(tape)) + result = ReverseDiff.hessian!(DiffResults.HessianResult(MVector(x)), t, x) + @test DiffResults.value(result) == value + @test DiffResults.gradient(result) == grad + @test DiffResults.hessian(result) == hess + end +end + end # module diff --git a/test/api/JacobianTests.jl b/test/api/JacobianTests.jl index 507edc7b..0f26db34 100644 --- a/test/api/JacobianTests.jl +++ b/test/api/JacobianTests.jl @@ -1,6 +1,6 @@ module JacobianTests -using DiffTests, ForwardDiff, ReverseDiff, Test +using DiffTests, ForwardDiff, ReverseDiff, StaticArrays, Test include(joinpath(dirname(@__FILE__), "../utils.jl")) @@ -312,4 +312,25 @@ for f in DiffTests.BINARY_MATRIX_TO_MATRIX_FUNCS # test_approx(Jb test_b) end +############################################################################################ + +f251(x) = x .^ 2 + +@testset "primal value of an immutable result (#251)" begin + x = MVector{2}(3.0, 5.0) + value, jac = [9.0, 25.0], [6.0 0.0; 0.0 10.0] + + result = ReverseDiff.jacobian!(DiffResults.JacobianResult(SVector{2}(0.0, 0.0), x), f251, x) + @test result isa DiffResults.ImmutableDiffResult + @test DiffResults.value(result) == value + @test DiffResults.jacobian(result) == jac + + tape = ReverseDiff.JacobianTape(f251, x) + for t in (tape, ReverseDiff.compile(tape)) + result = ReverseDiff.jacobian!(DiffResults.JacobianResult(SVector{2}(0.0, 0.0), x), t, x) + @test DiffResults.value(result) == value + @test DiffResults.jacobian(result) == jac + end +end + end # module From 55d69bf55b78349c40389abde5522ab40a951b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Thu, 17 Sep 2026 15:13:22 +0200 Subject: [PATCH 3/5] Bump version to `v1.18.0` --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 596d5dd1..a24ae60d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ReverseDiff" uuid = "37e2e3b7-166d-5795-8a7a-e32c996b4267" -version = "1.18.0-dev" +version = "1.18.0" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" From 3e9be1d3c7280961ff57b5d2275c51aeb162aee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Thu, 17 Sep 2026 23:45:07 +0200 Subject: [PATCH 4/5] Test the untracked-output path and drop unreachable methods Codecov flagged the two tuple `map` statements in `utils.jl` as uncovered. They were uncovered for different reasons. `extract_result!(result::Tuple, output)` was reachable: it runs when the target function's output does not depend on the input, so the recorded output is an untracked `Number` and every derivative is zero. No test ever differentiated such a function, so the whole family was uncovered, not just the tuple method. `seeded_reverse_pass!` now passes `input` on to the 3-arg `extract_result!`, which puts the untracked path on the same methods as the tracked one. The tuple method is then redundant, and a `result` that does not match the input is rejected by dispatch on both paths alike -- before, an untracked output silently filled a mismatched `result` with zeros. The `Number` methods constrain `input::TrackedArray` even though they ignore it. Leaving it unconstrained makes them match a tracked output too, since `TrackedReal <: Number`, which turns those dispatch errors back into silently-zero gradients. `extract_result_value!(result::Tuple, output)` was unreachable. Its only non-recursive call sites pass the `f!` output buffer, which `JacobianConfig` and `track!` constrain to an `AbstractArray`; a tuple of `DiffResult`s is handled one level up by `seeded_reverse_pass!`, which recurses per element. `extract_result_value!(::AbstractArray, ::TrackedArray)` was unreachable for the same reason: `output_hook` is an `Array{TrackedReal}` on those paths, never a `TrackedArray`. Both date back to the 2016 pre-release overhaul and are dropped. Co-Authored-By: Claude Opus 5 (1M context) --- src/api/utils.jl | 20 ++++---------------- test/api/GradientTests.jl | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/api/utils.jl b/src/api/utils.jl index 6540fe09..147cbd80 100644 --- a/src/api/utils.jl +++ b/src/api/utils.jl @@ -34,7 +34,7 @@ function seeded_reverse_pass!(result, output::TrackedReal, input, tape) end function seeded_reverse_pass!(result, output::Number, input, tape) - result = extract_result!(result, output) + result = extract_result!(result, output, input) return result end @@ -75,10 +75,6 @@ function extract_result!(result::NTuple{N,Any}, output, input::NTuple{N,Any}) wh return map((r, i) -> extract_result!(r, output, i), result, input) end -function extract_result!(result::Tuple, output) - return map(r -> extract_result!(r, output), result) -end - function extract_result!(result::AbstractArray, output::TrackedReal, input::TrackedArray) copyto!(result, deriv(input)) return result @@ -90,21 +86,18 @@ function extract_result!(result::DiffResult, output::TrackedReal, input::Tracked return result end -function extract_result!(result::AbstractArray, output::Number) +# `input` is unused, but constrained as above so a mismatched `result` still fails +function extract_result!(result::AbstractArray, output::Number, input::TrackedArray) fill_zeros!(result) return result end -function extract_result!(result::DiffResult, output::Number) +function extract_result!(result::DiffResult, output::Number, input::TrackedArray) result = DiffResults.value!(result, output) fill_zeros!(DiffResults.gradient(result)) return result end -function extract_result_value!(result::Tuple, output) - return map(r -> extract_result_value!(r, output), result) -end - function extract_result_value!(result::DiffResult, output::AbstractArray) result = DiffResults.value!(value, result, output) return result @@ -120,11 +113,6 @@ function extract_result_value!(result::AbstractArray, output::AbstractArray) return result end -function extract_result_value!(result::AbstractArray, output::TrackedArray) - copyto!(result, value(output)) - return result -end - fill_zeros!(result::AbstractArray) = fill!(result, zero(eltype(result))) ####################### diff --git a/test/api/GradientTests.jl b/test/api/GradientTests.jl index 92f2c93f..27b26840 100644 --- a/test/api/GradientTests.jl +++ b/test/api/GradientTests.jl @@ -331,4 +331,39 @@ g251(x, y) = sum(abs2, x) + sum(abs2, y) @test_throws MethodError ReverseDiff.gradient!((MVector(x), MVector(y), MVector(x)), g251, (x, y)) end +############################################################################################ + +# The output does not depend on the input, so it is recorded untracked and all derivatives +# are zero. +f_untracked(x) = 1.0 +g_untracked(x, y) = 2.0 + +@testset "output that does not depend on the input" begin + x, y = rand(3), rand(2) + + @test ReverseDiff.gradient(f_untracked, x) == zeros(3) + @test ReverseDiff.gradient(g_untracked, (x, y)) == (zeros(3), zeros(2)) + + result = ReverseDiff.gradient!(DiffResults.GradientResult(x), f_untracked, x) + @test DiffResults.value(result) == 1.0 + @test DiffResults.gradient(result) == zeros(3) + + result = (DiffResults.GradientResult(x), DiffResults.GradientResult(y)) + result = ReverseDiff.gradient!(result, g_untracked, (x, y)) + @test map(DiffResults.value, result) == (2.0, 2.0) + @test map(DiffResults.gradient, result) == (zeros(3), zeros(2)) + + tape = ReverseDiff.GradientTape(g_untracked, (x, y)) + for t in (tape, ReverseDiff.compile(tape)) + @test ReverseDiff.gradient!(t, (x, y)) == (zeros(3), zeros(2)) + @test ReverseDiff.gradient!((similar(x), similar(y)), t, (x, y)) == (zeros(3), zeros(2)) + end + + # a result that does not match the input tuple is rejected by dispatch, as when tracked + @test_throws MethodError ReverseDiff.gradient!((similar(x),), g_untracked, (x, y)) + @test_throws MethodError ReverseDiff.gradient!(similar(x), g_untracked, (x, y)) + @test_throws MethodError ReverseDiff.gradient!(similar(x), g251, (x, y)) + @test_throws MethodError ReverseDiff.gradient!(DiffResults.GradientResult(x), g251, (x, y)) +end + end # module From b69f5d79cc5d5439a1f556bd05d539e85771aaaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Thu, 17 Sep 2026 23:56:45 +0200 Subject: [PATCH 5/5] Document that an immutable `DiffResult` is replaced, not updated The `gradient!`/`jacobian!`/`hessian!` docstrings opened with "Returns `result`" and promised to store the derivatives in `result` rather than allocating. That is no longer the whole story: an `ImmutableDiffResult` cannot be mutated, so it is rebuilt and the caller must use the returned object. Since this is the release that makes the rebinding work, the contract needs to be stated where users read it. Co-Authored-By: Claude Opus 5 (1M context) --- src/api/gradients.jl | 14 ++++++++------ src/api/hessians.jl | 16 ++++++++++------ src/api/jacobians.jl | 15 +++++++++------ 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/api/gradients.jl b/src/api/gradients.jl index fe616651..234a44a8 100644 --- a/src/api/gradients.jl +++ b/src/api/gradients.jl @@ -29,13 +29,14 @@ end """ ReverseDiff.gradient!(result, f, input, cfg::GradientConfig = GradientConfig(input)) -Returns `result`. This method is exactly like `ReverseDiff.gradient(f, input, cfg)`, except -it stores the resulting gradient(s) in `result` rather than allocating new memory. +This method is exactly like `ReverseDiff.gradient(f, input, cfg)`, except it stores the +resulting gradient(s) in `result` rather than allocating new memory. `result` can be an `AbstractArray` or a `Tuple` of `AbstractArray`s. The `result` (or any of its elements, if `isa(result, Tuple)`), can also be a `DiffResults.DiffResult`, in which case the primal value `f(input)` (or `f(input...)`, if `isa(input, Tuple)`) will be stored -in it as well. +in it as well. An immutable `DiffResult` cannot be updated in place and is replaced, so use +the returned value: `result = ReverseDiff.gradient!(result, f, input, cfg)`. """ function gradient!(result, f, input, cfg::GradientConfig = GradientConfig(input)) tape = GradientTape(f, input, cfg) @@ -67,13 +68,14 @@ end """ ReverseDiff.gradient!(result, tape::Union{GradientTape,CompiledGradient}, input) -Returns `result`. This method is exactly like `ReverseDiff.gradient!(tape, input)`, except it -stores the resulting gradient(s) in `result` rather than allocating new memory. +This method is exactly like `ReverseDiff.gradient!(tape, input)`, except it stores the +resulting gradient(s) in `result` rather than allocating new memory. `result` can be an `AbstractArray` or a `Tuple` of `AbstractArray`s. The `result` (or any of its elements, if `isa(result, Tuple)`), can also be a `DiffResults.DiffResult`, in which case the primal value `f(input)` (or `f(input...)`, if `isa(input, Tuple)`) will be stored -in it as well. +in it as well. An immutable `DiffResult` cannot be updated in place and is replaced, so use +the returned value: `result = ReverseDiff.gradient!(result, tape, input)`. """ function gradient!(result, tape::Union{GradientTape,CompiledGradient}, input) seeded_forward_pass!(tape, input) diff --git a/src/api/hessians.jl b/src/api/hessians.jl index 6fb9175e..db66fec2 100644 --- a/src/api/hessians.jl +++ b/src/api/hessians.jl @@ -30,11 +30,13 @@ end ReverseDiff.hessian!(result::DiffResult, f, input::AbstractArray, cfg::HessianConfig = HessianConfig(result, input)) -Returns `result`. This method is exactly like `ReverseDiff.hessian(f, input, cfg)`, except -it stores the resulting Hessian in `result` rather than allocating new memory. +This method is exactly like `ReverseDiff.hessian(f, input, cfg)`, except it stores the +resulting Hessian in `result` rather than allocating new memory. If `result` is a `DiffResults.DiffResult`, the primal value `f(input)` and the gradient -`∇f(input)` will be stored in it along with the Hessian `H(f)(input)`. +`∇f(input)` will be stored in it along with the Hessian `H(f)(input)`. An immutable +`DiffResult` cannot be updated in place and is replaced, so use the returned value: +`result = ReverseDiff.hessian!(result, f, input, cfg)`. """ function hessian!(result, f, input::AbstractArray, cfg::HessianConfig = HessianConfig(input)) ∇f = x -> gradient(f, x, cfg.gradient_config) @@ -77,11 +79,13 @@ end ReverseDiff.hessian!(result::DiffResult, tape::Union{HessianTape,CompiledHessian}, input) -Returns `result`. This method is exactly like `ReverseDiff.hessian!(tape, input)`, except -it stores the resulting Hessian in `result` rather than allocating new memory. +This method is exactly like `ReverseDiff.hessian!(tape, input)`, except it stores the +resulting Hessian in `result` rather than allocating new memory. If `result` is a `DiffResults.DiffResult`, the primal value `f(input)` and the gradient -`∇f(input)` will be stored in it along with the Hessian `H(f)(input)`. +`∇f(input)` will be stored in it along with the Hessian `H(f)(input)`. An immutable +`DiffResult` cannot be updated in place and is replaced, so use the returned value: +`result = ReverseDiff.hessian!(result, tape, input)`. """ function hessian!(result::AbstractArray, tape::Union{HessianTape,CompiledHessian}, input::AbstractArray) seeded_forward_pass!(tape, input) diff --git a/src/api/jacobians.jl b/src/api/jacobians.jl index 800727a7..58c9fc5a 100644 --- a/src/api/jacobians.jl +++ b/src/api/jacobians.jl @@ -30,13 +30,14 @@ end """ ReverseDiff.jacobian!(result, f, input, cfg::JacobianConfig = JacobianConfig(input)) -Returns `result`. This method is exactly like `ReverseDiff.jacobian(f, input, cfg)`, except -it stores the resulting Jacobian(s) in `result` rather than allocating new memory. +This method is exactly like `ReverseDiff.jacobian(f, input, cfg)`, except it stores the +resulting Jacobian(s) in `result` rather than allocating new memory. `result` can be an `AbstractArray` or a `Tuple` of `AbstractArray`s. The `result` (or any of its elements, if `isa(result, Tuple)`), can also be a `DiffResults.DiffResult`, in which case the primal value `f(input)` (or `f(input...)`, if `isa(input, Tuple)`) will be stored -in it as well. +in it as well. An immutable `DiffResult` cannot be updated in place and is replaced, so use +the returned value: `result = ReverseDiff.jacobian!(result, f, input, cfg)`. """ function jacobian!(result, f, input, cfg::JacobianConfig = JacobianConfig(input)) tape = JacobianTape(f, input, cfg) @@ -110,12 +111,14 @@ end """ ReverseDiff.jacobian!(result, tape::Union{JacobianTape,CompiledJacobian}, input) -Returns `result`. This method is exactly like `ReverseDiff.jacobian!(tape, input)`, except it -stores the resulting Jacobian(s) in `result` rather than allocating new memory. +This method is exactly like `ReverseDiff.jacobian!(tape, input)`, except it stores the +resulting Jacobian(s) in `result` rather than allocating new memory. `result` can be an `AbstractArray` or a `Tuple` of `AbstractArray`s. The `result` (or any of its elements, if `isa(result, Tuple)`), can also be a `DiffResults.DiffResult`, in which -case the primal value of the target function will be stored in it as well. +case the primal value of the target function will be stored in it as well. An immutable +`DiffResult` cannot be updated in place and is replaced, so use the returned value: +`result = ReverseDiff.jacobian!(result, tape, input)`. """ function jacobian!(result, tape::Union{JacobianTape,CompiledJacobian}, input) seeded_forward_pass!(tape, input)