Statement coverage is the most fundamental structural coverage metric. It measures the percentage of executable RTL statements that were reached at least once during simulation. Every simulator that supports code coverage collects statement coverage, and it is typically the first number a verification engineer checks after a regression run — because uncovered statements represent code that was never executed at all, regardless of whether its output was correct.

What You'll Learn

  • What counts as a coverable statement in RTL and what does not
  • How statement coverage is measured and what the report shows
  • How statement coverage relates to block and branch coverage
  • How to identify, interpret, and close statement coverage holes

What Is a Coverable Statement?

Not every line in an RTL file is a coverable statement. Statement coverage only tracks executable statements — lines that the simulator actually processes during simulation. Declarations, comments, and structural keywords are not executable and are excluded from the coverage count.

Coverable (executable)Not coverable
Blocking assignments (a = b + c;)Signal declarations (logic [7:0] data;)
Non-blocking assignments (q <= d;)Port declarations (input clk)
Continuous assignments (assign out = a & b;)Module headers and endmodule
Task and function callsComments
Condition expressions (if, case)parameter and localparam definitions
Loop iterations (for, while)typedef, enum definitions

The exact set of coverable constructs varies slightly between simulators. Always consult your tool's coverage documentation to understand how it counts statements, particularly for generate blocks and SystemVerilog-specific constructs.

How Statement Coverage Is Measured

At compile time, the simulator instruments each coverable statement with a hit counter. During simulation, each time execution reaches a statement, its counter increments. After the regression completes and all per-test databases are merged, the coverage report shows which statements were hit and which were not.

Consider a simple synchronous counter with reset:


module counter (
    input  logic       clk,
    input  logic       rst_n,   // Active-low synchronous reset
    input  logic       enable,
    output logic [7:0] count
);

    always_ff @(posedge clk) begin
        if (!rst_n)             // Statement 1: condition evaluated every clock
            count <= 8'h00;     // Statement 2: reset path
        else if (enable)        // Statement 3: condition evaluated when not in reset
            count <= count + 1; // Statement 4: increment path
        else
            count <= count;     // Statement 5: hold path (enable=0, not in reset)
    end

endmodule

A testbench that asserts reset once and then always drives enable=1 would produce this coverage picture:


