Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1fae8e7
Support "Functor-like" `code_typed` invocation
topolarity Mar 27, 2025
395feea
Base: Support functors in `method_instance(s)`, `code_ircode`, and `c…
topolarity Jul 3, 2025
16c6e5e
InteractiveUtils: Support functors in `code_llvm`, `code_native`, and…
topolarity Jul 3, 2025
33e4b05
Support "Functor-like" `code_typed` invocation
topolarity Mar 27, 2025
3ffce1e
Base: Support functors in `method_instance(s)`, `code_ircode`, and `c…
topolarity Jul 3, 2025
ec2f4c8
InteractiveUtils: Support functors in `code_llvm`, `code_native`, and…
topolarity Jul 3, 2025
ef7727e
Add support for directly providing signature tuple
serenity4 Jul 4, 2025
394f15d
Merge branch 'master' of github.com:JuliaLang/julia into interactive-…
serenity4 Jul 4, 2025
76f030c
Merge branch 'ct/functor-reflection' of github.com:topolarity/julia i…
serenity4 Jul 4, 2025
84dd768
Merge branch 'master' into interactive-utils-functors
serenity4 Jul 7, 2025
865739e
Merge branch 'master' of github.com:JuliaLang/julia into interactive-…
serenity4 Jul 7, 2025
5f5db13
Improve error message
serenity4 Aug 6, 2025
57fbef2
Merge branch 'interactive-utils-functors' of github.com:serenity4/jul…
serenity4 Aug 6, 2025
551e0ea
Fix implementation for OpaqueClosure
serenity4 Aug 18, 2025
12dacbe
Add additional test
serenity4 Aug 18, 2025
8dea09e
Document `gen_call_with_extracted_types`
serenity4 Aug 19, 2025
c9ec156
Enable support for callable objects for source reflection macros
serenity4 Aug 19, 2025
73a9256
Add `kws...` in docs for `fcn` usage
serenity4 Aug 19, 2025
e7b88ee
Merge branch 'master' into interactive-utils-functors
serenity4 Aug 21, 2025
c8b7dc7
Properly handle (re)escaping and replace_ref_begin_end! interactions
serenity4 Aug 25, 2025
ae98622
Merge branch 'master' of github.com:JuliaLang/julia into interactive-…
serenity4 Aug 25, 2025
8a60b9f
Fix doctest failure
serenity4 Aug 25, 2025
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
103 changes: 53 additions & 50 deletions stdlib/InteractiveUtils/src/macros.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,19 @@ end

function rewrap_where(ex::Expr, where_params::Union{Nothing, Vector{Any}})
isnothing(where_params) && return ex
Expr(:where, ex, esc.(where_params)...)
Expr(:where, ex, where_params...)
end

get_typeof(ex::Ref) = ex[]
function get_typeof(@nospecialize ex)
isexpr(ex, :(::), 1) && return esc(ex.args[1])
isexpr(ex, :(::), 2) && return esc(ex.args[2])
isexpr(ex, :(::), 1) && return ex.args[1]
isexpr(ex, :(::), 2) && return ex.args[2]
if isexpr(ex, :..., 1)
splatted = ex.args[1]
isexpr(splatted, :(::), 1) && return Expr(:curly, :Vararg, esc(splatted.args[1]))
return :(Any[Core.Typeof(x) for x in $(esc(splatted))]...)
isexpr(splatted, :(::), 1) && return Expr(:curly, :Vararg, splatted.args[1])
return :(Any[Core.Typeof(x) for x in $splatted]...)
end
return :(Core.Typeof($(esc(ex))))
return :(Core.Typeof($ex))
end

function is_broadcasting_call(ex)
Expand Down Expand Up @@ -94,8 +95,8 @@ function recursive_dotcalls!(ex, args, i=1)
end

function extract_farg(@nospecialize arg)
!isexpr(arg, :(::), 1) && return esc(arg)
fT = esc(arg.args[1])
!isexpr(arg, :(::), 1) && return arg
fT = arg.args[1]
:($construct_callable($fT))
end

