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 calls | Comments |
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
elseblock 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. Anifwith noelsewhose 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 == MAXoraddr == 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