From f05b7869bbedea6f6a3a868f7548227837eae339 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Thu, 25 Jun 2026 07:52:35 -0400 Subject: [PATCH 1/2] QA: run_qa v1.6 form + ExplicitImports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert the hand-rolled test/qa/qa.jl (raw Aqua.test_* + per-function JET.report_call) to the SciMLTesting 1.6 `run_qa` form and enable the ExplicitImports checks. ExplicitImports findings (run vs released SciMLTesting 1.6.0): * no_stale_explicit_imports: removed the genuinely stale `ArrayInterface.allowed_getindex` import (never referenced; only `ismutable`/`allowed_setindex!` are used). * Made the `for i in 1:13 include("exp_generated/exp_$i.jl")` dynamic include in exp_noalloc.jl static (13 literal includes) so the module is analyzable — this unblocked no_implicit_imports and no_stale_explicit_imports (previously UnanalyzableModuleException). Verified Higham2005 matrix-exp still matches Base `exp` to ~6.7e-16. * all_qualified_accesses_via_owners / all_qualified_accesses_are_public / all_explicit_imports_are_public: ignore-listed other packages' non-public names (Base / LinearAlgebra(.BLAS/.LAPACK, incl. Stegr submodule) / ArrayInterface / libblastrampoline_jll); they go public as the base libs declare them. * no_implicit_imports: ~31 implicit names from `using LinearAlgebra, SparseArrays, Printf, PrecompileTools`. Making them explicit is a large refactor; marked ei_broken and tracked in #231 (auto-flags when fixed). Deps: test/qa/Project.toml SciMLTesting compat -> "1.6" (Aqua + ExplicitImports are transitive via SciMLTesting; Aqua kept a direct dep so the ambiguities sub-check's child process can resolve it; JET kept for the JET check). Root Project.toml SciMLTesting compat -> "1.6". QA group on Julia 1.10 (lts), released SciMLTesting 1.6.0: Quality Assurance | 17 Pass, 1 Broken, 0 Fail, 0 Error (no_implicit_imports broken per #231). On Julia 1.12 the JET typo check reports pre-existing "may be undefined" findings (kiops order/kest, Higham2005 ilo/ihi/scale/bal); master is already red there and the source fixes live in draft PR #229. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.8 (1M context) --- Project.toml | 2 +- src/ExponentialUtilities.jl | 2 +- src/exp_noalloc.jl | 16 +++++- test/qa/Project.toml | 8 +-- test/qa/qa.jl | 107 ++++++++++++------------------------ 5 files changed, 53 insertions(+), 82 deletions(-) diff --git a/Project.toml b/Project.toml index a5c275c8..dc611a3f 100644 --- a/Project.toml +++ b/Project.toml @@ -33,7 +33,7 @@ PrecompileTools = "1" Printf = "1.10" Random = "1" SafeTestsets = "0.1, 1" -SciMLTesting = "1" +SciMLTesting = "1.6" SparseArrays = "1.10" StaticArrays = "1.9.8" Test = "1" diff --git a/src/ExponentialUtilities.jl b/src/ExponentialUtilities.jl index 9e89db38..dcfa0068 100644 --- a/src/ExponentialUtilities.jl +++ b/src/ExponentialUtilities.jl @@ -1,6 +1,6 @@ module ExponentialUtilities using LinearAlgebra, SparseArrays, Printf -using ArrayInterface: ismutable, allowed_getindex, allowed_setindex! +using ArrayInterface: ismutable, allowed_setindex! using PrecompileTools import GenericSchur import GPUArraysCore diff --git a/src/exp_noalloc.jl b/src/exp_noalloc.jl index d93d6304..7fd1cc92 100644 --- a/src/exp_noalloc.jl +++ b/src/exp_noalloc.jl @@ -18,9 +18,19 @@ function alloc_mem(A, ::ExpMethodHigham2005) end # Import the generated code -for i in 1:13 - include("exp_generated/exp_$i.jl") -end +include("exp_generated/exp_1.jl") +include("exp_generated/exp_2.jl") +include("exp_generated/exp_3.jl") +include("exp_generated/exp_4.jl") +include("exp_generated/exp_5.jl") +include("exp_generated/exp_6.jl") +include("exp_generated/exp_7.jl") +include("exp_generated/exp_8.jl") +include("exp_generated/exp_9.jl") +include("exp_generated/exp_10.jl") +include("exp_generated/exp_11.jl") +include("exp_generated/exp_12.jl") +include("exp_generated/exp_13.jl") function getmem(cache, k) # Called from generated code return cache[k - 1] diff --git a/test/qa/Project.toml b/test/qa/Project.toml index 1eb4dc29..2b51c79f 100644 --- a/test/qa/Project.toml +++ b/test/qa/Project.toml @@ -6,13 +6,13 @@ SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" SciMLTesting = "09d9d899-5365-40a9-917a-5f67fddea283" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -[sources] -ExponentialUtilities = {path = "../.."} - [compat] Aqua = "0.8" JET = "0.9, 0.10, 0.11" SafeTestsets = "0.1, 1" -SciMLTesting = "1" +SciMLTesting = "1.6" Test = "1" julia = "1.10" + +[sources] +ExponentialUtilities = {path = "../.."} diff --git a/test/qa/qa.jl b/test/qa/qa.jl index e096d723..9679ba6d 100644 --- a/test/qa/qa.jl +++ b/test/qa/qa.jl @@ -1,73 +1,34 @@ -using ExponentialUtilities, Aqua, JET, Test - -@testset "Aqua" begin - Aqua.find_persistent_tasks_deps(ExponentialUtilities) - Aqua.test_ambiguities(ExponentialUtilities, recursive = false) - Aqua.test_deps_compat( - ExponentialUtilities, - ignore = [:libblastrampoline_jll] - ) - Aqua.test_piracies(ExponentialUtilities) - Aqua.test_project_extras(ExponentialUtilities) - Aqua.test_stale_deps(ExponentialUtilities) - Aqua.test_unbound_args(ExponentialUtilities) - Aqua.test_undefined_exports(ExponentialUtilities) -end - -# Analyze only ExponentialUtilities' own code. Without this, JET on Julia 1.12 traces -# into LinearAlgebra/Base internals (e.g. `norm(::Vector)` -> `norm_recursive_check`, -# and the broadcast `unalias`/`copyto_unaliased!` path over `Adjoint{T, Union{}}`) and -# reports abstract-interpretation artifacts there that are not under this package's -# control. Scoping to `ExponentialUtilities` keeps full coverage of this package's code -# (it still flags real `may be undefined` findings here) without asserting that all of -# the stdlib is JET-clean. -const JET_TARGET = (ExponentialUtilities,) - -@testset "JET static analysis" begin - @testset "expv" begin - rep = JET.report_call( - expv, (Float64, Matrix{Float64}, Vector{Float64}); target_modules = JET_TARGET - ) - @test length(JET.get_reports(rep)) == 0 - end - - @testset "arnoldi" begin - rep = JET.report_call( - arnoldi, (Matrix{Float64}, Vector{Float64}); target_modules = JET_TARGET - ) - @test length(JET.get_reports(rep)) == 0 - end - - @testset "phi" begin - rep = JET.report_call(phi, (Matrix{Float64}, Int); target_modules = JET_TARGET) - @test length(JET.get_reports(rep)) == 0 - end - - @testset "exponential!" begin - rep = JET.report_call( - ExponentialUtilities.exponential!, (Matrix{Float64},); target_modules = JET_TARGET - ) - @test length(JET.get_reports(rep)) == 0 - end - - @testset "phiv" begin - rep = JET.report_call( - phiv, (Float64, Matrix{Float64}, Vector{Float64}, Int); target_modules = JET_TARGET - ) - @test length(JET.get_reports(rep)) == 0 - end - - @testset "kiops" begin - rep = JET.report_call( - kiops, (Float64, Matrix{Float64}, Vector{Float64}); target_modules = JET_TARGET - ) - @test length(JET.get_reports(rep)) == 0 - end - - @testset "expv_timestep" begin - rep = JET.report_call( - expv_timestep, (Float64, Matrix{Float64}, Vector{Float64}); target_modules = JET_TARGET - ) - @test length(JET.get_reports(rep)) == 0 - end -end +using SciMLTesting, ExponentialUtilities, JET, Test + +run_qa( + ExponentialUtilities; + explicit_imports = true, + aqua_kwargs = (; deps_compat = (; ignore = [:libblastrampoline_jll])), + ei_kwargs = (; + # Names owned elsewhere but reached through LinearAlgebra.BLAS. + all_qualified_accesses_via_owners = (; + ignore = (:BlasFloat, :chkstride1, :libblastrampoline), + ), + # Non-public names of Base / LinearAlgebra(.BLAS/.LAPACK) accessed qualified. + all_qualified_accesses_are_public = (; + ignore = ( + Symbol("@aliasscope"), Symbol("@assume_effects"), + Symbol("@blasfunc"), Symbol("@propagate_inbounds"), + :BlasFloat, :Cartesian, :Const, :Experimental, + :checksquare, :chkfinite, :chklapackerror, :chkstride1, + :gebal!, :gesv!, :libblastrampoline, :rcswap!, :stegr!, + ), + ), + # Non-public names explicitly imported from LinearAlgebra(.BLAS/.LAPACK, + # incl. the Stegr submodule) / ArrayInterface / Base. + all_explicit_imports_are_public = (; + ignore = ( + :BlasInt, :checksquare, :allowed_setindex!, :ismutable, :typename, + Symbol("@blasfunc"), :stegr!, + ), + ), + ), + # Heavy `using LinearAlgebra, SparseArrays, Printf, PrecompileTools` brings ~31 + # names implicitly; making them explicit is a large refactor tracked separately. + ei_broken = (:no_implicit_imports,) +) # SciML/ExponentialUtilities.jl#231 From f3eac2bf9422b26b7f21c2f10cc5c2596579e9f4 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Mon, 29 Jun 2026 06:17:01 -0400 Subject: [PATCH 2/2] QA: fix latent undefined-balancing locals in exponential!(::ExpMethodHigham2005) The run_qa v1.6 conversion runs JET in report_package typo mode, which analyzes each method signature in isolation. The hand-rolled qa.jl this replaced used JET.report_call(exponential!, (Matrix{Float64},)), where ExpMethodHigham2005(A) sets do_balancing = (A isa StridedMatrix) as a constant that JET could constant-propagate, so both `if method.do_balancing` blocks folded to true and ilo/ihi/scale/bal were seen as always defined. In report_package the method is analyzed with an abstract ExpMethodHigham2005, so do_balancing is a runtime Bool, the two balancing blocks are not provably correlated, and the undo block reads ilo/ihi/scale/bal as possibly-undefined locals (20 JET typo reports on Julia 1.12; 1.10 abstract-interp did not reach them). Seed ilo=1/ihi=n/scale=_scale as no-op defaults and lift the GenericSchur row/col permutations into prow/pcol locals (nothing on the BLAS path, which never reads them), so every local read in the symmetric undo block is unconditionally defined. Behavior is unchanged: the seeds are only live when do_balancing is false (where the undo block does not run), and the BLAS vs GenericSchur branches use exactly the values they used before. Verified Julia 1.12.6 (released SciMLTesting 1.7.0, JET 0.11.5): report_package typo mode goes from 20 reports to 0. Verified Julia 1.10.11 numerics unchanged: strided-BLAS balancing relerr 3.3e-16, GenericSchur (BigFloat) balancing relerr 1.1e-16, no-balancing relerr 1.6e-16 vs reference exp. Co-Authored-By: Chris Rackauckas --- src/exp_noalloc.jl | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/exp_noalloc.jl b/src/exp_noalloc.jl index 7fd1cc92..80047db3 100644 --- a/src/exp_noalloc.jl +++ b/src/exp_noalloc.jl @@ -99,13 +99,22 @@ function exponential!(A, method::ExpMethodHigham2005, _cache = alloc_mem(A, meth n = LinearAlgebra.checksquare(A) nA = opnorm(A, 1) - # Maybe to balancing + # Maybe to balancing. `ilo`/`ihi`/`scale` are seeded with no-op defaults so they are + # always defined before the symmetric undo block below; the two `do_balancing` + # branches are not provably correlated to the compiler, so without these seeds the + # undo block reads possibly-undefined locals (flagged by JET typo-mode). + ilo = 1 + ihi = n + scale = _scale + prow = nothing # row/col permutations from the GenericSchur (non-BLAS) balancing path + pcol = nothing if method.do_balancing if A isa StridedMatrix{<:LinearAlgebra.BLAS.BlasFloat} ilo, ihi, scale = gebal_noalloc!('B', A, _scale) # modifies A and _scale else A, bal = GenericSchur.balance!(A) ilo, ihi, scale = bal.ilo, bal.ihi, bal.D + prow, pcol = bal.prow, bal.pcol end end @@ -144,12 +153,12 @@ function exponential!(A, method::ExpMethodHigham2005, _cache = alloc_mem(A, meth else if ilo > 1 # apply lower permutations in reverse order for j in (ilo - 1):-1:1 - LinearAlgebra.rcswap!(j, bal.prow[j], X) + LinearAlgebra.rcswap!(j, prow[j], X) end end if ihi < n # apply upper permutations in forward order for j in (ihi + 1):n - LinearAlgebra.rcswap!(j, bal.pcol[j - ihi], X) + LinearAlgebra.rcswap!(j, pcol[j - ihi], X) end end end