Coverage

67.6
531
115522
172

lib/matcha.ex

80.0
20
1288
4
Line Hits Source
0 defmodule Matcha do
1 @readme "README.md"
2 @external_resource @readme
3 @moduledoc_blurb @readme
4 |> File.read!()
5 |> String.split("<!-- MODULEDOC BLURB -->")
6 |> Enum.fetch!(1)
7 @moduledoc_snippet @readme
8 |> File.read!()
9 |> String.split("<!-- MODULEDOC SNIPPET -->")
10 |> Enum.fetch!(1)
11
12 @moduledoc """
13 #{@moduledoc_blurb}
14
15 #{@moduledoc_snippet}
16 """
17
18 alias Matcha.Context
19
20 @default_context Matcha.Context.FilterMap
21
22 @spec pattern(Macro.t()) :: Macro.t()
23 @doc """
24 Builds a `Matcha.Pattern` that represents a pattern matching operation on a given input.
25
26 For more information on match patterns, consult the `Matcha.Pattern` docs.
27
28 ## Examples
29
30 iex> require Matcha
31 ...> pattern = Matcha.pattern({x, y, x})
32 ...> Matcha.Pattern.match?(pattern, {1, 2, 3})
33 false
34 iex> Matcha.Pattern.match?(pattern, {1, 2, 1})
35 true
36
37 """
38 1 defmacro pattern(pattern) do
39 1 Matcha.Rewrite.build_pattern(__CALLER__, pattern)
40 end
41
42 @spec filter(Macro.t()) :: Macro.t()
43 @doc """
44 Builds a `Matcha.Filter` that represents a guarded match operation on a given input.
45
46 For more information on match filters, consult the `Matcha.Filter` docs.
47
48 ## Examples
49
50 iex> require Matcha
51 ...> filter = Matcha.filter({x, y, z} when x > z)
52 ...> Matcha.Filter.match?(filter, {1, 2, 3})
53 false
54 iex> Matcha.Filter.match?(filter, {3, 2, 1})
55 true
56
57 """
58 1 defmacro filter(filter) do
59 1 Matcha.Rewrite.build_filter(__CALLER__, filter)
60 end
61
62 @spec spec(Context.t(), Macro.t()) :: Macro.t()
63 @doc """
64 Builds a `Matcha.Spec` that represents a destructuring, pattern matching, and re-structuring operation in a given `context`.
65
66 The `context` may be #{Context.__core_context_aliases__() |> Keyword.keys() |> Enum.map_join(", ", &"`#{inspect(&1)}`")}, or a `Matcha.Context` module.
67 This is detailed in the `Matcha.Context` docs.
68
69 For more information on match specs, consult the `Matcha.Spec` docs.
70
71 ## Examples
72
73 iex> require Matcha
74 ...> spec = Matcha.spec(:table) do
75 ...> {x, y, x}
76 ...> when x > y and y > 0
77 ...> -> x
78 ...> {x, y, y}
79 ...> when x < y and y < 0
80 ...> -> y
81 ...> end
82 iex> Matcha.Spec.run(spec, [
83 ...> {2, 1, 2},
84 ...> {-2, -1, -1},
85 ...> {1, 2, 3}
86 ...> ])
87 {:ok, [2, -1]}
88
89 """
90 defmacro spec(context, spec)
91
92 57 defmacro spec(context, [do: clauses] = _spec) when is_list(clauses) do
93 57 Enum.each(clauses, fn
94 63 {:->, _, _} ->
95 :ok
96
97 other ->
98 0 raise ArgumentError,
99 message:
100 0 "#{__MODULE__}.spec/2 must be provided with `->` clauses," <>
101 " got: `#{Macro.to_string(other)}`"
102 end)
103
104 57 Matcha.Rewrite.build_spec(__CALLER__, context, clauses)
105 end
106
107 defmacro spec(_context, _spec = [do: not_a_list]) when not is_list(not_a_list) do
108 0 raise ArgumentError,
109 message:
110 0 "#{__MODULE__}.spec/2 must be provided with `->` clauses," <>
111 " got: `#{Macro.to_string(not_a_list)}`"
112 end
113
114 defmacro spec(_context, not_a_block) do
115 1 raise ArgumentError,
116 message:
117 1 "#{__MODULE__}.spec/2 requires a block argument," <>
118 " got: `#{Macro.to_string(not_a_block)}`"
119 end
120
121 @spec spec(Macro.t()) :: Macro.t()
122 @doc """
123 Builds a `Matcha.Spec` that represents a destructuring, pattern matching, and re-structuring operation on in-memory data.
124
125 Identical to calling `spec/2` with a `:filter_map` context. Note that this context is mostly used to experiment with match specs,
126 and you should generally prefer calling `spec/2` with either a `:table` or `:trace` context
127 depending on which `Matcha` APIs you intend to use:
128
129 - Use the `:trace` context if you intend to query data with `Matcha.Trace` functions
130 - Use the `:table` context if you intend to trace code execution with the `Matcha.Table` functions
131
132 ## Examples
133
134 iex> require Matcha
135 ...> spec = Matcha.spec do
136 ...> {x, y, x}
137 ...> when x > y and y > 0
138 ...> -> x
139 ...> {x, y, y}
140 ...> when x < y and y < 0
141 ...> -> y
142 ...> end
143 iex> Matcha.Spec.run(spec, [
144 ...> {2, 1, 2},
145 ...> {-2, -1, -1},
146 ...> {1, 2, 3}
147 ...> ])
148 {:ok, [2, -1]}
149
150 """
151 defmacro spec(spec)
152
153 521 defmacro spec([do: clauses] = _spec) when is_list(clauses) do
154 521 Matcha.Rewrite.build_spec(__CALLER__, @default_context, clauses)
155 end
156
157 defmacro spec(_spec = [do: not_a_list]) when not is_list(not_a_list) do
158 2 raise ArgumentError,
159 message:
160 2 "#{__MODULE__}.spec/1 must be provided with `->` clauses," <>
161 " got: `#{Macro.to_string(not_a_list)}`"
162 end
163
164 defmacro spec(not_a_block) do
165 1 raise ArgumentError,
166 message:
167 1 "#{__MODULE__}.spec/1 requires a block argument," <>
168 " got: `#{Macro.to_string(not_a_block)}`"
169 end
170 end

lib/matcha/context.ex

47.6
42
11137
22
Line Hits Source
0 defmodule Matcha.Context do
1 @moduledoc """
2 Defines the functions allowed in and the behaviour of `Matcha.Spec`s for different use-cases.
3
4 Different types of match spec are intended to be used for different purposes,
5 and support different instructions in their bodies when passed to different APIs.
6
7 The modules implementing the `Matcha.Context` behaviour define the different types of `Matcha.Spec`,
8 provide documentation for what specialized instructions that type supports, and are used during
9 Elixir-to-match spec conversion as a concrete function definition to use when expanding instructions
10 (since most of these specialized instructions do not exist anywhere as an actual functions,
11 this lets the Elixir compiler complain about invalid instructions as `UndefinedFunctionError`s).
12
13 ### Predefined contexts
14
15 Currently the supported applications of match specs are:
16
17 - `:filter_map`:
18
19 Matchspecs intended to be used to filter/map over an in-memory list in an optimized fashion.
20 These types of match spec reference the `Matcha.Context.FilterMap` module,
21 and can be used in the `Matcha.Spec` APIs.
22
23 - `:match`:
24
25 Matchspecs intended to be used to match over an in-memory list in an optimized fashion.
26 These types of match spec reference the `Matcha.Context.Match` module,
27 and can be used in the `Matcha.Spec` APIs.
28
29 - `:table`:
30
31 Matchspecs intended to be used to efficiently select data from BEAM VM "table"
32 tools, such as [`:ets`](https://www.erlang.org/doc/man/ets),
33 [`:dets`](https://www.erlang.org/doc/man/dets),
34 and [`:mnesia`](https://www.erlang.org/doc/man/mnesia), and massage the values returned.
35 These types of match spec reference the `Matcha.Context.Table` module,
36 and can be used in the `Matcha.Table` APIs.
37
38 - `:trace`:
39
40 Matchspecs intended to be used to instruct tracing utilities such as
41 [`:erlang.trace_pattern/3`](https://www.erlang.org/doc/man/erlang#trace_pattern-3),
42 [`:dbg`](https://www.erlang.org/doc/man/dbg),
43 and [`:recon_trace`](https://ferd.github.io/recon/recon_trace)
44 exactly what function calls with what arguments to trace,
45 and allows invoking special trace command instructions in response.
46 These types of match spec reference the `Matcha.Context.Trace` module,
47 and can be used in the `Matcha.Trace` APIs.
48
49 ### Custom contexts
50
51 The context mechanism is technically extensible: any module can implement the `Matcha.Context`
52 behaviour, define the callbacks, and list public no-op functions to allow their usage in
53 specs compiled with that context (via `Matcha.spec(CustomContext) do...`).
54
55 In practice there is little point in defining a custom context:
56 the supported use-cases for match specs are tightly coupled to the Erlang language,
57 and `Matcha` covers all of them with its provided contexts, which should be sufficient for any application.
58 The module+behaviour+callback implementation used in `Matcha` is less about offering extensibility,
59 but instead used to simplify special-casing in `Matcha.Spec` function implementations,
60 raise Elixir-flavored errors when an invalid instruction is used in the different types of spec,
61 and provide a place to document what they do when invoked.
62
63 """
64
65 defmacro __using__(_opts \\ []) do
66 quote do
67 @behaviour unquote(__MODULE__)
68 end
69 end
70
71 alias Matcha.Error
72 alias Matcha.Raw
73
74 alias Matcha.Spec
75
76 @type t :: module()
77
78 @core_context_aliases [
79 filter_map: Matcha.Context.FilterMap,
80 match: Matcha.Context.Match,
81 table: Matcha.Context.Table,
82 trace: Matcha.Context.Trace
83 ]
84
85 @spec __core_context_aliases__() :: Keyword.t()
86 @doc """
87 Maps the shortcut references to the core Matcha context modules.
88
89 This describes which shortcuts users may write, for example in `Matcha.spec(:some_shortcut)` instead of
90 the fully qualified module `Matcha.spec(Matcha.Context.SomeContext)`.
91 """
92 0 def __core_context_aliases__(), do: @core_context_aliases
93
94 @doc """
95 Which primitive Erlang context this context module wraps.
96 """
97 @callback __erl_spec_type__() :: Raw.type()
98
99 @doc """
100 A default value to use when executing match specs in this context.
101
102 This function is used to provide `Matcha.Raw.test/3` with a target value to test against,
103 in situations where it is being used to simply validate the match spec itself,
104 but we do not acutally care if the input matches the spec.
105
106 This value, when passed to this context's `c:Matcha.Context.__valid_match_target__/1` callback,
107 must produce a `true` value.
108 """
109 @callback __default_match_target__() :: any
110
111 @doc """
112 A validator that runs before executing a match spec against a `target` in this context.
113
114 This validator is run before any match specs are executed on inputs to `Matcha.Raw.test/3`,
115 and all elements of the enumerable input to `Matcha.Raw.run/2`.
116
117 If this function returns false, the match spec will not be executed, instead
118 returning a `t:Matcha.Error.error_problem` with a `t:Matcha.Error.message`
119 generated by the `c:Matcha.Context.__invalid_match_target_error_message__/1` callback.
120 """
121 @callback __valid_match_target__(match_target :: any) :: boolean()
122
123 @doc """
124 Describes an issue with a test target.
125
126 Invoked to generate a `t:Matcha.Error.message` when `c:Matcha.Context.__valid_match_target__/1` fails.
127 """
128 @callback __invalid_match_target_error_message__(match_target :: any) :: binary
129
130 @doc """
131 Allows this context module to modify match specs before their execution.
132
133 This hook is the main entrypoint for creating custom contexts,
134 allowing them to augment the match spec with new behaviour when executed in this context.
135
136 Care must be taken to handle the results of the modified match spec after execution correctly,
137 before they are returned to the caller. This should be implemented in the callbacks:
138
139 - `c:__transform_erl_run_results__/1`
140 - `c:__transform_erl_test_result__/1`
141 - `c:__emit_erl_test_result__/1`
142 """
143 @callback __prepare_source__(source :: Raw.uncompiled()) ::
144 {:ok, new_source :: Raw.uncompiled()} | {:error, Error.problems()}
145 @doc """
146 Transforms the result of a spec match just after calling `:erlang.match_spec_test/3`.
147
148 You can think of this as an opportunity to "undo" any modifications to the user's
149 provided matchspec made in `c:__prepare_source__/1`.
150
151 Must return `{:ok, result}` to indicate that the returned value is valid; otherwise
152 return `{:error, problems}` to raise an exception.
153 """
154 @callback __transform_erl_test_result__(result :: any) ::
155 {:ok, result :: any} | {:error, Error.problems()}
156
157 @doc """
158 Transforms the result of a spec match just after calling `:ets.match_spec_run/2`.
159
160 You can think of this as an opportunity to "undo" any modifications to the user's
161 provided matchspec made in `c:__prepare_source__/1`.
162
163 Must return `{:ok, result}` to indicate that the returned value is valid; otherwise
164 return `{:error, problems}` to raise an exception.
165 """
166 @callback __transform_erl_run_results__(results :: [any]) ::
167 {:ok, results :: [any]} | {:error, Error.problems()}
168
169 @doc """
170 Decides if the result of a spec match should be part of the result set.
171
172 This callback runs just after calls to `c:__transform_erl_test_result__/1` or `c:__transform_erl_test_result__/1`.
173
174 Must return `{:emit, result}` to include the transformed result of a spec match, when executing it
175 against in-memory data (as opposed to tracing or :ets) for validation or debugging purposes.
176 Otherwise, returning `:no_emit` will hide the result.
177 """
178 @callback __emit_erl_test_result__(result :: any) :: {:emit, new_result :: any} | :no_emit
179
180 @doc """
181 Determines whether or not specs in this context can be compiled.
182 """
183 @spec supports_compilation?(t) :: boolean
184 def supports_compilation?(context) do
185 365 context.__erl_spec_type__() == :table
186 end
187
188 @doc """
189 Determines whether or not specs in this context can used in tracing.
190 """
191 @spec supports_tracing?(t) :: boolean
192 def supports_tracing?(context) do
193 0 context.__erl_spec_type__() == :trace
194 end
195
196 @doc """
197 Resolves shortcut references to the core Matcha context modules.
198
199 This allows users to write, for example, `Matcha.spec(:trace)` instead of
200 the fully qualified module `Matcha.spec(Matcha.Context.Trace)`.
201 """
202 @spec resolve(atom() | t) :: t | no_return
203
204 for {alias, context} <- @core_context_aliases do
205 63 def resolve(unquote(alias)), do: unquote(context)
206 end
207
208 534 def resolve(context) when is_atom(context) do
209 534 context.__erl_spec_type__()
210 rescue
211 UndefinedFunctionError ->
212 1 reraise ArgumentError,
213 [
214 message:
215 "`#{inspect(context)}` is not one of: " <>
216 (Keyword.keys(@core_context_aliases)
217 4 |> Enum.map_join(", ", &"`#{inspect(&1)}`")) <>
218 " or a module that implements `Matcha.Context`"
219 ],
220 __STACKTRACE__
221 else
222 533 _ -> context
223 end
224
225 def resolve(context) do
226 1 raise ArgumentError,
227 message:
228 "`#{inspect(context)}` is not one of: " <>
229 (Keyword.keys(@core_context_aliases)
230 4 |> Enum.map_join(", ", &"`#{inspect(&1)}`")) <>
231 " or a module that implements `Matcha.Context`"
232 end
233
234 @spec run(Matcha.Spec.t(), Enumerable.t()) ::
235 {:ok, list(any)} | {:error, Error.problems()}
236 @doc """
237 Runs a `spec` against an `enumerable`.
238
239 This is a key function that ensures the input `spec` and results
240 are passed through the callbacks of a `#{inspect(__MODULE__)}`.
241
242 Returns either `{:ok, results}` or `{:error, problems}` (that other `!` APIs may use to raise an exception).
243 """
244 def run(%Spec{context: context} = spec, enumerable) do
245 365 case context.__prepare_source__(Spec.raw(spec)) do
246 {:ok, source} ->
247 365 match_targets = Enum.to_list(enumerable)
248 # TODO: validate targets pre-run
249 # spec.context.__valid_match_target__(match_target)
250
251 365 results =
252 if supports_compilation?(context) do
253 source
254 |> Raw.compile()
255 365 |> Raw.run(match_targets)
256 else
257 0 do_run_without_compilation(match_targets, spec, source)
258 end
259
260 365 spec.context.__transform_erl_run_results__(results)
261
262 0 {:error, problems} ->
263 {:error, problems}
264 end
265 end
266
267 defp do_run_without_compilation(match_targets, spec, source) do
268 match_targets
269 |> Enum.reduce([], fn match_target, results ->
270 0 case do_test(source, spec.context, match_target) do
271 {:ok, result} ->
272 0 case spec.context.__emit_erl_test_result__(result) do
273 0 {:emit, result} ->
274 [result | results]
275
276 0 :no_emit ->
277 {[], spec}
278 end
279
280 {:error, problems} ->
281 0 raise Spec.Error,
282 source: spec,
283 details: "when running match spec",
284 problems: problems
285 end
286 end)
287 0 |> :lists.reverse()
288 end
289
290 @doc """
291 Creates a lazy `Stream` that yields the results of running the `spec` against the provided `enumberable`.
292
293 This is a key function that ensures the input `spec` and results
294 are passed through the callbacks of a `#{inspect(__MODULE__)}`.
295
296 Returns either `{:ok, stream}` or `{:error, problems}` (that other `!` APIs may use to raise an exception).
297 """
298 @spec stream(Matcha.Spec.t(), Enumerable.t()) ::
299 {:ok, Enumerable.t()} | {:error, Error.problems()}
300 def stream(%Spec{context: context} = spec, enumerable) do
301 0 case context.__prepare_source__(Spec.raw(spec)) do
302 {:ok, source} ->
303 0 Stream.transform(enumerable, {spec, source}, fn match_target, {spec, source} ->
304 # TODO: validate targets midstream
305 # spec.context.__valid_match_target__(match_target)
306 0 do_stream_test(match_target, spec, source)
307 end)
308
309 0 {:error, problems} ->
310 {:error, problems}
311 end
312 end
313
314 defp do_stream_test(match_target, spec, source) do
315 0 case do_test(source, spec.context, match_target) do
316 {:ok, result} ->
317 0 case spec.context.__emit_erl_test_result__(result) do
318 {:emit, result} ->
319 0 case spec.context.__transform_erl_run_results__([result]) do
320 0 {:ok, results} ->
321 {:ok, results}
322
323 {:error, problems} ->
324 0 raise Spec.Error,
325 source: spec,
326 details: "when streaming match spec",
327 problems: problems
328 end
329
330 0 :no_emit ->
331 {[], spec}
332 end
333
334 {:error, problems} ->
335 0 raise Spec.Error,
336 source: spec,
337 details: "when streaming match spec",
338 problems: problems
339 end
340 end
341
342 @spec test(Spec.t()) ::
343 {:ok, any} | {:error, Error.problems()}
344 @doc """
345 Tests that the provided `spec` in its `Matcha.Context` is valid.
346
347 Invokes `c:__default_match_target__/0` and passes it into `:erlang.match_spec_test/3`.
348
349 Returns either `{:ok, stream}` or `{:error, problems}` (that other `!` APIs may use to raise an exception).
350 """
351 def test(%Spec{context: context} = spec) do
352 728 test(spec, context.__default_match_target__())
353 end
354
355 @spec test(Spec.t(), Raw.match_target()) ::
356 {:ok, any} | {:error, Error.problems()}
357 @doc """
358 Tests that the provided `spec` in its `Matcha.Context` correctly matches a provided `match_target`.
359
360 Passes the provided `match_target` into `:erlang.match_spec_test/3`.
361
362 Returns either `{:ok, stream}` or `{:error, problems}` (that other `!` APIs may use to raise an exception).
363 """
364 def test(%Spec{context: context} = spec, match_target) do
365 1309 case context.__prepare_source__(Spec.raw(spec)) do
366 {:ok, source} ->
367 1309 if context.__valid_match_target__(match_target) do
368 1309 do_test(source, context, match_target)
369 else
370 {:error,
371 [
372 error: context.__invalid_match_target_error_message__(match_target)
373 ]}
374 end
375
376 0 {:error, problems} ->
377 {:error, problems}
378 end
379 end
380
381 defp do_test(source, context, match_target) do
382 source
383 1309 |> Raw.test(context.__erl_spec_type__(), match_target)
384 1309 |> context.__transform_erl_test_result__()
385 end
386 end

