aihc-cabal-syntax-1.0.0.1: src/Aihc/Cabal/Internal/Lexer.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Read the outline of a Cabal file: fields, sections, and their positions.
-- This module follows the lexer and the outline parser of Cabal-syntax 3.12.
module Aihc.Cabal.Internal.Lexer
( Field (..), SectionArg (..), readFields, sectionArgText
) where
import Data.Char (isAsciiUpper, ord, toLower)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Aihc.Cabal.Internal.Types (Diagnostic (..), FieldLine (..), Position (..))
data Field
= Field !Position Text [FieldLine]
| Section !Position Text [SectionArg] [Field]
deriving Show
data SectionArg
= ArgName !Position Text
| ArgString !Position Text
| ArgOther !Position Text
deriving Show
sectionArgText :: SectionArg -> Text
sectionArgText (ArgName _ t) = t
sectionArgText (ArgString _ t) = t
sectionArgText (ArgOther _ t) = t
data Token
= TokSym Text | TokStr Text | TokOther Text | Indent Int | TokFieldLine Text
| Colon | OpenBrace | CloseBrace | EOF | LexicalError
deriving (Eq, Show)
data Mode = BolSection | InSection | BolFieldLayout | InFieldLayout | BolFieldBraces | InFieldBraces
deriving (Eq, Show)
-- | The state before a token. The token and the next state are computed
-- on demand, as in the Cabal-syntax parser.
data LexState = LexState
{ lexRow :: !Int
, lexColumn :: !Int
, lexInput :: !Text
, lexMode :: !Mode
}
data Stream = Stream LexState (Position, Token, Stream)
stream :: LexState -> Stream
stream st = Stream st (lexToken st)
setMode :: Mode -> Stream -> Stream
setMode m (Stream st _) = stream st {lexMode = m}
peek :: Stream -> (Position, Token)
peek (Stream _ (p, t, _)) = (p, t)
advance :: Stream -> Stream
advance (Stream _ (_, _, next)) = next
-- | The number of bytes in the UTF-8 encoding. Cabal-syntax counts columns in bytes.
width :: Char -> Int
width c
| n < 0x80 = 1
| n < 0x800 = 2
| n < 0x10000 = 3
| otherwise = 4
where n = ord c
widthOf :: Text -> Int
widthOf = T.foldl' (\a c -> a + width c) 0
isSpaceTab :: Char -> Bool
isSpaceTab c = c == ' ' || c == '\t'
-- | Characters in the Cabal lexer class @$printable@.
isPrintable :: Char -> Bool
isPrintable c = c > '\x1f' && c /= '\x7f'
isSymbol' :: Char -> Bool
isSymbol' c = c `elem` (",=<>+*&|!$%^@#?/\\~" :: String)
isNameChar :: Char -> Bool
isNameChar c = isPrintable c && not (c `elem` (" :\"{}()[]" :: String)) && not (isSymbol' c)
isOpChar :: Char -> Bool
isOpChar c = isSymbol' c || c == '-' || c == '.'
isParen :: Char -> Bool
isParen c = c `elem` ("()[]" :: String)
-- | Split a line break at the start of the text: @\\n@, @\\r\\n@, or @\\r@.
newline :: Text -> Maybe Text
newline t = case T.uncons t of
Just ('\n', rest) -> Just rest
Just ('\r', rest) -> Just (fromMaybe rest (T.stripPrefix "\n" rest))
_ -> Nothing
lexToken :: LexState -> (Position, Token, Stream)
lexToken st@(LexState row col input mode) = case mode of
BolSection -> bol st $ \ws rest -> case T.uncons rest of
Just ('{', r) | T.all isSpaceTab ws -> token col OpenBrace (LexState row (col + T.length ws + 1) r BolSection)
Just ('}', r) | T.all isSpaceTab ws -> token col CloseBrace (LexState row (col + T.length ws + 1) r BolSection)
_ -> indentation ws rest InSection
BolFieldLayout -> bol st $ \ws rest -> indentation ws rest InFieldLayout
BolFieldBraces -> bol st $ \_ _ -> lexToken st {lexMode = InFieldBraces}
InSection -> case T.uncons input of
Nothing -> token col EOF st
Just (c, rest)
| isSpaceTab c -> let (ws, r) = T.span isSpaceTab input in lexToken st {lexColumn = col + T.length ws, lexInput = r}
| "--" `T.isPrefixOf` input ->
let (comment, r) = T.span isCommentChar input
in lexToken st {lexColumn = col + widthOf comment, lexInput = r}
| Just r <- newline input -> lexToken (LexState (row + 1) 1 r BolSection)
| c == ':' -> token col Colon st {lexColumn = col + 1, lexInput = rest}
| c == '{' -> token col OpenBrace st {lexColumn = col + 1, lexInput = rest}
| c == '}' -> token col CloseBrace st {lexColumn = col + 1, lexInput = rest}
| c == '"' -> case stringToken rest of
Just (s, r) -> token col (TokStr s) st {lexColumn = col + widthOf s + 2, lexInput = r}
Nothing -> token col LexicalError st {lexInput = ""}
| isParen c -> token col (TokOther (T.singleton c)) st {lexColumn = col + 1, lexInput = rest}
| otherwise ->
-- The longest match wins. A name wins a tie with an operator.
let nameLen = T.length (T.takeWhile isNameChar input)
opLen = T.length (T.takeWhile isOpChar input)
in if nameLen == 0 && opLen == 0 then token col LexicalError st {lexInput = ""}
else if nameLen >= opLen then word TokSym nameLen
else word TokOther opLen
InFieldLayout -> fieldLine (const True)
InFieldBraces -> case T.uncons input of
Just ('{', rest) -> token col OpenBrace st {lexColumn = col + 1, lexInput = rest}
Just ('}', rest) -> token col CloseBrace st {lexColumn = col + 1, lexInput = rest}
_ -> fieldLine (\c -> c /= '{' && c /= '}')
where
token c t next = (Position row c, t, stream next)
word constructor n =
let (w, rest) = T.splitAt n input
in token col (constructor w) st {lexColumn = col + widthOf w, lexInput = rest}
isCommentChar c = isPrintable c || c == '\t'
-- Skip blank lines and comment lines at the start of a line.
bol s k =
let (ws, rest) = T.span (\c -> isSpaceTab c || c == '\xa0') (lexInput s)
in case newline rest of
Just r -> lexToken s {lexRow = lexRow s + 1, lexColumn = 1, lexInput = r}
Nothing
| "--" `T.isPrefixOf` T.dropWhile isSpaceTab (lexInput s) ->
let (sp, afterSp) = T.span isSpaceTab (lexInput s)
(comment, r) = T.span isCommentChar afterSp
in lexToken s {lexColumn = lexColumn s + T.length sp + widthOf comment, lexInput = r}
| otherwise -> k ws rest
indentation ws rest next
| T.null rest = (Position row col, EOF, stream st)
| otherwise =
let n = T.length ws
in token col (Indent n) (LexState row (col + n) rest next)
fieldLine allowed = case T.uncons input of
Nothing -> token col EOF st
Just (c, _)
| isSpaceTab c -> let (ws, r) = T.span isSpaceTab input in lexToken st {lexColumn = col + T.length ws, lexInput = r}
| Just r <- newline input ->
lexToken (LexState (row + 1) 1 r (if mode == InFieldLayout then BolFieldLayout else BolFieldBraces))
| isPrintable c && allowed c ->
let (line, r) = T.span (\x -> (isPrintable x || x == '\t') && allowed x) input
in token col (TokFieldLine line) st {lexColumn = col + widthOf line, lexInput = r}
| otherwise -> token col LexicalError st {lexInput = ""}
-- | The contents of a string token without its quotes. Escapes stay in the text.
-- The lexer uses the longest match: a quotation mark after a backslash can end
-- the string or continue it.
stringToken :: Text -> Maybe (Text, Text)
stringToken t = (\n -> (T.take n t, T.drop (n + 1) t)) <$> go Nothing ' ' 0 (T.unpack t)
where
go :: Maybe Int -> Char -> Int -> String -> Maybe Int
go end _ _ [] = end
go end previous !n (c : cs)
| c == '"' = if previous == '\\' then go (Just n) c (n + 1) cs else Just n
| isPrintable c = go end c (n + 1) cs
| otherwise = end
type Parse a = Stream -> Either Diagnostic (a, Stream)
parseError :: Position -> Token -> Either Diagnostic a
parseError p t = Left (Diagnostic (Just p) ("Unexpected " <> describe t))
where
describe x = case x of
TokSym s -> "symbol " <> T.pack (show s)
TokStr s -> "string " <> T.pack (show s)
TokOther s -> "operator " <> T.pack (show s)
Indent _ -> "new line"
TokFieldLine _ -> "field content"
Colon -> "\":\""
OpenBrace -> "\"{\""
CloseBrace -> "\"}\""
EOF -> "end of file"
LexicalError -> "character in input"
lowerName :: Text -> Text
lowerName = T.map (\c -> if isAsciiUpper c then toLower c else c)
-- | Read the fields and sections of a file. The input has no byte order mark.
readFields :: Text -> Either Diagnostic [Field]
readFields input = do
(fields, s) <- elements 0 (stream (LexState 1 1 input BolSection))
case peek s of
(_, EOF) -> Right fields
(p, t) -> parseError p t
elements :: Int -> Parse [Field]
elements level = go []
where
go acc s = case peek s of
(_, Indent j) | j >= level -> do
let s1 = advance s
case peek s1 of
(p, TokSym n) -> do
(f, s2) <- layoutElement (j + 1) p (lowerName n) (advance s1)
go (f : acc) s2
(p, t) -> parseError p t
(p, TokSym n) -> do
(f, s1) <- bracesElement p (lowerName n) (advance s)
go (f : acc) s1
_ -> Right (reverse acc, s)
sectionArgs :: Stream -> ([SectionArg], Stream)
sectionArgs = go []
where
go acc s = case peek s of
(p, TokSym x) -> go (ArgName p x : acc) (advance s)
(p, TokStr x) -> go (ArgString p x : acc) (advance s)
(p, TokOther x) -> go (ArgOther p x : acc) (advance s)
_ -> (reverse acc, s)
layoutElement :: Int -> Position -> Text -> Parse Field
layoutElement level p n s = case peek s of
(_, Colon) -> fieldLayoutOrBraces level p n (advance s)
_ -> do
let (args, s1) = sectionArgs s
case peek s1 of
(_, OpenBrace) -> do
(fields, s2) <- bracesBody (advance s1)
Right (Section p n args fields, s2)
_ -> do
(fields, s2) <- elements level s1
Right (Section p n args fields, s2)
bracesElement :: Position -> Text -> Parse Field
bracesElement p n s = case peek s of
(_, Colon) -> do
let s1 = advance s
case peek s1 of
(_, OpenBrace) -> fieldBraces p n (advance s1)
_ -> do
let s2 = setMode InFieldBraces s1
case peek s2 of
(q, TokFieldLine l) -> Right (Field p n [FieldLine q l], setMode InSection (advance s2))
_ -> Right (Field p n [], setMode InSection s2)
_ -> do
let (args, s1) = sectionArgs s
case peek s1 of
(_, OpenBrace) -> do
(fields, s2) <- bracesBody (advance s1)
Right (Section p n args fields, s2)
(q, t) -> parseError q t
bracesBody :: Parse [Field]
bracesBody s = do
(fields, s1) <- elements 0 s
let s2 = case peek s1 of
(_, Indent _) -> advance s1
_ -> s1
case peek s2 of
(_, CloseBrace) -> Right (fields, advance s2)
(q, t) -> parseError q t
fieldLayoutOrBraces :: Int -> Position -> Text -> Parse Field
fieldLayoutOrBraces level p n s = case peek s of
(_, OpenBrace) -> fieldBraces p n (advance s)
_ -> do
let s1 = setMode InFieldLayout s
(first, s2) = case peek s1 of
(q, TokFieldLine l) -> ([FieldLine q l], advance s1)
_ -> ([], s1)
rest acc st = case peek st of
(_, Indent j) | j >= level -> case peek (advance st) of
(q, TokFieldLine l) -> rest (FieldLine q l : acc) (advance (advance st))
(q, t) -> parseError q t
_ -> Right (reverse acc, st)
(more, s3) <- rest [] s2
Right (Field p n (first ++ more), setMode InSection s3)
fieldBraces :: Position -> Text -> Parse Field
fieldBraces p n s = do
let go acc st = case peek st of
(q, TokFieldLine l) -> go (FieldLine q l : acc) (advance st)
_ -> (reverse acc, setMode InSection st)
(ls, s1) = go [] (setMode InFieldBraces s)
case peek s1 of
(_, CloseBrace) -> Right (Field p n ls, advance s1)
(q, t) -> parseError q t