Skip to content

Commit 219d375

Browse files
authored
Merge pull request #29 from JuliaLang/cjf/core-parsing-fixes
Emit `LineNumberNode`s in `Expr` conversion
2 parents 734ac30 + 0976ded commit 219d375

File tree

12 files changed

+257
-97
lines changed

12 files changed

+257
-97
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ tree", but that term has also been used for the parse tree of the full formal
127127
grammar for a language including any grammar hacks required to solve
128128
ambiguities, etc. So we avoid this term.)
129129

130-
`JuliaSyntax` uses use a mostly recursive descent parser which closely
130+
`JuliaSyntax` uses a mostly recursive descent parser which closely
131131
follows the high level structure of the flisp reference parser. This makes the
132132
code familiar and reduces porting bugs. It also gives a lot of flexibility for
133133
designing the diagnostics, tree data structures, compatibility with different

src/expr.jl

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ function is_eventually_call(ex)
66
is_eventually_call(ex.args[1]))
77
end
88

9-
function _to_expr(node::SyntaxNode, iteration_spec=false)
9+
function _to_expr(node::SyntaxNode, iteration_spec=false, need_linenodes=true)
1010
if !haschildren(node)
1111
if node.val isa Union{Int128,UInt128,BigInt}
1212
# Ignore the values of large integers and convert them back to
@@ -27,12 +27,32 @@ function _to_expr(node::SyntaxNode, iteration_spec=false)
2727
headsym = !isnothing(headstr) ? Symbol(headstr) :
2828
error("Can't untokenize head of kind $(kind(node))")
2929
node_args = children(node)
30-
args = Vector{Any}(undef, length(node_args))
30+
insert_linenums = (headsym == :block || headsym == :toplevel) && need_linenodes
31+
args = Vector{Any}(undef, length(node_args)*(insert_linenums ? 2 : 1))
3132
if headsym == :for && length(node_args) == 2
32-
args[1] = _to_expr(node_args[1], true)
33-
args[2] = _to_expr(node_args[2], false)
33+
# No line numbers in for loop iteration spec
34+
args[1] = _to_expr(node_args[1], true, false)
35+
args[2] = _to_expr(node_args[2])
36+
elseif headsym == :let && length(node_args) == 2
37+
# No line numbers in let statement binding list
38+
args[1] = _to_expr(node_args[1], false, false)
39+
args[2] = _to_expr(node_args[2])
3440
else
35-
map!(_to_expr, args, node_args)
41+
if insert_linenums
42+
if isempty(node_args)
43+
push!(args, source_location(LineNumberNode, node.source, node.position))
44+
else
45+
for i in 1:length(node_args)
46+
n = node_args[i]
47+
args[2*i-1] = source_location(LineNumberNode, n.source, n.position)
48+
args[2*i] = _to_expr(n)
49+
end
50+
end
51+
else
52+
for i in 1:length(node_args)
53+
args[i] = _to_expr(node_args[i])
54+
end
55+
end
3656
end
3757
# Julia's standard `Expr` ASTs have children stored in a canonical
3858
# order which is often not always source order. We permute the children
@@ -136,31 +156,41 @@ function _to_expr(node::SyntaxNode, iteration_spec=false)
136156
# Strip string from interpolations in 1.5 and lower to preserve
137157
# "hi$("ho")" ==> (string "hi" "ho")
138158
elseif headsym == :(=)
139-
if is_eventually_call(args[1]) && !iteration_spec
140-
if Meta.isexpr(args[2], :block)
141-
pushfirst!(args[2].args, loc)
142-
else
143-
# Add block for short form function locations
144-
args[2] = Expr(:block, loc, args[2])
145-
end
159+
if is_eventually_call(args[1]) && !iteration_spec && !Meta.isexpr(args[2], :block)
160+
# Add block for short form function locations
161+
args[2] = Expr(:block, loc, args[2])
146162
end
163+
elseif headsym == :elseif
164+
# Block for conditional's source location
165+
args[1] = Expr(:block, loc, args[1])
147166
elseif headsym == :(->)
148167
if Meta.isexpr(args[2], :block)
149-
pushfirst!(args[2].args, loc)
168+
if node.parent isa SyntaxNode && kind(node.parent) != K"do"
169+
pushfirst!(args[2].args, loc)
170+
end
150171
else
151172
# Add block for source locations
152173
args[2] = Expr(:block, loc, args[2])
153174
end
154175
elseif headsym == :function
155-
if length(args) > 1 && Meta.isexpr(args[1], :tuple)
156-
# Convert to weird Expr forms for long-form anonymous functions.
157-
#
158-
# (function (tuple (... xs)) body) ==> (function (... xs) body)
159-
if length(args[1].args) == 1 && Meta.isexpr(args[1].args[1], :...)
160-
# function (xs...) \n body end
161-
args[1] = args[1].args[1]
176+
if length(args) > 1
177+
if Meta.isexpr(args[1], :tuple)
178+
# Convert to weird Expr forms for long-form anonymous functions.
179+
#
180+
# (function (tuple (... xs)) body) ==> (function (... xs) body)
181+
if length(args[1].args) == 1 && Meta.isexpr(args[1].args[1], :...)
182+
# function (xs...) \n body end
183+
args[1] = args[1].args[1]
184+
end
162185
end
186+
pushfirst!(args[2].args, loc)
187+
end
188+
elseif headsym == :macro
189+
if length(args) > 1
190+
pushfirst!(args[2].args, loc)
163191
end
192+
elseif headsym == :module
193+
pushfirst!(args[3].args, loc)
164194
end
165195
if headsym == :inert || (headsym == :quote && length(args) == 1 &&
166196
!(a1 = only(args); a1 isa Expr || a1 isa QuoteNode ||

src/hooks.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ function core_parser_hook(code, filename, lineno, offset, options)
3535
end
3636

3737
if any_error(stream)
38-
e = Expr(:error, ParseError(SourceFile(code), stream.diagnostics))
38+
e = Expr(:error, ParseError(SourceFile(code, filename=filename), stream.diagnostics))
3939
ex = options === :all ? Expr(:toplevel, e) : e
4040
else
41-
ex = build_tree(Expr, stream, wrap_toplevel_as_kind=K"None")
41+
ex = build_tree(Expr, stream, filename=filename, wrap_toplevel_as_kind=K"None")
4242
if Meta.isexpr(ex, :None)
4343
# The None wrapping is only to give somewhere for trivia to be
4444
# attached; unwrap!

src/parser.jl

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1852,7 +1852,7 @@ end
18521852

18531853
# Parse if-elseif-else-end expressions
18541854
#
1855-
# if a xx elseif b yy else zz end ==> (if a (block xx) (elseif (block b) (block yy) (block zz)))
1855+
# if a xx elseif b yy else zz end ==> (if a (block xx) (elseif b (block yy) (block zz)))
18561856
function parse_if_elseif(ps, is_elseif=false, is_elseif_whitespace_err=false)
18571857
mark = position(ps)
18581858
word = peek(ps)
@@ -1872,23 +1872,19 @@ function parse_if_elseif(ps, is_elseif=false, is_elseif_whitespace_err=false)
18721872
# if a xx end ==> (if a (block xx))
18731873
parse_cond(ps)
18741874
end
1875-
if is_elseif
1876-
# Wart: `elseif` condition is in a block but not `if` condition
1877-
emit(ps, cond_mark, K"block")
1878-
end
18791875
# if a \n\n xx \n\n end ==> (if a (block xx))
18801876
parse_block(ps)
18811877
bump_trivia(ps)
18821878
k = peek(ps)
18831879
if k == K"elseif"
1884-
# if a xx elseif b yy end ==> (if a (block xx) (elseif (block b) (block yy)))
1880+
# if a xx elseif b yy end ==> (if a (block xx) (elseif b (block yy)))
18851881
parse_if_elseif(ps, true)
18861882
elseif k == K"else"
18871883
emark = position(ps)
18881884
bump(ps, TRIVIA_FLAG)
18891885
if peek(ps) == K"if"
18901886
# Recovery: User wrote `else if` by mistake ?
1891-
# if a xx else if b yy end ==> (if a (block xx) (error-t) (elseif (block b) (block yy)))
1887+
# if a xx else if b yy end ==> (if a (block xx) (error-t) (elseif b (block yy)))
18921888
bump(ps, TRIVIA_FLAG)
18931889
emit(ps, emark, K"error", TRIVIA_FLAG,
18941890
error="use `elseif` instead of `else if`")

src/source_files.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
SourceFile(code [, filename])
2+
SourceFile(code [; filename=nothing])
33
44
A UTF-8 source code string with associated file name and indexing structures.
55
"""

test/expr.jl

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
2+
@testset "Expr conversion" begin
3+
@testset "Quote nodes" begin
4+
@test parseall(Expr, ":(a)", rule=:atom) == QuoteNode(:a)
5+
@test parseall(Expr, ":(:a)", rule=:atom) == Expr(:quote, QuoteNode(:a))
6+
@test parseall(Expr, ":(1+2)", rule=:atom) == Expr(:quote, Expr(:call, :+, 1, 2))
7+
# Compatibility hack for VERSION >= v"1.4"
8+
# https://github.com/JuliaLang/julia/pull/34077
9+
@test parseall(Expr, ":true", rule=:atom) == Expr(:quote, true)
10+
end
11+
12+
@testset "Line numbers" begin
13+
@testset "Blocks" begin
14+
@test parseall(Expr, "begin a\nb\n\nc\nend", rule=:statement) ==
15+
Expr(:block,
16+
LineNumberNode(1),
17+
:a,
18+
LineNumberNode(2),
19+
:b,
20+
LineNumberNode(4),
21+
:c,
22+
)
23+
@test parseall(Expr, "begin end", rule=:statement) ==
24+
Expr(:block,
25+
LineNumberNode(1)
26+
)
27+
28+
@test parseall(Expr, "a\n\nb") ==
29+
Expr(:toplevel,
30+
LineNumberNode(1),
31+
:a,
32+
LineNumberNode(3),
33+
:b,
34+
)
35+
36+
@test parseall(Expr, "module A\n\nbody\nend", rule=:statement) ==
37+
Expr(:module,
38+
true,
39+
:A,
40+
Expr(:block,
41+
LineNumberNode(1),
42+
LineNumberNode(3),
43+
:body,
44+
),
45+
)
46+
end
47+
48+
@testset "Function definition lines" begin
49+
@test parseall(Expr, "function f()\na\n\nb\nend", rule=:statement) ==
50+
Expr(:function,
51+
Expr(:call, :f),
52+
Expr(:block,
53+
LineNumberNode(1),
54+
LineNumberNode(2),
55+
:a,
56+
LineNumberNode(4),
57+
:b,
58+
)
59+
)
60+
@test parseall(Expr, "f() = 1", rule=:statement) ==
61+
Expr(:(=),
62+
Expr(:call, :f),
63+
Expr(:block,
64+
LineNumberNode(1),
65+
1
66+
)
67+
)
68+
69+
# function/macro without methods
70+
@test parseall(Expr, "function f end", rule=:statement) ==
71+
Expr(:function, :f)
72+
@test parseall(Expr, "macro f end", rule=:statement) ==
73+
Expr(:macro, :f)
74+
end
75+
76+
@testset "elseif" begin
77+
@test parseall(Expr, "if a\nb\nelseif c\n d\nend", rule=:statement) ==
78+
Expr(:if,
79+
:a,
80+
Expr(:block,
81+
LineNumberNode(2),
82+
:b),
83+
Expr(:elseif,
84+
Expr(:block,
85+
LineNumberNode(3), # Line number for elseif condition
86+
:c),
87+
Expr(:block,
88+
LineNumberNode(4),
89+
:d),
90+
)
91+
)
92+
end
93+
94+
@testset "No line numbers in for/let bindings" begin
95+
@test parseall(Expr, "for i=is, j=js\nbody\nend", rule=:statement) ==
96+
Expr(:for,
97+
Expr(:block,
98+
Expr(:(=), :i, :is),
99+
Expr(:(=), :j, :js),
100+
),
101+
Expr(:block,
102+
LineNumberNode(2),
103+
:body
104+
)
105+
)
106+
@test parseall(Expr, "let i=is, j=js\nbody\nend", rule=:statement) ==
107+
Expr(:let,
108+
Expr(:block,
109+
Expr(:(=), :i, :is),
110+
Expr(:(=), :j, :js),
111+
),
112+
Expr(:block,
113+
LineNumberNode(2),
114+
:body
115+
)
116+
)
117+
end
118+
end
119+
120+
@testset "Short form function line numbers" begin
121+
# A block is added to hold the line number node
122+
@test parseall(Expr, "f() = xs", rule=:statement) ==
123+
Expr(:(=),
124+
Expr(:call, :f),
125+
Expr(:block,
126+
LineNumberNode(1),
127+
:xs))
128+
# flisp parser quirk: In a for loop the block is not added, despite
129+
# this defining a short-form function.
130+
@test parseall(Expr, "for f() = xs\nend", rule=:statement) ==
131+
Expr(:for,
132+
Expr(:(=), Expr(:call, :f), :xs),
133+
Expr(:block,
134+
LineNumberNode(1)
135+
))
136+
end
137+
138+
@testset "Long form anonymous functions" begin
139+
@test parseall(Expr, "function (xs...)\nbody end", rule=:statement) ==
140+
Expr(:function,
141+
Expr(:..., :xs),
142+
Expr(:block,
143+
LineNumberNode(1),
144+
LineNumberNode(2),
145+
:body))
146+
end
147+
end

test/hooks.jl

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
@testset "Hooks for Core integration" begin
2-
JuliaSyntax.enable_in_core!()
2+
@testset "filename is used" begin
3+
ex = JuliaSyntax.core_parser_hook("@a", "somefile", 0, :statement)[1]
4+
@test Meta.isexpr(ex, :macrocall)
5+
@test ex.args[2] == LineNumberNode(1, "somefile")
6+
end
37

4-
@test Meta.parse("x + 1") == :(x + 1)
5-
@test Meta.parse("x + 1", 1) == (:(x + 1), 6)
8+
@testset "enable_in_core!" begin
9+
JuliaSyntax.enable_in_core!()
610

7-
# Test that parsing statements incrementally works and stops after
8-
# whitespace / comment trivia
9-
@test Meta.parse("x + 1\n(y)\n", 1) == (:(x + 1), 7)
10-
@test Meta.parse("x + 1\n(y)\n", 7) == (:y, 11)
11-
@test Meta.parse(" x#==#", 1) == (:x, 7)
11+
@test Meta.parse("x + 1") == :(x + 1)
12+
@test Meta.parse("x + 1", 1) == (:(x + 1), 6)
1213

13-
# Check that Meta.parse throws the JuliaSyntax.ParseError rather than
14-
# Meta.ParseError when Core integration is enabled.
15-
@test_throws JuliaSyntax.ParseError Meta.parse("[x")
14+
# Test that parsing statements incrementally works and stops after
15+
# whitespace / comment trivia
16+
@test Meta.parse("x + 1\n(y)\n", 1) == (:(x + 1), 7)
17+
@test Meta.parse("x + 1\n(y)\n", 7) == (:y, 11)
18+
@test Meta.parse(" x#==#", 1) == (:x, 7)
1619

17-
JuliaSyntax.enable_in_core!(false)
20+
# Check that Meta.parse throws the JuliaSyntax.ParseError rather than
21+
# Meta.ParseError when Core integration is enabled.
22+
@test_throws JuliaSyntax.ParseError Meta.parse("[x")
23+
24+
JuliaSyntax.enable_in_core!(false)
25+
end
1826
end

test/parser.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,14 +390,14 @@ tests = [
390390
"export (\$f)" => "(export (\$ f))"
391391
],
392392
JuliaSyntax.parse_if_elseif => [
393-
"if a xx elseif b yy else zz end" => "(if a (block xx) (elseif (block b) (block yy) (block zz)))"
393+
"if a xx elseif b yy else zz end" => "(if a (block xx) (elseif b (block yy) (block zz)))"
394394
"if end" => "(if (error) (block))"
395395
"if \n end" => "(if (error) (block))"
396396
"if a end" => "(if a (block))"
397397
"if a xx end" => "(if a (block xx))"
398398
"if a \n\n xx \n\n end" => "(if a (block xx))"
399-
"if a xx elseif b yy end" => "(if a (block xx) (elseif (block b) (block yy)))"
400-
"if a xx else if b yy end" => "(if a (block xx) (error-t) (elseif (block b) (block yy)))"
399+
"if a xx elseif b yy end" => "(if a (block xx) (elseif b (block yy)))"
400+
"if a xx else if b yy end" => "(if a (block xx) (error-t) (elseif b (block yy)))"
401401
"if a xx else yy end" => "(if a (block xx) (block yy))"
402402
],
403403
JuliaSyntax.parse_const_local_global => [

test/parser_api.jl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
@testset "parser API" begin
22
@testset "String and buffer input" begin
33
# String
4-
@test parse(Expr, "x+y\nz") == (Expr(:toplevel, :(x+y), :z), [], 6)
4+
let
5+
ex,diag,pos = parse(Expr, "x+y\nz")
6+
@test JuliaSyntax.remove_linenums!(ex) == Expr(:toplevel, :(x+y), :z)
7+
@test diag == []
8+
@test pos == 6
9+
end
510
@test parse(Expr, "x+y\nz", rule=:statement) == (:(x+y), [], 4)
611
@test parse(Expr, "x+y\nz", rule=:atom) == (:x, [], 2)
712
@test parse(Expr, "x+y\nz", 5, rule=:atom) == (:z, [], 6)
@@ -56,7 +61,7 @@
5661
end
5762

5863
@testset "parseall" begin
59-
@test parseall(Expr, " x ") == Expr(:toplevel, :x)
64+
@test JuliaSyntax.remove_linenums!(parseall(Expr, " x ")) == Expr(:toplevel, :x)
6065
@test parseall(Expr, " x ", rule=:statement) == :x
6166
@test parseall(Expr, " x ", rule=:atom) == :x
6267
# TODO: Fix this situation with trivia here; the brackets are trivia, but

0 commit comments

Comments
 (0)