Queue formal proof based on one-entry FIFO equivalence #11
|
@ -49,6 +49,18 @@ impl<T: Type> ReadyValid<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This debug port is only meant to assist the formal proof of the queue.
|
||||||
|
#[cfg(test)]
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[hdl]
|
||||||
|
pub struct QueueDebugPort<Element, Index> {
|
||||||
|
#[hdl(flip)]
|
||||||
|
index_to_check: Index,
|
||||||
|
stored: Element,
|
||||||
|
inp_index: Index,
|
||||||
|
out_index: Index,
|
||||||
|
}
|
||||||
|
|
||||||
#[hdl_module]
|
#[hdl_module]
|
||||||
pub fn queue<T: Type>(
|
pub fn queue<T: Type>(
|
||||||
ty: T,
|
ty: T,
|
||||||
|
@ -178,6 +190,22 @@ pub fn queue<T: Type>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// These debug ports expose some internal state during the Induction phase
|
||||||
|
// of Formal Verification. They are not present in normal use.
|
||||||
|
#[cfg(test)]
|
||||||
|
{
|
||||||
|
#[hdl]
|
||||||
|
let dbg: QueueDebugPort<T, UInt> = m.output(QueueDebugPort[ty][index_ty]);
|
||||||
|
// read the memory word currently stored at some fixed index
|
||||||
|
let debug_port = mem.new_read_port();
|
||||||
|
connect(debug_port.addr, dbg.index_to_check);
|
||||||
|
connect(debug_port.en, true);
|
||||||
|
connect(debug_port.clk, cd.clk);
|
||||||
|
connect(dbg.stored, debug_port.data);
|
||||||
|
// also expose the current read and write indices
|
||||||
|
connect(dbg.inp_index, inp_index_reg);
|
||||||
|
connect(dbg.out_index, out_index_reg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -196,13 +224,23 @@ mod tests {
|
||||||
format_args!("test_queue_{capacity}_{inp_ready_is_comb}_{out_valid_is_comb}"),
|
format_args!("test_queue_{capacity}_{inp_ready_is_comb}_{out_valid_is_comb}"),
|
||||||
queue_test(capacity, inp_ready_is_comb, out_valid_is_comb),
|
queue_test(capacity, inp_ready_is_comb, out_valid_is_comb),
|
||||||
FormalMode::Prove,
|
FormalMode::Prove,
|
||||||
14,
|
2,
|
||||||
None,
|
None,
|
||||||
ExportOptions {
|
ExportOptions {
|
||||||
simplify_enums: Some(SimplifyEnumsKind::ReplaceWithBundleOfUInts),
|
simplify_enums: Some(SimplifyEnumsKind::ReplaceWithBundleOfUInts),
|
||||||
..ExportOptions::default()
|
..ExportOptions::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
/// Formal verification of the FIFO queue
|
||||||
|
///
|
||||||
|
/// The strategy derives from the observation that, if we filter its
|
||||||
|
/// input and output streams to consider just one in every N reads and
|
||||||
|
/// writes (where N is the FIFO capacity), then the FIFO effectively
|
||||||
|
/// behaves as a one-entry FIFO.
|
||||||
|
///
|
||||||
|
/// In particular, any counterexample of the full FIFO behaving badly
|
||||||
|
/// will also be caught by one of the filtered versions (one which
|
||||||
|
/// happens to be in phase with the offending input or output).
|
||||||
#[hdl_module]
|
#[hdl_module]
|
||||||
fn queue_test(capacity: NonZeroUsize, inp_ready_is_comb: bool, out_valid_is_comb: bool) {
|
fn queue_test(capacity: NonZeroUsize, inp_ready_is_comb: bool, out_valid_is_comb: bool) {
|
||||||
#[hdl]
|
#[hdl]
|
||||||
|
@ -217,6 +255,8 @@ mod tests {
|
||||||
rst: formal_reset().to_reset(),
|
rst: formal_reset().to_reset(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// random input data
|
||||||
#[hdl]
|
#[hdl]
|
||||||
let inp_data: HdlOption<UInt<8>> = wire();
|
let inp_data: HdlOption<UInt<8>> = wire();
|
||||||
#[hdl]
|
#[hdl]
|
||||||
|
@ -225,16 +265,26 @@ mod tests {
|
||||||
} else {
|
} else {
|
||||||
connect(inp_data, HdlNone());
|
connect(inp_data, HdlNone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// assert output ready at random
|
||||||
#[hdl]
|
#[hdl]
|
||||||
let out_ready: Bool = wire();
|
let out_ready: Bool = wire();
|
||||||
connect(out_ready, any_seq(Bool));
|
connect(out_ready, any_seq(Bool));
|
||||||
let index_ty: UInt<32> = UInt::TYPE;
|
|
||||||
|
// The current number of elements in the FIFO ranges from zero to
|
||||||
|
// maximum capacity, inclusive.
|
||||||
|
let count_ty = UInt::range_inclusive(0..=capacity.get());
|
||||||
|
// type for counters that wrap around at the FIFO capacity
|
||||||
|
let index_ty = UInt::range(0..capacity.get());
|
||||||
|
|
||||||
|
// among all entries of the FIFO internal circular memory, choose
|
||||||
|
// one at random to check
|
||||||
#[hdl]
|
#[hdl]
|
||||||
let index_to_check = wire();
|
let index_to_check = wire(index_ty);
|
||||||
connect(index_to_check, any_const(index_ty));
|
connect(index_to_check, any_const(index_ty));
|
||||||
let index_max = !index_ty.zero();
|
hdl_assume(clk, index_to_check.cmp_lt(capacity.get()), "");
|
||||||
// we saturate at index_max, so only check indexes where we properly maintain position
|
|
||||||
hdl_assume(clk, index_to_check.cmp_ne(index_max), "");
|
// instantiate and connect the queue
|
||||||
#[hdl]
|
#[hdl]
|
||||||
let dut = instance(queue(
|
let dut = instance(queue(
|
||||||
UInt[ConstUsize::<8>],
|
UInt[ConstUsize::<8>],
|
||||||
|
@ -245,109 +295,172 @@ mod tests {
|
||||||
connect(dut.cd, cd);
|
connect(dut.cd, cd);
|
||||||
connect(dut.inp.data, inp_data);
|
connect(dut.inp.data, inp_data);
|
||||||
connect(dut.out.ready, out_ready);
|
connect(dut.out.ready, out_ready);
|
||||||
hdl_assume(
|
|
||||||
clk,
|
|
||||||
index_to_check.cmp_ne(!Expr::ty(index_to_check).zero()),
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// Keep an independent count of words in the FIFO. Ensure that
|
||||||
|
// it's always correct, and never overflows.
|
||||||
#[hdl]
|
#[hdl]
|
||||||
let expected_count_reg = reg_builder().clock_domain(cd).reset(0u32);
|
let expected_count_reg = reg_builder().clock_domain(cd).reset(count_ty.zero());
|
||||||
#[hdl]
|
|
||||||
let next_expected_count = wire();
|
|
||||||
connect(next_expected_count, expected_count_reg);
|
|
||||||
connect(expected_count_reg, next_expected_count);
|
|
||||||
#[hdl]
|
#[hdl]
|
||||||
if ReadyValid::firing(dut.inp) & !ReadyValid::firing(dut.out) {
|
if ReadyValid::firing(dut.inp) & !ReadyValid::firing(dut.out) {
|
||||||
connect_any(next_expected_count, expected_count_reg + 1u8);
|
hdl_assert(clk, expected_count_reg.cmp_ne(capacity.get()), "");
|
||||||
|
connect_any(expected_count_reg, expected_count_reg + 1u8);
|
||||||
} else if !ReadyValid::firing(dut.inp) & ReadyValid::firing(dut.out) {
|
} else if !ReadyValid::firing(dut.inp) & ReadyValid::firing(dut.out) {
|
||||||
connect_any(next_expected_count, expected_count_reg - 1u8);
|
hdl_assert(clk, expected_count_reg.cmp_ne(count_ty.zero()), "");
|
||||||
|
connect_any(expected_count_reg, expected_count_reg - 1u8);
|
||||||
}
|
}
|
||||||
hdl_assert(cd.clk, expected_count_reg.cmp_eq(dut.count), "");
|
hdl_assert(clk, expected_count_reg.cmp_eq(dut.count), "");
|
||||||
|
|
||||||
#[hdl]
|
|
||||||
let prev_out_ready_reg = reg_builder().clock_domain(cd).reset(!0_hdl_u3);
|
|
||||||
connect_any(
|
|
||||||
prev_out_ready_reg,
|
|
||||||
(prev_out_ready_reg << 1) | out_ready.cast_to(UInt[1]),
|
|
||||||
);
|
|
||||||
#[hdl]
|
|
||||||
let prev_inp_valid_reg = reg_builder().clock_domain(cd).reset(!0_hdl_u3);
|
|
||||||
connect_any(
|
|
||||||
prev_inp_valid_reg,
|
|
||||||
(prev_inp_valid_reg << 1) | HdlOption::is_some(inp_data).cast_to(UInt[1]),
|
|
||||||
);
|
|
||||||
hdl_assume(
|
|
||||||
clk,
|
|
||||||
(prev_out_ready_reg & prev_inp_valid_reg).cmp_ne(0u8),
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// keep an independent write index into the FIFO's circular buffer
|
||||||
#[hdl]
|
#[hdl]
|
||||||
let inp_index_reg = reg_builder().clock_domain(cd).reset(index_ty.zero());
|
let inp_index_reg = reg_builder().clock_domain(cd).reset(index_ty.zero());
|
||||||
#[hdl]
|
#[hdl]
|
||||||
let stored_inp_data_reg = reg_builder().clock_domain(cd).reset(0u8);
|
if ReadyValid::firing(dut.inp) {
|
||||||
|
|
||||||
#[hdl]
|
#[hdl]
|
||||||
if let HdlSome(data) = ReadyValid::firing_data(dut.inp) {
|
if inp_index_reg.cmp_ne(capacity.get() - 1) {
|
||||||
#[hdl]
|
|
||||||
if inp_index_reg.cmp_lt(index_max) {
|
|
||||||
connect_any(inp_index_reg, inp_index_reg + 1u8);
|
connect_any(inp_index_reg, inp_index_reg + 1u8);
|
||||||
#[hdl]
|
} else {
|
||||||
if inp_index_reg.cmp_eq(index_to_check) {
|
connect_any(inp_index_reg, 0_hdl_u0);
|
||||||
connect(stored_inp_data_reg, data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[hdl]
|
// keep an independent read index into the FIFO's circular buffer
|
||||||
if inp_index_reg.cmp_lt(index_to_check) {
|
|
||||||
hdl_assert(clk, stored_inp_data_reg.cmp_eq(0u8), "");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[hdl]
|
#[hdl]
|
||||||
let out_index_reg = reg_builder().clock_domain(cd).reset(index_ty.zero());
|
let out_index_reg = reg_builder().clock_domain(cd).reset(index_ty.zero());
|
||||||
#[hdl]
|
#[hdl]
|
||||||
let stored_out_data_reg = reg_builder().clock_domain(cd).reset(0u8);
|
if ReadyValid::firing(dut.out) {
|
||||||
|
|
||||||
#[hdl]
|
#[hdl]
|
||||||
if let HdlSome(data) = ReadyValid::firing_data(dut.out) {
|
if out_index_reg.cmp_ne(capacity.get() - 1) {
|
||||||
#[hdl]
|
|
||||||
if out_index_reg.cmp_lt(index_max) {
|
|
||||||
connect_any(out_index_reg, out_index_reg + 1u8);
|
connect_any(out_index_reg, out_index_reg + 1u8);
|
||||||
#[hdl]
|
|
||||||
if out_index_reg.cmp_eq(index_to_check) {
|
|
||||||
connect(stored_out_data_reg, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[hdl]
|
|
||||||
if out_index_reg.cmp_lt(index_to_check) {
|
|
||||||
hdl_assert(clk, stored_out_data_reg.cmp_eq(0u8), "");
|
|
||||||
}
|
|
||||||
|
|
||||||
hdl_assert(clk, inp_index_reg.cmp_ge(out_index_reg), "");
|
|
||||||
|
|
||||||
#[hdl]
|
|
||||||
if inp_index_reg.cmp_lt(index_max) & out_index_reg.cmp_lt(index_max) {
|
|
||||||
hdl_assert(
|
|
||||||
clk,
|
|
||||||
expected_count_reg.cmp_eq(inp_index_reg - out_index_reg),
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
hdl_assert(
|
connect_any(out_index_reg, 0_hdl_u0);
|
||||||
clk,
|
}
|
||||||
expected_count_reg.cmp_ge(inp_index_reg - out_index_reg),
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filter the input data stream, predicated by the read index
|
||||||
|
// matching the chosen position in the FIFO's circular buffer
|
||||||
#[hdl]
|
#[hdl]
|
||||||
if inp_index_reg.cmp_gt(index_to_check) & out_index_reg.cmp_gt(index_to_check) {
|
let inp_index_matches = wire();
|
||||||
hdl_assert(clk, stored_inp_data_reg.cmp_eq(stored_out_data_reg), "");
|
connect(inp_index_matches, inp_index_reg.cmp_eq(index_to_check));
|
||||||
|
#[hdl]
|
||||||
|
let inp_firing_data = wire();
|
||||||
|
connect(inp_firing_data, HdlNone());
|
||||||
|
#[hdl]
|
||||||
|
if inp_index_matches {
|
||||||
|
connect(inp_firing_data, ReadyValid::firing_data(dut.inp));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filter the output data stream, predicated by the write index
|
||||||
|
// matching the chosen position in the FIFO's circular buffer
|
||||||
|
#[hdl]
|
||||||
|
let out_index_matches = wire();
|
||||||
|
connect(out_index_matches, out_index_reg.cmp_eq(index_to_check));
|
||||||
|
#[hdl]
|
||||||
|
let out_firing_data = wire();
|
||||||
|
connect(out_firing_data, HdlNone());
|
||||||
|
#[hdl]
|
||||||
|
if out_index_matches {
|
||||||
|
connect(out_firing_data, ReadyValid::firing_data(dut.out));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement a one-entry FIFO and ensure its equivalence to the
|
||||||
|
// filtered FIFO.
|
||||||
|
//
|
||||||
|
// the holding register for our one-entry FIFO
|
||||||
|
#[hdl]
|
||||||
|
let stored_reg = reg_builder().clock_domain(cd).reset(HdlNone());
|
||||||
|
#[hdl]
|
||||||
|
match stored_reg {
|
||||||
|
// If the holding register is empty...
|
||||||
|
HdlNone => {
|
||||||
|
#[hdl]
|
||||||
|
match inp_firing_data {
|
||||||
|
// ... and we are not receiving data, then we must not
|
||||||
|
// transmit any data.
|
||||||
|
HdlNone => hdl_assert(clk, HdlOption::is_none(out_firing_data), ""),
|
||||||
|
// If we are indeed receiving some data...
|
||||||
|
HdlSome(data_in) => {
|
||||||
|
#[hdl]
|
||||||
|
match out_firing_data {
|
||||||
|
// ... and transmitting at the same time, we
|
||||||
|
// must be transmitting the input data itself,
|
||||||
|
// since the holding register is empty.
|
||||||
|
HdlSome(data_out) => hdl_assert(clk, data_out.cmp_eq(data_in), ""),
|
||||||
|
// If we are receiving, but not transmitting,
|
||||||
|
// store the received data in the holding
|
||||||
|
// register.
|
||||||
|
HdlNone => connect(stored_reg, HdlSome(data_in)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If there is some value stored in the holding register...
|
||||||
|
HdlSome(stored) => {
|
||||||
|
#[hdl]
|
||||||
|
match out_firing_data {
|
||||||
|
// ... and we are not transmitting it, we cannot
|
||||||
|
// receive any more data.
|
||||||
|
HdlNone => hdl_assert(clk, HdlOption::is_none(inp_firing_data), ""),
|
||||||
|
// If we are transmitting a previously stored value...
|
||||||
|
HdlSome(data_out) => {
|
||||||
|
// ... it must be the same data we stored earlier.
|
||||||
|
hdl_assert(clk, data_out.cmp_eq(stored), "");
|
||||||
|
// Also, accept new data, if any. Otherwise,
|
||||||
|
// let the holding register become empty.
|
||||||
|
connect(stored_reg, inp_firing_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// from now on, some extra assertions in order to pass induction
|
||||||
|
|
||||||
|
// sync the holding register, when it's occupied, to the
|
||||||
|
// corresponding entry in the FIFO's circular buffer
|
||||||
|
connect(dut.dbg.index_to_check, index_to_check);
|
||||||
|
#[hdl]
|
||||||
|
if let HdlSome(stored) = stored_reg {
|
||||||
|
hdl_assert(clk, stored.cmp_eq(dut.dbg.stored), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync the read and write indices
|
||||||
|
hdl_assert(clk, inp_index_reg.cmp_eq(dut.dbg.inp_index), "");
|
||||||
|
hdl_assert(clk, out_index_reg.cmp_eq(dut.dbg.out_index), "");
|
||||||
|
|
||||||
|
// the indices should never go past the capacity, but induction
|
||||||
|
// doesn't know that...
|
||||||
|
hdl_assert(clk, inp_index_reg.cmp_lt(capacity.get()), "");
|
||||||
|
hdl_assert(clk, out_index_reg.cmp_lt(capacity.get()), "");
|
||||||
|
|
||||||
|
// strongly constrain the state of the holding register
|
||||||
|
//
|
||||||
|
// The holding register is full if and only if the corresponding
|
||||||
|
// FIFO entry was written to and not yet read. In other words, if
|
||||||
|
// the number of pending reads until the chosen entry is read out
|
||||||
|
// is greater than the current FIFO count, then the entry couldn't
|
||||||
|
// be in the FIFO in the first place.
|
||||||
|
#[hdl]
|
||||||
|
let pending_reads: UInt = wire(index_ty);
|
||||||
|
// take care of wrap-around when subtracting indices, add the
|
||||||
|
// capacity amount to keep the result positive if necessary
|
||||||
|
#[hdl]
|
||||||
|
if index_to_check.cmp_ge(out_index_reg) {
|
||||||
|
connect(pending_reads, index_to_check - out_index_reg);
|
||||||
|
} else {
|
||||||
|
connect(
|
||||||
|
pending_reads,
|
||||||
|
index_to_check + capacity.get() - out_index_reg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// check whether the chosen entry is in the FIFO
|
||||||
|
#[hdl]
|
||||||
|
let expected_stored: Bool = wire();
|
||||||
|
connect(expected_stored, pending_reads.cmp_lt(dut.count));
|
||||||
|
// sync with the state of the holding register
|
||||||
|
hdl_assert(
|
||||||
|
clk,
|
||||||
|
expected_stored.cmp_eq(HdlOption::is_some(stored_reg)),
|
||||||
|
"",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -430,4 +543,24 @@ mod tests {
|
||||||
fn test_4_true_true() {
|
fn test_4_true_true() {
|
||||||
test_queue(NonZero::new(4).unwrap(), true, true);
|
test_queue(NonZero::new(4).unwrap(), true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_many_false_false() {
|
||||||
|
test_queue(NonZero::new((2 << 16) - 5).unwrap(), false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_many_false_true() {
|
||||||
|
test_queue(NonZero::new((2 << 16) - 5).unwrap(), false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_many_true_false() {
|
||||||
|
test_queue(NonZero::new((2 << 16) - 5).unwrap(), true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_many_true_true() {
|
||||||
|
test_queue(NonZero::new((2 << 16) - 5).unwrap(), true, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue