From 8ed2e521ba9e56bb9fed5729e71bd5386c6834ae Mon Sep 17 00:00:00 2001 From: Cody Tapscott <84105208+topolarity@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:15:34 -0500 Subject: [PATCH 01/38] Base: Replace many 1-arg `@assert`s with 2-arg (#61230) These `@assert`s are not directly problematic for `--trim` (thanks to an overload / monkey patch it does), but they can cause false positives for dynamic dispatches in JET and similar tools so it's probably best to replace them with the 2-arg variant (which just needs to print a String, rather than an `Expr` containing arbitrary Julia objects) (cherry picked from commit d64c0da64c1d487811e6b752fee28ef4304ab8f5) --- base/abstractarray.jl | 2 +- base/array.jl | 2 +- base/arrayshow.jl | 4 ++-- base/asyncevent.jl | 4 ++-- base/bitarray.jl | 2 +- base/channels.jl | 4 ++-- base/combinatorics.jl | 2 +- base/file.jl | 4 ++-- base/filesystem.jl | 2 +- base/gmp.jl | 2 +- base/hamt.jl | 2 +- base/idset.jl | 4 ++-- base/iostream.jl | 2 +- base/mpfr.jl | 2 +- base/multidimensional.jl | 2 +- base/partr.jl | 2 +- base/pkgid.jl | 2 +- base/process.jl | 4 ++-- base/reinterpretarray.jl | 2 +- base/ryu/exp.jl | 2 +- base/ryu/fixed.jl | 2 +- base/ryu/shortest.jl | 2 +- base/set.jl | 4 ++-- base/show.jl | 2 +- base/sort.jl | 8 ++++---- base/stream.jl | 20 ++++++++++---------- base/task.jl | 2 +- base/threadingconstructs.jl | 4 ++-- base/timing.jl | 2 +- base/toml/parser.jl | 4 ++-- base/views.jl | 2 +- 31 files changed, 52 insertions(+), 52 deletions(-) diff --git a/base/abstractarray.jl b/base/abstractarray.jl index a451bf56cce97..f70cd9cc3e365 100644 --- a/base/abstractarray.jl +++ b/base/abstractarray.jl @@ -3490,7 +3490,7 @@ julia> map!(+, zeros(Int, 5), 100:999, 1:3) ``` """ function map!(f::F, dest::AbstractArray, As::AbstractArray...) where {F} - @assert !isempty(As) # should dispatch to map!(f, A) + @assert !isempty(As) "should dispatch to map!(f, A)" map_n!(f, dest, As) end diff --git a/base/array.jl b/base/array.jl index 7cefac20fb377..38b81ef702311 100644 --- a/base/array.jl +++ b/base/array.jl @@ -2326,7 +2326,7 @@ function vcat(arrays::Vector{T}...) where T nd = 1 for a in arrays na = length(a) - @assert nd + na <= 1 + length(arr) # Concurrent modification of arrays? + @assert nd + na <= 1 + length(arr) "Concurrent modification of arrays?" unsafe_copyto!(arr, nd, a, 1, na) nd += na end diff --git a/base/arrayshow.jl b/base/arrayshow.jl index 903fb4f6b68bc..b456449ceee14 100644 --- a/base/arrayshow.jl +++ b/base/arrayshow.jl @@ -183,7 +183,7 @@ function _print_matrix(io, @nospecialize(X::AbstractVecOrMat), pre, sep, post, h screenwidth -= length(pre)::Int + length(post)::Int presp = repeat(" ", length(pre)::Int) # indent each row to match pre string postsp = "" - @assert textwidth(hdots) == textwidth(ddots) + @assert textwidth(hdots) == textwidth(ddots) "hdots and ddots must have same textwidth" sepsize = length(sep)::Int m, n = length(rowsA), length(colsA) # To figure out alignments, only need to look at as many rows as could @@ -421,7 +421,7 @@ _show_nonempty(io::IO, X::AbstractMatrix, prefix::String) = _show_nonempty(io, inferencebarrier(X), prefix, false, axes(X)) function _show_nonempty(io::IO, @nospecialize(X::AbstractMatrix), prefix::String, drop_brackets::Bool, axs::Tuple{AbstractUnitRange,AbstractUnitRange}) - @assert !isempty(X) + @assert !isempty(X) "X should be non-empty" limit = get(io, :limit, false)::Bool indr, indc = axs nr, nc = length(indr), length(indc) diff --git a/base/asyncevent.jl b/base/asyncevent.jl index a4a82b4aba120..b08cf7934fe43 100644 --- a/base/asyncevent.jl +++ b/base/asyncevent.jl @@ -126,13 +126,13 @@ mutable struct Timer associate_julia_struct(this.handle, this) iolock_begin() err = ccall(:uv_timer_init, Cint, (Ptr{Cvoid}, Ptr{Cvoid}), loop, this) - @assert err == 0 + @assert err == 0 "failed to initialize timer" finalizer(uvfinalize, this) ccall(:uv_update_time, Cvoid, (Ptr{Cvoid},), loop) err = ccall(:uv_timer_start, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, UInt64, UInt64), this, @cfunction(uv_timercb, Cvoid, (Ptr{Cvoid},)), timeoutms, intervalms) - @assert err == 0 + @assert err == 0 "failed to start timer" iolock_end() return this end diff --git a/base/bitarray.jl b/base/bitarray.jl index 04565411cabfb..45bcb8d772713 100644 --- a/base/bitarray.jl +++ b/base/bitarray.jl @@ -325,7 +325,7 @@ function copy_to_bitarray_chunks!(Bc::Vector{UInt64}, pos_d::Int, C::Array{Bool} bind += 1 end @inbounds if bind ≤ kd1 - @assert bind == kd1 + @assert bind == kd1 "bind != kd1" c = UInt64(0) for j = 0:ld1 c |= (UInt64(C[ind]) << j) diff --git a/base/channels.jl b/base/channels.jl index 5b57a6202dc2f..42ab075e748cf 100644 --- a/base/channels.jl +++ b/base/channels.jl @@ -345,8 +345,8 @@ of type `Channel{Any}(0)`. Returns a tuple, `(Array{Channel}, Array{Task})`, of the created channels and tasks. """ function channeled_tasks(n::Int, funcs...; ctypes=fill(Any,n), csizes=fill(0,n)) - @assert length(csizes) == n - @assert length(ctypes) == n + @assert length(csizes) == n "length(csizes) != n" + @assert length(ctypes) == n "length(ctypes) != n" chnls = map(i -> Channel{ctypes[i]}(csizes[i]), 1:n) tasks = Task[ Task(() -> f(chnls...)) for f in funcs ] diff --git a/base/combinatorics.jl b/base/combinatorics.jl index 5180f830ce187..575b43d4980d8 100644 --- a/base/combinatorics.jl +++ b/base/combinatorics.jl @@ -44,7 +44,7 @@ end # Basic functions for working with permutations @inline function _foldoneto(op, acc, ::Val{N}) where N - @assert N::Integer > 0 + @assert N::Integer > 0 "N must be positive" if @generated quote acc_0 = acc diff --git a/base/file.jl b/base/file.jl index f5e0ad2e9dc6f..f645c87966a7e 100644 --- a/base/file.jl +++ b/base/file.jl @@ -769,7 +769,7 @@ function _win_mkstemp(temppath::AbstractString) tempp, temppfx, UInt32(0), tname) windowserror("GetTempFileName", uunique == 0) lentname = something(findfirst(iszero, tname)) - @assert lentname > 0 + @assert lentname > 0 "unexpected index" resize!(tname, lentname - 1) return transcode(String, tname) end @@ -1403,7 +1403,7 @@ function readlink(path::AbstractString) if ret < 0 uv_fs_req_cleanup(req) uv_error("readlink($(repr(path)))", ret) - @assert false + @assert false "unexpected uv readlink error" end tgt = unsafe_string(ccall(:jl_uv_fs_t_ptr, Cstring, (Ptr{Cvoid},), req)) uv_fs_req_cleanup(req) diff --git a/base/filesystem.jl b/base/filesystem.jl index 36d60f1d3318a..c0abbdfd03b6d 100644 --- a/base/filesystem.jl +++ b/base/filesystem.jl @@ -268,7 +268,7 @@ function read(f::File, ::Type{UInt8}) ret = ccall(:jl_fs_read, Int32, (OS_HANDLE, Ptr{Cvoid}, Csize_t), f.handle, p, 1) uv_error("read", ret) - @assert ret <= sizeof(p) == 1 + @assert ret <= sizeof(p) == 1 "unexpected read size" ret < 1 && throw(EOFError()) return p[] % UInt8 end diff --git a/base/gmp.jl b/base/gmp.jl index ee8e620603e9d..748c256cc2401 100644 --- a/base/gmp.jl +++ b/base/gmp.jl @@ -261,7 +261,7 @@ function export!(a::AbstractVector{T}, n::BigInt; order::Integer=-1, nails::Inte count = Ref{Csize_t}() ccall((:__gmpz_export, libgmp), Ptr{T}, (Ptr{T}, Ref{Csize_t}, Cint, Csize_t, Cint, Csize_t, mpz_t), a, count, order, sizeof(T), endian, nails, n) - @assert count[] ≤ length(a) + @assert count[] ≤ length(a) "count[] > length(a)" return a, Int(count[]) end diff --git a/base/hamt.jl b/base/hamt.jl index c77c592b17e58..ff076651a9f4b 100644 --- a/base/hamt.jl +++ b/base/hamt.jl @@ -199,7 +199,7 @@ or grows the HAMT by inserting a new trie instead. end set!(trie, bi) else - @assert present + @assert present "!found && !present" # collision -> grow leaf = @inbounds trie.data[i]::Leaf{K,V} leaf_h = HashState(h, leaf.key) diff --git a/base/idset.jl b/base/idset.jl index 59b47dee64a04..e8d14a785c5bb 100644 --- a/base/idset.jl +++ b/base/idset.jl @@ -52,7 +52,7 @@ function push!(s::IdSet, @nospecialize(x)) else if s.max < length(s.list) idx = s.max - @assert !isassigned(s.list, idx + 1) + @assert !isassigned(s.list, idx + 1) "bucket is already occupied" s.list[idx + 1] = x s.max = idx + 1 else @@ -61,7 +61,7 @@ function push!(s::IdSet, @nospecialize(x)) idx = newidx[] s.max = idx < 0 ? -idx : idx + 1 end - @assert s.list[s.max] === x + @assert s.list[s.max] === x "unexpected object in bucket" setfield!(s, :idxs, ccall(:jl_idset_put_idx, Any, (Any, Any, Int), s.list, s.idxs, idx)) s.count += 1 end diff --git a/base/iostream.jl b/base/iostream.jl index c58818968c0b1..97d2f2645d785 100644 --- a/base/iostream.jl +++ b/base/iostream.jl @@ -504,7 +504,7 @@ function copyuntil(out::IOBuffer, s::IOStream, delim::UInt8; keep::Bool=false) (eof(s) || len == out.maxsize) && break len = min(2len + 64, out.maxsize) ensureroom(out, len) - @assert length(out.data) >= len + @assert length(out.data) >= len "length(out.data) < len" end return out end diff --git a/base/mpfr.jl b/base/mpfr.jl index f17ac72c7e198..83c62681ca113 100644 --- a/base/mpfr.jl +++ b/base/mpfr.jl @@ -1254,7 +1254,7 @@ function _prettify_bigfloat(s::String)::String else neg = startswith(int, '-') neg == true && (int = lstrip(int, '-')) - @assert length(int) == 1 + @assert length(int) == 1 "length(int) != 1" string(neg ? '-' : "", '0', '.', '0'^(-expo-1), int, frac == "0" ? "" : frac) end else diff --git a/base/multidimensional.jl b/base/multidimensional.jl index 28da821f5ee79..7c93047ea521a 100644 --- a/base/multidimensional.jl +++ b/base/multidimensional.jl @@ -1488,7 +1488,7 @@ function copy_to_bitarray_chunks!(Bc::Vector{UInt64}, pos_d::Int, C::StridedArra end @inbounds if bind ≤ kd1 - @assert bind == kd1 + @assert bind == kd1 "bind != kd1" c = UInt64(0) for j = 0:ld1 c |= (UInt64(unchecked_bool_convert(C[ind])) << j) diff --git a/base/partr.jl b/base/partr.jl index d488330f0c87e..0716b9dfe9ee7 100644 --- a/base/partr.jl +++ b/base/partr.jl @@ -139,7 +139,7 @@ end function multiq_insert(task::Task, priority::UInt16) tpid = ccall(:jl_get_task_threadpoolid, Int8, (Any,), task) - @assert tpid > -1 + @assert tpid > -1 "invalid tpid" heap_p = multiq_size(tpid) tp = tpid + 1 diff --git a/base/pkgid.jl b/base/pkgid.jl index 7ef7c58eee4cc..580f52ffbe17b 100644 --- a/base/pkgid.jl +++ b/base/pkgid.jl @@ -38,7 +38,7 @@ end function binunpack(s::String) io = IOBuffer(s) z = read(io, UInt8) - @assert z === 0x00 + @assert z === 0x00 "unexpected data" uuid = read(io, UInt128) name = read(io, String) return PkgId(UUID(uuid), name) diff --git a/base/process.jl b/base/process.jl index 141a7615b599b..933e3bcd8f17e 100644 --- a/base/process.jl +++ b/base/process.jl @@ -556,7 +556,7 @@ const SIGPIPE = 13 # !windows const SIGTERM = 15 function test_success(proc::Process) - @assert process_exited(proc) + @assert process_exited(proc) "process did not exit successfully" if proc.exitcode < 0 #TODO: this codepath is not currently tested throw(_UVError("could not start process " * repr(proc.cmd), proc.exitcode)) @@ -634,7 +634,7 @@ permissions). function kill(p::Process, signum::Integer=SIGTERM) iolock_begin() if process_running(p) - @assert p.handle != C_NULL + @assert p.handle != C_NULL "invalid handle" err = ccall(:uv_process_kill, Int32, (Ptr{Cvoid}, Int32), p.handle, signum) if err != 0 && err != UV_ESRCH throw(_UVError("kill", err)) diff --git a/base/reinterpretarray.jl b/base/reinterpretarray.jl index 39fee20a31227..1e0257e10ac12 100644 --- a/base/reinterpretarray.jl +++ b/base/reinterpretarray.jl @@ -267,7 +267,7 @@ to_index(i::SCartesianIndex2) = i struct SCartesianIndices2{K,R<:AbstractUnitRange{Int}} <: AbstractMatrix{SCartesianIndex2{K}} indices2::R end -SCartesianIndices2{K}(indices2::AbstractUnitRange{Int}) where {K} = (@assert K::Int > 1; SCartesianIndices2{K,typeof(indices2)}(indices2)) +SCartesianIndices2{K}(indices2::AbstractUnitRange{Int}) where {K} = (@assert K::Int > 1 "invalid index"; SCartesianIndices2{K,typeof(indices2)}(indices2)) eachindex(::IndexSCartesian2{K}, A::ReshapedReinterpretArray) where {K} = SCartesianIndices2{K}(eachindex(IndexLinear(), parent(A))) @inline function eachindex(style::IndexSCartesian2{K}, A::AbstractArray, B::AbstractArray...) where {K} diff --git a/base/ryu/exp.jl b/base/ryu/exp.jl index 4f749668867e2..cd225cdd7b8e8 100644 --- a/base/ryu/exp.jl +++ b/base/ryu/exp.jl @@ -1,7 +1,7 @@ function writeexp(buf, pos, v::T, precision=-1, plus=false, space=false, hash=false, expchar=UInt8('e'), decchar=UInt8('.'), trimtrailingzeros=false) where {T <: Base.IEEEFloat} - @assert 0 < pos <= length(buf) + @assert 0 < pos <= length(buf) "invalid pos" startpos = pos x = Float64(v) pos = append_sign(x, plus, space, buf, pos) diff --git a/base/ryu/fixed.jl b/base/ryu/fixed.jl index 96777059bc284..a5e178c4c4d1f 100644 --- a/base/ryu/fixed.jl +++ b/base/ryu/fixed.jl @@ -1,7 +1,7 @@ function writefixed(buf, pos, v::T, precision=-1, plus=false, space=false, hash=false, decchar=UInt8('.'), trimtrailingzeros=false) where {T <: Base.IEEEFloat} - @assert 0 < pos <= length(buf) + @assert 0 < pos <= length(buf) "invalid pos" startpos = pos x = Float64(v) pos = append_sign(x, plus, space, buf, pos) diff --git a/base/ryu/shortest.jl b/base/ryu/shortest.jl index 13d72f225f867..0add782757ecd 100644 --- a/base/ryu/shortest.jl +++ b/base/ryu/shortest.jl @@ -229,7 +229,7 @@ function writeshortest(buf::AbstractVector{UInt8}, pos, x::T, plus=false, space=false, hash=true, precision=-1, expchar=UInt8('e'), padexp=false, decchar=UInt8('.'), typed=false, compact=false) where {T} - @assert 0 < pos <= length(buf) + @assert 0 < pos <= length(buf) "invalid pos" # special cases if x == 0 if typed && x isa Float16 diff --git a/base/set.jl b/base/set.jl index 8b8f3d44603c8..aa97a55e78e58 100644 --- a/base/set.jl +++ b/base/set.jl @@ -881,8 +881,8 @@ askey(k, ::AbstractSet) = k function _replace!(new::Callable, res::Union{AbstractDict,AbstractSet}, A::Union{AbstractDict,AbstractSet}, count::Int) - @assert res isa AbstractDict && A isa AbstractDict || - res isa AbstractSet && A isa AbstractSet + @assert (res isa AbstractDict && A isa AbstractDict || + res isa AbstractSet && A isa AbstractSet) "type mismatch" count == 0 && return res c = 0 if res === A # cannot replace elements while iterating over A diff --git a/base/show.jl b/base/show.jl index 0701d4c50558b..83d16cc054e3f 100644 --- a/base/show.jl +++ b/base/show.jl @@ -3336,7 +3336,7 @@ function print_partition(io::IO, partition::Core.BindingPartition) print(io, "explicit `import` from ") print(io, partition_restriction(partition).globalref) else - @assert kind == PARTITION_KIND_GLOBAL + @assert kind == PARTITION_KIND_GLOBAL "unexpected partition kind" print(io, "global variable with type ") print(io, partition_restriction(partition)) end diff --git a/base/sort.jl b/base/sort.jl index f434aa3ed4e88..5c47335a37ef1 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -599,7 +599,7 @@ struct WithoutMissingVector{T, U} <: AbstractVector{T} end Base.@propagate_inbounds function Base.getindex(v::WithoutMissingVector, i::Integer) out = v.data[i] - @assert !(out isa Missing) + @assert !(out isa Missing) "encountered `missing` in WithoutMissingVector" out::eltype(v) end Base.@propagate_inbounds function Base.setindex!(v::WithoutMissingVector, x, i::Integer) @@ -1251,13 +1251,13 @@ function move!(v, target, source) # This function never dominates runtime—only add `@inbounds` if you can demonstrate a # performance improvement. And if you do, also double check behavior when `target` # is out of bounds. - @assert length(target) == length(source) + @assert length(target) == length(source) "length mismatch" if length(target) == 1 || isdisjoint(target, source) for (i, j) in zip(target, source) v[i], v[j] = v[j], v[i] end else - @assert minimum(source) <= minimum(target) + @assert minimum(source) <= minimum(target) "range mismatch" reverse!(v, minimum(source), maximum(target)) reverse!(v, minimum(target), maximum(target)) end @@ -1328,7 +1328,7 @@ function _sort!(v::AbstractVector, a::BracketedSort, o::Ordering, kw) # Specifically, this means that expected_middle_ln == ln, so # ln <= ... + 2.0expected_middle_ln && return ... # will trigger. - @assert false + @assert false "this should never happen" # But if it does happen, the kernel reduces to 0, hi elseif lo_signpost_i <= lo diff --git a/base/stream.jl b/base/stream.jl index 7b227458ec552..d2c1b93c4d2f9 100644 --- a/base/stream.jl +++ b/base/stream.jl @@ -918,8 +918,8 @@ readbytes!(s::LibuvStream, a::Vector{UInt8}, nb = length(a)) = readbytes!(s, a, function readbytes!(s::LibuvStream, a::Vector{UInt8}, nb::Int) iolock_begin() sbuf = s.buffer - @assert sbuf.seekable == false - @assert sbuf.maxsize >= nb + @assert sbuf.seekable == false "buffer should not be seekable" + @assert sbuf.maxsize >= nb "insufficient buffer size" function wait_locked(s, buf, nb) while bytesavailable(buf) < nb @@ -966,8 +966,8 @@ end function unsafe_read(s::LibuvStream, p::Ptr{UInt8}, nb::UInt) iolock_begin() sbuf = s.buffer - @assert sbuf.seekable == false - @assert sbuf.maxsize >= nb + @assert sbuf.seekable == false "buffer should not be seekable" + @assert sbuf.maxsize >= nb "insufficient buffer size" function wait_locked(s, buf, nb) while bytesavailable(buf) < nb @@ -1002,7 +1002,7 @@ end function read(this::LibuvStream, ::Type{UInt8}) iolock_begin() sbuf = this.buffer - @assert sbuf.seekable == false + @assert sbuf.seekable == false "buffer should not be seekable" while bytesavailable(sbuf) < 1 iolock_end() eof(this) && throw(EOFError()) @@ -1017,7 +1017,7 @@ function readavailable(this::LibuvStream) wait_readnb(this, 1) # unlike the other `read` family of functions, this one doesn't guarantee error reporting iolock_begin() buf = this.buffer - @assert buf.seekable == false + @assert buf.seekable == false "buffer should not be seekable" bytes = take!(buf) iolock_end() return bytes @@ -1026,7 +1026,7 @@ end function copyuntil(out::IO, x::LibuvStream, c::UInt8; keep::Bool=false) iolock_begin() buf = x.buffer - @assert buf.seekable == false + @assert buf.seekable == false "buffer should not be seekable" if !occursin(c, buf) # fast path checks first x.readerror === nothing || throw(x.readerror) if isopen(x) && x.status != StatusEOF @@ -1563,7 +1563,7 @@ function readavailable(this::BufferStream) bytes = lock(this.cond) do wait_readnb(this, 1) buf = this.buffer - @assert buf.seekable == false + @assert buf.seekable == false "buffer should not be seekable" take!(buf) end return bytes @@ -1579,8 +1579,8 @@ end function readbytes!(s::BufferStream, a::Vector{UInt8}, nb::Int) sbuf = s.buffer - @assert sbuf.seekable == false - @assert sbuf.maxsize >= nb + @assert sbuf.seekable == false "buffer should not be seekable" + @assert sbuf.maxsize >= nb "insufficient buffer size" function wait_locked(s, buf, nb) while bytesavailable(buf) < nb diff --git a/base/task.jl b/base/task.jl index 9841bd947fdee..f7f2691693b66 100644 --- a/base/task.jl +++ b/base/task.jl @@ -160,7 +160,7 @@ const task_state_failed = UInt8(2) elseif st === task_state_failed return :failed else - @assert false + @assert false "unexpected state" end elseif field === :backtrace # TODO: this field name should be deprecated in 2.0 diff --git a/base/threadingconstructs.jl b/base/threadingconstructs.jl index ab1ba40461af7..6b7210f690ef8 100644 --- a/base/threadingconstructs.jl +++ b/base/threadingconstructs.jl @@ -183,7 +183,7 @@ function threading_run(fun, static) # TODO: this should be the current pool (except interactive) if there # are ever more than two pools. _result = ccall(:jl_set_task_threadpoolid, Cint, (Any, Int8), t, _sym_to_tpid(:default)) - @assert _result == 1 + @assert _result == 1 "_result != 1" end tasks[i] = t schedule(t) @@ -444,7 +444,7 @@ function _spawn_set_thrpool(t::Task, tp::Symbol) tpid = _sym_to_tpid(:default) end _result = ccall(:jl_set_task_threadpoolid, Cint, (Any, Int8), t, tpid) - @assert _result == 1 + @assert _result == 1 "_result != 1" nothing end diff --git a/base/timing.jl b/base/timing.jl index 12e41491c80cf..57dc922424595 100644 --- a/base/timing.jl +++ b/base/timing.jl @@ -532,7 +532,7 @@ function _gen_allocation_measurer(ex, fname::Symbol) b1[] - b0[] end else - @assert fname === :allocations + @assert fname === :allocations "unexpected fname" return quote Experimental.@force_compile # Note this value is unused, but without it `allocated` and `allocations` diff --git a/base/toml/parser.jl b/base/toml/parser.jl index 9c83d09d2aa54..66c338985964b 100644 --- a/base/toml/parser.jl +++ b/base/toml/parser.jl @@ -290,7 +290,7 @@ end # used to show the interval where an error happened # Right now, it is only called with a == b function point_to_line(str::AbstractString, a::Int, b::Int, context) - @assert b >= a + @assert b >= a "invalid range" a = thisind(str, a) b = thisind(str, b) pos = something(findprev('\n', str, prevind(str, a)), 0) + 1 @@ -713,7 +713,7 @@ function parse_array(l::Parser{Dates})::Err{Vector} where Dates (Dates !== nothing && ((T === Dates.Date) || (T === Dates.Time) || (T === Dates.DateTime))) # do nothing, leave as Vector{Any} new = array - else @assert false end + else @assert false "unexpected type" end push!(l.static_arrays, new) return new end diff --git a/base/views.jl b/base/views.jl index fca7e7ced9027..49b715126e5f9 100644 --- a/base/views.jl +++ b/base/views.jl @@ -26,7 +26,7 @@ function replace_ref_begin_end_!(__module__::Module, ex, withex, in_quote_contex return ex end function handle_refexpr!(__module__::Module, ref_ex::Expr, main_ex::Expr, withex, in_quote_context, escs::Int) - @assert !in_quote_context + @assert !in_quote_context "handle_refexpr! should not be called in quote context" local used_withex ref_ex.args[1], used_withex = replace_ref_begin_end_!(__module__, ref_ex.args[1], withex, in_quote_context, escs) S = gensym(:S) # temp var to cache ex.args[1] if needed. if S is a global or expression, then it has side effects to use From 8eb1cfb1910b7b2528eed52dbbc7ca65cd3f4805 Mon Sep 17 00:00:00 2001 From: JamesWrigley Date: Sat, 14 Mar 2026 00:18:17 +0100 Subject: [PATCH 02/38] Bump SparseArrays version --- .../md5 | 1 + .../sha512 | 1 + .../md5 | 1 - .../sha512 | 1 - stdlib/SparseArrays.version | 2 +- 5 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 deps/checksums/SparseArrays-5307f25727e4ea6506a740a2d99566f3e65cf105.tar.gz/md5 create mode 100644 deps/checksums/SparseArrays-5307f25727e4ea6506a740a2d99566f3e65cf105.tar.gz/sha512 delete mode 100644 deps/checksums/SparseArrays-6b7e9ef30624f25d97bce047ad781a25bebe5551.tar.gz/md5 delete mode 100644 deps/checksums/SparseArrays-6b7e9ef30624f25d97bce047ad781a25bebe5551.tar.gz/sha512 diff --git a/deps/checksums/SparseArrays-5307f25727e4ea6506a740a2d99566f3e65cf105.tar.gz/md5 b/deps/checksums/SparseArrays-5307f25727e4ea6506a740a2d99566f3e65cf105.tar.gz/md5 new file mode 100644 index 0000000000000..5e12286eb5962 --- /dev/null +++ b/deps/checksums/SparseArrays-5307f25727e4ea6506a740a2d99566f3e65cf105.tar.gz/md5 @@ -0,0 +1 @@ +34a93d1aec1126e8d36306a77f476173 diff --git a/deps/checksums/SparseArrays-5307f25727e4ea6506a740a2d99566f3e65cf105.tar.gz/sha512 b/deps/checksums/SparseArrays-5307f25727e4ea6506a740a2d99566f3e65cf105.tar.gz/sha512 new file mode 100644 index 0000000000000..017985c54873e --- /dev/null +++ b/deps/checksums/SparseArrays-5307f25727e4ea6506a740a2d99566f3e65cf105.tar.gz/sha512 @@ -0,0 +1 @@ +6aaaef5d9ca68bd85aaca277e9e1a2e24616d08cf6a3812601b073973694e9b0bc82cdde1eb1901632db0cd9c5b4310e05b4815f3503b7aa51ab7b4abbfbf234 diff --git a/deps/checksums/SparseArrays-6b7e9ef30624f25d97bce047ad781a25bebe5551.tar.gz/md5 b/deps/checksums/SparseArrays-6b7e9ef30624f25d97bce047ad781a25bebe5551.tar.gz/md5 deleted file mode 100644 index a7ac76aa09e8a..0000000000000 --- a/deps/checksums/SparseArrays-6b7e9ef30624f25d97bce047ad781a25bebe5551.tar.gz/md5 +++ /dev/null @@ -1 +0,0 @@ -44270a346f9ddd509cea805b9bc81156 diff --git a/deps/checksums/SparseArrays-6b7e9ef30624f25d97bce047ad781a25bebe5551.tar.gz/sha512 b/deps/checksums/SparseArrays-6b7e9ef30624f25d97bce047ad781a25bebe5551.tar.gz/sha512 deleted file mode 100644 index fac4f9ef0e691..0000000000000 --- a/deps/checksums/SparseArrays-6b7e9ef30624f25d97bce047ad781a25bebe5551.tar.gz/sha512 +++ /dev/null @@ -1 +0,0 @@ -24997365906fb555362223b9ec49dfa1990d09a83685e4b149e34687377ec11616d97b38de99b9d750fd99ee4f62b3d6462790f0fd26c1dfd26a1ca341ef5658 diff --git a/stdlib/SparseArrays.version b/stdlib/SparseArrays.version index 86daeecdabe21..5bda1db7ea530 100644 --- a/stdlib/SparseArrays.version +++ b/stdlib/SparseArrays.version @@ -1,4 +1,4 @@ SPARSEARRAYS_BRANCH = release-1.13 -SPARSEARRAYS_SHA1 = 6b7e9ef30624f25d97bce047ad781a25bebe5551 +SPARSEARRAYS_SHA1 = 5307f25727e4ea6506a740a2d99566f3e65cf105 SPARSEARRAYS_GIT_URL := https://github.com/JuliaSparse/SparseArrays.jl.git SPARSEARRAYS_TAR_URL = https://api.github.com/repos/JuliaSparse/SparseArrays.jl/tarball/$1 From 20ec61f5109ed5b2b314e12ca577407a10948baa Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki <40514306+aviatesk@users.noreply.github.com> Date: Sun, 15 Mar 2026 13:25:39 +0900 Subject: [PATCH 03/38] inference: Cache tombstoned const-prop results to prevent non-termination (#61264) When a const-prop frame encounters a cycle and gets poisoned, `finishinfer!` marks the result as tombstoned and sets `cache_mode = CACHE_MODE_NULL`, which prevents `promotecache!` from pushing the result to the local inference cache. Meanwhile, `constprop_cache_lookup` (added in #57545) skips tombstoned entries entirely. This combination means the same const-prop is re-attempted on every cycle iteration, causing inference to never terminate when `aggressive_constant_propagation = true`. Fix this by: - In `const_prop_call`, explicitly pushing tombstoned results to the inference cache after a successful but limited `typeinf`, and returning `nothing` to fall back to the regular inference result. - In `constprop_cache_lookup`, no longer skipping tombstoned entries so they can be found on subsequent lookups, preventing re-attempts. - In `const_prop_call` cache-hit path, checking `inf_result.tombstone` and returning `nothing` (same as the existing cycle-hit handling). - Removing the now-unnecessary tombstone check from `OverlayCodeCache.get`, where the subsequent `overridden_by_const` and `isdefined` checks already filter out such entries. Note that #57545 added `tombstone && continue` to `cache_lookup` to prevent `LimitedAccuracy` results from propagating to callers via `const_prop_result`. This change removes that skip but achieves the same protection by explicitly checking `inf_result.tombstone` in `const_prop_call` and returning `nothing` instead of using the result. Fixes #61257 Co-authored-by: Claude Opus 4.6 --- Compiler/src/abstractinterpretation.jl | 17 ++++++++++++++--- Compiler/src/inferenceresult.jl | 11 +++++++++-- Compiler/src/typeinfer.jl | 10 ++++++---- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/Compiler/src/abstractinterpretation.jl b/Compiler/src/abstractinterpretation.jl index afa95d7dbca36..3f3605d48ead0 100644 --- a/Compiler/src/abstractinterpretation.jl +++ b/Compiler/src/abstractinterpretation.jl @@ -1307,7 +1307,6 @@ end function const_prop_call(interp::AbstractInterpreter, mi::MethodInstance, result::MethodCallResult, arginfo::ArgInfo, sv::AbsIntState, concrete_eval_result::Union{Nothing,ConstCallResult}=nothing) - inf_cache = get_inference_cache(interp) 𝕃ᵢ = typeinf_lattice(interp) forwarded_argtypes = compute_forwarded_argtypes(interp, arginfo, sv) # use `cache_argtypes` that has been constructed for fresh regular inference if available @@ -1318,8 +1317,13 @@ function const_prop_call(interp::AbstractInterpreter, cache_argtypes = matching_cache_argtypes(𝕃ᵢ, mi) end argtypes = matching_cache_argtypes(𝕃ᵢ, mi, forwarded_argtypes, cache_argtypes) - inf_result = cache_lookup(𝕃ᵢ, mi, argtypes, inf_cache) - if inf_result !== nothing + inf_result = cache_lookup(𝕃ᵢ, mi, argtypes, get_inference_cache(interp)) + if inf_result === missing + # a previous const-prop attempt hit a cycle and produced a limited result; + # don't re-attempt the same work that would lead to the same limited outcome + add_remark!(interp, sv, "[constprop] Found cached but limited constant inference result") + return nothing + elseif inf_result isa InferenceResult # found the cache for this constant prop' if inf_result.result === nothing add_remark!(interp, sv, "[constprop] Found cached constant inference in a cycle") @@ -1356,6 +1360,13 @@ function const_prop_call(interp::AbstractInterpreter, pop!(callstack) return nothing end + if inf_result.tombstone + # This const-prop attempt resolved but hit a cycle and produced a limited result. + # The tombstoned entry is already cached by `promotecache!` via the normal local + # cache mechanism, so `constprop_cache_lookup` will find it on subsequent lookups. + add_remark!(interp, sv, "[constprop] Constant inference produced a limited result") + return nothing + end existing_edge = result.edge inf_result.ci_as_edge = codeinst_as_edge(interp, frame, existing_edge) @assert frame.frameid != 0 && frame.cycleid == frame.frameid diff --git a/Compiler/src/inferenceresult.jl b/Compiler/src/inferenceresult.jl index f2ee4a531d111..b985a52ac02ba 100644 --- a/Compiler/src/inferenceresult.jl +++ b/Compiler/src/inferenceresult.jl @@ -182,8 +182,8 @@ function cache_lookup(𝕃::AbstractLattice, mi::MethodInstance, given_argtypes: cache::Vector{InferenceResult}) method = mi.def::Method nargtypes = length(given_argtypes) + found_tombstone = false for cached_result in cache - cached_result.tombstone && continue # ignore deleted entries (due to LimitedAccuracy) cached_result.linfo === mi || continue cache_argtypes = cached_result.argtypes @assert length(cache_argtypes) == nargtypes "invalid `cache_argtypes` for `mi`" @@ -193,8 +193,15 @@ function cache_lookup(𝕃::AbstractLattice, mi::MethodInstance, given_argtypes: @goto next_cache end end + # Don't return tombstoned entries as cache items: they represent rejected work + # (due to LimitedAccuracy). Instead, record that a tombstone was found so the + # caller can avoid re-attempting the same const-prop that would hit the same limit. + if cached_result.tombstone + found_tombstone = true + @goto next_cache + end return cached_result @label next_cache end - return nothing + return found_tombstone ? missing : nothing end diff --git a/Compiler/src/typeinfer.jl b/Compiler/src/typeinfer.jl index ed6d37229d9bd..57d3049fea657 100644 --- a/Compiler/src/typeinfer.jl +++ b/Compiler/src/typeinfer.jl @@ -608,12 +608,14 @@ function finishinfer!(me::InferenceState, interp::AbstractInterpreter, cycleid:: # A parent may be cached still, but not this intermediate work: # we can throw everything else away now. Caching anything can confuse later # heuristics to consider it worth trying to pursue compiling this further and - # finding infinite work as a result. Avoiding caching helps to ensure there is only - # a finite amount of work that can be discovered later (although potentially still a - # large multiplier on it). + # finding infinite work as a result. Avoiding global caching helps to ensure there + # is only a finite amount of work that can be discovered later (although potentially + # still a large multiplier on it). We still allow local caching so that tombstoned + # entries can be found by `constprop_cache_lookup` to prevent re-attempting the same + # const-prop work that would hit the same limit. result.src = nothing result.tombstone = true - me.cache_mode = CACHE_MODE_NULL + me.cache_mode &= ~CACHE_MODE_GLOBAL set_inlineable!(me.src, false) else # annotate fulltree with type information, From aabfe948be4e311177382669bf24be838eecaa33 Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Wed, 18 Feb 2026 13:44:46 +0100 Subject: [PATCH 04/38] Revert "make sched_task more robust (#59669)" This reverts commit 99dddb34ae4b15796684a974188dd2fcdd341e71. --- base/task.jl | 21 ++------------------- contrib/juliac/juliac-trim-base.jl | 1 - 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/base/task.jl b/base/task.jl index 9841bd947fdee..ae82fd8163e73 100644 --- a/base/task.jl +++ b/base/task.jl @@ -1161,25 +1161,9 @@ function throwto(t::Task, @nospecialize exc) return try_yieldto(identity) end -function wait_forever() +@inline function wait_forever() while true - try - while true - wait() - end - catch e - local errs = stderr - # try to display the failure atomically - errio = IOContext(PipeBuffer(), errs::IO) - emphasize(errio, "Internal Task ") - display_error(errio, current_exceptions()) - write(errs, errio) - # victimize another random Task also - if Threads.threadid() == 1 && isa(e, InterruptException) && isempty(Workqueue) - backend = repl_backend_task() - backend isa Task && throwto(backend, e) - end - end + wait() end end @@ -1240,7 +1224,6 @@ function wait() # thread sleep logic. sched_task = get_sched_task() if ct !== sched_task - istaskdone(sched_task) && (sched_task = @task wait()) return yieldto(sched_task) end task = ccall(:jl_task_get_next, Ref{Task}, (Any, Any, Any), trypoptask, W, checktaskempty) diff --git a/contrib/juliac/juliac-trim-base.jl b/contrib/juliac/juliac-trim-base.jl index fce275551751e..baa3d4825b971 100644 --- a/contrib/juliac/juliac-trim-base.jl +++ b/contrib/juliac/juliac-trim-base.jl @@ -12,7 +12,6 @@ end depwarn(msg, funcsym; force::Bool=false) = nothing _assert_tostring(msg) = "" reinit_stdio() = nothing - wait_forever() = while true; wait(); end JuliaSyntax.enable_in_core!() = nothing init_active_project() = ACTIVE_PROJECT[] = nothing set_active_project(projfile::Union{AbstractString,Nothing}) = ACTIVE_PROJECT[] = projfile From cd8c9ef58dbc4e2a4dc4ba10a9af53ea1badb16a Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Wed, 18 Feb 2026 13:25:53 +0100 Subject: [PATCH 05/38] Revert "simplify `wait()` (#58595)" This reverts commit 905a44b8ef49cc7f5e59e7ee65b0a71877aa8f58. --- base/task.jl | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/base/task.jl b/base/task.jl index ae82fd8163e73..324415678e92f 100644 --- a/base/task.jl +++ b/base/task.jl @@ -1217,19 +1217,30 @@ function wait() process_events() # get the next task to run + result = nothing + have_result = false W = workqueue_for(Threads.threadid()) task = trypoptask(W) - if task === nothing + if !(task isa Task) # No tasks to run; switch to the scheduler task to run the # thread sleep logic. sched_task = get_sched_task() if ct !== sched_task - return yieldto(sched_task) + result = yieldto(sched_task) + have_result = true + else + task = ccall(:jl_task_get_next, Ref{Task}, (Any, Any, Any), + trypoptask, W, checktaskempty) end - task = ccall(:jl_task_get_next, Ref{Task}, (Any, Any, Any), trypoptask, W, checktaskempty) end - set_next_task(task) - return try_yieldto(ensure_rescheduled) + # We may have already switched tasks (via the scheduler task), so + # only switch if we haven't. + if !have_result + @assert task isa Task + set_next_task(task) + result = try_yieldto(ensure_rescheduled) + end + return result end if Sys.iswindows() From 0ea99be7addc1b23dd81441d625214d4b23332e1 Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Wed, 18 Feb 2026 13:27:55 +0100 Subject: [PATCH 06/38] Revert "Scheduler: Use a "scheduler" task for thread sleep (#57544)" This reverts commit 0d4d6d927d531c82e454e5a08c4bf6802cf69001. --- base/task.jl | 50 ++++++++-------------------- contrib/juliac/juliac-buildscript.jl | 1 - stdlib/Sockets/test/runtests.jl | 4 ++- test/channels.jl | 5 +-- 4 files changed, 19 insertions(+), 41 deletions(-) diff --git a/base/task.jl b/base/task.jl index 324415678e92f..8e7ac3fc53ceb 100644 --- a/base/task.jl +++ b/base/task.jl @@ -1161,16 +1161,6 @@ function throwto(t::Task, @nospecialize exc) return try_yieldto(identity) end -@inline function wait_forever() - while true - wait() - end -end - -const get_sched_task = OncePerThread{Task}() do - Task(wait_forever) -end - function ensure_rescheduled(othertask::Task) ct = current_task() W = workqueue_for(Threads.threadid()) @@ -1207,39 +1197,25 @@ end checktaskempty = Partr.multiq_check_empty +@noinline function poptask(W::StickyWorkqueue) + task = trypoptask(W) + if !(task isa Task) + task = ccall(:jl_task_get_next, Ref{Task}, (Any, Any, Any), trypoptask, W, checktaskempty) + end + set_next_task(task) + nothing +end + function wait() ct = current_task() # [task] user_time -yield-or-done-> wait_time record_running_time!(ct) - # let GC run GC.safepoint() - # check for libuv events - process_events() - - # get the next task to run - result = nothing - have_result = false W = workqueue_for(Threads.threadid()) - task = trypoptask(W) - if !(task isa Task) - # No tasks to run; switch to the scheduler task to run the - # thread sleep logic. - sched_task = get_sched_task() - if ct !== sched_task - result = yieldto(sched_task) - have_result = true - else - task = ccall(:jl_task_get_next, Ref{Task}, (Any, Any, Any), - trypoptask, W, checktaskempty) - end - end - # We may have already switched tasks (via the scheduler task), so - # only switch if we haven't. - if !have_result - @assert task isa Task - set_next_task(task) - result = try_yieldto(ensure_rescheduled) - end + poptask(W) + result = try_yieldto(ensure_rescheduled) + process_events() + # return when we come out of the queue return result end diff --git a/contrib/juliac/juliac-buildscript.jl b/contrib/juliac/juliac-buildscript.jl index 7fd46e58870f8..0b73f5a43b9d0 100644 --- a/contrib/juliac/juliac-buildscript.jl +++ b/contrib/juliac/juliac-buildscript.jl @@ -83,7 +83,6 @@ end #entrypoint(join, (Base.GenericIOBuffer{Memory{UInt8}}, Array{String, 1}, Char)) entrypoint(Base.task_done_hook, (Task,)) entrypoint(Base.wait, ()) -entrypoint(Base.wait_forever, ()) entrypoint(Base.trypoptask, (Base.StickyWorkqueue,)) entrypoint(Base.checktaskempty, ()) diff --git a/stdlib/Sockets/test/runtests.jl b/stdlib/Sockets/test/runtests.jl index 37921371b3bc5..00ffec12ffda4 100644 --- a/stdlib/Sockets/test/runtests.jl +++ b/stdlib/Sockets/test/runtests.jl @@ -556,12 +556,14 @@ end fetch(r) end - let addr = Sockets.InetAddr(ip"192.0.2.5", 4444) + let addr = Sockets.InetAddr(ip"127.0.0.1", 4444) + srv = listen(addr) s = Sockets.TCPSocket() Sockets.connect!(s, addr) r = @async close(s) @test_throws Base._UVError("connect", Base.UV_ECANCELED) Sockets.wait_connected(s) fetch(r) + close(srv) end end diff --git a/test/channels.jl b/test/channels.jl index 721eb478bd13a..f646b41cfa1a0 100644 --- a/test/channels.jl +++ b/test/channels.jl @@ -463,14 +463,15 @@ end cb = first(async.cond.waitq) @test isopen(async) ccall(:uv_async_send, Cvoid, (Ptr{Cvoid},), async) + ccall(:uv_async_send, Cvoid, (Ptr{Cvoid},), async) + @test isempty(Base.Workqueue) Base.process_events() # schedule event Sys.iswindows() && Base.process_events() # schedule event (windows?) + @test length(Base.Workqueue) == 1 ccall(:uv_async_send, Cvoid, (Ptr{Cvoid},), async) @test tc[] == 0 yield() # consume event @test tc[] == 1 - ccall(:uv_async_send, Cvoid, (Ptr{Cvoid},), async) - Base.process_events() Sys.iswindows() && Base.process_events() # schedule event (windows?) yield() # consume event @test tc[] == 2 From aaeedb10ac9b3fa954072b844f47393ebbd8baaa Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Wed, 18 Mar 2026 14:32:17 +0100 Subject: [PATCH 07/38] =?UTF-8?q?compensate=20test=20for=20"Commit=203847f?= =?UTF-8?q?f1434=20=E2=80=94=20Replace=20Base.Workqueues=20with=20a=20Once?= =?UTF-8?q?PerThread=20=20=20(#58941)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/channels.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/channels.jl b/test/channels.jl index f646b41cfa1a0..c38574a8e08f2 100644 --- a/test/channels.jl +++ b/test/channels.jl @@ -464,10 +464,11 @@ end @test isopen(async) ccall(:uv_async_send, Cvoid, (Ptr{Cvoid},), async) ccall(:uv_async_send, Cvoid, (Ptr{Cvoid},), async) - @test isempty(Base.Workqueue) + W = Base.workqueue_for(Threads.threadid()) + @test isempty(W) Base.process_events() # schedule event Sys.iswindows() && Base.process_events() # schedule event (windows?) - @test length(Base.Workqueue) == 1 + @test length(W) == 1 ccall(:uv_async_send, Cvoid, (Ptr{Cvoid},), async) @test tc[] == 0 yield() # consume event From 76dac3f69d2d4b367c81081a36575724e90569dc Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Mon, 16 Mar 2026 19:09:45 -0400 Subject: [PATCH 08/38] precompilation: fix show_progress crash when bar.current goes negative (#61335) (cherry picked from commit a264a3d1ff6557fa336431412d891faa739547e7) --- base/precompilation.jl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/base/precompilation.jl b/base/precompilation.jl index 3c6c6fa996f06..10463b1d138fc 100644 --- a/base/precompilation.jl +++ b/base/precompilation.jl @@ -318,8 +318,8 @@ function show_progress(io::IO, p::MiniProgressBar; termwidth=nothing, carriagere end termwidth = @something termwidth (displaysize(io)::Tuple{Int,Int})[2] max_progress_width = max(0, min(termwidth - textwidth(p.header) - textwidth(progress_text) - 10 , p.width)) - n_filled = floor(Int, max_progress_width * perc / 100) - partial_filled = (max_progress_width * perc / 100) - n_filled + filled = max_progress_width * clamp(perc / 100, 0.0, 1.0) + (partial_filled, n_filled::Int64) = modf(filled) # get fractional / integer part n_left = max_progress_width - n_filled headers = split(p.header, ' ') to_print = sprint(; context=io) do io @@ -904,8 +904,11 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, if i > 1 print(iostr, ansi_cleartoend) end - bar.current = n_done[] - n_already_precomp[] - bar.max = n_total - n_already_precomp[] + # max(0,...) guards against a race where the print loop runs after + # n_already_precomp is incremented but before n_done is incremented, + # which would otherwise produce a negative value and crash repeat(). + bar.current = max(0, n_done[] - n_already_precomp[]) + bar.max = max(0, n_total - n_already_precomp[]) # when sizing to the terminal width subtract a little to give some tolerance to resizing the # window between print cycles termwidth = (displaysize(io)::Tuple{Int,Int})[2] - 4 From 32083c94970098b3f6db9f3ee623aee20916aaa7 Mon Sep 17 00:00:00 2001 From: matthias314 <56549971+matthias314@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:38:06 -0400 Subject: [PATCH 09/38] REPL: fix highlighting of the region and of enclosing parentheses (#60893) (cherry picked from commit be981a115c3d5c25780ed1726d911d0bb6206d3d) --- stdlib/REPL/src/LineEdit.jl | 2 +- stdlib/REPL/src/StylingPasses.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stdlib/REPL/src/LineEdit.jl b/stdlib/REPL/src/LineEdit.jl index 6786860f94b70..20efd9bd6cb93 100644 --- a/stdlib/REPL/src/LineEdit.jl +++ b/stdlib/REPL/src/LineEdit.jl @@ -671,7 +671,7 @@ function refresh_multi_line(termbuf::TerminalBuffer, terminal::UnixTerminal, buf full_input = String(buf.data[1:buf.size]) if !isempty(full_input) passes = StylingPass[] - context = StylingContext(buf_pos, regstart, regstop) + context = StylingContext(buf_pos + 1, regstart + 1, regstop) # StylingContext positions are 1-based # Add prompt-specific styling passes if the prompt has them and styling is enabled enable_style_input = prompt_obj === nothing ? false : diff --git a/stdlib/REPL/src/StylingPasses.jl b/stdlib/REPL/src/StylingPasses.jl index 3606566d341ea..04c3bef6bda40 100644 --- a/stdlib/REPL/src/StylingPasses.jl +++ b/stdlib/REPL/src/StylingPasses.jl @@ -137,7 +137,7 @@ function find_enclosing_parens(content::String, ast, cursor_pos::Int) elseif depthchange < 0 && !isempty(paren_stack) # Closing paren - pop from stack and check if cursor is inside open_pos, depth, open_ptype = pop!(paren_stack) - if open_ptype == ptype && open_pos <= cursor_pos < pos + if open_ptype == ptype && open_pos <= cursor_pos <= pos # Cursor is inside this paren pair - keep only innermost per type # Only update if this is the first pair or if it's smaller (more inner) than existing if !haskey(innermost_pairs, ptype) || (pos - open_pos) < (innermost_pairs[ptype][2] - innermost_pairs[ptype][1]) From 68e58692e50a54d3250f19f8c664bd644c100f0e Mon Sep 17 00:00:00 2001 From: Max Horn Date: Fri, 20 Mar 2026 21:23:00 +0100 Subject: [PATCH 10/38] deps: re-sign bundled zstd on macOS (#61355) Rewriting the bundled zstd executables with install_name_tool invalidates their existing signatures on macOS. Re-sign them ad hoc after updating the rpath so the CLI remains launchable for Pkg archive decompression. Add a regression test that runs the bundled zstd executable instead of only checking that libzstd can be loaded. Fixes #61354 (but see discussion there as to whether it really does?) Co-authored-by: Codex Co-authored-by: Codex (cherry picked from commit 3de619d29bdc52604fb66c2ab3e58e3ba64c3e1d) --- deps/zstd.mk | 3 +++ stdlib/Zstd_jll/test/runtests.jl | 3 +++ 2 files changed, 6 insertions(+) diff --git a/deps/zstd.mk b/deps/zstd.mk index ecce416ab3f38..7507341fa82ca 100644 --- a/deps/zstd.mk +++ b/deps/zstd.mk @@ -48,10 +48,13 @@ post-install-zstd: $(build_prefix)/manifest/zstd $(PATCHELF_MANIFEST) [ -e $(build_private_libexecdir)/zstd$(EXE) ] [ -e $(build_private_libexecdir)/zstdmt$(EXE) ] ifeq ($(OS), Darwin) + # zstd is relocated from bin/ to libexec/julia/, so its JLL rpath must be updated to keep finding ../libzstd. + # but this invalidates the existing signature, and macOS can then refuse to launch zstd, so also call codesign. for j in zstd zstdmt ; do \ [ -L $(build_private_libexecdir)/$$j ] && continue; \ install_name_tool -rpath @executable_path/$(reverse_build_private_libexecdir_rel) @loader_path/$(build_libdir_rel) $(build_private_libexecdir)/$$j 2>/dev/null || true; \ install_name_tool -rpath @loader_path/$(build_libdir_rel) @executable_path/$(reverse_build_private_libexecdir_rel) $(build_private_libexecdir)/$$j || exit 1; \ + codesign -s - -f $(build_private_libexecdir)/$$j || exit 1; \ done else ifneq (,$(findstring $(OS),Linux FreeBSD)) for j in zstd zstdmt ; do \ diff --git a/stdlib/Zstd_jll/test/runtests.jl b/stdlib/Zstd_jll/test/runtests.jl index 5cfa2a1375c73..f0b6be5aac0f4 100644 --- a/stdlib/Zstd_jll/test/runtests.jl +++ b/stdlib/Zstd_jll/test/runtests.jl @@ -4,4 +4,7 @@ using Test, Zstd_jll @testset "Zstd_jll" begin @test ccall((:ZSTD_versionNumber, libzstd), Cuint, ()) == 1_05_07 + let version = read(`$(zstd()) --version`, String) + @test contains(version, "Zstandard CLI") + end end From 705ed5de739940f392001b0e0d14ac63980cbc20 Mon Sep 17 00:00:00 2001 From: PatrickHaecker <152268010+PatrickHaecker@users.noreply.github.com> Date: Thu, 26 Mar 2026 04:35:08 +0100 Subject: [PATCH 11/38] Remove duplicated entry in NEWS.md for Julia 1.13 --- NEWS.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/NEWS.md b/NEWS.md index dc596460b9d39..8a2d55bacf9e5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -110,11 +110,6 @@ Deprecated or removed * The method `merge(combine::Callable, d::AbstractDict...)` is now deprecated to favor `mergewith` instead ([#59775]). -Deprecated or removed ---------------------- - -* The method `merge(combine::Callable, d::AbstractDict...)` is now deprecated to favor `mergewith` instead ([#59775]). - [#47102]: https://github.com/JuliaLang/julia/issues/47102 [#48507]: https://github.com/JuliaLang/julia/issues/48507 From 0cdca89ed99f6a20f7d4119e22d047e1e84526cc Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki <40514306+aviatesk@users.noreply.github.com> Date: Sat, 28 Mar 2026 04:58:51 +0900 Subject: [PATCH 12/38] subtype: isolate `Union` state in `subtype_ccheck` to prevent exponential hang (#61316) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Investigation of aviatesk/JETLS.jl#509 revealed that the root cause is a hang in the subtyping algorithm. A minimal reproducer has been added to `test/subtype.jl`, which does not terminate in a reasonable time on the current master branch. The fix was developed using Claude and Codex, with iterative cross-review between the two to arrive at what I believe is the most sound approach. However, I am not very familiar with the subtype algorithm implementation, so there may be implementation issues I have not caught. Note that the bulk of this patch was written by AI. --- `subtype_ccheck` calls `local_forall_exists_subtype`, which leaks right-side Union choices (via `pick_union_decision`) into the shared `Runions` statestack. When many Union-bounded type parameters share a common variable (e.g. 6 parameters all bounded by `Union{Ref{F},Val{F}}`), each bounds check adds decisions that `exists_subtype` must iterate over combinatorially, causing O(2^N) iterations. Make `subtype_ccheck` save and restore both the `Runions` state and variable environment (`save_env`/`restore_env`). When `pick_union_decision` added new entries during the check (detected via `Runions.used` growth) and the check succeeded, a merge loop (`ccheck_merge_env`) enumerates all successful right-side ∃ branches and merges their variable constraints via `simple_meet` (lb) / `simple_join` (ub). This yields the widest constraints valid across any successful branch, instead of discarding all constraints. A separate `ccheck_restore_metadata` step then overrides the metadata fields (`occurs_inv`, `occurs_cov`, `max_offset`, `innervars`) with the original values, since `merge_env`'s metadata merging (max for occurs, min for max_offset) tightens constraints — the opposite of what `subtype_ccheck` needs. The merge loop uses `next_union_state_from` to iterate only the ccheck's own ∃ decisions (starting from `oldRunions.used`), avoiding interference with outer Union iteration. Each branch runs a full ∀∃ check via `local_forall_exists_subtype`. The `ccheck_merging` flag prevents re-entrant merge loops from nested `subtype_ccheck` calls; the `!e->intersection` guard avoids conflicts with `intersect_all`'s own `merge_env` handling. When no Union splitting occurred, the variable constraints are deterministic and safe to keep; restoring them unconditionally introduces false negatives that break the `obvious_subtype` invariant. The `envout` array is preserved across the restore because inner `subtype_unionall` calls may have already written inferred type-parameter values that must survive. Known limitation: detection via `Runions.used` growth is imperfect — `local_forall_exists_subtype`'s exists-free path (path 1) internally pushes/pops its own `Runions`, so Union choices made there do not increase the outer `Runions.used`. Currently harmless because exists-free inputs have no exists-variable constraints to corrupt, but the detection mechanism has a structural gap. Fixes aviatesk/JETLS.jl#509. --------- Co-authored-by: Claude Opus 4.6 --- src/subtype.c | 44 +++++++++++++++++++++++++++++++++++++++++++- test/subtype.jl | 24 ++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/subtype.c b/src/subtype.c index e7a426ae7bc79..0fb6fbf61e9e0 100644 --- a/src/subtype.c +++ b/src/subtype.c @@ -679,7 +679,41 @@ static int subtype(jl_value_t *x, jl_value_t *y, jl_stenv_t *e, int param); static int local_forall_exists_subtype(jl_value_t *x, jl_value_t *y, jl_stenv_t *e, int param, int limit_slow); -// subtype for variable bounds consistency check. needs its own forall/exists environment. +// Check whether env (variable bounds: lb/ub) changed compared to saved env. +static int env_unchanged(jl_stenv_t *e, jl_savedenv_t *se) JL_NOTSAFEPOINT +{ + jl_value_t **roots = NULL; + if (se->gcframe.nroots == JL_GC_ENCODE_PUSHARGS(1)) { + jl_svec_t *sv = (jl_svec_t*)se->roots[0]; + assert(jl_is_svec(sv)); + roots = jl_svec_data(sv); + } + else if (se->gcframe.nroots) { + roots = se->roots; + } + if (!roots) + return 1; // no vars to compare + jl_varbinding_t *v = e->vars; + int i = 0; + while (v != NULL) { + if (v->lb != roots[i] || v->ub != roots[i + 1]) + return 0; + i += 3; // lb, ub, innervars + v = v->prev; + } + return 1; +} + +// Subtype check for variable bounds consistency. +// Needs its own forall/exists environment. +// +// Right-side Union choices made during bounds checks can leak into +// the shared Runions statestack, causing O(2^N) iteration in +// exists_subtype when many Union-bounded parameters are present. +// To prevent this, we monitor whether the env (variable bounds) +// changes during the check. If it does not, the Union choices are +// purely internal and we reset Runions.more to its previous value, +// preventing the outer loop from iterating over those choices. static int subtype_ccheck(jl_value_t *x, jl_value_t *y, jl_stenv_t *e) { if (jl_is_long(x) && jl_is_long(y)) @@ -694,8 +728,16 @@ static int subtype_ccheck(jl_value_t *x, jl_value_t *y, jl_stenv_t *e) return 1; if (x == (jl_value_t*)jl_any_type && jl_is_datatype(y)) return 0; + if (obviously_in_union(y, x)) + return 1; jl_saved_unionstate_t oldLunions; push_unionstate(&oldLunions, &e->Lunions); + int16_t oldRmore = e->Runions.more; + jl_savedenv_t se; + save_env(e, &se, 1); int sub = local_forall_exists_subtype(x, y, e, 0, 1); + if (env_unchanged(e, &se)) + e->Runions.more = oldRmore; + free_env(&se); pop_unionstate(&e->Lunions, &oldLunions); return sub; } diff --git a/test/subtype.jl b/test/subtype.jl index ee7864da1dd79..2b0383f0fa2d9 100644 --- a/test/subtype.jl +++ b/test/subtype.jl @@ -2793,3 +2793,27 @@ end #issue 58115 @test Tuple{Tuple{Vararg{Tuple{Vararg{Tuple{Vararg{Tuple{Vararg{Tuple{Vararg{ Union{Tuple{}, Tuple{Tuple{}}}}}}}}}}}}} , Tuple{}} <: Tuple{Tuple{Vararg{Tuple{Vararg{Tuple{Vararg{Tuple{Vararg{Tuple{Vararg{Tuple{Vararg{Union{Tuple{}, Tuple{Tuple{}}}}}}}}}}}}}}}, Tuple{}} + +# JETLS#509 — inference hang with Union-bounded parametric types +# subtype_ccheck leaked right-side Union choices into the shared +# Runions statestack, causing exponential state iteration when +# multiple Union-bounded parameters share a common variable. +struct JETLS509S{F, A<:Union{Ref{F},Val{F}}, B<:Union{Ref{F},Val{F}}, + C<:Union{Ref{F},Val{F}}, D<:Union{Ref{F},Val{F}}, + E<:Union{Ref{F},Val{F}}, G<:Union{Ref{F},Val{F}}} +end +JETLS509f(a::JETLS509S{F}, b::JETLS509S{F}, s::Union{Nothing,Ref{F}}=nothing) where {F} = 1 +let tt = Tuple{typeof(JETLS509f), + JETLS509S{F,A,B,C,D,E,G} where { + A<:Union{Ref{F},Val{F}}, B<:Union{Ref{F},Val{F}}, + C<:Union{Ref{F},Val{F}}, D<:Union{Ref{F},Val{F}}, + E<:Union{Ref{F},Val{F}}, G<:Union{Ref{F},Val{F}}}, + JETLS509S{F,A,B,C,D,E,G} where { + A<:Union{Ref{F},Val{F}}, B<:Union{Ref{F},Val{F}}, + C<:Union{Ref{F},Val{F}}, D<:Union{Ref{F},Val{F}}, + E<:Union{Ref{F},Val{F}}, G<:Union{Ref{F},Val{F}}}, + } where F + @test Base.code_typed_by_type(tt) isa Vector +end +@test !(Tuple{Union{Int16,Int8},Ref{Int16},Ref{Int16}} <: Tuple{<:Union{S,T},Ref{S},Ref{T}} where {S,T}) +@test !(Tuple{Ref{Int16},Ref{Int16},Union{Int16,Int8}} <: Tuple{Ref{S},Ref{T},<:Union{S,T}} where {S,T}) From fb9683bd282bd2b35120f2895da827730f87714d Mon Sep 17 00:00:00 2001 From: Andy Dienes <51664769+adienes@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:03:34 -0400 Subject: [PATCH 13/38] check `issimpleenoughtype` for PartialStruct during `issimplertype` (#61130) fixes https://github.com/JuliaLang/julia/issues/60715 `tmerge_types_slow` would check `issimpleenoughtype(u)` after the `PartialStruct`s were stripped from the types, but that path wasn't getting hit as it would early return via `tmerge_fast_path` Claude assisted the analysis but the proposed fix is mine --- Compiler/src/typelimits.jl | 1 + Compiler/test/inference.jl | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/Compiler/src/typelimits.jl b/Compiler/src/typelimits.jl index ce4a2a1ccbe91..d69cbf4520ee4 100644 --- a/Compiler/src/typelimits.jl +++ b/Compiler/src/typelimits.jl @@ -338,6 +338,7 @@ end typea === typeb && return true if typea isa PartialStruct aty = widenconst(typea) + issimplertype(𝕃, aty, widenconst(typeb)) || return false if typeb isa Const || typeb isa PartialStruct @assert n_initialized(typea) ≤ n_initialized(typeb) "typeb ⊑ typea is assumed" elseif typeb isa PartialStruct diff --git a/Compiler/test/inference.jl b/Compiler/test/inference.jl index 89b7de36fb26a..164fd19e43e91 100644 --- a/Compiler/test/inference.jl +++ b/Compiler/test/inference.jl @@ -5077,6 +5077,22 @@ let 𝕃ᵢ = Compiler.fallback_lattice @test t.fields == Any[Const(42), Int] end +# issue #60715 +let 𝕃 = Compiler.fallback_lattice + local fn, fn1, pn, pn1 + F(n) = iszero(n) ? Union{} : Tuple{Int, Union{Int, F(n-1)}} + P(n) = (f = F(n); Compiler.PartialStruct(𝕃, f, [Const(0), fieldtype(f, 2)])) + + n = 0 + while Compiler.issimpleenoughtype(F(n+1)) + n += 1 + fn, fn1, pn, pn1 = F(n), F(n+1), P(n), P(n+1) + @test Compiler.:⊑(𝕃, pn, pn1) + end + @test !Compiler.issimplertype(𝕃, pn1, pn) + @test !isa(Compiler.tmerge(𝕃, pn, pn1), Compiler.PartialStruct) +end + foo_empty_vararg(i...) = i[2] bar_empty_vararg(i) = foo_empty_vararg(10, 20, 30, i...) @test bar_empty_vararg(Union{}[]) === 20 @@ -6545,4 +6561,10 @@ function issue60883() end @test issue60883() === true +# issue #60715 +let + f() = 1; f(_, x...) = (0, f(x...)) + @test f(1, 2, 3) == (0, (0, (0, 1))) +end + end # module inference From 59153654bf44fdbbf07887ef97541a74773073bc Mon Sep 17 00:00:00 2001 From: Cody Tapscott <84105208+topolarity@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:25:00 -0400 Subject: [PATCH 14/38] Fix `nothrow` effects for `(s/z)ext_int` / `trunc_int` intrinsics (#61437) (cherry picked from commit cbb2be44c3e284a19a453bd82bbabe5c20353085) --- Compiler/src/tfuncs.jl | 10 ++++++++-- Compiler/test/effects.jl | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/Compiler/src/tfuncs.jl b/Compiler/src/tfuncs.jl index 98859efa8c87d..a711bd27b5dc3 100644 --- a/Compiler/src/tfuncs.jl +++ b/Compiler/src/tfuncs.jl @@ -2988,16 +2988,22 @@ function intrinsic_exct(𝕃::AbstractLattice, f::IntrinsicFunction, argtypes::V return ErrorException end - # fpext, fptrunc, fptoui, fptosi, uitofp, and sitofp have further - # restrictions on the allowed types. + # fpext, sext_int, zext_int, fptrunc, trunc_int, fptoui, fptosi, uitofp, and sitofp + # have further restrictions on the allowed types. if f === Intrinsics.fpext && !(ty <: CORE_FLOAT_TYPES && xty <: CORE_FLOAT_TYPES && Core.sizeof(ty) > Core.sizeof(xty)) return ErrorException end + if (f === Intrinsics.sext_int || f === Intrinsics.zext_int) && !(Core.sizeof(ty) > Core.sizeof(xty)) + return ErrorException + end if f === Intrinsics.fptrunc && !(ty <: CORE_FLOAT_TYPES && xty <: CORE_FLOAT_TYPES && Core.sizeof(ty) < Core.sizeof(xty)) return ErrorException end + if f === Intrinsics.trunc_int && !(Core.sizeof(ty) < Core.sizeof(xty)) + return ErrorException + end if (f === Intrinsics.fptoui || f === Intrinsics.fptosi) && !(xty <: CORE_FLOAT_TYPES) return ErrorException end diff --git a/Compiler/test/effects.jl b/Compiler/test/effects.jl index cbb77e1d8e2a3..fcd8eb8e88e88 100644 --- a/Compiler/test/effects.jl +++ b/Compiler/test/effects.jl @@ -1493,3 +1493,30 @@ function null_offset(offset) Ptr{UInt8}(C_NULL) + offset end @test null_offset(Int(100)) == Ptr{UInt8}(UInt(100)) + +# https://github.com/JuliaLang/julia/issues/61435 +function catch_error_61435(f, x) + try + f(x) + catch + return :caught + end +end +let f = (x) -> Core.Intrinsics.sext_int(Int16, x) + @test Compiler.is_nothrow(Base.infer_effects(f, (Int8,))) + @test !Compiler.is_nothrow(Base.infer_effects(f, (Int16,))) + @test !Compiler.is_nothrow(Base.infer_effects(f, (Int32,))) + @test catch_error_61435(f, Int16(0)) === :caught +end +let f = (x) -> Core.Intrinsics.zext_int(UInt16, x) + @test Compiler.is_nothrow(Base.infer_effects(f, (UInt8,))) + @test !Compiler.is_nothrow(Base.infer_effects(f, (UInt16,))) + @test !Compiler.is_nothrow(Base.infer_effects(f, (UInt32,))) + @test catch_error_61435(f, UInt16(0)) === :caught +end +let f = (x) -> Core.Intrinsics.trunc_int(Int16, x) + @test !Compiler.is_nothrow(Base.infer_effects(f, (Int8,))) + @test !Compiler.is_nothrow(Base.infer_effects(f, (Int16,))) + @test Compiler.is_nothrow(Base.infer_effects(f, (Int32,))) + @test catch_error_61435(f, Int16(0)) === :caught +end From 7005f63088240cb372f364151f7a7ce66fa13841 Mon Sep 17 00:00:00 2001 From: N5N3 <2642243996@qq.com> Date: Tue, 7 Apr 2026 06:25:52 +0800 Subject: [PATCH 15/38] =?UTF-8?q?subtype:=20ignore=20the=20rest=20?= =?UTF-8?q?=E2=88=83=20decisions=20if=20env=20is=20unchanged=20during=20`l?= =?UTF-8?q?ocal=5F=E2=88=80=5F=E2=88=83=5Fsubtype`=20(#61503)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Following up on #61316, this commit moves the `env_unchange` check and the reset of `Runion.more` to `local_forall_exists_subtype`, ensuring that similar optimizations are applied more broadly, and preventing unnecessary `save_env` cost. Additionally, `env_unchanged` now also detects diagonality changes in variables, which would fix ```julia Tuple{NTuple{2,Int}, Int8} <: Tuple{<:Union{NTuple{2,T},Tuple{S,T}}, <:T} where {S,T>:Int} ``` (cherry picked from commit d0828a87c0990f04c540ea6e5d284fc7d1f81921) --- src/subtype.c | 41 ++++++++++++++++------------------------- test/subtype.jl | 1 + 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/src/subtype.c b/src/subtype.c index 0fb6fbf61e9e0..031c9a95284a4 100644 --- a/src/subtype.c +++ b/src/subtype.c @@ -679,7 +679,9 @@ static int subtype(jl_value_t *x, jl_value_t *y, jl_stenv_t *e, int param); static int local_forall_exists_subtype(jl_value_t *x, jl_value_t *y, jl_stenv_t *e, int param, int limit_slow); -// Check whether env (variable bounds: lb/ub) changed compared to saved env. +static int is_leaf_typevar(jl_tvar_t *v) JL_NOTSAFEPOINT; + +// Check whether env (variable bounds & diagonality) changed compared to saved env. static int env_unchanged(jl_stenv_t *e, jl_savedenv_t *se) JL_NOTSAFEPOINT { jl_value_t **roots = NULL; @@ -691,29 +693,24 @@ static int env_unchanged(jl_stenv_t *e, jl_savedenv_t *se) JL_NOTSAFEPOINT else if (se->gcframe.nroots) { roots = se->roots; } - if (!roots) - return 1; // no vars to compare jl_varbinding_t *v = e->vars; - int i = 0; + int i = 0, j = 1; while (v != NULL) { - if (v->lb != roots[i] || v->ub != roots[i + 1]) - return 0; + assert(roots != NULL); + if (v->existential) { + if (v->lb != roots[i] || v->ub != roots[i + 1]) + return 0; // check if bounds changed + if (is_leaf_typevar(v->var) && v->occurs_inv == 0 && v->occurs_cov > 1 && se->buf[j] <= 1) + return 0; // check if a variable became digonal from non-diagonal + } i += 3; // lb, ub, innervars + j += 3; v = v->prev; } return 1; } -// Subtype check for variable bounds consistency. -// Needs its own forall/exists environment. -// -// Right-side Union choices made during bounds checks can leak into -// the shared Runions statestack, causing O(2^N) iteration in -// exists_subtype when many Union-bounded parameters are present. -// To prevent this, we monitor whether the env (variable bounds) -// changes during the check. If it does not, the Union choices are -// purely internal and we reset Runions.more to its previous value, -// preventing the outer loop from iterating over those choices. +// subtype for variable bounds consistency check. needs its own forall/exists environment. static int subtype_ccheck(jl_value_t *x, jl_value_t *y, jl_stenv_t *e) { if (jl_is_long(x) && jl_is_long(y)) @@ -731,13 +728,7 @@ static int subtype_ccheck(jl_value_t *x, jl_value_t *y, jl_stenv_t *e) if (obviously_in_union(y, x)) return 1; jl_saved_unionstate_t oldLunions; push_unionstate(&oldLunions, &e->Lunions); - int16_t oldRmore = e->Runions.more; - jl_savedenv_t se; - save_env(e, &se, 1); int sub = local_forall_exists_subtype(x, y, e, 0, 1); - if (env_unchanged(e, &se)) - e->Runions.more = oldRmore; - free_env(&se); pop_unionstate(&e->Lunions, &oldLunions); return sub; } @@ -1718,8 +1709,6 @@ static int local_forall_exists_subtype(jl_value_t *x, jl_value_t *y, jl_stenv_t break; if (limited || e->Runions.more == oldRmore) { // re-save env and freeze the ∃decision for previous ∀Union - // Note: We could ignore the rest `∃Union` decisions if `x` and `y` - // contain no ∃ typevar, as they have no effect on env. ini_count = count; push_unionstate(&latestLunions, &e->Lunions); re_save_env(e, &se, 1); @@ -1735,7 +1724,9 @@ static int local_forall_exists_subtype(jl_value_t *x, jl_value_t *y, jl_stenv_t } if (!sub) assert(e->Runions.more == oldRmore); - else if (limited) + else if (e->Runions.more > oldRmore && (limited || env_unchanged(e, &se))) + // Ignore the rest ∃Union decisions if env is unchanged/limited. + // As otherwise it might cause combinatorial explosion without making any difference to the result. e->Runions.more = oldRmore; free_env(&se); return sub; diff --git a/test/subtype.jl b/test/subtype.jl index 2b0383f0fa2d9..3fba7e920c738 100644 --- a/test/subtype.jl +++ b/test/subtype.jl @@ -2817,3 +2817,4 @@ let tt = Tuple{typeof(JETLS509f), end @test !(Tuple{Union{Int16,Int8},Ref{Int16},Ref{Int16}} <: Tuple{<:Union{S,T},Ref{S},Ref{T}} where {S,T}) @test !(Tuple{Ref{Int16},Ref{Int16},Union{Int16,Int8}} <: Tuple{Ref{S},Ref{T},<:Union{S,T}} where {S,T}) +@test Tuple{NTuple{2,Int}, Int8} <: Tuple{<:Union{NTuple{2,T},Tuple{S,T}}, <:T} where {S,T>:Int} From 41699d4a086a57048018096eb7baf18ffb67c013 Mon Sep 17 00:00:00 2001 From: Cody Tapscott <84105208+topolarity@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:58:22 -0400 Subject: [PATCH 16/38] Use `@static if` in `CompilerSupportLibraries_jll.__init__()` (#61516) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hopefully enough to fix JuliaC on `aarch64-linux` (https://buildkite.com/julialang/julia-master/builds/56266#019d6367-fc8d-4806-a1f1-00ec67370c77/L1315-L1319): ``` Error #2: unresolved call from statement string( getproperty( libquadmath::Any, :path::Symbol )::Any )::Any   Stacktrace: [1] __init__() @ CompilerSupportLibraries_jll ./CompilerSupportLibraries_jll/src/CompilerSupportLibraries_jll.jl:158 ``` (cherry picked from commit ea7a52ace0753a722397db5201cfb6a39d6a81a4) --- .../src/CompilerSupportLibraries_jll.jl | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/stdlib/CompilerSupportLibraries_jll/src/CompilerSupportLibraries_jll.jl b/stdlib/CompilerSupportLibraries_jll/src/CompilerSupportLibraries_jll.jl index 9a0729c50d01f..9aec22b58bf10 100644 --- a/stdlib/CompilerSupportLibraries_jll/src/CompilerSupportLibraries_jll.jl +++ b/stdlib/CompilerSupportLibraries_jll/src/CompilerSupportLibraries_jll.jl @@ -134,15 +134,13 @@ end # Conform to LazyJLLWrappers API function eager_mode() - if @isdefined(libatomic) - dlopen(libatomic) - end + dlopen(libatomic) dlopen(libgcc_s) dlopen(libgomp) - if @isdefined libquadmath + @static if @isdefined libquadmath dlopen(libquadmath) end - if @isdefined libssp + @static if @isdefined libssp dlopen(libssp) end dlopen(libgfortran) @@ -154,10 +152,10 @@ function __init__() global libatomic_path = string(libatomic.path) global libgcc_s_path = string(libgcc_s.path) global libgomp_path = string(libgomp.path) - if @isdefined libquadmath_path + @static if @isdefined libquadmath_path global libquadmath_path = string(libquadmath.path) end - if @isdefined libssp_path + @static if @isdefined libssp_path global libssp_path = string(libssp.path) end global libgfortran_path = string(libgfortran.path) From d4713ea07b11f05c290aa22925dd34f0f8c74730 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Tue, 7 Apr 2026 21:39:15 +0200 Subject: [PATCH 17/38] Fix `Base.summarysize` crash for instances of foreign types (#61517) Instances of foreign types are opaque to Julia and we can't traverse them. Teach `get_nth_pointer` about this. This in turn prevents `summarysize` from crashing when invoked on a foreign object. Fixes https://github.com/oscar-system/GAP.jl/issues/1366 (cherry picked from commit a992d84327d0b938cb69e5f670899285fba73508) --- src/datatype.c | 5 +++++ test/gcext/gcext-test.jl | 3 +++ 2 files changed, 8 insertions(+) diff --git a/src/datatype.c b/src/datatype.c index c08e9f783eb7c..3d1a8b5bb9bf4 100644 --- a/src/datatype.c +++ b/src/datatype.c @@ -2335,6 +2335,11 @@ jl_value_t *get_nth_pointer(jl_value_t *v, size_t i) uint32_t npointers = ly->npointers; if (i >= npointers) jl_bounds_error_int(v, i); + if (ly->flags.fielddesc_type == 3) { + // Foreign types can report that they contain pointers for GC purposes, + // but they do not expose an inline pointer-offset table to enumerate. + return NULL; + } const uint8_t *ptrs8 = (const uint8_t *)jl_dt_layout_ptrs(ly); const uint16_t *ptrs16 = (const uint16_t *)jl_dt_layout_ptrs(ly); const uint32_t *ptrs32 = (const uint32_t*)jl_dt_layout_ptrs(ly); diff --git a/test/gcext/gcext-test.jl b/test/gcext/gcext-test.jl index 11b0504b71c4d..eb8f0402ddcd5 100644 --- a/test/gcext/gcext-test.jl +++ b/test/gcext/gcext-test.jl @@ -66,6 +66,9 @@ end GC.gc(true) end @test Base.invokelatest(Foreign.get_nmark) > 0 + # Bugfix: the following used to crash + summarysize = Base.summarysize(Foreign.FObj()) + @test summarysize >= sizeof(Ptr) @time Base.invokelatest(Foreign.test, 10) GC.gc(true) @test Base.invokelatest(Foreign.get_nsweep) > 0 From 9671cb2777d2071a6ad63b6ec2bd40a146fab717 Mon Sep 17 00:00:00 2001 From: Nicholas Bauer Date: Tue, 7 Apr 2026 23:14:18 -0400 Subject: [PATCH 18/38] Fix performance regression in hvcat of simple matrices (#57422) Co-authored-by: Andy Dienes <51664769+adienes@users.noreply.github.com> (cherry picked from commit 211af65526b4a446ea0c4b29cc7a1b4f3cc5e58f) --- base/abstractarray.jl | 53 +++++++++++++++++++++++-------------------- base/indices.jl | 1 + 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/base/abstractarray.jl b/base/abstractarray.jl index f70cd9cc3e365..685b965a9bbea 100644 --- a/base/abstractarray.jl +++ b/base/abstractarray.jl @@ -2394,9 +2394,7 @@ end function _typed_hvncat(T::Type, ::Val{N}, xs::Number...) where N N < 0 && throw(ArgumentError("concatenation dimension must be non-negative")) - A = cat_similar(xs[1], T, (ntuple(Returns(1), Val(N - 1))..., length(xs))) - hvncat_fill!(A, false, xs) - return A + return reshape(T[xs...], (ntuple(Returns(1), Val(N - 1))..., length(xs))) end function _typed_hvncat(::Type{T}, ::Val{N}, as::AbstractArray...) where {T, N} @@ -2720,12 +2718,13 @@ function _typed_hvncat_shape(::Type{T}, shape::NTuple{N, Tuple}, row_first, as:: # copy into final array A = cat_similar(as[1], T, ntuple(i -> outdims[i], nd)) - hvncat_fill!(A, currentdims, blockcounts, d1, d2, as) + if !any(iszero, outdims) + hvncat_fill!(A, currentdims, blockcounts, d1, d2, as) + end return A end -function hvncat_fill!(A::AbstractArray{T, N}, scratch1::Vector{Int}, scratch2::Vector{Int}, - d1::Int, d2::Int, as::Tuple) where {T, N} +function hvncat_fill!(A::AbstractArray{T, N}, scratch1::Vector{Int}, scratch2::Vector{Int}, d1::Int, d2::Int, as::Tuple) where {T, N} N > 1 || throw(ArgumentError("dimensions of the destination array must be at least 2")) length(scratch1) == length(scratch2) == N || throw(ArgumentError("scratch vectors must have as many elements as the destination array has dimensions")) @@ -2733,42 +2732,46 @@ function hvncat_fill!(A::AbstractArray{T, N}, scratch1::Vector{Int}, scratch2::V 0 < d2 < 3 && d1 != d2 || throw(ArgumentError("d1 and d2 must be either 1 or 2, exclusive.")) - outdims = size(A) + outdimsprod = cumprod(size(A)) offsets = scratch1 inneroffsets = scratch2 for a ∈ as + startindex = CartesianIndex(ntuple(i -> offsets[i] + 1, Val(N))) if isa(a, AbstractArray) - for ai ∈ a - @inbounds Ai = hvncat_calcindex(offsets, inneroffsets, outdims, N) - A[Ai] = ai - - @inbounds for j ∈ 1:N - inneroffsets[j] += 1 - inneroffsets[j] < cat_size(a, j) && break - inneroffsets[j] = 0 + if !isempty(a) + if length(a) > 4 + endindex = CartesianIndex(ntuple(i -> offsets[i] + cat_size(a, i), Val(N))) + @inbounds A[startindex:endindex] = a + else + for ai ∈ a + @inbounds Ai = hvncat_calcindex(offsets, inneroffsets, outdimsprod, N) + @inbounds A[Ai] = ai + @inbounds for j ∈ 1:N + inneroffsets[j] += 1 + inneroffsets[j] < cat_size(a, j) && break + inneroffsets[j] = 0 + end + end end end else - @inbounds Ai = hvncat_calcindex(offsets, inneroffsets, outdims, N) - A[Ai] = a + @inbounds A[startindex] = a end - @inbounds for j ∈ (d1, d2, 3:N...) - offsets[j] += cat_size(a, j) - offsets[j] < outdims[j] && break - offsets[j] = 0 + @inbounds for i ∈ (d1, d2, 3:N...) + offsets[i] += cat_size(a, i) + offsets[i] < cat_size(A, i) && break + offsets[i] = 0 end end end @propagate_inbounds function hvncat_calcindex(offsets::Vector{Int}, inneroffsets::Vector{Int}, - outdims::Tuple{Vararg{Int}}, nd::Int) + outdimsprod::NTuple{N, Int}, nd::Int) where {N} Ai = inneroffsets[1] + offsets[1] + 1 for j ∈ 2:nd increment = inneroffsets[j] + offsets[j] - for k ∈ 1:j-1 - increment *= outdims[k] - end + increment *= outdimsprod[j - 1] Ai += increment end Ai diff --git a/base/indices.jl b/base/indices.jl index 0d0e56b12be4b..7965cb6e42152 100644 --- a/base/indices.jl +++ b/base/indices.jl @@ -215,6 +215,7 @@ end # those are the permutations that preserve the order of the non-singleton # dimensions. function setindex_shape_check(X::AbstractArray, I::Integer...) + @inline li = ndims(X) lj = length(I) i = j = 1 From a5b1dacd2d0edf05555efc1e1ee6dec5673bb9c5 Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Thu, 9 Apr 2026 12:17:17 +0200 Subject: [PATCH 19/38] store crc of sysimage at build time and stop recomputing it on every startup (#61459) (cherry picked from commit cf93ea6dfec5e20e1aa0036f15dedc303c96d045) --- src/aotcompile.cpp | 5 +++++ src/julia.h | 1 + src/staticdata.c | 11 ++++++++--- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/aotcompile.cpp b/src/aotcompile.cpp index 71e331d13e233..9db6d52189d96 100644 --- a/src/aotcompile.cpp +++ b/src/aotcompile.cpp @@ -2148,6 +2148,7 @@ void jl_dump_native_impl(void *native_code, sysimgM.setOverrideStackAlignment(OverrideStackAlignment); int compression = jl_options.compress_sysimage ? 15 : 0; + uint32_t sysimg_checksum = jl_crc32c(0, z->buf, z->size); ArrayRef sysimg_data{z->buf, (size_t)z->size}; SmallVector compressed_data; if (compression) { @@ -2176,6 +2177,10 @@ void jl_dump_native_impl(void *native_code, addComdat(new GlobalVariable(sysimgM, len->getType(), true, GlobalVariable::ExternalLinkage, len, "jl_system_image_size"), TheTriple); + Constant *checksum_val = ConstantInt::get(Type::getInt32Ty(Context), sysimg_checksum); + addComdat(new GlobalVariable(sysimgM, checksum_val->getType(), true, + GlobalVariable::ExternalLinkage, + checksum_val, "jl_system_image_checksum"), TheTriple); const char *unpack_func = compression ? "jl_image_unpack_zstd" : "jl_image_unpack_uncomp"; auto unpack = new GlobalVariable(sysimgM, DL.getIntPtrType(Context), true, diff --git a/src/julia.h b/src/julia.h index b2fd8f66de17c..db24a404dea5f 100644 --- a/src/julia.h +++ b/src/julia.h @@ -2147,6 +2147,7 @@ typedef struct { const char *data; size_t size; uint64_t base; + uint32_t checksum; } jl_image_buf_t; struct _jl_image_t; diff --git a/src/staticdata.c b/src/staticdata.c index 053b0b37603b4..f16f4c6fe7411 100644 --- a/src/staticdata.c +++ b/src/staticdata.c @@ -3551,20 +3551,26 @@ static void jl_prefetch_system_image(const char *data, size_t size) JL_DLLEXPORT void jl_image_unpack_uncomp(void *handle, jl_image_buf_t *image) { size_t *plen; + uint32_t *pchecksum; jl_dlsym(handle, "jl_system_image_size", (void **)&plen, 1, 0); jl_dlsym(handle, "jl_system_image_data", (void **)&image->data, 1, 0); jl_dlsym(handle, "jl_image_pointers", (void**)&image->pointers, 1, 0); + jl_dlsym(handle, "jl_system_image_checksum", (void **)&pchecksum, 1, 0); image->size = *plen; + image->checksum = *pchecksum; jl_prefetch_system_image(image->data, image->size); } JL_DLLEXPORT void jl_image_unpack_zstd(void *handle, jl_image_buf_t *image) { size_t *plen; + uint32_t *pchecksum; const char *data; jl_dlsym(handle, "jl_system_image_size", (void **)&plen, 1, 0); jl_dlsym(handle, "jl_system_image_data", (void **)&data, 1, 0); jl_dlsym(handle, "jl_image_pointers", (void **)&image->pointers, 1, 0); + jl_dlsym(handle, "jl_system_image_checksum", (void **)&pchecksum, 1, 0); + image->checksum = *pchecksum; jl_prefetch_system_image(data, *plen); image->size = ZSTD_getFrameContentSize(data, *plen); size_t page_size = jl_getpagesize(); /* jl_page_size is not set yet when loading sysimg */ @@ -4390,7 +4396,7 @@ static jl_value_t *jl_restore_package_image_from_stream(ios_t *f, jl_image_t *im sysimg = &f->buf[f->bpos]; if (needs_permalloc) success = ios_readall(f, sysimg, len) == len; - if (!success || jl_crc32c(0, sysimg, len) != (uint32_t)checksum) { + if (!success) { restored = jl_get_exceptionf(jl_errorexception_type, "Error reading package image file."); JL_SIGATOMIC_END(); } @@ -4495,8 +4501,7 @@ JL_DLLEXPORT void jl_restore_system_image(jl_image_t *image, jl_image_buf_t buf) JL_SIGATOMIC_BEGIN(); ios_static_buffer(&f, (char *)buf.data, buf.size); - uint32_t checksum = jl_crc32c(0, buf.data, buf.size); - jl_restore_system_image_from_stream(&f, image, checksum); + jl_restore_system_image_from_stream(&f, image, buf.checksum); ios_close(&f); JL_SIGATOMIC_END(); From 9d78958d17c7472b3326444ae90ad73b20c692df Mon Sep 17 00:00:00 2001 From: Cody Tapscott <84105208+topolarity@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:58:54 -0400 Subject: [PATCH 20/38] Fix-up `--trim` support for `hcat` / `vcat` / etc. (#61551) Add some targeted overloads for now to allow `--trim` to support these essential functions. (cherry picked from commit e9054e302a4883ceb75df4edf4dff558c48947d3) --- contrib/juliac/juliac-trim-base.jl | 23 +++++++++++ test/trimming/trimmability.jl | 66 ++++++++++++++++++++++++++++++ test/trimming/trimming.jl | 8 +++- 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/contrib/juliac/juliac-trim-base.jl b/contrib/juliac/juliac-trim-base.jl index baa3d4825b971..020aecd06faa9 100644 --- a/contrib/juliac/juliac-trim-base.jl +++ b/contrib/juliac/juliac-trim-base.jl @@ -83,6 +83,29 @@ end setfield!(typeof(print).name, :max_args, Int32(10), :monotonic) setfield!(typeof(println).name, :max_args, Int32(10), :monotonic) setfield!(typeof(print_to_string).name, :max_args, Int32(10), :monotonic) + + # not `--trim`-compatible if these resolve to a Varargs{...} specialization, primarily + # due to `Core._apply_iterate` having no dispatch-resolved form (#57830) + setfield!(typeof(_cat).name, :max_args, Int32(10), :monotonic) + setfield!(typeof(__cat).name, :max_args, Int32(10), :monotonic) + setfield!(typeof(__cat_offset!).name, :max_args, Int32(10), :monotonic) + setfield!(typeof(cat_size_shape).name, :max_args, Int32(10), :monotonic) + setfield!(typeof(_cat_size_shape).name, :max_args, Int32(10), :monotonic) + + # vcat / hcat / hvcat / hvncat / cat all use this + unwrap_val(dims) = dims + unwrap_val(::Val{dims}) where dims = dims + function _cat_t(dims, ::Type{T}, X::Vararg{AbstractArray,N}) where {T,N} + @inline + Ndims = maximum(map(ndims, X)) + catdims = ntuple(in(unwrap_val(dims)), Val(Ndims)) + shape = cat_size_shape(catdims, X...) + A = cat_similar(X[1], T, shape) + if count(!iszero, catdims)::Int > 1 + fill!(A, zero(T)) + end + return __cat(A, shape, catdims, X...) + end end @eval Base.Sys begin __init_build() = nothing # VersionNumber parsing is not supported yet diff --git a/test/trimming/trimmability.jl b/test/trimming/trimmability.jl index 0fd9eedc6999d..6e0b4d4c0683d 100644 --- a/test/trimming/trimmability.jl +++ b/test/trimming/trimmability.jl @@ -26,6 +26,70 @@ function add_one(x::Cint)::Cint return x + 1 end +function _test_cat() + # hcat + _cat1a = hcat(randn(3), rand(3), randn(3)) + _cat1b = [randn(3) rand(3) randn(3)] + _cat1c = hcat(randn(3,3), rand(3,3), randn(3,3)) + _cat1d = [randn(3,3) rand(3,3) randn(3,3)] + _cat1e = hcat(randn(3,3,3), rand(3,3,3), randn(3,3,3)) + _cat1f = [randn(3,3,3) rand(3,3,3) randn(3,3,3)] + + # v_cat + _cat2a = vcat(randn(3), rand(3), randn(3)) + _cat2b = [randn(3); rand(3); randn(3)] + _cat2c = vcat(randn(3,3), rand(3,3), randn(3,3)) + _cat2d = [randn(3,3); rand(3,3); randn(3,3)] + _cat2e = vcat(randn(3,3,3), rand(3,3,3), randn(3,3,3)) + _cat2f = [randn(3,3,3); rand(3,3,3); randn(3,3,3)] + + # hvcat + _cat3a = hvcat((2,2), rand(3,2), randn(3,4), rand(1,2), randn(1,4)) + _cat3b = [rand(3,2) randn(3,4); rand(1,2) randn(1,4)] + _cat3c = hvcat((2, 2), rand(5,2,3), rand(5,4,3), rand(1,2,3), rand(1,4,3)) + _cat3d = [rand(5,2,3) rand(5,4,3); rand(1,2,3) rand(1,4,3)] + + # cat + _cat4a = cat(randn(3), randn(3); dims = 1) + _cat4b = cat(randn(3,3,3), randn(3,3,3); dims = 2) + _cat4c = cat(randn(3), randn(3,3); dims = 2) + _cat4d = cat(randn(3), randn(3), rand(3), rand(3), randn(3), randn(3); dims = (1,)) + _cat4e = cat(randn(3,3), randn(3,3), rand(3,3), rand(3,3), randn(3,3), randn(3,3); dims = (1,2)) + _cat4f = cat(randn(3,3,3), randn(3,3); dims=(1,3)) + + # hvncat + _cat5a = hvncat(2, randn(3), randn(3), randn(3)) + _cat5b = [randn(3) ;; randn(3) ;; randn(3)] + _cat5c = hvncat(2, randn(3,3), randn(3,3), randn(3,3)) + _cat5d = [randn(3,3) ;; randn(3,3) ;; randn(3,3)] + _cat5e = hvncat((1, 2, 2), false, randn(2,3), randn(2,3), randn(2,3), randn(2,3)) + _cat5f = [randn(2,3) ;; randn(2,3) ;;; randn(2,3) ;; randn(2,3)] + + # stack + _cat6a = stack([randn(3), randn(3), randn(3)]) + _cat6b = stack([randn(3), randn(3), randn(3)]; dims=1) + _cat6c = stack([randn(2,3), randn(2,3)]; dims=3) + _cat6d = stack(x -> x .^ 2, [randn(3), randn(3)]) + + # repeat + _cat7a = repeat(randn(3), 2) + _cat7b = repeat(randn(2,3), 2, 3) + _cat7c = repeat(randn(2,3); inner=(2,1), outer=(1,3)) + _cat7d = repeat(randn(3,3,3), 1, 2, 1) + + # aggregate to prevent deletion + _cat1 = _cat1a[1] + _cat1b[1] + _cat1c[1] + _cat1d[1] + _cat1e[1] + _cat1f[1] + _cat2 = _cat2a[1] + _cat2b[1] + _cat2c[1] + _cat2d[1] + _cat2e[1] + _cat2f[1] + _cat3 = _cat3a[1] + _cat3b[1] + _cat3c[1] + _cat3d[1] + _cat4 = _cat4a[1] + _cat4b[1] + _cat4c[1] + _cat4d[1] + _cat4e[1] + _cat4f[1] + _cat5 = _cat5a[1] + _cat5b[1] + _cat5c[1] + _cat5d[1] + _cat5e[1] + _cat5f[1] + _cat6 = _cat6a[1] + _cat6b[1] + _cat6c[1] + _cat6d[1] + _cat7 = _cat7a[1] + _cat7b[1] + _cat7c[1] + _cat7d[1] + + return _cat1 + _cat2 + _cat3 + _cat4 + _cat5 + _cat6 + _cat7 +end + + function @main(args::Vector{String})::Cint println(Core.stdout, str()) println(Core.stdout, PROGRAM_FILE) @@ -45,6 +109,8 @@ function @main(args::Vector{String})::Cint d = mapreduce(x -> x^2, +, sorted_arr) # e = reduce(xor, rand(Int, 10)) + println(Core.stdout, _test_cat()) + for i = 1:10 # https://github.com/JuliaLang/julia/issues/60846 add_one(Cint(i)) diff --git a/test/trimming/trimming.jl b/test/trimming/trimming.jl index 7e15a4d196a0c..176f51106c858 100644 --- a/test/trimming/trimming.jl +++ b/test/trimming/trimming.jl @@ -15,7 +15,13 @@ let exe_suffix = splitext(Base.julia_exename())[2] @test filesize(hello_exe) < 1_900_000 trimmability_exe = joinpath(bindir, "trimmability" * exe_suffix) - @test readchomp(`$trimmability_exe arg1 arg2`) == "Hello, world!\n$trimmability_exe\narg1\narg2\n$(4.0+pi)" + lines = split(readchomp(`$trimmability_exe arg1 arg2`), "\n") + @test lines[1] == "Hello, world!" + @test lines[2] == trimmability_exe + @test lines[3] == "arg1" + @test lines[4] == "arg2" + @test lines[5] == string(4.0+pi) + @test parse(Float64, lines[6]) isa Float64 basic_jll_exe = joinpath(bindir, "basic_jll" * exe_suffix) lines = split(readchomp(`$basic_jll_exe`), "\n") From d108372d4362489805a84c0408707d5aa79d09ce Mon Sep 17 00:00:00 2001 From: Cody Tapscott <84105208+topolarity@users.noreply.github.com> Date: Sat, 11 Apr 2026 12:01:04 -0500 Subject: [PATCH 21/38] Fix `--trim` compatibility for `BoundsError` (#61561) This regressed in https://github.com/JuliaLang/julia/pull/61535 but wasn't being tested until https://github.com/JuliaLang/julia/pull/61551, which caused CI to fail only after merging both (sorry folks!) JuliaC.jl is already fixed up by https://github.com/JuliaLang/JuliaC.jl/pull/131. This PR resolves the Julia-side scripts. Also check in the `Manifest.toml` for JuliaC to prevent breaking CI in the future, although it wouldn't have prevented this regression. Supersedes https://github.com/JuliaLang/julia/pull/61558. (cherry picked from commit b9ba052bc0035a676fa946c85e81ba5c5570c5a3) --- contrib/juliac/juliac-trim-base.jl | 2 + deps/jlutilities/juliac/Manifest.toml | 391 ++++++++++++++++++++++++++ deps/jlutilities/juliac/Project.toml | 5 + 3 files changed, 398 insertions(+) create mode 100644 deps/jlutilities/juliac/Manifest.toml create mode 100644 deps/jlutilities/juliac/Project.toml diff --git a/contrib/juliac/juliac-trim-base.jl b/contrib/juliac/juliac-trim-base.jl index 020aecd06faa9..c7b5e5156611b 100644 --- a/contrib/juliac/juliac-trim-base.jl +++ b/contrib/juliac/juliac-trim-base.jl @@ -79,7 +79,9 @@ end end # these functions are not `--trim`-compatible if it resolves to a Varargs{...} specialization # and since it only has 1-argument methods this happens too often by default (just 2-3 args) + setfield!(typeof(throw_boundserror).name, :max_args, Int32(5), :monotonic) setfield!(typeof(throw_eachindex_mismatch_indices).name, :max_args, Int32(5), :monotonic) + setfield!(typeof(_throw_boundserror_indices).name, :max_args, Int32(5), :monotonic) setfield!(typeof(print).name, :max_args, Int32(10), :monotonic) setfield!(typeof(println).name, :max_args, Int32(10), :monotonic) setfield!(typeof(print_to_string).name, :max_args, Int32(10), :monotonic) diff --git a/deps/jlutilities/juliac/Manifest.toml b/deps/jlutilities/juliac/Manifest.toml new file mode 100644 index 0000000000000..466eecab79ed1 --- /dev/null +++ b/deps/jlutilities/juliac/Manifest.toml @@ -0,0 +1,391 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.14.0-DEV" +manifest_format = "2.1" +project_hash = "d3f4758b27fc8d19cd1b7603061a3b69d4285c69" + +[[deps.ArgTools]] +uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" +version = "1.1.2" + + [deps.ArgTools.syntax] + julia_version = "1.3.0" + +[[deps.Artifacts]] +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +version = "1.11.0" + + [deps.Artifacts.syntax] + julia_version = "1.14.0-DEV.2012" + +[[deps.Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +version = "1.11.0" + + [deps.Base64.syntax] + julia_version = "1.14.0-DEV.2012" + +[[deps.CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "1.3.0+1" + + [deps.CompilerSupportLibraries_jll.syntax] + julia_version = "1.6.0" + +[[deps.Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" +version = "1.11.0" + + [deps.Dates.syntax] + julia_version = "1.14.0-DEV.2012" + +[[deps.Downloads]] +deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] +uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +version = "1.7.0" + + [deps.Downloads.syntax] + julia_version = "1.10.0" + +[[deps.FileWatching]] +uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" +version = "1.11.0" + + [deps.FileWatching.syntax] + julia_version = "1.14.0-DEV.2012" + +[[deps.Glob]] +git-tree-sha1 = "83cb0092e2792b9e3a865b6655e88f5b862607e2" +registries = "General" +uuid = "c27321d9-0574-5035-807b-f59d2c89b15c" +version = "1.4.0" + + [deps.Glob.syntax] + julia_version = "1.0.0" + +[[deps.JLLWrappers]] +deps = ["Artifacts", "Preferences"] +git-tree-sha1 = "0533e564aae234aff59ab625543145446d8b6ec2" +registries = "General" +uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" +version = "1.7.1" + + [deps.JLLWrappers.syntax] + julia_version = "1.0.0" + +[[deps.JuliaC]] +deps = ["LazyArtifacts", "Libdl", "Mmap", "ObjectFile", "PackageCompiler", "Patchelf_jll", "Pkg", "RelocatableFolders", "StructIO"] +git-tree-sha1 = "d2d87d6d9f7a12c695bc23db869cdd9d4bcbccde" +repo-rev = "main" +repo-url = "https://github.com/JuliaLang/JuliaC.jl.git" +uuid = "acedd4c2-ced6-4a15-accc-2607eb759ba2" +version = "0.3.1" + + [deps.JuliaC.syntax] + julia_version = "1.10.0" + +[[deps.JuliaSyntaxHighlighting]] +deps = ["StyledStrings"] +uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" +version = "1.13.0" + + [deps.JuliaSyntaxHighlighting.syntax] + julia_version = "1.12.0" + +[[deps.LazyArtifacts]] +deps = ["Artifacts", "Pkg"] +uuid = "4af54fe1-eca0-43a8-85a7-787d91b784e3" +version = "1.11.0" + + [deps.LazyArtifacts.syntax] + julia_version = "1.11.0" + +[[deps.LibCURL]] +deps = ["LibCURL_jll", "MozillaCACerts_jll"] +uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" +version = "1.0.0" + + [deps.LibCURL.syntax] + julia_version = "1.3.0" + +[[deps.LibCURL_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "Zstd_jll", "nghttp2_jll"] +uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" +version = "8.19.0+0" + + [deps.LibCURL_jll.syntax] + julia_version = "1.11.0" + +[[deps.LibGit2]] +deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" +version = "1.11.0" + + [deps.LibGit2.syntax] + julia_version = "1.14.0-DEV.2012" + +[[deps.LibGit2_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "PCRE2_jll", "Zlib_jll"] +uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" +version = "1.9.2+0" + + [deps.LibGit2_jll.syntax] + julia_version = "1.9.0" + +[[deps.LibSSH2_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl", "OpenSSL_jll", "Zlib_jll"] +uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" +version = "1.11.3+1" + + [deps.LibSSH2_jll.syntax] + julia_version = "1.8.0" + +[[deps.Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +version = "1.11.0" + + [deps.Libdl.syntax] + julia_version = "1.14.0-DEV.2012" + +[[deps.Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" +version = "1.11.0" + + [deps.Logging.syntax] + julia_version = "1.14.0-DEV.2012" + +[[deps.Markdown]] +deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" +version = "1.11.0" + + [deps.Markdown.syntax] + julia_version = "1.14.0-DEV.2012" + +[[deps.Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" +version = "1.11.0" + + [deps.Mmap.syntax] + julia_version = "1.14.0-DEV.2012" + +[[deps.MozillaCACerts_jll]] +uuid = "14a3606d-f60d-562e-9121-12d972cd8159" +version = "2026.3.19" + + [deps.MozillaCACerts_jll.syntax] + julia_version = "1.14.0-DEV.2012" + +[[deps.NetworkOptions]] +uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" +version = "1.3.0" + + [deps.NetworkOptions.syntax] + julia_version = "1.12.0" + +[[deps.ObjectFile]] +deps = ["Reexport", "StructIO"] +git-tree-sha1 = "22faba70c22d2f03e60fbc61da99c4ebfc3eb9ba" +registries = "General" +uuid = "d8793406-e978-5875-9003-1fc021f44a92" +version = "0.5.0" + + [deps.ObjectFile.syntax] + julia_version = "1.6.0" + +[[deps.OpenSSL_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" +version = "3.5.6+0" + + [deps.OpenSSL_jll.syntax] + julia_version = "1.6.0" + +[[deps.PCRE2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "efcefdf7-47ab-520b-bdef-62a2eaa19f15" +version = "10.47.0+0" + + [deps.PCRE2_jll.syntax] + julia_version = "1.6.0" + +[[deps.PackageCompiler]] +deps = ["Artifacts", "Glob", "LazyArtifacts", "Libdl", "Pkg", "Printf", "RelocatableFolders", "TOML", "UUIDs", "p7zip_jll"] +git-tree-sha1 = "7b2dbae3d0eda41dad445b5f36f40588c208a942" +registries = "General" +uuid = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" +version = "2.2.5" + + [deps.PackageCompiler.syntax] + julia_version = "1.10.0" + +[[deps.Patchelf_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] +git-tree-sha1 = "ad1cab3e8273c912f722f40779e20f3e29d58f1f" +registries = "General" +uuid = "f2cf89d6-2bfd-5c44-bd2c-068eea195c0c" +version = "0.18.0+0" + + [deps.Patchelf_jll.syntax] + julia_version = "1.6.0" + +[[deps.Pkg]] +deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "Zstd_jll", "p7zip_jll"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +version = "1.14.0" + + [deps.Pkg.extensions] + REPLExt = "REPL" + + [deps.Pkg.syntax] + julia_version = "1.12.0" + + [deps.Pkg.weakdeps] + REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[deps.Preferences]] +deps = ["TOML"] +git-tree-sha1 = "8b770b60760d4451834fe79dd483e318eee709c4" +registries = "General" +uuid = "21216c6a-2e73-6563-6e65-726566657250" +version = "1.5.2" + + [deps.Preferences.syntax] + julia_version = "1.0.0" + +[[deps.Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" +version = "1.11.0" + + [deps.Printf.syntax] + julia_version = "1.14.0-DEV.2012" + +[[deps.Random]] +deps = ["SHA"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +version = "1.11.0" + + [deps.Random.syntax] + julia_version = "1.14.0-DEV.2012" + +[[deps.Reexport]] +git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b" +registries = "General" +uuid = "189a3867-3050-52da-a836-e630ba90ab69" +version = "1.2.2" + + [deps.Reexport.syntax] + julia_version = "1.0.0" + +[[deps.RelocatableFolders]] +deps = ["SHA", "Scratch"] +git-tree-sha1 = "ffdaf70d81cf6ff22c2b6e733c900c3321cab864" +registries = "General" +uuid = "05181044-ff0b-4ac5-8273-598c1e38db00" +version = "1.0.1" + + [deps.RelocatableFolders.syntax] + julia_version = "1.0.0" + +[[deps.SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" +version = "1.0.0" + + [deps.SHA.syntax] + julia_version = "1.0.0" + +[[deps.Scratch]] +deps = ["Dates"] +git-tree-sha1 = "9b81b8393e50b7d4e6d0a9f14e192294d3b7c109" +registries = "General" +uuid = "6c6a2e73-6563-6170-7368-637461726353" +version = "1.3.0" + + [deps.Scratch.syntax] + julia_version = "1.9.0" + +[[deps.StructIO]] +git-tree-sha1 = "c581be48ae1cbf83e899b14c07a807e1787512cc" +registries = "General" +uuid = "53d494c1-5632-5724-8f4c-31dff12d585f" +version = "0.3.1" + + [deps.StructIO.syntax] + julia_version = "1.0.0" + +[[deps.StyledStrings]] +uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" +version = "1.13.0" + + [deps.StyledStrings.syntax] + julia_version = "1.11.0" + +[[deps.TOML]] +deps = ["Dates"] +uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +version = "1.0.3" + + [deps.TOML.syntax] + julia_version = "1.6.0" + +[[deps.Tar]] +deps = ["ArgTools", "SHA"] +uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" +version = "1.10.0" + + [deps.Tar.syntax] + julia_version = "1.3.0" + +[[deps.UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +version = "1.11.0" + + [deps.UUIDs.syntax] + julia_version = "1.14.0-DEV.2012" + +[[deps.Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +version = "1.11.0" + + [deps.Unicode.syntax] + julia_version = "1.14.0-DEV.2012" + +[[deps.Zlib_jll]] +deps = ["Libdl"] +uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +version = "1.3.2+0" + + [deps.Zlib_jll.syntax] + julia_version = "1.6.0" + +[[deps.Zstd_jll]] +deps = ["CompilerSupportLibraries_jll", "Libdl"] +uuid = "3161d3a3-bdf6-5164-811a-617609db77b4" +version = "1.5.7+1" + + [deps.Zstd_jll.syntax] + julia_version = "1.6.0" + +[[deps.nghttp2_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" +version = "1.68.1+0" + + [deps.nghttp2_jll.syntax] + julia_version = "1.11.0" + +[[deps.p7zip_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" +version = "17.8.0+0" + + [deps.p7zip_jll.syntax] + julia_version = "1.6.0" + +[registries.General] +url = "https://github.com/JuliaRegistries/General.git" +uuid = "23338594-aafe-5451-b93e-139f81909106" diff --git a/deps/jlutilities/juliac/Project.toml b/deps/jlutilities/juliac/Project.toml new file mode 100644 index 0000000000000..3b1e7f84d6215 --- /dev/null +++ b/deps/jlutilities/juliac/Project.toml @@ -0,0 +1,5 @@ +[deps] +JuliaC = "acedd4c2-ced6-4a15-accc-2607eb759ba2" + +[sources] +JuliaC = {rev = "main", url = "https://github.com/JuliaLang/JuliaC.jl.git"} From 28817013ee539d9989db4c982b8833ee7abb1a9a Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Mon, 30 Mar 2026 19:43:26 -0400 Subject: [PATCH 22/38] subtype: Rename `right` to existential (#61424) Currently the subtyping algorithm uses `right` to mean `existential`, rather than having structurally come from the right. This doesn't matter as much, because right now, if the LHS and RHS and egal typevars, we rename them. However, I'm working on a unionall representation change that will make it important to distinguish which side of the expression a var came from. As such, rename `right` to `existential`, freeing up `right` to refer to having structurally come from the RHS. (cherry picked from commit 85c05807fc987f4a4b8865dbd5c563d21b346b56) --- src/subtype.c | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/subtype.c b/src/subtype.c index 031c9a95284a4..2442986e77c74 100644 --- a/src/subtype.c +++ b/src/subtype.c @@ -68,7 +68,7 @@ typedef struct jl_varbinding_t { jl_tvar_t *var; // store NULL to "delete" this from env (temporarily) jl_value_t *JL_NONNULL lb; jl_value_t *JL_NONNULL ub; - int8_t right; // whether this variable came from the right side of `A <: B` + int8_t existential; // whether this variable should be treated as existential int8_t occurs_inv; // occurs in invariant position int8_t occurs_cov; // # of occurrences in covariant position int8_t concrete; // 1 if another variable has a constraint forcing this one to be concrete @@ -797,7 +797,7 @@ static int var_lt(jl_tvar_t *b, jl_value_t *a, jl_stenv_t *e, int param) if (e->Loffset != 0 && !jl_is_typevar(a) && a != jl_bottom_type && a != (jl_value_t *)jl_any_type) return 0; - if (!bb->right) // check ∀b . b<:a + if (!bb->existential) // check ∀b . b<:a return subtype_left_var(bb->ub, a, e, param); if (bb->ub == a) return 1; @@ -818,7 +818,7 @@ static int var_lt(jl_tvar_t *b, jl_value_t *a, jl_stenv_t *e, int param) assert(bb->ub != (jl_value_t*)b); if (jl_is_typevar(a)) { jl_varbinding_t *aa = lookup(e, (jl_tvar_t*)a); - if (aa && !aa->right && in_union(bb->lb, a) && bb->depth0 != aa->depth0 && var_outside(e, b, (jl_tvar_t*)a)) { + if (aa && !aa->existential && in_union(bb->lb, a) && bb->depth0 != aa->depth0 && var_outside(e, b, (jl_tvar_t*)a)) { // an "exists" var cannot equal a "forall" var inside it unless the forall // var has equal bounds. return subtype_left_var(aa->ub, aa->lb, e, param); @@ -838,7 +838,7 @@ static int var_gt(jl_tvar_t *b, jl_value_t *a, jl_stenv_t *e, int param) if (e->Loffset != 0 && !jl_is_typevar(a) && a != jl_bottom_type && a != (jl_value_t *)jl_any_type) return 0; - if (!bb->right) // check ∀b . b>:a + if (!bb->existential) // check ∀b . b>:a return subtype_left_var(a, bb->lb, e, param); if (bb->lb == a) return 1; @@ -853,7 +853,7 @@ static int var_gt(jl_tvar_t *b, jl_value_t *a, jl_stenv_t *e, int param) assert(bb->lb != (jl_value_t*)b); if (jl_is_typevar(a)) { jl_varbinding_t *aa = lookup(e, (jl_tvar_t*)a); - if (aa && !aa->right && bb->depth0 != aa->depth0 && param == 2 && var_outside(e, b, (jl_tvar_t*)a)) + if (aa && !aa->existential && bb->depth0 != aa->depth0 && param == 2 && var_outside(e, b, (jl_tvar_t*)a)) return subtype_left_var(aa->ub, aa->lb, e, param); } return 1; @@ -1372,7 +1372,7 @@ static int subtype_tuple(jl_datatype_t *xd, jl_datatype_t *yd, jl_stenv_t *e, in } if (vvx != JL_VARARG_NONE && vvx != JL_VARARG_INT && (!xbb || !jl_is_long(xbb->lb))) { - if (vvx == JL_VARARG_UNBOUND || (xbb && !xbb->right)) { + if (vvx == JL_VARARG_UNBOUND || (xbb && !xbb->existential)) { // Unbounded on the LHS, bounded on the RHS if (vvy == JL_VARARG_NONE || vvy == JL_VARARG_INT) return 0; @@ -1412,7 +1412,7 @@ static int subtype_tuple(jl_datatype_t *xd, jl_datatype_t *yd, jl_stenv_t *e, in } static int try_subtype_by_bounds(jl_value_t *a, jl_value_t *b, jl_stenv_t *e); -static int has_exists_typevar(jl_value_t *x, jl_stenv_t *e) JL_NOTSAFEPOINT; +static int has_existential_typevar(jl_value_t *x, jl_stenv_t *e) JL_NOTSAFEPOINT; // `param` means we are currently looking at a parameter of a type constructor // (as opposed to being outside any type constructor, or comparing variable bounds). @@ -1443,7 +1443,7 @@ static int subtype(jl_value_t *x, jl_value_t *y, jl_stenv_t *e, int param) // Note: `x <: ∃y` performs a local ∀-∃ check between `x` and `yb->ub`. // We need to ensure that there's no ∃ typevar as otherwise that check // might cause false alarm due to the accumulated env change. - if (yb == NULL || yb->right == 0 || !has_exists_typevar(yb->ub, e)) + if (yb == NULL || !yb->existential || !has_existential_typevar(yb->ub, e)) return subtype_var(yvar, x, e, 1, param); } x = pick_union_element(x, e, 0); @@ -1461,7 +1461,7 @@ static int subtype(jl_value_t *x, jl_value_t *y, jl_stenv_t *e, int param) // variables lurking inside. // note: for forall var, there's no need to split y if it has no free typevars. jl_varbinding_t *xx = lookup(e, (jl_tvar_t *)x); - ui = ((xx && xx->right) || jl_has_free_typevars(y)) && pick_union_decision(e, 1); + ui = ((xx && xx->existential) || jl_has_free_typevars(y)) && pick_union_decision(e, 1); } if (ui == 1) y = pick_union_element(y, e, 1); @@ -1482,8 +1482,8 @@ static int subtype(jl_value_t *x, jl_value_t *y, jl_stenv_t *e, int param) if (yub == ylb && jl_is_typevar(yub)) return subtype(x, yub, e, param); } - int xr = xx && xx->right; // treat free variables as "forall" (left) - int yr = yy && yy->right; + int xr = xx && xx->existential; // treat free variables as "forall" (left) + int yr = yy && yy->existential; if (xr) { if (yy) record_var_occurrence(yy, e, param); if (yr) { @@ -1505,7 +1505,7 @@ static int subtype(jl_value_t *x, jl_value_t *y, jl_stenv_t *e, int param) if (jl_is_unionall(y)) { jl_varbinding_t *xb = lookup(e, (jl_tvar_t*)x); jl_value_t *xub = xb == NULL ? ((jl_tvar_t *)x)->ub : xb->ub; - if ((xb == NULL ? !e->ignore_free : !xb->right) && xub != y) { + if ((xb == NULL ? !e->ignore_free : !xb->existential) && xub != y) { // We'd better unwrap `y::UnionAll` eagerly if `x` isa ∀-var. // This makes sure the following cases work correct: // 1) `∀T <: Union{∃S, SomeType{P}} where {P}`: `S == Any` ==> `S >: T` @@ -1624,20 +1624,20 @@ static int is_definite_length_tuple_type(jl_value_t *x) return k == JL_VARARG_NONE || k == JL_VARARG_INT; } -static int is_exists_typevar(jl_value_t *x, jl_stenv_t *e) +static int is_existential_typevar(jl_value_t *x, jl_stenv_t *e) { if (!jl_is_typevar(x)) return 0; jl_varbinding_t *vb = lookup(e, (jl_tvar_t *)x); - return vb && vb->right; + return vb && vb->existential; } -static int has_exists_typevar(jl_value_t *x, jl_stenv_t *e) JL_NOTSAFEPOINT +static int has_existential_typevar(jl_value_t *x, jl_stenv_t *e) JL_NOTSAFEPOINT { jl_typeenv_t *env = NULL; jl_varbinding_t *v = e->vars; while (v != NULL) { - if (v->right) { + if (v->existential) { jl_typeenv_t *newenv = (jl_typeenv_t*)alloca(sizeof(jl_typeenv_t)); newenv->var = v->var; newenv->val = NULL; @@ -1662,8 +1662,8 @@ static int local_forall_exists_subtype(jl_value_t *x, jl_value_t *y, jl_stenv_t int kindy = !jl_has_free_typevars(y); if (kindx && kindy) return jl_subtype(x, y); - int has_exists = (!kindx && has_exists_typevar(x, e)) || - (!kindy && has_exists_typevar(y, e)); + int has_exists = (!kindx && has_existential_typevar(x, e)) || + (!kindy && has_existential_typevar(y, e)); if (!has_exists) { // We can use ∀_∃_subtype safely for ∃ free inputs. // This helps to save some bits in union stack. @@ -1675,7 +1675,7 @@ static int local_forall_exists_subtype(jl_value_t *x, jl_value_t *y, jl_stenv_t pop_unionstate(&e->Runions, &oldRunions); return sub; } - if (is_exists_typevar(x, e) != is_exists_typevar(y, e)) { + if (is_existential_typevar(x, e) != is_existential_typevar(y, e)) { e->Lunions.used = 0; while (1) { e->Lunions.more = 0; @@ -1746,7 +1746,7 @@ static int equal_var(jl_tvar_t *v, jl_value_t *x, jl_stenv_t *e) return e->ignore_free || ( local_forall_exists_subtype(x, v->lb, e, 2, !jl_has_free_typevars(x)) && local_forall_exists_subtype(v->ub, x, e, 0, 0)); - if (!vb->right) + if (!vb->existential) return local_forall_exists_subtype(x, vb->lb, e, 2, !jl_has_free_typevars(x)) && local_forall_exists_subtype(vb->ub, x, e, 0, 0); if (vb->lb == x) @@ -2753,14 +2753,14 @@ static int subtype_in_env_existential(jl_value_t *x, jl_value_t *y, jl_stenv_t * jl_varbinding_t *v = e->vars; int n = 0; while (v != NULL) { - rs[n++] = v->right; - v->right = 1; + rs[n++] = v->existential; + v->existential = 1; v = v->prev; } int issub = subtype_in_env(x, y, e); n = 0; v = e->vars; while (v != NULL) { - v->right = rs[n++]; + v->existential = rs[n++]; v = v->prev; } return issub; @@ -3382,7 +3382,7 @@ static jl_value_t *finish_unionall(jl_value_t *res JL_MAYBE_UNROOTED, jl_varbind } } - if (vb->right && e->envidx < e->envsz) { + if (vb->existential && e->envidx < e->envsz) { jl_value_t *oldval = e->envout[e->envidx]; if (!varval || (!is_leaf_bound(varval) && !vb->occurs_inv)) e->envout[e->envidx] = (jl_value_t*)vb->var; @@ -3803,7 +3803,7 @@ static void flip_vars(jl_stenv_t *e) { jl_varbinding_t *btemp = e->vars; while (btemp != NULL) { - btemp->right = !btemp->right; + btemp->existential = !btemp->existential; btemp = btemp->prev; } } From c60a268f20ef2b4d4c9ad849ee88c40ec1696f3b Mon Sep 17 00:00:00 2001 From: Jameson Nash Date: Thu, 30 Oct 2025 22:40:56 -0700 Subject: [PATCH 23/38] precompile: fix deps list for extensions relying on stdlibs Some packages add extensions to stdlibs, so we should not have prohibited precompiling those. This broke Compat, which adds an extension to LinearAlgebra. (cherry picked from commit 12adf71b98fe90cab7603599b10f9d0168d70e23) --- base/precompilation.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/base/precompilation.jl b/base/precompilation.jl index 10463b1d138fc..fe72c4878d2e7 100644 --- a/base/precompilation.jl +++ b/base/precompilation.jl @@ -602,9 +602,9 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, triggers[ext] = Base.PkgId[pkg] # depends on parent package all_triggers_available = true for trigger_uuid in trigger_uuids - trigger_name = env.names[trigger_uuid] - if trigger_uuid in keys(env.deps) - push!(triggers[ext], Base.PkgId(trigger_uuid, trigger_name)) + trigger_name = Base.PkgId(trigger_uuid, env.names[trigger_uuid]) + if trigger_uuid in keys(env.deps) || Base.in_sysimage(trigger_name) + push!(triggers[ext], trigger_name) else all_triggers_available = false break @@ -634,6 +634,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, for ext_a in keys(ext_to_parent) for ext_b in keys(ext_to_parent) if triggers[ext_a] ⊋ triggers[ext_b] + push!(triggers[ext_a], ext_b) push!(direct_deps[ext_a], ext_b) end end @@ -1038,9 +1039,8 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, if interrupted_or_done[] return end - # for extensions, any extension in our direct dependencies is one we have a right to load - # for packages, we may load any extension (all possible triggers are accounted for above) - loadable_exts = haskey(ext_to_parent, pkg) ? filter((dep)->haskey(ext_to_parent, dep), direct_deps[pkg]) : nothing + # for extensions, any extension that can trigger it needs to be accounted for here (even stdlibs, which are excluded from direct_deps) + loadable_exts = haskey(ext_to_parent, pkg) ? filter((dep)->haskey(ext_to_parent, dep), triggers[pkg]) : nothing if _from_loading && pkg in requested_pkgids # loading already took the cachefile_lock and printed logmsg for its explicit requests t = @elapsed ret = begin From dfd93dafe49f088bf02f5df134cb1b454629dbac Mon Sep 17 00:00:00 2001 From: Jameson Nash Date: Thu, 30 Oct 2025 22:29:10 -0700 Subject: [PATCH 24/38] Reapply "precompile: fail in (closer to) linear time and linear error messages" This reverts commit 81dbb722e2610dd8a421fbf50fc2368249195323. (cherry picked from commit cd0d942f1144807d46c132f6fd33ce62f0c03d71) --- base/precompilation.jl | 257 +++++++++++++++++++---------- doc/src/devdocs/precompile_hang.md | 2 +- src/jl_uv.c | 2 +- test/loading.jl | 4 +- test/precompile.jl | 176 ++++++++++++++++++-- 5 files changed, 336 insertions(+), 105 deletions(-) diff --git a/base/precompilation.jl b/base/precompilation.jl index fe72c4878d2e7..810354a49afbe 100644 --- a/base/precompilation.jl +++ b/base/precompilation.jl @@ -406,7 +406,7 @@ function excluded_circular_deps_explanation(io::IOContext{IO}, ext_to_parent::Di else line = " └" * "─" ^j * " " end - hascolor = get(io, :color, false)::Bool + hascolor = get(io, :color, false)::Bool # XXX: this output does not go to `io` so this is bad to call here line = _color_string(line, :light_black, hascolor) * full_name(ext_to_parent, pkg) * "\n" cycle_str *= line end @@ -471,6 +471,76 @@ function collect_all_deps(direct_deps, dep, alldeps=Set{Base.PkgId}()) end +""" + precompilepkgs(pkgs; kwargs...) + +Precompile packages and their dependencies, with support for parallel compilation, +progress tracking, and various compilation configurations. + +`pkgs::Union{Vector{String}, Vector{PkgId}}`: Packages to precompile. When +empty (default), precompiles all project dependencies. When specified, +precompiles only the given packages and their dependencies (unless +`manifest=true`). + +!!! note + Errors will only throw when precompiling the top-level dependencies, given that + not all manifest dependencies may be loaded by the top-level dependencies on the given system. + This can be overridden to make errors in all dependencies throw by setting the kwarg `strict` to `true` + +# Keyword Arguments +- `internal_call::Bool`: Indicates this is an automatic precompilation call + from somewhere external (e.g. Pkg). Do not use this parameter. + +- `strict::Bool`: Controls error reporting scope. When `false` (default), only reports + errors for direct project dependencies. Only relevant when `manifest=true`. + +- `warn_loaded::Bool`: When `true` (default), checks for and warns about packages that are + precompiled but already loaded with a different version. Displays a warning that Julia + needs to be restarted to use the newly precompiled versions. + +- `timing::Bool`: When `true` (not default), displays timing information for + each package compilation, but only if compilation might have succeeded. + Disables fancy progress bar output (timing is shown in simple text mode). + +- `_from_loading::Bool`: Internal flag indicating the call originated from the + package loading system. When `true` (not default): returns early instead of + throwing when packages are not found; suppresses progress messages when not + in an interactive session; allows packages outside the current environment to + be added as serial precompilation jobs; skips LOADING_CACHE initialization; + and changes cachefile locking behavior. + +- `configs::Union{Config,Vector{Config}}`: Compilation configurations to use. Each Config + is a `Pair{Cmd, Base.CacheFlags}` specifying command flags and cache flags. When + multiple configs are provided, each package is precompiled for each configuration. + +- `io::IO`: Output stream for progress messages, warnings, and errors. Can be + redirected (e.g., to `devnull` when called from loading in non-interactive mode). + +- `fancyprint::Bool`: Controls output format. When `true`, displays an animated progress + bar with spinners. When `false`, instead enables `timing` mode. Automatically + disabled when `timing=true` or when called from loading in non-interactive mode. + +- `manifest::Bool`: Controls the scope of packages to precompile. When `false` (default), + precompiles only packages specified in `pkgs` and their dependencies. When `true`, + precompiles all packages in the manifest (workspace mode), typically used by Pkg for + workspace precompile requests. + +- `ignore_loaded::Bool`: Controls whether already-loaded packages affect cache + freshness checks. When `false` (not default), loaded package versions are considered when + determining if cache files are fresh. + +# Return +- `Vector{String}`: Paths to cache files for the requested packages. +- `Nothing`: precompilation should be skipped + +# Notes +- Packages in circular dependency cycles are skipped with a warning. +- Packages with `__precompile__(false)` are skipped if they are from loading to + avoid repeated work on every session. +- Parallel compilation is controlled by `JULIA_NUM_PRECOMPILE_TASKS` environment variable + (defaults to CPU_THREADS + 1, capped at 16, halved on Windows). +- Extensions are precompiled when all their triggers are available in the environment. +""" function precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}=String[]; internal_call::Bool=false, strict::Bool = false, @@ -512,7 +582,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, timing::Bool, _from_loading::Bool, configs::Vector{Config}, - _io::IOContext{IO}, + io::IOContext{IO}, fancyprint::Bool, manifest::Bool, ignore_loaded::Bool) @@ -550,16 +620,20 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, # suppress precompilation progress messages when precompiling for loading packages, except during interactive sessions # or when specified by logging heuristics that explicitly require it # since the complicated IO implemented here can have somewhat disastrous consequences when happening in the background (e.g. #59599) - io = _io + logio = io logcalls = nothing - if _from_loading && !isinteractive() - io = IOContext{IO}(devnull) - fancyprint = false - logcalls = isinteractive() ? CoreLogging.Info : CoreLogging.Debug # sync with Base.compilecache + if _from_loading + if isinteractive() + logcalls = CoreLogging.Info # sync with Base.compilecache + else + logio = IOContext{IO}(devnull) + fancyprint = false + logcalls = CoreLogging.Debug # sync with Base.compilecache + end end nconfigs = length(configs) - hascolor = get(io, :color, false)::Bool + hascolor = get(logio, :color, false)::Bool color_string(cstr::String, col::Union{Int64, Symbol}) = _color_string(cstr, col, hascolor) stale_cache = Dict{StaleCacheKey, Bool}() @@ -748,8 +822,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, pkg_names = [pkg.name for pkg in project_deps] end keep = Set{Base.PkgId}() - for dep in direct_deps - dep_pkgid = first(dep) + for dep_pkgid in keys(direct_deps) if dep_pkgid.name in pkg_names push!(keep, dep_pkgid) collect_all_deps(direct_deps, dep_pkgid, keep) @@ -838,9 +911,10 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, pkg_liveprinted = Ref{Union{Nothing, PkgId}}(nothing) function monitor_std(pkg_config, pipe; single_requested_pkg=false) - pkg, config = pkg_config + local pkg, config = pkg_config try - liveprinting = false + local liveprinting = false + local thistaskwaiting = false while !eof(pipe) local str = readline(pipe, keep=true) if single_requested_pkg && (liveprinting || !isempty(str)) @@ -853,15 +927,18 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, end end write(get!(IOBuffer, std_outputs, pkg_config), str) - if !in(pkg_config, taskwaiting) && occursin("waiting for IO to finish", str) - !fancyprint && @lock print_lock begin - println(io, pkg.name, color_string(" Waiting for background task / IO / timer.", Base.warn_color())) + if thistaskwaiting + if occursin("Waiting for background task / IO / timer", str) + thistaskwaiting = true + !liveprinting && !fancyprint && @lock print_lock begin + println(io, pkg.name, color_string(str, Base.warn_color())) + end + push!(taskwaiting, pkg_config) end - push!(taskwaiting, pkg_config) - end - if !fancyprint && in(pkg_config, taskwaiting) - @lock print_lock begin - print(io, str) + else + # XXX: don't just re-enable IO for random packages without printing the context for them first + !liveprinting && !fancyprint && @lock print_lock begin + print(io, ansi_cleartoendofline, str) end end end @@ -877,10 +954,10 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, (isempty(pkg_queue) || interrupted_or_done[]) && return @lock print_lock begin if target[] !== nothing - printpkgstyle(io, :Precompiling, target[]) + printpkgstyle(logio, :Precompiling, target[]) end if fancyprint - print(io, ansi_disablecursor) + print(logio, ansi_disablecursor) end end t = Timer(0; interval=1/10) @@ -894,7 +971,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, n_print_rows = 0 while !printloop_should_exit[] @lock print_lock begin - term_size = displaysize(io)::Tuple{Int, Int} + term_size = displaysize(logio)::Tuple{Int, Int} num_deps_show = max(term_size[1] - 3, 2) # show at least 2 deps pkg_queue_show = if !interrupted_or_done[] && length(pkg_queue) > num_deps_show last(pkg_queue, num_deps_show) @@ -914,7 +991,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, # window between print cycles termwidth = (displaysize(io)::Tuple{Int,Int})[2] - 4 if !final_loop - s = sprint(io -> show_progress(io, bar; termwidth, carriagereturn=false); context=io) + s = sprint(io -> show_progress(io, bar; termwidth, carriagereturn=false); context=logio) print(iostr, Base._truncate_at_width_or_chars(true, s, termwidth), "\n") end for pkg_config in pkg_queue_show @@ -955,11 +1032,11 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, end last_length = length(pkg_queue_show) n_print_rows = count("\n", str_) - print(io, str_) + print(logio, str_) printloop_should_exit[] = interrupted_or_done[] && final_loop final_loop = interrupted_or_done[] # ensures one more loop to tidy last task after finish i += 1 - printloop_should_exit[] || print(io, ansi_moveup(n_print_rows), ansi_movecol1) + printloop_should_exit[] || print(logio, ansi_moveup(n_print_rows), ansi_movecol1) end wait(t) end @@ -969,7 +1046,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, # Base.display_error(ErrorException(""), Base.catch_backtrace()) handle_interrupt(err, true) || rethrow() finally - fancyprint && print(io, ansi_enablecursor) + fancyprint && print(logio, ansi_enablecursor) end end @@ -996,8 +1073,12 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, notify(was_processed[pkg_config]) continue end - # Heuristic for when precompilation is disabled - if occursin(r"\b__precompile__\(\s*false\s*\)", read(sourcepath, String)) + # Heuristic for when precompilation is disabled, which must not over-estimate however for any dependent + # since it will also block precompilation of all dependents + if _from_loading && single_requested_pkg && occursin(r"\b__precompile__\(\s*false\s*\)", read(sourcepath, String)) + @lock print_lock begin + Base.@logmsg logcalls "Disabled precompiling $(repr("text/plain", pkg)) since the text `__precompile__(false)` was found in file." + end notify(was_processed[pkg_config]) continue end @@ -1019,8 +1100,8 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, end if !circular && is_stale Base.acquire(parallel_limiter) - is_project_dep = pkg in project_deps is_serial_dep = pkg in serial_deps + is_project_dep = pkg in project_deps # std monitoring std_pipe = Base.link_pipe!(Pipe(); reader_supports_async=true, writer_supports_async=true) @@ -1030,7 +1111,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, name = describe_pkg(pkg, is_project_dep, is_serial_dep, flags, cacheflags) @lock print_lock begin if !fancyprint && isempty(pkg_queue) - printpkgstyle(io, :Precompiling, something(target[], "packages...")) + printpkgstyle(logio, :Precompiling, something(target[], "packages...")) end end push!(pkg_queue, pkg_config) @@ -1041,6 +1122,12 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, end # for extensions, any extension that can trigger it needs to be accounted for here (even stdlibs, which are excluded from direct_deps) loadable_exts = haskey(ext_to_parent, pkg) ? filter((dep)->haskey(ext_to_parent, dep), triggers[pkg]) : nothing + if !isempty(deps) + # if deps is empty, either it doesn't have any (so compiled-modules is + # irrelevant) or we couldn't compute them (so we actually should attempt + # serial compile, as the dependencies are not in the parallel list) + flags = `$flags --compiled-modules=strict` + end if _from_loading && pkg in requested_pkgids # loading already took the cachefile_lock and printed logmsg for its explicit requests t = @elapsed ret = begin @@ -1062,8 +1149,8 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, push!(freshpaths, freshpath) return nothing # returning nothing indicates another process did the recompile end - logcalls === nothing || @lock print_lock begin - Base.@logmsg logcalls "Precompiling $(repr("text/plain", pkg))" + logcalls === CoreLogging.Debug && @lock print_lock begin + @debug "Precompiling $(repr("text/plain", pkg))" end Base.compilecache(pkg, sourcepath, std_pipe, std_pipe, !ignore_loaded; flags, cacheflags, loadable_exts) @@ -1072,11 +1159,11 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, if ret isa Exception push!(precomperr_deps, pkg_config) !fancyprint && @lock print_lock begin - println(io, _timing_string(t), color_string(" ? ", Base.warn_color()), name) + println(logio, _timing_string(t), color_string(" ? ", Base.warn_color()), name) end else !fancyprint && @lock print_lock begin - println(io, _timing_string(t), color_string(" ✓ ", loaded ? Base.warn_color() : :green), name) + println(logio, _timing_string(t), color_string(" ✓ ", loaded ? Base.warn_color() : :green), name) end if ret !== nothing was_recompiled[pkg_config] = true @@ -1092,11 +1179,9 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, close(std_pipe.in) # close pipe to end the std output monitor wait(t_monitor) if err isa ErrorException || (err isa ArgumentError && startswith(err.msg, "Invalid header in cache file")) - errmsg = String(take!(get(IOBuffer, std_outputs, pkg_config))) - delete!(std_outputs, pkg_config) # so it's not shown as warnings, given error report - failed_deps[pkg_config] = (strict || is_project_dep) ? string(sprint(showerror, err), "\n", strip(errmsg)) : "" + failed_deps[pkg_config] = sprint(showerror, err) !fancyprint && @lock print_lock begin - println(io, " "^12, color_string(" ✗ ", Base.error_color()), name) + println(logio, " "^12, color_string(" ✗ ", Base.error_color()), name) end else rethrow() @@ -1144,9 +1229,25 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, quick_exit = any(t -> !istaskdone(t) || istaskfailed(t), tasks) || interrupted[] # all should have finished (to avoid memory corruption) seconds_elapsed = round(Int, (time_ns() - time_start) / 1e9) ndeps = count(values(was_recompiled)) - if ndeps > 0 || !isempty(failed_deps) || (quick_exit && !isempty(std_outputs)) - str = sprint(context=io) do iostr - if !quick_exit + # Determine if any of failures were a requested package + requested_errs = false + for ((dep, config), err) in failed_deps + if dep in requested_pkgids + requested_errs = true + break + end + end + # if every requested package succeeded, filter away output from failed packages + # since it didn't contribute to the overall success and can be regenerated if that package is later required + if !strict && !requested_errs + for (pkg_config, err) in failed_deps + delete!(std_outputs, pkg_config) + end + empty!(failed_deps) + end + if ndeps > 0 || !isempty(failed_deps) + if !quick_exit + logstr = sprint(context=logio) do iostr if fancyprint # replace the progress bar what = isempty(requested_pkgids) ? "packages finished." : "$(join((p.name for p in requested_pkgids), ", ", " and ")) finished." printpkgstyle(iostr, :Precompiling, what) @@ -1179,10 +1280,17 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, ) end end + @lock print_lock begin + println(logio, logstr) + end + end + end + if !isempty(std_outputs) + str = sprint(context=io) do iostr # show any stderr output, even if Pkg.precompile has been interrupted (quick_exit=true), given user may be - # interrupting a hanging precompile job with stderr output. julia#48371 + # interrupting a hanging precompile job with stderr output. let std_outputs = Tuple{PkgConfig,SubString{String}}[(pkg_config, strip(String(take!(io)))) for (pkg_config,io) in std_outputs] - filter!(kv -> !isempty(last(kv)), std_outputs) + filter!(!isempty∘last, std_outputs) if !isempty(std_outputs) local plural1 = length(std_outputs) == 1 ? "y" : "ies" local plural2 = length(std_outputs) == 1 ? "" : "s" @@ -1200,49 +1308,32 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, end end end - @lock print_lock begin + isempty(str) || @lock print_lock begin println(io, str) end - if interrupted[] - # done cleanup, now ensure caller aborts too - throw(InterruptException()) - end - quick_exit && return Vector{String}[] + end + # Done cleanup and sub-process output, now ensure caller aborts too with the right error + if interrupted[] + throw(InterruptException()) + end + # Fail noisily now with failed_deps if any. + # Include all messages from compilecache since any might be relevant in the failure. + if !isempty(failed_deps) err_str = IOBuffer() - n_direct_errs = 0 - for (pkg_config, err) in failed_deps - dep, config = pkg_config - if strict || (dep in project_deps) - print(err_str, "\n", dep.name, " ") - for cfg in config[1] - print(err_str, cfg, " ") - end - print(err_str, "\n\n", err) - n_direct_errs > 0 && write(err_str, "\n") - n_direct_errs += 1 - end + for ((dep, config), err) in failed_deps + write(err_str, "\n") + print(err_str, "\n", dep.name, " ") + join(err_str, config[1], " ") + print(err_str, "\n", err) end - if position(err_str) > 0 - skip(err_str, -1) - truncate(err_str, position(err_str)) - pluralde = n_direct_errs == 1 ? "y" : "ies" - direct = strict ? "" : "direct " - err_msg = "The following $n_direct_errs $(direct)dependenc$(pluralde) failed to precompile:\n$(String(take!(err_str)))" - if internal_call # aka. auto-precompilation - if isinteractive() - plural1 = length(failed_deps) == 1 ? "y" : "ies" - println(io, " ", color_string("$(length(failed_deps))", Base.error_color()), " dependenc$(plural1) errored.") - println(io, " For a report of the errors see `julia> err`. To retry use `pkg> precompile`") - setglobal!(Base.MainInclude, :err, PkgPrecompileError(err_msg)) - else - # auto-precompilation shouldn't throw but if the user can't easily access the - # error messages, just show them - print(io, "\n", err_msg) - end - else - println(io) - throw(PkgPrecompileError(err_msg)) - end + n_errs = length(failed_deps) + pluraled = n_errs == 1 ? "" : "s" + err_msg = "The following $n_errs package$(pluraled) failed to precompile:$(String(take!(err_str)))\n" + if internal_call + # Pkg does not implement correct error handling, so this sometimes handles them instead + print(io, err_msg) + else + throw(PkgPrecompileError(err_msg)) end end return collect(String, Iterators.flatten((v for (pkgid, v) in cachepath_cache if pkgid in requested_pkgids))) diff --git a/doc/src/devdocs/precompile_hang.md b/doc/src/devdocs/precompile_hang.md index 2204651848509..279ffec5360e8 100644 --- a/doc/src/devdocs/precompile_hang.md +++ b/doc/src/devdocs/precompile_hang.md @@ -17,7 +17,7 @@ If you follow the advice and hit `Ctrl-C`, you might see 1 dependency had warnings during precompilation: ┌ Test1 [ac89d554-e2ba-40bc-bc5c-de68b658c982] -│ [pid 2745] waiting for IO to finish: +│ [pid 2745] Waiting for background task / IO / timer to finish: │ Handle type uv_handle_t->data │ timer 0x55580decd1e0->0x7f94c3a4c340 ``` diff --git a/src/jl_uv.c b/src/jl_uv.c index 766e962288db6..e41b896320693 100644 --- a/src/jl_uv.c +++ b/src/jl_uv.c @@ -68,7 +68,7 @@ static void wait_empty_func(uv_timer_t *t) uv_unref((uv_handle_t*)&signal_async); if (!uv_loop_alive(t->loop)) return; - jl_safe_printf("\n[pid %zd] waiting for IO to finish:\n" + jl_safe_printf("\n[pid %zd] Waiting for background task / IO / timer to finish:\n" " Handle type uv_handle_t->data\n", (size_t)uv_os_getpid()); uv_walk(jl_io_loop, walk_print_cb, NULL); diff --git a/test/loading.jl b/test/loading.jl index b203d77761c0d..d7a2f0403ace1 100644 --- a/test/loading.jl +++ b/test/loading.jl @@ -1515,10 +1515,8 @@ end """) write(joinpath(foo_path, "Manifest.toml"), """ - # This file is machine-generated - editing it directly is not advised - julia_version = "1.13.0-DEV" + julia_version = "1.13.0" manifest_format = "2.0" - project_hash = "8699765aeeac181c3e5ddbaeb9371968e1f84d6b" [[deps.Foo51989]] path = "." diff --git a/test/precompile.jl b/test/precompile.jl index 127ec7a17e312..b17d687b2ca62 100644 --- a/test/precompile.jl +++ b/test/precompile.jl @@ -687,15 +687,7 @@ precompile_test_harness(false) do dir error("break me") end """) - try - Base.require(Main, :FooBar2) - error("the \"break me\" test failed") - catch exc - isa(exc, LoadError) || rethrow() - exc = exc.error - isa(exc, ErrorException) || rethrow() - "break me" == exc.msg || rethrow() - end + @test_throws Base.Precompilation.PkgPrecompileError Base.require(Main, :FooBar2) # Test that trying to eval into closed modules during precompilation is an error FooBar3_file = joinpath(dir, "FooBar3.jl") @@ -707,14 +699,7 @@ precompile_test_harness(false) do dir $code end """) - try - Base.require(Main, :FooBar3) - catch exc - isa(exc, LoadError) || rethrow() - exc = exc.error - isa(exc, ErrorException) || rethrow() - occursin("Evaluation into the closed module `Base` breaks incremental compilation", exc.msg) || rethrow() - end + @test_throws Base.Precompilation.PkgPrecompileError Base.require(Main, :FooBar3) end # Test transitive dependency for #21266 @@ -2545,4 +2530,161 @@ let io = IOBuffer() @test isempty(String(take!(io))) end +# Test --compiled-modules=strict in precompilepkgs +@testset "compiled-modules=strict with dependencies" begin + mkdepottempdir() do depot + # Create three packages: one that fails to precompile, one that loads it, one that doesn't + project_path = joinpath(depot, "testenv") + mkpath(project_path) + + # Create FailPkg - a package that can't be precompiled + fail_pkg_path = joinpath(depot, "dev", "FailPkg") + mkpath(joinpath(fail_pkg_path, "src")) + write(joinpath(fail_pkg_path, "Project.toml"), + """ + name = "FailPkg" + uuid = "10000000-0000-0000-0000-000000000001" + version = "0.1.0" + """) + write(joinpath(fail_pkg_path, "src", "FailPkg.jl"), + """ + module FailPkg + print("Now FailPkg is running.\n") + error("expected fail") + end + """) + + # Create LoadsFailPkg - depends on and loads FailPkg (should fail with strict) + loads_pkg_path = joinpath(depot, "dev", "LoadsFailPkg") + mkpath(joinpath(loads_pkg_path, "src")) + write(joinpath(loads_pkg_path, "Project.toml"), + """ + name = "LoadsFailPkg" + uuid = "20000000-0000-0000-0000-000000000002" + version = "0.1.0" + + [deps] + FailPkg = "10000000-0000-0000-0000-000000000001" + """) + write(joinpath(loads_pkg_path, "src", "LoadsFailPkg.jl"), + """ + module LoadsFailPkg + print("Now LoadsFailPkg is running.\n") + import FailPkg + print("unreachable\n") + end + """) + + # Create DependsOnly - depends on FailPkg but doesn't load it (should succeed) + depends_pkg_path = joinpath(depot, "dev", "DependsOnly") + mkpath(joinpath(depends_pkg_path, "src")) + write(joinpath(depends_pkg_path, "Project.toml"), + """ + name = "DependsOnly" + uuid = "30000000-0000-0000-0000-000000000003" + version = "0.1.0" + + [deps] + FailPkg = "10000000-0000-0000-0000-000000000001" + """) + write(joinpath(depends_pkg_path, "src", "DependsOnly.jl"), + """ + module DependsOnly + # Has FailPkg as a dependency but doesn't load it + print("Now DependsOnly is running.\n") + end + """) + + # Create main project with all packages + write(joinpath(project_path, "Project.toml"), + """ + [deps] + LoadsFailPkg = "20000000-0000-0000-0000-000000000002" + DependsOnly = "30000000-0000-0000-0000-000000000003" + """) + write(joinpath(project_path, "Manifest.toml"), + """ + julia_version = "1.13.0" + manifest_format = "2.0" + + [[DependsOnly]] + deps = ["FailPkg"] + uuid = "30000000-0000-0000-0000-000000000003" + version = "0.1.0" + + [[FailPkg]] + uuid = "10000000-0000-0000-0000-000000000001" + version = "0.1.0" + + [[LoadsFailPkg]] + deps = ["FailPkg"] + uuid = "20000000-0000-0000-0000-000000000002" + version = "0.1.0" + + [[deps.DependsOnly]] + deps = ["FailPkg"] + path = "../dev/DependsOnly/" + uuid = "30000000-0000-0000-0000-000000000003" + version = "0.1.0" + + [[deps.FailPkg]] + path = "../dev/FailPkg/" + uuid = "10000000-0000-0000-0000-000000000001" + version = "0.1.0" + + [[deps.LoadsFailPkg]] + deps = ["FailPkg"] + path = "../dev/LoadsFailPkg/" + uuid = "20000000-0000-0000-0000-000000000002" + version = "0.1.0" + """) + + # Call precompilepkgs with output redirected to a file + LoadsFailPkg_output = joinpath(depot, "LoadsFailPkg_output.txt") + DependsOnly_output = joinpath(depot, "DependsOnly_output.txt") + original_depot_path = copy(Base.DEPOT_PATH) + old_proj = Base.active_project() + try + push!(empty!(DEPOT_PATH), depot) + Base.set_active_project(project_path) + precompile_capture(file, pkg) = open(file, "w") do io + try + r = Base.Precompilation.precompilepkgs([pkg]; io, fancyprint=true) + @test r isa Vector{String} + r + catch ex + ex isa Base.Precompilation.PkgPrecompileError || rethrow() + ex + end + end + loadsfailpkg = precompile_capture(LoadsFailPkg_output, "LoadsFailPkg") + @test loadsfailpkg isa Base.Precompilation.PkgPrecompileError + dependsonly = precompile_capture(DependsOnly_output, "DependsOnly") + @test length(dependsonly) == 1 + finally + Base.set_active_project(old_proj) + append!(empty!(DEPOT_PATH), original_depot_path) + end + + output = read(LoadsFailPkg_output, String) + # LoadsFailPkg should fail because it tries to load FailPkg with --compiled-modules=strict + @test count("LoadError: expected fail", output) == 1 + @test count("expected fail", output) == 1 + @test count("✗ FailPkg", output) > 0 + @test count("✗ LoadsFailPkg", output) > 0 + @test count("Now FailPkg is running.", output) == 1 + @test count("Now LoadsFailPkg is running.", output) == 1 + @test count("DependsOnly precompiling.", output) == 0 + + # DependsOnly should succeed because it doesn't actually load FailPkg + output = read(DependsOnly_output, String) + @test count("LoadError: expected fail", output) == 0 + @test count("expected fail", output) == 0 + @test count("✗ FailPkg", output) > 0 + @test count("Precompiling DependsOnly finished.", output) == 1 + @test count("Now FailPkg is running.", output) == 0 + @test count("Now DependsOnly is running.", output) == 1 + end +end + finish_precompile_test!() From 633c488e26ef637110ab652a2bd375428368b95e Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Mon, 16 Mar 2026 15:15:48 -0400 Subject: [PATCH 25/38] precompilation: fix extension dep graph to iterate to fixed point (#61272) Co-authored-by: Claude (cherry picked from commit d7cc1c519b172136324a82581187861e761f3ab3) --- base/precompilation.jl | 40 ++++++----- test/precompile.jl | 147 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 15 deletions(-) diff --git a/base/precompilation.jl b/base/precompilation.jl index 810354a49afbe..124dfc6aa0961 100644 --- a/base/precompilation.jl +++ b/base/precompilation.jl @@ -728,21 +728,31 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, return indirect_deps end - # this loop must be run after the full direct_deps map has been populated - indirect_deps = expand_indirect_dependencies(direct_deps) - for ext in keys(ext_to_parent) - ext_loadable_in_pkg = Dict{Base.PkgId,Bool}() - for pkg in keys(direct_deps) - is_trigger = in(pkg, direct_deps[ext]) - is_extension = in(pkg, keys(ext_to_parent)) - has_triggers = issubset(direct_deps[ext], indirect_deps[pkg]) - ext_loadable_in_pkg[pkg] = !is_extension && has_triggers && !is_trigger - end - for (pkg, ext_loadable) in ext_loadable_in_pkg - if ext_loadable && !any((dep)->ext_loadable_in_pkg[dep], direct_deps[pkg]) - # add an edge if the extension is loadable by pkg, and was not loadable in any - # of the pkg's dependencies - push!(direct_deps[pkg], ext) + # This loop must be run after the full direct_deps map has been populated. + # Iterate to a fixed point because adding an extension edge (e.g. ExtA → TopPkg) + # may cause another extension (e.g. ExtAB, which depends on ExtA) to become + # loadable in TopPkg on the next iteration. + changed = true + while changed + changed = false + indirect_deps = expand_indirect_dependencies(direct_deps) + for ext in keys(ext_to_parent) + ext_loadable_in_pkg = Dict{Base.PkgId,Bool}() + for pkg in keys(direct_deps) + is_trigger = in(pkg, direct_deps[ext]) + is_extension = in(pkg, keys(ext_to_parent)) + has_triggers = issubset(direct_deps[ext], indirect_deps[pkg]) + ext_loadable_in_pkg[pkg] = !is_extension && has_triggers && !is_trigger + end + for (pkg, ext_loadable) in ext_loadable_in_pkg + if ext_loadable && !any((dep)->ext_loadable_in_pkg[dep], direct_deps[pkg]) + if ext ∉ direct_deps[pkg] + # add an edge if the extension is loadable by pkg, and was not loadable in any + # of the pkg's dependencies + push!(direct_deps[pkg], ext) + changed = true + end + end end end end diff --git a/test/precompile.jl b/test/precompile.jl index b17d687b2ca62..505d55d7783ac 100644 --- a/test/precompile.jl +++ b/test/precompile.jl @@ -2687,4 +2687,151 @@ end end end +# Issue #61198 - extensions with superset triggers must be in the precompilation dep graph +@testset "precompilation dep graph includes transitively-triggered extensions" begin + mkdepottempdir() do depot + project_path = joinpath(depot, "testenv") + mkpath(project_path) + + parent_uuid = "10000000-0000-0000-0000-000000000023" + triga_uuid = "10000000-0000-0000-0000-000000000050" + trigb_uuid = "20000000-0000-0000-0000-000000000001" + top_uuid = "10000000-0000-0000-0000-000000000064" + + # ParentPkg with two extensions: ExtA triggered by TrigA, + # ExtAB triggered by [TrigA, TrigB] (superset of ExtA's triggers) + parent_dir = joinpath(depot, "dev", "ParentPkg") + mkpath(joinpath(parent_dir, "src")) + mkpath(joinpath(parent_dir, "ext")) + write(joinpath(parent_dir, "Project.toml"), """ + name = "ParentPkg" + uuid = "$parent_uuid" + version = "0.1.0" + + [weakdeps] + TrigA = "$triga_uuid" + TrigB = "$trigb_uuid" + + [extensions] + ExtA = "TrigA" + ExtAB = ["TrigA", "TrigB"] + """) + write(joinpath(parent_dir, "src", "ParentPkg.jl"), """ + module ParentPkg + end + """) + write(joinpath(parent_dir, "ext", "ExtA.jl"), """ + module ExtA + using ParentPkg, TrigA + end + """) + write(joinpath(parent_dir, "ext", "ExtAB.jl"), """ + module ExtAB + using ParentPkg, TrigA, TrigB + end + """) + + triga_dir = joinpath(depot, "dev", "TrigA") + mkpath(joinpath(triga_dir, "src")) + write(joinpath(triga_dir, "Project.toml"), """ + name = "TrigA" + uuid = "$triga_uuid" + version = "0.1.0" + """) + write(joinpath(triga_dir, "src", "TrigA.jl"), """ + module TrigA + end + """) + + trigb_dir = joinpath(depot, "dev", "TrigB") + mkpath(joinpath(trigb_dir, "src")) + write(joinpath(trigb_dir, "Project.toml"), """ + name = "TrigB" + uuid = "$trigb_uuid" + version = "0.1.0" + """) + write(joinpath(trigb_dir, "src", "TrigB.jl"), """ + module TrigB + end + """) + + # TopPkg depends on ParentPkg + both triggers, so both extensions fire + top_dir = joinpath(depot, "dev", "TopPkg") + mkpath(joinpath(top_dir, "src")) + write(joinpath(top_dir, "Project.toml"), """ + name = "TopPkg" + uuid = "$top_uuid" + version = "0.1.0" + + [deps] + ParentPkg = "$parent_uuid" + TrigA = "$triga_uuid" + TrigB = "$trigb_uuid" + """) + write(joinpath(top_dir, "src", "TopPkg.jl"), """ + module TopPkg + using ParentPkg, TrigA, TrigB + end + """) + + write(joinpath(project_path, "Project.toml"), """ + [deps] + ParentPkg = "$parent_uuid" + TrigA = "$triga_uuid" + TrigB = "$trigb_uuid" + TopPkg = "$top_uuid" + """) + + write(joinpath(project_path, "Manifest.toml"), """ + manifest_format = "2.0" + + [[deps.ParentPkg]] + path = "../dev/ParentPkg/" + uuid = "$parent_uuid" + version = "0.1.0" + + [deps.ParentPkg.weakdeps] + TrigA = "$triga_uuid" + TrigB = "$trigb_uuid" + + [deps.ParentPkg.extensions] + ExtA = "TrigA" + ExtAB = ["TrigA", "TrigB"] + + [[deps.TrigA]] + path = "../dev/TrigA/" + uuid = "$triga_uuid" + version = "0.1.0" + + [[deps.TrigB]] + path = "../dev/TrigB/" + uuid = "$trigb_uuid" + version = "0.1.0" + + [[deps.TopPkg]] + deps = ["ParentPkg", "TrigA", "TrigB"] + path = "../dev/TopPkg/" + uuid = "$top_uuid" + version = "0.1.0" + """) + + original_depot_path = copy(Base.DEPOT_PATH) + old_proj = Base.active_project() + try + push!(empty!(DEPOT_PATH), depot) + Base.set_active_project(project_path) + + # First precompilation: compile everything + Base.Precompilation.precompilepkgs(; io=IOBuffer(), fancyprint=false) + + # Second precompilation: should not recompile anything + io = IOBuffer() + Base.Precompilation.precompilepkgs(; io, fancyprint=false) + @test isempty(takestring!(io)) + finally + Base.set_active_project(old_proj) + append!(empty!(DEPOT_PATH), original_depot_path) + end + end +end finish_precompile_test!() From b1f9a868560a90e1c98715a05eca7b8182fc69a6 Mon Sep 17 00:00:00 2001 From: James Wrigley Date: Wed, 25 Mar 2026 03:09:25 +0100 Subject: [PATCH 26/38] Set max_methods to 1 for merge/merge!/mergewith/mergewith! (#61330) Should prevent these invalidations seen when loading CurveFit.jl on 1.13: ```julia inserting merge!(m::DataStructures.SortedDict{K, D, Ord}, others::AbstractDict{K, D}...) where {K, D, Ord<:Ordering} @ DataStructures ~/.julia/packages/DataStructures/qUuAY/src/sorted_dict.jl:473 invalidated: mt_backedges: 1: signature Tuple{typeof(merge!), Any, AbstractDict} triggered MethodInstance for Base.grow_to!(::AbstractDict{K, V}, ::Base.Generator{_A, Base.Compiler.IRShow.var"#30#31"} where _A, ::Any) where {K, V} (0 children) 2: signature Tuple{typeof(merge!), Any, AbstractDict} triggered MethodInstance for Base.grow_to!(::AbstractDict{K, V}, ::Base.Generator, ::Any) where {K, V} (0 children) 3: signature Tuple{typeof(merge!), Any, AbstractDict} triggered MethodInstance for Base.grow_to!(::AbstractDict{K, V}, ::Base.Generator, ::Any) where {K, V} (0 children) 4: signature Tuple{typeof(merge!), Any, AbstractDict} triggered MethodInstance for Base.grow_to!(::AbstractDict{K, V}, ::Base.Generator{I, Base.var"#Generator##0#Generator##1"{NonlinearSolveBase.Utils.var"#norm_op##2#norm_op##3"{typeof(+)}}} where I<:(Base.Iterators.Zip{Is} where Is<:Tuple{Base.AbstractBroadcasted, Base.AbstractBroadcasted}), ::Tuple{Tuple{Any, Any}, Tuple{Any, Any}}) where {K, V} (0 children) 5: signature Tuple{typeof(merge!), Any, AbstractDict} triggered MethodInstance for Base.grow_to!(::AbstractDict{K, V}, ::Base.Generator{_A, F} where {_A, F<:DifferentiationInterface.var"#_prepare_jacobian_aux##0#_prepare_jacobian_aux##1"}, ::Any) where {K, V} (0 children) 6: signature Tuple{typeof(merge!), Any, AbstractDict} triggered MethodInstance for Base.grow_to!(::AbstractDict{K, V}, ::Base.Generator{_A, F} where {_A, F<:DifferentiationInterface.var"#_prepare_jacobian_aux##12#_prepare_jacobian_aux##13"}, ::Any) where {K, V} (0 children) 7: signature Tuple{typeof(merge!), Any, AbstractDict} triggered MethodInstance for Base.grow_to!(::AbstractDict{K, V}, ::Base.Generator{I, Base.var"#Generator##0#Generator##1"{NonlinearSolveBase.Utils.var"#norm_op##2#norm_op##3"{typeof(+)}}} where I<:(Base.Iterators.Zip{Is} where Is<:Tuple{Base.AbstractBroadcasted, Vector{Float64}}), ::Tuple{Tuple{Any, Any}, Int64}) where {K, V} (0 children) backedges: 1: superseding merge!(d::AbstractDict, others::AbstractDict...) @ Base abstractdict.jl:224 with MethodInstance for merge!(::AbstractDict, ::AbstractDict) (1126 children) inserting merge(d::OrderedCollections.OrderedDict, others::AbstractDict...) @ OrderedCollections ~/.julia/dev/OrderedCollections/src/ordered_dict.jl:488 invalidated: mt_backedges: <38 invalidations I deleted because they have enormous types> backedges: 1: superseding merge(d::AbstractDict, others::AbstractDict...) @ Base abstractdict.jl:359 with MethodInstance for merge(::AbstractDict, ::Base.Pairs{Symbol, Union{}, Nothing, @NamedTuple{}}) (21 children) 2: superseding merge(d::AbstractDict, others::AbstractDict...) @ Base abstractdict.jl:359 with MethodInstance for merge(::AbstractDict, ::Base.Pairs{Symbol, _A, Nothing, A} where {_A, A<:NamedTuple}) (2489 children) ``` Partial alternative to #61329. I added `mergewith()` and `mergewith!()` to be on the safe side. (cherry picked from commit c3b914f3159d4a8200e9180e300b87cfa5820cfc) --- base/abstractdict.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/base/abstractdict.jl b/base/abstractdict.jl index 31c00c5b4f8d5..f044264e327b0 100644 --- a/base/abstractdict.jl +++ b/base/abstractdict.jl @@ -232,6 +232,7 @@ function merge!(d::AbstractDict, others::AbstractDict...) end return d end +typeof(merge!).name.max_methods = UInt8(1) """ mergewith!(combine, d::AbstractDict, others::AbstractDict...) -> d @@ -282,6 +283,7 @@ Dict{Int64, Int64} with 3 entries: function mergewith!(combine, d::AbstractDict, others::AbstractDict...) foldl(mergewith!(combine), others; init = d) end +typeof(mergewith!).name.max_methods = UInt8(1) function mergewith!(combine, d1::AbstractDict, d2::AbstractDict) for (k, v) in d2 @@ -358,6 +360,7 @@ Dict{String, Float64} with 3 entries: """ merge(d::AbstractDict, others::AbstractDict...) = merge!(_typeddict(d, others...), others...) +typeof(merge).name.max_methods = UInt8(1) """ mergewith(combine, d::AbstractDict, others::AbstractDict...) @@ -405,6 +408,7 @@ Dict{Any, Any} with 1 entry: mergewith(combine, d::AbstractDict, others::AbstractDict...) = mergewith!(combine, _typeddict(d, others...), others...) mergewith(combine) = (args...) -> mergewith(combine, args...) +typeof(mergewith).name.max_methods = UInt8(1) merge(combine::Callable, d::AbstractDict, others::AbstractDict...) = merge!(combine, _typeddict(d, others...), others...) From 1cfbf42661d583a460751fcb94a0c1d7e9911a5f Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki <40514306+aviatesk@users.noreply.github.com> Date: Thu, 9 Apr 2026 08:58:40 +0900 Subject: [PATCH 27/38] inference: Respect `@nospecializeinfer` semantics in constant prop (#61502) `@nospecializeinfer` widens the `MethodInstance` signature so that inference does not specialize on `@nospecialize`'d arguments, but const prop was not respecting this and `matching_cache_argtypes` would expand a `Vararg` in the `MI`'s `cache_argtypes` to match the call-site arity, producing `argtypes` of different lengths for different const-prop call sites sharing the same `MI`. This caused an assertion failure in `constprop_cache_lookup` when a second call with a different number of varargs tried to look up a cached result. Fix this by widening the `@nospecialize`'d arguments back to the `cache_argtypes`, using the same `nospecialize` bitmask convention as `jl_compilation_sig` in `src/gf.c`. This ensures constprop argtypes always have the same shape as the `MI`'s baseline argtypes, and avoids unnecessary specialization that contradicts the intent of `@nospecializeinfer`. Fixes aviatesk/JETLS.jl#618. (cherry picked from commit 525c655eefa4f6e35d011d8e35c37731179586aa) --- Compiler/src/abstractinterpretation.jl | 1 + Compiler/src/inferenceresult.jl | 19 +++++++++++++++++++ Compiler/test/inference.jl | 14 ++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/Compiler/src/abstractinterpretation.jl b/Compiler/src/abstractinterpretation.jl index 3f3605d48ead0..18328b26c7521 100644 --- a/Compiler/src/abstractinterpretation.jl +++ b/Compiler/src/abstractinterpretation.jl @@ -1317,6 +1317,7 @@ function const_prop_call(interp::AbstractInterpreter, cache_argtypes = matching_cache_argtypes(𝕃ᵢ, mi) end argtypes = matching_cache_argtypes(𝕃ᵢ, mi, forwarded_argtypes, cache_argtypes) + argtypes = get_nospecializeinfer_argtypes(argtypes, cache_argtypes, mi.def::Method) inf_result = cache_lookup(𝕃ᵢ, mi, argtypes, get_inference_cache(interp)) if inf_result === missing # a previous const-prop attempt hit a cycle and produced a limited result; diff --git a/Compiler/src/inferenceresult.jl b/Compiler/src/inferenceresult.jl index b985a52ac02ba..43a113fc5d3fa 100644 --- a/Compiler/src/inferenceresult.jl +++ b/Compiler/src/inferenceresult.jl @@ -5,6 +5,25 @@ function matching_cache_argtypes(::AbstractLattice, mi::MethodInstance) return most_general_argtypes(isa(def, Method) ? def : nothing, specTypes) end +# For `@nospecializeinfer` methods, widen the `@nospecialize`'d argument positions back to +# `cache_argtypes` values to respect the `@nospecializeinfer` semantics. +# This also ensures that the constprop `argtypes` have the same length as `cache_argtypes`. +function get_nospecializeinfer_argtypes(argtypes::Vector{Any}, cache_argtypes::Vector{Any}, + method::Method) + is_nospecializeinfer(method) || return argtypes + nargs = Int(method.nargs) + new_argtypes = Vector{Any}(undef, length(cache_argtypes)) + for i = 1:length(cache_argtypes) + i_arg = min(i - 1, nargs - 1) # 0-indexed, 0 is the function slot + if i_arg > 0 && !iszero(method.nospecialize & (1 << (i_arg - 1))) + new_argtypes[i] = cache_argtypes[i] + else + new_argtypes[i] = argtypes[i] + end + end + return new_argtypes +end + struct SimpleArgtypes argtypes::Vector{Any} end diff --git a/Compiler/test/inference.jl b/Compiler/test/inference.jl index 164fd19e43e91..6b57981a72e6d 100644 --- a/Compiler/test/inference.jl +++ b/Compiler/test/inference.jl @@ -6567,4 +6567,18 @@ let @test f(1, 2, 3) == (0, (0, (0, 1))) end +# aviatesk/JETLS.jl/issues/618 +Base.@nospecializeinfer function jetls618(a, @nospecialize(rest...)) + if a > 0 + z = a + length(rest) + else + z = 0 + end + println(z) + return z +end +@test Base.infer_return_type() do + jetls618(1,2,3), jetls618(1,2,3,4) +end == Tuple{Int,Int} + end # module inference From c095d49f3f1782fdd3ec12ca5d1ef2bbc77d2b17 Mon Sep 17 00:00:00 2001 From: Joshua Lampert <51029046+JoshuaLampert@users.noreply.github.com> Date: Tue, 23 Dec 2025 23:14:36 +0100 Subject: [PATCH 28/38] Print `full_name` after precompiling extension (#60456) (cherry picked from commit 3690b1e09306e146718002db9b96edfb01253c72) --- base/precompilation.jl | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/base/precompilation.jl b/base/precompilation.jl index 124dfc6aa0961..ca1a3cb2df58d 100644 --- a/base/precompilation.jl +++ b/base/precompilation.jl @@ -941,7 +941,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, if occursin("Waiting for background task / IO / timer", str) thistaskwaiting = true !liveprinting && !fancyprint && @lock print_lock begin - println(io, pkg.name, color_string(str, Base.warn_color())) + println(io, full_name(ext_to_parent, pkg), color_string(str, Base.warn_color())) end push!(taskwaiting, pkg_config) end @@ -1147,7 +1147,8 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, else # allows processes to wait if another process is precompiling a given package to # a functionally identical package cache (except for preferences, which may differ) - t = @elapsed ret = precompile_pkgs_maybe_cachefile_lock(io, print_lock, fancyprint, pkg_config, pkgspidlocked, hascolor, parallel_limiter) do + fullname = full_name(ext_to_parent, pkg) + t = @elapsed ret = precompile_pkgs_maybe_cachefile_lock(io, print_lock, fancyprint, pkg_config, pkgspidlocked, hascolor, parallel_limiter, fullname) do # refresh and double-check the search now that we have global lock if interrupted_or_done[] return ErrorException("canceled") @@ -1259,7 +1260,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, if !quick_exit logstr = sprint(context=logio) do iostr if fancyprint # replace the progress bar - what = isempty(requested_pkgids) ? "packages finished." : "$(join((p.name for p in requested_pkgids), ", ", " and ")) finished." + what = isempty(requested_pkgids) ? "packages finished." : "$(join((full_name(ext_to_parent, p) for p in requested_pkgids), ", ", " and ")) finished." printpkgstyle(iostr, :Precompiling, what) end plural = length(configs) > 1 ? "dependency configurations" : ndeps == 1 ? "dependency" : "dependencies" @@ -1332,7 +1333,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, err_str = IOBuffer() for ((dep, config), err) in failed_deps write(err_str, "\n") - print(err_str, "\n", dep.name, " ") + print(err_str, "\n", full_name(ext_to_parent, dep), " ") join(err_str, config[1], " ") print(err_str, "\n", err) end @@ -1367,7 +1368,7 @@ end # - `nothing`: cache already existed # - `Tuple{String, Union{Nothing, String}}`: this process just compiled # - `Exception`: compilation failed -function precompile_pkgs_maybe_cachefile_lock(f, io::IO, print_lock::ReentrantLock, fancyprint::Bool, pkg_config, pkgspidlocked, hascolor, parallel_limiter::Base.Semaphore) +function precompile_pkgs_maybe_cachefile_lock(f, io::IO, print_lock::ReentrantLock, fancyprint::Bool, pkg_config, pkgspidlocked, hascolor, parallel_limiter::Base.Semaphore, fullname) if !(isdefined(Base, :mkpidlock_hook) && isdefined(Base, :trymkpidlock_hook) && Base.isdefined(Base, :parse_pidfile_hook)) return f() end @@ -1388,7 +1389,7 @@ function precompile_pkgs_maybe_cachefile_lock(f, io::IO, print_lock::ReentrantLo "another machine (hostname: $hostname, pid: $pid, pidfile: $pidfile)" end !fancyprint && @lock print_lock begin - println(io, " ", pkg.name, _color_string(" Being precompiled by $(pkgspidlocked[pkg_config])", Base.info_color(), hascolor)) + println(io, " ", fullname, _color_string(" Being precompiled by $(pkgspidlocked[pkg_config])", Base.info_color(), hascolor)) end Base.release(parallel_limiter) # release so other work can be done while waiting try From 03dd28beea3ff096157f79940ccd92d80ebaa969 Mon Sep 17 00:00:00 2001 From: Dilum Aluthge Date: Sun, 1 Mar 2026 09:35:58 -0500 Subject: [PATCH 29/38] REPL: Fix call to correct `hascolor` function (#61142) `REPL.hascolor` and `Base.Terminals.hascolor` are two different generic functions. The method for `::TextTerminal` is a method of the latter. Detected by JET. (cherry picked from commit 2aa3d629281f04fddabf2fae0a364f2f89f10bf1) --- stdlib/REPL/src/REPL.jl | 2 +- stdlib/REPL/test/repl.jl | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index d641853eb4c7a..b45bcf2f92585 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -710,7 +710,7 @@ mutable struct BasicREPL <: AbstractREPL end outstream(r::BasicREPL) = r.terminal -hascolor(r::BasicREPL) = hascolor(r.terminal) +hascolor(r::BasicREPL) = Terminals.hascolor(r.terminal) function run_frontend(repl::BasicREPL, backend::REPLBackendRef) repl.frontend_task = current_task() diff --git a/stdlib/REPL/test/repl.jl b/stdlib/REPL/test/repl.jl index b93c3371306b6..ba3de3b2e557e 100644 --- a/stdlib/REPL/test/repl.jl +++ b/stdlib/REPL/test/repl.jl @@ -2039,3 +2039,9 @@ end end end end + +@testset "REPL.hascolor(::BasicREPL)" begin + term = REPL.Terminals.TTYTerminal("dumb",IOBuffer("1+2\n"),IOContext(IOBuffer(),:foo=>true),IOBuffer()) + r = REPL.BasicREPL(term) + @test !REPL.hascolor(r) +end From 1685f4e2725397513cd330212a36ba789f1b2e4f Mon Sep 17 00:00:00 2001 From: Cody Tapscott <84105208+topolarity@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:02:22 -0500 Subject: [PATCH 30/38] Preferences: Fully serialize compile-time preferences (#61214) This change resolves some notable gaps in Preferences' tracking of compile-time dependencies: 1. Preferences tracks only preferences keyed on the active `__toplevel__` module, despite allowing you to query preferences keyed on any module / package. 2. Preference changes are checked for by comparing a hash, without a full value comparison. This hash comparison was probably something of a premature optimization, since it is done very rarely and it is impossible to compute the hash before asking the pkgimage what preferences it depended upon anyway. It has also led to a few serious bugs. Resolve this by storing a serialized (TOML) representation of the compile-time dependencies used in any Module and de-serializing this upon loading instead. Fixes https://github.com/JuliaPackaging/Preferences.jl/issues/86. Fixes https://github.com/JuliaLang/julia/issues/59344. Fixes https://github.com/JuliaLang/julia/issues/59257. --------- Co-authored-by: Claude (cherry picked from commit a25da508391cc8c28aa590b6d51515b5e31c3ea9) --- base/loading.jl | 168 +++++++++++++++++++++++-------------- src/staticdata_utils.c | 64 ++++---------- test/loading.jl | 186 ++++++++++++++++++++++++++++++++++++++++- test/precompile.jl | 186 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 489 insertions(+), 115 deletions(-) diff --git a/base/loading.jl b/base/loading.jl index 3bed81b6eb78d..cbc9640a5ef59 100644 --- a/base/loading.jl +++ b/base/loading.jl @@ -1254,7 +1254,7 @@ function find_all_in_cache_path(pkg::PkgId, DEPOT_PATH::typeof(DEPOT_PATH)=DEPOT if iszero(isvalid_cache_header(io)) false else - _, _, _, _, _, _, _, flags = parse_cache_header(io, path) + _, _, _, _, _, _, flags = parse_cache_header(io, path) CacheFlags(flags).use_pkgimages end finally @@ -2027,7 +2027,7 @@ function _tryrequire_from_serialized(pkg::PkgId, path::String, ocachepath::Union io = open(path, "r") try iszero(isvalid_cache_header(io)) && return ArgumentError("Incompatible header in cache file $path.") - _, (includes, _, _), depmodnames, _, _, _, clone_targets, _ = parse_cache_header(io, path) + _, (includes, _, _), depmodnames, _, _, clone_targets, _ = parse_cache_header(io, path) pkgimage = !isempty(clone_targets) @@ -3274,7 +3274,7 @@ function compilecache_dir(pkg::PkgId) return joinpath(DEPOT_PATH[1], entrypath) end -function compilecache_path(pkg::PkgId, prefs_hash::UInt64; flags::CacheFlags=CacheFlags(), project::String=something(Base.active_project(), ""))::String +function compilecache_path(pkg::PkgId, prefs_blob::String; flags::CacheFlags=CacheFlags(), project::String=something(Base.active_project(), ""))::String entrypath, entryfile = cache_file_entry(pkg) cachepath = joinpath(DEPOT_PATH[1], entrypath) isdir(cachepath) || mkpath(cachepath) @@ -3291,8 +3291,7 @@ function compilecache_path(pkg::PkgId, prefs_hash::UInt64; flags::CacheFlags=Cac cpu_target = unsafe_string(JLOptions().cpu_target) end crc = _crc32c(cpu_target, crc) - - crc = _crc32c(prefs_hash, crc) + crc = _crc32c(prefs_blob, crc) project_precompile_slug = slug(crc, 5) abspath(cachepath, string(entryfile, "_", project_precompile_slug, ".ji")) end @@ -3363,10 +3362,10 @@ function compilecache(pkg::PkgId, path::String, internal_stderr::IO = stderr, in Linking.link_image(tmppath_o, tmppath_so) end - # Read preferences hash back from .ji file (we can't precompute because - # we don't actually know what the list of compile-time preferences are without compiling) - prefs_hash = preferences_hash(tmppath) - cachefile = compilecache_path(pkg, prefs_hash; flags=cacheflags) + # Read preferences blob back from .ji file (we can't precompute because we don't + # actually know what the list of compile-time preferences are without compiling) + prefs_blob = preferences_blob(tmppath) + cachefile = compilecache_path(pkg, prefs_blob; flags=cacheflags) ocachefile = cache_objects ? ocachefile_from_cachefile(cachefile) : nothing # append checksum for so to the end of the .ji file: @@ -3596,18 +3595,10 @@ function _parse_cache_header(f::IO, cachefile::AbstractString) push!(includes, CacheHeaderIncludes(modkey, depname, fsize, hash, mtime, modpath)) end end - prefs = String[] - while true - n2 = read(f, Int32) - totbytes -= 4 - if n2 == 0 - break - end - push!(prefs, String(read(f, n2))) - totbytes -= n2 - end - prefs_hash = read(f, UInt64) - totbytes -= 8 + n2 = read(f, Int32) + totbytes -= 4 + prefs_blob = String(read(f, n2)) + totbytes -= n2 srctextpos = read(f, Int64) totbytes -= 8 @assert totbytes == 0 "header of cache file appears to be corrupt (totbytes == $(totbytes))" @@ -3618,12 +3609,12 @@ function _parse_cache_header(f::IO, cachefile::AbstractString) srcfiles = srctext_files(f, srctextpos, includes) - return modules, (includes, srcfiles, requires), required_modules, srctextpos, prefs, prefs_hash, clone_targets, flags + return modules, (includes, srcfiles, requires), required_modules, srctextpos, prefs_blob, clone_targets, flags end function parse_cache_header(f::IO, cachefile::AbstractString) modules, (includes, srcfiles, requires), required_modules, - srctextpos, prefs, prefs_hash, clone_targets, flags = _parse_cache_header(f, cachefile) + srctextpos, prefs_blob, clone_targets, flags = _parse_cache_header(f, cachefile) includes_srcfiles = CacheHeaderIncludes[] includes_depfiles = CacheHeaderIncludes[] @@ -3697,7 +3688,7 @@ function parse_cache_header(f::IO, cachefile::AbstractString) end end - return modules, (includes, includes_srcfiles, requires), required_modules, srctextpos, prefs, prefs_hash, clone_targets, flags + return modules, (includes, includes_srcfiles, requires), required_modules, srctextpos, prefs_blob, clone_targets, flags end function parse_cache_header(cachefile::String) @@ -3711,14 +3702,14 @@ function parse_cache_header(cachefile::String) end end -preferences_hash(f::IO, cachefile::AbstractString) = parse_cache_header(f, cachefile)[6] -function preferences_hash(cachefile::String) +preferences_blob(f::IO, cachefile::AbstractString) = parse_cache_header(f, cachefile)[5] +function preferences_blob(cachefile::String) io = open(cachefile, "r") try if iszero(isvalid_cache_header(io)) throw(ArgumentError("Incompatible header in cache file $cachefile.")) end - return preferences_hash(io, cachefile) + return preferences_blob(io, cachefile) finally close(io) end @@ -3740,7 +3731,7 @@ function cache_dependencies(cachefile::String) end function read_dependency_src(io::IO, cachefile::AbstractString, filename::AbstractString) - _, (includes, _, _), _, srctextpos, _, _, _, _ = parse_cache_header(io, cachefile) + _, (includes, _, _), _, srctextpos, _, _, _ = parse_cache_header(io, cachefile) srctextpos == 0 && error("no source-text stored in cache file") seek(io, srctextpos) return _read_dependency_src(io, filename, includes) @@ -3940,42 +3931,48 @@ function get_preferences(uuid::Union{UUID,Nothing} = nothing) return merged_prefs end -function get_preferences_hash(uuid::Union{UUID, Nothing}, prefs_list::Vector{String}) - # Start from a predictable hash point to ensure that the same preferences always - # hash to the same value, modulo changes in how Dictionaries are hashed. - h = UInt(0) - uuid === nothing && return UInt64(h) - - # Load the preferences - prefs = get_preferences(uuid) - - # Walk through each name that's called out as a compile-time preference - for name in prefs_list - prefs_value = get(prefs, name, nothing) - if prefs_value !== nothing - h = hash(prefs_value, h)::UInt - end - end - # We always return a `UInt64` so that our serialization format is stable - return UInt64(h) -end - -get_preferences_hash(m::Module, prefs_list::Vector{String}) = get_preferences_hash(PkgId(m).uuid, prefs_list) - # This is how we keep track of who is using what preferences at compile-time const COMPILETIME_PREFERENCES = Dict{UUID,Set{String}}() -# In `Preferences.jl`, if someone calls `load_preference(@__MODULE__, key)` while we're precompiling, -# we mark that usage as a usage at compile-time and call this method, so that at the end of `.ji` generation, -# we can record the list of compile-time preferences and embed that into the `.ji` header +# This is used by `Preferences.load_preference` to record any (pre)compile-time preference +# queries, which the resulting pkgimage will be keyed on to detect preference changes. function record_compiletime_preference(uuid::UUID, key::String) pref = get!(Set{String}, COMPILETIME_PREFERENCES, uuid) push!(pref, key) return nothing end -get_compiletime_preferences(uuid::UUID) = collect(get(Vector{String}, COMPILETIME_PREFERENCES, uuid)) -get_compiletime_preferences(m::Module) = get_compiletime_preferences(PkgId(m).uuid) -get_compiletime_preferences(::Nothing) = String[] + +# Return a serialized "blob" of all observed (at compile-time) preferences. +# This is called at pkgimage write time (.ji generation). +function get_preferences_blob() + isempty(COMPILETIME_PREFERENCES) && return "" + observed = Dict{String, Any}() + unset = Dict{String, Any}() + for (uuid, pref_keys) in COMPILETIME_PREFERENCES + uuid_prefs = get_preferences(uuid) + uuid_str = string(uuid) + for key in pref_keys + if haskey(uuid_prefs, key) + uuid_observed = get!(Dict{String, Any}, observed, uuid_str) + uuid_observed[key] = uuid_prefs[key] + else + # preferences that were not set are tracked in a + # separate table to prevent name / value collisions + uuid_unset = get!(Set{Any}, unset, uuid_str) + push!(uuid_unset, key) + end + end + end + for (uuid, uuid_unset) in pairs(unset) + unset[uuid] = sort!(collect(uuid_unset)) + end + if !isempty(unset) + observed["unset"] = unset + end + buf = IOBuffer() + TOML.Printer.print(buf, observed; sorted=true) + return String(take!(buf)) +end function check_clone_targets(clone_targets) rejection_reason = ccall(:jl_check_pkgimage_clones, Any, (Ptr{Cchar},), clone_targets) @@ -3989,10 +3986,10 @@ global mkpidlock_hook::Any global trymkpidlock_hook::Any global parse_pidfile_hook::Any -# The preferences hash is only known after precompilation so just assume no preferences. +# The preferences blob is only known after precompilation so just assume no preferences. # Also ignore the active project, which means that if all other conditions are equal, # the same package cannot be precompiled from different projects and/or different preferences at the same time. -compilecache_pidfile_path(pkg::PkgId; flags::CacheFlags=CacheFlags()) = compilecache_path(pkg, UInt64(0); project="", flags) * ".pidfile" +compilecache_pidfile_path(pkg::PkgId; flags::CacheFlags=CacheFlags()) = compilecache_path(pkg, ""; project="", flags) * ".pidfile" const compilecache_pidlock_stale_age = 10 @@ -4087,6 +4084,54 @@ function any_includes_stale(includes::Vector{CacheHeaderIncludes}, cachefile::St return false end +# This custom equality predicate is analogous to `===`, except that it also +# compares mutable containers structurally. This allows us to differentiate +# `1.0` / `1` / `true` in Preferences, despite these being normally `isequal` +toml_egal(@nospecialize(A), @nospecialize(B)) = A === B + +function toml_egal(A::AbstractArray, B::AbstractArray) + axes(A) != axes(B) && return false + for (a, b) in zip(A, B) + !toml_egal(a, b) && return false + end + return true +end + +function toml_egal(A::AbstractDict, B::AbstractDict) + isa(A,IdDict) != isa(B,IdDict) && return false + length(A) != length(B) && return false + for pair in A + !in(pair, B, toml_egal) && return false + end + return true +end + +function stale_prefs(prefs_blob::String) + # ensure any preferences observed match their precompile-time values + prefs_blob == "" && return false # no observed preferences (fast-path) + prefs_data = TOML.parse(TOML.Parser{nothing}(prefs_blob)) + + for (uuid, observed) in prefs_data + uuid == "unset" && continue + curr = get_preferences(UUID(uuid)) + for (key, val) in observed + # any set preferences should have the same value + !haskey(curr, key) && return true + !toml_egal(curr[key], val) && return true + end + end + if haskey(prefs_data, "unset") + for (uuid, observed) in prefs_data["unset"] + curr = get_preferences(UUID(uuid)) + for key in observed + # any unset preferences should still be unset + haskey(curr, key) && return true + end + end + end + return false +end + # returns true if it "cachefile.ji" is stale relative to "modpath.jl" and build_id for modkey # otherwise returns the list of dependencies to also check @constprop :none function stale_cachefile(modpath::String, cachefile::String; ignore_loaded::Bool = false, requested_flags::CacheFlags=CacheFlags(), reasons=nothing) @@ -4110,7 +4155,7 @@ end record_reason(reasons, "different Julia build configuration") return true # incompatible cache file end - modules, (includes, _, requires), required_modules, srctextpos, prefs, prefs_hash, clone_targets, actual_flags = parse_cache_header(io, cachefile) + modules, (includes, _, requires), required_modules, srctextpos, prefs_blob, clone_targets, actual_flags = parse_cache_header(io, cachefile) if isempty(modules) return true # ignore empty file end @@ -4266,9 +4311,8 @@ end end end - curr_prefs_hash = get_preferences_hash(id.uuid, prefs) - if prefs_hash != curr_prefs_hash - @debug "Rejecting cache file $cachefile because preferences hash does not match 0x$(string(prefs_hash, base=16)) != 0x$(string(curr_prefs_hash, base=16))" + if stale_prefs(prefs_blob) + @debug "Rejecting cache file $cachefile because preferences have changed" record_reason(reasons, "package preferences changed") return true end diff --git a/src/staticdata_utils.c b/src/staticdata_utils.c index 285dcfc1dabe8..2542f6fc9f537 100644 --- a/src/staticdata_utils.c +++ b/src/staticdata_utils.c @@ -720,14 +720,11 @@ static int64_t write_dependency_list(ios_t *s, jl_array_t* worklist, jl_array_t jl_task_t *ct = jl_current_task; size_t last_age = ct->world_age; ct->world_age = jl_atomic_load_acquire(&jl_world_counter); - jl_value_t *depots = NULL, *prefs_hash = NULL, *prefs_list = NULL; + jl_value_t *depots = NULL, *prefs_blob = NULL; jl_value_t *unique_func = NULL; jl_value_t *replace_depot_func = NULL; jl_value_t *normalize_depots_func = NULL; - jl_value_t *toplevel = NULL; - jl_value_t *prefs_hash_func = NULL; - jl_value_t *get_compiletime_prefs_func = NULL; - JL_GC_PUSH8(&depots, &prefs_list, &unique_func, &replace_depot_func, &normalize_depots_func, &toplevel, &prefs_hash_func, &get_compiletime_prefs_func); + JL_GC_PUSH5(&depots, &prefs_blob, &unique_func, &replace_depot_func, &normalize_depots_func); jl_array_t *udeps = (jl_array_t*)jl_get_global_value(jl_base_module, jl_symbol("_require_dependencies"), ct->world_age); *udepsp = udeps; @@ -798,51 +795,22 @@ static int64_t write_dependency_list(ios_t *s, jl_array_t* worklist, jl_array_t } write_int32(s, 0); // terminator, for ease of reading - // Calculate Preferences hash for current package. - if (jl_base_module) { - // Toplevel module is the module we're currently compiling, use it to get our preferences hash - toplevel = jl_get_global_value(jl_base_module, jl_symbol("__toplevel__"), ct->world_age); - prefs_hash_func = jl_eval_global_var(jl_base_module, jl_symbol("get_preferences_hash"), ct->world_age); - get_compiletime_prefs_func = jl_eval_global_var(jl_base_module, jl_symbol("get_compiletime_preferences"), ct->world_age); - - if (toplevel) { - // call get_compiletime_prefs(__toplevel__) - jl_value_t *args[3] = {get_compiletime_prefs_func, (jl_value_t*)toplevel, NULL}; - prefs_list = (jl_value_t*)jl_apply(args, 2); - JL_TYPECHK(write_dependency_list, array, prefs_list); - - // Call get_preferences_hash(__toplevel__, prefs_list) - args[0] = prefs_hash_func; - args[2] = prefs_list; - prefs_hash = (jl_value_t*)jl_apply(args, 3); - JL_TYPECHK(write_dependency_list, uint64, prefs_hash); - } - } + // Serialize any compile-time dependencies on preferences. + assert(jl_base_module); + jl_value_t *get_preferences_blob = jl_eval_global_var( + jl_base_module, jl_symbol("get_preferences_blob"), ct->world_age); + assert(get_preferences_blob); + jl_value_t *args[1] = { get_preferences_blob }; + prefs_blob = jl_apply(args, 1); + JL_TYPECHK(write_dependency_list, string, prefs_blob); ct->world_age = last_age; - // If we successfully got the preferences, write it out, otherwise write `0` for this `.ji` file. - if (prefs_hash != NULL && prefs_list != NULL) { - size_t i, l = jl_array_nrows(prefs_list); - for (i = 0; i < l; i++) { - jl_value_t *pref_name = jl_array_ptr_ref(prefs_list, i); - JL_TYPECHK(write_dependency_list, string, pref_name); - size_t slen = jl_string_len(pref_name); - write_int32(s, slen); - ios_write(s, jl_string_data(pref_name), slen); - } - write_int32(s, 0); // terminator - write_uint64(s, jl_unbox_uint64(prefs_hash)); - } - else { - // This is an error path, but let's at least generate a valid `.ji` file. - // We declare an empty list of preference names, followed by a zero-hash. - // The zero-hash is not what would be generated for an empty set of preferences, - // and so this `.ji` file will be invalidated by a future non-erroring pass - // through this function. - write_int32(s, 0); - write_uint64(s, 0); - } - JL_GC_POP(); // for depots, prefs_list + // Write out the serialized preferences "blob" + size_t slen = jl_string_len(prefs_blob); + write_int32(s, slen); + ios_write(s, jl_string_data(prefs_blob), slen); + + JL_GC_POP(); // for depots, prefs_blob #undef jl_is_deptuple // write a dummy file position to indicate the beginning of the source-text diff --git a/test/loading.jl b/test/loading.jl index d7a2f0403ace1..14f74a7ffd871 100644 --- a/test/loading.jl +++ b/test/loading.jl @@ -275,7 +275,7 @@ end pkg = recurse_package(n...) @test pkg == PkgId(UUID(uuid), n[end]) @test joinpath(@__DIR__, normpath(path)) == locate_package(pkg) - @test Base.compilecache_path(pkg, UInt64(0)) == Base.compilecache_path(pkg, UInt64(0)) + @test Base.compilecache_path(pkg, "") == Base.compilecache_path(pkg, "") end @test identify_package("Baz") === nothing @test identify_package("Qux") === nothing @@ -1105,6 +1105,190 @@ end end end +@testset "Preferences blob" begin + # Tests for get_preferences_blob() and stale_prefs() - the serialization of + # compile-time preference observations into a TOML blob embedded in cache files. + pkg_uuid = uuid4() + pkg2_uuid = uuid4() + mktempdir() do dir + # Set up a project with preferences for two packages + write(joinpath(dir, "Project.toml"), """ + [deps] + PkgA = "$(pkg_uuid)" + PkgB = "$(pkg2_uuid)" + + [preferences.PkgA] + key1 = "val1" + key2 = 42 + + [preferences.PkgB] + flag = true + """) + + old_load_path = copy(LOAD_PATH) + old_compiletime_prefs = Dict(k => copy(v) for (k, v) in Base.COMPILETIME_PREFERENCES) + try + copy!(LOAD_PATH, [joinpath(dir, "Project.toml")]) + empty!(Base.COMPILETIME_PREFERENCES) + + # Empty COMPILETIME_PREFERENCES → empty blob (fast-path) + @test Base.get_preferences_blob() == "" + + # A queried+set preference appears in the main table + Base.record_compiletime_preference(pkg_uuid, "key1") + blob = Base.get_preferences_blob() + @test !isempty(blob) + parsed = Base.TOML.parse(Base.TOML.Parser{nothing}(blob)) + @test parsed[string(pkg_uuid)]["key1"] == "val1" + @test !haskey(parsed, "unset") + + # A queried+unset preference appears in the [unset] table + empty!(Base.COMPILETIME_PREFERENCES) + Base.record_compiletime_preference(pkg_uuid, "missing_key") + blob = Base.get_preferences_blob() + parsed = Base.TOML.parse(Base.TOML.Parser{nothing}(blob)) + @test haskey(parsed, "unset") + @test "missing_key" in parsed["unset"][string(pkg_uuid)] + @test !haskey(get(parsed, string(pkg_uuid), Dict()), "missing_key") + + # Mix of set and unset preferences for the same UUID + empty!(Base.COMPILETIME_PREFERENCES) + Base.record_compiletime_preference(pkg_uuid, "key1") + Base.record_compiletime_preference(pkg_uuid, "missing_key") + blob = Base.get_preferences_blob() + parsed = Base.TOML.parse(Base.TOML.Parser{nothing}(blob)) + @test parsed[string(pkg_uuid)]["key1"] == "val1" + @test "missing_key" in parsed["unset"][string(pkg_uuid)] + + # Multiple UUIDs are tracked independently + empty!(Base.COMPILETIME_PREFERENCES) + Base.record_compiletime_preference(pkg_uuid, "key1") + Base.record_compiletime_preference(pkg2_uuid, "flag") + blob = Base.get_preferences_blob() + parsed = Base.TOML.parse(Base.TOML.Parser{nothing}(blob)) + @test parsed[string(pkg_uuid)]["key1"] == "val1" + @test parsed[string(pkg2_uuid)]["flag"] == true + + # stale_prefs: empty blob is never stale + @test !Base.stale_prefs("") + + # stale_prefs: set preference unchanged → not stale + empty!(Base.COMPILETIME_PREFERENCES) + Base.record_compiletime_preference(pkg_uuid, "key1") + blob = Base.get_preferences_blob() + @test !Base.stale_prefs(blob) + + # stale_prefs: set preference value changed → stale + mktempdir() do dir2 + write(joinpath(dir2, "Project.toml"), """ + [deps] + PkgA = "$(pkg_uuid)" + + [preferences.PkgA] + key1 = "different" + """) + copy!(LOAD_PATH, [joinpath(dir2, "Project.toml")]) + @test Base.stale_prefs(blob) + copy!(LOAD_PATH, [joinpath(dir, "Project.toml")]) + end + + # stale_prefs: set preference becomes unset → stale + mktempdir() do dir2 + write(joinpath(dir2, "Project.toml"), """ + [deps] + PkgA = "$(pkg_uuid)" + """) + copy!(LOAD_PATH, [joinpath(dir2, "Project.toml")]) + @test Base.stale_prefs(blob) + copy!(LOAD_PATH, [joinpath(dir, "Project.toml")]) + end + + # stale_prefs: unset preference still unset → not stale + empty!(Base.COMPILETIME_PREFERENCES) + Base.record_compiletime_preference(pkg_uuid, "missing_key") + blob_unset = Base.get_preferences_blob() + @test !Base.stale_prefs(blob_unset) + + # stale_prefs: unset preference becomes set → stale + mktempdir() do dir2 + write(joinpath(dir2, "Project.toml"), """ + [deps] + PkgA = "$(pkg_uuid)" + + [preferences.PkgA] + missing_key = "now_set" + """) + copy!(LOAD_PATH, [joinpath(dir2, "Project.toml")]) + @test Base.stale_prefs(blob_unset) + copy!(LOAD_PATH, [joinpath(dir, "Project.toml")]) + end + + # hash collision test: + # https://github.com/JuliaLang/julia/issues/59345#issue-3338450603 + # + # `1`, `1.0`, and `true` are all `isequal` in Julia, so == / isequal / hash + # cannot distinguish these semantically distinct values, but the preferences + # system should distinguish them since they are clearly different objects + mktempdir() do dir_bool + write(joinpath(dir_bool, "Project.toml"), """ + [deps] + PkgA = "$(pkg_uuid)" + + [preferences.PkgA] + flag = true + """) + copy!(LOAD_PATH, [joinpath(dir_bool, "Project.toml")]) + empty!(Base.COMPILETIME_PREFERENCES) + Base.record_compiletime_preference(pkg_uuid, "flag") + blob_bool = Base.get_preferences_blob() + + mktempdir() do dir_int + write(joinpath(dir_int, "Project.toml"), """ + [deps] + PkgA = "$(pkg_uuid)" + + [preferences.PkgA] + flag = 1 + """) + copy!(LOAD_PATH, [joinpath(dir_int, "Project.toml")]) + @test Base.stale_prefs(blob_bool) + end + copy!(LOAD_PATH, [joinpath(dir, "Project.toml")]) + end + + # hash collision test (on ≥1.13, where hash(false, UInt(0)) == UInt(0)) + # + # the old hash-based approach for preference invalidation would fail to detect + # the difference between `pref = false` and "no preference". + mktempdir() do dir2 + write(joinpath(dir2, "Project.toml"), """ + [deps] + PkgA = "$(pkg_uuid)" + + [preferences.PkgA] + key1 = false + """) + copy!(LOAD_PATH, [joinpath(dir2, "Project.toml")]) + empty!(Base.COMPILETIME_PREFERENCES) + Base.record_compiletime_preference(pkg_uuid, "key1") + blob_false = Base.get_preferences_blob() + mktempdir() do dir3 + write(joinpath(dir3, "Project.toml"), """ + [deps] + PkgA = "$(pkg_uuid)" + """) + copy!(LOAD_PATH, [joinpath(dir3, "Project.toml")]) + @test Base.stale_prefs(blob_false) + end + copy!(LOAD_PATH, [joinpath(dir, "Project.toml")]) + end + finally + copy!(LOAD_PATH, old_load_path) + empty!(Base.COMPILETIME_PREFERENCES) + merge!(Base.COMPILETIME_PREFERENCES, old_compiletime_prefs) + end + end +end @testset "Loading with incomplete manifest/depot #45977" begin mktempdir() do tmp diff --git a/test/precompile.jl b/test/precompile.jl index 505d55d7783ac..fb8d7948a4d6f 100644 --- a/test/precompile.jl +++ b/test/precompile.jl @@ -643,8 +643,7 @@ precompile_test_harness(false) do dir """) cachefile, _ = @test_logs (:debug, r"Generating object cache file for FooBar") min_level=Logging.Debug match_mode=:any Base.compilecache(Base.PkgId("FooBar")) - empty_prefs_hash = Base.get_preferences_hash(nothing, String[]) - @test cachefile == Base.compilecache_path(Base.PkgId("FooBar"), empty_prefs_hash) + @test cachefile == Base.compilecache_path(Base.PkgId("FooBar"), "") @test isfile(joinpath(cachedir, "FooBar.ji")) Tsc = Bool(Base.JLOptions().use_pkgimages) ? Tuple{<:Vector, String, UInt128} : Tuple{<:Vector, Nothing, UInt128} @test Base.stale_cachefile(FooBar_file, joinpath(cachedir, "FooBar.ji")) isa Tsc @@ -2124,7 +2123,7 @@ precompile_test_harness("Test flags") do load_path ji, ofile = Base.compilecache(Base.PkgId("TestFlags"); flags=`--check-bounds=no -O3`) open(ji, "r") do io Base.isvalid_cache_header(io) - _, _, _, _, _, _, _, flags = Base.parse_cache_header(io, ji) + _, _, _, _, _, _, flags = Base.parse_cache_header(io, ji) cacheflags = Base.CacheFlags(flags) @test cacheflags.check_bounds == 2 @test cacheflags.opt_level == 3 @@ -2139,7 +2138,7 @@ if Base.get_bool_env("CI", false) && (Sys.ARCH === :x86_64 || Sys.ARCH === :aarc idx = findfirst(cachefiles) do cf Base.stale_cachefile(pkgpath, cf) !== true end - targets = Base.parse_image_targets(Base.parse_cache_header(cachefiles[idx])[7]) + targets = Base.parse_image_targets(Base.parse_cache_header(cachefiles[idx])[6]) @test length(targets) > 1 end end @@ -2834,4 +2833,183 @@ end end end end + +precompile_test_harness("invalidation for 'foreign-keyed' Preferences") do load_path + # Test that compile-time preferences invalidate, even when queried from a + # "foreign" UUID / package namespace + foreign_uuid_str = "b5f1a95c-d45e-4b9e-84c6-b7ea3b5e5f22" + foreign_uuid = Base.UUID(foreign_uuid_str) + + # Package that records compile-time preferences for a "foreign" UUID + write(joinpath(load_path, "PkgCrossPrefs.jl"), + """ + module PkgCrossPrefs + # Query preferences for a foreign package UUID (cross-UUID tracking) + Base.record_compiletime_preference(Base.UUID("$foreign_uuid_str"), "debug") + # Also query a preference that won't be set + Base.record_compiletime_preference(Base.UUID("$foreign_uuid_str"), "unset_opt") + end + """) + + env_base_dir = mkpath(joinpath(load_path, "env_base")) + env_changed_dir = mkpath(joinpath(load_path, "env_changed")) + env_unset_dir = mkpath(joinpath(load_path, "env_unset_set")) + + env_base = joinpath(env_base_dir, "Project.toml") + env_changed = joinpath(env_changed_dir, "Project.toml") + env_unset_set = joinpath(env_unset_dir, "Project.toml") + + write(env_base, """ + [deps] + PkgForeign = "$foreign_uuid_str" + + [preferences.PkgForeign] + debug = false + """) + write(env_changed, """ + [deps] + PkgForeign = "$foreign_uuid_str" + + [preferences.PkgForeign] + debug = true + """) + write(env_unset_set, """ + [deps] + PkgForeign = "$foreign_uuid_str" + + [preferences.PkgForeign] + debug = false + unset_opt = "now_set" + """) + + old_proj = Base.active_project() + try + Base.set_active_project(env_base) + cachefile, _ = Base.compilecache(Base.PkgId("PkgCrossPrefs")) + pkg_file = joinpath(load_path, "PkgCrossPrefs.jl") + + # Cache is not stale with the original preferences + @test Base.stale_cachefile(pkg_file, cachefile) !== true + # Changing the (foreign) preferences makes the cache stale + Base.set_active_project(env_changed) + @test Base.stale_cachefile(pkg_file, cachefile) === true + # Restoring the original preferences makes it not stale again + Base.set_active_project(env_base) + @test Base.stale_cachefile(pkg_file, cachefile) !== true + # Setting a previously-unset preference also causes staleness + Base.set_active_project(env_unset_set) + @test Base.stale_cachefile(pkg_file, cachefile) === true + finally + Base.set_active_project(old_proj) + end +end + +precompile_test_harness("Preferences hash collision (issue #59344), part 1") do load_path + # Test for the preference hash collision bug (https://github.com/JuliaLang/julia/issues/59344) + # related to skipping unset preferences in preference hash + pkg_uuid_str = "c9de8a70-0fad-4996-b3e2-f9c45bce31af" + pkg_uuid = Base.UUID(pkg_uuid_str) + + pkg_src_dir = mkpath(joinpath(load_path, "PkgHashCollision", "src")) + write(joinpath(pkg_src_dir, "PkgHashCollision.jl"), + """ + module PkgHashCollision + Base.record_compiletime_preference(Base.UUID("$pkg_uuid_str"), "xyz") + Base.record_compiletime_preference(Base.UUID("$pkg_uuid_str"), "abc") + end + """) + write(joinpath(load_path, "PkgHashCollision", "Project.toml"), """ + name = "PkgHashCollision" + uuid = "$pkg_uuid_str" + """) + + env_xyz_dir = mkpath(joinpath(load_path, "env_xyz")) + env_abc_dir = mkpath(joinpath(load_path, "env_abc")) + + env_xyz = joinpath(env_xyz_dir, "Project.toml") + env_abc = joinpath(env_abc_dir, "Project.toml") + + # First config: xyz has the value, abc is unset + write(env_xyz, """ + [deps] + PkgHashCollision = "$pkg_uuid_str" + + [preferences.PkgHashCollision] + xyz = "same_value" + """) + # Second config: abc has the same value, xyz is unset + write(env_abc, """ + [deps] + PkgHashCollision = "$pkg_uuid_str" + + [preferences.PkgHashCollision] + abc = "same_value" + """) + + old_proj = Base.active_project() + try + Base.set_active_project(env_xyz) + cachefile, _ = Base.compilecache(Base.PkgId(pkg_uuid, "PkgHashCollision")) + pkg_file = joinpath(load_path, "PkgHashCollision", "src", "PkgHashCollision.jl") + + @test Base.stale_cachefile(pkg_file, cachefile) !== true + Base.set_active_project(env_abc) + @test Base.stale_cachefile(pkg_file, cachefile) === true + finally + Base.set_active_project(old_proj) + end +end + +precompile_test_harness("Preferences hash collision (issue #59344), part 2") do load_path + # On Julia ≥1.13, `pref = false` and an unset preference triggered a hash collision, + # causing preference changes not to invalidate. Test that this invalidates as expected. + pkg_uuid_str = "d2e8c371-1b3f-4f5a-8c7e-9a2b1d4e6f8c" + pkg_uuid = Base.UUID(pkg_uuid_str) + + pkg_src_dir = mkpath(joinpath(load_path, "PkgFalsePrefs", "src")) + write(joinpath(pkg_src_dir, "PkgFalsePrefs.jl"), + """ + module PkgFalsePrefs + Base.record_compiletime_preference(Base.UUID("$pkg_uuid_str"), "flag") + end + """) + write(joinpath(load_path, "PkgFalsePrefs", "Project.toml"), """ + name = "PkgFalsePrefs" + uuid = "$pkg_uuid_str" + """) + + env_false_dir = mkpath(joinpath(load_path, "env_false")) + env_none_dir = mkpath(joinpath(load_path, "env_none")) + + env_false = joinpath(env_false_dir, "Project.toml") + env_none = joinpath(env_none_dir, "Project.toml") + + # Compiled with flag = false + write(env_false, """ + [deps] + PkgFalsePrefs = "$pkg_uuid_str" + + [preferences.PkgFalsePrefs] + flag = false + """) + # Preference removed entirely + write(env_none, """ + [deps] + PkgFalsePrefs = "$pkg_uuid_str" + """) + + old_proj = Base.active_project() + try + Base.set_active_project(env_false) + cachefile, _ = Base.compilecache(Base.PkgId(pkg_uuid, "PkgFalsePrefs")) + pkg_file = joinpath(load_path, "PkgFalsePrefs", "src", "PkgFalsePrefs.jl") + + @test Base.stale_cachefile(pkg_file, cachefile) !== true + Base.set_active_project(env_none) + @test Base.stale_cachefile(pkg_file, cachefile) === true + finally + Base.set_active_project(old_proj) + end +end + finish_precompile_test!() From e4103a5b506d1dae4c131c61be9e270aa8257f92 Mon Sep 17 00:00:00 2001 From: Gabriel Baraldi Date: Mon, 9 Mar 2026 11:10:49 -0300 Subject: [PATCH 31/38] LateLowerGCFrame: Fix GC rooting for stores through select/phi of allocas (#61229) Co-authored-by: Claude Opus 4.6 (cherry picked from commit 8ce965452a228c1f9c590c0e711f091a0fa0b0b4) --- src/llvm-gc-interface-passes.h | 2 +- src/llvm-late-gc-lowering.cpp | 134 +++++++++--------- test/llvmpasses/late-lower-gc-select-store.ll | 72 ++++++++++ 3 files changed, 143 insertions(+), 65 deletions(-) create mode 100644 test/llvmpasses/late-lower-gc-select-store.ll diff --git a/src/llvm-gc-interface-passes.h b/src/llvm-gc-interface-passes.h index 0d21ea0a66cd8..b41a52cef8454 100644 --- a/src/llvm-gc-interface-passes.h +++ b/src/llvm-gc-interface-passes.h @@ -351,7 +351,7 @@ struct LateLowerGCFrame: private JuliaPassContext { void NoteOperandUses(State &S, BBState &BBS, Instruction &UI); void MaybeTrackDst(State &S, MemTransferInst *MI); - void MaybeTrackStore(State &S, StoreInst *I); + bool MaybeTrackStore(State &S, StoreInst *I); State LocalScan(Function &F); void ComputeLiveness(State &S); void ComputeLiveSets(State &S); diff --git a/src/llvm-late-gc-lowering.cpp b/src/llvm-late-gc-lowering.cpp index ae1351ae41ca1..a727359003914 100644 --- a/src/llvm-late-gc-lowering.cpp +++ b/src/llvm-late-gc-lowering.cpp @@ -1148,44 +1148,44 @@ void LateLowerGCFrame::FixUpRefinements(ArrayRef PHINumbers, State &S) } } -// Look through instructions to find all possible allocas that might become the sret argument -static std::optional> FindSretAllocas(Value* SRetArg) { - SmallSetVector allocas; - if (AllocaInst *OneSRet = dyn_cast(SRetArg)) { - allocas.insert(OneSRet); // Found it directly - } else { - SmallSetVector worklist; - worklist.insert(SRetArg); +// Look through selects and phis to find all possible alloca bases of a pointer. +// Returns an empty set if a non-alloca base is encountered. +static SmallSetVector FindAllocaBases(Value *V) { + SmallSetVector allocas; + if (AllocaInst *AI = dyn_cast(V)) { + allocas.insert(AI); // Found it directly + } + else { + SmallVector worklist; + SmallPtrSet visited; + worklist.push_back(V); + visited.insert(V); while (!worklist.empty()) { - Value *V = worklist.pop_back_val(); - if (AllocaInst *Alloca = dyn_cast(V->stripInBoundsOffsets())) { + Value *W = worklist.pop_back_val(); + if (AllocaInst *Alloca = dyn_cast(W->stripInBoundsOffsets())) { allocas.insert(Alloca); // Found a candidate - } else if (PHINode *Phi = dyn_cast(V)) { + } + else if (PHINode *Phi = dyn_cast(W)) { for (Value *Incoming : Phi->incoming_values()) { - worklist.insert(Incoming); + if (visited.insert(Incoming).second) + worklist.push_back(Incoming); } - } else if (SelectInst *SI = dyn_cast(V)) { - auto TrueBranch = SI->getTrueValue(); - auto FalseBranch = SI->getFalseValue(); - if (TrueBranch && FalseBranch) { - worklist.insert(TrueBranch); - worklist.insert(FalseBranch); - } else { - llvm_dump(SI); - dbgs() << "Malformed Select\n"; - return {}; - } - } else { - llvm_dump(V); - dbgs() << "Unexpected SRet argument\n"; - return {}; + } + else if (SelectInst *SI = dyn_cast(W)) { + if (visited.insert(SI->getTrueValue()).second) + worklist.push_back(SI->getTrueValue()); + if (visited.insert(SI->getFalseValue()).second) + worklist.push_back(SI->getFalseValue()); + } + else { + allocas.clear(); + return allocas; } } } - assert(allocas.size() > 0); - assert(std::all_of(allocas.begin(), allocas.end(), [&] (AllocaInst* SRetAlloca) JL_NOTSAFEPOINT { - return (SRetAlloca->getArraySize() == allocas[0]->getArraySize() && - SRetAlloca->getAllocatedType() == allocas[0]->getAllocatedType()); + assert(std::all_of(allocas.begin(), allocas.end(), [&] (AllocaInst *AI) JL_NOTSAFEPOINT { + return (AI->getArraySize() == allocas[0]->getArraySize() && + AI->getAllocatedType() == allocas[0]->getAllocatedType()); } )); return allocas; @@ -1250,15 +1250,14 @@ State LateLowerGCFrame::LocalScan(Function &F) { auto tracked = CountTrackedPointers(ElT, true); if (tracked.count) { HasDefBefore = true; - auto allocas_opt = FindSretAllocas((CI->arg_begin()[0])->stripInBoundsOffsets()); + auto allocas = FindAllocaBases((CI->arg_begin()[0])->stripInBoundsOffsets()); // We know that with the right optimizations we can forward a sret directly from an argument // This hasn't been seen without adding IPO effects to julia functions but it's possible we need to handle that too // If they are tracked.all we can just pass through but if they have a roots bundle it's possible we need to emit some copies ¯\_(ツ)_/¯ - if (!allocas_opt.has_value()) { + if (allocas.size() == 0) { llvm_dump(&F); abort(); } - auto allocas = allocas_opt.value(); for (AllocaInst *SRet : allocas) { if (!(SRet->isStaticAlloca() && isa(ElT) && ElT->getPointerAddressSpace() == AddressSpace::Tracked)) { assert(!tracked.derived); @@ -1267,12 +1266,7 @@ State LateLowerGCFrame::LocalScan(Function &F) { } else { Value *arg1 = (CI->arg_begin()[1])->stripInBoundsOffsets(); - auto gc_allocas_opt = FindSretAllocas(arg1); - if (!gc_allocas_opt.has_value()) { - llvm_dump(&F); - abort(); - } - auto gc_allocas = gc_allocas_opt.value(); + auto gc_allocas = FindAllocaBases(arg1); if (gc_allocas.size() == 0) { llvm_dump(CI); errs() << "Expected one Alloca at least\n"; @@ -1455,7 +1449,8 @@ State LateLowerGCFrame::LocalScan(Function &F) { } } else if (StoreInst *SI = dyn_cast(&I)) { NoteOperandUses(S, BBS, I); - MaybeTrackStore(S, SI); + if (MaybeTrackStore(S, SI)) + BBS.FirstSafepointAfterFirstDef = BBS.FirstSafepoint; } else if (isa(&I)) { NoteOperandUses(S, BBS, I); } else if (auto *ASCI = dyn_cast(&I)) { @@ -1577,37 +1572,48 @@ SmallVector ExtractTrackedValues(Value *Src, Type *STy, bool isptr, I // return Ptrs.size(); //} -void LateLowerGCFrame::MaybeTrackStore(State &S, StoreInst *I) { +bool LateLowerGCFrame::MaybeTrackStore(State &S, StoreInst *I) { Value *PtrBase = I->getPointerOperand()->stripInBoundsOffsets(); auto tracked = CountTrackedPointers(I->getValueOperand()->getType()); if (!tracked.count) - return; // nothing to track is being stored - if (AllocaInst *AI = dyn_cast(PtrBase)) { + return false; // nothing to track is being stored + // Find all alloca bases, looking through selects and phis. + // LLVM's SROA/InstCombine can merge conditional alloca stores into a + // select/phi over alloca pointers (see #60985). + auto Allocas = FindAllocaBases(PtrBase); + if (Allocas.empty()) + return false; // assume it is rooted--TODO: should we be more conservative? + bool needsTrackedStore = false; + bool Tracked = false; + for (AllocaInst *AI : Allocas) { Type *STy = AI->getAllocatedType(); if (!AI->isStaticAlloca() || (isa(STy) && STy->getPointerAddressSpace() == AddressSpace::Tracked) || S.ArrayAllocas.count(AI)) - return; // already numbered this - auto tracked = CountTrackedPointers(STy); - if (tracked.count) { - assert(!tracked.derived); - if (tracked.all) { + continue; // already numbered this + auto allocaTracked = CountTrackedPointers(STy); + if (allocaTracked.count) { + assert(!allocaTracked.derived); + if (allocaTracked.all) { // track the Alloca directly - S.ArrayAllocas[AI] = tracked.count * cast(AI->getArraySize())->getZExtValue(); - return; + S.ArrayAllocas[AI] = allocaTracked.count * cast(AI->getArraySize())->getZExtValue(); + Tracked = true; + continue; } } - } - else { - return; // assume it is rooted--TODO: should we be more conservative? - } - // track the Store with a Shadow - //auto &Shadow = S.ShadowAllocas[AI]; - //if (!Shadow) - // Shadow = new AllocaInst(ArrayType::get(T_prjlvalue, tracked.count), 0, "", MI); - //AI = Shadow; - //Value *Src = I->getValueOperand(); - //unsigned count = TrackWithShadow(Src, Src->getType(), false, AI, MI, TODO which slots are we actually clobbering?); - //assert(count == tracked.count); (void)count; - S.TrackedStores.push_back(std::make_pair(I, tracked.count)); + needsTrackedStore = true; + } + if (needsTrackedStore) { + // track the Store with a Shadow + //auto &Shadow = S.ShadowAllocas[AI]; + //if (!Shadow) + // Shadow = new AllocaInst(ArrayType::get(T_prjlvalue, tracked.count), 0, "", MI); + //AI = Shadow; + //Value *Src = I->getValueOperand(); + //unsigned count = TrackWithShadow(Src, Src->getType(), false, AI, MI, TODO which slots are we actually clobbering?); + //assert(count == tracked.count); (void)count; + S.TrackedStores.push_back(std::make_pair(I, tracked.count)); + Tracked = true; + } + return Tracked; } /* diff --git a/test/llvmpasses/late-lower-gc-select-store.ll b/test/llvmpasses/late-lower-gc-select-store.ll new file mode 100644 index 0000000000000..39a82bff139a8 --- /dev/null +++ b/test/llvmpasses/late-lower-gc-select-store.ll @@ -0,0 +1,72 @@ +; This file is a part of Julia. License is MIT: https://julialang.org/license + +; RUN: opt --load-pass-plugin=libjulia-codegen%shlibext -passes='function(LateLowerGCFrame)' -S %s | FileCheck %s + +; Test that stores of GC-tracked values through select/phi of alloca pointers +; are properly rooted. This is a regression test for +; https://github.com/JuliaLang/julia/issues/60985 + +declare ptr @julia.get_pgcstack() + +declare void @safepoint() "julia.safepoint" + +; Store through a select of two allocas +define void @store_select(ptr addrspace(10) %val, i1 %cond) { + ; CHECK-LABEL: @store_select + ; CHECK: %gcframe = call ptr @julia.new_gc_frame(i32 4) + %pgcstack = call ptr @julia.get_pgcstack() + %alloca1 = alloca [2 x ptr addrspace(10)], align 8 + %alloca2 = alloca [2 x ptr addrspace(10)], align 8 + %sel = select i1 %cond, ptr %alloca1, ptr %alloca2 + store ptr addrspace(10) %val, ptr %sel, align 8 + call void @safepoint() + ; CHECK: call void @julia.pop_gc_frame(ptr %gcframe) + ret void +} + +; Store through a phi with a cycle (phi references itself via loop back-edge). +; This is a regression test for an infinite loop in FindAllocaBases. +define void @store_phi_cycle(ptr addrspace(10) %val, i1 %cond) { +top: + ; CHECK-LABEL: @store_phi_cycle + ; CHECK: %gcframe = call ptr @julia.new_gc_frame(i32 4) + %pgcstack = call ptr @julia.get_pgcstack() + %alloca1 = alloca [2 x ptr addrspace(10)], align 8 + %alloca2 = alloca [2 x ptr addrspace(10)], align 8 + br label %loop + +loop: + %phi = phi ptr [ %alloca1, %top ], [ %sel, %loop ] + %sel = select i1 %cond, ptr %phi, ptr %alloca2 + store ptr addrspace(10) %val, ptr %sel, align 8 + call void @safepoint() + br i1 %cond, label %loop, label %exit + +exit: + ; CHECK: call void @julia.pop_gc_frame(ptr %gcframe) + ret void +} + +; Store through a phi of two allocas +define void @store_phi(ptr addrspace(10) %val, i1 %cond) { +top: + ; CHECK-LABEL: @store_phi + ; CHECK: %gcframe = call ptr @julia.new_gc_frame(i32 4) + %pgcstack = call ptr @julia.get_pgcstack() + %alloca1 = alloca [2 x ptr addrspace(10)], align 8 + %alloca2 = alloca [2 x ptr addrspace(10)], align 8 + br i1 %cond, label %left, label %right + +left: + br label %merge + +right: + br label %merge + +merge: + %phi = phi ptr [ %alloca1, %left ], [ %alloca2, %right ] + store ptr addrspace(10) %val, ptr %phi, align 8 + call void @safepoint() + ; CHECK: call void @julia.pop_gc_frame(ptr %gcframe) + ret void +} From a2a0c051d6f7f907d59b7510e2b65a92cf24d7d7 Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Fri, 13 Mar 2026 20:32:47 +0100 Subject: [PATCH 32/38] precompilation: make the handling of project vs workspace more similar (#61290) (cherry picked from commit 3be10451bdec3a2b667877b32a77db695c670779) --- base/precompilation.jl | 103 +++++++++++++----- test/precompile.jl | 29 +++++ .../Workspaces/PrecompileExt/Bar/Project.toml | 3 + .../Workspaces/PrecompileExt/Bar/src/Bar.jl | 3 + .../PrecompileExt/BarEnv/Project.toml | 3 + .../Workspaces/PrecompileExt/Foo/Project.toml | 9 ++ .../PrecompileExt/Foo/ext/FooBarExt.jl | 3 + .../Workspaces/PrecompileExt/Foo/src/Foo.jl | 3 + .../PrecompileExt/FooEnv/Project.toml | 2 + .../Workspaces/PrecompileExt/Manifest.toml | 25 +++++ .../Workspaces/PrecompileExt/Project.toml | 2 + 11 files changed, 156 insertions(+), 29 deletions(-) create mode 100644 test/project/Workspaces/PrecompileExt/Bar/Project.toml create mode 100644 test/project/Workspaces/PrecompileExt/Bar/src/Bar.jl create mode 100644 test/project/Workspaces/PrecompileExt/BarEnv/Project.toml create mode 100644 test/project/Workspaces/PrecompileExt/Foo/Project.toml create mode 100644 test/project/Workspaces/PrecompileExt/Foo/ext/FooBarExt.jl create mode 100644 test/project/Workspaces/PrecompileExt/Foo/src/Foo.jl create mode 100644 test/project/Workspaces/PrecompileExt/FooEnv/Project.toml create mode 100644 test/project/Workspaces/PrecompileExt/Manifest.toml create mode 100644 test/project/Workspaces/PrecompileExt/Project.toml diff --git a/base/precompilation.jl b/base/precompilation.jl index ca1a3cb2df58d..e86c191b9281b 100644 --- a/base/precompilation.jl +++ b/base/precompilation.jl @@ -2,18 +2,19 @@ module Precompilation using Base: CoreLogging, PkgId, UUID, SHA1, parsed_toml, project_file_name_uuid, project_names, project_file_manifest_path, get_deps, preferences_names, isaccessibledir, isfile_casesensitive, - base_project, isdefined + base_project, env_project_file, isdefined # This is currently only used for pkgprecompile but the plan is to use this in code loading in the future # see the `kc/codeloading2.0` branch struct ExplicitEnv path::String - project_deps::Dict{String, UUID} # [deps] in Project.toml - project_weakdeps::Dict{String, UUID} # [weakdeps] in Project.toml - project_extras::Dict{String, UUID} # [extras] in Project.toml - project_extensions::Dict{String, Vector{UUID}} # [exts] in Project.toml - deps::Dict{UUID, Vector{UUID}} # all dependencies in Manifest.toml - weakdeps::Dict{UUID, Vector{UUID}} # all weak dependencies in Manifest.toml + project_deps::Dict{String, UUID} # [deps] in the active project's Project.toml + project_weakdeps::Dict{String, UUID} # [weakdeps] in the active project's Project.toml + project_extras::Dict{String, UUID} # [extras] in the active project's Project.toml + project_extensions::Dict{String, Vector{UUID}} # [extensions] in the active project's Project.toml + workspace_deps::Dict{String, UUID} # union of [deps] from all workspace member Project.tomls + deps::Dict{UUID, Vector{UUID}} # full dependency graph from Manifest.toml + weakdeps::Dict{UUID, Vector{UUID}} # full weak dependency graph from Manifest.toml extensions::Dict{UUID, Dict{String, Vector{UUID}}} # Lookup name for a UUID names::Dict{UUID, String} @@ -33,6 +34,7 @@ function ExplicitEnv(::Nothing, envpath::String="") Dict{String, UUID}(), # project_weakdeps Dict{String, UUID}(), # project_extras Dict{String, Vector{UUID}}(), # project_extensions + Dict{String, UUID}(), # workspace_deps Dict{UUID, Vector{UUID}}(), # deps Dict{UUID, Vector{UUID}}(), # weakdeps Dict{UUID, Dict{String, Vector{UUID}}}(), # extensions @@ -260,8 +262,38 @@ function ExplicitEnv(envpath::String) end =# + # Collect the union of [deps] from all workspace member projects. + # For non-workspace projects, this is the same as project_deps. + workspace_deps = copy(project_deps) + base = base_project(envpath) + if base !== nothing + base_d = parsed_toml(base) + # Add deps from the workspace root project + for (name, _uuid) in get(Dict{String, Any}, base_d, "deps")::Dict{String, Any} + workspace_deps[name] = UUID(_uuid::String) + end + # Add deps from each workspace member project + ws = get(base_d, "workspace", nothing)::Union{Dict{String, Any}, Nothing} + if ws !== nothing + ws_projects = get(ws, "projects", nothing)::Union{Vector{String}, Nothing, String} + if ws_projects isa Vector + ws_root = dirname(base) + for ws_proj in ws_projects + ws_proj_dir = joinpath(ws_root, ws_proj) + ws_proj_file = Base.env_project_file(ws_proj_dir) + ws_proj_file isa String || continue + ws_d = parsed_toml(ws_proj_file) + for (name, _uuid) in get(Dict{String, Any}, ws_d, "deps")::Dict{String, Any} + workspace_deps[name] = UUID(_uuid::String) + end + end + end + end + end + return ExplicitEnv(envpath, project_deps, project_weakdeps, project_extras, - project_extensions, deps_expanded, weakdeps_expanded, extensions_expanded, + project_extensions, workspace_deps, + deps_expanded, weakdeps_expanded, extensions_expanded, names, lookup_strategy, #=prefs, local_prefs=#) end @@ -575,6 +607,14 @@ function _visit_indirect_deps!(direct_deps::Dict{PkgId, Vector{PkgId}}, visited: return end +function _collect_reachable!(pkg_uuids::Set{UUID}, deps::Dict{UUID, Vector{UUID}}, uuid::UUID) + uuid in pkg_uuids && return + push!(pkg_uuids, uuid) + for dep_uuid in get(Vector{UUID}, deps, uuid) + _collect_reachable!(pkg_uuids, deps, dep_uuid) + end +end + function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, internal_call::Bool, strict::Bool, @@ -664,20 +704,30 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, return name end + # Determine which packages to consider for precompilation by walking + # transitive dependencies from the appropriate roots. + # `manifest` controls the scope: workspace_deps (all members) vs project_deps (current project). + roots = manifest ? env.workspace_deps : env.project_deps + pkg_uuids = Set{UUID}() + for (_, uuid) in roots + _collect_reachable!(pkg_uuids, env.deps, uuid) + end + triggers = Dict{Base.PkgId,Vector{Base.PkgId}}() - for (dep, deps) in env.deps + for dep in pkg_uuids + haskey(env.deps, dep) || continue pkg = Base.PkgId(dep, env.names[dep]) Base.in_sysimage(pkg) && continue - deps = [Base.PkgId(x, env.names[x]) for x in deps] + deps = [Base.PkgId(x, env.names[x]) for x in env.deps[dep]] direct_deps[pkg] = filter!(!Base.in_sysimage, deps) - for (ext_name, trigger_uuids) in env.extensions[dep] + for (ext_name, trigger_uuids) in get(Dict{String, Vector{UUID}}, env.extensions, dep) ext_uuid = Base.uuid5(pkg.uuid, ext_name) ext = Base.PkgId(ext_uuid, ext_name) triggers[ext] = Base.PkgId[pkg] # depends on parent package all_triggers_available = true for trigger_uuid in trigger_uuids trigger_name = Base.PkgId(trigger_uuid, env.names[trigger_uuid]) - if trigger_uuid in keys(env.deps) || Base.in_sysimage(trigger_name) + if trigger_uuid in pkg_uuids || Base.in_sysimage(trigger_name) push!(triggers[ext], trigger_name) else all_triggers_available = false @@ -823,14 +873,8 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, @warn excluded_circular_deps_explanation(io, ext_to_parent, circular_deps, cycles) end - # If you have a workspace and want to precompile all projects in it, look through all packages in the manifest - # instead of collecting from a project i.e. not filter out packages that are in the current project. - # i.e. Pkg sets manifest to true for workspace precompile requests - # TODO: rename `manifest`? - if !manifest - if isempty(pkg_names) - pkg_names = [pkg.name for pkg in project_deps] - end + # Filter to specific requested packages if the caller asked for a subset + if !isempty(pkg_names) keep = Set{Base.PkgId}() for dep_pkgid in keys(direct_deps) if dep_pkgid.name in pkg_names @@ -853,15 +897,16 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, end end filter!(d->in(first(d), keep), direct_deps) - if isempty(direct_deps) - if _from_loading - # if called from loading precompilation it may be a package from another environment stack so - # don't error and allow serial precompilation to try - # TODO: actually handle packages from other envs in the stack - return - else - return - end + end + + if isempty(direct_deps) + if _from_loading + # if called from loading precompilation it may be a package from another environment stack so + # don't error and allow serial precompilation to try + # TODO: actually handle packages from other envs in the stack + return + else + return end end diff --git a/test/precompile.jl b/test/precompile.jl index fb8d7948a4d6f..4ed9899e274c2 100644 --- a/test/precompile.jl +++ b/test/precompile.jl @@ -3012,4 +3012,33 @@ precompile_test_harness("Preferences hash collision (issue #59344), part 2") do end end +# Workspace sub-environment precompilation should not precompile packages from other sub-environments +@testset "workspace sub-environment precompilation scoping" begin + mkdepottempdir() do depot + workspace_path = joinpath(@__DIR__, "project", "Workspaces", "PrecompileExt") + fooenv_path = joinpath(workspace_path, "FooEnv") + + original_depot_path = copy(Base.DEPOT_PATH) + old_proj = Base.active_project() + try + push!(empty!(DEPOT_PATH), depot) + # Activate FooEnv (only depends on Foo, not Bar) + Base.set_active_project(fooenv_path) + + io = IOBuffer() + ioc = IOContext(io, :color => false) + Base.Precompilation.precompilepkgs(; io=ioc, fancyprint=false) + output = String(take!(io)) + + # Foo should be precompiled + @test occursin("Foo", output) + # Bar should NOT be precompiled (it's in another sub-environment) + @test !occursin("Bar", output) + finally + Base.set_active_project(old_proj) + append!(empty!(DEPOT_PATH), original_depot_path) + end + end +end + finish_precompile_test!() diff --git a/test/project/Workspaces/PrecompileExt/Bar/Project.toml b/test/project/Workspaces/PrecompileExt/Bar/Project.toml new file mode 100644 index 0000000000000..2cb21380e2cb9 --- /dev/null +++ b/test/project/Workspaces/PrecompileExt/Bar/Project.toml @@ -0,0 +1,3 @@ +name = "Bar" +uuid = "ab000000-0000-0000-0000-000000000002" +version = "0.1.0" diff --git a/test/project/Workspaces/PrecompileExt/Bar/src/Bar.jl b/test/project/Workspaces/PrecompileExt/Bar/src/Bar.jl new file mode 100644 index 0000000000000..39d7fb696695f --- /dev/null +++ b/test/project/Workspaces/PrecompileExt/Bar/src/Bar.jl @@ -0,0 +1,3 @@ +module Bar +greet() = "hello from Bar" +end diff --git a/test/project/Workspaces/PrecompileExt/BarEnv/Project.toml b/test/project/Workspaces/PrecompileExt/BarEnv/Project.toml new file mode 100644 index 0000000000000..ef3559abfcccc --- /dev/null +++ b/test/project/Workspaces/PrecompileExt/BarEnv/Project.toml @@ -0,0 +1,3 @@ +[deps] +Foo = "ab000000-0000-0000-0000-000000000001" +Bar = "ab000000-0000-0000-0000-000000000002" diff --git a/test/project/Workspaces/PrecompileExt/Foo/Project.toml b/test/project/Workspaces/PrecompileExt/Foo/Project.toml new file mode 100644 index 0000000000000..7a085f8e1ba01 --- /dev/null +++ b/test/project/Workspaces/PrecompileExt/Foo/Project.toml @@ -0,0 +1,9 @@ +name = "Foo" +uuid = "ab000000-0000-0000-0000-000000000001" +version = "0.1.0" + +[weakdeps] +Bar = "ab000000-0000-0000-0000-000000000002" + +[extensions] +FooBarExt = "Bar" diff --git a/test/project/Workspaces/PrecompileExt/Foo/ext/FooBarExt.jl b/test/project/Workspaces/PrecompileExt/Foo/ext/FooBarExt.jl new file mode 100644 index 0000000000000..b4d48becd44ee --- /dev/null +++ b/test/project/Workspaces/PrecompileExt/Foo/ext/FooBarExt.jl @@ -0,0 +1,3 @@ +module FooBarExt +using Foo, Bar +end diff --git a/test/project/Workspaces/PrecompileExt/Foo/src/Foo.jl b/test/project/Workspaces/PrecompileExt/Foo/src/Foo.jl new file mode 100644 index 0000000000000..b094f86815cd8 --- /dev/null +++ b/test/project/Workspaces/PrecompileExt/Foo/src/Foo.jl @@ -0,0 +1,3 @@ +module Foo +greet() = "hello from Foo" +end diff --git a/test/project/Workspaces/PrecompileExt/FooEnv/Project.toml b/test/project/Workspaces/PrecompileExt/FooEnv/Project.toml new file mode 100644 index 0000000000000..646b29605cbd7 --- /dev/null +++ b/test/project/Workspaces/PrecompileExt/FooEnv/Project.toml @@ -0,0 +1,2 @@ +[deps] +Foo = "ab000000-0000-0000-0000-000000000001" diff --git a/test/project/Workspaces/PrecompileExt/Manifest.toml b/test/project/Workspaces/PrecompileExt/Manifest.toml new file mode 100644 index 0000000000000..685fe923a8cd4 --- /dev/null +++ b/test/project/Workspaces/PrecompileExt/Manifest.toml @@ -0,0 +1,25 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.14.0-DEV" +manifest_format = "2.1" +project_hash = "491b0496f036790ab9001fcf6b38ae497256ad30" + +[[deps.Bar]] +path = "Bar" +uuid = "ab000000-0000-0000-0000-000000000002" +version = "0.1.0" + + [deps.Bar.syntax] + julia_version = "1.14.0-DEV.1886" + +[[deps.Foo]] +path = "Foo" +uuid = "ab000000-0000-0000-0000-000000000001" +version = "0.1.0" +weakdeps = ["Bar"] + + [deps.Foo.extensions] + FooBarExt = "Bar" + + [deps.Foo.syntax] + julia_version = "1.14.0-DEV.1886" diff --git a/test/project/Workspaces/PrecompileExt/Project.toml b/test/project/Workspaces/PrecompileExt/Project.toml new file mode 100644 index 0000000000000..2361f324d6430 --- /dev/null +++ b/test/project/Workspaces/PrecompileExt/Project.toml @@ -0,0 +1,2 @@ +[workspace] +projects = ["FooEnv", "BarEnv"] From 82f57d006df7f0393eaa0ecd6e822f1b3e574539 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sat, 14 Mar 2026 12:47:31 -0400 Subject: [PATCH 33/38] precompilation: improve warn_loaded messaging for transitive deps (#61293) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing warning was brittle and wasn't shown when the package with a different version loaded wasn't actually precompiled by this session. i.e. `[21216c6a] + Preferences v1.5.2 [loaded: v1.5.1]` but v1.5.2 was actually already precompiled on the system. Developed with Claude. --- Name the loaded packages and count their affected dependents in the warning shown after Pkg.precompile. This helps users understand why a subsequent `using` triggers re-precompilation: a transitive dependency (e.g. Preferences) was loaded at one version but the manifest resolved a different version, so caches built by Pkg.precompile (ignore_loaded=true) are rejected at load time (ignore_loaded=false). The message now shows which packages are loaded at a different version and how many dependents will be affected, making the restart suggestion more actionable. ``` (@v1.14) pkg> st Status `~/.julia/environments/v1.14/Project.toml` [336ed68f] CSV v0.10.16 [0ca39b1e] Chairmarks v1.3.1 [a93c6f00] DataFrames v1.8.1 [7876af07] Example v0.5.5 ⌃ [21216c6a] Preferences v1.5.1 [295af30f] Revise v3.13.2 Info Packages marked with ⌃ have new versions available and may be upgradable. julia> using Preferences (@v1.14) pkg> activate --temp Activating new project at `/var/folders/1z/jf841bdj73bdj3vk7kc7f_3w0000gn/T/jl_PgrklO` (jl_PgrklO) pkg> add CSV Resolving package versions... Updating `/private/var/folders/1z/jf841bdj73bdj3vk7kc7f_3w0000gn/T/jl_PgrklO/Project.toml` [336ed68f] + CSV v0.10.16 Updating `/private/var/folders/1z/jf841bdj73bdj3vk7kc7f_3w0000gn/T/jl_PgrklO/Manifest.toml` [336ed68f] + CSV v0.10.16 [944b1d66] + CodecZlib v0.7.8 [34da2185] + Compat v4.18.1 [9a962f9c] + DataAPI v1.16.0 [e2d170a0] + DataValueInterfaces v1.0.0 [48062228] + FilePathsBase v0.9.24 [842dd82b] + InlineStrings v1.4.5 [82899510] + IteratorInterfaceExtensions v1.0.0 [bac558e1] + OrderedCollections v1.8.1 [69de0a69] + Parsers v2.8.3 [2dfb63ee] + PooledArrays v1.4.3 [aea7be01] + PrecompileTools v1.3.3 [21216c6a] + Preferences v1.5.2 [loaded: v1.5.1] [91c51154] + SentinelArrays v1.4.9 [3783bdb8] + TableTraits v1.0.1 [bd369af6] + Tables v1.12.1 [3bb67fe8] + TranscodingStreams v0.11.3 [ea10d353] + WeakRefStrings v1.4.2 [76eceee3] + WorkerUtilities v1.6.1 [ade2ca70] + Dates v1.11.0 [9fa8497b] + Future v1.11.0 [8f399da3] + Libdl v1.11.0 [a63ad114] + Mmap v1.11.0 [de0858da] + Printf v1.11.0 [9a3f8284] + Random v1.11.0 [ea8e919c] + SHA v1.0.0 [fa267f1f] + TOML v1.0.3 [cf7118a7] + UUIDs v1.11.0 [4ec0a83e] + Unicode v1.11.0 [83775a58] + Zlib_jll v1.3.2+0 Precompiling CSV finished. 1 dependency successfully precompiled in 9 seconds. 29 already precompiled. 1 dependency precompiled but a different version is currently loaded (Preferences). Restart julia to access the new version. Otherwise, 5 dependents of this package may trigger further precompilation to work with the unexpected version. ``` --------- Co-authored-by: Claude (cherry picked from commit 2998caa867a77f4c194f6aa93c937a5eb0f05f43) --- base/precompilation.jl | 50 ++++++++++++++++++++++--- test/precompile.jl | 85 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 5 deletions(-) diff --git a/base/precompilation.jl b/base/precompilation.jl index e86c191b9281b..e9004e4a0a1ab 100644 --- a/base/precompilation.jl +++ b/base/precompilation.jl @@ -937,6 +937,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, n_done = Ref(0) n_already_precomp = Ref(0) n_loaded = Ref(0) + loaded_pkgs = Base.PkgId[] interrupted = Ref(false) function handle_interrupt(err, in_printloop::Bool) @@ -1230,7 +1231,10 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, stale_cache[stale_cache_key] = false end end - loaded && (n_loaded[] += 1) + if loaded + n_loaded[] += 1 + @lock print_lock push!(loaded_pkgs, pkg) + end catch err close(std_pipe.in) # close pipe to end the std output monitor wait(t_monitor) @@ -1248,7 +1252,13 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, Base.release(parallel_limiter) end else - is_stale || (n_already_precomp[] += 1) + if !is_stale + n_already_precomp[] += 1 + if loaded + n_loaded[] += 1 + @lock print_lock push!(loaded_pkgs, pkg) + end + end end n_done[] += 1 notify(was_processed[pkg_config]) @@ -1319,14 +1329,44 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, local plural1 = length(configs) > 1 ? "dependency configurations" : n_loaded[] == 1 ? "dependency" : "dependencies" local plural2 = n_loaded[] == 1 ? "a different version is" : "different versions are" local plural3 = n_loaded[] == 1 ? "" : "s" - local plural4 = n_loaded[] == 1 ? "this package" : "these packages" + local loaded_names = join(sort!([full_name(ext_to_parent, p) for p in loaded_pkgs]), ", ", " and ") + # compute how many precompiled packages transitively depend on the loaded packages + local n_affected = 0 + local loaded_set = Set{Base.PkgId}(loaded_pkgs) + let reverse_deps = Dict{Base.PkgId, Vector{Base.PkgId}}() + for (p, deps) in direct_deps + for d in deps + push!(get!(Vector{Base.PkgId}, reverse_deps, d), p) + end + end + affected = Set{Base.PkgId}() + frontier = Base.PkgId[p for p in loaded_set] + while !isempty(frontier) + p = pop!(frontier) + for rdep in get(reverse_deps, p, Base.PkgId[]) + if rdep ∉ affected && rdep ∉ loaded_set + push!(affected, rdep) + push!(frontier, rdep) + end + end + end + n_affected = length(affected) + end print(iostr, "\n ", color_string(string(n_loaded[]), Base.warn_color()), " $(plural1) precompiled but ", color_string("$(plural2) currently loaded", Base.warn_color()), - ". Restart julia to access the new version$(plural3). \ - Otherwise, loading dependents of $(plural4) may trigger further precompilation to work with the unexpected version$(plural3)." + " (", loaded_names, ")", + ". Restart julia to access the new version$(plural3)." ) + if n_affected > 0 + local affected_plural = length(configs) > 1 ? "dependency configurations" : n_affected == 1 ? "dependent" : "dependents" + print(iostr, + " Otherwise, $(n_affected) $(affected_plural) of ", + n_loaded[] == 1 ? "this package" : "these packages", + " may trigger further precompilation to work with the unexpected version$(plural3)." + ) + end end if !isempty(precomperr_deps) pluralpc = length(configs) > 1 ? "dependency configurations" : precomperr_deps == 1 ? "dependency" : "dependencies" diff --git a/test/precompile.jl b/test/precompile.jl index 4ed9899e274c2..d0051a18937a2 100644 --- a/test/precompile.jl +++ b/test/precompile.jl @@ -3041,4 +3041,89 @@ end end end +# Test that warn_loaded names loaded packages and counts affected dependents +@testset "warn_loaded names packages and counts dependents" begin + mkdepottempdir() do depot; mktempdir() do dir + # Create LoadedDep — will be loaded before precompilepkgs runs + loaded_dep_path = joinpath(dir, "dev", "LoadedDep") + mkpath(joinpath(loaded_dep_path, "src")) + write(joinpath(loaded_dep_path, "Project.toml"), + """ + name = "LoadedDep" + uuid = "a1a1a1a1-0000-0000-0000-000000000001" + version = "0.1.0" + """) + write(joinpath(loaded_dep_path, "src", "LoadedDep.jl"), + """ + module LoadedDep + end + """) + + # Create DepUser — depends on LoadedDep + depuser_path = joinpath(dir, "dev", "DepUser") + mkpath(joinpath(depuser_path, "src")) + write(joinpath(depuser_path, "Project.toml"), + """ + name = "DepUser" + uuid = "b2b2b2b2-0000-0000-0000-000000000002" + version = "0.1.0" + + [deps] + LoadedDep = "a1a1a1a1-0000-0000-0000-000000000001" + """) + write(joinpath(depuser_path, "src", "DepUser.jl"), + """ + module DepUser + import LoadedDep + end + """) + + # Create project environment referencing both packages + project_path = joinpath(dir, "project") + mkpath(project_path) + write(joinpath(project_path, "Project.toml"), + """ + [deps] + DepUser = "b2b2b2b2-0000-0000-0000-000000000002" + LoadedDep = "a1a1a1a1-0000-0000-0000-000000000001" + """) + write(joinpath(project_path, "Manifest.toml"), + """ + manifest_format = "2.0" + + [[deps.DepUser]] + deps = ["LoadedDep"] + path = "../dev/DepUser/" + uuid = "b2b2b2b2-0000-0000-0000-000000000002" + version = "0.1.0" + + [[deps.LoadedDep]] + path = "../dev/LoadedDep/" + uuid = "a1a1a1a1-0000-0000-0000-000000000001" + version = "0.1.0" + """) + + script = """ + using LoadedDep + Base.Precompilation.precompilepkgs(; fancyprint=false, warn_loaded=true) + """ + + cmd = addenv(`$(Base.julia_cmd()) --startup-file=no --project=$(project_path) -e $script`, + "JULIA_DEPOT_PATH" => depot) + + out = Base.PipeEndpoint() + log = @async read(out, String) + try + proc = run(pipeline(cmd, stdout=out, stderr=out)) + @test success(proc) + catch + @show fetch(log) + rethrow() + end + output = fetch(log) + @test occursin("currently loaded", output) + @test occursin("LoadedDep", output) + end end +end + finish_precompile_test!() From 1b5cd3c71c5a3bae098a2b9e49be41affa4ce659 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 15 Mar 2026 19:01:15 -0400 Subject: [PATCH 34/38] fixup "precompilation: improve warn_loaded messaging for transitive deps (#61293)" (#61331) (cherry picked from commit a03bd9e921a1d845c7af5f715d6afb0bdbf6f026) --- base/precompilation.jl | 19 ++++-- test/precompile.jl | 133 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 141 insertions(+), 11 deletions(-) diff --git a/base/precompilation.jl b/base/precompilation.jl index e9004e4a0a1ab..6650034344088 100644 --- a/base/precompilation.jl +++ b/base/precompilation.jl @@ -1229,12 +1229,16 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, build_id, _ = Base.parse_cache_buildid(cachefile) stale_cache_key = (pkg, build_id, sourcepath, cachefile, ignore_loaded, cacheflags)::StaleCacheKey stale_cache[stale_cache_key] = false + if loaded && Base.module_build_id(Base.loaded_modules[pkg]) != build_id + n_loaded[] += 1 + @lock print_lock push!(loaded_pkgs, pkg) + end + elseif loaded + # another process compiled this package; conservatively warn + n_loaded[] += 1 + @lock print_lock push!(loaded_pkgs, pkg) end end - if loaded - n_loaded[] += 1 - @lock print_lock push!(loaded_pkgs, pkg) - end catch err close(std_pipe.in) # close pipe to end the std output monitor wait(t_monitor) @@ -1255,8 +1259,11 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}}, if !is_stale n_already_precomp[] += 1 if loaded - n_loaded[] += 1 - @lock print_lock push!(loaded_pkgs, pkg) + fresh_build_id, _ = Base.parse_cache_buildid(freshpath) + if Base.module_build_id(Base.loaded_modules[pkg]) != fresh_build_id + n_loaded[] += 1 + @lock print_lock push!(loaded_pkgs, pkg) + end end end end diff --git a/test/precompile.jl b/test/precompile.jl index d0051a18937a2..5f000d218f31f 100644 --- a/test/precompile.jl +++ b/test/precompile.jl @@ -3044,7 +3044,130 @@ end # Test that warn_loaded names loaded packages and counts affected dependents @testset "warn_loaded names packages and counts dependents" begin mkdepottempdir() do depot; mktempdir() do dir - # Create LoadedDep — will be loaded before precompilepkgs runs + # Create LoadedDep old source — loaded at the start of the script + loaded_dep_old_path = joinpath(dir, "dev", "LoadedDepOld") + mkpath(joinpath(loaded_dep_old_path, "src")) + write(joinpath(loaded_dep_old_path, "Project.toml"), + """ + name = "LoadedDep" + uuid = "a1a1a1a1-0000-0000-0000-000000000001" + version = "0.1.0" + """) + write(joinpath(loaded_dep_old_path, "src", "LoadedDep.jl"), + """ + module LoadedDep + const _v = 1 # old version marker forces a different build_id + end + """) + + # Create LoadedDep new source — resolved by the environment after switching + loaded_dep_new_path = joinpath(dir, "dev", "LoadedDepNew") + mkpath(joinpath(loaded_dep_new_path, "src")) + write(joinpath(loaded_dep_new_path, "Project.toml"), + """ + name = "LoadedDep" + uuid = "a1a1a1a1-0000-0000-0000-000000000001" + version = "0.2.0" + """) + write(joinpath(loaded_dep_new_path, "src", "LoadedDep.jl"), + """ + module LoadedDep + const _v = 2 # new version marker forces a different build_id + end + """) + + # Create DepUser — depends on LoadedDep + depuser_path = joinpath(dir, "dev", "DepUser") + mkpath(joinpath(depuser_path, "src")) + write(joinpath(depuser_path, "Project.toml"), + """ + name = "DepUser" + uuid = "b2b2b2b2-0000-0000-0000-000000000002" + version = "0.1.0" + + [deps] + LoadedDep = "a1a1a1a1-0000-0000-0000-000000000001" + """) + write(joinpath(depuser_path, "src", "DepUser.jl"), + """ + module DepUser + import LoadedDep + end + """) + + # old_project: used to load the old version of LoadedDep + old_project_path = joinpath(dir, "old_project") + mkpath(old_project_path) + write(joinpath(old_project_path, "Project.toml"), + """ + [deps] + LoadedDep = "a1a1a1a1-0000-0000-0000-000000000001" + """) + write(joinpath(old_project_path, "Manifest.toml"), + """ + manifest_format = "2.0" + + [[deps.LoadedDep]] + path = "../dev/LoadedDepOld/" + uuid = "a1a1a1a1-0000-0000-0000-000000000001" + version = "0.1.0" + """) + + # new_project: resolved after loading old version; has LoadedDep new source + new_project_path = joinpath(dir, "new_project") + mkpath(new_project_path) + write(joinpath(new_project_path, "Project.toml"), + """ + [deps] + DepUser = "b2b2b2b2-0000-0000-0000-000000000002" + LoadedDep = "a1a1a1a1-0000-0000-0000-000000000001" + """) + write(joinpath(new_project_path, "Manifest.toml"), + """ + manifest_format = "2.0" + + [[deps.DepUser]] + deps = ["LoadedDep"] + path = "../dev/DepUser/" + uuid = "b2b2b2b2-0000-0000-0000-000000000002" + version = "0.1.0" + + [[deps.LoadedDep]] + path = "../dev/LoadedDepNew/" + uuid = "a1a1a1a1-0000-0000-0000-000000000001" + version = "0.2.0" + """) + + # Load the old LoadedDep first, then switch to the new project whose manifest + # resolves a different source for LoadedDep — this causes a build_id mismatch + # and should trigger the warn_loaded warning. + script = """ + using LoadedDep + Base.set_active_project($(repr(new_project_path))) + Base.Precompilation.precompilepkgs(; fancyprint=false, warn_loaded=true) + """ + + cmd = addenv(`$(Base.julia_cmd()) --startup-file=no --project=$(old_project_path) -e $script`, + "JULIA_DEPOT_PATH" => depot) + + out = Base.PipeEndpoint() + log = @async read(out, String) + try + proc = run(pipeline(cmd, stdout=out, stderr=out)) + @test success(proc) + catch + @show fetch(log) + rethrow() + end + output = fetch(log) + @test occursin("currently loaded", output) + @test occursin("LoadedDep", output) + end end +end + +# Test that warn_loaded does not warn when the loaded dep is already at the correct version +@testset "warn_loaded does not warn when loaded dep matches env version" begin + mkdepottempdir() do depot; mktempdir() do dir loaded_dep_path = joinpath(dir, "dev", "LoadedDep") mkpath(joinpath(loaded_dep_path, "src")) write(joinpath(loaded_dep_path, "Project.toml"), @@ -3059,7 +3182,6 @@ end end """) - # Create DepUser — depends on LoadedDep depuser_path = joinpath(dir, "dev", "DepUser") mkpath(joinpath(depuser_path, "src")) write(joinpath(depuser_path, "Project.toml"), @@ -3078,7 +3200,6 @@ end end """) - # Create project environment referencing both packages project_path = joinpath(dir, "project") mkpath(project_path) write(joinpath(project_path, "Project.toml"), @@ -3087,6 +3208,7 @@ end DepUser = "b2b2b2b2-0000-0000-0000-000000000002" LoadedDep = "a1a1a1a1-0000-0000-0000-000000000001" """) + # Manifest points to the same source as what gets loaded — versions match write(joinpath(project_path, "Manifest.toml"), """ manifest_format = "2.0" @@ -3103,6 +3225,8 @@ end version = "0.1.0" """) + # Load LoadedDep first (caching it), then run precompilepkgs — the env resolves the + # same version that is loaded, so no version-mismatch warning should appear. script = """ using LoadedDep Base.Precompilation.precompilepkgs(; fancyprint=false, warn_loaded=true) @@ -3121,8 +3245,7 @@ end rethrow() end output = fetch(log) - @test occursin("currently loaded", output) - @test occursin("LoadedDep", output) + @test !occursin("currently loaded", output) end end end From 921785e228f2e55c265f7e831fa4dcf497bc7640 Mon Sep 17 00:00:00 2001 From: Gabriel Baraldi Date: Fri, 10 Apr 2026 11:58:03 -0300 Subject: [PATCH 35/38] codegen: emit bounds checks as cold branches instead of ANDs (#61535) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For reference the big win here is that this allows for loop unswitch to trigger since it doesn't really understand our `and` sequence - Replace AND-combined bounds check conditions with separate branches annotated with branch weight metadata (2000:1) to mark the error path as cold in LLVM codegen (`emit_bounds_check`, `emit_memoryref_direct`, `emit_memoryref`) - Change `checkbounds_indices` to use `&&` instead of `&` so each dimension check becomes its own branch rather than being combined into a single AND chain - Change `throw_boundserror` to accept varargs for multi-index cases, so the index tuple is only constructed inside the `@noinline` error path rather than on the hot path For a 2D array access `A[i, j]`, the net effect is: - **Before**: one AND-combined branch + `alloca` for index tuple in the entry block - **After**: two separate cold-annotated branches per dimension, zero tuple allocation on the hot path Before: ```llvm %"new::Tuple" = alloca [2 x i64], align 8 store i64 %"i::Int64", ptr %"new::Tuple", align 8 store i64 %"j::Int64", ptr %0, align 8 ... %.not = icmp ult i64 %0, %"x::Array.size.0" %1 = icmp ult i64 %2, %"x::Array.size.1" %combined = and i1 %.not, %1 br i1 %combined, label %pass, label %fail ``` After: ```llvm %.not = icmp ult i64 %0, %"x::Array.size.sroa.0.0.copyload" br i1 %.not, label %L27, label %L29 L27: %.not10.not = icmp ult i64 %1, %"x::Array.size.sroa.2.0.copyload" br i1 %.not10.not, label %L32, label %L29 L29: call void @j_throw_boundserror_1(ptr %"x::Array", i64 %"i::Int64", i64 %"j::Int64") unreachable ``` - [x] Full `make -j8` build succeeds including sysimage precompilation - [x] Verified LLVM IR output shows separate branches per dimension for `A[i, j]` - [x] Verified tuple `alloca` is eliminated from the hot path - [x] Verified single-index `A[i]` still works correctly (no tuple wrapping) - [x] Verified `BoundsError` messages are unchanged at runtime - [ ] CI tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) (cherry picked from commit 268f85e80b6b836bd6ba82e72a7b94adb6ff766b) --- Compiler/test/inline.jl | 8 +++++++- base/abstractarray.jl | 2 +- base/broadcast.jl | 12 ++++++++---- base/essentials.jl | 5 +++++ base/genericmemory.jl | 2 +- base/multidimensional.jl | 10 +++++----- src/cgutils.cpp | 32 +++++++++++++++++++++++--------- 7 files changed, 50 insertions(+), 21 deletions(-) diff --git a/Compiler/test/inline.jl b/Compiler/test/inline.jl index bde710a3ef9c0..1d71aa5638e8c 100644 --- a/Compiler/test/inline.jl +++ b/Compiler/test/inline.jl @@ -151,8 +151,14 @@ end end (src, _) = only(code_typed(sum27403, Tuple{Vector{Int}})) + is_bounds_throw_invoke_target(@nospecialize(callee)) = + callee === Base.throw_boundserror || + callee == Core.GlobalRef(Base, :throw_boundserror) || + callee == Core.GlobalRef(Base, :_throw_boundserror_indices) || + (callee isa Core.MethodInstance && (callee.def.def.name === :throw_boundserror || + callee.def.def.name === :_throw_boundserror_indices)) @test !any(src.code) do x - x isa Expr && x.head === :invoke && !(x.args[2] in (Core.GlobalRef(Base, :throw_boundserror), Base.throw_boundserror)) + x isa Expr && x.head === :invoke && !is_bounds_throw_invoke_target(x.args[2]) end end diff --git a/base/abstractarray.jl b/base/abstractarray.jl index 685b965a9bbea..1ff11a345e82d 100644 --- a/base/abstractarray.jl +++ b/base/abstractarray.jl @@ -725,7 +725,7 @@ See also [`checkbounds`](@ref). """ function checkbounds_indices(::Type{Bool}, inds::Tuple, I::Tuple{Any, Vararg}) @inline - return checkindex(Bool, get(inds, 1, OneTo(1)), I[1])::Bool & + return checkindex(Bool, get(inds, 1, OneTo(1)), I[1])::Bool && checkbounds_indices(Bool, safe_tail(inds), tail(I)) end diff --git a/base/broadcast.jl b/base/broadcast.jl index bbdb296515b66..0b185408d6d32 100644 --- a/base/broadcast.jl +++ b/base/broadcast.jl @@ -630,11 +630,15 @@ to_index(::Tuple{}) = CartesianIndex() to_index(Is::Tuple{Any}) = Is[1] to_index(Is::Tuple) = CartesianIndex(Is) -@inline Base.checkbounds(bc::Broadcasted, I::CartesianIndex) = - Base.checkbounds_indices(Bool, axes(bc), (I,)) || Base.throw_boundserror(bc, (I,)) +@inline function Base.checkbounds(bc::Broadcasted, I::CartesianIndex) + Base.checkbounds_indices(Bool, axes(bc), (I,)) || Base.throw_boundserror(bc, I) + nothing +end -@inline Base.checkbounds(bc::Broadcasted, I::Integer) = - Base.checkindex(Bool, eachindex(IndexLinear(), bc), I) || Base.throw_boundserror(bc, (I,)) +@inline function Base.checkbounds(bc::Broadcasted, I::Integer) + Base.checkindex(Bool, eachindex(IndexLinear(), bc), I) || Base.throw_boundserror(bc, I) + nothing +end """ diff --git a/base/essentials.jl b/base/essentials.jl index 2f6faadea08a6..0729fb6ab1418 100644 --- a/base/essentials.jl +++ b/base/essentials.jl @@ -10,7 +10,11 @@ const Bottom = Union{} size(a::Array) = getfield(a, :size) length(t::AbstractArray) = (@inline; prod(size(t))) size(a::GenericMemory) = (getfield(a, :length),) +throw_boundserror(A) = (@noinline; throw(BoundsError(A, ()))) throw_boundserror(A, I) = (@noinline; throw(BoundsError(A, I))) +throw_boundserror(A, i1, i2, I...) = (@noinline; throw(BoundsError(A, (i1, i2, I...)))) +_throw_boundserror_indices(A) = (@noinline; throw(BoundsError(A, ()))) +_throw_boundserror_indices(A, i1, I...) = (@noinline; throw(BoundsError(A, (i1, I...)))) # multidimensional getindex will be defined later on @@ -383,6 +387,7 @@ end function checkbounds(A::Union{Array, GenericMemory}, i::Int) @inline checkbounds(Bool, A, i) || throw_boundserror(A, (i,)) + nothing end default_access_order(::GenericMemory{:not_atomic}) = :not_atomic diff --git a/base/genericmemory.jl b/base/genericmemory.jl index b34095ea37d48..5a1e4d8d22d1e 100644 --- a/base/genericmemory.jl +++ b/base/genericmemory.jl @@ -272,7 +272,7 @@ end function setindex!(A::Memory{T}, x, i1::Int, i2::Int, I::Int...) where {T} @inline - @boundscheck (i2 == 1 && all(==(1), I)) || throw_boundserror(A, (i1, i2, I...)) + @boundscheck (i2 == 1 && all(==(1), I)) || throw_boundserror(A, i1, i2, I...) setindex!(A, x, i1) end diff --git a/base/multidimensional.jl b/base/multidimensional.jl index 7c93047ea521a..4c699a26750fe 100644 --- a/base/multidimensional.jl +++ b/base/multidimensional.jl @@ -759,12 +759,12 @@ end # Here we try to consume N of the indices (if there are that many available) @inline function checkbounds_indices(::Type{Bool}, inds::Tuple, I::Tuple{CartesianIndex,Vararg}) inds1, rest = IteratorsMD.split(inds, Val(length(I[1]))) - checkindex(Bool, inds1, I[1]) & checkbounds_indices(Bool, rest, tail(I)) + checkindex(Bool, inds1, I[1]) && checkbounds_indices(Bool, rest, tail(I)) end @inline checkindex(::Type{Bool}, inds::Tuple, I::CartesianIndex) = checkbounds_indices(Bool, inds, I.I) @inline checkindex(::Type{Bool}, inds::Tuple, i::AbstractRange{<:CartesianIndex}) = - isempty(i) | (checkindex(Bool, inds, first(i)) & checkindex(Bool, inds, last(i))) + isempty(i) | (checkindex(Bool, inds, first(i)) && checkindex(Bool, inds, last(i))) # Indexing into Array with mixtures of Integers and CartesianIndices is # extremely performance-sensitive. While the abstract fallbacks support this, @@ -781,7 +781,7 @@ end # Here we try to consume N of the indices (if there are that many available) @inline function checkbounds_indices(::Type{Bool}, inds::Tuple, I::Tuple{AbstractArray{CartesianIndex{N}},Vararg}) where N inds1, rest = IteratorsMD.split(inds, Val(N)) - checkindex(Bool, inds1, I[1]) & checkbounds_indices(Bool, rest, tail(I)) + checkindex(Bool, inds1, I[1]) && checkbounds_indices(Bool, rest, tail(I)) end @inline checkindex(::Type{Bool}, inds::Tuple, I::CartesianIndices) = checkbounds_indices(Bool, inds, I.indices) @@ -907,12 +907,12 @@ checkbounds(::Type{Bool}, A::AbstractArray, i::AbstractVector{Bool}) = checkindex(Bool, eachindex(IndexLinear(), A), i) @inline function checkbounds_indices(::Type{Bool}, inds::Tuple, I::Tuple{AbstractArray{Bool},Vararg}) inds1, rest = IteratorsMD.split(inds, Val(ndims(I[1]))) - checkindex(Bool, inds1, I[1]) & checkbounds_indices(Bool, rest, tail(I)) + checkindex(Bool, inds1, I[1]) && checkbounds_indices(Bool, rest, tail(I)) end checkindex(::Type{Bool}, inds::AbstractUnitRange, I::AbstractVector{Bool}) = axes1(I) == inds checkindex(::Type{Bool}, inds::AbstractUnitRange, I::AbstractRange{Bool}) = axes1(I) == inds checkindex(::Type{Bool}, inds::Tuple, I::AbstractArray{Bool}) = _check_boolean_axes(inds, axes(I)) -_check_boolean_axes(inds::Tuple, axes::Tuple) = (inds[1] == axes[1]) & _check_boolean_axes(tail(inds), tail(axes)) +_check_boolean_axes(inds::Tuple, axes::Tuple) = (inds[1] == axes[1]) && _check_boolean_axes(tail(inds), tail(axes)) _check_boolean_axes(::Tuple{}, axes::Tuple) = all(==(OneTo(1)), axes) ensure_indexable(I::Tuple{}) = () diff --git a/src/cgutils.cpp b/src/cgutils.cpp index d99be5ec08315..6cc232197c1d9 100644 --- a/src/cgutils.cpp +++ b/src/cgutils.cpp @@ -2087,7 +2087,9 @@ static Value *emit_bounds_check(jl_codectx_t &ctx, const jl_cgval_t &ainfo, jl_v setName(ctx.emission_context, ok, "boundscheck"); BasicBlock *failBB = BasicBlock::Create(ctx.builder.getContext(), "fail", ctx.f); BasicBlock *passBB = BasicBlock::Create(ctx.builder.getContext(), "pass"); - ctx.builder.CreateCondBr(ok, passBB, failBB); + MDBuilder MDB(ctx.builder.getContext()); + SmallVector Weights{2000, 1}; + ctx.builder.CreateCondBr(ok, passBB, failBB, MDB.createBranchWeights(Weights)); ctx.builder.SetInsertPoint(failBB); if (!ty) { // jl_value_t** tuple (e.g. the vararg) ctx.builder.CreateCall(prepare_call(jlvboundserror_func), { ainfo.V, len, i }); @@ -4736,7 +4738,9 @@ static jl_cgval_t emit_memoryref_direct(jl_codectx_t &ctx, const jl_cgval_t &mem Value *mlen = emit_genericmemorylen(ctx, boxmem, typ); Value *inbound = ctx.builder.CreateICmpULT(idx0, mlen); setName(ctx.emission_context, inbound, "memoryref_isinbounds"); - ctx.builder.CreateCondBr(inbound, endBB, failBB); + MDBuilder MDB(ctx.builder.getContext()); + SmallVector Weights{2000, 1}; + ctx.builder.CreateCondBr(inbound, endBB, failBB, MDB.createBranchWeights(Weights)); failBB->insertInto(ctx.f); ctx.builder.SetInsertPoint(failBB); ctx.builder.CreateCall(prepare_call(jlboundserror_func), @@ -4822,7 +4826,9 @@ static jl_cgval_t emit_memoryref(jl_codectx_t &ctx, const jl_cgval_t &ref, jl_cg Value *mlen = emit_genericmemorylen(ctx, mem, ref.typ); Value *inbound = ctx.builder.CreateICmpULT(newdata, mlen); setName(ctx.emission_context, offset, "memoryref_isinbounds"); - ctx.builder.CreateCondBr(inbound, endBB, failBB); + MDBuilder MDB(ctx.builder.getContext()); + SmallVector Weights{2000, 1}; + ctx.builder.CreateCondBr(inbound, endBB, failBB, MDB.createBranchWeights(Weights)); failBB->insertInto(ctx.f); ctx.builder.SetInsertPoint(failBB); ctx.builder.CreateCall(prepare_call(jlboundserror_func), @@ -4867,26 +4873,34 @@ static jl_cgval_t emit_memoryref(jl_codectx_t &ctx, const jl_cgval_t &ref, jl_cg endBB = BasicBlock::Create(ctx.builder.getContext(), "idxend"); Value *mlen = emit_genericmemorylen(ctx, mem, ref.typ); Value *mptr = emit_genericmemoryptr(ctx, mem, layout, 0); + MDBuilder MDB(ctx.builder.getContext()); + SmallVector Weights{2000, 1}; #if 0 Value *mend = mptr; Value *blen = ctx.builder.CreateMul(mlen, elsz, "", true, true); mend = ctx.builder.CreateInBoundsGEP(getInt8Ty(ctx.builder.getContext()), mptr, blen); + Value *notovflw = ctx.builder.CreateNot(ovflw); + BasicBlock *boundscheckBB = BasicBlock::Create(ctx.builder.getContext(), "boundscheck"); + ctx.builder.CreateCondBr(notovflw, boundscheckBB, failBB, MDB.createBranchWeights(Weights)); + boundscheckBB->insertInto(ctx.f); + ctx.builder.SetInsertPoint(boundscheckBB); Value *inbound = ctx.builder.CreateAnd( ctx.builder.CreateICmpULE(mptr, newdata), ctx.builder.CreateICmpULT(newdata, mend)); - inbound = ctx.builder.CreateAnd( - ctx.builder.CreateNot(ovflw), - inbound); #elif 1 Value *bidx0 = ctx.builder.CreateSub( ctx.builder.CreatePtrToInt(newdata, ctx.types().T_size), ctx.builder.CreatePtrToInt(mptr, ctx.types().T_size)); Value *blen = ctx.builder.CreateMul(mlen, elsz, "", true, true); setName(ctx.emission_context, blen, "memoryref_bytelen"); + Value *notovflw = ctx.builder.CreateNot(ovflw); + setName(ctx.emission_context, notovflw, "memoryref_notovflw"); + BasicBlock *boundscheckBB = BasicBlock::Create(ctx.builder.getContext(), "boundscheck"); + ctx.builder.CreateCondBr(notovflw, boundscheckBB, failBB, MDB.createBranchWeights(Weights)); + boundscheckBB->insertInto(ctx.f); + ctx.builder.SetInsertPoint(boundscheckBB); Value *inbound = ctx.builder.CreateICmpULT(bidx0, blen); setName(ctx.emission_context, inbound, "memoryref_isinbounds"); - inbound = ctx.builder.CreateAnd(ctx.builder.CreateNot(ovflw), inbound); - setName(ctx.emission_context, inbound, "memoryref_isinbounds¬ovflw"); #else Value *idx0; // (newdata - mptr) / elsz idx0 = ctx.builder.CreateSub( @@ -4895,7 +4909,7 @@ static jl_cgval_t emit_memoryref(jl_codectx_t &ctx, const jl_cgval_t &ref, jl_cg idx0 = ctx.builder.CreateExactUDiv(idx0, elsz); Value *inbound = ctx.builder.CreateICmpULT(idx0, mlen); #endif - ctx.builder.CreateCondBr(inbound, endBB, failBB); + ctx.builder.CreateCondBr(inbound, endBB, failBB, MDB.createBranchWeights(Weights)); failBB->insertInto(ctx.f); ctx.builder.SetInsertPoint(failBB); ctx.builder.CreateCall(prepare_call(jlboundserror_func), From d7e8801015c82399e98b84fb6a28b6835dd2bf44 Mon Sep 17 00:00:00 2001 From: James Wrigley Date: Tue, 14 Apr 2026 13:04:21 +0200 Subject: [PATCH 36/38] Convert to Float64 in TOML's printvalue() to avoid invalidations (#61509) (cherry picked from commit 3371d044edd9bd395fc0abc69e4bfdcd7d9cdf4a) --- base/toml/printer.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/base/toml/printer.jl b/base/toml/printer.jl index aaef4768be347..2a39fc76c382c 100644 --- a/base/toml/printer.jl +++ b/base/toml/printer.jl @@ -128,12 +128,15 @@ function printvalue(f::Function, io::IO, value::Integer, sorted::Bool) end function printvalue(f::Function, io::IO, value::AbstractFloat, sorted::Bool) + # The early conversion here avoids invalidations from isnan/isinf + value = Float64(value) + if isnan(value) Base.print(io, "nan") elseif isinf(value) Base.print(io, value > 0 ? "+inf" : "-inf") else - Base.print(io, Float64(value)) # TOML specifies IEEE 754 binary64 for float + Base.print(io, value) # TOML specifies IEEE 754 binary64 for float end end From 32f1b68db9cad79cb8ec86f416235fd9d1fe6280 Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Wed, 15 Apr 2026 08:32:54 +0200 Subject: [PATCH 37/38] reduce probability of spurious error when outputs are interleaved --- stdlib/REPL/src/precompile.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/REPL/src/precompile.jl b/stdlib/REPL/src/precompile.jl index e0290cdecd680..4b095c8d5c049 100644 --- a/stdlib/REPL/src/precompile.jl +++ b/stdlib/REPL/src/precompile.jl @@ -17,7 +17,7 @@ end function repl_workload() # these are intentionally triggered allowed_errors = [ - "BoundsError: attempt to access 0-element Vector{Any} at index [1]", + "BoundsError: attempt to access 0-element", "MethodError: no method matching f(::$Int, ::$Int)", "Padding of type", # reinterpret docstring has ERROR examples ] From b4510855fb13b01ce74292c6b1d6b3f74c0f7b14 Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Wed, 15 Apr 2026 08:50:17 +0200 Subject: [PATCH 38/38] fixup! allow finding stdlibs that are normal packages in the manifest but are now stdlibs (#60104) --- test/loading.jl | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test/loading.jl b/test/loading.jl index 14f74a7ffd871..40200f036975b 100644 --- a/test/loading.jl +++ b/test/loading.jl @@ -1762,15 +1762,6 @@ end "JULIA_DEPOT_PATH" => string(depot * Base.Filesystem.pathsep(), s), )) end - mktempdir() do depot - # This manifest has a LibGit2 entry that has a LibGit2_jll with a git-tree-hash1 - # which simulates an old manifest where LibGit2_jll was not a stdlib - badmanifest_test_dir2 = joinpath(@__DIR__, "project", "deps", "BadStdlibDeps2") - @test success(addenv( - `$(Base.julia_cmd()) --project=$badmanifest_test_dir2 --startup-file=no -e 'using LibGit2'`, - "JULIA_DEPOT_PATH" => depot * Base.Filesystem.pathsep(), - )) - end end @testset "code coverage disabled during precompilation" begin