Kawaii-Parser-2.0.0: Parser/Tokeniser.hs
{-# OPTIONS_GHC -Wall #-}
{-# LANGUAGE StandaloneDeriving #-}
{-|
Description: A simple state- and transition-based tokeniser with location tracking.
* Tokeniser
-}
module Parser.Tokeniser (
Tokeniser',
Tokens',
add_token,
current_line_and_char,
delete_char,
gather_token,
get_char,
get_token,
take_token,
tokenisation_error,
tokenise,
tokens_ended) where
import Control.Monad.Except (MonadError (..))
import Control.Monad.RWS.Strict (RWST, execRWST)
import Control.Monad.Reader (MonadReader (..))
import Control.Monad.State.Strict (MonadState (..))
import Control.Monad.Writer.Strict (MonadWriter (..))
import Parser.Errors (Error (..))
import Parser.Line_and_char (L (..), Line_and_char, init_line_and_char)
data State char_class = State {state_line_and_char :: Line_and_char, state_text :: [char_class]}
-- | A tokeniser that works with any kind of custom characters, tokens and errors. The custom character type is useful if you
-- need to classify characters according to their behavior before tokenisation - for example, wrap all operators, letters,
-- delimiters or digits in the same constructor to simplify pattern matching.
data Tokeniser' char_class token err t =
Tokeniser {run_tokeniser :: RWST (char_class -> Line_and_char -> Line_and_char) [L token] (State char_class) (Either err) t}
-- | A sequence of tokens with locations. For internal use in the parser.
data Tokens' token = Tokens [L token] Line_and_char
instance Applicative (Tokeniser' char_class token err) where
Tokeniser tokenise_0 <*> Tokeniser tokenise_1 = Tokeniser (tokenise_0 <*> tokenise_1)
pure x = Tokeniser (return x)
instance Functor (Tokeniser' char_class token err) where
fmap f (Tokeniser tokenise') = Tokeniser (f <$> tokenise')
instance Monad (Tokeniser' char_class token err) where
Tokeniser tokenise' >>= f = Tokeniser (tokenise' >>= run_tokeniser <$> f)
deriving instance Show char_class => Show (State char_class)
deriving instance Show token => Show (Tokens' token)
-- | Add the token to the output. Note that the order of adding tokens is important and you have to add the token before
-- deleting the respective characters to get the correct location.
add_token :: token -> Tokeniser' char_class token err ()
add_token token =
do
line_and_char <- state_line_and_char <$> Tokeniser get
add_token' line_and_char token
add_token' :: Line_and_char -> token -> Tokeniser' char_class token err ()
add_token' line_and_char token = Tokeniser (tell [L line_and_char token])
-- | Get the location of the first token or, if there are none, the end of file. For internal use in the parser.
current_line_and_char :: Tokens' token -> Line_and_char
current_line_and_char (Tokens tokens end_line_and_char) =
case tokens of
[] -> end_line_and_char
L line_and_char _ : _ -> line_and_char
-- | Delete the first character from the remaining text. Automatically updates the location.
delete_char :: Tokeniser' char_class token err ()
delete_char =
do
next_line_and_char <- Tokeniser ask
State line_and_char text <- Tokeniser get
case text of
[] -> return ()
char_class : text' -> Tokeniser (put (State (next_line_and_char char_class line_and_char) text'))
-- | Add a token that consists of several characters - for example, an operator, a word or a number. You have to provide a
-- function that recognises suitable characters and a function that transforms the resulting string into a token.
gather_token :: (char_class -> Maybe Char) -> (String -> token) -> Tokeniser' char_class token err ()
gather_token recognise_char string_to_token =
do
line_and_char <- state_line_and_char <$> Tokeniser get
token <- gather_token' recognise_char
add_token' line_and_char (string_to_token token)
gather_token' :: (char_class -> Maybe Char) -> Tokeniser' char_class token err String
gather_token' recognise_char =
let
f = gather_token' recognise_char
in
do
maybe_char_class <- get_char 0
case maybe_char_class >>= recognise_char of
Nothing -> return ""
Just char ->
do
delete_char
token <- f
return (char : token)
-- | Take a look at a character without deleting it. Returns @Nothing@ if the index is negative or if the remaining text is
-- too short.
get_char :: Int -> Tokeniser' char_class token err (Maybe char_class)
get_char i =
do
text <- state_text <$> Tokeniser get
return
(case 0 <= i && i < length text of
False -> Nothing
True -> Just (text !! i))
-- | Get the first token without deleting it. For internal use in the parser.
get_token :: Tokens' token -> Maybe token
get_token (Tokens tokens _) =
case tokens of
[] -> Nothing
L _ token : _ -> Just token
-- | Recognises tokens that fit a certain pattern and transforms them into something more useful - for example, a string or an
-- integer. Returns @Nothing@ if the first token does not fit the pattern, and returns the transformed token and the rest of
-- the sequence if it does fit. For internal use in the parser.
take_token :: (token -> Maybe t) -> Tokens' token -> Maybe (t, Tokens' token)
take_token f (Tokens tokens end_line_and_char) =
case tokens of
[] -> Nothing
L _ token : tokens' ->
do
x <- f token
return (x, Tokens tokens' end_line_and_char)
-- | Throw a tokenisation error at the current location.
tokenisation_error :: (Line_and_char -> err) -> Tokeniser' char_class token err t
tokenisation_error err =
do
line_and_char <- state_line_and_char <$> Tokeniser get
Tokeniser (throwError (err line_and_char))
-- | Tokenise the text. For internal use in the parser.
tokenise ::
(
(Char -> char_class) ->
(char_class -> Line_and_char -> Line_and_char) ->
Tokeniser' char_class token err () ->
String ->
Either Error (Either err (Tokens' token)))
tokenise classify_char next_line_and_char (Tokeniser tokenise') text =
case execRWST tokenise' next_line_and_char (State init_line_and_char (classify_char <$> text)) of
Left err -> Right (Left err)
Right (State line_and_char text', tokens) ->
case text' of
[] -> Right (Right (Tokens tokens line_and_char))
_ -> Left Incomplete_tokenisation
-- | Check whether the sequence of tokens has ended. For internal use in the parser.
tokens_ended :: Tokens' token -> Bool
tokens_ended (Tokens tokens _) = null tokens