packages feed

mello-0.7.0: src/Mello/Parse.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE OverloadedStrings #-}

module Mello.Parse
  ( OffsetSpan
  , OffsetSexp
  , Loc (..)
  , LocSpan
  , LocSexp
  , ParseErr (..)
  , sexpParser
   , parseSexp
   , parseSexpDetailed
   , parseSexpI
   , parseSexpDetailedI
   )
where

import Bowtie (Memo, pattern MemoP)
import Control.Exception (Exception (..))
import Control.Monad (guard, unless, void)
import Data.Bifunctor (first)
import Data.Char (isDigit, isSpace)
import Data.Hashable (Hashable)
import Data.Sequence (Seq (..))
import Data.Text (Text)
import Data.Text qualified as T
import Data.Void (Void)
import GHC.Generics (Generic)
import Looksee (Err, ParserT, Span (..), errSpan)
import Looksee qualified as L
import Mello.Syntax (Atom (..), Doc (..), SexpF (..), Sym (..))
import Mello.Recognize (RecogErr (..), recognizeSexpText)
import Mello.Text
  ( Brace
  , closeBraceChar
  , isAtomStart
  , isCharStart
  , isListStart
  , isQuoteStart
  , isStringStart
  , isSymCont
  , isSymStart
  , isUnquoteStart
  , openBraceChar
  )

-- Generic parser combinators

guard1P :: (Monad m) => (Char -> Bool) -> ParserT e m ()
guard1P f = L.headP >>= guard . f

cons1P :: (Monad m) => (Char -> Bool) -> (Char -> Bool) -> ParserT e m Text
cons1P f g = liftA2 T.cons (L.headP >>= \c -> c <$ guard (f c)) (L.takeWhileP g)

commitSameP :: (Monad m) => [ParserT e m a] -> ParserT e m a
commitSameP = L.commitP . fmap (\p -> (void p, p))

explainEmptyP :: (Monad m) => Text -> ParserT e m a -> ParserT e m a
explainEmptyP msg = L.explainP $ \case
  L.ReasonEmpty -> Just (msg, True)
  _ -> Nothing

-- The final recursive types

type OffsetSpan = Span Int

type OffsetSexp = Memo SexpF OffsetSpan

data Loc = Loc
  { locLine :: !Int
  , locCol :: !Int
  , locOffset :: !Int
  }
  deriving stock (Eq, Ord, Show, Generic)
  deriving anyclass (Hashable)

type LocSpan = Span Loc

type LocSexp = Memo SexpF LocSpan

data ParseErr
  = -- | An opening delimiter was not closed before EOF.
    ParseErrUnclosedList !Brace !LocSpan
  | -- | A closing delimiter appeared with no matching opener.
    ParseErrUnexpectedClose !Brace !LocSpan
  | -- | A closing delimiter did not match the most recent opener.
    ParseErrMismatchedBrace !Brace !LocSpan !Brace !LocSpan
  | -- | A generic Looksee parse error, paired with its span converted to
    -- Mello locations. This constructor intentionally exposes both values:
    -- clients can render or inspect the original 'Err' while using 'LocSpan'
    -- directly for Mello-style source annotations.
    ParseErrLooksee !LocSpan !(Err Void)
  deriving stock (Show)

instance Exception ParseErr where
  displayException = \case
    ParseErrUnclosedList brace _ -> "unclosed list; expected '" <> [closeBraceChar brace] <> "' before end of input"
    ParseErrUnexpectedClose brace _ -> "unexpected closing delimiter '" <> [closeBraceChar brace] <> "'"
    ParseErrMismatchedBrace expected _ actual _ -> "mismatched closing delimiter; expected '" <> [closeBraceChar expected] <> "' but got '" <> [closeBraceChar actual] <> "'"
    ParseErrLooksee _ err -> displayException err

-- Specific parsers

docStartP :: (Monad m) => ParserT e m ()
docStartP = L.textP_ ";|"

commentStartP :: (Monad m) => ParserT e m ()
commentStartP = L.charP_ ';'

spaceNP :: (Monad m) => Int -> ParserT e m Int
spaceNP !acc = do
  mc <- L.lookP L.unconsP
  case mc of
    Just ';' -> do
      mds <- L.lookP (L.optP docStartP)
      case mds of
        Just _ -> pure acc
        Nothing -> L.dropWhileP (/= '\n') >>= spaceNP . (acc +)
    Just c | isSpace c -> L.dropWhileP isSpace >>= spaceNP . (acc +)
    _ -> pure acc

spaceP, space1P :: (Monad m) => ParserT e m ()
spaceP = void (spaceNP 0)
space1P = do
  acc <- spaceNP 0
  unless (acc > 0) L.space1P -- Use this to fail

stripP, stripEndP :: (Monad m) => ParserT e m a -> ParserT e m a
stripP p = spaceP *> p <* spaceP
stripEndP p = p <* spaceP

symP :: (Monad m) => ParserT e m Sym
symP = fmap Sym (cons1P isSymStart isSymCont)

charLitP :: (Monad m) => ParserT e m Char
charLitP = L.charP_ '\'' *> L.headP <* L.charP_ '\''