lib/matcha/context/erlang.ex

100.0
2
63
0
Line Hits Source
0 defmodule Matcha.Context.Erlang do
1 moduledoc = """
2 Erlang functions and operators that any match specs can use in their bodies.
3
4 ## Omissions
5
6 This list aligns closely with what you would expect to be able to use in guards.
7 However, Erlang does not allow some guard-safe functions in match specs:
8
9 - `:erlang.is_record/2`
10 """
11
12 # TODO: Once Erlang/OTP 26 is the minimum supported version,
13 # we can metaprogram this as we used to try to do and 26 now does, via
14 # https://github.com/erlang/otp/pull/7046/files#diff-32b3cd3e6c0d949335e0d3da944dd750e07eeee7f2f8613e6865a7ae70b33e48R1167-R1173
15 # or how Elixir does, via
16 # https://github.com/elixir-lang/elixir/blob/f4b05d178d7b9bb5356beae7ef8e01c32324d476/lib/elixir/src/elixir_utils.erl#L24-L37
17
18 moduledoc =
19 if Matcha.Helpers.erlang_version() < 25 do
20 moduledoc <>
21 """
22
23 These functions only work in match specs in Erlang/OTP >= 25,
24 and are not available to you in Erlang/OTP #{Matcha.Helpers.erlang_version()}:
25
26 - `:erlang.binary_part/2`
27 - `:erlang.binary_part/3`
28 - `:erlang.byte_size/1`
29 """
30 else
31 moduledoc
32 end
33
34 moduledoc =
35 if Matcha.Helpers.erlang_version() < 26 do
36 moduledoc <>
37 """
38
39 These functions only work in match specs in Erlang/OTP >= 26,
40 and are not available to you in Erlang/OTP #{Matcha.Helpers.erlang_version()}:
41
42 - `:erlang.ceil/1`
43 - `:erlang.floor/1`
44 - `:erlang.is_function/2`
45 - `:erlang.tuple_size/1`
46 """
47 else
48 moduledoc
49 end
50
51 @moduledoc moduledoc
52
53 @allowed_short_circuit_expressions [
54 andalso: 2,
55 orelse: 2
56 ]
57
58 @allowed_functions [
59 # Used by or mapped to Elixir Kernel guards
60 -: 1,
61 -: 2,
62 "/=": 2,
63 "=/=": 2,
64 *: 2,
65 /: 2,
66 +: 1,
67 +: 2,
68 <: 2,
69 "=<": 2,
70 ==: 2,
71 "=:=": 2,
72 >: 2,
73 >=: 2,
74 abs: 1,
75 and: 2,
76 bit_size: 1,
77 div: 2,
78 element: 2,
79 hd: 1,
80 is_atom: 1,
81 is_binary: 1,
82 is_float: 1,
83 is_function: 1,
84 is_integer: 1,
85 is_list: 1,
86 is_map_key: 2,
87 is_map: 1,
88 is_number: 1,
89 is_pid: 1,
90 is_port: 1,
91 is_record: 3,
92 is_reference: 1,
93 is_tuple: 1,
94 length: 1,
95 map_size: 1,
96 map_get: 2,
97 node: 0,
98 node: 1,
99 not: 1,
100 or: 2,
101 self: 0,
102 rem: 2,
103 round: 1,
104 tl: 1,
105 trunc: 1,
106 # Used by or mapped to Elixir Bitwise guards
107 band: 2,
108 bor: 2,
109 bnot: 1,
110 bsl: 2,
111 bsr: 2,
112 bxor: 2,
113 # No Elixir equivalent
114 size: 1,
115 xor: 2
116 ]
117
118 if Matcha.Helpers.erlang_version() >= 25 do
119 @allowed_functions @allowed_functions ++ [binary_part: 2, binary_part: 3]
120 @allowed_functions @allowed_functions ++ [byte_size: 1]
121 end
122
123 if Matcha.Helpers.erlang_version() >= 26 do
124 @allowed_functions @allowed_functions ++ [ceil: 1, floor: 1]
125 @allowed_functions @allowed_functions ++ [is_function: 2]
126 @allowed_functions @allowed_functions ++ [tuple_size: 1]
127 end
128
129 for {function, arity} <- @allowed_functions do
130 @doc "All match specs can call `:erlang.#{function}/#{arity}`."
131 61 def unquote(function)(unquote_splicing(Macro.generate_arguments(arity, __MODULE__))),
132 do: :noop
133 end
134
135 for {function, arity} <- @allowed_short_circuit_expressions do
136 @doc "All match specs can call the `#{function}/#{arity}` [short-circuit expression](https://www.erlang.org/doc/reference_manual/expressions.html#short-circuit-expressions)."
137 2 def unquote(function)(unquote_splicing(Macro.generate_arguments(arity, __MODULE__))),
138 do: :noop
139 end
140 end

lib/matcha/context/filter_map.ex

75.0
16
11382
4
Line Hits Source
0 defmodule Matcha.Context.FilterMap do
1 @moduledoc """
2 Functions and operators that `:filter_map` match specs can use.
3
4 ???
5
6 Specs created in this context are unique in that they can differentiate
7 between specs that fail to find a matching clause for the given input,
8 and specs with matching clauses that literally return the `false` value.
9 They return `:no_return` in the former case, and `{:matched, result}` tuples in the latter,
10 where `result` can be a literal `false` returned from a clause.
11 """
12
13 # TODO: handle `:EXIT`s better in :filter_map/:match contexts
14
15 import Matcha
16
17 alias Matcha.Context
18 alias Matcha.Spec
19
20 use Context
21
22 ###
23 # CALLBACKS
24 ##
25
26 @impl Context
27 1709 def __erl_spec_type__ do
28 :table
29 end
30
31 @impl Context
32 499 def __default_match_target__ do
33 nil
34 end
35
36 @impl Context
37 1013 def __valid_match_target__(_match_target) do
38 true
39 end
40
41 @impl Context
42 0 def __invalid_match_target_error_message__(_match_target) do
43 ""
44 end
45
46 @impl Context
47 1188 def __prepare_source__(source) do
48 {:ok,
49 1193 for {match, guards, body} <- source do
50 1193 {last_expr, body} = List.pop_at(body, -1)
51 1193 body = [body | [{{:returned, last_expr}}]]
52 1193 {match, guards, body}
53 end ++ [{:_, [], [{{:no_return}}]}]}
54 end
55
56 @impl Context
57 0 def __emit_erl_test_result__({:returned, result}) do
58 {:emit, result}
59 end
60
61 0 def __emit_erl_test_result__({:no_return}) do
62 :no_emit
63 end
64
65 @impl Context
66 def __transform_erl_test_result__(result) do
67 1013 case result do
68 404 {:ok, {:no_return}, [], _warnings} ->
69 {:ok, nil}
70
71 609 {:ok, {:returned, result}, [], _warnings} ->
72 {:ok, result}
73
74 0 {:error, problems} ->
75 {:error, problems}
76 end
77 end
78
79 @impl Context
80 def __transform_erl_run_results__(results) do
81 spec(:table) do
82 {:returned, value} -> value
83 end
84 175 |> Spec.run(results)
85 end
86 end

lib/matcha/context/match.ex

75.0
16
628
4
Line Hits Source
0 defmodule Matcha.Context.Match do
1 @moduledoc """
2 Functions and operators that `:match` match specs can use.
3
4 Specs created in this are unique in that they can differentiate
5 between specs that fail to find a matching clause for the given input,
6 and specs with matching clauses that literally return the `false` value.
7 They return `:no_match` in the former case, and `{:matched, result}` tuples in the latter,
8 where `result` can be a literal `false` returned from a clause.
9 """
10
11 # TODO: handle `:EXIT`s better in :filter_map/:match contexts
12
13 import Matcha
14
15 alias Matcha.Context
16 alias Matcha.Spec
17
18 use Context
19
20 ###
21 # CALLBACKS
22 ##
23
24 @impl Context
25 77 def __erl_spec_type__ do
26 :table
27 end
28
29 @impl Context
30 29 def __default_match_target__ do
31 nil
32 end
33
34 @impl Context
35 62 def __valid_match_target__(_match_target) do
36 true
37 end
38
39 @impl Context
40 0 def __invalid_match_target_error_message__(_match_target) do
41 ""
42 end
43
44 @impl Context
45 65 def __prepare_source__(source) do
46 {:ok,
47 67 for {match, guards, body} <- source do
48 67 {last_expr, body} = List.pop_at(body, -1)
49 67 body = [body | [{{:matched, last_expr}}]]
50 67 {match, guards, body}
51 end ++ [{:_, [], [{{:no_match}}]}]}
52 end
53
54 @impl Context
55 0 def __emit_erl_test_result__({:matched, result}) do
56 {:emit, {:matched, result}}
57 end
58
59 def __emit_erl_test_result__({:no_match}) do
60 0 {:no_match}
61 end
62
63 @impl Context
64 def __transform_erl_test_result__(result) do
65 62 case result do
66 29 {:ok, {:no_match}, [], _warnings} ->
67 {:ok, :no_match}
68
69 33 {:ok, {:matched, result}, [], _warnings} ->
70 {:ok, {:matched, result}}
71
72 0 {:error, problems} ->
73 {:error, problems}
74 end
75 end
76
77 @impl Context
78 def __transform_erl_run_results__(results) do
79 spec(:table) do
80 {:matched, value} -> {:matched, value}
81 {:no_match} -> :no_match
82 end
83 3 |> Spec.run(results)
84 end
85 end

lib/matcha/context/table.ex

70.0
10
1908
3
Line Hits Source
0 defmodule Matcha.Context.Table do
1 @moduledoc """
2 Functions and operators that `:table` match specs can use.
3
4 The return values of specs created in this context do not differentiate
5 between specs that fail to find a matching clause for the given input,
6 and specs with matching clauses that literally return the `false` value;
7 they return `{:returned, result}` tuples either way.
8 """
9
10 alias Matcha.Context
11
12 use Context
13
14 ###
15 # CALLBACKS
16 ##
17
18 @impl Context
19 417 def __erl_spec_type__ do
20 :table
21 end
22
23 @impl Context
24 def __default_match_target__ do
25 197 {}
26 end
27
28 @impl Context
29 def __valid_match_target__(match_target) do
30 230 is_tuple(match_target)
31 end
32
33 @impl Context
34 def __invalid_match_target_error_message__(match_target) do
35 0 "test targets for `:table` specs must be a tuple, got: `#{inspect(match_target)}`"
36 end
37
38 @impl Context
39 417 def __prepare_source__(source) do
40 {:ok, source}
41 end
42
43 @impl Context
44 0 def __emit_erl_test_result__(result) do
45 {:emit, result}
46 end
47
48 @impl Context
49 def __transform_erl_test_result__(result) do
50 230 case result do
51 230 {:ok, result, [], _warnings} -> {:ok, result}
52 0 {:error, problems} -> {:error, problems}
53 end
54 end
55
56 @impl Context
57 187 def __transform_erl_run_results__(results) do
58 {:ok, results}
59 end
60 end

lib/matcha/context/trace.ex

