mello-0.7.0: src/Mello/Recognize.hs
-- | Lightweight lexical delimiter recognition for Mello s-expressions.
--
-- This module does not parse s-expressions. It only checks delimiter balance
-- while respecting strings, chars, comments, and escapes. Offsets are returned
-- directly so callers can convert them to their preferred source-location type.
module Mello.Recognize
( RecogErr (..)
, sexpRecognizer
, recognizeSexpText
)
where
import Control.Foldl (Fold (..))
import Control.Foldl qualified as Foldl
import Control.Monad.Except (ExceptT (..), MonadError (..), runExceptT)
import Control.Monad.State.Strict (State, gets, modify', runState)
import Data.Text (Text)
import Data.Text qualified as T
import Mello.Text (Brace, readCloseBrace, readOpenBrace)
data X e s = X !(Maybe e) !s
foldUntilErr :: (a -> ExceptT e (State s) ()) -> s -> (Maybe e -> s -> b) -> Fold a b
foldUntilErr step initial extract = Fold step' initial' extract'
where
step' x@(X me s) a =
case me of
Just _ -> x
Nothing ->
let (ea, s') = runState (runExceptT (step a)) s
in case ea of
Left e -> X (Just e) s'
Right _ -> X Nothing s'
initial' = X Nothing initial
extract' (X me s) = extract me s
data RecogErr
= -- | An opener was not closed before EOF: brace, opener offset, EOF offset.
RecogErrUnclosedList !Brace !Int !Int
| -- | A closer appeared without a matching opener: actual brace, closer offset.
RecogErrUnexpectedClose !Brace !Int
| -- | A closer did not match the most recent opener: expected brace, opener offset, actual brace, closer offset.
RecogErrMismatchedBrace !Brace !Int !Brace !Int
deriving stock (Eq, Ord, Show)
data RecogState = RecogState
{ rsOffset :: !Int
, rsMode :: !RecogMode
, rsStack :: ![OpenList]
}
deriving stock (Eq, Ord, Show)
initRecogState :: RecogState
initRecogState = RecogState 0 RecogModeDefault []
type RecogM = ExceptT RecogErr (State RecogState)
data RecogMode
= RecogModeDefault
| RecogModeString
| RecogModeChar
| RecogModeComment
| RecogModeEscape !RecogMode
deriving stock (Eq, Ord, Show)
data OpenList = OpenList !Brace !Int
deriving stock (Eq, Ord, Show)
data CharCase
= CharCaseNewline
| CharCaseDoubleQuote
| CharCaseSingleQuote
| CharCaseOpenComment
| CharCaseSlashEsc
| CharCaseOpenBrace !Brace
| CharCaseCloseBrace !Brace
deriving stock (Eq, Ord, Show)
readCharCase :: Char -> Maybe CharCase
readCharCase c =
if
| c == '\n' -> Just CharCaseNewline
| c == '"' -> Just CharCaseDoubleQuote
| c == '\'' -> Just CharCaseSingleQuote
| c == ';' -> Just CharCaseOpenComment
| c == '\\' -> Just CharCaseSlashEsc
| otherwise ->
case readOpenBrace c of
Just b -> Just (CharCaseOpenBrace b)
Nothing -> case readCloseBrace c of
Just b -> Just (CharCaseCloseBrace b)
Nothing -> Nothing
stepR :: Char -> RecogM ()
stepR c = go <* incOffset
where
go = do
mode <- gets rsMode
case mode of
RecogModeDefault -> goDefault
RecogModeString -> goString
RecogModeChar -> goChar
RecogModeComment -> goComment
RecogModeEscape mode' -> setMode mode'
goString = case readCharCase c of
Just CharCaseDoubleQuote -> setMode RecogModeDefault
Just CharCaseSlashEsc -> setMode (RecogModeEscape RecogModeString)
_ -> pure ()
goChar = case readCharCase c of
Just CharCaseSingleQuote -> setMode RecogModeDefault
Just CharCaseSlashEsc -> setMode (RecogModeEscape RecogModeChar)
_ -> pure ()
goComment = case readCharCase c of
Just CharCaseNewline -> setMode RecogModeDefault
_ -> pure ()
goDefault = case readCharCase c of
Just CharCaseDoubleQuote -> setMode RecogModeString
Just CharCaseSingleQuote -> setMode RecogModeChar
Just CharCaseOpenComment -> setMode RecogModeComment
Just (CharCaseOpenBrace b) -> pushOpen b
Just (CharCaseCloseBrace b) ->
peekOpen >>= \case
Just (OpenList b0 _) | b == b0 -> popOpen
Just (OpenList b0 offset) -> throwAt (RecogErrMismatchedBrace b0 offset b)
Nothing -> throwAt (RecogErrUnexpectedClose b)
_ -> pure ()
incOffset = modify' (\s -> s {rsOffset = rsOffset s + 1})
setMode mode = modify' (\s -> s {rsMode = mode})
pushOpen b = do
offset <- gets rsOffset
modify' (\s -> s {rsStack = OpenList b offset : rsStack s})
popOpen = modify' $ \s ->
case rsStack s of
[] -> s
_ : t -> s {rsStack = t}
peekOpen = gets $ \s ->
case rsStack s of
[] -> Nothing
h : _ -> Just h
throwAt f = gets rsOffset >>= throwError . f
extractR :: Maybe RecogErr -> RecogState -> Either RecogErr ()
extractR me s = case me of
Just err -> Left err
Nothing -> case rsStack s of
[] -> Right ()
OpenList brace openOffset : _ -> Left (RecogErrUnclosedList brace openOffset (rsOffset s))
sexpRecognizer :: Fold Char (Either RecogErr ())
sexpRecognizer = foldUntilErr stepR initRecogState extractR
-- | Recognize delimiter balance in a complete text value.
recognizeSexpText :: Text -> Either RecogErr ()
recognizeSexpText = Foldl.fold sexpRecognizer . T.unpack