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

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • Reading Paths
  • Glossary
  • Metrics Reference
  • Formal Cheat Sheet
  • Formal Foundations19
  • Properties and SVA16
  • Property Development and Constraints19
  • Proof Engines and Convergence23
  • Formal Applications20
  • Equivalence Checking14
  • Formal Coverage and Signoff15
  • Methodology and Debug13
    • The Formal Testplan
    • Managing Complexity in a Project
    • Debugging Formal Results
    • Formal in Regression
      • CI for Formal
      • Result Triage and Trends
      • From the Open Flow to Any Formal Tool
  • Interview Prep15
  • Capstone: A Chip-Block Verification Lab24
Loading lesson

←Formal in Regression
  1. Topics
  2. Formal Verification
  3. Methodology and Debug
  4. Formal in Regression
  5. Result Triage and Trends
On this page

    Part 8 of 10, lesson 12 of 13Part 8 of 10/Methodology and Debug/Lesson 12 of 13All levels

    Result Triage and Trends

    A new failure after an RTL change: real bug, assumption drift, or setup change; and what rising inconclusiveness means.

    Why it matters

    The nightly ledger turns red on three properties the morning after an RTL change. One is a genuine bug the change introduced. One is an assumption the change silently invalidated. One ran on a machine whose memory limit changed overnight. Reverting the RTL fixes the first, breaks nothing for the second, and misses the third entirely. Triage tells the three apart before anyone acts.

    Job of this lesson: classify ledger changes and read the inconclusive trend.

    The core idea

    A new counterexample after an RTL change is a real bug until the triage order says otherwise: replay the old trace, check the assumptions against the new RTL, check the setup and the revision hashes. Assumption drift shows as a failure under an environment rule the change altered. It is accepted as drift only when the neighbour block's updated guarantee is recorded; a rule changed in the same commit as the RTL, with no such guarantee, is an unreviewed change and is treated as a bug, because that is how a bug hides as drift. A setup change shows as identical RTL and rules with different outcomes; the fix is the environment, not the design. A newly inconclusive property is a slow proof wearing a new revision's clothes: triage it as one. The trend of inconclusive counts over releases is the project's early warning:

    Signal

    Distinguishing test

    Action

    Real bug

    Fails on new RTL with old assumptions and old setup

    Fix the RTL, keep the trace

    Assumption drift

    The rule changed and the neighbour's updated guarantee is recorded

    Accept the reviewed assumption update

    Unreviewed assumption change

    The rule changed with the RTL and no guarantee is recorded

    Treat as a bug until the guarantee exists

    Setup change

    Identical RTL and rules, different setup and outcome

    Fix the environment, rerun

    Newly inconclusive

    No failure, but no proof either

    Triage as a slow proof

    Worked example: the ledger diff

    The script below diffs two nightly runs and classifies each change. The injected regression is a_hold: its RTL changed and its interface rule changed in the same commit, and it fails. A naive classifier calls that drift and moves on; this one calls it an unreviewed assumption change, and the test pins that down, along with the same row once the neighbour's guarantee is recorded. The setup-change row has identical RTL and rule, so only the environment differs. The trend function flags a rise over three consecutive releases:

    example.py
    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
    def classify(o, n):
        if n["status"] == "counterexample":
            if n["rtl"] == o["rtl"] and n["rule"] == o["rule"] and n["setup"] != o["setup"]:
                return "setup-change"
            if n["rule"] != o["rule"]:
                if n.get("rule_discharged"):
                    return "assumption-drift"
                return "unreviewed-assumption-change (treat as bug)"
            if n["rtl"] != o["rtl"]:
                return "candidate-bug"
            return "unexplained: rerun"
        if n["status"] == "inconclusive" and o["status"] == "proven":
            return "newly-inconclusive"
        return "unchanged"
    
    def entry(name, status, rtl, rule, setup, **extra):
        return dict({"name": name, "status": status, "rtl": rtl, "rule": rule, "setup": setup}, **extra)
    
    old = {n: entry(n, "proven", "r100", "v1", "s1") for n in ("a_conserve", "a_hold", "a_ready", "a_data", "a_flags", "a_reset")}
    new = [
        entry("a_conserve", "counterexample", "r101", "v1", "s1"),
        entry("a_hold", "counterexample", "r101", "v2", "s1"),
        entry("a_ready", "counterexample", "r101", "v2", "s1", rule_discharged=True),
        entry("a_data", "inconclusive", "r101", "v1", "s1"),
        entry("a_flags", "counterexample", "r100", "v1", "s2"),
        entry("a_reset", "proven", "r101", "v1", "s1"),
    ]
    result = {n["name"]: classify(old[n["name"]], n) for n in new}
    assert result["a_conserve"] == "candidate-bug"
    assert result["a_hold"].startswith("unreviewed-assumption-change")      # the bug hiding as drift
    assert result["a_ready"] == "assumption-drift"                          # same change, guarantee recorded
    assert result["a_data"] == "newly-inconclusive"
    assert result["a_flags"] == "setup-change"
    assert result["a_reset"] == "unchanged"
    
    def rising(trend, releases=3):
        last = trend[-releases:]
        return len(last) == releases and all(a < b for a, b in zip(last, last[1:]))
    
    assert rising([0, 0, 1, 1, 2, 3])
    assert not rising([3, 2, 1, 1, 0, 0]) and not rising([0, 1, 1, 2])
    print("PASS: bug, hidden bug, drift, setup change and new inconclusiveness each classified; trend flagged")

    What the review looks for

    • Every ledger change names its class and the test that decided it.

    • Assumption updates are reviewed with the neighbour's guarantee, never silent; setup changes rerun before any RTL action.

    • The inconclusive trend is plotted per release and has an owner when it rises.

    Verify and debug

    When a change classifies as drift, demand the neighbour's updated guarantee before accepting the new assumption; drift accepted on one side without the other side's proof is over-constraint waiting to happen. When the same commit changes RTL and rule, split the commit and rerun with the old rule first. When the trend rises for two releases running, schedule complexity work, not more machines: the proofs are losing to the design's growth.

    A waived counterexample with no proof: the waiver says the failure cannot happen, with no invariant and no reviewed constraint behind it. Symptom is a red row turned clean by prose. Cause is triage that stops at classification. Fix: a waiver needs a proven invariant or a documented constraint reviewed by the neighbour block owner, recorded next to the row. Found by the signoff review asking for the evidence behind every waiver.

    “The ledger turns red on three properties overnight. Walk me through it.” Check revision hashes first, then classify each change as bug, assumption drift, setup change or new inconclusiveness using the distinguishing tests. Never revert before classifying: a revert fixes the bug case and hides the other three. State the waiver rule: prose is not evidence.

    Check yourself The RTL hash is unchanged but a proven property now fails. First suspect?

    The setup or the assumptions, not the design: identical RTL with different outcomes is a setup change until the assumption diff says otherwise.

    Check yourself Inconclusive counts rise three releases in a row while the old proofs stay proven. What does it mean?

    The proofs are losing to growing complexity. Schedule reduction, splitting or strengthening work; the proven ones are next.

    Related topics

    • Earlier: CI for Formal

    • Next: From the Open Flow to Any Formal Tool

    • Hardware Debug: Equivalence Failures

    Previous

    143 / 183

    Next: From the Open Flow to Any Formal Tool

    Up next

    Up nextFrom the Open Flow to Any Formal Tool

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error