Skip to content

Support arrays with non-concrete eltype (fixes #264) - #300

Merged
devmotion merged 2 commits into
masterfrom
dmw/nonconcrete-eltype-arrays
Sep 18, 2026
Merged

devmotion merged 2 commits into
masterfrom
dmw/nonconcrete-eltype-arrays

Conversation

@devmotion

Copy link
Copy Markdown
Member

Fixes #264.

istracked(::AbstractArray) only promises that an array may hold tracked elements. Two consumers mishandled that, in opposite directions.

increment_deriv!/decrement_deriv! assumed every element is tracked, so a Vector{Real} of plain Float64s threw MethodError: increment_deriv!(::Float64, ::Float64). They now skip untracked elements; a direct call with an untracked scalar still throws.

_add_to_deriv! matched only AbstractArray{<:TrackedReal}, so every @grad rule fell through to the nothing fallback and silently discarded the derivative:

f(x) = sum([Real[x[1], x[2]]; x[3]])
ForwardDiff.gradient(f, [1.0, 2.0, 3.0])  # [1.0, 1.0, 1.0]
ReverseDiff.gradient(f, [1.0, 2.0, 3.0])  # [0.0, 0.0, 1.0]

It now asks istracked directly. Same bound as 50b47bf widened once more — that commit incidentally fixed the MWE of #145 (bisected: broken in v1.5.0, fixed in v1.6.0) without closing it, and this covers what remained.

Finally, istracked(::AbstractArray{T}) used !isconcretetype(T) as a proxy for "could an element be tracked". Asking whether a tracked type can inhabit T is exact and folds to a compile-time constant. One behaviour change: istracked(::Vector{TrackedArray}) goes false → true, so value now unwraps it and tape finds its tape.

🤖 Generated with Claude Code

`istracked(::AbstractArray)` only promises that an array *may* hold tracked
elements: for a non-concrete eltype it cannot know. Two consumers mishandled
that, in opposite directions.

`increment_deriv!`/`decrement_deriv!` treated it as a promise that every
element *is* tracked and indexed straight into `increment_deriv!(t[i], ...)`,
so a `Vector{Real}` of plain `Float64`s threw `MethodError`. They now skip
untracked elements. A direct `increment_deriv!(::Float64, ::Float64)` still
throws, so a mis-wired call elsewhere stays loud.

`_add_to_deriv!` went the other way and matched only
`AbstractArray{<:TrackedReal}`, which a `Vector{Real}` does not satisfy, so
every `@grad` rule fell through to the `nothing` fallback and *silently*
discarded the derivative:

    f(x) = sum([Real[x[1], x[2]]; x[3]])
    ForwardDiff.gradient(f, [1.0, 2.0, 3.0])  # [1.0, 1.0, 1.0]
    ReverseDiff.gradient(f, [1.0, 2.0, 3.0])  # [0.0, 0.0, 1.0]

It now asks `istracked` directly. This is the same bound widened once more:
50b47bf took it from `TrackedArray` to `AbstractArray{<:TrackedReal}`, which
incidentally fixed the MWE of #145 without closing it; the non-concrete
eltype case is what remained.

Finally, `istracked(::AbstractArray{T})` used `!isconcretetype(T)` as a proxy
for "could an element be tracked". That is wrong in both directions: it
admits `AbstractString`, `Integer` and `Function` eltypes, and rejects
`Vector{TrackedArray}`, for which `tape` returned `NULL_TAPE` even though the
elements carry a tape. Asking whether a tracked type can inhabit `T` is exact
and folds to a compile-time constant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.92%. Comparing base (3ddf171) to head (97ee3b0).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #300      +/-   ##
==========================================
+ Coverage   86.69%   86.92%   +0.23%     
==========================================
  Files          19       19              
  Lines        1916     1950      +34     
==========================================
+ Hits         1661     1695      +34     
  Misses        255      255              

☔ 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.

Pullbacks may return their tangents lazily, and ChainRules' own rules
routinely do, but nothing in the test suite exercised that path: no rrule
returned a thunk. Cover both call sites of `_add_to_deriv!`, both concrete
`AbstractThunk` subtypes, and both legs of its `Union{TrackedReal,
AbstractArray}` signature.

The `g_thunk` cases also pin the `istracked` guard added here: thunks aimed
at an untracked argument are dropped rather than forced, and a repeated
tracked element in a `Vector{Real}` accumulates.

Without the `AbstractThunk` method every new assertion fails with a
`MethodError` from `increment_deriv!`, which only accepts arrays and reals.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@devmotion
devmotion merged commit e127522 into master Sep 18, 2026
8 checks passed
@devmotion
devmotion deleted the dmw/nonconcrete-eltype-arrays branch September 18, 2026 23:13
devmotion added a commit that referenced this pull request Sep 22, 2026
…, and pin #126 (#302)

* Test indexing a zero-dimensional `TrackedArray` (#126)

`gradient(x -> x[], fill(1.0))` threw a `MethodError` for an ambiguity between
the `AbstractRange...` and `Colon...` methods of `getindex`: a bare vararg
matches zero arguments, so `t[]` matched both equally.

4a50321 fixed it incidentally while fixing #202, by rewriting every
`getindex(t::TrackedArray, i::T...)` as `i1::T, is::T...`. Requiring a leading
index sends `t[]` down `Base`'s generic zero-index `AbstractArray` path, which
lands on the `Integer` method via linear indexing -- so, like any scalar
`getindex`, it aliases its parent through the origin and records nothing.

Nothing pinned that, and the ambiguity would come back unnoticed with any
future widening of those signatures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Give tracked reals with a free origin a `zero` and a `one` (fixes #172)

`map`ping over a tracked array mixes elements that carry an origin with ones
that do not, and the resulting eltype leaves the origin free. `reducedim_init`
allocates a reduction's accumulator by asking that eltype for its `zero`, or
`one` for `prod`, which fell through to `convert(T, 0)` and then to `valtype`,
defined only for a pinned origin:

    MethodError: no method matching valtype(::Type{TrackedReal{Float64, Float64}})

Neither identity depends on the origin, so both are deducible from the type
alone, as `zero` requires. `Nothing` is the origin `zero` has always used, and
`TrackedReal{V,D,Nothing}` is an instance of `TrackedReal{V,D}`, so the
documented `zero(T) isa T` holds. The signature matches that type exactly
rather than its subtypes: a `Union` of two origins is also a subtype, and no
single member of it could be returned.

The `zero` method is the one #173 added and #177 reverted wholesale over #175,
whose cause -- `increment_deriv!` indexing into untracked elements -- was fixed
by #300. `one` is its counterpart, which #173 never added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
devmotion added a commit that referenced this pull request Sep 24, 2026
`Diagonal` and `diagm` of a `Vector{Real}` give tracked diagonal entries
and untracked zeros, whose reverse pass threw `increment_deriv!(::Float64,
::Float64)` until #300 made it skip untracked elements. Check against the
analytical Jacobians, directly and on a compiled tape.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

increment_deriv!(::Float64, ::Float64) MethodError with a vector of Reals

1 participant