packages feed

miso-1.12.0.0: src/Miso/Util/Lexer.hs

{-# LANGUAGE CPP #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Miso.Util.Lexer
-- Copyright   :  (C) 2016-2026 David M. Johnson
-- License     :  BSD3-style (see the file LICENSE)
-- Maintainer  :  David M. Johnson <code@dmj.io>
-- Stability   :  experimental
-- Portability :  non-portable
--
-- = Overview
--
-- "Miso.Util.Lexer" is an internal lexer combinator library used by
-- miso's JSON pipeline ("Miso.JSON.Lexer") and URI\/router tokeniser
-- ("Miso.Router"). It is __not__ designed for general application use,
-- but is exposed for downstream code that needs to build custom lexers.
--
-- The central type is 'Lexer':
--
-- @
-- newtype 'Lexer' token = Lexer
--   { 'runLexer' :: 'Stream' -> Either 'LexerError' (token, 'Stream') }
-- @
--
-- 'Lexer' is a 'Monad', 'Alternative', and 'MonadFail'. Its
-- 'Control.Applicative.Alternative' instance implements the
-- /maximal munch/ rule: when both branches succeed, the one that
-- consumes the most input wins.
--
-- = Key types
--
-- * 'Stream' — the remaining input text together with a 'Location'
--   (line and column) cursor.
-- * 'Location' — @{line :: Int, column :: (Int, Int)}@, used in error
--   messages and 'Located' tokens.
-- * 'Located' token — a lexed value paired with the 'Location' at which
--   it was recognised.
-- * 'LexerError' — either @LexerError MisoString Location@ (unexpected
--   input) or @UnexpectedEOF Location@ (ran out of input).
--
-- = Primitive combinators
--
-- @
-- 'satisfy'  :: (Char -> Bool) -> 'Lexer' Char   -- consume one matching char
-- 'char'     :: Char -> 'Lexer' Char              -- consume a specific char
-- 'string'   :: 'Miso.String.MisoString' -> 'Lexer' 'Miso.String.MisoString'   -- consume a literal prefix
-- 'string''  :: String -> 'Lexer' String          -- same for 'String'
-- 'peek'     :: 'Lexer' (Maybe Char)              -- look ahead without consuming
-- 'oops'     :: 'Lexer' token                     -- always fails
-- @
--
-- = Stream and location access
--
-- @
-- 'getInput'     :: 'Lexer' 'Stream'
-- 'putInput'     :: 'Stream' -> 'Lexer' ()
-- 'modifyInput'  :: ('Stream' -> 'Stream') -> 'Lexer' ()
-- 'getLocation'  :: 'Lexer' 'Location'
-- 'setLocation'  :: 'Location' -> 'Lexer' ()
-- @
--
-- = Running a lexer
--
-- @
-- 'runLexer' :: 'Lexer' token -> 'Stream' -> Either 'LexerError' (token, 'Stream')
-- 'mkStream' :: 'Miso.String.MisoString' -> 'Stream'   -- create initial stream
-- @
--
-- = See also
--
-- * "Miso.Util.Parser" — parser combinator library that consumes 'Lexer' output
-- * "Miso.JSON.Lexer" — JSON tokeniser built on this module
-- * "Miso.Router" — URI tokeniser built on this module
-- * "Miso.Util" — higher-level 'Miso.Util.sepBy', 'Miso.Util.oneOf', …
----------------------------------------------------------------------------
module Miso.Util.Lexer
  ( -- ** Types
    Lexer (..)
  , Stream (..)
  , Located (..)
  , Location (..)
  , LexerError (..)
    -- ** Combinators
  , getStartColumn
  , zeroLocation
  , initialLocation
  , mkStream
  , oops
  , streamError
  , string
  , string'
  , char
  , satisfy
  , peek
  , getInput
  , putInput
  , getLocation
  , setLocation
  , modifyInput
  , withLocation
  ) where
----------------------------------------------------------------------------
import           Control.Monad
#if __GLASGOW_HASKELL__ <= 865
import           Control.Monad.Fail
#endif
import           Control.Applicative
----------------------------------------------------------------------------
import           Miso.String (MisoString, ToMisoString)
import qualified Miso.String as MS
----------------------------------------------------------------------------
-- | Potential errors during lexing
data LexerError
  = LexerError MisoString Location
  | UnexpectedEOF Location
  deriving (Eq)
----------------------------------------------------------------------------
instance Show LexerError where
  show (UnexpectedEOF loc) =
    "Unexpected EOF at: " <> show loc
  show (LexerError xs loc) =
    "Unexpected \"" <> take 5 (MS.unpack xs) <> "\"... at " <> show loc
----------------------------------------------------------------------------
-- | Type to hold the location (line and column) of a Token
data Location
  = Location
  { line :: Int
  -- ^ Current line number (1-based)
  , column :: (Int,Int)
  -- ^ @(start, end)@ column offsets for the current token (1-based)
  } deriving Eq
----------------------------------------------------------------------------
instance Show Location where
  show (Location l col) = show l <> " " <> show col
----------------------------------------------------------------------------
-- | Helper for extracting column from t'Location'
getStartColumn :: Location -> Int
getStartColumn = fst . column
----------------------------------------------------------------------------
-- | Initial t'Location'
initialLocation :: Location
initialLocation = Location 1 (1,1)
----------------------------------------------------------------------------
-- | Empty t'Location'
zeroLocation :: Location
zeroLocation = Location 0 (0,0)
----------------------------------------------------------------------------
-- | A Lexer is a state monad with optional failure the abides by the
-- maximal munch rule in its 'Alternative' instance.
newtype Lexer token
  = Lexer
  { runLexer :: Stream -> Either LexerError (token, Stream)
  -- ^ Run the lexer against a 'Stream'; returns the token and remaining input, or a 'LexerError'
  }
----------------------------------------------------------------------------
-- | Combinator that always fails to lex
oops :: Lexer token
oops = Lexer $ \s -> Left (streamError s)
----------------------------------------------------------------------------
-- | Smart constructor for t'LexerError'
streamError
  :: Stream
  -- ^ The stream at the point of failure; used to populate the error location
  -> LexerError
streamError (Stream xs l) = unexpected xs l
----------------------------------------------------------------------------
-- | Smart constructor for t'Stream'
mkStream
  :: MisoString
  -- ^ Input text to lex
  -> Stream
mkStream xs = Stream xs initialLocation
----------------------------------------------------------------------------
-- | A t'Stream' of text used as input to lexing
data Stream
  = Stream
  { stream :: MisoString
    -- ^ Current t'Stream' of text
  , currentLocation :: Location
    -- ^ current t'Location' in the t'Stream'
  } deriving Eq
----------------------------------------------------------------------------
-- | A t'Located' token holds the lexed output the t'Location' at which
-- the successful lex occurred.
data Located token
  = Located
  { token :: token
  -- ^ The lexed token value
  , location :: Location
  -- ^ 'Location' in the source at which this token was recognised
  } deriving Eq
----------------------------------------------------------------------------
instance Show token => Show (Located token) where
  show (Located t l) = show l <> " " <> show t
----------------------------------------------------------------------------
instance Functor Lexer where
  fmap f (Lexer l) = Lexer $ \input -> do
    (t, x) <- l input
    pure (f t, x)
----------------------------------------------------------------------------
instance Applicative Lexer where
  pure x = Lexer $ \input -> pure (x, input)
  Lexer l1 <*> Lexer l2 = Lexer $ \input -> do
    (f, x) <- l1 input
    (a, y) <- l2 x
    pure (f a, y)
----------------------------------------------------------------------------
instance Monad Lexer where
  m >>= f = Lexer $ \input -> do
    (x, s) <- runLexer m input
    runLexer (f x) s
----------------------------------------------------------------------------
instance MonadFail Lexer where
  fail _ = oops
----------------------------------------------------------------------------
instance Alternative Lexer where
  empty = Lexer $ \(Stream s l)  -> Left (unexpected s l)
  Lexer l1 <|> Lexer l2 = Lexer $ \input ->
    case (l1 input, l2 input) of
      (res, Left _) -> res
      (Left _, res) -> res
      (Right (x, Stream s sl), Right (y,Stream t tl)) ->
        if MS.length s <= MS.length t
        then Right (x, Stream s sl)
        else Right (y, Stream t tl)
----------------------------------------------------------------------------
instance MonadPlus Lexer where
  mplus = (<|>)
----------------------------------------------------------------------------
-- | Fetches the first character in the t'Stream', does not consume input
peek :: Lexer (Maybe Char)
peek = Lexer $ \ys ->
  pure $ case ys of
    Stream xs l ->
      case MS.uncons xs of
        Nothing -> (Nothing, Stream mempty l)
        Just (z,zs) -> (Just z, Stream (MS.singleton z <> zs) l)
----------------------------------------------------------------------------
-- | Predicate combinator that consumes matching input
satisfy
  :: (Char -> Bool)
  -- ^ Predicate; the next character is consumed only if this returns 'True'
  -> Lexer Char
satisfy predicate = Lexer $ \ys ->
  case ys of
    Stream s l ->
      case MS.uncons s of
        Nothing -> Left (unexpected s l)
        Just (z,zs)
          | predicate z -> Right (z, Stream zs l)
          | otherwise -> Left (unexpected zs l)
----------------------------------------------------------------------------
-- | Smart constructor for t'LexerError'
-- If the input is empty, an 'UnexpectedEOF' is issued.
unexpected :: MisoString -> Location -> LexerError
unexpected xs loc | MS.null xs = UnexpectedEOF loc
unexpected cs loc = LexerError cs loc
----------------------------------------------------------------------------
-- | Retrieves current input from t'Lexer'
getInput :: Lexer Stream
getInput = Lexer $ \s -> Right (s, s)
----------------------------------------------------------------------------
-- | Overrides current t'Stream' in t'Lexer' to user-specified t'Stream'.
putInput
  :: Stream
  -- ^ Replacement stream; replaces the current lexer input
  -> Lexer ()
putInput s = Lexer $ \_ -> Right ((), s)
----------------------------------------------------------------------------
-- | Retrieves the current t'Stream' t'Location'
getLocation :: Lexer Location
getLocation = Lexer $ \(Stream s l) -> pure (l, Stream s l)
----------------------------------------------------------------------------
-- | Sets the current t'Stream' t'Location'
setLocation
  :: Location
  -- ^ New location to record in the stream cursor
  -> Lexer ()
setLocation l = Lexer $ \(Stream s _) -> pure ((), Stream s l)
----------------------------------------------------------------------------
-- | Modifies a t'Stream'
modifyInput
  :: (Stream -> Stream)
  -- ^ Transform to apply to the current lexer input
  -> Lexer ()
modifyInput f = do
  s <- getInput
  putInput (f s)
----------------------------------------------------------------------------
-- | Lexer combinator for matching a 'Char'
char
  :: Char
  -- ^ The exact character to consume
  -> Lexer Char
char c = satisfy (== c)
----------------------------------------------------------------------------
-- | Lexer combinator for matching a 'String'
string'
  :: String
  -- ^ Literal string prefix to consume character by character
  -> Lexer String
string' = traverse char
----------------------------------------------------------------------------
-- | Lexer combinator for matching a 'MisoString'
string
  :: MisoString
  -- ^ Literal string prefix to consume from the input
  -> Lexer MisoString
string prefix = Lexer $ \s ->
  case s of
    Stream ys l
      | prefix `MS.isPrefixOf` ys ->
          Right (prefix, Stream (MS.drop (MS.length prefix) ys) l)
      | otherwise ->
          Left (unexpected ys l)
----------------------------------------------------------------------------
-- | Lexer combinator for executing a t'Lexer' with annotated t'Location' information
withLocation
  :: ToMisoString token
  => Lexer token
  -- ^ Inner lexer whose result is wrapped with its source 'Location'
  -> Lexer (Located token)
withLocation lexer = do
  result <- lexer
  let
    adjustLoc :: Location -> MisoString -> Location
    adjustLoc l = MS.foldl' adjust (next l)

  setLocation =<< adjustLoc <$> getLocation <*> pure (MS.ms result)
  Located result <$> getLocation
    where
      next :: Location -> Location
      next (Location l (_, end)) = Location l (end, end)

      adjust :: Location -> Char -> Location
      adjust (Location l (_, _)) '\n'       = Location (l + 1) (1,1)
      adjust (Location l (start, end)) '\t' = Location l (start, end + 8)
      adjust (Location l (start, end))   _  = Location l (start, end + 1)
----------------------------------------------------------------------------