Skip to content

Commit 51a2002

Browse files
committed
Add stacktrace capture to MacroExpansionError
`MacroExpansionError` has good information about error location, but does not preserve information about the error raised by user macros beyond the error message itself. Such information is very useful for tooling like language servers, and stacktraces are particularly important. This commit adds a `stacktrace::Vector{Base.StackTraces.StackFrame}` field to `MacroExpansionError`. New macro definitions still call the `MacroExpansionError(ex::SyntaxTree, msg::AbstractString; position=:all)` constructor, which internally calls `stacktrace(...)`, so the user-facing interface remains unchanged. Additionally, `scrub_expand_macro_stacktrace` is implemented to automatically trim information about JL internal functions that are not useful to users.
1 parent 65d6523 commit 51a2002

File tree

5 files changed

+57
-19
lines changed

5 files changed

+57
-19
lines changed

Project.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ version = "1.0.0-DEV"
66
[deps]
77
JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4"
88

9+
[sources]
10+
JuliaSyntax = {rev = "e02f29f", url = "https://github.com/JuliaLang/JuliaSyntax.jl"}
11+
912
[compat]
1013
julia = "1"
1114

src/macro_expansion.jl

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,21 @@ struct MacroExpansionError
7979
ex::SyntaxTree
8080
msg::String
8181
position::Symbol
82+
stacktrace::Vector{Base.StackTraces.StackFrame}
8283
end
8384

8485
"""
8586
`position` - the source position relative to the node - may be `:begin` or `:end` or `:all`
8687
"""
8788
function MacroExpansionError(ex::SyntaxTree, msg::AbstractString; position=:all)
88-
MacroExpansionError(nothing, ex, msg, position)
89+
MacroExpansionError(nothing, ex, msg, position, scrub_expand_macro_stacktrace(stacktrace(backtrace())))
90+
end
91+
92+
function scrub_expand_macro_stacktrace(stacktrace::Vector{Base.StackTraces.StackFrame})
93+
idx = @something findfirst(stacktrace) do stackframe::Base.StackTraces.StackFrame
94+
stackframe.func === :expand_macro && stackframe.file === Symbol(@__FILE__)
95+
end error("`scrub_expand_macro_stacktrace` is expected to be called from `expand_macro`")
96+
return stacktrace[1:idx-1]
8997
end
9098

9199
function Base.showerror(io::IO, exc::MacroExpansionError)
@@ -113,7 +121,7 @@ function Base.showerror(io::IO, exc::MacroExpansionError)
113121
highlight(io, src.file, byterange, note=exc.msg)
114122
end
115123

116-
function eval_macro_name(ctx, ex)
124+
function eval_macro_name(ctx::MacroExpansionContext, ex::SyntaxTree)
117125
# `ex1` might contain a nontrivial mix of scope layers so we can't just
118126
# `eval()` it, as it's already been partially lowered by this point.
119127
# Instead, we repeat the latter parts of `lower()` here.
@@ -127,7 +135,7 @@ function eval_macro_name(ctx, ex)
127135
eval(mod, expr_form)
128136
end
129137

130-
function expand_macro(ctx, ex)
138+
function expand_macro(ctx::MacroExpansionContext, ex::SyntaxTree)
131139
@assert kind(ex) == K"macrocall"
132140

133141
macname = ex[1]
@@ -151,9 +159,9 @@ function expand_macro(ctx, ex)
151159
if exc isa MacroExpansionError
152160
# Add context to the error.
153161
# TODO: Using rethrow() is kinda ugh. Is there a way to avoid it?
154-
rethrow(MacroExpansionError(mctx, exc.ex, exc.msg, exc.position))
162+
rethrow(MacroExpansionError(mctx, exc.ex, exc.msg, exc.position, exc.stacktrace))
155163
else
156-
throw(MacroExpansionError(mctx, ex, "Error expanding macro", :all))
164+
throw(MacroExpansionError(mctx, ex, "Error expanding macro", :all, scrub_expand_macro_stacktrace(stacktrace(catch_backtrace()))))
157165
end
158166
end
159167

