Mutation Coverage with MCY

Ask whether a property set is good enough by changing the design on purpose, and read the result as a statement about the properties.

Lesson
F6.5
Time
20 min
Runs on
SymbiYosys
Check
4 questions

A FIFO's formal bench had six properties, all proven, and the sign-off review asked how anyone knew six was enough. Someone corrupted one bit of the write data by hand and all six still passed: they checked the count and the flags, never the data. Mutation coverage asks that question forty times, with a script.

Property setKilledHolesEquivalent
Bookkeeping: count, flags, no push when full, no pop when empty24141
The same plus end-to-end order3801

39 mutations of fifo16 and one unmutated control, measured with MCY from the pinned suite.

Change the Design, Watch the Properties

#LineMutationBookkeeping setPlus order property
1-no change (control)equivalentequivalent
27logic_and A[0] (rd[0]) forced to 0survivedkilled by a_order
312add Y[2] inverted when bit 3 is 0killed by a_levelkilled by a_level
46eq B[2] forced to 1killed by a_fullkilled by a_full
512mux B[0] (wdata[0]) inverted when bit 5 is 0survivedkilled by a_order
610mux Y[0] forced to 1survivedkilled by a_order
712mux B[0] (wdata[0]) inverted when bit 7 is 1survivedkilled by a_order
85eq Y[0] (empty[0]) forced to 1killed by a_emptykilled by a_empty
913mux Y[1] forced to 0killed by a_levelkilled by a_level
1014add Y[0] forced to 1killed by a_levelkilled by a_level
1112mux B[2] (wdata[2]) forced to 1survivedkilled by a_order
1214sub Y[1] invertedkilled by a_levelkilled by a_level
137logic_not Y[0] forced to 1killed by a_boundkilled by a_bound
1413add Y[1] invertedkilled by a_levelkilled by a_level
157logic_not A[0] (full[0]) forced to 1survivedkilled by a_ahead
1610mux Y[0] inverted when bit 3 is 1survivedkilled by a_order
177logic_and A[0] (wr[0]) forced to 0survivedkilled by a_ahead
187logic_and Y[0] (do_wr[0]) forced to 1killed by a_nopopkilled by a_nopop
197logic_and Y[0] (do_rd[0]) forced to 1killed by a_boundkilled by a_bound
207logic_not A[0] (empty[0]) invertedkilled by a_boundkilled by a_bound
2113add A[0] forced to 1killed by a_levelkilled by a_level
227logic_and A[0] (rd[0]) forced to 1survivedkilled by a_ahead
2312mux B[5] (wdata[5]) forced to 0survivedkilled by a_order
2412add A[4] inverted when bit 0 is 1killed by a_levelkilled by a_level
257logic_not A[0] (empty[0]) forced to 1survivedkilled by a_order
267logic_not A[0] (empty[0]) forced to 0killed by a_boundkilled by a_bound
277logic_and Y[0] (do_rd[0]) forced to 0survivedkilled by a_order
2813mux Y[1] inverted when bit 2 is 0killed by a_levelkilled by a_level
296eq B[0] forced to 0equivalentequivalent
3014add Y[4] invertedkilled by a_levelkilled by a_level
317logic_and A[0] (wr[0]) invertedkilled by a_nopopkilled by a_ahead
325eq A[0] forced to 0killed by a_emptykilled by a_empty
3312add Y[1] forced to 1killed by a_levelkilled by a_level
347logic_not Y[0] invertedkilled by a_boundkilled by a_bound
355eq A[3] invertedkilled by a_emptykilled by a_empty
3614sub Y[4] forced to 0killed by a_levelkilled by a_ahead
375eq A[1] invertedkilled by a_emptykilled by a_empty
387logic_not Y[0] forced to 0survivedkilled by a_ahead
3912mux B[3] (wdata[3]) inverted when bit 0 is 0survivedkilled by a_order
407logic_and A[0] (rd[0]) invertedkilled by a_nopushkilled by a_ahead

Measured: 40 entries, MCY seed 1, flops and memory excluded. Each run is bmc depth 20 on the mutated design; "killed" names the first assertion to fail. Survivors were then checked against the original with abc pdr.

