packages feed

llvm-pretty-bc-parser-0.3.0.0: disasm-test/Main.hs

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE RecordWildCards #-}

module Main where

import Data.LLVM.BitCode (parseBitCodeLazyFromFile,Error(..),formatError)
import Text.LLVM.AST (Module)
import Text.LLVM.PP (ppLLVM,ppModule)

import Control.Monad (when)
import Data.Char (ord,isSpace,chr)
import Data.Monoid ( mconcat, Endo(..) )
import Data.Typeable (Typeable)
import System.Console.GetOpt
           ( ArgOrder(..), ArgDescr(..), OptDescr(..), getOpt, usageInfo )
import System.Directory (getTemporaryDirectory,removeFile)
import System.Environment (getArgs,getProgName)
import System.Exit (exitFailure,exitSuccess,ExitCode(..))
import System.FilePath ((<.>),dropExtension,takeFileName)
import System.IO
    (openBinaryTempFile,hClose,openTempFile,hPrint)
import System.Process
    (proc,createProcess,waitForProcess,cmdspec,CmdSpec(..),CreateProcess())
import qualified Control.Exception as X
import qualified Data.ByteString.Lazy as L


-- Option Parsing --------------------------------------------------------------

data Options = Options { optTests   :: [FilePath] -- ^ Tests
                       , optLlvmAs  :: String     -- ^ llvm-as  name
                       , optLlvmDis :: String     -- ^ llvm-dis name
                       , optHelp    :: Bool
                       } deriving (Show)

defaultOptions :: Options
defaultOptions  = Options { optTests   = []
                          , optLlvmAs  = "llvm-as"
                          , optLlvmDis = "llvm-dis"
                          , optHelp    = False
                          }

options :: [OptDescr (Endo Options)]
options  =
  [ Option "" ["with-llvm-as"] (ReqArg setLlvmAs "FILEPATH")
    "path to llvm-as"
  , Option "" ["with-llvm-dis"] (ReqArg setLlvmDis "FILEPATH")
    "path to llvm-dis"
  , Option "h" ["help"] (NoArg setHelp)
    "display this message"
  ]

setLlvmAs :: String -> Endo Options
setLlvmAs str = Endo (\opt -> opt { optLlvmAs = str })

setLlvmDis :: String -> Endo Options
setLlvmDis str = Endo (\opt -> opt { optLlvmDis = str })

setHelp :: Endo Options
setHelp  = Endo (\opt -> opt { optHelp = True })

addTest :: String -> Endo Options
addTest test = Endo (\opt -> opt { optTests = test : optTests opt })

getOptions :: IO Options
getOptions  =
  do args <- getArgs
     case getOpt (ReturnInOrder addTest) options args of

       (fs,[],[]) -> do let opts = appEndo (mconcat fs) defaultOptions

                        when (optHelp opts) $ do printUsage []
                                                 exitSuccess

                        return opts

       (_,_,errs) -> do printUsage errs
                        exitFailure

printUsage :: [String] -> IO ()
printUsage errs =
  do prog <- getProgName
     let banner = "Usage: " ++ prog ++ " [OPTIONS] test1.ll .. testn.ll"
     putStrLn (usageInfo (unlines (errs ++ [banner])) options)


-- Test Running ----------------------------------------------------------------

-- | Run all provided tests.
main :: IO ()
main  = do
  opts <- getOptions
  mapM_ (runTest opts) (optTests opts)

-- | A test failure.
data TestFailure
  = ParseError Error -- ^ A parser failure.
    deriving (Typeable,Show)

instance X.Exception TestFailure

-- | Attempt to compare the assembly generated by llvm-pretty and llvm-dis.
runTest :: Options -> FilePath -> IO ()
runTest opts file = do
  putStr (showString file ":")
  X.handle logError                                  $
    X.handle logCommandError                         $
    X.bracket (generateBitCode  opts pfx file) removeFile $ \ bc     ->
    X.bracket (normalizeBitCode opts pfx bc)   removeFile $ \ norm   ->
    X.bracket (processBitCode        pfx bc)   removeFile $ \ parsed -> do
      ignore (wait (proc "diff" ["-u",norm,parsed]))
      putStrLn "success"
  where
  pfx = dropExtension (takeFileName file)

  logError (ParseError msg) = do
    putStrLn "failure"
    putStrLn (unlines (map ("; " ++) (lines (formatError msg))))

  logCommandError (CommandFailed cmd) = do
    putStrLn "failure"
    putStrLn ("Command ``" ++ cmd ++ "'' failed\n\n")

