defmodule Rustler.MatchSpec.Compiler do @moduledoc false @reserved_vars MapSet.new([:_, :__MODULE__, :__ENV__, :__CALLER__]) @variables Map.new(1..255, &{&1, :"$#{&1}"}) def compile_block({:__block__, _meta, clauses}, caller), do: Enum.map(clauses, &compile_clause(&1, caller)) def compile_block(clauses, caller) when is_list(clauses), do: Enum.map(clauses, &compile_clause(&1, caller)) def compile_block(clause, caller), do: [compile_clause(clause, caller)] defp compile_clause({:->, _meta, [[{:when, _when_meta, [head, guard]}], body]}, caller) do {compiled_head, bindings} = compile_pattern(head, %{}) compiled_guard = compile_expr(guard, bindings, caller) compiled_body = compile_expr(body_expr(body), bindings, caller) {compiled_head, [compiled_guard], [compiled_body]} end defp compile_clause({:->, _meta, [[head], body]}, caller) do {compiled_head, bindings} = compile_pattern(head, %{}) compiled_body = compile_expr(body_expr(body), bindings, caller) {compiled_head, [], [compiled_body]} end defp compile_clause(other, _caller) do raise ArgumentError, "expected match_spec clauses in the form `pattern [when guard] -> body`, got: #{Macro.to_string(other)}" end defp body_expr([expr]), do: expr defp body_expr(exprs) when is_list(exprs), do: {:__block__, [], exprs} defp body_expr(expr), do: expr defp compile_pattern({:_, _meta, context}, bindings) when is_atom(context), do: {:_, bindings} defp compile_pattern({name, _meta, context}, bindings) when is_atom(name) and is_atom(context) do if reserved_var?(name), do: {{name, [], context}, bindings}, else: bind_var(name, bindings) end defp compile_pattern({left, right}, bindings) do {left, bindings} = compile_pattern(left, bindings) {right, bindings} = compile_pattern(right, bindings) {{left, right}, bindings} end defp compile_pattern({:{}, _meta, elements}, bindings) do compile_tuple(elements, bindings) end defp compile_pattern({:%{}, _meta, pairs}, bindings) do Enum.map_reduce(pairs, bindings, fn {key, value}, bindings -> {value, bindings} = compile_pattern(value, bindings) {{key, value}, bindings} end) |> then(fn {pairs, bindings} -> {Map.new(pairs), bindings} end) end defp compile_pattern({name, _meta, args}, bindings) when is_atom(name) and is_list(args) do {args, bindings} = Enum.map_reduce(args, bindings, &compile_pattern/2) {List.to_tuple([name | args]), bindings} end defp compile_pattern(list, bindings) when is_list(list) do Enum.map_reduce(list, bindings, &compile_pattern/2) end defp compile_pattern(literal, bindings), do: {literal, bindings} defp compile_tuple(elements, bindings) do {elements, bindings} = Enum.map_reduce(elements, bindings, &compile_pattern/2) {List.to_tuple(elements), bindings} end defp bind_var(name, bindings) do case Map.fetch(bindings, name) do {:ok, variable} -> {variable, bindings} :error -> variable = variable_for(map_size(bindings) + 1) {variable, Map.put(bindings, name, variable)} end end defp compile_expr({name, _meta, context}, bindings, _caller) when is_atom(name) and is_atom(context) do if reserved_var?(name), do: {name, [], context}, else: Map.fetch!(bindings, name) end defp compile_expr({:in, _meta, [left, right]}, bindings, caller) do {:member, compile_expr(left, bindings, caller), compile_expr(right, bindings, caller)} end defp compile_expr({op, _meta, args}, bindings, caller) when op in [ :is_atom, :is_binary, :is_boolean, :is_float, :is_integer, :is_list, :is_map, :is_number, :is_tuple, :not, :and, :or, :andalso, :orelse, :xor, :==, :===, :!=, :!==, :>, :>=, :<, :<=, :+, :-, :*, :div, :rem ] and is_list(args) do {match_spec_op(op), Enum.map(args, &compile_expr(&1, bindings, caller))} |> tuple_from_op() end defp compile_expr({left, right}, bindings, caller) do {compile_expr(left, bindings, caller), compile_expr(right, bindings, caller)} end defp compile_expr({:{}, _meta, elements}, bindings, caller) do elements |> Enum.map(&compile_expr(&1, bindings, caller)) |> List.to_tuple() end defp compile_expr({:%{}, _meta, pairs}, bindings, caller) do Map.new(pairs, fn {key, value} -> {key, compile_expr(value, bindings, caller)} end) end defp compile_expr(list, bindings, caller) when is_list(list) do Enum.map(list, &compile_expr(&1, bindings, caller)) end defp compile_expr(literal, _bindings, _caller), do: literal defp tuple_from_op({op, args}), do: List.to_tuple([op | args]) defp reserved_var?(name), do: MapSet.member?(@reserved_vars, name) defp variable_for(index) when index in 1..255, do: @variables[index] defp variable_for(index) do raise ArgumentError, "match_spec supports at most 255 variables, got variable #{index}" end defp match_spec_op(:===), do: :"=:=" defp match_spec_op(:!==), do: :"=/=" defp match_spec_op(:==), do: :== defp match_spec_op(:!=), do: :"/=" defp match_spec_op(:<=), do: :"=<" defp match_spec_op(op), do: op end