VAST

Metamorphic testing

Some changes to a program should not change its result. Adding zero, multiplying by one, or negating a boolean twice should all give you back the original. If the compiler gets a different answer after one of these no-op changes, it has a bug.

The idea

Some program transformations should not change the result. Adding zero to a number, multiplying by one, or negating a boolean twice should all produce the original value. If the compiler produces a different result after one of these transforms, something is wrong.

Metamorphic testing applies these semantics-preserving transforms to generated programs and checks that all execution paths still agree on the same result.

How it works

The process has four steps:

1. Generate program P
2. Execute P on all paths --> result R
3. Apply a semantics-preserving transform --> P'
4. Execute P' on all paths --> result R'
5. Verify: R == R'

If the original program P already has a mismatch (the paths disagree), VAST saves it as a regular differential testing failure and skips the metamorphic check. Metamorphic testing only applies to programs where all paths agree first.

If P passes but P' produces a different result, that is an equivalence failure: the transform should have been invisible to the compiler, but it was not.

Transforms

VAST implements seven semantics-preserving transforms:

TransformWhat it doesInvariant
Add zeroRewrites x to x + 0 or 0 + xAdding zero does not change the value
Multiply oneRewrites x to x * 1 or 1 * xMultiplying by one does not change the value
Double negationRewrites b to not(not(b))Negating a boolean twice returns the original
Introduce tempExtracts an expression into a local variableNaming a value does not change it
Concat emptyRewrites s to s + ""Concatenating an empty string does not change the string
Empty list lengthRewrites [].length to 0An empty list always has length zero
Singleton list lengthRewrites [x].length to 1A single-element list always has length one

Each transform targets specific expression types. Add-zero and multiply-one apply to integer expressions. Double-negation applies to boolean expressions. Concat-empty applies to string expressions. The list transforms apply to list literals.

Why this catches real bugs

Metamorphic testing catches a different class of bugs than plain differential testing. Differential testing finds cases where the compiler produces wrong output for a program. Metamorphic testing finds cases where the compiler treats equivalent programs differently.

Consider an optimizer that folds x + 0 into x. If the folding is correct, the result should not change. But if the optimizer incorrectly handles 0 + x (commutative case), the add-zero transform will expose it: the original program returns one value, and the transformed version returns another.

ScenarioWhat happensBug type
Optimizer folds x + 0 correctly but mishandles 0 + xTransform changes resultOptimizer commutativity bug
Constant folder evaluates not(not(b)) differentlyTransform changes resultOptimizer double-negation bug
Introducing a temp variable changes scopingTransform changes resultCodegen variable scoping bug
String concatenation with empty string produces different resultTransform changes resultString handling edge case

Example

A generated program returns 42:

def __vast_compute() -> Int {
    let x = 40
    let y = 2
    return x + y
}

The add-zero transform rewrites x + y to (x + y) + 0:

def __vast_compute() -> Int {
    let x = 40
    let y = 2
    return (x + y) + 0
}

Both programs should return 42. If the transformed version returns something else, the compiler mishandled the addition of zero.

Running metamorphic tests

Metamorphic testing is available via the --metamorphic flag:

vary vast --metamorphic --count 100 --seed 42

VAST generates programs, runs the standard multi-path comparison, then applies each applicable transform and verifies the result stays the same.

In CI deep mode, metamorphic testing runs automatically as part of the nightly verification.

Relationship to other techniques

Metamorphic testing complements differential testing and mutation testing:

TechniqueWhat it checks
Differential testingAll paths agree on one program
Metamorphic testingAll paths agree on equivalent programs
Mutation testingAll paths disagree on non-equivalent programs
← Differential testing
Mutation testing →