diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,19 @@
+# Changelog
+
+All notable changes to `generic-diff` will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Haskell Package Versioning Policy](https://pvp.haskell.org).
+
+## [Unreleased]
+
+## [0.1.0.0] - 30.05.2025
+
+### Added
+
+- First version. Released on an unsuspecting world.
+- Let users extend the built-in diff types with custom diffs via the `SpecialDiff` class in [#9](https://github.com/fpringle/generic-diff/pull/9).
+- Add example implementations of `SpecialDiff` for `containers` types in [#10](https://github.com/fpringle/generic-diff/pull/10).
+
+[unreleased]: https://github.com/fpringle/generic-diff/compare/v0.1.0.0...HEAD
+[0.1.0.0]: https://github.com/fpringle/generic-diff/releases/tag/v0.1.0.0
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2025, Frederick Pringle
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Frederick Pringle nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,47 @@
+[![Haskell CI](https://github.com/fpringle/generic-diff/actions/workflows/haskell.yml/badge.svg)](https://github.com/fpringle/generic-diff/actions/workflows/haskell.yml)
+
+# Generic structural diffs
+
+`generic-diff` lets us pinpoint exactly where two values differ, which can be very useful, for example for debugging failing tests.
+This functionality is provided by the `Diff` typeclass, for which instances can be derived automatically using `Generic` from
+[generics-sop](https://github.com/well-typed/generics-sop).
+
+For detailed information, see the [Hackage docs](https://hackage.haskell.org/package/generic-diff/docs/Generics-Diff.html).
+
+## Example
+
+```haskell
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DeriveAnyClass #-}
+
+import Generics.Diff
+import Generics.Diff.Render
+
+import qualified GHC.Generics as G
+import qualified Generics.SOP as SOP
+
+data BinOp = Plus | Minus
+  deriving stock (Show, G.Generic)
+  deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo, Diff)
+
+data Expr
+  = Atom Int
+  | Bin {left :: Expr, op :: BinOp, right :: Expr}
+  deriving stock (Show, G.Generic)
+  deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo, Diff)
+
+expr1, expr2 :: Expr
+expr1 = Bin (Atom 1) Plus (Bin (Atom 1) Plus (Atom 1))
+expr2 = Bin (Atom 1) Plus (Bin (Atom 1) Plus (Atom 2))
+```
+
+```haskell
+ghci> printDiffResult $ diff expr1 expr2
+In field right:
+  Both values use constructor Bin but fields don't match
+  In field right:
+    Both values use constructor Atom but fields don't match
+    In field 0 (0-indexed):
+      Not equal
+```
diff --git a/generic-diff.cabal b/generic-diff.cabal
new file mode 100644
--- /dev/null
+++ b/generic-diff.cabal
@@ -0,0 +1,106 @@
+cabal-version:      3.0
+name:               generic-diff
+version:            0.1.0.0
+synopsis:           Generic structural diffs
+description:
+  Generic structural diffs on arbitrary datatypes,
+  using [generics-sop](https://hackage.haskell.org/package/generics-sop).
+
+  See the module documentation in "Generics.Diff".
+license:            BSD-3-Clause
+license-file:       LICENSE
+author:             Frederick Pringle
+maintainer:         freddyjepringle@gmail.com
+copyright:          Copyright(c) Frederick Pringle 2025
+homepage:           https://github.com/fpringle/generic-diff
+category:           Generics, Test
+build-type:         Simple
+extra-doc-files:    CHANGELOG.md
+                    README.md
+tested-with:
+  GHC == 9.12.2
+  GHC == 9.10.1
+  GHC == 9.8.2
+  GHC == 9.6.5
+  GHC == 9.4.8
+  GHC == 9.2.8
+  GHC == 9.0.2
+  GHC == 8.10.7
+  GHC == 8.6.5
+
+source-repository head
+  type:     git
+  location: https://github.com/fpringle/generic-diff.git
+
+common warnings
+  ghc-options: -Wall
+
+common deps
+  build-depends:
+    , base >= 4.12 && < 5
+    , sop-core >= 0.4.0.1 && < 0.6
+    , generics-sop >= 0.4 && < 0.6
+    , text >= 1.1 && < 2.2
+
+common extensions
+  default-extensions:
+    AllowAmbiguousTypes
+    ConstraintKinds
+    DataKinds
+    DefaultSignatures
+    DeriveGeneric
+    FlexibleContexts
+    FlexibleInstances
+    GADTs
+    LambdaCase
+    OverloadedStrings
+    PolyKinds
+    RankNTypes
+    RecordWildCards
+    ScopedTypeVariables
+    StandaloneDeriving
+    TypeApplications
+    TypeFamilies
+    TypeOperators
+    UndecidableInstances
+    ViewPatterns
+
+library
+  import:
+      warnings
+    , deps
+    , extensions
+  exposed-modules:
+      Generics.Diff
+      Generics.Diff.Instances
+      Generics.Diff.Render
+      Generics.Diff.Special
+      Generics.Diff.Special.List
+  other-modules:
+      Generics.Diff.Class
+      Generics.Diff.Type
+
+  hs-source-dirs:   src
+  default-language: Haskell2010
+
+test-suite generic-diff-test
+  import:
+      warnings
+    , deps
+    , extensions
+  default-language: Haskell2010
+  type:             exitcode-stdio-1.0
+  hs-source-dirs:   test
+  main-is:          Spec.hs
+  other-modules:
+    Generics.Diff.UnitTestsSpec
+    Generics.Diff.PropertyTestsSpec
+    Util
+  build-tool-depends:
+      hspec-discover:hspec-discover
+  ghc-options:      -Wno-orphans
+  build-depends:
+    , generic-diff
+    , QuickCheck
+    , hspec
+    , basic-sop
diff --git a/src/Generics/Diff.hs b/src/Generics/Diff.hs
new file mode 100644
--- /dev/null
+++ b/src/Generics/Diff.hs
@@ -0,0 +1,192 @@
+{-# OPTIONS_GHC -Wno-partial-fields #-}
+
+{- | Generic detailed comparisons, without boilerplate.
+
+The simplest way in Haskell that we can use to compare two values of the same type is using 'Eq'.
+The '(==)' operator gives us a simple 'Bool'ean response: 'True' if the values are equal, or 'False'
+otherwise.
+Slightly more informative is 'Ord': the 'compare' function (or operators '(<=)', '(>)' etc) tells
+us, if two values are different, which one can be considered as "less than" or "more than" the other.
+
+One situation in which these might not be enough is testing. Say you have a pair of (de)serialisation
+functions (which we'll imagine are total, for simplicity), and which we expect to be inverses:
+
+@
+serialise   :: (Serialise a) => a      -> String
+deserialise :: (Serialise a) => String -> a
+@
+
+Let's imagine we have some ([Hspec](https://hackage.haskell.org/package/hspec/docs/Test-Hspec.html))
+tests, e.g.:
+
+@
+unitTest :: (Serialise a) => a -> Spec
+unitTest value =
+  let serialised = serialise value
+      deserailised = deserialise serialised
+  in  it "Serialisation should satisfy the round-trip property" $ deserialised \`shouldBe\` value
+@
+
+What happens if the test fails? If we're dealing with a simple type like 'Int', the error will be very
+easy to debug:
+
+@
+  1) Serialisation should satisfy the round-trip property
+       expected: 2
+        but got: 1
+@
+
+But what if our type is much more complicated, with lots of constructors, fields, nesting...
+Especially if the type has a derived show instance, the output can be very dense, littered with
+parentheses, and overall very difficult to play "spot the difference" with.
+
+That's where this library comes in handy. Using the 'Diff' typeclass, we can identify precisely
+where two values differ. A "top-level" diff will tell you at which constructor and which field the values
+are different; if the types of those fields also have useful 'Diff' instances, we can recurse into them to
+pinpoint the error even more accurately. Even better, we can derive our 'Diff' instances completely
+automatically as long as our types have instances for 'Generics.SOP.Generic' (and
+'Generics.SOP.HasDatatypeInfo') from [generics-sop](https://hackage.haskell.org/package/generics-sop).
+In fact, we use the types 'Generics.SOP.NP' and 'Generics.SOP.NS' extensively to define the types we
+use for representing diffs.
+
+= Understanding
+
+To aid understanding (both for this library and for @generics-sop@), I think it helps to think of an ADT
+as a grid:
+
+@
+data MyType
+  = Con1 Char (Either Int String)
+  | Con2 [Bool]
+@
+
+This corresponds to a grid with one row per constructor, one column per field:
+
++----------------+---------------+-------------------+
+| Constructor    |             Fields                |
++================+===============+===================+
+| Con1           |   Char        | Either Int String |
++----------------+---------------+-------------------+
+| Con2           |   [Bool]      |    N / A          |
++----------------+---------------+-------------------+
+
+A value of type @MyType@ can be thought of as being one row of the grid, with one value for each
+column in the row.
+
+Now, if we have two values of type @MyType@, there's two main ways they can differ. If they inhabit
+different rows of the grid, they're clearly not equal, and all we can say is "this one uses this
+constructor, that one uses that constructor" (see 'WrongConstructor'). In other words we just report the
+names of the two rows.
+If they inhabit the same row of the grid, we have to go column by column, comparing each pair of values (we'll
+get to how we compare them in a second).
+If they're all pairwise equal, we can conclude the two values are 'Equal'; if one pair fails the comparison
+test, we stop checking and say "the types differ at this constructor, in this field" (see 'FieldMismatch').
+Effectively, we point to a single cell of the grid and say "that's where the inequality is".
+
+You might note that this process is very similar to how a stock-derived 'Eq' instance works. All we've added
+so far is an extra dimension to the output, detailing not just that two values differ, but where in the grid
+they differ. Where things get interesting is how we do the pairwise comparison between fields. If the
+comparison test is '(==)', then it's as above: we find out that two values are either equal, or not equal; and
+if they're not equal, we find out at which field that inequality happens. However, as in @MyType@ above, types
+often refer to other types! @Either Int String@ also has its own grid:
+
+@
+-- excuse the pseudo-Haskell
+type Either Int String
+  = Left Int
+  | Right String
+@
+
+Similar to above, we have:
+
++----------------+---------------+
+| Constructor    | Fields        |
++================+===============+
+| Left           |   Int         |
++----------------+---------------+
+| Right          |   String      |
++----------------+---------------+
+
+In fact, if we squint a bit, this grid actually exists __inside__ the cell of the @MyType@ grid:
+
++----------------+---------------+-------+--------+
+|                |               | Left  | Int    |
+| Con1           |   Char        +-------+--------+
+|                |               | Right | String |
++----------------+---------------+-------+--------+
+| Con2           |   [Bool]      |    N / A       |
++----------------+---------------+----------------+
+
+This gives us an extra level of granularity: when we get to the pair of @Either Int String@ fields, rather than
+just delegating to 'Eq', we can go through the same procedure as above. Then instead of "the two values differ at the
+@Either Int String@ field of the @Con1@ constructor", we can say "the two values differ at the @Either Int String@
+field of the @Con1@ constructor, and those two field differs because one uses the @Left@ constructor and the other
+uses the @Right@ constructor"! And of course, once we have one step of recursion, we have as many as we want...
+
+== Implementing instances
+
+The 'Diff' class encapsulates the above behaviour with 'diff'. It's very strongly recommended that you don't
+implement 'diff' yourself, but use the default implementation using 'Generics.SOP.Generic', which is just 'gdiff'.
+In case you might want to implement 'diff' yourself, there are three other functions you might want to use.
+
+- 'eqDiff' simply delegates the entire process to '(==)', and will only ever give 'Equal' or 'TopLevelNotEqual'. This is
+no more useful than 'Eq', and should only be used for primitive types (e.g. all numeric types like 'Char' and 'Int')
+use 'eqDiff', since they don't really have ADTs or recursion.
+
+- 'gdiffTopLevel' does the above process, but without recursion. In other words each pair of fields is compared using
+'(==)'. This is definitely better than 'Eq', by one "level". One situation when this might be useful is when your
+type refers to types from other libraries, and you want to avoid orphan 'Diff' instances for those types. Another
+is when the types of the fields are "small" enough that we don't care about recursing into them. For example:
+
+@
+data HttpVersion
+  = Http1
+  | Http2
+  deriving (Eq)
+
+data Request = Request
+  { host :: String
+  , port :: Int
+  -- there's no instance of 'Diff' for @Map@, so just compare for equality using '(==)'
+  , parameters :: Map String String
+  -- 'Diff' doesn't really add anything over 'Eq' for enum types, so 'Eq' is fine
+  , httpVersion :: HttpVersion
+  }
+  deriving stock (GHC.Generics.Generic)
+  deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo)
+
+instance 'Diff' Request where
+  'diff' = 'gdiffTopLevel'
+@
+
+- 'diffWithSpecial' lets us handle edge cases for funky types with unusual 'Eq' instances or preserved
+invariants. See "Generics.Diff.Special".
+
+For completeness, we also provide one more implementation function: 'gdiffWith' lets you provide a set of
+'Differ's (comparison functions) to use for each pair of fields (one per cell of the grid).
+I'm not sure in what situation you'd want this, but there you go.
+-}
+module Generics.Diff
+  ( -- * Class
+    Diff (..)
+
+    -- ** Implementing diff
+  , gdiff
+  , gdiffTopLevel
+  , gdiffWith
+  , eqDiff
+
+    -- * Types
+  , DiffResult (..)
+  , DiffError (..)
+  , DiffErrorNested (..)
+  , ListDiffError (..)
+  , DiffAtField (..)
+  , (:*:) (..)
+  , Differ (..)
+  )
+where
+
+import Generics.Diff.Class
+import Generics.Diff.Instances ()
+import Generics.Diff.Type
diff --git a/src/Generics/Diff/Class.hs b/src/Generics/Diff/Class.hs
new file mode 100644
--- /dev/null
+++ b/src/Generics/Diff/Class.hs
@@ -0,0 +1,285 @@
+{-# LANGUAGE EmptyCase #-}
+{-# OPTIONS_GHC -Wno-redundant-constraints -Wno-orphans #-}
+
+module Generics.Diff.Class
+  ( -- * Class
+    Diff (..)
+
+    -- ** Implementing diff
+  , gdiff
+  , gdiffTopLevel
+  , gdiffWith
+  , eqDiff
+  , diffWithSpecial
+  , gspecialDiffNested
+
+    -- * Special case: lists
+  , diffListWith
+  )
+where
+
+import Data.SOP
+import Data.SOP.NP
+import qualified GHC.Generics as G
+import Generics.Diff.Render
+import Generics.Diff.Type
+import Generics.SOP as SOP
+import Generics.SOP.GGP as SOP
+
+{- | A type with an instance of 'Diff' permits a more nuanced comparison than 'Eq' or 'Ord'.
+If two values are not equal, 'diff' will tell you exactly where they differ ("in this contructor,
+at that field"). The granularity of the pinpointing of the difference (how many "levels" of 'Diff'
+we can "descend" through) depends on the implementation of the instance.
+
+For user-defined types, it's strongly recommended you derive your 'Diff' instance using 'Generic' from
+@generics-sop@. If those types refer to other types, those will need 'Diff' instances too. For example:
+
+However, in some cases we'll want to use a custom type for representing diffs of user-defined or
+third-party types. For example, if we have non-derived `Eq` instances, invariants etc. In that case,
+see "Generics.Diff.Special".
+
+@
+{\-# LANGUAGE DerivingStrategies #-\}
+{\-# LANGUAGE DeriveGeneric #-\}
+{\-# LANGUAGE DeriveAnyClass #-\}
+
+import qualified GHC.Generics as G
+import qualified Generics.SOP as SOP
+
+data BinOp = Plus | Minus
+  deriving stock (Show, G.Generic)
+  deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo, Diff)
+
+data Expr
+  = Atom Int
+  | Bin {left :: Expr, op :: BinOp, right :: Expr}
+  deriving stock (Show, G.Generic)
+  deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo, Diff)
+@
+
+Now that we have our instances, we can 'diff' values to find out exactly where they differ:
+
+@
+-- If two values are equal, 'diff' should always return 'Equal'.
+ghci> diff Plus Plus
+Equal
+
+ghci> diff Plus Minus
+Error (Nested (WrongConstructor (Z (Constructor \"Plus\")) (S (Z (Constructor \"Minus\")))))
+
+ghci> diff (Atom 1) (Atom 2)
+Error (Nested (FieldMismatch (AtLoc (Z (Constructor \"Atom\" :*: Z (Nested TopLevelNotEqual))))))
+
+ghci> diff (Bin (Atom 1) Plus (Atom 1)) (Atom 2)
+Error (Nested (WrongConstructor (S (Z (Constructor \"Bin\"))) (Z (Constructor \"Atom\"))))
+
+ghci> diff (Bin (Atom 1) Plus (Atom 1)) (Bin (Atom 1) Minus (Atom 1))
+Error (Nested (FieldMismatch (AtLoc (S (Z (Constructor \"Bin\" :*: S (Z (Nested (WrongConstructor (Z (Constructor \"Plus\")) (S (Z (Constructor \"Minus\"))))))))))))
+
+ghci> diff (Bin (Atom 1) Plus (Atom 1)) (Bin (Atom 1) Plus (Atom 2))
+Error (Nested (FieldMismatch (DiffAtField (S (Z (Record \"Bin\" (FieldInfo \"left\" :* FieldInfo \"op\" :* FieldInfo \"right\" :* Nil) :*: S (S (Z (Nested (FieldMismatch (DiffAtField (Z (Constructor \"Atom\" :*: Z TopLevelNotEqual)))))))))))))
+@
+
+Of course, these are just as difficult to understand as derived 'Show' instances, or more so. Fortunately we can
+use the functions in "Generics.Diff.Render" to get a nice, intuitive representation of the diffs:
+
+@
+ghci> printDiffResult $ diff Plus Plus
+Equal
+
+ghci> printDiffResult $ diff Plus Minus
+Wrong constructor
+Constructor of left value: Plus
+Constructor of right value: Minus
+
+ghci> printDiffResult $ diff (Atom 1) (Atom 2)
+Both values use constructor Atom but fields don't match
+In field 0 (0-indexed):
+  Not equal
+
+ghci> printDiffResult $ diff (Bin (Atom 1) Plus (Atom 1)) (Atom 2)
+Wrong constructor
+Constructor of left value: Bin
+Constructor of right value: Atom
+
+ghci> printDiffResult $ diff (Bin (Atom 1) Plus (Atom 1)) (Bin (Atom 1) Minus (Atom 1))
+Both values use constructor Bin but fields don't match
+In field op:
+  Wrong constructor
+  Constructor of left value: Plus
+  Constructor of right value: Minus
+
+ghci> printDiffResult $ diff (Bin (Atom 1) Plus (Atom 1)) (Bin (Atom 1) Plus (Atom 2))
+Both values use constructor Bin but fields don't match
+In field right:
+  Both values use constructor Atom but fields don't match
+  In field 0 (0-indexed):
+    Not equal
+@
+
+= Laws
+
+For type @a@ with @instance Diff a@, and values @x, y :: a@, the following should hold:
+
+@
+x == y   \<=\>  x \`diff\` y == 'Equal'
+@
+-}
+class Diff a where
+  -- | Detailed comparison of two values. It is strongly recommended to only use the
+  -- default implementation, or one of 'eqDiff' or 'gdiffTopLevel'.
+  diff :: a -> a -> DiffResult a
+  default diff :: (Generic a, HasDatatypeInfo a, All2 Diff (Code a)) => a -> a -> DiffResult a
+  diff = gdiff
+
+  -- | Compare two lists of values. This mostly exists so that we can define a custom instance for 'String',
+  -- in a similar vein to 'showList'.
+  diffList :: [a] -> [a] -> DiffResult [a]
+  diffList = diffWithSpecial
+
+-- | When we have an instance of 'SpecialDiff', we can implement 'diff' using 'DiffSpecial'.
+diffWithSpecial :: (SpecialDiff a) => a -> a -> DiffResult a
+diffWithSpecial l r = maybe Equal (Error . DiffSpecial) $ specialDiff l r
+
+instance (Diff a) => SpecialDiff [a] where
+  type SpecialDiffError [a] = ListDiffError a
+  specialDiff = diffListWith diff
+  renderSpecialDiffError = listDiffErrorDoc "list"
+
+{- | Given two lists and a way to 'diff' the elements of the list,
+return a 'ListDiffError'. Used to implement 'specialDiff' for list-like types.
+See "Generics.Diff.Special" for an example.
+-}
+diffListWith :: (a -> a -> DiffResult a) -> [a] -> [a] -> Maybe (ListDiffError a)
+diffListWith d = go 0
+  where
+    go _ [] [] = Nothing
+    go n [] ys = Just $ WrongLengths n (n + length ys)
+    go n xs [] = Just $ WrongLengths (n + length xs) n
+    go n (x : xs) (y : ys) = case d x y of
+      Equal -> go (n + 1) xs ys
+      Error err -> Just $ DiffAtIndex n err
+
+{- | The most basic 'Differ' possible. If the two values are equal, return 'Equal';
+otherwise, return 'TopLevelNotEqual'.
+-}
+eqDiff :: (Eq a) => a -> a -> DiffResult a
+eqDiff a b =
+  if a == b
+    then Equal
+    else Error TopLevelNotEqual
+
+{- | The default implementation of 'diff'. Follows the procedure described above. We keep recursing
+into the 'Diff' instances of the field types, as far as we can.
+-}
+gdiff ::
+  forall a.
+  (Generic a, HasDatatypeInfo a, All2 Diff (Code a)) =>
+  a ->
+  a ->
+  DiffResult a
+gdiff = gdiffWithPure @a @Diff (Differ diff)
+
+{- | Alternate implementation of 'diff' - basically one level of 'gdiff'. To compare individual fields of the
+top-level values, we just use '(==)'.
+-}
+gdiffTopLevel ::
+  forall a.
+  (Generic a, HasDatatypeInfo a, All2 Eq (Code a)) =>
+  a ->
+  a ->
+  DiffResult a
+gdiffTopLevel = gdiffWithPure @a @Eq (Differ eqDiff)
+
+{- | Follow the same algorithm as 'gdiff', but the caller can provide their own 'POP' grid of 'Differ's
+specifying how to compare each field we might come across.
+-}
+gdiffWith ::
+  forall a.
+  (Generic a, HasDatatypeInfo a) =>
+  POP Differ (Code a) ->
+  a ->
+  a ->
+  DiffResult a
+gdiffWith (POP ds) (from -> SOP xs) (from -> SOP ys) =
+  maybe Equal (Error . Nested) $ gdiff' (constructorInfo $ datatypeInfo $ Proxy @a) ds xs ys
+
+gdiffWithPure ::
+  forall a c.
+  (Generic a, HasDatatypeInfo a, All2 c (Code a)) =>
+  (forall x. (c x) => Differ x) ->
+  a ->
+  a ->
+  DiffResult a
+gdiffWithPure ds = gdiffWith $ cpure_POP (Proxy @c) ds
+
+{- | Helper function to implement 'specialDiff' for an instance of "GHC.Generic", with
+@SpecialDiffError a = DiffErrorNested xss@.
+
+For example, say we want to implement 'SpecialDiff' (and then 'Diff') for @Tree@ from @containers@.
+We'd ideally like to use a 'SOP.Generic' instance, but we don't have one. Nevertheless we can fake one,
+using 'G.Generic' from "GHC.Generics".
+
+@
+data Tree a = Node
+  { rootLabel :: a
+  , subForest :: [Tree a]
+  }
+  deriving ('G.Generic')
+
+instance ('Diff' a) => 'SpecialDiff' (Tree a) where
+  type 'SpecialDiffError' (Tree a) = 'DiffErrorNested' ('GCode' (Tree a))
+  'specialDiff' = 'gspecialDiffNested'
+
+  'renderSpecialDiffError' = 'diffErrorNestedDoc'
+
+instance ('Diff' a) => 'Diff' (Tree a) where
+  diff = 'diffWithSpecial'
+@
+-}
+gspecialDiffNested ::
+  forall a.
+  ( G.Generic a
+  , GFrom a
+  , GDatatypeInfo a
+  , All2 Diff (GCode a)
+  ) =>
+  a ->
+  a ->
+  Maybe (DiffErrorNested (GCode a))
+gspecialDiffNested l r = gdiff' constructors differs (unSOP $ gfrom l) (unSOP $ gfrom r)
+  where
+    differs = unPOP $ hcpure (Proxy @Diff) (Differ diff)
+    constructors = constructorInfo $ gdatatypeInfo $ Proxy @a
+
+------------------------------------------------------------
+-- Auxiliary functions
+
+{- | This is the workhorse of 'gdiff', 'gdiffWith' and 'gdiffTopLevel'. A 'Nothing' return value means there
+were no errors (diffs), and so we can return 'Equal'. A 'Just' value means the two top-level values are
+not equal, and tells us where.
+-}
+gdiff' ::
+  NP ConstructorInfo xss ->
+  NP (NP Differ) xss ->
+  NS (NP I) xss ->
+  NS (NP I) xss ->
+  Maybe (DiffErrorNested xss)
+gdiff' _ Nil xss _ = case xss of {}
+gdiff' (i :* _) (ds :* _) (Z xs) (Z ys) =
+  FieldMismatch . DiffAtField . Z . (i :*:) <$> goProduct ds xs ys
+  where
+    goProduct :: forall as. NP Differ as -> NP I as -> NP I as -> Maybe (NS DiffError as)
+    goProduct Nil Nil Nil = Nothing
+    goProduct (Differ d :* ds') (I x :* bs) (I y :* cs) =
+      case d x y of
+        Equal -> S <$> goProduct ds' bs cs
+        Error err -> Just $ Z err
+gdiff' (i :* is) _ (Z _) (S rest) = Just $ WrongConstructor (Z i) (S $ pickOut is rest)
+gdiff' (i :* is) _ (S rest) (Z _) = Just $ WrongConstructor (S $ pickOut is rest) (Z i)
+gdiff' (_ :* is) (_ :* dss) (S xs) (S ys) = shiftDiffError <$> gdiff' is dss xs ys
+
+shiftDiffError :: DiffErrorNested xs -> DiffErrorNested (x ': xs)
+shiftDiffError = \case
+  WrongConstructor xs ys -> WrongConstructor (S xs) (S ys)
+  FieldMismatch (DiffAtField ns) -> FieldMismatch (DiffAtField (S ns))
diff --git a/src/Generics/Diff/Instances.hs b/src/Generics/Diff/Instances.hs
new file mode 100644
--- /dev/null
+++ b/src/Generics/Diff/Instances.hs
@@ -0,0 +1,353 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE EmptyCase #-}
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+{- | Here we define orphan instances for as many @base@ types as we can. These fall into a few categories:
+
+- Primitive types e.g. 'Char' and 'Int' - these typically don't have Generic instances, so we use 'eqDiff'
+- Opaque types that don't expose constructors, so we couldn't write hand-rolled instances if we want to -
+  all we have is an 'Eq' instance, so again we use 'eqDiff'.
+- Compound types e.g. 'Maybe' and 'Either' - these have 'Generic' instances so we can just use 'gdiff'.
+- List-like types such as @[a]@ and @'NE.NonEmpty' a@ - these we give slightly special treatment, since they're
+  so ubiquitous and 'gdiff' would produce very hard-to-read output.
+-}
+module Generics.Diff.Instances where
+
+import Control.Applicative
+import Control.Concurrent
+import Control.Exception
+#if MIN_VERSION_base(4,17,0)
+import Data.Array.Byte
+#endif
+import Data.Char
+import Data.Complex
+import Data.Data
+import Data.Fixed
+import qualified Data.Functor.Compose as F
+import Data.Functor.Identity
+import Data.Functor.Product
+import Data.Functor.Sum
+import Data.IORef
+import Data.Int
+import qualified Data.List.NonEmpty as NE
+import qualified Data.Monoid as M
+import Data.Ord
+import Data.Ratio
+import Data.SOP
+import Data.STRef
+import qualified Data.Semigroup as S
+import qualified Data.Text as T
+import Data.Text.Encoding.Error (UnicodeException)
+import qualified Data.Text.Lazy as TL
+import qualified Data.Text.Lazy.Builder as TLB
+import Data.Type.Coercion
+import Generics.Diff.Special.List ()
+#if MIN_VERSION_base(4,16,0)
+import Data.Type.Ord
+#endif
+import Data.Unique
+import Data.Version
+import Data.Void
+import Data.Word
+import Numeric.Natural
+#if MIN_VERSION_base(4,18,0)
+import Foreign.C.ConstPtr
+#endif
+import Foreign.C.Error
+import Foreign.C.Types
+import Foreign.ForeignPtr
+import Foreign.Ptr
+import Foreign.StablePtr
+import GHC.Arr
+import GHC.Base
+import GHC.ByteOrder
+import GHC.Conc
+import GHC.Event
+import GHC.Fingerprint
+import qualified GHC.Generics as G
+import GHC.IO.Buffer
+import GHC.IO.Device
+import GHC.IOArray
+#if MIN_VERSION_base(4,18,0)
+import GHC.InfoProv
+#endif
+import GHC.StableName
+import GHC.Stack
+#if MIN_VERSION_base(4,17,0)
+import GHC.Stack.CloneStack
+#endif
+import GHC.TypeLits
+import Generics.Diff.Class
+import Generics.Diff.Type
+import Generics.SOP
+import System.Exit
+import System.IO
+import System.IO.Error
+#if MIN_VERSION_base(4,14,0)
+import System.Timeout
+#endif
+import Text.Read (Lexeme)
+import qualified Type.Reflection as TR
+
+-- primitive(ish) types
+
+{- FOURMOLU_DISABLE -}
+
+instance Diff Void where diff = \case {}
+instance Diff () where diff () () = Equal
+instance Diff (Proxy a) where diff Proxy Proxy = Equal
+instance Diff Bool where diff = eqDiff
+instance Diff Ordering where diff = eqDiff
+instance Diff Double where diff = eqDiff
+instance Diff Float where diff = eqDiff
+instance Diff Int where diff = eqDiff
+instance Diff Int8 where diff = eqDiff
+instance Diff Int16 where diff = eqDiff
+instance Diff Int32 where diff = eqDiff
+instance Diff Int64 where diff = eqDiff
+instance Diff Word where diff = eqDiff
+instance Diff Word8 where diff = eqDiff
+instance Diff Word16 where diff = eqDiff
+instance Diff Word32 where diff = eqDiff
+instance Diff Word64 where diff = eqDiff
+instance Diff Version where diff = eqDiff
+
+instance Diff Char where
+  diff = eqDiff
+  diffList = eqDiff
+
+instance (Eq a) => Diff (Ratio a) where diff = eqDiff
+instance Diff Integer where diff = eqDiff
+instance Diff ThreadId where diff = eqDiff
+instance Diff (Chan a) where diff = eqDiff
+instance Diff (MVar a) where diff = eqDiff
+instance Diff (IORef a) where diff = eqDiff
+instance Diff TypeRep where diff = eqDiff
+instance Diff (TR.TypeRep a) where diff = eqDiff
+instance Diff TyCon where diff = eqDiff
+instance Diff (STRef s a) where diff = eqDiff
+instance Diff Unique where diff = eqDiff
+instance Diff (ForeignPtr a) where diff = eqDiff
+instance Diff (Ptr a) where diff = eqDiff
+instance Diff (FunPtr a) where diff = eqDiff
+instance Diff IntPtr where diff = eqDiff
+instance Diff WordPtr where diff = eqDiff
+instance Diff (StablePtr a) where diff = eqDiff
+instance Diff Handle where diff = eqDiff
+instance Diff HandlePosn where diff = eqDiff
+instance Diff (StableName a) where diff = eqDiff
+instance Diff (TVar a) where diff = eqDiff
+instance Diff Natural where diff = eqDiff
+instance Diff Event where diff = eqDiff
+
+instance Diff CChar where diff = eqDiff
+instance Diff CSChar where diff = eqDiff
+instance Diff CUChar where diff = eqDiff
+instance Diff CShort where diff = eqDiff
+instance Diff CUShort where diff = eqDiff
+instance Diff CInt where diff = eqDiff
+instance Diff CUInt where diff = eqDiff
+instance Diff CLong where diff = eqDiff
+instance Diff CULong where diff = eqDiff
+instance Diff CPtrdiff where diff = eqDiff
+instance Diff CSize where diff = eqDiff
+instance Diff CWchar where diff = eqDiff
+instance Diff CSigAtomic where diff = eqDiff
+instance Diff CLLong where diff = eqDiff
+instance Diff CULLong where diff = eqDiff
+instance Diff CIntPtr where diff = eqDiff
+instance Diff CUIntPtr where diff = eqDiff
+instance Diff CIntMax where diff = eqDiff
+instance Diff CUIntMax where diff = eqDiff
+instance Diff CClock where diff = eqDiff
+instance Diff CTime where diff = eqDiff
+instance Diff CUSeconds where diff = eqDiff
+instance Diff CSUSeconds where diff = eqDiff
+instance Diff CFloat where diff = eqDiff
+instance Diff CDouble where diff = eqDiff
+
+instance Diff E0
+instance Diff E1
+instance Diff E2
+instance Diff E3
+instance Diff E6
+instance Diff E9
+instance Diff E12
+
+instance Diff T.Text where diff = eqDiff
+instance Diff TL.Text where diff = eqDiff
+instance Diff TLB.Builder where diff = eqDiff
+instance Diff UnicodeException where diff = eqDiff
+
+instance Diff ArithException where diff = eqDiff
+instance Diff ArrayException where diff = eqDiff
+instance Diff Associativity where diff = eqDiff
+instance Diff AsyncException where diff = eqDiff
+instance Diff BlockReason where diff = eqDiff
+instance Diff BufferMode where diff = eqDiff
+instance Diff BufferState where diff = eqDiff
+#if MIN_VERSION_base(4,17,0)
+instance Diff ByteArray where diff = eqDiff
+#endif
+instance Diff ByteOrder where diff = eqDiff
+instance Diff CBool where diff = eqDiff
+instance Diff (Coercion a b) where diff = eqDiff
+#if MIN_VERSION_base(4,18,0)
+instance Diff (ConstPtr a) where diff = eqDiff
+#endif
+instance Diff DataRep where diff = eqDiff
+instance Diff G.DecidedStrictness where diff = eqDiff
+instance Diff Errno where diff = eqDiff
+instance Diff ErrorCall where diff = eqDiff
+instance Diff ExitCode where diff = eqDiff
+instance Diff FdKey where diff = eqDiff
+instance Diff Fingerprint where diff = eqDiff
+instance Diff G.Fixity where diff = eqDiff
+instance Diff GeneralCategory where diff = eqDiff
+#if MIN_VERSION_base(4,18,0)
+instance Diff InfoProv where diff = eqDiff
+#endif
+instance Diff (IOArray i e) where diff = eqDiff
+instance Diff IODeviceType where diff = eqDiff
+instance Diff IOErrorType where diff = eqDiff
+instance Diff IOException where diff = eqDiff
+instance Diff IOMode where diff = eqDiff
+instance Diff Lexeme where diff = eqDiff
+instance Diff Lifetime where diff = eqDiff
+instance Diff MaskingState where diff = eqDiff
+#if MIN_VERSION_base(4,17,0)
+instance Diff (MutableByteArray a) where diff = eqDiff
+#endif
+instance Diff NewlineMode where diff = eqDiff
+instance Diff Newline where diff = eqDiff
+#if MIN_VERSION_base(4,16,0)
+instance Diff (OrderingI a b) where diff = eqDiff
+#endif
+instance Diff SeekMode where diff = eqDiff
+#if MIN_VERSION_base(4,16,0)
+instance Diff SomeChar where diff = eqDiff
+#endif
+instance Diff SomeNat where diff = eqDiff
+instance Diff SomeSymbol where diff = eqDiff
+instance Diff SrcLoc where diff = eqDiff
+#if MIN_VERSION_base(4,17,0)
+instance Diff StackEntry where diff = eqDiff
+#endif
+instance Diff (STArray s i a) where diff = eqDiff
+instance Diff ThreadStatus where diff = eqDiff
+instance Diff TimeoutKey where diff = eqDiff
+#if MIN_VERSION_base(4,14,0)
+instance Diff Timeout where diff = eqDiff
+#endif
+instance Diff TrName where diff = eqDiff
+
+{- FOURMOLU_ENABLE -}
+
+-- list-like types
+
+instance (Diff a) => Diff [a] where
+  {-# SPECIALIZE instance Diff [Char] #-}
+  diff = diffList
+
+instance (Diff a) => Diff (NE.NonEmpty a) where
+  diff = diffWithSpecial
+
+-- combinators - typically we'll use gdiff
+
+{- FOURMOLU_DISABLE -}
+
+instance (Diff a) => Diff (Identity a)
+instance (Diff a) => Diff (I a)
+instance (Diff a) => Diff (Maybe a)
+instance (Diff a, Diff b) => Diff (Either a b)
+instance (Diff a) => Diff (Complex a)
+instance (Diff a) => Diff (M.Dual a)
+instance Diff M.All
+instance Diff M.Any
+instance (Diff a) => Diff (M.Sum a)
+instance (Diff a) => Diff (M.Product a)
+instance (Diff a) => Diff (M.First a)
+instance (Diff a) => Diff (M.Last a)
+
+instance
+  (HasDatatypeInfo (M.Alt f a), All2 Diff (Code (M.Alt f a))) =>
+  Diff (M.Alt f a)
+
+instance
+  (HasDatatypeInfo (M.Ap f a), All2 Diff (Code (M.Ap f a))) =>
+  Diff (M.Ap f a)
+
+instance (Diff a) => Diff (Down a)
+instance (Diff a) => Diff (S.Min a)
+instance (Diff a) => Diff (S.Max a)
+instance (Diff a) => Diff (S.First a)
+instance (Diff a) => Diff (S.Last a)
+instance (Diff a) => Diff (S.WrappedMonoid a)
+instance (Diff a, Diff b) => Diff (S.Arg a b)
+
+#if !MIN_VERSION_base(4,16,0)
+instance (Diff a) => Diff (S.Option a)
+#endif
+
+{- FOURMOLU_ENABLE -}
+
+-- with phantom types
+
+instance
+  (HasDatatypeInfo (Const a b), All2 Diff (Code (Const a b))) =>
+  Diff (Const a b)
+
+instance
+  (HasDatatypeInfo (K a b), All2 Diff (Code (K a b))) =>
+  Diff (K a b)
+
+instance
+  (HasDatatypeInfo (Fixed a), All2 Diff (Code (Fixed a))) =>
+  Diff (Fixed a)
+
+instance
+  (HasDatatypeInfo ((:.:) f g a), All2 Diff (Code ((:.:) f g a))) =>
+  Diff ((:.:) f g a)
+
+instance
+  (HasDatatypeInfo (F.Compose f g a), All2 Diff (Code (F.Compose f g a))) =>
+  Diff (F.Compose f g a)
+
+instance
+  (HasDatatypeInfo (Product f g a), All2 Diff (Code (Product f g a))) =>
+  Diff (Product f g a)
+
+instance
+  (HasDatatypeInfo (Sum f g a), All2 Diff (Code (Sum f g a))) =>
+  Diff (Sum f g a)
+
+-- tuples
+
+{- FOURMOLU_DISABLE -}
+
+instance (Diff a, Diff b) => Diff (a, b)
+instance (Diff a, Diff b, Diff c) => Diff (a, b, c)
+instance (Diff a, Diff b, Diff c, Diff d) => Diff (a, b, c, d)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e) => Diff (a, b, c, d, e)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f) => Diff (a, b, c, d, e, f)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g) => Diff (a, b, c, d, e, f, g)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h) => Diff (a, b, c, d, e, f, g, h)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i) => Diff (a, b, c, d, e, f, g, h, i)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i, Diff j) => Diff (a, b, c, d, e, f, g, h, i, j)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i, Diff j, Diff k) => Diff (a, b, c, d, e, f, g, h, i, j, k)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i, Diff j, Diff k, Diff l) => Diff (a, b, c, d, e, f, g, h, i, j, k, l)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i, Diff j, Diff k, Diff l, Diff m) => Diff (a, b, c, d, e, f, g, h, i, j, k, l, m)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i, Diff j, Diff k, Diff l, Diff m, Diff n) => Diff (a, b, c, d, e, f, g, h, i, j, k, l, m, n)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i, Diff j, Diff k, Diff l, Diff m, Diff n, Diff o) => Diff (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i, Diff j, Diff k, Diff l, Diff m, Diff n, Diff o, Diff p) => Diff (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i, Diff j, Diff k, Diff l, Diff m, Diff n, Diff o, Diff p, Diff q) => Diff (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i, Diff j, Diff k, Diff l, Diff m, Diff n, Diff o, Diff p, Diff q, Diff r) => Diff (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i, Diff j, Diff k, Diff l, Diff m, Diff n, Diff o, Diff p, Diff q, Diff r, Diff s) => Diff (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i, Diff j, Diff k, Diff l, Diff m, Diff n, Diff o, Diff p, Diff q, Diff r, Diff s, Diff t) => Diff (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i, Diff j, Diff k, Diff l, Diff m, Diff n, Diff o, Diff p, Diff q, Diff r, Diff s, Diff t, Diff u) => Diff (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i, Diff j, Diff k, Diff l, Diff m, Diff n, Diff o, Diff p, Diff q, Diff r, Diff s, Diff t, Diff u, Diff v) => Diff (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i, Diff j, Diff k, Diff l, Diff m, Diff n, Diff o, Diff p, Diff q, Diff r, Diff s, Diff t, Diff u, Diff v, Diff w) => Diff (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i, Diff j, Diff k, Diff l, Diff m, Diff n, Diff o, Diff p, Diff q, Diff r, Diff s, Diff t, Diff u, Diff v, Diff w, Diff x) => Diff (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i, Diff j, Diff k, Diff l, Diff m, Diff n, Diff o, Diff p, Diff q, Diff r, Diff s, Diff t, Diff u, Diff v, Diff w, Diff x, Diff y) => Diff (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y)
+instance (Diff a, Diff b, Diff c, Diff d, Diff e, Diff f, Diff g, Diff h, Diff i, Diff j, Diff k, Diff l, Diff m, Diff n, Diff o, Diff p, Diff q, Diff r, Diff s, Diff t, Diff u, Diff v, Diff w, Diff x, Diff y, Diff z) => Diff (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z)
+{- FOURMOLU_ENABLE -}
diff --git a/src/Generics/Diff/Render.hs b/src/Generics/Diff/Render.hs
new file mode 100644
--- /dev/null
+++ b/src/Generics/Diff/Render.hs
@@ -0,0 +1,252 @@
+{- | The types in 'Generic.Diff' have derived 'Show' instances that don't help at all in
+one of the goals for the library, which is readability. This module lets us render those types
+in a friendly way.
+-}
+module Generics.Diff.Render
+  ( -- * Rendering
+    renderDiffResult
+  , renderDiffResultWith
+
+    -- * Printing
+  , printDiffResult
+  , printDiffResultWith
+
+    -- * Options
+  , RenderOpts
+  , defaultRenderOpts
+  , indentSize
+  , numberedLevels
+
+    -- * Helper rendering functions
+  , renderDiffError
+  , renderDiffErrorWith
+  , renderDiffErrorNested
+  , renderDiffErrorNestedWith
+
+    -- * Intermediate representation
+  , Doc (..)
+  , diffErrorDoc
+  , renderDoc
+  , listDiffErrorDoc
+  , diffErrorNestedDoc
+  , showB
+  , linesDoc
+  , makeDoc
+  )
+where
+
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.SOP.NS
+import qualified Data.Text as T
+import qualified Data.Text.Lazy.Builder as TB
+import qualified Data.Text.Lazy.IO as TL
+import Generics.Diff.Type
+import Generics.SOP as SOP
+
+-- | Sensible rendering defaults. No numbers, 2-space indentation.
+defaultRenderOpts :: RenderOpts
+defaultRenderOpts =
+  RenderOpts
+    { indentSize = 2
+    , numberedLevels = False
+    }
+
+-- | Print a 'DiffResult' to the terminal.
+printDiffResult :: DiffResult a -> IO ()
+printDiffResult = printDiffResultWith defaultRenderOpts
+
+-- | Print a 'DiffResult' to the terminal, using custom 'RenderOpts'.
+printDiffResultWith :: RenderOpts -> DiffResult a -> IO ()
+printDiffResultWith opts =
+  TL.putStrLn . TB.toLazyText . renderDiffResultWith opts
+
+-- | Render a 'DiffResult' using a lazy 'TB.Builder'.
+renderDiffResult :: DiffResult a -> TB.Builder
+renderDiffResult = renderDiffResultWith defaultRenderOpts
+
+-- | Render a 'DiffResult' using a lazy 'TB.Builder', using custom 'RenderOpts'.
+renderDiffResultWith :: RenderOpts -> DiffResult a -> TB.Builder
+renderDiffResultWith opts = renderDoc opts 0 . diffResultDoc
+
+-- | Render a 'DiffError' using a lazy 'TB.Builder'.
+renderDiffError :: DiffError a -> TB.Builder
+renderDiffError = renderDiffErrorWith defaultRenderOpts
+
+-- | Render a 'DiffError' using a lazy 'TB.Builder', using custom 'RenderOpts'.
+renderDiffErrorWith :: RenderOpts -> DiffError a -> TB.Builder
+renderDiffErrorWith opts = renderDoc opts 0 . diffErrorDoc
+
+-- | Render a 'DiffErrorNested' using a lazy 'TB.Builder'.
+renderDiffErrorNested :: DiffErrorNested xss -> TB.Builder
+renderDiffErrorNested = renderDiffErrorNestedWith defaultRenderOpts
+
+-- | Render a 'DiffErrorNested' using a lazy 'TB.Builder', using custom 'RenderOpts'.
+renderDiffErrorNestedWith :: RenderOpts -> DiffErrorNested xss -> TB.Builder
+renderDiffErrorNestedWith opts = renderDoc opts 0 . diffErrorNestedDoc
+
+------------------------------------------------------------
+-- Doc representation
+-- Rendering a 'DiffResult' happens in two steps: converting our strict SOP types into a much simpler
+-- intermediate representation, and then laying them out in a nice way.
+
+-- | Create a 'Doc' with a non-empty list of lines and a nested error.
+makeDoc :: NonEmpty TB.Builder -> DiffError a -> Doc
+makeDoc ls err = Doc ls (Just $ diffErrorDoc err)
+
+-- | Create a simple 'Doc' without a nested error.
+linesDoc :: NonEmpty TB.Builder -> Doc
+linesDoc ls = Doc ls Nothing
+
+diffResultDoc :: DiffResult a -> Doc
+diffResultDoc = \case
+  Equal -> linesDoc (pure "Equal")
+  Error err -> diffErrorDoc err
+
+-- | Convert a 'DiffError' to a 'Doc'.
+diffErrorDoc :: forall a. DiffError a -> Doc
+diffErrorDoc = \case
+  TopLevelNotEqual -> linesDoc (pure "Not equal")
+  Nested err -> diffErrorNestedDoc err
+  DiffSpecial err -> renderSpecialDiffError @a err
+
+{- | Convert a 'ListDiffError' to a 'Doc'.
+
+The first argument gives us a name for the type of list, for clearer output.
+For example:
+
+@
+ghci> 'TL.putStrLn' . 'TB.toLazyText' . 'renderDoc' 'defaultRenderOpts' 0 . 'listDiffErrorDoc' "list" $ 'DiffAtIndex' 3 'TopLevelNotEqual'
+Diff at list index 3 (0-indexed)
+  Not equal
+
+ghci> TL.putStrLn . TB.toLazyText . renderDoc defaultRenderOpts 0 . listDiffErrorDoc "non-empty list" $ WrongLengths 3 5
+non-empty lists are wrong lengths
+Length of left list: 3
+Length of right list: 5
+@
+-}
+listDiffErrorDoc :: TB.Builder -> ListDiffError a -> Doc
+listDiffErrorDoc lst = \case
+  DiffAtIndex idx err ->
+    let lns = pure $ "Diff at " <> lst <> " index " <> showB idx <> " (0-indexed)"
+    in  makeDoc lns err
+  WrongLengths l r ->
+    linesDoc $
+      (lst <> "s are wrong lengths")
+        :| [ "Length of left list: " <> showB l
+           , "Length of right list: " <> showB r
+           ]
+
+{- | Convert a 'DiffErrorNested' to a 'Doc'.
+
+This is exported in the case that we want to implement an instance of 'Generics.Diff.Diff' for an existing type (e.g.
+from a 3rd-party library) that does not have a 'SOP.Generic' instance.
+-}
+diffErrorNestedDoc :: DiffErrorNested xss -> Doc
+diffErrorNestedDoc = \case
+  WrongConstructor l r ->
+    let cName = collapse_NS . liftANS (K . constructorNameR)
+        lCons = cName l
+        rCons = cName r
+    in  linesDoc $
+          "Wrong constructor"
+            :| [ "Constructor of left value: " <> lCons
+               , "Constructor of right value: " <> rCons
+               ]
+  FieldMismatch (DiffAtField ns) ->
+    let (cName, fieldLoc, err) =
+          collapse_NS $
+            liftANS (\(cInfo :*: nsErr) -> K (unpackAtLocErr cInfo nsErr)) ns
+        lns =
+          ("Both values use constructor " <> cName <> " but fields don't match")
+            :| [renderRField fieldLoc <> ":"]
+    in  Doc lns (Just err)
+
+{- | Render a 'Doc' as a text 'TB.Builder'. This should be the only way we escape a 'Doc'.
+
+The output can be configured using 'RenderOpts'.
+-}
+renderDoc :: RenderOpts -> Int -> Doc -> TB.Builder
+renderDoc opts ind = unlinesB . go ind
+  where
+    go n Doc {..} =
+      let otherIndent = mkIndent opts False n
+          firstIndent = mkIndent opts True n
+          l :| ls = docLines
+          firstLine = firstIndent <> l
+          otherLines = [otherIndent <> line | line <- ls]
+          allLines = firstLine : otherLines
+      in  case docSubDoc of
+            Nothing -> allLines
+            Just err -> allLines <> go (n + 1) err
+
+type RConstructorName = TB.Builder
+
+type RFieldName = TB.Builder
+
+data InfixSide = ILeft | IRight
+
+data RField
+  = IdxField Int
+  | InfixField InfixSide
+  | RecordField RFieldName
+
+constructorNameR :: ConstructorInfo xs -> RConstructorName
+constructorNameR = \case
+  Constructor name -> TB.fromString name
+  Infix name _ _ -> "(" <> TB.fromString name <> ")"
+  Record name _ -> TB.fromString name
+
+unpackAtLocErr :: forall xs. ConstructorInfo xs -> NS DiffError xs -> (RConstructorName, RField, Doc)
+unpackAtLocErr cInfo nsErr =
+  let err = collapse_NS $ liftANS (K . diffErrorDoc) nsErr
+  in  case cInfo of
+        Constructor name -> (TB.fromString name, IdxField $ index_NS nsErr, err)
+        Infix name _ _ ->
+          let side = case nsErr of
+                Z _ -> ILeft
+                S _ -> IRight
+          in  ("(" <> TB.fromString name <> ")", InfixField side, err)
+        Record name fields ->
+          let fName = collapse_NS $ liftANS (K . TB.fromString . fieldName) $ pickOut fields nsErr
+          in  (TB.fromString name, RecordField fName, err)
+
+renderRField :: RField -> TB.Builder
+renderRField = \case
+  IdxField n -> "In field " <> showB n <> " (0-indexed)"
+  InfixField side -> case side of
+    ILeft -> "In the left-hand field"
+    IRight -> "In the right-hand field"
+  RecordField fName -> "In field " <> fName
+
+------------------------------------------------------------
+-- Util
+
+unlinesB :: [TB.Builder] -> TB.Builder
+unlinesB (b : bs) = b <> TB.singleton '\n' <> unlinesB bs
+unlinesB [] = mempty
+
+-- | 'show' a value as a 'TB.Builder'.
+showB :: (Show a) => a -> TB.Builder
+showB = TB.fromString . show
+{-# INLINE showB #-}
+
+liftANS :: forall f g xs. (forall a. f a -> g a) -> NS f xs -> NS g xs
+liftANS f = go
+  where
+    go :: forall ys. NS f ys -> NS g ys
+    go = \case
+      Z z -> Z (f z)
+      S s -> S (go s)
+
+mkIndent :: RenderOpts -> Bool -> Int -> TB.Builder
+mkIndent RenderOpts {..} isFirst ind =
+  let spaces = TB.fromText (T.replicate (ind * fromIntegral indentSize) " ")
+      number = showB (ind + 1) <> ". "
+      noNumber = "   "
+
+      withNumber = spaces <> number
+      withoutNumber = spaces <> noNumber
+  in  if numberedLevels
+        then if isFirst then withNumber else withoutNumber
+        else spaces
diff --git a/src/Generics/Diff/Special.hs b/src/Generics/Diff/Special.hs
new file mode 100644
--- /dev/null
+++ b/src/Generics/Diff/Special.hs
@@ -0,0 +1,86 @@
+{- | 'SpecialDiff' lets us define diff types for edge cases. For example, say we want to use
+a type like 'ListDiffError' to diff lists in "one go", rather than recursing into a level of
+SOP for each new element we examine.
+
+Let's take a look at the implementation for lists:
+
+@
+data 'ListDiffError' a
+  = 'DiffAtIndex' Int ('DiffError' a)   -- there's a diff between two elements at this index
+  | 'WrongLengths' Int Int            -- one list is a (strict) prefix of the other
+
+instance ('Generics.Diff.Diff' a) => 'SpecialDiff' [a] where
+  type 'SpecialDiffError' [a] = 'ListDiffError' a
+  'specialDiff' = 'diffListWith' 'Generics.Diff.diff'
+  'renderSpecialDiffError' = 'Generics.Diff.Render.listDiffErrorDoc' "list"
+
+'diffListWith' :: (a -> a -> 'DiffResult' a) -> [a] -> [a] -> Maybe ('ListDiffError' a)
+'diffListWith' d = go 0
+  where
+    -- we compare each element pairwise.
+    go ::
+      -- current index
+      Int ->
+      -- remaining input lists
+      [a] -> [a] ->
+      Maybe ('ListDiffError' a)
+
+    -- base case: if we've reach the end of both lists, they're equal, return Nothing
+    go _ [] [] = Nothing
+
+    -- if we reach the end of one list first, return a 'WrongLengths'
+    go n [] ys = Just $ 'WrongLengths' n (n + length ys)
+    go n xs [] = Just $ 'WrongLengths' (n + length xs) n
+
+    -- recursive step: comparing the two head elements using the provider differ
+    go n (x : xs) (y : ys) = case d x y of
+      'Equal' ->
+        -- the head elements are equal, recurse
+        go (n + 1) xs ys
+      'Error' err ->
+        -- the head elements are not equal, return the error with the index
+        Just $ 'DiffAtIndex' n err
+
+-- To construct a 'Doc' we need some lines at the top, and optionally a sub-error.
+'Generics.Diff.Render.listDiffErrorDoc' :: 'TB.Builder' -> 'ListDiffError' a -> 'Doc'
+'Generics.Diff.Render.listDiffErrorDoc' lst = \case
+  'DiffAtIndex' idx err ->
+    let
+      -- top line
+      lns = pure $ "Diff at " <> lst <> " index " <> 'Generics.Diff.Render.showB' idx <> " (0-indexed)"
+    in
+      -- 'Generics.Diff.Render.makeDoc' is a smart constructor for a 'Doc' with a sub error
+      'Generics.Diff.Render.makeDoc' lns err
+  'WrongLengths' l r ->
+      -- 'Generics.Diff.Render.linesDoc' is a smart constructor for a 'Doc' without a sub error
+    'Generics.Diff.Render.linesDoc' $
+      (lst <> "s are wrong lengths")
+        :| [ "Length of left list: " <> 'Generics.Diff.Render.showB' l
+           , "Length of right list: " <> 'Generics.Diff.Render.showB' r
+           ]
+@
+
+Note that 'diffListWith' and 'Generics.Diff.Render.listDiffErrorDoc' are exported functions, rather than
+written inline, because there are other list-like types which will have almost identical instances and can
+reuse the code. For example, the implementation of 'SpecialDiff' for 'NE.NonEmpty' lists is:
+
+@
+instance ('Generics.Diff.Diff' a) => 'SpecialDiff' ('NE.NonEmpty' a) where
+  type 'SpecialDiffError' ('NE.NonEmpty' a) = 'ListDiffError' a
+  'specialDiff' l r = 'diffListWith' 'Generics.Diff.diff' ('NE.toList' l) ('NE.toList' r)
+  'renderSpecialDiffError' = 'Generics.Diff.Render.listDiffErrorDoc' "non-empty list"
+@
+-}
+module Generics.Diff.Special
+  ( SpecialDiff (..)
+  , diffWithSpecial
+  , gspecialDiffNested
+
+    -- * Lists
+  , module List
+  )
+where
+
+import Generics.Diff.Class
+import Generics.Diff.Special.List as List
+import Generics.Diff.Type
diff --git a/src/Generics/Diff/Special/List.hs b/src/Generics/Diff/Special/List.hs
new file mode 100644
--- /dev/null
+++ b/src/Generics/Diff/Special/List.hs
@@ -0,0 +1,21 @@
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+{- | Diffs on lists as a special case. See "Generics.Diff.Special" for a detailed explanation
+of the implementation.
+-}
+module Generics.Diff.Special.List
+  ( ListDiffError (..)
+  , diffListWith
+  )
+where
+
+import Data.Function (on)
+import qualified Data.List.NonEmpty as NE
+import Generics.Diff.Class
+import Generics.Diff.Render
+import Generics.Diff.Type
+
+instance (Diff a) => SpecialDiff (NE.NonEmpty a) where
+  type SpecialDiffError (NE.NonEmpty a) = ListDiffError a
+  specialDiff = diffListWith diff `on` NE.toList
+  renderSpecialDiffError = listDiffErrorDoc "non-empty list"
diff --git a/src/Generics/Diff/Type.hs b/src/Generics/Diff/Type.hs
new file mode 100644
--- /dev/null
+++ b/src/Generics/Diff/Type.hs
@@ -0,0 +1,230 @@
+{-# LANGUAGE EmptyCase #-}
+
+module Generics.Diff.Type where
+
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.SOP.NP
+import qualified Data.Text.Lazy.Builder as TB
+import Generics.SOP as SOP
+import Numeric.Natural
+
+------------------------------------------------------------
+-- Types
+
+{- | A newtype wrapping a binary function producing a 'DiffResult'.
+The only reason for this newtype is so that we can use it as a functor with the types from
+@generic-sop@.
+-}
+newtype Differ x = Differ (x -> x -> DiffResult x)
+
+{- | A GADT representing an error during the diff algorithm - i.e. this tells us where and how two values differ.
+
+The 'DiffSpecial' constructors for instances of 'SpecialDiff' are so that we can treat these types uniquely.
+See 'SpecialDiff'.
+-}
+data DiffError a where
+  -- | All we can say is that the values being compared are not equal.
+  TopLevelNotEqual :: DiffError a
+  -- | We've identified a diff at a certain constructor or field
+  Nested :: DiffErrorNested (Code a) -> DiffError a
+  -- | Special case for special cases
+  DiffSpecial :: (SpecialDiff a) => SpecialDiffError a -> DiffError a
+
+{- | If we did a normal 'Generics.Diff.gdiff' on a linked list, we'd have to recurse through one "level" of
+'Generics.Diff.Diff's for each element of the input lists. The output would be really hard to read or understand.
+Therefore this type lets us treat lists as a special case, depending on how they differ.
+-}
+data ListDiffError a
+  = -- | If we find a difference when comparing the two lists pointwise, we report the index of the
+    -- error and the error caused by the elements at that index of the input lists.
+    DiffAtIndex Int (DiffError a)
+  | -- | The two lists have different lengths. If we get a 'WrongLengths' instead of an 'Equal' or a
+    -- 'DiffAtIndex' , we know that one of the lists must be a subset of the other.
+    WrongLengths Int Int
+  deriving (Show, Eq)
+
+infixr 6 :*:
+
+-- | Lifted product of functors. We could have used 'Data.Functor.Product.Product', but this is more concise.
+data (f :*: g) a = f a :*: g a
+  deriving (Show, Eq)
+
+{- | This is where we actually detail the difference between two values, and where in their structure the
+difference is.
+-}
+data DiffErrorNested xss
+  = -- | The two input values use different constructor, which are included.
+    WrongConstructor (NS ConstructorInfo xss) (NS ConstructorInfo xss)
+  | -- | The inputs use the same constructor, but differ at one of the fields.
+    -- 'DiffAtField' will tell us where and how.
+    FieldMismatch (DiffAtField xss)
+
+-- | The result of a 'Generics.Diff.diff'.
+data DiffResult a
+  = -- | There's a diff, here it is
+    Error (DiffError a)
+  | -- | No diff, inputs are equal
+    Equal
+  deriving (Show, Eq)
+
+{- | In the case that two values have the same constructor but differ at a certain field, we want two
+report two things: what the 'DiffError' is at that field, and exactly where that field is. Careful use
+of 'NS' gives us both of those things.
+-}
+newtype DiffAtField xss = DiffAtField (NS (ConstructorInfo :*: NS DiffError) xss)
+
+------------------------------------------------------------
+-- Classes
+
+{- | Sometimes we want to diff types that don't quite fit the structor of a 'DiffErrorNested',
+such as lists (see 'ListDiffError'), or even user-defined types that internally preserve invariants
+or have unusual 'Eq' instances. In this case we can implement an instance of 'SpecialDiff' for the
+type.
+
+For concrete examples implementing 'SpecialDiff' on types from "containers", see the
+[examples/containers-instances](https://github.com/fpringle/generic-diff/tree/main/examples/containers-instances)
+directory.
+-}
+class (Show (SpecialDiffError a), Eq (SpecialDiffError a)) => SpecialDiff a where
+  -- | A custom diff error type for the special case.
+  type SpecialDiffError a
+
+  -- | Compare two values. The result will be converted to a 'DiffResult': 'Nothing' will result
+  -- in 'Equal', whereas a 'Just' result will be converted to a 'DiffError' using 'DiffSpecial'.
+  specialDiff :: a -> a -> Maybe (SpecialDiffError a)
+
+  -- | As well as specifying how two diff two values, we also have to specify how to render
+  -- the output. See the helper functions in "Generics.Diff.Render".
+  renderSpecialDiffError :: SpecialDiffError a -> Doc
+
+------------------------------------------------------------
+-- Rendering
+
+{- | Configuration type used to tweak the output of 'Generics.Diff.Render.renderDiffResultWith'.
+
+Use 'Generics.Diff.Render.defaultRenderOpts' and the field accessors below to construct.
+-}
+data RenderOpts = RenderOpts
+  { indentSize :: Natural
+  -- ^ How many spaces to indent each new "level" of comparison.
+  , numberedLevels :: Bool
+  -- ^ Whether or not to include level numbers in the output.
+  }
+  deriving (Show)
+
+{- | An intermediate representation for diff output.
+
+We constrain output to follow a very simple pattern:
+
+- 'docLines' is a non-empty series of preliminary lines describing the error.
+- 'docSubDoc' is an optional 'Doc' representing a nested error, e.g. in 'FieldMismatch'.
+-}
+data Doc = Doc
+  { docLines :: NonEmpty TB.Builder
+  , docSubDoc :: Maybe Doc
+  }
+  deriving (Show)
+
+------------------------------------------------------------
+-- Instance madness
+
+deriving instance (Show (DiffError a))
+
+deriving instance (Eq (DiffError a))
+
+eqPair :: (f a -> f a -> Bool) -> (g a -> g a -> Bool) -> (f :*: g) a -> (f :*: g) a -> Bool
+eqPair onF onG (f1 :*: g1) (f2 :*: g2) =
+  onF f1 f2 && onG g1 g2
+
+showsPair :: (Int -> f a -> ShowS) -> (Int -> g a -> ShowS) -> Int -> (f :*: g) a -> ShowS
+showsPair onF onG d (fa :*: ga) =
+  showParen (d > 5) $
+    onF 6 fa
+      . showString " :*: "
+      . onG 5 ga
+
+instance Eq (DiffAtField xss) where
+  DiffAtField mss == DiffAtField nss =
+    eqNS (eqPair eqConstructorInfo (eqNS (==))) mss nss
+
+instance Show (DiffAtField xss) where
+  showsPrec d (DiffAtField ns) =
+    showParen (d > 10) $
+      showString "DiffAtField "
+        . showsNS (showsPair showsConstructorInfo (showsNS showsPrec)) 11 ns
+
+eqNP :: forall f xs. (SListI xs) => (forall x. f x -> f x -> Bool) -> NP f xs -> NP f xs -> Bool
+eqNP eq l r = and $ collapse_NP $ liftA2_NP (\x y -> K (eq x y)) l r
+
+showsNP :: forall f xs. (forall x. Int -> f x -> ShowS) -> Int -> NP f xs -> ShowS
+showsNP _ _ Nil = showString "Nil"
+showsNP s d ns = go d ns
+  where
+    go :: forall ys. Int -> NP f ys -> ShowS
+    go p = \case
+      Nil -> showString "Nil"
+      x :* xs -> showParen (p > 5) $ s 6 x . showString " :* " . go 5 xs
+
+eqNS :: forall f xs. (forall x. f x -> f x -> Bool) -> NS f xs -> NS f xs -> Bool
+eqNS eq = compare_NS False eq False
+
+showsNS :: forall f xs. (forall x. Int -> f x -> ShowS) -> Int -> NS f xs -> ShowS
+showsNS s = go
+  where
+    go :: forall ys. Int -> NS f ys -> ShowS
+    go d =
+      showParen (d > 10) . \case
+        Z z -> showString "Z " . s 11 z
+        S zs -> showString "S " . go 11 zs
+
+eqFieldInfo :: FieldInfo x -> FieldInfo x -> Bool
+eqFieldInfo (FieldInfo fn1) (FieldInfo fn2) = fn1 == fn2
+
+eqConstructorInfo :: ConstructorInfo xs -> ConstructorInfo xs -> Bool
+eqConstructorInfo (Constructor cn1) (Constructor cn2) = cn1 == cn2
+eqConstructorInfo (Infix cn1 a1 f1) (Infix cn2 a2 f2) =
+  cn1 == cn2 && a1 == a2 && f1 == f2
+eqConstructorInfo (Record cn1 f1) (Record cn2 f2) =
+  cn1 == cn2 && eqNP eqFieldInfo f1 f2
+eqConstructorInfo _ _ = False
+
+showsConstructorInfo :: Int -> ConstructorInfo xs -> ShowS
+showsConstructorInfo d =
+  showParen (d > 10) . \case
+    Constructor name -> showString "Constructor " . showsPrec 11 name
+    Infix name ass fix ->
+      showString "Infix "
+        . showsPrec 11 name
+        . showString " "
+        . showsPrec 11 ass
+        . showString " "
+        . showsPrec 11 fix
+    Record name fields ->
+      showString "Record "
+        . showsPrec 11 name
+        . showString " "
+        . showsNP showsPrec 11 fields
+
+instance Eq (DiffErrorNested xss) where
+  WrongConstructor l1 r1 == WrongConstructor l2 r2 =
+    eqNS eqConstructorInfo l1 l2 && eqNS eqConstructorInfo r1 r2
+  FieldMismatch al1 == FieldMismatch al2 = al1 == al2
+  _ == _ = False
+
+instance Show (DiffErrorNested xss) where
+  showsPrec d = \case
+    WrongConstructor l r ->
+      showParen (d > 10) $
+        showString "WrongConstructor "
+          . showsNS showsConstructorInfo 11 l
+          . showString " "
+          . showsNS showsConstructorInfo 11 r
+    FieldMismatch al ->
+      showParen (d > 10) $
+        showString "FieldMismatch "
+          . showsPrec 11 al
+
+pickOut :: NP f xs -> NS g xs -> NS f xs
+pickOut Nil gs = case gs of {}
+pickOut (x :* _) (Z _) = Z x
+pickOut (_ :* xs) (S gs) = S $ pickOut xs gs
diff --git a/test/Generics/Diff/PropertyTestsSpec.hs b/test/Generics/Diff/PropertyTestsSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Generics/Diff/PropertyTestsSpec.hs
@@ -0,0 +1,99 @@
+{-# OPTIONS_GHC -Wno-partial-fields #-}
+
+module Generics.Diff.PropertyTestsSpec where
+
+import Control.Monad
+import Data.Complex
+import Data.Fixed
+import qualified Data.Functor.Compose as F
+import Data.Functor.Const
+import Data.Functor.Identity
+import Data.Functor.Product
+import qualified Data.Monoid as M
+import Data.Proxy
+import qualified Data.Semigroup as S
+import Data.Version
+import Foreign.C.Types
+import Generics.Diff
+import Generics.Diff.Instances ()
+import Generics.Diff.UnitTestsSpec
+import qualified Test.Hspec as H
+import qualified Test.Hspec.QuickCheck as H
+import qualified Test.QuickCheck as Q
+import Util
+
+spec :: H.Spec
+spec = do
+  H.describe "x == y => x `diff` y == Equal" $
+    manyTypes True propEqualGivesEqual
+  H.describe "x `diff` y == Equal => x == y" $
+    manyTypes False propEqualMeansEqual
+
+-- | If the two inputs are equal, 'diff' should return 'Equal'.
+propEqualGivesEqual :: forall a. (Q.Arbitrary a, Diff a, Show a) => Proxy a -> Q.Property
+propEqualGivesEqual _ = Q.property $ \a -> propDiffResult @a a a Equal
+
+-- | If the two inputs are not equal, 'diff' should never return 'Equal'.
+propEqualMeansEqual :: forall a. (Q.Arbitrary a, Eq a, Diff a, Show a) => Proxy a -> Q.Property
+propEqualMeansEqual _ = Q.property $ \leftValue rightValue ->
+  leftValue /= rightValue Q.==>
+    diff @a leftValue rightValue /= Equal
+
+manyTypes :: Bool -> (forall x. (Q.Arbitrary x, Eq x, Diff x, Show x) => Proxy x -> Q.Property) -> H.Spec
+manyTypes allowUnit prop = do
+  H.describe "Primitive/opaque types" $ do
+    when allowUnit $ H.prop "()" $ prop $ Proxy @()
+    H.prop "Bool" $ prop $ Proxy @Char
+    H.prop "Char" $ prop $ Proxy @Char
+    H.prop "Int" $ prop $ Proxy @Int
+    H.prop "Rational" $ prop $ Proxy @Rational
+    H.prop "Version" $ prop $ Proxy @Version
+    H.prop "CLong" $ prop $ Proxy @CLong
+    H.prop "CChar" $ prop $ Proxy @CChar
+    H.prop "Uni" $ prop $ Proxy @Uni
+    H.prop "Deci" $ prop $ Proxy @Deci
+    H.prop "Centi" $ prop $ Proxy @Centi
+  H.describe "Newtypes" $ do
+    when allowUnit $ H.prop "Identity ()" $ prop $ Proxy @(Identity ())
+    H.prop "Identity Char" $ prop $ Proxy @(Identity Char)
+    H.prop "Identity Int" $ prop $ Proxy @(Identity Int)
+
+    H.prop "Dual Rational" $ prop $ Proxy @(M.Dual Rational)
+    H.prop "Dual Version" $ prop $ Proxy @(M.Dual Version)
+    H.prop "Dual CLong" $ prop $ Proxy @(M.Dual CLong)
+
+    H.prop "Sum CChar" $ prop $ Proxy @(M.Sum CChar)
+    H.prop "Sum Uni" $ prop $ Proxy @(M.Sum Uni)
+    H.prop "Sum Deci" $ prop $ Proxy @(M.Sum Deci)
+
+    H.prop "First ()" $ prop $ Proxy @(M.First ())
+    H.prop "First Char" $ prop $ Proxy @(M.First Char)
+    H.prop "First Int" $ prop $ Proxy @(M.First Int)
+
+    H.prop "Alt Identity Rational" $ prop $ Proxy @(M.Alt Identity Rational)
+    H.prop "Alt Identity Version" $ prop $ Proxy @(M.Alt Identity Version)
+    H.prop "Alt Identity CLong" $ prop $ Proxy @(M.Alt Identity CLong)
+
+    H.prop "Complex CChar" $ prop $ Proxy @(Complex CChar)
+    H.prop "Complex Uni" $ prop $ Proxy @(Complex Uni)
+
+    H.prop "Any" $ prop $ Proxy @S.Any
+    H.prop "All" $ prop $ Proxy @S.All
+
+    H.prop "Const Any Bool" $ prop $ Proxy @(Const S.Any Bool)
+    H.prop "Const All ()" $ prop $ Proxy @(Const S.All ())
+
+  H.describe "Combinator types" $ do
+    H.prop "Either Int Char" $ prop $ Proxy @(Either Int Char)
+    H.prop "Maybe String" $ prop $ Proxy @(Maybe String)
+    H.prop "[Bool]" $ prop $ Proxy @[Bool]
+    H.prop "Compose Maybe (Either Int) Char" $ prop $ Proxy @(F.Compose Maybe (Either Int) Char)
+    H.prop "Product Maybe (Either Int) Char" $ prop $ Proxy @(Product Maybe (Either Int) Char)
+
+  H.describe "Tuples" $ do
+    H.prop "((), Char)" $ prop $ Proxy @((), Char)
+    H.prop "(Int, Rational, Version)" $ prop $ Proxy @(Int, Rational, Version)
+    H.prop "(CLong, CChar, Uni, Deci)" $ prop $ Proxy @(CLong, CChar, Uni, Deci)
+
+  H.describe "Custom types" $ do
+    H.prop "CustomType" $ prop $ Proxy @CustomType
diff --git a/test/Generics/Diff/UnitTestsSpec.hs b/test/Generics/Diff/UnitTestsSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Generics/Diff/UnitTestsSpec.hs
@@ -0,0 +1,131 @@
+{-# OPTIONS_GHC -Wno-partial-fields #-}
+
+module Generics.Diff.UnitTestsSpec where
+
+import Data.Foldable
+import Data.SOP
+import qualified Data.Text as T
+import qualified GHC.Generics as G
+import Generics.Diff
+import Generics.Diff.Instances ()
+import Generics.SOP
+import Generics.SOP.Arbitrary (garbitrary)
+import qualified Test.Hspec as H
+import qualified Test.Hspec.QuickCheck as H
+import Test.QuickCheck as Q
+import Util
+
+spec :: H.Spec
+spec =
+  H.describe "Unit tests using hand-rolled datatypes" $
+    traverse_ specTestSet testSets
+
+specTestSet :: (Diff a, Show a) => TestSet a -> H.Spec
+specTestSet TestSet {..} =
+  H.prop (T.unpack setName) $
+    propDiffResult leftValue rightValue expectedDiffResult
+
+------------------------------------------------------------
+-- test type
+
+data CustomType
+  = -- | Normal constructor
+    Con1 (Either Int String) (Maybe Char)
+  | -- | Record constructor
+    Con2 {c11 :: Maybe String, c12 :: Bool}
+  | -- | Infix constructor
+    (Char, Int, ()) `Con3` [Maybe Int]
+  | -- | Recursive
+    Con4 CustomType CustomType
+  deriving (Show, Eq, G.Generic)
+
+instance Generic CustomType
+
+instance HasDatatypeInfo CustomType
+
+instance Diff CustomType
+
+instance Q.Arbitrary CustomType where
+  arbitrary = garbitrary
+  shrink = Q.genericShrink
+
+-- bunch of 'ConstructorInfo's, for convenience
+
+c1Info :: ConstructorInfo '[Either Int String, Maybe Char]
+c2Info :: ConstructorInfo '[Maybe String, Bool]
+c3Info :: ConstructorInfo '[(Char, Int, ()), [Maybe Int]]
+c4Info :: ConstructorInfo '[CustomType, CustomType]
+c1Info :* c2Info :* c3Info :* c4Info :* _ = constructorInfo $ datatypeInfo $ Proxy @CustomType
+
+nothingInfo :: ConstructorInfo '[]
+justInfo :: forall a. ConstructorInfo '[a]
+nothingInfo :* justInfo :* _ = constructorInfo $ datatypeInfo (Proxy :: Proxy (Maybe a))
+
+data TestSet a = TestSet
+  { setName :: T.Text
+  , leftValue :: a
+  , rightValue :: a
+  , expectedDiffResult :: DiffResult a
+  }
+  deriving (Show)
+
+-- | some pairs of 'CustomType's, with the 'DiffResult' we expect to get for each of them using 'gdiff'.
+testSets :: [TestSet CustomType]
+testSets =
+  [ TestSet
+      { setName = "Equal - Con1"
+      , leftValue = value1
+      , rightValue = value1
+      , expectedDiffResult = Equal
+      }
+  , TestSet
+      { setName = "Diff, WrongConstructor, Con3 and Con1"
+      , leftValue = leftValue2
+      , rightValue = rightValue2
+      , expectedDiffResult = Error error2
+      }
+  , TestSet
+      { setName = "Diff, WrongConstructor, Con3 and Con1"
+      , leftValue = ('a', 5, ()) `Con3` [Just 1]
+      , rightValue = Con1 (Left 0) (Just 'a')
+      , expectedDiffResult = Error (Nested $ WrongConstructor (S (S (Z c3Info))) (Z c1Info))
+      }
+  , TestSet
+      { setName = "Diff, FieldMismatch, normal Constructor, nested"
+      , leftValue = Con1 (Left 0) Nothing
+      , rightValue = Con1 (Left 0) (Just 'a')
+      , expectedDiffResult = Error (Nested $ FieldMismatch (DiffAtField (Z (c1Info :*: S (Z $ Nested (WrongConstructor (Z nothingInfo) (S (Z justInfo))))))))
+      }
+  , TestSet
+      { setName = "Diff, FieldMismatch, Infix constructor, right side, nested"
+      , leftValue = ('a', 5, ()) `Con3` [Just 1]
+      , rightValue = ('a', 5, ()) `Con3` [Nothing, Just 1]
+      , expectedDiffResult = Error (Nested $ FieldMismatch (DiffAtField (S (S (Z (c3Info :*: S (Z $ DiffSpecial (DiffAtIndex 0 (Nested (WrongConstructor (S (Z justInfo)) (Z nothingInfo)))))))))))
+      }
+  , TestSet
+      { setName = "Diff, FieldMismatch, Infix constructor, left side, nested"
+      , leftValue = ('a', 4, ()) `Con3` [Just 1]
+      , rightValue = ('a', 5, ()) `Con3` [Nothing, Just 1]
+      , expectedDiffResult = Error (Nested $ FieldMismatch (DiffAtField (S (S (Z (c3Info :*: Z (Nested $ FieldMismatch $ DiffAtField $ Z (Constructor "(,,)" :*: S (Z TopLevelNotEqual)))))))))
+      }
+  , TestSet
+      { setName = "Diff, FieldMismatch, recursive"
+      , leftValue = Con4 value1 leftValue2
+      , rightValue = Con4 value1 rightValue2
+      , expectedDiffResult = Error (Nested $ FieldMismatch (DiffAtField (S (S (S (Z (c4Info :*: S (Z error2))))))))
+      }
+  ]
+  where
+    value1 = Con1 (Left 0) Nothing
+
+    leftValue2 = Con1 (Left 0) Nothing
+    rightValue2 = Con2 Nothing False
+    error2 = Nested $ WrongConstructor (Z c1Info) (S (Z c2Info))
+
+-- | The expected 'DiffResult's of 'testSets'
+expectedGDiffResults :: [DiffResult CustomType]
+expectedGDiffResults = expectedDiffResult <$> testSets
+
+-- | The actual 'DiffResult's of 'testSets' - from calling 'gdiff'.
+actualGDiffResults :: [DiffResult CustomType]
+actualGDiffResults = [gdiff leftValue rightValue | TestSet {..} <- testSets]
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/test/Util.hs b/test/Util.hs
new file mode 100644
--- /dev/null
+++ b/test/Util.hs
@@ -0,0 +1,23 @@
+module Util where
+
+import qualified Data.Text.Lazy as TL
+import qualified Data.Text.Lazy.Builder as TB
+import Generics.Diff
+import Generics.Diff.Instances ()
+import Generics.Diff.Render
+import qualified Test.QuickCheck as Q
+
+propDiffResult :: (Diff a, Show a) => a -> a -> DiffResult a -> Q.Property
+propDiffResult leftValue rightValue expectedDiffResult =
+  let actualDiffResult = diff leftValue rightValue
+      eq = expectedDiffResult == actualDiffResult
+      showDiffResult = TL.unpack . TB.toLazyText . renderDiffResult
+      addLabel =
+        if eq
+          then Q.property
+          else
+            Q.counterexample ("Expected DiffResult:\n" <> showDiffResult expectedDiffResult)
+              . Q.counterexample ("Actual DiffResult:\n" <> showDiffResult actualDiffResult)
+              . Q.counterexample ("Left value:\n" <> show leftValue)
+              . Q.counterexample ("Right value:\n" <> show rightValue)
+  in  addLabel eq
