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. CI for Formal
On this page

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

    CI for Formal

    A per-commit set that finishes in minutes, a nightly set that proves everything, and a gate that fails builds honestly.

    Why it matters

    A team ran full proofs per commit, each taking hours, and within a month engineers stopped waiting and stopped committing cleanly. Another team ran nothing per commit and found every breakage at release. Both failed the same way: the wrong set at the wrong cadence. The per-commit set must finish in minutes; the nightly set may take hours; both must report the same three outcomes.

    Job of this lesson: split the two sets, describe the job that runs them, and build the gate between its ledger and the commit.

    The core idea

    The per-commit set holds small bounded checks and every required cover: it answers whether the obvious still holds, fast. The nightly set holds the full proofs to their planned depths: it answers whether everything still holds, thoroughly. Both are one CI job, described in generic terms by four things: the tasks it runs, how they run in parallel, where the ledger lands, and what fails the build. A timeout is inconclusive, never a pass, and a newly inconclusive proof blocks the build like a failure until it is triaged:

    Job

    Per-commit

    Nightly

    Release

    Tasks

    Bounded checks to the planned small depth, every required cover

    Full proofs to the planned depth, mutation spot-checks

    Every proof rerun on the final RTL

    Parallelism

    One task per block; the job ends when the slowest task ends, in minutes (illustrative)

    One task per property group, so one slow property cannot starve the rest; hours (illustrative)

    As needed

    Ledger

    One row per property and cover: outcome, depth, RTL revision hash

    The same rows, kept per night for the diff

    Pinned with the release hash

    Fails the build when

    Any counterexample, any required cover not reached, any row from another revision

    Any counterexample, any previously proven property now inconclusive

    Anything but proven with current evidence

    The job runs the task set and writes results to a database. The gate reads the ledger and either blocks the build or lets it pass, with no silent third option.
    The gate pipeline: the job runs tasks, results land in a database, and the gate blocks or passes the build.

    Worked example: the gate

    The script below implements the gate exactly as specified. A property row ends proven, counterexample or inconclusive. A cover row ends reached, or inconclusive when it was not reached within its depth; it is never called unreachable without an engine proof of that. The gate fails on any counterexample, any required cover not reached, any proof that was proven before and is inconclusive now, any row that ran on another RTL revision, and any status outside the allowed ones. The clean ledger goes through; the failing ledger, with one counterexample, one cover not reached and one newly inconclusive proof, is rejected on all three counts, and each of the other rules is tested on its own:

    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
    43
    44
    45
    OUTCOMES = ("proven", "counterexample", "inconclusive")
    COVER_OUTCOMES = ("reached", "inconclusive")
    
    def gate(run, previous, revision):
        prev = {p["name"]: p["status"] for p in previous}
        failures = []
        for p in run:
            allowed = COVER_OUTCOMES if p["kind"] == "cover" else OUTCOMES
            if p["status"] not in allowed:
                failures.append(p["name"] + ": unknown status " + p["status"])
            elif p["status"] == "counterexample":
                failures.append(p["name"] + ": counterexample")
            elif p["kind"] == "cover" and p["status"] != "reached":
                failures.append(p["name"] + ": required cover not reached within its depth")
            elif p["status"] == "inconclusive" and prev.get(p["name"]) == "proven":
                failures.append(p["name"] + ": newly inconclusive")
            if p["rtl"] != revision:
                failures.append(p["name"] + ": row from another revision")
        return failures
    
    def row(name, kind, status, rtl="r7"):
        return {"name": name, "kind": kind, "status": status, "rtl": rtl}
    
    previous = [row("a_conserve", "proof", "proven", "r6"), row("c_stall", "cover", "reached", "r6"),
                row("a_data", "proof", "proven", "r6")]
    good = [row("a_conserve", "proof", "proven"), row("c_stall", "cover", "reached"), row("a_data", "proof", "proven")]
    assert gate(good, previous, "r7") == []
    
    bad = [row("a_conserve", "proof", "counterexample"), row("c_stall", "cover", "inconclusive"),
           row("a_data", "proof", "inconclusive")]
    caught = gate(bad, previous, "r7")
    assert len(caught) == 3
    assert any("newly inconclusive" in f for f in caught)
    assert any("cover not reached" in f for f in caught)
    
    sneaky = [dict(r) for r in good]
    sneaky[2]["status"] = "timeout-pass"
    assert any("unknown status" in f for f in gate(sneaky, previous, "r7"))
    unreached = [dict(r) for r in good]
    unreached[1]["status"] = "unreached"
    assert any("unknown status" in f for f in gate(unreached, previous, "r7"))
    stale = [dict(r) for r in good]
    stale[0]["rtl"] = "r6"
    assert any("another revision" in f for f in gate(stale, previous, "r7"))
    print("PASS: clean ledger passes; counterexample, uncovered cover, drift, stale row and odd labels all fail")

    What the review looks for

    • The per-commit set is timed in minutes and the nightly set in hours across parallel tasks.

    • The gate fails on counterexamples, required covers not reached, newly inconclusive proofs and rows from another revision.

    • No status outside the allowed ones can pass the gate; unknown labels fail loudly.

    Verify and debug

    Test the gate quarterly with a seeded failing ledger: one counterexample, one cover not reached, one timeout. If any of the three passes the gate, the reporting chain is broken upstream of the gate and no result downstream can be trusted. Keep the previous run used for comparison pinned to a revision hash; comparing against a floating baseline hides drift.

    A stale proof cited at signoff: the report references a run on an older RTL revision and nobody reruns it. Symptom is a signoff whose ledger revision hash does not match the release hash. Cause is tying results to property names instead of to revision hashes. Fix: every ledger row carries the source hash and the gate reruns on every RTL change. Found by comparing the ledger hash against the release hash.

    “How do you put formal in CI without slowing the team?” A per-commit set of small bounded proofs and covers in minutes that blocks on any counterexample or cover not reached, plus a nightly set of full proofs whose ledger blocks release on any new failure or new inconclusiveness. Timeouts are inconclusive, and every row carries its RTL hash so staleness is visible.

    Check yourself The nightly set newly reports a proof inconclusive that was proven yesterday. Block or note?

    Block until triaged: a newly inconclusive proof is a regression signal, and the next lesson says how to tell drift from a real bug.

    Check yourself Why must the gate reject unknown statuses instead of ignoring them?

    Because ignoring them repeats the oldest failure in this course: a timeout counted as a pass. Anything the gate does not understand fails loudly.

    Related topics

    • Next: Result Triage and Trends

    • VFP: Drills

    • VFP: Sign-Off

    Previous

    142 / 183

    Next: Result Triage and Trends

    Up next

    Up nextResult Triage and Trends

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • From the Open Flow to Any Formal Tool