packages feed

sexp-grammar-2.2.0: src/Language/Sexp/Lexer.x

{
{-# LANGUAGE BangPatterns   #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing     #-}
{-# OPTIONS_GHC -fno-warn-tabs               #-}
{-# OPTIONS_GHC -fno-warn-unused-binds       #-}
{-# OPTIONS_GHC -fno-warn-unused-imports     #-}
{-# OPTIONS_GHC -fno-warn-unused-matches     #-}

module Language.Sexp.Lexer
  ( lexSexp
  ) where

import Data.Bifunctor
import qualified Data.ByteString.Lazy as BLW
import Data.ByteString.Lazy.Char8 (ByteString)
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.ByteString.Lazy.UTF8 as UTF8
import Data.Int
import Data.Scientific (Scientific)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import Data.Text.Lazy.Encoding (decodeUtf8)
import Data.Text.Read
import Data.Word

import Language.Sexp.Token
import Language.Sexp.Types (Position (..), LocatedBy (..))

}

$hspace     = [\ \t]
$whitespace = [$hspace\n\r\f\v]

$allgraphic = . # [\x00-\x20 \x7F-\xA0]

$digit      = 0-9
$hex        = [0-9 A-F a-f]
$alpha      = [a-z A-Z]

@number     = [\-\+]? $digit+ ([\.]$digit+)?

@escape     = \\ [nrt\\\"]
@string     = $allgraphic # [\"\\] | $whitespace | @escape

$unicode    = $allgraphic # [\x20-\x80]

$syminitial = [$alpha $digit \\\:\@\!\$\%\&\*\/\<\=\>\?\~\_\^\.\|\+\- $unicode]
$symsubseq  = [$syminitial \#\'\`\,]
@symbol     = $syminitial ($symsubseq)*

:-

$whitespace+       ;
";" .*             ;

"("                { just TokLParen   }
")"                { just TokRParen   }
"["                { just TokLBracket }
"]"                { just TokRBracket }
"{"                { just TokLBrace   }
"}"                { just TokRBrace   }

"'" / $allgraphic  { just (TokPrefix Quote)    }
"`" / $allgraphic  { just (TokPrefix Backtick) }
",@" / $allgraphic { just (TokPrefix CommaAt)  }
"," / $allgraphic  { just (TokPrefix Comma)    }
"#" / $allgraphic  { just (TokPrefix Hash)     }

@number            { TokNumber  `via` readNum    }
@symbol            { TokSymbol  `via` decode     }
\" @string* \"     { TokString  `via` readString }

{

----------------------------------------------------------------------
-- Actions

just :: Token -> AlexAction
just tok _ = tok

via :: (a -> Token) -> (ByteString -> a) -> AlexAction
via ftok f = ftok . f

----------------------------------------------------------------------
-- Decoders

readString :: ByteString -> T.Text
readString = TL.toStrict . unescape . TL.tail . TL.init . decodeUtf8

readNum :: ByteString -> Scientific
readNum = read . TL.unpack . decodeUtf8

decode :: ByteString -> T.Text
decode = TL.toStrict . decodeUtf8

----------------------------------------------------------------------
-- Entry point

lexSexp :: Position -> ByteString -> [LocatedBy Position Token]
lexSexp (Position fn line1 col1) =
  map (bimap fixPos id) . alexScanTokens . mkAlexInput (LineCol line1 col1)
  where
    fixPos (LineCol l c) = Position fn l c

----------------------------------------------------------------------
-- Machinery

type AlexAction = ByteString -> Token

alexScanTokens :: AlexInput -> [LocatedBy LineCol Token]
alexScanTokens input =
  case alexScan input defaultCode of
    AlexEOF ->
      [aiLineCol input :< TokEOF]
    AlexError (AlexInput {aiInput, aiLineCol}) ->
      let rest = T.takeWhile (/= '\n') $ decode $ UTF8.take 100 aiInput
      in [aiLineCol :< (TokUnknown rest)]
    AlexSkip input _ ->
      alexScanTokens input
    AlexToken input' tokLen action ->
      let inputText = UTF8.take (fromIntegral tokLen) (aiInput input)
      in (aiLineCol input :< action inputText) : alexScanTokens input'
  where
    defaultCode :: Int
    defaultCode = 0

data LineCol = LineCol {-# UNPACK #-} !Int {-# UNPACK #-} !Int

columnsInTab :: Int
columnsInTab = 8

advanceLineCol :: Char -> LineCol -> LineCol
advanceLineCol '\n' (LineCol line _)   = LineCol (line + 1) 1
advanceLineCol '\t' (LineCol line col) = LineCol line (((col + columnsInTab - 1) `div` columnsInTab) * columnsInTab + 1)
advanceLineCol _    (LineCol line col) = LineCol line (col + 1)

data AlexInput = AlexInput
  { aiInput     :: ByteString
  , aiPrevChar  :: {-# UNPACK #-} !Char
  , aiBytesLeft :: {-# UNPACK #-} !Int64
  , aiLineCol   :: !LineCol
  }

mkAlexInput :: LineCol -> ByteString -> AlexInput
mkAlexInput initPos source = AlexInput
  { aiInput     = source
  , aiPrevChar  = '\n'
  , aiBytesLeft = 0
  , aiLineCol   = initPos
  }

alexNextChar :: AlexInput -> Maybe AlexInput
alexNextChar input
  | aiBytesLeft input > 1 = Just $ input { aiBytesLeft = aiBytesLeft input - 1 }
  | otherwise = case UTF8.decode (aiInput input) of
      Just (c, n) -> Just $ input
        { aiPrevChar  = c
        , aiLineCol   = advanceLineCol c (aiLineCol input)
        , aiBytesLeft = n
        }
      Nothing -> Nothing

-- Alex interface - functions used by Alex
alexInputPrevChar :: AlexInput -> Char
alexInputPrevChar = aiPrevChar

alexGetByte :: AlexInput -> Maybe (Word8, AlexInput)
alexGetByte input =
  alexNextChar input >>= getByte
  where
    getByte :: AlexInput -> Maybe (Word8, AlexInput)
    getByte input =
      case BLW.uncons (aiInput input) of
        Just (w, rest) -> Just (w, input { aiInput = rest })
        Nothing -> Nothing
}