prosidyc-0.2.0.0: src/Prosidy/Compile/Core.hs
{-|
Module : Prosidy.Compile.Core
Description : Primitive type definitions and functions.
Copyright : ©2020 James Alexander Feldman-Crough
License : MPL-2.0
Maintainer : alex@fldcr.com
-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE QuantifiedConstraints #-}
{-# LANGUAGE GeneralisedNewtypeDeriving #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE LambdaCase #-}
module Prosidy.Compile.Core
( RuleF(..)
, RuleT
, Rule
, CanMatch(evalPattern, noMatchError)
, Pattern(..)
, Interpret
, BlockRegion
, InlineRegion
, LiteralRegion
, interpretWith
, evalPatterns
, rule
)
where
import Lens.Micro
import Prosidy.Compile.Error
import Prosidy ( Key
, HasLocation
, HasMetadata
, HasContent(Content)
)
import Data.Text ( Text )
import Data.Bifunctor ( Bifunctor(..) )
import Data.List.NonEmpty ( NonEmpty(..) )
import Control.Monad.Trans ( MonadTrans(..) )
import Data.Functor.Identity ( Identity )
import qualified Prosidy
import qualified Control.Applicative.Free.Final
as Ap
-- | A single compilation rule. Parameterized by the following types:
--
-- * @input@: The type of the Prosidy node that is currently accessible.
--
-- * @error@: Allows users to specify a custom error type to be used for
-- throwing errors. 'Data.Void.Void' can be used to rely solely on
-- the errors built into this library.
--
-- * @context@: A 'Monad' for performing contextual computation beyond what
-- is provided by this library. If additional contextual computation is not
-- desired, use 'Data.Functor.Identity.Identity' as the type.
--
-- * @output@: The resulting output type.
newtype RuleT input error context output = RuleT
(Ap.Ap (RuleF input error context) output)
deriving (Functor, Applicative)
instance MonadTrans (RuleT input error) where
lift = rule . Lift . const . fmap Right
-- | 'RuleT' without a contextual environment.
type Rule input error = RuleT input error Identity
-- | Lifts a 'RuleF' into a 'RuleT'.
rule :: RuleF i e f o -> RuleT i e f o
rule = RuleT . Ap.liftAp
-- | The control functor for compiling Prosidy elements. Each action
-- corresponds to an action to perform on the @input@ variable.
--
-- See 'RuleT' and 'Rule' for use of this type.
data RuleF input error context output where
-- | Throw an error.
Fail
::Error error
-> RuleF input error context output
-- | Embed a raw action as a rule. Note: Please avoid using this if
-- possible: it breaks static introspection!
Lift
::(input -> context (Either (Error error) output))
-> RuleF input error context output
-- | Given a non-empty list of potential cases, construct a Rule that
-- processes any items matching at least one of those cases.
TestMatch
::(CanMatch input)
=> NonEmpty (Pattern input error context output)
-> RuleF input error context output
Traverse
::Traversable t
=> (input -> t i)
-> (t o -> output)
-> RuleT i error context o
-> RuleF input error context output
-- | When @input@ is a value wrapping some 'Content', enable access to that
-- 'Content' by wrapping a 'RuleT'.
GetContent
::HasContent input
=> RuleT (Content input) error context output
-> RuleF input error context output
-- | Fetch a property from items with metadata.
GetProperty
::HasMetadata input
=> (Bool -> a)
-> Key
-> RuleF input error context a
-- | Fetch an /optional/ setting from items with metadata.
GetSetting
::HasMetadata input
=> (Maybe x -> output)
-> Key
-> (Text -> Either String x)
-> RuleF input error context output
-- | Fetch a /required/ setting from items with metadata.
GetRequiredSetting
::HasMetadata input
=> Key
-> (Text -> Either String output)
-> RuleF input error context output
-- | Get the raw text from a 'Text' node.
GetSelf
::(input -> output)
-> RuleF input error context output
instance Functor context => Functor (RuleF input error context) where
fmap fn = \case
Fail e -> Fail e
Lift lift -> Lift $ fmap (fmap fn) . lift
TestMatch matches -> TestMatch $ fmap (fmap fn) matches
Traverse f g rule -> Traverse f (fn . g) rule
GetContent rule -> GetContent $ fmap fn rule
GetProperty k key -> GetProperty (fn . k) key
GetSetting k key parse -> GetSetting (fn . k) key parse
GetRequiredSetting key parse ->
GetRequiredSetting key (fmap fn . parse)
GetSelf k -> GetSelf (fn . k)
-------------------------------------------------------------------------------
-- | A (lawless) typeclass for enabling fallible matching on nodes.
--
-- Implementing new instances of this class in library code is *unneccessary*
-- and *unsupported*.
class (forall i e. Functor (Pattern t i e), HasLocation t) => CanMatch t where
-- | A data type representing allowable fallible patterns for @t@.
data family Pattern t :: * -> (* -> *) -> * -> *
-- | Information about why a @Pattern@ failed to match.
data family NoMatch t :: *
-- | Attempt to match a pattern against a value.
evalPattern ::
Applicative g
=> Pattern t error context output
-- ^ The @Pattern@ to match against
-> Interpret error context g
-- ^ An interpreter for evaluating the match.
-> t
-- ^ The value to attempt to match against
-> Either (NoMatch t) (g output)
-- | Lift a @NoMatch@ error into the 'Error' type.
noMatchError :: NoMatch t -> Error e
instance CanMatch Prosidy.Block where
data Pattern Prosidy.Block error context output =
BlockTagP Key (RuleT BlockRegion error context output)
| LitTagP Key (RuleT LiteralRegion error context output)
| ParagraphP (RuleT (Prosidy.SeriesNE Prosidy.Inline) error context output)
deriving Functor
data NoMatch Prosidy.Block =
NoMatchBlockTag Key
| NoMatchLitTag Key
| NoMatchParagraph
evalPattern (BlockTagP key rule) = evalPatternWith
(Prosidy._BlockTag . Prosidy.tagged key)
(NoMatchBlockTag key)
rule
evalPattern (LitTagP key rule) = evalPatternWith
(Prosidy._BlockLiteral . Prosidy.tagged key)
(NoMatchLitTag key)
rule
evalPattern (ParagraphP rule) = evalPatternWith
(Prosidy._BlockParagraph . Prosidy.content)
NoMatchParagraph
rule
noMatchError (NoMatchBlockTag key) = ExpectedTag BlockKind key
noMatchError (NoMatchLitTag key) = ExpectedTag LiteralKind key
noMatchError NoMatchParagraph = ExpectedParagraph
instance CanMatch Prosidy.Inline where
data Pattern Prosidy.Inline error context output =
InlineTagP Key (RuleT InlineRegion error context output)
| BreakP (RuleT () error context output)
| TextP (RuleT Text error context output)
deriving Functor
data NoMatch Prosidy.Inline =
NoMatchInlineTag Key
| NoMatchBreak
| NoMatchText
evalPattern (InlineTagP key rule) = evalPatternWith
(Prosidy._InlineTag . Prosidy.tagged key)
(NoMatchInlineTag key)
rule
evalPattern (TextP rule) =
evalPatternWith (Prosidy._Text . Prosidy.fragment) NoMatchText rule
evalPattern (BreakP rule) =
evalPatternWith Prosidy._Break NoMatchBreak rule
noMatchError (NoMatchInlineTag key) = ExpectedTag InlineKind key
noMatchError NoMatchText = ExpectedText
noMatchError NoMatchBreak = ExpectedBreak
-- | Match one or more patterns, in sequence, against a value. The result from
-- the first successful pattern will be returned. Subsequent matches will not
-- be tried.
evalPatterns
:: (CanMatch i, IsError e, ApErrors e g)
=> NonEmpty (Pattern i e f o)
-> Interpret e f g
-> i
-> g o
evalPatterns (x :| xs) interpret input = either liftError id folded
where
folded = foldr (\pat acc -> doEval pat `orElse` acc) (doEval x) xs
doEval pat =
first (singleError . noMatchError) $ evalPattern pat interpret input
orElse lhs@Right{} _ = lhs
orElse (Left lhs) (Left rhs) = Left $ lhs <> rhs
orElse _ rhs = rhs
evalPatternWith
:: Applicative g
=> Traversal' i j
-> e
-> RuleT j e' f o
-> Interpret e' f g
-> i
-> Either e (g o)
evalPatternWith sel error rule interpret input =
second (interpretWith rule interpret)
. maybe (Left error) Right
$ input
^? sel
-------------------------------------------------------------------------------
-- | Build an interpreter into a functor @g@.
interpretWith :: Applicative g => RuleT i e f a -> Interpret e f g -> i -> g a
interpretWith (RuleT ap) int i = Ap.runAp (int i) ap
-------------------------------------------------------------------------------
-- | Runs a single 'RuleF' into an applicative @g@. Passing this value to
-- 'interpretWith' will fully evaluate a 'RuleT' into the same functor.
type Interpret e f g = forall i a . i -> RuleF i e f a -> g a
-------------------------------------------------------------------------------
-- | A 'Prosidy.Types.BlockTag' with the tag name removed.
type BlockRegion = Prosidy.Region (Prosidy.Series Prosidy.Block)
-- | An 'Prosidy.Types.InlineTag' with the tag name removed.
type InlineRegion = Prosidy.Region (Prosidy.Series Prosidy.Inline)
-- | A 'Prosidy.Types.LiteralTag' with the tag name removed.
type LiteralRegion = Prosidy.Region Text