Skip to content

Conversation

serenity4
Copy link
Member

Follow-up to the follow-up #57911, building on the changes to introspection functions to support signature tuples being provided as a single argument.

This enables support for calls of the form

@code_typed (::Returns{Int})(1)
@code_llvm (::Base.Fix2{typeof(+), Float64})(::Int)

by providing an extra use_signature_tuple::Bool = false parameter in gen_call_with_extracted_types. Setting this parameter to true changes the code generation from $fcn(f, Tuple{argtypes...}) to $fcn(Tuple{f, argtypes...}) (where $fcn can be e.g. code_typed, code_llvm etc).

topolarity and others added 7 commits July 3, 2025 13:49
This allows you to call, e.g., `code_typed((Foo, Int, Int))` to query
the code for `(::Foo)(::Int, ::Int)`
This allows you to call, e.g., `code_typed((Foo, Int, Int))` to query
the code for `(::Foo)(::Int, ::Int)`
@serenity4 serenity4 requested a review from topolarity July 4, 2025 14:40
@serenity4 serenity4 changed the title Support callable objects as functions in introspection macros InteractiveUtils: Support callable objects as functions in introspection macros Jul 4, 2025
@serenity4 serenity4 self-assigned this Jul 4, 2025
@topolarity
Copy link
Member

Has this impacted OpaqueClosure support on accident?

julia> const oc = Base.Experimental.@opaque Tuple{Int,Int}->Int (x,y)->(x+y)
(::Int64, ::Int64)->::Int64

julia> @code_typed oc(1,2)
OpaqueClosure(...) @ Core none:0 => Any

@serenity4
Copy link
Member Author

Oh, I think we had a branch on isa(f, OpaqueClosure) in the two-argument form of code_typed 🤔

@serenity4
Copy link
Member Author

serenity4 commented Jul 7, 2025

Perhaps we can have that branch for tt.parameters[1] <: OpaqueClosure for code_typed(tt::Type{<:Tuple})?

@topolarity
Copy link
Member

topolarity commented Jul 7, 2025

Yeah, that's why I ended up adding ArgInfo in #58891

I wonder if some equivalent should be moved into base/reflection.jl (or yeah, alternatively we could just make it the standard behavior of code_typed(::Type{<:Tuple}))

@serenity4
Copy link
Member Author

The tricky bit with opaque closures is that because every "method" can't be distinguished by their type alone, we need to provide the values to reflection functions (whereas at least for callables, we still use types)

To support that with the current implementation, we can use the 2-argument form for code_typed(f, tt) when we have an OpaqueClosure as first argument. The downside is that the macro will have to generate code for both code_typed(argtypes) and code_typed(f, tt) because we'll only know at runtime which branch should be taken.

It seems cleaner to use the ArgInfo form, but we can't assume the user function supports a method for it, otherwise all packages relying on gen_call_with_extracted_types_and_kwargs will stop working. Although, we could at least construct ArgInfo internally, then use a higher-order function to call a one-argument/two-argument method for a given function based on the type of arg0, so long as we never leak that internal structure to the user. I can give it a try.

@topolarity
Copy link
Member

otherwise all packages relying on gen_call_with_extracted_types_and_kwargs will stop working.

Isn't that already an issue with switching to the 1-arg form from the 2-arg form?

@serenity4
Copy link
Member Author

Isn't that already an issue with switching to the 1-arg form from the 2-arg form?

That's exactly why I only generate the 1-arg form conditionally on a non-default parameter being set. We could have a parameter that indicates whether or not to dispatch on ArgInfo, though.

@topolarity
Copy link
Member

Yeah, I agree with your breakdown.

I don't have a strong feeling among the alternatives, so I'd be fine if you want to pick one and see if the implementation feels all right.

serenity4 and others added 3 commits August 6, 2025 05:13
@serenity4 serenity4 force-pushed the interactive-utils-functors branch from 1aad66f to 551e0ea Compare August 18, 2025 11:07
@serenity4
Copy link
Member Author

I managed to fix the situation with fewer modifications than I thought it'd require. @topolarity feel free to have another look, then it should be good to merge.

Copy link
Member

@topolarity topolarity left a comment

Choose a reason for hiding this comment

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

Probably worth documenting the macro-dev-facing requirements for setting these boolean flags (I already forgot since last time, hah), but otherwise looks great.

@serenity4
Copy link
Member Author

You're right, I held off on documenting these things because I didn't want us to commit to anything but it would be helpful to macro developers using this functionality (and to core devs too).

@serenity4
Copy link
Member Author

@topolarity does the docstring look clear enough to you? I think I included pretty much everything that developers need to know. Suggestions welcome!

@topolarity
Copy link
Member

Reads beautifully (and so many of my old questions answered!)

No notes 👍 I think this is ready to merge

@topolarity
Copy link
Member

@serenity4 Can you fix up the conflicts + failing doctest here?

@serenity4
Copy link
Member Author

Yes, I started yesterday but #59239 brought a tough conflict (not a bad one though!). It changed the way that macro hygiene is handled in gen_call_with_extracted_types, and it took me some time to figure out what we should do. Notably because I also rewrote how we handle hygiene in this PR, and in a different direction than this other PR - and the design there is more composable than here so I'll most likely adopt it (unless I run into issues).

This commit refactors some of the recent additions to handle reescaping
in InteractiveUtils, along with tests to cover various hygiene contexts.

