packages feed

cgen-0.0.1: src/HaskellGen.hs

{-# LANGUAGE TemplateHaskell #-}
module HaskellGen
where

import Data.List
import Data.Maybe
import Text.Printf
import System.IO
import System.FilePath
import Control.Monad
import Control.Applicative
import qualified Data.Set as S
import qualified Data.Map as M

import Text.Regex.Posix
import Safe

import HeaderData
import CppUtils
import Utils
import DeriveMod

data Options = Options
  {
    outputdir         :: FilePath
  , interfacefile     :: String
  , inheritfile       :: FilePath
  , umbrellamodule    :: FilePath
  , excludepatterns   :: [String] 
  , defaultins        :: [String] 
  , defaultouts       :: [String] 
  , inparameters      :: [String] 
  , outparameters     :: [String] 
  , hierarchy         :: String
  }
  deriving (Show)
$(deriveMods ''Options)

defaultOptions :: Options
defaultOptions = Options "" "" "" "" [] [] [] [] [] ""

-- haskell c type descriptor, e.g. "Ptr CChar"
data HsCType = HsCType {
    hsname  :: String  -- ^ haskell c type, like "CChar"
  , numptrs :: Int     -- ^ number of pointers
  }
  deriving (Show)

-- descriptor on how to convert a haskell type to a c type
data CConv = WithLambda String | CConvFunc String | NoCConv
  deriving (Show)

-- descriptor on how to convert a c type to a haskell type
data HsConv = HsConv String | NoHsConv
  deriving (Show)

data HsFun = HsFun {
    cfilename  :: String            -- ^ c header file
  , cfunname   :: String            -- ^ c function name
  , cparams    :: [HsCType]         -- ^ haskell c type params
  , cretparam  :: HsCType           -- ^ haskell c type for the return type
  , hsfunname  :: String            -- ^ haskell function name
  , hsparams   :: [(CConv, String)]
     -- ^ (how to convert the c type to a haskell type,
     --   which haskell type to convert to)
  , hsrettypes :: [((CConv, String), HsConv)]
     -- ^ ((how to convert the c return types to a haskell type,
     --   which haskell type to convert to),
     -- ^ how to convert the c return types to a haskell type)
  }
  deriving (Show)

haskellGen :: Options -> [(FilePath, [Object])] -> IO ()
haskellGen opts objs = do
    let outdir   = outputdir opts
        excls    = excludepatterns opts
        funs     = map (apSnd (filter (\f -> not $ or (map (\e -> funname f =~ e) excls)))) $ map (apSnd getFuns) objs
        enums    = concatMap getEnums $ map snd objs
        enumnames = map (capitalize . enumname) enums
        alltypes = getAllTypesWithPtr (concatMap snd funs)
        typeValid :: String -> Bool
        typeValid t = not . isJust $ typeValidMsg enumnames opts t
        (cpptypes, rejtypes) = S.partition typeValid alltypes
        modprefix = hierarchy opts
    hPutStrLn stderr $ "Rejected types: "
    forM_ (S.toList rejtypes) print
    hPutStrLn stderr $ "Used types: "
    let hstypes = nub $ filter (not . isStdType . stripPtr) (S.toList cpptypes)
        typefile = outdir </> "Types.hs"
        hstypify = capitalize . stripPtr . removeNamespace

    -- Types module
    withFile typefile WriteMode $ \h -> do
        hPrintf h "module %sTypes\nwhere\n\n" modprefix
        hPutStrLn h importForeign
        hPrintf h "type CBool = CChar -- correct?\n\n"
        forM_ hstypes $ \t -> do
            case (filter (\e -> enumname e == (capitalize . stripPtr . stripNamespace) t)) enums of
              ((EnumDef en vs _):_) -> do
                let hstypename = hstypify en
                    decaptn    = decapitalize hstypename
                    constrs    = map (apFst hstypify) $ getEnumValues vs
                hPrintf h "\ndata %s = %s\n\n" hstypename (intercalate " | " (map fst constrs))
                hPrintf h "%sToCInt :: %s -> CInt\n" decaptn hstypename
                forM_ constrs $ \(c, v) -> do
                    hPrintf h "%sToCInt %s = %d\n" decaptn c v
                hPrintf h "\n"
                hPrintf h "cintTo%s :: CInt -> %s\n" hstypename hstypename
                forM_ constrs $ \(c, v) -> do
                    hPrintf h "cintTo%s %d = %s\n" hstypename v c
                hPrintf h "cintTo%s n = error $ \"cintTo%s: can not convert integer '\" ++ show n ++ \"' to %s\"\n" hstypename hstypename hstypename
                hPrintf h "\n"
              _                     -> let t' = hstypify t in hPrintf h "newtype %s = %s (Ptr %s) -- nullary data type\n" t' t' t'
        hPrintf h "\n"

        -- classes and instances
        when (not . null $ inheritfile opts) $ do
            inheritdata <- withFile (inheritfile opts) ReadMode $ \ih -> do
                conts <- hGetContents ih
                forM (lines conts) $ \l -> do
                    let (cname, inheritline) = break (== '|') l
                        inherits = map (dropWhile (== ',')) $ groupBy (\_ b -> b /= ',') $ tailSafe inheritline
                    return (cname, inherits)

            let hstypeset = (S.\\) (S.fromList (map hstypify hstypes)) (S.fromList enumnames) 
                inheritlist :: [(String, [String])]
                inheritlist = M.toList . foldr (\(k, a) acc -> M.insertWith (++) k [a] acc) M.empty . map swap . expand . catMaybes $ 
                  for inheritdata $ \(cname, superclasses) ->
                      if hstypify cname `S.member` hstypeset 
                         then Just (hstypify cname, catMaybes $ for superclasses $ \s ->
                                     if (s `S.member` hstypeset) then Just (hstypify s) else Nothing)
                         else Nothing
            forM_ inheritlist $ \(cname, inheritances) -> do
                when (cname `S.member` hstypeset && not (null inheritances)) $ do
                    hPrintf h "class C%s a where\n  to%s :: a -> %s\n\n" cname cname cname
                    forM_ inheritances $ \i -> do
                         hPrintf h "instance C%s %s where\n  to%s (%s p) = %s (castPtr p)\n\n" cname i cname i cname

    expfuns <- forM funs $ \(file, filefuns) ->
        withFile (outdir </> ((takeBaseName file) ++ ".hs")) WriteMode $ \h -> do
            allgenfuns <- catMaybes <$> (forM filefuns $ \fun -> do
                case cfunToHsFun enumnames opts file fun of
                  Right hsf -> return $ Just hsf
                  Left  err -> hPrintf stderr "Function %s discarded:\n\t%s\n" (getObjName fun) err >> return Nothing)
            let constructors = filter isConstructor allgenfuns
                withfunnames = map withFunName constructors
            hPrintf h "{-# LANGUAGE ForeignFunctionInterface #-}\n"
            hPrintf h "module %s%s(\n%s\n)\n\nwhere\n\nimport %sTypes\nimport Control.Monad\n\n" 
                      modprefix 
                      (takeBaseName file) 
                      (intercalate ", \n" $ withfunnames ++ map hsfunname allgenfuns)
                      modprefix
            hPutStrLn h importForeign
            mapM_ (addWithFun h) $ filter isConstructor allgenfuns
            forM_ allgenfuns $ addFun h
            return $ withfunnames ++ map hsfunname allgenfuns

    when (not . null $ umbrellamodule opts) $ do
        withFile (outdir </> (umbrellamodule opts)) WriteMode $ \h -> do
            hPrintf h "module %s%s(\n  %s\n)\n\nwhere\n\n%s\n" 
                      modprefix
                      (takeBaseName $ umbrellamodule opts)
                      (intercalate ", \n  " $ concat expfuns)
                      (intercalate "\n" $ map (("import " ++ modprefix) ++) (map (takeBaseName . fst) funs))

withFunName :: HsFun -> String
withFunName = replace "new" "with" . hsfunname

-- creates the HsFun.
cfunToHsFun :: [String] -> Options -> FilePath -> Object -> Either String HsFun
cfunToHsFun enumnames opts filename (FunDecl fname rt ps _ _ _ _) =
  case catMaybes (map (typeValidMsg enumnames opts) (map (correctType . stripConst) (rt:pts))) of
    [] -> Right $
        HsFun filename 
              fname 
              (map (cTypeToHsCType enumnames) pts)
              (cTypeToHsCType enumnames rt) 
              (decapitalize $ dropWhile (== '_') $ dropWhile (/= '_') fname)
              (map (cTypeToHsType enumnames) pts)
              ([(cTypeToHsType enumnames rt, convRevFunc enumnames rt)])
    l  -> Left (intercalate "\n" l)
 where pts = map vartype ps
cfunToHsFun _ _ _ _ = Left "Given object is not a function"

-- cTypeToHsCType "const char **" = HsCType CChar 2
cTypeToHsCType :: [String] -> String -> HsCType
cTypeToHsCType _     "void" = HsCType "()" 0
cTypeToHsCType enums t      
  | (stripNamespace . correctType . stripExtra . stripConst $ t) `elem` enums = HsCType "CInt" 0
  | otherwise =
      case cTypeToHs t of
        Nothing -> HsCType (capitalize . removeNamespace . correctType . stripExtra . stripConst $ t) (isPtr ct - 1)
        Just t' -> HsCType t' (isPtr ct)
     where ct = correctType . stripConst $ t

-- showHsCType (CChar, 2) = (Ptr (Ptr CChar))
showHsCType :: HsCType -> String
showHsCType h = hsPointerize (numptrs h) (hsname h)

-- cTypeToHsType "const int **" = (CConvFunc "fromIntegral", "Int")
cTypeToHsType :: [String] -> String -> (CConv, String)
cTypeToHsType _     "void"  = (NoCConv, "()")
cTypeToHsType enums t 
  | correctType (stripConst t) == "char*"
     = (WithLambda "withCString", "String")
  | otherwise =
      let entype = stripNamespace . correctType . stripExtra . stripConst $ t
      in if entype `elem` enums
           then (CConvFunc (printf "%sToCInt" $ decapitalize entype), entype)
           else
             case join $ fmap cleanCType $ cTypeToHs t of
               Nothing -> (NoCConv, printHsType enums t)
               Just t' -> (convFunc t, t')

