Skip to content

Commit ca626c0

Browse files
Merge branch 'develop-what4-respect-quantifiers-T254'. Close #254.
**Description** `Copilot.Language.Spec.prop` and `Copilot.Language.Spec.theorem` are both reified into as a bare stream of booleans, and do not retain information about whether the `Prop` passed as argument was a `Forall` or an `Exists`. Because this information is not retained when later using `Copilot.Theorem.Prove.prove` to check the properties, the results returned by the SMT solvers may be incorrect. **Type** - Bug: Information is lost, leading to unexpected results . **Additional context** None. **Requester** - Rob Dockins. **Method to check presence of bug** The issue can be detected with the following sample programs: ```haskell {-# LANGUAGE NoImplicitPrelude #-} module Main (main) where import Data.Foldable import Data.Functor import Copilot.Theorem.What4 import Language.Copilot trueThenFalses :: Stream Bool trueThenFalses = [True] ++ constant False spec :: Spec spec = do void $ prop "forAll trueThenFalses (should be invalid)" (forAll trueThenFalses) void $ prop "exists trueThenFalses (should be valid)" (exists trueThenFalses) main :: IO () main = do spec' <- reify spec results <- prove Z3 spec' forM_ results $ \(nm, res) -> do putStr $ nm <> ": " case res of Valid -> putStrLn "valid" Invalid -> putStrLn "invalid" Unknown -> putStrLn "unknown" ``` The following Dockerfile runs the prover on a Copilot spec that proves an existential, which produces an exception caught by the program (as expected), after which it prints the message "Success". If an exception is not caught, the program exits with an error code. ``` --- Dockerfile-verify-254 FROM ubuntu:focal ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update RUN apt-get install --yes \ libz-dev \ git \ curl \ gcc \ g++ \ make \ libgmp3-dev \ pkg-config \ z3 RUN mkdir -p $HOME/.ghcup/bin RUN curl https://downloads.haskell.org/~ghcup/0.1.40.0/x86_64-linux-ghcup-0.1.40.0 -o $HOME/.ghcup/bin/ghcup RUN chmod a+x $HOME/.ghcup/bin/ghcup ENV PATH=$PATH:/root/.ghcup/bin/ ENV PATH=$PATH:/root/.cabal/bin/ SHELL ["/bin/bash", "-c"] RUN ghcup install ghc 9.10.1 RUN ghcup install cabal 3.2 RUN ghcup set ghc 9.10.1 RUN cabal update ADD What4Existential.hs /tmp/What4Existential.hs SHELL ["/bin/bash", "-c"] CMD git clone $REPO && cd $NAME && git checkout $COMMIT && cd .. \ && cabal v1-sandbox init \ && cabal v1-install alex happy --constraint='happy <= 2' \ && cabal v1-install $NAME/copilot**/ \ && cabal v1-exec -- runhaskell /tmp/What4Existential.hs \ && echo "Success" --- What4Existential.hs {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE ScopedTypeVariables #-} module Main (main) where import Control.Exception ( SomeException, handle ) import Data.Foldable ( forM_ ) import Data.Functor ( void ) import System.Exit ( exitFailure, exitSuccess ) import Copilot.Theorem.What4 import Language.Copilot main :: IO () main = do handle (\(_ :: SomeException) -> exitSuccess) $ do proveSpec specF proveSpec specE putStrLn "The What4 backend tried to prove an existential proposition" exitFailure proveSpec :: Spec -> IO () proveSpec spec = do spec' <- reify spec results <- prove Z3 spec' forM_ results $ \(nm, res) -> do putStr $ nm <> ": " case res of Valid -> putStrLn "valid" Invalid -> putStrLn "invalid" Unknown -> putStrLn "unknown" specF :: Spec specF = void $ prop "forAll trueThenFalses (should be invalid)" (forAll trueThenFalses) specE :: Spec specE = void $ prop "exists trueThenFalses (shouldn't have a known result)" (exists trueThenFalses) trueThenFalses :: Stream Bool trueThenFalses = [True] ++ constant False ``` Command (substitute variables based on new path after merge): ``` $ docker run -e "REPO=https://github.com/Copilot-Language/copilot" -e "NAME=copilot" -e "COMMIT=<HASH>" -it copilot-verify-254 ``` **Expected result** Running the specs above or a similar variant via What4 should yield `valid` when the property is indeed valid (the second property above), indicating that respects the quantifier is being respected. If the prover is not able to prove a property of that kind, a suitable error message should be printed. Running the dockerfile above prints "Success", indicating that copilot-theorem correctly detected that one of the properties is existential and cannot be handled by the What4 backend. **Proposed solution** Modify `copilot-core` to introduce a type to represent existentially or universally quantified properties, keeping enough information about the quantifier. Modify `copilot-language` to carry enough information about the quantifier used. Modify `copilot-theorem` to ensure a proper encoding of the property when connecting to What4, and refuse to prove the property when the result would be incorrect by throwing an exception. Modify `copilot-prettyprinter` to handle the new type of quantified properties. **Further notes** None.
2 parents 0845db1 + 700508a commit ca626c0

