fptest-0.2.1.0: src/FPRun.hs
{- |
Module : FPRun
Description : "FPRun" executes IBM floating point test cases either directly,
or by generating a Haskell script that does it.
Copyright : (c) John Pavel, 2014
IBM Corp 2004
License : BSD3
Maintainer : jrp@dial.pipex.com
Stability : alpha
Portability : portable
See
<https://www.research.ibm.com/haifa/projects/verification/fpgen/papers/ieee-test-suite-v2.pdfi
Floating-Point Test-Suite for IEEE> and
<https://www.research.ibm.com/haifa/projects/verification/fpgen/syntax.txt
Syntax of the Test Cases>
-}
module FPRun where
import FPParse
import FPTypes
import Control.Monad (msum)
import Data.Char (toUpper)
import Data.Either (rights)
import Data.Fixed (divMod')
import Data.List (intercalate)
import Data.Word (Word32, Word64)
import Numeric (floatToDigits, showHex)
import Text.Parsec.Number (hexFloat, sign)
import Text.ParserCombinators.Parsec
import Unsafe.Coerce (unsafeCoerce)
{- |
= Direct test case execution -}
{- | 'Output' represents either an error message (mainly for unimplemented tests)
or a 'RealFloat' value. -}
type ErrMsg = String
type Output a = Either ErrMsg a
{- | 'execute' runs a test case directly, returning either an error message or
the resulting 'RealFloat'('Float' or 'Double') value.
TODO:: sync with 'translate' -}
execute :: (RealFloat a, HasNaN a) => ParsedTestCase -> Output a
execute TestCase { -- format = f,
operation = op,
{- roundingMode = rm,
trappedExceptions = te, -}
inputs = is
{- output = o
outputExceptions = oe -}
} =
case op of
Add -> return $ i1 + i2
Subtract -> return $ i1 - i2
Multiply -> return $ i1 * i2
Divide -> return $ i1 / i2
FusedMultiplyAdd -> unimplemented
SquareRoot -> return $ sqrt i1
Remainder -> return r where (_, r) = i1 `divMod'` i2 -- i1 bound to Integer
RoundFloatToInteger -> return $ fromIntegral $ round i1 -- defaults to Double
ConvertFloatToFloat -> return i1 -- ?
ConvertFloatToInteger -> unimplemented
ConvertIntegerToFloat -> unimplemented
ConvertDecimalToString -> unimplemented
ConvertStringToDecimal -> unimplemented
QuietComparison -> return $ boolNum (i1 == i2)
SignallingComparison -> unimplemented
Copy -> return i1
Negate -> return $ negate i1
Abs -> return $ abs i1
CopySign -> unimplemented {- Copysign(x, y) returns x with the sign of y.
Hence, abs(x) = copysign( x, 1.0), even if x is NaN -}
Scalb -> unimplemented {- Scalb(y, N) returns y × 2N for integral values N
without computing 2N -}
Logb -> return $ log i1
NextAfter -> unimplemented
Class -> return $ fpclass i1
IsSigned -> return $ boolNum $ i1 < 0.0
IsNormal -> return $ boolNum $ not $ isDenormalized i1
IsFinite -> return $ boolNum $ not $ isInfinite i1
IsZero -> return $ boolNum $ i1 == 0.0
IsSubNormal -> return $ boolNum $ isDenormalized i1
IsInf -> return $ boolNum $ isInfinite i1
IsNaN -> return $ boolNum $ isNaN i1
IsSignalling -> unimplemented
MinNum -> return $ min i1 i2
MaxNum -> return $ max i1 i2
MinNumMag -> return $ min (abs i1) (abs i2)
MaxNumMag -> return $ max (abs i1) (abs i2)
SameQuantum -> unimplemented -- only applicable to Decimal operands
Quantize -> unimplemented -- only applicable to Decimal operands
NextUp -> unimplemented
NextDown -> unimplemented
Equivalent -> unimplemented
where
i1 = hexToFloat $ head is
i2 = hexToFloat $ head $ tail is
fpclass x
-- \| x is signalling NaN = 0.0
| isNaN x = 1.0 -- quiet NaN (x != x)
| x < 0.0 && isInfinite x = 2.0 -- -INFINITY
| x < 0.0 && not (isDenormalized x) &&
not (isNegativeZero x) = 3.0 -- negative normalized nonzero
| x < 0.0 && isDenormalized x = 4.0 -- negative denormalized
| isNegativeZero x = 5.0
| x == 0.0 = 6.0
| x > 0.0 && isDenormalized x = 7.0 -- positive denormalized
| x > 0.0 && not (isDenormalized x) = 8.0 -- positive normalized nonzero
| x > 0.0 && isInfinite x = 9.0 -- +INFINITY
| otherwise = 10.0
unimplemented :: Either ErrMsg a
unimplemented = Left $ show op ++ " " ++ show is ++ " is unimplemented"
{- |
= Test case translation -}
{- | 'translate' a 'ParsedTestCase' into a Haskell (HUnit) test (or a diagnostic
string, mainly indicating that a test is unimplemented. -}
translate :: ParsedTestCase -> Either String String
translate TestCase { format = f,
operation = op,
roundingMode = rm,
trappedExceptions = te,
inputs = is,
output = o,
outputExceptions = oe
}
| te /= [] = Left $ "trapped exceptions unimplemented " ++ display te
| otherwise =
case f of
BasicFormat Binary32 -> translate' "F"
BasicFormat Binary64 -> translate' "D"
_ -> Left $ show f ++ " is not an implemented format"
where
translate' fmt =
case op of
Add -> return $ expected ++ i1 ++ " + " ++ i2
Subtract -> return $ expected ++ i1 ++ " - " ++ i2
Multiply -> return $ expected ++ i1 ++ " * " ++ i2
Divide -> return $ expected ++ i1 ++ " / " ++ i2
FusedMultiplyAdd -> unimplemented
SquareRoot -> return $ expected ++ "sqrt " ++ i1
Remainder -> return $ expected ++ "r where (_, r) = " ++ i1 ++ " `divMod'` " ++ i2
RoundFloatToInteger -> return $ expected ++ "round " ++ i1
ConvertFloatToFloat -> unimplemented
ConvertFloatToInteger -> unimplemented
ConvertIntegerToFloat -> unimplemented
ConvertDecimalToString -> unimplemented
ConvertStringToDecimal -> unimplemented
QuietComparison -> return $ expected ++ "boolNum (" ++ i1 ++ "==" ++ i2 ++ ")"
SignallingComparison -> unimplemented
Copy -> return $ expected ++ i1
Negate -> return $ expected ++ "negate " ++ i1
Abs -> return $ expected ++ "abs " ++ i1
CopySign -> unimplemented {- Copysign(x, y) returns x with the sign of y.
Hence, abs(x) = copysign( x, 1.0), even if x is NaN -}
Scalb -> unimplemented {- Scalb(y, N) returns y × 2N for integral values N
without computing 2N -}
Logb -> return $ expected ++ "log" ++ i1
NextAfter -> unimplemented
Class -> unimplemented
IsSigned -> return $ expected ++ "boolNum (signum " ++ i1 ++ " == -1.0)"
-- isNormal(x) is true if and only if x is normal (not zero, subnormal, infinite, or NaN).
IsNormal -> return $ expected ++ "boolNum (not ( isDenormalized " ++ i1 ++
" || isInfinite " ++ i1 ++ "|| isNaN " ++ i1 ++ " || " ++ i1 ++ " == 0 ))"
-- isFinite(x) is true if and only if x is zero, subnormal or normal (not infinite or NaN).
IsFinite -> return $ expected ++ "boolNum (not ( isInfinite " ++ i1 ++ " || isNaN " ++ i1 ++ "))"
IsZero -> return $ expected ++ "boolNum (0 == " ++ i1 ++ ")"
IsSubNormal -> return $ expected ++ "boolNum ( isDenormalized " ++ i1 ++ ")"
IsInf -> return $ expected ++ "boolNum (isInfinite " ++ i1 ++ ")"
IsNaN -> return $ expected ++ "boolNum (isNaN " ++ i1 ++ ")"
IsSignalling -> unimplemented
MinNum -> return $ expected ++ "min" ++ i1 ++ i2
MaxNum -> return $ expected ++ "max" ++ i1 ++ i2
MinNumMag -> unimplemented
MaxNumMag -> unimplemented
SameQuantum -> unimplemented -- only applicable to Decimal operands
Quantize -> unimplemented -- only applicable to Decimal operands
NextUp -> unimplemented
NextDown -> unimplemented
Equivalent -> unimplemented
where
expected = ot ++ " ~==? " -- NaN ~==? NaN is True
i1 = ft $ showHexToFloat $ head is
i2 = ft $ showHexToFloat $ head $ tail is
ot = ft $ showHexToFloat o
-- show will produce "Infinity", "NaN", etc, so wrap it
showHexToFloat s
| s == "+Zero" = "0.0"
| s == "-Zero" = "-0.0"
| s == "+Inf" = "1.0/0.0"
| s == "-Inf" = "(-1.0/0.0)"
| s == "Q" = "0/0"
| s == "S" = "0/0" -- for now. Should really produce Left
| s == "#" = "-999.0" -- output is ignored, so no matter
| fmt == "F" = show (hexToFloat s :: Float)
| otherwise = show (hexToFloat s :: Double)
ft :: String -> String
ft s = "(" ++ s ++ "::" ++ fmt ++ ")"
unimplemented :: Either String String
unimplemented = Left $ show op ++ " " ++ show is ++ " is unimplemented"
-- | 'checkResult' verifies the result of running a 'ParsedTestCase' directly
checkResult :: ParsedTestCase -> String
checkResult t@TestCase { format = f,
output = o,
outputExceptions = oe
} = display t ++ ":" ++
case f of
BasicFormat Binary32 ->
checkResult' ( execute t :: Output Float )
BasicFormat Binary64 ->
checkResult' ( execute t :: Output Double )
_ -> show f ++ " is an unimplemented floating point format"
where
checkResult' e =
case e of
Left err -> err -- pass error message on
Right result -- compare result with expectation
| o == "#" -> "No output expected: " ++ show result
| floatToHex result == o -> "Success!"
| otherwise -> " got " ++ floatToHex result
-- 'translateResult' turns a 'ParsedTestCase' into an HUnit test or a diagnostic String
translateResult :: ParsedTestCase -> Either String String
translateResult t@TestCase { format = f,
operation = op,
{- roundingMode = rm,
trappedExceptions = te, -}
inputs = is,
output = o
-- outputExceptions = oe
} =
case translate t of
Left diagnostic -> Left diagnostic
Right testtext -> return $ "\" " ++ display t ++ "\" ~: (" ++ testtext ++ ")"
-- 'strcmp' provides insensitive string comparison (unused)
strcmp :: String -> String -> Bool
strcmp [] [] = True
strcmp s1 s2 = case (s1, s2) of
(s11 : ss1, s21 : ss2)
| toUpper s11 == toUpper s21 -> strcmp ss1 ss2
| otherwise -> False
_ -> False
-- | == Helper functions for hex floating point format literals.
-- | 'hexToFloat' parses a string into a 'RealFloat'.
hexToFloat :: (RealFloat a, HasNaN a) => String -> a
hexToFloat s
| s == "+Zero" = 0.0
| s == "-Zero" = -0.0
| s == "+Inf" = 1 / 0
| s == "-Inf" = negate 1.0 / 0.0
| s == "Q" = quietNaN -- 0 / 0
| s == "S" = signallingNaN
| s == "true" = 1.0
| s == "false" = 0.0
| otherwise = hexToFloat' s
{- | Converts 'RealFloat' to a hex literal string. This version right-justifies the
significand, following IBM's test suite convention. This means that, for Floats,
which use 23-bits, the hex representation is different from the standard notation
because the last bit of the significand is padded to zero, rather than the first bit. -}
floatToHex :: (RealFloat a, Show a) => a -> String
floatToHex x
| not (isIEEE x) = "Not an IEEE floating point number: " ++ show x
{- | expnt == eMax + 1 && explicitBits == [] && x > 0 = "+Inf"
expnt == eMax + 1 && explicitBits == [] && x < 0 = "-Inf" -}
| isNaN x = "Q" -- Haskell seems to use only quiet NaNs
| isInfinite x && x > 0 = "+Inf"
| isInfinite x = "-Inf"
| isNegativeZero x = "-Zero"
| x == 0.0 = "+Zero"
| x > 0 = '+' : floatToHex' x
| x < 0 = '-' : floatToHex' (-x)
| otherwise = error "Invalid argument to floatToHex : " ++ show x
where
floatToHex' y
| expnt > eMax + 1 = "Exponent (" ++ show expnt ++ ") > " ++ show eMax
{- For the subnormal case, since the padding is on the left hand side,
the exponent needs to be adjusted -}
| expnt <= eMin = "0." ++ bitsToHex f' ++
"P" ++ exponentSign (expnt + (eMin - expnt)) ++
show (expnt + (eMin - expnt)) -- Denormalized case
| otherwise = "1." ++ bitsToHex f ++ "P" ++
exponentSign (expnt - 1) ++ show (expnt - 1)
where
{- Translate y into a list of bits; expnt is set so that the first
bit is one (will fail if y is 0) -}
(implicitBit : explicitBits, expnt) = floatToDigits 2 y
{- Normal case drops the implicit bit
Pad out with an initial bit / Float, not needed for Double, the explicit
bits follow, and -}
f = zeroPadding (paddedDigits - explicitDigits) ++
explicitBits ++
zeroPadding (paddedDigits - (paddedDigits - explicitDigits + length explicitBits))
{- Subnormal case retains the implicit bit
Conventionally literals are padded LEFT justified.
The IBM test suite right justifies them so we need to add more initial
padding -}
f' = zeroPadding (eMin - expnt + 1) ++
(implicitBit : (explicitBits ++
zeroPadding (paddedDigits - (eMin - expnt + 1 + 1 + length explicitBits))))
{- Conventionally literals are padded LEFT justified.
Normal case drops the implicit bit
f = explicitBits ++ zeroPadding (paddedDigits - length explicitBits)
Subnormal case retains the implicit bit
f' = zeroPadding (eMin - expnt) ++
(implicitBit : (explicitBits ++
zeroPadding (paddedDigits - (eMin - expnt + 1 + length explicitBits)))) -}
zeroPadding n = replicate n 0
{- | 'floatRange' returns (-125, 128) for Float and (-1021,1024)
for Double, which is one more than the conventional (eMin, eMax) -}
eMax = e - 1 -- 127 Float / 2013 Double
where
(_, e) = floatRange y
eMin = 1 - eMax {- eMin is required to be 1 - 'eMax' by IEEE754,
-126 Float / -1023 Double -}
digits = floatDigits y -- 24 Float / 53 Double
explicitDigits = digits - 1 {- 23 Float / 52 Double
Always pad to full precision in IBM representation; the standard drops
trailing 0s -}
paddedDigits -- 24 Float / 52 Double
| explicitDigits `rem` 4 == 0 = explicitDigits
| otherwise = ((explicitDigits `div` 4) + 1) * 4
-- Right justified version
bitsToHex :: [Int] -> String
bitsToHex s = map toUpper (bitsToHex' s)
where
bitsToHex' (a : b : c : d : es) =
showHex (8 * a + 4 * b + 2 * c + d) (bitsToHex' es)
bitsToHex' [a, b, c] = showHex (4 * a + 2 * b + c ) ""
bitsToHex' [a, b] = showHex (2 * a + b ) ""
bitsToHex' [a] = showHex a ""
bitsToHex' [] = ""
-- bitsToHex' e = error $ show e ++ " is (part of) an invalid argument to bitsToHex"
exponentSign s -- add a + sign for +ve exponents
| s < 0 = ""
| otherwise = "+"
{- |
= Helper functions -}
{- Some example code that allows NaNs to signal, from
<http://stackoverflow.com/questions/21344139/ieee-floating-point-signalling-nan-snan-in-haskell>
Doesn't do us much good here, as Haskell does not produce signalling NaNs at all -}
{- | 'HasNaN' gives two values (signalling and quiet NaN.
At present, although the values can be set, Haskell doesn't generate signalling NaNs -}
class (RealFloat a, Show a) => HasNaN a where
signallingNaN :: a
quietNaN :: a
hexToFloat' :: String -> a
hexToFloat' s =
case parse hexToFloatParser "hexToFloat" s of
Left err -> error ("Invalid hex format floating point number '" ++ s ++ "' :" ++ show err)
Right f -> f
hexToFloatParser :: RealFloat a => GenParser Char st a
hexToFloatParser = do
sn <- sign
h <- hexFloat False
eof
return $ sn $ case h of
Left i -> fromInteger i
Right f -> f
instance HasNaN Double where
signallingNaN = unsafeCoerce (0x7ff4000000000000 :: Word64)
quietNaN = unsafeCoerce (0x7ff8000000000000 :: Word64)
instance HasNaN Float where
signallingNaN = unsafeCoerce (0x7fa00000 :: Word32)
quietNaN = unsafeCoerce (0x7fc00000 :: Word32)
{- This IBM subnormal are half what the hex floating point literal would be for
floating point subnormals -}
hexToFloat' s
-- 2**(-127) is the largest denormalised float
| abs d <= 2 ** (-127) = realToFrac $ scaleFloat 1 d {- multiply by 2
multiply the fractional part by 2 because IBM format is right aligned -}
| otherwise = signum (realToFrac d) *
scaleFloat expnt (val (1 : tail (explicitBits ++ zeroPadding 24)))
where
(_ : explicitBits, expnt) = floatToDigits 2 (abs d)
val :: [Int] -> Float
val (h : t) = (fromIntegral h + val t) / 2
val [] = 0
zeroPadding n = replicate n 0
d = (case parse hexToFloatParser "hexToFloat" s of
Left err -> error ("Invalid hex format floating point number '" ++ s ++ "' :" ++ show err)
Right f -> f) :: Double
-- | 'boolNum' is a helper function that simply translates True / False to 1.0/0.0
boolNum :: RealFloat f => Bool -> f
boolNum True = 1.0
boolNum False = 0.0
{- |
= Running the test cases in a list of IBM floating point test specification files -}
{- | 'evalTestFiles' takes a list of IBM test case files (.fptest) and executes
them directly (interprets them).
TODO:: find a cleaner way of doing this without using do, etc
-}
evalTestFiles :: [FilePath] -> IO String
evalTestFiles = msum . map evalTestFile
-- | 'evalTestFile evaluate the test cases in the given file
evalTestFile :: FilePath -> IO String
evalTestFile filename = do
parsedTestCases <- parseFromFile testCaseFile filename
return $ "Running file " ++ filename ++ ":\n" ++
case parsedTestCases of
Left err -> show err ++ "\nTest case file " ++ filename ++ " failed to parse"
Right result -> unlines (map checkResult result)
{- | 'translateTestFiles' takes a list of IBM test cases and translates their contents
into a set of Haskell (HUnit) tests that can be run separately. -}
translateTestFiles :: [FilePath] -> IO String
translateTestFiles filenames = do
results <- mapM translateTestFile filenames
return $ preface ++
"\ntests = \"fptest\" ~: [\n " ++
concat results ++
" ]\n"
{- | 'translateTestFile' translates a files of IBM test cases into an
HUnit test group -}
translateTestFile :: FilePath -> IO String
translateTestFile filename = do
parsedTestCases <- parseFromFile testCaseFile filename
return $ case parsedTestCases of
Left err -> -- unsuccessful parse
show err ++ "\nTest case file " ++ filename ++ " failed to parse"
Right results -> -- successful parse
show filename ++ " ~: [\n " ++
{- Take only the rights, ignoring the lefts, which contain error messages,
indicating that a test is unimplemented, eg. -}
intercalate ",\n " (rights (map translateResult results)) ++ "]"
{- | 'preface' outputs some boilerplate code for placing at the head of
of a file of test case translations in Haskell (HUnit) format -}
preface :: String
preface = unlines [
"module Main where\n\n",
"import Test.HUnit\n",
"type D = Double",
"type F = Float\n",
"boolNum :: RealFloat a => Bool -> a",
"boolNum x",
" | x = 1",
" | otherwise = 0\n",
"-- Version that treats NaN == NaN",
"assertEqual' :: (Eq a, Show a, RealFloat a) => String -> a -> a -> Assertion",
"assertEqual' preface expected actual =",
" if (isNaN actual && isNaN expected) || (actual == expected)",
" then return () else assertFailure msg",
" where msg = (if null preface then \"\" else preface ++ \"\\n\") ++",
" \"expected: \" ++ show expected ++ \"\\n but got: \" ++ show actual",
"",
"infix 1 @==?, @?==, ~==?, ~?==",
"",
"(@==?) :: (Eq a, Show a, RealFloat a) => a -> a -> Assertion",
"expected @==? actual = assertEqual' \"\" expected actual",
"",
"(@?==) :: (Eq a, Show a, RealFloat a) => a -> a -> Assertion",
"actual @?== expected = assertEqual' \"\" expected actual",
"",
"(~==?) :: (Eq a, Show a, RealFloat a) => a -> a -> Test",
"expected ~==? actual = TestCase (expected @==? actual)",
"",
"(~?==) :: (Eq a, Show a, RealFloat a) => a -> a -> Test",
"actual ~?== expected = TestCase (actual @?== expected)",
"",
"main :: IO Counts",
"main = runTestTT tests"
]