Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • Reading Paths
  • Glossary
  • Metrics Reference
  • Formal Cheat Sheet
  • Formal Foundations19
  • Properties and SVA16
  • Property Development and Constraints19
  • Proof Engines and Convergence23
  • Formal Applications20
  • Equivalence Checking14
  • Formal Coverage and Signoff15
  • Methodology and Debug13
  • Interview Prep15
  • Capstone: A Chip-Block Verification Lab24
    • The Brief
    • Stage 1: FIFO and Handshake
      • Stage 1 FIFO Property Set
      • Stage 1 Bridge and the Assumption Contract
      • Stage 1 Covers and Mutants
    • Stage 2: Registers and Arbiter
    • Stage 3: Pipeline and Equivalence
    • Stage 4: Induction and Signoff
    • Solutions
    • Appendix: Run It Yourself on an Open-Source Flow
Loading lesson

←Stage 1: FIFO and Handshake
  1. Topics
  2. Formal Verification
  3. Capstone: A Chip-Block Verification Lab
  4. Stage 1: FIFO and Handshake
  5. Stage 1 Covers and Mutants
On this page

    Part 10 of 10, lesson 5 of 24Part 10 of 10/Capstone: A Chip-Block Verification Lab/Lesson 5 of 24All levels

    Stage 1 Covers and Mutants

    Reach the stage 1 covers, build the mutation table, and find which variants survive when a property family is removed.

    Why it matters

    A property set can pass every check and still be blind. The honest test has two receipts: a cover shows that a property's trigger can happen, and a mutation table shows that every fault is noticed by a named property. A set that passes only because it never looks at values would leave a value fault alive. This lesson makes you produce both receipts for stage 1 and find out what happens when a property family is taken away.

    Inputs

    • The two designs of the previous lessons, with their covers: for the FIFO a full queue, a simultaneous push and pop, an empty queue after use, and a tracked word being read; for the bridge a stall, skid use, a blocked input, simultaneous flow, and a tracked word leaving.

    • Your run results for the eight variants from lessons 1 and 2.

    sync_fifo.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    // formal: cover 8 expect reached slang top sync_fifo
    `define FORMAL
    // sync_fifo: 4-entry, 8-bit synchronous FIFO with a combinational read of the oldest word.
    // Synchronous active-high reset. count is an output so that properties can see it.
    // BUG selects a variant (0 is the design as specified); HELPER=0 removes the helper assertion.
    module sync_fifo #(parameter BUG = 0, parameter HELPER = 1) (
      input  wire       clk,
      input  wire       rst,
      input  wire       push,
      input  wire [7:0] wdata,
      input  wire       pop,
      output wire [7:0] rdata,
      output wire       full,
      output wire       empty,
      output reg  [2:0] count
    );
      reg [7:0] mem [0:3];
      reg [1:0] wptr, rptr;
    
      assign full  = (BUG == 3) ? (count >= 3'd3) : (count == 3'd4);
      assign empty = (count == 3'd0);
      assign rdata = mem[rptr];
    
      wire do_push = push && (!full  || BUG == 1);
      wire do_pop  = pop  && (!empty || BUG == 2);
    
      always @(posedge clk) begin
        if (rst) begin
          wptr  <= 2'd0;
          rptr  <= 2'd0;
          count <= 3'd0;
        end else begin
          if (do_push) begin
            mem[wptr] <= (BUG == 5) ? {1'b0, wdata[6:0]} : wdata;
            wptr      <= wptr + 2'd1;
          end
          if (do_pop) rptr <= rptr + 2'd1;
          if (BUG == 4 && do_push && do_pop) count <= count + 3'd1;
          else                               count <= count + {2'b0, do_push} - {2'b0, do_pop};
        end
      end
    
    `ifdef FORMAL
      // ---- harness: the first cycle is a reset, the state before it is arbitrary ----
      reg init = 1'b1;
      always @(posedge clk) init <= 1'b0;
      always @(*) if (init) assume (rst);
    
      // ---- state properties (after the reset cycle) ----
      always @(*) if (!init) begin
        a_range:   assert (count <= 3'd4);
        a_flags:   assert (full == (count == 3'd4) && empty == (count == 3'd0));
        a_not_both: assert (!(full && empty));
        a_ptr:     assert (count[1:0] == (wptr - rptr));
      end
    
      // ---- transition properties (they read history, so skip the cycle after reset) ----
      always @(posedge clk) if (!init && !$past(rst)) begin
        a_push_inc: assert (!($past(push) && !$past(pop) && !$past(full)) || count == $past(count) + 3'd1);
        a_pop_dec:  assert (!($past(pop) && !$past(push) && !$past(empty)) || count == $past(count) - 3'd1);
        a_ovf_ign:  assert (!($past(push) && !$past(pop) && $past(count) == 3'd4) || count == $past(count));
        a_both:     assert (!($past(push) && $past(pop) && !$past(full) && !$past(empty)) || count == $past(count));
      end
    
      // ---- data integrity by tracked-slot abstraction: one free slot, one shadow value ----
      // The free constant is an anyconst wire that is also assumed equal to its one-cycle-old copy;
      // this form works in both front ends, while a clocked assume of stability lags one step.
      (* anyconst *) wire [1:0] tslot;
      reg [1:0] tslot_q;
      always @(posedge clk) tslot_q <= tslot;
      always @(*) if (!init) assume (tslot == tslot_q);
      reg       tv;      // the tracked slot holds a word we are watching
      reg [7:0] td;      // the value that should be there
      always @(posedge clk) begin
        if (rst) tv <= 1'b0;
        else begin
          if (do_push && wptr == tslot) begin tv <= 1'b1; td <= wdata; end
          else if (do_pop && rptr == tslot) tv <= 1'b0;
        end
      end
      always @(*) if (!init && tv && !empty && rptr == tslot) a_data: assert (rdata == td);
      generate if (HELPER) begin : g_helper
        always @(*) if (!init && tv) a_tracked: assert (mem[tslot] == td);
      end endgenerate
    
      // ---- covers: the triggers of the properties can happen ----
      always @(*) if (!init) begin
        c_full:  cover (count == 3'd4);
        c_empty_after_use: cover (empty && tv == 1'b0 && wptr == 2'd2);
        c_tracked_read: cover (tv && !empty && rptr == tslot);
      end
      always @(posedge clk) if (!init) c_both: cover (push && pop && !full && !empty);
    `endif
    endmodule
    hs_bridge.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    // formal: cover 5 expect reached slang top hs_bridge
    `define FORMAL
    // hs_bridge: valid/ready register stage with a skid register (two words can be held).
    // Output stage ovld/odat plus skid stage svld/sdat. in_ready is a function of a register only.
    // Synchronous active-high reset empties both stages. A transfer happens in a cycle where valid
    // and ready are both 1. BUG selects a variant (0 is the design as specified); HELPER=0 removes the helpers.
    module hs_bridge #(parameter BUG = 0, parameter HELPER = 1) (
      input  wire       clk,
      input  wire       rst,
      input  wire       in_valid,
      output wire       in_ready,
      input  wire [7:0] in_data,
      output wire       out_valid,
      input  wire       out_ready,
      output wire [7:0] out_data
    );
      reg       ovld, svld;
      reg [7:0] odat, sdat;
    
      assign in_ready  = !svld;
      assign out_valid = ovld;
      assign out_data  = odat;
    
      wire in_xfer  = in_valid && in_ready;
      wire out_xfer = ovld && out_ready;
      wire stalled  = ovld && !out_ready;
    
      always @(posedge clk) begin
        if (rst) begin
          ovld <= 1'b0;
          svld <= 1'b0;
        end else if (stalled) begin
          if (in_xfer && BUG != 1) begin svld <= 1'b1; sdat <= in_data; end
          if (BUG == 2 && !in_valid) ovld <= 1'b0;
          if (BUG == 3 && in_xfer)   odat <= in_data;
        end else begin
          if (svld) begin
            odat <= sdat; ovld <= 1'b1; svld <= 1'b0;
          end else begin
            odat <= in_data; ovld <= in_valid;
          end
        end
      end
    
    `ifdef FORMAL
      reg init = 1'b1;
      always @(posedge clk) init <= 1'b0;
      always @(*) if (init) assume (rst);
    
      // ---- counters: accepted and delivered words (4 bits, wrap at 16) ----
      reg [3:0] acc, dlv;
      always @(posedge clk) begin
        if (rst) begin acc <= 4'd0; dlv <= 4'd0; end
        else begin
          if (in_xfer)  acc <= acc + 4'd1;
          if (out_xfer) dlv <= dlv + 4'd1;
        end
      end
    
      always @(*) if (!init) begin
        a_skid_needs_out: assert (!svld || ovld);
        a_conserve:       assert (acc - dlv == {3'b0, ovld} + {3'b0, svld});
      end
    
      // ---- protocol: history-based, so skip the cycle after reset ----
      always @(posedge clk) if (!init && !$past(rst)) begin
        a_valid_hold: assert (!($past(out_valid) && !$past(out_ready)) || out_valid);
        a_data_hold:  assert (!($past(out_valid) && !$past(out_ready)) || out_data == $past(out_data));
        a_no_ready_comb: assert (in_ready == !svld);
      end
    
      // ---- data integrity by tracked-word abstraction: follow the n-th accepted word ----
      (* anyconst *) wire [3:0] tn;
      reg [3:0] tn_q;
      always @(posedge clk) tn_q <= tn;
      always @(*) if (!init) assume (tn == tn_q);
      reg       tv;      // the tracked word has been accepted and not yet delivered
      reg [7:0] td;
      always @(posedge clk) begin
        if (rst) tv <= 1'b0;
        else begin
          if (in_xfer && acc == tn) begin tv <= 1'b1; td <= in_data; end
          else if (out_xfer && dlv == tn) tv <= 1'b0;
        end
      end
      always @(*) if (!init && out_xfer && tv && dlv == tn) a_data: assert (out_data == td);
      generate if (HELPER) begin : g_helper
        always @(*) if (!init) begin
          h_in_out:  assert (!(tv && dlv == tn) || (ovld && odat == td));
          h_in_skid: assert (!(tv && dlv + 4'd1 == tn) || (svld && sdat == td));
          h_pos:     assert (!tv || (dlv == tn) || (dlv + 4'd1 == tn));
        end
      end endgenerate
    
      // ---- covers ----
      always @(*) if (!init) begin
        c_stall:     cover (stalled);
        c_skid_used: cover (svld);
        c_backpress_full: cover (!in_ready);
        c_flow:      cover (in_xfer && out_xfer);
        c_tracked_out: cover (out_xfer && tv && dlv == tn);
      end
    `endif
    endmodule

    Expected outcome of both blocks: reached. A cover that is not reached within the depth is inconclusive, never unreachable, unless an engine proves it so.

    Deliverable

    Produce

    How

    A cover table

    Each cover and the step at which it is first reached; the property each cover is the trigger of.

    A mutation table

    Each of the eight variants, the properties that notice it, the step of each, and the smallest failing depth.

    A survivor report

    Remove one property family at a time (counts and flags, transitions, tracked data, helpers) and list the variants that no remaining property notices.

    New mutants

    Some stage 1 properties fail on none of the eight variants. Write a one-line mutant for each, so that every property fails at least once.

    Two cover rows and the stage rows

    fifo-covers, bridge-covers, and the proven and counterexample rows.

    Pass criterion

    • Every cover is reached at a step you have written down.

    • The mutation table has no empty row: every variant is killed by at least one property, shown by a run.

    • The survivor function below, applied to your kill table, reports the survivors of each reduced set.

    • The package passes the engine.

    example.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    def survivors(kills, planted, removed=()):
        live = set(planted)
        for prop, muts in kills.items():
            if prop not in removed:
                live -= set(muts)
        return sorted(live)
    # a demonstration kill table (illustrative: the one-bit toggle again)
    DEMO = {"a_flip": ["tog-W1"], "a_hold": ["tog-W2"], "a_reset": ["tog-W1", "tog-W3"]}
    PLANTED_DEMO = ["tog-W1", "tog-W2", "tog-W3"]
    assert survivors(DEMO, PLANTED_DEMO) == []
    assert survivors(DEMO, PLANTED_DEMO, removed=["a_hold"]) == ["tog-W2"]
    assert survivors(DEMO, PLANTED_DEMO, removed=["a_reset"]) == ["tog-W3"]
    assert survivors(DEMO, PLANTED_DEMO, removed=["a_flip", "a_reset"]) == ["tog-W1", "tog-W3"]
    # your table: property -> variants it kills, from your own runs
    YOUR_KILLS = {}
    PLANTED = ["fifo-W%d" % i for i in range(1, 6)] + ["bridge-W%d" % i for i in range(1, 4)]
    assert survivors(YOUR_KILLS, PLANTED) == sorted(PLANTED), "an empty table kills nothing"
    print("PASS: the survivor function reports every variant alive for an empty kill table")
    example.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    # ---- formal lab results ledger, engine v2: every row cites a typed run (kind, depth, outcome[, design]) ----
    STATUS = ("proven", "bounded", "cex", "cover-reached", "inconclusive")
    FROM_RUN = {"proven": {("prove", "proved")},
                "bounded": {("bmc", "bounded")},
                "cex": {("bmc", "counterexample"), ("prove", "counterexample")},
                "cover-reached": {("cover", "reached")},
                "inconclusive": {("prove", "unproven"), ("cover", "not-reached")}}
    NEED = {"sync_fifo": 9, "rr_arbiter": 5, "hs_bridge": 4, "csr_block": 3,
            "pipe3": 5, "pipe3_eq": 5, "mod10_counter": 15, "toggle_demo": 4}
    KEYS = ("property", "design", "status", "bound", "why", "run", "assumes", "cover", "kills", "rev")
    REV = 3
    ALWAYS = "reset-at-step-0"
    TRACKED = "tracked-constant-stable"
    def row_problems(e, runs, tracked):
        n = e.get("property", "?")
        miss = [k for k in KEYS if k not in e]
        if miss:
            return ["missing-field:%s:%s" % (n, ",".join(miss))]
        if e["status"] not in STATUS:
            return ["bad-status:" + n]
        p = []
        if e["rev"] != REV:
            p.append("stale:" + n)
        if e["run"] not in runs:
            return p + ["no-such-run:" + n]
        kind, depth, outcome = runs[e["run"]][:3]
        if len(runs[e["run"]]) > 3 and runs[e["run"]][3] != e["design"]:
            p.append("run-of-another-design:" + n)         # a run typed with its design may be cited only by that design's rows
        if (kind, outcome) not in FROM_RUN[e["status"]]:
            p.append("status-not-from-run:" + n)
        if e["bound"] != depth:
            p.append("bound-not-run-depth:" + n)
        has_cover = bool(str(e["cover"]).strip())
        if e["status"] in ("proven", "bounded") and not (has_cover and e["kills"]):
            p.append("no-evidence:" + n)
        if e["status"] == "cover-reached" and not has_cover:
            p.append("no-evidence:" + n)
        if e["status"] == "cex" and not e["kills"]:
            p.append("no-evidence:" + n)
        if e["status"] == "bounded":
            if e["design"] not in NEED:
                p.append("unknown-design:" + n)
            elif depth < NEED[e["design"]]:
                p.append("short-bound:" + n)
            if len(str(e["why"]).strip()) < 12:
                p.append("unjustified:" + n)
        if ALWAYS not in e["assumes"]:
            p.append("hidden-assume:%s:%s" % (n, ALWAYS))
        if n in tracked and TRACKED not in e["assumes"]:
            p.append("hidden-assume:%s:%s" % (n, TRACKED))
        return p
    def ledger_ok(ledger, runs, required, mutants, register, tracked, final=True):
        names = [e.get("property") for e in ledger]
        bad = ["duplicate:" + n for n in sorted({n for n in names if names.count(n) > 1})]
        bad += ["missing-row:" + r for r in sorted(set(required) - set(names))]
        bad += [q for e in ledger for q in row_problems(e, runs, tracked)]
        good = [e for e in ledger if not row_problems(e, runs, tracked)]      # only a row without complaints counts as evidence
        solid = {e["property"] for e in good if e["status"] in ("proven", "cover-reached")}
        for a, rows in sorted(register.items()):
            if rows is None:
                if final:
                    bad.append("deferred-assume:" + a)      # a stage may defer a discharge; a final package may not
            elif not (rows and all(r in solid for r in rows)):
                bad.append("open-assume:" + a)
        bad += ["unregistered-assume:%s:%s" % (e.get("property"), a) for e in ledger
                for a in e.get("assumes", []) if a not in register]
        killed = {m for e in good if e["status"] == "cex" for m in e["kills"]}
        bad += ["survivor:" + m for m in sorted(set(mutants) - killed)]
        bad += ["unsupported-kill:%s:%s" % (e.get("property"), m) for e in ledger
                if e.get("status") in ("proven", "bounded") for m in e.get("kills", []) if m not in killed]
        return bad
    def mutation_score(ledger, mutants):
        assert mutants, "no mutants registered"
        killed = {m for e in ledger if e.get("status") == "cex" for m in e.get("kills", [])}
        return len(killed & set(mutants)) / len(mutants)
    
    RUNS = {
        "fifo-cover": ("cover", 8, "reached"),
        "bridge-cover": ("cover", 5, "reached"),
    }
    REQUIRED = ["fifo-covers", "bridge-covers"]
    REGISTER = {"reset-at-step-0": None, "tracked-constant-stable": ["fifo-covers", "bridge-covers"]}
    YOUR_ROWS = []          # the two cover-reached rows, with the names and steps of your covers
    bad = ledger_ok(YOUR_ROWS, RUNS, REQUIRED, [], REGISTER, set(), final=False)
    if YOUR_ROWS:
        assert bad == [], bad
        print("PASS: stage 1 cover rows consistent")
    else:
        assert sorted(bad) == sorted(["missing-row:fifo-covers", "missing-row:bridge-covers", "open-assume:tracked-constant-stable"]), bad
        print("PASS: the empty package is rejected: the tracked-constant assumption has no discharging cover")

    Hint ladder

    • Hint 1: take the cover names from the listing and match each to the property it is the trigger of.

    • Hint 2: for the survivor report, turn the removed properties into covers in your copy, so the rest of the set is unchanged, and run each variant at depth 12.

    • Hint 3: a property that never fails on a planted variant needs a mutant of your own: change one line of the listing so that only that property should notice, and check that it does.

    • Hint 4: if a variant survives a reduced set, the missing property is the one you removed.

    Verify and debug

    Rerun the whole slice through the engine after any property change: a strengthened property can orphan a cover row, and a weakened set can bring a mutant back to life. Keep the kill steps in the table: a kill that moves to a later step after an edit means the property got weaker.

    Cover never run and never read: the dashboard is clean while required scenarios sit unreachable. Symptom is a proof set with silent covers. Cause is treating unreached covers as information instead of failures. Fix: an unreached cover on a required scenario blocks signoff and gets an owner. Found by filing the reached step of every trigger before the stage gate opens.

    “How do you know the pass is not vacuous?” Answer: every implication has an antecedent cover, every cover names its reached step, and every property has killed a mutant. Show your stage 1 tables: the covers with their steps beside the proofs, and the survivors of each reduced property set.

    Check yourself A property has a cover that is reached at step 2 and has never failed on any variant. Is it vacuous?

    No: the cover shows its trigger can happen. It is untested, not vacuous: write a mutant that only it can notice.

    Check yourself Why does the ledger count a kill only from a counterexample row?

    A proven row that claims a kill is a claim; a counterexample row is the run that shows it. The engine reports a kill nobody exhibited as unsupported.

    Related topics

    • Next: Stage 2 CSR Proofs from the Table

    • Earlier: Cover Properties, Vacuity and Reachability

    • After you have filed your rows: Solutions, FIFO Property Set

    Previous

    164 / 183

    Next: Stage 2: Registers and Arbiter

    Up next

    Up nextStage 2: Registers and Arbiter

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Stage 1 FIFO Property Set