packages feed

distributors-0.4.0.0: src/Data/Profunctor/Grammar/Parsector.hs

{-|
Module      : Data.Profunctor.Grammar.Parsector
Description : grammar distributor with errors
Copyright   : (C) 2026 - Eitan Chatav
License     : BSD-style (see the file LICENSE)
Maintainer  : Eitan Chatav <eitan.chatav@gmail.com>
Stability   : provisional
Portability : non-portable

See Leijen,
[Parsec: Direct Style Monadic Parser Combinators For The Real World]
(https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/parsec-paper-letter.pdf)
-}

module Data.Profunctor.Grammar.Parsector
  ( -- * Parsector
    Parsector (..)
  , parsecP
  , unparsecP
  , ParsecState (..)
  , ParsecError (..)
  ) where

import Control.Applicative
import Control.Arrow
import Control.Category
import Data.Function (fix)
import Control.Lens
import Control.Lens.Grammar.BackusNaur
import Control.Lens.Grammar.Boole
import Control.Lens.Grammar.Kleene
import Control.Lens.Grammar.Symbol
import Control.Lens.Grammar.Token
import Control.Lens.PartialIso
import Control.Monad
import Control.Monad.Fail.Try
import Data.Profunctor
import Data.Profunctor.Distributor
import Data.Profunctor.Filtrator
import Data.Profunctor.Monoidal
import Data.Tree
import GHC.Exts
import Prelude hiding (id, (.))
import Witherable

{- | `Parsector` is an invertible @LL(1)@ parser which is intended
to provide detailed error information, based on [Parsec]
(https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/parsec-paper-letter.pdf).
-}
newtype Parsector s a b = Parsector
  {runParsector :: forall x. (ParsecState s b -> x) -> ParsecState s a -> x}

{- | Run `Parsector` as a parser: consume tokens from @s@,
left to right, returning a `ParsecState` whose `parsecResult`
is `Nothing` on failure and `Just` the output syntax value on success. -}
parsecP
  :: Categorized (Item s)
  => Parsector s a b
  -> s -- ^ input stream
  -> ParsecState s b
parsecP p s = runParsector p id (ParsecState False 0 s mempty Nothing)

{- | Run `Parsector` as a printer: given a syntax value @a@ and
an input stream, append tokens to @s@ left to right,
returning a `ParsecState` whose `parsecResult` is
`Nothing` on failure or `Just` a successful output syntax value,
in which case, `parsecStream` is the output stream. -}
unparsecP
  :: Categorized (Item s)
  => Parsector s a b
  -> a -- ^ input syntax
  -> s -- ^ input stream
  -> ParsecState s b
unparsecP p a s = runParsector p id (ParsecState False 0 s mempty (Just a))

{- | `ParsecState` is both the input and output type of the
underlying function inside `Parsector`.
@Parsector s a b@ is equivalent to

@ParsecState s a -> ParsecState s b@

So `ParsecState` has a dual interpretation as input and output. -}
data ParsecState s a = ParsecState
  { parsecLooked :: !Bool
    {- ^ `True` once the parser has consumed/produced at least one token
    since the last `<|>` / `try` decision point.
    Controls @LL(1)@ commitment: a failure with `parsecLooked` `True`
    is propagated immediately without trying alternatives.
    Reset to `False` by `try` on failure.
    -}
  , parsecOffset :: !Word
    -- ^ Number of tokens consumed from the start of the stream.
  , parsecStream :: s -- ^ stream
  , parsecError  :: ParsecError s
    {- ^ `ParsecError` channel.

    * If `parsecResult` is `Nothing`, this is the hard failure.
    * If `parsecResult` is `Just`, this is deferred error/hint info
      from empty-failing alternatives at the current position.

    `<|>` and `>>=` propagate and merge this field to preserve
    expected-token reporting on downstream failures.
    -}
  , parsecResult :: Maybe a
    {- ^
    As input, `Nothing` means parse mode and
    `Just` means print mode with an input syntax value.

    As output `Nothing` means failure (inspect `parsecError`) and
    `Just` means success with an output syntax value.
    -}
  }

{- | `ParsecError` is the error payload produced by `Parsector`,
stored in `parsecError`.
`ParsecError` is a `Monoid` and `Parsector` merges errors/hints
when control flow reaches the same offset without commitment.
-}
data ParsecError s = ParsecError
  { parsecExpect :: TokenClass (Item s)
    {- ^ Class of expected token `Item`s at the `parsecOffset`.
    `tokenClass`es and `Tokenized` combinators specify expectations.
    Under `<>`, expectations are combined with disjunction `>||<`.
    In case of a parse error, contrast with the actual `parsecStream`,
    which is either unexpectedly empty or begins with an unexpected token.
    -}
  , parsecLabels :: [Tree String]
    {- ^ Forest of `rule` labels active at the `parsecOffset`.
    Each `rule` wraps its inner labels in a new `Node`.
    `ruleRec` & `fail` also create label nodes.
    Under `<>`, forests are concatenated as siblings.
    Use `drawForest` to display.
    -}
  }

