Current section
Files
Jump to
Current section
Files
lib/matcha.ex
defmodule Matcha do
@moduledoc """
First-class match specification and match patterns for Elixir.
The BEAM VM Match patterns and specs
"""
alias Matcha.Context
alias Matcha.Rewrite
alias Matcha.Source
alias Matcha.Pattern
alias Matcha.Spec
# TODO
# defmacro sigil_m, do: :noop
# defmacro sigil_M, do: :noop
@spec context_type(Source.type() | Context.t() | nil) :: {Context.t(), Source.type()}
defp context_type(context) do
case context do
:table -> {Context.Table, context}
:trace -> {Context.Trace, context}
nil -> {nil, :table}
module when is_atom(module) -> {module, module.__type__()}
end
end
@doc """
Macro for building a `Matcha.Pattern`.
The `context` may be `nil`, `:table`, `:trace`, or a `Matcha.Context` module.
## Examples
iex> require Matcha
...> Matcha.pattern({x, y})
#Matcha.Pattern<{:"$1", :"$2"}>
"""
defmacro pattern(context \\ nil, pattern) do
{context, type} = context_type(context)
rewrite = %Rewrite{env: __CALLER__, type: type, context: context, source: pattern}
source =
pattern
|> expand_pattern(rewrite)
|> rewrite_pattern(rewrite)
|> Macro.escape(unquote: true)
quote location: :keep do
%Pattern{source: unquote(source), type: unquote(type)}
|> Pattern.validate!()
end
end
defp expand_pattern(match, rewrite) do
{match, _env} = :elixir_expand.expand(match, Macro.Env.to_match(rewrite.env))
match
end
defp rewrite_pattern(match, rewrite) do
{rewrite, match} = Rewrite.rewrite_bindings(rewrite, match)
Rewrite.rewrite_match(rewrite, match)
end
@doc """
Macro for building a `Matcha.Spec`.
The `context` may be `nil`, `:table`, `:trace`, or a `Matcha.Context` module.
## Examples
iex> require Matcha
...> Matcha.spec do
...> {x, y, x} -> {y, x}
...> end
#Matcha.Spec<[{{:"$1", :"$2", :"$1"}, [], [{{:"$2", :"$1"}}]}]>
"""
defmacro spec(context \\ nil, _source = [do: clauses]) do
{context, type} = context_type(context)
rewrite = %Rewrite{env: __CALLER__, type: type, context: context, source: clauses}
source =
clauses
|> expand_spec(rewrite)
|> Enum.map(&normalize_clause(&1, rewrite))
|> Enum.map(&rewrite_clause(&1, rewrite))
|> Macro.escape(unquote: true)
quote location: :keep do
%Spec{source: unquote(source), type: unquote(type)}
|> Spec.validate!()
end
end
defp expand_spec(clauses, rewrite) do
expansion =
if rewrite.context do
quote do
import unquote(rewrite.context), warn: false
unquote({:fn, [], clauses})
end
else
{:fn, [], clauses}
end
{ast, _env} = :elixir_expand.expand(expansion, rewrite.env)
{_, clauses} =
Macro.prewalk(ast, nil, fn
{:fn, [], clauses} = fun, nil -> {fun, clauses}
other, clauses -> {other, clauses}
end)
clauses
end
defp normalize_clause({:->, _, [[head], body]}, _rewrite) do
{match, conditions} = :elixir_utils.extract_guards(head)
{match, conditions, List.wrap(body)}
end
defp normalize_clause(clause, rewrite) do
raise Rewrite.Error,
source: rewrite,
details: "normalizing clauses",
problems: [
error: "match spec clauses must be of arity 1, got: `#{Macro.to_string(clause)}`"
]
end
defp rewrite_clause({match, conditions, body}, rewrite) do
{rewrite, match} = Rewrite.rewrite_bindings(rewrite, match)
match = Rewrite.rewrite_match(rewrite, match)
conditions = Rewrite.rewrite_conditions(rewrite, conditions)
body = Rewrite.rewrite_body(rewrite, body)
{match, conditions, body}
end
end