Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: JETAnalyzer: reasons about concrete evaluation #338

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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 change: 1 addition & 0 deletions src/abstractinterpret/abstractanalyzer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ end
# otherwise, it means malformed report pass call, and we should inform users of it
function (rp::ReportPass)(T, @nospecialize(args...))
if !(T === NativeRemark ||
T === ConcreteError ||
T === InvalidConstantRedefinition ||
T === InvalidConstantDeclaration)
throw(MethodError(rp, (T, args...)))
Expand Down
41 changes: 38 additions & 3 deletions src/abstractinterpret/typeinfer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,42 @@ end # @static if IS_V18
# TODO correctly reasons about error found by concrete evaluation
# for now just always fallback to the constant-prop'
@static if IS_V18
function CC.concrete_eval_eligible(analyzer::AbstractAnalyzer,
const ConcreteResult = isdefined(CC, :ConcreteResult) ? CC.ConcreteResult : CC.ConstResult
function CC.concrete_eval_call(analyzer::AbstractAnalyzer,
@nospecialize(f), result::MethodCallResult, arginfo::ArgInfo, sv::InferenceState)
return false
CC.concrete_eval_eligible(analyzer, f, result, arginfo, sv) || return nothing
# this frame is happily concretizable, now let's throw away reports collected from
# the previous abstract interpretation and just trust the concrete runtime evaluation
filter_lineages!(analyzer, sv.result, result.edge)
args = CC.collect_const_args(arginfo)
world = get_world_counter(analyzer)
value = try
Core._call_in_world_total(world, f, args...)
catch err
# NOTE this report pass allows analyzers to opt in to report concretized errors
ReportPass(analyzer)(ConcreteError, analyzer, sv, err)

# The evaulation threw. By :consistent-cy, we're guaranteed this would have happened at runtime
return CC.ConstCallResults(Union{}, ConcreteResult(result.edge, result.edge_effects), result.edge_effects)
end
if CC.is_inlineable_constant(value) || CC.call_result_unused(sv)
# If the constant is not inlineable, still do the const-prop, since the
# code that led to the creation of the Const may be inlineable in the same
# circumstance and may be optimizable.
return CC.ConstCallResults(Const(value), ConcreteResult(result.edge, CC.EFFECTS_TOTAL, value), CC.EFFECTS_TOTAL)
end
return nothing
end
end # @static if IS_V18

@reportdef struct ConcreteError <: InferenceErrorReport
@nospecialize(err)
end
function print_report(io::IO, (; err, sig)::ConcreteError)
msg = lazy"may throw $(typeof(err))"
default_report_printer(io, msg, sig)
end

@static if IS_AFTER_42529
function CC.abstract_call(analyzer::AbstractAnalyzer,
arginfo::ArgInfo, sv::InferenceState, max_methods::Int = InferenceParams(analyzer).MAX_METHODS)
Expand Down Expand Up @@ -643,7 +673,12 @@ function islineage(parent::MethodInstance, current::MethodInstance)
vst = report.vst
length(vst) > 1 || return false
vst[1].linfo === parent || return false
return vst[2].linfo === current
if vst[2].linfo === current
# @info "remove" report
return true
else
return false
end
end
end
end
Expand Down
21 changes: 17 additions & 4 deletions src/analyzers/jetanalyzer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,16 @@ function (::BasicPass)(::Type{UncaughtExceptionReport}, analyzer::JETAnalyzer, f
report_uncaught_exceptions!(analyzer, frame, stmts)
return true
else
# the non-`Bottom` result may mean `throw` calls from the children frames
# (if exists) are caught and not propagated here
# we don't want to cache the caught `UncaughtExceptionReport`s for this frame and
# its parents, and just filter them away now
# the non-`Bottom` result mean `throw` calls or concretized calls within child frames
# (if exists) can be caught or necessarily don't happen here:
# for `BasicPass` we don't want to cache such reports for this frame and not propagate
# them to parent frames, so just throw them away now
# TODO this is not best place to do this
filter!(get_reports(analyzer, frame.result)) do report
report isa UncaughtExceptionReport && return false
# report isa ConcreteError && return false
return true
end
filter!(report->!isa(report, UncaughtExceptionReport), get_reports(analyzer, frame.result))
end
return false
Expand Down Expand Up @@ -479,6 +485,13 @@ must-reachable `throw` calls.
"""
CC.const_prop_entry_heuristic(::JETAnalyzer, result::MethodCallResult, sv::InferenceState) = true

@static if IS_V18
function (::SoundBasicPass)(::Type{ConcreteError}, analyzer::AbstractAnalyzer, sv::InferenceState, @nospecialize(err))
add_new_report!(analyzer, sv.result, ConcreteError(sv, err))
return true
end
end # @static if IS_V18

function CC.return_type_tfunc(analyzer::JETAnalyzer, argtypes::Argtypes, sv::InferenceState)
# report pass for invalid `Core.Compiler.return_type` call
ReportPass(analyzer)(InvalidReturnTypeCall, analyzer, sv, argtypes)
Expand Down
55 changes: 51 additions & 4 deletions test/abstractinterpret/test_typeinfer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,7 @@ end

@testset "integration with global code cache" begin
# analysis for `sum(::String)` is already cached, `sum′` and `sum′′` should use it
let
m = gen_virtual_module()
let m = Module()
result = Core.eval(m, quote
sum′(s) = sum(s)
sum′′(s) = sum′(s)
Expand All @@ -265,8 +264,7 @@ end
end

# incremental setup
let
m = gen_virtual_module()
let m = Module()

result = Core.eval(m, quote
$report_call() do
Expand Down Expand Up @@ -567,6 +565,55 @@ end
end
end

@static if isdefined(Base, Symbol("@assume_effects"))
Base.@assume_effects :terminates_locally function pow(x)
# this :terminates_locally allows `pow` to be constant-folded
res = 1
1 < x < 20 || error("bad pow")
while x > 1
res *= x
x -= 1
end
return res
end
function concretize_pow(n)
v = pow(n)
if v == 120
return v
else
return nothing
end
end

Base.@assume_effects :total_may_throw concretize(f, args...) = f(args...)

@testset "concrete evaluation" begin
test_call((Int,)) do x
concretize_pow(5) + x
end

let result = report_call((Int,)) do x
concretize_pow(42) + x # `ErrorException` should be reported
end
report = only(get_reports(result))
@test report isa ConcreteError
@test report.err == ErrorException("bad pow")
end

# throw away errors found by previous abstract interpretation
# this case especially shouldn't report possible errors by `sum(::String)`
test_call() do
concretize("julia") do x
if hasmethod(length, (typeof(x),))
return length(x)
else
return sum(x)
end
end
end
end
end # @static if isdefined(Base, Symbol("@assume_effects"))

@testset "additional analysis pass for task parallelism code" begin
# general case with `schedule(::Task)` pattern
result = report_call() do
Expand Down