Skip to content

[REPLCompletions] fix #51548, set up a mode to limit the scope of the aggressive inference #51581

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

Merged
merged 5 commits into from
Oct 6, 2023
Merged
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
68 changes: 46 additions & 22 deletions stdlib/REPL/src/REPLCompletions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -453,19 +453,21 @@ function get_code_cache()
end

struct REPLInterpreter <: CC.AbstractInterpreter
limit_aggressive_inference::Bool
world::UInt
inf_params::CC.InferenceParams
opt_params::CC.OptimizationParams
inf_cache::Vector{CC.InferenceResult}
code_cache::REPLInterpreterCache
function REPLInterpreter(; world::UInt = Base.get_world_counter(),
inf_params::CC.InferenceParams = CC.InferenceParams(;
aggressive_constant_propagation=true,
unoptimize_throw_blocks=false),
opt_params::CC.OptimizationParams = CC.OptimizationParams(),
inf_cache::Vector{CC.InferenceResult} = CC.InferenceResult[],
code_cache::REPLInterpreterCache = get_code_cache())
return new(world, inf_params, opt_params, inf_cache, code_cache)
function REPLInterpreter(limit_aggressive_inference::Bool=false;
world::UInt = Base.get_world_counter(),
inf_params::CC.InferenceParams = CC.InferenceParams(;
aggressive_constant_propagation=true,
unoptimize_throw_blocks=false),
opt_params::CC.OptimizationParams = CC.OptimizationParams(),
inf_cache::Vector{CC.InferenceResult} = CC.InferenceResult[],
code_cache::REPLInterpreterCache = get_code_cache())
return new(limit_aggressive_inference, world, inf_params, opt_params, inf_cache, code_cache)
end
end
CC.InferenceParams(interp::REPLInterpreter) = interp.inf_params
Expand Down Expand Up @@ -503,17 +505,22 @@ CC.bail_out_toplevel_call(::REPLInterpreter, ::CC.InferenceLoopState, ::CC.Infer
# Similarly to the aggressive binding resolution, aggressive concrete evaluation doesn't
# present any cache validation issues because "repl-frame" is never cached.

function is_call_graph_uncached(sv::CC.AbsIntState)
sv isa CC.InferenceState && sv.cached && return false
# `REPLInterpreter` is specifically used by `repl_eval_ex`, where all top-level frames are
# `repl_frame` always. However, this assumption wouldn't stand if `REPLInterpreter` were to
# be employed, for instance, by `typeinf_ext_toplevel`.
is_repl_frame(sv::CC.InferenceState) = sv.linfo.def isa Module && !sv.cached

function is_call_graph_uncached(sv::CC.InferenceState)
sv.cached && return false
parent = sv.parent
parent === nothing && return true
return is_call_graph_uncached(parent)
return is_call_graph_uncached(parent::CC.InferenceState)
end

# aggressive global binding resolution within `repl_frame`
function CC.abstract_eval_globalref(interp::REPLInterpreter, g::GlobalRef,
sv::CC.InferenceState)
if is_call_graph_uncached(sv)
if (interp.limit_aggressive_inference ? is_repl_frame(sv) : is_call_graph_uncached(sv))
if CC.isdefined_globalref(g)
return Const(ccall(:jl_get_globalref_value, Any, (Any,), g))
end
Expand All @@ -523,10 +530,18 @@ function CC.abstract_eval_globalref(interp::REPLInterpreter, g::GlobalRef,
sv::CC.InferenceState)
end

function is_repl_frame_getproperty(sv::CC.InferenceState)
def = sv.linfo.def
def isa Method || return false
def.name === :getproperty || return false
sv.cached && return false
return is_repl_frame(sv.parent)
end

# aggressive global binding resolution for `getproperty(::Module, ::Symbol)` calls within `repl_frame`
function CC.builtin_tfunction(interp::REPLInterpreter, @nospecialize(f),
argtypes::Vector{Any}, sv::CC.InferenceState)
if f === Core.getglobal && is_call_graph_uncached(sv)
if f === Core.getglobal && (interp.limit_aggressive_inference ? is_repl_frame_getproperty(sv) : is_call_graph_uncached(sv))
if length(argtypes) == 2
a1, a2 = argtypes
if isa(a1, Const) && isa(a2, Const)
Expand All @@ -549,20 +564,29 @@ end
function CC.concrete_eval_eligible(interp::REPLInterpreter, @nospecialize(f),
result::CC.MethodCallResult, arginfo::CC.ArgInfo,
sv::CC.InferenceState)
if is_call_graph_uncached(sv)
if (interp.limit_aggressive_inference ? is_repl_frame(sv) : is_call_graph_uncached(sv))
neweffects = CC.Effects(result.effects; consistent=CC.ALWAYS_TRUE)
result = CC.MethodCallResult(result.rt, result.edgecycle, result.edgelimited,
result.edge, neweffects)
end
return @invoke CC.concrete_eval_eligible(interp::CC.AbstractInterpreter, f::Any,
result::CC.MethodCallResult, arginfo::CC.ArgInfo,
sv::CC.InferenceState)
ret = @invoke CC.concrete_eval_eligible(interp::CC.AbstractInterpreter, f::Any,
result::CC.MethodCallResult, arginfo::CC.ArgInfo,
sv::CC.InferenceState)
if ret === :semi_concrete_eval
# while the base eligibility check probably won't permit semi-concrete evaluation
# for `REPLInterpreter` (given it completely turns off optimization),
# this ensures we don't inadvertently enter irinterp
ret = :none
end
return ret
end

# allow constant propagation for mutable constants
function CC.const_prop_argument_heuristic(interp::REPLInterpreter, arginfo::CC.ArgInfo, sv::CC.AbsIntState)
any(@nospecialize(a)->isa(a, Const), arginfo.argtypes) && return true # even if mutable
return @invoke CC.const_prop_argument_heuristic(interp::CC.AbstractInterpreter, arginfo::CC.ArgInfo, sv::CC.AbsIntState)
function CC.const_prop_argument_heuristic(interp::REPLInterpreter, arginfo::CC.ArgInfo, sv::CC.InferenceState)
if !interp.limit_aggressive_inference
any(@nospecialize(a)->isa(a, Const), arginfo.argtypes) && return true # even if mutable
end
return @invoke CC.const_prop_argument_heuristic(interp::CC.AbstractInterpreter, arginfo::CC.ArgInfo, sv::CC.InferenceState)
end

function resolve_toplevel_symbols!(src::Core.CodeInfo, mod::Module)
Expand All @@ -575,7 +599,7 @@ function resolve_toplevel_symbols!(src::Core.CodeInfo, mod::Module)
end

# lower `ex` and run type inference on the resulting top-level expression
function repl_eval_ex(@nospecialize(ex), context_module::Module)
function repl_eval_ex(@nospecialize(ex), context_module::Module; limit_aggressive_inference::Bool=false)
if (isexpr(ex, :toplevel) || isexpr(ex, :tuple)) && !isempty(ex.args)
# get the inference result for the last expression
ex = ex.args[end]
Expand All @@ -600,7 +624,7 @@ function repl_eval_ex(@nospecialize(ex), context_module::Module)
resolve_toplevel_symbols!(src, context_module)
@atomic mi.uninferred = src

interp = REPLInterpreter()
interp = REPLInterpreter(limit_aggressive_inference)
result = CC.InferenceResult(mi)
frame = CC.InferenceState(result, src, #=cache=#:no, interp)

Expand Down
18 changes: 18 additions & 0 deletions stdlib/REPL/test/replcompletions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1978,6 +1978,24 @@ let (c, r, res) = test_complete_context("getkeyelem(mutable_const_prop).value.")
end
end

# JuliaLang/julia/#51548
# don't return wrong result due to mutable inconsistentcy
function issue51548(T, a)
# if we fold `xs = getindex(T)` to `xs::Const(Vector{T}())`, then we may wrongly
# constant-fold `isempty(xs)::Const(true)` and return wrong result
xs = T[]
if a isa T
push!(xs, a)
end
return Val(isempty(xs))
end;
let inferred = REPL.REPLCompletions.repl_eval_ex(
:(issue51548(Any, r"issue51548")), @__MODULE__; limit_aggressive_inference=true)
@test !isnothing(inferred)
RT = Core.Compiler.widenconst(inferred)
@test Val{false} <: RT
end

# Test completion of var"" identifiers (#49280)
let s = "var\"complicated "
c, r = test_complete_foo(s)
Expand Down