-- | Assemble some llvm assembly, producing a bitcode file in /tmp.
generateBitCode :: Options -> FilePath -> FilePath -> IO FilePath
generateBitCode Options { .. } pfx file = do
  tmp    <- getTemporaryDirectory
  (bc,h) <- openBinaryTempFile tmp (pfx <.> "bc")
  hClose h
  wait (proc optLlvmAs ["-o", bc, file])
  return bc

-- | Use llvm-dis to parse a bitcode file, to obtain a normalized version of the
-- llvm assembly.
normalizeBitCode :: Options -> FilePath -> FilePath -> IO FilePath
normalizeBitCode Options { .. } pfx file = do
  tmp      <- getTemporaryDirectory
  (norm,h) <- openTempFile tmp (pfx <.> "ll")
  hClose h
  wait (proc optLlvmDis ["-o", norm, file])
  stripComments norm
  return norm

-- | Parse a bitcode file using llvm-pretty, failing the test if the parser
-- fails.
processBitCode :: FilePath -> FilePath -> IO FilePath
processBitCode pfx file = do
  let handler :: X.SomeException -> IO (Either Error Module)
      handler se = return (Left (Error [] (show se)))
  e <- parseBitCodeLazyFromFile file `X.catch` handler
  case e of
    Left err -> X.throwIO (ParseError err)
    Right m  -> do
      tmp        <- getTemporaryDirectory
      (parsed,h) <- openTempFile tmp (pfx <.> "ll")
      hPrint h (ppLLVM (ppModule m))
      hClose h
      stripComments parsed
      return parsed

-- | Remove comments from a .ll file, stripping everything including the
-- semi-colon.
stripComments :: FilePath -> IO ()
stripComments path = do
  bytes <- L.readFile path
  removeFile path
  mapM_ (writeLine . dropComments) (bsLines bytes)
  where
  writeLine bs | L.null bs = return ()
               | otherwise = do
                 L.appendFile path bs
                 L.appendFile path (L.singleton 0x0a)

-- | Split a bytestring by its lines.
bsLines :: L.ByteString -> [L.ByteString]
bsLines = L.split char
  where
  char = fromIntegral (ord '\n')

-- | Take characters until the llvm comment delimiter is found.
dropComments :: L.ByteString -> L.ByteString
dropComments  = dropTrailingSpace . L.takeWhile (/= char)
  where
  char = fromIntegral (ord ';')

-- | Drop trailing space from a bytestring.
dropTrailingSpace :: L.ByteString -> L.ByteString
dropTrailingSpace bs
  | len <= 0  = L.empty
  | otherwise = L.take (loop len) bs
  where
  len = L.length bs - 1
  loop n | isSpace (chr (fromIntegral (L.index bs n))) = loop (n-1)
         | otherwise                                   = n

-- | A shell-command failure.
data CommandError
  = CommandFailed String -- ^ The command failed when running.
    deriving (Typeable,Show)

instance X.Exception CommandError

-- | Construct a command error, given a description of the command that was run.
commandError :: CmdSpec -> CommandError
commandError (ShellCommand str)  = CommandFailed str
commandError (RawCommand p args) = CommandFailed (unwords (takeFileName p:args))

-- | Run a command, waiting for it to return.
wait :: CreateProcess -> IO ()
wait cmd = do
  (_,_,_,ph) <- createProcess cmd
  status     <- waitForProcess ph
  case status of
    ExitFailure{} -> X.throwIO (commandError (cmdspec cmd))
    _             -> return ()

-- | Ignore a command that fails.
ignore :: IO () -> IO ()
ignore  = X.handle f
  where
  f :: CommandError -> IO ()
  f _ = return ()