ee2a4e94

By: Tom Sydney Kerckhove <syd@cs-syd.eu>

Const-function mutation operators with diffable manifests

Add three mutation operators that replace expressions whose type is
arg1 -> ... -> argN -> T (with N ≥ 0) by a constant function returning
T's distinguished trivial value: ConstNothing for Maybe a, ConstEmptyList
for [a], and ConstBool for Bool.  At arity 0 the mutant is the bare
trivial value; at arity ≥ 1 it is a typed (\_ ... _ -> v) lambda
synthesized in the GhcTc AST so the desugarer accepts it without going
through Prelude.const.  ConstBool subsumes the old ConstBool plus a
hypothetical ConstBoolFn into one operator; the existing guard-only and
True/False/otherwise carveouts are preserved at arity 0.  Maybe and List
gain coverage that MaybeOp and ListLit (syntactic-only) miss: calls like
lookup k m and concat xs are now mutated to Nothing / [] respectively.

The operators are governed by two structural rules baked into the
walker.  First, when the matched expression sits at the operator-token
position of an infix application, the manifest preview rewrites the
whole OpApp source span in prefix form (e.g. n < 0 becomes
(\_ _ -> True) (n) (0)) instead of text-splicing a lambda into the
operator slot and producing nonsense source.  An OpAppCtx record on
InstrumentEnv carries the original LHS/RHS/operator-token spans and
operand source text, populated when the walker enters an
ExpandedThingTc (OrigExpr (OpApp ...)) and cleared on exit; a new
ReplaceOuterSpan SrcSpanDelta uses it.  Second, the walker tracks
HsApp function-position depth: it increments when descending into the
function side of an HsApp and resets to 0 on the argument side, so
const-family operators can skip arity-N firings (N ≥ 1) at sites where
the matched expression is already saturated by N enclosing
applications.  The dominance argument is that an arity-N
(\_ ... _ -> v) mutation inserted under N levels of HsApp evaluates
to v, which the arity-0 mutation on the outermost saturated expression
already produces; emitting both adds nothing.

Each module's manifest is now also written as a coloured human-readable
<Module>.txt next to the canonical <Module>.json, sharing the same
header + unified-diff layout as the existing surviving-mutation report.
The shared renderer (renderMutationRecord, renderManifest,
writeManifestTxtFile, plus renderUnifiedDiff and the colour helpers)
lives in sydtest-mutation-runtime so the plugin's manifest writer and
sydtest's report use one implementation; sydtest's Output.Common
re-exports the colour helpers for backward compatibility.  Each .txt
opens with a "N mutations in M groups" count line so empty manifests
are not mistakeable for rendering accidents.  The nix manifest check
diffs both .json and .txt against the committed tree.

Example library Example.ConstFnLib exercises arity 0, 1, 2, and 5 cases
across Maybe, list, and Bool; IgnoreLib's mark / markAction carry
DisableMutation: ConstEmptyList annotations because they exist to test
the ignore filter rather than be exhaustively covered.  applyTokenReplace
in the plugin now collapses multi-line spans correctly (a latent bug
that surfaced once arity-≥1 operators started emitting multi-line
replacements).  All 211 mutations in the example manifest are killed by
the existing and new tests; nix flake check passes.