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 JuliaLang#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 JuliaLang#35831 is that it needs to run
with a completely fresh cache, which JuliaLang#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 authored and simeonschaub committed Aug 11, 2020
1 parent 029a218 commit 61bac8b
Show file tree
Hide file tree
Showing 3 changed files with 26 additions and 6 deletions.
23 changes: 19 additions & 4 deletions base/compiler/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ end
function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), argtypes::Vector{Any}, @nospecialize(atype), sv::InferenceState,
max_methods::Int = InferenceParams(interp).MAX_METHODS)
mt = ccall(:jl_method_table_for, Any, (Any,), atype)
mt === nothing && return Any
if mt === nothing
add_remark!(interp, sv, "Could not identify method table for call")
return Any
end
mt = mt::Core.MethodTable
min_valid = UInt[typemin(UInt)]
max_valid = UInt[typemax(UInt)]
Expand All @@ -48,7 +51,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 @@ -57,6 +63,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 @@ -84,6 +91,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 @@ -135,6 +143,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
end
end
if is_unused && !(rettype === Bottom)
add_remark!(interp, sv, "Call result type was widened because the return value 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 @@ -288,8 +297,11 @@ function abstract_call_method_with_const_args(interp::AbstractInterpreter, @nosp
return result
end

const RECURSION_UNUSED_MSG = "Bounded recursion detected with unused result. Annotated return type may be wider than true result."

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 @@ -314,6 +326,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, RECURSION_UNUSED_MSG)
# 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 @@ -394,6 +407,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, RECURSION_UNUSED_MSG)
# 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 @@ -595,8 +609,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, "Core._apply called on a function of a non-concrete 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 @@ -920,6 +934,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, "Could not identify method table for call")
return Any
end
return abstract_call_gf_by_type(interp, nothing, argtypes, argtypes_to_type(argtypes), sv, max_methods)
Expand Down
1 change: 0 additions & 1 deletion 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
8 changes: 7 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 @@ -190,6 +189,13 @@ lock_mi_inference(ni::NativeInterpreter, mi::MethodInstance) = (mi.inInference =
"""
unlock_mi_inference(ni::NativeInterpreter, mi::MethodInstance) = (mi.inInference = false; nothing)

"""
Emit an analysis remark during inference for the current line (`sv.pc`). 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 61bac8b

Please sign in to comment.