lib/zig/compiler.ex

defmodule Zig.Compiler do
  @moduledoc """
  handles instrumenting elixir code with hooks for zig NIFs.
  """

  # TODO: refactor to use a struct

  require Logger

  alias Zig.Assembler
  alias Zig.Command
  alias Zig.EasyC
  alias Zig.Manifest
  alias Zig.Nif
  alias Zig.Options
  alias Zig.Sema
  alias Zig.Manifest

  defmacro __before_compile__(%{module: module, file: file}) do
    # NOTE: this is going to be called only from Elixir.  Erlang will not call this.
    # all functionality in this macro must be replicated when running compilation from
    # erlang.

    # TODO: verify that :otp_app exists
    code_dir = Path.dirname(file)

    opts =
      module
      |> Module.get_attribute(:zigler_opts)
      |> Options.normalize!()

    module
    |> Module.get_attribute(:zig_code_parts)
    |> Enum.reverse()
    |> IO.iodata_to_binary()
    |> compile(module, code_dir, Keyword.put(opts, :render, :render_elixir))
    |> Zig.Macro.inspect(opts)
  end

  # note that this directory is made public so that it can be both accessed
  # from the :zigler entrypoint for erlang parse transforms, as well as the
  # __before_compile__ entrypoint for Elixir
  def compile(base_code, module, code_dir, opts) do
    module_nif_zig = Path.join(code_dir, ".#{module}.zig")
    opts = Keyword.merge(opts, file: module_nif_zig)

    assembly_directory = Assembler.directory(module)

    # obtain the code
    easy_c_code = List.wrap(if opts[:easy_c], do: EasyC.build_from(opts))

    aliasing_code =
      case opts[:nifs] do
        {:auto, overridden} -> create_aliases(nifs: overridden)
        functions when is_list(functions) -> create_aliases(nifs: functions)
      end

    full_code = IO.iodata_to_binary([aliasing_code, easy_c_code, base_code])

    File.write!(module_nif_zig, full_code)

    if opts[:easy_c] do
      Command.fmt(module_nif_zig)
    end

    assembled = Keyword.get(opts, :assemble, true)
    precompiled = Keyword.get(opts, :precompile, true)
    compiled = Keyword.get(opts, :compile, true)
    renderer = Keyword.fetch!(opts, :render)

    with true <- assembled,
         assemble_opts =
           Keyword.take(opts, [:link_lib, :build_opts, :stage1, :include_dir, :c_src, :packages]),
         assemble_opts = Keyword.merge(assemble_opts, from: code_dir),
         Assembler.assemble(module, assemble_opts),
         true <- precompiled,
         # TODO: verify that this parsed correctly.
         manifest = Manifest.create(full_code),
         new_opts = Keyword.merge(opts, manifest: manifest),
         file = Keyword.fetch!(opts, :file),
         sema_result = Sema.run_sema!(file, module, new_opts),
         parsed_code = Zig.Parser.parse(base_code),
         new_opts = Keyword.merge(new_opts, parsed: parsed_code),
         sema_nifs = Sema.analyze_file!(sema_result, new_opts),
         new_opts = Keyword.put(new_opts, :nifs, sema_nifs),
         function_code = precompile(module, assembly_directory, new_opts),
         {true, _} <- {compiled, new_opts} do
      # parser should only operate on parsed, valid zig code.
      Command.compile(module, new_opts)
      apply(Zig.Module, renderer, [base_code, function_code, module, manifest, new_opts])
    else
      false ->
        apply(Zig.Module, renderer, [base_code, [], module, [], opts])

      {false, new_opts} ->
        apply(Zig.Module, renderer, [base_code, [], module, [], new_opts])
    end
  end

  defp precompile(module, directory, opts) do
    render_fn = Keyword.fetch!(opts, :render)

    nif_functions =
      opts
      |> Keyword.fetch!(:nifs)
      |> Enum.map(fn {name, nif_opts} ->
        doc =
          opts
          |> Keyword.fetch!(:parsed)
          |> Map.fetch!(:code)
          |> Enum.find_value(fn
            %{name: ^name, doc_comment: doc_comment} -> doc_comment
            _ -> nil
          end)

        Nif.new(name, Keyword.put(nif_opts, :doc, doc))
      end)

    function_code = Enum.map(nif_functions, &apply(Nif, render_fn, [&1]))

    nif_src_path = Path.join(directory, "module.zig")

    resource_opts = Keyword.get(opts, :resources, [])
    callbacks = Keyword.get(opts, :callbacks)

    File.write!(
      nif_src_path,
      Zig.Module.render_zig(nif_functions, resource_opts, callbacks, module)
    )

    Command.fmt(nif_src_path)

    Logger.debug("wrote module.zig to #{nif_src_path}")

    function_code
  end

  require EEx
  zig_alias_template = Path.join(__DIR__, "templates/alias.zig.eex")
  EEx.function_from_file(:defp, :create_aliases, zig_alias_template, [:assigns])

  defp dependencies_for(assemblies) do
    Enum.map(assemblies, fn assembly ->
      quote do
        @external_resource unquote(assembly.source)
      end
    end)
  end

  #############################################################################
  ## STEPS

  def assembly_dir(env, module) do
    System.tmp_dir()
    |> String.replace("\\", "/")
    |> Path.join(".zigler_compiler/#{env}/#{module}")
  end
end