A mutation is the design with one small change: a bit forced, an operator's output inverted, a mux input flipped. Run the whole property set against it. If some property fails, the mutation is killed. If all pass, it survives.

Most kills here come from a_level (11) and a_bound (5). Count and flags see every mutation of the bookkeeping logic.

Harmless or a Hole

Every Survivor Goes in One of Two BinsA mutation that no property catches is either harmless or a hole. An equivalence check of the mutated design against the original decides which.
Every survivor goes in one of two binsA mutation that no property catches is either harmless or a hole. An equivalence check of the mutated design against the original decides which. component 39 mutations one small change each component 24 killed some property failed component 15 not caught every propertypassed component 1 equivalent the outputs neverchange: discard component 14 holes the design changedand nothing noticed
Read the diagram as text
  • 39 mutations: one small change each
  • 24 killed: some property failed
  • 15 not caught: every property passed
  • 1 equivalent: the outputs never change: discard
  • 14 holes: the design changed and nothing noticed
  • 39 mutations → 24 killed
  • 39 mutations → 15 not caught
  • 15 not caught → 1 equivalent
  • 15 not caught → 14 holes

The check tries matched-flop LEC first, since the mutation leaves every flop in place. Then a two-copy miter from reset runs under PDR. It compares the flags always and the data whenever the FIFO holds an entry.

A survivor is one of two things. Either the change makes no difference to the outputs, or the properties cannot see a real change. A second test on each survivor, an equivalence check against the original, sorts them.

Here 1 survivor was equivalent: #29 forces bit 0 of the constant 16 in full = count == 16 to 0, which it already is. A matched-flop LEC run proved it in seconds. The other 14 are holes.

