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

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Part 0 · What Debugging Is6
  • Part 1 · Reproduce8
  • Part 2 · Observe9
  • Part 3 · Localise10
  • Part 4 · Hypothesise and Test6
    • A Hypothesis Must Predict
    • The Experiment That Splits Suspects
    • Keeping Several Hypotheses Alive
    • Confirmation Bias and Blaming the Tool
    • One Change at a Time
    • The Debug Log
  • Part 5 · Bug Classes16
  • Part 6 · Debug by Stage9
  • Part 7 · Hangs and Deadlocks7
  • Part 8 · Intermittent Failures7
  • Part 9 · Fix, Prove, Prevent7
  • Part 10 · Case Studies12
  • Part 11 · Debug as a Team5
  • Part 12 · Interview Prep5
Loading lesson

‹Part 4 · Hypothesise and Test
  1. Topics
  2. The Art of Hardware Debug
  3. Part 4 · Hypothesise and Test
  4. Confirmation Bias and Blaming the Tool

Part 4 · Hypothesise and Test · Working (W)

Confirmation Bias and Blaming the Tool

Seeing what you expect, and the "it must be a simulator bug" claim checked properly: a small FSM that hangs in Icarus and passes in Verilator turns out to hang on silicon too, and a dbglab seed where two simulators show two different bugs. How a real tool bug would be shown in a dozen lines.

