packages feed

lapack-ffi-tools-0.1: src/MainAll.hs

module Main where

import qualified CreateBinding
import qualified Type
import Parser.Signature (Identifier(Identifier))
import CreateBinding
         (formatForeignCall, formatCArrayWrapper, parseHeader,
          foreignFromParameters, arrayTypeFromBool)

import qualified Text.ParserCombinators.Parsec as Parsec
import qualified Text.ParserCombinators.Parsec.Error as ParsecError

import qualified Options.Applicative as OP

import qualified System.IO as IO
import qualified System.Path.PartClass as PathClass
import qualified System.Path.IO as PathIO
import qualified System.Path as Path
import System.Path.Directory (createDirectoryIfMissing)
import System.Path (takeFileName, (</>))

import Text.Printf (printf)

import qualified Data.Csv as Csv
import qualified Data.HashMap.Lazy as HashMap
import qualified Data.Map as Map
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.ByteString.Char8 as B
import qualified Data.Foldable as Fold
import qualified Data.List.HT as ListHT
import qualified Data.List as List
import Data.Traversable (Traversable, for, sequenceA, traverse)
import Data.Foldable (Foldable)
import Data.Vector (Vector)
import Data.ByteString (ByteString)
import Data.HashMap.Lazy (HashMap)
import Data.Map (Map)
import Data.Csv ((.:))
import Data.Tuple.HT (mapSnd, thd3)
import Data.Maybe (isNothing, mapMaybe)
import Data.Bool.HT (if')
import Data.Char (toUpper)
import Data.Monoid ((<>))

import qualified Control.Monad.Exception.Synchronous as ME
import qualified Control.Monad.Trans.Writer as MW
import qualified Control.Applicative.HT as App
import qualified Control.Functor.HT as Func
import Control.Monad (when, join, guard)
import Control.Applicative (Applicative, liftA2, (<$>), (<*>))



class Type.Unification f => Generalize f where
   formatGenericSig :: f Type.Poly -> String

instance Generalize Type.Foreign where
   formatGenericSig = Type.formatForeign

instance Generalize Type.Wrapper where
   formatGenericSig wrapper@(Type.Wrapper inputs _returnType _outputs) =
      concatMap
         (\(typ,dims,dir) ->
            printf "%s -> " (Type.formatArray (arrayTypeFromBool dir) typ dims))
         inputs ++
      "IO " ++ CreateBinding.formatOutputTuple wrapper

prepareClassCall ::
   (Generalize f) => (String, f Type.Poly) -> (String, String, String)
prepareClassCall (name, func) =
   let typeName = map toUpper name
       sig = formatGenericSig func
       newtypeDef =
         printf "newtype %s a = %s {get%s :: %s}" typeName typeName typeName sig
   in  (typeName, sig, newtypeDef)

formatClassCall ::
   (Generalize f) => Char -> Char -> String -> f Type.Poly -> String
formatClassCall singleAbbr doubleAbbr name typ =
   let (typeName, sig, newtypeDef) = prepareClassCall (name, typ)
       typeBranch :: Char -> String
       typeBranch abbr = printf "(%s %c.%s)" typeName abbr name
   in  unlines $
      "" :
      "" :
      newtypeDef :
      "" :
      printf "%s :: Class.Real a => %s" name sig :
      printf "%s = get%s $ Class.switchReal %s %s"
         name typeName (typeBranch singleAbbr) (typeBranch doubleAbbr) :
      []

formatClassCallRC ::
   (Generalize f) => String -> String -> String -> f Type.Poly -> String
formatClassCallRC genName realName complexName typ =
   let (typeName, sig, newtypeDef) = prepareClassCall (genName, typ)
       typeBranch :: Char -> String -> String
       typeBranch abbr name = printf "(%s %c.%s)" typeName abbr name
   in  unlines $
      "" :
      "" :
      newtypeDef :
      "" :
      printf "%s :: Class.Floating a => %s" genName sig :
      printf "%s = get%s $ Class.switchFloating %s %s %s %s" genName typeName
         (typeBranch 'S' realName) (typeBranch 'D' realName)
         (typeBranch 'C' complexName) (typeBranch 'Z' complexName) :
      []


insertExportList :: [String] -> String -> String
insertExportList exports modul =
   (\(prefix,suffixes) ->
      map fst prefix ++
      case suffixes of
         [] -> []
         (_,suffix):_ ->
            "(\n" ++ unlines (map (printf "   %s,") exports) ++
            "   ) " ++ suffix) $
   break (List.isPrefixOf "where" . snd) $
   zip modul (ListHT.tails modul)

writeHeader ::
   (PathClass.AbsRel ar) =>
   Path.Dir ar -> B.ByteString -> Maybe [String] -> IO (Path.File ar)
writeHeader dstDir modul mexports = do
   let makePath =
         either (ioError . userError) (return . (dstDir </>)) . Path.parse
   tmplFile <- makePath $ B.unpack modul ++ "_tmpl.hs"
   dstFile <- makePath $ B.unpack modul ++ ".hs"
   tmpl <- PathIO.readFile tmplFile
   PathIO.writeFile dstFile $
      "-- Do not edit! Automatically generated by create-lapack-ffi.\n" ++
      maybe tmpl (flip insertExportList tmpl) mexports
   return dstFile

indent :: String -> String
indent = unlines . map ("    " ++) . lines

formatFileError :: (PathClass.AbsRel ar) => Path.File ar -> String -> String
formatFileError path msg =
   printf "\n%s:1:1:\n%s" (Path.toString path) (indent msg)

formatParseError :: Parsec.ParseError -> String
formatParseError err =
   let source = Parsec.errorPos err
   in  printf "\n%s:%d:%d:\n%s"
         (Parsec.sourceName source)
         (Parsec.sourceLine source)
         (Parsec.sourceColumn source)
         (indent $ dropWhile ('\n'==) $
          ParsecError.showErrorMessages
            "or" "unknown parse error" "expecting" "unexpected" "end of input"
            (ParsecError.errorMessages err))

printError :: String -> IO ()
printError = IO.hPutStr IO.stderr

printFileError :: (PathClass.AbsRel ar) => Path.File ar -> String -> IO ()
printFileError path msg = printError $ formatFileError path msg


floatColumn, doubleColumn,
   complexFloatColumn, complexDoubleColumn,
   realColumn, complexColumn, genericColumn :: ByteString
floatColumn = B.pack "Float"
doubleColumn = B.pack "Double"
complexFloatColumn = B.pack "ComplexFloat"
complexDoubleColumn = B.pack "ComplexDouble"

realColumn = B.pack "Real"
complexColumn = B.pack "Complex"
genericColumn = B.pack "Generic"

data Unified a =
   Unified {
      unifiedGeneric, unifiedReal, unifiedComplex :: a
   }

instance Foldable Unified where
   foldMap f (Unified g r c) = f g <> f r <> f c

instance Functor Unified where
   fmap f (Unified g r c) = Unified (f g) (f r) (f c)

instance Traversable Unified where
   sequenceA (Unified g r c) = App.lift3 Unified g r c

instance Applicative Unified where
   pure a = Unified a a a
   Unified fg fr fc <*> Unified g r c = Unified (fg g) (fr r) (fc c)

unifiedColumns :: Unified ByteString
unifiedColumns = Unified genericColumn realColumn complexColumn

unifiedColumnsMap :: HashMap ByteString ()
unifiedColumnsMap =
   HashMap.fromList $ map (flip (,) ()) $ Fold.toList unifiedColumns

data FunctionSet =
   FunctionSet (Unified (Bool, String)) (Map ByteString (Bool, Path.RelFile))

newtype RelFile = RelFile {getRelFile :: Maybe (Bool, Path.RelFile)}

instance Csv.FromField RelFile where
   parseField str =
      let parsePath = either fail (return . Just) . Path.parse . B.unpack
      in fmap RelFile $
         case B.uncons str of
            Nothing -> return Nothing
            Just ('!', path) -> fmap ((,) False) <$> parsePath path
            Just _ -> fmap ((,) True) <$> parsePath str

checkExcl :: String -> (Bool, String)
checkExcl ('!':path) = (False, path)
checkExcl path = (True, path)

instance Csv.FromNamedRecord FunctionSet where
   parseNamedRecord m = do
      names <- fmap checkExcl <$> traverse (m.:) unifiedColumns
      when (null $ snd $ unifiedGeneric names) $ fail "empty Generic name"
      set <- Csv.parseNamedRecord $ HashMap.difference m unifiedColumnsMap
      return $ FunctionSet names $ Map.mapMaybe getRelFile set


type URL = String

processFunction ::
   (PathClass.AbsRel ar) =>
   Bool ->
   (Path.File ar, Path.File ar) ->
   URL -> String -> Path.File ar ->
   IO (Maybe (Type.Foreign Type.Mono, Type.Wrapper Type.Mono))
processFunction output (ffiFile, carrayFile) url haskellName srcPath = do
   content <- PathIO.readFile srcPath
   let msignature = do
         sig <-
            fmap MW.runWriter $
            ME.mapException formatParseError $ ME.fromEither $
            Parsec.parse parseHeader (Path.toString srcPath) content
         when
            (elem (thd3 $ fst sig)
               [Just Type.ComplexSingle, Just Type.ComplexDouble]) $
            ME.throw $
               formatFileError srcPath "complex function result not supported"
         return sig
   case msignature of
      ME.Exception msg -> printError msg >> return Nothing
      ME.Success ((Identifier name, params, returnType), msgs) -> do
         mapM_ (printFileError srcPath) msgs
         let haddock =
               if null url
                 then "\n"
                 else
                     printf "\n-- | <%s>\n"
                        (printf url (Path.toString $ takeFileName srcPath)
                           :: String)
         let foreignSig = foreignFromParameters params returnType
         carrayWrapper <- do
            let (mono,poly) =
                  formatCArrayWrapper (haskellName, params, returnType)
            when output $ PathIO.appendFile carrayFile $ haddock ++ mono
            return poly
         when output $ PathIO.appendFile ffiFile $
            haddock ++ formatForeignCall ((haskellName, name), foreignSig)
         return $ Just (foreignSig, carrayWrapper)

unifyFunctions ::
   (Type.Unification f, PathClass.AbsRel ar) =>
   String ->
   Map ByteString (Path.File ar, f Type.Mono) ->
   IO (Unified (Maybe (f Type.Poly)))
unifyFunctions msg monoSigs = do
   let unify uni (xPath,xSig) (yPath,ySig) = do
         let m = Type.unifySignature uni xSig ySig
         when (isNothing m) $ do
            printFileError xPath ""
            printFileError yPath msg
         return m
   real <-
      fmap join $ sequenceA $
      liftA2
         (unify Type.unifyPrecision)
         (Map.lookup floatColumn monoSigs)
         (Map.lookup doubleColumn monoSigs)
   complex <-
      fmap join $ sequenceA $
      liftA2
         (unify Type.unifyPrecision)
         (Map.lookup complexFloatColumn monoSigs)
         (Map.lookup complexDoubleColumn monoSigs)
   let generic =
         join $ liftA2 (Type.unifySignature Type.unifyRealComplex) real complex
   return $ Unified generic real complex


writePolymorphic ::
   (PathClass.AbsRel ar) =>
   Path.Dir ar ->
   ByteString ->
   [(String, String)] ->
   IO ()
writePolymorphic dir name polySigs = do
   file <- writeHeader dir name (Just $ map fst polySigs)
   mapM_ (PathIO.appendFile file . snd) polySigs

writeUnified ::
   (PathClass.AbsRel ar, Generalize f) =>
   Path.Dir ar ->
   Vector (Unified (Bool,String), Unified (Maybe (f Type.Poly))) ->
   IO ()
writeUnified dstDir unifiedSigVec = do
   let unifiedSigs = map maskOutput $ Fold.toList unifiedSigVec
   writePolymorphic dstDir realColumn
      (mapMaybe
         (\(Unified _ (name,func) _) ->
            (,) name . formatClassCall 'S' 'D' name <$> func)
         unifiedSigs)

   writePolymorphic dstDir complexColumn
      (mapMaybe
         (\(Unified _ _ (name,func)) ->
            (,) name . formatClassCall 'C' 'Z' name <$> func)
         unifiedSigs)

   writePolymorphic dstDir genericColumn
      (mapMaybe
         (\(Unified (genericName,func) (realName,_) (complexName,_)) ->
            (,) genericName .
                  formatClassCallRC genericName realName complexName
               <$> func)
         unifiedSigs)

maskOutput ::
   (Unified (Bool,String), Unified (Maybe (f Type.Poly))) ->
   Unified (String, Maybe (f Type.Poly))
maskOutput =
   uncurry $ liftA2 (\(output,name) func -> (name, guard output >> func))


pathArgument ::
   (PathClass.FileDir fd) =>
   OP.Mod OP.ArgumentFields (Path.AbsRel fd) -> OP.Parser (Path.AbsRel fd)
pathArgument = OP.argument (OP.eitherReader Path.parse)

optParser ::
   OP.Parser
      (URL, (Path.AbsRelFile, Path.AbsRelDir, Path.AbsRelDir, Path.AbsRelDir))
optParser =
   App.lift2 (,)
      (OP.strOption $
         OP.long "fortran-url" <>
         OP.metavar "URL" <>
         OP.value "" <>
         OP.help "Printf template for URL of online help") $
   App.lift4 (,,,)
      (pathArgument $
         OP.metavar "CSV" <>
         OP.help "Spreadsheet containing grouped Fortran file names")
      (pathArgument $
         OP.metavar "SRCDIR" <>
         OP.help "Input directory containing Fortran files")
      (pathArgument $
         OP.metavar "FFIDIR" <>
         OP.help "Output directory for Haskell FFI modules")
      (pathArgument $
         OP.metavar "CARRAYDIR" <>
         OP.help "Output directory for Haskell CArray wrapper modules")

optionInfo :: OP.Parser a -> OP.ParserInfo a
optionInfo parser =
   OP.info
      (OP.helper <*> parser)
      (OP.fullDesc <>
       OP.progDesc
         "Automated generation of low-level and mid-level bindings for LAPACK")


main :: IO ()
main = do
   (url, (csvFile, srcDir, ffiDir, carrayDir)) <-
      OP.execParser $ optionInfo optParser
   createDirectoryIfMissing True ffiDir
   createDirectoryIfMissing True carrayDir

   (headers, functionSets) <-
      either (ioError . userError) return . Csv.decodeByName
       =<< BL.readFile (Path.toString csvFile)

   dstFiles <-
      fmap Map.fromList $
      for (filter (not . flip HashMap.member unifiedColumnsMap) $
           Fold.toList headers) $ \columnName ->
         fmap ((,) columnName) $
         liftA2 (,)
            (writeHeader ffiDir columnName Nothing)
            (writeHeader carrayDir columnName Nothing)

   sigs <-
      for functionSets $ \(FunctionSet flaggedNames files) ->
         fmap ((,) flaggedNames . Map.mapMaybe id) $
         sequenceA $
         flip Map.mapWithKey files $ \modul (output,file) -> do
            let names = snd <$> flaggedNames
                name =
                  if' (modul == floatColumn || modul == doubleColumn)
                     (unifiedReal names) $
                  if' (modul == complexFloatColumn ||
                       modul == complexDoubleColumn)
                     (unifiedComplex names) $
                  (unifiedGeneric names)
            let srcPath = srcDir </> file
            fmap ((,) srcPath) <$>
               processFunction output (dstFiles Map.! modul) url name srcPath

   let runUnification dstDir msg select =
         writeUnified dstDir =<<
         traverse (Func.mapSnd (unifyFunctions msg . fmap (mapSnd select))) sigs

   runUnification ffiDir "Cannot generalize FFI functions." fst
   runUnification carrayDir "Cannot generalize CArray functions." snd