packages feed

sbv-14.4: SBVTestSuite/TestSuite/Basics/TPCaching.hs

-----------------------------------------------------------------------------
-- |
-- Module    : TestSuite.Basics.TPCaching
-- Copyright : (c) Levent Erkok
-- License   : BSD3
-- Maintainer: erkokl@gmail.com
-- Stability : experimental
--
-- Tests for the TP proof caching mechanism.
-----------------------------------------------------------------------------

{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeAbstractions    #-}

{-# OPTIONS_GHC -Wall -Werror #-}

module TestSuite.Basics.TPCaching(tests) where

import Utils.SBVTestFramework

import Data.SBV.TP (TP, Proof, runTPWith, lemma, calc, recall, tpStats, (|-), (=:), qed)

import Control.Monad (void)
import Control.Exception (try, SomeException)

import Data.Char (isSpace)
import Data.List (isPrefixOf, dropWhileEnd)

import Control.DeepSeq (($!!))

-- | Strip timing info like @[0.05s]@ from the end of output lines.
-- Only matches brackets whose content looks like a time value (digits, dots, and 's').
-- Handles multiple consecutive timings like @[0.001s][0.002s]@.
stripTiming :: String -> String
stripTiming s = reverse $ go $ reverse $ dropWhileEnd isSpace s
 where go (']':rest) | (inner, '[':before) <- break (== '[') rest
                     , all (`elem` ("0123456789.s" :: String)) inner
                     = go $ dropWhile isSpace before
       go xs = xs

-- | Filter out the statistics summary line from verbose output.
isStatsLine :: String -> Bool
isStatsLine s = "[SBV:" `isPrefixOf` dropWhile isSpace s

-- | Clean captured verbose output: strip timing and stats.
cleanStatsOutput :: String -> String
cleanStatsOutput = unlines . map stripTiming . filter (not . isStatsLine) . lines

-- Test suite
tests :: TestTree
tests = testGroup "Basics.TPCaching"
   [
   -- Normal mode: recall when cache is empty (cache miss).
   -- The proof runs from scratch; recallWith shows "Lemma:" one-liner.
     goldenCapturedIO "tpCache_miss" $ \rf -> do
        let cfg = z3 { redirectVerbose = Just rf }
        void $ runTPWith cfg $
           recall (lemma "fact" sTrue [])

   -- Normal mode: direct proof then recall (cache hit).
   -- The direct proof shows "Lemma:", the recall shows "Lemma: ... [Cached]".
   , goldenCapturedIO "tpCache_hit" $ \rf -> do
        let cfg = z3 { redirectVerbose = Just rf }
        void $ runTPWith cfg $ do
           _ <- lemma "fact" sTrue []
           recall (lemma "fact" sTrue [])

   -- Normal mode: same proposition proved under two names, then recalled (aliases).
   -- The recall shows "Lemma: ... [Cached]" with "(a.k.a. ...)" listing the other name.
   , goldenCapturedIO "tpCache_alias" $ \rf -> do
        let cfg = z3 { redirectVerbose = Just rf }
        void $ runTPWith cfg $ do
           _ <- lemma "nameA" sTrue []
           _ <- lemma "nameB" sTrue []
           recall (lemma "nameC" sTrue [])

   -- Normal mode: calc proof with steps, then recall (cache hit).
   -- The direct proof shows each step; the recall collapses to one line.
   , goldenCapturedIO "tpCache_calcCollapse" $ \rf -> do
        let cfg = z3 { redirectVerbose = Just rf }
        void $ runTPWith cfg $ do
           let addZeroProof = calc "addZero"
                                   (\(Forall @"x" (x :: SInteger)) -> x + 0 .== x) $
                                   \x -> [] |- x + 0
                                            =: (x :: SInteger)
                                            =: qed
           _ <- addZeroProof
           recall addZeroProof

   -- Normal mode: nested recall.
   -- First run proves inner and outer. Second run (via recall) hits cache for outer.
   , goldenCapturedIO "tpCache_nested" $ \rf -> do
        let cfg = z3 { redirectVerbose = Just rf }
        void $ runTPWith cfg $ do
           let myProof = do _ <- recall (lemma "inner" sTrue [])
                            lemma "outer" sTrue []
           _ <- myProof
           recall myProof

   -- Stats mode: recall when cache is empty (cache miss).
   -- In stats mode, recall does NOT suppress inner output, so full proof steps are visible.
   , goldenCapturedIO "tpCache_statsMiss" $ \rf -> do
        let cfg = (tpStats z3) { redirectVerbose = Just rf }
        void $ runTPWith cfg $
           recall (calc "addZero"
                        (\(Forall @"x" (x :: SInteger)) -> x + 0 .== x) $
                        \x -> [] |- x + 0
                                 =: (x :: SInteger)
                                 =: qed)
        contents <- readFile rf
        writeFile rf $!! cleanStatsOutput contents

   -- Stats mode: direct proof then recall (cache hit).
   -- Direct proof shows full steps; recall shows "Lemma: ... [Cached]" one-liner.
   , goldenCapturedIO "tpCache_statsHit" $ \rf -> do
        let cfg = (tpStats z3) { redirectVerbose = Just rf }
        void $ runTPWith cfg $ do
           let addZeroProof = calc "addZero"
                                   (\(Forall @"x" (x :: SInteger)) -> x + 0 .== x) $
                                   \x -> [] |- x + 0
                                            =: (x :: SInteger)
                                            =: qed
           _ <- addZeroProof
           recall addZeroProof
        contents <- readFile rf
        writeFile rf $!! cleanStatsOutput contents

   -- Stats mode: nested recall showing inner cache dynamics.
   -- First recall misses (shows full inner proofs). Second recall hits (shows "Lemma: ... [Cached]").
   , goldenCapturedIO "tpCache_statsNested" $ \rf -> do
        let cfg = (tpStats z3) { redirectVerbose = Just rf }
        void $ runTPWith cfg $ do
           _ <- recall (lemma "inner" sTrue [])
           _ <- lemma "outer" sTrue []
           _ <- recall (lemma "inner" sTrue [])
           recall (lemma "outer" sTrue [])
        contents <- readFile rf
        writeFile rf $!! cleanStatsOutput contents

   -- Recall of a failing proof: the lemma is false (x > x), so the proof should fail.
   , goldenCapturedIO "tpCache_recallFail" $ \rf -> do
        let cfg = z3 { redirectVerbose = Just rf }
        res <- try $ void $ runTPWith cfg $
           recall bad
        case res of
           Left  (_ :: SomeException) -> pure ()
           Right _                    -> appendFile rf "Unexpected success\n"

   -- Direct proof of a false lemma.
   , goldenCapturedIO "tpCache_fooFail" $ \rf -> do
        let cfg = z3 { redirectVerbose = Just rf }
        res <- try $ void $ runTPWith cfg foo
        case res of
           Left  (_ :: SomeException) -> pure ()
           Right _                    -> appendFile rf "Unexpected success\n"

   -- Recall of a failing lemma inside a larger proof.
   , goldenCapturedIO "tpCache_barFail" $ \rf -> do
        let cfg = z3 { redirectVerbose = Just rf }
        res <- try $ void $ runTPWith cfg bar
        case res of
           Left  (_ :: SomeException) -> pure ()
           Right _                    -> appendFile rf "Unexpected success\n"
   ]

-- | A trivially false lemma, used to test recall of a failing proof.
bad :: TP (Proof (Forall "x" Integer -> SBool))
bad = lemma "bad" (\(Forall @"x" (x :: SInteger)) -> x .> x) []

-- | A false lemma: x == x+1.
foo :: TP (Proof (Forall "x" Integer -> SBool))
foo = lemma "foo" (\(Forall @"x" (x :: SInteger)) -> x .== x + 1) []

-- | Recalls foo (which fails), then tries to prove another false lemma.
bar :: TP (Proof (Forall "x" Integer -> SBool))
bar = do _f <- recall foo
         lemma "bar" (\(Forall @"x" (x :: SInteger)) -> x .== x + 2) []