73.6
19
34
5
Line Hits Source
0 defmodule Matcha.Context.Trace do
1 @moduledoc """
2 Additional functions that trace match specs can use in their bodies.
3
4 The return values of specs created in this context do not differentiate
5 between specs that fail to find a matching clause for the given input,
6 and specs with matching clauses that literally return the `false` value;
7 they return `{:traced, result, flags}` tuples either way.
8
9 Tracing match specs offer a wide suite of instructions to drive Erlang's tracing engine
10 in response to matching certain calls.
11 Calls to these functions in match spec bodies will, when that clause is matched,
12 effect the documented change during tracing.
13
14 These instructions are documented and type-specced here as a convenient reference.
15 For more information, consult the [Erlang tracing match spec docs](https://www.erlang.org/doc/apps/erts/match_spec.html#functions-allowed-only-for-tracing).
16
17 In addition to general helpful informational functions, tracing supports:
18
19 ### Trace Flags
20
21 Match specs can change how tracing behaves by changing the trace flags on any process.
22 See `:erlang.trace/3` for more information.
23
24 Related functions:
25
26 - `enable_trace/1`
27 - `enabled_trace/2`
28 - `disable_trace/1`
29 - `disable_trace/2`
30 - `trace/2`
31 - `trace/3`
32
33 ### Sequential Tracing
34
35 Match specs can be used to transfer information between processes via sequential tracing.
36 See the [Erlang sequential tracing docs](https://www.erlang.org/doc/man/seq_trace.html#whatis)
37 for more information.
38
39 Related functions:
40 - `is_seq_trace/0`
41 - `set_seq_token/2`
42 - `get_seq_token/0`
43
44 """
45
46 alias Matcha.Context
47
48 use Context
49
50 ###
51 # CALLBACKS
52 ##
53
54 @impl Context
55 4 def __erl_spec_type__ do
56 :trace
57 end
58
59 @impl Context
60 3 def __default_match_target__ do
61 []
62 end
63
64 @impl Context
65 def __valid_match_target__(match_target) do
66 4 is_list(match_target)
67 end
68
69 @impl Context
70 def __invalid_match_target_error_message__(match_target) do
71 0 "test targets for `:trace` specs must be a list, got: `#{inspect(match_target)}`"
72 end
73
74 @impl Context
75 4 def __prepare_source__(source) do
76 {:ok, source}
77 end
78
79 @impl Context
80 0 def __emit_erl_test_result__(result) do
81 {:emit, result}
82 end
83
84 @impl Context
85 def __transform_erl_test_result__(result) do
86 4 case result do
87 {:ok, result, flags, _warnings} ->
88 4 result =
89 if is_list(result) do
90 0 List.to_string(result)
91 else
92 4 result
93 end
94
95 {:ok, {:traced, result, flags}}
96
97 {:error, problems} ->
98 0 {_warnings, errors} = Keyword.split(problems, [:warning])
99 {:error, Matcha.Rewrite.problems(errors)}
100 end
101 end
102
103 @impl Context
104 0 def __transform_erl_run_results__(results) do
105 {:ok, results}
106 end
107
108 ###
109 # INFORMATIONAL FUNCTIONS
110 ##
111
112 @dialyzer {:nowarn_function, message: 1}
113 @spec message(message | {message, false | message, true}) :: true when message: any
114 @doc """
115 Sets an additional `message` appended to the trace message sent.
116
117 One can only set one additional message in the body. Later calls replace the appended message.
118
119 Always returns `true`.
120
121 As a special case, `{message, false}` disables sending of trace messages ('call' and 'return_to')
122 for this function call, just like if the match specification had not matched.
123 This can be useful if only the side effects of the match spec clause's body part are desired.
124
125 Another special case is `{message, true}`, which sets the default behavior,
126 as if the function had no match specification;
127 trace message is sent with no extra information
128 (if no other calls to message are placed before `{message, true}`, it is in fact a "noop").
129 """
130 def message(message) do
131 _ignore = message
132 :noop
133 end
134
135 @dialyzer {:nowarn_function, return_trace: 0}
136 @spec return_trace :: true
137 @doc """
138 Causes a `return_from` trace message to be sent upon return from the current function.
139
140 If the process trace flag silent is active, the `return_from` trace message is inhibited.
141
142 Always returns `true`.
143
144 > ***Warning***:
145 >
146 > If the traced function is tail-recursive,
147 > this match specification function destroys that property.
148 > Hence, if a match specification executing this function is used on a perpetual server process,
149 > it can only be active for a limited period of time, or the emulator will eventually
150 > use all memory in the host machine and crash.
151 > If this match specification function is inhibited
152 > using process trace flag silent, tail-recursiveness still remains.
153 """
154 1 def return_trace do
155 :noop
156 end
157
158 @dialyzer {:nowarn_function, exception_trace: 0}
159 @spec exception_trace :: true
160 @doc """
161 Works as `return_trace/0`, generating an extra `exception_from` message on exceptions.
162
163 Causes a `return_from` trace message to be sent upon return from the current function.
164 Plus, if the traced function exits because of an exception,
165 an `exception_from` trace message is generated, ***whether or not the exception is caught***.
166
167 If the process trace flag silent is active, the `return_from` and `exception_from` trace messages are inhibited.
168
169 Always returns `true`.
170
171 > ***Warning***:
172 > If the traced function is tail-recursive,
173 > this match specification function destroys that property.
174 > Hence, if a match specification executing this function is used on a perpetual server process,
175 > t can only be active for a limited period of time, or the emulator will eventually
176 > use all memory in the host machine and crash.
177 > If this match specification function is inhibited
178 > using process trace flag silent, tail-recursiveness still remains.
179 """
180 1 def exception_trace do
181 :noop
182 end
183
184 @dialyzer {:nowarn_function, process_dump: 0}
185 @spec process_dump :: true
186 @doc """
187 Returns some textual information about the current process as a binary.
188 """
189 1 def process_dump do
190 :noop
191 end
192
193 @dialyzer {:nowarn_function, caller: 0}
194 @spec caller :: {module, function, arity :: non_neg_integer} | :undefined
195 @doc """
196 Returns the module/function/arity of the calling function.
197
198 If the calling function cannot be determined, returns `:undefined`.
199 This can happen with BIFs in particular.
200 """
201 1 def caller do
202 :noop
203 end
204
205 @dialyzer {:nowarn_function, display: 1}
206 @spec display(value :: any) :: {module, function, arity :: non_neg_integer} | :undefined
207 @doc """
208 Displays the given `value` on stdout for debugging purposes.
209
210 Always returns `true`.
211 """
212 def display(value) do
213 _ignore = value
214 :noop
215 end
216
217 @dialyzer {:nowarn_function, get_tcw: 0}
218 @spec get_tcw :: trace_control_word when trace_control_word: non_neg_integer
219 @doc """
220 Returns the value of the current node's trace control word.
221
222 Identical to calling `:erlang.system_info/1` with the argument `:trace_control_word`.
223
224 The trace control word is a 32-bit unsigned integer intended for generic trace control.
225 The trace control word can be tested and set both from within trace match specifications and with BIFs.
226 """
227 1 def get_tcw do
228 :noop
229 end
230
231 @dialyzer {:nowarn_function, set_tcw: 1}
232 @spec set_tcw(trace_control_word) :: trace_control_word when trace_control_word: non_neg_integer
233 @doc """
234 Sets the value of the current node's trace control word to `trace_control_word`.
235
236 Identical to calling `:erlang.system_flag/2` with the arguments `:trace_control_word` and `trace_control_word`.
237
238 Returns the previous value of the node's trace control word.
239 """
240 def set_tcw(trace_control_word) do
241 _ignore = trace_control_word
242 :noop
243 end
244
245 @dialyzer {:nowarn_function, silent: 1}
246 @spec silent(mode :: boolean | any) :: any
247 @doc """
248 Changes the verbosity of the current process's messaging `mode`.
249
250 - If `mode` is `true`, supresses all trace messages.
251 - If `mode` is `false`, re-enables trace messages in future calls.
252 - If `mode` is anything else, the current mode remains active.
253 """
254 def silent(mode) do
255 _ignore = mode
256 :noop
257 end
258
259 ###
260 # TRACE FLAG FUNCTIONS
261 ##
262
263 @type trace_flag ::
264 :all
265 | :send
266 | :receive
267 | :procs
268 | :ports
269 | :call
270 | :arity
271 | :return_to
272 | :silent
273 | :running
274 | :exiting
275 | :running_procs
276 | :running_ports
277 | :garbage_collection
278 | :timestamp
279 # | :cpu_timestamp
280 | :monotonic_timestamp
281 | :strict_monotonic_timestamp
282 | :set_on_spawn
283 | :set_on_first_spawn
284 | :set_on_link
285 | :set_on_first_link
286 @type tracer_trace_flag ::
287 {:tracer, pid | port}
288 | {:tracer, module, any}
289
290 @dialyzer {:nowarn_function, enable_trace: 1}
291 @spec enable_trace(trace_flag) :: true
292 @doc """
293 Turns on the provided `trace_flag` for the current process.
294
295 See the third parameter of `:erlang.trace/3`/ for a list of flags and their effects.
296 Note that the `:cpu_timestamp` and `:tracer` flags are not supported in this function.
297
298 Always returns `true`.
299 """
300 def enable_trace(trace_flag) do
301 _ignore = trace_flag
302 :noop
303 end
304
305 @dialyzer {:nowarn_function, enable_trace: 2}
306 @spec enable_trace(pid, trace_flag) :: non_neg_integer
307 @doc """
308 Turns on the provided `trace_flag` for the specified `pid`.
309
310 See the third parameter of `:erlang.trace/3`/ for a list of flags and their effects.
311 Note that the `:cpu_timestamp` and `:tracer` flags are not supported in this function.
312
313 Always returns `true`.
314 """
315 def enable_trace(pid, trace_flag) do
316 _ignore = pid
317 _ignore = trace_flag
318 :noop
319 end
320
321 @dialyzer {:nowarn_function, disable_trace: 1}
322 @spec disable_trace(trace_flag) :: true
323 @doc """
324 Turns off the provided `trace_flag` for the current process.
325
326 See the third parameter of `:erlang.trace/3`/ for a list of flags and their effects.
327 Note that the `:cpu_timestamp` and `:tracer` flags are not supported in this function.
328
329 Always returns `true`.
330 """
331 def disable_trace(trace_flag) do
332 _ignore = trace_flag
333 :noop
334 end
335
336 @dialyzer {:nowarn_function, disable_trace: 2}
337 @spec disable_trace(pid, trace_flag) :: true
338 @doc """
339 Turns off the provided `trace_flag` for the specified `pid`.
340
341 See the third parameter of `:erlang.trace/3`/ for a list of flags and their effects.
342 Note that the `:cpu_timestamp` and `:tracer` flags are not supported in this function.
343
344 Always returns `true`.
345 """
346 def disable_trace(pid, trace_flag) do
347 _ignore = pid
348 _ignore = trace_flag
349 :noop
350 end
351
352 @dialyzer {:nowarn_function, trace: 2}
353 @spec trace(
354 disable_flags :: [trace_flag | tracer_trace_flag],
355 enable_flags :: [trace_flag | tracer_trace_flag]
356 ) :: boolean
357 @doc """
358 Atomically disables and enables a set of trace flags for the current process in one go.
359
360 Flags enabled in the `enable_flags` list will override duplicate flags in the `disable_flags` list.
361
362 See the third parameter of `:erlang.trace/3`/ for a list of flags and their effects.
363 Note that the `:cpu_timestamp` flag is not supported in this function, however
364 unlike the `enable_trace/1` and `disable_trace/1` functions, the `:tracer` flags are supported..
365
366 If no `:tracer` is specified, the same tracer as the process executing the match specification is used (not the meta tracer).
367 If that process doesn't have tracer either, then trace flags are ignored.
368 When using a tracer module, the module must be loaded before the match specification is executed. If it is not loaded, the match fails.
369
370 Returns `true` if any trace property was changed for the current process, otherwise `false`.
371 """
372 def trace(disable_flags, enable_flags) do
373 _ignore = disable_flags
374 _ignore = enable_flags
375 :noop
376 end
377
378 @dialyzer {:nowarn_function, trace: 3}
379 @spec trace(
380 disable_flags :: [trace_flag | tracer_trace_flag],
381 enable_flags :: [trace_flag | tracer_trace_flag]
382 ) :: boolean
383 @doc """
384 Atomically disables and enables a set of trace flags for the given `pid` in one go.
385
386 Flags enabled in the `enable_flags` list will override duplicate flags in the `disable_flags` list.
387
388 See the third parameter of `:erlang.trace/3`/ for a list of flags and their effects.
389 Note that the `:cpu_timestamp` flag is not supported in this function, however
390 unlike the `enable_trace/1` and `disable_trace/1` functions, the `:tracer` flags are supported..
391
392 If no `:tracer` is specified, the same tracer as the process executing the match specification is used (not the meta tracer).
393 If that process doesn't have tracer either, then trace flags are ignored.
394 When using a tracer module, the module must be loaded before the match specification is executed. If it is not loaded, the match fails.
395
396 Returns `true` if any trace property was changed for the given `pid`, otherwise `false`.
397 """
398 def trace(pid, disable_flags, enable_flags) do
399 _ignore = pid
400 _ignore = disable_flags
401 _ignore = enable_flags
402 :noop
403 end
404
405 ###
406 # SEQUENTIAL TRACING FUNCTIONS
407 ##
408
409 @type seq_token :: {integer, boolean, any, any, any}
410 @type seq_token_flag ::
411 :send
412 | :receive
413 | :print
414 | :timestamp
415 | :monotonic_timestamp
416 | :strict_monotonic_timestamp
417 @type seq_token_component ::
418 :label
419 | :serial
420 | seq_token_flag
421 @type seq_token_label_value :: any
422 @type seq_token_serial_number :: non_neg_integer
423 @type seq_token_previous_serial_number :: seq_token_serial_number
424 @type seq_token_current_serial_number :: seq_token_serial_number
425 @type seq_token_serial_value ::
426 {seq_token_previous_serial_number, seq_token_current_serial_number}
427 @type seq_token_value :: seq_token_label_value | seq_token_serial_value | boolean
428
429 @dialyzer {:nowarn_function, is_seq_trace: 0}
430 @spec is_seq_trace() :: boolean
431 @doc """
432 Returns `true` if a sequential trace token is set for the current process, otherwise `false`.
433 """
434 1 def is_seq_trace do
435 :noop
436 end
437
438 @dialyzer {:nowarn_function, set_seq_token: 2}
439 @spec set_seq_token(seq_token_component, seq_token_value) :: true | charlist
440 @doc """
441 Sets a label, serial number, or flag `token` to `value` for sequential tracing.
442
443 Acts like `:seq_trace.set_token/2`, except
444 returns `true` on success, and `'EXIT'` on error or bad argument.
445
446 Note that this function cannot be used to exclude message passing from the trace,
447 since that is normally accomplished by passing `[]` into `:seq_trace.set_token/1`
448 (however there is no `set_seq_token/1` allowed in match specs).
449
450 Note that the values set here cannot be introspected in
451 a match spec tracing context
452 (`get_seq_token/0` returns an opaque representation of the current trace token,
453 but there's no `get_seq_token/1` to inspect individual values).
454
455 For more information, consult `:seq_trace.set_token/2` docs.
456 """
457 def set_seq_token(token, value) do
458 _ignore = token
459 _ignore = value
460 :noop
461 end
462
463 @dialyzer {:nowarn_function, get_seq_token: 0}
464 @spec get_seq_token() :: seq_token | []
465 @doc """
466 Retreives the (opaque) value of the trace token for the current process.
467
468 If the current process is not being traced, returns `[]`.
469
470 Acts identically to `:seq_trace.get_token/0`. The docs say that the return value
471 can be passed back into `:seq_trace.set_token/1`. However,
472 in a tracing match spec context, there is no equivalent
473 (`set_seq_token/2` works, but there's no `set_seq_token/1`).
474 So I am unsure what this can be used for.
475
476 For more information, consult `:seq_trace.get_token/0` docs.
477 """
478 1 def get_seq_token do
479 :noop
480 end
481 end

lib/matcha/error.ex

27.2
11
171
8
Line Hits Source
0 defmodule Matcha.Error do
1 @moduledoc """
2 Standard behaviour for `Matcha` errors.
3 """
4
5 alias __MODULE__
6
7 @type message :: binary | charlist
8 @type error_problem :: {:error, message}
9 @type warning_problem :: {:warning, message}
10 @type problem :: error_problem | warning_problem
11 @type problems :: [problem]
12
13 @doc """
14 Generates the "prelude" text for errors in the struct this error handles
15 into a string displayable in an error message.
16 """
17 @callback format_prelude(struct()) :: binary
18
19 @doc """
20 Converts the struct this error handles
21 into a string displayable in an error message.
22 """
23 @callback format_source(struct()) :: binary
24
25 0 defmacro __using__(source_type: source_type) do
26 quote do
27 @behaviour unquote(__MODULE__)
28
29 defexception [:source, :problems, details: nil]
30
31 0 @type t :: %unquote(__CALLER__.module){
32 source: unquote(source_type),
33 problems: Matcha.Error.problems()
34 }
35
36 @spec message(t()) :: binary
37 @doc """
38 Produces a human-readable message from the given `error`.
39 """
40 def message(%__MODULE__{} = error) do
41 [
42 Enum.join([format_prelude(error.source), error.details], ": "),
43 " ",
44 String.replace(format_source(error.source), "\n", "\n ")
45 | Enum.map(error.problems, &unquote(__MODULE__).format_problem/1)
46 ]
47 |> Enum.join("\n ")
48 end
49
50 defoverridable(message: 1)
51 end
52 end
53
54 57 def format_problem({type, problem}), do: " #{type}: #{problem}"
55 end
56
57 defmodule Matcha.Rewrite.Error do
58 @moduledoc """
59 Error raised when rewriting Elixir code into a match pattern/spec.
60 """
61
62 alias Matcha.Error
63 alias Matcha.Rewrite
64
65 use Error, source_type: Rewrite.t()
66
67 @impl Error
68 @spec format_prelude(Rewrite.t()) :: binary
69 57 def format_prelude(%Rewrite{} = _rewrite) do
70 "found problems rewriting code into a match spec"
71 end
72
73 @impl Error
74 @spec format_source(Rewrite.t()) :: binary
75 def format_source(%Rewrite{} = rewrite) do
76 57 Macro.to_string(Rewrite.code(rewrite))
77 end
78 end
79
80 defmodule Matcha.Pattern.Error do
81 @moduledoc """
82 Error raised when a `Matcha.Pattern` is invalid.
83 """
84
85 alias Matcha.Error
86 alias Matcha.Pattern
87
88 use Error, source_type: Pattern.t()
89
90 @impl Error
91 @spec format_prelude(Pattern.t()) :: binary
92 0 def format_prelude(%Pattern{} = _pattern) do
93 "found problems with match pattern"
94 end
95
96 @impl Error
97 @spec format_source(Pattern.t()) :: binary
98 def format_source(%Pattern{} = pattern) do
99 0 inspect(Pattern.raw(pattern))
100 end
101 end
102
103 defmodule Matcha.Filter.Error do
104 @moduledoc """
105 Error raised when a `Matcha.Filter` is invalid.
106 """
107
108 alias Matcha.Error
109 alias Matcha.Filter
110
111 use Error, source_type: Filter.t()
112
113 @impl Error
114 @spec format_prelude(Filter.t()) :: binary
115 0 def format_prelude(%Filter{} = _pattern) do
116 "found problems with match filter"
117 end
118
119 @impl Error
120 @spec format_source(Filter.t()) :: binary
121 def format_source(%Filter{} = pattern) do
122 0 inspect(Filter.raw(pattern))
123 end
124 end
125
126 defmodule Matcha.Spec.Error do
127 @moduledoc """
128 Error raised when a `Matcha.Spec` is invalid.
129 """
130
131 alias Matcha.Error
132 alias Matcha.Spec
133
134 use Error, source_type: Spec.t()
135
136 @impl Error
137 @spec format_prelude(Spec.t()) :: binary
138 0 def format_prelude(%Spec{} = _spec) do
139 "found problems with match spec"
140 end
141
142 @impl Error
143 @spec format_source(Spec.t()) :: binary
144 def format_source(%Spec{} = spec) do
145 0 inspect(Spec.raw(spec))
146 end
147 end

lib/matcha/filter.ex

