├── .github └── workflows │ └── main.yml ├── HasCal.cabal ├── LICENSE ├── README.md ├── cabal.project ├── default.nix ├── doctest └── Main.hs ├── shell.nix ├── src ├── HasCal.hs └── HasCal │ ├── Coroutine.hs │ ├── Expression.hs │ └── Property.hs └── tasty ├── HasCal └── Test │ ├── API.hs │ ├── AsyncInterface.hs │ ├── DieHard.hs │ ├── DiningPhilosophers.hs │ ├── EuclidAlg.hs │ ├── FIFO.hs │ ├── FastMutex.hs │ ├── Flags.hs │ ├── Hanoi.hs │ ├── HourClock.hs │ ├── InternalMemory.hs │ ├── Market.hs │ ├── Trade.hs │ └── Transfer.hs └── Main.hs /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: 'main' 2 | on: [ 'pull_request' ] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2.4.0 8 | - uses: cachix/install-nix-action@v15 9 | - run: nix-build 10 | -------------------------------------------------------------------------------- /HasCal.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 3.0 2 | name: HasCal 3 | version: 1.0.0 4 | synopsis: Haskell embedding of PlusCal 5 | bug-reports: https://github.com/Gabriel439/HasCal/issues 6 | license: BSD-3-Clause 7 | license-file: LICENSE 8 | author: Gabriella Gonzalez 9 | maintainer: Gabriel439@gmail.com 10 | copyright: 2021 Gabriella Gonzalez 11 | 12 | common common 13 | build-depends: base >=4.14.3.0 && < 5 14 | , Diff 15 | , aeson 16 | , ansi-terminal 17 | , exceptions 18 | , hashable 19 | , hashtables 20 | , logict 21 | , microlens-platform 22 | , mtl 23 | , prettyprinter 24 | , prettyprinter-ansi-terminal 25 | , profunctors 26 | , safe-exceptions 27 | , scientific 28 | , text 29 | , transformers 30 | , unordered-containers 31 | hs-source-dirs: src 32 | ghc-options: -Wall -O2 33 | default-language: Haskell2010 34 | 35 | library 36 | import: common 37 | exposed-modules: HasCal 38 | , HasCal.Coroutine 39 | , HasCal.Expression 40 | , HasCal.Property 41 | ghc-options: -Wall -O2 42 | 43 | test-suite tasty 44 | import: common 45 | type: exitcode-stdio-1.0 46 | main-is: Main.hs 47 | build-depends: base >=4.14.3.0 && < 5 48 | , HasCal 49 | , containers 50 | , tasty 51 | , tasty-expected-failure 52 | , tasty-hunit 53 | , tasty-discover 54 | other-modules: HasCal.Test.AsyncInterface 55 | , HasCal.Test.DieHard 56 | , HasCal.Test.EuclidAlg 57 | , HasCal.Test.Flags 58 | , HasCal.Test.FastMutex 59 | , HasCal.Test.Hanoi 60 | , HasCal.Test.HourClock 61 | , HasCal.Test.Market 62 | , HasCal.Test.Trade 63 | , HasCal.Test.Transfer 64 | hs-source-dirs: tasty 65 | ghc-options: -Wall -O2 -rtsopts 66 | default-language: Haskell2010 67 | 68 | test-suite doctest 69 | type: exitcode-stdio-1.0 70 | hs-source-dirs: doctest 71 | main-is: Main.hs 72 | build-depends: base, HasCal, doctest-parallel 73 | ghc-options: -Wall -O2 -threaded 74 | default-language: Haskell2010 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022, Gabriella Gonzalez 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Gabriella Gonzalez nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HasCal 2 | 3 | HasCal embeds PlusCal in Haskell as an ordinary Haskell package. Everything is 4 | implemented entirely in Haskell, including the model checker. 5 | 6 | ## Examples 7 | 8 | You can find lots of example code in the [test suite](./tasty/HasCal/Test), 9 | including an example which translates the following PlusCal code from the 10 | [Learn TLA+ book](https://learntla.com/introduction/example/): 11 | 12 | ``` 13 | ---- MODULE Transfer ---- 14 | EXTENDS Naturals, TLC 15 | 16 | (* --algorithm transfer 17 | variables alice_account = 10, bob_account = 10, 18 | account_total = alice_account + bob_account; 19 | 20 | process Transfer \in 1..2 21 | variable money \in 1..20; 22 | begin 23 | Transfer: 24 | if alice_account >= money then 25 | A: alice_account := alice_account - money; 26 | bob_account := bob_account + money; 27 | end if; 28 | C: assert alice_account >= 0; 29 | end process 30 | 31 | end algorithm *) 32 | 33 | MoneyInvariant == alice_account + bob_account = account_total 34 | ``` 35 | 36 | … into this Haskell code: 37 | 38 | ```haskell 39 | {-# LANGUAGE BlockArguments #-} 40 | {-# LANGUAGE DeriveAnyClass #-} 41 | {-# LANGUAGE DeriveGeneric #-} 42 | {-# LANGUAGE RecordWildCards #-} 43 | {-# LANGUAGE TemplateHaskell #-} 44 | 45 | import Control.Monad (when) 46 | import Prelude hiding ((.)) 47 | import HasCal 48 | 49 | data Global = Global 50 | { _alice_account :: Int 51 | , _bob_account :: Int 52 | , _account_total :: Int 53 | } deriving (Eq, Generic, Hashable, Show, ToJSON) 54 | 55 | data Local = Local { _money :: Int } 56 | deriving (Eq, Generic, Hashable, Show, ToJSON) 57 | 58 | data Label = Transfer | A | C deriving (Eq, Generic, Hashable, Show, ToJSON) 59 | 60 | makeLenses ''Global 61 | makeLenses ''Local 62 | 63 | main :: IO () 64 | main = do 65 | let transfer _ = Coroutine 66 | { startingLabel = Transfer 67 | 68 | , startingLocals = do 69 | _money <- [ 1 .. 20 ] 70 | return Local{..} 71 | 72 | , process = do 73 | _money <- use (local.money) 74 | 75 | alice_old <- use (global.alice_account) 76 | 77 | when (alice_old >= _money) do 78 | yield A 79 | global.alice_account -= _money 80 | global.bob_account += _money 81 | 82 | yield C 83 | alice_new <- use (global.alice_account) 84 | assert (alice_new >= 0) 85 | } 86 | 87 | model defaultModel 88 | { startingGlobals = do 89 | let _alice_account = 10 90 | let _bob_account = 10 91 | let _account_total = _alice_account + _bob_account 92 | return Global{..} 93 | 94 | , coroutine = traverse transfer [ 1 .. 2 ] 95 | 96 | , property = 97 | let predicate (Global{..}, _) = 98 | _alice_account + _bob_account == _account_total 99 | in always . arr predicate 100 | } 101 | ``` 102 | 103 | ## Notable differences from PlusCal 104 | 105 | HasCal differs from PlusCal in a few ways, which are enumerated here: 106 | 107 | * HasCal processes are fair by default 108 | 109 | You have to explicitly opt out of fairness by adding an explicit "do nothing" 110 | branch to a process 111 | 112 | * Liveness properties can be vacuously satisfied by infinite cycles 113 | 114 | If you disable the termination check then a process that goes into an infinite 115 | loop may satisfy a liveness property by virtue of never failing the property. 116 | 117 | For example, if have a liveness property that checks that a local variable is 118 | eventually equals 1, that property can be satisfied by an infinite loop that 119 | never sets the variable to 1. 120 | 121 | * HasCal does not support symmetry sets 122 | 123 | The performance overhead of implementing symmetry sets negates the benefit 124 | 125 | * HasCal does not support the `goto` keyword 126 | 127 | `goto` can be simulated in Haskell by mutually recursive named procedures 128 | 129 | See, for example, 130 | [the "FastMutex" example from the test suite](tasty/HasCal/Test/FastMutex.hs) 131 | -------------------------------------------------------------------------------- /cabal.project: -------------------------------------------------------------------------------- 1 | packages: . 2 | write-ghc-environment-files: always 3 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let 2 | nixpkgs = builtins.fetchTarball { 3 | url = "https://github.com/NixOS/nixpkgs/archive/faad370edcb37162401be50d45526f52bb16a713.tar.gz"; 4 | sha256 = "1d82d4vh0layf6n925j0h2nym16jbvcvps3l5m8ln9hxn0m6gadn"; 5 | }; 6 | 7 | overlay = pkgsNew: pkgsOld: { 8 | haskellPackages = pkgsOld.haskellPackages.override (old : { 9 | overrides = 10 | pkgsNew.lib.fold pkgsNew.lib.composeExtensions 11 | (old.overrides or (_: _: { })) 12 | [ (pkgsNew.haskell.lib.packageSourceOverrides { 13 | HasCal = ./.; 14 | }) 15 | (haskellPackagesNew: haskellPackagesOld: { 16 | doctest-parallel = 17 | pkgsNew.haskell.lib.dontCheck haskellPackagesOld.doctest-parallel; 18 | 19 | HasCal = 20 | pkgsNew.haskell.lib.overrideCabal 21 | haskellPackagesOld.HasCal 22 | (_: { 23 | testTarget = "tasty"; 24 | }); 25 | }) 26 | ]; 27 | }); 28 | }; 29 | 30 | config = { allowBroken = true; }; 31 | 32 | pkgs = import nixpkgs { inherit config; overlays = [ overlay ]; }; 33 | 34 | in 35 | pkgs.haskellPackages.HasCal 36 | -------------------------------------------------------------------------------- /doctest/Main.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import qualified Test.DocTest as DocTest 4 | import qualified System.Environment as Environment 5 | 6 | main :: IO () 7 | main = do 8 | args <- Environment.getArgs 9 | DocTest.mainFromCabal "HasCal" args 10 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import ./default.nix).env 2 | -------------------------------------------------------------------------------- /src/HasCal.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BangPatterns #-} 2 | {-# LANGUAGE BlockArguments #-} 3 | {-# LANGUAGE DefaultSignatures #-} 4 | {-# LANGUAGE DeriveAnyClass #-} 5 | {-# LANGUAGE DeriveFunctor #-} 6 | {-# LANGUAGE DeriveGeneric #-} 7 | {-# LANGUAGE DerivingStrategies #-} 8 | {-# LANGUAGE DuplicateRecordFields #-} 9 | {-# LANGUAGE ExistentialQuantification #-} 10 | {-# LANGUAGE FlexibleContexts #-} 11 | {-# LANGUAGE FlexibleInstances #-} 12 | {-# LANGUAGE MultiParamTypeClasses #-} 13 | {-# LANGUAGE NamedFieldPuns #-} 14 | {-# LANGUAGE OverloadedStrings #-} 15 | {-# LANGUAGE UndecidableInstances #-} 16 | 17 | -- | This documentation assumes that you are already familiar with PlusCal and 18 | -- TLA+. If not, then you will probably want to read at least one of the 19 | -- following resources first: 20 | -- 21 | -- * 22 | -- * 23 | -- 24 | -- This package implements PlusCal as an embedded domain-specific language 25 | -- (i.e. eDSL) in Haskell. In other words, this does not compile to any 26 | -- intermediate or external language; the whole thing is implemented in pure 27 | -- Haskell. 28 | -- 29 | -- The package is organized into the following modules, which you should study 30 | -- in order if you want to learn more: 31 | -- 32 | -- "HasCal.Coroutine" provides the domain-specific language for creating and 33 | -- and model-checking concurrent `Coroutine`s 34 | -- 35 | -- "HasCal.Property" provides the the domain-specific language for creating and 36 | -- checking temporal `Property`s 37 | -- 38 | -- "HasCal.Expression" provides assorted utilities for non-temporal expressions 39 | -- 40 | -- … and you can import this module if you want an \"all-in-one\" import for 41 | -- convenience. 42 | -- 43 | -- As a complete example, you can translate this PlusCal program from the 44 | -- \"Learn TLA+\" guide: 45 | -- 46 | -- > ---- MODULE Transfer ---- 47 | -- > EXTENDS Naturals, TLC 48 | -- > 49 | -- > (* --algorithm transfer 50 | -- > variables alice_account = 10, bob_account = 10, 51 | -- > account_total = alice_account + bob_account; 52 | -- > 53 | -- > process Transfer \in 1..2 54 | -- > variable money \in 1..20; 55 | -- > begin 56 | -- > Transfer: 57 | -- > if alice_account >= money then 58 | -- > A: alice_account := alice_account - money; 59 | -- > bob_account := bob_account + money; 60 | -- > end if; 61 | -- > C: assert alice_account >= 0; 62 | -- > end process 63 | -- > 64 | -- > end algorithm *) 65 | -- > 66 | -- > MoneyInvariant == alice_account + bob_account = account_total 67 | -- > 68 | -- > ==== 69 | -- 70 | -- … into a Haskell program that is also model-checked within Haskell: 71 | -- 72 | -- @ 73 | -- {-# LANGUAGE BlockArguments #-} 74 | -- {-# LANGUAGE DeriveAnyClass #-} 75 | -- {-# LANGUAGE DeriveGeneric #-} 76 | -- {-# LANGUAGE RecordWildCards #-} 77 | -- {-# LANGUAGE TemplateHaskell #-} 78 | -- 79 | -- import "Control.Monad" (`Control.Monad.when`) 80 | -- import "Prelude" hiding ((`Prelude..`)) 81 | -- import "HasCal" 82 | -- 83 | -- data Global = Global 84 | -- { _alice_account :: `Int` 85 | -- , _bob_account :: `Int` 86 | -- , _account_total :: `Int` 87 | -- } deriving (`Eq`, `Generic`, `Hashable`, `Show`, `ToJSON`) 88 | -- 89 | -- data Local = Local { _money :: `Int` } 90 | -- deriving (`Eq`, `Generic`, `Hashable`, `Show`, `ToJSON`) 91 | -- 92 | -- data Label = Transfer | A | C deriving (`Eq`, `Generic`, `Hashable`, `Show`, `ToJSON`) 93 | -- 94 | -- `Control.TH.makeLenses` ''Global 95 | -- `Control.TH.makeLenses` ''Local 96 | -- 97 | -- main :: `IO` () 98 | -- main = do 99 | -- let transfer _ = `Coroutine` 100 | -- { startingLabel = Transfer 101 | -- 102 | -- , startingLocals = do 103 | -- _money <- [ 1 .. 20 ] 104 | -- `return` Local{..} 105 | -- 106 | -- , process = do 107 | -- _money <- `use` (local.money) 108 | -- 109 | -- alice_old <- `use` (global.alice_account) 110 | -- 111 | -- when (alice_old `>=` _money) do 112 | -- `yield` A 113 | -- global.alice_account `-=` _money 114 | -- global.bob_account `+=` _money 115 | -- 116 | -- `yield` C 117 | -- alice_new <- `use` (global.alice_account) 118 | -- `assert` (alice_new `>=` 0) 119 | -- } 120 | -- 121 | -- `model` `defaultModel` 122 | -- { startingGlobals = do 123 | -- let _alice_account = 10 124 | -- let _bob_account = 10 125 | -- let _account_total = _alice_account `+` _bob_account 126 | -- `return` Global{..} 127 | -- 128 | -- , coroutine = `traverse` transfer [ 1 .. 2 ] 129 | -- 130 | -- , property = 131 | -- let predicate (Global{..}, _) = 132 | -- _alice_account `+` _bob_account `==` _account_total 133 | -- in `always` . `arr` predicate 134 | -- } 135 | -- @ 136 | 137 | module HasCal 138 | ( -- * Internal re-exports 139 | module HasCal.Expression 140 | , module HasCal.Property 141 | , module HasCal.Coroutine 142 | 143 | -- * External re-exports 144 | , module Lens.Micro.Platform 145 | , Generic 146 | , HashMap 147 | , Alternative(..) 148 | , Hashable 149 | , MonadIO(..) 150 | , ToJSON(..) 151 | , ToJSONKey(..) 152 | , Category(..) 153 | , Arrow(..) 154 | , Profunctor(..) 155 | , for_ 156 | , traverse_ 157 | ) where 158 | 159 | import Control.Arrow (Arrow(..)) 160 | import Control.Applicative (Alternative(..)) 161 | import Control.Category (Category(..)) 162 | import Control.Monad.IO.Class (MonadIO(..)) 163 | import Data.Aeson (ToJSON(..), ToJSONKey(..)) 164 | import Data.Foldable (for_, traverse_) 165 | import Data.Hashable (Hashable) 166 | import Data.HashMap.Strict (HashMap) 167 | import Data.Profunctor (Profunctor(..)) 168 | import GHC.Generics (Generic) 169 | import HasCal.Expression 170 | import HasCal.Property 171 | import HasCal.Coroutine 172 | import Lens.Micro.Platform 173 | 174 | -- TODO: Associate local state with process names 175 | -------------------------------------------------------------------------------- /src/HasCal/Coroutine.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BangPatterns #-} 2 | {-# LANGUAGE BlockArguments #-} 3 | {-# LANGUAGE DefaultSignatures #-} 4 | {-# LANGUAGE DeriveAnyClass #-} 5 | {-# LANGUAGE DeriveFunctor #-} 6 | {-# LANGUAGE DeriveGeneric #-} 7 | {-# LANGUAGE DerivingStrategies #-} 8 | {-# LANGUAGE DerivingVia #-} 9 | {-# LANGUAGE DuplicateRecordFields #-} 10 | {-# LANGUAGE ExistentialQuantification #-} 11 | {-# LANGUAGE FlexibleContexts #-} 12 | {-# LANGUAGE MultiParamTypeClasses #-} 13 | {-# LANGUAGE NamedFieldPuns #-} 14 | {-# LANGUAGE OverloadedStrings #-} 15 | {-# LANGUAGE RankNTypes #-} 16 | {-# LANGUAGE ScopedTypeVariables #-} 17 | {-# LANGUAGE TypeApplications #-} 18 | 19 | {-| This module provides the `Process` and `Coroutine` types and associated 20 | utilities for working with them 21 | -} 22 | module HasCal.Coroutine 23 | ( -- * Concurrent processes 24 | Process 25 | , Coroutine(..) 26 | 27 | -- * State management 28 | , Status(..) 29 | , global 30 | , local 31 | , globally 32 | , locally 33 | , zoomProcess 34 | , zoomCoroutine 35 | 36 | -- * PlusCal Statements 37 | -- $statements 38 | , yield 39 | , skip 40 | , either 41 | , with 42 | , while 43 | , await 44 | , assert 45 | , die 46 | , print 47 | 48 | -- * Model checking 49 | , Model(..) 50 | , defaultModel 51 | , model 52 | 53 | -- * Model input 54 | , Input(..) 55 | , state 56 | , label 57 | , stately 58 | , labelly 59 | 60 | -- * Error handling 61 | , ModelException(..) 62 | , PropertyFailedReason(..) 63 | ) where 64 | 65 | import Control.Applicative (Alternative(..), liftA2) 66 | import Control.Exception.Safe (Exception) 67 | import Control.Monad.Catch (MonadThrow(..)) 68 | import Control.Monad.IO.Class (MonadIO(..)) 69 | import Control.Monad.Logic (LogicT(..)) 70 | import Control.Monad.State.Strict (MonadState(get,put), StateT) 71 | import Control.Monad.Trans.Class (MonadTrans(..)) 72 | import Data.Aeson (ToJSON(..), Value(..)) 73 | import Data.Aeson.Key (Key) 74 | import Data.Aeson.KeyMap (KeyMap) 75 | import Data.Algorithm.Diff (PolyDiff(..)) 76 | import Data.HashSet (HashSet) 77 | import Data.Hashable (Hashable(..)) 78 | import Data.Monoid (Any(..), Ap) 79 | import Data.Text (Text) 80 | import GHC.Generics (Generic) 81 | import HasCal.Expression (Boolean(..), Universe(..)) 82 | import HasCal.Property (Check(..), Property) 83 | import Lens.Micro.Platform (Lens, Lens', zoom) 84 | import Numeric.Natural (Natural) 85 | import Prelude hiding (either, print) 86 | import Prettyprinter (Doc, Pretty(..)) 87 | import Prettyprinter.Render.Terminal (AnsiStyle, Color(..)) 88 | 89 | import qualified Control.Applicative as Applicative 90 | import qualified Control.Exception.Safe as Exception 91 | import qualified Control.Monad as Monad 92 | import qualified Control.Monad.Logic as Logic 93 | import qualified Control.Monad.State.Lazy as State.Lazy 94 | import qualified Control.Monad.State.Strict as State 95 | import qualified Control.Monad.Writer as Writer 96 | import qualified Data.Aeson.Key as Aeson.Key 97 | import qualified Data.Aeson.KeyMap as Aeson.KeyMap 98 | import qualified Data.Algorithm.Diff as Diff 99 | import qualified Data.Char as Char 100 | import qualified Data.Foldable as Foldable 101 | import qualified Data.HashSet as HashSet 102 | import qualified Data.HashTable.IO as HashTable 103 | import qualified Data.HashTable.ST.Cuckoo as Cuckoo 104 | import qualified Data.IORef as IORef 105 | import qualified Data.Scientific as Scientific 106 | import qualified Data.Text as Text 107 | import qualified HasCal.Property as Property 108 | import qualified Lens.Micro.Platform as Lens 109 | import qualified Prelude 110 | import qualified Prettyprinter as Pretty 111 | import qualified Prettyprinter.Render.String as Pretty.String 112 | import qualified Prettyprinter.Render.Terminal as Pretty.Terminal 113 | import qualified Prettyprinter.Render.Text as Pretty.Text 114 | import qualified System.Console.ANSI as ANSI 115 | import qualified System.Exit as Exit 116 | import qualified System.IO as IO 117 | import qualified Text.Show as Show 118 | 119 | {- $setup 120 | 121 | >>> :m -Prelude 122 | >>> import Prelude hiding ((.), id) 123 | >>> import HasCal 124 | -} 125 | 126 | -- | Convert a list to the equivalent `LogicT` 127 | select :: [a] -> LogicT m a 128 | select as = LogicT (\cons nil -> foldr cons nil as) 129 | 130 | {-| Assert that a `LogicT` makes progress by throwing a `Deadlock` exception if 131 | the `LogicT` is empty 132 | -} 133 | progressive :: LogicT IO a -> LogicT IO a 134 | progressive (LogicT k) = LogicT k' 135 | where 136 | k' cons nil = do 137 | (nonEmpty, s) <- k cons' nil' 138 | if nonEmpty 139 | then return s 140 | else Exception.throw Deadlock 141 | where 142 | cons' a m = fmap ((,) True) (cons a (fmap snd m)) 143 | 144 | nil' = fmap ((,) False) nil 145 | 146 | {-| A `Process` represents a sequence of @PlusCal@ statements. You can think of 147 | a `Process` as a non-deterministic finite automaton: 148 | 149 | * The `Process` transitions atomically between labeled states 150 | * The `Process` may explore multiple state transitions in parallel because 151 | is it non-deterministic 152 | 153 | The only caveat is that a `Process` does not include a starting state (which 154 | is only later specified upon conversion to a `Coroutine`). 155 | 156 | The type variables are: 157 | 158 | * @global@: The type of the global state shared by every `Process` 159 | * @local@: The type of the process-local state unique to this `Process` 160 | * @label@: The type of labels that this `Process` emits 161 | * @result@: The return value of the `Process` 162 | 163 | Processes support the following core functionality: 164 | 165 | * `yield` - Yield control alongside a label for the current state, ending 166 | an atomic transition 167 | * `pure` / `return` - Promote a value to a `Process` which does nothing 168 | and returns the value 169 | * `empty` - Terminate a `Process` 170 | * `liftIO` - Run an arbitrary `IO` action inside a `Process` 171 | * `throwM` - Throw an exception inside a `Process`, causing model checking 172 | to fail 173 | * `get` / `put` - Get and set the current `Process` state 174 | * `mempty` - A `Process` which does nothing 175 | 176 | Additionally, the utilities in the \"PlusCal utilities\" section wrap the 177 | above functionality to use more PlusCal-friendly names. 178 | 179 | You can combine multiple `Process`es using: 180 | 181 | * @do@ notation - Run `Process`es sequentially 182 | * (`<|>`) - Explore two `Process`es in parallel 183 | * (`<>`) - Run two `Process`es sequentially and combine their return values 184 | 185 | Finally, you will need to convert a `Process` into a `Coroutine` by wrapping 186 | the `Process` in the `Coroutine` constructor 187 | -} 188 | newtype Process global local label result 189 | = Choice 190 | { possibilities 191 | :: StateT (Status global local) (LogicT IO) 192 | (Step global local label result) 193 | } 194 | deriving stock (Functor) 195 | deriving (Boolean, Semigroup, Monoid) 196 | via (Ap (Process global local label) result) 197 | 198 | instance Applicative (Process global local label) where 199 | pure result = Choice (pure (Done result)) 200 | 201 | (<*>) = Monad.ap 202 | 203 | instance Monad (Process global local label) where 204 | Choice ps >>= f = Choice do 205 | p <- ps 206 | case p of 207 | Yield _label rest -> do 208 | return (Yield _label (rest >>= f)) 209 | Done result -> possibilities (f result) 210 | 211 | instance MonadFail (Process global local label) where 212 | fail _ = empty 213 | 214 | instance Alternative (Process global local label) where 215 | empty = Choice empty 216 | 217 | Choice psL <|> Choice psR = Choice (psL <|> psR) 218 | 219 | instance MonadState (Status global local) (Process global local label) where 220 | get = Choice (fmap Done get) 221 | 222 | put s = Choice (fmap Done (put s)) 223 | 224 | state k = Choice (fmap Done (State.state k)) 225 | 226 | instance MonadThrow (Process global local label) where 227 | throwM e = Choice (fmap Done (liftIO (throwM e))) 228 | 229 | instance MonadIO (Process global local label) where 230 | liftIO io = Choice (fmap Done (liftIO io)) 231 | 232 | data Step global local label result 233 | = Yield label (Process global local label result) 234 | | Done result 235 | deriving stock (Functor) 236 | 237 | {-| The `Status` type represents the state for every `Process`, which has two 238 | components: 239 | 240 | * @global@ - The global state shared by all `Process`es 241 | * @local@ - The local state unique to this `Process` 242 | -} 243 | data Status global local = Status 244 | { _global :: !global 245 | -- ^ Shared global state 246 | , _local :: !local 247 | -- ^ `Process`-local state 248 | } deriving stock (Eq, Generic, Show) 249 | deriving anyclass (Hashable, ToJSON) 250 | 251 | -- | A lens for accessing the global state of a `Process` 252 | global :: Lens' (Status global local) global 253 | global k (Status a b) = fmap (\a' -> Status a' b) (k a) 254 | {-# INLINABLE global #-} 255 | 256 | -- | A lens for accessing the local state of a `Process` 257 | local :: Lens' (Status global local) local 258 | local k (Status a b) = fmap (\b' -> Status a b') (k b) 259 | {-# INLINABLE local #-} 260 | 261 | {-| Lift a lens on global state to a lens on `Status` 262 | 263 | @ 264 | `globally` :: Lens' outer inner -> Lens' (`Status` outer local) (`Status` inner local) 265 | @ 266 | -} 267 | globally 268 | :: Lens outer outer' inner inner' 269 | -- ^ Lens to lift 270 | -> Lens (Status outer local) (Status outer' local) (Status inner local) (Status inner' local) 271 | globally l k (Status a b) = 272 | unFocusingGlobal (l (\a' -> FocusingGlobal (k (Status a' b))) a) 273 | 274 | data FocusingGlobal f local global = 275 | FocusingGlobal { unFocusingGlobal :: f (Status global local) } 276 | 277 | instance Functor f => Functor (FocusingGlobal f local) where 278 | fmap f (FocusingGlobal x) = FocusingGlobal (fmap adapt x) 279 | where 280 | adapt (Status a b) = Status (f a) b 281 | 282 | {-| Lift a lens on local state to a lens on `Status` 283 | 284 | @ 285 | `locally` :: Lens' outer inner -> Lens' (`Status` global outer) (`Status` global inner) 286 | @ 287 | -} 288 | locally 289 | :: Lens outer outer' inner inner' 290 | -- ^ Lens to lift 291 | -> Lens (Status global outer) (Status global outer') (Status global inner) (Status global inner') 292 | locally l k (Status a b) = 293 | unFocusingLocal (l (\b' -> FocusingLocal (k (Status a b'))) b) 294 | 295 | data FocusingLocal f global local = 296 | FocusingLocal { unFocusingLocal :: f (Status global local) } 297 | 298 | instance Functor f => Functor (FocusingLocal f local) where 299 | fmap f (FocusingLocal x) = FocusingLocal (fmap adapt x) 300 | where 301 | adapt (Status a b) = Status a (f b) 302 | 303 | {-| A `Coroutine` wraps a `Process` alongside a starting label and starting 304 | process-local state. Including the starting state makes a `Coroutine` a 305 | complete non-deterministic finite automaton that can be combined with 306 | other `Coroutine`s using `Applicative` operations. Combining `Coroutine`s 307 | in this way is the same as taking the Cartesian product of their 308 | equivalent non-deterministic finite automata. For a more detailed 309 | explanation, see: 310 | 311 | * 312 | 313 | The type variables are: 314 | 315 | * @global@: The type of the global state shared by every `Coroutine` 316 | * @label@: The type of labels that this `Coroutine` emits 317 | 318 | Carefully note that the `Process`-local state (i.e. the @local@ variable) 319 | is hidden upon conversion to a `Coroutine`, in order to ensure that the 320 | `Process`-local state of each `Coroutine` is isolated from one another. 321 | 322 | You can create a `Coroutine` in the following ways: 323 | 324 | * The `Coroutine`` constructor - Wrap a `Process` with a starting state and 325 | label to create a `Coroutine` 326 | * `pure` - A `Coroutine` with a single state and no valid transitions 327 | * `mempty` - A `Coroutine` that does nothing 328 | 329 | You can combine `Coroutine`s using: 330 | 331 | * `Functor` and `Applicative` utilities (e.g. (`<$>`) and (`<*>`)) 332 | * @do@ notation, if you enable @ApplicativeDo@ 333 | * (`<>`) - Run two `Coroutine`s in parallel and combine their results 334 | -} 335 | data Coroutine global label = 336 | forall local 337 | . (Eq local, Hashable local, ToJSON local, Show local) 338 | => Coroutine 339 | { startingLabel :: label 340 | , startingLocals :: [local] 341 | , process :: Process global local label () 342 | } 343 | deriving (Boolean, Semigroup, Monoid) 344 | via (Ap (Coroutine global) label) 345 | 346 | instance Functor (Coroutine global) where 347 | fmap = Applicative.liftA 348 | 349 | instance Applicative (Coroutine global) where 350 | pure _label = Coroutine _label (pure Unit) empty 351 | 352 | Coroutine label0F sF fs0 <*> Coroutine label0X sX xs0 = 353 | Coroutine label0FX s fxs 354 | where 355 | (label0FX, fxs) = loop (label0F, fs0) (label0X, xs0) 356 | 357 | s = liftA2 Pair sF sX 358 | 359 | loop (label1F, Choice fs) (label1X, Choice xs) = 360 | ( label1F label1X 361 | , Choice (fmap adaptF (Lens.zoom onLeft fs)) 362 | <|> Choice (fmap adaptX (Lens.zoom onRight xs)) 363 | ) 364 | where 365 | adaptF (Done result) = Done result 366 | adaptF (Yield labelF restF) = Yield labelFX restFX 367 | where 368 | (labelFX, restFX) = loop (labelF, restF) (label1X, Choice xs) 369 | 370 | adaptX (Done result) = Done result 371 | adaptX (Yield labelX restX) = Yield labelFX restFX 372 | where 373 | (labelFX, restFX) = loop (label1F, Choice fs) (labelX, restX) 374 | 375 | onLeft :: Lens' (Status global (Pair l r)) (Status global l) 376 | onLeft k (Status g (Pair l r)) = fmap adapt (k (Status g l)) 377 | where 378 | adapt (Status g' l') = Status g' (Pair l' r) 379 | 380 | onRight :: Lens' (Status global (Pair l r)) (Status global r) 381 | onRight k (Status g (Pair l r)) = fmap adapt (k (Status g r)) 382 | where 383 | adapt (Status g' r') = Status g' (Pair l r') 384 | 385 | data Unit = Unit 386 | deriving stock (Eq, Generic, Show) 387 | deriving anyclass (Hashable) 388 | 389 | instance ToJSON Unit where 390 | toJSON Unit = toJSON ([] :: [Value]) 391 | 392 | data Pair a b = Pair !a !b 393 | deriving stock (Eq, Generic, Show) 394 | deriving anyclass (Hashable) 395 | 396 | instance (ToJSON a, ToJSON b) => ToJSON (Pair a b) where 397 | toJSON (Pair a b) = 398 | case (toJSON a, toJSON b) of 399 | (Array as, Array bs) -> Array (as <> bs) 400 | (a' , b' ) -> toJSON [ a', b' ] 401 | 402 | {- $statements 403 | This section provides commands that correspond as closely as possible to the 404 | equivalent PlusCal commands of the same name. These commands do not 405 | represent the full extent of what you can do within Haskell processes, but 406 | they do provide feature parity with PlusCal (with the exception of the 407 | @goto@ command, which is not supported here). 408 | 409 | Many of these commands are synonyms for utilities from Haskell's standard 410 | library. For example, `await` is just a synonym for `Monad.guard`. These 411 | synonyms exist primarily for educational purposes, to illustrate how 412 | PlusCal idioms correspond to Haskell idioms, but you can still use these 413 | synonyms if you want the Haskell code to resemble PlusCal as much as 414 | possible. 415 | -} 416 | 417 | {-| End the current atomic transition alongside a label for the current state. 418 | This potentially yields control to other `Process`es. 419 | 420 | If the exact same label and state have been reached before then the 421 | model checker behavior depends on whether you enable the termination 422 | check: 423 | 424 | * If you enable the termination check then the model checker will 425 | `Exception.throw` a `Nontermination` exception since revisiting the same 426 | state indicates a simulation path that permits an infinite loop 427 | 428 | * If you disable the termination check then the model checker will still 429 | end the current simulation branch since it has already visited this 430 | state before 431 | 432 | -} 433 | yield :: label -> Process global local label () 434 | yield _label = Choice (pure (Yield _label mempty)) 435 | {-# INLINABLE yield #-} 436 | 437 | {-| A `Process` which does nothing, like the @skip@ statement in PlusCal 438 | 439 | This is a synonym for `mempty`, but with a `Process`-specific type 440 | signature. 441 | 442 | Note that you do not need to use `skip` in Haskell as often as in PlusCal. 443 | For example, consider the following PlusCal code: 444 | 445 | > A: 446 | > skip; 447 | > B: 448 | > either 449 | > skip; 450 | > or 451 | > C: 452 | > skip; 453 | > end either; 454 | 455 | The equivalent Haskell code can elide many of those `skip`s: 456 | 457 | @ 458 | example = do 459 | `yield` "A" 460 | `yield` "B" 461 | `either` 462 | [ `skip` 463 | , `yield` "C" 464 | ] 465 | @ 466 | 467 | … because `skip` literally does nothing and therefore can be omitted in 468 | most cases. 469 | -} 470 | skip :: Process global local label () 471 | skip = mempty 472 | {-# INLINE skip #-} 473 | 474 | {-| Non-deterministically simulate multiple subroutines, like an @either@ 475 | statement in PlusCal 476 | 477 | This is a synonym for @"Data.Foldable".`Foldable.asum`@, but with a 478 | `Process`-specific type signature. 479 | 480 | The model checker will explore all branches, succeeding only if all branches 481 | succeed. 482 | 483 | `either` obeys the following laws: 484 | 485 | @ 486 | `either` [ a ] = a 487 | 488 | `either` (as `<|>` bs) = `either` as `<|>` `either` bs 489 | 490 | `either` `empty` = `empty` 491 | @ 492 | 493 | … or equivalently: 494 | 495 | @ 496 | `either` (as `++` bs) = `either` as `<|>` `either` bs 497 | 498 | `either` [] = `empty` 499 | @ 500 | 501 | Those rules also imply that: 502 | 503 | @ 504 | `either` [ a, b ] = a `<|>` b 505 | @ 506 | -} 507 | either 508 | :: Foldable list 509 | => list (Process global local label result) 510 | -- ^ Subroutines to non-deterministically select from 511 | -> Process global local label result 512 | either = Foldable.asum 513 | {-# INLINE either #-} 514 | 515 | {-| Non-deterministically select from one of multiple possible values, like 516 | a @with@ statement in PlusCal 517 | 518 | `with` is (essentially) the same thing as using `either` to select from a 519 | list of `pure` subroutines: 520 | 521 | @ 522 | `with` results = `either` (`fmap` `pure` results) 523 | @ 524 | 525 | `with` obeys the following laws: 526 | 527 | @ 528 | `with` (as `<|>` bs) = `with` as `<|>` `with` bs 529 | 530 | `with` `empty` = `empty` 531 | 532 | `with` (`pure` a) = `pure` a 533 | @ 534 | 535 | … or equivalently: 536 | 537 | @ 538 | `with` (as `++` bs) = `with` as `<|>` `with` bs 539 | 540 | `with` [] = `empty` 541 | 542 | `with` [a] = `pure` a 543 | @ 544 | 545 | -} 546 | with :: Foldable list => list result -> Process global local label result 547 | with = foldr cons empty 548 | where 549 | cons result rest = pure result <|> rest 550 | {-# INLINABLE with #-} 551 | 552 | {-| Run a loop so long as the loop condition does not return `True`, like a 553 | @while@ statement in PlusCal 554 | 555 | You will typically /not/ want to use this and instead you will more likely 556 | want to use one of the utilities from "Control.Monad". This is only 557 | provided for parity with PlusCal. 558 | 559 | @ 560 | `while` (`pure` `True`) body = `Monad.forever` body 561 | 562 | `while` (`pure` `False`) body = skip 563 | @ 564 | -} 565 | while 566 | :: Process global local label Bool 567 | -- ^ Condition 568 | -> Process global local label () 569 | -- ^ Body of the loop 570 | -> Process global local label () 571 | while condition body = do 572 | bool <- condition 573 | Monad.when bool do 574 | body 575 | while condition body 576 | 577 | {-| Only permit the current state transition if the predicate is `True`, like 578 | an @await@ statement in PlusCal 579 | 580 | This is a synonym for @"Control.Monad".`Monad.guard`@, but with a 581 | `Process`-specific type signature. 582 | 583 | @ 584 | `await` `False` = `empty` 585 | `await` `True` = `mempty` 586 | 587 | `await` (a `||` b) = `await` a `<|>` `await` b 588 | `await` (a `&&` b) = `await` a `<>` `await` b 589 | @ 590 | -} 591 | await :: Bool -> Process global local label () 592 | await = Monad.guard 593 | {-# INLINE await #-} 594 | 595 | {-| Throw an exception if the condition does not evaluate to `True`, like an 596 | @assert@ statement in PlusCal 597 | 598 | The model checker will fail if any branch throws an exception (with `assert` 599 | or otherwise), but exceptions thrown using `assert` will also automatically 600 | include the current `Process` state 601 | 602 | @ 603 | `assert` `True` = `skip` 604 | @ 605 | -} 606 | assert 607 | :: (ToJSON local, ToJSON global, Show local, Show global) 608 | => Bool 609 | -- ^ Condition 610 | -> Process global local label () 611 | assert True = skip 612 | assert False = do 613 | _status <- get 614 | Exception.throw AssertionFailed{ _status } 615 | 616 | -- | Die with an error message 617 | die :: Text -> Process global local label result 618 | die _message = Exception.throw Failure{ _message } 619 | 620 | {-| Print a value to the console for debugging purposes, like a @print@ 621 | statement in PlusCal 622 | 623 | This is the same as @"Prelude".`Prelude.print`@, except wrapped in a 624 | `liftIO` 625 | -} 626 | print :: Show a => a -> Process global local label () 627 | print a = liftIO (Prelude.print a) 628 | {-# INLINABLE print #-} 629 | 630 | -- | Zoom in on a subset of a `Coroutine`'s global state using a lens 631 | zoomCoroutine 632 | :: Lens' outer inner -> Coroutine inner label -> Coroutine outer label 633 | zoomCoroutine l Coroutine{ startingLabel, startingLocals, process } = 634 | Coroutine 635 | { startingLabel 636 | , startingLocals 637 | , process = zoomProcess l process 638 | } 639 | 640 | -- | Zoom in on a subset of a `Process`'s global state using a lens 641 | zoomProcess 642 | :: Lens' outer inner 643 | -> Process inner local label result 644 | -> Process outer local label result 645 | zoomProcess l = adaptProcess 646 | where 647 | adaptProcess (Choice steps) = 648 | Choice (zoom (globally l) (fmap adaptStep steps)) 649 | 650 | adaptStep (Done result ) = Done result 651 | adaptStep (Yield _label rest) = Yield _label (adaptProcess rest) 652 | 653 | {-| The `ModelException` type represents all of the ways in which the model 654 | checker can fail 655 | -} 656 | data ModelException = 657 | forall value . (Show value, ToJSON value) 658 | => Nontermination { _history :: [value] } 659 | -- ^ The process does not necessarily terminate because at least one 660 | -- branch of execution permits an infinite cycle 661 | | Deadlock 662 | -- ^ The process deadlocked, meaning that no branch of execution 663 | -- successfully ran to completion 664 | | forall value . (Show value, ToJSON value) 665 | => AssertionFailed { _status :: value } 666 | -- ^ The process failed to satisfy an `assert` statement 667 | | forall value . (Show value, ToJSON value) 668 | => PropertyFailed 669 | { _inputHistory :: [value] 670 | , _reason :: PropertyFailedReason 671 | } 672 | -- ^ At least one branch of execution failed to satisfy the specified 673 | -- `Property` 674 | | Failure { _message :: Text } 675 | -- ^ Used by the `fail` method 676 | 677 | instance Pretty ModelException where 678 | pretty = Pretty.unAnnotate . prettyModelException 679 | 680 | -- | The reason why a `PropertyFailed` exception was thrown 681 | data PropertyFailedReason 682 | = Unsatisfiable 683 | -- ^ The `Property` can no longer satisfied, regardless of future input 684 | | UnsatisfyingConclusion 685 | -- ^ We could not satisfy the `Property` before reaching the end of the 686 | -- input sequence. For example, you would get this error if you check the 687 | -- `Property.eventually` `Property` against an input sequence where every 688 | -- input was `False` 689 | deriving (Show) 690 | 691 | instance Show ModelException where 692 | showsPrec _ Nontermination{ _history } = 693 | showString "Nontermination {_history = " 694 | . Show.showListWith shows _history 695 | . showString "}" 696 | showsPrec _ Deadlock = 697 | showString "Deadlock" 698 | showsPrec _ AssertionFailed{ _status } = 699 | showString "AssertionFailed {_status = " 700 | . shows _status 701 | . showString "}" 702 | showsPrec _ PropertyFailed{ _inputHistory, _reason } = 703 | showString "PropertyFailed {_inputHistory = " 704 | . Show.showListWith shows _inputHistory 705 | . showString ", _reason = " 706 | . shows _reason 707 | . showString "}" 708 | showsPrec _ Failure{ _message } = 709 | showString "Failure {_message = " 710 | . shows _message 711 | . showString "}" 712 | 713 | instance Exception ModelException where 714 | displayException exception = 715 | Pretty.String.renderString 716 | (Pretty.layoutPretty Pretty.defaultLayoutOptions (pretty exception)) 717 | 718 | data HistoryKey a b = HistoryKey{ _label :: !a, _status :: !b } 719 | deriving stock (Eq, Generic, Show) 720 | deriving anyclass (Hashable, ToJSON) 721 | 722 | prettyKey :: Text -> Doc ann 723 | prettyKey = 724 | Pretty.pretty 725 | . spaces 726 | . Text.concat 727 | . fmap capitalizeWord 728 | . Text.splitOn "_" 729 | . Text.dropWhile (== '_') 730 | where 731 | space c 732 | | Char.isUpper c = Text.pack [ ' ', c ] 733 | | otherwise = Text.singleton c 734 | 735 | capitalizeWord text = 736 | case Text.uncons text of 737 | Just (t, ext) -> 738 | Text.cons (Char.toUpper t) ext 739 | Nothing -> 740 | text 741 | 742 | spaces text = 743 | case Text.uncons text of 744 | Just (t, ext) -> 745 | Text.cons (Char.toUpper t) (Text.concatMap space ext) 746 | Nothing -> 747 | text 748 | 749 | prettyValue :: Value -> Doc ann 750 | prettyValue = loop 751 | where 752 | loop (Array values) = Pretty.group (Pretty.flatAlt long short) 753 | where 754 | long = Pretty.align (lined (fmap item values)) 755 | 756 | short 757 | | null values = "[ ]" 758 | | otherwise = "[ " <> commas (fmap loop values) <> " ]" 759 | 760 | item value = "- " <> loop value 761 | loop (Object keyValues) = 762 | Pretty.group (Pretty.flatAlt long short) 763 | where 764 | long = Pretty.align (lined (fmap processLong list)) 765 | 766 | short 767 | | null keyValues = "{ }" 768 | | otherwise = "{ " <> commas (fmap processShort list) <> " }" 769 | 770 | list = Aeson.KeyMap.toList keyValues 771 | 772 | processLong (key, value) = 773 | prettyKey (Aeson.Key.toText key) 774 | <> ":" 775 | <> Pretty.hardline 776 | <> " " 777 | <> loop value 778 | 779 | processShort (key, value) = 780 | prettyKey (Aeson.Key.toText key) <> ": " <> loop value 781 | loop (String text) = 782 | Pretty.pretty (show text) 783 | loop (Number scientific) = 784 | case Scientific.floatingOrInteger scientific of 785 | Left double -> Pretty.pretty @Double double 786 | Right integer -> Pretty.pretty @Integer integer 787 | loop (Bool bool) = 788 | Pretty.pretty bool 789 | loop Null = 790 | "null" 791 | 792 | prettyValueList :: [Value] -> Doc AnsiStyle 793 | prettyValueList values = 794 | case values of 795 | before : afters -> Pretty.group (Pretty.flatAlt long short) 796 | where 797 | docs = prettyValue before : diffs before afters 798 | 799 | long = Pretty.align (lined (fmap process docs)) 800 | 801 | short 802 | | null docs = "[ ]" 803 | | otherwise = "[ " <> commas docs <> " ]" 804 | 805 | process doc = "- " <> doc 806 | _ -> prettyValue (toJSON values) 807 | where 808 | diffs :: Value -> [Value] -> [Doc AnsiStyle] 809 | diffs _ [] = 810 | [ ] 811 | diffs before (after : afters) = do 812 | snd (diff before after) : diffs after afters 813 | 814 | diff :: Value -> Value -> (Any, Doc AnsiStyle) 815 | diff (Array old) (Array new) = do 816 | let oldList = Foldable.toList old 817 | let newList = Foldable.toList new 818 | 819 | let docs (o : ld) (n : ew) = do 820 | d <- diff o n 821 | ds <- docs ld ew 822 | return (d : ds) 823 | docs _ new' = do 824 | return (fmap (plus . prettyValue) new') 825 | 826 | (ds, Any matching) <- Writer.listen (docs oldList newList) 827 | 828 | ds' <- do 829 | if matching 830 | then do 831 | return ds 832 | else do 833 | let render (First a) = minus (prettyValue a) 834 | render (Second a) = plus (prettyValue a) 835 | render (Both a _) = prettyValue a 836 | 837 | return (fmap render (Diff.getDiff oldList newList)) 838 | 839 | let short 840 | | null ds' = 841 | "[ ]" 842 | | otherwise = 843 | "[ " <> commas ds' <> " ]" 844 | 845 | let long = Pretty.align (lined (fmap item ds')) 846 | where 847 | item d = "- " <> d 848 | 849 | return (Pretty.group (Pretty.flatAlt long short)) 850 | diff (Object old) (Object new) 851 | | let both = Aeson.KeyMap.intersection old new 852 | , not (Aeson.KeyMap.null both) = do 853 | Writer.tell (Any True) 854 | 855 | let extraOlds = Aeson.KeyMap.difference old new 856 | let extraNews = Aeson.KeyMap.difference new old 857 | 858 | let combine 859 | :: Key 860 | -> Value 861 | -> Value 862 | -> (Any, (Doc AnsiStyle, Doc AnsiStyle)) 863 | combine key o n = do 864 | doc <- diff o n 865 | let long = 866 | prettyKey (Aeson.Key.toText key) 867 | <> ":" 868 | <> Pretty.hardline 869 | <> " " 870 | <> doc 871 | 872 | let short = 873 | prettyKey (Aeson.Key.toText key) 874 | <> ": " 875 | <> doc 876 | 877 | return (long, short) 878 | 879 | let boths :: KeyMap (Any, (Doc AnsiStyle, Doc AnsiStyle)) 880 | boths = Aeson.KeyMap.intersectionWithKey combine old new 881 | 882 | (bothLongs, bothShorts) <- do 883 | fmap unzip (Monad.sequence (Aeson.KeyMap.elems boths)) 884 | 885 | let (extraNewLongs, extraNewShorts) = 886 | unzip (fmap (extra plus) (Aeson.KeyMap.toList extraNews)) 887 | 888 | let (extraOldLongs, extraOldShorts) = 889 | unzip (fmap (extra minus) (Aeson.KeyMap.toList extraOlds)) 890 | 891 | let longs = extraOldLongs <> extraNewLongs <> bothLongs 892 | let shorts = extraOldShorts <> extraNewShorts <> bothShorts 893 | 894 | let long = Pretty.align (lined longs) 895 | 896 | let short 897 | | null shorts = 898 | "{ }" 899 | | otherwise = 900 | "{ " <> commas shorts <> " }" 901 | 902 | return (Pretty.group (Pretty.flatAlt long short)) 903 | where 904 | extra sign (key, value) = 905 | ( sign 906 | ( prettyKey (Aeson.Key.toText key) 907 | <> ":" 908 | <> Pretty.hardline 909 | <> " " 910 | <> prettyValue value 911 | ) 912 | , sign 913 | ( prettyKey (Aeson.Key.toText key) 914 | <> ": " 915 | <> prettyValue value 916 | ) 917 | ) 918 | diff old new 919 | | old == new = do 920 | Writer.tell (Any True) 921 | return (prettyValue new) 922 | | otherwise = do 923 | return (plus (prettyValue new)) 924 | 925 | minus = Pretty.annotate (Pretty.Terminal.color Red ) 926 | plus = Pretty.annotate (Pretty.Terminal.color Green) 927 | 928 | lined :: Foldable list => list (Doc ann) -> Doc ann 929 | lined = Pretty.concatWith append 930 | where 931 | append x y = x <> Pretty.hardline <> y 932 | 933 | commas :: Foldable list => list (Doc ann) -> Doc ann 934 | commas = Pretty.concatWith append 935 | where 936 | append x y = x <> ", " <> y 937 | 938 | prettyModelException :: ModelException -> Doc AnsiStyle 939 | prettyModelException Nontermination{ _history } = 940 | Pretty.align 941 | ( "Non-termination" 942 | <> Pretty.hardline 943 | <> Pretty.hardline 944 | <> prettyValueList (fmap toJSON (reverse _history)) 945 | ) 946 | prettyModelException Deadlock = 947 | "Deadlock" 948 | prettyModelException AssertionFailed{ _status } = 949 | Pretty.align 950 | ( "Assertion failed" 951 | <> Pretty.hardline 952 | <> Pretty.hardline 953 | <> prettyValue (toJSON _status) 954 | ) 955 | prettyModelException PropertyFailed{ _inputHistory, _reason } = 956 | Pretty.align 957 | ( "Property failed: " <> reason 958 | <> Pretty.hardline 959 | <> Pretty.hardline 960 | <> prettyValueList (fmap toJSON (reverse _inputHistory)) 961 | ) 962 | where 963 | reason = case _reason of 964 | Unsatisfiable -> "unsatisfiable" 965 | UnsatisfyingConclusion -> "unsatisfying conclusion" 966 | 967 | prettyModelException Failure{ _message } = "Failure: " <> pretty _message 968 | 969 | {-| A `Model` represents the model to check, alongside all model-checking 970 | options 971 | -} 972 | data Model global label = Model 973 | { termination :: Bool 974 | -- ^ When `True`, throw a `Nontermination` exception if any cycles are 975 | -- detected 976 | , deadlock :: Bool 977 | -- ^ When `True`, throw a `Deadlock` exception if if the `Coroutine` 978 | -- cannot make progress 979 | , debug :: Bool 980 | -- ^ Set this to `True` if you want to pretty-print the `ModelException` 981 | -- and instead throw @`Exit.ExitFailure` 1@ in its place 982 | , statistics :: Bool 983 | -- ^ Set this to `True` to display model-checking statistics at the end 984 | -- of the run 985 | , coroutine :: Coroutine global label 986 | -- ^ `Coroutine` to check 987 | , property :: Property (Input global label) Bool 988 | -- ^ `Property` to check 989 | , startingGlobals :: [global] 990 | -- ^ Possible starting global states 991 | } 992 | 993 | {-| Default model-checking options 994 | 995 | @ 996 | defaultModel = `Model` 997 | { termination = `True` 998 | , deadlock = `True` 999 | , debug = `False` 1000 | , statistics = `False` 1001 | , coroutine = `mempty` 1002 | , property = `true` 1003 | , startingGlobals = `pure` () 1004 | } 1005 | @ 1006 | -} 1007 | defaultModel :: Model () () 1008 | defaultModel = Model 1009 | { termination = True 1010 | , deadlock = True 1011 | , debug = False 1012 | , statistics = False 1013 | , coroutine = mempty 1014 | , property = true 1015 | , startingGlobals = pure () 1016 | } 1017 | 1018 | {- This type is used internally within the `model` function to keep track of 1019 | state specific to one \"timeline\" of the model checker (i.e. one possible 1020 | branch of execution) 1021 | 1022 | This `Timeline` state is isolated from other branches of execution 1023 | -} 1024 | data Timeline global local label status = Timeline 1025 | { _history :: [ HistoryKey label (Status global local) ] 1026 | -- ^ This is kept for error reporting so that if things go wrong we can 1027 | -- report to the user what sequence of events led up to the problem 1028 | , _historySet :: !(HashSet (HistoryKey label (Status global local))) 1029 | -- ^ This always the same as @`HashSet.fromList` _history@, 1030 | -- but kept as a separate field for efficiently updating and querying 1031 | -- which states we've seen so far in order to detect cycles 1032 | , _propertyStatus :: !(HashSet status) 1033 | -- ^ This stores the internal state of the temporal `Property` 1034 | , _inputHistory :: [ Input global label ] 1035 | } 1036 | 1037 | {-| Run the model checker on a `Coroutine` by supplying a list of starting 1038 | states 1039 | 1040 | If you want to check more than one `Coroutine`, then combine those 1041 | `Coroutine`s using `Applicative` operations or @ApplicativeDo@ notation 1042 | 1043 | >>> model defaultModel -- The default model has no valid execution branches 1044 | *** Exception: Deadlock 1045 | 1046 | >>> -- An example coroutine with one execution branch that does nothing 1047 | >>> exampleCoroutine = Coroutine{ startingLabel = (), startingLocals = pure (), process = mempty } 1048 | >>> model defaultModel{ coroutine = exampleCoroutine } -- Success 1049 | 1050 | 1051 | >>> -- Create a coroutine that never terminates 1052 | >>> endlessCoroutine = Coroutine{ startingLabel = False, startingLocals = pure (), process = while true (yield True) } 1053 | >>> model defaultModel{ coroutine = endlessCoroutine, property = true } 1054 | *** Exception: Nontermination {_history = [HistoryKey {_label = True, _status = Status {_global = (), _local = ()}},HistoryKey {_label = True, _status = Status {_global = (), _local = ()}},HistoryKey {_label = False, _status = Status {_global = (), _local = ()}}]} 1055 | 1056 | >>> -- Enable debugging output for clarity 1057 | >>> model defaultModel{ coroutine = endlessCoroutine, property = true, debug = True } 1058 | Non-termination 1059 | ... 1060 | - { Label: False, Status: { Global: [ ], Local: [ ] } } 1061 | - { Label: True, Status: { Global: [ ], Local: [ ] } } 1062 | - { Label: True, Status: { Global: [ ], Local: [ ] } } 1063 | *** Exception: ExitFailure 1 1064 | 1065 | >>> -- Disable the termination checker if desired 1066 | >>> model defaultModel{ coroutine = endlessCoroutine, property = true, debug = True, termination = False } 1067 | 1068 | >>> -- Check a non-trivial property that succeeds 1069 | >>> exampleProperty = eventually . always . viewing label 1070 | >>> model defaultModel{ coroutine = endlessCoroutine, property = exampleProperty, debug = True, termination = False } 1071 | 1072 | >>> -- Check a non-trivial property that fails 1073 | >>> model defaultModel{ coroutine = endlessCoroutine, property = always . viewing (label . to not), debug = True, termination = False } 1074 | Property failed: unsatisfiable 1075 | ... 1076 | [ { Label: False, State: [ ] }, { Label: True, State: [ ] } ] 1077 | *** Exception: ExitFailure 1 1078 | -} 1079 | model 1080 | :: ( Eq global 1081 | , Eq label 1082 | , Hashable global 1083 | , Hashable label 1084 | , ToJSON label 1085 | , ToJSON global 1086 | , Show global 1087 | , Show label 1088 | ) 1089 | => Model global label 1090 | -- ^ Model checking options 1091 | -> IO () 1092 | model Model 1093 | { debug 1094 | , statistics 1095 | , termination 1096 | , deadlock 1097 | , property 1098 | , startingGlobals 1099 | , coroutine = Coroutine{ startingLabel, startingLocals, process } 1100 | } = do 1101 | terminal <- ANSI.hSupportsANSI IO.stdout 1102 | 1103 | let putDoc doc = 1104 | if terminal 1105 | then Pretty.Terminal.putDoc (doc <> "\n") 1106 | else Pretty.Text.putDoc (doc <> "\n") 1107 | 1108 | redundantStatesReference <- IORef.newIORef 0 1109 | 1110 | case Property.check property of 1111 | Check finalPropertyStatus stepProperty -> do 1112 | hashtable <- HashTable.new @Cuckoo.HashTable 1113 | 1114 | let action = do 1115 | startingGlobal <- lift (lift (select startingGlobals)) 1116 | 1117 | startingLocal <- lift (lift (select startingLocals)) 1118 | 1119 | let startingProcessStatus = Status 1120 | { _global = startingGlobal 1121 | , _local = startingLocal 1122 | } 1123 | 1124 | let startingInput = Input startingGlobal startingLabel 1125 | 1126 | let _inputHistory = [ startingInput ] 1127 | 1128 | let _propertyStatus = HashSet.fromList do 1129 | s <- universe 1130 | 1131 | -- The temporal `Property` only needs to return `True` 1132 | -- for the first output, indicating that the property 1133 | -- holds for the entire sequence 1134 | (True, s') <- State.Lazy.runStateT (stepProperty startingInput) s 1135 | 1136 | return s' 1137 | 1138 | Monad.when (HashSet.null _propertyStatus) do 1139 | let _reason = Unsatisfiable 1140 | 1141 | liftIO (Exception.throw PropertyFailed{ _inputHistory, _reason }) 1142 | 1143 | let historyKey = HistoryKey startingLabel startingProcessStatus 1144 | 1145 | lift (put $! startingProcessStatus) 1146 | 1147 | put $! Timeline 1148 | { _history = [ historyKey ] 1149 | , _historySet = HashSet.singleton historyKey 1150 | , _propertyStatus 1151 | , _inputHistory 1152 | } 1153 | 1154 | loop process 1155 | 1156 | loop (Choice steps) = do 1157 | let wrap 1158 | | deadlock = State.mapStateT progressive 1159 | | otherwise = id 1160 | 1161 | step <- lift (wrap steps) 1162 | 1163 | case step of 1164 | Done () -> do 1165 | Timeline{ _propertyStatus, _inputHistory } <- get 1166 | 1167 | Monad.unless (HashSet.member finalPropertyStatus _propertyStatus) do 1168 | let _reason = UnsatisfyingConclusion 1169 | 1170 | liftIO (Exception.throw PropertyFailed{ _inputHistory, _reason }) 1171 | 1172 | Yield _label rest -> do 1173 | _processStatus <- lift get 1174 | 1175 | Timeline{ _history, _historySet, _propertyStatus, _inputHistory } <- get 1176 | 1177 | let Status{ _global } = _processStatus 1178 | 1179 | let input = Input{ _state = _global, _label } 1180 | 1181 | let newPropertyStatus = HashSet.fromList do 1182 | s <- HashSet.toList _propertyStatus 1183 | 1184 | -- We're uninterested in the output of the 1185 | -- temporal `Property` for subsequent outputs 1186 | -- because we don't care if the temporal 1187 | -- `Property` holds for a suffix of the behavior 1188 | State.Lazy.execStateT (stepProperty input) s 1189 | 1190 | let seenKey = Seen{ _label, _processStatus, _propertyStatus = newPropertyStatus } 1191 | 1192 | let historyKey = HistoryKey _label _processStatus 1193 | 1194 | let newHistory = historyKey : _history 1195 | 1196 | let newInputHistory = input : _inputHistory 1197 | 1198 | Monad.when (HashSet.null newPropertyStatus) do 1199 | let _reason = Unsatisfiable 1200 | liftIO (Exception.throw PropertyFailed{ _inputHistory = newInputHistory, _reason }) 1201 | 1202 | Monad.when (HashSet.member historyKey _historySet && termination) do 1203 | liftIO (Exception.throw (Nontermination newHistory)) 1204 | 1205 | maybeSeen <- liftIO (HashTable.lookup hashtable seenKey) 1206 | 1207 | let member = case maybeSeen of 1208 | Nothing -> False 1209 | Just _ -> True 1210 | 1211 | Monad.when member do 1212 | liftIO (IORef.modifyIORef' redundantStatesReference (+ 1)) 1213 | empty 1214 | 1215 | liftIO (HashTable.insert hashtable seenKey ()) 1216 | 1217 | lift (put $! _processStatus) 1218 | put $! Timeline 1219 | { _history = newHistory 1220 | , _historySet = HashSet.insert historyKey _historySet 1221 | , _propertyStatus = newPropertyStatus 1222 | , _inputHistory = newInputHistory 1223 | } 1224 | 1225 | loop rest 1226 | 1227 | _successfulBranches <- handler 1228 | (Logic.runLogicT 1229 | (State.evalStateT 1230 | (State.evalStateT action uninitializedProcessStatus) 1231 | uninitializedTimeline 1232 | ) 1233 | (\_ m -> do 1234 | !n <- m 1235 | return (n + 1) 1236 | ) 1237 | (return (0 :: Natural)) 1238 | ) 1239 | 1240 | _visitedStates <- HashTable.foldM (\n _ -> return (n + 1)) 0 hashtable 1241 | 1242 | _redundantStates <- IORef.readIORef redundantStatesReference 1243 | 1244 | let stats = Statistics 1245 | { _redundantStates 1246 | , _successfulBranches 1247 | , _visitedStates 1248 | } 1249 | 1250 | Monad.when statistics do 1251 | putDoc (prettyValue (toJSON stats)) 1252 | where 1253 | uninitializedProcessStatus = 1254 | error "Internal error - Uninitialized process status" 1255 | 1256 | uninitializedTimeline = 1257 | error "Internal error - Uninitialized timeline" 1258 | 1259 | handler :: IO a -> IO a 1260 | handler 1261 | | debug = Exception.handle display 1262 | | otherwise = id 1263 | where 1264 | display exception = do 1265 | putDoc (prettyModelException exception) 1266 | 1267 | Exception.throwIO (Exit.ExitFailure 1) 1268 | 1269 | -- | The input to a temporal `Property` 1270 | data Input global label = Input 1271 | { _state :: !global 1272 | -- ^ The current (global) state 1273 | , _label :: !label 1274 | -- ^ The current label 1275 | } deriving stock (Generic, Show) 1276 | deriving anyclass (ToJSON) 1277 | 1278 | -- | A lens for accessing the global state of an `Input` 1279 | state :: Lens (Input global label) (Input global' label) global global' 1280 | state k (Input a b) = fmap (\a' -> Input a' b) (k a) 1281 | {-# INLINABLE state #-} 1282 | 1283 | -- | A lens for accessing the label of an `Input` 1284 | label :: Lens (Input global label) (Input global label') label label' 1285 | label k (Input a b) = fmap (\b' -> Input a b') (k b) 1286 | {-# INLINABLE label #-} 1287 | 1288 | {-| Lift a lens on states to a lens on `Input`s 1289 | 1290 | @ 1291 | `stately` :: Lens' outer inner -> Lens' (`Input` outer label) (`Input` inner label) 1292 | @ 1293 | -} 1294 | stately 1295 | :: Lens outer outer' inner inner' 1296 | -- ^ Lens to lift 1297 | -> Lens (Input outer label) (Input outer' label) (Input inner label) (Input inner' label) 1298 | stately l k (Input a b) = 1299 | unFocusingState (l (\a' -> FocusingState (k (Input a' b))) a) 1300 | 1301 | data FocusingState f local global = 1302 | FocusingState { unFocusingState :: f (Input global local) } 1303 | 1304 | instance Functor f => Functor (FocusingState f local) where 1305 | fmap f (FocusingState x) = FocusingState (fmap adapt x) 1306 | where 1307 | adapt (Input a b) = Input (f a) b 1308 | 1309 | {-| Lift a lens on labels to a lens on `Input`s 1310 | 1311 | @ 1312 | `labelly` :: Lens' outer inner -> Lens' (`Input` state outer) (`Input` state inner) 1313 | @ 1314 | -} 1315 | labelly 1316 | :: Lens outer outer' inner inner' 1317 | -- ^ Lens to lift 1318 | -> Lens (Input state outer) (Input state outer') (Input state inner) (Input state inner') 1319 | labelly l k (Input a b) = 1320 | unFocusingLabel (l (\b' -> FocusingLabel (k (Input a b'))) b) 1321 | 1322 | data FocusingLabel f global local = 1323 | FocusingLabel { unFocusingLabel :: f (Input global local) } 1324 | 1325 | instance Functor f => Functor (FocusingLabel f local) where 1326 | fmap f (FocusingLabel x) = FocusingLabel (fmap adapt x) 1327 | where 1328 | adapt (Input a b) = Input a (f b) 1329 | 1330 | -- | Used internally to detect cycles 1331 | data Seen label processStatus propertyStatus = Seen 1332 | { _label :: !label 1333 | , _processStatus :: !processStatus 1334 | , _propertyStatus :: !propertyStatus 1335 | } deriving stock (Eq, Generic) 1336 | deriving anyclass (Hashable) 1337 | 1338 | data Statistics = Statistics 1339 | { _successfulBranches :: Natural 1340 | , _visitedStates :: Natural 1341 | , _redundantStates :: Natural 1342 | } deriving stock (Generic) 1343 | deriving anyclass (ToJSON) 1344 | -------------------------------------------------------------------------------- /src/HasCal/Expression.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DefaultSignatures #-} 2 | {-# LANGUAGE DeriveAnyClass #-} 3 | {-# LANGUAGE DerivingStrategies #-} 4 | {-# LANGUAGE FlexibleContexts #-} 5 | {-# LANGUAGE StandaloneDeriving #-} 6 | {-# LANGUAGE TypeApplications #-} 7 | {-# LANGUAGE TypeOperators #-} 8 | 9 | {-| This module contains utilities that correspond to TLA+ expressions 10 | 11 | Note that most TLA+ functionality is covered either by Haskell's standard 12 | library (for non-temporal expressions) and the "Temporal.Property" module 13 | (for temporal expression). This module only adds a few missing utilities 14 | not covered by either of those two for parity with TLA+ and also to use 15 | names more familiar to TLA+ users. 16 | -} 17 | module HasCal.Expression 18 | ( -- * TLA+ expressions 19 | forall_ 20 | , exists_ 21 | , Boolean(..) 22 | , (==>) 23 | , (<=>) 24 | , boolean 25 | , (-->) 26 | , (|->) 27 | , domain 28 | , range 29 | , subset 30 | , choose 31 | 32 | -- * Universe 33 | , Universe(..) 34 | ) where 35 | 36 | import Control.Applicative (Alternative(..), liftA2) 37 | import Data.Hashable (Hashable) 38 | import Data.HashMap.Strict (HashMap) 39 | import Data.Monoid (Ap(..)) 40 | import GHC.Generics 41 | 42 | import qualified Control.Monad as Monad 43 | import qualified Data.Foldable as Foldable 44 | import qualified Data.HashMap.Strict as HashMap 45 | import qualified Data.List as List 46 | 47 | {- $setup 48 | 49 | >>> import Data.Functor.Identity (Identity(..)) 50 | >>> import Data.List.NonEmpty (NonEmpty(..)) 51 | -} 52 | 53 | {-| Verify that all elements satisfy the given predicate, like @\\A@ in TLA+ 54 | 55 | `forall_` is like `all` but with the arguments `flip`ped. 56 | 57 | For example, the following TLA+ code: 58 | 59 | > \A i \in 1 .. 3 : 0 < i 60 | 61 | … would correspond to this Haskell code: 62 | 63 | >>> forall_ [ 1 .. 3 ] (\i -> 0 < i) 64 | True 65 | 66 | `forall_` obeys the following laws: 67 | 68 | @ 69 | `forall_` [] f = `True` 70 | 71 | `forall_` (xs `++` ys) f = `forall_` xs f `&&` `forall_` ys f 72 | 73 | `forall_` [ x ] f = f x 74 | @ 75 | -} 76 | forall_ :: Foldable list => list a -> (a -> Bool) -> Bool 77 | forall_ = flip all 78 | {-# INLINABLE forall_ #-} 79 | 80 | {-| Verify that any element satisfies the given predicate, like @\\E@ in TLA+ 81 | 82 | `exists_` is like `any` but with the arguments `flip`ped 83 | 84 | For example, the following TLA+ code: 85 | 86 | > \E i in 1 .. 3 : i = 3 87 | 88 | … would correspond to this Haskell code: 89 | 90 | >>> exists_ [ 1 .. 3 ] (\i -> i == 3) 91 | True 92 | 93 | `exists_` obeys the following laws: 94 | 95 | @ 96 | `exists_` [] f = `False` 97 | 98 | `exists_` (xs `++` ys) f = `exists_` xs f `||` exists_ ys f 99 | 100 | `exists_` [ x ] f = f x 101 | @ 102 | -} 103 | exists_ :: Foldable list => list a -> (a -> Bool) -> Bool 104 | exists_ = flip any 105 | {-# INLINABLE exists_ #-} 106 | 107 | {-| A class for types that support boolean algebra 108 | 109 | Laws: 110 | 111 | @ 112 | (x `/\` y) `/\` z = x `/\` (y `/\` z) 113 | 114 | x `/\` `true` = x 115 | 116 | `true` `/\` x = x 117 | @ 118 | 119 | @ 120 | (x `\/` y) `\/` z = x `\/` (y `\/` z) 121 | 122 | x `\/` `false` = x 123 | 124 | `false` `\/` x = x 125 | @ 126 | 127 | @ 128 | `false` `/\` x = `false` 129 | 130 | x `/\` `false` = `false` 131 | 132 | `true` `/\` x = `true` 133 | 134 | x `/\` `true` = `true` 135 | @ 136 | 137 | @ 138 | (x `===` y) `===` z = x `===` (y `===` z) 139 | 140 | x `===` `true` = x 141 | 142 | `true` `===` x = x 143 | @ 144 | 145 | @ 146 | (x `=/=` y) `=/=` z = x `=/=` (y `=/=` z) 147 | 148 | x `=/=` `false` = x 149 | 150 | `false` `=/=` x = x 151 | @ 152 | -} 153 | class Boolean a where 154 | -- | Generalizes `True` 155 | true :: a 156 | 157 | -- | Generalizes `False` 158 | false :: a 159 | 160 | -- | Generalizes `&&` 161 | (/\) :: a -> a -> a 162 | 163 | -- | Generalizes `||` 164 | (\/) :: a -> a -> a 165 | 166 | -- | Generalizes `==` on `Bool`s 167 | (===) :: a -> a -> a 168 | 169 | -- | Generalizes `/=` on `Bool`s 170 | (=/=) :: a -> a -> a 171 | 172 | infixr 3 /\ 173 | infixr 2 \/ 174 | infixr 4 === 175 | infixr 4 =/= 176 | 177 | instance Boolean Bool where 178 | true = True 179 | 180 | false = False 181 | 182 | (/\) = (&&) 183 | 184 | (\/) = (||) 185 | 186 | (===) = (==) 187 | 188 | (=/=) = (/=) 189 | 190 | instance Boolean b => Boolean (a -> b) where 191 | true = pure true 192 | 193 | false = pure false 194 | 195 | (/\) = liftA2 (/\) 196 | 197 | (\/) = liftA2 (\/) 198 | 199 | (===) = liftA2 (===) 200 | 201 | (=/=) = liftA2 (=/=) 202 | 203 | instance (Applicative f, Boolean a) => Boolean (Ap f a) where 204 | true = Ap (pure true) 205 | 206 | false = Ap (pure false) 207 | 208 | Ap l /\ Ap r = Ap (liftA2 (/\) l r) 209 | 210 | Ap l \/ Ap r = Ap (liftA2 (\/) l r) 211 | 212 | Ap l === Ap r = Ap (liftA2 (===) l r) 213 | 214 | Ap l =/= Ap r = Ap (liftA2 (=/=) l r) 215 | 216 | {-| Logical implication, like @=>@ in TLA+ 217 | 218 | @p `==>` q@ is the same as \"if @p@ then @q@\" 219 | 220 | @ 221 | p `==>` q = (p `===` `false`) `\/` q 222 | @ 223 | -} 224 | (==>) :: Boolean bool => bool -> bool -> bool 225 | p ==> q = (p === false) \/ q 226 | {-# INLINABLE (==>) #-} 227 | 228 | {-| Bidirectional logical implication, like @<=>@ in TLA+ 229 | 230 | @p `<=>` q@ is the same as \"if and only if @p@ then @q@\" 231 | 232 | @ 233 | p `<=>` q = (p `==>` q) `&&` (q `==>` p) 234 | @ 235 | -} 236 | (<=>) :: Boolean bool => bool -> bool -> bool 237 | p <=> q = (p ==> q) /\ (q ==> p) 238 | {-# INLINABLE (<=>) #-} 239 | 240 | infixr 1 ==>, <=> 241 | 242 | {-| All possible boolean values, like the @BOOLEAN@ set in TLA+ 243 | 244 | @ 245 | `boolean` = `universe` @`Bool` 246 | @ 247 | -} 248 | boolean :: [Bool] 249 | boolean = universe @Bool 250 | 251 | {-| A function set, like the @->@ operator in TLA+ 252 | 253 | >>> [ 1, 2 ] --> [ False, True ] 254 | [fromList [(1,False),(2,False)],fromList [(1,False),(2,True)],fromList [(1,True),(2,False)],fromList [(1,True),(2,True)]] 255 | 256 | This operator also supports ranges and domains other than lists. For 257 | example, you can limit the domain or range to only one value by using 258 | `Data.Functor.Identity.Identity` instead of a list: 259 | 260 | >>> [ 1, 2 ] --> Identity True 261 | Identity (fromList [(1,True),(2,True)]) 262 | 263 | >>> Identity 1 --> [ False, True ] 264 | [fromList [(1,False)],fromList [(1,True)]] 265 | 266 | >>> Identity 1 --> Identity True 267 | Identity (fromList [(1,True)]) 268 | 269 | … and if the range has only one value then there will only be one 270 | \"function\" in the function set. 271 | 272 | This operator also works with `Maybe` for the domain or range: 273 | 274 | >>> [ 1, 2 ] --> Just True 275 | Just (fromList [(1,True),(2,True)]) 276 | 277 | >>> [ 1, 2 ] --> Nothing 278 | Nothing 279 | 280 | >>> Just 1 --> [ True, False ] 281 | [fromList [(1,True)],fromList [(1,False)]] 282 | 283 | >>> Nothing --> [ True, False ] 284 | [fromList []] 285 | 286 | … and also `Data.List.NonEmpty.NonEmpty` lists: 287 | 288 | >>> [ 1, 2 ] --> (False :| [ True ]) 289 | fromList [(1,False),(2,False)] :| [fromList [(1,False),(2,True)],fromList [(1,True),(2,False)],fromList [(1,True),(2,True)]] 290 | 291 | >>> (1 :| [ 2 ]) --> [ False, True ] 292 | [fromList [(1,False),(2,False)],fromList [(1,False),(2,True)],fromList [(1,True),(2,False)],fromList [(1,True),(2,True)]] 293 | -} 294 | (-->) 295 | :: (Traversable domain, Applicative range, Eq key, Hashable key) 296 | => domain key 297 | -- ^ Domain 298 | -> range value 299 | -- ^ Range 300 | -> range (HashMap key value) 301 | keys --> values = 302 | fmap (HashMap.fromList . Foldable.toList) (traverse process keys) 303 | where 304 | process key = fmap ((,) key) values 305 | 306 | {-| A function set, like the @|->@ operator in TLA+ 307 | 308 | For example, the following TLA+ code: 309 | 310 | > [ i \in 1 .. 3 |-> i + 1 ] 311 | 312 | … would correspond to this Haskell code: 313 | 314 | >>> [ 1 .. 3 ] |-> \i -> i + 1 315 | fromList [(1,2),(2,3),(3,4)] 316 | -} 317 | (|->) 318 | :: (Foldable list, Functor list, Eq key, Hashable key) 319 | => list key -> (key -> value) -> HashMap key value 320 | keys |-> function = HashMap.fromList (Foldable.toList (fmap adapt keys)) 321 | where 322 | adapt key = (key, function key) 323 | 324 | {-| The domain of a function set, like the @DOMAIN@ function in TLA+ 325 | 326 | `domain` is a synonym for @"Data.HashMap.Strict".`HashMap.keys`@. 327 | -} 328 | domain :: HashMap key value -> [key] 329 | domain = HashMap.keys 330 | {-# INLINE domain #-} 331 | 332 | {-| The range of a function set, like the @RANGE@ function that TLA+ projects 333 | commonly define 334 | 335 | `range` is a synonym for @"Data.HashMap.Strict".`HashMap.elems`@. 336 | -} 337 | range :: HashMap key value -> [value] 338 | range = HashMap.elems 339 | {-# INLINE range #-} 340 | 341 | {-| The powerset of a list, like the @SUBSET@ function in TLA+ 342 | 343 | >>> subset [ 1, 2, 3 ] 344 | [[],[3],[2],[2,3],[1],[1,3],[1,2],[1,2,3]] 345 | -} 346 | subset :: [a] -> [[a]] 347 | subset = Monad.filterM (\_ -> [False, True]) 348 | 349 | {-| Find the first matching element, like the @CHOOSE@ function in TLA+ except 350 | that this will return a `Nothing` instead of throwing an exception 351 | 352 | `choose` is like `List.find`, but with the arguments `flip`ped. 353 | 354 | For example, the following TLA+ code: 355 | 356 | > CHOOSE i \in 1 .. 3 : 1 < i 357 | 358 | … would correspond to this Haskell code: 359 | 360 | >>> choose [ 1 .. 3 ] (\i -> 1 < i ) 361 | Just 2 362 | 363 | `choose` obeys the following laws: 364 | 365 | @ 366 | `choose` `Control.Applicative.empty` f = `Control.Applicative.empty` 367 | 368 | `choose` (xs `Control.Applicative.<|>` ys) f = `choose` xs f `Control.Applicative.<|>` `choose` ys f 369 | @ 370 | 371 | … or equivalently: 372 | 373 | @ 374 | `choose` [] f = `Nothing` 375 | 376 | `choose` (xs `++` ys) f = `choose` xs f `Control.Applicative.<|>` `choose` ys f 377 | @ 378 | 379 | -} 380 | choose :: Foldable list => list a -> (a -> Bool) -> Maybe a 381 | choose = flip List.find 382 | {-# INLINABLE choose #-} 383 | 384 | -- | A type where all possible values can be enumerated as a list 385 | -- 386 | -- You can derive `Universe` for any type that implements `Generic`: 387 | -- 388 | -- @ 389 | -- {-# LANGUAGE DerivingStrategies #-} 390 | -- {-# LANGUAGE DeriveAnyClass #-} 391 | -- {-# LANGUAGE DeriveGeneric #-} 392 | -- 393 | -- data Example = … 394 | -- deriving stock (`Generic`) 395 | -- deriving anyclass (`Universe`) 396 | -- @ 397 | class Universe a where 398 | universe :: [a] 399 | default universe :: (Generic a, GenericUniverse (Rep a)) => [a] 400 | universe = fmap to genericUniverse 401 | 402 | deriving anyclass instance Universe () 403 | 404 | deriving anyclass instance Universe Bool 405 | 406 | class GenericUniverse f where 407 | genericUniverse :: [f a] 408 | 409 | instance GenericUniverse f => GenericUniverse (M1 i t f) where 410 | genericUniverse = fmap M1 genericUniverse 411 | 412 | instance GenericUniverse U1 where 413 | genericUniverse = pure U1 414 | 415 | instance GenericUniverse V1 where 416 | genericUniverse = empty 417 | 418 | instance (GenericUniverse l, GenericUniverse r) => GenericUniverse (l :*: r) where 419 | genericUniverse = do 420 | l <- genericUniverse 421 | r <- genericUniverse 422 | return (l :*: r) 423 | 424 | instance (GenericUniverse l, GenericUniverse r) => GenericUniverse (l :+: r) where 425 | genericUniverse = fmap L1 genericUniverse <|> fmap R1 genericUniverse 426 | 427 | instance Universe c => GenericUniverse (K1 i c) where 428 | genericUniverse = fmap K1 universe 429 | -------------------------------------------------------------------------------- /src/HasCal/Property.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BangPatterns #-} 2 | {-# LANGUAGE BlockArguments #-} 3 | {-# LANGUAGE DeriveAnyClass #-} 4 | {-# LANGUAGE DeriveGeneric #-} 5 | {-# LANGUAGE DerivingStrategies #-} 6 | {-# LANGUAGE DerivingVia #-} 7 | {-# LANGUAGE ExistentialQuantification #-} 8 | {-# LANGUAGE ScopedTypeVariables #-} 9 | {-# LANGUAGE TupleSections #-} 10 | {-# LANGUAGE TypeApplications #-} 11 | 12 | {-| This module implements support for temporal logic 13 | 14 | Specifically, the module provides a temporal `Property` type and utilities 15 | for creating and checking `Property`s against sequences of inputs and 16 | outputs 17 | -} 18 | 19 | module HasCal.Property 20 | ( 21 | -- * Property 22 | Property 23 | , eventually 24 | , always 25 | , (~>) 26 | , viewing 27 | , prime 28 | , following 29 | , infer 30 | 31 | -- * Check 32 | , Check(..) 33 | , check 34 | , checkList 35 | ) where 36 | 37 | import Control.Applicative (liftA2) 38 | import Control.Arrow (Arrow(..)) 39 | import Control.Category (Category(..)) 40 | import Control.Monad.Trans.State (State, StateT(..)) 41 | import Data.Hashable (Hashable(..)) 42 | import Data.Monoid (Ap(..)) 43 | import Data.Profunctor (Profunctor(..)) 44 | import GHC.Generics (Generic) 45 | import HasCal.Expression (Boolean(..), Universe(..), (==>)) 46 | import Lens.Micro.Platform (Getting) 47 | import Prelude hiding (id, (.)) 48 | 49 | import qualified Control.Monad as Monad 50 | import qualified Control.Monad.Trans.State as State 51 | import qualified Data.HashSet as HashSet 52 | import qualified Data.HashMap.Strict as HashMap 53 | import qualified Lens.Micro.Platform as Lens 54 | 55 | {- $setup 56 | 57 | >>> :m -Prelude 58 | >>> import Prelude hiding ((.), id) 59 | >>> import HasCal 60 | -} 61 | 62 | {-| A temporal `Property` is a stateful transformation from an @input@ to an 63 | @output@ 64 | 65 | The type variables are: 66 | 67 | * @input@: The type of the input to the property 68 | * @output@: The type of the output that changes in response to the input 69 | 70 | You can create a `Property` using: 71 | 72 | * `arr` - Lift a pure function from an @input@ to an @output@ into the 73 | equivalent `Property` 74 | * `eventually` - A `Property` that @output@s `True` if the @input@ will 75 | `eventually` be `True` (either now or in the future) 76 | * `always` - A `Property` that @output@s `True` if the @input@ will `always` 77 | be `True` (both now and in the future) 78 | * `id` - The trivial `Property` whose @output@ always matches the @input@ 79 | * `pure` - A `Property` with a constant @output@ that ignores the @input@ 80 | * `mempty` - A `Property` with a constant empty @output@ 81 | * A numeric literal - A `Property` with a constant numeric @output@ 82 | 83 | You can combine one or more `Property`s using: 84 | 85 | * (`.`) - You can compose two `Property`s end-to-end, feeding the @output@ 86 | of one property as the @input@ to another `Property` 87 | * `Applicative` operations (or @do@ notation if you enable @ApplicativeDo@) 88 | - this shares their @input@ and combines their @output@s pointwise 89 | * (`<>`) - This combines their @output@s pointwise using (`<>`) 90 | * Numeric operators - These operators combine their @output@s pointwise 91 | 92 | You can transform a `Property` using: 93 | 94 | * `fmap` - Transform the @output@ 95 | * `lmap` - Transform the @input@ 96 | 97 | You can consume a `Property` using: 98 | 99 | * `infer` - Convert a `Property` to the equivalent transformation on 100 | lists that infers the outputs from the inputs 101 | * `check` - Convert a `Property` to a `Check` for checking that a 102 | sequence of outputs is consistent with the inputs 103 | -} 104 | data Property input output = 105 | forall state . (Eq state, Hashable state, Universe state) 106 | => Property state (input -> State state output) 107 | deriving (Boolean, Monoid, Semigroup) via (Ap (Property input) output) 108 | 109 | {- Under the hood, a `Property` is essentially the exact same thing as the 110 | `Scan` type from the @foldl@ package, but with two differences: 111 | 112 | * The `Property` type is actually used to process the sequence of inputs 113 | in reverse, resulting in a sequence of outputs that are also reversed 114 | 115 | * We add more constraints to the existentially quantified @state@ type so 116 | that we can invert the scan when converting to the `Check` type 117 | -} 118 | 119 | instance Functor (Property input) where 120 | fmap f (Property s k) = Property s (fmap f . k) 121 | 122 | instance Applicative (Property input) where 123 | pure b = Property () (\_ -> pure b) 124 | 125 | Property sL kL <*> Property sR kR = Property s k 126 | where 127 | s = Pair sL sR 128 | 129 | k a = State.state step 130 | where 131 | step !(Pair l r) = (f x, Pair l' r') 132 | where 133 | (f, l') = State.runState (kL a) l 134 | (x, r') = State.runState (kR a) r 135 | 136 | instance Num output => Num (Property input output) where 137 | fromInteger = pure . fromInteger 138 | 139 | negate = fmap negate 140 | abs = fmap abs 141 | signum = fmap signum 142 | 143 | (+) = liftA2 (+) 144 | (*) = liftA2 (*) 145 | (-) = liftA2 (-) 146 | 147 | instance Fractional output => Fractional (Property input output) where 148 | fromRational = pure . fromRational 149 | 150 | recip = fmap recip 151 | 152 | (/) = liftA2 (/) 153 | 154 | instance Floating output => Floating (Property input output) where 155 | pi = pure pi 156 | 157 | exp = fmap exp 158 | sqrt = fmap sqrt 159 | log = fmap log 160 | sin = fmap sin 161 | tan = fmap tan 162 | cos = fmap cos 163 | asin = fmap asin 164 | atan = fmap atan 165 | acos = fmap acos 166 | sinh = fmap sinh 167 | tanh = fmap tanh 168 | cosh = fmap cosh 169 | asinh = fmap asinh 170 | atanh = fmap atanh 171 | acosh = fmap acosh 172 | 173 | (**) = liftA2 (**) 174 | logBase = liftA2 logBase 175 | 176 | instance Category Property where 177 | id = Property () pure 178 | 179 | Property sL kL . Property sR kR = Property s k 180 | where 181 | s = Pair sL sR 182 | 183 | k a = State.state step 184 | where 185 | step !(Pair l r) = (c, Pair l' r') 186 | where 187 | (b, r') = State.runState (kR a) r 188 | (c, l') = State.runState (kL b) l 189 | 190 | instance Arrow Property where 191 | arr f = fmap f id 192 | 193 | first (Property state step) = Property state step' 194 | where 195 | step' (a, b) = 196 | State.state (\s -> first (,b) (State.runState (step a) s)) 197 | 198 | second (Property state step) = Property state step' 199 | where 200 | step' (a, b) = 201 | State.state (\s -> first (a,) (State.runState (step b) s)) 202 | 203 | instance Profunctor Property where 204 | lmap f (Property s k) = Property s (k . f) 205 | 206 | rmap = fmap 207 | 208 | data Pair a b = Pair !a !b 209 | deriving stock (Eq, Generic) 210 | deriving anyclass (Hashable) 211 | 212 | instance (Universe a, Universe b) => Universe (Pair a b) where 213 | universe = liftA2 Pair universe universe 214 | 215 | {-| This property outputs `True` if the current input or any future input is 216 | `True`, and outputs `False` otherwise 217 | 218 | This is equivalent to the @<>@ temporal operator from TLA+ 219 | 220 | >>> infer eventually [ False, True, False ] 221 | [True,True,False] 222 | -} 223 | eventually :: Property Bool Bool 224 | eventually = Property False (\l -> State.state (\r -> let b = l || r in (b, b))) 225 | {-# INLINABLE eventually #-} 226 | 227 | {-| This property outputs `False` if the current input or any future input is 228 | `False`, and outputs `True` otherwise 229 | 230 | This is equivalent to the @[]@ temporal operator from TLA+ 231 | 232 | >>> infer always [ True, False, True ] 233 | [False,False,True] 234 | -} 235 | always :: Property Bool Bool 236 | always = Property True (\l -> State.state (\r -> let b = l && r in (b, b))) 237 | {-# INLINABLE always #-} 238 | 239 | {-| @f `~>` g@ returns `True` if every `True` output of @f@ is eventually 240 | followed by a `True` output from @g@ 241 | 242 | > f ~> g = always . (liftA2 (==>) f (eventually . g)) 243 | 244 | This is equivalent to the @~>@ temporal operator from TLA+ 245 | 246 | >>> infer (arr even ~> arr odd) [ 1, 2, 3, 4 ] 247 | [False,False,False,False] 248 | >>> infer (arr even ~> arr odd) [ 0, 1, 2, 3 ] 249 | [True,True,True,True] 250 | -} 251 | (~>) :: Property a Bool -> Property a Bool -> Property a Bool 252 | f ~> g = always . (liftA2 (==>) f (eventually . g)) 253 | {-# INLINABLE (~>) #-} 254 | 255 | {-| Turn a getter into the equivalent `Property` 256 | 257 | > `viewing` getter = `arr` (`Lens.view` getter) 258 | 259 | This comes in handy in conjunction with the `HasCal.Coroutine.state` and 260 | `HasCal.Coroutine.label` lenses: 261 | 262 | > `eventually` . `viewing` (`label` . `to` (== "foo")) 263 | > :: Property (Input global String) Bool 264 | 265 | >>> infer (viewing _1) [ (True, 1), (False, 2), (True, 3) ] 266 | [True,False,True] 267 | -} 268 | viewing :: Getting b a b -> Property a b 269 | viewing getter = arr (Lens.view getter) 270 | {-# INLINABLE viewing #-} 271 | 272 | {-| This property outputs each element with the following element (or `Nothing` 273 | if there is no following element) 274 | 275 | This is called \"prime\" as a reference to the TLA+ convention of referring 276 | to the next value of @x@ using @x'@ (i.e. \"@x@ prime\") 277 | 278 | >>> infer prime [ False, True, True ] 279 | [Just (False,True),Just (True,True),Nothing] 280 | -} 281 | prime :: (Eq a, Hashable a, Universe a) => Property a (Maybe (a, a)) 282 | prime = Property Zero step 283 | where 284 | step a0 = State.state f 285 | where 286 | f Zero = (Nothing, One a0) 287 | f (One a1 ) = (Just (a0, a1), Two a0 a1) 288 | f (Two a1 _) = (Just (a0, a1), Two a0 a1) 289 | {-# INLINABLE prime #-} 290 | 291 | data Prime a = Zero | One !a | Two !a !a 292 | deriving (Eq, Generic, Hashable) 293 | 294 | instance Universe a => Universe (Prime a) where 295 | universe = Zero : fmap One universe <> liftA2 Two universe universe 296 | 297 | {-| This is a more ergonomic version of `prime` for the common case where you 298 | want to compare each temporal input against the next input using an 299 | equivalence relation 300 | 301 | >>> infer (following (/=)) [ False, False, True, False ] 302 | [False,True,True,True] 303 | -} 304 | following 305 | :: (Eq input, Hashable input, Universe input) 306 | => (input -> input -> Bool) -> Property input Bool 307 | following (?) = arr adapt . prime 308 | where 309 | adapt Nothing = True 310 | adapt (Just (x, y)) = x ? y 311 | {-# INLINABLE following #-} 312 | 313 | {-| Convert a `Property` into the equivalent list transformation 314 | 315 | >>> infer (arr even) [ 2, 3, 5 ] 316 | [True,False,False] 317 | >>> infer eventually [ False, True, False ] 318 | [True,True,False] 319 | >>> infer always [ True, False, True ] 320 | [False,False,True] 321 | >>> infer (always . arr odd) [ 2, 3, 5 ] 322 | [False,True,True] 323 | >>> infer (eventually . arr even) [ 2, 3, 5 ] 324 | [True,False,False] 325 | >>> infer (eventually . always . arr odd) [ 2, 3, 5 ] 326 | [True,True,True] 327 | >>> infer (eventually . always . arr even) [ 2, 3, 5 ] 328 | [False,False,False] 329 | 330 | Note that `infer` has to `reverse` the list twice in order to infer the 331 | outputs from the inputs, so `infer` does not run in constant space. Use 332 | `check` or `checkList` if you want something that processes the input in a 333 | single forward pass. 334 | -} 335 | infer :: Property input output -> [input] -> [output] 336 | infer (Property s k) as = 337 | reverse (State.evalState (traverse k (reverse as)) s) 338 | 339 | {-| A `Check` is like a temporal `Property` except that you can check a sequence 340 | of @input@s against a corresponding sequence of @output@s in a single 341 | forward pass in constant space. This `Check` has the following two 342 | performance properties: 343 | 344 | * The `Check` can terminate early with a negative result if it determines 345 | that the temporal `Property` can no longer hold, regardless of future 346 | @input@ / @output@ values 347 | 348 | * The `Check` cannot terminate early if the @input@ / @output@ pairs satisfy 349 | the original temporal `Property`. The entire sequence must be traversed 350 | in order to establish an affirmative result 351 | 352 | Unlike `Property`, a `Check` cannot infer the @output@s from the @input@s 353 | because doing so would require knowing in advance what the future @input@s 354 | would be, and that is incompatible with traversing the @input@s in a single 355 | forward pass. For example, @`check` `always`@ cannot necessarily tell if 356 | the current @output@ should return `True` until it has seen all future 357 | @input@s, too. 358 | 359 | Other than the difference in algorithmic complexity, the `Check` type is 360 | similar to the `Property` type, meaning that they both share the same type 361 | parameters and the same instances. However, you generally should prefer to 362 | use the instances for the `Property` type because those are more efficient. 363 | 364 | The main difference between `Property` and `Check` is that the `Property` 365 | type is abstract, whereas the `Check` type is not That means that you can 366 | pattern match on the `Check` type in order to obtain two values: 367 | 368 | * A stateful step function that you feed an inputs to get a list of 369 | acceptable outputs 370 | * An expected final state 371 | 372 | Study the source code for the `checkList` utility if you want to see an 373 | example for how you would use these two values to validate a list of 374 | @input@ / @output@ pairs against a temporal `Property`. 375 | -} 376 | data Check input output = 377 | forall state . (Eq state, Hashable state, Universe state) 378 | => Check 379 | state 380 | -- ^ Expected final state 381 | (input -> StateT state [] output) 382 | -- ^ Given an @input@ and an old @state@, return a list of possible 383 | -- new @(output, state)@ pairs 384 | deriving (Boolean, Monoid, Semigroup) via (Ap (Check input) output) 385 | 386 | instance Functor (Check input) where 387 | fmap f (Check s k) = Check s (fmap f . k) 388 | 389 | instance Applicative (Check input) where 390 | pure b = Check () (\_ -> pure b) 391 | 392 | Check sL kL <*> Check sR kR = Check s k 393 | where 394 | s = Pair sL sR 395 | 396 | k a = StateT step 397 | where 398 | step !(Pair l r) = do 399 | (f, l') <- State.runStateT (kL a) l 400 | (x, r') <- State.runStateT (kR a) r 401 | return (f x, Pair l' r') 402 | 403 | instance Num output => Num (Check input output) where 404 | fromInteger = pure . fromInteger 405 | 406 | negate = fmap negate 407 | abs = fmap abs 408 | signum = fmap signum 409 | 410 | (+) = liftA2 (+) 411 | (*) = liftA2 (*) 412 | (-) = liftA2 (-) 413 | 414 | instance Fractional output => Fractional (Check input output) where 415 | fromRational = pure . fromRational 416 | 417 | recip = fmap recip 418 | 419 | (/) = liftA2 (/) 420 | 421 | instance Floating output => Floating (Check input output) where 422 | pi = pure pi 423 | 424 | exp = fmap exp 425 | sqrt = fmap sqrt 426 | log = fmap log 427 | sin = fmap sin 428 | tan = fmap tan 429 | cos = fmap cos 430 | asin = fmap asin 431 | atan = fmap atan 432 | acos = fmap acos 433 | sinh = fmap sinh 434 | tanh = fmap tanh 435 | cosh = fmap cosh 436 | asinh = fmap asinh 437 | atanh = fmap atanh 438 | acosh = fmap acosh 439 | 440 | (**) = liftA2 (**) 441 | logBase = liftA2 logBase 442 | 443 | instance Category Check where 444 | id = Check () pure 445 | 446 | Check sL kL . Check sR kR = Check s k 447 | where 448 | s = Pair sL sR 449 | 450 | k a = StateT step 451 | where 452 | step !(Pair l r) = do 453 | (b, r') <- State.runStateT (kR a) r 454 | (c, l') <- State.runStateT (kL b) l 455 | return (c, Pair l' r') 456 | 457 | instance Profunctor Check where 458 | lmap f (Check s k) = Check s (k . f) 459 | 460 | rmap = fmap 461 | 462 | {-| Convert a `Property` into a `Check` 463 | 464 | See the documentation for `Check` for more details on how to use a 465 | `Check` 466 | 467 | This is mostly used to implement `HasCal.Coroutine.model`, but you 468 | can use this to implement your own efficient temporal property 469 | checker 470 | -} 471 | check :: Property input output -> Check input output 472 | check (Property s k) = Check s k' 473 | where 474 | relation a = HashMap.fromListWith (<>) (fmap adapt universe) 475 | where 476 | adapt old = (new, [(b, old)]) 477 | where 478 | (b, new) = State.runState (k a) old 479 | 480 | k' a = StateT (\new -> HashMap.findWithDefault [] new (relation a)) 481 | {-# INLINABLE check #-} 482 | 483 | {-| This function checks that a list of @input@ and @output@ pairs is 484 | consistent with the given temporal `Property` 485 | 486 | You can think of `checkList` as having the following definition: 487 | 488 | > checkList property pairs = infer property inputs == outputs 489 | > where 490 | > (inputs, outputs) = unzip pairs 491 | 492 | … except that `checkList` processes the list in a single forward pass 493 | (unlike `infer`) 494 | 495 | >>> checkList eventually [(False, True), (True, True), (False, False)] 496 | True 497 | -} 498 | checkList :: Eq output => Property input output -> [(input, output)] -> Bool 499 | checkList temporal = 500 | case check temporal of 501 | Check finalState k -> loop (HashSet.fromList universe) 502 | where 503 | loop states [] = 504 | HashSet.member finalState states 505 | loop states ((input, output) : pairs) 506 | | HashSet.null states = False 507 | | otherwise = loop newStates pairs 508 | where 509 | newStates = HashSet.fromList do 510 | state <- HashSet.toList states 511 | 512 | (output', newState) <- State.runStateT (k input) state 513 | 514 | Monad.guard (output == output') 515 | 516 | return newState 517 | -------------------------------------------------------------------------------- /tasty/HasCal/Test/API.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ApplicativeDo #-} 2 | {-# LANGUAGE BlockArguments #-} 3 | {-# LANGUAGE DeriveAnyClass #-} 4 | {-# LANGUAGE DeriveGeneric #-} 5 | {-# LANGUAGE RecordWildCards #-} 6 | {-# LANGUAGE TemplateHaskell #-} 7 | 8 | {-| This example is from the "Example: Rate Limiting" section of the "Learn 9 | TLA+" guide 10 | 11 | > ---- MODULE api ---- 12 | > EXTENDS Integers, TLC 13 | > (* --algorithm api 14 | > variables made_calls = 0, max_calls \in 5..10, reserved_calls = 0; 15 | > 16 | > 17 | > macro make_calls(n) begin 18 | > made_calls := made_calls + n; 19 | > assert made_calls <= max_calls; 20 | > end macro; 21 | > 22 | > 23 | > macro reserve(n) begin 24 | > await made_calls + reserved_calls + n <= max_calls; 25 | > reserved_calls := reserved_calls + n; 26 | > end macro 27 | > 28 | > process reset_limit = -1 29 | > begin 30 | > Reset: 31 | > while TRUE do 32 | > made_calls := 0; 33 | > end while 34 | > end process 35 | > 36 | > process get_collection = 0 37 | > begin 38 | > GCGetCalls: 39 | > reserve(1); 40 | > Request: 41 | > make_calls(1); 42 | > reserved_calls := reserved_calls - 1; 43 | > either goto GCGetCalls 44 | > or skip 45 | > end either; 46 | > end process; 47 | > 48 | > process get_put \in 1..3 49 | > begin 50 | > GPGetCalls: 51 | > reserve(2); 52 | > Call: 53 | > with c \in {1, 2} do 54 | > make_calls(c) 55 | > end with; 56 | > reserved_calls := reserved_calls - 2; 57 | > end process; 58 | > 59 | > end algorithm; *) 60 | 61 | -} 62 | 63 | module HasCal.Test.API where 64 | 65 | import HasCal 66 | import Prelude hiding ((.)) 67 | import Test.Tasty (TestTree) 68 | 69 | import qualified Test.Tasty.HUnit as HUnit 70 | 71 | data Global = Global 72 | { _made_calls :: Int 73 | , _max_calls :: Int 74 | , _reserved_calls :: Int 75 | } deriving (Eq, Generic, Hashable, Show, ToJSON) 76 | 77 | data ResetLimitLabel = ResetBegin | Reset 78 | deriving (Eq, Generic, Hashable, Show, ToJSON) 79 | 80 | data GetCollectionLabel = GetCollectionBegin | GCGetCalls | Request 81 | deriving (Eq, Generic, Hashable, Show, ToJSON) 82 | 83 | data GetPutLabel = GetPutBegin | GPGetCalls | Call 84 | deriving (Eq, Generic, Hashable, Show, ToJSON) 85 | 86 | data Labels = Labels 87 | { _reset_limit :: ResetLimitLabel 88 | , _get_collection :: GetCollectionLabel 89 | , _get_put :: GetPutLabel 90 | } deriving (Eq, Generic, Hashable, Show, ToJSON) 91 | 92 | makeLenses ''Global 93 | 94 | reserve :: Int -> Process Global local label () 95 | reserve n = do 96 | Global{..} <- use global 97 | 98 | await (_made_calls + _reserved_calls + n <= _max_calls) 99 | 100 | global.reserved_calls += n 101 | 102 | make_calls :: (Show local, ToJSON local) => Int -> Process Global local label () 103 | make_calls n = do 104 | global.made_calls += n 105 | 106 | Global{..} <- use global 107 | assert (_made_calls <= _max_calls) 108 | 109 | test_api :: TestTree 110 | test_api = HUnit.testCase "API" do 111 | let reset_limit = Coroutine 112 | { startingLabel = ResetBegin 113 | 114 | , startingLocals = pure () 115 | 116 | , process = do 117 | while true do 118 | yield Reset 119 | global.made_calls .= 0 120 | } 121 | 122 | let get_collection = Coroutine 123 | { startingLabel = GetCollectionBegin 124 | 125 | , startingLocals = pure () 126 | 127 | , process = do 128 | let gcGetCalls = do 129 | yield GCGetCalls 130 | 131 | reserve 1 132 | 133 | yield Request 134 | 135 | make_calls 1 136 | 137 | global.reserved_calls -= 1 138 | 139 | gcGetCalls <|> skip 140 | 141 | gcGetCalls 142 | } 143 | 144 | let get_put = Coroutine 145 | { startingLabel = GetPutBegin 146 | 147 | , startingLocals = pure () 148 | 149 | , process = do 150 | yield GPGetCalls 151 | 152 | reserve 2 153 | 154 | yield Call 155 | 156 | c <- with [ 1, 2 ] 157 | 158 | make_calls c 159 | 160 | global.reserved_calls -= 2 161 | } 162 | 163 | model defaultModel 164 | { startingGlobals = do 165 | let _made_calls = 0 166 | 167 | _max_calls <- [ 5 .. 10 ] 168 | 169 | let _reserved_calls = 0 170 | 171 | return Global{..} 172 | 173 | , coroutine = do 174 | _reset_limit <- reset_limit 175 | _get_collection <- get_collection 176 | _get_put <- get_put 177 | return Labels{..} 178 | 179 | , property = true 180 | 181 | , termination = False 182 | } 183 | -------------------------------------------------------------------------------- /tasty/HasCal/Test/AsyncInterface.hs: -------------------------------------------------------------------------------- 1 | {-| This is based on the [AsynchInterface example](https://github.com/tlaplus/Examples/blob/master/specifications/SpecifyingSystems/AsynchronousInterface/AsynchInterface.tla) 2 | from figure 3.1 on page 27 in Lamport's \"Specifying Systems\" book: 3 | 4 | > ------------------ MODULE AsynchInterface --------------------- 5 | > EXTENDS Naturals 6 | > CONSTANT Data 7 | > VARIABLES val, rdy, ack 8 | > 9 | > TypeInvariant == /\ val \in Data 10 | > /\ rdy \in {0, 1} 11 | > /\ ack \in {0, 1} 12 | > --------------------------------------------------------------- 13 | > Init == /\ val \in Data 14 | > /\ rdy \in {0, 1} 15 | > /\ ack = rdy 16 | > 17 | > Send == /\ rdy = ack 18 | > /\ val' \in Data 19 | > /\ rdy' = 1 - rdy 20 | > /\ UNCHANGED ack 21 | > 22 | > Rcv == /\ rdy # ack 23 | > /\ ack' = 1 - ack 24 | > /\ UNCHANGED <> 25 | > 26 | > Next == Send \/ Rcv 27 | > 28 | > Spec == Init /\ [][Next]_<> 29 | > --------------------------------------------------------------- 30 | > THEOREM Spec => []TypeInvariant 31 | > =============================================================== 32 | -} 33 | 34 | {-# LANGUAGE BlockArguments #-} 35 | {-# LANGUAGE DeriveAnyClass #-} 36 | {-# LANGUAGE DeriveGeneric #-} 37 | {-# LANGUAGE RecordWildCards #-} 38 | {-# LANGUAGE TemplateHaskell #-} 39 | 40 | module HasCal.Test.AsyncInterface where 41 | 42 | import HasCal 43 | import Prelude hiding ((.)) 44 | import Test.Tasty (TestTree) 45 | 46 | import qualified Test.Tasty.HUnit as HUnit 47 | import qualified Control.Monad as Monad 48 | 49 | data Chan = Chan { _val :: Data, _rdy :: Bool, _ack :: Bool } 50 | deriving (Eq, Generic, Hashable, Show, ToJSON) 51 | 52 | data Label = Init | Send | Rcv deriving (Eq, Generic, Hashable, Show, ToJSON) 53 | 54 | data Data = D1 | D2 | D3 deriving (Eq, Generic, Hashable, Show, ToJSON, Universe) 55 | 56 | makeLenses ''Chan 57 | 58 | -- `send` and `rcv` are factored out into top-level utilities so that they can 59 | -- be reused by the "HasCal.Test.FIFO" example 60 | 61 | send :: Data -> Process Chan local label () 62 | send message = do 63 | Chan{..} <- use global 64 | await (_rdy == _ack) 65 | global.val .= message 66 | global.rdy %= not 67 | 68 | rcv :: Process Chan local label () 69 | rcv = do 70 | Chan{..} <- use global 71 | await (_rdy /= _ack) 72 | global.ack %= not 73 | 74 | channelModel :: Model Chan Label 75 | channelModel = defaultModel 76 | { termination = False 77 | 78 | , startingGlobals = do 79 | _val <- universe 80 | _rdy <- universe 81 | let _ack = _rdy 82 | return Chan{..} 83 | 84 | , coroutine = Coroutine 85 | { startingLabel = Init 86 | 87 | , startingLocals = pure () 88 | 89 | , process = Monad.forever 90 | ( (do msg <- with universe 91 | send msg 92 | yield Send 93 | ) 94 | <|> (do rcv 95 | yield Rcv 96 | ) 97 | ) 98 | } 99 | 100 | , property = true 101 | } 102 | 103 | test_asyncInterface :: TestTree 104 | test_asyncInterface = HUnit.testCase "Async interface" do 105 | model channelModel 106 | -------------------------------------------------------------------------------- /tasty/HasCal/Test/DieHard.hs: -------------------------------------------------------------------------------- 1 | {-| The following is the "Die Hard" 2 | [example](https://github.com/tlaplus/Examples/blob/master/specifications/DieHard/DieHard.tla) 3 | from Lamport's [*TLA+ Video 4 | Course*](http://lamport.azurewebsites.net/video/videos.html). 5 | 6 | > VARIABLES small, big 7 | > 8 | > TypeOK = /\ small \in 0..3 9 | > /\ big \in 0..5 10 | > 11 | > Init == /\ big = 0 12 | > /\ small = 0 13 | > 14 | > FillSmall == /\ small' = 3 15 | > /\ big' = big 16 | > 17 | > FillBig == /\ big' = 5 18 | > /\ small' = small 19 | > 20 | > EmptySmall == /\ small' = 0 21 | > /\ big' = big 22 | > 23 | > EmptyBig == /\ big' = 0 24 | > /\ small' = small 25 | > 26 | > SmallToBig == /\ big' = Min(big + small, 5) 27 | > /\ small' = small - (big' - big) 28 | > 29 | > BigToSmall == /\ small' = Min(big + small, 3) 30 | > /\ big' = big - (small' - small) 31 | 32 | This module ports the above TLA+ code to HasCal. 33 | -} 34 | 35 | {-# LANGUAGE BlockArguments #-} 36 | {-# LANGUAGE DeriveAnyClass #-} 37 | {-# LANGUAGE DeriveGeneric #-} 38 | {-# LANGUAGE RecordWildCards #-} 39 | {-# LANGUAGE TemplateHaskell #-} 40 | {-# LANGUAGE TypeApplications #-} 41 | 42 | module HasCal.Test.DieHard where 43 | 44 | import HasCal 45 | import Prelude hiding (either, init, (.)) 46 | import Test.Tasty (TestTree) 47 | 48 | import qualified Test.Tasty.HUnit as HUnit 49 | import qualified Test.Tasty.ExpectedFailure as Failure 50 | import qualified Control.Monad as Monad 51 | 52 | data Global = Global { _small :: Int, _big :: Int } 53 | deriving (Eq, Generic, Hashable, Show, ToJSON) 54 | 55 | makeLenses ''Global 56 | 57 | data Label = Init | FillSmall | FillBig | EmptySmall | EmptyBig | SmallToBig | BigToSmall 58 | deriving (Eq, Generic, Hashable, Show, ToJSON) 59 | 60 | typeOK :: Global -> Bool 61 | typeOK Global {..} = _small `elem` [0..3] && _big `elem` [0..5] 62 | 63 | init, fillSmall, fillBig, emptySmall, emptyBig, smallToBig, bigToSmall :: Process Global () Label () 64 | 65 | init = Monad.forever next 66 | 67 | fillSmall = do 68 | yield FillSmall 69 | global.small .= 3 70 | 71 | fillBig = do 72 | yield FillBig 73 | global.big .= 5 74 | 75 | emptySmall = do 76 | yield EmptySmall 77 | global.small .= 0 78 | 79 | emptyBig = do 80 | yield EmptyBig 81 | global.big .= 0 82 | 83 | smallToBig = do 84 | yield SmallToBig 85 | _small <- use (global.small) 86 | _big <- use (global.big) 87 | _big' <- global.big <.= min (_big + _small) 5 88 | global.small .= _small - (_big' - _big) 89 | 90 | bigToSmall = do 91 | yield BigToSmall 92 | _small <- use (global.small) 93 | _big <- use (global.big) 94 | _small' <- global.small <.= min (_big + _small) 3 95 | global.big .= _big - (_small' - _small) 96 | 97 | next :: Process Global () Label () 98 | next = 99 | either 100 | [ fillSmall 101 | , fillBig 102 | , emptySmall 103 | , emptyBig 104 | , smallToBig 105 | , bigToSmall 106 | ] 107 | 108 | test_dieHard :: TestTree 109 | test_dieHard = 110 | Failure.expectFailBecause "The solution to the puzzle is the counterexample" do 111 | HUnit.testCase "Die Hard" do 112 | model defaultModel 113 | { termination = False 114 | 115 | , startingGlobals = do 116 | let _small = 0 117 | _big = 0 118 | return Global{..} 119 | 120 | , coroutine = Coroutine 121 | { startingLabel = Init 122 | , startingLocals = pure () 123 | , process = init 124 | } 125 | 126 | , property = always . viewing (state . big . to (/= 4)) 127 | } 128 | -------------------------------------------------------------------------------- /tasty/HasCal/Test/DiningPhilosophers.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BlockArguments #-} 2 | {-# LANGUAGE DeriveAnyClass #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | {-# LANGUAGE TemplateHaskell #-} 6 | 7 | {-| This example is from the "Processes" section of the "Learn TLA+" guide 8 | 9 | > EXTENDS Integers, Sequences, TLC, FiniteSets 10 | > CONSTANTS NumPhilosophers, NULL 11 | > ASSUME NumPhilosophers > 0 12 | > NP == NumPhilosophers 13 | > 14 | > (* --algorithm dining_philosophers 15 | > 16 | > variables forks = [fork \in 1..NP |-> NULL] 17 | > 18 | > define 19 | > LeftFork(p) == p 20 | > RightFork(p) == IF p = NP THEN 1 ELSE p + 1 21 | > 22 | > HeldForks(p) == 23 | > { x \in {LeftFork(p), RightFork(p)}: forks[x] = p} 24 | > 25 | > AvailableForks(p) == 26 | > { x \in {LeftFork(p), RightFork(p)}: forks[x] = NULL} 27 | > 28 | > end define; 29 | > process philosopher \in 1..NP 30 | > variables hungry = TRUE; 31 | > begin P: 32 | > while hungry do 33 | > with fork \in AvailableForks(self) do 34 | > forks[fork] := self; 35 | > end with; 36 | > Eat: 37 | > if Cardinality(HeldForks(self)) = 2 then 38 | > hungry := FALSE; 39 | > forks[LeftFork(self)] := NULL || 40 | > forks[RightFork(self)] := NULL; 41 | > end if; 42 | > end while; 43 | > end process; 44 | > end algorithm; *) 45 | 46 | -} 47 | 48 | module HasCal.Test.DiningPhilosophers where 49 | 50 | import HasCal 51 | import Prelude hiding ((.)) 52 | import Test.Tasty (TestTree) 53 | 54 | import qualified Control.Monad as Monad 55 | import qualified Test.Tasty.ExpectedFailure as Failure 56 | import qualified Test.Tasty.HUnit as HUnit 57 | 58 | type Fork = Int 59 | 60 | type Philosopher = Int 61 | 62 | data Global = Global { _forks :: HashMap Fork (Maybe Philosopher) } 63 | deriving (Eq, Generic, Hashable, ToJSON, Show) 64 | 65 | data Local = Local { _hungry :: Bool } 66 | deriving (Eq, Generic, Hashable, ToJSON, Show) 67 | 68 | data Labels = P | Eat 69 | deriving (Eq, Generic, Hashable, ToJSON, Show) 70 | 71 | makeLenses ''Global 72 | makeLenses ''Local 73 | 74 | diningPhilosophers :: Int -> IO () 75 | diningPhilosophers numPhilosophers = do 76 | let leftFork :: Philosopher -> Fork 77 | leftFork p = p 78 | 79 | let rightFork :: Philosopher -> Fork 80 | rightFork p = if p == numPhilosophers then 1 else p + 1 81 | 82 | let coroutine self = Coroutine 83 | { startingLabel = P 84 | 85 | , startingLocals = do 86 | let _hungry = True 87 | return Local{..} 88 | 89 | , process = do 90 | while (use (local.hungry)) do 91 | oldForks <- use (global.forks) 92 | 93 | let availableForks :: Philosopher -> [Fork] 94 | availableForks p = do 95 | x <- [ leftFork p, rightFork p ] 96 | Monad.guard (oldForks ^? ix x == Just Nothing) 97 | return x 98 | 99 | fork <- with (availableForks self) 100 | 101 | global.forks.ix fork .= Just self 102 | 103 | yield Eat 104 | 105 | newForks <- use (global.forks) 106 | 107 | let heldForks :: Philosopher -> [Fork] 108 | heldForks p = do 109 | x <- [ leftFork p, rightFork p ] 110 | Monad.guard (newForks ^? ix x == Just (Just p)) 111 | return x 112 | 113 | Monad.when (length (heldForks self) == 2) do 114 | local.hungry .= False 115 | global.forks.ix (leftFork self) .= Nothing 116 | global.forks.ix (rightFork self) .= Nothing 117 | } 118 | 119 | model defaultModel 120 | { startingGlobals = do 121 | let _forks = [ 1 .. numPhilosophers ] |-> \_fork -> Nothing 122 | return Global{..} 123 | 124 | , coroutine = traverse coroutine [ 1 .. numPhilosophers ] 125 | 126 | , property = true 127 | } 128 | 129 | test_diningPhilosophers :: TestTree 130 | test_diningPhilosophers = 131 | Failure.expectFailBecause "The original example has a deliberate deadlock" do 132 | HUnit.testCase "Dining philosophers" do 133 | diningPhilosophers 2 134 | -------------------------------------------------------------------------------- /tasty/HasCal/Test/EuclidAlg.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BlockArguments #-} 2 | {-# LANGUAGE DeriveAnyClass #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | {-# LANGUAGE TemplateHaskell #-} 6 | 7 | {-| This example is taken from sections 2.0 through 2.4 of "A PlusCal User's 8 | Manual" 9 | -} 10 | 11 | module HasCal.Test.EuclidAlg where 12 | 13 | import Control.Monad (when) 14 | import HasCal 15 | import Prelude hiding (gcd, print, (.)) 16 | import Test.Tasty (TestTree) 17 | 18 | import qualified Prelude 19 | import qualified Test.Tasty.HUnit as HUnit 20 | 21 | data Global = Global { _u :: Int, _v :: Int } 22 | deriving (Eq, Generic, Hashable, Show, ToJSON) 23 | 24 | makeLenses ''Global 25 | 26 | initialU :: Int 27 | initialU = 24 28 | 29 | euclidAlg :: Int -> IO () 30 | euclidAlg n = do 31 | model defaultModel 32 | { startingGlobals = do 33 | _v <- [ 1 .. n ] 34 | let _u = initialU 35 | return Global{..} 36 | 37 | , coroutine = Coroutine 38 | { startingLabel = () 39 | 40 | , startingLocals = pure () 41 | 42 | , process = do 43 | initialV <- use (global.v) 44 | while (do u_ <- use (global.u); return (u_ /= 0)) do 45 | tempU <- use (global.u) 46 | tempV <- use (global.v) 47 | when (tempU < tempV) do 48 | global.u .= tempU 49 | global.v .= tempV 50 | newV <- use (global.v) 51 | global.u -= newV 52 | finalV <- use (global.v) 53 | assert (Just finalV == gcd initialU initialV) 54 | assert (finalV == Prelude.gcd initialU initialV) 55 | } 56 | 57 | , property = true 58 | } 59 | 60 | gcd :: Int -> Int -> Maybe Int 61 | gcd x y = 62 | choose [ 1 .. x ] \i -> 63 | x `mod` i == 0 64 | && y `mod` i == 0 65 | && forall_ [ 1 .. x ] \j -> 66 | x `mod` j == 0 67 | && y `mod` j == 0 68 | ==> i >= j 69 | 70 | test_euclidAlg :: TestTree 71 | test_euclidAlg = HUnit.testCase "Euclid's algorithm" do 72 | euclidAlg 4 73 | -------------------------------------------------------------------------------- /tasty/HasCal/Test/FIFO.hs: -------------------------------------------------------------------------------- 1 | {-| This is based on the [Channel module](https://github.com/tlaplus/Examples/blob/master/specifications/SpecifyingSystems/FIFO/Channel.tla) 2 | from figure 3.2 on page 30 in Lamport's \"Specifying Systems\" book: 3 | 4 | > -------------------------- MODULE Channel ----------------------------- 5 | > EXTENDS Naturals 6 | > CONSTANT Data 7 | > VARIABLE chan 8 | > 9 | > TypeInvariant == chan \in [val : Data, rdy : {0, 1}, ack : {0, 1}] 10 | > ----------------------------------------------------------------------- 11 | > Init == /\ TypeInvariant 12 | > /\ chan.ack = chan.rdy 13 | > 14 | > Send(d) == /\ chan.rdy = chan.ack 15 | > /\ chan' = [chan EXCEPT !.val = d, !.rdy = 1 - @] 16 | > 17 | > Rcv == /\ chan.rdy # chan.ack 18 | > /\ chan' = [chan EXCEPT !.ack = 1 - @] 19 | > 20 | > Next == (\E d \in Data : Send(d)) \/ Rcv 21 | > 22 | > Spec == Init /\ [][Next]_chan 23 | > ----------------------------------------------------------------------- 24 | > THEOREM Spec => []TypeInvariant 25 | > ======================================================================= 26 | 27 | … and the [InnerFIFO module](https://github.com/tlaplus/Examples/blob/master/specifications/SpecifyingSystems/FIFO/InnerFIFO.tla) 28 | from figure 4.1 on page 38 in Lamport's \"Specifying Systems\" book: 29 | 30 | > ---------------------------- MODULE InnerFIFO ------------------------------- 31 | > EXTENDS Naturals, Sequences 32 | > CONSTANT Message 33 | > VARIABLES in, out, q 34 | > InChan == INSTANCE Channel WITH Data <- Message, chan <- in 35 | > OutChan == INSTANCE Channel WITH Data <- Message, chan <- out 36 | > ----------------------------------------------------------------------------- 37 | > Init == /\ InChan!Init 38 | > /\ OutChan!Init 39 | > /\ q = << >> 40 | > 41 | > TypeInvariant == /\ InChan!TypeInvariant 42 | > /\ OutChan!TypeInvariant 43 | > /\ q \in Seq(Message) 44 | > 45 | > SSend(msg) == /\ InChan!Send(msg) \* Send msg on channel `in'. 46 | > /\ UNCHANGED <> 47 | > 48 | > BufRcv == /\ InChan!Rcv \* Receive message from channel `in'. 49 | > /\ q' = Append(q, in.val) \* and append it to tail of q. 50 | > /\ UNCHANGED out 51 | > 52 | > BufSend == /\ q # << >> \* Enabled only if q is nonempty. 53 | > /\ OutChan!Send(Head(q)) \* Send Head(q) on channel `out' 54 | > /\ q' = Tail(q) \* and remove it from q. 55 | > /\ UNCHANGED in 56 | > 57 | > RRcv == /\ OutChan!Rcv \* Receive message from channel `out'. 58 | > /\ UNCHANGED <> 59 | > 60 | > Next == \/ \E msg \in Message : SSend(msg) 61 | > \/ BufRcv 62 | > \/ BufSend 63 | > \/ RRcv 64 | > 65 | > Spec == Init /\ [][Next]_<> 66 | > ----------------------------------------------------------------------------- 67 | > THEOREM Spec => []TypeInvariant 68 | > ============================================================================= 69 | 70 | … and the [FIFO example](https://github.com/tlaplus/Examples/blob/master/specifications/SpecifyingSystems/FIFO/FIFO.tla) 71 | on page 41: 72 | 73 | > ------------------------ MODULE FIFO ------------------------- 74 | > CONSTANT Message 75 | > VARIABLES in, out 76 | > Inner(q) == INSTANCE InnerFIFO 77 | > Spec == \EE q : Inner(q)!Spec 78 | > ============================================================== 79 | 80 | For the @Channel@ we reuse the "HasCal.Test.AsyncInterface" module, but 81 | the other two modules we inline here. 82 | -} 83 | 84 | {-# LANGUAGE ApplicativeDo #-} 85 | {-# LANGUAGE BlockArguments #-} 86 | {-# LANGUAGE DeriveAnyClass #-} 87 | {-# LANGUAGE DeriveGeneric #-} 88 | {-# LANGUAGE NamedFieldPuns #-} 89 | {-# LANGUAGE RecordWildCards #-} 90 | {-# LANGUAGE TemplateHaskell #-} 91 | 92 | module HasCal.Test.FIFO where 93 | 94 | import Data.Sequence (Seq, ViewL(..), (|>)) 95 | import HasCal 96 | import HasCal.Test.AsyncInterface (Chan, Data, val) 97 | import Prelude hiding ((.)) 98 | import Test.Tasty (TestTree) 99 | 100 | import qualified Control.Monad as Monad 101 | import qualified Data.Sequence as Seq 102 | import qualified HasCal.Test.AsyncInterface as Channel 103 | import qualified Test.Tasty.HUnit as HUnit 104 | 105 | data Global = Global 106 | { _inChannel :: Chan 107 | , _outChannel :: Chan 108 | } deriving (Eq, Generic, Hashable, Show, ToJSON) 109 | 110 | data Local = Local { _q :: Seq Data } 111 | deriving (Eq, Generic, Hashable, Show, ToJSON) 112 | 113 | data Label = Init | SSend | BufRcv | BufSend | RRcv 114 | deriving (Eq, Generic, Hashable, Show, ToJSON) 115 | 116 | makeLenses ''Global 117 | makeLenses ''Local 118 | 119 | -- There's technically no need to rename `channelModel` to `inChan` / `outChan`. 120 | -- since they're pure and instantiated identically. We could have just used 121 | -- `channelModel` directly in their place. These synonyms are just to show the 122 | -- correspondence to the TLA+ model. 123 | 124 | inChan :: Model Chan Channel.Label 125 | inChan = Channel.channelModel 126 | 127 | outChan :: Model Chan Channel.Label 128 | outChan = Channel.channelModel 129 | 130 | test_asyncInterface :: TestTree 131 | test_asyncInterface = HUnit.testCase "FIFO" do 132 | model defaultModel 133 | { termination = termination inChan /\ termination outChan 134 | 135 | , startingGlobals = do 136 | _inChannel <- startingGlobals inChan 137 | _outChannel <- startingGlobals outChan 138 | return Global{..} 139 | 140 | , coroutine = Coroutine 141 | { startingLabel = Init 142 | 143 | , startingLocals = do 144 | let _q = mempty 145 | return Local{..} 146 | 147 | , process = do 148 | let ssend = do 149 | _q <- use (local.q) 150 | await (Seq.length _q <= 3) 151 | msg <- with universe 152 | zoomProcess inChannel (Channel.send msg) 153 | 154 | let bufRcv = do 155 | zoomProcess inChannel Channel.rcv 156 | _val <- use (global.inChannel.val) 157 | local.q %= (|> _val) 158 | 159 | let bufSend = do 160 | _q <- use (local.q) 161 | 162 | case Seq.viewl _q of 163 | h :< t -> do 164 | zoomProcess outChannel (Channel.send h) 165 | local.q .= t 166 | EmptyL -> 167 | empty 168 | 169 | let rRcv = do 170 | zoomProcess outChannel Channel.rcv 171 | 172 | Monad.forever 173 | ( (do ssend ; yield SSend ) 174 | <|> (do bufRcv ; yield BufRcv ) 175 | <|> (do bufSend; yield BufSend) 176 | <|> (do rRcv ; yield RRcv ) 177 | ) 178 | } 179 | 180 | , property = true 181 | } 182 | -------------------------------------------------------------------------------- /tasty/HasCal/Test/FastMutex.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BlockArguments #-} 2 | {-# LANGUAGE DeriveAnyClass #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | {-# LANGUAGE TemplateHaskell #-} 6 | 7 | -- | This example is taken from section 2.6 of "A PlusCal User's Manual" 8 | module HasCal.Test.FastMutex where 9 | 10 | import Control.Monad (when) 11 | import Data.Traversable (for) 12 | import HasCal 13 | import Numeric.Natural (Natural) 14 | import Prelude hiding ((.)) 15 | import Test.Tasty (TestTree) 16 | 17 | import qualified Control.Monad as Monad 18 | import qualified Test.Tasty.HUnit as HUnit 19 | 20 | data Global = Global{ _x :: Int, _y :: Int, _b :: HashMap Int Bool } 21 | deriving (Eq, Generic, Hashable, Show, ToJSON) 22 | 23 | data Label 24 | = Default 25 | | NonCriticalSection 26 | | Start 27 | | CriticalSection 28 | | Line Natural 29 | deriving (Eq, Generic, Hashable, Show, ToJSON) 30 | 31 | makeLenses ''Global 32 | 33 | fastMutex :: Int -> IO () 34 | fastMutex n = model defaultModel 35 | { termination = False 36 | 37 | , startingGlobals = do 38 | let _x = 0 39 | let _y = 0 40 | let _b = [ 1.. n ] |-> \_i -> False 41 | return Global{..} 42 | 43 | , coroutine = for [ 1 .. n ] proc 44 | 45 | , property = 46 | let predicate labels = 47 | length (filter (== CriticalSection) labels) <= 1 48 | in always . viewing (label . to predicate) 49 | } 50 | where 51 | proc :: Int -> Coroutine Global Label 52 | proc self = Coroutine 53 | { startingLabel = Default 54 | 55 | , startingLocals = pure () 56 | 57 | , process = ncs 58 | } 59 | where 60 | ncs = do 61 | yield NonCriticalSection 62 | start 63 | 64 | start :: Process Global () Label a 65 | start = do 66 | yield Start 67 | global.b.ix(self) .= True 68 | 69 | yield (Line 1) 70 | global.x .= self 71 | 72 | yield (Line 2) 73 | y0 <- use (global.y) 74 | when (y0 /= 0) do 75 | yield (Line 3) 76 | global.b.ix(self) .= False 77 | 78 | yield (Line 4) 79 | y1 <- use (global.y) 80 | await (y1 == 0) 81 | start 82 | 83 | yield (Line 5) 84 | global.y .= self 85 | 86 | yield (Line 6) 87 | x0 <- use (global.x) 88 | when (x0 /= self) do 89 | yield (Line 7) 90 | global.b.ix(self) .= False 91 | 92 | yield (Line 8) 93 | Monad.forM_ [ 1 .. n ] \j -> do 94 | Just bool <- preuse (global.b.ix(j)) 95 | await (not bool) 96 | 97 | yield (Line 9) 98 | y1 <- use (global.y) 99 | when (y1 /= self) do 100 | yield (Line 10) 101 | y2 <- use (global.y) 102 | await (y2 == 0) 103 | start 104 | 105 | yield CriticalSection 106 | 107 | yield (Line 11) 108 | global.y .= 0 109 | 110 | yield (Line 12) 111 | global.b.ix(self) .= False 112 | 113 | ncs 114 | 115 | test_fastMutex :: TestTree 116 | test_fastMutex = HUnit.testCase "Fast mutex algorithm" do 117 | fastMutex 3 118 | -------------------------------------------------------------------------------- /tasty/HasCal/Test/Flags.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BlockArguments #-} 2 | {-# LANGUAGE DeriveAnyClass #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | {-# LANGUAGE TemplateHaskell #-} 6 | {-# LANGUAGE TypeApplications #-} 7 | 8 | {-| This example is from the "Behaviors" section of the "Learn TLA+" guide 9 | 10 | > ---- MODULE flags ---- 11 | > EXTENDS TLC, Integers 12 | > (* --algorithm flags 13 | > variables f1 \in BOOLEAN, f2 \in BOOLEAN, f3 \in BOOLEAN 14 | > begin 15 | > while TRUE do 16 | > with f \in {1, 2, 3} do 17 | > if f = 1 then 18 | > either 19 | > f1 := TRUE; 20 | > or 21 | > f1 := FALSE; 22 | > end either; 23 | > elsif f = 2 then 24 | > either 25 | > f2 := TRUE; 26 | > or 27 | > f2 := FALSE; 28 | > end either; 29 | > else 30 | > either 31 | > f3 := TRUE; 32 | > or 33 | > f3 := FALSE; 34 | > end either; 35 | > end if; 36 | > end with; 37 | > end while; 38 | > end algorithm; *) 39 | > 40 | > ==== 41 | 42 | -} 43 | module HasCal.Test.Flags where 44 | 45 | import Control.Monad (forever) 46 | import HasCal 47 | import Prelude hiding ((.)) 48 | import Test.Tasty (TestTree) 49 | 50 | import qualified Test.Tasty.HUnit as HUnit 51 | 52 | data Global = Global{ _f1 :: Bool, _f2 :: Bool, _f3 :: Bool } 53 | deriving (Eq, Generic, Hashable, Show, ToJSON) 54 | 55 | data Label = A | B deriving (Eq, Generic, Hashable, Show, ToJSON) 56 | 57 | makeLenses ''Global 58 | 59 | test_flags :: TestTree 60 | test_flags = HUnit.testCase "Flags" do 61 | model defaultModel 62 | { termination = False 63 | 64 | , startingGlobals = do 65 | _f1 <- universe @Bool 66 | _f2 <- universe @Bool 67 | _f3 <- universe @Bool 68 | return Global{..} 69 | 70 | , coroutine = Coroutine 71 | { startingLabel = A 72 | 73 | , startingLocals = pure () 74 | 75 | , process = forever do 76 | yield B 77 | f <- with [ f1, f2, f3 ] 78 | bool <- with universe 79 | global.f .= bool 80 | } 81 | 82 | , property = true 83 | } 84 | -------------------------------------------------------------------------------- /tasty/HasCal/Test/Hanoi.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BlockArguments #-} 2 | {-# LANGUAGE DeriveAnyClass #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | 5 | {-| This example is from the "Tuples And Structures" section of the "Learn TLA+" 6 | guide 7 | 8 | > ---- MODULE hanoi ---- 9 | > EXTENDS TLC, Sequences, Integers 10 | > 11 | > (* --algorithm hanoi 12 | > variables tower = <<<<1, 2, 3>>, <<>>, <<>>>>, 13 | > 14 | > define 15 | > D == DOMAIN tower 16 | > end define; 17 | > 18 | > begin 19 | > while TRUE do 20 | > assert tower[3] /= <<1, 2, 3>>; 21 | > with from \in {x \in D : tower[x] /= <<>>}, 22 | > to \in { 23 | > y \in D : 24 | > \/ tower[y] = <<>> 25 | > \/ Head(tower[from]) < Head(tower[y]) 26 | > } 27 | > do 28 | > tower[from] := Tail(tower[from]) || 29 | > tower[to] := <> \o tower[to]; 30 | > end with; 31 | > end while; 32 | > end algorithm; *) 33 | > ==== 34 | 35 | -} 36 | 37 | module HasCal.Test.Hanoi where 38 | 39 | import Control.Monad (forever) 40 | import HasCal hiding (to) 41 | import Prelude hiding ((.)) 42 | import Test.Tasty (TestTree) 43 | 44 | import qualified Test.Tasty.HUnit as HUnit 45 | import qualified Test.Tasty.ExpectedFailure as Failure 46 | 47 | data Label = A | B deriving (Eq, Generic, Hashable, Show, ToJSON) 48 | 49 | test_hanoi :: TestTree 50 | test_hanoi = 51 | Failure.expectFailBecause "The original example intentionally fails" do 52 | HUnit.testCase "Hanoi" do 53 | model defaultModel 54 | { termination = False 55 | 56 | , startingGlobals = do 57 | let board :: [[Int]] 58 | board = [ [ 1, 2, 3 ], [ ], [ ] ] 59 | 60 | pure board 61 | 62 | , coroutine = Coroutine 63 | { startingLabel = A 64 | 65 | , startingLocals = pure () 66 | 67 | , process = forever do 68 | yield B 69 | 70 | _position2 <- use (global.ix 2) 71 | assert (_position2 /= [ 1, 2, 3 ]) 72 | 73 | from <- with [ 0 .. 2 ] 74 | to <- with [ 0 .. 2 ] 75 | 76 | src <- use (global.ix from) 77 | dst <- use (global.ix to ) 78 | 79 | case (src, dst) of 80 | (s : rc, []) -> do 81 | global.ix from .= rc 82 | global.ix to .= [ s ] 83 | (s : rc, d : st) | s < d -> do 84 | global.ix from .= rc 85 | global.ix to .= s : d : st 86 | _ -> do 87 | empty 88 | } 89 | 90 | , property = true 91 | } 92 | -------------------------------------------------------------------------------- /tasty/HasCal/Test/HourClock.hs: -------------------------------------------------------------------------------- 1 | {-| This is based on the [HourClock example](https://github.com/tlaplus/Examples/blob/master/specifications/SpecifyingSystems/HourClock/HourClock.tla) 2 | from figure 2.1 on page 20 in Lamport's \"Specifying Systems\" book: 3 | 4 | > ---------------------- MODULE HourClock ---------------------- 5 | > EXTENDS Naturals 6 | > VARIABLE hr 7 | > HCini == hr \in (1 .. 12) 8 | > HCnxt == hr' = IF hr # 12 THEN hr + 1 ELSE 1 9 | > HC == HCini /\ [][HCnxt]_hr 10 | > -------------------------------------------------------------- 11 | > THEOREM HC => []HCini 12 | > ============================================================== 13 | -} 14 | 15 | {-# LANGUAGE BlockArguments #-} 16 | {-# LANGUAGE DeriveAnyClass #-} 17 | {-# LANGUAGE DeriveGeneric #-} 18 | {-# LANGUAGE RecordWildCards #-} 19 | {-# LANGUAGE TemplateHaskell #-} 20 | 21 | module HasCal.Test.HourClock where 22 | 23 | import HasCal 24 | import Prelude hiding ((.)) 25 | import Test.Tasty (TestTree) 26 | 27 | import qualified Test.Tasty.HUnit as HUnit 28 | import qualified Control.Monad as Monad 29 | 30 | data Global = Global { _hr :: Int } 31 | deriving (Eq, Generic, Hashable, Show, ToJSON) 32 | 33 | data Label = Ini | Nxt 34 | deriving (Eq, Generic, Hashable, Show, ToJSON) 35 | 36 | makeLenses ''Global 37 | 38 | tick :: Int -> Int 39 | tick hour = hour `mod` 12 + 1 40 | 41 | test_hourClock :: TestTree 42 | test_hourClock = HUnit.testCase "Hour clock" do 43 | model defaultModel 44 | { termination = False 45 | 46 | , startingGlobals = do 47 | _hr <- [1 .. 12] 48 | return Global{..} 49 | 50 | , coroutine = Coroutine 51 | { startingLabel = Ini 52 | , startingLocals = pure () 53 | , process = Monad.forever do 54 | yield Nxt 55 | global.hr %= tick 56 | } 57 | 58 | , property = always . viewing (state . hr . to (`elem` [ 1 .. 12 ])) 59 | } 60 | -------------------------------------------------------------------------------- /tasty/HasCal/Test/InternalMemory.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BlockArguments #-} 2 | {-# LANGUAGE DeriveAnyClass #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | {-# LANGUAGE TemplateHaskell #-} 6 | 7 | {-| This is based on the [InternalMemory module](https://github.com/tlaplus/Examples/blob/master/specifications/SpecifyingSystems/CachingMemory/InternalMemory.tla) 8 | from figure 5.2 on page 52 in Lamport's \"Specifying Systems\" book: 9 | 10 | > ------------------ MODULE InternalMemory --------------------- 11 | > EXTENDS MemoryInterface 12 | > VARIABLES mem, ctl, buf 13 | > -------------------------------------------------------------- 14 | > IInit == /\ mem \in [Adr->Val] 15 | > /\ ctl = [p \in Proc |-> "rdy"] 16 | > /\ buf = [p \in Proc |-> NoVal] 17 | > /\ memInt \in InitMemInt 18 | > 19 | > TypeInvariant == 20 | > /\ mem \in [Adr->Val] 21 | > /\ ctl \in [Proc -> {"rdy", "busy","done"}] 22 | > /\ buf \in [Proc -> MReq \cup Val \cup {NoVal}] 23 | > 24 | > Req(p) == /\ ctl[p] = "rdy" 25 | > /\ \E req \in MReq : 26 | > /\ Send(p, req, memInt, memInt') 27 | > /\ buf' = [buf EXCEPT ![p] = req] 28 | > /\ ctl' = [ctl EXCEPT ![p] = "busy"] 29 | > /\ UNCHANGED mem 30 | > 31 | > Do(p) == 32 | > /\ ctl[p] = "busy" 33 | > /\ mem' = IF buf[p].op = "Wr" 34 | > THEN [mem EXCEPT ![buf[p].adr] = buf[p].val] 35 | > ELSE mem 36 | > /\ buf' = [buf EXCEPT ![p] = IF buf[p].op = "Wr" 37 | > THEN NoVal 38 | > ELSE mem[buf[p].adr]] 39 | > /\ ctl' = [ctl EXCEPT ![p] = "done"] 40 | > /\ UNCHANGED memInt 41 | > 42 | > Rsp(p) == /\ ctl[p] = "done" 43 | > /\ Reply(p, buf[p], memInt, memInt') 44 | > /\ ctl' = [ctl EXCEPT ![p]= "rdy"] 45 | > /\ UNCHANGED <> 46 | > 47 | > INext == \E p \in Proc: Req(p) \/ Do(p) \/ Rsp(p) 48 | > 49 | > ISpec == IInit /\ [][INext]_<> 50 | > -------------------------------------------------------------- 51 | > THEOREM ISpec => []TypeInvariant 52 | > ============================================================== 53 | 54 | … and the [MemoryInterface module](https://raw.githubusercontent.com/tlaplus/Examples/master/specifications/SpecifyingSystems/CachingMemory/MemoryInterface.tla) 55 | from figure 5.1 on page 48 in Lamport's \"Specifying Systems\" book: 56 | 57 | > -------------------------- MODULE MemoryInterface --------------------------- 58 | > VARIABLE memInt 59 | > CONSTANTS Send(_, _, _, _), 60 | > Reply(_, _, _, _), 61 | > InitMemInt, 62 | > Proc, 63 | > Adr, 64 | > Val 65 | > 66 | > (***************************************************************************) 67 | > (* We comment out the assumption because TLC cannot handle unbounded *) 68 | > (* quantifiers. *) 69 | > (***************************************************************************) 70 | > \* ASSUME \A p, d, miOld, miNew : 71 | > \* /\ Send(p,d,miOld,miNew) \in BOOLEAN 72 | > \* /\ Reply(p,d,miOld,miNew) \in BOOLEAN 73 | > 74 | > ----------------------------------------------------------------------------- 75 | > MReq == [op : {"Rd"}, adr: Adr] 76 | > \cup [op : {"Wr"}, adr: Adr, val : Val] 77 | > 78 | > NoVal == CHOOSE v : v \notin Val 79 | > ============================================================================= 80 | -} 81 | module HasCal.Test.InternalMemory where 82 | 83 | import HasCal 84 | import Prelude hiding ((.)) 85 | import Test.Tasty (TestTree) 86 | 87 | import qualified Control.Monad as Monad 88 | import qualified Test.Tasty.HUnit as HUnit 89 | 90 | data Adr = A1 | A2 | A3 91 | deriving (Eq, Generic, Hashable, Show, ToJSON, ToJSONKey, Universe) 92 | 93 | data Proc = P1 | P2 94 | deriving (Eq, Generic, Hashable, Show, ToJSONKey, ToJSON, Universe) 95 | 96 | data Val = V1 | V2 97 | deriving (Eq, Generic, Hashable, Show, ToJSON, Universe) 98 | 99 | data Local = Local 100 | { _mem :: HashMap Adr Val 101 | , _ctl :: HashMap Proc Ctl 102 | , _buf :: HashMap Proc Buf 103 | , _memInt :: (Proc, Buf) 104 | } deriving (Eq, Generic, Hashable, Show, ToJSON) 105 | 106 | data Ctl = Rdy | Busy | Done 107 | deriving (Eq, Generic, Hashable, Show, ToJSON) 108 | 109 | data Buf 110 | = Rd { _adr :: Adr } 111 | | Wr { _adr :: Adr, _val :: Val } 112 | | Val Val 113 | | NoVal 114 | deriving (Eq, Generic, Hashable, Show, ToJSON, Universe) 115 | 116 | data Label = Init | Req | Do | Rsp 117 | deriving (Eq, Generic, Hashable, Show, ToJSON) 118 | 119 | makeLenses ''Local 120 | 121 | -- https://github.com/tlaplus/Examples/blob/master/specifications/SpecifyingSystems/Liveness/MCInternalMemory.tla#L36-L37 122 | send :: Proc -> Buf -> Process () Local Label () 123 | send p d = local.memInt .= (p, d) 124 | 125 | reply :: Proc -> Buf -> Process () Local Label () 126 | reply p d = local.memInt .= (p, d) 127 | 128 | test_internalMemory :: TestTree 129 | test_internalMemory = HUnit.testCase "Internal memory" do 130 | model defaultModel 131 | { termination = False 132 | 133 | , startingGlobals = pure () 134 | 135 | , coroutine = Coroutine 136 | { startingLabel = Init 137 | 138 | , startingLocals = do 139 | _mem <- universe --> universe 140 | let _ctl = universe |-> \_p -> Rdy 141 | let _buf = universe |-> \_p -> NoVal 142 | let _memInt = (P1, NoVal) 143 | return Local{..} 144 | 145 | , process = Monad.forever do 146 | p <- with universe 147 | 148 | Just _ctl <- preuse (local.ctl.ix p) 149 | 150 | case _ctl of 151 | Rdy -> do 152 | yield Req 153 | 154 | req <- with universe 155 | 156 | send p req 157 | 158 | local.buf.ix p .= req 159 | 160 | local.ctl.ix p .= Busy 161 | 162 | Busy -> do 163 | yield Do 164 | 165 | Just _buf <- preuse (local.buf.ix p) 166 | 167 | case _buf of 168 | Wr{..} -> do 169 | local.mem.ix _adr .= _val 170 | 171 | local.buf.ix p .= NoVal 172 | 173 | Rd{..} -> do 174 | Just _val <- preuse (local.mem.ix _adr) 175 | 176 | local.buf.ix p .= Val _val 177 | 178 | _ -> do 179 | skip 180 | 181 | local.ctl.ix p .= Done 182 | 183 | Done -> do 184 | yield Rsp 185 | 186 | Just _buf <- preuse (local.buf.ix p) 187 | 188 | reply p _buf 189 | 190 | local.ctl.ix p .= Rdy 191 | } 192 | 193 | , property = true 194 | } 195 | -------------------------------------------------------------------------------- /tasty/HasCal/Test/Market.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BangPatterns #-} 2 | {-# LANGUAGE BlockArguments #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | {-# LANGUAGE DeriveAnyClass #-} 5 | {-# LANGUAGE NamedFieldPuns #-} 6 | {-# LANGUAGE RecordWildCards #-} 7 | {-# LANGUAGE TemplateHaskell #-} 8 | {-# LANGUAGE TypeApplications #-} 9 | 10 | {-| This example is from the "Example: Arbitrage" section of the "Learn TLA+" 11 | guide 12 | 13 | > ---- MODULE market ---- 14 | > EXTENDS Integers 15 | > CONSTANTS Item, MaxPrice, Vendor, MaxActions 16 | > 17 | > I == Item 18 | > V == Vendor 19 | > P == 1..MaxPrice 20 | > 21 | > ValidMarkets == [V \X I -> [buy : P, sell : P]] 22 | > 23 | > (* --algorithm market 24 | > variables market \in ValidMarkets, 25 | > backpack = {}, \* items we have 26 | > actions = 0, 27 | > profit = 0; 28 | > 29 | > begin 30 | > Act: 31 | > while actions < MaxActions do 32 | > either 33 | > Buy: 34 | > with v \in V, i \in Item \ backpack do 35 | > profit := profit - market[<>].sell; 36 | > backpack := backpack \union {i}; 37 | > end with; 38 | > or 39 | > Sell: 40 | > with v \in V, i \in backpack do 41 | > profit := profit + market[<>].buy; 42 | > backpack := backpack \ {i}; 43 | > end with; 44 | > end either; 45 | > Loop: 46 | > actions := actions + 1; 47 | > end while; 48 | > end algorithm; *) 49 | > 50 | > \* Translation 51 | > 52 | > NoArbitrage == profit <= 0 53 | > ==== 54 | 55 | -} 56 | module HasCal.Test.Market where 57 | 58 | import Data.Set (Set) 59 | import Prelude hiding (either, (.)) 60 | import HasCal 61 | import Test.Tasty (TestTree) 62 | 63 | import qualified Control.Monad as Monad 64 | import qualified Data.HashMap.Strict as HashMap 65 | import qualified Data.Set as Set 66 | import qualified Test.Tasty.HUnit as HUnit 67 | import qualified Test.Tasty.ExpectedFailure as Failure 68 | 69 | data Item = Ore | Sheep | Brick 70 | deriving (Eq, Hashable, Generic, Ord, Show, ToJSON, ToJSONKey, Universe) 71 | 72 | data Vendor = Alice 73 | deriving (Eq, Generic, Hashable, Show, ToJSON, Universe) 74 | 75 | data Offer = Offer { _buy :: !Int, _sell :: !Int } 76 | deriving (Eq, Generic, Hashable, Show, ToJSON) 77 | 78 | data Global = Global 79 | { _market :: !(HashMap (Vendor, Item) Offer) 80 | , _trades :: !(HashMap [Item] Item) 81 | , _backpack :: !(Set Item) 82 | , _profit :: !Int 83 | , _actions :: !Int 84 | } deriving (Eq, Generic, Hashable, Show, ToJSON) 85 | 86 | data Label = Act | Buy | Sell | Trade | Loop 87 | deriving (Eq, Generic, Hashable, Show, ToJSON) 88 | 89 | makeLenses ''Offer 90 | makeLenses ''Global 91 | 92 | arbitrage :: Int -> Int -> IO () 93 | arbitrage maxPrice maxActions = do 94 | let _I = universe @Item 95 | let _V = universe @Vendor 96 | let _P = [ 1 .. maxPrice ] 97 | 98 | model defaultModel 99 | { startingGlobals = do 100 | let _domain = do 101 | v <- _V 102 | i <- _I 103 | return (v, i) 104 | 105 | let _range = do 106 | _buy <- _P 107 | _sell <- _P 108 | Monad.guard (_buy <= _sell) 109 | return Offer{..} 110 | 111 | _market <- _domain --> _range 112 | 113 | let validMarket = and do 114 | ((_, item0), Offer{ _buy }) <- HashMap.toList _market 115 | ((_, item1), Offer{ _sell }) <- HashMap.toList _market 116 | return (item0 == item1 ==> _buy <= _sell) 117 | 118 | Monad.guard validMarket 119 | 120 | _trades <- filter (\items -> 1 < length items) (subset _I) --> _I 121 | 122 | let _backpack = Set.empty 123 | 124 | let _profit = 0 125 | 126 | let _actions = 0 127 | 128 | return Global{..} 129 | 130 | , coroutine = Coroutine 131 | { startingLabel = Act 132 | 133 | , startingLocals = pure () 134 | 135 | , process = do 136 | while (do a <- use (global.actions); pure (a < maxActions)) do 137 | either 138 | [ do 139 | yield Buy 140 | _backpack <- use (global.backpack) 141 | v <- with _V 142 | i <- with (Set.toList (Set.difference (Set.fromList _I) _backpack)) 143 | Just loss <- preuse (global.market.ix (v, i).sell) 144 | global.profit -= loss 145 | global.backpack %= Set.insert i 146 | , do 147 | yield Sell 148 | _backpack <- use (global.backpack) 149 | v <- with _V 150 | i <- with (Set.toList _backpack) 151 | Just gain <- preuse (global.market.ix (v, i).buy) 152 | global.profit += gain 153 | global.backpack %= Set.delete i 154 | , do 155 | yield Trade 156 | _backpack <- use (global.backpack) 157 | _trades <- use (global.trades) 158 | itemsLost <- with (Set.toList (Set.intersection (Set.fromList (subset (Set.toList _backpack))) (Set.fromList (domain _trades)))) 159 | Just itemGained <- preuse (global.trades.ix itemsLost) 160 | global.backpack %= Set.insert itemGained . (`Set.difference` (Set.fromList itemsLost)) 161 | ] 162 | 163 | yield Loop 164 | global.actions += 1 165 | } 166 | 167 | , property = always . viewing (state . profit . to (<= 0)) 168 | } 169 | 170 | test_market :: TestTree 171 | test_market = 172 | Failure.expectFailBecause "The original example has a deliberate failure" do 173 | HUnit.testCase "Market" do 174 | arbitrage 6 5 175 | -------------------------------------------------------------------------------- /tasty/HasCal/Test/Trade.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BlockArguments #-} 2 | {-# LANGUAGE DeriveAnyClass #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE RecordWildCards #-} 6 | {-# LANGUAGE TemplateHaskell #-} 7 | {-# LANGUAGE TypeApplications #-} 8 | 9 | {-| This example is from the "Introduction" section of the "Learn TLA+" guide 10 | 11 | > People == {"alice", "bob"} 12 | > Items == {"ore", "sheep", "brick"} 13 | > (* --algorithm trade 14 | > variable owner_of \in [Items -> People] 15 | > 16 | > process giveitem \in 1..3 \* up to three possible trades made 17 | > variables item \in Items, 18 | > owner = owner_of[item], 19 | > to \in People, 20 | > origin_of_trade \in People 21 | > begin Give: 22 | > if origin_of_trade = owner then 23 | > owner_of[item] := to; 24 | > end if; 25 | > end process; 26 | > end algorithm; *) 27 | 28 | -} 29 | 30 | module HasCal.Test.Trade where 31 | 32 | import Control.Monad (when) 33 | import HasCal hiding (to) 34 | import Prelude hiding ((.)) 35 | import Test.Tasty (TestTree) 36 | 37 | import qualified Test.Tasty.HUnit as HUnit 38 | 39 | data People = Alice | Bob 40 | deriving (Eq, Generic, Hashable, Show, ToJSON, Universe) 41 | 42 | data Items = Ore | Sheep | Brick 43 | deriving (Eq, Generic, Hashable, Show, ToJSON, ToJSONKey, Universe) 44 | 45 | data Global = Global { _owner_of :: HashMap Items People } 46 | deriving (Eq, Generic, Hashable, Show, ToJSON) 47 | 48 | makeLenses ''Global 49 | 50 | test_trade :: TestTree 51 | test_trade = HUnit.testCase "Trade" do 52 | model defaultModel 53 | { startingGlobals = do 54 | _owner_of <- universe @Items --> universe @People 55 | return Global{..} 56 | 57 | , coroutine = traverse_ giveitem [ 1 .. 3 ] 58 | 59 | , property = true 60 | } 61 | where 62 | giveitem :: Int -> Coroutine Global () 63 | giveitem _ = Coroutine 64 | { startingLabel = () 65 | 66 | , startingLocals = pure () 67 | 68 | , process = do 69 | item <- with (universe @Items) 70 | to <- with (universe @People) 71 | origin_of_trade <- with (universe @People) 72 | 73 | Just owner <- preuse (global.owner_of.ix item) 74 | 75 | when (origin_of_trade == owner) do 76 | global.owner_of.ix item .= to 77 | } 78 | -------------------------------------------------------------------------------- /tasty/HasCal/Test/Transfer.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BlockArguments #-} 2 | {-# LANGUAGE DeriveAnyClass #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | {-# LANGUAGE TemplateHaskell #-} 6 | 7 | {-| This example is from the "An Example" section of the "Learn TLA+" guide 8 | 9 | > ---- MODULE Transfer ---- 10 | > EXTENDS Naturals, TLC 11 | > 12 | > (* --algorithm transfer 13 | > variables alice_account = 10, bob_account = 10, 14 | > account_total = alice_account + bob_account; 15 | > 16 | > process Transfer \in 1..2 17 | > variable money \in 1..20; 18 | > begin 19 | > Transfer: 20 | > if alice_account >= money then 21 | > A: alice_account := alice_account - money; 22 | > bob_account := bob_account + money; 23 | > end if; 24 | > C: assert alice_account >= 0; 25 | > end process 26 | > 27 | > end algorithm *) 28 | > 29 | > MoneyInvariant == alice_account + bob_account = account_total 30 | > 31 | > ==== 32 | 33 | -} 34 | module HasCal.Test.Transfer where 35 | 36 | import Control.Monad (when) 37 | import HasCal 38 | import Prelude hiding ((.)) 39 | import Test.Tasty (TestTree) 40 | 41 | import qualified Test.Tasty.HUnit as HUnit 42 | import qualified Test.Tasty.ExpectedFailure as Failure 43 | 44 | data Global = Global 45 | { _alice_account :: Int 46 | , _bob_account :: Int 47 | , _account_total :: Int 48 | } deriving (Eq, Generic, Hashable, Show, ToJSON) 49 | 50 | data Local = Local { _money :: Int } 51 | deriving (Eq, Generic, Hashable, Show, ToJSON) 52 | 53 | data Label = Transfer | A | C deriving (Eq, Generic, Hashable, Show, ToJSON) 54 | 55 | makeLenses ''Global 56 | makeLenses ''Local 57 | 58 | test_transfer :: TestTree 59 | test_transfer = 60 | Failure.expectFailBecause "The example has a deliberate TOCTOU bug" do 61 | HUnit.testCase "Transfer" do 62 | model defaultModel 63 | { startingGlobals = do 64 | let _alice_account = 10 65 | let _bob_account = 10 66 | let _account_total = _alice_account + _bob_account 67 | return Global{..} 68 | 69 | , coroutine = traverse transfer [ 1 .. 2 ] 70 | 71 | , property = 72 | let predicate Global{..} = 73 | _alice_account + _bob_account == _account_total 74 | in always . viewing (state . to predicate) 75 | } 76 | where 77 | transfer :: Int -> Coroutine Global Label 78 | transfer _ = Coroutine 79 | { startingLabel = Transfer 80 | 81 | , startingLocals = do 82 | _money <- [ 1 .. 20 ] 83 | return Local{..} 84 | 85 | , process = do 86 | _money <- use (local.money) 87 | 88 | alice_old <- use (global.alice_account) 89 | 90 | when (alice_old >= _money) do 91 | yield A 92 | global.alice_account -= _money 93 | global.bob_account += _money 94 | 95 | yield C 96 | alice_new <- use (global.alice_account) 97 | assert (alice_new >= 0) 98 | } 99 | -------------------------------------------------------------------------------- /tasty/Main.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -F -pgmF tasty-discover -optF --hide-successes #-} 2 | --------------------------------------------------------------------------------