Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,402 changes: 782 additions & 620 deletions base/compiler/abstractinterpretation.jl

Large diffs are not rendered by default.

102 changes: 99 additions & 3 deletions base/compiler/inferencestate.jl
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ mutable struct InferenceState
stmt_info::Vector{CallInfo}

#= intermediate states for interprocedural abstract interpretation =#
tasks::Vector{WorkThunk}
pclimitations::IdSet{InferenceState} # causes of precision restrictions (LimitedAccuracy) on currpc ssavalue
limitations::IdSet{InferenceState} # causes of precision restrictions (LimitedAccuracy) on return
cycle_backedges::Vector{Tuple{InferenceState, Int}} # call-graph backedges connecting from callee to caller
Expand Down Expand Up @@ -328,6 +329,7 @@ mutable struct InferenceState
limitations = IdSet{InferenceState}()
cycle_backedges = Vector{Tuple{InferenceState,Int}}()
callstack = AbsIntState[]
tasks = WorkThunk[]

valid_worlds = WorldRange(1, get_world_counter())
bestguess = Bottom
Expand All @@ -351,7 +353,7 @@ mutable struct InferenceState
this = new(
mi, world, mod, sptypes, slottypes, src, cfg, method_info,
currbb, currpc, ip, handler_info, ssavalue_uses, bb_vartables, ssavaluetypes, stmt_edges, stmt_info,
pclimitations, limitations, cycle_backedges, callstack, 0, 0, 0,
tasks, pclimitations, limitations, cycle_backedges, callstack, 0, 0, 0,
result, unreachable, valid_worlds, bestguess, exc_bestguess, ipo_effects,
restrict_abstract_call_sites, cache_mode, insert_coverage,
interp)
Expand Down Expand Up @@ -800,6 +802,7 @@ mutable struct IRInterpretationState
const ssa_refined::BitSet
const lazyreachability::LazyCFGReachability
valid_worlds::WorldRange
const tasks::Vector{WorkThunk}
const edges::Vector{Any}
callstack #::Vector{AbsIntState}
frameid::Int
Expand All @@ -825,10 +828,11 @@ mutable struct IRInterpretationState
ssa_refined = BitSet()
lazyreachability = LazyCFGReachability(ir)
valid_worlds = WorldRange(min_world, max_world == typemax(UInt) ? get_world_counter() : max_world)
tasks = WorkThunk[]
edges = Any[]
callstack = AbsIntState[]
return new(method_info, ir, mi, world, curridx, argtypes_refined, ir.sptypes, tpdum,
ssa_refined, lazyreachability, valid_worlds, edges, callstack, 0, 0)
ssa_refined, lazyreachability, valid_worlds, tasks, edges, callstack, 0, 0)
end
end

