Skip to content
Closed
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
119 changes: 32 additions & 87 deletions base/compiler/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,11 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
all_effects = EFFECTS_TOTAL
if !matches.nonoverlayed
# currently we don't have a good way to execute the overlayed method definition,
# so we should give up pure/concrete eval when any of the matched methods is overlayed
# so we should give up concrete eval when any of the matched methods is overlayed
f = nothing
all_effects = Effects(all_effects; nonoverlayed=false)
end

# try pure-evaluation
val = pure_eval_call(interp, f, applicable, arginfo)
val !== nothing && return CallMeta(val, all_effects, MethodResultPure(info)) # TODO: add some sort of edge(s)

𝕃ₚ = ipo_lattice(interp)
for i in 1:napplicable
match = applicable[i]::MethodMatch
Expand All @@ -117,69 +113,34 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
break
end
this_rt = Bottom
splitunions = false
# TODO: this used to trigger a bug in inference recursion detection, and is unmaintained now
# sigtuple = unwrap_unionall(sig)::DataType
# splitunions = 1 < unionsplitcost(sigtuple.parameters) * napplicable <= InferenceParams(interp).max_union_splitting
if splitunions
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't remove this branch. We have kinda maintained this branch since we may enable it again in the future. And this change is unrelated to this PR.

splitsigs = switchtupleunion(sig)
for sig_n in splitsigs
result = abstract_call_method(interp, method, sig_n, svec(), multiple_matches, si, sv)
(; rt, edge, effects) = result
this_argtypes = isa(matches, MethodMatches) ? argtypes : matches.applicable_argtypes[i]
this_arginfo = ArgInfo(fargs, this_argtypes)
const_call_result = abstract_call_method_with_const_args(interp,
result, f, this_arginfo, si, match, sv)
const_result = nothing
if const_call_result !== nothing
if const_call_result.rt ⊑ₚ rt
rt = const_call_result.rt
(; effects, const_result, edge) = const_call_result
else
add_remark!(interp, sv, "[constprop] Discarded because the result was wider than inference")
end
end
all_effects = merge_effects(all_effects, effects)
push!(const_results, const_result)
any_const_result |= const_result !== nothing
edge === nothing || push!(edges, edge)
this_rt = tmerge(this_rt, rt)
if bail_out_call(interp, this_rt, sv)
break
end
end
this_conditional = ignorelimited(this_rt)
this_rt = widenwrappedconditional(this_rt)
else
result = abstract_call_method(interp, method, sig, match.sparams, multiple_matches, si, sv)
(; rt, edge, effects) = result
this_conditional = ignorelimited(rt)
this_rt = widenwrappedconditional(rt)
# try constant propagation with argtypes for this match
# this is in preparation for inlining, or improving the return result
this_argtypes = isa(matches, MethodMatches) ? argtypes : matches.applicable_argtypes[i]
this_arginfo = ArgInfo(fargs, this_argtypes)
const_call_result = abstract_call_method_with_const_args(interp,
result, f, this_arginfo, si, match, sv)
const_result = nothing
if const_call_result !== nothing
this_const_conditional = ignorelimited(const_call_result.rt)
this_const_rt = widenwrappedconditional(const_call_result.rt)
# return type of const-prop' inference can be wider than that of non const-prop' inference
# e.g. in cases when there are cycles but cached result is still accurate
if this_const_rt ⊑ₚ this_rt
this_conditional = this_const_conditional
this_rt = this_const_rt
(; effects, const_result, edge) = const_call_result
else
add_remark!(interp, sv, "[constprop] Discarded because the result was wider than inference")
end
result = abstract_call_method(interp, method, sig, match.sparams, multiple_matches, si, sv)
(; rt, edge, effects) = result
this_conditional = ignorelimited(rt)
this_rt = widenwrappedconditional(rt)
# try constant propagation with argtypes for this match
# this is in preparation for inlining, or improving the return result
this_argtypes = isa(matches, MethodMatches) ? argtypes : matches.applicable_argtypes[i]
this_arginfo = ArgInfo(fargs, this_argtypes)
const_call_result = abstract_call_method_with_const_args(interp,
result, f, this_arginfo, si, match, sv)
const_result = nothing
if const_call_result !== nothing
this_const_conditional = ignorelimited(const_call_result.rt)
this_const_rt = widenwrappedconditional(const_call_result.rt)
# return type of const-prop' inference can be wider than that of non const-prop' inference
# e.g. in cases when there are cycles but cached result is still accurate
if this_const_rt ⊑ₚ this_rt
this_conditional = this_const_conditional
this_rt = this_const_rt
(; effects, const_result, edge) = const_call_result
else
add_remark!(interp, sv, "[constprop] Discarded because the result was wider than inference")
end
all_effects = merge_effects(all_effects, effects)
push!(const_results, const_result)
any_const_result |= const_result !== nothing
edge === nothing || push!(edges, edge)
end
all_effects = merge_effects(all_effects, effects)
push!(const_results, const_result)
any_const_result |= const_result !== nothing
edge === nothing || push!(edges, edge)
@assert !(this_conditional isa Conditional || this_rt isa MustAlias) "invalid lattice element returned from inter-procedural context"
seen += 1
rettype = tmerge(𝕃ₚ, rettype, this_rt)
Expand Down Expand Up @@ -788,15 +749,6 @@ struct MethodCallResult
end
end

