typed-peg-0.2.0.0: src/PEG/CharSet.hs
{-# LANGUAGE BangPatterns #-}
-- | Compact character sets used by the 'PEG.Syntax.Sat' combinator.
--
-- A character class such as @[a-zA-Z0-9_]@ used to be compiled into a chain of
-- 63 ordered choices, so matching a single character could cost 63 parser
-- steps. A 'CharSet' answers the same question with one bit test.
--
-- The Latin-1 range (@\\0@ .. @\\255@), which covers essentially every class
-- that appears in a practical grammar, is stored as a 256-bit bitmap held in
-- four 'Word64's. Characters above that range fall back to a list of ranges.
module PEG.CharSet
( CharSet (..)
, memberCS
, fromRanges
, notInRanges
, fromList
, singletonCS
, complementCS
, nullCS
, anyCS
) where
import Data.Bits (setBit, testBit)
import Data.Char (chr, ord)
import Data.Word (Word64)
-- | A set of characters. The four 'Word64' fields form a bitmap of the
-- Latin-1 range; 'csWide' holds any ranges that reach beyond it.
--
-- Negation is a flag rather than an actual complement, so a negated class is
-- exactly as cheap to test as a positive one and stays exact for the whole of
-- 'Char' (complementing the ranges above Latin-1 explicitly would not).
data CharSet = CharSet
{ csNeg :: !Bool
, csB0 :: !Word64
, csB1 :: !Word64
, csB2 :: !Word64
, csB3 :: !Word64
, csWide :: ![(Char, Char)]
}
deriving (Eq, Show)
-- | Is the character a member of the set? @O(1)@ for Latin-1 characters.
memberCS :: Char -> CharSet -> Bool
memberCS c cs = csNeg cs /= rawMember c cs
{-# INLINE memberCS #-}
-- | Membership ignoring the negation flag.
rawMember :: Char -> CharSet -> Bool
rawMember c (CharSet _ b0 b1 b2 b3 wide)
| n < 64 = testBit b0 n
| n < 128 = testBit b1 (n - 64)
| n < 192 = testBit b2 (n - 128)
| n < 256 = testBit b3 (n - 192)
| otherwise = inWide wide
where
!n = ord c
inWide [] = False
inWide ((lo, hi) : rs) = (n >= ord lo && n <= ord hi) || inWide rs
{-# INLINE rawMember #-}
-- | Build a set from a list of inclusive character ranges.
fromRanges :: [(Char, Char)] -> CharSet
fromRanges = mkRanges False
-- | The complement of 'fromRanges': every character /outside/ the given
-- ranges. This is what the quasi-quoter emits for @[^\"]@.
notInRanges :: [(Char, Char)] -> CharSet
notInRanges = mkRanges True
-- | Flip a set\'s polarity.
complementCS :: CharSet -> CharSet
complementCS cs = cs { csNeg = not (csNeg cs) }
mkRanges :: Bool -> [(Char, Char)] -> CharSet
mkRanges neg rs = CharSet neg (word 0) (word 64) (word 128) (word 192) wide
where
lows = [ n | (lo, hi) <- rs, n <- [ord lo .. min 255 (ord hi)] ]
word base = go 0 lows
where
go !w [] = w
go !w (n : ns)
| n >= base && n < base + 64 = go (setBit w (n - base)) ns
| otherwise = go w ns
wide = [ (max lo (chr 256), hi) | (lo, hi) <- rs, ord hi > 255 ]
-- | Build a set from an explicit list of characters.
fromList :: [Char] -> CharSet
fromList cs = fromRanges [ (c, c) | c <- cs ]
-- | The set containing exactly one character.
singletonCS :: Char -> CharSet
singletonCS c = fromRanges [(c, c)]
-- | The set of every character. This is what @.*@ compiles to.
anyCS :: CharSet
anyCS = notInRanges []
-- | Is the set empty?
nullCS :: CharSet -> Bool
nullCS (CharSet False 0 0 0 0 []) = True
nullCS _ = False