haal-0.7.0.0: src/Haal/BlackBox.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
#ifdef LIQUID
{-# OPTIONS_GHC -fplugin=LiquidHaskell
-fplugin-opt=LiquidHaskell:--prune-unsorted
-fplugin-opt=LiquidHaskell:--no-termination #-}
{- HLINT ignore "Use error" -}
#endif
{- | This module defines the BlackBox type class as well as the Automaton and SUL
sub classes.
-}
module Haal.BlackBox (
Automaton (..),
SUL (..),
Finite,
FiniteEq,
FiniteOrd,
inputs,
outputs,
walk,
queryChecked,
stepPure,
walkPure,
resetPure,
initial,
distinguish,
accessSequences,
localCharacterizingSet,
globalCharacterizingSet,
reachable,
difference,
)
where
import Control.Exception (ErrorCall (..), throw)
import Control.Monad.Identity (Identity, runIdentity)
import qualified Data.Bifunctor as Bif
import qualified Data.List as List
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import qualified Data.Set as Set
{- | The 'SUL' type class defines the basic interface for a black box automaton.
It provides methods to step through the automaton and reset it to its initial state.
It also requires a monad m, that may be 'Identity' in case of a pure SUL, or 'IO'
in case of an external program that performs IO.
Active automata learning requires queries to be independent, so the library
calls 'reset' before every membership query and every equivalence test case
(see 'query'). 'reset' must therefore bring the system back to its initial
state, and the library always continues with the value that 'reset' and 'step'
return. This supports two kinds of SULs:
* persistent ones, such as pure automata, where 'step' returns a new value and
leaves the old one unchanged;
* stateful ones, such as a driver for a running process or a socket, where
'step' and 'reset' change the external system and may return the same handle.
-}
class (Monad m) => SUL sul m where
step :: sul i o -> i -> m (sul i o, o)
reset :: sul i o -> m (sul i o)
{-# MINIMAL step, reset #-}
-- | Run a single query: reset the SUL, then feed it the inputs and collect the
-- outputs. Every query the library sends to a SUL goes through this method, so
-- that queries are independent of each other.
--
-- Override it when a SUL can answer a whole query more efficiently than step by
-- step, e.g. by sending the whole word in one message. An override must:
--
-- * start from the initial state, like 'reset' does;
-- * return exactly one output per input;
-- * agree with 'reset' followed by 'walk'.
--
-- The library checks the number of outputs at runtime (see 'queryChecked').
query :: sul i o -> [i] -> m [o]
query sul is = do
sul' <- reset sul
(_, os) <- walk sul' is
pure os
{- | 'query', checking that the SUL returned exactly one output per input.
A 'SUL' instance may override 'query', and the learners rely on this property,
so they query through this function. A violation is a bug in the instance and
fails with an error naming the expected and actual number of outputs.
-}
{-@ queryChecked :: (SUL sul m) => sul i o -> xs:[i] -> m {ys:[o] | len ys == len xs} @-}
queryChecked :: (SUL sul m) => sul i o -> [i] -> m [o]
queryChecked sul xs = do
os <- query sul xs
if length os == length xs
then pure os
else contractViolation (length xs) (length os)
{- | Fail because a 'query' override broke its contract. Unlike an @impossible@
error, this one is reachable. LiquidHaskell gives 'error' a @false@ precondition,
so this throws an t'ErrorCall' directly instead, which behaves the same at runtime.
-}
contractViolation :: Int -> Int -> a
contractViolation expected actual =
throw . ErrorCall $
"Haal.BlackBox.queryChecked: the SUL returned "
++ show actual
++ " outputs for a query of "
++ show expected
++ " inputs; an overridden 'query' must return one output per input"
-- | Finite is an alias for (Enum, Bounded).
type Finite i = (Enum i, Bounded i)
-- | FiniteEq is an alias for (Eq, Finite).
type FiniteEq i = (Eq i, Finite i)
-- | FiniteOrd is an alias for (Ord, Finite).
type FiniteOrd i = (Ord i, Finite i)
-- | Generalization of 'step' that operates on a list of inputs.
{-@ walk :: (SUL sul m) => sul i o -> xs:[i] -> m (sul i o, {ys:[o] | len ys == len xs}) @-}
walk :: (SUL sul m) => sul i o -> [i] -> m (sul i o, [o])
walk sul [] = pure (sul, [])
walk sul (x : xs) = do
(sul', o) <- step sul x
(sul'', os) <- walk sul' xs
pure (sul'', o : os)
{-@ rangeIN :: (Enum i, Bounded i) => sul i o -> {is:[i] | len is > 0} @-}
rangeIN :: (Finite i) => sul i o -> [i]
rangeIN _ = minBound : [succ minBound .. maxBound]
{-@ rangeOUT :: (Enum o, Bounded o) => sul i o -> {os:[o] | len os > 0} @-}
rangeOUT :: (Finite o) => sul i o -> [o]
rangeOUT _ = minBound : [succ minBound .. maxBound]
{-@ assume Set.fromList :: Ord a => xs:[a] -> {s:Set.Set a | len xs > 0 => Set.size s > 0} @-}
-- | Return a Set containing only the valid inputs of the SUL.
{-@ inputs :: (Ord i, Enum i, Bounded i) => sul i o -> {is:Set.Set i | Set.size is > 0} @-}
inputs :: (FiniteOrd i) => sul i o -> Set.Set i
inputs x = Set.fromList $ rangeIN x
-- | Return a Set containing only the valid outputs of the SUL.
{-@ outputs :: (Ord o, Enum o, Bounded o) => sul i o -> {os:Set.Set o | Set.size os > 0} @-}
outputs :: (FiniteOrd o) => sul i o -> Set.Set o
outputs x = Set.fromList $ rangeOUT x
{- | The 'Automaton' type class extends the 'SUL' type class and adds
support for automata operations. Automatons are models, not programs,
so they are pure and operate in the Identity monad.
-}
class (SUL (aut s) Identity) => Automaton aut s where
transitions ::
(FiniteOrd i, FiniteOrd s) =>
aut s i o ->
Map.Map (s, i) (s, o)
states :: (FiniteOrd s) => aut s i o -> Set.Set s
current :: aut s i o -> s
update :: aut s i o -> s -> aut s i o
-- | Pure instance of 'step'.
stepPure :: (SUL sul Identity) => sul i o -> i -> (sul i o, o)
stepPure sul i = runIdentity (step sul i)
-- | Pure instance of 'walk'.
{-@ walkPure :: (SUL sul Identity) => sul i o -> is:[i] -> (sul i o, {os:[o] | len os == len is})@-}
walkPure :: (SUL sul Identity) => sul i o -> [i] -> (sul i o, [o])
walkPure sul i = runIdentity (walk sul i)
-- | Pure instance of 'reset'.
resetPure :: (SUL sul Identity) => sul i o -> sul i o
resetPure sul = runIdentity (reset sul)
-- | Return the initial state of an automaton.
initial :: (Automaton aut s) => aut s i o -> s
initial = current . resetPure
-- | Return the set of reachable states of an automaton.
reachable :: forall s i o aut. (Automaton aut s, Ord s, FiniteOrd i) => aut s i o -> Set.Set s
reachable aut = bfs [initial aut] $ Set.singleton (initial aut)
where
alphabet = inputs aut
bfs :: [s] -> Set.Set s -> Set.Set s
bfs [] visited = visited
bfs (st : queue) visited = bfs queue' visited'
where
aut' = update aut st
neighbours = Set.map (current . fst . stepPure aut') alphabet
visited' = visited `Set.union` neighbours
queue' = Set.toList (neighbours `Set.difference` visited) ++ queue
-- | Returns a map containing the shortest sequence to access each reachable state from the initial state.
accessSequences ::
forall s i o aut.
(Automaton aut s, FiniteOrd i, Ord s) =>
aut s i o ->
Map.Map s [i]
accessSequences aut = bfs [(initialSt, [])] (Set.singleton initialSt) (Map.singleton initialSt [])
where
alphabet = Set.toList (inputs aut)
initialSt = initial aut
bfs :: [(s, [i])] -> Set.Set s -> Map.Map s [i] -> Map.Map s [i]
bfs [] _ acc = Map.map List.reverse acc
bfs ((_, prefix) : rest) visited acc =
bfs (rest ++ newQueue) newVisited newMap
where
mo = fst $ walkPure (resetPure aut) (reverse prefix)
successors =
[ (nextState, input : prefix)
| input <- alphabet
, let nextState = current . fst $ stepPure mo input
, nextState `Set.notMember` visited
]
newMap = foldr (uncurry Map.insert) acc successors
newVisited = foldr (Set.insert . fst) visited successors
newQueue = successors
differenceFrom ::
( FiniteOrd i
, FiniteOrd s
, FiniteOrd s'
, Eq o
, Automaton aut1 s
, Automaton aut2 s'
) =>
aut1 s i o ->
s ->
aut2 s' i o ->
s' ->
Maybe [i]
differenceFrom aut1 s1 aut2 s2 = explore Map.empty [(s1, s2, [])]
where
alphabet = Set.toList (inputs aut1)
stepAndCurrent mo i = Bif.first current (stepPure mo i)
explore _ [] = Nothing
explore visited ((q1, q2, prefix) : queue) =
if (q1, q2) `Map.member` visited
then explore visited queue
else case discrepancy of
Just symbol -> Just $ reverse (symbol : prefix)
Nothing -> explore newVisited (queue ++ newQueue)
where
newVisited = Map.insert (q1, q2) prefix visited
mo1 = update aut1 q1
mo2 = update aut2 q2
(nextStates1, outputs1) = unzip $ map (stepAndCurrent mo1) alphabet
(nextStates2, outputs2) = unzip $ map (stepAndCurrent mo2) alphabet
discrepancy = snd <$> List.find fst (zip (zipWith (/=) outputs1 outputs2) alphabet)
appended = map (: prefix) alphabet
toBeVisited = Map.fromList $ zip (zip nextStates1 nextStates2) appended
newQueue = [(s1', s2', p) | ((s1', s2'), p) <- Map.toList toBeVisited, (s1', s2') `Map.notMember` visited]
{- | Finds a distinguishing sequence between two automata starting from their
- initial states.
-}
difference ::
( FiniteOrd i
, FiniteOrd s
, FiniteOrd s'
, Eq o
, Automaton aut1 s
, Automaton aut2 s'
) =>
aut1 s i o ->
aut2 s' i o ->
Maybe [i]
difference aut1 aut2 = differenceFrom aut1 (initial aut1) aut2 (initial aut2)
{- | Returns an input sequence that distinguishes the given states in
the given automaton.
-}
{-@ distinguish :: (Automaton aut s, FiniteOrd i, Ord s, Eq o) =>
aut s i o ->
s1:s ->
s2:s ->
{is:[i] | s1 == s2 ==> len is = 0}
@-}
distinguish ::
( Automaton aut s
, FiniteOrd s
, FiniteOrd i
, Eq o
) =>
aut s i o ->
s ->
s ->
[i]
distinguish _ s1 s2 | s1 == s2 = []
distinguish m s1 s2 = fromMaybe [] $ differenceFrom m s1 m s2
{- | Returns a set of lists of inputs that can be used to distinguish between the given state and
- any other state of the automaton.
-}
localCharacterizingSet ::
( Automaton aut s
, FiniteOrd i
, FiniteOrd s
, Eq o
) =>
aut s i o ->
s ->
Set.Set [i]
localCharacterizingSet m s = Set.fromList [d s sx | sx <- Set.toList $ states m, s /= sx]
where
d = distinguish m
{- | Returns a set of lists of inputs that can be used to distinguish between any two different states
of the automaton.
-}
globalCharacterizingSet ::
( Automaton aut s
, FiniteOrd i
, FiniteOrd s
, Eq o
) =>
aut s i o ->
Set.Set [i]
globalCharacterizingSet m = Set.fromList [d s1 s2 | s1 <- sts, s2 <- sts, s1 < s2]
where
sts = Set.toList $ states m
d = distinguish m