Skip to content

Commit e48edd0

Browse files
committed
fix: align ellipsis and range-step parsing with Elixir
Update parser semantics around ellipsis and range-step handling to match Elixir behavior for edge cases involving //. What changed: - In parse_infix_expression/2, handle infix // as a range-step operator only when the lhs is a range node, producing :..//. - For non-range lhs, record the same range-step diagnostic intent Elixir uses, instead of silently accepting a generic binary //. - In parse_ellipsis_op/1, treat :ternary_op as a continuation candidate rather than a standalone-ellipsis stop token, which fixes newline forms like x...\n//y. - Add a regression test asserting non-range // produces an error path for x...//y. Why: The previous behavior diverged from Elixir in two directions: it accepted non-range infix // and parsed x...\n//y as ternary continuation instead of preserving ellipsis unary continuation semantics. This change resolves both at grammar/parse semantics level rather than adding context-specific lookahead hacks. Special considerations: - Tokenizer behavior was already aligned with Elixir for these inputs; the fix is parser-only. - Existing semicolon-separated ellipsis cases remain covered and passing.
1 parent e4f928b commit e48edd0

2 files changed

Lines changed: 35 additions & 1 deletion

File tree

lib/spitfire.ex

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1423,8 +1423,28 @@ defmodule Spitfire do
14231423
{rhs, parser}
14241424
end
14251425

1426+
parser =
1427+
if token == :"//" and not match?({:.., _, [_, _]}, lhs) do
1428+
put_error(
1429+
pre_parser,
1430+
{meta,
1431+
"the range step operator (//) must immediately follow the range definition operator (..), for example: 1..9//2. If you wanted to define a default argument, use (\\\\) instead. Syntax error before: '//'"}
1432+
)
1433+
else
1434+
parser
1435+
end
1436+
14261437
ast =
14271438
case token do
1439+
:"//" ->
1440+
case lhs do
1441+
{:.., lhs_meta, [left, middle]} ->
1442+
{:..//, lhs_meta, [left, middle, rhs]}
1443+
1444+
_ ->
1445+
{token, newlines ++ meta, [lhs, rhs]}
1446+
end
1447+
14281448
:"not in" ->
14291449
{:not, meta, [{:in, meta, [lhs, rhs]}]}
14301450

@@ -2927,7 +2947,7 @@ defmodule Spitfire do
29272947
if MapSet.member?(@terminals_with_comma, peek_token(parser)) or
29282948
peek_token(parser) == :";" or
29292949
peek in [:stab_op, :do, :end, :block_identifier] or
2930-
(is_binary_op?(peek) and peek != :dual_op) do
2950+
(is_binary_op?(peek) and peek not in [:dual_op, :ternary_op]) do
29312951
{{:..., current_meta(parser), []}, parser}
29322952
else
29332953
meta = current_meta(parser)

test/spitfire_test.exs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2321,6 +2321,20 @@ defmodule SpitfireTest do
23212321
[{[line: 1, column: 5], "unknown token: %"}]}
23222322
end
23232323

2324+
test "range step operator requires a range lhs" do
2325+
code = "x...//y"
2326+
2327+
assert {:error, _} = s2q(code)
2328+
assert {:error, _ast, errors} = Spitfire.parse(code)
2329+
2330+
assert Enum.any?(errors, fn {_meta, message} ->
2331+
String.contains?(
2332+
message,
2333+
"the range step operator (//) must immediately follow the range definition operator (..)"
2334+
)
2335+
end)
2336+
end
2337+
23242338
test "missing bitstring brackets" do
23252339
code = """
23262340
<<one::

0 commit comments

Comments
 (0)