Skip to content

Commit 5a718b4

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 5a718b4

File tree

10 files changed

+478
-120
lines changed

10 files changed

+478
-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

0 commit comments

Comments
 (0)