50.0
26
20
13
Line Hits Source
0 defmodule Matcha.Filter do
1 @moduledoc """
2 About filters.
3 """
4
5 alias __MODULE__
6
7 alias Matcha.Context
8 alias Matcha.Error
9 alias Matcha.Rewrite
10 alias Matcha.Raw
11
12 alias Matcha.Spec
13
14 import Kernel, except: [match?: 2]
15
16 defstruct [:raw, :bindings]
17
18 @test_spec_context Matcha.Context.Match
19 @default_to_spec_context @test_spec_context
20
21 @type bindings :: %{atom() => term()}
22
23 @type t :: %__MODULE__{
24 raw: Raw.filter(),
25 bindings: bindings()
26 }
27
28 @spec raw(t()) :: Raw.filter()
29 def raw(%__MODULE__{raw: raw} = _filter) do
30 3 raw
31 end
32
33 @spec bindings(t()) :: bindings()
34 def bindings(%__MODULE__{bindings: bindings} = _filter) do
35 0 bindings
36 end
37
38 @spec matches(t(), Enumerable.t()) :: Enumerable.t()
39 def matches(%__MODULE__{} = filter, enumerable) do
40 0 with {:ok, spec} <- to_spec(@test_spec_context, filter) do
41 0 Spec.run(spec, enumerable)
42 end
43 end
44
45 @spec match?(t(), term()) :: boolean()
46 def match?(%__MODULE__{} = filter, term) do
47 2 case do_test(filter, term) do
48 1 {:ok, {:matched, ^term}} -> true
49 1 _ -> false
50 end
51 end
52
53 @spec match!(t(), term()) :: term() | no_return()
54 def match!(%__MODULE__{} = filter, term) do
55 0 if match?(filter, term) do
56 0 term
57 else
58 0 raise MatchError, term: term
59 end
60 end
61
62 @spec matched_variables(t(), term()) :: %{atom() => term()} | nil
63 def matched_variables(%__MODULE__{} = filter, term) do
64 0 with {:ok, spec} <- Rewrite.filter_to_matched_variables_spec(@test_spec_context, filter) do
65 0 case Context.test(spec, term) do
66 {:ok, {:matched, results}} ->
67 0 Map.new(
68 0 for {binding, index} <- filter.bindings, index > 0 do
69 {binding, Enum.at(results, index - 1)}
70 end
71 )
72
73 0 {:ok, :no_match} ->
74 nil
75 end
76 end
77 end
78
79 @spec to_spec(context :: Context.t(), t()) :: {:ok, Spec.t()} | {:error, Error.problems()}
80 def to_spec(context \\ @default_to_spec_context, %__MODULE__{} = filter) do
81 context
82 |> Context.resolve()
83 3 |> Rewrite.filter_to_spec(filter)
84 end
85
86 @spec validate(t()) :: {:ok, t()} | {:error, Error.problems()}
87 def validate(%__MODULE__{} = filter) do
88 1 case do_test(filter) do
89 1 {:ok, _result} -> {:ok, filter}
90 0 {:error, problems} -> {:error, problems}
91 end
92 end
93
94 @spec validate!(t()) :: t() | no_return()
95 def validate!(%__MODULE__{} = filter) do
96 1 case validate(filter) do
97 {:ok, filter} ->
98 1 filter
99
100 {:error, problems} ->
101 0 raise Filter.Error,
102 source: filter,
103 details: "when validating filter",
104 problems: problems
105 end
106 end
107
108 defp do_test(%__MODULE__{} = filter) do
109 1 with {:ok, spec} <- to_spec(@test_spec_context, filter) do
110 1 Context.test(spec)
111 end
112 end
113
114 defp do_test(%__MODULE__{} = filter, match_target) do
115 2 with {:ok, spec} <- to_spec(@test_spec_context, filter) do
116 2 Context.test(spec, match_target)
117 end
118 end
119 end

lib/matcha/helpers.ex

66.6
3
63
1
Line Hits Source
0 defmodule Matcha.Helpers do
1 @moduledoc false
2
3 # def module_exists?(module) do
4 # :code.module_status(module) == :loaded
5 # end
6
7 # def module_exists?(module) do
8 # module.__info__(:module)
9 # rescue
10 # _ -> false
11 # else
12 # ^module -> true
13 # _ -> false
14 # end
15
16 # def function_exists?(module, function) do
17 # module_exists?(module) and
18 # Keyword.has_key?(module.__info__(:functions), function)
19 # end
20
21 # def function_with_arity_exists?(module, function, arity) do
22 # {module, function, arity} |> dbg()
23 # function_exported?(module, function, arity) |> dbg()
24 # end
25
26 def application_loaded?(name) do
27 0 Application.loaded_applications() |> Enum.find(&match?({^name, _, _}, &1))
28 end
29
30 @spec erlang_version() :: non_neg_integer()
31 def erlang_version do
32 14 :erlang.system_info(:otp_release) |> List.to_integer()
33 end
34
35 @spec elixir_version() :: Version.t()
36 def elixir_version do
37 49 System.version() |> Version.parse!()
38 end
39 end

lib/matcha/inspect.ex

0.0
7
0
7
Line Hits Source
0 defimpl Inspect, for: Matcha.Pattern do
1 @moduledoc false
2
3 import Inspect.Algebra
4
5 alias Matcha.Pattern
6
7 def inspect(%Pattern{} = pattern, options) do
8 0 concat([
9 "##{inspect(@for)}<",
10 break(""),
11 to_doc(Pattern.raw(pattern), options),
12 ",",
13 break(" "),
14 0 string("bindings: #{inspect(pattern.bindings)}"),
15 break(""),
16 ">"
17 ])
18 end
19 end
20
21 defimpl Inspect, for: Matcha.Filter do
22 @moduledoc false
23
24 import Inspect.Algebra
25
26 alias Matcha.Filter
27
28 def inspect(%Filter{} = pattern, options) do
29 0 concat([
30 "##{inspect(@for)}<",
31 break(""),
32 to_doc(Filter.raw(pattern), options),
33 ",",
34 break(" "),
35 0 string("bindings: #{inspect(pattern.bindings)}"),
36 break(""),
37 ">"
38 ])
39 end
40 end
41
42 defimpl Inspect, for: Matcha.Spec do
43 @moduledoc false
44
45 import Inspect.Algebra
46
47 alias Matcha.Spec
48
49 def inspect(%Spec{} = spec, options) do
50 0 concat([
51 "##{inspect(@for)}<",
52 break(""),
53 to_doc(Spec.raw(spec), options),
54 ",",
55 break(" "),
56 0 string("context: #{inspect(spec.context)}"),
57 ",",
58 break(" "),
59 0 string("bindings: #{inspect(spec.bindings)}"),
60 break(""),
61 ">"
62 ])
63 end
64 end

lib/matcha/pattern.ex

50.0
26
20
13
Line Hits Source
0 defmodule Matcha.Pattern do
1 @moduledoc """
2 About patterns.
3 """
4
5 alias __MODULE__
6
7 alias Matcha.Context
8 alias Matcha.Error
9 alias Matcha.Rewrite
10 alias Matcha.Raw
11
12 alias Matcha.Spec
13
14 import Kernel, except: [match?: 2]
15
16 defstruct [:raw, :bindings]
17
18 @test_spec_context Matcha.Context.Match
19 @default_to_spec_context @test_spec_context
20
21 @type t :: %__MODULE__{
22 raw: Raw.pattern(),
23 bindings: %{atom() => non_neg_integer()}
24 }
25
26 @spec raw(t()) :: Raw.pattern()
27 def raw(%__MODULE__{raw: raw} = _pattern) do
28 3 raw
29 end
30
31 @spec bindings(t()) :: %{atom() => non_neg_integer()}
32 def bindings(%__MODULE__{bindings: bindings} = _pattern) do
33 0 bindings
34 end
35
36 @spec matches(t(), Enumerable.t()) :: Enumerable.t()
37 def matches(%__MODULE__{} = pattern, enumerable) do
38 0 with {:ok, spec} <- to_spec(@test_spec_context, pattern) do
39 0 Spec.run(spec, enumerable)
40 end
41 end
42
43 @spec match?(t(), term()) :: boolean()
44 def match?(%__MODULE__{} = pattern, term) do
45 2 case do_test(pattern, term) do
46 1 {:ok, {:matched, ^term}} -> true
47 1 _ -> false
48 end
49 end
50
51 @spec match!(t(), term()) :: term() | no_return()
52 def match!(%__MODULE__{} = pattern, term) do
53 0 if match?(pattern, term) do
54 0 term
55 else
56 0 raise MatchError, term: term
57 end
58 end
59
60 @spec matched_variables(t(), term()) :: %{atom() => term()} | nil
61 def matched_variables(%__MODULE__{} = pattern, term) do
62 0 with {:ok, spec} <- Rewrite.pattern_to_matched_variables_spec(@test_spec_context, pattern) do
63 0 case Context.test(spec, term) do
64 {:ok, {:matched, results}} ->
65 0 Map.new(
66 0 for {binding, index} <- pattern.bindings, index > 0 do
67 {binding, Enum.at(results, index - 1)}
68 end
69 )
70
71 0 {:ok, :no_match} ->
72 nil
73 end
74 end
75 end
76
77 @spec to_spec(context :: Context.t(), t()) :: {:ok, Spec.t()} | {:error, Error.problems()}
78 def to_spec(context \\ @default_to_spec_context, %__MODULE__{} = pattern) do
79 context
80 |> Context.resolve()
81 3 |> Rewrite.pattern_to_spec(pattern)
82 end
83
84 @spec validate(t()) :: {:ok, t()} | {:error, Error.problems()}
85 def validate(%__MODULE__{} = pattern) do
86 1 case do_test(pattern) do
87 1 {:ok, _result} -> {:ok, pattern}
88 0 {:error, problems} -> {:error, problems}
89 end
90 end
91
92 @spec validate!(t()) :: t() | no_return()
93 def validate!(%__MODULE__{} = pattern) do
94 1 case validate(pattern) do
95 {:ok, pattern} ->
96 1 pattern
97
98 {:error, problems} ->
99 0 raise Pattern.Error,
100 source: pattern,
101 details: "when validating pattern",
102 problems: problems
103 end
104 end
105
106 defp do_test(%__MODULE__{} = pattern) do
107 1 with {:ok, spec} <- to_spec(@test_spec_context, pattern) do
108 1 Context.test(spec)
109 end
110 end
111
112 defp do_test(%__MODULE__{} = pattern, match_target) do
113 2 with {:ok, spec} <- to_spec(@test_spec_context, pattern) do
114 2 Context.test(spec, match_target)
115 end
116 end
117 end

lib/matcha/rewrite.ex

83.5
85
19591
14
Line Hits Source
0 defmodule Matcha.Rewrite do
1 @moduledoc """
2 The compiler used to expand and rewrite Elixir code into `Matcha` constructs.
3 """
4
5 alias Matcha.Rewrite
6 import Rewrite.AST, only: :macros
7
8 alias Matcha.Context
9 alias Matcha.Error
10 alias Matcha.Raw
11
12 alias Matcha.Pattern
13 alias Matcha.Filter
14 alias Matcha.Spec
15
16 defstruct [:env, :context, :code, bindings: %{vars: %{}, count: 0}, guards: []]
17
18 @type bindings :: %{Rewrite.Bindings.var_ref() => Rewrite.Bindings.var_binding()}
19
20 @type t :: %__MODULE__{
21 env: Macro.Env.t(),
22 context: Context.t() | nil,
23 code: Macro.t(),
24 bindings: %{
25 vars: bindings(),
26 count: non_neg_integer()
27 }
28 }
29
30 @spec code(t()) :: Raw.uncompiled()
31 def code(%Rewrite{code: code} = _rewrite) do
32 57 code
33 end
34
35 # Handle change in private :elixir_expand API around v1.13
36 if function_exported?(:elixir_expand, :expand, 3) do
37 def perform_expansion(ast, env) do
38 1149 {ast, _ex, _env} = :elixir_expand.expand(ast, :elixir_env.env_to_ex(env), env)
39 1139 ast
40 end
41 else
42 def perform_expansion(ast, env) do
43 {ast, _env} = :elixir_expand.expand(ast, env)
44 ast
45 end
46 end
47
48 # Rewrite Elixir AST to Macha constructs
49
50 def build_pattern(env = %Macro.Env{}, pattern) do
51 1 {raw, bindings} =
52 %Rewrite{env: env, code: pattern}
53 |> pattern(pattern)
54
55 1 raw = Macro.escape(raw, unquote: true)
56 1 bindings = Macro.escape(bindings, unquote: true)
57
58 quote location: :keep do
59 %Matcha.Pattern{
60 raw: unquote(raw),
61 bindings: unquote(bindings)
62 }
63 |> Matcha.Pattern.validate!()
64 end
65 end
66
67 def pattern(rewrite = %Rewrite{}, pattern) do
68 1 pattern = expand_pattern(rewrite, pattern)
69 1 rewrite_pattern(rewrite, pattern)
70 end
71
72 defp expand_pattern(rewrite, pattern) do
73 1 perform_expansion(pattern, Macro.Env.to_match(rewrite.env))
74 end
75
76 defp rewrite_pattern(rewrite, pattern) do
77 1 {rewrite, pattern} = Rewrite.Bindings.rewrite(rewrite, pattern)
78 1 {Rewrite.Match.rewrite(rewrite, pattern), rewrite.bindings.vars}
79 end
80
81 def build_filter(env = %Macro.Env{}, filter) do
82 1 {raw, bindings} =
83 %Rewrite{env: env, code: filter}
84 |> filter(filter)
85
86 1 raw = Macro.escape(raw, unquote: true)
87 1 bindings = Macro.escape(bindings, unquote: true)
88
89 quote location: :keep do
90 %Matcha.Filter{
91 raw: unquote(raw),
92 bindings: unquote(bindings)
93 }
94 |> Matcha.Filter.validate!()
95 end
96 end
97
98 def filter(rewrite = %Rewrite{}, filter) do
99 1 filter = expand_filter(rewrite, filter)
100 1 {rewrite, filter} = rewrite_filter(rewrite, filter)
101 1 {filter, rewrite.bindings.vars}
102 end
103
104 defp expand_filter(rewrite, filter) do
105 1 {_, bound_vars} =
106 Macro.prewalk(filter, [], fn
107 {_ref, _, _} = var, bound_vars when is_named_var(var) ->
108 5 if Rewrite.Bindings.outer_var?(rewrite, var) do
109 {var, bound_vars}
110 else
111 {var, [var | bound_vars]}
112 end
113
114 3 other, bound_vars ->
115 {other, bound_vars}
116 end)
117
118 # Expand with a body that uses all bound vars to prevent warnings
119 1 clause = {:->, [], [[filter], [{:{}, [], Enum.uniq(bound_vars)}]]}
120 1 _ = handle_pre_expansion!(clause, rewrite)
121
122 1 elixir_ast =
123 quote do
124 # keep up to date with the replacements in Matcha.Rewrite.Kernel
125 import Kernel,
126 except: [
127 and: 2,
128 is_boolean: 1,
129 is_exception: 1,
130 is_exception: 2,
131 is_struct: 1,
132 is_struct: 2,
133 or: 2
134 ]
135
136 # use special variants of kernel macros, that otherwise wouldn't work in match spec bodies
137 import Matcha.Rewrite.Kernel, warn: false
138 # mimic a `fn` definition for purposes of expanding clauses
139 unquote({:fn, [], [clause]})
140 end
141
142 1 expansion = perform_expansion(elixir_ast, rewrite.env)
143
144 1 {_, clause} =
145 Macro.prewalk(expansion, nil, fn
146 1 {:fn, [], [clause]}, nil -> {nil, clause}
147 3 other, clause -> {other, clause}
148 end)
149
150 1 {:->, _, [[filter], _]} = clause
151 1 :elixir_utils.extract_guards(filter)
152 end
153
154 defp rewrite_filter(rewrite, {match, guards}) do
155 1 {rewrite, match} = Rewrite.Bindings.rewrite(rewrite, match)
156 1 match = Rewrite.Match.rewrite(rewrite, match)
157 1 guards = Rewrite.Guards.rewrite(guards, rewrite)
158 {rewrite, {match, guards}}
159 end
160
161 def build_spec(env = %Macro.Env{}, context, clauses) do
162 578 context =
163 context
164 |> perform_expansion(env)
165 |> Context.resolve()
166
167 576 {raw, bindings} =
168 %Rewrite{env: env, context: context, code: clauses}
169 |> spec(clauses)
170 |> Enum.with_index()
171 543 |> Enum.reduce({[], %{}}, fn {{clause, bindings}, index}, {raw, all_bindings} ->
172 {[clause | raw], Map.put(all_bindings, index, bindings)}
173 end)
174
175 535 raw = Macro.escape(:lists.reverse(raw), unquote: true)
176 535 bindings = Macro.escape(bindings)
177
178 quote location: :keep do
179 %Matcha.Spec{
180 raw: unquote(raw),
181 context: unquote(context),
182 bindings: unquote(bindings)
183 }
184 |> Matcha.Spec.validate!()
185 end
186 end
187
188 def spec(rewrite = %Rewrite{}, spec) do
189 expand_spec_clauses(rewrite, spec)
190 562 |> Enum.map(&Rewrite.Clause.new(&1, rewrite))
191 576 |> Enum.map(&Rewrite.Clause.rewrite(&1, rewrite))
192 end
193
194 defp expand_spec_clauses(rewrite, clauses) do
195 576 clauses =
196 for clause <- clauses do
197 584 handle_pre_expansion!(clause, rewrite)
198 end
199
200 564 elixir_ast =
201 quote do
202 # keep up to date with the replacements in Matcha.Rewrite.Kernel
203 import Kernel,
204 except: [
205 and: 2,
206 is_boolean: 1,
207 is_exception: 1,
208 is_exception: 2,
209 is_struct: 1,
210 is_struct: 2,
211 or: 2
212 ]
213
214 # use special variants of kernel macros, that otherwise wouldn't work in match spec bodies
215 import Matcha.Rewrite.Kernel, warn: false
216 # make special functions for this context available unadorned during expansion
217 564 import unquote(rewrite.context), warn: false
218 # mimic a `fn` definition for purposes of expanding clauses
219 unquote({:fn, [], clauses})
220 end
221
222 564 expansion = perform_expansion(elixir_ast, rewrite.env)
223
224 554 {_, clauses} =
225 Macro.prewalk(expansion, nil, fn
226 554 {:fn, [], clauses}, nil -> {nil, clauses}
227 2216 other, clauses -> {other, clauses}
228 end)
229
230 554 clauses
231 end
232
233 defp handle_pre_expansion!(clause, rewrite) do
234 585 handle_in_operator!(clause, rewrite)
235 end
236
237 defp handle_in_operator!(clause, rewrite) do
238 585 Macro.postwalk(clause, fn
239 {:in, _, [left, right]} = ast ->
240 47 cond do
241 # Expand Kernel list-generating sigil literals in guard contexts specially
242 47 match?({:sigil_C, _, _}, right) ->
243 0 sigil_expansion = perform_expansion(right, %{rewrite.env | context: :guard})
244 0 {:in, [], [left, sigil_expansion]}
245
246 47 match?({:sigil_c, _, _}, right) ->
247 0 sigil_expansion = perform_expansion(right, %{rewrite.env | context: :guard})
248 0 {:in, [], [left, sigil_expansion]}
249
250 47 match?({:sigil_W, _, _}, right) ->
251 0 sigil_expansion = perform_expansion(right, %{rewrite.env | context: :guard})
252 0 {:in, [], [left, sigil_expansion]}
253
254 47 match?({:sigil_w, _, _}, right) ->
255 5 sigil_expansion = perform_expansion(right, %{rewrite.env | context: :guard})
256 5 {:in, [], [left, sigil_expansion]}
257
258 # Allow literal lists
259 42 is_list(right) and Macro.quoted_literal?(right) ->
260 8 ast
261
262 # Literal range syntax
263 34 match?({:.., _, [_left, _right | []]}, right) ->
264 12 ast
265
266 # Literal range with step syntax
267 22 match?({:"..//", _, [_left, _right, _step | []]}, right) ->
268 10 ast
269
270 12 true ->
271 12 raise ArgumentError,
272 message:
273 Enum.join(
274 [
275 "invalid right argument for operator \"in\"",
276 "it expects a compile-time proper list or compile-time range on the right side when used in match spec expressions",
277 "got: `#{Macro.to_string(right)}`"
278 ],
279 ", "
280 )
281 end
282
283 ast ->
284 4992 ast
285 end)
286 end
287
288 ###
289 # Rewrite problems
290 ##
291
292 @spec problems(problems) :: Error.problems()
293 when problems: [{type, description}],
294 type: :error | :warning,
295 description: charlist() | binary
296 def problems(problems) do
297 0 Enum.map(problems, &problem/1)
298 end
299
300 @spec problem({type, description}) :: Error.problem()
301 when type: :error | :warning, description: charlist() | binary
302 def problem(problem)
303
304 0 def problem({type, description}) when type in [:error, :warning] and is_list(description) do
305 {type, List.to_string(description)}
306 end
307
308 0 def problem({type, description}) when type in [:error, :warning] and is_binary(description) do
309 {type, description}
310 end
311
312 ###
313 # Rewrite matches and filters to specs
314 ##
315
316 @spec pattern_to_spec(Context.t(), Pattern.t()) :: {:ok, Spec.t()} | {:error, Error.problems()}
317 def pattern_to_spec(context, %Pattern{} = pattern) do
318 %Spec{
319 raw: [{Pattern.raw(pattern), [], [Raw.__match_all__()]}],
320 context: Context.resolve(context),
321 3 bindings: %{0 => pattern.bindings}
322 }
323 3 |> Spec.validate()
324 end
325
326 @spec pattern_to_matched_variables_spec(Context.t(), Pattern.t()) ::
327 {:ok, Spec.t()} | {:error, Error.problems()}
328 def pattern_to_matched_variables_spec(context, %Pattern{} = pattern) do
329 %Spec{
330 raw: [{Pattern.raw(pattern), [], [Raw.__all_matches__()]}],
331 context: Context.resolve(context),
332 0 bindings: %{0 => pattern.bindings}
333 }
334 0 |> Spec.validate()
335 end
336
337 @spec filter_to_spec(Context.t(), Filter.t()) :: {:ok, Spec.t()} | {:error, Error.problems()}
338 def filter_to_spec(context, %Filter{} = filter) do
339 3 {match, conditions} = Filter.raw(filter)
340
341 %Spec{
342 raw: [{match, conditions, [Raw.__match_all__()]}],
343 context: Context.resolve(context),
344 3 bindings: %{0 => filter.bindings}
345 }
346 3 |> Spec.validate()
347 end
348
349 @spec filter_to_matched_variables_spec(Context.t(), Filter.t()) ::
350 {:ok, Spec.t()} | {:error, Error.problems()}
351 def filter_to_matched_variables_spec(context, %Filter{} = filter) do
352 0 {match, conditions} = Filter.raw(filter)
353
354 %Spec{
355 raw: [{match, conditions, [Raw.__all_matches__()]}],
356 context: Context.resolve(context),
357 0 bindings: %{0 => filter.bindings}
358 }
359 0 |> Spec.validate()
360 end
361 end

