crackNum-3.30: src/CrackNum/Output.hs
---------------------------------------------------------------------------
-- |
-- Module : CrackNum.Output
-- Copyright : (c) Levent Erkok
-- License : BSD3
-- Maintainer : erkokl@gmail.com
-- Stability : experimental
--
-- Massaging the output, and laying out the formats that have no IEEE look-alike
-----------------------------------------------------------------------------
{-# LANGUAGE DataKinds #-}
{-# OPTIONS_GHC -Wall -Werror #-}
module CrackNum.Output(
retype, printAs, modOut, isClassification, dropNaNUniquenessNote, canonicalNaN,
ExtraE3M4(..), toD, inBases, fp4e0m3Layout, e8m0Bias, e8m0Value, e8m0Layout
) where
import Data.Char (intToDigit, toUpper)
import Data.List (intercalate, isInfixOf)
import Numeric (showIntAtBase)
import Data.SBV
import qualified Data.SBV as SBV
import CrackNum.Types
-- The non-IEEE formats are all modeled by an IEEE look-alike, so SBV displays the look-alike's
-- type name. Rewrite it to the format the user actually asked for.
retype :: FP -> SatResult -> String
retype fmt res@(SatResult (Satisfiable{})) = intercalate "\n" $ map fixType (lines (show res))
where fixType :: String -> String
fixType s
| any (`isInfixOf` s) ["ENCODED", "DECODED"]
= takeWhile (/= ':') s ++ ":: " ++ show fmt
| True
= s
retype _ res = show res
-- Print a model for one of the non-IEEE formats: the look-alike does all the work,
-- we merely fix the type name it prints.
printAs :: FP -> SatResult -> IO ()
printAs fmt = putStrLn . retype fmt
-- Handle modified output. The bit-layout of these values is precisely what the IEEE look-alike
-- says it is, so we take that part verbatim; but the value itself, and everything that is derived
-- from it, has to come from the double we actually mean. Note that this works for encoding just
-- as well as it does for decoding; the only difference is the label SBV uses.
modOut :: Bool -> Bool -> Double -> FP -> SatResult -> IO ()
modOut debug sign val fmt ieeeResult = do
let sval :: Double
sval | sign = -val
| True = val
modifiedResult = SBV.crack debug (literal sval :: SDouble)
fixVal l = case [tag | tag <- ["ENCODED", "DECODED"], tag `isInfixOf` l] of
tag : _ -> " " ++ tag ++ " = " ++ show sval ++ " :: " ++ show fmt
[] -> l
-- Print from the original result upto Classification, rest from the modified result
mapM_ (putStrLn . fixVal) $ takeWhile (not . isClassification) (lines (show ieeeResult))
mapM_ putStrLn $ dropWhile (not . isClassification) (lines modifiedResult)
-- | The line SBV's cracker prints the classification on. Everything from here down
-- describes the value itself rather than its layout, which is the split the formats
-- that deviate from IEEE need: they take the layout from the look-alike (or lay it
-- out by hand) and the rest from the value they actually mean.
isClassification :: String -> Bool
isClassification = ("Classification:" `isInfixOf`)
-- | SBV notes that a NaN's representation is not unique. That holds for IEEE formats,
-- but not for the ones here that have exactly one NaN pattern (E4M3 and E8M0), so drop
-- the note for those rather than claim an ambiguity the format does not have.
dropNaNUniquenessNote :: [String] -> [String]
dropNaNUniquenessNote = filter (not . ("Representation for NaN's is not unique" `isInfixOf`))
-- | The canonical quiet-NaN pattern for a float with @eb@ exponent bits and @sb@
-- significand bits (including the implicit one): sign 0, all-ones exponent, and only
-- the leading stored significand bit set. For single-precision this is 0x7FC00000.
canonicalNaN :: Int -> Int -> Integer
canonicalNaN eb sb = (2 ^ eb - 1) * 2 ^ (sb - 1) + 2 ^ (sb - 2)
-- Bool is True if negative
data ExtraE3M4 = E240 Bool -- Not really extra but can be mapped to
| E256 Bool
| E288 Bool
| E320 Bool
| E352 Bool
| E384 Bool
| E416 Bool
| E448 Bool
deriving Show
toD :: ExtraE3M4 -> Double
toD (E240 isNeg) = if isNeg then -240 else 240
toD (E256 isNeg) = if isNeg then -256 else 256
toD (E288 isNeg) = if isNeg then -288 else 288
toD (E320 isNeg) = if isNeg then -320 else 320
toD (E352 isNeg) = if isNeg then -352 else 352
toD (E384 isNeg) = if isNeg then -384 else 384
toD (E416 isNeg) = if isNeg then -416 else 416
toD (E448 isNeg) = if isNeg then -448 else 448
neg4 :: Bool -> (String, String, String, String) -> (String, String, String, String)
neg4 True (a, b, c, d) = ('-':a, '-':b, '-':c, '-':d)
neg4 False (a, b, c, d) = (a, b, c, d)
-- binary, octal, decimal, hex
inBases :: ExtraE3M4 -> (String, String, String, String)
inBases (E240 isNeg) = neg4 isNeg ("0b1.111p+7", "0o3.6p+6", "240.0", "0xFp+4")
inBases (E256 isNeg) = neg4 isNeg ("0b1p+8", "0o4p+6", "256.0", "0x1p+8")
inBases (E288 isNeg) = neg4 isNeg ("0b1.001p+8", "0o4.4p+6", "288.0", "0x1.2p+8")
inBases (E320 isNeg) = neg4 isNeg ("0b1.01p+8", "0o5p+6", "320.0", "0x1.4p+8")
inBases (E352 isNeg) = neg4 isNeg ("0b1.011p+8", "0o5.4p+6", "352.0", "0x1.6p+8")
inBases (E384 isNeg) = neg4 isNeg ("0b1.1p+8", "0o6p+6", "384.0", "0x1.8p+8")
inBases (E416 isNeg) = neg4 isNeg ("0b1.101p+8", "0o6.4p+6", "416.0", "0x1.Ap+8")
inBases (E448 isNeg) = neg4 isNeg ("0b1.11p+8", "0o7p+6", "448.0", "0x1.Cp+8")
-- FP4E0M3 is a 4-bit sign-magnitude integer: a sign bit and a 3-bit magnitude, covering
-- -7 to 7, with both a positive and a negative zero. Having no exponent at all, it has no
-- IEEE look-alike we could lean on, so we lay the bits out by hand; the shape follows what
-- crackNum prints for the plain integer formats, which is what this format really is.
fp4e0m3Layout :: String -> Bool -> Int -> [String]
fp4e0m3Layout tag isNeg mag =
[ "Satisfiable. Model:"
, " " ++ tag ++ " = " ++ sign ++ show mag ++ " :: " ++ show FP4E0M3
, " 3 210"
, " S -M-"
, " Binary layout: " ++ (if isNeg then '1' else '0') : ' ' : pad 3 (inBase 2 mag)
, " Hex layout: " ++ map toUpper (inBase 16 ((if isNeg then 8 else 0) + mag))
, " Type: 4-bit sign-magnitude integer"
, " Sign: " ++ (if isNeg then "Negative" else "Positive")
, " Binary: " ++ sign ++ "0b" ++ inBase 2 mag
, " Octal: " ++ sign ++ "0o" ++ inBase 8 mag
, " Decimal: " ++ sign ++ show mag
, " Hex: " ++ sign ++ "0x" ++ inBase 16 mag
]
where sign = if isNeg then "-" else ""
inBase b v = showIntAtBase b intToDigit v ""
pad n s = replicate (n - length s) '0' ++ s
-- | E8M0 is the OCP Microscaling (MX) scale format: the value that scales a block of
-- MXFP8/MXFP6/MXFP4 elements. All 8 bits are exponent -- there is no sign bit and no
-- significand at all -- so every value is the power of two 2^(E-127), and 0xFF is its
-- one and only NaN. Having no significand, it has no zero and no subnormals either:
-- with nothing for the E=0 encoding to mean, it simply denotes 2^-127.
e8m0Bias :: Int
e8m0Bias = 127
-- | The value a stored E8M0 exponent denotes. All 254 finite values are exactly
-- representable as a Double, since 2^(+/-127) is nowhere near its range limits; note
-- that 'encodeFloat' builds them exactly, which @2 **@ would not be guaranteed to do.
e8m0Value :: Int -> Double
e8m0Value 255 = 0/0
e8m0Value e = encodeFloat 1 (e - e8m0Bias)
-- | Lay out an E8M0 value. With no sign and no significand there is no IEEE look-alike
-- to lean on, so the layout is built by hand, following the shape crackNum prints for
-- the other formats. Everything from the classification down describes the value rather
-- than its layout, so that part comes from cracking the equivalent Double -- the same
-- division of labor 'modOut' uses for the E4M3 and FP4 deviations.
e8m0Layout :: Bool -> String -> Int -> [String]
e8m0Layout debug tag stored =
[ "Satisfiable. Model:"
, " " ++ tag ++ " = " ++ show v ++ " :: " ++ show E8M0
, " 76543210"
, " ---E8---"
, " Binary layout: " ++ pad 8 (inBase 2 stored)
, " Hex layout: " ++ map toUpper (pad 2 (inBase 16 stored))
, " Precision: 8 exponent bits, no significand"
-- NB. There is no sign bit: bit 7 is the exponent's MSB. We print the line anyway,
-- so the block keeps the same shape as every other format, but say outright that
-- it can never read anything else.
, " Sign: Positive (always)"
, " Exponent: " ++ show (stored - e8m0Bias) ++ " (Stored: " ++ show stored ++ ", Bias: " ++ show e8m0Bias ++ ")"
]
++ dropNaNUniquenessNote (dropWhile (not . isClassification) (lines (SBV.crack debug (literal v :: SDouble))))
where v = e8m0Value stored
inBase b x = showIntAtBase b intToDigit x ""
pad n x = replicate (n - length x) '0' ++ x