Skip to content

Commit

Permalink
reflection: support additional call syntaxes for @invoke[latest] (J…
Browse files Browse the repository at this point in the history
…uliaLang#47705)

Like `@invoke (xs::Xs)[i::I] = v::V` and `@invokelatest x.f = v`.

Co-Authored-By: Jameson Nash <[email protected]>
  • Loading branch information
aviatesk and vtjnash authored Nov 29, 2022
1 parent f6e911a commit 4fd26ba
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 35 deletions.
113 changes: 98 additions & 15 deletions base/reflection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1885,6 +1885,12 @@ When an argument's type annotation is omitted, it's replaced with `Core.Typeof`
To invoke a method where an argument is untyped or explicitly typed as `Any`, annotate the
argument with `::Any`.
It also supports the following syntax:
- `@invoke (x::X).f` expands to `invoke(getproperty, Tuple{X,Symbol}, x, :f)`
- `@invoke (x::X).f = v::V` expands to `invoke(setproperty!, Tuple{X,Symbol,V}, x, :f, v)`
- `@invoke (xs::Xs)[i::I]` expands to `invoke(getindex, Tuple{Xs,I}, xs, i)`
- `@invoke (xs::Xs)[i::I] = v::V` expands to `invoke(setindex!, Tuple{Xs,V,I}, xs, v, i)`
# Examples
```jldoctest
Expand All @@ -1893,16 +1899,32 @@ julia> @macroexpand @invoke f(x::T, y)
julia> @invoke 420::Integer % Unsigned
0x00000000000001a4
julia> @macroexpand @invoke (x::X).f
:(Core.invoke(Base.getproperty, Tuple{X, Core.Typeof(:f)}, x, :f))
julia> @macroexpand @invoke (x::X).f = v::V
:(Core.invoke(Base.setproperty!, Tuple{X, Core.Typeof(:f), V}, x, :f, v))
julia> @macroexpand @invoke (xs::Xs)[i::I]
:(Core.invoke(Base.getindex, Tuple{Xs, I}, xs, i))
julia> @macroexpand @invoke (xs::Xs)[i::I] = v::V
:(Core.invoke(Base.setindex!, Tuple{Xs, V, I}, xs, v, i))
```
!!! compat "Julia 1.7"
This macro requires Julia 1.7 or later.
!!! compat "Julia 1.9"
This macro is exported as of Julia 1.9.
!!! compat "Julia 1.10"
The additional syntax is supported as of Julia 1.10.
"""
macro invoke(ex)
f, args, kwargs = destructure_callex(ex)
topmod = Core.Compiler._topmod(__module__) # well, except, do not get it via CC but define it locally
f, args, kwargs = destructure_callex(topmod, ex)
types = Expr(:curly, :Tuple)
out = Expr(:call, GlobalRef(Core, :invoke))
isempty(kwargs) || push!(out.args, Expr(:parameters, kwargs...))
Expand All @@ -1927,29 +1949,90 @@ Provides a convenient way to call [`Base.invokelatest`](@ref).
`@invokelatest f(args...; kwargs...)` will simply be expanded into
`Base.invokelatest(f, args...; kwargs...)`.
It also supports the following syntax:
- `@invokelatest x.f` expands to `Base.invokelatest(getproperty, x, :f)`
- `@invokelatest x.f = v` expands to `Base.invokelatest(setproperty!, x, :f, v)`
- `@invokelatest xs[i]` expands to `invoke(getindex, xs, i)`
- `@invokelatest xs[i] = v` expands to `invoke(setindex!, xs, v, i)`
```jldoctest
julia> @macroexpand @invokelatest f(x; kw=kwv)
:(Base.invokelatest(f, x; kw = kwv))
julia> @macroexpand @invokelatest x.f
:(Base.invokelatest(Base.getproperty, x, :f))
julia> @macroexpand @invokelatest x.f = v
:(Base.invokelatest(Base.setproperty!, x, :f, v))
julia> @macroexpand @invokelatest xs[i]
:(Base.invokelatest(Base.getindex, xs, i))
julia> @macroexpand @invokelatest xs[i] = v
:(Base.invokelatest(Base.setindex!, xs, v, i))
```
!!! compat "Julia 1.7"
This macro requires Julia 1.7 or later.
!!! compat "Julia 1.10"
The additional syntax is supported as of Julia 1.10.
"""
macro invokelatest(ex)
f, args, kwargs = destructure_callex(ex)
return esc(:($(GlobalRef(@__MODULE__, :invokelatest))($(f), $(args...); $(kwargs...))))
topmod = Core.Compiler._topmod(__module__) # well, except, do not get it via CC but define it locally
f, args, kwargs = destructure_callex(topmod, ex)
out = Expr(:call, GlobalRef(Base, :invokelatest))
isempty(kwargs) || push!(out.args, Expr(:parameters, kwargs...))
push!(out.args, f)
append!(out.args, args)
return esc(out)
end

function destructure_callex(ex)
isexpr(ex, :call) || throw(ArgumentError("a call expression f(args...; kwargs...) should be given"))
function destructure_callex(topmod::Module, @nospecialize(ex))
function flatten(xs)
out = Any[]
for x in xs
if isexpr(x, :tuple)
append!(out, x.args)
else
push!(out, x)
end
end
return out
end

f = first(ex.args)
args = []
kwargs = []
for x in ex.args[2:end]
if isexpr(x, :parameters)
append!(kwargs, x.args)
elseif isexpr(x, :kw)
push!(kwargs, x)
kwargs = Any[]
if isexpr(ex, :call) # `f(args...)`
f = first(ex.args)
args = Any[]
for x in ex.args[2:end]
if isexpr(x, :parameters)
append!(kwargs, x.args)
elseif isexpr(x, :kw)
push!(kwargs, x)
else
push!(args, x)
end
end
elseif isexpr(ex, :.) # `x.f`
f = GlobalRef(topmod, :getproperty)
args = flatten(ex.args)
elseif isexpr(ex, :ref) # `x[i]`
f = GlobalRef(topmod, :getindex)
args = flatten(ex.args)
elseif isexpr(ex, :(=)) # `x.f = v` or `x[i] = v`
lhs, rhs = ex.args
if isexpr(lhs, :.)
f = GlobalRef(topmod, :setproperty!)
args = flatten(Any[lhs.args..., rhs])
elseif isexpr(lhs, :ref)
f = GlobalRef(topmod, :setindex!)
args = flatten(Any[lhs.args[1], rhs, lhs.args[2]])
else
push!(args, x)
throw(ArgumentError("expected a `setproperty!` expression `x.f = v` or `setindex!` expression `x[i] = v`"))
end
else
throw(ArgumentError("expected a `:call` expression `f(args...; kwargs...)`"))
end

return f, args, kwargs
end
103 changes: 83 additions & 20 deletions test/misc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -906,38 +906,87 @@ end
module atinvokelatest
f(x) = 1
g(x, y; z=0) = x * y + z
mutable struct X; x; end
Base.getproperty(::X, ::Any) = error("overload me")
Base.setproperty!(::X, ::Any, ::Any) = error("overload me")
struct Xs
xs::Vector{Any}
end

let foo() = begin
@eval atinvokelatest.f(x::Int) = 3
return Base.@invokelatest atinvokelatest.f(0)
end
@test foo() == 3
Base.getindex(::Xs, ::Any) = error("overload me")
Base.setindex!(::Xs, ::Any, ::Any) = error("overload me")
end

let foo() = begin
let call_test() = begin
@eval atinvokelatest.f(x::Int) = 3
return Base.@invokelatest atinvokelatest.f(0)
return @invokelatest atinvokelatest.f(0)
end
@test foo() == 3
@test call_test() == 3

bar() = begin
call_with_kws_test() = begin
@eval atinvokelatest.g(x::Int, y::Int; z=3) = z
return Base.@invokelatest atinvokelatest.g(2, 3; z=1)
return @invokelatest atinvokelatest.g(2, 3; z=1)
end
@test call_with_kws_test() == 1

getproperty_test() = begin
@eval Base.getproperty(x::atinvokelatest.X, f::Symbol) = getfield(x, f)
x = atinvokelatest.X(nothing)
return @invokelatest x.x
end
@test isnothing(getproperty_test())

setproperty!_test() = begin
@eval Base.setproperty!(x::atinvokelatest.X, f::Symbol, @nospecialize(v)) = setfield!(x, f, v)
x = atinvokelatest.X(nothing)
@invokelatest x.x = 1
return x
end
@test bar() == 1
x = setproperty!_test()
@test getfield(x, :x) == 1

getindex_test() = begin
@eval Base.getindex(xs::atinvokelatest.Xs, idx::Int) = xs.xs[idx]
xs = atinvokelatest.Xs(Any[nothing])
return @invokelatest xs[1]
end
@test isnothing(getindex_test())

setindex!_test() = begin
@eval function Base.setindex!(xs::atinvokelatest.Xs, @nospecialize(v), idx::Int)
xs.xs[idx] = v
end
xs = atinvokelatest.Xs(Any[nothing])
@invokelatest xs[1] = 1
return xs
end
xs = setindex!_test()
@test xs.xs[1] == 1
end

abstract type InvokeX end
Base.getproperty(::InvokeX, ::Symbol) = error("overload InvokeX")
Base.setproperty!(::InvokeX, ::Symbol, @nospecialize(v::Any)) = error("overload InvokeX")
mutable struct InvokeX2 <: InvokeX; x; end
Base.getproperty(x::InvokeX2, f::Symbol) = getfield(x, f)
Base.setproperty!(x::InvokeX2, f::Symbol, @nospecialize(v::Any)) = setfield!(x, f, v)

abstract type InvokeXs end
Base.getindex(::InvokeXs, ::Int) = error("overload InvokeXs")
Base.setindex!(::InvokeXs, @nospecialize(v::Any), ::Int) = error("overload InvokeXs")
struct InvokeXs2 <: InvokeXs
xs::Vector{Any}
end
Base.getindex(xs::InvokeXs2, idx::Int) = xs.xs[idx]
Base.setindex!(xs::InvokeXs2, @nospecialize(v::Any), idx::Int) = xs.xs[idx] = v

@testset "@invoke macro" begin
# test against `invoke` doc example
let
f(x::Real) = x^2
let f(x::Real) = x^2
f(x::Integer) = 1 + @invoke f(x::Real)
@test f(2) == 5
end

let
f1(::Integer) = Integer
let f1(::Integer) = Integer
f1(::Real) = Real;
f2(x::Real) = _f2(x)
_f2(::Integer) = Integer
Expand All @@ -949,8 +998,7 @@ end
end

# when argment's type annotation is omitted, it should be specified as `Core.Typeof(x)`
let
f(_) = Any
let f(_) = Any
f(x::Integer) = Integer
@test f(1) === Integer
@test @invoke(f(1::Any)) === Any
Expand All @@ -963,13 +1011,28 @@ end
end

# handle keyword arguments correctly
let
f(a; kw1 = nothing, kw2 = nothing) = a + max(kw1, kw2)
let f(a; kw1 = nothing, kw2 = nothing) = a + max(kw1, kw2)
f(::Integer; kwargs...) = error("don't call me")

@test_throws Exception f(1; kw1 = 1, kw2 = 2)
@test 3 == @invoke f(1::Any; kw1 = 1, kw2 = 2)
end

# additional syntax test
let x = InvokeX2(nothing)
@test_throws "overload InvokeX" @invoke (x::InvokeX).x
@test isnothing(@invoke x.x)
@test_throws "overload InvokeX" @invoke (x::InvokeX).x = 42
@invoke x.x = 42
@test 42 == x.x

xs = InvokeXs2(Any[nothing])
@test_throws "overload InvokeXs" @invoke (xs::InvokeXs)[1]
@test isnothing(@invoke xs[1])
@test_throws "overload InvokeXs" @invoke (xs::InvokeXs)[1] = 42
@invoke xs[1] = 42
@test 42 == xs.xs[1]
end
end

# Endian tests
Expand Down

0 comments on commit 4fd26ba

Please sign in to comment.