Expand All @@ -105,7 +106,8 @@ function construct_callable(@nospecialize(func::Type))
# Don't support type annotations otherwise, we don't want to give wrong answers
# for callables such as `(::Returns{Int})(args...)` where using `Returns{Int}`
# would give us code for the constructor, not for the callable object.
throw(ArgumentError("If a function type is explicitly provided, it must be a singleton whose only instance is the callable object"))
throw(ArgumentError("If a function type is explicitly provided, it must be a singleton whose only instance is the callable object.
To alleviate this restriction, the reflection macro may set `use_signature_tuple = true` if using `gen_call_with_extracted_types`."))
end

function separate_kwargs(exs::Vector{Any})
Expand Down Expand Up @@ -147,7 +149,7 @@ function generate_merged_namedtuple_type(kwargs::Vector{Any})
push!(nts, generate_namedtuple_type(ntargs))
empty!(ntargs)
end
push!(nts, Expr(:call, typeof_nt, esc(ex.args[1])))
push!(nts, Expr(:call, typeof_nt, ex.args[1]))
elseif isexpr(ex, :kw, 2)
push!(ntargs, ex.args[1]::Symbol => get_typeof(ex.args[2]))
elseif isexpr(ex, :(::), 2)
Expand Down Expand Up @@ -190,12 +192,30 @@ function merge_namedtuple_types(nt::Type{<:NamedTuple}, nts::Type{<:NamedTuple}.
end
end
end
NamedTuple{Tuple(names), Tuple{types...}}
return NamedTuple{Tuple(names), Tuple{types...}}
end

function gen_call(fcn, args, where_params, kws; use_signature_tuple::Bool)
f, args... = args
args = collect(Any, args)
!use_signature_tuple && return :($fcn($(esc(extract_farg(f))), $(esc(typesof_expr(args, where_params))); $(kws...)))
# We use a signature tuple only if we are sure we won't get an opaque closure as first argument.
# If we do get one, we have to use the 2-argument form.
with_signature_tuple = :($fcn($(esc(typesof_expr(Any[f, args...], where_params))); $(kws...)))
isexpr(f, :(::)) && return with_signature_tuple # we have a type, not a value, so not an OpaqueClosure
return quote
f = $(esc(f))
if isa(f, Core.OpaqueClosure)
$fcn(f, $(esc(typesof_expr(args, where_params))); $(kws...))
else
$with_signature_tuple
end
end
end

is_code_macro(fcn) = startswith(string(fcn), "code_")

function gen_call_with_extracted_types(__module__, fcn, ex0, kws = Expr[]; is_source_reflection = !is_code_macro(fcn), supports_binding_reflection = false)
function gen_call_with_extracted_types(__module__, fcn, ex0, kws = Expr[]; is_source_reflection = !is_code_macro(fcn), supports_binding_reflection = false, use_signature_tuple = false)
if isexpr(ex0, :ref)
ex0 = replace_ref_begin_end!(ex0)
end
Expand Down Expand Up @@ -249,10 +269,10 @@ function gen_call_with_extracted_types(__module__, fcn, ex0, kws = Expr[]; is_so
end
fully_qualified_symbol &= ex1 isa Symbol
if fully_qualified_symbol || isexpr(ex1, :(::), 1)
call_reflection = :($(fcn)(Base.getproperty, $(typesof_expr(ex0.args, where_params))))
call_reflection = gen_call(fcn, [:(Base.getproperty); ex0.args], where_params, kws; use_signature_tuple)
isexpr(ex0.args[1], :(::), 1) && return call_reflection
if supports_binding_reflection
binding_reflection = :($fcn(arg1, $(ex0.args[2])))
binding_reflection = :($fcn(arg1, $(ex0.args[2]); $(kws...)))
else
binding_reflection = :(error("expression is not a function call"))
end
Expand All @@ -279,28 +299,22 @@ function gen_call_with_extracted_types(__module__, fcn, ex0, kws = Expr[]; is_so
$(esc(ex0)) # trigger syntax errors if any
end
nt = generate_merged_namedtuple_type(kwargs)
tt = rewrap_where(:(Tuple{$nt, $(get_typeof.(args)...)}), where_params)
return :($(fcn)(Core.kwcall, $tt; $(kws...)))
nt = Ref(nt) # ignore `get_typeof` handling
Copy link
Member

Choose a reason for hiding this comment

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

This seems very sketchy, since Ref(nt) is a perfectly fine, and somewhat common, thing to normally encounter in an AST already (after this function called macroexpand). There should not be any unique behavior of calling typeof based on the value of a object found in the code.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's... completely true. That should be changed.

return gen_call(fcn, Any[:(Core.kwcall), nt, args...], where_params, kws; use_signature_tuple)
elseif ex0.head === :call
argtypes = Any[get_typeof(arg) for arg in ex0.args[2:end]]
args = copy(ex0.args)
if ex0.args[1] === :^ && length(ex0.args) >= 3 && isa(ex0.args[3], Int)
farg = :(Base.literal_pow)
pushfirst!(argtypes, :(typeof(^)))
argtypes[3] = :(Val{$(ex0.args[3])})
else
farg = extract_farg(ex0.args[1])
pushfirst!(args, :(Base.literal_pow))
args[4] = :(Val($(ex0.args[3])))
end
tt = rewrap_where(:(Tuple{$(argtypes...)}), where_params)
return Expr(:call, fcn, farg, tt, kws...)
return gen_call(fcn, args, where_params, kws; use_signature_tuple)
elseif ex0.head === :(=) && length(ex0.args) == 2
lhs, rhs = ex0.args
if isa(lhs, Expr)
if lhs.head === :(.)
return Expr(:call, fcn, Base.setproperty!,
typesof_expr(Any[lhs.args..., rhs], where_params), kws...)
return gen_call(fcn, Any[:(Base.setproperty!), lhs.args..., rhs], where_params, kws; use_signature_tuple)
elseif lhs.head === :ref
return Expr(:call, fcn, Base.setindex!,
typesof_expr(Any[lhs.args[1], rhs, lhs.args[2:end]...], where_params), kws...)
return gen_call(fcn, Any[:(Base.setindex!), lhs.args[1], rhs, lhs.args[2:end]...], where_params, kws; use_signature_tuple)
end
end
elseif ex0.head === :vcat || ex0.head === :typed_vcat
Expand All @@ -316,54 +330,43 @@ function gen_call_with_extracted_types(__module__, fcn, ex0, kws = Expr[]; is_so
lens = map(length, rows)
args = Any[Expr(:tuple, lens...); vcat(rows...)]
ex0.head === :typed_vcat && pushfirst!(args, ex0.args[1])
return Expr(:call, fcn, hf, typesof_expr(args, where_params), kws...)
return gen_call(fcn, Any[hf, args...], where_params, kws; use_signature_tuple)
else
return Expr(:call, fcn, f, typesof_expr(ex0.args, where_params), kws...)
return gen_call(fcn, Any[f, ex0.args...], where_params, kws; use_signature_tuple)
end
else
for (head, f) in (:ref => Base.getindex, :hcat => Base.hcat, :(.) => Base.getproperty, :vect => Base.vect, Symbol("'") => Base.adjoint, :typed_hcat => Base.typed_hcat, :string => string)
if ex0.head === head
return Expr(:call, fcn, f, typesof_expr(ex0.args, where_params), kws...)
return gen_call(fcn, Any[f, ex0.args...], where_params, kws; use_signature_tuple)
end
end
end
end
if isa(ex0, Expr) && ex0.head === :macrocall # Make @edit @time 1+2 edit the macro by using the types of the *expressions*
return Expr(:call, fcn, esc(ex0.args[1]), Tuple{#=__source__=#LineNumberNode, #=__module__=#Module, Any[ Core.Typeof(a) for a in ex0.args[3:end] ]...}, kws...)
args = [#=__source__::=#LineNumberNode, #=__module__::=#Module, Core.Typeof.(ex0.args[3:end])...]
return gen_call(fcn, Any[ex0.args[1], Ref.(args)...], where_params, kws; use_signature_tuple)
Copy link
Member

Choose a reason for hiding this comment

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

same comment about Ref.() being problematic here (because it shouldn't affect behavior and because is is the wrong way to map to an Any array)

end

ex = Meta.lower(__module__, ex0)
if !isa(ex, Expr)
return Expr(:call, :error, "expression is not a function call or symbol")
end

exret = Expr(:none)
if ex.head === :call
if any(@nospecialize(x) -> isexpr(x, :...), ex0.args) &&
(ex.args[1] === GlobalRef(Core,:_apply_iterate) ||
ex.args[1] === GlobalRef(Base,:_apply_iterate))
# check for splatting
Comment on lines -389 to -392
Copy link
Member

Choose a reason for hiding this comment

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

Did this get deleted by accident? I don't see why you are still calling Meta.lower if you unconditionally ignore the result now (and I'm also not entirely certain what this part of the code did before either).

Copy link
Member Author

Choose a reason for hiding this comment

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

I never encountered any execution of that code, and at some point it was committed with an unconditional error (it was using an undefined variable) so I am quite confident it was dead code. My speculative explanation for it would be that we started generating _apply_iterate expressions differently at some point so this wasn't hit anymore.

Meta.lower was just used with the previous check (isa(ex, Expr)) but I agree that we should probably use that other error below instead regardless.

exret = Expr(:call, ex.args[2], fcn,
Expr(:tuple, extract_farg(ex.args[3]), typesof_expr(ex.args[4:end], where_params)))
else
exret = Expr(:call, fcn, extract_farg(ex.args[1]), typesof_expr(ex.args[2:end], where_params), kws...)
end
end
if ex.head === :thunk || exret.head === :none
exret = Expr(:call, :error, "expression is not a function call, "
return Expr(:call, :error, "expression is not a function call, "
* "or is too complex for @$fcn to analyze; "
* "break it down to simpler parts if possible. "
* "In some cases, you may want to use Meta.@lower.")
end
return exret
return Expr(:none)
end

"""
Same behaviour as `gen_call_with_extracted_types` except that keyword arguments
of the form "foo=bar" are passed on to the called function as well.
The keyword arguments must be given before the mandatory argument.
"""
function gen_call_with_extracted_types_and_kwargs(__module__, fcn, ex0; is_source_reflection = !is_code_macro(fcn), supports_binding_reflection = false)
function gen_call_with_extracted_types_and_kwargs(__module__, fcn, ex0; is_source_reflection = !is_code_macro(fcn), supports_binding_reflection = false, use_signature_tuple = false)
kws = Expr[]
arg = ex0[end] # Mandatory argument
for i in 1:length(ex0)-1
Expand All @@ -377,7 +380,7 @@ function gen_call_with_extracted_types_and_kwargs(__module__, fcn, ex0; is_sourc
return Expr(:call, :error, "@$fcn expects only one non-keyword argument")
end
end
return gen_call_with_extracted_types(__module__, fcn, arg, kws; is_source_reflection, supports_binding_reflection)
return gen_call_with_extracted_types(__module__, fcn, arg, kws; is_source_reflection, supports_binding_reflection, use_signature_tuple)
end

for fname in [:which, :less, :edit, :functionloc]
Expand All @@ -398,13 +401,13 @@ end
for fname in [:code_warntype, :code_llvm, :code_native,
:infer_return_type, :infer_effects, :infer_exception_type]
@eval macro ($fname)(ex0...)
gen_call_with_extracted_types_and_kwargs(__module__, $(QuoteNode(fname)), ex0; is_source_reflection = false)
gen_call_with_extracted_types_and_kwargs(__module__, $(QuoteNode(fname)), ex0; is_source_reflection = false, use_signature_tuple = $(in(fname, [:code_warntype, :code_llvm, :code_native])))
end
end

for fname in [:code_typed, :code_lowered, :code_ircode]
@eval macro ($fname)(ex0...)
thecall = gen_call_with_extracted_types_and_kwargs(__module__, $(QuoteNode(fname)), ex0; is_source_reflection = false)
thecall = gen_call_with_extracted_types_and_kwargs(__module__, $(QuoteNode(fname)), ex0; is_source_reflection = false, use_signature_tuple = true)
quote
local results = $thecall
length(results) == 1 ? results[1] : results
Expand Down
28 changes: 28 additions & 0 deletions stdlib/InteractiveUtils/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ end
@test (@which Int[::Int 2;3 (::Int)]).name === :typed_hvcat
@test (@which (::Vector{Float64})').name === :adjoint
@test (@which "$(::Symbol) is a symbol").sig === Tuple{typeof(string), Vararg{Union{Char, String, Symbol}}}
@test (@which (::Int)^4).name === :literal_pow
@test (@which +(some_x::Int, some_y::Float64)).name === :+
@test (@which +(::Any, ::Any, ::Any, ::Any...)).sig === Tuple{typeof(+), Any, Any, Any, Vararg{Any}}
@test (@which +(::Any, ::Any, ::Any, ::Vararg{Any})).sig === Tuple{typeof(+), Any, Any, Any, Vararg{Any}}
Expand Down Expand Up @@ -392,6 +393,33 @@ end
@test (@code_typed optimize=false ::Vector{Int} .= ::Int)[2] == Vector{Int}
@test (@code_typed optimize=false ::Vector{Float64} .= 1 .+ ::Vector{Int})[2] == Vector{Float64}
@test (@code_typed optimize=false ::Vector{Float64} .= 1 .+ round.(base = ::Int, ::Vector{Int}; digits = 3))[2] == Vector{Float64}

@testset "Callable objects" begin
@test (@code_typed (::Base.Fix2{typeof(+), Float64})(3))[2] === Float64
@test (@code_typed optimize=false (::Returns{Float64})(::Int64; name::String))[2] === Float64
@test (@code_typed (::Returns{T})(3.0) where {T<:Real})[2] === Real
end

@testset "Opaque closures" begin
opaque_f(@nospecialize(x::Type), @nospecialize(y::Type)) = sizeof(x) == sizeof(y)
src, _ = only(code_typed(opaque_f, (Type, Type)))
src.slottypes[1] = Tuple{}

# from CodeInfo
oc = Core.OpaqueClosure(src; sig = Tuple{Type, Type}, rettype = Bool, nargs = 2)
ret = @code_typed oc(Int64, Float64)
@test [ret] == code_typed(oc)
_, rt = ret
@test rt === Bool

# from optimized IR
ir = Core.Compiler.inflate_ir(src)
oc = Core.OpaqueClosure(ir)
ret = @code_typed oc(Int64, Float64)
@test [ret] == code_typed(oc)
_, rt = ret
@test rt === Bool
end
end

module MacroTest
Expand Down