defmodule Matcha do
@readme "README.md"
@external_resource @readme
@moduledoc_blurb @readme
|> File.read!()
|> String.split("<!-- MODULEDOC BLURB -->")
|> Enum.fetch!(1)
@moduledoc_snippet @readme
|> File.read!()
|> String.split("<!-- MODULEDOC SNIPPET -->")
|> Enum.fetch!(1)
@moduledoc """
#{@moduledoc_blurb}
#{@moduledoc_snippet}
### Known Limitations
Currently, it is not possible to:
- Use the `Kernel.in/2` macro in guards. *(see: [open issue](https://github.com/christhekeele/matcha/issues/2))*
- Use the `Kernel.tuple_size/1` or `:erlang.tuple_size/1` guards. *(see: [documentation](https://hexdocs.pm/matcha/Matcha.Context.Common.html#module-limitations))*
- This is a fundamental limitation of match specs.
- Use any `is_record` guards (neither Elixir's implementation because of the `Kernel.tuple_size/1` limitation above, nor erlang's implementation for other reasons). *(see: [documentation](https://hexdocs.pm/matcha/Matcha.Context.Common.html#module-limitations))*
- Both destructure values from a data structure into bindings, and assign the datastructure to a variable, except at the top-level of a clause.
- This is how match specs work by design; though there may be a work-around using `:erlang.map_get/2` for maps, but at this time introducing an inconsistency doesn't seem worth it.
"""
alias Matcha.Context
alias Matcha.Rewrite
alias Matcha.Pattern
alias Matcha.Spec
alias Matcha.Trace
@default_context_module Context.FilterMap
@default_context_type @default_context_module.__context_name__()
@spec pattern(Macro.t()) :: Macro.t()
@doc """
Builds a `Matcha.Pattern` that represents a "filter" operation on a given input.
Match patterns represent a "filter" operation on a given input,
ignoring anything that does not fit the match "shape" specified.
For more information on match patterns, consult the `Matcha.Pattern` docs.
## Examples
iex> require Matcha
...> pattern = Matcha.pattern({x, y, x})
#Matcha.Pattern<{:"$1", :"$2", :"$1"}>
iex> Matcha.Pattern.match?(pattern, {1, 2, 3})
false
iex> Matcha.Pattern.match?(pattern, {1, 2, 1})
true
"""
defmacro pattern(pattern) do
source =
%Rewrite{env: __CALLER__, source: pattern}
|> Rewrite.ast_to_pattern_source(pattern)
quote location: :keep do
%Pattern{source: unquote(source)}
|> Pattern.validate!()
end
end
@spec spec(Context.t(), Macro.t()) :: Macro.t()
@doc """
Builds a `Matcha.Spec` that represents a "filter+map" operation on a given input.
The `context` may be `:filter_map`, `:table`, `:trace`, or a `Matcha.Context` module.
This is detailed in the `Matcha.Context` docs.
For more information on match specs, consult the `Matcha.Spec` docs.
## Examples
iex> require Matcha
...> Matcha.spec do
...> {x, y, x}
...> when x > y and y > 0
...> -> x
...> {x, y, y}
...> when x < y and y < 0
...> -> y
...> end
#Matcha.Spec<[{{:"$1", :"$2", :"$1"}, [{:andalso, {:>, :"$1", :"$2"}, {:>, :"$2", 0}}], [:"$1"]}, {{:"$1", :"$2", :"$2"}, [{:andalso, {:<, :"$1", :"$2"}, {:<, :"$2", 0}}], [:"$2"]}], context: :filter_map>
"""
defmacro spec(context \\ @default_context_type, spec)
defmacro spec(context, _spec = [do: clauses]) when is_list(clauses) do
Enum.each(clauses, fn
{:->, _, _} ->
:ok
other ->
raise ArgumentError,
message:
"#{__MODULE__}.spec/2 must be provided with `->` clauses," <>
" got: `#{Macro.to_string(other)}`"
end)
context =
context
|> Rewrite.perform_expansion(__CALLER__)
|> Rewrite.resolve_context()
source =
%Rewrite{env: __CALLER__, context: context, source: clauses}
|> Rewrite.ast_to_spec_source(clauses)
quote location: :keep do
%Spec{source: unquote(source), context: unquote(context)}
|> Spec.validate!()
end
end
defmacro spec(_context, _spec = [do: not_a_list]) when not is_list(not_a_list) do
raise ArgumentError,
message:
"#{__MODULE__}.spec/2 must be provided with `->` clauses," <>
" got: `#{Macro.to_string(not_a_list)}`"
end
defmacro spec(_context, not_a_block) do
raise ArgumentError,
message:
"#{__MODULE__}.spec/2 requires a block argument," <>
" got: `#{Macro.to_string(not_a_block)}`"
end
@doc """
Traces `function` calls to `module`, executing a `spec` on matching arguments.
Tracing is a powerful feature of the BEAM VM, allowing for near zero-cost
monitoring of what is happening in running systems.
The functions in `Matcha.Trace` provide utilities for accessing this functionality.
One of the most powerful forms of tracing uses match specifications:
rather that just print information on when a certain function signature
with some number of arguments is invoked, they let you:
- dissect the arguments in question with pattern-matching and guards
- take special actions in response (documented in `Matcha.Context.Trace`)
This macro is a shortcut for constructing a `spec` with the `:trace` context via `Matcha.spec/2`,
and tracing the specified `module` and `function` with it via `Matcha.Trace.calls/4`.
For more information on tracing in general, consult the `Matcha.Trace` docs.
## Examples
iex> require Matcha
...> Matcha.trace_calls(Enum, :join, limit: 3) do
...> [_enumerable] -> message("using default joiner")
...> [_enumerable, ""] -> message("using default joiner (but explicitly)")
...> [_enumerable, _custom] -> message("using custom joiner")
...> end
...> Enum.join(1..3)
# Prints a trace message with "using default joiner" appended
"123"
iex> Enum.join(1..3, "")
# Prints a trace message with "using default joiner (but explicitly)" appended
"123"
iex> Enum.join(1..3, ", ")
# Prints a trace message with "using custom joiner" appended
"1, 2, 3"
"""
defmacro trace_calls(module, function, opts \\ [], spec) do
quote do
Trace.calls(
unquote(module),
unquote(function),
Matcha.spec(unquote(Context.Trace.__context_name__()), unquote(spec)),
unquote(opts)
)
end
end
end