Current section

Files

Jump to
zigler lib zig compiler.ex
Raw

lib/zig/compiler.ex

defmodule Zig.Compiler do
@moduledoc false
# handles instrumenting elixir code with hooks for zig NIFs.
require Logger
alias Zig.Attributes
alias Zig.Builder
alias Zig.Command
alias Zig.EasyC
alias Zig.Manifest
alias Zig.Nif
alias Zig.Parser
alias Zig.Sema
@nofile ~w[nofile iex]
defmacro __before_compile__(%{module: module, file: file} = env) 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.
opts =
module
|> Module.get_attribute(:zigler_opts)
|> Keyword.put(:attributes, Attributes.from_module(module))
|> Keyword.update(:nifs, {:auto, []}, &normalize_elixir_nif_spec/1)
|> Zig.Module.new(__CALLER__)
code_dir =
case {opts.dir, file} do
{nil, nofile} when nofile in @nofile -> File.cwd!()
{nil, file} -> Path.dirname(file)
{dir, _} -> dir
end
code =
cond do
opts.easy_c ->
if match?({:auto, _}, opts.nifs) do
raise CompileError,
file: file,
line: env.line,
description: "easy-c mode cannot have automatically detected nifs"
end
EasyC.build_from(opts)
path = opts.zig_code_path ->
# check for existence of :zig_code_parts
unless [] == Module.get_attribute(module, :zig_code_parts) do
raise CompileError,
file: file,
line: env.line,
description:
"(module #{inspect(module)}) you may not use ~Z when `:zig_code_path` is specified"
end
path =
if is_binary(path) do
env.file
|> Path.dirname()
|> Path.join(path)
else
# This allows for using module attributes and
# other non-literal constructs for the file path
{result, _} = Code.eval_quoted(path, [], env)
result
end
# Register the file as an @external_resource so that the
# module recompiles whenever the zig source changes
Module.put_attribute(module, :external_resource, path)
File.read!(path)
:else ->
module
|> Module.get_attribute(:zig_code_parts)
|> Enum.reverse()
|> then(
&[
"// this code is autogenerated, do not check it into to your code repository\n\n"
| &1
]
)
|> IO.iodata_to_binary()
end
code
|> compile(code_dir, opts)
|> Zig.Macro.inspect(opts)
end
# if the elixir `nif` option contains `...` then this should be converted
# into `{:auto, <other_options>}`. Also, if the nif entry is just an atom,
# converts that entry into `{nif, []}`
#
# This function might reverse the list, but since order doesn't matter for this
# option, it is okay.
defp normalize_elixir_nif_spec(:auto), do: {:auto, []}
defp normalize_elixir_nif_spec({:auto, list}) do
{:auto,
Enum.map(list, fn
nif_name when is_atom(nif_name) ->
{nif_name, []}
{_, _} = kv ->
kv
end)}
end
defp normalize_elixir_nif_spec(opts) do
Enum.reduce(opts, [], fn
{:..., _, _}, {:auto, _} = so_far -> so_far
{:..., _, _}, list -> {:auto, list}
other, so_far -> prepend_nif(so_far, other)
end)
end
defp prepend_nif({:auto, so_far}, nif_name) when is_atom(nif_name),
do: {:auto, [{nif_name, []} | so_far]}
defp prepend_nif(so_far, nif_name) when is_atom(nif_name), do: [{nif_name, []} | so_far]
defp prepend_nif({:auto, so_far}, nif_info), do: {:auto, [nif_info | so_far]}
defp prepend_nif(so_far, nif_info), do: [nif_info | so_far]
# note that this function 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(zig_code, code_dir, opts) do
zig_code_path =
code_dir
|> Path.join(".#{opts.module}.zig")
|> Path.expand()
opts
|> Map.replace!(:zig_code_path, zig_code_path)
|> tap(&write_code!(&1, zig_code))
|> Builder.stage()
|> Manifest.create(zig_code)
|> Sema.run_sema!()
|> apply_parser(zig_code)
|> Sema.analyze_file!()
|> verify_nifs_exist()
|> add_nif_resources()
|> bind_documentation()
|> tap(&precompile/1)
|> Command.compile!()
|> Manifest.unload()
|> elixir_save_zigler_opts()
|> case do
%{language: Elixir} = module ->
Zig.Module.render_elixir(module, zig_code)
%{language: :erlang} = module ->
Zig.Module.render_erlang(module, zig_code)
end
end
defp write_code!(module, zig_code) do
File.write!(module.zig_code_path, zig_code)
end
defp apply_parser(module, zig_code) do
parsed =
zig_code
|> String.replace("\r\n", "\n")
|> Parser.parse()
external_resources =
parsed
|> recursive_resource_search(module.file, MapSet.new())
|> Enum.to_list()
%{module | parsed: parsed, external_resources: external_resources}
end
defp precompile(module) do
path =
module.module
|> Builder.staging_directory()
|> Path.join("module.zig")
File.write!(path, Zig.Module.render_zig(module))
Command.fmt(path)
Logger.debug("wrote module code to #{path}")
end
defp recursive_resource_search(parsed, path, so_far) do
Enum.reduce(parsed.dependencies, so_far, fn dep, so_far ->
dep_path =
dep
|> Path.expand(Path.dirname(path))
|> Path.relative_to_cwd()
cond do
dep_path in so_far ->
so_far
Path.extname(dep_path) == ".zig" ->
dep_path
|> File.read!()
|> Parser.parse()
|> recursive_resource_search(dep_path, MapSet.put(so_far, dep_path))
true ->
MapSet.put(so_far, dep_path)
end
end)
end
#############################################################################
## STEPS
def assembly_dir(env, module) do
System.tmp_dir()
|> String.replace("\\", "/")
|> Path.join(".zigler_compiler/#{env}/#{module}")
end
defp verify_nifs_exist(module) do
if module.nifs == [] do
raise CompileError, description: "no nifs found in module.", file: module.file
end
module
end
defp add_nif_resources(module) do
# some nifs (threaded, yielding) must use their own resources to work correctly.
# this adds those resources to the list.
nif_resources = Enum.flat_map(module.nifs, &Nif.resources/1)
%{module | resources: module.resources ++ nif_resources}
end
defp bind_documentation(module) do
Map.update!(module, :nifs, fn nifs ->
Enum.map(nifs, &bind_nif_documentation(&1, module.parsed.code))
end)
end
defp bind_nif_documentation(%{name: name} = nif, code) do
Map.replace!(nif, :doc, Enum.find_value(code, &doc_if_name(&1, name)))
end
defp doc_if_name(%{name: name, doc_comment: comment}, name),
do: if(comment, do: String.trim(comment))
defp doc_if_name(_, _), do: nil
defp elixir_save_zigler_opts(%{language: Elixir} = opts) do
Module.put_attribute(opts.module, :zigler_opts, opts)
opts
end
defp elixir_save_zigler_opts(opts), do: opts
end