lib/jx/catalog.ex

defmodule Jx.Catalog.Helper do
  @moduledoc false

  def define_catalog_module(module, options \\ []) do
    only_keys = case options[:only] do
      nil ->
        module |> apply(:__info__, [:functions]) |> MapSet.new

      names when is_list(names) ->
        MapSet.new(names)
    end
    except = options |> Keyword.get(:except, []) |> MapSet.new
    included_keys = MapSet.difference(only_keys, except)

    extra_defs = Keyword.get(options, :defs, [])

    quote do
      defmodule unquote(Module.concat(Jx.Catalog, module)) do
        @moduledoc false

        unquote_splicing(extra_defs)

        def j({:&, _, [{:/, _, [{:j, _, _}, arity]}]} = a) do
          included_keys = unquote(Macro.escape(included_keys))

          index = Enum.flat_map(included_keys, fn
            {name, ^arity} -> [Function.capture(unquote(module), name, arity)]
            _ -> []
          end)

          %Jx{index: index}
        end

        def j(expr) do
          nil
        end
      end
    end
  end
end

defmodule Jx.Catalog do
  @moduledoc """
  This module tracks the modules and functions that are used as possible matches in function matching. 
  """
  alias __MODULE__
  alias Jx.Catalog.Helper

  module_info = [
    {
      Function, only: [identity: 1],
      defs: [
        quote do
          def j({:=, _, [pattern, {:identity, _, [arg1]}]}) do
            %Jx{expr: {:=, [], [pattern, arg1]}, index: nil}
          end
        end
      ]
    },
    {
      Integer, except: [to_char_list: 1, to_char_list: 2],
      defs: [
        quote do
          def j({:=, _, [pattern, {:pow, _, args}]}) do
            Catalog.Kernel.j({:=, [], [pattern, {:**, [], args}]})
          end
        end
      ]
    }, 
    { 
      List, 
      defs: [
        quote do
          def j({:=, _, [{:j, _, _}, {:duplicate, _, _}]}) do
            nil
          end

          def j({:=, _, [lhs, {:duplicate, _, [_, b]}]}) when is_list(lhs) and is_integer(b) do
            if length(lhs) !== b do
              %Jx{no_match: true}
            else
              nil
            end
          end

          def j({:=, _, [lhs, {:duplicate, _, [_, _]}]}) do
            %Jx{no_match: true}
          end
        end
      ]
    },
    Tuple, Bitwise, 
    {
      Enum, except: [chunk: 2, partition: 2, uniq: 2, shuffe: 1, random: 1],
      defs: [
        quote do
          def j({:=, _, [_, {:group_by, _, [_enumerable, key_fun]}]}) when not is_function(key_fun) do
            %Jx{no_match: true}
          end

          def j({:=, _, [_, {:into, _, [_, [_ | _]]}]}) do
            %Jx{no_match: true}
          end
        end
      ] 
    },
    { 
      Keyword, except: [size: 1, map: 2]
    }, 
    {
      String, except: [
        to_atom: 1, rstrip: 1, valid_character?: 1, strip: 1, lstrip: 1, next_grapheme_size: 1, to_char_list: 1, ljust: 2, rjust: 2, strip: 2, lstrip: 2, rstrip: 2
      ]
    },
    {
      Kernel, only: [
        **: 2, ++: 2, --: 2, get_in: 2, max: 2, min: 2, put_elem: 3,
        *: 2, +: 1, +: 2, -: 1, -: 2, /: 2, !=: 2, !==: 2, <: 2, <=: 2, ==: 2, ===: 2, >: 2, >=: 2,
        abs: 1, binary_part: 3, bit_size: 1, byte_size: 1, ceil: 1, div: 2, elem: 2, floor: 1, hd: 1, 
        is_atom: 1, is_binary: 1, is_bitstring: 1, is_boolean: 1, is_float: 1, is_function: 1, is_function: 2, is_integer: 1, is_list: 1, is_map: 1,
        is_map_key: 2, is_number: 1, is_pid: 1, is_port: 1, is_reference: 1, is_tuple: 1, 
        length: 1, map_size: 1, not: 1, rem: 2, round: 1, tl: 1, trunc: 1, tuple_size: 1
      ],
      defs: [
        quote do
          def j({:=, _, [x, {:+, _, [a, {:j, _, _} = j]}]}) do
            %Jx{expr: {:=, [], [x - a, j]}, index: nil}
          end

          def j({:=, _, [x, {:+, _, [{:j, _, _} = j, a]}]}) do
            %Jx{expr: {:=, [], [x - a, j]}, index: nil}
          end

          def j({:=, _, [x, {:*, _, [0, {:j, _, _} = j]}]}) when is_integer(x) and x !== 0 do
            %Jx{no_match: true} 
          end

          def j({:=, _, [x, {:*, _, [{:j, _, _} = j, 0]}]}) when is_integer(x) and x !== 0 do
            %Jx{no_match: true} 
          end

          def j({:=, _, [x, {:*, _, [a, {:j, _, _} = j]}]}) when is_integer(x) and is_integer(a) and a !== 0 do
            %Jx{expr: {:=, [], [div(x, a), j]}, index: nil} 
          end

          def j({:=, _, [x, {:*, _, [{:j, _, _} = j, a]}]}) when is_integer(x) and is_integer(a) and a !== 0 do
            %Jx{expr: {:=, [], [div(x, a), j]}, index: nil} 
          end

          def j({:=, _, [x, {:**, _, [1, _]}]}) when is_integer(x) and x > 1 do
            %Jx{no_match: true}
          end

          def j({:=, _, [x, {:**, _, [a, term]}]} = expr) when is_integer(x) and x > 0 and is_integer(a) and a > 1 do
            case :math.log2(x) / :math.log2(a) do
              result when result - trunc(result) < 0.00001 ->
                %Jx{expr: {:=, [], [trunc(result), term]}, index: nil}
              _ ->
                %Jx{no_match: true}
            end
          end

          def j({:=, _, [x, {:**, _, [x, b]}]}) when is_integer(x) and x > 0 and is_integer(b) do
            case {x, b} do
              {0, 0} -> %Jx{no_match: true}
              {1, 0} -> %Jx{expr: {:=, [], [x, x]}, index: nil}
              {_, 1} -> %Jx{expr: {:=, [], [x, x]}, index: nil}
              {_, _} -> %Jx{no_match: true}
            end
          end

          def j({:=, _, [_, {:**, _, _}]}) do
            %Jx{no_match: true}
          end
        end
      ]
    }
  ]

  module_info
  |> Stream.map(fn
    (module) when is_atom(module) -> {module, []}
    info -> info
  end)
  |> Enum.each(fn {module, options} ->
      quoted_code = Helper.define_catalog_module(module, options)
      Module.eval_quoted(__MODULE__, quoted_code)
  end)

  @_modules Enum.map(module_info, fn {name, _} -> name; name -> name end)

  @doc """
  Returns a `%Jx{}` context that searches the modules of the catalog for a match.
  
  ## Examples
  ```elixir 
  iex> Jx.Catalog.j(quote(do: &j/1))
  #Jx<>
  ```
  """
  def j({:&, _, [{:/, _, [{:j, _, atom}, _arity]}]} = expr) when is_atom(atom) do
    modules = @_modules
    index = for m <- modules, do: {Module.concat(__MODULE__, m), :j, [expr]}
    %Jx{index: index}
  end
end