The symptom: a word vanishes under backpressure, yet a review note says the block “proved clean”. A proof is only as strong as the model around it. Before you accept a clean result, list what the proof assumed about the inputs. This lesson gives you the contract of the bridge and one input rule that a colleague proposes “to make the proof faster”; you decide what the rule does to the faults.
The bridge below, with the BUG switch (W1 to W3) and the HELPER switch for the three helper assertions.
The property set already written: skid-needs-output, conservation, valid hold, data hold, ready independence, tracked-word integrity with its helpers, and five covers.
The contract table.
Read the BUG == lines of the listing last, after you have read each trace. The listing adds one cover, c_tracked_out, to the four covers of the bridge in the earlier sections; it belongs to this lab only.
Side | Rule | Where it is checked |
|---|---|---|
Inputs in_valid, in_data, out_ready | Free in every cycle. The only constraint on the model is the reset at step 0. | Nothing is assumed beyond the harness |
Outputs out_valid, out_data | A stalled output keeps its valid and its data in the next cycle. | a_valid_hold, a_data_hold |
Output in_ready | A function of the skid register alone, with no path from out_ready. | a_no_ready_comb |
Counts | Accepted words minus delivered words equal the words held. | a_conserve |
Tracked word | The word delivered as number tn is the word accepted as number tn. | a_data with its helpers |
// formal: prove 4 expect pass slang top hs_bridge
`define FORMAL
// hs_bridge: valid/ready register stage with a skid register (two words can be held).
// Output stage ovld/odat plus skid stage svld/sdat. in_ready is a function of a register only.
// Synchronous active-high reset empties both stages. A transfer happens in a cycle where valid
// and ready are both 1. BUG selects a variant (0 is the design as specified); HELPER=0 removes the helpers.
module hs_bridge #(parameter BUG = 0, parameter HELPER = 1) (
input wire clk,
input wire rst,
input wire in_valid,
output wire in_ready,
input wire [7:0] in_data,
output wire out_valid,
input wire out_ready,
output wire [7:0] out_data
);
reg ovld, svld;
reg [7:0] odat, sdat;
assign in_ready = !svld;
assign out_valid = ovld;
assign out_data = odat;
wire in_xfer = in_valid && in_ready;
wire out_xfer = ovld && out_ready;
wire stalled = ovld && !out_ready;
always @(posedge clk) begin
if (rst) begin
ovld <= 1'b0;
svld <= 1'b0;
end else if (stalled) begin
if (in_xfer && BUG != 1) begin svld <= 1'b1; sdat <= in_data; end
if (BUG == 2 && !in_valid) ovld <= 1'b0;
if (BUG == 3 && in_xfer) odat <= in_data;
end else begin
if (svld) begin
odat <= sdat; ovld <= 1'b1; svld <= 1'b0;
end else begin
odat <= in_data; ovld <= in_valid;
end
end
end
`ifdef FORMAL
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// ---- counters: accepted and delivered words (4 bits, wrap at 16) ----
reg [3:0] acc, dlv;
always @(posedge clk) begin
if (rst) begin acc <= 4'd0; dlv <= 4'd0; end
else begin
if (in_xfer) acc <= acc + 4'd1;
if (out_xfer) dlv <= dlv + 4'd1;
end
end
always @(*) if (!init) begin
a_skid_needs_out: assert (!svld || ovld);
a_conserve: assert (acc - dlv == {3'b0, ovld} + {3'b0, svld});
end
// ---- protocol: history-based, so skip the cycle after reset ----
always @(posedge clk) if (!init && !$past(rst)) begin
a_valid_hold: assert (!($past(out_valid) && !$past(out_ready)) || out_valid);
a_data_hold: assert (!($past(out_valid) && !$past(out_ready)) || out_data == $past(out_data));
a_no_ready_comb: assert (in_ready == !svld);
end
// ---- data integrity by tracked-word abstraction: follow the n-th accepted word ----
(* anyconst *) wire [3:0] tn;
reg [3:0] tn_q;
always @(posedge clk) tn_q <= tn;
always @(*) if (!init) assume (tn == tn_q);
reg tv; // the tracked word has been accepted and not yet delivered
reg [7:0] td;
always @(posedge clk) begin
if (rst) tv <= 1'b0;
else begin
if (in_xfer && acc == tn) begin tv <= 1'b1; td <= in_data; end
else if (out_xfer && dlv == tn) tv <= 1'b0;
end
end
always @(*) if (!init && out_xfer && tv && dlv == tn) a_data: assert (out_data == td);
generate if (HELPER) begin : g_helper
always @(*) if (!init) begin
h_in_out: assert (!(tv && dlv == tn) || (ovld && odat == td));
h_in_skid: assert (!(tv && dlv + 4'd1 == tn) || (svld && sdat == td));
h_pos: assert (!tv || (dlv == tn) || (dlv + 4'd1 == tn));
end
end endgenerate
// ---- covers ----
always @(*) if (!init) begin
c_stall: cover (stalled);
c_skid_used: cover (svld);
c_backpress_full: cover (!in_ready);
c_flow: cover (in_xfer && out_xfer);
c_tracked_out: cover (out_xfer && tv && dlv == tn);
end
`endif
endmoduleThe proposed input rule lives in a wrapper: the upstream never sends while the output is not ready. The block below shows the good design under the rule, which passes; the deliverable is what happens to each variant with the rule on and off.
// formal: bmc 12 expect pass slang top bridge_rule_top define BUG=0 define RULE=1
`define FORMAL
// hs_bridge: valid/ready register stage with a skid register (two words can be held).
// Output stage ovld/odat plus skid stage svld/sdat. in_ready is a function of a register only.
// Synchronous active-high reset empties both stages. A transfer happens in a cycle where valid
// and ready are both 1. BUG selects a variant (0 is the design as specified); HELPER=0 removes the helpers.
module hs_bridge #(parameter BUG = 0, parameter HELPER = 1) (
input wire clk,
input wire rst,
input wire in_valid,
output wire in_ready,
input wire [7:0] in_data,
output wire out_valid,
input wire out_ready,
output wire [7:0] out_data
);
reg ovld, svld;
reg [7:0] odat, sdat;
assign in_ready = !svld;
assign out_valid = ovld;
assign out_data = odat;
wire in_xfer = in_valid && in_ready;
wire out_xfer = ovld && out_ready;
wire stalled = ovld && !out_ready;
always @(posedge clk) begin
if (rst) begin
ovld <= 1'b0;
svld <= 1'b0;
end else if (stalled) begin
if (in_xfer && BUG != 1) begin svld <= 1'b1; sdat <= in_data; end
if (BUG == 2 && !in_valid) ovld <= 1'b0;
if (BUG == 3 && in_xfer) odat <= in_data;
end else begin
if (svld) begin
odat <= sdat; ovld <= 1'b1; svld <= 1'b0;
end else begin
odat <= in_data; ovld <= in_valid;
end
end
end
`ifdef FORMAL
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// ---- counters: accepted and delivered words (4 bits, wrap at 16) ----
reg [3:0] acc, dlv;
always @(posedge clk) begin
if (rst) begin acc <= 4'd0; dlv <= 4'd0; end
else begin
if (in_xfer) acc <= acc + 4'd1;
if (out_xfer) dlv <= dlv + 4'd1;
end
end
always @(*) if (!init) begin
a_skid_needs_out: assert (!svld || ovld);
a_conserve: assert (acc - dlv == {3'b0, ovld} + {3'b0, svld});
end
// ---- protocol: history-based, so skip the cycle after reset ----
always @(posedge clk) if (!init && !$past(rst)) begin
a_valid_hold: assert (!($past(out_valid) && !$past(out_ready)) || out_valid);
a_data_hold: assert (!($past(out_valid) && !$past(out_ready)) || out_data == $past(out_data));
a_no_ready_comb: assert (in_ready == !svld);
end
// ---- data integrity by tracked-word abstraction: follow the n-th accepted word ----
(* anyconst *) wire [3:0] tn;
reg [3:0] tn_q;
always @(posedge clk) tn_q <= tn;
always @(*) if (!init) assume (tn == tn_q);
reg tv; // the tracked word has been accepted and not yet delivered
reg [7:0] td;
always @(posedge clk) begin
if (rst) tv <= 1'b0;
else begin
if (in_xfer && acc == tn) begin tv <= 1'b1; td <= in_data; end
else if (out_xfer && dlv == tn) tv <= 1'b0;
end
end
always @(*) if (!init && out_xfer && tv && dlv == tn) a_data: assert (out_data == td);
generate if (HELPER) begin : g_helper
always @(*) if (!init) begin
h_in_out: assert (!(tv && dlv == tn) || (ovld && odat == td));
h_in_skid: assert (!(tv && dlv + 4'd1 == tn) || (svld && sdat == td));
h_pos: assert (!tv || (dlv == tn) || (dlv + 4'd1 == tn));
end
end endgenerate
// ---- covers ----
always @(*) if (!init) begin
c_stall: cover (stalled);
c_skid_used: cover (svld);
c_backpress_full: cover (!in_ready);
c_flow: cover (in_xfer && out_xfer);
c_tracked_out: cover (out_xfer && tv && dlv == tn);
end
`endif
endmodule
// lesson-local wrapper: the bridge under an extra rule on its input. RULE=0 removes the rule.
module bridge_rule_top #(parameter BUG = 0, parameter RULE = 1) (
input wire clk,
input wire rst,
input wire in_valid,
input wire [7:0] in_data,
input wire out_ready,
output wire in_ready,
output wire out_valid,
output wire [7:0] out_data
);
hs_bridge #(.BUG(BUG)) u (.clk(clk), .rst(rst), .in_valid(in_valid),
.in_ready(in_ready), .in_data(in_data), .out_valid(out_valid),
.out_ready(out_ready), .out_data(out_data));
`ifdef FORMAL
reg t_init = 1'b1;
always @(posedge clk) t_init <= 1'b0;
always @(*) if (t_init) assume (rst);
// the rule: the upstream never sends while the output is not ready
always @(*) if (RULE && !t_init) assume (!in_valid || out_ready);
`endif
endmoduleEdit the header of the first block (plain bridge) for W1 to W3, and the header of the second block (wrapper) for the rule: define RULE=1 and define RULE=0.
Variant | Edit the header of the block above to | Expected category |
|---|---|---|
W1 |
| counterexample |
W2 |
| counterexample |
W3 |
| counterexample |
Produce | How |
|---|---|
The failing property and step for each variant, without the rule | Plain bridge, depth 12, then lower the depth. |
A table of variant against rule on/off | Wrapper with |
A verdict on the rule | Which variants does it hide, and what does that make the rule? |
Four rows | Conservation, hold rules, tracked-word integrity, and the covers; the register entries each one needs. |
The good design is proved; each variant gives a counterexample on the plain bridge.
Your table shows which variants stay visible with the rule on. A rule under which a variant passes is not filed as evidence.
The package check passes for your rows.
# ---- 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)
RUNS = {
"bridge-prove": ("prove", 4, "proved"),
"bridge-rule-base": ("bmc", 12, "bounded"),
}
REQUIRED = ["bridge-conservation", "bridge-hold-rules", "bridge-data-integrity"]
MUTANTS = ["bridge-W%d" % i for i in range(1, 4)]
REGISTER = {"reset-at-step-0": None, "tracked-constant-stable": None} # discharged by later rows
TRACKED_ROWS = {"bridge-data-integrity"}
YOUR_ROWS = [] # fill from your own runs
bad = ledger_ok(YOUR_ROWS, RUNS, REQUIRED, MUTANTS, REGISTER, TRACKED_ROWS, final=False)
if YOUR_ROWS:
assert bad == [], bad
verdict = "stage 1 bridge package consistent"
else:
expected = ["missing-row:" + r for r in REQUIRED] + ["survivor:" + m for m in MUTANTS]
assert sorted(bad) == sorted(expected), bad
verdict = "the empty package is rejected with %d complaints" % len(bad)
# an input rule is an assumption: it must be registered, and a row that uses it must say so
rule_row = dict(property="bridge-under-rule", design="hs_bridge", status="bounded", bound=12, why="rule on: no input while stalled",
run="bridge-rule-base", assumes=["reset-at-step-0", "no-input-while-stalled"], cover="c_stall step 2",
kills=[], rev=3)
bad_rule = ledger_ok([rule_row], RUNS, [], [], REGISTER, set(), final=False)
assert "unregistered-assume:bridge-under-rule:no-input-while-stalled" in bad_rule, bad_rule
assert "no-evidence:bridge-under-rule" in bad_rule
print("PASS: %s; a row that leans on an unregistered input rule is refused" % verdict)Hint 1: for each variant, start from the property family that matches its symptom word: loss is counted, a changed word is a value, a dropped valid is a protocol rule.
Hint 2: run the variant at depth 12, lower the depth to find the failing step, and write the trace as a table of inputs and state.
Hint 3: for the rule, ask which inputs the rule forbids and whether any variant needs those inputs to show itself.
Hint 4: remove the rule again and rerun; the verdict of the rule is the difference between the two runs, not the design.
When a proof passes suspiciously early with a variant selected, list the assumptions before reading the design: the fastest pass in the lab is a model with nothing left to check. Replay a counterexample against the unconstrained design before trusting a fix, and confirm the fix by watching the same step pass on the corrected design.
Two models, not two designs. Which behaviours the rule removed, and whether you accept that, is the verdict the deliverable asks for; the Solutions topic gives the reference verdict.
The reset at step 0 and, for the tracked-word property, the free constant tn assumed equal to its one-cycle-old copy. Every input is free.