Improving Formal Verification Support in CIRCT
Amelia Dobis – April 2024 to August 2024
1
Goal of my work
⇒ Improve efficiency and capabilities of Formal Verification inside of CIRCT and Chisel.
2
Goal of my work
⇒ Improve efficiency and capabilities of Formal Verification inside of CIRCT and Chisel.
3
Goal of my work
⇒ Improve efficiency and capabilities of Formal Verification inside of CIRCT and Chisel.
4
Overview:
5
Improving Existing Verification Infrastructure
6
Overview:
7
Verification Maintenance and Debugging
8
Updating LTL
9
Improving SVA Property Emission
10
Verification Debugging
11
Overview:
12
Overview:
13
Re-imagining Formal Verification in Chisel and CIRCT
14
Background: Formal Verification
What is Formal Verification?
15
Background: Verification Conditions
Idea: Convert design into conjunction of constraints (signal definitions) and negated assertion conditions.
16
val a = IO(Input(32.W))
val b = a + 1.U
assert (b > a)
(and
(eq b (add a 1)) // define b
(not (gt b a)) // can assertion be violated?
)
Background: Verification Conditions
Handling State: Create “state-transition systems” from registers + memories, requires Bounded Model Checking to be verified.
17
Background: BTOR2 and btormc
18
1 sort bitvector 32 ; declare a 32-bit type
2 state 1 count ; declare a 32-bit state
3 one 1 ; declare a 32-bit constant of value 1
4 and 1 2 3
5 next 1 2 4 ; count := count & 1
Motivation: Formal Verification
class Counter extends Module {
val count = RegInit(0.U(32.W))
when(count === 42.U) { count := 0.U }
otherwise { count := count + 1.U }
assert(count < 42.U)
}
19
CIRCT
Counter.sv
Yosys
JasperGold
c.btor2
c.smt2
btormc
z3
SAT/UNSAT
Motivation: Formal Verification
class Counter extends Module {
val count = RegInit(0.U(32.W))
when(count === 42.U) { count := 0.U }
otherwise { count := count + 1.U }
assert(count < 42.U)
}
20
CIRCT
c.btor2
btormc
SAT/UNSAT
circt-bmc
z3
Overview:
21
Inline Formal: Unified formal test interface
22
Inline Formal: Unified formal test interface
23
class Foo extends Module {
val in = IO(Input(UInt(32.W))
val out = IO(Output(UInt(32.W))
/* body of the module */
// Some formal test
test formal testFoo(500) {
val dut = Instantiate(Foo)
AssertProperty(/* some property */)
}
}
Inline Formal: Unified formal test interface
24
class Foo extends Module {
val in = IO(Input(UInt(32.W))
val out = IO(Output(UInt(32.W))
/* body of the module */
// Some formal test
test formal testFoo(500) {
val dut = Instantiate(Foo)
AssertProperty(/* some property */)
}
}
Overview:
25
Overview:
26
Handling Modularity in Designs under Verification
→ We only want to verify a module exactly once.
(not once per instance)
27
Handling Modularity in Designs under Verification
28
Handling Modularity in Designs under Verification
29
Inline Formal: Unified formal test interface
30
class Foo extends Module with Contract {
val in = IO(Input(UInt(32.W))
val out = IO(Output(UInt(32.W))
// define contract
contract {
require(in > 0.U)
require(in < 1000.U)
ensure(/* some post-condition */)
}
/* body of the module */
// Some formal test
test formal testFoo(500) {
val dut = Instantiate(Foo)
AssertProperty(/* some property */)
}
}
Inline Formal: Unified formal test interface
31
class Foo extends Module with Contract {
val in = IO(Input(UInt(32.W))
val out = IO(Output(UInt(32.W))
// define contract
contract {
require(in > 0.U)
require(in < 1000.U)
ensure(/* some post-condition */)
}
/* body of the module */
// Some formal test
test formal testFoo(500) {
val dut = Instantiate(Foo)
AssertProperty(/* some property */)
}
}
// module test
test formal Foo(500) {
val dut = Instantiate(Foo)
Assume(dut.in > 0.U)
Assume(dut.in < 1000.U)
Assert(/*Body*/ && /*Post-conditions*/)
}
// module instance test
test formal testFoo(500) {
val dut = Instantiate(Foo)
Assert(dut.in > 0.U)
Assert(dut.in < 1000.U)
Assume(/*Post-conditions*/)
AssertProperty(/* some property */)
}
Overview:
32
End-to-End Verification Flow
33
class Foo extends Module with Contract {
val in = IO(Input(UInt(32.W))
val out = IO(Output(UInt(32.W))
// define contract
contract {
require(in > 0.U)
require(in < 1000.U)
ensure(/* some post-condition */)
}
/* body of the module */
// Some formal test
test formal testFoo(500) {
val dut = Instantiate(Foo)
AssertProperty(/* some property */)
}
}
class Bar extends Module with Contract {
val in = IO(Input(UInt(32.W))
val sign = IO(Input(Bool())
val out = IO(Output(UInt(32.W))
contract {
require(sign |-> in > 0.U)
require(!sign |-> in < 0.U)
// Ensures support SVA properties
ensure((sign |=> out > 0.U) &&
(!sign |=> out < 0.U))
}
val foo1 = Instantiate(Foo)
val foo2 = Instantiate(Foo)
/* body of bar that uses multiple Foos */
test formal testBar(500) {
val foo1 = Instantiate(Bar)
AssertProperty(/*some property*/)
}
}
object Bar extends App {
ChiselStage.emitBtor2(new Bar)
}
Chisel
End-to-End Verification Flow
34
class Foo extends Module with Contract {
val in = IO(Input(UInt(32.W))
val out = IO(Output(UInt(32.W))
// define contract
contract {
require(in > 0.U)
require(in < 1000.U)
ensure(/* some post-condition */)
}
/* body of the module */
// Some formal test
test formal testFoo(500) {
val dut = Instantiate(Foo)
AssertProperty(/* some property */)
}
}
class Bar extends Module with Contract {
val in = IO(Input(UInt(32.W))
val sign = IO(Input(Bool())
val out = IO(Output(UInt(32.W))
contract {
require(sign |-> in > 0.U)
require(!sign |-> in < 0.U)
// Ensures support SVA properties
ensure((sign |=> out > 0.U) &&
(!sign |=> out < 0.U))
}
val foo1 = Instantiate(Foo)
val foo2 = Instantiate(Foo)
/* body of bar that uses multiple Foos */
test formal testBar(500) {
val foo1 = Instantiate(Bar)
AssertProperty(/*some property*/)
}
}
object Bar extends App {
ChiselStage.emitBtor2(new Bar)
}
Chisel
sbt run
circuit Bar:
public module Foo:
input in : UInt<32>
output out : UInt<32>
contract:
node prec0 = gt(in, 0)
require prec0
node prec1 = lt(in, 1000)
require prec1
node post = ;;some post-condition;;
ensure post
;; Body of the module
;; Some Formal Test
public module _Foo:
input s_in : UInt<32>
inst dut of Foo
connect dut.in, s_in
intrinsic(circt_verif_assert(...))
formal testFoo of _Foo, bound = 500
public module Bar:
input in : UInt<32>
input sign : Bool
output out : UInt<32>
contract:
node prec0 = intrinsic(circt_ltl_implication(...))
require prec0
node prec1 = intrinsic(circt_ltl_implication(...))
require prec1
node post = ;;some post-condition;;
ensure post
inst foo1 of Foo
inst foo2 of Foo
;; body of bar that uses multiple Foos ;;
public module _Bar:
input s_in : UInt<32>
input s_en : Bool
inst dut of Bar
connect dut.in, s_in
connect dut.en, s_en
intrinsic(circt_verif_assert(...))
formal testBar of _Bar, bound = 500
FIRRTL
End-to-End Verification Flow
35
class Foo extends Module with Contract {
val in = IO(Input(UInt(32.W))
val out = IO(Output(UInt(32.W))
// define contract
contract {
require(in > 0.U)
require(in < 1000.U)
ensure(/* some post-condition */)
}
/* body of the module */
// Some formal test
test formal testFoo(500) {
val dut = Instantiate(Foo)
AssertProperty(/* some property */)
}
}
class Bar extends Module with Contract {
val in = IO(Input(UInt(32.W))
val sign = IO(Input(Bool())
val out = IO(Output(UInt(32.W))
contract {
require(sign |-> in > 0.U)
require(!sign |-> in < 0.U)
// Ensures support SVA properties
ensure((sign |=> out > 0.U) &&
(!sign |=> out < 0.U))
}
val foo1 = Instantiate(Foo)
val foo2 = Instantiate(Foo)
/* body of bar that uses multiple Foos */
test formal testBar(500) {
val foo1 = Instantiate(Bar)
AssertProperty(/*some property*/)
}
}
object Bar extends App {
ChiselStage.emitBtor2(new Bar)
}
Chisel
sbt run
circuit Bar:
public module Foo:
input in : UInt<32>
output out : UInt<32>
contract:
node prec0 = gt(in, 0)
require prec0
node prec1 = lt(in, 1000)
require prec1
node post = ;;some post-condition;;
ensure post
;; Body of the module
;; Some Formal Test
public module _Foo:
input s_in : UInt<32>
inst dut of Foo
connect dut.in, s_in
intrinsic(circt_verif_assert(...))
formal testFoo of _Foo, bound = 500
public module Bar:
input in : UInt<32>
input sign : Bool
output out : UInt<32>
contract:
node prec0 = intrinsic(circt_ltl_implication(...))
require prec0
node prec1 = intrinsic(circt_ltl_implication(...))
require prec1
node post = ;;some post-condition;;
ensure post
inst foo1 of Foo
inst foo2 of Foo
;; body of bar that uses multiple Foos ;;
public module _Bar:
input s_in : UInt<32>
input s_en : Bool
inst dut of Bar
connect dut.in, s_in
connect dut.en, s_en
intrinsic(circt_verif_assert(...))
formal testBar of _Bar, bound = 500
FIRRTL
End-to-End Verification Flow
36
FIRRTL
lowerToHW
module {
hw.module @Foo(in %in : i32, out : i32) {
%foo.0 = verif.contract(%out) : i32 -> (i32) {
^bb0(%foo.0 : i32):
%c0_i32 = hw.constant 0 : i32
%prec0 = comb.icmp bin ugt %in, %c0_i32 : i32
verif.require %prec0 : i1
%c1000_i32 = hw.constant 1000 : i32
%prec1 = comb.icmp bin ult %in, c1000_i32 : i32
verif.require %prec1 : i1
%post = ...
verif.ensure %post : i1
verif.yield %foo.0 : i32
}
%out = ... : i32
hw.output %foo.0
}
verif.formal @testFoo(k = 500) {
%s_in = verif.symbolic_input : i32
%foo.0 = hw.instance "foo" @Foo(
in: %s_in : i32
) -> ("" : i32)
%spec = ...
verif.assert %spec : i1
}
CORE
hw.module @Bar(in %in : i32, in %sign : i1, out : i32) {
%bar.0 = verif.contract(%out) {
^bb0(%bar.0 : i32):
%c0_i32 = hw.constant 0 : i32
%ingt0 = comb.icmp bin ugt %in, %c0_i32 : i32
%prec0 = ltl.implication %sign, %ingt0 : !ltl.property
verif.require %prec0 : !ltl.property
%inlt0 = comb.icmp bin ult %in, %c0_i32 : i32
%true = hw.constant 1 : i1
%ns = comb.xor %sign, %true : i1
%prec1 = ltl.implication %ns, %inlt0 : !ltl.property
verif.require %prec1 : !ltl.property
verif.ensure …
verif.yield %bar.0 : i32
}
%foo1.0 = hw.instance "foo1" @Foo(...) -> (...)
%foo2.0 = hw.instance "foo2" @Foo(...) -> (...)
%out = ...
hw.output %bar.0
}
verif.formal @testBar(k = 500) {
%s_in = verif.symbolic_input : i32
%s_sign = verif.symbolic_input : i1
%bar.0 = hw.instance "dut" @Bar(
in: %s_in : i32, sign: %s_sign: i32
) -> ("" : i32)
%spec = ...
verif.assert %spec : i1
}
}
End-to-End Verification Flow
37
FIRRTL
lowerToHW
module {
hw.module @Foo(in %in : i32, out : i32) {
%foo.0 = verif.contract(%out) : i32 -> (i32) {
^bb0(%foo.0 : i32):
%c0_i32 = hw.constant 0 : i32
%prec0 = comb.icmp bin ugt %in, %c0_i32 : i32
verif.require %prec0 : i1
%c1000_i32 = hw.constant 1000 : i32
%prec1 = comb.icmp bin ult %in, c1000_i32 : i32
verif.require %prec1 : i1
%post = ...
verif.ensure %post : i1
verif.yield %foo.0 : i32
}
%out = ... : i32
hw.output %foo.0
}
verif.formal @testFoo(k = 500) {
%s_in = verif.symbolic_input : i32
%foo.0 = hw.instance "foo" @Foo(
in: %s_in : i32
) -> ("" : i32)
%spec = ...
verif.assert %spec : i1
}
CORE
hw.module @Bar(in %in : i32, in %sign : i1, out : i32) {
%bar.0 = verif.contract(%out) {
^bb0(%bar.0 : i32):
%c0_i32 = hw.constant 0 : i32
%ingt0 = comb.icmp bin ugt %in, %c0_i32 : i32
%prec0 = ltl.implication %sign, %ingt0 : !ltl.property
verif.require %prec0 : !ltl.property
%inlt0 = comb.icmp bin ult %in, %c0_i32 : i32
%true = hw.constant 1 : i1
%ns = comb.xor %sign, %true : i1
%prec1 = ltl.implication %ns, %inlt0 : !ltl.property
verif.require %prec1 : !ltl.property
verif.ensure …
verif.yield %bar.0 : i32
}
%foo1.0 = hw.instance "foo1" @Foo(...) -> (...)
%foo2.0 = hw.instance "foo2" @Foo(...) -> (...)
%out = ...
hw.output %bar.0
}
verif.formal @testBar(k = 500) {
%s_in = verif.symbolic_input : i32
%s_sign = verif.symbolic_input : i1
%bar.0 = hw.instance "dut" @Bar(
in: %s_in : i32, sign: %s_sign: i32
) -> ("" : i32)
%spec = ...
verif.assert %spec : i1
}
}
End-to-End Verification Flow
38
Core
PrepareForFormal
Formal
verif.formal @Bar(k = 500) {
%s_in = verif.symbolic_input : i32
%s_sign = verif.symbolic_input : i1
%bar.0 = verif.symbolic_input : i32
// body logic
%c0_i32 = hw.constant 0 : i32
%ingt0 = comb.icmp bin ugt %s_in, %c0_i32 : i32
%prec0 = ltl.implication %s_sign, %ingt0 : !ltl.property
verif.assume %prec0 : !ltl.property
%inlt0 = comb.icmp bin ult %s_in, %c0_i32 : i32
%true = hw.constant 1 : i1
%ns = comb.xor %s_sign, %true : i1
%prec1 = ltl.implication %ns, %inlt0 : !ltl.property
verif.assume %prec1 : !ltl.property
verif.assert …
// foo 1 instance
%foo1.0 = verif.symbolic_input : i32
%c0_i32 = hw.constant 0 : i32
%prec0 = comb.icmp bin ugt %..., %c0_i32 : i32
verif.assert %prec0 : i1
%c1000_i32 = hw.constant 1000 : i32
%prec1 = comb.icmp bin ult %..., c1000_i32 : i32
verif.assert %prec1 : i1
%post = ...
verif.assume %post : i1
// foo 2 instance
%foo2.0 = verif.symbolic_input : i32
%prec0_0 = comb.icmp bin ugt %..., %c0_i32 : i32
verif.assert %prec0_0 : i1
%prec1_0 = comb.icmp bin ult %..., c1000_i32 : i32
verif.assert %prec1_0 : i1
%post_0 = ...
verif.assume %post_0 : i1
%out = ...
}
module {
// k can be a pass argument for modules
verif.formal @Foo(k = 500) {
%s_in = verif.symbolic_input : i32
%foo.0 = verif.symbolic_input : i32
%c0_i32 = hw.constant 0 : i32
%prec0 = comb.icmp bin ugt %in, %c0_i32 : i32
verif.assume %prec0 : i1
%c1000_i32 = hw.constant 1000 : i32
%prec1 = comb.icmp bin ult %in, c1000_i32 : i32
verif.assume %prec1 : i1
%post = ...
verif.assert %post : i1
// rest of the module
}
verif.formal @testFoo(k = 500) {
%s_in = verif.symbolic_input : i32
%foo.0 = verif.symbolic_input : i32
%c0_i32 = hw.constant 0 : i32
%prec0 = comb.icmp bin ugt %in, %c0_i32 : i32
verif.assert %prec0 : i1
%c1000_i32 = hw.constant 1000 : i32
%prec1 = comb.icmp bin ult %in, c1000_i32 : i32
verif.assert %prec1 : i1
%post = ...
verif.assume %post : i1
%spec = ...
verif.assert %spec : i1
}
verif.formal @testBar(k = 500) {
%s_in = verif.symbolic_input : i32
%s_sign = verif.symbolic_input : i1
%bar.0 = verif.symbolic_input : i32
%c0_i32 = hw.constant 0 : i32
%ingt0 = comb.icmp bin ugt %s_in, %c0_i32 : i32
%prec0 = ltl.implication %s_sign, %ingt0 : !ltl.property
verif.assert %prec0 : !ltl.property
%inlt0 = comb.icmp bin ult %s_in, %c0_i32 : i32
%true = hw.constant 1 : i1
%ns = comb.xor %s_sign, %true : i1
%prec1 = ltl.implication %ns, %inlt0 : !ltl.property
verif.assert %prec1 : !ltl.property
verif.assume ...
%spec = ...
verif.assert %spec : i1
}
}
End-to-End Verification Flow
39
Core
PrepareForFormal
Formal
verif.formal @Bar(k = 500) {
%s_in = verif.symbolic_input : i32
%s_sign = verif.symbolic_input : i1
%bar.0 = verif.symbolic_input : i32
// body logic
%c0_i32 = hw.constant 0 : i32
%ingt0 = comb.icmp bin ugt %s_in, %c0_i32 : i32
%prec0 = ltl.implication %s_sign, %ingt0 : !ltl.property
verif.assume %prec0 : !ltl.property
%inlt0 = comb.icmp bin ult %s_in, %c0_i32 : i32
%true = hw.constant 1 : i1
%ns = comb.xor %s_sign, %true : i1
%prec1 = ltl.implication %ns, %inlt0 : !ltl.property
verif.assume %prec1 : !ltl.property
verif.assert …
// foo 1 instance
%foo1.0 = verif.symbolic_input : i32
%c0_i32 = hw.constant 0 : i32
%prec0 = comb.icmp bin ugt %..., %c0_i32 : i32
verif.assert %prec0 : i1
%c1000_i32 = hw.constant 1000 : i32
%prec1 = comb.icmp bin ult %..., c1000_i32 : i32
verif.assert %prec1 : i1
%post = ...
verif.assume %post : i1
// foo 2 instance
%foo2.0 = verif.symbolic_input : i32
%prec0_0 = comb.icmp bin ugt %..., %c0_i32 : i32
verif.assert %prec0_0 : i1
%prec1_0 = comb.icmp bin ult %..., c1000_i32 : i32
verif.assert %prec1_0 : i1
%post_0 = ...
verif.assume %post_0 : i1
%out = ...
}
module {
// k can be a pass argument for modules
verif.formal @Foo(k = 500) {
%s_in = verif.symbolic_input : i32
%foo.0 = verif.symbolic_input : i32
%c0_i32 = hw.constant 0 : i32
%prec0 = comb.icmp bin ugt %in, %c0_i32 : i32
verif.assume %prec0 : i1
%c1000_i32 = hw.constant 1000 : i32
%prec1 = comb.icmp bin ult %in, c1000_i32 : i32
verif.assume %prec1 : i1
%post = ...
verif.assert %post : i1
// rest of the module
}
verif.formal @testFoo(k = 500) {
%s_in = verif.symbolic_input : i32
%foo.0 = verif.symbolic_input : i32
%c0_i32 = hw.constant 0 : i32
%prec0 = comb.icmp bin ugt %in, %c0_i32 : i32
verif.assert %prec0 : i1
%c1000_i32 = hw.constant 1000 : i32
%prec1 = comb.icmp bin ult %in, c1000_i32 : i32
verif.assert %prec1 : i1
%post = ...
verif.assume %post : i1
%spec = ...
verif.assert %spec : i1
}
verif.formal @testBar(k = 500) {
%s_in = verif.symbolic_input : i32
%s_sign = verif.symbolic_input : i1
%bar.0 = verif.symbolic_input : i32
%c0_i32 = hw.constant 0 : i32
%ingt0 = comb.icmp bin ugt %s_in, %c0_i32 : i32
%prec0 = ltl.implication %s_sign, %ingt0 : !ltl.property
verif.assert %prec0 : !ltl.property
%inlt0 = comb.icmp bin ult %s_in, %c0_i32 : i32
%true = hw.constant 1 : i1
%ns = comb.xor %s_sign, %true : i1
%prec1 = ltl.implication %ns, %inlt0 : !ltl.property
verif.assert %prec1 : !ltl.property
verif.assume ...
%spec = ...
verif.assert %spec : i1
}
}
PrepareForFormal
40
End-to-End Verification Flow
41
Formal
ExportBtor2
module {
verif.formal @Foo(k = 500) {
…
}
verif.formal @testFoo(k = 500) {
…
}
verif.formal @Bar(k = 500) {
…
}
verif.formal @testBar(k = 500) {
…
}
}
foo.btor2
testFoo.btor2
bar.btor2
testBar.btor2
btormc
If all unsat
If one sat
counter-example
Overview:
42
What has been done?:
43
Future Work:
44
Conclusion
⇒ This is a WIP, the core building blocks and passes are there, still need to provide Chisel interfaces and connect everything together.
→ Please reach out if you want to help out!
45
What has been done?:
46