lib/matcha/rewrite/ast.ex

0.0
7
0
7
Line Hits Source
0 defmodule Matcha.Rewrite.AST do
1 @moduledoc """
2 Helpers for reasoning about expanded Elixir AST.
3 """
4
5 0 defguard is_var(var)
6 when is_tuple(var) and is_atom(elem(var, 0)) and is_list(elem(var, 1)) and
7 is_atom(elem(var, 2))
8
9 0 defguard is_named_var(var)
10 when is_var(var) and elem(var, 0) != :_
11
12 0 defguard is_call(call)
13 when (is_atom(elem(call, 0)) or is_tuple(elem(call, 0))) and is_list(elem(call, 1)) and
14 is_list(elem(call, 2))
15
16 0 defguard is_invocation(invocation)
17 when is_call(invocation) and elem(invocation, 0) == :. and is_list(elem(invocation, 1)) and
18 length(elem(invocation, 2)) == 2 and
19 is_atom(hd(elem(invocation, 2))) and is_atom(hd(tl(elem(invocation, 2))))
20
21 0 defguard is_remote_call(call)
22 when is_invocation(elem(call, 0)) and is_list(elem(call, 1)) and
23 is_list(elem(call, 2))
24
25 0 defguard is_atomic_literal(ast)
26 when is_atom(ast) or is_integer(ast) or is_float(ast) or is_binary(ast)
27
28 # or ast == [] or ast == {} or ast == %{}
29
30 0 defguard is_non_literal(ast)
31 when is_list(ast) or
32 (is_tuple(ast) and tuple_size(ast) == 2) or is_call(ast) or is_var(ast)
33
34 # def literal?(data)
35
36 # def literal?(literal) when is_atomic_literal(literal) do
37 # true
38 # end
39
40 # def literal?(list) when is_list(list) do
41 # Enum.all?(list, &literal?/1)
42 # end
43
44 # def literal?(tuple) when is_tuple(tuple) do
45 # tuple
46 # |> Tuple.to_list()
47 # |> literal?
48 # end
49
50 # def literal?(map) when is_map(map) do
51 # literal?(Map.keys(map)) and literal?(Map.values(map))
52 # end
53 end

lib/matcha/rewrite/bindings.ex

73.0
89
15394
24
Line Hits Source
0 defmodule Matcha.Rewrite.Bindings do
1 @moduledoc """
2 Rewrites expanded Elixir variable bindings into Erlang match specification variables.
3
4 In cases where variable bindings are not possible in ms syntax, specifically nested bindings,
5 converts nested variables into a sequence of extra guards in terms of an allowed outer binding.
6 """
7
8 alias Matcha.Rewrite
9 alias Matcha.Raw
10
11 import Matcha.Rewrite.AST, only: :macros
12
13 # TODO: we can probably re-write "outer bounds" checks
14 # simply by adding them to the rewrite bindings at start
15
16 # TODO: we can probably make this recursive at a higher level
17 # so that we are not checking for assignment both at top-level
18 # and while expanding within destructuring/assignment
19
20 @type var_ast :: {atom, list, atom | nil}
21 @type var_ref :: atom
22 @type var_binding :: non_neg_integer | var_ast
23
24 @spec bound?(Matcha.Rewrite.t(), var_ref()) :: boolean
25 def bound?(rewrite = %Rewrite{} = %Rewrite{}, ref) do
26 2288 get(rewrite, ref) != nil
27 end
28
29 @spec get(Matcha.Rewrite.t(), var_ref()) :: var_binding()
30 def get(rewrite = %Rewrite{}, ref) do
31 3888 rewrite.bindings.vars[ref]
32 end
33
34 @spec outer_var?(Matcha.Rewrite.t(), var_ast) :: boolean
35 def outer_var?(rewrite, var)
36
37 def outer_var?(rewrite = %Rewrite{}, {ref, _, context}) do
38 692 Macro.Env.has_var?(rewrite.env, {ref, context})
39 end
40
41 0 def outer_var?(_rewrite = %Rewrite{}, _) do
42 false
43 end
44
45 def bound_var_to_source(_rewrite, 0) do
46 3 Raw.__match_all__()
47 end
48
49 def bound_var_to_source(_rewrite, integer) when is_integer(integer) and integer > 0 do
50 1591 :"$#{integer}"
51 end
52
53 def bound_var_to_source(_rewrite, expr) do
54 5 expr
55 end
56
57 @spec rewrite(Matcha.Rewrite.t(), Macro.t()) :: Macro.t()
58 def rewrite(rewrite, ast)
59
60 def rewrite(rewrite = %Rewrite{}, {:=, _, [{ref, _, _} = var, match]}) when is_named_var(var) do
61 0 rewrite = bind_toplevel_match(rewrite, ref)
62 0 do_rewrite(rewrite, match)
63 end
64
65 def rewrite(rewrite = %Rewrite{}, {:=, _, [match, {ref, _, _} = var]}) when is_named_var(var) do
66 3 rewrite = bind_toplevel_match(rewrite, ref)
67 3 do_rewrite(rewrite, match)
68 end
69
70 def rewrite(rewrite = %Rewrite{}, match) do
71 560 do_rewrite(rewrite, match)
72 end
73
74 @spec do_rewrite(Matcha.Rewrite.t(), Macro.t()) :: Macro.t()
75 def do_rewrite(rewrite = %Rewrite{}, match) do
76 563 {ast, rewrite} =
77 Macro.prewalk(match, rewrite, fn
78 {:=, _, [left, right]}, rewrite when is_named_var(left) and is_named_var(right) ->
79 7 if outer_var?(rewrite, left) or
80 7 outer_var?(rewrite, right) do
81 1 do_rewrite_outer_assignment(rewrite, {left, right})
82 else
83 6 do_rewrite_variable_match_assignment(rewrite, {left, right})
84 end
85
86 {:=, _, [var = {ref, _, _}, expression]}, rewrite when is_named_var(var) ->
87 13 if outer_var?(rewrite, var) do
88 4 raise_match_on_outer_var_error!(rewrite, var, expression)
89 else
90 9 rewrite = bind_var(rewrite, ref)
91
92 9 do_rewrite_match_binding(
93 rewrite,
94 bound_var_to_source(rewrite, get(rewrite, ref)),
95 expression
96 )
97 end
98
99 {:=, _, [expression, var = {ref, _, _}]}, rewrite when is_named_var(var) ->
100 6 if outer_var?(rewrite, var) do
101 5 raise_match_on_outer_var_error!(rewrite, var, expression)
102 else
103 1 rewrite = bind_var(rewrite, ref)
104
105 1 do_rewrite_match_binding(
106 rewrite,
107 bound_var_to_source(rewrite, get(rewrite, ref)),
108 expression
109 )
110 end
111
112 {ref, _, _} = var, rewrite when is_named_var(var) ->
113 650 if outer_var?(rewrite, var) do
114 {var, rewrite}
115 else
116 {var, bind_var(rewrite, ref)}
117 end
118
119 16 {:^, _, [var]}, rewrite when is_named_var(var) ->
120 {var, rewrite}
121
122 297 other, rewrite ->
123 {other, rewrite}
124 end)
125
126 {rewrite, ast}
127 end
128
129 @spec raise_match_on_outer_var_error!(Rewrite.t(), var_ast(), Macro.t()) ::
130 no_return()
131 def raise_match_on_outer_var_error!(rewrite = %Rewrite{}, var, expression) do
132 9 raise Rewrite.Error,
133 source: rewrite,
134 details: "when binding variables",
135 problems: [
136 error:
137 "cannot match `#{Macro.to_string(var)}` to `#{Macro.to_string(expression)}`:" <>
138 " `#{Macro.to_string(var)}` is already bound outside of the match spec"
139 ]
140 end
141
142 @spec raise_pin_on_missing_outer_var_error!(Rewrite.t(), var_ast(), Macro.t()) ::
143 no_return()
144 def raise_pin_on_missing_outer_var_error!(rewrite = %Rewrite{}, var, expression) do
145 0 raise Rewrite.Error,
146 source: rewrite,
147 details: "when binding variables",
148 problems: [
149 error:
150 "undefined variable #{Macro.to_string(expression)}. " <>
151 "No variable \"#{Macro.to_string(var)}\" has been defined before the current pattern"
152 ]
153 end
154
155 @spec bind_toplevel_match(Matcha.Rewrite.t(), Macro.t()) :: Matcha.Rewrite.t()
156 def bind_toplevel_match(rewrite = %Rewrite{}, ref) do
157 3 if bound?(rewrite, ref) do
158 0 rewrite
159 else
160 3 var = 0
161 3 bindings = %{rewrite.bindings | vars: Map.put(rewrite.bindings.vars, ref, var)}
162 3 %{rewrite | bindings: bindings}
163 end
164 end
165
166 @spec bind_var(Matcha.Rewrite.t(), var_ref()) :: Matcha.Rewrite.t()
167 def bind_var(rewrite = %Rewrite{}, ref, value \\ nil) do
168 665 if bound?(rewrite, ref) do
169 6 rewrite
170 else
171 659 bindings =
172 if value do
173 %{
174 5 rewrite.bindings
175 5 | vars: Map.put(rewrite.bindings.vars, ref, value)
176 }
177 else
178 654 count = rewrite.bindings.count + 1
179 654 var = count
180
181 %{
182 654 rewrite.bindings
183 654 | vars: Map.put(rewrite.bindings.vars, ref, var),
184 count: count
185 }
186 end
187
188 659 %{rewrite | bindings: bindings}
189 end
190 end
191
192 @spec rebind_var(Matcha.Rewrite.t(), var_ref(), var_ref()) ::
193 Matcha.Rewrite.t()
194 def rebind_var(rewrite = %Rewrite{}, ref, new_ref) do
195 6 var = Map.get(rewrite.bindings.vars, ref)
196 6 bindings = %{rewrite.bindings | vars: Map.put(rewrite.bindings.vars, new_ref, var)}
197 6 %{rewrite | bindings: bindings}
198 end
199
200 @spec do_rewrite_outer_assignment(Matcha.Rewrite.t(), Macro.t()) ::
201 {Macro.t(), Matcha.Rewrite.t()}
202 def do_rewrite_outer_assignment(
203 rewrite = %Rewrite{},
204 {{left_ref, _, _} = left, {right_ref, _, _} = right}
205 ) do
206 1 cond do
207 outer_var?(rewrite, left) ->
208 0 rewrite = bind_var(rewrite, right_ref, left)
209 {left, rewrite}
210
211 1 outer_var?(rewrite, right) ->
212 1 rewrite = bind_var(rewrite, left_ref, right)
213 {right, rewrite}
214 end
215 end
216
217 @spec do_rewrite_variable_match_assignment(Matcha.Rewrite.t(), Macro.t()) ::
218 {Macro.t(), Matcha.Rewrite.t()}
219 def do_rewrite_variable_match_assignment(
220 rewrite = %Rewrite{},
221 {{left_ref, _, _} = left, {right_ref, _, _} = right}
222 ) do
223 6 cond do
224 bound?(rewrite, left_ref) ->
225 3 rewrite = rebind_var(rewrite, left_ref, right_ref)
226 {left, rewrite}
227
228 3 bound?(rewrite, right_ref) ->
229 2 rewrite = rebind_var(rewrite, right_ref, left_ref)
230 {right, rewrite}
231
232 1 true ->
233 1 rewrite = rewrite |> bind_var(left_ref) |> rebind_var(left_ref, right_ref)
234 {left, rewrite}
235 end
236 end
237
238 def do_rewrite_match_binding(rewrite = %Rewrite{}, var, expression) do
239 10 {rewrite, guards} =
240 do_rewrite_match_binding_into_guards(rewrite, var, expression)
241
242 10 rewrite = %{rewrite | guards: rewrite.guards ++ guards}
243 {var, rewrite}
244 end
245
246 10 def do_rewrite_match_binding_into_guards(rewrite, context, guards \\ [], expression)
247
248 # Rewrite literal two-tuples into tuple AST to fit other tuple literals
249 def do_rewrite_match_binding_into_guards(
250 rewrite = %Rewrite{},
251 context,
252 guards,
253 {two, tuple}
254 ) do
255 0 do_rewrite_match_binding_into_guards(
256 rewrite,
257 context,
258 guards,
259 {:{}, [], [two, tuple]}
260 )
261 end
262
263 def do_rewrite_match_binding_into_guards(
264 rewrite = %Rewrite{},
265 context,
266 guards,
267 {:{}, _meta, elements}
268 )
269 when is_list(elements) do
270 0 guards = [
271 {:__matcha__,
272 {:bound, {:andalso, {:is_tuple, context}, {:==, {:tuple_size, context}, length(elements)}}}}
273 | guards
274 ]
275
276 0 for {element, index} <- Enum.with_index(elements), reduce: {rewrite, guards} do
277 {rewrite, guards} ->
278 0 do_rewrite_match_binding_into_guards(
279 rewrite,
280 {:element, index + 1, context},
281 guards,
282 element
283 )
284 end
285 end
286
287 def do_rewrite_match_binding_into_guards(
288 rewrite = %Rewrite{},
289 context,
290 guards,
291 {:%{}, _meta, pairs}
292 )
293 when is_list(pairs) do
294 8 guards = [
295 {:__matcha__, {:bound, {:is_map, context}}} | guards
296 ]
297
298 8 for {key, value} <- pairs, reduce: {rewrite, guards} do
299 {rewrite, guards} ->
300 9 key =
301 case key do
302 key when is_atomic_literal(key) ->
303 8 key
304
305 {:^, _, [var]} when is_named_var(var) ->
306 1 {:unquote, [], [var]}
307 end
308
309 9 guards = [{:__matcha__, {:bound, {:is_map_key, key, context}}} | guards]
310
311 9 do_rewrite_match_binding_into_guards(
312 rewrite,
313 {:map_get, key, context},
314 guards,
315 value
316 )
317 end
318 end
319
320 def do_rewrite_match_binding_into_guards(
321 rewrite = %Rewrite{},
322 context,
323 guards,
324 elements
325 )
326 when is_list(elements) do
327 0 guards = [
328 {:__matcha__, {:bound, {:is_list, context}}} | guards
329 ]
330
331 0 {rewrite, _context, guards} =
332 0 for element <- elements, reduce: {rewrite, context, guards} do
333 {rewrite, context, guards} ->
334 0 case element do
335 {:|, _, [head, tail]} ->
336 0 {rewrite, guards} =
337 do_rewrite_match_binding_into_guards(
338 rewrite,
339 {:hd, context},
340 guards,
341 head
342 )
343
344 0 {rewrite, guards} =
345 do_rewrite_match_binding_into_guards(
346 rewrite,
347 {:tl, context},
348 guards,
349 tail
350 )
351
352 0 {rewrite, nil, guards}
353
354 element ->
355 0 {rewrite, guards} =
356 do_rewrite_match_binding_into_guards(
357 rewrite,
358 {:hd, context},
359 guards,
360 element
361 )
362
363 0 {rewrite, {:tl, context}, guards}
364 end
365 end
366
367 {rewrite, guards}
368 end
369
370 def do_rewrite_match_binding_into_guards(
371 rewrite = %Rewrite{},
372 context,
373 guards,
374 {:=, _, [{ref, _, _} = var, expression]}
375 )
376 when is_named_var(var) do
377 0 rewrite = bind_var(rewrite, ref, context)
378
379 0 do_rewrite_match_binding_into_guards(
380 rewrite,
381 bound_var_to_source(rewrite, get(rewrite, ref)),
382 guards,
383 expression
384 )
385 end
386
387 def do_rewrite_match_binding_into_guards(
388 rewrite = %Rewrite{},
389 context,
390 guards,
391 {:=, _, [expression, {ref, _, _} = var]}
392 )
393 when is_named_var(var) do
394 0 rewrite = bind_var(rewrite, ref, context)
395
396 0 do_rewrite_match_binding_into_guards(
397 rewrite,
398 bound_var_to_source(rewrite, get(rewrite, ref)),
399 guards,
400 expression
401 )
402 end
403
404 # Handle pinned vars
405 def do_rewrite_match_binding_into_guards(
406 rewrite = %Rewrite{},
407 context,
408 guards,
409 {:^, _, [var]} = expression
410 )
411 when is_named_var(var) do
412 2 if outer_var?(rewrite, var) do
413 {rewrite, [{:==, {:unquote, [], [var]}, context} | guards]}
414 else
415 0 raise_pin_on_missing_outer_var_error!(rewrite, var, expression)
416 end
417 end
418
419 # Handle unpinned vars
420 def do_rewrite_match_binding_into_guards(
421 rewrite = %Rewrite{},
422 context,
423 guards,
424 {ref, _, _} = var
425 )
426 when is_named_var(var) do
427 4 rewrite = bind_var(rewrite, ref, context)
428 {rewrite, guards}
429 end
430
431 5 def do_rewrite_match_binding_into_guards(
432 rewrite = %Rewrite{},
433 context,
434 guards,
435 literal
436 )
437 when is_atomic_literal(literal) do
438 {rewrite,
439 [
440 {:==, context, {:__matcha__, {:const, literal}}}
441 | guards
442 ]}
443 end
444 end

