Skip to content

Commit c9d6087

Browse files
committed
macroexpand: preserve hygiene markers during early expansion
This is to preserve the line number. But we make slightly more changes than strictly necessary to prepare for future improvements in this area.
1 parent 6d70d2a commit c9d6087

File tree

17 files changed

+214
-102
lines changed

17 files changed

+214
-102
lines changed

base/boot.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,11 +533,10 @@ import Core: CodeInfo, MethodInstance, CodeInstance, GotoNode, GotoIfNot, Return
533533
end # module IR
534534

535535
# docsystem basics
536-
const unescape = Symbol("hygienic-scope")
537536
macro doc(x...)
538537
docex = atdoc(__source__, __module__, x...)
539538
isa(docex, Expr) && docex.head === :escape && return docex
540-
return Expr(:escape, Expr(unescape, docex, typeof(atdoc).name.module))
539+
return Expr(:escape, Expr(:var"hygienic-scope", docex, typeof(atdoc).name.module, __source__))
541540
end
542541
macro __doc__(x)
543542
return Expr(:escape, Expr(:block, Expr(:meta, :doc), x))

base/docs/Docs.jl

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -286,12 +286,26 @@ catdoc(xs...) = vcat(xs...)
286286
const keywords = Dict{Symbol, DocStr}()
287287

288288
function unblock(@nospecialize ex)
289+
while isexpr(ex, :var"hygienic-scope")
290+
isexpr(ex.args[1], :escape) || break
291+
ex = ex.args[1].args[1]
292+
end
289293
isexpr(ex, :block) || return ex
290294
exs = filter(ex -> !(isa(ex, LineNumberNode) || isexpr(ex, :line)), ex.args)
291295
length(exs) == 1 || return ex
292296
return unblock(exs[1])
293297
end
294298

299+
# peek through ex to figure out what kind of expression it may eventually act like
300+
# but ignoring scopes and line numbers
301+
function unescape(@nospecialize ex)
302+
ex = unblock(ex)
303+
while isexpr(ex, :escape) || isexpr(ex, :var"hygienic-scope")
304+
ex = unblock(ex.args[1])
305+
end
306+
return ex
307+
end
308+
295309
uncurly(@nospecialize ex) = isexpr(ex, :curly) ? ex.args[1] : ex
296310

297311
namify(@nospecialize x) = astname(x, isexpr(x, :macro))::Union{Symbol,Expr,GlobalRef}
@@ -351,18 +365,19 @@ function metadata(__source__, __module__, expr, ismodule)
351365
fields = P[]
352366
last_docstr = nothing
353367
for each in (expr.args[3]::Expr).args
354-
if isa(each, Symbol) || isexpr(each, :(::))
368+
eachex = unescape(each)
369+
if isa(eachex, Symbol) || isexpr(eachex, :(::))
355370
# a field declaration
356371
if last_docstr !== nothing
357-
push!(fields, P(namify(each::Union{Symbol,Expr}), last_docstr))
372+
push!(fields, P(namify(eachex::Union{Symbol,Expr}), last_docstr))
358373
last_docstr = nothing
359374
end
360-
elseif isexpr(each, :function) || isexpr(each, :(=))
375+
elseif isexpr(eachex, :function) || isexpr(eachex, :(=))
361376
break
362-
elseif isa(each, String) || isexpr(each, :string) || isexpr(each, :call) ||
363-
(isexpr(each, :macrocall) && each.args[1] === Symbol("@doc_str"))
377+
elseif isa(eachex, String) || isexpr(eachex, :string) || isexpr(eachex, :call) ||
378+
(isexpr(eachex, :macrocall) && eachex.args[1] === Symbol("@doc_str"))
364379
# forms that might be doc strings
365-
last_docstr = each::Union{String,Expr}
380+
last_docstr = each
366381
end
367382
end
368383
dict = :($(Dict{Symbol,Any})($([(:($(P)($(quot(f)), $d)))::Expr for (f, d) in fields]...)))
@@ -627,8 +642,9 @@ function loaddocs(docs::Vector{Core.SimpleVector})
627642
for (mod, ex, str, file, line) in docs
628643
data = Dict{Symbol,Any}(:path => string(file), :linenumber => line)
629644
doc = docstr(str, data)
630-
docstring = docm(LineNumberNode(line, file), mod, doc, ex, false) # expand the real @doc macro now
631-
Core.eval(mod, Expr(Core.unescape, docstring, Docs))
645+
lno = LineNumberNode(line, file)
646+
docstring = docm(lno, mod, doc, ex, false) # expand the real @doc macro now
647+
Core.eval(mod, Expr(:var"hygienic-scope", docstring, Docs, lno))
632648
end
633649
empty!(docs)
634650
nothing

