Skip to content

Commit

Permalink
Inference annotations that can be read by an external tool
Browse files Browse the repository at this point in the history
This builds on top of #35831, letting inference emit a custom message
whenever it gives up on infering something. These messages are intended
to be displayed by external tools, either to debug what inference is doing
(e.g. for Cthulhu) or, if an external compiler needs to disallow certain
kinds of missing information (e.g. no dynamic dispatch on GPUs), can be
used to improve diagnostics. This is mostly a proof of concept, I think
these messages/hooks need to be a bit richer for a full solution, though
I think this can be already useful if we hook up something like Cthulhu.
As a proof of concept, I hacked up a 10 line function that reads these messagse.
It works something like the following:

```
function bar()
    sin = eval(:sin)
    sin(1)
end
foo() = bar()
```

```
julia> Compiler3.analyze_static(Tuple{typeof(foo)})
In function: bar()
ERROR: The called function was unknown
1| function bar()
2|     sin = eval(:sin)
=>     sin(1)
4| end

[1] In foo()
```

The reason this needs to sit on top of #35831 is that it needs to run
with a completely fresh cache, which #35831 provides the primitives for.
Otherwise, while you could still get the annotations out, that would
only work the first time something is inferred anywhere in the system.
With a fresh cache, everything is analyzed again, and any messages like
these that are opted in to can be collected.
  • Loading branch information
Keno committed Jun 29, 2020
1 parent 6185d24 commit 13b7bb1
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 9 deletions.
29 changes: 24 additions & 5 deletions base/compiler/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,17 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
max_methods::Int = InferenceParams(interp).MAX_METHODS)
atype_params = unwrap_unionall(atype).parameters
ft = unwrap_unionall(atype_params[1]) # TODO: ccall jl_method_table_for here
isa(ft, DataType) || return Any # the function being called is unknown. can't properly handle this backedge right now
if !isa(ft, DataType)
# can't properly handle this backedge right now
add_remark!(interp, sv, "the function being called is unknown")
return Any
end
ftname = ft.name
isdefined(ftname, :mt) || return Any # not callable. should be Bottom, but can't track this backedge right now
if !isdefined(ftname, :mt)
# should be Bottom, but can't track this backedge right now
add_remark!(interp, sv, "The function is not callable")
return Any # not callable.
end
if ftname === _TYPE_NAME
tname = ft.parameters[1]
if isa(tname, TypeVar)
Expand All @@ -49,6 +57,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
if !isa(tname, DataType)
# can't track the backedge to the ctor right now
# for things like Union
add_remark!(interp, sv, "The called constructor is too complicated")
return Any
end
end
Expand All @@ -61,7 +70,10 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
for sig_n in splitsigs
xapplicable = matching_methods(sig_n, sv.matching_methods_cache, max_methods,
get_world_counter(interp), min_valid, max_valid)
xapplicable === false && return Any
if xapplicable === false
add_remark!(interp, sv, "For one of the union split cases, too many methods matched")
return Any
end
append!(applicable, xapplicable)
end
else
Expand All @@ -70,6 +82,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
if applicable === false
# this means too many methods matched
# (assume this will always be true, so we don't compute / update valid age in this case)
add_remark!(interp, sv, "Too many methods matched")
return Any
end
end
Expand Down Expand Up @@ -97,6 +110,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
sig = match[1]
if istoplevel && !isdispatchtuple(sig)
# only infer concrete call sites in top-level expressions
add_remark!(interp, sv, "Refusing to infer non-concrete call site in top-level expression")
rettype = Any
break
end
Expand Down Expand Up @@ -148,6 +162,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
end
end
if is_unused && !(rettype === Bottom)
add_remark!(interp, sv, "Call result was widened, because the return type is unused")
# We're mainly only here because the optimizer might want this code,
# but we ourselves locally don't typically care about it locally
# (beyond checking if it always throws).
Expand Down Expand Up @@ -303,6 +318,7 @@ end

