packages feed

lattest-lib-0.1.0.0: src/Lattest/Util/ModelParsingUtils.hs

{-# LANGUAGE OverloadedStrings #-}

module Lattest.Util.ModelParsingUtils (readAutFile, dumpLTSdot) where

import Lattest.Model.Alphabet(IOAct(..))
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import qualified Data.Set as Set
import Data.Maybe (mapMaybe)

import Data.List (isSuffixOf)
import Data.Containers.ListUtils (nubOrd)

{-|
    Read an .aut file representing an LTS from the provided filepath, parse its content and return:
    - [String]: Input Alphabet
    - [String]: Output Alphabet
    - Set.Set String: Set of all LTS states
    - String: Initial state
    - Maybe [(String, IOAct String String, String)]: List of LTS transition tuples as (InitialState, Action, EndState)
    NOTE: In order to parse actions correctly, inputs and outputs must end in _i and _o respectively. The first line of the .aut file must follow the structure des (initState,nEdges,nStates).
-}
readAutFile :: FilePath -> IO ([String], [String], Set.Set String, String, Maybe [(String, IOAct String String, String)])
readAutFile path = do
    contents <- TIO.readFile path
    let linesT = T.lines contents
    case linesT of
      [] -> error $ "Error: empty file: " <> path
      firstLine : restLines ->
        case parseInitialState firstLine of
          Nothing -> error "Error: Could not parse initial state from header."
          Just initialState ->
            let parsed = mapMaybe parseTupleLine restLines
                inputAlphabet  = nubOrd [s | (_, In s, _)  <- parsed]
                outputAlphabet = nubOrd [s | (_, Out s, _) <- parsed]
                allStates = Set.fromList $
                            [s1 | (s1, _, _) <- parsed] ++
                            [s2 | (_, _, s2) <- parsed]
            in return (inputAlphabet, outputAlphabet, allStates, initialState, Just parsed)

-- | Parse initial line of .aut file and return initialState. The line must follow the structure des (initState,nEdges,nStates).
parseInitialState :: T.Text -> Maybe String
parseInitialState line =
  case T.stripPrefix "des (" (T.strip line) of
    Nothing -> Nothing
    Just rest ->
      let elems = T.split (==',') (T.replace ")," "" rest)
      in case elems of
           (s:_) -> Just (T.unpack (T.strip s))
           _     -> Nothing

-- | Parse a text line of the form (state, action, state), and return the transition tuple
-- | NOTE: only action labels finished in "_i" or "_o" are considered
parseTupleLine :: T.Text -> Maybe (String, IOAct String String, String)
parseTupleLine line =
    let stripped = T.strip line -- Remove trailing whitespaces
        removedParen = (T.replace ")" "" . T.replace "(" "" . T.replace ")," "") stripped
        transition = T.split (==',') removedParen
    in case transition of
        [s1, act, s2] ->
            let actionStr = T.unpack (T.strip act)
                initState = T.unpack (T.strip s1)
                endState = T.unpack (T.strip s2)
            in if "_i" `isSuffixOf` actionStr then
                   Just (initState, In actionStr, endState)
               else if "_o" `isSuffixOf` actionStr then
                   Just (initState, Out actionStr, endState)
               else
                   Nothing -- Non-valid action
        _ -> Nothing  -- Malformed line

-- | Build a .dot file representation of LTS transitions and save it in the specified File Path
dumpLTSdot :: (Show s, Show i, Show o) => FilePath -> [(s, IOAct i o, s)] -> IO ()
dumpLTSdot path transitions = do
    let edges = [ (show from, T.unpack (T.replace "!" "" . T.replace "?" "" $ T.pack (show label)), show to)
                | (from, label, to) <- transitions ]
    let dotPath = if ".dot" `isSuffixOf` path then path else path ++ ".dot"
    writeFile dotPath $
        unlines $
            ["digraph Automaton {"] ++
            [ "    " ++ from ++ " -> " ++ to ++ " [label=" ++ label ++ "];" 
            | (from, label, to) <- edges
            ] ++ ["}"]
    putStrLn $ "DOT file written to: " ++ dotPath