packages feed

fsmActions 0.3.0 → 0.4.0

raw patch · 11 files changed

+595/−92 lines, 11 filesdep +MissingHdep +filepathdep ~graphvizPVP ok

version bump matches the API change (PVP)

Dependencies added: MissingH, filepath

Dependency ranges changed: graphviz

API changes (from Hackage documentation)

- Data.FsmActions.ActionMatrix: parseActionMxFile :: FilePath -> IO Action
- Data.FsmActions.Error: MxError :: String -> String -> MxError
- Data.FsmActions.Error: data MxError
- Data.FsmActions.Error: instance Eq MxError
- Data.FsmActions.Error: instance Error MxError
- Data.FsmActions.Error: instance Show MxError
- Data.FsmActions.Error: type ReadMxMonad = Either MxError
- Data.FsmActions.FsmMatrix: parseFsmMxFile :: FilePath -> IO (FSM String)
+ Data.FsmActions.ActionMatrix: loadActionMxFsm :: FilePath -> IO (FSM String)
+ Data.FsmActions.ActionMatrix: parseFsmActionMxs :: (Ord sy, Show sy) => [(sy, String)] -> ReadFsmMonad (FSM sy)
+ Data.FsmActions.ActionMatrix: printFsmActionMx :: FSM String -> [(String, FilePath)] -> (String, [(FilePath, String)])
+ Data.FsmActions.ActionMatrix: saveActionMxFsm :: FSM String -> FilePath -> [(String, FilePath)] -> IO ()
+ Data.FsmActions.Error: FsmError :: String -> String -> FsmError
+ Data.FsmActions.Error: data FsmError
+ Data.FsmActions.Error: instance Eq FsmError
+ Data.FsmActions.Error: instance Error FsmError
+ Data.FsmActions.Error: instance Exception FsmError
+ Data.FsmActions.Error: instance Show FsmError
+ Data.FsmActions.Error: instance Typeable FsmError
+ Data.FsmActions.Error: type ReadFsmMonad = Either FsmError
+ Data.FsmActions.FsmEdges: instance Eq Edge
+ Data.FsmActions.FsmEdges: instance Show Edge
+ Data.FsmActions.FsmEdges: loadFsmEdges :: FilePath -> IO (FSM String)
+ Data.FsmActions.FsmEdges: parseFsmEdges :: String -> ReadFsmMonad (FSM String)
+ Data.FsmActions.FsmEdges: printFsmEdges :: FSM String -> String
+ Data.FsmActions.FsmEdges: saveFsmEdges :: FSM String -> FilePath -> IO ()
+ Data.FsmActions.FsmMatrix: loadFsmMx :: FilePath -> IO (FSM String)
+ Data.FsmActions.FsmMatrix: saveFsmMx :: FSM String -> FilePath -> IO ()
+ Data.FsmActions.Graph: cleanShow :: (CleanShow a) => a -> String
+ Data.FsmActions.Graph: fglDot :: (Ord b, CleanShow b, Graph gr) => gr a b -> DotGraph Int
+ Data.FsmActions.Graph: fglToFsm :: (Graph gr, Ord sy, Show sy) => gr a sy -> ReadFsmMonad (FSM sy, [LNode a])
+ Data.FsmActions.Graph: instance [overlap ok] CleanShow ()
+ Data.FsmActions.IO: FsmActionMatrices :: FsmIO
+ Data.FsmActions.IO: FsmEdges :: FsmIO
+ Data.FsmActions.IO: FsmMatrix :: FsmIO
+ Data.FsmActions.IO: data FsmIO
+ Data.FsmActions.IO: fsmFormats :: FilePath -> [FsmIO]
+ Data.FsmActions.IO: instance Bounded FsmIO
+ Data.FsmActions.IO: instance Enum FsmIO
+ Data.FsmActions.IO: instance Eq FsmIO
+ Data.FsmActions.IO: instance Ord FsmIO
+ Data.FsmActions.IO: instance Show FsmIO
+ Data.FsmActions.IO: loadFsm :: FilePath -> [FsmIO] -> IO (Either FsmError (FSM String))
+ Data.FsmActions.IO: saveFsm :: FSM String -> FilePath -> Maybe FsmIO -> IO ()
+ Data.FsmActions.WellFormed: polishFSM :: (Ord sy, Show sy) => FSM sy -> ReadFsmMonad (FSM sy)
- Data.FsmActions.ActionMatrix: parseActionMx :: String -> ReadMxMonad Action
+ Data.FsmActions.ActionMatrix: parseActionMx :: String -> ReadFsmMonad Action
- Data.FsmActions.ActionMatrix: parseFsmActionMxFiles :: (Ord sy) => [(sy, FilePath)] -> IO (FSM sy)
+ Data.FsmActions.ActionMatrix: parseFsmActionMxFiles :: (Ord sy, Show sy) => [(sy, FilePath)] -> IO (FSM sy)
- Data.FsmActions.Error: msg :: MxError -> String
+ Data.FsmActions.Error: msg :: FsmError -> String
- Data.FsmActions.Error: value :: MxError -> String
+ Data.FsmActions.Error: value :: FsmError -> String
- Data.FsmActions.FsmMatrix: parseFsmMx :: String -> ReadMxMonad (FSM String)
+ Data.FsmActions.FsmMatrix: parseFsmMx :: String -> ReadFsmMonad (FSM String)
- Data.FsmActions.Graph: fsmToDot :: (Ord sy, CleanShow sy) => FSM sy -> DotGraph
+ Data.FsmActions.Graph: fsmToDot :: (Ord sy, CleanShow sy) => FSM sy -> DotGraph Int

Files

Data/FsmActions.hs view
@@ -141,7 +141,7 @@ -- | Compute the 'DestinationSet' reached by following some 'Action' -- from some 'State'. actionLookup :: Action -> State -> DestinationSet-actionLookup (Action ds) src = ds !! src+actionLookup (Action ds) = (ds !!)  -- | Compute the 'Action' for some 'Word' over some 'FSM'.  The word -- might contain symbols outside the FSM's alphabet, so the result
Data/FsmActions/ActionMatrix.hs view
@@ -11,27 +11,50 @@ than one 1, the corresponding 'Data.FsmActions.Action' and 'Data.FsmActions.FSM' will be nondeterministic. +An ActionSpecFile is a list of (symbol string, path to ActionMatrix+file) pairs.  Its syntax is as follows:++  - Symbols, and paths, should all be enclosed in double quotes (with+    the nice side-effect that spaces are thus allowed).++  - A symbol/path pair is separated by whitespace.++  - The list of symbol/path pairs is delimited by semicolons (and+    optional whitespace).  A trailing semicolon is optional.++  - Line comments, starting with -- (as in Haskell), are allowed+    anywhere whitespace is allowed.+ -}  module Data.FsmActions.ActionMatrix (-    -- * Input+    -- * I/O+    loadActionMxFsm,+    saveActionMxFsm,+    -- * Parsing     parseFsmActionMxFiles,-    parseActionMxFile,+    parseFsmActionMxs,     parseActionMx,-    -- * Output+    -- * Pretty-printing+    printFsmActionMx,     printActionMx ) where +import Control.Arrow (second)+import Control.Exception import Control.Monad.Error import qualified Data.List as L+import Data.String.Utils import Data.Maybe (mapMaybe)-import System.IO.Error (mkIOError, userErrorType)+import System.FilePath import qualified Text.ParserCombinators.Parsec as P+import qualified Text.ParserCombinators.Parsec.Language as L+import qualified Text.ParserCombinators.Parsec.Token as T import Text.PrettyPrint.HughesPJ  import Data.FsmActions import Data.FsmActions.Error-+import Data.FsmActions.WellFormed  -- | This module's internal represenation of adjacency matrices is as -- nested lists of booleans.  These are only ever used as intermediate@@ -43,38 +66,86 @@ type ActionMatrix = [ActionMatrixRow] type ActionMatrixRow = [Bool] --- | Given a list of (symbol, path) pairs, compute an+-- | Load an 'Data.FsmActions.FSM' from action matrices, given a path+-- to an ActionSpec file.+loadActionMxFsm :: FilePath -> IO (FSM String)+loadActionMxFsm specPath =+    do contents <- readFile specPath+       let result = parseFsmActionSpec contents+       case result of+         Right actionSpecs -> parseFsmActionMxFiles (addDir actionSpecs)+             where addDir = map (second (replaceFileName specPath))+         Left e -> throw $ FsmError (show e) specPath++-- | Save an 'Data.FsmActions.FSM' to an ActionSpec file (whose path+-- is specified) and a set of action matrices (whose paths may be+-- optionally specified using a (label, path) association list).+saveActionMxFsm :: FSM String -> FilePath -> [(String, FilePath)] -> IO ()+saveActionMxFsm fsm spath labelPaths =+    do let (spec, mxs) = printFsmActionMx fsm labelPaths+       writeFile spath spec+       mapM_ (\(p, m) -> writeFile (combine (takeDirectory spath) p) m) mxs++++-- | Parse an fsmActionSpec string to a (label, ActionMatrix path)+-- association list.+parseFsmActionSpec :: String -> ReadFsmMonad [(String, FilePath)]+parseFsmActionSpec actionSpecString =+    case P.parse fsmActionSpecParser "" actionSpecString of+      Right actionSpecs -> return actionSpecs+      Left err -> throw $ FsmError "FSM action specs parse error" (show err)++-- | Parser for fsmActionSpec strings.+fsmActionSpecParser :: P.Parser [(String, FilePath)]+fsmActionSpecParser = P.sepEndBy1 lineParser (T.semi l)+    where lineParser :: P.Parser (String, FilePath)+          lineParser = do T.whiteSpace l+                          label <- T.stringLiteral l+                          mxPath <- T.stringLiteral l+                          return (label, mxPath)+          l :: T.TokenParser st+          l = T.makeTokenParser L.emptyDef {+                L.commentLine = "--"+              }++-- | Given a (symbol, path) association list, compute an -- 'Data.FsmActions.FSM' whose actions are read from action matrices--- in the specified paths, associated with their corresponding+-- in the specified paths, and associated with their corresponding -- symbols.+parseFsmActionMxFiles :: (Ord sy, Show sy) => [(sy, FilePath)] -> IO (FSM sy)+parseFsmActionMxFiles xs =+    do files <- readActionMxFiles xs+       let result = parseFsmActionMxs files+       case result of+         Right fsm -> return fsm+         Left e -> throw $ FsmError "ActionMatrix parse error" (show e)+    where readActionMxFiles :: Ord sy => [(sy, FilePath)] -> IO [(sy, String)]+          readActionMxFiles = mapM (liftMSnd readFile)++liftMSnd :: Monad m => (a -> m b) -> (c, a) -> m (c, b)+liftMSnd f (x, y) = f y >>= \z -> return (x, z)++-- | Given a (symbol, ActionMatrix string) association list, parse the+-- strings and construct an FSM.  Includes normalisation and+-- well-formedness checks.  Parse errors in individual action strings+-- result in an error here (ReadFsmMonad is in the Either monad). -- -- Note that if the same symbol appears multiple times, only one -- instance will appear in the 'Data.FsmActions.FSM'; the choice of which -- appears is not defined.-parseFsmActionMxFiles :: Ord sy => [(sy, FilePath)] -> IO (FSM sy)-parseFsmActionMxFiles xs =-    liftM fromList $ mapM (liftMSnd parseActionMxFile) xs-        where liftMSnd :: Monad m => (a -> m b) -> (c, a) -> m (c, b)-              liftMSnd f (x, y) = f y >>= \z -> return (x, z)---- | Read an action matrix from a specified file, and parse it into an--- 'Data.FsmActions.Action'.-parseActionMxFile :: FilePath -> IO Action-parseActionMxFile path =-    do contents <- readFile path-       let act = parseActionMx contents-       case act of-         Right a -> return a-         Left e -> throwError (mkIOError userErrorType (show e)-                                         Nothing (Just path))+parseFsmActionMxs :: (Ord sy, Show sy) => [(sy, String)]+                        -> ReadFsmMonad (FSM sy)+parseFsmActionMxs xs =+    liftM fromList (mapM (liftMSnd parseActionMx) xs) >>= polishFSM  -- | Parse an action matrix string, and turn it into an -- 'Data.FsmActions.Action'.-parseActionMx :: String -> ReadMxMonad Action+parseActionMx :: String -> ReadFsmMonad Action parseActionMx actionString =     case P.parse actionMxParser "" actionString of       Right mx -> interpretActionMx mx-      Left err -> throwError (MxError "Action matrix parse error" (show err))+      Left err -> throw $ FsmError "Action matrix parse error" (show err)  -- | Parse an action matrix from a string. --@@ -97,11 +168,11 @@  -- | Given an 'ActionMatrix', compute the corresponding -- 'Data.FsmActions.Action'.-interpretActionMx :: ActionMatrix -> ReadMxMonad Action+interpretActionMx :: ActionMatrix -> ReadFsmMonad Action interpretActionMx rows =     if all (== length transitions) rowLengths -- check matrix is square     then return $ normaliseAction $ mkAction transitions-    else throwError (MxError "action matrix is not square (see row lengths)"+    else throwError (FsmError "action matrix is not square (see row lengths)"                              (show rowLengths))     where transitions = L.map parseActionMxRow rows           rowLengths = L.map length rows@@ -114,6 +185,55 @@           withIdxs ys = zip ys [0..(length ys-1)]           -- | Iff the cell is set, include its index..           isSet (cell, index) = if cell then Just index else Nothing++++-- | Pretty-print a string FSM into an ActionSpec string and an+-- (ActionMatrix path, ActionMatrix string) association list.  (The+-- paths will be interpreted relative to the ActionSpec's location.)+-- Filenames (per action label) may be specified by providing a+-- (label, path) association list; whenever a lookup in that list+-- fails, a default is computed from the label.+printFsmActionMx :: FSM String -> [(String, FilePath)] ->+                    (String, [(FilePath, String)])+printFsmActionMx fsm paths = (spec, matrices)+    where spec = show $ ppActionSpec $ map (\(l, p, _) -> (l, p)) actionData+          matrices = map (\(_, p, m) -> (p, m)) actionData+          actionData :: [(String, FilePath, String)]+          actionData = fsmMap (getActionData paths) fsm+          -- Given a (label, file path) association list, a label, and+          -- an action, compute a triple consisting of the specified+          -- label, the file path for that label's action matrix, and+          -- the action matrix as a string.  If label lookup fails,+          -- compute a default path from the label.+          getActionData pd l a = (l, mxPath, printActionMx a)+              where mxPath = case L.lookup l pd of+                               Just x -> x+                               Nothing -> defaultPath l++-- | Compute the default path for a symbol; we take the symbol,+-- replace various undesirable characters with underscores, and append+-- ".actionmx".+defaultPath :: String -> FilePath+defaultPath s = addExtension clean ".actionmx"+    where -- There's got to be a nicer way to do this!?+          clean = foldr ($) s (map (flip replace "_") undesirable)+          -- This is slightly cock-eyed: we define a string of+          -- undesirable characters, each of which will be replaced+          -- with an underscore.  However, the replace function we're+          -- using (from the MissingH package), takes a _string_ to+          -- replace, not a character, so we then turn each of those+          -- characters into a single-character string.+          undesirable :: [String]+          undesirable = map (: []) " \t\n\f\r\'\"\\&:/"++-- Pretty printer to ActionSpec format.  Takes a (label, path)+-- association list.+ppActionSpec :: [(String, FilePath)] -> Doc+ppActionSpec = vcat . map (uncurry ppOnePair)+    where ppOnePair :: String -> FilePath -> Doc+          ppOnePair symbol path = quoth symbol <+> quoth path <+> semi+              where quoth = doubleQuotes . text  -- | Pretty-print an action in action matrix format. printActionMx :: Action -> String
Data/FsmActions/Error.hs view
@@ -7,27 +7,33 @@ -- Copyright (c) 2009 Andy Gimblett - http://www.cs.swan.ac.uk/~csandy/ -- BSD Licence (see http://www.opensource.org/licenses/bsd-license.php) +{-# LANGUAGE DeriveDataTypeable #-}+ module Data.FsmActions.Error (-  MxError(..),-  ReadMxMonad+  FsmError(..),+  ReadFsmMonad ) where +import Control.Exception import Control.Monad.Error+import Data.Typeable  -- | Errors when reading matrices from strings.-data MxError = MxError { +data FsmError = FsmError {        -- | Explanatory message       msg :: String,       -- | Offending value       value :: String-    } deriving (Eq)+    } deriving (Eq, Typeable) -instance Error MxError where-    noMsg    = MxError "Matrix error" ""-    strMsg s = MxError s ""+instance Exception FsmError -instance Show MxError where-    show (MxError message val) = message ++ " (" ++ val ++ ")"+instance Error FsmError where+    noMsg    = FsmError "FSM error" ""+    strMsg s = FsmError s "" --- | Error monad for reading matrices from strings.-type ReadMxMonad = Either MxError+instance Show FsmError where+    show (FsmError message val) = message ++ " (" ++ val ++ ")"++-- | Error monad for reading FSMs in.+type ReadFsmMonad = Either FsmError
+ Data/FsmActions/FsmEdges.hs view
@@ -0,0 +1,110 @@+{- |++Serialisation/deserialisation of 'Data.FsmActions.FSM's in edge list+format.++An 'Data.FsmActions.FSM' may be represented textually as list of+{source_state->destination_state,label} strings, each of which+represents an edge in its directed graph.  (This representation is+interesting because it's used by Mathematica for graph I/O.)++-}++module Data.FsmActions.FsmEdges (+    -- * I/O+    loadFsmEdges,+    saveFsmEdges,+    -- * Parsing+    parseFsmEdges,+    -- * Pretty-printing+    printFsmEdges+) where++import Control.Exception+import Control.Monad.Error+import qualified Data.Graph.Inductive.Graph as G+import Data.Graph.Inductive.Tree (Gr)+import Data.List+import qualified Text.ParserCombinators.Parsec as P+import qualified Text.ParserCombinators.Parsec.Language as L+import qualified Text.ParserCombinators.Parsec.Token as T+import Text.PrettyPrint.HughesPJ++import Data.FsmActions+import Data.FsmActions.Error+import Data.FsmActions.Graph++-- The intermediate result of parsing will be a list of edges.+data Edge = Edge Integer Integer String+            deriving (Eq, Show)++-- | Load an 'Data.FsmActions.FSM' from an FsmEdges file.+loadFsmEdges :: FilePath -> IO (FSM String)+loadFsmEdges path =+    do contents <- readFile path+       let result = parseFsmEdges contents+       case result of+         Right fsm -> return fsm+         Left e -> throw $ FsmError (show e) path++-- | Save an 'Data.FsmActions.FSM' to an FsmMatrix file.+saveFsmEdges :: FSM String -> FilePath -> IO ()+saveFsmEdges fsm mxPath = do let mx = printFsmEdges fsm+                             writeFile mxPath mx++++-- | Parse an FsmEdges-formatted FSM held in a string.  Includes+-- normalisation and well-formedness checks.+parseFsmEdges :: String -> ReadFsmMonad (FSM String)+parseFsmEdges fsmString =+    case P.parse fsmEdgesParser "" fsmString of+      Right edges -> do (fsm, _) <- (fglToFsm . edgesToFGL) edges+                        return fsm+      Left err -> throw $ FsmError "FSM edges parse error" (show err)++-- FsmEdges-format parser.+fsmEdgesParser :: P.Parser [Edge]+fsmEdgesParser = T.braces l $ T.commaSep l fsmEdgeParser+    where fsmEdgeParser :: P.Parser Edge+          fsmEdgeParser = T.braces l $ do source <- T.natural l+                                          T.symbol l "->"+                                          destination <- T.natural l+                                          T.comma l+                                          label <- T.identifier l+                                          return $ Edge source destination label+          l :: T.TokenParser st+          l = T.makeTokenParser L.emptyDef++-- Turn a list of edges into an FGL graph, inferring the node list+-- from those mentioned in the edges.+edgesToFGL :: [Edge] -> Gr () String+edgesToFGL edges = G.mkGraph gNodes gEdges+    where gEdges = map fst extracted+          gNodes = sort $ nub $ concatMap snd extracted+          extracted = map edgeExtract edges+          -- Turn an FsmEdges Edge into an FGL (LEdge, [LNodes]) pair.+          edgeExtract :: Edge -> (G.LEdge String, [G.LNode ()])+          edgeExtract (Edge s' d' l) = ((s, d, l), [(s, ()), (d, ())])+              where s = fromInteger s'+                    d = fromInteger d'++++-- | Pretty-print a string FSM in FsmMatrix format.+printFsmEdges :: FSM String -> String+printFsmEdges = show . ppFsmEdges++-- Pretty printer to FsmEdges format (building Doc not String).+ppFsmEdges :: FSM String -> Doc+ppFsmEdges fsm = ppFsmEdges' $ map tweakEdge $ G.labEdges $ fsmToFGL fsm Trim+    where -- Convert fgl-format Edge to the ones used by this module.+          tweakEdge :: CleanShow sy => G.LEdge sy -> Edge+          tweakEdge (s, d, l) = Edge (toInteger s) (toInteger d) (cleanShow l)+          ppFsmEdges' :: [Edge] -> Doc+          ppFsmEdges' = braces . vcat . punctuate comma . map ppFsmEdge+          ppFsmEdge :: Edge -> Doc+          ppFsmEdge (Edge src dest label) =+              braces $ integer src <> text "->" <>+                       integer dest <> comma <>+                       text label
Data/FsmActions/FsmMatrix.hs view
@@ -15,17 +15,19 @@ -}  module Data.FsmActions.FsmMatrix (-    -- * Input-    parseFsmMxFile,+    -- * I/O+    loadFsmMx,+    saveFsmMx,+    -- * Parsing     parseFsmMx,-    -- * Output+    -- * Pretty-printing     printFsmMx ) where +import Control.Exception import Control.Monad.Error import Data.Char (isSpace) import qualified Data.List as L-import System.IO.Error (mkIOError, userErrorType) import qualified Text.ParserCombinators.Parsec as P import Text.PrettyPrint.HughesPJ @@ -33,36 +35,29 @@ import Data.FsmActions.Error import Data.FsmActions.WellFormed --- | Parse an FsmMatrix-formatted FSM held in a file, by reading the--- file and calling 'parseFsmString'.-parseFsmMxFile :: FilePath -> IO (FSM String)-parseFsmMxFile path =+-- | Load an 'Data.FsmActions.FSM' from an FsmMatrix file.+loadFsmMx :: FilePath -> IO (FSM String)+loadFsmMx path =     do contents <- readFile path        let result = parseFsmMx contents        case result of          Right fsm -> return fsm-         Left e -> throwError (mkIOError userErrorType (show e)-                               Nothing (Just path))+         Left e -> throw $ FsmError (show e) path +-- | Save an 'Data.FsmActions.FSM' to an FsmMatrix file.+saveFsmMx :: FSM String -> FilePath -> IO ()+saveFsmMx fsm mxPath = do let mx = printFsmMx fsm+                          writeFile mxPath mx+++ -- | Parse an FsmMatrix-formatted FSM held in a string.  Includes -- normalisation and well-formedness checks.-parseFsmMx :: String -> ReadMxMonad (FSM String)+parseFsmMx :: String -> ReadFsmMonad (FSM String) parseFsmMx fsmString =     case P.parse fsmMatrixParser "" fsmString of-      Right parts ->-          do fsm <- interpretFsmMx parts-             case (isWellFormed fsm) of-               WellFormed -> return fsm-               Disconnected wccs ->-                   throwError (MxError "FSM disconnected" (show wccs))-               err -> throwError (MxError "FSM matrix ill-formed" (show err))-      Left err ->-          throwError (MxError "FSM matrix parse error" (show err))---- TODO: there are well-formedness checks here, but not when reading--- in from action matrices.  Generalise!  Either remove the checks--- here, or factor them out into an handy "run this after input"--- function.+      Right parts -> interpretFsmMx parts >>= polishFSM+      Left err -> throw $ FsmError "FSM matrix parse error" (show err)  -- FsmMatrix-format parser. fsmMatrixParser :: P.Parser ([String], [[[Int]]])@@ -89,14 +84,16 @@           nonEOLSpace = P.many1 (P.satisfy (\c -> isSpace c && c /= '\n'))  -- Turn some FsmMatrix-formatted data into an (normalised) FSM.-interpretFsmMx :: ([String], [[[Int]]]) -> ReadMxMonad (FSM String)+interpretFsmMx :: ([String], [[[Int]]]) -> ReadFsmMonad (FSM String) interpretFsmMx (actionNames, stateLines) =      if all (== (length actionNames)) lineLengths       then return $ normalise $ fromList $ zip actionNames actions-      else throwError (MxError "FSM matrix ill-formed" (show lineLengths))+      else throwError (FsmError "FSM matrix ill-formed" (show lineLengths))     where actions = map mkAction $ L.transpose stateLines           lineLengths = L.map length stateLines ++ -- | Pretty-print a string FSM in FsmMatrix format. printFsmMx :: FSM String -> String printFsmMx = show . ppFsmMx@@ -125,4 +122,3 @@           commas [] = empty           commas (x:[]) = x           commas (x:xs) = x <> comma <> commas xs-
Data/FsmActions/Graph.hs view
@@ -1,10 +1,10 @@ {- | -Generating and drawing graphs of FSMs.+Generating, interpreting, and drawing graphs of FSMs.  Includes: -  - Interface to fgl graph library+  - Interface to fgl graph library for graph input/output     (<http://hackage.haskell.org/package/fgl>).    - Interface to graphviz library for dot output@@ -28,27 +28,40 @@     -- * FGL graph operations.     SelfLoops(..),     fsmToFGL,+    fglDot, -- XXX     strongCCs,     weakCCs,     -- * Dot and GML format output.     CleanShow,+    cleanShow,     fsmToDot,-    fsmToGML+    fsmToGML,+    -- * Input.+    fglToFsm ) where  import Data.Graph.Inductive.Basic (undir)-import Data.Graph.Inductive.Graph (Graph, labEdges, mkGraph)+import Data.Graph.Inductive.Graph (Graph, labEdges, labNodes, Node,+                                   LNode, mkGraph) import qualified Data.Graph.Inductive.PatriciaTree as P import qualified Data.Graph.Inductive.Tree as T  import Data.Graph.Inductive.Query.DFS (scc) import Data.GraphViz+import Data.List (foldr, nub, sort)+import qualified Data.Map as M+import Data.Map.Utils (forceLookupM) import Text.PrettyPrint.HughesPJ -import Data.FsmActions+import Data.FsmActions hiding (mkAction)+import Data.FsmActions.Error+import Data.FsmActions.WellFormed (polishFSM)  -- | When converting an 'Data.FsmActions.FSM' into a graph, do we keep -- all self-loops, or only those which are sources of nondeterminism?-data SelfLoops = Keep | Trim+data SelfLoops = -- | Keep them all+                 Keep+                 -- | Trim any which aren't nondeterminism sources.+               | Trim  -- | Turn an FSM into an fgl graph with labelled edges. fsmToFGL :: FSM sy -> SelfLoops -> T.Gr () sy@@ -116,6 +129,8 @@     cleanShow :: a -> String     cleanShow = show -- by default, turn it to a String instance (Show a) => CleanShow a+instance CleanShow () where+    cleanShow _ = "" instance CleanShow String where     cleanShow = id -- don't need to do anything for a String instance CleanShow Char where@@ -125,12 +140,12 @@  -- | Turn an FSM into a 'Data.GraphViz.DotGraph', trimming any -- self-loops which aren't sources of nondeterminism.-fsmToDot :: (Ord sy, CleanShow sy) => FSM sy -> DotGraph+fsmToDot :: (Ord sy, CleanShow sy) => FSM sy -> DotGraph Int fsmToDot = fglDot . flip fsmToFGL Trim  -- Turn an FGL into a DotGraph with labelled edges.-fglDot :: (Ord b, CleanShow b, Graph gr) => gr a b -> DotGraph-fglDot g = graphToDot g [] nodeFn edgeFn+fglDot :: (Ord b, CleanShow b, Graph gr) => gr a b -> DotGraph Int+fglDot g = graphToDot True g [] nodeFn edgeFn     where nodeFn _ = []           edgeFn (_, _, label) = [Label $ StrLabel $ cleanShow label] @@ -160,3 +175,107 @@         text "target" <+> text (show dest),         text "label" <+> doubleQuotes (text $ cleanShow label)     ])++++-- And now for some input.++-- | Turn an FGL graph (interpreted as being a directed graph) into an+-- FSM.  Self-loops are inserted as required.  Also returns a list of+-- the graph's labelled nodes, since the labels are discarded by the+-- FSM construction.  FSM states are numbered [0..] and thus may be+-- used as an index into that list of labelled nodes, in order to+-- relate FSM states back to the original graph nodes and their+-- labels.+fglToFsm :: (Graph gr, Ord sy, Show sy) => gr a sy ->+            ReadFsmMonad (FSM sy, [LNode a])+fglToFsm g = do let (nodes, actions) = graphActions g+                let fsm = fromList actions+                p <- polishFSM fsm+                return (p, nodes)++-- | Turn a graph into a list of its labelled nodes (indexed by+-- corresponding FSM state) and a list of (symbol,+-- 'Data.FsmActions.Action') pairs.+graphActions :: (Graph gr, Ord b) => gr a b -> ([LNode a], [(b, Action)])+graphActions g = (nodeList, actions)+    where nodeList = labNodes g+          actions = mkActions destsMap nodeMap srcStates actionSymbols+          srcStates = map fst nodeList+          actionSymbols = symbols g+          destsMap = mkDestsMap g+          nodeMap = mkNodeMap g++mkActions :: Ord sy => DestsMap sy -> NodeMap -> [Node] -> [sy] ->+             [(sy, Action)]+mkActions destsMap nodeMap srcs ls =+    map (mkAction destsMap nodeMap srcs) ls++mkAction :: Ord sy => DestsMap sy -> NodeMap -> [Node] -> sy ->+            (sy, Action)+mkAction destsMap nodeMap srcs l =+    (l, Action $ map (mkDestSet destsMap nodeMap l) srcs)++mkDestSet :: Ord b => DestsMap b -> NodeMap -> b -> Node ->+             DestinationSet+mkDestSet destsMap nodeMap l src =+    DestinationSet $ map (nodeToState nodeMap) destNodes+        where destNodes = M.findWithDefault [src] (l, src) destsMap++-- | Given a labelled directed graph, compute the list of its unique+-- labels, which will be the corresponding FSM's symbols.+symbols :: (Graph gr, Ord sy) => gr a sy -> [sy]+symbols = sort . nub . map (\(_,_,s) -> s) . labEdges++-- | A DestsMap maps (action symbol, source graph node) pairs to+-- [destination graph node] lists, which provide a handy lookup when+-- building 'Data.FsmActions.DestinationSet's.+type DestsMap sy = M.Map (sy, Node) [Node]++-- | Build a DestsMap from a graph.+mkDestsMap :: (Graph gr, Ord b) => gr a b -> DestsMap b+mkDestsMap = foldr insertEdge M.empty . labEdges+    where insertEdge (s, d, l) = M.insertWith (++) (l, s) [d]++-- | A NodeMap is a map from graph node numbers to FSM state numbers,+-- the inverse of the indexing produced by labNodes, ie+-- forall n . (fst (labNodes g !! n)) `M.lookup` nodeMap g == n+type NodeMap = M.Map Node State++-- | Construct a NodeMap.+mkNodeMap :: Graph gr => gr a b -> NodeMap+mkNodeMap = M.fromList . map flipPair . zipWithIndex . map fst . labNodes+    where flipPair (a,b) = (b,a)++-- | Perform a node -> state lookup in a NodeMap; throws an exception+-- if it fails, which it shouldn't ever here.+nodeToState :: NodeMap -> Node -> State+nodeToState nodeMap node = forceLookupM err node nodeMap+    where err = "Node -> State lookup failure (can't happen?)"++++-- Examples for hacking.++{-+love :: T.Gr Char String+love = mkGraph nodes edges+    where nodes = [(1, 'x'), (3, 'y'), (5, 'z')]+          edges = [(1,3,"a"), (3,5,"a"), (5,3,"a"),+                   (1,1,"b"), (3,3,"b"), (5,1,"b"),+                   (1,1,"c"), (3,3,"c"), (5,1,"c")+                  ]++mooKid :: T.Gr () String+mooKid = mkGraph (map mkNode nodes) edges+    where mkNode x = (x, ())+          nodes = [0,1,2]+          edges = [(0,0,"a")+                  ,(0,1,"a")+                  ,(0,2,"c")+                  ,(1,1,"b")+                  ,(1,0,"a")+                  ,(2,0,"c")+                  ]++-}
+ Data/FsmActions/Graph.hs-boot view
@@ -0,0 +1,14 @@+-- | There's a module import cycle between Graph and WellFormed,+-- because WellFormed wants to be able to check if an FSM is+-- weakly-connected, and Graph wants to perform well-formedness checks+-- when converting a graph to an FSM.  We break the cycle here, by+-- being explicit about the first case (since its the smaller of the+-- two).++module Data.FsmActions.Graph (+    weakCCs+) where++import Data.FsmActions++weakCCs :: Eq sy => FSM sy -> [[State]]
+ Data/FsmActions/IO.hs view
@@ -0,0 +1,120 @@+{- |++High-level input/output interface for finite state machines.++This module allows one to load and save FSMs, where the format to be+used may be either explicitly specified, or guessed according to the+filename's extension.++-}++-- Copyright (c) 2009 Andy Gimblett - http://www.cs.swan.ac.uk/~csandy/+-- BSD Licence (see http://www.opensource.org/licenses/bsd-license.php)++module Data.FsmActions.IO (+    FsmIO(..),+    fsmFormats,+    loadFsm,+    saveFsm+) where++import Control.Exception+import Data.Char (toLower)+import Data.Either+import Data.List+import qualified Data.Map as M+import qualified Data.List as L+import Data.Maybe (mapMaybe)+import System.FilePath++import Data.FsmActions+import Data.FsmActions.ActionMatrix+import Data.FsmActions.Error+import Data.FsmActions.FsmEdges+import Data.FsmActions.FsmMatrix++-- | Known FSM I/O formats.+data FsmIO =+           -- | ActionMatrix format: use+           -- 'Data.FsmActions.ActionMatrix.loadActionMxFsm' and+           -- 'Data.FsmActions.ActionMatrix.saveActionMxFsm'; filename+           -- extensions: actions, actionspec, actionmxs,+           -- actionmatrices, fsmactions.+             FsmActionMatrices+           -- | FsmEdges format: use+           -- 'Data.FsmActions.FsmEdges.loadFsmEdges' and+           -- 'Data.FsmActions.FsmEdges.saveFsmEdges'; filename+           -- extensions: edges, fsmedges, graph, mathematica.+           | FsmEdges+           -- | FsmMatrix format: use+           -- 'Data.FsmActions.FsmMatrix.loadFsmMx' and+           -- 'Data.FsmActions.FsmMatrix.saveFsmMx'; filename+           -- extensions: mx, matrix, fsmmx, fsmmatrix, fsm.+           | FsmMatrix+    deriving (Bounded, Enum, Eq, Ord, Show)++-- | Mapping from 'FsmIO' formats to lists of expected filename+-- extensions for those formats.+fsmIOExts :: M.Map FsmIO [String]+fsmIOExts = M.fromList [+       (FsmActionMatrices, ["actions", "actionspec", "actionmxs",+                            "actionmatrices", "fsmactions",+                            "fsmactionmxs"])+      ,(FsmEdges, ["edges", "fsmedges", "graph", "mathematica"])++      ,(FsmMatrix, ["mx", "matrix", "fsmmx", "fsmmatrix", "fsm"])+      ]++-- | Given a path, return a list of all 'FsmIO' formats, with guesses+-- (according to the file extension) at the front.+fsmFormats :: FilePath -> [FsmIO]+fsmFormats path = guesses ++ ([minBound..maxBound] \\ guesses)+    where guesses = seek `rLookup` fsmIOExts+          (_, ext) = splitExtension path+          seek = L.map toLower (tail ext)++-- | Reverse lookup in map from keys to lists of values.+rLookup :: (Ord k, Eq v) => v -> M.Map k [v] -> [k]+rLookup val m = mapMaybe (uncurry seek) $ M.toList m+    where seek key vs = if val `elem` vs then Just key else Nothing++-- | Read an 'Data.FsmActions.FSM' from a file.  If the user specifies+-- any 'FsmIO' formats, try each of those in turn; otherwise, try+-- every format known, using the filename extension to guess which to+-- try first.+--+-- The returned value is either the resultant 'Data.FsmActions.FSM',+-- or the error message produced by trying to load it with the _first_+-- format (so in the case of guessing formats, if the guess is wrong+-- and the file is corrupt, you might get an unhelpful error message).+loadFsm :: FilePath -> [FsmIO] -> IO (Either FsmError (FSM String))+loadFsm p formats' =+    do results <- mapM (uncurry tryLoadFsm) $ zip (repeat p) formats+       let success = rights results+       case success of+         (fsm:_) -> return $ Right fsm+         _ -> return $ Left $ head $ lefts results+    where formats = if L.null formats' then fsmFormats p+                                       else formats'++-- | Try loading an FSM from a file with a particular format.+tryLoadFsm :: FilePath -> FsmIO -> IO (Either FsmError (FSM String))+tryLoadFsm p f = try (case f of+                        FsmActionMatrices -> loadActionMxFsm p+                        FsmEdges -> loadFsmEdges p+                        FsmMatrix -> loadFsmMx p)++-- | Save an 'Data.FsmActions.FSM' to a file.  If the user specifies+-- an 'FsmIO' format, it is used; otherwise, it is guessed from the+-- filename extension (and failing that, the first guess, ie+-- 'FsmActionMatrices', is used).+saveFsm :: FSM String -> FilePath -> Maybe FsmIO -> IO ()+saveFsm fsm p format' =+    case format of+      FsmActionMatrices -> saveActionMxFsm fsm p []+      FsmEdges -> saveFsmEdges fsm p+      FsmMatrix -> saveFsmMx fsm p+    where format = case format' of+                     Just f -> f+                     -- None specified: use first guess.+                     Nothing -> head $ fsmFormats p
Data/FsmActions/WellFormed.hs view
@@ -10,18 +10,16 @@ module Data.FsmActions.WellFormed (     WellFormed(..),     isWellFormed,+    polishFSM ) where -{---- We use a PatriciaTree because we care about speed, and it doesn't--- matter if duplicate edges are lost when checked for SCCs.-import Data.Graph.Inductive.PatriciaTree (Gr)--}+import Control.Monad.Error import qualified Data.Map as M import qualified Data.List as L  import Data.FsmActions-import Data.FsmActions.Graph+import Data.FsmActions.Error+import {-# SOURCE #-} Data.FsmActions.Graph (weakCCs)  -- | An 'FSM' is well-formed if all its actions are the same length, -- none of its actions contain destinations which are out of range,@@ -67,6 +65,23 @@           -- Compute the FSM's undirected strongly-connected           -- components.           wccs = weakCCs fsm++-- | Given an FSM, normalise it and check it's well-formed.+--+-- This should be called whenever an FSM is read/computed from an+-- outside source.  If parsing, the right time to call this is+-- immediately after you've decided if the parse of the FSM was+-- successful or not.  (In other words, here are some static checks!)+polishFSM :: (Ord sy, Show sy) => FSM sy -> ReadFsmMonad (FSM sy)+polishFSM fsm =+    let norm = normalise fsm in+    case (isWellFormed norm) of+                WellFormed -> return norm+                Disconnected wccs ->+                    throwError (FsmError "FSM disconnected" (show wccs))+                err -> throwError (FsmError "FSM ill-formed" (show err))++  -- Check if every element of a list is identical. allSame :: Eq a => [a] -> Bool
doc/fsmActions.pdf view

binary file changed (143688 → 147536 bytes)

fsmActions.cabal view
@@ -1,5 +1,5 @@ Name:                   fsmActions-Version:                0.3.0+Version:                0.4.0 Stability:              Alpha Synopsis:               Finite state machines and FSM actions Description:@@ -25,12 +25,15 @@     Location:     http://code.haskell.org/fsmActions  Library-  Build-Depends: base >= 3 && < 5, containers, fgl, graphviz-                        >=2999.1.0.1, mtl, parsec, pretty-  Exposed-modules:      Data.FsmActions,-                        Data.FsmActions.ActionMatrix,-                        Data.FsmActions.Error,-                        Data.FsmActions.FsmMatrix,-                        Data.FsmActions.Graph,-                        Data.FsmActions.WellFormed-  ghc-options:          -fwarn-tabs -Wall+  Build-Depends:   base >= 3 && < 5, containers, filepath, fgl,+                   graphviz >= 2999.6.0.0, MissingH, mtl, parsec,+                   pretty+  Exposed-modules: Data.FsmActions,+                   Data.FsmActions.ActionMatrix,+                   Data.FsmActions.Error,+                   Data.FsmActions.FsmEdges,+                   Data.FsmActions.FsmMatrix,+                   Data.FsmActions.Graph,+                   Data.FsmActions.IO,+                   Data.FsmActions.WellFormed+  ghc-options:     -fwarn-tabs -Wall