Skip to content

Commit

Permalink
add anonymous function supports for @constprop and @assume_effects (
Browse files Browse the repository at this point in the history
  • Loading branch information
aviatesk authored Feb 8, 2023
1 parent 68ada71 commit 1b3b630
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 37 deletions.
110 changes: 90 additions & 20 deletions base/expr.jl
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ Small functions typically do not need the `@inline` annotation,
as the compiler does it automatically. By using `@inline` on bigger functions,
an extra nudge can be given to the compiler to inline it.
`@inline` can be applied immediately before the definition or in its function body.
`@inline` can be applied immediately before a function definition or within a function body.
```julia
# annotate long-form definition
Expand Down Expand Up @@ -271,7 +271,7 @@ Small functions are typically inlined automatically.
By using `@noinline` on small functions, auto-inlining can be
prevented.
`@noinline` can be applied immediately before the definition or in its function body.
`@noinline` can be applied immediately before a function definition or within a function body.
```julia
# annotate long-form definition
Expand Down Expand Up @@ -358,32 +358,66 @@ macro pure(ex)
end

"""
@constprop setting ex
@constprop setting [ex]
`@constprop` controls the mode of interprocedural constant propagation for the
annotated function. Two `setting`s are supported:
Control the mode of interprocedural constant propagation for the annotated function.
- `@constprop :aggressive ex`: apply constant propagation aggressively.
Two `setting`s are supported:
- `@constprop :aggressive [ex]`: apply constant propagation aggressively.
For a method where the return type depends on the value of the arguments,
this can yield improved inference results at the cost of additional compile time.
- `@constprop :none ex`: disable constant propagation. This can reduce compile
- `@constprop :none [ex]`: disable constant propagation. This can reduce compile
times for functions that Julia might otherwise deem worthy of constant-propagation.
Common cases are for functions with `Bool`- or `Symbol`-valued arguments or keyword arguments.
`@constprop` can be applied immediately before a function definition or within a function body.
```julia
# annotate long-form definition
@constprop :aggressive function longdef(x)
...
end
# annotate short-form definition
@constprop :aggressive shortdef(x) = ...
# annotate anonymous function that a `do` block creates
f() do
@constprop :aggressive
...
end
```
!!! compat "Julia 1.10"
The usage within a function body requires at least Julia 1.10.
"""
macro constprop(setting, ex)
if isa(setting, QuoteNode)
setting = setting.value
sym = constprop_setting(setting)
isa(ex, Expr) && return esc(pushmeta!(ex, sym))
throw(ArgumentError(LazyString("Bad expression `", ex, "` in `@constprop settings ex`")))
end
macro constprop(setting)
sym = constprop_setting(setting)
return Expr(:meta, sym)
end

function constprop_setting(@nospecialize setting)
isa(setting, QuoteNode) && (setting = setting.value)
if setting === :aggressive
return :aggressive_constprop
elseif setting === :none
return :no_constprop
end
setting === :aggressive && return esc(isa(ex, Expr) ? pushmeta!(ex, :aggressive_constprop) : ex)
setting === :none && return esc(isa(ex, Expr) ? pushmeta!(ex, :no_constprop) : ex)
throw(ArgumentError("@constprop $setting not supported"))
throw(ArgumentError(LazyString("@constprop "), setting, "not supported"))
end

"""
@assume_effects setting... ex
@assume_effects setting... [ex]
`@assume_effects` overrides the compiler's effect modeling for the given method.
`ex` must be a method definition or `@ccall` expression.
Override the compiler's effect modeling for the given method or foreign call.
`@assume_effects` can be applied immediately before a function definition or within a function body.
It can also be applied immediately before a `@ccall` expression.
!!! compat "Julia 1.8"
Using `Base.@assume_effects` requires Julia version 1.8.
Expand All @@ -410,10 +444,31 @@ julia> code_typed() do
1 ─ return 479001600
) => Int64
julia> code_typed() do
map((2,3,4)) do x
# this :terminates_locally allows this anonymous function to be constant-folded
Base.@assume_effects :terminates_locally
res = 1
1 < x < 20 || error("bad pow")
while x > 1
res *= x
x -= 1
end
return res
end
end
1-element Vector{Any}:
CodeInfo(
1 ─ return (2, 6, 24)
) => Tuple{Int64, Int64, Int64}
julia> Base.@assume_effects :total !:nothrow @ccall jl_type_intersection(Vector{Int}::Any, Vector{<:Integer}::Any)::Any
Vector{Int64} (alias for Array{Int64, 1})
```
!!! compat "Julia 1.10"
The usage within a function body requires at least Julia 1.10.
!!! warning
Improper use of this macro causes undefined behavior (including crashes,
incorrect answers, or other hard to track bugs). Use with care and only as a
Expand Down Expand Up @@ -660,9 +715,21 @@ Another advantage is that effects introduced by `@assume_effects` are propagated
callers interprocedurally while a purity defined by `@pure` is not.
"""
macro assume_effects(args...)
lastex = args[end]
inner = unwrap_macrocalls(lastex)
if is_function_def(inner)
ex = lastex
idx = length(args)-1
elseif isexpr(lastex, :macrocall) && lastex.args[1] === Symbol("@ccall")
ex = lastex
idx = length(args)-1
else # anonymous function case
ex = nothing
idx = length(args)
end
(consistent, effect_free, nothrow, terminates_globally, terminates_locally, notaskstate, inaccessiblememonly) =
(false, false, false, false, false, false, false, false)
for org_setting in args[1:end-1]
for org_setting in args[1:idx]
(setting, val) = compute_assumed_setting(org_setting)
if setting === :consistent
consistent = val
Expand All @@ -688,16 +755,19 @@ macro assume_effects(args...)
throw(ArgumentError("@assume_effects $org_setting not supported"))
end
end
ex = args[end]
isa(ex, Expr) || throw(ArgumentError("Bad expression `$ex` in `@assume_effects [settings] ex`"))
if ex.head === :macrocall && ex.args[1] === Symbol("@ccall")
if is_function_def(inner)
return esc(pushmeta!(ex, :purity,
consistent, effect_free, nothrow, terminates_globally, terminates_locally, notaskstate, inaccessiblememonly))
elseif isexpr(ex, :macrocall) && ex.args[1] === Symbol("@ccall")
ex.args[1] = GlobalRef(Base, Symbol("@ccall_effects"))
insert!(ex.args, 3, Core.Compiler.encode_effects_override(Core.Compiler.EffectsOverride(
consistent, effect_free, nothrow, terminates_globally, terminates_locally, notaskstate, inaccessiblememonly,
)))
return esc(ex)
else # anonymous function case
return Expr(:meta, Expr(:purity,
consistent, effect_free, nothrow, terminates_globally, terminates_locally, notaskstate, inaccessiblememonly))
end
return esc(pushmeta!(ex, :purity, consistent, effect_free, nothrow, terminates_globally, terminates_locally, notaskstate, inaccessiblememonly))
end

function compute_assumed_setting(@nospecialize(setting), val::Bool=true)
Expand Down
15 changes: 15 additions & 0 deletions test/compiler/effects.jl
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@ recur_termination22(x) = x * recur_termination21(x-1)
recur_termination21(12) + recur_termination22(12)
end

# anonymous function support for `@assume_effects`
@test fully_eliminated() do
map((2,3,4)) do x
# this :terminates_locally allows this anonymous function to be constant-folded
Base.@assume_effects :terminates_locally
res = 1
1 < x < 20 || error("bad pow")
while x > 1
res *= x
x -= 1
end
return res
end
end

# control flow backedge should taint `terminates`
@test Base.infer_effects((Int,)) do n
for i = 1:n; end
Expand Down
60 changes: 43 additions & 17 deletions test/compiler/inference.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3957,23 +3957,49 @@ g38888() = S38888(Base.inferencebarrier(3), nothing)
f_inf_error_bottom(x::Vector) = isempty(x) ? error(x[1]) : x
@test only(Base.return_types(f_inf_error_bottom, Tuple{Vector{Any}})) == Vector{Any}

# @constprop :aggressive
@noinline g_nonaggressive(y, x) = Val{x}()
@noinline Base.@constprop :aggressive g_aggressive(y, x) = Val{x}()

f_nonaggressive(x) = g_nonaggressive(x, 1)
f_aggressive(x) = g_aggressive(x, 1)

# The first test just makes sure that improvements to the compiler don't
# render the annotation effectless.
@test Base.return_types(f_nonaggressive, Tuple{Int})[1] == Val
@test Base.return_types(f_aggressive, Tuple{Int})[1] == Val{1}

# @constprop :none
@noinline Base.@constprop :none g_noaggressive(flag::Bool) = flag ? 1 : 1.0
ftrue_noaggressive() = g_noaggressive(true)
@test only(Base.return_types(ftrue_noaggressive, Tuple{})) == Union{Int,Float64}

# @constprop annotation
@noinline f_constprop_simple(f, x) = (f(x); Val{x}())
Base.@constprop :aggressive f_constprop_aggressive(f, x) = (f(x); Val{x}())
Base.@constprop :aggressive @noinline f_constprop_aggressive_noinline(f, x) = (f(x); Val{x}())
Base.@constprop :none f_constprop_none(f, x) = (f(x); Val{x}())
Base.@constprop :none @inline f_constprop_none_inline(f, x) = (f(x); Val{x}())

@test !Core.Compiler.is_aggressive_constprop(only(methods(f_constprop_simple)))
@test !Core.Compiler.is_no_constprop(only(methods(f_constprop_simple)))
@test Core.Compiler.is_aggressive_constprop(only(methods(f_constprop_aggressive)))
@test !Core.Compiler.is_no_constprop(only(methods(f_constprop_aggressive)))
@test Core.Compiler.is_aggressive_constprop(only(methods(f_constprop_aggressive_noinline)))
@test !Core.Compiler.is_no_constprop(only(methods(f_constprop_aggressive_noinline)))
@test !Core.Compiler.is_aggressive_constprop(only(methods(f_constprop_none)))
@test Core.Compiler.is_no_constprop(only(methods(f_constprop_none)))
@test !Core.Compiler.is_aggressive_constprop(only(methods(f_constprop_none_inline)))
@test Core.Compiler.is_no_constprop(only(methods(f_constprop_none_inline)))

# make sure that improvements to the compiler don't render the annotation effectless.
@test Base.return_types((Function,)) do f
f_constprop_simple(f, 1)
end |> only == Val
@test Base.return_types((Function,)) do f
f_constprop_aggressive(f, 1)
end |> only == Val{1}
@test Base.return_types((Function,)) do f
f_constprop_aggressive_noinline(f, 1)
end |> only == Val{1}
@test Base.return_types((Function,)) do f
f_constprop_none(f, 1)
end |> only == Val
@test Base.return_types((Function,)) do f
f_constprop_none_inline(f, 1)
end |> only == Val

# anonymous function support for `@constprop`
@test Base.return_types((Function,)) do f
map((1,2,3)) do x
Base.@constprop :aggressive
f(x)
return Val{x}()
end
end |> only == Tuple{Val{1},Val{2},Val{3}}

function splat_lotta_unions()
a = Union{Tuple{Int},Tuple{String,Vararg{Int}},Tuple{Int,Vararg{Int}}}[(2,)][1]
Expand Down

0 comments on commit 1b3b630

Please sign in to comment.