defmodule Mix.Tasks.Compile.LockstepRewrite do @moduledoc """ Mix compiler that rewrites source files via `Lockstep.Rewriter` before the standard Elixir compiler runs. ## Setup In your `mix.exs`: def project, do: [ ... compilers: compilers(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()), lockstep_rewrite: lockstep_rewrite(Mix.env()), ] defp compilers(:test), do: [:lockstep_rewrite] ++ Mix.compilers() defp compilers(_), do: Mix.compilers() defp elixirc_paths(:test) do ["_build/test/lockstep_rewritten/lib", "test/support"] end defp elixirc_paths(_), do: ["lib"] defp lockstep_rewrite(:test) do %{paths: ["lib/**/*.ex", "src/**/*.erl"]} end defp lockstep_rewrite(_), do: nil In test env this will: 1. Read every `lib/**/*.ex` and `src/**/*.erl` file. 2. Run `.ex` through `Lockstep.Rewriter` and `.erl` through `Lockstep.ErlangRewriter` -- both rewrite vanilla OTP calls (`GenServer.call`, `gen_server:call`, `send/2`, `Pid ! Msg`, bare `receive`, ...) to their `Lockstep.*` controller-aware equivalents. 3. Write the rewritten copy to `_build/test/lockstep_rewritten/...`. 4. The standard compilers then read from there because `elixirc_paths(:test)` and `erlc_paths(:test)` are overridden. In dev / prod, the original sources compile normally; the rewriter never runs. ## Configuration The `:lockstep_rewrite` project option takes a map: * `:paths` (required) -- list of glob patterns to rewrite. * `:output` (optional) -- output directory. Defaults to `_build/$ENV/lockstep_rewritten`. Set the option to `nil` (or omit it) to skip rewriting in that env. ## Limitations * Comments and source formatting are lost (the rewriter round- trips through AST). * Only handles user code matching the configured paths. Dep libraries (in `deps/`) need a separate workflow because the dep's compiled BEAM in `_build/$ENV/lib//ebin` would conflict with our rewritten copy. See the README's "Rewriting deps" section. """ use Mix.Task.Compiler @impl Mix.Task.Compiler def run(_args) do case Mix.Project.config()[:lockstep_rewrite] do nil -> {:ok, []} config when is_map(config) -> do_run(config) end end defp do_run(config) do case Lockstep.MixCompiler.compile(config) do {:ok, files} -> if files != [], do: Mix.shell().info("[lockstep] rewrote #{length(files)} file(s)") {:ok, []} {:error, {:parse_error, path, meta, message, token}} -> diagnostic = %Mix.Task.Compiler.Diagnostic{ file: path, severity: :error, message: "lockstep_rewrite parse error: #{message} #{inspect(token)}", position: Keyword.get(meta, :line, 0), compiler_name: "lockstep_rewrite" } {:error, [diagnostic]} end end end