├── .formatter.exs ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .travis.yml ├── README.md ├── lib └── ex2ms.ex ├── mix.exs ├── mix.lock └── test ├── ex2ms_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Test 8 | runs-on: ubuntu-20.04 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | include: 13 | - erlang: "26.2" 14 | elixir: "1.16.1" 15 | lint: true 16 | - erlang: "21.3" 17 | elixir: "1.7.4" 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Install OTP and Elixir 22 | uses: erlef/setup-beam@v1 23 | with: 24 | otp-version: ${{matrix.erlang}} 25 | elixir-version: ${{matrix.elixir}} 26 | 27 | - name: Install dependencies 28 | run: mix deps.get 29 | 30 | - name: Check for unused dependencies 31 | run: mix deps.unlock --check-unused 32 | if: ${{matrix.lint}} 33 | 34 | - name: Compile with --warnings-as-errors 35 | run: mix compile --warnings-as-errors 36 | if: ${{matrix.lint}} 37 | 38 | - name: Check mix format 39 | run: mix format --check-formatted 40 | if: ${{matrix.lint}} 41 | 42 | - name: Run tests 43 | run: mix test --trace 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /log 5 | erl_crash.dump 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.0.0 4 | - 1.9.4 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ex2ms 2 | 3 | Translates Elixir functions to match specifications for use with `ets`. 4 | Requires Elixir 1.0 or later. 5 | 6 | ## Usage 7 | 8 | Add ex2ms to your Mix dependencies: 9 | 10 | ```elixir 11 | defp deps do 12 | [{:ex2ms, "~> 1.0"}] 13 | end 14 | ``` 15 | 16 | In your shell write the following to get up and running to try ex2ms out: 17 | 18 | ```bash 19 | mix deps.get 20 | iex -S mix 21 | ``` 22 | 23 | ```elixir 24 | iex(1)> import Ex2ms 25 | iex(2)> fun do { x, y } = z when x > 10 -> z end 26 | [{{:"$1",:"$2"},[{:>,:"$1",10}],[:"$_"]}] 27 | iex(3)> :ets.test_ms({ 42, 43 }, v(2)) 28 | {:ok,{42,43}} 29 | iex(4)> :ets.test_ms({ 0, 10 }, v(2)) 30 | {:ok,false} 31 | ``` 32 | 33 | ## License 34 | 35 | Copyright 2013 Eric Meadows-Jönsson 36 | 37 | Licensed under the Apache License, Version 2.0 (the "License"); 38 | you may not use this file except in compliance with the License. 39 | You may obtain a copy of the License at 40 | 41 | http://www.apache.org/licenses/LICENSE-2.0 42 | 43 | Unless required by applicable law or agreed to in writing, software 44 | distributed under the License is distributed on an "AS IS" BASIS, 45 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 46 | See the License for the specific language governing permissions and 47 | limitations under the License. 48 | -------------------------------------------------------------------------------- /lib/ex2ms.ex: -------------------------------------------------------------------------------- 1 | defmodule Ex2ms do 2 | @moduledoc """ 3 | This module provides the `Ex2ms.fun/1` macro for translating Elixir functions 4 | to match specifications. 5 | """ 6 | 7 | @bool_functions [ 8 | :is_atom, 9 | :is_binary, 10 | :is_float, 11 | :is_function, 12 | :is_integer, 13 | :is_list, 14 | :is_map_key, 15 | :is_number, 16 | :is_pid, 17 | :is_port, 18 | :is_record, 19 | :is_reference, 20 | :is_tuple, 21 | :and, 22 | :not, 23 | :or, 24 | :xor 25 | ] 26 | 27 | @extra_guard_functions [ 28 | :-, 29 | :!=, 30 | :!==, 31 | :*, 32 | :/, 33 | :+, 34 | :<, 35 | :<=, 36 | :==, 37 | :===, 38 | :>, 39 | :>=, 40 | :abs, 41 | :band, 42 | :binary_part, 43 | :bnot, 44 | :bor, 45 | :bsl, 46 | :bsr, 47 | :bxor, 48 | :count, 49 | :div, 50 | :element, 51 | :hd, 52 | :map_get, 53 | :max, 54 | :min, 55 | :node, 56 | :rem, 57 | :round, 58 | :self, 59 | :size, 60 | :tl, 61 | :trunc 62 | ] 63 | 64 | @guard_functions @bool_functions ++ @extra_guard_functions 65 | 66 | @action_functions [ 67 | :caller_line, 68 | :caller, 69 | :current_stacktrace, 70 | :disable_trace, 71 | :display, 72 | :enable_trace, 73 | :exception_trace, 74 | :get_seq_token, 75 | :message, 76 | :process_dump, 77 | :return_trace, 78 | :set_seq_token, 79 | :set_tcw, 80 | :silent, 81 | :trace 82 | ] 83 | 84 | @elixir_erlang [ 85 | !=: :"/=", 86 | !==: :"=/=", 87 | <=: :"=<", 88 | ===: :"=:=", 89 | and: :andalso, 90 | or: :orelse 91 | ] 92 | 93 | Enum.each(@guard_functions, fn atom -> 94 | defp is_guard_function(unquote(atom)), do: true 95 | end) 96 | 97 | defp is_guard_function(_), do: false 98 | 99 | Enum.each(@action_functions, fn atom -> 100 | defp is_action_function(unquote(atom)), do: true 101 | end) 102 | 103 | defp is_action_function(_), do: false 104 | 105 | Enum.each(@elixir_erlang, fn {elixir, erlang} -> 106 | defp map_elixir_erlang(unquote(elixir)), do: unquote(erlang) 107 | end) 108 | 109 | defp map_elixir_erlang(atom), do: atom 110 | 111 | @doc """ 112 | Translates an anonymous function to a match specification. 113 | 114 | ## Examples 115 | iex> Ex2ms.fun do {x, y} -> x == 2 end 116 | [{{:"$1", :"$2"}, [], [{:==, :"$1", 2}]}] 117 | """ 118 | defmacro fun(do: clauses) do 119 | clauses 120 | |> Enum.map(fn {:->, _, clause} -> translate_clause(clause, __CALLER__) end) 121 | |> Macro.escape(unquote: true) 122 | end 123 | 124 | defmacrop is_literal(term) do 125 | quote do 126 | is_atom(unquote(term)) or is_number(unquote(term)) or is_binary(unquote(term)) 127 | end 128 | end 129 | 130 | defp translate_clause([head, body], caller) do 131 | {head, conds, state} = translate_head(head, caller) 132 | 133 | case head do 134 | %{} -> 135 | raise_parameter_error(head) 136 | 137 | _ -> 138 | body = translate_body(body, state) 139 | {head, conds, body} 140 | end 141 | end 142 | 143 | defp translate_body({:__block__, _, exprs}, state) when is_list(exprs) do 144 | Enum.map(exprs, &translate_cond(&1, state)) 145 | end 146 | 147 | defp translate_body(expr, state) do 148 | [translate_cond(expr, state)] 149 | end 150 | 151 | defp translate_cond({name, _, context}, state) when is_atom(name) and is_atom(context) do 152 | if match_var = state.vars[{name, context}] do 153 | :"#{match_var}" 154 | else 155 | raise ArgumentError, 156 | message: 157 | "variable `#{name}` is unbound in matchspec (use `^` for outer variables and expressions)" 158 | end 159 | end 160 | 161 | defp translate_cond({left, right}, state), do: translate_cond({:{}, [], [left, right]}, state) 162 | 163 | defp translate_cond({:{}, _, list}, state) when is_list(list) do 164 | {list |> Enum.map(&translate_cond(&1, state)) |> List.to_tuple()} 165 | end 166 | 167 | defp translate_cond({:^, _, [var]}, _state) do 168 | {:const, {:unquote, [], [var]}} 169 | end 170 | 171 | defp translate_cond(fun_call = {fun, _, args}, state) when is_atom(fun) and is_list(args) do 172 | cond do 173 | is_guard_function(fun) -> 174 | match_args = Enum.map(args, &translate_cond(&1, state)) 175 | match_fun = map_elixir_erlang(fun) 176 | [match_fun | match_args] |> List.to_tuple() 177 | 178 | expansion = is_expandable(fun_call, state.caller) -> 179 | translate_cond(expansion, state) 180 | 181 | is_action_function(fun) -> 182 | match_args = Enum.map(args, &translate_cond(&1, state)) 183 | [fun | match_args] |> List.to_tuple() 184 | 185 | true -> 186 | raise_expression_error(fun_call) 187 | end 188 | end 189 | 190 | defp translate_cond(list, state) when is_list(list) do 191 | translate_list(list, state) 192 | end 193 | 194 | defp translate_cond(literal, _state) when is_literal(literal) do 195 | literal 196 | end 197 | 198 | defp translate_cond(expr, _state), do: raise_expression_error(expr) 199 | 200 | defp translate_list([], _state) do 201 | [] 202 | end 203 | 204 | defp translate_list([{:|, _, [left, right]}], state) do 205 | left_p = translate_cond(left, state) 206 | right_p = translate_cond(right, state) 207 | [left_p | right_p] 208 | end 209 | 210 | defp translate_list([head | tail], state) do 211 | head_p = translate_cond(head, state) 212 | tail_p = translate_list(tail, state) 213 | [head_p | tail_p] 214 | end 215 | 216 | defp translate_head([{:when, _, [param, cond]}], caller) do 217 | {head, state} = translate_param(param, caller) 218 | cond = translate_cond(cond, state) 219 | {head, [cond], state} 220 | end 221 | 222 | defp translate_head([param], caller) do 223 | {head, state} = translate_param(param, caller) 224 | {head, [], state} 225 | end 226 | 227 | defp translate_head(expr, _caller), do: raise_parameter_error(expr) 228 | 229 | defp translate_param(param, caller) do 230 | param = Macro.expand(param, %{caller | context: :match}) 231 | 232 | {param, state} = 233 | case param do 234 | {:=, _, [{name, _, context}, param]} when is_atom(name) and is_atom(context) -> 235 | state = %{vars: %{{name, context} => "$_"}, count: 0, caller: caller} 236 | {Macro.expand(param, %{caller | context: :match}), state} 237 | 238 | {:=, _, [param, {name, _, context}]} when is_atom(name) and is_atom(context) -> 239 | state = %{vars: %{{name, context} => "$_"}, count: 0, caller: caller} 240 | {Macro.expand(param, %{caller | context: :match}), state} 241 | 242 | {name, _, context} when is_atom(name) and is_atom(context) -> 243 | {param, %{vars: %{}, count: 0, caller: caller}} 244 | 245 | {:{}, _, list} when is_list(list) -> 246 | {param, %{vars: %{}, count: 0, caller: caller}} 247 | 248 | {:%{}, _, list} when is_list(list) -> 249 | {param, %{vars: %{}, count: 0, caller: caller}} 250 | 251 | {_, _} -> 252 | {param, %{vars: %{}, count: 0, caller: caller}} 253 | 254 | _ -> 255 | raise_parameter_error(param) 256 | end 257 | 258 | do_translate_param(param, state) 259 | end 260 | 261 | defp do_translate_param({:_, _, context}, state) when is_atom(context) do 262 | {:_, state} 263 | end 264 | 265 | defp do_translate_param({name, _, context}, state) when is_atom(name) and is_atom(context) do 266 | if match_var = state.vars[{name, context}] do 267 | {:"#{match_var}", state} 268 | else 269 | match_var = "$#{state.count + 1}" 270 | 271 | state = %{ 272 | state 273 | | vars: Map.put(state.vars, {name, context}, match_var), 274 | count: state.count + 1 275 | } 276 | 277 | {:"#{match_var}", state} 278 | end 279 | end 280 | 281 | defp do_translate_param({left, right}, state) do 282 | do_translate_param({:{}, [], [left, right]}, state) 283 | end 284 | 285 | defp do_translate_param({:{}, _, list}, state) when is_list(list) do 286 | {list, state} = Enum.map_reduce(list, state, &do_translate_param(&1, &2)) 287 | {List.to_tuple(list), state} 288 | end 289 | 290 | defp do_translate_param({:^, _, [expr]}, state) do 291 | {{:unquote, [], [expr]}, state} 292 | end 293 | 294 | defp do_translate_param(list, state) when is_list(list) do 295 | Enum.map_reduce(list, state, &do_translate_param(&1, &2)) 296 | end 297 | 298 | defp do_translate_param(literal, state) when is_literal(literal) do 299 | {literal, state} 300 | end 301 | 302 | defp do_translate_param({:%{}, _, list}, state) do 303 | Enum.reduce(list, {%{}, state}, fn {key, value}, {map, state} -> 304 | {key, key_state} = do_translate_param(key, state) 305 | {value, value_state} = do_translate_param(value, key_state) 306 | {Map.put(map, key, value), value_state} 307 | end) 308 | end 309 | 310 | defp do_translate_param(expr, _state), do: raise_parameter_error(expr) 311 | 312 | defp is_expandable(ast, env) do 313 | expansion = Macro.expand_once(ast, env) 314 | if ast !== expansion, do: expansion, else: false 315 | end 316 | 317 | defp raise_expression_error(expr) do 318 | message = "illegal expression in matchspec: #{Macro.to_string(expr)}" 319 | raise ArgumentError, message: message 320 | end 321 | 322 | defp raise_parameter_error(expr) do 323 | message = 324 | "illegal parameter to matchspec (has to be a single variable or tuple): #{Macro.to_string(expr)}" 325 | 326 | raise ArgumentError, message: message 327 | end 328 | end 329 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Ex2ms.Mixfile do 2 | use Mix.Project 3 | 4 | @version "1.7.0" 5 | @github_url "https://github.com/ericmj/ex2ms" 6 | 7 | def project do 8 | [ 9 | app: :ex2ms, 10 | version: @version, 11 | elixir: "~> 1.7", 12 | deps: deps(), 13 | docs: docs(), 14 | description: "Translates Elixir functions to match specifications for use with `ets`.", 15 | package: package() 16 | ] 17 | end 18 | 19 | def application do 20 | [] 21 | end 22 | 23 | defp deps do 24 | [{:ex_doc, ">= 0.0.0", only: :dev}] 25 | end 26 | 27 | defp docs do 28 | [ 29 | source_ref: "v#{@version}", 30 | source_url: @github_url, 31 | main: "readme", 32 | extras: ["README.md"] 33 | ] 34 | end 35 | 36 | defp package do 37 | [ 38 | maintainers: ["Eric Meadows-Jönsson"], 39 | licenses: ["Apache-2.0"], 40 | links: %{"GitHub" => @github_url} 41 | ] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, 3 | "ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"}, 4 | "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, 6 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.4", "29563475afa9b8a2add1b7a9c8fb68d06ca7737648f28398e04461f008b69521", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f4ed47ecda66de70dd817698a703f8816daa91272e7e45812469498614ae8b29"}, 7 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 8 | } 9 | -------------------------------------------------------------------------------- /test/ex2ms_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ex2msTest do 2 | use ExUnit.Case, async: true 3 | 4 | doctest Ex2ms 5 | 6 | require Record 7 | Record.defrecordp(:user, [:name, :age]) 8 | 9 | import TestHelpers 10 | import Ex2ms 11 | 12 | test "basic" do 13 | assert (fun do 14 | x -> x 15 | end) == [{:"$1", [], [:"$1"]}] 16 | end 17 | 18 | test "$_" do 19 | assert (fun do 20 | {x, y} = z -> z 21 | end) == [{{:"$1", :"$2"}, [], [:"$_"]}] 22 | end 23 | 24 | test "gproc" do 25 | assert (fun do 26 | {{:n, :l, {:client, id}}, pid, _} -> {id, pid} 27 | end) == [{{{:n, :l, {:client, :"$1"}}, :"$2", :_}, [], [{{:"$1", :"$2"}}]}] 28 | end 29 | 30 | test "gproc with bound variables" do 31 | id = 5 32 | 33 | assert (fun do 34 | {{:n, :l, {:client, ^id}}, pid, _} -> pid 35 | end) == [{{{:n, :l, {:client, 5}}, :"$1", :_}, [], [:"$1"]}] 36 | end 37 | 38 | test "gproc with 3 variables" do 39 | assert (fun do 40 | {{:n, :l, {:client, id}}, pid, third} -> {id, pid, third} 41 | end) == [ 42 | {{{:n, :l, {:client, :"$1"}}, :"$2", :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]} 43 | ] 44 | end 45 | 46 | test "gproc with 1 variable and 2 bound variables" do 47 | one = 11 48 | two = 22 49 | 50 | ms = 51 | fun do 52 | {{:n, :l, {:client, ^one}}, pid, ^two} -> {^one, pid} 53 | end 54 | 55 | self_pid = self() 56 | assert ms == [{{{:n, :l, {:client, 11}}, :"$1", 22}, [], [{{{:const, 11}, :"$1"}}]}] 57 | assert {:ok, {one, self_pid}} === :ets.test_ms({{:n, :l, {:client, 11}}, self_pid, two}, ms) 58 | end 59 | 60 | test "cond" do 61 | assert (fun do 62 | x when true -> 0 63 | end) == [{:"$1", [true], [0]}] 64 | 65 | assert (fun do 66 | x when true and false -> 0 67 | end) == [{:"$1", [{:andalso, true, false}], [0]}] 68 | end 69 | 70 | test "multiple funs" do 71 | ms = 72 | fun do 73 | x -> 0 74 | y -> y 75 | end 76 | 77 | assert ms == [{:"$1", [], [0]}, {:"$1", [], [:"$1"]}] 78 | end 79 | 80 | test "multiple exprs in body" do 81 | ms = 82 | fun do 83 | x -> 84 | x 85 | 0 86 | end 87 | 88 | assert ms == [{:"$1", [], [:"$1", 0]}] 89 | end 90 | 91 | test "guard function" do 92 | ms = 93 | fun do 94 | x when is_list(x) -> x 95 | end 96 | 97 | assert ms == [{:"$1", [is_list: :"$1"], [:"$1"]}] 98 | 99 | ms = 100 | fun do 101 | map when is_map_key(map, :key) -> map 102 | end 103 | 104 | assert ms == [{:"$1", [{:is_map_key, :"$1", :key}], [:"$1"]}] 105 | end 106 | 107 | test "invalid guard function" do 108 | assert_raise ArgumentError, "illegal expression in matchspec: does_not_exist(x)", fn -> 109 | delay_compile( 110 | fun do 111 | x when does_not_exist(x) -> x 112 | end 113 | ) 114 | end 115 | end 116 | 117 | test "custom guard macro" do 118 | ms = 119 | fun do 120 | x when custom_guard(x) -> x 121 | end 122 | 123 | assert ms == [{:"$1", [{:andalso, {:>, :"$1", 3}, {:"/=", :"$1", 5}}], [:"$1"]}] 124 | end 125 | 126 | test "nested custom guard macro" do 127 | ms = 128 | fun do 129 | x when nested_custom_guard(x) -> x 130 | end 131 | 132 | assert ms == [ 133 | { 134 | :"$1", 135 | [ 136 | { 137 | :andalso, 138 | {:andalso, {:>, :"$1", 3}, {:"/=", :"$1", 5}}, 139 | {:andalso, {:>, {:+, :"$1", 1}, 3}, {:"/=", {:+, :"$1", 1}, 5}} 140 | } 141 | ], 142 | [:"$1"] 143 | } 144 | ] 145 | end 146 | 147 | test "map is illegal alone in body" do 148 | assert_raise ArgumentError, "illegal expression in matchspec: %{x: z}", fn -> 149 | delay_compile( 150 | fun do 151 | {x, z} -> %{x: z} 152 | end 153 | ) 154 | end 155 | end 156 | 157 | test "map in head tuple" do 158 | ms = 159 | fun do 160 | {x, %{a: y, c: z}} -> {y, z} 161 | end 162 | 163 | assert ms == [{{:"$1", %{a: :"$2", c: :"$3"}}, [], [{{:"$2", :"$3"}}]}] 164 | end 165 | 166 | test "map is not allowed in the head of function" do 167 | assert_raise ArgumentError, 168 | "illegal parameter to matchspec (has to be a single variable or tuple): %{x: :\"$1\"}", 169 | fn -> 170 | delay_compile( 171 | fun do 172 | %{x: z} -> z 173 | end 174 | ) 175 | end 176 | end 177 | 178 | test "invalid fun args" do 179 | assert_raise FunctionClauseError, fn -> 180 | delay_compile(fun(123)) 181 | end 182 | end 183 | 184 | test "raise on invalid fun head" do 185 | assert_raise ArgumentError, 186 | "illegal parameter to matchspec (has to be a single variable or tuple): [x, y]", 187 | fn -> 188 | delay_compile( 189 | fun do 190 | x, y -> 0 191 | end 192 | ) 193 | end 194 | 195 | assert_raise ArgumentError, 196 | "illegal parameter to matchspec (has to be a single variable or tuple): y = z", 197 | fn -> 198 | delay_compile( 199 | fun do 200 | {x, y = z} -> 0 201 | end 202 | ) 203 | end 204 | 205 | assert_raise ArgumentError, 206 | "illegal parameter to matchspec (has to be a single variable or tuple): 123", 207 | fn -> 208 | delay_compile( 209 | fun do 210 | 123 -> 0 211 | end 212 | ) 213 | end 214 | end 215 | 216 | test "unbound variable" do 217 | assert_raise ArgumentError, 218 | "variable `y` is unbound in matchspec (use `^` for outer variables and expressions)", 219 | fn -> 220 | delay_compile( 221 | fun do 222 | x -> y 223 | end 224 | ) 225 | end 226 | end 227 | 228 | test "invalid expression" do 229 | assert_raise ArgumentError, "illegal expression in matchspec: x = y", fn -> 230 | delay_compile( 231 | fun do 232 | x -> x = y 233 | end 234 | ) 235 | end 236 | 237 | assert_raise ArgumentError, "illegal expression in matchspec: abc(x)", fn -> 238 | delay_compile( 239 | fun do 240 | x -> abc(x) 241 | end 242 | ) 243 | end 244 | end 245 | 246 | test "record" do 247 | ms = 248 | fun do 249 | user(age: x) = n when x > 18 -> n 250 | end 251 | 252 | assert ms == [{{:user, :_, :"$1"}, [{:>, :"$1", 18}], [:"$_"]}] 253 | 254 | x = 18 255 | 256 | ms = 257 | fun do 258 | user(name: name, age: ^x) -> name 259 | end 260 | 261 | assert ms == [{{:user, :"$1", 18}, [], [:"$1"]}] 262 | 263 | # Records nils will be converted to :_, if nils are needed, we should explicitly match on it 264 | ms = 265 | fun do 266 | user(age: age) = n when age == nil -> n 267 | end 268 | 269 | assert ms == [{{:user, :_, :"$1"}, [{:==, :"$1", nil}], [:"$_"]}] 270 | end 271 | 272 | test "action function" do 273 | ms = 274 | fun do 275 | _ -> return_trace() 276 | end 277 | 278 | assert ms == [{:_, [], [{:return_trace}]}] 279 | 280 | # action functions with arguments get turned into :atom, args... tuples 281 | ms = 282 | fun do 283 | arg when arg == :foo -> set_seq_token(:label, :foo) 284 | end 285 | 286 | assert ms == [{:"$1", [{:==, :"$1", :foo}], [{:set_seq_token, :label, :foo}]}] 287 | end 288 | 289 | test "composite bound variables in guards" do 290 | one = {1, 2, 3} 291 | 292 | ms = 293 | fun do 294 | arg when arg < ^one -> arg 295 | end 296 | 297 | assert ms == [{:"$1", [{:<, :"$1", {:const, {1, 2, 3}}}], [:"$1"]}] 298 | end 299 | 300 | test "composite bound variables in return value" do 301 | bound = {1, 2, 3} 302 | 303 | ms = 304 | fun do 305 | arg -> {^bound, arg} 306 | end 307 | 308 | assert ms == [{:"$1", [], [{{{:const, {1, 2, 3}}, :"$1"}}]}] 309 | assert {:ok, {bound, {:some, :record}}} === :ets.test_ms({:some, :record}, ms) 310 | end 311 | 312 | test "outer expressions get evaluated" do 313 | ms = 314 | fun do 315 | arg -> {^{1, 1 + 1, 3}, arg} 316 | end 317 | 318 | assert ms == [{:"$1", [], [{{{:const, {1, 2, 3}}, :"$1"}}]}] 319 | end 320 | 321 | defmacro test_contexts(var) do 322 | quote do 323 | var = {1, 2, 3} 324 | 325 | fun do 326 | {^var, _} -> ^unquote(var) 327 | end 328 | end 329 | end 330 | 331 | test "contexts are preserved" do 332 | var = 42 333 | ms = test_contexts(var) 334 | 335 | assert {:ok, 42} === :ets.test_ms({{1, 2, 3}, 123}, ms) 336 | end 337 | 338 | test "cons cells are working" do 339 | ms = 340 | fun do 341 | {k, l} when is_list(l) -> {k, [:marker | l]} 342 | end 343 | 344 | assert ms == [{{:"$1", :"$2"}, [is_list: :"$2"], [{{:"$1", [:marker | :"$2"]}}]}] 345 | end 346 | 347 | @tag skip: Version.compare(System.version(), "1.16.0") 348 | test "binary_part" do 349 | prefix = "1234" 350 | 351 | ms = 352 | fun do 353 | bid when binary_part(bid, 0, 4) == ^prefix -> bid 354 | end 355 | 356 | assert ms == [{:"$1", [{:==, {:binary_part, :"$1", 0, 4}, {:const, prefix}}], [:"$1"]}] 357 | assert {:ok, "12345"} == :ets.test_ms("12345", ms) 358 | end 359 | end 360 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.configure(exclude: [:skip]) 2 | ExUnit.start() 3 | 4 | defmodule TestHelpers do 5 | defmacro delay_compile(quoted) do 6 | quoted = Macro.escape(quoted) 7 | 8 | quote do 9 | Code.eval_quoted(unquote(quoted), [], __ENV__) 10 | end 11 | end 12 | 13 | defmacro custom_guard(x) do 14 | quote do 15 | unquote(x) > 3 and unquote(x) != 5 16 | end 17 | end 18 | 19 | defmacro nested_custom_guard(x) do 20 | quote do 21 | custom_guard(unquote(x)) and custom_guard(unquote(x) + 1) 22 | end 23 | end 24 | end 25 | --------------------------------------------------------------------------------