Skip to content

Commit c3e7b1b

Browse files
serenity4aviatesk
andauthored
InteractiveUtils: support type annotations as substitutes for values (#57909)
Extend code introspection macros (`@which`, `@code_typed` and friends) to recognize type annotations of the form `f(1, ::Float64)` as types to be forwarded as is to the relevant function. --------- Co-authored-by: Cédric Belmant <[email protected]> Co-authored-by: Shuhei Kadowaki <[email protected]>
1 parent d0703e7 commit c3e7b1b

File tree

5 files changed

+225
-56
lines changed

5 files changed

+225
-56
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ Standard library changes
4747

4848
#### InteractiveUtils
4949

50+
* Introspection utilities such as `@code_typed`, `@which` and `@edit` now accept type annotations as substitutes for values, recognizing forms such as `f(1, ::Float64, 3)` or even `sum(::Vector{T}; init = ::T) where {T<:Real}` ([#57909]).
51+
5052
External dependencies
5153
---------------------
5254

base/errorshow.jl

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,9 +1070,8 @@ Experimental.register_error_hint(nonsetable_type_hint_handler, MethodError)
10701070

10711071
# Display a hint in case the user tries to use the + operator on strings
10721072
# (probably attempting concatenation)
1073-
function string_concatenation_hint_handler(io, ex, arg_types, kwargs)
1074-
@nospecialize
1075-
if (ex.f === +) && !isempty(arg_types) && all(i -> i <: AbstractString, arg_types)
1073+
function string_concatenation_hint_handler(@nospecialize(io::IO), ex::MethodError, arg_types::Vector{Any}, kwargs::Vector{Any})
1074+
if (ex.f === +) && !isempty(arg_types) && all(@nospecialize(a) -> unwrapva(a) <: AbstractString, arg_types)
10761075
print(io, "\nString concatenation is performed with ")
10771076
printstyled(io, "*", color=:cyan)
10781077
print(io, " (See also: https://docs.julialang.org/en/v1/manual/strings/#man-concatenation).")

doc/src/base/reflection.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ be passed instead!). For example:
8787

8888
```jldoctest; setup = :(using InteractiveUtils)
8989
julia> InteractiveUtils.macroexpand(@__MODULE__, :(@edit println("")) )
90-
:(InteractiveUtils.edit(println, (Base.typesof)("")))
90+
:(InteractiveUtils.edit(println, InteractiveUtils.Tuple{(InteractiveUtils.Core).Typeof("")}))
9191
```
9292

9393
The functions `Base.Meta.show_sexpr` and [`dump`](@ref) are used to display S-expr style views

stdlib/InteractiveUtils/src/macros.jl

Lines changed: 161 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,34 @@
22

33
# macro wrappers for various reflection functions
44

5-
using Base: typesof, insert!, replace_ref_begin_end!,
6-
infer_return_type, infer_exception_type, infer_effects, code_ircode
5+
using Base: insert!, replace_ref_begin_end!,
6+
infer_return_type, infer_exception_type, infer_effects, code_ircode, isexpr
77

88
# defined in Base so it's possible to time all imports, including InteractiveUtils and its deps
99
# via. `Base.@time_imports` etc.
1010
import Base: @time_imports, @trace_compile, @trace_dispatch
1111

12-
separate_kwargs(args...; kwargs...) = (args, values(kwargs))
12+
typesof_expr(args::Vector{Any}, where_params::Union{Nothing, Vector{Any}} = nothing) = rewrap_where(:(Tuple{$(get_typeof.(args)...)}), where_params)
13+
14+
function extract_where_parameters(ex::Expr)
15+
isexpr(ex, :where) || return ex, nothing
16+
ex.args[1], ex.args[2:end]
17+
end
18+
19+
function rewrap_where(ex::Expr, where_params::Union{Nothing, Vector{Any}})
20+
isnothing(where_params) && return ex
21+
Expr(:where, ex, esc.(where_params)...)
22+
end
23+
24+
function get_typeof(@nospecialize ex)
25+
isexpr(ex, :(::), 1) && return esc(ex.args[1])
26+
if isexpr(ex, :..., 1)
27+
splatted = ex.args[1]
28+
isexpr(splatted, :(::), 1) && return Expr(:curly, :Vararg, esc(splatted.args[1]))
29+
return :(Any[Core.Typeof(x) for x in $(esc(splatted))]...)
30+
end
31+
return :(Core.Typeof($(esc(ex))))
32+
end
1333

1434
"""
1535
Transform a dot expression into one where each argument has been replaced by a
@@ -20,7 +40,7 @@ function recursive_dotcalls!(ex, args, i=1)
2040
if !(ex isa Expr) || ((ex.head !== :. || !(ex.args[2] isa Expr)) &&
2141
(ex.head !== :call || string(ex.args[1])[1] != '.'))
2242
newarg = Symbol('x', i)
23-
if Meta.isexpr(ex, :...)
43+
if isexpr(ex, :...)
2444
push!(args, only(ex.args))
2545
return Expr(:..., newarg), i+1
2646
else
@@ -37,20 +57,121 @@ function recursive_dotcalls!(ex, args, i=1)
3757
return ex, i
3858
end
3959

60+
function extract_farg(@nospecialize arg)
61+
!isexpr(arg, :(::), 1) && return esc(arg)
62+
fT = esc(arg.args[1])
63+
:($construct_callable($fT))
64+
end
65+
66+
function construct_callable(@nospecialize(func::Type))
67+
# Support function singleton types such as `(::typeof(f))(args...)`
68+
Base.issingletontype(func) && isdefined(func, :instance) && return func.instance
69+
# Don't support type annotations otherwise, we don't want to give wrong answers
70+
# for callables such as `(::Returns{Int})(args...)` where using `Returns{Int}`
71+
# would give us code for the constructor, not for the callable object.
72+
throw(ArgumentError("If a function type is explicitly provided, it must be a singleton whose only instance is the callable object"))
73+
end
74+
75+
function separate_kwargs(exs::Vector{Any})
76+
args = []
77+
kwargs = []
78+
for ex in exs
79+
if isexpr(ex, :kw)
80+
push!(kwargs, ex)
81+
elseif isexpr(ex, :parameters)
82+
for kw in ex.args
83+
push!(kwargs, kw)
84+
end
85+
else
86+
push!(args, ex)
87+
end
88+
end
89+
args, kwargs
90+
end
91+
92+
function are_kwargs_valid(kwargs::Vector{Any})
93+
for kwarg in kwargs
94+
isexpr(kwarg, :..., 1) && continue
95+
isexpr(kwarg, :kw, 2) && isa(kwarg.args[1], Symbol) && continue
96+
isa(kwarg, Symbol) && continue
97+
return false
98+
end
99+
return true
100+
end
101+
102+
# Generate an expression that merges `kwargs` onto a single `NamedTuple`
103+
function generate_merged_namedtuple_type(kwargs::Vector{Any})
104+
nts = Any[]
105+
ntargs = Pair{Symbol, Any}[]
106+
for ex in kwargs
107+
if isexpr(ex, :..., 1)
108+
if !isempty(ntargs)
109+
# Construct a `NamedTuple` containing the previous parameters.
110+
push!(nts, generate_namedtuple_type(ntargs))
111+
empty!(ntargs)
112+
end
113+
push!(nts, Expr(:call, typeof_nt, esc(ex.args[1])))
114+
elseif isexpr(ex, :kw, 2)
115+
push!(ntargs, ex.args[1]::Symbol => get_typeof(ex.args[2]))
116+
else
117+
ex::Symbol
118+
push!(ntargs, ex => get_typeof(ex))
119+
end
120+
end
121+
!isempty(ntargs) && push!(nts, generate_namedtuple_type(ntargs))
122+
return :($merge_namedtuple_types($(nts...)))
123+
end
124+
125+
function generate_namedtuple_type(ntargs::Vector{Pair{Symbol, Any}})
126+
names = Expr(:tuple)
127+
tt = Expr(:curly, :Tuple)
128+
for (name, type) in ntargs
129+
push!(names.args, QuoteNode(name))
130+
push!(tt.args, type)
131+
end
132+
return :(NamedTuple{$names, $tt})
133+
end
134+
135+
typeof_nt(nt::NamedTuple) = typeof(nt)
136+
typeof_nt(nt::Base.Pairs) = typeof(values(nt))
137+
138+
function merge_namedtuple_types(nt::Type{<:NamedTuple}, nts::Type{<:NamedTuple}...)
139+
@nospecialize
140+
isempty(nts) && return nt
141+
names = Symbol[]
142+
types = Any[]
143+
for nt in (nt, nts...)
144+
for (name, type) in zip(fieldnames(nt), fieldtypes(nt))
145+
i = findfirst(==(name), names)
146+
if isnothing(i)
147+
push!(names, name)
148+
push!(types, type)
149+
else
150+
types[i] = type
151+
end
152+
end
153+
end
154+
NamedTuple{Tuple(names), Tuple{types...}}
155+
end
156+
40157
function gen_call_with_extracted_types(__module__, fcn, ex0, kws=Expr[])
41-
if Meta.isexpr(ex0, :ref)
158+
if isexpr(ex0, :ref)
42159
ex0 = replace_ref_begin_end!(ex0)
43160
end
44161
# assignments get bypassed: @edit a = f(x) <=> @edit f(x)
45162
if isa(ex0, Expr) && ex0.head == :(=) && isa(ex0.args[1], Symbol) && isempty(kws)
46163
return gen_call_with_extracted_types(__module__, fcn, ex0.args[2])
47164
end
165+
where_params = nothing
166+
if isa(ex0, Expr)
167+
ex0, where_params = extract_where_parameters(ex0)
168+
end
48169
if isa(ex0, Expr)
49-
if ex0.head === :do && Meta.isexpr(get(ex0.args, 1, nothing), :call)
170+
if ex0.head === :do && isexpr(get(ex0.args, 1, nothing), :call)
50171
if length(ex0.args) != 2
51172
return Expr(:call, :error, "ill-formed do call")
52173
end
53-
i = findlast(a->(Meta.isexpr(a, :kw) || Meta.isexpr(a, :parameters)), ex0.args[1].args)
174+
i = findlast(@nospecialize(a)->(isexpr(a, :kw) || isexpr(a, :parameters)), ex0.args[1].args)
54175
args = copy(ex0.args[1].args)
55176
insert!(args, (isnothing(i) ? 2 : 1+i::Int), ex0.args[2])
56177
ex0 = Expr(:call, args...)
@@ -66,8 +187,7 @@ function gen_call_with_extracted_types(__module__, fcn, ex0, kws=Expr[])
66187
dotfuncdef = Expr(:local, Expr(:(=), Expr(:call, dotfuncname, xargs...), ex))
67188
return quote
68189
$(esc(dotfuncdef))
69-
local args = $typesof($(map(esc, args)...))
70-
$(fcn)($(esc(dotfuncname)), args; $(kws...))
190+
$(fcn)($(esc(dotfuncname)), $(typesof_expr(args, where_params)); $(kws...))
71191
end
72192
elseif !codemacro
73193
fully_qualified_symbol = true # of the form A.B.C.D
@@ -80,7 +200,9 @@ function gen_call_with_extracted_types(__module__, fcn, ex0, kws=Expr[])
80200
ex1 = ex1.args[1]
81201
end
82202
fully_qualified_symbol &= ex1 isa Symbol
83-
if fully_qualified_symbol
203+
if fully_qualified_symbol || isexpr(ex1, :(::), 1)
204+
getproperty_ex = :($(fcn)(Base.getproperty, $(typesof_expr(ex0.args, where_params))))
205+
isexpr(ex0.args[1], :(::), 1) && return getproperty_ex
84206
return quote
85207
local arg1 = $(esc(ex0.args[1]))
86208
if isa(arg1, Module)
@@ -90,8 +212,7 @@ function gen_call_with_extracted_types(__module__, fcn, ex0, kws=Expr[])
90212
:(error("expression is not a function call"))
91213
end)
92214
else
93-
local args = $typesof($(map(esc, ex0.args)...))
94-
$(fcn)(Base.getproperty, args)
215+
$getproperty_ex
95216
end
96217
end
97218
else
@@ -102,32 +223,35 @@ function gen_call_with_extracted_types(__module__, fcn, ex0, kws=Expr[])
102223
end
103224
end
104225
end
105-
if any(a->(Meta.isexpr(a, :kw) || Meta.isexpr(a, :parameters)), ex0.args)
106-
return quote
107-
local arg1 = $(esc(ex0.args[1]))
108-
local args, kwargs = $separate_kwargs($(map(esc, ex0.args[2:end])...))
109-
$(fcn)(Core.kwcall,
110-
Tuple{typeof(kwargs), Core.Typeof(arg1), map(Core.Typeof, args)...};
111-
$(kws...))
226+
if any(@nospecialize(a)->(isexpr(a, :kw) || isexpr(a, :parameters)), ex0.args)
227+
args, kwargs = separate_kwargs(ex0.args)
228+
are_kwargs_valid(kwargs) || return quote
229+
error("keyword argument format unrecognized; they must be of the form `x` or `x = <value>`")
230+
$(esc(ex0)) # trigger syntax errors if any
112231
end
232+
nt = generate_merged_namedtuple_type(kwargs)
233+
tt = rewrap_where(:(Tuple{$nt, $(get_typeof.(args)...)}), where_params)
234+
return :($(fcn)(Core.kwcall, $tt; $(kws...)))
113235
elseif ex0.head === :call
236+
argtypes = Any[get_typeof(arg) for arg in ex0.args[2:end]]
114237
if ex0.args[1] === :^ && length(ex0.args) >= 3 && isa(ex0.args[3], Int)
115-
return Expr(:call, fcn, :(Base.literal_pow),
116-
Expr(:call, typesof, esc(ex0.args[1]), esc(ex0.args[2]),
117-
esc(Val(ex0.args[3]))))
238+
farg = :(Base.literal_pow)
239+
pushfirst!(argtypes, :(typeof(^)))
240+
argtypes[3] = :(Val{$(ex0.args[3])})
241+
else
242+
farg = extract_farg(ex0.args[1])
118243
end
119-
return Expr(:call, fcn, esc(ex0.args[1]),
120-
Expr(:call, typesof, map(esc, ex0.args[2:end])...),
121-
kws...)
244+
tt = rewrap_where(:(Tuple{$(argtypes...)}), where_params)
245+
return Expr(:call, fcn, farg, tt, kws...)
122246
elseif ex0.head === :(=) && length(ex0.args) == 2
123247
lhs, rhs = ex0.args
124248
if isa(lhs, Expr)
125249
if lhs.head === :(.)
126250
return Expr(:call, fcn, Base.setproperty!,
127-
Expr(:call, typesof, map(esc, lhs.args)..., esc(rhs)), kws...)
251+
typesof_expr(Any[lhs.args..., rhs], where_params), kws...)
128252
elseif lhs.head === :ref
129253
return Expr(:call, fcn, Base.setindex!,
130-
Expr(:call, typesof, esc(lhs.args[1]), esc(rhs), map(esc, lhs.args[2:end])...), kws...)
254+
typesof_expr(Any[lhs.args[1], rhs, lhs.args[2:end]...], where_params), kws...)
131255
end
132256
end
133257
elseif ex0.head === :vcat || ex0.head === :typed_vcat
@@ -138,23 +262,19 @@ function gen_call_with_extracted_types(__module__, fcn, ex0, kws=Expr[])
138262
f, hf = Base.typed_vcat, Base.typed_hvcat
139263
args = ex0.args[2:end]
140264
end
141-
if any(a->isa(a,Expr) && a.head === :row, args)
265+
if any(@nospecialize(a)->isa(a,Expr) && a.head === :row, args)
142266
rows = Any[ (isa(x,Expr) && x.head === :row ? x.args : Any[x]) for x in args ]
143267
lens = map(length, rows)
144-
return Expr(:call, fcn, hf,
145-
Expr(:call, typesof,
146-
(ex0.head === :vcat ? [] : Any[esc(ex0.args[1])])...,
147-
Expr(:tuple, lens...),
148-
map(esc, vcat(rows...))...), kws...)
268+
args = Any[Expr(:tuple, lens...); vcat(rows...)]
269+
ex0.head === :typed_vcat && pushfirst!(args, ex0.args[1])
270+
return Expr(:call, fcn, hf, typesof_expr(args, where_params), kws...)
149271
else
150-
return Expr(:call, fcn, f,
151-
Expr(:call, typesof, map(esc, ex0.args)...), kws...)
272+
return Expr(:call, fcn, f, typesof_expr(ex0.args, where_params), kws...)
152273
end
153274
else
154275
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)
155276
if ex0.head === head
156-
return Expr(:call, fcn, f,
157-
Expr(:call, typesof, map(esc, ex0.args)...), kws...)
277+
return Expr(:call, fcn, f, typesof_expr(ex0.args, where_params), kws...)
158278
end
159279
end
160280
end
@@ -170,16 +290,14 @@ function gen_call_with_extracted_types(__module__, fcn, ex0, kws=Expr[])
170290

171291
exret = Expr(:none)
172292
if ex.head === :call
173-
if any(e->(isa(e, Expr) && e.head === :(...)), ex0.args) &&
293+
if any(@nospecialize(x) -> isexpr(x, :...), ex0.args) &&
174294
(ex.args[1] === GlobalRef(Core,:_apply_iterate) ||
175295
ex.args[1] === GlobalRef(Base,:_apply_iterate))
176296
# check for splatting
177297
exret = Expr(:call, ex.args[2], fcn,
178-
Expr(:tuple, esc(ex.args[3]),
179-
Expr(:call, typesof, map(esc, ex.args[4:end])...)))
298+
Expr(:tuple, extract_farg(ex.args[3]), typesof_expr(ex.args[4:end], where_params)))
180299
else
181-
exret = Expr(:call, fcn, esc(ex.args[1]),
182-
Expr(:call, typesof, map(esc, ex.args[2:end])...), kws...)
300+
exret = Expr(:call, fcn, extract_farg(ex.args[1]), typesof_expr(ex.args[2:end], where_params), kws...)
183301
end
184302
end
185303
if ex.head === :thunk || exret.head === :none
@@ -460,7 +578,7 @@ For `@activate Compiler`, the following options are available:
460578
"""
461579
macro activate(what)
462580
options = Symbol[]
463-
if Meta.isexpr(what, :ref)
581+
if isexpr(what, :ref)
464582
Component = what.args[1]
465583
for i = 2:length(what.args)
466584
arg = what.args[i]

0 commit comments

Comments
 (0)