Liveness property checked incorrectly
gusbicalho opened this issue · 1 comments
Hello! First of all, impressive work with his library. Thanks for releasing it.
I was trying to port some stuff from TLA+ to Spectacle and run into some weird behaviour with eventually
and eventually . always
properties.
The test file below reproduces the issues.
The expected behaviour of the model is that a Right counter
value will be incremented until it gets to the maxCounter constant, and then it shifts to the final state Left counter
. Thus, it should hold that #counter
is eventually equal to Left maxCounter
, and it is also the case that it eventually is always equal to Left counter
. However, this property should be violated if the initialCounter value is larger than maxCounter.
What I see, though, is that eventually
seems to reject cases where the model gets stuck forever in a state, even though that state matches the formula. On the other hand, it looks like eventually . always
accepts possibilities that actually never achieve the desired state.
Of course it may be that I'm making a mistake using the library. If that's the case I would be very thankful if you could point me in the right direction.
Versions:
- MacOS Mojave
- GHC 8.10.7
- Spectacle - git commit 7e7efd6
Test module
{-# LANGUAGE DataKinds, ImportQualifiedPost, OverloadedLabels, TypeOperators #-}
module Specs.SimpleCounterSpec where
import Data.Either qualified as Either
import Language.Spectacle (Action, Initial, Invariant, always, define, eventually, modelCheck, plain, weakFair, (.=), type (#))
import Language.Spectacle.Specification (Specification (..))
import Test.Hspec (Spec, describe, hspec, it, shouldSatisfy)
main :: IO ()
main = hspec spec
spec :: Spec
spec =
describe "Counter spec" $ do
describe "eventually is maxCounter" $ do
let specification constants =
Specification
{ initialAction = initSpec constants
, nextAction = next
, temporalFormula = eventually $ formula constants
, terminationFormula = Nothing
, fairnessConstraint = weakFair
}
it "passes when initialCounter < maxCounter" $ do
modelCheck (specification (1, 4)) `shouldSatisfy` Either.isRight
it "passes when initialCounter = maxCounter" $ do
modelCheck (specification (4, 4)) `shouldSatisfy` Either.isRight
it "fails when initialCounter > maxCounter" $ do
modelCheck (specification (4, 2)) `shouldSatisfy` Either.isLeft
describe "eventually always is maxCounter" $ do
let specification constants =
Specification
{ initialAction = initSpec constants
, nextAction = next
, temporalFormula = eventually . always $ formula constants
, terminationFormula = Nothing
, fairnessConstraint = weakFair
}
it "passes when initialCounter < maxCounter" $ do
modelCheck (specification (1, 4)) `shouldSatisfy` Either.isRight
it "passes when initialCounter = maxCounter" $ do
modelCheck (specification (4, 4)) `shouldSatisfy` Either.isRight
it "fails when initialCounter > maxCounter" $ do
modelCheck (specification (4, 2)) `shouldSatisfy` Either.isLeft
type CounterSpec = '["goal" # Word, "counter" # Either Word Word]
initSpec :: (Word, Word) -> Initial CounterSpec ()
initSpec (initialCounter, maxCounter) = do
#goal `define` pure maxCounter
#counter `define` pure (Right initialCounter)
next :: Action CounterSpec Bool
next = do
goal <- plain #goal
counter <- plain #counter
case counter of
Right c
| c < goal -> do
#counter .= pure (Right $ c + 1)
pure True
| otherwise -> do
#counter .= pure (Left c)
pure True
Left _ -> pure True
formula :: (Word, Word) -> Invariant CounterSpec Bool
formula (_, maxCounter) = do
counter <- plain #counter
pure $ counter == Left maxCounter
Output of test run:
Counter spec
eventually is maxCounter
passes when initialCounter < maxCounter [✘]
passes when initialCounter = maxCounter [✘]
fails when initialCounter > maxCounter [✔]
eventually always is maxCounter
passes when initialCounter < maxCounter [✔]
passes when initialCounter = maxCounter [✔]
fails when initialCounter > maxCounter [✘]
Failures:
test/Specs/SimpleCounterSpec.hs:24:9:
1) Counter spec, eventually is maxCounter, passes when initialCounter < maxCounter
predicate failed on: Left [MCFormulaError <<0x86c03a317:goal=4 counter=Left 4 RNil>> --> <<0x86c03a317:goal=4 counter=Left 4 RNil>> (Just (SrcLoc {srcLocPackage = "main", srcLocModule = "Specs.SimpleCounterSpec", srcLocFile = "test/Specs/SimpleCounterSpec.hs", srcLocStartLine = 19, srcLocStartCol = 35, srcLocEndLine = 19, srcLocEndCol = 65})) EventuallyPropK]
To rerun use: --match "/Counter spec/eventually is maxCounter/passes when initialCounter < maxCounter/"
test/Specs/SimpleCounterSpec.hs:26:9:
2) Counter spec, eventually is maxCounter, passes when initialCounter = maxCounter
predicate failed on: Left [MCFormulaError <<0x86c03a317:goal=4 counter=Left 4 RNil>> --> <<0x86c03a317:goal=4 counter=Left 4 RNil>> (Just (SrcLoc {srcLocPackage = "main", srcLocModule = "Specs.SimpleCounterSpec", srcLocFile = "test/Specs/SimpleCounterSpec.hs", srcLocStartLine = 19, srcLocStartCol = 35, srcLocEndLine = 19, srcLocEndCol = 65})) EventuallyPropK]
To rerun use: --match "/Counter spec/eventually is maxCounter/passes when initialCounter = maxCounter/"
test/Specs/SimpleCounterSpec.hs:43:9:
3) Counter spec, eventually always is maxCounter, fails when initialCounter > maxCounter
predicate failed on: Right (ModelMetrics {_distinctStates = 2, _treeDepth = 2, _treeWidth = 0})
To rerun use: --match "/Counter spec/eventually always is maxCounter/fails when initialCounter > maxCounter/"
Randomized with seed 1147279803
Finished in 0.0219 seconds
6 examples, 3 failures
*** Exception: ExitFailure 1
I just adapted my example to use the current version from main (commit 21337e4), and everything seem to be working now! New code below:
module Specs.SimpleCounterSpec where
import Data.Either qualified as Either
import Data.Hashable (Hashable)
import GHC.Generics (Generic)
import Language.Spectacle (
ActionType (ActionWF),
Fairness (WeakFair),
Modality (Eventually, Stays),
Specification (..),
Temporal,
TemporalType (PropF, PropFG),
modelcheck,
plain,
(.=),
pattern ConF,
pattern NilF,
type (#),
)
import Test.Hspec (Spec, describe, hspec, it, shouldSatisfy)
main :: IO ()
main = hspec spec
spec :: Spec
spec =
describe "SimpleCounter spec" $ do
describe "eventually is maxCounter" $ do
specification <- pure $ specification @Eventually PropF
it "passes when initialCounter < maxCounter" $ do
result <- modelcheck (specification (1, 4))
result `shouldSatisfy` Either.isRight
it "passes when initialCounter = maxCounter" $ do
result <- modelcheck (specification (4, 4))
result `shouldSatisfy` Either.isRight
it "fails when initialCounter > maxCounter" $ do
result <- modelcheck (specification (4, 2))
result `shouldSatisfy` Either.isLeft
describe "eventually always is maxCounter" $ do
specification <- pure $ specification @Stays PropFG
it "passes when initialCounter < maxCounter" $ do
result <- modelcheck (specification (1, 4))
result `shouldSatisfy` Either.isRight
it "passes when initialCounter = maxCounter" $ do
result <- modelcheck (specification (4, 4))
result `shouldSatisfy` Either.isRight
it "fails when initialCounter > maxCounter" $ do
result <- modelcheck (specification (4, 2))
result `shouldSatisfy` Either.isLeft
data Counter = Counting Word | Done Word
deriving stock (Eq, Show, Generic)
deriving anyclass (Hashable)
type CounterSpec modality =
Specification CounterState CounterActions (CounterProperty modality)
type CounterState =
'[ "goal" # Word
, "counter" # Counter
]
type CounterActions =
'[ "counterInc" # 'WeakFair
]
type CounterProperty modality =
'["counterEqualsMax" # modality]
specification ::
(forall s. Temporal s Bool -> TemporalType s modality) ->
(Word, Word) ->
CounterSpec modality
specification toProp (initialCounter, maxCounter) =
Specification
{ specInit =
ConF #goal (pure maxCounter)
. ConF #counter (pure (Counting initialCounter))
$ NilF
, specNext =
ConF #counterInc (ActionWF counterInc) NilF
, specProp =
ConF #counterEqualsMax (toProp counterEqualsMax) NilF
}
where
counterInc =
plain #counter >>= \case
Done _ -> pure False
Counting counter -> do
goal <- plain #goal
let counter' =
if counter < goal
then Counting $ counter + 1
else Done counter
#counter .= pure counter'
pure True
counterEqualsMax = do
goal <- plain #goal
counter <- plain #counter
pure
case counter of
Done c -> c == goal
_ -> False