Skip to content

Commit ca5ed38

Browse files
committed
Macro expansion for old-style Expr macros
The main difficulty here is managing hygiene correctly. We choose to represent new-style scoped identifiers passed to old macros using `Expr(:scope_layer, name, layer_id)` where necessary. But only where necessary - in most contexts, old-style macros will see unadorned identifiers just as they currently do. The only time the new `Expr` construct is visible is when new macros interpolate an expression into a call to an old-style macro in the returned code. Previously, such macro-calling-macro situations would result in the inner macro call seeing `Expr(:escape, ...)` and it's rare for old-style macros to handle this correctly. Old-style macros may still return `Expr(:escape)` expressions resulting from manual escaping. When consuming the output of old macros, we process these manual escapes by escaping up the macro expansion stack in the same way we currently do.
1 parent 39b1eb4 commit ca5ed38

File tree

7 files changed

+259
-53
lines changed

7 files changed

+259
-53
lines changed

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,65 @@ discussed in Adams' paper:
288288

289289
TODO: Write more here...
290290

291+
292+
### Compatibility with `Expr` macros
293+
294+
In order to have compatibility with old-style macros which expect an `Expr`-based
295+
data structure as input, we convert `SyntaxTree` to `Expr`, call the old-style
296+
macro, then convert `SyntaxTree` back to `Expr` and continue with the expansion
297+
process. This involves some loss of provenance precision but allows full
298+
interoperability in the package ecosystem without a need to make breaking
299+
changes.
300+
301+
Let's look at an example. Suppose a manually escaped old-style macro
302+
`@oldstyle` is implemented as
303+
304+
```julia
305+
macro oldstyle(a, b)
306+
quote
307+
x = "x in @oldstyle"
308+
@newstyle $(esc(a)) $(esc(b)) x
309+
end
310+
end
311+
```
312+
313+
along with two correctly escaped new-style macros:
314+
315+
```julia
316+
macro call_oldstyle_macro(y)
317+
quote
318+
x = "x in call_oldstyle_macro"
319+
@oldstyle $y x
320+
end
321+
end
322+
323+
macro newstyle(x, y, z)
324+
quote
325+
x = "x in @newstyle"
326+
($x, $y, $z, x)
327+
end
328+
end
329+
```
330+
331+
Then want some code like the following to "just work" with respect to hygiene
332+
333+
```julia
334+
let
335+
x = "x in outer ctx"
336+
@call_oldstyle_macro x
337+
end
338+
```
339+
340+
When calling `@oldstyle`, we must convert `SyntaxTree` into `Expr`, but we need
341+
to preserve the scope layer of the `x` from the outer context as it is passed
342+
into `@oldstyle` as a macro argument. To do this, we use `Expr(:scope_layer,
343+
:x, outer_layer_id)`. (In the old system, this would be `Expr(:escape, :x)`
344+
instead, presuming that `@call_oldstyle_macro` was implemented using `esc()`.)
345+
346+
When receiving output from old style macro invocations, we preserve the escape
347+
handling of the existing system for any symbols which aren't tagged with a
348+
scope layer.
349+
291350
## Pass 2: Syntax desugaring
292351

293352
This pass recursively converts many special surface syntax forms to a smaller

src/compat.jl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ function expr_to_syntaxtree(@nospecialize(e), lnn::Union{LineNumberNode, Nothing
2424
SyntaxGraph(),
2525
kind=Kind, syntax_flags=UInt16,
2626
source=SourceAttrType, var_id=Int, value=Any,
27-
name_val=String, is_toplevel_thunk=Bool)
27+
name_val=String, is_toplevel_thunk=Bool,
28+
scope_layer=LayerId)
2829
expr_to_syntaxtree(graph, e, lnn)
2930
end
3031

@@ -423,6 +424,13 @@ function _insert_convert_expr(@nospecialize(e), graph::SyntaxGraph, src::SourceA
423424
@assert nargs === 1
424425
child_exprs[1] = Expr(:quoted_symbol, e.args[1])
425426
end
427+
elseif e.head === :scope_layer
428+
@assert nargs === 2
429+
@assert e.args[1] isa Symbol
430+
@assert e.args[2] isa LayerId
431+
st_id, src = _insert_convert_expr(e.args[1], graph, src)
432+
setattr!(graph, st_id, scope_layer=e.args[2])
433+
return st_id, src
426434
elseif e.head === :symbolicgoto || e.head === :symboliclabel
427435
@assert nargs === 1
428436
st_k = e.head === :symbolicgoto ? K"symbolic_label" : K"symbolic_goto"

src/desugaring.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function DesugaringContext(ctx)
1515
scope_type=Symbol, # :hard or :soft
1616
var_id=IdTag,
1717
is_toplevel_thunk=Bool)
18-
DesugaringContext(graph, ctx.bindings, ctx.scope_layers, ctx.current_layer.mod)
18+
DesugaringContext(graph, ctx.bindings, ctx.scope_layers, first(ctx.scope_layers).mod)
1919
end
2020