lib/matcha/rewrite/calls.ex

94.1
17
12828
1
Line Hits Source
0 defmodule Matcha.Rewrite.Calls do
1 @moduledoc """
2 Rewrites expanded Elixir function calls into Erlang match specification tuple-calls.
3 """
4
5 alias Matcha.Rewrite
6 alias Matcha.Context
7 import Matcha.Rewrite.AST, only: :macros
8
9 @spec rewrite(Macro.t(), Rewrite.t()) :: Macro.t()
10 def rewrite(ast, rewrite) do
11 1333 do_rewrite(ast, rewrite)
12 end
13
14 @spec do_rewrite(Macro.t(), Rewrite.t()) :: Macro.t()
15 defp do_rewrite(ast, rewrite)
16
17 defp do_rewrite(
18 {{:., _, [module, function]}, _, args} = call,
19 %Rewrite{context: context} = rewrite
20 )
21 when is_remote_call(call) and module == context do
22 2 args = do_rewrite(args, rewrite)
23
24 # Permitted calls to special functions unique to specific contexts can be looked up from the spec's context module.
25 2 if {function, length(args)} in module.__info__(:functions) do
26 2 List.to_tuple([function | args])
27 else
28 0 raise_invalid_call_error!(rewrite, {module, function, args})
29 end
30 end
31
32 defp do_rewrite({{:., _, [:erlang = module, function]}, _, args} = call, rewrite)
33 when is_remote_call(call) do
34 964 args = do_rewrite(args, rewrite)
35
36 # Permitted calls to unqualified functions and operators that appear
37 # to reference the `:erlang` kernel module post expansion.
38 # They are intercepted here and looked up instead from the Erlang context before becoming an instruction.
39 964 if {function, length(args)} in Context.Erlang.__info__(:functions) do
40 962 List.to_tuple([function | args])
41 else
42 2 raise_invalid_call_error!(rewrite, {module, function, args})
43 end
44 end
45
46 defp do_rewrite(
47 {{:., _, [module, function]}, _, args} = call,
48 rewrite = %Rewrite{}
49 )
50 when is_remote_call(call) do
51 1 raise_invalid_call_error!(rewrite, {module, function, args})
52 end
53
54 3393 defp do_rewrite([head | tail] = list, rewrite) when is_list(list) do
55 [do_rewrite(head, rewrite) | do_rewrite(tail, rewrite)]
56 end
57
58 2109 defp do_rewrite([] = list, _rewrite) when is_list(list) do
59 []
60 end
61
62 defp do_rewrite(tuple, rewrite) when is_tuple(tuple) do
63 tuple
64 |> Tuple.to_list()
65 |> do_rewrite(rewrite)
66 471 |> List.to_tuple()
67 end
68
69 defp do_rewrite(ast, _rewrite) do
70 # when is_atom(ast) or is_number(ast) or is_bitstring(ast) or is_map(ast) do
71 2614 ast
72 end
73
74 @spec raise_invalid_call_error!(Rewrite.t(), Rewrite.Bindings.var_ast()) :: no_return()
75 defp raise_invalid_call_error!(rewrite, call)
76
77 if Matcha.Helpers.erlang_version() < 25 do
78 for {erlang_25_function, erlang_25_arity} <- [binary_part: 2, binary_part: 3, byte_size: 1] do
79 defp raise_invalid_call_error!(rewrite = %Rewrite{}, {module, function, args})
80 when module == :erlang and
81 function == unquote(erlang_25_function) and
82 length(args) == unquote(erlang_25_arity) do
83 raise Rewrite.Error,
84 source: rewrite,
85 details: "unsupported function call",
86 problems: [
87 error:
88 "Erlang/OTP #{Matcha.Helpers.erlang_version()} does not support calling" <>
89 " `#{inspect(module)}.#{function}/#{length(args)}`" <>
90 " in match specs, you must be using Erlang/OTP 25 or greater"
91 ]
92 end
93 end
94 end
95
96 defp raise_invalid_call_error!(rewrite = %Rewrite{}, {module, function, args}) do
97 3 raise Rewrite.Error,
98 source: rewrite,
99 details: "unsupported function call",
100 problems: [
101 error:
102 "cannot call remote function" <>
103 3 " `#{inspect(module)}.#{function}/#{length(args)}`" <>
104 3 " in `#{inspect(rewrite.context)}` spec"
105 ]
106 end
107 end

lib/matcha/rewrite/clause.ex

100.0
10
4435
0
Line Hits Source
0 defmodule Matcha.Rewrite.Clause do
1 @moduledoc """
2 Rewrites expanded Elixir clauses into Erlang match specification entries.
3 """
4
5 alias Matcha.Rewrite
6
7 defstruct [:match, guards: [], body: []]
8
9 def new({:->, _, [[head], body]}, _rewrite) do
10 561 {match, guards} = :elixir_utils.extract_guards(head)
11
12 561 %__MODULE__{
13 match: match,
14 guards: guards,
15 body: [body]
16 }
17 end
18
19 def new(clause, rewrite) do
20 1 raise Rewrite.Error,
21 source: rewrite,
22 details: "when rewriting clauses",
23 problems: [
24 error: "match spec clauses must be of arity 1, got: `#{Macro.to_string(clause)}`"
25 ]
26 end
27
28 def rewrite(clause = %__MODULE__{}, rewrite) do
29 561 {rewrite, match} = Rewrite.Bindings.rewrite(rewrite, clause.match)
30 552 match = Rewrite.Match.rewrite(rewrite, match)
31 552 guards = Rewrite.Guards.rewrite(clause.guards, rewrite)
32 551 body = rewrite_body(rewrite, clause.body)
33 543 {{match, guards, body}, rewrite.bindings.vars}
34 end
35
36 @spec rewrite_body(Rewrite.t(), Macro.t()) :: Macro.t()
37 def rewrite_body(rewrite, ast)
38
39 def rewrite_body(rewrite, [{:__block__, _, body}]) do
40 2 rewrite_body(rewrite, body)
41 end
42
43 def rewrite_body(rewrite, body) do
44 551 Rewrite.Expression.rewrite(body, rewrite)
45 end
46 end

lib/matcha/rewrite/expression.ex

83.3
36
17000
6
Line Hits Source
0 defmodule Matcha.Rewrite.Expression do
1 @moduledoc """
2 Rewrites expanded Elixir expressions into Erlang match specification expressions.
3 """
4
5 alias Matcha.Rewrite
6 import Matcha.Rewrite.AST, only: :macros
7
8 @spec rewrite(Macro.t(), Rewrite.t()) :: Macro.t()
9 def rewrite(expression, rewrite) do
10 expression
11 |> rewrite_bindings(rewrite)
12 |> rewrite_literals(rewrite)
13 785 |> Rewrite.Calls.rewrite(rewrite)
14 end
15
16 @spec rewrite_bindings(Macro.t(), Rewrite.t()) :: Macro.t()
17 defp rewrite_bindings(ast, rewrite) do
18 785 Macro.postwalk(ast, fn
19 {ref, _, context} = var when is_named_var(var) ->
20 965 cond do
21 Rewrite.Bindings.bound?(rewrite, ref) ->
22 944 case Rewrite.Bindings.get(rewrite, ref) do
23 1 outer_var when is_named_var(outer_var) ->
24 {:__matcha__, {:const, {:unquote, [], [outer_var]}}}
25
26 943 bound ->
27 {:__matcha__, {:bound, Rewrite.Bindings.bound_var_to_source(rewrite, bound)}}
28 end
29
30 21 Macro.Env.has_var?(rewrite.env, {ref, context}) ->
31 {:__matcha__, {:const, {:unquote, [], [var]}}}
32
33 2 true ->
34 2 raise_unbound_variable_error!(rewrite, var)
35 end
36
37 other ->
38 5146 other
39 end)
40 end
41
42 @spec raise_unbound_variable_error!(Rewrite.t(), Rewrite.Bindings.var_ast()) :: no_return()
43 defp raise_unbound_variable_error!(rewrite = %Rewrite{}, var) when is_var(var) do
44 2 raise Rewrite.Error,
45 source: rewrite,
46 details: "when binding variables",
47 problems: [
48 error:
49 "variable `#{Macro.to_string(var)}` was not bound in the match head:" <>
50 " variables can only be introduced in the heads of clauses in match specs"
51 ]
52 end
53
54 @spec rewrite_literals(Macro.t(), Rewrite.t()) :: Macro.t()
55 def rewrite_literals(ast, rewrite) do
56 807 do_rewrite_literals(ast, rewrite)
57 end
58
59 # Literal two-tuples passing literals/externals without semantic AST meaning
60 25 defp do_rewrite_literals({:__matcha__, {:const, value}}, _rewrite) do
61 {:const, value}
62 end
63
64 # Literal two-tuples dereferencing bindings
65 defp do_rewrite_literals({:__matcha__, {:bound, expression}}, _rewrite) do
66 956 expression
67 end
68
69 # Other two-tuple literals should follow rules for all other tuples
70 defp do_rewrite_literals({left, right}, rewrite) do
71 25 do_rewrite_literals({:{}, nil, [left, right]}, rewrite)
72 end
73
74 # Structs should refuse to work with update syntax
75 defp do_rewrite_literals(
76 {:%, _,
77 [_module, {:%{}, _, [{:|, _, [{:%{}, _, _map_elements}, _map_updates]}]} = map_ast]},
78 rewrite
79 ) do
80 0 raise_map_update_error!(rewrite, map_ast)
81 end
82
83 # Structs should expand to maps
84 defp do_rewrite_literals({:%, _, [module, {:%{}, meta, map_elements}]}, rewrite) do
85 2 do_rewrite_literals({:%{}, meta, [{:__struct__, module} | map_elements]}, rewrite)
86 end
87
88 # Maps should refuse to work with update syntax
89 defp do_rewrite_literals(
90 {:%{}, _, [{:|, _, _}]} = map_ast,
91 rewrite
92 ) do
93 2 raise_map_update_error!(rewrite, map_ast)
94 end
95
96 # Maps should expand keys and values separately, and refuse to work with update syntax
97 defp do_rewrite_literals({:%{}, _, map_elements}, rewrite)
98 when is_list(map_elements) do
99 11 Enum.map(map_elements, fn {key, value} ->
100 {do_rewrite_literals(key, rewrite), do_rewrite_literals(value, rewrite)}
101 end)
102 3 |> Enum.into(%{})
103 end
104
105 # Tuple literals should be wrapped in a tuple to differentiate from AST
106 defp do_rewrite_literals({:{}, _, tuple_elements}, rewrite) do
107 54 {tuple_elements |> do_rewrite_literals(rewrite) |> List.to_tuple()}
108 end
109
110 # Ignored assignments become the actual value
111 defp do_rewrite_literals({:=, _, [{:_, _, _}, value]}, rewrite) do
112 1 do_rewrite_literals(value, rewrite)
113 end
114
115 defp do_rewrite_literals({:=, _, [value, {:_, _, _}]}, rewrite) do
116 0 do_rewrite_literals(value, rewrite)
117 end
118
119 defp do_rewrite_literals({:=, _, [{:_, _, _}, value]}, rewrite) do
120 0 do_rewrite_literals(value, rewrite)
121 end
122
123 # Other assignments are invalid in expressions
124 defp do_rewrite_literals({:=, _, [left, right]}, rewrite) do
125 2 raise_match_in_expression_error!(rewrite, left, right)
126 end
127
128 # Ignored variables become the 'ignore' token
129 0 defp do_rewrite_literals({:_, _, _} = _var, _rewrite) do
130 :_
131 end
132
133 # Leave other calls alone, only expanding arguments
134 defp do_rewrite_literals({name, meta, arguments}, rewrite) do
135 976 {name, meta, do_rewrite_literals(arguments, rewrite)}
136 end
137
138 4 defp do_rewrite_literals([head | [{:|, _, [left_element, right_element]}]], rewrite) do
139 [
140 do_rewrite_literals(head, rewrite)
141 | [
142 do_rewrite_literals(left_element, rewrite)
143 | do_rewrite_literals(right_element, rewrite)
144 ]
145 ]
146 end
147
148 3 defp do_rewrite_literals([{:|, _, [left_element, right_element]}], rewrite) do
149 [
150 do_rewrite_literals(left_element, rewrite)
151 | do_rewrite_literals(right_element, rewrite)
152 ]
153 end
154
155 2341 defp do_rewrite_literals([head | tail], rewrite) do
156 [
157 do_rewrite_literals(head, rewrite)
158 | do_rewrite_literals(tail, rewrite)
159 ]
160 end
161
162 1580 defp do_rewrite_literals([], _rewrite) do
163 []
164 end
165
166 defp do_rewrite_literals(var, _rewrite)
167 when is_var(var) do
168 0 var
169 end
170
171 defp do_rewrite_literals({name, meta, arguments} = call, rewrite) when is_call(call) do
172 0 {name, meta, do_rewrite_literals(arguments, rewrite), rewrite}
173 end
174
175 defp do_rewrite_literals(ast, _rewrite) when is_atomic_literal(ast) do
176 608 ast
177 end
178
179 @spec raise_match_in_expression_error!(
180 Rewrite.t(),
181 Rewrite.Bindings.var_ast(),
182 Rewrite.Bindings.var_ast()
183 ) :: no_return()
184 defp raise_match_in_expression_error!(rewrite = %Rewrite{}, left, right) do
185 2 raise Rewrite.Error,
186 source: rewrite,
187 details: "when binding variables",
188 problems: [
189 error:
190 "cannot match `#{Macro.to_string(right)}` to `#{Macro.to_string(left)}`:" <>
191 " cannot use the match operator in match spec bodies"
192 ]
193 end
194
195 @spec raise_map_update_error!(Rewrite.t(), Macro.t()) :: no_return()
196 defp raise_map_update_error!(rewrite = %Rewrite{}, map_update) do
197 2 raise Rewrite.Error,
198 source: rewrite,
199 problems: [
200 error:
201 "cannot use map update syntax in match specs, got: `#{Macro.to_string(map_update)}`"
202 ]
203 end
204 end

