fnotation-0.1.0.0: src/FNotation/Lexer.hs
module FNotation.Lexer where
import Data.Char (isDigit, isLetter, ord)
import Data.IORef
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Unsafe qualified as TU
import Data.Vector qualified as V
import Data.Vector.Mutable qualified as VM
import Diagnostician
import FNotation.Config
import FNotation.Names
import FNotation.Tokens
import Prettyprinter
import Prelude hiding (error, getChar, head, init, last, lex, lookup, span, tail)
import Prelude qualified as P
-- Buffer
--------------------------------------------------------------------------------
data Buffer a = Buffer
{ next :: IORef Int,
size :: Int,
values :: VM.IOVector a
}
bufferWithCapacity :: Int -> IO (Buffer a)
bufferWithCapacity n = do
next <- newIORef 0
values <- VM.unsafeNew n
pure $ Buffer next n values
bufferUnsafeFreeze :: Buffer a -> IO (V.Vector a)
bufferUnsafeFreeze b = do
l <- readIORef b.next
V.take l <$> V.unsafeFreeze b.values
push :: Buffer a -> a -> IO ()
push b e = do
i <- readIORef b.next
if (i < b.size)
then do
VM.unsafeWrite b.values i e
writeIORef b.next (i + 1)
else P.error "cannot push because buffer is full"
-- Lexer diagnostics
--------------------------------------------------------------------------------
data LexerCode
= UnexpectedCharacter
| UncontinuedQualifiedName
deriving (Eq, Ord)
lexerCodeTable :: Map LexerCode CodeMeta
lexerCodeTable =
Map.fromList
[ (UnexpectedCharacter, CodeMeta 0 SError Nothing),
(UncontinuedQualifiedName, CodeMeta 1 SError Nothing)
]
-- Lex monad
--------------------------------------------------------------------------------
data LexState = LexState
{ pos :: IORef Int,
prev :: IORef Int,
iter :: IORef TU.Iter,
out :: Buffer Token,
file :: File,
reporter :: ReporterFor LexerCode,
config :: ConfTable Kind
}
-- Fundamental lexing actions
--------------------------------------------------------------------------------
span :: LexState -> IO Span
span st = Span <$> readIORef st.prev <*> readIORef st.pos
emit :: LexState -> Kind -> TokenValue -> IO ()
emit st k v = do
s <- span st
push st.out (Token k v s)
readIORef st.pos >>= writeIORef st.prev
emit0 :: LexState -> Kind -> IO ()
emit0 st k = emit st k VEmpty
-- peek the character being currently scrutinized
peek :: LexState -> IO Char
peek st = do
TU.Iter c _ <- readIORef st.iter
pure c
advance :: LexState -> IO ()
advance st = do
let src = st.file.contents
TU.Iter _ j <- readIORef st.iter
i <- readIORef st.pos
let i' = i + j
writeIORef st.pos i'
if i' >= TU.lengthWord8 src
then writeIORef st.iter (TU.Iter '\0' 0)
else writeIORef st.iter (TU.iter src i')
classify :: LexState -> Kind -> IO ()
classify st k = advance st >> emit0 st k
skip :: LexState -> IO ()
skip st = do
advance st
readIORef st.pos >>= writeIORef st.prev
advanceWhile :: LexState -> (Char -> Bool) -> IO ()
advanceWhile st f =
peek st >>= \case
'\0' -> pure ()
c ->
if f c
then advance st >> advanceWhile st f
else pure ()
slice :: LexState -> Span -> IO T.Text
slice st s = pure $ sliceWord8 s.start s.end st.file.contents
isAlphaNum :: Char -> Bool
isAlphaNum c
| isLetter c = True
| isDigit c = True
| c == '_' || c == '-' = True
| otherwise = False
-- TODO: more symbols, including unicode symbols? See issue #6
isSymbol :: Char -> Bool
isSymbol = \case
'<' -> True
'>' -> True
'-' -> True
'+' -> True
'/' -> True
'*' -> True
'~' -> True
':' -> True
'=' -> True
_ -> False
report :: LexState -> LexerCode -> DDoc -> IO ()
report st c m = do
s <- span st
let d = Diagnostic c m [Note (Just (SourceLoc (st.file) s)) Nothing]
reportTo st.reporter d
unexpectedChar :: LexState -> Char -> IO ()
unexpectedChar st c = do
advance st
report st UnexpectedCharacter $ "Unexpected character" <+> "'" <> pretty c <> "'"
-- Lexemes
--------------------------------------------------------------------------------
sliceWhile :: LexState -> (Char -> Bool) -> IO Text
sliceWhile st f = do
s <- readIORef st.pos
advanceWhile st f
e <- readIORef st.pos
slice st (Span s e)
classifyName :: LexState -> Name -> Kind
classifyName st x = case confTableLookup st.config x.last of
Just kind -> kind
Nothing ->
if isAlphaNumStart (T.head x.last)
then AIdent
else SIdent
isAlphaNumStart :: Char -> Bool
isAlphaNumStart c
| isLetter c = True
| c == '_' = True
| otherwise = False
nameSeg :: LexState -> IO Text
nameSeg st =
peek st >>= \case
c | isAlphaNumStart c -> sliceWhile st isAlphaNum
c | isSymbol c -> sliceWhile st isSymbol
_ -> P.error "nameSeg should only be called if current character is a letter"
nameFromHeadTail :: Text -> [Text] -> Name
nameFromHeadTail head tail =
let go s [] = ([], s)
go s (t : ts) = let (ts', t') = go t ts in (s : ts', t')
(init, last) = go head tail
in Name init last
-- | Lex a name
name :: LexState -> IO Name
name st = nameFromHeadTail <$> nameSeg st <*> tail
where
tail =
peek st >>= \case
'/' -> advance st >> (:) <$> nameSeg st <*> tail
_ -> pure []
ident :: LexState -> IO ()
ident st = do
x <- name st
emit st (classifyName st x) (VName x)
int :: LexState -> IO Int
int st = go 0
where
go i = do
c <- peek st
if isDigit c
then advance st >> go (i * 10 + (ord c - ord '0'))
else pure i
comment :: LexState -> IO ()
comment st = do
advanceWhile st (/= '\n')
advance st
string :: LexState -> IO ()
string st = do
s <- readIORef st.pos
advance st
advanceWhile st (/= '\"')
advance st
e <- readIORef st.pos
x <- slice st (Span (s + 1) (e - 1))
emit st String (VString x)
-- Top-level lexing interface
--------------------------------------------------------------------------------
-- | Lex all the tokens
run :: LexState -> IO ()
run st =
peek st >>= \case
'\t' -> skip st >> run st
' ' -> skip st >> run st
'(' -> classify st LParen >> run st
')' -> classify st RParen >> run st
'[' -> classify st LBrack >> run st
']' -> classify st RBrack >> run st
'{' -> classify st LCurly >> run st
'}' -> classify st RCurly >> run st
',' -> classify st Comma >> run st
';' -> classify st Semicolon >> run st
'\n' -> classify st Nl >> run st
'#' -> comment st >> run st
'\"' -> string st >> run st
'.' -> advance st >> (name st >>= emit st Field . VName) >> run st
'\'' -> advance st >> (name st >>= emit st Tag . VName) >> run st
'\0' -> emit0 st Eof
c | isDigit c -> (int st >>= emit st Int . VInt) >> run st
c | isLetter c || c == '_' || isSymbol c -> ident st >> run st
c -> unexpectedChar st c >> run st
-- | Run the lexer, return a vector of tokens ready for parsing.
lex :: ConfTable Kind -> ReporterFor LexerCode -> File -> IO (V.Vector Token)
lex config reporter file = do
pos <- newIORef 0
prev <- newIORef 0
iter <- newIORef $ TU.iter file.contents 0
out <- bufferWithCapacity (TU.lengthWord8 file.contents)
let st = LexState pos prev iter out file reporter config
run st
bufferUnsafeFreeze st.out