function pure_eval_eligible(interp::AbstractInterpreter,
@nospecialize(f), applicable::Vector{Any}, arginfo::ArgInfo)
# XXX we need to check that this pure function doesn't call any overlayed method
return f !== nothing &&
length(applicable) == 1 &&
is_method_pure(applicable[1]::MethodMatch) &&
is_all_const_arg(arginfo, #=start=#2)
end

function is_method_pure(method::Method, @nospecialize(sig), sparams::SimpleVector)
if isdefined(method, :generator)
method.generator.expand_early || return false
Expand All @@ -810,11 +762,6 @@ function is_method_pure(method::Method, @nospecialize(sig), sparams::SimpleVecto
end
is_method_pure(match::MethodMatch) = is_method_pure(match.method, match.spec_types, match.sparams)

function pure_eval_call(interp::AbstractInterpreter,
@nospecialize(f), applicable::Vector{Any}, arginfo::ArgInfo)
pure_eval_eligible(interp, f, applicable, arginfo) || return nothing
return _pure_eval_call(f, arginfo)
end
function _pure_eval_call(@nospecialize(f), arginfo::ArgInfo)
args = collect_const_args(arginfo, #=start=#2)
value = try
Expand Down Expand Up @@ -2022,7 +1969,7 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f),
if isa(rty, Conditional)
return CallMeta(Conditional(rty.slot, rty.elsetype, rty.thentype), EFFECTS_TOTAL, NoCallInfo()) # swap if-else
elseif isa(rty, Const)
return CallMeta(Const(rty.val === false), EFFECTS_TOTAL, MethodResultPure())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't remove MethodResultPure. This would result in bad performance.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couldn't this just check the effects rather than using MethodResultPure?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently don't propagate per-statement level effects. CallInfo is what we use for propagating per-statement information to the optimizer.

return CallMeta(Const(rty.val === false), EFFECTS_TOTAL, NoCallInfo())
end
return CallMeta(rty, EFFECTS_TOTAL, NoCallInfo())
elseif la == 3 && istopfunction(f, :(>:))
Expand All @@ -2038,22 +1985,20 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f),
elseif la == 2 &&
(a2 = argtypes[2]; isa(a2, Const)) && (svecval = a2.val; isa(svecval, SimpleVector)) &&
istopfunction(f, :length)
# mark length(::SimpleVector) as @pure
return CallMeta(Const(length(svecval)), EFFECTS_TOTAL, MethodResultPure())
return CallMeta(Const(length(svecval)), EFFECTS_TOTAL, NoCallInfo())
elseif la == 3 &&
(a2 = argtypes[2]; isa(a2, Const)) && (svecval = a2.val; isa(svecval, SimpleVector)) &&
(a3 = argtypes[3]; isa(a3, Const)) && (idx = a3.val; isa(idx, Int)) &&
istopfunction(f, :getindex)
# mark getindex(::SimpleVector, i::Int) as @pure
if 1 <= idx <= length(svecval) && isassigned(svecval, idx)
return CallMeta(Const(getindex(svecval, idx)), EFFECTS_TOTAL, MethodResultPure())
return CallMeta(Const(getindex(svecval, idx)), EFFECTS_TOTAL, NoCallInfo())
end
elseif la == 2 && istopfunction(f, :typename)
return CallMeta(typename_static(argtypes[2]), EFFECTS_TOTAL, MethodResultPure())
return CallMeta(typename_static(argtypes[2]), EFFECTS_TOTAL, NoCallInfo())
elseif la == 3 && istopfunction(f, :typejoin)
if is_all_const_arg(arginfo, #=start=#2)
val = _pure_eval_call(f, arginfo)
return CallMeta(val === nothing ? Type : val, EFFECTS_TOTAL, MethodResultPure())
return CallMeta(val === nothing ? Type : val, EFFECTS_TOTAL, NoCallInfo())
end
end
atype = argtypes_to_type(argtypes)
Expand Down
1 change: 1 addition & 0 deletions base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,7 @@ function compute_frameinfo(ir::IRCode, call_resolved::Bool)
check_effect_free!(ir, idx, stmt, inst[:type], 𝕃ₒ)
end
if callinfo !== nothing && isexpr(stmt, :call)
# TODO: pass effects here
callinfo[idx] = resolve_call(ir, stmt, inst[:info])
elseif isexpr(stmt, :enter)
@assert idx nstmts "try/catch inside new_nodes unsupported"
Expand Down
7 changes: 3 additions & 4 deletions base/compiler/ssair/EscapeAnalysis/interprocedural.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import Core.Compiler:
MethodInstance, InferenceResult, Signature, ConstPropResult, ConcreteResult,
SemiConcreteResult, CallInfo, NoCallInfo, MethodResultPure, MethodMatchInfo,
SemiConcreteResult, CallInfo, NoCallInfo, MethodMatchInfo,
UnionSplitInfo, ConstCallInfo, InvokeCallInfo,
call_sig, argtypes_to_type, is_builtin, is_return_type, istopfunction,
validate_sparams, specialize_method, invoke_rewrite
Expand All @@ -14,6 +14,7 @@ struct EACallInfo
end

function resolve_call(ir::IRCode, stmt::Expr, @nospecialize(info::CallInfo))
# TODO: if effect free, return true
sig = call_sig(ir, stmt)
if sig === nothing
return missing
Expand All @@ -35,9 +36,7 @@ function resolve_call(ir::IRCode, stmt::Expr, @nospecialize(info::CallInfo))
elseif is_return_type(f)
return true
end
if info isa MethodResultPure
return true
elseif info === NoCallInfo
if info === NoCallInfo
return missing
end
# TODO handle OpaqueClosureCallInfo
Expand Down
6 changes: 0 additions & 6 deletions base/compiler/ssair/inlining.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1559,7 +1559,6 @@ end

function handle_modifyfield!_call!(ir::IRCode, idx::Int, stmt::Expr, info::ModifyFieldInfo, state::InliningState)
info = info.info
info isa MethodResultPure && (info = info.info)
info isa ConstCallInfo && (info = info.call)
info isa MethodMatchInfo || return nothing
length(info.results) == 1 || return nothing
Expand Down Expand Up @@ -1655,11 +1654,6 @@ function assemble_inline_todo!(ir::IRCode, state::InliningState)
continue
end

# Check whether this call was @pure and evaluates to a constant
if info isa MethodResultPure
inline_const_if_inlineable!(ir[SSAValue(idx)]) && continue
info = info.info
end
if info === NoCallInfo()
# Inference determined this couldn't be analyzed. Don't question it.
continue
Expand Down
15 changes: 0 additions & 15 deletions base/compiler/stmtinfo.jl
Original file line number Diff line number Diff line change
Expand Up @@ -91,21 +91,6 @@ nsplit_impl(info::ConstCallInfo) = nsplit(info.call)
getsplit_impl(info::ConstCallInfo, idx::Int) = getsplit(info.call, idx)
getresult_impl(info::ConstCallInfo, idx::Int) = info.results[idx]

"""
info::MethodResultPure <: CallInfo

This struct represents a method result constant was proven to be
effect-free, including being no-throw (typically because the value was computed
by calling an `@pure` function).
"""
struct MethodResultPure <: CallInfo
info::CallInfo
end
let instance = MethodResultPure(NoCallInfo())
global MethodResultPure
MethodResultPure() = instance
end

"""
ainfo::AbstractIterationInfo

Expand Down
2 changes: 1 addition & 1 deletion base/compiler/tfuncs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2489,7 +2489,7 @@ function return_type_tfunc(interp::AbstractInterpreter, argtypes::Vector{Any}, s
else
call = abstract_call(interp, ArgInfo(nothing, argtypes_vec), si, sv, -1)
end
info = verbose_stmt_info(interp) ? MethodResultPure(ReturnTypeCallInfo(call.info)) : MethodResultPure()
info = verbose_stmt_info(interp) ? ReturnTypeCallInfo(call.info) : NoCallInfo()
rt = widenslotwrapper(call.rt)
if isa(rt, Const)
# output was computed to be constant
Expand Down
23 changes: 23 additions & 0 deletions base/deprecated.jl
Original file line number Diff line number Diff line change
Expand Up @@ -362,3 +362,26 @@ end
end

# END 1.9 deprecations

"""
@pure ex

`@pure` gives the compiler a hint for the definition of a pure function,
helping for type inference.

!!! warning
This macro is intended for internal compiler use and may be subject to changes.

!!! warning
In Julia 1.8 and higher, it is favorable to use [`@assume_effects`](@ref) instead of `@pure`.
This is because `@assume_effects` allows a finer grained control over Julia's purity
modeling and the effect system enables a wider range of optimizations.

!!! note
In Julia 1.10 this is deprecated in favor of [`@assume_effects`](@ref).
Specifically, `@assume_effects :total` provides similar guarentees.
"""
@deprecate
macro pure(ex)
esc(isa(ex, Expr) ? :(Base.@assume_effects :total $ex) : ex)
end
27 changes: 0 additions & 27 deletions base/expr.jl
Original file line number Diff line number Diff line change
Expand Up @@ -339,23 +339,6 @@ macro noinline(x)
return annotate_meta_def_or_block(x, :noinline)
end

"""
@pure ex

`@pure` gives the compiler a hint for the definition of a pure function,
helping for type inference.

!!! warning
This macro is intended for internal compiler use and may be subject to changes.

!!! warning
In Julia 1.8 and higher, it is favorable to use [`@assume_effects`](@ref) instead of `@pure`.
This is because `@assume_effects` allows a finer grained control over Julia's purity
modeling and the effect system enables a wider range of optimizations.
"""
macro pure(ex)
esc(isa(ex, Expr) ? pushmeta!(ex, :pure) : ex)
end

"""
@constprop setting [ex]
Expand Down Expand Up @@ -703,16 +686,6 @@ the following other `setting`s:
Effect names may be prefixed by `!` to indicate that the effect should be removed
from an earlier meta effect. For example, `:total !:nothrow` indicates that while
the call is generally total, it may however throw.

---
## Comparison to `@pure`

`@assume_effects :foldable` is similar to [`@pure`](@ref) with the primary
distinction that the `:consistent`-cy requirement applies world-age wise rather
than globally as described above. However, in particular, a method annotated
`@pure` should always be at least `:foldable`.
Another advantage is that effects introduced by `@assume_effects` are propagated to
callers interprocedurally while a purity defined by `@pure` is not.
"""
macro assume_effects(args...)
lastex = args[end]
Expand Down
12 changes: 0 additions & 12 deletions test/compiler/contextual.jl
Original file line number Diff line number Diff line change
Expand Up @@ -119,23 +119,11 @@ f() = 2
# Test that MiniCassette is at least somewhat capable by overdubbing gcd
@test overdub(Ctx(), gcd, 10, 20) === gcd(10, 20)

# Test that pure propagates for Cassette
Base.@pure isbitstype(T) = Base.isbitstype(T)
f31012(T) = Val(isbitstype(T))
@test @inferred(overdub(Ctx(), f31012, Int64)) == Val(true)

@generated bar(::Val{align}) where {align} = :(42)
foo(i) = i+bar(Val(1))

@test @inferred(overdub(Ctx(), foo, 1)) == 43

# Check that misbehaving pure functions propagate their error
Base.@pure func1() = 42
Base.@pure func2() = (this_is_an_exception; func1())
func3() = func2()
@test_throws UndefVarError func3()


# overlay method tables
# =====================

Expand Down
25 changes: 2 additions & 23 deletions test/compiler/inference.jl
Original file line number Diff line number Diff line change
Expand Up @@ -563,27 +563,6 @@ f18450() = ifelse(true, Tuple{Vararg{Int}}, Tuple{Vararg})
# issue #18569
@test !Core.Compiler.isconstType(Type{Tuple})

# ensure pure attribute applies correctly to all signatures of fpure
Base.@pure function fpure(a=rand(); b=rand())
# use the `rand` function since it is known to be `@inline`
# but would be too big to inline
return a + b + rand()
end
gpure() = fpure()
gpure(x::Irrational) = fpure(x)
@test which(fpure, ()).pure
@test which(fpure, (typeof(pi),)).pure
@test !which(gpure, ()).pure
@test !which(gpure, (typeof(pi),)).pure
@test code_typed(gpure, ())[1][1].pure
@test code_typed(gpure, (typeof(π),))[1][1].pure
@test gpure() == gpure() == gpure()
@test gpure(π) == gpure(π) == gpure(π)

# Make sure @pure works for functions using the new syntax
Base.@pure (fpure2(x::T) where T) = T
@test which(fpure2, (Int64,)).pure

# issue #10880
function cat10880(a, b)
Tuple{a.parameters..., b.parameters...}
Expand Down Expand Up @@ -4716,8 +4695,8 @@ end |> only === Type{Float64}
global it_count47688 = 0
struct CountsIterate47688{N}; end
function Base.iterate(::CountsIterate47688{N}, n=0) where N
global it_count47688 += 1
n <= N ? (n, n+1) : nothing
global it_count47688 += 1
n <= N ? (n, n+1) : nothing
end
foo47688() = tuple(CountsIterate47688{5}()...)
bar47688() = foo47688()
Expand Down
Loading