From dd7b5086ef627799ab5558a46cb87d31e8fcffe2 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Tue, 6 Oct 2026 17:45:12 -0400 Subject: [PATCH 1/6] Update JET to 0.12 and run the package analysis as a test group Mooncake 0.5.45 lifted its `JET = "0.9 - 0.11"` cap, so JET 0.12 is now resolvable. Add `test/other/jet.jl`, which runs `JET.test_package` over TensorKit, and gate it in `runtests.jl`. JET 0.12 only works on Julia 1.12+; on older versions it loads empty stubs and signals this through `JET_AVAILABLE`. Whole-package analysis is therefore pinned to the 0.12 generation, so its reports need not be curated for several JET/Julia combinations. Mooncake's tangent tests reach JET through Mooncake's extension and work with any functional JET, so they are gated separately. The `Mooncake = "0.5.45"` floor is load-bearing: with an older Mooncake the resolver settles on JET 0.11 and the analysis would be silently skipped. Co-Authored-By: Claude Opus 5 (1M context) --- test/Project.toml | 2 +- test/README.md | 16 ++++++++++------ test/mooncake/tangent.jl | 6 +++++- test/other/jet.jl | 26 ++++++++++++++++++++++++++ test/runtests.jl | 11 +++++++++++ 5 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 test/other/jet.jl diff --git a/test/Project.toml b/test/Project.toml index ce51b70a3..a63d0b3e9 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -39,7 +39,7 @@ AllocCheck = "0.2" ChainRulesTestUtils = "1" Combinatorics = "1" cuTENSOR = "6" -JET = "0.9, 0.10, 0.11" +JET = "0.9, 0.12" ParallelTestRunner = "2" Test = "1" TestExtras = "0.2,0.3" diff --git a/test/README.md b/test/README.md index d978612f6..c5d7f49f2 100644 --- a/test/README.md +++ b/test/README.md @@ -1,8 +1,8 @@ # TensorKit.jl test suite -Tests use [ParallelTestRunner.jl](https://github.com/vchuravy/ParallelTestRunner.jl) for parallel -execution. Each test file runs in its own worker process. Shared helpers are loaded automatically -via `init_code` — test files do not need to include `setup.jl` themselves. +Tests use [ParallelTestRunner.jl](https://github.com/vchuravy/ParallelTestRunner.jl) for parallel execution. +Each test file runs in its own worker process. +Shared helpers are loaded automatically via `init_code` — test files do not need to include `setup.jl` themselves. ## Running tests @@ -37,17 +37,21 @@ julia --project=test test/runtests.jl --jobs=4 |-------|----------| | `symmetries` | Spaces and fusion trees | | `tensors` | Core tensor operations, factorizations, planar tensors, diagonal tensors | -| `other` | Aqua code-quality checks, bug-fix regressions | +| `other` | Aqua code-quality checks, JET static analysis, bug-fix regressions | | `chainrules` | ChainRulesCore AD tests | | `mooncake` | Mooncake AD tests | | `cuda` | CUDA GPU tests (only run when a functional GPU is present) | +JET only functions on the Julia versions it supports, which it reports through `JET.JET_AVAILABLE`. +`runtests.jl` drops the JET-dependent files from the testsuite when that is `false`: +`other/jet` (whole-package analysis, which additionally requires the JET 0.12 generation) +and `mooncake/tangent` (reaches JET through Mooncake's extension). + ## Fast mode (`--fast`) Skips `chainrules` and `mooncake` groups entirely, and reduces coverage in the remaining tests: -- **Sector types**: tests only `Z2Irrep`, `SU2Irrep`, `FermionParity ⊠ U1Irrep ⊠ SU2Irrep`, - and `FibonacciAnyon` (instead of the full `sectorlist`) +- **Sector types**: tests only `Z2Irrep`, `SU2Irrep`, `FermionParity ⊠ U1Irrep ⊠ SU2Irrep`, and `FibonacciAnyon` (instead of the full `sectorlist`) - **Space lists**: tests only `(Vtr, Vℤ₂, VSU₂)` (trivial, abelian, non-abelian) - **Scalar types**: tests only `Float64` and `ComplexF64` (instead of all integer/float variants) diff --git a/test/mooncake/tangent.jl b/test/mooncake/tangent.jl index 7b7c9ab4d..3c0c14fd2 100644 --- a/test/mooncake/tangent.jl +++ b/test/mooncake/tangent.jl @@ -10,8 +10,12 @@ rng = Random.default_rng() spacelist = ad_spacelist(fast_tests) eltypes = (Float64, ComplexF64) +# `test_data` reaches JET through Mooncake's extension, so it needs a functional JET; this is also +# gated in runtests.jl. JET < 0.12 does not define `JET_AVAILABLE`. +jet_available = !isdefined(JET, :JET_AVAILABLE) || JET.JET_AVAILABLE + # only run on Linux since allocation tests are broken on other versions -Sys.islinux() && @timedtestset "Mooncake - Tangent type: $(TensorKit.type_repr(sectortype(eltype(V)))) ($T)" for V in spacelist, T in eltypes +jet_available && Sys.islinux() && @timedtestset "Mooncake - Tangent type: $(TensorKit.type_repr(sectortype(eltype(V)))) ($T)" for V in spacelist, T in eltypes A = randn(T, V[1] ⊗ V[2] ⊗ V[3] ← (V[4] ⊗ V[5])') Mooncake.TestUtils.test_data(rng, A) diff --git a/test/other/jet.jl b/test/other/jet.jl new file mode 100644 index 000000000..a091799bf --- /dev/null +++ b/test/other/jet.jl @@ -0,0 +1,26 @@ +using TensorKit +using Test +using JET + +# Reports that are not TensorKit's to fix. Each entry needs an upstream issue. +const IGNORED = ( + # `schur_full`/`schur_vals` route `Diagonal` inputs to `DiagonalAlgorithm`, which schur does + # not implement, so `schur_full(::DiagonalTensorMap)` throws a `MethodError`. This reproduces + # with a plain `LinearAlgebra.Diagonal` and no TensorKit involved: + # https://github.com/QuantumKitHub/MatrixAlgebraKit.jl/issues/276 + JET.LastFrameMethod(:schur_full!), + JET.LastFrameMethod(:schur_vals!), + # `local variable kwargs may be undefined` inside `GenericTreeTransformer`, coming entirely + # from Base's `@debug` expansion (`local msg, kwargs` bound in a short-circuit guard) rather + # than from any TensorKit code: https://github.com/aviatesk/JET.jl/issues/860 + # `AnyFrameMethod` rather than `LastFrameMethod`: the `@debug` sits inside a `taskforeach` + # closure, so the innermost frame is a gensym'd closure name rather than the constructor. + JET.AnyFrameMethod(:GenericTreeTransformer), +) + +# also gated in runtests.jl; JET < 0.12 does not define `JET_AVAILABLE` +if isdefined(JET, :JET_AVAILABLE) && JET.JET_AVAILABLE + JET.test_package(TensorKit; target_modules = (TensorKit,), ignored_modules = IGNORED) +else + @info "Full JET functionality is unavailable on Julia $VERSION; skipping JET analysis" +end diff --git a/test/runtests.jl b/test/runtests.jl index c5645cd66..ae8446f2a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -13,6 +13,17 @@ CUDA.functional() || filter!(!startswith("cuda") ∘ first, testsuite) using AMDGPU AMDGPU.functional() || filter!(!startswith("amd") ∘ first, testsuite) +# JET tests: JET ≥ 0.12 (Julia 1.12+) signals through `JET_AVAILABLE` whether it is functional, +# and loads empty stubs when it is not. Older JET versions, still needed on Julia < 1.12, do not +# define it, hence the `isdefined` check instead of `using JET: JET_AVAILABLE`. +using JET: JET +const jet_new_generation = isdefined(JET, :JET_AVAILABLE) +const jet_available = !jet_new_generation || JET.JET_AVAILABLE +# whole-package analysis is pinned to the JET 0.12 generation, so its reports need not be curated for several JET/Julia combinations +(jet_new_generation && jet_available) || delete!(testsuite, "other/jet") +# Mooncake's tangent tests reach JET through Mooncake's extension: any functional JET works, the empty stubs do not +jet_available || delete!(testsuite, "mooncake/tangent") + # On Buildkite (GPU CI runner): only run CUDA and AMDGPU tests if get(ENV, "BUILDKITE", "false") == "true" f(str) = startswith(first(str), "cuda") || startswith(first(str), "amd") From 9e5af3c92cca02b6b6c5f3c361bf49a165ae90f2 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Tue, 6 Oct 2026 17:45:33 -0400 Subject: [PATCH 2/6] Fix genuine errors uncovered by JET Four latent bugs on paths that no test exercises: - `@planar ... order=(...)` called a bare `normalizeindex`, which is not in scope: an outright `UndefVarError`. Nothing tests `order=`. - `_extract_contraction_pairs` interpolated `$ex` into its error message, but the argument is named `rhs`, so constructing the error threw instead. - `src/tensors/linalg.jl` threw `DimensionError`, which does not exist; the intended type is `IndexError`. - `numout`/`numin`'s fallbacks built `MethodError(f, T)` from a bare type. `MethodError`'s second field must be the argument *tuple*, so `showerror` itself threw `ErrorException("expected tuple type")` and the intended error was never printable. Every other site in the file already uses `(T,)`. Co-Authored-By: Claude Opus 5 (1M context) --- src/planar/macros.jl | 2 +- src/planar/preprocessors.jl | 2 +- src/tensors/abstracttensor.jl | 4 ++-- src/tensors/linalg.jl | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/planar/macros.jl b/src/planar/macros.jl index a14695812..895917efc 100644 --- a/src/planar/macros.jl +++ b/src/planar/macros.jl @@ -56,7 +56,7 @@ function planarparser(planarexpr, kwargs...) if name == :order isexpr(val, :tuple) || throw(ArgumentError("Invalid use of `order`, should be `order=(...,)`")) - indexorder = map(normalizeindex, val.args) + indexorder = map(TO.normalizeindex, val.args) parser.contractiontreebuilder = network -> TO.indexordertree( network, indexorder ) diff --git a/src/planar/preprocessors.jl b/src/planar/preprocessors.jl index 9e8363205..435b43a5f 100644 --- a/src/planar/preprocessors.jl +++ b/src/planar/preprocessors.jl @@ -609,7 +609,7 @@ function _extract_contraction_pairs(rhs, lhs, pre, temporaries) newarg = _extract_contraction_pairs(rhs.args[3], lhs, pre, temporaries) return Expr(:call, :\, rhs.args[2], newarg) else - throw(ArgumentError("unknown tensor expression $ex")) + throw(ArgumentError("unknown tensor expression $rhs")) end end diff --git a/src/tensors/abstracttensor.jl b/src/tensors/abstracttensor.jl index ff2f44f10..0413d295b 100644 --- a/src/tensors/abstracttensor.jl +++ b/src/tensors/abstracttensor.jl @@ -238,7 +238,7 @@ See also [`numin`](@ref) and [`numind`](@ref). """ numout numout(x) = numout(typeof(x)) -numout(T::Type) = throw(MethodError(numout, T)) # avoid infinite recursion +numout(T::Type) = throw(MethodError(numout, (T,))) # avoid infinite recursion numout(::Type{<:AbstractTensorMap{T, S, N₁}}) where {T, S, N₁} = N₁ @doc """ @@ -252,7 +252,7 @@ See also [`numout`](@ref) and [`numind`](@ref). """ numin numin(x) = numin(typeof(x)) -numin(T::Type) = throw(MethodError(numin, T)) # avoid infinite recursion +numin(T::Type) = throw(MethodError(numin, (T,))) # avoid infinite recursion numin(::Type{<:AbstractTensorMap{T, S, N₁, N₂}}) where {T, S, N₁, N₂} = N₂ """ diff --git a/src/tensors/linalg.jl b/src/tensors/linalg.jl index a0893749d..16d595ba7 100644 --- a/src/tensors/linalg.jl +++ b/src/tensors/linalg.jl @@ -586,7 +586,7 @@ tdst[sub_axes...] .= tsrc[sub_axes...] absorb(tdst::AbstractTensorMap, tsrc::AbstractTensorMap) = absorb!(copy(tdst), tsrc) function absorb!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap) numin(tdst) == numin(tsrc) && numout(tdst) == numout(tsrc) || - throw(DimensionError("Incompatible number of indices for source and destination")) + throw(IndexError("Incompatible number of indices for source and destination")) S = check_spacetype(tdst, tsrc) dom = mapreduce(infimum, ⊗, domain(tdst), domain(tsrc); init = one(S)) cod = mapreduce(infimum, ⊗, codomain(tdst), codomain(tsrc); init = one(S)) From 4068898ef717257923c6feb449e7b0c8980630ac Mon Sep 17 00:00:00 2001 From: lkdvos Date: Tue, 6 Oct 2026 17:45:49 -0400 Subject: [PATCH 3/6] Make the JET analysis clean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No behavioural changes; each hunk silences a report that JET cannot see through. Inference cannot narrow a `local` binding that is only assigned inside a branch, so replace the sentinel-by-undefinedness idiom with real sentinel values: `@cached`'s `hasparams`/`typed` flags become `nothing` checks on `params`/`typeex`, `planar_trace`'s `local i, j` becomes `i = j = 0`, and `_construct_braidingtensors!` tracks `V1`/`V2` as `nothing` rather than via `foundV1`/`foundV2`. `OneOrNoneIterator`'s default `state = true` is a `Bool`, but a recursive call passes an `Int`, so `if state` is reported as a non-boolean context; `isone` accepts both and keeps the `Bool` default. `fusiontensor` used `TupleTools.prod(d)` on a value inferred as `Union{Tuple, NamedTuple}`; `reshape(F, :, sz[end])` computes the same leading dimension without the union split. The remaining reports came from functions that serve both tensors and spaces under one name. Julia narrows a slot after a call that would otherwise throw a `MethodError`, so an untyped argument passed to such a function is refined to `Union{AbstractTensorMap, TensorMapSpace}` rather than to a tensor, and the next call splits on a branch that has no method. `_braid!`/`_transpose!` are fixed structurally rather than by annotation: they now take `space(tdst)`/`space(tsrc)` once and pass the spaces to both `spacecheck_transform` and `treebraider`/`treetransposer`, whose tensor-accepting forwarding methods are therefore removed. `permute!`/`braid!`/`transpose!` likewise pass spaces to `spacecheck_transform`, so its tensor-accepting method is removed too. `space` has no space-accepting method, so this narrows the arguments on its own, and the non-dense path now computes two spaces instead of four. Where no such restructuring applies, the contract is written down instead: `_trace_permute!` (matching `add_transform!` next door), the `UniqueTreeTransformer`/`GenericTreeTransformer` constructors (`::HomSpace`, their only caller always passes one), `parenttype(::Type{Union{}})`, and the primal `A` of `trace_permute_pullback_ΔA!`. Co-Authored-By: Claude Opus 5 (1M context) --- src/auxiliary/caches.jl | 22 ++++++++++------------ src/auxiliary/iterators.jl | 2 +- src/fusiontrees/duality_manipulations.jl | 2 +- src/fusiontrees/fusiontrees.jl | 3 +-- src/planar/preprocessors.jl | 8 ++------ src/pullbacks/tensoroperations.jl | 5 ++++- src/tensors/adjoint.jl | 1 + src/tensors/indexmanipulations.jl | 18 +++++++++--------- src/tensors/tensoroperations.jl | 10 ++++++++-- src/tensors/treetransformers.jl | 12 ++---------- 10 files changed, 39 insertions(+), 44 deletions(-) diff --git a/src/auxiliary/caches.jl b/src/auxiliary/caches.jl index bdc07ca08..d6753ead9 100644 --- a/src/auxiliary/caches.jl +++ b/src/auxiliary/caches.jl @@ -87,18 +87,16 @@ function _cached(mod::Module, ex) end fcall = ex.args[1] if Meta.isexpr(fcall, :where) - hasparams = true params = fcall.args[2:end] fcall = fcall.args[1] else - hasparams = false + params = nothing end if Meta.isexpr(fcall, :(::)) - typed = true typeex = fcall.args[2] fcall = fcall.args[1] else - typed = false + typeex = nothing end Meta.isexpr(fcall, :call) || _cached_error("expected a function signature", fcall) fname = fcall.args[1] @@ -117,14 +115,14 @@ function _cached(mod::Module, ex) # actual implenetation, with underscore name _fname = Symbol(:_, basename) _fcall = Expr(:call, _fname, fargs...) - if hasparams + if !isnothing(params) _fcall = Expr(:where, _fcall, params...) end _fex = Expr(:function, _fcall, _fbody) # implementation that chooses the cache style newfcall = fcall - if hasparams + if !isnothing(params) newfcall = Expr(:where, newfcall, params...) end cachestylevar = gensym(:cachestyle) @@ -138,11 +136,11 @@ function _cached(mod::Module, ex) # nocache implementation fnocachecall = Expr(:call, fname, fargs..., :(::$NoCache)) - if hasparams + if !isnothing(params) fnocachecall = Expr(:where, fnocachecall, params...) end fnocachebody = _timeit_expr(misslabel, Expr(:call, _fname, fargnames...)) - if typed + if !isnothing(typeex) T = gensym(:T) fnocachebody = Expr(:block, Expr(:(=), T, typeex), Expr(:(::), fnocachebody, T)) end @@ -151,7 +149,7 @@ function _cached(mod::Module, ex) # tasklocal cache implementation Dvar = gensym(:D) flocalcachecall = Expr(:call, fname, fargs..., :(::$TaskLocalCache{$Dvar})) - if hasparams + if !isnothing(params) flocalcachecall = Expr(:where, flocalcachecall, params..., Dvar) else flocalcachecall = Expr(:where, flocalcachecall, Dvar) @@ -172,7 +170,7 @@ function _cached(mod::Module, ex) end missex = _timeit_expr(misslabel, Expr(:call, _fname, fargnames...)) getvalex = _timeit_expr(lookuplabel, :(get!(() -> $missex, $cachevar, $key))) - if typed + if !isnothing(typeex) T = gensym(:T) flocalcachebody = Expr( :block, @@ -193,11 +191,11 @@ function _cached(mod::Module, ex) # # global cache implementation fglobalcachecall = Expr(:call, fname, fargs..., :(::$GlobalLRUCache)) - if hasparams + if !isnothing(params) fglobalcachecall = Expr(:where, fglobalcachecall, params...) end getglobalcachex = Expr(:(=), cachevar, globalcachename) - if typed + if !isnothing(typeex) T = gensym(:T) fglobalcachebody = Expr( :block, diff --git a/src/auxiliary/iterators.jl b/src/auxiliary/iterators.jl index 211c0dd93..6a4be06b7 100644 --- a/src/auxiliary/iterators.jl +++ b/src/auxiliary/iterators.jl @@ -4,7 +4,7 @@ struct OneOrNoneIterator{T} end function Base.iterate(it::OneOrNoneIterator, state = true) - if state && it.cond + if isone(state) && it.cond return (it.first, false) else return nothing diff --git a/src/fusiontrees/duality_manipulations.jl b/src/fusiontrees/duality_manipulations.jl index c595afdc2..e399f87ab 100644 --- a/src/fusiontrees/duality_manipulations.jl +++ b/src/fusiontrees/duality_manipulations.jl @@ -657,7 +657,7 @@ function planar_trace(f::FusionTree, (q₁, q₂)::Index2Tuple) # We thus handle the total trace recursively, by first looking for and # tracing away neighbouring pairs. k = 1 - local i, j + i = j = 0 while k <= length(q₁) if mod1(q₁[k] + 1, length(f)) == q₂[k] i = q₁[k] diff --git a/src/fusiontrees/fusiontrees.jl b/src/fusiontrees/fusiontrees.jl index 7b97223ec..f20445f36 100644 --- a/src/fusiontrees/fusiontrees.jl +++ b/src/fusiontrees/fusiontrees.jl @@ -338,8 +338,7 @@ function fusiontensor((f₁, f₂)::FusionTreePair) d1 = TupleTools.front(sz1) d2 = TupleTools.front(sz2) return reshape( - reshape(F₁, TupleTools.prod(d1), sz1[end]) * - reshape(F₂, TupleTools.prod(d2), sz2[end])', (d1..., d2...) + reshape(F₁, :, sz1[end]) * reshape(F₂, :, sz2[end])', (d1..., d2...) ) end fusiontensor(src::FusionTreeBlock) = sum(fusiontensor, fusiontrees(src)) diff --git a/src/planar/preprocessors.jl b/src/planar/preprocessors.jl index 435b43a5f..77c614ae1 100644 --- a/src/planar/preprocessors.jl +++ b/src/planar/preprocessors.jl @@ -180,22 +180,18 @@ function _construct_braidingtensors!(ex, preargs, indexmap, non_braiding) # ex i i1a, i2a, = rightind end - foundV1, foundV2 = false, false + V1 = V2 = nothing if haskey(indexmap, i1a) V1 = indexmap[i1a] - foundV1 = true elseif haskey(indexmap, i1b) V1 = Expr(:call, :dual, indexmap[i1b]) - foundV1 = true end if haskey(indexmap, i2a) V2 = indexmap[i2a] - foundV2 = true elseif haskey(indexmap, i2b) V2 = Expr(:call, :dual, indexmap[i2b]) - foundV2 = true end - if foundV1 && foundV2 + if !isnothing(V1) && !isnothing(V2) s = gensym(:τ) scalarex = Expr( :call, :promote_type, diff --git a/src/pullbacks/tensoroperations.jl b/src/pullbacks/tensoroperations.jl index 64507861f..4e784af5b 100644 --- a/src/pullbacks/tensoroperations.jl +++ b/src/pullbacks/tensoroperations.jl @@ -47,8 +47,11 @@ function blas_contract_pullback_ΔB!( return nothing end +# Only the primal `A` is annotated: it fixes the element and space types of the workspace `E` +# below, which `twist!` needs. The tangents stay untyped because their representation differs +# per AD backend (Mooncake passes a tangent object, Enzyme the `.dval` shadow). function trace_permute_pullback_ΔA!( - ΔA, ΔC, A, p, q, α, backend + ΔA, ΔC, A::AbstractTensorMap, p, q, α, backend ) ip = invperm((linearize(p)..., q[1]..., q[2]...)) pdA = TO.repartition(ip, numout(A)) diff --git a/src/tensors/adjoint.jl b/src/tensors/adjoint.jl index 820e87375..6aa5810e4 100644 --- a/src/tensors/adjoint.jl +++ b/src/tensors/adjoint.jl @@ -13,6 +13,7 @@ end Base.parent(t::AdjointTensorMap) = t.parent parenttype(t::AdjointTensorMap) = parenttype(typeof(t)) parenttype(::Type{AdjointTensorMap{T, S, N₁, N₂, TT}}) where {T, S, N₁, N₂, TT} = TT +parenttype(::Type{Union{}}) = throw(MethodError(parenttype, (Union{},))) # Constructor: construct from taking adjoint of a tensor Base.adjoint(t::AdjointTensorMap) = parent(t) diff --git a/src/tensors/indexmanipulations.jl b/src/tensors/indexmanipulations.jl index 61a4a6a12..dcb8262de 100644 --- a/src/tensors/indexmanipulations.jl +++ b/src/tensors/indexmanipulations.jl @@ -219,7 +219,7 @@ See also [`permute`](@ref) for creating a new tensor. α::Number = One(), β::Number = Zero(), backend::AbstractBackend = TO.DefaultBackend(), allocator = TO.DefaultAllocator() ) - @boundscheck spacecheck_transform(permute, tdst, tsrc, p) + @boundscheck spacecheck_transform(permute, space(tdst), space(tsrc), p) @timeit_debug GLOBAL_TIMER "permute!/braid!" begin tdst′, tsrc′, p′, _, conjsrc′, α′, β′ = unwrap_adjoints(tdst, tsrc, p, nothing, false, α, β) @inbounds _braid!(tdst′, tsrc′, p′, conjsrc′, allind(tsrc′), α′, β′, backend, allocator) @@ -309,7 +309,7 @@ See also [`braid`](@ref) for creating a new tensor. α::Number = One(), β::Number = Zero(), backend::AbstractBackend = TO.DefaultBackend(), allocator = TO.DefaultAllocator() ) - @boundscheck spacecheck_transform(braid, tdst, tsrc, p, levels) + @boundscheck spacecheck_transform(braid, space(tdst), space(tsrc), p, levels) @timeit_debug GLOBAL_TIMER "permute!/braid!" begin tdst′, tsrc′, p′, levels′, conjsrc′, α′, β′ = unwrap_adjoints(tdst, tsrc, p, levels, false, α, β) @inbounds _braid!(tdst′, tsrc′, p′, conjsrc′, levels′, α′, β′, backend, allocator) @@ -382,7 +382,7 @@ end α::Number = One(), β::Number = Zero(), backend::AbstractBackend = TO.DefaultBackend(), allocator = TO.DefaultAllocator() ) - @boundscheck spacecheck_transform(transpose, tdst, tsrc, p) + @boundscheck spacecheck_transform(transpose, space(tdst), space(tsrc), p) @timeit_debug GLOBAL_TIMER "transpose!" begin tdst′, tsrc′, p′, _, conjsrc′, α′, β′ = unwrap_adjoints(tdst, tsrc, p, nothing, false, α, β) @inbounds _transpose!(tdst′, tsrc′, p′, conjsrc′, α′, β′, backend, allocator) @@ -505,8 +505,6 @@ for (operation, manipulation) in ( end end -spacecheck_transform(f, tdst::AbstractTensorMap, tsrc::AbstractTensorMap, args...) = - spacecheck_transform(f, space(tdst), space(tsrc), args...) @noinline function spacecheck_transform(f, Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple) check_spacetype(Vdst, Vsrc) f(Vsrc, p) == Vdst || @@ -604,9 +602,10 @@ end @propagate_inbounds function _braid!( tdst, tsrc, p::Index2Tuple, conjsrc::Bool, levels::IndexTuple, α, β, backend, allocator ) - @boundscheck spacecheck_transform(permute, tdst, tsrc, p, conjsrc) + Vdst, Vsrc = space(tdst), space(tsrc) + @boundscheck spacecheck_transform(permute, Vdst, Vsrc, p, conjsrc) all(has_array_view, (tdst, tsrc)) && return _dense_transform!(tdst, tsrc, p, conjsrc, α, β, backend, allocator) - transformer = treebraider(tdst, tsrc, p, conjsrc, levels) + transformer = treebraider(storagetype(tdst), Vdst, Vsrc, p, conjsrc, levels) return add_transform!(tdst, tsrc, p, conjsrc, transformer, α, β, backend, allocator) end @@ -614,9 +613,10 @@ end @propagate_inbounds function _transpose!( tdst, tsrc, p::Index2Tuple, conjsrc::Bool, α, β, backend, allocator ) - @boundscheck spacecheck_transform(permute, tdst, tsrc, p, conjsrc) + Vdst, Vsrc = space(tdst), space(tsrc) + @boundscheck spacecheck_transform(permute, Vdst, Vsrc, p, conjsrc) all(has_array_view, (tdst, tsrc)) && return _dense_transform!(tdst, tsrc, p, conjsrc, α, β, backend, allocator) - transformer = treetransposer(tdst, tsrc, p, conjsrc) + transformer = treetransposer(storagetype(tdst), Vdst, Vsrc, p, conjsrc) return add_transform!(tdst, tsrc, p, conjsrc, transformer, α, β, backend, allocator) end diff --git a/src/tensors/tensoroperations.jl b/src/tensors/tensoroperations.jl index 14e803c47..e11a1edf0 100644 --- a/src/tensors/tensoroperations.jl +++ b/src/tensors/tensoroperations.jl @@ -236,7 +236,10 @@ function trace_permute!( return tdst end -function _trace_permute!(::UniqueFusion, tdst, tsrc, (p₁, p₂), (q₁, q₂), α, β, backend) +function _trace_permute!( + ::UniqueFusion, tdst::AbstractTensorMap, tsrc::AbstractTensorMap, + (p₁, p₂), (q₁, q₂), α, β, backend + ) scale!(tdst, β) r₁, r₂ = (p₁..., q₁...), (p₂..., q₂...) N₁, N₂ = length(p₁), length(p₂) @@ -261,7 +264,10 @@ function _trace_permute!(::UniqueFusion, tdst, tsrc, (p₁, p₂), (q₁, q₂), return tdst end -function _trace_permute!(::FusionStyle, tdst, tsrc, (p₁, p₂), (q₁, q₂), α, β, backend) +function _trace_permute!( + ::FusionStyle, tdst::AbstractTensorMap, tsrc::AbstractTensorMap, + (p₁, p₂), (q₁, q₂), α, β, backend + ) scale!(tdst, β) r₁, r₂ = (p₁..., q₁...), (p₂..., q₂...) N₁, N₂ = length(p₁), length(p₂) diff --git a/src/tensors/treetransformers.jl b/src/tensors/treetransformers.jl index 990e50d2d..b274c6f8c 100644 --- a/src/tensors/treetransformers.jl +++ b/src/tensors/treetransformers.jl @@ -68,7 +68,7 @@ struct GenericTreeTransformer{T, N} <: TreeTransformer structure_src::Vector{StridedStructure{N}} end -function UniqueTreeTransformer(::Type{A}, transform, p, Vdst, Vsrc, conjsrc::Bool) where {A} +function UniqueTreeTransformer(::Type{A}, transform, p, Vdst::HomSpace, Vsrc::HomSpace, conjsrc::Bool) where {A} t₀ = Base.time() spacecheck_transform(permute, Vdst, Vsrc, p, conjsrc) @@ -93,7 +93,7 @@ function UniqueTreeTransformer(::Type{A}, transform, p, Vdst, Vsrc, conjsrc::Boo return transformer end -function GenericTreeTransformer(::Type{A}, transform, p, Vdst, Vsrc, conjsrc::Bool) where {A} +function GenericTreeTransformer(::Type{A}, transform, p, Vdst::HomSpace, Vsrc::HomSpace, conjsrc::Bool) where {A} t₀ = Base.time() spacecheck_transform(permute, Vdst, Vsrc, p, conjsrc) # the fusion blocks that are transformed are those of the adjoint space for a conjugated source @@ -183,11 +183,6 @@ function TreeTransformer( end # braid is special because it has levels -function treebraider( - tdst::AbstractTensorMap, tsrc::AbstractTensorMap, p::Index2Tuple, conjsrc::Bool, levels::IndexTuple - ) - return treebraider(storagetype(tdst), space(tdst), space(tsrc), p, conjsrc, levels) -end @cached function treebraider( A::Type{TA}, Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple, conjsrc::Bool, levels::IndexTuple )::treetransformertype(A, Vdst, Vsrc) where {TA} @@ -199,9 +194,6 @@ end return TreeTransformer(A, fusiontreebraider, p, Vdst, Vsrc, conjsrc) end -function treetransposer(tdst::AbstractTensorMap, tsrc::AbstractTensorMap, p::Index2Tuple, conjsrc::Bool) - return treetransposer(storagetype(tdst), space(tdst), space(tsrc), p, conjsrc) -end @cached function treetransposer( A::Type{TA}, Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple, conjsrc::Bool )::treetransformertype(A, Vdst, Vsrc) where {TA} From e3dbf79f31df971941d76a8422cece56da0b645c Mon Sep 17 00:00:00 2001 From: lkdvos Date: Thu, 15 Oct 2026 08:41:56 -0400 Subject: [PATCH 4/6] Fix JET reports in code merged from main - `_recouple!`: pass `true`/`false` as gemm scalars so they cannot be union-split independently of `U`'s eltype - `nothing_or_block`: only accept a `Diagonal` wrapping a `SectorVector`, the only one `block` supports - `_fsbraid`: assert the `FusionTreeBlock` before the final `repartition` - drop `numind(::AbstractTensorMap)`, which the generic `numind(x) = numin(x) + numout(x)` already covers Co-Authored-By: Claude Opus 5.5 --- src/factorizations/pullbacks.jl | 2 +- src/fusiontrees/braiding_manipulations.jl | 2 +- src/tensors/abstracttensor.jl | 1 - src/tensors/indexmanipulations.jl | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/factorizations/pullbacks.jl b/src/factorizations/pullbacks.jl index fe2b186b6..a568a6316 100644 --- a/src/factorizations/pullbacks.jl +++ b/src/factorizations/pullbacks.jl @@ -1,5 +1,5 @@ nothing_or_block(x, c) = isnothing(x) ? x : block(x, c) -nothing_or_block(x::Diagonal, c) = block(MAK.diagonal(parent(x)), c) +nothing_or_block(x::Diagonal{T, <:SectorVector{T}}, c) where {T} = block(MAK.diagonal(parent(x)), c) for pullback! in ( :qr_pullback!, :lq_pullback!, :left_polar_pullback!, :right_polar_pullback!, diff --git a/src/fusiontrees/braiding_manipulations.jl b/src/fusiontrees/braiding_manipulations.jl index ce08f510c..368e326a7 100644 --- a/src/fusiontrees/braiding_manipulations.jl +++ b/src/fusiontrees/braiding_manipulations.jl @@ -332,7 +332,7 @@ end if N₂ == 0 return dst => U else - dst, U_tmp = repartition(dst, N₁) + dst, U_tmp = repartition(dst::FusionTreeBlock{I}, N₁) U = U_tmp * U return dst => U end diff --git a/src/tensors/abstracttensor.jl b/src/tensors/abstracttensor.jl index 0413d295b..d8ca7354f 100644 --- a/src/tensors/abstracttensor.jl +++ b/src/tensors/abstracttensor.jl @@ -314,7 +314,6 @@ InnerProductStyle(t::AbstractTensorMap) = InnerProductStyle(typeof(t)) numout(t::AbstractTensorMap) = numout(typeof(t)) numin(t::AbstractTensorMap) = numin(typeof(t)) -numind(t::AbstractTensorMap) = numind(typeof(t)) # tensor characteristics: data structure and properties #------------------------------------------------------ diff --git a/src/tensors/indexmanipulations.jl b/src/tensors/indexmanipulations.jl index dcb8262de..f6623c197 100644 --- a/src/tensors/indexmanipulations.jl +++ b/src/tensors/indexmanipulations.jl @@ -737,7 +737,7 @@ function _recouple!( buffer::CPUStorage{Complex{R}}, buffer_dst::StridedView, buffer_src::StridedView, U::Matrix{R} ) where {R <: LinearAlgebra.BlasReal} rbuffer = reinterpret(R, buffer) - LinearAlgebra.BLAS.gemm!('N', 'T', one(R), _realview(rbuffer, buffer_src), U, zero(R), _realview(rbuffer, buffer_dst)) + LinearAlgebra.BLAS.gemm!('N', 'T', true, _realview(rbuffer, buffer_src), U, false, _realview(rbuffer, buffer_dst)) return nothing end # real view of the complex column-major view `b` into the reinterpreted buffer `rbuffer` From 364152dfbc268290292706273ba90b7f7512af5e Mon Sep 17 00:00:00 2001 From: lkdvos Date: Thu, 15 Oct 2026 10:56:28 -0400 Subject: [PATCH 5/6] Drop the schur DefaultAlgorithm forwarding and its JET ignores TensorKit has no schur implementation for tensors, so `schur_full`/`schur_vals` already fail in `select_algorithm`. The forwarding methods only gave JET an unimplemented `DiagonalAlgorithm` path to report; MatrixAlgebraKit#276 itself has been fixed upstream. Co-Authored-By: Claude Opus 5.5 --- src/factorizations/matrixalgebrakit.jl | 1 - test/other/jet.jl | 6 ------ 2 files changed, 7 deletions(-) diff --git a/src/factorizations/matrixalgebrakit.jl b/src/factorizations/matrixalgebrakit.jl index 5974a60de..17b8cb660 100644 --- a/src/factorizations/matrixalgebrakit.jl +++ b/src/factorizations/matrixalgebrakit.jl @@ -103,7 +103,6 @@ MAK.zero!(t::AbstractTensorMap) = zerovector!(t) for f in [ :lq_full, :lq_compact, :lq_null, :qr_full, :qr_compact, :qr_null, - :schur_full, :schur_vals, :eig_full, :eig_vals, :eig_trunc, :eig_trunc_no_error, :eigh_full, :eigh_vals, :eigh_trunc, :eigh_trunc_no_error, :svd_full, :svd_compact, :svd_trunc, :svd_trunc_no_error, :svd_vals, diff --git a/test/other/jet.jl b/test/other/jet.jl index a091799bf..d611625e5 100644 --- a/test/other/jet.jl +++ b/test/other/jet.jl @@ -4,12 +4,6 @@ using JET # Reports that are not TensorKit's to fix. Each entry needs an upstream issue. const IGNORED = ( - # `schur_full`/`schur_vals` route `Diagonal` inputs to `DiagonalAlgorithm`, which schur does - # not implement, so `schur_full(::DiagonalTensorMap)` throws a `MethodError`. This reproduces - # with a plain `LinearAlgebra.Diagonal` and no TensorKit involved: - # https://github.com/QuantumKitHub/MatrixAlgebraKit.jl/issues/276 - JET.LastFrameMethod(:schur_full!), - JET.LastFrameMethod(:schur_vals!), # `local variable kwargs may be undefined` inside `GenericTreeTransformer`, coming entirely # from Base's `@debug` expansion (`local msg, kwargs` bound in a short-circuit guard) rather # than from any TensorKit code: https://github.com/aviatesk/JET.jl/issues/860 From f46f0a0384273f95a855ffdd44e1c5dea9cac5b5 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Thu, 15 Oct 2026 11:46:08 -0400 Subject: [PATCH 6/6] fix _dense_strides for ndims = 1 --- ext/TensorKitGPUArraysExt.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/TensorKitGPUArraysExt.jl b/ext/TensorKitGPUArraysExt.jl index 948f9fa75..66e867e8b 100644 --- a/ext/TensorKitGPUArraysExt.jl +++ b/ext/TensorKitGPUArraysExt.jl @@ -195,7 +195,7 @@ struct DeviceGenericTreeTransformer{VO <: AbstractVector{Int}, DA <: DeviceUniqu end # strides of a dense array of shape `sz` -_dense_strides(size::Dims) = (1, Base.front(cumprod(size))...) +_dense_strides(size::Dims) = Base.front((1, cumprod(size)...)) # `permute(Vsrc, p) == Vdst` is enforced when the transformer is built, so the permuted # source shape always matches `sz_dst` and the two views share Cartesian inds.