Mutare.Sandbox.CompilerOptions (mutare v0.3.1)

Copy Markdown View Source

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.

Summary

Functions

Extra mix compile CLI switches for the metamutant compile.

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

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.

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.

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

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

Functions

compile_args(version \\ System.version())

@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()

@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()

@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(inherited)

@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(source)

@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(manifest)

@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.