defmodule Formular.Client.Compiler do
@moduledoc """
Compiler for making the string code into an Elixir module.
Here we use Agent to prevent concurrent compilation.
"""
alias Formular.Client.PubSub
use GenServer
require Logger
@reg Formular.Client.Compiler.Registry
@compile_timeout :timer.minutes(1)
@idle_timeout :timer.minutes(1)
def handle_new_code_revision({pid, ref}, name, code, config, opts) do
{:ok, server} = try_start(name)
timeout = Map.get(config, :read_timeout, @compile_timeout)
GenServer.call(server, {:compile, {pid, ref, code, config, opts}}, timeout)
:ok
end
defp try_start(name) do
case GenServer.start_link(__MODULE__, name, name: via_tuple(name)) do
{:ok, pid} -> {:ok, pid}
{:error, {:already_started, pid}} -> {:ok, pid}
end
end
defp via_tuple(name),
do: {:via, Registry, {@reg, name}}
@impl true
def init(name) do
{:ok, name}
end
@impl true
def handle_call({:compile, {pid, ref, code, config, opts}}, _from, name) do
case config do
%{compiler: {m, f, a}} ->
apply(m, f, [{code, name, opts} | a])
|> report_result({pid, ref}, {name, opts}, code)
%{compiler: f} when is_function(f, 1) ->
apply(f, [{code, name, opts}])
|> report_result({pid, ref}, {name, opts}, code)
end
{:reply, :ok, name, @idle_timeout}
end
@impl true
def handle_info(:timeout, name) do
{:stop, :normal, name}
end
def compile({code, name, opts}) do
mod = Keyword.get(opts, :compile_as)
Logger.info(["Recompiling formula: #{name} to module #{mod}. Code: #{inspect(code)}"])
case mod do
nil ->
:ok
_ when is_atom(mod) ->
temporarily_disable_compilation_warning(fn ->
do_compile(code, name, opts)
end)
end
end
defp do_compile(code, _name, opts) do
mod = Keyword.get(opts, :compile_as)
with {:module, ^mod} <- Formular.compile_to_module!(code, mod, opts) do
:ok
end
rescue
e in CompileError ->
{:error, e}
end
defp temporarily_disable_compilation_warning(f) do
prev_mc_config = Code.get_compiler_option(:ignore_module_conflict)
Code.put_compiler_option(:ignore_module_conflict, true)
ret = f.()
Code.put_compiler_option(:ignore_module_conflict, prev_mc_config)
ret
end
defp report_result(:ok, {pid, ref}, {name, opts}, code) do
broadcast_success(name, code, opts[:compile_as], opts[:context])
send(pid, {ref, :ok})
end
defp report_result({:error, e}, {pid, ref}, {name, opts}, code) do
broadcast_failure(e, name, code, opts[:compile_as], opts[:context])
send(pid, {ref, {:error, e}})
end
defp broadcast_success(name, code, mod, context) do
PubSub.dispatch({:compiled, name, code, mod: mod, context: context})
end
defp broadcast_failure(err, name, code, mod, context) do
PubSub.dispatch({:compile_failed, name, err, code, mod: mod, context: context})
end
end