c-expr-dsl-0.2.0.0: src/C/Expr/Parse/Infra.hs
-- | Infrastructure for parsing
module C.Expr.Parse.Infra (
-- * Parser type
Parser
, runParser
-- * Parse errors
, MacroParseError(..)
-- * Dealing with individual tokens
, token
-- * Punctuation
, punctuation
, parens
, comma
-- * Parse tokens
, TokenParser
, parseTokenOfKind
) where
import Control.Exception
import Control.Monad
import Data.Bifunctor
import Data.Text (Text)
import Data.Text qualified as Text
import GHC.Generics
import Text.Parsec hiding (runParser, token, tokens)
import Text.Parsec qualified as Parsec
import Text.Parsec.Pos
import Clang.Enum.Simple
import Clang.HighLevel.Types
import Clang.LowLevel.Core
import Clang.Paths (getSourcePath)
{-------------------------------------------------------------------------------
Parser type
-------------------------------------------------------------------------------}
type Parser = Parsec [Token SourcePath TokenSpelling] ()
-- | Run a parser on a stream of tokens
runParser ::
FilePath
-> Parser a
-> [Token SourcePath TokenSpelling]
-> Either MacroParseError a
runParser sourcePath p tokens =
first unrecognized $ Parsec.runParser p () sourcePath tokens
where
unrecognized :: ParseError -> MacroParseError
unrecognized err = MacroParseError{
parseError = show err
, parseErrorTokens = tokens
}
{-------------------------------------------------------------------------------
Parse errors
-------------------------------------------------------------------------------}
data MacroParseError = MacroParseError {
parseError :: String
, parseErrorTokens :: [Token SourcePath TokenSpelling]
}
deriving stock (Show, Eq, Generic)
deriving anyclass (Exception)
{-------------------------------------------------------------------------------
Dealing with individual tokens
-------------------------------------------------------------------------------}
token :: (Token SourcePath TokenSpelling -> Maybe a) -> Parser a
token = Parsec.token tokenPretty tokenSourcePos
where
tokenPretty :: Token SourcePath TokenSpelling -> String
tokenPretty Token{tokenKind, tokenSpelling} = concat [
show $ Text.unpack (getTokenSpelling tokenSpelling)
, " ("
, show tokenKind
, ")"
]
tokenSourcePos :: Token SourcePath a -> SourcePos
tokenSourcePos t =
newPos
(getSourcePath $ singleLocPath start)
(singleLocLine start)
(singleLocColumn start)
where
start :: SingleLoc SourcePath
start = rangeStart $ multiLocExpansion <$> tokenExtent t
tokenOfKind :: CXTokenKind -> (Text -> Maybe a) -> Parser a
tokenOfKind kind f = token $ \t ->
if fromSimpleEnum (tokenKind t) == Right kind
then f $ getTokenSpelling (tokenSpelling t)
else Nothing
tokenOfKind' :: CXTokenKind -> (Text -> Bool) -> Parser ()
tokenOfKind' kind cmp = tokenOfKind kind (\actual -> guard $ cmp actual)
{-------------------------------------------------------------------------------
Punctuation
-------------------------------------------------------------------------------}
punctuation :: Text -> Parser ()
punctuation expected = tokenOfKind' CXToken_Punctuation $
\actual -> Text.unpack expected == removeMultilines (Text.unpack actual)
parens :: Parser a -> Parser a
parens p = punctuation "(" *> p <* punctuation ")"
comma :: Parser ()
comma = punctuation ","
-- | Remove multiline characters from the string
--
-- Multiline characters are a pair of characters of the form "\\\n". These
-- characters are sometimes included in (punctuation) tokens. In other cases
-- @libclang@ handles multiline characters for us and does not report them. We
-- should remove multiline characters before comparing against a target string.
-- For example, we want @punctuation "("@ to match with a token that has
-- spelling "\\\n(".
--
-- >>> removeMultilines "a\\\ngbe\\\n"
-- "agbe"
--
removeMultilines :: String -> String
removeMultilines = \case
[] -> []
(c:cs) -> go c cs
where
go prev [] = [prev]
go '\\' ('\n':cs) = removeMultilines cs
go prev (c :cs) = prev : go c cs
{-------------------------------------------------------------------------------
Parse individual tokens
-------------------------------------------------------------------------------}
type TokenParser = Parsec Text ()
parseTokenOfKind :: CXTokenKind -> TokenParser a -> Parser a
parseTokenOfKind kind p = tokenOfKind kind $ \str ->
either (const Nothing) Just $
Parsec.parse (p <* Parsec.eof) "" str