From a9ab90bfaaa9b1a49e81cd1a808168ba44c7f312 Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Mon, 19 Oct 2026 19:22:31 +0200 Subject: [PATCH] Globals through their bindings, as native code A global that is not a constant (`PARTITION_KIND_GLOBAL` in the world of the code) is read and written through its binding's `value` field, as Julia's code generator does (`emit_globalop`): - reads are an unordered load (`jtbaa_binding`) with a null check that throws `UndefVarError` through `ijl_undefined_var_error`; - assignments of globals of type `Any` are a release store and the binding's write barrier. This also covers `getglobal` and `setglobal!` (default order) with constant arguments. Other bindings (imported, unresolved) and assignments of typed globals, which convert the value, still go through the runtime. GPUCompiler's validation recognizes these accesses ("use of a non-constant global"); calls of `getglobal` it reports as dynamic invocations. Assisted-by: Claude Code (Opus 5.5) --- NEWS.md | 4 +++ src/builtins.jl | 88 +++++++++++++++++++++++++++++++++++++++++++--- src/exceptions.jl | 4 ++- src/runtime.jl | 4 +++ test/exceptions.jl | 25 +++++++++++++ 5 files changed, 119 insertions(+), 6 deletions(-) diff --git a/NEWS.md b/NEWS.md index e53f8a7..23f5ec9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -74,3 +74,7 @@ - Layouts for targets whose data layout aligns a field differently than Julia does (e.g. `Int128` on SPIR-V, Metal and BPF): a packed struct with the fields at Julia's offsets and explicit padding. +- Globals (not constants) are read and written through their binding, as native code + does: an unordered load of the binding's value (with `UndefVarError` from the runtime) + and, for globals of type `Any`, a release store and the binding's write barrier; also + for `getglobal` and `setglobal!` with constant arguments. diff --git a/src/builtins.jl b/src/builtins.jl index 41b826d..8f7aa5d 100644 --- a/src/builtins.jl +++ b/src/builtins.jl @@ -295,9 +295,71 @@ emit_new_dynamic!(fc::FnCtx, T::CGVal, args, @nospecialize(typ)) = emit_new_runtime(fc, T, CGVal[args...], typ) # a read of a global that is not constant, by the runtime -nonconst_global(fc::FnCtx, ref::GlobalRef) = - emit_dynamic_call(fc, const_value(fc, Core.getglobal), - CGVal[const_value(fc, ref.mod), const_value(fc, ref.name)], Any) +# A global (not a constant) is read and written through its binding's `value`, as native +# code does (`emit_globalop`): with an unordered load, or a release store and the binding's +# write barrier. Bindings of other kinds (imported, not yet resolved) go through the +# runtime, as do assignments of globals with a declared type, which convert the value. +const BINDING_VALUE_OFFSET = fieldoffset(Core.Binding, Base.fieldindex(Core.Binding, :value)) + +# the binding of the global `ref` in the world of the code and its declared type, if it is +# a global +function direct_binding(fc::FnCtx, ref::GlobalRef) + p = Base.lookup_binding_partition(codegen_world(fc), ref) + Base.binding_kind(p) == Base.PARTITION_KIND_GLOBAL || return nothing + return convert(Core.Binding, ref), p.restriction +end + +# the address of the value of the binding `bnd`: an untracked pointer into it, as native +# code addresses it (through the global that holds the binding's address) +function binding_value_address(fc::FnCtx, bnd::Core.Binding) + t = literal_pointer(fc, bnd) + raw = t isa LLVM.AddrSpaceCastInst ? LLVM.operands(t)[1] : LLVM.addrspacecast!(fc.builder, t, T_ptr()) + return LLVM.inbounds_gep!(fc.builder, LLVM.Int8Type(), raw, [iptr(fc, BINDING_VALUE_OFFSET)]) +end + +function nonconst_global(fc::FnCtx, ref::GlobalRef) + d = direct_binding(fc, ref) + if d === nothing + return emit_dynamic_call(fc, const_value(fc, Core.getglobal), + CGVal[const_value(fc, ref.mod), const_value(fc, ref.name)], Any) + end + bnd, T = d + b = fc.builder + v = LLVM.load!(b, T_tracked(), binding_value_address(fc, bnd)) + LLVM.ordering!(v, LLVM.API.LLVMAtomicOrderingUnordered) + LLVM.alignment!(v, config(fc).target.pointer_size) + tbaa!(fc, v, :binding) + # undefined: an UndefVarError, from the runtime as native code's + f = current_function(fc) + err = LLVM.BasicBlock(f, "err") + ok = LLVM.BasicBlock(f, "ok") + LLVM.br!(b, nonnull(fc, v), ok, err) + LLVM.position!(b, LLVM.at_end(err)) + rooted(x) = LLVM.addrspacecast!(b, literal_pointer(fc, x), LLVM.PointerType(AddrSpace.CalleeRooted)) + uve = declare_runtime(fc.cgm.mod, dialect(fc), :jl_undefined_var_error) + LLVM.call!(b, LLVM.function_type(uve), uve, LLVM.Value[rooted(ref.name), rooted(ref.mod)]) + LLVM.unreachable!(b) + LLVM.position!(b, LLVM.at_end(ok)) + return boxed(T isa Type ? T : Any, v) +end + +# `global ref = v` of a global of type `Any` through its binding, as native code does; +# `false` for other bindings +function emit_binding_assignment!(fc::FnCtx, ref::GlobalRef, v::CGVal) + d = direct_binding(fc, ref) + (d === nothing || d[2] !== Any) && return false + bnd, _ = d + b = fc.builder + p = emit_box(fc, v) + addr = binding_value_address(fc, bnd) + st = LLVM.store!(b, p, addr) + LLVM.ordering!(st, LLVM.API.LLVMAtomicOrderingRelease) + LLVM.alignment!(st, config(fc).target.pointer_size) + tbaa!(fc, st, :binding) + write_barrier!(b, fc.cgm.mod, dialect(fc), literal_pointer(fc, bnd), + LLVM.addrspacecast!(b, addr, T_derived()), p) + return true +end # the field index of the constant field `f` of `T` function field_index(@nospecialize(T), f::CGVal) @@ -482,8 +544,23 @@ function emit_getglobal(fc::FnCtx, args::Vector{CGVal}, @nospecialize(rt)) m, s = constant_value(args[1]), constant_value(args[2]) (m isa Module && s isa Symbol) || return nothing c = const_global(fc.cgm.world, m, s) - c === nothing && return nothing - return const_value(fc, something(c)) + c === nothing || return const_value(fc, something(c)) + # a global, through its binding + ref = GlobalRef(m, s) + direct_binding(fc, ref) === nothing && return nothing + return convert_repr(fc, nonconst_global(fc, ref), rt) +end + +# `setglobal!(m, s, x, [order])` of a global of type `Any` with the default (release) order, +# through its binding +function emit_setglobal!(fc::FnCtx, args::Vector{CGVal}, @nospecialize(rt)) + (length(args) in (3, 4) && isconstant(args[1]) && isconstant(args[2])) || return nothing + length(args) == 4 && !(isconstant(args[4]) && constant_value(args[4]) === :release) && + return nothing + m, s = constant_value(args[1]), constant_value(args[2]) + (m isa Module && s isa Symbol) || return nothing + emit_binding_assignment!(fc, GlobalRef(m, s), args[3]) || return nothing + return convert_repr(fc, args[3], rt) end # `isdefined(x, field)` with a constant field of a concrete type @@ -519,6 +596,7 @@ const BUILTINS = IdDict{Any,Function}( Core.throw => emit_throw, Core.sizeof => emit_sizeof, Core.getglobal => emit_getglobal, + Core.setglobal! => emit_setglobal!, ) """ diff --git a/src/exceptions.jl b/src/exceptions.jl index 19c6471..0f71117 100644 --- a/src/exceptions.jl +++ b/src/exceptions.jl @@ -227,7 +227,8 @@ function emit_exception_head!(fc::FnCtx, idx::Int, ex::Expr, @nospecialize(typ)) end return ghost(Nothing, nothing) elseif head === :(=) - # the assignment of a global: `setglobal!` with release order, as native code + # the assignment of a global: through its binding, or `setglobal!` with release + # order, as native code lhs = ex.args[1] if lhs isa Core.BindingPartition lhs = (ccall(:jl_binding_partition_owner, Any, (Any,), lhs)::Core.Binding).globalref @@ -235,6 +236,7 @@ function emit_exception_head!(fc::FnCtx, idx::Int, ex::Expr, @nospecialize(typ)) lhs isa GlobalRef || unsupported("an assignment to ", typeof(lhs)) rhs = operand(fc, ex.args[2]) rhs.kind === Bottom && return rhs + emit_binding_assignment!(fc, lhs, rhs) && return rhs emit_dynamic_call_raw(fc, const_value(fc, Core.setglobal!), CGVal[const_value(fc, lhs.mod), const_value(fc, lhs.name), rhs, const_value(fc, :release)]) diff --git a/src/runtime.jl b/src/runtime.jl index 6741b15..5a20571 100644 --- a/src/runtime.jl +++ b/src/runtime.jl @@ -49,6 +49,10 @@ function declare_runtime(mod::LLVM.Module, dialect::JuliaIR.Dialect, name::Symbo push!(f.function_attributes, LLVM.EnumAttribute("willreturn")) elseif name === :jl_current_exception f = LLVM.Function(mod, sym, LLVM.FunctionType(T_tracked(), [T_ptr()])) + elseif name === :jl_undefined_var_error + # `void (jl_sym_t *var, jl_value_t *scope)` + f = LLVM.Function(mod, sym, LLVM.FunctionType(LLVM.VoidType(), [T_rooted, T_rooted])) + push!(f.function_attributes, LLVM.EnumAttribute("noreturn")) elseif name === :jl_throw f = LLVM.Function(mod, sym, LLVM.FunctionType(LLVM.VoidType(), [T_rooted])) push!(f.function_attributes, LLVM.EnumAttribute("noreturn")) diff --git a/test/exceptions.jl b/test/exceptions.jl index 4485628..d9eac14 100644 --- a/test/exceptions.jl +++ b/test/exceptions.jl @@ -37,6 +37,12 @@ function e_undef(c) end e_global_counter = 0 e_assign(x) = (global e_global_counter = x; e_global_counter) +global e_typed_global::Int = 1 +e_typed_read() = e_typed_global + 1 +e_typed_assign(x) = (global e_typed_global = x; e_typed_global) +e_read_undefined() = e_never_assigned +global e_never_assigned +e_read_imported() = Base.Filesystem.JL_O_RDONLY + 0 # (an imported constant) @testset "exceptions" begin @@ -66,6 +72,25 @@ end @cgtest e_undef(false) # UndefVarError @test cgrun(e_assign, 7) == 7 @test e_global_counter == 7 + # globals with a declared type, and declared globals without a value + @cgtest e_typed_read() + @test cgrun(e_typed_assign, 5) == 5 + @test e_typed_global == 5 + @cgtest e_read_undefined() +end + +@testset "globals" begin + # read and written through the binding, as native code does + @test @filecheck begin + @check_label "define" + @check_not "@ijl_apply_generic" + @check "store atomic ptr addrspace(10) %{{[0-9]+}}, ptr %{{[0-9]+}} release" + @check "load atomic ptr addrspace(10), ptr %{{[0-9]+}} unordered" + @check "call void @ijl_undefined_var_error" + test_ir(e_assign, (Int,)) + end + # a typed global is assigned through the runtime, which converts the value + @test occursin("@ijl_apply_generic", test_ir(e_typed_assign, (Int,))) end @testset "code" begin