diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.markdown
@@ -0,0 +1,7 @@
+# Change log
+
+Salve uses [Semantic Versioning][].
+The change log is available through the [releases on GitHub][].
+
+[Semantic Versioning]: http://semver.org/spec/v2.0.0.html
+[releases on GitHub]: https://github.com/tfausak/salve/releases
diff --git a/LICENSE.markdown b/LICENSE.markdown
new file mode 100644
--- /dev/null
+++ b/LICENSE.markdown
@@ -0,0 +1,21 @@
+# [The MIT License](https://opensource.org/licenses/MIT)
+
+Copyright 2017 Taylor Fausak
+
+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.
diff --git a/benchmarks/benchmark.hs b/benchmarks/benchmark.hs
new file mode 100644
--- /dev/null
+++ b/benchmarks/benchmark.hs
@@ -0,0 +1,46 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE StandaloneDeriving #-}
+
+import Control.DeepSeq
+import Criterion.Main
+import GHC.Generics
+import Salve.Internal
+
+deriving instance Generic Build
+deriving instance Generic Constraint
+deriving instance Generic Operator
+deriving instance Generic PreRelease
+deriving instance Generic Version
+
+deriving instance NFData Build
+deriving instance NFData Constraint
+deriving instance NFData Operator
+deriving instance NFData PreRelease
+deriving instance NFData Version
+
+main :: IO ()
+main = defaultMain
+  [ bgroup "parseVersion" (map
+    (\ x -> bench x (nf parseVersion x))
+    [ "0.0.0"
+    , "123.456.789-pre.release+build.metadata"
+    ])
+  , bgroup "parseConstraint" (map
+    (\ x -> bench x (nf parseConstraint x))
+    [ ">=0.0.0"
+    , ">1.2.3 || =1.2.3 >=1.2.3 <1.2.3 || 1.2.3 <=1.2.3 ~1.2.3 ^1.2.3"
+    ])
+  , bgroup "renderVersion" (map
+    (\ x -> let y = unsafeParseVersion x in bench x (nf renderVersion y))
+    [ "0.0.0"
+    , "123.456.789-pre.release+build.metadata"
+    ])
+  , bgroup "renderConstraint" (map
+    (\ x -> let y = unsafeParseConstraint x in bench x (nf renderConstraint y))
+    [ ">=0.0.0"
+    , ">1.2.3 || =1.2.3 >=1.2.3 <1.2.3 || 1.2.3 <=1.2.3 ~1.2.3 ^1.2.3"
+    ])
+  ]
diff --git a/library/Salve.hs b/library/Salve.hs
new file mode 100644
--- /dev/null
+++ b/library/Salve.hs
@@ -0,0 +1,82 @@
+-- | This module defines types and functions for working with versions as
+-- defined by [Semantic Versioning](http://semver.org/spec/v2.0.0.html). It
+-- also provides types and functions for working with version constraints as
+-- described by [npm](https://docs.npmjs.com/misc/semver#ranges).
+module Salve (
+-- | This module doesn't export anything that conflicts with the "Prelude", so
+-- you can import it unqualified.
+--
+-- >>> import Salve
+--
+-- The 'Version' data type is the core of this module. Use 'parseVersion' to
+-- make versions and 'renderVersion' to convert them into strings.
+--
+-- >>> renderVersion <$> parseVersion "1.2.3"
+-- Just "1.2.3"
+--
+-- The 'Constraint' data type allows you to specify version constraints. Use
+-- 'parseConstraint' to make constraints and 'renderConstraint' to convert them
+-- into strings.
+--
+-- >>> renderConstraint <$> parseConstraint ">1.2.0"
+-- Just ">1.2.0"
+--
+-- Use 'satisfies' to see if a version satisfies a constraint.
+--
+-- >>> satisfies <$> parseVersion "1.2.3" <*> parseConstraint ">1.2.0"
+-- Just True
+
+-- * Types
+Version,
+PreRelease,
+Build,
+Constraint,
+
+-- * Parsing
+parseVersion,
+parsePreRelease,
+parseBuild,
+parseConstraint,
+
+-- ** Unsafe
+-- | These functions can be used to unsafely parse strings. Instead of
+-- returning 'Nothing', they raise an exception. Only use these if you are sure
+-- the string can be successfully parsed!
+unsafeParseVersion,
+unsafeParsePreRelease,
+unsafeParseBuild,
+unsafeParseConstraint,
+
+-- * Rendering
+renderVersion,
+renderPreRelease,
+renderBuild,
+renderConstraint,
+
+-- * Predicates
+isUnstable,
+isStable,
+
+-- * Helpers
+bumpMajor,
+bumpMinor,
+bumpPatch,
+satisfies,
+
+-- * Lenses
+-- | These lenses can be used to access and modify specific parts of a
+-- 'Version'.
+--
+-- Don't be scared by these type signatures. They are provided in full to avoid
+-- the @RankNTypes@ language extension. The type signature
+-- @'Functor' f => (a -> f a) -> 'Version' -> f 'Version'@ is the same as
+-- @'Lens.Micro.Lens'' 'Version' a@ (from "Lens.Micro"), which you may already
+-- be familiar with.
+majorLens,
+minorLens,
+patchLens,
+preReleasesLens,
+buildsLens,
+) where
+
+import Salve.Internal
diff --git a/library/Salve/Internal.hs b/library/Salve/Internal.hs
new file mode 100644
--- /dev/null
+++ b/library/Salve/Internal.hs
@@ -0,0 +1,805 @@
+-- | WARNING: This module should be considered private! If you find yourself
+-- wanting to import something from this module, please open an issue to get
+-- that thing exported from "Salve".
+module Salve.Internal where
+
+-- $setup
+-- >>> import Lens.Micro.Extras
+
+-- * Public
+
+-- | A semantic version number. Versions have five parts:
+--
+-- 1. 'majorLens': The major version number.
+-- 2. 'minorLens': The minor version number.
+-- 3. 'patchLens': The patch version number.
+-- 4. 'preReleasesLens': A list of pre-release identifiers.
+-- 5. 'buildsLens': A list of build metadata.
+--
+-- Use 'parseVersion' to create versions.
+data Version = Version
+  { versionMajor :: Word
+  , versionMinor :: Word
+  , versionPatch :: Word
+  , versionPreReleases :: [PreRelease]
+  , versionBuilds :: [Build]
+  } deriving (Eq, Show)
+
+-- | In general, 'Version's compare in the way that you would expect. First the
+-- major version numbers are compared, then the minors, then the patches.
+--
+-- >>> compare <$> parseVersion "1.2.3" <*> parseVersion "2.0.0"
+-- Just LT
+-- >>> compare <$> parseVersion "1.2.3" <*> parseVersion "1.3.0"
+-- Just LT
+-- >>> compare <$> parseVersion "1.2.3" <*> parseVersion "1.2.4"
+-- Just LT
+--
+-- Numbers are compared numerically, not alphabetically.
+--
+-- >>> compare <$> parseVersion "0.0.9" <*> parseVersion "0.0.10"
+-- Just LT
+--
+-- If all the numbers are the same, the pre-releases are compared.
+--
+-- >>> compare <$> parseVersion "1.2.3-a" <*> parseVersion "1.2.3-b"
+-- Just LT
+--
+-- A version with a pre-release is always less than a version without one as
+-- long as the other parts are the same.
+--
+-- >>> compare <$> parseVersion "1.2.3-pre" <*> parseVersion "1.2.3"
+-- Just LT
+-- >>> compare <$> parseVersion "1.2.4-pre" <*> parseVersion "1.2.3"
+-- Just GT
+--
+-- Builds are not considered when comparing versions.
+--
+-- >>> compare <$> parseVersion "1.2.3+a" <*> parseVersion "1.2.3+b"
+-- Just EQ
+-- >>> (==) <$> parseVersion "1.2.3+a" <*> parseVersion "1.2.3+b"
+-- Just False
+instance Ord Version where
+  compare x y = mconcat
+    [ comparing versionMajor x y
+    , comparing versionMinor x y
+    , comparing versionPatch x y
+    , case both versionPreReleases (x, y) of
+      ([], []) -> EQ
+      ([], _) -> GT
+      (_, []) -> LT
+      (p, q) -> compare p q
+    ]
+
+-- | Pre-release information attached to a version. These can either be numeric
+-- or textual. They must not be empty.
+--
+-- - Numeric: Can be any non-negative integer. Cannot have leading zeros.
+--
+-- - Textual: Can be any string of ASCII digits, letters, or hyphens. Cannot be
+--   all digits, as that would be numeric.
+--
+-- In general, pre-releases must match the regular expression
+-- @\/^[-0-9A-Za-z]+$\/@.
+--
+-- Use 'parsePreRelease' to create pre-releases.
+data PreRelease
+  = PreReleaseNumeric Word
+  | PreReleaseTextual String
+  deriving (Eq, Show)
+
+-- | Numeric pre-releases are always less than textual pre-releases.
+--
+-- >>> compare <$> parsePreRelease "1" <*> parsePreRelease "a"
+-- Just LT
+--
+-- Numeric pre-releases are compared numerically.
+--
+-- >>> compare <$> parsePreRelease "9" <*> parsePreRelease "10"
+-- Just LT
+--
+-- Textual pre-releases are compared alphabetically.
+--
+-- >>> compare <$> parsePreRelease "p10" <*> parsePreRelease "p9"
+-- Just LT
+instance Ord PreRelease where
+  compare x y = case (x, y) of
+    (PreReleaseNumeric n, PreReleaseNumeric m) -> compare n m
+    (PreReleaseNumeric _, PreReleaseTextual _) -> LT
+    (PreReleaseTextual _, PreReleaseNumeric _) -> GT
+    (PreReleaseTextual s, PreReleaseTextual t) -> compare s t
+
+-- | Build metadata attached to a version. These are similar to
+-- 'PreRelease's with some key differences:
+--
+-- 1. There is no such thing as numeric builds. Even though builds can look
+--    like numbers, all builds are textual.
+-- 2. As a result, builds that look numeric are allowed to have leading zeros.
+-- 3. Builds cannot be compared. That is, they do not have an 'Ord' instance.
+--
+-- Use 'parseBuild' to create builds.
+newtype Build = Build String deriving (Eq, Show)
+
+-- | Constrains allowable version numbers.
+--
+-- Use 'parseConstraint' to create constraints and 'satisfies' to see if a
+-- version number satisfies a constraint.
+data Constraint
+  = ConstraintCompare Operator Version
+  | ConstraintAnd Constraint Constraint
+  | ConstraintOr Constraint Constraint
+  deriving (Eq, Show)
+
+-- | Attempts to parse a version. This parser follows [SemVer's
+-- BNF](https://github.com/mojombo/semver/blob/eb9aac5/semver.md#backusnaur-form-grammar-for-valid-semver-versions).
+--
+-- >>> parseVersion "1.2.3-p.4+b.5"
+-- Just (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [PreReleaseTextual "p",PreReleaseNumeric 4], versionBuilds = [Build "b",Build "5"]})
+--
+-- Returns 'Nothing' if the parse fails.
+--
+-- >>> parseVersion "wrong"
+-- Nothing
+--
+-- Whitespace is not allowed and will cause the parser to fail.
+--
+-- >>> parseVersion " 1.2.3 "
+-- Nothing
+parseVersion :: String -> Maybe Version
+parseVersion s = parse versionP s
+
+-- | Attempts to parse a pre-release.
+--
+-- >>> parsePreRelease "pre"
+-- Just (PreReleaseTextual "pre")
+-- >>> parsePreRelease "1"
+-- Just (PreReleaseNumeric 1)
+--
+-- Returns 'Nothing' if the parse fails.
+--
+-- >>> parsePreRelease "wrong!"
+-- Nothing
+--
+-- Numeric pre-releases cannot contain leading zeros.
+--
+-- >>> parsePreRelease "01"
+-- Nothing
+parsePreRelease :: String -> Maybe PreRelease
+parsePreRelease s = parse preReleaseP s
+
+-- | Attempts to parse a build.
+--
+-- >>> parseBuild "build"
+-- Just (Build "build")
+-- >>> parseBuild "1"
+-- Just (Build "1")
+--
+-- Returns 'Nothing' if the parse fails.
+--
+-- >>> parseBuild "wrong!"
+-- Nothing
+--
+-- Unlike pre-releases, numeric builds can have leading zeros.
+--
+-- >>> parseBuild "01"
+-- Just (Build "01")
+parseBuild :: String -> Maybe Build
+parseBuild s = parse buildP s
+
+-- | Attempts to parse a constraint. This parser follows [npm's
+-- BNF](https://github.com/npm/npm/blob/d081cc6/doc/misc/semver.md#range-grammar),
+-- except that neither the so-called "x-ranges" nor partial version numbers are
+-- not supported. So you cannot use @1.2.x@ or @>1.2@ as version constraints.
+--
+-- >>> parseConstraint ">1.2.3"
+-- Just (ConstraintCompare OperatorGT (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}))
+--
+-- Returns 'Nothing' if the parse fails.
+--
+-- >>> parseConstraint "wrong"
+-- Nothing
+parseConstraint :: String -> Maybe Constraint
+parseConstraint s = parse constraintsP s
+
+-- | Parses a version.
+--
+-- >>> unsafeParseVersion "1.2.3-p.4+b.5"
+-- Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [PreReleaseTextual "p",PreReleaseNumeric 4], versionBuilds = [Build "b",Build "5"]}
+--
+-- Raises an exception if the parse fails.
+--
+-- >>> unsafeParseVersion "wrong"
+-- *** Exception: invalid version: "wrong"
+-- ...
+--
+-- See 'parseVersion' for a safe version of this function.
+unsafeParseVersion :: String -> Version
+unsafeParseVersion s = case parseVersion s of
+  Nothing -> error ("invalid version: " ++ show s)
+  Just v -> v
+
+-- | Parses a pre-release.
+--
+-- >>> unsafeParsePreRelease "pre"
+-- PreReleaseTextual "pre"
+--
+-- Raises an exception if the parse fails.
+--
+-- >>> unsafeParsePreRelease "wrong!"
+-- *** Exception: invalid pre-release: "wrong!"
+-- ...
+--
+-- See 'parsePreRelease' for a safe version of this function.
+unsafeParsePreRelease :: String -> PreRelease
+unsafeParsePreRelease s = case parsePreRelease s of
+  Nothing -> error ("invalid pre-release: " ++ show s)
+  Just p -> p
+
+-- | Parses a build.
+--
+-- >>> unsafeParseBuild "build"
+-- Build "build"
+--
+-- Raises an exception if the parse fails.
+--
+-- >>> unsafeParseBuild "wrong!"
+-- Build "*** Exception: invalid build: "wrong!"
+-- ...
+--
+-- See 'parseBuild' for a safe version of this function.
+unsafeParseBuild :: String -> Build
+unsafeParseBuild s = case parseBuild s of
+  Nothing -> error ("invalid build: " ++ show s)
+  Just b -> b
+
+-- | Parses a constraint.
+--
+-- >>> unsafeParseConstraint ">1.2.3"
+-- ConstraintCompare OperatorGT (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []})
+--
+-- Raises an exception if the parse fails.
+--
+-- >>> unsafeParseConstraint "wrong"
+-- *** Exception: invalid constraint: "wrong"
+-- ...
+--
+-- See 'parseConstraint' for a safe version of this function.
+unsafeParseConstraint :: String -> Constraint
+unsafeParseConstraint s = case parseConstraint s of
+  Nothing -> error ("invalid constraint: " ++ show s)
+  Just c -> c
+
+-- | Renders a version.
+--
+-- >>> renderVersion <$> parseVersion "1.2.3-p.4+b.5"
+-- Just "1.2.3-p.4+b.5"
+renderVersion :: Version -> String
+renderVersion v = mconcat
+  [ show (versionMajor v)
+  , "."
+  , show (versionMinor v)
+  , "."
+  , show (versionPatch v)
+  , renderPreReleases (versionPreReleases v)
+  , renderBuilds (versionBuilds v)
+  ]
+
+-- | Renders a pre-release.
+--
+-- >>> renderPreRelease <$> parsePreRelease "pre"
+-- Just "pre"
+-- >>> renderPreRelease <$> parsePreRelease "1"
+-- Just "1"
+renderPreRelease :: PreRelease -> String
+renderPreRelease p = case p of
+  PreReleaseNumeric n -> show n
+  PreReleaseTextual s -> s
+
+-- | Renders a build.
+--
+-- >>> renderBuild <$> parseBuild "build"
+-- Just "build"
+-- >>> renderBuild <$> parseBuild "1"
+-- Just "1"
+renderBuild :: Build -> String
+renderBuild (Build b) = b
+
+-- | Renders a constraint.
+--
+-- >>> renderConstraint <$> parseConstraint ">1.2.3"
+-- Just ">1.2.3"
+--
+-- Parsing and rendering a constraint doesn't always return what you started
+-- with.
+--
+-- >>> renderConstraint <$> parseConstraint "=1.2.3"
+-- Just "1.2.3"
+renderConstraint :: Constraint -> String
+renderConstraint c = case c of
+  ConstraintCompare o v ->
+    let s = renderVersion v
+    in case o of
+      OperatorLT -> '<' : s
+      OperatorLE -> '<' : '=' : s
+      OperatorEQ -> s
+      OperatorGE -> '>' : '=' : s
+      OperatorGT -> '>' : s
+  ConstraintAnd l r -> join ' ' (map renderConstraint [l, r])
+  ConstraintOr l r -> join ' ' [renderConstraint l, "||", renderConstraint r]
+
+-- | Returns 'True' if the major version number is zero, 'False' otherwise.
+--
+-- >>> isUnstable <$> parseVersion "0.1.2"
+-- Just True
+-- >>> isUnstable <$> parseVersion "1.0.0"
+-- Just False
+isUnstable :: Version -> Bool
+isUnstable v = versionMajor v == 0
+
+-- | Returns 'True' if the major version number is not zero, 'False' otherwise.
+--
+-- >>> isStable <$> parseVersion "1.0.0"
+-- Just True
+-- >>> isStable <$> parseVersion "0.1.2"
+-- Just False
+isStable :: Version -> Bool
+isStable v = not (isUnstable v)
+
+-- | Increments the major version number.
+--
+-- >>> bumpMajor <$> parseVersion "0.0.0"
+-- Just (Version {versionMajor = 1, versionMinor = 0, versionPatch = 0, versionPreReleases = [], versionBuilds = []})
+--
+-- The minor and patch numbers are reset to zero.
+--
+-- >>> bumpMajor <$> parseVersion "1.2.3"
+-- Just (Version {versionMajor = 2, versionMinor = 0, versionPatch = 0, versionPreReleases = [], versionBuilds = []})
+--
+-- The pre-releases and builds are removed.
+--
+-- >>> bumpMajor <$> parseVersion "0.0.0-pre+build"
+-- Just (Version {versionMajor = 1, versionMinor = 0, versionPatch = 0, versionPreReleases = [], versionBuilds = []})
+--
+-- Consider using 'majorLens' if you want to arbitrarily change the major
+-- number, or if you don't want the other parts of the version to change.
+bumpMajor :: Version -> Version
+bumpMajor v = v
+  { versionMajor = versionMajor v + 1
+  , versionMinor = 0
+  , versionPatch = 0
+  , versionPreReleases = []
+  , versionBuilds = []
+  }
+
+-- | Increments the minor version number.
+--
+-- >>> bumpMinor <$> parseVersion "0.0.0"
+-- Just (Version {versionMajor = 0, versionMinor = 1, versionPatch = 0, versionPreReleases = [], versionBuilds = []})
+--
+-- The patch number is reset to zero.
+--
+-- >>> bumpMinor <$> parseVersion "1.2.3"
+-- Just (Version {versionMajor = 1, versionMinor = 3, versionPatch = 0, versionPreReleases = [], versionBuilds = []})
+--
+-- The pre-releases and builds are removed.
+--
+-- >>> bumpMinor <$> parseVersion "0.0.0-pre+build"
+-- Just (Version {versionMajor = 0, versionMinor = 1, versionPatch = 0, versionPreReleases = [], versionBuilds = []})
+--
+-- Consider using 'minorLens' if you want to arbitrarily change the minor
+-- number, or if you don't want the other parts of the version to change.
+bumpMinor :: Version -> Version
+bumpMinor v = v
+  { versionMinor = versionMinor v + 1
+  , versionPatch = 0
+  , versionPreReleases = []
+  , versionBuilds = []
+  }
+
+-- | Increments the patch number.
+--
+-- >>> bumpPatch <$> parseVersion "0.0.0"
+-- Just (Version {versionMajor = 0, versionMinor = 0, versionPatch = 1, versionPreReleases = [], versionBuilds = []})
+--
+-- The major and minor numbers are not changed.
+--
+-- >>> bumpPatch <$> parseVersion "1.2.3"
+-- Just (Version {versionMajor = 1, versionMinor = 2, versionPatch = 4, versionPreReleases = [], versionBuilds = []})
+--
+-- The pre-releases and builds are removed.
+--
+-- >>> bumpPatch <$> parseVersion "0.0.0-pre+build"
+-- Just (Version {versionMajor = 0, versionMinor = 0, versionPatch = 1, versionPreReleases = [], versionBuilds = []})
+--
+-- Consider using 'patchLens' if you want to arbitrarily change the patch
+-- number, or if you don't want the other parts of the version to change.
+bumpPatch :: Version -> Version
+bumpPatch v = v
+  { versionPatch = versionPatch v + 1
+  , versionPreReleases = []
+  , versionBuilds = []
+  }
+
+-- | Returns 'True' if the version satisfies the constraint, 'False' otherwise.
+--
+-- >>> satisfies <$> parseVersion "1.2.3" <*> parseConstraint ">1.2.0"
+-- Just True
+satisfies :: Version -> Constraint -> Bool
+satisfies v c = case c of
+  ConstraintCompare o w -> case o of
+    OperatorLE -> v <= w
+    OperatorLT -> v < w
+    OperatorEQ -> compare v w == EQ
+    OperatorGT -> v > w
+    OperatorGE -> v >= w
+  ConstraintAnd l r -> satisfies v l && satisfies v r
+  ConstraintOr l r -> satisfies v l || satisfies v r
+
+-- | Focuses on the major version number.
+--
+-- >>> view majorLens <$> parseVersion "1.2.3-pre.4+build.5"
+-- Just 1
+majorLens :: Functor f => (Word -> f Word) -> Version -> f Version
+majorLens f v = fmap
+  (\ m -> v { versionMajor = m })
+  (f (versionMajor v))
+
+-- | Focuses on the minor version number.
+--
+-- >>> view minorLens <$> parseVersion "1.2.3-pre.4+build.5"
+-- Just 2
+minorLens :: Functor f => (Word -> f Word) -> Version -> f Version
+minorLens f v = fmap
+  (\ n -> v { versionMinor = n })
+  (f (versionMinor v))
+
+-- | Focuses on the patch version number.
+--
+-- >>> view patchLens <$> parseVersion "1.2.3-pre.4+build.5"
+-- Just 3
+patchLens :: Functor f => (Word -> f Word) -> Version -> f Version
+patchLens f v = fmap
+  (\ p -> v { versionPatch = p })
+  (f (versionPatch v))
+
+-- | Focuses on the pre-release identifiers.
+--
+-- >>> view preReleasesLens <$> parseVersion "1.2.3-pre.4+build.5"
+-- Just [PreReleaseTextual "pre",PreReleaseNumeric 4]
+preReleasesLens :: Functor f => ([PreRelease] -> f [PreRelease]) -> Version -> f Version
+preReleasesLens f v = fmap
+  (\ ps -> v { versionPreReleases = ps })
+  (f (versionPreReleases v))
+
+-- | Focuses on the build metadata.
+--
+-- >>> view buildsLens <$> parseVersion "1.2.3-pre.4+build.5"
+-- Just [Build "build",Build "5"]
+buildsLens :: Functor f => ([Build] -> f [Build]) -> Version -> f Version
+buildsLens f v = fmap
+  (\ bs -> v { versionBuilds = bs })
+  (f (versionBuilds v))
+
+-- * Private
+
+-- ** Types
+
+data Operator
+  = OperatorLT
+  | OperatorLE
+  | OperatorEQ
+  | OperatorGE
+  | OperatorGT
+  deriving (Eq, Show)
+
+constraintLT :: Version -> Constraint
+constraintLT v = ConstraintCompare OperatorLT v
+
+constraintLE :: Version -> Constraint
+constraintLE v = ConstraintCompare OperatorLE v
+
+constraintEQ :: Version -> Constraint
+constraintEQ v = ConstraintCompare OperatorEQ v
+
+constraintGE :: Version -> Constraint
+constraintGE v = ConstraintCompare OperatorGE v
+
+constraintGT :: Version -> Constraint
+constraintGT v = ConstraintCompare OperatorGT v
+
+constraintAnd :: Constraint -> Constraint -> Constraint
+constraintAnd l r = ConstraintAnd l r
+
+constraintOr :: Constraint -> Constraint -> Constraint
+constraintOr l r = ConstraintOr l r
+
+-- ** Parsing
+
+versionP :: Parser Version
+versionP = do
+  major <- numberP
+  _ <- charP '.'
+  minor <- numberP
+  _ <- charP '.'
+  patch <- numberP
+  preReleases <- preReleasesP
+  builds <- buildsP
+  pure Version
+    { versionMajor = major
+    , versionMinor = minor
+    , versionPatch = patch
+    , versionPreReleases = preReleases
+    , versionBuilds = builds
+    }
+
+preReleasesP :: Parser [PreRelease]
+preReleasesP = optionP [] (do
+  _ <- charP '-'
+  sepBy1P (charP '.') preReleaseP)
+
+preReleaseP :: Parser PreRelease
+preReleaseP = choiceP preReleaseStringP preReleaseNumberP
+
+preReleaseNumberP :: Parser PreRelease
+preReleaseNumberP = do
+  n <- numberP
+  pure (PreReleaseNumeric n)
+
+preReleaseStringP :: Parser PreRelease
+preReleaseStringP = do
+  s <- someP (satisfyP isIdentifier)
+  if all isAsciiDigit s
+    then failP
+    else pure (PreReleaseTextual s)
+
+buildsP :: Parser [Build]
+buildsP = optionP [] (do
+  _ <- charP '+'
+  sepBy1P (charP '.') buildP)
+
+buildP :: Parser Build
+buildP = do
+  b <- someP (satisfyP isIdentifier)
+  pure (Build b)
+
+numberP :: Parser Word
+numberP = choiceP zeroP nonZeroP
+
+zeroP :: Parser Word
+zeroP = do
+  _ <- charP '0'
+  pure 0
+
+nonZeroP :: Parser Word
+nonZeroP = do
+  x <- satisfyP isAsciiDigitNonZero
+  ys <- manyP (satisfyP isAsciiDigit)
+  pure (read (x : ys))
+
+constraintsP :: Parser Constraint
+constraintsP = do
+  _ <- spacesP
+  cs <- sepBy1P orP constraintP
+  _ <- spacesP
+  pure (foldr1 constraintOr cs)
+
+constraintP :: Parser Constraint
+constraintP = choiceP hyphenatedP simplesP
+
+hyphenatedP :: Parser Constraint
+hyphenatedP = do
+  v <- versionP
+  _ <- hyphenP
+  w <- versionP
+  pure (constraintAnd (constraintGE v) (constraintLE w))
+
+simplesP :: Parser Constraint
+simplesP = do
+  cs <- sepBy1P spaceP simpleP
+  pure (foldr1 constraintAnd cs)
+
+simpleP :: Parser Constraint
+simpleP = choiceP caretP (choiceP tildeP primitiveP)
+
+caretP :: Parser Constraint
+caretP = do
+  _ <- charP '^'
+  v <- versionP
+  let w = caretUpper v
+  pure (constraintAnd (constraintGE v) (constraintLT w))
+
+caretUpper :: Version -> Version
+caretUpper v =
+  if versionMinor v == 0
+  then bumpPatch v
+  else if versionMajor v == 0
+  then bumpMinor v
+  else bumpMajor v
+
+tildeP :: Parser Constraint
+tildeP = do
+  _ <- charP '~'
+  v <- versionP
+  let w = tildeUpper v
+  pure (constraintAnd (constraintGE v) (constraintLT w))
+
+tildeUpper :: Version -> Version
+tildeUpper v = bumpMinor v
+
+primitiveP :: Parser Constraint
+primitiveP = do
+  o <- operatorP
+  v <- versionP
+  pure (ConstraintCompare o v)
+
+operatorP :: Parser Operator
+operatorP = oneOfP [leP, geP, ltP, gtP, eqP, pure OperatorEQ]
+
+leP :: Parser Operator
+leP = do
+  _ <- stringP "<="
+  pure OperatorLE
+
+geP :: Parser Operator
+geP = do
+  _ <- stringP ">="
+  pure OperatorGE
+
+ltP :: Parser Operator
+ltP = do
+  _ <- charP '<'
+  pure OperatorLT
+
+gtP :: Parser Operator
+gtP = do
+  _ <- charP '>'
+  pure OperatorGT
+
+eqP :: Parser Operator
+eqP = do
+  _ <- charP '='
+  pure OperatorEQ
+
+hyphenP :: Parser String
+hyphenP = stringP " - "
+
+orP :: Parser String
+orP = stringP " || "
+
+spacesP :: Parser String
+spacesP = manyP spaceP
+
+spaceP :: Parser Char
+spaceP = charP ' '
+
+-- *** Helpers
+
+parse :: Parser a -> String -> Maybe a
+parse p s = safeHead (do
+  (x, "") <- runParser p s
+  pure x)
+
+newtype Parser a = Parser { runParser :: String -> [(a, String)] }
+
+instance Functor Parser where
+  fmap f p = Parser (\s -> do
+    (x, t) <- runParser p s
+    pure (f x, t))
+
+instance Applicative Parser where
+  pure x = Parser (\ s -> pure (x, s))
+
+  p <*> q = Parser (\ s -> do
+    (f, t) <- runParser p s
+    (x, u) <- runParser q t
+    pure (f x, u))
+
+instance Monad Parser where
+  fail x = Parser (\ _ -> fail x)
+
+  p >>= f = Parser (\ s -> do
+    (x, t) <- runParser p s
+    runParser (f x) t)
+
+charP :: Char -> Parser Char
+charP c = satisfyP (\ d -> d == c)
+
+choiceP :: Parser a -> Parser a -> Parser a
+choiceP p q = Parser (\ s -> case runParser p s of
+  [] -> runParser q s
+  xs -> xs)
+
+failP :: Parser a
+failP = fail undefined
+
+getP :: Parser Char
+getP = Parser (\ s -> case s of
+  "" -> []
+  c : t -> pure (c, t))
+
+manyP :: Parser a -> Parser [a]
+manyP p = choiceP (someP p) (pure [])
+
+oneOfP :: [Parser a] -> Parser a
+oneOfP ps = foldr choiceP failP ps
+
+optionP :: a -> Parser a -> Parser a
+optionP x p = choiceP p (pure x)
+
+satisfyP :: (Char -> Bool) -> Parser Char
+satisfyP f = do
+  c <- getP
+  if f c then pure c else failP
+
+sepByP :: Parser b -> Parser a -> Parser [a]
+sepByP q p = choiceP (sepBy1P q p) (pure [])
+
+sepBy1P :: Parser b -> Parser a -> Parser [a]
+sepBy1P q p = do
+  x <- p
+  xs <- optionP [] (do
+    _ <- q
+    sepBy1P q p)
+  pure (x : xs)
+
+someP :: Parser a -> Parser [a]
+someP p = do
+  x <- p
+  xs <- manyP p
+  pure (x : xs)
+
+stringP :: String -> Parser String
+stringP s = case s of
+  "" -> pure s
+  c : t -> do
+    _ <- charP c
+    _ <- stringP t
+    pure s
+
+-- ** Rendering
+
+renderPreReleases :: [PreRelease] -> String
+renderPreReleases ps = if null ps
+  then ""
+  else '-' : join '.' (map renderPreRelease ps)
+
+renderBuilds :: [Build] -> String
+renderBuilds bs = if null bs
+  then ""
+  else '+' : join '.' (map renderBuild bs)
+
+-- ** Helpers
+
+both :: (a -> b) -> (a, a) -> (b, b)
+both f (x, y) = (f x, f y)
+
+comparing :: Ord b => (a -> b) -> a -> a -> Ordering
+comparing f x y = compare (f x) (f y)
+
+isAsciiAlpha :: Char -> Bool
+isAsciiAlpha c = (isAsciiAlphaUpper c) || (isAsciiAlphaLower c)
+
+isAsciiAlphaLower :: Char -> Bool
+isAsciiAlphaLower c = ('a' <= c) && (c <= 'z')
+
+isAsciiAlphaUpper :: Char -> Bool
+isAsciiAlphaUpper c = ('A' <= c) && (c <= 'Z')
+
+isAsciiDigit :: Char -> Bool
+isAsciiDigit c = ('0' <= c) && (c <= '9')
+
+isAsciiDigitNonZero :: Char -> Bool
+isAsciiDigitNonZero c = isAsciiDigit c && (c /= '0')
+
+isIdentifier :: Char -> Bool
+isIdentifier c = (c == '-') || isAsciiDigit c || isAsciiAlpha c
+
+join :: Char -> [String] -> String
+join c ss = case ss of
+  [] -> ""
+  s : ts -> mconcat (s : map (\ t -> c : t) ts)
+
+safeHead :: [a] -> Maybe a
+safeHead xs = case xs of
+  [] -> Nothing
+  x : _ -> Just x
diff --git a/salve.cabal b/salve.cabal
new file mode 100644
--- /dev/null
+++ b/salve.cabal
@@ -0,0 +1,53 @@
+-- This file has been generated from package.yaml by hpack version 0.17.1.
+--
+-- see: https://github.com/sol/hpack
+
+name:           salve
+version:        0.0.0
+synopsis:       Semantic version numbers and constraints.
+description:    Salve provides semantic version numbers and constraints.
+category:       Distribution
+maintainer:     Taylor Fausak
+license:        MIT
+license-file:   LICENSE.markdown
+build-type:     Simple
+cabal-version:  >= 1.10
+
+extra-source-files:
+    CHANGELOG.markdown
+
+library
+  hs-source-dirs:
+      library
+  ghc-options: -Wall
+  build-depends:
+      base >=4.8.2 && <4.11
+  exposed-modules:
+      Salve
+      Salve.Internal
+  default-language: Haskell2010
+
+test-suite doctest
+  type: exitcode-stdio-1.0
+  main-is: doctest.hs
+  hs-source-dirs:
+      tests
+  ghc-options: -Wall
+  build-depends:
+      base >=4.8.2 && <4.11
+    , doctest >=0.10.1 && <0.14
+    , microlens >=0.3.5 && <0.5
+  default-language: Haskell2010
+
+benchmark benchmark
+  type: exitcode-stdio-1.0
+  main-is: benchmark.hs
+  hs-source-dirs:
+      benchmarks
+  ghc-options: -Wall
+  build-depends:
+      base >=4.8.2 && <4.11
+    , criterion >=1.1.0 && <1.3
+    , deepseq >=1.4.1 && <1.5
+    , salve
+  default-language: Haskell2010
diff --git a/tests/doctest.hs b/tests/doctest.hs
new file mode 100644
--- /dev/null
+++ b/tests/doctest.hs
@@ -0,0 +1,4 @@
+import Test.DocTest
+
+main :: IO ()
+main = doctest ["library"]
