-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
InteractiveUtils: Support callable objects as functions in introspection macros #58905
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
InteractiveUtils: Support callable objects as functions in introspection macros #58905
Conversation
This allows you to call, e.g., `code_typed((Foo, Int, Int))` to query the code for `(::Foo)(::Int, ::Int)`
… `code_warntype`
This allows you to call, e.g., `code_typed((Foo, Int, Int))` to query the code for `(::Foo)(::Int, ::Int)`
… `code_warntype`
Has this impacted 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 |
Oh, I think we had a branch on |
Perhaps we can have that branch for |
Yeah, that's why I ended up adding I wonder if some equivalent should be moved into |
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 It seems cleaner to use the |
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 |
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. |
…ia; branch 'master' of github.com:JuliaLang/julia into interactive-utils-functors
1aad66f
to
551e0ea
Compare
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. |
There was a problem hiding this 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.
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). |
@topolarity does the docstring look clear enough to you? I think I included pretty much everything that developers need to know. Suggestions welcome! |
Reads beautifully (and so many of my old questions answered!) No notes 👍 I think this is ready to merge |
@serenity4 Can you fix up the conflicts + failing doctest here? |
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 |
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.
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)
@topolarity feel free to take a quick look at the recent commits, and otherwise merge. Thanks for having followed this PR until now 💯 |
There was a problem hiding this 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
function reescape(f::Function, @nospecialize ex) | ||
isa(ex, Expr) || return f(ex) | ||
unescaped = Meta.unescape(ex) | ||
new = f(unescaped) | ||
return reescape(new, ex) | ||
end |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
isexpr(kwarg, :escape) && continue | ||
isexpr(kwarg, :var"hygienic-scope") && continue |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 ...
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)
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 |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
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
by providing an extra
use_signature_tuple::Bool = false
parameter ingen_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).