----------------------------------------------------------------------------- -- | -- Module : Data.SBV.Tools.BoundedFix -- Copyright : (c) Levent Erkok -- License : BSD3 -- Maintainer: erkokl@gmail.com -- Stability : experimental -- -- Bounded fixed-point unrolling. ----------------------------------------------------------------------------- {-# LANGUAGE FlexibleContexts #-} {-# OPTIONS_GHC -Wall -Werror #-} module Data.SBV.Tools.BoundedFix ( bfix ) where import Data.SBV -- Doctest only: -- $setup -- >>> bfac = bfix 10 "fac" fact where fact f n = ite (n .== 0) 1 ((n :: SInteger) * f (n-1)) -- | Bounded fixed-point operation. The call @bfix bnd nm f@ unrolls the recursion in @f@ at most -- @bnd@ times, and uninterprets the function (with the name @nm@) after the bound is reached. -- -- This combinator is handy for dealing with recursive definitions that are not symbolically terminating -- and when the property we are interested in does not require an infinite unrolling, or when we are happy -- with a bounded proof. In particular, this operator can be used as a basis of software-bounded model -- checking algorithms built on top of SBV. The bound can be successively refined in a CEGAR like loop -- as necessary, by analyzing the counter-examples and rejecting them if they are false-negatives. -- -- For instance, we can define the factorial function using the bounded fixed-point operator like this: -- -- @ -- bfac :: SInteger -> SInteger -- bfac = bfix 10 "fac" fact -- where fact f n = ite (n .== 0) 1 (n * f (n-1)) -- @ -- -- This definition unrolls the recursion in factorial at most 10 times before uninterpreting the result. -- We can now prove: -- -- >>> prove $ \n -> n .>= 1 .&& n .<= 9 .=> bfac n .== n * bfac (n-1) -- Q.E.D. -- -- And we would get a bogus counter-example if the proof of our property needs a larger bound: -- -- >>> prove $ \n -> n .== 10 .=> bfac n .== 3628800 -- Falsifiable. Counter-example: -- s0 = 10 :: Integer -- <BLANKLINE> -- fac :: Integer -> Integer -- fac _ = 2 -- -- The counter-example is telling us how it instantiated the function @fac@ when the recursion -- bottomed out: It simply made it return @2@ for all arguments at that point, which provides -- the (unintended) counter-example. -- -- By design, if a function defined via `bfix` is given a concrete argument, it will unroll -- the recursion as much as necessary to complete the call (which can of course diverge). The bound -- only applies if the given argument is symbolic. This fact can be used to observe concrete -- values to see where the bounded-model-checking approach fails: -- -- >>> prove $ \n -> n .== 10 .=> observe "bfac_n" (bfac n) .== observe "bfac_10" (bfac 10) -- Falsifiable. Counter-example: -- bfac_10 = 3628800 :: Integer -- bfac_n = 7257600 :: Integer -- s0 = 10 :: Integer -- <BLANKLINE> -- fac :: Integer -> Integer -- fac _ = 2 -- -- Here, we see further evidence that the SMT solver must have decided to assign the -- value @2@ in the final call just as it was reaching the base case, and thus got the -- final result incorrect. (Note that @7257600 = 2 * 3628800@.) A wrapper algorithm can -- then assert the actual value of @bfac 10@ here as an extra constraint and can -- search for "deeper bugs." bfix :: (SymVal a, Uninterpreted (SBV a -> r)) => Int -> String -> ((SBV a -> r) -> (SBV a -> r)) -> SBV a -> r bfix bound nm f x | isConcrete x = g x | True = unroll bound x where g = f g unroll 0 = uninterpret nm unroll i = f (unroll (i-1))