File tree

13 files changed

+189
-36
lines changed

13 files changed

+189
-36
lines changed

copilot-core/CHANGELOG

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
2025-02-24
1+
2025-02-28
22
* Fix typo in documentation. (#587)
33
* Add a Show instance for Type. (#589)
4+
* Add a Prop type to capture how a property is quantified. (#254)
45

56
2025-01-07
67
* Version bump (4.2). (#577)

copilot-core/src/Copilot/Core/Spec.hs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ module Copilot.Core.Spec
2020
, Trigger (..)
2121
, Spec (..)
2222
, Property (..)
23+
, Prop (..)
24+
, extractProp
2325
)
2426
where
2527

@@ -62,9 +64,26 @@ data Trigger = Trigger
6264
-- universally quantified over time.
6365
data Property = Property
6466
{ propertyName :: Name
65-
, propertyExpr :: Expr Bool
67+
, propertyProp :: Prop
6668
}
6769

70+
-- | A proposition, representing a boolean stream that is existentially or
71+
-- universally quantified over time.
72+
data Prop
73+
= Forall (Expr Bool)
74+
| Exists (Expr Bool)
75+
76+
-- | Extract the underlying stream from a quantified proposition.
77+
--
78+
-- Think carefully before using this function, as this function will remove the
79+
-- quantifier from a proposition. Universally quantified streams usually require
80+
-- separate treatment from existentially quantified ones, so carelessly using
81+
-- this function to remove quantifiers can result in hard-to-spot soundness
82+
-- bugs.
83+
extractProp :: Prop -> Expr Bool
84+
extractProp (Forall e) = e
85+
extractProp (Exists e) = e
86+
6887
-- | A Copilot specification is a list of streams, together with monitors on
6988
-- these streams implemented as observers, triggers or properties.
7089
data Spec = Spec

copilot-language/CHANGELOG

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
2025-01-28
1+
2025-02-28
22
* Fix typo in documentation. (#587)
3+
* Record how a Property's underlying proposition is quantified. (#254)
34

45
2025-01-07
56
* Version bump (4.2). (#577)

copilot-language/src/Copilot/Language/Analyze.hs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,11 @@ analyzeObserver refStreams (Observer _ e) = analyzeExpr refStreams e
110110
--
111111
-- This function can fail with one of the exceptions in 'AnalyzeException'.
112112
analyzeProperty :: IORef Env -> Property -> IO ()
113-
analyzeProperty refStreams (Property _ e) = analyzeExpr refStreams e
113+
analyzeProperty refStreams (Property _ p) =
114+
-- Soundness note: it is OK to call `extractProp` here to drop the quantifier
115+
-- from the proposition `p`, as the analysis does not depend on what the
116+
-- quantifier is.
117+
analyzeExpr refStreams (extractProp p)
114118

115119
data SeenExtern = NoExtern
116120
| SeenFun
@@ -291,7 +295,12 @@ specExts refStreams spec = do
291295
env' args
292296

293297
propertyExts :: ExternEnv -> Property -> IO ExternEnv
294-
propertyExts env (Property _ stream) = collectExts refStreams stream env
298+
propertyExts env (Property _ p) =
299+
-- Soundness note: it is OK to call `extractProp` here to drop the
300+
-- quantifier from the proposition `p`. This is because we are simply
301+
-- gathering externs from `p`, and the presence of externs does not depend
302+
-- on what the quantifier is.
303+
collectExts refStreams (extractProp p) env
295304

296305
theoremExts :: ExternEnv -> (Property, UProof) -> IO ExternEnv
297306
theoremExts env (p, _) = propertyExts env p

copilot-language/src/Copilot/Language/Reify.hs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
-- specification.
55

66
{-# LANGUAGE ExistentialQuantification #-}
7+
{-# LANGUAGE GADTs #-}
78
{-# LANGUAGE Rank2Types #-}
89
{-# LANGUAGE Safe #-}
910

@@ -105,11 +106,22 @@ mkProperty
105106
-> IORef [Core.Stream]
106107
-> Property
107108
-> IO Core.Property
108-
mkProperty refMkId refStreams refMap (Property name guard) = do
109-
w1 <- mkExpr refMkId refStreams refMap guard
109+
mkProperty refMkId refStreams refMap (Property name p) = do
110+
p' <- mkProp refMkId refStreams refMap p
110111
return Core.Property
111112
{ Core.propertyName = name
112-
, Core.propertyExpr = w1 }
113+
, Core.propertyProp = p' }
114+
115+
-- | Transform a Copilot proposition into a Copilot Core proposition.
116+
mkProp :: IORef Int
117+
-> IORef (Map Core.Id)
118+
-> IORef [Core.Stream]
119+
-> Prop a
120+
-> IO Core.Prop
121+
mkProp refMkId refStreams refMap prop =
122+
case prop of
123+
Forall e -> Core.Forall <$> mkExpr refMkId refStreams refMap e
124+
Exists e -> Core.Exists <$> mkExpr refMkId refStreams refMap e
113125

114126
-- | Transform a Copilot stream expression into a Copilot Core expression.
115127
{-# INLINE mkExpr #-}

copilot-language/src/Copilot/Language/Spec.hs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ trigger name e args = tell [TriggerItem $ Trigger name e args]
153153
-- | A property, representing a boolean stream that is existentially or
154154
-- universally quantified over time.
155155
data Property where
156-
Property :: String -> Stream Bool -> Property
156+
Property :: String -> Prop a -> Property
157157

158158
-- | A proposition, representing the quantification of a boolean streams over
159159
-- time.
@@ -170,6 +170,12 @@ exists :: Stream Bool -> Prop Existential
170170
exists = Exists
171171

172172
-- | Extract the underlying stream from a quantified proposition.
173+
--
174+
-- Think carefully before using this function, as this function will remove the
175+
-- quantifier from a proposition. Universally quantified streams usually require
176+
-- separate treatment from existentially quantified ones, so carelessly using
177+
-- this function to remove quantifiers can result in hard-to-spot soundness
178+
-- bugs.
173179
extractProp :: Prop a -> Stream Bool
174180
extractProp (Forall p) = p
175181
extractProp (Exists p) = p
@@ -180,15 +186,15 @@ extractProp (Exists p) = p
180186
-- This function returns, in the monadic context, a reference to the
181187
-- proposition.
182188
prop :: String -> Prop a -> Writer [SpecItem] (PropRef a)
183-
prop name e = tell [PropertyItem $ Property name (extractProp e)]
189+
prop name e = tell [PropertyItem $ Property name e]
184190
>> return (PropRef name)
185191

186192
-- | A theorem, or proposition together with a proof.
187193
--
188194
-- This function returns, in the monadic context, a reference to the
189195
-- proposition.
190196
theorem :: String -> Prop a -> Proof a -> Writer [SpecItem] (PropRef a)
191-
theorem name e (Proof p) = tell [TheoremItem (Property name (extractProp e), p)]
197+
theorem name e (Proof p) = tell [TheoremItem (Property name e, p)]
192198
>> return (PropRef name)
193199

194200
-- | Construct a function argument from a stream.

copilot-prettyprinter/CHANGELOG

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
2025-02-28
2+
* Update pretty-printing to handle Props. (#254)
3+
14
2025-01-07
25
* Version bump (4.2). (#577)
36
* Remove uses of Copilot.Core.Expr.UExpr.uExprExpr. (#565)

copilot-prettyprinter/src/Copilot/PrettyPrint.hs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,15 @@ ppProperty :: Property -> Doc
175175
ppProperty
176176
Property
177177
{ propertyName = name
178-
, propertyExpr = e }
178+
, propertyProp = p }
179179
= text "property \"" <> text name <> text "\""
180180
<+> text "="
181-
<+> ppExpr e
181+
<+> ppProp p
182+
183+
-- | Pretty-print a Copilot proposition.
184+
ppProp :: Prop -> Doc
185+
ppProp (Forall e) = text "forall" <+> parens (ppExpr e)
186+
ppProp (Exists e) = text "exists" <+> parens (ppExpr e)
182187

183188
-- | Pretty-print a Copilot specification, in the following order:
184189
--

copilot-theorem/CHANGELOG

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
2025-02-24
1+
2025-02-28
22
* Fix multiple typos in README. (#560)
33
* Fix typo in documentation. (#587)
44
* Add function to produce counterexamples for invalid properties. (#589)
5+
* Reject existentially quantified properties in What4 backend. (#254)
56

67
2025-01-07
78
* Version bump (4.2). (#577)

copilot-theorem/src/Copilot/Theorem/IL/Translate.hs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,13 @@ translate' b (C.Spec {C.specStreams, C.specProperties}) = runTrans b $ do
6363
localConstraints <- popLocalConstraints
6464
properties <- Map.fromList <$>
6565
forM specProperties
66-
(\(C.Property {C.propertyName, C.propertyExpr}) -> do
67-
e' <- expr propertyExpr
66+
(\(C.Property {C.propertyName, C.propertyProp}) -> do
67+
-- Soundness note: it is OK to call `extractProp` here to drop the
68+
-- quantifier from the proposition `propertyProp`. This is because we
69+
-- IL translation always occurs within the context of a function that
70+
-- returns a `Proof`, and these `Proof` functions are always careful to
71+
-- use `Prover`s that respect the propositions's quantifier.
72+
e' <- expr (C.extractProp propertyProp)
6873
propConds <- popLocalConstraints
6974
return (propertyName, (propConds, e')))
7075

0 commit comments

Comments
 (0)