Skip to content

Commit 6bb1a24

Browse files
KenoKristofferC
authored andcommitted
Strengthen language around @assume_effects :consistent (#58254)
The current language of `:consistent` for assume_effects is problematic, because it explicitly promises something false: That you're allowed to mark a method as `:consistent` even in the face of redefinitions. To understand this problem a bit better, we need to think about the primary use case of the `:consistent` annotation: To perform constant propagation evaluation of fuctions with constant arguments. However, since constant evaluation only runs in a single world (unlike the abstract interpreter, which symbolically runs in multiple worlds and keeps track of which world range its result is valid for), we run into a bit of a problem. In principle, for full correctness and under the current definition of consistentcy, we can only claim that we know the result for a single world age (the one that we ran in). This is problematic, because it would force us to re-infer the function for every new world age. In practice however, ignoring `@assume_effects` for the moment, we have a stronger guarantee. When inference sets the :consistent effect bit, it guarantees consistentcy across the entire computed min_world:max_world range. If there is a redefinition, the world range will be terminated, even if it is `:consistent` both before and after and even if the redefinition would not otherwise affect inference's computed information. This is useful, because it lets us conclude that the information derived from concrete evluation is valid for the entire min_world:max_world range of the corresponding code instance. Coming back to `@assume_effects` we run into a problem however, because we have no way to provide the required edges to inference. In practice inference will probably be able to figure it out, but this is insufficient as a semantic guarantee. After some discussion within the compiler team, we came to the conclusion that the best resolution was to change the documented semantics of `@assume_effects :consistent` to require consistent-cy across the entire defined world range of the method, not just world-age-by-world-age. This is a significantly stronger guarantee, but appears necessary for semantic correctness. In the future we may want to consider `:consistent` annotations for particular if blocks, which would not require the same restrictions (because it would still rely on inference to add appropriate edges for redefinitions). Closes #46156 (cherry picked from commit d7cd271)
1 parent bcc885c commit 6bb1a24

File tree

3 files changed

+44
-13
lines changed

3 files changed

+44
-13
lines changed

Compiler/src/abstractinterpretation.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3452,6 +3452,20 @@ function merge_override_effects!(interp::AbstractInterpreter, effects::Effects,
34523452
# It is possible for arguments (GlobalRef/:static_parameter) to throw,
34533453
# but these will be recomputed during SSA construction later.
34543454
override = decode_statement_effects_override(sv)
3455+
if override.consistent
3456+
m = sv.linfo.def
3457+
if isa(m, Method)
3458+
# N.B.: We'd like deleted_world here, but we can't add an appropriate edge at this point.
3459+
# However, in order to reach here in the first place, ordinary method lookup would have
3460+
# had to add an edge and appropriate invalidation trigger.
3461+
valid_worlds = WorldRange(m.primary_world, typemax(Int))
3462+
if sv.world.this in valid_worlds
3463+
update_valid_age!(sv, valid_worlds)
3464+
else
3465+
override = EffectsOverride(override, consistent=false)
3466+
end
3467+
end
3468+
end
34553469
effects = override_effects(effects, override)
34563470
set_curr_ssaflag!(sv, flags_for_effects(effects), IR_FLAGS_EFFECTS)
34573471
merge_effects!(interp, sv, effects)

Compiler/src/typeinfer.jl

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -356,11 +356,17 @@ function cycle_fix_limited(@nospecialize(typ), sv::InferenceState, cycleid::Int)
356356
return typ
357357
end
358358

359-
function adjust_effects(ipo_effects::Effects, def::Method)
359+
function adjust_effects(ipo_effects::Effects, def::Method, world::UInt)
360360
# override the analyzed effects using manually annotated effect settings
361361
override = decode_effects_override(def.purity)
362+
valid_worlds = WorldRange(0, typemax(UInt))
362363
if is_effect_overridden(override, :consistent)
363-
ipo_effects = Effects(ipo_effects; consistent=ALWAYS_TRUE)
364+
# See note on `typemax(Int)` instead of `deleted_world` in adjust_effects!
365+
override_valid_worlds = WorldRange(def.primary_world, typemax(Int))
366+
if world in override_valid_worlds
367+
ipo_effects = Effects(ipo_effects; consistent=ALWAYS_TRUE)
368+
valid_worlds = override_valid_worlds
369+
end
364370
end
365371
if is_effect_overridden(override, :effect_free)
366372
ipo_effects = Effects(ipo_effects; effect_free=ALWAYS_TRUE)
@@ -388,7 +394,7 @@ function adjust_effects(ipo_effects::Effects, def::Method)
388394
if is_effect_overridden(override, :nortcall)
389395
ipo_effects = Effects(ipo_effects; nortcall=true)
390396
end
391-
return ipo_effects
397+
return (ipo_effects, valid_worlds)
392398
end
393399

394400
function adjust_effects(sv::InferenceState)
@@ -442,7 +448,8 @@ function adjust_effects(sv::InferenceState)
442448
# override the analyzed effects using manually annotated effect settings
443449
def = sv.linfo.def
444450
if isa(def, Method)
445-
ipo_effects = adjust_effects(ipo_effects, def)
451+
(ipo_effects, valid_worlds) = adjust_effects(ipo_effects, def, sv.world.this)
452+
update_valid_age!(sv, valid_worlds)
446453
end
447454

448455
return ipo_effects
@@ -480,9 +487,9 @@ function finishinfer!(me::InferenceState, interp::AbstractInterpreter, cycleid::
480487
end
481488
end
482489
result = me.result
483-
result.valid_worlds = me.world.valid_worlds
484490
result.result = bestguess
485491
ipo_effects = result.ipo_effects = me.ipo_effects = adjust_effects(me)
492+
result.valid_worlds = me.world.valid_worlds
486493
result.exc_result = me.exc_bestguess = refine_exception_type(me.exc_bestguess, ipo_effects)
487494
me.src.rettype = widenconst(ignorelimited(bestguess))
488495
me.src.ssaflags = me.ssaflags
@@ -943,8 +950,13 @@ function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize
943950
update_valid_age!(caller, frame.world.valid_worlds)
944951
local isinferred = is_inferred(frame)
945952
local edge = isinferred ? edge_ci : nothing
946-
local effects = isinferred ? frame.result.ipo_effects : # effects are adjusted already within `finish` for ipo_effects
947-
adjust_effects(effects_for_cycle(frame.ipo_effects), method)
953+
local effects, valid_worlds
954+
if isinferred
955+
effects = frame.result.ipo_effects # effects are adjusted already within `finish` for ipo_effects
956+
else
957+
(effects, valid_worlds) = adjust_effects(effects_for_cycle(frame.ipo_effects), method, frame.world.this)
958+
update_valid_age!(caller, valid_worlds)
959+
end
948960
local bestguess = frame.bestguess
949961
local exc_bestguess = refine_exception_type(frame.exc_bestguess, effects)
950962
# propagate newly inferred source to the inliner, allowing efficient inlining w/o deserialization:
@@ -967,7 +979,8 @@ function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize
967979
# return the current knowledge about this cycle
968980
frame = frame::InferenceState
969981
update_valid_age!(caller, frame.world.valid_worlds)
970-
effects = adjust_effects(effects_for_cycle(frame.ipo_effects), method)
982+
(effects, valid_worlds) = adjust_effects(effects_for_cycle(frame.ipo_effects), method, frame.world.this)
983+
update_valid_age!(caller, valid_worlds)
971984
bestguess = frame.bestguess
972985
exc_bestguess = refine_exception_type(frame.exc_bestguess, effects)
973986
return Future(MethodCallResult(interp, caller, method, bestguess, exc_bestguess, effects, nothing, edgecycle, edgelimited))

base/expr.jl

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -532,16 +532,20 @@ The `:consistent` setting asserts that for egal (`===`) inputs:
532532
contents) are not egal.
533533
534534
!!! note
535-
The `:consistent`-cy assertion is made world-age wise. More formally, write
536-
``fᵢ`` for the evaluation of ``f`` in world-age ``i``, then this setting requires:
535+
The `:consistent`-cy assertion is made with respect to a particular world range `R`.
536+
More formally, write ``fᵢ`` for the evaluation of ``f`` in world-age ``i``, then this setting requires:
537537
```math
538-
∀ i, x, y: x ≡ y → fᵢ(x) ≡ fᵢ(y)
538+
∀ i ∈ R, j ∈ R, x, y: x ≡ y → fᵢ(x) ≡ fⱼ(y)
539539
```
540-
However, for two world ages ``i``, ``j`` s.t. ``i ≠ j``, we may have ``fᵢ(x) ≢ fⱼ(y)``.
540+
541+
For `@assume_effects`, the range `R` is `m.primary_world:m.deleted_world` of
542+
the annotated or containing method.
543+
544+
For ordinary code instances, `R` is `ci.min_world:ci.max_world`.
541545
542546
A further implication is that `:consistent` functions may not make their
543547
return value dependent on the state of the heap or any other global state
544-
that is not constant for a given world age.
548+
that is not constant over the given world age range.
545549
546550
!!! note
547551
The `:consistent`-cy includes all legal rewrites performed by the optimizer.

0 commit comments

Comments
 (0)