# `Mutare.Manifest`
[🔗](https://github.com/foxbenjaminfox/mutare/blob/v0.2.1/lib/mutare/manifest.ex#L1)

A map from mutant ids to their generated line ranges in a rendered metamutant.

`Mutare.Transform` writes the metamutant; this is what `Mutare.Poison` reads
back. It is built **lazily** (`from_source/2`), by `Mutare.Poison` on a failed
compile, only for the file(s) the error names — not eagerly during the scan,
where re-parsing every rendered metamutant was the scan's dominant cost yet is
read only when a compile actually fails (rare — built-in mutators are
compile-safe). `Poison` memoizes it within one recovery so a file faulting on
several lines is walked once.

**`Mutare.Poison`** uses each mutant's *generated ranges* — the metamutant line
spans of the code that exists only because of that mutant, so a compile error's
line maps back to the corresponding mutant id.

(Coverage is recorded separately: the metamutant self-records coverage at runtime
— see `Mutare.Coverage.Recorder` — keyed by mutant id directly, so there is no
metamutant `{module, line}` location to precompute.)

## Why ranges, not a single line

Poison recovery used to match a compile error's line against the *start line of
a selector clause body* only. That missed every poison whose bad code isn't on
that exact line:

  * **lifted mutations** — a guard/head-pattern mutant's code appears in a generated
    `defp <base>(mutare_active, …) when mutare_active === <id> …` clause, not in
    the dispatcher (which only forwards). The error points into that gated clause,
    away from the dispatcher;
  * **multiline bodies** — an in-place mutant whose body spans several lines can
    fault on any of them, not just the first;
  * **structural errors** — the compiler sometimes points at the surrounding
    `case` rather than a clause body.

So we record, per mutant, the *full* line ranges of its generated code:

  * each selector clause body (`<id> -> <mutated>`) — catches in-place mutants,
    including multiline bodies;
  * each lifted mutant clause (gated `when mutare_active === <id>`) — catches a
    guard/head-pattern poison, whose code is outside the dispatcher;
  * the whole selector `case`, attributed to *all* the mutant ids it hosts — the
    coarse fallback for a structural error that points at the `case` itself.

`ids_at_line/2` resolves an error line by **narrowest containing range**: a line
inside a specific clause/def range maps to that one mutant; only when nothing
narrower contains it does the whole-`case` fallback fire (dropping every mutant
the `case` hosts — a bounded over-drop that still recovers the build, never the
old "couldn't map it → abort").

Ranges are in **metamutant line space**, which only exists after rendering, so
the manifest is built by re-parsing the rendered metamutant and ranging its
generated nodes with `Sourceror.get_range/1`, recognising selectors via
`Mutare.Metamutant.subject?/2`. A selector inside a function body reads the
file's **dispatch variable** rather than `:persistent_term` directly, and a
lifted or tupled mutant clause is gated on it (`<var> === <id>`) — so that name
must be supplied to the reader. It is per file (`:mutare_active`, or a salted variant
when the source already uses that identifier), chosen by the transform and
handed out with the metamutant (`Mutare.Transform.Result.dispatch_var`,
`Mutare.Schema`'s `:dispatch_vars`); `from_source/2` takes it alongside the source.

The re-parse uses Elixir's own `Code.string_to_quoted!` (with `:token_metadata`
+ `:columns`), **not** `Sourceror.parse_string!`. `get_range/1` only needs that
token metadata (`:line`/`:column`/`:closing`/`:end`/`:end_of_expression`), which
the stdlib parser already produces; what `Sourceror.parse_string!` *adds* is a
comment-merging pass that is quadratic on a large metamutant (a lifted 1.8 MB
file re-parses in ~0.6 s here, versus minutes for Sourceror) and that the
manifest never reads. A `:literal_encoder` reproduces Sourceror's
`{:__block__, meta, [literal]}` wrapping so the recognisers — already tolerant of
both shapes — accept the resulting nodes; the two parses yield identical
ranges. (Sourceror is still the *renderer*; only this readback parse changed.)

# `region`

```elixir
@type region() :: %{ids: [pos_integer()], lo: pos_integer(), hi: pos_integer()}
```

A generated line range and the mutant ids whose code occupies it.

# `t`

```elixir
@type t() :: %Mutare.Manifest{ast: Macro.t() | nil, regions: [region()]}
```

One file's manifest:

  * `:regions` — generated line ranges, for mapping a compile error back to a mutant.
  * `:ast` — the parsed metamutant the regions were ranged over, retained so the
    macro-expansion fallback (`ids_in_named_calls/2`) can range a blamed macro's calls in
    the same tree rather than parse the file a second time. `nil` on a hand-built manifest.

# `from_source`

```elixir
@spec from_source(String.t(), atom()) :: t()
```

Build a manifest from one file's rendered metamutant source and its dispatch variable.

Re-parses (for `Sourceror.get_range/1`) and walks the tree once, attributing
every selector clause, lifted private definition, and selector `case` to the
mutant id(s) it belongs to. `dispatch_var` is the name the transform chose for this
file (`Mutare.Transform.Result.dispatch_var`): the hoisted selectors read it as their
subject and the gated mutant clauses compare it, so it is what makes them recognisable.
The parse is kept on the manifest (`:ast`): one failed compile can need both
attributions of the same file, and `Mutare.Poison` holds one manifest per file for the
round.

## Examples

    iex> source = "defmodule Demo do\n  def add(a, b), do: a + b\nend\n"
    iex> result = Mutare.transform_string(source, mutators: [:arithmetic])
    iex> %Mutare.Manifest{regions: regions} =
    ...>   Mutare.Manifest.from_source(result.metamutant, result.dispatch_var)
    iex> regions == []
    false

# `ids_at_line`

```elixir
@spec ids_at_line(t(), pos_integer()) :: [pos_integer()]
```

Mutant ids whose generated code occupies `line`.

Resolved by **narrowest containing range**: a specific clause/def range beats the
coarse whole-`case` fallback, so a precise error drops exactly the offending
mutant; a structural error that only the `case` range contains drops every mutant
it hosts. Returns `[]` when no generated code spans `line`.

## Examples

    iex> manifest = %Mutare.Manifest{
    ...>   regions: [
    ...>     %{ids: [1, 2], lo: 3, hi: 8},
    ...>     %{ids: [1], lo: 5, hi: 5}
    ...>   ]
    ...> }
    iex> Mutare.Manifest.ids_at_line(manifest, 5)
    [1]
    iex> Mutare.Manifest.ids_at_line(manifest, 4)
    [1, 2]
    iex> Mutare.Manifest.ids_at_line(manifest, 99)
    []

# `ids_in_named_calls`

```elixir
@spec ids_in_named_calls(t(), MapSet.t(atom())) :: %{optional(atom()) =&gt; MapSet.t()}
```

Mutant ids inside a call to one of `names`, grouped by that call's function name.

The **macro-expansion fallback**'s attribution (`Mutare.Poison.macro_poison/4`): when a
mutation splices a selector `case` into an argument a macro rewrites at compile time, the
macro raises during expansion and the compiler reports the macro *call* line — which no
region covers — so `ids_at_line/2` finds nothing. Given the macro name from the compiler's
`expanding macro:` frame, this instead finds every call of that name in the rendered
metamutant, takes its **full** line range (`Sourceror.get_range/1`, to the closing
delimiter — so a literal argument on its own line is still spanned), and collects the ids of
every region contained in it.

Works in **metamutant space**: the rendered source is what the compiler read, so the ids
found inside a blamed macro's span are exactly the ones that could have poisoned it — no
mapping back to original-source coordinates is needed, or possible. Reads the manifest's
retained `:ast` (a `from_source/2` manifest, whose regions were ranged with the file's
dispatch variable), so the file is parsed once for both attributions. Returns
`%{fun_atom => MapSet.t()}`, empty when nothing matched.

---

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