packages feed

language-boogie-0.1: Boogaloo.hs

{-# LANGUAGE DeriveDataTypeable #-}

module Main where

import Language.Boogie.AST
import Language.Boogie.Util
import Language.Boogie.Position
import qualified Language.Boogie.Parser as Parser (program)
import Language.Boogie.TypeChecker
import Language.Boogie.PrettyPrinter
import Language.Boogie.Interpreter
import Language.Boogie.Tester
import System.Environment
import System.Console.CmdArgs
import System.Random
import Data.Time.Calendar
import Data.List
import Data.Map (Map, (!))
import qualified Data.Map as M
import Control.Monad.State
import Control.Applicative
import Text.PrettyPrint hiding (mode)
import Text.ParserCombinators.Parsec (parse, parseFromFile)

programName = "boogaloo"
versionName = "0.1"
releaseDate = fromGregorian 2012 10 25

-- | Execute or test a Boogie program, according to command-line arguments
main = do
  res <- cmdArgsRun $ mode
  case res of
    Exec file entry -> executeFromFile file entry
    args -> testFromFile (file args) (proc_ args) (testMethod args) (verbose args)

{- Command line arguments -}

data CommandLineArgs
    = Exec { file :: String, entry :: String }
    | Test { file :: String, proc_ :: [String], limits :: (Integer, Integer), dlimits :: (Integer, Integer), verbose :: Bool  }
    | RTest { file :: String, proc_ :: [String], limits :: (Integer, Integer), dlimits :: (Integer, Integer), tc_count :: Int, seed :: Maybe Int, verbose :: Bool }
      deriving (Data, Typeable, Show, Eq)

execute = Exec {
  entry = "Main"  &= help "Program entry point (must not have in- or out-parameters)" &= typ "PROCEDURE",
  file  = ""      &= typFile &= argPos 0
  } &= auto &= help "Execute program"
      
test_ = Test {
  proc_   = []      &= help "Procedures to test" &= typ "PROCEDURE",
  limits  = (-3, 3) &= help "Interval of input values to try for an integer variable" &= typ "NUM, NUM",
  dlimits = (0, 2)  &= help dlimitsMsg &= typ "NUM, NUM" ,
  file    = ""      &= typFile &= argPos 0,
  verbose = False   &= help verboseMsg
  } &= help "Test program exhaustively"
  
rtest = RTest {
  proc_     = []        &= help "Procedures to test" &= typ "PROCEDURE",
  limits    = (-32, 32) &= help "Interval of input values to draw from for an integer variable" &= typ "NUM, NUM",
  dlimits   = (0, 2)    &= help dlimitsMsg &= typ "NUM, NUM",
  tc_count  = 10        &= help "Number of test cases to generate per procedure implementation" &= name "n" &= typ "NUM",
  seed      = Nothing   &= help "Seed for the random number generator" &= typ "NUM",
  file      = ""        &= typFile &= argPos 0,
  verbose = False       &= help verboseMsg
  } &= help "Test program on random inputs"
  
dlimitsMsg = "Given a map with an integer domain, different range values will be tried for domain values in this interval"
verboseMsg = "Output all executed test cases"
    
mode = cmdArgsMode $ modes [execute, test_, rtest] &= 
  help "Boogie interpreter" &= 
  program programName &= 
  summary (programName ++ " v" ++ versionName ++ ", " ++ showGregorian releaseDate)
  
-- | Set up a test method depending on command-line arguments  
testMethod :: CommandLineArgs -> Program -> Context -> [Id] -> IO [TestCase]
testMethod (Test _ _ limits dlimits _ ) program context procNames = 
  let settings = ExhaustiveSettings {
      esIntRange = interval limits,
      esIntMapDomainRange = interval dlimits,
      esGenericTypeRange = defaultGenericTypeRange context,
      esMapTypeRange = defaultMapTypeRange context
    }
  in return $ testProgram settings program context procNames
testMethod (RTest _ _ limits dlimits tc_count seed _) program context procNames = do
  defaultGen <- getStdGen
  randomGen <- case seed of
    Nothing -> getStdGen
    Just s -> return $ mkStdGen s
  let settings = RandomSettings {
      rsRandomGen = randomGen,
      rsCount = tc_count,
      rsIntLimits = limits,
      rsIntMapDomainRange = interval dlimits,
      rsGenericTypeRange = defaultGenericTypeRange context,
      rsMapTypeRange = defaultMapTypeRange context     
    }  
  return $ testProgram settings program context procNames
    
{- Interfacing internal modules -}

-- | Execute procedure entryPoint from file
-- | and output either errors or the final values of global variables
executeFromFile :: String -> String -> IO ()
executeFromFile file entryPoint = runOnFile printFinalState file
  where
    printFinalState p context = case M.lookup entryPoint (ctxProcedures context) of
      Nothing -> print (text "Cannot find program entry point" <+> text entryPoint)
      Just sig -> if not (goodEntryPoint sig)
        then print (text "Program entry point" <+> text entryPoint <+> text "does not have the required signature" <+> doubleQuotes (sigDoc [] []))
        else case executeProgram p context entryPoint of
          Left err -> print err
          Right env -> (print . varsDoc . envGlobals) env
    goodEntryPoint sig = null (psigTypeVars sig) && null (psigArgTypes sig) && null (psigRetTypes sig)

-- | Test procedures procNames from file with a testMethod
-- | and output the test outcomes
testFromFile :: String -> [String] -> (Program -> Context -> [String] -> IO [TestCase]) -> Bool -> IO ()
testFromFile file procNames testMethod printAll = runOnFile printTestOutcomes file
  where
    printTestOutcomes p context = do
      let (present, missing) = partition (`M.member` ctxProcedures context) procNames
      when (not (null missing)) $ print (text "Cannot find procedures under test:" <+> commaSep (map text missing))
      testResults <- testMethod p context present
      print $ testSessionSummary testResults
      when printAll $ putStr "\n" >> mapM_ print testResults

-- | Parse file, type-check the resulting program, then execute command on the resulting program and type context
runOnFile :: (Program -> Context -> IO ()) -> String -> IO ()      
runOnFile command file = do 
  parseResult <- parseFromFile Parser.program file
  case parseResult of
    Left parseErr -> print parseErr
    Right p -> case checkProgram p of
      Left typeErrs -> print (typeErrorsDoc typeErrs)
      Right context -> command p context
      
{- Helpers for testing internal functions -}      
      
-- | Harness for testing various internal functions
harness file = runOnFile printOutcome file
  where
    printOutcome p context = do
      let env = execState (collectDefinitions p) emptyEnv { envTypeContext = context }
      print $ envGlobals env
      
-- | Test that print . parse == print . parse . print .parse      
testParser :: String -> IO ()      
testParser file = do
  result <- parseFromFile Parser.program file
  case (result) of
    Left err -> print err
    Right p -> do
      case parse Parser.program ('*' : file) (show p) of
        Left err -> print err
        Right p' -> if p == p'
          then putStr ("Passed.\n")
          else putStr ("Failed with different ASTs.\n")