packages feed

prosidyc-0.3.0.0: src/Prosidy/Compile/Run.hs

{-|
Module      : Prosidy.Compile.Run
Description : A basic intepreter for 'Prosidy.Compile.Core.Rules'.
Copyrighr   : ©2020 James Alexander Feldman-Crough
License     : MPL-2.0
Maintainer  : alex@fldcr.com
-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
module Prosidy.Compile.Run (RunError(..), RunErrors(..), RunT, Run, run, runT) where

import           Lens.Micro

import qualified Prosidy                       as P
import qualified Prosidy.Source                as PS
import           Prosidy.Types.Series           ( pattern Empty
                                                , pattern (:<:)
                                                , pattern (:<<:)
                                                )

import Control.Exception (Exception(..))
import           Prosidy.Compile.Core
import           Data.Function                  ( on )
import           Data.Functor.Identity          ( Identity(..) )
import           Data.Bifunctor                 ( Bifunctor(..) )
import           Data.Profunctor                ( Profunctor(..)
                                                , Strong(..)
                                                )
import Data.Either.Valid (Valid(..))
import qualified Data.Either.Valid as Valid
import           Data.Text                      ( Text )
import Control.Monad (unless)
import Data.Set (Set)
import Data.Foldable (toList, foldl')
import qualified Data.HashMap.Strict as HM
import Data.Semigroup (Semigroup(..))

import Data.Text.Prettyprint.Doc (Pretty(..), (<+>))
import qualified Data.Text.Prettyprint.Doc as PP
import qualified Data.Text.Prettyprint.Doc.Render.String as PPS

import qualified Data.HashSet as HashSet
import qualified Data.Set                      as Set
import qualified Data.Text as Text

-- | 'RunT' specialized to 'Identity'.
type Run = RunT Identity

-- | An interpreter over 'Rules'.
newtype RunT f t a = RunT
  { _run :: t -> Observe -> Valid RunErrors (f a, Observe) }
--   deriving (Functor, Applicative) via Compose ((->) t) (Compose (Result RunError) _)

instance Functor f => Functor (RunT f t) where
    fmap = rmap

instance Applicative f => Applicative (RunT f t) where
    pure x = RunT $ \_ o -> pure (pure x, o)

    RunT lhs <*> RunT rhs = RunT $ \t obs ->
      let
        combine ~(f, o1) ~(x, o2) = (f <*> x, o1 <> o2)
      in
        combine <$> lhs t obs <*> rhs t obs

instance Applicative f => Alternative (RunT f t) where
    empty = RunT $ \_ _ -> empty

    RunT lhs <|> RunT rhs = RunT $ \t obs ->
        lhs t obs <|> rhs t obs 

instance Functor f => Profunctor (RunT f) where
    dimap f g = RunT . go . _run
      where
        go r t obs = first (fmap g) <$> r (f t) obs

instance Functor f => Strong (RunT f) where
    first' = RunT . go . _run
      where
        go r (t, c) obs = first (fmap (, c)) <$> r t obs
    second' = RunT . go . _run
      where
        go r (c, t) obs = first (fmap (c ,)) <$> r t obs

-- | Run a 'Run' interpreter to completion.
run :: i -> Run i a -> Either RunErrors a
run = (fmap runIdentity .) . runT

-- | Run a 'RunT' interpreter to completion.
runT :: i -> RunT f i a -> Either RunErrors (f a)
runT i = fmap fst . Valid.toEither . (\r -> _run r i mempty)

runWith :: (t -> Valid RunErrors (f a)) -> RunT f t a
runWith f = RunT $ \t obs -> (, obs) <$> f t

-- | Errors that may be returned from the interpreter.
data RunError =
    Group Location (Set RunError)
    -- ^ Groups a set of errors with a location for more helpful error 
    -- messages.
  | MatchError Text
    -- ^ Expected a different type. Thrown on failed matches of sum types.
  | ParseError P.Key String
    -- ^ The provided parser failed to parse a setting.
  | RequiredSetting P.Key
    -- ^ A setting was required, but not found on a node.
  | TooFewElements
    -- ^ Expected more elements when matching sequentially.
  | TooManyElements
    -- ^ Expected fewer elements when matching sequentially.
  | UnexpectedProperties (HashSet.HashSet P.Key) (HashSet.HashSet P.Key)
    -- ^ A property was found on a node, but not mentioned in its specification.
  | UnexpectedSettings (HashSet.HashSet P.Key) (HashSet.HashSet P.Key)
    -- ^ A setting was found on a node, but not mentioned in its specification.
  deriving (Show, Eq, Ord)

instance Exception RunError where
    displayException = prettyString

instance Pretty RunError where
    pretty (Group loc errors) = PP.nest 4 $ PP.vsep
        [ "Encountered" <+> errorNoun <+> "in" <+> pretty loc
        , pretty (RunErrors errors)
        ] 
      where
        errorCount = length errors
        errorNoun 
          | errorCount == 1 = "an error"
          | otherwise       = pretty errorCount <+> "errors"

    pretty (MatchError desc) =
        "Expected" <+> pretty desc

    pretty (ParseError key msg) =
        "Failed to parse setting" <+> pretty key <> ":" <+> pretty msg

    pretty (RequiredSetting key) =
        "Node is missing the required setting" <+> pretty key

    pretty TooFewElements =
        "Expected one or more additional nodes within the current context."

    pretty TooManyElements =
        "Expected no further elements in the current context."

    pretty (UnexpectedProperties allowed got) = PP.nest 4 $ PP.vsep 
        [ "Encountered at least one unexpected property on the current node."
        , "Allowed properties:   " <+> pretty (toList allowed)
        , "Unexpected properties:" <+> pretty (toList got)
        ]

    pretty (UnexpectedSettings allowed got) = PP.nest 4 $ PP.vsep 
        [ "Encountered at least one unexpected setting on the current node."
        , "Allowed settings:   " <+> pretty (toList allowed)
        , "Unexpected settings:" <+> pretty (toList got)
        ]

-- | A newtype wrapper over a set of 'RunError's. 
--
-- This is defined to allow an instances of 'Exception' and 'Pretty' for error
-- sets.
newtype RunErrors = RunErrors (Set RunError)
  deriving (Show, Eq, Ord)

instance Exception RunErrors where
    displayException = prettyString

instance Semigroup RunErrors where
    lhs@(RunErrors lset) <> rhs@(RunErrors rset) 
      | null lset = rhs
      | null rset = lhs
      | otherwise = RunErrors $ combineErrors [lhs, rhs]
    sconcat = RunErrors . combineErrors

instance Monoid RunErrors where
    mempty = RunErrors mempty
    mconcat = RunErrors . combineErrors

instance Pretty RunErrors where
    pretty = \(RunErrors es) -> 
        mconcat (zipWith combine delims (toList es)) <> PP.rbracket
      where
        delims = PP.lbracket : repeat PP.comma
        combine delim item =
            delim <> PP.flatAlt " " mempty <> pretty item <> PP.line'

runErrors :: RunErrors -> Set RunError
runErrors = \(RunErrors es) -> es

failure :: RunError -> Valid RunErrors a
failure = Invalid . RunErrors . Set.singleton

groupErrors :: P.HasLocation t => RunT f t a -> RunT f t a
groupErrors (RunT f) = RunT $ \i o ->
    case f i o of
        Invalid errors@(RunErrors errorSet)
          | shouldWrap errors 
          , Just loc <- i ^? P.location
          -> Invalid . RunErrors . Set.singleton $ Group (Location loc) errorSet
        other -> other

shouldWrap :: RunErrors -> Bool
shouldWrap (RunErrors es) 
  | count >= 2 = True
  | otherwise  = any (\case Group{} -> False; _ -> True) es
  where count = length es

combineErrors :: Foldable f => f RunErrors -> Set RunError
combineErrors =
      HM.foldlWithKey' go mempty 
    . groupGroups 
    . foldMap runErrors
  where
    go acc key val = Set.union acc $ case key of
        Just loc -> Set.singleton (Group (Location loc) val)
        Nothing  -> val

groupGroups :: Foldable f => f RunError -> HM.HashMap (Maybe P.Location) (Set RunError)
groupGroups = foldl' 
    (\acc -> \case
        Group (Location loc) e -> HM.insertWith (<>) (Just loc) e acc
        other -> HM.insertWith (<>) Nothing (Set.singleton other) acc)
    mempty
    
newtype Location = Location P.Location
  deriving (Show, Eq)

instance Pretty Location where
    pretty (Location l) = pretty l

instance Ord Location where
    compare = compare `on` \(Location loc) ->
        (PS.sourceName (PS.locationSource loc), PS.locationOffset loc)

-------------------------------------------------------------------------------
instance Applicative f => Context (RunT f) where
    type Local (RunT f) = f

    runSelf  = RunT $ \t obs -> pure (pure t, obs)
    liftRule r = RunT $ \_ obs -> pure (r, obs)

instance Applicative f => Interpret (RunT f) P.Block where
    runRule = groupErrors . \case
        BlockRuleBlockTag nested -> RunT $ \block obs -> case block of
            P.BlockTag tag -> _run (pedantic $ interpret nested) tag mempty
            _              -> (, obs) <$> expected "BlockTag"

        BlockRuleLiteralTag nested -> RunT $ \block obs -> case block of
            P.BlockLiteral tag -> _run (pedantic $ interpret nested) tag mempty
            _                  -> (, obs) <$> expected "BlockLiteral"

        BlockRuleParagraph nested -> RunT $ \block obs -> case block of
            P.BlockParagraph pg -> _run (interpret nested) pg mempty
            _                   -> (, obs) <$> expected "BlockParagraph"

instance Applicative f => Interpret (RunT f) P.Document where
    runRule (DocumentRule regionRule) =
        RunT $ _run (runRule regionRule) . P.documentToRegion

instance Applicative f => Interpret (RunT f) P.Fragment where
    runRule = \case
        FragmentRuleLocation callback -> 
            runWith $ pure . pure . callback . P.fragmentLocation

        FragmentRuleText callback -> 
            runWith $ pure . pure . callback . P.fragmentText

instance Applicative f => Interpret (RunT f) P.Inline where
    runRule = \case
        InlineRuleBreak item -> runWith $ \inline -> case inline of
            P.Break -> pure $ pure item
            _       -> expected "Break"

        InlineRuleInlineTag nested -> RunT $ \inline obs -> case inline of
            P.InlineTag tag -> _run (pedantic $ interpret nested) tag mempty
            _               -> (, obs) <$> expected "InlineTag"

        InlineRuleFragment nested -> RunT $ \inline obs -> case inline of
            P.InlineText fragment -> _run (interpret nested) fragment mempty
            _                     -> (, obs) <$> expected "InlineText"

instance Applicative f => Interpret (RunT f) P.Metadata where
    runRule = \case
        MetadataRuleProperty callback key ->
            observeProperty key *>
            runWith (pure . pure . callback . (^. P.hasProperty key))

        MetadataRuleSetting parse def key -> 
            observeSetting key *>
            runWith (\metadata ->
                case metadata ^. P.atSetting key of
                    Nothing -> maybe (failure $ RequiredSetting key) (pure . pure) def
                    Just raw ->
                        Valid.fromEither
                            . bimap (RunErrors . Set.singleton . ParseError key) pure
                            $ parse raw)

        MetadataRuleAllowUnknown x -> RunT $ \_ _ ->
            pure (pure x, NoObserve)

instance Applicative f => Interpret (RunT f) P.Paragraph where
    runRule = \case
        ParagraphRuleContent nested ->
            RunT $ _run (interpret nested) . P.paragraphContent

        ParagraphRuleLocation callback ->
            runWith $ pure . pure . callback . P.paragraphLocation

instance (Applicative f, Interpret (RunT f) t) => Interpret (RunT f) (P.Region t) where
    runRule = \case
        RegionRuleContent nested -> RunT $ \t obs ->
            second (const obs)
            <$> _run (interpret nested) (P.regionContent t) mempty

        RegionRuleLocation callback ->
            runWith $ Valid.Valid . pure @f . callback . P.regionLocation

        RegionRuleMetadata rule ->
            RunT $ _run (runRule rule) . P.regionMetadata

instance (Applicative f, Interpret (RunT f) t) => Interpret (RunT f) (P.Tag t) where
    runRule = \case
        TagRuleKey key ret ->
            runWith
                $ \tag -> if P.tagName tag == key
                      then pure $ pure ret
                      else failure . MatchError $ "rawKey == " <> Text.pack (show $ P.rawKey key)

        TagRuleRegion nested -> RunT $ _run (runRule nested) . P.tagToRegion

instance (Applicative f, Interpret (RunT f) t) => Interpret (RunT f) (P.Series t) where
    runRule = \case
        SeriesRuleNext rule -> RunT $ \series -> case series of
            x :<: xs -> _run (runRule rule) (x :<<: xs)
            Empty    -> const $ failure TooFewElements

        SeriesRuleEmpty ret -> runWith $ \series -> case series of
            _ :<: _ -> failure TooManyElements
            Empty   -> pure $ pure ret

instance (Applicative f, Interpret (RunT f) t) => Interpret (RunT f) (P.SeriesNE t) where
    runRule (SeriesNERule combine rule rules) = RunT $ \(x :<<: xs) o ->
        (\(f1, o1) (f2, o2) -> (combine <$> f1 <*> f2, o1 <> o2))
            <$> _run (interpret rule) x o
            <*> _run (interpret rules) xs o

instance Applicative f => Interpret (RunT f) Text

expected :: Text -> Valid RunErrors a
expected = failure . MatchError

-------------------------------------------------------------------------------
data Observe = 
    NoObserve
  | Observe !Observing
  deriving Show

instance Semigroup Observe where
    Observe lhs <> Observe rhs = Observe (lhs <> rhs)
    NoObserve <> _ = NoObserve
    _ <> NoObserve = NoObserve

instance Monoid Observe where
    mempty = NoObserve

data Observing = Observing
  { obsProperties :: !(HashSet.HashSet P.Key)
  , obsSettings :: !(HashSet.HashSet P.Key)
  }
  deriving Show

instance Semigroup Observing where
    Observing a b <> Observing z y = Observing (a <> z) (b <> y)

instance Monoid Observing where
    mempty = Observing mempty mempty

observeProperty :: Applicative f => P.Key -> RunT f t ()
observeProperty k = RunT $ \_ o -> 
  let
    o' = case o of
        Observe obs -> Observe $ obs 
            { obsProperties = HashSet.insert k $ obsProperties obs }
        NoObserve -> o
  in
    pure (pure (), o')

observeSetting :: Applicative f => P.Key -> RunT f t ()
observeSetting k = RunT $ \_ o -> 
  let
    o' = case o of
        Observe obs -> Observe $ obs 
            { obsSettings = HashSet.insert k $ obsSettings obs }
        NoObserve -> o
  in
    pure (pure (), o')

pedantic :: P.HasMetadata t => RunT f t a -> RunT f t a
pedantic = RunT . go . _run
  where
    check t = \case
        Observe (Observing props settings) -> do
            let unexpectedProps = t ^.. P.properties . folded . filtered (not . flip HashSet.member props)
                unexpectedSettings = t ^.. P.settings . P._Assoc . to HM.keys . folded . filtered (not . flip HashSet.member settings)
            unless (null unexpectedProps) $
                failure (UnexpectedProperties props (HashSet.fromList unexpectedProps))
            unless (null unexpectedSettings) $
                failure (UnexpectedSettings settings (HashSet.fromList unexpectedSettings))
            pure ()

        NoObserve -> 
            pure ()

    go f t obs = 
      let
        result = f t $ case obs of
            NoObserve -> Observe mempty
            _         -> obs
      in
        result <* case result of
            Valid.Valid (_, o) -> check t o
            _ -> pure ()

-------------------------------------------------------------------------------
prettyString :: Pretty a => a -> String
prettyString = 
    PPS.renderString
        . PP.layoutPretty (PP.defaultLayoutOptions { PP.layoutPageWidth = PP.Unbounded }) 
        . pretty