Skip to content

Commit 4a7e846

Browse files
authored
Make left hand side of -> a tuple of arguments (#522)
This makes argument lists on the left hand side of `->` parse as tuples. In particular, this fixes a very inconsistent case where the left hand side previously parsed as a `K"block"`; we now have: * `(a;x=1)->b` ==> `(-> (tuple-p a (parameters (= x 1))) b` rather than `(block a (= x 1))` on the left hand side. In addition, the following forms are treated consistently * `a->b` ==> `(-> (tuple a) b)` * `(a)->b` ==> `(-> (tuple-p a) b)` * `(a,)->b` ==> `(-> (tuple-p-, a) b)` The upside of this is that expression processing of `->` syntax should be much easier. There's one aberrant case involving `where` which is particularly difficult and still not dealt with: `(x where T) -> b` does not parse as `(where (tuple x) T)` on the left hand side. However, `where` precedence involving `::` and `->` is already horribly broken and this syntax will always be awful to write unless we make breaking changes. So I'm too tempted to call this a lost cause for now 😬. Compatibility shims for converting the `SyntaxNode` form back to `Expr` in order to keep `Expr` stable are included. (At some point we should consider fixing this and deleting these shims because the new form is so much more consistent and would be reflected neatly into `Expr` form.)
1 parent 5ff59c2 commit 4a7e846

File tree

5 files changed

+72
-6
lines changed

5 files changed

+72
-6
lines changed

docs/src/reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class of tokenization errors and lets the parser deal with them.
7070
* Standalone dotted operators are always parsed as `(. op)`. For example `.*(x,y)` is parsed as `(call (. *) x y)` (#240)
7171
* The `K"="` kind is used for keyword syntax rather than `kw`, to avoid various inconsistencies and ambiguities (#103)
7272
* Unadorned postfix adjoint is parsed as `call` rather than as a syntactic operator for consistency with suffixed versions like `x'ᵀ` (#124)
73+
* The argument list in the left hand side of `->` is always a tuple. For example, `x->y` parses as `(-> (tuple x) y)` rather than `(-> x y)` (#522)
7374

7475
### Improvements to awkward AST forms
7576

src/expr.jl

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,11 +388,35 @@ function _internal_node_to_Expr(source, srcrange, head, childranges, childheads,
388388
# Block for conditional's source location
389389
args[1] = Expr(:block, loc, args[1])
390390
elseif k == K"->"
391+
a1 = args[1]
392+
if @isexpr(a1, :tuple)
393+
# TODO: This makes the Expr form objectively worse for the sake of
394+
# compatibility. We should consider deleting this special case in
395+
# the future as a minor change.
396+
if length(a1.args) == 1 &&
397+
(!has_flags(childheads[1], PARENS_FLAG) ||
398+
!has_flags(childheads[1], TRAILING_COMMA_FLAG)) &&
399+
!Meta.isexpr(a1.args[1], :parameters)
400+
# `(a) -> c` is parsed without tuple on lhs in Expr form
401+
args[1] = a1.args[1]
402+
elseif length(a1.args) == 2 && (a11 = a1.args[1]; @isexpr(a11, :parameters) &&
403+
length(a11.args) <= 1 && !Meta.isexpr(a1.args[2], :(...)))
404+
# `(a; b=1) -> c` parses args as `block` in Expr form :-(
405+
if length(a11.args) == 0
406+
args[1] = Expr(:block, a1.args[2])
407+
else
408+
a111 = only(a11.args)
409+
assgn = @isexpr(a111, :kw) ? Expr(:(=), a111.args...) : a111
410+
argloc = source_location(LineNumberNode, source, last(childranges[1]))
411+
args[1] = Expr(:block, a1.args[2], argloc, assgn)
412+
end
413+
end
414+
end
391415
a2 = args[2]
416+
# Add function source location to rhs; add block if necessary
392417
if @isexpr(a2, :block)
393418
pushfirst!(a2.args, loc)
394419
else
395-
# Add block for source locations
396420
args[2] = Expr(:block, loc, args[2])
397421
end
398422
elseif k == K"function"

src/parser.jl

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,8 +1409,21 @@ function parse_decl_with_initial_ex(ps::ParseState, mark)
14091409
emit(ps, mark, K"::", INFIX_FLAG)
14101410
end
14111411
if peek(ps) == K"->"
1412-
# x -> y ==> (-> x y)
1413-
# a::b->c ==> (-> (::-i a b) c)
1412+
kb = peek_behind(ps).kind
1413+
if kb == K"tuple"
1414+
# (x,y) -> z
1415+
# (x) -> y
1416+
# (x; a=1) -> y
1417+
elseif kb == K"where"
1418+
# `where` and `->` have the "wrong" precedence when writing anon functons.
1419+
# So ignore this case to allow use of grouping brackets with `where`.
1420+
# This needs to worked around in lowering :-(
1421+
# (x where T) -> y ==> (-> (x where T) y)
1422+
else
1423+
# x -> y ==> (-> (tuple x) y)
1424+
# a::b->c ==> (-> (tuple (::-i a b)) c)
1425+
emit(ps, mark, K"tuple")
1426+
end
14141427
bump(ps, TRIVIA_FLAG)
14151428
# -> is unusual: it binds tightly on the left and loosely on the right.
14161429
parse_eq_star(ps)
@@ -3073,7 +3086,8 @@ function parse_paren(ps::ParseState, check_identifiers=true)
30733086
initial_semi = peek(ps) == K";"
30743087
opts = parse_brackets(ps, K")") do had_commas, had_splat, num_semis, num_subexprs
30753088
is_tuple = had_commas || (had_splat && num_semis >= 1) ||
3076-
(initial_semi && (num_semis == 1 || num_subexprs > 0))
3089+
(initial_semi && (num_semis == 1 || num_subexprs > 0)) ||
3090+
(peek(ps, 2) == K"->" && peek_behind(ps).kind != K"where")
30773091
return (needs_parameters=is_tuple,
30783092
is_tuple=is_tuple,
30793093
is_block=num_semis > 0)

test/expr.jl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,31 @@
130130
@testset "->" begin
131131
@test parsestmt("a -> b") ==
132132
Expr(:->, :a, Expr(:block, LineNumberNode(1), :b))
133+
@test parsestmt("(a,) -> b") ==
134+
Expr(:->, Expr(:tuple, :a), Expr(:block, LineNumberNode(1), :b))
135+
@test parsestmt("(a where T) -> b") ==
136+
Expr(:->, Expr(:where, :a, :T), Expr(:block, LineNumberNode(1), :b))
133137
# @test parsestmt("a -> (\nb;c)") ==
134138
# Expr(:->, :a, Expr(:block, LineNumberNode(1), :b))
135139
@test parsestmt("a -> begin\nb\nc\nend") ==
136140
Expr(:->, :a, Expr(:block,
137141
LineNumberNode(1),
138142
LineNumberNode(2), :b,
139143
LineNumberNode(3), :c))
144+
@test parsestmt("(a;b=1) -> c") ==
145+
Expr(:->,
146+
Expr(:block, :a, LineNumberNode(1), Expr(:(=), :b, 1)),
147+
Expr(:block, LineNumberNode(1), :c))
148+
@test parsestmt("(a...;b...) -> c") ==
149+
Expr(:->,
150+
Expr(:tuple, Expr(:parameters, Expr(:(...), :b)), Expr(:(...), :a)),
151+
Expr(:block, LineNumberNode(1), :c))
152+
@test parsestmt("(;) -> c") ==
153+
Expr(:->,
154+
Expr(:tuple, Expr(:parameters)),
155+
Expr(:block, LineNumberNode(1), :c))
156+
@test parsestmt("a::T -> b") ==
157+
Expr(:->, Expr(:(::), :a, :T), Expr(:block, LineNumberNode(1), :b))
140158
end
141159

142160
@testset "elseif" begin

test/parser.jl

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,18 @@ tests = [
288288
"begin x end::T" => "(::-i (block x) T)"
289289
# parse_decl_with_initial_ex
290290
"a::b" => "(::-i a b)"
291-
"a->b" => "(-> a b)"
292291
"a::b::c" => "(::-i (::-i a b) c)"
293-
"a::b->c" => "(-> (::-i a b) c)"
292+
"a->b" => "(-> (tuple a) b)"
293+
"(a,b)->c" => "(-> (tuple-p a b) c)"
294+
"(a;b=1)->c" => "(-> (tuple-p a (parameters (= b 1))) c)"
295+
"x::T->c" => "(-> (tuple (::-i x T)) c)"
296+
# `where` combined with `->` still parses strangely. However:
297+
# * It's extra hard to add a tuple around the `x` in this syntax corner case.
298+
# * The user already needs to add additional, ugly, parens to get this
299+
# to parse correctly because the precendence of `where` is
300+
# inconsistent with `::` and `->` in this case.
301+
"(x where T)->c" => "(-> (parens (where x T)) c)"
302+
"((x::T) where T)->c" => "(-> (parens (where (parens (::-i x T)) T)) c)"
294303
],
295304
JuliaSyntax.parse_unary_subtype => [
296305
"<: )" => "<:"

0 commit comments

Comments
 (0)