Skip to content

Commit 92ee09a

Browse files
authored
Add scope layer for macro arguments of normally-quoted AST fragments (#109)
Macros may pull apart an expression (eg, a module expression or the right hand side of a `.` expression) or quote that expression, and we should keep track of the scope where this originated. A particular example is the `@eval` macro. Consider ``` let name = :x @eval A.$name end ``` In this case the right hand side of `.` would normally be quoted (as a plain symbol) but in the case of `@eval` an extra `quote` is added around the expression to make the `name` variable valid unquoted code after quote expansion. In general, macros may pull apart or rearrange what's passed to them, so we can't make the assumption that normally-inert syntax passed to them should go without a scope layer. To fix this, this change adds a scope layer to all ASTs passed to macros. After macro expansion is done, we can then remove the layer from any AST we know is definitely inert to prevent it from interfering with future lowering passes over that quoted code. This helps but isn't a full solution - see #111 for further work.
1 parent f55df67 commit 92ee09a

File tree

5 files changed

+121
-10
lines changed

5 files changed

+121
-10
lines changed

src/eval.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,11 @@ end
461461
_eval(mod, iter)
462462
end
463463

464+
# Version of eval() taking `Expr` (or Expr tree leaves of any type)
465+
function eval(mod::Module, ex; opts...)
466+
eval(mod, expr_to_syntaxtree(ex); opts...)
467+
end
468+
464469
if VERSION >= v"1.13.0-DEV.1199" # https://github.com/JuliaLang/julia/pull/59604
465470

466471
function _eval(mod, iter)

src/macro_expansion.jl

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -189,13 +189,9 @@ end
189189
function set_macro_arg_hygiene(ctx, ex, layer_ids, layer_idx)
190190
k = kind(ex)
191191
scope_layer = get(ex, :scope_layer, layer_ids[layer_idx])
192-
if k == K"module" || k == K"toplevel" || k == K"inert"
193-
makenode(ctx, ex, ex, children(ex);
194-
scope_layer=scope_layer)
195-
elseif k == K"."
196-
makenode(ctx, ex, ex, set_macro_arg_hygiene(ctx, ex[1], layer_ids, layer_idx), ex[2],
197-
scope_layer=scope_layer)
198-
elseif !is_leaf(ex)
192+
if is_leaf(ex)
193+
makeleaf(ctx, ex, ex; scope_layer=scope_layer)
194+
else
199195
inner_layer_idx = layer_idx
200196
if k == K"escape"
201197
inner_layer_idx = layer_idx - 1
@@ -210,8 +206,6 @@ function set_macro_arg_hygiene(ctx, ex, layer_ids, layer_idx)
210206
end
211207
mapchildren(e->set_macro_arg_hygiene(ctx, e, layer_ids, inner_layer_idx),
212208
ctx, ex; scope_layer=scope_layer)
213-
else
214-
makeleaf(ctx, ex, ex; scope_layer=scope_layer)
215209
end
216210
end
217211

@@ -359,6 +353,20 @@ function append_sourceref(ctx, ex, secondary_prov)
359353
end
360354
end
361355

356+
function remove_scope_layer!(ex)
357+
if !is_leaf(ex)
358+
for c in children(ex)
359+
remove_scope_layer!(c)
360+
end
361+
end
362+
deleteattr!(ex, :scope_layer)
363+
ex
364+
end
365+
366+
function remove_scope_layer(ctx, ex)
367+
remove_scope_layer!(copy_ast(ctx, ex))
368+
end
369+
362370
"""
363371
Lowering pass 1
364372
@@ -441,7 +449,12 @@ function expand_forms_1(ctx::MacroExpansionContext, ex::SyntaxTree)
441449
elseif k == K"macrocall"
442450
expand_macro(ctx, ex)
443451
elseif k == K"module" || k == K"toplevel" || k == K"inert"
444-
ex
452+
# Remove scope layer information from any inert syntax which survives
453+
# macro expansion so that it doesn't contaminate lowering passes which
454+
# are later run against the quoted code. TODO: This works as a first
455+
# approximation but is incorrect in general. We need to revisit such
456+
# "deferred hygiene" situations (see https://github.com/c42f/JuliaLowering.jl/issues/111)
457+
remove_scope_layer(ctx, ex)
445458
elseif k == K"." && numchildren(ex) == 2
446459
# Handle quoted property access like `x.:(foo)` or `Core.:(!==)`
447460
# Unwrap the quote to get the identifier before expansion

src/syntax_graph.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ function setattr!(graph::SyntaxGraph, id; attrs...)
155155
end
156156
end
157157

158+
function deleteattr!(graph::SyntaxGraph, id::NodeId, name::Symbol)
159+
delete!(getattr(graph, name), id)
160+
end
161+
158162
function Base.getproperty(graph::SyntaxGraph, name::Symbol)
159163
# TODO: Remove access to internals?
160164
name === :edge_ranges && return getfield(graph, :edge_ranges)
@@ -294,6 +298,10 @@ function setattr!(ex::SyntaxTree; attrs...)
294298
setattr!(ex._graph, ex._id; attrs...)
295299
end
296300

301+
function deleteattr!(ex::SyntaxTree, name::Symbol)
302+
deleteattr!(ex._graph, ex._id, name)
303+
end
304+
297305
# JuliaSyntax tree API
298306

299307
function JuliaSyntax.is_leaf(ex::SyntaxTree)

test/macros.jl

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
@testset "macro tests" begin
22

33
test_mod = Module(:macro_test)
4+
Base.eval(test_mod, :(const var"@ast" = $(JuliaLowering.var"@ast")))
5+
Base.eval(test_mod, :(const var"@K_str" = $(JuliaLowering.var"@K_str")))
46

57
JuliaLowering.include_string(test_mod, raw"""
68
module M
@@ -406,4 +408,74 @@ end
406408

407409
end
408410

411+
@testset "scope layers for normally-inert ASTs" begin
412+
# Right hand side of `.`
413+
@test JuliaLowering.include_string(test_mod, raw"""
414+
let x = :(hi)
415+
:(A.$x)
416+
end
417+
""") @ast_ [K"."
418+
"A"::K"Identifier"
419+
"hi"::K"Identifier"
420+
]
421+
# module
422+
@test JuliaLowering.include_string(test_mod, raw"""
423+
let x = :(AA)
424+
:(module $x
425+
end
426+
)
427+
end
428+
""") @ast_ [K"module"
429+
"AA"::K"Identifier"
430+
[K"block"
431+
]
432+
]
433+
434+
# In macro expansion, require that expressions passed in as macro
435+
# *arguments* get the lexical scope of the calling context, even for the
436+
# `x` in `M.$x` where the right hand side of `.` is normally quoted.
437+
@test JuliaLowering.include_string(test_mod, raw"""
438+
let x = :(someglobal)
439+
@eval M.$x
440+
end
441+
""") == "global in module M"
442+
443+
JuliaLowering.include_string(test_mod, raw"""
444+
let y = 101
445+
@eval module AA
446+
x = $y
447+
end
448+
end
449+
""")
450+
@test test_mod.AA.x == 101
451+
452+
# "Deferred hygiene" in macros which emit quoted code currently doesn't
453+
# work as might be expected.
454+
#
455+
# The old macro system also doesn't handle this - here's the equivalent
456+
# implementation
457+
# macro make_quoted_code(init, y)
458+
# QuoteNode(:(let
459+
# x = "inner x"
460+
# $(esc(init))
461+
# ($(esc(y)), x)
462+
# end))
463+
# end
464+
#
465+
# TODO: The following should throw an error rather than producing a
466+
# surprising value, or work "as expected" whatever that is!
467+
JuliaLowering.include_string(test_mod, raw"""
468+
macro make_quoted_code(init, y)
469+
q = :(let
470+
x = "inner x"
471+
$init
472+
($y, x)
473+
end)
474+
@ast q q [K"inert" q]
475+
end
476+
""")
477+
code = JuliaLowering.include_string(test_mod, """@make_quoted_code(x="outer x", x)""")
478+
@test_broken JuliaLowering.eval(test_mod, code) == ("outer x", "inner x")
479+
end
480+
409481
end

test/quoting_ir.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@ end
3131
3 (call JuliaLowering.interpolate_ast SyntaxTree (inert (block (quote (block (call-i ($ ($ x)) + 1))))) %₂)
3232
4 (return %₃)
3333

34+
########################################
35+
# Symbols on `.` right hand side need to be scoped correctly
36+
let x = 1
37+
:(A.$x)
38+
end
39+
#---------------------
40+
1 1
41+
2 (= slot₁/x %₁)
42+
3 slot₁/x
43+
4 (call core.tuple %₃)
44+
5 (call JuliaLowering.interpolate_ast SyntaxTree (inert (. A ($ x))) %₄)
45+
6 (return %₅)
46+
3447
########################################
3548
# Error: Double escape
3649
quote

0 commit comments

Comments
 (0)