packages feed

fast-tags-2.0.0: src/FastTags/Lexer.x

{
----------------------------------------------------------------------------
-- |
-- Module      :  FastTags.Lexer
-- Copyright   :  (c) Sergey Vinokurov 2019
----------------------------------------------------------------------------

{-# LANGUAGE BangPatterns        #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE NamedFieldPuns      #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}

{-# OPTIONS_GHC -fno-warn-tabs #-}

module FastTags.Lexer (tokenize) where

import Control.Applicative as A
import Control.Monad
import Control.Monad.Writer.Strict
import Control.Monad.State.Strict

import qualified Data.ByteString as BS
import Data.Text (Text)
import qualified Data.Text as T
import Data.Void (Void, absurd)
import Data.Word
import Foreign.Ptr (plusPtr)

import FastTags.LensBlaze
import FastTags.LexerM
import FastTags.LexerTypes
import FastTags.Token

}

$ascspace  = [\ \t\r]
$unispace  = \x01
$space     = [$ascspace $unispace]
$nl        = [\n]
$ws        = [$space\f\v] # $nl

$dot       = [\.]

$asclarge  = [A-Z]
$unilarge  = \x02
$large     = [$asclarge $unilarge]

$ascsmall  = [a-z]
$unismall  = \x03
$small     = [$ascsmall $unismall]

-- These symbols can be part of operators but are reserved when occur by
-- themselves.
$symbols_reserved_as_standalone = [ \→ \∷ \⇒ \∀ ]

$special_sym  = [\(\)\,\;\[\]\`\{\}]
$ascsymbol    = [\!\#\$\%\&\*\+\.\/\<\=\>\?\@\\\^\|\-\~\:]
$unisymbol    = \x04
$symbol       = [$ascsymbol $unisymbol $symbols_reserved_as_standalone] # [$special_sym \_\'\"]

$ascident  = [$ascsmall $asclarge]
$uniident  = [$unismall $unilarge]
$ascdigit  = [0-9]
$unidigit  = \x05
$digit     = [$ascdigit $unidigit]
$unisuffix = \x06
$ident_nonsym = [$ascident $uniident $unisuffix $digit] # [$symbol]
$ident_syms   = [\'\_\#]
$ident     = [$ident_nonsym $ident_syms]

-- Stands for "→", "∷", "⇒", "⦇", "⦈", "∀", "⟦", "⟧"
$reserved_symbol = \x07

-- Random characters for which there's no category.
$other = \x08

-- Some rules assume that there's an optional \r before \n so don't change this definition.
@nl = ( [\r]? $nl )

@qualificationPrefix = ( $large $ident* $dot )*

@arrow       =  "->"
@doublecolon =  "::"
@implies     =  "=>"

@lbanana     =  "(|"
@rbanana     =  "|)"

$charesc    = [a b f n r t v \\ \" \' \&]
$octdigit   = [0-7]
$hexdigit   = [0-9a-fA-F]
@charescape = [\\] ( $charesc | $asclarge+ | "o" $octdigit+ | "x" $hexdigit+ )

@float_number =  ( [\+\-]? ( $digit+ ( "." $digit+ )? | $digit* "." $digit+ ) ( [eE] [\+\-]? $digit* )? )

@number = ( [\+\-]? $digit+ | 0 ( [oO] $octdigit+ | [xX] $hexdigit ) | @float_number )

@cpp_ws          = ( $ascspace | [\\] @nl )
@cpp_opt_ws      = @cpp_ws*
@cpp_nonempty_ws = ( $ascspace @cpp_ws* | @cpp_ws* $ascspace )
@cpp_dir_start   = "#"+ @cpp_opt_ws
@define_name     = [$ascident $ascdigit _ ']+
@define_body     = ( [^ \\ $nl]+ | [\\] ( @nl | . ) )+ @nl

-- Except "define"
@cppdirective = ( "if" | "ifdef" | "ifndef" | "endif" | "elif" | "else" | "undef" | "line" | "error" | "warning" | "include" )

-- Except cpp directives, "let", and "enum".
@hscdirective = ( "def" | "const" | "const_str" | "type" | "peek" | "poke" | "ptr" | "offset" | "size" | "alignment" )

-- Except "enum"
@all_cpp_and_hsc_directives = ( "define" | "let" | @cppdirective | @hscdirective )

:-

-- Literate Haskell support. 'literate' code handles all text except actual
-- Haskell program text. It aims to strip all non-Haskell text.
<literate> {
@nl ">" $ws*
  { \input len -> (Newline $! countInputSpace input len) <$ startLiterateBird }
@nl "\begin{code}" @nl $space*
  { \input len -> (Newline $! countInputSpace input len) <$ startLiterateLatex }
@nl ;
.
  { \_ _ -> dropUntilNL' }
}

-- Drop shebang
<0, literate> $nl "#!" .* ;

-- Can skip whitespace everywhere since it does not affect meaning in any
-- state.
<0, comment, qq, literate> $ws+ ;

<0> {

-- Analyse "#if 0" constructs used in e.g. GHC.Base
^ @cpp_dir_start "if" @cpp_opt_ws "0" ( @cpp_nonempty_ws .* )?
  { \_ _ -> startPreprocessorStripping }

-- Named defines, implicitly drops: '( @cpp_ws+ | "(" ) @define_body'
^ @cpp_dir_start ("define" | "let") @cpp_ws+ @define_name
  { \input len -> do
    modify $ \s -> s { asInput = dropUntilUnescapedNL $ asInput s }
    pure $! CppDefine $! extractDefineOrLetName input len
  }

^ @cpp_dir_start @cppdirective .* ( [\\] @nl .* )* ;

^ @cpp_dir_start ("{" (@cpp_ws | @nl)*)? "enum"
  { \_ _ -> pure HSCEnum }
^ @cpp_dir_start @all_cpp_and_hsc_directives
  { \_ _ -> pure HSCDirective }
^ @cpp_dir_start "{" (@cpp_ws | @nl)* @all_cpp_and_hsc_directives
  { \_ _ -> pure HSCDirectiveBraced }

-- Drop everything else that starts with #, e.g.
-- '# 17 "/usr/include/stdc-predef.h" 3 4'
-- #{get_area "Queue.T"}

^ "#" $ascspace* "{"? @define_name+
  { \_ _ -> dropUntilNL' }

}

<stripCpp> {
@cpp_dir_start @cpp_opt_ws ( "ifdef" | "if" ) .*
  { \_ _ -> startPreprocessorStripping }
@cpp_dir_start @cpp_opt_ws ("else" | "elif" | "endif") .*
  { \_ _ -> endPreprocessorStripping }
@cpp_dir_start @cpp_opt_ws ( "define" | "undef" | "line" | "error" | "warning" | "include"  | "let" )
  { \_ _ -> dropUntilNL' }
$nl ;
.
  { \_ _ -> dropUntilNLOr' 35 -- '#'
  }
}

-- Newlines and comments.
<0> {

@nl ">" $space*
  / { isLiterateEnabled' }
  { \input len -> pure $! Newline $! countInputSpace input len }
@nl
  / { shouldEndLiterateBird }
  { \_ _   -> Newline 0 <$ endLiterate }
@nl "\end{code}"
  / { shouldEndLiterateLatex }
  { \_ _   -> endLiterate' }

[\\]? @nl $space* "{-"  { \input len -> startIndentationCounting (countInputSpace input len) }
[\\]? @nl $space*       { \input len -> pure $! Newline $! len - countBackslashCR input - 1 }
[\-][\-]+ ~[$symbol $nl] { \_ _ -> dropUntilNL' }
[\-][\-]+ / @nl         ;

}

-- Nested comments
<0, comment>
  "{-"                  { \_ _ -> startComment }
<comment> "-}"          { \_ _ -> endComment startCode }
-- 45  - '-'
-- 123 - '{'
<comment> @nl ;
<comment> ($other | .)  { \_ _ -> dropUntilNLOrEither' 45 123 }
<0> "-}"                { \_ _ -> errorAtLine "Unmatched -}" }

<indentComment>
  "{-"                  { \_ _ -> startIndentComment }
<indentComment> "-}"    { \_ _ -> endComment indentCountCode }
<indentComment> (. | @nl) ;

<indentCount> {
$space* "{-"            { \input len -> addIndentationSize (fromIntegral (countInputSpace input len)) *> startIndentComment }
$space*                 { \_ len -> endIndentationCounting len }
}

<0> {
-- Strings
[\"] ( [^\" \\ \r \n] | [\\] . | [\\] @nl ( $ws* [\\] )? )* [\"]
                        { \_ _ -> pure String }

[\"]                    { \_ _ -> pure DQuote }

-- Characters.
[\'] ( [^ \' \\ \n \r] | @charescape ) [\']
                        { kw Character }
}

-- Template Haskell quasiquoters

<0> {
"[|"                    { \_ _ -> startUnconditionalQuasiQuoter }
"[" [\$\(]* @qualificationPrefix $ident+ [\)]*  "|"
                        { \input _ -> startQuasiquoter input }
"$("                    { \_ _ -> startSplice CtxHaskell }
^ "$" [\']* @qualificationPrefix $ident+
                        { \_ _ -> pure ToplevelSplice }

}

<qq> {
"$("                    { \_ _ -> startSplice CtxQuasiquoter }
"|]"                    { \_ _ -> endQuasiquoter }
$reserved_symbol        { \input _len -> reservedSymbolQQ (unsafeTextHead (aiPtr input)) }
(. | @nl)               ;
}

-- Vanilla tokens
<0> {

"case"                  { kw KWCase }
"class"                 { kw KWClass }
"data"                  { kw KWData }
"default"               { kw KWDefault }
"deriving"              { kw KWDeriving }
"do"                    { kw KWDo }
"else"                  { kw KWElse }
"family"                { kw KWFamily }
"forall"                { \_ _ -> pure forallTokenVal }
"foreign"               { kw KWForeign }
"if"                    { kw KWIf }
"import"                { kw KWImport }
"in"                    { kw KWIn }
"infix"                 { kw KWInfix }
"infixl"                { kw KWInfixl }
"infixr"                { kw KWInfixr }
"instance"              { kw KWInstance }
"let"                   { kw KWLet }
"module"                { kw KWModule }
"newtype"               { kw KWNewtype }
"of"                    { kw KWOf }
"pattern"               { \_ _ -> pure patternTokenVal }
"then"                  { kw KWThen }
"type"                  { kw KWType }
"where"                 { kw KWWhere }
@arrow                  { kw Arrow }
"@"                     { kw At }
"`"                     { kw Backtick }
","                     { kw Comma }
"."                     { kw Dot }
@doublecolon            { kw DoubleColon }
"="                     { kw Equals }
"!"                     { kw ExclamationMark }
@implies                { kw Implies }
"{"                     { kw LBrace }
"["                     { kw LBracket }
"("                     { pushLParen }
"|"                     { kw Pipe }
"}"                     { kw RBrace }
"]"                     { kw RBracket }
")"                     { popRParen }
"~"                     { kw Tilde }
";"                     { kw Semicolon }

[\\]                    { kw LambdaBackslash }

-- Not interested in numbers, but it takes time to extract their text so
-- it's quicker to just ignore them.
@number                 { kw Number }

[\']* @qualificationPrefix ($ident | $large)+
                        { \input len -> pure $! T $! takeText input len }
@qualificationPrefix $symbol+
                        { \input len -> pure $! T $! takeText input len }

$reserved_symbol        { \input _len -> reservedSymbol (unsafeTextHead (aiPtr input)) }

@lbanana / ~[$symbol]   { \_ _ -> pure LBanana }
@rbanana                { \_ _ -> pure RBanana }

}

{

type AlexAction = AlexInput -> Int -> AlexM TokenVal
type AlexPred a = a -> AlexInput -> Int -> AlexInput -> Bool

{-# INLINE kw #-}
kw :: TokenVal -> AlexAction
kw tok = \_ _ -> pure tok

isLiterateEnabled'
  :: AlexPred (LitMode a)
isLiterateEnabled' litLoc _inputBefore _len _inputAfter =
    isLiterateEnabled litLoc

shouldEndLiterateBird
  :: AlexPred (LitMode LitStyle)
shouldEndLiterateBird litLoc inputBefore _len _inputAfter =
    case unsafeTextHeadAscii $ (`plusPtr` 1) $ aiPtr inputBefore of
        -- 62 = '>'
        62 -> False
        _  -> isLiterateBirdInside litLoc

shouldEndLiterateLatex
  :: AlexPred (LitMode LitStyle)
shouldEndLiterateLatex litLoc _inputBefore _len _inputAfter =
    isLiterateLatexInside litLoc

tokenize
  :: FilePath -> LitMode Void -> Bool -> BS.ByteString -> Either Text [Token]
tokenize filename litLoc trackPrefixesOffsets input =
    case runAlexM filename trackPrefixesOffsets litLoc code input scanTokens of
        (Nothing, xs) -> Right xs
        (Just err, _) -> Left err
    where
    code = case litLoc of
        LitVanilla  -> startCode
        LitOutside  -> literateCode
        LitInside x -> absurd x

scanTokens :: AlexM (Maybe Text)
scanTokens = go
    where
    go = do
        nextTok <- continueScanning
        case nextTok of
            EOF       -> pure Nothing
            Error err -> pure $ Just err
            _         -> do
                -- Use input after reading token to get proper prefix that includes
                -- token we currently read.
                AlexState{asInput} <- get
                tell [(asInput, nextTok)]
                go

continueScanning :: AlexM TokenVal
continueScanning = do
    s@AlexState{asInput} <- get
    go (view asCodeL s) (view asLiterateLocL s) asInput
    where
        go :: AlexCode -> LitMode LitStyle -> AlexInput -> AlexM TokenVal
        go !code !litLoc = go'
            where
            go' input = do
                case alexScanUser litLoc input (unAlexCode code) :: AlexReturn AlexAction of
                  AlexEOF                        ->
                      pure EOF
                  AlexError input                -> do
                      code' <- gets (view asCodeL)
                      pure $ Error $ "Lexical error while in state " <> T.pack (show code') <>
                        " at line " <>
                        T.pack (show (unLine (view aiLineL input))) <> ": '" <> takeText input 40 <> "'"
                  AlexSkip input' _              ->
                      go' input'
                  AlexToken input' tokLen action ->
                      alexSetInput input' *> action input tokLen

dropUntilNL_ :: AlexM ()
dropUntilNL_ =
    modify $ \s -> s { asInput = dropUntilNL $ asInput s }

dropUntilNL' :: AlexM TokenVal
dropUntilNL' = dropUntilNL_ *> continueScanning

dropUntilNLOr' :: Word8 -> AlexM TokenVal
dropUntilNLOr' w = do
    modify $ \s -> s { asInput = dropUntilNLOr w $ asInput s }
    continueScanning

dropUntilNLOrEither' :: Word8 -> Word8 -> AlexM TokenVal
dropUntilNLOrEither' w1 w2 = do
    modify $ \s -> s { asInput = dropUntilNLOrEither w1 w2 $ asInput s }
    continueScanning

startIndentationCounting :: Int -> AlexM TokenVal
startIndentationCounting !n = do
    modify (\s -> set asCommentDepthL 1 $ set asIndentationSizeL (fromIntegral n) s)
    alexSetNextCode indentCommentCode
    continueScanning

endIndentationCounting :: Int -> AlexM TokenVal
endIndentationCounting !n = do
    alexSetNextCode startCode
    Newline . (+ n) . fromIntegral <$> gets (view asIndentationSizeL)

startIndentComment :: AlexM TokenVal
startIndentComment = do
    void $ modifyCommentDepth (+1)
    alexSetNextCode indentCommentCode
    continueScanning

startPreprocessorStripping :: AlexM TokenVal
startPreprocessorStripping = do
    void $ modifyPreprocessorDepth (+1)
    alexSetNextCode stripCppCode
    continueScanning

endPreprocessorStripping :: AlexM TokenVal
endPreprocessorStripping = do
    newDepth <- modifyPreprocessorDepth (\x -> x - 1)
    when (newDepth == 0) $
        alexSetNextCode startCode
    continueScanning

startComment :: AlexM TokenVal
startComment = do
    void $ modifyCommentDepth (+1)
    alexSetNextCode commentCode
    continueScanning

endComment :: AlexCode -> AlexM TokenVal
endComment nextCode = do
    newDepth <- modifyCommentDepth (\x -> x - 1)
    when (newDepth == 0) $
        alexSetNextCode nextCode
    continueScanning

startQuasiquoter :: AlexInput -> AlexM TokenVal
startQuasiquoter AlexInput{aiPtr} = do
    !haveEnd     <- gets (view asHaveQQEndL)
    isEndPresent <- case haveEnd of
        Nothing    -> do
            let haveEnd' = checkQuasiQuoteEndPresent aiPtr
            modify $ set asHaveQQEndL (Just haveEnd')
            pure haveEnd'
        Just ends' -> pure ends'
    case isEndPresent of
        -- No chance of quasi-quote closing till the end of current file.
        -- Assume that file ought to be well-formed and treat currently
        -- matched input (and throw away the pipe character).
        False -> pure LBracket
        True  -> startUnconditionalQuasiQuoter

startUnconditionalQuasiQuoter :: AlexM TokenVal
startUnconditionalQuasiQuoter =
    QuasiquoterStart <$ alexSetNextCode qqCode

startSplice :: Context -> AlexM TokenVal
startSplice ctx = do
    alexSetNextCode startCode
    pushContext ctx
    pure SpliceStart

endQuasiquoter :: AlexM TokenVal
endQuasiquoter =
    QuasiquoterEnd <$ alexSetNextCode startCode

pushLParen :: AlexAction
pushLParen _ _ =
    LParen <$ pushContext CtxHaskell

popRParen :: AlexAction
popRParen _ _ = do
    cs <- gets asContextStack
    case cs of
        [] -> pure ()
        c : cs' -> do
            modify $ \s -> s { asContextStack = cs' }
            alexSetNextCode $ case c of
                CtxHaskell     -> startCode
                CtxQuasiquoter -> qqCode
    pure RParen

{-# INLINE errorAtLine #-}
errorAtLine
  :: MonadState AlexState m
  => Text -> m TokenVal
errorAtLine msg = do
    line <- gets (unLine . view aiLineL . asInput)
    return $ Error $ "Error at line " <> T.pack (show line) <> ": " <> msg

startLiterateBird :: AlexM ()
startLiterateBird = do
    alexSetNextCode startCode
    alexEnterBirdLiterateEnv

startLiterateLatex :: AlexM ()
startLiterateLatex = do
    alexSetNextCode startCode
    alexEnterLiterateLatexEnv

endLiterate :: AlexM ()
endLiterate = do
    alexSetNextCode literateCode
    alexExitLiterateEnv

endLiterate' :: AlexM TokenVal
endLiterate' = do
    alexSetNextCode literateCode
    alexExitLiterateEnv
    continueScanning

reservedSymbol :: Char -> AlexM TokenVal
reservedSymbol = \case
    '→' -> pure Arrow
    '∷' -> pure DoubleColon
    '⇒' -> pure Implies
    '∀' -> pure forallTokenVal
    '⦇' -> pure LBanana
    '⦈' -> A.pure RBanana
    '⟦' -> startUnconditionalQuasiQuoter
    '⟧' -> endQuasiquoter
    c   -> error $ "Unexpected reserved symbol: " ++ show c

reservedSymbolQQ :: Char -> AlexM TokenVal
reservedSymbolQQ = \case
    '⟧' -> endQuasiquoter
    _   -> continueScanning

{-# INLINE countBackslashCR #-}
countBackslashCR :: AlexInput -> Int
countBackslashCR AlexInput{aiPtr} = case unsafeTextHeadAscii aiPtr of
    -- '\\'
    92 -> case unsafeTextHeadOfTailAscii aiPtr of
        -- '\r'
        13 -> 2
        _  -> 1
    -- '\r'
    13 -> 1
    _  -> 0

-- Known codes

{-# INLINE startCode         #-}
{-# INLINE qqCode            #-}
{-# INLINE commentCode       #-}
{-# INLINE indentCommentCode #-}
{-# INLINE indentCountCode   #-}
{-# INLINE literateCode      #-}
{-# INLINE stripCppCode      #-}
startCode, qqCode, commentCode, indentCommentCode, indentCountCode, literateCode, stripCppCode :: AlexCode
startCode          = AlexCode 0
qqCode             = AlexCode qq
commentCode        = AlexCode comment
indentCommentCode  = AlexCode indentComment
indentCountCode    = AlexCode indentCount
literateCode       = AlexCode literate
stripCppCode       = AlexCode stripCpp

}