From fbb43ce1bdd84305aa5808097177add16e39bc07 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Wed, 21 Oct 2026 12:59:10 +0200 Subject: [PATCH 1/4] Assert the type of the target datalayout in julia_datalayout `llvm_datalayout` is called on an abstractly-typed target, so its result was inferred as `Any`, and the string concatenation in `julia_datalayout` was compiled with an edge to `*(::Any, ::String)`. Packages that add methods to `*`, like ChainRulesCore, invalidated it and the `compile_method_instance` closure that calls it. Part of #1048. --- src/interface.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interface.jl b/src/interface.jl index 1b656eaf..0ada05fe 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -61,7 +61,7 @@ llvm_targetinfo(@nospecialize(target::AbstractCompilerTarget)) = nothing # the target's datalayout, with Julia's non-integral address spaces added to it, as a # `DataLayout` the caller owns (or `nothing`) function julia_datalayout(@nospecialize(target::AbstractCompilerTarget)) - dl = llvm_datalayout(target) + dl = llvm_datalayout(target)::Union{Nothing,String} dl === nothing && return nothing LLVM.DataLayout(dl * "-ni:10:11:12:13") end From 53556d90f8fafea1921414e0f7316fdbe5ce3d3f Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Wed, 21 Oct 2026 12:59:10 +0200 Subject: [PATCH 2/4] Keep compiler code concretely typed where packages extend Base Code that calls Base functions on abstractly-typed values is compiled with edges to their abstract signatures, e.g. `Colon(::Int64, ::Real)` for `x[2:end]` on a value of unknown type. Packages loaded before GPUCompiler that extend those functions (`promote_rule` by ChainRulesCore, `Colon` by Static) then make Julia discard GPUCompiler's precompiled code when it is loaded, and recompile it for the first kernel. Assert types that are known, narrow before indexing, and use typed array literals. --- src/irgen.jl | 15 ++++++--------- src/mangling.jl | 4 ++-- src/runtime.jl | 2 +- src/validation.jl | 6 ++++-- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/irgen.jl b/src/irgen.jl index c40f508c..87a5572e 100644 --- a/src/irgen.jl +++ b/src/irgen.jl @@ -979,7 +979,7 @@ function (self::AddKernelState)(mod::LLVM.Module) position!(builder, LLVM.before(val)) state_arg = call!(builder, state_intr_ft, state_intr, Value[], "state") new_val = if val isa LLVM.CallInst - call!(builder, ft, f, [state_arg, val.arguments...], + call!(builder, ft, f, Value[state_arg, val.arguments...], val.operand_bundles) else # TODO: invoke and callbr @@ -997,13 +997,10 @@ function (self::AddKernelState)(mod::LLVM.Module) # XXX: we won't have to do this with opaque pointers. position!(builder, LLVM.before(val)) target_ft = val.called_type - new_args = map(zip(target_ft.parameters, - val.arguments)) do (param_typ, arg) - if arg.value_type != param_typ - const_bitcast(arg, param_typ) - else - arg - end + new_args = Value[] + for (param_typ, arg) in zip(target_ft.parameters, val.arguments) + push!(new_args, arg.value_type != param_typ ? + const_bitcast(arg, param_typ) : arg) end new_val = call!(builder, val.called_type, val.called_operand, new_args, val.operand_bundles) @@ -1460,7 +1457,7 @@ function add_input_arguments!(@nospecialize(job::CompilerJob), mod::LLVM.Module, position!(builder, LLVM.before(val)) new_val = if val isa LLVM.CallInst call!(builder, new_f.function_type, new_f, - [val.arguments..., callee_f.parameters[end-nargs+1:end]...], + Value[val.arguments..., callee_f.parameters[end-nargs+1:end]...], val.operand_bundles) else # TODO: invoke and callbr diff --git a/src/mangling.jl b/src/mangling.jl index 9272f05a..35a53df2 100644 --- a/src/mangling.jl +++ b/src/mangling.jl @@ -82,7 +82,7 @@ function mangle_param(@nospecialize(t), substitutions = Any[], top = false) else # encode typevars as template parameters if isempty(t.parameters) - w_types = t.name.wrapper.types + w_types = (t.name.wrapper::DataType).types if !isempty(w_types) && !isempty(w_types[end] isa Core.TypeofVararg) # If the type accepts a variable amount of parameters, # e.g. `Tuple{}`, then we mark it as empty: "Tuple<>" @@ -123,7 +123,7 @@ function mangle_param(@nospecialize(t), substitutions = Any[], top = false) # `N` is a `TypeVar` when the length is still parametric, which is what unwrapping a # `UnionAll` such as `NTuple{N,Int}` leaves behind. There is no count to repeat then, # so the type is mangled like the unbounded `Vararg{T}` it is equal to. - N = isdefined(t, :N) && isa(t.N, Integer) ? t.N : nothing + N = isdefined(t, :N) && isa(t.N, Int) ? t.N::Int : nothing if N !== nothing # For NTuple, repeat the type as needed str = "" diff --git a/src/runtime.jl b/src/runtime.jl index 52168924..f1a14605 100644 --- a/src/runtime.jl +++ b/src/runtime.jl @@ -202,7 +202,7 @@ const gc_bits = 0x3 # FIXME T_pjlvalue = convert(LLVMType, Any; allow_boxed=true) # this isn't really a function, but we abuse it to get the JIT to resolve the address - typ = LLVM.Function(current_module(builder), "jl_" * String(type_name) * "_type", + typ = LLVM.Function(current_module(builder), "jl_" * String(type_name::Symbol) * "_type", LLVM.FunctionType(T_pjlvalue)) typ_var = bitcast!(builder, typ, T_ptag) diff --git a/src/validation.jl b/src/validation.jl index 2fef286a..87e144ed 100644 --- a/src/validation.jl +++ b/src/validation.jl @@ -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 = job.source.specTypes::DataType ft = sig.parameters[1] tt = Tuple{sig.parameters[2:end]...} @@ -795,7 +795,9 @@ end function describe_call(src::CodeInfo, mi::MethodInstance, pc::Int, @nospecialize(callee), world::UInt; strict::Bool=false) stmt = src.code[pc] - (stmt isa Expr && stmt.head === :call) || return nothing + # separate checks, so that inference narrows `stmt` to an `Expr` + stmt isa Expr || return nothing + stmt.head === :call || return nothing f = constant_value(stmt.args[1], world) if callee !== nothing f === nothing && strict && return nothing From cc61ce5f00be0ec4156a08bd5613fdb3a9dabd40 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Wed, 21 Oct 2026 12:59:17 +0200 Subject: [PATCH 3/4] Import | explicitly in the runtime module The generated boxing functions use `|` from Base. Left implicit, its binding is only created when a downstream package (e.g. KernelAbstractions) precompiles code that calls them, and Julia 1.13 then reports the edges to that binding as invalidations every time that package is loaded. The code is not actually recompiled, but the spurious entries obscure real invalidations in SnoopCompile. --- src/runtime.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/runtime.jl b/src/runtime.jl index f1a14605..205092bc 100644 --- a/src/runtime.jl +++ b/src/runtime.jl @@ -13,6 +13,11 @@ using ..GPUCompiler using LLVM, LLVM.IR, LLVM.Build using LLVM.Interop +# imported explicitly, so that the binding exists before the generated functions below +# first use it. Julia 1.13 reports edges to a binding that a downstream package's +# precompilation created as invalidations (JuliaLang/julia#59135). +import Base: | + ## representation of a runtime method instance From 8edcfd110d54fd037b75dccda6c314be9f41d554 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Thu, 22 Oct 2026 20:36:27 +0200 Subject: [PATCH 4/4] Keep more of the compiler concretely typed (#1053) --- src/debug.jl | 4 +++- src/determinism.jl | 4 ++-- src/driver.jl | 8 +++++++- src/irgen.jl | 2 +- src/mangling.jl | 4 ++-- src/metal.jl | 9 +++++---- src/relocation.jl | 2 +- src/rtlib.jl | 2 +- src/utils.jl | 3 +++ src/validation.jl | 1 + 10 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/debug.jl b/src/debug.jl index 881bad94..382865a2 100644 --- a/src/debug.jl +++ b/src/debug.jl @@ -21,7 +21,9 @@ function backtrace(inst::LLVM.Instruction, bt = StackTraces.StackFrame[]) name = replace(something(scope.name, ""), r";$"=>"") isempty(name) && (name = "unknown") file = scope.file - path = file === nothing ? "unknown" : joinpath(file.directory, file.filename) + path = file === nothing ? "unknown" : + joinpath(file.directory::Union{Nothing,String}, + file.filename::Union{Nothing,String}) line = loc.line push!(bt, StackTraces.StackFrame(name, path, line)) end diff --git a/src/determinism.jl b/src/determinism.jl index 893e4307..cf39af62 100644 --- a/src/determinism.jl +++ b/src/determinism.jl @@ -78,12 +78,12 @@ function dedup_compile_units!(mod::LLVM.Module) cus = mds["llvm.dbg.cu"].operands length(cus) <= 1 && return false - canonical = Dict{Tuple,LLVM.Metadata}() + canonical = Dict{Vector{Union{Nothing,LLVM.Metadata}},LLVM.Metadata}() canonical_cus = LLVM.Metadata[] replacement = Dict{LLVM.Metadata,LLVM.Metadata}() for cu in cus cu === nothing && continue - key = Tuple(cu.operands) + key = Union{Nothing,LLVM.Metadata}[cu.operands...] canon = get!(canonical, key, cu) if canon == cu push!(canonical_cus, cu) diff --git a/src/driver.jl b/src/driver.jl index 13c3f81d..1065ca3c 100644 --- a/src/driver.jl +++ b/src/driver.jl @@ -282,7 +282,13 @@ const __llvm_initialized = Ref(false) # which was loaded in a newer world. dyn_ir, dyn_meta = @invokelatest deferred_codegen(dyn_job, job) dyn_entry_fn = dyn_meta.entry.name - merge!(compiled, dyn_meta.compiled) + # the sibling compiler's result isn't inferred + dyn_compiled = dyn_meta.compiled + if dyn_compiled isa Dict{Any,Any} + merge!(compiled, dyn_compiled) + else + Base.inferencebarrier(merge!)(compiled, dyn_compiled) + end @assert dyn_ir.context == ir.context link_relocatable!(ir, relocations, dyn_ir, dyn_meta.relocations) diff --git a/src/irgen.jl b/src/irgen.jl index 87a5572e..7ded0af6 100644 --- a/src/irgen.jl +++ b/src/irgen.jl @@ -565,7 +565,7 @@ end # is not passed at the LLVM level. function classify_arguments(@nospecialize(job::CompilerJob), codegen_ft::LLVM.FunctionType; post_optimization::Bool=false) - source_sig = job.source.specTypes + source_sig = job.source.specTypes::DataType source_types = collect(Any, source_sig.parameters) source_argnames = Base.method_argnames(job.source.def) diff --git a/src/mangling.jl b/src/mangling.jl index 35a53df2..5f58e7a0 100644 --- a/src/mangling.jl +++ b/src/mangling.jl @@ -71,7 +71,7 @@ function mangle_param(@nospecialize(t), substitutions = Any[], top = false) # check if we already know this base type str = find_substitution(t.name.wrapper) if str === nothing - tn = safe_name(t) + tn = safe_name(t)::String str = "$(length(tn))$tn" push!(substitutions, t.name.wrapper) end @@ -171,7 +171,7 @@ function mangle_param(@nospecialize(t), substitutions = Any[], top = false) bits = string(reinterpret(UInt64, t); base=16) "Ld$(bits)E" else - tn = safe_name(t) # TODO: actually does support digits... + tn = safe_name(t)::String # TODO: actually does support digits... if startswith(tn, r"\d") # C++ classes cannot start with a digit, so mangling doesn't support it tn = "_$(tn)" diff --git a/src/metal.jl b/src/metal.jl index 649f4bc1..aeff57dc 100644 --- a/src/metal.jl +++ b/src/metal.jl @@ -2271,18 +2271,19 @@ function msl_identifier(@nospecialize(T)) str = String(strip(replace(julia_type_words(T), r"[^A-Za-z0-9_]+" => "_"), '_')) return isempty(str) || isdigit(str[1]) ? "_" * str : str end -function julia_type_words(@nospecialize(x)) +# (recursive, so the result type is declared for inference) +function julia_type_words(@nospecialize(x))::String if x isa DataType - join([String(nameof(x)); map(julia_type_words, x.parameters)], "_") + join([String(nameof(x)); String[julia_type_words(p) for p in x.parameters]], "_") elseif x isa Union # (sorted, as Julia orders union members by module name, which can be generated) - join(["Union"; sort!(map(julia_type_words, Base.uniontypes(x)))], "_") + join(["Union"; sort!(String[julia_type_words(T) for T in Base.uniontypes(x)])], "_") elseif x isa UnionAll julia_type_words(Base.unwrap_unionall(x)) elseif x isa TypeVar String(x.name) elseif x isa Tuple - join(map(julia_type_words, x), "_") + join(String[julia_type_words(y) for y in x], "_") elseif x isa Union{Number,Symbol,AbstractChar,AbstractString} string(x) else diff --git a/src/relocation.jl b/src/relocation.jl index 59da580f..31bfab2a 100644 --- a/src/relocation.jl +++ b/src/relocation.jl @@ -83,7 +83,7 @@ end function resolve_relocation_target(target::CGlobalRef) if target.library === nothing # `jl_cglobal` accepts the symbol directly and does the process-wide `jl_dlfind`. - address = ccall(:jl_cglobal, Any, (Any, Any), target.symbol, UInt) + address = ccall(:jl_cglobal, Any, (Any, Any), target.symbol, UInt)::Ptr{UInt} return unsafe_load(address + target.offset) end handle = Libdl.dlopen(target.library) diff --git a/src/rtlib.jl b/src/rtlib.jl index 6c2d16a5..e38bd00c 100644 --- a/src/rtlib.jl +++ b/src/rtlib.jl @@ -37,7 +37,7 @@ function LLVM.call!(builder, rt::Runtime.RuntimeMethodInstance, args=LLVM.Value[ runtime_function_type(job, rt) end LLVM.Function(mod, rt.llvm_name, ft) - end + end::LLVM.Function ft = f.function_type if !isdeclaration(f) && (rt.name !== :gc_pool_alloc && rt.name !== :report_exception) # XXX: uses of the gc_pool_alloc intrinsic can be introduced _after_ the runtime diff --git a/src/utils.jl b/src/utils.jl index 3fc15d79..02758d79 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -72,6 +72,9 @@ end struct ExternalBackend library::String # path to the shared library, as exported by its JLL prefix::String # e.g. "NVPTX" for `NVPTXCompile` + + # JLLs are loaded lazily, so the type of their library paths isn't inferred + ExternalBackend(library, prefix) = new(library::String, prefix::String) end function api(backend::ExternalBackend, name::String) diff --git a/src/validation.jl b/src/validation.jl index 87e144ed..2b0ea8d9 100644 --- a/src/validation.jl +++ b/src/validation.jl @@ -656,6 +656,7 @@ function emitted_functions(mod::LLVM.Module, compiled) for entry in values(compiled) for name in (entry.func, entry.specfunc) name === nothing && continue + name = name::String # `compiled` holds entries of any type emitted[name] = entry # functions may have been renamed since, but not their debug information f = get(functions, name, nothing)