Skip to content

Commit d7cd271

Browse files
authored
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
1 parent 89003fe commit d7cd271

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
@@ -3499,6 +3499,20 @@ function merge_override_effects!(interp::AbstractInterpreter, effects::Effects,
34993499
# It is possible for arguments (GlobalRef/:static_parameter) to throw,
35003500
# but these will be recomputed during SSA construction later.
35013501
override = decode_statement_effects_override(sv)
3502+
if override.consistent
3503+
m = sv.linfo.def
3504+
if isa(m, Method)
3505+
# N.B.: We'd like deleted_world here, but we can't add an appropriate edge at this point.
3506+
# However, in order to reach here in the first place, ordinary method lookup would have
3507+
# had to add an edge and appropriate invalidation trigger.
3508+
valid_worlds = WorldRange(m.primary_world, typemax(Int))
3509+
if sv.world.this in valid_worlds
3510+
update_valid_age!(sv, valid_worlds)
3511+
else
3512+
override = EffectsOverride(override, consistent=false)
3513+
end
3514+
end
3515+
end
35023516
effects = override_effects(effects, override)
35033517
set_curr_ssaflag!(sv, flags_for_effects(effects), IR_FLAGS_EFFECTS)
35043518
merge_effects!(interp, sv, effects)

Compiler/src/typeinfer.jl

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -368,11 +368,17 @@ function cycle_fix_limited(@nospecialize(typ), sv::InferenceState, cycleid::Int)
368368
return typ
369369
end
370370

371-
function adjust_effects(ipo_effects::Effects, def::Method)
371+
function adjust_effects(ipo_effects::Effects, def::Method, world::UInt)
372372
# override the analyzed effects using manually annotated effect settings
373373
override = decode_effects_override(def.purity)
374+
valid_worlds = WorldRange(0, typemax(UInt))
374375
if is_effect_overridden(override, :consistent)
375-
ipo_effects = Effects(ipo_effects; consistent=ALWAYS_TRUE)
376+
# See note on `typemax(Int)` instead of `deleted_world` in adjust_effects!
377+
override_valid_worlds = WorldRange(def.primary_world, typemax(Int))
378+
if world in override_valid_worlds
379+
ipo_effects = Effects(ipo_effects; consistent=ALWAYS_TRUE)
380+
valid_worlds = override_valid_worlds
381+
end
376382
end
377383
if is_effect_overridden(override, :effect_free)
378384
ipo_effects = Effects(ipo_effects; effect_free=ALWAYS_TRUE)
@@ -400,7 +406,7 @@ function adjust_effects(ipo_effects::Effects, def::Method)
400406
if is_effect_overridden(override, :nortcall)
401407
ipo_effects = Effects(ipo_effects; nortcall=true)
402408
end
403-
return ipo_effects
409+
return (ipo_effects, valid_worlds)
404410
end
405411

406412
function adjust_effects(sv::InferenceState)
@@ -454,7 +460,8 @@ function adjust_effects(sv::InferenceState)
454460
# override the analyzed effects using manually annotated effect settings
455461
def = sv.linfo.def
456462
if isa(def, Method)
457-
ipo_effects = adjust_effects(ipo_effects, def)
463+
(ipo_effects, valid_worlds) = adjust_effects(ipo_effects, def, sv.world.this)
464+
update_valid_age!(sv, valid_worlds)
458465
end
459466

460467
return ipo_effects
@@ -492,9 +499,9 @@ function finishinfer!(me::InferenceState, interp::AbstractInterpreter, cycleid::
492499
end
493500
end
494501
result = me.result
495-
result.valid_worlds = me.world.valid_worlds
496502
result.result = bestguess
497503
ipo_effects = result.ipo_effects = me.ipo_effects = adjust_effects(me)
504+
result.valid_worlds = me.world.valid_worlds
498505
result.exc_result = me.exc_bestguess = refine_exception_type(me.exc_bestguess, ipo_effects)
499506
me.src.rettype = widenconst(ignorelimited(bestguess))
500507
me.src.ssaflags = me.ssaflags
@@ -957,8 +964,13 @@ function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize
957964
update_valid_age!(caller, frame.world.valid_worlds)
958965
local isinferred = is_inferred(frame)
959966
local edge = isinferred ? edge_ci : nothing
960-
local effects = isinferred ? frame.result.ipo_effects : # effects are adjusted already within `finish` for ipo_effects
961-
adjust_effects(effects_for_cycle(frame.ipo_effects), method)
967+
local effects, valid_worlds
968+
if isinferred
969+
effects = frame.result.ipo_effects # effects are adjusted already within `finish` for ipo_effects
970+
else
971+
(effects, valid_worlds) = adjust_effects(effects_for_cycle(frame.ipo_effects), method, frame.world.this)
972+
update_valid_age!(caller, valid_worlds)
973+
end
962974
local bestguess = frame.bestguess
963975
local exc_bestguess = refine_exception_type(frame.exc_bestguess, effects)
964976
# propagate newly inferred source to the inliner, allowing efficient inlining w/o deserialization:
@@ -981,7 +993,8 @@ function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize
981993
# return the current knowledge about this cycle
982994
frame = frame::InferenceState
983995
update_valid_age!(caller, frame.world.valid_worlds)
984-
effects = adjust_effects(effects_for_cycle(frame.ipo_effects), method)
996+
(effects, valid_worlds) = adjust_effects(effects_for_cycle(frame.ipo_effects), method, frame.world.this)
997+
update_valid_age!(caller, valid_worlds)
985998
bestguess = frame.bestguess
986999
exc_bestguess = refine_exception_type(frame.exc_bestguess, effects)
9871000
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
@@ -540,16 +540,20 @@ The `:consistent` setting asserts that for egal (`===`) inputs:
540540
contents) are not egal.
541541
542542
!!! note
543-
The `:consistent`-cy assertion is made world-age wise. More formally, write
544-
``fᵢ`` for the evaluation of ``f`` in world-age ``i``, then this setting requires:
543+
The `:consistent`-cy assertion is made with respect to a particular world range `R`.
544+
More formally, write ``fᵢ`` for the evaluation of ``f`` in world-age ``i``, then this setting requires:
545545
```math
546-
∀ i, x, y: x ≡ y → fᵢ(x) ≡ fᵢ(y)
546+
∀ i ∈ R, j ∈ R, x, y: x ≡ y → fᵢ(x) ≡ fⱼ(y)
547547
```
548-
However, for two world ages ``i``, ``j`` s.t. ``i ≠ j``, we may have ``fᵢ(x) ≢ fⱼ(y)``.
548+
549+
For `@assume_effects`, the range `R` is `m.primary_world:m.deleted_world` of
550+
the annotated or containing method.
551+
552+
For ordinary code instances, `R` is `ci.min_world:ci.max_world`.
549553
550554
A further implication is that `:consistent` functions may not make their
551555
return value dependent on the state of the heap or any other global state
552-
that is not constant for a given world age.
556+
that is not constant over the given world age range.
553557
554558
!!! note
555559
The `:consistent`-cy includes all legal rewrites performed by the optimizer.

0 commit comments

Comments
 (0)