Skip to content

Commit cbfa88e

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 5ab1a89 commit cbfa88e

File tree

6 files changed

+217
-47
lines changed

6 files changed

+217
-47
lines changed

README.md

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

289289
TODO: Write more here...
290290

291+
292+
### Compatibility with `Expr` macros
293+
294+
Suppose a manually escaped old-style macro `@oldstyle` is implemented as
295+
296+
```julia
297+
macro oldstyle(a, b)
298+
quote
299+
x = "x in @oldstyle"
300+
@newstyle $(esc(a)) $(esc(b)) x
301+
end
302+
end
303+
```
304+
305+
along with two correctly escaped new-style macros:
306+
307+
```julia
308+
macro call_oldstyle_macro(y)
309+
quote
310+
x = "x in call_oldstyle_macro"
311+
@oldstyle $y x
312+
end
313+
end
314+
315+
macro newstyle(x, y, z)
316+
quote
317+
x = "x in @newstyle"
318+
($x, $y, $z, x)
319+
end
320+
end
321+
```
322+
323+
Then want some code like the following to "just work" with respect to hygiene
324+
325+
```julia
326+
let
327+
x = "x in outer ctx"
328+
@call_oldstyle_macro x
329+
end
330+
```
331+
332+
When calling `@oldstyle`, we must convert `SyntaxTree` into `Expr`, but we need
333+
to preserve the scope layer of the `x` from the outer context as it is passed
334+
into `@oldstyle` as a macro argument. To do this, we use `Expr(:scope_layer,
335+
:x, outer_layer_id)`. (In the old system, this would be `Expr(:escape, :x)`
336+
instead, presuming that `@call_oldstyle_macro` was implemented using `esc()`.)
337+
338+
When receiving output from old style macro invocations, we preserve the escape
339+
handling of the existing system for any symbols which aren't tagged with a
340+
scope layer.
341+
291342
## Pass 2: Syntax desugaring
292343

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

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
@@ -7,6 +7,9 @@ function _register_kinds()
77
# expansion, and known to lowering. These are part of the AST API but
88
# without having surface syntax.
99
"BEGIN_EXTENSION_KINDS"
10+
# Used for converting `esc()`'d expressions arising from old macro
11+
# invocations during macro expansion
12+
"escape"
1013
# atomic fields or accesses (see `@atomic`)
1114
"atomic"
1215
# Flag for @generated parts of a functon

src/macro_expansion.jl

Lines changed: 112 additions & 42 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)
@@ -122,58 +125,119 @@ function eval_macro_name(ctx, ex)
122125
ctx3, ex3 = resolve_scopes(ctx2, ex2)
123126
ctx4, ex4 = convert_closures(ctx3, ex3)
124127
ctx5, ex5 = linearize_ir(ctx4, ex4)
125-
mod = ctx.current_layer.mod
128+
mod = current_layer(ctx).mod
126129
expr_form = to_lowered_expr(mod, ex5)
127130
eval(mod, expr_form)
128131
end
129132

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

