Skip to content

Commit 7700894

Browse files
c42fmlechu
andcommitted
Macro expansion for old-style Expr macros
Implements mixed macro expansion for old-style macros (those written to expect an `Expr` data structure) and new-style macros (those written to expect `SyntaxTree`). 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, ...)` but they now see `Expr(:scope_layer)`. However, and it's rare for old-style macros to de- and re-construct escaped expressions correctly so this should be a minor issue for compatibility. 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. Also add `parent_layer` id to `ScopeLayer` to preserve the macro expansion stack there for use by JETLS. Co-authored-by: Em Chu <[email protected]>
1 parent bc0ad7f commit 7700894

File tree

10 files changed

+441
-120
lines changed

10 files changed

+441
-120
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/ast.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -662,8 +662,8 @@ function to_symbol(ctx, ex)
662662
@ast ctx ex ex=>K"Symbol"
663663
end
664664

665-
function new_scope_layer(ctx, mod_ref::Module=ctx.mod; is_macro_expansion=true)
666-
new_layer = ScopeLayer(length(ctx.scope_layers)+1, ctx.mod, is_macro_expansion)
665+
function new_scope_layer(ctx, mod_ref::Module=ctx.mod)
666+
new_layer = ScopeLayer(length(ctx.scope_layers)+1, ctx.mod, 0, false)
667667
push!(ctx.scope_layers, new_layer)
668668
new_layer.id
669669
end

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: 2 additions & 2 deletions
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
#-------------------------------------------------------------------------------
@@ -2555,7 +2555,7 @@ function keyword_function_defs(ctx, srcref, callex_srcref, name_str, typevar_nam
25552555
end
25562556
# TODO: Is the layer correct here? Which module should be the parent module
25572557
# of this body function?
2558-
layer = new_scope_layer(ctx; is_macro_expansion=false)
2558+
layer = new_scope_layer(ctx)
25592559
body_func_name = adopt_scope(@ast(ctx, callex_srcref, mangled_name::K"Identifier"), layer)
25602560

25612561
kwcall_arg_names = SyntaxList(ctx)

src/kinds.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,11 @@ function _register_kinds()
4848
# Internal initializer for struct types, for inner constructors/functions
4949
"new"
5050
"splatnew"
51-
# For expr-macro compatibility; gone after expansion
51+
# Used for converting `esc()`'d expressions arising from old macro
52+
# invocations during macro expansion (gone after macro expansion)
5253
"escape"
54+
# Used for converting the old-style macro hygienic-scope form (gone
55+
# after macro expansion)
5356
"hygienic_scope"
5457
# An expression which will eventually be evaluated "statically" in
5558
# the context of a CodeInfo and thus allows access only to globals

src/macro_expansion.jl

Lines changed: 144 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,25 @@ generates a new layer.
1111
struct ScopeLayer
1212
id::LayerId
1313
mod::Module
14+
parent_layer::LayerId # Index of parent layer in a macro expansion. Equal to 0 for no parent
1415
is_macro_expansion::Bool # FIXME
1516
end
1617

1718
struct MacroExpansionContext{GraphType} <: AbstractLoweringContext
1819
graph::GraphType
1920
bindings::Bindings
2021
scope_layers::Vector{ScopeLayer}
21-
current_layer::ScopeLayer
22+
scope_layer_stack::Vector{LayerId}
2223
end
2324

25+
function MacroExpansionContext(graph::SyntaxGraph, mod::Module)
26+
layers = ScopeLayer[ScopeLayer(1, mod, 0, false)]
27+
MacroExpansionContext(graph, Bindings(), layers, LayerId[length(layers)])
28+
end
29+
30+
current_layer(ctx::MacroExpansionContext) = ctx.scope_layers[last(ctx.scope_layer_stack)]
31+
current_layer_id(ctx::MacroExpansionContext) = last(ctx.scope_layer_stack)
32+
2433
#--------------------------------------------------
2534
# Expansion of quoted expressions
2635
function collect_unquoted!(ctx, unquoted, ex, depth)
@@ -130,7 +139,7 @@ function eval_macro_name(ctx::MacroExpansionContext, mctx::MacroContext, ex::Syn
130139
ctx3, ex3 = resolve_scopes(ctx2, ex2)
131140
ctx4, ex4 = convert_closures(ctx3, ex3)
132141
ctx5, ex5 = linearize_ir(ctx4, ex4)
133-
mod = ctx.current_layer.mod
142+
mod = current_layer(ctx).mod
134143
expr_form = to_lowered_expr(mod, ex5)
135144
try
136145
eval(mod, expr_form)
@@ -139,54 +148,124 @@ function eval_macro_name(ctx::MacroExpansionContext, mctx::MacroContext, ex::Syn
139148
end
140149
end
141150

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

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

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
257+
if kind(expanded) != K"Value"
180258
expanded = append_sourceref(ctx, expanded, ex)
181259
# Module scope for the returned AST is the module where this particular
182260
# method was defined (may be different from `parentmodule(macfunc)`)
183-
mod_for_ast = lookup_method_instance(macfunc, macro_args, macro_invocation_world).def.module
184-
new_layer = ScopeLayer(length(ctx.scope_layers)+1, mod_for_ast, true)
261+
mod_for_ast = lookup_method_instance(macfunc, macro_args,
262+
macro_world).def.module
263+
new_layer = ScopeLayer(length(ctx.scope_layers)+1, mod_for_ast,
264+
current_layer_id(ctx), true)
185265
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"
266+
push!(ctx.scope_layer_stack, new_layer.id)
267+
expanded = expand_forms_1(ctx, expanded)
268+
pop!(ctx.scope_layer_stack)
190269
end
191270
return expanded
192271
end
@@ -231,18 +310,37 @@ function expand_forms_1(ctx::MacroExpansionContext, ex::SyntaxTree)
231310
# turned into normal bindings (eg, assigned to)
232311
@ast ctx ex name_str::K"core"
233312
else
234-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
313+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
235314
makeleaf(ctx, ex, ex, kind=K"Identifier", scope_layer=layerid)
236315
end
237316
elseif k == K"Identifier" || k == K"MacroName" || k == K"StringMacroName"
238-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
317+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
239318
makeleaf(ctx, ex, ex, kind=K"Identifier", scope_layer=layerid)
240319
elseif k == K"var" || k == K"char" || k == K"parens"
241320
# Strip "container" nodes
242321
@chk numchildren(ex) == 1
243322
expand_forms_1(ctx, ex[1])
323+
elseif k == K"escape"
324+
# For processing of old-style macros
325+
@chk numchildren(ex) >= 1 "`escape` requires an argument"
326+
if length(ctx.scope_layer_stack) === 1
327+
throw(MacroExpansionError(ex, "`escape` node in outer context"))
328+
end
329+
top_layer = pop!(ctx.scope_layer_stack)
330+
escaped_ex = expand_forms_1(ctx, ex[1])
331+
push!(ctx.scope_layer_stack, top_layer)
332+
escaped_ex
333+
elseif k == K"hygienic_scope"
334+
@chk numchildren(ex) >= 2 && ex[2].value isa Module (ex,"`hygienic_scope` requires an AST and a module")
335+
new_layer = ScopeLayer(length(ctx.scope_layers)+1, ex[2].value,
336+
current_layer_id(ctx), true)
337+
push!(ctx.scope_layers, new_layer)
338+
push!(ctx.scope_layer_stack, new_layer.id)
339+
hyg_ex = expand_forms_1(ctx, ex[1])
340+
pop!(ctx.scope_layer_stack)
341+
hyg_ex
244342
elseif k == K"juxtapose"
245-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
343+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
246344
@chk numchildren(ex) == 2
247345
@ast ctx ex [K"call"
248346
"*"::K"Identifier"(scope_layer=layerid)
@@ -330,7 +428,7 @@ function expand_forms_1(ctx::MacroExpansionContext, ex::SyntaxTree)
330428
elseif k == K"<:" || k == K">:" || k == K"-->"
331429
# TODO: Should every form get layerid systematically? Or only the ones
332430
# which expand_forms_2 needs?
333-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
431+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
334432
mapchildren(e->expand_forms_1(ctx,e), ctx, ex; scope_layer=layerid)
335433
else
336434
mapchildren(e->expand_forms_1(ctx,e), ctx, ex)
@@ -343,13 +441,12 @@ function expand_forms_1(mod::Module, ex::SyntaxTree)
343441
scope_layer=LayerId,
344442
__macro_ctx__=Nothing,
345443
meta=CompileHints)
346-
layers = ScopeLayer[ScopeLayer(1, mod, false)]
347-
ctx = MacroExpansionContext(graph, Bindings(), layers, layers[1])
444+
ctx = MacroExpansionContext(graph, mod)
348445
ex2 = expand_forms_1(ctx, reparent(ctx, ex))
349446
graph2 = delete_attributes(graph, :__macro_ctx__)
350447
# 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)
448+
# to carry state into the next pass. We might fix this by attaching such
449+
# data to the graph itself as global attributes?
450+
ctx2 = MacroExpansionContext(graph2, ctx.bindings, ctx.scope_layers, LayerId[])
354451
return ctx2, reparent(ctx2, ex2)
355452
end

0 commit comments

Comments
 (0)