2121
#-------------------------------------------------------------------------------

src/kinds.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ function _register_kinds()
66
# expansion, and known to lowering. These are part of the AST API but
77
# without having surface syntax.
88
"BEGIN_EXTENSION_KINDS"
9+
# Used for converting `esc()`'d expressions arising from old macro
10+
# invocations during macro expansion
11+
"escape"
912
# atomic fields or accesses (see `@atomic`)
1013
"atomic"
1114
# Flag for @generated parts of a functon

src/macro_expansion.jl

Lines changed: 119 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ struct MacroExpansionContext{GraphType} <: AbstractLoweringContext
1818
graph::GraphType
1919
bindings::Bindings
2020
scope_layers::Vector{ScopeLayer}
21-
current_layer::ScopeLayer
21+
scope_layer_stack::Vector{LayerId}
2222
end
2323

24+
current_layer(ctx::MacroExpansionContext) = ctx.scope_layers[last(ctx.scope_layer_stack)]
25+
current_layer_id(ctx::MacroExpansionContext) = last(ctx.scope_layer_stack)
26+
2427
#--------------------------------------------------
2528
# Expansion of quoted expressions
2629
function collect_unquoted!(ctx, unquoted, ex, depth)
@@ -126,7 +129,7 @@ function eval_macro_name(ctx::MacroExpansionContext, mctx::MacroContext, ex::Syn
126129
ctx3, ex3 = resolve_scopes(ctx2, ex2)
127130
ctx4, ex4 = convert_closures(ctx3, ex3)
128131
ctx5, ex5 = linearize_ir(ctx4, ex4)
129-
mod = ctx.current_layer.mod
132+
mod = current_layer(ctx).mod
130133
expr_form = to_lowered_expr(mod, ex5)
131134
try
132135
eval(mod, expr_form)
@@ -135,54 +138,119 @@ function eval_macro_name(ctx::MacroExpansionContext, mctx::MacroContext, ex::Syn
135138
end
136139
end
137140

138-
function expand_macro(ctx::MacroExpansionContext, ex::SyntaxTree)
141+
# Record scope layer information for symbols passed to a macro by setting
142+
# scope_layer for each expression and also processing any K"escape" arising
143+
# from previous expansion of old-style macros.
144+
#
145+
# See also set_scope_layer()
146+
function set_macro_arg_hygiene(ctx, ex, layer_ids, layer_idx)
147+
k = kind(ex)
148+
scope_layer = get(ex, :scope_layer, layer_ids[layer_idx])
149+
if k == K"module" || k == K"toplevel" || k == K"inert"
150+
makenode(ctx, ex, ex, children(ex);
151+
scope_layer=scope_layer)
152+
elseif k == K"."
153+
makenode(ctx, ex, ex, set_macro_arg_hygiene(ctx, ex[1], layer_ids, layer_idx), ex[2],
154+
scope_layer=scope_layer)
155+
elseif !is_leaf(ex)
156+
inner_layer_idx = layer_idx
157+
if k == K"escape"
158+
inner_layer_idx = layer_idx - 1
159+
if inner_layer_idx < 1
160+
# If we encounter too many escape nodes, there's probably been
161+
# an error in the previous macro expansion.
162+
# todo: The error here isn't precise about that - maybe we
163+
# should record that macro call expression with the scope layer
164+
# if we want to report the error against the macro call?
165+
throw(MacroExpansionError(ex, "`escape` node in outer context"))
166+
end
167+
end
168+
mapchildren(e->set_macro_arg_hygiene(ctx, e, layer_ids, inner_layer_idx),
169+
ctx, ex; scope_layer=scope_layer)
170+
else
171+
makeleaf(ctx, ex, ex; scope_layer=scope_layer)
172+
end
173+
end
174+
175+
function expand_macro(ctx, ex)
139176
@assert kind(ex) == K"macrocall"
140177

141178
macname = ex[1]
142-
mctx = MacroContext(ctx.graph, ex, ctx.current_layer)
179+
mctx = MacroContext(ctx.graph, ex, current_layer(ctx))
143180
macfunc = eval_macro_name(ctx, mctx, macname)
144-
# Macro call arguments may be either
145-
# * Unprocessed by the macro expansion pass
146-
# * Previously processed, but spliced into a further macro call emitted by
147-
# a macro expansion.
148-
# In either case, we need to set any unset scope layers before passing the
149-
# arguments to the macro call.
150-
macro_args = Any[mctx]
151-
for i in 2:numchildren(ex)
152-
push!(macro_args, set_scope_layer(ctx, ex[i], ctx.current_layer.id, false))
153-
end
154-
macro_invocation_world = Base.get_world_counter()
155-
expanded = try
156-
# TODO: Allow invoking old-style macros for compat
157-
invokelatest(macfunc, macro_args...)
158-
catch exc
159-
if exc isa MacroExpansionError
160-
# Add context to the error.
161-
newexc = MacroExpansionError(mctx, exc.ex, exc.msg, exc.position, exc.err)
181+
raw_args = ex[2:end]
182+
# We use a specific well defined world age for the next checks and macro
183+
# expansion invocations. This avoids inconsistencies if the latest world
184+
# age changes concurrently.
185+
#
186+
# TODO: Allow this to be passed in
187+
macro_world = Base.get_world_counter()
188+
if hasmethod(macfunc, Tuple{typeof(mctx), typeof.(raw_args)...}; world=macro_world)
189+
macro_args = Any[mctx]
190+
for arg in raw_args
191+
# Add hygiene information to be carried along with macro arguments.
192+
#
193+
# Macro call arguments may be either
194+
# * Unprocessed by the macro expansion pass
195+
# * Previously processed, but spliced into a further macro call emitted by
196+
# a macro expansion.
197+
# In either case, we need to set scope layers before passing the
198+
# arguments to the macro call.
199+
push!(macro_args, set_macro_arg_hygiene(ctx, arg, ctx.scope_layer_stack,
200+
length(ctx.scope_layer_stack)))
201+
end
202+
expanded = try
203+
Base.invoke_in_world(macro_world, macfunc, macro_args...)
204+
catch exc
205+
newexc = exc isa MacroExpansionError ?
206+
MacroExpansionError(mctx, exc.ex, exc.msg, exc.position, exc.err) :
207+
MacroExpansionError(mctx, ex, "Error expanding macro", :all, exc)
208+
# TODO: We can delete this rethrow when we move to AST-based error propagation.
209+
rethrow(newexc)
210+
end
211+
if expanded isa SyntaxTree
212+
if !is_compatible_graph(ctx, expanded)
213+
# If the macro has produced syntax outside the macro context,
214+
# copy it over. TODO: Do we expect this always to happen? What
215+
# is the API for access to the macro expansion context?
216+
expanded = copy_ast(ctx, expanded)
217+
end
162218
else
163-
newexc = MacroExpansionError(mctx, ex, "Error expanding macro", :all, exc)
219+
expanded = @ast ctx ex expanded::K"Value"
164220
end
165-
# TODO: We can delete this rethrow when we move to AST-based error propagation.
166-
rethrow(newexc)
221+
else
222+
# Compat: attempt to invoke an old-style macro if there's no applicable
223+
# method for new-style macro arguments.
224+
macro_loc = source_location(LineNumberNode, ex)
225+
macro_args = Any[macro_loc, current_layer(ctx).mod]
226+
for arg in raw_args
227+
# For hygiene in old-style macros, we omit any additional scope
228+
# layer information from macro arguments. Old-style macros will
229+
# handle that using manual escaping in the macro itself.
230+
#
231+
# Note that there's one somewhat-incompatibility here for
232+
# identifiers interpolated into the `raw_args` from outer macro
233+
# expansions of new-style macros which call old-style macros.
234+
# Instead of seeing `Expr(:escape)` in such situations, old-style
235+
# macros will now see `Expr(:scope_layer)` inside `macro_args`.
236+
push!(macro_args, Expr(arg))
237+
end
238+
# TODO: Error handling
239+
expanded = Base.invoke_in_world(macro_world, macfunc, macro_args...)
240+
expanded = expr_to_syntaxtree(expanded, macro_loc, syntax_graph(ctx))
167241
end
168242

169-
if expanded isa SyntaxTree
170-
if !is_compatible_graph(ctx, expanded)
171-
# If the macro has produced syntax outside the macro context, copy it over.
172-
# TODO: Do we expect this always to happen? What is the API for access
173-
# to the macro expansion context?
174-
expanded = copy_ast(ctx, expanded)
175-
end
243+
if kind(expanded) != K"Value"
176244
expanded = append_sourceref(ctx, expanded, ex)
177245
# Module scope for the returned AST is the module where this particular
178246
# method was defined (may be different from `parentmodule(macfunc)`)
179-
mod_for_ast = lookup_method_instance(macfunc, macro_args, macro_invocation_world).def.module
247+
mod_for_ast = lookup_method_instance(macfunc, macro_args,
248+
macro_world).def.module
180249
new_layer = ScopeLayer(length(ctx.scope_layers)+1, mod_for_ast, true)
181250
push!(ctx.scope_layers, new_layer)
182-
inner_ctx = MacroExpansionContext(ctx.graph, ctx.bindings, ctx.scope_layers, new_layer)
183-
expanded = expand_forms_1(inner_ctx, expanded)
184-
else
185-
expanded = @ast ctx ex expanded::K"Value"
251+
push!(ctx.scope_layer_stack, new_layer.id)
252+
expanded = expand_forms_1(ctx, expanded)
253+
pop!(ctx.scope_layer_stack)
186254
end
187255
return expanded
188256
end
@@ -224,18 +292,24 @@ function expand_forms_1(ctx::MacroExpansionContext, ex::SyntaxTree)
224292
elseif is_ccall_or_cglobal(name_str)
225293
@ast ctx ex name_str::K"core"
226294
else
227-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
295+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
228296
makeleaf(ctx, ex, ex, kind=K"Identifier", scope_layer=layerid)
229297
end
230298
elseif k == K"Identifier" || k == K"MacroName" || k == K"StringMacroName"
231-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
299+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
232300
makeleaf(ctx, ex, ex, kind=K"Identifier", scope_layer=layerid)
233301
elseif k == K"var" || k == K"char" || k == K"parens"
234302
# Strip "container" nodes
235303
@chk numchildren(ex) == 1
236304
expand_forms_1(ctx, ex[1])
305+
elseif k == K"escape"
306+
# For processing of old-style macros
307+
top_layer = pop!(ctx.scope_layer_stack)
308+
escaped_ex = expand_forms_1(ctx, ex[1])
309+
push!(ctx.scope_layer_stack, top_layer)
310+
escaped_ex
237311
elseif k == K"juxtapose"
238-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
312+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
239313
@chk numchildren(ex) == 2
240314
@ast ctx ex [K"call"
241315
"*"::K"Identifier"(scope_layer=layerid)
@@ -323,7 +397,7 @@ function expand_forms_1(ctx::MacroExpansionContext, ex::SyntaxTree)
323397
elseif k == K"<:" || k == K">:" || k == K"-->"
324398
# TODO: Should every form get layerid systematically? Or only the ones
325399
# which expand_forms_2 needs?
326-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
400+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
327401
mapchildren(e->expand_forms_1(ctx,e), ctx, ex; scope_layer=layerid)
328402
else
329403
mapchildren(e->expand_forms_1(ctx,e), ctx, ex)
@@ -337,12 +411,12 @@ function expand_forms_1(mod::Module, ex::SyntaxTree)
337411
__macro_ctx__=Nothing,
338412
meta=CompileHints)
339413
layers = ScopeLayer[ScopeLayer(1, mod, false)]
340-
ctx = MacroExpansionContext(graph, Bindings(), layers, layers[1])
414+
ctx = MacroExpansionContext(graph, Bindings(), layers, LayerId[1])
341415
ex2 = expand_forms_1(ctx, reparent(ctx, ex))
342416
graph2 = delete_attributes(graph, :__macro_ctx__)
343417
# TODO: Returning the context with pass-specific mutable data is a bad way
344-
# to carry state into the next pass.
345-
ctx2 = MacroExpansionContext(graph2, ctx.bindings, ctx.scope_layers,
346-
ctx.current_layer)
418+
# to carry state into the next pass. We might fix this by attaching such
419+
# data to the graph itself as global attributes?
420+
ctx2 = MacroExpansionContext(graph2, ctx.bindings, ctx.scope_layers, LayerId[])
347421
return ctx2, reparent(ctx2, ex2)
348422
end

src/syntax_graph.jl

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ attrsummary(name, value::Number) = "$name=$value"
429429
function _value_string(ex)
430430
k = kind(ex)
431431
str = k == K"Identifier" || k == K"MacroName" || is_operator(k) ? ex.name_val :
432-
k == K"Placeholder" ? ex.name_val :
432+
k == K"Placeholder" ? ex.name_val :
433433
k == K"SSAValue" ? "%" :
434434
k == K"BindingId" ? "#" :
435435
k == K"label" ? "label" :
@@ -546,9 +546,16 @@ JuliaSyntax.byte_range(ex::SyntaxTree) = byte_range(sourceref(ex))
546546

547547
function JuliaSyntax._expr_leaf_val(ex::SyntaxTree, _...)
548548
name = get(ex, :name_val, nothing)
549-
!isnothing(name) && return Symbol(name)
550-
name = get(ex, :value, nothing)
551-
return name
549+
if !isnothing(name)
550+
n = Symbol(name)
551+
if hasattr(ex, :scope_layer)
552+
Expr(:scope_layer, n, ex.scope_layer)
553+
else
554+
n
555+
end
556+
else
557+
get(ex, :value, nothing)
558+
end
552559
end
553560

554561
Base.Expr(ex::SyntaxTree) = JuliaSyntax.to_expr(ex)

0 commit comments

Comments
 (0)