From 502fc899bdd4593c12e8e3c1da40e870c6f03270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Fri, 16 Oct 2026 14:57:51 +0200 Subject: [PATCH 1/5] Fix type piracy in demo tests The demos generated overloads such as `(op::typeof(-))(::Vararg{Union{Dual,Float64},2})`, which also match calls with only `Float64` arguments. This pirates e.g. `-(::Float64, ::Float64)` and is ambiguous with Base's methods, so the demo tests error with `MethodError: -(::Float64, ::Float64) is ambiguous`. For functions without arguments, the same code also overwrote ChainRulesCore's `_usethunks()`. Now one method is generated per position of the first `Dual`/`Tracked` argument, so every method requires at least one such argument and nothing is defined for functions without arguments. Co-Authored-By: Claude Opus 5.5 --- test/demos/forwarddiffzero.jl | 26 +++++++++++++++++--------- test/demos/reversediffzero.jl | 34 +++++++++++++++++++++------------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/test/demos/forwarddiffzero.jl b/test/demos/forwarddiffzero.jl index ae93459..362f37d 100644 --- a/test/demos/forwarddiffzero.jl +++ b/test/demos/forwarddiffzero.jl @@ -40,17 +40,25 @@ function define_dual_overload(sig) all(argT isa Type && Float64 <: argT for argT in argTs) || return # only handling purely Float64 ops. N = length(sig.parameters) - 1 # skip the op - fdef = quote - # we use the function call overloading form as it lets us avoid namespacing issues - # as we can directly interpolate the function type into to the AST. - function (op::$opT)(dual_args::Vararg{Union{Dual, Float64}, $N}; kwargs...) - ȧrgs = (NoTangent(), partial.(dual_args)...) - args = (op, primal.(dual_args)...) - y, ẏ = frule(ȧrgs, args...; kwargs...) - return Dual(y, ẏ) # if y, ẏ are not `Float64` this will error. + names = [Symbol(:x, j) for j in 1:N] + # Define one method per position of the first `Dual` argument, so that every method + # requires at least one `Dual` and we never overload `op` for purely `Float64` arguments + # (which would be type piracy). In particular, nothing is defined if `op` takes no arguments. + for i in 1:N + Ts = [fill(Float64, i - 1); Dual; fill(Union{Dual, Float64}, N - i)] + fdef = quote + # we use the function call overloading form as it lets us avoid namespacing issues + # as we can directly interpolate the function type into the AST. + function (op::$opT)($(map((x, T) -> :($x::$T), names, Ts)...); kwargs...) + dual_args = ($(names...),) + ȧrgs = (NoTangent(), partial.(dual_args)...) + args = (op, primal.(dual_args)...) + y, ẏ = frule(ȧrgs, args...; kwargs...) + return Dual(y, ẏ) # if y, ẏ are not `Float64` this will error. + end end + eval(fdef) end - eval(fdef) end # !Important!: Attach the define function to the `on_new_rule` hook diff --git a/test/demos/reversediffzero.jl b/test/demos/reversediffzero.jl index 578c6e0..b291783 100644 --- a/test/demos/reversediffzero.jl +++ b/test/demos/reversediffzero.jl @@ -69,22 +69,30 @@ function define_tracked_overload(sig) all(argT isa Type && Float64 <: argT for argT in argTs) || return # only handling purely Float64 ops. N = length(sig.parameters) - 1 # skip the op - fdef = quote - # we use the function call overloading form as it lets us avoid namespacing issues - # as we can directly interpolate the function type into to the AST. - function (op::$opT)(tracked_args::Vararg{Union{Tracked, Float64}, $N}; kwargs...) - args = (op, primal.(tracked_args)...) - y, y_pullback = rrule(args...; kwargs...) - the_tape = get_tape(tracked_args) - y_tracked = Tracked(y, the_tape) do ȳ - # pull this partial back and propagate it to the input's partial store - _, ārgs = Iterators.peel(y_pullback(ȳ)) - accum!.(tracked_args, ārgs) + names = [Symbol(:x, j) for j in 1:N] + # Define one method per position of the first `Tracked` argument, so that every method + # requires at least one `Tracked` and we never overload `op` for purely `Float64` arguments + # (which would be type piracy). In particular, nothing is defined if `op` takes no arguments. + for i in 1:N + Ts = [fill(Float64, i - 1); Tracked; fill(Union{Tracked, Float64}, N - i)] + fdef = quote + # we use the function call overloading form as it lets us avoid namespacing issues + # as we can directly interpolate the function type into the AST. + function (op::$opT)($(map((x, T) -> :($x::$T), names, Ts)...); kwargs...) + tracked_args = ($(names...),) + args = (op, primal.(tracked_args)...) + y, y_pullback = rrule(args...; kwargs...) + the_tape = get_tape(tracked_args) + y_tracked = Tracked(y, the_tape) do ȳ + # pull this partial back and propagate it to the input's partial store + _, ārgs = Iterators.peel(y_pullback(ȳ)) + accum!.(tracked_args, ārgs) + end + return y_tracked end - return y_tracked end + eval(fdef) end - eval(fdef) end # !Important!: Attach the define function to the `on_new_rule` hook From 6e868fb3585418f4b793c685e7ceb01251866cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Fri, 16 Oct 2026 15:35:45 +0200 Subject: [PATCH 2/5] Document how to avoid type piracy in generated overloads Co-Authored-By: Claude Opus 5.5 --- docs/src/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/src/index.md b/docs/src/index.md index d29896f..3b60327 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -48,6 +48,12 @@ Packages like [ExprTools.jl](https://github.com/invenia/ExprTools.jl) can be hel Its generally fairly simple, though can become complex if you need to handle complicated type-constraints. Examples are shown below. +Make sure every generated overload requires at least one argument of your AD's type. +An overload such as `(op::typeof(+))(::Vararg{Union{Dual,Float64},2})` also matches +`+(::Float64, ::Float64)`, which is type piracy and is ambiguous with Base's methods. +One way to avoid this is to define one method per position of the first argument of your +AD's type, as the examples do. + The hook is automatically triggered whenever a package is loaded. It can also be triggers manually using `refresh_rules`(@ref). This is useful for example if new rules are define in the REPL, or if a package defining rules is modified. From e4655894fe8acb54c282a7a457093972310dc873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Fri, 16 Oct 2026 15:59:52 +0200 Subject: [PATCH 3/5] Update outdated GitHub Actions GitHub refuses to run workflows using `actions/cache@v2`. Also bump the other actions to supported versions. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/CI.yml | 18 +++++++++--------- .github/workflows/IntegrationTest.yml | 8 ++++---- .github/workflows/JuliaNightly.yml | 14 +++++++------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d2cd299..5b38a57 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -34,12 +34,12 @@ jobs: version: "1.5" arch: x64 steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 + - uses: actions/checkout@v7 + - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v2 + - uses: actions/cache@v4 env: cache-name: cache-artifacts with: @@ -50,19 +50,19 @@ jobs: ${{ runner.os }}-${{ matrix.arch }}-test- ${{ runner.os }}-${{ matrix.arch }}- ${{ runner.os }}- - - uses: julia-actions/julia-buildpkg@latest - - uses: julia-actions/julia-runtest@latest + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v6 with: - file: lcov.info + files: lcov.info docs: name: Documentation runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 + - uses: actions/checkout@v7 + - uses: julia-actions/setup-julia@v2 with: version: '1.6' - run: | diff --git a/.github/workflows/IntegrationTest.yml b/.github/workflows/IntegrationTest.yml index 139a131..b789aa2 100644 --- a/.github/workflows/IntegrationTest.yml +++ b/.github/workflows/IntegrationTest.yml @@ -18,14 +18,14 @@ jobs: # - {user: Invenia, repo: Nabla.jl} steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 + - uses: actions/checkout@v7 + - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.julia-version }} arch: x64 - - uses: julia-actions/julia-buildpkg@latest + - uses: julia-actions/julia-buildpkg@v1 - name: Clone Downstream - uses: actions/checkout@v2 + uses: actions/checkout@v7 with: repository: ${{ matrix.package.user }}/${{ matrix.package.repo }} path: downstream diff --git a/.github/workflows/JuliaNightly.yml b/.github/workflows/JuliaNightly.yml index 951a1a2..a67e242 100644 --- a/.github/workflows/JuliaNightly.yml +++ b/.github/workflows/JuliaNightly.yml @@ -12,12 +12,12 @@ jobs: name: Julia Nightly - Ubuntu - x64 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 + - uses: actions/checkout@v7 + - uses: julia-actions/setup-julia@v2 with: version: nightly arch: x64 - - uses: actions/cache@v2 + - uses: actions/cache@v4 env: cache-name: julia-nightly-cache-artifacts with: @@ -25,9 +25,9 @@ jobs: key: ${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} restore-keys: | ${{ env.cache-name }}- - - uses: julia-actions/julia-buildpkg@latest - - uses: julia-actions/julia-runtest@latest + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v6 with: - file: lcov.info + files: lcov.info From c88ab39024ecfe2c530f4c204d12eba05e58d1e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Fri, 16 Oct 2026 16:21:18 +0200 Subject: [PATCH 4/5] Update GitHub Actions to latest versions and use native binaries Run Julia with the runner's native architecture instead of forcing x64. Julia 1.0 has no native Apple Silicon binaries, so it is no longer tested on macOS. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/CI.yml | 15 +++++++++------ .github/workflows/IntegrationTest.yml | 3 +-- .github/workflows/JuliaNightly.yml | 7 +++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5b38a57..b85b222 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -20,7 +20,7 @@ jobs: - macOS-latest - windows-latest arch: - - x64 + - default - x86 exclude: # Test 32-bit only on Linux @@ -28,18 +28,21 @@ jobs: arch: x86 - os: windows-latest arch: x86 + # No native Apple Silicon binaries for Julia 1.0 + - os: macOS-latest + version: "1.0" include: # Add a 1.5 job because that's what Invenia actually uses - os: ubuntu-latest version: "1.5" - arch: x64 + arch: default steps: - uses: actions/checkout@v7 - - uses: julia-actions/setup-julia@v2 + - uses: julia-actions/setup-julia@v3 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v4 + - uses: actions/cache@v6 env: cache-name: cache-artifacts with: @@ -53,7 +56,7 @@ jobs: - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v6 + - uses: codecov/codecov-action@v7 with: files: lcov.info @@ -62,7 +65,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - uses: julia-actions/setup-julia@v2 + - uses: julia-actions/setup-julia@v3 with: version: '1.6' - run: | diff --git a/.github/workflows/IntegrationTest.yml b/.github/workflows/IntegrationTest.yml index b789aa2..fbf1372 100644 --- a/.github/workflows/IntegrationTest.yml +++ b/.github/workflows/IntegrationTest.yml @@ -19,10 +19,9 @@ jobs: steps: - uses: actions/checkout@v7 - - uses: julia-actions/setup-julia@v2 + - uses: julia-actions/setup-julia@v3 with: version: ${{ matrix.julia-version }} - arch: x64 - uses: julia-actions/julia-buildpkg@v1 - name: Clone Downstream uses: actions/checkout@v7 diff --git a/.github/workflows/JuliaNightly.yml b/.github/workflows/JuliaNightly.yml index a67e242..d5af79a 100644 --- a/.github/workflows/JuliaNightly.yml +++ b/.github/workflows/JuliaNightly.yml @@ -13,11 +13,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - uses: julia-actions/setup-julia@v2 + - uses: julia-actions/setup-julia@v3 with: version: nightly - arch: x64 - - uses: actions/cache@v4 + - uses: actions/cache@v6 env: cache-name: julia-nightly-cache-artifacts with: @@ -28,6 +27,6 @@ jobs: - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v6 + - uses: codecov/codecov-action@v7 with: files: lcov.info From 9de02806e3de11f3c77f614008b478c00b47aca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Fri, 16 Oct 2026 16:31:26 +0200 Subject: [PATCH 5/5] Test Julia 1.0 on an Intel macOS runner Co-Authored-By: Claude Opus 5.5 --- .github/workflows/CI.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b85b222..a435a82 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -28,7 +28,7 @@ jobs: arch: x86 - os: windows-latest arch: x86 - # No native Apple Silicon binaries for Julia 1.0 + # No native Apple Silicon binaries for Julia 1.0, test it on an Intel runner instead - os: macOS-latest version: "1.0" include: @@ -36,6 +36,9 @@ jobs: - os: ubuntu-latest version: "1.5" arch: default + - os: macos-15-intel + version: "1.0" + arch: default steps: - uses: actions/checkout@v7 - uses: julia-actions/setup-julia@v3