packages feed

crucible-syntax-0.4.1: src/Lang/Crucible/Syntax/ExprParse.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ViewPatterns #-}

module Lang.Crucible.Syntax.ExprParse
  ( SyntaxParse
  , syntaxParseIO

  -- * Errors
  , SyntaxError(..)
  , printSyntaxError

  -- * Testing utilities
  , TrivialAtom(..)
  , test
  ) where

import Control.Applicative
import Control.Lens hiding (List, cons, backwards)
import Control.Monad (MonadPlus(..), ap)
import Control.Monad.IO.Class (MonadIO)
import Control.Monad.Reader (MonadReader(..), ReaderT(..))
import qualified Control.Monad.State.Strict as Strict

import Data.Foldable as Foldable
import Data.List
import qualified Data.List.NonEmpty as NE
import Data.List.NonEmpty (NonEmpty(..))
import Data.String
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T

import GHC.Stack

import Lang.Crucible.Syntax.SExpr

import qualified Text.Megaparsec as MP

import Lang.Crucible.Syntax.Monad

data Search a = Try a (Search a) | Fail | Cut
  deriving Functor

instance Applicative Search where
  pure x = Try x Fail
  (<*>) = ap

instance Alternative Search where
  empty = Fail
  x <|> y =
    case x of
      Try first rest -> Try first (rest <|> y)
      Fail -> y
      Cut -> Cut

instance Monad Search where
  m >>= f =
    case m of
      Try x more -> f x <|> (more >>= f)
      Fail -> Fail
      Cut -> Fail

instance MonadPlus Search where
  mzero = empty
  mplus = (<|>)

instance Semigroup (Search a) where
  (<>) = (<|>)

instance Monoid (Search a) where
  mempty  = empty

instance Foldable Search where
  foldMap f (Try x xs) = f x `mappend` foldMap f xs
  foldMap _ _ = mempty

  toList (Try x xs) = x : toList xs
  toList _          = []

instance Traversable Search where
  traverse f (Try x xs) = Try <$> f x <*> traverse f xs
  traverse _ Fail = pure Fail
  traverse _ Cut = pure Cut

delimitSearch :: Search a -> Search a
delimitSearch (Try first rest) = Try first $ delimitSearch rest
delimitSearch Fail = Fail
delimitSearch Cut = Fail

cutSearch :: Search a
cutSearch = Cut

data Failure atom = Ok | Oops Progress (NonEmpty (Reason atom))
  deriving (Functor, Show)

instance Semigroup (Failure atom) where
  Ok              <> e2 = e2
  e1@(Oops _ _)   <> Ok = e1
  e1@(Oops p1 r1) <> e2@(Oops p2 r2) =
    case compare p1 p2 of
      LT -> e2
      GT -> e1
      EQ -> Oops p1 (r1 <> r2)

instance Monoid (Failure atom) where
  mempty = Ok

data P atom a = P { _success :: Search a
                  , _failure :: Failure atom
                  }
  deriving Functor

instance Semigroup (P atom a) where
  P s1 f1 <> P s2 f2 = P (s1 <> s2) (f1 <> f2)

instance Monoid (P atom a) where
  mempty = P mempty mempty

instance Applicative (P atom) where
  pure x = P (pure x) mempty
  f <*> x = ap f x

instance Alternative (P atom) where
  empty = mempty
  (<|>) = mappend

instance Monad (P atom) where
  (P xs e) >>= f = mappend (foldMap f xs) (P empty e)

instance MonadPlus (P atom) where
  mzero = empty
  mplus = (<|>)

newtype STP atom a = STP { runSTP :: IO (P atom a) }
  deriving (Functor, Semigroup, Monoid)

instance Applicative (STP atom) where
  pure = STP . pure . pure
  (<*>) = ap

instance Monad (STP atom) where
  STP m >>= f = STP $ do
    P xs e <- m
    mappend (runSTP (foldMap f xs)) (return $ P empty e)

instance MonadIO (STP atom) where
  liftIO m = STP $ return <$> m

data SyntaxParseCtx atom =
  SyntaxParseCtx { _parseProgress :: Progress
                 , _parseReason :: Reason atom
                 , _parseFocus :: Syntax atom
                 }
  deriving Show

parseProgress :: Simple Lens (SyntaxParseCtx atom) Progress
parseProgress = lens _parseProgress (\s v -> s { _parseProgress = v })

parseReason :: Simple Lens (SyntaxParseCtx atom) (Reason atom)
parseReason = lens _parseReason (\s v -> s { _parseReason = v })

