From 8bdf1fa27c19790856960090e3543f2f9d7e641b Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki <40514306+aviatesk@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:00:07 +0900 Subject: [PATCH] effects: add new `@consistent_overlay` macro (#54322) This PR serves to replace #51080 and close #52940. It extends the `:nonoverlayed` to `UInt8` and introduces the `CONSISTENT_OVERLAY` effect bit, allowing for concrete evaluation of overlay methods using the original non-overlayed counterparts when applied. This newly added `:nonoverlayed`-bit is enabled through the newly added `Base.Experimental.@consistent_overlay mt def` macro. `@consistent_overlay` is similar to `@overlay`, but it sets the `:nonoverlayed`-bit to `CONSISTENT_OVERLAY` for the target method definition, allowing the method to be concrete-evaluated. To use this feature safely, I have also added quite precise documentation to `@consistent_overlay`. --- base/boot.jl | 3 +- base/compiler/abstractinterpretation.jl | 14 +++- base/compiler/compiler.jl | 11 ++- base/compiler/effects.jl | 59 +++++++++------ base/compiler/inferencestate.jl | 5 +- base/compiler/ssair/show.jl | 3 +- base/compiler/typeinfer.jl | 3 + base/essentials.jl | 36 ++++++--- base/experimental.jl | 98 +++++++++++++++++++++++-- base/expr.jl | 17 +++-- src/julia.h | 14 ++-- src/method.c | 30 +++++--- test/compiler/AbstractInterpreter.jl | 85 +++++++++++++++++---- test/namedtuple.jl | 4 +- 14 files changed, 291 insertions(+), 91 deletions(-) diff --git a/base/boot.jl b/base/boot.jl index bfee3c17336bc..e8156b325a843 100644 --- a/base/boot.jl +++ b/base/boot.jl @@ -282,7 +282,8 @@ macro _foldable_meta() #=:notaskstate=#true, #=:inaccessiblememonly=#true, #=:noub=#true, - #=:noub_if_noinbounds=#false)) + #=:noub_if_noinbounds=#false, + #=:consistent_overlay=#false)) end macro inline() Expr(:meta, :inline) end diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index b13df848ce605..ba8a352c1b528 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -491,7 +491,7 @@ function add_call_backedges!(interp::AbstractInterpreter, @nospecialize(rettype) # ignore the `:nonoverlayed` property if `interp` doesn't use overlayed method table # since it will never be tainted anyway if !isoverlayed(method_table(interp)) - all_effects = Effects(all_effects; nonoverlayed=false) + all_effects = Effects(all_effects; nonoverlayed=ALWAYS_FALSE) end all_effects === Effects() && return nothing end @@ -889,7 +889,15 @@ function concrete_eval_eligible(interp::AbstractInterpreter, mi = result.edge if mi !== nothing && is_foldable(effects) if f !== nothing && is_all_const_arg(arginfo, #=start=#2) - if is_nonoverlayed(interp) || is_nonoverlayed(effects) + if (is_nonoverlayed(interp) || is_nonoverlayed(effects) || + # Even if overlay methods are involved, when `:consistent_overlay` is + # explicitly applied, we can still perform concrete evaluation using the + # original methods for executing them. + # While there's a chance that the non-overlayed counterparts may raise + # non-egal exceptions, it will not impact the compilation validity, since: + # - the results of the concrete evaluation will not be inlined + # - the exception types from the concrete evaluation will not be propagated + is_consistent_overlay(effects)) return :concrete_eval end # disable concrete-evaluation if this function call is tainted by some overlayed @@ -2770,7 +2778,7 @@ function override_effects(effects::Effects, override::EffectsOverride) notaskstate = override.notaskstate ? true : effects.notaskstate, inaccessiblememonly = override.inaccessiblememonly ? ALWAYS_TRUE : effects.inaccessiblememonly, noub = override.noub ? ALWAYS_TRUE : - override.noub_if_noinbounds && effects.noub !== ALWAYS_TRUE ? NOUB_IF_NOINBOUNDS : + (override.noub_if_noinbounds && effects.noub !== ALWAYS_TRUE) ? NOUB_IF_NOINBOUNDS : effects.noub) end diff --git a/base/compiler/compiler.jl b/base/compiler/compiler.jl index 8b9c26be2ec81..4e2fe3da3496f 100644 --- a/base/compiler/compiler.jl +++ b/base/compiler/compiler.jl @@ -47,10 +47,11 @@ struct EffectsOverride inaccessiblememonly::Bool noub::Bool noub_if_noinbounds::Bool + consistent_overlay::Bool end function EffectsOverride( override::EffectsOverride = - EffectsOverride(false, false, false, false, false, false, false, false, false); + EffectsOverride(false, false, false, false, false, false, false, false, false, false); consistent::Bool = override.consistent, effect_free::Bool = override.effect_free, nothrow::Bool = override.nothrow, @@ -59,7 +60,8 @@ function EffectsOverride( notaskstate::Bool = override.notaskstate, inaccessiblememonly::Bool = override.inaccessiblememonly, noub::Bool = override.noub, - noub_if_noinbounds::Bool = override.noub_if_noinbounds) + noub_if_noinbounds::Bool = override.noub_if_noinbounds, + consistent_overlay::Bool = override.consistent_overlay) return EffectsOverride( consistent, effect_free, @@ -69,9 +71,10 @@ function EffectsOverride( notaskstate, inaccessiblememonly, noub, - noub_if_noinbounds) + noub_if_noinbounds, + consistent_overlay) end -const NUM_EFFECTS_OVERRIDES = 9 # sync with julia.h +const NUM_EFFECTS_OVERRIDES = 10 # sync with julia.h # essential files and libraries include("essentials.jl") diff --git a/base/compiler/effects.jl b/base/compiler/effects.jl index a3d30baef9efa..0375b8dba922c 100644 --- a/base/compiler/effects.jl +++ b/base/compiler/effects.jl @@ -43,16 +43,21 @@ following meanings: except that it may access or modify mutable memory pointed to by its call arguments. This may later be refined to `ALWAYS_TRUE` in a case when call arguments are known to be immutable. This state corresponds to LLVM's `inaccessiblemem_or_argmemonly` function attribute. -- `noub::UInt8`: indicates that the method will not execute any undefined behavior (for any input). - Note that undefined behavior may technically cause the method to violate any other effect - assertions (such as `:consistent` or `:effect_free`) as well, but we do not model this, - and they assume the absence of undefined behavior. - * `ALWAYS_TRUE`: this method is guaranteed to not execute any undefined behavior. +- `noub::UInt8`: + * `ALWAYS_TRUE`: this method is guaranteed to not execute any undefined behavior (for any input). * `ALWAYS_FALSE`: this method may execute undefined behavior. * `NOUB_IF_NOINBOUNDS`: this method is guaranteed to not execute any undefined behavior if the caller does not set nor propagate the `@inbounds` context. -- `nonoverlayed::Bool`: indicates that any methods that may be called within this method - are not defined in an [overlayed method table](@ref OverlayMethodTable). + Note that undefined behavior may technically cause the method to violate any other effect + assertions (such as `:consistent` or `:effect_free`) as well, but we do not model this, + and they assume the absence of undefined behavior. +- `nonoverlayed::UInt8`: + * `ALWAYS_TRUE`: this method is guaranteed to not invoke any methods that defined in an + [overlayed method table](@ref OverlayMethodTable). + * `CONSISTENT_OVERLAY`: this method may invoke overlayed methods, but all such overlayed + methods are `:consistent` with their non-overlayed original counterparts + (see [`Base.@assume_effects`](@ref) for the exact definition of `:consistenct`-cy). + * `ALWAYS_FALSE`: this method may invoke overlayed methods. Note that the representations above are just internal implementation details and thus likely to change in the future. See [`Base.@assume_effects`](@ref) for more detailed explanation @@ -94,8 +99,10 @@ The output represents the state of different effect properties in the following - `+u` (green): `true` - `-u` (red): `false` - `?u` (yellow): `NOUB_IF_NOINBOUNDS` - -Additionally, if the `nonoverlayed` property is false, a red prime symbol (′) is displayed after the tuple. +8. `:nonoverlayed` (`o`): + - `+o` (green): `ALWAYS_TRUE` + - `-o` (red): `ALWAYS_FALSE` + - `?o` (yellow): `CONSISTENT_OVERLAY` """ struct Effects consistent::UInt8 @@ -105,7 +112,7 @@ struct Effects notaskstate::Bool inaccessiblememonly::UInt8 noub::UInt8 - nonoverlayed::Bool + nonoverlayed::UInt8 function Effects( consistent::UInt8, effect_free::UInt8, @@ -114,7 +121,7 @@ struct Effects notaskstate::Bool, inaccessiblememonly::UInt8, noub::UInt8, - nonoverlayed::Bool) + nonoverlayed::UInt8) return new( consistent, effect_free, @@ -150,10 +157,13 @@ const INACCESSIBLEMEM_OR_ARGMEMONLY = 0x01 << 1 # :noub bits const NOUB_IF_NOINBOUNDS = 0x01 << 1 -const EFFECTS_TOTAL = Effects(ALWAYS_TRUE, ALWAYS_TRUE, true, true, true, ALWAYS_TRUE, ALWAYS_TRUE, true) -const EFFECTS_THROWS = Effects(ALWAYS_TRUE, ALWAYS_TRUE, false, true, true, ALWAYS_TRUE, ALWAYS_TRUE, true) -const EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, ALWAYS_FALSE, true) # unknown mostly, but it's not overlayed at least (e.g. it's not a call) -const _EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, ALWAYS_FALSE, false) # unknown really +# :nonoverlayed bits +const CONSISTENT_OVERLAY = 0x01 << 1 + +const EFFECTS_TOTAL = Effects(ALWAYS_TRUE, ALWAYS_TRUE, true, true, true, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE) +const EFFECTS_THROWS = Effects(ALWAYS_TRUE, ALWAYS_TRUE, false, true, true, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE) +const EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, ALWAYS_FALSE, ALWAYS_TRUE) # unknown mostly, but it's not overlayed at least (e.g. it's not a call) +const _EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, ALWAYS_FALSE, ALWAYS_FALSE) # unknown really function Effects(effects::Effects = _EFFECTS_UNKNOWN; consistent::UInt8 = effects.consistent, @@ -163,7 +173,7 @@ function Effects(effects::Effects = _EFFECTS_UNKNOWN; notaskstate::Bool = effects.notaskstate, inaccessiblememonly::UInt8 = effects.inaccessiblememonly, noub::UInt8 = effects.noub, - nonoverlayed::Bool = effects.nonoverlayed) + nonoverlayed::UInt8 = effects.nonoverlayed) return Effects( consistent, effect_free, @@ -229,8 +239,11 @@ function is_better_effects(new::Effects, old::Effects) elseif new.noub != old.noub return false end - if new.nonoverlayed - any_improved |= !old.nonoverlayed + if new.nonoverlayed == ALWAYS_TRUE + any_improved |= old.nonoverlayed != ALWAYS_TRUE + elseif new.nonoverlayed == CONSISTENT_OVERLAY + old.nonoverlayed == ALWAYS_TRUE && return false + any_improved |= old.nonoverlayed != CONSISTENT_OVERLAY elseif new.nonoverlayed != old.nonoverlayed return false end @@ -265,7 +278,7 @@ is_notaskstate(effects::Effects) = effects.notaskstate is_inaccessiblememonly(effects::Effects) = effects.inaccessiblememonly === ALWAYS_TRUE is_noub(effects::Effects) = effects.noub === ALWAYS_TRUE is_noub_if_noinbounds(effects::Effects) = effects.noub === NOUB_IF_NOINBOUNDS -is_nonoverlayed(effects::Effects) = effects.nonoverlayed +is_nonoverlayed(effects::Effects) = effects.nonoverlayed === ALWAYS_TRUE # implies `is_notaskstate` & `is_inaccessiblememonly`, but not explicitly checked here is_foldable(effects::Effects) = @@ -295,6 +308,8 @@ is_effect_free_if_inaccessiblememonly(effects::Effects) = !iszero(effects.effect is_inaccessiblemem_or_argmemonly(effects::Effects) = effects.inaccessiblememonly === INACCESSIBLEMEM_OR_ARGMEMONLY +is_consistent_overlay(effects::Effects) = effects.nonoverlayed === CONSISTENT_OVERLAY + function encode_effects(e::Effects) return ((e.consistent % UInt32) << 0) | ((e.effect_free % UInt32) << 3) | @@ -315,7 +330,7 @@ function decode_effects(e::UInt32) _Bool((e >> 7) & 0x01), UInt8((e >> 8) & 0x03), UInt8((e >> 10) & 0x03), - _Bool((e >> 12) & 0x01)) + UInt8((e >> 12) & 0x03)) end function encode_effects_override(eo::EffectsOverride) @@ -329,6 +344,7 @@ function encode_effects_override(eo::EffectsOverride) eo.inaccessiblememonly && (e |= (0x0001 << 6)) eo.noub && (e |= (0x0001 << 7)) eo.noub_if_noinbounds && (e |= (0x0001 << 8)) + eo.consistent_overlay && (e |= (0x0001 << 9)) return e end @@ -342,7 +358,8 @@ function decode_effects_override(e::UInt16) !iszero(e & (0x0001 << 5)), !iszero(e & (0x0001 << 6)), !iszero(e & (0x0001 << 7)), - !iszero(e & (0x0001 << 8))) + !iszero(e & (0x0001 << 8)), + !iszero(e & (0x0001 << 9))) end decode_statement_effects_override(ssaflag::UInt32) = diff --git a/base/compiler/inferencestate.jl b/base/compiler/inferencestate.jl index f08fbac8af360..9dd676685cfda 100644 --- a/base/compiler/inferencestate.jl +++ b/base/compiler/inferencestate.jl @@ -329,7 +329,10 @@ mutable struct InferenceState end if def isa Method - ipo_effects = Effects(ipo_effects; nonoverlayed=is_nonoverlayed(def)) + nonoverlayed = is_nonoverlayed(def) ? ALWAYS_TRUE : + is_effect_overridden(def, :consistent_overlay) ? CONSISTENT_OVERLAY : + ALWAYS_FALSE + ipo_effects = Effects(ipo_effects; nonoverlayed) end restrict_abstract_call_sites = isa(def, Module) diff --git a/base/compiler/ssair/show.jl b/base/compiler/ssair/show.jl index 3936a82a6560e..873ab2c68f7a8 100644 --- a/base/compiler/ssair/show.jl +++ b/base/compiler/ssair/show.jl @@ -1019,8 +1019,9 @@ function Base.show(io::IO, e::Effects) printstyled(io, effectbits_letter(e, :inaccessiblememonly, 'm'); color=effectbits_color(e, :inaccessiblememonly)) print(io, ',') printstyled(io, effectbits_letter(e, :noub, 'u'); color=effectbits_color(e, :noub)) + print(io, ',') + printstyled(io, effectbits_letter(e, :nonoverlayed, 'o'); color=effectbits_color(e, :nonoverlayed)) print(io, ')') - e.nonoverlayed || printstyled(io, '′'; color=:red) end @specialize diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index 8b049efa241e7..83575cd6f9278 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -492,6 +492,9 @@ function adjust_effects(ipo_effects::Effects, def::Method) elseif is_effect_overridden(override, :noub_if_noinbounds) && ipo_effects.noub !== ALWAYS_TRUE ipo_effects = Effects(ipo_effects; noub=NOUB_IF_NOINBOUNDS) end + if is_effect_overridden(override, :consistent_overlay) + ipo_effects = Effects(ipo_effects; nonoverlayed=CONSISTENT_OVERLAY) + end return ipo_effects end diff --git a/base/essentials.jl b/base/essentials.jl index 5fdc718085df0..45f0829da66df 100644 --- a/base/essentials.jl +++ b/base/essentials.jl @@ -212,7 +212,8 @@ macro _total_meta() #=:notaskstate=#true, #=:inaccessiblememonly=#true, #=:noub=#true, - #=:noub_if_noinbounds=#false)) + #=:noub_if_noinbounds=#false, + #=:consistent_overlay=#false)) end # can be used in place of `@assume_effects :foldable` (supposed to be used for bootstrapping) macro _foldable_meta() @@ -225,7 +226,8 @@ macro _foldable_meta() #=:notaskstate=#true, #=:inaccessiblememonly=#true, #=:noub=#true, - #=:noub_if_noinbounds=#false)) + #=:noub_if_noinbounds=#false, + #=:consistent_overlay=#false)) end # can be used in place of `@assume_effects :terminates_locally` (supposed to be used for bootstrapping) macro _terminates_locally_meta() @@ -238,7 +240,8 @@ macro _terminates_locally_meta() #=:notaskstate=#false, #=:inaccessiblememonly=#false, #=:noub=#false, - #=:noub_if_noinbounds=#false)) + #=:noub_if_noinbounds=#false, + #=:consistent_overlay=#false)) end # can be used in place of `@assume_effects :terminates_globally` (supposed to be used for bootstrapping) macro _terminates_globally_meta() @@ -251,7 +254,8 @@ macro _terminates_globally_meta() #=:notaskstate=#false, #=:inaccessiblememonly=#false, #=:noub=#false, - #=:noub_if_noinbounds=#false)) + #=:noub_if_noinbounds=#false, + #=:consistent_overlay=#false)) end # can be used in place of `@assume_effects :terminates_globally :notaskstate` (supposed to be used for bootstrapping) macro _terminates_globally_notaskstate_meta() @@ -264,7 +268,8 @@ macro _terminates_globally_notaskstate_meta() #=:notaskstate=#true, #=:inaccessiblememonly=#false, #=:noub=#false, - #=:noub_if_noinbounds=#false)) + #=:noub_if_noinbounds=#false, + #=:consistent_overlay=#false)) end # can be used in place of `@assume_effects :terminates_globally :noub` (supposed to be used for bootstrapping) macro _terminates_globally_noub_meta() @@ -277,7 +282,8 @@ macro _terminates_globally_noub_meta() #=:notaskstate=#false, #=:inaccessiblememonly=#false, #=:noub=#true, - #=:noub_if_noinbounds=#false)) + #=:noub_if_noinbounds=#false, + #=:consistent_overlay=#false)) end # can be used in place of `@assume_effects :effect_free :terminates_locally` (supposed to be used for bootstrapping) macro _effect_free_terminates_locally_meta() @@ -290,7 +296,8 @@ macro _effect_free_terminates_locally_meta() #=:notaskstate=#false, #=:inaccessiblememonly=#false, #=:noub=#false, - #=:noub_if_noinbounds=#false)) + #=:noub_if_noinbounds=#false, + #=:consistent_overlay=#false)) end # can be used in place of `@assume_effects :nothrow :noub` (supposed to be used for bootstrapping) macro _nothrow_noub_meta() @@ -303,7 +310,8 @@ macro _nothrow_noub_meta() #=:notaskstate=#false, #=:inaccessiblememonly=#false, #=:noub=#true, - #=:noub_if_noinbounds=#false)) + #=:noub_if_noinbounds=#false, + #=:consistent_overlay=#false)) end # can be used in place of `@assume_effects :nothrow` (supposed to be used for bootstrapping) macro _nothrow_meta() @@ -316,7 +324,8 @@ macro _nothrow_meta() #=:notaskstate=#false, #=:inaccessiblememonly=#false, #=:noub=#false, - #=:noub_if_noinbounds=#false)) + #=:noub_if_noinbounds=#false, + #=:consistent_overlay=#false)) end # can be used in place of `@assume_effects :nothrow` (supposed to be used for bootstrapping) macro _noub_meta() @@ -329,7 +338,8 @@ macro _noub_meta() #=:notaskstate=#false, #=:inaccessiblememonly=#false, #=:noub=#true, - #=:noub_if_noinbounds=#false)) + #=:noub_if_noinbounds=#false, + #=:consistent_overlay=#false)) end # can be used in place of `@assume_effects :notaskstate` (supposed to be used for bootstrapping) macro _notaskstate_meta() @@ -342,7 +352,8 @@ macro _notaskstate_meta() #=:notaskstate=#true, #=:inaccessiblememonly=#false, #=:noub=#false, - #=:noub_if_noinbounds=#false)) + #=:noub_if_noinbounds=#false, + #=:consistent_overlay=#false)) end # can be used in place of `@assume_effects :noub_if_noinbounds` (supposed to be used for bootstrapping) macro _noub_if_noinbounds_meta() @@ -355,7 +366,8 @@ macro _noub_if_noinbounds_meta() #=:notaskstate=#false, #=:inaccessiblememonly=#false, #=:noub=#false, - #=:noub_if_noinbounds=#true)) + #=:noub_if_noinbounds=#true, + #=:consistent_overlay=#false)) end # another version of inlining that propagates an inbounds context diff --git a/base/experimental.jl b/base/experimental.jl index 8dbdcacd65376..58c7258120f3f 100644 --- a/base/experimental.jl +++ b/base/experimental.jl @@ -318,7 +318,7 @@ function show_error_hints(io, ex, args...) isnothing(hinters) && return for handler in hinters try - Base.invokelatest(handler, io, ex, args...) + @invokelatest handler(io, ex, args...) catch err tn = typeof(handler).name @error "Hint-handler $handler for $(typeof(ex)) in $(tn.module) caused an error" @@ -330,17 +330,99 @@ end include("opaque_closure.jl") """ - Experimental.@overlay mt [function def] + Base.Experimental.@overlay mt def Define a method and add it to the method table `mt` instead of to the global method table. This can be used to implement a method override mechanism. Regular compilation will not consider these methods, and you should customize the compilation flow to look in these method tables (e.g., using [`Core.Compiler.OverlayMethodTable`](@ref)). + +!!! note + Please be aware that when defining overlay methods using `@overlay`, it is not necessary + to have an original method that corresponds exactly in terms of how the method dispatches. + This means that the method overlay mechanism enabled by `@overlay` is not implemented by + replacing the methods themselves, but through an additional and prioritized method + lookup during the method dispatch. + + Considering this, it is important to understand that in compilations using an overlay + method table like the following, the method dispatched by `callx(x)` is not the regular + method `callx(::Float64)`, but the overlay method `callx(x::Real)`: + ```julia + callx(::Real) = :real + @overlay SOME_OVERLAY_MT callx(::Real) = :overlay_real + callx(::Float64) = :float64 + + # some overlay callsite + let x::Float64 + callx(x) #> :overlay_real + end + ``` """ macro overlay(mt, def) - def = macroexpand(__module__, def) # to expand @inline, @generated, etc - is_function_def(def) || error("@overlay requires a function definition") - return esc(overlay_def!(mt, def)) + inner = Base.unwrap_macrocalls(def) + is_function_def(inner) || error("@overlay requires a function definition") + overlay_def!(mt, inner) + return esc(def) +end + +""" + Base.Experimental.@consistent_overlay mt def + +This macro operates almost identically to [`Base.Experimental.@overlay`](@ref), defining a +new overlay method. The key difference with this macro is that it informs the compiler that +the invocation of the overlay method it defines is `:consistent` with a regular, +non-overlayed method call. + +More formally, when evaluating a generic function call ``f(x)`` at a specific world age +``i``, if a regular method call ``fᵢ(x)`` is redirected to an overlay method call ``fᵢ′(x)`` +defined by this macro, ``fᵢ(x)`` and ``fᵢ′(x)`` are considered `:consistent` if the following +conditions are met: +- If ``fᵢ(x)`` returns a value ``y``, then ``fᵢ′(x)`` also returns some value ``yᵢ``, and ``y ≡ yᵢ`` holds. +- If ``fᵢ(x)`` throws an exception, then ``fᵢ′(x)`` also throws some exception. + +For a detailed definition of `:consistent`-cy, consult the corresponding section in +[`Base.@assume_effects`](@ref). + +!!! note + Note that the requirements for `:consistent`-cy include not only that the return values + are egal, but also that the manner of termination is the same. However, it's important + to aware that when they throw exceptions, the exceptions themselves don't necessarily + have to be egal. In other words, if ``fᵢ(x)`` throws an exception, ``fᵢ′(x)`` is + required to also throw one, but the exact exceptions may differ. + +!!! note + Please note that the `:consistent`-cy requirement applies not to method itself but to + _method invocation_. This means that for the use of `@consistent_overlay`, it is + necessary for method invocations with the native regular compilation and those with + a compilation with overlay method table to be `:consistent`. + + For example, it is important to understand that, `@consistent_overlay` can be used like + the following: + ```julia + callsin(x::Real) = x < 0 ? error(x) : sin(x) + @consistent_overlay SOME_OVERLAY_MT callsin(x::Float64) = + x < 0 ? error_somehow(x) : sin(x) + ``` + However, be aware that this `@consistent_overlay` will immediately become invalid if a + new method for `callsin` is defined subsequently, such as: + ```julia + callsin(x::Float64) = cos(x) + ``` + + This specifically implies that the use of `@consistent_overlay` should be restricted as + much as possible to cases where a regular method with a concrete signature is replaced + by an overlay method with the same concrete signature. + + This constraint is closely related to the note in [`Base.Experimental.@overlay`](@ref); + you are advised to consult that as well. +""" +macro consistent_overlay(mt, def) + inner = Base.unwrap_macrocalls(def) + is_function_def(inner) || error("@consistent_overlay requires a function definition") + overlay_def!(mt, inner) + override = Core.Compiler.EffectsOverride(; consistent_overlay=true) + Base.pushmeta!(def::Expr, Base.form_purity_expr(override)) + return esc(def) end function overlay_def!(mt, @nospecialize ex) @@ -367,11 +449,11 @@ let new_mt(name::Symbol, mod::Module) = begin end """ - Experimental.@MethodTable(name) + Base.Experimental.@MethodTable name Create a new MethodTable in the current module, bound to `name`. This method table can be -used with the [`Experimental.@overlay`](@ref) macro to define methods for a function without -adding them to the global method table. +used with the [`Base.Experimental.@overlay`](@ref) macro to define methods for a function +without adding them to the global method table. """ :@MethodTable diff --git a/base/expr.jl b/base/expr.jl index b90f75e75a76f..dc85f3591efa1 100644 --- a/base/expr.jl +++ b/base/expr.jl @@ -484,7 +484,7 @@ CodeInfo( !!! compat "Julia 1.10" The usage within a function body requires at least Julia 1.10. -!!! compact "Julia 1.11" +!!! compat "Julia 1.11" The code block annotation requires at least Julia 1.11. !!! warning @@ -530,7 +530,7 @@ The `:consistent` setting asserts that for egal (`===`) inputs: !!! note The `:consistent`-cy assertion is made world-age wise. More formally, write - ``fᵢ`` for the evaluation of ``f`` in world-age ``i``, then we require: + ``fᵢ`` for the evaluation of ``f`` in world-age ``i``, then this setting requires: ```math ∀ i, x, y: x ≡ y → fᵢ(x) ≡ fᵢ(y) ``` @@ -742,7 +742,7 @@ macro assume_effects(args...) lastex = args[end] override = compute_assumed_settings(args[begin:end-1]) if is_function_def(unwrap_macrocalls(lastex)) - return esc(pushmeta!(lastex, form_purity_expr(override))) + return esc(pushmeta!(lastex::Expr, form_purity_expr(override))) elseif isexpr(lastex, :macrocall) && lastex.args[1] === Symbol("@ccall") lastex.args[1] = GlobalRef(Base, Symbol("@ccall_effects")) insert!(lastex.args, 3, Core.Compiler.encode_effects_override(override)) @@ -767,7 +767,7 @@ function compute_assumed_settings(settings) for setting in settings override = compute_assumed_setting(override, setting) override === nothing && - throw(ArgumentError("@assume_effects $setting not supported")) + throw(ArgumentError("`@assume_effects $setting` not supported")) end return override end @@ -815,10 +815,11 @@ function compute_assumed_setting(override::EffectsOverride, @nospecialize(settin end function form_purity_expr(override::EffectsOverride) - return Expr(:purity, - override.consistent, override.effect_free, override.nothrow, - override.terminates_globally, override.terminates_locally, override.notaskstate, - override.inaccessiblememonly, override.noub, override.noub_if_noinbounds) + ex = Expr(:purity) + for i = 1:Core.Compiler.NUM_EFFECTS_OVERRIDES + push!(ex.args, getfield(override, i)) + end + return ex end """ diff --git a/src/julia.h b/src/julia.h index 59e85e878e162..3a7d594562243 100644 --- a/src/julia.h +++ b/src/julia.h @@ -261,11 +261,12 @@ typedef union __jl_purity_overrides_t { uint16_t ipo_inaccessiblememonly : 1; uint16_t ipo_noub : 1; uint16_t ipo_noub_if_noinbounds : 1; + uint16_t ipo_consistent_overlay : 1; } overrides; uint16_t bits; } _jl_purity_overrides_t; -#define NUM_EFFECTS_OVERRIDES 9 +#define NUM_EFFECTS_OVERRIDES 10 #define NUM_IR_FLAGS 12 // This type describes a single function body @@ -439,13 +440,14 @@ typedef struct _jl_code_instance_t { // uint8_t ipo_inaccessiblememonly : 2; _Atomic(uint32_t) purity_bits; // purity_flags: - // uint8_t consistent : 2; + // uint8_t consistent : 3; // uint8_t effect_free : 2; - // uint8_t nothrow : 2; - // uint8_t terminates : 2; - // uint8_t nonoverlayed : 1; - // uint8_t notaskstate : 2; + // uint8_t nothrow : 1; + // uint8_t terminates : 1; + // uint8_t notaskstate : 1; // uint8_t inaccessiblememonly : 2; + // uint8_t noub : 2; + // uint8_t nonoverlayed : 2; jl_value_t *analysis_results; // Analysis results about this code (IPO-safe) // compilation state cache diff --git a/src/method.c b/src/method.c index ac5475d54a4d5..7e6c82b5221b2 100644 --- a/src/method.c +++ b/src/method.c @@ -347,15 +347,27 @@ static void jl_code_info_set_ir(jl_code_info_t *li, jl_expr_t *ir) li->constprop = 2; else if (jl_is_expr(ma) && ((jl_expr_t*)ma)->head == jl_purity_sym) { if (jl_expr_nargs(ma) == NUM_EFFECTS_OVERRIDES) { - li->purity.overrides.ipo_consistent = jl_unbox_bool(jl_exprarg(ma, 0)); - li->purity.overrides.ipo_effect_free = jl_unbox_bool(jl_exprarg(ma, 1)); - li->purity.overrides.ipo_nothrow = jl_unbox_bool(jl_exprarg(ma, 2)); - li->purity.overrides.ipo_terminates_globally = jl_unbox_bool(jl_exprarg(ma, 3)); - li->purity.overrides.ipo_terminates_locally = jl_unbox_bool(jl_exprarg(ma, 4)); - li->purity.overrides.ipo_notaskstate = jl_unbox_bool(jl_exprarg(ma, 5)); - li->purity.overrides.ipo_inaccessiblememonly = jl_unbox_bool(jl_exprarg(ma, 6)); - li->purity.overrides.ipo_noub = jl_unbox_bool(jl_exprarg(ma, 7)); - li->purity.overrides.ipo_noub_if_noinbounds = jl_unbox_bool(jl_exprarg(ma, 8)); + // N.B. this code allows multiple :purity expressions to be present in a single `:meta` node + int8_t consistent = jl_unbox_bool(jl_exprarg(ma, 0)); + if (consistent) li->purity.overrides.ipo_consistent = consistent; + int8_t effect_free = jl_unbox_bool(jl_exprarg(ma, 1)); + if (effect_free) li->purity.overrides.ipo_effect_free = effect_free; + int8_t nothrow = jl_unbox_bool(jl_exprarg(ma, 2)); + if (nothrow) li->purity.overrides.ipo_nothrow = nothrow; + int8_t terminates_globally = jl_unbox_bool(jl_exprarg(ma, 3)); + if (terminates_globally) li->purity.overrides.ipo_terminates_globally = terminates_globally; + int8_t terminates_locally = jl_unbox_bool(jl_exprarg(ma, 4)); + if (terminates_locally) li->purity.overrides.ipo_terminates_locally = terminates_locally; + int8_t notaskstate = jl_unbox_bool(jl_exprarg(ma, 5)); + if (notaskstate) li->purity.overrides.ipo_notaskstate = notaskstate; + int8_t inaccessiblememonly = jl_unbox_bool(jl_exprarg(ma, 6)); + if (inaccessiblememonly) li->purity.overrides.ipo_inaccessiblememonly = inaccessiblememonly; + int8_t noub = jl_unbox_bool(jl_exprarg(ma, 7)); + if (noub) li->purity.overrides.ipo_noub = noub; + int8_t noub_if_noinbounds = jl_unbox_bool(jl_exprarg(ma, 8)); + if (noub_if_noinbounds) li->purity.overrides.ipo_noub_if_noinbounds = noub_if_noinbounds; + int8_t consistent_overlay = jl_unbox_bool(jl_exprarg(ma, 9)); + if (consistent_overlay) li->purity.overrides.ipo_consistent_overlay = consistent_overlay; } } else diff --git a/test/compiler/AbstractInterpreter.jl b/test/compiler/AbstractInterpreter.jl index 776b9fd69ce9b..507773dabb35f 100644 --- a/test/compiler/AbstractInterpreter.jl +++ b/test/compiler/AbstractInterpreter.jl @@ -10,7 +10,7 @@ include("newinterp.jl") # OverlayMethodTable # ================== -using Base.Experimental: @MethodTable, @overlay +using Base.Experimental: @MethodTable, @overlay, @consistent_overlay # @overlay method with return type annotation @MethodTable RT_METHOD_DEF @@ -20,8 +20,8 @@ using Base.Experimental: @MethodTable, @overlay end @newinterp MTOverlayInterp -@MethodTable OverlayedMT -CC.method_table(interp::MTOverlayInterp) = CC.OverlayMethodTable(CC.get_inference_world(interp), OverlayedMT) +@MethodTable OVERLAY_MT +CC.method_table(interp::MTOverlayInterp) = CC.OverlayMethodTable(CC.get_inference_world(interp), OVERLAY_MT) function CC.add_remark!(interp::MTOverlayInterp, ::CC.InferenceState, remark) if interp.meta !== nothing @@ -32,7 +32,8 @@ function CC.add_remark!(interp::MTOverlayInterp, ::CC.InferenceState, remark) end strangesin(x) = sin(x) -@overlay OverlayedMT strangesin(x::Float64) = iszero(x) ? nothing : cos(x) +@overlay OVERLAY_MT strangesin(x::Float64) = + iszero(x) ? throw(StrangeSinError()) : x < 0 ? nothing : cos(x) # inference should use the overlayed method table @test Base.return_types((Float64,); interp=MTOverlayInterp()) do x @@ -84,7 +85,7 @@ end |> only === Float64 # not fully covered overlay method match overlay_match(::Any) = nothing -@overlay OverlayedMT overlay_match(::Int) = missing +@overlay OVERLAY_MT overlay_match(::Int) = missing @test Base.return_types((Any,); interp=MTOverlayInterp()) do x overlay_match(x) end |> only === Union{Nothing,Missing} @@ -116,11 +117,48 @@ Base.@assume_effects :total totalcall(f, args...) = f(args...) end end |> only === Nothing +# override `:native_executable` to allow concrete-eval for overlay-ed methods +function myfactorial(x::Int, raise) + res = 1 + 0 ≤ x < 20 || raise("x is too big") + Base.@assume_effects :terminates_locally while x > 1 + res *= x + x -= 1 + end + return res +end +raise_on_gpu1(x) = error(x) +@overlay OVERLAY_MT @noinline raise_on_gpu1(x) = #=do something with GPU=# error(x) +raise_on_gpu2(x) = error(x) +@consistent_overlay OVERLAY_MT @noinline raise_on_gpu2(x) = #=do something with GPU=# error(x) +raise_on_gpu3(x) = error(x) +@consistent_overlay OVERLAY_MT @noinline Base.@assume_effects :foldable raise_on_gpu3(x) = #=do something with GPU=# error_on_gpu(x) +cpu_factorial(x::Int) = myfactorial(x, error) +gpu_factorial1(x::Int) = myfactorial(x, raise_on_gpu1) +gpu_factorial2(x::Int) = myfactorial(x, raise_on_gpu2) +gpu_factorial3(x::Int) = myfactorial(x, raise_on_gpu3) + +@test Base.infer_effects(cpu_factorial, (Int,); interp=MTOverlayInterp()) |> Core.Compiler.is_nonoverlayed +@test Base.infer_effects(gpu_factorial1, (Int,); interp=MTOverlayInterp()) |> !Core.Compiler.is_nonoverlayed +@test Base.infer_effects(gpu_factorial2, (Int,); interp=MTOverlayInterp()) |> Core.Compiler.is_consistent_overlay +let effects = Base.infer_effects(gpu_factorial3, (Int,); interp=MTOverlayInterp()) + # check if `@consistent_overlay` together works with `@assume_effects` + # N.B. the overlaid `raise_on_gpu3` is not :foldable otherwise since `error_on_gpu` is (intetionally) undefined. + @test Core.Compiler.is_consistent_overlay(effects) + @test Core.Compiler.is_foldable(effects) +end +@test Base.infer_return_type(; interp=MTOverlayInterp()) do + Val(gpu_factorial2(3)) +end == Val{6} +@test Base.infer_return_type(; interp=MTOverlayInterp()) do + Val(gpu_factorial3(3)) +end == Val{6} + # GPUCompiler needs accurate inference through kwfunc with the overlay of `Core.throw_inexacterror` # https://github.com/JuliaLang/julia/issues/48097 @newinterp Issue48097Interp -@MethodTable Issue48097MT -CC.method_table(interp::Issue48097Interp) = CC.OverlayMethodTable(CC.get_inference_world(interp), Issue48097MT) +@MethodTable ISSUE_48097_MT +CC.method_table(interp::Issue48097Interp) = CC.OverlayMethodTable(CC.get_inference_world(interp), ISSUE_48097_MT) CC.InferenceParams(::Issue48097Interp) = CC.InferenceParams(; unoptimize_throw_blocks=false) function CC.concrete_eval_eligible(interp::Issue48097Interp, @nospecialize(f), result::CC.MethodCallResult, arginfo::CC.ArgInfo, sv::CC.AbsIntState) @@ -132,7 +170,7 @@ function CC.concrete_eval_eligible(interp::Issue48097Interp, end return ret end -@overlay Issue48097MT @noinline Core.throw_inexacterror(f::Symbol, ::Type{T}, val) where {T} = return +@overlay ISSUE_48097_MT @noinline Core.throw_inexacterror(f::Symbol, ::Type{T}, val) where {T} = return issue48097(; kwargs...) = return 42 @test fully_eliminated(; interp=Issue48097Interp(), retval=42) do issue48097(; a=1f0, b=1.0) @@ -146,28 +184,45 @@ inner52938(x, types::Type, args...; kwargs...) = x outer52938(x) = @inline inner52938(x, Tuple{}; foo=Ref(42), bar=1) @test fully_eliminated(outer52938, (Any,); interp=Issue52938Interp(), retval=Argument(2)) +# https://github.com/JuliaGPU/CUDA.jl/issues/2241 +@newinterp Cuda2241Interp +@MethodTable CUDA_2241_MT +CC.method_table(interp::Cuda2241Interp) = CC.OverlayMethodTable(CC.get_inference_world(interp), CUDA_2241_MT) +inner2241(f, types::Type, args...; kwargs...) = nothing +function outer2241(f) + @inline inner2241(f, Tuple{}; foo=Ref(42), bar=1) + return nothing +end +# NOTE CUDA.jl overlays `throw_boundserror` in a way that causes effects, but these effects +# are ignored for this call graph at the `@assume_effects` annotation on `typejoin`. +# Here it's important to use `@consistent_overlay` to avoid tainting the `:nonoverlayed` bit. +const cuda_kernel_state = Ref{Any}() +@consistent_overlay CUDA_2241_MT @inline Base.throw_boundserror(A, I) = + (cuda_kernel_state[] = (A, I); error()) +@test fully_eliminated(outer2241, (Nothing,); interp=Cuda2241Interp(), retval=nothing) + # Should not concrete-eval overlayed methods in semi-concrete interpretation @newinterp OverlaySinInterp -@MethodTable OverlaySinMT -CC.method_table(interp::OverlaySinInterp) = CC.OverlayMethodTable(CC.get_inference_world(interp), OverlaySinMT) +@MethodTable OVERLAY_SIN_MT +CC.method_table(interp::OverlaySinInterp) = CC.OverlayMethodTable(CC.get_inference_world(interp), OVERLAY_SIN_MT) overlay_sin1(x) = error("Not supposed to be called.") -@overlay OverlaySinMT overlay_sin1(x) = cos(x) -@overlay OverlaySinMT Base.sin(x::Union{Float32,Float64}) = overlay_sin1(x) +@overlay OVERLAY_SIN_MT overlay_sin1(x) = cos(x) +@overlay OVERLAY_SIN_MT Base.sin(x::Union{Float32,Float64}) = overlay_sin1(x) let oc = Base.code_ircode(; interp=OverlaySinInterp()) do sin(0.) end |> only |> first |> Core.OpaqueClosure @test oc() == cos(0.) end -@overlay OverlaySinMT Base.sin(x::Union{Float32,Float64}) = @noinline overlay_sin1(x) +@overlay OVERLAY_SIN_MT Base.sin(x::Union{Float32,Float64}) = @noinline overlay_sin1(x) let oc = Base.code_ircode(; interp=OverlaySinInterp()) do sin(0.) end |> only |> first |> Core.OpaqueClosure @test oc() == cos(0.) end _overlay_sin2(x) = error("Not supposed to be called.") -@overlay OverlaySinMT _overlay_sin2(x) = cos(x) +@overlay OVERLAY_SIN_MT _overlay_sin2(x) = cos(x) overlay_sin2(x) = _overlay_sin2(x) -@overlay OverlaySinMT Base.sin(x::Union{Float32,Float64}) = @noinline overlay_sin2(x) +@overlay OVERLAY_SIN_MT Base.sin(x::Union{Float32,Float64}) = @noinline overlay_sin2(x) let oc = Base.code_ircode(; interp=OverlaySinInterp()) do sin(0.) end |> only |> first |> Core.OpaqueClosure diff --git a/test/namedtuple.jl b/test/namedtuple.jl index 48aa8ea4a2591..558ba94bda8d0 100644 --- a/test/namedtuple.jl +++ b/test/namedtuple.jl @@ -398,14 +398,14 @@ for f in (Base.merge, Base.structdiff) fallback_func(a::NamedTuple, b::NamedTuple) = @invoke f(a::NamedTuple, b::NamedTuple) @testset let eff = Base.infer_effects(fallback_func) @test Core.Compiler.is_foldable(eff) - @test eff.nonoverlayed + @test Core.Compiler.is_nonoverlayed(eff) end @test only(Base.return_types(fallback_func)) == NamedTuple # test if `max_methods = 4` setting works as expected general_func(a::NamedTuple, b::NamedTuple) = f(a, b) @testset let eff = Base.infer_effects(general_func) @test Core.Compiler.is_foldable(eff) - @test eff.nonoverlayed + @test Core.Compiler.is_nonoverlayed(eff) end @test only(Base.return_types(general_func)) == NamedTuple end