Skip to content

Comments

fix: enforce fn-head nested parens at parse time#118

Open
mkaput wants to merge 4 commits intoelixir-tools:mainfrom
mkaput:pr/fn-head-nested-parens
Open

fix: enforce fn-head nested parens at parse time#118
mkaput wants to merge 4 commits intoelixir-tools:mainfrom
mkaput:pr/fn-head-nested-parens

Conversation

@mkaput
Copy link
Contributor

@mkaput mkaput commented Feb 19, 2026

I dove way to deep into the rabbithole with these tests. They popped for me through proptests, I used LLMs to find all these variations, and I fixed them by having an LLM port Elixir's parser logic.

  1. stab_expr fn-clause head shapes
    Ported concept: separate and normalize bare heads vs parenthesized heads before building -> clauses.
    Source: elixir_parser.yrl stab_expr rules

  2. when vs <- / \\ precedence behavior in fn heads
    Ported concept: preserve operator precedence model, and lower guard parsing precedence only for specific simple fn-head forms.
    Source: precedence table

  3. unwrap_when-style reassociation
    Ported concept: reassociate trailing when in head args so non-when operators bind to the last head arg/guard the same way compiler parser does.
    Source: unwrap_when/1

  4. Nested-parens rejection semantics (unexpected parentheses)
    Ported concept: reject nested parenthesized fn-head arg forms during fn-head construction, but keep Spitfire recovery by recording errors and continuing AST construction.

I dove way to deep into the rabbithole with these tests. They popped for me through proptests, I used LLMs to find all these variations, and I fixed them by having an LLM port Elixir's parser logic.

1. **`stab_expr` fn-clause head shapes**
   Ported concept: separate and normalize bare heads vs parenthesized heads before building `->` clauses.
   Source: [`elixir_parser.yrl` `stab_expr` rules](https://github.com/elixir-lang/elixir/blob/v1.18.2/lib/elixir/src/elixir_parser.yrl#L340-L353)

2. **`when` vs `<-` / `\\` precedence behavior in fn heads**
   Ported concept: preserve operator precedence model, and lower guard parsing precedence only for specific simple fn-head forms.
   Source: [precedence table](https://github.com/elixir-lang/elixir/blob/v1.18.2/lib/elixir/src/elixir_parser.yrl#L59-L63)

3. **`unwrap_when`-style reassociation**
   Ported concept: reassociate trailing `when` in head args so non-`when` operators bind to the last head arg/guard the same way compiler parser does.
   Source: [`unwrap_when/1`](https://github.com/elixir-lang/elixir/blob/v1.18.2/lib/elixir/src/elixir_parser.yrl#L1135-L1141)

4. **Nested-parens rejection semantics (`unexpected parentheses`)**
   Ported concept: reject nested parenthesized fn-head arg forms during fn-head construction, but keep Spitfire recovery by recording errors and continuing AST construction.
Recent fn-head parsing changes introduced infix reassociation branches that assume comma/when operands always carry list arguments. In the absinthe matrix CI run, a malformed intermediate AST used , which passed the  guard and crashed in .

This change hardens the parser by requiring list operands before reassociation in , and by making fn-head nested-parens validation a no-op for non-list argument payloads.

This preserves existing behavior for valid AST shapes while preventing parser crashes on recoverable malformed intermediates encountered during large real-world repo parsing.
Add a focused parser parity regression for the snippet that triggered the CI crash:
comma = ascii_char([?,])

This comes from absinthe-graphql/absinthe lexer code and protects against regressions where an identifier named `comma` can produce malformed intermediate lhs state during infix parsing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant