packages feed

automata (empty) → 0.1.0.0

raw patch · 13 files changed

+1828/−0 lines, 13 filesdep +HUnitdep +QuickCheckdep +automatasetup-changed

Dependencies added: HUnit, QuickCheck, automata, base, bytestring, containers, contiguous, enum-types, leancheck, leancheck-enum-instances, primitive, primitive-containers, quickcheck-classes, quickcheck-enum-instances, semirings, tasty, tasty-hunit, tasty-leancheck, tasty-quickcheck, transformers

Files

+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for automaton++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Andrew Martin (c) 2018++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Andrew Martin nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,1 @@+# automaton
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ automata.cabal view
@@ -0,0 +1,76 @@+cabal-version: 2.2+name: automata+version: 0.1.0.0+synopsis: automata+description:+  This package implements the following:+  .+  Deterministic Finite State Automata (DFSA)+  .+  Non-Deterministic Finite State Automata (NFSA)+  .+  Deterministic Finite State Transducers (DFST)+  .+  Non-Deterministic Finite State Transducers (NFST)+category: Data, Math+homepage: https://github.com/andrewthad/automata+bug-reports: https://github.com/andrewthad/automata/issues+author: Andrew Martin+maintainer: andrew.thaddeus@gmail.com+copyright: 2018 Andrew Martin+license: BSD-3-Clause+license-file: LICENSE+build-type: Simple+extra-source-files:+  ChangeLog.md+  README.md++source-repository head+  type: git+  location: https://github.com/andrewthad/automata++library+  hs-source-dirs: src+  exposed-modules:+    Automata.Dfsa+    Automata.Nfsa+    Automata.Nfsa.Builder+    Automata.Internal+    Automata.Internal.Transducer+    Automata.Nfst+    Automata.Dfst+  build-depends:+    , base >=4.10.1.0 && <5+    , bytestring >= 0.10.8+    , primitive >= 0.6.4+    , primitive-containers >= 0.3+    , containers >= 0.5.9+    , contiguous+    , semirings >= 0.3.1.1+    , transformers+  ghc-options: -O2+  default-language: Haskell2010++test-suite test+  type: exitcode-stdio-1.0+  main-is: Main.hs+  hs-source-dirs:+      test+  ghc-options: -threaded -rtsopts -with-rtsopts=-N -O2+  build-depends:+    , HUnit+    , QuickCheck+    , automata+    , base >=4.7 && <5+    , containers+    , enum-types >= 0.1+    , leancheck+    , leancheck-enum-instances >= 0.1+    , primitive+    , quickcheck-classes+    , quickcheck-enum-instances >= 0.1+    , tasty+    , tasty-hunit+    , tasty-leancheck+    , tasty-quickcheck+  default-language: Haskell2010
+ src/Automata/Dfsa.hs view
@@ -0,0 +1,116 @@+{-# language BangPatterns #-}+{-# language DeriveFunctor #-}+{-# language DerivingStrategies #-}+{-# language MagicHash #-}+{-# language RankNTypes #-}+{-# language ScopedTypeVariables #-}+{-# language UnboxedTuples #-}++module Automata.Dfsa+  ( -- * Static+    -- ** Types+    Dfsa+    -- ** Evaluation+  , evaluate+    -- ** Composition+  , union+  , intersection+    -- ** Special DFA+  , acceptance+  , rejection+    -- * Builder+    -- ** Types+  , Builder+  , State+    -- ** Functions+  , build+  , state+  , transition+  , accept+  ) where++import Automata.Internal (Dfsa(..),State(..),union,intersection,acceptance,rejection,minimize)+import Data.Foldable (foldl',for_)+import Data.Primitive (Array)+import Data.Semigroup (Last(..))+import Control.Monad.ST (runST)++import qualified Data.Primitive.Contiguous as C+import qualified Data.Map.Interval.DBTSLL as DM+import qualified Data.Set.Unboxed as SU++-- | Evaluate a foldable collection of tokens against the DFA. This+-- returns true if the string is accepted by the language.+evaluate :: (Foldable f, Ord t) => Dfsa t -> f t -> Bool+evaluate (Dfsa transitions finals) tokens = SU.member+  (foldl' (\(active :: Int) token -> DM.lookup token (C.index transitions active)) 0 tokens)+  finals++newtype Builder t s a = Builder (Int -> [Edge t] -> [Int] -> Result t a)+  deriving stock (Functor)++instance Applicative (Builder t s) where+  pure a = Builder (\i es fs -> Result i es fs a)+  Builder f <*> Builder g = Builder $ \i es fs -> case f i es fs of+    Result i' es' fs' x -> case g i' es' fs' of+      Result i'' es'' fs'' y -> Result i'' es'' fs'' (x y)++instance Monad (Builder t s) where+  Builder f >>= g = Builder $ \i es fs -> case f i es fs of+    Result i' es' fs' a -> case g a of+      Builder g' -> g' i' es' fs'++data Result t a = Result !Int ![Edge t] ![Int] a+  deriving stock (Functor)++data Edge t = Edge !Int !Int !t !t++data EdgeDest t = EdgeDest !Int t t++-- | The argument function takes a start state and builds an NFA. This+-- function will execute the builder.+build :: forall t a. (Bounded t, Ord t, Enum t) => (forall s. State s -> Builder t s a) -> Dfsa t+build fromStartState =+  case state >>= fromStartState of+    Builder f -> case f 0 [] [] of+      Result totalStates edges final _ ->+        let ts = runST $ do+              transitions <- C.replicateM totalStates (DM.pure Nothing)+              outbounds <- C.replicateM totalStates []+              for_ edges $ \(Edge source destination lo hi) -> do+                edgeDests0 <- C.read outbounds source+                let !edgeDests1 = EdgeDest destination lo hi : edgeDests0+                C.write outbounds source edgeDests1+              (outbounds' :: Array [EdgeDest t]) <- C.unsafeFreeze outbounds+              flip C.imapMutable' transitions $ \i _ ->+                let dests = C.index outbounds' i+                 in mconcat+                      ( map+                        (\(EdgeDest dest lo hi) -> DM.singleton Nothing lo hi (Just (Last dest)))+                        dests+                      )+              C.unsafeFreeze transitions+         in minimize (fmap (DM.map (maybe 0 getLast)) ts) (SU.fromList final)+  +-- | Generate a new state in the NFA. On any input, the state transitions to+--   the start state.+state :: Builder t s (State s)+state = Builder $ \i edges final ->+  Result (i + 1) edges final (State i)++-- | Mark a state as being an accepting state. +accept :: State s -> Builder t s ()+accept (State n) = Builder $ \i edges final -> Result i edges (n : final) ()++-- | Add a transition from one state to another when the input token+--   is inside the inclusive range. If multiple transitions from+--   a state are given, the last one given wins.+transition ::+     t -- ^ inclusive lower bound+  -> t -- ^ inclusive upper bound+  -> State s -- ^ from state+  -> State s -- ^ to state+  -> Builder t s ()+transition lo hi (State source) (State dest) =+  Builder $ \i edges final -> Result i (Edge source dest lo hi : edges) final ()+
+ src/Automata/Dfst.hs view
@@ -0,0 +1,190 @@+{-# language BangPatterns #-}+{-# language DeriveFunctor #-}+{-# language DerivingStrategies #-}+{-# language MagicHash #-}+{-# language RankNTypes #-}+{-# language ScopedTypeVariables #-}+{-# language UnboxedTuples #-}++module Automata.Dfst+  ( -- * Static+    -- ** Types+    Dfst+    -- ** Functions+  , evaluate+  , evaluateAscii+  , union+  , map+    -- ** Special Transducers+  , rejection+    -- * Builder+    -- ** Types+  , Builder+  , State+    -- ** Functions+  , build+  , state+  , transition+  , accept+  ) where++import Prelude hiding (map)++import Automata.Internal (State(..),Dfsa(..),composeMapping)+import Automata.Internal.Transducer (Dfst(..),MotionDfst(..),Edge(..),EdgeDest(..))+import Control.Monad.ST (runST)+import Data.Foldable (foldl',for_)+import Data.Map.Strict (Map)+import Data.Maybe (fromMaybe)+import Data.Primitive (Array,indexArray)+import Data.Semigroup (Last(..))+import Data.Set (Set)+import Data.ByteString (ByteString)++import qualified Data.ByteString.Char8 as BC+import qualified Data.List as L+import qualified Data.Map.Interval.DBTSLL as DM+import qualified Data.Map.Strict as M+import qualified Data.Primitive.Contiguous as C+import qualified Data.Set as S+import qualified Data.Set.Unboxed as SU+import qualified GHC.Exts as E++-- | Map over the output tokens.+map :: Eq n => (m -> n) -> Dfst t m -> Dfst t n+map f (Dfst t m) =+  -- Revisit this implementation if we ever start supporting the canonization+  -- and minimization of DFST.+  Dfst (fmap (DM.map (\(MotionDfst s x) -> MotionDfst s (f x))) t) m++-- | Rejects all input, producing the monoidal identity as its output.+rejection :: (Bounded t, Monoid m) => Dfst t m+rejection = Dfst (C.singleton (DM.pure (MotionDfst 0 mempty))) SU.empty++union :: forall t m. (Ord t, Bounded t, Enum t, Monoid m) => Dfst t m -> Dfst t m -> Dfst t m+union a@(Dfst ax _) b@(Dfst bx _) =+  let (mapping, Dfsa t0 f) = composeMapping (||) (unsafeToDfsa a) (unsafeToDfsa b)+      -- The revMapping goes from a new state to all a-b old state pairs.+      revMapping :: Map Int (Set (Int,Int))+      revMapping = M.foldlWithKey' (\acc k v -> M.insertWith (<>) v (S.singleton k) acc) M.empty mapping+      t1 :: Array (DM.Map t (MotionDfst m))+      t1 = C.imap+        (\source m -> DM.mapBijection+          (\dest ->+            let oldSources = fromMaybe (error "Automata.Nfst.toDfst: missing old source") (M.lookup source revMapping)+                oldDests = fromMaybe (error "Automata.Nfst.toDfst: missing old dest") (M.lookup dest revMapping)+                -- Do we need to deal with epsilon stuff in here? I don't think so.+                newOutput = foldMap+                  (\(oldSourceA,oldSourceB) -> mconcat $ E.toList $ do+                    MotionDfst oldDestA outA <- DM.elems (indexArray ax oldSourceA)+                    MotionDfst oldDestB outB <- DM.elems (indexArray bx oldSourceB)+                    if S.member (oldDestA,oldDestB) oldDests then pure (outA <> outB) else mempty+                  ) oldSources+             in MotionDfst dest newOutput+          ) m+        ) t0+   in Dfst t1 f++-- | Returns @Nothing@ if the transducer did not end up in an+--   accepting state. Returns @Just@ if it did. The array of+--   output tokens always matches the length of the input.+evaluate :: (Foldable f, Ord t) => Dfst t m -> f t -> Maybe (Array m)+evaluate (Dfst transitions finals) tokens =+  let !(!finalState,!totalSize,!allOutput) = foldl'+        (\(!active,!sz,!output) token ->+          let MotionDfst nextState outputToken = DM.lookup token (indexArray transitions active)+           in (nextState,sz + 1,outputToken : output)+        ) (0,0,[]) tokens+   in if SU.member finalState finals+        then Just (C.unsafeFromListReverseN totalSize allOutput)+        else Nothing++evaluateAscii :: forall m. Ord m => Dfst Char m -> ByteString -> Maybe (Array m)+evaluateAscii (Dfst transitions finals) !tokens =+  let !(!finalState,!allOutput) = BC.foldl'+        (\(!active,!output) token ->+          let MotionDfst nextState outputToken = DM.lookup token (indexArray transitions active)+           in (nextState,outputToken : output)+        ) (0,[]) tokens+   in if SU.member finalState finals+        then Just (C.unsafeFromListReverseN (BC.length tokens) allOutput)+        else Nothing++newtype Builder t m s a = Builder (Int -> [Edge t m] -> [Int] -> Result t m a)+  deriving stock (Functor)++data Result t m a = Result !Int ![Edge t m] ![Int] a+  deriving stock (Functor)++instance Applicative (Builder t m s) where+  pure a = Builder (\i es fs -> Result i es fs a)+  Builder f <*> Builder g = Builder $ \i es fs -> case f i es fs of+    Result i' es' fs' x -> case g i' es' fs' of+      Result i'' es'' fs'' y -> Result i'' es'' fs'' (x y)++instance Monad (Builder t m s) where+  Builder f >>= g = Builder $ \i es fs -> case f i es fs of+    Result i' es' fs' a -> case g a of+      Builder g' -> g' i' es' fs'++-- | Generate a new state in the NFA. On any input, the state transitions to+--   the start state.+state :: Builder t m s (State s)+state = Builder $ \i edges final ->+  Result (i + 1) edges final (State i)++-- | Mark a state as being an accepting state. +accept :: State s -> Builder t m s ()+accept (State n) = Builder $ \i edges final -> Result i edges (n : final) ()++-- | Add a transition from one state to another when the input token+--   is inside the inclusive range. If multiple transitions from+--   a state are given, the last one given wins.+transition ::+     t -- ^ inclusive lower bound+  -> t -- ^ inclusive upper bound+  -> m -- ^ output token+  -> State s -- ^ from state+  -> State s -- ^ to state+  -> Builder t m s ()+transition lo hi output (State source) (State dest) =+  Builder $ \i edges final -> Result i (Edge source dest lo hi output : edges) final ()++-- | The argument function turns a start state into an NFST builder. This+-- function converts the builder to a usable transducer.+build :: forall t m a. (Bounded t, Ord t, Enum t, Monoid m, Ord m) => (forall s. State s -> Builder t m s a) -> Dfst t m+build fromStartState =+  case state >>= fromStartState of+    Builder f -> case f 0 [] [] of+      Result totalStates edges final _ ->+        let ts0 = runST $ do+              transitions <- C.replicateM totalStates (DM.pure Nothing)+              outbounds <- C.replicateM totalStates []+              for_ edges $ \(Edge source destination lo hi output) -> do+                edgeDests0 <- C.read outbounds source+                let !edgeDests1 = EdgeDest destination lo hi output : edgeDests0+                C.write outbounds source edgeDests1+              (outbounds' :: Array [EdgeDest t m]) <- C.unsafeFreeze outbounds+              flip C.imapMutable' transitions $ \i _ -> +                let dests = C.index outbounds' i+                 in mconcat+                      ( L.map+                        (\(EdgeDest dest lo hi output) ->+                          DM.singleton mempty lo hi (Just (Last (MotionDfst dest output)))+                        )+                        dests+                      )+              C.unsafeFreeze transitions+         in Dfst (fmap (DM.map (maybe (MotionDfst 0 mempty) getLast)) ts0) (SU.fromList final)++-- collapse :: Dfst t m -> Dfst t m+-- collapse = MotionDfst ++-- Convert a DFST to a DFSA. However, the DFSA is not necessarily minimal, so+-- equality on it is incorrect. Its states have a one-to-one mapping with the+-- states on the DFST.+unsafeToDfsa :: Dfst t m -> Dfsa t+unsafeToDfsa (Dfst t f) = Dfsa (fmap (DM.map motionDfstState) t) f+++
+ src/Automata/Internal.hs view
@@ -0,0 +1,443 @@+{-# language BangPatterns #-}+{-# language LambdaCase #-}+{-# language MagicHash #-}+{-# language UnboxedTuples #-}+{-# language ScopedTypeVariables #-}++module Automata.Internal+  ( -- * Types+    Dfsa(..)+  , Nfsa(..)+  , TransitionNfsa(..)+    -- * Builder Types+  , State(..)+  , Epsilon(..)+    -- * NFA Functions +  , toDfsa+  , toDfsaMapping+  , append+  , empty+  , rejectionNfsa+  , unionNfsa+  , epsilonClosure+    -- * DFA Functions+  , union+  , intersection+  , acceptance+  , rejection+  , minimize+  , minimizeMapping+  , composeMapping+  ) where++import Control.Applicative (liftA2)+import Control.Monad (forM_,(<=<))+import Control.Monad.ST (runST)+import Data.Foldable (foldl',toList)+import Data.Map (Map)+import Data.Maybe (fromMaybe,isNothing,mapMaybe)+import Data.Primitive (Array,indexArray)+import Data.Semigroup (First(..))+import Data.Semiring (Semiring)+import Data.Set (Set)++import Debug.Trace++import qualified Data.List as L+import qualified Data.Set as S+import qualified Data.Set.Unboxed as SU+import qualified Data.Map.Strict as M+import qualified Control.Monad.Trans.State.Strict as State+import qualified Data.Map.Interval.DBTSLL as DM+import qualified Data.Map.Unboxed.Lifted as MUL+import qualified Data.Primitive.Contiguous as C+import qualified Data.Primitive as PM+import qualified Data.Map.Lifted.Lifted as MLL+import qualified GHC.Exts as E+import qualified Data.Semiring++-- | Deterministic Finite State Automaton.+--+-- The start state is always zero.+data Dfsa t = Dfsa+  { dfaTransition :: !(Array (DM.Map t Int))+    -- ^ Given a state and transition, this field tells you what+    --   state to go to next. The length of this array must match+    --   the total number of states.+  , dfaFinal :: !(SU.Set Int)+    -- ^ A string that ends in any of these set of states is+    --   considered to have been accepted by the grammar.+  } deriving (Eq,Show)++-- | Non-Deterministic Finite State Automaton.+--+-- Some notes on the implementation and design:+--+-- * You can transition to any non-negative number of states (including 0).+-- * There is only one start state.+-- * We use the Thompson encoding. This means that there is an epsilon+--   transition that consumes no input.+-- * We store the full epsilon closure for every state. This means that,+--   when evaluating the NFA, we do not ever need to compute the closure.+-- * There is no Eq instance for NFA. In general, this can take exponential+--   time. If you really need to do this, convert the NFA to a DFA.+--+-- Invariants:+-- +-- * The start state is always the state at position 0.+-- * The length of nfaTransition is given by nfaStates.+data Nfsa t = Nfsa+  { nfaTransition :: !(Array (TransitionNfsa t))+    -- ^ Given a state and transition, this field tells you what+    --   state to go to next. The length of this array must match+    --   the total number of states. The data structure inside is+    --   a diet map. This is capable of collapsing adjacent key-value+    --   pairs into ranges.+  , nfaFinal :: !(SU.Set Int)+    -- ^ A string that ends in any of these set of states is+    --   considered to have been accepted by the grammar.+  } deriving (Show)++data TransitionNfsa t = TransitionNfsa+  { transitionNfsaEpsilon :: {-# UNPACK #-} !(SU.Set Int)+  , transitionNfsaConsume :: {-# UNPACK #-} !(DM.Map t (SU.Set Int))+  } deriving (Eq,Show)++data Conversion = Conversion+  { conversionLabel :: !Int+    -- The state identifier to be assigned to the next state.+  , conversionResolutions :: !(Map (SU.Set Int) Int)+    -- The map from subsets of states to new state identifiers.+    -- This is a bidirectional map.+  , conversionTraversed :: !(Set Int)+    -- The new state identifiers that have already been dealt with.+    -- This must be a subset of the keys of resolutions.+  , conversionPending :: !(Map Int (SU.Set Int))+    -- Newly created states that we need to consider transitions for.+    -- The keys in this should all be less than the label.+  }++data Pairing = Pairing+  { pairingMap :: !(Map (Int,Int) Int)+  , pairingReversedOld :: ![(Int,Int)]+  , pairingState :: !Int+  }++append :: Nfsa t -> Nfsa t -> Nfsa t+append (Nfsa t1 f1) (Nfsa t2 f2) = +  let n1 = C.size t1+      n2 = C.size t2+      n3 = n1 + n2+      f3 = SU.mapMonotonic (+n1) f2+      t3 = fmap (\(TransitionNfsa eps consume) -> TransitionNfsa (SU.mapMonotonic (+n1) eps) (DM.mapBijection (SU.mapMonotonic (+n1)) consume)) t2+      t4 = fmap (\(TransitionNfsa eps consume) -> TransitionNfsa eps (DM.mapBijection (\states -> if SU.null (SU.intersection states f1) then states else states <> transitionNfsaEpsilon (C.index t3 0)) consume)) t1+      !(# placeholder #) = C.index# t1 0+      t5 = runST $ do+        m <- C.replicateM n3 placeholder+        C.copy m 0 t4 0 n1+        C.copy m n1 t3 0 n2+        flip SU.traverse_ f1 $ \ix -> do+          TransitionNfsa epsilon consume <- C.read m ix+          let transition = TransitionNfsa (epsilon <> transitionNfsaEpsilon (C.index t3 0)) consume+          C.write m ix transition+        C.unsafeFreeze m+   in Nfsa t5 f3++nextIdentifier :: State.State Conversion Int+nextIdentifier = do+  Conversion n a b c <- State.get +  State.put (Conversion (n + 1) a b c)+  return n++-- Mark a new state as having been completed.+complete :: Int -> State.State Conversion ()+complete s = do+  c <- State.get+  State.put c+    { conversionTraversed = S.insert s (conversionTraversed c)+    , conversionPending = M.delete s (conversionPending c)+    }++-- Convert the subset of NFA states to a single DFA state.+resolveSubset :: Array (TransitionNfsa t) -> SU.Set Int -> State.State Conversion Int+resolveSubset transitions s0 = do+  let s = epsilonClosure transitions s0+  Conversion _ resolutions0 _ _ <- State.get+  case M.lookup s resolutions0 of+    Nothing -> do+      ident <- nextIdentifier+      c <- State.get+      State.put c+        { conversionResolutions = M.insert s ident (conversionResolutions c)+        , conversionPending = M.insert ident s (conversionPending c)+        }+      return ident+    Just ident -> return ident+  +epsilonClosure :: Array (TransitionNfsa t) -> SU.Set Int -> SU.Set Int+epsilonClosure s states = go states SU.empty where+  go new old = if new == old+    then new+    else+      let together = old <> new+       in go (mconcat (map (\ident -> transitionNfsaEpsilon (indexArray s ident)) (SU.toList together)) <> together) together++data Node t = Node+  !Int -- identifier+  !(DM.Map t Int) -- transitions++-- | Convert an NFSA to a DFSA. For certain inputs, this causes+--   the number of states to blow up expontentially, so do not+--   call this on untrusted input.+toDfsa :: (Ord t, Bounded t, Enum t) => Nfsa t -> Dfsa t+toDfsa = snd . toDfsaMapping++toDfsaMapping :: forall t. (Ord t, Bounded t, Enum t) => Nfsa t -> (Map (SU.Set Int) Int, Dfsa t)+toDfsaMapping (Nfsa t0 f0) = runST $ do+  let ((len,nodes),c) = State.runState+        (go 0 [])+        (Conversion 1 (M.singleton startClosure 0) S.empty (M.singleton 0 startClosure))+      resolutions = conversionResolutions c+  marr <- C.new len+  forM_ nodes $ \(Node ident transitions) -> C.write marr ident transitions+  arr <- C.unsafeFreeze marr+  let f1 = SU.fromList (M.foldrWithKey (\k v xs -> if SU.null (SU.intersection k f0) then xs else v : xs) [] resolutions)+  let (canonB,r) = minimizeMapping arr f1+      canon = fmap (fromMaybe (error "toDfsaMapping: missing canon value") . flip M.lookup canonB) resolutions+  return (canon,r)+  where+  startClosure :: SU.Set Int+  startClosure = epsilonClosure t0 (SU.singleton 0)+  go :: Int -> [Node t] -> State.State Conversion (Int, [Node t])+  go !n !edges0 = do+    Conversion _ _ _ pending <- State.get+    case M.foldMapWithKey (\k v -> Just (First (k,v))) pending of+      Nothing -> return (n, edges0)+      Just (First (m,states)) -> do+        t <- DM.traverseBijection (resolveSubset t0) (mconcat (map (transitionNfsaConsume . indexArray t0) (SU.toList states)))+        complete m+        go (n + 1) (Node m t : edges0)++-- | This uses Hopcroft's Algorithm. It is like a smart constructor for Dfsa.+minimize :: (Ord t, Bounded t, Enum t) => Array (DM.Map t Int) -> SU.Set Int -> Dfsa t+minimize t0 f0 = snd (minimizeMapping t0 f0)++-- | This uses Hopcroft's Algorithm. It also provides the mapping from old+--   state number to new state number. We need this mapping for a special+--   NFST to DFST minimizer.+minimizeMapping :: forall t. (Ord t, Bounded t, Enum t) => Array (DM.Map t Int) -> SU.Set Int -> (Map Int Int, Dfsa t)+minimizeMapping t0 f0 =+  let partitions0 = go (S.fromList [f1,S.difference q0 f1]) (S.singleton f1)+      -- We move the partition containing the start state to the front.+      partitions1 = case L.find (S.member 0) partitions0 of+        Just startStates -> startStates : deletePredicate (\s -> S.member 0 s || S.null s) (S.toList partitions0)+        Nothing -> error "Automata.Nfsa.minimize: incorrect"+      -- Creates a map from old state to new state. This is not a bijection+      -- since two old states may map to the same new state. However, we+      -- may treat it as a bijection since at most one of the old states+      -- is preserved.+      assign :: Int -> Map Int Int -> [Set Int] -> Map Int Int+      assign !_ !m [] = m+      assign !ix !m (s : ss) = assign (ix + 1) (M.union (M.fromSet (const ix) s) m) ss+      assignments = assign 0 M.empty partitions1+      newTransitions0 = E.fromList (map (\s -> DM.map (\oldState -> fromMaybe (error "Automata.Nfsa.minimize: missing state") (M.lookup oldState assignments)) (PM.indexArray t1 (S.findMin s))) partitions1)+      canonization = establishOrder newTransitions0+      description = "[canonization=" ++ show canonization ++ "][assignments=" ++ show assignments ++ "]"+      newTransitions1 :: Array (DM.Map t Int) = C.map' (DM.mapBijection (\s -> fromMaybe (error ("Automata.Nfsa.minimize: canonization missing state [state=" ++ show s ++ "]" ++ description)) (M.lookup s canonization))) newTransitions0+      newTransitions2 = runST $ do+        marr <- C.replicateM (M.size canonization) (error ("Automata.Nfsa.minimize: uninitialized element " ++ description))+        flip C.itraverse_ newTransitions1 $ \ix dm -> C.write marr (fromMaybe (error ("Automata.Nfsa.minimize: missing state while rearranging [state=" ++ show ix ++ "]" ++ description)) (M.lookup ix canonization)) dm+        C.unsafeFreeze marr+      newAcceptingStates = foldMap (maybe SU.empty SU.singleton . (flip M.lookup canonization <=< flip M.lookup assignments)) f1+      finalCanonization = fmap (fromMaybe (error ("minimizeMapping: failed to connect the canons.\npartitions:\n" ++ show partitions1 ++ "\ninitial canon:\n" ++ show initialCanonization ++ "\nsecond canon:\n" ++ show canonization)) . (flip M.lookup canonization <=< flip M.lookup assignments)) initialCanonization+   in (finalCanonization,Dfsa newTransitions2 newAcceptingStates)+  where+  q0 = S.fromList (enumFromTo 0 (C.size t1 - 1))+  f1 = S.fromList (mapMaybe (\x -> M.lookup x initialCanonization) (SU.toList f0))+  -- Do we actually need to canonize the states twice? Yes, we do.+  t1' :: Array (DM.Map t Int)+  t1' = C.map' (DM.mapBijection (\s -> fromMaybe (error "Automata.Nfsa.minimize: t1 prime") (M.lookup s initialCanonization))) t0+  t1 = runST $ do+    marr <- C.replicateM (M.size initialCanonization) (error "Automata.Nfsa.minimize: t1 uninitialized element")+    flip C.itraverse_ t1' $ \ix dm -> case M.lookup ix initialCanonization of+      Nothing -> return ()+      Just newIx -> C.write marr newIx dm+    C.unsafeFreeze marr+  initialCanonization = establishOrder t0+  -- The inverted transitions has the destination state as well as+  -- all source states that lead to it when the token is consumed. +  invertedTransitions :: DM.Map t (MLL.Map Int (Set Int))+  invertedTransitions = mconcat (toList (C.imap (\ix m -> DM.mapBijection (\dest -> MLL.singleton dest (S.singleton ix)) m) t1 :: Array (DM.Map t (MLL.Map Int (Set Int)))))+  -- The result of go is set of disjoint sets. It represents the equivalence classes+  -- that have been established. All references to any state in an equivalence class+  -- can be replaced with any of the other states in the same equivalence class.+  go :: Set (Set Int) -> Set (Set Int) -> Set (Set Int)+  go p1 w1 = case S.minView w1 of+    Nothing -> p1+    Just (a,w2) ->+      let (p2,w3) = DM.foldl'+            (\(p3,w4) m ->+              let x = foldMap (\s -> fromMaybe S.empty (MLL.lookup s m)) a+               in foldl'+                    (\(p4, w5) y ->+                      let diffYX = S.difference y x+                          intersectYX = S.intersection y x+                       in if not (S.disjoint x y) && not (S.null diffYX)+                            then+                              ( S.insert diffYX (S.insert intersectYX (S.delete y p4))+                              , if S.member y w5+                                  then S.insert diffYX (S.insert intersectYX (S.delete y w5))+                                  else if S.size intersectYX <= S.size diffYX+                                    then S.insert intersectYX w5+                                    else S.insert diffYX w5+                              )+                            else (S.insert y p4, w5)+                    ) (S.empty, w4) p3+            ) (p1,w2) invertedTransitions+       in go p2 w3++-- This gives a canonical order to the states. Any state missing from+-- the resulting map was not reachable. The map goes from old state+-- identifiers to new state identifiers. It is a bijection.+establishOrder :: Array (DM.Map t Int) -> Map Int Int+establishOrder t = go 0 [0] M.empty where+  go :: Int -> [Int] -> Map Int Int -> Map Int Int+  go !ident !unvisited0 !assignments = case unvisited0 of+    [] -> assignments+    state : unvisited1 -> if isNothing (M.lookup state assignments)+      then+        let unvisited2 = DM.foldl'+             (\unvisited s -> if isNothing (M.lookup s assignments) then s : unvisited else unvisited)+             unvisited1+             (PM.indexArray t state)+         in go (ident + 1) unvisited2 (M.insert state ident assignments)+      else go ident unvisited1 assignments++-- removeUnreachable :: Array (DM.Map t Int) -> SU.Set Int -> (Array (DM.Map t Int), SU.Set Int)++deletePredicate :: (a -> Bool) -> [a] -> [a]+deletePredicate _ [] = []+deletePredicate p (y:ys) = if p y then deletePredicate p ys else y : deletePredicate p ys++-- | Accepts input that is accepted by both of the two argument DFAs. This is also known+--   as completely synchronous composition in the literature.+intersection :: (Ord t, Bounded t, Enum t) => Dfsa t -> Dfsa t -> Dfsa t+intersection = compose (&&)++-- Adjusts all the values in the first interval map by multiplying+-- them by the number of states in the second automaton. Then,+-- adds these to the states numbers from the second automaton+scoot :: Ord t => Int -> DM.Map t Int -> DM.Map t Int -> DM.Map t Int+scoot n2 d1 d2 = DM.unionWith (\s1 s2 -> n2 * s1 + s2) d1 d2++{-# NOINLINE errorThunkUnion #-}+errorThunkUnion :: a+errorThunkUnion = error "Automata.Dfsa.union: slot not filled"++-- | Accepts input that is accepted by either of the two argument DFAs. This is also known+--   as synchronous composition in the literature.+union :: (Ord t, Bounded t, Enum t) => Dfsa t -> Dfsa t -> Dfsa t+union = compose (||)++composeMapping :: (Ord t, Bounded t, Enum t) => (Bool -> Bool -> Bool) -> Dfsa t -> Dfsa t -> (Map (Int,Int) Int, Dfsa t)+composeMapping combineFinalMembership (Dfsa t1 f1) (Dfsa t2 f2) = runST $ do+  let Pairing oldToNew reversedOld n3 = compositionReachable t1 t2+  m <- PM.newArray n3 errorThunkUnion+  let go !_ [] = return ()+      go !ix (statePair@(stateA,stateB) : xs) = do+        PM.writeArray m ix (DM.unionWith (\x y -> fromMaybe (error "Automata.Dfsa.union: could not find pair in oldToNew") (M.lookup (x,y) oldToNew)) (PM.indexArray t1 stateA) (PM.indexArray t2 stateB))+        go (ix - 1) xs+  go (n3 - 1) reversedOld+  frozen <- PM.unsafeFreezeArray m+  let finals = SU.fromList (M.foldrWithKey (\(stateA,stateB) stateNew xs -> if combineFinalMembership (SU.member stateA f1) (SU.member stateB f2) then stateNew : xs else xs) [] oldToNew)+  let (secondMapping, r) = minimizeMapping frozen finals+  return (M.map (\x -> fromMaybe (error "composeMapping: bad lookup") (M.lookup x secondMapping)) oldToNew, r)++compose :: (Ord t, Bounded t, Enum t) => (Bool -> Bool -> Bool) -> Dfsa t -> Dfsa t -> Dfsa t+compose combineFinalMembership a b = snd (composeMapping combineFinalMembership a b)++compositionReachable :: Ord t => Array (DM.Map t Int) -> Array (DM.Map t Int) -> Pairing+compositionReachable a b = State.execState (go 0 0) (Pairing M.empty [] 0) where+  !szA = PM.sizeofArray a+  !szB = PM.sizeofArray b+  go :: Int -> Int -> State.State Pairing ()+  go !stateA !stateB = do+    Pairing m xs s <- State.get+    case M.lookup (stateA,stateB) m of+      Just _ -> return ()+      Nothing -> do+        State.put (Pairing (M.insert (stateA,stateB) s m) ((stateA,stateB) : xs) (s + 1))+        DM.traverse_ (uncurry go) (DM.unionWith (,) (PM.indexArray a stateA) (PM.indexArray b stateB))+    ++-- | Docs for this are at @Automata.Nfsa.union@.+unionNfsa :: (Bounded t) => Nfsa t -> Nfsa t -> Nfsa t+unionNfsa (Nfsa t1 f1) (Nfsa t2 f2) = Nfsa+  ( runST $ do+      m <- C.replicateM (n1 + n2 + 1)+        ( TransitionNfsa+          (mconcat+            [ SU.mapMonotonic (+1) (transitionNfsaEpsilon (C.index t1 0))+            , SU.mapMonotonic (\x -> 1 + n1) (transitionNfsaEpsilon (C.index t2 0))+            , SU.tripleton 0 1 (1 + n1)+            ]+          )+          (DM.pure SU.empty)+        )+      C.copy m 1 (fmap (translateTransitionNfsa 1) t1) 0 n1+      C.copy m (1 + n1) (fmap (translateTransitionNfsa (1 + n1)) t2) 0 n2+      C.unsafeFreeze m+  )+  (SU.mapMonotonic (+1) f1 <> SU.mapMonotonic (\x -> 1 + n1 + x) f2)+  where+  !n1 = PM.sizeofArray t1+  !n2 = PM.sizeofArray t2++translateTransitionNfsa :: Int -> TransitionNfsa t -> TransitionNfsa t+translateTransitionNfsa n (TransitionNfsa eps m) = TransitionNfsa+  (SU.mapMonotonic (+n) eps)+  (DM.mapBijection (SU.mapMonotonic (+n)) m)++-- | Automaton that accepts all input. This is the identity+-- for 'intersection'.+acceptance :: Bounded t => Dfsa t+acceptance = Dfsa (C.singleton (DM.pure 0)) (SU.singleton 0)++-- | Automaton that rejects all input. This is the identity+-- for 'union'.+rejection :: Bounded t => Dfsa t+rejection = Dfsa (C.singleton (DM.pure 0)) SU.empty++-- | Automaton that accepts the empty string and rejects all+-- other strings. This is the identity for 'append'.+empty :: Bounded t => Nfsa t+empty = Nfsa+  ( C.doubleton+    (TransitionNfsa (SU.singleton 0) (DM.pure (SU.singleton 1)))+    (TransitionNfsa (SU.singleton 1) (DM.pure SU.empty))+  )+  (SU.singleton 0)++-- | Docs for this are at @Automata.Nfsa.rejection@.+rejectionNfsa :: Bounded t => Nfsa t+rejectionNfsa = Nfsa+  (C.singleton (TransitionNfsa (SU.singleton 0) (DM.pure SU.empty)))+  SU.empty++-- | This uses 'union' for @plus@ and 'intersection' for @times@.+instance (Ord t, Enum t, Bounded t) => Semiring (Dfsa t) where+  plus = union+  times = intersection+  zero = rejection+  one = acceptance++-- | This uses @union@ for @plus@ and @append@ for @times@.+instance (Bounded t) => Semiring (Nfsa t) where+  plus = unionNfsa+  times = append+  zero = rejectionNfsa+  one = empty+  +data Epsilon = Epsilon !Int !Int++newtype State s = State Int
+ src/Automata/Internal/Transducer.hs view
@@ -0,0 +1,108 @@+{-# language BangPatterns #-}++module Automata.Internal.Transducer+  ( Nfst(..)+  , TransitionNfst(..)+  , Dfst(..)+  , MotionDfst(..)+  , Edge(..)+  , EdgeDest(..)+  , epsilonClosure+  , rejection+  , union+  ) where++import Control.Monad.ST (runST)+import Data.Primitive (Array)+import Data.Primitive (indexArray)+import qualified Data.Primitive as PM+import qualified Data.Primitive.Contiguous as C+import qualified Data.Set.Unboxed as SU+import qualified Data.Map.Interval.DBTSLL as DM+import qualified Data.Map.Lifted.Unlifted as MLN++-- | A deterministic finite state transducer.+data Dfst t m = Dfst+  { dfstTransition :: !(Array (DM.Map t (MotionDfst m)))+    -- ^ Given a state and transition, this field tells you what+    --   state to go to next. The length of this array must match+    --   the total number of states.+  , dfstFinal :: !(SU.Set Int)+    -- ^ A string that ends in any of these set of states is+    --   considered to have been accepted by the grammar.+  } deriving (Eq,Show)++data MotionDfst m = MotionDfst+  { motionDfstState :: !Int+  , motionDfstOutput :: !m+  } deriving (Eq,Show)++-- | A nondeterministic finite state transducer. The @t@ represents the input token on+-- which a transition occurs. The @m@ represents the output token that+-- is generated when a transition is taken. On an epsilon transation,+-- no output is generated.+data Nfst t m = Nfst+  { nfstTransition :: !(Array (TransitionNfst t m))+    -- ^ Given a state and transition, this field tells you what+    --   state to go to next. The length of this array must match+    --   the total number of states. The data structure inside is+    --   an interval map. This is capable of collapsing adjacent key-value+    --   pairs into ranges.+  , nfstFinal :: !(SU.Set Int)+    -- ^ A string that ends in any of these set of states is+    --   considered to have been accepted by the grammar.+  } deriving (Eq,Show)++data TransitionNfst t m = TransitionNfst+  { transitionNfstEpsilon :: {-# UNPACK #-} !(SU.Set Int)+  , transitionNfstConsume :: {-# UNPACK #-} !(DM.Map t (MLN.Map m (SU.Set Int)))+  } deriving (Eq,Show)++epsilonClosure :: Array (TransitionNfst m t) -> SU.Set Int -> SU.Set Int+epsilonClosure s states = go states SU.empty where+  go new old = if new == old+    then new+    else+      let together = old <> new+       in go (mconcat (map (\ident -> transitionNfstEpsilon (indexArray s ident)) (SU.toList together)) <> together) together++data Edge t m = Edge !Int !Int !t !t !m++data EdgeDest t m = EdgeDest !Int !t !t !m++-- | Transducer that rejects all input, generating the monoid identity as output.+-- This is the identity for 'union'.+rejection :: (Ord t, Bounded t, Monoid m, Ord m) => Nfst t m+rejection = Nfst+  (C.singleton (TransitionNfst (SU.singleton 0) (DM.pure mempty)))+  SU.empty++-- | Accepts input that is accepts by either of the transducers, producing the+--   output of both of them.+union :: (Bounded t, Ord m) => Nfst t m -> Nfst t m -> Nfst t m+union (Nfst t1 f1) (Nfst t2 f2) = Nfst+  ( runST $ do+      m <- C.replicateM (n1 + n2 + 1)+        ( TransitionNfst+          (mconcat+            [ SU.mapMonotonic (+1) (transitionNfstEpsilon (C.index t1 0))+            , SU.mapMonotonic (\x -> 1 + n1) (transitionNfstEpsilon (C.index t2 0))+            , SU.tripleton 0 1 (1 + n1)+            ]+          )+          (DM.pure mempty)+        )+      C.copy m 1 (fmap (translateTransitionNfst 1) t1) 0 n1+      C.copy m (1 + n1) (fmap (translateTransitionNfst (1 + n1)) t2) 0 n2+      C.unsafeFreeze m+  )+  (SU.mapMonotonic (+1) f1 <> SU.mapMonotonic (\x -> 1 + n1 + x) f2)+  where+  !n1 = PM.sizeofArray t1+  !n2 = PM.sizeofArray t2++translateTransitionNfst :: Int -> TransitionNfst t m -> TransitionNfst t m+translateTransitionNfst n (TransitionNfst eps m) = TransitionNfst+  (SU.mapMonotonic (+n) eps)+  (DM.mapBijection (MLN.map (SU.mapMonotonic (+n))) m)+
+ src/Automata/Nfsa.hs view
@@ -0,0 +1,63 @@+{-# language BangPatterns #-}+{-# language LambdaCase #-}+{-# language MagicHash #-}+{-# language UnboxedTuples #-}+{-# language ScopedTypeVariables #-}++module Automata.Nfsa+  ( -- * Types+    Nfsa+    -- * Conversion+  , toDfsa+    -- * Evaluation+  , evaluate+    -- * Composition+  , AI.append+  , union+    -- * Special NFA+  , rejection+  , AI.empty+  ) where++import Automata.Internal (Nfsa(..),Dfsa(..),TransitionNfsa(..),toDfsa)+import Data.Semigroup (First(..))+import Control.Monad.Trans.State.Strict (State)+import Data.Set (Set)+import Data.Map (Map)+import Control.Monad.ST (runST)+import Data.Primitive (Array,indexArray)+import Control.Monad (forM_)+import Data.Foldable (foldl')++import qualified Automata.Internal as AI+import qualified Data.Set as S+import qualified Data.Set.Unboxed as SU+import qualified Data.Map.Strict as M+import qualified Control.Monad.Trans.State.Strict as State+import qualified Data.Map.Interval.DBTSLL as DM+import qualified Data.Map.Unboxed.Lifted as MUL+import qualified Data.Primitive.Contiguous as C+import qualified Data.Primitive as PM++fromDfsa :: Dfsa t -> Nfsa t+fromDfsa (Dfsa t f) =+  Nfsa (fmap (TransitionNfsa SU.empty . DM.mapBijection SU.singleton) t) f++rejection :: Bounded t => Nfsa t+rejection = AI.rejectionNfsa++union :: (Bounded t) => Nfsa t -> Nfsa t -> Nfsa t+union = AI.unionNfsa++-- note: turn foldl' + mconcat into single foldMap?+evaluate :: (Foldable f, Ord t) => Nfsa t -> f t -> Bool+evaluate (Nfsa transitions finals) tokens = not $ SU.null $ SU.intersection+  ( foldl'+    ( \(active :: SU.Set Int) token -> mconcat $ SU.foldl'+      (\xs state -> DM.lookup token (transitionNfsaConsume (C.index transitions state)) : xs)+      ([] :: [SU.Set Int])+      active+    ) (transitionNfsaEpsilon (C.index transitions 0)) tokens+  )+  finals+
+ src/Automata/Nfsa/Builder.hs view
@@ -0,0 +1,111 @@+{-# language BangPatterns #-}+{-# language DeriveFunctor #-}+{-# language DerivingStrategies #-}+{-# language RankNTypes #-}+{-# language ScopedTypeVariables #-}++module Automata.Nfsa.Builder+  ( Builder+  , run+  , state+  , transition+  , accept+  , epsilon+  ) where++import Automata.Internal (Nfsa(..),TransitionNfsa(..),epsilonClosure)+import Control.Monad.ST (runST)+import Data.Foldable (for_)+import Data.Primitive (Array)+import qualified Data.Map.Interval.DBTSLL as DM+import qualified Data.Primitive.Contiguous as C+import qualified Data.Set.Unboxed as SU++newtype Builder t s a = Builder (Int -> [Edge t] -> [Epsilon] -> [Int] -> Result t a)+  deriving stock (Functor)++instance Applicative (Builder t s) where+  pure a = Builder (\i es eps fs -> Result i es eps fs a)+  Builder f <*> Builder g = Builder $ \i es eps fs -> case f i es eps fs of+    Result i' es' eps' fs' x -> case g i' es' eps' fs' of+      Result i'' es'' eps'' fs'' y -> Result i'' es'' eps'' fs'' (x y)++instance Monad (Builder t s) where+  Builder f >>= g = Builder $ \i es eps fs -> case f i es eps fs of+    Result i' es' eps' fs' a -> case g a of+      Builder g' -> g' i' es' eps' fs'++data Result t a = Result !Int ![Edge t] ![Epsilon] ![Int] a+  deriving stock (Functor)++data Edge t = Edge !Int !Int !t !t++data EdgeDest t = EdgeDest !Int !t !t++data Epsilon = Epsilon !Int !Int++newtype State s = State Int++-- | The argument function takes a start state and builds an NFSA. This+-- function will execute the builder.+run :: forall t a. (Bounded t, Ord t, Enum t) => (forall s. State s -> Builder t s a) -> Nfsa t+run fromStartState =+  case state >>= fromStartState of+    Builder f -> case f 0 [] [] [] of+      Result totalStates edges epsilons final _ ->+        let ts0 = runST $ do+              transitions <- C.replicateM totalStates (TransitionNfsa SU.empty (DM.pure SU.empty))+              outbounds <- C.replicateM totalStates []+              epsilonArr <- C.replicateM totalStates []+              for_ epsilons $ \(Epsilon source destination) -> do+                edgeDests0 <- C.read epsilonArr source+                let !edgeDests1 = destination : edgeDests0+                C.write epsilonArr source edgeDests1+              (epsilonArr' :: Array [Int]) <- C.unsafeFreeze epsilonArr+              for_ edges $ \(Edge source destination lo hi) -> do+                edgeDests0 <- C.read outbounds source+                let !edgeDests1 = EdgeDest destination lo hi : edgeDests0+                C.write outbounds source edgeDests1+              (outbounds' :: Array [EdgeDest t]) <- C.unsafeFreeze outbounds+              flip C.imapMutable' transitions $ \i (TransitionNfsa _ _) -> +                let dests = C.index outbounds' i+                    eps = C.index epsilonArr' i+                 in TransitionNfsa (SU.fromList eps)+                      ( mconcat+                        ( map+                          (\(EdgeDest dest lo hi) -> DM.singleton SU.empty lo hi (SU.singleton dest))+                          dests+                        )+                      )+              C.unsafeFreeze transitions+            ts1 = C.imap (\s (TransitionNfsa eps consume) -> TransitionNfsa (epsilonClosure ts0 (SU.singleton s <> eps)) (DM.map (epsilonClosure ts0) consume)) ts0+         in Nfsa ts1 (SU.fromList final)+  +-- | Generate a new state in the NFA. On any input, the+--   state transitions to zero states.+state :: Builder t s (State s)+state = Builder $ \i edges eps final -> Result (i + 1) edges eps final (State i)++-- | Mark a state as being an accepting state. +accept :: State s -> Builder t s ()+accept (State n) = Builder $ \i edges eps final -> Result i edges eps (n : final) ()++-- | Add a transition from one state to another when the input token+--   is inside the inclusive range.+transition ::+     t -- ^ inclusive lower bound+  -> t -- ^ inclusive upper bound+  -> State s -- ^ from state+  -> State s -- ^ to state+  -> Builder t s ()+transition lo hi (State source) (State dest) =+  Builder $ \i edges eps final -> Result i (Edge source dest lo hi : edges) eps final ()++-- | Add a transition from one state to another that consumes no input.+epsilon ::+     State s -- ^ from state+  -> State s -- ^ to state+  -> Builder t s ()+epsilon (State source) (State dest) = +  Builder $ \i edges eps final -> Result i edges (if source /= dest then Epsilon source dest : eps else eps) final ()+
+ src/Automata/Nfst.hs view
@@ -0,0 +1,218 @@+{-# language BangPatterns #-}+{-# language DeriveFunctor #-}+{-# language DerivingStrategies #-}+{-# language LambdaCase #-}+{-# language MagicHash #-}+{-# language UnboxedTuples #-}+{-# language RankNTypes #-}+{-# language ScopedTypeVariables #-}++module Automata.Nfst+  ( -- * Static+    -- ** Types+    Nfst+    -- ** Functions+  , evaluate+  , evaluateAscii+  , union+  , toDfst+  , toNfsa+    -- ** Special Transducers+  , rejection+    -- * Builder+    -- ** Types+  , Builder+  , State+    -- ** Functions+  , build+  , state+  , transition+  , epsilon+  , accept+  ) where++import Automata.Internal (State(..),Epsilon(..),Nfsa(..),Dfsa(..),TransitionNfsa(..),toDfsaMapping)+import Automata.Internal.Transducer (Nfst(..),Dfst(..),TransitionNfst(..),MotionDfst(..),Edge(..),EdgeDest(..),epsilonClosure,rejection,union)+import Control.Monad.ST (runST)+import Data.ByteString (ByteString)+import Data.Foldable (for_,fold)+import Data.Map.Strict (Map)+import Data.Maybe (fromMaybe)+import Data.Monoid (Any(..))+import Data.Primitive (Array,indexArray)+import Data.Set (Set)++import Debug.Trace++import qualified Data.ByteString.Char8 as BC+import qualified Data.Map.Strict as M+import qualified Data.Set as S+import qualified Data.Set.Unboxed as SU+import qualified Data.Map.Interval.DBTSLL as DM+import qualified Data.Map.Lifted.Unlifted as MLN+import qualified Data.Primitive.Contiguous as C+import qualified Data.Foldable as F++debugTrace :: Show a => a -> a+debugTrace = id++-- | Evaluate an NFST. If the output is the empty set, the input string+-- did not belong to the language. Otherwise, all possible outputs given.+-- The output token lists are in reverse order, and they are all the exact+-- same length as the input string. The reversed order is done to maximize+-- opportunities for sharing common output prefixes. To get the output tokens+-- in the right order, reverse the NFST before evaluating an input string+-- against it. Then, the output tokens will be in the right order, and they will+-- share common suffixes in memory.+evaluate :: forall f t m. (Foldable f, Ord t, Ord m) => Nfst t m -> f t -> Set [m]+evaluate (Nfst transitions finals) tokens = S.unions $ M.elems $ M.filterWithKey+  (\k _ -> SU.member k finals)+  (F.foldl' step (M.unionsWith (<>) (map (\s -> M.singleton s (S.singleton [])) (SU.toList (transitionNfstEpsilon (C.index transitions 0))))) tokens)+  where+  step :: Map Int (Set [m]) -> t -> Map Int (Set [m])+  step active token = M.unionsWith (<>) $ M.foldlWithKey'+    ( \xs state outputSets -> MLN.foldlWithKey'+        (\zs outputTokenNext nextStates -> M.unionsWith (<>) (map (\s -> M.singleton s (S.mapMonotonic (outputTokenNext:) outputSets)) (SU.toList nextStates)) : zs)+        xs+        (DM.lookup token (transitionNfstConsume (C.index transitions state)))+    ) [] active++evaluateAscii :: forall m. Ord m => Nfst Char m -> ByteString -> Set [m]+evaluateAscii (Nfst transitions finals) tokens = S.unions $ M.elems $ M.filterWithKey+  (\k _ -> SU.member k finals)+  (BC.foldl' step (M.unionsWith (<>) (map (\s -> M.singleton s (S.singleton [])) (SU.toList (transitionNfstEpsilon (C.index transitions 0))))) tokens)+  where+  step :: Map Int (Set [m]) -> Char -> Map Int (Set [m])+  step active token = M.unionsWith (<>) $ M.foldlWithKey'+    ( \xs state outputSets -> MLN.foldlWithKey'+        (\zs outputTokenNext nextStates -> M.unionsWith (<>) (map (\s -> M.singleton s (S.mapMonotonic (outputTokenNext:) outputSets)) (SU.toList nextStates)) : zs)+        xs+        (DM.lookup token (transitionNfstConsume (C.index transitions state)))+    ) [] active++-- | Convert an NFST to a DFST that accepts the same input and produces+--   output. Since NFST are more powerful than DFST, it is not possible+--   to preserve output of the NFST during this conversion. However,+--   this function makes the guarantee that if the NFST would accepts+--   an input string and produces the output+--+--   > [[a1,a2,a3,...],[b1,b2,b3,...],...]+--+--   Then DFST will accept the same input and produce an output of+--+--   > ∃ ω1 ω2. [ω1 <> a1 <> b1 <> ..., ω2 <> a1 <> b1 <> ...]+--+--   This must be a commutative semigroup, and the existentially+--   quantified values appended to the output cannot be easily+--   predicted.+toDfst :: forall t m. (Ord t, Bounded t, Enum t, Monoid m) => Nfst t m -> Dfst t m+toDfst x@(Nfst tx _) =+  let (mapping,Dfsa t0 f) = toDfsaMapping (toNfsa x)+      mapping' = debugTrace mapping+      -- The revMapping goes from new state id to a set of old state subsets+      revMapping :: Map Int (SU.Set Int)+      revMapping = debugTrace $ M.foldlWithKey' (\acc k v -> M.insertWith (<>) v k acc) M.empty mapping'+      t1 = C.imap+        (\source m -> DM.mapBijection+          (\dest ->+            let oldSources = fromMaybe (error "Automata.Nfst.toDfst: missing old source") (M.lookup source revMapping)+                oldDests = fromMaybe (error "Automata.Nfst.toDfst: missing old dest") (M.lookup dest revMapping)+                -- Do we need to deal with epsilon stuff in here? I don't think so.+                -- Also, this part could be greatly improved. We are using a very simple heuristic,+                -- and we could prune out far more outputs if we were more clever about this.+                newOutput = SU.foldMap (\oldSource -> DM.foldMap (MLN.foldMapWithKey' (\output oldDestStates -> if getAny (SU.foldMap (\oldDest -> Any (SU.member oldDest oldDests)) oldDestStates) then output else mempty)) (transitionNfstConsume (indexArray tx oldSource))) oldSources+             in MotionDfst dest newOutput+          ) m+        ) t0+   in Dfst t1 f++-- | Discard information about output tokens.+toNfsa :: Nfst t m -> Nfsa t+toNfsa (Nfst t f) = Nfsa+  (fmap (\(TransitionNfst eps m) -> TransitionNfsa eps (DM.map (MLN.foldlWithKey' (\acc _ x -> acc <> x) mempty) m)) t)+  f++newtype Builder t m s a = Builder (Int -> [Edge t m] -> [Epsilon] -> [Int] -> Result t m a)+  deriving stock (Functor)++data Result t m a = Result !Int ![Edge t m] ![Epsilon] ![Int] a+  deriving stock (Functor)++instance Applicative (Builder t m s) where+  pure a = Builder (\i es eps fs -> Result i es eps fs a)+  Builder f <*> Builder g = Builder $ \i es eps fs -> case f i es eps fs of+    Result i' es' eps' fs' x -> case g i' es' eps' fs' of+      Result i'' es'' eps'' fs'' y -> Result i'' es'' eps'' fs'' (x y)++instance Monad (Builder t m s) where+  Builder f >>= g = Builder $ \i es eps fs -> case f i es eps fs of+    Result i' es' eps' fs' a -> case g a of+      Builder g' -> g' i' es' eps' fs'++-- | Generate a new state in the NFA. On any input, the+--   state transitions to zero states.+state :: Builder t m s (State s)+state = Builder $ \i edges eps final -> Result (i + 1) edges eps final (State i)++-- | Mark a state as being an accepting state. +accept :: State s -> Builder t m s ()+accept (State n) = Builder $ \i edges eps final -> Result i edges eps (n : final) ()++-- | Add a transition from one state to another when the input token+--   is inside the inclusive range.+transition ::+     t -- ^ inclusive lower bound+  -> t -- ^ inclusive upper bound+  -> m -- ^ output token+  -> State s -- ^ from state+  -> State s -- ^ to state+  -> Builder t m s ()+transition lo hi output (State source) (State dest) =+  Builder $ \i edges eps final -> Result i (Edge source dest lo hi output : edges) eps final ()++-- | Add a transition from one state to another that consumes no input.+--   No output is generated on such a transition.+epsilon ::+     State s -- ^ from state+  -> State s -- ^ to state+  -> Builder t m s ()+epsilon (State source) (State dest) = +  Builder $ \i edges eps final -> Result i edges (if source /= dest then Epsilon source dest : eps else eps) final ()++-- | The argument function turns a start state into an NFST builder. This+-- function converts the builder to a usable transducer.+build :: forall t m a. (Bounded t, Ord t, Enum t, Monoid m, Ord m) => (forall s. State s -> Builder t m s a) -> Nfst t m+build fromStartState =+  case state >>= fromStartState of+    Builder f -> case f 0 [] [] [] of+      Result totalStates edges epsilons final _ ->+        let ts0 = runST $ do+              transitions <- C.replicateM totalStates (TransitionNfst SU.empty (DM.pure mempty))+              outbounds <- C.replicateM totalStates []+              epsilonArr <- C.replicateM totalStates []+              for_ epsilons $ \(Epsilon source destination) -> do+                edgeDests0 <- C.read epsilonArr source+                let !edgeDests1 = destination : edgeDests0+                C.write epsilonArr source edgeDests1+              (epsilonArr' :: Array [Int]) <- C.unsafeFreeze epsilonArr+              for_ edges $ \(Edge source destination lo hi output) -> do+                edgeDests0 <- C.read outbounds source+                let !edgeDests1 = EdgeDest destination lo hi output : edgeDests0+                C.write outbounds source edgeDests1+              (outbounds' :: Array [EdgeDest t m]) <- C.unsafeFreeze outbounds+              flip C.imapMutable' transitions $ \i (TransitionNfst _ _) -> +                let dests = C.index outbounds' i+                    eps = C.index epsilonArr' i+                 in TransitionNfst+                      ( SU.fromList eps )+                      ( mconcat+                        ( map+                          (\(EdgeDest dest lo hi output) ->+                            DM.singleton mempty lo hi (MLN.singleton output (SU.singleton dest)) :: DM.Map t (MLN.Map m (SU.Set Int))+                          )+                          dests+                        )+                      )+              C.unsafeFreeze transitions+            ts1 = C.imap (\s (TransitionNfst eps consume) -> TransitionNfst (epsilonClosure ts0 (SU.singleton s <> eps)) (DM.map (MLN.map (epsilonClosure ts0)) consume)) ts0+         in Nfst ts1 (SU.fromList final)
+ test/Main.hs view
@@ -0,0 +1,467 @@+{-# language DerivingStrategies #-}+{-# language LambdaCase #-}+{-# language ScopedTypeVariables #-}++import Automata.Dfsa (Dfsa)+import Automata.Dfst (Dfst)+import Automata.Nfsa (Nfsa)+import Automata.Nfst (Nfst)+import Control.Monad (forM_,replicateM)+import Data.Enum.Types (B(..),D(..))+import Data.Monoid (All(..))+import Data.Primitive (Array)+import Data.Proxy (Proxy(..))+import Data.Set (Set)+import Test.HUnit ((@?=),assertBool)+import Test.LeanCheck (Listable,(\/),cons0)+import Test.LeanCheck.Instances.Enum ()+import Test.QuickCheck (Arbitrary)+import Test.QuickCheck.Instances.Enum ()+import Test.Tasty (TestTree,defaultMain,testGroup,adjustOption)+import Test.Tasty.HUnit (testCase)++import qualified Automata.Nfsa as Nfsa+import qualified Automata.Nfst as Nfst+import qualified Automata.Dfsa as Dfsa+import qualified Automata.Dfst as Dfst+import qualified Automata.Nfsa.Builder as B+import qualified Data.Set as S+import qualified Data.List as L+import qualified GHC.Exts as E+import qualified Test.Tasty.LeanCheck as TL+import qualified Test.QuickCheck as QC+import qualified Test.Tasty.QuickCheck as TQC+import qualified Test.QuickCheck.Classes as QCC++main :: IO ()+main = defaultMain+  $ adjustOption (\_ -> TL.LeanCheckTests 5000)+  $ tests ++tests :: TestTree+tests = testGroup "Automata"+  [ testGroup "Nfsa"+    [ testGroup "evaluate"+      [ testCase "A" (Nfsa.evaluate ex1 [D3,D1] @?= False)+      , testCase "B" (Nfsa.evaluate ex1 [D0,D1,D3] @?= True)+      , testCase "C" (Nfsa.evaluate ex1 [D1,D3,D3] @?= True)+      , testCase "D" (Nfsa.evaluate ex1 [D0,D0,D0] @?= False)+      , testCase "E" (Nfsa.evaluate ex1 [D0,D0] @?= False)+      , testCase "F" (Nfsa.evaluate ex1 [D1] @?= True)+      , testCase "G" (Nfsa.evaluate ex1 [D1,D3] @?= True)+      , testCase "H" (Nfsa.evaluate ex2 [D3,D3,D0] @?= False)+      , testCase "I" (Nfsa.evaluate ex2 [D3,D3,D2] @?= True)+      , testCase "J" (Nfsa.evaluate ex3 [D1] @?= False)+      , testCase "K" (Nfsa.evaluate ex3 [D1,D3] @?= True)+      , testCase "L" (Nfsa.evaluate ex3 [D1,D3,D0] @?= False)+      , testCase "M" (Nfsa.evaluate ex3 [D1,D3,D0,D2,D3] @?= True)+      , testCase "N" (Nfsa.evaluate ex3 [D1,D3,D3] @?= True)+      ]+    , testGroup "append"+      [ testCase "A" (Nfsa.evaluate (Nfsa.append ex1 ex2) [D0,D1,D3,D3,D3,D2] @?= True)+      , testCase "B" (Nfsa.evaluate (Nfsa.append ex1 ex2) [D0,D0,D3,D0] @?= False)+      , testCase "C" (Nfsa.evaluate (Nfsa.append ex1 ex2) [D1,D3] @?= True)+      , testCase "D" (Nfsa.evaluate (Nfsa.append ex2 ex3) [D3,D3,D2,D1,D3,D3] @?= True)+      , testCase "E" (Nfsa.evaluate (Nfsa.append ex2 ex3) [D3,D3,D2] @?= False)+      ]+    , testGroup "union"+      [ testGroup "unit"+        [ testCase "A" (Nfsa.evaluate (Nfsa.union ex1 ex2) [D3,D1] @?= True)+        , testCase "B" (Nfsa.evaluate (Nfsa.union ex1 ex2) [D3,D3,D2] @?= True)+        , testCase "C" (Nfsa.evaluate (Nfsa.union ex1 ex2) [D0,D0,D0] @?= False)+        ]+      ]+    , testGroup "toDfsa"+      [ testGroup "unit"+        [ testCase "A" (Dfsa.evaluate (Nfsa.toDfsa ex1) [D0,D1,D3] @?= True)+        , testCase "B" (Dfsa.evaluate (Nfsa.toDfsa ex1) [D3,D1] @?= False)+        , testCase "C" (Dfsa.evaluate (Nfsa.toDfsa (Nfsa.append ex1 ex2)) [D0,D1,D3,D3,D3,D2] @?= True)+        , testCase "D" (Dfsa.evaluate (Nfsa.toDfsa (Nfsa.append ex2 ex3)) [D3,D3,D2,D1,D3,D3] @?= True)+        , testCase "E" (Dfsa.evaluate (Nfsa.toDfsa (Nfsa.append ex1 ex2)) [D0,D0,D3,D0] @?= False)+        , testCase "F" (Nfsa.toDfsa ex1 == Nfsa.toDfsa ex4 @?= True)+        , testCase "G" (Nfsa.toDfsa ex1 == Nfsa.toDfsa ex2 @?= False)+        , testCase "H" (Nfsa.toDfsa ex5 == Nfsa.toDfsa ex6 @?= True)+        ]+      , testGroup "evaluation"+        [ TL.testProperty "1" $ \(a,b,c,d) -> Dfsa.evaluate (Nfsa.toDfsa ex1) [a,b,c,d] == Nfsa.evaluate ex1 [a,b,c,d]+        , TL.testProperty "2" $ \(a,b,c,d) -> Dfsa.evaluate (Nfsa.toDfsa ex2) [a,b,c,d] == Nfsa.evaluate ex2 [a,b,c,d]+        , TL.testProperty "3" $ \(a,b,c,d) -> Dfsa.evaluate (Nfsa.toDfsa ex3) [a,b,c,d] == Nfsa.evaluate ex3 [a,b,c,d]+        , TL.testProperty "4" $ \(a,b,c,d) -> Dfsa.evaluate (Nfsa.toDfsa ex4) [a,b,c,d] == Nfsa.evaluate ex4 [a,b,c,d]+        , TL.testProperty "5" $ \(a,b,c,d) -> Dfsa.evaluate (Nfsa.toDfsa ex5) [a,b,c,d] == Nfsa.evaluate ex5 [a,b,c,d]+        , TL.testProperty "6" $ \(a,b,c,d) -> Dfsa.evaluate (Nfsa.toDfsa ex6) [a,b,c,d] == Nfsa.evaluate ex6 [a,b,c,d]+        , TL.testProperty "7" $ \(a,b,c,d) -> Dfsa.evaluate (Nfsa.toDfsa ex7) [a,b,c,d] == Nfsa.evaluate ex7 [a,b,c,d]+        ]+      , lawsToTest (QCC.semiringLaws (Proxy :: Proxy (Nfsa D)))+      ]+    ]+  , testGroup "Dfsa"+    [ testGroup "evaluate"+      [ testCase "A" (Dfsa.evaluate exDfsa1 [D1] @?= True)+      , testCase "B" (Dfsa.evaluate exDfsa1 [D3,D2,D1,D2,D0] @?= True)+      , testCase "C" (Dfsa.evaluate exDfsa2 [D3,D3] @?= False)+      , testCase "D" (Dfsa.evaluate exDfsa2 [D1] @?= True)+      , testCase "E" (Dfsa.evaluate exDfsa2 [D0,D2] @?= True)+      ]+    , testGroup "union"+      [ testGroup "unit"+        [ testCase "A" (Dfsa.evaluate (Dfsa.union (Nfsa.toDfsa ex1) (Nfsa.toDfsa ex3)) [D0,D1,D3] @?= True)+        , testCase "B" (Dfsa.evaluate (Dfsa.union (Nfsa.toDfsa ex1) (Nfsa.toDfsa ex3)) [D2,D3] @?= True)+        , testCase "C" (Dfsa.evaluate (Dfsa.union (Nfsa.toDfsa ex1) (Nfsa.toDfsa ex3)) [D1,D3] @?= True)+        , testCase "D" (Dfsa.evaluate (Dfsa.union (Nfsa.toDfsa ex1) (Nfsa.toDfsa ex3)) [D1,D3,D0] @?= False)+        , testCase "E" (Dfsa.evaluate (Dfsa.union (Nfsa.toDfsa ex1) (Nfsa.toDfsa ex3)) [D1] @?= True)+        , testCase "F" (Dfsa.evaluate (Dfsa.union (Nfsa.toDfsa ex1) (Nfsa.toDfsa ex3)) [D3] @?= False)+        , testCase "G" (Dfsa.union (Nfsa.toDfsa ex1) (Nfsa.toDfsa ex3) @?= Dfsa.union (Nfsa.toDfsa ex3) (Nfsa.toDfsa ex1))+        , testCase "H" (Dfsa.union (Nfsa.toDfsa ex1) (Nfsa.toDfsa ex1) @?= (Nfsa.toDfsa ex1))+        , testCase "I" (Dfsa.union (Nfsa.toDfsa ex3) (Nfsa.toDfsa ex3) @?= (Nfsa.toDfsa ex3))+        ]+      , TL.testProperty "idempotent" $ \x -> let y = mkBinDfsa x in y == Dfsa.union y y+      , testGroup "identity"+        [ TL.testProperty "left" $ \x -> let y = mkBinDfsa x in y == Dfsa.union Dfsa.rejection y+        , TL.testProperty "right" $ \x -> let y = mkBinDfsa x in y == Dfsa.union y Dfsa.rejection+        ]+      ]+    , testGroup "intersection"+      [ TL.testProperty "idempotent" $ \x -> let y = mkBinDfsa x in y == Dfsa.intersection y y+      , testGroup "identity"+        [ TL.testProperty "left" $ \x -> let y = mkBinDfsa x in y == Dfsa.intersection Dfsa.acceptance y+        , TL.testProperty "right" $ \x -> let y = mkBinDfsa x in y == Dfsa.intersection y Dfsa.acceptance+        ]+      ]+    , lawsToTest (QCC.semiringLaws (Proxy :: Proxy (Dfsa D)))+    ]+  , testGroup "Nfst"+    [ testGroup "evaluate"+      [ testCase "A" (Nfst.evaluate exNfst1 [D0,D1] @?= S.singleton [B1,B0])+      , testCase "B" (Nfst.evaluate exNfst1 [D2,D1,D3] @?= S.singleton [B1,B1,B1])+      , testCase "C" (Nfst.evaluate exNfst2 [D0,D0] @?= S.singleton [B0,B0])+      , testCase "D" (Nfst.evaluate exNfst2 [D1,D0] @?= S.fromList [[B0,B0],[B0,B1]])+      , testCase "E" (Nfst.evaluate exNfst3 [D0,D2] @?= S.singleton [B1,B0])+      , testCase "F" (Nfst.evaluate exNfst3 [D0,D1] @?= S.singleton [B0,B1])+      , testCase "G" (Nfst.evaluate (Nfst.union Nfst.rejection exNfst3) [D0,D1] @?= S.singleton [B0,B1])+      , testCase "H" (Nfst.evaluate (Nfst.union exNfst1 exNfst3) [D0,D1] @?= S.fromList [[B1,B0],[B0,B1]])+      , testCase "I" (Nfst.evaluate (Nfst.union exNfst3 exNfst1) [D0,D1] @?= S.fromList [[B1,B0],[B0,B1]])+      ]+    , testGroup "toDfst"+      [ testGroup "unit"+        [ testCase "A" (let x = Dfst.evaluate (Nfst.toDfst exNfst4) [D0,D1] in assertBool (show x) (setSubresult [B1, B0] x))+        , testCase "B" (let x = Dfst.evaluate (Nfst.toDfst exNfst5) [D1,D2] in assertBool (show x) (setSubresult [B0, B1] x))+        ]+      , testGroup "evaluation"+        [ TL.testProperty "4" $ \(input :: [D]) -> getAll (foldMap (\x -> All (subresult (L.reverse x) (Dfst.evaluate (Nfst.toDfst exNfst4) input))) (Nfst.evaluate exNfst4 input))+        , TL.testProperty "5" $ \(input :: [D]) -> getAll (foldMap (\x -> All (subresult (L.reverse x) (Dfst.evaluate (Nfst.toDfst exNfst5) input))) (Nfst.evaluate exNfst5 input))+        , TL.testProperty "6" $ \(input :: [D]) -> getAll (foldMap (\x -> All (subresult (L.reverse x) (Dfst.evaluate (Nfst.toDfst exNfst6) input))) (Nfst.evaluate exNfst6 input))+        ]+      ]+    ]+  , testGroup "Dfst"+    [ testGroup "evaluate"+      [ testCase "A" (Dfst.evaluate exDfst1 [D0,D2] @?= Nothing)+      , testCase "B" (Dfst.evaluate exDfst1 [D0,D1] @?= Just (E.fromList [B1,B0]))+      ]+    , testGroup "union"+      [ testGroup "unit"+        [ testCase "A" (let x = Dfst.evaluate (Dfst.union (Dfst.map S.singleton exDfst1) (Dfst.map S.singleton exDfst2)) [D0,D1] in assertBool (show x) (setSubresult [B0, B1] x))+        , testCase "B" (let x = Dfst.evaluate (Dfst.union (Dfst.map S.singleton exDfst1) (Dfst.map S.singleton exDfst2)) [D0,D3] in assertBool (show x) (setSubresult [B0, B0] x))+        ]+      ]+    ]+  ]++subresult :: Ord a => [Set a] -> Maybe (Array (Set a)) -> Bool+subresult xs = \case+  Nothing -> False+  Just ys -> length xs == length ys && all (uncurry S.isSubsetOf) (zip xs (E.toList ys))++setSubresult :: Ord a => [a] -> Maybe (Array (Set a)) -> Bool+setSubresult xs = \case+  Nothing -> False+  Just ys -> length xs == length ys && all (uncurry S.member) (zip xs (E.toList ys))++lawsToTest :: QCC.Laws -> TestTree+lawsToTest (QCC.Laws name pairs) = testGroup name (map (uncurry TQC.testProperty) pairs)++instance Semigroup B where+  (<>) = max++instance Monoid B where+  mempty = minBound++instance (Arbitrary t, Bounded t, Enum t, Ord t) => Arbitrary (Dfsa t) where+  arbitrary = do+    let states = 6+    n <- QC.choose (0,30)+    (ts :: [(Int,Int,t,t)]) <- QC.vectorOf n $ (,,,)+      <$> QC.choose (0,states)+      <*> QC.choose (0,states)+      <*> QC.arbitrary+      <*> QC.arbitrary+    return $ Dfsa.build $ \s0 -> do+      states <- fmap (s0:) (replicateM states Dfsa.state)+      Dfsa.accept (states L.!! 3)+      forM_ ts $ \(source,dest,a,b) -> do+        let lo = min a b+            hi = max a b+        Dfsa.transition lo hi (states L.!! source) (states L.!! dest)++instance (Arbitrary t, Bounded t, Enum t, Ord t) => Arbitrary (Nfsa t) where+  arbitrary = do+    let states = 3+    n <- QC.choose (0,20)+    (ts :: [(Int,Int,t,t,Bool)]) <- QC.vectorOf n $ (,,,,)+      <$> QC.choose (0,states)+      <*> QC.choose (0,states)+      <*> QC.arbitrary+      <*> QC.arbitrary+      <*> QC.frequency [(975,pure False),(25,pure True)]+    return $ B.run $ \s0 -> do+      states <- fmap (s0:) (replicateM states B.state)+      B.accept (states L.!! 1)+      forM_ ts $ \(source,dest,a,b,epsilon) -> do+        let lo = min a b+            hi = max a b+        if epsilon+          then B.epsilon (states L.!! source) (states L.!! dest)+          else B.transition lo hi (states L.!! source) (states L.!! dest)++-- This instance is provided for testing. The library does not provide+-- an Eq instance for Nfsa since there is no efficent algorithm to do this+-- in general.+instance (Ord t, Bounded t, Enum t) => Eq (Nfsa t) where+  a == b = Nfsa.toDfsa a == Nfsa.toDfsa b++ex1 :: Nfsa D+ex1 = B.run $ \s0 -> do+  s1 <- B.state+  B.accept s1+  B.transition D1 D2 s0 s1+  B.transition D0 D0 s0 s0+  B.transition D3 D3 s1 s1++ex2 :: Nfsa D+ex2 = B.run $ \s0 -> do+  s1 <- B.state+  B.accept s1+  B.transition D1 D2 s0 s1+  B.transition D0 D0 s0 s0+  B.transition D3 D3 s0 s0+  B.transition D3 D3 s0 s1+  B.transition D3 D3 s1 s1++ex3 :: Nfsa D+ex3 = B.run $ \s0 -> do+  s1 <- B.state+  s2 <- B.state+  B.accept s2+  B.transition D1 D2 s0 s1+  B.transition D3 D3 s1 s2+  B.transition D2 D3 s1 s0+  B.transition D0 D0 s2 s0+  B.epsilon s2 s1++ex4 :: Nfsa D+ex4 = B.run $ \s0 -> do+  s1 <- B.state+  s2 <- B.state+  B.accept s1+  B.accept s2+  B.transition D1 D2 s0 s1+  B.transition D0 D0 s0 s0+  B.transition D3 D3 s1 s1+  B.transition D1 D2 s0 s2+  B.transition D3 D3 s2 s2++ex5 :: Nfsa D+ex5 = B.run $ \s0 -> do+  s1 <- B.state+  s2 <- B.state+  B.accept s2+  B.transition D0 D1 s0 s1+  B.transition D1 D2 s1 s2++-- Note: ex5 and ex6 accept the same inputs.+ex6 :: Nfsa D+ex6 = B.run $ \s0 -> do+  -- s3, s4, and s5 are unreachable+  s3 <- B.state+  s4 <- B.state+  s5 <- B.state+  s2 <- B.state+  s1 <- B.state+  B.accept s2+  B.transition D0 D1 s0 s1+  B.transition D1 D2 s1 s2+  B.epsilon s3 s4+  B.transition D0 D2 s4 s5+  B.transition D2 D2 s5 s3+  B.transition D1 D2 s5 s3++ex7 :: Nfsa D+ex7 = B.run $ \s0 -> do+  s1 <- B.state+  s2 <- B.state+  s3 <- B.state+  s4 <- B.state+  s5 <- B.state+  B.accept s3+  B.accept s4+  B.transition D0 D0 s0 s1+  B.transition D0 D0 s0 s2+  B.transition D2 D2 s1 s3+  B.transition D0 D0 s1 s4+  B.transition D1 D1 s2 s4+  B.transition D3 D3 s1 s3+  B.transition D3 D3 s2 s5+  B.transition D2 D3 s4 s4+  B.epsilon s4 s5+  B.epsilon s5 s4+++exDfsa1 :: Dfsa D+exDfsa1 = Dfsa.build $ \s0 -> do+  s1 <- Dfsa.state+  Dfsa.accept s1+  Dfsa.transition D0 D1 s0 s1++exDfsa2 :: Dfsa D+exDfsa2 = Dfsa.build $ \s0 -> do+  s1 <- Dfsa.state+  s2 <- Dfsa.state+  Dfsa.accept s2+  Dfsa.transition D0 D3 s0 s1+  Dfsa.transition D1 D2 s0 s2+  Dfsa.transition D2 D3 s1 s1+  Dfsa.transition D2 D2 s1 s2+  Dfsa.transition D3 D3 s2 s2++exNfst1 :: Nfst D B+exNfst1 = Nfst.build $ \s0 -> do+  s1 <- Nfst.state+  Nfst.accept s1+  Nfst.transition D0 D1 B0 s0 s1 +  Nfst.transition D2 D3 B1 s0 s1 +  Nfst.transition D0 D3 B1 s1 s1 ++exNfst2 :: Nfst D B+exNfst2 = Nfst.build $ \s0 -> do+  s1 <- Nfst.state+  s2 <- Nfst.state+  Nfst.epsilon s0 s1+  Nfst.accept s2+  Nfst.transition D0 D1 B0 s0 s1 +  Nfst.transition D2 D3 B1 s0 s1 +  Nfst.transition D0 D0 B0 s1 s2 +  Nfst.transition D1 D3 B1 s1 s1+  Nfst.transition D0 D0 B0 s2 s2+  Nfst.transition D1 D3 B1 s2 s0++exNfst3 :: Nfst D B+exNfst3 = Nfst.build $ \s0 -> do+  s1 <- Nfst.state+  s2 <- Nfst.state+  s3 <- Nfst.state+  s4 <- Nfst.state+  Nfst.accept s3+  Nfst.accept s4+  Nfst.transition D0 D0 B0 s0 s1+  Nfst.transition D0 D0 B1 s0 s2+  Nfst.transition D2 D2 B1 s1 s3+  Nfst.transition D1 D1 B0 s2 s4+  Nfst.transition D3 D3 B0 s1 s3+  Nfst.transition D3 D3 B0 s2 s4++exNfst4 :: Nfst D (Set B)+exNfst4 = Nfst.build $ \s0 -> do+  s1 <- Nfst.state+  s2 <- Nfst.state+  s3 <- Nfst.state+  s4 <- Nfst.state+  s5 <- Nfst.state+  Nfst.accept s3+  Nfst.accept s4+  Nfst.transition D0 D0 (S.singleton B0) s0 s1+  Nfst.transition D0 D0 (S.singleton B1) s0 s2+  Nfst.transition D2 D2 (S.singleton B1) s1 s3+  Nfst.transition D0 D0 (S.singleton B0) s1 s4+  Nfst.transition D1 D1 (S.singleton B0) s2 s4+  Nfst.transition D3 D3 (S.singleton B0) s1 s3+  Nfst.transition D3 D3 (S.singleton B0) s2 s5+  Nfst.transition D2 D3 (S.singleton B1) s4 s4+  Nfst.epsilon s4 s5+  Nfst.epsilon s5 s4++exNfst5 :: Nfst D (Set B)+exNfst5 = Nfst.build $ \s0 -> do+  s1 <- Nfst.state+  s2 <- Nfst.state+  Nfst.accept s2+  Nfst.transition D1 D1 (S.singleton B0) s0 s1+  Nfst.transition D2 D2 (S.singleton B1) s1 s2++exNfst6 :: Nfst D (Set B)+exNfst6 = Nfst.build $ \s0 -> do+  s1 <- Nfst.state+  s2 <- Nfst.state+  s3 <- Nfst.state+  s4 <- Nfst.state+  s5 <- Nfst.state+  s6 <- Nfst.state+  Nfst.epsilon s0 s4+  Nfst.accept s2+  Nfst.accept s6+  Nfst.transition D1 D1 (S.singleton B1) s0 s1+  Nfst.transition D3 D3 (S.singleton B0) s0 s4+  Nfst.transition D2 D2 (S.singleton B1) s1 s2+  Nfst.transition D3 D3 (S.singleton B0) s1 s4+  Nfst.transition D2 D2 (S.singleton B1) s1 s2+  Nfst.transition D0 D1 (S.singleton B0) s4 s6+  Nfst.transition D1 D1 (S.singleton B1) s6 s4+  Nfst.transition D0 D0 (S.singleton B0) s4 s1++exDfst1 :: Dfst D B+exDfst1 = Dfst.build $ \s0 -> do+  s1 <- Dfst.state+  s2 <- Dfst.state+  s3 <- Dfst.state+  s4 <- Dfst.state+  Dfst.accept s3+  Dfst.accept s4+  Dfst.transition D0 D0 B0 s0 s1+  Dfst.transition D0 D0 B1 s0 s2+  Dfst.transition D2 D2 B1 s1 s3+  Dfst.transition D1 D1 B0 s2 s4+  Dfst.transition D3 D3 B0 s1 s3+  Dfst.transition D3 D3 B0 s2 s4++exDfst2 :: Dfst D B+exDfst2 = Dfst.build $ \s0 -> do+  s1 <- Dfst.state+  s2 <- Dfst.state+  Dfst.accept s2+  Dfst.transition D0 D0 B0 s0 s1+  Dfst.transition D1 D1 B1 s1 s2+  Dfst.transition D2 D2 B0 s2 s0++-- This uses s3 as a dead state. So, we are roughly testing+-- all DFA with three nodes, a binary transition function,+-- and a single fixed end state.+mkBinDfsa :: ((D,D),(D,D),(D,D)) -> Dfsa B+mkBinDfsa (ws,xs,ys) = Dfsa.build $ \s0 -> do+  s1 <- Dfsa.state+  s2 <- Dfsa.state+  s3 <- Dfsa.state+  Dfsa.accept s1+  let resolve = \case+        D0 -> s0+        D1 -> s1+        D2 -> s2+        D3 -> s3+      binTransitions (a,b) s = do+        Dfsa.transition B0 B0 s (resolve a)+        Dfsa.transition B1 B1 s (resolve b)+  binTransitions ws s0+  binTransitions xs s1+  binTransitions ys s2+  Dfsa.transition B0 B1 s3 s3+++