Skip to content

Commit ff21c47

Browse files
authored
Permit parens in function call signatures (#131)
This permits the extra parentheses in things like function (funcname(some, long, argument, list) where {Type,Params}) body end This syntax "works" in the reference parser and has been seen in the wild so we need to support it for compatibility. (However, the precedence of `where` and `::` is broken when used inside the parens which suggests this is more of a syntactic aberration rather than an intentional feature. Perhaps we can warn in future.)
1 parent 86d7f90 commit ff21c47

File tree

6 files changed

+193
-48
lines changed

6 files changed

+193
-48
lines changed

src/kinds.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,7 @@ const _kind_names =
880880
"tuple"
881881
"ref"
882882
"vect"
883+
"parens"
883884
# Concatenation syntax
884885
"braces"
885886
"bracescat"

src/parse_stream.jl

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,46 @@ function peek_behind(stream::ParseStream, pos::ParseStreamPosition)
528528
end
529529
end
530530

531+
function first_child_position(stream::ParseStream, pos::ParseStreamPosition)
532+
# Find the first nontrivia range which is a child of this range but not a
533+
# child of the child
534+
c = 0
535+
@assert pos.range_index > 0
536+
parent = stream.ranges[pos.range_index]
537+
i = pos.range_index-1
538+
while i >= 1
539+
if stream.ranges[i].first_token >= parent.first_token &&
540+
(c == 0 || stream.ranges[i].first_token < stream.ranges[c].first_token) &&
541+
!is_trivia(stream.ranges[i])
542+
c = i
543+
end
544+
i -= 1
545+
end
546+
547+
# Find first nontrivia token
548+
t = 0
549+
for i = parent.first_token:parent.last_token
550+
if !is_trivia(stream.tokens[i])
551+
t = i
552+
break
553+
end
554+
end
555+
556+
if c != 0
557+
if t != 0
558+
if stream.ranges[c].first_token > t
559+
return ParseStreamPosition(t, c-1)
560+
else
561+
return ParseStreamPosition(stream.ranges[c].last_token, c)
562+
end
563+
else
564+
return ParseStreamPosition(stream.ranges[c].last_token, c)
565+
end
566+
else
567+
return ParseStreamPosition(t, c)
568+
end
569+
end
570+
531571
function peek_behind(stream::ParseStream; skip_trivia::Bool=true)
532572
pos = position(stream)
533573
if !skip_trivia || !token_is_last(stream, pos)

src/parser.jl

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,22 @@ function is_valid_identifier(k)
306306
!(is_syntactic_operator(k) || k in KSet"? .'")
307307
end
308308

309+
# The expression is a call after stripping `where` and `::`
310+
function was_eventually_call(ps::ParseState)
311+
stream = ps.stream
312+
p = position(ps)
313+
while true
314+
kb = peek_behind(stream, p).kind
315+
if kb == K"call"
316+
return true
317+
elseif kb == K"where" || kb == K"::"
318+
p = first_child_position(ps.stream, p)
319+
else
320+
return false
321+
end
322+
end
323+
end
324+
309325
#-------------------------------------------------------------------------------
310326
# Parser
311327
#
@@ -1786,7 +1802,26 @@ function parse_resword(ps::ParseState)
17861802
emit(ps, mark, K"error", error="expected assignment after `const`")
17871803
end
17881804
elseif word in KSet"function macro"
1789-
parse_function(ps)
1805+
bump(ps, TRIVIA_FLAG)
1806+
bump_trivia(ps)
1807+
has_body = parse_function_signature(ps, word == K"function")
1808+
if has_body
1809+
# The function body
1810+
# function f() \n a \n b end ==> (function (call f) (block a b))
1811+
# function f() end ==> (function (call f) (block))
1812+
parse_block(ps)
1813+
bump_closing_token(ps, K"end")
1814+
emit(ps, mark, word)
1815+
else
1816+
# Function/macro definition with no methods
1817+
# function f end ==> (function f)
1818+
# (function f \n end) ==> (function f)
1819+
# function f \n\n end ==> (function f)
1820+
# function $f end ==> (function ($ f))
1821+
# macro f end ==> (macro f)
1822+
bump(ps, TRIVIA_FLAG, skip_newlines=true)
1823+
emit(ps, mark, word)
1824+
end
17901825
elseif word == K"abstract"
17911826
# Abstract type definitions
17921827
# abstract type A end ==> (abstract A)
@@ -1968,23 +2003,17 @@ function parse_global_local_const_vars(ps)
19682003
end
19692004

19702005
# Parse function and macro definitions
1971-
function parse_function(ps::ParseState)
1972-
mark = position(ps)
1973-
word = peek(ps)
1974-
@check word in KSet"macro function"
1975-
is_function = word == K"function"
2006+
function parse_function_signature(ps::ParseState, is_function::Bool)
19762007
is_anon_func = false
1977-
bump(ps, TRIVIA_FLAG)
1978-
bump_trivia(ps)
19792008

1980-
def_mark = position(ps)
2009+
mark = position(ps)
19812010
if !is_function
19822011
# Parse macro name
19832012
parse_identifier_or_interpolate(ps)
19842013
kb = peek_behind(ps).orig_kind
19852014
if is_initial_reserved_word(ps, kb)
19862015
# macro while(ex) end ==> (macro (call (error while) ex) (block))
1987-
emit(ps, def_mark, K"error", error="Invalid macro name")
2016+
emit(ps, mark, K"error", error="Invalid macro name")
19882017
else
19892018
# macro f() end ==> (macro (call f) (block))
19902019
# macro (:)(ex) end ==> (macro (call : ex) (block))
@@ -1997,6 +2026,7 @@ function parse_function(ps::ParseState)
19972026
# When an initial parenthesis is present, we might either have
19982027
# * the function name in parens, followed by (args...)
19992028
# * an anonymous function argument list in parens
2029+
# * the whole function declaration in parens
20002030
#
20012031
# This should somewhat parse as in parse_paren() (this is what
20022032
# the flisp parser does), but that results in weird parsing of
@@ -2005,22 +2035,36 @@ function parse_function(ps::ParseState)
20052035
bump(ps, TRIVIA_FLAG)
20062036
is_empty_tuple = peek(ps, skip_newlines=true) == K")"
20072037
opts = parse_brackets(ps, K")") do _, _, _, _
2008-
_is_anon_func = peek(ps, 2) != K"("
2038+
_parsed_call = was_eventually_call(ps)
2039+
_is_anon_func = peek(ps, 2) != K"(" && !_parsed_call
20092040
return (needs_parameters = _is_anon_func,
2010-
is_anon_func = _is_anon_func)
2041+
is_anon_func = _is_anon_func,
2042+
parsed_call = _parsed_call)
20112043
end
20122044
is_anon_func = opts.is_anon_func
2045+
if opts.parsed_call
2046+
# Compat: Ugly case where extra parentheses existed and we've
2047+
# already parsed the whole signature.
2048+
# function (f() where T) end ==> (function (where (call f) T) (block))
2049+
# function (f()::S) end ==> (function (:: (call f) S) (block))
2050+
#
2051+
# TODO: Warn for use of parens? The precedence of `::` and
2052+
# `where` don't work inside parens so this is a bit of a syntax
2053+
# oddity/aberration.
2054+
return true
2055+
end
20132056
if is_anon_func
20142057
# function (x) body end ==> (function (tuple x) (block body))
2058+
# function (x::f()) end ==> (function (tuple (:: x (call f))) (block))
20152059
# function (x,y) end ==> (function (tuple x y) (block))
20162060
# function (x=1) end ==> (function (tuple (= x 1)) (block))
20172061
# function (;x=1) end ==> (function (tuple (parameters (= x 1))) (block))
2018-
emit(ps, def_mark, K"tuple")
2062+
emit(ps, mark, K"tuple")
20192063
elseif is_empty_tuple
20202064
# Weird case which is consistent with parse_paren but will be
20212065
# rejected in lowering
20222066
# function ()(x) end ==> (function (call (tuple) x) (block))
2023-
emit(ps, def_mark, K"tuple")
2067+
emit(ps, mark, K"tuple")
20242068
else
20252069
# function (:)() end ==> (function (call :) (block))
20262070
# function (x::T)() end ==> (function (call (:: x T)) (block))
@@ -2033,7 +2077,7 @@ function parse_function(ps::ParseState)
20332077
kb = peek_behind(ps).orig_kind
20342078
if is_reserved_word(kb)
20352079
# function begin() end ==> (function (call (error begin)) (block))
2036-
emit(ps, def_mark, K"error", error="Invalid function name")
2080+
emit(ps, mark, K"error", error="Invalid function name")
20372081
else
20382082
# function f() end ==> (function (call f) (block))
20392083
# function type() end ==> (function (call type) (block))
@@ -2045,26 +2089,18 @@ function parse_function(ps::ParseState)
20452089
end
20462090
end
20472091
if peek(ps, skip_newlines=true) == K"end" && !is_anon_func
2048-
# Function/macro definition with no methods
2049-
# function f end ==> (function f)
2050-
# (function f \n end) ==> (function f)
2051-
# function f \n\n end ==> (function f)
2052-
# function $f end ==> (function ($ f))
2053-
# macro f end ==> (macro f)
2054-
bump(ps, TRIVIA_FLAG, skip_newlines=true)
2055-
emit(ps, mark, word)
2056-
return
2092+
return false
20572093
end
20582094
if !is_anon_func
20592095
# Parse function argument list
20602096
# function f(x,y) end ==> (function (call f x y) (block))
20612097
# function f{T}() end ==> (function (call (curly f T)) (block))
20622098
# function A.f() end ==> (function (call (. A (quote f))) (block))
2063-
parse_call_chain(ps, def_mark)
2099+
parse_call_chain(ps, mark)
20642100
if peek_behind(ps).kind != K"call"
20652101
# function f body end ==> (function (error f) (block body))
2066-
emit(ps, def_mark, K"error",
2067-
error="Invalid signature in $(untokenize(word)) definition")
2102+
emit(ps, mark, K"error",
2103+
error="Invalid signature in $(is_function ? "function" : "macro") definition")
20682104
end
20692105
end
20702106
if is_function && peek(ps) == K"::"
@@ -2073,21 +2109,16 @@ function parse_function(ps::ParseState)
20732109
# function f()::g(T) end ==> (function (:: (call f) (call g T)) (block))
20742110
bump(ps, TRIVIA_FLAG)
20752111
parse_call(ps)
2076-
emit(ps, def_mark, K"::")
2112+
emit(ps, mark, K"::")
20772113
end
20782114
if peek(ps) == K"where"
20792115
# Function signature where syntax
20802116
# function f() where {T} end ==> (function (where (call f) T) (block))
20812117
# function f() where T end ==> (function (where (call f) T) (block))
2082-
parse_where_chain(ps, def_mark)
2118+
parse_where_chain(ps, mark)
20832119
end
2084-
2085-
# The function body
2086-
# function f() \n a \n b end ==> (function (call f) (block a b))
2087-
# function f() end ==> (function (call f) (block))
2088-
parse_block(ps)
2089-
bump_closing_token(ps, K"end")
2090-
emit(ps, mark, word)
2120+
# function f()::S where T end ==> (function (where (:: (call f) S) T) (block))
2121+
return true
20912122
end
20922123

