xmlbf-0.5: lib/Xmlbf.hs
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | XML back and forth!
--
-- @xmlbf@ doesn't do any parsing of raw XML on its own. Instead, one should
-- rely on libraries like
-- [xmlbf-xeno](https://hackage.haskell.org/package/xmlbf-xeno) or
-- [xmlbf-xmlhtml](https://hackage.haskell.org/package/xmlbf-xmlhtml) for
-- this.
--
-- @xmlbf@ provides a 'FromXml' class intended to be used as the familiar
-- 'Data.Aeson.FromJSON' from the @aeson@ package. This relies on the
-- 'Parser' type and the related tools.
--
-- @xmlbf@ provides a 'ToXml' class intended to be used as the familiar
-- 'Data.Aeson.toJSON' from the @aeson@ package.
--
-- @xmlb@ provides tools like 'dfpos' and 'dfposM' for finding a fixpoint
-- of a XML structure.
module Xmlbf
( -- * Parsing
FromXml(fromXml)
, Parser
, runParser
, pFail
, pElement
, pAnyElement
, pName
, pAttr
, pAttrs
, pChildren
, pText
, pEndOfInput
-- * Rendering
, ToXml(toXml)
, encode
, Node
, node
, pattern Element
, element
, element'
, pattern Text
, text
, text'
-- * Fixpoints
, dfpos
, dfposM
, dfpre
, dfpreM
) where
import Control.DeepSeq (NFData(rnf))
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Builder.Prim as BBP
import qualified Data.Char as Char
import Data.Foldable (for_, toList)
import Data.Functor.Identity (Identity(Identity), runIdentity)
import qualified Data.HashMap.Strict as HM
import Data.Monoid ((<>))
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TL
import Data.Traversable (for)
import Data.Word (Word8)
import Control.Applicative (Alternative(empty, (<|>)))
import Control.Monad (MonadPlus(mplus, mzero), join, when)
import Control.Monad.Fail (MonadFail(fail))
--------------------------------------------------------------------------------
-- | Either a text or an element node in an XML fragment body.
--
-- Construct with 'text' or 'element'. Destruct with 'Text' or 'Element'.
data Node
= Element' !T.Text !(HM.HashMap T.Text T.Text) ![Node]
| Text' !TL.Text
deriving (Eq)
instance NFData Node where
rnf = \case
Element' n as cs -> rnf n `seq` rnf as `seq` rnf cs `seq` ()
Text' t -> rnf t `seq` ()
instance Show Node where
showsPrec n = \x -> showParen (n > 10) $ case x of
Text' t -> showString "Text " . showsPrec 0 t
Element' t as cs ->
showString "Element " .
showsPrec 0 t . showChar ' ' .
showsPrec 0 (HM.toList as) . showChar ' ' .
showsPrec 0 cs
-- | Destruct an element 'Node'.
pattern Element :: T.Text -> (HM.HashMap T.Text T.Text) -> [Node] -> Node
pattern Element t as cs <- Element' t as cs
{-# COMPLETE Element #-} -- TODO this leads to silly pattern matching warnings
-- | Destruct a text 'Node'.
pattern Text :: TL.Text -> Node
pattern Text t <- Text' t
{-# COMPLETE Text #-} -- TODO this leads to silly pattern matching warnings
-- | Case analysis for a 'Node'.
node
:: (T.Text -> HM.HashMap T.Text T.Text -> [Node] -> a)
-- ^ Transform an 'Element' node.
-> (TL.Text -> a)
-- ^ Transform a 'Text' node.
-> Node
-> a
{-# INLINE node #-}
node fe ft = \case
Text' t -> ft t
Element' t as cs -> fe t as cs
-- | Normalizes 'Node's by concatenating consecutive 'Text' nodes.
normalize :: [Node] -> [Node]
{-# INLINE normalize #-}
normalize = \case
-- Note that @'Text' ""@ is forbidden by construction, actually. But we do
-- take care of it in case the 'Node' was constructed unsafely somehow.
Text' "" : ns -> normalize ns
Text' a : Text' b : ns -> normalize (text (a <> b) <> ns)
Text' a : ns -> Text' a : normalize ns
Element' t as cs : ns -> Element' t as (normalize cs) : normalize ns
[] -> []
-- | Construct a XML fragment body containing a single 'Text' 'Node', if
-- possible.
--
-- This function will return empty list if it is not possible to construct the
-- 'Text' with the given input. To learn more about /why/ it was not possible to
-- construct it, use 'text'' instead.
--
-- Using 'text'' rather than 'text' is recommended, so that you are forced to
-- acknowledge a failing situation in case it happens. However, 'text' is at
-- times more convenient to use, whenever you know the input is valid.
text :: TL.Text -> [Node]
{-# INLINE text #-}
text t = case text' t of
Right x -> [x]
Left _ -> []
-- | Construct a 'Text' 'Node', if possible.
--
-- Returns 'Left' if the 'Text' 'Node' can't be created, with an explanation
-- of why.
text' :: TL.Text -> Either String Node
{-# INLINE text' #-}
text' = \case
"" -> Left "Empty text"
t -> Right (Text' t)
-- | Construct a XML fragment body containing a single 'Element' 'Node', if
-- possible.
--
-- This function will return empty list if it is not possible to construct the
-- 'Element' with the given input. To learn more about /why/ it was not possible
-- to construct it, use 'element' instead.
--
-- Using 'element'' rather than 'element' is recommended, so that you are forced
-- to acknowledge a failing situation in case it happens. However, 'element' is
-- at times more convenient to use, whenever you know the input is valid.
element
:: T.Text -- ^ Element' name.
-> HM.HashMap T.Text T.Text -- ^ Attributes.
-> [Node] -- ^ Children.
-> [Node]
{-# INLINE element #-}
element t hm ns = case element' t hm ns of
Right x -> [x]
Left _ -> []
-- | Construct an 'Element' 'Node'.
--
-- Returns 'Left' if the 'Element' 'Node' can't be created, with an explanation
-- of why.
element'
:: T.Text -- ^ Element' name.
-> HM.HashMap T.Text T.Text -- ^ Attributes.
-> [Node] -- ^ Children.
-> Either String Node
element' t0 hm0 ns0 = do
when (t0 /= T.strip t0)
(Left ("Element name has surrounding whitespace: " ++ show t0))
when (T.null t0)
(Left ("Element name is blank: " ++ show t0))
for_ (HM.keys hm0) $ \k -> do
when (k /= T.strip k)
(Left ("Attribute name has surrounding whitespace: " ++ show k))
when (T.null k)
(Left ("Attribute name is blank: " ++ show k))
Right (Element' t0 hm0 (normalize ns0))
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Parsing
class FromXml a where
-- | Parses an XML fragment body into a value of type @a@.
--
-- If a 'ToXml' instance for @a@ exists, then:
--
-- @
-- 'runParser' 'fromXml' ('toXml' a) == 'Right' a
-- @
fromXml :: Parser a
-- | XML parser monad. To be run with 'runParser'.
--
-- You can build a 'Parser' using 'pElement', 'pAttr', 'pAttrs', 'pText',
-- 'pFail', or any of the 'Applicative', 'Alternative' or 'Monad' combinators.
newtype Parser a = Parser { unParser :: S -> Either String (a, S) }
deriving (Functor)
-- | Run a parser on an XML fragment body. If the parser fails, then a 'String'
-- with an error message is returned.
--
-- Notice that this function doesn't enforce that all input is consumed. If you
-- want that behavior, then please use 'pEndOfInput' in the given 'Parser'.
runParser :: Parser a -> [Node] -> Either String a
runParser p0 = fmap fst . unParser p0 . STop . normalize
-- | Internal parser state.
data S
= STop ![Node]
-- ^ Parsing the top-level nodes.
| SReg !T.Text !(HM.HashMap T.Text T.Text) ![Node]
-- ^ Parsing a particular root element.
deriving (Show)
instance Applicative Parser where
{-# INLINE pure #-}
pure = \a -> Parser (\s -> Right (a, s))
{-# INLINE (<*>) #-}
Parser gf <*> Parser ga = Parser $ \s0 -> do
(f, s1) <- gf s0
(a, s2) <- ga s1
pure (f a, s2)
instance Monad Parser where
{-# INLINE (>>=) #-}
Parser ga >>= k = Parser $ \s0 -> do
(a, s1) <- ga s0
unParser (k a) s1
fail = pFail
instance MonadFail Parser where
fail = pFail
-- | Backtracks.
instance Alternative Parser where
{-# INLINE empty #-}
empty = Parser (\_ -> Left "empty")
{-# INLINE (<|>) #-}
Parser a <|> Parser b = Parser (\s -> either (\_ -> b s) Right (a s))
-- | Backtracks.
instance MonadPlus Parser where
{-# INLINE mzero #-}
mzero = empty
{-# INLINE mplus #-}
mplus = (<|>)
--------------------------------------------------------------------------------
-- Some parsers
-- | A 'Parser' that always fails with the given error message.
pFail :: String -> Parser a
pFail e = Parser (\_ -> Left e)
-- | @'pElement' "foo" p@ runs a 'Parser' @p@ inside a element node named
-- @"foo"@. This fails if such element does not exist at the current position.
--
-- Leading whitespace is ignored. If you need to preserve that whitespace for
-- some reason, capture it using 'pText' before using 'pElement'.
--
-- Consumes the element from the parser state.
pElement :: T.Text -> Parser a -> Parser a
{-# INLINABLE pElement #-}
pElement t0 p0 = Parser $ \case
SReg t1 as0 (Element' t as cs : cs0) | t == t0 -> do
(a,_) <- unParser p0 (SReg t as cs)
Right (a, SReg t1 as0 cs0)
STop (Element' t as cs : cs0) | t == t0 -> do
(a,_) <- unParser p0 (SReg t as cs)
Right (a, STop cs0)
-- skip leading whitespace
SReg t as (Text' x : cs) | TL.all Char.isSpace x ->
unParser (pElement t0 p0) (SReg t as cs)
STop (Text' x : cs) | TL.all Char.isSpace x ->
unParser (pElement t0 p0) (STop cs)
_ -> Left ("Missing element " ++ show t0)
-- | @'pAnyElement' p@ runs a 'Parser' @p@ inside the element node at the
-- current position, if any. Otherwise, if no such element exists, this parser
-- fails.
--
-- You can recover the name of the matched element using 'pName' inside the
-- given 'Parser'. However, if you already know beforehand the name of the
-- element that you want to match, it's better to use 'pElement' rather than
-- 'pAnyElement'.
--
-- Leading whitespace is ignored. If you need to preserve that whitespace for
-- some reason, capture it using 'pText' before using 'pAnyElement'.
--
-- Consumes the element from the parser state.
pAnyElement :: Parser a -> Parser a
{-# INLINABLE pAnyElement #-}
pAnyElement p0 = Parser $ \case
SReg t0 as0 (Element' t as cs : cs0) -> do
(a,_) <- unParser p0 (SReg t as cs)
Right (a, SReg t0 as0 cs0)
STop (Element' t as cs : cs0) -> do
(a,_) <- unParser p0 (SReg t as cs)
Right (a, STop cs0)
-- skip leading whitespace
SReg t as (Text' x : cs) | TL.all Char.isSpace x ->
unParser (pAnyElement p0) (SReg t as cs)
STop (Text' x : cs) | TL.all Char.isSpace x ->
unParser (pAnyElement p0) (STop cs)
_ -> Left "Missing element"
-- | Returns the name of the currently selected element.
--
-- This parser fails if there's no currently selected element.
--
-- Doesn't modify the parser state.
pName :: Parser T.Text
{-# INLINABLE pName #-}
pName = Parser $ \case
SReg t as cs -> Right (t, SReg t as cs)
STop _ -> Left "Before selecting an name, you must select an element"
-- | Return the value of the requested attribute, if defined. May return an
-- empty string in case the attribute is defined but no value was given to it.
--
-- This parser fails if there's no currently selected element.
--
-- Consumes the attribute from the parser state.
pAttr :: T.Text -> Parser T.Text
{-# INLINABLE pAttr #-}
pAttr n = Parser $ \case
STop _ -> Left "Before selecting an attribute, you must select an element"
SReg t as cs -> case HM.lookup n as of
Just x -> Right (x, SReg t (HM.delete n as) cs)
Nothing -> Left ("Missing attribute " ++ show n)
-- | Returns all of the available element attributes. May return empty strings
-- as values in case an attribute is defined but no value was given to it.
--
-- This parser fails if there's no currently selected element.
--
-- Consumes all of the remaining attributes for this element from the parser
-- state.
pAttrs :: Parser (HM.HashMap T.Text T.Text)
{-# INLINABLE pAttrs #-}
pAttrs = Parser $ \case
STop _ -> Left "Before selecting an attribute, you must select an element"
SReg t as cs -> Right (as, SReg t mempty cs)
-- | Returns all of the immediate children of the current element.
--
-- If parsing top-level nodes rather than a particular element (that is, if
-- 'pChildren' is /not/ being run inside 'pElement'), then all of the top level
-- 'Node's will be returned.
--
-- Consumes all of the returned nodes from the parser state.
pChildren :: Parser [Node]
{-# INLINABLE pChildren #-}
pChildren = Parser $ \case
STop cs -> Right (cs, STop mempty)
SReg t as cs -> Right (cs, SReg t as mempty)
-- | Return a text node value.
--
-- Surrounidng whitespace is not removed, as it is considered to be part of the
-- text node.
--
-- If there is no text node at the current position, then this parser fails.
-- This implies that 'pText' /never/ returns an empty 'T.Text', since there is
-- no such thing as a text node without text.
--
-- Please note that consecutive text nodes are always concatenated and returned
-- together.
--
-- @
-- 'runParser' 'pText' ('text' \"Ha\" <> 'text' \"sk\" <> 'text' \"ell\")
-- == 'Right' ('text' "Haskell")
-- @
--
-- The returned text is consumed from the parser state. This implies that if you
-- perform two consecutive 'pText' calls, the second will always fail.
--
-- @
-- 'runParser' ('pText' >> 'pText') ('text' \"Ha\" <> 'text' \"sk\" <> 'text' \"ell\")
-- == 'Left' "Missing text node"
-- @
pText :: Parser TL.Text
{-# INLINABLE pText #-}
pText = Parser $ \case
-- Note: this works only because we asume 'normalize' has been used.
STop (Text x : ns) -> Right (x, STop ns)
SReg t as (Text x : cs) -> Right (x, SReg t as cs)
_ -> Left "Missing text node"
-- | Succeeds if all of the elements, attributes and text nodes have
-- been consumed.
pEndOfInput :: Parser ()
{-# INLINABLE pEndOfInput #-}
pEndOfInput = Parser (\s ->
if isEof s then Right ((), s)
else Left "Not end of input yet")
isEof :: S -> Bool
{-# INLINE isEof #-}
isEof = \case
SReg _ as cs -> HM.null as && null cs
STop ns -> null ns
--------------------------------------------------------------------------------
-- Rendering
class ToXml a where
-- | Renders a value of type @a@ into an XML fragment body.
--
-- If a 'FromXml' instance for @a@ exists, then:
--
-- @
-- 'runParser' 'fromXml' ('toXml' a) == 'Right' a
-- @
toXml :: a -> [Node]
-- | Encodes a list of XML 'Node's, representing an XML fragment body, to an
-- UTF8-encoded and XML-escaped bytestring.
--
-- This function doesn't render self-closing elements. Instead, all
-- elements have a corresponding closing tag.
--
-- Also, it doesn't render CDATA sections. Instead, all text is escaped as
-- necessary.
encode :: [Node] -> BB.Builder
encode xs = mconcat (map encodeNode xs)
where
encodeNode :: Node -> BB.Builder
encodeNode = \case
Text x -> encodeXmlUtf8Lazy x
Element t as cs ->
-- This ugly code is so that we make sure we always bind concatenation
-- to the right with as little effort as possible, using (<>).
"<" <> encodeUtf8 t
<> encodeAttrs (">" <> encode cs <> "</" <> encodeUtf8 t <> ">") as
encodeAttrs :: BB.Builder -> HM.HashMap T.Text T.Text -> BB.Builder
encodeAttrs = HM.foldlWithKey'
(\o k v -> " " <> encodeUtf8 k <> "=\"" <> encodeXmlUtf8 v <> "\"" <> o)
{-# INLINE encodeNode #-}
{-# INLINE encodeAttrs #-}
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Node fixpoint
-- | Post-order depth-first replacement of 'Node' and all of its children.
--
-- This function works like 'Data.Function.fix', but the given function is
-- trying to find a fixpoint for the individual children nodes, not for the root
-- node.
--
-- For example, the following function renames every node named @"w"@ to @"y"@,
-- and every node named @"y"@ to @"z"@. It accomplishes this by first renaming
-- @"w"@ nodes to @"x"@, and then, by using @k@ recursively to further rename
-- all @"x"@ nodes (including the ones that were just created) to @"y"@ in a
-- post-order depth-first manner. After renaming an @"x"@ node to @"y"@, the
-- recursion stops (i.e., @k@ is not used), so our new @"y"@ nodes won't be
-- further renamed to @"z"@. However, nodes that were named @"y"@ initially will
-- be renamed to @"z"@.
--
-- In our example we only replace one node with another, but a node can be
-- replaced with zero or more nodes, depending on the length of the resulting
-- list.
--
-- @
-- foo :: 'Node' -> ['Node']
-- foo = 'dfpos' $ \\k -> \\case
-- 'Element' "w" as cs -> 'element'' "x" as cs >>= k
-- 'Element' "x" as cs -> 'element'' "y" as cs
-- 'Element' "y" as cs -> 'element'' "z" as cs >>= k
-- @
--
-- See 'dfpre' for pre-orderd depth-first replacement.
--
-- /WARNING/ If you call @k@ in every branch, then 'dfpos' will never terminate.
-- Make sure the recursion stops at some point by simply returning a list of
-- nodes instead of calling @k@.
dfpos :: ((Node -> [Node]) -> Node -> [Node]) -> Node -> [Node]
dfpos f = runIdentity . dfposM (\k -> Identity . f (runIdentity . k))
-- | Monadic version of 'dfpos'.
dfposM :: Monad m => ((Node -> m [Node]) -> Node -> m [Node]) -> Node -> m [Node]
dfposM f = \n0 -> do
c1 <- traverseChildren (dfposM f) (cursorFromNode n0)
c2 <- traverseRightSiblings (dfposM f) c1
fmap (normalize . join)
(traverse (f (dfposM f)) (cursorSiblings c2))
-- | Pre-order depth-first replacement of 'Node' and all of its children.
--
-- This is just like 'dfpos' but the search proceeds in a different order.
dfpre :: ((Node -> [Node]) -> Node -> [Node]) -> Node -> [Node]
dfpre f = runIdentity . dfpreM (\k -> Identity . f (runIdentity . k))
-- | Monadic version of 'dfpre'.
dfpreM :: Monad m => ((Node -> m [Node]) -> Node -> m [Node]) -> Node -> m [Node]
dfpreM f = \n0 -> do
ns <- f (dfpreM f) n0
fmap (normalize . join) $ for ns $ \n -> do
c1 <- traverseChildren (dfpreM f) (cursorFromNode n)
cursorSiblings <$> traverseRightSiblings (dfpreM f) c1
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- INTERNAL: Cursor
--
-- Most of this comes from Chris Smith's xmlhtml, BSD3 licensed
-- https://hackage.haskell.org/package/xmlhtml
-- | Zipper into a 'Node' tree.
data Cursor = Cursor
{ _cursorCurrent :: !Node
-- ^ Retrieves the current node of a 'Cursor'.
, _cursorLefts :: !(Seq Node)
-- ^ Nodes to the left (ordered right to left).
, _cursorRights :: !(Seq Node)
-- ^ Nodes to the right (ordered left to right).
, _cursorParents :: !(Seq (Seq Node, T.Text, HM.HashMap T.Text T.Text, Seq Node))
-- ^ Parents' name, attributes, and siblings.
}
------------------------------------------------------------------------------
-- | The cursor if left where it starts.
traverseChildren :: Monad m => (Node -> m [Node]) -> Cursor -> m Cursor
{-# INLINE traverseChildren #-}
traverseChildren f c0 = case _cursorCurrent c0 of
Text _ -> pure c0
Element t as cs -> do
n1s <- fmap (normalize . join) (traverse f cs)
pure (c0 {_cursorCurrent = Element' t as n1s})
-- | The cursor if left in the rightmost sibling.
traverseRightSiblings :: Monad m => (Node -> m [Node]) -> Cursor -> m Cursor
{-# INLINE traverseRightSiblings #-}
traverseRightSiblings f c0 = case cursorRemoveRight c0 of
Nothing -> pure c0
Just (n1, c1) -> do
n2s <- fmap normalize (f n1)
traverseRightSiblings f (cursorInsertManyRight n2s c1)
-- | Builds a 'Cursor' for navigating a tree. That is, a forest with a single
-- root 'Node'.
cursorFromNode :: Node -> Cursor
{-# INLINE cursorFromNode #-}
cursorFromNode n = Cursor n mempty mempty mempty
-- | Retrieves a list of the 'Node's at the same level as the current position
-- of a cursor, including the current node.
cursorSiblings :: Cursor -> [Node]
{-# INLINE cursorSiblings #-}
cursorSiblings (Cursor cur ls rs _) =
toList (Seq.reverse ls <> (cur Seq.<| rs))
-- | Removes the node to the right and return it.
cursorRemoveRight :: Cursor -> Maybe (Node, Cursor)
{-# INLINE cursorRemoveRight #-}
cursorRemoveRight = \case
Cursor n ls rs0 ps | not (Seq.null rs0) ->
case Seq.viewl rs0 of
r Seq.:< rs -> Just (r, Cursor n ls rs ps)
_ -> undefined -- unreachable, rs0 is not empty
_ -> Nothing
-- | Inserts a list of new 'Node's to the right of the current position.
cursorInsertManyRight :: [Node] -> Cursor -> Cursor
{-# INLINE cursorInsertManyRight #-}
cursorInsertManyRight ns (Cursor nn ls rs ps) =
Cursor nn ls (Seq.fromList ns <> rs) ps
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Miscellaneous
encodeUtf8 :: T.Text -> BB.Builder
{-# INLINE encodeUtf8 #-}
encodeUtf8 = T.encodeUtf8Builder
encodeXmlUtf8 :: T.Text -> BB.Builder
{-# INLINE encodeXmlUtf8 #-}
encodeXmlUtf8 = T.encodeUtf8BuilderEscaped xmlEscaped
encodeXmlUtf8Lazy :: TL.Text -> BB.Builder
{-# INLINE encodeXmlUtf8Lazy #-}
encodeXmlUtf8Lazy = TL.encodeUtf8BuilderEscaped xmlEscaped
xmlEscaped :: BBP.BoundedPrim Word8
{-# INLINE xmlEscaped #-}
xmlEscaped =
BBP.condB (== 38) (fixed5 (38,(97,(109,(112,59))))) $ -- '&' -> "&"
BBP.condB (== 60) (fixed4 (38,(108,(116,59)))) $ -- '<' -> "<"
BBP.condB (== 62) (fixed4 (38,(103,(116,59)))) $ -- '>' -> ">"
BBP.condB (== 34) (fixed5 (38,(35,(51,(52,59))))) $ -- '"' -> """
BBP.liftFixedToBounded BBP.word8
where
{-# INLINE fixed4 #-}
fixed4 :: (Word8, (Word8, (Word8, Word8))) -> BBP.BoundedPrim Word8
fixed4 x = BBP.liftFixedToBounded
(const x BBP.>$< BBP.word8 BBP.>*< BBP.word8
BBP.>*< BBP.word8 BBP.>*< BBP.word8)
{-# INLINE fixed5 #-}
fixed5 :: (Word8, (Word8, (Word8, (Word8, Word8)))) -> BBP.BoundedPrim Word8
fixed5 x = BBP.liftFixedToBounded
(const x BBP.>$< BBP.word8 BBP.>*< BBP.word8
BBP.>*< BBP.word8 BBP.>*< BBP.word8 BBP.>*< BBP.word8)