Expression coverage is a code coverage metric that measures whether each operand in a Boolean expression has independently influenced the outcome of that expression at least once during simulation. It goes deeper than branch or condition coverage by requiring proof that every sub-expression actually mattered to the result — not just that it was seen in both states, but that flipping it alone was enough to change the output. This makes expression coverage particularly effective at catching masked logic, where one operand dominates the expression and silently hides bugs in others.
What You'll Learn
- What expression coverage measures and how it differs from condition coverage
- How operand masking works and why it hides bugs from simpler metrics
- How simulators define and track expression coverage bins
- How to interpret expression coverage holes and close them with targeted stimulus
The Core Concept: Operand Independence
Consider a simple two-input AND gate in RTL:
assign grant = req_valid && !fifo_full;
For grant to be fully verified by expression coverage, the tool needs to observe two specific test scenarios for each operand:
- A test where that operand is the deciding factor — it is 1 and the expression is 1, and changing it to 0 would make the expression 0 (all other operands held at 1)
- A test where that operand is 0 and thus forces the expression to 0 on its own
For the expression req_valid && !fifo_full, the tool generates these coverage bins:
| Bin | req_valid | !fifo_full | Result | Purpose |
|---|---|---|---|---|
| A | 1 | 1 | 1 | Expression is true — baseline |
| B | 0 | 1 | 0 | req_valid independently causes false |
| C | 1 | 0 | 0 | !fifo_full independently causes false |
Bin A proves the expression can be true. Bin B proves req_valid matters — when it goes low (with FIFO not full), the grant is withdrawn. Bin C proves !fifo_full matters — when the FIFO fills (with a valid request present), the grant is also withdrawn. All three bins must be hit for 100% expression coverage on this expression.
Expression coverage does not require exhaustive enumeration of all input combinations. For a 3-operand AND expression, that would be 8 combinations. Instead, it requires only the minimal set of test pairs that prove each operand's independence — typically N+1 tests for an N-operand expression.
Operand Masking: What Expression Coverage Catches
The key scenario expression coverage is designed to detect is masking — when one operand dominates the expression and makes the other operands irrelevant. Simpler metrics like branch and condition coverage cannot detect this.
Consider a multi-condition enable gate:
always_ff @(posedge clk) begin
if (clk_en && pwr_ok && !scan_mode)
data_out <= data_in;
end
Suppose the testbench always drives clk_en = 0 in the scenarios where reset is de-asserted. The AND expression is always false, so the assignment never fires. Now consider what each metric reports:
- Branch coverage: reports a hole — the true branch was never taken. This is caught.
- Condition coverage: requires each operand to be seen as 0 and 1. If
clk_entoggles somewhere, condition coverage may pass even whilepwr_okandscan_modeare never the deciding factor. - Expression coverage: requires proof that
pwr_okalone can switch the outcome (bin:clk_en=1, pwr_ok=0, scan_mode=0) and that!scan_modealone can switch it (bin:clk_en=1, pwr_ok=1, scan_mode=1). If these bins are never hit, expression coverage flags the holes even if condition coverage passes.
The practical consequence: a bug in the pwr_ok qualification logic — for example, the signal is hardwired to 1 in the RTL by mistake — would be invisible to branch and condition coverage but would immediately surface as an expression coverage hole, because no test ever demonstrated that pwr_ok going low could block the grant.
Condition coverage passing does not imply expression coverage passing. A signal can toggle between 0 and 1 across different tests while never being the operand that independently decided the expression outcome. Always enable expression coverage alongside condition coverage.
Quick Check
Q: For the expression a && b && c, condition coverage requires each of a, b, c to be seen as 0 and 1. Does that guarantee expression coverage is also met?
Show Answer
No. Condition coverage only checks that each operand was observed in both states at some point. Expression coverage additionally requires that each operand was the deciding factor at least once — meaning a test where that operand alone was 0 while all others were 1, proving it can independently force the expression false. Condition coverage can pass while all three operands were masked by each other across different tests.
Expression Coverage for OR Expressions
The masking logic is symmetric for OR expressions, but the dominant value is 1 instead of 0. For an OR expression, an operand that is always 1 masks all other operands — the expression is always true regardless of what the others do.
assign interrupt = timeout_err || parity_err || overflow_err;
For this OR expression, the coverage bins are:
| Bin | timeout_err | parity_err | overflow_err | Result | Purpose |
|---|---|---|---|---|---|
| A | 0 | 0 | 0 | 0 | All clear — interrupt inactive |
| B | 1 | 0 | 0 | 1 | timeout_err alone triggers interrupt |
| C | 0 | 1 | 0 | 1 | parity_err alone triggers interrupt |
| D | 0 | 0 | 1 | 1 | overflow_err alone triggers interrupt |
Each error source must independently trigger the interrupt with all other sources clear. This is exactly the scenario most testbenches miss: they may inject multiple simultaneous errors, which hits Bin A and covers the OR being true — but never isolates individual error sources. A bug where parity_err is accidentally disconnected from the OR logic would go undetected unless Bin C is specifically hit.
Expression Coverage in Interrupt and Enable Logic
Clock enable trees, power-domain gating qualifications, and interrupt aggregation logic are the most common sources of expression coverage holes in real designs. These circuits combine multiple status signals with AND or OR logic, and testbenches rarely drive each source in isolation. Expression coverage bins directly identify which error sources or enable conditions were never independently exercised, turning a vague "we tested the interrupt logic" claim into a concrete, measurable verification statement.
How Simulators Report Expression Coverage
The simulator tracks expression coverage at the level of individual Boolean expressions in the RTL. Each expression gets its own set of bins in the coverage database. A typical report entry looks like this:
Expression: clk_en && pwr_ok && !scan_mode [line 42, module: clk_gate]
Bin clk_en pwr_ok scan_mode Result Status
--- ------ ------ --------- ------ ------
A 1 1 0 1 COVERED
B 0 1 0 0 COVERED
C 1 0 0 0 NOT HIT << pwr_ok never deciding
D 1 1 1 0 NOT HIT << scan_mode never deciding
Expression Coverage: 50% (2/4 bins covered)
Bins C and D are the actionable findings. They tell you precisely which operands were never the deciding factor and exactly what stimulus is needed to close the holes: a test with clk_en=1, pwr_ok=0, scan_mode=0 to hit Bin C, and a test with clk_en=1, pwr_ok=1, scan_mode=1 to hit Bin D.
Quick Check
Q: The expression coverage report shows Bin A and Bin B covered for a && b, but Bin C (a=1, b=0) is not hit. What does this tell you about the testbench?
Show Answer
The testbench has never driven a scenario where b is 0 while a is 1. This means b has never been the deciding factor that blocked the expression from being true. A bug in b's logic — wrong polarity, wrong source signal, stuck-at-1 — would be undetectable in the current test suite.
Expression Coverage for Continuous Assignments
Expression coverage applies to assign statements as well as procedural blocks. Any Boolean expression in the RTL — whether in an if condition, a ternary assignment, or a continuous assign — generates coverage bins.
// Ternary — expression coverage tracks both the condition and the select
assign out = (sel && valid) ? data_a : data_b;
// Multi-operand gate — each input must independently drive the output
assign bus_error = addr_err || data_err || prot_err;
// Nested — tool decomposes into sub-expressions
assign ready = (fifo_count > THRESHOLD) && !stall && (mode == ACTIVE);
For nested expressions, simulators typically decompose them into sub-expressions and generate bins for each. A complex condition like (fifo_count > THRESHOLD) && !stall && (mode == ACTIVE) will produce separate bin sets for the comparison sub-expression and each Boolean operand. Check your tool's documentation for how it handles operator precedence in decomposition.
Common Mistakes
Mistake #1: Assuming condition coverage implies expression coverage
These are distinct metrics. Condition coverage only checks that each operand was observed as 0 and 1 somewhere across the test suite. Expression coverage additionally verifies that each operand independently controlled the outcome. It is straightforward to have 100% condition coverage with significant expression coverage holes — particularly when operands toggle in tests where they are already masked by another dominant operand.
Mistake #2: Testing expressions with simultaneous multi-operand assertions
Driving multiple error signals simultaneously, or enabling multiple conditions at once, tends to cover Bin A (expression true) repeatedly while leaving individual-operand bins untouched. Expression coverage specifically requires each operand to be isolated. Targeted directed tests — one error source at a time, one enable condition at a time — are often more effective than random stimulus for closing expression coverage holes.
Mistake #3: Overlooking expression coverage on assign statements
Verification teams commonly focus expression coverage analysis on procedural if conditions and overlook continuous assignments. Clock enables, reset qualifications, interrupt aggregation, and bus error detection logic are often implemented as assign statements, and they carry the same masking risk. Review expression coverage holes across both procedural and continuous assignment contexts.
Summary
In this tutorial, you learned:
- Expression coverage requires each Boolean operand to independently influence the expression outcome — not just to be seen in both states, which is what condition coverage checks
- Masking occurs when a dominant operand (0 in AND, 1 in OR) makes all other operands irrelevant; expression coverage catches this where branch and condition coverage cannot
- The simulator generates one coverage bin per operand per expression, showing exactly which operands were never the deciding factor and what stimulus is needed to close each hole
- Interrupt aggregation, clock enable trees, and power-gating qualifications are the highest-risk sites for expression coverage holes in real RTL designs