-- ParsecError instances
deriving stock instance
  ( Categorized (Item s)
  , Show (Item s), Show (Categorize (Item s))
  ) => Show (ParsecError s)
deriving stock instance
  ( Categorized (Item s)
  , Read (Item s), Read (Categorize (Item s))
  ) => Read (ParsecError s)
deriving stock instance Categorized (Item s) => Eq (ParsecError s)
deriving stock instance Categorized (Item s) => Ord (ParsecError s)
instance Categorized (Item s) => Semigroup (ParsecError s) where
  ParsecError e1 l1 <> ParsecError e2 l2 = ParsecError (e1 >||< e2) (l1 ++ l2)
instance Categorized (Item s) => Monoid (ParsecError s) where
  mempty = ParsecError falseB []

-- ParsecState instances
deriving stock instance Functor (ParsecState s)
deriving stock instance Foldable (ParsecState s)
deriving stock instance Traversable (ParsecState s)
deriving stock instance
  ( Categorized (Item s)
  , Show (Item s), Show (Categorize (Item s))
  , Show a, Show s
  ) => Show (ParsecState s a)
deriving stock instance
  ( Categorized (Item s)
  , Read (Item s), Read (Categorize (Item s))
  , Read a, Read s
  ) => Read (ParsecState s a)
deriving stock instance
  ( Categorized (Item s)
  , Eq a, Eq s
  ) => Eq (ParsecState s a)
deriving stock instance
  ( Categorized (Item s)
  , Ord a, Ord s
  ) => Ord (ParsecState s a)

-- Parsector instances
instance
  ( Categorized token, Item s ~ token
  , Cons s s token token, Snoc s s token token
  ) => Tokenized token (Parsector s token token) where
  anyToken = tokenClass anyToken
  token t = tokenClass (token t)
  oneOf ts = tokenClass (oneOf ts)
  notOneOf ts = tokenClass (notOneOf ts)
  asIn cat = tokenClass (asIn cat)
  notAsIn cat = tokenClass (notAsIn cat)
instance
  ( Categorized token, Item s ~ token
  , Cons s s token token, Snoc s s token token
  ) => TokenAlgebra token (Parsector s token token) where
    tokenClass test = Parsector $ \callback query ->
      let
        stream = parsecStream query
        mode = parsecResult query
        offset = parsecOffset query
        replyOk tok str = query
          { parsecLooked = True
          , parsecError  = mempty
          , parsecStream = str
          , parsecOffset = offset + 1
          , parsecResult = Just tok
          }
        replyErr = query
          { parsecError  = ParsecError test []
          , parsecResult = Nothing }
      in
        callback $ case mode of
          -- print mode
          Just tok
            | tokenClass test tok -> replyOk tok (snoc stream tok)
            | otherwise -> replyErr
          -- parse mode
          Nothing -> case uncons stream of
            Just (tok, rest)
              | tokenClass test tok -> replyOk tok rest
              | otherwise -> replyErr
            Nothing -> replyErr
instance BackusNaurForm (Parsector s a b) where
  -- | Wraps inner `parsecLabels` in a new `Node name` on failure.
  -- Has no effect on success.
  rule name p = Parsector $ \callback query ->
    flip (runParsector p) query $ \reply -> callback $
      case parsecResult reply of
        Nothing -> reply
          { parsecError =
              let ParsecError expect labels = parsecError reply
              in ParsecError expect [Node name labels]
          }
        Just _ -> reply
  ruleRec name = rule name . fix
instance
  ( Categorized token, Item s ~ token
  , Cons s s token token, Snoc s s token token
  ) => TerminalSymbol token (Parsector s () ())
instance Functor (Parsector s a) where
  fmap = rmap
instance Categorized (Item s) => Applicative (Parsector s a) where
  pure b = Parsector $ \callback query ->
    callback query { parsecResult = Just b }
  (<*>) = ap

instance Categorized (Item s) => Monad (Parsector s a) where
  return = pure
  p >>= f = Parsector $ \callback query ->
    flip (runParsector p) query $ \reply ->
      case parsecResult reply of
        Nothing -> callback reply { parsecResult = Nothing }
        Just b ->
          let
            hintP  = parsecError reply
            fQuery = reply
              { parsecLooked = False
              , parsecError  = mempty
              , parsecResult = parsecResult query
              }
          in
            flip (runParsector (f b)) fQuery $ \fReply -> callback $
              if parsecLooked fReply
                then fReply
                else fReply
                  { parsecLooked = parsecLooked reply
                  , parsecError  = hintP <> parsecError fReply
                  }
instance Categorized (Item s) => Alternative (Parsector s a) where
  -- | Always fails without consuming input; expects nothing.
  empty = Parsector $ \callback query ->
    callback query { parsecError = mempty, parsecResult = Nothing }
  p <|> q = Parsector $ \callback query ->
    flip (runParsector p) query $ \replyP -> callback $
      case parsecResult replyP of
        -- if p succeeds, take p's branch
        Just _ -> replyP
        -- if p failed after consuming (committed), propagate immediately
        Nothing | parsecLooked replyP -> replyP
        -- if p failed without consuming, try q
        Nothing ->
          let errP = parsecError replyP
          in flip (runParsector q) query $ \replyQ ->
          case (parsecLooked replyQ, parsecResult replyQ) of
            -- q consumed (ok or err): propagate as-is, drop errP
            (True, _)         -> replyQ
            -- q empty ok: carry errP forward as hint for downstream
            (False, Just _)   -> replyQ { parsecError = errP <> parsecError replyQ }
            -- both empty fail: merge errors
            (False, Nothing)  -> replyP { parsecError = errP <> parsecError replyQ }
