From 3b3d7bc72a482071ee2bcd302e468350987b04a3 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Fri, 25 Nov 2022 18:09:24 +0900 Subject: [PATCH] reflection: support additional call syntaxes for `@invoke[latest]` Like `@invoke (xs::Xs)[i::I] = v::V` and `@invokelatest x.f = v`. Co-Authored-By: Jameson Nash --- base/reflection.jl | 113 +++++++++++++++++++++++++++++++++++++++------ test/misc.jl | 103 +++++++++++++++++++++++++++++++++-------- 2 files changed, 181 insertions(+), 35 deletions(-) diff --git a/base/reflection.jl b/base/reflection.jl index de0296447be58..8ff89152c0a96 100644 --- a/base/reflection.jl +++ b/base/reflection.jl @@ -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 @@ -1886,6 +1892,18 @@ 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" @@ -1893,9 +1911,13 @@ julia> @invoke 420::Integer % Unsigned !!! 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...)) @@ -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]` + 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 diff --git a/test/misc.jl b/test/misc.jl index ee7f59bf67359..8a4b274978895 100644 --- a/test/misc.jl +++ b/test/misc.jl @@ -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 @@ -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 @@ -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