paramList :: HsFun -> String
paramList = intercalate " " . paramNames . length . hsparams

-- creates the Haskell function definition.
hsFunDefinition :: HsFun -> String
hsFunDefinition h = printf "%s %s = %s" 
               (hsfunname h) 
               (paramList h)
               (hsFunDef (hsfunname h) (hsparams h) (snd $ head $ hsrettypes h))

  where
    hsFunDef :: String -> [(CConv, String)] -> HsConv -> String
    hsFunDef fn inparams retparam = 
      let ptypes = zip (paramNames maxBound) (map (correctType . stripConst . snd) inparams)
          cstrings = filter (\(_, t) -> t == "String") ptypes
          mkCString (pnm, _) = printf "withCString %s $ \\c%s -> \n  " pnm pnm
          resLift = case retparam of
                      NoHsConv -> ""
                      HsConv n -> "liftM " ++ n ++ " $ "
          funcall = cPrefix ++ fn
          funparams = intercalate " " (map paramcall (zip inparams (map fst ptypes)))
          paramcall :: ((CConv, String), String) -> String
          paramcall ((cv, ct), pt) = pprefix ++ pname ++ psuffix
             where pname              = if ct == "String"
                                          then 'c' : pt
                                          else pt
                   (pprefix, psuffix) = case cv of
                                          CConvFunc n  -> ("(" ++ n ++ " ", ")")
                                          _            -> ("", "")
      in concatMap mkCString cstrings ++ " " ++ resLift ++ " " ++ funcall ++ " " ++ funparams

