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.
--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.
--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.
--level both runs AST mutations first, then bytecode mutations, and deduplicates the combined results.
Seventeen classic operators, selected with --operators (comma-separated). Use --operators CLASSIC to select only these:
arithmetic)| Original | Replacement |
|---|---|
+ | - |
- | + |
* | / |
/ | * |
% | *, / |
relational)| Original | Replacements |
|---|---|
< | <=, >, == |
<= | <, >=, == |
> | >=, <, == |
>= | >, <=, == |
== | != |
!= | == |
logical)| Original | Replacement |
|---|---|
and | or |
or | and |
not expr | expr (negation removed) |
literal)| Original | Replacements |
|---|---|
0 (Int) | 1, -1 |
n (Int, n != 0) | 0, n+1, n-1, -n (if n > 0) |
true | false |
false | true |
"" (empty string) | "mutant" |
"text" (non-empty) | "" |
statement)Replaces statements with pass. Excluded: pass, break, continue, function/class/interface definitions.
boundary)| Original | Replacement |
|---|---|
< | <= |
<= | < |
> | >= |
>= | > |
ret_default)Replaces return expr with return 0, return "", return False, or return None. Each return statement produces up to 4 mutants.
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)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)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_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)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)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)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)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)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)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)
}
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)Compound mutations that affect multiple related expressions at once.
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)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)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)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)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)Uses one field twice instead of two distinct fields in an expression.
let area = width * height --> let area = width * width
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)Mutations that target type-level assumptions: null handling, collection checks, and numeric edge cases.
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)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)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)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
Use these aliases with --operators to select families of operators:
| Alias | Operators |
|---|---|
CLASSIC | All 6 core operators: arithmetic, relational, logical, literal, statement, boundary |
SEMANTIC | All 5 semantic operators: ret_default, skip_effect, skip_block, drop_element, swap_args |
CONTRACT | All 3 contract operators: contract_precondition, contract_postcondition, contract_remove |
MATCH | Both match operators: match_swap, match_pattern |
HIGHER_ORDER | boundary_shift, guard_mismatch |
FIELD | field_swap, omitted_read, duplicate_field, misbound_constructor |
TYPE_SEMANTIC | null_weaken, null_strengthen, collection_simplify, numeric_boundary |
* | All operators |
Six operators, selected with --bc-operators (comma-separated):
arith)Replaces JVM arithmetic opcodes across all numeric types (int, long, float, double). IADD becomes ISUB, IMUL becomes IDIV, and so on.
cond)Replaces conditional jump instructions: integer comparisons (IF_ICMPLT to IF_ICMPLE), null checks (IFNULL to IFNONNULL), and reference equality.
ret)Replaces return values with type-appropriate defaults: 0 for int, 0L for long, 0.0 for float/double, null for objects.
neg)Removes negation instructions (INEG, LNEG, FNEG, DNEG) by replacing them with NOP.
callskip)Removes method calls entirely. Pops all arguments and the receiver from the stack, pushes a default return value. Never skips constructors.
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.
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.
| Original | Mutated to |
|---|---|
IADD | ISUB |
ISUB | IADD |
IMUL | IDIV |
IDIV | IMUL |
IREM | IMUL, IDIV |
LADD | LSUB |
LSUB | LADD |
LMUL | LDIV |
LDIV | LMUL |
LREM | LMUL, LDIV |
FADD | FSUB |
FSUB | FADD |
FMUL | FDIV |
FDIV | FMUL |
FREM | FMUL, FDIV |
DADD | DSUB |
DSUB | DADD |
DMUL | DDIV |
DDIV | DMUL |
DREM | DMUL, DDIV |
| Original | Mutated to |
|---|---|
IFEQ (== 0) | IFNE |
IFNE (!= 0) | IFEQ |
IFLT (< 0) | IFLE, IFGE |
IFLE (<= 0) | IFLT, IFGT |
IFGT (> 0) | IFGE, IFLE |
IFGE (>= 0) | IFGT, IFLT |
IFNULL | IFNONNULL |
IFNONNULL | IFNULL |
IF_ICMPEQ | IF_ICMPNE |
IF_ICMPNE | IF_ICMPEQ |
IF_ACMPEQ | IF_ACMPNE |
IF_ACMPNE | IF_ACMPEQ |
| Return instruction | Pop | Default pushed |
|---|---|---|
IRETURN | POP | ICONST_0 (0) |
LRETURN | POP2 | LCONST_0 (0L) |
FRETURN | POP | FCONST_0 (0.0f) |
DRETURN | POP2 | DCONST_0 (0.0) |
ARETURN | POP | ACONST_NULL (null) |
| Return instruction | Pop | Poison pushed |
|---|---|---|
IRETURN | POP | ICONST_M1 (-1) |
LRETURN | POP2 | LDC -1L |
FRETURN | POP | LDC Float.MAX_VALUE |
DRETURN | POP2 | LDC Double.MAX_VALUE |
ARETURN | POP | LDC "" |