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
  • Interview Prep15
  • Capstone: A Chip-Block Verification Lab24
    • The Brief
    • Stage 1: FIFO and Handshake
    • Stage 2: Registers and Arbiter
    • Stage 3: Pipeline and Equivalence
    • Stage 4: Induction and Signoff
      • Stage 4 Induction on the Counter and FIFO Invariants
      • Stage 4 Signoff Ledger and Waivers
      • Stage 4 Final Manifest and Review
    • Solutions
    • Appendix: Run It Yourself on an Open-Source Flow
Loading lesson

←Stage 4: Induction and Signoff
  1. Topics
  2. Formal Verification
  3. Capstone: A Chip-Block Verification Lab
  4. Stage 4: Induction and Signoff
  5. Stage 4 Final Manifest and Review
On this page

    Part 10 of 10, lesson 14 of 24Part 10 of 10/Capstone: A Chip-Block Verification Lab/Lesson 14 of 24All levels

    Stage 4 Final Manifest and Review

    Run every ledger check as one manifest, show that each check can fail, and prepare the review answers before opening the solutions.

    Why it matters

    The review opens with one line on the screen: the manifest verdict. Everything the reviewer asks after that is a drill into a row: why this bound, who discharges that assumption, which property killed this fault, what is still bounded and why. A manifest that prints PASS for an empty ledger is worse than none, so this lesson makes the manifest earn its line: it must accept the clean ledger, and it must reject twenty-three defects injected one at a time.

    Inputs

    • The engine of the harness lesson and the twenty-three injectors, embedded in the block below.

    • The demonstration ledger of the toggle design from the harness lesson, used here so that no answer of the lab is revealed.

    • Your own ledger from stages 1 to 4, in the shape of YOUR_WORLD.

    • The four review questions below.

    Review question

    Your answer must cite

    Which results stay bounded, and where are their justifications?

    The bounded and inconclusive rows and the cover steps.

    Which abstractions were taken?

    The tracked-slot and tracked-word arguments and the history registers, each with its justification.

    Which assumptions need confirmation?

    The register, and the rows that discharge each entry.

    What reruns on the next change?

    The revision field and the freshness check.

    Deliverable

    • A manifest script that runs the ledger checks over your complete ledger and prints exactly one PASS line only when they all agree.

    • A demonstration, on the toggle ledger, that each of the twenty-three injected defects raises its complaint.

    • Written answers to the four review questions, each citing rows of your ledger.

    • A list of what the package does not claim: bounded results, abstraction risks, and behaviour outside the two-state model.

    Pass criterion

    • The manifest block below passes, and its injection count equals twenty-three.

    • Your own ledger passes the same manifest.

    • Your review answers match the reference answers of the Solutions topic in substance, not in wording.

    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
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    # ---- formal lab results ledger, engine v2: every row cites a typed run (kind, depth, outcome[, design]) ----
    STATUS = ("proven", "bounded", "cex", "cover-reached", "inconclusive")
    FROM_RUN = {"proven": {("prove", "proved")},
                "bounded": {("bmc", "bounded")},
                "cex": {("bmc", "counterexample"), ("prove", "counterexample")},
                "cover-reached": {("cover", "reached")},
                "inconclusive": {("prove", "unproven"), ("cover", "not-reached")}}
    NEED = {"sync_fifo": 9, "rr_arbiter": 5, "hs_bridge": 4, "csr_block": 3,
            "pipe3": 5, "pipe3_eq": 5, "mod10_counter": 15, "toggle_demo": 4}
    KEYS = ("property", "design", "status", "bound", "why", "run", "assumes", "cover", "kills", "rev")
    REV = 3
    ALWAYS = "reset-at-step-0"
    TRACKED = "tracked-constant-stable"
    def row_problems(e, runs, tracked):
        n = e.get("property", "?")
        miss = [k for k in KEYS if k not in e]
        if miss:
            return ["missing-field:%s:%s" % (n, ",".join(miss))]
        if e["status"] not in STATUS:
            return ["bad-status:" + n]
        p = []
        if e["rev"] != REV:
            p.append("stale:" + n)
        if e["run"] not in runs:
            return p + ["no-such-run:" + n]
        kind, depth, outcome = runs[e["run"]][:3]
        if len(runs[e["run"]]) > 3 and runs[e["run"]][3] != e["design"]:
            p.append("run-of-another-design:" + n)         # a run typed with its design may be cited only by that design's rows
        if (kind, outcome) not in FROM_RUN[e["status"]]:
            p.append("status-not-from-run:" + n)
        if e["bound"] != depth:
            p.append("bound-not-run-depth:" + n)
        has_cover = bool(str(e["cover"]).strip())
        if e["status"] in ("proven", "bounded") and not (has_cover and e["kills"]):
            p.append("no-evidence:" + n)
        if e["status"] == "cover-reached" and not has_cover:
            p.append("no-evidence:" + n)
        if e["status"] == "cex" and not e["kills"]:
            p.append("no-evidence:" + n)
        if e["status"] == "bounded":
            if e["design"] not in NEED:
                p.append("unknown-design:" + n)
            elif depth < NEED[e["design"]]:
                p.append("short-bound:" + n)
            if len(str(e["why"]).strip()) < 12:
                p.append("unjustified:" + n)
        if ALWAYS not in e["assumes"]:
            p.append("hidden-assume:%s:%s" % (n, ALWAYS))
        if n in tracked and TRACKED not in e["assumes"]:
            p.append("hidden-assume:%s:%s" % (n, TRACKED))
        return p
    def ledger_ok(ledger, runs, required, mutants, register, tracked, final=True):
        names = [e.get("property") for e in ledger]
        bad = ["duplicate:" + n for n in sorted({n for n in names if names.count(n) > 1})]
        bad += ["missing-row:" + r for r in sorted(set(required) - set(names))]
        bad += [q for e in ledger for q in row_problems(e, runs, tracked)]
        good = [e for e in ledger if not row_problems(e, runs, tracked)]      # only a row without complaints counts as evidence
        solid = {e["property"] for e in good if e["status"] in ("proven", "cover-reached")}
        for a, rows in sorted(register.items()):
            if rows is None:
                if final:
                    bad.append("deferred-assume:" + a)      # a stage may defer a discharge; a final package may not
            elif not (rows and all(r in solid for r in rows)):
                bad.append("open-assume:" + a)
        bad += ["unregistered-assume:%s:%s" % (e.get("property"), a) for e in ledger
                for a in e.get("assumes", []) if a not in register]
        killed = {m for e in good if e["status"] == "cex" for m in e["kills"]}
        bad += ["survivor:" + m for m in sorted(set(mutants) - killed)]
        bad += ["unsupported-kill:%s:%s" % (e.get("property"), m) for e in ledger
                if e.get("status") in ("proven", "bounded") for m in e.get("kills", []) if m not in killed]
        return bad
    def mutation_score(ledger, mutants):
        assert mutants, "no mutants registered"
        killed = {m for e in ledger if e.get("status") == "cex" for m in e.get("kills", [])}
        return len(killed & set(mutants)) / len(mutants)
    
    import copy
    def world_ok(w):
        return ledger_ok(w["ledger"], w["runs"], w["required"], w["mutants"], w["register"], w["tracked"])
    def pick(w, status):
        return next(e for e in w["ledger"] if e["status"] == status)
    def injections(w):
        out = []
        def add(name, expect, fn):
            v = copy.deepcopy(w)
            fn(v)
            out.append((name, expect, v))
        def short_bound(v):
            e = pick(v, "bounded")
            e["bound"] = NEED[e["design"]] - 1
            v["runs"][e["run"]] = ("bmc", e["bound"], "bounded")
        add("empty ledger", "missing-row:", lambda v: v["ledger"].clear())
        add("rev field deleted", "missing-field:", lambda v: pick(v, "proven").pop("rev"))
        add("stale revision", "stale:", lambda v: pick(v, "proven").update(rev=REV - 1))
        add("proven row without cover", "no-evidence:", lambda v: pick(v, "proven").update(cover=""))
        add("proven row without kills", "no-evidence:", lambda v: pick(v, "proven").update(kills=[]))
        add("bound differs from the run", "bound-not-run-depth:", lambda v: pick(v, "cex").update(bound=pick(v, "cex")["bound"] + 1))
        add("proven row cites a counterexample run", "status-not-from-run:",
            lambda v: pick(v, "proven").update(run=pick(v, "cex")["run"]))
        add("bounded row below the design minimum", "short-bound:", short_bound)
        add("bounded row without a reason", "unjustified:", lambda v: pick(v, "bounded").update(why="ok"))
        add("status outside the vocabulary", "bad-status:", lambda v: pick(v, "proven").update(status="done"))
        add("discharge row not proven", "open-assume:",
            lambda v: next(e for e in v["ledger"] if e["property"] == v["register"][ALWAYS][0]).update(status="inconclusive"))
        add("counterexample row deleted", "survivor:", lambda v: v["ledger"].remove(pick(v, "cex")))
        add("duplicate row", "duplicate:", lambda v: v["ledger"].append(copy.deepcopy(pick(v, "proven"))))
        add("run not in the run table", "no-such-run:", lambda v: pick(v, "proven").update(run="run-never-made"))
        add("kill nobody exhibited", "unsupported-kill:", lambda v: pick(v, "proven")["kills"].append("mutant-nobody-ran"))
        def other_design(v):
            e = pick(v, "proven")
            other = [r for r, t in v["runs"].items() if len(t) > 3 and t[3] != e["design"] and t[0] == "prove" and t[2] == "proved"]
            if not other:
                v["runs"]["run-elsewhere"] = ("prove", e["bound"], "proved", e["design"] + "_elsewhere")
                other = ["run-elsewhere"]
            e["run"] = other[0]
        add("tracked constant not declared", "hidden-assume:",
            lambda v: v["ledger"][[e["property"] for e in v["ledger"]].index(sorted(v["tracked"])[0])]["assumes"].remove(TRACKED))
        add("cover row without a cover", "no-evidence:", lambda v: next(e for e in v["ledger"] if e["status"] == "cover-reached").update(cover=""))
        add("counterexample row without a kill", "no-evidence:", lambda v: pick(v, "cex").update(kills=[]))
        add("unknown design on a bounded row", "unknown-design:", lambda v: pick(v, "bounded").update(design="no_such_design"))
        add("row omits the reset assumption", "hidden-assume:", lambda v: pick(v, "proven").update(assumes=[]))
        add("assumption not in the register", "unregistered-assume:", lambda v: pick(v, "proven")["assumes"].append("inputs-are-legal"))
        add("final package defers a discharge", "deferred-assume:", lambda v: v["register"].update({ALWAYS: None}))
        add("row cites a run of another design", "run-of-another-design:", other_design)
        return out
    def check_injections(w):
        assert world_ok(w) == [], world_ok(w)
        done = injections(w)
        for name, expect, v in done:
            got = world_ok(v)
            assert any(c.startswith(expect) for c in got), (name, expect, got)
        return len(done)
    
    RUNS = {"tog-prove": ("prove", 4, "proved"), "tog-bmc": ("bmc", 6, "bounded"),
            "tog-w1": ("bmc", 3, "counterexample"), "tog-cover": ("cover", 3, "reached")}
    def row(**k):
        base = dict(assumes=["reset-at-step-0"], cover="", kills=[], rev=3, why="")
        base.update(k)
        return base
    DEMO = dict(
        ledger=[row(property="tog-flips", design="toggle_demo", status="proven", bound=4, why="induction closes", run="tog-prove",
                    cover="c_flip step 2", kills=["tog-W1"]),
                row(property="tog-deep", design="toggle_demo", status="bounded", bound=6, why="depth 6 is past the cover at step 2",
                    run="tog-bmc", cover="c_flip step 2", kills=["tog-W1"], assumes=["reset-at-step-0", "tracked-constant-stable"]),
                row(property="tog-W1@a_flip", design="toggle_demo", status="cex", bound=3, why="fails at step 2", run="tog-w1", kills=["tog-W1"]),
                row(property="tog-covers", design="toggle_demo", status="cover-reached", bound=3, why="witness", run="tog-cover", cover="c_flip step 2")],
        runs=RUNS, required=["tog-flips", "tog-covers"], mutants=["tog-W1"],
        register={"reset-at-step-0": ["tog-covers"], "tracked-constant-stable": ["tog-flips"]}, tracked={"tog-deep"})
    def manifest(world, expect_injections=23):
        problems = world_ok(world)
        assert problems == [], problems
        caught = check_injections(world)
        assert caught == expect_injections, caught
        return caught
    YOUR_WORLD = None        # assemble your own ledger, runs, required rows, mutants, register and tracked rows
    if YOUR_WORLD is not None:
        manifest(YOUR_WORLD)
        print("PASS: formal lab ledger consistent")
    else:
        n = manifest(DEMO)
        # the manifest refuses what it must refuse: an empty world, and a world with a stale row
        assert world_ok(dict(DEMO, ledger=[])) != []
        assert world_ok(dict(DEMO, ledger=[dict(r, rev=2) for r in DEMO["ledger"]])) != []
        print("PASS: the manifest accepts the demonstration ledger and refuses %d injected defects" % n)

    Hint ladder

    • Hint 1: read the property name after the colon of a complaint first; it names the row to open.

    • Hint 2: reproduce the underlying run at the stated depth before editing the ledger.

    • Hint 3: most bound complaints are unfinished runs, and most stale complaints follow an unbumped revision.

    • Hint 4: if an injected defect raises no complaint, fix the engine, not the ledger.

    Verify and debug

    Run the manifest from the page text, never from a hand copy: the runner reads the page, and a hand copy can agree while the page disagrees. Keep the kill steps in the table beside the ledger; a kill that moved to a later step after an edit means the property got weaker. End every stage the same way: rows, engine, one line.

    Timeout or unknown reported as a pass: the summary counts “not failed” as proven. Symptom is a dashboard that stays clean while behaviours sit unexamined. Cause is treating anything without a failure as a pass. Fix: three separate counts, proven, counterexample and inconclusive, with the depth beside every bounded line. The manifest enforces it: a status outside the five words is refused, and a status must be the one its run can give.

    “Your block proves in 3 seconds. Do you believe it?” Answer with the manifest: trigger covers reached, assumptions discharged, every mutant killed at a named step, every row on the final revision. Then open one row and walk it: the run it cites, the bound, the cover step, the kill step, the revision.

    Check yourself The manifest prints its line. What does that line claim, exactly?

    That every row is well formed, backed by a run that can support it, covered, mutant-tested and fresh, that every assumption is registered and discharged, and that each check was shown to catch its injected defect. It claims the package is complete, not that the designs are perfect.

    Check yourself A reviewer asks what is left: bounded results, abstraction risks, neighbour assumptions. Where are the answers?

    In rows, not in memory: the bounded and inconclusive rows, the tracked-slot and tracked-word arguments, and the assumption register with its discharging rows.

    Related topics

    • Next: Solutions

    • Earlier: The Formal Signoff Package

    • After you have filed your rows: Solutions, Ledger and Final Manifest

    Previous

    173 / 183

    Next: Solutions

    Up next

    Up nextSolutions

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Stage 4 Induction on the Counter and FIFO Invariants