Skip to content

Commit 7b56524

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 7b56524

File tree

8 files changed

+282
-61
lines changed

8 files changed

+282
-61
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)
@@ -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,119 @@ 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"
168224
end
169-
# TODO: We can delete this rethrow when we move to AST-based error propagation.
170-
rethrow(newexc)
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 somewhat-incompatibility here for
236+
# identifiers interpolated into the `raw_args` from outer macro
237+
# expansions of new-style macros which call old-style macros.
238+
# Instead of seeing `Expr(:escape)` in such situations, old-style
239+
# macros will now see `Expr(:scope_layer)` inside `macro_args`.
240+
push!(macro_args, Expr(arg))
241+
end
242+
# TODO: Error handling
243+
expanded = Base.invoke_in_world(macro_world, macfunc, macro_args...)
244+
expanded = expr_to_syntaxtree(expanded, macro_loc, syntax_graph(ctx))
171245
end
172246

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
247+
if kind(expanded) != K"Value"
180248
expanded = append_sourceref(ctx, expanded, ex)
181249
# Module scope for the returned AST is the module where this particular
182250
# method was defined (may be different from `parentmodule(macfunc)`)
183-
mod_for_ast = lookup_method_instance(macfunc, macro_args, macro_invocation_world).def.module
251+
mod_for_ast = lookup_method_instance(macfunc, macro_args,
252+
macro_world).def.module
184253
new_layer = ScopeLayer(length(ctx.scope_layers)+1, mod_for_ast, true)
185254
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"
255+
push!(ctx.scope_layer_stack, new_layer.id)
256+
expanded = expand_forms_1(ctx, expanded)
257+
pop!(ctx.scope_layer_stack)
190258
end
191259
return expanded
192260
end
@@ -231,18 +299,24 @@ function expand_forms_1(ctx::MacroExpansionContext, ex::SyntaxTree)
231299
# turned into normal bindings (eg, assigned to)
232300
@ast ctx ex name_str::K"core"
233301
else
234-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
302+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
235303
makeleaf(ctx, ex, ex, kind=K"Identifier", scope_layer=layerid)
236304
end
237305
elseif k == K"Identifier" || k == K"MacroName" || k == K"StringMacroName"
238-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
306+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
239307
makeleaf(ctx, ex, ex, kind=K"Identifier", scope_layer=layerid)
240308
elseif k == K"var" || k == K"char" || k == K"parens"
241309
# Strip "container" nodes
242310
@chk numchildren(ex) == 1
243311
expand_forms_1(ctx, ex[1])
312+
elseif k == K"escape"
313+
# For processing of old-style macros
314+
top_layer = pop!(ctx.scope_layer_stack)
315+
escaped_ex = expand_forms_1(ctx, ex[1])
316+
push!(ctx.scope_layer_stack, top_layer)
317+
escaped_ex
244318
elseif k == K"juxtapose"
245-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
319+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
246320
@chk numchildren(ex) == 2
247321
@ast ctx ex [K"call"
248322
"*"::K"Identifier"(scope_layer=layerid)
@@ -330,7 +404,7 @@ function expand_forms_1(ctx::MacroExpansionContext, ex::SyntaxTree)
330404
elseif k == K"<:" || k == K">:" || k == K"-->"
331405
# TODO: Should every form get layerid systematically? Or only the ones
332406
# which expand_forms_2 needs?
333-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
407+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
334408
mapchildren(e->expand_forms_1(ctx,e), ctx, ex; scope_layer=layerid)
335409
else
336410
mapchildren(e->expand_forms_1(ctx,e), ctx, ex)
@@ -344,12 +418,12 @@ function expand_forms_1(mod::Module, ex::SyntaxTree)
344418
__macro_ctx__=Nothing,
345419
meta=CompileHints)
346420
layers = ScopeLayer[ScopeLayer(1, mod, false)]
347-
ctx = MacroExpansionContext(graph, Bindings(), layers, layers[1])
421+
ctx = MacroExpansionContext(graph, Bindings(), layers, LayerId[1])
348422
ex2 = expand_forms_1(ctx, reparent(ctx, ex))
349423
graph2 = delete_attributes(graph, :__macro_ctx__)
350424
# 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)
425+
# to carry state into the next pass. We might fix this by attaching such
426+
# data to the graph itself as global attributes?
427+
ctx2 = MacroExpansionContext(graph2, ctx.bindings, ctx.scope_layers, LayerId[])
354428
return ctx2, reparent(ctx2, ex2)
355429
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)