Skip to content

Instantly share code, notes, and snippets.

@thoughtpolice
Last active January 21, 2023 07:01
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thoughtpolice/f339e8aa55dbf2dd4f2ba63fa517410f to your computer and use it in GitHub Desktop.
Save thoughtpolice/f339e8aa55dbf2dd4f2ba63fa517410f to your computer and use it in GitHub Desktop.
Formal verification of a Clash circuit, using Yosys/SymbiYosys.
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE TypeFamilies #-}
module Assert
( assertProperty
, initialAssume
, getReset
) where
import Clash.Prelude
import Clash.Signal.Internal
-- | Assert a SystemVerilog(-ish) property over a given @'Signal'@, returning
-- another @'Signal'@.
--
-- During both simulation and synthesis, @'assertProperty' b v@ is equivalent to
-- @v@. However, during verification (with @yosys-smtbmc@ or @symbiyosys@), this
-- asserts that the given @'Bool'@ @'Signal'@ always holds @'True'@.
assertProperty
:: Signal dom Bool
-- ^ The assertion to check. This is a stateful @'Signal'@ which is assumed to
-- be some property to check over a given circuit.
-> Signal dom a
-- ^ Input @'Signal'@. This value is simply returned.
-> Signal dom a
-- ^ Output @'Signal'@. Identical to the input @'Signal'@.
assertProperty = \_ x -> x
{-# NOINLINE assertProperty #-}
-- | Assume a SystemVerilog(-ish) property for the initial clock cycle, over
-- a given @'Signal'@, returning another @'Signal'@.
--
-- During both simulation and synthesis, @'initialAssume' b v@ is equivalent to
-- @v@. However, during verification (with @yosys-smtbmc@ or @symbiyosys@), this
-- introduces the necessary assumption that the @'Bool'@ holds true on the
-- initial clock cycle; i.e. it tells that the given @'Bool'@ @'Signal'@ holds
-- @'True'@ the verification tool to only consider SMT traces where this case is
-- true.
initialAssume
:: Signal dom Bool
-- ^ Signal to assume true on the initial clock cycle.
-> Signal dom a
-- ^ Input @'Signal'@. This value is simply returned.
-> Signal dom a
-- ^ Output @'Signal'@. Identical to the input @'Signal'@.
initialAssume = \_ x -> x
{-# NOINLINE initialAssume #-}
-- | Convert an explicit @'Reset'@ into a @'Bool'@ @'Signal'@. This is often
-- used to assert/assume things about the reset during verification (e.g.
-- @'initialAssume'@ that the given @'Reset'@ is set on the first clock cycle,
-- to initialize registers).
getReset
:: Reset dom sync
-- ^ @'Reset'@ to convert.
-> Signal dom Bool
-- ^ @'Bool'@ @'Signal'@ for the given @'Reset'@.
getReset (Async rst) = rst
getReset (Sync rst) = rst
{-# INLINEABLE getReset #-}
[ { "BlackBox" :
{ "name" : "Assert.assertProperty"
, "templateD" : "// assertProperty begin
`ifdef FORMAL
assert property (~ARG[0]);
`endif
assign ~RESULT = ~ARG[1];
// assertProperty end"
}
},
{ "BlackBox" :
{ "name" : "Assert.initialAssume"
, "templateD" : "// initialAssume begin
`ifdef FORMAL
initial assume (~ARG[0]);
`endif
assign ~RESULT = ~ARG[1];
// initialAssume end"
}
}
]
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -fplugin GHC.TypeLits.Normalise #-}
{-# OPTIONS_GHC -fplugin GHC.TypeLits.Extra.Solver #-}
{-# OPTIONS_GHC -fplugin GHC.TypeLits.KnownNat.Solver #-}
module Counter
( top
) where
import GHC.Stack
import Clash.Prelude
import TopGen
import Assert
-- | Simple verification harness, for time-invariant properties over a
-- @'Signal'@, with a given @'Clock'@ and @'Reset'@ line. Given a clocked signal
-- @v@, the circuit @'verify' prop v@ asserts that @prop v@ should always be
-- @'True'@ for every clock cycle. @'verify'@ also abstracts over the
-- synchronous reset logic and properly assumes existence of the reset signal,
-- or that the property is upheld.
verify
:: ( HasCallStack
, HasClockReset dom gate sync
)
=> (a -> Bool)
-> Signal dom a
-> Signal dom a
verify f s
= initialAssume rst -- assume: rst is high on clk #0
$ assertProperty (rst .||. prop) -- assert: either rst is high, or prop is ok
$ s
where
-- property: the output counter signal is always lower than the specified
-- limit, which is 32, in this case.
prop = fmap f s
-- reset line, converted to a Bool, used for our verification assumptions.
-- 'hasReset' ties the explicit reset argument to our implicit reset line.
rst = getReset hasReset
-- | A counter circuit, that counts from 0 to 15 (inclusive) and then resets
-- to zero. This counter requires a clock and reset line to be attached.
counter
:: ( HasCallStack -- ^ Constraint: Contains a callstack
, HasClockReset dom gate sync -- ^ Constraint: clock and reset lines
)
=> Signal dom (Unsigned 6) -- ^ Output: unsigned 6-bit number.
counter = verify prop out
where
-- property: the output counter signal is always lower than the specified
-- limit, which is 32, in this case.
prop = (< lim) where lim = 32 :: Unsigned 6
-- the counter: counts from 0 to 15, and resets back to 0. this is defined
-- as a feedback loop using a register, and it simply maps a pure function
-- onto the stateful component to apply the logic.
out = register 0 (fmap f out)
f x | x == 15 = 0
| otherwise = x + 1
-- | Exported @'TopEntity'@. Ties @'counter'@ to an appropriate @'Clock'@ and
-- @'Reset'@.
top
:: "clk" ::: Clock System 'Source
-> "rst" ::: Reset System 'Synchronous
-> "out" ::: Signal System (Unsigned 6)
top clk rst = withClockReset clk rst counter
$(makeTopEntity 'top) -- auto generate
[options]
mode prove
depth 17
[engines]
smtbmc
[script]
read_verilog -formal top.v
prep -top top
[files]
verilog/Counter/top/top.v
[nix-shell:~/src/clash-playground/src/fpga]$ clash --verilog Counter.hs && sby -f counter.sby
Loading dependencies took 2.602479004s
Compiling: Counter.top
Applied 61 transformations
Normalisation took 0.727354546s
Netlist generation took 0.002188659s
Total compilation took 3.333886118s
SBY [counter] Removing direcory 'counter'.
SBY [counter] Copy 'verilog/Counter/top/top.v' to 'counter/src/top.v'.
SBY [counter] engine_0: smtbmc
SBY [counter] script: starting process "cd counter/src; yosys -ql ../model/design.log ../model/design.ys"
SBY [counter] script: finished (returncode=0)
SBY [counter] smt2: starting process "cd counter/model; yosys -ql design_smt2.log design_smt2.ys"
SBY [counter] smt2: finished (returncode=0)
SBY [counter] engine_0.basecase: starting process "cd counter; yosys-smtbmc --noprogress -t 17 --append 0 --dump-vcd engine_0/trace.vcd --dump-vlogtb engine_0/trace_tb.v --dump-smtc engine_0/trace.smtc model/design_smt2.smt2"
SBY [counter] engine_0.induction: starting process "cd counter; yosys-smtbmc --noprogress -i -t 17 --append 0 --dump-vcd engine_0/trace_induct.vcd --dump-vlogtb engine_0/trace_induct_tb.v --dump-smtc engine_0/trace_induct.smtc model/design_smt2.smt2"
SBY [counter] engine_0.basecase: ## 0 0:00:00 Solver: yices
SBY [counter] engine_0.basecase: ## 0 0:00:00 Checking assertions in step 0..
SBY [counter] engine_0.basecase: ## 0 0:00:00 Checking assertions in step 1..
SBY [counter] engine_0.basecase: ## 0 0:00:00 Checking assertions in step 2..
SBY [counter] engine_0.basecase: ## 0 0:00:00 Checking assertions in step 3..
SBY [counter] engine_0.basecase: ## 0 0:00:00 Checking assertions in step 4..
SBY [counter] engine_0.basecase: ## 0 0:00:00 Checking assertions in step 5..
SBY [counter] engine_0.basecase: ## 0 0:00:00 Checking assertions in step 6..
SBY [counter] engine_0.basecase: ## 0 0:00:00 Checking assertions in step 7..
SBY [counter] engine_0.basecase: ## 0 0:00:00 Checking assertions in step 8..
SBY [counter] engine_0.basecase: ## 0 0:00:00 Checking assertions in step 9..
SBY [counter] engine_0.basecase: ## 0 0:00:00 Checking assertions in step 10..
SBY [counter] engine_0.basecase: ## 0 0:00:00 Checking assertions in step 11..
SBY [counter] engine_0.basecase: ## 0 0:00:00 Checking assertions in step 12..
SBY [counter] engine_0.basecase: ## 0 0:00:00 Checking assertions in step 13..
SBY [counter] engine_0.basecase: ## 0 0:00:00 Checking assertions in step 14..
SBY [counter] engine_0.basecase: ## 0 0:00:00 Checking assertions in step 15..
SBY [counter] engine_0.basecase: ## 0 0:00:00 Checking assertions in step 16..
SBY [counter] engine_0.basecase: ## 0 0:00:00 Status: PASSED
SBY [counter] engine_0.induction: ## 0 0:00:00 Solver: yices
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 17..
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 16..
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 15..
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 14..
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 13..
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 12..
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 11..
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 10..
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 9..
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 8..
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 7..
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 6..
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 5..
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 4..
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 3..
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 2..
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 1..
SBY [counter] engine_0.induction: ## 0 0:00:00 Trying induction in step 0..
SBY [counter] engine_0.induction: ## 0 0:00:00 Temporal induction successful.
SBY [counter] engine_0.induction: ## 0 0:00:00 Status: PASSED
SBY [counter] engine_0.basecase: finished (returncode=0)
SBY [counter] engine_0: Status returned by engine for basecase: PASS
SBY [counter] engine_0.induction: finished (returncode=0)
SBY [counter] engine_0: Status returned by engine for induction: PASS
SBY [counter] summary: Elapsed clock time [H:MM:SS (secs)]: 0:00:00 (0)
SBY [counter] summary: Elapsed process time [H:MM:SS (secs)]: 0:00:00 (0)
SBY [counter] summary: engine_0 (smtbmc) returned PASS for basecase
SBY [counter] summary: engine_0 (smtbmc) returned PASS for induction
SBY [counter] summary: successful proof by k-induction.
SBY [counter] DONE (PASS, rc=0)
[nix-shell:~/src/clash-playground/src/fpga]$
{-# OPTIONS_GHC -Wall -Wno-orphans #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveLift #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE ViewPatterns #-}
module TopGen
( makeTopEntity
) where
-- base
import Prelude
-- template-haskell
import Language.Haskell.TH
import Language.Haskell.TH.Syntax
-- clash
import Clash.NamedTypes ((:::))
import Clash.Annotations.TopEntity
--
-- utilities
--
-- matches against `"nam" ::: ty` using (:::) from Clash, but we
-- can't check the type constructor here :(
pattern NamedTy :: Type -> String -> Type
pattern NamedTy con nam <-
SigT (AppT (AppT con (LitT (StrTyLit nam))) _) _
-- matches a type `a -> b`
pattern ArrowTy :: Type -> Type -> Type
pattern ArrowTy a b = AppT (AppT ArrowT a) b
-- orphans
deriving instance Lift PortName
deriving instance Lift TopEntity
isTupT :: Type -> Bool
isTupT (AppT x _) = isTupT x
isTupT (TupleT _) = True
isTupT _ = False
collectTupT :: Type -> [Type]
collectTupT (AppT (TupleT _) v) = [v]
collectTupT (AppT f v) = collectTupT f ++ collectTupT v
collectTupT (TupleT _) = []
collectTupT x = [x]
-- | Given an multi-arity function type @f :: a -> b -> c -> ...@, get
-- the final return type.
getReturnTy :: Type -> Q Type
getReturnTy (ArrowTy _ b) = getReturnTy b
getReturnTy b = return b
-- | Automatically create a @'TopEntity'@ for a given @Name@.
makeTopEntity :: Name -> DecsQ
makeTopEntity n = reify n >>= \case
VarI nam typ _ -> do
-- helpers
let prag t = PragmaD (AnnP (valueAnnotation nam) t)
-- get a Name for this type operator so we can check it
-- in the ArrowTy case
nty <- [t| (:::) |]
-- examine the arguments
let examine ty = go ty [] where
go (ArrowTy (NamedTy con a) b) xs
| con == nty
= do { v <- go b xs; return (a:v) }
go (ArrowTy _ _) _
= fail $ "makeTopEntity: All types for an autogenerated "
++ "top-entity (both input and output) must have a name!"
go _ xs = return xs
ins <- map PortName <$> examine typ
out <- getReturnTy typ >>= \case
-- single output
NamedTy con r | con == nty -> return $ PortName r
-- multi output
xs@(isTupT -> True) -> do
let go (NamedTy con r) | con == nty = return (PortName r)
go _ = fail $ "makeTopEntity: Output tuple must have "
++ "names for all components!"
PortField mempty <$> mapM go (collectTupT xs)
-- invalid type
_ -> fail "makeTopEntity: Invalid return type!"
-- Return the annotation
top <- lift $ TopEntity
{ t_name = nameBase nam
, t_inputs = ins
, t_output = out
}
return [prag top]
-- failure case: we weren't provided with the name of a binder
_ -> fail "makeTopEntity: invalid Name, must be a top-level binding!"
@blaxill
Copy link

blaxill commented Jul 8, 2019

Hey this is great- very informative. Your makeTopEntity is also widely applicable. Have you thought about upstreaming it? If you are busy I could do it and reference this gist.

@thoughtpolice
Copy link
Author

@blaxill Yes, I have thought about upstreaming makeTopEntity, though it's incredibly ugly (IMO), and I think when I initially discussed it with Christiaan, it had a few deficiencies, I think -- this one, for example, does support tuples for the return type, but not nested tuples (which is a natural extension, IMO, but perhaps a minor use case,) nor does it support tuples for the input types. That might not matter so much in reality though, I just never got around to generalizing it for those cases. And that could always be fixed later with little API impact, too.

I'm not actively writing much Clash at the moment (beyond some spare side projects I occasionally pick up), so at the moment I don't have time to upstream it -- feel free to do so yourself. If you want to do that, I also suggest you start with this version instead -- this version of TopGen.hs in my clash-playground repository has extra docs, as well as an extra makeTopEntityWithName so you can choose the entity name, beyond just using the reified Haskell function name (this version is an evolution of the original one in this gist)

https://github.com/thoughtpolice/clash-playground/blob/master/src/TopGen.hs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment