scxml-statecharts-0.1.0.0: src/Scxml/Statechart/Model.hs
{-# LANGUAGE DeriveLift #-}
-- | The untyped statechart model. This is what the SCXML parser produces and
-- what the evaluator runs on. The Template Haskell layer generates typed
-- wrappers around it.
--
-- The chart is a tree and nothing else: a state owns its children, so a
-- dangling child cannot be represented, and there is no parent link or
-- document index to disagree with the structure. Since a transition may only
-- target a sibling, every consumer works by walking the tree, so none of that
-- would have a reader anyway.
module Scxml.Statechart.Model
( -- * The tree
StateId
, Kind (..)
, Node (..)
, Chart (..)
, nodeChildren
, childrenOfKind
, initialChild
, isParallel
, completes
, chartStates
) where
import Data.List.NonEmpty (NonEmpty ((:|)))
import qualified Data.List.NonEmpty as NE
import Data.Map.Strict (Map)
import Data.Text (Text)
import Language.Haskell.TH.Syntax (Lift)
-- | A state's @id@ attribute, which is also its Haskell constructor name.
type StateId = Text
-- | What kind of state a node is, together with the children it owns:
--
-- * 'Atomic' is a leaf and 'Final' is terminal, so neither has children.
-- * 'Compound' has children of which exactly one is active. The first is the
-- one entering it leads to, so a compound state cannot lack an initial
-- child or name one that is not its own.
-- * 'Parallel' has regions, all of which are active at once, so there is no
-- initial one to choose. Their order is document order.
data Kind
= Atomic
| Compound (NonEmpty Node)
| Parallel (NonEmpty Node)
| Final
deriving (Eq, Show, Lift)
-- | One state and everything inside it.
data Node = Node
{ nodeId :: StateId
, nodeKind :: Kind
, nodeTransitions :: Map Text StateId
-- ^ event name to the sibling state it enters. At most one transition per
-- event, so nothing has to break a tie.
, nodeOnEntry :: [Text] -- ^ names of @<onentry><script>@ callbacks
, nodeOnExit :: [Text] -- ^ names of @<onexit><script>@ callbacks
}
deriving (Eq, Show, Lift)
-- | A whole chart. This is what the quasiquoter lifts into the generated code.
data Chart = Chart
{ chartName :: Maybe Text
, chartRoot :: NonEmpty Node -- ^ children of @<scxml>@; the first is entered
, chartEvents :: [Text] -- ^ every event a transition names, in document order
}
deriving (Eq, Show, Lift)
-- | Children of a state, the first being its initial child when it has one.
nodeChildren :: Node -> [Node]
nodeChildren = childrenOfKind . nodeKind
-- | Children of a kind, the first being its initial child when it has one.
childrenOfKind :: Kind -> [Node]
childrenOfKind Atomic = []
childrenOfKind Final = []
childrenOfKind (Compound cs) = NE.toList cs
childrenOfKind (Parallel rs) = NE.toList rs
-- | The child that entering a compound state leads to.
initialChild :: Kind -> Maybe Node
initialChild (Compound (c :| _)) = Just c
initialChild _ = Nothing
-- | Whether every child is active at once.
isParallel :: Kind -> Bool
isParallel (Parallel _) = True
isParallel _ = False
-- | Can this state ever raise its @done.state@ event? A parallel state can,
-- once every region is final; a compound state needs a @<final>@ child.
completes :: Node -> Bool
completes n = case nodeKind n of
Parallel _ -> True
Compound cs -> any ((== Final) . nodeKind) (NE.toList cs)
_ -> False
-- | Every state in the chart, in document order. Needs no index: it is the
-- pre-order walk of the tree.
chartStates :: Chart -> [Node]
chartStates = concatMap preorder . NE.toList . chartRoot
where
preorder n = n : concatMap preorder (nodeChildren n)