20932124
# Parse a try block

test/parse_stream.jl

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,22 @@
66
using JuliaSyntax: ParseStream,
77
peek, peek_token,
88
bump, bump_trivia, bump_invisible,
9-
emit, emit_diagnostic, TRIVIA_FLAG, INFIX_FLAG
10-
11-
code = """
12-
for i = 1:10
13-
xx[i] + 2
14-
# hi
15-
yy
16-
end
17-
"""
18-
19-
st = ParseStream(code)
9+
emit, emit_diagnostic, TRIVIA_FLAG, INFIX_FLAG,
10+
ParseStreamPosition, first_child_position
2011

2112
# Here we manually issue parse events in the order the Julia parser would issue
2213
# them
2314
@testset "ParseStream" begin
15+
code = """
16+
for i = 1:10
17+
xx[i] + 2
18+
# hi
19+
yy
20+
end
21+
"""
22+
23+
st = ParseStream(code)
24+
2425
p1 = position(st)
2526
@test peek(st) == K"for"
2627
bump(st, TRIVIA_FLAG)
@@ -102,3 +103,34 @@ end
102103
end
103104
end
104105
end
106+
107+
@testset "ParseStream tree traversal" begin
108+
# NB: ParseStreamPosition.token_index includes an initial sentinel token so
109+
# indices here are one more than "might be expected".
110+
st = parse_sexpr("((a b) c)")
111+
child1_pos = first_child_position(st, position(st))
112+
@test child1_pos == ParseStreamPosition(7, 1)
113+
child2_pos = first_child_position(st, child1_pos)
114+
@test child2_pos == ParseStreamPosition(4, 0)
115+
116+
st = parse_sexpr("( (a b) c)")
117+
child1_pos = first_child_position(st, position(st))
118+
@test child1_pos == ParseStreamPosition(8, 1)
119+
child2_pos = first_child_position(st, child1_pos)
120+
@test child2_pos == ParseStreamPosition(5, 0)
121+
122+
st = parse_sexpr("(a (b c))")
123+
@test first_child_position(st, position(st)) == ParseStreamPosition(3, 0)
124+
125+
st = parse_sexpr("( a (b c))")
126+
@test first_child_position(st, position(st)) == ParseStreamPosition(4, 0)
127+
128+
st = parse_sexpr("a (b c)")
129+
@test first_child_position(st, position(st)) == ParseStreamPosition(5, 0)
130+
131+
st = parse_sexpr("(a) (b c)")
132+
@test first_child_position(st, position(st)) == ParseStreamPosition(7, 0)
133+
134+
st = parse_sexpr("(() ())")
135+
@test first_child_position(st, position(st)) == ParseStreamPosition(4, 1)
136+
end

