Skip to content

Commit d43ca71

Browse files
committed
macroexpand: stop pre-running the hygiene pass
This prepares us to delete the separate hygiene pass, and make it part of lowering resolve-scopes. This affects a few macros which expect to run macroexpand then mangle the result more, and the result of those macros is not already wrapped in `esc()`. The damage seems relatively minor however, and generally a good improvement.
1 parent c19c68e commit d43ca71

File tree

5 files changed

+34
-138
lines changed

5 files changed

+34
-138
lines changed

doc/src/base/reflection.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ be passed instead!). For example:
8787

8888
```jldoctest; setup = :(using InteractiveUtils)
8989
julia> InteractiveUtils.macroexpand(@__MODULE__, :(@edit println("")) )
90-
:(InteractiveUtils.edit(println, (Base.typesof)("")))
90+
:($(Expr(Symbol("hygienic-scope"), :(edit($(Expr(:escape, :println)), (Base.typesof)($(Expr(:escape, ""))))), InteractiveUtils)))
9191
```
9292

9393
The functions `Base.Meta.show_sexpr` and [`dump`](@ref) are used to display S-expr style views

doc/src/manual/metaprogramming.md

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -535,20 +535,21 @@ this is an extremely useful tool for debugging macros):
535535

536536
```julia-repl sayhello2
537537
julia> ex = macroexpand(Main, :(@sayhello("human")) )
538-
:(Main.println("Hello, ", "human"))
538+
:($(Expr(Symbol("hygienic-scope"), :(println("Hello, ", "human")), Main)))
539539
540540
julia> typeof(ex)
541541
Expr
542542
```
543543

544-
We can see that the `"human"` literal has been interpolated into the expression.
544+
We can see that the macro expression has been replaced by a `hygienic-scope` expression in
545+
its place and the `"human"` literal has been interpolated into the expression.
545546

546547
There also exists a macro [`@macroexpand`](@ref) that is perhaps a bit more convenient than the `macroexpand` function:
547548

548549

549550
```jldoctest sayhello2
550551
julia> @macroexpand @sayhello "human"
551-
:(println("Hello, ", "human"))
552+
:($(Expr(Symbol("hygienic-scope"), :(println("Hello, ", "human")), Main)))
552553
```
553554

554555
### Hold up: why macros?
@@ -579,7 +580,7 @@ julia> typeof(ex)
579580
Expr
580581
581582
julia> ex
582-
:(println("I execute at runtime. The argument is: ", $(Expr(:copyast, :($(QuoteNode(:((1, 2, 3)))))))))
583+
:($(Expr(Symbol("hygienic-scope"), :(println("I execute at runtime. The argument is: ", $(Expr(:copyast, :($(QuoteNode(:((1, 2, 3))))))))), Main)))
583584
584585
julia> eval(ex)
585586
I execute at runtime. The argument is: (1, 2, 3)
@@ -728,18 +729,18 @@ of a macro expansion with the aptly named [`@macroexpand`](@ref) macro:
728729

729730
```julia-repl assert2
730731
julia> @macroexpand @assert a == b
731-
:(if Main.a == Main.b
732-
Main.nothing
733-
else
734-
Main.throw(Main.AssertionError("a == b"))
735-
end)
732+
:($(Expr(Symbol("hygienic-scope"), :(if $(Expr(:escape, :(a == b)))
733+
nothing
734+
else
735+
throw(AssertionError("a == b"))
736+
end), Base)))
736737
737738
julia> @macroexpand @assert a==b "a should equal b!"
738-
:(if Main.a == Main.b
739-
Main.nothing
740-
else
741-
Main.throw(Main.AssertionError("a should equal b!"))
742-
end)
739+
:($(Expr(Symbol("hygienic-scope"), :(if $(Expr(:escape, :(a == b)))
740+
nothing
741+
else
742+
throw(AssertionError("a should equal b!"))
743+
end), Base)))
743744
```
744745

