Skip to content
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
113 changes: 98 additions & 15 deletions base/reflection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1878,6 +1878,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 @@ -1886,16 +1892,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 @@ -1920,29 +1942,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]`
Copy link
Member

Choose a reason for hiding this comment

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

We already have code like this to support the code_* macros. Can we avoid duplicating work here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, we probably want to factor out destructure_callex with

function gen_call_with_extracted_types(__module__, fcn, ex0, kws=Expr[])

Copy link
Member Author

Choose a reason for hiding this comment

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

But maybe with a separate PR.

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