`replace_ref_begin_end!` was recently replaced in InteractiveUtils
by its low-level counterpart, `replace_ref_begin_end_!`, to better
handle escapes. However, we were using it in a way that
required duplication of the code generation logic to match the code
that we emit it for other code paths. Because the logic is now more complex with
the recently added `use_signature_tuple` behavior, it is preferable to
allow reuse of the same code path for all code we emit. This commit
implements this refactor, which unfortunately also requires a manual
escape of the temporary variable that is emitted by
`replace_ref_begin_end!` in its current design.
@serenity4
Copy link
Member Author

Alright, after quite a few efforts, I managed to come up with a conflict resolution I am more satisfied with in c8b7dc7 (this commit also shows removal of some of the parts from the merge that were commented out).

This replaces the example with a macro call
that has a simpler output, now that `edit`
generates a bigger expression (as it now
handles opaque closure arguments)
@serenity4
Copy link
Member Author

@topolarity feel free to take a quick look at the recent commits, and otherwise merge. Thanks for having followed this PR until now 💯

Copy link
Member

@topolarity topolarity left a comment

Choose a reason for hiding this comment

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

I haven't reviewed the hygiene changes, but otherwise this looks great to me.

Thanks for all the follow-through @serenity4

@topolarity topolarity merged commit b24014a into JuliaLang:master Aug 26, 2025
7 checks passed
Comment on lines +25 to +30
function reescape(f::Function, @nospecialize ex)
isa(ex, Expr) || return f(ex)
unescaped = Meta.unescape(ex)
new = f(unescaped)
return reescape(new, ex)
end
Copy link
Member

Choose a reason for hiding this comment

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

Why are you vendoring a buggy copy of Meta.reescape now?

Copy link
Member Author

Choose a reason for hiding this comment

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

What is buggy with its implementation? My understanding is that the pattern is:

  • Get the escaped argument (with Meta.unescape)
  • Process it to possibly generate a new Expr (that's "user"-defined)
  • Re-apply the original hygiene environment (escapes, hygienic scopes etc)

Copy link
Member

Choose a reason for hiding this comment

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

It is reimplementing something and it might execute any functions f instead of rewrapping them

Comment on lines +154 to +155
isexpr(kwarg, :escape) && continue
isexpr(kwarg, :var"hygienic-scope") && continue
Copy link
Member

Choose a reason for hiding this comment

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

These are invalid kwargs, unless unescaping them contains a valid kwarg

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 true, I guess the correct way would be to apply the check to whatever is inside.

assignment = decl.args[1]
isexpr(assignment, :(=), 2) || return
variable = assignment.args[1]
startswith(string(variable), "##S#") || return
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 like a bad idea to find some random variable by name and then apply random mutation to it (mainly it seems it would result in incorrect code being executed if someone has a statement with multiple begin or end like a[end][end])

@@ -262,10 +420,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, [getproperty; ex0.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.

Style (and performance) nit: we tend to use ... instead of ; for explicit concat, and an explicit Any type:

                call_reflection = gen_call(fcn, Any[getproperty, ex0.args...], where_params, kws; use_signature_tuple)

This seems to have been detected as a issue elsewhere already, but then incorrectly worked around by splatting ex0.args to undo the mistake here:

function gen_call(fcn, args, where_params, kws; use_signature_tuple::Bool, not_an_opaque_closure::Bool = true)
    f, args... = args
    args = collect(Any, args)

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, yeah I didn't notice this array was not typed with Any[. Performance/style-wise I thought that ; was preferred for this pattern, but I don't mind at all using ....

Copy link
Member Author

Choose a reason for hiding this comment

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

The args = collect(Any, args) statement was because I thought the args... syntax did use the type of actual arguments, rather than reuse the original array type, but if that's not the case the statement may indeed be removed.

Copy link
Member

@vtjnash vtjnash Aug 26, 2025

Choose a reason for hiding this comment

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

Good question. Seems like it keeps the original type:

julia> a,b... = Any[1,2,3]
3-element Vector{Any}:
 1
 2
 3

julia> b
2-element Vector{Any}:
 2
 3

... implies vect and ; implies vcat. It is thus strange to have vcat when some of the elements are not arrays, though perhaps efficient. With Any[...] it actually implies getindex, and getindex(::Type{Any}) is marked no specialize.

@@ -292,41 +450,24 @@ 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.

else
PairSymAny = Pair{Symbol, Any}
for (head, f) in (PairSymAny(:hcat, Base.hcat),
Copy link
Member

Choose a reason for hiding this comment

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

Please reapply this change to add PairSymAny, so that this part of the code can be type-stable

Copy link
Member Author

@serenity4 serenity4 Aug 26, 2025

Choose a reason for hiding this comment

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

Is there any actual benefit in doing so? I would mind in most cases, but not in a high-level macro like that, where I'd rather reduce visual noise instead.

Copy link
Member

Choose a reason for hiding this comment

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

I guess not, just seemed cleaner for future linters

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 think we have quite a few places (probably a lot of expression-processing functions notably) where we iterate ::Vector{Any} anyways, instead of iterating over indices then index into the array (which I guess might be the "best" pattern for type-stability).

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(ex0.args[i]) for i in 3:length(ex0.args) ]...}, 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)

Comment on lines -389 to -392
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
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants