packages feed

unwitch (empty) → 1.0.0

raw patch · 57 files changed

+4712/−0 lines, 57 filesdep +basedep +bytestringdep +ghc-bignum

Dependencies added: base, bytestring, ghc-bignum, hspec, hspec-core, text, unwitch

Files

+ Changelog.md view
@@ -0,0 +1,11 @@+# Change log for unwitch project++## Version 0.1.0 ++Implement some of Double.+Write description++## Version 0.0.0 ++import [template](https://github.com/jappeace/haskell-template-project).+
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2021 Jappie Klooster++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ Readme.md view
@@ -0,0 +1,99 @@+[![https://jappieklooster.nl](https://img.shields.io/badge/blog-jappieklooster.nl-lightgrey)](https://jappieklooster.nl/tag/haskell.html)+[![Jappiejappie](https://img.shields.io/badge/twitch.tv-jappiejappie-purple?logo=twitch)](https://www.twitch.tv/jappiejappie)+[![Jappiejappie](https://img.shields.io/badge/discord-jappiejappie-black?logo=discord)](https://discord.gg/Hp4agqy)++> Three hundred and twenty years have passed since the coven sank in the dark. ++Removes the magic from witch.+This provides safe conversions like witch does.+But it [doesn't](https://jappie.me/death-to-type-classes.html) use [type classes](https://www.haskellforall.com/2012/05/scrap-your-type-classes.html)+or exceptions.+This has a couple of advantages:++1. No need to use type application for function selection.+2. Functions get names that describe what they do.+   This allows ctags to work as well.+3. No trouble with orphans.+4. Custom errors instead of the prelude based ones allow client+   code to recover with typesafety even on partial conversions.+   +## Usage++The idea is to select some module *from* which type you want to convert.+So if you have a double you do:++```haskell+import qualified Unwitch.Convert.Double as Double++```+Next you simply use the functions within the module+to convert to something you want, for example:++```haskell+spec = do+  Double.toFloat 5.6 `shouldBe` 5.6+```++Although dubious since float holds less information+then double,+this can never fail in the current implementation.+But there are situation where conversion can fail,+such as when converting to an integer:+```haskell+spec = do+  Double.toInteger 5.0 `shouldBe` Right 5+  Double.toInteger 5.6 `shouldBe` Left $ RationalConversion $ DenomNotOne (5 % 6)+```++In these situation you'll `Right` for success, or `Left`+for an error.+The library will attempt to give you as much information+as possible,+in this case the denominator wasn't 1 so the rational+conversion failed.++## Comparison ++### Witch++Type applications are unnecessary because there is no polymorphism.+Since there are no polymorphic types we don't need to distinct+between conversions that fail and conversions that don't,+This is encoded in types.+Furthermore on failure, if the client can figure out how to go on with a+Rational, they can.+In witch this information gets squashed, we happily provide it.+It's not an exception like witch implied.+++## Hacking+### Tools+Enter the nix shell.+```+nix-shell+```+You can checkout the makefile to see what's available:+```+cat makefile+```++### Running+```+make run+```++### Fast filewatch which runs tests+```+make ghcid+```++## Design decisions+++ Modules names indicate where you're converting *from*.++ Errors should be packed with information to allow recovery++ Only restricted to primitives in base.+  The original witch had support for Map, and Text.+  I feel this is out of scope.++https://serokell.io/blog/introduction-to-template-haskell+  
+ src/Unwitch.hs view
@@ -0,0 +1,25 @@+-- just used to load ghci+module Unwitch () where++import Unwitch.Errors ()+import Unwitch.Constant()+import Unwitch.Convert.ByteString ()+import Unwitch.Convert.Double ()+import Unwitch.Convert.Float ()+import Unwitch.Convert.Int ()+import Unwitch.Convert.Int8 ()+import Unwitch.Convert.Int16 ()+import Unwitch.Convert.Int32 ()+import Unwitch.Convert.Int64 ()+import Unwitch.Convert.Integer ()+import Unwitch.Convert.LazyByteString ()+import Unwitch.Convert.LazyText ()+import Unwitch.Convert.Natural ()+import Unwitch.Convert.Ratio ()+import Unwitch.Convert.ShortByteString ()+import Unwitch.Convert.Text ()+import Unwitch.Convert.Word ()+import Unwitch.Convert.Word8 ()+import Unwitch.Convert.Word16 ()+import Unwitch.Convert.Word32 ()+import Unwitch.Convert.Word64 ()
+ src/Unwitch/Constant.hs view
@@ -0,0 +1,15 @@+module Unwitch.Constant+  ( maxIntegralRepDouble+  , maxIntegralRepFloat+  )+where++-- | The maximum integral value that can be unambiguously represented as a+-- 'Double'. Equal to 9,007,199,254,740,991 (2^53 - 1).+maxIntegralRepDouble :: Num a => a+maxIntegralRepDouble = 9007199254740991++-- | The maximum integral value that can be unambiguously represented as a+-- 'Float'. Equal to 16,777,215 (2^24 - 1).+maxIntegralRepFloat :: Num a => a+maxIntegralRepFloat = 16777215
+ src/Unwitch/Convert/ByteString.hs view
@@ -0,0 +1,55 @@+module Unwitch.Convert.ByteString+  ( toLazyByteString+  , toShortByteString+  , toWord8s+  , fromWord8s+  , toTextLatin1+  , toTextUtf8+  , toTextUtf16LE+  , toTextUtf16BE+  , toTextUtf32LE+  , toTextUtf32BE+  )+where++import Data.ByteString (ByteString)+import Data.ByteString qualified as BS+import Data.ByteString.Lazy qualified as LBS+import Data.ByteString.Short qualified as SBS+import Data.ByteString.Short (ShortByteString)+import Data.Text (Text)+import Data.Text.Encoding qualified as TE+import Data.Text.Encoding.Error (UnicodeException)+import Data.Word (Word8)+import Unwitch.TryEvaluate (tryEvaluate)++toLazyByteString :: ByteString -> LBS.ByteString+toLazyByteString = LBS.fromStrict++toShortByteString :: ByteString -> ShortByteString+toShortByteString = SBS.toShort++toWord8s :: ByteString -> [Word8]+toWord8s = BS.unpack++fromWord8s :: [Word8] -> ByteString+fromWord8s = BS.pack++toTextLatin1 :: ByteString -> Text+toTextLatin1 = TE.decodeLatin1++toTextUtf8 :: ByteString -> Either UnicodeException Text+toTextUtf8 = TE.decodeUtf8'++toTextUtf16LE :: ByteString -> Either UnicodeException Text+toTextUtf16LE = tryEvaluate . TE.decodeUtf16LE++toTextUtf16BE :: ByteString -> Either UnicodeException Text+toTextUtf16BE = tryEvaluate . TE.decodeUtf16BE++toTextUtf32LE :: ByteString -> Either UnicodeException Text+toTextUtf32LE = tryEvaluate . TE.decodeUtf32LE++toTextUtf32BE :: ByteString -> Either UnicodeException Text+toTextUtf32BE = tryEvaluate . TE.decodeUtf32BE+
+ src/Unwitch/Convert/Char.hs view
@@ -0,0 +1,61 @@+module Unwitch.Convert.Char+  ( toInt+  , toWord+  , fromInt+  , fromWord+  , fromInt#+  , fromWord#+  )+where++import Data.Char (ord, chr)+import GHC.Exts (Int(..), Word(..), Char(..), chr#,+                 word2Int#, (>=#), (<=#), leWord#, gtWord#)++-- | Converts a Char to its Unicode codepoint as Int. Infallible.+toInt :: Char -> Int+toInt = ord++-- | Converts a Char to its Unicode codepoint as Word. Infallible.+toWord :: Char -> Word+toWord = fromIntegral . ord++-- | Converts an Int to a Char if it is a valid Unicode codepoint.+-- Valid range: 0..0xD7FF and 0xE000..0x10FFFF (excludes surrogates).+fromInt :: Int -> Maybe Char+fromInt i = if isValidCodepoint (fromIntegral i)+  then Just $ chr i+  else Nothing++-- | Unboxed variant of 'fromInt'. Checks valid Unicode codepoint range.+fromInt# :: Int -> (# Char | (# #) #)+fromInt# (I# i#) = case i# >=# 0# of+  1# -> case i# <=# 0xD7FF# of+    1# -> (# C# (chr# i#) | #)+    _  -> case i# >=# 0xE000# of+      1# -> case i# <=# 0x10FFFF# of+        1# -> (# C# (chr# i#) | #)+        _  -> (# | (# #) #)+      _  -> (# | (# #) #)+  _  -> (# | (# #) #)++-- | Converts a Word to a Char if it is a valid Unicode codepoint.+-- Valid range: 0..0xD7FF and 0xE000..0x10FFFF (excludes surrogates).+fromWord :: Word -> Maybe Char+fromWord w = if isValidCodepoint (fromIntegral w)+  then Just $ chr (fromIntegral w)+  else Nothing++-- | Unboxed variant of 'fromWord'. Checks valid Unicode codepoint range.+fromWord# :: Word -> (# Char | (# #) #)+fromWord# (W# w#) = case leWord# w# 0xD7FF## of+  1# -> (# C# (chr# (word2Int# w#)) | #)+  _  -> case gtWord# w# 0xDFFF## of+    1# -> case leWord# w# 0x10FFFF## of+      1# -> (# C# (chr# (word2Int# w#)) | #)+      _  -> (# | (# #) #)+    _  -> (# | (# #) #)++isValidCodepoint :: Integer -> Bool+isValidCodepoint cp =+  (cp >= 0 && cp <= 0xD7FF) || (cp >= 0xE000 && cp <= 0x10FFFF)
+ src/Unwitch/Convert/Complex.hs view
@@ -0,0 +1,18 @@+module Unwitch.Convert.Complex+  ( fromReal+  , toReal+  )+where++import Data.Complex (Complex((:+)), imagPart, realPart)++-- | Wraps a real number as a complex number with zero imaginary part.+fromReal :: (Num a) => a -> Complex a+fromReal x = x :+ 0++-- | Extracts the real part if the imaginary part is zero.+toReal :: (Eq a, Num a) => Complex a -> Maybe a+toReal c = if imagPart c == 0+  then Just $ realPart c+  else Nothing+
+ src/Unwitch/Convert/Double.hs view
@@ -0,0 +1,110 @@+module Unwitch.Convert.Double+  ( toFloat+  , toRational+  , toInteger+  , toInt8+  , toInt16+  , toInt32+  , toInt64+  , toInt+  , toWord8+  , toWord16+  , toWord32+  , toWord64+  , toWord+  , toNatural+  , ViaIntegerErrors(..)+  , IntegerErrors(..)+  , RationalErrors(..)+  )+where++import           Data.Bifunctor(first)+import           Unwitch.Constant+import qualified GHC.Float as F+import           Unwitch.Convert.Ratio(unwrapIfDenominatorOne)+import qualified Prelude+import           Unwitch.Errors+import           Prelude hiding (toRational, toInteger)+import qualified Unwitch.Convert.Integer as Integer+import Data.Word+import Data.Int+import Numeric.Natural (Natural)++-- loses precision?!+toFloat :: Double -> Float+toFloat = F.double2Float++data IntegerErrors = IntegerFlow Integer Overflows+                   | RationalConversion RationalErrors+                   | DenomNotOne Rational+  deriving (Show, Eq)++data ViaIntegerErrors = MkInteger IntegerErrors+                      | BitConversionFailed Integer+  deriving (Show, Eq)++toInt8 :: Double -> Either ViaIntegerErrors Int8+toInt8 = toViaInteger Integer.toInt8++toInt16 :: Double -> Either ViaIntegerErrors Int16+toInt16 = toViaInteger Integer.toInt16++toInt32 :: Double -> Either ViaIntegerErrors Int32+toInt32 = toViaInteger Integer.toInt32++toInt64 :: Double -> Either ViaIntegerErrors Int64+toInt64 = toViaInteger Integer.toInt64++toInt :: Double -> Either ViaIntegerErrors Int+toInt = toViaInteger Integer.toInt++toWord8 :: Double -> Either ViaIntegerErrors Word8+toWord8 = toViaInteger Integer.toWord8++toWord16 :: Double -> Either ViaIntegerErrors Word16+toWord16 = toViaInteger Integer.toWord16++toWord32 :: Double -> Either ViaIntegerErrors Word32+toWord32 = toViaInteger Integer.toWord32++toWord64 :: Double -> Either ViaIntegerErrors Word64+toWord64 = toViaInteger Integer.toWord64++toWord :: Double -> Either ViaIntegerErrors Word+toWord = toViaInteger Integer.toWord++toNatural :: Double -> Either ViaIntegerErrors Natural+toNatural double = do+  integer <- first MkInteger $ toInteger double+  case Integer.toNatural integer of+    Left err -> Left $ MkInteger $ IntegerFlow integer err+    Right n -> Right n++toViaInteger :: (Integer -> Maybe a) -> Double -> Either ViaIntegerErrors a+toViaInteger fun x = do+  integer <- first MkInteger $ toInteger x+  maybe (Left $ BitConversionFailed integer) Right $ fun integer++toInteger :: Double -> Either IntegerErrors Integer+toInteger double = do+  rational <- first RationalConversion $ toRational double+  integer <- maybe (Left $ DenomNotOne rational) Right $ unwrapIfDenominatorOne rational+  if+    | integer < -maxIntegralRepDouble -> Left $ IntegerFlow integer Underflow+    | integer > maxIntegralRepDouble -> Left $ IntegerFlow integer Overflow+    | otherwise -> Right integer+++data RationalErrors = IsNan+                    | IsInf Overflows+  deriving (Show, Eq)++toRational :: Double -> Either RationalErrors Rational+toRational double = if+  | isNaN double      -> Left IsNan+  | isInfinite double -> if+      | double > 0 -> Left $ IsInf Overflow+      | otherwise  -> Left $ IsInf Underflow+  | True              -> Right $ Prelude.toRational double+
+ src/Unwitch/Convert/Fixed.hs view
@@ -0,0 +1,40 @@+module Unwitch.Convert.Fixed+  ( fromInteger+  , toInteger+  , toRational+  , toFixed+  )+where++import Data.Fixed (Fixed, HasResolution)+import Data.Ratio (denominator, numerator)+import qualified Prelude+import Prelude hiding (fromInteger, toInteger, toRational)++-- | Converts an Integer to a Fixed value. Infallible.+fromInteger :: (HasResolution a) => Integer -> Fixed a+fromInteger = Prelude.fromInteger++-- | Converts a Fixed value to Integer, succeeding only if there is+-- no fractional part (i.e. the value is a whole number).+toInteger :: (HasResolution a) => Fixed a -> Maybe Integer+toInteger fixed =+  let r = Prelude.toRational fixed+  in if denominator r == 1+     then Just $ numerator r+     else Nothing++-- | Converts a Fixed value to Rational. Infallible, exact.+toRational :: (HasResolution a) => Fixed a -> Rational+toRational = Prelude.toRational++-- | Converts between Fixed types with potentially different resolutions.+-- Succeeds only if the value can be exactly represented in the target resolution.+toFixed :: (HasResolution a, HasResolution b) => Fixed a -> Maybe (Fixed b)+toFixed source =+  let r = Prelude.toRational source+      target = Prelude.fromRational r+  in if Prelude.toRational target == r+     then Just target+     else Nothing+
+ src/Unwitch/Convert/Float.hs view
@@ -0,0 +1,113 @@+module Unwitch.Convert.Float+  ( toDouble+  , toRational+  , toInteger+  , toInt8+  , toInt16+  , toInt32+  , toInt64+  , toInt+  , toWord8+  , toWord16+  , toWord32+  , toWord64+  , toWord+  , toNatural+  , ViaIntegerErrors(..)+  , IntegerErrors(..)+  , RationalErrors(..)+  )+where++import           Data.Bifunctor(first)+import           Unwitch.Constant+import qualified GHC.Float as F+import           Unwitch.Convert.Ratio(unwrapIfDenominatorOne)+import qualified Prelude+import           Unwitch.Errors+import           Prelude hiding (toRational, toInteger)+import qualified Unwitch.Convert.Integer as Integer+import Data.Word+import Data.Int+import Numeric.Natural (Natural)++toDouble :: Float -> Double+toDouble = F.float2Double++data IntegerErrors = IntegerFlow Integer Overflows+                   | RationalConversion RationalErrors+                   | DenomNotOne Rational+  deriving (Show, Eq)++data ViaIntegerErrors = MkInteger IntegerErrors+                      | BitConversionFailed Integer+  deriving (Show, Eq)++toInt8 :: Float -> Either ViaIntegerErrors Int8+toInt8 = toViaInteger Integer.toInt8+++toInt16 :: Float -> Either ViaIntegerErrors Int16+toInt16 = toViaInteger Integer.toInt16+++toInt32 :: Float -> Either ViaIntegerErrors Int32+toInt32 = toViaInteger Integer.toInt32+++toInt64 :: Float -> Either ViaIntegerErrors Int64+toInt64 = toViaInteger Integer.toInt64+++toInt :: Float -> Either ViaIntegerErrors Int+toInt = toViaInteger Integer.toInt+++toWord8 :: Float -> Either ViaIntegerErrors Word8+toWord8 = toViaInteger Integer.toWord8+++toWord16 :: Float -> Either ViaIntegerErrors Word16+toWord16 = toViaInteger Integer.toWord16++toWord32 :: Float -> Either ViaIntegerErrors Word32+toWord32 = toViaInteger Integer.toWord32++toWord64 :: Float -> Either ViaIntegerErrors Word64+toWord64 = toViaInteger Integer.toWord64++toWord :: Float -> Either ViaIntegerErrors Word+toWord = toViaInteger Integer.toWord++toNatural :: Float -> Either ViaIntegerErrors Natural+toNatural float = do+  integer <- first MkInteger $ toInteger float+  case Integer.toNatural integer of+    Left err -> Left $ MkInteger $ IntegerFlow integer err+    Right n -> Right n++toViaInteger :: (Integer -> Maybe a) -> Float -> Either ViaIntegerErrors a+toViaInteger fun x = do+  integer <- first MkInteger $ toInteger x+  maybe (Left $ BitConversionFailed integer) Right $ fun integer++toInteger :: Float -> Either IntegerErrors Integer+toInteger float = do+  rational <- first RationalConversion $ toRational float+  integer <- maybe (Left $ DenomNotOne rational) Right $ unwrapIfDenominatorOne rational+  if+    | integer < -maxIntegralRepFloat -> Left $ IntegerFlow integer Underflow+    | integer > maxIntegralRepFloat -> Left $ IntegerFlow integer Overflow+    | otherwise -> Right integer++data RationalErrors = IsNan+                    | IsInf Overflows+  deriving (Show, Eq)++toRational :: Float -> Either RationalErrors Rational+toRational float = if+  | isNaN float      -> Left IsNan+  | isInfinite float -> if+      | float > 0 -> Left $ IsInf Overflow+      | otherwise  -> Left $ IsInf Underflow+  | True              -> Right $ Prelude.toRational float
+ src/Unwitch/Convert/Int.hs view
@@ -0,0 +1,177 @@+module Unwitch.Convert.Int+  ( toInt8+  , toInt8#+  , toInt16+  , toInt16#+  , toInt32+  , toInt32#+  , toInt64+  , toInteger+  , toWord8+  , toWord8#+  , toWord16+  , toWord16#+  , toWord32+  , toWord32#+  , toWord64+  , toWord64#+  , toWord+  , toWord#+  , toNatural+  , toNatural#+  , toFloat+  , toFloat#+  , toDouble+  , toDouble#+  )+where++import           Unwitch.Errors+import           Unwitch.Constant+import qualified Data.Bits as Bits+import           Data.Word+import           Data.Int+import           Numeric.Natural (Natural)+import           Prelude hiding (toInteger)+import           GHC.Exts (Int(..), Word(..), Float(..), Double(..),+                           intToInt8#, int8ToInt#, intToInt16#, int16ToInt#,+                           intToInt32#, int32ToInt#,+                           int2Word#, wordToWord8#, word8ToWord#,+                           wordToWord16#, word16ToWord#,+                           wordToWord32#, word32ToWord#,+                           wordToWord64#,+                           int2Float#, int2Double#,+                           (==#), (>=#), (<#), (>#),+                           word2Int#)+import           GHC.Int (Int8(..), Int16(..), Int32(..))+import           GHC.Word (Word8(..), Word16(..), Word32(..), Word64(..))+import           GHC.Num.Natural (Natural(NS))++toInt8 :: Int -> Maybe Int8+toInt8 = Bits.toIntegralSized++toInt16 :: Int -> Maybe Int16+toInt16 = Bits.toIntegralSized++toInt32 :: Int -> Maybe Int32+toInt32 = Bits.toIntegralSized++toInt64 :: Int -> Int64+toInt64 = fromIntegral++toInteger :: Int -> Integer+toInteger = fromIntegral++toWord8 :: Int -> Maybe Word8+toWord8 = Bits.toIntegralSized++toWord16 :: Int -> Maybe Word16+toWord16 = Bits.toIntegralSized++toWord32 :: Int -> Maybe Word32+toWord32 = Bits.toIntegralSized++toWord64 :: Int -> Maybe Word64+toWord64 = Bits.toIntegralSized++toWord :: Int -> Maybe Word+toWord = Bits.toIntegralSized++toNatural :: Int -> Either Overflows Natural+toNatural x = if+  | x < 0     -> Left Underflow+  | otherwise  -> Right $ fromIntegral x++toFloat :: Int -> Either Overflows Float+toFloat x = if+  | x < -maxIntegralRepFloat -> Left Underflow+  | x > maxIntegralRepFloat  -> Left Overflow+  | otherwise                -> Right $ fromIntegral x++toDouble :: Int -> Either Overflows Double+toDouble x = if+  | fromIntegral x < (-maxIntegralRepDouble :: Integer) -> Left Underflow+  | fromIntegral x > (maxIntegralRepDouble :: Integer)  -> Left Overflow+  | otherwise                                           -> Right $ fromIntegral x++-- | Signed narrowing, roundtrip at Int#+toInt8# :: Int -> (# Int8 | (# #) #)+toInt8# (I# x#) =+  let n# = intToInt8# x#+  in case int8ToInt# n# ==# x# of+    1# -> (# I8# n# | #)+    _  -> (# | (# #) #)++-- | Signed narrowing, roundtrip at Int#+toInt16# :: Int -> (# Int16 | (# #) #)+toInt16# (I# x#) =+  let n# = intToInt16# x#+  in case int16ToInt# n# ==# x# of+    1# -> (# I16# n# | #)+    _  -> (# | (# #) #)++-- | Signed narrowing, roundtrip at Int#+toInt32# :: Int -> (# Int32 | (# #) #)+toInt32# (I# x#) =+  let n# = intToInt32# x#+  in case int32ToInt# n# ==# x# of+    1# -> (# I32# n# | #)+    _  -> (# | (# #) #)++-- | Signed->unsigned narrow, roundtrip via Word# back to Int#+toWord8# :: Int -> (# Word8 | (# #) #)+toWord8# (I# x#) =+  let n# = wordToWord8# (int2Word# x#)+  in case word2Int# (word8ToWord# n#) ==# x# of+    1# -> (# W8# n# | #)+    _  -> (# | (# #) #)++-- | Signed->unsigned narrow, roundtrip via Word# back to Int#+toWord16# :: Int -> (# Word16 | (# #) #)+toWord16# (I# x#) =+  let n# = wordToWord16# (int2Word# x#)+  in case word2Int# (word16ToWord# n#) ==# x# of+    1# -> (# W16# n# | #)+    _  -> (# | (# #) #)++-- | Signed->unsigned narrow, roundtrip via Word# back to Int#+toWord32# :: Int -> (# Word32 | (# #) #)+toWord32# (I# x#) =+  let n# = wordToWord32# (int2Word# x#)+  in case word2Int# (word32ToWord# n#) ==# x# of+    1# -> (# W32# n# | #)+    _  -> (# | (# #) #)++-- | Signed->unsigned, check non-negative+toWord64# :: Int -> (# Word64 | (# #) #)+toWord64# (I# x#) = case x# >=# 0# of+  1# -> (# W64# (wordToWord64# (int2Word# x#)) | #)+  _  -> (# | (# #) #)++-- | Signed->unsigned, check non-negative+toWord# :: Int -> (# Word | (# #) #)+toWord# (I# x#) = case x# >=# 0# of+  1# -> (# W# (int2Word# x#) | #)+  _  -> (# | (# #) #)++-- | Check non-negative, construct NS directly+toNatural# :: Int -> (# Overflows | Natural #)+toNatural# (I# i#) = case i# >=# 0# of+  1# -> (# | NS (int2Word# i#) #)+  _  -> (# Underflow | #)++-- | Bounds-checked float conversion+toFloat# :: Int -> (# Overflows | Float #)+toFloat# (I# i#) = case i# <# -16777215# of+  1# -> (# Underflow | #)+  _  -> case i# ># 16777215# of+    1# -> (# Overflow | #)+    _  -> (# | F# (int2Float# i#) #)++-- | Bounds-checked double conversion+toDouble# :: Int -> (# Overflows | Double #)+toDouble# (I# i#) = case i# <# -9007199254740991# of+  1# -> (# Underflow | #)+  _  -> case i# ># 9007199254740991# of+    1# -> (# Overflow | #)+    _  -> (# | D# (int2Double# i#) #)
+ src/Unwitch/Convert/Int16.hs view
@@ -0,0 +1,127 @@+module Unwitch.Convert.Int16+  ( toInt8+  , toInt32+  , toInt64+  , toInt+  , toInteger+  , toWord8+  , toWord16+  , toWord32+  , toWord64+  , toWord+  , toNatural+  , toFloat+  , toDouble+  , toInt8#+  , toWord8#+  , toWord16#+  , toWord32#+  , toWord64#+  , toWord#+  , toNatural#+  )+where++import           Unwitch.Errors+import qualified Data.Bits as Bits+import           Data.Word+import           Data.Int+import           Numeric.Natural (Natural)+import           Prelude hiding (toInteger)+import           GHC.Exts (Word(..), int16ToInt#, intToInt8#, int8ToInt#,+                           int2Word#, word2Int#,+                           wordToWord8#, word8ToWord#,+                           wordToWord16#, wordToWord32#, wordToWord64#,+                           (==#), (>=#))+import           GHC.Int (Int8(..), Int16(..))+import           GHC.Word (Word8(..), Word16(..), Word32(..), Word64(..))+import           GHC.Num.Natural (Natural(NS))++toInt8 :: Int16 -> Maybe Int8+toInt8 = Bits.toIntegralSized++toInt32 :: Int16 -> Int32+toInt32 = fromIntegral++toInt64 :: Int16 -> Int64+toInt64 = fromIntegral++toInt :: Int16 -> Int+toInt = fromIntegral++toInteger :: Int16 -> Integer+toInteger = fromIntegral++toWord8 :: Int16 -> Maybe Word8+toWord8 = Bits.toIntegralSized++toWord16 :: Int16 -> Maybe Word16+toWord16 = Bits.toIntegralSized++toWord32 :: Int16 -> Maybe Word32+toWord32 = Bits.toIntegralSized++toWord64 :: Int16 -> Maybe Word64+toWord64 = Bits.toIntegralSized++toWord :: Int16 -> Maybe Word+toWord = Bits.toIntegralSized++toNatural :: Int16 -> Either Overflows Natural+toNatural x = if+  | x < 0     -> Left Underflow+  | otherwise  -> Right $ fromIntegral x++toFloat :: Int16 -> Float+toFloat = fromIntegral++toDouble :: Int16 -> Double+toDouble = fromIntegral++-- | Signed narrowing, roundtrip at Int#+toInt8# :: Int16 -> (# Int8 | (# #) #)+toInt8# (I16# x16#) =+  let i# = int16ToInt# x16#+      n# = intToInt8# i#+  in case int8ToInt# n# ==# i# of+    1# -> (# I8# n# | #)+    _  -> (# | (# #) #)++-- | Signed->unsigned narrow, roundtrip via Word# back to Int#+toWord8# :: Int16 -> (# Word8 | (# #) #)+toWord8# (I16# x16#) =+  let i# = int16ToInt# x16#+      n# = wordToWord8# (int2Word# i#)+  in case word2Int# (word8ToWord# n#) ==# i# of+    1# -> (# W8# n# | #)+    _  -> (# | (# #) #)++-- | Signed->unsigned, check non-negative+toWord16# :: Int16 -> (# Word16 | (# #) #)+toWord16# (I16# x16#) = case int16ToInt# x16# >=# 0# of+  1# -> (# W16# (wordToWord16# (int2Word# (int16ToInt# x16#))) | #)+  _  -> (# | (# #) #)++-- | Signed->unsigned, check non-negative+toWord32# :: Int16 -> (# Word32 | (# #) #)+toWord32# (I16# x16#) = case int16ToInt# x16# >=# 0# of+  1# -> (# W32# (wordToWord32# (int2Word# (int16ToInt# x16#))) | #)+  _  -> (# | (# #) #)++-- | Signed->unsigned, check non-negative+toWord64# :: Int16 -> (# Word64 | (# #) #)+toWord64# (I16# x16#) = case int16ToInt# x16# >=# 0# of+  1# -> (# W64# (wordToWord64# (int2Word# (int16ToInt# x16#))) | #)+  _  -> (# | (# #) #)++-- | Signed->unsigned, check non-negative+toWord# :: Int16 -> (# Word | (# #) #)+toWord# (I16# x16#) = case int16ToInt# x16# >=# 0# of+  1# -> (# W# (int2Word# (int16ToInt# x16#)) | #)+  _  -> (# | (# #) #)++-- | Check non-negative, construct NS directly+toNatural# :: Int16 -> (# Overflows | Natural #)+toNatural# (I16# x16#) = case int16ToInt# x16# >=# 0# of+  1# -> (# | NS (int2Word# (int16ToInt# x16#)) #)+  _  -> (# Underflow | #)
+ src/Unwitch/Convert/Int32.hs view
@@ -0,0 +1,164 @@+module Unwitch.Convert.Int32+  ( toInt8+  , toInt16+  , toInt64+  , toInt+  , toInteger+  , toWord8+  , toWord16+  , toWord32+  , toWord64+  , toWord+  , toNatural+  , toFloat+  , toDouble+  , toInt8#+  , toInt16#+  , toInt#+  , toWord8#+  , toWord16#+  , toWord32#+  , toWord64#+  , toWord#+  , toNatural#+  , toFloat#+  )+where++import           Unwitch.Errors+import           Unwitch.Constant+import qualified Data.Bits as Bits+import           Data.Word+import           Data.Int+import           Numeric.Natural (Natural)+import           Prelude hiding (toInteger)+import           GHC.Exts (Int(..), Word(..), Float(..),+                           int32ToInt#, intToInt8#, int8ToInt#,+                           intToInt16#, int16ToInt#,+                           int2Word#, word2Int#,+                           wordToWord8#, word8ToWord#,+                           wordToWord16#, word16ToWord#,+                           wordToWord32#, wordToWord64#,+                           int2Float#,+                           (==#), (>=#), (<#), (>#))+import           GHC.Int (Int8(..), Int16(..), Int32(..))+import           GHC.Word (Word8(..), Word16(..), Word32(..), Word64(..))+import           GHC.Num.Natural (Natural(NS))++toInt8 :: Int32 -> Maybe Int8+toInt8 = Bits.toIntegralSized++toInt16 :: Int32 -> Maybe Int16+toInt16 = Bits.toIntegralSized++toInt64 :: Int32 -> Int64+toInt64 = fromIntegral++toInt :: Int32 -> Maybe Int+toInt = Bits.toIntegralSized++toInteger :: Int32 -> Integer+toInteger = fromIntegral++toWord8 :: Int32 -> Maybe Word8+toWord8 = Bits.toIntegralSized++toWord16 :: Int32 -> Maybe Word16+toWord16 = Bits.toIntegralSized++toWord32 :: Int32 -> Maybe Word32+toWord32 = Bits.toIntegralSized++toWord64 :: Int32 -> Maybe Word64+toWord64 = Bits.toIntegralSized++toWord :: Int32 -> Maybe Word+toWord = Bits.toIntegralSized++toNatural :: Int32 -> Either Overflows Natural+toNatural x = if+  | x < 0     -> Left Underflow+  | otherwise  -> Right $ fromIntegral x++toFloat :: Int32 -> Either Overflows Float+toFloat x = if+  | x < -maxIntegralRepFloat -> Left Underflow+  | x > maxIntegralRepFloat  -> Left Overflow+  | otherwise                -> Right $ fromIntegral x++toDouble :: Int32 -> Double+toDouble = fromIntegral++-- | Signed narrowing, roundtrip at Int#+toInt8# :: Int32 -> (# Int8 | (# #) #)+toInt8# (I32# x32#) =+  let i# = int32ToInt# x32#+      n# = intToInt8# i#+  in case int8ToInt# n# ==# i# of+    1# -> (# I8# n# | #)+    _  -> (# | (# #) #)++-- | Signed narrowing, roundtrip at Int#+toInt16# :: Int32 -> (# Int16 | (# #) #)+toInt16# (I32# x32#) =+  let i# = int32ToInt# x32#+      n# = intToInt16# i#+  in case int16ToInt# n# ==# i# of+    1# -> (# I16# n# | #)+    _  -> (# | (# #) #)++-- | Int32 always fits in Int (Int is at least 32 bits)+toInt# :: Int32 -> (# Int | (# #) #)+toInt# (I32# x32#) = (# I# (int32ToInt# x32#) | #)++-- | Signed->unsigned narrow, roundtrip via Word# back to Int#+toWord8# :: Int32 -> (# Word8 | (# #) #)+toWord8# (I32# x32#) =+  let i# = int32ToInt# x32#+      n# = wordToWord8# (int2Word# i#)+  in case word2Int# (word8ToWord# n#) ==# i# of+    1# -> (# W8# n# | #)+    _  -> (# | (# #) #)++-- | Signed->unsigned narrow, roundtrip via Word# back to Int#+toWord16# :: Int32 -> (# Word16 | (# #) #)+toWord16# (I32# x32#) =+  let i# = int32ToInt# x32#+      n# = wordToWord16# (int2Word# i#)+  in case word2Int# (word16ToWord# n#) ==# i# of+    1# -> (# W16# n# | #)+    _  -> (# | (# #) #)++-- | Signed->unsigned, check non-negative+toWord32# :: Int32 -> (# Word32 | (# #) #)+toWord32# (I32# x32#) = case int32ToInt# x32# >=# 0# of+  1# -> (# W32# (wordToWord32# (int2Word# (int32ToInt# x32#))) | #)+  _  -> (# | (# #) #)++-- | Signed->unsigned, check non-negative+toWord64# :: Int32 -> (# Word64 | (# #) #)+toWord64# (I32# x32#) = case int32ToInt# x32# >=# 0# of+  1# -> (# W64# (wordToWord64# (int2Word# (int32ToInt# x32#))) | #)+  _  -> (# | (# #) #)++-- | Signed->unsigned, check non-negative+toWord# :: Int32 -> (# Word | (# #) #)+toWord# (I32# x32#) = case int32ToInt# x32# >=# 0# of+  1# -> (# W# (int2Word# (int32ToInt# x32#)) | #)+  _  -> (# | (# #) #)++-- | Check non-negative, construct NS directly+toNatural# :: Int32 -> (# Overflows | Natural #)+toNatural# (I32# x32#) = case int32ToInt# x32# >=# 0# of+  1# -> (# | NS (int2Word# (int32ToInt# x32#)) #)+  _  -> (# Underflow | #)++-- | Bounds-checked float conversion+toFloat# :: Int32 -> (# Overflows | Float #)+toFloat# (I32# x32#) =+  let i# = int32ToInt# x32#+  in case i# <# -16777215# of+    1# -> (# Underflow | #)+    _  -> case i# ># 16777215# of+      1# -> (# Overflow | #)+      _  -> (# | F# (int2Float# i#) #)
+ src/Unwitch/Convert/Int64.hs view
@@ -0,0 +1,213 @@+module Unwitch.Convert.Int64+  ( toInt8+  , toInt16+  , toInt32+  , toInt+  , toInteger+  , toWord8+  , toWord16+  , toWord32+  , toWord64+  , toWord+  , toNatural+  , toFloat+  , toDouble+  , toInt8#+  , toInt16#+  , toInt32#+  , toInt#+  , toWord8#+  , toWord16#+  , toWord32#+  , toWord64#+  , toWord#+  , toNatural#+  , toFloat#+  , toDouble#+  )+where++import           Unwitch.Errors+import           Unwitch.Constant+import qualified Data.Bits as Bits+import           Data.Word+import           Data.Int+import           Numeric.Natural (Natural)+import           Prelude hiding (toInteger)+import           GHC.Exts (Int(..), Word(..), Float(..), Double(..),+                           int64ToInt#, intToInt64#,+                           intToInt8#, int8ToInt#,+                           intToInt16#, int16ToInt#,+                           intToInt32#, int32ToInt#,+                           int2Word#, word2Int#,+                           wordToWord8#, word8ToWord#,+                           wordToWord16#, word16ToWord#,+                           wordToWord32#, word32ToWord#,+                           wordToWord64#,+                           int2Float#, int2Double#,+                           (<#), (>#),+                           eqInt64#, geInt64#)+import           GHC.Int (Int8(..), Int16(..), Int32(..), Int64(..))+import           GHC.Word (Word8(..), Word16(..), Word32(..), Word64(..))+import           GHC.Num.Natural (Natural(NS))++toInt8 :: Int64 -> Maybe Int8+toInt8 = Bits.toIntegralSized++toInt16 :: Int64 -> Maybe Int16+toInt16 = Bits.toIntegralSized++toInt32 :: Int64 -> Maybe Int32+toInt32 = Bits.toIntegralSized++toInt :: Int64 -> Maybe Int+toInt = Bits.toIntegralSized++toInteger :: Int64 -> Integer+toInteger = fromIntegral++toWord8 :: Int64 -> Maybe Word8+toWord8 = Bits.toIntegralSized++toWord16 :: Int64 -> Maybe Word16+toWord16 = Bits.toIntegralSized++toWord32 :: Int64 -> Maybe Word32+toWord32 = Bits.toIntegralSized++toWord64 :: Int64 -> Maybe Word64+toWord64 = Bits.toIntegralSized++toWord :: Int64 -> Maybe Word+toWord = Bits.toIntegralSized++toNatural :: Int64 -> Either Overflows Natural+toNatural x = if+  | x < 0     -> Left Underflow+  | otherwise  -> Right $ fromIntegral x++toFloat :: Int64 -> Either Overflows Float+toFloat x = if+  | x < -maxIntegralRepFloat -> Left Underflow+  | x > maxIntegralRepFloat  -> Left Overflow+  | otherwise                -> Right $ fromIntegral x++toDouble :: Int64 -> Either Overflows Double+toDouble x = if+  | x < -maxIntegralRepDouble -> Left Underflow+  | x > maxIntegralRepDouble  -> Left Overflow+  | otherwise                 -> Right $ fromIntegral x++-- | Narrow through Int#, compare at Int64#+toInt8# :: Int64 -> (# Int8 | (# #) #)+toInt8# (I64# x64#) =+  let i# = int64ToInt# x64#+      n# = intToInt8# i#+  in case eqInt64# (intToInt64# (int8ToInt# n#)) x64# of+    1# -> (# I8# n# | #)+    _  -> (# | (# #) #)++-- | Signed narrowing via Int64# comparison+toInt16# :: Int64 -> (# Int16 | (# #) #)+toInt16# (I64# x64#) =+  let i# = int64ToInt# x64#+      n# = intToInt16# i#+  in case eqInt64# (intToInt64# (int16ToInt# n#)) x64# of+    1# -> (# I16# n# | #)+    _  -> (# | (# #) #)++-- | Signed narrowing via Int64# comparison+toInt32# :: Int64 -> (# Int32 | (# #) #)+toInt32# (I64# x64#) =+  let i# = int64ToInt# x64#+      n# = intToInt32# i#+  in case eqInt64# (intToInt64# (int32ToInt# n#)) x64# of+    1# -> (# I32# n# | #)+    _  -> (# | (# #) #)++-- | Roundtrip check at Int64# level for platform safety+toInt# :: Int64 -> (# Int | (# #) #)+toInt# (I64# x64#) =+  let i# = int64ToInt# x64#+  in case eqInt64# (intToInt64# i#) x64# of+    1# -> (# I# i# | #)+    _  -> (# | (# #) #)++-- | Signed->unsigned narrow via Int64# comparison+toWord8# :: Int64 -> (# Word8 | (# #) #)+toWord8# (I64# x64#) =+  let i# = int64ToInt# x64#+      n# = wordToWord8# (int2Word# i#)+  in case eqInt64# (intToInt64# (word2Int# (word8ToWord# n#))) x64# of+    1# -> (# W8# n# | #)+    _  -> (# | (# #) #)++-- | Signed->unsigned narrow via Int64# comparison+toWord16# :: Int64 -> (# Word16 | (# #) #)+toWord16# (I64# x64#) =+  let i# = int64ToInt# x64#+      n# = wordToWord16# (int2Word# i#)+  in case eqInt64# (intToInt64# (word2Int# (word16ToWord# n#))) x64# of+    1# -> (# W16# n# | #)+    _  -> (# | (# #) #)++-- | Signed->unsigned narrow via Int64# comparison+toWord32# :: Int64 -> (# Word32 | (# #) #)+toWord32# (I64# x64#) =+  let i# = int64ToInt# x64#+      n# = wordToWord32# (int2Word# i#)+  in case eqInt64# (intToInt64# (word2Int# (word32ToWord# n#))) x64# of+    1# -> (# W32# n# | #)+    _  -> (# | (# #) #)++-- | Signed->unsigned, check non-negative at Int64# level+toWord64# :: Int64 -> (# Word64 | (# #) #)+toWord64# (I64# x64#) = case geInt64# x64# (intToInt64# 0#) of+  1# -> (# W64# (wordToWord64# (int2Word# (int64ToInt# x64#))) | #)+  _  -> (# | (# #) #)++-- | Signed->unsigned, check non-negative at Int64# then roundtrip+toWord# :: Int64 -> (# Word | (# #) #)+toWord# (I64# x64#) = case geInt64# x64# (intToInt64# 0#) of+  1# -> let i# = int64ToInt# x64#+         in case eqInt64# (intToInt64# i#) x64# of+           1# -> (# W# (int2Word# i#) | #)+           _  -> (# | (# #) #)+  _  -> (# | (# #) #)++-- | Check non-negative at Int64# level, construct NS+toNatural# :: Int64 -> (# Overflows | Natural #)+toNatural# (I64# x64#) = case geInt64# x64# (intToInt64# 0#) of+  1# -> let i# = int64ToInt# x64#+         in case eqInt64# (intToInt64# i#) x64# of+           1# -> (# | NS (int2Word# i#) #)+           _  -> (# Overflow | #)+  _  -> (# Underflow | #)++-- | Bounds-checked float conversion via Int#+toFloat# :: Int64 -> (# Overflows | Float #)+toFloat# (I64# x64#) =+  let i# = int64ToInt# x64#+  in case eqInt64# (intToInt64# i#) x64# of+    1# -> case i# <# -16777215# of+      1# -> (# Underflow | #)+      _  -> case i# ># 16777215# of+        1# -> (# Overflow | #)+        _  -> (# | F# (int2Float# i#) #)+    _  -> case geInt64# x64# (intToInt64# 0#) of+      1# -> (# Overflow | #)+      _  -> (# Underflow | #)++-- | Bounds-checked double conversion via Int#+toDouble# :: Int64 -> (# Overflows | Double #)+toDouble# (I64# x64#) =+  let i# = int64ToInt# x64#+  in case eqInt64# (intToInt64# i#) x64# of+    1# -> case i# <# -9007199254740991# of+      1# -> (# Underflow | #)+      _  -> case i# ># 9007199254740991# of+        1# -> (# Overflow | #)+        _  -> (# | D# (int2Double# i#) #)+    _  -> case geInt64# x64# (intToInt64# 0#) of+      1# -> (# Overflow | #)+      _  -> (# Underflow | #)
+ src/Unwitch/Convert/Int8.hs view
@@ -0,0 +1,112 @@+module Unwitch.Convert.Int8+  ( toInt16+  , toInt32+  , toInt64+  , toInt+  , toInteger+  , toWord8+  , toWord16+  , toWord32+  , toWord64+  , toWord+  , toNatural+  , toFloat+  , toDouble+  , toWord8#+  , toWord16#+  , toWord32#+  , toWord64#+  , toWord#+  , toNatural#+  )+where++import           Unwitch.Errors+import qualified Data.Bits as Bits+import           Data.Word+import           Data.Int+import           Numeric.Natural (Natural)+import           Prelude hiding (toInteger)+import           GHC.Exts (Word(..), int8ToInt#, int2Word#,+                           wordToWord8#, wordToWord16#, wordToWord32#,+                           wordToWord64#, (>=#))+import           GHC.Int (Int8(..))+import           GHC.Word (Word8(..), Word16(..), Word32(..), Word64(..))+import           GHC.Num.Natural (Natural(NS))++toInt16 :: Int8 -> Int16+toInt16 = fromIntegral++toInt32 :: Int8 -> Int32+toInt32 = fromIntegral++toInt64 :: Int8 -> Int64+toInt64 = fromIntegral++toInt :: Int8 -> Int+toInt = fromIntegral++toInteger :: Int8 -> Integer+toInteger = fromIntegral++toWord8 :: Int8 -> Maybe Word8+toWord8 = Bits.toIntegralSized++toWord16 :: Int8 -> Maybe Word16+toWord16 = Bits.toIntegralSized++toWord32 :: Int8 -> Maybe Word32+toWord32 = Bits.toIntegralSized++toWord64 :: Int8 -> Maybe Word64+toWord64 = Bits.toIntegralSized++toWord :: Int8 -> Maybe Word+toWord = Bits.toIntegralSized++toNatural :: Int8 -> Either Overflows Natural+toNatural x = if+  | x < 0     -> Left Underflow+  | otherwise  -> Right $ fromIntegral x++toFloat :: Int8 -> Float+toFloat = fromIntegral++toDouble :: Int8 -> Double+toDouble = fromIntegral++-- | Signed->unsigned, check non-negative+toWord8# :: Int8 -> (# Word8 | (# #) #)+toWord8# (I8# x8#) = case int8ToInt# x8# >=# 0# of+  1# -> (# W8# (wordToWord8# (int2Word# (int8ToInt# x8#))) | #)+  _  -> (# | (# #) #)++-- | Signed->unsigned, check non-negative+toWord16# :: Int8 -> (# Word16 | (# #) #)+toWord16# (I8# x8#) = case int8ToInt# x8# >=# 0# of+  1# -> (# W16# (wordToWord16# (int2Word# (int8ToInt# x8#))) | #)+  _  -> (# | (# #) #)++-- | Signed->unsigned, check non-negative+toWord32# :: Int8 -> (# Word32 | (# #) #)+toWord32# (I8# x8#) = case int8ToInt# x8# >=# 0# of+  1# -> (# W32# (wordToWord32# (int2Word# (int8ToInt# x8#))) | #)+  _  -> (# | (# #) #)++-- | Signed->unsigned, check non-negative+toWord64# :: Int8 -> (# Word64 | (# #) #)+toWord64# (I8# x8#) = case int8ToInt# x8# >=# 0# of+  1# -> (# W64# (wordToWord64# (int2Word# (int8ToInt# x8#))) | #)+  _  -> (# | (# #) #)++-- | Signed->unsigned, check non-negative+toWord# :: Int8 -> (# Word | (# #) #)+toWord# (I8# x8#) = case int8ToInt# x8# >=# 0# of+  1# -> (# W# (int2Word# (int8ToInt# x8#)) | #)+  _  -> (# | (# #) #)++-- | Check non-negative, construct NS directly+toNatural# :: Int8 -> (# Overflows | Natural #)+toNatural# (I8# x8#) = case int8ToInt# x8# >=# 0# of+  1# -> (# | NS (int2Word# (int8ToInt# x8#)) #)+  _  -> (# Underflow | #)
+ src/Unwitch/Convert/Integer.hs view
@@ -0,0 +1,238 @@+module Unwitch.Convert.Integer+  ( toDouble+  , toDouble#+  , toFloat+  , toFloat#+  , toNatural+  , toNatural#+  , toInt8+  , toInt8#+  , toInt16+  , toInt16#+  , toInt32+  , toInt32#+  , toInt64+  , toInt64#+  , toInt+  , toInt#+  , toWord8+  , toWord8#+  , toWord16+  , toWord16#+  , toWord32+  , toWord32#+  , toWord64+  , toWord64#+  , toWord+  , toWord#+  )+where++import           Unwitch.Errors+import           Unwitch.Constant+import qualified Data.Bits as Bits+import Data.Word+import Data.Int+import Numeric.Natural (Natural)+import           GHC.Exts (Int(..), Word(..), Float(..), Double(..),+                           intToInt8#, int8ToInt#,+                           intToInt16#, int16ToInt#,+                           intToInt32#, int32ToInt#,+                           intToInt64#,+                           int2Word#, word2Int#,+                           wordToWord8#, word8ToWord#,+                           wordToWord16#, word16ToWord#,+                           wordToWord32#, word32ToWord#,+                           wordToWord64#,+                           int2Float#, int2Double#,+                           (==#), (>=#), (<#), (>#))+import           GHC.Int (Int8(..), Int16(..), Int32(..), Int64(..))+import           GHC.Word (Word8(..), Word16(..), Word32(..), Word64(..))+import           GHC.Num.Integer (Integer(..), integerToWord#,+                                  integerFromWord#, integerEq#)+import           GHC.Num.Natural (Natural(NS, NB))+++toDouble :: Integer -> Either Overflows Double+toDouble integer = if+    | integer < -maxIntegralRepDouble -> Left Underflow+    | integer > maxIntegralRepDouble -> Left Overflow+    | otherwise -> Right $ Prelude.fromIntegral integer++toFloat :: Integer -> Either Overflows Float+toFloat integer = if+    | integer < -maxIntegralRepFloat -> Left Underflow+    | integer > maxIntegralRepFloat -> Left Overflow+    | otherwise -> Right $ Prelude.fromIntegral integer++toNatural :: Integer -> Either Overflows Natural+toNatural integer = if+    | integer < 0 -> Left Underflow+    | otherwise -> Right $ Prelude.fromIntegral integer++toInt8 :: Integer -> Maybe Int8+toInt8 = Bits.toIntegralSized++toInt16 :: Integer -> Maybe Int16+toInt16 = Bits.toIntegralSized++toInt32 :: Integer -> Maybe Int32+toInt32 = Bits.toIntegralSized++toInt64 :: Integer -> Maybe Int64+toInt64 = Bits.toIntegralSized++toInt :: Integer -> Maybe Int+toInt = Bits.toIntegralSized++toWord8 :: Integer -> Maybe Word8+toWord8 = Bits.toIntegralSized++toWord16 :: Integer -> Maybe Word16+toWord16 = Bits.toIntegralSized++toWord32 :: Integer -> Maybe Word32+toWord32 = Bits.toIntegralSized++toWord64 :: Integer -> Maybe Word64+toWord64 = Bits.toIntegralSized++toWord :: Integer -> Maybe Word+toWord = Bits.toIntegralSized++-- | Bounds-checked double conversion via IS/IP/IN+toDouble# :: Integer -> (# Overflows | Double #)+toDouble# x = case x of+  IS i# -> case i# <# -9007199254740991# of+    1# -> (# Underflow | #)+    _  -> case i# ># 9007199254740991# of+      1# -> (# Overflow | #)+      _  -> (# | D# (int2Double# i#) #)+  IP _ -> (# Overflow | #)+  IN _ -> (# Underflow | #)++-- | Bounds-checked float conversion via IS/IP/IN+toFloat# :: Integer -> (# Overflows | Float #)+toFloat# x = case x of+  IS i# -> case i# <# -16777215# of+    1# -> (# Underflow | #)+    _  -> case i# ># 16777215# of+      1# -> (# Overflow | #)+      _  -> (# | F# (int2Float# i#) #)+  IP _ -> (# Overflow | #)+  IN _ -> (# Underflow | #)++-- | Integer->Natural via IS/IP/IN+toNatural# :: Integer -> (# Overflows | Natural #)+toNatural# x = case x of+  IS i# -> case i# >=# 0# of+    1# -> (# | NS (int2Word# i#) #)+    _  -> (# Underflow | #)+  IP ba# -> (# | NB ba# #)+  IN _ -> (# Underflow | #)++-- | Integer->Int8 via IS/IP/IN, narrow and roundtrip at Int#+toInt8# :: Integer -> (# Int8 | (# #) #)+toInt8# x = case x of+  IS i# ->+    let n# = intToInt8# i#+    in case int8ToInt# n# ==# i# of+      1# -> (# I8# n# | #)+      _  -> (# | (# #) #)+  IP _ -> (# | (# #) #)+  IN _ -> (# | (# #) #)++-- | Integer->Int16 via IS/IP/IN+toInt16# :: Integer -> (# Int16 | (# #) #)+toInt16# x = case x of+  IS i# ->+    let n# = intToInt16# i#+    in case int16ToInt# n# ==# i# of+      1# -> (# I16# n# | #)+      _  -> (# | (# #) #)+  IP _ -> (# | (# #) #)+  IN _ -> (# | (# #) #)++-- | Integer->Int32 via IS/IP/IN+toInt32# :: Integer -> (# Int32 | (# #) #)+toInt32# x = case x of+  IS i# ->+    let n# = intToInt32# i#+    in case int32ToInt# n# ==# i# of+      1# -> (# I32# n# | #)+      _  -> (# | (# #) #)+  IP _ -> (# | (# #) #)+  IN _ -> (# | (# #) #)++-- | Integer->Int64 via IS/IP/IN+toInt64# :: Integer -> (# Int64 | (# #) #)+toInt64# x = case x of+  IS i# -> (# I64# (intToInt64# i#) | #)+  IP _ -> (# | (# #) #)+  IN _ -> (# | (# #) #)++-- | Integer->Int via IS/IP/IN+toInt# :: Integer -> (# Int | (# #) #)+toInt# x = case x of+  IS i# -> (# I# i# | #)+  IP _ -> (# | (# #) #)+  IN _ -> (# | (# #) #)++-- | Integer->Word8, IS case uses signed->unsigned narrow+toWord8# :: Integer -> (# Word8 | (# #) #)+toWord8# x = case x of+  IS i# ->+    let n# = wordToWord8# (int2Word# i#)+    in case word2Int# (word8ToWord# n#) ==# i# of+      1# -> (# W8# n# | #)+      _  -> (# | (# #) #)+  IP _ -> (# | (# #) #)+  IN _ -> (# | (# #) #)++-- | Integer->Word16+toWord16# :: Integer -> (# Word16 | (# #) #)+toWord16# x = case x of+  IS i# ->+    let n# = wordToWord16# (int2Word# i#)+    in case word2Int# (word16ToWord# n#) ==# i# of+      1# -> (# W16# n# | #)+      _  -> (# | (# #) #)+  IP _ -> (# | (# #) #)+  IN _ -> (# | (# #) #)++-- | Integer->Word32+toWord32# :: Integer -> (# Word32 | (# #) #)+toWord32# x = case x of+  IS i# ->+    let n# = wordToWord32# (int2Word# i#)+    in case word2Int# (word32ToWord# n#) ==# i# of+      1# -> (# W32# n# | #)+      _  -> (# | (# #) #)+  IP _ -> (# | (# #) #)+  IN _ -> (# | (# #) #)++-- | Integer->Word64: IS checks non-negative; IP uses integerToWord# roundtrip+toWord64# :: Integer -> (# Word64 | (# #) #)+toWord64# x = case x of+  IS i# -> case i# >=# 0# of+    1# -> (# W64# (wordToWord64# (int2Word# i#)) | #)+    _  -> (# | (# #) #)+  IP _ ->+    let w# = integerToWord# x+    in case integerEq# (integerFromWord# w#) x of+      1# -> (# W64# (wordToWord64# w#) | #)+      _  -> (# | (# #) #)+  IN _ -> (# | (# #) #)++-- | Integer->Word: IS checks non-negative; IP uses integerToWord# roundtrip+toWord# :: Integer -> (# Word | (# #) #)+toWord# x = case x of+  IS i# -> case i# >=# 0# of+    1# -> (# W# (int2Word# i#) | #)+    _  -> (# | (# #) #)+  IP _ ->+    let w# = integerToWord# x+    in case integerEq# (integerFromWord# w#) x of+      1# -> (# W# w# | #)+      _  -> (# | (# #) #)+  IN _ -> (# | (# #) #)
+ src/Unwitch/Convert/LazyByteString.hs view
@@ -0,0 +1,48 @@+module Unwitch.Convert.LazyByteString+  ( toByteString+  , toWord8s+  , fromWord8s+  , toLazyTextLatin1+  , toLazyTextUtf8+  , toLazyTextUtf16LE+  , toLazyTextUtf16BE+  , toLazyTextUtf32LE+  , toLazyTextUtf32BE+  )+where++import Data.ByteString (ByteString)+import Data.ByteString.Lazy qualified as LBS+import Data.Text.Lazy qualified as LT+import Data.Text.Lazy.Encoding qualified as LTE+import Data.Text.Encoding.Error (UnicodeException)+import Data.Word (Word8)+import Unwitch.TryEvaluate (tryEvaluate)++toByteString :: LBS.ByteString -> ByteString+toByteString = LBS.toStrict++toWord8s :: LBS.ByteString -> [Word8]+toWord8s = LBS.unpack++fromWord8s :: [Word8] -> LBS.ByteString+fromWord8s = LBS.pack++toLazyTextLatin1 :: LBS.ByteString -> LT.Text+toLazyTextLatin1 = LTE.decodeLatin1++toLazyTextUtf8 :: LBS.ByteString -> Either UnicodeException LT.Text+toLazyTextUtf8 = LTE.decodeUtf8'++toLazyTextUtf16LE :: LBS.ByteString -> Either UnicodeException LT.Text+toLazyTextUtf16LE = tryEvaluate . LTE.decodeUtf16LE++toLazyTextUtf16BE :: LBS.ByteString -> Either UnicodeException LT.Text+toLazyTextUtf16BE = tryEvaluate . LTE.decodeUtf16BE++toLazyTextUtf32LE :: LBS.ByteString -> Either UnicodeException LT.Text+toLazyTextUtf32LE = tryEvaluate . LTE.decodeUtf32LE++toLazyTextUtf32BE :: LBS.ByteString -> Either UnicodeException LT.Text+toLazyTextUtf32BE = tryEvaluate . LTE.decodeUtf32BE+
+ src/Unwitch/Convert/LazyText.hs view
@@ -0,0 +1,50 @@+module Unwitch.Convert.LazyText+  ( toText+  , toString+  , fromString+  , toLazyByteStringUtf8+  , toLazyByteStringUtf16LE+  , toLazyByteStringUtf16BE+  , toLazyByteStringUtf32LE+  , toLazyByteStringUtf32BE+  , toLazyByteStringLatin1+  )+where++import Data.ByteString.Lazy qualified as LBS+import Data.ByteString.Lazy.Char8 qualified as LBSC8+import Data.Text (Text)+import Data.Text.Lazy qualified as LT+import Data.Text.Lazy.Encoding qualified as LTE++toText :: LT.Text -> Text+toText = LT.toStrict++toString :: LT.Text -> String+toString = LT.unpack++fromString :: String -> LT.Text+fromString = LT.pack++toLazyByteStringUtf8 :: LT.Text -> LBS.ByteString+toLazyByteStringUtf8 = LTE.encodeUtf8++toLazyByteStringUtf16LE :: LT.Text -> LBS.ByteString+toLazyByteStringUtf16LE = LTE.encodeUtf16LE++toLazyByteStringUtf16BE :: LT.Text -> LBS.ByteString+toLazyByteStringUtf16BE = LTE.encodeUtf16BE++toLazyByteStringUtf32LE :: LT.Text -> LBS.ByteString+toLazyByteStringUtf32LE = LTE.encodeUtf32LE++toLazyByteStringUtf32BE :: LT.Text -> LBS.ByteString+toLazyByteStringUtf32BE = LTE.encodeUtf32BE++toLazyByteStringLatin1 :: LT.Text -> Maybe LBS.ByteString+toLazyByteStringLatin1 t = if LT.all isLatin1 t+  then Just $ LBSC8.pack (LT.unpack t)+  else Nothing++isLatin1 :: Char -> Bool+isLatin1 c = c <= '\xFF'
+ src/Unwitch/Convert/Natural.hs view
@@ -0,0 +1,196 @@+module Unwitch.Convert.Natural+  ( toWord8+  , toWord16+  , toWord32+  , toWord64+  , toWord+  , toInt8+  , toInt16+  , toInt32+  , toInt64+  , toInt+  , toInteger+  , toFloat+  , toDouble+  , toWord8#+  , toWord16#+  , toWord32#+  , toWord64#+  , toWord#+  , toInt8#+  , toInt16#+  , toInt32#+  , toInt64#+  , toInt#+  , toFloat#+  , toDouble#+  )+where++import           Unwitch.Errors+import           Unwitch.Constant+import qualified Data.Bits as Bits+import           Data.Word+import           Data.Int+import           Numeric.Natural (Natural)+import           Prelude hiding (toInteger)+import           GHC.Exts (Int(..), Word(..), Float(..), Double(..),+                           word2Int#,+                           wordToWord8#, word8ToWord#,+                           wordToWord16#, word16ToWord#,+                           wordToWord32#, word32ToWord#,+                           wordToWord64#,+                           intToInt8#,+                           intToInt16#,+                           intToInt32#,+                           intToInt64#,+                           int2Float#, int2Double#,+                           eqWord#, leWord#, (>=#))+import           GHC.Int (Int8(..), Int16(..), Int32(..), Int64(..))+import           GHC.Word (Word8(..), Word16(..), Word32(..), Word64(..))+import           GHC.Num.Natural (naturalToWordMaybe#)++toWord8 :: Natural -> Maybe Word8+toWord8 = Bits.toIntegralSized++toWord16 :: Natural -> Maybe Word16+toWord16 = Bits.toIntegralSized++toWord32 :: Natural -> Maybe Word32+toWord32 = Bits.toIntegralSized++toWord64 :: Natural -> Maybe Word64+toWord64 = Bits.toIntegralSized++toWord :: Natural -> Maybe Word+toWord = Bits.toIntegralSized++toInt8 :: Natural -> Maybe Int8+toInt8 = Bits.toIntegralSized++toInt16 :: Natural -> Maybe Int16+toInt16 = Bits.toIntegralSized++toInt32 :: Natural -> Maybe Int32+toInt32 = Bits.toIntegralSized++toInt64 :: Natural -> Maybe Int64+toInt64 = Bits.toIntegralSized++toInt :: Natural -> Maybe Int+toInt = Bits.toIntegralSized++toInteger :: Natural -> Integer+toInteger = fromIntegral++toFloat :: Natural -> Either Overflows Float+toFloat x = if+  | x > maxIntegralRepFloat -> Left Overflow+  | otherwise               -> Right $ fromIntegral x++toDouble :: Natural -> Either Overflows Double+toDouble x = if+  | x > maxIntegralRepDouble -> Left Overflow+  | otherwise                -> Right $ fromIntegral x++-- | Via naturalToWordMaybe#, then narrow and roundtrip at Word#+toWord8# :: Natural -> (# Word8 | (# #) #)+toWord8# nat = case naturalToWordMaybe# nat of+  (# (# #) | #) -> (# | (# #) #)+  (# | w# #) ->+    let n# = wordToWord8# w#+    in case word8ToWord# n# `eqWord#` w# of+      1# -> (# W8# n# | #)+      _  -> (# | (# #) #)++-- | Via naturalToWordMaybe#, then narrow+toWord16# :: Natural -> (# Word16 | (# #) #)+toWord16# nat = case naturalToWordMaybe# nat of+  (# (# #) | #) -> (# | (# #) #)+  (# | w# #) ->+    let n# = wordToWord16# w#+    in case word16ToWord# n# `eqWord#` w# of+      1# -> (# W16# n# | #)+      _  -> (# | (# #) #)++-- | Via naturalToWordMaybe#, then narrow+toWord32# :: Natural -> (# Word32 | (# #) #)+toWord32# nat = case naturalToWordMaybe# nat of+  (# (# #) | #) -> (# | (# #) #)+  (# | w# #) ->+    let n# = wordToWord32# w#+    in case word32ToWord# n# `eqWord#` w# of+      1# -> (# W32# n# | #)+      _  -> (# | (# #) #)++-- | Via naturalToWordMaybe#, then widen to Word64+toWord64# :: Natural -> (# Word64 | (# #) #)+toWord64# nat = case naturalToWordMaybe# nat of+  (# (# #) | #) -> (# | (# #) #)+  (# | w# #) -> (# W64# (wordToWord64# w#) | #)++-- | Via naturalToWordMaybe#+toWord# :: Natural -> (# Word | (# #) #)+toWord# nat = case naturalToWordMaybe# nat of+  (# (# #) | #) -> (# | (# #) #)+  (# | w# #) -> (# W# w# | #)++-- | Via naturalToWordMaybe#, check upper bound for Int8+toInt8# :: Natural -> (# Int8 | (# #) #)+toInt8# nat = case naturalToWordMaybe# nat of+  (# (# #) | #) -> (# | (# #) #)+  (# | w# #) -> case leWord# w# 127## of+    1# -> (# I8# (intToInt8# (word2Int# w#)) | #)+    _  -> (# | (# #) #)++-- | Via naturalToWordMaybe#, check upper bound for Int16+toInt16# :: Natural -> (# Int16 | (# #) #)+toInt16# nat = case naturalToWordMaybe# nat of+  (# (# #) | #) -> (# | (# #) #)+  (# | w# #) -> case leWord# w# 32767## of+    1# -> (# I16# (intToInt16# (word2Int# w#)) | #)+    _  -> (# | (# #) #)++-- | Via naturalToWordMaybe#, check upper bound for Int32+toInt32# :: Natural -> (# Int32 | (# #) #)+toInt32# nat = case naturalToWordMaybe# nat of+  (# (# #) | #) -> (# | (# #) #)+  (# | w# #) -> case leWord# w# 2147483647## of+    1# -> (# I32# (intToInt32# (word2Int# w#)) | #)+    _  -> (# | (# #) #)++-- | Via naturalToWordMaybe#, check fits in non-negative Int64+toInt64# :: Natural -> (# Int64 | (# #) #)+toInt64# nat = case naturalToWordMaybe# nat of+  (# (# #) | #) -> (# | (# #) #)+  (# | w# #) ->+    let i# = word2Int# w#+    in case i# >=# 0# of+      1# -> (# I64# (intToInt64# i#) | #)+      _  -> (# | (# #) #)++-- | Via naturalToWordMaybe#, check fits in non-negative Int+toInt# :: Natural -> (# Int | (# #) #)+toInt# nat = case naturalToWordMaybe# nat of+  (# (# #) | #) -> (# | (# #) #)+  (# | w# #) ->+    let i# = word2Int# w#+    in case i# >=# 0# of+      1# -> (# I# i# | #)+      _  -> (# | (# #) #)++-- | Via naturalToWordMaybe#, bounds-checked float+toFloat# :: Natural -> (# Overflows | Float #)+toFloat# nat = case naturalToWordMaybe# nat of+  (# (# #) | #) -> (# Overflow | #)+  (# | w# #) -> case leWord# w# 16777215## of+    1# -> (# | F# (int2Float# (word2Int# w#)) #)+    _  -> (# Overflow | #)++-- | Via naturalToWordMaybe#, bounds-checked double+toDouble# :: Natural -> (# Overflows | Double #)+toDouble# nat = case naturalToWordMaybe# nat of+  (# (# #) | #) -> (# Overflow | #)+  (# | w# #) -> case leWord# w# 9007199254740991## of+    1# -> (# | D# (int2Double# (word2Int# w#)) #)+    _  -> (# Overflow | #)
+ src/Unwitch/Convert/Ratio.hs view
@@ -0,0 +1,30 @@+module Unwitch.Convert.Ratio+  ( unwrapIfDenominatorOne+  , fromIntegralToRatio+  , toFloat+  , toDouble+  )+where++import Data.Ratio(Ratio, (%))+import qualified Data.Ratio as Ratio++-- | Converts if denominator == 1+unwrapIfDenominatorOne :: (Eq a, Num a) => Ratio a -> Maybe a+unwrapIfDenominatorOne s = if Ratio.denominator s == 1 then+  Just $ Ratio.numerator s+  else Nothing++-- | Wraps an integral value as a Ratio with denominator 1.+fromIntegralToRatio :: (Integral a) => a -> Ratio a+fromIntegralToRatio x = x % 1++-- | Converts a Rational to Float. This is lossy for rationals+-- that cannot be exactly represented as Float.+toFloat :: Rational -> Float+toFloat = fromRational++-- | Converts a Rational to Double. This is lossy for rationals+-- that cannot be exactly represented as Double.+toDouble :: Rational -> Double+toDouble = fromRational
+ src/Unwitch/Convert/ShortByteString.hs view
@@ -0,0 +1,20 @@+module Unwitch.Convert.ShortByteString+  ( toByteString+  , toWord8s+  , fromWord8s+  )+where++import Data.ByteString (ByteString)+import Data.ByteString.Short (ShortByteString)+import Data.ByteString.Short qualified as SBS+import Data.Word (Word8)++toByteString :: ShortByteString -> ByteString+toByteString = SBS.fromShort++toWord8s :: ShortByteString -> [Word8]+toWord8s = SBS.unpack++fromWord8s :: [Word8] -> ShortByteString+fromWord8s = SBS.pack
+ src/Unwitch/Convert/Text.hs view
@@ -0,0 +1,52 @@+module Unwitch.Convert.Text+  ( toLazyText+  , toString+  , fromString+  , toByteStringUtf8+  , toByteStringUtf16LE+  , toByteStringUtf16BE+  , toByteStringUtf32LE+  , toByteStringUtf32BE+  , toByteStringLatin1+  )+where++import Data.ByteString (ByteString)+import Data.ByteString.Char8 qualified as BSC8+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.Encoding qualified as TE+import Data.Text.Lazy qualified as LT++toLazyText :: Text -> LT.Text+toLazyText = LT.fromStrict++toString :: Text -> String+toString = T.unpack++fromString :: String -> Text+fromString = T.pack++toByteStringUtf8 :: Text -> ByteString+toByteStringUtf8 = TE.encodeUtf8++toByteStringUtf16LE :: Text -> ByteString+toByteStringUtf16LE = TE.encodeUtf16LE++toByteStringUtf16BE :: Text -> ByteString+toByteStringUtf16BE = TE.encodeUtf16BE++toByteStringUtf32LE :: Text -> ByteString+toByteStringUtf32LE = TE.encodeUtf32LE++toByteStringUtf32BE :: Text -> ByteString+toByteStringUtf32BE = TE.encodeUtf32BE++toByteStringLatin1 :: Text -> Maybe ByteString+toByteStringLatin1 t = if T.all isLatin1 t+  then Just $ BSC8.pack (T.unpack t)+  else Nothing++isLatin1 :: Char -> Bool+isLatin1 c = c <= '\xFF'+
+ src/Unwitch/Convert/Word.hs view
@@ -0,0 +1,158 @@+module Unwitch.Convert.Word+  ( toWord8+  , toWord16+  , toWord32+  , toWord64+  , toNatural+  , toInt8+  , toInt16+  , toInt32+  , toInt64+  , toInt+  , toInteger+  , toFloat+  , toDouble+  , toWord8#+  , toWord16#+  , toWord32#+  , toInt8#+  , toInt16#+  , toInt32#+  , toInt64#+  , toInt#+  , toFloat#+  , toDouble#+  )+where++import           Unwitch.Errors+import           Unwitch.Constant+import qualified Data.Bits as Bits+import           Data.Word+import           Data.Int+import           Numeric.Natural (Natural)+import           Prelude hiding (toInteger)+import           GHC.Exts (Int(..), Word(..), Float(..), Double(..),+                           wordToWord8#, word8ToWord#,+                           wordToWord16#, word16ToWord#,+                           wordToWord32#, word32ToWord#,+                           word2Int#,+                           intToInt8#, intToInt16#, intToInt32#,+                           intToInt64#,+                           int2Float#, int2Double#,+                           eqWord#, leWord#, (>=#))+import           GHC.Int (Int8(..), Int16(..), Int32(..), Int64(..))+import           GHC.Word (Word8(..), Word16(..), Word32(..))++toWord8 :: Word -> Maybe Word8+toWord8 = Bits.toIntegralSized++toWord16 :: Word -> Maybe Word16+toWord16 = Bits.toIntegralSized++toWord32 :: Word -> Maybe Word32+toWord32 = Bits.toIntegralSized++toWord64 :: Word -> Word64+toWord64 = fromIntegral++toNatural :: Word -> Natural+toNatural = fromIntegral++toInt8 :: Word -> Maybe Int8+toInt8 = Bits.toIntegralSized++toInt16 :: Word -> Maybe Int16+toInt16 = Bits.toIntegralSized++toInt32 :: Word -> Maybe Int32+toInt32 = Bits.toIntegralSized++toInt64 :: Word -> Maybe Int64+toInt64 = Bits.toIntegralSized++toInt :: Word -> Maybe Int+toInt = Bits.toIntegralSized++toInteger :: Word -> Integer+toInteger = fromIntegral++toFloat :: Word -> Either Overflows Float+toFloat x = if+  | x > maxIntegralRepFloat -> Left Overflow+  | otherwise               -> Right $ fromIntegral x++toDouble :: Word -> Either Overflows Double+toDouble x = if+  | fromIntegral x > (maxIntegralRepDouble :: Integer) -> Left Overflow+  | otherwise                                          -> Right $ fromIntegral x++-- | Unsigned narrowing, roundtrip at Word#+toWord8# :: Word -> (# Word8 | (# #) #)+toWord8# (W# w#) =+  let n# = wordToWord8# w#+  in case word8ToWord# n# `eqWord#` w# of+    1# -> (# W8# n# | #)+    _  -> (# | (# #) #)++-- | Unsigned narrowing, roundtrip at Word#+toWord16# :: Word -> (# Word16 | (# #) #)+toWord16# (W# w#) =+  let n# = wordToWord16# w#+  in case word16ToWord# n# `eqWord#` w# of+    1# -> (# W16# n# | #)+    _  -> (# | (# #) #)++-- | Unsigned narrowing, roundtrip at Word#+toWord32# :: Word -> (# Word32 | (# #) #)+toWord32# (W# w#) =+  let n# = wordToWord32# w#+  in case word32ToWord# n# `eqWord#` w# of+    1# -> (# W32# n# | #)+    _  -> (# | (# #) #)++-- | Check upper bound for signed target+toInt8# :: Word -> (# Int8 | (# #) #)+toInt8# (W# w#) = case leWord# w# 127## of+  1# -> (# I8# (intToInt8# (word2Int# w#)) | #)+  _  -> (# | (# #) #)++-- | Check upper bound for signed target+toInt16# :: Word -> (# Int16 | (# #) #)+toInt16# (W# w#) = case leWord# w# 32767## of+  1# -> (# I16# (intToInt16# (word2Int# w#)) | #)+  _  -> (# | (# #) #)++-- | Check upper bound for signed target+toInt32# :: Word -> (# Int32 | (# #) #)+toInt32# (W# w#) = case leWord# w# 2147483647## of+  1# -> (# I32# (intToInt32# (word2Int# w#)) | #)+  _  -> (# | (# #) #)++-- | Check high bit not set for Int64+toInt64# :: Word -> (# Int64 | (# #) #)+toInt64# (W# w#) =+  let i# = word2Int# w#+  in case i# >=# 0# of+    1# -> (# I64# (intToInt64# i#) | #)+    _  -> (# | (# #) #)++-- | Check high bit not set for Int+toInt# :: Word -> (# Int | (# #) #)+toInt# (W# w#) =+  let i# = word2Int# w#+  in case i# >=# 0# of+    1# -> (# I# i# | #)+    _  -> (# | (# #) #)++-- | Bounds-checked float conversion+toFloat# :: Word -> (# Overflows | Float #)+toFloat# (W# w#) = case leWord# w# 16777215## of+  1# -> (# | F# (int2Float# (word2Int# w#)) #)+  _  -> (# Overflow | #)++-- | Bounds-checked double conversion+toDouble# :: Word -> (# Overflows | Double #)+toDouble# (W# w#) = case leWord# w# 9007199254740991## of+  1# -> (# | D# (int2Double# (word2Int# w#)) #)+  _  -> (# Overflow | #)
+ src/Unwitch/Convert/Word16.hs view
@@ -0,0 +1,98 @@+module Unwitch.Convert.Word16+  ( toWord8+  , toWord32+  , toWord64+  , toWord+  , toNatural+  , toInt8+  , toInt16+  , toInt32+  , toInt64+  , toInt+  , toInteger+  , toFloat+  , toDouble+  , toWord8#+  , toInt8#+  , toInt16#+  )+where++import qualified Data.Bits as Bits+import           Data.Word+import           Data.Int+import           Numeric.Natural (Natural)+import           Prelude hiding (toInteger)+import           GHC.Exts (word16ToWord#, word2Int#,+                           wordToWord8#, word8ToWord#,+                           intToInt8#, int8ToInt#,+                           intToInt16#, int16ToInt#,+                           eqWord#, (==#))+import           GHC.Int (Int8(..), Int16(..))+import           GHC.Word (Word8(..), Word16(..))++toWord8 :: Word16 -> Maybe Word8+toWord8 = Bits.toIntegralSized++toWord32 :: Word16 -> Word32+toWord32 = fromIntegral++toWord64 :: Word16 -> Word64+toWord64 = fromIntegral++toWord :: Word16 -> Word+toWord = fromIntegral++toNatural :: Word16 -> Natural+toNatural = fromIntegral++toInt8 :: Word16 -> Maybe Int8+toInt8 = Bits.toIntegralSized++toInt16 :: Word16 -> Maybe Int16+toInt16 = Bits.toIntegralSized++toInt32 :: Word16 -> Int32+toInt32 = fromIntegral++toInt64 :: Word16 -> Int64+toInt64 = fromIntegral++toInt :: Word16 -> Int+toInt = fromIntegral++toInteger :: Word16 -> Integer+toInteger = fromIntegral++toFloat :: Word16 -> Float+toFloat = fromIntegral++toDouble :: Word16 -> Double+toDouble = fromIntegral++-- | Unsigned narrowing, roundtrip at Word#+toWord8# :: Word16 -> (# Word8 | (# #) #)+toWord8# (W16# w16#) =+  let w# = word16ToWord# w16#+      n# = wordToWord8# w#+  in case word8ToWord# n# `eqWord#` w# of+    1# -> (# W8# n# | #)+    _  -> (# | (# #) #)++-- | Unsigned->signed, source fits in Int#, roundtrip at Int#+toInt8# :: Word16 -> (# Int8 | (# #) #)+toInt8# (W16# w16#) =+  let i# = word2Int# (word16ToWord# w16#)+      n# = intToInt8# i#+  in case int8ToInt# n# ==# i# of+    1# -> (# I8# n# | #)+    _  -> (# | (# #) #)++-- | Unsigned->signed, source fits in Int#, roundtrip at Int#+toInt16# :: Word16 -> (# Int16 | (# #) #)+toInt16# (W16# w16#) =+  let i# = word2Int# (word16ToWord# w16#)+      n# = intToInt16# i#+  in case int16ToInt# n# ==# i# of+    1# -> (# I16# n# | #)+    _  -> (# | (# #) #)
+ src/Unwitch/Convert/Word32.hs view
@@ -0,0 +1,147 @@+module Unwitch.Convert.Word32+  ( toWord8+  , toWord16+  , toWord64+  , toWord+  , toNatural+  , toInt8+  , toInt16+  , toInt32+  , toInt64+  , toInt+  , toInteger+  , toFloat+  , toDouble+  , toWord8#+  , toWord16#+  , toWord#+  , toInt8#+  , toInt16#+  , toInt32#+  , toInt#+  , toFloat#+  )+where++import           Unwitch.Errors+import           Unwitch.Constant+import qualified Data.Bits as Bits+import           Data.Word+import           Data.Int+import           Numeric.Natural (Natural)+import           Prelude hiding (toInteger)+import           GHC.Exts (Int(..), Word(..), Float(..),+                           word32ToWord#, word2Int#,+                           wordToWord8#, word8ToWord#,+                           wordToWord16#, word16ToWord#,+                           intToInt8#, int8ToInt#,+                           intToInt16#, int16ToInt#,+                           intToInt32#, int32ToInt#,+                           int2Float#,+                           eqWord#, leWord#, (==#), (>=#))+import           GHC.Int (Int8(..), Int16(..), Int32(..))+import           GHC.Word (Word8(..), Word16(..), Word32(..))++toWord8 :: Word32 -> Maybe Word8+toWord8 = Bits.toIntegralSized++toWord16 :: Word32 -> Maybe Word16+toWord16 = Bits.toIntegralSized++toWord64 :: Word32 -> Word64+toWord64 = fromIntegral++toWord :: Word32 -> Maybe Word+toWord = Bits.toIntegralSized++toNatural :: Word32 -> Natural+toNatural = fromIntegral++toInt8 :: Word32 -> Maybe Int8+toInt8 = Bits.toIntegralSized++toInt16 :: Word32 -> Maybe Int16+toInt16 = Bits.toIntegralSized++toInt32 :: Word32 -> Maybe Int32+toInt32 = Bits.toIntegralSized++toInt64 :: Word32 -> Int64+toInt64 = fromIntegral++toInt :: Word32 -> Maybe Int+toInt = Bits.toIntegralSized++toInteger :: Word32 -> Integer+toInteger = fromIntegral++toFloat :: Word32 -> Either Overflows Float+toFloat x = if+  | x > maxIntegralRepFloat -> Left Overflow+  | otherwise               -> Right $ fromIntegral x++toDouble :: Word32 -> Double+toDouble = fromIntegral++-- | Unsigned narrowing, roundtrip at Word#+toWord8# :: Word32 -> (# Word8 | (# #) #)+toWord8# (W32# w32#) =+  let w# = word32ToWord# w32#+      n# = wordToWord8# w#+  in case word8ToWord# n# `eqWord#` w# of+    1# -> (# W8# n# | #)+    _  -> (# | (# #) #)++-- | Unsigned narrowing, roundtrip at Word#+toWord16# :: Word32 -> (# Word16 | (# #) #)+toWord16# (W32# w32#) =+  let w# = word32ToWord# w32#+      n# = wordToWord16# w#+  in case word16ToWord# n# `eqWord#` w# of+    1# -> (# W16# n# | #)+    _  -> (# | (# #) #)++-- | Word32 always fits in Word (Word is at least 32 bits)+toWord# :: Word32 -> (# Word | (# #) #)+toWord# (W32# w32#) = (# W# (word32ToWord# w32#) | #)++-- | Unsigned->signed, source fits in Int#, roundtrip at Int#+toInt8# :: Word32 -> (# Int8 | (# #) #)+toInt8# (W32# w32#) =+  let i# = word2Int# (word32ToWord# w32#)+      n# = intToInt8# i#+  in case int8ToInt# n# ==# i# of+    1# -> (# I8# n# | #)+    _  -> (# | (# #) #)++-- | Unsigned->signed, source fits in Int#, roundtrip at Int#+toInt16# :: Word32 -> (# Int16 | (# #) #)+toInt16# (W32# w32#) =+  let i# = word2Int# (word32ToWord# w32#)+      n# = intToInt16# i#+  in case int16ToInt# n# ==# i# of+    1# -> (# I16# n# | #)+    _  -> (# | (# #) #)++-- | Unsigned->signed, roundtrip at Int#+toInt32# :: Word32 -> (# Int32 | (# #) #)+toInt32# (W32# w32#) =+  let i# = word2Int# (word32ToWord# w32#)+      n# = intToInt32# i#+  in case int32ToInt# n# ==# i# of+    1# -> (# I32# n# | #)+    _  -> (# | (# #) #)++-- | Word32 fits in non-negative Int on all platforms, check via sign bit+toInt# :: Word32 -> (# Int | (# #) #)+toInt# (W32# w32#) =+  let i# = word2Int# (word32ToWord# w32#)+  in case i# >=# 0# of+    1# -> (# I# i# | #)+    _  -> (# | (# #) #)++-- | Bounds-checked float conversion+toFloat# :: Word32 -> (# Overflows | Float #)+toFloat# (W32# w32#) = case leWord# (word32ToWord# w32#) 16777215## of+  1# -> (# | F# (int2Float# (word2Int# (word32ToWord# w32#))) #)+  _  -> (# Overflow | #)
+ src/Unwitch/Convert/Word64.hs view
@@ -0,0 +1,178 @@+module Unwitch.Convert.Word64+  ( toWord8+  , toWord16+  , toWord32+  , toWord+  , toNatural+  , toInt8+  , toInt16+  , toInt32+  , toInt64+  , toInt+  , toInteger+  , toFloat+  , toDouble+  , toWord8#+  , toWord16#+  , toWord32#+  , toWord#+  , toInt8#+  , toInt16#+  , toInt32#+  , toInt64#+  , toInt#+  , toFloat#+  , toDouble#+  )+where++import           Unwitch.Errors+import           Unwitch.Constant+import qualified Data.Bits as Bits+import           Data.Word+import           Data.Int+import           Numeric.Natural (Natural)+import           Prelude hiding (toInteger)+import           GHC.Exts (Int(..), Word(..), Float(..), Double(..),+                           word64ToWord#, wordToWord64#,+                           word2Int#,+                           wordToWord8#, word8ToWord#,+                           wordToWord16#, word16ToWord#,+                           wordToWord32#, word32ToWord#,+                           intToInt8#, intToInt16#, intToInt32#,+                           intToInt64#,+                           int2Float#, int2Double#,+                           eqWord64#, leWord64#,+                           (>=#))+import           GHC.Int (Int8(..), Int16(..), Int32(..), Int64(..))+import           GHC.Word (Word8(..), Word16(..), Word32(..), Word64(..))++toWord8 :: Word64 -> Maybe Word8+toWord8 = Bits.toIntegralSized++toWord16 :: Word64 -> Maybe Word16+toWord16 = Bits.toIntegralSized++toWord32 :: Word64 -> Maybe Word32+toWord32 = Bits.toIntegralSized++toWord :: Word64 -> Maybe Word+toWord = Bits.toIntegralSized++toNatural :: Word64 -> Natural+toNatural = fromIntegral++toInt8 :: Word64 -> Maybe Int8+toInt8 = Bits.toIntegralSized++toInt16 :: Word64 -> Maybe Int16+toInt16 = Bits.toIntegralSized++toInt32 :: Word64 -> Maybe Int32+toInt32 = Bits.toIntegralSized++toInt64 :: Word64 -> Maybe Int64+toInt64 = Bits.toIntegralSized++toInt :: Word64 -> Maybe Int+toInt = Bits.toIntegralSized++toInteger :: Word64 -> Integer+toInteger = fromIntegral++toFloat :: Word64 -> Either Overflows Float+toFloat x = if+  | x > maxIntegralRepFloat -> Left Overflow+  | otherwise               -> Right $ fromIntegral x++toDouble :: Word64 -> Either Overflows Double+toDouble x = if+  | x > maxIntegralRepDouble -> Left Overflow+  | otherwise                -> Right $ fromIntegral x++-- | Unsigned narrowing via Word64# comparison+toWord8# :: Word64 -> (# Word8 | (# #) #)+toWord8# (W64# w64#) =+  let w# = word64ToWord# w64#+      n# = wordToWord8# w#+  in case eqWord64# (wordToWord64# (word8ToWord# n#)) w64# of+    1# -> (# W8# n# | #)+    _  -> (# | (# #) #)++-- | Unsigned narrowing via Word64# comparison+toWord16# :: Word64 -> (# Word16 | (# #) #)+toWord16# (W64# w64#) =+  let w# = word64ToWord# w64#+      n# = wordToWord16# w#+  in case eqWord64# (wordToWord64# (word16ToWord# n#)) w64# of+    1# -> (# W16# n# | #)+    _  -> (# | (# #) #)++-- | Unsigned narrowing via Word64# comparison+toWord32# :: Word64 -> (# Word32 | (# #) #)+toWord32# (W64# w64#) =+  let w# = word64ToWord# w64#+      n# = wordToWord32# w#+  in case eqWord64# (wordToWord64# (word32ToWord# n#)) w64# of+    1# -> (# W32# n# | #)+    _  -> (# | (# #) #)++-- | Roundtrip check at Word64# level+toWord# :: Word64 -> (# Word | (# #) #)+toWord# (W64# w64#) =+  let w# = word64ToWord# w64#+  in case eqWord64# (wordToWord64# w#) w64# of+    1# -> (# W# w# | #)+    _  -> (# | (# #) #)++-- | Check upper bound at Word64# level+toInt8# :: Word64 -> (# Int8 | (# #) #)+toInt8# (W64# w64#) = case leWord64# w64# (wordToWord64# 127##) of+  1# -> (# I8# (intToInt8# (word2Int# (word64ToWord# w64#))) | #)+  _  -> (# | (# #) #)++-- | Check upper bound for Int16+toInt16# :: Word64 -> (# Int16 | (# #) #)+toInt16# (W64# w64#) = case leWord64# w64# (wordToWord64# 32767##) of+  1# -> (# I16# (intToInt16# (word2Int# (word64ToWord# w64#))) | #)+  _  -> (# | (# #) #)++-- | Check upper bound for Int32+toInt32# :: Word64 -> (# Int32 | (# #) #)+toInt32# (W64# w64#) = case leWord64# w64# (wordToWord64# 2147483647##) of+  1# -> (# I32# (intToInt32# (word2Int# (word64ToWord# w64#))) | #)+  _  -> (# | (# #) #)++-- | Check high bit not set for Int64+toInt64# :: Word64 -> (# Int64 | (# #) #)+toInt64# (W64# w64#) =+  let w# = word64ToWord# w64#+      i# = word2Int# w#+  in case i# >=# 0# of+    1# -> case eqWord64# (wordToWord64# w#) w64# of+      1# -> (# I64# (intToInt64# i#) | #)+      _  -> (# | (# #) #)+    _  -> (# | (# #) #)++-- | Check fits in non-negative Int range+toInt# :: Word64 -> (# Int | (# #) #)+toInt# (W64# w64#) =+  let w# = word64ToWord# w64#+      i# = word2Int# w#+  in case i# >=# 0# of+    1# -> case eqWord64# (wordToWord64# w#) w64# of+      1# -> (# I# i# | #)+      _  -> (# | (# #) #)+    _  -> (# | (# #) #)++-- | Bounds-checked float conversion at Word64# level+toFloat# :: Word64 -> (# Overflows | Float #)+toFloat# (W64# w64#) = case leWord64# w64# (wordToWord64# 16777215##) of+  1# -> (# | F# (int2Float# (word2Int# (word64ToWord# w64#))) #)+  _  -> (# Overflow | #)++-- | Bounds-checked double conversion at Word64# level+toDouble# :: Word64 -> (# Overflows | Double #)+toDouble# (W64# w64#) = case leWord64# w64# (wordToWord64# 9007199254740991##) of+  1# -> (# | D# (int2Double# (word2Int# (word64ToWord# w64#))) #)+  _  -> (# Overflow | #)
+ src/Unwitch/Convert/Word8.hs view
@@ -0,0 +1,75 @@+module Unwitch.Convert.Word8+  ( toWord16+  , toWord32+  , toWord64+  , toWord+  , toNatural+  , toInt8+  , toInt16+  , toInt32+  , toInt64+  , toInt+  , toInteger+  , toFloat+  , toDouble+  , toInt8#+  )+where++import qualified Data.Bits as Bits+import           Data.Word+import           Data.Int+import           Numeric.Natural (Natural)+import           Prelude hiding (toInteger)+import           GHC.Exts (word8ToWord#, word2Int#, intToInt8#, int8ToInt#,+                           (==#))+import           GHC.Int (Int8(..))+import           GHC.Word (Word8(..))++toWord16 :: Word8 -> Word16+toWord16 = fromIntegral++toWord32 :: Word8 -> Word32+toWord32 = fromIntegral++toWord64 :: Word8 -> Word64+toWord64 = fromIntegral++toWord :: Word8 -> Word+toWord = fromIntegral++toNatural :: Word8 -> Natural+toNatural = fromIntegral++toInt8 :: Word8 -> Maybe Int8+toInt8 = Bits.toIntegralSized++toInt16 :: Word8 -> Int16+toInt16 = fromIntegral++toInt32 :: Word8 -> Int32+toInt32 = fromIntegral++toInt64 :: Word8 -> Int64+toInt64 = fromIntegral++toInt :: Word8 -> Int+toInt = fromIntegral++toInteger :: Word8 -> Integer+toInteger = fromIntegral++toFloat :: Word8 -> Float+toFloat = fromIntegral++toDouble :: Word8 -> Double+toDouble = fromIntegral++-- | Unsigned->signed, source fits in Int#, roundtrip at Int#+toInt8# :: Word8 -> (# Int8 | (# #) #)+toInt8# (W8# w8#) =+  let i# = word2Int# (word8ToWord# w8#)+      n# = intToInt8# i#+  in case int8ToInt# n# ==# i# of+    1# -> (# I8# n# | #)+    _  -> (# | (# #) #)
+ src/Unwitch/Errors.hs view
@@ -0,0 +1,8 @@+module Unwitch.Errors+  ( Overflows(..)+  )+where++data Overflows = Overflow+               | Underflow+  deriving (Show, Eq)
+ src/Unwitch/TryEvaluate.hs view
@@ -0,0 +1,11 @@+module Unwitch.TryEvaluate+  ( tryEvaluate+  )+where++import Control.Exception (Exception, evaluate, try)+import System.IO.Unsafe (unsafePerformIO)++tryEvaluate :: Exception e => a -> Either e a+tryEvaluate = unsafePerformIO . try . evaluate+{-# NOINLINE tryEvaluate #-}
+ test/Spec.hs view
@@ -0,0 +1,36 @@+module Main where++import Test.Hspec+import qualified Test.Convert.Int8Spec+import qualified Test.Convert.Int16Spec+import qualified Test.Convert.Int32Spec+import qualified Test.Convert.Int64Spec+import qualified Test.Convert.IntSpec+import qualified Test.Convert.Word8Spec+import qualified Test.Convert.Word16Spec+import qualified Test.Convert.Word32Spec+import qualified Test.Convert.Word64Spec+import qualified Test.Convert.WordSpec+import qualified Test.Convert.NaturalSpec+import qualified Test.Convert.IntegerSpec+import qualified Test.Convert.FloatSpec+import qualified Test.Convert.DoubleSpec+import qualified Test.Convert.PropertySpec++main :: IO ()+main = hspec $ do+  Test.Convert.Int8Spec.spec+  Test.Convert.Int16Spec.spec+  Test.Convert.Int32Spec.spec+  Test.Convert.Int64Spec.spec+  Test.Convert.IntSpec.spec+  Test.Convert.Word8Spec.spec+  Test.Convert.Word16Spec.spec+  Test.Convert.Word32Spec.spec+  Test.Convert.Word64Spec.spec+  Test.Convert.WordSpec.spec+  Test.Convert.NaturalSpec.spec+  Test.Convert.IntegerSpec.spec+  Test.Convert.FloatSpec.spec+  Test.Convert.DoubleSpec.spec+  Test.Convert.PropertySpec.spec
+ test/Test/Convert/ByteStringSpec.hs view
@@ -0,0 +1,72 @@+module Test.Convert.ByteStringSpec (spec) where++import Test.Hspec+import Data.ByteString qualified as BS+import Data.Word (Word8)+import qualified Unwitch.Convert.ByteString as ByteString+import qualified Unwitch.Convert.LazyByteString as LazyByteString+import qualified Unwitch.Convert.ShortByteString as ShortByteString++spec :: Spec+spec = describe "Unwitch.Convert.ByteString" $ do++  describe "toLazyByteString / toByteString round-trip" $+    it "round-trips" $+      let bs = BS.pack [104, 101, 108, 108, 111]+      in LazyByteString.toByteString (ByteString.toLazyByteString bs) `shouldBe` bs++  describe "toShortByteString round-trip" $+    it "round-trips with ShortByteString.toByteString" $+      let bs = BS.pack [1, 2, 3]+      in ShortByteString.toByteString (ByteString.toShortByteString bs) `shouldBe` bs++  describe "toWord8s / fromWord8s" $ do+    it "round-trips" $+      let ws = [72, 105] :: [Word8]+      in ByteString.toWord8s (ByteString.fromWord8s ws) `shouldBe` ws+    it "empty list" $+      ByteString.toWord8s (ByteString.fromWord8s []) `shouldBe` ([] :: [Word8])++  describe "toTextLatin1" $+    it "decodes latin1 bytes" $+      let bs = BS.pack [0xE9, 0xE8] -- e-acute, e-grave+      in ByteString.toTextLatin1 bs `shouldBe` "\x00E9\x00E8"++  describe "toTextUtf8" $ do+    it "decodes valid UTF-8" $+      ByteString.toTextUtf8 "hello" `shouldBe` Right "hello"+    it "fails on invalid UTF-8" $+      let bs = BS.pack [0xFF, 0xFE]+      in case ByteString.toTextUtf8 bs of+           Left _  -> pure ()+           Right _ -> expectationFailure "expected Left"++  describe "toTextUtf16LE" $ do+    it "decodes valid UTF-16LE" $+      let bs = BS.pack [0x41, 0x00] -- 'A' in UTF-16LE+      in ByteString.toTextUtf16LE bs `shouldBe` Right "A"+    it "fails on truncated input" $+      let bs = BS.pack [0x41] -- odd byte count+      in case ByteString.toTextUtf16LE bs of+           Left _  -> pure ()+           Right _ -> expectationFailure "expected Left"++  describe "toTextUtf16BE" $+    it "decodes valid UTF-16BE" $+      let bs = BS.pack [0x00, 0x42] -- 'B' in UTF-16BE+      in ByteString.toTextUtf16BE bs `shouldBe` Right "B"++  describe "toTextUtf32LE" $+    it "decodes valid UTF-32LE" $+      let bs = BS.pack [0x43, 0x00, 0x00, 0x00] -- 'C' in UTF-32LE+      in ByteString.toTextUtf32LE bs `shouldBe` Right "C"++  describe "toTextUtf32BE" $ do+    it "decodes valid UTF-32BE" $+      let bs = BS.pack [0x00, 0x00, 0x00, 0x44] -- 'D' in UTF-32BE+      in ByteString.toTextUtf32BE bs `shouldBe` Right "D"+    it "fails on invalid input" $+      let bs = BS.pack [0x01] -- not 4-byte aligned+      in case ByteString.toTextUtf32BE bs of+           Left _  -> pure ()+           Right _ -> expectationFailure "expected Left"
+ test/Test/Convert/CharSpec.hs view
@@ -0,0 +1,41 @@+module Test.Convert.CharSpec (spec) where++import Test.Hspec+import qualified Unwitch.Convert.Char as Char++spec :: Spec+spec = describe "Unwitch.Convert.Char" $ do++  describe "toInt" $ do+    it "converts 'A' to 65" $+      Char.toInt 'A' `shouldBe` 65+    it "converts '0' to 48" $+      Char.toInt '0' `shouldBe` 48++  describe "toWord" $ do+    it "converts 'A' to 65" $+      Char.toWord 'A' `shouldBe` (65 :: Word)+    it "converts null to 0" $+      Char.toWord '\0' `shouldBe` (0 :: Word)++  describe "fromInt" $ do+    it "succeeds for valid codepoint" $+      Char.fromInt 65 `shouldBe` Just 'A'+    it "fails for negative" $+      Char.fromInt (-1) `shouldBe` Nothing+    it "fails for value > 0x10FFFF" $+      Char.fromInt 0x110000 `shouldBe` Nothing+    it "fails for surrogate" $+      Char.fromInt 0xD800 `shouldBe` Nothing++  describe "fromWord" $ do+    it "succeeds for valid codepoint" $+      Char.fromWord 65 `shouldBe` Just 'A'+    it "fails for value > 0x10FFFF" $+      Char.fromWord 0x110000 `shouldBe` Nothing+    it "fails for surrogate" $+      Char.fromWord 0xD800 `shouldBe` Nothing++  describe "round-trip" $+    it "fromInt . toInt == Just" $+      Char.fromInt (Char.toInt 'Z') `shouldBe` Just 'Z'
+ test/Test/Convert/ComplexSpec.hs view
@@ -0,0 +1,24 @@+module Test.Convert.ComplexSpec (spec) where++import Test.Hspec+import Data.Complex (Complex((:+)))+import qualified Unwitch.Convert.Complex as Complex++spec :: Spec+spec = describe "Unwitch.Convert.Complex" $ do++  describe "fromReal" $ do+    it "wraps with zero imaginary part" $+      Complex.fromReal (3.0 :: Double) `shouldBe` (3.0 :+ 0.0)+    it "wraps zero" $+      Complex.fromReal (0 :: Int) `shouldBe` (0 :+ 0)++  describe "toReal" $ do+    it "succeeds when imaginary is 0" $+      Complex.toReal (5.0 :+ 0.0 :: Complex Double) `shouldBe` Just 5.0+    it "fails when imaginary is nonzero" $+      Complex.toReal (5.0 :+ 1.0 :: Complex Double) `shouldBe` Nothing++  describe "round-trip" $+    it "toReal . fromReal == Just" $+      Complex.toReal (Complex.fromReal (42.0 :: Double)) `shouldBe` Just 42.0
+ test/Test/Convert/DoubleSpec.hs view
@@ -0,0 +1,87 @@+module Test.Convert.DoubleSpec (spec) where++import Test.Hspec+import Data.Int+import Data.Word+import Numeric.Natural (Natural)+import Unwitch.Errors+import qualified Unwitch.Convert.Double as Double++spec :: Spec+spec = describe "Unwitch.Convert.Double" $ do++  describe "toFloat" $+    it "converts 0" $+      Double.toFloat 0.0 `shouldBe` 0.0++  describe "toRational" $ do+    it "converts finite value" $+      Double.toRational 1.5 `shouldBe` Right (3 / 2)+    it "rejects NaN" $+      Double.toRational (0 / 0 :: Double) `shouldSatisfy` isLeft+    it "rejects positive Infinity" $+      Double.toRational (1 / 0 :: Double) `shouldBe` Left (Double.IsInf Overflow)+    it "rejects negative Infinity" $+      Double.toRational ((-1) / 0 :: Double) `shouldBe` Left (Double.IsInf Underflow)++  describe "toInteger" $ do+    it "converts whole number" $+      Double.toInteger 42.0 `shouldBe` Right 42+    it "rejects fractional" $+      Double.toInteger 1.5 `shouldSatisfy` isLeft++  describe "toInt8" $ do+    it "converts in-range" $+      Double.toInt8 42.0 `shouldBe` Right (42 :: Int8)+    it "rejects out-of-range" $+      Double.toInt8 200.0 `shouldSatisfy` isLeft++  describe "toInt16" $+    it "converts in-range" $+      Double.toInt16 1000.0 `shouldBe` Right (1000 :: Int16)++  describe "toInt32" $+    it "converts in-range" $+      Double.toInt32 100000.0 `shouldBe` Right (100000 :: Int32)++  describe "toInt64" $+    it "converts in-range" $+      Double.toInt64 100000.0 `shouldBe` Right (100000 :: Int64)++  describe "toInt" $+    it "converts in-range" $+      Double.toInt 42.0 `shouldBe` Right (42 :: Int)++  describe "toWord8" $ do+    it "converts in-range" $+      Double.toWord8 200.0 `shouldBe` Right (200 :: Word8)+    it "rejects negative" $+      Double.toWord8 (-1.0) `shouldSatisfy` isLeft++  describe "toWord16" $+    it "converts in-range" $+      Double.toWord16 1000.0 `shouldBe` Right (1000 :: Word16)++  describe "toWord32" $+    it "converts in-range" $+      Double.toWord32 100000.0 `shouldBe` Right (100000 :: Word32)++  describe "toWord64" $+    it "converts in-range" $+      Double.toWord64 100000.0 `shouldBe` Right (100000 :: Word64)++  describe "toWord" $+    it "converts in-range" $+      Double.toWord 42.0 `shouldBe` Right (42 :: Word)++  describe "toNatural" $ do+    it "converts positive whole number" $+      Double.toNatural 42.0 `shouldBe` Right (42 :: Natural)+    it "rejects negative" $+      Double.toNatural (-1.0) `shouldSatisfy` isLeft+    it "rejects fractional" $+      Double.toNatural 1.5 `shouldSatisfy` isLeft++isLeft :: Either a b -> Bool+isLeft (Left _) = True+isLeft (Right _) = False
+ test/Test/Convert/FixedSpec.hs view
@@ -0,0 +1,38 @@+module Test.Convert.FixedSpec (spec) where++import Test.Hspec+import Data.Fixed (Fixed, E2, E6)+import Data.Ratio (numerator)+import qualified Unwitch.Convert.Fixed as Fixed++spec :: Spec+spec = describe "Unwitch.Convert.Fixed" $ do++  describe "fromInteger" $ do+    it "converts integer to Fixed E2" $+      Fixed.fromInteger 42 `shouldBe` (42.0 :: Fixed E2)+    it "converts zero" $+      Fixed.fromInteger 0 `shouldBe` (0.0 :: Fixed E2)++  describe "toInteger" $ do+    it "succeeds for whole Fixed values" $+      Fixed.toInteger (42.0 :: Fixed E2) `shouldBe` Just 42+    it "fails for fractional Fixed values" $+      Fixed.toInteger (1.50 :: Fixed E2) `shouldBe` Nothing++  describe "toRational" $ do+    it "converts Fixed to exact Rational" $+      Fixed.toRational (1.50 :: Fixed E2) `shouldBe` (3 / 2)+    it "round-trips whole values through toInteger" $+      let f = 10.0 :: Fixed E2+      in Fixed.toInteger f `shouldBe` Just (numerator (Fixed.toRational f))++  describe "toFixed" $ do+    it "succeeds for compatible resolutions" $+      Fixed.toFixed (1.50 :: Fixed E2) `shouldBe` Just (1.500000 :: Fixed E6)+    it "fails for incompatible resolutions" $+      -- 0.01 as E2 is representable, but converting to E6 should also work+      -- However 1/3 cannot be represented in either, so let's test a value+      -- that exists in E6 but not in E2:+      -- 1.123 in E6 -> 1.12 in E2 loses precision+      Fixed.toFixed (1.123000 :: Fixed E6) `shouldBe` (Nothing :: Maybe (Fixed E2))
+ test/Test/Convert/FloatSpec.hs view
@@ -0,0 +1,91 @@+module Test.Convert.FloatSpec (spec) where++import Test.Hspec+import Data.Int+import Data.Word+import Numeric.Natural (Natural)+import Unwitch.Errors+import qualified Unwitch.Convert.Float as Float++spec :: Spec+spec = describe "Unwitch.Convert.Float" $ do++  describe "toDouble (infallible)" $ do+    it "converts 0" $+      Float.toDouble 0.0 `shouldBe` 0.0+    it "converts 1.5" $+      Float.toDouble 1.5 `shouldBe` 1.5++  describe "toRational" $ do+    it "converts finite value" $+      Float.toRational 1.5 `shouldBe` Right (3 / 2)+    it "rejects NaN" $+      Float.toRational (0 / 0 :: Float) `shouldSatisfy` isLeft+    it "rejects positive Infinity" $+      Float.toRational (1 / 0 :: Float) `shouldBe` Left (Float.IsInf Overflow)+    it "rejects negative Infinity" $+      Float.toRational ((-1) / 0 :: Float) `shouldBe` Left (Float.IsInf Underflow)++  describe "toInteger" $ do+    it "converts whole number" $+      Float.toInteger 42.0 `shouldBe` Right 42+    it "rejects fractional" $+      Float.toInteger 1.5 `shouldSatisfy` isLeft+    it "rejects NaN" $+      Float.toInteger (0 / 0 :: Float) `shouldSatisfy` isLeft++  describe "toInt8" $ do+    it "converts in-range" $+      Float.toInt8 42.0 `shouldBe` Right (42 :: Int8)+    it "rejects out-of-range" $+      Float.toInt8 200.0 `shouldSatisfy` isLeft+    it "rejects fractional" $+      Float.toInt8 1.5 `shouldSatisfy` isLeft++  describe "toInt16" $+    it "converts in-range" $+      Float.toInt16 1000.0 `shouldBe` Right (1000 :: Int16)++  describe "toInt32" $+    it "converts in-range" $+      Float.toInt32 100000.0 `shouldBe` Right (100000 :: Int32)++  describe "toInt64" $+    it "converts in-range" $+      Float.toInt64 100000.0 `shouldBe` Right (100000 :: Int64)++  describe "toInt" $+    it "converts in-range" $+      Float.toInt 42.0 `shouldBe` Right (42 :: Int)++  describe "toWord8" $ do+    it "converts in-range" $+      Float.toWord8 200.0 `shouldBe` Right (200 :: Word8)+    it "rejects negative" $+      Float.toWord8 (-1.0) `shouldSatisfy` isLeft++  describe "toWord16" $+    it "converts in-range" $+      Float.toWord16 1000.0 `shouldBe` Right (1000 :: Word16)++  describe "toWord32" $+    it "converts in-range" $+      Float.toWord32 100000.0 `shouldBe` Right (100000 :: Word32)++  describe "toWord64" $+    it "converts in-range" $+      Float.toWord64 100000.0 `shouldBe` Right (100000 :: Word64)++  describe "toWord" $+    it "converts in-range" $+      Float.toWord 42.0 `shouldBe` Right (42 :: Word)++  describe "toNatural" $ do+    it "converts positive whole number" $+      Float.toNatural 42.0 `shouldBe` Right (42 :: Natural)+    it "rejects negative" $+      Float.toNatural (-1.0) `shouldSatisfy` isLeft++isLeft :: Either a b -> Bool+isLeft (Left _) = True+isLeft (Right _) = False
+ test/Test/Convert/Int16Spec.hs view
@@ -0,0 +1,39 @@+module Test.Convert.Int16Spec (spec) where++import Test.Hspec+import Data.Int+import Data.Word+import qualified Unwitch.Convert.Int16 as Int16++-- Property tests cover: toInt8 narrowing, toInt32/64 round-trips,+-- toWord16 (same-width), toNatural iff negative, toFloat/toDouble.+-- Kept: toInt (no property), cross-width signed-to-unsigned.++spec :: Spec+spec = describe "Unwitch.Convert.Int16" $ do++  describe "toInt (infallible)" $+    it "widens maxBound" $+      Int16.toInt maxBound `shouldBe` 32767++  describe "toWord8 (fallible)" $ do+    it "rejects negative" $+      Int16.toWord8 (-1 :: Int16) `shouldBe` Nothing+    it "rejects too large" $+      Int16.toWord8 (256 :: Int16) `shouldBe` Nothing+    it "converts in range" $+      Int16.toWord8 (255 :: Int16) `shouldBe` Just (255 :: Word8)++  describe "toWord32 (fallible)" $ do+    it "rejects negative" $+      Int16.toWord32 (-1 :: Int16) `shouldBe` Nothing+    it "converts maxBound" $+      Int16.toWord32 (32767 :: Int16) `shouldBe` Just (32767 :: Word32)++  describe "toWord64 (fallible)" $+    it "rejects negative" $+      Int16.toWord64 (-1 :: Int16) `shouldBe` Nothing++  describe "toWord (fallible)" $+    it "rejects negative" $+      Int16.toWord (-1 :: Int16) `shouldBe` Nothing
+ test/Test/Convert/Int32Spec.hs view
@@ -0,0 +1,43 @@+module Test.Convert.Int32Spec (spec) where++import Test.Hspec+import Data.Int+import Data.Word+import qualified Unwitch.Convert.Int32 as Int32++-- Property tests cover: toInt16 narrowing, toInt64 round-trip,+-- toWord32 (same-width), toNatural iff negative,+-- toFloat range check, toDouble preserve value.+-- Kept: toInt8 (no property), toInt (no property),+-- cross-width signed-to-unsigned.++spec :: Spec+spec = describe "Unwitch.Convert.Int32" $ do++  describe "toInt8 (fallible)" $ do+    it "narrows in-range" $+      Int32.toInt8 (100 :: Int32) `shouldBe` Just (100 :: Int8)+    it "rejects out-of-range" $+      Int32.toInt8 (200 :: Int32) `shouldBe` Nothing++  describe "toInt (fallible via toIntegralSized)" $+    it "converts 0" $+      Int32.toInt 0 `shouldBe` Just 0++  describe "toWord8 (fallible)" $ do+    it "rejects negative" $+      Int32.toWord8 (-1 :: Int32) `shouldBe` Nothing+    it "converts in-range" $+      Int32.toWord8 (255 :: Int32) `shouldBe` Just (255 :: Word8)++  describe "toWord16 (fallible)" $+    it "rejects too large" $+      Int32.toWord16 (70000 :: Int32) `shouldBe` Nothing++  describe "toWord64 (fallible)" $+    it "rejects negative" $+      Int32.toWord64 (-1 :: Int32) `shouldBe` Nothing++  describe "toWord (fallible)" $+    it "rejects negative" $+      Int32.toWord (-1 :: Int32) `shouldBe` Nothing
+ test/Test/Convert/Int64Spec.hs view
@@ -0,0 +1,44 @@+module Test.Convert.Int64Spec (spec) where++import Test.Hspec+import Data.Int+import qualified Unwitch.Convert.Int64 as Int64++-- Property tests cover: toInt32 narrowing, toInteger widening,+-- toWord64 (same-width), toNatural iff negative,+-- toFloat range check, toDouble range check.+-- Kept: toInt8/toInt16 (no property), toInt (no property),+-- cross-width signed-to-unsigned.++spec :: Spec+spec = describe "Unwitch.Convert.Int64" $ do++  describe "toInt8 (fallible)" $ do+    it "narrows in-range" $+      Int64.toInt8 (42 :: Int64) `shouldBe` Just (42 :: Int8)+    it "rejects out-of-range" $+      Int64.toInt8 (200 :: Int64) `shouldBe` Nothing++  describe "toInt16 (fallible)" $+    it "rejects out-of-range" $+      Int64.toInt16 (40000 :: Int64) `shouldBe` Nothing++  describe "toInt (fallible)" $+    it "converts 0" $+      Int64.toInt 0 `shouldBe` Just 0++  describe "toWord8 (fallible)" $+    it "rejects negative" $+      Int64.toWord8 (-1 :: Int64) `shouldBe` Nothing++  describe "toWord16 (fallible)" $+    it "rejects negative" $+      Int64.toWord16 (-1 :: Int64) `shouldBe` Nothing++  describe "toWord32 (fallible)" $+    it "rejects negative" $+      Int64.toWord32 (-1 :: Int64) `shouldBe` Nothing++  describe "toWord (fallible)" $+    it "rejects negative" $+      Int64.toWord (-1 :: Int64) `shouldBe` Nothing
+ test/Test/Convert/Int8Spec.hs view
@@ -0,0 +1,38 @@+module Test.Convert.Int8Spec (spec) where++import Test.Hspec+import Data.Int+import Data.Word+import qualified Unwitch.Convert.Int8 as Int8++-- Only cross-width signed-to-unsigned conversions that no property test covers.+-- All other Int8 conversions are fully covered by PropertySpec:+--   round-trips, toNatural iff negative, toFloat/toDouble preserve value,+--   path independence, signed-to-unsigned (same-width Int8->Word8).++spec :: Spec+spec = describe "Unwitch.Convert.Int8" $ do++  describe "toWord16 (fallible)" $ do+    it "rejects negative values" $+      Int8.toWord16 (-1 :: Int8) `shouldBe` Nothing+    it "converts maxBound" $+      Int8.toWord16 (127 :: Int8) `shouldBe` Just (127 :: Word16)++  describe "toWord32 (fallible)" $ do+    it "rejects negative values" $+      Int8.toWord32 (-1 :: Int8) `shouldBe` Nothing+    it "converts maxBound" $+      Int8.toWord32 (127 :: Int8) `shouldBe` Just (127 :: Word32)++  describe "toWord64 (fallible)" $ do+    it "rejects negative values" $+      Int8.toWord64 (-1 :: Int8) `shouldBe` Nothing+    it "converts maxBound" $+      Int8.toWord64 (127 :: Int8) `shouldBe` Just (127 :: Word64)++  describe "toWord (fallible)" $ do+    it "rejects negative values" $+      Int8.toWord (-1 :: Int8) `shouldBe` Nothing+    it "converts maxBound" $+      Int8.toWord (127 :: Int8) `shouldBe` Just (127 :: Word)
+ test/Test/Convert/IntSpec.hs view
@@ -0,0 +1,73 @@+module Test.Convert.IntSpec (spec) where++import Test.Hspec+import Data.Int+import Numeric.Natural (Natural)+import Unwitch.Errors+import qualified Unwitch.Convert.Int as Int++-- Property tests only cover Int.toNatural (iff negative).+-- All other Int conversions have no property coverage.++spec :: Spec+spec = describe "Unwitch.Convert.Int" $ do++  describe "toInt8 (fallible)" $ do+    it "narrows in-range" $+      Int.toInt8 (42 :: Int) `shouldBe` Just (42 :: Int8)+    it "rejects out-of-range" $+      Int.toInt8 (200 :: Int) `shouldBe` Nothing++  describe "toInt16 (fallible)" $+    it "rejects out-of-range" $+      Int.toInt16 (40000 :: Int) `shouldBe` Nothing++  describe "toInt32 (fallible)" $+    it "converts in-range" $+      Int.toInt32 (1000 :: Int) `shouldBe` Just (1000 :: Int32)++  describe "toInt64 (infallible)" $+    it "widens maxBound" $+      Int.toInt64 maxBound `shouldBe` fromIntegral (maxBound :: Int)++  describe "toInteger (infallible)" $+    it "converts maxBound" $+      Int.toInteger maxBound `shouldBe` fromIntegral (maxBound :: Int)++  describe "toWord8 (fallible)" $+    it "rejects negative" $+      Int.toWord8 (-1 :: Int) `shouldBe` Nothing++  describe "toWord16 (fallible)" $+    it "rejects negative" $+      Int.toWord16 (-1 :: Int) `shouldBe` Nothing++  describe "toWord32 (fallible)" $+    it "rejects negative" $+      Int.toWord32 (-1 :: Int) `shouldBe` Nothing++  describe "toWord64 (fallible)" $+    it "rejects negative" $+      Int.toWord64 (-1 :: Int) `shouldBe` Nothing++  describe "toWord (fallible)" $ do+    it "rejects negative" $+      Int.toWord (-1 :: Int) `shouldBe` Nothing+    it "converts 0" $+      Int.toWord 0 `shouldBe` Just (0 :: Word)++  describe "toNatural" $ do+    it "rejects negative with Underflow" $+      Int.toNatural (-1 :: Int) `shouldBe` Left Underflow+    it "converts 0" $+      Int.toNatural 0 `shouldBe` Right (0 :: Natural)++  describe "toFloat (range-checked)" $ do+    it "converts at boundary" $+      Int.toFloat (16777215 :: Int) `shouldBe` Right 16777215.0+    it "rejects above boundary" $+      Int.toFloat (16777216 :: Int) `shouldBe` Left Overflow++  describe "toDouble (range-checked)" $+    it "converts 0" $+      Int.toDouble 0 `shouldBe` Right (0.0 :: Double)
+ test/Test/Convert/IntegerSpec.hs view
@@ -0,0 +1,46 @@+module Test.Convert.IntegerSpec (spec) where++import Test.Hspec+import Data.Int+import Data.Word+import qualified Unwitch.Convert.Integer as Integer++-- Property tests cover: toDouble/toFloat/toNatural range checks,+-- toInt8/toWord8 narrowing (success implies fromIntegral).+-- Kept: toInt16, toInt32, toInt64, toInt, toWord16, toWord32,+-- toWord64, toWord (no direct property coverage).++spec :: Spec+spec = describe "Unwitch.Convert.Integer" $ do++  describe "toInt16 (fallible)" $+    it "rejects too large" $+      Integer.toInt16 32768 `shouldBe` Nothing++  describe "toInt32 (fallible)" $+    it "rejects too large" $+      Integer.toInt32 2147483648 `shouldBe` Nothing++  describe "toInt64 (fallible)" $+    it "converts in-range" $+      Integer.toInt64 42 `shouldBe` Just (42 :: Int64)++  describe "toInt (fallible)" $+    it "converts 0" $+      Integer.toInt 0 `shouldBe` Just (0 :: Int)++  describe "toWord16 (fallible)" $+    it "rejects too large" $+      Integer.toWord16 65536 `shouldBe` Nothing++  describe "toWord32 (fallible)" $+    it "rejects too large" $+      Integer.toWord32 4294967296 `shouldBe` Nothing++  describe "toWord64 (fallible)" $+    it "converts in-range" $+      Integer.toWord64 42 `shouldBe` Just (42 :: Word64)++  describe "toWord (fallible)" $+    it "converts 0" $+      Integer.toWord 0 `shouldBe` Just (0 :: Word)
+ test/Test/Convert/LazyByteStringSpec.hs view
@@ -0,0 +1,56 @@+module Test.Convert.LazyByteStringSpec (spec) where++import Test.Hspec+import Data.ByteString.Lazy qualified as LBS+import Data.Word (Word8)+import qualified Unwitch.Convert.LazyByteString as LazyByteString+import qualified Unwitch.Convert.ByteString as ByteString++spec :: Spec+spec = describe "Unwitch.Convert.LazyByteString" $ do++  describe "toByteString / toLazyByteString round-trip" $+    it "round-trips" $+      let lbs = LBS.pack [10, 20, 30]+      in ByteString.toLazyByteString (LazyByteString.toByteString lbs) `shouldBe` lbs++  describe "toWord8s / fromWord8s" $ do+    it "round-trips" $+      let ws = [1, 2, 3, 4] :: [Word8]+      in LazyByteString.toWord8s (LazyByteString.fromWord8s ws) `shouldBe` ws+    it "empty list" $+      LazyByteString.toWord8s (LazyByteString.fromWord8s []) `shouldBe` ([] :: [Word8])++  describe "toLazyTextLatin1" $+    it "decodes latin1 bytes" $+      let lbs = LBS.pack [0xFC] -- u-umlaut+      in LazyByteString.toLazyTextLatin1 lbs `shouldBe` "\x00FC"++  describe "toLazyTextUtf8" $ do+    it "decodes valid UTF-8" $+      LazyByteString.toLazyTextUtf8 "hello" `shouldBe` Right "hello"+    it "fails on invalid UTF-8" $+      let lbs = LBS.pack [0xFF, 0xFE]+      in case LazyByteString.toLazyTextUtf8 lbs of+           Left _  -> pure ()+           Right _ -> expectationFailure "expected Left"++  describe "toLazyTextUtf16LE" $+    it "decodes valid UTF-16LE" $+      let lbs = LBS.pack [0x41, 0x00]+      in LazyByteString.toLazyTextUtf16LE lbs `shouldBe` Right "A"++  describe "toLazyTextUtf16BE" $+    it "decodes valid UTF-16BE" $+      let lbs = LBS.pack [0x00, 0x42]+      in LazyByteString.toLazyTextUtf16BE lbs `shouldBe` Right "B"++  describe "toLazyTextUtf32LE" $+    it "decodes valid UTF-32LE" $+      let lbs = LBS.pack [0x43, 0x00, 0x00, 0x00]+      in LazyByteString.toLazyTextUtf32LE lbs `shouldBe` Right "C"++  describe "toLazyTextUtf32BE" $+    it "decodes valid UTF-32BE" $+      let lbs = LBS.pack [0x00, 0x00, 0x00, 0x44]+      in LazyByteString.toLazyTextUtf32BE lbs `shouldBe` Right "D"
+ test/Test/Convert/LazyTextSpec.hs view
@@ -0,0 +1,60 @@+module Test.Convert.LazyTextSpec (spec) where++import Test.Hspec+import Data.Text.Lazy qualified as LT+import qualified Unwitch.Convert.LazyText as LazyText+import qualified Unwitch.Convert.Text as Text+import qualified Unwitch.Convert.LazyByteString as LazyByteString++spec :: Spec+spec = describe "Unwitch.Convert.LazyText" $ do++  describe "toText / toLazyText round-trip" $+    it "round-trips" $+      let lt = LT.pack "hello world"+      in Text.toLazyText (LazyText.toText lt) `shouldBe` lt++  describe "toString / fromString" $ do+    it "round-trips" $+      let s = "test string"+      in LazyText.toString (LazyText.fromString s) `shouldBe` s+    it "handles unicode" $+      let s = "\x00E9\x00FC\x00F1"+      in LazyText.toString (LazyText.fromString s) `shouldBe` s++  describe "toLazyByteStringUtf8" $ do+    it "encodes ASCII" $+      LazyText.toLazyByteStringUtf8 "hello" `shouldBe` "hello"+    it "round-trips with LazyByteString.toLazyTextUtf8" $+      let lt = LT.pack "caf\x00E9"+      in LazyByteString.toLazyTextUtf8 (LazyText.toLazyByteStringUtf8 lt) `shouldBe` Right lt++  describe "toLazyByteStringUtf16LE" $+    it "produces valid output" $+      let lt = LT.pack "A"+      in LazyByteString.toLazyTextUtf16LE (LazyText.toLazyByteStringUtf16LE lt) `shouldBe` Right lt++  describe "toLazyByteStringUtf16BE" $+    it "produces valid output" $+      let lt = LT.pack "B"+      in LazyByteString.toLazyTextUtf16BE (LazyText.toLazyByteStringUtf16BE lt) `shouldBe` Right lt++  describe "toLazyByteStringUtf32LE" $+    it "produces valid output" $+      let lt = LT.pack "C"+      in LazyByteString.toLazyTextUtf32LE (LazyText.toLazyByteStringUtf32LE lt) `shouldBe` Right lt++  describe "toLazyByteStringUtf32BE" $+    it "produces valid output" $+      let lt = LT.pack "D"+      in LazyByteString.toLazyTextUtf32BE (LazyText.toLazyByteStringUtf32BE lt) `shouldBe` Right lt++  describe "toLazyByteStringLatin1" $ do+    it "succeeds for Latin1 range chars" $+      let lt = LT.pack "\x00E9\x00FC"+      in case LazyText.toLazyByteStringLatin1 lt of+           Just lbs -> LazyByteString.toLazyTextLatin1 lbs `shouldBe` lt+           Nothing  -> expectationFailure "expected Just"+    it "fails for chars above 0xFF" $+      let lt = LT.pack "\x0100"+      in LazyText.toLazyByteStringLatin1 lt `shouldBe` Nothing
+ test/Test/Convert/NaturalSpec.hs view
@@ -0,0 +1,69 @@+module Test.Convert.NaturalSpec (spec) where++import Test.Hspec+import Data.Int+import Data.Word+import Numeric.Natural (Natural)+import Unwitch.Errors+import qualified Unwitch.Convert.Natural as Natural++spec :: Spec+spec = describe "Unwitch.Convert.Natural" $ do++  describe "toWord8 (fallible)" $ do+    it "rejects too large" $+      Natural.toWord8 (256 :: Natural) `shouldBe` Nothing+    it "converts in-range" $+      Natural.toWord8 (255 :: Natural) `shouldBe` Just (255 :: Word8)++  describe "toWord16 (fallible)" $+    it "rejects too large" $+      Natural.toWord16 (65536 :: Natural) `shouldBe` Nothing++  describe "toWord32 (fallible)" $+    it "rejects too large" $+      Natural.toWord32 (4294967296 :: Natural) `shouldBe` Nothing++  describe "toWord64 (fallible)" $+    it "rejects too large" $+      Natural.toWord64 (18446744073709551616 :: Natural) `shouldBe` Nothing++  describe "toWord (fallible)" $+    it "converts 0" $+      Natural.toWord 0 `shouldBe` Just (0 :: Word)++  describe "toInt8 (fallible)" $+    it "rejects too large" $+      Natural.toInt8 (128 :: Natural) `shouldBe` Nothing++  describe "toInt16 (fallible)" $+    it "rejects too large" $+      Natural.toInt16 (32768 :: Natural) `shouldBe` Nothing++  describe "toInt32 (fallible)" $+    it "rejects too large" $+      Natural.toInt32 (2147483648 :: Natural) `shouldBe` Nothing++  describe "toInt64 (fallible)" $+    it "converts in-range" $+      Natural.toInt64 (100 :: Natural) `shouldBe` Just (100 :: Int64)++  describe "toInt (fallible)" $+    it "converts 0" $+      Natural.toInt 0 `shouldBe` Just (0 :: Int)++  describe "toInteger (infallible)" $+    it "converts large value" $+      Natural.toInteger (999999999999 :: Natural) `shouldBe` 999999999999++  describe "toFloat (range-checked)" $ do+    it "converts in-range" $+      Natural.toFloat (16777215 :: Natural) `shouldBe` Right 16777215.0+    it "rejects too large" $+      Natural.toFloat (16777216 :: Natural) `shouldBe` Left Overflow++  describe "toDouble (range-checked)" $ do+    it "converts in-range" $+      Natural.toDouble (9007199254740991 :: Natural) `shouldBe` Right 9007199254740991.0+    it "rejects too large" $+      Natural.toDouble (9007199254740992 :: Natural) `shouldBe` Left Overflow
+ test/Test/Convert/PropertySpec.hs view
@@ -0,0 +1,495 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Test.Convert.PropertySpec (spec) where++import Test.Hspec+import Test.Hspec.QuickCheck (prop)+import Data.Int+import Data.Word+import qualified Unwitch.Convert.Int8 as Int8+import qualified Unwitch.Convert.Int16 as Int16+import qualified Unwitch.Convert.Int32 as Int32+import qualified Unwitch.Convert.Int64 as Int64+import qualified Unwitch.Convert.Int as Int+import qualified Unwitch.Convert.Word8 as Word8+import qualified Unwitch.Convert.Word16 as Word16+import qualified Unwitch.Convert.Word32 as Word32+import qualified Unwitch.Convert.Word64 as Word64+import qualified Unwitch.Convert.Natural as Natural+import qualified Unwitch.Convert.Integer as Integer+import qualified Unwitch.Convert.Word as Word+import qualified Unwitch.Convert.Char as Char+import qualified Unwitch.Convert.Float as Float+import qualified Unwitch.Convert.Double as Double+import Numeric.Natural (Natural)++spec :: Spec+spec = describe "Property tests" $ do++  describe "Round-trip: widen then narrow recovers original" $ do+    prop "Int8 -> Int16 -> Int8" $ \(x :: Int8) ->+      Int16.toInt8 (Int8.toInt16 x) `shouldBe` Just x++    prop "Int8 -> Int32 -> Int8" $ \(x :: Int8) ->+      Int32.toInt8 (Int8.toInt32 x) `shouldBe` Just x++    prop "Int8 -> Int64 -> Int8" $ \(x :: Int8) ->+      Int64.toInt8 (Int8.toInt64 x) `shouldBe` Just x++    prop "Int8 -> Integer -> Int8" $ \(x :: Int8) ->+      Integer.toInt8 (Int8.toInteger x) `shouldBe` Just x++    prop "Int16 -> Int32 -> Int16" $ \(x :: Int16) ->+      Int32.toInt16 (Int16.toInt32 x) `shouldBe` Just x++    prop "Int16 -> Int64 -> Int16" $ \(x :: Int16) ->+      Int64.toInt16 (Int16.toInt64 x) `shouldBe` Just x++    prop "Int32 -> Int64 -> Int32" $ \(x :: Int32) ->+      Int64.toInt32 (Int32.toInt64 x) `shouldBe` Just x++    prop "Word8 -> Word16 -> Word8" $ \(x :: Word8) ->+      Word16.toWord8 (Word8.toWord16 x) `shouldBe` Just x++    prop "Word8 -> Word32 -> Word8" $ \(x :: Word8) ->+      Word32.toWord8 (Word8.toWord32 x) `shouldBe` Just x++    prop "Word8 -> Word64 -> Word8" $ \(x :: Word8) ->+      Word64.toWord8 (Word8.toWord64 x) `shouldBe` Just x++    prop "Word16 -> Word32 -> Word16" $ \(x :: Word16) ->+      Word32.toWord16 (Word16.toWord32 x) `shouldBe` Just x++    prop "Word16 -> Word64 -> Word16" $ \(x :: Word16) ->+      Word64.toWord16 (Word16.toWord64 x) `shouldBe` Just x++    prop "Word32 -> Word64 -> Word32" $ \(x :: Word32) ->+      Word64.toWord32 (Word32.toWord64 x) `shouldBe` Just x++    prop "Word8 -> Int16 -> Word8" $ \(x :: Word8) ->+      Int16.toWord8 (Word8.toInt16 x) `shouldBe` Just x++    prop "Word8 -> Int32 -> Word8" $ \(x :: Word8) ->+      Int32.toWord8 (Word8.toInt32 x) `shouldBe` Just x++    -- Int via Int+    prop "Int8 -> Int -> Int8" $ \(x :: Int8) ->+      Int.toInt8 (Int8.toInt x) `shouldBe` Just x++    prop "Int16 -> Int -> Int16" $ \(x :: Int16) ->+      Int.toInt16 (Int16.toInt x) `shouldBe` Just x++    -- Int via Integer+    prop "Int16 -> Integer -> Int16" $ \(x :: Int16) ->+      Integer.toInt16 (Int16.toInteger x) `shouldBe` Just x++    prop "Int32 -> Integer -> Int32" $ \(x :: Int32) ->+      Integer.toInt32 (Int32.toInteger x) `shouldBe` Just x++    prop "Int64 -> Integer -> Int64" $ \(x :: Int64) ->+      Integer.toInt64 (Int64.toInteger x) `shouldBe` Just x++    prop "Int -> Int64 -> Int" $ \(x :: Int) ->+      Int64.toInt (Int.toInt64 x) `shouldBe` Just x++    prop "Int -> Integer -> Int" $ \(x :: Int) ->+      Integer.toInt (Int.toInteger x) `shouldBe` Just x++    -- Word via Word+    prop "Word8 -> Word -> Word8" $ \(x :: Word8) ->+      Word.toWord8 (Word8.toWord x) `shouldBe` Just x++    prop "Word16 -> Word -> Word16" $ \(x :: Word16) ->+      Word.toWord16 (Word16.toWord x) `shouldBe` Just x++    -- Word via Natural+    prop "Word8 -> Natural -> Word8" $ \(x :: Word8) ->+      Natural.toWord8 (Word8.toNatural x) `shouldBe` Just x++    prop "Word16 -> Natural -> Word16" $ \(x :: Word16) ->+      Natural.toWord16 (Word16.toNatural x) `shouldBe` Just x++    prop "Word32 -> Natural -> Word32" $ \(x :: Word32) ->+      Natural.toWord32 (Word32.toNatural x) `shouldBe` Just x++    prop "Word64 -> Natural -> Word64" $ \(x :: Word64) ->+      Natural.toWord64 (Word64.toNatural x) `shouldBe` Just x++    prop "Word -> Natural -> Word" $ \(x :: Word) ->+      Natural.toWord (Word.toNatural x) `shouldBe` Just x++    -- Word via Integer+    prop "Word64 -> Integer -> Word64" $ \(x :: Word64) ->+      Integer.toWord64 (Word64.toInteger x) `shouldBe` Just x++    prop "Word -> Word64 -> Word" $ \(x :: Word) ->+      Word64.toWord (Word.toWord64 x) `shouldBe` Just x++    prop "Word -> Integer -> Word" $ \(x :: Word) ->+      Integer.toWord (Word.toInteger x) `shouldBe` Just x++    -- Cross-sign: Word via signed+    prop "Word8 -> Int64 -> Word8" $ \(x :: Word8) ->+      Int64.toWord8 (Word8.toInt64 x) `shouldBe` Just x++    prop "Word8 -> Int -> Word8" $ \(x :: Word8) ->+      Int.toWord8 (Word8.toInt x) `shouldBe` Just x++    prop "Word8 -> Integer -> Word8" $ \(x :: Word8) ->+      Integer.toWord8 (Word8.toInteger x) `shouldBe` Just x++    prop "Word16 -> Int32 -> Word16" $ \(x :: Word16) ->+      Int32.toWord16 (Word16.toInt32 x) `shouldBe` Just x++    prop "Word16 -> Int64 -> Word16" $ \(x :: Word16) ->+      Int64.toWord16 (Word16.toInt64 x) `shouldBe` Just x++    prop "Word16 -> Int -> Word16" $ \(x :: Word16) ->+      Int.toWord16 (Word16.toInt x) `shouldBe` Just x++    prop "Word16 -> Integer -> Word16" $ \(x :: Word16) ->+      Integer.toWord16 (Word16.toInteger x) `shouldBe` Just x++    prop "Word32 -> Int64 -> Word32" $ \(x :: Word32) ->+      Int64.toWord32 (Word32.toInt64 x) `shouldBe` Just x++    prop "Word32 -> Integer -> Word32" $ \(x :: Word32) ->+      Integer.toWord32 (Word32.toInteger x) `shouldBe` Just x++    -- Natural via Integer (Either pattern, generated via Word64)+    prop "Natural -> Integer -> Natural" $ \(w :: Word64) ->+      let x = fromIntegral w :: Natural+      in Integer.toNatural (Natural.toInteger x) `shouldBe` Right x++    -- Char+    prop "Char -> Int -> Char" $ \(x :: Char) ->+      Char.fromInt (Char.toInt x) `shouldBe` Just x++    prop "Char -> Word -> Char" $ \(x :: Char) ->+      Char.fromWord (Char.toWord x) `shouldBe` Just x++    -- Via Float (Either ViaIntegerErrors pattern)+    prop "Int8 -> Float -> Int8" $ \(x :: Int8) ->+      Float.toInt8 (Int8.toFloat x) `shouldBe` Right x++    prop "Int16 -> Float -> Int16" $ \(x :: Int16) ->+      Float.toInt16 (Int16.toFloat x) `shouldBe` Right x++    prop "Word8 -> Float -> Word8" $ \(x :: Word8) ->+      Float.toWord8 (Word8.toFloat x) `shouldBe` Right x++    prop "Word16 -> Float -> Word16" $ \(x :: Word16) ->+      Float.toWord16 (Word16.toFloat x) `shouldBe` Right x++    -- Via Double (Either ViaIntegerErrors pattern)+    prop "Int8 -> Double -> Int8" $ \(x :: Int8) ->+      Double.toInt8 (Int8.toDouble x) `shouldBe` Right x++    prop "Int16 -> Double -> Int16" $ \(x :: Int16) ->+      Double.toInt16 (Int16.toDouble x) `shouldBe` Right x++    prop "Int32 -> Double -> Int32" $ \(x :: Int32) ->+      Double.toInt32 (Int32.toDouble x) `shouldBe` Right x++    prop "Word8 -> Double -> Word8" $ \(x :: Word8) ->+      Double.toWord8 (Word8.toDouble x) `shouldBe` Right x++    prop "Word16 -> Double -> Word16" $ \(x :: Word16) ->+      Double.toWord16 (Word16.toDouble x) `shouldBe` Right x++    prop "Word32 -> Double -> Word32" $ \(x :: Word32) ->+      Double.toWord32 (Word32.toDouble x) `shouldBe` Right x++  describe "Fallible narrowing success agrees with fromIntegral" $ do+    prop "Int16 -> Int8: success implies fromIntegral match" $ \(x :: Int16) ->+      case Int16.toInt8 x of+        Just y  -> fromIntegral y `shouldBe` x+        Nothing -> pure ()++    prop "Int32 -> Int16: success implies fromIntegral match" $ \(x :: Int32) ->+      case Int32.toInt16 x of+        Just y  -> fromIntegral y `shouldBe` x+        Nothing -> pure ()++    prop "Int64 -> Int32: success implies fromIntegral match" $ \(x :: Int64) ->+      case Int64.toInt32 x of+        Just y  -> fromIntegral y `shouldBe` x+        Nothing -> pure ()++    prop "Word16 -> Word8: success implies fromIntegral match" $ \(x :: Word16) ->+      case Word16.toWord8 x of+        Just y  -> fromIntegral y `shouldBe` x+        Nothing -> pure ()++    prop "Word32 -> Word16: success implies fromIntegral match" $ \(x :: Word32) ->+      case Word32.toWord16 x of+        Just y  -> fromIntegral y `shouldBe` x+        Nothing -> pure ()++    prop "Word64 -> Word32: success implies fromIntegral match" $ \(x :: Word64) ->+      case Word64.toWord32 x of+        Just y  -> fromIntegral y `shouldBe` x+        Nothing -> pure ()++    prop "Integer -> Int8: success implies fromIntegral match" $ \(x :: Integer) ->+      case Integer.toInt8 x of+        Just y  -> fromIntegral y `shouldBe` x+        Nothing -> pure ()++    prop "Integer -> Word8: success implies fromIntegral match" $ \(x :: Integer) ->+      case Integer.toWord8 x of+        Just y  -> fromIntegral y `shouldBe` x+        Nothing -> pure ()++    prop "Int32 -> Int8: success implies fromIntegral match" $ \(x :: Int32) ->+      case Int32.toInt8 x of+        Just y  -> fromIntegral y `shouldBe` x+        Nothing -> pure ()++    prop "Int64 -> Int16: success implies fromIntegral match" $ \(x :: Int64) ->+      case Int64.toInt16 x of+        Just y  -> fromIntegral y `shouldBe` x+        Nothing -> pure ()++    prop "Int64 -> Int8: success implies fromIntegral match" $ \(x :: Int64) ->+      case Int64.toInt8 x of+        Just y  -> fromIntegral y `shouldBe` x+        Nothing -> pure ()++    prop "Word32 -> Word8: success implies fromIntegral match" $ \(x :: Word32) ->+      case Word32.toWord8 x of+        Just y  -> fromIntegral y `shouldBe` x+        Nothing -> pure ()++    prop "Word64 -> Word16: success implies fromIntegral match" $ \(x :: Word64) ->+      case Word64.toWord16 x of+        Just y  -> fromIntegral y `shouldBe` x+        Nothing -> pure ()++    prop "Word64 -> Word8: success implies fromIntegral match" $ \(x :: Word64) ->+      case Word64.toWord8 x of+        Just y  -> fromIntegral y `shouldBe` x+        Nothing -> pure ()++  describe "toNatural rejects iff negative" $ do+    prop "Int8 -> Natural: Left iff x < 0" $ \(x :: Int8) ->+      isLeft (Int8.toNatural x) `shouldBe` (x < 0)++    prop "Int16 -> Natural: Left iff x < 0" $ \(x :: Int16) ->+      isLeft (Int16.toNatural x) `shouldBe` (x < 0)++    prop "Int32 -> Natural: Left iff x < 0" $ \(x :: Int32) ->+      isLeft (Int32.toNatural x) `shouldBe` (x < 0)++    prop "Int64 -> Natural: Left iff x < 0" $ \(x :: Int64) ->+      isLeft (Int64.toNatural x) `shouldBe` (x < 0)++    prop "Int -> Natural: Left iff x < 0" $ \(x :: Int) ->+      isLeft (Int.toNatural x) `shouldBe` (x < 0)++    prop "Integer -> Natural: Left iff x < 0" $ \(x :: Integer) ->+      isLeft (Integer.toNatural x) `shouldBe` (x < 0)++  describe "toNatural success preserves value" $ do+    prop "Int64 -> Natural -> Integer round-trips via Integer" $ \(x :: Int64) ->+      case Int64.toNatural x of+        Right n  -> Natural.toInteger n `shouldBe` fromIntegral x+        Left _   -> pure ()++    prop "Word64 -> Natural -> Integer preserves value" $ \(x :: Word64) ->+      Natural.toInteger (Word64.toNatural x) `shouldBe` fromIntegral x++  describe "Float range check is exact at boundary" $ do+    prop "Int32 -> Float: succeeds iff abs value <= maxRepFloat" $ \(x :: Int32) ->+      let xi = fromIntegral x :: Integer+      in isRight (Int32.toFloat x) `shouldBe`+           (xi >= -maxRepFloat && xi <= maxRepFloat)++    prop "Word32 -> Float: succeeds iff value <= maxRepFloat" $ \(x :: Word32) ->+      isRight (Word32.toFloat x) `shouldBe`+        (fromIntegral x <= (maxRepFloat :: Integer))++    prop "Int64 -> Float: succeeds iff abs value <= maxRepFloat" $ \(x :: Int64) ->+      let xi = fromIntegral x :: Integer+      in isRight (Int64.toFloat x) `shouldBe`+           (xi >= -maxRepFloat && xi <= maxRepFloat)++    prop "Word64 -> Float: succeeds iff value <= maxRepFloat" $ \(x :: Word64) ->+      isRight (Word64.toFloat x) `shouldBe`+        (fromIntegral x <= (maxRepFloat :: Integer))++    prop "Integer -> Float: succeeds iff abs value <= maxRepFloat" $ \(x :: Integer) ->+      isRight (Integer.toFloat x) `shouldBe`+        (x >= -maxRepFloat && x <= maxRepFloat)++  describe "Double range check is exact at boundary" $ do+    prop "Int64 -> Double: succeeds iff abs value <= maxRepDouble" $ \(x :: Int64) ->+      let xi = fromIntegral x :: Integer+      in isRight (Int64.toDouble x) `shouldBe`+           (xi >= -maxRepDouble && xi <= maxRepDouble)++    prop "Word64 -> Double: succeeds iff value <= maxRepDouble" $ \(x :: Word64) ->+      isRight (Word64.toDouble x) `shouldBe`+        (fromIntegral x <= (maxRepDouble :: Integer))++    prop "Integer -> Double: succeeds iff abs value <= maxRepDouble" $ \(x :: Integer) ->+      isRight (Integer.toDouble x) `shouldBe`+        (x >= -maxRepDouble && x <= maxRepDouble)++  describe "Path independence: different widening paths agree" $ do+    prop "Int8: toInt32 == toInt16 >> Int16.toInt32" $ \(x :: Int8) ->+      Int8.toInt32 x `shouldBe` Int16.toInt32 (Int8.toInt16 x)++    prop "Int8: toInt64 == toInt32 >> Int32.toInt64" $ \(x :: Int8) ->+      Int8.toInt64 x `shouldBe` Int32.toInt64 (Int8.toInt32 x)++    prop "Int8: toInteger == toInt64 >> Int64.toInteger" $ \(x :: Int8) ->+      Int8.toInteger x `shouldBe` Int64.toInteger (Int8.toInt64 x)++    prop "Word8: toWord32 == toWord16 >> Word16.toWord32" $ \(x :: Word8) ->+      Word8.toWord32 x `shouldBe` Word16.toWord32 (Word8.toWord16 x)++    prop "Word8: toWord64 == toWord32 >> Word32.toWord64" $ \(x :: Word8) ->+      Word8.toWord64 x `shouldBe` Word32.toWord64 (Word8.toWord32 x)++    prop "Word8: toInteger == toWord64 >> Word64.toInteger" $ \(x :: Word8) ->+      Word8.toInteger x `shouldBe` Word64.toInteger (Word8.toWord64 x)++  describe "Infallible conversions preserve numeric value (via Integer)" $ do+    prop "Int8.toFloat preserves value" $ \(x :: Int8) ->+      Int8.toFloat x `shouldBe` fromIntegral x++    prop "Int8.toDouble preserves value" $ \(x :: Int8) ->+      Int8.toDouble x `shouldBe` fromIntegral x++    prop "Int16.toFloat preserves value" $ \(x :: Int16) ->+      Int16.toFloat x `shouldBe` fromIntegral x++    prop "Int16.toDouble preserves value" $ \(x :: Int16) ->+      Int16.toDouble x `shouldBe` fromIntegral x++    prop "Word8.toFloat preserves value" $ \(x :: Word8) ->+      Word8.toFloat x `shouldBe` fromIntegral x++    prop "Word8.toDouble preserves value" $ \(x :: Word8) ->+      Word8.toDouble x `shouldBe` fromIntegral x++    prop "Word16.toFloat preserves value" $ \(x :: Word16) ->+      Word16.toFloat x `shouldBe` fromIntegral x++    prop "Word16.toDouble preserves value" $ \(x :: Word16) ->+      Word16.toDouble x `shouldBe` fromIntegral x++    prop "Int32.toDouble preserves value" $ \(x :: Int32) ->+      Int32.toDouble x `shouldBe` fromIntegral x++    prop "Word32.toDouble preserves value" $ \(x :: Word32) ->+      Word32.toDouble x `shouldBe` fromIntegral x++  describe "Signed-to-unsigned: Nothing iff negative or too large" $ do+    prop "Int8 -> Word8: Nothing iff x < 0" $ \(x :: Int8) ->+      (Int8.toWord8 x == Nothing) `shouldBe` (x < 0)++    prop "Int16 -> Word16: Nothing iff x < 0" $ \(x :: Int16) ->+      (Int16.toWord16 x == Nothing) `shouldBe` (x < 0)++    prop "Int32 -> Word32: Nothing iff x < 0" $ \(x :: Int32) ->+      (Int32.toWord32 x == Nothing) `shouldBe` (x < 0)++    prop "Int64 -> Word64: Nothing iff x < 0" $ \(x :: Int64) ->+      (Int64.toWord64 x == Nothing) `shouldBe` (x < 0)++  describe "Cross-sign narrowing: exact failure condition" $ do+    prop "Int16 -> Word8: Nothing iff x < 0 || x > 255" $ \(x :: Int16) ->+      (Int16.toWord8 x == Nothing) `shouldBe` (x < 0 || x > 255)++    prop "Int32 -> Word16: Nothing iff x < 0 || x > 65535" $ \(x :: Int32) ->+      (Int32.toWord16 x == Nothing) `shouldBe` (x < 0 || x > 65535)++    prop "Int64 -> Word32: Nothing iff x < 0 || x > 4294967295" $ \(x :: Int64) ->+      (Int64.toWord32 x == Nothing) `shouldBe` (x < 0 || x > 4294967295)++    prop "Word16 -> Int8: Nothing iff x > 127" $ \(x :: Word16) ->+      (Word16.toInt8 x == Nothing) `shouldBe` (x > 127)++    prop "Word32 -> Int16: Nothing iff x > 32767" $ \(x :: Word32) ->+      (Word32.toInt16 x == Nothing) `shouldBe` (x > 32767)++    prop "Word64 -> Int32: Nothing iff x > 2147483647" $ \(x :: Word64) ->+      (Word64.toInt32 x == Nothing) `shouldBe` (x > 2147483647)++  describe "Float/Double to integer: exact success condition" $ do+    prop "Float -> Integer: succeeds iff finite and whole" $ \(x :: Float) ->+      isRight (Float.toInteger x) `shouldBe` isWholeFloat x++    prop "Float -> Int8: succeeds iff finite, whole, in [-128,127]" $ \(x :: Float) ->+      let i = truncate x :: Integer+      in isRight (Float.toInt8 x) `shouldBe`+           (isWholeFloat x && i >= -128 && i <= 127)++    prop "Float -> Word8: succeeds iff finite, whole, in [0,255]" $ \(x :: Float) ->+      let i = truncate x :: Integer+      in isRight (Float.toWord8 x) `shouldBe`+           (isWholeFloat x && i >= 0 && i <= 255)++    prop "Double -> Integer: succeeds iff finite and whole" $ \(x :: Double) ->+      isRight (Double.toInteger x) `shouldBe` isWholeDouble x++    prop "Double -> Int8: succeeds iff finite, whole, in [-128,127]" $ \(x :: Double) ->+      let i = truncate x :: Integer+      in isRight (Double.toInt8 x) `shouldBe`+           (isWholeDouble x && i >= -128 && i <= 127)++    prop "Double -> Word8: succeeds iff finite, whole, in [0,255]" $ \(x :: Double) ->+      let i = truncate x :: Integer+      in isRight (Double.toWord8 x) `shouldBe`+           (isWholeDouble x && i >= 0 && i <= 255)++  describe "Integer to Float/Double round-trip via toInteger" $ do+    prop "Int32 -> Float -> Integer: success round-trips" $ \(x :: Int32) ->+      case Int32.toFloat x of+        Right y  -> Float.toInteger y `shouldBe` Right (fromIntegral x)+        Left _   -> pure ()++    prop "Int64 -> Double -> Integer: success round-trips" $ \(x :: Int64) ->+      case Int64.toDouble x of+        Right y  -> Double.toInteger y `shouldBe` Right (fromIntegral x)+        Left _   -> pure ()++    prop "Word32 -> Float -> Integer: success round-trips" $ \(x :: Word32) ->+      case Word32.toFloat x of+        Right y  -> Float.toInteger y `shouldBe` Right (fromIntegral x)+        Left _   -> pure ()++    prop "Word64 -> Double -> Integer: success round-trips" $ \(x :: Word64) ->+      case Word64.toDouble x of+        Right y  -> Double.toInteger y `shouldBe` Right (fromIntegral x)+        Left _   -> pure ()++  describe "Float to Double round-trip" $ do+    prop "Float -> Double -> Float preserves value" $ \(x :: Float) ->+      let y = Double.toFloat (Float.toDouble x)+      in if isNaN x+         then y `shouldSatisfy` isNaN+         else y `shouldBe` x++maxRepFloat :: Num a => a+maxRepFloat = 16777215++maxRepDouble :: Num a => a+maxRepDouble = 9007199254740991++isLeft :: Either a b -> Bool+isLeft (Left _)  = True+isLeft (Right _) = False++isRight :: Either a b -> Bool+isRight (Right _) = True+isRight (Left _)  = False++isWholeFloat :: Float -> Bool+isWholeFloat x = not (isNaN x) && not (isInfinite x)+  && x == fromIntegral (truncate x :: Integer)++isWholeDouble :: Double -> Bool+isWholeDouble x = not (isNaN x) && not (isInfinite x)+  && x == fromIntegral (truncate x :: Integer)
+ test/Test/Convert/RatioSpec.hs view
@@ -0,0 +1,35 @@+module Test.Convert.RatioSpec (spec) where++import Test.Hspec+import Data.Ratio ((%))+import qualified Unwitch.Convert.Ratio as Ratio++spec :: Spec+spec = describe "Unwitch.Convert.Ratio" $ do++  describe "fromIntegralToRatio" $ do+    it "wraps integer as ratio with denominator 1" $+      Ratio.fromIntegralToRatio (5 :: Integer) `shouldBe` (5 % 1)+    it "wraps zero" $+      Ratio.fromIntegralToRatio (0 :: Integer) `shouldBe` (0 % 1)+    it "wraps negative" $+      Ratio.fromIntegralToRatio ((-3) :: Integer) `shouldBe` ((-3) % 1)++  describe "unwrapIfDenominatorOne" $ do+    it "round-trips with fromIntegralToRatio" $+      Ratio.unwrapIfDenominatorOne (Ratio.fromIntegralToRatio (7 :: Integer))+        `shouldBe` Just 7+    it "rejects non-unit denominator" $+      Ratio.unwrapIfDenominatorOne (3 % 2 :: Rational) `shouldBe` Nothing++  describe "toFloat" $ do+    it "converts simple rational" $+      Ratio.toFloat (3 % 2) `shouldBe` (1.5 :: Float)+    it "converts integer rational" $+      Ratio.toFloat (4 % 1) `shouldBe` (4.0 :: Float)++  describe "toDouble" $ do+    it "converts simple rational" $+      Ratio.toDouble (3 % 2) `shouldBe` (1.5 :: Double)+    it "converts integer rational" $+      Ratio.toDouble (4 % 1) `shouldBe` (4.0 :: Double)
+ test/Test/Convert/ShortByteStringSpec.hs view
@@ -0,0 +1,22 @@+module Test.Convert.ShortByteStringSpec (spec) where++import Test.Hspec+import Data.ByteString.Short qualified as SBS+import Data.Word (Word8)+import qualified Unwitch.Convert.ShortByteString as ShortByteString+import qualified Unwitch.Convert.ByteString as ByteString++spec :: Spec+spec = describe "Unwitch.Convert.ShortByteString" $ do++  describe "toByteString round-trip" $+    it "round-trips with ByteString.toShortByteString" $+      let sbs = SBS.pack [5, 10, 15]+      in ByteString.toShortByteString (ShortByteString.toByteString sbs) `shouldBe` sbs++  describe "toWord8s / fromWord8s" $ do+    it "round-trips" $+      let ws = [100, 200, 255] :: [Word8]+      in ShortByteString.toWord8s (ShortByteString.fromWord8s ws) `shouldBe` ws+    it "empty list" $+      ShortByteString.toWord8s (ShortByteString.fromWord8s []) `shouldBe` ([] :: [Word8])
+ test/Test/Convert/TextSpec.hs view
@@ -0,0 +1,68 @@+module Test.Convert.TextSpec (spec) where++import Test.Hspec+import Data.Text qualified as T+import qualified Unwitch.Convert.Text as Text+import qualified Unwitch.Convert.LazyText as LazyText+import qualified Unwitch.Convert.ByteString as ByteString++spec :: Spec+spec = describe "Unwitch.Convert.Text" $ do++  describe "toLazyText / toText round-trip" $+    it "round-trips" $+      let t = T.pack "hello world"+      in LazyText.toText (Text.toLazyText t) `shouldBe` t++  describe "toString / fromString" $ do+    it "round-trips" $+      let s = "test string"+      in Text.toString (Text.fromString s) `shouldBe` s+    it "handles unicode" $+      let s = "\x00E9\x00FC\x00F1"+      in Text.toString (Text.fromString s) `shouldBe` s++  describe "toByteStringUtf8" $ do+    it "encodes ASCII" $+      Text.toByteStringUtf8 "hello" `shouldBe` "hello"+    it "round-trips with ByteString.toTextUtf8" $+      let t = T.pack "caf\x00E9"+      in ByteString.toTextUtf8 (Text.toByteStringUtf8 t) `shouldBe` Right t++  describe "toByteStringUtf16LE" $+    it "produces valid UTF-16LE output" $+      let t = T.pack "A"+          bs = Text.toByteStringUtf16LE t+      in ByteString.toTextUtf16LE bs `shouldBe` Right t++  describe "toByteStringUtf16BE" $+    it "produces valid UTF-16BE output" $+      let t = T.pack "B"+          bs = Text.toByteStringUtf16BE t+      in ByteString.toTextUtf16BE bs `shouldBe` Right t++  describe "toByteStringUtf32LE" $+    it "produces valid UTF-32LE output" $+      let t = T.pack "C"+          bs = Text.toByteStringUtf32LE t+      in ByteString.toTextUtf32LE bs `shouldBe` Right t++  describe "toByteStringUtf32BE" $+    it "produces valid UTF-32BE output" $+      let t = T.pack "D"+          bs = Text.toByteStringUtf32BE t+      in ByteString.toTextUtf32BE bs `shouldBe` Right t++  describe "toByteStringLatin1" $ do+    it "succeeds for ASCII text" $+      Text.toByteStringLatin1 "hello" `shouldSatisfy` \case+        Just _  -> True+        Nothing -> False+    it "succeeds for Latin1 range chars" $+      let t = T.pack "\x00E9\x00FC"+      in case Text.toByteStringLatin1 t of+           Just bs -> ByteString.toTextLatin1 bs `shouldBe` t+           Nothing -> expectationFailure "expected Just"+    it "fails for chars above 0xFF" $+      let t = T.pack "\x0100" -- Latin Extended-A+      in Text.toByteStringLatin1 t `shouldBe` Nothing
+ test/Test/Convert/UnboxedSpec.hs view
@@ -0,0 +1,154 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Test.Convert.UnboxedSpec+  ( spec+  )+where++import Test.Hspec+import Test.Hspec.QuickCheck (prop)++import qualified Unwitch.Convert.Integer as Integer+import qualified Unwitch.Convert.Int as Int+import qualified Unwitch.Convert.Int8 as Int8+import qualified Unwitch.Convert.Int16 as Int16+import qualified Unwitch.Convert.Int32 as Int32+import qualified Unwitch.Convert.Int64 as Int64+import qualified Unwitch.Convert.Word as Word+import qualified Unwitch.Convert.Word8 as Word8+import qualified Unwitch.Convert.Word16 as Word16+import qualified Unwitch.Convert.Word32 as Word32+import qualified Unwitch.Convert.Word64 as Word64+import qualified Unwitch.Convert.Natural as Natural+import qualified Unwitch.Convert.Char as Char+import Data.Int+import Data.Word+import Numeric.Natural (Natural)++spec :: Spec+spec = describe "Unboxed sum variants" $ do++  describe "Integer" $ do+    prop "toInt8# agrees with toInt8" $ \(x :: Integer) ->+      case Integer.toInt8# x of+        (# y | #)      -> Integer.toInt8 x `shouldBe` Just y+        (# | (# #) #)  -> Integer.toInt8 x `shouldBe` Nothing++    prop "toDouble# agrees with toDouble" $ \(x :: Integer) ->+      case Integer.toDouble# x of+        (# e | #) -> Integer.toDouble x `shouldBe` Left e+        (# | y #) -> Integer.toDouble x `shouldBe` Right y++  describe "Int" $ do+    prop "toInt8# agrees with toInt8" $ \(x :: Int) ->+      case Int.toInt8# x of+        (# y | #)      -> Int.toInt8 x `shouldBe` Just y+        (# | (# #) #)  -> Int.toInt8 x `shouldBe` Nothing++    prop "toNatural# agrees with toNatural" $ \(x :: Int) ->+      case Int.toNatural# x of+        (# e | #) -> Int.toNatural x `shouldBe` Left e+        (# | y #) -> Int.toNatural x `shouldBe` Right y++  describe "Int8" $ do+    prop "toWord8# agrees with toWord8" $ \(x :: Int8) ->+      case Int8.toWord8# x of+        (# y | #)      -> Int8.toWord8 x `shouldBe` Just y+        (# | (# #) #)  -> Int8.toWord8 x `shouldBe` Nothing++    prop "toNatural# agrees with toNatural" $ \(x :: Int8) ->+      case Int8.toNatural# x of+        (# e | #) -> Int8.toNatural x `shouldBe` Left e+        (# | y #) -> Int8.toNatural x `shouldBe` Right y++  describe "Int16" $ do+    prop "toInt8# agrees with toInt8" $ \(x :: Int16) ->+      case Int16.toInt8# x of+        (# y | #)      -> Int16.toInt8 x `shouldBe` Just y+        (# | (# #) #)  -> Int16.toInt8 x `shouldBe` Nothing++  describe "Int32" $ do+    prop "toInt8# agrees with toInt8" $ \(x :: Int32) ->+      case Int32.toInt8# x of+        (# y | #)      -> Int32.toInt8 x `shouldBe` Just y+        (# | (# #) #)  -> Int32.toInt8 x `shouldBe` Nothing++    prop "toFloat# agrees with toFloat" $ \(x :: Int32) ->+      case Int32.toFloat# x of+        (# e | #) -> Int32.toFloat x `shouldBe` Left e+        (# | y #) -> Int32.toFloat x `shouldBe` Right y++  describe "Int64" $ do+    prop "toInt8# agrees with toInt8" $ \(x :: Int64) ->+      case Int64.toInt8# x of+        (# y | #)      -> Int64.toInt8 x `shouldBe` Just y+        (# | (# #) #)  -> Int64.toInt8 x `shouldBe` Nothing++    prop "toDouble# agrees with toDouble" $ \(x :: Int64) ->+      case Int64.toDouble# x of+        (# e | #) -> Int64.toDouble x `shouldBe` Left e+        (# | y #) -> Int64.toDouble x `shouldBe` Right y++  describe "Word" $ do+    prop "toWord8# agrees with toWord8" $ \(x :: Word) ->+      case Word.toWord8# x of+        (# y | #)      -> Word.toWord8 x `shouldBe` Just y+        (# | (# #) #)  -> Word.toWord8 x `shouldBe` Nothing++    prop "toFloat# agrees with toFloat" $ \(x :: Word) ->+      case Word.toFloat# x of+        (# e | #) -> Word.toFloat x `shouldBe` Left e+        (# | y #) -> Word.toFloat x `shouldBe` Right y++  describe "Word8" $ do+    prop "toInt8# agrees with toInt8" $ \(x :: Word8) ->+      case Word8.toInt8# x of+        (# y | #)      -> Word8.toInt8 x `shouldBe` Just y+        (# | (# #) #)  -> Word8.toInt8 x `shouldBe` Nothing++  describe "Word16" $ do+    prop "toWord8# agrees with toWord8" $ \(x :: Word16) ->+      case Word16.toWord8# x of+        (# y | #)      -> Word16.toWord8 x `shouldBe` Just y+        (# | (# #) #)  -> Word16.toWord8 x `shouldBe` Nothing++  describe "Word32" $ do+    prop "toWord8# agrees with toWord8" $ \(x :: Word32) ->+      case Word32.toWord8# x of+        (# y | #)      -> Word32.toWord8 x `shouldBe` Just y+        (# | (# #) #)  -> Word32.toWord8 x `shouldBe` Nothing++    prop "toFloat# agrees with toFloat" $ \(x :: Word32) ->+      case Word32.toFloat# x of+        (# e | #) -> Word32.toFloat x `shouldBe` Left e+        (# | y #) -> Word32.toFloat x `shouldBe` Right y++  describe "Word64" $ do+    prop "toWord8# agrees with toWord8" $ \(x :: Word64) ->+      case Word64.toWord8# x of+        (# y | #)      -> Word64.toWord8 x `shouldBe` Just y+        (# | (# #) #)  -> Word64.toWord8 x `shouldBe` Nothing++    prop "toFloat# agrees with toFloat" $ \(x :: Word64) ->+      case Word64.toFloat# x of+        (# e | #) -> Word64.toFloat x `shouldBe` Left e+        (# | y #) -> Word64.toFloat x `shouldBe` Right y++  describe "Natural" $ do+    prop "toWord8# agrees with toWord8" $ \(w :: Word) ->+      let x = fromIntegral w :: Natural+      in case Natural.toWord8# x of+        (# y | #)      -> Natural.toWord8 x `shouldBe` Just y+        (# | (# #) #)  -> Natural.toWord8 x `shouldBe` Nothing++    prop "toFloat# agrees with toFloat" $ \(w :: Word) ->+      let x = fromIntegral w :: Natural+      in case Natural.toFloat# x of+        (# e | #) -> Natural.toFloat x `shouldBe` Left e+        (# | y #) -> Natural.toFloat x `shouldBe` Right y++  describe "Char" $ do+    prop "fromInt# agrees with fromInt" $ \(x :: Int) ->+      case Char.fromInt# x of+        (# y | #)      -> Char.fromInt x `shouldBe` Just y+        (# | (# #) #)  -> Char.fromInt x `shouldBe` Nothing+
+ test/Test/Convert/Word16Spec.hs view
@@ -0,0 +1,49 @@+module Test.Convert.Word16Spec (spec) where++import Test.Hspec+import Data.Int+import Data.Word+import Numeric.Natural (Natural)+import qualified Unwitch.Convert.Word16 as Word16++-- Property tests cover: Word16->Word32/64 round-trips,+-- Word16->Word8 narrowing, toFloat/toDouble preserve value.+-- Kept: toWord, toNatural, toInt8, toInt16, toInt32, toInt64, toInt,+-- toInteger (all have no direct property coverage).++spec :: Spec+spec = describe "Unwitch.Convert.Word16" $ do++  describe "toWord (infallible)" $+    it "widens maxBound" $+      Word16.toWord maxBound `shouldBe` 65535++  describe "toNatural (infallible)" $+    it "converts maxBound" $+      Word16.toNatural maxBound `shouldBe` (65535 :: Natural)++  describe "toInt8 (fallible)" $+    it "rejects too large" $+      Word16.toInt8 (128 :: Word16) `shouldBe` Nothing++  describe "toInt16 (fallible)" $ do+    it "rejects too large" $+      Word16.toInt16 (32768 :: Word16) `shouldBe` Nothing+    it "converts in-range" $+      Word16.toInt16 (32767 :: Word16) `shouldBe` Just (32767 :: Int16)++  describe "toInt32 (infallible)" $+    it "widens maxBound" $+      Word16.toInt32 maxBound `shouldBe` 65535++  describe "toInt64 (infallible)" $+    it "widens maxBound" $+      Word16.toInt64 maxBound `shouldBe` 65535++  describe "toInt (infallible)" $+    it "widens maxBound" $+      Word16.toInt maxBound `shouldBe` 65535++  describe "toInteger (infallible)" $+    it "widens maxBound" $+      Word16.toInteger maxBound `shouldBe` 65535
+ test/Test/Convert/Word32Spec.hs view
@@ -0,0 +1,54 @@+module Test.Convert.Word32Spec (spec) where++import Test.Hspec+import Data.Int+import Data.Word+import Numeric.Natural (Natural)+import qualified Unwitch.Convert.Word32 as Word32++-- Property tests cover: Word32->Word64 round-trip,+-- Word32->Word16 narrowing, toFloat range check,+-- toDouble preserve value.+-- Kept: toWord8 (no property), toWord, toNatural, toInt8, toInt16,+-- toInt32, toInt64, toInt, toInteger.++spec :: Spec+spec = describe "Unwitch.Convert.Word32" $ do++  describe "toWord8 (fallible)" $+    it "rejects too large" $+      Word32.toWord8 (256 :: Word32) `shouldBe` Nothing++  describe "toWord (fallible)" $+    it "converts 0" $+      Word32.toWord 0 `shouldBe` Just (0 :: Word)++  describe "toNatural (infallible)" $+    it "converts maxBound" $+      Word32.toNatural maxBound `shouldBe` (4294967295 :: Natural)++  describe "toInt8 (fallible)" $+    it "rejects too large" $+      Word32.toInt8 (200 :: Word32) `shouldBe` Nothing++  describe "toInt16 (fallible)" $+    it "rejects too large" $+      Word32.toInt16 (40000 :: Word32) `shouldBe` Nothing++  describe "toInt32 (fallible)" $ do+    it "rejects too large" $+      Word32.toInt32 (2147483648 :: Word32) `shouldBe` Nothing+    it "converts at boundary" $+      Word32.toInt32 (2147483647 :: Word32) `shouldBe` Just (2147483647 :: Int32)++  describe "toInt64 (infallible)" $+    it "widens maxBound" $+      Word32.toInt64 maxBound `shouldBe` 4294967295++  describe "toInt (fallible)" $+    it "converts 0" $+      Word32.toInt 0 `shouldBe` Just (0 :: Int)++  describe "toInteger (infallible)" $+    it "converts maxBound" $+      Word32.toInteger maxBound `shouldBe` 4294967295
+ test/Test/Convert/Word64Spec.hs view
@@ -0,0 +1,53 @@+module Test.Convert.Word64Spec (spec) where++import Test.Hspec+import Data.Int+import Data.Word+import qualified Unwitch.Convert.Word64 as Word64++-- Property tests cover: Word64->Word32 narrowing,+-- Word64->Natural->Integer round-trip,+-- toFloat range check, toDouble range check.+-- Kept: toWord8, toWord16, toWord, toInt8, toInt16, toInt32,+-- toInt64, toInt, toInteger (all have no direct property coverage).++spec :: Spec+spec = describe "Unwitch.Convert.Word64" $ do++  describe "toWord8 (fallible)" $+    it "rejects too large" $+      Word64.toWord8 (256 :: Word64) `shouldBe` Nothing++  describe "toWord16 (fallible)" $+    it "rejects too large" $+      Word64.toWord16 (65536 :: Word64) `shouldBe` Nothing++  describe "toWord (fallible)" $+    it "converts 0" $+      Word64.toWord 0 `shouldBe` Just (0 :: Word)++  describe "toInt8 (fallible)" $+    it "rejects too large" $+      Word64.toInt8 (200 :: Word64) `shouldBe` Nothing++  describe "toInt16 (fallible)" $+    it "rejects too large" $+      Word64.toInt16 (40000 :: Word64) `shouldBe` Nothing++  describe "toInt32 (fallible)" $+    it "rejects too large" $+      Word64.toInt32 (3000000000 :: Word64) `shouldBe` Nothing++  describe "toInt64 (fallible)" $ do+    it "rejects too large" $+      Word64.toInt64 (maxBound :: Word64) `shouldBe` Nothing+    it "converts at boundary" $+      Word64.toInt64 (9223372036854775807 :: Word64) `shouldBe` Just (9223372036854775807 :: Int64)++  describe "toInt (fallible)" $+    it "converts 0" $+      Word64.toInt 0 `shouldBe` Just (0 :: Int)++  describe "toInteger (infallible)" $+    it "converts maxBound" $+      Word64.toInteger maxBound `shouldBe` 18446744073709551615
+ test/Test/Convert/Word8Spec.hs view
@@ -0,0 +1,38 @@+module Test.Convert.Word8Spec (spec) where++import Test.Hspec+import Data.Int+import Data.Word+import Numeric.Natural (Natural)+import qualified Unwitch.Convert.Word8 as Word8++-- Property tests cover: Word8->Word16/32/64 round-trips,+-- Word8->Int16/32 round-trips, Word8->Integer path independence,+-- toFloat/toDouble preserve value.+-- Kept: toWord (no property), toNatural (no property),+-- toInt8 (no property), toInt64 (no property), toInt (no property).++spec :: Spec+spec = describe "Unwitch.Convert.Word8" $ do++  describe "toWord (infallible)" $+    it "widens maxBound" $+      Word8.toWord maxBound `shouldBe` 255++  describe "toNatural (infallible)" $+    it "converts maxBound" $+      Word8.toNatural maxBound `shouldBe` (255 :: Natural)++  describe "toInt8 (fallible)" $ do+    it "rejects too large" $+      Word8.toInt8 (128 :: Word8) `shouldBe` Nothing+    it "converts in-range" $+      Word8.toInt8 (127 :: Word8) `shouldBe` Just (127 :: Int8)++  describe "toInt64 (infallible)" $+    it "widens maxBound" $+      Word8.toInt64 maxBound `shouldBe` 255++  describe "toInt (infallible)" $+    it "widens maxBound" $+      Word8.toInt maxBound `shouldBe` 255
+ test/Test/Convert/WordSpec.hs view
@@ -0,0 +1,66 @@+module Test.Convert.WordSpec (spec) where++import Test.Hspec+import Data.Int+import Data.Word+import Unwitch.Errors+import qualified Unwitch.Convert.Word as Word++spec :: Spec+spec = describe "Unwitch.Convert.Word" $ do++  describe "toWord8 (fallible)" $+    it "rejects too large" $+      Word.toWord8 (256 :: Word) `shouldBe` Nothing++  describe "toWord16 (fallible)" $+    it "rejects too large" $+      Word.toWord16 (65536 :: Word) `shouldBe` Nothing++  describe "toWord32 (fallible)" $+    it "converts in-range" $+      Word.toWord32 (100 :: Word) `shouldBe` Just (100 :: Word32)++  describe "toWord64 (infallible)" $+    it "widens maxBound" $+      Word.toWord64 maxBound `shouldBe` fromIntegral (maxBound :: Word)++  describe "toNatural (infallible)" $+    it "converts maxBound" $+      Word.toNatural maxBound `shouldBe` fromIntegral (maxBound :: Word)++  describe "toInt8 (fallible)" $+    it "rejects too large" $+      Word.toInt8 (200 :: Word) `shouldBe` Nothing++  describe "toInt16 (fallible)" $+    it "rejects too large" $+      Word.toInt16 (40000 :: Word) `shouldBe` Nothing++  describe "toInt32 (fallible)" $+    it "rejects too large" $+      Word.toInt32 (3000000000 :: Word) `shouldBe` Nothing++  describe "toInt64 (fallible)" $+    it "converts in-range" $+      Word.toInt64 (100 :: Word) `shouldBe` Just (100 :: Int64)++  describe "toInt (fallible)" $ do+    it "rejects maxBound" $+      Word.toInt (maxBound :: Word) `shouldBe` Nothing+    it "converts 0" $+      Word.toInt 0 `shouldBe` Just (0 :: Int)++  describe "toInteger (infallible)" $+    it "converts maxBound" $+      Word.toInteger maxBound `shouldBe` fromIntegral (maxBound :: Word)++  describe "toFloat (range-checked)" $ do+    it "converts in-range" $+      Word.toFloat (16777215 :: Word) `shouldBe` Right 16777215.0+    it "rejects too large" $+      Word.toFloat (16777216 :: Word) `shouldBe` Left Overflow++  describe "toDouble (range-checked)" $+    it "converts 0" $+      Word.toDouble 0 `shouldBe` Right (0.0 :: Double)
+ unwitch.cabal view
@@ -0,0 +1,151 @@+cabal-version:      3.0++name:           unwitch+version:        1.0.0+homepage:       https://github.com/jappeace/haskell-unwitch-project#readme+synopsis:  converts between primitives+description: +  Removes the magic from witch.+  This provides safe conversions like witch does.+  But it [doesn't](https://jappie.me/death-to-type-classes.html) use [type classes](https://www.haskellforall.com/2012/05/scrap-your-type-classes.html)+  or exceptions.+  This has a couple of advantages:++  1. No need to use type application for function selection.+  2. Functions get names that describe what they do.+    This allows ctags to work as well.+  3. No trouble with orphans.+  4. Custom errors instead of the prelude based ones allow client+    code to recover with typesafety even on partial conversions.+bug-reports:    https://github.com/jappeace/haskell-unwitch-project/issues+author:         Jappie Klooster+maintainer:     jappieklooster@hotmail.com+copyright:      2020 Jappie Klooster+license:        MIT+category: Data+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    Readme.md+    LICENSE+extra-doc-files:+    Changelog.md++source-repository head+  type: git+  location: https://github.com/jappeace/haskell-unwitch-project++common common-options+  default-extensions:+      EmptyCase+      FlexibleContexts+      FlexibleInstances+      InstanceSigs+      MultiParamTypeClasses+      LambdaCase+      MultiWayIf+      NamedFieldPuns+      TupleSections+      DeriveFoldable+      DeriveFunctor+      DeriveGeneric+      DeriveLift+      DeriveTraversable+      DerivingStrategies+      GeneralizedNewtypeDeriving+      StandaloneDeriving+      OverloadedStrings+      TypeApplications+      NumericUnderscores+      ImportQualifiedPost+      UnboxedSums+      UnboxedTuples+      MagicHash++  ghc-options:+    -Wall -Wincomplete-uni-patterns+    -Wincomplete-record-updates -Widentities -Wredundant-constraints+    -Wcpp-undef -fwarn-tabs -Wpartial-fields+    -fdefer-diagnostics -Wunused-packages+    -fenable-th-splice-warnings+    -fno-omit-yields++  build-depends:+      base >=4.18.0.0 && <4.23,+      ghc-bignum < 2,+      bytestring >= 0.10 && < 0.13,+      text >= 1.2 && < 2.2++  default-language: Haskell2010++library+  import: common-options+  exposed-modules:+      Unwitch.Convert.ByteString+      Unwitch.Convert.Char+      Unwitch.Convert.Complex+      Unwitch.Convert.Double+      Unwitch.Convert.Fixed+      Unwitch.Convert.Float+      Unwitch.Convert.Int+      Unwitch.Convert.Int8+      Unwitch.Convert.Int16+      Unwitch.Convert.Int32+      Unwitch.Convert.Int64+      Unwitch.Convert.Integer+      Unwitch.Convert.LazyByteString+      Unwitch.Convert.LazyText+      Unwitch.Convert.Natural+      Unwitch.Convert.Ratio+      Unwitch.Convert.ShortByteString+      Unwitch.Convert.Text+      Unwitch.Convert.Word+      Unwitch.Convert.Word8+      Unwitch.Convert.Word16+      Unwitch.Convert.Word32+      Unwitch.Convert.Word64+      Unwitch.Errors+  other-modules:+      Unwitch+      Unwitch.Constant+      Unwitch.TryEvaluate+  hs-source-dirs:+      src++test-suite unit+  import: common-options+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  ghc-options: -threaded -rtsopts "-with-rtsopts=-N" -Wno-unused-packages+  other-modules:+      Test.Convert.ByteStringSpec+      Test.Convert.Int8Spec+      Test.Convert.Int16Spec+      Test.Convert.Int32Spec+      Test.Convert.Int64Spec+      Test.Convert.IntSpec+      Test.Convert.LazyByteStringSpec+      Test.Convert.LazyTextSpec+      Test.Convert.Word8Spec+      Test.Convert.Word16Spec+      Test.Convert.Word32Spec+      Test.Convert.Word64Spec+      Test.Convert.WordSpec+      Test.Convert.NaturalSpec+      Test.Convert.IntegerSpec+      Test.Convert.FloatSpec+      Test.Convert.DoubleSpec+      Test.Convert.PropertySpec+      Test.Convert.RatioSpec+      Test.Convert.FixedSpec+      Test.Convert.ComplexSpec+      Test.Convert.CharSpec+      Test.Convert.ShortByteStringSpec+      Test.Convert.TextSpec+      Test.Convert.UnboxedSpec+  hs-source-dirs:+      test+  build-depends:+      hspec,+      hspec-core,+      unwitch