From 58340c06f9c69a87746ad6a788276ada9428a17b Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Fri, 15 Apr 2022 21:13:42 +0900 Subject: [PATCH] wip: reasons about concrete evaluation --- src/abstractinterpret/abstractanalyzer.jl | 1 + src/abstractinterpret/typeinfer.jl | 41 +++++++++++++++-- src/analyzers/jetanalyzer.jl | 21 +++++++-- test/abstractinterpret/test_typeinfer.jl | 55 +++++++++++++++++++++-- 4 files changed, 107 insertions(+), 11 deletions(-) diff --git a/src/abstractinterpret/abstractanalyzer.jl b/src/abstractinterpret/abstractanalyzer.jl index c1a811b18..d59e22e52 100644 --- a/src/abstractinterpret/abstractanalyzer.jl +++ b/src/abstractinterpret/abstractanalyzer.jl @@ -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...))) diff --git a/src/abstractinterpret/typeinfer.jl b/src/abstractinterpret/typeinfer.jl index c04eea9c1..15800b8a7 100644 --- a/src/abstractinterpret/typeinfer.jl +++ b/src/abstractinterpret/typeinfer.jl @@ -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) @@ -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 diff --git a/src/analyzers/jetanalyzer.jl b/src/analyzers/jetanalyzer.jl index 8bb129fa0..8355455fa 100644 --- a/src/analyzers/jetanalyzer.jl +++ b/src/analyzers/jetanalyzer.jl @@ -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 @@ -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) diff --git a/test/abstractinterpret/test_typeinfer.jl b/test/abstractinterpret/test_typeinfer.jl index 8965d0685..0ae61895f 100644 --- a/test/abstractinterpret/test_typeinfer.jl +++ b/test/abstractinterpret/test_typeinfer.jl @@ -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) @@ -265,8 +264,7 @@ end end # incremental setup - let - m = gen_virtual_module() + let m = Module() result = Core.eval(m, quote $report_call() do @@ -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