From 92a39550aa4a69740580f87c03657867bbccea75 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Sun, 11 Jun 2023 07:01:27 +0900 Subject: [PATCH] improve the interplay between bounds checking system and effect system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the current state of the Julia compiler, bounds checking and its related optimization code, such as `@boundscheck` and `@inbounds`, pose a significant handicap for effect analysis. As a result, we're encountering an ironic situation where the application of `@inbounds` annotations, which are intended to optimize performance, instead obstruct the program's optimization, thereby preventing us from achieving optimal performance. This PR is designed to resolve this situation. It aims to enhance the relationship between bounds checking and effect analysis, thereby correctly improving the performance of programs that have `@inbounds` annotations. In the following, I'll first explain the reasons that have led to this situation for better understanding, and then I'll present potential improvements to address these issues. This commit is a collection of various improvement proposals. It's necessary that we incorporate all of them simultaneously to enhance the situation without introducing any regressions. \## Core of the Problem There are fundamentally two reasons why effect analysis of code containing bounds checking is difficult: 1. The evaluation value of `Expr(:boundscheck)` is influenced by the `@inbounds` macro and the `--check-bounds` flag. Hence, when performing a concrete evaluation of a method containing `Expr(:boundscheck)`, it's crucial to respect the `@inbounds` macro context and the `--check-bounds` settings, ensuring the method's behavior is consistent across the compile time concrete evaluation and the runtime execution. 1. If the code, from which bounds checking has been removed due to `@inbounds` or `--check-bounds=no`, is unsafe, it may lead to undefined behavior due to uncertain memory access. \## Current State The current Julia compiler handles these two problems as follows: \### Current State 1 Regarding the first problem, if a code or method call containing `Expr(:boundscheck)` is within an `@inbounds` context, a concrete evaluation is immediately prohibited. For instance, in the following case, when analyzing `bar()`, if you simply perform concrete evaluation of `foo()`, it wouldn't properly respect the `@inbounds` context present in `bar()`. However, since the concrete evaluation of `foo()` is prohibited, it doesn't pose an issue: ```julia foo() = (r = 0; @boundscheck r += 1; return r) bar() = @inbounds foo() ``` Conversely, in the following case, there is _no need_ to prohibit the concrete evaluation of `A1_inbounds` due to the presence of `@inbounds`. This is because the execution of the `@boundscheck` block is determined by the presence of local `@inbounds`: ```julia function A1_inbounds() r = 0 @inbounds begin @boundscheck r += 1 end return r end ``` However, currently, we prohibit the concrete evaluation of such code as well. Moreover, we are not handling such local `@inbounds` contexts effectively, which results in incorrect execution of `A1_inbounds()` (even our test is incorrect for this example: ). Furthermore, there is room for improvement when the `--check-bounds` flag is specified. Specifically, when the `--check-bounds` flag is set, the evaluation value of `Expr(:boundscheck)` is determined irrespective of the `@inbounds` context. Hence, there is no need to prohibit concrete evaluations due to inconsistency in the evaluation value of `Expr(:boundscheck)`. \### Current State 2 Next, we've ensured that concrete evaluation isn't performed when there's potentially unsafe code that may have bounds checking removed, or when the `--check-bounds=no` flag is set, which could lead to bounds checking being removed always. For instance, if you perform concrete evaluation for the function call `baz((1,2,3), 4)` in the following example, it may return a value accessed from illicit memory and introduce undefined behaviors into the program: ```julia baz(t::Tuple, i::Int) = @inbounds t[i] baz((1,2,3), 4) ``` However, it's evident that the above code is incorrect and unsafe program and I believe undefined behavior in such programs is deemed, as explicitly stated in the `@inbounds` documentation: > │ Warning > │ > │ Using @inbounds may return incorrect results/crashes/corruption for > │ out-of-bounds indices. The user is responsible for checking it > │ manually. Only use @inbounds when it is certain from the information > │ locally available that all accesses are in bounds. Actually, the `@inbounds` macro is primarily an annotation to "improve performance by removing bounds checks from safe programs". Therefore, I opine that it would be more reasonable to utilize it to alleviate potential errors due to bounds checking within `@inbounds` contexts. To bring up another associated concern, in the current compiler implementation, the `:nothrow` modelings for `getfield`/`arrayref`/`arrayset` is a bit risky, and `:nothrow`-ness is assumed when their bounds checking is turned off by call argument. If our intended direction aligns with the removal of bounds checking based on `@inbounds` as proposed in issue #48245, then assuming `:nothrow`-ness due to `@inbounds` seems reasonable. However, presuming `:nothrow`-ness due to bounds checking argument or the `--check-bounds` flag appears to be risky, especially considering it's not documented. \## This Commit This commit implements all proposed improvements against the current issues as mentioned above. In summary, the enhancements include: - allowing concrete evaluation within a local `@inbounds` context - folding out `Expr(:boundscheck)` when the `--check-bounds` flag is set (and allow concrete evaluation) - changing the `:nothrow` effect bit to `UInt8` type, and refining `:nothrow` information when in an `@inbounds` context - removing dangerous assumptions of `:nothrow`-ness for built-in functions when bounds checking is turned off - replacing the `@_safeindex` hack with `@inbounds` --- base/array.jl | 71 ++---- base/compiler/abstractinterpretation.jl | 115 ++++++---- base/compiler/effects.jl | 42 ++-- base/compiler/optimize.jl | 11 +- base/compiler/ssair/inlining.jl | 9 +- base/compiler/ssair/ir.jl | 2 +- base/compiler/tfuncs.jl | 215 +++++++++--------- base/compiler/typeinfer.jl | 2 +- base/reflection.jl | 2 +- base/tuple.jl | 2 - test/boundscheck_exec.jl | 45 +++- .../EscapeAnalysis/interprocedural.jl | 4 + test/compiler/EscapeAnalysis/local.jl | 12 +- test/compiler/effects.jl | 103 +++++---- test/compiler/inference.jl | 20 +- 15 files changed, 344 insertions(+), 311 deletions(-) diff --git a/base/array.jl b/base/array.jl index 3a12b38c5bc26..f3a8a5a2d1bae 100644 --- a/base/array.jl +++ b/base/array.jl @@ -122,46 +122,12 @@ const DenseVecOrMat{T} = Union{DenseVector{T}, DenseMatrix{T}} using Core: arraysize, arrayset, const_arrayref -""" - @_safeindex - -This internal macro converts: -- `getindex(xs::Tuple, )` -> `__inbounds_getindex(args...)` -- `setindex!(xs::Vector, args...)` -> `__inbounds_setindex!(xs, args...)` -to tell the compiler that indexing operations within the applied expression are always -inbounds and do not need to taint `:consistent` and `:nothrow`. -""" -macro _safeindex(ex) - return esc(_safeindex(__module__, ex)) -end -function _safeindex(__module__, ex) - isa(ex, Expr) || return ex - if ex.head === :(=) - lhs = arrayref(true, ex.args, 1) - if isa(lhs, Expr) && lhs.head === :ref # xs[i] = x - rhs = arrayref(true, ex.args, 2) - xs = arrayref(true, lhs.args, 1) - args = Vector{Any}(undef, length(lhs.args)-1) - for i = 2:length(lhs.args) - arrayset(true, args, _safeindex(__module__, arrayref(true, lhs.args, i)), i-1) - end - return Expr(:call, GlobalRef(__module__, :__inbounds_setindex!), xs, _safeindex(__module__, rhs), args...) - end - elseif ex.head === :ref # xs[i] - return Expr(:call, GlobalRef(__module__, :__inbounds_getindex), ex.args...) - end - args = Vector{Any}(undef, length(ex.args)) - for i = 1:length(ex.args) - arrayset(true, args, _safeindex(__module__, arrayref(true, ex.args, i)), i) - end - return Expr(ex.head, args...) -end - vect() = Vector{Any}() function vect(X::T...) where T @_terminates_locally_meta - vec = Vector{T}(undef, length(X)) - @_safeindex for i = 1:length(X) + n = length(X) + vec = Vector{T}(undef, n) + @inbounds for i = 1:n vec[i] = X[i] end return vec @@ -443,9 +409,10 @@ julia> getindex(Int8, 1, 2, 3) function getindex(::Type{T}, vals...) where T @inline @_effect_free_terminates_locally_meta - a = Vector{T}(undef, length(vals)) + n = length(vals) + a = Vector{T}(undef, n) if vals isa NTuple - @_safeindex for i in 1:length(vals) + @inbounds for i in 1:n a[i] = vals[i] end else @@ -460,8 +427,9 @@ end function getindex(::Type{Any}, @nospecialize vals...) @_effect_free_terminates_locally_meta - a = Vector{Any}(undef, length(vals)) - @_safeindex for i = 1:length(vals) + n = length(vals) + a = Vector{Any}(undef, n) + @inbounds for i = 1:n a[i] = vals[i] end return a @@ -1021,11 +989,6 @@ function setindex! end @eval setindex!(A::Array{T}, x, i1::Int, i2::Int, I::Int...) where {T} = (@inline; arrayset($(Expr(:boundscheck)), A, x isa T ? x : convert(T,x)::T, i1, i2, I...)) -__inbounds_setindex!(A::Array{T}, x, i1::Int) where {T} = - arrayset(false, A, convert(T,x)::T, i1) -__inbounds_setindex!(A::Array{T}, x, i1::Int, i2::Int, I::Int...) where {T} = - (@inline; arrayset(false, A, convert(T,x)::T, i1, i2, I...)) - # This is redundant with the abstract fallbacks but needed and helpful for bootstrap function setindex!(A::Array, X::AbstractArray, I::AbstractVector{Int}) @_propagate_inbounds_meta @@ -1115,14 +1078,14 @@ function push!(a::Vector{T}, item) where T # convert first so we don't grow the array if the assignment won't work itemT = item isa T ? item : convert(T, item)::T _growend!(a, 1) - @_safeindex a[length(a)] = itemT + @inbounds a[length(a)] = itemT return a end # specialize and optimize the single argument case function push!(a::Vector{Any}, @nospecialize x) _growend!(a, 1) - @_safeindex a[length(a)] = x + @inbounds a[length(a)] = x return a end function push!(a::Vector{Any}, @nospecialize x...) @@ -1130,7 +1093,7 @@ function push!(a::Vector{Any}, @nospecialize x...) na = length(a) nx = length(x) _growend!(a, nx) - @_safeindex for i = 1:nx + @inbounds for i = 1:nx a[na+i] = x[i] end return a @@ -1194,7 +1157,7 @@ function _append!(a::AbstractVector, ::Union{HasLength,HasShape}, iter) resize!(a, n+Int(length(iter))::Int) for (i, item) in zip(i+1:lastindex(a), iter) if isa(a, Vector) # give better effects for builtin vectors - @_safeindex a[i] = item + @inbounds a[i] = item else a[i] = item end @@ -1263,7 +1226,7 @@ function _prepend!(a::Vector, ::Union{HasLength,HasShape}, iter) _growbeg!(a, n) i = 0 for item in iter - @_safeindex a[i += 1] = item + @inbounds a[i += 1] = item end a end @@ -1470,14 +1433,14 @@ julia> pushfirst!([1, 2, 3, 4], 5, 6) function pushfirst!(a::Vector{T}, item) where T item = item isa T ? item : convert(T, item)::T _growbeg!(a, 1) - @_safeindex a[1] = item + @inbounds a[1] = item return a end # specialize and optimize the single argument case function pushfirst!(a::Vector{Any}, @nospecialize x) _growbeg!(a, 1) - @_safeindex a[1] = x + @inbounds a[1] = x return a end function pushfirst!(a::Vector{Any}, @nospecialize x...) @@ -1485,7 +1448,7 @@ function pushfirst!(a::Vector{Any}, @nospecialize x...) na = length(a) nx = length(x) _growbeg!(a, nx) - @_safeindex for i = 1:nx + @inbounds for i = 1:nx a[i] = x[i] end return a diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 5fe0014ef3e60..fcbc629a6df8e 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -153,7 +153,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), elseif isa(matches, MethodMatches) ? (!matches.fullmatch || any_ambig(matches)) : (!all(matches.fullmatches) || 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) + all_effects = Effects(all_effects; nothrow=ALWAYS_FALSE) end rettype = from_interprocedural!(interp, rettype, sv, arginfo, conditionals) @@ -835,18 +835,22 @@ end function concrete_eval_eligible(interp::AbstractInterpreter, @nospecialize(f), result::MethodCallResult, arginfo::ArgInfo, sv::AbsIntState) (;effects) = result - if inbounds_option() === :off + boundscheck = inbounds_option() + if boundscheck === :off if !is_nothrow(effects) # Disable concrete evaluation in `--check-bounds=no` mode, # unless it is known to not throw. return :none end - end - if !effects.noinbounds && stmt_taints_inbounds_consistency(sv) - # If the current statement is @inbounds or we propagate inbounds, the call's consistency - # is tainted and not consteval eligible. - add_remark!(interp, sv, "[constprop] Concrete evel disabled for inbounds") - return :none + elseif boundscheck === :default + if !effects.noinbounds && stmt_taints_inbounds_consistency(sv) + # If the current statement is @inbounds or we propagate inbounds, the call's consistency + # is tainted and not consteval eligible. + add_remark!(interp, sv, "[constprop] Concrete evel disabled for inbounds") + return :none + end + else + # if bounds check is globally enabled, `:boundscheck` is always `true` and thus consistent end if isoverlayed(method_table(interp)) && !is_nonoverlayed(effects) # disable concrete-evaluation if this function call is tainted by some overlayed @@ -1158,6 +1162,7 @@ function semi_concrete_eval_call(interp::AbstractInterpreter, # that are newly resovled by irinterp # state = InliningState(interp) # ir = ssa_inlining_pass!(irsv.ir, state, propagate_inbounds(irsv)) + nothrow = nothrow ? ALWAYS_TRUE : ALWAYS_FALSE new_effects = Effects(result.effects; nothrow) return ConstCallResults(rt, SemiConcreteResult(mi, ir, new_effects), new_effects, mi) end @@ -1854,7 +1859,7 @@ function abstract_call_unionall(interp::AbstractInterpreter, argtypes::Vector{An a2 = argtypes[2] a3 = argtypes[3] ⊑ᵢ = ⊑(typeinf_lattice(interp)) - nothrow = a2 ⊑ᵢ TypeVar && (a3 ⊑ᵢ Type || a3 ⊑ᵢ TypeVar) + nothrow = a2 ⊑ᵢ TypeVar && (a3 ⊑ᵢ Type || a3 ⊑ᵢ TypeVar) ? ALWAYS_TRUE : ALWAYS_FALSE if isa(a3, Const) body = a3.val elseif isType(a3) @@ -1978,13 +1983,15 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), end rt = abstract_call_builtin(interp, f, arginfo, sv, max_methods) effects = builtin_effects(𝕃ᵢ, f, arginfo, rt) + if is_nothrow_if_inbounds(effects) && !iszero(get_curr_ssaflag(sv) & IR_FLAG_INBOUNDS) + effects = Effects(effects; nothrow=ALWAYS_TRUE) + end if f === getfield && (fargs !== nothing && isexpr(fargs[end], :boundscheck)) && !is_nothrow(effects) && isa(sv, InferenceState) # As a special case, we delayed tainting `noinbounds` for getfield calls in case we can prove # in-boundedness indepedently. Here we need to put that back in other cases. # N.B.: This isn't about the effects of the call itself, but a delayed contribution of the :boundscheck - # statement, so we need to merge this directly into sv, rather than modifying thte effects. - merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; noinbounds=false, - consistent = (get_curr_ssaflag(sv) & IR_FLAG_INBOUNDS) != 0 ? ALWAYS_FALSE : ALWAYS_TRUE)) + # statement, so we need to merge this directly into sv, rather than modifying the effects. + merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; noinbounds=false)) end return CallMeta(rt, effects, NoCallInfo()) elseif isa(f, Core.OpaqueClosure) @@ -2077,7 +2084,7 @@ function abstract_call_opaque_closure(interp::AbstractInterpreter, (aty, rty) = (unwrap_unionall(ftt)::DataType).parameters rty = rewrap_unionall(rty isa TypeVar ? rty.lb : rty, ftt) if !(rt ⊑ₚ rty && tuple_tfunc(𝕃ₚ, arginfo.argtypes[2:end]) ⊑ₚ rewrap_unionall(aty, ftt)) - effects = Effects(effects; nothrow=false) + effects = Effects(effects; nothrow=ALWAYS_FALSE) end end rt = from_interprocedural!(interp, rt, sv, arginfo, match.spec_types) @@ -2182,45 +2189,54 @@ function abstract_eval_cfunction(interp::AbstractInterpreter, e::Expr, vtypes::U end function abstract_eval_value_expr(interp::AbstractInterpreter, e::Expr, vtypes::Union{VarTable,Nothing}, sv::AbsIntState) - rt = Any head = e.head if head === :static_parameter n = e.args[1]::Int - nothrow = false + nothrow = ALWAYS_FALSE if 1 <= n <= length(sv.sptypes) sp = sv.sptypes[n] rt = sp.typ - nothrow = !sp.undef + if !sp.undef + nothrow = ALWAYS_TRUE + end + else + rt = Any # make this Bottom? end merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; nothrow)) return rt elseif head === :boundscheck + boundscheck = inbounds_option() + if boundscheck === :on + return Const(true) + elseif boundscheck === :off + return Const(false) + end + if !iszero(get_curr_ssaflag(sv) & IR_FLAG_INBOUNDS) + return Const(false) + end if isa(sv, InferenceState) stmt = sv.src.code[sv.currpc] - if isexpr(stmt, :call) - f = abstract_eval_value(interp, stmt.args[1], vtypes, sv) + rhs = isexpr(stmt, :(=)) ? stmt.args[2] : stmt + if isexpr(rhs, :call) + f = abstract_eval_value(interp, rhs.args[1], vtypes, sv) if f isa Const && f.val === getfield # boundscheck of `getfield` call is analyzed by tfunc potentially without # tainting :inbounds or :consistent when it's known to be nothrow - @goto delay_effects_analysis + return Bool end end - # If there is no particular `@inbounds` for this function, then we only taint `:noinbounds`, - # which will subsequently taint `:consistent`-cy if this function is called from another - # function that uses `@inbounds`. However, if this `:boundscheck` is itself within an - # `@inbounds` region, its value depends on `--check-bounds`, so we need to taint - # `:consistent`-cy here also. - merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; noinbounds=false, - consistent = (get_curr_ssaflag(sv) & IR_FLAG_INBOUNDS) != 0 ? ALWAYS_FALSE : ALWAYS_TRUE)) + # Taint `:noinbounds`, which will subsequently prohibit concrete-evaluation for + # this function if it is called from a context that uses `@inbounds`. + merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; noinbounds=false)) end - @label delay_effects_analysis - rt = Bool + return Bool elseif head === :inbounds @assert false && "Expected this to have been moved into flags" elseif head === :the_exception merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; consistent=ALWAYS_FALSE)) + return Any end - return rt + return Any end function abstract_eval_special_value(interp::AbstractInterpreter, @nospecialize(e), vtypes::Union{VarTable,Nothing}, sv::AbsIntState) @@ -2232,11 +2248,11 @@ function abstract_eval_special_value(interp::AbstractInterpreter, @nospecialize( if vtypes !== nothing vtyp = vtypes[slot_id(e)] if vtyp.undef - merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; nothrow=false)) + merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; nothrow=ALWAYS_FALSE)) end return vtyp.typ end - merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; nothrow=false)) + merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; nothrow=ALWAYS_FALSE)) return Any elseif isa(e, Argument) if vtypes !== nothing @@ -2317,8 +2333,7 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp elseif ehead === :new t, isexact = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv)) ut = unwrap_unionall(t) - consistent = ALWAYS_FALSE - nothrow = false + nothrow = consistent = ALWAYS_FALSE if isa(ut, DataType) && !isabstracttype(ut) ismutable = ismutabletype(ut) fcount = datatype_fieldcount(ut) @@ -2336,7 +2351,7 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp consistent = ALWAYS_TRUE end if isconcretedispatch(t) - nothrow = true + nothrow = ALWAYS_TRUE @assert fcount !== nothing && fcount ≥ nargs "malformed :new expression" # syntactically enforced by the front-end ats = Vector{Any}(undef, nargs) local anyrefine = false @@ -2344,7 +2359,9 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp for i = 1:nargs at = widenslotwrapper(abstract_eval_value(interp, e.args[i+1], vtypes, sv)) ft = fieldtype(t, i) - nothrow && (nothrow = at ⊑ᵢ ft) + if nothrow === ALWAYS_TRUE + nothrow = at ⊑ᵢ ft ? ALWAYS_TRUE : ALWAYS_FALSE + end at = tmeet(𝕃ᵢ, at, ft) at === Bottom && @goto always_throw if ismutable && !isconst(t, i) @@ -2380,7 +2397,7 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp effects = Effects(EFFECTS_TOTAL; consistent, nothrow) elseif ehead === :splatnew t, isexact = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv)) - nothrow = false # TODO: More precision + nothrow = ALWAYS_FALSE # TODO: More precision if length(e.args) == 2 && isconcretedispatch(t) && !ismutabletype(t) at = abstract_eval_value(interp, e.args[2], vtypes, sv) n = fieldcount(t) @@ -2388,13 +2405,13 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp (let t = t, at = at all(i::Int->getfield(at.val::Tuple, i) isa fieldtype(t, i), 1:n) end)) - nothrow = isexact + nothrow = isexact ? ALWAYS_TRUE : ALWAYS_FALSE t = Const(ccall(:jl_new_structt, Any, (Any, Any), t, at.val)) elseif (isa(at, PartialStruct) && at ⊑ᵢ Tuple && n > 0 && n == length(at.fields::Vector{Any}) && !isvarargtype(at.fields[end]) && (let t = t, at = at, ⊑ᵢ = ⊑ᵢ all(i::Int->(at.fields::Vector{Any})[i] ⊑ᵢ fieldtype(t, i), 1:n) end)) - nothrow = isexact + nothrow = isexact ? ALWAYS_TRUE : ALWAYS_FALSE t = PartialStruct(t, at.fields::Vector{Any}) end else @@ -2530,7 +2547,7 @@ function abstract_eval_foreigncall(interp::AbstractInterpreter, e::Expr, vtypes: effects = Effects( override.consistent ? ALWAYS_TRUE : effects.consistent, override.effect_free ? ALWAYS_TRUE : effects.effect_free, - override.nothrow ? true : effects.nothrow, + override.nothrow ? ALWAYS_TRUE : effects.nothrow, override.terminates_globally ? true : effects.terminates, override.notaskstate ? true : effects.notaskstate, override.inaccessiblememonly ? ALWAYS_TRUE : effects.inaccessiblememonly, @@ -2564,15 +2581,15 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), return abstract_eval_special_value(interp, e, vtypes, sv) end (; rt, effects) = abstract_eval_statement_expr(interp, e, vtypes, sv) + if is_nothrow_if_inbounds(effects) && !iszero(get_curr_ssaflag(sv) & IR_FLAG_INBOUNDS) + effects = Effects(effects; nothrow=ALWAYS_TRUE) + end if !effects.noinbounds if !propagate_inbounds(sv) # The callee read our inbounds flag, but unless we propagate inbounds, # we ourselves don't read our parent's inbounds. effects = Effects(effects; noinbounds=true) end - if (get_curr_ssaflag(sv) & IR_FLAG_INBOUNDS) != 0 - effects = Effects(effects; consistent=ALWAYS_FALSE) - end end merge_effects!(interp, sv, effects) e = e::Expr @@ -2606,15 +2623,14 @@ abstract_eval_global(M::Module, s::Symbol) = abstract_eval_globalref(GlobalRef(M function abstract_eval_globalref(interp::AbstractInterpreter, g::GlobalRef, sv::AbsIntState) rt = abstract_eval_globalref(g) consistent = inaccessiblememonly = ALWAYS_FALSE - nothrow = false + nothrow = ALWAYS_FALSE if isa(rt, Const) - consistent = ALWAYS_TRUE - nothrow = true + nothrow = consistent = ALWAYS_TRUE if is_mutation_free_argtype(rt) inaccessiblememonly = ALWAYS_TRUE end elseif isdefined_globalref(g) - nothrow = true + nothrow = ALWAYS_TRUE elseif InferenceParams(interp).assume_bindings_static consistent = inaccessiblememonly = ALWAYS_TRUE rt = Union{} @@ -2624,9 +2640,10 @@ function abstract_eval_globalref(interp::AbstractInterpreter, g::GlobalRef, sv:: end function handle_global_assignment!(interp::AbstractInterpreter, frame::InferenceState, lhs::GlobalRef, @nospecialize(newty)) - effect_free = ALWAYS_FALSE - nothrow = global_assignment_nothrow(lhs.mod, lhs.name, newty) - inaccessiblememonly = ALWAYS_FALSE + inaccessiblememonly = nothrow = effect_free = ALWAYS_FALSE + if global_assignment_nothrow(lhs.mod, lhs.name, newty) + nothrow = ALWAYS_TRUE + end merge_effects!(interp, frame, Effects(EFFECTS_TOTAL; effect_free, nothrow, inaccessiblememonly)) return nothing end diff --git a/base/compiler/effects.jl b/base/compiler/effects.jl index 7d09769e5b31b..ced8fba86a2ac 100644 --- a/base/compiler/effects.jl +++ b/base/compiler/effects.jl @@ -70,6 +70,7 @@ The output represents the state of different effect properties in the following 3. `nothrow` (`n`): - `+n` (green): `true` - `-n` (red): `false` + - `?n` (yellow): `NOTHROW_IF_INBOUNDS` 4. `terminates` (`t`): - `+t` (green): `true` - `-t` (red): `false` @@ -89,7 +90,7 @@ Additionally, if the `nonoverlayed` property is false, a red prime symbol (′) struct Effects consistent::UInt8 effect_free::UInt8 - nothrow::Bool + nothrow::UInt8 terminates::Bool notaskstate::Bool inaccessiblememonly::UInt8 @@ -98,7 +99,7 @@ struct Effects function Effects( consistent::UInt8, effect_free::UInt8, - nothrow::Bool, + nothrow::UInt8, terminates::Bool, notaskstate::Bool, inaccessiblememonly::UInt8, @@ -126,18 +127,21 @@ const CONSISTENT_IF_INACCESSIBLEMEMONLY = 0x01 << 2 # :effect_free-ness bits const EFFECT_FREE_IF_INACCESSIBLEMEMONLY = 0x01 << 1 +# :nothrow-ness bits +const NOTHROW_IF_INBOUNDS = 0x01 << 1 + # :inaccessiblememonly bits const INACCESSIBLEMEM_OR_ARGMEMONLY = 0x01 << 1 -const EFFECTS_TOTAL = Effects(ALWAYS_TRUE, ALWAYS_TRUE, true, true, true, ALWAYS_TRUE, true, true) -const EFFECTS_THROWS = Effects(ALWAYS_TRUE, ALWAYS_TRUE, false, true, true, ALWAYS_TRUE, true, true) -const EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, true, true) # unknown mostly, but it's not overlayed and noinbounds at least (e.g. it's not a call) -const _EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, false, false) # unknown really +const EFFECTS_TOTAL = Effects(ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, true, true, ALWAYS_TRUE, true, true) +const EFFECTS_THROWS = Effects(ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_FALSE, true, true, ALWAYS_TRUE, true, true) +const EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, ALWAYS_FALSE, false, false, ALWAYS_FALSE, true, true) # unknown mostly, but it's not overlayed and noinbounds at least (e.g. it's not a call) +const _EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, ALWAYS_FALSE, false, false, ALWAYS_FALSE, false, false) # unknown really function Effects(e::Effects = _EFFECTS_UNKNOWN; consistent::UInt8 = e.consistent, effect_free::UInt8 = e.effect_free, - nothrow::Bool = e.nothrow, + nothrow::UInt8 = e.nothrow, terminates::Bool = e.terminates, notaskstate::Bool = e.notaskstate, inaccessiblememonly::UInt8 = e.inaccessiblememonly, @@ -176,7 +180,7 @@ merge_effectbits(old::Bool, new::Bool) = old & new is_consistent(effects::Effects) = effects.consistent === ALWAYS_TRUE is_effect_free(effects::Effects) = effects.effect_free === ALWAYS_TRUE -is_nothrow(effects::Effects) = effects.nothrow +is_nothrow(effects::Effects) = effects.nothrow === ALWAYS_TRUE is_terminates(effects::Effects) = effects.terminates is_notaskstate(effects::Effects) = effects.notaskstate is_inaccessiblememonly(effects::Effects) = effects.inaccessiblememonly === ALWAYS_TRUE @@ -206,29 +210,31 @@ is_consistent_if_inaccessiblememonly(effects::Effects) = !iszero(effects.consist is_effect_free_if_inaccessiblememonly(effects::Effects) = !iszero(effects.effect_free & EFFECT_FREE_IF_INACCESSIBLEMEMONLY) +is_nothrow_if_inbounds(effects::Effects) = !iszero(effects.nothrow & NOTHROW_IF_INBOUNDS) + is_inaccessiblemem_or_argmemonly(effects::Effects) = effects.inaccessiblememonly === INACCESSIBLEMEM_OR_ARGMEMONLY function encode_effects(e::Effects) return ((e.consistent % UInt32) << 0) | ((e.effect_free % UInt32) << 3) | ((e.nothrow % UInt32) << 5) | - ((e.terminates % UInt32) << 6) | - ((e.notaskstate % UInt32) << 7) | - ((e.inaccessiblememonly % UInt32) << 8) | - ((e.nonoverlayed % UInt32) << 10)| - ((e.noinbounds % UInt32) << 11) + ((e.terminates % UInt32) << 7) | + ((e.notaskstate % UInt32) << 8) | + ((e.inaccessiblememonly % UInt32) << 9) | + ((e.nonoverlayed % UInt32) << 11)| + ((e.noinbounds % UInt32) << 12) end function decode_effects(e::UInt32) return Effects( UInt8((e >> 0) & 0x07), UInt8((e >> 3) & 0x03), - _Bool((e >> 5) & 0x01), - _Bool((e >> 6) & 0x01), + UInt8((e >> 5) & 0x03), _Bool((e >> 7) & 0x01), - UInt8((e >> 8) & 0x03), - _Bool((e >> 10) & 0x01), - _Bool((e >> 11) & 0x01)) + _Bool((e >> 8) & 0x01), + UInt8((e >> 9) & 0x03), + _Bool((e >> 11) & 0x01), + _Bool((e >> 12) & 0x01)) end struct EffectsOverride diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index 8810857ce81a7..6a51a1a98c42a 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -216,12 +216,14 @@ is_stmt_noinline(stmt_flag::UInt8) = stmt_flag & IR_FLAG_NOINLINE ≠ 0 is_stmt_throw_block(stmt_flag::UInt8) = stmt_flag & IR_FLAG_THROW_BLOCK ≠ 0 """ - stmt_effect_flags(stmt, rt, src::Union{IRCode,IncrementalCompact}) -> + stmt_effect_flags(𝕃ₒ::AbstractLattice, stmt, rt, + src::Union{IRCode,IncrementalCompact}, flag::UInt8) -> (consistent::Bool, effect_free_and_nothrow::Bool, nothrow::Bool) Returns a tuple of `(:consistent, :effect_free_and_nothrow, :nothrow)` flags for a given statement. """ -function stmt_effect_flags(𝕃ₒ::AbstractLattice, @nospecialize(stmt), @nospecialize(rt), src::Union{IRCode,IncrementalCompact}) +function stmt_effect_flags(𝕃ₒ::AbstractLattice, @nospecialize(stmt), @nospecialize(rt), + src::Union{IRCode,IncrementalCompact}, flag::UInt8=IR_FLAG_NULL) # TODO: We're duplicating analysis from inference here. isa(stmt, PiNode) && return (true, true, true) isa(stmt, PhiNode) && return (true, true, true) @@ -247,7 +249,7 @@ function stmt_effect_flags(𝕃ₒ::AbstractLattice, @nospecialize(stmt), @nospe if f === UnionAll # TODO: This is a weird special case - should be determined in inference argtypes = Any[argextype(args[arg], src) for arg in 2:length(args)] - nothrow = _builtin_nothrow(𝕃ₒ, f, argtypes, rt) + nothrow = _builtin_nothrow(Bool, 𝕃ₒ, f, argtypes, rt) return (true, nothrow, nothrow) end if f === Intrinsics.cglobal @@ -261,6 +263,9 @@ function stmt_effect_flags(𝕃ₒ::AbstractLattice, @nospecialize(stmt), @nospe effects = builtin_effects(𝕃ₒ, f, ArgInfo(args, argtypes), rt) consistent = is_consistent(effects) effect_free = is_effect_free(effects) + if is_nothrow_if_inbounds(effects) && !iszero(flag & IR_FLAG_INBOUNDS) + effects = Effects(effects; nothrow=ALWAYS_TRUE) + end nothrow = is_nothrow(effects) return (consistent, effect_free & nothrow, nothrow) elseif head === :new diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index 17df27bd5f637..65d764c92e5da 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -1231,7 +1231,7 @@ function check_effect_free!(ir::IRCode, idx::Int, @nospecialize(stmt), @nospecia return check_effect_free!(ir, idx, stmt, rt, optimizer_lattice(state.interp)) end function check_effect_free!(ir::IRCode, idx::Int, @nospecialize(stmt), @nospecialize(rt), 𝕃ₒ::AbstractLattice) - (consistent, effect_free_and_nothrow, nothrow) = stmt_effect_flags(𝕃ₒ, stmt, rt, ir) + (consistent, effect_free_and_nothrow, nothrow) = stmt_effect_flags(𝕃ₒ, stmt, rt, ir, ir.stmts[idx][:flag]) if consistent ir.stmts[idx][:flag] |= IR_FLAG_CONSISTENT end @@ -1406,7 +1406,7 @@ function compute_inlining_cases(@nospecialize(info::CallInfo), flag::UInt8, sig: fully_covered &= split_fully_covered end - fully_covered || (joint_effects = Effects(joint_effects; nothrow=false)) + fully_covered || (joint_effects = Effects(joint_effects; nothrow=ALWAYS_FALSE)) if handled_all_cases && revisit_idx !== nothing # we handled everything except one match with unmatched sparams, @@ -1724,7 +1724,7 @@ function early_inline_special_case( elseif contains_is(_PURE_BUILTINS, f) return SomeCase(quoted(val)) elseif contains_is(_EFFECT_FREE_BUILTINS, f) - if _builtin_nothrow(optimizer_lattice(state.interp), f, argtypes[2:end], type) + if _builtin_nothrow(Bool, optimizer_lattice(state.interp), f, argtypes[2:end], type) return SomeCase(quoted(val)) end elseif f === Core.get_binding_type @@ -1779,7 +1779,8 @@ function late_inline_special_case!( elseif length(argtypes) == 3 && istopfunction(f, :(>:)) # special-case inliner for issupertype # that works, even though inference generally avoids inferring the `>:` Method - if isa(type, Const) && _builtin_nothrow(optimizer_lattice(state.interp), <:, Any[argtypes[3], argtypes[2]], type) + if isa(type, Const) && _builtin_nothrow(Bool, + optimizer_lattice(state.interp), <:, Any[argtypes[3], argtypes[2]], type) return SomeCase(quoted(type.val)) end subtype_call = Expr(:call, GlobalRef(Core, :(<:)), stmt.args[3], stmt.args[2]) diff --git a/base/compiler/ssair/ir.jl b/base/compiler/ssair/ir.jl index debad8bfb0d66..ad83cfd2b371c 100644 --- a/base/compiler/ssair/ir.jl +++ b/base/compiler/ssair/ir.jl @@ -824,7 +824,7 @@ function recompute_inst_flag(newinst::NewInstruction, src::Union{IRCode,Incremen flag !== nothing && return flag flag = IR_FLAG_NULL (consistent, effect_free_and_nothrow, nothrow) = stmt_effect_flags( - fallback_lattice, newinst.stmt, newinst.type, src) + fallback_lattice, newinst.stmt, newinst.type, src, flag) if consistent flag |= IR_FLAG_CONSISTENT end diff --git a/base/compiler/tfuncs.jl b/base/compiler/tfuncs.jl index 79e3cfefc7ff1..eafe3db75e492 100644 --- a/base/compiler/tfuncs.jl +++ b/base/compiler/tfuncs.jl @@ -948,32 +948,27 @@ end function getfield_nothrow(𝕃::AbstractLattice, arginfo::ArgInfo, boundscheck::Symbol=getfield_boundscheck(arginfo)) (;argtypes) = arginfo - boundscheck === :unknown && return false + boundscheck === :unknown && return ALWAYS_FALSE ordering = Const(:not_atomic) if length(argtypes) == 4 - isvarargtype(argtypes[4]) && return false + isvarargtype(argtypes[4]) && return ALWAYS_FALSE if widenconst(argtypes[4]) !== Bool ordering = argtypes[4] end elseif length(argtypes) == 5 ordering = argtypes[5] elseif length(argtypes) != 3 - return false + return ALWAYS_FALSE end - isa(ordering, Const) || return false + isa(ordering, Const) || return ALWAYS_FALSE ordering = ordering.val - isa(ordering, Symbol) || return false + isa(ordering, Symbol) || return ALWAYS_FALSE if ordering !== :not_atomic # TODO: this is assuming not atomic - return false + return ALWAYS_FALSE end - return getfield_nothrow(𝕃, argtypes[2], argtypes[3], !(boundscheck === :off)) + return getfield_nothrow(𝕃, argtypes[2], argtypes[3]) end -@nospecs function getfield_nothrow(𝕃::AbstractLattice, s00, name, boundscheck::Bool) - # If we don't have boundscheck off and don't know the field, don't even bother - if boundscheck - isa(name, Const) || return false - end - +@nospecs function getfield_nothrow(𝕃::AbstractLattice, s00, name) ⊑ = Core.Compiler.:⊑(𝕃) # If we have s00 being a const, we can potentially refine our type-based analysis above @@ -985,51 +980,56 @@ end end if isa(name, Const) nval = name.val - if !isa(nval, Symbol) - isa(sv, Module) && return false - isa(nval, Int) || return false + if isa(nval, Int) + isa(sv, Module) && return ALWAYS_FALSE + elseif !isa(nval, Symbol) + return ALWAYS_FALSE end - return isdefined(sv, nval) + return isdefined(sv, nval) ? ALWAYS_TRUE : ALWAYS_FALSE end - boundscheck && return false - # If bounds checking is disabled and all fields are assigned, - # we may assume that we don't throw - isa(sv, Module) && return false - name ⊑ Int || name ⊑ Symbol || return false + # if all fields are assigned, we may assume that we don't throw if `@inbounds` applied + isa(sv, Module) && return ALWAYS_FALSE + name ⊑ Int || name ⊑ Symbol || return ALWAYS_FALSE for i = 1:fieldcount(typeof(sv)) - isdefined(sv, i) || return false + isdefined(sv, i) || return ALWAYS_FALSE end - return true + return NOTHROW_IF_INBOUNDS end s0 = widenconst(s00) s = unwrap_unionall(s0) if isa(s, Union) - return getfield_nothrow(𝕃, rewrap_unionall(s.a, s00), name, boundscheck) && - getfield_nothrow(𝕃, rewrap_unionall(s.b, s00), name, boundscheck) + return merge_effectbits( + getfield_nothrow(𝕃, rewrap_unionall(s.a, s00), name), + getfield_nothrow(𝕃, rewrap_unionall(s.b, s00), name)) elseif isType(s) && isTypeDataType(s.parameters[1]) s = s0 = DataType end - if isa(s, DataType) - # Can't say anything about abstract types - isabstracttype(s) && return false - # If all fields are always initialized, and bounds check is disabled, - # we can assume we don't throw - if !boundscheck && s.name.n_uninitialized == 0 - name ⊑ Int || name ⊑ Symbol || return false - return true - end - # Else we need to know what the field is - isa(name, Const) || return false + isa(s, DataType) || return ALWAYS_FALSE + # Can't say anything about abstract types + isabstracttype(s) && return ALWAYS_FALSE + if isa(name, Const) field = try_compute_fieldidx(s, name.val) - field === nothing && return false - isfieldatomic(s, field) && return false # TODO: currently we're only testing for ordering === :not_atomic - field <= datatype_min_ninitialized(s) && return true - # `try_compute_fieldidx` already check for field index bound. - !isvatuple(s) && isbitstype(fieldtype(s0, field)) && return true + if field !== nothing + isfieldatomic(s, field) && return ALWAYS_FALSE # TODO: currently we're only testing for ordering === :not_atomic + field <= datatype_min_ninitialized(s) && return ALWAYS_TRUE + # a `isbitstype` field is initialized with undefined value and thus won't throw + isvatuple(s) && return ALWAYS_FALSE + isbitstype(fieldtype(s0, field)) && return ALWAYS_TRUE + return ALWAYS_FALSE + end end - - return false + # the precise field is unknown, but we can still perform exhaustive check on all fields + nf = nfields_tfunc(𝕃, s0) + nf isa Const || return ALWAYS_FALSE + nfv = nf.val + nfv isa Int || return ALWAYS_FALSE + any(isfieldatomic(s, i) for i = 1:nfv) && return ALWAYS_FALSE + isvatuple(s) && return ALWAYS_FALSE + for i = 1:s.name.n_uninitialized + isbitstype(fieldtype(s0, nfv-i+1)) || return ALWAYS_FALSE + end + return NOTHROW_IF_INBOUNDS end @nospecs function getfield_tfunc(𝕃::AbstractLattice, s00, name, boundscheck_or_order) @@ -2043,24 +2043,17 @@ end function array_builtin_common_nothrow(argtypes::Vector{Any}, isarrayref::Bool) first_idx_idx = isarrayref ? 3 : 4 - length(argtypes) ≥ first_idx_idx || return false + length(argtypes) ≥ first_idx_idx || return ALWAYS_FALSE boundscheck = argtypes[1] arytype = argtypes[2] - array_builtin_common_typecheck(boundscheck, arytype, argtypes, first_idx_idx) || return false + array_builtin_common_typecheck(boundscheck, arytype, argtypes, first_idx_idx) || return ALWAYS_FALSE if isarrayref # If we could potentially throw undef ref errors, bail out now. arytype = widenconst(arytype) - array_type_undefable(arytype) && return false + array_type_undefable(arytype) && return ALWAYS_FALSE end - # If we have @inbounds (first argument is false), we're allowed to assume - # we don't throw bounds errors. - if isa(boundscheck, Const) - boundscheck.val::Bool || return true - end - # Else we can't really say anything here - # TODO: In the future we may be able to track the shapes of arrays though - # inference. - return false + # TODO: In the future we may be able to track the shapes of arrays though inference. + return NOTHROW_IF_INBOUNDS end @nospecs function array_builtin_common_typecheck(boundscheck, arytype, @@ -2087,89 +2080,82 @@ end @nospecs function _builtin_nothrow(𝕃::AbstractLattice, f, argtypes::Vector{Any}, rt) ⊑ = Core.Compiler.:⊑(𝕃) if f === arrayset - array_builtin_common_nothrow(argtypes, #=isarrayref=#false) || return false + res = array_builtin_common_nothrow(argtypes, #=isarrayref=#false) # Additionally check element type compatibility - return arrayset_typecheck(argtypes[2], argtypes[3]) + res === ALWAYS_FALSE && return ALWAYS_FALSE + arrayset_typecheck(argtypes[2], argtypes[3]) || return ALWAYS_FALSE + return res elseif f === arrayref || f === const_arrayref return array_builtin_common_nothrow(argtypes, #=isarrayref=#true) elseif f === Core._expr - length(argtypes) >= 1 || return false - return argtypes[1] ⊑ Symbol + return (length(argtypes) ≥ 1 && argtypes[1] ⊑ Symbol) ? ALWAYS_TRUE : ALWAYS_FALSE end # These builtins are not-vararg, so if we have varars, here, we can't guarantee # the correct number of arguments. na = length(argtypes) - (na ≠ 0 && isvarargtype(argtypes[end])) && return false + (na ≠ 0 && isvarargtype(argtypes[end])) && return ALWAYS_FALSE if f === arraysize - na == 2 || return false - return arraysize_nothrow(argtypes[1], argtypes[2]) + return (na == 2 && arraysize_nothrow(argtypes[1], argtypes[2])) ? ALWAYS_TRUE : ALWAYS_FALSE elseif f === Core._typevar - na == 3 || return false - return typevar_nothrow(𝕃, argtypes[1], argtypes[2], argtypes[3]) + return (na == 3 && typevar_nothrow(𝕃, argtypes[1], argtypes[2], argtypes[3])) ? ALWAYS_TRUE : ALWAYS_FALSE elseif f === invoke - return false + return ALWAYS_FALSE elseif f === getfield return getfield_nothrow(𝕃, ArgInfo(nothing, Any[Const(f), argtypes...])) elseif f === setfield! + # TODO if na == 3 - return setfield!_nothrow(𝕃, argtypes[1], argtypes[2], argtypes[3]) + return setfield!_nothrow(𝕃, argtypes[1], argtypes[2], argtypes[3]) ? ALWAYS_TRUE : ALWAYS_FALSE elseif na == 4 - return setfield!_nothrow(𝕃, argtypes[1], argtypes[2], argtypes[3], argtypes[4]) + return setfield!_nothrow(𝕃, argtypes[1], argtypes[2], argtypes[3], argtypes[4]) ? ALWAYS_TRUE : ALWAYS_FALSE + else + return ALWAYS_FALSE end - return false elseif f === fieldtype - na == 2 || return false - return fieldtype_nothrow(𝕃, argtypes[1], argtypes[2]) + return (na == 2 && fieldtype_nothrow(𝕃, argtypes[1], argtypes[2])) ? ALWAYS_TRUE : ALWAYS_FALSE elseif f === apply_type - return apply_type_nothrow(𝕃, argtypes, rt) + return apply_type_nothrow(𝕃, argtypes, rt) ? ALWAYS_TRUE : ALWAYS_FALSE elseif f === isa - na == 2 || return false - return isa_nothrow(𝕃, nothing, argtypes[2]) + return (na == 2 && isa_nothrow(𝕃, nothing, argtypes[2])) ? ALWAYS_TRUE : ALWAYS_FALSE elseif f === (<:) - na == 2 || return false - return subtype_nothrow(𝕃, argtypes[1], argtypes[2]) + return (na == 2 && subtype_nothrow(𝕃, argtypes[1], argtypes[2])) ? ALWAYS_TRUE : ALWAYS_FALSE elseif f === UnionAll - return na == 2 && (argtypes[1] ⊑ TypeVar && argtypes[2] ⊑ Type) + return (na == 2 && (argtypes[1] ⊑ TypeVar && argtypes[2] ⊑ Type)) ? ALWAYS_TRUE : ALWAYS_FALSE elseif f === isdefined - return isdefined_nothrow(𝕃, argtypes) + return isdefined_nothrow(𝕃, argtypes) ? ALWAYS_TRUE : ALWAYS_FALSE elseif f === Core.sizeof - na == 1 || return false - return sizeof_nothrow(argtypes[1]) + return (na == 1 && sizeof_nothrow(argtypes[1])) ? ALWAYS_TRUE : ALWAYS_FALSE elseif f === Core.ifelse - na == 3 || return false - return ifelse_nothrow(𝕃, argtypes[1], nothing, nothing) + return (na == 3 && ifelse_nothrow(𝕃, argtypes[1], nothing, nothing)) ? ALWAYS_TRUE : ALWAYS_FALSE elseif f === typeassert - na == 2 || return false - return typeassert_nothrow(𝕃, argtypes[1], argtypes[2]) + return (na == 2 && typeassert_nothrow(𝕃, argtypes[1], argtypes[2])) ? ALWAYS_TRUE : ALWAYS_FALSE elseif f === getglobal if na == 2 - return getglobal_nothrow(argtypes[1], argtypes[2]) + return getglobal_nothrow(argtypes[1], argtypes[2]) ? ALWAYS_TRUE : ALWAYS_FALSE elseif na == 3 - return getglobal_nothrow(argtypes[1], argtypes[2], argtypes[3]) + return getglobal_nothrow(argtypes[1], argtypes[2], argtypes[3]) ? ALWAYS_TRUE : ALWAYS_FALSE end - return false + return ALWAYS_FALSE elseif f === setglobal! if na == 3 - return setglobal!_nothrow(argtypes[1], argtypes[2], argtypes[3]) + return setglobal!_nothrow(argtypes[1], argtypes[2], argtypes[3]) ? ALWAYS_TRUE : ALWAYS_FALSE elseif na == 4 - return setglobal!_nothrow(argtypes[1], argtypes[2], argtypes[3], argtypes[4]) + return setglobal!_nothrow(argtypes[1], argtypes[2], argtypes[3], argtypes[4]) ? ALWAYS_TRUE : ALWAYS_FALSE + else + return ALWAYS_FALSE end - return false elseif f === Core.get_binding_type - na == 2 || return false - return get_binding_type_nothrow(𝕃, argtypes[1], argtypes[2]) + return (na == 2 && get_binding_type_nothrow(𝕃, argtypes[1], argtypes[2])) ? ALWAYS_TRUE : ALWAYS_FALSE elseif f === donotdelete - return true + return ALWAYS_TRUE elseif f === Core.finalizer - 2 <= na <= 4 || return false # Core.finalizer does no error checking - that's done in Base.finalizer - return true + return 2 <= na <= 4 ? ALWAYS_TRUE : ALWAYS_FALSE elseif f === Core.compilerbarrier - na == 2 || return false - return compilerbarrier_nothrow(argtypes[1], nothing) + return (na == 2 && compilerbarrier_nothrow(argtypes[1], nothing)) ? ALWAYS_TRUE : ALWAYS_FALSE end - return false + return ALWAYS_FALSE end # known to be always effect-free (in particular nothrow) @@ -2301,7 +2287,7 @@ function isdefined_effects(𝕃::AbstractLattice, argtypes::Vector{Any}) end end end - nothrow = isdefined_nothrow(𝕃, argtypes) + nothrow = isdefined_nothrow(𝕃, argtypes) ? ALWAYS_TRUE : ALWAYS_FALSE if hasintersect(widenconst(wobj), Module) inaccessiblememonly = ALWAYS_FALSE elseif is_mutation_free_argtype(wobj) @@ -2330,7 +2316,7 @@ function getfield_effects(𝕃::AbstractLattice, arginfo::ArgInfo, @nospecialize end bcheck = getfield_boundscheck(arginfo) nothrow = getfield_nothrow(𝕃, arginfo, bcheck) - if !nothrow + if nothrow !== ALWAYS_TRUE if !(bcheck === :on || bcheck === :boundscheck) # If we cannot independently prove inboundsness, taint consistency. # The inbounds-ness assertion requires dynamic reachability, while @@ -2354,12 +2340,11 @@ function getfield_effects(𝕃::AbstractLattice, arginfo::ArgInfo, @nospecialize end function getglobal_effects(argtypes::Vector{Any}, @nospecialize(rt)) - consistent = inaccessiblememonly = ALWAYS_FALSE - nothrow = false + inaccessiblememonly = nothrow = consistent = ALWAYS_FALSE if length(argtypes) ≥ 2 M, s = argtypes[1], argtypes[2] if getglobal_nothrow(M, s) - nothrow = true + nothrow = ALWAYS_TRUE # typeasserts below are already checked in `getglobal_nothrow` Mval, sval = (M::Const).val::Module, (s::Const).val::Symbol if isconst(Mval, sval) @@ -2410,7 +2395,11 @@ function builtin_effects(𝕃::AbstractLattice, @nospecialize(f::Builtin), argin else effect_free = ALWAYS_FALSE end - nothrow = (isempty(argtypes) || !isvarargtype(argtypes[end])) && builtin_nothrow(𝕃, f, argtypes, rt) + if (isempty(argtypes) || !isvarargtype(argtypes[end])) + nothrow = builtin_nothrow(𝕃, f, argtypes, rt) + else + nothrow = ALWAYS_FALSE + end if contains_is(_INACCESSIBLEMEM_BUILTINS, f) inaccessiblememonly = ALWAYS_TRUE elseif contains_is(_ARGMEM_BUILTINS, f) @@ -2423,11 +2412,16 @@ function builtin_effects(𝕃::AbstractLattice, @nospecialize(f::Builtin), argin end function builtin_nothrow(𝕃::AbstractLattice, @nospecialize(f), argtypes::Vector{Any}, @nospecialize(rt)) - rt === Bottom && return false - contains_is(_PURE_BUILTINS, f) && return true + rt === Bottom && return ALWAYS_FALSE + contains_is(_PURE_BUILTINS, f) && return ALWAYS_TRUE return _builtin_nothrow(𝕃, f, argtypes, rt) end +@nospecs _builtin_nothrow(::Type{Bool}, 𝕃::AbstractLattice, f, argtypes::Vector{Any}, rt) = + _builtin_nothrow(𝕃, f, argtypes, rt) === ALWAYS_TRUE +@nospecs builtin_nothrow(::Type{Bool}, 𝕃::AbstractLattice, f, argtypes::Vector{Any}, rt) = + builtin_nothrow(𝕃, f, argtypes, rt) === ALWAYS_TRUE + function builtin_tfunction(interp::AbstractInterpreter, @nospecialize(f), argtypes::Vector{Any}, sv::Union{AbsIntState, Nothing}) 𝕃ᵢ = typeinf_lattice(interp) @@ -2598,7 +2592,8 @@ function intrinsic_effects(f::IntrinsicFunction, argtypes::Vector{Any}) consistent = ALWAYS_TRUE end effect_free = !(f === Intrinsics.pointerset) ? ALWAYS_TRUE : ALWAYS_FALSE - nothrow = (isempty(argtypes) || !isvarargtype(argtypes[end])) && intrinsic_nothrow(f, argtypes) + nothrow = (isempty(argtypes) || !isvarargtype(argtypes[end])) && intrinsic_nothrow(f, argtypes) ? + ALWAYS_TRUE : ALWAYS_FALSE if f === arraylen inaccessiblememonly = INACCESSIBLEMEM_OR_ARGMEMONLY else @@ -2896,7 +2891,7 @@ end function array_resize_effects() return Effects(EFFECTS_TOTAL; effect_free = EFFECT_FREE_IF_INACCESSIBLEMEMONLY, - nothrow = false, + nothrow = ALWAYS_FALSE, inaccessiblememonly = INACCESSIBLEMEM_OR_ARGMEMONLY) end @@ -2914,7 +2909,7 @@ function alloc_array_ndims(name::Symbol) end function alloc_array_effects(@specialize(abstract_eval), args::Vector{Any}, ndims::Int) - nothrow = alloc_array_nothrow(abstract_eval, args, ndims) + nothrow = alloc_array_nothrow(abstract_eval, args, ndims) ? ALWAYS_TRUE : ALWAYS_FALSE return Effects(EFFECTS_TOTAL; consistent=CONSISTENT_IF_NOTRETURNED, nothrow) end @@ -2933,7 +2928,7 @@ function alloc_array_nothrow(@specialize(abstract_eval), args::Vector{Any}, ndim end function new_array_effects(@specialize(abstract_eval), args::Vector{Any}) - nothrow = new_array_nothrow(abstract_eval, args) + nothrow = new_array_nothrow(abstract_eval, args) ? ALWAYS_TRUE : ALWAYS_FALSE return Effects(EFFECTS_TOTAL; consistent=CONSISTENT_IF_NOTRETURNED, nothrow) end diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index 77e1fd02de8d0..93695feb14e5e 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -491,7 +491,7 @@ function adjust_effects(sv::InferenceState) ipo_effects = Effects(ipo_effects; effect_free=ALWAYS_TRUE) end if is_effect_overridden(override, :nothrow) - ipo_effects = Effects(ipo_effects; nothrow=true) + ipo_effects = Effects(ipo_effects; nothrow=ALWAYS_TRUE) end if is_effect_overridden(override, :terminates_globally) ipo_effects = Effects(ipo_effects; terminates=true) diff --git a/base/reflection.jl b/base/reflection.jl index bcfc39d2bd3a8..2d5ecdb6d1725 100644 --- a/base/reflection.jl +++ b/base/reflection.jl @@ -1615,7 +1615,7 @@ function infer_effects(@nospecialize(f), @nospecialize(types=default_tt(f)); effects = Core.Compiler.EFFECTS_TOTAL if matches.ambig || !any(match::Core.MethodMatch->match.fully_covers, matches.matches) # account for the fact that we may encounter a MethodError with a non-covered or ambiguous signature. - effects = Core.Compiler.Effects(effects; nothrow=false) + effects = Core.Compiler.Effects(effects; nothrow=Core.Compiler.ALWAYS_FALSE) end for match in matches.matches match = match::Core.MethodMatch diff --git a/base/tuple.jl b/base/tuple.jl index 59fe2c1e531e1..62ce7c6446911 100644 --- a/base/tuple.jl +++ b/base/tuple.jl @@ -30,8 +30,6 @@ size(@nospecialize(t::Tuple), d::Integer) = (d == 1) ? length(t) : throw(Argumen axes(@nospecialize t::Tuple) = (OneTo(length(t)),) @eval getindex(@nospecialize(t::Tuple), i::Int) = getfield(t, i, $(Expr(:boundscheck))) @eval getindex(@nospecialize(t::Tuple), i::Integer) = getfield(t, convert(Int, i), $(Expr(:boundscheck))) -__inbounds_getindex(@nospecialize(t::Tuple), i::Int) = getfield(t, i, false) -__inbounds_getindex(@nospecialize(t::Tuple), i::Integer) = getfield(t, convert(Int, i), false) getindex(t::Tuple, r::AbstractArray{<:Any,1}) = (eltype(t)[t[ri] for ri in r]...,) getindex(t::Tuple, b::AbstractArray{Bool,1}) = length(b) == length(t) ? getindex(t, findall(b)) : throw(BoundsError(t, b)) getindex(t::Tuple, c::Colon) = t diff --git a/test/boundscheck_exec.jl b/test/boundscheck_exec.jl index f2eb2ea630893..5a21b481d2092 100644 --- a/test/boundscheck_exec.jl +++ b/test/boundscheck_exec.jl @@ -7,6 +7,8 @@ using Test, Random, InteractiveUtils @enum BCOption bc_default bc_on bc_off bc_opt = BCOption(Base.JLOptions().check_bounds) +include("./compiler/irutils.jl") + # test for boundscheck block eliminated at same level @inline function A1() r = 0 @@ -29,9 +31,12 @@ function A1_inbounds() end A1_wrap() = @inbounds return A1_inbounds() +# local `@inbounds`/`@boundscheck` expression should not prohibit concrete-evaluation +@test Core.Compiler.is_foldable(Base.infer_effects(A1_inbounds)) + if bc_opt == bc_default @test A1() == 1 - @test A1_inbounds() == 1 + @test A1_inbounds() == 0 @test A1_wrap() == 0 elseif bc_opt == bc_on @test A1() == 1 @@ -138,7 +143,7 @@ end B1_wrap() = @inbounds return B1() if bc_opt == bc_default - @test_throws BoundsError B1() + @test B1() == 0 @test B1_wrap() == 0 elseif bc_opt == bc_off @test B1() == 0 @@ -298,4 +303,40 @@ end typeintersect(Int, Integer) end |> only === Type{Int} +# a method containing `@boundscheck` should not be concrete-evaluated from a `@inbounds` context +function f_boundscheck_inconsistent(i) + @boundscheck i += 1 + return i +end +f_boundscheck_wrap1() = f_boundscheck_inconsistent(0) +f_boundscheck_wrap2() = @inbounds f_boundscheck_inconsistent(0) +if bc_opt == bc_default + @test !Base.infer_effects(f_boundscheck_inconsistent, (Int,)).noinbounds + @test fully_eliminated(; retval=1) do + f_boundscheck_wrap1() + end + @test f_boundscheck_wrap1() == 1 + @test f_boundscheck_wrap2() == 0 +elseif bc_opt == bc_off + @test Base.infer_effects(f_boundscheck_inconsistent, (Int,)).noinbounds + @test fully_eliminated(; retval=0) do + f_boundscheck_wrap1() + end + @test fully_eliminated(; retval=0) do + f_boundscheck_wrap2() + end + @test f_boundscheck_wrap1() == 0 + @test f_boundscheck_wrap2() == 0 +else + @test Base.infer_effects(f_boundscheck_inconsistent, (Int,)).noinbounds + @test fully_eliminated(; retval=1) do + f_boundscheck_wrap1() + end + @test fully_eliminated(; retval=1) do + f_boundscheck_wrap2() + end + @test f_boundscheck_wrap1() == 1 + @test f_boundscheck_wrap2() == 1 +end + end diff --git a/test/compiler/EscapeAnalysis/interprocedural.jl b/test/compiler/EscapeAnalysis/interprocedural.jl index 756e5489ed637..3ac825e513781 100644 --- a/test/compiler/EscapeAnalysis/interprocedural.jl +++ b/test/compiler/EscapeAnalysis/interprocedural.jl @@ -2,6 +2,8 @@ # =========== # EA works on pre-inlining IR +module IPOEATest + include(normpath(@__DIR__, "setup.jl")) # callsites @@ -260,3 +262,5 @@ let result = code_escapes() do r = only(findall(isreturn, result.ir.stmts.inst)) @test !has_return_escape(result.state[SSAValue(i)], r) end + +end # module IPOEATest diff --git a/test/compiler/EscapeAnalysis/local.jl b/test/compiler/EscapeAnalysis/local.jl index dd324c3619dc7..80e3fddfd9232 100644 --- a/test/compiler/EscapeAnalysis/local.jl +++ b/test/compiler/EscapeAnalysis/local.jl @@ -2,6 +2,8 @@ # ============= # EA works on post-inlining IR +module LocalEATest + include(normpath(@__DIR__, "setup.jl")) @testset "basics" begin @@ -1997,9 +1999,9 @@ let result = code_escapes((Int,String,)) do n,s i = only(findall(isarrayalloc, result.ir.stmts.inst)) r = only(findall(isreturn, result.ir.stmts.inst)) @test has_return_escape(result.state[SSAValue(i)], r) - @test !has_thrown_escape(result.state[SSAValue(i)]) + # @test !has_thrown_escape(result.state[SSAValue(i)]) @test has_return_escape(result.state[Argument(3)], r) # s - @test !has_thrown_escape(result.state[Argument(3)]) # s + # @test !has_thrown_escape(result.state[Argument(3)]) # s end let result = code_escapes((Int,String,)) do n,s xs = String[] @@ -2011,9 +2013,9 @@ let result = code_escapes((Int,String,)) do n,s i = only(findall(isarrayalloc, result.ir.stmts.inst)) r = only(findall(isreturn, result.ir.stmts.inst)) @test has_return_escape(result.state[SSAValue(i)], r) # xs - @test !has_thrown_escape(result.state[SSAValue(i)]) # xs + # @test !has_thrown_escape(result.state[SSAValue(i)]) # xs @test has_return_escape(result.state[Argument(3)], r) # s - @test !has_thrown_escape(result.state[Argument(3)]) # s + # @test !has_thrown_escape(result.state[Argument(3)]) # s end let result = code_escapes((String,String,String)) do s, t, u xs = String[] @@ -2204,3 +2206,5 @@ end # end # return m # end + +end # module LocalEATest diff --git a/test/compiler/effects.jl b/test/compiler/effects.jl index 99e788c0cff12..dbdf5ffb954f7 100644 --- a/test/compiler/effects.jl +++ b/test/compiler/effects.jl @@ -337,17 +337,6 @@ invoke44763(x) = @invoke increase_x44763!(x) end |> only === Int @test x44763 == 0 -# `@inbounds`/`@boundscheck` expression should taint :consistent-cy correctly -# https://github.com/JuliaLang/julia/issues/48099 -function A1_inbounds() - r = 0 - @inbounds begin - @boundscheck r += 1 - end - return r -end -@test !Core.Compiler.is_consistent(Base.infer_effects(A1_inbounds)) - # Test that purity doesn't try to accidentally run unreachable code due to # boundscheck elimination function f_boundscheck_elim(n) @@ -394,7 +383,8 @@ end |> !Core.Compiler.is_foldable entry_to_be_invalidated('a') end -@test !Core.Compiler.builtin_nothrow(Core.Compiler.fallback_lattice, Core.get_binding_type, Any[Rational{Int}, Core.Const(:foo)], Any) +@test !Core.Compiler.builtin_nothrow(Bool, Core.Compiler.fallback_lattice, + Core.get_binding_type, Any[Rational{Int}, Core.Const(:foo)], Any) # Nothrow for assignment to globals global glob_assign_int::Int = 0 @@ -479,17 +469,6 @@ end return getfield(obj, :value) end |> Core.Compiler.is_consistent -# getfield is nothrow when bounds checking is turned off -@test Base.infer_effects((Tuple{Int,Int},Int)) do t, i - getfield(t, i, false) -end |> Core.Compiler.is_nothrow -@test Base.infer_effects((Tuple{Int,Int},Symbol)) do t, i - getfield(t, i, false) -end |> Core.Compiler.is_nothrow -@test Base.infer_effects((Tuple{Int,Int},String)) do t, i - getfield(t, i, false) # invalid name type -end |> !Core.Compiler.is_nothrow - @test Core.Compiler.is_consistent(Base.infer_effects(setindex!, (Base.RefValue{Int}, Int))) # :inaccessiblememonly effect @@ -680,6 +659,33 @@ end end @test !Core.Compiler.is_removable_if_unused(Base.infer_effects(unremovable_if_unused3!)) +# assume `:nothrow`-ness of `getfield` when applied `@inbounds` +mygetproperty(x, f) = getfield(x, f) +for FT = Any[Symbol,Int], func = Any[getfield, mygetproperty] + @testset let FT = FT, func = func + @test Base.infer_effects((Some{Any}, FT)) do x, f + func(x, f) + end |> Core.Compiler.is_nothrow_if_inbounds + @test Base.infer_effects((Some{Any}, FT)) do x, f + @inbounds func(x, f) + end |> Core.Compiler.is_nothrow + end +end + +struct GetfieldExhaustiveCheck{T} + a + b + c::T + GetfieldExhaustiveCheck(a, b) = new{Any}(a, b) + GetfieldExhaustiveCheck(a, b, c::T) where T = new{T}(a, b, c) +end +@test Base.infer_effects((GetfieldExhaustiveCheck{Int}, Symbol)) do x, f + getfield(x, f) +end |> Core.Compiler.is_nothrow_if_inbounds +@test Base.infer_effects((GetfieldExhaustiveCheck{Any}, Symbol)) do x, f + getfield(x, f) # the `c` field may be uninitialized +end |> !Core.Compiler.is_nothrow_if_inbounds + # array ops # ========= @@ -762,39 +768,47 @@ end for tt = Any[(Bool,Vector{Any},Int), (Bool,Matrix{Any},Int,Int)] - @testset let effects = Base.infer_effects(Base.arrayref, tt) + @testset let tt = tt, + effects = Base.infer_effects(Base.arrayref, tt) @test Core.Compiler.is_consistent_if_inaccessiblememonly(effects) @test Core.Compiler.is_effect_free(effects) @test !Core.Compiler.is_nothrow(effects) @test Core.Compiler.is_terminates(effects) end end +@test Core.Compiler.is_nothrow_if_inbounds(Base.infer_effects(Base.arrayref, (Bool,Vector{Int},Int))) +@test !Core.Compiler.is_nothrow_if_inbounds(Base.infer_effects(Base.arrayref, (Bool,Vector{Any},Int))) # may raise `UndefRefError` also + +# assume :nothrow-ness of `arrayref` in `@inbounds` region +@test @eval Base.infer_effects((Vector{Int},Int,)) do a, i + @inbounds Base.arrayref($(Expr(:boundscheck)), a, i) +end |> Core.Compiler.is_nothrow +@test Base.infer_effects((Vector{Int},Int)) do a, i + @inbounds a[i] +end |> Core.Compiler.is_nothrow # arrayset # -------- for tt = Any[(Bool,Vector{Any},Any,Int), (Bool,Matrix{Any},Any,Int,Int)] - @testset let effects = Base.infer_effects(Base.arrayset, tt) + @testset let tt = tt, + effects = Base.infer_effects(Base.arrayset, tt) @test Core.Compiler.is_consistent_if_inaccessiblememonly(effects) @test Core.Compiler.is_effect_free_if_inaccessiblememonly(effects) @test !Core.Compiler.is_nothrow(effects) @test Core.Compiler.is_terminates(effects) end end -# nothrow for arrayset -@test Base.infer_effects((Vector{Int},Int,Int)) do a, v, i - Base.arrayset(true, a, v, i) -end |> !Core.Compiler.is_nothrow -@test Base.infer_effects((Vector{Int},Int,Int)) do a, v, i - a[i] = v # may throw -end |> !Core.Compiler.is_nothrow -# when bounds checking is turned off, it should be safe -@test Base.infer_effects((Vector{Int},Int,Int)) do a, v, i - Base.arrayset(false, a, v, i) +@test Core.Compiler.is_nothrow_if_inbounds(Base.infer_effects(Base.arrayset, (Bool,Vector{Int},Int,Int))) +@test Core.Compiler.is_nothrow_if_inbounds(Base.infer_effects(Base.arrayset, (Bool,Vector{Any},Any,Int))) + +# assume :nothrow-ness of `arrayset` in `@inbounds` region +@test @eval Base.infer_effects((Vector{Int},Int,Int)) do a, v, i + @inbounds Base.arrayset($(Expr(:boundscheck)), a, v, i) end |> Core.Compiler.is_nothrow -@test Base.infer_effects((Vector{Number},Number,Int)) do a, v, i - Base.arrayset(false, a, v, i) +@test Base.infer_effects((Vector{Int},Int,Int)) do a, v, i + @inbounds a[i] = v end |> Core.Compiler.is_nothrow # arraysize @@ -885,21 +899,6 @@ end |> Core.Compiler.is_foldable return WrapperOneField == (WrapperOneField{T} where T) end |> Core.Compiler.is_foldable_nothrow -# Test that dead `@inbounds` does not taint consistency -# https://github.com/JuliaLang/julia/issues/48243 -@test Base.infer_effects(Tuple{Int64}) do i - false && @inbounds (1,2,3)[i] - return 1 -end |> Core.Compiler.is_foldable_nothrow - -@test Base.infer_effects(Tuple{Int64}) do i - @inbounds (1,2,3)[i] -end |> !Core.Compiler.is_consistent - -@test Base.infer_effects(Tuple{Tuple{Int64}}) do x - @inbounds x[1] -end |> Core.Compiler.is_foldable_nothrow - # Test that :new of non-concrete, but otherwise known type # does not taint consistency. @eval struct ImmutRef{T} diff --git a/test/compiler/inference.jl b/test/compiler/inference.jl index da5772744607d..89d92d1e95f67 100644 --- a/test/compiler/inference.jl +++ b/test/compiler/inference.jl @@ -729,17 +729,17 @@ let fieldtype_tfunc(@nospecialize args...) = @test TypeVar <: fieldtype_tfunc(Any, Any) end -import Core.Compiler: MaybeUndef, builtin_nothrow +using Core.Compiler: MaybeUndef, builtin_nothrow let 𝕃ₒ = Core.Compiler.OptimizerLattice() - @test !builtin_nothrow(𝕃ₒ, setfield!, Any[Base.RefValue{String}, Core.Const(:x), MaybeUndef(String)], Any) - @test !builtin_nothrow(𝕃ₒ, setfield!, Any[Base.RefValue{String}, Core.Const(:x), MaybeUndef(String), Core.Const(:not_atomic)], Any) - @test !builtin_nothrow(𝕃ₒ, isdefined, Any[Any,MaybeUndef(Symbol)], Bool) - @test !builtin_nothrow(𝕃ₒ, fieldtype, Any[MaybeUndef(Any),Symbol], Any) - @test !builtin_nothrow(𝕃ₒ, isa, Any[Type,MaybeUndef(Type)], Any) - @test !builtin_nothrow(𝕃ₒ, <:, Any[MaybeUndef(Any),MaybeUndef(Any)], Any) - @test !builtin_nothrow(𝕃ₒ, Core.ifelse, Any[MaybeUndef(Bool),Any,Any], Any) - @test !builtin_nothrow(𝕃ₒ, typeassert, Any[MaybeUndef(Any),Type{Symbol}], Any) - @test !builtin_nothrow(𝕃ₒ, Core.get_binding_type, Any[Module,MaybeUndef(Symbol)], Any) + @test !builtin_nothrow(Bool, 𝕃ₒ, setfield!, Any[Base.RefValue{String}, Core.Const(:x), MaybeUndef(String)], Any) + @test !builtin_nothrow(Bool, 𝕃ₒ, setfield!, Any[Base.RefValue{String}, Core.Const(:x), MaybeUndef(String), Core.Const(:not_atomic)], Any) + @test !builtin_nothrow(Bool, 𝕃ₒ, isdefined, Any[Any,MaybeUndef(Symbol)], Bool) + @test !builtin_nothrow(Bool, 𝕃ₒ, fieldtype, Any[MaybeUndef(Any),Symbol], Any) + @test !builtin_nothrow(Bool, 𝕃ₒ, isa, Any[Type,MaybeUndef(Type)], Any) + @test !builtin_nothrow(Bool, 𝕃ₒ, <:, Any[MaybeUndef(Any),MaybeUndef(Any)], Any) + @test !builtin_nothrow(Bool, 𝕃ₒ, Core.ifelse, Any[MaybeUndef(Bool),Any,Any], Any) + @test !builtin_nothrow(Bool, 𝕃ₒ, typeassert, Any[MaybeUndef(Any),Type{Symbol}], Any) + @test !builtin_nothrow(Bool, 𝕃ₒ, Core.get_binding_type, Any[Module,MaybeUndef(Symbol)], Any) end # issue #11480