packages feed

typed-peg-0.2.0.0: src/PEG/Stream.hs

{-# LANGUAGE BangPatterns      #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MagicHash         #-}
{-# LANGUAGE UnboxedSums       #-}
{-# LANGUAGE UnboxedTuples     #-}

-- | Input streams the parser can consume.
--
-- A 'Stream' is anything the parser can read one 'Char' at a time and slice
-- chunks out of.  Instances are provided for 'String', strict and lazy
-- 'Data.Text.Text', and strict and lazy 'Data.ByteString.ByteString'.
--
-- == ByteString is Latin-1
--
-- The 'ByteString' instances read each byte as the 'Char' with that code
-- point, exactly as "Data.ByteString.Char8" does.  This is what makes them
-- fast — every character lands in the Latin-1 range that
-- "PEG.CharSet" answers with a single bit test — and it is correct for
-- grammars over ASCII or Latin-1 text.  It is /wrong/ for UTF-8: a
-- multi-byte character arrives as its individual bytes, and columns and
-- offsets count bytes rather than characters.  Decode to 'Data.Text.Text'
-- first if that matters.
--
-- Two laws follow, and only the 'ByteString' instances need the caveat:
--
-- * @'chunkToString' . 'packString' == 'id'@, for arguments in the range the
--   stream can represent (all of 'Char' except for 'ByteString', where it is
--   @\'\\0\'@ .. @\'\\255\'@).
-- * A 'PEG.CharSet.CharSet' containing only characters above @\'\\255\'@
--   never matches a 'ByteString', with no diagnostic.
--
-- == Writing an instance
--
-- Only 'unconsS' has no default.  Everything else is derived from it, so a
-- minimal instance is one method — but a type with native slicing should
-- override 'spanS', 'takeS', 'lengthS' and 'foldlS'' , which is where the
-- performance of 'Text' and 'ByteString' comes from.
module PEG.Stream
  ( Stream (..)
  ) where

import Data.Char (chr, ord)

import qualified Data.ByteString            as B
import qualified Data.ByteString.Char8      as BC
import qualified Data.ByteString.Lazy       as BL
import qualified Data.ByteString.Lazy.Char8 as BLC
import qualified Data.Text                  as T
import qualified Data.Text.Lazy             as TL

-- | A sequence of characters the parser can consume.
--
-- The chunk type is the stream type itself: slicing a 'Data.Text.Text'
-- yields a 'Data.Text.Text', so a character class such as @[a-z]+@ produces
-- a real slice rather than unpacking into a @['Char']@.
class Stream s where
  -- | Split off the first character.
  --
  -- This returns an unboxed sum rather than @'Maybe' ('Char', s)@ on
  -- purpose.  It is called once per character of input, and the boxed
  -- version would allocate a @Just@ and a pair every time — behind a class
  -- dictionary GHC cannot cancel them, so the parser's zero-allocation
  -- terminal path would be lost.
  unconsS :: s -> (# (# #) | (# Char, s #) #)

  -- | @'spanS' p s@ splits @s@ into the longest prefix all of whose
  -- characters satisfy @p@, and the rest.
  spanS :: (Char -> Bool) -> s -> (s, s)

  -- | Strict left fold over the characters.  Used to advance the column
  -- across a chunk that has already been matched in bulk.
  foldlS' :: (b -> Char -> b) -> b -> s -> b

  -- | Prepend a character.  @O(1)@ for 'String' and the lazy types; the
  -- strict types must copy.
  consS :: Char -> s -> s

  -- | @'takeS' n s@ is the first @n@ characters of @s@.
  takeS :: Int -> s -> s

  -- | Number of characters.
  lengthS :: s -> Int

  -- | Is the stream empty?
  nullS :: s -> Bool

  -- | Convert a chunk to a 'String'.  Semantic actions need this whenever a
  -- character class feeds something that expects a 'String', such as 'read'.
  chunkToString :: s -> String

  -- | Build a chunk from a 'String'.
  packString :: String -> s

  -- Defaults, all in terms of 'unconsS'.

  spanS p s0 = go id s0
    where
      go acc s = case unconsS s of
        (# | (# c, s' #) #) | p c -> go (acc . (c :)) s'
        _                         -> (packString (acc []), s)

  foldlS' f = go
    where
      go !acc s = case unconsS s of
        (# | (# c, s' #) #) -> go (f acc c) s'
        _                   -> acc

  consS c s = packString (c : chunkToString s)

  takeS n0 s0 = packString (go n0 s0)
    where
      go n s
        | n <= 0    = []
        | otherwise = case unconsS s of
            (# | (# c, s' #) #) -> c : go (n - 1) s'
            _                   -> []

  lengthS = foldlS' (\ !n _ -> n + 1) 0

  nullS s = case unconsS s of
    (# (# #) | #) -> True
    _             -> False

  chunkToString s = case unconsS s of
    (# | (# c, s' #) #) -> c : chunkToString s'
    _                   -> []

  {-# MINIMAL unconsS, packString #-}

--------------------------------------------------------------------------------
-- String
--------------------------------------------------------------------------------

instance Stream [Char] where
  unconsS (c : cs) = (# | (# c, cs #) #)
  unconsS []       = (# (# #) | #)
  {-# INLINE unconsS #-}

  -- NOT 'Data.List.span': that one is lazy in its pair, so it allocates a
  -- tuple and two selector thunks for every character it accepts.  Finding
  -- the split point first and slicing costs one tuple in total.
  spanS p s0    = go (0 :: Int) s0
    where
      go !n s = case s of
        (c : cs) | p c -> go (n + 1) cs
        _              -> (take n s0, s)
  foldlS' f     = go
    where
      go !acc (c : cs) = go (f acc c) cs
      go !acc []       = acc
  consS         = (:)
  takeS         = take
  lengthS       = length
  nullS         = null
  chunkToString = id
  packString    = id
  {-# INLINE spanS #-}
  {-# INLINE foldlS' #-}
  {-# INLINE consS #-}
  {-# INLINE takeS #-}
  {-# INLINE lengthS #-}
  {-# INLINE nullS #-}
  {-# INLINE chunkToString #-}
  {-# INLINE packString #-}

--------------------------------------------------------------------------------
-- Text
--------------------------------------------------------------------------------

instance Stream T.Text where
  unconsS t = case T.uncons t of
    Just (c, t') -> (# | (# c, t' #) #)
    Nothing      -> (# (# #) | #)
  {-# INLINE unconsS #-}

  spanS         = T.span
  foldlS'       = T.foldl'
  consS         = T.cons
  takeS         = T.take
  lengthS       = T.length
  nullS         = T.null
  chunkToString = T.unpack
  packString    = T.pack
  {-# INLINE spanS #-}
  {-# INLINE foldlS' #-}
  {-# INLINE takeS #-}
  {-# INLINE lengthS #-}
  {-# INLINE nullS #-}

instance Stream TL.Text where
  unconsS t = case TL.uncons t of
    Just (c, t') -> (# | (# c, t' #) #)
    Nothing      -> (# (# #) | #)
  {-# INLINE unconsS #-}

  spanS         = TL.span
  foldlS'       = TL.foldl'
  consS         = TL.cons
  takeS n       = TL.take (fromIntegral n)
  lengthS       = fromIntegral . TL.length
  nullS         = TL.null
  chunkToString = TL.unpack
  packString    = TL.pack
  {-# INLINE spanS #-}
  {-# INLINE foldlS' #-}
  {-# INLINE nullS #-}

--------------------------------------------------------------------------------
-- ByteString (Latin-1)
--------------------------------------------------------------------------------

-- | Byte to character, Latin-1.
w2c :: Int -> Char
w2c = chr
{-# INLINE w2c #-}

instance Stream B.ByteString where
  unconsS b = case B.uncons b of
    Just (w, b') -> (# | (# w2c (fromIntegral w), b' #) #)
    Nothing      -> (# (# #) | #)
  {-# INLINE unconsS #-}

  spanS         = BC.span
  foldlS'       = BC.foldl'
  consS         = BC.cons
  takeS         = B.take
  lengthS       = B.length
  nullS         = B.null
  chunkToString = BC.unpack
  -- 'BC.pack' truncates characters above '\255'; clamp explicitly so the
  -- behaviour is the documented one rather than whatever pack happens to do.
  packString    = BC.pack . map clampLatin1
  {-# INLINE spanS #-}
  {-# INLINE foldlS' #-}
  {-# INLINE takeS #-}
  {-# INLINE lengthS #-}
  {-# INLINE nullS #-}

instance Stream BL.ByteString where
  unconsS b = case BL.uncons b of
    Just (w, b') -> (# | (# w2c (fromIntegral w), b' #) #)
    Nothing      -> (# (# #) | #)
  {-# INLINE unconsS #-}

  spanS         = BLC.span
  foldlS'       = BLC.foldl'
  consS         = BLC.cons
  takeS n       = BL.take (fromIntegral n)
  lengthS       = fromIntegral . BL.length
  nullS         = BL.null
  chunkToString = BLC.unpack
  packString    = BLC.pack . map clampLatin1
  {-# INLINE spanS #-}
  {-# INLINE foldlS' #-}
  {-# INLINE nullS #-}

-- | Characters a 'ByteString' cannot represent become @\'\\255\'@ rather
-- than silently wrapping around modulo 256.
clampLatin1 :: Char -> Char
clampLatin1 c
  | ord c > 255 = '\255'
  | otherwise   = c
{-# INLINE clampLatin1 #-}