patat-0.14.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.Set as S
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Patat.Eval.Internal
import Patat.Presentation.Internal
import Patat.Presentation.Syntax
import Patat.Unique
import System.Exit (ExitCode (..))
import qualified System.IO as IO
import qualified System.Process as Process
--------------------------------------------------------------------------------
parseEvalBlocks :: Presentation -> Presentation
parseEvalBlocks presentation =
let ((pres, varGen), evalBlocks) = runWriter $
runStateT work (pUniqueGen presentation) in
pres {pEvalBlocks = evalBlocks, pUniqueGen = 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 UniqueGen (Writer (HMS.HashMap Var EvalBlock)) a
--------------------------------------------------------------------------------
evalSlide :: EvalSettingsMap -> Slide -> ExtractEvalM Slide
evalSlide settings slide = case slideContent slide of
TitleSlide _ _ -> pure slide
ContentSlide blocks -> do
blocks1 <- dftBlocks (evalBlock settings) (pure . pure) blocks
pure slide {slideContent = ContentSlide blocks1}
--------------------------------------------------------------------------------
evalBlock
:: EvalSettingsMap -> Block
-> ExtractEvalM [Block]
evalBlock settings orig@(CodeBlock attr@(_, classes, _) txt)
| [s@EvalSettings {..}] <- lookupSettings classes settings = do
var <- Var <$> state freshUnique
tell $ HMS.singleton var $ EvalBlock s attr txt Nothing
case (evalReveal, evalReplace) of
(False, True) -> pure [VarBlock var]
(False, False) -> pure [orig, VarBlock var]
(True, True) -> do
revealID <- RevealID <$> state freshUnique
pure $ pure $ Reveal ConcatWrapper $ RevealSequence
revealID
[revealID]
[ (S.singleton 0, [orig])
, (S.singleton 1, [VarBlock var])
]
(True, False) -> do
revealID <- RevealID <$> state freshUnique
pure $ pure $ Reveal ConcatWrapper $ RevealSequence
revealID
[revealID]
[ (S.fromList [0, 1], [orig])
, (S.fromList [1], [VarBlock var])
]
| _ : _ : _ <- lookupSettings classes settings =
let msg = "patat eval matched multiple settings for " <>
T.intercalate "," classes in
pure [CodeBlock attr msg]
evalBlock _ block =
pure [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 -> ([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 -> [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))