Over-Constraint and Vacuity

Hide a FIFO bug behind one assumption, find it again with the honest one, and let a cover tell the two apart.

Lesson
F3.2
Time
15 min
Runs on
SymbiYosys
Check
4 questions

A FIFO bench carried assume(!(push && count == 16)) from its first week, added to silence a failure until the producer model was ready. The failure was the bug: the exported full lagged the count by one cycle, so an obedient producer could push a seventeenth entry. Every property passed for four months, and one cover on "push while the count is sixteen" would have come back unreachable on day one.

The lag cycle: count is 16 and full is still low
012345clkfreepushfreepopcount1314151617fullcount 16, full 0, pushcount 17SBY reports FAIL

Measured with smtbmc yices at depth 20. Steps 1 to 16 are one push each from reset. The sampled violation is step 18, reported at 19: the last sample a depth-20 run checks.

Too Many Assumptions

Two Assumptions, Two Different WorldsThe same intent, "the producer never pushes into a full FIFO", written against two signals. Arrows mean "leaves the solver".
Two assumptions, two different worldsThe same intent, "the producer never pushes into a full FIFO", written against two signals. Arrows mean "leaves the solver". component No assumption pushes into a fullFIFO allowed component assume(!(push&& full)) reads the port theproducer can see component assume(!(push&& count ==16)) reads a register theproducer cannot see component False failures illegal producers(F3.3) component Every legalproducer including the one thatpushes in the lagcycle component Every legalproducer but one the push that breaksthe FIFO is gone
Read the diagram as text
  • No assumption: pushes into a full FIFO allowed
  • assume(!(push && full)): reads the port the producer can see
  • assume(!(push && count == 16)): reads a register the producer cannot see
  • False failures: illegal producers (F3.3)
  • Every legal producer: including the one that pushes in the lag cycle
  • Every legal producer but one: the push that breaks the FIFO is gone
  • No assumption → False failures
  • assume(!(push && full)) → Every legal producer
  • assume(!(push && count == 16)) → Every legal producer but one

The right-hand zone is over-constraint: the proof is true, of a world the hardware never lives in. A pass looks the same in both right-hand columns. A cover does not.

An assumption removes input sequences. Remove the right ones and the tool checks the legal environment. Remove one too many and it checks a world the chip never sees: over-constraint. Nothing in a PASS says which zone you are in (F1.3).

The internal assumption also breaks the master rule of F3.1: it constrains the design through its own register. The FIFO bug lives exactly in the gap between full and count == 16.

RunMeasured resultTime
A: assume(!(push && count == 16)), assert count <= 16BMC PASS to depth 20535 ms
B: assume(!(push && full)), assert count <= 16FAIL at reported step 19577 ms
B in concurrent SVA (props.sv)FAIL at reported step 19686 ms
restrict(count < 16) instead of an assumptionBMC PASS to depth 20533 ms
Fixed RTL (full from the count), honest assumptionBMC PASS to depth 20535 ms

fifo16 with the lagging full; mode bmc, depth 20, smtbmc yices, reset assumed in step 0.

restrict is an assumption that only formal honours, and it hides the bug just as well. It exists for case splits (F4.3), never for silencing a failure.

The Cover Is the Alarm

Where the internal assumption cuts

The honest run, steps 16 to 18. The internal assumption forbids the push at step 17; the honest one allows it, because full is still 0.

  1. push=1, pop=0

    count=15, full=0

    count <= 16 holds

    ✓ Holds
  2. push=1, pop=0

    count=16, full=0

    internal assumption: no push here

    Assumed for induction
  3. push=0, pop=0

    count=17, full=1

    count <= 16 fails

    × Fails

Select a frame to inspect its inputs and assertion.

Measured values from data/F3-2.json. SBY reports the step-18 failure at step 19.

CoverUnder the internal assumptionUnder the honest assumption
fullreached, reported step 19reached, reported step 19
count == 16 && pushUnreached cover statement at top: c_push_at16reached, reported step 18
pop && emptyreached, reported step 2reached, reported step 2

Mode cover, depth 20, the same buggy RTL. Measured: 602 ms and 641 ms.

For every behaviour an assumption touches, cover the case you still expect to happen. A push at sixteen is legal for this producer, and the internal assumption made it unreachable. SBY names the unreached cover in its log: that line is the alarm.

Vacuity in Formal

Vacuous in simulation

push && count == 16 |-> ...
// this test never filled the FIFO

One test's stimulus never made the antecedent true.

Vacuous in formal

