Skip to content

Commit 4d7da74

Browse files
authored
add macroexpand! function and add legacyscope kwarg (#59276)
Setting `legacyscope=false` is intended to make it much easier to debug and test macro expansion, since it no longer runs a buggy symbol mangling pass automatically. Adding the mutating version (`macroexpand!`) is mainly a handy way to opt in to the new legacyscope=true, without needing to spell that out. More background: the macroexpand.scm pass design is buggy, so we'd like to stop using in the future. Currently changing the default causes visible breakage to a lot of buggy packages tests, so for now just provide the option to skip the legacy scope resolution. This is a continuation of #49793 and a prerequisite for eventually replacing the flisp code with JuliaLowering (once we can deprecate this parameter). Implement in-place macro expansion with `macroexpand!` (no corresponding `@macroexpand!`) that avoids copying AST nodes when the original expression is no longer needed anyways. But more importantly, add a `legacyscope::Bool` keyword argument to the functions that allows opting out of the legacy scope mangling. Changes: - Consolidate `jl_macroexpand` C functions with added parameters for `recursive`, `inplace`, and the (legacy) `expand_scope` control. - Add `macroexpand!` Julia function with `legacyscope=false` default. - Update `macroexpand` to have `legacyscope` (default `true`) for backward compatibility, until v2 or earlier. Added to backporting so that new code can start to be written with `legacyscope=false`. Not entirely a new feature, since this is just adding the ability to disable an old (long deprecated) feature. 🤖 Generated with Claude
1 parent 4c723db commit 4d7da74

File tree

7 files changed

+90
-27
lines changed

7 files changed

+90
-27
lines changed

NEWS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ New library features
6060
* `sort(keys(::Dict))` and `sort(values(::Dict))` now automatically collect, they previously threw ([#56978]).
6161
* `Base.AbstractOneTo` is added as a supertype of one-based axes, with `Base.OneTo` as its subtype ([#56902]).
6262
* `takestring!(::IOBuffer)` removes the content from the buffer, returning the content as a `String`.
63+
* The `macroexpand` (with default true) and the new `macroexpand!` (with default false)
64+
functions now support a `legacyscope` boolean keyword argument to control whether to run
65+
the legacy scope resolution pass over the result. The legacy scope resolution code has
66+
known design bugs and will be disabled by default in a future version. Users should
67+
migrate now by calling `legacyscope=false` or using `macroexpand!`. This may often require
68+
fixes to the code calling `macroexpand` with `Meta.unescape` and `Meta.reescape` or by
69+
updating tests to expect `hygienic-scope` or `escape` markers might appear in the result.
6370

6471
Standard library changes
6572
------------------------

base/exports.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,7 @@ export
837837
gensym,
838838
@kwdef,
839839
macroexpand,
840+
macroexpand!,
840841
@macroexpand1,
841842
@macroexpand,
842843
parse,

base/expr.jl

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,12 @@ function ==(x::DebugInfo, y::DebugInfo)
174174
end
175175

176176
"""
177-
macroexpand(m::Module, x; recursive=true)
177+
macroexpand(m::Module, x; recursive=true, legacyscope=true)
178178
179179
Take the expression `x` and return an equivalent expression with all macros removed (expanded)
180180
for executing in module `m`.
181181
The `recursive` keyword controls whether deeper levels of nested macros are also expanded.
182+
The `legacyscope` keyword controls whether legacy macroscope expansion is performed.
182183
This is demonstrated in the example below:
183184
```jldoctest; filter = r"#= .*:6 =#"
184185
julia> module M
@@ -198,12 +199,28 @@ julia> macroexpand(M, :(@m2()), recursive=false)
198199
:(#= REPL[1]:6 =# @m1)
199200
```
200201
"""
201-
function macroexpand(m::Module, @nospecialize(x); recursive=true)
202-
if recursive
203-
ccall(:jl_macroexpand, Any, (Any, Any), x, m)
204-
else
205-
ccall(:jl_macroexpand1, Any, (Any, Any), x, m)
206-
end
202+
function macroexpand(m::Module, @nospecialize(x); recursive=true, legacyscope=true)
203+
ccall(:jl_macroexpand, Any, (Any, Any, Cint, Cint, Cint), x, m, recursive, false, legacyscope)
204+
end
205+
206+
"""
207+
macroexpand!(m::Module, x; recursive=true, legacyscope=false)
208+
209+
Take the expression `x` and return an equivalent expression with all macros removed (expanded)
210+
for executing in module `m`, modifying `x` in place without copying.
211+
The `recursive` keyword controls whether deeper levels of nested macros are also expanded.
212+
The `legacyscope` keyword controls whether legacy macroscope expansion is performed.
213+
214+
This function performs macro expansion without the initial copy step, making it more efficient
215+
when the original expression is no longer needed. By default, macroscope expansion is disabled
216+
for in-place expansion as it can be called separately if needed.
217+
218+
!!! warning
219+
This function modifies the input expression `x` in place. Use `macroexpand` if you need
220+
to preserve the original expression.
221+
"""
222+
function macroexpand!(m::Module, @nospecialize(x); recursive=true, legacyscope=false)
223+
ccall(:jl_macroexpand, Any, (Any, Any, Cint, Cint, Cint), x, m, recursive, true, legacyscope)
207224
end
208225

209226
"""
@@ -250,10 +267,10 @@ With `macroexpand` the expression expands in the module given as the first argum
250267
The two-argument form requires at least Julia 1.11.
251268
"""
252269
macro macroexpand(code)
253-
return :(macroexpand($__module__, $(QuoteNode(code)), recursive=true))
270+
return :(macroexpand($__module__, $(QuoteNode(code)); recursive=true, legacyscope=true))
254271
end
255272
macro macroexpand(mod, code)
256-
return :(macroexpand($(esc(mod)), $(QuoteNode(code)), recursive=true))
273+
return :(macroexpand($(esc(mod)), $(QuoteNode(code)); recursive=true, legacyscope=true))
257274
end
258275

259276
"""
@@ -262,10 +279,10 @@ end
262279
Non recursive version of [`@macroexpand`](@ref).
263280
"""
264281
macro macroexpand1(code)
265-
return :(macroexpand($__module__, $(QuoteNode(code)), recursive=false))
282+
return :(macroexpand($__module__, $(QuoteNode(code)); recursive=false, legacyscope=true))
266283
end
267284
macro macroexpand1(mod, code)
268-
return :(macroexpand($(esc(mod)), $(QuoteNode(code)), recursive=false))
285+
return :(macroexpand($(esc(mod)), $(QuoteNode(code)); recursive=false, legacyscope=true))
269286
end
270287

271288
## misc syntax ##

doc/src/base/base.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,7 @@ Meta.parse(::AbstractString)
538538
Meta.ParseError
539539
Core.QuoteNode
540540
Base.macroexpand
541+
Base.macroexpand!
541542
Base.@macroexpand
542543
Base.@macroexpand1
543544
Base.code_lowered

src/ast.c

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,24 +1252,15 @@ static jl_value_t *jl_expand_macros(jl_value_t *expr, jl_module_t *inmodule, str
12521252
return expr;
12531253
}
12541254

1255-
JL_DLLEXPORT jl_value_t *jl_macroexpand(jl_value_t *expr, jl_module_t *inmodule)
1255+
JL_DLLEXPORT jl_value_t *jl_macroexpand(jl_value_t *expr, jl_module_t *inmodule, int recursive, int inplace, int expand_scope)
12561256
{
12571257
JL_TIMING(LOWERING, LOWERING);
12581258
JL_GC_PUSH1(&expr);
1259-
expr = jl_copy_ast(expr);
1260-
expr = jl_expand_macros(expr, inmodule, NULL, 0, jl_atomic_load_acquire(&jl_world_counter), 0);
1261-
expr = jl_call_scm_on_ast("jl-expand-macroscope", expr, inmodule);
1262-
JL_GC_POP();
1263-
return expr;
1264-
}
1265-
1266-
JL_DLLEXPORT jl_value_t *jl_macroexpand1(jl_value_t *expr, jl_module_t *inmodule)
1267-
{
1268-
JL_TIMING(LOWERING, LOWERING);
1269-
JL_GC_PUSH1(&expr);
1270-
expr = jl_copy_ast(expr);
1271-
expr = jl_expand_macros(expr, inmodule, NULL, 1, jl_atomic_load_acquire(&jl_world_counter), 0);
1272-
expr = jl_call_scm_on_ast("jl-expand-macroscope", expr, inmodule);
1259+
if (!inplace)
1260+
expr = jl_copy_ast(expr);
1261+
expr = jl_expand_macros(expr, inmodule, NULL, !recursive, jl_atomic_load_acquire(&jl_world_counter), 0);
1262+
if (expand_scope)
1263+
expr = jl_call_scm_on_ast("jl-expand-macroscope", expr, inmodule);
12731264
JL_GC_POP();
12741265
return expr;
12751266
}

src/jl_exported_funcs.inc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,6 @@
294294
XX(jl_lseek) \
295295
XX(jl_lstat) \
296296
XX(jl_macroexpand) \
297-
XX(jl_macroexpand1) \
298297
XX(jl_malloc) \
299298
XX(jl_malloc_stack) \
300299
XX(jl_matching_methods) \

test/syntax.jl

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4555,3 +4555,50 @@ let d = Dict(:a=>1)
45554555
foo(a::Int) = 2
45564556
@test foo() == 1
45574557
end
4558+
4559+
# Test new macroexpand functionality - define test module at top level
4560+
module MacroExpandTestModule
4561+
macro test_basic(x)
4562+
return :($x + 1)
4563+
end
4564+
end
4565+
4566+
@testset "hygienic-scope" begin
4567+
# Test macroexpand! (in-place expansion)
4568+
expr = :(MacroExpandTestModule.@test_basic(5))
4569+
result = macroexpand!(@__MODULE__, expr)
4570+
# macroexpand! returns a hygienic-scope wrapper with legacyscope=false (default)
4571+
@test Meta.isexpr(result, Symbol("hygienic-scope"))
4572+
@test result.args[1] == :(5 + 1)
4573+
@test result.args[2] === MacroExpandTestModule
4574+
@test result.args[3] isa Core.LineNumberNode
4575+
4576+
# Test legacyscope parameter
4577+
hygiene_expr = :(MacroExpandTestModule.@test_basic(100))
4578+
4579+
# With legacyscope=true (default for macroexpand)
4580+
expanded_with_scope = macroexpand(@__MODULE__, hygiene_expr; legacyscope=true)
4581+
@test expanded_with_scope == :($(GlobalRef(MacroExpandTestModule, :(+)))(100, 1))
4582+
4583+
# With legacyscope=false
4584+
expanded_no_scope = macroexpand(@__MODULE__, hygiene_expr; legacyscope=false)
4585+
@test Meta.isexpr(expanded_no_scope, Symbol("hygienic-scope"))
4586+
@test expanded_no_scope.args[1] == :(100 + 1)
4587+
@test expanded_no_scope.args[2] === MacroExpandTestModule
4588+
@test expanded_no_scope.args[3] isa Core.LineNumberNode
4589+
4590+
# Test macroexpand! with legacyscope=false (default for macroexpand!)
4591+
hygiene_copy = copy(hygiene_expr)
4592+
result_no_scope = macroexpand!(@__MODULE__, hygiene_copy; legacyscope=false)
4593+
@test Meta.isexpr(result_no_scope, Symbol("hygienic-scope"))
4594+
@test result_no_scope.args[1] == :(100 + 1)
4595+
@test result_no_scope.args[2] === MacroExpandTestModule
4596+
@test result_no_scope.args[3] isa Core.LineNumberNode
4597+
end
4598+
4599+
# Test error handling for malformed macro calls
4600+
@testset "macroexpand error handling" begin
4601+
# Test with undefined macro
4602+
@test_throws UndefVarError macroexpand(@__MODULE__, :(@undefined_macro(x)))
4603+
@test_throws UndefVarError macroexpand!(@__MODULE__, :(@undefined_macro(x)))
4604+
end

0 commit comments

Comments
 (0)