Skip to content

Commit

Permalink
wip: reasons about concrete evaluation
Browse files Browse the repository at this point in the history
  • Loading branch information
aviatesk committed Apr 19, 2022
1 parent 06a0ab8 commit 58340c0
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 11 deletions.
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

0 comments on commit 58340c0

Please sign in to comment.