Mutation

Operators

Vary's mutation engine includes 33 operators across two levels and three families (classic, semantic, bytecode). For how these operators fit into contracts, observability, and scoring, see Advanced overview. For caching, policy gates, and CLI flags, see Infrastructure.

Mutation levels

Bytecode mutation (default)

--level bytecode is the primary mutation mode. It compiles your code once, then patches individual JVM bytecode instructions per mutant. Each mutant loads in an isolated classloader and runs in parallel, so runs finish fast enough for regular development use.

The bytecode runner also supports changed-only mode: it hashes each method and only re-tests methods whose hash changed since the last run. Use --all to override this and test everything.

AST mutation

--level ast is a secondary mode for deeper analysis. It mutates the parsed syntax tree before compilation. Each mutant gets its own compile pass (constant folding, type checking, bytecode generation). This is slower but gives access to structural mutations that bytecode cannot express, like removing statements, dropping list elements, or swapping function arguments. Use it when investigating specific survivor patterns or when the bytecode operators are not enough.

The AST runner includes a canary check: it picks the first relational or logical mutant and verifies it gets killed. If the canary survives, the test harness is broken and the run aborts.

Both

--level both runs AST mutations first, then bytecode mutations, and deduplicates the combined results.

AST operators

Seventeen classic operators, selected with --operators (comma-separated). Use --operators CLASSIC to select only these:

Arithmetic (arithmetic)

