packages feed

HerbiePlugin-0.2.0.0: src/Herbie/ForeignInterface.hs

{-# LANGUAGE OverloadedStrings #-}

module Herbie.ForeignInterface
    where

import Control.Applicative
import Control.Exception
import Control.DeepSeq
import Data.List
import Data.String
import qualified Data.Text as T
import Database.SQLite.Simple
import Database.SQLite.Simple.FromRow
import Database.SQLite.Simple.FromField
import Database.SQLite.Simple.ToField
import GHC.Generics hiding (modName)
import System.Directory
import System.Process
import System.Timeout

import Paths_HerbiePlugin
import Herbie.MathInfo
import Herbie.MathExpr

import Prelude

-- | Given a MathExpr, return a numerically stable version.
stabilizeMathExpr :: DbgInfo -> MathExpr -> IO (StabilizerResult MathExpr)
stabilizeMathExpr dbgInfo cmdin = do
    let (cmdinLisp,varmap) = getCanonicalLispCmd $ haskellOpsToHerbieOps cmdin
    res <- stabilizeLisp dbgInfo cmdinLisp
    cmdout' <- do
        -- FIXME:
        -- Due to a bug in Herbie, fromCanonicalLispCmd sometimes throws an exception.
        ret <- try $ do
            let ret = herbieOpsToHaskellOps $ fromCanonicalLispCmd (cmdout res,varmap)
            deepseq ret $ return ret
        case ret of
            Left (SomeException e) -> do
                putStrLn $ "WARNING in stabilizeMathExpr: "++show e
                return cmdin
            Right x -> return x
    let res' = res
            { cmdin  = cmdin
            , cmdout = cmdout'
            }
--     putStrLn $ "  cmdin:   "++cmdinLisp
--     putStrLn $ "  cmdout:  "++cmdout res
--     putStrLn $ "  stabilizeLisp': "++mathExpr2lisp (fromCanonicalLispCmd (cmdout res,varmap))
    return res'

-- | Given a Lisp command, return a numerically stable version.
-- It first checks if the command is in the global database;
-- if it's not, then it runs "execHerbie".
stabilizeLisp :: DbgInfo -> String -> IO (StabilizerResult String)
stabilizeLisp dbgInfo cmd = do
    dbResult <- lookupDatabase cmd
    ret <- case dbResult of
        Just x -> do
            return x
        Nothing -> do
            putStrLn "  Not found in database.  Running Herbie..."
            res <- execHerbie cmd
            insertDatabase res
            return res
    insertDatabaseDbgInfo dbgInfo ret

    -- FIXME:
    -- Herbie has a bug where it sometimes outputs a less numerically stable version.
    -- So we need to check to make sure we return the more stable output.
    return $ if errin ret > errout ret
        then ret
        else ret { errout = errin ret, cmdout = cmdin ret }

-- | Run the `herbie` command and return the result
execHerbie :: String -> IO (StabilizerResult String)
execHerbie lisp = do

    -- build the command string we will pass to Herbie
    let varstr = "("++unwords (lisp2vars lisp)++")"
        stdin = "(herbie-test "++varstr++" \"cmd\" "++lisp++") \n"

    -- Herbie can take a long time to run.
    -- Here we limit it to 2 minutes.
    --
    -- FIXME:
    -- This should be a parameter the user can pass to the plugin
    ret <- timeout 120000000 $ do

        -- launch Herbie with a fixed seed to ensure reproducible builds
        (_,stdout,stderr) <- readProcessWithExitCode
            "herbie-exec"
            [ "-r", "#(1461197085 2376054483 1553562171 1611329376 2497620867 2308122621)" ]
            stdin

        -- try to parse Herbie's output;
        -- if we can't parse it, that means Herbie had an error and we should abort gracefully
        ret <- try $ do
            let (line1:line2:line3:_) = lines stdout
            let ret = StabilizerResult
                    { errin
                        = read
                        $ drop 1
                        $ dropWhile (/=':') line1
                    , errout
                        = read
                        $ drop 1
                        $ dropWhile (/=':') line2
                    , cmdin
                        = lisp
                    , cmdout
                        = (!!2)
                        $ groupByParens
                        $ init
                        $ tail line3
                    }
            deepseq ret $ return ret

        case ret of
            Left (SomeException e) -> do
                putStrLn $ "WARNING in execHerbie: "++show e
                putStrLn $ "WARNING in execHerbie: stdin="++stdin
                putStrLn $ "WARNING in execHerbie: stdout="++stdout
                return StabilizerResult
                    { errin  = 0/0
                    , errout = 0/0
                    , cmdin  = lisp
                    , cmdout = lisp
                    }
            Right x -> return x

    case ret of
        Just x -> return x
        Nothing -> do
            putStrLn $ "WARNING: Call to Herbie timed out after 2 minutes."
            return $ StabilizerResult
                { errin  = 0/0
                , errout = 0/0
                , cmdin  = lisp
                , cmdout = lisp
                }


-- | The result of running Herbie
data StabilizerResult a = StabilizerResult
    { cmdin  :: !a
    , cmdout :: !a
    , errin  :: !Double
    , errout :: !Double
    }
    deriving (Show,Generic,NFData)

instance FromField a => FromRow (StabilizerResult a) where
    fromRow = StabilizerResult <$> field <*> field <*> field <*> field

instance ToField a => ToRow (StabilizerResult a) where
    toRow (StabilizerResult cmdin cmdout errin errout) = toRow (cmdin, cmdout, errin, errout)

-- | Returns a connection to the sqlite3 database
mkConn = do
    path <- getDataFileName "Herbie.db"
    open path

-- | Check the database to see if we already know the answer for running Herbie
--
-- FIXME:
-- When Herbie times out, NULL gets inserted into the database for errin and errout.
-- The Sqlite3 bindings don't support putting NULL into Double's as NaNs,
-- so the query below raises an exception.
-- This isn't so bad, except a nasty error message gets printed,
-- and the plugin attempts to run Herbie again (wasting a lot of time).
lookupDatabase :: String -> IO (Maybe (StabilizerResult String))
lookupDatabase cmdin = do
    ret <- try $ do
        conn <- mkConn
        res <- queryNamed
            conn
            "SELECT cmdin,cmdout,errin,errout from StabilizerResults where cmdin = :cmdin"
            [":cmdin" := cmdin]
            :: IO [StabilizerResult String]
        close conn
        return $ case res of
            [x] -> Just x
            []  -> Nothing
    case ret of
        Left (SomeException e) -> do
            putStrLn $ "WARNING in lookupDatabase: "++show e
            return Nothing
        Right x -> return x

-- | Inserts a "StabilizerResult" into the global database of commands
insertDatabase :: StabilizerResult String -> IO ()
insertDatabase res = do
    ret <- try $ do
        conn <- mkConn
        execute_ conn $ fromString $
            "CREATE TABLE IF NOT EXISTS StabilizerResults "
            ++"( id INTEGER PRIMARY KEY"
            ++", cmdin  TEXT UNIQUE NOT NULL"
            ++", cmdout TEXT        NOT NULL"
            ++", errin  DOUBLE      "
            ++", errout DOUBLE      "
            ++")"
        execute_ conn "CREATE INDEX IF NOT EXISTS StabilizerResultsIndex ON StabilizerResults(cmdin)"
        execute conn "INSERT INTO StabilizerResults (cmdin,cmdout,errin,errout) VALUES (?,?,?,?)" res
        close conn
    case ret of
        Left (SomeException e) -> putStrLn $ "WARNING in insertDatabase: "++show e
        Right _ -> return ()
    return ()


-- | This information gets stored in a separate db table for debugging purposes
data DbgInfo = DbgInfo
    { dbgComments   :: String
    , modName       :: String
    , functionName  :: String
    , functionType  :: String
    }

insertDatabaseDbgInfo :: DbgInfo -> StabilizerResult String -> IO ()
insertDatabaseDbgInfo dbgInfo res = do
    ret <- try $ do
        conn <- mkConn
        execute_ conn $ fromString $
            "CREATE TABLE IF NOT EXISTS DbgInfo "
            ++"( id INTEGER PRIMARY KEY"
            ++", resid INTEGER NOT NULL"
            ++", dbgComments TEXT"
            ++", modName TEXT"
            ++", functionName TEXT"
            ++", functionType TEXT"
            ++")"
        res <- queryNamed
            conn
            "SELECT id,cmdout from StabilizerResults where cmdin = :cmdin"
            [":cmdin" := cmdin res]
            :: IO [(Int,String)]
        execute conn "INSERT INTO DbgInfo (resid,dbgComments,modName,functionName,functionType) VALUES (?,?,?,?,?)" (fst $ head res,dbgComments dbgInfo,modName dbgInfo,functionName dbgInfo,functionType dbgInfo)
        close conn
    case ret of
        Left (SomeException e) -> putStrLn $ "WARNING in insertDatabaseDbgInfo: "++show e
        Right _ -> return ()
    return ()