diff --git a/ip.cabal b/ip.cabal
--- a/ip.cabal
+++ b/ip.cabal
@@ -1,5 +1,5 @@
 name: ip
-version: 1.2.1
+version: 1.3.0
 synopsis: Library for IP and MAC addresses
 homepage: https://github.com/andrewthad/haskell-ip#readme
 license: BSD3
@@ -49,15 +49,12 @@
   build-depends:
       base >= 4.9 && < 5
     , attoparsec >= 0.13 && < 0.14
-    , aeson >= 0.9 && < 1.4
-    , hashable >= 1.2  && < 1.3
+    , aeson >= 0.9 && < 1.5
+    , hashable >= 1.2 && < 1.3
     , text >= 1.2  && < 1.3
     , bytestring >= 0.10 && < 0.11
     , vector >= 0.11 && < 0.13
     , primitive >= 0.6 && < 0.7
-    , semigroups >= 0.16 && < 0.19
-  -- if impl(ghcjs)
-  --   build-depends: ghcjs-base >= 0.2 && < 0.3
   ghc-options: -Wall -O2
   default-language: Haskell2010
 
@@ -93,7 +90,7 @@
       base
     , ip
     , hspec
-    , hspec >= 2.4.4 && < 2.5
+    , hspec >= 2.4.4
   other-modules:
     Net.IPv4Spec
     Net.IPv4.RangeSpec