On this page

    Why this matters

    When you expect an answer, every result looks like support, and "the other simulator passes" feels like proof that the tool is wrong. Most such claims are the design's own X or ordering problem, and waving them through ships a chip that hangs at power-up.

    After this lesson you can: test a belief by looking for the result that would refute it, check a "simulator bug" claim with a minimal program, the language rules and a third run, and write a tool bug report someone can act on.

    Note

    Level: Working (W) · Debug-loop step: Test

    Before this: A Hypothesis Must Predict, X in a Waveform

    "Icarus is broken"

    A message in the team chat: "burst_fsm times out in Icarus and passes in Verilator. The FSM is trivial, so it's an Icarus bug. Can we drop Icarus from the regression?" Attached, two real runs of the same file:

    Terminal
    $ iverilog -g2012 -o naive.vvp naive.sv && vvp -n naive.vvp
    FATAL: naive.sv:38: TIMEOUT: no done 100 cycles after start (state=xx)
           Time: 1040000  Scope: tb
    $ verilator --binary --timing -Wno-fatal --top-module tb -Mdir vl naive.sv > vl.log 2>&1 && vl/Vtb
    PASS: done 8 cycles after start
    ...

    The author has evidence for their view: one tool passes, the FSM "looks right", and Icarus does lack some SystemVerilog features. What would you need to see before agreeing?

    What people usually do, and why it fails

    They collect support. The passing run is support. The code review ("three states, what could go wrong?") is support. A forum post about an Icarus limitation is support. Nobody runs the experiment that could show the claim is wrong, because the claim already feels proven. This is confirmation bias: we look for, notice and remember evidence that fits what we already believe, and we explain away the rest.

    In debugging it has two common forms. Blaming the tool when two tools disagree. And seeing the expected bug in a result that is really a different one. Both end the same way: the hypothesis is never tested, because every result was read as agreeing with it.

    The method: try to break your own idea

    1. State the claim so it can fail. "Icarus mis-simulates this case statement" predicts that a minimal case statement gives a result the language forbids.

    2. Ask what you would see if you were wrong, and run that first. For a tool claim: if the design is at fault, a different model of the same chip (random start values) should fail too.

    3. Shrink until it fits on a screen. A tool difference that survives shrinking to ten lines is easy to judge; one that vanishes when you shrink was the design.

    4. Name the rule. Which sentence of the language does the tool break? "It should obviously do X" is not a rule.

    5. Distinguish freedom from error. Two-state and four-state simulators are allowed to differ on X; race-prone code is allowed to give different orders. A difference inside the language's freedom is your bug.

    6. Only then file it, with the program, both outputs, the versions and the rule.

    The picture: is it the tool?

    Flowchart: two tools disagree. Shrink; if the difference does not survive in about ten lines, it is the design or bench. If it does and involves X, Z, power-up or same-time ordering, rerun with random start values or another order; if the design fails there too, it is the design. Otherwise find the rule the tool breaks; with no rule it is the design, with a rule it is a tool bug to file with the minimal program, both outputs, versions and the rule.
    A claim of a tool bug has to survive shrinking, a run with random start values or another ordering, and a named rule.

    Do it: the FSM that "only fails in Icarus"

    The design as written

    burst_fsm.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
    // bad-example: burst_fsm with the bench its designer wrote. It passes in Verilator and times out in Icarus.
    `timescale 1ns/1ps
    module burst_fsm (
      input  logic clk, rst_n, start,
      output logic busy, done
    );
      localparam logic [1:0] IDLE = 2'd0, RUN = 2'd1, DONE = 2'd2;
      logic [1:0] state;
      logic [2:0] beat;
      // No reset on state: "IDLE is 0, and flip-flops come up at 0".
      always @(posedge clk)
        case (state)
          IDLE:    if (start) state <= RUN;
          RUN:     if (beat == 3'd7) state <= DONE;
          DONE:    state <= IDLE;
          default: ;                                  // "unreachable"
        endcase
      always @(posedge clk or negedge rst_n)
        if (!rst_n)            beat <= '0;
        else if (state == RUN) beat <= beat + 3'd1;
      assign busy = (state == RUN);
      assign done = (state == DONE);
    endmodule
    
    module tb;
      logic clk = 1'b0, rst_n = 1'b0, start = 1'b0, busy, done;
      int   cyc = 0;
      always #5 clk = ~clk;
      burst_fsm dut (.*);
      initial begin
        repeat (2) @(posedge clk);
        rst_n = 1'b1;
        @(negedge clk) start = 1'b1;
        @(negedge clk) start = 1'b0;
        while (done !== 1'b1) begin
          @(negedge clk);
          cyc++;
          if (cyc > 100) $fatal(1, "TIMEOUT: no done 100 cycles after start (state=%b)", dut.state);
        end
        $display("PASS: done %0d cycles after start", cyc);
        $finish;
      end
    endmodule

    The Icarus run already said something the chat message skipped: state=xx. The state register is unknown 100 cycles after start.

    Shrink it until it fits on a screen

    Ten lines that keep only the suspected construct, a case on an unknown selector:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // case_x_tb: the smallest program that shows the difference. A two-bit selector that is
    // unknown, a case with three items and a default. Which branch runs?
    `timescale 1ns/1ps
    module case_x_tb;
      logic [1:0] s;                 // never assigned: X in a four-state simulator
      initial begin
        #1;
        case (s)
          2'd0:    $display("s=%b took item 2'd0", s);
          2'd1:    $display("s=%b took item 2'd1", s);
          2'd2:    $display("s=%b took item 2'd2", s);
          default: $display("s=%b took the default", s);
        endcase
        $display("PASS: case on s ran one branch");
        $finish;
      end
    endmodule
    Terminal
    $ iverilog -g2012 -o cx.vvp casex_min.sv && vvp -n cx.vvp
    s=xx took the default
    PASS: case on s ran one branch
    ...
    $ verilator --binary --timing -Wno-fatal --top-module case_x_tb -Mdir vlc casex_min.sv > vlc.log 2>&1 && vlc/Vcase_x_tb
    s=00 took item 2'd0
    PASS: case on s ran one branch
    ...

    The difference survives shrinking, so check the rule. A case compares the selector with each item using case equality, where X and Z bits must match exactly. xx matches none of 00, 01, 10, so the default runs. That is what Icarus did. Verilator is a two-state simulator by design: it has no X, and an unassigned variable starts at zero unless you ask for random values. That is what Verilator did. Both tools followed their rules. The difference is inside the language's freedom about unknown values, which by the flow above means: suspect the design.

    The run that could prove the design innocent

    If the design were fine and Icarus wrong, then a two-state simulator that powers the state register up at random values, as a chip does, should always pass. That is the disconfirming experiment the chat message never ran:

    Terminal
    $ verilator --binary --timing -Wno-fatal --x-assign unique --x-initial unique --top-module tb -Mdir vlx naive.sv > vlx.log 2>&1
    $ for n in $(seq 1 20); do printf "seed %2d: " $n; vlx/Vtb +verilator+rand+reset+2 +verilator+seed+$n 2>&1 | grep -E "PASS|TIMEOUT" | head -1; done
    seed  1: PASS: done 8 cycles after start
    seed  2: [1040000] %Fatal: naive.sv:38: Assertion failed in tb: TIMEOUT: no done 100 cycles after start (state=11)
    ...
    seed  6: PASS: done 6 cycles after start
    ...
    seed 14: [1040000] %Fatal: naive.sv:38: Assertion failed in tb: TIMEOUT: no done 100 cycles after start (state=11)
    ...

    Four of twenty power-ups hang in state 11, the encoding no item names, where default: ; holds it for ever. Four more finish in 6 cycles instead of 8: they powered up in RUN, were already counting a burst nobody asked for, and swallowed the real start. Twelve pass. The chip would hang on some power-ups. Icarus showed it as X; the passing Verilator run was the one hiding it.

    Prove it in one bench that passes in both simulators

    Rather than rely on random start values, power the design up in each of its four states on purpose and check what it does. The same bench checks the fix: a reset on state, and a default that returns to IDLE.

    burst_fsm.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
    // powerup_tb: is it the simulator or the design? Power burst_fsm up in each of its four
    // possible states (what a chip can do) and watch whether it finishes a burst. The same
    // check runs on the fix: a reset on state and a default that recovers.
    `timescale 1ns/1ps
    module burst_fsm (                                  // as written: no reset on state
      input  logic clk, rst_n, start,
      output logic busy, done
    );
      localparam logic [1:0] IDLE = 2'd0, RUN = 2'd1, DONE = 2'd2;
      logic [1:0] state;
      logic [2:0] beat;
      always @(posedge clk)
        case (state)
          IDLE:    if (start) state <= RUN;
          RUN:     if (beat == 3'd7) state <= DONE;
          DONE:    state <= IDLE;
          default: ;                                    // "unreachable"
        endcase
      always @(posedge clk or negedge rst_n)
        if (!rst_n)            beat <= '0;
        else if (state == RUN) beat <= beat + 3'd1;
      assign busy = (state == RUN);
      assign done = (state == DONE);
    endmodule
    
    module burst_fsm_fixed (                            // reset on state; default recovers
      input  logic clk, rst_n, start,
      output logic busy, done
    );
      localparam logic [1:0] IDLE = 2'd0, RUN = 2'd1, DONE = 2'd2;
      logic [1:0] state;
      logic [2:0] beat;
      always @(posedge clk or negedge rst_n)
        if (!rst_n) state <= IDLE;
        else
          case (state)
            IDLE:    if (start) state <= RUN;
            RUN:     if (beat == 3'd7) state <= DONE;
            DONE:    state <= IDLE;
            default: state <= IDLE;
          endcase
      always @(posedge clk or negedge rst_n)
        if (!rst_n)            beat <= '0;
        else if (state == RUN) beat <= beat + 3'd1;
      assign busy = (state == RUN);
      assign done = (state == DONE);
    endmodule
    
    Terminal
    $ iverilog -g2012 -o pu.vvp powerup.sv && vvp -n pu.vvp
    power-up state 0: as written, done after 8 cycles; fixed, after 8
    power-up state 1: as written, done after 6 cycles; fixed, after 8
    power-up state 2: as written, done after 8 cycles; fixed, after 8
    power-up state 3: as written, done after -1 cycles; fixed, after 8
    PASS: as written, stuck from state 3 and early from state 1; fixed, 8 cycles from every state
    ...
    $ verilator --binary --timing --assert -Wno-fatal --top-module powerup_tb -Mdir vlp powerup.sv > vlp.log 2>&1 && vlp/Vpowerup_tb
    power-up state 0: as written, done after 8 cycles; fixed, after 8
    ...
    power-up state 3: as written, done after -1 cycles; fixed, after 8
    PASS: as written, stuck from state 3 and early from state 1; fixed, 8 cycles from every state
    ...

    Same lines in both simulators. Once the power-up value is fixed by the bench, the tools agree completely, which is the strongest sign the original difference was never theirs. The bug is the missing reset, made worse by a default that holds an illegal state instead of leaving it.

    Seeing the bug you expect: two simulators, two bugs

    The second form of bias is quieter. dbglab's random test on seed 16 fails in both simulators, differently:

    Terminal
    $ tb/run.sh random 16
    dbglab: test=random seed=16
    FAIL test=random seed=16 cycle=686: SCOREBOARD: RAM word 759 (address 00001bdc) read xxxxxxxx, expected 00000000 (last written by DMA run #3)
    $ tb/run.sh random 16 verilator
    dbglab: test=random seed=16
    FAIL test=random seed=16 cycle=12811: STREAM: word #203 = 5484683d, expected 3caa023d

    An engineer who knows the FILL bug and knows that Verilator hides X writes the obvious story: same bug; two-state Verilator turns the X into a value, so the damage shows up later, on the stream. It fits. What would you see if it were wrong? The FILL fix would not change the Verilator failure. So run that:

    Copy of HEAD

    Icarus

    two-state Verilator

    unchanged

    FAIL at 686, X

    FAIL at 12811, stream word #203

    FILL in the reset block

    FAIL at 12811, stream word #203

    FAIL at 12811, stream word #203

    counter width CW = 10

    FAIL at 686, X

    PASS (20915 cycles)

    The Verilator failure ignores the FILL fix and vanishes with the counter fix. The two simulators were showing two different bugs: Icarus the FILL bug, which fires first, and two-state Verilator, where FILL starts at the right value, the counter bug further on. The expected story would have closed the counter failure as a duplicate. The run took a minute because the story was turned into a prediction that could fail.

    If it really is the tool

    Tools do have bugs, and Icarus 13 does reject or mis-handle some SystemVerilog constructs. When the flow above ends at "rule found", the report is short and complete:

    Field

    For the case_x_tb example (had it been a bug)

    Program

    the ten-line case_x_tb, nothing else

    Command and version

    iverilog -g2012, vvp -n, Icarus Verilog 13.0; the same for the second tool

    Expected, with the rule

    "default branch, because case items match by case equality and xx matches no item"

    Observed

    the exact output lines from both tools

    Workaround

    how to avoid the construct until it is fixed

    A report like that gets fixed. "My 40,000-line design behaves differently in your simulator" gets closed.

    Traps

    Common pitfall

    "The other simulator passes." A pass in a two-state simulator is not evidence against an X failure, and a pass in one event ordering is not evidence against a race. Ask what model of the hardware each tool used before counting its vote.

    Common pitfall

    Dropping the tool that disagrees. Removing Icarus from the regression would have removed the only run that showed the hang. A disagreement between tools is information; keep both until you understand it.

    Common pitfall

    A story that explains every result. "Verilator just shows it later" fit seed 16 perfectly. Any explanation that fits easily deserves the one run that could break it.

    Why this matters

    Senior lens. Keep two simulators in the regression on purpose, one four-state and one two-state with random start values, and treat every disagreement as a bug report against the design until shown otherwise. In dbglab the disagreements found an X bug and a hidden second bug; neither tool alone found both.

    When someone senior says "tool bug", ask for the ten-line program. It is not a challenge to their experience; it is the only form in which the claim can be checked, by them or by the tool's maintainers.

    Interview

    Asked: "Your test passes in one simulator and fails in another. Is it a tool bug?"

    Weak answer: Probably one of the simulators has a bug. I would use the one that passes, since it is likely the more mature one, and report the issue.

    Strong answer: Usually not. First I look at what differs: X handling, start values, or the order of same-time events. A two-state simulator hides X; a race can resolve either way. I shrink the design until the difference fits in a few lines, then check the language rule. I run the passing simulator with random start values or a different ordering to see whether the design fails there too. Only if a minimal program breaks a named rule do I call it a tool bug, and then I report the program, both outputs, the versions and the rule.

    Follow-ups: (1) Which constructs make two simulators legitimately disagree? (2) How would you show it is a race and not X?

    Check yourself The designer adds default: state <= IDLE; but no reset. Does Icarus still time out? Is the design now safe?

    In Icarus, state is still X at the first edge, so the case takes the default and loads IDLE: the FSM recovers and the test passes (done after 8 cycles, run for real). In silicon, a power-up in state 3 also recovers. But a power-up in RUN still runs a burst nobody asked for: with random start values, 4 of 20 power-ups finish after 6 cycles instead of 8. So the reset is still needed, and passing in Icarus is not proof of safety either: check every power-up state, as powerup_tb does.

    Check yourself A colleague says seed 16's Verilator failure "must be the FILL bug, since FILL is the known bug for this test". Which single run would you ask for, and what result would prove them wrong?

    Rerun two-state Verilator on a copy with only the FILL fix. If they are right, the failure at 12811 disappears. It does not: it fails identically. That one result rules out their story, and the counter fix then shows the real cause.

    Remember

    • Ask what you would see if you were wrong, then go and look for exactly that. Results that fit your idea are cheap; one that could break it is worth ten.

    • Two simulators that disagree are usually both right: a four-state one shows the X, a two-state one picks a value. Random start values show which the chip resembles.

    • Call it a tool bug only with a program of a few lines, the rule it breaks, and the output of two tools and their versions.

    Rule of thumb: before you blame the tool, shrink the design until the difference fits on one screen and name the rule the tool breaks.

    How this was checked

    Icarus Verilog 13.0 (iverilog -g2012, vvp -n) and Verilator 5.048 (verilator --binary --timing -Wno-fatal --top-module <top>; with --x-assign unique --x-initial unique, run with +verilator+rand+reset+2 +verilator+seed+N), 2026-10-17. naive.sv: Icarus TIMEOUT with state=xx; Verilator PASS in 8 cycles; random start values for N = 1 to 20: 4 TIMEOUT (state=11), 4 done after 6 cycles, 12 after 8. case_x_tb and powerup_tb as shown, in both simulators (powerup_tb also with --assert). With default: state <= IDLE; and still no reset: Icarus PASS in 8 cycles; random start values N = 1 to 20: 16 after 8 cycles, 4 after 6.

    dbglab at HEAD 49c5f04: tb/run.sh random 16 (Icarus) fails at cycle 686 with xxxxxxxx; tb/run.sh random 16 verilator fails at cycle 12811 on stream word #203. On copies of HEAD: with FILL in the reset block both simulators fail at 12811; with CW = 10, Icarus fails at 686 and Verilator passes in 20915 cycles.

    Limits: the claim about the language comes from the case statement's matching rule (case equality: X and Z bits must match exactly); this lesson does not show a real tool bug, only how one would be reported. Random start values are one model of power-up, not all of them.

    Keep going

    • Practice: FSM incomplete case and simulation vs synthesis mismatches

    • Practice: Safe state machine with illegal state recovery

    • Interview: run a mock interview on this topic

    Related topics

    • Part 4 · Hypothesise and Test

    • X in a waveform: optimism, pessimism and two-state simulators

    • Bisection across configurations: the simulator matrix

    • RTL: FSM bug stories

    • Next: one change at a time

    Previous

    37 / 107

    Next: One Change at a Time

    Up next

    Up nextOne Change at a Time

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • A Hypothesis Must Predict
    • The Experiment That Splits Suspects
    • The Debug Log