-- prints out the haskell function declaration and definition.
-- TODO: take defined out-params into account.
addFun :: Handle -> HsFun -> IO ()
addFun h hsf = do
  -- FFI import
  hPutStrLn h (ffiHsFun hsf)

  -- type signature
  hPutStrLn h (typeSigHsFun hsf)

  -- function definition
  hPutStrLn h (hsFunDefinition hsf)
  hPutStrLn h ""

 where
  ffiHsFun :: HsFun -> String
  ffiHsFun hf = hsFFIFun (cfilename hf) (cfunname hf) (hsfunname hf) (map showHsCType (cparams hf)) (showHsCType (cretparam hf))

  hsFFIFun :: String -> String -> String -> [String] -> String -> String
  hsFFIFun file fn cfn inparams retparam = printf "foreign import ccall \"%s %s\" %s%s :: %sIO %s" 
                            file fn cPrefix cfn
                            (printHsParams inparams)
                            (retparam)

  typeSigHsFun :: HsFun -> String
  typeSigHsFun hf = 
    printf "%s :: %sIO %s" 
                      (hsfunname hf)
                      (typeSigList hf)
                      (hsFunRetType hf)

hsFunRetType :: HsFun -> String
hsFunRetType hf =
  let hsr          = map (snd . fst) $ hsrettypes hf
      (pref, suff) = if length hsr == 1 then ("", "") else ("(", ")")
  in pref ++ intercalate ", " hsr ++ suff

-- e.g. "String -> String -> String ->"
typeSigList :: HsFun -> String
typeSigList hf = if null (hsparams hf) then "" else intercalate " -> " (map snd $ hsparams hf) ++ " -> "

-- checks whether the given type can be used.
-- The type can be used, if:
-- The type is a standard c type either as a value or
-- as a pointer with a defined meaning, or
-- the type is a pointer to a handle, or
-- the type is a defined enum, or
-- the type is void.
--
-- The meaning (in/out/in+out) of a standard c type is "defined", if
-- the type is not a pointer, or
-- the type is a pointer, and the meaning is given. (not yet implemented)
typeValidMsg :: [String] -> Options -> String -> Maybe String
typeValidMsg enums opts t = validateAll [(t /= "void*", "Void pointer not supported"),
                               (not (isTemplate t), "Template types not supported"),
                               ((any (==(stripNamespace t)) enums || 
                                t == "void" || 
                                isStdType t || 
                                isPtr t > 0), "type " ++ t ++ " is a value of a non-standard type"),
                               (hasDir, "direction for the type " ++ t ++ " has not been defined in the interface file")]
  where validateAll = foldl' validate Nothing
          where validate (Just n) _        = Just n
                validate Nothing  (f, str) = if not f then Just str else Nothing
        hasDir = isPtr t == 0 ||
                   (isPtr t == 1 && (not $ isJust $ cTypeToHs t)) ||
                   t `elem` defaultins opts

addWithFun :: Handle -> HsFun -> IO ()
addWithFun h fun = do
  let fn  = hsfunname fun
      fnw = withFunName fun
      dst = replace "new.*" "delete" fn
      typ = hsFunRetType fun
      pl  = paramList fun
  hPrintf h "%s :: %s(%s -> IO a) -> IO a\n" fnw (typeSigList fun) typ
  hPrintf h "%s %s f = do\n" fnw pl
  hPrintf h "    obj <- %s %s\n" fn pl
  hPrintf h "    res <- f obj\n"
  hPrintf h "    %s obj\n" dst
  hPrintf h "    return res\n\n"

isConstructor :: HsFun -> Bool
isConstructor = constructor . hsfunname

isDestructor :: HsFun -> Bool
isDestructor = destructor . hsfunname

destructor :: String -> Bool
destructor fn = fn =~ ".*_delete$"

constructor :: String -> Bool
constructor fn = fn =~ ".*_new[0-9]*$"

importForeign :: String
importForeign = "import Foreign\nimport Foreign.C.String\nimport Foreign.C.Types\n"

cPrefix :: String
cPrefix = "c_"

-- in: a c type, like "int"
-- out: the haskell conversion function for converting from haskell data type
convFunc :: String -> CConv
convFunc ptype =
  case fromMaybe "" $ cTypeToHs ptype of
    "CChar"   -> CConvFunc "castCCharToChar" 
    "CSChar"  -> CConvFunc "fromIntegral" 
    "CUChar"  -> CConvFunc "fromIntegral" 
    "CShort"  -> CConvFunc "fromIntegral" 
    "CUShort" -> CConvFunc "fromIntegral" 
    "CInt"    -> CConvFunc "fromIntegral" 
    "CUInt"   -> CConvFunc "fromIntegral" 
    "CSize"   -> CConvFunc "fromIntegral" 
    "CLong"   -> CConvFunc "fromIntegral" 
    "CULong"  -> CConvFunc "fromIntegral" 
    "CFloat"  -> CConvFunc "realToFrac" 
    "CDouble" -> CConvFunc "realToFrac" 
    "CBool"   -> CConvFunc "fromBool" 
    _         -> NoCConv

