lib/kinda_codegen.ex

defmodule Kinda.CodeGen do
  @moduledoc """
  Behavior for customizing your source code generation.
  """

  alias Kinda.CodeGen.{KindDecl, NIFDecl}

  defmacro __using__(opts) do
    quote do
      mod = Keyword.fetch!(unquote(opts), :with)
      root = Keyword.fetch!(unquote(opts), :root)
      forward = Keyword.fetch!(unquote(opts), :forward)
      {ast, mf} = Kinda.CodeGen.nif_ast(mod.kinds(), mod.nifs(), root, forward)
      ast |> Code.eval_quoted([], __ENV__)
      mf
    end
  end

  @callback kinds() :: [KindDecl.t()]
  @callback nifs() :: [{atom(), integer()}]
  def kinds(), do: []

  def nif_ast(kinds, nifs, root_module, forward_module) do
    # generate stubs for generated NIFs
    extra_kind_nifs =
      kinds
      |> Enum.map(&NIFDecl.from_resource_kind/1)
      |> List.flatten()

    nifs = nifs ++ extra_kind_nifs

    for nif <- nifs do
      nif =
        case nif do
          {wrapper_name, arity} when is_atom(wrapper_name) and is_integer(arity) ->
            %NIFDecl{
              wrapper_name: wrapper_name,
              nif_name: Module.concat(root_module, wrapper_name),
              params: arity
            }

          {wrapper_name, params} when is_atom(wrapper_name) and is_list(params) ->
            %NIFDecl{
              wrapper_name: wrapper_name,
              nif_name: Module.concat(root_module, wrapper_name),
              params: params
            }

          %NIFDecl{} ->
            nif
        end

      {args_ast, arity} =
        if is_list(nif.params) do
          {Enum.map(nif.params, &Macro.var(&1, __MODULE__)), length(nif.params)}
        else
          {Macro.generate_unique_arguments(nif.params, __MODULE__), nif.params}
        end

      %NIFDecl{wrapper_name: wrapper_name, nif_name: nif_name} = nif

      wrapper_name =
        if is_bitstring(wrapper_name) do
          String.to_atom(wrapper_name)
        else
          wrapper_name
        end

      wrapper_ast =
        if nif_name != wrapper_name do
          quote do
            def unquote(wrapper_name)(unquote_splicing(args_ast)) do
              refs = Kinda.unwrap_ref([unquote_splicing(args_ast)])
              ret = apply(__MODULE__, unquote(nif_name), refs)
              unquote(forward_module).check!(ret)
            end
          end
        end

      quote do
        @doc false
        def unquote(nif_name)(unquote_splicing(args_ast)),
          do: :erlang.nif_error(:not_loaded)

        unquote(wrapper_ast)
      end
      |> then(&{&1, {nif_name, arity}})
    end
    |> Enum.unzip()
  end
end