# `Mutare.Sandbox.CompilerOptions`
[🔗](https://github.com/foxbenjaminfox/mutare/blob/v0.2.1/lib/mutare/sandbox/compiler_options.ex#L1)

The switches that tune the **one** metamutant compile for speed.

Mutare compiles the metamutant exactly once before any mutant runs. Inference
and verification feed diagnostics, so disabling them changes no runtime
behavior. The SSA alias optimization does affect generated code; measurements
found its compile cost bought no meaningful runtime benefit for the measured
AST-rewriting suite, but binary-building loops do benefit. The default keeps the
measured compilation saving; see NOTES "Rechecking SSA alias analysis" for the
workload-dependent tradeoff. Three switches, one home (this module), three delivery routes:

  * `compiler_env/0` — the `ERL_COMPILER_OPTIONS` entry `Mutare.Runner`
    applies to the single `mix compile` (the SSA alias pass off).
  * `compile_args/1` — extra `mix compile` CLI switches for the same
    invocation (`--no-verification`, Elixir ≥ 1.19: skips the
    `Module.ParallelChecker` verify pass — undefined-remote warnings and
    cross-module type checking).
  * `project_source/1` — wraps project modules declared in the sandbox's `mix.exs` so their
    effective `elixirc_options` always disable type-signature *inference*.
    This survives Mix's project cache and applies on every sandbox boot,
    including umbrella children and projects with a custom config path. It
    reports whether the wrapper was applied and, when it was not, why. Some source
    forms cannot be wrapped; those projects compile with inference on.

`seed_manifest/1` reconciles the inference option in a transplanted Elixir
compile manifest with that wrapper. Without it, Elixir 1.18/1.19 detects changed
`elixirc_options` and discards every seeded app beam on the first compile. It
must run only for an app `project_source/1` reported wrapping: applied to one
compiling with inference on, it manufactures the very mismatch it exists to
prevent, and the seed reports reuse for a build Mix discards.

The last two exist because Elixir's type checker (≥ 1.18/1.19) is
pathological on metamutant-shaped code: measured on phoenix_live_view
(~23k sites), inference + verification take a 14 s compile past 80 *minutes*;
with both off it is 14 s again. See NOTES "Type inference and verification
on the metamutant compile".

A per-mutant `mix test` never recompiles the lib (sources unchanged), so the
env and args carry nothing there. The project option remains the same on later
baseline/probe boots, preventing inference configuration changes from making
Mix recheck the metamutant.

# `compile_args`

```elixir
@spec compile_args(String.t()) :: [String.t()]
```

Extra `mix compile` CLI switches for the metamutant compile.

`--no-verification` (Elixir ≥ 1.19.0) skips the
`Module.ParallelChecker` verify pass — undefined-remote-function warnings and
cross-module type checking. Warnings on a build artifact are noise, and the
type checker is pathological on metamutant-shaped code (measured: ~12 minutes
on phoenix_live_view *with inference already off*). Poison detection is
unaffected: it reads hard compile **errors**, which are raised during
compilation, not verification.

`version` defaults to the running Elixir; it is a parameter so the gate is
unit-testable.

# `compiler_env`

```elixir
@spec compiler_env() :: [{String.t(), String.t()}]
```

Env entries that tune the **one** metamutant compile for speed: an
`ERL_COMPILER_OPTIONS` disabling the SSA alias-analysis pass
(`no_ssa_opt_alias`).

Read by `Mutare.Runner` for the single `mix compile`. Scoped there on purpose: a
per-mutant `mix test` never recompiles the lib (sources unchanged), so it carries
nothing. Merges with any `ERL_COMPILER_OPTIONS` already in the environment so a
user's own compiler options survive — ours is prepended (`erl_compiler_options/1`).

# `env_var`

```elixir
@spec env_var() :: String.t()
```

The name of the env var `compiler_env/0` sets (`ERL_COMPILER_OPTIONS`).
Listed in `Mutare.Sandbox.Command.Invocation.reserved_env_names/0`, since the
compile appends the partition entry after this one.

# `erl_compiler_options`

```elixir
@spec erl_compiler_options(String.t() | nil) :: String.t()
```

Build the `ERL_COMPILER_OPTIONS` value for the metamutant compile: prepend
`no_ssa_opt_alias` to any `inherited` value (an Erlang term-list string,
or `nil`/`""` for none), always returning a well-formed `[...]` list string. Pure,
so the merge is unit-testable.

# `project_source`

```elixir
@spec project_source(String.t()) ::
  {:hooked, String.t()} | {:declined, String.t(), String.t()}
```

Override inference in the sandbox copy of a Mix project's effective options.

Returns `{:hooked, source}` when a hook was attached to a module defined in this file,
and `{:declined, source, reason}` otherwise, `reason` briefly describing why no hook was attached.
The caller needs both. `Mutare.Sandbox.Seed` may only realign the compile manifest of an
app that really will compile with inference off, and the source alone does not establish that:
a file that defines no module of its own still comes back *changed* (the bootstrap is
prepended unconditionally), so `!=` is not a proxy for it. And a declined project compiles
with inference on, which on metamutant-shaped code can stretch the one compile from
seconds to hours, so the reason is included in sandbox diagnostics.

A `before_compile` hook wraps `project/0`, preserving its computed configuration
and every other compiler option. Only sandbox project files are rewritten;
the target's original options remain untouched. Unsupported Elixir versions
retain the original configuration. Quoted module definitions are left alone.

The function returns `{:declined, source, reason}` with the original source byte-for-byte when:

  * the source does not parse;
  * the rewritten file does not parse, or Elixir reads it back as anything other than
    the bootstrap followed by the original with each module body hooked.
    The two are compared as parsed programs, ignoring only spellings that evaluate
    alike (metadata, block nesting, an empty block as `nil`, a charlist as a `~c`
    sigil), so a render that still parses but moved an expression, altered a literal,
    or lost a hook is caught instead of becoming the sandbox's entry point;
  * the rewrite raises, throws, or exits.

Parsing and rendering are best-effort, so an unused umbrella child or a scaffolding
template cannot abort sandbox preparation. Keeping the original merely leaves inference
on, where a `mix.exs` rendered into a different program would break the sandbox or
change what it builds.

For a `mix.exs` defining no module of its own, the result is also `:declined`, but includes rewritten source (the
bootstrap is inert there): a project module built entirely in an externally required
file is outside this source rewrite, and its compiler options must currently disable
inference themselves.

`:hooked` reports the source rewrite, which is one step short of certainty: the hook
itself checks at compile time that the module is a Mix project defining `project/0`, so
a `mix.exs` holding only an unrelated helper module alongside a required-in project
reports `:hooked` and is nonetheless declined. Nothing observable before the compile can
close that gap, and the manifest is stamped before it.

Merely setting `Code.put_compiler_option(:infer_signatures, false)` before
compilation is insufficient: Elixir 1.20 Mix unconditionally derives inference
from `elixirc_options`, defaulting to true. Updating the current project stack
also fails when Mix pushes a cached umbrella project again. Wrapping the
project's return value covers both cases without interpreting its build code.

# `seed_manifest`

```elixir
@spec seed_manifest(term()) :: term()
```

Align a freshly seeded Elixir manifest with the sandbox inference override.

Only the inference entry of the compiler cache key changes. Existing beams
remain valid with inference disabled; all other options, source paths and
dependency/configuration records retain their ordinary invalidation behavior.
This must only run when transplanting the target's build, never on a retained
sandbox build or a dependency build.

Mix's manifest is private: recognize the 1.18/1.19 layouts (versions 26–29),
and leave unknown layouts alone, allowing Mix to cold-compile if necessary.
Elixir 1.20 removes inference from this key and needs no adjustment here.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