assume(!(push && count == 5'd16));
// no legal input sequence fills it and pushes

No input sequence the assumptions allow, within the bound, makes it true.

In simulation (A1.3 Implication and vacuous success) vacuity is a weak test. In formal it is a statement about the whole environment, caused by the assumptions or by dead logic (F3.5). Commercial tools flag some of it automatically; in SBY you write the cover yourself, so make it a habit.

The habit is cheap: one cover per assumption, naming the legal behaviour closest to what the assumption forbids. For !(push && full) that is a push at sixteen, which an honest producer may still make in the lag cycle.

Asked in Interviews

QuestionAnswer
What is over-constraining and how do you detect it?Assumptions that remove legal behaviour. A cover on behaviour you expect comes back unreachable.
What is vacuity in formal?A pass whose antecedent no legal input sequence can reach under the assumptions.
Can restrict hide a bug?Yes. To the prover it is an assumption.

Try It

Concurrent SVA

assert property (@(posedge clk) disable iff (rst) count <= 5'd16);
assume property (@(posedge clk) disable iff (rst) !(push && full));
// the dishonest variant:
assume property (@(posedge clk) disable iff (rst) !(push && count == 5'd16));

One claim, and the producer promise written two ways.

The clocked immediate form the editor runs

always @(posedge clk) if (!rst) begin
  assume(!(push && count == 5'd16));
  assert(count <= 5'd16);
end

No delays, so each line maps one to one.

The task. For the cover runs set mode cover; the depth stays at the default, 20.
[options]
mode bmc
depth 20
[engines]
smtbmc yices
[script]
read_verilog -formal -sv design.sv
prep -top fifo16
[files]
design.sv
  1. Run the internal assumption

    Run as it is: BMC PASS to depth 20. Fixed?

  2. Cover under it

    Swap the assert for the three covers and set mode cover. Read which cover the log names as unreached.

  3. Honest assumption

    Back in mode bmc, assume !(push && full) instead: FAIL at reported step 19. Find the lag cycle in the trace.

  4. Cover under the honest one

    Run the three covers again: all reached.

  5. Fix the RTL

    Drive full from count == 16 combinationally and rerun the honest bench: BMC PASS to depth 20.

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 lagging full, mode cover, default depth 20, smtbmc yices, reset assumed in step 0, assume(!(push && count == 16)) active, covers on full, count == 16 && push and pop && empty. The overall cover result is:
module fifo16(input clk, rst, push, pop, input [7:0] wdata,
  output [7:0] rdata, output empty, output reg full=0);
  reg [7:0] mem [0:15];
  reg [3:0] wr_ptr=0, rd_ptr=0;
  reg [4:0] count=0;
  assign empty = count == 5'd0;
  wire wr_en = push && !full, rd_en = pop && !empty;
  assign rdata = mem[rd_ptr];
  always @(posedge clk)
    if (rst) begin wr_ptr <= 0; rd_ptr <= 0; count <= 0; full <= 0; end
    else begin
      if (wr_en) begin mem[wr_ptr] <= wdata; wr_ptr <= wr_ptr + 4'd1; end
      if (rd_en) rd_ptr <= rd_ptr + 4'd1;
      count <= count + wr_en - rd_en;
      full <= count == 5'd16;   // bug: one cycle behind count
    end
`ifdef FORMAL
  always @(posedge clk) begin
    if ($initstate) begin assume(rst); end else begin assume(!rst); end
  end
  always @(posedge clk) if (!rst) begin
    assume(!(push && count == 5'd16));
    c_full:      cover(full);
    c_push_at16: cover(count == 5'd16 && push);
    c_pop_empty: cover(pop && empty);
  end
`endif
endmodule
ConceptWhich assumption in a FIFO bench is wrong for the master rule?
ConceptHow does vacuity in formal differ from vacuity in simulation?
Read the waveformIn the honest run, at which step is count 16 while full is still 0?
The lag cycle: count is 16 and full is still low
012345clkfreepushfreepopcount1314151617fullcount 16, full 0, pushcount 17SBY reports FAIL

Measured with smtbmc yices at depth 20. Steps 1 to 16 are one push each from reset. The sampled violation is step 18, reported at 19: the last sample a depth-20 run checks.

Remember

  • Over-constraint: an assumption removes legal behaviour, and the proof becomes true of a world the chip never sees.
  • Write assumptions on ports the neighbour can see, never on the design's internal state.
  • For every assumption, cover the behaviour you still expect; an unreached cover is the alarm.
  • restrict is an assumption to the prover and can hide a bug just as well.