comark-parser-0.1.0: src/Comark/ParserCombinators/Prim.hs
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
module Comark.ParserCombinators.Prim
( Position(..)
, Parser()
, runParser
, ParserState()
, ParseError(..)
, withConsumed
, consumedBy
, string
, (<?>)
, runParserWithUnconsumed
, getPosition
, setPosition
, satisfy
, peekChar
, peekLastChar
, replacing
, endOfInput
, takeWhile
, takeWhile1
, untilTheEnd
, skip
, skipWhile
, skipWhile1
, stringCaseless
, scan
, lookAhead
, notFollowedBy
) where
import Control.Applicative
import Control.Monad
import Data.String
import Data.Text (Text)
import qualified Data.Text as Text
import Prelude hiding (takeWhile)
data Position
= Position
{ line :: Int
, column :: Int
, point :: Int
} deriving (Ord, Eq)
instance Show Position where
show (Position ln cn pn) =
concat ["line ", show ln, " column ", show cn, " point ", show pn]
data ParseError
= ParseError
{ parseErrorPosition :: Position
, parseErrorReason :: String
} deriving (Show)
data ParserState
= ParserState
{ subject :: Text
, position :: Position
, lastChar :: Maybe Char
}
advance :: ParserState -> Text -> ParserState
advance = Text.foldl' go
where
go :: ParserState -> Char -> ParserState
go st c =
ParserState
{ subject = Text.drop 1 (subject st)
, position =
case c of
'\n' -> Position
{ line = line (position st) + 1
, column = 1
, point = point (position st) + 1
}
_ -> Position
{ line = line (position st)
, column = column (position st) + 1
, point = point (position st) + 1
}
, lastChar = Just c
}
newtype Parser a
= Parser
{ evalParser :: ParserState -> Either ParseError (ParserState, a) }
-- | Returns the text that was consumed by a parser alongside with its result
withConsumed :: Parser a -> Parser (a,Text)
withConsumed p = Parser $ \st ->
case (evalParser p) st of
Left err -> Left err
Right (st', res) ->
let consumedLength = point (position st') - point (position st)
in Right (st', (res, Text.take consumedLength (subject st)))
consumedBy :: Parser a -> Parser Text
consumedBy = fmap snd . withConsumed
instance Functor Parser where
fmap f (Parser g) = Parser $ \st ->
case g st of
Right (st', x) -> Right (st', f x)
Left e -> Left e
{-# INLINE fmap #-}
instance Applicative Parser where
pure x = Parser $ \st -> Right (st, x)
(Parser f) <*> (Parser g) = Parser $ \st ->
case f st of
Left e -> Left e
Right (st', h) -> case g st' of
Right (st'', x) -> Right (st'', h x)
Left e -> Left e
{-# INLINE pure #-}
{-# INLINE (<*>) #-}
instance Alternative Parser where
empty = Parser $ \st -> Left $ ParseError (position st) "(empty)"
(Parser f) <|> (Parser g) = Parser $ \st ->
case f st of
Right res -> Right res
Left (ParseError pos msg) ->
case g st of
Right res -> Right res
Left (ParseError pos' msg') -> Left $
case () of
-- return error for farthest match
_ | pos' > pos -> ParseError pos' msg'
| pos' < pos -> ParseError pos msg
| otherwise {- pos' == pos -}
-> ParseError pos (msg ++ " or " ++ msg')
{-# INLINE empty #-}
{-# INLINE (<|>) #-}
instance Monad Parser where
return x = Parser $ \st -> Right (st, x)
fail e = Parser $ \st -> Left $ ParseError (position st) e
p >>= g = Parser $ \st ->
case evalParser p st of
Left e -> Left e
Right (st',x) -> evalParser (g x) st'
{-# INLINE return #-}
{-# INLINE (>>=) #-}
instance MonadPlus Parser where
mzero = Parser $ \st -> Left $ ParseError (position st) "(mzero)"
mplus p1 p2 = Parser $ \st ->
case evalParser p1 st of
Right res -> Right res
Left _ -> evalParser p2 st
{-# INLINE mzero #-}
{-# INLINE mplus #-}
instance (a ~ Text) => IsString (Parser a) where
fromString = string . Text.pack
string :: Text -> Parser Text
string s = Parser $ \st ->
if s `Text.isPrefixOf` (subject st)
then success (advance st s) s
else failure st "string"
{-# INLINE string #-}
failure :: ParserState -> String -> Either ParseError (ParserState, a)
failure st msg = Left $ ParseError (position st) msg
{-# INLINE failure #-}
success :: ParserState -> a -> Either ParseError (ParserState, a)
success st x = Right (st, x)
{-# INLINE success #-}
(<?>) :: Parser a -> String -> Parser a
p <?> msg = Parser $ \st ->
let startpos = position st in
case evalParser p st of
Left (ParseError _ _) ->
Left $ ParseError startpos msg
Right r -> Right r
{-# INLINE (<?>) #-}
infixl 5 <?>
runParser :: Parser a -> Text -> Either ParseError a
runParser p t =
fmap snd $ evalParser p ParserState { subject = t
, position = Position 1 1 1
, lastChar = Nothing
}
runParserWithUnconsumed :: Parser a -> Text -> Either ParseError (a,Text)
runParserWithUnconsumed p t =
fmap (\(st,res) -> (res, subject st))
$ evalParser p ParserState { subject = t
, position = Position 1 1 1
, lastChar = Nothing
}
getState :: Parser ParserState
getState = Parser (\st -> success st st)
getPosition :: Parser Position
getPosition = position <$> getState
{-# INLINE getPosition #-}
satisfy :: (Char -> Bool) -> Parser Char
satisfy f = Parser g
where g st = case Text.uncons (subject st) of
Just (c, _) | f c ->
success (advance st (Text.singleton c)) c
_ -> failure st "character meeting condition"
{-# INLINE satisfy #-}
-- | Get the next character without consuming it.
peekChar :: Parser (Maybe Char)
peekChar = maybeHead . subject <$> getState
where maybeHead = fmap fst . Text.uncons
{-# INLINE peekChar #-}
-- | Get the last consumed character.
peekLastChar :: Parser (Maybe Char)
peekLastChar = lastChar <$> getState
{-# INLINE peekLastChar #-}
-- | Takes a parser that returns a 'Text', runs that
-- parser consuming some input and then prepends the
-- result to the rest of subject.
replacing :: Parser Text -> Parser ()
replacing p = do
s0 <- getState
t <- p
s1Subject <- subject <$> getState
Parser $ \_ -> success s0 { subject = Text.append t s1Subject } ()
endOfInput :: Parser ()
endOfInput = Parser $ \st ->
if Text.null (subject st)
then success st ()
else failure st "end of input"
{-# INLINE endOfInput #-}
-- note: this does not actually change the position in the subject;
-- it only changes what column counts as column N. It is intended
-- to be used in cases where we're parsing a partial line but need to
-- have accurate column information.
setPosition :: Position -> Parser ()
setPosition pos = Parser $ \st -> success st{ position = pos } ()
{-# INLINE setPosition #-}
takeWhile :: (Char -> Bool) -> Parser Text
takeWhile f = Parser $ \st ->
let t = Text.takeWhile f (subject st) in
success (advance st t) t
{-# INLINE takeWhile #-}
takeWhile1 :: (Char -> Bool) -> Parser Text
takeWhile1 f = Parser $ \st ->
case Text.takeWhile f (subject st) of
t | Text.null t -> failure st "characters satisfying condition"
| otherwise -> success (advance st t) t
{-# INLINE takeWhile1 #-}
-- | Consumes all the available input (until endOfInput) and returns it.
untilTheEnd :: Parser Text
untilTheEnd = Parser $ \st ->
success (advance st (subject st)) (subject st)
{-# INLINE untilTheEnd #-}
skip :: (Char -> Bool) -> Parser ()
skip f = Parser $ \st ->
case Text.uncons (subject st) of
Just (c,_) | f c -> success (advance st (Text.singleton c)) ()
_ -> failure st "character satisfying condition"
{-# INLINE skip #-}
skipWhile :: (Char -> Bool) -> Parser ()
skipWhile f = Parser $ \st ->
let t' = Text.takeWhile f (subject st) in
success (advance st t') ()
{-# INLINE skipWhile #-}
skipWhile1 :: (Char -> Bool) -> Parser ()
skipWhile1 f = Parser $ \st ->
case Text.takeWhile f (subject st) of
t' | Text.null t' -> failure st "characters satisfying condition"
| otherwise -> success (advance st t') ()
{-# INLINE skipWhile1 #-}
stringCaseless :: Text -> Parser Text
stringCaseless (Text.toCaseFold -> s) = Parser $ \st ->
if Text.toCaseFold s `Text.isPrefixOf` Text.toCaseFold (subject st)
then success (advance st s) s
else failure st "stringCaseless"
{-# INLINE stringCaseless #-}
scan :: s -> (s -> Char -> Maybe s) -> Parser Text
scan s0 f = Parser $ go s0 []
where go s cs st =
case Text.uncons (subject st) of
Nothing -> finish st cs
Just (c, _) -> case f s c of
Just s' -> go s' (c:cs)
(advance st (Text.singleton c))
Nothing -> finish st cs
finish st cs =
success st (Text.pack (reverse cs))
{-# INLINE scan #-}
lookAhead :: Parser a -> Parser a
lookAhead p = Parser $ \st ->
either
(const (failure st "lookAhead"))
(success st . snd)
(evalParser p st)
{-# INLINE lookAhead #-}
notFollowedBy :: Parser a -> Parser ()
notFollowedBy p = Parser $ \st ->
either
(const (success st ()))
(const (failure st "notFollowedBy"))
(evalParser p st)
{-# INLINE notFollowedBy #-}