cake3-0.2.1.0: src/Development/Cake3/Monad.hs
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE OverlappingInstances #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE DeriveDataTypeable #-}
module Development.Cake3.Monad where
import Control.Applicative
import Control.Monad
import Control.Monad.State
import Control.Monad.Reader
import Control.Monad.Trans
import Control.Monad.Loc
import Data.Data
import Data.Typeable
import Data.Monoid
import Data.Maybe
import qualified Data.Map as M
import Data.Map(Map)
import qualified Data.Set as S
import Data.Set(Set)
import qualified Data.String as STR
import Data.List as L hiding (foldl')
import Data.Either
import Data.Foldable (Foldable(..), foldl')
import qualified Data.ByteString.Char8 as BS
import qualified Data.Foldable as F
import qualified Data.Traversable as F
import qualified Data.Text as T
import Development.Cake3.Types
import qualified System.IO as IO
import Text.Printf
import Text.QuasiMake
import Language.Haskell.TH.Quote
import Language.Haskell.TH hiding(unsafe)
import Language.Haskell.Meta (parseExp)
import System.FilePath.Wrapper
type Location = String
-- | MakeState describes the state of the EDSL synthesizers during the
-- the program execution.
data MakeState = MS {
prebuilds :: Recipe
-- ^ Prebuild commands. targets/prerequsites of the recipe are ignored,
-- commands are executed before any target
, postbuilds :: Recipe
-- ^ Postbuild commands.
, recipes :: Set Recipe
-- ^ The set of recipes
, sloc :: Location
-- ^ Current location. FIXME: fix or remove
, makeDeps :: Set File
-- ^ Set of files which the Makefile depends on
, placement :: [File]
-- ^ Placement list is the order of targets to be placed in the output file
, includes :: Set File
-- ^ Set of files to include in the output file (Makefile specific thing)
, errors :: String
-- ^ Errors found so far
, warnings :: String
-- ^ Warnings found so far
, outputFile :: File
}
-- Oh, such a boilerplate
initialMakeState mf = MS defr defr mempty mempty mempty mempty mempty mempty mempty mf where
defr = emptyRecipe "<internal>"
getPlacementPos :: Make Int
getPlacementPos = L.length <$> placement <$> get
addPlacement :: Int -> File -> Make ()
addPlacement pos r = modify $ \ms -> ms { placement = r`insertInto`(placement ms) } where
insertInto x xs = let (h,t) = splitAt pos xs in h ++ (x:t)
addMakeDep :: File -> Make ()
addMakeDep f = modify (\ms -> ms { makeDeps = S.insert f (makeDeps ms) })
-- | Add prebuild command
prebuild, postbuild :: (MonadMake m) => CommandGen -> m ()
prebuild cmdg = liftMake $ do
s <- get
pb <- fst <$> runA' (prebuilds s) (shell cmdg)
put s { prebuilds = pb }
postbuild cmdg = liftMake $ do
s <- get
pb <- fst <$> runA' (postbuilds s) (shell cmdg)
put s { postbuilds = pb }
-- | Find recipes without targets
checkForEmptyTarget :: (Foldable f) => f Recipe -> String
checkForEmptyTarget rs = foldl' checker mempty rs where
checker es r | S.null (rtgt r) = es++e
| otherwise = es where
e = printf "Error: Recipe without targets\n\t%s\n" (show r)
-- | Find recipes sharing a target
checkForTargetConflicts :: (Foldable f) => f Recipe -> String
checkForTargetConflicts rs = foldl' checker mempty (groupRecipes rs) where
checker es rs | S.size rs > 1 = es++e
| otherwise = es where
e = printf "Error: Recipes share one or more targets\n\t%s\n" (show rs)
-- | A Monad providing access to MakeState. TODO: not mention IO here.
class (Monad m) => MonadMake m where
liftMake :: (Make' IO) a -> m a
newtype Make' m a = Make { unMake :: (StateT MakeState m) a }
deriving(Monad, Functor, Applicative, MonadState MakeState, MonadIO, MonadFix)
type Make a = Make' IO a
instance MonadMake (Make' IO) where
liftMake = id
instance (MonadMake m) => MonadMake (A' m) where
liftMake m = A' (lift (liftMake m))
instance (MonadMake m) => MonadMake (StateT s m) where
liftMake = lift . liftMake
-- | Returns a MakeState
evalMake :: (Monad m) => File -> Make' m a -> m MakeState
evalMake mf mk = do
ms <- flip execStateT (initialMakeState mf) (unMake mk)
return ms {
errors = checkForEmptyTarget (recipes ms) ++ checkForTargetConflicts (recipes ms)
}
modifyLoc f = modify $ \ms -> ms { sloc = f (sloc ms) }
addRecipe :: Recipe -> Make ()
addRecipe r = modify $ \ms ->
let rs = recipes ms ; k = rtgt r
in ms { recipes = (S.insert r (recipes ms)) }
getLoc :: Make String
getLoc = sloc <$> get
-- | Add 'include ...' directive to the final Makefile for each input file.
includeMakefile :: (Foldable t) => t File -> Make ()
includeMakefile fs = foldl' scan (return ()) fs where
scan a f = do
modify $ \ms -> ms {includes = S.insert f (includes ms)}
return ()
instance (Monad m) => MonadLoc (Make' m) where
withLoc l' (Make um) = Make $ do
modifyLoc (\l -> l') >> um
-- | 'A' here stands for Action. It is a State monad carrying a Recipe as its
-- state. Various monadic actions add targets, prerequisites and shell commands
-- to this recipe. After that, @rule@ function records it to the @MakeState@.
-- After the recording, no modification is allowed for this recipe.
newtype A' m a = A' { unA' :: StateT Recipe m a }
deriving(Monad, Functor, Applicative, MonadState Recipe, MonadIO,MonadFix)
-- | Verison of Action monad with fixed parents
type A a = A' (Make' IO) a
-- | A class of monads providing access to the underlying A monad
class (Monad m, Monad t) => MonadAction t m | t -> m where
liftAction :: A' m x -> t x
instance (Monad m) => MonadAction (A' m) m where
liftAction = id
-- | Run the Action monad, using already existing Recipe as input.
runA' :: (Monad m) => Recipe -> A' m a -> m (Recipe, a)
runA' r act = do
(a,r) <- runStateT (unA' act) r
return (r,a)
-- | Create new empty recipe and run action on it.
runA :: (Monad m)
=> String -- ^ Location string (in the Cakefile.hs)
-> A' m a -- ^ Recipe builder
-> m (Recipe, a)
runA loc act = runA' (emptyRecipe loc) act
-- | Version of runA discarding the result of A's computation
runA_ :: (Monad m) => String -> A' m a -> m Recipe
runA_ loc act = runA loc act >>= return .fst
-- | Get a list of targets added so far
targets :: (Applicative m, Monad m) => A' m (Set File)
targets = rtgt <$> get
-- | Get a list of prerequisites added so far
prerequisites :: (Applicative m, Monad m) => A' m (Set File)
prerequisites = rsrc <$> get
-- | Mark the recipe as 'PHONY' i.e. claim that all it's targets are not real
-- files. Makefile-specific.
markPhony :: (Monad m) => A' m ()
markPhony = modify $ \r -> r { rflags = S.insert Phony (rflags r) }
-- | Mark the recipe as 'INTERMEDIATE' i.e. claim that all it's targets may be
-- removed after the build process. Makefile-specific.
markIntermediate :: (Monad m) => A' m ()
markIntermediate = modify $ \r -> r { rflags = S.insert Intermediate (rflags r) }
-- | Obtain the contents of a File. Note, that this generally means, that
-- Makefile should be regenerated each time the File is changed.
readFileForMake :: (MonadMake m)
=> File -- ^ File to read contents of
-> m BS.ByteString
readFileForMake f = liftMake (addMakeDep f >> liftIO (BS.readFile (toFilePath f)))
-- | CommandGen is a recipe-builder packed in the newtype to prevent partial
-- expantion of it's commands
newtype CommandGen' m = CommandGen' { unCommand :: A' m Command }
type CommandGen = CommandGen' (Make' IO)
-- | Pack the command builder into a CommandGen
commandGen :: A Command -> CommandGen
commandGen mcmd = CommandGen' mcmd
-- | Modifie the recipe builder: ignore all the dependencies
ignoreDepends :: (Monad m) => A' m a -> A' m a
ignoreDepends action = do
r <- get
a <- action
modify $ \r' -> r' { rsrc = rsrc r, rvars = rvars r }
return a
-- | Apply the recipe builder to the current recipe state. Return the list of
-- targets of the current @Recipe@ under construction
shell :: (Monad m)
=> CommandGen' m -- ^ Command builder as returned by cmd quasi-quoter
-> A' m [File]
shell cmdg = do
line <- unCommand cmdg
commands [line]
r <- get
return (S.toList (rtgt r))
-- | Version of @shell@ which doesn't track it's dependencies
unsafeShell :: (Monad m) => CommandGen' m -> A' m [File]
unsafeShell cmdg = ignoreDepends (shell cmdg)
-- | Simple wrapper for strings, a target for various typeclass instances.
newtype CakeString = CakeString String
deriving(Show,Eq,Ord)
-- | An alias to CakeString constructor
string :: String -> CakeString
string = CakeString
-- | Class of things which may be referenced using '\@(expr)' syntax of the
-- quasi-quoted shell expressions.
class (Monad m) => RefOutput m x where
-- | Register the output item, return it's shell-command representation. Files
-- are rendered using space protection quotation, variables are wrapped into
-- $(VAR) syntax, item lists are converted into space-separated lists.
refOutput :: x -> A' m Command
instance (Monad m) => RefOutput m File where
refOutput f = do
modify $ \r -> r { rtgt = f `S.insert` (rtgt r)}
return_file f
-- FIXME: inbetween will not notice if spaces are already exists
inbetween x mx = (concat`liftM`mx) >>= \l -> return (inbetween' x l) where
inbetween' x [] = []
inbetween' x [a] = [a]
inbetween' x (a:as) = a:x:(inbetween' x as)
spacify l = (CmdStr " ") `inbetween` l
instance (Monad m) => RefOutput m [File] where
refOutput xs = spacify $ mapM refOutput (xs)
instance (Monad m) => RefOutput m (Set File) where
refOutput xs = refOutput (S.toList xs)
instance (RefOutput m x) => RefOutput m (Maybe x) where
refOutput mx = case mx of
Nothing -> return mempty
Just x -> refOutput x
-- | Class of things which may be referenced using '\$(expr)' from inside
-- of quasy-quoted shell expressions
class (MonadAction a m) => RefInput a m x where
-- | Register the input item, return it's shell-script representation
refInput :: x -> a Command
instance (MonadAction a m) => RefInput a m File where
refInput f = liftAction $ do
modify $ \r -> r { rsrc = f `S.insert` (rsrc r)}
return_file f
instance (MonadAction a m) => RefInput a m Recipe where
refInput r = refInput (rtgt r)
instance (RefInput a m x) => RefInput a m [x] where
refInput xs = spacify $ mapM refInput xs
instance (MonadAction a m) => RefInput a m (Set File) where
refInput xs = refInput (S.toList xs)
instance (MonadIO a, RefInput a m x) => RefInput a m (IO x) where
refInput mx = liftIO mx >>= refInput
instance (MonadAction a m, MonadMake a) => RefInput a m (Make Recipe) where
refInput mr = liftMake mr >>= refInput
instance (RefInput a m x, MonadMake a) => RefInput a m (Make x) where
refInput mx = liftMake mx >>= refInput
instance (RefInput a m x) => RefInput a m (Maybe x) where
refInput mx = case mx of
Nothing -> return mempty
Just x -> refInput x
instance (MonadAction a m) => RefInput a m Variable where
refInput v@(Variable n _) = liftAction $ do
variables [v]
return_text $ printf "$(%s)" n
instance (MonadAction a m) => RefInput a m CakeString where
refInput v@(CakeString s) = do
return_text s
-- | Add it's argument to the list of dependencies (prerequsites) of a current
-- recipe under construction
depend :: (RefInput a m x)
=> x -- ^ File or [File] or (Set File) or other form of dependency.
-> a ()
depend x = refInput x >> return ()
-- | Declare that current recipe produces some producable item.
produce :: (RefOutput m x)
=> x -- ^ File or [File] or other form of target.
-> A' m ()
produce x = refOutput x >> return ()
-- | Add variables to the list of variables referenced by the current recipe
variables :: (Foldable t, Monad m)
=> (t Variable) -- ^ A set of variables to depend the recipe on
-> A' m ()
variables vs = modify (\r -> r { rvars = foldl' (\a v -> S.insert v a) (rvars r) vs } )
-- | Add commands to the list of commands of a current recipe under
-- construction. Warning: this function behaves like unsafeShell i.e. it doesn't
-- analyze the command text
commands :: (Monad m) => [Command] -> A' m ()
commands cmds = modify (\r -> r { rcmd = (rcmd r) ++ cmds } )
-- | Set the recipe's location in the Cakefile.hs
location :: (Monad m) => String -> A' m ()
location l = modify (\r -> r { rloc = l } )
-- | Set additional flags
flags :: (Monad m) => Set Flag -> A' m ()
flags f = modify (\r -> r { rflags = (rflags r) `mappend` f } )
-- | Has effect of a function @QQ -> CommandGen@ where QQ is a string supporting
-- the following syntax:
--
-- * $(expr) evaluates to expr and adds it to the list of dependencies (prerequsites)
--
-- * \@(expr) evaluates to expr and adds it to the list of targets
--
-- * $$ and \@\@ evaluates to $ and \@
--
-- /Example/
--
-- > [cmd|gcc $flags -o @file|]
--
-- is equivalent to
--
-- > return $ CommandGen $ do
-- > s1 <- refInput "gcc "
-- > s2 <- refInput (flags :: Variable)
-- > s3 <- refInput " -o "
-- > s4 <- refOutput (file :: File)
-- > return (s1 ++ s2 ++ s3 ++ s4)
--
-- Later, this command may be examined or passed to the shell function to apply
-- it to the recipe
--
cmd :: QuasiQuoter
cmd = QuasiQuoter
{ quotePat = undefined
, quoteType = undefined
, quoteDec = undefined
, quoteExp = \s -> appE [| \x -> CommandGen' x |] (qqact s)
} where
qqact s =
let chunks = flip map (getChunks (STR.fromString s)) $ \c ->
case c of
T t -> let t' = T.unpack t in [| return_text t' |]
E c t -> case parseExp (T.unpack t) of
Left e -> error e
Right e -> case c of
'$' -> appE [| refInput |] (return e)
'@' -> appE [| refOutput |] (return e)
in appE [| \l -> L.concat <$> (sequence l) |] (listE chunks)