@@ -237,7 +245,7 @@ function expand_forms_1(ctx::MacroExpansionContext, ex::SyntaxTree)
237245
@chk numchildren(ex) == 1
238246
# TODO: Upstream should set a general flag for detecting parenthesized
239247
# expressions so we don't need to dig into `green_tree` here. Ugh!
240-
plain_symbol = has_flags(ex, JuliaSyntax.COLON_QUOTE) &&
248+
plain_symbol = has_flags(ex, JuliaSyntax.COLON_QUOTE) &&
241249
kind(ex[1]) == K"Identifier" &&
242250
(sr = sourceref(ex); sr isa SourceRef && kind(sr.green_tree[2]) != K"parens")
243251
if plain_symbol
@@ -337,4 +345,3 @@ function expand_forms_1(mod::Module, ex::SyntaxTree)
337345
ctx.current_layer)
338346
return ctx2, reparent(ctx2, ex2)
339347
end
340-

test/ccall_demo.jl

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ function ccall_macro_lower(ex, convention, func, rettype, types, args, num_varar
105105
push!(roots, argi)
106106
push!(cargs, ast":(Base.unsafe_convert($type, $argi))")
107107
end
108-
push!(statements,
108+
push!(statements,
109109
@ast ex ex [K"foreigncall"
110110
func
111111
rettype
@@ -126,5 +126,4 @@ function var"@ccall"(ctx::JuliaLowering.MacroContext, ex)
126126
ccall_macro_lower(ex, "ccall", ccall_macro_parse(ex)...)
127127
end
128128

129-
end
130-
129+
end # module CCall

test/macros.jl

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
@testset "macros" begin
1+
module macros
22

3-
test_mod = Module()
3+
using JuliaLowering, Test
4+
5+
module test_mod end
46

57
JuliaLowering.include_string(test_mod, """
68
module M
@@ -75,7 +77,7 @@ end
7577
""")
7678

7779
@test JuliaLowering.include_string(test_mod, """
78-
let
80+
let
7981
x = "`x` from outer scope"
8082
M.@foo x
8183
end
@@ -89,7 +91,7 @@ end
8991

9092
@test !isdefined(test_mod.M, :a_global)
9193
@test JuliaLowering.include_string(test_mod, """
92-
begin
94+
begin
9395
M.@set_a_global 42
9496
M.a_global
9597
end
@@ -133,13 +135,42 @@ M.@recursive 3
133135
""") == (3, (2, (1, 0)))
134136

135137
@test let
136-
ex = parsestmt(SyntaxTree, "M.@outer()", filename="foo.jl")
138+
ex = JuliaLowering.parsestmt(JuliaLowering.SyntaxTree, "M.@outer()", filename="foo.jl")
137139
expanded = JuliaLowering.macroexpand(test_mod, ex)
138-
sourcetext.(flattened_provenance(expanded[2]))
140+
JuliaLowering.sourcetext.(JuliaLowering.flattened_provenance(expanded[2]))
139141
end == [
140142
"M.@outer()"
141143
"@inner"
142144
"2"
143145
]
144146

147+
JuliaLowering.include_string(test_mod, """
148+
f_throw(x) = throw(x)
149+
macro m_throw(x)
150+
:(\$(f_throw(x)))
151+
end
152+
""")
153+
let ret = try
154+
JuliaLowering.include_string(test_mod, "_never_exist = @m_throw 42")
155+
catch err
156+
err
157+
end
158+
@test ret isa JuliaLowering.MacroExpansionError
159+
@test length(ret.stacktrace) == 2
160+
@test ret.stacktrace[1].func === :f_throw
161+
@test ret.stacktrace[2].func === Symbol("@m_throw")
145162
end
163+
164+
include("ccall_demo.jl")
165+
@test JuliaLowering.include_string(CCall, "@ccall strlen(\"foo\"::Cstring)::Csize_t") == 3
166+
let ret = try
167+
JuliaLowering.include_string(CCall, "@ccall strlen(\"foo\"::Cstring)")
168+
catch e
169+
e
170+
end
171+
@test ret isa JuliaLowering.MacroExpansionError
172+
@test ret.msg == "Expected a return type annotation like `::T`"
173+
@test any(sf->sf.func===:ccall_macro_parse, ret.stacktrace)
174+
end
175+
176+
end # module macros

test/runtests.jl

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ using Test
33
include("utils.jl")
44

55
@testset "JuliaLowering.jl" begin
6-
76
include("syntax_graph.jl")
87

98
include("ir_tests.jl")
@@ -20,11 +19,10 @@ include("utils.jl")
2019
include("generators.jl")
2120
include("import.jl")
2221
include("loops.jl")
23-
include("macros.jl")
22+
@testset "macros" include("macros.jl")
2423
include("misc.jl")
2524
include("modules.jl")
2625
include("quoting.jl")
2726
include("scopes.jl")
2827
include("typedefs.jl")
29-
3028
end

0 commit comments

Comments
 (0)