Skip to content

Support arity syntax in definition patterns - #10

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

Support arity syntax in definition patterns#10
dannote merged 3 commits into
elixir-vibe:masterfrom
rodrigues:vr/arity_syntax

Conversation

@rodrigues

Copy link
Copy Markdown
Contributor

Definition patterns can now be constrained by argument count with name/arity syntax. /N is an exact arity, /_ matches any arity, and the name is either captured or ignored with _:

Pattern Matches
def name/2 any 2-arity def, capturing the name
def _/0 any 0-arity def, name ignored
def name/_ a def of any arity, capturing the name
defp name/1 only private 1-arity defs
defmacro name/2 only 2-arity macro definitions

Works across def/defp/defmacro/defmacrop, respects the definition kind (defp name/1 won't match a public def), and looks through guard clauses (def clamp(x) when x > 0 is arity 1). Empty-paren heads like def ping() are treated as arity 0.

Before

$ mix ex_ast.search --count "def name/4 do ... end" lib
0

name/2 was matched literally as a / operator, so it never lined up with a definition head.

After

$ mix ex_ast.search --count "def name/4 do ... end" lib
8
$ mix ex_ast.search "def name/0 do ... end" lib
lib/ex_ast/cli/selector_options.ex:69
  def switches, do: @switches
  name: :switches

lib/ex_ast/selector.ex:239
  def piped, do: predicate(:piped, nil)
  name: :piped

lib/ex_ast/selector.ex:243
  def first, do: predicate(:first, nil)
  name: :first

lib/ex_ast/selector.ex:247
  def last, do: predicate(:last, nil)
  name: :last


4 match(es)

_ on the name is a non-capturing arity constraint — same matches, no name: binding:

$ mix ex_ast.search "def _/0 do ... end" lib/ex_ast/selector.ex
lib/ex_ast/selector.ex:239
def piped, do: predicate(:piped, nil)

lib/ex_ast/selector.ex:243
def first, do: predicate(:first, nil)

lib/ex_ast/selector.ex:247
def last, do: predicate(:last, nil)

3 match(es)
$ mix ex_ast.search --count "defp _/_ do ... end"
783

Fixed-shape definition patterns (def name(a, b), def ()) are unchanged. _(_) doesn't count when there are guards, Maybe we want to change that?

$ mix ex_ast.search --count "defp _(_) do ... end"
254
$ mix ex_ast.search --count "defp _/1 do ... end"
361

Arity constraints are opt-in via the / syntax.

@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.

I like the arity syntax, but there are two consistency issues to sort out.

Comment thread lib/ex_ast/pattern.ex
)
when form in [:def, :defp, :defmacro, :defmacrop] do
with {:ok, caps} <- match_def_head_arity(shead, name_pat, arity_pat, caps) do
do_match(srest, prest, caps)

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.

The examples say def name/2 should work on its own, but prest is empty in that case and srest contains the body, so it never matches a normal definition:

Patcher.find_all(
  "def apply(a, b), do: {a, b}",
  "def name/2"
)
# []

Could you either make the short form ignore the body or update the API/docs to require def name/2 do ... end?

Comment thread lib/ex_ast/pattern.ex Outdated

defp match_def_name(_name, _name_pat, _caps), do: :error

defp name_atom_value(name) when is_atom(name), do: name

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 changes the capture from the identifier AST ({:foo, nil, nil}) to :foo. Besides being inconsistent with existing definition captures, it breaks repeated captures:

Patcher.find_all(
  "def foo(foo), do: foo",
  "def name/1 do name end"
)
# []

Could we keep the same capture shape as existing definition patterns?

`def name/2`, `def _/0`, `def name/_` constrain a definition by argument
count. `/N` is an exact arity, `/_` matches any, and the name is captured
(or `_` to ignore it). Works across def/defp/defmacro/defmacrop and matches
guard clauses.
Address two consistency issues in `def name/arity` definition patterns:

- Capture the name as the identifier AST `{name, nil, nil}` rather than a
  raw atom, matching how every other pattern capture binds AST nodes. This
  fixes repeated captures, so `def name/1 do name end` matches
  `def foo(foo), do: foo` and rejects `def foo(bar), do: bar`.

- Document that the arity form mirrors the parenthesized head form's body
  handling: `def name/2 do ... end` behaves like `def name(_, _) do ... end`,
  and body-presence must line up on both sides. The bare short form matches
  only bodyless defs, exactly like the bare paren form. No matching change;
  moduledoc now shows the arity syntax with a `do ... end` body throughout.
@rodrigues
rodrigues requested a review from dannote July 21, 2026 16:34
# Conflicts:
#	lib/ex_ast/pattern.ex
#	test/ex_ast/pattern_test.exs
@dannote
dannote merged commit a677c20 into elixir-vibe:master Jul 22, 2026
2 checks passed
@rodrigues
rodrigues deleted the vr/arity_syntax branch July 23, 2026 10:10
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