Mutare.Metamutant (mutare v0.4.1)

Copy Markdown View Source

The shape of a selector case subject in the metamutant.

This is the one piece of generated structure that Mutare.Transform writes and that Mutare.Manifest recognises for Mutare.Poison readback. Owning it in a single module keeps the producer from hand-building the same AST literal twice and keeps the consumer from re-deriving how to spot a selector.

A selector case looks like:

case :persistent_term.get(:mutare_active, 0) do
  17 -> <mutated>     # one clause per mutant id hosted here
  18 -> <mutated>
  _  -> <original>    # the catch-all: baseline + every inactive mutant
end

The subject is one of two shapes. A module-level / :scaffold / head-default selector reads :persistent_term inline (the form above). A selector inside a function body instead reads the hoisted active id — a bare reference to the dispatch variable mutare_active, bound once per function activation (the lifted dispatcher threads it as the base clause's first parameter; a non-lifted function binds it in a :do-block prologue), so the per-site :persistent_term.get is gone:

case mutare_active do          # the hoisted read — same shape, cheaper subject
  17 -> <mutated>
  mutare_active -> <original>
end

Recognising the subject is enough to find every selector:

  • subject_ast/0 builds the inline subject Transform splices in,
  • subject?/2 recognises either shape — the inline read unconditionally, the hoisted bare variable only when the active-id variable name is supplied (the predicate Mutare.Manifest uses to walk a rendered metamutant, under the name the transform recorded for the file — Mutare.Transform.Result.dispatch_var).

subject?/2 is tolerant of how the subject is parsed back: bare ASTs may keep :persistent_term/:mutare_active as atoms, while Mutare.Manifest re-parses with Code.string_to_quoted! plus a literal encoder that wraps literals as {:__block__, _, [literal]} for Sourceror.get_range/1 compatibility. The predicate must match both shapes.

Mutare.Selector defines the runtime constants (the :persistent_term key and the baseline id); this module builds their AST.

Schema builds pass a file namespace to subject_ast/1. Its read projects the single global selection before it is used by any local selector or lifted guard:

case :persistent_term.get(:mutare_active, 0) do
  {:"lib/example.ex", mutare_local_id} when :erlang.is_integer(mutare_local_id) ->
    mutare_local_id

  0 ->
    0

  _ ->
    :inactive
end

This projection is hoisted with the read in function bodies; default arguments and other inline positions keep it self-contained. Local gates and exclusions remain integer-based. :inactive differs from baseline so other files' coverage gates short-circuit without consulting the tracking flag. subject?/2 recognises the complete projection, including after literal-encoded reparse. Its inner case has no positive integer branches and therefore contributes no mutant to Manifest.

Summary

Functions

Returns whether node is a tupled selector subject.

Build the tupled selector subject with one recording point after both inputs succeed. The clause-local temporaries never enter source clause scopes, and the original scrutinee's bindings escape unchanged. pattern_subject?/2 reads this same structure after rendering; coverage gate construction belongs to Recorder.

Returns whether node is a selector subject.

The selector subject Transform splices into every selector/dispatcher case: :persistent_term.get(<key>, <baseline>), optionally projected into a file namespace.

Functions

pattern_subject?(node, var \\ nil)

@spec pattern_subject?(Macro.t(), atom() | nil) :: boolean()

Returns whether node is a tupled selector subject.

The tuple-the-scrutinee path starts with {<selector_subject>, <scrutinee>}. A generated single-clause case records coverage after evaluating this tuple and returns the tuple unchanged; its temporary bindings stay outside source clause scopes. Recognise that wrapper only through its coverage record and matching input/output variables. The mutant clauses gate on the active id in guards.

var is compared against the variable the pattern binds, not against anything inside the record: Mutare.Coverage.Recorder.record?/1 deliberately ignores the gate, so the gate may later become a hoisted boolean without breaking attribution. That the gate and the pattern name the same variable is therefore an emitter invariant now, held by pattern_subject_ast/5 and its caller building both from one Config.active_var, and not something this reader can re-check.

Both the bare two-tuple and the {:__block__, _, [{first, scrutinee}]} wrapper produced by literal-encoded reparse are accepted. var is passed through so the hoisted selector-variable form is recognised too.

Examples

iex> subject = {Mutare.Metamutant.subject_ast(), {:value, [], nil}}
iex> Mutare.Metamutant.pattern_subject?(subject)
true

iex> subject = {{:mutare_active, [], nil}, {:value, [], nil}}
iex> Mutare.Metamutant.pattern_subject?(subject)
false
iex> Mutare.Metamutant.pattern_subject?(subject, :mutare_active)
true

pattern_subject_ast(read, scrutinee, active_var, subject_var, record)

@spec pattern_subject_ast(Macro.t(), Macro.t(), atom(), atom(), Macro.t()) ::
  Macro.t()

Build the tupled selector subject with one recording point after both inputs succeed. The clause-local temporaries never enter source clause scopes, and the original scrutinee's bindings escape unchanged. pattern_subject?/2 reads this same structure after rendering; coverage gate construction belongs to Recorder.

active_var must be the same variable the record was gated on — this function is the one place that pairs them, because the reader no longer can (see pattern_subject?/2).

subject?(node, var \\ nil)

@spec subject?(Macro.t(), atom() | nil) :: boolean()

Returns whether node is a selector subject.

Mutare.Manifest uses this while walking rendered metamutant source. Three shapes match:

  • the inline read built by subject_ast/0: :persistent_term.get(<key>, <baseline>)
  • the namespace projection built by subject_ast/1
  • the hoisted read used inside function bodies: a bare reference to the active mutant variable var

The hoisted form is recognised only when var is supplied. That prevents an ordinary source-level case some_var do ... from being treated as a selector.

Literal wrappers added by Mutare.Manifest's readback parse are accepted, as are bare atoms from ordinary quoted ASTs.

Examples

iex> Mutare.Metamutant.subject?(Mutare.Metamutant.subject_ast())
true
iex> Mutare.Metamutant.subject?({:mutare_active, [], nil})
false
iex> Mutare.Metamutant.subject?({:mutare_active, [], nil}, :mutare_active)
true

subject_ast(namespace \\ nil)

@spec subject_ast(String.t() | nil) :: Macro.t()

The selector subject Transform splices into every selector/dispatcher case: :persistent_term.get(<key>, <baseline>), optionally projected into a file namespace.

Examples

iex> Mutare.Metamutant.subject_ast() |> Mutare.Metamutant.subject?()
true