diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,40 @@
 # Changelog
 
+## 4.0.0 (2020-10-20)
+
+#### Changed
+
+- **Breaking:** `VChunk` now cannot be empty.
+- **Breaking:** A `Version` now guarantees `NonEmpty` chunks.
+- **Breaking:** A `Mess` now guarantees `NonEmpty` chunks, and its structure has
+  been significantly changed. Particularly, `Mess` values are now aware of the
+  `Int` values they hold (when they do), as well as "revision" values of the
+  pattern `rXYZ`.
+- Comparison of `Version` values is more memory efficient.
+
+#### Added
+
+- `Version` now has an extra field, `_vMeta :: [VChunk]` for capturing
+  "metadata" like Semver. This prevents otherwise nice-looking versions from
+  being demoted to `Mess`.
+- The `MChunk` type to accomodate the changes to `Mess` mentioned above.
+
+#### Removed
+
+- **Breaking:** `Version` no longer has a `Monoid` instance.
+
+#### Fixed
+
+- `""` no longer parses in any way. [#32]
+- Version strings with trailing whitespace no longer parse via `versioning`. [#33]
+- Particular edge cases involving `Mess` comparisons. [aura#646]
+- A particular edge case involving prereleases in `Version` comparisons. [aura#586]
+
+[#32]: https://github.com/fosskers/versions/issues/32
+[#33]: https://github.com/fosskers/versions/issues/33
+[aura#646]: https://github.com/fosskers/aura/issues/646
+[aura#586]: https://github.com/fosskers/aura/issues/586
+
 ## 3.5.4 (2020-05-12)
 
 #### Added
diff --git a/Data/Versions.hs b/Data/Versions.hs
--- a/Data/Versions.hs
+++ b/Data/Versions.hs
@@ -31,7 +31,7 @@
 --
 -- == Using the Parsers
 -- In general, `versioning` is the function you want. It attempts to parse a
--- given `T.Text` using the three individual parsers, `semver`, `version` and
+-- given `Text` using the three individual parsers, `semver`, `version` and
 -- `mess`. If one fails, it tries the next. If you know you only want to parse
 -- one specific version type, use that parser directly (e.g. `semver`).
 
@@ -42,6 +42,7 @@
   , PVP(..)
   , Version(..)
   , Mess(..), messMajor, messMinor, messPatch, messPatchChunk
+  , MChunk(..)
   , VUnit(..), digits, str
   , VChunk
   , VSep(..)
@@ -58,7 +59,7 @@
   , Traversal'
   , Semantic(..)
     -- ** Traversing Text
-    -- | When traversing `T.Text`, leveraging its `Semantic` instance will
+    -- | When traversing `Text`, leveraging its `Semantic` instance will
     -- likely benefit you more than using these Traversals directly.
   , _Versioning, _SemVer, _Version, _Mess
     -- ** Versioning Traversals
@@ -69,13 +70,17 @@
   , _Digits, _Str
   ) where
 
+import qualified Control.Applicative.Combinators.NonEmpty as PC
 import           Control.DeepSeq
+import           Control.Monad (void)
 import           Data.Bool (bool)
 import           Data.Char (isAlpha)
+import           Data.Foldable (fold)
 import           Data.Hashable (Hashable)
 import           Data.List (intersperse)
 import           Data.List.NonEmpty (NonEmpty(..))
 import qualified Data.List.NonEmpty as NEL
+import           Data.Text (Text)
 import qualified Data.Text as T
 import           Data.Void (Void)
 import           GHC.Generics (Generic)
@@ -133,15 +138,27 @@
 
 -- | Convert a `SemVer` to a `Version`.
 vFromS :: SemVer -> Version
-vFromS (SemVer m i p r _) = Version Nothing [[Digits m], [Digits i], [Digits p]] r
+vFromS (SemVer ma mi pa re me) =
+  Version Nothing ((Digits ma :| []) :| [(Digits mi :| []), Digits pa :| []]) re me
 
 -- | Convert a `Version` to a `Mess`.
 mFromV :: Version -> Mess
-mFromV (Version e v r) = maybe affix (\a -> VNode [showt a] VColon affix) e
+mFromV (Version e v m r) = maybe affix (\a -> Mess (MDigit a (showt a) :| []) $ Just (VColon, affix)) e
   where
     affix :: Mess
-    affix = VNode (chunksAsT v) VHyphen $ VLeaf (chunksAsT r)
+    affix = Mess (chunksAsM v) m'
 
+    m' :: Maybe (VSep, Mess)
+    m' = case NEL.nonEmpty m of
+      Nothing  -> r'
+      Just m'' -> Just (VPlus, Mess (chunksAsM m'') r')
+
+    r' :: Maybe (VSep, Mess)
+    r' = case NEL.nonEmpty r of
+      Nothing  -> Nothing
+      Just r'' -> Just (VHyphen, Mess (chunksAsM r'') Nothing)
+
+
 -- | Special logic for when semver-like values can be extracted from a `Mess`.
 -- This avoids having to "downcast" the `SemVer` into a `Mess` before comparing,
 -- and in some cases can offer better comparison results.
@@ -163,7 +180,7 @@
       Just EQ -> fallback
       Nothing -> case messPatchChunk m of
         Nothing             -> fallback
-        Just (Digits pa':_) -> case compare pa pa' of
+        Just (Digits pa':|_) -> case compare pa pa' of
           LT -> LT
           GT -> GT
           EQ -> GT  -- This follows semver's rule!
@@ -209,22 +226,22 @@
 -- λ "1.2.3" & _Versioning . _Ideal . patch %~ (+ 1)  -- or just: "1.2.3" & patch %~ (+ 1)
 -- "1.2.4"
 -- @
-_Versioning :: Traversal' T.Text Versioning
+_Versioning :: Traversal' Text Versioning
 _Versioning f t = either (const (pure t)) (fmap prettyV . f) $ versioning t
 {-# INLINE _Versioning #-}
 
 -- | Traverse some Text for its inner SemVer.
-_SemVer :: Traversal' T.Text SemVer
+_SemVer :: Traversal' Text SemVer
 _SemVer f t = either (const (pure t)) (fmap prettySemVer . f) $ semver t
 {-# INLINE _SemVer #-}
 
 -- | Traverse some Text for its inner Version.
-_Version :: Traversal' T.Text Version
+_Version :: Traversal' Text Version
 _Version f t = either (const (pure t)) (fmap prettyVer . f) $ version t
 {-# INLINE _Version #-}
 
 -- | Traverse some Text for its inner Mess.
-_Mess :: Traversal' T.Text Mess
+_Mess :: Traversal' Text Mess
 _Mess f t = either (const (pure t)) (fmap prettyMess . f) $ mess t
 {-# INLINE _Mess #-}
 
@@ -289,7 +306,7 @@
   -- | A Natural Transformation into an proper `SemVer`.
   semantic :: Traversal' v SemVer
 
-instance Semantic T.Text where
+instance Semantic Text where
   major    = _Versioning . major
   minor    = _Versioning . minor
   patch    = _Versioning . patch
@@ -317,11 +334,11 @@
 --
 -- For more information, see http://semver.org
 data SemVer = SemVer
-  { _svMajor  :: Word
-  , _svMinor  :: Word
-  , _svPatch  :: Word
-  , _svPreRel :: [VChunk]
-  , _svMeta   :: [VChunk] }
+  { _svMajor  :: !Word
+  , _svMinor  :: !Word
+  , _svPatch  :: !Word
+  , _svPreRel :: ![VChunk]
+  , _svMeta   :: ![VChunk] }
   deriving stock (Show, Generic)
   deriving anyclass (NFData, Hashable)
 
@@ -375,7 +392,7 @@
 -- | A single unit of a Version. May be digits or a string of characters. Groups
 -- of these are called `VChunk`s, and are the identifiers separated by periods
 -- in the source.
-data VUnit = Digits Word | Str T.Text
+data VUnit = Digits Word | Str Text
   deriving stock (Eq, Show, Read, Ord, Generic)
   deriving anyclass (NFData, Hashable)
 
@@ -397,7 +414,7 @@
 digits = Digits
 
 -- | Smart constructor for a `VUnit` made of letters.
-str :: T.Text -> Maybe VUnit
+str :: Text -> Maybe VUnit
 str t = bool Nothing (Just $ Str t) $ T.all isAlpha t
 
 _Digits :: Traversal' VUnit Word
@@ -405,14 +422,14 @@
 _Digits _ v          = pure v
 {-# INLINE _Digits #-}
 
-_Str :: Traversal' VUnit T.Text
+_Str :: Traversal' VUnit Text
 _Str f (Str t) = Str . (\t' -> bool t t' (T.all isAlpha t')) <$> f t
 _Str _ v       = pure v
 {-# INLINE _Str #-}
 
 -- | A logical unit of a version number. Can consist of multiple letters
 -- and numbers.
-type VChunk = [VUnit]
+type VChunk = NonEmpty VUnit
 
 --------------------------------------------------------------------------------
 -- (Haskell) PVP
@@ -485,26 +502,24 @@
 -- | A (General) Version.
 -- Not quite as ideal as a `SemVer`, but has some internal consistancy
 -- from version to version.
--- Generally conforms to the @x.x.x-x@ pattern, and may optionally have an /epoch/.
--- These are prefixes marked by a colon, like in @1:2.3.4@.
 --
--- Examples of @Version@ that are not @SemVer@: 0.25-2, 8.u51-1, 20150826-1, 1:2.3.4
+-- Generally conforms to the @a.b.c-p@ pattern, and may optionally have an
+-- /epoch/ and /metadata/. Epochs are prefixes marked by a colon, like in
+-- @1:2.3.4@. Metadata is prefixed by @+@, and unlike SemVer can appear before
+-- the "prerelease" (the @-p@).
+--
+-- Examples of @Version@ that are not @SemVer@: 0.25-2, 8.u51-1, 20150826-1,
+-- 1:2.3.4, 1.11.0+20200830-1
 data Version = Version
-  { _vEpoch  :: Maybe Word
-  , _vChunks :: [VChunk]
-  , _vRel    :: [VChunk] }
+  { _vEpoch  :: !(Maybe Word)
+  , _vChunks :: !(NonEmpty VChunk)
+  , _vMeta   :: ![VChunk]
+  , _vRel    :: ![VChunk] }
   deriving stock (Eq, Show, Generic)
   deriving anyclass (NFData, Hashable)
 
 instance Semigroup Version where
-  Version e c r <> Version e' c' r' = Version ((+) <$> e <*> e') (c ++ c') (r ++ r')
-
-instance Monoid Version where
-  mempty = Version Nothing [] []
-
-#if !MIN_VERSION_base(4,11,0)
-  mappend = (<>)
-#endif
+  Version e c m r <> Version e' c' m' r' = Version ((+) <$> e <*> e') (c <> c') (m <> m') (r <> r')
 
 -- | Set a `Version`'s epoch to `Nothing`.
 wipe :: Version -> Version
@@ -512,71 +527,84 @@
 
 -- | Customized.
 instance Ord Version where
-  -- | The obvious base case.
-  compare (Version _ [] []) (Version _ [] []) = EQ
-
   -- | For the purposes of Versions with epochs, `Nothing` is the same as `Just 0`,
   -- so we need to compare their actual version numbers.
-  compare v0@(Version (Just 0) _ _) v1@(Version Nothing _ _) = compare (wipe v0) v1
-  compare v0@(Version Nothing _ _) v1@(Version (Just 0) _ _) = compare v0 (wipe v1)
+  compare v0@(Version (Just 0) _ _ _) v1@(Version Nothing _ _ _) = compare (wipe v0) v1
+  compare v0@(Version Nothing _ _ _) v1@(Version (Just 0) _ _ _) = compare v0 (wipe v1)
 
   -- | If a version has an epoch > 1 and the other has no epoch, the first will
   -- be considered greater.
-  compare (Version (Just _) _ _) (Version Nothing _ _) = GT
-  compare (Version Nothing _ _) (Version (Just _) _ _) = LT
+  compare (Version (Just _) _ _ _) (Version Nothing _ _ _) = GT
+  compare (Version Nothing _ _ _) (Version (Just _) _ _ _) = LT
 
   -- | If two epochs are equal, we need to compare their actual version numbers.
   -- Otherwise, the comparison of the epochs is the only thing that matters.
-  compare v0@(Version (Just n) _ _) v1@(Version (Just m) _ _) | n == m = compare (wipe v0) (wipe v1)
-                                                              | otherwise = compare n m
-
-  -- | If the two Versions were otherwise equal and recursed down this far,
-  -- we need to compare them by their "release" values.
-  compare (Version _ [] rs) (Version _ [] rs') = compare (Version Nothing rs []) (Version Nothing rs' [])
-
-  -- | If one side has run out of chunks to compare but the other hasn't,
-  -- the other must be newer.
-  compare Version{}  (Version _ [] _) = GT
-  compare (Version _ [] _) Version{}  = LT
+  compare v0@(Version (Just n) _ _ _) v1@(Version (Just m) _ _ _) | n == m = compare (wipe v0) (wipe v1)
+                                                                  | otherwise = compare n m
 
   -- | The usual case. If first VChunks of each Version is equal, then we keep
   -- recursing. Otherwise, we don't need to check further. Consider @1.2@
   -- compared to @1.1.3.4.5.6@.
-  compare (Version _ (a:as) rs) (Version _ (b:bs) rs') = case f a b of
-    EQ  -> compare (Version Nothing as rs) (Version Nothing bs rs')
-    res -> res
-    where f [] [] = EQ
+  compare (Version _ as _ rs) (Version _ bs _ rs') = g (NEL.toList as) (NEL.toList bs)
+    where
+      g :: [VChunk] -> [VChunk] -> Ordering
+      -- | If the two Versions were otherwise equal and recursed down this far,
+      -- we need to compare them by their "release" values.
+      g [] [] = g rs rs'
 
-          -- | Opposite of the above. If we've recursed this far and one side
-          -- has fewer chunks, it must be the "greater" version. A Chunk break
-          -- only occurs in a switch from digits to letters and vice versa, so
-          -- anything "extra" must be an @rc@ marking or similar. Consider @1.1@
-          -- compared to @1.1rc1@.
-          f [] _  = GT
-          f _ []  = LT
+      -- | If all chunks up until this point were equal, but one side continues
+      -- on with "lettered" sections, these are considered to be indicating a
+      -- beta\/prerelease, and thus are /less/ than the side who already ran out
+      -- of chunks.
+      g [] ((Str _ :| _):_) = GT
+      g ((Str _ :| _):_) [] = LT
 
-          -- | The usual case.
-          f (Digits n:ns) (Digits m:ms) | n > m = GT
-                                        | n < m = LT
-                                        | otherwise = f ns ms
-          f (Str n:ns) (Str m:ms) | n > m = GT
-                                  | n < m = LT
-                                  | otherwise = f ns ms
+      -- | If one side has run out of chunks to compare but the other hasn't,
+      -- the other must be newer.
+      g _ []  = GT
+      g [] _  = LT
 
-          -- | An arbitrary decision to prioritize digits over letters.
-          f (Digits _ :_) (Str _ :_) = GT
-          f (Str _ :_ ) (Digits _ :_) = LT
+      -- | The usual case.
+      g (x:xs) (y:ys) = case f (NEL.toList x) (NEL.toList y) of
+        EQ  -> g xs ys
+        res -> res
 
+      f :: [VUnit] -> [VUnit] -> Ordering
+      f [] [] = EQ
+
+      -- | Opposite of the above. If we've recursed this far and one side
+      -- has fewer chunks, it must be the "greater" version. A Chunk break
+      -- only occurs in a switch from digits to letters and vice versa, so
+      -- anything "extra" must be an @rc@ marking or similar. Consider @1.1@
+      -- compared to @1.1rc1@.
+      f [] _  = GT
+      f _ []  = LT
+
+      -- | The usual case.
+      f (Digits n:ns) (Digits m:ms) | n > m = GT
+                                    | n < m = LT
+                                    | otherwise = f ns ms
+      f (Str n:ns) (Str m:ms) | n > m = GT
+                              | n < m = LT
+                              | otherwise = f ns ms
+
+      -- | An arbitrary decision to prioritize digits over letters.
+      f (Digits _ :_) (Str _ :_) = GT
+      f (Str _ :_ ) (Digits _ :_) = LT
+
 instance Semantic Version where
-  major f (Version e ([Digits n] : cs) rs) = (\n' -> Version e ([Digits n'] : cs) rs) <$> f n
+  major f (Version e ((Digits n :| []) :| cs) me rs) =
+    (\n' -> Version e ((Digits n' :| []) :| cs) me rs) <$> f n
   major _ v = pure v
   {-# INLINE major #-}
 
-  minor f (Version e (c : [Digits n] : cs) rs) = (\n' -> Version e (c : [Digits n'] : cs) rs) <$> f n
+  minor f (Version e (c :| (Digits n :| []) : cs) me rs) =
+    (\n' -> Version e (c :| (Digits n' :| []) : cs) me rs) <$> f n
   minor _ v = pure v
   {-# INLINE minor #-}
 
-  patch f (Version e (c : d : [Digits n] : cs) rs) = (\n' -> Version e (c : d : [Digits n'] : cs) rs) <$> f n
+  patch f (Version e (c :| d : (Digits n :| []) : cs) me rs) =
+    (\n' -> Version e (c :| d : (Digits n' :| []) : cs) me rs) <$> f n
   patch _ v = pure v
   {-# INLINE patch #-}
 
@@ -588,7 +616,8 @@
   meta _ v = pure v
   {-# INLINE meta #-}
 
-  semantic f (Version _ ([Digits a] : [Digits b] : [Digits c] : _) rs) = vFromS <$> f (SemVer a b c rs [])
+  semantic f (Version _ ((Digits a:|[]) :| (Digits b:|[]) : (Digits c:|[]) : _) me rs) =
+    vFromS <$> f (SemVer a b c me rs)
   semantic _ v = pure v
   {-# INLINE semantic #-}
 
@@ -599,15 +628,37 @@
 --------------------------------------------------------------------------------
 -- (Complex) Mess
 
+-- | Possible values of a section of a `Mess`. A numeric value is extracted if
+-- it could be, alongside the original text it came from. This preserves both
+-- `Ord` and pretty-print behaviour for versions like @1.003.0@.
+data MChunk
+  = MDigit Word Text
+  -- ^ A nice numeric value.
+  | MRev Word Text
+  -- ^ A numeric value preceeded by an @r@, indicating a revision.
+  | MPlain Text
+  -- ^ Anything else.
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData, Hashable)
+
+instance Ord MChunk where
+  compare (MDigit a _) (MDigit b _) = compare a b
+  compare (MRev a _) (MRev b _)     = compare a b
+  compare (MPlain a) (MPlain b)     = compare a b
+  compare a b                       = compare (mchunkText a) (mchunkText b)
+
+-- | A total extraction of the `Text` from an `MChunk`.
+mchunkText :: MChunk -> Text
+mchunkText (MDigit _ t) = t
+mchunkText (MRev _ t)   = t
+mchunkText (MPlain t)   = t
+
 -- | A (Complex) Mess. This is a /descriptive/ parser, based on examples of
 -- stupidly crafted version numbers used in the wild.
 --
 -- Groups of letters/numbers, separated by a period, can be further separated by
 -- the symbols @_-+:@
 --
--- Unfortunately, @VChunk@s cannot be used here, as some developers have numbers
--- like @1.003.04@ which make parsers quite sad.
---
 -- Some `Mess` values have a shape that is tantalizingly close to a `SemVer`.
 -- Example: @1.6.0a+2014+m872b87e73dfb-1@. For values like these, we can extract
 -- the semver-compatible values out with `messMajor`, etc.
@@ -615,27 +666,27 @@
 -- Not guaranteed to have well-defined ordering (@Ord@) behaviour, but so far
 -- internal tests show consistency. `messMajor`, etc., are used internally where
 -- appropriate to enhance accuracy.
-data Mess = VLeaf [T.Text] | VNode [T.Text] VSep Mess
+data Mess = Mess !(NonEmpty MChunk) !(Maybe (VSep, Mess))
   deriving stock (Eq, Show, Generic)
   deriving anyclass (NFData, Hashable)
 
 -- | Try to extract the "major" version number from `Mess`, as if it were a
 -- `SemVer`.
 messMajor :: Mess -> Maybe Word
-messMajor (VNode (m:_) _ _) = hush $ parse (digitsP <* eof) "Major" m
-messMajor _                 = Nothing
+messMajor (Mess (MDigit i _ :| _) _) = Just i
+messMajor _                          = Nothing
 
 -- | Try to extract the "minor" version number from `Mess`, as if it were a
 -- `SemVer`.
 messMinor :: Mess -> Maybe Word
-messMinor (VNode (_:m:_) _ _) = hush $ parse (digitsP <* eof) "Minor" m
-messMinor _                   = Nothing
+messMinor (Mess (_ :| MDigit i _ : _) _) = Just i
+messMinor _                              = Nothing
 
 -- | Try to extract the "patch" version number from `Mess`, as if it were a
 -- `SemVer`.
 messPatch :: Mess -> Maybe Word
-messPatch (VNode (_:_:p:_) _ _) = hush $ parse (digitsP <* eof) "Patch" p
-messPatch _                     = Nothing
+messPatch (Mess (_ :| _ : MDigit i _ : _) _) = Just i
+messPatch _                                  = Nothing
 
 -- | Okay, fine, say `messPatch` couldn't find a nice value. But some `Mess`es
 -- have a "proper" patch-plus-release-candidate value in their patch position,
@@ -643,30 +694,29 @@
 --
 -- Example: @1.6.0a+2014+m872b87e73dfb-1@ We should be able to extract @0a@ safely.
 messPatchChunk :: Mess -> Maybe VChunk
-messPatchChunk (VNode (_:_:p:_) _ _) = hush $ parse chunk "Chunk" p
-messPatchChunk _                     = Nothing
+messPatchChunk (Mess (_ :| _ : MPlain p : _) _) = hush $ parse chunk "Chunk" p
+messPatchChunk _                                = Nothing
 
 instance Ord Mess where
-  compare (VLeaf l1) (VLeaf l2)     = compare l1 l2
-  compare (VNode t1 _ _) (VLeaf t2) = compare t1 t2
-  compare (VLeaf t1) (VNode t2 _ _) = compare t1 t2
-  compare (VNode t1 _ v1) (VNode t2 _ v2) | t1 < t2 = LT
-                                          | t1 > t2 = GT
-                                          | otherwise = compare v1 v2
+  compare (Mess t1 Nothing) (Mess t2 Nothing) = compare t1 t2
+  compare (Mess t1 m1) (Mess t2 m2) = case compare t1 t2 of
+    EQ  -> case (m1, m2) of
+      (Just (_, v1), Just (_, v2)) -> compare v1 v2
+      (Just (_, _), Nothing)       -> GT
+      (Nothing, Just (_, _))       -> LT
+      (Nothing, Nothing)           -> EQ
+    res -> res
 
 instance Semantic Mess where
-  major f v@(VNode (t : ts) s ms) = either (const $ pure v) g $ parse digitsP "Major" t
-    where g n = (\n' -> VNode (showt n' : ts) s ms) <$> f n
+  major f (Mess (MDigit n _ :| ts) m) = (\n' -> Mess (MDigit n' (showt n') :| ts) m) <$> f n
   major _ v = pure v
   {-# INLINE major #-}
 
-  minor f v@(VNode (t0 : t : ts) s ms) = either (const $ pure v) g $ parse digitsP "Minor" t
-    where g n = (\n' -> VNode (t0 : showt n' : ts) s ms) <$> f n
+  minor f (Mess (t0 :| MDigit n _ : ts) m) = (\n' -> Mess (t0 :| MDigit n' (showt n') : ts) m) <$> f n
   minor _ v = pure v
   {-# INLINE minor #-}
 
-  patch f v@(VNode (t0 : t1 : t : ts) s ms) = either (const $ pure v) g $ parse digitsP "Patch" t
-    where g n = (\n' -> VNode (t0 : t1 : showt n' : ts) s ms) <$> f n
+  patch f (Mess (t0 :| t1 : MDigit n _ : ts) m) = (\n' -> Mess (t0 :| t1 : MDigit n' (showt n') : ts) m) <$> f n
   patch _ v = pure v
   {-# INLINE patch #-}
 
@@ -679,11 +729,8 @@
   {-# INLINE meta #-}
 
   -- | Good luck.
-  semantic f v@(VNode (t0 : t1 : t2 : _) _ _) = either (const $ pure v) (fmap (mFromV . vFromS)) $
-    (\a b c -> f $ SemVer a b c [] [])
-    <$> parse digitsP "Major" t0
-    <*> parse digitsP "Minor" t1
-    <*> parse digitsP "Patch" t2
+  semantic f (Mess (MDigit t0 _ :| MDigit t1 _ : MDigit t2 _ : _) _) =
+    mFromV . vFromS <$> (f $ SemVer t0 t1 t2 [] [])
   semantic _ v = pure v
   {-# INLINE semantic #-}
 
@@ -702,104 +749,120 @@
 -- Parsing
 
 -- | A synonym for the more verbose `megaparsec` error type.
-type ParsingError = ParseErrorBundle T.Text Void
+type ParsingError = ParseErrorBundle Text Void
 
--- | Parse a piece of `T.Text` into either an (Ideal) `SemVer`, a (General)
+-- | Parse a piece of `Text` into either an (Ideal) `SemVer`, a (General)
 -- `Version`, or a (Complex) `Mess`.
-versioning :: T.Text -> Either ParsingError Versioning
+versioning :: Text -> Either ParsingError Versioning
 versioning = parse versioning' "versioning"
 
 -- | Parse a `Versioning`. Assumes the version number is the last token in
 -- the string.
-versioning' :: Parsec Void T.Text Versioning
-versioning' = choice [ try (fmap Ideal semver'    <* eof)
-                     , try (fmap General version' <* eof)
-                     , fmap Complex mess'         <* eof ]
+versioning' :: Parsec Void Text Versioning
+versioning' = choice [ try (fmap Ideal semver''    <* eof)
+                     , try (fmap General version'' <* eof)
+                     , fmap Complex mess''         <* eof ]
 
 -- | Parse a (Ideal) Semantic Version.
-semver :: T.Text -> Either ParsingError SemVer
-semver = parse (semver' <* eof) "Semantic Version"
+semver :: Text -> Either ParsingError SemVer
+semver = parse (semver'' <* eof) "Semantic Version"
 
 -- | Internal megaparsec parser of `semver`.
-semver' :: Parsec Void T.Text SemVer
-semver' = L.lexeme space (SemVer <$> majorP <*> minorP <*> patchP <*> preRel <*> metaData)
+semver' :: Parsec Void Text SemVer
+semver' = L.lexeme space semver''
 
+semver'' :: Parsec Void Text SemVer
+semver'' = SemVer <$> majorP <*> minorP <*> patchP <*> preRel <*> metaData
+
 -- | Parse a group of digits, which can't be lead by a 0, unless it is 0.
-digitsP :: Parsec Void T.Text Word
+digitsP :: Parsec Void Text Word
 digitsP = read <$> ((T.unpack <$> string "0") <|> some digitChar)
 
-majorP :: Parsec Void T.Text Word
+majorP :: Parsec Void Text Word
 majorP = digitsP <* char '.'
 
-minorP :: Parsec Void T.Text Word
+minorP :: Parsec Void Text Word
 minorP = majorP
 
-patchP :: Parsec Void T.Text Word
+patchP :: Parsec Void Text Word
 patchP = digitsP
 
-preRel :: Parsec Void T.Text [VChunk]
+preRel :: Parsec Void Text [VChunk]
 preRel = (char '-' *> chunks) <|> pure []
 
-metaData :: Parsec Void T.Text [VChunk]
+metaData :: Parsec Void Text [VChunk]
 metaData = (char '+' *> chunks) <|> pure []
 
-chunks :: Parsec Void T.Text [VChunk]
+chunksNE :: Parsec Void Text (NonEmpty VChunk)
+chunksNE = chunk `PC.sepBy1` char '.'
+
+chunks :: Parsec Void Text [VChunk]
 chunks = chunk `sepBy` char '.'
 
 -- | Handling @0@ is a bit tricky. We can't allow runs of zeros in a chunk,
 -- since a version like @1.000.1@ would parse as @1.0.1@.
-chunk :: Parsec Void T.Text VChunk
-chunk = try zeroWithLetters <|> oneZero <|> many (iunit <|> sunit)
-  where oneZero = (:[]) . Digits . read . T.unpack <$> string "0"
+chunk :: Parsec Void Text VChunk
+chunk = try zeroWithLetters <|> oneZero <|> PC.some (iunit <|> sunit)
+  where oneZero = (:|[]) . Digits . read . T.unpack <$> string "0"
         zeroWithLetters = do
           z <- Digits . read . T.unpack <$> string "0"
-          s <- some sunit
-          c <- chunk
-          pure $ (z : s) ++ c
+          s <- PC.some sunit
+          c <- optional chunk
+          case c of
+            Nothing -> pure $ NEL.cons z s
+            Just c' -> pure $ NEL.cons z s <> c'
 
-iunit :: Parsec Void T.Text VUnit
+iunit :: Parsec Void Text VUnit
 iunit = Digits . read <$> some digitChar
 
-sunit :: Parsec Void T.Text VUnit
+sunit :: Parsec Void Text VUnit
 sunit = Str . T.pack <$> some letterChar
 
 -- | Parse a (Haskell) `PVP`, as defined above.
-pvp :: T.Text -> Either ParsingError PVP
+pvp :: Text -> Either ParsingError PVP
 pvp = parse (pvp' <* eof) "PVP"
 
 -- | Internal megaparsec parser of `pvp`.
-pvp' :: Parsec Void T.Text PVP
+pvp' :: Parsec Void Text PVP
 pvp' = L.lexeme space (PVP . NEL.fromList <$> L.decimal `sepBy` char '.')
 
 -- | Parse a (General) `Version`, as defined above.
-version :: T.Text -> Either ParsingError Version
-version = parse (version' <* eof) "Version"
+version :: Text -> Either ParsingError Version
+version = parse (version'' <* eof) "Version"
 
 -- | Internal megaparsec parser of `version`.
-version' :: Parsec Void T.Text Version
-version' = L.lexeme space (Version <$> optional (try epochP) <*> chunks <*> preRel)
+version' :: Parsec Void Text Version
+version' = L.lexeme space version''
 
-epochP :: Parsec Void T.Text Word
+version'' :: Parsec Void Text Version
+version'' = Version <$> optional (try epochP) <*> chunksNE <*> metaData <*> preRel
+
+epochP :: Parsec Void Text Word
 epochP = read <$> (some digitChar <* char ':')
 
 -- | Parse a (Complex) `Mess`, as defined above.
-mess :: T.Text -> Either ParsingError Mess
-mess = parse (mess' <* eof) "Mess"
+mess :: Text -> Either ParsingError Mess
+mess = parse (mess'' <* eof) "Mess"
 
 -- | Internal megaparsec parser of `mess`.
-mess' :: Parsec Void T.Text Mess
-mess' = L.lexeme space (try node <|> leaf)
+mess' :: Parsec Void Text Mess
+mess' = L.lexeme space mess''
 
-leaf :: Parsec Void T.Text Mess
-leaf = VLeaf <$> tchunks
+mess'' :: Parsec Void Text Mess
+mess'' = Mess <$> mchunks <*> optional ((,) <$> sep <*> mess')
 
-node :: Parsec Void T.Text Mess
-node = VNode <$> tchunks <*> sep <*> mess'
+mchunks :: Parsec Void Text (NonEmpty MChunk)
+mchunks = mchunk `PC.sepBy1` char '.'
 
-tchunks :: Parsec Void T.Text [T.Text]
-tchunks = (T.pack <$> some (letterChar <|> digitChar)) `sepBy` char '.'
+mchunk :: Parsec Void Text MChunk
+mchunk = choice [ try $ (\(t, i) -> MDigit i t) <$> match (L.decimal <* next)
+                , try $ (\(t, i) -> MRev i t) <$> (match (single 'r' *> L.decimal <* next))
+                , MPlain . T.pack <$> some (letterChar <|> digitChar) ]
+  where
+    next :: Parsec Void Text ()
+    next = lookAhead (void (single '.') <|> void sep <|> eof)
 
-sep :: Parsec Void T.Text VSep
+sep :: Parsec Void Text VSep
 sep = choice [ VColon  <$ char ':'
              , VHyphen <$ char '-'
              , VPlus   <$ char '+'
@@ -812,40 +875,56 @@
 sepCh VUnder  = '_'
 
 -- | Convert any parsed Versioning type to its textual representation.
-prettyV :: Versioning -> T.Text
+prettyV :: Versioning -> Text
 prettyV (Ideal sv)  = prettySemVer sv
 prettyV (General v) = prettyVer v
 prettyV (Complex m) = prettyMess m
 
 -- | Convert a `SemVer` back to its textual representation.
-prettySemVer :: SemVer -> T.Text
+prettySemVer :: SemVer -> Text
 prettySemVer (SemVer ma mi pa pr me) = mconcat $ ver <> pr' <> me'
-  where ver = intersperse "." [ showt ma, showt mi, showt pa ]
-        pr' = foldable [] ("-" :) $ intersperse "." (chunksAsT pr)
-        me' = foldable [] ("+" :) $ intersperse "." (chunksAsT me)
+  where
+    ver = intersperse "." [ showt ma, showt mi, showt pa ]
+    pr' = foldable [] ("-" :) $ intersperse "." (chunksAsT pr)
+    me' = foldable [] ("+" :) $ intersperse "." (chunksAsT me)
 
 -- | Convert a `PVP` back to its textual representation.
-prettyPVP :: PVP -> T.Text
+prettyPVP :: PVP -> Text
 prettyPVP (PVP (m :| rs)) = T.intercalate "." . map showt $ m : rs
 
 -- | Convert a `Version` back to its textual representation.
-prettyVer :: Version -> T.Text
-prettyVer (Version ep cs pr) = ep' <> mconcat (ver <> pr')
-  where ver = intersperse "." $ chunksAsT cs
-        pr' = foldable [] ("-" :) $ intersperse "." (chunksAsT pr)
-        ep' = maybe "" (\e -> showt e <> ":") ep
+prettyVer :: Version -> Text
+prettyVer (Version ep cs me pr) = ep' <> mconcat (ver <> me' <> pr')
+  where
+    ver = intersperse "." . chunksAsT $ NEL.toList cs
+    me' = foldable [] ("+" :) $ intersperse "." (chunksAsT me)
+    pr' = foldable [] ("-" :) $ intersperse "." (chunksAsT pr)
+    ep' = maybe "" (\e -> showt e <> ":") ep
 
 -- | Convert a `Mess` back to its textual representation.
-prettyMess :: Mess -> T.Text
-prettyMess (VLeaf t)     = mconcat $ intersperse "." t
-prettyMess (VNode t s v) = T.snoc t' (sepCh s) <> prettyMess v
-  where t' = mconcat $ intersperse "." t
+prettyMess :: Mess -> Text
+prettyMess (Mess t m) = case m of
+  Nothing     -> t'
+  Just (s, v) -> T.snoc t' (sepCh s) <> prettyMess v
+  where
+    t' :: Text
+    t' = fold . NEL.intersperse "." $ NEL.map mchunkText t
 
-chunksAsT :: [VChunk] -> [T.Text]
-chunksAsT = map (mconcat . map f)
-  where f (Digits i) = showt i
-        f (Str s)    = s
+chunksAsT :: Functor t => t VChunk -> t Text
+chunksAsT = fmap (foldMap f)
+  where
+    f :: VUnit -> Text
+    f (Digits i) = showt i
+    f (Str s)    = s
 
+chunksAsM :: Functor t => t VChunk -> t MChunk
+chunksAsM = fmap f
+  where
+    f :: VChunk -> MChunk
+    f (Digits i :| [])        = MDigit i $ showt i
+    f (Str "r" :| [Digits i]) = MRev i . T.cons 'r' $ showt i
+    f vc                      = MPlain . T.concat $ chunksAsT [vc]
+
 -- | Analogous to `maybe` and `either`. If a given Foldable is empty,
 -- a default value is returned. Else, a function is applied to that Foldable.
 foldable :: Foldable f => f b -> (f a -> f b) -> f a -> f b
@@ -859,7 +938,7 @@
 opposite GT = LT
 
 -- Yes, `text-show` exists, but this reduces external dependencies.
-showt :: Show a => a -> T.Text
+showt :: Show a => a -> Text
 showt = T.pack . show
 
 hush :: Either a b -> Maybe b
diff --git a/Setup.hs b/Setup.hs
deleted file mode 100644
--- a/Setup.hs
+++ /dev/null
@@ -1,2 +0,0 @@
-import Distribution.Simple
-main = defaultMain
diff --git a/test/Test.hs b/test/Test.hs
--- a/test/Test.hs
+++ b/test/Test.hs
@@ -1,13 +1,15 @@
+{-# LANGUAGE OverloadedLists   #-}
 {-# LANGUAGE OverloadedStrings #-}
 
 {-# OPTIONS_GHC -fno-warn-orphans #-}
 
-module Main where
+module Main ( main ) where
 
 import           Data.Char (chr)
 import           Data.Either (isLeft)
 import           Data.Foldable (fold)
 import           Data.List (groupBy)
+import qualified Data.List.NonEmpty as NEL
 import qualified Data.Text as T
 import           Data.Versions
 import           Data.Void (Void)
@@ -25,11 +27,14 @@
   arbitrary = SemVer <$> arbitrary <*> arbitrary <*> arbitrary <*> chunks <*> chunks
 
 -- | Sane generation of VChunks.
-chunks :: Gen [[VUnit]]
+chunks :: Gen [VChunk]
 chunks = resize 10 . listOf1 . fmap simplify . resize 10 $ listOf1 arbitrary
 
-simplify :: [VUnit] -> [VUnit]
-simplify = map fold . groupBy f
+chunksNE :: Gen (NEL.NonEmpty VChunk)
+chunksNE = NEL.fromList <$> chunks
+
+simplify :: [VUnit] -> NEL.NonEmpty VUnit
+simplify = NEL.fromList . map fold . groupBy f
   where f (Digits _) (Digits _) = True
         f (Str _) (Str _)       = True
         f _ _                   = False
@@ -45,16 +50,19 @@
   arbitrary = Letter . chr <$> choose (97, 122)
 
 instance Arbitrary Version where
-  arbitrary = Version <$> arbitrary <*> chunks <*> chunks
+  arbitrary = Version <$> arbitrary <*> chunksNE <*> chunks <*> chunks
 
 -- | These don't need to parse as a SemVer.
 goodVers :: [T.Text]
 goodVers = [ "1", "1.2", "1.0rc0", "1.0rc1", "1.1rc1", "1.58.0-3",  "44.0.2403.157-1"
            , "0.25-2",  "8.u51-1", "21-2", "7.1p1-1", "20150826-1", "1:0.10.16-3"
-           ]
+           , "1.11.0.git.20200404-1", "1.11.0+20200830-1" ]
 
+badVers :: [T.Text]
+badVers = ["", "1.2 "]
+
 messes :: [T.Text]
-messes = [ "10.2+0.93+1-1", "003.03-3", "002.000-7", "20.26.1_0-2" ]
+messes = [ "10.2+0.93+1-1", "003.03-3", "002.000-7", "20.26.1_0-2", "1.6.0a+2014+m872b87e73dfb-1" ]
 
 messComps :: [T.Text]
 messComps = [ "10.2+0.93+1-1", "10.2+0.93+1-2", "10.2+0.93+2-1"
@@ -63,7 +71,7 @@
 
 badSemVs :: [T.Text]
 badSemVs = [ "1", "1.2", "1.2.3+a1b2bc3.1-alpha.2", "a.b.c", "1.01.1"
-           , "1.2.3+a1b!2c3.1"
+           , "1.2.3+a1b!2c3.1", "", "1.2.3 "
            ]
 
 goodSemVs :: [T.Text]
@@ -123,6 +131,8 @@
     , testGroup "(General) Versions"
       [ testGroup "Good Versions" $
         map (\s -> testCase (T.unpack s) $ isomorphV s) goodVers
+      , testGroup "Bad Versions (shouldn't parse)" $
+        map (\s -> testCase (T.unpack s) $ assertBool "A bad version parsed" $ isLeft $ version s) badVers
       , testGroup "Comparisons" $
         testCase "1.2-5 < 1.2.3-1" (comp version "1.2-5" "1.2.3-1") :
         testCase "1.0rc1 < 1.0" (comp version "1.0rc1" "1.0") :
@@ -135,6 +145,8 @@
     , testGroup "(Complex) Mess"
       [ testGroup "Good Versions" $
         map (\s -> testCase (T.unpack s) $ isomorphM s) messes
+      , testGroup "Bad Versions (shouldn't parse)" $
+        map (\s -> testCase (T.unpack s) $ assertBool "A bad version parsed" $ isLeft $ mess s) badVers
       , testGroup "Comparisons" $
         map (\(a,b) -> testCase (T.unpack $ a <> " < " <> b) $ comp mess a b) $
         zip messComps (tail messComps)
@@ -159,10 +171,12 @@
         , testCase "1.2.3r1 is Version" $ check $ isVersion <$> versioning "1.2.3r1"
         , testCase "0.25-2 is Version" $ check $ isVersion <$> versioning "0.25-2"
         , testCase "1:1.2.3-1 is Version" $ check $ isVersion <$> versioning "1:1.2.3-1"
-        , testCase "1.2.3+1-1 is Mess" $ check $ isMess <$> versioning "1.2.3+1-1"
+        , testCase "1.2.3+1-1 is Version" $ check $ isVersion <$> versioning "1.2.3+1-1"
         , testCase "000.007-1 is Mess" $ check $ isMess <$> versioning "000.007-1"
         , testCase "20.26.1_0-2 is Mess" $ check $ isMess <$> versioning "20.26.1_0-2"
         ]
+      , testGroup "Bad Versions" $
+        map (\s -> testCase (T.unpack s) $ assertBool "A bad version parsed" $ isLeft $ versioning s) badVers
       , testGroup "Isomorphisms" $
         map (\s -> testCase (T.unpack s) $ isomorph s) $ goodSemVs ++ goodVers ++ messes
       , testGroup "Comparisons"
@@ -173,13 +187,16 @@
         , testCase "1.2-5 < 1.2.3-1"       $ comp versioning "1.2-5" "1.2.3-1"
         , testCase "1.6.0a+2014+m872b87e73dfb-1 < 1.6.0-1"
           $ comp versioning "1.6.0a+2014+m872b87e73dfb-1" "1.6.0-1"
+        , testCase "1.11.0.git.20200404-1 < 1.11.0+20200830-1"
+          $ comp versioning "1.11.0.git.20200404-1" "1.11.0+20200830-1"
+        , testCase "0.17.0+r8+gc41db5f1-1 < 0.17.0+r157+g584760cf-1"
+          $ comp versioning "0.17.0+r8+gc41db5f1-1" "0.17.0+r157+g584760cf-1"
         ]
       ]
     , testGroup "Lenses and Traversals"
       [ testCase "SemVer - Increment Patch" incPatch
       , testCase "SemVer - Increment Patch from Text" incFromT
       , testCase "SemVer - Get patches" patches
-      , testCase "Traverse `General` as `Ideal`" noInc
       ]
     , testGroup "Megaparsec Behaviour"
       [ testCase "manyTill" $ parse nameGrab "manyTill" "linux-firmware-3.2.14-1-x86_64.pkg.tar.xz" @?= Right "linux-firmware"
@@ -190,21 +207,31 @@
 
 -- | Does pretty-printing return a Versioning to its original form?
 isomorph :: T.Text -> Assertion
-isomorph t = Right t @=? (prettyV <$> versioning t)
+isomorph t = case prettyV <$> versioning t of
+  Right t' -> t @?= t'
+  Left e   -> assertBool (errorBundlePretty e) False
 
 -- | Does pretty-printing return a Version to its original form?
 isomorphV :: T.Text -> Assertion
-isomorphV t = Right t @=? (prettyVer <$> version t)
+isomorphV t = case prettyVer <$> version t of
+  Right t' -> t @?= t'
+  Left e   -> assertBool (errorBundlePretty e) False
 
 -- | Does pretty-printing return a SemVer to its original form?
 isomorphSV :: T.Text -> Assertion
-isomorphSV t = Right t @=? (prettySemVer <$> semver t)
+isomorphSV t = case prettySemVer <$> semver t of
+  Right t' -> t @?= t'
+  Left e   -> assertBool (errorBundlePretty e) False
 
 isomorphPVP :: T.Text -> Assertion
-isomorphPVP t = Right t @=? (prettyPVP <$> pvp t)
+isomorphPVP t = case prettyPVP <$> pvp t of
+  Right t' -> t @?= t'
+  Left e   -> assertBool (errorBundlePretty e) False
 
 isomorphM :: T.Text -> Assertion
-isomorphM t =  Right t @=? (prettyMess <$> mess t)
+isomorphM t = case prettyMess <$> mess t of
+  Right t' -> t @?= t'
+  Left e   -> assertBool (errorBundlePretty e) False
 
 comp :: Ord b => (T.Text -> Either a b) -> T.Text -> T.Text -> Assertion
 comp f a b = check $ (<) <$> f a <*> f b
@@ -228,11 +255,6 @@
 incPatch = (v1 & patch %~ (+ 1)) @?= v2
   where v1 = Ideal $ SemVer 1 2 3 [] []
         v2 = Ideal $ SemVer 1 2 4 [] []
-
--- | Nothing should happen.
-noInc :: Assertion
-noInc = (v & patch %~ (+ 1)) @?= v
-  where v = General $ Version Nothing [] []
 
 incFromT :: Assertion
 incFromT = (("1.2.3" :: T.Text) & patch %~ (+ 1)) @?= "1.2.4"
diff --git a/versions.cabal b/versions.cabal
--- a/versions.cabal
+++ b/versions.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.2
 name:               versions
-version:            3.5.4
+version:            4.0.0
 synopsis:           Types and parsers for software version numbers.
 description:
   A library for parsing and comparing software version numbers. We like to give
@@ -48,6 +48,7 @@
   build-depends:
     , deepseq   >=1.4
     , hashable  >=1.2
+    , parser-combinators >= 1.0
 
 test-suite versions-test
   import:         commons
