Skip to content

Support ... in tuple patterns - #9

Merged
dannote merged 3 commits into
elixir-vibe:masterfrom
rodrigues:vr/tuple_ellipsis
Jul 22, 2026
Merged

Support ... in tuple patterns#9
dannote merged 3 commits into
elixir-vibe:masterfrom
rodrigues:vr/tuple_ellipsis

Conversation

@rodrigues

@rodrigues rodrigues commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

... now works inside tuple patterns, and matches consistently across every tuple arity — including 2-tuples, which were silently skipped:

Pattern Matches
{...} any tuple, including {} and 2-tuples
{:ok, ...} any tuple whose leading element is :ok
{..., :done} any tuple whose trailing element is :done
{a, b, ...} any tuple of ≥2 elements, capturing the first two
{first, ..., last} captures both ends; ... absorbs the middle

Fixed-length tuple patterns ({:ok, _}, {a, b}, {1, 2, 3}) are unchanged: still an exact-length match.

Before

$ mix ex_ast.search "{...}" lib | rg ok
  {:ok, name, arity}
  {:ok, identifier_name(name), 0}
  {:ok, identifier_name(name), length(args)}
  {:ok, _ast, comments}

Didn't return 2-arity tuples, like {:ok, _}.

$ mix ex_ast.search "{:ok, ...}" lib

0 match(es)

After

$ mix ex_ast.search "{...}" lib | rg ok
  {:ok, right_id}
  {:ok, right_id}
  ...
$ mix ex_ast.search "{:ok, ...}" lib
lib/ex_ast.ex:389
  {:ok, matches}

lib/ex_ast/diff/matcher.ex:48
  {:ok, right_id}

lib/ex_ast/diff/matcher.ex:80
  {:ok, right_id}

$ mix ex_ast.search "{first, ..., last}" lib/ex_ast/ident.ex
lib/ex_ast/ident.ex:31
  {:ok, parsed, rest}
  first: :ok
  last: rest

1 match(es)

Map and keyword patterns (%{a: 1}, [a: 1]) are unaffected — their entries are also {key, value} 2-tuples, and they keep matching as before.

Edit: Adding a commit that fixes a case where a 2-tuple sub-pattern matched nothing when it appeared as a call/operator argument or a list element, such as the LHS of {:ok, _} = _, with {:ok, _} <- _, or [{:ok, _}], because the pattern kept its bare shape while the source folded it; do_match now reconciles the two.

@rodrigues
rodrigues force-pushed the vr/tuple_ellipsis branch 3 times, most recently from f10433a to ca46efb Compare July 19, 2026 12:11

@dannote dannote left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tuple ellipsis looks good overall; there's one regression with quoted ASTs.

Comment thread lib/ex_ast/pattern.ex
# `normalize_entry/1` — only standalone tuples reach here.
def normalize({left, right}),
do: {normalize(left), normalize(right)}
do: {:{}, nil, [normalize(left), normalize(right)]}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes pattern normalization and source normalization asymmetric. A plain quoted source tuple stays {a, b}, while the pattern is folded to {:{}, nil, [...]}:

Pattern.match(
  quote(do: {:ok, value}),
  quote(do: {:ok, _})
)
# :error

Could you normalize both paths the same way (or handle the reverse shape) and add a test with an ordinary quote source?

`{...}`, `{:ok, ...}` and `{..., :done}` matched nothing, and plain `{...}`
even missed 2-tuples like `{:ok, value}` while matching `{}` and `{1, 2, 3}`.
Two causes:

Candidate prefiltering treated `...` and the variadic tuple head `:{}` as
calls, deriving signatures no raw source tuple can satisfy, so find_all
dropped the node before matching. And Elixir encodes 2-tuples as a bare
`{a, b}` but other arities as `{:{}, meta, [elems]}`, so `{...}` (the
variadic form) never reconciled with a literal 2-tuple.

Exclude `:{}` and `...` from signature derivation so tuple patterns become
match-all candidates, like `{_, _}` already was. Normalize genuine 2-tuple
literals into the variadic `{:{}, _, [a, b]}` form so every arity flows
through the one ellipsis path already used by calls and lists.

Map and keyword entries are also `{key, value}` 2-tuples but live inside
lists/maps, so they stay bare and keep matching as before.

Cover every arity and the leading/trailing/both-sides capture forms with
find_all tests, plus a map/keyword regression guard.
A genuine 2-tuple that appears as a call/operator argument or a list
element (e.g. the LHS of `{:ok, _} = _`, `with {:ok, _} <- _`, or
`[{:ok, _}]`) matched nothing.

The source normalizer folds genuine 2-tuples to the variadic
`{:{}, _, [a, b]}` form (Sourceror marks them with a `__block__` wrapper),
but a pattern 2-tuple in that position is indistinguishable from a keyword
entry at parse time, so it stays bare `{a, b}`. `do_match` had no clause to
reconcile the two shapes.

Bridge them in `do_match`: a bare 2-tuple pattern now matches a folded
2-element source tuple.
@rodrigues
rodrigues force-pushed the vr/tuple_ellipsis branch from ca46efb to d25d789 Compare July 21, 2026 13:24
Pattern.match(quote(do: {:ok, value}), quote(do: {:ok, _})) returned :error.

Standalone tuple patterns fold to the variadic `{:{}, _, [a, b]}` form, and
source normalization folds Sourceror's `{:__block__, _, [{a, b}]}` encoding to
match. But a quoted source has no such wrapper: a genuine 2-tuple stays bare
`{a, b}`, indistinguishable from a keyword/map entry, so the source side can't
fold it and the shapes never reconciled.

Folding bare source tuples isn't an option: find_all visits map/keyword entries
as standalone nodes, so `{...}` would wrongly match `%{a: 1}`. Instead bridge the
reverse shape at match time, mirroring the existing folded-source vs bare-pattern
clause: a bare source 2-tuple now matches a folded pattern tuple.
@rodrigues
rodrigues requested a review from dannote July 21, 2026 14:47
@dannote
dannote merged commit 3d52b30 into elixir-vibe:master Jul 22, 2026
2 checks passed
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.

2 participants