-- in: a c type, like "int"
-- out: the haskell conversion function for converting to haskell data type
convRevFunc :: [String] -> String -> HsConv
convRevFunc enums t
  | fromMaybe "" (cTypeToHs t) == "CBool" = HsConv "toBool"
  | otherwise = 
     let ccf = convFunc t
     in case ccf of
          CConvFunc n -> HsConv n
          _           -> 
            let entype = stripNamespace . correctType . stripExtra . stripConst $ t
            in if entype `elem` enums
                 then HsConv $ printf "cintTo%s" entype
                 else NoHsConv

paramNames :: Int -> [String]
paramNames n = map ('p':) (map show [1..n])

-- printHsType "const char **" = "(Ptr (Ptr CChar))"
printHsType :: [String] -> String -> String
printHsType _     "void" = "()"
printHsType enums t      = 
  case cTypeToHs t of
    Nothing -> hsPointerize (isPtr ct - 1) $ capitalize . fixNamespace enums . correctType . stripConst $ t
    Just t' -> hsPointerize (isPtr ct) t'
 where ct = correctType . stripConst $ t

hsPointerize :: Int -> String -> String
hsPointerize numPtrs t = 
  concat (replicate numPtrs "(Ptr ") ++ (stripPtr t) ++ concat (replicate numPtrs ")")

cTypeToHs :: String -> Maybe String
cTypeToHs = cTypeToHs' . clearType
    where -- strips const, pointers: const char ** => char
      clearType :: String -> String
      clearType = stripPtr . correctType . stripConst

cTypeToHs' :: String -> Maybe String
cTypeToHs' "float" = Just "CFloat"
cTypeToHs' "double" = Just "CDouble"
cTypeToHs' "char" = Just "CChar"
cTypeToHs' "int" = Just "CInt"
cTypeToHs' "unsigned int" = Just "CUInt"
cTypeToHs' "signed int" = Just "CInt"
cTypeToHs' "long" = Just "CLong"
cTypeToHs' "unsigned long" = Just "CULong"
cTypeToHs' "signed long" = Just "CLong"
cTypeToHs' "bool" = Just "CBool"
cTypeToHs' "short" = Just "CShort"
cTypeToHs' "unsigned short" = Just "CUShort"
cTypeToHs' "signed short" = Just "CShort"
cTypeToHs' "unsigned" = Just "CInt"
cTypeToHs' "long long" = Just "CLong"
cTypeToHs' "unsigned long long" = Just "CULong"
cTypeToHs' "int8_t" = Just "CChar"
cTypeToHs' "uint8_t" = Just "CUChar"
cTypeToHs' "int16_t" = Just "CShort"
cTypeToHs' "uint16_t" = Just "CUShort"
cTypeToHs' "int32_t" = Just "CLong"
cTypeToHs' "uint32_t" = Just "CULong"
cTypeToHs' "int64_t" = Just "CLong"
cTypeToHs' "uint64_t" = Just "CULong"
cTypeToHs' "size_t" = Just "CSize"
cTypeToHs' "uint8" = Just "CUChar"
cTypeToHs' "uint16" = Just "CUShort"
cTypeToHs' "uint32" = Just "CULong"
cTypeToHs' "uint64" = Just "CULong"
cTypeToHs' _ = Nothing

cleanCType :: String -> Maybe String
cleanCType "CFloat"      = Just "Float"
cleanCType "CDouble"     = Just "Double"
cleanCType "CChar"       = Just "Char"
cleanCType "CSChar"      = Just "Int"
cleanCType "CUChar"      = Just "Int"
cleanCType "CInt"        = Just "Int"
cleanCType "CUInt"       = Just "Int"
cleanCType "CLong"       = Just "Int"
cleanCType "CULong"      = Just "Int"
cleanCType "CBool"       = Just "Bool"
cleanCType "CShort"      = Just "Int"
cleanCType "CUShort"     = Just "Int"
cleanCType "CSize"       = Just "Int"
cleanCType _ = Nothing

printHsParams :: [String] -> String
printHsParams [] = ""
printHsParams types = 
  intercalate " -> " types ++ " -> " 

removeNamespace :: String -> String
removeNamespace = map (\c -> if c == ':' then '_' else c)

stripNamespace :: String -> String
stripNamespace = last . takeWhile (not . null) . iterate (dropWhile (==':') . snd . break (== ':'))

fixNamespace :: [String] -> String -> String
fixNamespace enums n = (if (stripNamespace n) `elem` enums then stripNamespace else removeNamespace) n