patat-0.13.0.0: lib/Patat/Eval.hs
--------------------------------------------------------------------------------
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Patat.Eval
( parseEvalBlocks
, evalVar
, evalActiveVars
, evalAllVars
) where
--------------------------------------------------------------------------------
import qualified Control.Concurrent.Async as Async
import Control.Exception (IOException, catch, finally)
import Control.Monad (foldM, when)
import Control.Monad.State (StateT, runStateT, state)
import Control.Monad.Writer (Writer, runWriter, tell)
import Data.Foldable (for_)
import qualified Data.HashMap.Strict as HMS
import qualified Data.IORef as IORef
import Data.List (foldl')
import Data.Maybe (maybeToList)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Patat.Eval.Internal
import Patat.Presentation.Instruction
import Patat.Presentation.Internal
import System.Exit (ExitCode (..))
import qualified System.IO as IO
import qualified System.Process as Process
import qualified Text.Pandoc.Definition as Pandoc
--------------------------------------------------------------------------------
parseEvalBlocks :: Presentation -> Presentation
parseEvalBlocks presentation =
let ((pres, varGen), evalBlocks) = runWriter $
runStateT work (pVarGen presentation) in
pres {pEvalBlocks = evalBlocks, pVarGen = varGen}
where
work = case psEval (pSettings presentation) of
Nothing -> pure presentation
Just settings -> do
slides <- traverse (evalSlide settings) (pSlides presentation)
pure presentation {pSlides = slides}
--------------------------------------------------------------------------------
lookupSettings :: [T.Text] -> EvalSettingsMap -> [EvalSettings]
lookupSettings classes settings = do
c <- classes
maybeToList $ HMS.lookup c settings
--------------------------------------------------------------------------------
-- | Monad used for identifying and extracting the evaluation blocks from a
-- presentation.
type ExtractEvalM a = StateT VarGen (Writer (HMS.HashMap Var EvalBlock)) a
--------------------------------------------------------------------------------
evalSlide :: EvalSettingsMap -> Slide -> ExtractEvalM Slide
evalSlide settings slide = case slideContent slide of
TitleSlide _ _ -> pure slide
ContentSlide instrs0 -> do
instrs1 <- traverse (evalInstruction settings) (toList instrs0)
pure slide {slideContent = ContentSlide . fromList $ concat instrs1}
--------------------------------------------------------------------------------
evalInstruction
:: EvalSettingsMap -> Instruction Pandoc.Block
-> ExtractEvalM [Instruction Pandoc.Block]
evalInstruction settings instr = case instr of
Pause -> pure [Pause]
ModifyLast i -> map ModifyLast <$> evalInstruction settings i
Append [] -> pure [Append []]
Append blocks -> concat <$> traverse (evalBlock settings) blocks
AppendVar v ->
-- Should not happen since we don't do recursive evaluation.
pure [AppendVar v]
Delete -> pure [Delete]
--------------------------------------------------------------------------------
evalBlock
:: EvalSettingsMap -> Pandoc.Block
-> ExtractEvalM [Instruction Pandoc.Block]
evalBlock settings orig@(Pandoc.CodeBlock attr@(_, classes, _) txt)
| [s@EvalSettings {..}] <- lookupSettings classes settings = do
var <- state freshVar
tell $ HMS.singleton var $ EvalBlock s attr txt Nothing
pure $ case (evalFragment, evalReplace) of
(False, True) -> [AppendVar var]
(False, False) -> [Append [orig], AppendVar var]
(True, True) ->
[ Append [orig], Pause
, Delete, AppendVar var
]
(True, False) ->
[Append [orig], Pause, AppendVar var]
| _ : _ : _ <- lookupSettings classes settings =
let msg = "patat eval matched multiple settings for " <>
T.intercalate "," classes in
pure [Append [Pandoc.CodeBlock attr msg]]
evalBlock _ block =
pure [Append [block]]
--------------------------------------------------------------------------------
newAccum :: Monoid m => (m -> IO ()) -> IO (m -> IO ())
newAccum f = do
ref <- IORef.newIORef mempty
pure $ \x ->
IORef.atomicModifyIORef' ref (\y -> let z = y <> x in (z, z)) >>= f
--------------------------------------------------------------------------------
evalVar :: Var -> ([Pandoc.Block] -> IO ()) -> Presentation -> IO Presentation
evalVar var writeOutput presentation = case HMS.lookup var evalBlocks of
Nothing -> pure presentation
Just EvalBlock {..} | Just _ <- ebAsync -> pure presentation
Just eb@EvalBlock {..} -> do
let EvalSettings {..} = ebSettings
writeChunk <- newAccum (writeOutput . renderEvalBlock eb)
let drainLines copy h = do
c <- catch (T.hGetChunk h) ((\_ -> pure "") :: IOException -> IO T.Text)
when (c /= "") $ do
when copy $ writeChunk c
drainLines copy h
let proc = (Process.shell $ T.unpack evalCommand)
{ Process.std_in = Process.CreatePipe
, Process.std_out = Process.CreatePipe
, Process.std_err = Process.CreatePipe
}
(Just hIn, Just hOut, Just hErr, hProc) <- Process.createProcess proc
async <- Async.async $
Async.withAsync (T.hPutStr hIn ebInput `finally` IO.hClose hIn) $ \_ ->
Async.withAsync (drainLines True hOut) $ \outAsync ->
Async.withAsync (drainLines evalStderr hErr) $ \errAsync ->
Async.withAsync (Process.waitForProcess hProc) $ \exitCodeAsync -> do
erExitCode <- Async.wait exitCodeAsync
_ <- Async.wait outAsync
_ <- Async.wait errAsync
case erExitCode of
ExitSuccess -> pure ()
ExitFailure i -> writeChunk $
evalCommand <> ": exit code " <> T.pack (show i) <> "\n"
pure presentation
{ pEvalBlocks = HMS.insert var eb {ebAsync = Just async} evalBlocks
}
where
evalBlocks = pEvalBlocks presentation
--------------------------------------------------------------------------------
evalActiveVars
:: (Var -> [Pandoc.Block] -> IO ()) -> Presentation -> IO Presentation
evalActiveVars update presentation = foldM
(\p var -> evalVar var (update var) p)
presentation
(activeVars presentation)
--------------------------------------------------------------------------------
evalAllVars :: Presentation -> IO Presentation
evalAllVars pres = do
updates <- IORef.newIORef []
let forceEvalVar pres0 var = do
pres1 <- evalVar
var
(\u -> IORef.atomicModifyIORef' updates (\l -> (l ++ [u], ())))
pres0
case HMS.lookup var (pEvalBlocks pres1) of
Nothing -> pure pres1
Just eb -> do
for_ (ebAsync eb) Async.wait
IORef.atomicModifyIORef' updates $ \l ->
([], foldl' (\p u -> updateVar var u p) pres1 l)
foldM forceEvalVar pres (HMS.keys (pEvalBlocks pres))