133170
macname = ex[1]
134171
macfunc = eval_macro_name(ctx, macname)
135-
# Macro call arguments may be either
136-
# * Unprocessed by the macro expansion pass
137-
# * Previously processed, but spliced into a further macro call emitted by
138-
# a macro expansion.
139-
# In either case, we need to set any unset scope layers before passing the
140-
# arguments to the macro call.
141-
mctx = MacroContext(ctx.graph, ex, ctx.current_layer)
142-
macro_args = Any[mctx]
143-
for i in 2:numchildren(ex)
144-
push!(macro_args, set_scope_layer(ctx, ex[i], ctx.current_layer.id, false))
145-
end
172+
mctx = MacroContext(ctx.graph, ex, current_layer(ctx))
173+
raw_args = ex[2:end]
146174
macro_invocation_world = Base.get_world_counter()
147-
expanded = try
148-
# TODO: Allow invoking old-style macros for compat
149-
invokelatest(macfunc, macro_args...)
150-
catch exc
151-
if exc isa MacroExpansionError
152-
# Add context to the error.
153-
# TODO: Using rethrow() is kinda ugh. Is there a way to avoid it?
154-
rethrow(MacroExpansionError(mctx, exc.ex, exc.msg, exc.position))
175+
if hasmethod(macfunc, Tuple{typeof(mctx), typeof.(raw_args)...}; world=Base.get_world_counter())
176+
macro_args = Any[mctx]
177+
for arg in raw_args
178+
# Add hygiene information to be carried along with macro arguments.
179+
#
180+
# Macro call arguments may be either
181+
# * Unprocessed by the macro expansion pass
182+
# * Previously processed, but spliced into a further macro call emitted by
183+
# a macro expansion.
184+
# In either case, we need to set scope layers before passing the
185+
# arguments to the macro call.
186+
push!(macro_args, set_macro_arg_hygiene(ctx, arg, ctx.scope_layer_stack,
187+
length(ctx.scope_layer_stack)))
188+
end
189+
expanded = try
190+
invokelatest(macfunc, macro_args...)
191+
catch exc
192+
if exc isa MacroExpansionError
193+
# Add context to the error.
194+
# TODO: Using rethrow() is kinda ugh. Is there a way to avoid it?
195+
rethrow(MacroExpansionError(mctx, exc.ex, exc.msg, exc.position))
196+
else
197+
throw(MacroExpansionError(mctx, ex, "Error expanding macro", :all))
198+
end
199+
end
200+
if expanded isa SyntaxTree
201+
if !is_compatible_graph(ctx, expanded)
202+
# If the macro has produced syntax outside the macro context,
203+
# copy it over. TODO: Do we expect this always to happen? What
204+
# is the API for access to the macro expansion context?
205+
expanded = copy_ast(ctx, expanded)
206+
end
155207
else
156-
throw(MacroExpansionError(mctx, ex, "Error expanding macro", :all))
208+
expanded = @ast ctx ex expanded::K"Value"
157209
end
210+
else
211+
# Compat: attempt to invoke an old-style macro if there's no applicable
212+
# method for new-style macro arguments.
213+
macro_args = Any[source_location(LineNumberNode, ex), current_layer(ctx).mod]
214+
for arg in raw_args
215+
# For hygiene in old-style macros, we omit any additional scope
216+
# layer information from macro arguments. Old-style macros will
217+
# handle that using manual escaping in the macro itself.
218+
#
219+
# Note that there's one somewhat-incompatibility here for
220+
# identifiers interpolated into the `raw_args` from outer macro
221+
# expansions of new-style macros which call old-style macros.
222+
# Instead of seeing `Expr(:escape)` in such situations, old-style
223+
# macros will now see `Expr(:scope_layer)` inside `macro_args`.
224+
push!(macro_args, Expr(arg))
225+
end
226+
expanded = invokelatest(macfunc, macro_args...)
227+
expanded = expr_to_SyntaxTree(syntax_graph(ctx), expanded)
158228
end
159229

160-
if expanded isa SyntaxTree
161-
if !is_compatible_graph(ctx, expanded)
162-
# If the macro has produced syntax outside the macro context, copy it over.
163-
# TODO: Do we expect this always to happen? What is the API for access
164-
# to the macro expansion context?
165-
expanded = copy_ast(ctx, expanded)
166-
end
230+
if kind(expanded) != K"Value"
167231
expanded = append_sourceref(ctx, expanded, ex)
168232
# Module scope for the returned AST is the module where this particular
169233
# method was defined (may be different from `parentmodule(macfunc)`)
170-
mod_for_ast = lookup_method_instance(macfunc, macro_args, macro_invocation_world).def.module
234+
mod_for_ast = lookup_method_instance(macfunc, macro_args,
235+
macro_invocation_world).def.module
171236
new_layer = ScopeLayer(length(ctx.scope_layers)+1, mod_for_ast, true)
172237
push!(ctx.scope_layers, new_layer)
173-
inner_ctx = MacroExpansionContext(ctx.graph, ctx.bindings, ctx.scope_layers, new_layer)
174-
expanded = expand_forms_1(inner_ctx, expanded)
175-
else
176-
expanded = @ast ctx ex expanded::K"Value"
238+
push!(ctx.scope_layer_stack, new_layer.id)
239+
expanded = expand_forms_1(ctx, expanded)
240+
pop!(ctx.scope_layer_stack)
177241
end
178242
return expanded
179243
end
@@ -215,18 +279,24 @@ function expand_forms_1(ctx::MacroExpansionContext, ex::SyntaxTree)
215279
elseif is_ccall_or_cglobal(name_str)
216280
@ast ctx ex name_str::K"core"
217281
else
218-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
282+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
219283
makeleaf(ctx, ex, ex, kind=K"Identifier", scope_layer=layerid)
220284
end
221285
elseif k == K"Identifier" || k == K"MacroName" || k == K"StringMacroName"
222-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
286+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
223287
makeleaf(ctx, ex, ex, kind=K"Identifier", scope_layer=layerid)
224288
elseif k == K"var" || k == K"char" || k == K"parens"
225289
# Strip "container" nodes
226290
@chk numchildren(ex) == 1
227291
expand_forms_1(ctx, ex[1])
292+
elseif k == K"escape"
293+
# For processing of old-style macros
294+
top_layer = pop!(ctx.scope_layer_stack)
295+
escaped_ex = expand_forms_1(ctx, ex[1])
296+
push!(ctx.scope_layer_stack, top_layer)
297+
escaped_ex
228298
elseif k == K"juxtapose"
229-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
299+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
230300
@chk numchildren(ex) == 2
231301
@ast ctx ex [K"call"
232302
"*"::K"Identifier"(scope_layer=layerid)
@@ -311,7 +381,7 @@ function expand_forms_1(ctx::MacroExpansionContext, ex::SyntaxTree)
311381
elseif k == K"<:" || k == K">:" || k == K"-->"
312382
# TODO: Should every form get layerid systematically? Or only the ones
313383
# which expand_forms_2 needs?
314-
layerid = get(ex, :scope_layer, ctx.current_layer.id)
384+
layerid = get(ex, :scope_layer, current_layer_id(ctx))
315385
mapchildren(e->expand_forms_1(ctx,e), ctx, ex; scope_layer=layerid)
316386
else
317387
mapchildren(e->expand_forms_1(ctx,e), ctx, ex)
@@ -325,13 +395,13 @@ function expand_forms_1(mod::Module, ex::SyntaxTree)
325395
__macro_ctx__=Nothing,
326396
meta=CompileHints)
327397
layers = ScopeLayer[ScopeLayer(1, mod, false)]
328-
ctx = MacroExpansionContext(graph, Bindings(), layers, layers[1])
398+
ctx = MacroExpansionContext(graph, Bindings(), layers, LayerId[1])
329399
ex2 = expand_forms_1(ctx, reparent(ctx, ex))
330400
graph2 = delete_attributes(graph, :__macro_ctx__)
331401
# TODO: Returning the context with pass-specific mutable data is a bad way
332-
# to carry state into the next pass.
333-
ctx2 = MacroExpansionContext(graph2, ctx.bindings, ctx.scope_layers,
334-
ctx.current_layer)
402+
# to carry state into the next pass. We might fix this by attaching such
403+
# data to the graph itself as global attributes?
404+
ctx2 = MacroExpansionContext(graph2, ctx.bindings, ctx.scope_layers, LayerId[])
335405
return ctx2, reparent(ctx2, ex2)
336406
end
337407

