From d3af3f7da2f6b11b3f5eaef0c217e244487c9507 Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Mon, 3 Aug 2026 17:25:51 +0200 Subject: [PATCH] Use ABI queries from Julia Instead of reimplementing them and adjusting when they change upstream Assisted-by: Claude Code (Opus 5) --- src/GPUCompiler.jl | 1 + src/abi.jl | 163 +++++++++++++++++++++++++++++++++++++++++++++ src/gcn.jl | 8 ++- src/irgen.jl | 112 +++++++++++++++++++++++-------- src/jlgen.jl | 43 +++++++----- src/validation.jl | 4 +- test/native/abi.jl | 133 ++++++++++++++++++++++++++++++++++++ 7 files changed, 416 insertions(+), 48 deletions(-) create mode 100644 src/abi.jl create mode 100644 test/native/abi.jl diff --git a/src/GPUCompiler.jl b/src/GPUCompiler.jl index d1ef48e2..4ba76432 100644 --- a/src/GPUCompiler.jl +++ b/src/GPUCompiler.jl @@ -67,6 +67,7 @@ include("runtime.jl") # compiler implementation include("deprecated.jl") include("jlgen.jl") +include("abi.jl") include("irgen.jl") include("optim.jl") include("validation.jl") diff --git a/src/abi.jl b/src/abi.jl new file mode 100644 index 00000000..49e757c3 --- /dev/null +++ b/src/abi.jl @@ -0,0 +1,163 @@ +# Julia's specsig ABI, as reported by Julia itself. +# +# Everything here mirrors the `jl_get_specsig_layout` interface in `src/julia.h`. +# Where it is available we ask Julia how it lowers a signature instead of +# re-deriving the rules; the fallbacks in `irgen.jl` implement the same rules by +# hand for older Julia versions. + +const JL_ABI_LAYOUT_VERSION = UInt32(1) + +# jl_abi_retcc_t +const JL_ABI_RET_BOXED = Int32(0) +const JL_ABI_RET_REGISTER = Int32(1) +const JL_ABI_RET_SRET = Int32(2) +const JL_ABI_RET_UNION = Int32(3) +const JL_ABI_RET_GHOSTS = Int32(4) + +# jl_abi_argcc_t +const JL_ABI_ARG_ELIDED = Int32(0) +const JL_ABI_ARG_VALUE = Int32(1) +const JL_ABI_ARG_INDIRECT = Int32(2) +const JL_ABI_ARG_BOXED = Int32(3) + +# jl_abi_elide_t +const JL_ABI_ELIDE_NONE = Int32(0) +const JL_ABI_ELIDE_GHOST = Int32(1) +const JL_ABI_ELIDE_UNIQUEREP = Int32(2) + +struct JLAbiArgInfo + typ::Ptr{Cvoid} + cc::Int32 + param_idx::Int32 + roots_idx::Int32 + elide_reason::Int32 + _reserved::Int32 +end + +struct JLAbiLayout + version::UInt32 + specsig::Int32 + needsparams::Int32 + sigt::Ptr{Cvoid} + rettype::Ptr{Cvoid} + rettype_cc::Int32 + return_roots::UInt32 + all_roots::Int32 + union_bytes::Csize_t + union_align::Csize_t + union_minalign::Csize_t + sret_idx::Int32 + return_roots_idx::Int32 + pgcstack_idx::Int32 + nprefix_params::Int32 + nargs::Int32 + nparams::Int32 +end +JLAbiLayout() = JLAbiLayout(JL_ABI_LAYOUT_VERSION, 0, 0, C_NULL, C_NULL, 0, 0, 0, + 0, 0, 0, -1, -1, -1, 0, 0, 0) + +struct JLAbiQuery + version::UInt32 + ci::Ptr{Cvoid} + sigt::Ptr{Cvoid} + rt::Ptr{Cvoid} + is_opaque_closure::Int32 + cgparams::Ptr{Base.CodegenParams} + mod::Ptr{Cvoid} + datalayout::Ptr{UInt8} + triple::Ptr{UInt8} + name::Ptr{UInt8} + decl_out::Ptr{Ptr{Cvoid}} +end + +function _have_symbol(lib::String, sym::Symbol) + handle = try + Libdl.dlopen(Libdl.dlpath(lib)) + catch + return false + end + return Libdl.dlsym(handle, sym; throw_error=false) !== nothing +end + +const _LIBJULIA_CODEGEN = Base.isdebugbuild() ? "libjulia-codegen-debug" : "libjulia-codegen" +const _LIBJULIA_INTERNAL = Base.isdebugbuild() ? "libjulia-internal-debug" : "libjulia-internal" + +""" +Whether this Julia can report its own specsig ABI. Probed rather than +version-gated so that the feature is picked up by backports too. +""" +const HAS_ABI_LAYOUT = _have_symbol(_LIBJULIA_CODEGEN, :jl_get_specsig_layout) + +""" +Whether Julia exports the boxing predicates that decide the specsig ABI; if not, +`irgen.jl` falls back to its own copy of the rules. +""" +const HAS_DESERVES_CCALL = _have_symbol(_LIBJULIA_INTERNAL, :jl_deserves_stack) + +# jl_value_t* of an arbitrary object, including immutable ones like Types, which +# `pointer_from_objref` refuses +_value_ptr(@nospecialize x) = ccall(:jl_value_ptr, Ptr{Cvoid}, (Any,), x) + +""" + abi_layout(job; mod=nothing) + +Ask Julia for the specsig layout of `job.source`, using this job's +[`codegen_params`](@ref) — `gcstack_arg=false` removes a leading parameter and +so shifts every index. Returns `(layout::JLAbiLayout, args::Vector{JLAbiArgInfo})`. + +Pass `mod` to have the declaration built with the target's data layout and +triple. That only changes the address spaces of the emitted pointers, not the +parameter count or ordering, so callers that just want the index mapping (such +as [`classify_arguments`](@ref)) can leave it out. +""" +function abi_layout(@nospecialize(job::CompilerJob); mod::Union{Nothing,LLVM.Module}=nothing) + sigt = abi_signature(job.source) + rt = typeinf_type(job.source; interp=get_interpreter(job)) + return abi_layout(sigt, rt; params=codegen_params(job), mod) +end + +function abi_layout(@nospecialize(sigt), @nospecialize(rt); + params::Base.CodegenParams, + mod::Union{Nothing,LLVM.Module}=nothing, + is_opaque_closure::Bool=false) + HAS_ABI_LAYOUT || + error("this Julia does not provide jl_get_specsig_layout") + nargs = length((sigt::DataType).parameters) + args = Vector{JLAbiArgInfo}(undef, max(nargs, 1)) + layout = Ref(JLAbiLayout()) + pparams = Ref(params) + local ret + GC.@preserve sigt rt args layout pparams begin + query = Ref(JLAbiQuery(JL_ABI_LAYOUT_VERSION, + C_NULL, _value_ptr(sigt), _value_ptr(rt), + Int32(is_opaque_closure), + Base.unsafe_convert(Ptr{Base.CodegenParams}, pparams), + mod === nothing ? C_NULL : convert(Ptr{Cvoid}, mod.ref), + C_NULL, C_NULL, C_NULL, C_NULL)) + ret = @ccall jl_get_specsig_layout(query::Ptr{JLAbiQuery}, layout::Ptr{JLAbiLayout}, + pointer(args)::Ptr{JLAbiArgInfo}, + Int32(nargs)::Int32)::Cint + end + ret == 0 || error("jl_get_specsig_layout failed for $sigt -> $rt (code $ret)") + l = layout[] + return l, args[1:l.nargs] +end + +""" + abi_signature(source) + +The signature `source` is compiled against. This is its `specTypes` unless a +`Core.ABIOverride` replaces it, which `specTypes` alone would miss. +""" +abi_signature(mi::Core.MethodInstance) = mi.specTypes +@static if isdefined(Core, :ABIOverride) + abi_signature(ci::Core.CodeInstance) = + @static if isdefined(Base, :get_ci_abi) + Base.get_ci_abi(ci) + else + def = ci.def + def isa Core.ABIOverride ? def.abi : (def::Core.MethodInstance).specTypes + end +else + abi_signature(ci::Core.CodeInstance) = (ci.def::Core.MethodInstance).specTypes +end diff --git a/src/gcn.jl b/src/gcn.jl index dc06c8b3..7eacdfe0 100644 --- a/src/gcn.jl +++ b/src/gcn.jl @@ -97,9 +97,11 @@ function add_kernarg_address_spaces!( ) ft = function_type(f) - # find the byref parameters by checking for the byref attribute directly, - # rather than re-classifying arguments (which can fail on typed-pointer LLVM - # due to element type mismatches in classify_arguments assertions). + # find the byref parameters by checking for the byref attribute directly. + # This pass runs after optimization, so the parameter list is no longer the + # one Julia emitted; reading the attributes we ourselves applied in `irgen` + # is the only thing that stays valid. (It also sidesteps the assertions in + # `_classify_arguments_legacy`, which fire on typed-pointer LLVM.) byref_kind = LLVM.API.LLVMGetEnumAttributeKindForName("byref", 5) byref_mask = BitVector(undef, length(parameters(ft))) for i in 1:length(parameters(ft)) diff --git a/src/irgen.jl b/src/irgen.jl index 9129e20c..a4b0bb27 100644 --- a/src/irgen.jl +++ b/src/irgen.jl @@ -62,7 +62,7 @@ function irgen(@nospecialize(job::CompilerJob)) if job.config.name !== nothing LLVM.name!(entry, safe_name(job.config.name)) elseif job.config.kernel - LLVM.name!(entry, mangle_sig(job.source.specTypes)) + LLVM.name!(entry, mangle_sig(abi_signature(job.source))) end if job.config.entry_abi === :specfunc func = compiled[job.source].func @@ -547,30 +547,90 @@ end # - `name`: the name of the argument # - `idx`: the index of the argument in the LLVM function type, or `nothing` if the argument # is not passed at the LLVM level. +# - `roots_idx`: the index of the extra `.roots.` shadow parameter codegen emits for an +# aggregate holding some-but-not-all tracked pointers, or `nothing`. function classify_arguments(@nospecialize(job::CompilerJob), codegen_ft::LLVM.FunctionType; post_optimization::Bool=false) - source_sig = job.source.specTypes - source_types = [source_sig.parameters...] + # `post_optimization` asks a different question: by then our own passes have + # rewritten the parameter list (`lower_byval`, Metal's `pass_by_reference!`, + # ...), and callers want the convention of the function *as it now stands*. + # Only the untouched entry point is described by Julia's specsig ABI. + if HAS_ABI_LAYOUT && !post_optimization + return _classify_arguments_abi(job) + else + return _classify_arguments_legacy(job, codegen_ft; post_optimization) + end +end - source_argnames = Base.method_argnames(job.source.def) - while length(source_argnames) < length(source_types) +# argument names come from the method, not from Julia's codegen: the back-ends +# (Metal in particular) want the names a user would recognize, and codegen only +# reports the CodeInfo slot names. +function source_argnames(@nospecialize(job::CompilerJob), nargs::Int) + names = Base.method_argnames(job.source.def) + while length(names) < nargs # this is probably due to a trailing vararg; repeat its name - push!(source_argnames, source_argnames[end]) + push!(names, names[end]) end + return names +end + +function _classify_arguments_abi(@nospecialize(job::CompilerJob)) + layout, arginfos = abi_layout(job) + layout.specsig != 0 || + error("$(job.source) does not use the specialized signature; cannot classify arguments") + # GPUCompiler always requests gcstack_arg=false, so the only leading + # parameters that can appear are the return slots (kernels return nothing + # and so have none, but `:specfunc` entry points for ordinary functions do) + @assert layout.pgcstack_idx == -1 + + names = source_argnames(job, Int(layout.nargs)) + + # `param_idx` already counts the leading return slots; the kernel-state + # parameter only exists after optimization, which this path does not serve + args = [] + for (i, info) in enumerate(arginfos) + typ = unsafe_pointer_to_objref(info.typ) + cc = if info.cc == JL_ABI_ARG_ELIDED + GHOST + elseif info.cc == JL_ABI_ARG_VALUE + BITS_VALUE + elseif info.cc == JL_ABI_ARG_INDIRECT + BITS_REF + else + MUT_REF + end + idx = info.param_idx < 0 ? nothing : Int(info.param_idx) + 1 + roots_idx = info.roots_idx < 0 ? nothing : Int(info.roots_idx) + 1 + push!(args, (cc=cc, typ=typ, name=names[i], idx=idx, roots_idx=roots_idx)) + end + return args +end + +# The pre-`jl_get_specsig_layout` implementation: reconstruct the mapping by +# walking the signature in lockstep with the LLVM function type codegen produced. +# Kept for older Julia versions, and as the differential-test reference. +function _classify_arguments_legacy(@nospecialize(job::CompilerJob), codegen_ft::LLVM.FunctionType; + post_optimization::Bool=false) + source_sig = abi_signature(job.source) + source_types = [source_sig.parameters...] + + argnames = source_argnames(job, length(source_types)) codegen_types = parameters(codegen_ft) if post_optimization && kernel_state_type(job) !== Nothing args = [] - push!(args, (cc=KERNEL_STATE, typ=kernel_state_type(job), name=:kernel_state, idx=1)) + push!(args, (cc=KERNEL_STATE, typ=kernel_state_type(job), name=:kernel_state, + idx=1, roots_idx=nothing)) codegen_i = 2 else args = [] codegen_i = 1 end - for (source_typ, source_name) in zip(source_types, source_argnames) + for (source_typ, source_name) in zip(source_types, argnames) if isghosttype(source_typ) || Core.Compiler.isconstType(source_typ) - push!(args, (cc=GHOST, typ=source_typ, name=source_name, idx=nothing)) + push!(args, (cc=GHOST, typ=source_typ, name=source_name, idx=nothing, + roots_idx=nothing)) continue end @@ -582,19 +642,23 @@ function classify_arguments(@nospecialize(job::CompilerJob), codegen_ft::LLVM.Fu # - literal pointer values if source_typ <: Ptr || source_typ <: Core.LLVMPtr @assert llvm_source_typ == codegen_typ - push!(args, (cc=BITS_VALUE, typ=source_typ, name=source_name, idx=codegen_i)) + push!(args, (cc=BITS_VALUE, typ=source_typ, name=source_name, idx=codegen_i, + roots_idx=nothing)) # - boxed values # XXX: use `deserves_retbox` instead? elseif llvm_source_typ isa LLVM.PointerType @assert llvm_source_typ == codegen_typ - push!(args, (cc=MUT_REF, typ=source_typ, name=source_name, idx=codegen_i)) + push!(args, (cc=MUT_REF, typ=source_typ, name=source_name, idx=codegen_i, + roots_idx=nothing)) # - references to aggregates else @assert llvm_source_typ != codegen_typ - push!(args, (cc=BITS_REF, typ=source_typ, name=source_name, idx=codegen_i)) + push!(args, (cc=BITS_REF, typ=source_typ, name=source_name, idx=codegen_i, + roots_idx=nothing)) end else - push!(args, (cc=BITS_VALUE, typ=source_typ, name=source_name, idx=codegen_i)) + push!(args, (cc=BITS_VALUE, typ=source_typ, name=source_name, idx=codegen_i, + roots_idx=nothing)) end codegen_i += 1 @@ -608,18 +672,9 @@ function is_immutable_datatype(T::Type) end function is_inlinealloc(T::Type) - mayinlinealloc = (T.name.flags >> 2) & 1 == true - # FIXME: To simple - if mayinlinealloc - if !Base.datatype_pointerfree(T) - t_name(dt::DataType)=dt.name - if t_name(T).n_uninitialized != 0 - return false - end - end - return true - end - return false + # jl_datatype_isinlinealloc has been exported for a long time; no need to + # reimplement the mayinlinealloc/n_uninitialized/fielddesc rules here + ccall(:jl_datatype_isinlinealloc, Cint, (Any, Cint), T, 0) != 0 end function is_concrete_immutable(T::Type) @@ -634,14 +689,17 @@ function is_pointerfree(T::Type) end function deserves_stack(@nospecialize(T)) + if HAS_DESERVES_CCALL + return ccall(:jl_deserves_stack, Cint, (Any,), T) != 0 + end if !is_concrete_immutable(T) return false end return is_inlinealloc(T) end -deserves_argbox(T) = !deserves_stack(T) -deserves_retbox(T) = deserves_argbox(T) +deserves_argbox(@nospecialize(T)) = !deserves_stack(T) +deserves_retbox(@nospecialize(T)) = deserves_argbox(T) function deserves_sret(T, llvmT) @assert isa(T,DataType) sizeof(T) > sizeof(Ptr{Cvoid}) && !isa(llvmT, LLVM.FloatingPointType) && !isa(llvmT, LLVM.VectorType) diff --git a/src/jlgen.jl b/src/jlgen.jl index d4820377..323ecad6 100644 --- a/src/jlgen.jl +++ b/src/jlgen.jl @@ -407,6 +407,32 @@ function lookup_ci(cache::CodeCache, mi::MethodInstance, min_world::UInt, max_wo end end +""" + codegen_params(job; lookup_cb=nothing) + +The `Base.CodegenParams` this job compiles with. Anything that needs to reason +about the ABI Julia will emit (see `classify_arguments`) has to use exactly +these: `gcstack_arg=false` in particular removes a leading LLVM parameter, which +shifts every parameter index relative to the host defaults. +""" +function codegen_params(@nospecialize(job::CompilerJob); lookup_cb=nothing) + cgparams = (; + track_allocations = false, + code_coverage = false, + prefer_specsig = true, + gnu_pubnames = false, + debug_info_kind = Cint(llvm_debug_info(job)), + safepoint_on_entry = can_safepoint(job), + gcstack_arg = false) + if VERSION < v"1.12.0-DEV.1667" && lookup_cb !== nothing + cgparams = (; lookup = Base.unsafe_convert(Ptr{Nothing}, lookup_cb), cgparams...) + end + if v"1.12.0-DEV.2126" <= VERSION < v"1.13-" || VERSION >= v"1.13.0-DEV.285" + cgparams = (; force_emit_all = true, cgparams...) + end + return Base.CodegenParams(; cgparams...) +end + function compile_method_instance(@nospecialize(job::CompilerJob)) if job.source.def.primary_world > job.world error("Cannot compile $(job.source) for world $(job.world); method is only valid from world $(job.source.def.primary_world) onwards") @@ -454,22 +480,7 @@ function compile_method_instance(@nospecialize(job::CompilerJob)) end # set-up the compiler interface - debug_info_kind = llvm_debug_info(job) - cgparams = (; - track_allocations = false, - code_coverage = false, - prefer_specsig = true, - gnu_pubnames = false, - debug_info_kind = Cint(debug_info_kind), - safepoint_on_entry = can_safepoint(job), - gcstack_arg = false) - if VERSION < v"1.12.0-DEV.1667" - cgparams = (; lookup = Base.unsafe_convert(Ptr{Nothing}, lookup_cb), cgparams... ) - end - if v"1.12.0-DEV.2126" <= VERSION < v"1.13-" || VERSION >= v"1.13.0-DEV.285" - cgparams = (; force_emit_all = true , cgparams...) - end - params = Base.CodegenParams(; cgparams...) + params = codegen_params(job; lookup_cb) # generate IR GC.@preserve lookup_cb begin diff --git a/src/validation.jl b/src/validation.jl index c8e97638..59061063 100644 --- a/src/validation.jl +++ b/src/validation.jl @@ -25,7 +25,7 @@ function typeinf_type(mi::MethodInstance; interp::CC.AbstractInterpreter) end function check_method(@nospecialize(job::CompilerJob)) - ft = job.source.specTypes.parameters[1] + ft = abi_signature(job.source).parameters[1] ft <: Core.Builtin && error("$(unsafe_function_from_type(ft)) is not a generic function") for sparam in job.source.sparam_vals @@ -73,7 +73,7 @@ function explain_nonisbits(@nospecialize(dt), depth=1; maxdepth=10) end function check_invocation(@nospecialize(job::CompilerJob)) - sig = job.source.specTypes + sig = abi_signature(job.source) ft = sig.parameters[1] tt = Tuple{sig.parameters[2:end]...} diff --git a/test/native/abi.jl b/test/native/abi.jl new file mode 100644 index 00000000..203d1ffd --- /dev/null +++ b/test/native/abi.jl @@ -0,0 +1,133 @@ +@testset "ABI layout" begin + +# Signatures chosen to exercise the parts of the specsig ABI that the legacy +# lockstep walk in `_classify_arguments_legacy` gets right, plus the two it does +# not: the leading return slots, and the `.roots.` shadow parameter. +struct SmallImm + a::Int + b::Int +end +struct BigImm + a::NTuple{8,Int} +end +# inline-allocated, but only *some* fields are tracked pointers, so codegen adds +# a `.roots.` shadow parameter after the value pointer +struct SomeRoots + a::Int + b::String +end + +kernel_int(x::Int) = nothing +kernel_ghost(::Nothing, x::Int) = nothing +kernel_agg(x::SmallImm) = nothing +kernel_many(a::Int, b::Float64, c::SmallImm, d::Ptr{Int}) = nothing + +SIGNATURES = Any[ + (kernel_int, (Int,)), + (kernel_ghost, (Nothing, Int)), + (kernel_agg, (SmallImm,)), + (kernel_many, (Int, Float64, SmallImm, Ptr{Int})), +] + +@testset "differential vs. the legacy classification" begin + if !GPUCompiler.HAS_ABI_LAYOUT + @test_skip "jl_get_specsig_layout unavailable" + else + for (f, tt) in SIGNATURES + job, _ = Native.create_job(f, tt) + JuliaContext() do ctx + _, meta = GPUCompiler.compile(:llvm, job) + ft = LLVM.function_type(meta.entry) + new = GPUCompiler._classify_arguments_abi(job) + old = GPUCompiler._classify_arguments_legacy(job, ft) + @test length(new) == length(old) + for (n, o) in zip(new, old) + @test n.cc == o.cc + @test n.typ == o.typ + @test n.name == o.name + @test n.idx == o.idx + end + # and the indices actually address the emitted function + for n in new + n.idx === nothing || @test n.idx <= length(LLVM.parameters(ft)) + end + end + end + end +end + +@testset "coverage the legacy path lacks" begin + if !GPUCompiler.HAS_ABI_LAYOUT + @test_skip "jl_get_specsig_layout unavailable" + else + # An aggregate with *some* tracked pointers gets an extra `.roots.` slot + # that the legacy walk never accounts for, shifting every later index. + # (Such kernels are rejected by `check_invocation`, but the + # classification still has to be right for non-kernel jobs.) + roots_fn(x::SomeRoots, y::Int) = nothing + job, _ = Native.create_job(roots_fn, (SomeRoots, Int)) + layout, _ = GPUCompiler.abi_layout(job) + args = GPUCompiler._classify_arguments_abi(job) + @test args[2].cc == GPUCompiler.BITS_REF + @test args[2].roots_idx !== nothing + @test args[2].roots_idx == args[2].idx + 1 + # the trailing Int comes after the shadow slot; the legacy walk puts it + # in the shadow slot's place + @test args[3].idx == args[2].roots_idx + 1 + @test args[3].idx == layout.nparams + + # A `:specfunc` entry returning a large immutable takes a leading sret + # pointer, which the legacy walk never counts. + sret_fn(x::Int) = BigImm(ntuple(i -> x + i, 8)) + job, _ = Native.create_job(sret_fn, (Int,); entry_abi=:specfunc) + layout, _ = GPUCompiler.abi_layout(job) + @test layout.rettype_cc == GPUCompiler.JL_ABI_RET_SRET + @test layout.sret_idx == 0 + @test layout.nprefix_params == 1 + args = GPUCompiler._classify_arguments_abi(job) + @test args[2].cc == GPUCompiler.BITS_VALUE + @test args[2].idx == 2 # after the sret pointer + JuliaContext() do ctx + _, meta = GPUCompiler.compile(:llvm, job) + ft = LLVM.function_type(meta.entry) + @test length(LLVM.parameters(ft)) == layout.nparams + # what the legacy path used to report, for the record + old = GPUCompiler._classify_arguments_legacy(job, ft) + @test old[2].idx == 1 + @test old[2].idx != args[2].idx + end + end +end + +@testset "GPUCompiler compiles without a gcstack argument" begin + if GPUCompiler.HAS_ABI_LAYOUT + for (f, tt) in SIGNATURES + job, _ = Native.create_job(f, tt) + layout, _ = GPUCompiler.abi_layout(job) + @test layout.pgcstack_idx == -1 + @test layout.specsig == 1 # prefer_specsig is always set + end + end +end + +@testset "abi_signature" begin + plus(x::Int, y::Int) = x + y + mi = Base.method_instance(plus, (Int, Int)) + @test GPUCompiler.abi_signature(mi) === Tuple{typeof(plus),Int,Int} +end + +@testset "boxing predicates agree with Julia" begin + for T in Any[Int, Float64, Nothing, SmallImm, BigImm, SomeRoots, Ptr{Int}, + Vector{Int}, Any, Integer, Tuple{Int,Int}, Tuple{Int,Any}] + @test GPUCompiler.deserves_argbox(T) == !GPUCompiler.deserves_stack(T) + @test GPUCompiler.deserves_retbox(T) == GPUCompiler.deserves_argbox(T) + end + @test GPUCompiler.deserves_stack(Int) + @test GPUCompiler.deserves_stack(SmallImm) + @test GPUCompiler.deserves_stack(SomeRoots) + @test GPUCompiler.deserves_argbox(Vector{Int}) # mutable + @test GPUCompiler.deserves_argbox(Any) # abstract + @test GPUCompiler.deserves_argbox(Tuple{Int,Any}) # not inline-allocatable +end + +end