Statement 1 (if !rst_n):        COVERED   -- evaluated every clock cycle
Statement 2 (count <= 8'h00):   COVERED   -- reset was asserted once
Statement 3 (else if enable):   COVERED   -- evaluated after reset de-asserted
Statement 4 (count <= count+1): COVERED   -- enable=1 drives this every cycle
Statement 5 (count <= count):   NOT HIT   -- enable was never 0 after reset

Statement Coverage: 80% (4/5 statements)

The hole on Statement 5 immediately identifies a missing test scenario: drive the counter out of reset with enable=0 to verify the hold behavior.

Quick Check

Q: In the example above, if the testbench never de-asserts rst_n, which statements remain uncovered?

Show Answer

Statements 3, 4, and 5 remain uncovered. Statement 1 is covered (the condition is evaluated every clock). Statement 2 is covered (reset keeps firing). But the else if and its two downstream paths (statements 3–5) are never reached because rst_n is always asserted.

Statement Coverage vs Block Coverage

Block coverage treats an entire basic block as a single unit — if the block was entered, every statement inside it is considered covered. Statement coverage is more granular: it tracks each line individually, which means it can detect partial execution within a block.

In practice, this difference surfaces when a block contains multiple statements and only some of them are reachable. A common scenario in synthesizable RTL is a function with an early return — the statements after the return are inside the same block from the tool's perspective, but can never execute once the return fires:


// Synthesizable priority encoder function
function automatic logic [2:0] encode_priority (input logic [7:0] req);
    if (req[7]) return 3'd7;     // Statement A — highest priority, returns early
    if (req[6]) return 3'd6;     // Statement B
    if (req[5]) return 3'd5;     // Statement C
    if (req[4]) return 3'd4;     // Statement D
    if (req[3]) return 3'd3;     // Statement E
    if (req[2]) return 3'd2;     // Statement F
    if (req[1]) return 3'd1;     // Statement G
    return 3'd0;                 // Statement H — only reached if no bits set
endfunction

A testbench that only ever drives req[7]=1 will always hit Statement A and return immediately. Block coverage may report the function body as covered (it was entered). Statement coverage correctly flags Statements B through H as never executed — because the early return on Statement A always fires first, preventing any lower-priority path from being tested.

The same pattern appears in combinational always_comb blocks with cascaded priority conditions where the highest-priority arm always wins in the available test scenarios, starving the lower-priority statements of any execution.

Use block coverage for a fast high-level view of which code regions were reached, and statement coverage to pinpoint exactly which lines within those regions were missed.

Statement Coverage vs Branch Coverage

Statement coverage and branch coverage are frequently confused because they both flag missing else paths. The distinction is in scope:

  • Statement coverage only flags a hole when a statement was never executed. If an else block exists and was never entered, every statement inside it shows as uncovered.
  • Branch coverage flags a hole at the decision point itself, even when there is no explicit else. An if with no else whose condition was always true has no uncovered statements — but branch coverage still reports the false direction as a hole.

// No else branch
always_ff @(posedge clk) begin
    if (error_flag)
        status <= ERROR;    // Statement A
end

If error_flag is never asserted: statement coverage reports Statement A as uncovered. Branch coverage additionally reports the false path of the if (error_flag=0) as a missing branch — even though there is no statement associated with it. Branch coverage is therefore the more complete metric for decision-point testing, while statement coverage is the more direct indicator of dead code.

Quick Check

Q: An if with no else has its condition always evaluate to false. How many statement coverage holes does this create, and how many branch coverage holes?

Show Answer

Statement coverage: one hole for each statement inside the if body (they were never executed). Branch coverage: one hole for the true direction of the decision (the condition was never true). Statement coverage counts the unreachable lines; branch coverage counts the untaken decision direction.

Statement Coverage in Block-Level Signoff

In a typical block-level signoff checklist, statement coverage is used as a quick sanity check before reviewing finer-grained metrics. A statement coverage hole in an error-handling or power-management path often reveals that an entire class of stimulus — fault injection, low-power entry sequences, protocol violations — was completely absent from the regression suite. Closing these holes early prevents late-stage discovery of missing scenarios during chip-level integration.

Interpreting a Statement Coverage Report

A merged coverage report from a full regression typically presents statement coverage per module and per file. The key columns to look at are the hit count (how many times a statement executed), the line number, and the coverage status. A hit count of zero is the actionable finding.


-- Statement Coverage Report: alu.sv --

Line  Hit Count   Statement
  8   14392       result <= a + b;
  12  0           result <= a - b;       << NOT HIT
  16  0           result <= a & b;       << NOT HIT
  20  8201        result <= a | b;
  24  0           overflow <= 1'b1;      << NOT HIT

This report tells you that the subtraction, bitwise AND, and overflow paths were never exercised. Each zero-hit statement is a directed test opportunity: generate stimulus that drives the corresponding opcode or condition.

When reviewing a report, pay particular attention to:

  • Error and exception handling paths — these are the most commonly missed and the most important to test
  • Boundary conditions — statements guarded by conditions like count == MAX or addr == LAST_ADDR
  • Power-down and initialization sequences — often exercised only once and easy to miss in random stimulus
  • Rarely-asserted control signals — flush, stall, cancel, abort paths that require specific protocol sequences to trigger

Common Mistakes

Mistake #1: Mistaking 100% statement coverage for full verification

Statement coverage confirms that every line was reached, not that every line produced the correct output. A statement like result <= a + b; is covered the first time it executes, regardless of whether the operands or the result were valid. Always pair statement coverage with output checkers and assertions to verify correctness, not just execution.

Mistake #2: Ignoring low hit counts

A statement with a hit count of 1 across millions of simulation cycles is technically covered but may represent a fragile test. A reset path that fires once and an error-handling path that fires once are very different in terms of verification confidence. Review low-hit-count statements alongside zero-hit statements — they may indicate scenarios that are under-stimulated rather than fully tested.

Mistake #3: Excluding statements in shared utility logic

Common RTL utilities — arbiter modules, FIFO controllers, clock-domain crossing synchronizers — are often instantiated in multiple places. Statement coverage holes in these modules can be incorrectly excluded as "low priority" because they are not the primary DUT. These modules are used across the chip and deserve their own targeted coverage closure.

Summary

In this tutorial, you learned:

  • Statement coverage tracks each executable RTL statement individually; declarations, comments, and structural keywords are not coverable
  • The simulator instruments statements at compile time and reports which were hit and how many times after the merged regression
  • Statement coverage is more granular than block coverage (per line vs per block) but less complete than branch coverage (no coverage for implicit false paths with no else)
  • Zero-hit statements in error-handling, boundary, and control paths are the highest-priority holes to close and should drive targeted directed test development
Did you find what you were looking for?

Quiz

No quiz questions available for this article.

Cron Job Starts