Expand Down Expand Up @@ -870,6 +874,7 @@ function print_callstack(frame::AbsIntState)
print(frame_instance(sv))
is_cached(sv) || print(" [uncached]")
sv.parentid == idx - 1 || print(" [parent=", sv.parentid, "]")
isempty(callers_in_cycle(sv)) || print(" [cycle=", sv.cycleid, "]")
println()
@assert sv.frameid == idx
end
Expand Down Expand Up @@ -994,7 +999,10 @@ of the same cycle, only if it is part of a cycle with multiple frames.
function callers_in_cycle(sv::InferenceState)
callstack = sv.callstack::Vector{AbsIntState}
cycletop = cycleid = sv.cycleid
while cycletop < length(callstack) && (callstack[cycletop + 1]::InferenceState).cycleid == cycleid
while cycletop < length(callstack)
frame = callstack[cycletop + 1]
frame isa InferenceState || break
frame.cycleid == cycleid || break
cycletop += 1
end
return AbsIntCycle(callstack, cycletop == cycleid ? 0 : cycleid, cycletop)
Expand Down Expand Up @@ -1054,6 +1062,7 @@ function merge_effects!(::AbstractInterpreter, caller::InferenceState, effects::
effects = Effects(effects; effect_free=ALWAYS_TRUE)
end
caller.ipo_effects = merge_effects(caller.ipo_effects, effects)
nothing
end
merge_effects!(::AbstractInterpreter, ::IRInterpretationState, ::Effects) = return

Expand Down Expand Up @@ -1116,3 +1125,90 @@ function get_max_methods_for_module(mod::Module)
max_methods < 0 && return nothing
return max_methods
end

"""
Future{T}

Delayed return value for a value of type `T`, similar to RefValue{T}, but
explicitly represents completed as a `Bool` rather than as `isdefined`.
Set once with `f[] = v` and accessed with `f[]` afterwards.

Can also be constructed with the `completed` flag value and a closure to
produce `x`, as well as the additional arguments to avoid always capturing the
same couple of values.
Comment on lines +1136 to +1138
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this feature isn't implemented? Or we could refactor the constructor that takes interp and sv below and split it into the pure core functionality of Future and a separate function for AbsIntState that uses it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is implemented. The additional arguments are interp and sv. Looks like these could use splatting for them, but didn't seem like much point, and that it could be more confusing.

"""
struct Future{T}
later::Union{Nothing,RefValue{T}}
now::Union{Nothing,T}
Future{T}() where {T} = new{T}(RefValue{T}(), nothing)
Future{T}(x) where {T} = new{T}(nothing, x)
Future(x::T) where {T} = new{T}(nothing, x)
end
isready(f::Future) = f.later === nothing
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems weird to me that isready is false, even after a later setindex!.

getindex(f::Future{T}) where {T} = (later = f.later; later === nothing ? f.now::T : later[])
setindex!(f::Future, v) = something(f.later)[] = v
convert(::Type{Future{T}}, x) where {T} = Future{T}(x) # support return type conversion
convert(::Type{Future{T}}, x::Future) where {T} = x::Future{T}
Comment on lines +1150 to +1151
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before merging this PR, I'd like to make sure the code works without this convert.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The convert is very useful convenience syntax, since it means return-types work

function Future{T}(f, immediate::Bool, interp::AbstractInterpreter, sv::AbsIntState) where {T}
if immediate
return Future{T}(f(interp, sv))
else
@assert applicable(f, interp, sv)
result = Future{T}()
push!(sv.tasks, function (interp, sv)
result[] = f(interp, sv)
return true
end)
return result
end
end
function Future{T}(f, prev::Future{S}, interp::AbstractInterpreter, sv::AbsIntState) where {T, S}
later = prev.later
if later === nothing
return Future{T}(f(prev[], interp, sv))
else
@assert Core._hasmethod(Tuple{Core.Typeof(f), S, typeof(interp), typeof(sv)})
result = Future{T}()
push!(sv.tasks, function (interp, sv)
result[] = f(later[], interp, sv) # capture just later, instead of all of prev
return true
end)
return result
end
end


"""
doworkloop(args...)

Run a tasks inside the abstract interpreter, returning false if there are none.
Tasks will be run in DFS post-order tree order, such that all child tasks will
be run in the order scheduled, prior to running any subsequent tasks. This
allows tasks to generate more child tasks, which will be run before anything else.
Each task will be run repeatedly when returning `false`, until it returns `true`.
"""
function doworkloop(interp::AbstractInterpreter, sv::AbsIntState)
tasks = sv.tasks
prev = length(tasks)
prev == 0 && return false
task = pop!(tasks)
completed = task(interp, sv)
tasks = sv.tasks # allow dropping gc root over the previous call
completed isa Bool || throw(TypeError(:return, "", Bool, task)) # print the task on failure as part of the error message, instead of just "@ workloop:line"
completed || push!(tasks, task)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem like it guarantees convergence. I understand that the below requires child tasks to complete before it's run again, but what if a dependency is earlier in the task list - we will never run this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list is always post-dom sorted: dependencies are later by construction

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a comment to that effect?

# efficient post-order visitor: items pushed are executed in reverse post order such
# that later items are executed before earlier ones, but are fully executed
# (including any dependencies scheduled by them) before going on to the next item
reverse!(tasks, #=start=#prev)
return true
end


#macro workthunk(name::Symbol, body)
# name = esc(name)
# body = esc(body)
# return replace_linenums!(
# :(function $name($(esc(interp)), $(esc(sv)))
# $body
# end), __source__)
#end
1 change: 1 addition & 0 deletions base/compiler/ssair/ir.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1432,6 +1432,7 @@ function process_node!(compact::IncrementalCompact, result_idx::Int, inst::Instr
elseif isa(stmt, OldSSAValue)
ssa_rename[idx] = ssa_rename[stmt.id]
elseif isa(stmt, GotoNode) && cfg_transforms_enabled
stmt.label < 0 && (println(stmt); println(compact))
label = bb_rename_succ[stmt.label]
@assert label > 0
ssa_rename[idx] = SSAValue(result_idx)
Expand Down
38 changes: 19 additions & 19 deletions base/compiler/ssair/irinterp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ end

function abstract_call(interp::AbstractInterpreter, arginfo::ArgInfo, irsv::IRInterpretationState)
si = StmtInfo(true) # TODO better job here?
call = abstract_call(interp, arginfo, si, irsv)
irsv.ir.stmts[irsv.curridx][:info] = call.info
call = abstract_call(interp, arginfo, si, irsv)::Future
Future{Nothing}(call, interp, irsv) do call, interp, irsv
irsv.ir.stmts[irsv.curridx][:info] = call.info
nothing
end
return call
end

Expand Down Expand Up @@ -143,7 +146,19 @@ function reprocess_instruction!(interp::AbstractInterpreter, inst::Instruction,
head = stmt.head
if (head === :call || head === :foreigncall || head === :new || head === :splatnew ||
head === :static_parameter || head === :isdefined || head === :boundscheck)
(; rt, effects) = abstract_eval_statement_expr(interp, stmt, nothing, irsv)
@assert isempty(irsv.tasks) # TODO: this whole function needs to be converted to a stackless design to be a valid AbsIntState, but this should work here for now
result = abstract_eval_statement_expr(interp, stmt, nothing, irsv)
reverse!(irsv.tasks)
while true
if length(irsv.callstack) > irsv.frameid
typeinf(interp, irsv.callstack[irsv.frameid + 1])
elseif !doworkloop(interp, irsv)
break
end
end
@assert length(irsv.callstack) == irsv.frameid && isempty(irsv.tasks)
result isa Future && (result = result[])
(; rt, effects) = result
add_flag!(inst, flags_for_effects(effects))
elseif head === :invoke
rt, (nothrow, noub) = abstract_eval_invoke_inst(interp, inst, irsv)
Expand Down Expand Up @@ -293,7 +308,7 @@ function is_all_const_call(@nospecialize(stmt), interp::AbstractInterpreter, irs
return true
end

function _ir_abstract_constant_propagation(interp::AbstractInterpreter, irsv::IRInterpretationState;
function ir_abstract_constant_propagation(interp::AbstractInterpreter, irsv::IRInterpretationState;
externally_refined::Union{Nothing,BitSet} = nothing)
(; ir, tpdum, ssa_refined) = irsv

Expand Down Expand Up @@ -449,18 +464,3 @@ function _ir_abstract_constant_propagation(interp::AbstractInterpreter, irsv::IR

return Pair{Any,Tuple{Bool,Bool}}(maybe_singleton_const(ultimate_rt), (nothrow, noub))
end

function ir_abstract_constant_propagation(interp::NativeInterpreter, irsv::IRInterpretationState)
if __measure_typeinf__[]
inf_frame = Timings.InferenceFrameInfo(irsv.mi, irsv.world, VarState[], Any[], length(irsv.ir.argtypes))
Timings.enter_new_timer(inf_frame)
ret = _ir_abstract_constant_propagation(interp, irsv)
append!(inf_frame.slottypes, irsv.ir.argtypes)
Timings.exit_current_timer(inf_frame)
return ret
else
return _ir_abstract_constant_propagation(interp, irsv)
end
end
ir_abstract_constant_propagation(interp::AbstractInterpreter, irsv::IRInterpretationState) =
_ir_abstract_constant_propagation(interp, irsv)
5 changes: 4 additions & 1 deletion base/compiler/ssair/verify.jl
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# This file is a part of Julia. License is MIT: https://julialang.org/license

function maybe_show_ir(ir::IRCode)
if isdefined(Core, :Main)
if isdefined(Core, :Main) && isdefined(Core.Main, :Base)
# ensure we use I/O that does not yield, as this gets called during compilation
invokelatest(Core.Main.Base.show, Core.stdout, "text/plain", ir)
else
Core.show(ir)
end
end

Expand All @@ -25,6 +27,7 @@ is_toplevel_expr_head(head::Symbol) = head === :global || head === :method || he
is_value_pos_expr_head(head::Symbol) = head === :static_parameter
function check_op(ir::IRCode, domtree::DomTree, @nospecialize(op), use_bb::Int, use_idx::Int, printed_use_idx::Int, print::Bool, isforeigncall::Bool, arg_idx::Int, allow_frontend_forms::Bool)
if isa(op, SSAValue)
op.id > 0 || @verify_error "Def ($(op.id)) is invalid in final IR"
if op.id > length(ir.stmts)
def_bb = block_for_inst(ir.cfg, ir.new_nodes.info[op.id - length(ir.stmts)].pos)
else
Expand Down
Loading