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 |
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 |
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 |
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 |
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 |
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 |
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 | ||
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
Line | Hits | Source |
---|---|---|
0 | defmodule Matcha.Table.Query do | |
1 | @moduledoc """ | |
2 | Experimental. | |
3 | """ | |
4 | defstruct [:type, :table, :query] | |
5 | end |