lib/matcha/rewrite/guards.ex

100.0
12
3021
0
Line Hits Source
0 defmodule Matcha.Rewrite.Guards do
1 @moduledoc """
2 Rewrites expanded Elixir guard lists into Erlang match spec guard lists.
3 """
4
5 alias Matcha.Rewrite
6
7 @spec rewrite(Macro.t(), Rewrite.t()) :: Macro.t()
8 def rewrite(guards, rewrite) do
9 guards
10 234 |> Enum.map(&Rewrite.Expression.rewrite(&1, rewrite))
11 553 |> merge_guards(rewrite)
12 end
13
14 def merge_guards(guards, rewrite) do
15 552 extra_guard =
16 552 for extra_guard <- :lists.reverse(rewrite.guards), reduce: [] do
17 10 [] -> Rewrite.Expression.rewrite_literals(extra_guard, rewrite)
18 14 guard -> {:andalso, guard, Rewrite.Expression.rewrite_literals(extra_guard, rewrite)}
19 end
20
21 552 case {guards, extra_guard} do
22 314 {[], []} ->
23 []
24
25 {guards, []} ->
26 228 guards
27
28 8 {[], extra_guard} ->
29 [extra_guard]
30
31 {guards, extra_guard} ->
32 2 for guard <- guards do
33 2 {:andalso, guard, extra_guard}
34 end
35 end
36 end
37 end

lib/matcha/rewrite/kernel.ex

0.0
0
0
0
Line Hits Source
0 defmodule Matcha.Rewrite.Kernel do
1 @moduledoc """
2 Replacements for Kernel functions when rewriting Elixir into match specs.
3
4 These are versions that play nicer with Erlang's match spec limitations.
5 """
6
7 # Keep up to date with the imports in Matcha.Rewrite's expand_spec_ast
8 import Kernel,
9 except: [
10 and: 2,
11 is_boolean: 1,
12 is_exception: 1,
13 is_exception: 2,
14 is_struct: 1,
15 is_struct: 2,
16 or: 2
17 ]
18
19 @doc """
20 Re-implements `Kernel.and/2`.
21
22 This ensures that Elixir 1.6.0+'s [boolean optimizations](https://github.com/elixir-lang/elixir/commit/25dc8d8d4f27ca105d36b06f3f23dbbd0b823fd0)
23 don't create (disallowed) case statements inside match spec bodies.
24 """
25 defmacro left and right do
26 quote do
27 :erlang.andalso(unquote(left), unquote(right))
28 end
29 end
30
31 @doc """
32 Re-implements `Kernel.is_boolean/1`.
33
34 The original simply calls out to `:erlang.is_boolean/1`,
35 which is not allowed in match specs (as of Erlang/OTP 25).
36 Instead, we re-implement it in terms of things that are.
37
38 See: https://github.com/erlang/otp/issues/7045
39 """
40
41 # TODO: Once Erlang/OTP 26 is the minimum supported version,
42 # we can remove this from Matcha.Rewrite.Kernel,
43 # as that is when support in match specs was introduced. See:
44 # https://github.com/erlang/otp/pull/7046
45
46 defmacro is_boolean(value) do
47 quote do
48 unquote(value) == true or unquote(value) == false
49 end
50 end
51
52 @doc """
53 Re-implements `Kernel.is_exception/1`.
54
55 This borrows the guard-specific implementation [from Elixir](https://github.com/elixir-lang/elixir/blob/6730d669fb319411f8e411d4126f1f4067ef9231/lib/elixir/lib/kernel.ex#L2494-L2499)
56 since what Elixir wants to do in normal bodies is invalid in match specs.
57 """
58 defmacro is_exception(term) do
59 quote do
60 is_map(unquote(term)) and :erlang.is_map_key(:__struct__, unquote(term)) and
61 is_atom(:erlang.map_get(:__struct__, unquote(term))) and
62 :erlang.is_map_key(:__exception__, unquote(term)) and
63 :erlang.map_get(:__exception__, unquote(term)) == true
64 end
65 end
66
67 @doc """
68 Re-implements `Kernel.is_exception/2`.
69
70 This borrows the guard-specific implementation [from Elixir](https://github.com/elixir-lang/elixir/blob/6730d669fb319411f8e411d4126f1f4067ef9231/lib/elixir/lib/kernel.ex#L2538-L2545)
71 since what Elixir wants to do in normal bodies is invalid in match specs.
72 """
73 defmacro is_exception(term, name) do
74 quote do
75 is_map(unquote(term)) and
76 (is_atom(unquote(name)) or :fail) and
77 :erlang.is_map_key(:__struct__, unquote(term)) and
78 :erlang.map_get(:__struct__, unquote(term)) == unquote(name) and
79 :erlang.is_map_key(:__exception__, unquote(term)) and
80 :erlang.map_get(:__exception__, unquote(term)) == true
81 end
82 end
83
84 @doc """
85 Re-implements `Kernel.is_struct/1`.
86
87 This borrows the guard-specific implementation [from Elixir](https://github.com/elixir-lang/elixir/blob/6730d669fb319411f8e411d4126f1f4067ef9231/lib/elixir/lib/kernel.ex#L2414-L2417)
88 since what Elixir wants to do in normal bodies is invalid in match specs.
89 """
90 defmacro is_struct(term) do
91 quote do
92 is_map(unquote(term)) and :erlang.is_map_key(:__struct__, unquote(term)) and
93 is_atom(:erlang.map_get(:__struct__, unquote(term)))
94 end
95 end
96
97 @doc """
98 Re-implements `Kernel.is_struct/2`.
99
100 This borrows the guard-specific implementation [from Elixir](https://github.com/elixir-lang/elixir/blob/6730d669fb319411f8e411d4126f1f4067ef9231/lib/elixir/lib/kernel.ex#L2494-L2499)
101 since what Elixir wants to do in normal bodies is invalid in match specs.
102 """
103 defmacro is_struct(term, name) do
104 quote do
105 is_map(unquote(term)) and
106 (is_atom(unquote(name)) or :fail) and
107 :erlang.is_map_key(:__struct__, unquote(term)) and
108 :erlang.map_get(:__struct__, unquote(term)) == unquote(name)
109 end
110 end
111
112 @doc """
113 Re-implements `Kernel.or/2`.
114
115 This ensures that Elixir 1.6.0+'s [boolean optimizations](https://github.com/elixir-lang/elixir/commit/25dc8d8d4f27ca105d36b06f3f23dbbd0b823fd0)
116 don't create (disallowed) case statements inside match spec bodies.
117 """
118 defmacro left or right do
119 quote do
120 :erlang.orelse(unquote(left), unquote(right))
121 end
122 end
123 end

lib/matcha/rewrite/match.ex

81.4
27
6503
5
Line Hits Source
0 defmodule Matcha.Rewrite.Match do
1 @moduledoc """
2 Rewrites expanded Elixir match heads into Erlang match patterns.
3 """
4
5 alias Matcha.Rewrite
6 import Matcha.Rewrite.AST, only: :macros
7
8 @spec rewrite(Rewrite.t(), Macro.t()) :: Macro.t()
9 def rewrite(rewrite, match)
10
11 def rewrite(rewrite = %Rewrite{}, {:=, _, [match, var]}) when is_named_var(var) do
12 0 rewrite(rewrite, match)
13 end
14
15 def rewrite(rewrite = %Rewrite{}, {:=, _, [var, match]}) when is_named_var(var) do
16 0 rewrite(rewrite, match)
17 end
18
19 def rewrite(rewrite = %Rewrite{}, match) do
20 554 do_rewrite(rewrite, match)
21 end
22
23 @spec do_rewrite(Rewrite.t(), Macro.t()) :: Macro.t()
24 defp do_rewrite(rewrite, match) do
25 match
26 |> rewrite_bindings(rewrite)
27 |> rewrite_literals(rewrite)
28 554 |> Rewrite.Calls.rewrite(rewrite)
29 end
30
31 @spec rewrite_literals(Macro.t(), Rewrite.t()) :: Macro.t()
32 defp rewrite_literals(ast, _rewrite) do
33 554 ast |> do_rewrite_literals
34 end
35
36 defp do_rewrite_literals({:%, meta, [struct, map]})
37 when is_atom(struct) and is_list(meta) do
38 1 map |> do_rewrite_literals |> Map.put(:__struct__, struct)
39 end
40
41 defp do_rewrite_literals({:{}, meta, tuple_elements})
42 when is_list(tuple_elements) and is_list(meta) do
43 101 tuple_elements |> do_rewrite_literals |> List.to_tuple()
44 end
45
46 defp do_rewrite_literals({:%{}, meta, map_elements})
47 when is_list(map_elements) and is_list(meta) do
48 9 map_elements |> do_rewrite_literals |> Enum.into(%{})
49 end
50
51 6 defp do_rewrite_literals([head | [{:|, _meta, [left_element, right_element]}]]) do
52 [
53 do_rewrite_literals(head)
54 | [do_rewrite_literals(left_element) | do_rewrite_literals(right_element)]
55 ]
56 end
57
58 4 defp do_rewrite_literals([{:|, _meta, [left_element, right_element]}]) do
59 [do_rewrite_literals(left_element) | do_rewrite_literals(right_element)]
60 end
61
62 254 defp do_rewrite_literals([head | tail]) do
63 [do_rewrite_literals(head) | do_rewrite_literals(tail)]
64 end
65
66 131 defp do_rewrite_literals([]) do
67 []
68 end
69
70 67 defp do_rewrite_literals({left, right}) do
71 {do_rewrite_literals(left), do_rewrite_literals(right)}
72 end
73
74 33 defp do_rewrite_literals({:_, _, _} = ignored_var)
75 when is_var(ignored_var) do
76 :_
77 end
78
79 defp do_rewrite_literals(var)
80 when is_var(var) do
81 18 var
82 end
83
84 defp do_rewrite_literals({name, meta, arguments} = call) when is_call(call) do
85 18 {name, meta, do_rewrite_literals(arguments)}
86 end
87
88 defp do_rewrite_literals(ast) when is_atomic_literal(ast) do
89 709 ast
90 end
91
92 @spec rewrite_bindings(Macro.t(), Rewrite.t()) :: Macro.t()
93 defp rewrite_bindings(ast, rewrite = %Rewrite{}) do
94 554 Macro.postwalk(ast, fn
95 {ref, _, context} = var when is_named_var(var) ->
96 664 cond do
97 664 Macro.Env.has_var?(rewrite.env, {ref, context}) ->
98 18 {:unquote, [], [var]}
99
100 646 Rewrite.Bindings.bound?(rewrite, ref) ->
101 646 Rewrite.Bindings.bound_var_to_source(rewrite, Rewrite.Bindings.get(rewrite, ref))
102
103 0 true ->
104 0 raise_unbound_match_variable_error!(rewrite, var)
105 end
106
107 # {:=, _, [var, literal]} when is_named_var(var) and is_atomic_literal(literal) ->
108 # {:=, _, [var, literal]}
109
110 # {:=, _, [literal, var]} when is_named_var(var) and is_atomic_literal(literal) ->
111 # {:=, _, [literal, var]}
112
113 other ->
114 298 other
115 end)
116 end
117
118 @spec raise_unbound_match_variable_error!(Rewrite.t(), Rewrite.Bindings.var_ast()) ::
119 no_return()
120 defp raise_unbound_match_variable_error!(rewrite = %Rewrite{}, var) when is_var(var) do
121 0 raise Rewrite.Error,
122 source: rewrite,
123 details: "when binding variables",
124 problems: [error: "variable `#{Macro.to_string(var)}` was unbound"]
125 end
126 end

lib/matcha/source.ex

54.5
11
2778
5
Line Hits Source
0 defmodule Matcha.Raw do
1 @moduledoc """
2 Functions that work with the raw Erlang terms representing a match spec.
3
4 The raw "source" code of a match specification is what Matcha calls data that fits the Erlang
5 [match specification](https://www.erlang.org/doc/apps/erts/match_spec.html) grammar.
6
7 Matcha compiles Elixir code into such data, and wraps that data in structs.
8 This module is the bridge between those structs and the Erlang functions that
9 know how to operate on them.
10 """
11
12 @match_all :"$_"
13 @all_matches :"$$"
14
15 @type match_all :: unquote(@match_all)
16 @type all_matches :: unquote(@all_matches)
17
18 @type pattern :: tuple | atom
19 @type conditions :: [condition]
20 @type condition :: tuple
21 @type filter :: {pattern, conditions}
22 @type body :: [expression]
23 @type expression :: atom | match_all | all_matches | tuple | term
24 @type clause :: {pattern, conditions, body}
25 @type spec :: [clause]
26 @type uncompiled :: spec
27
28 @type type :: :table | :trace
29
30 @type trace_flags :: list()
31 @type trace_message :: charlist()
32
33 @type match_target :: tuple() | list(tuple()) | term()
34 @type match_result ::
35 {:ok, any, trace_flags, [{:error | :warning, charlist}]}
36 | {:error, [{:error | :warning, charlist}]}
37
38 @type table_match_result ::
39 {:ok, any, [], [{:error | :warning, charlist}]}
40 | {:error, [{:error | :warning, charlist}]}
41 @type trace_match_result ::
42 {:ok, boolean | trace_message, trace_flags, [{:error | :warning, charlist}]}
43 | {:error, [{:error | :warning, charlist}]}
44
45 @type compiled :: :ets.comp_match_spec()
46
47 9 def __match_all__, do: @match_all
48 0 def __all_matches__, do: @all_matches
49
50 @spec compile(source :: uncompiled) :: compiled
51 @doc """
52 Compiles raw match spec `source` into an opaque, more efficient internal representation.
53 """
54 def compile(source) do
55 365 :ets.match_spec_compile(source)
56 end
57
58 @spec compiled?(any) :: boolean
59 @doc """
60 Checks if provided `value` is a compiled match spec source.
61 """
62 def compiled?(value) do
63 365 :ets.is_compiled_ms(value)
64 end
65
66 @spec ensure_compiled(source :: uncompiled | compiled) :: compiled
67 @doc """
68 Ensures provided raw match spec `source` is compiled.
69 """
70 def ensure_compiled(source) do
71 0 if :ets.is_compiled_ms(source) do
72 0 source
73 else
74 0 compile(source)
75 end
76 end
77
78 @spec run(source :: uncompiled | compiled, list) :: list
79 @doc """
80 Runs a raw match spec `source` against a list of values.
81 """
82 def run(source, list) do
83 365 if compiled?(source) do
84 365 :ets.match_spec_run(list, source)
85 else
86 0 :ets.match_spec_run(list, compile(source))
87 end
88 end
89
90 @spec test(source :: uncompiled, type, match_target) :: match_result
91 @doc """
92 Validates raw match spec `source` of variant `type` and tries to match it against `match_target`.
93 """
94 def test(source, type, match_target) do
95 1309 :erlang.match_spec_test(match_target, source, type)
96 end
97 end

lib/matcha/spec.ex

