defmodule :zigler do @moduledoc """ Parse transform module for using Zigler with erlang. For the canonical example, see: https://www.erlang.org/doc/man/erl_id_trans.html ## Prerequisites In order to use Zigler in an erlang project, you must have the Elixir runtime. You may do this any way you wish, but Zigler recommends rebar_mix: https://github.com/Supersonido/rebar_mix There are instructions on how to make sure Elixir is available at compile time for your erlang project. ## Building a Zig Module General documentation on parse transforms is very light. To use zigler as a parse transform: ```erlang -module(my_erlang_module). -compile({parse_transform, zigler}). -export([...]). -zig_code(" pub fn hello_world() [] const u8 { return "Hello, world!"; } ") -zig_opts([{otp_app, my_app}]). ``` This creates the `hello_world/0` function in your module which returns the "Hello, world!" binary. for options to be delivered in the `zig_opts` attribute, see the `Zig` module documentation. Note that the `...` for the `nifs` option is not representable in erlang AST. Instead, use the atom `auto`. > ### Note {: .warning } > > Erlang integration is highly experimental and the interface > may be changed in the future. """ @typedoc "foobarbaz" @type foo :: term alias Zig.Compiler alias Zig.EasyC @doc """ performs a parse transformation on the AST for an erlang module, converting public functions in the """ def parse_transform(ast, _opts) do Application.ensure_all_started(:logger) {line, opts} = case Enum.find(ast, &match?({:attribute, _, :zig_opts, _}, &1)) do nil -> raise "No zig opts found" {:attribute, {line, _}, :zig_opts, opts} -> {line, opts} end otp_app = case Keyword.fetch(opts, :otp_app) do {:ok, otp_app} -> otp_app _ -> raise "No otp_app found in zig opts" end # utility functions: Make sure certain apps are running # and make sure certain libraries are loaded. ensure_eex() ensure_libs() ensure_priv_dir(otp_app) {:attribute, _, :file, {file, _}} = Enum.find(ast, &match?({:attribute, _, :file, _}, &1)) module_dir = file |> Path.dirname() |> Path.absname() module = case Enum.find(ast, &match?({:attribute, _, :module, _}, &1)) do nil -> raise "No module definition found" {:attribute, _, :module, module} -> module end code = if opts[:easy_c] do EasyC.build_from(opts) else ast |> Enum.flat_map(fn {:attribute, {line, _}, :zig_code, code} -> ["// ref #{file}:#{line}", code] _ -> [] end) |> IO.iodata_to_binary() end code_dir = case Keyword.fetch(opts, :code_dir) do {:ok, code_dir = "/" <> _} -> code_dir {:ok, code_dir} -> Path.join(module_dir, code_dir) _ -> module_dir end module_struct = Compiler.before_compile_erlang(module, {file, line}, opts) rendered = Compiler.compile(code, code_dir, module_struct) ast = ast |> Enum.reject(&match?({:attribute, _, :zig_code, _}, &1)) |> append_attrib(:on_load, {:__init__, 0}) |> append_attrib(:zig_code, code) |> Kernel.++(rendered) |> Enum.sort(__MODULE__) if opts[:dump] do dump(ast) else ast end rescue e -> require Logger Logger.error( "Error compiling Zigler: #{Exception.message(e)}\n\n#{Exception.format_stacktrace(__STACKTRACE__)}" ) end defp append_attrib(ast, key, value), do: ast ++ [{:attribute, {1, 1}, key, value}] @order %{file: 0, attribute: 1, function: 2, error: 3, eof: 10} @doc false # This is a comparison function for sorting the AST. By implementing # the compare/2 informal behaviour, we are able to sort the contents # of erlang ast def compare(ast1, ast2) do case {Map.fetch!(@order, elem(ast1, 0)), Map.fetch!(@order, elem(ast2, 0))} do {order1, order2} when order1 < order2 -> :lt {order1, order2} when order1 > order2 -> :gt _ -> :eq end end defp ensure_eex do # rebar_mix doesn't include the eex dependency out of the gate. This function # retrieves the location of the eex code and adds it to the code path, ensuring # that the BEAM vm will have access to the eex code (at least, at compile time) :eex |> path_relative_to(:elixir) |> String.to_charlist() |> :code.add_path() end defp ensure_libs do # we can't put this dependency into rebar.config because it has resolution issues # since jason decimal requirement is 1.0.0 or 2.0.0 Enum.each(~w[jason zig_parser]a, fn lib -> lib |> path_relative_to(:zigler) |> String.to_charlist() |> :code.add_path() end) end defp ensure_priv_dir(otp_app) do # makes sure the priv dir exists otp_app |> :code.lib_dir() |> Path.join("priv") |> File.mkdir_p!() end defp path_relative_to(target, start) do start |> :code.lib_dir() |> Path.join("../#{target}/ebin") |> Path.absname() |> to_string end defp dump(ast) do Enum.each(ast, fn tuple when elem(tuple, 0) == :attribute -> tuple |> :erl_pp.attribute() |> IO.puts() tuple when elem(tuple, 0) == :function -> tuple |> :erl_pp.function() |> IO.puts() _ -> :ok end) ast end end