src/syntax_graph.jl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ attrsummary(name, value::Number) = "$name=$value"
422422
function _value_string(ex)
423423
k = kind(ex)
424424
str = k == K"Identifier" || k == K"MacroName" || is_operator(k) ? ex.name_val :
425-
k == K"Placeholder" ? ex.name_val :
425+
k == K"Placeholder" ? ex.name_val :
426426
k == K"SSAValue" ? "%" :
427427
k == K"BindingId" ? "#" :
428428
k == K"label" ? "label" :
@@ -540,7 +540,12 @@ JuliaSyntax.byte_range(ex::SyntaxTree) = byte_range(sourceref(ex))
540540
function JuliaSyntax._expr_leaf_val(ex::SyntaxTree)
541541
name = get(ex, :name_val, nothing)
542542
if !isnothing(name)
543-
Symbol(name)
543+
n = Symbol(name)
544+
if hasattr(ex, :scope_layer)
545+
Expr(:scope_layer, n, ex.scope_layer)
546+
else
547+
n
548+
end
544549
else
545550
ex.value
546551
end

test/demo.jl

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ end
4343

4444
#-------------------------------------------------------------------------------
4545
# Module containing macros used in the demo.
46-
define_macros = false
46+
define_macros = true
4747
if !define_macros
4848
eval(:(module M end))
4949
else
@@ -95,6 +95,27 @@ eval(JuliaLowering.@SyntaxTree :(baremodule M
9595
end
9696
end
9797

98+
macro call_show(x)
99+
quote
100+
z = "z in @call_show"
101+
@show z $x
102+
end
103+
end
104+
105+
macro call_oldstyle_macro(y)
106+
quote
107+
x = "x in call_oldstyle_macro"
108+
@oldstyle $y x
109+
end
110+
end
111+
112+
macro newstyle(x, y, z)
113+
quote
114+
x = "x in @newstyle"
115+
($x, $y, $z, x)
116+
end
117+
end
118+
98119
macro set_a_global(val)
99120
quote
100121
global a_global = $val
@@ -182,6 +203,16 @@ eval(JuliaLowering.@SyntaxTree :(baremodule M
182203

183204
end))
184205
end
206+
207+
Base.eval(M, :(
208+
macro oldstyle(a, b)
209+
quote
210+
x = "x in @oldstyle"
211+
@newstyle $(esc(a)) $(esc(b)) x
212+
end
213+
end
214+
))
215+
185216
#
186217
#-------------------------------------------------------------------------------
187218
# Demos of the prototype
@@ -794,7 +825,17 @@ end
794825
"""
795826

796827
src = """
797-
cglobal(:jl_uv_stdin, Ptr{Cvoid})
828+
let
829+
z = "z in outer ctx"
830+
@call_show z
831+
end
832+
"""
833+
834+
src = """
835+
let
836+
x = "x in outer ctx"
837+
@call_oldstyle_macro x
838+
end
798839
"""
799840

800841
ex = parsestmt(SyntaxTree, src, filename="foo.jl")

0 commit comments

Comments
 (0)