packages feed

wireform-core-0.2.0.0: src/Wireform/Parser/Switch.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UnboxedSums #-}
{-# LANGUAGE UnboxedTuples #-}

-- \$(switch [| case _ of
--     \"if\"   -> pure TIf
--     \"else\" -> pure TElse
--     _      -> identifier |])
-- @

{- | Efficient literal branching using Template Haskell.

Compiles string-literal case branches into a trie of primitive byte
comparisons with grouped bounds checks.

@
-}
module Wireform.Parser.Switch (
  switch,
  switchWithPost,

  -- * TH literal splices
  char,
  string,

  -- * Helpers used by generated code (not for direct use)
  switchFailed,
  switchBranch,
  switchAnyWord8Unsafe,
  switchPeekWord8Unsafe,
  switchSkip1,
) where

import Control.Monad (forM)
import Data.ByteString.Char8 qualified as BSC
import Data.Char (ord)
import Data.Foldable (foldl')
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as M
import Data.Word (Word8)
import GHC.Exts
import GHC.Word (Word8 (..))
import Language.Haskell.TH
import Language.Haskell.TH.Syntax (Lift (..))
import Wireform.Parser (byteString, word8)
import Wireform.Parser.Internal


------------------------------------------------------------------------
-- Helpers called by generated code
------------------------------------------------------------------------

-- | Fail without consuming input.
switchFailed :: Parser m e a
switchFailed = Parser \_ _ _ st -> (# st, Fail# #)
{-# INLINE switchFailed #-}


-- | Unsafe read — caller must have ensured at least 1 byte.
switchAnyWord8Unsafe :: Parser m e Word8
switchAnyWord8Unsafe = Parser \_ _ s st ->
  case indexWord8OffAddr# s 0# of
    w# -> (# st, OK# (W8# w#) (plusAddr# s 1#) #)
{-# INLINE switchAnyWord8Unsafe #-}


{- | Peek the next byte without consuming. Used by the trie code
generator so that a wildcard-branch falling back to the
current node's terminal action does not eat a byte that
belongs to whatever runs after the switch.

(Previously the generator used 'switchAnyWord8Unsafe' for the
byte dispatch, which consumed the byte and then ran the
terminal action /past/ that byte. For input like @\"1,…\"@ on a
switch with literals @\"1\", \"1.0\", \"1.00\"@, that meant the
@\"1\"@ terminal action ran from after the comma — silently
breaking every quality-weighted Accept-* parser whose first
entry used @q=1@.)
-}
switchPeekWord8Unsafe :: Parser m e Word8
switchPeekWord8Unsafe = Parser \_env _eob s st ->
  case indexWord8OffAddr# s 0# of
    w# -> (# st, OK# (W8# w#) s #)
{-# INLINE switchPeekWord8Unsafe #-}


{- | Skip one byte (used after 'switchPeekWord8Unsafe' has
decided a child branch matches).
-}
switchSkip1 :: Parser m e ()
switchSkip1 = Parser \_env _eob s st ->
  (# st, OK# () (plusAddr# s 1#) #)
{-# INLINE switchSkip1 #-}


-- | Branch on an ensure check: if enough bytes, run @t@; else @f@.
switchBranch :: Int -> Parser m e a -> Parser m e a -> Parser m e a
switchBranch (I# n#) (Parser t) (Parser f) = Parser \env eob s st ->
  case n# <=# minusAddr# eob s of
    1# -> t env eob s st
    _ -> f env eob s st
{-# INLINE switchBranch #-}


------------------------------------------------------------------------
-- Trie
------------------------------------------------------------------------

type Rule = Maybe Int


data Trie a = Branch !a !(Map Word8 (Trie a))


nilTrie :: Trie Rule
nilTrie = Branch Nothing mempty


insertTrie :: Int -> [Word8] -> Trie Rule -> Trie Rule
insertTrie rule = go
  where
    go [] (Branch r ts) = Branch (Just $ maybe rule (min rule) r) ts
    go (c : cs) (Branch r ts) =
      Branch r (M.alter (Just . maybe (go cs nilTrie) (go cs)) c ts)


listToTrie :: [(Int, String)] -> Trie Rule
listToTrie = foldl' (\t (!r, !s) -> insertTrie r (charToBytes s) t) nilTrie


charToBytes :: String -> [Word8]
charToBytes = concatMap go
  where
    go c
      | n < 0x80 = [fromIntegral n]
      | n < 0x800 =
          [ fromIntegral (0xC0 + n `div` 64)
          , fromIntegral (0x80 + n `mod` 64)
          ]
      | otherwise = [fromIntegral n] -- simplified; ASCII-only for v1
      where
        n = ord c


mindepths :: Trie Rule -> Trie (Rule, Int)
mindepths (Branch rule ts)
  | M.null ts = Branch (rule, 0) mempty
  | otherwise =
      let ts' = M.map mindepths ts
          d =
            minimum $
              M.elems $
                M.map
                  ( \(Branch (r, depth) _) ->
                      maybe (depth + 1) (const 1) r
                  )
                  ts'
      in Branch (rule, d) ts'

------------------------------------------------------------------------
-- TH code generation
------------------------------------------------------------------------

#if MIN_VERSION_base(4,15,0)
mkDoE :: [Stmt] -> Exp
mkDoE = DoE Nothing
#else
mkDoE :: [Stmt] -> Exp
mkDoE = DoE
#endif


switch :: Q Exp -> Q Exp
switch = switchWithPost Nothing


switchWithPost :: Maybe (Q Exp) -> Q Exp -> Q Exp
switchWithPost postAction qexp = do
  post <- sequence postAction
  (cases, deflt) <- parseSwitchExp qexp
  genSwitch post cases deflt


parseSwitchExp :: Q Exp -> Q ([(String, Exp)], Maybe Exp)
parseSwitchExp qexp =
  qexp >>= \case
    CaseE (UnboundVarE _) [] -> fail "switch: empty case list"
    CaseE (UnboundVarE _) matches -> do
      let (ini, lst) = (init matches, last matches)
      cases <- forM ini \case
        Match (LitP (StringL s)) (NormalB rhs) [] -> pure (s, rhs)
        _ -> fail "switch: expected string literal pattern"
      (cases', deflt) <- case lst of
        Match (LitP (StringL s)) (NormalB rhs) [] -> pure (cases <> [(s, rhs)], Nothing)
        Match WildP (NormalB rhs) [] -> pure (cases, Just rhs)
        _ -> fail "switch: expected string literal or wildcard pattern"
      pure (cases', deflt)
    _ -> fail "switch: expected 'case _ of' expression"


genSwitch :: Maybe Exp -> [(String, Exp)] -> Maybe Exp -> Q Exp
genSwitch post cases deflt = do
  let indexed = zip [0 :: Int ..] cases
      trie = mindepths (listToTrie [(i, s) | (i, (s, _)) <- indexed])

  let ruleMap =
        M.fromList $
          (Nothing, VarE 'switchFailed)
            : [(Just i, applyPost post rhs) | (i, (_, rhs)) <- indexed]
      defltExp = maybe (VarE 'switchFailed) id deflt

  bindings <- forM (M.toList (M.insert Nothing defltExp ruleMap)) \(k, e) -> do
    n <- newName ("r" <> maybe "D" show k)
    pure (k, n, e)

  let nameMap = M.fromList [(k, n) | (k, n, _) <- bindings]

  body <- genTrieCode nameMap trie
  letE
    [valD (varP n) (normalB (pure e)) [] | (_, n, e) <- bindings]
    (pure body)


genTrieCode :: Map (Maybe Int) Name -> Trie (Rule, Int) -> Q Exp
genTrieCode names = go 0
  where
    ruleName r = case M.lookup r names of
      Just n -> n
      Nothing -> error "switch: missing rule"

    go :: Int -> Trie (Rule, Int) -> Q Exp
    go ensured (Branch (rule, depth) ts)
      | M.null ts = pure (VarE (ruleName rule))
      | otherwise = do
          let need = ensured < 1
          branches <- forM (M.toList ts) \(w, sub) -> do
            e <- go (if need then depth - 1 else ensured - 1) sub
            pure (w, InfixE (Just (VarE 'switchSkip1)) (VarE '(>>)) (Just e))

          let defE = VarE (ruleName rule)
              body =
                mkDoE
                  [ BindS (VarP (mkName "c")) (VarE 'switchPeekWord8Unsafe)
                  , NoBindS
                      ( CaseE
                          (VarE (mkName "c"))
                          ( [ Match (LitP (IntegerL (fromIntegral w))) (NormalB e) []
                            | (w, e) <- branches
                            ]
                              <> [Match WildP (NormalB defE) []]
                          )
                      )
                  ]

          if need
            then [|switchBranch depth $(pure body) $(pure defE)|]
            else pure body


applyPost :: Maybe Exp -> Exp -> Exp
applyPost Nothing rhs = rhs
applyPost (Just p) rhs = InfixE (Just p) (VarE '(>>)) (Just rhs)


------------------------------------------------------------------------
-- TH literal splices (flatparse-compatible)
------------------------------------------------------------------------

{- | @$(char \'x\')@ compiles to @word8 0xNN@ for ASCII characters,
or the appropriate multi-byte UTF-8 sequence otherwise.
Only ASCII is supported for now.
-}
char :: Char -> Q Exp
char c
  | n < 0x80 = [|word8 $(lift (fromIntegral n :: Word8))|]
  | otherwise = error ("Wireform.Parser.Switch.char: non-ASCII character: " <> show c)
  where
    n = ord c


-- | @$(string \"foo\")@ compiles to @byteString \"foo\"@.
string :: String -> Q Exp
string s = [|byteString $(lift (BSC.pack s))|]