base/osutils.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ macro static(ex)
1616
@label loop
1717
hd = ex.head
1818
if hd (:if, :elseif, :&&, :||)
19-
cond = Core.eval(__module__, ex.args[1])
19+
cond = Core.eval(__module__, ex.args[1])::Bool
2020
if xor(cond, hd === :||)
2121
return esc(ex.args[2])
2222
elseif length(ex.args) == 3

base/threadcall.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ macro threadcall(f, rettype, argtypes, argvals...)
4747
push!(body, :(return Int(Core.sizeof($rettype))))
4848

4949
# return code to generate wrapper function and send work request thread queue
50-
wrapper = Expr(Symbol("hygienic-scope"), wrapper, @__MODULE__)
50+
wrapper = Expr(:var"hygienic-scope", wrapper, @__MODULE__, __source__)
5151
return :(let fun_ptr = @cfunction($wrapper, Int, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}))
5252
# use cglobal to look up the function on the calling thread
5353
do_threadcall(fun_ptr, cglobal($f), $rettype, Any[$(argtypes...)], Any[$(argvals...)])

base/util.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ macro kwdef(expr)
604604
kwdefs = nothing
605605
end
606606
return quote
607-
Base.@__doc__ $(esc(expr))
607+
$(esc(:($Base.@__doc__ $expr)))
608608
$kwdefs
609609
end
610610
end

doc/make.jl

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -265,12 +265,6 @@ DocMeta.setdocmeta!(
265265
maybe_revise(:(using Base.BinaryPlatforms));
266266
recursive=true, warn=false,
267267
)
268-
DocMeta.setdocmeta!(
269-
Pkg.LazilyInitializedFields,
270-
:DocTestSetup,
271-
maybe_revise(:(using Pkg.LazilyInitializedFields));
272-
recursive=true, warn=false,
273-
)
274268

275269
let r = r"buildroot=(.+)", i = findfirst(x -> occursin(r, x), ARGS)
276270
global const buildroot = i === nothing ? (@__DIR__) : first(match(r, ARGS[i]).captures)

doc/src/manual/metaprogramming.md

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ Now, consider a slightly more complex macro:
517517

518518
```jldoctest sayhello2
519519
julia> macro sayhello(name)
520-
return :( println("Hello, ", $name) )
520+
return esc(:( println("Hello, ", $name) ))
521521
end
522522
@sayhello (macro with 1 method)
523523
```
@@ -768,15 +768,20 @@ of expressions inside the macro body.
768768
### Hygiene
769769