diff --git a/src/Net/IPv4/Range.hs b/src/Net/IPv4/Range.hs
--- a/src/Net/IPv4/Range.hs
+++ b/src/Net/IPv4/Range.hs
@@ -9,6 +9,7 @@
 module Net.IPv4.Range
   ( -- * Range functions
     range
+  , fromBounds
   , normalize
   , contains
   , member
@@ -72,6 +73,25 @@
 --   sized and sets masked bits in the 'IPv4' to zero.
 range :: IPv4 -> Word8 -> IPv4Range
 range addr len = normalize (IPv4Range addr len)
+
+-- | Given an inclusive lower and upper ip address, create the smallest
+-- 'IPv4Range' that contains the two. This is helpful in situations where
+-- input given as a range like @192.168.16.0-192.168.19.255@ needs to be
+-- handled. This makes the range broader if it cannot be represented in
+-- CIDR notation.
+--
+-- >>> print $ fromBounds (fromOctets 192 168 16 0) (fromOctets 192 168 19 255)
+-- 192.168.16.0/22
+-- >>> print $ fromBounds (fromOctets 10 0 5 7) (fromOctets 10 0 5 14)
+-- 10.0.5.0/28
+fromBounds :: IPv4 -> IPv4 -> IPv4Range
+fromBounds (IPv4 a) (IPv4 b) =
+  let lo = min a b
+      hi = max a b
+   in normalize (IPv4Range (IPv4 lo) (maskFromBounds lo hi))
+
+maskFromBounds :: Word32 -> Word32 -> Word8
+maskFromBounds lo hi = fromIntegral (Bits.countLeadingZeros (Bits.xor lo hi))
 
 -- | Checks to see if an 'IPv4' address belongs in the 'IPv4Range'.
 --
diff --git a/src/Net/IPv6.hs b/src/Net/IPv6.hs
--- a/src/Net/IPv6.hs
+++ b/src/Net/IPv6.hs
@@ -1,4 +1,9 @@
-{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE BangPatterns        #-}
+{-# LANGUAGE InstanceSigs        #-}
+{-# LANGUAGE LambdaCase          #-}
+{-# LANGUAGE MagicHash           #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE UnboxedTuples       #-}
 
 {-# OPTIONS_GHC -Wall #-}
 
@@ -26,21 +31,30 @@
   , print
   ) where
 
-import Prelude hiding (any, print)
+import Net.IPv4 (IPv4(..))
+import qualified Net.IPv4 as IPv4
+
+import Control.Applicative
+import Control.Monad.Primitive
+import Control.Monad.ST
 import Data.Bits
-import Data.List (intercalate, group)
-import Data.Word
 import Data.Char (chr)
-import Control.Applicative
+import Data.List (intercalate, group)
+import Data.Primitive.Addr
+import Data.Primitive.ByteArray
+import Data.Primitive.Types (Prim(..))
 import Data.Text (Text)
-import Text.Read (Read(..),Lexeme(Ident),lexP,parens)
+import Data.Word
+import GHC.Exts
+import Numeric (showHex)
+import Prelude hiding (any, print)
 import Text.ParserCombinators.ReadPrec (prec,step)
-import qualified Data.Text as Text
-import qualified Data.Text.IO as TIO
-import qualified Data.Attoparsec.Text as Atto
+import Text.Read (Read(..),Lexeme(Ident),lexP,parens)
 import qualified Data.Aeson as Aeson
 import qualified Data.Attoparsec.Text as AT
-import Numeric (showHex)
+import qualified Data.Attoparsec.Text as Atto
+import qualified Data.Text as Text
+import qualified Data.Text.IO as TIO
 
 -- $setup
 --
@@ -77,6 +91,61 @@
     where
     (a,b,c,d,e,f,g,h) = toWord16s addr
 
+instance Prim IPv6 where
+  sizeOf# _ = 2# *# sizeOf# (undefined :: Word64)
+  alignment# _ = alignment# (undefined :: Word64)
+  indexByteArray# arr# i# =
+    let i = I# i#
+        arr = ByteArray arr#
+    in IPv6 (indexByteArray arr (2 * i + 0)) (indexByteArray arr (2 * i + 1))
+  readByteArray# :: forall s. () => MutableByteArray# s -> Int# -> State# s -> (# State# s, IPv6 #)
+  readByteArray# arr# i# = internal $ do
+    let i = I# i#
+        arr = MutableByteArray arr#
+    a <- readByteArray arr (2 * i + 0) :: ST s Word64
+    b <- readByteArray arr (2 * i + 1)
+    return (IPv6 a b)
+  writeByteArray# :: forall s. () => MutableByteArray# s -> Int# -> IPv6 -> State# s -> State# s
+  writeByteArray# arr# i# (IPv6 a b) = internal_ $ do
+    let i = I# i#
+        arr = MutableByteArray arr#
+    writeByteArray arr (2 * i + 0) a
+    writeByteArray arr (2 * i + 1) b :: ST s ()
+  setByteArray# arr# i# len# ident = go 0#
+    where
+      go ix# s0 = if isTrue# (ix# <# len#)
+        then case writeByteArray# arr# (i# +# ix#) ident s0 of
+          s1 -> go (ix# +# 1#) s1
+        else s0
+  indexOffAddr# :: Addr# -> Int# -> IPv6
+  indexOffAddr# addr# i# =
+    let i = I# i#
+        addr = Addr addr#
+    in IPv6 (indexOffAddr addr (2 * i + 0)) (indexOffAddr addr (2 * i + 1))
+  readOffAddr# :: forall s. () => Addr# -> Int# -> State# s -> (# State# s, IPv6 #)
+  readOffAddr# addr# i# = internal $ do
+    let i = I# i#
+        addr = Addr addr#
+    a <- readOffAddr addr (2 * i + 0) :: ST s Word64
+    b <- readOffAddr addr (2 * i + 1)
+    return (IPv6 a b)
+  writeOffAddr# :: forall s. () => Addr# -> Int# -> IPv6 -> State# s -> State# s
+  writeOffAddr# addr# i# (IPv6 a b) = internal_ $ do
+    let i = I# i#
+        addr = Addr addr#
+    writeOffAddr addr (2 * i + 0) a
+    writeOffAddr addr (2 * i + 1) b :: ST s ()
+  setOffAddr# addr# i# len# ident = go 0#
+    where
+      go ix# s0 = if isTrue# (ix# <# len#)
+        then case writeOffAddr# addr# (i# +# ix#) ident s0 of
+          s1 -> go (ix# +# 1#) s1
+        else s0
+
+internal_ :: PrimBase m => m () -> State# (PrimState m) -> State# (PrimState m)
+internal_ m s = case internal m s of
+  (# s', _ #) -> s'
+
 print :: IPv6 -> IO ()
 print = TIO.putStrLn . encode
 
@@ -206,9 +275,23 @@
 
 -- | Encodes the IP, using zero-compression on the leftmost-longest string of
 -- zeroes in the address.
+-- Per <https://tools.ietf.org/html/rfc5952#section-5 RFC 5952 Section 5>,
+-- this uses mixed notation when encoding an IPv4-mapped IPv6 address:
+--
+-- >>> T.putStrLn $ encode $ fromWord16s 0xDEAD 0xBEEF 0x0 0x0 0x0 0x0 0x0 0x1234
+-- dead:beef::1234
+-- >>> T.putStrLn $ encode $ fromWord16s 0x0 0x0 0x0 0x0 0x0 0xFFFF 0x6437 0xA5B4
+-- ::ffff:100.55.165.180
+-- >>> T.putStrLn $ encode $ fromWord16s 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
+-- ::
 encode :: IPv6 -> Text
-encode ip = toText [w1, w2, w3, w4, w5, w6, w7, w8]
+encode ip =
+  if isIPv4MappedAddress
+  -- This representation is RECOMMENDED by https://tools.ietf.org/html/rfc5952#section-5
+  then Text.pack "::ffff:" `mappend` IPv4.encode (IPv4.IPv4 (fromIntegral w7 `unsafeShiftL` 16 .|. fromIntegral w8))
+  else toText [w1, w2, w3, w4, w5, w6, w7, w8]
   where
+  isIPv4MappedAddress = w1 == 0 && w2 == 0 && w3 == 0 && w4 == 0 && w5 == 0 && w6 == 0xFFFF
   (w1, w2, w3, w4, w5, w6, w7, w8) = toWord16s ip
   toText ws = Text.pack $ intercalate ":" $ expand 0 longestZ grouped
     where
@@ -226,106 +309,90 @@
     longestZ = maximum . (0:) . map snd . filter ((==0) . fst) $ grouped
     grouped = map (\x -> (head x, length x)) (group ws)
 
+-- | Decode an IPv6 address. This accepts both standard IPv6
+-- notation (with zero compression) and mixed notation for
+-- IPv4-mapped IPv6 addresses.
 decode :: Text -> Maybe IPv6
 decode t = rightToMaybe (AT.parseOnly (parser <* AT.endOfInput) t)
 
 parser :: Atto.Parser IPv6
-parser = do
-  s <- start
-  case toIPv6 s of
-    Nothing -> fail "Wrong number of octets in IPv6 address"
-    Just ip -> return ip
-  where
-  msg = "All chunks in a formatted IPv6 address must be between 0x0000 and 0xFFFF"
-  colonMsg = "Cannot use double colon for omitting zeroes more than once in an IPv6 address"
-  start = do
-    c <- Atto.peekChar'
-    if c == ':'
-      then go (-1) 0 []
-      else Atto.hexadecimal >>= \w -> go (-1) 1 [w]
-    -- r <- fmap Just Atto.hexadecimal <|> (Nothing <$ Atto.char ':')
-    -- case r of
-    --   Just !w -> go (-1) 1 [w]
-    --   Nothing -> go 0 0 []
-  go !colonIndex !currentIndex !ws = do
-    r <- do
-      m <- Atto.peekChar
-      case m of
-        Nothing -> return ResDone
-        Just c -> if c == ':'
-          then do
-            _ <- Atto.anyChar -- should be a colon
-            if colonIndex == currentIndex
-              then fmap ResWord Atto.hexadecimal <|> pure ResDone
-              else do
-                d <- Atto.peekChar'
-                if d == ':'
-                  then return ResColon
-                  else fmap ResWord Atto.hexadecimal
-          else return ResDone
-    case r of
-      ResDone -> pure (S colonIndex currentIndex ws)
-      ResColon -> if alreadySet colonIndex
-        then fail colonMsg
-        else go currentIndex currentIndex ws
-      ResWord w -> restrictTo16 msg w >> go colonIndex (currentIndex + 1) (w : ws)
-
-toIPv6 :: S -> Maybe IPv6
-toIPv6 (S colonIndex total input) = case compare total 8 of
-  EQ -> if colonIndex == (-1)
-    then go 0 0 input
-    else Nothing
-  GT -> Nothing
-  LT -> go 0 0 input
+parser = startIP >>= makeIP
   where
-  revColonIndex = total - colonIndex
-  spacesToSkip = 8 - total
-  go :: Int -> Word64 -> [Word64] -> Maybe IPv6
-  go !ix !acc ws = if ix > 3
-    then fmap (flip IPv6 acc) (go2 ix 0 ws)
-    else case ws of
-      w : wsNext -> if ix == revColonIndex
-        then go (ix + spacesToSkip) acc (w : wsNext)
-        else go (ix + 1) (acc .|. unsafeShiftL w (times16 ix)) wsNext
-      [] -> if ix == revColonIndex
-        then Just $ IPv6 0 acc
-        else Nothing -- Not enough word16s in list
-  go2 :: Int -> Word64 -> [Word64] -> Maybe Word64
-  go2 !ix !acc ws = case ws of
-    w : wsNext -> if ix == revColonIndex
-      then go2 (ix + spacesToSkip) acc (w : wsNext)
-      else go2 (ix + 1) (acc .|. unsafeShiftL w (times16 ix - 64)) wsNext
-    [] -> if ix == revColonIndex || ix > 7
-      then Just acc
-      else Nothing -- Not enough word16s in list
-
-times16 :: Int -> Int
-times16 a = unsafeShiftL a 4
+  -- handles the case where an IP starts with ::
+  startIP :: Atto.Parser ([Word16], Maybe [Word16])
+  startIP = 
+    (\ends -> ([], Just ends)) <$> (Atto.char ':' *> Atto.char ':' *> restOfIP []) <|>
+    fullIP []
 
-alreadySet :: Int -> Bool
-alreadySet i = i /= (-1)
+  -- a full IP that might contain ::
+  fullIP :: [Word16] -> Atto.Parser ([Word16], Maybe [Word16])
+  fullIP starts =
+    ((\x -> (x ++ starts, Nothing)) <$> ipv4) <|>
+    startPart starts
 
-restrictTo16 :: String -> Word64 -> Atto.Parser ()
-restrictTo16 msg w = if w > 65535
-  then fail msg
-  else return ()
+  -- an IP that cannot contain ::
+  restOfIP :: [Word16] -> Atto.Parser [Word16]
+  restOfIP ends =
+    ((\x -> x ++ ends) <$> ipv4) <|>
+    endPart ends <|>
+    pure ends
+  
+  ipv4 = ipv4ToWord16s <$> IPv4.parser
+  ipv4ToWord16s (IPv4 ip) = [fromIntegral (ip .&. 0xFFFF), fromIntegral (ip `unsafeShiftR` 16)]
+  
+  -- a colon-separated part before ::
+  startPart (starts) = do
+    part <- Atto.hexadecimal
+    let result = (part : starts)
+    Atto.peekChar >>= \case 
+      Just ':' -> do
+        _ <- Atto.anyChar -- will be ':'
+        Atto.peekChar >>= \case
+          Just ':' -> do
+            _ <- Atto.anyChar -- will be ':' 
+            (\ends -> (result, Just ends)) <$> restOfIP []
+          _ ->
+            fullIP result
+      _ ->
+        pure (result, Nothing)
 
--- | This is an internal data type used as the result
---   after parsing an ipv6 address. The first field
---   indicates the index at which a double colon occurs.
---   The second is the length of the third.
---   The third is a reversed list of the 16s
---   that comprise the ipv6 address.
-data S = S
-  { _sDoubleColon :: {-# UNPACK #-} !Int
-  , _sTotal :: {-# UNPACK #-} !Int
-  , _sRevWords :: ![Word64]
-  } deriving (Show,Read)
+  -- a colon-separated part after ::
+  endPart ends = do
+    part <- Atto.hexadecimal
+    let result = part : ends
+    Atto.peekChar >>= \case 
+      Just ':' -> do
+        _ <- Atto.anyChar -- will be ':'
+        Atto.peekChar >>= \case
+          Just ':' -> do
+            fail "Cannot use double colon for omitting zeroes more than once in an IPv6 address"
+          _ ->
+            restOfIP result
+      _ ->
+        pure result
 
-data Res
-  = ResWord {-# UNPACK #-} !Word64
-  | ResColon
-  | ResDone
+  makeIP :: ([Word16], Maybe [Word16]) -> Atto.Parser IPv6
+  makeIP (starts, mends) =
+    case mends of 
+      -- Nothing indicates we never encountered double-colon, so we must have
+      -- all 8 parts:
+      Nothing -> do
+        if numStarts /= 8
+        then fail "not enough colon-separated parts in IPv6 address"
+        else
+          let [w1, w2, w3, w4, w5, w6, w7, w8] = reverse starts in
+          pure (fromWord16s w1 w2 w3 w4 w5 w6 w7 w8)
+      -- otherwise, we did encounter a double-colon, so we expand it to fill:
+      Just ends ->
+        let numEnds = length ends in
+        case compare (numStarts + numEnds) 8 of
+          GT -> fail "too many colon-separated parts in IPv6 address"
+          EQ -> fail "unnecessary double-colon in IPv6 address"
+          LT -> 
+            let [w1, w2, w3, w4, w5, w6, w7, w8] = reverse starts ++ replicate (8 - (numStarts + numEnds)) 0 ++ reverse ends in
+            pure (fromWord16s w1 w2 w3 w4 w5 w6 w7 w8)
+    where
+    numStarts = length starts
 
 fromOctetsV6 ::
      Word64 -> Word64 -> Word64 -> Word64
diff --git a/src/Net/Mac.hs b/src/Net/Mac.hs
--- a/src/Net/Mac.hs
+++ b/src/Net/Mac.hs
@@ -56,14 +56,10 @@
 import GHC.Generics (Generic)
 import Data.Char (ord,chr)
 import Data.Primitive.Types (Prim(..))
+import GHC.Exts
 import Text.Read (Read(..),Lexeme(Ident),lexP,parens)
 import Text.ParserCombinators.ReadPrec (prec,step)
 import GHC.Word (Word16(W16#))
-import GHC.Int (Int(I#))
-import Data.Primitive.Addr (Addr(..),writeOffAddr)
-import Data.Primitive.ByteArray (MutableByteArray(..),writeByteArray)
-import Control.Monad.Primitive (PrimState,PrimBase,internal)
-import Control.Monad.ST (ST)
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Unsafe as BU
 import qualified Data.ByteString.Builder as BB
@@ -77,9 +73,6 @@
 import qualified Data.Aeson.Types as Aeson
 import qualified Data.Text.IO as TIO
 
-import GHC.Exts (Word#,Int#,State#,MutableByteArray#,Addr#,(*#),(+#),indexWord16Array#,readWord16Array#,
-  indexWord16OffAddr#,readWord16OffAddr#,writeWord16Array#,writeWord16OffAddr#)
-
 #if MIN_VERSION_aeson(1,0,0) 
 import Data.Aeson (ToJSONKey(..),FromJSONKey(..),
   ToJSONKeyFunction(..),FromJSONKeyFunction(..))
@@ -592,9 +585,20 @@
     s1 -> case writeWord16OffAddr# arr (i +# 1#) (macToWord16B# m) s1 of
       s2 -> writeWord16OffAddr# arr (i +# 2#) (macToWord16C# m) s2
     where !i = 3# *# i0
-  setByteArray# = defaultSetByteArray#
-  setOffAddr# = defaultSetOffAddr#
-  
+  setByteArray# arr# i# len# ident = go 0#
+    where
+      go ix# s0 = if isTrue# (ix# <# len#)
+        then case writeByteArray# arr# (i# +# ix#) ident s0 of
+          s1 -> go (ix# +# 1#) s1
+        else s0
+  setOffAddr# addr# i# len# ident = go 0#
+    where
+      go ix# s0 = if isTrue# (ix# <# len#)
+        then case writeOffAddr# addr# (i# +# ix#) ident s0 of
+          s1 -> go (ix# +# 1#) s1
+        else s0
+
+
 macToWord16A# :: Mac -> Word#
 macToWord16A# (Mac w) = case word64ToWord16 (unsafeShiftR w 32) of
   W16# x -> x
@@ -618,32 +622,6 @@
 
 word64ToWord16 :: Word64 -> Word16
 word64ToWord16 = fromIntegral
-
-defaultSetByteArray# :: forall s a. Prim a => MutableByteArray# s -> Int# -> Int# -> a -> State# s -> State# s
-defaultSetByteArray# arr# i# len# ident = internal_ (go 0)
-  where
-  !len = I# len#
-  !i = I# i#
-  !arr = MutableByteArray arr#
-  go :: Int -> ST s ()
-  go !ix = if ix < len
-    then writeByteArray arr (i + ix) ident >> go (i + 1)
-    else return ()
-
-defaultSetOffAddr# :: forall s a. Prim a => Addr# -> Int# -> Int# -> a -> State# s -> State# s
-defaultSetOffAddr# addr# i# len# ident = internal_ (go 0)
-  where
-  !len = I# len#
-  !i = I# i#
-  !addr = Addr addr#
-  go :: Int -> ST s ()
-  go !ix = if ix < len
-    then writeOffAddr addr (i + ix) ident >> go (i + 1)
-    else return ()
-
-internal_ :: PrimBase m => m () -> State# (PrimState m) -> State# (PrimState m)
-internal_ m s = case internal m s of
-  (# s', () #) -> s'
 
 -- What this instance does is to display the
 -- inner contents in hexadecimal and pad them out to
diff --git a/test/Bench.hs b/test/Bench.hs
--- a/test/Bench.hs
+++ b/test/Bench.hs
@@ -5,6 +5,7 @@
 import qualified Data.Text as Text
 import qualified Net.Mac as Mac
 import qualified Net.IPv4 as IPv4
+import qualified Net.IPv6 as IPv6
 
 import qualified Naive
 import qualified IPv4Text1
@@ -19,6 +20,10 @@
   let ipAddr = IPv4 1000000009
       ipText = Text.pack "192.168.5.99"
       mac = Mac.fromOctets 0xFA 0xBB 0x43 0xA1 0x22 0x09
+      ip6Text = Text.pack "::"
+      ip6TextBigger = Text.pack "1:2:3:4:5:6:7:8"
+      ip6TextSkip = Text.pack "1:2::7:8"
+      ip6TextHex = Text.pack "a:b::c:d"
   defaultMain
     [ bgroup "Mac to Text"
       [ bench "Current Implementation, pairs" $ whnf Mac.encode mac
@@ -49,5 +54,11 @@
       [ bench "Naive" $ whnf Naive.encodeByteString ipAddr
       , bench "Preallocated: No Lookup Tables" $ whnf IPv4ByteString1.encode ipAddr
       , bench "Preallocated" $ whnf IPv4.encodeUtf8 ipAddr
+      ]
+    , bgroup "IPv6 from Text"
+      [ bench "New '::'" $ whnf IPv6.decode ip6Text
+      , bench "New '1:2:3:4:5:6:7:8'" $ whnf IPv6.decode ip6TextBigger
+      , bench "New '1:2::7:8'" $ whnf IPv6.decode ip6TextSkip
+      , bench "New 'a:b::c:d'" $ whnf IPv6.decode ip6TextHex
       ]
     ]
diff --git a/test/Test.hs b/test/Test.hs
--- a/test/Test.hs
+++ b/test/Test.hs
@@ -10,7 +10,7 @@
 import Test.Framework (defaultMain, testGroup, Test)
 import Test.Framework.Providers.QuickCheck2 (testProperty)
 import Test.QuickCheck (Arbitrary(..),Property,oneof,Gen,elements,choose)
-import Test.HUnit (Assertion,(@?=))
+import Test.HUnit (Assertion,(@?=),(@=?))
 import Numeric (showHex)
 import Test.QuickCheck.Property (failed,succeeded,Result(..))
 import Data.Bifunctor
@@ -109,6 +109,7 @@
     , testGroup "IPv6"
       [ lawsToTest (jsonLaws (Proxy :: Proxy IPv6))
       , lawsToTest (showReadLaws (Proxy :: Proxy IPv6))
+      , lawsToTest (primLaws (Proxy :: Proxy IPv6))
       ]
     , testGroup "IP"
       [ lawsToTest (jsonLaws (Proxy :: Proxy IP))
@@ -171,7 +172,7 @@
   where
   go a b c d e f str =
     Just (HexMac (Mac.fromOctets a b c d e f))
-    @?= fmap HexMac (Mac.decodeUtf8 (BC8.pack str))
+    @=? fmap HexMac (Mac.decodeUtf8 (BC8.pack str))
 
 testIPv4Parser :: Assertion
 testIPv4Parser = do
@@ -180,7 +181,7 @@
   where
   go a b c d str =
     Right (IPv4.fromOctets a b c d)
-    @?= (AB.parseOnly
+    @=? (AB.parseOnly
           (IPv4.parserUtf8 <* AT.endOfInput)
           (BC8.pack str)
         )
@@ -209,7 +210,7 @@
   where
   go a b c d e f g h str =
     Right (HexIPv6 (IPv6.fromWord16s a b c d e f g h))
-    @?= fmap HexIPv6
+    @=? fmap HexIPv6
       (AT.parseOnly
         (IPv6.parser <* AT.endOfInput)
         (Text.pack str)
@@ -220,10 +221,18 @@
   go "1111:2222:3333:4444:5555:6666::7777:8888"
   go "1111:2222:3333:4444:5555:6666:7777:8888:9999"
   go "1111:2222:3333:4444:5555:6666:7777:8888::9999"
+
+  go "1:127.0.0.1" -- not enough
+  go "1:2:3:127.0.0.1"
+  go "1:2:3:4:127.0.0.1"
+  go "1:2:3:4:5:127.0.0.1"
+
+  go "1:2:3:4:5:6:7:127.0.0.1" -- too much
+  go "1:2:3:4:5:6:7:8:127.0.0.1"
   where
   go str =
     Left ()
-    @?= bimap (\_ -> ()) HexIPv6
+    @=? bimap (\_ -> ()) HexIPv6
       (AT.parseOnly
         (IPv6.parser <* AT.endOfInput)
         (Text.pack str)
@@ -258,6 +267,15 @@
     -- works with only first or last:
     "::2:3:4:5:6:7:8" `roundTripsTo` "::2:3:4:5:6:7:8"
     "1:2:3:4:5:6:7::" `roundTripsTo` "1:2:3:4:5:6:7::"
+
+    -- decimal notation in IPv6 addresses:
+    "1:2:3:4:5:6:0.7.0.8" `roundTripsTo` "1:2:3:4:5:6:7:8"
+    "::0.0.0.0" `roundTripsTo` "::"
+
+    -- per https://tools.ietf.org/html/rfc5952#section-5
+    "::ffff:0:0" `roundTripsTo` "::ffff:0.0.0.0"
+    "::ffff:00ff:ff00" `roundTripsTo` "::ffff:0.255.255.0"
+    "::ffff:203.0.113.17" `roundTripsTo` "::ffff:203.0.113.17"
 
    where
    roundTripsTo s sExpected =
