dotparse-0.0.2: src/DotParse/FlatParse/TH.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}
{-# OPTIONS_GHC -Wno-unused-local-binds #-}
-- | TH stage restriction guff for flatparsing
module DotParse.FlatParse.TH where
import Data.ByteString hiding (head, length, reverse)
import qualified Data.ByteString.Char8 as B
import Data.Char hiding (isDigit)
import Data.Functor
import FlatParse.Basic
import Language.Haskell.TH
-- | Consume whitespace.
ws :: Parser e ()
ws =
$( switch
[|
case _ of
-- order matters
" " -> ws
"\\\n" -> ws
"\\\\\\n" -> ws
"\n" -> ws
"\t" -> ws
"\r" -> ws
";" -> ws
"//" -> lineComment
"/*" -> multilineComment
_ -> pure ()
|]
)
-- | Consume whitespace after running a parser.
token :: Parser e a -> Parser e a
token p = p <* ws
{-# INLINE token #-}
-- | Parse a line comment.
lineComment :: Parser e ()
lineComment =
withOption
anyWord8
( \case
10 -> ws
_ -> lineComment
)
(pure ())
-- | Parse a potentially nested multiline comment.
multilineComment :: Parser e ()
multilineComment = go (1 :: Int)
where
go 0 = ws
go n =
$( switch
[|
case _ of
"*/" -> go (n - 1)
"/*" -> go (n + 1)
_ -> branch anyWord8 (go n) (pure ())
|]
)
-- | Parse a HTML-Like string by counting the angle brackets
htmlLike :: Parser e String
htmlLike = ws *> $(char '<') *> go (1 :: Int) "<"
where
go 0 acc = ws $> reverse acc
go n acc =
($(char '>') *> go (n - 1) ('>' : acc))
<|> ($(char '<') *> go (n + 1) ('<' : acc))
<|> (anyChar >>= (\c -> go n (c : acc)))
-- | First character of a dot identifier.
isValidStartChar :: Char -> Bool
isValidStartChar c =
isAsciiUpper c
|| isAsciiLower c
|| ('\200' <= c && c <= '\377')
|| (c == '_')
-- | character of a dot identifier.
isValidChar :: Char -> Bool
isValidChar c = isValidStartChar c || isDigit c
-- | Read a starting character of an identifier.
identStartChar :: Parser e Char
identStartChar = satisfy isValidStartChar
-- | Read a non-starting character of an identifier.
identChar :: Parser e Char
identChar = satisfy isValidChar
-- | Parse a non-keyword string.
symbol :: String -> Q Exp
symbol str = [|token $(string str)|]
-- | Parse a keyword string.
keyword :: String -> Q Exp
keyword str = [|token ($(string str) `notFollowedBy` identChar)|]
-- | Parser a non-keyword string, throw precise error on failure.
symbol' :: String -> Q Exp
symbol' str = [|$(symbol str) `cut'` strToUtf8 str|]
-- | Parse a keyword string, throw precise error on failure.
keyword' :: String -> Q Exp
keyword' str = [|$(keyword str) `cut'` strToUtf8 str|]
-- | Parse an identifier.
ident :: Parser e ByteString
ident =
token $
byteStringOf $
identStartChar *> skipMany identChar
-- | Parse an identifier, throw a precise error on failure.
ident' :: Parser Error ByteString
ident' = ident `cut'` "identifier"
-- | A parsing error.
data Error
= -- | A precisely known error, like leaving out "in" from "let".
Precise Pos ByteString
| -- | An imprecise error, when we expect a number of different things,
-- but parse something else.
Imprecise Pos [ByteString]
deriving (Eq, Show)
-- | position of error
errorPos :: Error -> Pos
errorPos (Precise p _) = p
errorPos (Imprecise p _) = p
-- | Merge two errors. Inner errors (which were thrown at points with more consumed inputs)
-- are preferred. If errors are thrown at identical input positions, we prefer precise errors
-- to imprecise ones.
--
-- The point of prioritizing inner and precise errors is to suppress the deluge of "expected"
-- items, and instead try to point to a concrete issue to fix.
merge :: Error -> Error -> Error
merge e e' = case (errorPos e, errorPos e') of
(p, p') | p < p' -> e'
(p, p') | p > p' -> e
(p, _) -> case (e, e') of
(Precise {}, _) -> e
(_, Precise {}) -> e'
(Imprecise _ es, Imprecise _ es') -> Imprecise p (es ++ es')
{-# NOINLINE merge #-} -- merge is "cold" code, so we shouldn't inline it.
-- | Pretty print an error. The `ByteString` input is the source file. The offending line from the
-- source is displayed in the output.
prettyError :: ByteString -> Error -> ByteString
prettyError b e =
let pos :: Pos
pos = case e of
Imprecise pos _ -> pos
Precise pos _ -> pos
ls = B.lines b
(l, c) = head $ posLineCols b [pos]
line = if l < length ls then ls !! l else ""
linum = strToUtf8 $ show l
lpad = B.replicate (B.length linum) ' '
err (Precise _ e) = e
err (Imprecise _ es) = imprec es
imprec :: [ByteString] -> ByteString
imprec [] = error "impossible"
imprec [e] = e
imprec (e : es) = e <> go es
where
go [] = ""
go [e] = " or " <> e
go (e : es) = ", " <> e <> go es
in strToUtf8 (show l)
<> ":"
<> strToUtf8 (show c)
<> ":\n"
<> lpad
<> "|\n"
<> linum
<> "| "
<> line
<> "\n"
<> lpad
<> "| "
<> B.replicate c ' '
<> "^\n"
<> "parse error: expected "
<> err e
-- | Imprecise cut: we slap a list of items on inner errors.
cut :: Parser Error a -> [ByteString] -> Parser Error a
cut p es = do
pos <- getPos
cutting p (Imprecise pos es) merge
-- | Precise cut: we propagate at most a single error.
cut' :: Parser Error a -> ByteString -> Parser Error a
cut' p e = do
pos <- getPos
cutting p (Precise pos e) merge