745746
There is yet another case that the actual `@assert` macro handles: what if, in addition to printing
@@ -777,15 +778,22 @@ of expressions inside the macro body.
777778
### Hygiene
778779

779780
An issue that arises in more complex macros is that of [hygiene](https://en.wikipedia.org/wiki/Hygienic_macro).
780-
In short, macros must ensure that the variables they introduce in their returned expressions do
781-
not accidentally clash with existing variables in the surrounding code they expand into. Conversely,
782-
the expressions that are passed into a macro as arguments are often *expected* to evaluate in
783-
the context of the surrounding code, interacting with and modifying the existing variables. Another
784-
concern arises from the fact that a macro may be called in a different module from where it was
785-
defined. In this case we need to ensure that all global variables are resolved to the correct
786-
module. Julia already has a major advantage over languages with textual macro expansion (like
787-
C) in that it only needs to consider the returned expression. All the other variables (such as
788-
`msg` in `@assert` above) follow the [normal scoping block behavior](@ref scope-of-variables).
781+
We saw this a bit earlier with the hygienic-scope expression in the result of macroexpand
782+
that demarcated where our expression came from as a result of interpolation of the expanded
783+
macro into the rest of the expression.
784+
785+
This already gives Julia a major advantage over languages with textual macro expansion (like
786+
C) in that it only needs to consider the returned expression, and it cannot change the
787+
parsing of the expression surrounding it. Thus, all the other variables (such as `msg` in
788+
`@assert` above) follow the [normal scoping block behavior](@ref scope-of-variables).
789+
790+
Hygiene markers exists because macros must ensure that the variables they introduce in their
791+
returned expressions do not accidentally clash with existing variables in the surrounding
792+
code they expand into. Conversely, the expressions that are passed into a macro as arguments
793+
are often *expected* to evaluate in the context of the surrounding code, interacting with
794+
and modifying the existing variables. Another concern arises from the fact that a macro may
795+
be called in a different module from where it was defined. In this case we need to ensure
796+
that all global variables are resolved to the correct module.
789797

790798
To demonstrate these issues, let us consider writing a `@time` macro that takes an expression
791799
as its argument, records the time, evaluates the expression, records the time again, prints the

src/ast.c

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,7 +1255,6 @@ JL_DLLEXPORT jl_value_t *jl_macroexpand(jl_value_t *expr, jl_module_t *inmodule)
12551255
JL_GC_PUSH1(&expr);
12561256
expr = jl_copy_ast(expr);
12571257
expr = jl_expand_macros(expr, inmodule, NULL, 0, jl_atomic_load_acquire(&jl_world_counter), 0);
1258-
expr = jl_call_scm_on_ast("jl-expand-macroscope", expr, inmodule);
12591258
JL_GC_POP();
12601259
return expr;
12611260
}
@@ -1266,7 +1265,6 @@ JL_DLLEXPORT jl_value_t *jl_macroexpand1(jl_value_t *expr, jl_module_t *inmodule
12661265
JL_GC_PUSH1(&expr);
12671266
expr = jl_copy_ast(expr);
12681267
expr = jl_expand_macros(expr, inmodule, NULL, 1, jl_atomic_load_acquire(&jl_world_counter), 0);
1269-
expr = jl_call_scm_on_ast("jl-expand-macroscope", expr, inmodule);
12701268
JL_GC_POP();
12711269
return expr;
12721270
}

src/jlfrontend.scm

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,6 @@
194194
(define (jl-expand-to-thunk-stmt expr file line)
195195
(expand-to-thunk-stmt- expr file line))
196196

