Skip to content

Commit f6227bd

Browse files
authored
Fixes for quote interpolation in Expr compat mode (#57)
Ensure that interpolating values into quoted expressions works in Expr compat mode. * To produce `Expr` in `CodeInfo` conversion, add an `as_Expr` attribute to `K"inert"`. expand_quote * In analogy with `eval()`, add `expr_compat_mode` keyword to `include_string`. We may want to rethink this when we rethink how options to `eval()` work. * Adapt the runtime function `interpolate_ast` to work with both `Expr` and `SyntaxTree` as the source expression. * Allow `Symbol` to be interpolated into `SyntaxTree` as syntax rather than a `K"Value": Even in "new style macro mode", `:x` is interpreted as a `Symbol` rather than an AST as it's used by many packages as a kind of lightweight untyped enum (ie, a usage which is almost entirely unrelated to AST manipulation) * Fix `SyntaxTree->Expr` conversion so that embedded Expr `K"Value"` means a leaf, not an embedded tree.
1 parent 2d2d677 commit f6227bd

File tree

5 files changed

+164
-40
lines changed

5 files changed

+164
-40
lines changed

src/eval.jl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,8 @@ function to_lowered_expr(mod, ex, ssa_offset=0)
261261
elseif k == K"return"
262262
Core.ReturnNode(to_lowered_expr(mod, ex[1], ssa_offset))
263263
elseif k == K"inert"
264-
ex[1]
264+
e1 = ex[1]
265+
getmeta(ex, :as_Expr, false) ? QuoteNode(Expr(e1)) : e1
265266
elseif k == K"code_info"
266267
funcname = ex.is_toplevel_thunk ?
267268
"top-level scope" :
@@ -391,7 +392,8 @@ end
391392
392393
Like `include`, except reads code from the given string rather than from a file.
393394
"""
394-
function include_string(mod::Module, code::AbstractString, filename::AbstractString="string")
395-
eval(mod, parseall(SyntaxTree, code; filename=filename))
395+
function include_string(mod::Module, code::AbstractString, filename::AbstractString="string";
396+
expr_compat_mode=false)
397+
eval(mod, parseall(SyntaxTree, code; filename=filename); expr_compat_mode)
396398
end
397399

src/macro_expansion.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function expand_quote(ctx, ex)
5757
# the entire expression produced by `quote` expansion. We could, but it
5858
# seems unnecessary for `quote` because the surface syntax is a transparent
5959
# representation of the expansion process. However, it's useful to add the
60-
# extra srcref in a more targetted way for $ interpolations inside
60+
# extra srcref in a more targeted way for $ interpolations inside
6161
# interpolate_ast, so we do that there.
6262
#
6363
# In principle, particular user-defined macros could opt into a similar
@@ -69,7 +69,7 @@ function expand_quote(ctx, ex)
6969
@ast ctx ex [K"call"
7070
interpolate_ast::K"Value"
7171
(ctx.expr_compat_mode ? Expr : SyntaxTree)::K"Value"
72-
[K"inert" ex]
72+
[K"inert"(meta=CompileHints(:as_Expr, ctx.expr_compat_mode)) ex]
7373
unquoted...
7474
]
7575
end

src/runtime.jl

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,57 +21,106 @@ struct InterpolationContext{Graph} <: AbstractLoweringContext
2121
current_index::Ref{Int}
2222
end
2323

24+
# Context for `Expr`-based AST interpolation in compat mode
25+
struct ExprInterpolationContext <: AbstractLoweringContext
26+
values::Tuple
27+
current_index::Ref{Int}
28+
end
29+
30+
# Helper functions to make shared interpolation code which works with both
31+
# SyntaxTree and Expr data structures.
32+
_interp_kind(ex::SyntaxTree) = kind(ex)
33+
function _interp_kind(@nospecialize(ex))
34+
return (ex isa Expr && ex.head === :quote) ? K"quote" :
35+
(ex isa Expr && ex.head === :$) ? K"$" :
36+
K"None" # Other cases irrelevant to interpolation
37+
end
38+
39+
_children(ex::SyntaxTree) = children(ex)
40+
_children(@nospecialize(ex)) = ex isa Expr ? ex.args : ()
41+
42+
_numchildren(ex::SyntaxTree) = numchildren(ex)
43+
_numchildren(@nospecialize(ex)) = ex isa Expr ? length(ex.args) : 0
44+
45+
_syntax_list(ctx::InterpolationContext) = SyntaxList(ctx)
46+
_syntax_list(ctx::ExprInterpolationContext) = Any[]
47+
48+
_interp_makenode(ctx::InterpolationContext, ex, args) = makenode(ctx, ex, ex, args)
49+
_interp_makenode(ctx::ExprInterpolationContext, ex, args) = Expr((ex::Expr).head, args...)
50+
51+
_to_syntax_tree(ex::SyntaxTree) = ex
52+
_to_syntax_tree(@nospecialize(ex)) = expr_to_syntaxtree(ex)
53+
54+
2455
function _contains_active_interp(ex, depth)
25-
k = kind(ex)
56+
k = _interp_kind(ex)
2657
if k == K"$" && depth == 0
2758
return true
59+
elseif _numchildren(ex) == 0
60+
return false
2861
end
2962
inner_depth = k == K"quote" ? depth + 1 :
3063
k == K"$" ? depth - 1 :
3164
depth
32-
return any(_contains_active_interp(c, inner_depth) for c in children(ex))
65+
return any(_contains_active_interp(c, inner_depth) for c in _children(ex))
3366
end
3467

3568
# Produce interpolated node for `$x` syntax
36-
function _interpolated_value(ctx, srcref, ex)
69+
function _interpolated_value(ctx::InterpolationContext, srcref, ex)
3770
if ex isa SyntaxTree
3871
if !is_compatible_graph(ctx, ex)
3972
ex = copy_ast(ctx, ex)
4073
end
4174
append_sourceref(ctx, ex, srcref)
75+
elseif ex isa Symbol
76+
# Plain symbols become identifiers. This is an accomodation for
77+
# compatibility to allow `:x` (a Symbol) and `:(x)` (a SyntaxTree) to
78+
# be used interchangably in macros.
79+
makeleaf(ctx, srcref, K"Identifier", string(ex))
4280
else
4381
makeleaf(ctx, srcref, K"Value", ex)
4482
end
4583
end
4684

47-
function _interpolate_ast(ctx::InterpolationContext, ex, depth)
85+
function _interpolated_value(::ExprInterpolationContext, _, ex)
86+
ex
87+
end
88+
89+
function copy_ast(::ExprInterpolationContext, @nospecialize(ex))
90+
@ccall(jl_copy_ast(ex::Any)::Any)
91+
end
92+
93+
function _interpolate_ast(ctx, ex, depth)
4894
if ctx.current_index[] > length(ctx.values) || !_contains_active_interp(ex, depth)
4995
return ex
5096
end
5197

5298
# We have an interpolation deeper in the tree somewhere - expand to an
53-
# expression
54-
inner_depth = kind(ex) == K"quote" ? depth + 1 :
55-
kind(ex) == K"$" ? depth - 1 :
99+
# expression which performs the interpolation.
100+
k = _interp_kind(ex)
101+
inner_depth = k == K"quote" ? depth + 1 :
102+
k == K"$" ? depth - 1 :
56103
depth
57-
expanded_children = SyntaxList(ctx)
58-
for e in children(ex)
59-
if kind(e) == K"$" && inner_depth == 0
104+
105+
expanded_children = _syntax_list(ctx)
106+
107+
for e in _children(ex)
108+
if _interp_kind(e) == K"$" && inner_depth == 0
60109
vals = ctx.values[ctx.current_index[]]::Tuple
61110
ctx.current_index[] += 1
62111
for (i,v) in enumerate(vals)
63-
srcref = numchildren(e) == 1 ? e : e[i]
112+
srcref = _numchildren(e) == 1 ? e : _children(e)[i]
64113
push!(expanded_children, _interpolated_value(ctx, srcref, v))
65114
end
66115
else
67116
push!(expanded_children, _interpolate_ast(ctx, e, inner_depth))
68117
end
69118
end
70119

71-
makenode(ctx, ex, head(ex), expanded_children)
120+
_interp_makenode(ctx, ex, expanded_children)
72121
end
73122

74-
function interpolate_ast(::Type{SyntaxTree}, ex, values...)
123+
function _setup_interpolation(::Type{SyntaxTree}, ex, values)
75124
# Construct graph for interpolation context. We inherit this from the macro
76125
# context where possible by detecting it using __macro_ctx__. This feels
77126
# hacky though.
@@ -92,30 +141,33 @@ function interpolate_ast(::Type{SyntaxTree}, ex, values...)
92141
value=Any, name_val=String, scope_layer=LayerId)
93142
end
94143
ctx = InterpolationContext(graph, values, Ref(1))
144+
return ctx
145+
end
146+
147+
function _setup_interpolation(::Type{Expr}, ex, values)
148+
return ExprInterpolationContext(values, Ref(1))
149+
end
150+
151+
function interpolate_ast(::Type{T}, ex, values...) where {T}
152+
ctx = _setup_interpolation(T, ex, values)
153+
95154
# We must copy the AST into our context to use it as the source reference
96-
# of generated expressions.
155+
# of generated expressions (and in the Expr case at least, to avoid mutation)
97156
ex1 = copy_ast(ctx, ex)
98-
if kind(ex1) == K"$"
157+
if _interp_kind(ex1) == K"$"
99158
@assert length(values) == 1
100159
vs = values[1]
101160
if length(vs) > 1
102161
# :($($(xs...))) where xs is more than length 1
103-
throw(LoweringError(ex1, "More than one value in bare `\$` expression"))
162+
throw(LoweringError(_to_syntax_tree(ex1),
163+
"More than one value in bare `\$` expression"))
104164
end
105165
_interpolated_value(ctx, ex1, only(vs))
106166
else
107167
_interpolate_ast(ctx, ex1, 0)
108168
end
109169
end
110170

111-
function interpolate_ast(::Type{Expr}, ex, values...)
112-
# TODO: Adjust `_interpolated_value` to ensure that incoming `Expr` data
113-
# structures are treated as AST in Expr compat mode, rather than `K"Value"`?
114-
# Or convert `ex` to `Expr` early during lowering and implement
115-
# `interpolate_ast` for `Expr`?
116-
Expr(interpolate_ast(SyntaxTree, ex, values...))
117-
end
118-
119171
#--------------------------------------------------
120172
# Functions called by closure conversion
121173
function eval_closure_type(mod::Module, closure_type_name::Symbol, field_names, field_is_box)

src/syntax_graph.jl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,14 @@ function JuliaSyntax._expr_leaf_val(ex::SyntaxTree, _...)
583583
n
584584
end
585585
else
586-
get(ex, :value, nothing)
586+
val = get(ex, :value, nothing)
587+
if kind(ex) == K"Value" && val isa Expr || val isa LineNumberNode
588+
# Expr AST embedded in a SyntaxTree should be quoted rather than
589+
# becoming part of the output AST.
590+
QuoteNode(val)
591+
else
592+
val
593+
end
587594
end
588595
end
589596

@@ -774,6 +781,7 @@ end
774781

775782
function Base.deleteat!(v::SyntaxList, inds)
776783
deleteat!(v.ids, inds)
784+
v
777785
end
778786

779787
function Base.copy(v::SyntaxList)

test/quoting.jl

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,14 @@ end
9595
@test ex[2].name_val == "a"
9696

9797
# interpolations at multiple depths
98-
ex = JuliaLowering.include_string(test_mod, """
98+
ex = JuliaLowering.include_string(test_mod, raw"""
9999
let
100100
args = (:(x),:(y))
101101
quote
102102
x = 1
103103
y = 2
104104
quote
105-
f(\$\$(args...))
105+
f($$(args...))
106106
end
107107
end
108108
end
@@ -140,23 +140,85 @@ ex2 = JuliaLowering.eval(test_mod, ex)
140140
@test JuliaLowering.include_string(test_mod, ":(x)") isa SyntaxTree
141141

142142
# Double interpolation
143-
ex = JuliaLowering.include_string(test_mod, """
143+
double_interp_ex = JuliaLowering.include_string(test_mod, raw"""
144144
let
145145
args = (:(xxx),)
146-
:(:(\$\$(args...)))
146+
:(:($$(args...)))
147147
end
148148
""")
149149
Base.eval(test_mod, :(xxx = 111))
150-
ex2 = JuliaLowering.eval(test_mod, ex)
151-
@test kind(ex2) == K"Value"
152-
@test ex2.value == 111
150+
dinterp_eval = JuliaLowering.eval(test_mod, double_interp_ex)
151+
@test kind(dinterp_eval) == K"Value"
152+
@test dinterp_eval.value == 111
153153

154-
double_interp_ex = JuliaLowering.include_string(test_mod, """
154+
multi_interp_ex = JuliaLowering.include_string(test_mod, raw"""
155155
let
156156
args = (:(x), :(y))
157-
:(:(\$\$(args...)))
157+
:(:($$(args...)))
158+
end
159+
""")
160+
@test_throws LoweringError JuliaLowering.eval(test_mod, multi_interp_ex)
161+
162+
# Interpolation of SyntaxTree Identifier vs plain Symbol
163+
symbol_interp = JuliaLowering.include_string(test_mod, raw"""
164+
let
165+
x = :xx # Plain Symbol
166+
y = :(yy) # SyntaxTree K"Identifier"
167+
:(f($x, $y, z))
168+
end
169+
""")
170+
@test symbol_interp @ast_ [K"call"
171+
"f"::K"Identifier"
172+
"xx"::K"Identifier"
173+
"yy"::K"Identifier"
174+
"z"::K"Identifier"
175+
]
176+
@test sourcetext(symbol_interp[2]) == "\$x" # No provenance for plain Symbol
177+
@test sourcetext(symbol_interp[3]) == "yy"
178+
179+
# Mixing Expr into a SyntaxTree doesn't graft it onto the SyntaxTree AST but
180+
# treats it as a plain old value. (This is the conservative API choice and also
181+
# encourages ASTs to be written in the new form. However we may choose to
182+
# change this if necessary for compatibility.)
183+
expr_interp_is_value = JuliaLowering.include_string(test_mod, raw"""
184+
let
185+
x = Expr(:call, :f, :x)
186+
:(g($x))
158187
end
159188
""")
160-
@test_throws LoweringError JuliaLowering.eval(test_mod, double_interp_ex)
189+
@test expr_interp_is_value @ast_ [K"call"
190+
"g"::K"Identifier"
191+
Expr(:call, :f, :x)::K"Value"
192+
# ^^ NB not [K"call" "f"::K"Identifier" "x"::K"Identifier"]
193+
]
194+
@test Expr(expr_interp_is_value) == Expr(:call, :g, QuoteNode(Expr(:call, :f, :x)))
195+
196+
@testset "Interpolation in Expr compat mode" begin
197+
expr_interp = JuliaLowering.include_string(test_mod, raw"""
198+
let
199+
x = :xx
200+
:(f($x, z))
201+
end
202+
""", expr_compat_mode=true)
203+
@test expr_interp == Expr(:call, :f, :xx, :z)
204+
205+
double_interp_expr = JuliaLowering.include_string(test_mod, raw"""
206+
let
207+
x = :xx
208+
:(:(f($$x, $y)))
209+
end
210+
""", expr_compat_mode=true)
211+
@test double_interp_expr == Expr(:quote, Expr(:call, :f, Expr(:$, :xx), Expr(:$, :y)))
212+
213+
# Test that ASTs are copied before they're seen by the user
214+
@test JuliaLowering.include_string(test_mod, raw"""
215+
exs = []
216+
for i = 1:2
217+
push!(exs, :(f(x,y)))
218+
push!(exs[end].args, :z)
219+
end
220+
exs
221+
""", expr_compat_mode=true) == Any[Expr(:call, :f, :x, :y, :z), Expr(:call, :f, :x, :y, :z)]
222+
end
161223

162224
end

0 commit comments

Comments
 (0)