Skip to content

Commit 5b8bb30

Browse files
authored
Allow choice fallback after traversal errors (#154)
Fixes #146 Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
1 parent 083498e commit 5b8bb30

2 files changed

Lines changed: 80 additions & 5 deletions

File tree

lib/nimble_parsec/compiler.ex

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ defmodule NimbleParsec.Compiler do
365365
end
366366
end
367367

368-
defp traverse(traversal, next, rest, user_acc, context, line, offset, _) do
368+
defp traverse(traversal, next, rest, user_acc, context, line, offset, config) do
369369
case apply_traverse(traversal, rest, user_acc, context, line, offset) do
370370
{:{}, _, [rest, expanded_acc, context]} ->
371371
quote do
@@ -382,23 +382,34 @@ defmodule NimbleParsec.Compiler do
382382
end
383383

384384
{:error, reason} ->
385-
quote do
386-
{:error, unquote(reason), rest, context, line, offset}
387-
end
385+
traverse_error(reason, config)
388386

389387
quoted ->
388+
error = traverse_error(quote(do: reason), config)
389+
390390
quote generated: true do
391391
case unquote(quoted) do
392392
{rest, user_acc, context} when is_list(user_acc) ->
393393
unquote(next)(rest, user_acc ++ acc, stack, context, line, offset)
394394

395395
{:error, reason} ->
396-
{:error, reason, rest, context, line, offset}
396+
unquote(error)
397397
end
398398
end
399399
end
400400
end
401401

402+
defp traverse_error(reason, %{catch_all: nil}) do
403+
quote do
404+
{:error, unquote(reason), rest, context, line, offset}
405+
end
406+
end
407+
408+
defp traverse_error(_reason, %{catch_all: catch_all, acc_depth: n}) do
409+
{_, _, _, body} = build_proxy_to(:unused, catch_all, n)
410+
body
411+
end
412+
402413
defp apply_traverse(mfargs, rest, acc, context, line, offset) do
403414
apply_traverse(Enum.reverse(mfargs), {:{}, [], [rest, acc, context]}, line, offset)
404415
end

test/nimble_parsec_test.exs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
defmodule NimbleParsecTest.QuotedTraversal do
2+
def error(_rest, _acc, _context, _line, _offset), do: {:error, "quoted traversal error"}
3+
end
4+
15
defmodule NimbleParsecTest do
26
use ExUnit.Case, async: true
37

@@ -1252,9 +1256,44 @@ defmodule NimbleParsecTest do
12521256
end
12531257

12541258
describe "choice/2 combinator" do
1259+
three_ascii_chars = times(ascii_char([?a..?z, ?\n]), 3)
1260+
12551261
defparsecp :simple_choice,
12561262
choice([ascii_char([?a..?z]), ascii_char([?A..?Z]), ascii_char([?0..?9])])
12571263

1264+
defparsecp :choice_post_traverse,
1265+
choice([
1266+
post_traverse(three_ascii_chars, {__MODULE__, :error_when_last_is_z, []}),
1267+
replace(three_ascii_chars, nil)
1268+
])
1269+
1270+
defparsecp :choice_pre_traverse,
1271+
choice([
1272+
pre_traverse(three_ascii_chars, {__MODULE__, :error_when_last_is_z, []}),
1273+
replace(three_ascii_chars, nil)
1274+
])
1275+
1276+
defparsecp :choice_quoted_post_traverse,
1277+
choice([
1278+
quoted_post_traverse(
1279+
three_ascii_chars,
1280+
{NimbleParsecTest.QuotedTraversal, :error, []}
1281+
),
1282+
replace(three_ascii_chars, nil)
1283+
])
1284+
1285+
defparsecp :standalone_quoted_post_traverse,
1286+
quoted_post_traverse(
1287+
three_ascii_chars,
1288+
{NimbleParsecTest.QuotedTraversal, :error, []}
1289+
)
1290+
1291+
defparsecp :choice_post_traverse_without_matching_fallback,
1292+
choice([
1293+
post_traverse(three_ascii_chars, {__MODULE__, :error_when_last_is_z, []}),
1294+
string("xyz")
1295+
])
1296+
12581297
defparsecp :choice_label,
12591298
choice([ascii_char([?a..?z]), ascii_char([?A..?Z]), ascii_char([?0..?9])])
12601299
|> label("something")
@@ -1304,6 +1343,31 @@ defmodule NimbleParsecTest do
13041343
assert simple_choice("+=") == {:error, @error, "+=", %{}, {1, 0}, 0}
13051344
end
13061345

1346+
test "falls back after a post-traversal error" do
1347+
assert choice_post_traverse("a\nc!") == {:ok, ~c"a\nc", "!", %{}, {2, 2}, 3}
1348+
1349+
assert choice_post_traverse("a\nz!", context: %{source: :initial}) ==
1350+
{:ok, [nil], "!", %{source: :initial}, {2, 2}, 3}
1351+
end
1352+
1353+
test "falls back after a pre-traversal error" do
1354+
assert choice_pre_traverse("a\nz!") == {:ok, [nil], "!", %{}, {2, 2}, 3}
1355+
end
1356+
1357+
test "falls back after a quoted traversal error" do
1358+
assert choice_quoted_post_traverse("a\nc!") == {:ok, [nil], "!", %{}, {2, 2}, 3}
1359+
1360+
assert standalone_quoted_post_traverse("a\nc!") ==
1361+
{:error, "quoted traversal error", "!", %{}, {2, 2}, 3}
1362+
end
1363+
1364+
test "returns a parser error when no branch succeeds after a traversal error" do
1365+
assert {:error, reason, "a\nz!", %{}, {1, 0}, 0} =
1366+
choice_post_traverse_without_matching_fallback("a\nz!")
1367+
1368+
assert is_binary(reason)
1369+
end
1370+
13071371
@error "expected something"
13081372

13091373
test "returns ok/error with wrapping label" do

0 commit comments

Comments
 (0)