function abstract_call_method(interp::AbstractInterpreter, method::Method, @nospecialize(sig), sparams::SimpleVector, hardlimit::Bool, sv::InferenceState)
if method.name === :depwarn && isdefined(Main, :Base) && method.module === Main.Base
add_remark!(interp, sv, "Refusing to infer into `depwarn`")
return Any, false, nothing
end
topmost = nothing
Expand All @@ -327,6 +343,7 @@ function abstract_call_method(interp::AbstractInterpreter, method::Method, @nosp
# avoid widening when detecting self-recursion
# TODO: merge call cycle and return right away
if call_result_unused(sv)
add_remark!(interp, sv, "Widening unused call result early to avoid unbounded recursion")
# since we don't use the result (typically),
# we have a self-cycle in the call-graph, but not in the inference graph (typically):
# break this edge now (before we record it) by returning early
Expand Down Expand Up @@ -407,6 +424,7 @@ function abstract_call_method(interp::AbstractInterpreter, method::Method, @nosp
# continue inference, but note that we've limited parameter complexity
# on this call (to ensure convergence), so that we don't cache this result
if call_result_unused(sv)
add_remark!(interp, sv, "Widening unused call result early to avoid unbounded recursion")
# if we don't (typically) actually care about this result,
# don't bother trying to examine some complex abstract signature
# since it's very unlikely that we'll try to inline this,
Expand Down Expand Up @@ -608,8 +626,8 @@ function abstract_apply(interp::AbstractInterpreter, @nospecialize(itft), @nospe
aftw = widenconst(aft)
if !isa(aft, Const) && (!isType(aftw) || has_free_typevars(aftw))
if !isconcretetype(aftw) || (aftw <: Builtin)
# non-constant function of unknown type: bail now,
# since it seems unlikely that abstract_call will be able to do any better after splitting
add_remark!(interp, sv, "non-constant function of unknown type")
# bail now, since it seems unlikely that abstract_call will be able to do any better after splitting
# this also ensures we don't call abstract_call_gf_by_type below on an IntrinsicFunction or Builtin
return Any
end
Expand Down Expand Up @@ -933,6 +951,7 @@ function abstract_call(interp::AbstractInterpreter, fargs::Union{Nothing,Vector{
# non-constant function, but the number of arguments is known
# and the ft is not a Builtin or IntrinsicFunction
if typeintersect(widenconst(ft), Builtin) != Union{}
add_remark!(interp, sv, "The called function was unknown")
return Any
end
return abstract_call_gf_by_type(interp, nothing, argtypes, argtypes_to_type(argtypes), sv, max_methods)
Expand Down
9 changes: 6 additions & 3 deletions base/compiler/typeinfer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ function typeinf(interp::AbstractInterpreter, frame::InferenceState)
return true
end


function CodeInstance(result::InferenceResult, min_valid::UInt, max_valid::UInt,
may_compress=true, allow_discard_tree=true)
inferred_result = result.src
Expand Down Expand Up @@ -148,7 +147,8 @@ function cache_result!(interp::AbstractInterpreter, result::InferenceResult, min

# TODO: also don't store inferred code if we've previously decided to interpret this function
if !already_inferred
code_cache(interp)[result.linfo] = CodeInstance(result, min_valid, max_valid)
code_cache(interp)[result.linfo] = CodeInstance(result, min_valid, max_valid,
may_compress(interp), may_discard_trees(interp))
end
unlock_mi_inference(interp, result.linfo)
nothing
Expand All @@ -165,12 +165,15 @@ function finish(me::InferenceState, interp::AbstractInterpreter)
else
# annotate fulltree with type information
type_annotate!(me)
run_optimizer = (me.cached || me.parent !== nothing)
can_optimize = may_optimize(interp)
run_optimizer = (me.cached || me.parent !== nothing) && can_optimize
if run_optimizer
# construct the optimizer for later use, if we're building this IR to cache it
# (otherwise, we'll run the optimization passes later, outside of inference)
opt = OptimizationState(me, OptimizationParams(interp), interp)
me.result.src = opt
elseif !can_optimize
me.result.src = me.src
end
end
me.result.result = me.bestguess
Expand Down
12 changes: 11 additions & 1 deletion base/compiler/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ All AbstractInterpreters are expected to provide at least the following methods:
"""
abstract type AbstractInterpreter; end


"""
InferenceResult
Expand Down Expand Up @@ -189,3 +188,14 @@ lock_mi_inference(ni::NativeInterpreter, mi::MethodInstance) = (mi.inInference =
See lock_mi_inference
"""
unlock_mi_inference(ni::NativeInterpreter, mi::MethodInstance) = (mi.inInference = false; nothing)

"""
Emit an annotation when inference widens a result. These annotations are ignored
by the native interpreter, but can be used by external tooling to annotate
inference results.
"""
add_remark!(ni::NativeInterpreter, sv, s) = nothing

may_optimize(ni::NativeInterpreter) = true
may_compress(ni::NativeInterpreter) = true
may_discard_trees(ni::NativeInterpreter) = true

0 comments on commit 13b7bb1

Please sign in to comment.