56.5
23
7258
10
Line Hits Source
0 defmodule Matcha.Spec do
1 @moduledoc """
2 About specs.
3 """
4
5 alias __MODULE__
6
7 alias Matcha.Context
8 alias Matcha.Error
9 alias Matcha.Raw
10
11 defstruct [:raw, :context, :bindings]
12
13 @type bindings :: %{non_neg_integer() => %{atom() => non_neg_integer()}}
14
15 @type t :: %__MODULE__{
16 raw: Raw.uncompiled(),
17 context: Context.t(),
18 bindings: bindings()
19 }
20
21 @spec raw(t()) :: Raw.uncompiled()
22 def raw(%__MODULE__{raw: raw} = _spec) do
23 1992 raw
24 end
25
26 @spec bindings(t()) :: bindings()
27 def bindings(%__MODULE__{bindings: bindings} = _spec) do
28 0 bindings
29 end
30
31 @spec call(t(), Raw.match_target()) ::
32 {:ok, Raw.match_result()} | {:error, Error.problems()}
33 def call(%__MODULE__{} = spec, test) do
34 577 Context.test(spec, test)
35 end
36
37 @spec call!(t(), Raw.match_target()) :: Raw.match_result() | no_return
38 def call!(%__MODULE__{} = spec, test) do
39 524 case call(spec, test) do
40 {:ok, result} ->
41 524 result
42
43 {:error, problems} ->
44 0 raise Spec.Error, source: spec, details: "when calling match spec", problems: problems
45 end
46 end
47
48 @doc """
49 Wraps an existing `raw` match specification into a `#{inspect(__MODULE__)}` struct for usage in Matcha APIs.
50
51 Assumes the spec is written to be used in `Matcha.Context.Table` context, and validates it as such.
52 To modify this validation behaviour, see `from_raw/2`.
53
54 Returns `{:ok, %{#{inspect(__MODULE__)}}}` if validation succeeds, or `{:error, problems}` if not.
55 """
56 @spec from_raw(Raw.spec()) :: {:ok, t} | {:error, Error.problems()}
57 def from_raw(raw) do
58 0 from_raw(:table, raw)
59 end
60
61 @doc """
62 Wraps an existing `raw` match specification into a `#{inspect(__MODULE__)}` struct for usage in Matcha APIs.
63
64 Accepts a `context` module or specifier against which to validate.
65
66 Returns `{:ok, %{#{inspect(__MODULE__)}}}` if validation succeeds, or `{:error, problems}` if not.
67 """
68 @spec from_raw(Context.t() | Raw.type(), Raw.spec()) ::
69 {:ok, t} | {:error, Error.problems()}
70 @spec from_raw(Context.t() | Raw.type(), Raw.spec(), bindings()) ::
71 {:ok, t} | {:error, Error.problems()}
72 def from_raw(context, raw, bindings \\ %{}) do
73 %__MODULE__{
74 raw: raw,
75 context: Context.resolve(context),
76 bindings: bindings
77 }
78 0 |> validate
79 end
80
81 @doc """
82 Wraps an existing `raw` match specification into a `Matcha.Spec` struct for usage in Matcha APIs.
83
84 Assumes the spec is written to be used in `Matcha.Context.Table` context, and validates it as such.
85 To modify this validation behaviour, see `from_raw!/2`.
86
87 Returns a `#{inspect(__MODULE__)}` struct if validation succeeds, otherwise raises a `#{inspect(__MODULE__)}.Error`.
88 """
89 @spec from_raw!(Raw.spec()) :: t | no_return
90 def from_raw!(source) do
91 8 from_raw!(:table, source)
92 end
93
94 @doc """
95 Wraps an existing `raw` match specification into a `#{inspect(__MODULE__)}` struct for usage in Matcha APIs.
96
97 Accepts a `context` module or specifier against which to validate.
98
99 Returns a `#{inspect(__MODULE__)}` struct if validation succeeds, otherwise raises a `#{inspect(__MODULE__)}.Error`.
100 """
101 @spec from_raw!(Context.t() | Raw.type(), Raw.spec()) ::
102 t | no_return
103 def from_raw!(context, raw, bindings \\ %{}) do
104 %__MODULE__{
105 raw: raw,
106 context: Context.resolve(context),
107 bindings: bindings
108 }
109 8 |> validate!
110 end
111
112 @spec run(t(), Enumerable.t()) :: {:ok, list} | {:error, Error.problems()}
113 @doc """
114 Runs a match `spec` over each item in an `enumerable`.
115
116 ## Examples
117
118 iex> require Matcha
119 ...> Matcha.spec(:filter_map) do
120 ...> {amount, tax} when is_integer(amount) and amount > 0 -> {:credit, amount + tax}
121 ...> end
122 ...> |> Matcha.Spec.run!([
123 ...> {9001, 0},
124 ...> {-200, -2.50},
125 ...> {-3, -0.5},
126 ...> {:error, "bank was offline"},
127 ...> {100, 0},
128 ...> {-743, -16.0},
129 ...> ])
130 [credit: 9001, credit: 100]
131
132 ## Note
133
134 This function converts the `enumerable` to a list,
135 which will trigger full enumeration of things like lazy `Stream`s.
136 If used with an infinite stream, it will run forever!
137 Consider using `stream/2` if you need lazy filter/mapping.
138 It isn't as efficient, but plays nicer with infinite streams,
139 and fits into the `Stream` APIs.
140 """
141 def run(%__MODULE__{} = spec, enumerable) do
142 365 Context.run(spec, enumerable)
143 end
144
145 @spec run!(t(), Enumerable.t()) :: list | no_return
146 @doc """
147 Runs a match `spec` over each item in an `enumerable`.
148 """
149 def run!(%__MODULE__{} = spec, enumerable) do
150 184 case run(spec, enumerable) do
151 {:ok, results} ->
152 184 results
153
154 {:error, problems} ->
155 0 raise Spec.Error, source: spec, details: "when running match spec", problems: problems
156 end
157 end
158
159 @spec valid?(t()) :: boolean()
160 def valid?(%__MODULE__{} = spec) do
161 0 case validate(spec) do
162 0 {:ok, _spec} -> true
163 0 {:error, _problems} -> false
164 end
165 end
166
167 @spec validate(t()) :: {:ok, t()} | {:error, Error.problems()}
168 def validate(%__MODULE__{} = spec) do
169 726 case Context.test(spec) do
170 726 {:ok, _result} -> {:ok, spec}
171 0 {:error, problems} -> {:error, problems}
172 end
173 end
174
175 @spec validate!(t()) :: t() | no_return()
176 def validate!(%__MODULE__{} = spec) do
177 720 case validate(spec) do
178 {:ok, spec} ->
179 720 spec
180
181 {:error, problems} ->
182 0 raise Spec.Error, source: spec, details: "when validating match spec", problems: problems
183 end
184 end
185 end

lib/matcha/table.ex

0.0
0
0
0
Line Hits Source
0 defmodule Matcha.Table do
1 @moduledoc """
2 High-level APIs for querying
3 [`:ets`](https://www.erlang.org/doc/man/ets),
4 [`:dets`](https://www.erlang.org/doc/man/dets),
5 and [`:mnesia`](https://www.erlang.org/doc/man/mnesia) tables.
6
7 These three Erlang storage systems can be queried very efficiently with
8 match patterns and match specifications. You can use the results of
9 `Matcha.Pattern.raw/1` and `Matcha.Spec.raw/1` anywhere these constructs
10 are accepted in those modules.
11
12 For convenience, you can also use these `Matcha.Table` modules instead,
13 which implement a more Elixir-ish interface to just the matching parts of their APIs,
14 automatically unwrap `Matcha` constructs where appropriate, and provide
15 high-level macros for constructing and immediately querying them in a fluent way.
16
17 > #### Using `Matcha.Table.Mnesia` {: .info}
18 >
19 > The `Matcha.Table.Mnesia` modules and functions are only available
20 > if your application has specified [`:mnesia`](https://www.erlang.org/doc/man/mnesia) in its list of
21 > `:extra_applications` in your `mix.exs` `applications/0` callback.
22 """
23
24 @doc """
25 Builds a `Matcha.Spec` for table querying purposes.
26
27 Shorthand for `Matcha.spec(:table, spec)`.
28 """
29 defmacro spec(spec) do
30 quote location: :keep do
31 require Matcha
32
33 Matcha.spec(:table, unquote(spec))
34 end
35 end
36 end

lib/matcha/table/ets.ex

0.0
2
0
2
Line Hits Source
0 defmodule Matcha.Table.ETS do
1 @moduledoc """
2 High-level macros for querying [`:ets`](https://www.erlang.org/doc/man/ets).
3
4 The macros in this module build and execute match patterns/specs
5 against [`:ets`](https://www.erlang.org/doc/man/ets) tables in one go.
6 For more fine-grained usage, or if you are passing around or re-using
7 the same pattern/spec, see the `Matcha.Table.ETS.Match` and
8 `Matcha.Table.ETS.Select` modules.
9 """
10
11 @type table :: atom() | :ets.tid()
12 @type object :: tuple()
13
14 @default_match_operation :all
15 @match_operations Matcha.Table.ETS.Match.__info__(:functions) |> Keyword.keys()
16
17 0 defmacro match(table, operation \\ @default_match_operation, pattern)
18 when operation in @match_operations do
19 quote location: :keep do
20 require Matcha
21
22 Matcha.Table.ETS.Match.unquote(operation)(
23 unquote(table),
24 Matcha.pattern(unquote(pattern))
25 )
26 end
27 end
28
29 @default_select_operation :all
30 @select_operations Matcha.Table.ETS.Select.__info__(:functions) |> Keyword.keys()
31
32 0 defmacro select(table, operation \\ @default_select_operation, spec)
33 when operation in @select_operations do
34 quote location: :keep do
35 require Matcha.Table
36
37 Matcha.Table.ETS.Select.unquote(operation)(
38 unquote(table),
39 Matcha.Table.spec(unquote(spec))
40 )
41 end
42 end
43 end

lib/matcha/table/ets/match.ex

0.0
3
0
3
Line Hits Source
0 defmodule Matcha.Table.ETS.Match do
1 @moduledoc """
2 Wrapper around [`:ets`](https://www.erlang.org/doc/man/ets) functions that accept `Matcha.Pattern`s.
3 """
4
5 alias Matcha.Pattern
6 alias Matcha.Table.ETS
7
8 @type operation :: :all | :delete | :object
9
10 @spec all(ETS.table(), Pattern.t()) :: [[term()]]
11 def all(table, pattern = %Pattern{}) do
12 0 :ets.match(table, Pattern.raw(pattern))
13 end
14
15 @spec delete(ETS.table(), Pattern.t()) :: true
16 def delete(table, pattern = %Pattern{}) do
17 0 :ets.match_delete(table, Pattern.raw(pattern))
18 end
19
20 @spec object(ETS.table(), Pattern.t()) :: [ETS.object()]
21 def object(table, pattern = %Pattern{}) do
22 0 :ets.match_object(table, Pattern.raw(pattern))
23 end
24 end

lib/matcha/table/ets/select.ex

0.0
5
0
5
Line Hits Source
0 defmodule Matcha.Table.ETS.Select do
1 @moduledoc """
2 Wrapper around [`:ets`](https://www.erlang.org/doc/man/ets) functions that accept `Matcha.Spec`s.
3 """
4
5 alias Matcha.Spec
6 alias Matcha.Context.Table
7 alias Matcha.Table.ETS
8
9 @type operation :: :all | :count | :delete | :replace | :reverse
10
11 @spec all(ETS.table(), Spec.t()) :: [term()]
12 def all(table, spec = %Spec{context: Table}) do
13 0 :ets.select(table, Spec.raw(spec))
14 end
15
16 @spec count(ETS.table(), Spec.t()) :: non_neg_integer()
17 def count(table, spec = %Spec{context: Table}) do
18 0 :ets.select_count(table, Spec.raw(spec))
19 end
20
21 @spec delete(ETS.table(), Spec.t()) :: non_neg_integer()
22 def delete(table, spec = %Spec{context: Table}) do
23 0 :ets.select_delete(table, Spec.raw(spec))
24 end
25
26 @spec replace(ETS.table(), Spec.t()) :: non_neg_integer()
27 def replace(table, spec = %Spec{context: Table}) do
28 0 :ets.select_replace(table, Spec.raw(spec))
29 end
30
31 @spec reverse(ETS.table(), Spec.t()) :: [term()]
32 def reverse(table, spec = %Spec{context: Table}) do
33 0 :ets.select_reverse(table, Spec.raw(spec))
34 end
35 end

lib/matcha/table/mnesia.ex

0.0
1
0
1
Line Hits Source
0 if Matcha.Helpers.application_loaded?(:mnesia) do
1 defmodule Matcha.Table.Mnesia do
2 @moduledoc """
3 High-level macros for querying [`:mnesia`](https://www.erlang.org/doc/man/mnesia).
4
5 The macros in this module build and execute match patterns/specs
6 against [`:mnesia`](https://www.erlang.org/doc/man/mnesia) tables in one go.
7 For more fine-grained usage, or if you are passing around or re-using
8 the same pattern/spec, see the `Matcha.Table.Mnesia.Match` and
9 `Matcha.Table.Mnesia.Select` modules.
10 """
11
12 @default_lock :read
13 0 def __default_lock__, do: @default_lock
14
15 @type table :: atom()
16 @type lock :: :read | :sticky_write | :write
17 @type default_lock :: unquote(@default_lock)
18 @type opts :: [{:lock, lock}]
19
20 @doc """
21 Returns all objects from `table` matching `pattern`.
22
23 Accepted `opts`:
24
25 - `lock:` defaults to `#{inspect(@default_lock)}`
26
27 This is a wrapper around `Matcha.Table.Mnesia.Match.objects/3`, consult those docs
28 for more information.
29
30 ### Examples
31
32 director = "Steven Spielberg"
33 composer = "John Williams"
34
35 require Matcha.Table.Mnesia
36 Matcha.Table.Mnesia.match(table, {Movie, _id, _title, _year, ^director, ^composer})
37 #=> [
38 {Movie, "tt0073195", "Jaws", 1975, "Steven Spielberg", "John Williams"},
39 {Movie, "tt0082971", "Raiders of the Lost Ark", 1981, "Steven Spielberg", "John Williams"},
40 # ...
41 ]
42 """
43 defmacro match(table, pattern, opts \\ []) do
44 quote location: :keep do
45 require Matcha
46
47 Matcha.Table.Mnesia.Match.objects(
48 unquote(table),
49 Matcha.pattern(unquote(pattern)),
50 unquote(opts)
51 )
52 end
53 end
54
55 @doc """
56 Selects and transforms all objects from `table` using `spec`.
57
58 Accepted `opts`:
59
60 - `lock:` defaults to `#{inspect(@default_lock)}`
61
62 This is a wrapper around `Matcha.Table.Mnesia.Select.all/3`, consult those docs
63 for more information.
64
65 ### Examples
66
67 director = "Steven Spielberg"
68 composer = "John Williams"
69
70 require Matcha.Table.Mnesia
71 Matcha.Table.Mnesia.select(table) do
72 {_, _id, title, ^director, ^composer} when year > 1980
73 -> title
74 end
75 #=> [
76 "Raiders of the Lost Ark",
77 "E.T. the Extra-Terrestrial",
78 # ...
79 ]
80 """
81 defmacro select(table, opts \\ [], spec) do
82 quote location: :keep do
83 require Matcha.Table
84
85 Matcha.Table.Mnesia.Select.all(
86 unquote(table),
87 Matcha.Table.spec(unquote(spec)),
88 unquote(opts)
89 )
90 end
91 end
92 end
93 end

lib/matcha/table/mnesia/match.ex

0.0
3
0
3
Line Hits Source
0 if Matcha.Helpers.application_loaded?(:mnesia) do
1 defmodule Matcha.Table.Mnesia.Match do
2 @moduledoc """
3 Wrapper around [`:mnesia`](https://www.erlang.org/doc/man/mnesia) functions that accept `Matcha.Pattern`s.
4 """
5
6 alias Matcha.Pattern
7 alias Matcha.Table.Mnesia
8
9 @type operation :: :object
10
11 @spec objects(Pattern.t()) :: [tuple()]
12 @doc """
13 Returns all objects from a table matching `pattern`.
14
15 The [`:mnesia`](https://www.erlang.org/doc/man/mnesia) table that will be queried
16 corresponds to the first element of the provided `pattern`.
17
18 This is a wrapper around `:mnesia.match_object/1`, consult those docs
19 for more information.
20 """
21 def objects(pattern = %Pattern{}) do
22 0 :mnesia.match_object(Pattern.raw(pattern))
23 end
24
25 @spec objects(Mnesia.table(), Pattern.t(), Mnesia.opts()) :: [tuple()]
26 @doc """
27 Returns all objects from `table` matching `pattern`.
28
29 Accepted `opts`:
30
31 - `lock:` defaults to `#{inspect(Mnesia.__default_lock__())}`
32
33 This is a wrapper around `:mnesia.match_object/3`, consult those docs
34 for more information.
35 """
36 def objects(table, pattern = %Pattern{}, opts \\ []) do
37 0 lock = Keyword.get(opts, :lock, Mnesia.__default_lock__())
38
39 0 :mnesia.match_object(table, Pattern.raw(pattern), lock)
40 end
41 end
42 end

lib/matcha/table/mnesia/select.ex

0.0
2
0
2
Line Hits Source
0 if Matcha.Helpers.application_loaded?(:mnesia) do
1 defmodule Matcha.Table.Mnesia.Select do
2 @moduledoc """
3 Wrapper around [`:mnesia`](https://www.erlang.org/doc/man/mnesia) functions that accept `Matcha.Spec`s.
4 """
5
6 alias Matcha.Spec
7 alias Matcha.Context.Table
8 alias Matcha.Table.Mnesia
9
10 @type operation :: :all
11
12 @spec all(Mnesia.table(), Spec.t(), Mnesia.opts()) :: [tuple()]
13 @doc """
14 Selects and transforms all objects from `table` using `spec`.
15
16 Accepted `opts`:
17
18 - `lock:` defaults to `#{inspect(Mnesia.__default_lock__())}`
19
20 This is a wrapper around `:mnesia.select/2` and `:mnesia.select/3`, consult those docs
21 for more information.
22 """
23 def all(table, spec = %Spec{context: Table}, opts \\ []) do
24 0 lock = Keyword.get(opts, :lock, Mnesia.__default_lock__())
25
26 0 :mnesia.select(table, Spec.raw(spec), lock)
27 end
28 end
29 end

lib/matcha/table/query.ex

0.0
0
0
0
Line Hits Source
0 defmodule Matcha.Table.Query do
1 @moduledoc """
2 Experimental.
3 """
4 defstruct [:type, :table, :query]
5 end