OriginalReplacement
+-
-+
*/
/*
%*, /

Relational (relational)

OriginalReplacements
<<=, >, ==
<=<, >=, ==
>>=, <, ==
>=>, <=, ==
==!=
!===

Logical (logical)

OriginalReplacement
andor
orand
not exprexpr (negation removed)

Literal (literal)

OriginalReplacements
0 (Int)1, -1
n (Int, n != 0)0, n+1, n-1, -n (if n > 0)
truefalse
falsetrue
"" (empty string)"mutant"
"text" (non-empty)""

Statement removal (statement)

Replaces statements with pass. Excluded: pass, break, continue, function/class/interface definitions.

Boundary (boundary)

OriginalReplacement
<<=
<=<
>>=
>=>

Return default (ret_default)

Replaces return expr with return 0, return "", return False, or return None. Each return statement produces up to 4 mutants.

Skip effect (skip_effect)

Removes side-effecting function calls (calls used as statements, not as values) by replacing them with pass.

validate(data)  -->  pass
cache.update(key)  -->  pass

Skip block (skip_block)

Replaces control flow block bodies with pass:

if cond { body }     -->  if cond { pass }
for x in items { body }  -->  for x in items { pass }
while cond { body }  -->  while cond { pass }

Drop element (drop_element)

Removes one element from a list or dict literal. Only targets literals with 2+ elements.

[a, b, c]  -->  [b, c], [a, c], [a, b]

Swap arguments (swap_args)

Swaps adjacent arguments with matching types in function calls.

f(a, b, c)  -->  f(b, a, c), f(a, c, b)

Only swaps pairs where both arguments are the same kind of expression.

Contract precondition (contract_precondition)

Mutates expressions inside in {} precondition blocks using the same transforms as arithmetic, relational, and literal operators. A surviving precondition mutant means no test exercises the boundary the precondition defines.

def withdraw(amount: Int) -> Int {
    in { amount > 0 }       # mutated to: amount >= 0, amount < 0, etc.
    ...
}

Contract postcondition (contract_postcondition)

Mutates expressions inside out(r) {} and post {} postcondition blocks. A surviving postcondition mutant means the guarantee can be weakened without any test noticing.

def abs_val(x: Int) -> Int {
    out (r) { r >= 0 }      # mutated to: r > 0, r <= 0, etc.
    ...
}

Contract mutation types are counted separately in the contract adequacy score.

Enum replace (enum_replace)

Replaces a nullary enum variant reference with another variant from the same enum. Targets EnumName.Variant expressions in value position (comparisons, returns, arguments, assignments). Payload variants are excluded. Only simple variants without parameters are swapped.

enum Color { Red, Green, Blue }

def theme() -> Color {
    return Color.Red        # mutated to: Color.Green, Color.Blue
}

For a 3-variant enum, each reference generates 2 mutants (one per alternative). A surviving enum mutant means tests do not distinguish between variants.

Contract remove (contract_remove)

Removes an entire contract block (in {}, out(r) {}, or post {}), making the function accept any input or guarantee nothing about its output. A surviving contract-remove mutant means no test exercises the boundary the contract defines.

def withdraw(amount: Int) -> Int {
    in { amount > 0 }       # mutated to: (contract removed entirely)
    ...
}

Match swap (match_swap)

Swaps the bodies of two match cases. A surviving match-swap mutant means tests do not distinguish between the outcomes of different cases.

match status {
    case Status.Active { return 1 }    # body swapped with next case
    case Status.Inactive { return 0 }
}

Match pattern (match_pattern)

Mutates match patterns: removes guards, replaces specific patterns with wildcards, or substitutes literal values. A surviving match-pattern mutant means the pattern is not tested precisely enough.

match x {
    case n if n > 10 { ... }   # mutated to: case n { ... } (guard removed)
}

Semantic operators

Ten operators that mutate program meaning rather than syntax. Classic mutation catches arithmetic slips and flipped conditions; semantic operators catch the subtler bugs where a developer reads the wrong field, passes arguments in the wrong order, or weakens a null check. Select with --operators SEMANTIC or by individual group: HIGHER_ORDER, FIELD, TYPE_SEMANTIC.

Higher-order operators (HIGHER_ORDER)

Compound mutations that affect multiple related expressions at once.

Boundary shift (boundary_shift)

Shifts a loop bound and its comparison operator together. The classic boundary operator changes < to <= in isolation. Boundary shift adjusts both the comparison and the limit value, which is closer to how real off-by-one bugs happen.

for i in range(n) { ... }  -->  for i in range(n - 1) { ... }
while i < len(items) { ... }  -->  while i <= len(items) { ... }

Guard mismatch (guard_mismatch)

Replaces the field checked in a guard condition with a sibling field of the same type. If order has both price: Int and qty: Int, a guard on order.price gets swapped to order.qty.

if order.price > 0 { ... }  -->  if order.qty > 0 { ... }

Field operators (FIELD)

Mutations on field access, construction, and data use. These catch the class of bug where you type self.x when you meant self.y.

Field swap (field_swap)

Reads a sibling field instead of the intended one. Targets field accesses on data types and classes where another field of the same type exists.

let total = item.price * item.qty  -->  let total = item.qty * item.qty

Omitted read (omitted_read)

Removes a field access from a calculation by replacing it with a default value for its type.

let margin = revenue - cost  -->  let margin = revenue - 0

Duplicate field (duplicate_field)

Uses one field twice instead of two distinct fields in an expression.

let area = width * height  -->  let area = width * width

Misbound constructor (misbound_constructor)

Swaps constructor arguments that have compatible types. If both parameters are Int, the values get flipped.

Point(x, y)  -->  Point(y, x)

Type-semantic operators (TYPE_SEMANTIC)

Mutations that target type-level assumptions: null handling, collection checks, and numeric edge cases.

Null weaken (null_weaken)

Removes or weakens a null check, allowing None to flow where it should not.

if x is not None { use(x) }  -->  use(x)

Null strengthen (null_strengthen)

Removes a null-safe fallback, forcing a path that assumes the value is always present.

let v = x ?: default_val  -->  let v = x!!

Collection simplify (collection_simplify)

Weakens a collection emptiness or membership check. A len(items) > 0 guard becomes True, letting the empty-list path through.

if len(items) > 0 { ... }  -->  if True { ... }

Numeric boundary (numeric_boundary)

Shifts a numeric boundary value or changes division type. Catches off-by-one thresholds and integer-vs-float division bugs.

if amount >= 100 { ... }  -->  if amount >= 99 { ... }
x / 3  -->  x // 3

Operator group aliases

Use these aliases with --operators to select families of operators:

AliasOperators
CLASSICAll 6 core operators: arithmetic, relational, logical, literal, statement, boundary
SEMANTICAll 5 semantic operators: ret_default, skip_effect, skip_block, drop_element, swap_args
CONTRACTAll 3 contract operators: contract_precondition, contract_postcondition, contract_remove
MATCHBoth match operators: match_swap, match_pattern
HIGHER_ORDERboundary_shift, guard_mismatch
FIELDfield_swap, omitted_read, duplicate_field, misbound_constructor
TYPE_SEMANTICnull_weaken, null_strengthen, collection_simplify, numeric_boundary
*All operators

Bytecode operators

Six operators, selected with --bc-operators (comma-separated):

Arithmetic (arith)

Replaces JVM arithmetic opcodes across all numeric types (int, long, float, double). IADD becomes ISUB, IMUL becomes IDIV, and so on.

Conditional (cond)

Replaces conditional jump instructions: integer comparisons (IF_ICMPLT to IF_ICMPLE), null checks (IFNULL to IFNONNULL), and reference equality.

Return value (ret)

Replaces return values with type-appropriate defaults: 0 for int, 0L for long, 0.0 for float/double, null for objects.

Negation (neg)

Removes negation instructions (INEG, LNEG, FNEG, DNEG) by replacing them with NOP.

Call skip (callskip)

Removes method calls entirely. Pops all arguments and the receiver from the stack, pushes a default return value. Never skips constructors.

Return poison (poison)

Replaces return values with adversarial (non-default) values: -1 for int, -1L for long, Float.MAX_VALUE, Double.MAX_VALUE, "" for objects. These values are chosen to trigger failures in callers that assume specific value ranges.

Bytecode opcode reference

The full opcode-by-opcode mapping for each bytecode operator. The I* (32-bit int) and F* (32-bit float) variants are rare in compiled Vary code because Int compiles to 64-bit long and Float to 64-bit double, but the operators handle them for completeness.

Arithmetic mutations

OriginalMutated to
IADDISUB
ISUBIADD
IMULIDIV
IDIVIMUL
IREMIMUL, IDIV
LADDLSUB
LSUBLADD
LMULLDIV
LDIVLMUL
LREMLMUL, LDIV
FADDFSUB
FSUBFADD
FMULFDIV
FDIVFMUL
FREMFMUL, FDIV
DADDDSUB
DSUBDADD
DMULDDIV
DDIVDMUL
DREMDMUL, DDIV

Conditional mutations

OriginalMutated to
IFEQ (== 0)IFNE
IFNE (!= 0)IFEQ
IFLT (< 0)IFLE, IFGE
IFLE (<= 0)IFLT, IFGT
IFGT (> 0)IFGE, IFLE
IFGE (>= 0)IFGT, IFLT
IFNULLIFNONNULL
IFNONNULLIFNULL
IF_ICMPEQIF_ICMPNE
IF_ICMPNEIF_ICMPEQ
IF_ACMPEQIF_ACMPNE
IF_ACMPNEIF_ACMPEQ

Return value defaults

Return instructionPopDefault pushed
IRETURNPOPICONST_0 (0)
LRETURNPOP2LCONST_0 (0L)
FRETURNPOPFCONST_0 (0.0f)
DRETURNPOP2DCONST_0 (0.0)
ARETURNPOPACONST_NULL (null)

Return poison values

Return instructionPopPoison pushed
IRETURNPOPICONST_M1 (-1)
LRETURNPOP2LDC -1L
FRETURNPOPLDC Float.MAX_VALUE
DRETURNPOP2LDC Double.MAX_VALUE
ARETURNPOPLDC ""
← Contracts
Lie detection: deep dive →