770770
An issue that arises in more complex macros is that of [hygiene](https://en.wikipedia.org/wiki/Hygienic_macro).
771-
In short, macros must ensure that the variables they introduce in their returned expressions do
772-
not accidentally clash with existing variables in the surrounding code they expand into. Conversely,
773-
the expressions that are passed into a macro as arguments are often *expected* to evaluate in
774-
the context of the surrounding code, interacting with and modifying the existing variables. Another
775-
concern arises from the fact that a macro may be called in a different module from where it was
776-
defined. In this case we need to ensure that all global variables are resolved to the correct
777-
module. Julia already has a major advantage over languages with textual macro expansion (like
778-
C) in that it only needs to consider the returned expression. All the other variables (such as
779-
`msg` in `@assert` above) follow the [normal scoping block behavior](@ref scope-of-variables).
771+
We saw this a bit earlier with the calls to `esc` that wrapped the return value of the macros so far.
772+
773+
This already gives Julia a major advantage over languages with textual macro expansion (like
774+
C) in that it only needs to consider the returned expression, and it cannot change the
775+
parsing of the expression surrounding it. Thus, all the other variables (such as `msg` in
776+
`@assert` above) follow the [normal scoping block behavior](@ref scope-of-variables).
777+
778+
Hygiene markers exists because macros must ensure that the variables they introduce in their
779+
returned expressions do not accidentally clash with existing variables in the surrounding
780+
code they expand into. Conversely, the expressions that are passed into a macro as arguments
781+
are often *expected* to evaluate in the context of the surrounding code, interacting with
782+
and modifying the existing variables. Another concern arises from the fact that a macro may
783+
be called in a different module from where it was defined. In this case we need to ensure
784+
that all global variables are resolved to the correct module.
780785

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

src/ast.c

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,8 @@ static jl_value_t *scm_to_julia(fl_context_t *fl_ctx, value_t e, jl_module_t *mo
434434
}
435435
JL_CATCH {
436436
// if expression cannot be converted, replace with error expr
437+
//jl_(jl_current_exception());
438+
//jlbacktrace();
437439
jl_expr_t *ex = jl_exprn(jl_error_sym, 1);
438440
v = (jl_value_t*)ex;
439441
jl_array_ptr_set(ex->args, 0, jl_cstr_to_string("invalid AST"));
@@ -998,7 +1000,59 @@ int jl_has_meta(jl_array_t *body, jl_sym_t *sym) JL_NOTSAFEPOINT
9981000
return 0;
9991001
}
10001002

1001-
static jl_value_t *jl_invoke_julia_macro(jl_array_t *args, jl_module_t *inmodule, jl_module_t **ctx, size_t world, int throw_load_error)
1003+
// Utility function to return whether `e` is any of the special AST types or
1004+
// will always evaluate to itself exactly unchanged. This corresponds to
1005+
// `is_self_quoting` in Core.Compiler utilities.
1006+
int jl_is_ast_node(jl_value_t *e) JL_NOTSAFEPOINT
1007+
{
1008+
return jl_is_newvarnode(e)
1009+
|| jl_is_code_info(e)
1010+
|| jl_is_linenode(e)
1011+
|| jl_is_gotonode(e)
1012+
|| jl_is_gotoifnot(e)
1013+
|| jl_is_returnnode(e)
1014+
|| jl_is_ssavalue(e)
1015+
|| jl_is_slotnumber(e)
1016+
|| jl_is_argument(e)
1017+
|| jl_is_quotenode(e)
1018+
|| jl_is_globalref(e)
1019+
|| jl_is_symbol(e)
1020+
|| jl_is_pinode(e)
1021+
|| jl_is_phinode(e)
1022+
|| jl_is_phicnode(e)
1023+
|| jl_is_upsilonnode(e)
1024+
|| jl_is_expr(e);
1025+
}
1026+
1027+
static int is_self_quoting_expr(jl_expr_t *e)
1028+
{
1029+
return (e->head == jl_inert_sym ||
1030+
e->head == jl_core_sym ||
1031+
e->head == jl_line_sym ||
1032+
e->head == jl_lineinfo_sym ||
1033+
e->head == jl_meta_sym ||
1034+
e->head == jl_boundscheck_sym ||
1035+
e->head == jl_inline_sym ||
1036+
e->head == jl_noinline_sym);
1037+
}
1038+
1039+
// any AST, except those that cannot contain symbols
1040+
// and have no side effects
1041+
int need_esc_node(jl_value_t *e) JL_NOTSAFEPOINT
1042+
{
1043+
if (jl_is_linenode(e)
1044+
|| jl_is_ssavalue(e)
1045+
|| jl_is_slotnumber(e)
1046+
|| jl_is_argument(e)
1047+
|| jl_is_quotenode(e))
1048+
return 0;
1049+
if (jl_is_expr(e))
1050+
return !is_self_quoting_expr((jl_expr_t*)e);
1051+
// note: jl_is_globalref(e) is not included here, since we care a little about about having a line number for it
1052+
return jl_is_ast_node(e);
1053+
}
1054+
1055+
static jl_value_t *jl_invoke_julia_macro(jl_array_t *args, jl_module_t *inmodule, jl_module_t **ctx, jl_value_t **lineinfo, size_t world, int throw_load_error)
10021056
{
10031057
jl_task_t *ct = jl_current_task;
10041058
JL_TIMING(MACRO_INVOCATION, MACRO_INVOCATION);
@@ -1010,10 +1064,9 @@ static jl_value_t *jl_invoke_julia_macro(jl_array_t *args, jl_module_t *inmodule
10101064
margs[0] = jl_array_ptr_ref(args, 0);
10111065
// __source__ argument
10121066
jl_value_t *lno = jl_array_ptr_ref(args, 1);
1067+
if (!jl_is_linenode(lno))
1068+
lno = jl_new_struct(jl_linenumbernode_type, jl_box_long(0), jl_nothing);
10131069
margs[1] = lno;
1014-
if (!jl_typetagis(lno, jl_linenumbernode_type)) {
1015-
margs[1] = jl_new_struct(jl_linenumbernode_type, jl_box_long(0), jl_nothing);
1016-
}
10171070
margs[2] = (jl_value_t*)inmodule;
10181071
for (i = 3; i < nargs; i++)
10191072
margs[i] = jl_array_ptr_ref(args, i - 1);
@@ -1052,6 +1105,7 @@ static jl_value_t *jl_invoke_julia_macro(jl_array_t *args, jl_module_t *inmodule
10521105
}
10531106
}
10541107
ct->world_age = last_age;
1108+
*lineinfo = margs[1];
10551109
JL_GC_POP();
10561110
return result;
10571111
}
@@ -1074,36 +1128,47 @@ static jl_value_t *jl_expand_macros(jl_value_t *expr, jl_module_t *inmodule, str
10741128
JL_GC_POP();
10751129
return expr;
10761130
}
1077-
if (e->head == jl_hygienicscope_sym && jl_expr_nargs(e) == 2) {
1131+
if (e->head == jl_hygienicscope_sym && jl_expr_nargs(e) >= 2) {
10781132
struct macroctx_stack newctx;
10791133
newctx.m = (jl_module_t*)jl_exprarg(e, 1);
10801134
JL_TYPECHK(hygienic-scope, module, (jl_value_t*)newctx.m);
10811135
newctx.parent = macroctx;
10821136
jl_value_t *a = jl_exprarg(e, 0);
10831137
jl_value_t *a2 = jl_expand_macros(a, inmodule, &newctx, onelevel, world, throw_load_error);
1084-
if (a != a2)
1138+
if (jl_is_expr(a2) && ((jl_expr_t*)a2)->head == jl_escape_sym && !need_esc_node(jl_exprarg(a2, 0)))
1139+
expr = jl_exprarg(a2, 0);
1140+
else if (!need_esc_node(a2))
1141+
expr = a2;
1142+
else if (a != a2)
10851143
jl_array_ptr_set(e->args, 0, a2);
10861144
return expr;
10871145
}
10881146
if (e->head == jl_macrocall_sym) {
10891147
struct macroctx_stack newctx;
10901148
newctx.m = macroctx ? macroctx->m : inmodule;
10911149
newctx.parent = macroctx;
1092-
jl_value_t *result = jl_invoke_julia_macro(e->args, inmodule, &newctx.m, world, throw_load_error);
1150+
jl_value_t *lineinfo = NULL;
1151+
jl_value_t *result = jl_invoke_julia_macro(e->args, inmodule, &newctx.m, &lineinfo, world, throw_load_error);
1152+
if (!need_esc_node(result))
1153+
return result;
10931154
jl_value_t *wrap = NULL;
1094-
JL_GC_PUSH3(&result, &wrap, &newctx.m);
1155+
JL_GC_PUSH4(&result, &wrap, &newctx.m, &lineinfo);
10951156
// copy and wrap the result in `(hygienic-scope ,result ,newctx)
10961157
if (jl_is_expr(result) && ((jl_expr_t*)result)->head == jl_escape_sym)
10971158
result = jl_exprarg(result, 0);
10981159
else
1099-
wrap = (jl_value_t*)jl_exprn(jl_hygienicscope_sym, 2);
1160+
wrap = (jl_value_t*)jl_exprn(jl_hygienicscope_sym, 3);
11001161
result = jl_copy_ast(result);
11011162
if (!onelevel)
11021163
result = jl_expand_macros(result, inmodule, wrap ? &newctx : macroctx, onelevel, world, throw_load_error);
1103-
if (wrap) {
1164+
if (wrap && need_esc_node(result)) {
11041165
jl_exprargset(wrap, 0, result);
11051166
jl_exprargset(wrap, 1, newctx.m);
1106-
result = wrap;
1167+
jl_exprargset(wrap, 2, lineinfo);
1168+
if (jl_is_expr(result) && ((jl_expr_t*)result)->head == jl_escape_sym)
1169+
result = jl_exprarg(result, 0);
1170+
else
1171+
result = wrap;
11071172
}
11081173
JL_GC_POP();
11091174
return result;

src/jlfrontend.scm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@
175175

176176
(define (jl-expand-macroscope expr)
177177
(error-wrap (lambda ()
178-
(julia-expand-macroscope expr))))
178+
(julia-expand-macroscope expr #t))))
179179

180180
;; construct default definitions of `eval` for non-bare modules
181181
;; called by jl_eval_module_expr

src/macroexpand.scm

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -446,10 +446,12 @@
446446
binds))
447447
,body)))
448448
((hygienic-scope) ; TODO: move this lowering to resolve-scopes, instead of reimplementing it here badly
449-
(let ((parent-scope (cons (list env m) parent-scope))
450-
(body (cadr e))
451-
(m (caddr e)))
452-
(resolve-expansion-vars-with-new-env body env m parent-scope inarg #t)))
449+
(let* ((parent-scope (cons (list env m) parent-scope))
450+
(body (cadr e))
451+
(m (caddr e))
452+
(lno (cdddr e))
453+
(body (resolve-expansion-vars-with-new-env body env m parent-scope inarg #t)))
454+
(if *keepmarkers* `(hygienic-scope (escape ,body) ,m ,@lno) body)))
453455
((tuple)
454456
(cons (car e)
455457
(map (lambda (x)
@@ -573,9 +575,11 @@
573575
((eq? (car e) 'inert) e)
574576
((eq? (car e) 'module) e)
575577
((eq? (car e) 'hygienic-scope)
576-
(let ((form (cadr e)) ;; form is the expression returned from expand-macros
577-
(modu (caddr e))) ;; m is the macro's def module
578-
(resolve-expansion-vars form modu)))
578+
(let* ((form (cadr e)) ;; form is the expression returned from expand-macros
579+
(modu (caddr e)) ;; m is the macro's def module
580+
(lno (cdddr e)) ;; lno is (optionally) the line number node
581+
(form (resolve-expansion-vars form modu)))
582+
(if *keepmarkers* `(hygienic-scope (escape ,form) ,modu ,@lno) form)))
579583
(else
580584
(map julia-expand-macroscopes- e))))
581585

@@ -585,8 +589,9 @@
585589
((eq? (car e) 'hygienic-scope)
586590
(let ((parent-scope (list relabels parent-scope))
587591
(body (cadr e))
588-
(m (caddr e)))
589-
`(hygienic-scope ,(rename-symbolic-labels- (cadr e) (table) parent-scope) ,m)))
592+
(m (caddr e))
593+
(lno (cdddr e)))
594+
`(hygienic-scope ,(rename-symbolic-labels- (cadr e) (table) parent-scope) ,m ,@lno)))
590595
((and (eq? (car e) 'escape) (not (null? parent-scope)))
591596
`(escape ,(apply rename-symbolic-labels- (cadr e) parent-scope)))
592597
((or (eq? (car e) 'symbolicgoto) (eq? (car e) 'symboliclabel))
@@ -603,17 +608,39 @@
603608
(define (rename-symbolic-labels e)
604609
(rename-symbolic-labels- e (table) '()))
605610

611+
; find the first line number in this expression, before we eliminate them
612+
(define (first-lineno blk)
613+
(cond ((not (pair? blk)) #f)
614+
((eq? (car blk) 'line) blk)
615+
((and (eq? (car blk) 'hygienic-scope) (pair? (cdddr blk)) (pair? (cadddr blk)) (eq? (car (cadddr blk)) 'line))
616+
(cadddr blk))
617+
((memq (car blk) '(toplevel block escape hygienic-scope))
618+
(let loop ((xs (cdr blk)))
619+
(and (pair? xs)
620+
(let ((elt (first-lineno (car xs))))
621+
(or elt (loop (cdr xs)))))))
622+
(else #f)))
623+
606624
;; macro expander entry point
607625

608626
;; TODO: delete this file and fold this operation into resolve-scopes
609-
(define (julia-expand-macroscope e)
610-
(julia-expand-macroscopes-
611-
(rename-symbolic-labels
612-
(julia-expand-quotes e))))
627+
(define (julia-expand-macroscope e (keepmarkers #f))
628+
; if we are expanding macro macroscopes too early, keep the hygiene markers around for later
629+
; they have line numbers attached to them, and we might need those again
630+
; if we are macro expanding as normal, then move the first line number we
631+
; find to the head position of the expression where we will not easily lose it later
632+
(set! *keepmarkers* keepmarkers)
633+
(let ((lno (and (not keepmarkers) (first-lineno e)))
634+
(form (julia-expand-macroscopes-
635+
(rename-symbolic-labels
636+
(julia-expand-quotes e)))))
637+
(if lno `(block ,lno ,form) form)))
613638

614639
(define (contains-macrocall e)
615640
(and (pair? e)
616641
(contains (lambda (e) (and (pair? e) (eq? (car e) 'macrocall))) e)))
617642

618643
(define (julia-bq-macro x)
619644
(julia-bq-expand x 0))
645+
646+
(define *keepmarkers* #f)

0 commit comments

Comments
 (0)