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