Skip to content

Commit 7ebc13f

Browse files
mlechuc42f
authored andcommitted
Fix macros producing Expr(:toplevel) (#81)
1 parent d862d9e commit 7ebc13f

File tree

2 files changed

+39
-4
lines changed

2 files changed

+39
-4
lines changed

src/macro_expansion.jl

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,44 @@ function prepare_macro_args(ctx, mctx, raw_args)
212212
return macro_args
213213
end
214214

215+
# TODO: Do we need to handle :scope_layer or multiple escapes here?
216+
# See https://github.com/c42f/JuliaLowering.jl/issues/39
217+
"""
218+
Insert a hygienic-scope around each arg of K"toplevel" returned from a macro.
219+
220+
It isn't correct for macro expansion to recurse into a K"toplevel" expression
221+
since one child may define a macro and the next may use it. However, not
222+
recursing now means we lose some important context: the module of the macro we
223+
just expanded, which is necessary for resolving the identifiers in the
224+
K"toplevel" AST. The solution implemented in JuliaLang/julia#53515 was to save
225+
our place and expand later using `Expr(:hygienic-scope toplevel_child mod)`.
226+
227+
Of course, these hygienic-scopes are also necessary because existing user code
228+
contains the corresponding escaping, which would otherwise cause errors. We
229+
already consumed the hygienic-scope that comes with every expansion, but won't
230+
be looking for escapes under :toplevel, so push hygienic-scope under toplevel
231+
"""
232+
function fix_toplevel_expansion(ctx, ex::SyntaxTree, mod::Module, lnn::LineNumberNode)
233+
if kind(ex) === K"toplevel"
234+
mapchildren(ctx, ex) do e
235+
@ast ctx ex [K"hygienic_scope" e mod::K"Value" lnn::K"Value"]
236+
end
237+
else
238+
mapchildren(e->fix_toplevel_expansion(ctx, e, mod, lnn), ctx, ex)
239+
end
240+
end
241+
215242
function expand_macro(ctx, ex)
216243
@assert kind(ex) == K"macrocall"
217244

218245
macname = ex[1]
219246
mctx = MacroContext(ctx.graph, ex, current_layer(ctx))
220247
macfunc = eval_macro_name(ctx, mctx, macname)
221248
raw_args = ex[2:end]
249+
macro_loc = let loc = source_location(LineNumberNode, ex)
250+
# Some macros, e.g. @cmd, don't play nicely with file == nothing
251+
isnothing(loc.file) ? LineNumberNode(loc.line, :none) : loc
252+
end
222253
# We use a specific well defined world age for the next checks and macro
223254
# expansion invocations. This avoids inconsistencies if the latest world
224255
# age changes concurrently.
@@ -248,10 +279,6 @@ function expand_macro(ctx, ex)
248279
else
249280
# Compat: attempt to invoke an old-style macro if there's no applicable
250281
# method for new-style macro arguments.
251-
macro_loc = let loc = source_location(LineNumberNode, ex)
252-
# Some macros, e.g. @cmd, don't play nicely with file == nothing
253-
isnothing(loc.file) ? LineNumberNode(loc.line, :none) : loc
254-
end
255282
macro_args = Any[macro_loc, current_layer(ctx).mod]
256283
for arg in raw_args
257284
# For hygiene in old-style macros, we omit any additional scope
@@ -287,6 +314,7 @@ function expand_macro(ctx, ex)
287314
# method was defined (may be different from `parentmodule(macfunc)`)
288315
mod_for_ast = lookup_method_instance(macfunc, macro_args,
289316
ctx.macro_world).def.module
317+
expanded = fix_toplevel_expansion(ctx, expanded, mod_for_ast, macro_loc)
290318
new_layer = ScopeLayer(length(ctx.scope_layers)+1, mod_for_ast,
291319
current_layer_id(ctx), true)
292320
push!(ctx.scope_layers, new_layer)

test/macros.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,13 @@ end
353353
@test identity(123) === 123
354354
"""; expr_compat_mode=true)
355355
@test test_result.value === true
356+
357+
# @enum produces Expr(:toplevel)
358+
JuliaLowering.include_string(test_mod, """
359+
@enum SOME_ENUM X1 X2 X3
360+
"""; expr_compat_mode=true)
361+
@test test_mod.SOME_ENUM <: Enum
362+
@test test_mod.X1 isa Enum
356363
end
357364

358365
end

0 commit comments

Comments
 (0)