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.
| Time to Start | Worker time | Duration | Time to finish | |
| Config | 5m51s | 2s | 2s | 5m54s |
| Eval | 6m20s | 56s | 56s | 7m16s |
| Build | 7m17s | 2m19s | 5m47s | 13m05s |
| Test | - | - | - | - |
| Deploy | - | - | - | - |
| Suite | 5m51s | 3m18s | 7m13s | 13m05s |