Of the holes, 5 corrupt the write data (#5, #7, #11, #23, #39). Most of the rest stop writes or reads from happening. In #17 the write request is tied to 0 inside the accept logic, and a FIFO that never accepts a write breaks none of the six properties.

Name the Missing Property

The missing property: data out equals data in, in order, checked on one write chosen by a free input (F4.4).
assert property (@(posedge clk) disable iff (rst)
  tracking && do_rd && t_ahead == 0 |-> rdata == t_data);

With it added, all 14 holes are killed, by a_order or its helper a_ahead (the followed entry is still inside the count). The rate goes from 24 of 38 (63 %) to 38 of 38 (100 %) of the mutations that change the design. This is the end-to-end property F3.4 argued for.

Cost and Practice

The campaign above took 49.4 s and 30.3 s on a laptop: one full formal run per mutation, plus an equivalence check per survivor. That is why teams build a fixed list once, rerun it when the properties change, and leave it out of every commit's regression.

Mutating a flop changes the clocking the engine can model, so flops and memories were excluded here. Commercial tools offer the same idea as a coverage view beside cone-of-influence and proof-core coverage (F7.1).

Asked in Interviews

QuestionAnswer
What is mutation coverage, and what does it say?Small changes to the design, each run against the property set. A non-equivalent survivor is a behaviour the properties cannot see. The rate measures the properties, not the design.
Why must it include an equivalence test?A change that never alters the outputs cannot be caught by any correct property; counting it would blame the properties for nothing.
Why not run it on every commit?Each mutation is a full formal run. Fix the list once and rerun it when the properties change.

Try It

The bookkeeping property set, inside the design for the runner.
  always @(posedge clk) begin
    if ($initstate) begin assume(rst); end else begin assume(!rst); end
  end
  // registered copies: primary-input history is kept in flops
  reg past = 0, wr_q = 0, rd_q = 0, full_q = 0, empty_q = 0;
  reg [4:0] count_q = 0;
  always @(posedge clk) begin
    past <= !rst; wr_q <= wr; rd_q <= rd; full_q <= full; empty_q <= empty; count_q <= count;
  end
  always @(posedge clk) if (!rst) begin
    a_bound: assert(count <= 5'd16);
    a_level: assert(count == wr_ptr - rd_ptr);
    a_full:  assert(full == (count == 5'd16));
    a_empty: assert(empty == (count == 5'd0));
    if (past && wr_q && full_q && !rd_q) a_nopush: assert(count == count_q);
    if (past && rd_q && empty_q && !wr_q) a_nopop: assert(count == count_q);
  end
The config.mcy behind the table: test_props runs the properties, test_eq the equivalence check.
[options]
size 40
seed 1
select t:$dff t:$mem_v2 %u %n
tags KILLED SURVIVED EQUIV

[script]
read_verilog -sv fifo16.sv
prep -top fifo16

[files]
fifo16.sv

[logic]
if result("test_props") == "FAIL":
    tag("KILLED")
elif result("test_eq") == "PASS":
    tag("EQUIV")
else:
    tag("SURVIVED")

[report]
print("Killed %d, survived %d, equivalent %d" % (tags("KILLED"), tags("SURVIVED"), tags("EQUIV")))

[test test_props]
expect PASS FAIL
run bash $PRJDIR/test_props.sh

[test test_eq]
expect PASS FAIL
run bash $PRJDIR/test_eq.sh
  1. Run the sample

    As loaded, the runner draws four entries (an unmutated control and three mutations) and proves each: COMPLETED: 2 killed, 2 survived, in 2.9 s. The control is one of the survivors.

  2. Read the full table

    Find the survivors on the write-data path. Which property would have to fail for each?

  3. Change the file, change the sample

    Add the order property (the order block in the shared source) and the runner draws a different sample, because it samples the cells of the file you give it: COMPLETED: 2 killed, 2 survived. Compare rates only on a fixed list.

design.sv

props.sv

Use assert property (@(posedge clk) ...);. Leave blank for immediate assertions in the design.

Inputs are free unless constrained. Run to find a counterexample or check a proof.

Model: one positive-edge clock, binary data. Sampled history requires declaration-initialized variables; primary-input history is unsupported.

SBY summary and log

Check Yourself

Predict the outputfifo16 with the bookkeeping property set, this site's MCY mode (four sampled entries including a control, each proved with smtbmc yices at depth 20). The verdict is:
module fifo16(input clk, rst, wr, rd, input [7:0] wdata, input pick,
  output [7:0] rdata, output empty, full);
  reg [7:0] mem [0:15];
  reg [4:0] wr_ptr=0, rd_ptr=0, count=0;
  assign empty = count == 5'd0;
  assign full  = count == 5'd16;
  wire do_wr = wr && !full, do_rd = rd && !empty;
  assign rdata = mem[rd_ptr[3:0]];
  always @(posedge clk)
    if (rst) begin wr_ptr <= 0; rd_ptr <= 0; count <= 0; end
    else begin
      if (do_wr) begin mem[wr_ptr[3:0]] <= wdata; wr_ptr <= wr_ptr + 5'd1; end
      if (do_rd) rd_ptr <= rd_ptr + 5'd1;
      count <= count + do_wr - do_rd;
    end
`ifdef FORMAL
  always @(posedge clk) begin
    if ($initstate) begin assume(rst); end else begin assume(!rst); end
  end
  // registered copies: primary-input history is kept in flops
  reg past = 0, wr_q = 0, rd_q = 0, full_q = 0, empty_q = 0;
  reg [4:0] count_q = 0;
  always @(posedge clk) begin
    past <= !rst; wr_q <= wr; rd_q <= rd; full_q <= full; empty_q <= empty; count_q <= count;
  end
  always @(posedge clk) if (!rst) begin
    a_bound: assert(count <= 5'd16);
    a_level: assert(count == wr_ptr - rd_ptr);
    a_full:  assert(full == (count == 5'd16));
    a_empty: assert(empty == (count == 5'd0));
    if (past && wr_q && full_q && !rd_q) a_nopush: assert(count == count_q);
    if (past && rd_q && empty_q && !wr_q) a_nopop: assert(count == count_q);
  end
`endif
endmodule
ConceptWhich sentence about a kill rate is false?
ConceptWhy must mutation coverage include an equivalence test?
ConceptA mutation inverts one bit of the write data before it enters the memory. Which property kills it?

Remember

  • A mutation is killed when some property fails on it, and survives when all pass.
  • Survivors are either equivalent (harmless) or holes; an equivalence check sorts them.
  • The kill rate measures the property set on a fixed mutation list, not the design.
  • Each survivor names a missing property: here, end-to-end data order.