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 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, }`. 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 |> add_fingerprint(zig_code) |> 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(%{precompiled: nil} = 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 precompile(_), do: :ok 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 Zig._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 defp add_fingerprint(opts, code) do import Bitwise fingerprint = :erlang.crc32("#{opts.otp_app}") <<< 32 ||| :erlang.crc32(code) %{opts | fingerprint: "0x#{Integer.to_string(fingerprint, 16)}"} end end