197-
(define (jl-expand-macroscope expr)
198-
(error-wrap (lambda ()
199-
(julia-expand-macroscope expr))))
200-
201197
;; construct default definitions of `eval` for non-bare modules
202198
;; called by jl_eval_module_expr
203199
(define (module-default-defs name file line)

test/syntax.jl

Lines changed: 2 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1557,7 +1557,7 @@ let ex = Meta.parse("@test27521(2) do y; y; end")
15571557
fex = Expr(:(->), Expr(:tuple, :y), Expr(:block, LineNumberNode(1,:none), :y))
15581558
@test ex == Expr(:do, Expr(:macrocall, Symbol("@test27521"), LineNumberNode(1,:none), 2),
15591559
fex)
1560-
@test macroexpand(@__MODULE__, ex) == Expr(:tuple, fex, 2)
1560+
@test macroexpand(@__MODULE__, ex).args[1] == Expr(:tuple, esc(fex), 2)
15611561
end
15621562

15631563
# issue #43018
@@ -3530,110 +3530,4 @@ end
35303530
@test eval(:(if false
35313531
elseif false || (()->true)()
35323532
42
3533-
end)) == 42
3534-
3535-
macro _macroexpand(x, m=__module__)
3536-
:($__source__; macroexpand($m, Expr(:var"hygienic-scope", $(esc(Expr(:quote, x))), $m)))
3537-
end
3538-
3539-
@testset "unescaping in :global expressions" begin
3540-
m = @__MODULE__
3541-
@test @_macroexpand(global x::T) == :(global x::$(GlobalRef(m, :T)))
3542-
@test @_macroexpand(global (x, $(esc(:y)))) == :(global (x, y))
3543-
@test @_macroexpand(global (x::S, $(esc(:y))::$(esc(:T)))) ==
3544-
:(global (x::$(GlobalRef(m, :S)), y::T))
3545-
@test @_macroexpand(global (; x, $(esc(:y)))) == :(global (; x, y))
3546-
@test @_macroexpand(global (; x::S, $(esc(:y))::$(esc(:T)))) ==
3547-
:(global (; x::$(GlobalRef(m, :S)), y::T))
3548-
3549-
@test @_macroexpand(global x::T = a) == :(global x::$(GlobalRef(m, :T)) = $(GlobalRef(m, :a)))
3550-
@test @_macroexpand(global (x, $(esc(:y))) = a) == :(global (x, y) = $(GlobalRef(m, :a)))
3551-
@test @_macroexpand(global (x::S, $(esc(:y))::$(esc(:T))) = a) ==
3552-
:(global (x::$(GlobalRef(m, :S)), y::T) = $(GlobalRef(m, :a)))
3553-
@test @_macroexpand(global (; x, $(esc(:y))) = a) == :(global (; x, y) = $(GlobalRef(m, :a)))
3554-
@test @_macroexpand(global (; x::S, $(esc(:y))::$(esc(:T))) = a) ==
3555-
:(global (; x::$(GlobalRef(m, :S)), y::T) = $(GlobalRef(m, :a)))
3556-
end
3557-
3558-
# issue #49920
3559-
let line1 = (quote end).args[1],
3560-
line2 = (quote end).args[1],
3561-
line3 = (quote end).args[1]
3562-
@test 1 === eval(Meta.lower(Main, Expr(:block, line1, 1, line2, line3)))
3563-
end
3564-
3565-
# issue #49984
3566-
macro z49984(s); :(let a; $(esc(s)); end); end
3567-
@test let a = 1; @z49984(a) === 1; end
3568-
3569-
# issues #37783, #39929, #42552, #43379, and #48332
3570-
let x = 1 => 2
3571-
@test_throws ErrorException @eval a => b = 2
3572-
@test_throws "function Base.=> must be explicitly imported to be extended" @eval a => b = 2
3573-
end
3574-
3575-
# Splatting in non-final default value (Ref #50518)
3576-
for expr in (quote
3577-
function g1(a=(1,2)..., b...=3)
3578-
b
3579-
end
3580-
end,quote
3581-
function g2(a=(1,2)..., b=3, c=4)
3582-
(b, c)
3583-
end
3584-
end,quote
3585-
function g3(a=(1,2)..., b=3, c...=4)
3586-
(b, c)
3587-
end
3588-
end)
3589-
let exc = try eval(expr); catch exc; exc end
3590-
@test isa(exc, ErrorException)
3591-
@test startswith(exc.msg, "syntax: invalid \"...\" in non-final positional argument default value")
3592-
end
3593-
end
3594-
3595-
# Test that bad lowering does not segfault (ref #50518)
3596-
@test_throws ErrorException("syntax: Attempted to use slot marked unused") @eval function funused50518(::Float64)
3597-
$(Symbol("#unused#"))
3598-
end
3599-
3600-
@testset "public keyword" begin
3601-
p(str) = Base.remove_linenums!(Meta.parse(str))
3602-
# tests ported from JuliaSyntax.jl
3603-
@test p("function f(public)\n public + 3\nend") == Expr(:function, Expr(:call, :f, :public), Expr(:block, Expr(:call, :+, :public, 3)))
3604-
@test p("public A, B") == Expr(:public, :A, :B)
3605-
@test p("if true \n public *= 4 \n end") == Expr(:if, true, Expr(:block, Expr(:*=, :public, 4)))
3606-
@test p("module Mod\n public A, B \n end") == Expr(:module, true, :Mod, Expr(:block, Expr(:public, :A, :B)))
3607-
@test p("module Mod2\n a = 3; b = 6; public a, b\n end") == Expr(:module, true, :Mod2, Expr(:block, Expr(:(=), :a, 3), Expr(:(=), :b, 6), Expr(:public, :a, :b)))
3608-
@test p("a = 3; b = 6; public a, b") == Expr(:toplevel, Expr(:(=), :a, 3), Expr(:(=), :b, 6), Expr(:public, :a, :b))
3609-
@test_throws Meta.ParseError p("begin \n public A, B \n end")
3610-
@test_throws Meta.ParseError p("if true \n public A, B \n end")
3611-
@test_throws Meta.ParseError p("public export=true foo, bar")
3612-
@test_throws Meta.ParseError p("public experimental=true foo, bar")
3613-
@test p("public(x::String) = false") == Expr(:(=), Expr(:call, :public, Expr(:(::), :x, :String)), Expr(:block, false))
3614-
@test p("module M; export @a; end") == Expr(:module, true, :M, Expr(:block, Expr(:export, :var"@a")))
3615-
@test p("module M; public @a; end") == Expr(:module, true, :M, Expr(:block, Expr(:public, :var"@a")))
3616-
@test p("module M; export ⤈; end") == Expr(:module, true, :M, Expr(:block, Expr(:export, :)))
3617-
@test p("module M; public ⤈; end") == Expr(:module, true, :M, Expr(:block, Expr(:public, :)))
3618-
@test p("public = 4") == Expr(:(=), :public, 4)
3619-
@test p("public[7] = 5") == Expr(:(=), Expr(:ref, :public, 7), 5)
3620-
@test p("public() = 6") == Expr(:(=), Expr(:call, :public), Expr(:block, 6))
3621-
end
3622-
3623-
@testset "removing argument sideeffects" begin
3624-
# Allow let blocks in broadcasted LHSes, but only evaluate them once:
3625-
execs = 0
3626-
array = [1]
3627-
let x = array; execs += 1; x; end .+= 2
3628-
@test array == [3]
3629-
@test execs == 1
3630-
let; execs += 1; array; end .= 4
3631-
@test array == [4]
3632-
@test execs == 2
3633-
let x = array; execs += 1; x; end::Vector{Int} .+= 2
3634-
@test array == [6]
3635-
@test execs == 3
3636-
let; execs += 1; array; end::Vector{Int} .= 7
3637-
@test array == [7]
3638-
@test execs == 4
3639-
end
3533+
end)) == 42

0 commit comments

Comments
 (0)