Alpha. Vary is under active development and not ready for production use. Syntax, APIs, performance, and behaviour may change between releases.
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)
| Original | Replacement |
|---|---|
+ | - |
- | + |
* | / |
/ | * |
% | *, / |
Relational (relational)
| Original | Replacements |
|---|---|
< | <=, >, == |
<= | <, >=, == |
> | >=, <, == |
>= | >, <=, == |
== | != |
!= | == |
Logical (logical)
| Original | Replacement |
|---|---|
and | or |
or | and |
not expr | expr (negation removed) |
Literal (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 removal (statement)
Replaces statements with pass. Excluded: pass, break, continue, function/class/interface definitions.
Boundary (boundary)
| Original | Replacement |
|---|---|
< | <= |
<= | < |
> | >= |
>= | > |
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:
| 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 |
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
| 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 |
Conditional mutations
| 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 value defaults
| 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 poison values
| 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 "" |