Skip to content
VLSI Mentor

Linux & Bash · Part II · Module 29 · Log and Report Investigation

Warnings That Matter: X Propagation and Friends

Some logs warn about everything and some warn about nothing, and both leave you choosing which messages indicate a real design problem. This chapter measures the hardest case: a value that was never initialised, silently steering branches and poisoning arithmetic, with not one warning anywhere — plus the ternary behaviour that corrupts the log line reporting it.

Advanced22 min readLinuxPart IILogsX PropagationVerification

Part II · Module 29 · Chapter 29.6 · Warnings That Matter: X Propagation and Friends

Chapter 29.3 counted severities and noted that severity is set by whoever wrote the message, so it is a poor guide to importance. This chapter selects by class instead, and starts with the class that produces no message at all.

1. The Measurement: Zero Warnings

A testbench reading an eight-bit value that was never assigned:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_xprop;
  logic [7:0] uninit;          // never assigned -> x
  logic [7:0] sum;
  logic       cmp;
  logic       en;              // never assigned -> x
  initial begin
    $display("TB  uninit            = %b (%h)", uninit, uninit);
    if (uninit) $display("TB  if (uninit) took   = THEN");
    else        $display("TB  if (uninit) took   = ELSE");
    cmp = (uninit == 8'h00);
    $display("TB  (uninit == 0)      = %b", cmp);
    $display("TB  (uninit === 8'hxx) = %b", (uninit === 8'hxx));
    sum = uninit + 8'd1;
    $display("TB  uninit + 1         = %b", sum);
    if (en) $display("TB  write HAPPENED");
    else    $display("TB  write SKIPPED (en was %b)", en);
    $display("TB  TEST PASSED");
    $finish;
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ vvp logs/xprop.vvp
TB  uninit            = xxxxxxxx (xx)
TB  if (uninit) took   = ELSE
TB  (uninit == 0)      = x
TB  (uninit === 8'hxx) = 1
TB  uninit + 1         = xxxxxxxx
TB  write SKIPPED (en was x)
TB  TEST PASSED

$ vvp logs/xprop.vvp 2>&1 | grep -ciE 'warn|x-prop|uninitial'
0

Not one warning. An uninitialised value silently chose a branch, silently poisoned an addition, silently suppressed a write, and the run reported TEST PASSED.

2. What an X Actually Does

Four behaviours, each with a different consequence for debugging.

In a condition, an X is false. if (uninit) took the ELSE branch. Not an error, not a coin flip — the else, deterministically. Which means an X in an enable produces the behaviour of a disabled design, and a testbench that was checking for the disabled behaviour passes.

In a comparison, an X produces an X. (uninit == 8'h00) is x, not 0. And since an X in a condition is false, if (uninit == 8'h00) also takes the else — so both x == 0 and x != 0 behave as false, which is a situation with no counterpart in ordinary programming.

The reliable comparisons are the four-state ones. === and !== compare x and z as values rather than propagating them:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TB  (uninit === 8'hxx) = 1
TB  (uninit !== 8'h00) = 1

This is why every check in the fixtures throughout Part II uses !== rather than !=. With !=, a comparison against an X-valued output yields x, the if takes the else, and the check silently passes.

In arithmetic, an X poisons the whole result. uninit + 8'd1 is xxxxxxxx — not x in one bit, but every bit, because the carry chain spreads it. A single uninitialised bit anywhere in an address calculation produces an entirely unknown address.

And one case that is not total:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TB  partial            = 1010xx01, == 8'haa is 0, nonzero test takes THEN

A partially-X vector compares as 0 against a specific value — because two known bits already differ, which is enough to decide — and is true in a condition, because a known 1 bit is present. So partial X values behave inconsistently with fully-X ones, and the inconsistency is correct.

3. The Ternary That Corrupted My Own Log

While writing §1 I printed a branch result with a ternary, and the log came out like this:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TB  (uninit == 0)      = x   and if() of it takes DHAD

DHAD. The expression was cmp ? "THEN" : "ELSE" with cmp equal to x, and the output is neither operand.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ python3 - <<'EOF'
a,b="THEN","ELSE"
for x,y in zip(a,b):
    print("   %s/%s  0x%02x & 0x%02x = 0x%02x  '%s'"%(x,y,ord(x),ord(y),ord(x)&ord(y),chr(ord(x)&ord(y))))
EOF
   T/E  0x54 & 0x45 = 0x44  'D'
   H/L  0x48 & 0x4c = 0x48  'H'
   E/S  0x45 & 0x53 = 0x41  'A'
   N/E  0x4e & 0x45 = 0x44  'D'

DHAD is the bitwise AND of "THEN" and "ELSE" — which is what you get when the agreeing bits pass through and the disagreeing bits become x, rendered as 0 by %s.

Confirmed minimally:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic c;                       // x
r1 = c ? 8'hFF : 8'h00;        // no common bits
r2 = c ? 8'hF0 : 8'h30;        // common: 0x30
r3 = c ? 8'hAA : 8'hAA;        // identical operands
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  x ? FF : 00 = xxxxxxxx
  x ? F0 : 30 = xx110000   (common bits 0011_0000 survive)
  x ? AA : AA = 10101010   (identical operands: no X at all)

4. Making an X Visible

Since the tool will not report it, the design or the testbench must. Three techniques in increasing cost.

Check with !== and print the value. The cheapest, and it is the one the fixtures use:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (dout !== exp) begin
  errors = errors + 1;
  $display("TB  ERROR @%0t dout=%h expected %h", $time, dout, exp);
end

An X-valued dout prints as xx, which makes the log line self-explaining: dout=xx expected a5 is immediately a different bug from dout=00 expected a5. That distinction is free and it is the single highest-value habit in this chapter.

Assert that control signals are known. For the signals whose X-ness is invisible per §3:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// an X here produces correct-looking output, so check it explicitly
always @(posedge clk) begin
  if (rst_n === 1'b1 && en === 1'bx)
    $display("TB  ERROR @%0t en is X -- the write decision is unknown", $time);
end

The rst_n === 1'b1 guard matters: during reset an X is expected everywhere, and an unguarded check produces a warning per cycle until reset releases, which is how a useful check gets switched off.

Then grep for the class, not the severity. With the checks in place the log becomes searchable:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# X-related failures across a regression, as a class
grep -h -E '=(x+|X+)( |$)|is X|has X' regress/*/run.log | sed 's/@[0-9][0-9]*/@T/g' \
  | sort | uniq -c | sort -rn

Note =(x+|X+)( |$) rather than a bare x: a pattern of just x matches the word "expected", the hex digit x in 0x, and most English. Selecting an X value needs the same positional discipline as Chapter 29.4's normalisation — anchor it to the = and to a boundary.

5. Selecting Among the Warnings a Tool Does Emit

The other half of the chapter's title. When a tool is verbose, the problem inverts: hundreds of warnings, a handful of which matter. The useful move is to classify by id and then decide once per class, rather than deciding per line.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
# warnclass LOG... — group warnings by class and count them, so a decision can
# be made per class rather than per line.
set -u
[ $# -gt 0 ] || { echo "usage: warnclass LOG..." >&2; exit 2; }

for f in "$@"; do
  [ -r "$f" ] || { echo "warnclass: cannot read $f" >&2; continue; }
  # Chapter 29.3's rule, applied to this tool: cut the summary region off
  # first, or the line "UVM_WARNING : 8" becomes a warning class of its own.
  end=$(grep -n -e 'Report Summary' "$f" 2>/dev/null | head -1 | cut -d: -f1)
  if [ -n "${end:-}" ]; then head -n $((end - 1)) "$f"; else cat "$f"; fi
done \
  | grep -h -E '^UVM_WARNING |\*\* Warning|[Ww]arning:' 2>/dev/null \
  | sed 's/@[0-9][0-9]*/@T/g; s/([0-9][0-9]*)/(L)/g; s/=[0-9a-fA-F][0-9a-fA-F]*/=V/g' \
  | sed 's/.*\(\[[A-Z_][A-Z_0-9]*\]\)/\1/' \
  | sed 's/\([^0-9]\)[0-9][0-9]*\([^0-9]\)/\1N\2/g; s/  */ /g' \
  | sort | uniq -c | sort -rn
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ warnclass axi_burst_test.log
   8 [SB_LATE] response arrived N cycles late

Eight warnings, one class. That is the number a person can act on: the question is no longer "are these eight warnings important" but "is SB_LATE important", asked once.

And it cross-checks against Chapter 29.3's two other measurements:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ end=$(grep -n 'Report Summary' axi_burst_test.log | head -1 | cut -d: -f1)
$ head -n $((end-1)) axi_burst_test.log | grep -cE '^UVM_WARNING '
8
$ sed -n 's/^UVM_WARNING *: *\([0-9]*\).*/\1/p' axi_burst_test.log | tail -1
8
$ warnclass axi_burst_test.log | awk '{s+=$1} END{print s}'
8

Three independent counts agreeing is the point of having three.

Three rules for that decision, each of which is about the class and not the instance.

A warning that fires on every run is either real and accepted, or noise. There is no third option, and whichever it is should be written down — because an unexplained constant warning trains everybody to ignore the warning region entirely.

A warning whose count changes is interesting even if the class is known noise. Eight SB_LATE every night and then eighty is a signal regardless of what SB_LATE means. That is why the count is kept rather than just the class list.

A warning that appears only on failing runs is a candidate cause. Comparing warning classes between the passing and failing sets is a two-command investigation:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# classes seen only in runs that failed
comm -13 <(warnclass pass_*/run.log | sed 's/^ *[0-9]* //' | sort -u) \
         <(warnclass fail_*/run.log | sed 's/^ *[0-9]* //' | sort -u)

comm -13 prints lines unique to the second file, which is the set of warning classes the failures have and the passes do not. On this fixture that set is empty, and an empty answer is still worth having: it says the warnings are not what distinguishes the failures, so look elsewhere.

6. The Boundary of What a Log Can Tell You

Module 29 has been building toward this, and X propagation is where it is clearest.

A log can tell you that something complained — Chapter 29.3's counts. It can tell you what kind and how often — Chapter 29.4's signatures. It can tell you whether the run finished — Chapter 29.5's states.

It cannot tell you that nothing complained because nothing looked. The measurements in this module that demonstrate it:

  • noreset_s1 passed with a design that has no reset (Chapter 29.4).
  • An uninitialised value steered branches and poisoned arithmetic with zero warnings (§1).
  • An X in a control path produced clean output when both operands agreed (§3).

All three are invisible to any amount of log processing, and all three are caught by the same thing: deliberately breaking the design and checking that the suite notices. The fixture's noreset_s1 exists because that mutation was not noticed — it is a recorded failure of the test suite, kept as a specimen.

That is not a log-analysis technique and it is the honest end of a log-analysis chapter. The log is evidence about what the checks saw. It is silent about what they did not.

Practice

  1. Read an uninitialised logic vector and print it. Count the warnings the simulator issued.
  2. Test if (uninit) and record which branch runs.
  3. Evaluate (uninit == 8'h00) and then use the result in an if. Explain why both the equality and the inequality behave as false.
  4. Compare with === and !== and confirm they give definite answers.
  5. Add 1 to an X-valued vector and count how many bits became unknown.
  6. Make a partially-X vector and test it both against a value and for nonzero. Explain the two different answers.
  7. Print a ternary whose condition is X with two different string operands, and compute the bitwise AND of those strings.
  8. Do it with identical operands and confirm no X appears.
  9. Explain why that is the dangerous case, in terms of when it would first be noticed.
  10. Change a check from != to !== on a design that outputs X, and see whether the check starts failing.
  11. Add a control-signal X assertion with a reset guard, then remove the guard and count the messages during reset.
  12. Write a pattern that matches an X value and confirm a bare x matches the word "expected".
  13. Write warnclass and reduce a log's warnings to classes with counts.
  14. Compare warning classes between passing and failing runs with comm -13, and decide what an empty result tells you.

Check Your Understanding

  1. How many warnings did the simulator issue about the uninitialised value?
  2. Why is that not an oversight in the tool?
  3. Which of Module 29's machinery applies to X propagation, and why?
  4. Which branch does if (x) take?
  5. Why do both x == 0 and x != 0 behave as false in a condition?
  6. Which operators give a definite answer, and what does using != instead cost?
  7. Why did uninit + 1 become entirely unknown rather than unknown in one bit?
  8. What did x ? "THEN" : "ELSE" print, and what operation does it correspond to?
  9. What does x ? 8'hAA : 8'hAA produce, and why is that the dangerous case?
  10. When would a design with an X in a select line first misbehave?
  11. Why does printing the value in an error message matter so much?
  12. Why must a control-signal X assertion be guarded by reset?
  13. Why can a bare x not be used as a search pattern?
  14. What does comm -13 give you between passing and failing warning classes, and what does an empty result mean?
  15. State the boundary of what a log can tell you, and the one technique that crosses it.

7. Remember

  • The simulator issued zero warnings while an uninitialised value steered a branch, poisoned an addition and suppressed a write — and the run reported TEST PASSED.
  • There is nothing to grep for, so none of this module's counting machinery applies. The work is producing the messages, not reading them.
  • if (x) takes the else, deterministically — so an X in an enable looks like a disabled design.
  • Both x == 0 and x != 0 are false in a condition. Use === and !==; != against an X output makes the check silently pass.
  • Arithmetic spreads an X across every bit via the carry chain.
  • A ternary with an X condition merges both operands bit by bit — x ? "THEN" : "ELSE" printed DHAD, their bitwise AND.
  • Identical operands hide the X completely, which is why an X in a control path can be silent for months.
  • Print the value in every error message. dout=xx is a different bug from dout=00, for free.
  • Guard a control-signal X check with reset, or it fires every cycle and gets disabled.
  • Anchor an X search pattern; a bare x matches "expected".
  • Group warnings by class, decide once per class, and watch the counts change.
  • A log is evidence about what the checks saw, and silent about what they did not. Only breaking the design on purpose crosses that line.

8. What Is Next

One log at a time has been read as far as it can be. Chapter 29.7 follows a single problem across the three logs that mention it — where the root cause appears in exactly one of them and the other two are louder.

Where this fits

Part of the Linux & Bash curriculum.