test/parser.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,13 +444,16 @@ tests = [
444444
"global const x" => "(global (error (const x)))"
445445
"const global x" => "(error (const (global x)))"
446446
],
447-
JuliaSyntax.parse_function => [
447+
JuliaSyntax.parse_resword => [
448+
# Macros and functions
448449
"macro while(ex) end" => "(macro (call (error while) ex) (block))"
449450
"macro f() end" => "(macro (call f) (block))"
450451
"macro (:)(ex) end" => "(macro (call : ex) (block))"
451452
"macro (type)(ex) end" => "(macro (call type ex) (block))"
452453
"macro \$f() end" => "(macro (call (\$ f)) (block))"
453454
"macro (\$f)() end" => "(macro (call (\$ f)) (block))"
455+
"function (f() where T) end" => "(function (where (call f) T) (block))" => Expr(:function, Expr(:where, Expr(:call, :f), :T), Expr(:block))
456+
"function (f()::S) end"=> "(function (:: (call f) S) (block))" => Expr(:function, Expr(:(::), Expr(:call, :f), :S), Expr(:block))
454457
"function (x) body end"=> "(function (tuple x) (block body))"
455458
"function (x,y) end" => "(function (tuple x y) (block))"
456459
"function (x=1) end" => "(function (tuple (= x 1)) (block))"

test/test_utils.jl

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,41 @@ function show_green_tree(code; version::VersionNumber=v"1.6")
259259
t = JuliaSyntax.parseall(GreenNode, code, version=version)
260260
sprint(show, MIME"text/plain"(), t, code)
261261
end
262+
263+
#-------------------------------------------------------------------------------
264+
# Parse s-expressions
265+
function parse_sexpr(code)
266+
st = ParseStream(code)
267+
pos_stack = ParseStreamPosition[]
268+
while true
269+
k = peek(st)
270+
if k == K"("
271+
push!(pos_stack, position(st))
272+
bump(st, TRIVIA_FLAG)
273+
elseif k == K")"
274+
if isempty(pos_stack)
275+
bump(st, error="Mismatched `)` with no opening `(`")
276+
break
277+
else
278+
bump(st, TRIVIA_FLAG)
279+
end
280+
emit(st, pop!(pos_stack), K"parens")
281+
elseif k == K"Identifier" || k == K"Integer"
282+
bump(st)
283+
elseif k == K"NewlineWs"
284+
bump(st, TRIVIA_FLAG)
285+
elseif k == K"EndMarker"
286+
if !isempty(pos_stack)
287+
bump_invisible(st, K"error", error="Mismatched `)`")
288+
end
289+
break
290+
else
291+
bump(st, error="Unexpected token")
292+
end
293+
end
294+
if JuliaSyntax.any_error(st)
295+
throw(JuliaSyntax.ParseError(st))
296+
end
297+
st
298+
end
299+

0 commit comments

Comments
 (0)