stringLitP :: (Monad m) => ParserT e m Text
stringLitP = L.strP '"'

openBraceP :: (Monad m) => ParserT e m Brace
openBraceP = commitSameP (fmap (\b -> b <$ L.charP_ (openBraceChar b)) [minBound .. maxBound])

closeBraceP :: (Monad m) => Brace -> ParserT e m ()
closeBraceP = L.charP_ . closeBraceChar

docLinesP :: (Monad m) => ParserT e m Doc
docLinesP = go True Empty
 where
  lineStartP isFirst = if isFirst then docStartP else commentStartP
  lineP isFirst = do
    lineStartP isFirst
    lin <- L.takeWhileP (/= '\n')
    L.charP_ '\n'
    pure lin
  go !isFirst !acc = do
    mx <- L.lookP (L.optP (lineStartP isFirst))
    case mx of
      Nothing -> pure (Doc acc)
      Just _ -> do
        lin <- lineP isFirst
        go False (acc :|> lin)

-- | A parser for S-expressions
sexpParser :: (Monad m) => ParserT e m OffsetSexp
sexpParser = stripP rootP
 where
  rootP =
    explainEmptyP "Not a recognizable Sexp" $
      L.spanAroundP MemoP $
        L.commitP
          [ (guard1P isListStart, L.labelP "list" listP)
          , (guard1P isQuoteStart, L.labelP "quote" quoteP)
          , (guard1P isUnquoteStart, L.labelP "unquote" unquoteP)
          , (guard1P isAtomStart, L.labelP "atom" atomP)
          , (docStartP, L.labelP "doc" docP)
          ]
  listP = do
    b <- stripEndP openBraceP
    ss <- stripEndP (L.sepByP space1P rootP)
    closeBraceP b
    pure (SexpListF b ss)
  quoteP = L.charP_ '`' *> fmap SexpQuoteF rootP
  unquoteP = L.charP_ ',' *> fmap SexpUnquoteF rootP
  guardNumStartP = do
    c <- L.headP
    if isDigit c
      then pure ()
      else do
        guard (c == '-')
        void (L.headP >>= guard . isDigit)
  atomP =
    SexpAtomF
      <$> L.commitP
        [ (guardNumStartP, L.labelP "num" (fmap (either AtomInt AtomSci) L.numP))
        , (guard1P isSymStart, L.labelP "sym" (fmap AtomSym symP))
        , (guard1P isStringStart, L.labelP "str" (fmap AtomStr stringLitP))
        , (guard1P isCharStart, L.labelP "char" (fmap AtomChar charLitP))
        ]
  docP = do
    doc <- docLinesP
    fmap (SexpDocF doc) rootP

parseSexp :: Text -> Either (Err Void) LocSexp
parseSexp txt = do
  sexp <- L.parse sexpParser txt
  let v = L.calculateLineCol txt
      mkLoc o = let (l, c) = L.lookupLineCol o v in Loc l c o
  pure (fmap (fmap mkLoc) sexp)

-- | Parse an s-expression with Mello-domain diagnostics.
--
-- Delimiter errors are classified before running the full parser. Generic
-- parser failures keep the original Looksee error and include a converted
-- Mello location span via 'ParseErrLooksee'.
parseSexpDetailed :: Text -> Either ParseErr LocSexp
parseSexpDetailed txt = do
  first (parseErrRecognize txt) (recognizeSexpText txt)
  first (parseErrLooksee txt) (parseSexp txt)

parseErrLooksee :: Text -> Err Void -> ParseErr
parseErrLooksee txt err = ParseErrLooksee (fmap (locAt txt) (errSpan err)) err

parseErrRecognize :: Text -> RecogErr -> ParseErr
parseErrRecognize txt = \case
  RecogErrUnclosedList brace openOffset eofOffset -> ParseErrUnclosedList brace (Span (locAt txt openOffset) (locAt txt eofOffset))
  RecogErrUnexpectedClose brace closeOffset -> ParseErrUnexpectedClose brace (offsetSpan txt closeOffset)
  RecogErrMismatchedBrace expected openOffset actual closeOffset -> ParseErrMismatchedBrace expected (offsetSpan txt openOffset) actual (offsetSpan txt closeOffset)

offsetSpan :: Text -> Int -> LocSpan
offsetSpan txt offset = Span (locAt txt offset) (locAt txt (offset + 1))

locAt :: Text -> Int -> Loc
locAt txt offset =
  let lookupTable = L.calculateLineCol txt
      (line, col) = L.lookupLineCol offset lookupTable
   in Loc line col offset

parseSexpI :: Text -> IO (Either (Err Void) LocSexp)
parseSexpI txt = do
  let ea = parseSexp txt
  case ea of
    Left e -> L.printE "<interactive>" txt e
    Right _ -> pure ()
  pure ea

parseSexpDetailedI :: Text -> IO (Either ParseErr LocSexp)
parseSexpDetailedI txt = do
  let ea = parseSexpDetailed txt
  case ea of
    Left (ParseErrLooksee _ err) -> L.printE "<interactive>" txt err
    Left err -> putStrLn (displayException err)
    Right _ -> pure ()
  pure ea