From f8086062906d80ef56c0dcd595a13438ef028293 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki <40514306+aviatesk@users.noreply.github.com> Date: Wed, 18 Sep 2024 00:41:52 +0900 Subject: [PATCH 1/4] use `inferencebarrier` instead of `invokelatest` for 1-arg `@assert` (#55783) This version would be better as per this comment: I confirmed this still allows us to avoid invalidations reported at JuliaLang/julia#55583. --- base/error.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/base/error.jl b/base/error.jl index ee533cee0b57d1..c49ede624607d1 100644 --- a/base/error.jl +++ b/base/error.jl @@ -232,12 +232,14 @@ macro assert(ex, msgs...) msg = msg # pass-through elseif !isempty(msgs) && (isa(msg, Expr) || isa(msg, Symbol)) # message is an expression needing evaluating - msg = :(Main.Base.invokelatest(Main.Base.string, $(esc(msg)))) + # N.B. To reduce the risk of invalidation caused by the complex callstack involved + # with `string`, use `inferencebarrier` here to hide this `string` from the compiler. + msg = :(Main.Base.inferencebarrier(Main.Base.string)($(esc(msg)))) elseif isdefined(Main, :Base) && isdefined(Main.Base, :string) && applicable(Main.Base.string, msg) msg = Main.Base.string(msg) else # string() might not be defined during bootstrap - msg = :(Main.Base.invokelatest(_assert_tostring, $(Expr(:quote,msg)))) + msg = :(Main.Base.inferencebarrier(_assert_tostring)($(Expr(:quote,msg)))) end return :($(esc(ex)) ? $(nothing) : throw(AssertionError($msg))) end From 61c044ca4fe35ed357c77fc50d9a8cf70f6724b0 Mon Sep 17 00:00:00 2001 From: Gabriel Baraldi Date: Tue, 17 Sep 2024 13:18:44 -0300 Subject: [PATCH 2/4] Inline statically known method errors. (#54972) This replaces the `Expr(:call, ...)` with a call of a new builtin `Core.throw_methoderror` This is useful because it makes very clear if something is a static method error or a plain dynamic dispatch that always errors. Tools such as AllocCheck or juliac can notice that this is not a genuine dynamic dispatch, and prevent it from becoming a false positive compile-time error. Dependent on https://github.com/JuliaLang/julia/pull/55705 --------- Co-authored-by: Cody Tapscott --- base/compiler/abstractinterpretation.jl | 41 ++++++++------- base/compiler/ssair/inlining.jl | 52 +++++++++++-------- base/compiler/stmtinfo.jl | 17 ++++-- base/compiler/tfuncs.jl | 7 ++- base/compiler/types.jl | 6 +++ test/compiler/AbstractInterpreter.jl | 9 ++++ .../compiler/EscapeAnalysis/EscapeAnalysis.jl | 4 +- test/compiler/inline.jl | 39 +++++++++----- test/threads_exec.jl | 1 + 9 files changed, 111 insertions(+), 65 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index bb5f2dd1ad180a..f126389c42d2d3 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -209,8 +209,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), rettype = exctype = Any all_effects = Effects() else - if (matches isa MethodMatches ? (!matches.fullmatch || any_ambig(matches)) : - (!all(matches.fullmatches) || any_ambig(matches))) + if !fully_covering(matches) || any_ambig(matches) # Account for the fact that we may encounter a MethodError with a non-covered or ambiguous signature. all_effects = Effects(all_effects; nothrow=false) exctype = exctype ⊔ₚ MethodError @@ -275,21 +274,23 @@ struct MethodMatches applicable::Vector{Any} info::MethodMatchInfo valid_worlds::WorldRange - mt::MethodTable - fullmatch::Bool end -any_ambig(info::MethodMatchInfo) = info.results.ambig +any_ambig(result::MethodLookupResult) = result.ambig +any_ambig(info::MethodMatchInfo) = any_ambig(info.results) any_ambig(m::MethodMatches) = any_ambig(m.info) +fully_covering(info::MethodMatchInfo) = info.fullmatch +fully_covering(m::MethodMatches) = fully_covering(m.info) struct UnionSplitMethodMatches applicable::Vector{Any} applicable_argtypes::Vector{Vector{Any}} info::UnionSplitInfo valid_worlds::WorldRange - mts::Vector{MethodTable} - fullmatches::Vector{Bool} end -any_ambig(m::UnionSplitMethodMatches) = any(any_ambig, m.info.matches) +any_ambig(info::UnionSplitInfo) = any(any_ambig, info.matches) +any_ambig(m::UnionSplitMethodMatches) = any_ambig(m.info) +fully_covering(info::UnionSplitInfo) = all(info.fullmatches) +fully_covering(m::UnionSplitMethodMatches) = fully_covering(m.info) function find_method_matches(interp::AbstractInterpreter, argtypes::Vector{Any}, @nospecialize(atype); max_union_splitting::Int = InferenceParams(interp).max_union_splitting, @@ -307,7 +308,7 @@ is_union_split_eligible(𝕃::AbstractLattice, argtypes::Vector{Any}, max_union_ function find_union_split_method_matches(interp::AbstractInterpreter, argtypes::Vector{Any}, @nospecialize(atype), max_methods::Int) split_argtypes = switchtupleunion(typeinf_lattice(interp), argtypes) - infos = MethodMatchInfo[] + infos = MethodLookupResult[] applicable = Any[] applicable_argtypes = Vector{Any}[] # arrays like `argtypes`, including constants, for each match valid_worlds = WorldRange() @@ -323,29 +324,29 @@ function find_union_split_method_matches(interp::AbstractInterpreter, argtypes:: if matches === nothing return FailedMethodMatch("For one of the union split cases, too many methods matched") end - push!(infos, MethodMatchInfo(matches)) + push!(infos, matches) for m in matches push!(applicable, m) push!(applicable_argtypes, arg_n) end valid_worlds = intersect(valid_worlds, matches.valid_worlds) thisfullmatch = any(match::MethodMatch->match.fully_covers, matches) - found = false + mt_found = false for (i, mt′) in enumerate(mts) if mt′ === mt fullmatches[i] &= thisfullmatch - found = true + mt_found = true break end end - if !found + if !mt_found push!(mts, mt) push!(fullmatches, thisfullmatch) end end - info = UnionSplitInfo(infos) + info = UnionSplitInfo(infos, mts, fullmatches) return UnionSplitMethodMatches( - applicable, applicable_argtypes, info, valid_worlds, mts, fullmatches) + applicable, applicable_argtypes, info, valid_worlds) end function find_simple_method_matches(interp::AbstractInterpreter, @nospecialize(atype), max_methods::Int) @@ -360,10 +361,9 @@ function find_simple_method_matches(interp::AbstractInterpreter, @nospecialize(a # (assume this will always be true, so we don't compute / update valid age in this case) return FailedMethodMatch("Too many methods matched") end - info = MethodMatchInfo(matches) fullmatch = any(match::MethodMatch->match.fully_covers, matches) - return MethodMatches( - matches.matches, info, matches.valid_worlds, mt, fullmatch) + info = MethodMatchInfo(matches, mt, fullmatch) + return MethodMatches(matches.matches, info, matches.valid_worlds) end """ @@ -584,9 +584,10 @@ function add_call_backedges!(interp::AbstractInterpreter, @nospecialize(rettype) # also need an edge to the method table in case something gets # added that did not intersect with any existing method if isa(matches, MethodMatches) - matches.fullmatch || add_mt_backedge!(sv, matches.mt, atype) + fully_covering(matches) || add_mt_backedge!(sv, matches.info.mt, atype) else - for (thisfullmatch, mt) in zip(matches.fullmatches, matches.mts) + matches::UnionSplitMethodMatches + for (thisfullmatch, mt) in zip(matches.info.fullmatches, matches.info.mts) thisfullmatch || add_mt_backedge!(sv, mt, atype) end end diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index 70318b9e1a9797..727e015b670625 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -50,12 +50,13 @@ struct InliningCase end struct UnionSplit - fully_covered::Bool + handled_all_cases::Bool # All possible dispatches are included in the cases + fully_covered::Bool # All handled cases are fully covering atype::DataType cases::Vector{InliningCase} bbs::Vector{Int} - UnionSplit(fully_covered::Bool, atype::DataType, cases::Vector{InliningCase}) = - new(fully_covered, atype, cases, Int[]) + UnionSplit(handled_all_cases::Bool, fully_covered::Bool, atype::DataType, cases::Vector{InliningCase}) = + new(handled_all_cases, fully_covered, atype, cases, Int[]) end struct InliningEdgeTracker @@ -215,7 +216,7 @@ end function cfg_inline_unionsplit!(ir::IRCode, idx::Int, union_split::UnionSplit, state::CFGInliningState, params::OptimizationParams) - (; fully_covered, #=atype,=# cases, bbs) = union_split + (; handled_all_cases, fully_covered, #=atype,=# cases, bbs) = union_split inline_into_block!(state, block_for_inst(ir, idx)) from_bbs = Int[] delete!(state.split_targets, length(state.new_cfg_blocks)) @@ -235,7 +236,7 @@ function cfg_inline_unionsplit!(ir::IRCode, idx::Int, union_split::UnionSplit, end end push!(from_bbs, length(state.new_cfg_blocks)) - if !(i == length(cases) && fully_covered) + if !(i == length(cases) && (handled_all_cases && fully_covered)) # This block will have the next condition or the final else case push!(state.new_cfg_blocks, BasicBlock(StmtRange(idx, idx))) push!(state.new_cfg_blocks[cond_bb].succs, length(state.new_cfg_blocks)) @@ -244,7 +245,10 @@ function cfg_inline_unionsplit!(ir::IRCode, idx::Int, union_split::UnionSplit, end end # The edge from the fallback block. - fully_covered || push!(from_bbs, length(state.new_cfg_blocks)) + # NOTE This edge is only required for `!handled_all_cases` and not `!fully_covered`, + # since in the latter case we inline `Core.throw_methoderror` into the fallback + # block, which is must-throw, making the subsequent code path unreachable. + !handled_all_cases && push!(from_bbs, length(state.new_cfg_blocks)) # This block will be the block everyone returns to push!(state.new_cfg_blocks, BasicBlock(StmtRange(idx, idx), from_bbs, orig_succs)) join_bb = length(state.new_cfg_blocks) @@ -523,7 +527,7 @@ assuming their order stays the same post-discovery in `ml_matches`. function ir_inline_unionsplit!(compact::IncrementalCompact, idx::Int, argexprs::Vector{Any}, union_split::UnionSplit, boundscheck::Symbol, todo_bbs::Vector{Tuple{Int,Int}}, interp::AbstractInterpreter) - (; fully_covered, atype, cases, bbs) = union_split + (; handled_all_cases, fully_covered, atype, cases, bbs) = union_split stmt, typ, line = compact.result[idx][:stmt], compact.result[idx][:type], compact.result[idx][:line] join_bb = bbs[end] pn = PhiNode() @@ -538,7 +542,7 @@ function ir_inline_unionsplit!(compact::IncrementalCompact, idx::Int, argexprs:: cond = true nparams = fieldcount(atype) @assert nparams == fieldcount(mtype) - if !(i == ncases && fully_covered) + if !(i == ncases && fully_covered && handled_all_cases) for i = 1:nparams aft, mft = fieldtype(atype, i), fieldtype(mtype, i) # If this is always true, we don't need to check for it @@ -597,14 +601,18 @@ function ir_inline_unionsplit!(compact::IncrementalCompact, idx::Int, argexprs:: end bb += 1 # We're now in the fall through block, decide what to do - if !fully_covered + if !handled_all_cases ssa = insert_node_here!(compact, NewInstruction(stmt, typ, line)) push!(pn.edges, bb) push!(pn.values, ssa) insert_node_here!(compact, NewInstruction(GotoNode(join_bb), Any, line)) finish_current_bb!(compact, 0) + elseif !fully_covered + insert_node_here!(compact, NewInstruction(Expr(:call, GlobalRef(Core, :throw_methoderror), argexprs...), Union{}, line)) + insert_node_here!(compact, NewInstruction(ReturnNode(), Union{}, line)) + finish_current_bb!(compact, 0) + ncases == 0 && return insert_node_here!(compact, NewInstruction(nothing, Any, line)) end - # We're now in the join block. return insert_node_here!(compact, NewInstruction(pn, typ, line)) end @@ -1348,10 +1356,6 @@ function compute_inlining_cases(@nospecialize(info::CallInfo), flag::UInt32, sig # Too many applicable methods # Or there is a (partial?) ambiguity return nothing - elseif length(meth) == 0 - # No applicable methods; try next union split - handled_all_cases = false - continue end local split_fully_covered = false for (j, match) in enumerate(meth) @@ -1392,12 +1396,16 @@ function compute_inlining_cases(@nospecialize(info::CallInfo), flag::UInt32, sig handled_all_cases &= handle_any_const_result!(cases, result, match, argtypes, info, flag, state; allow_typevars=true) end + if !fully_covered + atype = argtypes_to_type(sig.argtypes) + # We will emit an inline MethodError so we need a backedge to the MethodTable + add_uncovered_edges!(state.edges, info, atype) + end elseif !isempty(cases) # if we've not seen all candidates, union split is valid only for dispatch tuples filter!(case::InliningCase->isdispatchtuple(case.sig), cases) end - - return cases, (handled_all_cases & fully_covered), joint_effects + return cases, handled_all_cases, fully_covered, joint_effects end function handle_call!(todo::Vector{Pair{Int,Any}}, @@ -1405,9 +1413,9 @@ function handle_call!(todo::Vector{Pair{Int,Any}}, state::InliningState) cases = compute_inlining_cases(info, flag, sig, state) cases === nothing && return nothing - cases, all_covered, joint_effects = cases + cases, handled_all_cases, fully_covered, joint_effects = cases atype = argtypes_to_type(sig.argtypes) - handle_cases!(todo, ir, idx, stmt, atype, cases, all_covered, joint_effects) + handle_cases!(todo, ir, idx, stmt, atype, cases, handled_all_cases, fully_covered, joint_effects) end function handle_match!(cases::Vector{InliningCase}, @@ -1496,19 +1504,19 @@ function concrete_result_item(result::ConcreteResult, @nospecialize(info::CallIn end function handle_cases!(todo::Vector{Pair{Int,Any}}, ir::IRCode, idx::Int, stmt::Expr, - @nospecialize(atype), cases::Vector{InliningCase}, all_covered::Bool, + @nospecialize(atype), cases::Vector{InliningCase}, handled_all_cases::Bool, fully_covered::Bool, joint_effects::Effects) # If we only have one case and that case is fully covered, we may either # be able to do the inlining now (for constant cases), or push it directly # onto the todo list - if all_covered && length(cases) == 1 + if fully_covered && handled_all_cases && length(cases) == 1 handle_single_case!(todo, ir, idx, stmt, cases[1].item) - elseif length(cases) > 0 + elseif length(cases) > 0 || handled_all_cases isa(atype, DataType) || return nothing for case in cases isa(case.sig, DataType) || return nothing end - push!(todo, idx=>UnionSplit(all_covered, atype, cases)) + push!(todo, idx=>UnionSplit(handled_all_cases, fully_covered, atype, cases)) else add_flag!(ir[SSAValue(idx)], flags_for_effects(joint_effects)) end diff --git a/base/compiler/stmtinfo.jl b/base/compiler/stmtinfo.jl index 69d2ac7ae45a09..33fca90b6261e5 100644 --- a/base/compiler/stmtinfo.jl +++ b/base/compiler/stmtinfo.jl @@ -33,10 +33,13 @@ not a call to a generic function. """ struct MethodMatchInfo <: CallInfo results::MethodLookupResult + mt::MethodTable + fullmatch::Bool end nsplit_impl(info::MethodMatchInfo) = 1 getsplit_impl(info::MethodMatchInfo, idx::Int) = (@assert idx == 1; info.results) getresult_impl(::MethodMatchInfo, ::Int) = nothing +add_uncovered_edges_impl(edges::Vector{Any}, info::MethodMatchInfo, @nospecialize(atype)) = (!info.fullmatch && push!(edges, info.mt, atype); ) """ info::UnionSplitInfo <: CallInfo @@ -48,20 +51,27 @@ each partition (`info.matches::Vector{MethodMatchInfo}`). This info is illegal on any statement that is not a call to a generic function. """ struct UnionSplitInfo <: CallInfo - matches::Vector{MethodMatchInfo} + matches::Vector{MethodLookupResult} + mts::Vector{MethodTable} + fullmatches::Vector{Bool} end nmatches(info::MethodMatchInfo) = length(info.results) function nmatches(info::UnionSplitInfo) n = 0 for mminfo in info.matches - n += nmatches(mminfo) + n += length(mminfo) end return n end nsplit_impl(info::UnionSplitInfo) = length(info.matches) -getsplit_impl(info::UnionSplitInfo, idx::Int) = getsplit_impl(info.matches[idx], 1) +getsplit_impl(info::UnionSplitInfo, idx::Int) = info.matches[idx] getresult_impl(::UnionSplitInfo, ::Int) = nothing +function add_uncovered_edges_impl(edges::Vector{Any}, info::UnionSplitInfo, @nospecialize(atype)) + for (mt, fullmatch) in zip(info.mts, info.fullmatches) + !fullmatch && push!(edges, mt, atype) + end +end abstract type ConstResult end @@ -105,6 +115,7 @@ end nsplit_impl(info::ConstCallInfo) = nsplit(info.call) getsplit_impl(info::ConstCallInfo, idx::Int) = getsplit(info.call, idx) getresult_impl(info::ConstCallInfo, idx::Int) = info.results[idx] +add_uncovered_edges_impl(edges::Vector{Any}, info::ConstCallInfo, @nospecialize(atype)) = add_uncovered_edges!(edges, info.call, atype) """ info::MethodResultPure <: CallInfo diff --git a/base/compiler/tfuncs.jl b/base/compiler/tfuncs.jl index 64a93bd07c2fa5..6bb73ded8660de 100644 --- a/base/compiler/tfuncs.jl +++ b/base/compiler/tfuncs.jl @@ -2983,9 +2983,9 @@ function abstract_applicable(interp::AbstractInterpreter, argtypes::Vector{Any}, # also need an edge to the method table in case something gets # added that did not intersect with any existing method if isa(matches, MethodMatches) - matches.fullmatch || add_mt_backedge!(sv, matches.mt, atype) + fully_covering(matches) || add_mt_backedge!(sv, matches.info.mt, atype) else - for (thisfullmatch, mt) in zip(matches.fullmatches, matches.mts) + for (thisfullmatch, mt) in zip(matches.info.fullmatches, matches.info.mts) thisfullmatch || add_mt_backedge!(sv, mt, atype) end end @@ -3001,8 +3001,7 @@ function abstract_applicable(interp::AbstractInterpreter, argtypes::Vector{Any}, add_backedge!(sv, edge) end - if isa(matches, MethodMatches) ? (!matches.fullmatch || any_ambig(matches)) : - (!all(matches.fullmatches) || any_ambig(matches)) + if !fully_covering(matches) || any_ambig(matches) # Account for the fact that we may encounter a MethodError with a non-covered or ambiguous signature. rt = Bool end diff --git a/base/compiler/types.jl b/base/compiler/types.jl index f315b7968fd9b8..015b1dbc00a6f9 100644 --- a/base/compiler/types.jl +++ b/base/compiler/types.jl @@ -450,10 +450,16 @@ abstract type CallInfo end nsplit(info::CallInfo) = nsplit_impl(info)::Union{Nothing,Int} getsplit(info::CallInfo, idx::Int) = getsplit_impl(info, idx)::MethodLookupResult +add_uncovered_edges!(edges::Vector{Any}, info::CallInfo, @nospecialize(atype)) = add_uncovered_edges_impl(edges, info, atype) + getresult(info::CallInfo, idx::Int) = getresult_impl(info, idx) +# must implement `nsplit`, `getsplit`, and `add_uncovered_edges!` to opt in to inlining nsplit_impl(::CallInfo) = nothing getsplit_impl(::CallInfo, ::Int) = error("unexpected call into `getsplit`") +add_uncovered_edges_impl(edges::Vector{Any}, info::CallInfo, @nospecialize(atype)) = error("unexpected call into `add_uncovered_edges!`") + +# must implement `getresult` to opt in to extended lattice return information getresult_impl(::CallInfo, ::Int) = nothing @specialize diff --git a/test/compiler/AbstractInterpreter.jl b/test/compiler/AbstractInterpreter.jl index d95354cefa80c7..e92b67f9809422 100644 --- a/test/compiler/AbstractInterpreter.jl +++ b/test/compiler/AbstractInterpreter.jl @@ -409,6 +409,7 @@ end CC.nsplit_impl(info::NoinlineCallInfo) = CC.nsplit(info.info) CC.getsplit_impl(info::NoinlineCallInfo, idx::Int) = CC.getsplit(info.info, idx) CC.getresult_impl(info::NoinlineCallInfo, idx::Int) = CC.getresult(info.info, idx) +CC.add_uncovered_edges_impl(edges::Vector{Any}, info::NoinlineCallInfo, @nospecialize(atype)) = CC.add_uncovered_edges!(edges, info.info, atype) function CC.abstract_call(interp::NoinlineInterpreter, arginfo::CC.ArgInfo, si::CC.StmtInfo, sv::CC.InferenceState, max_methods::Int) @@ -431,6 +432,8 @@ end @inline function inlined_usually(x, y, z) return x * y + z end +foo_split(x::Float64) = 1 +foo_split(x::Int) = 2 # check if the inlining algorithm works as expected let src = code_typed1((Float64,Float64,Float64)) do x, y, z @@ -444,6 +447,7 @@ let NoinlineModule = Module() main_func(x, y, z) = inlined_usually(x, y, z) @eval NoinlineModule noinline_func(x, y, z) = $inlined_usually(x, y, z) @eval OtherModule other_func(x, y, z) = $inlined_usually(x, y, z) + @eval NoinlineModule bar_split_error() = $foo_split(Core.compilerbarrier(:type, nothing)) interp = NoinlineInterpreter(Set((NoinlineModule,))) @@ -473,6 +477,11 @@ let NoinlineModule = Module() @test count(isinvoke(:inlined_usually), src.code) == 0 @test count(iscall((src, inlined_usually)), src.code) == 0 end + + let src = code_typed1(NoinlineModule.bar_split_error) + @test count(iscall((src, foo_split)), src.code) == 0 + @test count(iscall((src, Core.throw_methoderror)), src.code) > 0 + end end # Make sure that Core.Compiler has enough NamedTuple infrastructure diff --git a/test/compiler/EscapeAnalysis/EscapeAnalysis.jl b/test/compiler/EscapeAnalysis/EscapeAnalysis.jl index 31c21f72280140..8c3e0658182084 100644 --- a/test/compiler/EscapeAnalysis/EscapeAnalysis.jl +++ b/test/compiler/EscapeAnalysis/EscapeAnalysis.jl @@ -2240,13 +2240,13 @@ end # accounts for ThrownEscape via potential MethodError # no method error -@noinline identity_if_string(x::SafeRef) = (println("preventing inlining"); nothing) +@noinline identity_if_string(x::SafeRef{<:AbstractString}) = (println("preventing inlining"); nothing) let result = code_escapes((SafeRef{String},)) do x identity_if_string(x) end @test has_no_escape(ignore_argescape(result.state[Argument(2)])) end -let result = code_escapes((Union{SafeRef{String},Nothing},)) do x +let result = code_escapes((SafeRef,)) do x identity_if_string(x) end i = only(findall(iscall((result.ir, identity_if_string)), result.ir.stmts.stmt)) diff --git a/test/compiler/inline.jl b/test/compiler/inline.jl index a8b5fd66dcd0d0..80c8ddbb08c69a 100644 --- a/test/compiler/inline.jl +++ b/test/compiler/inline.jl @@ -876,7 +876,7 @@ let src = code_typed1((Any,)) do x abstract_unionsplit_fallback(x) end @test count(isinvoke(:abstract_unionsplit_fallback), src.code) == 2 - @test count(iscall((src, abstract_unionsplit_fallback)), src.code) == 1 # fallback dispatch + @test count(iscall((src, Core.throw_methoderror)), src.code) == 1 # fallback method error end let src = code_typed1((Union{Type,Number},)) do x abstract_unionsplit_fallback(x) @@ -912,7 +912,7 @@ let src = code_typed1((Any,)) do x @test count(iscall((src, typeof)), src.code) == 2 @test count(isinvoke(:println), src.code) == 0 @test count(iscall((src, println)), src.code) == 0 - @test count(iscall((src, abstract_unionsplit_fallback)), src.code) == 1 # fallback dispatch + @test count(iscall((src, Core.throw_methoderror)), src.code) == 1 # fallback method error end let src = code_typed1((Union{Type,Number},)) do x abstract_unionsplit_fallback(false, x) @@ -960,8 +960,8 @@ let # aggressive inlining of single, abstract method match end |> only |> first # both callsites should be inlined @test count(isinvoke(:has_free_typevars), src.code) == 2 - # `isGoodType(y::Any)` isn't fully covered, thus a runtime type check and fallback dynamic dispatch should be inserted - @test count(iscall((src,isGoodType)), src.code) == 1 + # `isGoodType(y::Any)` isn't fully covered, so the fallback is a method error + @test count(iscall((src, Core.throw_methoderror)), src.code) == 1 # fallback method error end @inline isGoodType2(cnd, @nospecialize x::Type) = @@ -973,8 +973,8 @@ let # aggressive inlining of single, abstract method match (with constant-prop'e # both callsite should be inlined with constant-prop'ed result @test count(isinvoke(:isType), src.code) == 2 @test count(isinvoke(:has_free_typevars), src.code) == 0 - # `isGoodType(y::Any)` isn't fully covered, thus a runtime type check and fallback dynamic dispatch should be inserted - @test count(iscall((src,isGoodType2)), src.code) == 1 + # `isGoodType(y::Any)` isn't fully covered, thus a MethodError gets inserted + @test count(iscall((src, Core.throw_methoderror)), src.code) == 1 # fallback method error end @noinline function checkBadType!(@nospecialize x::Type) @@ -989,8 +989,8 @@ let # aggressive static dispatch of single, abstract method match end |> only |> first # both callsites should be resolved statically @test count(isinvoke(:checkBadType!), src.code) == 2 - # `checkBadType!(y::Any)` isn't fully covered, thus a runtime type check and fallback dynamic dispatch should be inserted - @test count(iscall((src,checkBadType!)), src.code) == 1 + # `checkBadType!(y::Any)` isn't fully covered, thus a MethodError gets inserted + @test count(iscall((src, Core.throw_methoderror)), src.code) == 1 # fallback method error end @testset "late_inline_special_case!" begin @@ -2004,7 +2004,7 @@ f48397(::Tuple{String,String}) = :ok let src = code_typed1((Union{Bool,Tuple{String,Any}},)) do x f48397(x) end - @test any(iscall((src, f48397)), src.code) + @test any(iscall((src, Core.throw_methoderror)), src.code) # fallback method error) end g48397::Union{Bool,Tuple{String,Any}} = ("48397", 48397) let res = @test_throws MethodError let @@ -2175,11 +2175,6 @@ let src = code_typed1() do @test count(isinvoke(:iterate), src.code) == 0 end -# JuliaLang/julia#53062: proper `joint_effects` for call with empty method matches -let ir = first(only(Base.code_ircode(setproperty!, (Base.RefValue{Int},Symbol,Base.RefValue{Int})))) - i = findfirst(iscall((ir, convert)), ir.stmts.stmt)::Int - @test iszero(ir.stmts.flag[i] & Core.Compiler.IR_FLAG_NOTHROW) -end function issue53062(cond) x = Ref{Int}(0) if cond @@ -2214,3 +2209,19 @@ let ir = Base.code_ircode((Issue52644,); optimize_until="Inlining") do t @test irfunc(Issue52644(Tuple{})) === :DataType @test_throws MethodError irfunc(Issue52644(Tuple{<:Integer})) end + +foo_split(x::Float64) = 1 +foo_split(x::Int) = 2 +bar_inline_error() = foo_split(nothing) +bar_split_error() = foo_split(Core.compilerbarrier(:type,nothing)) + +let src = code_typed1(bar_inline_error, Tuple{}) + # Should inline method errors + @test count(iscall((src, foo_split)), src.code) == 0 + @test count(iscall((src, Core.throw_methoderror)), src.code) > 0 +end +let src = code_typed1(bar_split_error, Tuple{}) + # Should inline method errors + @test count(iscall((src, foo_split)), src.code) == 0 + @test count(iscall((src, Core.throw_methoderror)), src.code) > 0 +end diff --git a/test/threads_exec.jl b/test/threads_exec.jl index 595f8991d58d7a..ac54dd009390c4 100644 --- a/test/threads_exec.jl +++ b/test/threads_exec.jl @@ -1235,6 +1235,7 @@ end @testset "throw=true" begin tasks, event = create_tasks() push!(tasks, Threads.@spawn error("Error")) + wait(tasks[end]; throw=false) @test_throws CompositeException begin waitany(convert_tasks(tasks_type, tasks); throw=true) From 48ddd2dc2859f41fc1a82876cc5654fb31d7b2ba Mon Sep 17 00:00:00 2001 From: norci Date: Wed, 18 Sep 2024 01:14:35 +0800 Subject: [PATCH 3/4] Fix shell `cd` error when working dir has been deleted (#41244) root cause: if current dir has been deleted, then pwd() will throw an IOError: pwd(): no such file or directory (ENOENT) --------- Co-authored-by: Ian Butterworth --- base/client.jl | 13 +++++++++---- test/file.jl | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/base/client.jl b/base/client.jl index 2ca88c40aeb7e9..a04556507d5dcb 100644 --- a/base/client.jl +++ b/base/client.jl @@ -41,7 +41,6 @@ function repl_cmd(cmd, out) if isempty(cmd.exec) throw(ArgumentError("no cmd to execute")) elseif cmd.exec[1] == "cd" - new_oldpwd = pwd() if length(cmd.exec) > 2 throw(ArgumentError("cd method only takes one argument")) elseif length(cmd.exec) == 2 @@ -52,11 +51,17 @@ function repl_cmd(cmd, out) end dir = ENV["OLDPWD"] end - cd(dir) else - cd() + dir = homedir() end - ENV["OLDPWD"] = new_oldpwd + try + ENV["OLDPWD"] = pwd() + catch ex + ex isa IOError || rethrow() + # if current dir has been deleted, then pwd() will throw an IOError: pwd(): no such file or directory (ENOENT) + delete!(ENV, "OLDPWD") + end + cd(dir) println(out, pwd()) else @static if !Sys.iswindows() diff --git a/test/file.jl b/test/file.jl index de6d488056a029..de258c92e02bcb 100644 --- a/test/file.jl +++ b/test/file.jl @@ -1908,6 +1908,26 @@ end end end +@testset "pwd tests" begin + mktempdir() do dir + cd(dir) do + withenv("OLDPWD" => nothing) do + io = IOBuffer() + Base.repl_cmd(@cmd("cd"), io) + Base.repl_cmd(@cmd("cd -"), io) + @test realpath(pwd()) == realpath(dir) + if !Sys.iswindows() + # Delete the working directory and check we can cd out of it + # Cannot delete the working directory on Windows + rm(dir) + @test_throws Base._UVError("pwd()", Base.UV_ENOENT) pwd() + Base.repl_cmd(@cmd("cd \\~"), io) + end + end + end + end +end + @testset "readdir tests" begin ≛(a, b) = sort(a) == sort(b) mktempdir() do dir From 00739173311983479fee3f5f826e4b763872860a Mon Sep 17 00:00:00 2001 From: Jameson Nash Date: Tue, 17 Sep 2024 19:47:48 -0400 Subject: [PATCH 4/4] codegen: fix bits compare for UnionAll (#55770) Fixes #55768 in two parts: one is making the type computation in emit_bits_compare agree with the parent function and two is not using the optimized egal code for UnionAll kinds, which is different from how the egal code itself works for kinds. --- src/codegen.cpp | 21 ++++++++++++--------- test/compiler/codegen.jl | 5 +++++ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/codegen.cpp b/src/codegen.cpp index 73a5f844b31dae..6ae4f56a53ee27 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -3484,25 +3484,26 @@ static size_t emit_masked_bits_compare(callback &emit_desc, jl_datatype_t *aty, static Value *emit_bits_compare(jl_codectx_t &ctx, jl_cgval_t arg1, jl_cgval_t arg2) { ++EmittedBitsCompares; + jl_value_t *argty = (arg1.constant ? jl_typeof(arg1.constant) : arg1.typ); bool isboxed; Type *at = julia_type_to_llvm(ctx, arg1.typ, &isboxed); - assert(jl_is_datatype(arg1.typ) && arg1.typ == arg2.typ && !isboxed); + assert(jl_is_datatype(arg1.typ) && arg1.typ == (arg2.constant ? jl_typeof(arg2.constant) : arg2.typ) && !isboxed); if (type_is_ghost(at)) return ConstantInt::get(getInt1Ty(ctx.builder.getContext()), 1); if (at->isIntegerTy() || at->isPointerTy() || at->isFloatingPointTy()) { Type *at_int = INTT(at, ctx.emission_context.DL); - Value *varg1 = emit_unbox(ctx, at_int, arg1, arg1.typ); - Value *varg2 = emit_unbox(ctx, at_int, arg2, arg2.typ); + Value *varg1 = emit_unbox(ctx, at_int, arg1, argty); + Value *varg2 = emit_unbox(ctx, at_int, arg2, argty); return ctx.builder.CreateICmpEQ(varg1, varg2); } if (at->isVectorTy()) { - jl_svec_t *types = ((jl_datatype_t*)arg1.typ)->types; + jl_svec_t *types = ((jl_datatype_t*)argty)->types; Value *answer = ConstantInt::get(getInt1Ty(ctx.builder.getContext()), 1); - Value *varg1 = emit_unbox(ctx, at, arg1, arg1.typ); - Value *varg2 = emit_unbox(ctx, at, arg2, arg2.typ); + Value *varg1 = emit_unbox(ctx, at, arg1, argty); + Value *varg2 = emit_unbox(ctx, at, arg2, argty); for (size_t i = 0, l = jl_svec_len(types); i < l; i++) { jl_value_t *fldty = jl_svecref(types, i); Value *subAns, *fld1, *fld2; @@ -3517,7 +3518,7 @@ static Value *emit_bits_compare(jl_codectx_t &ctx, jl_cgval_t arg1, jl_cgval_t a } if (at->isAggregateType()) { // Struct or Array - jl_datatype_t *sty = (jl_datatype_t*)arg1.typ; + jl_datatype_t *sty = (jl_datatype_t*)argty; size_t sz = jl_datatype_size(sty); if (sz > 512 && !sty->layout->flags.haspadding && sty->layout->flags.isbitsegal) { Value *varg1 = arg1.ispointer() ? data_pointer(ctx, arg1) : @@ -3721,8 +3722,10 @@ static Value *emit_f_is(jl_codectx_t &ctx, const jl_cgval_t &arg1, const jl_cgva if (jl_type_intersection(rt1, rt2) == (jl_value_t*)jl_bottom_type) // types are disjoint (exhaustive test) return ConstantInt::get(getInt1Ty(ctx.builder.getContext()), 0); - bool justbits1 = jl_is_concrete_immutable(rt1); - bool justbits2 = jl_is_concrete_immutable(rt2); + // can compare any concrete immutable by bits, except for UnionAll + // which has a special non-bits based egal + bool justbits1 = jl_is_concrete_immutable(rt1) && !jl_is_kind(rt1); + bool justbits2 = jl_is_concrete_immutable(rt2) && !jl_is_kind(rt2); if (justbits1 || justbits2) { // whether this type is unique'd by value return emit_nullcheck_guard2(ctx, nullcheck1, nullcheck2, [&] () -> Value* { jl_datatype_t *typ = (jl_datatype_t*)(justbits1 ? rt1 : rt2); diff --git a/test/compiler/codegen.jl b/test/compiler/codegen.jl index 0260113044a3b3..07308713bb7896 100644 --- a/test/compiler/codegen.jl +++ b/test/compiler/codegen.jl @@ -999,3 +999,8 @@ for (T, StructName) in ((Int128, :Issue55558), (UInt128, :UIssue55558)) @test sizeof($(StructName)) == 48 broken=broken_i128 end end + +@noinline Base.@nospecializeinfer f55768(@nospecialize z::UnionAll) = z === Vector +@test f55768(Vector) +@test f55768(Vector{T} where T) +@test !f55768(Vector{S} where S)