packages feed

statechart-0.1.0: src/Model.hs

module Model
  ( Class         (..)
  , Attribute     (..)
  , StateChart    (..)
  , Init          (..)
  , Transition    (..)
  , Name
  , Id
  , Code
  , parseModel
  ) where

import Data.List
import Data.Maybe

import Rhapsody

type Name = String
type Id   = String
type Code = String

data Class = Class Name [Attribute] [StateChart] deriving Show

data Attribute = Attribute
  { attrName :: Name
  , attrType :: String
  , attrInit :: Maybe String
  } deriving Show

data StateChart' = StateChart' Name [State] [Transition'] deriving Show

data State = State
  { stateName         :: Name
  , stateId           :: Id
  , stateParent       :: Maybe Id
  , stateDefaultTrans :: Maybe Id
  , stateEntryAction  :: Maybe Code
  , stateExitAction   :: Maybe Code
  } deriving Show

data Transition' = Transition'
  { transition'Name    :: Name
  , transition'Id      :: Id
  , transition'Timeout :: Maybe Code
  , transition'Guard   :: Maybe Code
  , transition'Action  :: Maybe Code
  , transition'Source  :: Maybe Id
  , transition'Target  :: Id
  } deriving Show

data StateChart = StateChart Name [(Id, Name)] Init [Transition] deriving Show

data Init = Init
  { initAction :: [Code]
  , initTarget :: [Id]
  } deriving Show

data Transition = Transition
  { transitionName    :: (Name, Name)
  , transitionSource  :: [Id]
  , transitionTimeout :: [(Id, Code)]
  , transitionGuard   :: [Code]
  , transitionAction  :: [Code]
  , transitionTarget  :: [Id]
  } deriving Show

formatStateChart :: StateChart' -> StateChart
formatStateChart (StateChart' name states transitions) = if not defaultTransitionsCorrect
  then error "invalid transition:  default transitions must only point to direct sub-states"
  else if not transitionsCorrect
    then error "invalid transition:  transitions must not cross state boundries"
    else if not allParentStatesHaveDefaultTrans
      then error "invalid transition:  all states containing sub-states must have a default transition"
      else if not $ null redundentStateNames
        then error $ "invalid states:  state names are not unique: " ++ show redundentStateNames
        else StateChart name idsNames init transitions'
  where
  defaultTransitionsCorrect = and [ elem (transition'Target t) (children (stateId s)) | s <- states, t <- transitions, Just (transition'Id t) == stateDefaultTrans s ]
  transitionsCorrect = and [ parent (fromJust $ transition'Source t) == parent (transition'Target t) | t <- transitions, isJust $ transition'Source t ]
  allParentStatesHaveDefaultTrans = and [ stateDefaultTrans s /= Nothing | s <- states, not $ null $ children $ stateId s ] 
  redundentStateNames = foldr delete names (nub names)
  names = [ stateName s | s <- states ]
  idsNames = [ (id, stateName $ state id) | id <- nub $ concat [ transitionSource t ++ transitionTarget t | t <- transitions' ] ++ initTarget init ]

  transitions' = [ appendTransitions (appendTransitions pre $ convertTrans t) $ entryTransition $ transition'Target t | t <- transitions, isJust $ transition'Source t, pre <- exitTransitions $ fromJust $ transition'Source t ]

  initTrans = entryTransition $ head [ stateId s | s <- states, stateParent s == Nothing ]
  init = Init
    { initAction = transitionAction initTrans
    , initTarget = transitionTarget initTrans
    }

  state :: Id -> State
  state id = case [ s | s <- states, stateId s == id ] of
    [a] -> a
    _ -> error $ "state not found: " ++ id
    
  parent :: Id -> Id
  parent = fromJust . stateParent . state
  
  children :: Id -> [Id]
  children id = [ stateId s | s <- states, stateParent s == Just id ]

  trans :: Id -> Transition'
  trans id = case [ t | t <- transitions, transition'Id t == id ] of
    [a] -> a
    _ -> error $ "transition not found: " ++ id

  -- Elaborated entry transition given a state id.
  entryTransition :: Id -> Transition
  entryTransition id = case stateDefaultTrans $ state id of
    Nothing  -> entry
    Just tId -> appendTransitions (appendTransitions entry $ convertTrans t) $ entryTransition $ transition'Target t
      where
      t = trans tId
    where
    entry = Transition
      { transitionName    = ("", "")
      , transitionSource  = []
      , transitionTimeout = []
      , transitionGuard   = []
      , transitionAction  = maybeToList $ stateEntryAction $ state id
      , transitionTarget  = [id]
      }

  -- Elaborated exit transitions given a state id.
  exitTransitions :: Id -> [Transition]
  exitTransitions id = if null subExits then [exit] else [ appendTransitions a exit | a <- subExits ]
    where
    exit = Transition
      { transitionName    = ("", "")
      , transitionSource  = [id]
      , transitionTimeout = []
      , transitionGuard   = []
      , transitionAction  = maybeToList $ stateExitAction $ state id
      , transitionTarget  = []
      }
    subExits = concat [ exitTransitions a | a <- children id ]


  convertTrans :: Transition' -> Transition
  convertTrans t = Transition
    { transitionName = case transition'Source t of
        Nothing -> ("", "")
        Just src -> (stateName $ state $ src, stateName $ state $ transition'Target t)
    , transitionSource  = maybeToList $ transition'Source t
    , transitionTimeout = case (transition'Source t, transition'Timeout t) of
        (Just id, Just code) -> [(id, code)]
        _ -> []
    , transitionGuard   = maybeToList $ transition'Guard  t
    , transitionAction  = maybeToList $ transition'Action t
    , transitionTarget  = [transition'Target t]
    }
  
appendTransitions :: Transition -> Transition -> Transition
appendTransitions a b = Transition
  { transitionName    = (fst (transitionName   a) ++ fst (transitionName    b), snd (transitionName   a) ++ snd (transitionName    b))
  , transitionSource  = nub $ transitionSource  a ++ transitionSource  b  --XXX Will b every have sources?
  , transitionTimeout = transitionTimeout a ++ transitionTimeout b
  , transitionGuard   = transitionGuard   a ++ transitionGuard   b
  , transitionAction  = transitionAction  a ++ transitionAction  b
  , transitionTarget  = nub $ transitionTarget  a ++ transitionTarget  b
  }

parseModel :: Record -> [Class]
parseModel file = map parseClass $ containerRecords "Classes" file

parseClass :: Record -> Class
parseClass a = Class (head $ values "_name" a) (map parseAttribute $ containerRecords "Attrs" a)  (map (formatStateChart . parseStateChart) $ containerRecords "StateCharts" a)

parseAttribute :: Record -> Attribute
parseAttribute a = Attribute
  { attrName = value "_name" a
  , attrType = value "_name" $ head $ records "_typeOf" a
  , attrInit = case containerRecords "ValueSpecifications" a of
      []  -> Nothing
      [a] -> Just $ value "_value" a
      _   -> error "too many value specifications"
  }

parseStateChart :: Record -> StateChart'
parseStateChart a = if not $ null $ containerRecords "Connectors" a then error "connectors not supported" else StateChart'
  (value "_name" a)
  (map parseState      $ containerRecords "States"      a)
  (map parseTransition $ containerRecords "Transitions" a)

parseState :: Record -> State
parseState a = if values "_stateType" a == ["And"] then error $ "invalid state: AND states not supported: " ++ value "_name" a else State
  { stateName         = if null (value "_name" a) then "root" else value "_name" a
  , stateId           = ident "_id"           a
  , stateParent       = maybeList $ ident "_parent"       a
  , stateDefaultTrans = maybeList $ ident "_defaultTrans" a
  , stateEntryAction  = maybeList $ body  "_entryAction"  a
  , stateExitAction   = maybeList $ body  "_exitAction"   a
  } 

parseTransition :: Record -> Transition'
parseTransition a = Transition'
  { transition'Name    = value "_name" a
  , transition'Id      = ident "_id"   a
  , transition'Timeout = timeout
  , transition'Guard   = maybeList $ if guard == "else" then error "else guards not supported" else guard
  , transition'Action  = maybeList $ body "_itsAction"  label
  , transition'Source  = maybeList $ ident "_itsSource" a
  , transition'Target  = ident "_itsTarget" a
  }
  where
  label = head $ records "_itsLabel" a
  timeout = case body "_itsTrigger" label of
    a | isPrefixOf "tm(" a -> Just $ drop 3 $ init a
      | null a             -> Nothing
      | otherwise          -> error $ "not supported non tm() events: " ++ a
  guard = body "_itsGuard" label

maybeList :: [a] -> Maybe [a]
maybeList a = if null a then Nothing else Just a