instance Categorized (Item s) => MonadPlus (Parsector s a)
instance Categorized (Item s) => MonadFail (Parsector s a) where
  fail msg = rule msg empty
instance Categorized (Item s) => MonadTry (Parsector s a) where
  -- | On failure, resets `parsecLooked` to @False@, allowing
  -- the enclosing `<|>` to try the next alternative even if @p@
  -- consumed input. Also restores the stream/offset decision state.
  -- Has no effect on success.
  try p = Parsector $ \callback query ->
    flip (runParsector p) query $ \reply -> callback $
      case parsecResult reply of
        Nothing -> query
          { parsecLooked = False
          , parsecError  = parsecError reply
          , parsecResult = Nothing
          }
        Just _ -> reply
instance Categorized (Item s) => Filterable (Parsector s a) where
  mapMaybe = dimapMaybe Just
instance Category (Parsector s) where
  id = Parsector id
  Parsector q . Parsector p = Parsector (p . q)
instance Categorized (Item s) => Arrow (Parsector s) where
  arr f = Parsector $ \callback reply -> callback (f <$> reply)
  (***) = (>*<)
  first = first'
  second = second'
instance Categorized (Item s) => ArrowZero (Parsector s) where
  zeroArrow = empty
instance Categorized (Item s) => ArrowPlus (Parsector s) where
  (<+>) = (<|>)
instance Categorized (Item s) => ArrowChoice (Parsector s) where
  (+++) = (>+<)
  left = left'
  right = right'
instance Profunctor (Parsector s) where
  dimap f g (Parsector p) = Parsector $
    dimap (lmap (fmap g)) (lmap (fmap f)) p
instance Strong (Parsector s) where
  first' p = Parsector $ \callback reply0 ->
    flip (runParsector p) (fst <$> reply0) $ \reply1 ->
      callback reply1
        { parsecResult = (,)
            <$> parsecResult reply1
            <*> (snd <$> parsecResult reply0)
        }
  second' p = Parsector $ \callback reply0 ->
    flip (runParsector p) (snd <$> reply0) $ \reply1 ->
      callback reply1
        { parsecResult = (,)
            <$> (fst <$> parsecResult reply0)
            <*> parsecResult reply1
        }
instance Categorized (Item s) => Choice (Parsector s) where
  left' = alternate . Left
  right' = alternate . Right
instance Categorized (Item s) => Distributor (Parsector s)
instance Categorized (Item s) => Alternator (Parsector s) where
  alternate (Left p) = Parsector $ \callback query -> callback $
    let
      replyOk = query
        { parsecResult = case parsecResult query of
            Nothing         -> Nothing
            Just (Left a)   -> Just a
            Just (Right _)  -> Nothing
        }
      replyErr = query { parsecError = mempty, parsecResult = Nothing }
    in
      case (parsecResult query, parsecResult replyOk) of
        (Just _, Nothing) -> replyErr
        _________________ ->
          flip (runParsector p) replyOk $ \reply -> reply
            { parsecResult = fmap Left (parsecResult reply) }
  alternate (Right p) = Parsector $ \callback query -> callback $
    let
      replyOk = query
        { parsecResult = case parsecResult query of
            Nothing         -> Nothing
            Just (Left _)   -> Nothing
            Just (Right b)  -> Just b
        }
      replyErr = query { parsecError = mempty, parsecResult = Nothing }
    in
      case (parsecResult query, parsecResult replyOk) of
        (Just _, Nothing) -> replyErr
        _________________ ->
          flip (runParsector p) replyOk $ \reply -> reply
            { parsecResult = fmap Right (parsecResult reply) }
  optionP def p = Parsector $ \callback query ->
    case parsecResult query of
      Nothing -> runParsector (p <|> pureP def) callback query
      Just _ -> runParsector (pureP def <|> p) callback query
instance Categorized (Item s) => Cochoice (Parsector s) where
  unleft = fst . filtrate
  unright = snd . filtrate
instance Categorized (Item s) => Filtrator (Parsector s) where
  filtrate p =
    ( Parsector $ \callback query ->
        flip (runParsector p) (Left <$> query) $ \reply ->
          callback reply
          { parsecError = case parsecResult reply of
            Just (Right _) -> mempty
            _ -> parsecError reply
          , parsecResult =
            parsecResult reply >>= either Just (const Nothing)
          }
    , Parsector $ \callback query ->
        flip (runParsector p) (Right <$> query) $ \reply ->
          callback reply
          { parsecError = case parsecResult reply of
            Just (Left _) -> mempty
            _ -> parsecError reply
          , parsecResult =
            parsecResult reply >>= either (const Nothing) Just
          }
    )