imperative-edsl-0.6: src/Language/Embedded/Backend/C.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE QuasiQuotes #-}
-- | C code generation for 'Program'
module Language.Embedded.Backend.C
( module Language.Embedded.Backend.C.Expression
, module Language.Embedded.Backend.C
, Default (..)
) where
#if __GLASGOW_HASKELL__ < 710
import Control.Applicative
import Data.Monoid
#endif
import Control.Exception
import Data.Time (getCurrentTime, formatTime, defaultTimeLocale)
import System.Directory (getTemporaryDirectory, removeFile)
import System.Exit (ExitCode (..))
import System.IO
import System.Process (system, readProcess)
import Data.Default.Class
import Data.Loc (noLoc)
import qualified Language.C.Syntax as C
import Text.PrettyPrint.Mainland (pretty)
import Control.Monad.Operational.Higher
import System.IO.Fake
import Language.C.Monad
import Language.Embedded.Backend.C.Expression
--------------------------------------------------------------------------------
-- * Utilities
--------------------------------------------------------------------------------
-- | Create a named type
namedType :: String -> C.Type
namedType t = C.Type
(C.DeclSpec [] [] (C.Tnamed (C.Id t noLoc) [] noLoc) noLoc)
(C.DeclRoot noLoc)
noLoc
-- | Return the argument of a boolean negation expression
viewNotExp :: C.Exp -> Maybe C.Exp
viewNotExp (C.UnOp C.Lnot a _) = Just a
viewNotExp (C.FnCall (C.Var (C.Id "!" _) _) [a] _) = Just a
-- Apparently this is what `!` parses to
viewNotExp _ = Nothing
arrayInit :: [C.Exp] -> C.Initializer
arrayInit as = C.CompoundInitializer
[(Nothing, C.ExpInitializer a noLoc) | a <- as]
noLoc
--------------------------------------------------------------------------------
-- * Code generation user interface
--------------------------------------------------------------------------------
-- | Compile a program to C code represented as a string. To compile the
-- resulting C code, use something like
--
-- > cc -std=c99 YOURPROGRAM.c
--
-- This function returns only the first (main) module. To get all C translation
-- unit, use 'compileAll'.
compile :: (Interp instr CGen (Param2 exp pred), HFunctor instr) =>
Program instr (Param2 exp pred) a -> String
compile = snd . head . compileAll
-- | Compile a program to C modules, each one represented as a pair of a name
-- and the code represented as a string. To compile the resulting C code, use
-- something like
--
-- > cc -std=c99 YOURPROGRAM.c
compileAll :: (Interp instr CGen (Param2 exp pred), HFunctor instr) =>
Program instr (Param2 exp pred) a -> [(String, String)]
compileAll
= map (("", pretty 80) <*>) . prettyCGen . liftSharedLocals
. wrapMain . interpret
-- | Compile a program to C code and print it on the screen. To compile the
-- resulting C code, use something like
--
-- > cc -std=c99 YOURPROGRAM.c
icompile :: (Interp instr CGen (Param2 exp pred), HFunctor instr) =>
Program instr (Param2 exp pred) a -> IO ()
icompile prog = case compileAll prog of
[m] -> putStrLn $ snd m
ms -> mapM_ (\(n, m) -> putStrLn ("// module " ++ n) >> putStrLn m) ms
removeFileIfPossible :: FilePath -> IO ()
removeFileIfPossible file =
catch (removeFile file) (\(_ :: SomeException) -> return ())
data ExternalCompilerOpts = ExternalCompilerOpts
{ externalKeepFiles :: Bool -- ^ Keep generated files?
, externalFlagsPre :: [String] -- ^ External compiler flags (e.g. @["-Ipath"]@)
, externalFlagsPost :: [String] -- ^ External compiler flags after C source (e.g. @["-lm","-lpthread"]@)
, externalSilent :: Bool -- ^ Don't print anything besides what the program prints
}
instance Default ExternalCompilerOpts
where
def = ExternalCompilerOpts
{ externalKeepFiles = False
, externalFlagsPre = []
, externalFlagsPost = []
, externalSilent = False
}
maybePutStrLn :: Bool -> String -> IO ()
maybePutStrLn False str = putStrLn str
maybePutStrLn _ _ = return ()
-- | Generate C code and use CC to compile it
compileC :: (Interp instr CGen (Param2 exp pred), HFunctor instr)
=> ExternalCompilerOpts
-> Program instr (Param2 exp pred) a -- ^ Program to compile
-> IO FilePath -- ^ Path to the generated executable
compileC (ExternalCompilerOpts {..}) prog = do
tmp <- getTemporaryDirectory
t <- fmap (formatTime defaultTimeLocale format) getCurrentTime
(exeFile,exeh) <- openTempFile tmp ("edsl_" ++ t)
hClose exeh
let cFile = exeFile ++ ".c"
writeFile cFile $ compile prog
when externalKeepFiles $ maybePutStrLn externalSilent $
"Created temporary file: " ++ cFile
let compileCMD = unwords
$ ["cc", "-std=c99"]
++ externalFlagsPre
++ [cFile, "-o", exeFile]
++ externalFlagsPost
maybePutStrLn externalSilent compileCMD
exit <- system compileCMD
unless externalKeepFiles $ removeFileIfPossible cFile
case exit of
ExitSuccess -> return exeFile
err -> do removeFileIfPossible exeFile
error "compileC: failed to compile generated C code"
where
format = if externalKeepFiles then "%a-%H-%M-%S_" else ""
-- | Generate C code and use CC to check that it compiles (no linking)
compileAndCheck' :: (Interp instr CGen (Param2 exp pred), HFunctor instr) =>
ExternalCompilerOpts -> Program instr (Param2 exp pred) a -> IO ()
compileAndCheck' opts prog = do
let opts' = opts {externalFlagsPre = "-c" : externalFlagsPre opts}
exe <- compileC opts' prog
removeFileIfPossible exe
-- | Generate C code and use CC to check that it compiles (no linking)
compileAndCheck :: (Interp instr CGen (Param2 exp pred), HFunctor instr) =>
Program instr (Param2 exp pred) a -> IO ()
compileAndCheck = compileAndCheck' def
-- | Generate C code, use CC to compile it, and run the resulting executable
runCompiled' :: (Interp instr CGen (Param2 exp pred), HFunctor instr) =>
ExternalCompilerOpts -> Program instr (Param2 exp pred) a -> IO ()
runCompiled' opts@(ExternalCompilerOpts {..}) prog = bracket
(compileC opts prog)
removeFileIfPossible
( \exe -> do
maybePutStrLn externalSilent ""
maybePutStrLn externalSilent "#### Running:"
system exe >> return ()
)
-- | Generate C code, use CC to compile it, and run the resulting executable
runCompiled :: (Interp instr CGen (Param2 exp pred), HFunctor instr) =>
Program instr (Param2 exp pred) a -> IO ()
runCompiled = runCompiled' def
-- | Compile a program and make it available as an 'IO' function from 'String'
-- to 'String' (connected to @stdin@/@stdout@. respectively). Note that
-- compilation only happens once, even if the 'IO' function is used many times
-- in the body.
withCompiled' :: (Interp instr CGen (Param2 exp pred), HFunctor instr)
=> ExternalCompilerOpts
-> Program instr (Param2 exp pred) a -- ^ Program to compile
-> ((String -> IO String) -> IO b)
-- ^ Function that has access to the compiled executable as a function
-> IO b
withCompiled' opts prog body = bracket
(compileC opts prog)
removeFileIfPossible
(\exe -> body $ readProcess exe [])
-- | Compile a program and make it available as an 'IO' function from 'String'
-- to 'String' (connected to @stdin@/@stdout@. respectively). Note that
-- compilation only happens once, even if the 'IO' function is used many times
-- in the body.
withCompiled :: (Interp instr CGen (Param2 exp pred), HFunctor instr)
=> Program instr (Param2 exp pred) a -- ^ Program to compile
-> ((String -> IO String) -> IO b)
-- ^ Function that has access to the compiled executable as a function
-> IO b
withCompiled = withCompiled' def {externalSilent = True}
-- | Like 'runCompiled'' but with explicit input/output connected to
-- @stdin@/@stdout@. Note that the program will be compiled every time the
-- function is applied to a string. In order to compile once and run many times,
-- use the function 'withCompiled''.
captureCompiled' :: (Interp instr CGen (Param2 exp pred), HFunctor instr)
=> ExternalCompilerOpts
-> Program instr (Param2 exp pred) a -- ^ Program to run
-> String -- ^ Input to send to @stdin@
-> IO String -- ^ Result from @stdout@
captureCompiled' opts prog inp = withCompiled' opts prog ($ inp)
-- | Like 'runCompiled' but with explicit input/output connected to
-- @stdin@/@stdout@. Note that the program will be compiled every time the
-- function is applied to a string. In order to compile once and run many times,
-- use the function 'withCompiled'.
captureCompiled :: (Interp instr CGen (Param2 exp pred), HFunctor instr)
=> Program instr (Param2 exp pred) a -- ^ Program to run
-> String -- ^ Input to send to @stdin@
-> IO String -- ^ Result from @stdout@
captureCompiled = captureCompiled' def
-- | Compare the content written to @stdout@ from the reference program and from
-- running the compiled C code
compareCompiled' :: (Interp instr CGen (Param2 exp pred), HFunctor instr)
=> ExternalCompilerOpts
-> Program instr (Param2 exp pred) a -- ^ Program to run
-> IO a -- ^ Reference program
-> String -- ^ Input to send to @stdin@
-> IO ()
compareCompiled' opts@(ExternalCompilerOpts {..}) prog ref inp = do
maybePutStrLn externalSilent "#### Reference program:"
outRef <- fakeIO ref inp
maybePutStrLn externalSilent outRef
maybePutStrLn externalSilent "#### runCompiled:"
outComp <- captureCompiled' opts prog inp
maybePutStrLn externalSilent outComp
if outRef /= outComp
then error "runCompiled differs from reference program"
else maybePutStrLn externalSilent
" -- runCompiled is consistent with reference program\n\n\n\n"
-- | Compare the content written to @stdout@ from the reference program and from
-- running the compiled C code
compareCompiled :: (Interp instr CGen (Param2 exp pred), HFunctor instr)
=> Program instr (Param2 exp pred) a -- ^ Program to run
-> IO a -- ^ Reference program
-> String -- ^ Input to send to @stdin@
-> IO ()
compareCompiled = compareCompiled' def