parseFocus :: Simple Lens (SyntaxParseCtx atom) (Syntax atom)
parseFocus = lens _parseFocus (\s v -> s { _parseFocus = v })

-- | The default parsing monad. Use its 'MonadSyntax' instance to write parsers.
newtype SyntaxParse atom a =
  SyntaxParse { runSyntaxParse :: ReaderT (SyntaxParseCtx atom)
                                          (STP atom)
                                          a }
  deriving ( Functor, Applicative, Monad
           , MonadReader (SyntaxParseCtx atom), MonadIO
           )

instance Alternative (SyntaxParse atom) where
  empty =
    SyntaxParse $ ReaderT $ \(SyntaxParseCtx p r _) ->
      STP $ return $ P empty (Oops p (pure r))
  (SyntaxParse (ReaderT x)) <|> (SyntaxParse (ReaderT y)) =
    SyntaxParse $ ReaderT $ \ctx -> STP $ do
      a <- runSTP $ x ctx
      b <- runSTP $ y ctx
      return $ a <|> b

instance MonadPlus (SyntaxParse atom) where
  mzero = empty
  mplus = (<|>)

instance MonadSyntax atom (SyntaxParse atom) where
  anything = view parseFocus
  progress = view parseProgress
  withFocus stx = local $ set parseFocus stx
  withProgress f = local $ over parseProgress f
  withReason r = local $ set parseReason r
  cut =
    SyntaxParse $
    ReaderT $
    \(SyntaxParseCtx p r _) ->
      STP $ return $
      P cutSearch (Oops p (pure r))
  delimit (SyntaxParse (ReaderT f)) =
    SyntaxParse $
    ReaderT $
    \r -> STP $ do
      P s e <- runSTP $ f r
      return $ P (delimitSearch s) e
  call (SyntaxParse (ReaderT p)) =
    SyntaxParse $
    ReaderT $
    \r -> STP $ do
      P s e <- runSTP $ p r
      return $ case s of
        Try x _ -> P (Try x Fail) Ok
        Cut -> P Cut e
        Fail -> P Fail e

-- | Syntax errors explain why the error occurred.
data SyntaxError atom = SyntaxError (NonEmpty (Reason atom))
  deriving (Show, Eq)

-- | Convert an internal error structure into a form suitable for
-- humans to read.
printSyntaxError :: IsAtom atom => SyntaxError atom -> Text
printSyntaxError (SyntaxError rs) =
  T.intercalate "\n\tor\n" $ nub $ map printGroup $ groupReasons rs
 where
    reasonPos (Reason found _) = syntaxPos found
    groupReasons reasons =
      [ (reasonPos repr, g)
      | g@(repr :| _) <- NE.groupBy (\x y -> reasonPos x == reasonPos y) (NE.toList reasons)
      ]
    printGroup (p, r@(Reason found _) :| more) =
      T.concat
      [ "At ", T.pack (show p)
      , ", expected ", T.intercalate " or " (nub $ sort [ wanted | Reason _ wanted <- r:more ])
      , " but got ", toText mempty found]

-- | Attempt to parse the given piece of syntax, returning the first success found,
--   or the error(s) with the greatest progress otherwise.
syntaxParseIO :: IsAtom atom => SyntaxParse atom a -> Syntax atom -> IO (Either (SyntaxError atom) a)
syntaxParseIO p stx = do
  (P yes no) <-
        runSTP $ runReaderT (runSyntaxParse p) $
          SyntaxParseCtx emptyProgress (Reason stx (T.pack "bad syntax")) stx
  case Foldable.toList yes of
    [] ->
      return $ Left $ SyntaxError $
        case no of
          Ok        -> error "Internal error: no reason provided, yet no successful parse found."
          Oops _ rs -> rs
    (r:_) -> return $ Right r

-- | A trivial atom, which is a wrapper around 'Text', for use when testing the library.
newtype TrivialAtom = TrivialAtom Text deriving (Show, Eq)

instance IsAtom TrivialAtom where
  showAtom (TrivialAtom x) = x

instance IsString TrivialAtom where
  fromString x = TrivialAtom (fromString x)

-- | Test a parser on some input, displaying the result.
test :: (HasCallStack, Show a) => Text -> SyntaxParse TrivialAtom a -> IO ()
test txt p =
  case MP.parse (skipWhitespace *> sexp (TrivialAtom <$> identifier) <* MP.eof) "input" txt of
     Left err -> putStrLn "Reader error: " >> putStrLn (MP.errorBundlePretty err)
     Right sexpr ->
       syntaxParseIO p sexpr >>= \case
         Left e -> T.putStrLn (printSyntaxError e)
         Right ok -> print ok