From 9b04d0613eaa66f45e1ef6891ff551667f8a16e6 Mon Sep 17 00:00:00 2001 From: doorgan Date: Sat, 31 Jan 2026 05:07:13 -0300 Subject: [PATCH] fix: improve handling of matched/unmatched expressions Unary operators behave differently depending on wether they are applied to a matched or unmatched expression in Elixir's grammar. calls with do/end blocks are unmatched expressions, and Elixir allows unary operators to wrap a full expression in that position, not just the immediate block. Spitfire doesn't make that distinction, it always parses the unary operand at unary precedence, making it bind too tightly when the operand is an unparenthesized do/end block. So, this code from the tests in #78: `+case a do _ -> b end |> c ** d >>> e` Is parsed like this by Spitfire: `(+(case a do _ -> b end) |> c ** d) >>> e` But Elixir parses it like this: `+(case a do _ -> b end |> c ** d >>> e)` The fix is to detect when the unary operand is an unparenthesized do/end block and reparse the RHS with the lowest precedence, making the unary operand a full expression, forcing it to behave like Elixir. More generally, we probably want to make the distinction more explicit in Spitfires' parser, but for now I'm fixing one issue at a time to achieve parity with Elixir first and have a baseline for more improvements. --- lib/spitfire.ex | 19 +++++++++++++++++++ test/spitfire_test.exs | 10 ++++++++++ 2 files changed, 29 insertions(+) diff --git a/lib/spitfire.ex b/lib/spitfire.ex index 77e38c9..01d8f3d 100644 --- a/lib/spitfire.ex +++ b/lib/spitfire.ex @@ -788,14 +788,33 @@ defmodule Spitfire do end parser = parser |> next_token() |> eat_eoe() + rhs_parser = parser {rhs, parser} = parse_expression(parser, precedence, false, false, false) + {rhs, parser} = + if unparenthesized_do_end_block?(rhs) do + parse_expression(rhs_parser, @lowest, false, false, false) + else + {rhs, parser} + end + ast = {token, meta, [rhs]} {ast, parser} end end + defp unparenthesized_do_end_block?(ast) do + case ast do + {_, meta, _} when is_list(meta) -> + Keyword.has_key?(meta, :do) && Keyword.has_key?(meta, :end) && + not Keyword.has_key?(meta, :parens) + + _ -> + false + end + end + defp parse_prefix_lone_identifer(parser) do trace "parse_prefix_lone_identifer", trace_meta(parser) do token = current_token(parser) diff --git a/test/spitfire_test.exs b/test/spitfire_test.exs index 857215e..107bacc 100644 --- a/test/spitfire_test.exs +++ b/test/spitfire_test.exs @@ -2163,6 +2163,16 @@ defmodule SpitfireTest do assert Spitfire.parse(code) == s2q(code) end end + + test "operators on unmatched expression" do + code = ~S''' + +case a do + _ -> b + end |> c ** d >>> e + ''' + + assert Spitfire.parse(code) == s2q(code) + end end describe "code with errors" do