Skip to content

Commit c244cf7

Browse files
committed
Rewrite macros to use automatic hygiene
Manual `gensym` code often contains a lot of mistakes, either because the user uses something like `.` or `+`, or because it combines code from multiple modules (gensym is only unique within a pre-compile unit). This replaces most uses for macro local variables with proper scope markers. I did not rewrite `_lift_one_interp_helper` or `replace_ref_begin_end_` however, since, while possible by adding `esc` to every argument that has not used a gensym value, if any other argument did is a value, I worried that could lead to macroexpand.scm making more new mistakes so I left if for a separate PR. Better yet, we could make a unhygienic-scope and unescape pair for marking the inverse/dual of the usual operations (marking a symbol as unescaped within a region of unhygienic (escaped) code to make these various uses easier to implement. But rewrite `replace_ref_begin_end_` to respect argument order and evaluation count (similar to its julia-syntax.scm counterpart) and scoping (not adding `let` unpredictably).
1 parent 6634dc2 commit c244cf7

File tree

12 files changed

+159
-94
lines changed

12 files changed

+159
-94
lines changed

base/boot.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ end
746746

747747
# module providing the IR object model
748748
# excluding types already exported by Core (GlobalRef, QuoteNode, Expr, LineNumberNode)
749-
# any type beyond these is self-quoting (see also Base.is_ast_node)
749+
# any type beyond these is self-quoting (see also Base.isa_ast_node)
750750
module IR
751751

752752
export CodeInfo, MethodInstance, CodeInstance, GotoNode, GotoIfNot, ReturnNode,

base/cartesian.jl

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,14 @@ If you want just a post-expression, supply [`nothing`](@ref) for the pre-express
3636
parentheses and semicolons, you can supply multi-statement expressions.
3737
"""
3838
macro nloops(N, itersym, rangeexpr, args...)
39-
_nloops(N, itersym, rangeexpr, args...)
39+
_nloops(N, itersym, true, rangeexpr, args...)
4040
end
4141

42-
function _nloops(N::Int, itersym::Symbol, arraysym::Symbol, args::Expr...)
43-
@gensym d
44-
_nloops(N, itersym, :($d->Base.axes($arraysym, $d)), args...)
42+
function _nloops(N::Int, itersym::Symbol, esc_rng::Bool, arraysym::Symbol, args::Expr...)
43+
_nloops(N, itersym, false, :(d->axes($(esc(arraysym)), d)), args...)
4544
end
4645

47-
function _nloops(N::Int, itersym::Symbol, rangeexpr::Expr, args::Expr...)
46+
function _nloops(N::Int, itersym::Symbol, esc_rng::Bool, rangeexpr::Expr, args::Expr...)
4847
if rangeexpr.head !== :->
4948
throw(ArgumentError("second argument must be an anonymous function expression to compute the range"))
5049
end
@@ -55,11 +54,13 @@ function _nloops(N::Int, itersym::Symbol, rangeexpr::Expr, args::Expr...)
5554
ex = Expr(:escape, body)
5655
for dim = 1:N
5756
itervar = inlineanonymous(itersym, dim)
57+
itervar = esc(itervar)
5858
rng = inlineanonymous(rangeexpr, dim)
59+
esc_rng && (rng = esc(rng))
5960
preexpr = length(args) > 1 ? inlineanonymous(args[1], dim) : (:(nothing))
6061
postexpr = length(args) > 2 ? inlineanonymous(args[2], dim) : (:(nothing))
6162
ex = quote
62-
for $(esc(itervar)) = $(esc(rng))
63+
for $itervar = $rng
6364
$(esc(preexpr))
6465
$ex
6566
$(esc(postexpr))

base/experimental.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ within this scope, even if the compiler can't prove this to be the case.
4141
Experimental API. Subject to change without deprecation.
4242
"""
4343
macro aliasscope(body)
44-
sym = gensym()
44+
sym = :aliasscope_result
4545
quote
4646
$(Expr(:aliasscope))
4747
$sym = $(esc(body))

base/meta.jl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,21 @@ function unescape(@nospecialize ex)
628628
return ex
629629
end
630630

631+
"""
632+
Meta.reescape(unescaped_expr, original_expr)
633+
634+
Re-wrap `unescaped_expr` with the same level of escaping as `original_expr` had.
635+
This is the inverse operation of [`unescape`](@ref) - if the original expression
636+
was escaped, the unescaped expression is wrapped in `:escape` again.
637+
"""
638+
function reescape(@nospecialize(unescaped_expr), @nospecialize(original_expr))
639+
if isexpr(original_expr, :escape) || isexpr(original_expr, :var"hygienic-scope")
640+
return reescape(Expr(:escape, unescaped_expr), original_expr.args[1])
641+
else
642+
return unescaped_expr
643+
end
644+
end
645+
631646
"""
632647
Meta.uncurly(expr)
633648

base/reflection.jl

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,19 +1305,19 @@ macro invoke(ex)
13051305
f, args, kwargs = destructure_callex(topmod, ex)
13061306
types = Expr(:curly, :Tuple)
13071307
out = Expr(:call, GlobalRef(Core, :invoke))
1308-
isempty(kwargs) || push!(out.args, Expr(:parameters, kwargs...))
1309-
push!(out.args, f)
1308+
isempty(kwargs) || push!(out.args, Expr(:parameters, Any[esc(kw) for kw in kwargs]...))
1309+
push!(out.args, esc(f))
13101310
push!(out.args, types)
13111311
for arg in args
13121312
if isexpr(arg, :(::))
1313-
push!(out.args, arg.args[1])
1314-
push!(types.args, arg.args[2])
1313+
push!(out.args, esc(arg.args[1]))
1314+
push!(types.args, esc(arg.args[2]))
13151315
else
1316-
push!(out.args, arg)
1317-
push!(types.args, Expr(:call, GlobalRef(Core, :Typeof), arg))
1316+
push!(out.args, esc(arg))
1317+
push!(types.args, Expr(:call, GlobalRef(Core, :Typeof), esc(arg)))
13181318
end
13191319
end
1320-
return esc(out)
1320+
return out
13211321
end
13221322

13231323
getglobalref(gr::GlobalRef, world::UInt) = ccall(:jl_eval_globalref, Any, (Any, UInt), gr, world)
@@ -1367,42 +1367,42 @@ macro invokelatest(ex)
13671367

13681368
if !isa(f, GlobalRef)
13691369
out_f = Expr(:call, GlobalRef(Base, :invokelatest))
1370-
isempty(kwargs) || push!(out_f.args, Expr(:parameters, kwargs...))
1370+
isempty(kwargs) || push!(out_f.args, Expr(:parameters, Any[esc(kw) for kw in kwargs]...))
13711371

13721372
if isexpr(f, :(.))
1373-
s = gensym()
1373+
s = :s
13741374
check = quote
1375-
$s = $(f.args[1])
1375+
$s = $(esc(f.args[1]))
13761376
isa($s, Module)
13771377
end
1378-
push!(out_f.args, Expr(:(.), s, f.args[2]))
1378+
push!(out_f.args, Expr(:(.), s, esc(f.args[2])))
13791379
else
1380-
push!(out_f.args, f)
1380+
push!(out_f.args, esc(f))
13811381
end
1382-
append!(out_f.args, args)
1382+
append!(out_f.args, Any[esc(arg) for arg in args])
13831383

13841384
if @isdefined(s)
1385-
f = :(GlobalRef($s, $(f.args[2])))
1386-
elseif !isa(f, Symbol)
1387-
return esc(out_f)
1385+
f = :(GlobalRef($s, $(esc(f.args[2]))))
1386+
elseif isa(f, Symbol)
1387+
check = esc(:($(Expr(:isglobal, f))))
13881388
else
1389-
check = :($(Expr(:isglobal, f)))
1389+
return out_f
13901390
end
13911391
end
13921392

13931393
out_gr = Expr(:call, GlobalRef(Base, :invokelatest_gr))
1394-
isempty(kwargs) || push!(out_gr.args, Expr(:parameters, kwargs...))
1394+
isempty(kwargs) || push!(out_gr.args, Expr(:parameters, Any[esc(kw) for kw in kwargs]...))
13951395
push!(out_gr.args, isa(f, GlobalRef) ? QuoteNode(f) :
13961396
isa(f, Symbol) ? QuoteNode(GlobalRef(__module__, f)) :
13971397
f)
1398-
append!(out_gr.args, args)
1398+
append!(out_gr.args, Any[esc(arg) for arg in args])
13991399

14001400
if isa(f, GlobalRef)
1401-
return esc(out_gr)
1401+
return out_gr
14021402
end
14031403

14041404
# f::Symbol
1405-
return esc(:($check ? $out_gr : $out_f))
1405+
return :($check ? $out_gr : $out_f)
14061406
end
14071407

14081408
function destructure_callex(topmod::Module, @nospecialize(ex))

base/show.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# This file is a part of Julia. License is MIT: https://julialang.org/license
22

33
using .Compiler: has_typevar
4-
using .Meta: isidentifier, isoperator, isunaryoperator, isbinaryoperator, ispostfixoperator,
4+
using .Meta: isidentifier, isoperator, isunaryoperator, isbinaryoperator, ispostfixoperator,
55
is_id_start_char, is_id_char, _isoperator, is_syntactic_operator, is_valid_identifier
66

77
function show(io::IO, ::MIME"text/plain", u::UndefInitializer)

base/simdloop.jl

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,22 @@ function compile(x, ivdep)
6060
check_body!(x)
6161

6262
var,range = parse_iteration_space(x.args[1])
63-
r = gensym("r") # Range value
64-
j = gensym("i") # Iteration variable for outer loop
65-
n = gensym("n") # Trip count for inner loop
66-
i = gensym("i") # Trip index for inner loop
67-
quote
63+
# r: Range value
64+
# j: Iteration variable for outer loop
65+
# n: Trip count for inner loop
66+
# i: Trip index for inner loop
67+
return quote
6868
# Evaluate range value once, to enhance type and data flow analysis by optimizers.
69-
let $r = $range
70-
for $j in Base.simd_outer_range($r)
71-
let $n = Base.simd_inner_length($r,$j)
72-
if zero($n) < $n
69+
let r = $(esc(range))
70+
for j in Base.simd_outer_range(r)
71+
let n = Base.simd_inner_length(r,j)
72+
if zero(n) < n
7373
# Lower loop in way that seems to work best for LLVM 3.3 vectorizer.
74-
let $i = zero($n)
75-
while $i < $n
76-
local $var = Base.simd_index($r,$j,$i)
77-
$(x.args[2]) # Body of loop
78-
$i += 1
74+
let i = zero(n)
75+
while i < n
76+
local $(esc(var)) = Base.simd_index(r,j,i)
77+
$(esc(x.args[2])) # Body of loop
78+
i += 1
7979
$(Expr(:loopinfo, Symbol("julia.simdloop"), ivdep)) # Mark loop as SIMD loop
8080
end
8181
end
@@ -125,12 +125,12 @@ either case, your inner loop should have the following properties to allow vecto
125125
* No iteration ever waits on a previous iteration to make forward progress.
126126
"""
127127
macro simd(forloop)
128-
esc(compile(forloop, nothing))
128+
compile(forloop, nothing)
129129
end
130130

131131
macro simd(ivdep, forloop)
132132
if ivdep === :ivdep
133-
esc(compile(forloop, Symbol("julia.ivdep")))
133+
compile(forloop, Symbol("julia.ivdep"))
134134
else
135135
throw(SimdError("Only ivdep is valid as the first argument to @simd"))
136136
end

base/some.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,9 @@ macro something(args...)
152152
which is why we need the last argument first
153153
when building the final expression.
154154
=#
155-
for arg in reverse(args)
156-
val = gensym()
155+
for i in reverse(eachindex(args))
156+
arg = args[i]
157+
val = Cartesian.inlineanonymous(:val, i)
157158
expr = quote
158159
$val = $(esc(arg))
159160
if !isnothing($val)

base/views.jl

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ function replace_ref_begin_end_!(ex, withex)
2929
elseif isa(ex,Expr)
3030
if ex.head === :ref
3131
ex.args[1], used_withex = replace_ref_begin_end_!(ex.args[1], withex)
32-
S = isa(ex.args[1],Symbol) ? ex.args[1]::Symbol : gensym(:S) # temp var to cache ex.args[1] if needed
32+
S = gensym(:S) # temp var to cache ex.args[1] if needed. if S is a global or expression, then it has side effects to use
33+
assignments = []
3334
used_S = false # whether we actually need S
3435
# new :ref, so redefine withex
3536
nargs = length(ex.args)-1
@@ -39,29 +40,62 @@ function replace_ref_begin_end_!(ex, withex)
3940
# replace with lastindex(S)
4041
ex.args[2], used_S = replace_ref_begin_end_!(ex.args[2], (:($firstindex($S)),:($lastindex($S))))
4142
else
42-
n = 1
43+
ni = 1
44+
nx = 0
4345
J = lastindex(ex.args)
46+
need_temps = false # whether any arg needs temporaries
47+
48+
# First pass: determine if any argument will needs temporaries
49+
for j = 2:J
50+
exj = ex.args[j]
51+
if isexpr(exj, :...)
52+
need_temps = true
53+
break
54+
end
55+
end
56+
57+
# Second pass: if any need temps, create temps for all args
58+
temp_vars = Tuple{Int,Symbol}[]
4459
for j = 2:J
60+
n = nx === 0 ? ni : :($nx + $ni)
4561
exj, used = replace_ref_begin_end_!(ex.args[j], (:($firstindex($S,$n)),:($lastindex($S,$n))))
4662
used_S |= used
4763
ex.args[j] = exj
48-
if isa(exj,Expr) && exj.head === :...
49-
# splatted object
50-
exjs = exj.args[1]
51-
n = :($n + length($exjs))
52-
elseif isa(n, Expr)
53-
# previous expression splatted
54-
n = :($n + 1)
55-
else
56-
# an integer
57-
n += 1
64+
ni += 1
65+
if need_temps
66+
isva = isexpr(exj, :...) # implied need_temps
67+
if isva
68+
exj = exj.args[1]
69+
end
70+
if isa_ast_node(exj) # create temp to preserve evaluation order and count in case `used` gets set later
71+
exj = gensym(:arg)
72+
push!(temp_vars, (j, exj))
73+
end
74+
if isva
75+
ni -= 1
76+
nx = nx === 0 ? :(length($exj)) : :($nx + length($exj))
77+
end
78+
end
79+
end
80+
81+
# Third pass: if `used`, need to actually make those temp assignments now
82+
if used_S
83+
for (j, temp_var) in temp_vars
84+
exj = ex.args[j]
85+
isva = isexpr(exj, :...) # implied need_temps
86+
if isva
87+
exj = exj.args[1]
88+
end
89+
push!(assignments, :(local $temp_var = $exj))
90+
ex.args[j] = isva ? Expr(:..., temp_var) : temp_var
5891
end
5992
end
6093
end
61-
if used_S && S !== ex.args[1]
94+
95+
if used_S
6296
S0 = ex.args[1]
6397
ex.args[1] = S
64-
ex = Expr(:let, :($S = $S0), ex)
98+
ex = :(local $S = $S0; $(assignments...); $ex)
6599
end
66100
else
67101
# recursive search
@@ -131,11 +165,13 @@ macro view(ex)
131165
# `view(A, idx) = xxx` in cases such as `@view(A[idx]) = xxx.`
132166
if Meta.isexpr(ex, :ref)
133167
ex = Expr(:call, view, ex.args...)
134-
elseif Meta.isexpr(ex, :let) && (arg2 = ex.args[2]; Meta.isexpr(arg2, :ref))
168+
elseif Meta.isexpr(ex, :block)
169+
arg2 = ex.args[end]
170+
Meta.isexpr(arg2, :ref) || error("unsupported replace_ref_begin_end result")
135171
# ex replaced by let ...; foo[...]; end
136-
ex.args[2] = Expr(:call, view, arg2.args...)
172+
ex.args[end] = Expr(:call, view, arg2.args...)
137173
else
138-
error("invalid expression")
174+
error("unsupported replace_ref_begin_end result")
139175
end
140176
return esc(ex)
141177
end
@@ -176,10 +212,7 @@ function _views(ex::Expr)
176212

177213
# temp vars to avoid recomputing a and i,
178214
# which will be assigned in a let block:
179-
a = gensym(:a)
180-
i = let lhs=lhs # #15276
181-
[gensym(:i) for k = 1:length(lhs.args)-1]
182-
end
215+
i = Symbol[Symbol(:i, k) for k = 1:length(lhs.args)-1]
183216

184217
# for splatted indices like a[i, j...], we need to
185218
# splat the corresponding temp var.
@@ -194,14 +227,15 @@ function _views(ex::Expr)
194227
end
195228
end
196229

197-
Expr(:let,
198-
Expr(:block,
199-
:($a = $(_views(lhs.args[1]))),
200-
Any[:($(i[k]) = $(_views(lhs.args[k+1]))) for k=1:length(i)]...),
201-
Expr(first(h) == '.' ? :(.=) : :(=), :($a[$(I...)]),
202-
Expr(:call, Symbol(h[1:end-1]),
203-
:($maybeview($a, $(I...))),
204-
mapany(_views, ex.args[2:end])...)))
230+
Expr(:var"hygienic-scope", # assign a and i to the macro's scope
231+
Expr(:let,
232+
Expr(:block,
233+
:(a = $(esc(_views(lhs.args[1])))),
234+
Any[:($(i[k]) = $(esc(_views(lhs.args[k+1])))) for k=1:length(i)]...),
235+
Expr(first(h) == '.' ? :(.=) : :(=), :(a[$(I...)]),
236+
Expr(:call, esc(Symbol(h[1:end-1])),
237+
:($maybeview(a, $(I...))),
238+
mapany(e -> esc(_views(e)), ex.args[2:end])...))), Base)
205239
else
206240
exprarray(ex.head, mapany(_views, ex.args))
207241
end

doc/src/devdocs/ast.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,9 @@ These expressions are represented as `LineNumberNode`s in Julia.
236236
### Macros
237237

238238
Macro hygiene is represented through the expression head pair `escape` and `hygienic-scope`.
239-
The result of a macro expansion is automatically wrapped in `(hygienic-scope block module)`,
239+
The result of a macro expansion is automatically wrapped in `(hygienic-scope block module [lno])`,
240240
to represent the result of the new scope. The user can insert `(escape block)` inside
241-
to interpolate code from the caller.
241+
to interpolate code from the caller. The lno is the `__source__` argument of the macro, if included.
242242

243243

244244
## Lowered form

0 commit comments

Comments
 (0)