Skip to content

Commit c5ed1b5

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 e7c46ce commit c5ed1b5

File tree

9 files changed

+378
-102
lines changed

9 files changed

+378
-102
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: 123 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)
@@ -130,7 +133,7 @@ function eval_macro_name(ctx::MacroExpansionContext, mctx::MacroContext, ex::Syn
130133
ctx3, ex3 = resolve_scopes(ctx2, ex2)
131134
ctx4, ex4 = convert_closures(ctx3, ex3)
132135
ctx5, ex5 = linearize_ir(ctx4, ex4)
133-
mod = ctx.current_layer.mod
136+
mod = current_layer(ctx).mod
134137
expr_form = to_lowered_expr(mod, ex5)
135138
try
136139
eval(mod, expr_form)
@@ -139,54 +142,123 @@ function eval_macro_name(ctx::MacroExpansionContext, mctx::MacroContext, ex::Syn
139142
end
140143
end
141144

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

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

173-
if expanded isa SyntaxTree
174-
if !is_compatible_graph(ctx, expanded)
175-
# If the macro has produced syntax outside the macro context, copy it over.
176-
# TODO: Do we expect this always to happen? What is the API for access
177-
# to the macro expansion context?
178-
expanded = copy_ast(ctx, expanded)
179-
end
251+
if kind(expanded) != K"Value"
180252
expanded = append_sourceref(ctx, expanded, ex)
181253
# Module scope for the returned AST is the module where this particular
182254
# method was defined (may be different from `parentmodule(macfunc)`)
183-
mod_for_ast = lookup_method_instance(macfunc, macro_args, macro_invocation_world).def.module
255+
mod_for_ast = lookup_method_instance(macfunc, macro_args,
256+
macro_world).def.module
184257
new_layer = ScopeLayer(length(ctx.scope_layers)+1, mod_for_ast, true)
185258
push!(ctx.scope_layers, new_layer)
186-
inner_ctx = MacroExpansionContext(ctx.graph, ctx.bindings, ctx.scope_layers, new_layer)
187-
expanded = expand_forms_1(inner_ctx, expanded)
188-
else
189-
expanded = @ast ctx ex expanded::K"Value"
259+
push!(ctx.scope_layer_stack, new_layer.id)
260+
expanded = expand_forms_1(ctx, expanded)
261+
pop!(ctx.scope_layer_stack)
190262
end
191263
return expanded
192264
end
@@ -231,18 +303,24 @@ function expand_forms_1(ctx::MacroExpansionContext, ex::SyntaxTree)
231303
# turned into normal bindings (eg, assigned to)
232304
@ast ctx ex name_str::K"core"
233305
else
234-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
306+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
235307
makeleaf(ctx, ex, ex, kind=K"Identifier", scope_layer=layerid)
236308
end
237309
elseif k == K"Identifier" || k == K"MacroName" || k == K"StringMacroName"
238-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
310+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
239311
makeleaf(ctx, ex, ex, kind=K"Identifier", scope_layer=layerid)
240312
elseif k == K"var" || k == K"char" || k == K"parens"
241313
# Strip "container" nodes
242314
@chk numchildren(ex) == 1
243315
expand_forms_1(ctx, ex[1])
316+
elseif k == K"escape"
317+
# For processing of old-style macros
318+
top_layer = pop!(ctx.scope_layer_stack)
319+
escaped_ex = expand_forms_1(ctx, ex[1])
320+
push!(ctx.scope_layer_stack, top_layer)
321+
escaped_ex
244322
elseif k == K"juxtapose"
245-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
323+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
246324
@chk numchildren(ex) == 2
247325
@ast ctx ex [K"call"
248326
"*"::K"Identifier"(scope_layer=layerid)
@@ -330,7 +408,7 @@ function expand_forms_1(ctx::MacroExpansionContext, ex::SyntaxTree)
330408
elseif k == K"<:" || k == K">:" || k == K"-->"
331409
# TODO: Should every form get layerid systematically? Or only the ones
332410
# which expand_forms_2 needs?
333-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
411+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
334412
mapchildren(e->expand_forms_1(ctx,e), ctx, ex; scope_layer=layerid)
335413
else
336414
mapchildren(e->expand_forms_1(ctx,e), ctx, ex)
@@ -344,12 +422,12 @@ function expand_forms_1(mod::Module, ex::SyntaxTree)
344422
__macro_ctx__=Nothing,
345423
meta=CompileHints)
346424
layers = ScopeLayer[ScopeLayer(1, mod, false)]
347-
ctx = MacroExpansionContext(graph, Bindings(), layers, layers[1])
425+
ctx = MacroExpansionContext(graph, Bindings(), layers, LayerId[1])
348426
ex2 = expand_forms_1(ctx, reparent(ctx, ex))
349427
graph2 = delete_attributes(graph, :__macro_ctx__)
350428
# TODO: Returning the context with pass-specific mutable data is a bad way
351-
# to carry state into the next pass.
352-
ctx2 = MacroExpansionContext(graph2, ctx.bindings, ctx.scope_layers,
353-
ctx.current_layer)
429+
# to carry state into the next pass. We might fix this by attaching such
430+
# data to the graph itself as global attributes?
431+
ctx2 = MacroExpansionContext(graph2, ctx.bindings, ctx.scope_layers, LayerId[])
354432
return ctx2, reparent(ctx2, ex2)
355433
end

src/runtime.jl

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ function (g::GeneratedFunctionStub)(world::UInt, source::Method, @nospecialize a
307307

308308
# Macro expansion
309309
layers = ScopeLayer[ScopeLayer(1, mod, false)]
310-
ctx1 = MacroExpansionContext(graph, Bindings(), layers, layers[1])
310+
ctx1 = MacroExpansionContext(graph, Bindings(), layers, LayerId[1])
311311

312312
# Run code generator - this acts like a macro expander and like a macro
313313
# expander it gets a MacroContext.
@@ -326,11 +326,11 @@ function (g::GeneratedFunctionStub)(world::UInt, source::Method, @nospecialize a
326326
# Expand any macros emitted by the generator
327327
ex1 = expand_forms_1(ctx1, reparent(ctx1, ex0))
328328
ctx1 = MacroExpansionContext(delete_attributes(graph, :__macro_ctx__),
329-
ctx1.bindings, ctx1.scope_layers, ctx1.current_layer)
329+
ctx1.bindings, ctx1.scope_layers, LayerId[])
330330
ex1 = reparent(ctx1, ex1)
331331

332332
# Desugaring
333-
ctx2, ex2 = expand_forms_2( ctx1, ex1)
333+
ctx2, ex2 = expand_forms_2(ctx1, ex1)
334334

335335
# Wrap expansion in a non-toplevel lambda and run scope resolution
336336
ex2 = @ast ctx2 ex0 [K"lambda"(is_toplevel_thunk=false)
@@ -342,12 +342,11 @@ function (g::GeneratedFunctionStub)(world::UInt, source::Method, @nospecialize a
342342
]
343343
ex2
344344
]
345-
ctx3, ex3 = resolve_scopes( ctx2, ex2)
346-
345+
ctx3, ex3 = resolve_scopes(ctx2, ex2)
347346

348347
# Rest of lowering
349348
ctx4, ex4 = convert_closures(ctx3, ex3)
350-
ctx5, ex5 = linearize_ir( ctx4, ex4)
349+
ctx5, ex5 = linearize_ir(ctx4, ex4)
351350
ci = to_lowered_expr(mod, ex5)
352351
@assert ci isa Core.CodeInfo
353352
return ci

0 commit comments

Comments
 (0)