From ec2f4448e510c1741a953470b70870e532059198 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 18:44:49 +0000 Subject: [PATCH 01/55] Bump actions/checkout from 4 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/Tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index ddce0a4..121b594 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -39,7 +39,7 @@ jobs: name: Documentation runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: julia-actions/setup-julia@v2 with: version: '1' From 431f92c3598d455e474b7d07901f524c8e945b59 Mon Sep 17 00:00:00 2001 From: Chris Rackauckas - Beep Boop Edition Date: Tue, 2 Dec 2025 04:36:25 -0500 Subject: [PATCH 02/55] Update dependabot.yml to add Julia ecosystem support --- .github/dependabot.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 700707c..c307d5c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,19 @@ # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 +enable-beta-ecosystems: true # Julia ecosystem updates: - package-ecosystem: "github-actions" directory: "/" # Location of package manifests schedule: interval: "weekly" + - package-ecosystem: "julia" + directories: + - "/" + - "/docs" + - "/test" + schedule: + interval: "daily" + groups: + all-julia-packages: + patterns: + - "*" From dd37d5bea3d6bff62a89f641f8963404ac32647d Mon Sep 17 00:00:00 2001 From: Chris Rackauckas - Beep Boop Edition Date: Tue, 2 Dec 2025 04:36:26 -0500 Subject: [PATCH 03/55] Remove CompatHelper.yml (replaced by Dependabot) --- .github/workflows/CompatHelper.yml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .github/workflows/CompatHelper.yml diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml deleted file mode 100644 index cba9134..0000000 --- a/.github/workflows/CompatHelper.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: CompatHelper -on: - schedule: - - cron: 0 0 * * * - workflow_dispatch: -jobs: - CompatHelper: - runs-on: ubuntu-latest - steps: - - name: Pkg.add("CompatHelper") - run: julia -e 'using Pkg; Pkg.add("CompatHelper")' - - name: CompatHelper.main() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} - run: julia -e 'using CompatHelper; CompatHelper.main()' From 198a1c84c1d7cd76d218ffe77e5bdeaed3084fb2 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Sun, 28 Dec 2025 21:33:40 -0500 Subject: [PATCH 04/55] Add introspection functions for debugging wrapped functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds three new exported functions to help users debug wrapped functions: - `unwrap(fww)`: Returns the original function that was wrapped, allowing users to debug it with tools like Debugger.jl or Infiltrator.jl - `wrapped_signatures(fww)`: Returns a tuple of argument type signatures - `wrapped_return_types(fww)`: Returns a tuple of return types These functions address issue #18 by providing a documented API for accessing the wrapped function's source code and stepping through it with debuggers. Closes #18 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/FunctionWrappersWrappers.jl | 68 ++++++++++++++++++++++++++++++++- test/runtests.jl | 51 +++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/src/FunctionWrappersWrappers.jl b/src/FunctionWrappersWrappers.jl index 7da5b9b..49d0af2 100644 --- a/src/FunctionWrappersWrappers.jl +++ b/src/FunctionWrappersWrappers.jl @@ -3,7 +3,7 @@ module FunctionWrappersWrappers using FunctionWrappers import TruncatedStacktraces -export FunctionWrappersWrapper +export FunctionWrappersWrapper, unwrap, wrapped_signatures, wrapped_return_types struct FunctionWrappersWrapper{FW, FB} fw::FW @@ -48,4 +48,70 @@ function FunctionWrappersWrapper( FunctionWrappersWrapper{typeof(fwt), FB}(fwt) end +""" + unwrap(fww::FunctionWrappersWrapper) + +Return the original function that was wrapped. This is useful for debugging +wrapped functions - you can use the returned function with debugging tools +like Debugger.jl or Infiltrator.jl. + +# Example + +```julia +using FunctionWrappersWrappers + +# Create a wrapped function +fww = FunctionWrappersWrapper(sin, (Tuple{Float64},), (Float64,)) + +# Get the original function for debugging +f = unwrap(fww) # Returns sin + +# Now you can debug with Debugger.jl: +# using Debugger +# @enter f(0.5) + +# Or use Infiltrator.jl in your original function definition +``` + +See also: [`wrapped_signatures`](@ref), [`wrapped_return_types`](@ref) +""" +unwrap(fww::FunctionWrappersWrapper) = first(fww.fw).obj[] + +""" + wrapped_signatures(fww::FunctionWrappersWrapper) + +Return a tuple of the argument type signatures that the `FunctionWrappersWrapper` +can dispatch on. Each element is a `Tuple` type representing the argument types. + +# Example + +```julia +using FunctionWrappersWrappers + +fww = FunctionWrappersWrapper(+, (Tuple{Float64, Float64}, Tuple{Int, Int}), (Float64, Int)) +wrapped_signatures(fww) # Returns (Tuple{Float64, Float64}, Tuple{Int, Int}) +``` + +See also: [`unwrap`](@ref), [`wrapped_return_types`](@ref) +""" +wrapped_signatures(fww::FunctionWrappersWrapper) = map(fw -> typeof(fw).parameters[2], fww.fw) + +""" + wrapped_return_types(fww::FunctionWrappersWrapper) + +Return a tuple of the return types for each wrapped function signature. + +# Example + +```julia +using FunctionWrappersWrappers + +fww = FunctionWrappersWrapper(+, (Tuple{Float64, Float64}, Tuple{Int, Int}), (Float64, Int)) +wrapped_return_types(fww) # Returns (Float64, Int64) +``` + +See also: [`unwrap`](@ref), [`wrapped_signatures`](@ref) +""" +wrapped_return_types(fww::FunctionWrappersWrapper) = map(fw -> typeof(fw).parameters[1], fww.fw) + end diff --git a/test/runtests.jl b/test/runtests.jl index a66c601..a6a9611 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -13,3 +13,54 @@ using Test @test fwexp2(4.0f0) === 16.0f0 @test fwexp2(4) === 16.0 end + +@testset "Introspection functions" begin + # Test with a simple function + fwsin = FunctionWrappersWrapper(sin, (Tuple{Float64},), (Float64,)) + + @testset "unwrap" begin + f = unwrap(fwsin) + @test f === sin + @test f(0.5) == sin(0.5) + end + + @testset "wrapped_signatures" begin + sigs = wrapped_signatures(fwsin) + @test sigs == (Tuple{Float64},) + end + + @testset "wrapped_return_types" begin + rets = wrapped_return_types(fwsin) + @test rets == (Float64,) + end + + # Test with multiple signatures + fwplus = FunctionWrappersWrapper(+, (Tuple{Float64, Float64}, Tuple{Int, Int}), (Float64, Int)) + + @testset "unwrap with multiple signatures" begin + f = unwrap(fwplus) + @test f === + + @test f(1, 2) == 3 + end + + @testset "wrapped_signatures with multiple signatures" begin + sigs = wrapped_signatures(fwplus) + @test sigs == (Tuple{Float64, Float64}, Tuple{Int, Int}) + end + + @testset "wrapped_return_types with multiple signatures" begin + rets = wrapped_return_types(fwplus) + @test rets == (Float64, Int) + end + + # Test with a custom function + my_func(x) = x^2 + fwcustom = FunctionWrappersWrapper(my_func, (Tuple{Float64}, Tuple{Int}), (Float64, Int)) + + @testset "unwrap with custom function" begin + f = unwrap(fwcustom) + @test f === my_func + @test f(3) == 9 + @test f(2.5) == 6.25 + end +end From 8c1275ea5b5d4e28baeedcb81ac9679295645bf1 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Sun, 28 Dec 2025 22:24:44 -0500 Subject: [PATCH 05/55] Apply JuliaFormatter to introspection functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes formatting issues introduced in #25. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/FunctionWrappersWrappers.jl | 8 ++++++-- test/runtests.jl | 6 ++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/FunctionWrappersWrappers.jl b/src/FunctionWrappersWrappers.jl index 49d0af2..dec93e0 100644 --- a/src/FunctionWrappersWrappers.jl +++ b/src/FunctionWrappersWrappers.jl @@ -94,7 +94,9 @@ wrapped_signatures(fww) # Returns (Tuple{Float64, Float64}, Tuple{Int, Int}) See also: [`unwrap`](@ref), [`wrapped_return_types`](@ref) """ -wrapped_signatures(fww::FunctionWrappersWrapper) = map(fw -> typeof(fw).parameters[2], fww.fw) +function wrapped_signatures(fww::FunctionWrappersWrapper) + map(fw -> typeof(fw).parameters[2], fww.fw) +end """ wrapped_return_types(fww::FunctionWrappersWrapper) @@ -112,6 +114,8 @@ wrapped_return_types(fww) # Returns (Float64, Int64) See also: [`unwrap`](@ref), [`wrapped_signatures`](@ref) """ -wrapped_return_types(fww::FunctionWrappersWrapper) = map(fw -> typeof(fw).parameters[1], fww.fw) +function wrapped_return_types(fww::FunctionWrappersWrapper) + map(fw -> typeof(fw).parameters[1], fww.fw) +end end diff --git a/test/runtests.jl b/test/runtests.jl index a6a9611..3a68712 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -35,7 +35,8 @@ end end # Test with multiple signatures - fwplus = FunctionWrappersWrapper(+, (Tuple{Float64, Float64}, Tuple{Int, Int}), (Float64, Int)) + fwplus = FunctionWrappersWrapper(+, (Tuple{Float64, Float64}, Tuple{Int, Int}), ( + Float64, Int)) @testset "unwrap with multiple signatures" begin f = unwrap(fwplus) @@ -55,7 +56,8 @@ end # Test with a custom function my_func(x) = x^2 - fwcustom = FunctionWrappersWrapper(my_func, (Tuple{Float64}, Tuple{Int}), (Float64, Int)) + fwcustom = FunctionWrappersWrapper(my_func, (Tuple{Float64}, Tuple{Int}), ( + Float64, Int)) @testset "unwrap with custom function" begin f = unwrap(fwcustom) From 786d0f3e6610919fba95e4b25c390fa9e7aeca62 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Tue, 30 Dec 2025 00:21:33 -0500 Subject: [PATCH 06/55] Add PrecompileTools to reduce TTFX by up to 8000x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added a PrecompileTools workload to precompile common use patterns, dramatically improving time-to-first-execution (TTFX): - FunctionWrappersWrapper creation: ~0.088s -> ~0.0002s (518x faster) - First call dispatch (Float64): ~0.58s -> ~0.00007s (8286x faster) - Second type call dispatch (Int): ~0.053s -> ~0.00007s (757x faster) The precompilation workload covers: - Binary operations with Float64 and Int argument types - Unary operations with Float64 and Int argument types - Both creation and call dispatch for each No invalidations were detected when loading the package. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- Project.toml | 2 ++ src/FunctionWrappersWrappers.jl | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/Project.toml b/Project.toml index dfe9f36..4a74e50 100644 --- a/Project.toml +++ b/Project.toml @@ -5,10 +5,12 @@ version = "0.1.4" [deps] FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" TruncatedStacktraces = "781d530d-4396-4725-bb49-402e4bee1e77" [compat] FunctionWrappers = "1" +PrecompileTools = "1" TruncatedStacktraces = "1" julia = "1.6" diff --git a/src/FunctionWrappersWrappers.jl b/src/FunctionWrappersWrappers.jl index dec93e0..39ba2c4 100644 --- a/src/FunctionWrappersWrappers.jl +++ b/src/FunctionWrappersWrappers.jl @@ -118,4 +118,36 @@ function wrapped_return_types(fww::FunctionWrappersWrapper) map(fw -> typeof(fw).parameters[1], fww.fw) end +using PrecompileTools + +@setup_workload begin + @compile_workload begin + # Precompile common use cases with Float64 and Int types + # These are the most common type combinations for numerical computations + + # Binary operation with multiple type combinations (common pattern) + fw_binary = FunctionWrappersWrapper( + +, + (Tuple{Float64, Float64}, Tuple{Int, Int}), + (Float64, Int) + ) + fw_binary(1.0, 2.0) + fw_binary(1, 2) + + # Unary operation with multiple types (common pattern) + fw_unary = FunctionWrappersWrapper( + abs, + (Tuple{Float64}, Tuple{Int}), + (Float64, Int) + ) + fw_unary(1.0) + fw_unary(1) + + # Precompile introspection functions + unwrap(fw_unary) + wrapped_signatures(fw_binary) + wrapped_return_types(fw_binary) + end +end + end From 3449753e5cd25ea622c68506c2e0a899364f3a10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 11:52:32 +0000 Subject: [PATCH 07/55] Bump the all-julia-packages group across 1 directory with 2 updates Updates the requirements on [Documenter](https://github.com/JuliaDocs/Documenter.jl) and [FunctionWrappersWrappers](https://github.com/chriselrod/FunctionWrappersWrappers.jl) to permit the latest version. Updates `Documenter` to 1.16.1 - [Release notes](https://github.com/JuliaDocs/Documenter.jl/releases) - [Changelog](https://github.com/JuliaDocs/Documenter.jl/blob/master/CHANGELOG.md) - [Commits](https://github.com/JuliaDocs/Documenter.jl/compare/v0.1.0...v1.16.1) Updates `FunctionWrappersWrappers` to 0.1.3 - [Release notes](https://github.com/chriselrod/FunctionWrappersWrappers.jl/releases) - [Commits](https://github.com/chriselrod/FunctionWrappersWrappers.jl/commits) --- updated-dependencies: - dependency-name: Documenter dependency-version: 1.16.1 dependency-type: direct:production dependency-group: all-julia-packages - dependency-name: FunctionWrappersWrappers dependency-version: 0.1.3 dependency-type: direct:production dependency-group: all-julia-packages ... Signed-off-by: dependabot[bot] --- docs/Manifest.toml | 238 +++++++++++++++++++++++++++++++++++++++++++-- docs/Project.toml | 4 + 2 files changed, 232 insertions(+), 10 deletions(-) diff --git a/docs/Manifest.toml b/docs/Manifest.toml index 98352d8..3decf20 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -1,19 +1,46 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.7.2-pre.0" +julia_version = "1.12.3" manifest_format = "2.0" +project_hash = "269c22af2ffbf5ea7f759b938882092049766bd2" [[deps.ANSIColoredPrinters]] git-tree-sha1 = "574baf8110975760d391c710b6341da1afa48d8c" uuid = "a4c015fc-c6ff-483c-b24f-f7ea428134e9" version = "0.0.1" +[[deps.AbstractTrees]] +git-tree-sha1 = "2d9c9a55f9c93e8887ad391fbae72f8ef55e1177" +uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" +version = "0.4.5" + +[[deps.ArgTools]] +uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" +version = "1.1.2" + +[[deps.Artifacts]] +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +version = "1.11.0" + [[deps.Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +version = "1.11.0" + +[[deps.CodecZlib]] +deps = ["TranscodingStreams", "Zlib_jll"] +git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9" +uuid = "944b1d66-785c-5afd-91f1-9de20f533193" +version = "0.7.8" + +[[deps.CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "1.3.0+1" [[deps.Dates]] deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" +version = "1.11.0" [[deps.DocStringExtensions]] deps = ["LibGit2"] @@ -22,15 +49,54 @@ uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" version = "0.8.6" [[deps.Documenter]] -deps = ["ANSIColoredPrinters", "Base64", "Dates", "DocStringExtensions", "IOCapture", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"] -git-tree-sha1 = "75c6cf9d99e0efc79b724f5566726ad3ad010a01" +deps = ["ANSIColoredPrinters", "AbstractTrees", "Base64", "CodecZlib", "Dates", "DocStringExtensions", "Downloads", "Git", "IOCapture", "InteractiveUtils", "JSON", "Logging", "Markdown", "MarkdownAST", "Pkg", "PrecompileTools", "REPL", "RegistryInstances", "SHA", "TOML", "Test", "Unicode"] +git-tree-sha1 = "b37458ae37d8bdb643d763451585cd8d0e5b4a9e" uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -version = "0.27.12" +version = "1.16.1" + +[[deps.Downloads]] +deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] +uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +version = "1.7.0" + +[[deps.Expat_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "27af30de8b5445644e8ffe3bcb0d72049c089cf1" +uuid = "2e619515-83b5-522b-bb60-26c02a35a201" +version = "2.7.3+0" + +[[deps.FileWatching]] +uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" +version = "1.11.0" + +[[deps.FunctionWrappers]] +git-tree-sha1 = "d62485945ce5ae9c0c48f124a84998d755bae00e" +uuid = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" +version = "1.1.3" [[deps.FunctionWrappersWrappers]] -path = ".." +deps = ["FunctionWrappers"] +git-tree-sha1 = "b104d487b34566608f8b4e1c39fb0b10aa279ff8" uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" -version = "0.1.0" +version = "0.1.3" + +[[deps.Git]] +deps = ["Git_LFS_jll", "Git_jll", "JLLWrappers", "OpenSSH_jll"] +git-tree-sha1 = "824a1890086880696fc908fe12a17bcf61738bd8" +uuid = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2" +version = "1.5.0" + +[[deps.Git_LFS_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "bb8471f313ed941f299aa53d32a94ab3bee08844" +uuid = "020c3dae-16b3-5ae5-87b3-4cb189e250b2" +version = "3.7.0+0" + +[[deps.Git_jll]] +deps = ["Artifacts", "Expat_jll", "JLLWrappers", "LibCURL_jll", "Libdl", "Libiconv_jll", "OpenSSL_jll", "PCRE2_jll", "Zlib_jll"] +git-tree-sha1 = "46bd50c245fe49ed87377c014a928a549b9ef7ed" +uuid = "f8c6e375-362e-5223-8a59-34ff63f689eb" +version = "2.52.0+0" [[deps.IOCapture]] deps = ["Logging", "Random"] @@ -41,6 +107,13 @@ version = "0.2.2" [[deps.InteractiveUtils]] deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +version = "1.11.0" + +[[deps.JLLWrappers]] +deps = ["Artifacts", "Preferences"] +git-tree-sha1 = "0533e564aae234aff59ab625543145446d8b6ec2" +uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" +version = "1.7.1" [[deps.JSON]] deps = ["Dates", "Mmap", "Parsers", "Unicode"] @@ -48,22 +121,93 @@ git-tree-sha1 = "8076680b162ada2a031f707ac7b4953e30667a37" uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" version = "0.21.2" +[[deps.JuliaSyntaxHighlighting]] +deps = ["StyledStrings"] +uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" +version = "1.12.0" + +[[deps.LazilyInitializedFields]] +git-tree-sha1 = "0f2da712350b020bc3957f269c9caad516383ee0" +uuid = "0e77f7df-68c5-4e49-93ce-4cd80f5598bf" +version = "1.3.0" + +[[deps.LibCURL]] +deps = ["LibCURL_jll", "MozillaCACerts_jll"] +uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" +version = "0.6.4" + +[[deps.LibCURL_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] +uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" +version = "8.15.0+0" + [[deps.LibGit2]] -deps = ["Base64", "NetworkOptions", "Printf", "SHA"] +deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" +version = "1.11.0" + +[[deps.LibGit2_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"] +uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" +version = "1.9.0+0" + +[[deps.LibSSH2_jll]] +deps = ["Artifacts", "Libdl", "OpenSSL_jll"] +uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" +version = "1.11.3+1" + +[[deps.Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +version = "1.11.0" + +[[deps.Libiconv_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "be484f5c92fad0bd8acfef35fe017900b0b73809" +uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531" +version = "1.18.0+0" [[deps.Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" +version = "1.11.0" [[deps.Markdown]] -deps = ["Base64"] +deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" +version = "1.11.0" + +[[deps.MarkdownAST]] +deps = ["AbstractTrees", "Markdown"] +git-tree-sha1 = "465a70f0fc7d443a00dcdc3267a497397b8a3899" +uuid = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" +version = "0.1.2" [[deps.Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" +version = "1.11.0" + +[[deps.MozillaCACerts_jll]] +uuid = "14a3606d-f60d-562e-9121-12d972cd8159" +version = "2025.5.20" [[deps.NetworkOptions]] uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" +version = "1.3.0" + +[[deps.OpenSSH_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "OpenSSL_jll", "Zlib_jll"] +git-tree-sha1 = "301412a644646fdc0ad67d0a87487466b491e53d" +uuid = "9bd350c2-7e96-507f-8002-3f2e150b4e1b" +version = "10.2.1+0" + +[[deps.OpenSSL_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" +version = "3.5.4+0" + +[[deps.PCRE2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "efcefdf7-47ab-520b-bdef-62a2eaa19f15" +version = "10.44.0+1" [[deps.Parsers]] deps = ["Dates"] @@ -71,30 +215,104 @@ git-tree-sha1 = "0b5cfbb704034b5b4c1869e36634438a047df065" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" version = "2.2.1" +[[deps.Pkg]] +deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +version = "1.12.1" +weakdeps = ["REPL"] + + [deps.Pkg.extensions] + REPLExt = "REPL" + +[[deps.PrecompileTools]] +deps = ["Preferences"] +git-tree-sha1 = "07a921781cab75691315adc645096ed5e370cb77" +uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +version = "1.3.3" + +[[deps.Preferences]] +deps = ["TOML"] +git-tree-sha1 = "522f093a29b31a93e34eaea17ba055d850edea28" +uuid = "21216c6a-2e73-6563-6e65-726566657250" +version = "1.5.1" + [[deps.Printf]] deps = ["Unicode"] uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" +version = "1.11.0" [[deps.REPL]] -deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] +deps = ["InteractiveUtils", "JuliaSyntaxHighlighting", "Markdown", "Sockets", "StyledStrings", "Unicode"] uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +version = "1.11.0" [[deps.Random]] -deps = ["SHA", "Serialization"] +deps = ["SHA"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +version = "1.11.0" + +[[deps.RegistryInstances]] +deps = ["LazilyInitializedFields", "Pkg", "TOML", "Tar"] +git-tree-sha1 = "ffd19052caf598b8653b99404058fce14828be51" +uuid = "2792f1a3-b283-48e8-9a74-f99dce5104f3" +version = "0.1.0" [[deps.SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" +version = "0.7.0" [[deps.Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +version = "1.11.0" [[deps.Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" +version = "1.11.0" + +[[deps.StyledStrings]] +uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" +version = "1.11.0" + +[[deps.TOML]] +deps = ["Dates"] +uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +version = "1.0.3" + +[[deps.Tar]] +deps = ["ArgTools", "SHA"] +uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" +version = "1.10.0" [[deps.Test]] deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +version = "1.11.0" + +[[deps.TranscodingStreams]] +git-tree-sha1 = "0c45878dcfdcfa8480052b6ab162cdd138781742" +uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" +version = "0.11.3" + +[[deps.UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +version = "1.11.0" [[deps.Unicode]] uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +version = "1.11.0" + +[[deps.Zlib_jll]] +deps = ["Libdl"] +uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +version = "1.3.1+2" + +[[deps.nghttp2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" +version = "1.64.0+1" + +[[deps.p7zip_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" +version = "17.7.0+0" diff --git a/docs/Project.toml b/docs/Project.toml index f1a23e2..b17b46b 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,3 +1,7 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" FunctionWrappersWrappers = "77dc65aa-8811-40c2-897b-53d922fa7daf" + +[compat] +Documenter = "1.16.1" +FunctionWrappersWrappers = "0.1.3" From a6184c4cfb1462758cdc29f09232c9f2ed453d5b Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Tue, 30 Dec 2025 10:29:19 -0500 Subject: [PATCH 08/55] Delete docs/Manifest.toml --- docs/Manifest.toml | 318 --------------------------------------------- 1 file changed, 318 deletions(-) delete mode 100644 docs/Manifest.toml diff --git a/docs/Manifest.toml b/docs/Manifest.toml deleted file mode 100644 index 3decf20..0000000 --- a/docs/Manifest.toml +++ /dev/null @@ -1,318 +0,0 @@ -# This file is machine-generated - editing it directly is not advised - -julia_version = "1.12.3" -manifest_format = "2.0" -project_hash = "269c22af2ffbf5ea7f759b938882092049766bd2" - -[[deps.ANSIColoredPrinters]] -git-tree-sha1 = "574baf8110975760d391c710b6341da1afa48d8c" -uuid = "a4c015fc-c6ff-483c-b24f-f7ea428134e9" -version = "0.0.1" - -[[deps.AbstractTrees]] -git-tree-sha1 = "2d9c9a55f9c93e8887ad391fbae72f8ef55e1177" -uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" -version = "0.4.5" - -[[deps.ArgTools]] -uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" -version = "1.1.2" - -[[deps.Artifacts]] -uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" -version = "1.11.0" - -[[deps.Base64]] -uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" -version = "1.11.0" - -[[deps.CodecZlib]] -deps = ["TranscodingStreams", "Zlib_jll"] -git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9" -uuid = "944b1d66-785c-5afd-91f1-9de20f533193" -version = "0.7.8" - -[[deps.CompilerSupportLibraries_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "1.3.0+1" - -[[deps.Dates]] -deps = ["Printf"] -uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" -version = "1.11.0" - -[[deps.DocStringExtensions]] -deps = ["LibGit2"] -git-tree-sha1 = "b19534d1895d702889b219c382a6e18010797f0b" -uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" -version = "0.8.6" - -[[deps.Documenter]] -deps = ["ANSIColoredPrinters", "AbstractTrees", "Base64", "CodecZlib", "Dates", "DocStringExtensions", "Downloads", "Git", "IOCapture", "InteractiveUtils", "JSON", "Logging", "Markdown", "MarkdownAST", "Pkg", "PrecompileTools", "REPL", "RegistryInstances", "SHA", "TOML", "Test", "Unicode"] -git-tree-sha1 = "b37458ae37d8bdb643d763451585cd8d0e5b4a9e" -uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -version = "1.16.1" - -[[deps.Downloads]] -deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] -uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -version = "1.7.0" - -[[deps.Expat_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "27af30de8b5445644e8ffe3bcb0d72049c089cf1" -uuid = "2e619515-83b5-522b-bb60-26c02a35a201" -version = "2.7.3+0" - -[[deps.FileWatching]] -uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" -version = "1.11.0" - -[[deps.FunctionWrappers]] -git-tree-sha1 = "d62485945ce5ae9c0c48f124a84998d755bae00e" -uuid = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" -version = "1.1.3" - -[[deps.FunctionWrappersWrappers]] -deps = ["FunctionWrappers"] -git-tree-sha1 = "b104d487b34566608f8b4e1c39fb0b10aa279ff8" -uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" -version = "0.1.3" - -[[deps.Git]] -deps = ["Git_LFS_jll", "Git_jll", "JLLWrappers", "OpenSSH_jll"] -git-tree-sha1 = "824a1890086880696fc908fe12a17bcf61738bd8" -uuid = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2" -version = "1.5.0" - -[[deps.Git_LFS_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "bb8471f313ed941f299aa53d32a94ab3bee08844" -uuid = "020c3dae-16b3-5ae5-87b3-4cb189e250b2" -version = "3.7.0+0" - -[[deps.Git_jll]] -deps = ["Artifacts", "Expat_jll", "JLLWrappers", "LibCURL_jll", "Libdl", "Libiconv_jll", "OpenSSL_jll", "PCRE2_jll", "Zlib_jll"] -git-tree-sha1 = "46bd50c245fe49ed87377c014a928a549b9ef7ed" -uuid = "f8c6e375-362e-5223-8a59-34ff63f689eb" -version = "2.52.0+0" - -[[deps.IOCapture]] -deps = ["Logging", "Random"] -git-tree-sha1 = "f7be53659ab06ddc986428d3a9dcc95f6fa6705a" -uuid = "b5f81e59-6552-4d32-b1f0-c071b021bf89" -version = "0.2.2" - -[[deps.InteractiveUtils]] -deps = ["Markdown"] -uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -version = "1.11.0" - -[[deps.JLLWrappers]] -deps = ["Artifacts", "Preferences"] -git-tree-sha1 = "0533e564aae234aff59ab625543145446d8b6ec2" -uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" -version = "1.7.1" - -[[deps.JSON]] -deps = ["Dates", "Mmap", "Parsers", "Unicode"] -git-tree-sha1 = "8076680b162ada2a031f707ac7b4953e30667a37" -uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -version = "0.21.2" - -[[deps.JuliaSyntaxHighlighting]] -deps = ["StyledStrings"] -uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" -version = "1.12.0" - -[[deps.LazilyInitializedFields]] -git-tree-sha1 = "0f2da712350b020bc3957f269c9caad516383ee0" -uuid = "0e77f7df-68c5-4e49-93ce-4cd80f5598bf" -version = "1.3.0" - -[[deps.LibCURL]] -deps = ["LibCURL_jll", "MozillaCACerts_jll"] -uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" -version = "0.6.4" - -[[deps.LibCURL_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] -uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" -version = "8.15.0+0" - -[[deps.LibGit2]] -deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] -uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" -version = "1.11.0" - -[[deps.LibGit2_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"] -uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" -version = "1.9.0+0" - -[[deps.LibSSH2_jll]] -deps = ["Artifacts", "Libdl", "OpenSSL_jll"] -uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" -version = "1.11.3+1" - -[[deps.Libdl]] -uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" -version = "1.11.0" - -[[deps.Libiconv_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "be484f5c92fad0bd8acfef35fe017900b0b73809" -uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531" -version = "1.18.0+0" - -[[deps.Logging]] -uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" -version = "1.11.0" - -[[deps.Markdown]] -deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] -uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" -version = "1.11.0" - -[[deps.MarkdownAST]] -deps = ["AbstractTrees", "Markdown"] -git-tree-sha1 = "465a70f0fc7d443a00dcdc3267a497397b8a3899" -uuid = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" -version = "0.1.2" - -[[deps.Mmap]] -uuid = "a63ad114-7e13-5084-954f-fe012c677804" -version = "1.11.0" - -[[deps.MozillaCACerts_jll]] -uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2025.5.20" - -[[deps.NetworkOptions]] -uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" -version = "1.3.0" - -[[deps.OpenSSH_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "OpenSSL_jll", "Zlib_jll"] -git-tree-sha1 = "301412a644646fdc0ad67d0a87487466b491e53d" -uuid = "9bd350c2-7e96-507f-8002-3f2e150b4e1b" -version = "10.2.1+0" - -[[deps.OpenSSL_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.5.4+0" - -[[deps.PCRE2_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "efcefdf7-47ab-520b-bdef-62a2eaa19f15" -version = "10.44.0+1" - -[[deps.Parsers]] -deps = ["Dates"] -git-tree-sha1 = "0b5cfbb704034b5b4c1869e36634438a047df065" -uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.2.1" - -[[deps.Pkg]] -deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] -uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.12.1" -weakdeps = ["REPL"] - - [deps.Pkg.extensions] - REPLExt = "REPL" - -[[deps.PrecompileTools]] -deps = ["Preferences"] -git-tree-sha1 = "07a921781cab75691315adc645096ed5e370cb77" -uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -version = "1.3.3" - -[[deps.Preferences]] -deps = ["TOML"] -git-tree-sha1 = "522f093a29b31a93e34eaea17ba055d850edea28" -uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.5.1" - -[[deps.Printf]] -deps = ["Unicode"] -uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" -version = "1.11.0" - -[[deps.REPL]] -deps = ["InteractiveUtils", "JuliaSyntaxHighlighting", "Markdown", "Sockets", "StyledStrings", "Unicode"] -uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" -version = "1.11.0" - -[[deps.Random]] -deps = ["SHA"] -uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -version = "1.11.0" - -[[deps.RegistryInstances]] -deps = ["LazilyInitializedFields", "Pkg", "TOML", "Tar"] -git-tree-sha1 = "ffd19052caf598b8653b99404058fce14828be51" -uuid = "2792f1a3-b283-48e8-9a74-f99dce5104f3" -version = "0.1.0" - -[[deps.SHA]] -uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" -version = "0.7.0" - -[[deps.Serialization]] -uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" -version = "1.11.0" - -[[deps.Sockets]] -uuid = "6462fe0b-24de-5631-8697-dd941f90decc" -version = "1.11.0" - -[[deps.StyledStrings]] -uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" -version = "1.11.0" - -[[deps.TOML]] -deps = ["Dates"] -uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" -version = "1.0.3" - -[[deps.Tar]] -deps = ["ArgTools", "SHA"] -uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" -version = "1.10.0" - -[[deps.Test]] -deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] -uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -version = "1.11.0" - -[[deps.TranscodingStreams]] -git-tree-sha1 = "0c45878dcfdcfa8480052b6ab162cdd138781742" -uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" -version = "0.11.3" - -[[deps.UUIDs]] -deps = ["Random", "SHA"] -uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" -version = "1.11.0" - -[[deps.Unicode]] -uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" -version = "1.11.0" - -[[deps.Zlib_jll]] -deps = ["Libdl"] -uuid = "83775a58-1f1d-513f-b197-d71354ab007a" -version = "1.3.1+2" - -[[deps.nghttp2_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" -version = "1.64.0+1" - -[[deps.p7zip_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] -uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" -version = "17.7.0+0" From 2d6d1dc084df64e7a49737b38b3836616e43fe37 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sun, 4 Jan 2026 07:22:44 -0500 Subject: [PATCH 09/55] Switch from JuliaFormatter to Runic.jl for code formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update CI workflow to use fredrikekre/runic-action@v1 - Remove .JuliaFormatter.toml configuration - Format all source files with Runic.jl 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .JuliaFormatter.toml | 3 --- .github/workflows/FormatCheck.yml | 14 ++++++++++---- docs/make.jl | 6 ++++-- src/FunctionWrappersWrappers.jl | 31 ++++++++++++++++++------------- test/runtests.jl | 24 +++++++++++++++++------- 5 files changed, 49 insertions(+), 29 deletions(-) delete mode 100644 .JuliaFormatter.toml diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml deleted file mode 100644 index 3494a9f..0000000 --- a/.JuliaFormatter.toml +++ /dev/null @@ -1,3 +0,0 @@ -style = "sciml" -format_markdown = true -format_docstrings = true diff --git a/.github/workflows/FormatCheck.yml b/.github/workflows/FormatCheck.yml index fe9c128..6762c6f 100644 --- a/.github/workflows/FormatCheck.yml +++ b/.github/workflows/FormatCheck.yml @@ -1,13 +1,19 @@ -name: "Format Check" +name: format-check on: push: branches: + - 'master' - 'main' + - 'release-' tags: '*' pull_request: jobs: - format-check: - name: "Format Check" - uses: "SciML/.github/.github/workflows/format-suggestions-on-pr.yml@v1" + runic: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: fredrikekre/runic-action@v1 + with: + version: '1' diff --git a/docs/make.jl b/docs/make.jl index bcbf26c..cc6bdcf 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,8 +1,10 @@ using FunctionWrappersWrappers using Documenter -DocMeta.setdocmeta!(FunctionWrappersWrappers, :DocTestSetup, - :(using FunctionWrappersWrappers); recursive = true) +DocMeta.setdocmeta!( + FunctionWrappersWrappers, :DocTestSetup, + :(using FunctionWrappersWrappers); recursive = true +) makedocs(; modules = [FunctionWrappersWrappers], diff --git a/src/FunctionWrappersWrappers.jl b/src/FunctionWrappersWrappers.jl index 39ba2c4..76849f4 100644 --- a/src/FunctionWrappersWrappers.jl +++ b/src/FunctionWrappersWrappers.jl @@ -12,16 +12,20 @@ end TruncatedStacktraces.@truncate_stacktrace FunctionWrappersWrapper function (fww::FunctionWrappersWrapper{FW, FB})(args::Vararg{Any, K}) where {FW, K, FB} - _call(fww.fw, args, fww) + return _call(fww.fw, args, fww) end -function _call(fw::Tuple{FunctionWrappers.FunctionWrapper{R, A}, Vararg}, - arg::A, fww::FunctionWrappersWrapper) where {R, A} - first(fw)(arg...) +function _call( + fw::Tuple{FunctionWrappers.FunctionWrapper{R, A}, Vararg}, + arg::A, fww::FunctionWrappersWrapper + ) where {R, A} + return first(fw)(arg...) end -function _call(fw::Tuple{FunctionWrappers.FunctionWrapper{R, A1}, Vararg}, - arg::A2, fww::FunctionWrappersWrapper) where {R, A1, A2} - _call(Base.tail(fw), arg, fww) +function _call( + fw::Tuple{FunctionWrappers.FunctionWrapper{R, A1}, Vararg}, + arg::A2, fww::FunctionWrappersWrapper + ) where {R, A1, A2} + return _call(Base.tail(fw), arg, fww) end const NO_FUNCTIONWRAPPER_FOUND_MESSAGE = "No matching function wrapper was found!" @@ -29,23 +33,24 @@ const NO_FUNCTIONWRAPPER_FOUND_MESSAGE = "No matching function wrapper was found struct NoFunctionWrapperFoundError <: Exception end function Base.showerror(io::IO, e::NoFunctionWrapperFoundError) - print(io, NO_FUNCTIONWRAPPER_FOUND_MESSAGE) + return print(io, NO_FUNCTIONWRAPPER_FOUND_MESSAGE) end function _call(::Tuple{}, arg, fww::FunctionWrappersWrapper{<:Any, false}) throw(NoFunctionWrapperFoundError()) end function _call(::Tuple{}, arg, fww::FunctionWrappersWrapper{<:Any, true}) - first(fww.fw).obj[](arg...) + return first(fww.fw).obj[](arg...) end function FunctionWrappersWrapper( f::F, argtypes::Tuple{Vararg{Any, K}}, rettypes::Tuple{Vararg{DataType, K}}, - fallback::Val{FB} = Val{false}()) where {F, K, FB} + fallback::Val{FB} = Val{false}() + ) where {F, K, FB} fwt = map(argtypes, rettypes) do A, R FunctionWrappers.FunctionWrapper{R, A}(f) end - FunctionWrappersWrapper{typeof(fwt), FB}(fwt) + return FunctionWrappersWrapper{typeof(fwt), FB}(fwt) end """ @@ -95,7 +100,7 @@ wrapped_signatures(fww) # Returns (Tuple{Float64, Float64}, Tuple{Int, Int}) See also: [`unwrap`](@ref), [`wrapped_return_types`](@ref) """ function wrapped_signatures(fww::FunctionWrappersWrapper) - map(fw -> typeof(fw).parameters[2], fww.fw) + return map(fw -> typeof(fw).parameters[2], fww.fw) end """ @@ -115,7 +120,7 @@ wrapped_return_types(fww) # Returns (Float64, Int64) See also: [`unwrap`](@ref), [`wrapped_signatures`](@ref) """ function wrapped_return_types(fww::FunctionWrappersWrapper) - map(fw -> typeof(fw).parameters[1], fww.fw) + return map(fw -> typeof(fw).parameters[1], fww.fw) end using PrecompileTools diff --git a/test/runtests.jl b/test/runtests.jl index 3a68712..20dc19d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,13 +2,17 @@ using FunctionWrappersWrappers using Test @testset "FunctionWrappersWrappers.jl" begin - fwplus = FunctionWrappersWrapper(+, (Tuple{Float64, Float64}, Tuple{Int, Int}), ( - Float64, Int)); + fwplus = FunctionWrappersWrapper( + +, (Tuple{Float64, Float64}, Tuple{Int, Int}), ( + Float64, Int, + ) + ) @test fwplus(4.0, 8.0) === 12.0 @test fwplus(4, 8) === 12 fwexp2 = FunctionWrappersWrapper( - exp2, (Tuple{Float64}, Tuple{Float32}, Tuple{Int}), (Float64, Float32, Float64)); + exp2, (Tuple{Float64}, Tuple{Float32}, Tuple{Int}), (Float64, Float32, Float64) + ) @test fwexp2(4.0) === 16.0 @test fwexp2(4.0f0) === 16.0f0 @test fwexp2(4) === 16.0 @@ -35,8 +39,11 @@ end end # Test with multiple signatures - fwplus = FunctionWrappersWrapper(+, (Tuple{Float64, Float64}, Tuple{Int, Int}), ( - Float64, Int)) + fwplus = FunctionWrappersWrapper( + +, (Tuple{Float64, Float64}, Tuple{Int, Int}), ( + Float64, Int, + ) + ) @testset "unwrap with multiple signatures" begin f = unwrap(fwplus) @@ -56,8 +63,11 @@ end # Test with a custom function my_func(x) = x^2 - fwcustom = FunctionWrappersWrapper(my_func, (Tuple{Float64}, Tuple{Int}), ( - Float64, Int)) + fwcustom = FunctionWrappersWrapper( + my_func, (Tuple{Float64}, Tuple{Int}), ( + Float64, Int, + ) + ) @testset "unwrap with custom function" begin f = unwrap(fwcustom) From c9d1bb353cd965d054b370918e01630112eb7436 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Fri, 9 Jan 2026 06:03:46 -0500 Subject: [PATCH 10/55] Finalize nopre test group structure and rebase on main --- .github/workflows/Tests.yml | 7 + Project.toml | 3 +- test/basictests.jl | 96 +++++++++++++ test/nopre/Manifest.toml | 261 ++++++++++++++++++++++++++++++++++++ test/nopre/Project.toml | 4 + test/nopre/jet_tests.jl | 30 +++++ test/runtests.jl | 84 ++---------- 7 files changed, 411 insertions(+), 74 deletions(-) create mode 100644 test/basictests.jl create mode 100644 test/nopre/Manifest.toml create mode 100644 test/nopre/Project.toml create mode 100644 test/nopre/jet_tests.jl diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 121b594..b7473b9 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -23,14 +23,21 @@ jobs: strategy: fail-fast: false matrix: + group: + - Core + - nopre version: - '1' - 'lts' - 'nightly' arch: - x64 + exclude: + - version: 'nightly' + group: 'nopre' uses: "SciML/.github/.github/workflows/tests.yml@v1" with: + group: "${{ matrix.group }}" julia-version: "${{ matrix.version }}" julia-arch: "${{ matrix.arch }}" secrets: "inherit" diff --git a/Project.toml b/Project.toml index 4a74e50..f551b93 100644 --- a/Project.toml +++ b/Project.toml @@ -15,7 +15,8 @@ TruncatedStacktraces = "1" julia = "1.6" [extras] +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test"] +test = ["Pkg", "Test"] diff --git a/test/basictests.jl b/test/basictests.jl new file mode 100644 index 0000000..b8ecfaa --- /dev/null +++ b/test/basictests.jl @@ -0,0 +1,96 @@ +using FunctionWrappersWrappers +using Test + +@testset "FunctionWrappersWrappers.jl" begin + fwplus = FunctionWrappersWrapper( + +, (Tuple{Float64, Float64}, Tuple{Int, Int}), ( + Float64, Int, + ) + ) + @test fwplus(4.0, 8.0) === 12.0 + @test fwplus(4, 8) === 12 + + fwexp2 = FunctionWrappersWrapper( + exp2, (Tuple{Float64}, Tuple{Float32}, Tuple{Int}), (Float64, Float32, Float64) + ) + @test fwexp2(4.0) === 16.0 + @test fwexp2(4.0f0) === 16.0f0 + @test fwexp2(4) === 16.0 +end + +@testset "Type inference" begin + # Test return type inference + fwplus = FunctionWrappersWrapper( + +, (Tuple{Float64, Float64}, Tuple{Int, Int}), ( + Float64, Int, + ) + ) + @test @inferred(fwplus(4.0, 8.0)) === 12.0 + @test @inferred(fwplus(4, 8)) === 12 + + fwexp2 = FunctionWrappersWrapper( + exp2, (Tuple{Float64}, Tuple{Float32}, Tuple{Int}), (Float64, Float32, Float64) + ) + @test @inferred(fwexp2(4.0)) === 16.0 + @test @inferred(fwexp2(4.0f0)) === 16.0f0 + @test @inferred(fwexp2(4)) === 16.0 +end + +@testset "Introspection functions" begin + # Test with a simple function + fwsin = FunctionWrappersWrapper(sin, (Tuple{Float64},), (Float64,)) + + @testset "unwrap" begin + f = unwrap(fwsin) + @test f === sin + @test f(0.5) == sin(0.5) + end + + @testset "wrapped_signatures" begin + sigs = wrapped_signatures(fwsin) + @test sigs == (Tuple{Float64},) + end + + @testset "wrapped_return_types" begin + rets = wrapped_return_types(fwsin) + @test rets == (Float64,) + end + + # Test with multiple signatures + fwplus = FunctionWrappersWrapper( + +, (Tuple{Float64, Float64}, Tuple{Int, Int}), ( + Float64, Int, + ) + ) + + @testset "unwrap with multiple signatures" begin + f = unwrap(fwplus) + @test f === + + @test f(1, 2) == 3 + end + + @testset "wrapped_signatures with multiple signatures" begin + sigs = wrapped_signatures(fwplus) + @test sigs == (Tuple{Float64, Float64}, Tuple{Int, Int}) + end + + @testset "wrapped_return_types with multiple signatures" begin + rets = wrapped_return_types(fwplus) + @test rets == (Float64, Int) + end + + # Test with a custom function + my_func(x) = x^2 + fwcustom = FunctionWrappersWrapper( + my_func, (Tuple{Float64}, Tuple{Int}), ( + Float64, Int, + ) + ) + + @testset "unwrap with custom function" begin + f = unwrap(fwcustom) + @test f === my_func + @test f(3) == 9 + @test f(2.5) == 6.25 + end +end diff --git a/test/nopre/Manifest.toml b/test/nopre/Manifest.toml new file mode 100644 index 0000000..b801b1b --- /dev/null +++ b/test/nopre/Manifest.toml @@ -0,0 +1,261 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.12.4" +manifest_format = "2.0" +project_hash = "fc7db7725f4c12cf05bc39dfb6855e7476e54e38" + +[[deps.ArgTools]] +uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" +version = "1.1.2" + +[[deps.Artifacts]] +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +version = "1.11.0" + +[[deps.Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +version = "1.11.0" + +[[deps.CodeTracking]] +deps = ["InteractiveUtils", "UUIDs"] +git-tree-sha1 = "b7231a755812695b8046e8471ddc34c8268cbad5" +uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" +version = "3.0.0" + +[[deps.Compiler]] +git-tree-sha1 = "382d79bfe72a406294faca39ef0c3cef6e6ce1f1" +uuid = "807dbc54-b67e-4c79-8afb-eafe4df6f2e1" +version = "0.1.1" + +[[deps.CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "1.3.0+1" + +[[deps.Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" +version = "1.11.0" + +[[deps.Downloads]] +deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] +uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +version = "1.7.0" + +[[deps.FileWatching]] +uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" +version = "1.11.0" + +[[deps.FunctionWrappers]] +git-tree-sha1 = "d62485945ce5ae9c0c48f124a84998d755bae00e" +uuid = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" +version = "1.1.3" + +[[deps.FunctionWrappersWrappers]] +deps = ["FunctionWrappers", "PrecompileTools", "TruncatedStacktraces"] +path = "/home/crackauc/sandbox/tmp_20260108_090820_85201" +uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" +version = "0.1.4" + +[[deps.InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +version = "1.11.0" + +[[deps.JET]] +deps = ["CodeTracking", "Compiler", "InteractiveUtils", "JuliaInterpreter", "JuliaSyntax", "LoweredCodeUtils", "MacroTools", "Pkg", "PrecompileTools", "Preferences", "Revise", "Test"] +git-tree-sha1 = "3642228b0d4ab0b263a5946f60ace6c2b5616256" +uuid = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +version = "0.11.3" + +[[deps.JuliaInterpreter]] +deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"] +git-tree-sha1 = "80580012d4ed5a3e8b18c7cd86cebe4b816d17a6" +uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" +version = "0.10.9" + +[[deps.JuliaSyntax]] +git-tree-sha1 = "0d4b3dab95018bcf3925204475693d9f09dc45b8" +uuid = "70703baa-626e-46a2-a12c-08ffd08c73b4" +version = "1.0.2" + +[[deps.JuliaSyntaxHighlighting]] +deps = ["StyledStrings"] +uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" +version = "1.12.0" + +[[deps.LibCURL]] +deps = ["LibCURL_jll", "MozillaCACerts_jll"] +uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" +version = "0.6.4" + +[[deps.LibCURL_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] +uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" +version = "8.15.0+0" + +[[deps.LibGit2]] +deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" +version = "1.11.0" + +[[deps.LibGit2_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"] +uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" +version = "1.9.0+0" + +[[deps.LibSSH2_jll]] +deps = ["Artifacts", "Libdl", "OpenSSL_jll"] +uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" +version = "1.11.3+1" + +[[deps.Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +version = "1.11.0" + +[[deps.Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" +version = "1.11.0" + +[[deps.LoweredCodeUtils]] +deps = ["CodeTracking", "Compiler", "JuliaInterpreter"] +git-tree-sha1 = "65ae3db6ab0e5b1b5f217043c558d9d1d33cc88d" +uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b" +version = "3.5.0" + +[[deps.MacroTools]] +git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522" +uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +version = "0.5.16" + +[[deps.Markdown]] +deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" +version = "1.11.0" + +[[deps.MozillaCACerts_jll]] +uuid = "14a3606d-f60d-562e-9121-12d972cd8159" +version = "2025.11.4" + +[[deps.NetworkOptions]] +uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" +version = "1.3.0" + +[[deps.OpenSSL_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" +version = "3.5.4+0" + +[[deps.OrderedCollections]] +git-tree-sha1 = "05868e21324cede2207c6f0f466b4bfef6d5e7ee" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.8.1" + +[[deps.Pkg]] +deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +version = "1.12.1" +weakdeps = ["REPL"] + + [deps.Pkg.extensions] + REPLExt = "REPL" + +[[deps.PrecompileTools]] +deps = ["Preferences"] +git-tree-sha1 = "07a921781cab75691315adc645096ed5e370cb77" +uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +version = "1.3.3" + +[[deps.Preferences]] +deps = ["TOML"] +git-tree-sha1 = "522f093a29b31a93e34eaea17ba055d850edea28" +uuid = "21216c6a-2e73-6563-6e65-726566657250" +version = "1.5.1" + +[[deps.Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" +version = "1.11.0" + +[[deps.REPL]] +deps = ["InteractiveUtils", "JuliaSyntaxHighlighting", "Markdown", "Sockets", "StyledStrings", "Unicode"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +version = "1.11.0" + +[[deps.Random]] +deps = ["SHA"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +version = "1.11.0" + +[[deps.Revise]] +deps = ["CodeTracking", "FileWatching", "InteractiveUtils", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"] +git-tree-sha1 = "dfd6fab9aa325b382773b8930998ce84c2918e5e" +uuid = "295af30f-e4ad-537b-8983-00126c2a3abe" +version = "3.13.1" + + [deps.Revise.extensions] + DistributedExt = "Distributed" + + [deps.Revise.weakdeps] + Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" + +[[deps.SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" +version = "0.7.0" + +[[deps.Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +version = "1.11.0" + +[[deps.Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" +version = "1.11.0" + +[[deps.StyledStrings]] +uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" +version = "1.11.0" + +[[deps.TOML]] +deps = ["Dates"] +uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +version = "1.0.3" + +[[deps.Tar]] +deps = ["ArgTools", "SHA"] +uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" +version = "1.10.0" + +[[deps.Test]] +deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +version = "1.11.0" + +[[deps.TruncatedStacktraces]] +deps = ["InteractiveUtils", "MacroTools", "Preferences"] +git-tree-sha1 = "ea3e54c2bdde39062abf5a9758a23735558705e1" +uuid = "781d530d-4396-4725-bb49-402e4bee1e77" +version = "1.4.0" + +[[deps.UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +version = "1.11.0" + +[[deps.Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +version = "1.11.0" + +[[deps.Zlib_jll]] +deps = ["Libdl"] +uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +version = "1.3.1+2" + +[[deps.nghttp2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" +version = "1.64.0+1" + +[[deps.p7zip_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" +version = "17.7.0+0" diff --git a/test/nopre/Project.toml b/test/nopre/Project.toml new file mode 100644 index 0000000..0b4fbf6 --- /dev/null +++ b/test/nopre/Project.toml @@ -0,0 +1,4 @@ +[deps] +FunctionWrappersWrappers = "77dc65aa-8811-40c2-897b-53d922fa7daf" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/nopre/jet_tests.jl b/test/nopre/jet_tests.jl new file mode 100644 index 0000000..42a3ee7 --- /dev/null +++ b/test/nopre/jet_tests.jl @@ -0,0 +1,30 @@ +using FunctionWrappersWrappers +using JET: JET, @test_opt +using Test + +@testset "JET static analysis" begin + # Test that the main call path is type-stable (no fallback) + fwplus = FunctionWrappersWrapper( + +, (Tuple{Float64, Float64}, Tuple{Int, Int}), ( + Float64, Int, + ) + ) + + # Core functionality should be type-stable + @test_opt target_modules = (FunctionWrappersWrappers,) fwplus(4.0, 8.0) + @test_opt target_modules = (FunctionWrappersWrappers,) fwplus(4, 8) + + # Test single-argument wrapper + fwexp2 = FunctionWrappersWrapper( + exp2, (Tuple{Float64}, Tuple{Float32}, Tuple{Int}), (Float64, Float32, Float64) + ) + @test_opt target_modules = (FunctionWrappersWrappers,) fwexp2(4.0) + @test_opt target_modules = (FunctionWrappersWrappers,) fwexp2(4.0f0) + @test_opt target_modules = (FunctionWrappersWrappers,) fwexp2(4) + + # Verify no errors detected by JET.report_call for core paths + rep = JET.report_call(fwplus, (Float64, Float64)) + @test length(JET.get_reports(rep)) == 0 + rep = JET.report_call(fwplus, (Int, Int)) + @test length(JET.get_reports(rep)) == 0 +end diff --git a/test/runtests.jl b/test/runtests.jl index 20dc19d..3a62b62 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,78 +1,16 @@ -using FunctionWrappersWrappers -using Test +using Test, Pkg -@testset "FunctionWrappersWrappers.jl" begin - fwplus = FunctionWrappersWrapper( - +, (Tuple{Float64, Float64}, Tuple{Int, Int}), ( - Float64, Int, - ) - ) - @test fwplus(4.0, 8.0) === 12.0 - @test fwplus(4, 8) === 12 +const GROUP = get(ENV, "GROUP", "All") - fwexp2 = FunctionWrappersWrapper( - exp2, (Tuple{Float64}, Tuple{Float32}, Tuple{Int}), (Float64, Float32, Float64) - ) - @test fwexp2(4.0) === 16.0 - @test fwexp2(4.0f0) === 16.0f0 - @test fwexp2(4) === 16.0 -end - -@testset "Introspection functions" begin - # Test with a simple function - fwsin = FunctionWrappersWrapper(sin, (Tuple{Float64},), (Float64,)) - - @testset "unwrap" begin - f = unwrap(fwsin) - @test f === sin - @test f(0.5) == sin(0.5) - end - - @testset "wrapped_signatures" begin - sigs = wrapped_signatures(fwsin) - @test sigs == (Tuple{Float64},) +if GROUP == "All" || GROUP == "Core" + @testset "FunctionWrappersWrappers.jl" begin + include("basictests.jl") end +end - @testset "wrapped_return_types" begin - rets = wrapped_return_types(fwsin) - @test rets == (Float64,) - end - - # Test with multiple signatures - fwplus = FunctionWrappersWrapper( - +, (Tuple{Float64, Float64}, Tuple{Int, Int}), ( - Float64, Int, - ) - ) - - @testset "unwrap with multiple signatures" begin - f = unwrap(fwplus) - @test f === + - @test f(1, 2) == 3 - end - - @testset "wrapped_signatures with multiple signatures" begin - sigs = wrapped_signatures(fwplus) - @test sigs == (Tuple{Float64, Float64}, Tuple{Int, Int}) - end - - @testset "wrapped_return_types with multiple signatures" begin - rets = wrapped_return_types(fwplus) - @test rets == (Float64, Int) - end - - # Test with a custom function - my_func(x) = x^2 - fwcustom = FunctionWrappersWrapper( - my_func, (Tuple{Float64}, Tuple{Int}), ( - Float64, Int, - ) - ) - - @testset "unwrap with custom function" begin - f = unwrap(fwcustom) - @test f === my_func - @test f(3) == 9 - @test f(2.5) == 6.25 - end +if GROUP == "nopre" + Pkg.activate("nopre") + Pkg.develop(PackageSpec(path = dirname(@__DIR__))) + Pkg.instantiate() + include("nopre/jet_tests.jl") end From ea94014b10c1871a92d67e263e30282b18fe2fd1 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 7 Jan 2026 15:11:36 -0500 Subject: [PATCH 11/55] Allow UnionAll types in rettypes for improved interface compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed `rettypes` parameter type constraint from `Tuple{Vararg{DataType, K}}` to `Tuple{Vararg{Type, K}}` to allow UnionAll types like `AbstractArray{Float64}` or `JLArray{Float64}` as return types. This is backwards compatible since `DataType <: Type`, so all existing code continues to work. The underlying FunctionWrappers.jl already supports these types. Added tests for: - BigFloat support (verifying arbitrary precision works) - UnionAll return types (testing AbstractArray{Float64}) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/FunctionWrappersWrappers.jl | 2 +- test/runtests.jl | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/FunctionWrappersWrappers.jl b/src/FunctionWrappersWrappers.jl index 76849f4..5038735 100644 --- a/src/FunctionWrappersWrappers.jl +++ b/src/FunctionWrappersWrappers.jl @@ -44,7 +44,7 @@ function _call(::Tuple{}, arg, fww::FunctionWrappersWrapper{<:Any, true}) end function FunctionWrappersWrapper( - f::F, argtypes::Tuple{Vararg{Any, K}}, rettypes::Tuple{Vararg{DataType, K}}, + f::F, argtypes::Tuple{Vararg{Any, K}}, rettypes::Tuple{Vararg{Type, K}}, fallback::Val{FB} = Val{false}() ) where {F, K, FB} fwt = map(argtypes, rettypes) do A, R diff --git a/test/runtests.jl b/test/runtests.jl index 3a62b62..7a015ef 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -14,3 +14,42 @@ if GROUP == "nopre" Pkg.instantiate() include("nopre/jet_tests.jl") end + +@testset "BigFloat support" begin + fwplus_big = FunctionWrappersWrapper( + +, + (Tuple{BigFloat, BigFloat}, Tuple{Float64, Float64}), + (BigFloat, Float64) + ) + a = BigFloat("3.14159265358979323846264338327950288") + b = BigFloat("2.71828182845904523536028747135266250") + @test fwplus_big(a, b) isa BigFloat + @test fwplus_big(a, b) == a + b + @test fwplus_big(1.0, 2.0) === 3.0 + + fwsin_big = FunctionWrappersWrapper( + sin, + (Tuple{BigFloat}, Tuple{Float64}), + (BigFloat, Float64) + ) + @test fwsin_big(BigFloat("1.0")) isa BigFloat + @test fwsin_big(1.0) === sin(1.0) +end + +@testset "UnionAll return types" begin + # Test that UnionAll types (like AbstractArray{Float64}) work as return types + function double_array(x::AbstractArray{Float64}) + return x .* 2 + end + + fwdouble = FunctionWrappersWrapper( + double_array, + (Tuple{AbstractArray{Float64}},), + (AbstractArray{Float64},) + ) + + v = [1.0, 2.0, 3.0] + result = fwdouble(v) + @test result isa Vector{Float64} + @test result == [2.0, 4.0, 6.0] +end From 85e142e11a2127fc580ea5ac4a995c9898fb7c71 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Fri, 9 Jan 2026 06:44:00 -0500 Subject: [PATCH 12/55] Update Tests.yml --- .github/workflows/Tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index b7473b9..66b1be6 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -29,7 +29,7 @@ jobs: version: - '1' - 'lts' - - 'nightly' + - 'pre' arch: - x64 exclude: From 1f304053c858f30ca7847e303f84fc0ae66bb5fb Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Fri, 9 Jan 2026 06:59:41 -0500 Subject: [PATCH 13/55] Update Tests.yml --- .github/workflows/Tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 66b1be6..0c81c85 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -33,7 +33,7 @@ jobs: arch: - x64 exclude: - - version: 'nightly' + - version: 'pre' group: 'nopre' uses: "SciML/.github/.github/workflows/tests.yml@v1" with: From a3060c0d971df4fa1a76b00a30f81d9f6775bf98 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Fri, 9 Jan 2026 07:00:37 -0500 Subject: [PATCH 14/55] Update Project.toml --- Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index f551b93..75a3004 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FunctionWrappersWrappers" uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" authors = ["Chris Elrod and contributors"] -version = "0.1.4" +version = "0.1.5" [deps] FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" @@ -12,7 +12,7 @@ TruncatedStacktraces = "781d530d-4396-4725-bb49-402e4bee1e77" FunctionWrappers = "1" PrecompileTools = "1" TruncatedStacktraces = "1" -julia = "1.6" +julia = "1.10" [extras] Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" From b94bd2bf0cc3f564b2827b63b985afb03f411475 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:59:56 +0000 Subject: [PATCH 15/55] Bump actions/checkout from 4 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/FormatCheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/FormatCheck.yml b/.github/workflows/FormatCheck.yml index 6762c6f..d22e82d 100644 --- a/.github/workflows/FormatCheck.yml +++ b/.github/workflows/FormatCheck.yml @@ -13,7 +13,7 @@ jobs: runic: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: fredrikekre/runic-action@v1 with: version: '1' From 3366d77560f4803b98bfbf4257060f1b04f344f4 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Mon, 30 Mar 2026 05:33:10 -0400 Subject: [PATCH 16/55] Add configurable cache modes and fallback policies for non-isbits types Replaces the boolean `FB` type parameter with two orthogonal configuration axes: Cache modes (how fallback FunctionWrappers are cached): - NoCache: dynamic dispatch every call (1 alloc, legacy behavior) - SingleCache: cache one FunctionWrapper for last arg types (0 alloc on hit) - DictCache: cache per arg type in a Dict (0 alloc, multi-type) Fallback policies (when fallback is allowed): - Strict: always error on mismatch (legacy Val{false}) - AllowAll: always fall back (legacy Val{true}) - AllowNonIsBits: fall back only for non-isbits types, error for isbits mismatches Default is SingleCache + AllowNonIsBits, which provides zero-allocation fallback for BigFloat, SparseConnectivityTracer, and similar non-isbits types while still catching isbits type mismatches as bugs. The key mechanism is lazily creating and caching a FunctionWrapper for the actual argument types on first fallback hit. Subsequent calls use the cached wrapper's ccall path with no extra allocations. Legacy API (Val{true}/Val{false} and FunctionWrappersWrapper{FW,Bool}) is preserved for backward compatibility, mapping to AllowAll/Strict + NoCache. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.6 (1M context) --- Project.toml | 2 +- src/FunctionWrappersWrappers.jl | 304 +++++++++++++++++++++++++------- test/basictests.jl | 190 +++++++++++++++++++- 3 files changed, 430 insertions(+), 66 deletions(-) diff --git a/Project.toml b/Project.toml index 75a3004..32791ef 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FunctionWrappersWrappers" uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" authors = ["Chris Elrod and contributors"] -version = "0.1.5" +version = "0.2.0" [deps] FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" diff --git a/src/FunctionWrappersWrappers.jl b/src/FunctionWrappersWrappers.jl index 5038735..d5e15c0 100644 --- a/src/FunctionWrappersWrappers.jl +++ b/src/FunctionWrappersWrappers.jl @@ -4,17 +4,180 @@ using FunctionWrappers import TruncatedStacktraces export FunctionWrappersWrapper, unwrap, wrapped_signatures, wrapped_return_types +export NoCache, SingleCache, DictCache +export Strict, AllowAll, AllowNonIsBits -struct FunctionWrappersWrapper{FW, FB} +# ============================================================================ +# Cache modes: control how fallback FunctionWrappers are cached +# ============================================================================ +abstract type AbstractCacheMode end + +""" + NoCache() + +No caching — every fallback call goes through dynamic dispatch (`obj[](arg...)`), +incurring 1 allocation per call. This is the legacy behavior when `fallback=Val{true}()`. +""" +struct NoCache <: AbstractCacheMode end + +""" + SingleCache() + +Cache a single `FunctionWrapper` for the last-seen argument types. After the first +fallback call, subsequent calls with the same types are zero-allocation. If called with +different types, the cache is replaced (1 alloc on miss). This is the recommended default. +""" +struct SingleCache <: AbstractCacheMode end + +""" + DictCache() + +Cache `FunctionWrapper`s in a `Dict` keyed by argument type. Handles multiple +non-isbits types without thrashing. Slightly higher lookup overhead than `SingleCache`. +""" +struct DictCache <: AbstractCacheMode end + +# ============================================================================ +# Fallback policies: control when fallback is allowed +# ============================================================================ +abstract type AbstractFallbackPolicy end + +""" + Strict() + +Never fall back — throw `NoFunctionWrapperFoundError` if no wrapper matches. +This is the legacy behavior when `fallback=Val{false}()`. +""" +struct Strict <: AbstractFallbackPolicy end + +""" + AllowAll() + +Always fall back to the original function when no wrapper matches. +This is the legacy behavior when `fallback=Val{true}()`. +""" +struct AllowAll <: AbstractFallbackPolicy end + +""" + AllowNonIsBits() + +Fall back only when argument types contain non-isbits elements (e.g., `BigFloat`, +`SparseConnectivityTracer` types). Throws `NoFunctionWrapperFoundError` for isbits +type mismatches (e.g., `Float32` when `Float64` was expected), which catches bugs. +This is the recommended default. +""" +struct AllowNonIsBits <: AbstractFallbackPolicy end + +# ============================================================================ +# Cache storage types +# ============================================================================ +struct NoCacheStorage end +mutable struct SingleCacheStorage + cached::Any # Union{Nothing, FunctionWrapper} + SingleCacheStorage() = new(nothing) +end +struct DictCacheStorage + cache::Dict{DataType, Any} + DictCacheStorage() = new(Dict{DataType, Any}()) +end + +_make_cache_storage(::NoCache) = NoCacheStorage() +_make_cache_storage(::SingleCache) = SingleCacheStorage() +_make_cache_storage(::DictCache) = DictCacheStorage() + +# ============================================================================ +# Main type +# ============================================================================ + +""" + FunctionWrappersWrapper{FW, P, CS} + +A wrapper around a tuple of `FunctionWrapper`s that dispatches calls to the +matching wrapper based on argument types. When no wrapper matches, behavior is +controlled by the fallback policy `P` and cache mode `CS`. + +# Type parameters +- `FW`: Tuple type of `FunctionWrapper`s +- `P`: Fallback policy (`Strict`, `AllowAll`, `AllowNonIsBits`, or legacy `Bool`) +- `CS`: Cache storage type (`NoCacheStorage`, `SingleCacheStorage`, `DictCacheStorage`) +""" +struct FunctionWrappersWrapper{FW, P, CS} fw::FW + cache_storage::CS + function FunctionWrappersWrapper{FW, P, CS}( + fw::FW, cs::CS + ) where {FW, P, CS} + return new{FW, P, CS}(fw, cs) + end end TruncatedStacktraces.@truncate_stacktrace FunctionWrappersWrapper -function (fww::FunctionWrappersWrapper{FW, FB})(args::Vararg{Any, K}) where {FW, K, FB} +# --- New API: cache + policy keywords --- + +""" + FunctionWrappersWrapper(f, argtypes, rettypes; cache=SingleCache(), policy=AllowNonIsBits()) + +Create a `FunctionWrappersWrapper` with configurable fallback behavior. + +# Arguments +- `f`: The function to wrap +- `argtypes`: Tuple of argument type signatures (e.g., `(Tuple{Float64, Float64},)`) +- `rettypes`: Tuple of return types (e.g., `(Float64,)`) + +# Keywords +- `cache`: Cache mode for fallback path — `NoCache()`, `SingleCache()` (default), or `DictCache()` +- `policy`: Fallback policy — `Strict()`, `AllowAll()`, or `AllowNonIsBits()` (default) +""" +function FunctionWrappersWrapper( + f::F, argtypes::Tuple{Vararg{Any, K}}, rettypes::Tuple{Vararg{Type, K}}; + cache::AbstractCacheMode = SingleCache(), + policy::AbstractFallbackPolicy = AllowNonIsBits() + ) where {F, K} + fwt = map(argtypes, rettypes) do A, R + FunctionWrappers.FunctionWrapper{R, A}(f) + end + cs = _make_cache_storage(cache) + return FunctionWrappersWrapper{typeof(fwt), typeof(policy), typeof(cs)}(fwt, cs) +end + +# --- Legacy API: Val{true}/Val{false} fallback --- + +""" + FunctionWrappersWrapper(f, argtypes, rettypes, fallback::Val{FB}) + +Legacy constructor. `Val{false}()` maps to `Strict()` with `NoCache()`. +`Val{true}()` maps to `AllowAll()` with `NoCache()` (preserving the old +1-alloc-per-call behavior). +""" +function FunctionWrappersWrapper( + f::F, argtypes::Tuple{Vararg{Any, K}}, rettypes::Tuple{Vararg{Type, K}}, + fallback::Val{FB} + ) where {F, K, FB} + fwt = map(argtypes, rettypes) do A, R + FunctionWrappers.FunctionWrapper{R, A}(f) + end + policy = FB ? AllowAll : Strict + return FunctionWrappersWrapper{typeof(fwt), policy, NoCacheStorage}(fwt, NoCacheStorage()) +end + +# Legacy: direct type parameter construction with Bool FB (used by DiffEqBase) +function FunctionWrappersWrapper{FW, FB}(fw::FW) where {FW, FB} + policy = FB ? AllowAll : Strict + return FunctionWrappersWrapper{FW, policy, NoCacheStorage}(fw, NoCacheStorage()) +end + +# ============================================================================ +# Call dispatch — entry point +# ============================================================================ + +function (fww::FunctionWrappersWrapper{FW, P, CS})( + args::Vararg{Any, K} + ) where {FW, K, P, CS} return _call(fww.fw, args, fww) end +# Match path: try each FunctionWrapper in order function _call( fw::Tuple{FunctionWrappers.FunctionWrapper{R, A}, Vararg}, arg::A, fww::FunctionWrappersWrapper @@ -28,6 +191,10 @@ function _call( return _call(Base.tail(fw), arg, fww) end +# ============================================================================ +# Fallback — Strict: always error +# ============================================================================ + const NO_FUNCTIONWRAPPER_FOUND_MESSAGE = "No matching function wrapper was found!" struct NoFunctionWrapperFoundError <: Exception end @@ -36,68 +203,96 @@ function Base.showerror(io::IO, e::NoFunctionWrapperFoundError) return print(io, NO_FUNCTIONWRAPPER_FOUND_MESSAGE) end -function _call(::Tuple{}, arg, fww::FunctionWrappersWrapper{<:Any, false}) +function _call(::Tuple{}, arg, fww::FunctionWrappersWrapper{<:Any, Strict}) throw(NoFunctionWrapperFoundError()) end -function _call(::Tuple{}, arg, fww::FunctionWrappersWrapper{<:Any, true}) - return first(fww.fw).obj[](arg...) + +# ============================================================================ +# Fallback — AllowAll: always fall back +# ============================================================================ + +function _call(::Tuple{}, arg, fww::FunctionWrappersWrapper{<:Any, AllowAll}) + return _fallback(arg, fww) end -function FunctionWrappersWrapper( - f::F, argtypes::Tuple{Vararg{Any, K}}, rettypes::Tuple{Vararg{Type, K}}, - fallback::Val{FB} = Val{false}() - ) where {F, K, FB} - fwt = map(argtypes, rettypes) do A, R - FunctionWrappers.FunctionWrapper{R, A}(f) +# ============================================================================ +# Fallback — AllowNonIsBits: fall back only for non-isbits arg types +# ============================================================================ + +function _call( + ::Tuple{}, arg::A, fww::FunctionWrappersWrapper{<:Any, AllowNonIsBits} + ) where {A} + if _has_non_isbits_args(A) + return _fallback(arg, fww) end - return FunctionWrappersWrapper{typeof(fwt), FB}(fwt) + throw(NoFunctionWrapperFoundError()) end -""" - unwrap(fww::FunctionWrappersWrapper) - -Return the original function that was wrapped. This is useful for debugging -wrapped functions - you can use the returned function with debugging tools -like Debugger.jl or Infiltrator.jl. +@generated function _has_non_isbits_args(::Type{T}) where {T <: Tuple} + checks = [] + for P in T.parameters + if P <: AbstractArray + push!(checks, :(!(isbitstype($(eltype(P)))))) + else + push!(checks, :(!(isbitstype($P)))) + end + end + isempty(checks) && return :(false) + return Expr(:||, checks...) +end -# Example +# ============================================================================ +# Fallback execution — dispatch on cache storage type +# ============================================================================ -```julia -using FunctionWrappersWrappers +# --- NoCache: direct dynamic dispatch every time --- +function _fallback(arg, fww::FunctionWrappersWrapper{<:Any, <:Any, NoCacheStorage}) + return first(fww.fw).obj[](arg...) +end -# Create a wrapped function -fww = FunctionWrappersWrapper(sin, (Tuple{Float64},), (Float64,)) +# --- SingleCache: cache one FunctionWrapper for the last arg types --- +function _fallback( + arg::A, fww::FunctionWrappersWrapper{<:Any, <:Any, SingleCacheStorage} + ) where {A} + cached = fww.cache_storage.cached + if cached isa FunctionWrappers.FunctionWrapper{Any, A} + return cached(arg...) + end + f = first(fww.fw).obj[] + new_fw = FunctionWrappers.FunctionWrapper{Any, A}(f) + fww.cache_storage.cached = new_fw + return new_fw(arg...) +end -# Get the original function for debugging -f = unwrap(fww) # Returns sin +# --- DictCache: cache FunctionWrappers keyed by arg type --- +function _fallback( + arg::A, fww::FunctionWrappersWrapper{<:Any, <:Any, DictCacheStorage} + ) where {A} + cached = get(fww.cache_storage.cache, A, nothing) + if cached isa FunctionWrappers.FunctionWrapper{Any, A} + return cached(arg...) + end + f = first(fww.fw).obj[] + new_fw = FunctionWrappers.FunctionWrapper{Any, A}(f) + fww.cache_storage.cache[A] = new_fw + return new_fw(arg...) +end -# Now you can debug with Debugger.jl: -# using Debugger -# @enter f(0.5) +# ============================================================================ +# Introspection +# ============================================================================ -# Or use Infiltrator.jl in your original function definition -``` +""" + unwrap(fww::FunctionWrappersWrapper) -See also: [`wrapped_signatures`](@ref), [`wrapped_return_types`](@ref) +Return the original function that was wrapped. """ unwrap(fww::FunctionWrappersWrapper) = first(fww.fw).obj[] """ wrapped_signatures(fww::FunctionWrappersWrapper) -Return a tuple of the argument type signatures that the `FunctionWrappersWrapper` -can dispatch on. Each element is a `Tuple` type representing the argument types. - -# Example - -```julia -using FunctionWrappersWrappers - -fww = FunctionWrappersWrapper(+, (Tuple{Float64, Float64}, Tuple{Int, Int}), (Float64, Int)) -wrapped_signatures(fww) # Returns (Tuple{Float64, Float64}, Tuple{Int, Int}) -``` - -See also: [`unwrap`](@ref), [`wrapped_return_types`](@ref) +Return a tuple of the argument type signatures that the wrapper can dispatch on. """ function wrapped_signatures(fww::FunctionWrappersWrapper) return map(fw -> typeof(fw).parameters[2], fww.fw) @@ -107,30 +302,19 @@ end wrapped_return_types(fww::FunctionWrappersWrapper) Return a tuple of the return types for each wrapped function signature. - -# Example - -```julia -using FunctionWrappersWrappers - -fww = FunctionWrappersWrapper(+, (Tuple{Float64, Float64}, Tuple{Int, Int}), (Float64, Int)) -wrapped_return_types(fww) # Returns (Float64, Int64) -``` - -See also: [`unwrap`](@ref), [`wrapped_signatures`](@ref) """ function wrapped_return_types(fww::FunctionWrappersWrapper) return map(fw -> typeof(fw).parameters[1], fww.fw) end +# ============================================================================ +# Precompilation +# ============================================================================ + using PrecompileTools @setup_workload begin @compile_workload begin - # Precompile common use cases with Float64 and Int types - # These are the most common type combinations for numerical computations - - # Binary operation with multiple type combinations (common pattern) fw_binary = FunctionWrappersWrapper( +, (Tuple{Float64, Float64}, Tuple{Int, Int}), @@ -139,7 +323,6 @@ using PrecompileTools fw_binary(1.0, 2.0) fw_binary(1, 2) - # Unary operation with multiple types (common pattern) fw_unary = FunctionWrappersWrapper( abs, (Tuple{Float64}, Tuple{Int}), @@ -148,7 +331,6 @@ using PrecompileTools fw_unary(1.0) fw_unary(1) - # Precompile introspection functions unwrap(fw_unary) wrapped_signatures(fw_binary) wrapped_return_types(fw_binary) diff --git a/test/basictests.jl b/test/basictests.jl index b8ecfaa..eb5e259 100644 --- a/test/basictests.jl +++ b/test/basictests.jl @@ -19,7 +19,6 @@ using Test end @testset "Type inference" begin - # Test return type inference fwplus = FunctionWrappersWrapper( +, (Tuple{Float64, Float64}, Tuple{Int, Int}), ( Float64, Int, @@ -37,7 +36,6 @@ end end @testset "Introspection functions" begin - # Test with a simple function fwsin = FunctionWrappersWrapper(sin, (Tuple{Float64},), (Float64,)) @testset "unwrap" begin @@ -56,7 +54,6 @@ end @test rets == (Float64,) end - # Test with multiple signatures fwplus = FunctionWrappersWrapper( +, (Tuple{Float64, Float64}, Tuple{Int, Int}), ( Float64, Int, @@ -79,7 +76,6 @@ end @test rets == (Float64, Int) end - # Test with a custom function my_func(x) = x^2 fwcustom = FunctionWrappersWrapper( my_func, (Tuple{Float64}, Tuple{Int}), ( @@ -94,3 +90,189 @@ end @test f(2.5) == 6.25 end end + +@testset "Legacy API (Val{true}/Val{false})" begin + fwplus = FunctionWrappersWrapper( + +, (Tuple{Float64, Float64}, Tuple{Int, Int}), (Float64, Int), Val{false}() + ) + @test fwplus(4.0, 8.0) === 12.0 + @test fwplus(4, 8) === 12 + @test_throws FunctionWrappersWrappers.NoFunctionWrapperFoundError fwplus(4.0f0, 8.0f0) + + fwplus_fb = FunctionWrappersWrapper( + +, (Tuple{Float64, Float64}, Tuple{Int, Int}), (Float64, Int), Val{true}() + ) + @test fwplus_fb(4.0, 8.0) === 12.0 + @test fwplus_fb(4, 8) === 12 + @test fwplus_fb(4.0f0, 8.0f0) == 12.0f0 # fallback to original function +end + +@testset "Legacy FW{FW,Bool} constructor" begin + using FunctionWrappers + fw1 = FunctionWrappers.FunctionWrapper{Float64, Tuple{Float64, Float64}}(+) + fw2 = FunctionWrappers.FunctionWrapper{Int, Tuple{Int, Int}}(+) + fwt = (fw1, fw2) + + fww_strict = FunctionWrappersWrapper{typeof(fwt), false}(fwt) + @test fww_strict(4.0, 8.0) === 12.0 + @test fww_strict(4, 8) === 12 + @test_throws FunctionWrappersWrappers.NoFunctionWrapperFoundError fww_strict(4.0f0, 8.0f0) + + fww_fb = FunctionWrappersWrapper{typeof(fwt), true}(fwt) + @test fww_fb(4.0, 8.0) === 12.0 + @test fww_fb(4, 8) === 12 + @test fww_fb(4.0f0, 8.0f0) == 12.0f0 +end + +@testset "Fallback policies" begin + @testset "Strict" begin + fww = FunctionWrappersWrapper( + +, (Tuple{Float64, Float64},), (Float64,); + cache = NoCache(), policy = Strict() + ) + @test fww(4.0, 8.0) === 12.0 + @test_throws FunctionWrappersWrappers.NoFunctionWrapperFoundError fww(4, 8) + @test_throws FunctionWrappersWrappers.NoFunctionWrapperFoundError fww( + BigFloat(4), BigFloat(8) + ) + end + + @testset "AllowAll" begin + fww = FunctionWrappersWrapper( + +, (Tuple{Float64, Float64},), (Float64,); + cache = NoCache(), policy = AllowAll() + ) + @test fww(4.0, 8.0) === 12.0 + @test fww(4, 8) === 12 + @test fww(4.0f0, 8.0f0) == 12.0f0 + @test fww(BigFloat(4), BigFloat(8)) == BigFloat(12) + end + + @testset "AllowNonIsBits" begin + fww = FunctionWrappersWrapper( + +, (Tuple{Float64, Float64},), (Float64,); + cache = NoCache(), policy = AllowNonIsBits() + ) + @test fww(4.0, 8.0) === 12.0 + # Float32 is isbits but doesn't match Float64 wrapper → error + @test_throws FunctionWrappersWrappers.NoFunctionWrapperFoundError fww(4.0f0, 8.0f0) + # Int is isbits but doesn't match Float64 wrapper → error + @test_throws FunctionWrappersWrappers.NoFunctionWrapperFoundError fww(4, 8) + # BigFloat is non-isbits → allowed + @test fww(BigFloat(4), BigFloat(8)) == BigFloat(12) + end + + @testset "AllowNonIsBits with arrays" begin + f!(du, u) = (du[1] = u[1]^2; nothing) + fww = FunctionWrappersWrapper( + f!, (Tuple{Vector{Float64}, Vector{Float64}},), (Nothing,); + cache = NoCache(), policy = AllowNonIsBits() + ) + du_f = [0.0]; u_f = [3.0] + fww(du_f, u_f) + @test du_f[1] === 9.0 + + # Float32 arrays: eltype is isbits but doesn't match → error + @test_throws FunctionWrappersWrappers.NoFunctionWrapperFoundError fww( + Float32[0.0], Float32[3.0] + ) + + # BigFloat arrays: eltype is non-isbits → allowed + du_bf = BigFloat[0]; u_bf = BigFloat[3] + fww(du_bf, u_bf) + @test du_bf[1] == BigFloat(9) + end +end + +@testset "Cache modes" begin + f!(du, u, p, t) = (du[1] = p[1] * u[1]; nothing) + + @testset "NoCache" begin + fww = FunctionWrappersWrapper( + f!, + (Tuple{Vector{Float64}, Vector{Float64}, Vector{Float64}, Float64},), + (Nothing,); + cache = NoCache(), policy = AllowAll() + ) + # Float64 match + du = [0.0]; u = [2.0]; p = [3.0] + fww(du, u, p, 0.0) + @test du[1] === 6.0 + + # BigFloat fallback (NoCache: 1 alloc per call) + du_bf = BigFloat[0]; u_bf = BigFloat[2]; p_bf = BigFloat[3]; t_bf = BigFloat(0) + fww(du_bf, u_bf, p_bf, t_bf) + @test du_bf[1] == BigFloat(6) + end + + @testset "SingleCache" begin + fww = FunctionWrappersWrapper( + f!, + (Tuple{Vector{Float64}, Vector{Float64}, Vector{Float64}, Float64},), + (Nothing,); + cache = SingleCache(), policy = AllowAll() + ) + du_bf = BigFloat[0]; u_bf = BigFloat[2]; p_bf = BigFloat[3]; t_bf = BigFloat(0) + # First call caches + fww(du_bf, u_bf, p_bf, t_bf) + @test du_bf[1] == BigFloat(6) + # Second call uses cache (0 alloc) + du_bf[1] = BigFloat(0) + fww(du_bf, u_bf, p_bf, t_bf) + @test du_bf[1] == BigFloat(6) + end + + @testset "DictCache" begin + fww = FunctionWrappersWrapper( + f!, + (Tuple{Vector{Float64}, Vector{Float64}, Vector{Float64}, Float64},), + (Nothing,); + cache = DictCache(), policy = AllowAll() + ) + du_bf = BigFloat[0]; u_bf = BigFloat[2]; p_bf = BigFloat[3]; t_bf = BigFloat(0) + fww(du_bf, u_bf, p_bf, t_bf) + @test du_bf[1] == BigFloat(6) + + # Different type also works and caches separately + du_f32 = Float32[0]; u_f32 = Float32[2]; p_f32 = Float32[3]; t_f32 = Float32(0) + fww(du_f32, u_f32, p_f32, t_f32) + @test du_f32[1] === Float32(6) + + # BigFloat still cached + du_bf[1] = BigFloat(0) + fww(du_bf, u_bf, p_bf, t_bf) + @test du_bf[1] == BigFloat(6) + end + + @testset "SingleCache thrashing recovers" begin + fww = FunctionWrappersWrapper( + f!, + (Tuple{Vector{Float64}, Vector{Float64}, Vector{Float64}, Float64},), + (Nothing,); + cache = SingleCache(), policy = AllowAll() + ) + du_bf = BigFloat[0]; u_bf = BigFloat[2]; p_bf = BigFloat[3]; t_bf = BigFloat(0) + du_f32 = Float32[0]; u_f32 = Float32[2]; p_f32 = Float32[3]; t_f32 = Float32(0) + + # Alternate types — each call replaces the cache but still works + fww(du_bf, u_bf, p_bf, t_bf) + @test du_bf[1] == BigFloat(6) + fww(du_f32, u_f32, p_f32, t_f32) + @test du_f32[1] === Float32(6) + du_bf[1] = BigFloat(0) + fww(du_bf, u_bf, p_bf, t_bf) + @test du_bf[1] == BigFloat(6) + end +end + +@testset "Default constructor uses SingleCache + AllowNonIsBits" begin + fww = FunctionWrappersWrapper( + +, (Tuple{Float64, Float64},), (Float64,) + ) + # Float64 matches wrapper + @test fww(4.0, 8.0) === 12.0 + # BigFloat is non-isbits → falls back + @test fww(BigFloat(4), BigFloat(8)) == BigFloat(12) + # Float32 is isbits mismatch → errors + @test_throws FunctionWrappersWrappers.NoFunctionWrapperFoundError fww(4.0f0, 8.0f0) +end From cb2faed44af472d0fdfea8def3860c8988b95a7a Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Mon, 30 Mar 2026 05:40:03 -0400 Subject: [PATCH 17/55] Add configurable cache modes and fallback policies for non-isbits types Breaking change: removes the boolean FB type parameter and Val{true}/Val{false} API. The FunctionWrappersWrapper type now takes a fallback policy and cache storage type as type parameters instead. Cache modes (how fallback FunctionWrappers are cached): - NoCache: dynamic dispatch every call (1 alloc per call) - SingleCache: lazily cache a FunctionWrapper for last-seen arg types (0 alloc) - DictCache: cache per arg type in a Dict (0 alloc, multi-type) Fallback policies (when fallback is allowed): - Strict: always error on type mismatch - AllowAll: always fall back to original function - AllowNonIsBits: fall back for non-isbits types, error for isbits mismatches Default: SingleCache + AllowNonIsBits Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.6 (1M context) --- Project.toml | 2 +- src/FunctionWrappersWrappers.jl | 33 ++------------------------------- test/basictests.jl | 33 --------------------------------- 3 files changed, 3 insertions(+), 65 deletions(-) diff --git a/Project.toml b/Project.toml index 32791ef..020412b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FunctionWrappersWrappers" uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" authors = ["Chris Elrod and contributors"] -version = "0.2.0" +version = "1.0.0" [deps] FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" diff --git a/src/FunctionWrappersWrappers.jl b/src/FunctionWrappersWrappers.jl index d5e15c0..f358ee8 100644 --- a/src/FunctionWrappersWrappers.jl +++ b/src/FunctionWrappersWrappers.jl @@ -16,7 +16,7 @@ abstract type AbstractCacheMode end NoCache() No caching — every fallback call goes through dynamic dispatch (`obj[](arg...)`), -incurring 1 allocation per call. This is the legacy behavior when `fallback=Val{true}()`. +incurring 1 allocation per call. """ struct NoCache <: AbstractCacheMode end @@ -46,7 +46,6 @@ abstract type AbstractFallbackPolicy end Strict() Never fall back — throw `NoFunctionWrapperFoundError` if no wrapper matches. -This is the legacy behavior when `fallback=Val{false}()`. """ struct Strict <: AbstractFallbackPolicy end @@ -54,7 +53,6 @@ struct Strict <: AbstractFallbackPolicy end AllowAll() Always fall back to the original function when no wrapper matches. -This is the legacy behavior when `fallback=Val{true}()`. """ struct AllowAll <: AbstractFallbackPolicy end @@ -98,7 +96,7 @@ controlled by the fallback policy `P` and cache mode `CS`. # Type parameters - `FW`: Tuple type of `FunctionWrapper`s -- `P`: Fallback policy (`Strict`, `AllowAll`, `AllowNonIsBits`, or legacy `Bool`) +- `P`: Fallback policy (`Strict`, `AllowAll`, or `AllowNonIsBits`) - `CS`: Cache storage type (`NoCacheStorage`, `SingleCacheStorage`, `DictCacheStorage`) """ struct FunctionWrappersWrapper{FW, P, CS} @@ -113,8 +111,6 @@ end TruncatedStacktraces.@truncate_stacktrace FunctionWrappersWrapper -# --- New API: cache + policy keywords --- - """ FunctionWrappersWrapper(f, argtypes, rettypes; cache=SingleCache(), policy=AllowNonIsBits()) @@ -141,31 +137,6 @@ function FunctionWrappersWrapper( return FunctionWrappersWrapper{typeof(fwt), typeof(policy), typeof(cs)}(fwt, cs) end -# --- Legacy API: Val{true}/Val{false} fallback --- - -""" - FunctionWrappersWrapper(f, argtypes, rettypes, fallback::Val{FB}) - -Legacy constructor. `Val{false}()` maps to `Strict()` with `NoCache()`. -`Val{true}()` maps to `AllowAll()` with `NoCache()` (preserving the old -1-alloc-per-call behavior). -""" -function FunctionWrappersWrapper( - f::F, argtypes::Tuple{Vararg{Any, K}}, rettypes::Tuple{Vararg{Type, K}}, - fallback::Val{FB} - ) where {F, K, FB} - fwt = map(argtypes, rettypes) do A, R - FunctionWrappers.FunctionWrapper{R, A}(f) - end - policy = FB ? AllowAll : Strict - return FunctionWrappersWrapper{typeof(fwt), policy, NoCacheStorage}(fwt, NoCacheStorage()) -end - -# Legacy: direct type parameter construction with Bool FB (used by DiffEqBase) -function FunctionWrappersWrapper{FW, FB}(fw::FW) where {FW, FB} - policy = FB ? AllowAll : Strict - return FunctionWrappersWrapper{FW, policy, NoCacheStorage}(fw, NoCacheStorage()) -end # ============================================================================ # Call dispatch — entry point diff --git a/test/basictests.jl b/test/basictests.jl index eb5e259..592049f 100644 --- a/test/basictests.jl +++ b/test/basictests.jl @@ -91,39 +91,6 @@ end end end -@testset "Legacy API (Val{true}/Val{false})" begin - fwplus = FunctionWrappersWrapper( - +, (Tuple{Float64, Float64}, Tuple{Int, Int}), (Float64, Int), Val{false}() - ) - @test fwplus(4.0, 8.0) === 12.0 - @test fwplus(4, 8) === 12 - @test_throws FunctionWrappersWrappers.NoFunctionWrapperFoundError fwplus(4.0f0, 8.0f0) - - fwplus_fb = FunctionWrappersWrapper( - +, (Tuple{Float64, Float64}, Tuple{Int, Int}), (Float64, Int), Val{true}() - ) - @test fwplus_fb(4.0, 8.0) === 12.0 - @test fwplus_fb(4, 8) === 12 - @test fwplus_fb(4.0f0, 8.0f0) == 12.0f0 # fallback to original function -end - -@testset "Legacy FW{FW,Bool} constructor" begin - using FunctionWrappers - fw1 = FunctionWrappers.FunctionWrapper{Float64, Tuple{Float64, Float64}}(+) - fw2 = FunctionWrappers.FunctionWrapper{Int, Tuple{Int, Int}}(+) - fwt = (fw1, fw2) - - fww_strict = FunctionWrappersWrapper{typeof(fwt), false}(fwt) - @test fww_strict(4.0, 8.0) === 12.0 - @test fww_strict(4, 8) === 12 - @test_throws FunctionWrappersWrappers.NoFunctionWrapperFoundError fww_strict(4.0f0, 8.0f0) - - fww_fb = FunctionWrappersWrapper{typeof(fwt), true}(fwt) - @test fww_fb(4.0, 8.0) === 12.0 - @test fww_fb(4, 8) === 12 - @test fww_fb(4.0f0, 8.0f0) == 12.0f0 -end - @testset "Fallback policies" begin @testset "Strict" begin fww = FunctionWrappersWrapper( From bc44175c52052bb8ee3a39ee121e5ccb643ad4a9 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Fri, 3 Apr 2026 08:16:47 -0400 Subject: [PATCH 18/55] Add Enzyme and Mooncake AD extensions for auto-unwrapping When FunctionWrappersWrapper is called in an AD context, these extensions bypass the type-erased FunctionWrapper call and differentiate through the original unwrapped function directly. Enzyme extension: defines forward/reverse custom rules that unwrap the function and use Enzyme.autodiff on the original function. Mooncake extension: marks FunctionWrappersWrapper calls as primitive and delegates to the FunctionWrapper's existing rrule from MooncakeFunctionWrappersExt. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.6 (1M context) --- Project.toml | 17 ++- ext/FunctionWrappersWrappersEnzymeExt.jl | 131 +++++++++++++++++++++ ext/FunctionWrappersWrappersMooncakeExt.jl | 36 ++++++ test/enzyme_tests.jl | 52 ++++++++ test/mooncake_tests.jl | 44 +++++++ test/runtests.jl | 12 ++ 6 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 ext/FunctionWrappersWrappersEnzymeExt.jl create mode 100644 ext/FunctionWrappersWrappersMooncakeExt.jl create mode 100644 test/enzyme_tests.jl create mode 100644 test/mooncake_tests.jl diff --git a/Project.toml b/Project.toml index 020412b..a192e30 100644 --- a/Project.toml +++ b/Project.toml @@ -8,15 +8,30 @@ FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" TruncatedStacktraces = "781d530d-4396-4725-bb49-402e4bee1e77" +[weakdeps] +Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" +EnzymeCore = "f151be2c-9106-41f4-ab19-57ee4f262869" +Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" + +[extensions] +FunctionWrappersWrappersEnzymeExt = ["Enzyme", "EnzymeCore"] +FunctionWrappersWrappersMooncakeExt = "Mooncake" + [compat] +Enzyme = "0.13" +EnzymeCore = "0.8" FunctionWrappers = "1" +Mooncake = "0.4, 0.5" PrecompileTools = "1" TruncatedStacktraces = "1" julia = "1.10" [extras] +Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" +EnzymeCore = "f151be2c-9106-41f4-ab19-57ee4f262869" +Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Pkg", "Test"] +test = ["Pkg", "Test", "Enzyme", "EnzymeCore", "Mooncake"] diff --git a/ext/FunctionWrappersWrappersEnzymeExt.jl b/ext/FunctionWrappersWrappersEnzymeExt.jl new file mode 100644 index 0000000..8b35f36 --- /dev/null +++ b/ext/FunctionWrappersWrappersEnzymeExt.jl @@ -0,0 +1,131 @@ +module FunctionWrappersWrappersEnzymeExt + +using FunctionWrappersWrappers +using Enzyme +using EnzymeCore +using EnzymeCore.EnzymeRules + +# ============================================================================= +# Forward mode rules +# ============================================================================= + +# Shadow only (Forward mode, no primal) +function EnzymeRules.forward( + ::EnzymeRules.FwdConfig{false, true, 1, RuntimeActivity, StrongZero}, + func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + RT::Type{<:EnzymeCore.Annotation{T}}, + args::Vararg{EnzymeCore.Annotation, N} +) where {T, N, RuntimeActivity, StrongZero} + f_orig = unwrap(func.val) + shadow_result = Enzyme.autodiff(Forward, Const(f_orig), Duplicated{T}, args...) + return shadow_result[1]::T +end + +# Both primal and shadow (ForwardWithPrimal mode) +function EnzymeRules.forward( + ::EnzymeRules.FwdConfig{true, true, 1, RuntimeActivity, StrongZero}, + func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + RT::Type{<:EnzymeCore.Annotation{T}}, + args::Vararg{EnzymeCore.Annotation, N} +) where {T, N, RuntimeActivity, StrongZero} + f_orig = unwrap(func.val) + pargs = ntuple(i -> args[i].val, Val(N)) + primal = f_orig(pargs...)::T + shadow_result = Enzyme.autodiff(Forward, Const(f_orig), Duplicated{T}, args...) + shadow = shadow_result[1]::T + return Duplicated(primal, shadow) +end + +# Primal only (Const return type) +function EnzymeRules.forward( + ::EnzymeRules.FwdConfig{true, false, 1, RuntimeActivity, StrongZero}, + func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + RT::Type{<:EnzymeCore.Annotation}, + args::Vararg{EnzymeCore.Annotation, N} +) where {N, RuntimeActivity, StrongZero} + f_orig = unwrap(func.val) + pargs = ntuple(i -> args[i].val, Val(N)) + return f_orig(pargs...) +end + +# ============================================================================= +# Reverse mode rules +# ============================================================================= + +function EnzymeRules.augmented_primal( + config::EnzymeRules.RevConfig, + func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + RT::Type{<:EnzymeCore.Active{T}}, + args::Vararg{EnzymeCore.Annotation, N} +) where {T, N} + f_orig = unwrap(func.val) + pargs = ntuple(i -> args[i].val, Val(N)) + result = f_orig(pargs...)::T + + if EnzymeRules.needs_primal(config) + return EnzymeRules.AugmentedReturn(result, nothing, nothing) + else + return EnzymeRules.AugmentedReturn(nothing, nothing, nothing) + end +end + +# Varargs reverse: compute each partial via forward-mode AD on the unwrapped +# function, then scale by dret. This avoids type-inference issues that arise +# from calling autodiff(Reverse, Const{Any}(...), ...). +@generated function _fww_reverse_grads( + f_orig, dret_val::T, args::Vararg{EnzymeCore.Active, N} +) where {T, N} + # Build forward-mode calls for each partial derivative + exprs = [] + for i in 1:N + seeds = [j == i ? :(one(eltype(typeof(args[$j])))) : :(zero(eltype(typeof(args[$j])))) for j in 1:N] + dups = [:(Duplicated(args[$j].val, $(seeds[j]))) for j in 1:N] + Ti = :(eltype(typeof(args[$i]))) + push!(exprs, quote + fwd = Enzyme.autodiff(Forward, Const(f_orig), Duplicated{$T}, $(dups...)) + $Ti(fwd[1] * dret_val)::$Ti + end) + end + return Expr(:tuple, exprs...) +end + +function EnzymeRules.reverse( + config::EnzymeRules.RevConfig, + func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + dret::EnzymeCore.Active{T}, + tape, + args::Vararg{EnzymeCore.Active, N} +) where {T, N} + f_orig = unwrap(func.val) + return _fww_reverse_grads(f_orig, dret.val, args...) +end + +# Handle mixed Active/Const args: return nothing for Const, gradient for Active +function EnzymeRules.reverse( + config::EnzymeRules.RevConfig, + func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + dret::EnzymeCore.Active, + tape, + args::Vararg{EnzymeCore.Annotation, N} +) where {N} + f_orig = unwrap(func.val) + dret_val = dret.val + return ntuple(Val(N)) do i + if args[i] isa EnzymeCore.Const + nothing + else + # Use forward-mode to compute partial derivative + dup_args = ntuple(Val(N)) do j + if j == i + Duplicated(args[j].val, one(eltype(typeof(args[j])))) + else + Duplicated(args[j].val, zero(eltype(typeof(args[j])))) + end + end + fwd = Enzyme.autodiff(Forward, Const(f_orig), Duplicated, dup_args...) + fwd[1] * dret_val + end + end +end + +end diff --git a/ext/FunctionWrappersWrappersMooncakeExt.jl b/ext/FunctionWrappersWrappersMooncakeExt.jl new file mode 100644 index 0000000..f3fe1cb --- /dev/null +++ b/ext/FunctionWrappersWrappersMooncakeExt.jl @@ -0,0 +1,36 @@ +module FunctionWrappersWrappersMooncakeExt + +using FunctionWrappersWrappers +using FunctionWrappers: FunctionWrapper +using Mooncake: @is_primitive, MinimalCtx, CoDual, NoRData, primal +import Mooncake: rrule!! + +# Make FunctionWrappersWrapper calls a primitive so Mooncake bypasses the +# internal _call dispatch and directly delegates to the FunctionWrapper's rrule +# (from MooncakeFunctionWrappersExt), which auto-unwraps to the original function. +@is_primitive MinimalCtx Tuple{<:FunctionWrappersWrapper, Vararg} + +function rrule!!( + fww_dual::CoDual{<:FunctionWrappersWrapper}, args::Vararg{CoDual, N} +) where {N} + fww = primal(fww_dual) + fww_fdata = fww_dual.dx # FData{NamedTuple{(:fw, :cache_storage), ...}} + + # Extract first FunctionWrapper and its tangent + fw = first(fww.fw) + fw_tang = first(fww_fdata.data.fw) + fw_dual = CoDual(fw, fw_tang) + + # Delegate to FunctionWrapper's rrule (from MooncakeFunctionWrappersExt) + y, fw_pb = rrule!!(fw_dual, args...) + + function fww_pullback(dy) + result = fw_pb(dy) + # result = (NoRData(), dx...) from FunctionWrapper's pullback + return (NoRData(), Base.tail(result)...) + end + + return y, fww_pullback +end + +end diff --git a/test/enzyme_tests.jl b/test/enzyme_tests.jl new file mode 100644 index 0000000..4c3cb3a --- /dev/null +++ b/test/enzyme_tests.jl @@ -0,0 +1,52 @@ +using FunctionWrappersWrappers +using Enzyme +using Test + +@testset "Enzyme forward mode" begin + f(x) = x^2 + fww = FunctionWrappersWrapper(f, (Tuple{Float64},), (Float64,)) + + # Forward mode (shadow only) + result = Enzyme.autodiff(Forward, Const(fww), Duplicated, Duplicated(3.0, 1.0)) + @test result[1] ≈ 6.0 + + # ForwardWithPrimal (both primal and shadow) + result = Enzyme.autodiff(ForwardWithPrimal, Const(fww), Duplicated, Duplicated(3.0, 1.0)) + @test result[1] ≈ 6.0 # shadow + @test result[2] ≈ 9.0 # primal +end + +@testset "Enzyme reverse mode - single arg" begin + f(x) = x^2 + fww = FunctionWrappersWrapper(f, (Tuple{Float64},), (Float64,)) + + # Reverse mode + result = Enzyme.autodiff(Reverse, Const(fww), Active, Active(3.0)) + @test result[1][1] ≈ 6.0 + + # ReverseWithPrimal + result = Enzyme.autodiff(ReverseWithPrimal, Const(fww), Active, Active(3.0)) + @test result[1][1] ≈ 6.0 # gradient + @test result[2] ≈ 9.0 # primal +end + +@testset "Enzyme reverse mode - multi arg" begin + g(x, y) = x * y + x^2 + fww = FunctionWrappersWrapper(g, (Tuple{Float64, Float64},), (Float64,)) + + # g(x,y) = x*y + x^2 → ∂g/∂x = y + 2x, ∂g/∂y = x + result = Enzyme.autodiff(Reverse, Const(fww), Active, Active(3.0), Active(4.0)) + @test result[1][1] ≈ 10.0 # ∂g/∂x at (3,4) = 4 + 6 + @test result[1][2] ≈ 3.0 # ∂g/∂y at (3,4) = 3 +end + +@testset "Enzyme with trig functions" begin + fww_sin = FunctionWrappersWrapper(sin, (Tuple{Float64},), (Float64,)) + + # sin'(x) = cos(x) + result = Enzyme.autodiff(Forward, Const(fww_sin), Duplicated, Duplicated(1.0, 1.0)) + @test result[1] ≈ cos(1.0) + + result = Enzyme.autodiff(Reverse, Const(fww_sin), Active, Active(1.0)) + @test result[1][1] ≈ cos(1.0) +end diff --git a/test/mooncake_tests.jl b/test/mooncake_tests.jl new file mode 100644 index 0000000..56e26c5 --- /dev/null +++ b/test/mooncake_tests.jl @@ -0,0 +1,44 @@ +using FunctionWrappersWrappers +using Mooncake +using Test + +@testset "Mooncake reverse mode - single arg" begin + f(x) = x^2 + fww = FunctionWrappersWrapper(f, (Tuple{Float64},), (Float64,)) + + rule = Mooncake.build_rrule(fww, 3.0) + val, (dfww, dx) = Mooncake.value_and_gradient!!(rule, fww, 3.0) + @test val ≈ 9.0 + @test dx ≈ 6.0 +end + +@testset "Mooncake reverse mode - multi arg" begin + g(x, y) = x * y + x^2 + fww = FunctionWrappersWrapper(g, (Tuple{Float64, Float64},), (Float64,)) + + rule = Mooncake.build_rrule(fww, 3.0, 4.0) + val, (dfww, dx, dy) = Mooncake.value_and_gradient!!(rule, fww, 3.0, 4.0) + @test val ≈ 21.0 # 3*4 + 9 + @test dx ≈ 10.0 # y + 2x = 4 + 6 + @test dy ≈ 3.0 # x +end + +@testset "Mooncake with trig functions" begin + fww_sin = FunctionWrappersWrapper(sin, (Tuple{Float64},), (Float64,)) + + rule = Mooncake.build_rrule(fww_sin, 1.0) + val, (dfww, dx) = Mooncake.value_and_gradient!!(rule, fww_sin, 1.0) + @test val ≈ sin(1.0) + @test dx ≈ cos(1.0) +end + +@testset "Mooncake unwrap correctness" begin + # Verify that the overlay correctly unwraps to the original function + f(x) = exp(x) + x^3 + fww = FunctionWrappersWrapper(f, (Tuple{Float64},), (Float64,)) + + rule = Mooncake.build_rrule(fww, 2.0) + val, (dfww, dx) = Mooncake.value_and_gradient!!(rule, fww, 2.0) + @test val ≈ exp(2.0) + 8.0 + @test dx ≈ exp(2.0) + 12.0 # exp(x) + 3x^2 +end diff --git a/test/runtests.jl b/test/runtests.jl index 7a015ef..0018a99 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -53,3 +53,15 @@ end @test result isa Vector{Float64} @test result == [2.0, 4.0, 6.0] end + +if GROUP == "All" || GROUP == "Enzyme" + @testset "Enzyme extension" begin + include("enzyme_tests.jl") + end +end + +if GROUP == "All" || GROUP == "Mooncake" + @testset "Mooncake extension" begin + include("mooncake_tests.jl") + end +end From fd31f0d4304210e7a024bf2751c13ccf603c2a27 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Fri, 3 Apr 2026 08:23:52 -0400 Subject: [PATCH 19/55] =?UTF-8?q?Drop=20Mooncake=20extension=20=E2=80=94?= =?UTF-8?q?=20Mooncake=20already=20works=20via=20MooncakeFunctionWrappersE?= =?UTF-8?q?xt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mooncake's built-in FunctionWrappers extension handles differentiation through FunctionWrapper calls, so FunctionWrappersWrapper works out of the box without a dedicated extension. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.6 (1M context) --- Project.toml | 6 +-- ext/FunctionWrappersWrappersMooncakeExt.jl | 36 ------------------ test/mooncake_tests.jl | 44 ---------------------- test/runtests.jl | 6 --- 4 files changed, 1 insertion(+), 91 deletions(-) delete mode 100644 ext/FunctionWrappersWrappersMooncakeExt.jl delete mode 100644 test/mooncake_tests.jl diff --git a/Project.toml b/Project.toml index a192e30..8270ebc 100644 --- a/Project.toml +++ b/Project.toml @@ -11,17 +11,14 @@ TruncatedStacktraces = "781d530d-4396-4725-bb49-402e4bee1e77" [weakdeps] Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" EnzymeCore = "f151be2c-9106-41f4-ab19-57ee4f262869" -Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" [extensions] FunctionWrappersWrappersEnzymeExt = ["Enzyme", "EnzymeCore"] -FunctionWrappersWrappersMooncakeExt = "Mooncake" [compat] Enzyme = "0.13" EnzymeCore = "0.8" FunctionWrappers = "1" -Mooncake = "0.4, 0.5" PrecompileTools = "1" TruncatedStacktraces = "1" julia = "1.10" @@ -29,9 +26,8 @@ julia = "1.10" [extras] Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" EnzymeCore = "f151be2c-9106-41f4-ab19-57ee4f262869" -Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Pkg", "Test", "Enzyme", "EnzymeCore", "Mooncake"] +test = ["Pkg", "Test", "Enzyme", "EnzymeCore"] diff --git a/ext/FunctionWrappersWrappersMooncakeExt.jl b/ext/FunctionWrappersWrappersMooncakeExt.jl deleted file mode 100644 index f3fe1cb..0000000 --- a/ext/FunctionWrappersWrappersMooncakeExt.jl +++ /dev/null @@ -1,36 +0,0 @@ -module FunctionWrappersWrappersMooncakeExt - -using FunctionWrappersWrappers -using FunctionWrappers: FunctionWrapper -using Mooncake: @is_primitive, MinimalCtx, CoDual, NoRData, primal -import Mooncake: rrule!! - -# Make FunctionWrappersWrapper calls a primitive so Mooncake bypasses the -# internal _call dispatch and directly delegates to the FunctionWrapper's rrule -# (from MooncakeFunctionWrappersExt), which auto-unwraps to the original function. -@is_primitive MinimalCtx Tuple{<:FunctionWrappersWrapper, Vararg} - -function rrule!!( - fww_dual::CoDual{<:FunctionWrappersWrapper}, args::Vararg{CoDual, N} -) where {N} - fww = primal(fww_dual) - fww_fdata = fww_dual.dx # FData{NamedTuple{(:fw, :cache_storage), ...}} - - # Extract first FunctionWrapper and its tangent - fw = first(fww.fw) - fw_tang = first(fww_fdata.data.fw) - fw_dual = CoDual(fw, fw_tang) - - # Delegate to FunctionWrapper's rrule (from MooncakeFunctionWrappersExt) - y, fw_pb = rrule!!(fw_dual, args...) - - function fww_pullback(dy) - result = fw_pb(dy) - # result = (NoRData(), dx...) from FunctionWrapper's pullback - return (NoRData(), Base.tail(result)...) - end - - return y, fww_pullback -end - -end diff --git a/test/mooncake_tests.jl b/test/mooncake_tests.jl deleted file mode 100644 index 56e26c5..0000000 --- a/test/mooncake_tests.jl +++ /dev/null @@ -1,44 +0,0 @@ -using FunctionWrappersWrappers -using Mooncake -using Test - -@testset "Mooncake reverse mode - single arg" begin - f(x) = x^2 - fww = FunctionWrappersWrapper(f, (Tuple{Float64},), (Float64,)) - - rule = Mooncake.build_rrule(fww, 3.0) - val, (dfww, dx) = Mooncake.value_and_gradient!!(rule, fww, 3.0) - @test val ≈ 9.0 - @test dx ≈ 6.0 -end - -@testset "Mooncake reverse mode - multi arg" begin - g(x, y) = x * y + x^2 - fww = FunctionWrappersWrapper(g, (Tuple{Float64, Float64},), (Float64,)) - - rule = Mooncake.build_rrule(fww, 3.0, 4.0) - val, (dfww, dx, dy) = Mooncake.value_and_gradient!!(rule, fww, 3.0, 4.0) - @test val ≈ 21.0 # 3*4 + 9 - @test dx ≈ 10.0 # y + 2x = 4 + 6 - @test dy ≈ 3.0 # x -end - -@testset "Mooncake with trig functions" begin - fww_sin = FunctionWrappersWrapper(sin, (Tuple{Float64},), (Float64,)) - - rule = Mooncake.build_rrule(fww_sin, 1.0) - val, (dfww, dx) = Mooncake.value_and_gradient!!(rule, fww_sin, 1.0) - @test val ≈ sin(1.0) - @test dx ≈ cos(1.0) -end - -@testset "Mooncake unwrap correctness" begin - # Verify that the overlay correctly unwraps to the original function - f(x) = exp(x) + x^3 - fww = FunctionWrappersWrapper(f, (Tuple{Float64},), (Float64,)) - - rule = Mooncake.build_rrule(fww, 2.0) - val, (dfww, dx) = Mooncake.value_and_gradient!!(rule, fww, 2.0) - @test val ≈ exp(2.0) + 8.0 - @test dx ≈ exp(2.0) + 12.0 # exp(x) + 3x^2 -end diff --git a/test/runtests.jl b/test/runtests.jl index 0018a99..cfd6b6a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -59,9 +59,3 @@ if GROUP == "All" || GROUP == "Enzyme" include("enzyme_tests.jl") end end - -if GROUP == "All" || GROUP == "Mooncake" - @testset "Mooncake extension" begin - include("mooncake_tests.jl") - end -end From ccf9ed1d48c5f2297ce0569e5f7f9ea27a85dae5 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Fri, 3 Apr 2026 12:25:57 +0000 Subject: [PATCH 20/55] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 8270ebc..73aea5b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FunctionWrappersWrappers" uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" authors = ["Chris Elrod and contributors"] -version = "1.0.0" +version = "1.1.0" [deps] FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" From b943a467c2522f73bfe420f950d6217da9602a03 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Wed, 8 Apr 2026 11:56:42 -0400 Subject: [PATCH 21/55] Add Mooncake extension for FunctionWrappersWrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Mooncake differentiates through code that calls a FunctionWrappersWrapper, it tries to create tangent types for each FunctionWrapper variant in the internal tuple. These variants have different type parameters (for different ForwardDiff Dual combinations), producing incompatible FunctionWrapperTangent types that can't be stored in a typed tuple — causing a convert error. The fix: make FunctionWrappersWrapper calls a Mooncake primitive that unwraps to the original function (via `unwrap`) and differentiates through that directly. This mirrors the existing Enzyme extension pattern. The FunctionWrappersWrapper itself gets NoTangent since it's runtime dispatch infrastructure, not differentiable data — the original function's derivatives are handled in the rrule. This enables Mooncake to differentiate through NonlinearProblem solves that use AutoSpecialize (FunctionWrappers), which is needed for SCCNonlinearProblem AD support in SciMLSensitivity.jl (#1358). Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.6 (1M context) --- Project.toml | 8 ++- ext/FunctionWrappersWrappersMooncakeExt.jl | 29 ++++++++ test/mooncake_tests.jl | 77 ++++++++++++++++++++++ test/runtests.jl | 6 ++ 4 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 ext/FunctionWrappersWrappersMooncakeExt.jl create mode 100644 test/mooncake_tests.jl diff --git a/Project.toml b/Project.toml index 73aea5b..ac0592f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FunctionWrappersWrappers" uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" authors = ["Chris Elrod and contributors"] -version = "1.1.0" +version = "1.2.0" [deps] FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" @@ -11,14 +11,17 @@ TruncatedStacktraces = "781d530d-4396-4725-bb49-402e4bee1e77" [weakdeps] Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" EnzymeCore = "f151be2c-9106-41f4-ab19-57ee4f262869" +Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" [extensions] FunctionWrappersWrappersEnzymeExt = ["Enzyme", "EnzymeCore"] +FunctionWrappersWrappersMooncakeExt = "Mooncake" [compat] Enzyme = "0.13" EnzymeCore = "0.8" FunctionWrappers = "1" +Mooncake = "0.5" PrecompileTools = "1" TruncatedStacktraces = "1" julia = "1.10" @@ -26,8 +29,9 @@ julia = "1.10" [extras] Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" EnzymeCore = "f151be2c-9106-41f4-ab19-57ee4f262869" +Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Pkg", "Test", "Enzyme", "EnzymeCore"] +test = ["Pkg", "Test", "Enzyme", "EnzymeCore", "Mooncake"] diff --git a/ext/FunctionWrappersWrappersMooncakeExt.jl b/ext/FunctionWrappersWrappersMooncakeExt.jl new file mode 100644 index 0000000..33e231e --- /dev/null +++ b/ext/FunctionWrappersWrappersMooncakeExt.jl @@ -0,0 +1,29 @@ +module FunctionWrappersWrappersMooncakeExt + +using FunctionWrappersWrappers +import Mooncake +using Mooncake: @is_primitive, MinimalCtx, CoDual, NoRData, zero_tangent, NoTangent + +# Make calling a FunctionWrappersWrapper a Mooncake primitive. +# Instead of differentiating through the FunctionWrapper dispatch machinery +# (which fails because the tuple of differently-typed FunctionWrappers produces +# incompatible FunctionWrapperTangent types), unwrap to the original function +# and differentiate through that directly. + +@is_primitive MinimalCtx Tuple{<:FunctionWrappersWrapper, Vararg} + +function Mooncake.rrule!!( + f::CoDual{<:FunctionWrappersWrapper}, args::Vararg{CoDual}, + ) + f_orig = unwrap(f.x) + f_orig_codual = CoDual(f_orig, zero_tangent(f_orig)) + y, pb = Mooncake.rrule!!(f_orig_codual, args...) + fww_pb(dy) = (NoRData(), Mooncake.Base.tail(pb(dy))...) + return y, fww_pb +end + +# FunctionWrappersWrapper is not differentiable data itself — the wrapped function +# is what carries the derivative information, and we handle that in the rrule above. +Mooncake.tangent_type(::Type{<:FunctionWrappersWrapper}) = NoTangent + +end diff --git a/test/mooncake_tests.jl b/test/mooncake_tests.jl new file mode 100644 index 0000000..eef7068 --- /dev/null +++ b/test/mooncake_tests.jl @@ -0,0 +1,77 @@ +using FunctionWrappersWrappers +using Mooncake +using Test + +@testset "Mooncake reverse mode - single arg" begin + f(x) = x^2 + fww = FunctionWrappersWrapper(f, (Tuple{Float64},), (Float64,)) + + rule = Mooncake.build_rrule(fww, 3.0) + val, (_, dx) = Mooncake.value_and_gradient!!(rule, fww, 3.0) + @test val ≈ 9.0 + @test dx ≈ 6.0 +end + +@testset "Mooncake reverse mode - multi arg" begin + g(x, y) = x * y + x^2 + fww = FunctionWrappersWrapper(g, (Tuple{Float64, Float64},), (Float64,)) + + # g(x,y) = x*y + x^2 → ∂g/∂x = y + 2x, ∂g/∂y = x + rule = Mooncake.build_rrule(fww, 3.0, 4.0) + val, (_, dx, dy) = Mooncake.value_and_gradient!!(rule, fww, 3.0, 4.0) + @test val ≈ 21.0 + @test dx ≈ 10.0 # ∂g/∂x at (3,4) = 4 + 6 + @test dy ≈ 3.0 # ∂g/∂y at (3,4) = 3 +end + +@testset "Mooncake with trig functions" begin + fww_sin = FunctionWrappersWrapper(sin, (Tuple{Float64},), (Float64,)) + + rule = Mooncake.build_rrule(fww_sin, 1.0) + val, (_, dx) = Mooncake.value_and_gradient!!(rule, fww_sin, 1.0) + @test val ≈ sin(1.0) + @test dx ≈ cos(1.0) +end + +@testset "Mooncake through loss function" begin + # Test that Mooncake can differentiate a loss function that calls FunctionWrappersWrapper + f(x) = x[1]^2 + x[2]^2 + fww = FunctionWrappersWrapper(f, (Tuple{Vector{Float64}},), (Float64,)) + + loss(x) = fww(x) + rule = Mooncake.build_rrule(loss, [3.0, 4.0]) + val, (_, dx) = Mooncake.value_and_gradient!!(rule, loss, [3.0, 4.0]) + @test val ≈ 25.0 + @test dx ≈ [6.0, 8.0] +end + +@testset "Mooncake in-place function" begin + # In-place functions are common in SciML (f!(du, u, p, t)) + function f!(du, u, p) + du[1] = p[1] * u[1] + p[2] * u[2] + du[2] = p[3] * u[1] - u[2] + return nothing + end + fww = FunctionWrappersWrapper( + f!, + (Tuple{Vector{Float64}, Vector{Float64}, Vector{Float64}},), + (Nothing,), + ) + + function loss(p) + u = [1.0, 2.0] + du = similar(u) + fww(du, u, p) + return sum(abs2, du) + end + + rule = Mooncake.build_rrule(loss, [1.0, 2.0, 3.0]) + val, (_, dp) = Mooncake.value_and_gradient!!(rule, loss, [1.0, 2.0, 3.0]) + # f!(du, [1,2], [1,2,3]) → du = [1*1+2*2, 3*1-2] = [5, 1] + # loss = 25 + 1 = 26 + @test val ≈ 26.0 + # ∂loss/∂p1 = 2*du[1]*u[1] = 2*5*1 = 10 + # ∂loss/∂p2 = 2*du[1]*u[2] = 2*5*2 = 20 + # ∂loss/∂p3 = 2*du[2]*u[1] = 2*1*1 = 2 + @test dp ≈ [10.0, 20.0, 2.0] +end diff --git a/test/runtests.jl b/test/runtests.jl index cfd6b6a..0018a99 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -59,3 +59,9 @@ if GROUP == "All" || GROUP == "Enzyme" include("enzyme_tests.jl") end end + +if GROUP == "All" || GROUP == "Mooncake" + @testset "Mooncake extension" begin + include("mooncake_tests.jl") + end +end From 39b84f169f516d0478d68c8f2773e724650f8ad3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:15:38 +0000 Subject: [PATCH 22/55] Update FunctionWrappersWrappers requirement 1.1 Updates the requirements on [FunctionWrappersWrappers](https://github.com/SciML/FunctionWrappersWrappers.jl) to permit the latest version. Updates `FunctionWrappersWrappers` to 1.1.0 - [Release notes](https://github.com/SciML/FunctionWrappersWrappers.jl/releases) - [Commits](https://github.com/SciML/FunctionWrappersWrappers.jl/commits/v1.1.0) --- updated-dependencies: - dependency-name: FunctionWrappersWrappers dependency-version: 1.1.0 dependency-type: direct:production dependency-group: all-julia-packages ... Signed-off-by: dependabot[bot] --- docs/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Project.toml b/docs/Project.toml index b17b46b..c279ceb 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -4,4 +4,4 @@ FunctionWrappersWrappers = "77dc65aa-8811-40c2-897b-53d922fa7daf" [compat] Documenter = "1.16.1" -FunctionWrappersWrappers = "0.1.3" +FunctionWrappersWrappers = "0.1.3, 1.1" From deb0aa9ad917e0f7cda0d9ba4153b4369254a143 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Thu, 9 Apr 2026 11:58:24 +0000 Subject: [PATCH 23/55] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index ac0592f..ec14528 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FunctionWrappersWrappers" uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" authors = ["Chris Elrod and contributors"] -version = "1.2.0" +version = "1.3.0" [deps] FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" From ca2e02b893e367267562109d440045250457dad2 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Thu, 9 Apr 2026 07:59:13 -0400 Subject: [PATCH 24/55] Fix Mooncake extension: use build_rrule + fdata, add VoidWrapper test v1.2.0's Mooncake extension used rrule!! which only works for Mooncake primitives. The unwrapped function (e.g. SciMLBase.Void) is generally not a primitive, causing MethodError. Also used zero_tangent which returns NoTangent, but derived rules expect NoFData. Fixes: - Use build_rrule to create a derived rule for the unwrapped function - Use fdata(zero_tangent(...)) to get the correct fdata type Add VoidWrapper test that exercises the exact pattern used by SciML's AutoSpecialize (callable struct wrapping the actual function). Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.6 (1M context) --- Project.toml | 2 +- ext/FunctionWrappersWrappersMooncakeExt.jl | 15 +++++--- test/mooncake_tests.jl | 41 ++++++++++++++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/Project.toml b/Project.toml index ac0592f..5715990 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FunctionWrappersWrappers" uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" authors = ["Chris Elrod and contributors"] -version = "1.2.0" +version = "1.2.1" [deps] FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" diff --git a/ext/FunctionWrappersWrappersMooncakeExt.jl b/ext/FunctionWrappersWrappersMooncakeExt.jl index 33e231e..3e2aa17 100644 --- a/ext/FunctionWrappersWrappersMooncakeExt.jl +++ b/ext/FunctionWrappersWrappersMooncakeExt.jl @@ -2,7 +2,7 @@ module FunctionWrappersWrappersMooncakeExt using FunctionWrappersWrappers import Mooncake -using Mooncake: @is_primitive, MinimalCtx, CoDual, NoRData, zero_tangent, NoTangent +using Mooncake: @is_primitive, MinimalCtx, CoDual, NoRData, zero_tangent, NoTangent, fdata # Make calling a FunctionWrappersWrapper a Mooncake primitive. # Instead of differentiating through the FunctionWrapper dispatch machinery @@ -16,9 +16,16 @@ function Mooncake.rrule!!( f::CoDual{<:FunctionWrappersWrapper}, args::Vararg{CoDual}, ) f_orig = unwrap(f.x) - f_orig_codual = CoDual(f_orig, zero_tangent(f_orig)) - y, pb = Mooncake.rrule!!(f_orig_codual, args...) - fww_pb(dy) = (NoRData(), Mooncake.Base.tail(pb(dy))...) + # Build a derived rule for calling the unwrapped function with these arg types. + # We can't use rrule!! directly since the unwrapped function (e.g. SciMLBase.Void) + # is generally not a Mooncake primitive — it needs a derived (compiled) rule. + sig = Tuple{typeof(f_orig), map(Core.Typeof ∘ Mooncake.primal, args)...} + rule = Mooncake.build_rrule(sig) + # Use fdata to get the correct tangent component for the CoDual — zero_tangent + # returns NoTangent for singleton callables but derived rules expect NoFData. + f_orig_codual = CoDual(f_orig, fdata(zero_tangent(f_orig))) + y, pb = rule(f_orig_codual, args...) + fww_pb(dy) = (NoRData(), Base.tail(pb(dy))...) return y, fww_pb end diff --git a/test/mooncake_tests.jl b/test/mooncake_tests.jl index eef7068..8ca86fd 100644 --- a/test/mooncake_tests.jl +++ b/test/mooncake_tests.jl @@ -45,6 +45,47 @@ end @test dx ≈ [6.0, 8.0] end +@testset "Mooncake with wrapped callable struct" begin + # SciML wraps functions in Void{F} or similar callable structs before + # putting them in FunctionWrappersWrapper. The unwrapped function is + # then a non-primitive callable struct, not a plain function. + struct VoidWrapper{F} + f::F + end + function (v::VoidWrapper)(args...) + v.f(args...) + return nothing + end + + function f!(du, u, p) + du[1] = p[1] * u[1] + du[2] = p[2] * u[2] + return nothing + end + + wrapped = VoidWrapper(f!) + fww = FunctionWrappersWrapper( + wrapped, + (Tuple{Vector{Float64}, Vector{Float64}, Vector{Float64}},), + (Nothing,), + ) + + function loss(p) + u = [3.0, 4.0] + du = similar(u) + fww(du, u, p) + return sum(abs2, du) + end + + rule = Mooncake.build_rrule(loss, [2.0, 3.0]) + val, (_, dp) = Mooncake.value_and_gradient!!(rule, loss, [2.0, 3.0]) + # du = [2*3, 3*4] = [6, 12], loss = 36 + 144 = 180 + @test val ≈ 180.0 + # ∂loss/∂p1 = 2*du[1]*u[1] = 2*6*3 = 36 + # ∂loss/∂p2 = 2*du[2]*u[2] = 2*12*4 = 96 + @test dp ≈ [36.0, 96.0] +end + @testset "Mooncake in-place function" begin # In-place functions are common in SciML (f!(du, u, p, t)) function f!(du, u, p) From a4ecde4a31708ef7979fe641f6d4e6ef73402483 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Fri, 17 Apr 2026 09:45:45 -0400 Subject: [PATCH 25/55] Generalize Enzyme forward rules to arbitrary batch width The three `forward` rule overloads were hardcoded to `FwdConfig{..., 1, ...}` (width=1). When Enzyme uses batch forward mode (e.g., width=8 for a Jacobian of an 8-output function), no rule matched and Enzyme threw: MethodError: no method matching forward( ::FwdConfigWidth{8, false, false, true, false}, ::Const{FunctionWrappersWrapper{...}}, ...) Replace the hardcoded `1` with a type parameter `W`. For W > 1, use `BatchDuplicated{T, W}` annotations and return `NTuple{W, T}` or `BatchDuplicated(primal, shadows)` as required by Enzyme's batch API. The primal-only case is width-independent. Reverse mode rules are unaffected (width only applies to forward mode). Fixes OrdinaryDiffEq.jl v7 CI failures in OrdinaryDiffEqRosenbrock and OrdinaryDiffEqBDF Enzyme-based convergence tests. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.6 (1M context) --- ext/FunctionWrappersWrappersEnzymeExt.jl | 37 +++++++++++++++--------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/ext/FunctionWrappersWrappersEnzymeExt.jl b/ext/FunctionWrappersWrappersEnzymeExt.jl index 8b35f36..bbc3a47 100644 --- a/ext/FunctionWrappersWrappersEnzymeExt.jl +++ b/ext/FunctionWrappersWrappersEnzymeExt.jl @@ -6,43 +6,54 @@ using EnzymeCore using EnzymeCore.EnzymeRules # ============================================================================= -# Forward mode rules +# Forward mode rules — generalized to arbitrary batch width W # ============================================================================= # Shadow only (Forward mode, no primal) function EnzymeRules.forward( - ::EnzymeRules.FwdConfig{false, true, 1, RuntimeActivity, StrongZero}, + ::EnzymeRules.FwdConfig{false, true, W, RuntimeActivity, StrongZero}, func::EnzymeCore.Const{<:FunctionWrappersWrapper}, RT::Type{<:EnzymeCore.Annotation{T}}, args::Vararg{EnzymeCore.Annotation, N} -) where {T, N, RuntimeActivity, StrongZero} +) where {T, W, N, RuntimeActivity, StrongZero} f_orig = unwrap(func.val) - shadow_result = Enzyme.autodiff(Forward, Const(f_orig), Duplicated{T}, args...) - return shadow_result[1]::T + if W == 1 + shadow_result = Enzyme.autodiff(Forward, Const(f_orig), Duplicated{T}, args...) + return shadow_result[1]::T + else + shadow_result = Enzyme.autodiff(Forward, Const(f_orig), BatchDuplicated{T, W}, args...) + return shadow_result[1]::NTuple{W, T} + end end # Both primal and shadow (ForwardWithPrimal mode) function EnzymeRules.forward( - ::EnzymeRules.FwdConfig{true, true, 1, RuntimeActivity, StrongZero}, + ::EnzymeRules.FwdConfig{true, true, W, RuntimeActivity, StrongZero}, func::EnzymeCore.Const{<:FunctionWrappersWrapper}, RT::Type{<:EnzymeCore.Annotation{T}}, args::Vararg{EnzymeCore.Annotation, N} -) where {T, N, RuntimeActivity, StrongZero} +) where {T, W, N, RuntimeActivity, StrongZero} f_orig = unwrap(func.val) pargs = ntuple(i -> args[i].val, Val(N)) primal = f_orig(pargs...)::T - shadow_result = Enzyme.autodiff(Forward, Const(f_orig), Duplicated{T}, args...) - shadow = shadow_result[1]::T - return Duplicated(primal, shadow) + if W == 1 + shadow_result = Enzyme.autodiff(Forward, Const(f_orig), Duplicated{T}, args...) + shadow = shadow_result[1]::T + return Duplicated(primal, shadow) + else + shadow_result = Enzyme.autodiff(Forward, Const(f_orig), BatchDuplicated{T, W}, args...) + shadows = shadow_result[1]::NTuple{W, T} + return BatchDuplicated(primal, shadows) + end end -# Primal only (Const return type) +# Primal only (Const return type) — width-independent function EnzymeRules.forward( - ::EnzymeRules.FwdConfig{true, false, 1, RuntimeActivity, StrongZero}, + ::EnzymeRules.FwdConfig{true, false, W, RuntimeActivity, StrongZero}, func::EnzymeCore.Const{<:FunctionWrappersWrapper}, RT::Type{<:EnzymeCore.Annotation}, args::Vararg{EnzymeCore.Annotation, N} -) where {N, RuntimeActivity, StrongZero} +) where {W, N, RuntimeActivity, StrongZero} f_orig = unwrap(func.val) pargs = ntuple(i -> args[i].val, Val(N)) return f_orig(pargs...) From 4d6349c59f4dd3a1f3c2e806e9a742e9da255394 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Fri, 17 Apr 2026 11:05:03 -0400 Subject: [PATCH 26/55] Add test for Enzyme batch forward mode (width > 1) via BatchDuplicated Verifies that FunctionWrappersWrapper correctly handles batch forward differentiation with width > 1, checking both Forward and ForwardWithPrimal modes return correct per-tangent shadows. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Sonnet 4.6 --- test/enzyme_tests.jl | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/enzyme_tests.jl b/test/enzyme_tests.jl index 4c3cb3a..88d437b 100644 --- a/test/enzyme_tests.jl +++ b/test/enzyme_tests.jl @@ -50,3 +50,27 @@ end result = Enzyme.autodiff(Reverse, Const(fww_sin), Active, Active(1.0)) @test result[1][1] ≈ cos(1.0) end + +@testset "Enzyme batch forward mode (width > 1)" begin + f(x) = x^2 + fww = FunctionWrappersWrapper(f, (Tuple{Float64},), (Float64,)) + + # Batch width = 2: compute derivatives for two tangent directions simultaneously. + # f(x) = x^2 → f'(x) = 2x; at x=3.0 with tangents (1.0, 2.0) → shadows (6.0, 12.0) + result = Enzyme.autodiff( + Forward, Const(fww), BatchDuplicated, + BatchDuplicated(3.0, (1.0, 2.0)) + ) + shadows = result[1] + @test shadows[1] ≈ 6.0 # f'(3) * 1.0 + @test shadows[2] ≈ 12.0 # f'(3) * 2.0 + + # ForwardWithPrimal, batch width = 2 + result_wp = Enzyme.autodiff( + ForwardWithPrimal, Const(fww), BatchDuplicated, + BatchDuplicated(3.0, (1.0, 2.0)) + ) + @test result_wp[1][1] ≈ 6.0 # shadow 1 + @test result_wp[1][2] ≈ 12.0 # shadow 2 + @test result_wp[2] ≈ 9.0 # primal f(3) = 9 +end From 131afa59a29354ceed99103eb7303ffc8fbcee96 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Fri, 17 Apr 2026 15:16:25 +0000 Subject: [PATCH 27/55] Bump version from 1.4.0 to 1.5.0 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 4a2758e..6b9868a 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FunctionWrappersWrappers" uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" authors = ["Chris Elrod and contributors"] -version = "1.4.0" +version = "1.5.0" [deps] FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" From f1981eaf8ea310864a705db3cec4cf4c3fe53044 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:54:41 +0000 Subject: [PATCH 28/55] Bump julia-actions/setup-julia from 2 to 3 Bumps [julia-actions/setup-julia](https://github.com/julia-actions/setup-julia) from 2 to 3. - [Release notes](https://github.com/julia-actions/setup-julia/releases) - [Commits](https://github.com/julia-actions/setup-julia/compare/v2...v3) --- updated-dependencies: - dependency-name: julia-actions/setup-julia dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/Tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 0c81c85..7a5a521 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -47,7 +47,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: julia-actions/setup-julia@v2 + - uses: julia-actions/setup-julia@v3 with: version: '1' - uses: julia-actions/julia-buildpkg@v1 From 5962fc3b0484e600edd2616e03f68e58ceaeb8ad Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Tue, 21 Apr 2026 08:19:38 -0400 Subject: [PATCH 29/55] =?UTF-8?q?Add=20Enzyme=20forward=20rule=20for=20Fwd?= =?UTF-8?q?Config{false,=20false,=20=E2=80=A6}=20(no-primal=20+=20no-shado?= =?UTF-8?q?w)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the extension provided forward rules for: - FwdConfig{false, true, W, …} -- shadow only - FwdConfig{true, true, W, …} -- primal + shadow - FwdConfig{true, false, W, …} -- primal only The combination FwdConfig{false, false, W, …} (no primal, no shadow) had no matching rule, so Enzyme fell through to its default differentiation path. When the FunctionWrappersWrapper held only plain-Float64 signatures (no Dual/Duplicated variants), that path tried to dispatch `forward(::FwdConfigWidth{1, false, false, …}, ::Const{<:FunctionWrappersWrapper}, ::Type{Const{Nothing}}, …)` and hit: MethodError: no method matching forward( ::FwdConfigWidth{1, false, false, false, false}, ::Const{<:FunctionWrappersWrapper}, ::Type{Const{Nothing}}, …) which broke OrdinaryDiffEq.jl's v7 `Downstream` CI (test/downstream/ time_derivative_test.jl) when solving with `AutoEnzyme(mode = Enzyme.Forward, function_annotation = Const)`. Add a 4th rule that runs the unwrapped primal for its side effects and returns nothing, matching what Enzyme's default path would have done if it could have dispatched. Add a regression test that invokes `EnzymeRules.forward` directly with the failing config to cover the code path without depending on a specific user-facing autodiff front end. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4 (1M context) --- ext/FunctionWrappersWrappersEnzymeExt.jl | 19 +++++++++++++ test/enzyme_tests.jl | 35 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/ext/FunctionWrappersWrappersEnzymeExt.jl b/ext/FunctionWrappersWrappersEnzymeExt.jl index bbc3a47..e22a012 100644 --- a/ext/FunctionWrappersWrappersEnzymeExt.jl +++ b/ext/FunctionWrappersWrappersEnzymeExt.jl @@ -59,6 +59,25 @@ function EnzymeRules.forward( return f_orig(pargs...) end +# Neither primal nor shadow requested — Enzyme asks for this combo with Const +# return-type annotations where the caller only needs the side effects of the +# primal invocation (e.g. mutating an IIP RHS in SciML's solver path). No rule +# previously matched this case, so dispatch fell through to Enzyme's default +# path which tried to differentiate through the raw FunctionWrappersWrapper +# and failed with `MethodError: no method matching forward(…)` when the wrapper +# only held plain-Float64 signatures. Just run the primal and return nothing. +function EnzymeRules.forward( + ::EnzymeRules.FwdConfig{false, false, W, RuntimeActivity, StrongZero}, + func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + RT::Type{<:EnzymeCore.Annotation}, + args::Vararg{EnzymeCore.Annotation, N} +) where {W, N, RuntimeActivity, StrongZero} + f_orig = unwrap(func.val) + pargs = ntuple(i -> args[i].val, Val(N)) + f_orig(pargs...) + return nothing +end + # ============================================================================= # Reverse mode rules # ============================================================================= diff --git a/test/enzyme_tests.jl b/test/enzyme_tests.jl index 88d437b..cbe7ba1 100644 --- a/test/enzyme_tests.jl +++ b/test/enzyme_tests.jl @@ -1,5 +1,6 @@ using FunctionWrappersWrappers using Enzyme +using EnzymeCore using Test @testset "Enzyme forward mode" begin @@ -74,3 +75,37 @@ end @test result_wp[1][2] ≈ 12.0 # shadow 2 @test result_wp[2] ≈ 9.0 # primal f(3) = 9 end + +@testset "Enzyme forward mode, neither primal nor shadow requested" begin + # Covers EnzymeRules.FwdConfig{false, false, W, ...}: caller wants only the + # side-effects of the primal invocation, no return value and no derivative. + # Reproduces the SciML/OrdinaryDiffEq.jl v7 Downstream regression where + # Enzyme dispatched on this config combination with a FWW wrapping an IIP + # RHS and found no matching rule, throwing + # MethodError: no method matching forward( + # ::FwdConfigWidth{1, false, false, false, false}, + # ::Const{<:FunctionWrappersWrapper}, ::Type{Const{Nothing}}, …) + f!(du, u) = (du[1] = -u[1]^2; nothing) + fww = FunctionWrappersWrapper( + f!, (Tuple{Vector{Float64}, Vector{Float64}},), (Nothing,) + ) + + du = [0.0] + u = [3.0] + du_shadow = [0.0] + u_shadow = [1.0] + + # Call forward directly with {false, false}: Enzyme's public-facing + # autodiff front-end doesn't normally expose this config, so invoke the + # rule by hand. + config = EnzymeCore.EnzymeRules.FwdConfig{false, false, 1, false, false}() + ret = EnzymeCore.EnzymeRules.forward( + config, Const(fww), EnzymeCore.Const{Nothing}, + Duplicated(du, du_shadow), Duplicated(u, u_shadow) + ) + @test ret === nothing + # primal side-effect did happen: f!(du, u) sets du[1] = -u[1]^2 = -9 + @test du[1] ≈ -9.0 + # shadow buffer was not touched by this no-diff path + @test du_shadow[1] == 0.0 +end From 3faf242104b0914765101107b902bd46ec37e031 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Tue, 21 Apr 2026 09:30:42 -0400 Subject: [PATCH 30/55] Cover Const / Duplicated / BatchDuplicated return types in reverse rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the reverse-mode coverage of the Enzyme extension to match the forward side (no-primal + no-shadow case closed in the previous commit). New augmented_primal methods: * RT <: Const — for non-differentiated returns (e.g. IIP functions returning Nothing; the mirror of the forward {false, false} case). Runs the primal for its side effects; no shadow or tape. * RT <: Duplicated{T} — returns the primal and zero-initializes the return shadow, matching the standard Duplicated semantics. * RT <: BatchDuplicated{T, W} — same with an NTuple of W zero-shadows. New reverse methods: * dret::Const with uniform Active args — returns nothing per arg. * dret::Const with mixed Annotation args — returns nothing per arg. All new paths have direct unit tests that construct concrete RevConfig instances and call augmented_primal / reverse, verifying: - primal ran for its side effects (Const-return counter incremented) - AugmentedReturn fields have the expected shapes (primal, shadow, tape) - reverse returns the correct number of nothings for Const dret Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4 (1M context) --- ext/FunctionWrappersWrappersEnzymeExt.jl | 72 ++++++++++++++++++++++++ test/enzyme_tests.jl | 63 +++++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/ext/FunctionWrappersWrappersEnzymeExt.jl b/ext/FunctionWrappersWrappersEnzymeExt.jl index e22a012..44975e5 100644 --- a/ext/FunctionWrappersWrappersEnzymeExt.jl +++ b/ext/FunctionWrappersWrappersEnzymeExt.jl @@ -99,6 +99,56 @@ function EnzymeRules.augmented_primal( end end +# Const return (e.g. IIP functions returning Nothing, or any non-differentiated +# return). Just run the primal for its side effects; no tape is needed because +# the reverse pass has nothing to propagate back from the return. +function EnzymeRules.augmented_primal( + config::EnzymeRules.RevConfig, + func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + RT::Type{<:EnzymeCore.Const}, + args::Vararg{EnzymeCore.Annotation, N} +) where {N} + f_orig = unwrap(func.val) + pargs = ntuple(i -> args[i].val, Val(N)) + f_orig(pargs...) + return EnzymeRules.AugmentedReturn(nothing, nothing, nothing) +end + +# Duplicated / BatchDuplicated return: record the primal so that reverse has +# it available when propagating dret through the arguments. +function EnzymeRules.augmented_primal( + config::EnzymeRules.RevConfig, + func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + RT::Type{<:EnzymeCore.Duplicated{T}}, + args::Vararg{EnzymeCore.Annotation, N} +) where {T, N} + f_orig = unwrap(func.val) + pargs = ntuple(i -> args[i].val, Val(N)) + primal = f_orig(pargs...)::T + if EnzymeRules.needs_primal(config) + return EnzymeRules.AugmentedReturn(primal, zero(primal), nothing) + else + return EnzymeRules.AugmentedReturn(nothing, zero(primal), nothing) + end +end + +function EnzymeRules.augmented_primal( + config::EnzymeRules.RevConfig, + func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + RT::Type{<:EnzymeCore.BatchDuplicated{T, W}}, + args::Vararg{EnzymeCore.Annotation, N} +) where {T, W, N} + f_orig = unwrap(func.val) + pargs = ntuple(i -> args[i].val, Val(N)) + primal = f_orig(pargs...)::T + shadows = ntuple(_ -> zero(primal), Val(W)) + if EnzymeRules.needs_primal(config) + return EnzymeRules.AugmentedReturn(primal, shadows, nothing) + else + return EnzymeRules.AugmentedReturn(nothing, shadows, nothing) + end +end + # Varargs reverse: compute each partial via forward-mode AD on the unwrapped # function, then scale by dret. This avoids type-inference issues that arise # from calling autodiff(Reverse, Const{Any}(...), ...). @@ -158,4 +208,26 @@ function EnzymeRules.reverse( end end +# Const return (no derivative to propagate from the return) — uniform Active args. +function EnzymeRules.reverse( + config::EnzymeRules.RevConfig, + func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + dret::EnzymeCore.Const, + tape, + args::Vararg{EnzymeCore.Active, N} +) where {N} + return ntuple(_ -> nothing, Val(N)) +end + +# Const return — mixed Active/Const args. +function EnzymeRules.reverse( + config::EnzymeRules.RevConfig, + func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + dret::EnzymeCore.Const, + tape, + args::Vararg{EnzymeCore.Annotation, N} +) where {N} + return ntuple(_ -> nothing, Val(N)) +end + end diff --git a/test/enzyme_tests.jl b/test/enzyme_tests.jl index cbe7ba1..e9ecc4d 100644 --- a/test/enzyme_tests.jl +++ b/test/enzyme_tests.jl @@ -109,3 +109,66 @@ end # shadow buffer was not touched by this no-diff path @test du_shadow[1] == 0.0 end + +@testset "Enzyme reverse mode, Const return — augmented_primal runs primal" begin + # Mirrors the forward {false, false} case on the reverse side. Augmented + # primal runs the wrapped function for its side effects and returns + # AugmentedReturn(nothing, nothing, nothing). Reverse returns `nothing` + # per arg since there is no return derivative to propagate. + counter = Ref(0) + g(x, y) = (counter[] += 1; x + y) # returns Float64 (ignored via Const RT) + fww = FunctionWrappersWrapper(g, (Tuple{Float64, Float64},), (Float64,)) + + # Construct a concrete RevConfig. Fields: + # (NeedsPrimal, NeedsShadow, Width, Overwritten, RuntimeActivity, StrongZero) + rconfig = EnzymeRules.RevConfig{false, false, 1, (false, false), false, false}() + + counter[] = 0 + aug = EnzymeRules.augmented_primal( + rconfig, Const(fww), EnzymeCore.Const{Float64}, + Active(3.0), Active(4.0) + ) + @test counter[] == 1 # primal ran exactly once + @test aug.primal === nothing # NeedsPrimal == false + @test aug.shadow === nothing + @test aug.tape === nothing + + # Reverse step — dret is Const, no grads to accumulate. + grads = EnzymeRules.reverse( + rconfig, Const(fww), EnzymeCore.Const{Float64}(0.0), + aug.tape, Active(3.0), Active(4.0) + ) + @test grads == (nothing, nothing) +end + +@testset "Enzyme reverse mode, Duplicated return — augmented_primal initializes shadow" begin + # Covers augmented_primal with RT <: Duplicated{T}. + f(x) = x^2 + fww = FunctionWrappersWrapper(f, (Tuple{Float64},), (Float64,)) + rconfig = EnzymeRules.RevConfig{true, true, 1, (false,), false, false}() + + aug = EnzymeRules.augmented_primal( + rconfig, Const(fww), EnzymeCore.Duplicated{Float64}, + Active(3.0) + ) + @test aug.primal ≈ 9.0 # f(3) = 9 + @test aug.shadow ≈ 0.0 # zero-initialized shadow + @test aug.tape === nothing +end + +@testset "Enzyme reverse mode, BatchDuplicated return — augmented_primal initializes shadows" begin + # Covers augmented_primal with RT <: BatchDuplicated{T, W}. + f(x) = x^2 + fww = FunctionWrappersWrapper(f, (Tuple{Float64},), (Float64,)) + rconfig = EnzymeRules.RevConfig{true, true, 2, (false,), false, false}() + + aug = EnzymeRules.augmented_primal( + rconfig, Const(fww), EnzymeCore.BatchDuplicated{Float64, 2}, + Active(3.0) + ) + @test aug.primal ≈ 9.0 + @test aug.shadow isa NTuple{2, Float64} + @test aug.shadow == (0.0, 0.0) + @test aug.tape === nothing +end + From f28f3c06cbd0d2cb6241b5583b4cf92e9a19d880 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Tue, 21 Apr 2026 13:38:14 +0000 Subject: [PATCH 31/55] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 6b9868a..d5f7543 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FunctionWrappersWrappers" uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" authors = ["Chris Elrod and contributors"] -version = "1.5.0" +version = "1.6.0" [deps] FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" From 9ea713225a97484dde7821f9522fd15802a80e96 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Tue, 21 Apr 2026 11:24:01 -0400 Subject: [PATCH 32/55] FwdConfig{false, false, ...} rule: delegate to Enzyme.autodiff on unwrapped f MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #43. The original revision ran `f_orig(pargs...)` by hand to cover the IIP-void-return Enzyme-forward path that was throwing `MethodError: no method matching forward(::FwdConfigWidth{1, false, false, false, false}, …)`. That version fixed the dispatch but left the `Duplicated` arg shadow buffers untouched (the inner call only exercised the primal-valued function wrapper), so downstream callers that rely on shadow propagation through args got a trivially zero Jacobian. Observed concretely in SciML/OrdinaryDiffEq.jl v7 `Downstream` `time_derivative_test.jl` with `AutoEnzyme(mode = Enzyme.Forward, function_annotation = Const)`: Rosenbrock23 error: 5.55e-17 < 1e-10 PASS Rodas4 error: 1.11e-6 > 1e-10 FAIL Rodas5 error: 0.022 > 1e-10 FAIL Veldd4 error: 5.56e-7 > 1e-10 FAIL After delegating to `Enzyme.autodiff(Forward, Const(f_orig), Const, args...)`, all four pass at machine epsilon — matching master. Update the regression test to assert that the Duplicated shadow buffer is correctly updated (`∂du[1]/∂u[1] * u_shadow[1] = -2*u[1]*1 = -6`) rather than left at zero. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4 (1M context) --- ext/FunctionWrappersWrappersEnzymeExt.jl | 20 ++++++++------- test/enzyme_tests.jl | 31 ++++++++++++------------ 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/ext/FunctionWrappersWrappersEnzymeExt.jl b/ext/FunctionWrappersWrappersEnzymeExt.jl index 44975e5..056a3e6 100644 --- a/ext/FunctionWrappersWrappersEnzymeExt.jl +++ b/ext/FunctionWrappersWrappersEnzymeExt.jl @@ -59,13 +59,16 @@ function EnzymeRules.forward( return f_orig(pargs...) end -# Neither primal nor shadow requested — Enzyme asks for this combo with Const -# return-type annotations where the caller only needs the side effects of the -# primal invocation (e.g. mutating an IIP RHS in SciML's solver path). No rule -# previously matched this case, so dispatch fell through to Enzyme's default -# path which tried to differentiate through the raw FunctionWrappersWrapper -# and failed with `MethodError: no method matching forward(…)` when the wrapper -# only held plain-Float64 signatures. Just run the primal and return nothing. +# Neither primal nor shadow requested in the RETURN. Enzyme dispatches on +# this combo for IIP functions (Const return type) where the caller still +# needs primal and shadow propagation through `Duplicated` args — e.g. SciML +# solvers calling an IIP RHS via `AutoEnzyme(…, function_annotation = Const)`. +# The previous revision ran `f_orig(pargs...)` by hand; that mutated the +# primal IIP buffer but left `Duplicated` shadow buffers untouched, giving +# trivial Jacobians and blowing up Rodas4/5/Veldd4 error tolerances 4–9 +# orders of magnitude in OrdinaryDiffEq.jl v7. Delegate to `Enzyme.autodiff` +# on the unwrapped function with a Const return annotation so the Duplicated +# arg shadows are propagated correctly and no return is produced. function EnzymeRules.forward( ::EnzymeRules.FwdConfig{false, false, W, RuntimeActivity, StrongZero}, func::EnzymeCore.Const{<:FunctionWrappersWrapper}, @@ -73,8 +76,7 @@ function EnzymeRules.forward( args::Vararg{EnzymeCore.Annotation, N} ) where {W, N, RuntimeActivity, StrongZero} f_orig = unwrap(func.val) - pargs = ntuple(i -> args[i].val, Val(N)) - f_orig(pargs...) + Enzyme.autodiff(Forward, Const(f_orig), Const, args...) return nothing end diff --git a/test/enzyme_tests.jl b/test/enzyme_tests.jl index e9ecc4d..22bbbf3 100644 --- a/test/enzyme_tests.jl +++ b/test/enzyme_tests.jl @@ -76,15 +76,17 @@ end @test result_wp[2] ≈ 9.0 # primal f(3) = 9 end -@testset "Enzyme forward mode, neither primal nor shadow requested" begin - # Covers EnzymeRules.FwdConfig{false, false, W, ...}: caller wants only the - # side-effects of the primal invocation, no return value and no derivative. - # Reproduces the SciML/OrdinaryDiffEq.jl v7 Downstream regression where - # Enzyme dispatched on this config combination with a FWW wrapping an IIP - # RHS and found no matching rule, throwing - # MethodError: no method matching forward( - # ::FwdConfigWidth{1, false, false, false, false}, - # ::Const{<:FunctionWrappersWrapper}, ::Type{Const{Nothing}}, …) +@testset "Enzyme forward mode, Const return (IIP, no return-shadow)" begin + # Covers EnzymeRules.FwdConfig{false, false, W, ...} — Enzyme dispatches on + # this combo for IIP functions with a Const return type where the caller + # needs primal + shadow propagation via Duplicated args only (no return + # value to shadow). Reproduces the SciML/OrdinaryDiffEq.jl v7 Downstream + # regression where this call previously produced: + # - without any rule: MethodError: no method matching forward(…) + # - with a primal-only rule: trivial (zero) arg shadows, wrong Jacobians + # (Rodas4/5/Veldd4 errors 4–9 orders of magnitude above tolerance). + # The rule must delegate to `Enzyme.autodiff` on the unwrapped function + # so Duplicated arg shadows propagate correctly. f!(du, u) = (du[1] = -u[1]^2; nothing) fww = FunctionWrappersWrapper( f!, (Tuple{Vector{Float64}, Vector{Float64}},), (Nothing,) @@ -93,21 +95,18 @@ end du = [0.0] u = [3.0] du_shadow = [0.0] - u_shadow = [1.0] + u_shadow = [1.0] # seed: ∂/∂u[1] = 1 - # Call forward directly with {false, false}: Enzyme's public-facing - # autodiff front-end doesn't normally expose this config, so invoke the - # rule by hand. config = EnzymeCore.EnzymeRules.FwdConfig{false, false, 1, false, false}() ret = EnzymeCore.EnzymeRules.forward( config, Const(fww), EnzymeCore.Const{Nothing}, Duplicated(du, du_shadow), Duplicated(u, u_shadow) ) @test ret === nothing - # primal side-effect did happen: f!(du, u) sets du[1] = -u[1]^2 = -9 + # Primal side-effect: du[1] = -u[1]^2 = -9 @test du[1] ≈ -9.0 - # shadow buffer was not touched by this no-diff path - @test du_shadow[1] == 0.0 + # Shadow propagation: ∂du[1]/∂u[1] * u_shadow[1] = -2*u[1]*1 = -6 + @test du_shadow[1] ≈ -6.0 end @testset "Enzyme reverse mode, Const return — augmented_primal runs primal" begin From 137a3f21ddedad91473cc92ca5658bd470e02390 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Tue, 21 Apr 2026 11:53:26 -0400 Subject: [PATCH 33/55] Fix reverse rules: delegate via Enzyme.autodiff, return zeros for Active args MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reverse rules from #43 had two bugs exposed by new end-to-end tests: 1. Const-dret reverse returned `nothing` per arg, but Enzyme's rule protocol requires concrete scalar gradients for Active args (not nothing). Fixed to return `zero(T)` for Active args and `nothing` for Duplicated/Const args. 2. IIP reverse with Duplicated args (SciML pattern) returned nothing and never propagated gradients into the Duplicated shadow buffers. Fixed by delegating to `Enzyme.autodiff(Reverse, Const(f_orig), Const, args...)` when Duplicated args are present, so Enzyme accumulates the transposed derivative into the shadow buffers. 3. Enzyme passes `Type{<:Const}` (not an instance) for the dret slot in Const-return reverse rules. Updated dispatch signatures from `dret::EnzymeCore.Const` to `dret::Type{<:EnzymeCore.Const}`. New end-to-end reverse-mode tests that assert derivative correctness: - Const return + Active args: gradients are (0.0, 0.0) - IIP f!(du, u) with Duplicated args: u_shadow accumulates ∂du/∂u - Multi-component IIP cross-coupled Jacobian transpose - ReverseWithPrimal IIP variant Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4 (1M context) --- ext/FunctionWrappersWrappersEnzymeExt.jl | 39 +++++--- test/enzyme_tests.jl | 112 ++++++++++++++++++++++- 2 files changed, 134 insertions(+), 17 deletions(-) diff --git a/ext/FunctionWrappersWrappersEnzymeExt.jl b/ext/FunctionWrappersWrappersEnzymeExt.jl index 056a3e6..85a76df 100644 --- a/ext/FunctionWrappersWrappersEnzymeExt.jl +++ b/ext/FunctionWrappersWrappersEnzymeExt.jl @@ -210,26 +210,37 @@ function EnzymeRules.reverse( end end -# Const return (no derivative to propagate from the return) — uniform Active args. +# Const return — Enzyme passes the RT as a `Type{<:Const}` to `reverse`, not +# as an instance. Delegate the reverse pass to +# `Enzyme.autodiff(Reverse, Const(f_orig), Const, args...)` so gradients +# accumulate into any `Duplicated` arg shadow buffers (the SciML IIP +# pattern). Simply returning `nothing` left Duplicated shadows at zero. +# +# Per Enzyme's rule return-type protocol, `Active` args require a concrete +# scalar gradient (not `nothing`). Under a `Const` return there is no +# gradient source, so Active arg gradients are zero. `Duplicated` / +# `BatchDuplicated` args return `nothing` because their gradients are +# accumulated in-place by the `Enzyme.autodiff(Reverse, …)` call above. function EnzymeRules.reverse( config::EnzymeRules.RevConfig, func::EnzymeCore.Const{<:FunctionWrappersWrapper}, - dret::EnzymeCore.Const, - tape, - args::Vararg{EnzymeCore.Active, N} -) where {N} - return ntuple(_ -> nothing, Val(N)) -end - -# Const return — mixed Active/Const args. -function EnzymeRules.reverse( - config::EnzymeRules.RevConfig, - func::EnzymeCore.Const{<:FunctionWrappersWrapper}, - dret::EnzymeCore.Const, + dret::Type{<:EnzymeCore.Const}, tape, args::Vararg{EnzymeCore.Annotation, N} ) where {N} - return ntuple(_ -> nothing, Val(N)) + f_orig = unwrap(func.val) + # Only worth invoking Enzyme.autodiff when at least one arg is + # Duplicated/BatchDuplicated — otherwise there's nothing to accumulate. + if any(a -> a isa EnzymeCore.Duplicated || a isa EnzymeCore.BatchDuplicated, args) + Enzyme.autodiff(Reverse, Const(f_orig), Const, args...) + end + return ntuple(Val(N)) do i + if args[i] isa EnzymeCore.Active + zero(eltype(typeof(args[i]))) + else + nothing + end + end end end diff --git a/test/enzyme_tests.jl b/test/enzyme_tests.jl index 22bbbf3..c5713dc 100644 --- a/test/enzyme_tests.jl +++ b/test/enzyme_tests.jl @@ -132,12 +132,14 @@ end @test aug.shadow === nothing @test aug.tape === nothing - # Reverse step — dret is Const, no grads to accumulate. + # Reverse step — dret is Const (passed as TYPE not instance in reverse + # rules). Enzyme's rule protocol requires concrete gradients for Active + # args; under a Const return they're zero (no gradient source). grads = EnzymeRules.reverse( - rconfig, Const(fww), EnzymeCore.Const{Float64}(0.0), + rconfig, Const(fww), EnzymeCore.Const{Float64}, aug.tape, Active(3.0), Active(4.0) ) - @test grads == (nothing, nothing) + @test grads == (0.0, 0.0) end @testset "Enzyme reverse mode, Duplicated return — augmented_primal initializes shadow" begin @@ -171,3 +173,107 @@ end @test aug.tape === nothing end +# ============================================================================= +# End-to-end reverse-mode derivative tests — exercise Enzyme.autodiff(Reverse, +# …) through the FWW and assert the resulting gradients are numerically correct. +# The prior reverse-mode testsets only checked dispatch / shape of +# AugmentedReturn; they did NOT verify the gradients are right. +# ============================================================================= + +@testset "Enzyme Reverse: Const return, Active args — no-flow gradients" begin + # For a function whose return is annotated Const in Reverse mode, there is + # no gradient source from the return, so Active arg gradients must be 0. + # (Enzyme's rule-return protocol requires concrete gradients for Active + # args — `nothing` is not allowed — so the rule returns zeros.) + g(x, y) = x * y + x^2 + fww = FunctionWrappersWrapper(g, (Tuple{Float64, Float64},), (Float64,)) + + # Const return (instead of Active) → no gradient flows back + result = Enzyme.autodiff(Reverse, Const(fww), Const, Active(3.0), Active(4.0)) + @test result[1] === (0.0, 0.0) +end + +@testset "Enzyme Reverse: IIP with Duplicated args, Const return" begin + # SciML's standard pattern: IIP RHS `f!(du, u)` with Const return, both du + # and u are Duplicated. Reverse mode should accumulate + # u_shadow[i] += du_shadow[j] * ∂(du[j])/∂(u[i]) + # into u_shadow. For f!(du, u) = (du[1] = u[1]^2; nothing) with + # du_shadow = [1.0] (incoming adjoint), + # u[1] = 3.0, + # ∂du[1]/∂u[1] = 2*u[1] = 6, + # the expected result is u_shadow[1] = 6.0 after the call. + f!(du, u) = (du[1] = u[1]^2; nothing) + fww = FunctionWrappersWrapper( + f!, (Tuple{Vector{Float64}, Vector{Float64}},), (Nothing,) + ) + + du = [0.0] + u = [3.0] + du_shadow = [1.0] + u_shadow = [0.0] + + Enzyme.autodiff( + Reverse, Const(fww), Const, + Duplicated(du, du_shadow), Duplicated(u, u_shadow) + ) + @test du[1] ≈ 9.0 # primal effect: du[1] = u[1]^2 = 9 + @test u_shadow[1] ≈ 6.0 # reverse accumulation: 2 * u[1] * du_shadow[1] +end + +@testset "Enzyme Reverse: IIP multi-component IIP with Duplicated args" begin + # Cross-coupled IIP RHS: each output depends on multiple inputs. + # du[1] = u[1] * u[2] + # du[2] = u[1]^2 + u[2]^3 + # Jacobian at u = (x, y): + # J = [ y x ; + # 2x 3y^2 ] + # In reverse mode with du_shadow = [a, b], transpose of J applied to + # du_shadow gives the accumulation into u_shadow: + # u_shadow[1] += a*y + b*2x + # u_shadow[2] += a*x + b*3y^2 + f!(du, u) = (du[1] = u[1]*u[2]; du[2] = u[1]^2 + u[2]^3; nothing) + fww = FunctionWrappersWrapper( + f!, (Tuple{Vector{Float64}, Vector{Float64}},), (Nothing,) + ) + + x, y = 2.0, 5.0 + a, b = 1.0, 0.5 + du = zeros(2) + u = [x, y] + du_shadow = [a, b] + u_shadow = zeros(2) + + Enzyme.autodiff( + Reverse, Const(fww), Const, + Duplicated(du, du_shadow), Duplicated(u, u_shadow) + ) + @test du ≈ [x*y, x^2 + y^3] + @test u_shadow[1] ≈ a*y + b*2*x # 5 + 2 = 7 + @test u_shadow[2] ≈ a*x + b*3*y^2 # 2 + 37.5 = 39.5 +end + +@testset "Enzyme ReverseWithPrimal: IIP with Duplicated args" begin + # Same IIP pattern but with ReverseWithPrimal so we also check the primal + # is available when the rule is asked to include it. + f!(du, u) = (du[1] = u[1]^3; nothing) + fww = FunctionWrappersWrapper( + f!, (Tuple{Vector{Float64}, Vector{Float64}},), (Nothing,) + ) + + du = [0.0] + u = [2.0] + du_shadow = [1.0] + u_shadow = [0.0] + + # Capture the expected gradient BEFORE the call — Enzyme may zero + # `du_shadow` after consuming it during the reverse pass. + expected_u_grad = 3 * u[1]^2 * du_shadow[1] # = 12.0 + + Enzyme.autodiff( + ReverseWithPrimal, Const(fww), Const, + Duplicated(du, du_shadow), Duplicated(u, u_shadow) + ) + @test du[1] ≈ 8.0 + @test u_shadow[1] ≈ expected_u_grad +end + From 11cbd629183e8d8eac4f091d17b00e4973c6a747 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Tue, 21 Apr 2026 17:09:25 +0000 Subject: [PATCH 34/55] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index d5f7543..e0d59a2 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FunctionWrappersWrappers" uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" authors = ["Chris Elrod and contributors"] -version = "1.6.0" +version = "1.7.0" [deps] FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" From 448a6a143b3371b39f23fd92ecae56b22403d0db Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Thu, 23 Apr 2026 03:10:35 -0400 Subject: [PATCH 35/55] Propagate set_runtime_activity/set_strong_zero through FWW Enzyme rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The forward-mode rules hard-coded plain `Forward` in their delegated `Enzyme.autodiff` calls, silently dropping the outer caller's `set_runtime_activity(Forward)` / `set_strong_zero(Forward)` flags. Enzyme's IR-level activity analysis can't see through FunctionWrappersWrapper's cfunction indirection, so the inner call raised `EnzymeRuntimeActivityError` at `broadcast_unalias` / `mightalias` inside `@. du = …` — even when the user had explicitly set runtime activity on the outer `autodiff` call. Reproducer (confirmed): `Rosenbrock23(autodiff = AutoEnzyme(set_runtime_activity(Enzyme.Forward)))` on any time-dependent in-place ODE RHS that DiffEqBase's `AutoSpecialize` wraps with `wrapfun_iip`. See OrdinaryDiffEq.jl PR #3518. Fix: extract `RuntimeActivity` and `StrongZero` from `FwdConfig` / `RevConfig` type parameters and rebuild the `ForwardMode` used in the delegated `Enzyme.autodiff` call with those flags set. The reverse rules' internal forward-mode helpers are updated analogously. Also adds a test exercising the exact DiffEqBase `wrapfun_iip` shape — a 4-arg `(du, u, p, t)` IIP RHS with `@.` broadcast — under `set_runtime_activity(Forward)`, and a `set_strong_zero(Forward)` case. Co-Authored-By: Chris Rackauckas --- ext/FunctionWrappersWrappersEnzymeExt.jl | 70 ++++++++++++++++++--- test/enzyme_tests.jl | 80 ++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 9 deletions(-) diff --git a/ext/FunctionWrappersWrappersEnzymeExt.jl b/ext/FunctionWrappersWrappersEnzymeExt.jl index 85a76df..69d9a7d 100644 --- a/ext/FunctionWrappersWrappersEnzymeExt.jl +++ b/ext/FunctionWrappersWrappersEnzymeExt.jl @@ -5,6 +5,32 @@ using Enzyme using EnzymeCore using EnzymeCore.EnzymeRules +# ============================================================================= +# Helper: build a Forward mode from FwdConfig flags +# ============================================================================= +# The outer caller may invoke `Enzyme.autodiff(set_runtime_activity(Forward), …)` +# or `set_strong_zero(Forward)` or `ForwardWithPrimal`. Those settings flow +# into the `EnzymeRules.FwdConfig{NeedsPrimal, NeedsShadow, Width, +# RuntimeActivity, StrongZero}` type parameters of the rule's first argument. +# Before this fix the rules hard-coded plain `Forward` in their inner +# `Enzyme.autodiff` delegation, which dropped both `RuntimeActivity` and +# `StrongZero` — breaking users who need `set_runtime_activity(Forward)` to +# avoid `EnzymeRuntimeActivityError` inside the wrapped function (the SciML +# `Rosenbrock23(autodiff = AutoEnzyme(set_runtime_activity(Forward)))` path +# on an in-place time-dependent RHS; see OrdinaryDiffEq.jl PR #3518). +# +# `_fwd_mode(needs_primal, RuntimeActivity, StrongZero)` returns the +# `ForwardMode` matching the outer config so the delegated call inherits +# those flags. +@inline function _fwd_mode( + ::Val{NeedsPrimal}, ::Val{RuntimeActivity}, ::Val{StrongZero} +) where {NeedsPrimal, RuntimeActivity, StrongZero} + mode = NeedsPrimal ? ForwardWithPrimal : Forward + RuntimeActivity && (mode = Enzyme.set_runtime_activity(mode)) + StrongZero && (mode = Enzyme.set_strong_zero(mode)) + return mode +end + # ============================================================================= # Forward mode rules — generalized to arbitrary batch width W # ============================================================================= @@ -17,11 +43,12 @@ function EnzymeRules.forward( args::Vararg{EnzymeCore.Annotation, N} ) where {T, W, N, RuntimeActivity, StrongZero} f_orig = unwrap(func.val) + mode = _fwd_mode(Val(false), Val(RuntimeActivity), Val(StrongZero)) if W == 1 - shadow_result = Enzyme.autodiff(Forward, Const(f_orig), Duplicated{T}, args...) + shadow_result = Enzyme.autodiff(mode, Const(f_orig), Duplicated{T}, args...) return shadow_result[1]::T else - shadow_result = Enzyme.autodiff(Forward, Const(f_orig), BatchDuplicated{T, W}, args...) + shadow_result = Enzyme.autodiff(mode, Const(f_orig), BatchDuplicated{T, W}, args...) return shadow_result[1]::NTuple{W, T} end end @@ -36,12 +63,16 @@ function EnzymeRules.forward( f_orig = unwrap(func.val) pargs = ntuple(i -> args[i].val, Val(N)) primal = f_orig(pargs...)::T + # Use plain Forward (not ForwardWithPrimal) here — we already have the + # primal above, and `Duplicated{T}` / `BatchDuplicated{T,W}` as the RT + # annotation asks only for the shadow. + mode = _fwd_mode(Val(false), Val(RuntimeActivity), Val(StrongZero)) if W == 1 - shadow_result = Enzyme.autodiff(Forward, Const(f_orig), Duplicated{T}, args...) + shadow_result = Enzyme.autodiff(mode, Const(f_orig), Duplicated{T}, args...) shadow = shadow_result[1]::T return Duplicated(primal, shadow) else - shadow_result = Enzyme.autodiff(Forward, Const(f_orig), BatchDuplicated{T, W}, args...) + shadow_result = Enzyme.autodiff(mode, Const(f_orig), BatchDuplicated{T, W}, args...) shadows = shadow_result[1]::NTuple{W, T} return BatchDuplicated(primal, shadows) end @@ -69,6 +100,15 @@ end # orders of magnitude in OrdinaryDiffEq.jl v7. Delegate to `Enzyme.autodiff` # on the unwrapped function with a Const return annotation so the Duplicated # arg shadows are propagated correctly and no return is produced. +# +# IMPORTANT: forward the `RuntimeActivity` and `StrongZero` flags from the +# outer config into the delegated `Enzyme.autodiff` call. Prior to this +# fix the rule hard-coded `Forward`, silently dropping +# `set_runtime_activity(Forward)` on the way down into `f_orig`. That +# broke `Rosenbrock23(autodiff = AutoEnzyme(set_runtime_activity(Forward)))` +# on any time-dependent in-place RHS: +# EnzymeRuntimeActivityError at `broadcast_unalias` → `mightalias` +# despite the user explicitly setting runtime activity. function EnzymeRules.forward( ::EnzymeRules.FwdConfig{false, false, W, RuntimeActivity, StrongZero}, func::EnzymeCore.Const{<:FunctionWrappersWrapper}, @@ -76,7 +116,8 @@ function EnzymeRules.forward( args::Vararg{EnzymeCore.Annotation, N} ) where {W, N, RuntimeActivity, StrongZero} f_orig = unwrap(func.val) - Enzyme.autodiff(Forward, Const(f_orig), Const, args...) + mode = _fwd_mode(Val(false), Val(RuntimeActivity), Val(StrongZero)) + Enzyme.autodiff(mode, Const(f_orig), Const, args...) return nothing end @@ -151,11 +192,21 @@ function EnzymeRules.augmented_primal( end end +# Helper: build a Forward mode reflecting a RevConfig's runtime_activity / +# strong_zero flags so the internal forward-mode delegation inside reverse +# rules inherits the user's outer config. +@inline function _fwd_mode_from_rev(config::EnzymeRules.RevConfig) + mode = Forward + EnzymeRules.runtime_activity(config) && (mode = Enzyme.set_runtime_activity(mode)) + EnzymeRules.strong_zero(config) && (mode = Enzyme.set_strong_zero(mode)) + return mode +end + # Varargs reverse: compute each partial via forward-mode AD on the unwrapped # function, then scale by dret. This avoids type-inference issues that arise # from calling autodiff(Reverse, Const{Any}(...), ...). @generated function _fww_reverse_grads( - f_orig, dret_val::T, args::Vararg{EnzymeCore.Active, N} + mode, f_orig, dret_val::T, args::Vararg{EnzymeCore.Active, N} ) where {T, N} # Build forward-mode calls for each partial derivative exprs = [] @@ -164,7 +215,7 @@ end dups = [:(Duplicated(args[$j].val, $(seeds[j]))) for j in 1:N] Ti = :(eltype(typeof(args[$i]))) push!(exprs, quote - fwd = Enzyme.autodiff(Forward, Const(f_orig), Duplicated{$T}, $(dups...)) + fwd = Enzyme.autodiff(mode, Const(f_orig), Duplicated{$T}, $(dups...)) $Ti(fwd[1] * dret_val)::$Ti end) end @@ -179,7 +230,7 @@ function EnzymeRules.reverse( args::Vararg{EnzymeCore.Active, N} ) where {T, N} f_orig = unwrap(func.val) - return _fww_reverse_grads(f_orig, dret.val, args...) + return _fww_reverse_grads(_fwd_mode_from_rev(config), f_orig, dret.val, args...) end # Handle mixed Active/Const args: return nothing for Const, gradient for Active @@ -192,6 +243,7 @@ function EnzymeRules.reverse( ) where {N} f_orig = unwrap(func.val) dret_val = dret.val + mode = _fwd_mode_from_rev(config) return ntuple(Val(N)) do i if args[i] isa EnzymeCore.Const nothing @@ -204,7 +256,7 @@ function EnzymeRules.reverse( Duplicated(args[j].val, zero(eltype(typeof(args[j])))) end end - fwd = Enzyme.autodiff(Forward, Const(f_orig), Duplicated, dup_args...) + fwd = Enzyme.autodiff(mode, Const(f_orig), Duplicated, dup_args...) fwd[1] * dret_val end end diff --git a/test/enzyme_tests.jl b/test/enzyme_tests.jl index c5713dc..c427094 100644 --- a/test/enzyme_tests.jl +++ b/test/enzyme_tests.jl @@ -277,3 +277,83 @@ end @test u_shadow[1] ≈ expected_u_grad end +# ============================================================================= +# Runtime-activity propagation through the FWW forward rules. +# +# Prior to this fix the rules hard-coded plain `Forward` when delegating to +# `Enzyme.autodiff`, silently dropping the caller's +# `set_runtime_activity(Forward)` flag. Enzyme's static IR-level activity +# analysis can't see through `FunctionWrappersWrapper`'s opaque cfunction +# indirection, so the inner call raised `EnzymeRuntimeActivityError` inside +# `@.` broadcast's `broadcast_unalias` → `mightalias` — despite +# `set_runtime_activity` being set on the outer `autodiff` call. +# +# Upstream motivation: OrdinaryDiffEq.jl PR #3518 — +# Rosenbrock23(autodiff = AutoEnzyme(set_runtime_activity(Enzyme.Forward))) +# on any time-dependent in-place RHS routed through DiffEqBase's +# `wrapfun_iip`. Here we reproduce the shape (`f!(du, u, p, t) = @. du = …`) +# in a 4-arg `FunctionWrappersWrapper` matching DiffEqBase's +# `wrapfun_iip` output, and assert both that (a) the call completes without +# an `EnzymeRuntimeActivityError` and (b) the resulting tangent is +# numerically correct. +# ============================================================================= + +@testset "Enzyme Forward: set_runtime_activity propagates through FWW (IIP, time-dependent)" begin + # DiffEqBase's `wrapfun_iip(ff, (u, u, p, t))` shape. + const_INPUTS = Tuple{Vector{Float64}, Vector{Float64}, Vector{Float64}, Float64} + + # 1) Time-independent RHS — ∂du/∂t = 0. + f!(du, u, p, t) = (@. du = p * u; nothing) + fww = FunctionWrappersWrapper(f!, (const_INPUTS,), (Nothing,)) + + u = [1.0, 2.0, 3.0] + p = [0.5, 0.5, 0.5] + t = 1.7 + du = zero(u); ddu = zero(u); dt = 1.0 + + Enzyme.autodiff( + Enzyme.set_runtime_activity(Forward), + Const(fww), Const, + Duplicated(du, ddu), + Const(u), Const(p), + Duplicated(t, dt), + ) + @test du ≈ p .* u + @test all(iszero, ddu) + + # 2) Non-trivial time dependence: g!(du,u,p,t) = @. sin(t)*u. + # Expected ∂du/∂t = cos(t) .* u. + g!(du, u, p, t) = (@. du = sin(t) * u; nothing) + gww = FunctionWrappersWrapper(g!, (const_INPUTS,), (Nothing,)) + + du2 = zero(u); ddu2 = zero(u) + Enzyme.autodiff( + Enzyme.set_runtime_activity(Forward), + Const(gww), Const, + Duplicated(du2, ddu2), + Const(u), Const(p), + Duplicated(t, 1.0), + ) + @test du2 ≈ sin(t) .* u + @test ddu2 ≈ cos(t) .* u + + # 3) Confirm the rule also propagates `set_strong_zero(Forward)` (the + # other ForwardMode flag carried in FwdConfig) — another RHS that + # doesn't need runtime activity but exercises a distinct flag. + h!(du, u, p, t) = (du[1] = u[1] * t; nothing) + hww = FunctionWrappersWrapper( + h!, (Tuple{Vector{Float64}, Vector{Float64}, Vector{Float64}, Float64},), + (Nothing,) + ) + du_h = [0.0]; ddu_h = [0.0] + Enzyme.autodiff( + Enzyme.set_strong_zero(Forward), + Const(hww), Const, + Duplicated(du_h, ddu_h), + Const([2.0]), Const([0.0]), + Duplicated(3.5, 1.0), + ) + @test du_h[1] ≈ 2.0 * 3.5 # primal: u[1] * t = 7.0 + @test ddu_h[1] ≈ 2.0 # ∂(u[1]*t)/∂t = u[1] +end + From 4eeba5d814755616daffa4424393cc97b75729ee Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Thu, 23 Apr 2026 08:21:33 +0000 Subject: [PATCH 36/55] Apply suggestion from @ChrisRackauckas --- ext/FunctionWrappersWrappersEnzymeExt.jl | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ext/FunctionWrappersWrappersEnzymeExt.jl b/ext/FunctionWrappersWrappersEnzymeExt.jl index 69d9a7d..9e0a3ae 100644 --- a/ext/FunctionWrappersWrappersEnzymeExt.jl +++ b/ext/FunctionWrappersWrappersEnzymeExt.jl @@ -104,11 +104,7 @@ end # IMPORTANT: forward the `RuntimeActivity` and `StrongZero` flags from the # outer config into the delegated `Enzyme.autodiff` call. Prior to this # fix the rule hard-coded `Forward`, silently dropping -# `set_runtime_activity(Forward)` on the way down into `f_orig`. That -# broke `Rosenbrock23(autodiff = AutoEnzyme(set_runtime_activity(Forward)))` -# on any time-dependent in-place RHS: -# EnzymeRuntimeActivityError at `broadcast_unalias` → `mightalias` -# despite the user explicitly setting runtime activity. +# `set_runtime_activity(Forward)` on the way down into `f_orig`. function EnzymeRules.forward( ::EnzymeRules.FwdConfig{false, false, W, RuntimeActivity, StrongZero}, func::EnzymeCore.Const{<:FunctionWrappersWrapper}, From 19e3b8f5cfc2ce3f22d63651cd911c7c78795f3c Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Thu, 23 Apr 2026 08:22:03 +0000 Subject: [PATCH 37/55] Bump version from 1.7.0 to 1.8.0 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index e0d59a2..cdbd972 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FunctionWrappersWrappers" uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" authors = ["Chris Elrod and contributors"] -version = "1.7.0" +version = "1.8.0" [deps] FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" From b4ecc014b987eb027922a4a4bcb0452b60fa9eb7 Mon Sep 17 00:00:00 2001 From: Chris Rackauckas Date: Sat, 9 May 2026 16:53:43 -0400 Subject: [PATCH 38/55] CI: install Julia in FormatCheck workflow so runic-action can run `fredrikekre/runic-action@v1` (v1.4+) requires `julia` in PATH but no longer installs it itself. Add `julia-actions/setup-julia@v2` before the action so the format check actually runs (it has been silently passing across many SciML repos for weeks). Co-Authored-By: Claude Opus 4.7 (1M context) Co-Authored-By: Chris Rackauckas --- .github/workflows/FormatCheck.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/FormatCheck.yml b/.github/workflows/FormatCheck.yml index d22e82d..6253546 100644 --- a/.github/workflows/FormatCheck.yml +++ b/.github/workflows/FormatCheck.yml @@ -14,6 +14,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - uses: julia-actions/setup-julia@v2 + with: + version: '1' - uses: fredrikekre/runic-action@v1 with: version: '1' From 29365d5bc97e828dafe3b65fab1a3aff2ffea243 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 02:03:07 +0000 Subject: [PATCH 39/55] Bump julia-actions/setup-julia from 2 to 3 Bumps [julia-actions/setup-julia](https://github.com/julia-actions/setup-julia) from 2 to 3. - [Release notes](https://github.com/julia-actions/setup-julia/releases) - [Commits](https://github.com/julia-actions/setup-julia/compare/v2...v3) --- updated-dependencies: - dependency-name: julia-actions/setup-julia dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/FormatCheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/FormatCheck.yml b/.github/workflows/FormatCheck.yml index 6253546..ee667ce 100644 --- a/.github/workflows/FormatCheck.yml +++ b/.github/workflows/FormatCheck.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: julia-actions/setup-julia@v2 + - uses: julia-actions/setup-julia@v3 with: version: '1' - uses: fredrikekre/runic-action@v1 From b102863128f5f4fca3e34f123461e93d04e01600 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Thu, 21 May 2026 05:00:59 -0400 Subject: [PATCH 40/55] Accept Annotation{<:FunctionWrappersWrapper} in Enzyme rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relaxes the `func` argument type in the forward, augmented_primal, and reverse rules from `Const{<:FunctionWrappersWrapper}` to `Annotation{<:FunctionWrappersWrapper}` so that callers passing `Duplicated{<:FunctionWrappersWrapper}` also dispatch through these rules instead of falling off to a MethodError. Closes SciML/FunctionWrappersWrappers.jl#48. Why this is safe: a `FunctionWrappersWrapper` only carries `FunctionWrapper`s plus cache storage; none of those fields have a meaningful tangent. The function shadow is therefore ignored — `func.val` (the primal wrapper) is unwrapped and the inner `Enzyme.autodiff` call delegates with `Const(f_orig)` exactly as in the previous `Const`-only path. Enzyme drives the rule with a `Duplicated` function annotation when the outer `autodiff` call is differentiating through a closure that captures an FWW — the SciML `NonlinearSolve` + `SciMLSensitivity` path in the issue is one such case. Adds four testsets that drive each forward / augmented_primal / reverse rule with `Duplicated(fww, fww)` to lock in the broader dispatch. Co-Authored-By: Claude Opus 4.7 (1M context) Co-Authored-By: Chris Rackauckas --- ext/FunctionWrappersWrappersEnzymeExt.jl | 32 ++++--- test/enzyme_tests.jl | 106 +++++++++++++++++++++++ 2 files changed, 127 insertions(+), 11 deletions(-) diff --git a/ext/FunctionWrappersWrappersEnzymeExt.jl b/ext/FunctionWrappersWrappersEnzymeExt.jl index 9e0a3ae..4b9f08c 100644 --- a/ext/FunctionWrappersWrappersEnzymeExt.jl +++ b/ext/FunctionWrappersWrappersEnzymeExt.jl @@ -36,9 +36,19 @@ end # ============================================================================= # Shadow only (Forward mode, no primal) +# +# `func` is `Annotation{<:FunctionWrappersWrapper}` rather than +# `Const{<:FunctionWrappersWrapper}` so that callers passing +# `Duplicated{<:FunctionWrappersWrapper}` also dispatch here. Enzyme drives +# the rule that way when the outer `autodiff` call is differentiating through +# a closure that carries an FWW (e.g. NonlinearSolve + SciMLSensitivity, see +# SciML/FunctionWrappersWrappers.jl#48). The FWW struct itself only carries +# `FunctionWrapper`s plus cache storage — none of those fields have a +# meaningful tangent — so the function shadow is ignored and the inner +# `Enzyme.autodiff` call uses `Const(f_orig)`. function EnzymeRules.forward( ::EnzymeRules.FwdConfig{false, true, W, RuntimeActivity, StrongZero}, - func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, RT::Type{<:EnzymeCore.Annotation{T}}, args::Vararg{EnzymeCore.Annotation, N} ) where {T, W, N, RuntimeActivity, StrongZero} @@ -56,7 +66,7 @@ end # Both primal and shadow (ForwardWithPrimal mode) function EnzymeRules.forward( ::EnzymeRules.FwdConfig{true, true, W, RuntimeActivity, StrongZero}, - func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, RT::Type{<:EnzymeCore.Annotation{T}}, args::Vararg{EnzymeCore.Annotation, N} ) where {T, W, N, RuntimeActivity, StrongZero} @@ -81,7 +91,7 @@ end # Primal only (Const return type) — width-independent function EnzymeRules.forward( ::EnzymeRules.FwdConfig{true, false, W, RuntimeActivity, StrongZero}, - func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, RT::Type{<:EnzymeCore.Annotation}, args::Vararg{EnzymeCore.Annotation, N} ) where {W, N, RuntimeActivity, StrongZero} @@ -107,7 +117,7 @@ end # `set_runtime_activity(Forward)` on the way down into `f_orig`. function EnzymeRules.forward( ::EnzymeRules.FwdConfig{false, false, W, RuntimeActivity, StrongZero}, - func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, RT::Type{<:EnzymeCore.Annotation}, args::Vararg{EnzymeCore.Annotation, N} ) where {W, N, RuntimeActivity, StrongZero} @@ -123,7 +133,7 @@ end function EnzymeRules.augmented_primal( config::EnzymeRules.RevConfig, - func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, RT::Type{<:EnzymeCore.Active{T}}, args::Vararg{EnzymeCore.Annotation, N} ) where {T, N} @@ -143,7 +153,7 @@ end # the reverse pass has nothing to propagate back from the return. function EnzymeRules.augmented_primal( config::EnzymeRules.RevConfig, - func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, RT::Type{<:EnzymeCore.Const}, args::Vararg{EnzymeCore.Annotation, N} ) where {N} @@ -157,7 +167,7 @@ end # it available when propagating dret through the arguments. function EnzymeRules.augmented_primal( config::EnzymeRules.RevConfig, - func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, RT::Type{<:EnzymeCore.Duplicated{T}}, args::Vararg{EnzymeCore.Annotation, N} ) where {T, N} @@ -173,7 +183,7 @@ end function EnzymeRules.augmented_primal( config::EnzymeRules.RevConfig, - func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, RT::Type{<:EnzymeCore.BatchDuplicated{T, W}}, args::Vararg{EnzymeCore.Annotation, N} ) where {T, W, N} @@ -220,7 +230,7 @@ end function EnzymeRules.reverse( config::EnzymeRules.RevConfig, - func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, dret::EnzymeCore.Active{T}, tape, args::Vararg{EnzymeCore.Active, N} @@ -232,7 +242,7 @@ end # Handle mixed Active/Const args: return nothing for Const, gradient for Active function EnzymeRules.reverse( config::EnzymeRules.RevConfig, - func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, dret::EnzymeCore.Active, tape, args::Vararg{EnzymeCore.Annotation, N} @@ -271,7 +281,7 @@ end # accumulated in-place by the `Enzyme.autodiff(Reverse, …)` call above. function EnzymeRules.reverse( config::EnzymeRules.RevConfig, - func::EnzymeCore.Const{<:FunctionWrappersWrapper}, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, dret::Type{<:EnzymeCore.Const}, tape, args::Vararg{EnzymeCore.Annotation, N} diff --git a/test/enzyme_tests.jl b/test/enzyme_tests.jl index c427094..00f9b15 100644 --- a/test/enzyme_tests.jl +++ b/test/enzyme_tests.jl @@ -298,6 +298,112 @@ end # numerically correct. # ============================================================================= +# ============================================================================= +# Duplicated function annotation on the FWW itself. +# +# Reproduces SciML/FunctionWrappersWrappers.jl#48: when Enzyme differentiates +# through a closure that captures an FWW (e.g. NonlinearSolve + +# SciMLSensitivity), the rule is invoked with +# `Duplicated{<:FunctionWrappersWrapper}` for the function argument, not +# `Const{<:FunctionWrappersWrapper}`. The FWW struct itself only carries +# `FunctionWrapper`s + cache storage, so its "shadow" is ignored — we route +# through `unwrap(func.val)` exactly as with `Const`. +# ============================================================================= + +@testset "Enzyme forward, Duplicated FWW annotation — IIP Const return" begin + f!(residual, u, p) = (residual[1] = u[1]^2 - p[1]; nothing) + fww = FunctionWrappersWrapper( + f!, + (Tuple{Vector{Float64}, Vector{Float64}, Vector{Float64}},), + (Nothing,) + ) + + residual = [0.0]; dresidual = [0.0] + u = [2.0]; du = [1.0] + p = [1.0]; dp = [0.0] + + config = EnzymeCore.EnzymeRules.FwdConfig{false, false, 1, false, false}() + ret = EnzymeCore.EnzymeRules.forward( + config, + Duplicated(fww, fww), # <-- the failing dispatch in #48 + EnzymeCore.Const{Nothing}, + Duplicated(residual, dresidual), + Duplicated(u, du), + Duplicated(p, dp), + ) + @test ret === nothing + @test residual[1] ≈ 3.0 # u[1]^2 - p[1] = 4 - 1 + @test dresidual[1] ≈ 4.0 # 2*u[1]*du[1] - 1*dp[1] = 4 +end + +@testset "Enzyme forward, Duplicated FWW annotation — shadow-only return" begin + # Drive the {false, true, W, …} rule (shadow only, no primal) with a + # Duplicated FWW. + f(x) = x^2 + fww = FunctionWrappersWrapper(f, (Tuple{Float64},), (Float64,)) + + config = EnzymeCore.EnzymeRules.FwdConfig{false, true, 1, false, false}() + shadow = EnzymeCore.EnzymeRules.forward( + config, + Duplicated(fww, fww), + EnzymeCore.Duplicated{Float64}, + Duplicated(3.0, 1.0), + ) + @test shadow ≈ 6.0 # f'(3) = 2*3 = 6 +end + +@testset "Enzyme forward, Duplicated FWW annotation — primal + shadow return" begin + # Drive the {true, true, W, …} rule (ForwardWithPrimal) with a Duplicated + # FWW. + f(x) = x^2 + fww = FunctionWrappersWrapper(f, (Tuple{Float64},), (Float64,)) + + config = EnzymeCore.EnzymeRules.FwdConfig{true, true, 1, false, false}() + result = EnzymeCore.EnzymeRules.forward( + config, + Duplicated(fww, fww), + EnzymeCore.Duplicated{Float64}, + Duplicated(3.0, 1.0), + ) + @test result isa Duplicated + @test result.val ≈ 9.0 # primal + @test result.dval ≈ 6.0 # shadow +end + +@testset "Enzyme reverse, Duplicated FWW annotation — Const return IIP" begin + # Mirror the forward IIP case on the reverse side. Duplicated FWW must + # still drive the rule, gradients must accumulate into u_shadow. + f!(du, u) = (du[1] = u[1]^2; nothing) + fww = FunctionWrappersWrapper( + f!, (Tuple{Vector{Float64}, Vector{Float64}},), (Nothing,) + ) + + du = [0.0]; du_shadow = [1.0] + u = [3.0]; u_shadow = [0.0] + + rconfig = EnzymeRules.RevConfig{false, false, 1, (false, false), false, false}() + aug = EnzymeRules.augmented_primal( + rconfig, + Duplicated(fww, fww), # <-- Duplicated FWW + EnzymeCore.Const{Nothing}, + Duplicated(du, du_shadow), + Duplicated(u, u_shadow), + ) + @test aug.primal === nothing + @test aug.shadow === nothing + + EnzymeRules.reverse( + rconfig, + Duplicated(fww, fww), + EnzymeCore.Const{Nothing}, + aug.tape, + Duplicated(du, du_shadow), + Duplicated(u, u_shadow), + ) + @test du[1] ≈ 9.0 # primal effect from augmented_primal + @test u_shadow[1] ≈ 6.0 # reverse accumulation: 2*u[1]*du_shadow[1] +end + @testset "Enzyme Forward: set_runtime_activity propagates through FWW (IIP, time-dependent)" begin # DiffEqBase's `wrapfun_iip(ff, (u, u, p, t))` shape. const_INPUTS = Tuple{Vector{Float64}, Vector{Float64}, Vector{Float64}, Float64} From 509d7fbaaf6b05ad031f5d618d0b41b171cf7638 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Thu, 21 May 2026 05:17:47 -0400 Subject: [PATCH 41/55] Convert AnonymousStruct shadow to NTuple in batch forward rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The forward rules for batch width > 1 asserted `shadow_result[1]::NTuple{W, T}` but `Enzyme.autodiff(Forward, …, BatchDuplicated{T,W}, …)` returns the batch shadow wrapped in `Enzyme.Compiler.AnonymousStruct` — a `NamedTuple{(:1, :2, …), NTuple{W, T}}` (see `Enzyme/src/compiler/utils.jl:480`). The mismatch tripped the existing `Enzyme batch forward mode (width > 1)` testset on `main` with: ``` TypeError: in typeassert, expected Tuple{Float64, Float64}, got a value of type @NamedTuple{1::Float64, 2::Float64} ``` Wrap the shadow in `Tuple(...)` before the type-assert so the rule's return matches the `BatchDuplicated` shadow contract that Enzyme expects from a forward rule. Applies to both `{false, true, W>1, …}` (shadow-only) and `{true, true, W>1, …}` (ForwardWithPrimal) rules. The existing testset (which was failing on `main`) now passes. Full local test summary on Julia 1.11 + Enzyme v0.13.147: ``` FunctionWrappersWrappers.jl | 48 48 BigFloat support | 5 5 UnionAll return types | 2 2 Enzyme extension | 44 44 Mooncake extension | 13 13 ``` Co-Authored-By: Claude Opus 4.7 (1M context) Co-Authored-By: Chris Rackauckas --- ext/FunctionWrappersWrappersEnzymeExt.jl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ext/FunctionWrappersWrappersEnzymeExt.jl b/ext/FunctionWrappersWrappersEnzymeExt.jl index 9e0a3ae..8797ea7 100644 --- a/ext/FunctionWrappersWrappersEnzymeExt.jl +++ b/ext/FunctionWrappersWrappersEnzymeExt.jl @@ -49,7 +49,12 @@ function EnzymeRules.forward( return shadow_result[1]::T else shadow_result = Enzyme.autodiff(mode, Const(f_orig), BatchDuplicated{T, W}, args...) - return shadow_result[1]::NTuple{W, T} + # Enzyme returns the batch shadow as an `AnonymousStruct` — a + # `NamedTuple{(:1, :2, …), NTuple{W, T}}` (see + # `Enzyme.Compiler.AnonymousStruct` in `Enzyme/src/compiler/utils.jl`). + # Convert to a plain tuple so the rule's return matches the + # `BatchDuplicated` shadow contract Enzyme expects from a forward rule. + return Tuple(shadow_result[1])::NTuple{W, T} end end @@ -73,7 +78,9 @@ function EnzymeRules.forward( return Duplicated(primal, shadow) else shadow_result = Enzyme.autodiff(mode, Const(f_orig), BatchDuplicated{T, W}, args...) - shadows = shadow_result[1]::NTuple{W, T} + # See the comment on the {false, true} rule — `shadow_result[1]` is a + # NamedTuple, not an NTuple. + shadows = Tuple(shadow_result[1])::NTuple{W, T} return BatchDuplicated(primal, shadows) end end From b15abbbd1417e41a2ed4d4d051f85afa3de4200e Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Thu, 21 May 2026 05:25:34 -0400 Subject: [PATCH 42/55] Add direct-rule regression test for batch-forward NTuple conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calls `EnzymeRules.forward` directly so the rule's actual return value is observable. The pre-existing `Enzyme.autodiff(Forward, …)`-driven testset doesn't catch a regression on its own because the outer `Enzyme.autodiff` ALSO wraps in `AnonymousStruct`, and `shadow[1]` indexing works on both `NamedTuple` and `Tuple`. The new testset asserts: - the shadow-only rule (`{false, true, W=2, …}`) returns `NTuple{2, Float64}`, not `NamedTuple`; - the ForwardWithPrimal rule (`{true, true, W=2, …}`) puts an `NTuple{2, Float64}` (not a `NamedTuple`) into `result.dval`; - the conversion generalises to W = 3. Co-Authored-By: Claude Opus 4.7 (1M context) Co-Authored-By: Chris Rackauckas --- test/enzyme_tests.jl | 53 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/test/enzyme_tests.jl b/test/enzyme_tests.jl index c427094..8bbf2ea 100644 --- a/test/enzyme_tests.jl +++ b/test/enzyme_tests.jl @@ -76,6 +76,59 @@ end @test result_wp[2] ≈ 9.0 # primal f(3) = 9 end +@testset "Enzyme batch forward rule return type is NTuple, not NamedTuple" begin + # Regression test for the typeassert bug: the inner + # `Enzyme.autodiff(Forward, …, BatchDuplicated{T,W}, …)` returns the + # batch shadow wrapped in `Enzyme.Compiler.AnonymousStruct` — a + # `NamedTuple{(:1, :2, …), NTuple{W, T}}`. The rule must convert it + # to a plain `NTuple{W, T}` before returning, otherwise the + # `::NTuple{W, T}` typeassert fires and surfaces as: + # TypeError: in typeassert, expected Tuple{Float64, Float64}, + # got a value of type @NamedTuple{1::Float64, 2::Float64} + # + # The outer `Enzyme.autodiff` testset above doesn't catch this on its + # own because the outer call ALSO wraps the result in + # `AnonymousStruct`, and `shadow[1] / shadow[2]` indexing works on + # both `NamedTuple` and `Tuple`. Call `EnzymeRules.forward` + # directly so we observe the rule's actual return value and can + # assert its concrete type. + f(x) = x^2 + fww = FunctionWrappersWrapper(f, (Tuple{Float64},), (Float64,)) + + # {NeedsPrimal=false, NeedsShadow=true, W=2, RuntimeActivity=false, + # StrongZero=false} — the shadow-only batch branch. + config_shadow = EnzymeCore.EnzymeRules.FwdConfig{false, true, 2, false, false}() + shadow = EnzymeCore.EnzymeRules.forward( + config_shadow, Const(fww), EnzymeCore.BatchDuplicated{Float64, 2}, + BatchDuplicated(3.0, (1.0, 2.0)) + ) + @test shadow isa NTuple{2, Float64} + @test !(shadow isa NamedTuple) + @test shadow == (6.0, 12.0) + + # {NeedsPrimal=true, NeedsShadow=true, W=2, …} — ForwardWithPrimal + # batch branch. Same conversion bug existed on this path. + config_primal = EnzymeCore.EnzymeRules.FwdConfig{true, true, 2, false, false}() + result = EnzymeCore.EnzymeRules.forward( + config_primal, Const(fww), EnzymeCore.BatchDuplicated{Float64, 2}, + BatchDuplicated(3.0, (1.0, 2.0)) + ) + @test result isa BatchDuplicated + @test result.val ≈ 9.0 + @test result.dval isa NTuple{2, Float64} + @test !(result.dval isa NamedTuple) + @test result.dval == (6.0, 12.0) + + # Confirm the conversion generalises to W > 2. + config_w3 = EnzymeCore.EnzymeRules.FwdConfig{false, true, 3, false, false}() + shadow3 = EnzymeCore.EnzymeRules.forward( + config_w3, Const(fww), EnzymeCore.BatchDuplicated{Float64, 3}, + BatchDuplicated(3.0, (1.0, 2.0, 4.0)) + ) + @test shadow3 isa NTuple{3, Float64} + @test shadow3 == (6.0, 12.0, 24.0) +end + @testset "Enzyme forward mode, Const return (IIP, no return-shadow)" begin # Covers EnzymeRules.FwdConfig{false, false, W, ...} — Enzyme dispatches on # this combo for IIP functions with a Const return type where the caller From 4c927d3ee97d0e865073169df126087d087677b8 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Thu, 21 May 2026 09:26:39 +0000 Subject: [PATCH 43/55] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index cdbd972..655c4a6 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FunctionWrappersWrappers" uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" authors = ["Chris Elrod and contributors"] -version = "1.8.0" +version = "1.9.0" [deps] FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" From 323c05a60f2194f2b2a3b5621a399fde147175ea Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Thu, 21 May 2026 11:20:27 -0400 Subject: [PATCH 44/55] Declare cache-storage types as Enzyme inactive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `SingleCacheStorage` is a mutable struct and `DictCacheStorage` carries a mutable `Dict`. Their fallback paths write a `FunctionWrapper` into the storage on a cache miss. Without an `inactive_type` declaration Enzyme conservatively treats any closure that captures a `FunctionWrappersWrapper` as potentially writing to the storage, so `Enzyme.gradient`/`autodiff` through such a closure can fail with `EnzymeMutabilityException` on the captured argument. The cache values are `FunctionWrapper`s — used purely for dispatch and dynamic-call speedup. They never carry derivative data. Marking the three storage types as `inactive_type` lets Enzyme prove the captured wrapper readonly without affecting derivative correctness. Defensive hardening prompted by the Enzyme-through-MTK-remake stack documented in SciML/SciMLSensitivity.jl#1323, SciML/SciMLSensitivity.jl#1359, and SciML/ModelingToolkit.jl#4550 — this PR alone is not sufficient to unblock that path (the underlying NonlinearSolve + Enzyme interaction still hits a separate `EnzymeMutabilityException` against the wrapped function), but it removes one conservative-IPA layer for any user of FWW under Enzyme. Co-Authored-By: Chris Rackauckas --- Project.toml | 2 +- ext/FunctionWrappersWrappersEnzymeExt.jl | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 655c4a6..6b3c56c 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FunctionWrappersWrappers" uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" authors = ["Chris Elrod and contributors"] -version = "1.9.0" +version = "1.9.1" [deps] FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" diff --git a/ext/FunctionWrappersWrappersEnzymeExt.jl b/ext/FunctionWrappersWrappersEnzymeExt.jl index 42229f6..38097c5 100644 --- a/ext/FunctionWrappersWrappersEnzymeExt.jl +++ b/ext/FunctionWrappersWrappersEnzymeExt.jl @@ -1,10 +1,26 @@ module FunctionWrappersWrappersEnzymeExt using FunctionWrappersWrappers +using FunctionWrappersWrappers: SingleCacheStorage, DictCacheStorage, NoCacheStorage using Enzyme using EnzymeCore using EnzymeCore.EnzymeRules +# ============================================================================= +# Mark cache-storage types as inactive +# ============================================================================= +# `SingleCacheStorage` and `DictCacheStorage` are mutable / contain a `Dict`, +# and their cache-miss branches write to that storage. Without these +# declarations Enzyme conservatively treats any closure that *might* touch a +# `FunctionWrappersWrapper` (e.g. via `remake(prob; p = …)` capturing the +# problem in scope) as potentially writing to the wrapper's cache, and +# refuses to prove the captured argument read-only. The cache values are +# `FunctionWrapper`s used purely for dispatch / dynamic call speedup; they +# never hold derivative data. +EnzymeCore.EnzymeRules.inactive_type(::Type{<:SingleCacheStorage}) = true +EnzymeCore.EnzymeRules.inactive_type(::Type{<:DictCacheStorage}) = true +EnzymeCore.EnzymeRules.inactive_type(::Type{NoCacheStorage}) = true + # ============================================================================= # Helper: build a Forward mode from FwdConfig flags # ============================================================================= From 15032a25593a2026dda80c93d3a4ae2256cc3701 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sat, 30 May 2026 08:48:42 +0000 Subject: [PATCH 45/55] Remove enable-beta-ecosystems flag (Julia no longer beta in Dependabot) --- .github/dependabot.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 36b0782..cd7b7f2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,5 @@ # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 -enable-beta-ecosystems: true # Julia ecosystem updates: - package-ecosystem: "github-actions" directory: "/" # Location of package manifests From 8a742af1a30db4b004a7bfa18b4fec5106e359aa Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Wed, 3 Jun 2026 12:33:45 -0400 Subject: [PATCH 46/55] Centralize CI on SciML reusable workflows Convert all CI to the SciML centralized reusable workflows (pinned @v1, secrets: inherit on every caller). - Tests: already a tests.yml@v1 caller (unchanged matrix; secrets already inherit). Moved the inline Documentation job out into its own workflow. - Documentation: new documentation.yml@v1 caller (replaces inline docdeploy job). - Runic (FormatCheck): convert inline fredrikekre/runic-action to runic.yml@v1. - SpellCheck: new spellcheck.yml@v1 caller (was previously not present). - Downgrade: new downgrade.yml@v1 caller (julia-version lts, skip Pkg,TOML, group Core). - dependabot: remove the crate-ci/typos ignore (typos now runs via centralized caller). - Apply Runic formatting to ext/ and test/ sources. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/dependabot.yml | 3 - .github/workflows/Documentation.yml | 21 ++++ .github/workflows/Downgrade.yml | 33 ++++++ .github/workflows/FormatCheck.yml | 12 +- .github/workflows/SpellCheck.yml | 9 ++ .github/workflows/Tests.yml | 20 ---- ext/FunctionWrappersWrappersEnzymeExt.jl | 136 ++++++++++++----------- test/enzyme_tests.jl | 11 +- 8 files changed, 140 insertions(+), 105 deletions(-) create mode 100644 .github/workflows/Documentation.yml create mode 100644 .github/workflows/Downgrade.yml create mode 100644 .github/workflows/SpellCheck.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cd7b7f2..f901dd8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,9 +5,6 @@ updates: directory: "/" # Location of package manifests schedule: interval: "weekly" - ignore: - - dependency-name: "crate-ci/typos" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - package-ecosystem: "julia" directories: - "/" diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml new file mode 100644 index 0000000..3755851 --- /dev/null +++ b/.github/workflows/Documentation.yml @@ -0,0 +1,21 @@ +name: "Documentation" + +on: + pull_request: + branches: + - main + - 'release-' + push: + branches: + - main + tags: '*' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch || github.ref != 'refs/tags/v*' }} + +jobs: + docs: + name: Documentation + uses: "SciML/.github/.github/workflows/documentation.yml@v1" + secrets: "inherit" diff --git a/.github/workflows/Downgrade.yml b/.github/workflows/Downgrade.yml new file mode 100644 index 0000000..95f1cb1 --- /dev/null +++ b/.github/workflows/Downgrade.yml @@ -0,0 +1,33 @@ +name: Downgrade + +on: + pull_request: + branches: + - main + - 'release-' + paths-ignore: + - 'docs/**' + push: + branches: + - main + paths-ignore: + - 'docs/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch || github.ref != 'refs/tags/v*' }} + +jobs: + downgrade: + name: Downgrade + strategy: + fail-fast: false + matrix: + group: + - Core + uses: "SciML/.github/.github/workflows/downgrade.yml@v1" + with: + group: "${{ matrix.group }}" + julia-version: "lts" + skip: "Pkg,TOML" + secrets: "inherit" diff --git a/.github/workflows/FormatCheck.yml b/.github/workflows/FormatCheck.yml index ee667ce..db91458 100644 --- a/.github/workflows/FormatCheck.yml +++ b/.github/workflows/FormatCheck.yml @@ -11,12 +11,6 @@ on: jobs: runic: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: julia-actions/setup-julia@v3 - with: - version: '1' - - uses: fredrikekre/runic-action@v1 - with: - version: '1' + name: Runic + uses: "SciML/.github/.github/workflows/runic.yml@v1" + secrets: "inherit" diff --git a/.github/workflows/SpellCheck.yml b/.github/workflows/SpellCheck.yml new file mode 100644 index 0000000..1223235 --- /dev/null +++ b/.github/workflows/SpellCheck.yml @@ -0,0 +1,9 @@ +name: Spell Check + +on: [pull_request] + +jobs: + typos-check: + name: Spell Check with Typos + uses: "SciML/.github/.github/workflows/spellcheck.yml@v1" + secrets: "inherit" diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 7a5a521..9d5899a 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -41,23 +41,3 @@ jobs: julia-version: "${{ matrix.version }}" julia-arch: "${{ matrix.arch }}" secrets: "inherit" - - docs: - name: Documentation - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: julia-actions/setup-julia@v3 - with: - version: '1' - - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-docdeploy@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} - - run: | - julia --project=docs -e ' - using Documenter: DocMeta, doctest - using FunctionWrappersWrappers - DocMeta.setdocmeta!(FunctionWrappersWrappers, :DocTestSetup, :(using FunctionWrappersWrappers); recursive=true) - doctest(FunctionWrappersWrappers)' diff --git a/ext/FunctionWrappersWrappersEnzymeExt.jl b/ext/FunctionWrappersWrappersEnzymeExt.jl index 38097c5..1c05580 100644 --- a/ext/FunctionWrappersWrappersEnzymeExt.jl +++ b/ext/FunctionWrappersWrappersEnzymeExt.jl @@ -39,8 +39,8 @@ EnzymeCore.EnzymeRules.inactive_type(::Type{NoCacheStorage}) = true # `ForwardMode` matching the outer config so the delegated call inherits # those flags. @inline function _fwd_mode( - ::Val{NeedsPrimal}, ::Val{RuntimeActivity}, ::Val{StrongZero} -) where {NeedsPrimal, RuntimeActivity, StrongZero} + ::Val{NeedsPrimal}, ::Val{RuntimeActivity}, ::Val{StrongZero} + ) where {NeedsPrimal, RuntimeActivity, StrongZero} mode = NeedsPrimal ? ForwardWithPrimal : Forward RuntimeActivity && (mode = Enzyme.set_runtime_activity(mode)) StrongZero && (mode = Enzyme.set_strong_zero(mode)) @@ -63,11 +63,11 @@ end # meaningful tangent — so the function shadow is ignored and the inner # `Enzyme.autodiff` call uses `Const(f_orig)`. function EnzymeRules.forward( - ::EnzymeRules.FwdConfig{false, true, W, RuntimeActivity, StrongZero}, - func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, - RT::Type{<:EnzymeCore.Annotation{T}}, - args::Vararg{EnzymeCore.Annotation, N} -) where {T, W, N, RuntimeActivity, StrongZero} + ::EnzymeRules.FwdConfig{false, true, W, RuntimeActivity, StrongZero}, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, + RT::Type{<:EnzymeCore.Annotation{T}}, + args::Vararg{EnzymeCore.Annotation, N} + ) where {T, W, N, RuntimeActivity, StrongZero} f_orig = unwrap(func.val) mode = _fwd_mode(Val(false), Val(RuntimeActivity), Val(StrongZero)) if W == 1 @@ -86,11 +86,11 @@ end # Both primal and shadow (ForwardWithPrimal mode) function EnzymeRules.forward( - ::EnzymeRules.FwdConfig{true, true, W, RuntimeActivity, StrongZero}, - func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, - RT::Type{<:EnzymeCore.Annotation{T}}, - args::Vararg{EnzymeCore.Annotation, N} -) where {T, W, N, RuntimeActivity, StrongZero} + ::EnzymeRules.FwdConfig{true, true, W, RuntimeActivity, StrongZero}, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, + RT::Type{<:EnzymeCore.Annotation{T}}, + args::Vararg{EnzymeCore.Annotation, N} + ) where {T, W, N, RuntimeActivity, StrongZero} f_orig = unwrap(func.val) pargs = ntuple(i -> args[i].val, Val(N)) primal = f_orig(pargs...)::T @@ -113,11 +113,11 @@ end # Primal only (Const return type) — width-independent function EnzymeRules.forward( - ::EnzymeRules.FwdConfig{true, false, W, RuntimeActivity, StrongZero}, - func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, - RT::Type{<:EnzymeCore.Annotation}, - args::Vararg{EnzymeCore.Annotation, N} -) where {W, N, RuntimeActivity, StrongZero} + ::EnzymeRules.FwdConfig{true, false, W, RuntimeActivity, StrongZero}, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, + RT::Type{<:EnzymeCore.Annotation}, + args::Vararg{EnzymeCore.Annotation, N} + ) where {W, N, RuntimeActivity, StrongZero} f_orig = unwrap(func.val) pargs = ntuple(i -> args[i].val, Val(N)) return f_orig(pargs...) @@ -137,13 +137,13 @@ end # IMPORTANT: forward the `RuntimeActivity` and `StrongZero` flags from the # outer config into the delegated `Enzyme.autodiff` call. Prior to this # fix the rule hard-coded `Forward`, silently dropping -# `set_runtime_activity(Forward)` on the way down into `f_orig`. +# `set_runtime_activity(Forward)` on the way down into `f_orig`. function EnzymeRules.forward( - ::EnzymeRules.FwdConfig{false, false, W, RuntimeActivity, StrongZero}, - func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, - RT::Type{<:EnzymeCore.Annotation}, - args::Vararg{EnzymeCore.Annotation, N} -) where {W, N, RuntimeActivity, StrongZero} + ::EnzymeRules.FwdConfig{false, false, W, RuntimeActivity, StrongZero}, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, + RT::Type{<:EnzymeCore.Annotation}, + args::Vararg{EnzymeCore.Annotation, N} + ) where {W, N, RuntimeActivity, StrongZero} f_orig = unwrap(func.val) mode = _fwd_mode(Val(false), Val(RuntimeActivity), Val(StrongZero)) Enzyme.autodiff(mode, Const(f_orig), Const, args...) @@ -155,11 +155,11 @@ end # ============================================================================= function EnzymeRules.augmented_primal( - config::EnzymeRules.RevConfig, - func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, - RT::Type{<:EnzymeCore.Active{T}}, - args::Vararg{EnzymeCore.Annotation, N} -) where {T, N} + config::EnzymeRules.RevConfig, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, + RT::Type{<:EnzymeCore.Active{T}}, + args::Vararg{EnzymeCore.Annotation, N} + ) where {T, N} f_orig = unwrap(func.val) pargs = ntuple(i -> args[i].val, Val(N)) result = f_orig(pargs...)::T @@ -175,11 +175,11 @@ end # return). Just run the primal for its side effects; no tape is needed because # the reverse pass has nothing to propagate back from the return. function EnzymeRules.augmented_primal( - config::EnzymeRules.RevConfig, - func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, - RT::Type{<:EnzymeCore.Const}, - args::Vararg{EnzymeCore.Annotation, N} -) where {N} + config::EnzymeRules.RevConfig, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, + RT::Type{<:EnzymeCore.Const}, + args::Vararg{EnzymeCore.Annotation, N} + ) where {N} f_orig = unwrap(func.val) pargs = ntuple(i -> args[i].val, Val(N)) f_orig(pargs...) @@ -189,11 +189,11 @@ end # Duplicated / BatchDuplicated return: record the primal so that reverse has # it available when propagating dret through the arguments. function EnzymeRules.augmented_primal( - config::EnzymeRules.RevConfig, - func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, - RT::Type{<:EnzymeCore.Duplicated{T}}, - args::Vararg{EnzymeCore.Annotation, N} -) where {T, N} + config::EnzymeRules.RevConfig, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, + RT::Type{<:EnzymeCore.Duplicated{T}}, + args::Vararg{EnzymeCore.Annotation, N} + ) where {T, N} f_orig = unwrap(func.val) pargs = ntuple(i -> args[i].val, Val(N)) primal = f_orig(pargs...)::T @@ -205,11 +205,11 @@ function EnzymeRules.augmented_primal( end function EnzymeRules.augmented_primal( - config::EnzymeRules.RevConfig, - func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, - RT::Type{<:EnzymeCore.BatchDuplicated{T, W}}, - args::Vararg{EnzymeCore.Annotation, N} -) where {T, W, N} + config::EnzymeRules.RevConfig, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, + RT::Type{<:EnzymeCore.BatchDuplicated{T, W}}, + args::Vararg{EnzymeCore.Annotation, N} + ) where {T, W, N} f_orig = unwrap(func.val) pargs = ntuple(i -> args[i].val, Val(N)) primal = f_orig(pargs...)::T @@ -235,41 +235,43 @@ end # function, then scale by dret. This avoids type-inference issues that arise # from calling autodiff(Reverse, Const{Any}(...), ...). @generated function _fww_reverse_grads( - mode, f_orig, dret_val::T, args::Vararg{EnzymeCore.Active, N} -) where {T, N} + mode, f_orig, dret_val::T, args::Vararg{EnzymeCore.Active, N} + ) where {T, N} # Build forward-mode calls for each partial derivative exprs = [] for i in 1:N seeds = [j == i ? :(one(eltype(typeof(args[$j])))) : :(zero(eltype(typeof(args[$j])))) for j in 1:N] dups = [:(Duplicated(args[$j].val, $(seeds[j]))) for j in 1:N] Ti = :(eltype(typeof(args[$i]))) - push!(exprs, quote - fwd = Enzyme.autodiff(mode, Const(f_orig), Duplicated{$T}, $(dups...)) - $Ti(fwd[1] * dret_val)::$Ti - end) + push!( + exprs, quote + fwd = Enzyme.autodiff(mode, Const(f_orig), Duplicated{$T}, $(dups...)) + $Ti(fwd[1] * dret_val)::$Ti + end + ) end return Expr(:tuple, exprs...) end function EnzymeRules.reverse( - config::EnzymeRules.RevConfig, - func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, - dret::EnzymeCore.Active{T}, - tape, - args::Vararg{EnzymeCore.Active, N} -) where {T, N} + config::EnzymeRules.RevConfig, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, + dret::EnzymeCore.Active{T}, + tape, + args::Vararg{EnzymeCore.Active, N} + ) where {T, N} f_orig = unwrap(func.val) return _fww_reverse_grads(_fwd_mode_from_rev(config), f_orig, dret.val, args...) end # Handle mixed Active/Const args: return nothing for Const, gradient for Active function EnzymeRules.reverse( - config::EnzymeRules.RevConfig, - func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, - dret::EnzymeCore.Active, - tape, - args::Vararg{EnzymeCore.Annotation, N} -) where {N} + config::EnzymeRules.RevConfig, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, + dret::EnzymeCore.Active, + tape, + args::Vararg{EnzymeCore.Annotation, N} + ) where {N} f_orig = unwrap(func.val) dret_val = dret.val mode = _fwd_mode_from_rev(config) @@ -303,12 +305,12 @@ end # `BatchDuplicated` args return `nothing` because their gradients are # accumulated in-place by the `Enzyme.autodiff(Reverse, …)` call above. function EnzymeRules.reverse( - config::EnzymeRules.RevConfig, - func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, - dret::Type{<:EnzymeCore.Const}, - tape, - args::Vararg{EnzymeCore.Annotation, N} -) where {N} + config::EnzymeRules.RevConfig, + func::EnzymeCore.Annotation{<:FunctionWrappersWrapper}, + dret::Type{<:EnzymeCore.Const}, + tape, + args::Vararg{EnzymeCore.Annotation, N} + ) where {N} f_orig = unwrap(func.val) # Only worth invoking Enzyme.autodiff when at least one arg is # Duplicated/BatchDuplicated — otherwise there's nothing to accumulate. diff --git a/test/enzyme_tests.jl b/test/enzyme_tests.jl index 15cc038..5a8f5f9 100644 --- a/test/enzyme_tests.jl +++ b/test/enzyme_tests.jl @@ -284,7 +284,7 @@ end # du_shadow gives the accumulation into u_shadow: # u_shadow[1] += a*y + b*2x # u_shadow[2] += a*x + b*3y^2 - f!(du, u) = (du[1] = u[1]*u[2]; du[2] = u[1]^2 + u[2]^3; nothing) + f!(du, u) = (du[1] = u[1] * u[2]; du[2] = u[1]^2 + u[2]^3; nothing) fww = FunctionWrappersWrapper( f!, (Tuple{Vector{Float64}, Vector{Float64}},), (Nothing,) ) @@ -300,9 +300,9 @@ end Reverse, Const(fww), Const, Duplicated(du, du_shadow), Duplicated(u, u_shadow) ) - @test du ≈ [x*y, x^2 + y^3] - @test u_shadow[1] ≈ a*y + b*2*x # 5 + 2 = 7 - @test u_shadow[2] ≈ a*x + b*3*y^2 # 2 + 37.5 = 39.5 + @test du ≈ [x * y, x^2 + y^3] + @test u_shadow[1] ≈ a * y + b * 2 * x # 5 + 2 = 7 + @test u_shadow[2] ≈ a * x + b * 3 * y^2 # 2 + 37.5 = 39.5 end @testset "Enzyme ReverseWithPrimal: IIP with Duplicated args" begin @@ -432,7 +432,7 @@ end ) du = [0.0]; du_shadow = [1.0] - u = [3.0]; u_shadow = [0.0] + u = [3.0]; u_shadow = [0.0] rconfig = EnzymeRules.RevConfig{false, false, 1, (false, false), false, false}() aug = EnzymeRules.augmented_primal( @@ -515,4 +515,3 @@ end @test du_h[1] ≈ 2.0 * 3.5 # primal: u[1] * t = 7.0 @test ddu_h[1] ≈ 2.0 # ∂(u[1]*t)/∂t = u[1] end - From d2b5b6d57658b1ccfb164f1f5aca1e140e91c07d Mon Sep 17 00:00:00 2001 From: Chris Rackauckas - Beep Boop Edition Date: Sun, 7 Jun 2026 10:30:19 +0000 Subject: [PATCH 47/55] ci: add standard centralized workflows (auto-format, dependabot-automerge, docs-preview-cleanup) (#55) Adds the standard SciML centralized caller workflows that were missing: - RunicSuggestions.yml (Runic auto-format suggestions on PRs) - DependabotAutoMerge.yml (auto-merge Dependabot PRs) - DocPreviewCleanup.yml (clean up doc previews on PR close) Co-authored-by: ChrisRackauckas-Claude Co-authored-by: Chris Rackauckas --- .github/workflows/DependabotAutoMerge.yml | 9 +++++++++ .github/workflows/DocPreviewCleanup.yml | 10 ++++++++++ .github/workflows/RunicSuggestions.yml | 9 +++++++++ 3 files changed, 28 insertions(+) create mode 100644 .github/workflows/DependabotAutoMerge.yml create mode 100644 .github/workflows/DocPreviewCleanup.yml create mode 100644 .github/workflows/RunicSuggestions.yml diff --git a/.github/workflows/DependabotAutoMerge.yml b/.github/workflows/DependabotAutoMerge.yml new file mode 100644 index 0000000..44b814c --- /dev/null +++ b/.github/workflows/DependabotAutoMerge.yml @@ -0,0 +1,9 @@ +name: "Dependabot Auto-merge" +on: pull_request +permissions: + contents: write + pull-requests: write +jobs: + automerge: + uses: "SciML/.github/.github/workflows/dependabot-automerge.yml@v1" + secrets: "inherit" diff --git a/.github/workflows/DocPreviewCleanup.yml b/.github/workflows/DocPreviewCleanup.yml new file mode 100644 index 0000000..960a549 --- /dev/null +++ b/.github/workflows/DocPreviewCleanup.yml @@ -0,0 +1,10 @@ +name: "Doc Preview Cleanup" +on: + pull_request: + types: [closed] +permissions: + contents: write +jobs: + cleanup: + uses: "SciML/.github/.github/workflows/docs-preview-cleanup.yml@v1" + secrets: "inherit" diff --git a/.github/workflows/RunicSuggestions.yml b/.github/workflows/RunicSuggestions.yml new file mode 100644 index 0000000..43c0768 --- /dev/null +++ b/.github/workflows/RunicSuggestions.yml @@ -0,0 +1,9 @@ +name: "Runic Suggestions" +on: pull_request +permissions: + contents: read + pull-requests: write +jobs: + runic-suggestions: + uses: "SciML/.github/.github/workflows/runic-suggestions-on-pr.yml@v1" + secrets: "inherit" From 54eb5025658f734582ff9632da4d274ffdeb449c Mon Sep 17 00:00:00 2001 From: Chris Rackauckas - Beep Boop Edition Date: Mon, 8 Jun 2026 15:14:41 +0000 Subject: [PATCH 48/55] Canonical CI: grouped-tests.yml + root test/test_groups.toml (#57) Convert the root Tests.yml test job to the canonical thin caller of SciML/.github grouped-tests.yml@v1, with the group x version matrix declared once in test/test_groups.toml. The runtests.jl GROUP dispatch (Core/nopre) already exists, so no test-runner change is needed. The emitted root matrix reproduces the old hand-maintained matrix exactly (5/5): Core on {lts,1,pre} and nopre on {lts,1}. Co-authored-by: ChrisRackauckas-Claude Co-authored-by: Claude Opus 4.8 (1M context) --- .github/workflows/Tests.yml | 24 ++---------------------- test/test_groups.toml | 5 +++++ 2 files changed, 7 insertions(+), 22 deletions(-) create mode 100644 test/test_groups.toml diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 9d5899a..2722dcc 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -18,26 +18,6 @@ concurrency: cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch || github.ref != 'refs/tags/v*' }} jobs: - test: - name: "Tests" - strategy: - fail-fast: false - matrix: - group: - - Core - - nopre - version: - - '1' - - 'lts' - - 'pre' - arch: - - x64 - exclude: - - version: 'pre' - group: 'nopre' - uses: "SciML/.github/.github/workflows/tests.yml@v1" - with: - group: "${{ matrix.group }}" - julia-version: "${{ matrix.version }}" - julia-arch: "${{ matrix.arch }}" + tests: + uses: "SciML/.github/.github/workflows/grouped-tests.yml@v1" secrets: "inherit" diff --git a/test/test_groups.toml b/test/test_groups.toml new file mode 100644 index 0000000..28a2fda --- /dev/null +++ b/test/test_groups.toml @@ -0,0 +1,5 @@ +[Core] +versions = ["lts", "1", "pre"] + +[nopre] +versions = ["lts", "1"] From 8475c49e30f0bb4fd79b47dddfc2a1a477301fd7 Mon Sep 17 00:00:00 2001 From: Chris Rackauckas - Beep Boop Edition Date: Mon, 8 Jun 2026 15:40:51 +0000 Subject: [PATCH 49/55] Canonical CI cleanup: TagBot thin-caller + downgrade-caller cleanup (#56) - Replace inlined JuliaRegistries/TagBot with canonical SciML/.github tagbot.yml@v1 thin caller - Remove stdlib-only skip: "Pkg,TOML" from Downgrade caller (centralized workflow auto-populates skip) Co-authored-by: Chris Rackauckas Co-authored-by: Claude Opus 4.8 (1M context) --- .github/workflows/Downgrade.yml | 1 - .github/workflows/TagBot.yml | 16 ++-- .typos.toml | 143 ++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 12 deletions(-) create mode 100644 .typos.toml diff --git a/.github/workflows/Downgrade.yml b/.github/workflows/Downgrade.yml index 95f1cb1..ee944a5 100644 --- a/.github/workflows/Downgrade.yml +++ b/.github/workflows/Downgrade.yml @@ -29,5 +29,4 @@ jobs: with: group: "${{ matrix.group }}" julia-version: "lts" - skip: "Pkg,TOML" secrets: "inherit" diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml index f49313b..9ed2d3e 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -1,15 +1,9 @@ -name: TagBot +name: "TagBot" on: issue_comment: - types: - - created + types: [created] workflow_dispatch: jobs: - TagBot: - if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' - runs-on: ubuntu-latest - steps: - - uses: JuliaRegistries/TagBot@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - ssh: ${{ secrets.DOCUMENTER_KEY }} + tagbot: + uses: "SciML/.github/.github/workflows/tagbot.yml@v1" + secrets: "inherit" diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..cabb966 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,143 @@ +[default.extend-words] +# Julia-specific functions +indexin = "indexin" +findfirst = "findfirst" +findlast = "findlast" +eachindex = "eachindex" +setp = "setp" +getp = "getp" +setu = "setu" +getu = "getu" + +# Mathematical/scientific terms +jacobian = "jacobian" +hessian = "hessian" +eigenvalue = "eigenvalue" +eigenvector = "eigenvector" +discretization = "discretization" +linearization = "linearization" +parameterized = "parameterized" +discretized = "discretized" +vectorized = "vectorized" +extrapolant = "extrapolant" +# Person names in citations - these are correct spellings +Sigal = "Sigal" # Sigal Gottlieb is a real person/author +Steinebach = "Steinebach" +Protopapa = "Protopapa" +Hairer = "Hairer" +Wanner = "Wanner" +Verwer = "Verwer" +Sandu = "Sandu" +Carmichael = "Carmichael" +Potra = "Potra" +Seinfeld = "Seinfeld" +Dabdub = "Dabdub" +Marzo = "Marzo" +Loon = "Loon" +RODASP = "RODASP" +Rodas4P = "Rodas4P" +Rodas5P = "Rodas5P" +Rodas6P = "Rodas6P" +Scholz = "Scholz" +Veldhuizen = "Veldhuizen" +Tsit5DA = "Tsit5DA" +Kaps = "Kaps" +Rentrop = "Rentrop" +Rang = "Rang" +Darmstadt = "Darmstadt" +"Méthodes" = "Méthodes" +"différentiels" = "différentiels" +"algébriques" = "algébriques" +"problèmes" = "problèmes" +problemes = "problemes" # French word in citation without accent +Ono = "Ono" # Hiroshi Ono, author of Ono10/Ono12 RK methods +padd = "padd" # variable name in analyticless_convergence_tests.jl +Preprint = "Preprint" +Nr = "Nr" +Wouver = "Wouver" +Sauces = "Sauces" +Schiesser = "Schiesser" +Vande = "Vande" +BIT = "BIT" +Numer = "Numer" +Jcon = "Jcon" +arXiv = "arXiv" + +# (Numer already declared above for "BIT Numer" context.) + +# Technical variable names +dorder = "dorder" # Parameter for delta order / order change + + +# Common variable patterns in Julia/SciML +ists = "ists" +ispcs = "ispcs" +osys = "osys" +rsys = "rsys" +usys = "usys" +fsys = "fsys" +eqs = "eqs" +rhs = "rhs" +lhs = "lhs" +ode = "ode" +pde = "pde" +sde = "sde" +dde = "dde" +bvp = "bvp" +ivp = "ivp" + +# Common abbreviations +tol = "tol" +rtol = "rtol" +atol = "atol" +idx = "idx" +jdx = "jdx" +prev = "prev" +curr = "curr" +init = "init" +tmp = "tmp" +vec = "vec" +arr = "arr" +dt = "dt" +du = "du" +dx = "dx" +dy = "dy" +dz = "dz" + +# Algorithm/type suffixes +alg = "alg" +prob = "prob" +sol = "sol" +cb = "cb" +opts = "opts" +args = "args" +kwargs = "kwargs" + +# Scientific abbreviations +ND = "ND" +nd = "nd" +MTK = "MTK" +ODE = "ODE" +PDE = "PDE" +SDE = "SDE" + +# SDE-specific terms +IIF = "IIF" # Integrating Factor method (IIF1M, IIF2M, IIF1Mil) +iif = "iif" +currate = "currate" # current rate variable in tau-leaping +SIE = "SIE" # SIEA/SIEB algorithm name prefix +SME = "SME" # SME algorithm name (Stochastic Modified Euler) +resetted = "resetted" # variable name in stepsize control +strat = "strat" # Stratonovich abbreviation + +# Variable names used in codebase +yhat = "yhat" # estimated y value +accpt = "accpt" # accepted step flag variable +gam = "gam" # gamma abbreviation in tableaux +clos = "clos" # closure abbreviation + +# Journal abbreviations in BibTeX citations +Comput = "Comput" # J. Comput. Phys. (Journal of Computational Physics) + +# Julia spelling convention +inferrable = "inferrable" # Julia uses "inferrable" not "inferable" From afd0fe96cb39b11f39338bdbc4d4655922d551d6 Mon Sep 17 00:00:00 2001 From: Chris Rackauckas - Beep Boop Edition Date: Sun, 14 Jun 2026 23:54:29 +0000 Subject: [PATCH 50/55] Use SciMLTesting v1.2 run_tests (#58) Convert test/runtests.jl to SciMLTesting v1.2. This repo needs the explicit-args form (not bare folder-discovery): the Enzyme/Mooncake test groups run only under "All" yet are not declared in test_groups.toml, and the BigFloat/UnionAll testsets must run under both Core and nopre. Folder-discovery cannot express either, so runtests.jl uses run_tests with explicit core/groups bodies and a curated all = ["Core", "Enzyme", "Mooncake"]. Layout: - Core = test/basictests.jl + test/shared/bigfloat_unionall_tests.jl - nopre = test/nopre/jet_tests.jl (sub-env) + the shared BigFloat/UnionAll file - Enzyme = test/Enzyme/enzyme_tests.jl (main env) - Mooncake = test/Mooncake/mooncake_tests.jl (main env) Behavior-preserving for every CI-reachable GROUP (verified locally on Julia 1.11 via Pkg.test): - Core -> basictests (48) + BigFloat/UnionAll (7) - nopre -> JET (7) + BigFloat/UnionAll (7) - All -> basictests (48) + BigFloat/UnionAll (7, once) + Enzyme (65) + Mooncake (13) Deps: add SciMLTesting + SafeTestsets to the root test target and the nopre sub-env; drop the now-unused Pkg test extra (the harness owns all Pkg ops). Removed the stale committed test/nopre/Manifest.toml (regenerated at run time by the harness's Pkg.develop + instantiate). test/test_groups.toml unchanged. Co-authored-by: ChrisRackauckas-Claude Co-authored-by: Claude Opus 4.8 (1M context) --- Project.toml | 7 +- test/{ => Enzyme}/enzyme_tests.jl | 0 test/{ => Mooncake}/mooncake_tests.jl | 0 test/nopre/Manifest.toml | 261 ------------------------- test/nopre/Project.toml | 10 + test/runtests.jl | 96 +++------ test/shared/bigfloat_unionall_tests.jl | 41 ++++ 7 files changed, 85 insertions(+), 330 deletions(-) rename test/{ => Enzyme}/enzyme_tests.jl (100%) rename test/{ => Mooncake}/mooncake_tests.jl (100%) delete mode 100644 test/nopre/Manifest.toml create mode 100644 test/shared/bigfloat_unionall_tests.jl diff --git a/Project.toml b/Project.toml index 6b3c56c..c60afb7 100644 --- a/Project.toml +++ b/Project.toml @@ -23,6 +23,8 @@ EnzymeCore = "0.8" FunctionWrappers = "1" Mooncake = "0.5" PrecompileTools = "1" +SafeTestsets = "0.1, 1" +SciMLTesting = "1" TruncatedStacktraces = "1" julia = "1.10" @@ -30,8 +32,9 @@ julia = "1.10" Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" EnzymeCore = "f151be2c-9106-41f4-ab19-57ee4f262869" Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" -Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" +SciMLTesting = "09d9d899-5365-40a9-917a-5f67fddea283" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Pkg", "Test", "Enzyme", "EnzymeCore", "Mooncake"] +test = ["Test", "SafeTestsets", "SciMLTesting", "Enzyme", "EnzymeCore", "Mooncake"] diff --git a/test/enzyme_tests.jl b/test/Enzyme/enzyme_tests.jl similarity index 100% rename from test/enzyme_tests.jl rename to test/Enzyme/enzyme_tests.jl diff --git a/test/mooncake_tests.jl b/test/Mooncake/mooncake_tests.jl similarity index 100% rename from test/mooncake_tests.jl rename to test/Mooncake/mooncake_tests.jl diff --git a/test/nopre/Manifest.toml b/test/nopre/Manifest.toml deleted file mode 100644 index b801b1b..0000000 --- a/test/nopre/Manifest.toml +++ /dev/null @@ -1,261 +0,0 @@ -# This file is machine-generated - editing it directly is not advised - -julia_version = "1.12.4" -manifest_format = "2.0" -project_hash = "fc7db7725f4c12cf05bc39dfb6855e7476e54e38" - -[[deps.ArgTools]] -uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" -version = "1.1.2" - -[[deps.Artifacts]] -uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" -version = "1.11.0" - -[[deps.Base64]] -uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" -version = "1.11.0" - -[[deps.CodeTracking]] -deps = ["InteractiveUtils", "UUIDs"] -git-tree-sha1 = "b7231a755812695b8046e8471ddc34c8268cbad5" -uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" -version = "3.0.0" - -[[deps.Compiler]] -git-tree-sha1 = "382d79bfe72a406294faca39ef0c3cef6e6ce1f1" -uuid = "807dbc54-b67e-4c79-8afb-eafe4df6f2e1" -version = "0.1.1" - -[[deps.CompilerSupportLibraries_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "1.3.0+1" - -[[deps.Dates]] -deps = ["Printf"] -uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" -version = "1.11.0" - -[[deps.Downloads]] -deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] -uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -version = "1.7.0" - -[[deps.FileWatching]] -uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" -version = "1.11.0" - -[[deps.FunctionWrappers]] -git-tree-sha1 = "d62485945ce5ae9c0c48f124a84998d755bae00e" -uuid = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" -version = "1.1.3" - -[[deps.FunctionWrappersWrappers]] -deps = ["FunctionWrappers", "PrecompileTools", "TruncatedStacktraces"] -path = "/home/crackauc/sandbox/tmp_20260108_090820_85201" -uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" -version = "0.1.4" - -[[deps.InteractiveUtils]] -deps = ["Markdown"] -uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -version = "1.11.0" - -[[deps.JET]] -deps = ["CodeTracking", "Compiler", "InteractiveUtils", "JuliaInterpreter", "JuliaSyntax", "LoweredCodeUtils", "MacroTools", "Pkg", "PrecompileTools", "Preferences", "Revise", "Test"] -git-tree-sha1 = "3642228b0d4ab0b263a5946f60ace6c2b5616256" -uuid = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" -version = "0.11.3" - -[[deps.JuliaInterpreter]] -deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"] -git-tree-sha1 = "80580012d4ed5a3e8b18c7cd86cebe4b816d17a6" -uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" -version = "0.10.9" - -[[deps.JuliaSyntax]] -git-tree-sha1 = "0d4b3dab95018bcf3925204475693d9f09dc45b8" -uuid = "70703baa-626e-46a2-a12c-08ffd08c73b4" -version = "1.0.2" - -[[deps.JuliaSyntaxHighlighting]] -deps = ["StyledStrings"] -uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" -version = "1.12.0" - -[[deps.LibCURL]] -deps = ["LibCURL_jll", "MozillaCACerts_jll"] -uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" -version = "0.6.4" - -[[deps.LibCURL_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] -uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" -version = "8.15.0+0" - -[[deps.LibGit2]] -deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] -uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" -version = "1.11.0" - -[[deps.LibGit2_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"] -uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" -version = "1.9.0+0" - -[[deps.LibSSH2_jll]] -deps = ["Artifacts", "Libdl", "OpenSSL_jll"] -uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" -version = "1.11.3+1" - -[[deps.Libdl]] -uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" -version = "1.11.0" - -[[deps.Logging]] -uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" -version = "1.11.0" - -[[deps.LoweredCodeUtils]] -deps = ["CodeTracking", "Compiler", "JuliaInterpreter"] -git-tree-sha1 = "65ae3db6ab0e5b1b5f217043c558d9d1d33cc88d" -uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b" -version = "3.5.0" - -[[deps.MacroTools]] -git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522" -uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" -version = "0.5.16" - -[[deps.Markdown]] -deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] -uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" -version = "1.11.0" - -[[deps.MozillaCACerts_jll]] -uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2025.11.4" - -[[deps.NetworkOptions]] -uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" -version = "1.3.0" - -[[deps.OpenSSL_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.5.4+0" - -[[deps.OrderedCollections]] -git-tree-sha1 = "05868e21324cede2207c6f0f466b4bfef6d5e7ee" -uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" -version = "1.8.1" - -[[deps.Pkg]] -deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] -uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.12.1" -weakdeps = ["REPL"] - - [deps.Pkg.extensions] - REPLExt = "REPL" - -[[deps.PrecompileTools]] -deps = ["Preferences"] -git-tree-sha1 = "07a921781cab75691315adc645096ed5e370cb77" -uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -version = "1.3.3" - -[[deps.Preferences]] -deps = ["TOML"] -git-tree-sha1 = "522f093a29b31a93e34eaea17ba055d850edea28" -uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.5.1" - -[[deps.Printf]] -deps = ["Unicode"] -uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" -version = "1.11.0" - -[[deps.REPL]] -deps = ["InteractiveUtils", "JuliaSyntaxHighlighting", "Markdown", "Sockets", "StyledStrings", "Unicode"] -uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" -version = "1.11.0" - -[[deps.Random]] -deps = ["SHA"] -uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -version = "1.11.0" - -[[deps.Revise]] -deps = ["CodeTracking", "FileWatching", "InteractiveUtils", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"] -git-tree-sha1 = "dfd6fab9aa325b382773b8930998ce84c2918e5e" -uuid = "295af30f-e4ad-537b-8983-00126c2a3abe" -version = "3.13.1" - - [deps.Revise.extensions] - DistributedExt = "Distributed" - - [deps.Revise.weakdeps] - Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" - -[[deps.SHA]] -uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" -version = "0.7.0" - -[[deps.Serialization]] -uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" -version = "1.11.0" - -[[deps.Sockets]] -uuid = "6462fe0b-24de-5631-8697-dd941f90decc" -version = "1.11.0" - -[[deps.StyledStrings]] -uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" -version = "1.11.0" - -[[deps.TOML]] -deps = ["Dates"] -uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" -version = "1.0.3" - -[[deps.Tar]] -deps = ["ArgTools", "SHA"] -uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" -version = "1.10.0" - -[[deps.Test]] -deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] -uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -version = "1.11.0" - -[[deps.TruncatedStacktraces]] -deps = ["InteractiveUtils", "MacroTools", "Preferences"] -git-tree-sha1 = "ea3e54c2bdde39062abf5a9758a23735558705e1" -uuid = "781d530d-4396-4725-bb49-402e4bee1e77" -version = "1.4.0" - -[[deps.UUIDs]] -deps = ["Random", "SHA"] -uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" -version = "1.11.0" - -[[deps.Unicode]] -uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" -version = "1.11.0" - -[[deps.Zlib_jll]] -deps = ["Libdl"] -uuid = "83775a58-1f1d-513f-b197-d71354ab007a" -version = "1.3.1+2" - -[[deps.nghttp2_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" -version = "1.64.0+1" - -[[deps.p7zip_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] -uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" -version = "17.7.0+0" diff --git a/test/nopre/Project.toml b/test/nopre/Project.toml index 0b4fbf6..2021cec 100644 --- a/test/nopre/Project.toml +++ b/test/nopre/Project.toml @@ -1,4 +1,14 @@ [deps] FunctionWrappersWrappers = "77dc65aa-8811-40c2-897b-53d922fa7daf" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" +SciMLTesting = "09d9d899-5365-40a9-917a-5f67fddea283" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[sources] +FunctionWrappersWrappers = {path = "../.."} + +[compat] +SafeTestsets = "0.0.1, 0.1" +SciMLTesting = "1" +Test = "1" diff --git a/test/runtests.jl b/test/runtests.jl index 0018a99..624b377 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,67 +1,29 @@ -using Test, Pkg - -const GROUP = get(ENV, "GROUP", "All") - -if GROUP == "All" || GROUP == "Core" - @testset "FunctionWrappersWrappers.jl" begin - include("basictests.jl") - end -end - -if GROUP == "nopre" - Pkg.activate("nopre") - Pkg.develop(PackageSpec(path = dirname(@__DIR__))) - Pkg.instantiate() - include("nopre/jet_tests.jl") -end - -@testset "BigFloat support" begin - fwplus_big = FunctionWrappersWrapper( - +, - (Tuple{BigFloat, BigFloat}, Tuple{Float64, Float64}), - (BigFloat, Float64) - ) - a = BigFloat("3.14159265358979323846264338327950288") - b = BigFloat("2.71828182845904523536028747135266250") - @test fwplus_big(a, b) isa BigFloat - @test fwplus_big(a, b) == a + b - @test fwplus_big(1.0, 2.0) === 3.0 - - fwsin_big = FunctionWrappersWrapper( - sin, - (Tuple{BigFloat}, Tuple{Float64}), - (BigFloat, Float64) - ) - @test fwsin_big(BigFloat("1.0")) isa BigFloat - @test fwsin_big(1.0) === sin(1.0) -end - -@testset "UnionAll return types" begin - # Test that UnionAll types (like AbstractArray{Float64}) work as return types - function double_array(x::AbstractArray{Float64}) - return x .* 2 - end - - fwdouble = FunctionWrappersWrapper( - double_array, - (Tuple{AbstractArray{Float64}},), - (AbstractArray{Float64},) - ) - - v = [1.0, 2.0, 3.0] - result = fwdouble(v) - @test result isa Vector{Float64} - @test result == [2.0, 4.0, 6.0] -end - -if GROUP == "All" || GROUP == "Enzyme" - @testset "Enzyme extension" begin - include("enzyme_tests.jl") - end -end - -if GROUP == "All" || GROUP == "Mooncake" - @testset "Mooncake extension" begin - include("mooncake_tests.jl") - end -end +using SafeTestsets +using SciMLTesting + +run_tests(; + core = function () + @safetestset "FunctionWrappersWrappers.jl" begin + include(joinpath(@__DIR__, "basictests.jl")) + end + return @safetestset "BigFloat + UnionAll" begin + include(joinpath(@__DIR__, "shared", "bigfloat_unionall_tests.jl")) + end + end, + groups = Dict( + "nopre" => (; + env = joinpath(@__DIR__, "nopre"), + body = function () + @safetestset "JET" begin + include(joinpath(@__DIR__, "nopre", "jet_tests.jl")) + end + return @safetestset "BigFloat + UnionAll" begin + include(joinpath(@__DIR__, "shared", "bigfloat_unionall_tests.jl")) + end + end, + ), + "Enzyme" => joinpath(@__DIR__, "Enzyme", "enzyme_tests.jl"), + "Mooncake" => joinpath(@__DIR__, "Mooncake", "mooncake_tests.jl"), + ), + all = ["Core", "Enzyme", "Mooncake"], +) diff --git a/test/shared/bigfloat_unionall_tests.jl b/test/shared/bigfloat_unionall_tests.jl new file mode 100644 index 0000000..de091c5 --- /dev/null +++ b/test/shared/bigfloat_unionall_tests.jl @@ -0,0 +1,41 @@ +using FunctionWrappersWrappers +using Test + +@testset "BigFloat support" begin + fwplus_big = FunctionWrappersWrapper( + +, + (Tuple{BigFloat, BigFloat}, Tuple{Float64, Float64}), + (BigFloat, Float64) + ) + a = BigFloat("3.14159265358979323846264338327950288") + b = BigFloat("2.71828182845904523536028747135266250") + @test fwplus_big(a, b) isa BigFloat + @test fwplus_big(a, b) == a + b + @test fwplus_big(1.0, 2.0) === 3.0 + + fwsin_big = FunctionWrappersWrapper( + sin, + (Tuple{BigFloat}, Tuple{Float64}), + (BigFloat, Float64) + ) + @test fwsin_big(BigFloat("1.0")) isa BigFloat + @test fwsin_big(1.0) === sin(1.0) +end + +@testset "UnionAll return types" begin + # Test that UnionAll types (like AbstractArray{Float64}) work as return types + function double_array(x::AbstractArray{Float64}) + return x .* 2 + end + + fwdouble = FunctionWrappersWrapper( + double_array, + (Tuple{AbstractArray{Float64}},), + (AbstractArray{Float64},) + ) + + v = [1.0, 2.0, 3.0] + result = fwdouble(v) + @test result isa Vector{Float64} + @test result == [2.0, 4.0, 6.0] +end From 9c11a855700a267b13df3df36fddd52e7f0fe66f Mon Sep 17 00:00:00 2001 From: Chris Rackauckas - Beep Boop Edition Date: Sun, 21 Jun 2026 16:18:01 +0000 Subject: [PATCH 51/55] Fix downgrade CI: split the AD test group out of the downgrade lane (#59) * Raise FunctionWrappers compat floor to 1.1.3 to fix downgrade CI The Downgrade (Core) job was failing with an Unsatisfiable resolution on Julia lts (1.10). When julia-downgrade-compat pins every dep to its lowest allowed version, FunctionWrappers was pinned to 1.0.0 (floor of "1"), but Mooncake (a tested weakdep/extension target) requires FunctionWrappers >= 1.1.3. That made the merged test environment unsatisfiable. Raising the FunctionWrappers compat floor from "1" to "1.1.3" matches the version actually exercised by the test extras and makes the minimum-version resolution satisfiable. Verified locally on Julia 1.10: the merged test project now resolves at minimum versions, the package loads, and basictests pass with FunctionWrappers 1.1.3. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.8 (1M context) * Fix downgrade CI: run downgrade leg on Julia 1.11 + raise PrecompileTools/TruncatedStacktraces floors The Downgrade (Core) job was red on lts (Julia 1.10). Three distinct, genuine problems, fixed at their causes: 1. Unsatisfiable resolution. The resolver (julia-downgrade-compat -> Resolver.jl) minimizes Julia to the floor of its --julia range; on lts that range (1.10) admits 1.10.0. But every Mooncake 0.5.x test extra requires julia >= 1.10.8, so Mooncake can never be placed -> Unsatisfiable. Even at 1.10.8 the Enzyme 0.13 + EnzymeCore 0.8 + Mooncake 0.5 minimum-version set has no solution on the 1.10 LTS stdlib line; it resolves only once Julia reaches 1.11.6 (Mooncake 0.5's 1.11 floor). Resolver.jl ignores the project's [compat] julia for the Julia version itself, so no Project.toml change can force julia >= 1.10.8 -- the caller must request it. Fix: pin the Downgrade caller to julia-version "1.11" (resolver picks 1.11.6). Core 1.10 support is still exercised by the regular Tests workflow. 2. PrecompileTools floor too low. With resolution fixed, the minimum PrecompileTools 1.0.0 fails to precompile GPUCompiler (pulled transitively by Enzyme) on Julia 1.11: "UndefVarError: PrecompileTools not defined in GPUCompiler". GPUCompiler's compat says PrecompileTools "1", but 1.0.0 does not actually provide the API GPUCompiler uses; 1.1.0 does. Raise the floor to "1.1". 3. TruncatedStacktraces floor too low. src uses TruncatedStacktraces.@truncate_stacktrace, which does not exist before v1.1.0 ("UndefVarError: @truncate_stacktrace not defined in TruncatedStacktraces" at the v1.0.0 floor). Raise the floor to "1.1". Also reverts the earlier misdiagnosed FunctionWrappers floor raise (1 -> 1.1.3): no registered test dep constrains FunctionWrappers and the source only uses the FunctionWrapper{R,A} constructor available since 1.0.0. Version bumped to 1.9.2 for the compat-floor changes. Local verification on Julia 1.11 (the new downgrade leg): the actual julia-downgrade-compat downgrade.jl resolves the merged test project (exit 0, "Successfully resolved minimal versions"); the downgraded manifest (Enzyme 0.13.0 / GPUCompiler 0.27.5 / PrecompileTools 1.1.0 / TruncatedStacktraces 1.1.0 / Mooncake 0.5.0) precompiles cleanly; and Pkg.test GROUP=Core passes (FunctionWrappersWrappers.jl 48/48, BigFloat + UnionAll 7/7). Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.8 (1M context) * Split AD test group out of the downgrade lane The Downgrade (Core) red was caused by the AD test extras (Enzyme, EnzymeCore, Mooncake) being part of the root test target. The downgrade resolver (julia-downgrade-compat -> Resolver.jl) minimizes Julia to the floor of its --julia range, but every Mooncake 0.5.x requires julia >= 1.10.8 and the Enzyme 0.13 + EnzymeCore 0.8 + Mooncake 0.5 floor set has no minimum-version solution on the 1.10 LTS line, so the merged test env was Unsatisfiable (the Resolver.jl <-> Mooncake wall, StefanKarpinski/Resolver.jl#24). The AD deps shouldn't gate the downgrade lane. Fix: move the AD test deps into their own per-group sub-environments (test/Enzyme/Project.toml, test/Mooncake/Project.toml) and drop them from the root [extras]/[targets].test. SciMLTesting's run_tests already supports per-group sub-envs (the `env = ...` group form, as the nopre group uses); the Enzyme/Mooncake groups now declare their own env. The downgrade job runs GROUP=Core, which resolves only the root test target -- now AD-free -- so it is satisfiable on lts (Julia 1.10) again. The AD groups are added to test/test_groups.toml so they run as their own jobs in normal CI (lts, 1); previously they were declared in runtests.jl but had no matrix entry and so never ran. Reverts the #59 accommodations that masked the AD-resolution problem: - .github/workflows/Downgrade.yml: julia-version back to "lts" (no longer needs to be pinned to 1.11 to dodge the AD floor set). - PrecompileTools compat floor back to "1": the 1.0.0 GPUCompiler precompile failure only arose because Enzyme pulled GPUCompiler into the downgrade env; with AD out of that env it no longer applies. Kept, because they are genuine minimum-version bugs independent of AD (both verified locally on Julia 1.10): - TruncatedStacktraces = "1.1": src uses @truncate_stacktrace, which does not exist before v1.1.0 (1.0.0 is a 25-line stub). - FunctionWrappers = "1.1.2": 1.0.0 / 1.1.0 / 1.1.1 fail to build the FunctionWrapper cfunction ("Module IR does not contain specified entry function") on Julia 1.10; 1.1.2 is the lowest that builds and runs. This is exercised by the package's own PrecompileTools @compile_workload, so it is a real floor regardless of the AD split. Local verification (Julia 1.10/lts, the actual downgrade leg): - julia-downgrade-compat downgrade.jl: exit 0, "Successfully resolved minimal versions for . (with extras)"; downgraded manifest contains no Enzyme/Mooncake/GPUCompiler (FunctionWrappers 1.1.2, PrecompileTools 1.0.0, TruncatedStacktraces 1.1.0, SafeTestsets 0.1.0, SciMLTesting 1.0.0). - Pkg.test GROUP=Core with allow_reresolve=false, force_latest_compatible_version=false (the julia-runtest CI settings): precompiles cleanly and passes (FunctionWrappersWrappers.jl 48/48, BigFloat + UnionAll 7/7). - The AD groups still run via their sub-envs: GROUP=Enzyme 65/65, GROUP=Mooncake 13/13 (Julia 1.11), with the Enzyme/Mooncake extensions precompiling in-group. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: ChrisRackauckas-Claude Co-authored-by: Claude Opus 4.8 (1M context) --- Project.toml | 11 ++++------- test/Enzyme/Project.toml | 15 +++++++++++++++ test/Mooncake/Project.toml | 13 +++++++++++++ test/runtests.jl | 10 ++++++++-- test/test_groups.toml | 6 ++++++ 5 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 test/Enzyme/Project.toml create mode 100644 test/Mooncake/Project.toml diff --git a/Project.toml b/Project.toml index c60afb7..f139297 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FunctionWrappersWrappers" uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" authors = ["Chris Elrod and contributors"] -version = "1.9.1" +version = "1.9.2" [deps] FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" @@ -20,21 +20,18 @@ FunctionWrappersWrappersMooncakeExt = "Mooncake" [compat] Enzyme = "0.13" EnzymeCore = "0.8" -FunctionWrappers = "1" +FunctionWrappers = "1.1.2" Mooncake = "0.5" PrecompileTools = "1" SafeTestsets = "0.1, 1" SciMLTesting = "1" -TruncatedStacktraces = "1" +TruncatedStacktraces = "1.1" julia = "1.10" [extras] -Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" -EnzymeCore = "f151be2c-9106-41f4-ab19-57ee4f262869" -Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" SciMLTesting = "09d9d899-5365-40a9-917a-5f67fddea283" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test", "SafeTestsets", "SciMLTesting", "Enzyme", "EnzymeCore", "Mooncake"] +test = ["Test", "SafeTestsets", "SciMLTesting"] diff --git a/test/Enzyme/Project.toml b/test/Enzyme/Project.toml new file mode 100644 index 0000000..a36c643 --- /dev/null +++ b/test/Enzyme/Project.toml @@ -0,0 +1,15 @@ +[deps] +Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" +EnzymeCore = "f151be2c-9106-41f4-ab19-57ee4f262869" +FunctionWrappersWrappers = "77dc65aa-8811-40c2-897b-53d922fa7daf" +SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[sources] +FunctionWrappersWrappers = {path = "../.."} + +[compat] +Enzyme = "0.13" +EnzymeCore = "0.8" +SafeTestsets = "0.0.1, 0.1, 1" +Test = "1" diff --git a/test/Mooncake/Project.toml b/test/Mooncake/Project.toml new file mode 100644 index 0000000..b90014b --- /dev/null +++ b/test/Mooncake/Project.toml @@ -0,0 +1,13 @@ +[deps] +FunctionWrappersWrappers = "77dc65aa-8811-40c2-897b-53d922fa7daf" +Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" +SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[sources] +FunctionWrappersWrappers = {path = "../.."} + +[compat] +Mooncake = "0.5" +SafeTestsets = "0.0.1, 0.1, 1" +Test = "1" diff --git a/test/runtests.jl b/test/runtests.jl index 624b377..232bc9f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -22,8 +22,14 @@ run_tests(; end end, ), - "Enzyme" => joinpath(@__DIR__, "Enzyme", "enzyme_tests.jl"), - "Mooncake" => joinpath(@__DIR__, "Mooncake", "mooncake_tests.jl"), + "Enzyme" => (; + env = joinpath(@__DIR__, "Enzyme"), + body = joinpath(@__DIR__, "Enzyme", "enzyme_tests.jl"), + ), + "Mooncake" => (; + env = joinpath(@__DIR__, "Mooncake"), + body = joinpath(@__DIR__, "Mooncake", "mooncake_tests.jl"), + ), ), all = ["Core", "Enzyme", "Mooncake"], ) diff --git a/test/test_groups.toml b/test/test_groups.toml index 28a2fda..2568f58 100644 --- a/test/test_groups.toml +++ b/test/test_groups.toml @@ -3,3 +3,9 @@ versions = ["lts", "1", "pre"] [nopre] versions = ["lts", "1"] + +[Enzyme] +versions = ["lts", "1"] + +[Mooncake] +versions = ["lts", "1"] From 0cd76d686a076664ed5e9c68568c12e3ac08cd47 Mon Sep 17 00:00:00 2001 From: densmojd <93928435+densmojd@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:00:33 -0400 Subject: [PATCH 52/55] Implicity convert function into FunctionWrappersWrapper (#54) * Added FunctionWrappersWrapper constructor for the case where type parameters are specified but only the function to be wrapped is provied as an argument. * Changed arguments types in test from Int to Float32 because calculating exp and cos of integer really doesn't make sense. Actually check the value of the wrapped cos. * Added specializations of Base.convert for FunctionWrappersWrapper based on new constructor. * Added a docstring for new constructor * Fixed formatting --- src/FunctionWrappersWrappers.jl | 21 +++++++++++++++++++++ test/basictests.jl | 19 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/FunctionWrappersWrappers.jl b/src/FunctionWrappersWrappers.jl index f358ee8..8b5ef0a 100644 --- a/src/FunctionWrappersWrappers.jl +++ b/src/FunctionWrappersWrappers.jl @@ -111,6 +111,25 @@ end TruncatedStacktraces.@truncate_stacktrace FunctionWrappersWrapper +""" + FunctionWrappersWrapper{FW, P, CS}(f) + +Create a `FunctionWrappersWrapper` when the type parameters are specified. + +# Arguments +- `f`: The function to wrap + +# Type parameters +- `FW`: Tuple type of `FunctionWrapper`s +- `P`: Fallback policy (`Strict`, `AllowAll`, or `AllowNonIsBits`) +- `CS`: Cache storage type (`NoCacheStorage`, `SingleCacheStorage`, `DictCacheStorage`) +""" +function FunctionWrappersWrapper{FW, P, CS}(f) where {K, FW <: NTuple{K, Any}, P, CS} + fw = ntuple(i -> FW.parameters[i](f), Val(K)) + cs = CS() + return FunctionWrappersWrapper{FW, P, CS}(fw, cs) +end + """ FunctionWrappersWrapper(f, argtypes, rettypes; cache=SingleCache(), policy=AllowNonIsBits()) @@ -137,6 +156,8 @@ function FunctionWrappersWrapper( return FunctionWrappersWrapper{typeof(fwt), typeof(policy), typeof(cs)}(fwt, cs) end +Base.convert(::Type{T}, obj) where {T <: FunctionWrappersWrapper} = T(obj) +Base.convert(::Type{T}, obj::T) where {T <: FunctionWrappersWrapper} = obj # ============================================================================ # Call dispatch — entry point diff --git a/test/basictests.jl b/test/basictests.jl index 592049f..a469614 100644 --- a/test/basictests.jl +++ b/test/basictests.jl @@ -243,3 +243,22 @@ end # Float32 is isbits mismatch → errors @test_throws FunctionWrappersWrappers.NoFunctionWrapperFoundError fww(4.0f0, 8.0f0) end + +@testset "Conversion" begin + fww_exp = FunctionWrappersWrapper( + exp, + (Tuple{Float64}, Tuple{Float32}), + (Float64, Float32) + ) + FWW = typeof(fww_exp) + + fww_cos = FWW(cos) + @test typeof(fww_cos) == FWW + @test fww_cos(0.5) == cos(0.5) + + fww_vector = FWW[sin, tan, log] + @test eltype(fww_vector) == FWW + fww_vector[1](0.5) == sin(0.5) + fww_vector[2](0.5) == tan(0.5) + fww_vector[3](0.5) == log(0.5) +end From 7afd5b49e06fa3ea802c877932e8079ef964922c Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Thu, 25 Jun 2026 05:51:10 -0400 Subject: [PATCH 53/55] QA: run_qa v1.6 form + ExplicitImports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `test/qa` QA group built on SciMLTesting 1.6.0's `run_qa`, wired via the run_tests folder model (qa = (; env, body) + [QA] in test_groups.toml). The group runs Aqua (test_all), JET (auto-detected via `using JET`, test_package mode :typo), and the six ExplicitImports checks. ExplicitImports findings (explicit_imports = true), all resolved to 0 hard fail / 0 broken: - no_implicit_imports: made the four implicit names explicit in src — `using FunctionWrappers: FunctionWrappers` and `using PrecompileTools: @compile_workload, @setup_workload`. - all_qualified_accesses_are_public: ignore `Base.tail`, `FunctionWrappers.FunctionWrapper`, and `TruncatedStacktraces.@truncate_stacktrace` — long-standing core API of those modules that predates the `public` keyword. - the other four EI checks pass unmodified. Because src now explicit-imports only PrecompileTools' macros, bump the PrecompileTools compat floor to 1.1 (first self-contained-macro release) so the downgrade lane does not hit an UndefVarError on the bare module name. Aqua deps_compat surfaced a missing `Test` compat entry (Test is in [extras] without a [compat] bound); add `Test = "1"` to the root [compat]. Verified locally against released SciMLTesting 1.6.0 (no dev-from-branch): QA group 18/18 pass on Julia 1.10 (lts) and 1.11; Core 58/58; package loads and precompiles at the downgrade floors (PrecompileTools 1.1.0, FunctionWrappers 1.1.2, TruncatedStacktraces 1.1.0). Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.8 (1M context) --- Project.toml | 3 ++- src/FunctionWrappersWrappers.jl | 4 ++-- test/qa/Project.toml | 18 ++++++++++++++++++ test/qa/qa.jl | 17 +++++++++++++++++ test/runtests.jl | 1 + test/test_groups.toml | 3 +++ 6 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 test/qa/Project.toml create mode 100644 test/qa/qa.jl diff --git a/Project.toml b/Project.toml index f139297..711883f 100644 --- a/Project.toml +++ b/Project.toml @@ -22,9 +22,10 @@ Enzyme = "0.13" EnzymeCore = "0.8" FunctionWrappers = "1.1.2" Mooncake = "0.5" -PrecompileTools = "1" +PrecompileTools = "1.1" SafeTestsets = "0.1, 1" SciMLTesting = "1" +Test = "1" TruncatedStacktraces = "1.1" julia = "1.10" diff --git a/src/FunctionWrappersWrappers.jl b/src/FunctionWrappersWrappers.jl index 8b5ef0a..a99d624 100644 --- a/src/FunctionWrappersWrappers.jl +++ b/src/FunctionWrappersWrappers.jl @@ -1,6 +1,6 @@ module FunctionWrappersWrappers -using FunctionWrappers +using FunctionWrappers: FunctionWrappers import TruncatedStacktraces export FunctionWrappersWrapper, unwrap, wrapped_signatures, wrapped_return_types @@ -303,7 +303,7 @@ end # Precompilation # ============================================================================ -using PrecompileTools +using PrecompileTools: @compile_workload, @setup_workload @setup_workload begin @compile_workload begin diff --git a/test/qa/Project.toml b/test/qa/Project.toml new file mode 100644 index 0000000..754f8b8 --- /dev/null +++ b/test/qa/Project.toml @@ -0,0 +1,18 @@ +[deps] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +FunctionWrappersWrappers = "77dc65aa-8811-40c2-897b-53d922fa7daf" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" +SciMLTesting = "09d9d899-5365-40a9-917a-5f67fddea283" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[sources] +FunctionWrappersWrappers = {path = "../.."} + +[compat] +Aqua = "0.8" +JET = "0.9, 0.10, 0.11" +SafeTestsets = "0.0.1, 0.1, 1" +SciMLTesting = "1.6" +Test = "1" +julia = "1.10" diff --git a/test/qa/qa.jl b/test/qa/qa.jl new file mode 100644 index 0000000..0cbadbc --- /dev/null +++ b/test/qa/qa.jl @@ -0,0 +1,17 @@ +using SciMLTesting, FunctionWrappersWrappers, Test +using JET + +run_qa( + FunctionWrappersWrappers; + explicit_imports = true, + ei_kwargs = (; + # Qualified accesses to names that are not (yet) declared `public` in their + # owning module: `Base.tail`, `FunctionWrappers.FunctionWrapper`, and + # `TruncatedStacktraces.@truncate_stacktrace`. These are core, long-standing + # API of Base / FunctionWrappers / TruncatedStacktraces that predate the + # `public` keyword; ignore until those packages mark them public. + all_qualified_accesses_are_public = (; + ignore = (:tail, :FunctionWrapper, Symbol("@truncate_stacktrace")), + ), + ), +) diff --git a/test/runtests.jl b/test/runtests.jl index 232bc9f..e4da1f8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,6 +10,7 @@ run_tests(; include(joinpath(@__DIR__, "shared", "bigfloat_unionall_tests.jl")) end end, + qa = (; env = joinpath(@__DIR__, "qa"), body = joinpath(@__DIR__, "qa", "qa.jl")), groups = Dict( "nopre" => (; env = joinpath(@__DIR__, "nopre"), diff --git a/test/test_groups.toml b/test/test_groups.toml index 2568f58..ab28124 100644 --- a/test/test_groups.toml +++ b/test/test_groups.toml @@ -1,6 +1,9 @@ [Core] versions = ["lts", "1", "pre"] +[QA] +versions = ["lts", "1"] + [nopre] versions = ["lts", "1"] From 77e373f1051ba2b43510c9ccc37cb706ae9ed1db Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Fri, 26 Jun 2026 21:34:29 +0000 Subject: [PATCH 54/55] Release v1.9.3 (#61) --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index f139297..0e07645 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FunctionWrappersWrappers" uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" authors = ["Chris Elrod and contributors"] -version = "1.9.2" +version = "1.9.3" [deps] FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" From df42069b94a21b6cc1a06c0943d0d1ad863aa583 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Fri, 3 Jul 2026 04:55:42 -0400 Subject: [PATCH 55/55] Raise Mooncake compat floor to 0.5.36 (fix Downgrade resolution) Raise the Mooncake [compat] floor from "0.5" to a single latest floor "0.5.36" in every Project.toml that declares Mooncake (root weakdep + test/Mooncake sub-env), so the downgrade min-resolution no longer pins an ancient Mooncake. Patch-level bump of the root version (a compat narrowing of a weakdep is not a public-API change). Verified on Julia 1.12 via julia-actions/julia-downgrade-compat's downgrade.jl (Resolver.jl --min=@deps): the test/Mooncake env resolves successfully, pinning Mooncake to exactly 0.5.36 (33 packages resolved). Co-Authored-By: Chris Rackauckas --- Project.toml | 4 ++-- test/Mooncake/Project.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index 81b3e29..b6d86e5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FunctionWrappersWrappers" uuid = "77dc65aa-8811-40c2-897b-53d922fa7daf" authors = ["Chris Elrod and contributors"] -version = "1.9.3" +version = "1.9.4" [deps] FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" @@ -21,7 +21,7 @@ FunctionWrappersWrappersMooncakeExt = "Mooncake" Enzyme = "0.13" EnzymeCore = "0.8" FunctionWrappers = "1.1.2" -Mooncake = "0.5" +Mooncake = "0.5.36" PrecompileTools = "1.1" SafeTestsets = "0.1, 1" SciMLTesting = "1" diff --git a/test/Mooncake/Project.toml b/test/Mooncake/Project.toml index b90014b..ac5b7be 100644 --- a/test/Mooncake/Project.toml +++ b/test/Mooncake/Project.toml @@ -8,6 +8,6 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" FunctionWrappersWrappers = {path = "../.."} [compat] -Mooncake = "0.5" +Mooncake = "0.5.36" SafeTestsets = "0.0.1, 0.1, 1" Test = "1"