diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,1 @@
+2025-08-01: initial version
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Stephanie Weirich, Noe De Santo
+
+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/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,135 @@
+# Rebound
+
+
+`Rebound` is a variable binding library based on well-scoped de Bruijn indices
+and environments.
+
+This library is represents variables using the index type `Fin n`; a type of
+bounded natural numbers. The key way to manipulate these indices is using an
+*environment*, a simultaneous substitutions similar to a function of type `Fin n
+-> Exp m`. Applying an environment converts an expression in scope `n` to one in
+scope `m`.
+
+## Design goals
+
+The goal of this library is to be an effective tool for language
+experimentation. Say you want to implement a new language idea that you have
+read about in a PACMPL paper? This library will help you put together a
+prototype implementation quickly.
+
+1. *Correctness*: This library uses Dependent Haskell to statically track the
+    scopes of bound variables. Because variables are represented by de Bruijn
+    indices, scopes are represented by natural numbers, bounding the indices
+    that can be used. If the scope is 0, then the term must be closed.
+
+2. *Convenience*: The library is based on a type-directed approach to binding,
+    where AST terms can indicate binding structure through the use of types
+    defined in this library. As a result the library provides a clean, uniform,
+    and automatic interface to common operations such as substitution,
+    alpha-equality, and scope change.
+
+3. *Efficiency*: Behind the scenes, the library uses explicit substitutions
+    (environments) to delay the execution of operations such as shifting and
+    substitution. However, these environments are accessible to library users
+    who would like fine control over when these operations.
+
+4. *Accessibility*: This library comes with several examples demonstrating how
+    to use it effectively. Many of these are also examples of programming with
+    Dependent Haskell.
+
+## Examples
+
+### Calculi
+
+1. [Untyped lambda calculus](examples/LC.hs)
+
+   Defines the syntax and substitution functions for the untyped lambda
+   calculus. Uses these definitions to implement several interpreters.
+
+2. [Untyped lambda calculus with let rec and nested lets](examples/LCLet.hs)
+
+   Example of advanced binding forms: recursive definitions and sequenced
+   definitions.
+
+3. [Untyped lambda calculus with pattern matching](examples/Pat.hs)
+
+   Extends the lambda calculus example with pattern matching.
+
+4. [System F](examples/SystemF.hs)
+
+   Working with two separate scopes (type and term variables) is tricky. This
+   example shows one way to do it.
+
+5. [Pure System F](examples/PureSystemF.hs)
+
+   An alternative way of defining System F, using one single syntactic class.
+   Also demonstrates how to use the `ScopedReader` monad for typechecking and
+   pretty-printing.
+
+6. [Simple implementation of dependent types](examples/PTS.hs)
+
+   An implementation of a simple type checker for a dependent-type system.
+   Language includes Pi and Sigma types.
+
+7. [Dependent Pattern Matching](examples/DepMatch.hs)
+
+   A dependent type system with nested, dependent pattern matching. Patterns may
+   also include scoped terms.
+
+8. [Linear Lambda Calculus](examples/LinLC.hs)
+
+   A linear version of the (simply typed) lambda calculus. Demonstrates how to
+   thread a typing context using the `ScopedState` monad.
+
+### Working with well-scoped expressions
+
+1. [Scope checking](examples/ScopeCheck.hs)
+
+   Demonstrates how to convert a "named" (or _nominal_) expression to a
+   well-scoped expression.
+
+2. [QuickCheck](examples/LCQC.hs)
+
+   Demonstrates the use of well-scoped terms with
+   [QuickCheck](https://hackage.haskell.org/package/QuickCheck).
+
+3. [HOAS](examples/HOAS.hs)
+
+   Demonstrates how to layer a HOAS representation on top of a de Bruijn
+   representation. Based on Conor McBride's ["Classy
+   Hack"](https://mazzo.li/epilogue/index.html%3Fp=773.html).
+
+4. [PatGen](examples/PatGen.hs)
+
+   A variant of the [Pat](examples/Pat.hs) example, which demonstrates how
+   generic programming can be used to derive some definitions.
+
+## Related libraries
+
+- [Bound](https://hackage.haskell.org/package/bound)
+
+  `Bound` is the most closely related library. Like `Rebound`, it is a
+  scope-safe approach to de Bruijn indices in Haskell. The key difference is
+  that `bound` requires fewer language extensions by using nested datatypes
+  instead of GADTs. Use this library if you would like to avoid extensions such
+  as `GADTs`, `DataKinds`, and `TypeFamilies`.
+
+- [Unbound-Generics](https://hackage.haskell.org/package/unbound-generics)
+
+  The `Unbound` library uses a locally-nameless reprsentation. `Rebound` draws
+  inspiration for its design from the type-directed approach to the binding
+  interface found in `Unbound`. However, `Unbound` is not not-scope safe. As a
+  result it is easier to get started. However, working with a locally nameless
+  representation requires a monad for fresh name generation. It also can be
+  slow.
+
+- [Foil and Free Foil](https://hackage.haskell.org/package/free-foil)
+
+  GHC internally uses a *nominal* representation of binding, where both bound
+  and free variables are represented by names. In this approach, users must
+  rename the bound variable in abstraction if it is already in the current
+  scope.
+
+- [binder](https://hackage.haskell.org/package/binder)
+
+  Uses HOAS.
diff --git a/examples/DepMatch.hs b/examples/DepMatch.hs
new file mode 100644
--- /dev/null
+++ b/examples/DepMatch.hs
@@ -0,0 +1,707 @@
+-- | A dependent type system, with nested dependent pattern matching for Sigma types.
+-- This is an advanced usage of the binding library, demonstrating the use of Scoped patterns.
+-- It doesn't correspond to any current system, but has its own elegance
+
+{-# LANGUAGE OverloadedLists #-}
+module DepMatch where
+
+import Rebound
+import Rebound.Context
+
+
+import qualified Rebound.Bind.Pat as Pat
+import qualified Rebound.Bind.Scoped as Scoped
+import Rebound.Bind.PatN as PN
+
+import Control.Monad (guard, zipWithM_)
+import Control.Monad.Except (ExceptT, MonadError (..), runExceptT)
+import Data.Fin
+import Data.Maybe qualified as Maybe
+import Data.Set (Set)
+import Data.Set qualified as Set
+import Data.Vec qualified
+import Data.Scoped.List (List, pattern Nil, pattern (:<))
+import Data.Scoped.List qualified as List
+import GHC.Generics (Generic1)
+
+-- In this system, `Match` introduces a Pi type and generalizes
+-- dependent functions
+-- If the pattern is a single variable, or an annotated variable,
+-- then the `Match` term is just a normal lambda expression.
+-- But the pattern could be more structured than that, supporting
+-- a general form of pattern matching. In this simple language,
+-- only type that supports pattern matching is a Sigma type. So
+-- every match expression should have a single branch. But, for
+-- generality, we pretend that more are possible.
+data Exp (n :: Nat)
+  = Star
+  | Pi (Exp n) (Bind1 Exp Exp n)
+  | Var (Fin n)
+  | Match (List Branch n)  -- case lambda
+  | App (Exp n) (Exp n)
+  | Sigma (Exp n) (Bind1 Exp Exp n)
+  | Pair (Exp n) (Exp n)
+  | Annot (Exp n) (Exp n)
+      deriving (Generic1)
+
+-- | A single branch in a match expression
+data Branch (n :: Nat)
+  = forall p. Branch (Scoped.Bind Exp Exp (Pat p) n)
+
+
+-- | Patterns, which may include embedded type annotations
+-- `p` is the number of variables bound by the pattern
+-- `n` is the number of free variables in type annotations in the pattern
+data Pat (p :: Nat) (n :: Nat) where
+  PVar :: Pat N1 n
+  -- Patterns are "telescopic"
+  -- In Pair pattern, we increase the scope so that variables
+  -- bound in the left subterm can be referred to in the right subterm
+  PPair :: Pat p1 n -> Pat p2 (p1 + n) -> Pat (p2 + p1) n
+  -- Patterns can also include type annotations.
+  PAnnot :: Pat p n -> Exp n -> Pat p n
+
+
+-- This definitions support telescopes: variables bound earlier in the pattern
+-- can appear later.  For example, the pattern for a type paired with
+-- a term of that type can look like this
+--     (x, (y :: x))
+
+pat0 :: Pat N2 N0
+pat0 = PPair PVar (PAnnot PVar (Var f0))
+
+-- The type of this pattern is
+--     Sigma x:Star.x
+ty0 :: Exp Z
+ty0 = Sigma Star (bind1 (Var f0))
+
+-------------------------------------------------------
+-- definitions for pattern matching
+-------------------------------------------------------
+
+instance Sized (Pat p n) where
+  type Size (Pat p n) = p
+  size :: Pat p n -> SNat p
+  size PVar = s1
+  size (PPair p1 p2) = sPlus (size p2) (size p1)
+  size (PAnnot p _) = size p
+
+-- Because Pat is a scope-indexed pattern, we need to also 
+-- instantiate the `ScopedSized` class
+instance Scoped.ScopedSized (Pat p) where
+  type ScopedSize (Pat p) = p
+
+-- A term that matches the "(x,(y:x))" and has type exists x:*. x
+tm0 :: Exp Z
+tm0 = Pair Star ty0
+
+-- >>> patternMatch pat0 tm0
+-- Just [(0,Sigma *. 0),(1,*)]
+
+-- | Compare a pattern with an expression, potentially
+-- producing a substitution for all of the variables
+-- bound in the pattern
+patternMatch :: Pat p n -> Exp n -> Maybe (Env Exp p n)
+patternMatch PVar e = Just $ oneE e
+patternMatch (PPair p1 p2) (Pair e1 e2) =
+  -- two append operations require implicit sizes in the context
+  withSNat (size p1) $ withSNat (size p2) $ do
+    env1 <- patternMatch p1 e1
+    -- NOTE: substitute in p2 with env1 before pattern matching
+    env2 <- patternMatch (applyE (env1 .++ idE) p2) e2
+    return (env2 .++ env1)
+-- ignore type annotates when pattern matching
+patternMatch (PAnnot p _) e = patternMatch p e
+patternMatch p (Annot e _) = patternMatch p e
+patternMatch _ _ = Nothing
+
+findBranch :: Exp n -> List Branch n -> Maybe (Exp n)
+findBranch e Nil = Nothing
+findBranch e (Branch (bnd :: Scoped.Bind Exp Exp (Pat p) n) :< brs) =
+  case patternMatch (Scoped.getPat bnd) e of
+    Just r -> Just $ Scoped.instantiate bnd r
+    Nothing -> findBranch e brs
+
+----------------------------------------------
+-- * Subst instances
+
+instance SubstVar Exp where
+  var = Var
+
+instance Shiftable Exp where
+  shift = shiftFromApplyE @Exp
+
+instance Subst Exp Exp where
+  isVar (Var x) = Just (Refl, x)
+  isVar _ = Nothing
+
+  {-
+  -- The generic definition above is equivalent to this code
+  applyE r Star = Star
+  applyE r (Pi a b) = Pi (applyE r a) (applyE r b)
+  applyE r (Var x) = applyEnv r x
+  applyE r (App e1 e2) = App (applyE r e1) (applyE r e2)
+  applyE r (Sigma a b) = Sigma (applyE r a) (applyE r b)
+  applyE r (Pair a b) = Pair (applyE r a) (applyE r b)
+  applyE r (Match brs) = Match (List.map (applyE r) brs)
+  applyE r (Annot a t) = Annot (applyE r a) (applyE r t)
+  -}
+
+instance Shiftable (Pat p) where
+  shift = shiftFromApplyE @Exp
+
+-- This definition cannot be generic because Pat is a GADT
+instance Subst Exp (Pat p) where
+  applyE :: Env Exp n m -> Pat p n -> Pat p m
+  applyE r PVar = PVar
+  -- need to account for new pattern variables from p1 bound in p2
+  applyE r (PPair p1 p2) = PPair (applyE r p1) (applyE (upN (size p1) r) p2)
+  applyE r (PAnnot p t) = PAnnot (applyE r p) (applyE r t)
+
+
+instance Shiftable Branch where
+  shift = shiftFromApplyE @Exp
+
+-- This definition also cannot be generic due to the existential
+instance Subst Exp Branch where
+  applyE :: Env Exp n m -> Branch n -> Branch m
+  applyE r (Branch b) = Branch (applyE r b)
+
+
+----------------------------------------------
+-- Free variable calculation
+----------------------------------------------
+
+t00 :: Exp N2
+t00 = App (Var f0) (Var f0)
+
+t01 :: Exp N2
+t01 = App (Var f0) (Var f1)
+
+-- >>> appearsFree f0 t00
+-- True
+
+-- >>> appearsFree f1 t00
+-- False
+
+instance FV Exp where
+  {-
+  -- Generic programming produces the following definitions:
+  appearsFree n (Var x) = n == x
+  appearsFree n Star = False
+  appearsFree n (Pi a b) = appearsFree n a || appearsFree (FS n) (getBody1 b)
+  appearsFree n (App a b) = appearsFree n a || appearsFree n b
+  appearsFree n (Sigma a b) = appearsFree n a || appearsFree (FS n) (getBody1 b)
+  appearsFree n (Pair a b) = appearsFree n a || appearsFree n b
+  appearsFree n (Match b) = List.any (appearsFree n) b
+  appearsFree n (Annot a t) = appearsFree n a || appearsFree n t
+
+  freeVars :: Exp n -> Set (Fin n)
+  freeVars (Var x) = Set.singleton x
+  freeVars Star = Set.empty
+  freeVars (Pi a b) = freeVars a <> rescope s1 (freeVars (getBody1 b))
+  freeVars (App a b) = freeVars a <> freeVars b
+  freeVars (Sigma a b) = freeVars a <> rescope s1 (freeVars (getBody1 b))
+  freeVars (Pair a b) = freeVars a <> freeVars b
+  freeVars (Match b) = List.foldMap freeVars b
+  freeVars (Annot a t) = freeVars a <> freeVars t
+  -}
+
+-- cannot be generic
+instance FV Branch where
+  appearsFree n (Branch bnd) = appearsFree n bnd
+  freeVars (Branch bnd)= freeVars bnd
+
+-- cannot be generic
+instance FV (Pat p) where
+  appearsFree n PVar = False
+  appearsFree n (PPair p1 p2) = appearsFree n p1 || appearsFree (shiftN (size p1) n) p2
+  appearsFree n (PAnnot p t) = appearsFree n p || appearsFree n t
+
+  freeVars PVar = Set.empty
+  freeVars (PPair p1 p2) = freeVars p1 <> rescope (size p1) (freeVars p2)
+  freeVars (PAnnot p t) = freeVars p <> freeVars t
+
+----------------------------------------------
+-- weakening (convenience functions)
+----------------------------------------------
+
+-- >>> :t weaken' s1 t00
+-- weaken' s1 t00 :: Exp ('S ('S N1))
+
+-- >>> weaken' s1 t00
+-- 0 0
+
+weaken' :: SNat m -> Exp n -> Exp (m + n)
+weaken' m = applyE @Exp (weakenE' m)
+
+weakenBind' :: SNat m -> Bind1 Exp Exp n -> Bind1 Exp Exp (m + n)
+weakenBind' m = applyE @Exp (weakenE' m)
+
+----------------------------------------------
+-- strengthening
+----------------------------------------------
+
+-- >>> strengthenRec s1 s1 snat t00
+-- Just (0 0)
+
+-- >>> strengthenRec s1 s1 snat t01
+-- Nothing
+
+instance Strengthen Exp where
+  {-
+  strengthenRec k m n (Var x) = Var <$> strengthenRec k m n x
+  strengthenRec k m n Star = pure Star
+  strengthenRec k m n (Pi a b) = Pi <$> strengthenRec k m n a <*> strengthenRec k m n b
+  strengthenRec k m n (App a b) = App <$> strengthenRec k m n a <*> strengthenRec k m n b
+  strengthenRec k m n (Pair a b) = Pair <$> strengthenRec k m n a <*> strengthenRec k m n b
+  strengthenRec k m n (Sigma a b) = Sigma <$> strengthenRec k m n a <*> strengthenRec k m n b
+  strengthenRec k m n (Match b) = Match <$> List.mapM (strengthenRec k m n) b
+  strengthenRec k m n (Annot a t) = Annot <$> strengthenRec k m n a <*> strengthenRec k m n t
+  -}
+
+instance Strengthen (Pat p) where
+  strengthenRec k m n PVar = pure PVar
+  strengthenRec (k :: SNat k) (m :: SNat m) (n :: SNat n) (PPair (p1 :: Pat p1 (k + (m + n)))
+    (p2 :: Pat p2 (p1 + (k + (m + n))))) =
+      case (axiomAssoc @p1 @k @(m + n),
+            axiomAssoc @p1 @k @n) of
+       (Refl, Refl) ->
+         let r = strengthenRec (sPlus (size p1) k) m n p2 in
+         PPair <$> strengthenRec k m n p1 <*> r
+  strengthenRec k m n (PAnnot p1 e2) = PAnnot <$> strengthenRec k m n p1 <*> strengthenRec k m n e2
+
+instance Strengthen Branch where
+  strengthenRec k m n (Branch bnd) = Branch <$> strengthenRec k m n bnd
+----------------------------------------------
+-- Some Examples
+----------------------------------------------
+
+star :: Exp n
+star = Star
+
+-- No annotation on the binder
+lam :: Exp (S n) -> Exp n
+lam b = Match [Branch (Scoped.bind PVar b)]
+
+-- Annotation on the binder
+alam :: Exp n -> Exp (S n) -> Exp n
+alam t b = Match [Branch (Scoped.bind (PAnnot PVar t) b)]
+
+-- The identity function "λ x. x". With de Bruijn indices
+-- we write it as "λ. 0", though with `Match` it looks a bit different
+t0 :: Exp Z
+t0 = lam (Var f0)
+
+-- A larger term "λ x. λy. x (λ z. z z)"
+-- λ. λ. 1 (λ. 0 0)
+t1 :: Exp Z
+t1 =
+  lam
+    ( lam
+        (Var f1 `App` lam (Var f0 `App` Var f0))
+    )
+
+-- To show lambda terms, we can write a simple recursive instance of
+-- Haskell's `Show` type class. In the case of a binder, we use the `unbind`
+-- operation to access the body of the lambda expression.
+
+-- >>> t0
+-- λ_. 0
+
+-- >>> t1
+-- λ_. (λ_. (1 (λ_. (0 0))))
+
+-- Polymorphic identity function and its type
+
+tyid = Pi star (bind1 (Pi (Var f0) (bind1 (Var f1))))
+
+tmid = lam (lam (Var f0))
+
+-- >>> tyid
+-- Pi *. 0 -> 1
+
+-- >>> tmid
+-- λ_. (λ_. 0)
+
+--------------------------------------------------------
+
+-- * Show instances
+
+--------------------------------------------------------
+
+
+instance Show (Exp n) where
+  showsPrec :: Int -> Exp n -> String -> String
+  showsPrec _ Star = showString "*"
+  showsPrec d (Pi a b)
+    | appearsFree FZ (getBody1 b) =
+        showParen (d > 9) $
+          showString "Pi "
+            . shows a
+            . showString ". "
+            . shows (getBody1 b)
+    | otherwise =
+        showParen (d > 9) $
+          showsPrec 11 a
+            . showString " -> "
+            . showsPrec 9 (getBody1 b)
+  showsPrec d (Sigma a b)
+    | appearsFree FZ (getBody1 b) =
+        showParen (d > 9) $
+          showString "Sigma "
+            . shows a
+            . showString ". "
+            . shows (getBody1 b)
+    | otherwise =
+        showParen (d > 9) $
+          showsPrec 11 a
+            . showString " * "
+            . showsPrec 9 (getBody1 b)
+  showsPrec _ (Var x) = shows x
+  showsPrec d (App e1 e2) =
+    showParen (d > 0) $
+      showsPrec 10 e1
+        . showString " "
+        . showsPrec 11 e2
+  showsPrec d (Pair e1 e2) =
+    showParen (d > 0) $
+      showsPrec 10 e1
+        . showString ", "
+        . showsPrec 11 e2
+  showsPrec d (Match [b]) =
+    showParen (d > 9) $
+      showString "λ"
+        . showsPrec 9 b
+  showsPrec d (Match b) =
+    showParen (d > 10) $
+      showString "match"
+        . showsPrec 10 b
+  showsPrec d (Annot a t) =
+    showParen (d > 10) $
+      showsPrec 10 a
+        . showString " : "
+        . showsPrec 10 t
+
+instance Show (Branch b) where
+  showsPrec d (Branch b) =
+    showsPrec 10 (Scoped.getPat b)
+      . showString ". "
+      . showsPrec 11 (Scoped.getBody b)
+
+instance Show (Pat p n) where
+  showsPrec d PVar = showString "_"
+  showsPrec d (PPair e1 e2) =
+    showParen (d > 0) $
+      showsPrec 10 e1
+        . showString ", "
+        . showsPrec 11 e2
+  showsPrec d (PAnnot e1 e2) =
+    showParen (d > 0) $
+      showsPrec 10 e1
+        . showString " : "
+        . showsPrec 11 e2
+
+--------------------------------------------------------
+
+-- * Alpha equivalence
+
+--------------------------------------------------------
+
+
+-- The derivable equality instance is alpha-equivalence
+deriving instance (Eq (Exp n))
+
+instance PatEq (Pat p1 n) (Pat p2 n) where
+  patEq :: Pat p1 n -> Pat p2 n -> Maybe (p1 :~: p2)
+  patEq PVar PVar = Just Refl
+  patEq (PPair p1 p2) (PPair p1' p2') = do
+    Refl <- patEq p1 p1'
+    Refl <- patEq p2 p2'
+    return Refl
+  patEq (PAnnot p1 p2) (PAnnot p1' p2') = do
+    Refl <- patEq p1 p1'
+    guard (p2 == p2')
+    return Refl
+  patEq _ _ = Nothing
+
+-- This equality is not derivable
+instance Eq (Branch n) where
+  (==) :: Branch n -> Branch n -> Bool
+  (Branch (p1 :: Scoped.Bind Exp Exp (Pat m1) n))
+    == (Branch (p2 :: Scoped.Bind Exp Exp (Pat m2) n)) =
+      case testEquality
+        (size (Scoped.getPat p1) :: SNat m1)
+        (size (Scoped.getPat p2) :: SNat m2) of
+        Just Refl -> p1 == p2
+        Nothing -> False
+
+
+--------------------------------------------------------
+
+-- * big-step evaluation
+
+--------------------------------------------------------
+
+-- We can write the usual operations for evaluating
+-- lambda terms to values
+
+-- >>> eval t1
+-- λ_. (λ_. (1 (λ_. (0 0))))
+
+-- >>> eval (t1 `App` t0)
+-- λ_. ((λ_. 0) (λ_. (0 0)))
+
+eval :: Exp n -> Exp n
+eval (Var x) = Var x
+eval (Match b) = Match b
+eval (App e1 e2) =
+  let v = eval e2
+   in case eval e1 of
+        Match b -> case findBranch v b of
+          Just e -> eval e
+          Nothing -> error "pattern match failure"
+        t -> App t v
+eval Star = Star
+eval (Pi a b) = Pi a b
+eval (Sigma a b) = Sigma a b
+eval (Annot a t) = eval a
+eval (Pair a b) = Pair a b
+
+-- small-step evaluation
+
+-- >>> step (t1 `App` t0)
+-- Just (λ_. (λ_. 0 (λ_. (0 0))))
+
+step :: Exp n -> Maybe (Exp n)
+step (Var x) = Nothing
+step (Match b) = Nothing
+step (App (Match bs) e2)
+  | Just r <- findBranch e2 bs =
+      Just r
+step (App e1 e2)
+  | Just e1' <- step e1 = Just (App e1' e2)
+  | Just e2' <- step e2 = Just (App e1 e2')
+  | otherwise = Nothing
+step Star = Nothing
+step (Pi a b) = Nothing
+step (Sigma a b) = Nothing
+step (Pair a b) = Nothing
+step (Annot a t) = step a
+
+eval' :: Exp n -> Exp n
+eval' e
+  | Just e' <- step e = eval' e'
+  | otherwise = e
+
+----------------------------------------------------------------
+-- Check for equality
+----------------------------------------------------------------
+data Err where
+  NotEqual :: Exp n -> Exp n -> Err
+  PiExpected :: Exp n -> Err
+  PiExpectedPat :: Pat p1 n1 -> Err
+  SigmaExpected :: Exp n -> Err
+  VarEscapes :: Exp n -> Err
+  PatternMismatch :: Pat p1 n1 -> Pat p2 n2 -> Err
+  PatternTypeMismatch :: Pat p1 n1 -> Exp n1 -> Err
+  AnnotationNeeded :: Exp n -> Err
+  AnnotationNeededPat :: Pat p1 n1 -> Err
+
+deriving instance (Show Err)
+
+-- find the head form
+whnf :: Exp n -> Exp n
+whnf (App a1 a2) = case whnf a1 of
+  Match bs -> case findBranch (eval a2) bs of
+    Just b -> whnf b
+    Nothing -> App (Match bs) a2
+  t -> App t a2
+whnf (Annot a t) = whnf a
+whnf a = a
+
+equate :: (MonadError Err m) => Exp n -> Exp n -> m ()
+equate t1 t2 = do
+  let n1 = whnf t1
+      n2 = whnf t2
+  equateWHNF n1 n2
+
+equatePat ::
+  (MonadError Err m) =>
+  Pat p1 n ->
+  Pat p2 n ->
+  m ()
+equatePat PVar PVar = pure ()
+equatePat (PPair p1 p1') (PPair p2 p2')
+  | Just Refl <- testEquality (size p1) (size p2) =
+        equatePat p1 p2 >> equatePat p1' p2'
+equatePat (PAnnot p1 e1) (PAnnot p2 e2) =
+  equatePat p1 p2 >> equate e1 e2
+equatePat p1 p2 = throwError (PatternMismatch p1 p2)
+
+equateBranch :: (MonadError Err m) => Branch n -> Branch n -> m ()
+equateBranch (Branch b1) (Branch b2) =
+  let p1 = Scoped.getPat b1
+      p2 = Scoped.getPat b2
+      body1 = Scoped.getBody b1
+      body2 = Scoped.getBody b2 
+  in
+      case testEquality (size p1) (size p2) of
+        Just Refl ->
+          equatePat p1 p2 >> equate body1 body2
+        Nothing ->
+          throwError (PatternMismatch (Scoped.getPat b1) (Scoped.getPat b2))
+
+equateWHNF :: (MonadError Err m) => Exp n -> Exp n -> m ()
+equateWHNF n1 n2 =
+  case (n1, n2) of
+    (Star, Star) -> pure ()
+    (Var x, Var y) | x == y -> pure ()
+    (Match b1, Match b2) ->
+      List.zipWithM_ equateBranch b1 b2
+    (App a1 a2, App b1 b2) -> do
+      equateWHNF a1 b1
+      equate a2 b2
+    (Pi tyA1 b1, Pi tyA2 b2) -> do
+      equate tyA1 tyA2
+      equate (getBody1 b1) (getBody1 b2)
+    (Sigma tyA1 b1, Sigma tyA2 b2) -> do
+      equate tyA1 tyA2
+      equate (getBody1 b1) (getBody1 b2)
+    (_, _) -> throwError (NotEqual n1 n2)
+
+----------------------------------------------------------------
+
+-- * Type checking
+
+----------------------------------------------------------------
+
+
+inferPattern ::
+  (MonadError Err m) =>
+  Ctx Exp n -> -- input context
+  Pat p n -> -- pattern to check
+  m (Ctx Exp (p + n), Exp (p + n), Exp n)
+inferPattern g (PAnnot p ty) = do
+  (g', e) <- checkPattern g p ty
+  pure (g', e, ty)
+inferPattern g p = throwError (AnnotationNeededPat p)
+
+-- | type check a pattern and produce an extended typing context,
+-- plus expression form of the pattern (for dependent pattern matching)
+checkPattern ::
+  (MonadError Err m) =>
+  Ctx Exp n -> -- input context
+  Pat p n -> -- pattern to check
+  Exp n -> -- expected type of pattern (should be in whnf)
+  m (Ctx Exp (p + n), Exp (p + n))
+checkPattern g PVar a = do
+  pure (g +++ a, var f0)
+checkPattern g (PPair (p1 :: Pat p1 n) (p2 :: Pat p2 (p1 + n))) (Sigma tyA tyB) = do
+  -- need to know that Plus is associative
+  case axiomAssoc @p2 @p1 @n of
+    Refl -> do
+      (g', e1) <- checkPattern g p1 tyA
+      let tyB' = weakenBind' (size p1) tyB
+      let tyB'' = whnf (instantiate1 tyB' e1)
+      (g'', e2) <- checkPattern g' p2 tyB''
+      let e1' = weaken' (size p2) e1
+      return (g'', Pair e1' e2)
+checkPattern g p ty = do
+  (g', e, ty') <- inferPattern g p
+  equate ty ty'
+  return (g', e)
+
+-----------------------------------------------------------
+-- Checking branches
+-----------------------------------------------------------
+
+--      G |- p : A => G'      G' |- b : B { p / x}
+--   ----------------------------------------------
+--       G |- p => b : Pi x : A . B
+
+checkBranch ::
+  (MonadError Err m) =>
+  Ctx Exp n ->
+  Exp n ->
+  Branch n ->
+  m ()
+checkBranch g (Pi tyA tyB) (Branch bnd) = do
+    let pat  = Scoped.getPat bnd
+    let body = Scoped.getBody bnd
+    let p    = size pat
+
+    -- find the extended context and pattern expression
+    (g', a) <- checkPattern g pat tyA
+
+    -- shift tyB to the scope of the pattern and instantiate it with 'a'
+    -- must be done simultaneously because 'a' is from a larger scope
+    let tyB' = applyE (a .: shiftNE p) (getBody1 tyB)
+
+    -- check the body of the branch in the scope of the pattern
+    checkType g' body tyB'
+checkBranch g t e = throwError (PiExpected t)
+
+-- should only check with a type in whnf
+checkType ::
+  (MonadError Err m) =>
+  Ctx Exp n ->
+  Exp n ->
+  Exp n ->
+  m ()
+checkType g (Pair a b) ty = do
+  tyA <- inferType g a
+  tyB <- inferType g b
+  case ty of
+    (Sigma tyA tyB) -> do
+      checkType g a tyA
+      checkType g b (instantiate1 tyB a)
+    _ -> throwError (SigmaExpected ty)
+checkType g (Match bs) ty = do
+  List.mapM_ (checkBranch g ty) bs
+checkType g e t1 = do
+  t2 <- inferType g e
+  equate (whnf t2) t1
+
+-- | infer the type of an expression. This type may not
+-- necessarily be in whnf
+inferType ::
+  (MonadError Err m) =>
+  Ctx Exp n ->
+  Exp n ->
+  m (Exp n)
+inferType g (Var x) = pure (applyEnv g x)
+inferType g Star = pure star
+inferType g (Pi a b) = do
+  checkType g a star
+  checkType (g +++ a) (getBody1 b) star
+  pure star
+inferType g (App a b) = do
+  tyA <- inferType g a
+  case whnf tyA of
+    Pi tyA1 tyB1 -> do
+      checkType g b tyA1
+      pure $ instantiate1 tyB1 b
+    t -> throwError (PiExpected t)
+inferType g (Sigma a b) = do
+  checkType g a star
+  checkType (g +++ a) (getBody1 b) star
+  pure star
+inferType g a =
+  throwError (AnnotationNeeded a)
+
+-- >>> tmid
+-- λ_. (λ_. 0)
+
+-- >>> tyid
+-- Pi *. 0 -> 1
+
+-- >>> :t tyid
+-- tyid :: Exp n
+
+-- >>> (checkType zeroE tmid tyid :: Either Err ())
+-- Right ()
+
+
+-- >>> (inferType zeroE (App tmid tyid) :: Either Err (Exp N0))
+-- Left (AnnotationNeeded (λ_. (λ_. 0)))
diff --git a/examples/HOAS.hs b/examples/HOAS.hs
new file mode 100644
--- /dev/null
+++ b/examples/HOAS.hs
@@ -0,0 +1,93 @@
+
+module HOAS where
+
+{-
+This module demonstrates how to layer a HOAS-based representation
+on top of a de Bruijn representation, to make it easier to generate well scoped
+lambda terms.
+
+It is based on Conor McBride's "Classy Hack"
+https://mazzo.li/epilogue/index.html%3Fp=773.html
+
+-}
+
+import LC qualified
+import Data.Fin
+import Rebound.Bind.Single
+import Rebound
+
+-- Here are some HOAS lambda calculus terms
+
+tru :: Tm Z
+tru = Lam $ \x -> Lam $ \y -> Var x
+
+fls :: Tm Z
+fls = Lam $ \x -> Lam $ \y -> Var y
+
+app :: Tm Z
+app = Lam $ \f -> Lam $ \x -> App (Var f) (Var x)
+
+omega :: Tm Z
+omega = App delta delta where
+    delta = Lam $ \x -> App (Var x) (Var x)
+
+-- We can convert them to a de Bruijn-indexed
+-- representation easily
+
+-- >>> cvt tru
+-- (λ. (λ. 1))
+
+-- >>> cvt fls
+-- (λ. (λ. 0))
+
+-- >>> cvt app
+-- (λ. (λ. (1 0)))
+
+-- >>> cvt omega
+-- ((λ. (0 0)) (λ. (0 0)))
+
+
+-- These terms are elements of the following datatype
+-- that uses a form of "weak higher-order abstract syntax"
+-- for variable binding. A type class constraint in the variable
+-- constructor constructs the appropriate de Bruijn index.
+data Tm (a :: Nat) where
+  Var :: (b ⊆ a) => Proxy b -> Tm a
+  App :: Tm a -> Tm a -> Tm a
+  Lam :: (Proxy (S a) -> Tm (S a)) -> Tm a
+
+instance Cvt Tm LC.Exp where
+  cvt :: Tm m -> LC.Exp m
+  cvt (Var x)   = LC.Var (cvtVar x)
+  cvt (App a b) = LC.App (cvt a) (cvt b)
+  cvt (Lam f)   = LC.Lam (cvtBind f)
+
+------------------------------------------------------------
+-- The rest of this file is independent of the language that we are using
+-- and can be called reusable "library" code
+-- It depends on overlapping instances
+
+-- Conversion type class
+class Cvt t u | t -> u where
+  cvt :: t m -> u m
+
+class (b :: Nat) ⊆ (a :: Nat) where
+    inj :: Fin b -> Fin a
+instance {-# OVERLAPPING #-} n ⊆ n where inj = id
+instance {-# OVERLAPPING #-} (o ~ S n, m ⊆ n) => m ⊆ o where inj = FS . inj
+
+-- Note: you don't actually need overlapping instances for this example:
+--- instance n ⊆ n where inj = id
+--- instance n ⊆ S n where inj = FS
+-- would work just as well
+
+newtype Proxy b = P (Fin b)
+
+zeroVar :: Proxy (S b)
+zeroVar = P FZ
+
+cvtVar :: (b ⊆ a) => Proxy b -> Fin a
+cvtVar (P x) = inj x
+
+cvtBind :: (Subst v u, Cvt t u) => (Proxy (S a) -> t (S a)) -> Bind v u a
+cvtBind f = bind (cvt (f zeroVar))
diff --git a/examples/LC.hs b/examples/LC.hs
new file mode 100644
--- /dev/null
+++ b/examples/LC.hs
@@ -0,0 +1,268 @@
+-- |
+-- Module      : LC
+-- Description : Untyped lambda calculus
+-- Stability   : experimental
+--
+-- An implementation of the untyped lambda calculus including evaluation
+-- and small-step reduction.
+--
+-- This module demonstrates the use of well-scoped lambda calculus terms using `Rebound`.
+-- The natural number index `n` is the scoping level -- a bound on the number
+-- of free variables that can appear in the term. If `n` is 0, then the
+-- term must be closed.
+module LC where
+
+import Data.Fin
+import Data.Vec qualified
+import Rebound
+import Rebound.Bind.Single
+import Data.Fin
+import Data.Vec qualified
+import qualified Data.Maybe as Maybe
+
+-- | Datatype of well-scoped lambda-calculus expressions
+--
+-- The @Var@ constructor of this datatype takes an index that must
+-- be strictly less than the bound. Note that the type `Fin (S n)`
+-- has `n` different elements.
+
+-- The @Lam@ constructor binds a variable, using the the type `Bind`
+-- from the library. The type arguments state that the binder is
+-- for a single expression variable, inside an expression term, that may
+-- have at most `n` free variables.
+data Exp (n :: Nat) where
+  Var :: Fin n -> Exp n
+  Lam :: Bind Exp Exp n -> Exp n
+  App :: Exp n -> Exp n -> Exp n
+    deriving (Generic1)
+
+ 
+
+----------------------------------------------
+-- Example lambda-calculus expressions
+----------------------------------------------
+
+-- To make it easier to construct lambda calculus
+-- expressions, we'll first define some helper
+-- definitions
+
+-- | a lambda expression
+lam :: Exp (S n) -> Exp n
+lam = Lam . bind
+-- | an application expression
+(@@) :: Exp n -> Exp n -> Exp n
+(@@) = App
+-- | variable with index 0
+v0 :: Exp (S n)
+v0 = Var f0
+-- | variable with index 1
+v1 :: Exp (S (S n))
+v1 = Var f1
+
+
+-- | The identity function "λ x. x".
+-- With de Bruijn indices we write it as "λ. 0"
+t0 :: Exp Z
+t0 = lam v0
+
+-- >>> t0
+-- (λ. 0)
+
+
+-- For example, we can write
+-- (λx. ((x ((λy. y) x)) (λz. z)))
+-- using this term with de Bruijn indices
+-- (λ. ((0 ((λ. 0) 0)) (λ. 0)))
+-- and then construct it with the definitions above
+t :: Exp Z
+t = lam ((v0 @@ ((lam v0) @@ v0)) @@ (lam v0))
+
+----------------------------------------------
+-- (Alpha-)Equivalence
+----------------------------------------------
+
+-- The nice thing about de Bruijn indices is that
+-- we can use structural equality as alpha equivalence.
+-- The built-in Eq instance for Bind, makes sure that 
+-- the delayed substitutions are not observable here.
+deriving instance Eq (Exp n)
+
+
+----------------------------------------------
+-- Substitution
+----------------------------------------------
+
+-- To work with this library, we need two type class instances.
+-- First, we tell the library how to construct variables in the expression
+-- type. This class is necessary to construct an indentity
+-- substitution---one that maps each variable to itself.
+instance SubstVar Exp where
+  var :: Fin n -> Exp n
+  var = Var
+
+-- Second, the operation `applyE` applies an environment
+-- (explicit substitution) to an expression, and can be
+-- automatically generated by the `Subst` type class, as
+-- long as it can identify the variable constructor.
+-- (Insead of generic programming, this operation can also
+-- be written explicitly.)
+
+instance Subst Exp Exp where
+  isVar (Var x) = Just (Refl, x)
+  isVar _ = Nothing
+
+
+----------------------------------------------
+-- Display (Show)
+----------------------------------------------
+
+-- | To show lambda terms, we use a simple recursive instance of
+-- Haskell's `Show` type class. In the case of a binder, we use the `getBody`
+-- operation to access the body of the lambda expression.
+instance Show (Exp n) where
+  showsPrec :: Int -> Exp n -> String -> String
+  showsPrec _ (Var x) = shows x
+  showsPrec d (App e1 e2) =
+    showParen True $
+      showsPrec 10 e1
+        . showString " "
+        . showsPrec 11 e2
+  showsPrec d (Lam b) =
+    showParen True $
+      showString "λ. "
+        . shows (getBody b)
+
+-----------------------------------------------
+-- (big-step) evaluation
+-----------------------------------------------
+
+-- | Calculate the value of a lambda-calculus expression
+-- This function looks like it uses call-by-value evaluation:
+-- in an application it evaluates the argument `e2` before
+-- using the `instantiate` function from the library to substitute
+-- the bound variable of `Bind` by v. However, this is Haskell,
+-- a lazy language, so that result won't be evaluated unless the
+-- function actually uses its argument.
+eval :: Exp Z -> Exp Z
+eval (Var x) = case x of {}
+eval (Lam b) = Lam b
+eval (App e1 e2) =
+  let v = eval e2
+   in case eval e1 of
+        Lam b -> eval (instantiate b v)
+        t -> App t v
+
+
+-- >>> t0
+-- (λ. 0)
+
+-- >>> eval (t `App` t0)
+-- (λ. 0)
+
+-- ((λ. (λ. 1)) ((λ. 0) (λ. 0)))
+
+t2 = App (Lam (bind (Lam (bind (Var f1)))))
+         (App (Lam (bind (Var f0))) (Lam (bind (Var f0))))
+
+-- >>> t2
+-- ((λ. (λ. 1)) ((λ. 0) (λ. 0)))
+
+-- >>> eval t2
+-- (λ. (λ. 0))
+
+----------------------------------------------
+-- small-step evaluation
+----------------------------------------------
+
+-- | Do one step of evaluation, if possible
+-- If the function is already a value or is stuck
+-- this function returns `Nothing`
+step :: Exp n -> Maybe (Exp n)
+step (Var x) = Nothing
+step (Lam b) = Nothing
+step (App (Lam b) e2) = Just (instantiate b e2)
+step (App e1 e2)
+  | Just e1' <- step e1 = Just (App e1' e2)
+  | Just e2' <- step e2 = Just (App e1 e2')
+  | otherwise = Nothing
+
+-- | Evaluate the term as much as possible
+eval' :: Int -> Exp n -> Maybe (Exp n)
+eval' 0 e = Nothing
+eval' k e = case step e of
+              Just e' -> eval' (k - 1) e'
+              Nothing -> Just e
+
+-- >>> step (t0 `App` t0)
+-- Just (λ. 0)
+
+-- >>> eval' 5 (t `App` t0)
+-- Just (λ. 0)
+
+
+--------------------------------------------------------
+-- full normalization
+--------------------------------------------------------
+
+-- | Calculate the normal form of a lambda expression. This
+-- is like evaluation except that it also reduces underneath
+-- the binders of @Lam@ expressions. There, we must first `getBody`
+-- the binder and then rebind when finished
+nf :: Exp n -> Exp n
+nf (Var x) = Var x
+nf (Lam b) = Lam (bind (nf (getBody b)))
+nf (App e1 e2) =
+  case nf e1 of
+    Lam b -> nf (instantiate b e2)
+    t -> App t (nf e2)
+
+--------------------------------------------------------
+-- weak-head normalization / full reduction
+--------------------------------------------------------
+
+nf1 :: Exp n -> Exp n
+nf1 (Var x) = Var x
+nf1 (Lam b) = Lam (bind (nf1 (getBody b)))
+nf1 (App e1 e2) =
+  case whnf e1 of
+    Lam b -> nf1 (instantiate b (whnf e2))
+    t -> App t (nf e2)
+
+whnf :: Exp n -> Exp n
+whnf (Var x) = Var x
+whnf (Lam b) = Lam b
+whnf (App e1 e2) =
+  case nf e1 of
+    Lam b -> nf (instantiate b (whnf e2))
+    t -> App t (nf e2)
+
+--------------------------------------------------------
+-- environment based evaluation / normalization
+--------------------------------------------------------
+
+-- invariant: expressions in the range of the environment are in whnf
+whnfEnv :: Env Exp m n -> Exp m -> Exp n
+whnfEnv r (Var x) = applyEnv r x
+whnfEnv r (Lam b) = applyE r (Lam b)
+whnfEnv r (App f a) =
+  case whnfEnv r f of
+     Lam b ->
+       instantiateWith b (whnfEnv r a) whnfEnv
+        -- unbindWith b (\r' e' -> whnfEnv (whnfrEnv r a .: r') e')
+     f' -> App f' (applyE r a)
+
+-- >>> whnfEnv zeroE t     -- start with "empty environment"
+-- (λ. ((0 ((λ. 0) 0)) (λ. 0)))
+
+-- For full reduction, we need to normalize under the binder too.
+nfEnv :: Exp n -> Exp n
+nfEnv (Var x) = Var x
+nfEnv (Lam b) = Lam (bind (nfEnv (getBody b)))
+nfEnv (App f a) =
+   case whnfEnv idE f of
+        Lam b -> nfEnv (instantiate b (whnfEnv idE a))
+        f' -> App (nfEnv f') (nfEnv a)
+
+
+
+----------------------------------------------------------------
diff --git a/examples/LCLet.hs b/examples/LCLet.hs
new file mode 100644
--- /dev/null
+++ b/examples/LCLet.hs
@@ -0,0 +1,279 @@
+-- |
+-- Module      : LC
+-- Description : Untyped lambda calculus
+-- Stability   : experimental
+--
+-- An implementation of the untyped lambda calculus including let, letrec,
+-- mutual letrec and let* expressions.
+-- TODO: add example terms and fix Show instance
+module LCLet where
+
+import Rebound
+import Rebound.Bind.Single
+import qualified Rebound.Bind.Pat as Pat
+import Rebound.Bind.PatN as PatN (BindN, bindN, instantiateN, getBodyN)
+import Data.Fin
+import Data.Vec qualified as Vec
+
+-- | Datatype of well-scoped lambda-calculus expressions
+data Exp (n :: Nat) where
+  Var :: Fin n -> Exp n
+  Lam :: Bind Exp Exp n -> Exp n
+  App :: Exp n -> Exp n -> Exp n
+  Let ::
+    -- | single let expression
+    -- "let x = e1 in e2" where x is bound in e2
+    Exp n ->
+    (Bind Exp Exp n) ->
+    Exp n
+  LetRec ::
+    -- | "let rec x = e1 in e2" where x is bound in both e1 and e2
+    Rec n ->
+    Exp n
+  LetTele ::
+    -- | sequence of nested lets, where each one may depend on
+    -- the previous binding
+    -- "let x1 = e1 in x2 = e2 in ... in e" where x1 is bound
+    -- in e2, e3 ... and e, x2 is bound in e3 and e, etc.
+    Tele n ->
+    Exp n
+  LetMutRec ::
+    -- | mutual recursive lets, where each one may depend on
+    -- any other variable
+    -- "let x1 = e1 in x2 = e2 in ... in e" where x1 ... xn
+    -- are bound in e1, e2, e3 ... and e
+    MutRec n ->
+    Exp n
+
+data Rec n =
+  Rec { rec_rhs  :: Bind Exp Exp n,   -- single RHS
+        rec_body :: Bind Exp Exp n }  -- body of let
+
+data MutRec n = forall m. SNatI m =>
+  MutRec { mutrec_rhss :: Vec m (BindN Exp Exp m n), -- Vector of RHSs
+           mutrec_body :: BindN Exp Exp m n  -- body of let
+           }
+
+data Tele n where
+  LetStar :: Exp n -> Bind Exp Tele n -> Tele n
+  Body :: Exp n -> Tele n
+
+----------------------------------------------
+-- Example lambda-calculus expressions
+----------------------------------------------
+
+-- some variables
+v0 :: Exp (S n)
+v0 = Var f0
+v1 :: Exp (S (S n))
+v1 = Var f1
+v2 :: Exp (S (S (S n)))
+v2 = Var f2
+-- | an application expression
+(@@) :: Exp n -> Exp n -> Exp n
+(@@) = App
+-- | a lambda expression
+lam :: Exp (S n) -> Exp n
+lam = Lam . bind
+
+letrec :: Exp (S n) -> Exp (S n) -> Exp n
+letrec e1 e2 = LetRec (Rec (bind e1) (bind e2))
+
+letstar :: Exp n -> Tele (S n) -> Tele n
+letstar e t = LetStar e (bind t)
+
+-- | The identity function "λ x. x".
+-- With de Bruijn indices we write it as "λ. 0"
+-- The `bind` function creates the binder
+-- t0 :: Exp Z
+t0 = lam v0
+
+-- >>> t0
+-- (λ. 0)
+
+-- | A larger term "λ x. λy. x ((λ z. z) y)"
+-- λ. λ. 1 ((λ. 0) 0))
+t1 :: Exp Z
+t1 = lam (lam (v1 @@ ((lam v0) @@ v0)))
+
+-- >>> t1
+-- (λ. (λ. (1 ((λ. 0) 0))))
+
+-- let x = \y.y in x x
+t2 :: Exp Z
+t2 = Let t0 (bind (App v0 v0))
+
+-- >>> t2
+-- (let (λ. 0) in (0 0))
+
+-- let rec fix = \f. f (fix f) in f
+t3 :: Exp Z
+t3 = letrec (lam (v0 @@(v1 @@ v0))) v0
+-- >>> t3
+-- (let rec (λ. (0 (1 0))) in 0)
+
+-- let* x1 = \x.x ; x2 = x1 x1 ; x3 = x2 s1 in x3 x2 x1
+t4 = LetTele
+       (letstar t0
+         (letstar (v0 @@ v0)
+            (letstar (v0 @@ v1)
+              (Body ((v0 @@ v1) @@ v2)))))
+
+-- >>> t4
+-- <let-tele>
+
+----------------------------------------------
+-- (Alpha-)Equivalence
+----------------------------------------------
+
+-- | The derivable equality instance
+-- is alpha-equivalence
+deriving instance (Eq (Exp n))
+
+deriving instance (Eq (Tele n))
+
+deriving instance (Eq (Rec n))
+
+instance Eq (MutRec n) where
+  (==) :: MutRec n -> MutRec n -> Bool
+  MutRec { mutrec_rhss= (rhss1 :: Vec m1 t1), mutrec_body=body1} ==
+    MutRec { mutrec_rhss= (rhss2 :: Vec m2 t2), mutrec_body=body2}
+    = case testEquality (snat @m1) (snat @m2) of
+       Just Refl -> Vec.all2 (==) rhss1 rhss2 && body1 == body2
+       Nothing -> False
+----------------------------------------------
+-- Substitution
+----------------------------------------------
+
+-- To work with this library, we need two type class instances.
+
+-- | Tell the library how to construct variables in the expression
+-- type. This class is necessary to construct an indentity
+-- substitution---one that maps each variable to itself.
+instance SubstVar Exp where
+  var :: Fin n -> Exp n
+  var = Var
+
+-- The library represents a substitution using an "Environment".
+-- The type `Env Exp n m` is a substitution that can be applied to
+-- indices bounded by n. It produces a result `Exp` with indices
+-- bounded by m. The function `applyEnv` looks up a mapping in
+-- an environment.
+
+-- | The operation `applyE` applies an environment
+-- (explicit substitution) to an expression.
+--
+-- The implementation of this operation applies the environment to
+-- variable index in the variable case. All other caseas follow
+-- via recursion. The library includes a type class instance for
+-- the Bind type which handles the variable lifting needed under
+-- the binder.
+instance Shiftable Exp where
+  shift = shiftFromApplyE @Exp
+instance Subst Exp Exp where
+  applyE :: Env Exp n m -> Exp n -> Exp m
+  applyE r (Var x) = applyEnv r x
+  applyE r (Lam b) = Lam (applyE r b)
+  applyE r (App e1 e2) = App (applyE r e1) (applyE r e2)
+  applyE r (Let e1 e2) = Let (applyE r e1) (applyE r e2)
+  applyE r (LetRec e) = LetRec (applyE r e)
+  applyE r (LetTele e) = LetTele (applyE r e)
+  applyE r (LetMutRec e) = LetMutRec (applyE r e)
+
+instance Subst Exp Rec where
+  applyE r (Rec rhs body) = Rec (applyE r rhs) (applyE r body)
+
+instance Shiftable Tele where
+  shift = shiftFromApplyE @Exp
+instance Subst Exp Tele where
+  applyE r (Body e) = Body (applyE r e)
+  applyE r (LetStar e1 e2) = LetStar (applyE r e1) (applyE r e2)
+
+instance Subst Exp MutRec where
+  applyE r (MutRec rhss body) =
+    MutRec (fmap (applyE r) rhss) (applyE r body)
+
+----------------------------------------------
+-- Display (Show)
+----------------------------------------------
+
+-- | To show lambda terms, we use a simple recursive instance of
+-- Haskell's `Show` type class. In the case of a binder, we use the `getBody`
+-- operation to access the body of the lambda expression.
+instance Show (Exp n) where
+  showsPrec :: Int -> Exp n -> String -> String
+  showsPrec _ (Var x) = shows x
+  showsPrec d (App e1 e2) =
+    showParen True $
+      showsPrec 10 e1
+        . showString " "
+        . showsPrec 11 e2
+  showsPrec d (Lam b) =
+    showParen True $
+      showString "λ. "
+        . shows (getBody b)
+  showsPrec d (Let e1 e2) =
+    showParen True $
+      showString "let "
+        . showsPrec 10 e1
+        . showString " in "
+        . shows (getBody e2)
+  showsPrec d (LetRec (Rec{rec_rhs=rhs,rec_body=body})) =
+    showParen True $
+      showString "let rec "
+        . shows (getBody rhs)
+        . showString " in "
+        . shows (getBody body)
+  showsPrec d (LetTele e) = showString "<let-tele>"
+  showsPrec d (LetMutRec (MutRec {mutrec_rhss=rhss, mutrec_body=body})) =
+    showParen True $
+      showString "let rec "
+        . showString " rhss " -- (getBodyN e1)
+        . showString " in "
+        . shows (getBodyN body)
+-----------------------------------------------
+-- (big-step) evaluation
+-----------------------------------------------
+
+-- >>> eval t1
+-- (λ. (λ. (1 ((λ. 0) 0))))
+
+-- >>> eval (t1 @@ t0)
+-- (λ. ((λ. 0) ((λ. 0) 0)))
+
+-- >>> eval t2
+-- (λ. 0)
+
+-- This one is an infinite loop
+-- >>> eval t3
+-- ProgressCancelledException
+
+-- >>> eval t4
+-- (λ. 0)
+
+eval :: Exp n -> Exp n
+eval (Var x) = Var x
+eval (Lam b) = Lam b
+eval (App e1 e2) =
+  let v = eval e2
+   in case eval e1 of
+        Lam b -> eval (instantiate b v)
+        t -> App t v
+eval (Let e1 e2) =
+  eval (instantiate e2 (eval e1))
+eval (LetRec e) =
+  -- use a Haskell recursive definition
+  -- to tie the knot for a recursive definition
+  -- in the object language
+  let v = instantiate (rec_rhs e) v
+   in eval (instantiate (rec_body e) v)
+eval (LetTele e) = evalTele e
+eval (LetMutRec (MutRec { mutrec_rhss = rhss, mutrec_body = body})) =
+  -- use a Haskell recursive definition
+  let vs = fmap (\b -> instantiateN b vs) rhss
+  in eval (instantiateN body vs)
+
+evalTele :: Tele n -> Exp n
+evalTele (Body e) = eval e
+evalTele (LetStar e t) =
+  evalTele (instantiate t (eval e))
diff --git a/examples/LCQC.hs b/examples/LCQC.hs
new file mode 100644
--- /dev/null
+++ b/examples/LCQC.hs
@@ -0,0 +1,86 @@
+-- |
+-- Module      : LCQC
+-- Description : Generators for well-scoped lambda calculus terms
+-- Stability   : experimental
+--
+-- This module demonstrates the use of well-scoped lambda calculus terms
+-- with QuickCheck. It then demonstrates how to use QC to test the normalization
+-- functions in the `LC` module.
+module LCQC where
+
+import LC
+import Test.QuickCheck
+import Rebound
+import Rebound.Bind.Single
+import Data.Fin
+import Data.Maybe as Maybe
+
+----------------------------------------------
+-- Generating well-scoped expressions
+----------------------------------------------
+
+-- | Generate an expression in scope `n`, using
+-- size parameter sz
+-- >>> sample' (genExp s3 10)
+-- [(λ. (λ. (0 2))),(((λ. 0) (1 2)) ((0 2) 2)),(((2 1) 0) (1 1)),1,((λ. 2) (2 (λ. 1))),(λ. 2),2,0,((λ. (0 1)) (λ. (1 3))),(((1 2) 2) (λ. (0 2))),(λ. 2)]
+genExp :: forall n. SNat n -> Int -> Gen (Exp n)
+genExp n sz =
+    let
+        genLam = Lam <$> (bind <$> genExp (next n) (sz `div` 2))
+        genApp = App <$> genExp n (sz `div` 2) <*> genExp n (sz `div` 2)
+    in
+    case snat_ n of
+       SZ_ -> if sz <= 1
+                then pure $ Lam (bind (Var 0))  -- smallest closed term
+                else oneof [genLam, genApp]     -- closed term, no vars
+       SS_ x ->
+         let
+            genVar = withSNat x $ elements $ map Var universe
+         in
+            if sz <= 1
+              then genVar
+              else oneof [genVar, genLam, genApp]
+
+-- | shrink a lambda calculus term, maintaining the same scope.
+shrinkExp :: SNatI n => Exp n -> [Exp n]
+shrinkExp (Var FZ) = []
+shrinkExp (Var x ) = [ Var (pred x) ]
+shrinkExp (Lam t)  = [ Lam (bind t') | t' <- shrinkExp (getBody t) ]
+shrinkExp (App f a) =
+  [f,a] ++ [ App f' a | f' <- shrinkExp f]
+        ++ [ App f a' | a' <- shrinkExp a]
+
+-- | arbitrary instance for lambda calculus terms
+instance SNatI n => Arbitrary (Exp n) where
+  arbitrary :: SNatI n => Gen (Exp n)
+  arbitrary = sized (genExp snat)
+  shrink :: SNatI n => Exp n -> [Exp n]
+  shrink = shrinkExp
+
+
+----------------------------------------------
+-- Property-based testing example
+----------------------------------------------
+
+{-
+
+Let's use QuickCheck to make sure that our various
+normalization functions for the lambda calculus all produce the
+same result.
+
+However, we need to deal with the fact that not all lambda
+calculus terms normalize. Therefore, we will instruct QC to discard
+the test case if the expression does not normalize within 0.1 seconds.
+We will also discard expressions that are already in normal form.
+-}
+
+prop_normalize :: (Exp n -> Exp n) -> Exp n -> Property
+prop_normalize f e = discardAfter 100000 $
+    e /= e' ==> e' == f e
+   where e' = nf e
+
+prop_nf1 :: Exp Z -> Property
+prop_nf1 = prop_normalize nf1
+
+prop_nfEnv :: Exp Z -> Property
+prop_nfEnv = prop_normalize nfEnv
diff --git a/examples/LinLC.hs b/examples/LinLC.hs
new file mode 100644
--- /dev/null
+++ b/examples/LinLC.hs
@@ -0,0 +1,214 @@
+-- |
+-- Module       : LinLC
+-- Description  : Linear simply typed lambda calculus
+-- Stability    : experimental
+--
+-- A typechecker for a linear lambda calculus. This module demonstrates the
+-- usage of the `ScopedState` monad, which can be used when a typing context
+-- has to be threaded during typechecking.
+module LinLC where
+
+import Control.Monad
+import Control.Monad.Except
+import Data.Fin
+import Data.Vec as Vec hiding (bind)
+import Rebound hiding (rescope)
+import Rebound.Bind.Single hiding (rescope)
+import Rebound.MonadScoped
+import Prelude
+
+-- | Run a monadic computation, then another, and return the result of the
+-- first. Or, put another way: the reverse of `<<`.
+(<<) :: (Monad m) => m a -> m b -> m a
+l << r = do
+  vl <- l
+  r
+  return vl
+
+--------------------------------------------------------------------------------
+--- Basic definitions
+--------------------------------------------------------------------------------
+
+-- | Represent the (current) usage of a variable.
+data Usage where
+  Unused :: Usage
+  Used :: Usage
+  deriving (Show, Eq)
+
+-- | Representation of types.
+data Ty where
+  TyUnit :: Ty
+  TyArrow :: Ty -> Ty -> Ty
+  deriving (Show, Eq)
+
+-- | Representation of terms.
+data Exp (n :: Nat) where
+  Var :: Fin n -> Exp n
+  CUnit :: Exp n
+  Lam :: Bind Exp Exp n -> Exp n
+  App :: Exp n -> Exp n -> Exp n
+  deriving (Eq, Generic1)
+
+-- Some instances required by rebound. See LC.hs for more explanations.
+
+instance SubstVar Exp where var = Var
+
+instance Subst Exp Exp where
+  isVar (Var x) = Just (Refl, x)
+  isVar _ = Nothing
+
+--------------------------------------------------------------------------------
+--- Some helper-constructors
+--------------------------------------------------------------------------------
+
+-- | a lambda expression
+lam :: Exp (S n) -> Exp n
+lam = Lam . bind
+
+-- | an application expression
+(@@) :: Exp n -> Exp n -> Exp n
+(@@) = App
+
+-- | variable with index 0
+v0 :: Exp (S n)
+v0 = Var f0
+
+-- | variable with index 1
+v1 :: Exp (S (S n))
+v1 = Var f1
+
+-- | Notation for the arrow type
+(~>) :: Ty -> Ty -> Ty
+(~>) = TyArrow
+
+infixr 8 ~>
+
+--------------------------------------------------------------------------------
+--- Typechecking infrastructure
+--------------------------------------------------------------------------------
+
+-- | A typing environment has to keep track of two things about variables:
+-- 1. What their type is (`types`).
+-- 2. Whether they've already been used or not (`usages`).
+data TCEnv n = TCEnv
+  { types :: Vec n Ty,
+    usages :: Vec n Usage
+  }
+
+-- | The typechecking monad.
+--
+-- Unlike other calculi, the typing environment is not local. When two
+-- sub-expressions have to be typechecked, the second sub-expression has to be
+-- typechecked in the environment generated by the first sub-expression. Note
+-- that this new environment will not include any new binding, but typing the
+-- first sub-expression may have altered the usage of variables. This means that
+-- `ScopedReader` cannot be used, as any changes done to the context are
+-- forgotten when the scope is reverted. Hence, the `ScopedState` monad must be
+-- used instead.
+type TC n a = ScopedStateT TCEnv (Except String) n a
+
+-- | Attempt to "consume" a variable, returning its type. Fails if the variable
+-- was already used.
+consumeVar :: Fin n -> TC n Ty
+consumeVar i = setUsage i >> getsS ((! i) . types)
+  where
+    -- \| Set a variable to `Used`. Fails if the variable was already used.
+    setUsage :: Fin n -> TC n ()
+    setUsage i = do
+      current <- getsS usages
+      let (new, old) = set i Used current
+      unless (old == Unused) $ throwError "Variable has already been used."
+      modifyS $ \s -> s {usages = new}
+
+    -- \| Set a value in the vector. Return the updated vector as well as the
+    -- previous value.
+    set :: Fin n -> t -> Vec n t -> (Vec n t, t)
+    set FZ v (h ::: t) = (v ::: t, h)
+    set (FS i) v (h ::: t) =
+      let (t', v') = set i v t
+       in (h ::: t', v')
+
+-- | Add a variable to the scope.
+--
+-- Since the typing environment is threaded rather than passed down, as would be
+-- the case with `ScopedReader`'s `localS`, we need to provide functions to both
+-- enter a new scope, as with `localS`, and to leave it. Additionally, we need
+-- to check that variables were used when they go out of scope.
+addBinder :: Ty -> TC (S n) a -> TC n a
+addBinder ty m = rescope enter leave (m << checkUsed FZ)
+  where
+    checkUsed :: Fin n -> TC n ()
+    checkUsed i = do
+      u <- getsS ((! i) . usages)
+      unless (u == Used) $ throwError "Variable was not used."
+
+    -- \| Add the binding in the scope.
+    enter :: TCEnv n -> TCEnv (S n)
+    enter e = e {types = ty ::: types e, usages = Unused ::: usages e}
+
+    -- \| And remove it.
+    leave :: TCEnv (S n) -> TCEnv n
+    leave e = e {types = Vec.tail $ types e, usages = Vec.tail $ usages e}
+
+-- | Run a computation in the typechecking monad, assuming that all free
+-- variables need to be used.
+runTC :: forall n a. (SNatI n) => Vec n Ty -> TC n a -> Either String a
+runTC ts c = runExcept $ evalScopedStateT (c << checkAllUsed) initEnv
+  where
+    -- \| The initial environment, in which all variables are unused.
+    initEnv :: TCEnv n
+    initEnv = TCEnv {types = ts, usages = Vec.tabulate $ const Unused}
+
+    -- \| Checks that all values are tagged as used.
+    checkAllUsed :: TC n ()
+    checkAllUsed = do
+      us <- getsS usages
+      let r = Vec.foldr (\u acc -> u == Used && acc) True us
+      unless r $ throwError "Some variables in the initial scope were not used."
+
+--------------------------------------------------------------------------------
+--- Bi-directional typechecking
+--------------------------------------------------------------------------------
+
+-- Most of the unusual stuff about linear type systems is contained in the
+-- handling of variables, which was implemented above. Hence the following code
+-- should be rather unsurprising.
+
+inferType :: Exp n -> TC n Ty
+inferType (Var i) = consumeVar i
+inferType CUnit = return TyUnit
+inferType _ = throwError "Cannot infer type of this construct."
+
+checkType :: Exp n -> Ty -> TC n ()
+checkType (Lam bnd) ty = do
+  let t = unbindl bnd
+  (xTy, tTy) <- ensureArrow ty
+  addBinder xTy $ checkType t tTy
+  where
+    ensureArrow :: Ty -> TC n (Ty, Ty)
+    ensureArrow (TyArrow l r) = return (l, r)
+    ensureArrow _ = throwError "Type is not arrow."
+checkType (App f a) rTy = do
+  aTy <- inferType a
+  checkType f (TyArrow aTy rTy)
+checkType t ty = do
+  ty' <- inferType t
+  unless (ty == ty') $ throwError "Inferred type does not match expected type."
+
+-- >>> runTC empty $ checkType (lam $ v0) (TyUnit ~> TyUnit)
+-- Right ()
+
+-- >>> runTC empty $ checkType (lam $ lam $ v0 @@ v1) (TyUnit ~> (TyUnit ~> TyUnit) ~> TyUnit)
+-- Right ()
+
+-- >>> runTC empty $ checkType (lam $ lam $ v1) (TyUnit ~> (TyUnit ~> TyUnit) ~> TyUnit)
+-- Left "Variable was not used."
+
+-- >>> runTC empty $ checkType (lam $ lam $ v0) (TyUnit ~> (TyUnit ~> TyUnit) ~> TyUnit)
+-- Left "Inferred type does not match expected type."
+
+-- >>> runTC empty $ checkType (lam $ lam $ v0) (TyUnit ~> (TyUnit ~> TyUnit) ~> TyUnit ~> TyUnit)
+-- Left "Variable was not used."
+
+-- >>> runTC (TyUnit ::: (TyUnit ~> TyUnit) ::: TyUnit ::: Vec.empty) $ checkType (v1 @@ v0) (TyUnit)
+-- Left "Some variables in the initial scope were not used."
diff --git a/examples/PTS.hs b/examples/PTS.hs
new file mode 100644
--- /dev/null
+++ b/examples/PTS.hs
@@ -0,0 +1,439 @@
+-- Pure-type system-like example
+-- Includes Pi/Sigma, untyped equivalence
+module PTS where
+
+import Rebound
+
+import qualified Rebound.Bind.Pat as Pat
+import Rebound.Bind.PatN as PatN
+import Rebound.Context
+import Control.Monad.Except (ExceptT, MonadError (..), runExceptT)
+import Data.Fin(Fin(..), f0,f1,f2)
+import Data.Fin qualified as Fin
+import Data.Vec qualified as Vec
+import GHC.Generics (Generic1)
+
+-- In a pure type system, terms and types are combined
+-- into the same syntactic class.
+
+data Exp (n :: Nat) where
+  -- | sort
+  Star :: Exp n
+  -- | dependent type `Pi x : A . B`
+  Pi :: Exp n -> Bind1 Exp Exp n -> Exp n
+  -- | variable
+  Var :: Fin n -> Exp n
+  -- | lambda expression, with type annotation  `lambda x:A.B`
+  Lam :: Exp n -> Bind1 Exp Exp n -> Exp n
+  -- | application
+  App :: Exp n -> Exp n -> Exp n
+  -- | dependent pair `Sigma x:A . B`
+  Sigma :: Exp n -> Bind1 Exp Exp n -> Exp n
+  -- | construct a pair, third argument is type annotation
+  Pair :: Exp n -> Exp n -> Exp n -> Exp n
+  -- | elimination form for pairs. `split e1 as (x,y) in e2`
+  -- Binds two variables to
+  -- the two components of the pair
+  Split :: Exp n -> Bind2 Exp Exp n -> Exp n
+
+deriving instance (Generic1 Exp)
+----------------------------------------------
+
+instance SubstVar Exp where
+  var :: Fin n -> Exp n
+  var = Var
+
+instance Shiftable Exp where
+  shift = shiftFromApplyE @Exp
+
+instance Subst Exp Exp where
+  isVar (Var x) = Just (Refl, x)
+  isVar _ = Nothing
+----------------------------------------------
+
+t00 :: Exp N2
+t00 = App (Var f0) (Var f0)
+
+t01 :: Exp N2
+t01 = App (Var f0) (Var f1)
+
+-- Does a variable appear free in a term?
+
+-- >>> appearsFree f0 t00
+-- True
+
+-- >>> appearsFree f1 t00
+-- False
+instance FV Exp where
+
+-- >>> :t weaken' s1 t00
+-- weaken' s1 t00 :: Exp ('S ('S N1))
+
+-- >>> weaken' s1 t00
+-- 0 0
+
+weaken' :: SNat m -> Exp n -> Exp (m + n)
+weaken' m = applyE @Exp (weakenE' m)
+
+-- >>> strengthenRec s1 s1 snat t00
+-- Just (0 0)
+
+-- >>> strengthenRec s1 s1 snat t01
+-- Nothing
+
+instance Strengthen Exp where
+
+----------------------------------------------
+-- Examples
+
+-- The identity function "λ x. x". With de Bruijn indices
+-- we write it as "λ. 0"
+t0 :: Exp Z
+t0 = Lam Star (bind1 (Var f0))
+
+-- A larger term "λ x. λy. x (λ z. z z)"
+-- λ. λ. 1 (λ. 0 0)
+t1 :: Exp Z
+t1 =
+  Lam
+    Star
+    ( bind1
+        ( Lam
+            Star
+            ( bind1
+                ( Var f1
+                    `App` (Lam Star (bind1 (Var f0)) `App` Var f0)
+                )
+            )
+        )
+    )
+
+-- To show lambda terms, we can write a simple recursive instance of
+-- Haskell's `Show` type class. In the case of a binder, we use the `unbind1`
+-- operation to access the body of the lambda expression.
+
+-- >>> t0
+-- λ *. 0
+
+-- >>> t1
+-- λ *. λ *. 1 ((λ *. 0) 0)
+
+-- Polymorphic identity function and its type
+
+tyid = Pi Star (bind1 (Pi (Var f0) (bind1 (Var f1))))
+
+tmid = Lam Star (bind1 (Lam (Var f0) (bind1 (Var f0))))
+
+-- >>> tyid
+-- Pi *. 0 -> 1
+
+-- >>> tmid
+-- λ *. λ 0. 0
+
+instance Show (Exp n) where
+  showsPrec :: Int -> Exp n -> String -> String
+  showsPrec _ Star = showString "*"
+  showsPrec d (Pi a b)
+    | appearsFree FZ (getBody1 b) =
+        showParen (d > 9) $
+          showString "Pi "
+            . shows a
+            . showString ". "
+            . shows (getBody1 b)
+    | otherwise =
+        showParen (d > 9) $
+          showsPrec 11 a
+            . showString " -> "
+            . showsPrec 9 (getBody1 b)
+  showsPrec d (Sigma a b)
+    | appearsFree FZ (getBody1 b) =
+        showParen (d > 9) $
+          showString "Sigma "
+            . shows a
+            . showString ". "
+            . shows (getBody1 b)
+    | otherwise =
+        showParen (d > 9) $
+          showsPrec 11 a
+            . showString " * "
+            . showsPrec 9 (getBody1 b)
+  showsPrec _ (Var x) = shows x
+  showsPrec d (App e1 e2) =
+    showParen (d > 0) $
+      showsPrec 10 e1
+        . showString " "
+        . showsPrec 11 e2
+  showsPrec d (Lam t b) =
+    showParen (d > 9) $
+      showString "λ "
+        . shows t
+        . showString ". "
+        . shows (getBody1 b)
+  showsPrec d (Pair e1 e2 t) =
+    showParen (d > 0) $
+      showsPrec 10 e1
+        . showString ", "
+        . showsPrec 11 e2
+  showsPrec d (Split t b) =
+    showParen (d > 10) $
+      showString "split"
+        . showsPrec 10 t
+        . showString " in "
+        . shows (getBody2 b)
+
+-- With the instance above the derivable equality instance
+-- is alpha-equivalence
+deriving instance (Eq (Exp n))
+
+--------------------------------------------------------
+
+{- We can write the usual operations for evaluating
+   lambda terms to values -}
+
+-- big-step evaluation
+
+-- >>> eval t1
+-- λ *. λ *. 1 ((λ *. 0) 0)
+
+-- >>> eval (t1 `App` t0)
+-- λ *. (λ *. 0) ((λ *. 0) 0)
+
+eval :: Exp n -> Exp n
+eval (Var x) = Var x
+eval (Lam a b) = Lam a b
+eval (App e1 e2) =
+  let v = eval e2
+   in case eval e1 of
+        Lam a b -> eval (instantiate1 b v)
+        t -> App t v
+eval Star = Star
+eval (Pi a b) = Pi a b
+eval (Sigma a b) = Sigma a b
+eval (Pair a b t) = Pair a b t
+eval (Split a b) =
+  case eval a of
+    Pair a1 a2 _ ->
+      eval (instantiate2 b (eval a1) (eval a2))
+    t -> Split t b
+
+-- small-step evaluation
+
+-- >>> step (t1 `App` t0)
+-- Just (λ *. λ *. 0 (λ *. 0 0))
+
+step :: Exp n -> Maybe (Exp n)
+step (Var x) = Nothing
+step (Lam a b) = Nothing
+step (App (Lam a b) e2) = Just (instantiate1 b e2)
+step (App e1 e2)
+  | Just e1' <- step e1 = Just (App e1' e2)
+  | Just e2' <- step e2 = Just (App e1 e2')
+  | otherwise = Nothing
+step Star = Nothing
+step (Pi a b) = Nothing
+step (Sigma a b) = Nothing
+step (Pair a b _) = Nothing
+step (Split (Pair a1 a2 _) b) = Just (PatN.instantiate2 b a1 a2)
+step (Split a b)
+  | Just a' <- step a = Just (Split a' b)
+  | otherwise = Nothing
+
+eval' :: Exp n -> Exp n
+eval' e
+  | Just e' <- step e = eval' e'
+  | otherwise = e
+
+-- normalization
+-- to normalize under a lambda expression, we must first unbind
+-- it and then rebind it when finished
+
+-- >>> nf t1
+-- λ. λ. 1 0
+
+-- >>> nf (t1 `App` t0)
+-- λ *. 0
+
+-- reduce the term everywhere, as much as possible
+nf :: Exp n -> Exp n
+nf (Var x) = Var x
+nf (Lam a b) = Lam a (bind1 (nf (getBody1 b)))
+nf (App e1 e2) =
+  case nf e1 of
+    Lam a b -> nf (instantiate1 b e2)
+    t -> App t (nf e2)
+nf Star = Star
+nf (Pi a b) = Pi (nf a) (bind1 (nf (getBody1 b)))
+nf (Sigma a b) = Sigma (nf a) (bind1 (nf (getBody1 b)))
+nf (Pair a b t) = Pair (nf a) (nf b) (nf t)
+nf (Split a b) =
+  case nf a of
+    Pair a1 a2 _ -> nf (PatN.instantiate2 b a1 a2)
+    t -> Split t (PatN.bind2 (nf (getBody2 b)))
+
+-- first find the head form
+whnf :: Exp n -> Exp n
+whnf (App a1 a2) = case whnf a1 of
+  Lam a b -> whnf (instantiate1 b a1)
+  t -> App t a2
+whnf (Split a b) = case whnf a of
+  Pair a1 a2 _ -> whnf (PatN.instantiate2 b a1 a2)
+  t -> Split t b
+-- all other terms are already in head form
+whnf a = a
+
+norm :: Exp n -> Exp n
+norm a = case whnf a of
+  Lam a b -> Lam (norm a) (bind1 (norm (getBody1 b)))
+  Pi a b -> Pi (norm a) (bind1 (norm (getBody1 b)))
+  Sigma a b -> Sigma (norm a) (bind1 (norm (getBody1 b)))
+  Pair a b t -> Pair (norm a) (norm b) (norm t)
+  Star -> Star
+  App a b -> App a (norm b)
+  Split a b -> Split a (PatN.bind2 (norm (getBody2 b)))
+  Var x -> Var x
+
+--------------------------------------------------------
+-- We can also write functions that manipulate the
+-- environment explicitly
+
+-- >>> evalEnv idE t1
+-- λ *. λ *. 1 ((λ *. 0) 0)
+
+-- Below, if n is 0, then this function acts like an
+-- "environment-based" bigstep evaluator. The result of
+-- evaluating a lambda expression is a closure --- the body
+-- of the lambda paired with its environment. That is exactly
+-- what the implementation of bind1 does.
+
+-- In the case of beta-reduction, the `unBindWith` operation
+-- applies its argument to the environment and subterm in the
+-- closure. In other words, this function calls `evalEnv`
+-- recursively with the saved environment and body of the lambda term.
+
+evalEnv :: Env Exp m n -> Exp m -> Exp n
+evalEnv r (Var x) = applyEnv r x
+evalEnv r (Lam a b) = applyE r (Lam a b)
+evalEnv r (App e1 e2) =
+  let v = evalEnv r e2
+   in case evalEnv r e1 of
+        Lam a b ->
+          unbindWith1 b (\r' e' -> evalEnv (v .: r') e')
+        t -> App t v
+evalEnv r Star = Star
+evalEnv r (Pi a b) = applyE r (Pi a b)
+evalEnv r (Sigma a b) = applyE r (Sigma a b)
+evalEnv r (Pair a b t) = applyE r (Pair a b t)
+evalEnv r (Split a b) =
+  case evalEnv r a of
+    Pair a1 a2 _ ->
+      PatN.unbindWith2 b ( \r' e' -> evalEnv (a1 .: (a2 .: (r' .>> r))) e')
+    t -> Split t (applyE r b)
+
+----------------------------------------------------------------
+data Err where
+  Equate :: Exp n -> Exp n -> Err
+  PiExpected :: Exp n -> Err
+  SigmaExpected :: Exp n -> Err
+  VarEscapes :: Exp n -> Err
+
+deriving instance (Show Err)
+
+equate :: (MonadError Err m) => Exp n -> Exp n -> m ()
+equate t1 t2 = do
+  let n1 = whnf t1
+      n2 = whnf t2
+  equateWHNF n1 n2
+
+equateWHNF :: (MonadError Err m) => Exp n -> Exp n -> m ()
+equateWHNF n1 n2 =
+  case (n1, n2) of
+    (Star, Star) -> pure ()
+    (Var x, Var y) | x == y -> pure ()
+    (Lam _ b1, Lam _ b2) -> equate (getBody1 b1) (getBody1 b2)
+    (App a1 a2, App b1 b2) -> do
+      equateWHNF a1 b1
+      equate a2 b2
+    (Pi tyA1 b1, Pi tyA2 b2) -> do
+      equate tyA1 tyA2
+      equate (getBody1 b1) (getBody1 b2)
+    (Pair a1 a2 _, Pair b1 b2 _) -> do
+      equate a1 b1
+      equate a2 b2
+    (Split a1 b1, Split a2 b2) -> do
+      equateWHNF a1 a2
+      equate (getBody2 b1) (getBody2 b2)
+    (Sigma tyA1 b1, Sigma tyA2 b2) -> do
+      equate tyA1 tyA2
+      equate (getBody1 b1) (getBody1 b2)
+    (_, _) -> throwError (Equate n1 n2)
+
+----------------------------------------------------------------
+
+checkType ::
+  forall n m.
+  (MonadError Err m, SNatI n) =>
+  Ctx Exp n ->
+  Exp n ->
+  Exp n ->
+  m ()
+checkType g e t1 = do
+  t2 <- inferType g e
+  equate (whnf t2) t1
+
+inferType ::
+  forall n m.
+  (MonadError Err m, SNatI n) =>
+  Ctx Exp n ->
+  Exp n ->
+  m (Exp n)
+inferType g (Var x) = pure (applyEnv g x)
+inferType g Star = pure Star
+inferType g (Pi a b) = do
+  checkType g a Star
+  checkType (g +++ a) (getBody1 b) Star
+  pure Star
+inferType g (Lam tyA b) = do
+  checkType g tyA Star
+  tyB <- inferType (g +++ tyA) (getBody1 b)
+  return $ Pi tyA (bind1 tyB)
+inferType g (App a b) = do
+  tyA <- inferType g a
+  case whnf tyA of
+    Pi tyA1 tyB1 -> do
+      checkType g b tyA1
+      pure $ instantiate1 tyB1 b
+    t -> throwError (PiExpected t)
+inferType g (Sigma a b) = do
+  checkType g a Star
+  checkType (g +++ a) (getBody1 b) Star
+  pure Star
+inferType g (Pair a b ty) = do
+  tyA <- inferType g a
+  tyB <- inferType g b
+  case ty of
+    (Sigma tyA tyB) -> do
+      checkType g a tyA
+      checkType g b (instantiate1 tyB a)
+      pure ty
+    _ -> throwError (SigmaExpected ty)
+inferType g (Split a b) = do
+  tyA <- inferType g a
+  case whnf tyA of
+    Sigma tyA' tyB' -> do
+      let g' :: Ctx Exp (S (S n))
+          g' = g +++ tyA' +++ getBody1 tyB'
+      ty <- inferType g' (getBody2 b)
+      let ty' = whnf ty
+      case strengthenN s2 ty' of
+        Nothing -> throwError (VarEscapes ty)
+        Just ty'' -> pure ty''
+    _ -> throwError (SigmaExpected tyA)
+
+
+-- >>> inferType zeroE tmid :: Either Err (Exp N0)
+-- Right (Pi *. 0 -> 1)
+
+
+-- >>> inferType zeroE (App tmid tyid) :: Either Err (Exp N0)
+-- Right ((Pi *. 0 -> 1) -> Pi *. 0 -> 1)
+
diff --git a/examples/Pat.hs b/examples/Pat.hs
new file mode 100644
--- /dev/null
+++ b/examples/Pat.hs
@@ -0,0 +1,535 @@
+-- \| The untyped lambda calculus with pattern matching
+--
+
+-- |
+-- Module      : Pat
+-- Description : Untyped lambda calculus, with pattern matching
+-- Stability   : experimental
+--
+-- An implementation of the untyped lambda calculus with pattern matching.
+--
+-- This example extends the lambda calculus with constants (like 'nil and 'cons)
+-- and arbitrary pattern matching. Case expressions include a list of branches,
+-- where each branch is a pattern and a right-hand side. The pattern can bind
+-- multiple variables and the index ensures that the rhs matches the number of
+-- variables bound in the pattern.
+-- 
+-- This example also includes pairs and "irrefutable patterns" i.e. let binding
+-- that can deeply deconstruct cons pairs (only).
+module Pat where
+
+import Rebound
+
+import Rebound.Bind.PatN as PatN
+import qualified Rebound.Bind.Pat as Pat
+import Rebound.Bind.Scoped qualified as Scoped
+import Data.Maybe qualified as Maybe
+import Data.Type.Equality
+import Data.Fin ( Fin, f0, f1 )
+import Data.Fin qualified as Fin
+import Data.Vec qualified as Vec
+
+----------------------------------------------
+
+-- * Syntax
+
+----------------------------------------------
+
+-- The untyped lambda calculus extended with
+-- symbols ("con"stants) and pattern matching
+-- expression (case)
+-- A constant applied to any number of arguments
+-- is a value
+data Exp (n :: Nat) where
+  Var :: Fin n -> Exp n
+  Lam :: Bind1 Exp Exp n -> Exp n
+  App :: Exp n -> Exp n -> Exp n
+  LetPair  :: Exp n -> Branch PairPat n -> Exp n
+  -- ^ deep pattern matching against pairs (only)
+  Con :: String -> Exp n
+  -- ^ constant (or symbol) like 'cons or 'nil'
+  Case :: Exp n -> [Branch Pat n] -> Exp n
+  -- ^ deep pattern matching against any pattern
+   
+
+-- Each branch in a case expression is a pattern binding,
+-- i.e. a data structure that binds m variables in some
+-- expression body with scope n
+-- Here, the variable m does not appear
+-- in the result type `Branch pat n`, so is an existential.
+data Branch pat (n :: Nat) where
+  Branch :: SNatI m => Pat.Bind Exp Exp (pat m) n -> Branch pat n
+
+-- Patterns for case expressions.
+-- The index `m` in the pattern is the number of occurrences of
+-- PVar, i.e. the number variables bound by the pattern.
+-- These variables are ordered left to right.
+-- For example (PCon "cons" `PApp` PVar `PApp` PVar) is the
+-- representation of the pattern "cons x y", which binds two
+-- variables.
+-- To prevent patterns of the form "x y z", this type is split
+-- into top level patterns (Pat) and applications of constants (ConApp)
+data Pat (m :: Nat) where
+  PVar :: Pat N1 -- binds exactly one variable
+  PHead :: ConApp m -> Pat m
+
+data ConApp (m :: Nat) where
+  PCon :: String -> ConApp N0 -- binds zero variables
+  PApp :: ConApp m1 -> Pat m2 -> ConApp (m2 + m1)
+
+-- Patterns for pairs only, a special case of the above
+data PairPat (m :: Nat) where
+  PPVar :: PairPat N1
+  PPair :: PairPat m1 -> PairPat m2 -> PairPat (m2 + m1)
+
+----------------------------------------------
+
+-- * Sized instance
+
+----------------------------------------------
+
+-- Any type that is used as a pattern must be an
+-- instance of the `Sized` type class, so that the library
+-- can determine the number of binding variables both
+-- statically and dynamically.
+
+-- The `Pat` type tells us how many variables are bound
+-- the pattern with the index `n`. We can also recover
+-- that number from the pattern itself by counting the number
+-- of occurrences of `PVar`.
+
+instance Sized (Pat m) where
+  type Size (Pat m) = m
+
+  size :: Pat m -> SNat (Size (Pat m))
+  size PVar = s1
+  size (PHead p) = size p
+
+
+instance Sized (ConApp m) where
+  type Size (ConApp m) = m
+
+  size :: ConApp m -> SNat (Size (ConApp m))
+  size (PApp p1 p2) = sPlus (size p2) (size p1)
+  size (PCon s) = s0
+
+
+instance Sized (PairPat m) where
+  type Size (PairPat m) = m
+
+  size :: PairPat m -> SNat (Size (PairPat m))
+  size PPVar = s1
+  size (PPair p1 p2) = sPlus (size p2) (size p1)
+
+
+----------------------------------------------
+
+-- * Substitution
+
+----------------------------------------------
+
+instance SubstVar Exp where
+  var :: Fin n -> Exp n
+  var = Var
+
+instance Shiftable Exp where
+  shift = shiftFromApplyE @Exp
+
+instance Subst Exp Exp where
+  applyE :: Env Exp n m -> Exp n -> Exp m
+  applyE r (Var x) = applyEnv r x
+  applyE r (Lam b) = Lam (applyE r b)
+  applyE r (App e1 e2) = App (applyE r e1) (applyE r e2)
+  applyE r (Con s) = Con s
+  applyE r (Case e brs) = Case (applyE r e) (map (applyE r) brs)
+  applyE r (LetPair e1 b) = LetPair (applyE r e1) (applyE r b)
+
+
+instance Shiftable (Branch pat) where
+  shift = shiftFromApplyE @Exp
+
+instance Subst Exp (Branch pat) where
+  applyE :: Env Exp n m -> Branch pat n -> Branch pat m
+  applyE r (Branch bnd) = Branch (applyE r bnd)
+
+
+----------------------------------------------
+-- Example terms
+----------------------------------------------
+
+-- The identity function "λ x. x". With de Bruijn indices
+-- we write it as "λ. 0"
+t0 :: Exp Z
+t0 = Lam (bind1 (Var f0))
+
+-- A larger term "λ x. λy. x (λ z. z z)"
+-- λ. λ. 1 (λ. 0 0)
+t1 :: Exp Z
+t1 =
+  Lam
+    ( bind1
+        ( Lam
+            ( bind1
+                ( Var f1
+                    `App` (Lam (bind1 (Var f0)) `App` Var f0)
+                )
+            )
+        )
+    )
+
+-- "head function"
+-- \x -> case x of [nil -> x ; cons y z -> y]
+t2 :: Exp Z
+t2 =
+  Lam
+    ( bind1
+        ( Case
+            (Var f0)
+            [ Branch
+                ( Pat.bind @(Pat N0)
+                    (PHead (PCon "Nil"))
+                    (Var f0)
+                ),
+              Branch
+                ( Pat.bind @(Pat N2)
+                    (PHead (PCon "Cons" `PApp` PVar `PApp` PVar))
+                    (Var f0)
+                )
+            ]
+        )
+    )
+
+-- a "list"  ['a','b']
+t3 :: Exp Z
+t3 = Con "cons" `App` Con "a" `App` (Con "cons" `App` Con "b" `App` Con "nil")
+
+--------------------------------------------------------------
+
+-- * Show implementation
+
+--------------------------------------------------------------
+
+-- >>> t0
+-- λ. 0
+
+-- >>> t1
+-- λ. λ. 1 (λ. 0 0)
+
+-- >>> t2
+-- λ. case 0 of [Nil => 0,(Cons V) V => 0]
+
+-- >>> t3
+-- (cons a) ((cons b) nil)
+
+
+instance Show (Exp n) where
+  showsPrec :: Int -> Exp n -> String -> String
+  showsPrec _ (Var x) = shows x
+  showsPrec d (App e1 e2) =
+    showParen (d > 0) $
+      showsPrec 10 e1
+        . showString " "
+        . showsPrec 11 e2
+  showsPrec d (Lam b) =
+    showParen (d > 10) $
+      showString "λ. "
+        . shows (getBody1 b)
+  showsPrec d (Con s) = showString s
+  showsPrec d (Case e brs) =
+    showParen (d > 10) $
+      showString "case "
+        . shows e
+        . showString " of "
+        . shows brs
+  showsPrec d (LetPair e (Branch b)) = 
+    showString "let "
+    . shows (Pat.getPat b)
+    . showString " = "
+    . shows e
+    . showString " in "
+    . showsPrec d (Pat.getBody b)
+
+instance Show (PairPat m) where
+  showsPrec :: Int -> PairPat m -> String -> String
+  showsPrec d PPVar = showString "V"
+  showsPrec d (PPair p1 p2) =
+    showParen True $
+      shows p1
+        . showString ","
+        . shows p2
+
+instance Show (Pat m) where
+  showsPrec :: Int -> Pat m -> String -> String
+  showsPrec d PVar = showString "V"
+  showsPrec d (PHead p) = showsPrec d p
+
+instance Show (ConApp m) where
+  showsPrec d (PApp p1 p2) =
+    showParen (d > 0) $
+      showsPrec 10 p1
+        . showString " "
+        . showsPrec 11 p2
+  showsPrec d (PCon s) = showString s
+
+
+-- In a `PatBind` term, we can access the pattern with `getPat`
+-- and the RHS with `getBody`
+instance Show (Branch Pat n) where
+  showsPrec :: Int -> Branch Pat n -> String -> String
+  showsPrec d (Branch bnd) =
+    shows (Pat.getPat bnd)
+      . showString " => "
+      . showsPrec d (Pat.getBody bnd)
+
+--------------------------------------------------------------
+
+-- * Eq implementation
+
+--------------------------------------------------------------
+
+-- We would like to derive equality for patterns, i.e. 
+-- 
+--     deriving instance (Eq (Pat m))
+-- 
+-- but because of the application case, this process fails.
+-- We don't know that each subpattern binds the same
+-- number of variables!
+
+
+-- Therefore to compare Pats for equality, we generalize the
+-- `testEquality` function from Data.Type.Equality. (This
+-- class is often used for comparisons between indexed types.
+-- but only works if the index is the last type parameter.
+-- In our case, we need to produce an equality for the
+-- first type parameter.)
+-- This function can be applied, even if the number of
+-- pattern-bound variables are not known to be equal.
+-- (cf. m1 and m2 below). If the patterns are indeed equal,
+-- then `patEq` *also* returns a proof that the indices
+-- are equal. (The type `a :~: b` is a GADT with a single
+-- constructor `Refl` that can only be used when a and be are
+-- equal. Pattern matching on this GADT brings an equality
+-- between a and b into the context of the term.)
+
+instance PatEq (Pat m1) (Pat m2) where
+  patEq PVar PVar = Just Refl
+  patEq (PHead p1) (PHead p2) = do
+    Refl <- patEq p1 p2
+    return Refl
+  patEq _ _ = Nothing
+
+instance PatEq (ConApp m1) (ConApp m2) where
+  patEq (PApp p1 p2) (PApp p1' p2') = do
+    Refl <- patEq p1 p1'
+    Refl <- patEq p2 p2'
+    return Refl
+  patEq (PCon s1) (PCon s2) | s1 == s2 = Just Refl
+  patEq _ _ = Nothing
+
+instance PatEq (PairPat m1) (PairPat m2) where
+  patEq (PPair p1 p2) (PPair p1' p2') = do
+    Refl <- patEq p1 p1'
+    Refl <- patEq p2 p2'
+    return Refl
+  patEq PPVar PPVar = Just Refl
+  patEq _ _ = Nothing
+
+
+-- the generalized equality can be used for the usual equality
+instance Eq (Pat m) where
+  p1 == p2 = Maybe.isJust (patEq p1 p2)
+
+instance Eq (PairPat m) where
+  p1 == p2 = Maybe.isJust (patEq p1 p2)
+
+instance SizeIndex PairPat p
+instance SizeIndex Pat p
+
+
+-- Because the Branch type is parameterized by a pattern type, `pat` of kind 
+-- `Nat -> Type` we need to make some assumptions about that type to construct
+-- the `Eq` instance. (1) we need to be able to test patterns for equality
+-- no matter what their size is. (2) we need to know that the index *is* the 
+-- size of the pattern, i.e. Size (pat m) ~ m. The `SizeIndex` class captures
+-- this relationship in a way that can be quantifed over all m.
+-- If we did not parameterize the `Branch` type by the pattern type, we would not
+-- need this complexity.
+
+instance (forall m. Eq (pat m),                    -- 1
+          forall m. SizeIndex pat m)               -- 2
+          => Eq (Branch pat n) where
+  (==) :: Branch pat n -> Branch pat n -> Bool
+  (Branch (p1 :: Pat.Bind Exp Exp (pat m1) n))
+    == (Branch (p2 :: Pat.Bind Exp Exp (pat m2) n)) =
+      case testEquality
+        (size (Pat.getPat p1) :: SNat m1)
+        (size (Pat.getPat p2) :: SNat m2) of
+        Just Refl -> p1 == p2
+        Nothing -> False
+
+
+-- With the instance above the derivable equality instance
+-- is alpha-equivalence
+deriving instance (Eq (Exp n))
+
+
+--------------------------------------------------------
+-- Pattern matching code
+--------------------------------------------------------
+
+p1 :: Pat N2
+p1 = PHead $ PApp (PApp (PCon "C") PVar) PVar
+
+p2 :: Pat N2
+p2 = PHead $ PApp (PApp (PCon "D") PVar) PVar
+
+e1 :: Exp N0
+e1 = App (App (Con "C") (Con "A")) (Con "B")
+
+e2 :: Exp N0
+e2 = App (App (Con "D") (Con "A")) (Con "C")
+
+-- >>> patternMatch p1 e1
+-- Just [(0,B),(1,A)]
+
+-- >>> patternMatch p2 e1
+-- Nothing
+
+-- >>> patternMatch p1 e2
+-- Nothing
+
+-- >>> patternMatch p2 e2
+-- Just [(0,C),(1,A)]
+
+
+-- | Compare a "pair" pattern with a pair pattern, potentially
+-- producing a substitution for all of the variables bound in the pattern.
+ppatternMatch :: PairPat p -> Exp m -> Maybe (Env Exp p m)
+ppatternMatch PPVar e = Just $ oneE e
+ppatternMatch (PPair p1 p2) (App (App (Con "cons") e1) e2) = do
+  env1 <- ppatternMatch p1 e1
+  env2 <- ppatternMatch p2 e2
+  withSNat (size p2) $ return (env2 .++ env1)
+ppatternMatch _ _ = Nothing
+
+-- | Compare a pattern with an expression, potentially
+-- producing a substitution for all of the variables bound in the pattern.
+patternMatch :: Pat p -> Exp m -> Maybe (Env Exp p m)
+patternMatch PVar e = Just $ oneE e
+patternMatch (PHead p) e = patternMatchApp p e
+
+patternMatchApp :: ConApp p -> Exp m -> Maybe (Env Exp p m)
+patternMatchApp (PApp p1 p2) (App e1 e2) = do
+  env1 <- patternMatchApp p1 e1
+  env2 <- patternMatch p2 e2
+  withSNat (size p2) $ return (env2 .++ env1)
+patternMatchApp (PCon s1) (Con s2) =
+  if s1 == s2 then Just zeroE else Nothing
+patternMatchApp _ _ = Nothing
+
+-- Compare the scrutinee against multiple patterns and return 
+-- the matching branch
+findBranch :: Exp n -> [Branch Pat n] -> Maybe (Exp n)
+findBranch e [] = Nothing
+findBranch e (Branch bind : brs) =
+  case patternMatch (Pat.getPat bind) e of
+    Just r -> Just $ Pat.instantiate bind r
+    Nothing -> findBranch e brs
+
+
+
+
+--------------------------------------------------------
+-- Eval and step
+--------------------------------------------------------
+
+{- We can write the usual operations for evaluating
+   lambda terms to values -}
+-- big-step evaluation
+-- >>> eval t1
+-- λ. λ. 1 (λ. 0 0)
+-- >>> eval (t1 `App` t0)
+-- λ. λ. 0 (λ. 0 0)
+t4 = t2 `App` t3
+
+-- >>> t4
+-- λ. case 0 of [Nil => 0,(Cons V) V => 0] ((cons a) ((cons b) nil))
+-- >>> eval t4
+-- case (cons a) ((cons b) nil) of [Nil => (cons a) ((cons b) nil),(Cons V) V => 0]
+eval :: Exp n -> Exp n
+eval (Var x) = Var x
+eval (Lam b) = Lam b
+eval (App e1 e2) =
+  let v = eval e2
+   in case eval e1 of
+        Lam b -> eval (instantiate1 b v)
+        t -> App t v -- if cannot reduce, return neutral term
+eval (Con s) = Con s
+eval (Case e brs) =
+  let v = eval e
+   in case findBranch v brs of
+        Just br -> eval br
+        Nothing -> Case v brs -- if cannot reduce, return neutral term
+eval (LetPair e (Branch b)) = 
+  case ppatternMatch (Pat.getPat b) (eval e) of
+    Just r -> eval (Pat.instantiate b r)
+    Nothing -> error "No match!"
+
+-- | small-step evaluation
+-- >>> step (t1 `App` t0)
+-- Just (λ. λ. 0 (λ. 0 0))
+step :: Exp n -> Maybe (Exp n)
+step (Var x) = Nothing
+step (Lam b) = Nothing
+step (App (Lam b) e2) = Just (instantiate1 b e2)
+step (App e1 e2)
+  | Just e1' <- step e1 = Just (App e1' e2)
+  | Just e2' <- step e2 = Just (App e1 e2')
+  | otherwise = Nothing
+step (LetPair a (Branch b)) 
+  | Just r <- ppatternMatch (Pat.getPat b) a
+  = Just (Pat.instantiate b r)
+step (LetPair e b) 
+  | Just e' <- step e = Just (LetPair e' b)
+  | otherwise = Nothing
+step (Con s) = Nothing
+step (Case e brs)
+  | Just br <- findBranch e brs = Just br
+  | Just e' <- step e = Just (Case e' brs)
+  | otherwise = Nothing
+
+eval' :: Exp n -> Exp n
+eval' e
+  | Just e' <- step e = eval' e'
+  | otherwise = e
+
+-- full normalization
+-- to normalize under a lambda expression, we must first unbind
+-- it and then rebind it when finished
+
+-- >>> nf t1
+-- λ. λ. 1 0
+-- >>> nf (t1 `App` t0)
+-- λ. λ. 0 0
+nf :: Exp n -> Exp n
+nf (Var x) = Var x
+nf (Lam b) = Lam (bind1 (nf (getBody1 b)))
+nf (App e1 e2) =
+  case nf e1 of
+    Lam b -> instantiate1 b (nf e2)
+    t -> App t (nf e2)
+nf (Con s) = Con s
+nf (Case e brs) =
+  let v = nf e
+   in case findBranch v brs of
+        Just b -> nf b
+        Nothing -> Case e (map nfBr brs)
+nf (LetPair e br@(Branch b)) = 
+  let v = nf e in
+  case ppatternMatch (Pat.getPat b) v of 
+    Just r -> nf (Pat.instantiate b r)
+    Nothing -> LetPair (nf e) (nfBr br)
+
+nfBr :: (forall n. Sized (pat n)) => Branch pat n -> Branch pat n
+nfBr (Branch bnd) =
+  Branch (Pat.bind (Pat.getPat bnd) (nf (Pat.getBody bnd)))
diff --git a/examples/PureSystemF.hs b/examples/PureSystemF.hs
new file mode 100644
--- /dev/null
+++ b/examples/PureSystemF.hs
@@ -0,0 +1,277 @@
+-- | An implementation of System F as a (quasi) Pure Type System.
+module PureSystemF where
+
+import Control.Monad (unless)
+import Control.Monad.Except (Except (..), MonadError (..), runExcept)
+import Data.Fin (f0, f1, f2)
+import Data.Vec ((!))
+import Data.Vec qualified as Vec
+import Rebound
+import Rebound.Bind.Local
+import Rebound.MonadNamed qualified as Scoped
+import Rebound.MonadScoped (MonadScopedReader (..), ScopedReader, ScopedReaderT (..), asksS, runScopedReader)
+import Rebound.MonadScoped qualified as Scoped
+import Text.Read (Lexeme (String))
+
+-- | We represent both terms and types using one single
+-- syntactic class. We use one single constructor for variables,
+-- regardless of whether they stand for a term or a
+-- variable. We also use an additional constructor, 'Kind',
+-- which is used to represent the type of types.
+data Exp (n :: Nat) where
+  Var :: Fin n -> Exp n
+  Kind :: Exp n
+  -- Types
+  TAll :: Bind Ty Ty n -> Ty n
+  TArr :: Ty n -> Ty n -> Ty n
+  -- Terms
+  Abs :: Ty n -> Bind Exp Exp n -> Exp n
+  App :: Exp n -> Exp n -> Exp n
+  TAbs :: Bind Ty Exp n -> Exp n
+  TApp :: Exp n -> Ty n -> Exp n
+  deriving (Eq)
+
+-- | An alias used for readability.
+type Ty = Exp
+
+--------------------------------------------------------------------------------
+--- Instances required by Rebound
+--------------------------------------------------------------------------------
+
+instance SubstVar Exp where
+  var = Var
+
+instance Subst Exp Exp where
+  applyE :: forall n m. Env Exp n m -> Exp n -> Exp m
+  applyE env t = case t of
+    Var x -> applyEnv env x
+    Kind -> Kind
+    TAll bnd -> TAll (r bnd)
+    TArr t1 t2 -> TArr (r t1) (r t2)
+    Abs ty bnd -> Abs (r ty) (r bnd)
+    App t1 t2 -> App (r t1) (r t2)
+    TAbs bnd -> TAbs (r bnd)
+    TApp t1 t2 -> TApp (r t1) (r t2)
+    where
+      r :: forall t. (Subst Exp t) => t n -> t m
+      r = applyE env
+
+-- We will be needing strengthening in the type-checker;
+-- more on that later.
+instance Strengthen Exp where
+  strengthenRec ::
+    forall k m n.
+    SNat k ->
+    SNat m ->
+    SNat n ->
+    Exp (k + (m + n)) ->
+    Maybe (Exp (k + n))
+  strengthenRec k m n t = case t of
+    Var x -> Var <$> strengthenRec k m n x
+    Kind -> return Kind
+    TAll bnd -> TAll <$> r bnd
+    TArr t1 t2 -> TArr <$> r t1 <*> r t2
+    Abs ty bnd -> Abs <$> r ty <*> r bnd
+    App t1 t2 -> App <$> r t1 <*> r t2
+    TAbs bnd -> TAbs <$> r bnd
+    TApp t1 t2 -> TApp <$> r t1 <*> r t2
+    where
+      r :: (Strengthen t) => t (k + (m + n)) -> Maybe (t (k + n))
+      r = strengthenRec k m n
+
+--------------------------------------------------------------------------------
+--- Typechecking
+--------------------------------------------------------------------------------
+
+-- | An environment mapping (de Bruijn) variables to
+-- a user-defined name and its type.
+data TcEnv n = TcEnv
+  { names :: Vec n LocalName,
+    types :: Ctx Exp n
+  }
+
+emptyEnv :: TcEnv Z
+emptyEnv = TcEnv {names = Vec.empty, types = zeroE}
+
+-- | Add a new binding to the environment
+extendE :: (LocalName, Exp n) -> TcEnv n -> TcEnv (S n)
+extendE (n, t) (TcEnv ns ts) =
+  TcEnv (n ::: ns) (ts +++ t)
+
+-- | Search for a binding. Lookup cannot fail
+-- thanks to extrinsic scoping.
+lookupE :: TcEnv n -> Fin n -> (LocalName, Exp n)
+lookupE (TcEnv ns ts) i = (ns ! i, applyEnv ts i)
+
+type Error = String
+
+-- | Typechecking monad.
+newtype TC n a = TC (ScopedReaderT TcEnv (Except Error) n a)
+  deriving (Functor, Applicative, Monad, MonadError Error)
+
+-- Trivial lifting through a newtype.
+instance MonadScopedReader TcEnv TC where
+  askS = TC askS
+  localS f (TC m) = TC (localS f m)
+
+-- | Run the type-checking monad. Returns
+-- either the result, or an error.
+runTC :: TcEnv n -> TC n a -> Either Error a
+runTC env (TC m) = runExcept $ runScopedReaderT m env
+
+-- | Extend the current (latent) scope with a new binding.
+push :: LocalName -> Exp n -> TC (S n) a -> TC n a
+push n t = Scoped.localS $ extendE (n, t)
+
+-- | Lookup a binding in the (latent) scope.
+get :: Fin n -> TC n (LocalName, Exp n)
+get i = readerS (`lookupE` i)
+
+-- | Checks that a given type is indeed a (valid) type,
+-- by ensuring that its own type is 'Kind'.
+ensureType :: (SNatI n) => Ty n -> TC n ()
+ensureType Kind = return ()
+ensureType ty = do
+  k <- inferType ty
+  unless (k == Kind) $ throwError "Not a type"
+
+-- | Infer the type of an expression.
+inferType :: (SNatI n) => Exp n -> TC n (Ty n)
+inferType (Var x) = do
+  (_, ty) <- get x
+  ensureType ty
+  return ty
+inferType Kind =
+  -- Kind is used internally to represent a well-formed
+  -- type, but should not be used otherwise.
+  throwError "Cannot type 'Kind'"
+-- Types
+inferType (TAll bnd) = do
+  let (x, t) = unbindl bnd
+  push x Kind $ ensureType t
+  return Kind
+inferType (TArr l r) =
+  ensureType l >> ensureType r >> return Kind
+-- Terms
+inferType (Abs xTy bnd) = do
+  let (x, t) = unbindl bnd
+  ensureType xTy
+  tTy <- push x xTy $ inferType t
+  -- Because the type system is not dependent, we cannot
+  -- allow 'x' to occur in 'tTy'. Ensuring this and bringing
+  -- 'tTy' into the outer scope is done using 'strengthenN'.
+  case strengthenN s1 tTy of
+    Just tTy' -> return $ TArr xTy tTy'
+    Nothing -> throwError "Term variable occurs in type"
+inferType (App l r) = do
+  lTy <- inferType l
+  rTy <- inferType r
+  case lTy of
+    TArr rTy' retTy -> do
+      unless (rTy == rTy') $ throwError "Argument mismatch"
+      return retTy
+    _ -> throwError "Left hand-side of application is not an arrow"
+inferType (TAbs bnd) = do
+  let (x, t) = unbindl bnd
+  tTy <- push x Kind $ inferType t
+  return $ TAll $ bind x tTy
+inferType (TApp l r) = do
+  lTy <- inferType l
+  ensureType r
+  case lTy of
+    TAll bnd -> return $ instantiate bnd r
+    _ -> throwError "Left hand-side is not a forall"
+
+--------------------------------------------------------------------------------
+--- (Pretty) Printing
+--------------------------------------------------------------------------------
+
+-- | An environment mapping variables to their (user-defined) name.
+data PpEnv n = PpEnv
+  { ppnames :: Vec n String,
+    pplevel :: Int
+  }
+
+-- | Pretty-print a term.
+pp :: Vec n LocalName -> Exp n -> String
+pp s e = runScopedReader (pp' e) (PpEnv {ppnames = fmap name s, pplevel = 0})
+  where
+    setLevel :: Int -> ScopedReader PpEnv n String -> ScopedReader PpEnv n String
+    setLevel newLevel = localS (\e -> e {pplevel = newLevel})
+
+    atLevel :: Int -> ScopedReader PpEnv n String -> ScopedReader PpEnv n String
+    atLevel newLevel m = do
+      level <- asksS pplevel
+      let m' = if level <= newLevel then m else (\s -> "(" ++ s ++ ")") <$> m
+      setLevel newLevel m'
+
+    push n = localS (\e -> e {ppnames = n ::: ppnames e})
+
+    pp' :: Exp n -> ScopedReader PpEnv n String
+    pp' (Var f) = asksS (\e -> ppnames e ! f)
+    pp' Kind = return "Kind"
+    pp' (TAll bnd) = atLevel 0 $ do
+      let (LocalName x, b) = unbindl bnd
+      b' <- push x $ pp' b
+      return $ "∀" ++ x ++ ". " ++ b'
+    pp' (TArr l r) = atLevel 1 $ do
+      l' <- atLevel 2 $ pp' l
+      r' <- pp' r
+      return $ l' ++ " -> " ++ r'
+    pp' (Abs ty bnd) = atLevel 0 $ do
+      let (LocalName x, b) = unbindl bnd
+      b' <- push x $ pp' b
+      return $ "λ" ++ x ++ ". " ++ b'
+    pp' (App l r) = atLevel 2 $ do
+      l' <- pp' l
+      r' <- atLevel 3 $ pp' r
+      return $ l' ++ " " ++ r'
+    pp' (TAbs bnd) = atLevel 0 $ do
+      let (LocalName x, b) = unbindl bnd
+      b' <- push x $ pp' b
+      return $ "Λ" ++ x ++ ". " ++ b'
+    pp' (TApp l r) = atLevel 2 $ do
+      l' <- pp' l
+      r' <- setLevel 0 $ pp' r
+      return $ l' ++ " [" ++ r' ++ "]"
+
+instance Show (Exp Z) where
+  show = pp Vec.empty
+
+t0, t1, t2 :: Exp Z
+t0 = TAbs (bind (LocalName "X") $ Abs (var f0) (bind (LocalName "x") $ var f0))
+-- >>> t0
+-- >>> runTC emptyEnv $ inferType t0
+-- ΛX. λx. x
+-- Right ∀X. X -> X
+
+t1 = TAbs (bind (LocalName "X") $ Abs (TAll (bind (LocalName "Y") $ TArr (var f0) (var f0))) (bind (LocalName "f") $ Abs (var f1) (bind (LocalName "x") $ App (TApp (var f1) (var f2)) (var f0))))
+-- >>> t1
+-- >>> runTC emptyEnv $ inferType t1
+-- ΛX. λf. λx. f [X] x
+-- Right ∀X. (∀Y. Y -> Y) -> X -> X
+
+t2 = Abs Kind (bind (LocalName "X") $ Abs (var f0) (bind (LocalName "x") (var f0)))
+-- >>> t2
+-- >>> runTC emptyEnv $ inferType t2
+-- λX. λx. x
+-- Left "Term variable occurs in type"
+
+bbn0, bbn1, bbn2 :: Exp Z
+bbn0 = TAbs (bind (LocalName "X") $ Abs (TArr (var f0) (var f0)) (bind (LocalName "f") $ Abs (var f1) (bind (LocalName "z") $ (var f0))))
+bbn1 = TAbs (bind (LocalName "X") $ Abs (TArr (var f0) (var f0)) (bind (LocalName "f") $ Abs (var f1) (bind (LocalName "z") $ App (var f1) (var f0))))
+bbn2 = TAbs (bind (LocalName "X") $ Abs (TArr (var f0) (var f0)) (bind (LocalName "f") $ Abs (var f1) (bind (LocalName "z") $ App (var f1) (App (var f1) (var f0)))))
+-- >>> bbn0
+-- >>> runTC emptyEnv $ inferType bbn0
+-- ΛX. λf. λz. z
+-- Right ∀X. (X -> X) -> X -> X
+
+-- >>> bbn1
+-- >>> runTC emptyEnv $ inferType bbn1
+-- ΛX. λf. λz. f z
+-- Right ∀X. (X -> X) -> X -> X
+
+-- >>> bbn2
+-- >>> runTC emptyEnv $ inferType bbn2
+-- ΛX. λf. λz. f (f z)
+-- Right ∀X. (X -> X) -> X -> X
diff --git a/examples/ScopeCheck.hs b/examples/ScopeCheck.hs
new file mode 100644
--- /dev/null
+++ b/examples/ScopeCheck.hs
@@ -0,0 +1,64 @@
+-- |
+-- Module      : ScopeCheck
+-- Description : Scope checking the Untyped lambda calculus
+-- Stability   : experimental
+--
+-- This module demonstrates a translation from unscoped to well-scoped terms
+
+module ScopeCheck where
+
+import Rebound
+import Rebound.Bind.Single
+import Data.Maybe (fromJust)
+import LC qualified
+import Rebound.Lib
+
+-- | Named representation for the untyped lambda calculus
+-- The type parameter 'a' is the variable type
+data Exp (a :: Type) where
+  Var :: a -> Exp a
+  Lam :: a -> Exp a -> Exp a
+  App :: Exp a -> Exp a -> Exp a
+
+-- | Convert a named expression to deBruijn indices, checking to make
+-- sure that the expression is well scoped
+scopeCheck :: (Eq a) => Exp a -> Maybe (LC.Exp Z)
+scopeCheck = to []
+  where
+    to :: (Eq a) => [(a, Fin n)] -> Exp a -> Maybe (LC.Exp n)
+    to vs (Var v) = do
+      x <- lookup v vs
+      return $ LC.Var x
+    to vs (Lam v b) = do
+      b' <- to ((v, FZ) : map (fmap FS) vs) b
+      return $ LC.Lam (bind b')
+    to vs (App f a) = do
+      f' <- to vs f
+      a' <- to vs a
+      return $ LC.App f' a'
+
+
+----------------------------------------------
+-- Examples
+----------------------------------------------
+
+-- | Identity function
+idExp :: Exp String
+idExp = Lam "x" (Var "x")
+
+-- | "True"
+trueExp :: Exp String
+trueExp = Lam "x" (Lam "y" (Var "x"))
+
+-- | An ill-scoped term (`y` is never bound)
+illScoped :: Exp String
+illScoped = Lam "x" (Var "y")
+
+-- >>> scopeCheck idExp
+-- Just (λ. 0)
+
+-- >>> scopeCheck trueExp
+-- Just (λ. (λ. 1))
+
+-- >>> scopeCheck illScoped
+-- Nothing
diff --git a/examples/SystemF.hs b/examples/SystemF.hs
new file mode 100644
--- /dev/null
+++ b/examples/SystemF.hs
@@ -0,0 +1,103 @@
+-- | This is an example of the use of the library with two separate variable types
+module SystemF where
+
+{- One issue with this example is that we only store one sort of environment at each binder. 
+   However, terms are subject to two different forms of substitution --- either for terms or types.
+   So, applying the "wrong" sort through a binder means that we don't gain any advantage from 
+   the caching --- we need to bind and unbind the to propagate.
+
+-}
+
+import Prelude hiding (lookup)
+import Rebound
+import Rebound.Bind.Single
+
+
+data Ty (n :: Nat) where
+    TVar :: Fin n -> Ty n
+    TAll :: Bind Ty Ty n -> Ty n
+    TArr :: Ty n -> Ty n -> Ty n
+      deriving (Eq)
+
+-- swap the order of the scopes so that we can talk about 
+-- substituting a type inside of an expression
+newtype TyExp n m = TyExp { unTyExp :: Exp m n }
+
+data Exp (m :: Nat) (n :: Nat) where
+    EVar  :: Fin n -> Exp m n
+    ELam  :: Ty m -> Bind (Exp m) (Exp m) n -> Exp m n 
+    EApp  :: Exp m n -> Exp m n -> Exp m n
+    ETLam :: Bind Ty (TyExp n) m -> Exp m n
+    ETApp :: Exp m n -> Ty m -> Exp m n
+
+instance SubstVar Ty where
+    var = TVar 
+instance Subst Ty Ty where
+    applyE r (TVar x) = applyEnv r x
+    applyE r (TAll b) = TAll (applyE r b)
+    applyE r (TArr t1 t2) = TArr (applyE r t1) (applyE r t2)
+
+instance SubstVar (Exp m) where
+    var = EVar
+
+-- apply type substitution to an expression, using the newtype
+substTy :: Env Ty m1 m2 -> Exp m1 n -> Exp m2 n
+substTy r e = unTyExp (applyE r (TyExp e))
+
+instance Subst Ty (TyExp n) where
+    applyE :: forall m1 m2 n. Env Ty m1 m2 -> TyExp n m1 -> TyExp n m2
+    applyE r (TyExp (EVar x)) = TyExp (EVar x)
+    applyE r (TyExp (ELam ty b)) = 
+        let q = substTy r (getBody b)
+        in TyExp (ELam (applyE r ty) (bind q))
+    applyE r (TyExp (EApp e1 e2)) = TyExp (EApp (substTy r e1) (substTy r e2))
+    applyE r (TyExp (ETLam b)) = 
+        let q = applyE (up r) (getBody b)
+        in TyExp (ETLam (bind q))
+    applyE r (TyExp (ETApp e1 t2)) = 
+        TyExp (ETApp (substTy r e1) (applyE r t2))
+
+-- | shift the type scope in the range of a term substiution
+upTyScope :: Env (Exp m) n1 n2 -> Env (Exp (S m)) n1 n2
+upTyScope = transform (substTy shift1E)
+    
+instance Subst (Exp m) (Exp m) where
+    applyE :: forall m n1 n2. Env (Exp m) n1 n2 -> Exp m n1 -> Exp m n2
+    applyE r (EVar x) = applyEnv r x
+    applyE r (ELam ty b) = ELam ty (applyE r b)
+    applyE r (EApp t1 t2) = EApp (applyE r t1) (applyE r t2)
+    applyE r (ETLam b) =
+        let (TyExp te) = getBody b 
+        in ETLam (bind (TyExp (applyE (upTyScope r) te)))
+    applyE r (ETApp e t) = ETApp (applyE r e) t    
+
+-- System F context
+data FCtx m n where
+    Empty     :: FCtx Z Z
+    ConsTmVar :: Ty m -> FCtx m n -> FCtx m (S n)
+    ConsTyVar :: FCtx m n -> FCtx (S m) n
+
+lookup :: Fin n -> FCtx m n -> Ty m
+lookup FZ (ConsTmVar ty _) = ty
+lookup FZ (ConsTyVar g) = applyE @Ty shift1E $ lookup FZ g
+lookup (FS x) (ConsTmVar _ g) = lookup x g
+lookup (FS x) (ConsTyVar g) = applyE @Ty shift1E $ lookup (FS x) g
+
+tc :: FCtx m n -> Exp m n -> Maybe (Ty m)
+tc g (EVar x) = return $ lookup x g
+tc g (ELam ty b) = tc (ConsTmVar ty g) (getBody b)
+tc g (EApp a b) = do 
+    t1 <- tc g a
+    t2 <- tc g b
+    case t1 of 
+        TArr t11 t12 -> if t1 == t2 then return t12 else Nothing
+        _ -> Nothing
+tc g (ETLam b) = do
+    t1 <- tc (ConsTyVar g) (unTyExp (getBody b))
+    return (TAll (bind t1))
+tc g (ETApp a ty) = do 
+    t1 <- tc g a 
+    case t1 of 
+        TAll tb -> return $ instantiate tb ty
+        _ -> Nothing
+             
diff --git a/rebound.cabal b/rebound.cabal
new file mode 100644
--- /dev/null
+++ b/rebound.cabal
@@ -0,0 +1,122 @@
+cabal-version:  3.0
+name:           rebound
+version:        0.1.0.0
+description:    Please see the README on GitHub at <https://github.com/sweirich/rebound>
+homepage:       https://github.com/sweirich/rebound
+bug-reports:    https://github.com/sweirich/rebound/issues
+author:         Stephanie Weirich, Noe De Santo
+maintainer:     sweirich@seas.upenn.edu, ndesanto@seas.upenn.edu
+copyright:      2025 Stephanie Weirich, Noe De Santo
+license:        MIT
+license-file:   LICENSE
+build-type:     Simple
+extra-doc-files:
+    README.md
+    ChangeLog.md
+category:       Language
+synopsis:       A variable binding library based on well-scoped de Bruijn indices.
+
+common common-stanza
+  ghc-options:
+     -Wno-type-defaults
+     -Wincomplete-patterns
+  default-language:
+    GHC2021
+  default-extensions:
+    KindSignatures
+    , DataKinds
+    , GADTs
+    , StandaloneDeriving
+    , LambdaCase
+    , QuantifiedConstraints
+    , TypeFamilies
+    , AllowAmbiguousTypes
+    , UndecidableInstances
+    , FunctionalDependencies
+    , ViewPatterns
+    , PatternSynonyms
+    , PackageImports
+    , DerivingStrategies
+
+library
+  import:
+      common-stanza
+  build-depends:
+      base >= 4.15 && < 5.0
+    , containers >= 0.6.7 && < 0.7
+    , deepseq >= 1.4.8 && < 1.5
+    , fin >= 0.3 && < 0.4
+    , mtl >= 2.3.1 && < 2.4
+    , QuickCheck >= 2.14.3 && < 2.15
+    , vec >= 0.5 && < 0.6
+  exposed-modules:
+      Rebound
+    , Rebound.Classes
+    , Rebound.Context
+    , Rebound.Env
+    , Rebound.Env.Strict
+    , Rebound.Env.Lazy
+    , Rebound.Env.LazyA
+    , Rebound.Env.LazyB
+    , Rebound.Env.StrictA
+    , Rebound.Env.StrictB
+    , Rebound.Env.Functional
+    , Rebound.Generics
+    , Rebound.Lib
+    , Rebound.MonadNamed
+    , Rebound.MonadScoped
+    , Rebound.Bind.Single
+    , Rebound.Bind.Local
+    , Rebound.Bind.PatN
+    , Rebound.Bind.Pat
+    , Rebound.Bind.Scoped
+    , Rebound.Refinement
+    , Data.SNat
+    , Data.Fin
+    , Data.Vec
+    , Data.LocalName
+    , Data.Scoped.Telescope
+    , Data.Scoped.List
+    , Data.Scoped.Classes
+    , Data.Scoped.Maybe
+  hs-source-dirs: src
+
+test-suite rebound-tests
+  import:
+    common-stanza
+  build-depends:
+      base
+    , rebound
+    , containers
+    , mtl
+    , QuickCheck
+    , tasty
+    , tasty-hunit
+    , tasty-quickcheck
+  type:
+    exitcode-stdio-1.0
+  hs-source-dirs:
+    examples
+    test
+  main-is:
+    All.hs
+  other-modules:
+    LC
+    LCQC
+    LCLet
+    PTS
+    Pat
+    DepMatch
+    ScopeCheck
+    HOAS
+    SystemF
+    PureSystemF
+    LinLC
+    Utils
+    Examples.LC
+    Examples.LCLet
+    Examples.Pat
+    Examples.PureSystemF
+    Examples.PTS
+    Examples.DepMatch
+    Examples.LinLC
diff --git a/src/Data/Fin.hs b/src/Data/Fin.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Fin.hs
@@ -0,0 +1,238 @@
+-- |
+-- Module      : Data.Fin
+-- Description : Bounded natural numbers
+--
+-- This file re-exports definitions from [fin](https://hackage.haskell.org/package/fin)'s
+-- [Data.Fin](https://hackage.haskell.org/package/fin-0.3.2/docs/Data-Fin.html), while adding a few more
+-- that are relevant to this context. Like [Data.Fin](https://hackage.haskell.org/package/fin-0.3.2/docs/Data-Fin.html),
+-- it is meant to be used qualified.
+--
+-- @
+-- import 'Fin' ('Fin' (..))
+-- import qualified 'Fin' as 'Fin'
+-- @
+{-# LANGUAGE PackageImports #-}
+module Data.Fin(
+  Nat(..), SNat(..),
+  Fin(..),
+  toNat, fromNat, toInteger,
+  mirror,
+  absurd,
+  universe,
+  f0,f1,f2,f3,
+  invert,
+  shiftN,
+  shift1,
+  weakenFin,
+  weakenFinRight,
+  weaken1Fin,
+  weaken1FinRight,
+  strengthen1Fin,
+  strengthenRecFin
+ ) where
+
+import Data.Nat
+import Data.SNat
+import "fin" Data.Fin hiding (cata)
+import Data.Proxy (Proxy (..))
+-- for efficient rescoping
+import Unsafe.Coerce (unsafeCoerce)
+
+-------------------------------------------------------------------------------
+-- toInt
+-------------------------------------------------------------------------------
+
+-- | The `toInteger` instance for Fin has an unnecessary
+-- type class constraint (NatI n) for Fin. So we
+-- also include this class for simple conversion.
+instance ToInt (Fin n) where
+  toInt :: Fin n -> Int
+  toInt FZ = 0
+  toInt (FS x) = 1 + toInt x
+
+-- >>> [minBound .. maxBound] :: [Fin N3]
+-- [0,1,2]
+
+-- | List all numbers up to some size
+-- >>> universe :: [Fin N3]
+-- [0,1,2]
+
+-- | Convert an "index" Fin to a "level" Fin and vice versa.
+invert :: forall n. (SNatI n) => Fin n -> Fin n
+invert f = case snat @n of
+  SZ -> case f of {}
+  SS -> maxBound - f
+
+-------------------------------------------------------------------------------
+-- * Shifting
+-------------------------------------------------------------------------------
+
+-- We use the term "Weakening" to mean: Adding a new binding to the front of
+-- the typing context without changing existing indices.
+-- In contrast, "Shifting" means: Adjusting the indices of free variables
+-- within a term to reflect a new binding added to the end of the context.
+--
+-- Shifting functions add some specified amount to the given
+-- `Fin` value, also incrementing its type.
+--
+-- Shifting is implemented in the Data.Fin libary using the `weakenRight`
+-- function, which changes the value of a Fin and its type.
+-- >>> :t weakenRight
+-- weakenRight :: SNatI n => Proxy n -> Fin m -> Fin (Plus n m)
+--
+-- >>> weakenRight (Proxy :: Proxy N1) (f1 :: Fin N2) :: Fin N3
+-- 2
+--
+-- In this module, we call the same operation `shiftN` and give
+-- it a slightly more convenient interface.
+-- >>> shiftN s1 (f1 :: Fin N2)
+-- 2
+--
+-- | Increment by a fixed amount (on the left).
+shiftN :: forall n m. SNat n -> Fin m -> Fin (n + m)
+shiftN p f = withSNat p $ weakenRight (Proxy :: Proxy n) f
+
+-- | Increment by one.
+shift1 :: Fin m -> Fin (S m)
+shift1 = shiftN s1
+
+-- We could also include a dual function, which increments on the right
+-- but we haven't needed that operation anywhere.
+
+-------------------------------------------------------------------------------
+-- * Weakening
+-------------------------------------------------------------------------------
+
+-- | Weaken the bound of a 'Fin' by an arbitrary amount, without
+-- changing its index.
+
+-- | Weakenening changes the bound of a nat-indexed type without changing
+-- its value.
+-- These operations can either be defined for the n-ary case (as in Fin below)
+-- or be defined in terms of a single-step operation.
+-- However, as both of these operations are identity functions,
+-- it is justified to use unsafeCoerce.
+--
+-- The corresponding function in the Data.Fin library is `weakenLeft`.
+--
+-- @
+-- -- >>> :t weakenLeft
+-- weakenLeft :: SNatI n => Proxy m -> Fin n -> Fin (Plus n m)
+-- @
+--
+-- This function does not change the value, it only changes its type.
+--
+-- @
+-- -- >>> weakenLeft (Proxy :: Proxy N1) (f1 :: Fin N2) :: Fin N3
+-- 1
+-- @
+--
+-- We could use the following definition:
+--
+-- @
+-- weakenFin m f = withSNat m $ weakenLeft (Proxy :: Proxy m) f
+-- @
+--
+-- But, by using an 'unsafeCoerce' implementation, we can avoid the
+-- @'SNatI' n@ constraint in the type of this operation.
+--
+-- @
+-- -- >>> weakenFin (Proxy :: Proxy N1) (f1 :: Fin N2) :: Fin N3
+-- 1
+-- @
+weakenFin :: proxy m -> Fin n -> Fin (m + n)
+weakenFin _ f = unsafeCoerce f
+
+-- | Weaken the bound of a 'Fin' by 1.
+weaken1Fin :: Fin n -> Fin (S n)
+weaken1Fin = weakenFin s1
+
+-- | Weaken the bound of of a 'Fin' by an arbitrary amount on the right.
+-- This is also an identity function
+-- >>> weakenFinRight (s1 :: SNat N1) (f1 :: Fin N2) :: Fin N3
+-- 1
+weakenFinRight :: proxy m -> Fin n -> Fin (n + m)
+weakenFinRight m f = unsafeCoerce f
+
+-- | Weaken the bound of a 'Fin' by 1.
+weaken1FinRight :: Fin n -> Fin (n + N1)
+weaken1FinRight = weakenFinRight s1
+
+-------------------------------------------------------------------------------
+-- * Aliases
+-------------------------------------------------------------------------------
+
+-- Convenient names for fin values. These have polymorphic types so they
+-- will work in any scope. (These are also called fin0, fin1, fin2, etc
+-- in Data.Fin)
+
+-- | 0.
+f0 :: Fin (S n)
+f0 = FZ
+
+-- | 1.
+f1 :: Fin (S (S n))
+f1 = FS f0
+
+-- | 2.
+f2 :: Fin (S (S (S n)))
+f2 = FS f1
+
+-- | 3.
+f3 :: Fin (S (S (S (S n))))
+f3 = FS f2
+
+-- >>> f2
+-- 2
+
+-------------------------------------------------------------------------------
+-- * Strengthening
+-------------------------------------------------------------------------------
+
+-- | With strengthening, we make sure that variable f0 is not used,
+-- and we decrement all other indices by 1. This allows us to
+-- also decrement the scope by one.
+--- >>> strengthen1Fin (f0 :: Fin (S N3)) :: Maybe (Fin N3)
+-- Nothing
+-- >>> strengthen1Fin (f1 :: Fin (S N3)) :: Maybe (Fin N3)
+-- Just 0
+-- >>> strengthen1Fin (f2 :: Fin (S N3)) :: Maybe (Fin N3)
+-- Just 1
+strengthen1Fin :: forall n. SNatI n => Fin (S n) -> Maybe (Fin n)
+strengthen1Fin = strengthenRecFin s0 s1 undefined
+
+-- | We implement strengthening with the following operation that
+-- generalizes the induction hypothesis, so that we can strengthen
+-- in the middle of the scope. The scope of the Fin should have the form
+-- @k + (m + n)@
+--
+-- Indices in the middle part of the scope @m@ are "strengthened" away.
+--
+--- >>> strengthenRecFin s1 s1 s2 (f1 :: Fin (N1 + N1 + N2)) :: Maybe (Fin (N1 + N2))
+-- Nothing
+--
+-- Variables that are in the first part of the scope @k@ (the ones that have
+-- most recently entered the context) do not change when strengthening.
+--
+--- >>> strengthenRecFin s1 s1 s2 (f0 :: Fin (N1 + N1 + N2))
+-- Just 0
+--
+-- Variables in the last part of the scope @n@ are decremented by strengthening
+--
+-- >>> strengthenRecFin s1 s1 s2 (f2 :: Fin (N1 + N1 + N2)) :: Maybe (Fin N3)
+-- Just 1
+--
+-- >>> strengthenRecFin s1 s1 s2 (f3 :: Fin (N1 + N1 + N2)) :: Maybe (Fin N3)
+-- Just 2
+--
+strengthenRecFin ::
+   SNat k -> SNat m -> proxy n -> Fin (k + (m + n)) -> Maybe (Fin (k + n))
+strengthenRecFin SZ SZ n x = Just x  -- Base case: k = 0, m = 0
+strengthenRecFin SZ (snat_ -> SS_ m) n FZ = Nothing
+  -- Case: k = 0, m > 0, and x is in the `m` range
+strengthenRecFin SZ (snat_ -> SS_ m) n (FS x) =
+    strengthenRecFin SZ m n x
+strengthenRecFin (snat_ -> SS_ k) m n FZ = Just FZ
+  -- Case: x < k, leave it alone
+strengthenRecFin (snat_ -> SS_ k) m n (FS x) =
+    FS <$> strengthenRecFin k m n x
diff --git a/src/Data/LocalName.hs b/src/Data/LocalName.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/LocalName.hs
@@ -0,0 +1,19 @@
+-- |
+-- Module      : Data.LocalName
+-- Description : Strings with an "identity" equality
+module Data.LocalName where
+
+-- | A simple wrapper for strings
+-- All local names are equal so that when they are used as patterns
+-- they will be ignored.
+newtype LocalName = LocalName {name :: String}
+
+instance Eq LocalName where
+  x1 == x2 = True
+
+instance Show LocalName where
+  show (LocalName x) = x
+
+-- | A default name.
+internalName :: LocalName
+internalName = LocalName "_internal"
diff --git a/src/Data/SNat.hs b/src/Data/SNat.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/SNat.hs
@@ -0,0 +1,133 @@
+-- |
+-- Module      : Data.SNat
+-- Description : Singleton naturals
+--
+-- Runtime data that connects to type-level nats.
+module Data.SNat(
+  Nat(..), toNatural, fromNatural,
+  SNat(..),  snatToNat,
+  SNatI(..), snat, withSNat, reify, reflect,
+  type (+),
+  N0, N1, N2, N3,
+  s0, s1, s2, s3,
+  sPlus,
+  axiomPlusZ,
+  axiomAssoc,
+  SNat_(..), snat_,
+  prev,
+  next,
+  ToInt(..),
+ ) where
+
+-- Singleton nats are purely runtime
+
+import Data.Type.Equality
+import Data.Type.Nat
+import Test.QuickCheck
+import Unsafe.Coerce (unsafeCoerce)
+import Prelude hiding (pred, succ)
+
+-----------------------------------------------------
+-- axioms (use unsafeCoerce)
+-----------------------------------------------------
+
+-- | '0' is identity element for @+@
+axiomPlusZ :: forall m. m + Z :~: m
+axiomPlusZ = unsafeCoerce Refl
+
+-- | @+@ is associative.
+axiomAssoc :: forall p m n. p + (m + n) :~: (p + m) + n
+axiomAssoc = unsafeCoerce Refl
+
+-----------------------------------------------------
+-- Nats (singleton nats and implicit singletons)
+-----------------------------------------------------
+
+-- | 0.
+type N0 = Z
+
+-- | 1.
+type N1 = S N0
+
+-- | 2.
+type N2 = S N1
+
+-- | 3.
+type N3 = S N2
+
+---------------------------------------------------------
+-- Singletons and instances
+---------------------------------------------------------
+
+-- | 0.
+s0 :: SNat N0
+s0 = snat
+
+-- | 1.
+s1 :: SNat N1
+s1 = snat
+
+-- | 2.
+s2 :: SNat N2
+s2 = snat
+
+-- | 3.
+s3 :: SNat N3
+s3 = snat
+
+instance (SNatI n) => Arbitrary (SNat n) where
+  arbitrary :: (SNatI n) => Gen (SNat n)
+  arbitrary = pure snat
+
+-- | Conversion to 'Int'.
+class ToInt a where
+  toInt :: a -> Int
+
+instance ToInt (SNat n) where
+  toInt :: SNat n -> Int
+  toInt = fromInteger . toInteger . snatToNat
+
+---------------------------------------------------------
+-- Addition
+---------------------------------------------------------
+
+-- | Notation for the addition of naturals.
+type family (n :: Nat) + (m :: Nat) :: Nat where
+  m + n = Plus m n
+
+-- | Addition of singleton naturals.
+sPlus :: forall n1 n2. SNat n1 -> SNat n2 -> SNat (n1 + n2)
+sPlus SZ n = n
+sPlus x@SS y = withSNat (sPlus (prev x) y) SS
+
+-- >>> reflect $ sPlus s3 s1
+-- 4
+
+---------------------------------------------------------
+-- View pattern access to the predecessor
+---------------------------------------------------------
+
+-- | View pattern allowing pattern matching on naturals.
+-- See 'snat_'.
+data SNat_ n where
+  SZ_ :: SNat_ Z
+  SS_ :: SNat n -> SNat_ (S n)
+
+-- | View pattern allowing pattern matching on naturals.
+--
+-- @
+-- f :: forall p. SNat p -> ...
+-- f SZ = ...
+-- f (snat_ -> SS_ m) = ...
+-- @
+snat_ :: SNat n -> SNat_ n
+snat_ SZ = SZ_
+snat_ SS = SS_ snat
+
+-- | Predecessor of a natural.
+prev :: SNat (S n) -> SNat n
+prev SS = snat
+
+-- | Successor of a natural.
+next :: SNat n -> SNat (S n)
+next x = withSNat x SS
diff --git a/src/Data/Scoped/Classes.hs b/src/Data/Scoped/Classes.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Scoped/Classes.hs
@@ -0,0 +1,131 @@
+-- |
+-- Module      : Data.Scoped.Classes
+-- Description : Structures for scoped types
+-- 
+-- These classes provide access to scoped versions of higher-kinded classes
+-- such as 'Functor'/'Foldable' etc.
+-- All instances of this class should be coercible to existing instances of 
+-- these classes. (Which are used in the default definitions.)
+
+module Data.Scoped.Classes(
+    type (~>)(..),
+    ScopedFunctor(..),
+    ScopedFoldable(..),
+    ScopedTraversable(..),
+    ScopedApplicative(..),
+    ScopedMonad(..)
+) where
+
+import Data.Coerce
+import Control.Monad as M
+import Data.Kind (Type)
+import GHC.Generics
+import Test.QuickCheck
+import Control.DeepSeq
+
+import Data.Foldable qualified as F
+import Data.Traversable qualified as T
+
+-- | A scoped function (i.e., a function whose input & output are scoped).
+newtype (~>) a b n = MkArr (a n -> b n) 
+    deriving newtype (Semigroup, Monoid, Arbitrary, CoArbitrary, Testable, NFData)
+    deriving stock (Generic)
+
+-- | Scoped 'Functor'.
+class (forall a n. Coercible (f a n) (k (a n)), Functor k) => ScopedFunctor k f | f -> k where
+    fmap :: Functor k => (a n -> b n) -> f a n -> f b n
+    fmap f = coerce (M.fmap @k f)
+
+-- | Scoped 'Foldable'.
+class (forall a n. Coercible (f a n) (k (a n)), Foldable k) => ScopedFoldable k f | f -> k where
+    fold :: (Monoid (a n)) => f a n -> a n
+    fold x = F.fold @k (coerce x)
+
+    foldMap :: (Monoid m) => (a n -> m) -> f a n -> m
+    foldMap f x = F.foldMap @k f (coerce x)
+
+    foldMap' :: (Monoid m) => (a n -> m) -> f a n -> m
+    foldMap' f x = F.foldMap' @k f (coerce x)
+
+    foldr :: (a n -> b -> b) -> b -> f a n -> b
+    foldr f b x = F.foldr @k f b (coerce x)
+
+    foldr' :: (a n -> b -> b) -> b -> f a n -> b
+    foldr' f b x = F.foldr' @k f b (coerce x)
+
+    foldl ::  (b -> a n -> b) -> b -> f a n -> b
+    foldl f b x = F.foldl @k f b (coerce x)
+
+    foldl' ::  (b -> a n -> b) -> b -> f a n -> b
+    foldl' f b x = F.foldl @k f b (coerce x)
+
+    foldr1 ::  (a n -> a n -> a n) -> f a n -> a n
+    foldr1 f x = F.foldr1 @k f (coerce x)
+
+    foldl1 :: (a n -> a n -> a n) -> f a n -> a n
+    foldl1 f x = F.foldl1 @k f (coerce x)
+
+    null :: forall a n. f a n -> Bool
+    null x = F.null (coerce x :: k (a n))
+
+    length :: forall a n. f a n -> Int
+    length x = F.length @k (coerce x :: k (a n))
+
+    elem :: (Eq (a n)) => a n -> f a n -> Bool 
+    elem a x = F.elem @k a (coerce x)
+
+    maximum :: (Ord (a n)) => f a n -> a n
+    maximum x = F.maximum @k (coerce x)
+
+    minimum :: (Ord (a n)) => f a n -> a n
+    minimum x = F.maximum @k (coerce x)
+
+    sum :: (Num (a n)) => f a n -> a n
+    sum x = F.sum @k (coerce x)
+
+    product :: (Num (a n)) => f a n -> a n
+    product x = F.product @k (coerce x)
+
+    any :: (a n -> Bool) -> f a n -> Bool
+    any f x = F.any @k f (coerce x)
+
+    all :: (a n -> Bool) -> f a n -> Bool
+    all f x = F.all @k f (coerce x)
+
+    mapM_ :: (Monad m) => (a n -> m b) -> f a n -> m ()
+    mapM_ f x = M.mapM_ @k f (coerce x)
+
+-- | Scoped 'Applicative'.
+class (forall a n. Coercible (t a n) (k (a n)), Applicative k) => ScopedApplicative k t | t -> k where
+    pure  :: a n -> t a n
+    pure x = coerce (Prelude.pure @k x) 
+
+    (<*>) :: forall a b n. t (a ~> b) n -> t a n -> t b n  
+    f <*> x = coerce (fk Prelude.<*> coerce x) where
+        fk :: k (a n -> b n)
+        fk = coerce <$> (coerce f :: k ((a ~> b) n))
+
+-- | Scoped 'Monad'.
+class (forall a n. Coercible (t a n) (k (a n)), Monad k, ScopedApplicative k t) => 
+       ScopedMonad k t | t -> k where
+    return :: a n -> t a n
+    return = Data.Scoped.Classes.pure
+
+    (>>=) :: forall a n b m. t a n -> (a n -> t b m) -> t b m
+    ma >>= kb = coerce r where
+        r :: k (b m)
+        r = (M.>>=) (coerce ma :: k (a n)) (coerce kb)
+
+-- The default definitions do not have 0-cost coercions due to role limitations
+-- | Scoped 'Traversable'.
+class (forall a n. Coercible (t a n) (k (a n)), Traversable k) => ScopedTraversable k t | t -> k where
+   traverse :: forall a b n f. Applicative f => (a n -> f (b n)) -> t a n -> f (t b n)
+   traverse f x = coerce <$> T.traverse @k f (coerce x)
+   
+   mapM :: Monad m => (a n -> m (b n)) -> t a n -> m (t b n)
+   mapM f x = coerce <$> T.mapM @k f (coerce x)
+   
+   -- TODO ???
+   -- sequenceA :: Applicative f => t (f n) -> f (t a)
+   -- sequence :: Monad m => t (m a) -> m (t a)
+
diff --git a/src/Data/Scoped/List.hs b/src/Data/Scoped/List.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Scoped/List.hs
@@ -0,0 +1,108 @@
+-- |
+-- Module: Data.Scoped.List
+-- Description : Scoped lists
+--
+-- This module defines a type of lists indexed by a scope
+-- The lists are homogenous, and every type in the list must be indexed
+-- by the same scope.
+-- This module is intended to be imported qualified and used with the
+-- OverloadedLists Haskell language extension. Many of the operations
+-- in this module have the same name as prelude functions.
+{-# LANGUAGE DerivingStrategies, DeriveAnyClass #-}
+module Data.Scoped.List (List,
+      pattern Nil, pattern (:<),
+      Data.Scoped.List.uncons,
+      (Data.Scoped.List.++),
+      Data.Scoped.List.concat,
+      Data.Scoped.List.filter,
+      Data.Scoped.List.zipWith,
+      Data.Scoped.List.zipWithM_,
+      IsList(..),
+      module Data.Scoped.Classes) where
+
+import Data.Nat ( Nat )
+import Data.Kind ( Type )
+import GHC.IsList ( IsList(..) )
+import GHC.Generics
+import Test.QuickCheck ( Arbitrary )
+import Control.DeepSeq ( NFData )
+import Data.Coerce ( coerce )
+import Control.Monad qualified as M
+import Data.Scoped.Classes
+
+-- | Lists where every element has type (a n)
+-- Note: the @n@ is *not* the length of the list, it is a common scope
+-- for all elements in the list.
+newtype List a n = MkList [a n]
+    deriving newtype (Eq, Ord, Read, Show, Semigroup, Monoid, Generic, Arbitrary, NFData)
+    deriving anyclass (ScopedFoldable [], ScopedTraversable [],
+                        ScopedFunctor [], ScopedApplicative [], ScopedMonad [])
+
+-- | Separate the head of the list from its tail, if applicable.
+uncons :: List a n -> Maybe (a n, List a n)
+uncons x = case coerce x of
+      [] -> Nothing
+      x:xs -> Just (x,coerce xs)
+
+{-# COMPLETE (:<), Nil #-}
+-- | Pattern for the empty list.
+pattern Nil :: forall a n. List a n
+pattern Nil <- (uncons -> Nothing)
+ where
+   Nil = coerce ([] :: [a n])
+
+-- | Pattern for a cons-ed list.
+pattern (:<) :: a n -> List a n -> List a n
+pattern x :< xs <- (uncons -> Just (x,xs))
+  where
+    x :< xs = coerce (x : coerce xs)
+
+-- * Prelude / Control.Monad list operations
+
+-- | See 'Prelude.++'.
+(++) :: forall t n. List t n -> List t n -> List t n
+(++) = coerce ((Prelude.++):: [t n] -> [t n] -> [t n])
+
+-- | Lists flattening / Monadic join.
+concat :: List (List t) n -> List t n
+concat = Data.Scoped.Classes.foldr (Data.Scoped.List.++) Nil
+
+-- | See 'Prelude.filter'.
+filter :: (a n -> Bool) -> List a n -> List a n
+filter f = coerce (Prelude.filter f)
+
+-- | See 'Prelude.zipWith'.
+zipWith :: (a n -> b n -> c n) -> List a n -> List b n -> List c n
+zipWith f = coerce (Prelude.zipWith f)
+
+-- | See 'Prelude.zipWithM_'.
+zipWithM_ :: forall m k f1 f2 a b c n. (Applicative m)
+    => (a n -> b n -> m c) -> List a n -> List b n -> m ()
+zipWithM_ f = coerce (M.zipWithM_ f)
+
+-- | A general conversion to the standard list type.
+instance IsList (List v n) where
+   type Item (List v n) = v n
+
+   fromList :: [Item (List v n)] -> List v n
+   fromList = coerce
+
+   toList :: List v n -> [Item (List v n)]
+   toList = coerce
+
+-- | Enable generic programming for the `List` type
+-- We can't derive the `Data.Generic1` instance for `List`
+-- using newtype deriving because the kinds differ.
+-- Therefore we need to write it by hand.
+instance Generic1 (List a :: Nat -> Type) where
+   type Rep1 (List a) =
+      U1 :+: (Rec1 a :*: Rec1 (List a))
+
+   from1 :: List a n -> Rep1 (List a) n
+   from1 (MkList []) = L1 U1
+   from1 (MkList (x:xs)) = R1 (Rec1 x :*: Rec1 (MkList xs))
+
+   to1 :: Rep1 (List a) n -> List a n
+   to1 (L1 U1) = MkList []
+   to1 (R1 (Rec1 x :*: Rec1 (MkList xs))) = MkList (x : xs)
+
diff --git a/src/Data/Scoped/Maybe.hs b/src/Data/Scoped/Maybe.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Scoped/Maybe.hs
@@ -0,0 +1,76 @@
+-- |
+-- Module: Data.Scoped.Maybe
+-- Description : Scoped maybe
+--
+-- This module defines a Maybe type indexed by a scope
+-- This module should be imported qualified. Many of the operations
+-- in this module have the same name as prelude functions.
+{-# LANGUAGE DeriveAnyClass #-}
+module Data.Scoped.Maybe where
+
+import Data.Nat ( Nat )
+import Data.Kind ( Type )
+import GHC.Generics
+import GHC.Stack.Types (HasCallStack)
+import Data.Maybe qualified as M
+import Prelude hiding (Maybe(..), maybe)
+
+import Data.Coerce ( coerce )
+import Test.QuickCheck (Arbitrary)
+import Control.DeepSeq (NFData)
+import Data.Scoped.Classes
+
+-- | 'M.Maybe' whose (hypothetical) content is scoped.
+newtype Maybe a n = MkMaybe (M.Maybe (a n))
+    deriving newtype (Eq, Ord, Show, Semigroup, Monoid, Generic, Arbitrary, NFData)
+    deriving anyclass (ScopedFoldable M.Maybe, ScopedTraversable M.Maybe,
+        ScopedFunctor M.Maybe, ScopedApplicative M.Maybe, ScopedMonad M.Maybe)
+
+{-# COMPLETE Nothing, Just #-}
+-- | Pattern for 'M.Nothing'.
+pattern Nothing :: Maybe a n
+pattern Nothing = MkMaybe M.Nothing
+
+-- | Pattern for 'M.Just'.
+pattern Just :: a n -> Maybe a n
+pattern Just a = MkMaybe (M.Just a)
+
+-- | See 'M.maybe'.
+maybe :: b -> (a n -> b) -> Maybe a n -> b
+maybe b f = coerce (M.maybe b f)
+
+-- | See 'M.isJust'.
+isJust :: forall a n. Maybe a n -> Bool
+isJust = coerce (M.isJust :: M.Maybe (a n) -> Bool)
+
+-- | See 'M.isNothing'.
+isNothing :: forall a n. Maybe a n -> Bool
+isNothing = coerce (M.isNothing :: M.Maybe (a n) -> Bool)
+
+-- | See 'M.fromJust'.
+fromJust :: forall a n. HasCallStack => Maybe a n -> a n
+fromJust = coerce (M.fromJust :: M.Maybe (a n) -> a n)
+
+-- | See 'M.fromMaybe'.
+fromMaybe :: forall a n. a n -> Maybe a n -> a n
+fromMaybe = coerce (M.fromMaybe :: a n -> M.Maybe (a n) -> a n)
+
+-- | See 'M.maybeToList'.
+maybeToList  :: forall a n. Maybe a n -> [a n]
+maybeToList = coerce (M.maybeToList :: M.Maybe (a n) -> [a n])
+
+-- | See 'M.listToMaybe'.
+listToMaybe :: forall a n. [a n] -> Maybe a n
+listToMaybe = coerce (M.listToMaybe :: [a n] -> M.Maybe (a n))
+
+instance Generic1 (Maybe a :: Nat -> Type) where
+   type Rep1 (Maybe a) =
+      U1 :+: Rec1 a
+
+   from1 :: Maybe a n -> Rep1 (Maybe a) n
+   from1 Nothing = L1 U1
+   from1 (Just x) = R1 (Rec1 x)
+
+   to1 :: Rep1 (Maybe a) n -> Maybe a n
+   to1 (L1 U1) = Nothing
+   to1 (R1 (Rec1 x)) = Just x
diff --git a/src/Data/Scoped/Telescope.hs b/src/Data/Scoped/Telescope.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Scoped/Telescope.hs
@@ -0,0 +1,72 @@
+-- |
+-- Stability: experimental
+{-# OPTIONS_HADDOCK hide #-}
+module Data.Scoped.Telescope {-# WARNING "This module is experimental" #-} where
+
+import Rebound.Classes
+import Rebound.Env (Shiftable (..))
+import Data.Fin (Fin)
+import Data.Nat
+import Data.Type.Equality ((:~:) (..))
+import Data.Type.Nat
+import Data.Vec.Lazy qualified as Vec
+import Rebound.Lib (axiomAssoc, axiomPlusZ)
+import Data.SNat
+
+-- | Unlike 'Scoped.TeleList', this datatype does not nest: it is effectively a
+-- 'List.List'/'Data.Vec.Vec' but with extra scoping inside.
+data Telescope u s n m where
+  TNil :: Telescope u s Z m
+  TCons :: (u, s (n + m)) -> !(Telescope u s n m) -> Telescope u s (S n) m
+
+tmap :: (forall k. u -> s k -> (u', s' k)) -> Telescope u s n m -> Telescope u' s' n m
+tmap f TNil = TNil
+tmap f (TCons (u, s) xs) = TCons (f u s) (tmap f xs)
+
+empty :: Telescope u s Z m
+empty = TNil
+
+singleton :: (u, s n) -> Telescope u s (S Z) n
+singleton h = TCons h TNil
+
+append :: forall u s nl nr m. Telescope u s nl (nr + m) -> Telescope u s nr m -> (SNat nl, Telescope u s (nl + nr) m)
+append TNil r = (SZ, r)
+append (TCons l ls) r =
+  case axiomAssoc @nl @nr @m of
+    Refl -> let (k, ls') = append ls r in withSNat k (SS, TCons l ls')
+
+toTelescope :: forall p n u s. (Shiftable s) => Vec.Vec p (u, s n) -> Telescope u s p n
+toTelescope = snd . iter
+  where
+    iter :: forall p. Vec.Vec p (u, s n) -> (SNat p, Telescope u s p n)
+    iter Vec.VNil = (SZ, TNil)
+    iter ((Vec.:::) @_ @p' (u, s) xs) =
+      let (sp', sc') :: (SNat p', Telescope u s p' n) = iter xs
+          s' :: s (p' + n) = shift sp' s
+       in (withSNat sp' SS, TCons (u, s') sc')
+
+-- fromTelescope :: forall s u p n. (Shiftable s) => Telescope u s p n -> (SNat p, Vec.Vec p (u, s (p + n)))
+-- fromTelescope = iter SZ
+--   where
+--     iter :: forall u s k n m. (Shiftable s) => SNat k -> Telescope u s n m -> (SNat (k + n), Vec.Vec n (u, s (k + n + m)))
+--     iter sk TNil = case axiomPlusZ @k of Refl -> (sk, Vec.empty)
+--     iter sk (TCons @_ @_ @n' (u, s) sc) =
+--       case axiomSus @k @n' of
+--         Refl ->
+--           let x' :: (u, s (k + n + m)) = case axiomAssoc @k @n' @m of Refl -> (u, shift (addOne sk) s)
+--               (sn', sc') :: (SNat (k + n), Vec.Vec n' (u, s (k + n + m))) = iter (addOne sk) sc
+--            in (sn', x' Vec.::: sc')
+
+--     addOne :: SNat k -> SNat (S k)
+--     addOne k = withSNat k SS
+
+emptyTelescope = TNil
+
+-- nth :: forall s n m u. (Shiftable s) => Fin n -> Telescope u s n m -> (u, s (n + m))
+-- nth i s = snd (fromTelescope s) Vec.! i
+
+instance Sized (Telescope u s n m) where
+  type Size (Telescope u s n m) = n
+  size :: Telescope u s n m -> SNat n
+  size TNil = s0
+  size (TCons _ t) = withSNat (size t) SS
diff --git a/src/Data/Vec.hs b/src/Data/Vec.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Vec.hs
@@ -0,0 +1,63 @@
+-- |
+-- Module      : Data.Vec
+-- Description : Vectors, or length-indexed lists
+--
+-- This file re-exports definitions from [vec](https://hackage.haskell.org/package/vec)'s
+-- [Data.Vec.Lazy](https://hackage.haskell.org/package/vec-0.5.1/docs/Data-Vec-Lazy.html).
+--
+-- @
+-- import 'Vec' ('Vec' (..))
+-- import qualified 'Vec' as 'Vec'
+-- @
+module Data.Vec
+  ( module Data.Vec.Lazy,
+    vlength,
+    append,
+    setAt,
+    all2
+ ) where
+
+-- based on
+-- https://hackage.haskell.org/package/fin
+
+
+import Data.Fin (Fin (..))
+import Data.Fin qualified
+import Data.Nat
+import Data.SNat
+import Data.Type.Equality
+import Data.Vec.Lazy
+import Test.QuickCheck
+import Prelude hiding (lookup, repeat, zipWith)
+
+-----------------------------------------------------
+-- Vectors (additional definitions)
+-----------------------------------------------------
+
+-- | Update a vector value at a given index
+setAt :: Fin n -> Vec n a -> a -> Vec n a
+setAt FZ (_ ::: vs) w = w ::: vs
+setAt (FS x) (w1 ::: env) w2 = w1 ::: setAt x env w2
+
+-- | Concatenate two vectors
+append :: forall n m a. Vec n a -> Vec m a -> Vec (n + m) a
+append = (Data.Vec.Lazy.++)
+
+-- | Access elements by position
+lookup :: Fin n -> Vec n a -> a
+lookup = flip (!)
+
+-- | Calculate length as a singleton value
+vlength :: Vec n a -> SNat n
+vlength VNil = SZ
+vlength (_ ::: v) = withSNat (vlength v) SS
+
+
+-- >>> all (\x -> x > 3) (4 ::: 5 ::: VNil)
+-- True
+
+-- | Ensure that a binary predicate holds for
+-- corresponding elements in two vectors
+all2 :: (a -> b -> Bool) -> Vec n a -> Vec n b -> Bool
+all2 f (x ::: xs) (y ::: ys) = f x y && all2 f xs ys
+all2 f VNil VNil = True
diff --git a/src/Rebound.hs b/src/Rebound.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound.hs
@@ -0,0 +1,29 @@
+-- |
+-- Module      : Rebound
+-- Description : Efficient, Expressive, and Well-Scoped Binding
+--
+-- This top level module re-exports the core of the library.
+-- It should be used in conjunction with one (or more) module
+-- in "Rebound.Bind".
+module Rebound
+  (module Rebound.Classes,
+   module Rebound.Env,
+   module Rebound.Refinement,
+   module Rebound.Generics,
+   module Rebound.Lib,
+   module Rebound.Context,
+   module Data.LocalName,
+   Generic(..),
+   Generic1(..))
+where
+
+import Rebound.Classes
+import Rebound.Context
+import Rebound.Env
+import Rebound.Refinement
+import Rebound.Generics
+import Rebound.Lib
+import Data.LocalName
+import GHC.Generics(Generic(..),Generic1(..))
+
+
diff --git a/src/Rebound/Bind/Local.hs b/src/Rebound/Bind/Local.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Bind/Local.hs
@@ -0,0 +1,100 @@
+-- |
+-- Module       : Rebound.Bind.Single
+-- Description  : Bind a single variable, with a name
+--
+-- Single variable binder, but includes a name (represented by a 'LocalName') for pretty printing.
+-- This is a specialization of "Rebound.Bind.Pat".
+module Rebound.Bind.Local
+  ( module Rebound,
+    type Bind,
+    bind,
+    getLocalName,
+    internalBind,
+    getBody,
+    unbind,
+    unbindl,
+    instantiate,
+    applyUnder,
+    bindWith,
+    unbindWith,
+    instantiateWith
+  )
+where
+
+import Rebound
+import Rebound.Bind.Pat qualified as Pat
+import Data.Fin qualified as Fin
+
+-- | Type binding a single variable.
+-- This data structure includes a delayed
+-- substitution for the variables in the body of the binder.
+type Bind v c n = Pat.Bind v c LocalName n
+
+-- | Bind a variable, using the identity substitution.
+bind :: (Subst v c) => LocalName -> c (S n) -> Bind v c n
+bind = Pat.bind
+
+-- | Bind a variable, while suspending the provided substitution.
+bindWith :: forall v c m n. LocalName -> Env v m n -> c (S m) -> Bind v c n
+bindWith = Pat.bindWith
+
+-- | Bind the default \"internal\" variable, while suspending the provided substitution.
+internalBind :: (Subst v c) => c (S n) -> Bind v c n
+internalBind = Pat.bind internalName
+
+-- | Retrieve the name of the bound variable.
+getLocalName :: Bind v c n -> LocalName
+getLocalName = Pat.getPat
+
+-- | Retrieve the body of the binding.
+getBody :: (Subst v c) => Bind v c n -> c (S n)
+getBody = Pat.getBody
+
+-- | Run a function on the body (and bound name), after applying the delayed substitution.
+unbind :: (Subst v c) => Bind v c n -> ((LocalName, c (S n)) -> d) -> d
+unbind b f = f (getLocalName b, getBody b)
+
+-- | Retrieve the body, as well as the bound name.
+unbindl :: (Subst v c) => Bind v c n -> (LocalName, c (S n))
+unbindl b = (getLocalName b, getBody b)
+
+-- | Instantiate the body (i.e. replace the bound variable) with the provided term.
+instantiate :: (Subst v c) => Bind v c n -> v n -> c n
+instantiate b e = Pat.instantiate b (oneE e)
+
+-- | Apply a function under the binder.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+applyUnder ::
+  (Subst v c) =>
+  (forall m. Env v m (S n2) -> c m -> c (S n2)) ->
+  Env v n1 n2 ->
+  Bind v c n1 ->
+  Bind v c n2
+applyUnder = Pat.applyUnder
+
+-- | Run a function on the body.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+unbindWith :: (SubstVar v) => Bind v c n -> (forall m. LocalName -> Env v m n -> c (S m) -> d) -> d
+unbindWith = Pat.unbindWith
+
+-- | Instantiate the body (i.e. replace the bound variable) with the provided term.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+instantiateWith :: (SubstVar v) => Bind v c n -> v n -> (forall m. Env v m n -> c m -> c n) -> c n
+instantiateWith b v = Pat.instantiateWith b (oneE v)
+
+
+-- Example
+
+data Exp n = Var (Fin n) | App (Exp n) (Exp n) | Lam (Bind Exp Exp n)
+  deriving (Eq,Generic1)
+instance SubstVar Exp where var = Var
+instance Subst Exp Exp where
+  isVar (Var x) = Just (Refl,x)
+  isVar _ = Nothing
+t1 :: Exp Z
+t1 = Lam (bind (LocalName "x") (Var Fin.f0))
+t2 :: Exp Z
+t2 = Lam (bind (LocalName "y") (Var Fin.f0))
+
+-- >>> t1 == t2
+-- True
diff --git a/src/Rebound/Bind/Pat.hs b/src/Rebound/Bind/Pat.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Bind/Pat.hs
@@ -0,0 +1,246 @@
+-- |
+-- Module       : Rebound.Bind.Pat
+-- Description  : Bind variables according to a pattern
+--
+-- Bind variables according to a user-defined pattern.
+module Rebound.Bind.Pat
+  ( module Rebound,
+    type Bind,
+    bind,
+    unbind,
+    unbindl,
+    getPat,
+    getBody,
+    instantiate,
+    bindWith,
+    unbindWith,
+    instantiateWith,
+    applyUnder,
+    type Rebind (..),
+    type PatList (..),
+    lengthPL,
+  )
+where
+
+import Rebound
+
+import qualified Data.Fin as Fin
+import qualified Data.Vec as Vec
+import Data.Set (Set)
+import qualified Data.Set as Set
+
+----------------------------------------------------------
+-- * Bind type
+----------------------------------------------------------
+
+-- | Type binding 'Size pat' variables.
+-- This data structure includes a delayed
+-- substitution for the variables in the body of the binder.
+data Bind v c (pat :: Type) (n :: Nat) where
+  Bind :: pat -> Env v m n -> c (Size pat + m) -> Bind v c pat n
+
+-- | To compare pattern binders, we need to unbind, but also
+-- first make sure that the patterns are equal.
+instance (Eq pat, Sized pat, forall n. Eq (c n), Subst v c) => Eq (Bind v c pat n) where
+  b1 == b2 =
+    getPat b1 == getPat b2
+      && getBody b1 == getBody b2
+
+-- | Bind a pattern, using the identity substitution.
+bind ::
+  (Sized pat, Subst v c) =>
+  pat ->
+  c (Size pat + n) ->
+  Bind v c pat n
+bind pat = Bind pat idE
+
+-- | Bind a pattern, while suspending the provided substitution.
+bindWith :: pat -> Env v m n -> c (Size pat + m) -> Bind v c pat n
+bindWith = Bind
+
+-- | Retrieve the pattern of the binding.
+getPat :: Bind v c pat n -> pat
+getPat (Bind pat env t) = pat
+
+-- | Retrieve the body of the binding.
+getBody ::
+  forall v c pat n.
+  (Sized pat, Subst v c) =>
+  Bind v c pat n ->
+  c (Size pat + n)
+getBody (Bind (pat :: pat) (env :: Env v m n) t) =
+  applyOpt applyE (upN (size pat) env) t
+
+-- | Run a function on the body (and pattern), after applying the delayed substitution.
+-- The size of the (current) scope is made available at runtime.
+unbind ::
+  forall v c pat n d.
+  (SNatI n, Sized pat, Subst v v, Subst v c) =>
+  Bind v c pat n ->
+  ((SNatI (Size pat + n)) => pat -> c (Size pat + n) -> d) ->
+  d
+unbind bnd f =
+  withSNat (sPlus (size (getPat bnd)) (snat @n)) $
+    f (getPat bnd) (getBody bnd)
+
+-- | Retrieve the body, as well as the bound pattern.
+unbindl :: (Sized pat, Subst v c) => Bind v c pat n -> (pat, c (Size pat + n))
+unbindl bnd = (getPat bnd, getBody bnd)
+
+-- | Instantiate the body (i.e. replace the bound variables) with the provided terms.
+instantiate ::
+  forall v c pat n.
+  (Sized pat, Subst v c) =>
+  Bind v c pat n ->
+  Env v (Size pat) n ->
+  c n
+instantiate b e =
+  unbindWith
+    b
+    (\p r t -> applyOpt applyE (withSNat (size p) $ e .++ r) t)
+
+-- | Apply a function under the binder.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+applyUnder ::
+  (Sized pat, Subst v c2) =>
+  (forall m. Env v m (Size pat + n2) -> c1 m -> c2 (Size pat + n2)) ->
+  Env v n1 n2 ->
+  Bind v c1 pat n1 ->
+  Bind v c2 pat n2
+applyUnder f r2 (Bind p r1 t) =
+  Bind p idE (f r' t)
+  where
+    r' = upN (size p) (r1 .>> r2)
+
+-- | Run a function on the body.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+unbindWith ::
+  (Sized pat, SubstVar v) =>
+  Bind v c pat n ->
+  (forall m. pat -> Env v m n -> c (Size pat + m) -> d) ->
+  d
+unbindWith (Bind pat (r :: Env v m n) t) f =
+  f pat r t
+
+-- | Instantiate the body (i.e. replace the bound variable) with the provided term.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+instantiateWith ::
+  (Sized pat, SubstVar v) =>
+  Bind v c pat n ->
+  Env v (Size pat) n ->
+  (forall m. Env v m n -> c m -> c n) ->
+  c n
+instantiateWith b v f = unbindWith b (\p r e -> withSNat (size p) $ f (v .++ r) e)
+
+-----------------------------------------------------------------
+-- instances for Bind (Subst, FV, Strengthen)
+-----------------------------------------------------------------
+
+-- | The substitution operation composes the explicit
+-- substitution with the one stored at the binder
+instance (SubstVar v) => Shiftable (Bind v c p) where
+  shift = shiftFromApplyE @v
+
+instance (SubstVar v) => Subst v (Bind v c p) where
+  applyE :: Env v n m -> Bind v c p n -> Bind v c p m
+  applyE env1 (Bind p env2 m) = Bind p (env2 .>> env1) m
+
+instance (Subst v c, Sized p, FV c) => FV (Bind v c p) where
+  appearsFree :: Fin n -> Bind v c p n -> Bool
+  appearsFree n b =
+    appearsFree (Fin.shiftN (size (getPat b)) n) (getBody b)
+  freeVars :: forall n. Bind v c p n -> Set (Fin n)
+  freeVars b = rescope (size (getPat b)) (freeVars (getBody b))
+
+
+instance (Sized p, Subst v c, Strengthen c) => Strengthen (Bind v c p) where
+  strengthenRec ::
+    SNat k ->
+    SNat m ->
+    SNat n ->
+    Bind v c p (k + (m + n)) ->
+    Maybe (Bind v c p (k + n))
+  strengthenRec (k :: SNat k) (m :: SNat m) (n :: SNat n) bnd =
+    withSNat (sPlus k (sPlus m n)) $
+      unbind bnd $ \(p :: p) t' ->
+        case ( axiomAssoc @(Size p) @k @(m + n),
+               axiomAssoc @(Size p) @k @n
+             ) of
+          (Refl, Refl) ->
+            bind p <$> strengthenRec (sPlus (size p) k) m n t'
+
+-----------------------------------------------------------------
+-- * Rebind type
+---------------------------------------------------------------
+
+data Rebind pat p2 n where
+  Rebind :: pat -> p2 (Size pat + n) -> Rebind pat p2 n
+
+instance (SubstVar v, Sized p1, Subst v p2) => Shiftable (Rebind p1 p2) where
+  shift = shiftFromApplyE @v
+
+instance (SubstVar v, Sized p1, Subst v p2) => Subst v (Rebind p1 p2) where
+  applyE :: Env v n m -> Rebind p1 p2 n -> Rebind p1 p2 m
+  applyE r (Rebind p1 p2) = Rebind p1 (applyE (upN (size p1) r) p2)
+
+instance (Sized p1, FV p2) => FV (Rebind p1 p2) where
+  appearsFree :: (Sized p1, FV p2) => Fin n -> Rebind p1 p2 n -> Bool
+  appearsFree n (Rebind p1 p2) = appearsFree (Fin.shiftN (size p1) n) p2
+
+  freeVars :: (Sized p1, FV p2) => Rebind p1 p2 n -> Set (Fin n)
+  freeVars = undefined
+
+instance (Sized p1, Strengthen p2) => Strengthen (Rebind p1 p2) where
+  strengthenRec (k :: SNat k) (m :: SNat m) (n :: SNat n) (Rebind (p1 :: p1) p2) =
+    case ( axiomAssoc @(Size p1) @k @(m + n),
+           axiomAssoc @(Size p1) @k @n
+         ) of
+      (Refl, Refl) ->
+        Rebind p1 <$> strengthenRec (sPlus (size p1) k) m n p2
+
+--------------------------------------------------------------
+-- * Lists of patterns
+--------------------------------------------------------------
+
+-- | lists of patterns where variables at each position bind
+-- later in the pattern
+data PatList (pat :: Nat -> Type) p where
+  PNil :: PatList pat N0
+  PCons ::
+    (Size (pat p1) ~ p1) =>
+    pat p1 ->
+    PatList pat p2 ->
+    PatList pat (p2 + p1)
+
+-- | The length of a pattern list is the number of patterns,
+-- not the number of variables that it binds
+lengthPL :: PatList pat p -> Int
+lengthPL PNil = 0
+lengthPL (PCons _ ps) = 1 + lengthPL ps
+
+instance (forall n. Sized (pat n)) => Sized (PatList pat p) where
+  type Size (PatList pat p) = p
+  size PNil = s0
+  size (PCons (p1 :: pat p1) (p2 :: PatList pat p2)) =
+    sPlus @p2 @(Size (pat p1)) (size p2) (size p1)
+
+instance
+  (forall p1 p2. PatEq (pat p1) (pat p2)) =>
+  PatEq (PatList pat p1) (PatList pat p2)
+  where
+  patEq :: PatList pat p1 -> PatList pat p2 -> Maybe (p1 :~: p2)
+  patEq PNil PNil = Just Refl
+  patEq (PCons p1 ps1) (PCons p2 ps2) = do
+    Refl <- patEq p1 p2
+    Refl <- patEq ps1 ps2
+    return Refl
+  patEq _ _ = Nothing
+
+-- instance
+--   (forall p n. WithData v (pat p) n) =>
+--   WithData v (PatList pat p) n
+--   where
+--   extendWithData PNil = id
+--   extendWithData (PCons (p1 :: pat p1') (ps :: PatList pat ps')) =
+--     case axiomAssoc @ps' @p1' @n of
+--       Refl -> extendWithData @v ps . extendWithData @v p1
diff --git a/src/Rebound/Bind/PatN.hs b/src/Rebound/Bind/PatN.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Bind/PatN.hs
@@ -0,0 +1,280 @@
+-- |
+-- Module       : Rebound.Bind.PatN
+-- Description  : Bind a number of variables, without metadata
+--
+-- Bind a number of variables, with no other information stored with the binder.
+-- This is a specialization of "Rebound.Bind.Pat".
+module Rebound.Bind.PatN
+  ( module Rebound,
+
+    PatN(..),
+
+    -- * single binder --
+    Bind1 (..),
+    bind1,
+    unbind1,
+    unbindl1,
+    getBody1,
+    instantiate1,
+    bindWith1,
+    unbindWith1,
+    instantiateWith1,
+    applyUnder1,
+
+    -- * double binder --
+    Bind2 (..),
+    bind2,
+    unbind2,
+    getBody2,
+    instantiate2,
+    bindWith2,
+    unbindWith2,
+    instantiateWith2,
+    applyUnder2,
+
+    -- * N-ary binder ---
+    BindN (..),
+    bindN,
+    unbindN,
+    unbindlN,
+    getBodyN,
+    instantiateN,
+    bindWithN,
+    unbindWithN,
+    instantiateWithN,
+    applyUnderN,
+  )
+where
+
+import Rebound.Bind.Pat qualified as Pat
+import Rebound
+
+import Data.Fin qualified as Fin
+import Data.Vec qualified as Vec
+
+
+
+----------------------------------------------------------------
+-- N-ary patterns
+----------------------------------------------------------------
+
+-- | A pattern that binds @p@ variables.
+newtype PatN (p :: Nat) where
+  PatN :: SNat p -> PatN p
+
+deriving instance (Eq (PatN p))
+deriving instance (TestEquality PatN)
+
+instance SNatI p => SizeIndex PatN p
+
+instance (SNatI p) => Sized (PatN p) where
+  type Size (PatN p) = p
+  size (PatN sn) = sn
+
+-- | Type binding a number of variables.
+-- This data structure includes a delayed
+-- substitution for the variables in the body of the binder.
+type BindN v c m n = Pat.Bind v c (PatN m) n
+
+-- | Bind a number of variables, using the identity substitution.
+bindN :: forall m v c n. (Subst v c, SNatI m) => c (m + n) -> BindN v c m n
+bindN = Pat.bind (PatN (snat @m))
+
+-- | Bind a number of variables, while suspending the provided substitution.
+bindWithN :: forall p v c m n. (SNatI p) => Env v m n -> c (p + m) -> BindN v c p n
+bindWithN = Pat.bindWith (PatN (snat @p))
+
+-- | Run a function on the body, after applying the delayed substitution.
+unbindN :: forall m v c n d. (Subst v c, SNatI n, SNatI m) => BindN v c m n -> ((SNatI (m + n)) => c (m + n) -> d) -> d
+unbindN bnd f = Pat.unbind bnd (const f)
+
+-- | Retrieve the body of the binding.
+-- For this kind of binding, it is equivalent to 'getBodyN'.
+unbindlN :: forall m v c n. (Subst v c, SNatI m) => BindN v c m n -> c (m + n)
+unbindlN = Pat.getBody
+
+-- | Retrieve the body of the binding.
+getBodyN :: forall m v c n. (Subst v c, SNatI m) => BindN v c m n -> c (m + n)
+getBodyN = Pat.getBody
+
+-- | Instantiate the body (i.e. replace the bound variables) with the provided terms.
+instantiateN :: (Subst v c, SNatI m) => BindN v c m n -> Vec m (v n) -> c n
+instantiateN b v = Pat.instantiate b (fromVec v)
+
+-- | Run a function on the body.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+unbindWithN ::
+  (SubstVar v, SNatI m) =>
+  BindN v c m n ->
+  (forall m1. Env v m1 n -> c (m + m1) -> d) ->
+  d
+unbindWithN b f = Pat.unbindWith b (const f)
+
+-- | Instantiate the body (i.e. replace the bound variable) with the provided terms.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+instantiateWithN ::
+  forall m v c d n.
+  (SubstVar v, SNatI n, SNatI m) =>
+  BindN v c m n ->
+  Vec m (v n) ->
+  (forall m. Env v m n -> c m -> d n) ->
+  d n
+instantiateWithN b v f =
+  unbindWithN b (f . appendE (snat @m) (fromVec v))
+
+-- | Apply a function under the binder.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+applyUnderN ::
+  (Subst v c2, SNatI k) =>
+  (forall m. Env v m (k + n2) -> c1 m -> c2 (k + n2)) ->
+  Env v n1 n2 ->
+  BindN v c1 k n1 ->
+  BindN v c2 k n2
+applyUnderN = Pat.applyUnder
+
+----------------------------------------------------------------
+-- Single binder
+----------------------------------------------------------------
+
+-- | Type binding 1 variable.
+-- This data structure includes a delayed
+-- substitution for the variables in the body of the binder.
+type Bind1 v c n = Pat.Bind v c (PatN N1) n
+
+-- | Bind 1 variable, using the identity substitution.
+bind1 :: (Subst v c) => c (S n) -> Bind1 v c n
+bind1 = Pat.bind (PatN s1)
+
+-- | Bind 1 variable, while suspending the provided substitution.
+bindWith1 :: forall v c m n. Env v m n -> c (S m) -> Bind1 v c n
+bindWith1 = Pat.bindWith (PatN s1)
+
+-- | Run a function on the body, after applying the delayed substitution.
+unbind1 ::
+  forall v c n d.
+  (SNatI n, Subst v c) =>
+  Bind1 v c n ->
+  ((SNatI (S n)) => c (S n) -> d) ->
+  d
+unbind1 b f = f (Pat.getBody b)
+
+-- | Retrieve the body of the binding.
+-- For this kind of binding, it is equivalent to 'getBody1'.
+unbindl1 :: forall v c n. (Subst v c) => Bind1 v c n -> c (S n)
+unbindl1 = Pat.getBody
+
+-- | Retrieve the body of the binding.
+getBody1 ::
+  forall v c n.
+  (Subst v c) =>
+  Bind1 v c n ->
+  c (S n)
+getBody1 = Pat.getBody
+
+-- | Instantiate the body (i.e. replace the bound variable) with the provided term.
+instantiate1 :: (Subst v c) => Bind1 v c n -> v n -> c n
+instantiate1 b v1 = Pat.instantiate b (v1 .: zeroE)
+
+-- | Run a function on the body.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+unbindWith1 ::
+  (SubstVar v) =>
+  Bind1 v c n ->
+  (forall m. Env v m n -> c (S m) -> d) ->
+  d
+unbindWith1 b f = Pat.unbindWith b (const f)
+
+-- | Instantiate the body (i.e. replace the bound variable) with the provided terms.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+instantiateWith1 ::
+  (SubstVar v) =>
+  Bind1 v c n ->
+  v n ->
+  (forall m. Env v m n -> c m -> d n) ->
+  d n
+instantiateWith1 b v1 f =
+  unbindWith1 b (\r e -> f (v1 .: r) e)
+
+-- | Apply a function under the binder.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+applyUnder1 ::
+  (Subst v c2) =>
+  (forall m. Env v m (S n2) -> c1 m -> c2 (S n2)) ->
+  Env v n1 n2 ->
+  Bind1 v c1 n1 ->
+  Bind1 v c2 n2
+applyUnder1 = Pat.applyUnder
+
+----------------------------------------------------------------
+-- Double binder
+----------------------------------------------------------------
+
+-- | Type binding 2 variables.
+-- This data structure includes a delayed
+-- substitution for the variables in the body of the binder.
+type Bind2 v c n = Pat.Bind v c (PatN N2) n
+
+-- | Bind 2 variables, using the identity substitution.
+bind2 :: (Subst v c) => c (S (S n)) -> Bind2 v c n
+bind2 = Pat.bind (PatN s2)
+
+-- | Bind 2 variables, while suspending the provided substitution.
+bindWith2 :: forall v c m n. Env v m n -> c (S (S m)) -> Bind2 v c n
+bindWith2 = Pat.bindWith (PatN s2)
+
+-- | Run a function on the body, after applying the delayed substitution.
+unbind2 ::
+  forall v c n d.
+  (Subst v c) =>
+  Bind2 v c n ->
+  (c (S (S n)) -> d) ->
+  d
+unbind2 b f = f (getBody2 b)
+
+-- | Retrieve the body of the binding.
+-- For this kind of binding, it is equivalent to 'getBody2'.
+unbindl2 :: forall v c n. (Subst v c) => Bind2 v c n -> c (S (S n))
+unbindl2 = Pat.getBody
+
+-- | Retrieve the body of the binding.
+getBody2 ::
+  forall v c n.
+  (Subst v c) =>
+  Bind2 v c n ->
+  c (S (S n))
+getBody2 = Pat.getBody
+
+-- | Instantiate the body (i.e. replace the bound variable) with the provided term.
+instantiate2 :: (Subst v c) => Bind2 v c n -> v n -> v n -> c n
+instantiate2 b v1 v2 = Pat.instantiate b (v1 .: (v2 .: zeroE))
+
+-- | Run a function on the body.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+unbindWith2 ::
+  (SubstVar v) =>
+  Bind2 v c n ->
+  (forall m. Env v m n -> c (S (S m)) -> d) ->
+  d
+unbindWith2 b f = Pat.unbindWith b (const f)
+
+-- | Instantiate the body (i.e. replace the bound variable) with the provided terms.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+instantiateWith2 ::
+  (SubstVar v, SNatI n) =>
+  Bind2 v c n ->
+  v n ->
+  v n ->
+  (forall m. Env v m n -> c m -> d n) ->
+  d n
+instantiateWith2 b v1 v2 f =
+  unbindWith2 b (\r e -> f (v1 .: (v2 .: r)) e)
+
+-- | Apply a function under the binder.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+applyUnder2 ::
+  (Subst v c2) =>
+  (forall m. Env v m (S (S n2)) -> c1 m -> c2 (S (S n2))) ->
+  Env v n1 n2 ->
+  Bind2 v c1 n1 ->
+  Bind2 v c2 n2
+applyUnder2 = Pat.applyUnder
diff --git a/src/Rebound/Bind/Scoped.hs b/src/Rebound/Bind/Scoped.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Bind/Scoped.hs
@@ -0,0 +1,473 @@
+-- | 
+-- Module       : Rebound.Bind.Scoped
+-- Description  : Bind variables while referring to them
+--
+-- A "Scoped" pattern binds variables but can also include subterms that
+-- reference free variables that are already in scope. This is useful for type
+-- annotations and telescopes. The pattern type typically has kind
+-- @'Nat' -> 'Type'@, the 'Nat' is used to track the (initial) number of free
+-- variables. For a simpler interface, see 'Rebound.Bind.Pat.Pat'.
+module Rebound.Bind.Scoped (
+    module Rebound,
+    Bind,
+    bind,
+    getPat,
+    getBody,
+    unbind,
+    unbindl,
+    instantiate,
+    unbindWith,
+    instantiateWith,
+    applyUnder,
+    instantiateWeakenEnv,
+
+    -- * Number of binding vars in pats
+    ScopedSized(..),
+    scopedSize,
+    scopedPatEq,
+    EqSized,
+    EqScopedSized,
+    
+    -- * Telescopes
+    -- IScoped make sense, but are never used anywhere; should be remove it?
+    IScopedSized,
+    iscopedSize,
+    iscopedPatEq,
+    TeleList(..),
+    lengthTele,
+    nil, (<:>),(<++>),
+  ) where
+
+import Rebound
+import Rebound.Bind.Pat qualified as Pat
+
+import Data.Set (Set)
+import Data.Set qualified as Set
+import Data.Maybe qualified as Maybe
+import Data.Fin qualified as Fin
+import Data.Vec qualified as Vec
+
+----------------------------------------------------------
+-- Sized type class for patterns
+----------------------------------------------------------
+
+-- | Constrain 'ScopedSized' to agree with 'Sized'.
+class (Sized (t p), Size (t p) ~ ScopedSize t) => EqSized t p
+
+instance (Sized (t p), Size (t p) ~ ScopedSize t) => EqSized t p
+
+-- | Type class for the size of scoped patterns.
+-- The size it returns must be the same as the one returned by 'Sized'.
+--
+-- This type class is there to force the size of the pattern to be independent
+-- of the number of variables in scope. This technique is described by:
+-- https://blog.poisson.chat/posts/2022-09-21-quantified-constraint-trick.html
+class (forall p. EqSized pat p) => ScopedSized pat where
+  type ScopedSize (pat :: Nat -> Type) :: Nat
+
+-- | 'Rebound.Classes.size', but with a type referring to 'ScopedSize'.
+scopedSize :: forall pat p. (ScopedSized pat) => pat p -> SNat (ScopedSize pat)
+scopedSize = size
+
+-- | Compare two patterns for equality. Provide a proof of equality of their
+-- size in case of success.
+scopedPatEq ::
+  (ScopedSized pat1, ScopedSized pat2, PatEq (pat1 p1) (pat2 p2)) =>
+  pat1 p1 ->
+  pat2 p2 ->
+  Maybe (ScopedSize pat1 :~: ScopedSize pat2)
+scopedPatEq = patEq
+
+-- This file uses `ScopedSize`, `scopedSize`, and `scopedNames`,
+-- instead of `Size`, `size`, and `names` throughout.
+
+----------------------------------------------------------
+-- Scoped Pattern binding
+----------------------------------------------------------
+
+-- | The `Bind` type binds (ScopedSize p) variables.
+-- Patterns can also include free occurrences of variables, so
+-- the type is indexed by a scope level.
+-- This data structure includes a delayed
+-- substitution for the variables in the body of the binder.
+data Bind v c (pat :: Nat -> Type) (n :: Nat) where
+  Bind ::
+    pat n ->
+    Env v m n ->
+    c (ScopedSize pat + m) ->
+    Bind v c pat n
+
+-- | To compare pattern binders, we need to unbind, but also
+-- first make sure that the patterns are equal.
+instance (forall n. Eq (c n), 
+    PatEq (pat m n) (pat m n), 
+    ScopedSized (pat m), 
+    Subst v c) => Eq (Bind v c (pat m) n) where
+  b1 == b2 =
+    Maybe.isJust (patEq (getPat b1) (getPat b2))
+      && getBody b1 == getBody b2
+
+-- | Bind a pattern, using the identity substitution.
+bind ::
+  forall v c pat n.
+  (ScopedSized pat, Subst v c) =>
+  pat n ->
+  c (ScopedSize pat + n) ->
+  Bind v c pat n
+bind pat = Bind pat idE
+
+-- | Bind a pattern, while suspending the provided substitution.
+bindWith ::
+  (ScopedSized pat, Subst v c) =>
+  pat n -> Env v m n -> c (ScopedSize pat + m) -> Bind v c pat n
+bindWith = Bind
+
+-- | Retrieve the pattern of the binding.
+getPat :: Bind v c pat n -> pat n
+getPat (Bind pat env t) = pat
+
+-- | Retrieve the body of the binding.
+getBody ::
+  forall v c pat n.
+  (ScopedSized pat, Subst v v, Subst v c) =>
+  Bind v c pat n ->
+  c (ScopedSize pat + n)
+getBody (Bind (pat :: pat n) (env :: Env v m n) t) =
+  applyE @v @c @(ScopedSize pat + m) (upN (scopedSize pat) env) t
+
+-- | Run a function on the body (and pattern), after applying the delayed substitution.
+-- The size of the (current) scope is made available at runtime.
+unbind ::
+  forall v c pat n d.
+  (SNatI n, forall n. ScopedSized pat, Subst v v, Subst v c) =>
+  Bind v c pat n ->
+  ((SNatI (ScopedSize pat + n)) => pat n -> c (ScopedSize pat + n) -> d) ->
+  d
+unbind bnd f =
+  withSNat (sPlus (scopedSize (getPat bnd)) (snat @n)) $
+    f (getPat bnd) (getBody bnd)
+
+-- | Retrieve the body, as well as the bound pattern.
+unbindl :: (SNatI n, Subst v c, ScopedSized pat) => Bind v c pat n -> (pat n, c (ScopedSize pat + n))
+unbindl bnd = (getPat bnd, getBody bnd)
+
+-- | Instantiate the body (i.e. replace the bound variables) with the provided terms.
+instantiate ::
+  forall v c pat n.
+  (forall n. ScopedSized pat, Subst v c) =>
+  Bind v c pat n ->
+  Env v (ScopedSize pat) n ->
+  c n
+instantiate b e =
+  unbindWith
+    b
+    (\p r t -> withSNat (scopedSize p) $ applyE (e .++ r) t)
+
+-- | Apply a function under the binder.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+applyUnder ::
+  forall pat v c n1 n2.
+  (ScopedSized pat, Subst v v, Subst v c, Subst v pat) =>
+  (forall m. Env v m (ScopedSize pat + n2) -> c m -> c (ScopedSize pat + n2)) ->
+  Env v n1 n2 ->
+  Bind v c pat n1 ->
+  Bind v c pat n2
+applyUnder f r2 (Bind p r1 t) =
+  Bind p' idE (f r' t)
+  where
+    r' = upN sp' (r1 .>> r2)
+    sp' :: SNat (ScopedSize pat)
+    sp' = size p'
+    p' :: pat n2
+    p' = applyE r2 p
+
+-- | Run a function on the body.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+unbindWith ::
+  (forall n. Sized (pat n), SubstVar v) =>
+  Bind v c pat n ->
+  (forall m. pat n -> Env v m n -> c (ScopedSize pat + m) -> d) ->
+  d
+unbindWith (Bind pat r t) f = f pat r t
+
+-- | Instantiate the body (i.e. replace the bound variable) with the provided term.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+instantiateWith ::
+  (ScopedSized pat, SubstVar v) =>
+  Bind v c pat n ->
+  Env v (ScopedSize pat) n ->
+  (forall m. Env v m n -> c m -> c n) ->
+  c n
+instantiateWith b v f =
+  unbindWith b (\p r e -> withSNat (scopedSize p) $ f (v .++ r) e)
+
+-- | Map variable 0 to given value, and shift everything else
+-- in the environment.
+instantiateWeakenEnv ::
+  forall p n v c.
+  (SubstVar v, Subst v v) =>
+  SNat p ->
+  SNat n ->
+  v (p + n) ->
+  Env v (S n) (p + n)
+instantiateWeakenEnv p n a =
+  a .: shiftNE p
+
+-----------------------------------------------------------------
+-- instances for Bind
+-----------------------------------------------------------------
+
+instance (ScopedSized pat, Subst v pat, Subst v v) => Shiftable (Bind v c pat) where
+  shift = shiftFromApplyE @v
+
+instance (ScopedSized pat, Subst v pat, Subst v v) => Subst v (Bind v c pat) where
+  applyE (env1 :: Env v n m) (Bind (pat :: pat n) (env2 :: Env v m1 n) m) =
+    Bind (applyE env1 pat) (env2 .>> env1) m
+
+instance
+  ( Subst v c,
+    ScopedSized p,
+    FV p,
+    FV c
+  ) =>
+  FV (Bind v c p)
+  where
+  appearsFree n b =
+    let pat = getPat b
+     in appearsFree n pat
+          || appearsFree (Fin.shiftN (scopedSize pat) n) (getBody b)
+
+  freeVars :: forall n. (Subst v c, ScopedSized p, FV p, FV c) =>
+     Bind v c p n -> Set (Fin n)
+  freeVars b =
+    let pat = getPat b
+        body = getBody b
+    in
+       freeVars pat <> rescope (scopedSize pat) (freeVars body)
+
+
+instance (ScopedSized p, SubstVar v, Subst v v, Subst v c, Strengthen c, Strengthen p) =>
+  Strengthen (Bind v c p)
+  where
+  strengthenRec (k :: SNat k) (m :: SNat m) (n :: SNat n) bnd =
+    withSNat (sPlus k (sPlus m n)) $
+      unbind bnd $ \(p :: p (k + (m + n))) t' ->
+        case ( axiomAssoc @(ScopedSize p) @k @(m + n),
+               axiomAssoc @(ScopedSize p) @k @n
+             ) of
+          (Refl, Refl) ->
+            let p' :: Maybe (p (k + n))
+                p' = strengthenRec k m n p
+
+                r :: Maybe (c (ScopedSize p + (k + n)))
+                r = strengthenRec (sPlus (scopedSize p) k) m n t'
+             in bind <$> p' <*> r
+
+-----------------------------------------------------------------
+-- Telescopes
+---------------------------------------------------------------
+
+-- Telescopes are parameterized by scoped patterns, with kinds
+-- `pat :: Nat -> Nat -> Type`. For these types, we need to know
+-- that the first argument is the number of binding variables,
+-- (i.e. Size or ScopedSize) so we need yet *another* type class
+-- to make this constraint.
+
+-- | Constrain 'IScopedSized' to agree with 'ScopedSized'.
+class (ScopedSize (t p) ~ p) => EqScopedSized t p
+
+instance (ScopedSize (t p) ~ p) => EqScopedSized t p
+
+-- | An indexed 'ScopedSized'.
+class
+  ( forall p. ScopedSized (pat p),
+    forall p. EqScopedSized pat p
+  ) =>
+  IScopedSized pat
+
+-- | 'Rebound.Classes.size', but with a type referring to 'IScopedSized'.
+iscopedSize :: (IScopedSized pat) => pat p n -> SNat p
+iscopedSize = scopedSize
+
+-- | Compare two patterns for equality. Provide a proof of equality of their
+-- size in case of success.
+iscopedPatEq ::
+  (IScopedSized pat1, IScopedSized pat2, PatEq (pat1 p1 n1) (pat2 p2 n2)) =>
+  pat1 p1 n1 ->
+  pat2 p2 n2 ->
+  Maybe (p1 :~: p2)
+iscopedPatEq = scopedPatEq
+
+-- | A telescope binds a linear sequence of variables. Each variable can have
+-- metadata attached, and that metadata can be indexed. Each piece of metadata
+-- can refer to every variable initially in scope, as well as every variables
+-- previously introduced by the telescope itself.
+-- 
+-- The type parameters are
+-- - @p@ is the number of variables introduced by the telescope
+-- - @n@ is the number of free variables for @A1@ (and @A2@ has @S n@, etc.)
+--
+-- We include some arithmetic properties with each constructors, so that these
+-- get brought in scope when pattern matching. Smart constructors 'nil'
+-- and '<:>' can be used to easily construct 'TeleList'.
+data TeleList (pat :: Nat -> Nat -> Type) p n where
+  TNil :: ( n + N0 ~ n) =>
+    TeleList pat N0 n
+  TCons ::
+    ( IScopedSized pat,
+      p2 + (p1 + n) ~ (p2 + p1) + n
+    ) =>
+    pat p1 n ->
+    TeleList pat p2 (p1 + n) ->
+    TeleList pat (p2 + p1) n
+
+-- | Length of a 'TeleList'.
+lengthTele :: TeleList pat p n -> Int
+lengthTele TNil = 0
+lengthTele (TCons _ ps) = 1 + lengthTele ps
+
+-- | Smart constructor for 'TNil'.
+nil :: forall pat n. TeleList pat N0 n
+nil = case axiomPlusZ @n of Refl -> TNil
+
+-- | Smart constructor for 'TCons'.
+(<:>) ::
+  forall p1 p2 pat n.
+  (IScopedSized pat) =>
+  pat p1 n ->
+  TeleList pat p2 (p1 + n) ->
+  TeleList pat (p2 + p1) n
+e <:> t = case axiomAssoc @p2 @p1 @n of Refl -> TCons e t
+
+-- | Append two telescopes.
+(<++>) ::
+  forall p1 p2 pat n.
+  (IScopedSized pat) =>
+  TeleList pat p1 n ->
+  TeleList pat p2 (p1 + n) ->
+  TeleList pat (p2 + p1) n
+TNil <++> t = case axiomPlusZ @p2 of Refl -> t
+(TCons @_ @p12 @p11 h t) <++> t' = case axiomAssoc @p2 @p12 @p11 of Refl -> h <:> (t <++> t')
+
+infixr 9 <:>
+
+instance IScopedSized (TeleList pat)
+
+instance ScopedSized (TeleList pat p) where
+  type ScopedSize (TeleList pat p) = p
+
+instance Sized (TeleList pat p n) where
+  type Size (TeleList pat p n) = p
+  size TNil = s0
+  size (TCons p1 p2) = sPlus (size p2) (iscopedSize p1)
+
+instance (IScopedSized pat, Subst v v, forall p. Subst v (pat p)) => Shiftable (TeleList pat p) where
+  shift = shiftFromApplyE @v
+
+instance
+  (IScopedSized pat, Subst v v, forall p. Subst v (pat p)) =>
+  Subst v (TeleList pat p)
+  where
+  applyE r TNil = nil
+  applyE r (TCons p1 p2) =
+    applyE r p1 <:> applyE (upN (iscopedSize p1) r) p2
+
+instance (IScopedSized pat, forall p. FV (pat p)) => FV (TeleList pat p) where
+  appearsFree ::
+    forall n.
+    (IScopedSized pat, forall p1. FV (pat p1)) =>
+    Fin n ->
+    TeleList pat p n ->
+    Bool
+  appearsFree n TNil = False
+  appearsFree n (TCons p1 p2) = appearsFree n p1 || appearsFree (Fin.shiftN (iscopedSize p1) n) p2
+
+  freeVars :: TeleList pat p n -> Set (Fin n)
+  freeVars TNil = Set.empty
+  freeVars (TCons p1 p2) = freeVars p1 <> rescope (iscopedSize p1) (freeVars p2)
+
+instance (forall p1. Strengthen (pat p1)) => Strengthen (TeleList pat p) where
+  strengthenRec k m n TNil = Just nil
+  strengthenRec (k :: SNat k) (m :: SNat m) (n :: SNat n) (TCons (p1 :: pat p1 (k + (m + n))) p2) =
+    case ( axiomAssoc @p1 @k @(m + n),
+           axiomAssoc @p1 @k @n
+         ) of
+      (Refl, Refl) ->
+        (<:>)
+          <$> strengthenRec k m n p1
+          <*> strengthenRec (sPlus (iscopedSize p1) k) m n p2
+
+instance
+  (forall p1 p2 n1 n2. PatEq (pat p1 n1) (pat p2 n2), IScopedSized pat) =>
+  PatEq (TeleList pat p1 n1) (TeleList pat p2 n2)
+  where
+  patEq TNil TNil = Just Refl
+  patEq (TCons p1 p2) (TCons p1' p2')
+    | Just Refl <- iscopedPatEq p1 p1',
+      Just Refl <- iscopedPatEq p2 p2' =
+        Just Refl
+  patEq _ _ = Nothing
+
+-----------------------------------------------------------------
+-- Rebind
+-- TODO: this is the binary version of a telescope.
+-- Captures the left-to-right relationship between two patterns
+-- without the list.
+---------------------------------------------------------------
+{-
+data Rebind p1 p2 n where
+  Rebind ::
+    Plus (Size (p2 n)) (Plus (Size (p1 n)) n) ~ Plus (Plus (Size (p2 n)) (Size (p1 n))) n =>
+    p1 n -> p2 (Plus (Size (p1 n)) n) -> Rebind p1 p2 n
+
+rebind :: forall p1 p2 n. p1 n -> p2 (Plus (Size (p1 n)) n) -> Rebind p1 p2 n
+rebind p1 p2 =
+  case axiomAssoc @(Size (p2 n)) @(Size (p1 n)) @n of
+    Refl -> Rebind p1 p2
+
+instance (ScopedSized p1, ScopedSized p2) => Sized (Rebind p1 p2 n) where
+    type Size (Rebind p1 p2 n) = Plus (Size (p2 n)) (Size (p1 n))
+    size (Rebind p1 p2) = sPlus @(Size (p2 n)) @(Size (p1 n)) (size p2) (size p1)
+
+-- instance (Sized p1, Sized p2) => Sized (Rebind p1 p2) where
+--  type Size (Rebind p1 p2) = Plus (Size p2) (Size p1)
+--  size (Rebind p1 p2) = sPlus (size p2) (size p1)
+
+instance
+  (Subst v v, forall n. ScopedSized p1, Subst v p1, Subst v p2) =>
+  Subst v (Rebind p1 p2)
+  where
+  applyE ::
+    (Subst v v, ScopedSized p1, Subst v p2) =>
+    Env v n m ->
+    Rebind p1 p2 n ->
+    Rebind p1 p2 m
+  applyE r (Rebind p1 p2) =
+    rebind (applyE r p1) (applyE (upN (size p1) r) p2)
+
+instance (forall n. ScopedSized p1, FV p2) => FV (Rebind p1 p2) where
+  appearsFree :: (ScopedSized p1, FV p2) => Fin n -> Rebind p1 p2 n -> Bool
+  appearsFree n (Rebind p1 p2) = appearsFree (shiftN (size p1) n) p2
+
+unRebind ::
+  forall p1 p2 n c.
+  (ScopedSized p1, ScopedSized p2, SNatI n) =>
+  Rebind p1 p2 n ->
+  ( ( SNatI (Size (p1 n)),
+      SNatI (Size (p2 n)),
+      SNatI (Plus (Size (p1 n)) n),
+      Plus (Size (p2 n)) (Plus (Size (p1 n)) n) ~ Plus (Plus (Size (p2 n)) (Size (p1 n))) n
+    ) =>
+    p1 n ->
+    p2 (Plus (Size (p1 n)) n) ->
+    c
+  ) ->
+  c
+unRebind (Rebind p1 p2) f =
+  case axiomAssoc @(Size (p2 n)) @(Size (p1 n)) @n of
+    Refl ->
+      withSNat (size p1) $
+        withSNat (size p2) $
+          withSNat (sPlus (size p1) (snat @n)) $
+            f p1 p2
+-}
diff --git a/src/Rebound/Bind/Single.hs b/src/Rebound/Bind/Single.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Bind/Single.hs
@@ -0,0 +1,70 @@
+-- |
+-- Module       : Rebound.Bind.Single
+-- Description  : Bind a single variable, without metadata
+--
+-- Simplest form of binding: a single variable with no other information stored with the binder.
+-- This is a specialization of "Rebound.Bind.PatN".
+module Rebound.Bind.Single
+  ( module Rebound,
+    Bind (..),
+    bind,
+    unbind,
+    unbindl,
+    getBody,
+    instantiate,
+    bindWith,
+    unbindWith,
+    instantiateWith,
+    applyUnder,
+  )
+where
+
+import Rebound
+import Rebound.Bind.PatN
+import Rebound.Classes
+
+-- | Type binding a single variable.
+-- This data structure includes a delayed
+-- substitution for the variables in the body of the binder.
+type Bind v c n = Bind1 v c n
+
+-- | Bind a variable, using the identity substitution.
+bind :: (Subst v c) => c (S n) -> Bind v c n
+bind = bind1
+
+-- | Bind a variable, while suspending the provided substitution.
+bindWith :: forall v c m n. Env v m n -> c (S m) -> Bind v c n
+bindWith = bindWith1
+
+-- | Run a function on the body, after applying the delayed substitution.
+unbind :: forall v c n d. (SNatI n, Subst v c) => Bind v c n -> ((SNatI (S n)) => c (S n) -> d) -> d
+unbind = unbind1
+
+-- | Retrieve the body of the binding.
+-- For this kind of binding, it is equivalent to 'getBody'.
+unbindl :: (Subst v c) => Bind v c n -> c (S n)
+unbindl = unbindl1
+
+-- | Retrieve the body of the binding.
+getBody :: forall v c n. (Subst v c) => Bind v c n -> c (S n)
+getBody = getBody1
+
+-- | Instantiate the body (i.e. replace the bound variable) with the provided term.
+instantiate :: (Subst v c) => Bind v c n -> v n -> c n
+instantiate = instantiate1
+
+-- | Run a function on the body.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+unbindWith :: (SubstVar v) => Bind v c n -> (forall m. Env v m n -> c (S m) -> d) -> d
+unbindWith = unbindWith1
+
+-- | Instantiate the body (i.e. replace the bound variable) with the provided term.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+instantiateWith :: (SubstVar v) => Bind v c n -> v n -> (forall m. Env v m n -> c m -> d n) -> d n
+instantiateWith = instantiateWith1
+
+-- | Apply a function under the binder.
+-- The delayed substitution is __not__ applied, but is passed to the function instead.
+applyUnder :: (Subst v c2) => (forall m. Env v m (S n2) -> c1 m -> c2 (S n2)) -> Env v n1 n2 -> Bind v c1 n1 -> Bind v c2 n2
+applyUnder = applyUnder1
+
diff --git a/src/Rebound/Classes.hs b/src/Rebound/Classes.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Classes.hs
@@ -0,0 +1,211 @@
+-- |
+-- Module      : Rebound.Classes
+-- Description : Type class definitions
+--
+-- Main typeclasses used by the library.
+
+{-# LANGUAGE DefaultSignatures #-}
+module Rebound.Classes where
+
+import Rebound.Lib
+import Data.LocalName
+import Data.Scoped.List(List, pattern Nil, pattern (:<))
+import Data.Scoped.List qualified as List
+
+import Data.Foldable
+import Data.Vec qualified as Vec
+import Data.Fin qualified as Fin
+import Data.Set (Set)
+import Data.Set qualified as Set
+
+import GHC.Generics (Generic1(..))
+
+----------------------------------------------------------
+-- Indices/variables shifting
+----------------------------------------------------------
+
+-- | Bring a scoped type into a new, bigger, scope, through variable shifting.
+-- 
+-- This class is used for types which are scoped, yet do not allow
+-- substitution in general. Typical examples are data-structures which
+-- associate metadata to variables.
+-- See 'Rebound.Refinement.Refinement' for an example. 
+class Shiftable t where
+  shift :: SNat k -> t n -> t (k + n)
+  -- a good default implementation of this is `shiftFromApply`. But the 
+  -- `Subst` class is not yet in scope.  
+  
+----------------------------------------------------------
+-- Free variables
+----------------------------------------------------------
+
+-- | Computes the set of free variables in a term.
+class FV (t :: Nat -> Type) where
+  -- | Does a particular variable appear free?
+  appearsFree :: Fin n -> t n -> Bool
+  default appearsFree :: (Generic1 t, GFV (Rep1 t)) => Fin n -> t n -> Bool
+  appearsFree x e = gappearsFree x (from1 e)
+  {-# INLINE appearsFree #-}
+
+  -- | Calculate all of the free variables in a term.
+  freeVars :: t n -> Set (Fin n)
+  default freeVars :: (Generic1 t, GFV (Rep1 t)) => t n -> Set (Fin n)
+  freeVars e = gfreeVars (from1 e)
+  {-# INLINE freeVars #-}
+
+-- | Generic programming support for 'FV'.
+class GFV (t :: Nat -> Type) where
+  gappearsFree :: Fin n -> t n -> Bool
+  gfreeVars :: t n -> Set (Fin n)
+
+----------------------------------------------------------
+-- * Strengthening
+----------------------------------------------------------
+
+-- Strengthening cannot be implemented through substitution because it
+-- must fail if the term uses invalid variables. Therefore, we make a
+-- class of scoped types that can be strengthened.
+
+-- | Eliminates the most recently bound variable from the term (if unused).
+strengthen :: forall n t. (Strengthen t, SNatI n) => t (S n) -> Maybe (t n)
+strengthen = strengthenRec s0 s1 (snat :: SNat n)
+
+-- | Eliminates the @n@ most recently bound variables from the term (if unused).
+strengthenN :: forall m n t. (Strengthen t, SNatI n) => SNat m -> t (m + n) -> Maybe (t n)
+strengthenN m = strengthenRec s0 m (snat :: SNat n)
+
+-- | Bring scoped terms into a smaller scope, if possible.
+--
+-- Strengthening is only possible if the term only refers to variable which
+-- are in the smaller scope.
+class Strengthen t where
+  -- generalize strengthening -- remove m variables from the middle of the scope
+  strengthenRec :: SNat k -> SNat m -> SNat n -> t (k + (m + n)) -> Maybe (t (k + n))
+  default strengthenRec :: (Generic1 t, GStrengthen (Rep1 t)) =>
+     SNat k -> SNat m -> SNat n -> t (k + (m + n)) -> Maybe (t (k + n))
+  strengthenRec k m n t = to1 <$> gstrengthenRec k m n (from1 t)
+
+  -- Remove a single variable from the middle of the scope
+  strengthenOneRec :: forall k n. SNat k -> SNat n -> t (k + S n) -> Maybe (t (k + n))
+  strengthenOneRec k = strengthenRec k s1
+
+-- | Generic programming support for 'Strengthen'.
+class GStrengthen (t :: Nat -> Type) where
+  gstrengthenRec :: SNat k -> SNat m -> SNat n -> t (k + (m + n)) -> Maybe (t (k + n))
+
+----------------------------------------------------------
+-- FV and Strengthen instances for Data.Scoped.List
+---------------------------------------------------------
+
+instance FV t => FV (List t) where
+  appearsFree :: Fin n -> List t n -> Bool
+  appearsFree x = List.any (appearsFree x)
+
+  freeVars :: List t n -> Set (Fin n)
+  freeVars = List.foldr (\x s -> freeVars x `Set.union` s) Set.empty
+
+instance Strengthen t => Strengthen (List t) where
+  strengthenRec :: SNat k -> SNat m -> SNat n -> List t (k + (m + n)) -> Maybe (List t (k + n))
+  strengthenRec k m n Nil = Just Nil
+  strengthenRec k m n (x :< xs) = (:<) <$> strengthenRec k m n x <*> strengthenRec k m n xs
+
+----------------------------------------------------------
+-- FV and Strengthen instances for Fin
+---------------------------------------------------------
+
+instance FV Fin where
+  appearsFree = (==)
+  freeVars = Set.singleton
+
+instance Strengthen Fin where
+  strengthenRec :: SNat k -> SNat m -> SNat n-> Fin (k + (m + n)) -> Maybe (Fin (k + n))
+  strengthenRec = Fin.strengthenRecFin
+
+-- | Update a set of free variables to a new scope through strengthening
+rescope :: forall n k. SNat k -> Set (Fin (k + n)) -> Set (Fin n)
+rescope k = foldMap g where
+   g :: Fin (k + n) -> Set (Fin n)
+   g x = maybe
+     Set.empty Set.singleton
+     (Fin.strengthenRecFin s0 k (undefined :: SNat n) x)
+
+----------------------------------------------------------
+-- Type classes for patterns
+----------------------------------------------------------
+
+-- | Calculate the number of binding variables in the pattern
+-- This number does not need to be an explicit parameter of the type
+-- so that we have flexibility about what types we can use as
+-- patterns.
+class Sized (t :: Type) where
+  -- Retrieve size from the type (number of variables bound by the pattern)
+  type Size t :: Nat
+  -- Access size as a term
+  size :: t -> SNat (Size t)
+
+-- | Pairs of types that can be compared with each other as patterns
+class PatEq (t1 :: Type) (t2 :: Type) where
+    patEq :: t1 -> t2 -> Maybe (Size t1 :~: Size t2)
+
+-- | Class of patterns that are indexed by a natural number
+-- where the size is that index directly
+class (Sized (t p), Size (t p) ~ p) => SizeIndex t p
+
+
+---------------------------------------------------------
+-- Pattern Class Instances for Prelude and Lib Types
+---------------------------------------------------------
+
+-- ** LocalNames
+
+instance Sized LocalName where
+  type Size LocalName = N1
+  size _ = s1
+
+instance PatEq LocalName LocalName where
+  patEq p1 p2 = Just Refl
+
+-- ** SNats
+instance Sized (SNat n) where
+  type Size (SNat n) = n
+  size n = n
+
+instance PatEq (SNat n1) (SNat n2) where
+  patEq = testEquality
+
+
+-- ** Vectors
+
+instance Sized (Vec n a) where
+  type Size (Vec n a) = n
+  size = Vec.vlength
+
+instance Eq a => PatEq (Vec n1 a) (Vec n2 a) where
+  patEq VNil VNil = Just Refl
+  patEq (x ::: xs) (y ::: ys) | x == y,
+    Just Refl <- patEq xs ys
+    = Just Refl
+  patEq _ _ = Nothing
+
+-- ** Unit (trivial)
+
+instance Sized () where { type Size () = N0 ;  size _ = SZ }
+
+instance PatEq () () where patEq _ _ = Just Refl
+
+-- ** Pairs
+
+instance (Sized a, Sized b) => Sized (a,b) where
+   type Size (a,b) = Size a + Size b
+   size (x,y) = sPlus (size x) (size y)
+
+instance (PatEq a1 a2, PatEq b1 b2) => PatEq (a1, b1) (a2, b2) where
+   patEq (x1,y1) (x2,y2)
+     | Just Refl <- patEq x1 x2
+     , Just Refl <- patEq y1 y2
+     = Just Refl
+   patEq _ _ = Nothing
+
+------------------------------------------
+
+
diff --git a/src/Rebound/Context.hs b/src/Rebound/Context.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Context.hs
@@ -0,0 +1,57 @@
+-- |
+-- Module       : Rebound.Context
+-- Description  : Typing contexts
+module Rebound.Context(Ctx, emptyC, (+++), (++++)) where
+
+import Rebound.Lib
+import Rebound.Env
+import Rebound.Classes
+
+----------------------------------------------------------------
+-- Typing context utilities for dependently-typed languages
+----------------------------------------------------------------
+
+-- | A typing context maps indices to type in the same scope.
+type Ctx v n = Env v n n
+
+-- This is not weakening --- it increments all variables by one
+shiftC :: forall v n. (SubstVar v) => v n -> v (S n)
+shiftC = applyE @v shift1E
+
+shiftCtx :: (SubstVar v) => Env v n n -> Env v n (S n)
+shiftCtx g = g .>> shift1E
+
+-- | An empty context, that includes no variable assumptions
+emptyC :: Ctx v N0
+emptyC = zeroE
+
+-- | "Snoc" a new definition to the end of the context
+-- All existing types in the context need to be shifted (lazily)
+(+++) :: forall v n. (SubstVar v) => Ctx v n -> v n -> Ctx v (S n)
+g +++ a = applyE @v shift1E a .: (g .>> shift1E)
+
+
+-- | Append contexts. Shifts all indices in the first argument by the length
+-- of the second.
+(++++) :: forall v n n' m. (SNatI n', SubstVar v) => Env v n m -> Env v n' (n' + m) -> Env v (n' + n) (n' + m)
+l ++++ r =
+  let p = snat @n'
+   in r .++ (l .>> shiftNE p)
+
+
+-- Example usage
+
+data Exp n = Star | Var (Fin n) deriving Show
+instance SubstVar Exp where var = Var
+instance Subst Exp Exp where
+    applyE s Star = Star
+    applyE s (Var x) = applyEnv s x
+
+
+-- c :: Ctx Exp N4
+-- x : * , y : x, z : x , w : *
+c = emptyC +++ Star +++ Var FZ +++ Var (FS FZ) +++ Star
+
+-- >>> applyEnv c (FS FZ)
+-- Var 3
+
diff --git a/src/Rebound/Env.hs b/src/Rebound/Env.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Env.hs
@@ -0,0 +1,211 @@
+{-# LANGUAGE UndecidableSuperClasses #-}
+{-# LANGUAGE ViewPatterns #-}
+{-# LANGUAGE PatternSynonyms #-}
+
+-- |
+-- Module      : Rebound.Env
+-- Description : Environments, or mappings from variables to terms
+--
+-- Environments, also called _parallel substitutions_ or _multi-substitutions_,
+-- map all variables in a scope to terms in another scope.
+
+
+module Rebound.Env
+  ( Env,
+    applyEnv,
+    SubstVar (..),
+    Subst (..),
+    Shiftable (..),
+    GSubst (..),
+    gapplyE,
+    applyOpt,
+    transform,
+    zeroE,
+    oneE,
+    singletonE,
+    idE,
+    (.>>),
+    (.:),
+    (.++),
+    head,
+    tail,
+    appendE,
+    up,
+    upN,
+    shift1E,
+    shiftNE,
+    fromVec,
+    toVec,
+    tabulate,
+    fromTable,
+    weakenE',
+    weakenER,
+    shiftFromApplyE,
+  )
+where
+
+-- The concrete implementation of environments can be changed by replacing
+-- this import with an alternative one.
+import Rebound.Env.Lazy
+
+import Rebound.Classes (Shiftable (..))
+import Rebound.Lib
+import Control.Monad
+import Data.Scoped.List (List, pattern Nil, pattern (:<))
+
+import Data.Fin qualified as Fin
+import Data.Map qualified as Map
+import Data.Vec qualified as Vec
+import GHC.Generics hiding (S)
+import Prelude hiding (head, tail)
+
+----------------------------------------------
+-- operations on environments/substitutions
+----------------------------------------------
+
+-- | Convert a function into an environment.
+env :: forall m v n. (SubstVar v, SNatI m) => (Fin m -> v n) -> Env v m n
+env f = fromVec v
+  where
+    v :: Vec m (v n)
+    v = Vec.tabulate f
+
+-- | A singleton environment (single index domain),
+-- which maps that single variable to the provided term.
+oneE :: (SubstVar v) => v n -> Env v (S Z) n
+oneE v = v .: zeroE
+
+-- | An environment that maps index 0 to the provided term, and maps
+-- all other indices to themselves.
+singletonE :: (SubstVar v) => v n -> Env v (S n) n
+singletonE v = v .: idE
+
+-- | An identity environment, which maps all indices to themselves.
+idE :: (SubstVar v) => Env v n n
+idE = shiftNE s0
+
+-- | Append two environments.
+--
+-- The `SNatI` constraint is a runtime witness for the length
+-- of the domain of the first environment.
+(.++) ::
+  (SNatI p, SubstVar v) =>
+  Env v p n ->
+  Env v m n ->
+  Env v (p + m) n
+(.++) = appendE snat
+-- By using a class constraint, this can be an infix operation.
+
+-- | Append two environments, with the length @SNat p@ explicitly required.
+--
+-- If the length is implicitly available, '.++' might be preferable.
+appendE ::
+  (SubstVar v) =>
+  SNat p ->
+  Env v p n ->
+  Env v m n ->
+  Env v (p + m) n
+appendE SZ e1 e2 = e2
+appendE (snat_ -> SS_ p1) e1 e2 =
+  head e1 .: appendE p1 (tail e1) e2
+
+newtype AppendE v m n p = MkAppendE
+  { getAppendE ::
+      Env v p n ->
+      Env v m n ->
+      Env v (p + m) n
+  }
+
+-- | Access the term at index 0.
+head :: (SubstVar v) => Env v (S n) m -> v m
+head f = applyEnv f FZ
+
+-- | Increment all free variables in image by 1.
+shift1E :: (SubstVar v) => Env v n (S n)
+shift1E = shiftNE s1
+
+-- | Increment all free variables by @p@.
+upN ::
+  forall v p m n.
+  (Subst v v) =>
+  SNat p ->
+  Env v m n ->
+  Env v (p + m) (p + n)
+upN p = getUpN @_ @_ @_ @p (withSNat p (induction base step))
+  where
+    base :: UpN v m n Z
+    base = MkUpN id
+    step :: forall p1. UpN v m n p1 -> UpN v m n (S p1)
+    step (MkUpN r) = MkUpN $
+      \e -> var Fin.f0 .: (r e .>> shiftNE s1)
+
+newtype UpN v m n p = MkUpN {getUpN :: Env v m n -> Env v (p + m) (p + n)}
+
+-- | Allow to implement 'Shiftable' using 'Subst'.
+shiftFromApplyE :: forall v c k n. (SubstVar v, Subst v c) => SNat k -> c n -> c (k + n)
+shiftFromApplyE k = applyE @v (shiftNE k)
+
+----------------------------------------------------
+-- Create an environment from a length-indexed
+-- vector of scoped values
+
+-- | Convert an environment to a 'Vec'.
+fromVec :: (SubstVar v) => Vec m (v n) -> Env v m n
+fromVec VNil = zeroE
+fromVec (x ::: vs) = x .: fromVec vs
+
+-- | Convert a 'Vec' to an environment.
+toVec :: (SubstVar v) => SNat m -> Env v m n -> Vec m (v n)
+toVec SZ r = VNil
+toVec m@(snat_ -> SS_ m') r = head r ::: toVec m' (tail r)
+
+----------------------------------------------------------------
+-- show for environments
+----------------------------------------------------------------
+
+instance (SNatI n, Show (v m), SubstVar v) => Show (Env v n m) where
+  show x = show (tabulate x)
+
+-- | Convert an environment to an association list.
+tabulate :: (SNatI n, Subst v v) => Env v n m -> [(Fin n, v m)]
+tabulate r = map (\f -> (f, applyEnv r f)) Fin.universe
+
+-- | Convert an association list to an environment.
+fromTable ::
+  forall n v.
+  (SNatI n, SubstVar v) =>
+  [(Fin n, v n)] ->
+  Env v n n
+fromTable rho =
+  env $ \f -> case lookup f rho of
+    Just t -> t
+    Nothing -> var f
+
+
+
+----------------------------------------------------------------
+-- Subst instances for List and Fin
+----------------------------------------------------------------
+
+-- Scoped List
+
+instance Subst v t => Subst v (List t) where
+  applyE r Nil = Nil
+  applyE r (x :< xs) = applyE r x :< applyE r xs
+
+-- Fin
+
+instance Shiftable Fin where
+  shift = Fin.shiftN
+
+instance SubstVar Fin where
+  var x = x
+
+instance {-# OVERLAPS #-} Subst Fin Fin where
+  applyE = applyEnv
+
+instance {-# OVERLAPPABLE #-} (SubstVar v) => Subst v Fin where
+  applyE = error "BUG: missing isVar definition?"
+
+instance GSubst b Fin where
+  gsubst s f = error "BUG: missing isVar definition?"
diff --git a/src/Rebound/Env/Functional.hs b/src/Rebound/Env/Functional.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Env/Functional.hs
@@ -0,0 +1,121 @@
+{-# LANGUAGE DefaultSignatures #-}
+{-# LANGUAGE UndecidableSuperClasses #-}
+{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
+{-# HLINT ignore "Use lambda-case" #-}
+{-# OPTIONS_HADDOCK hide #-}
+module Rebound.Env.Functional where
+
+-- Represents the environment using a function
+
+
+import Rebound.Lib
+import Data.Fin (Fin(..))
+import qualified Data.Fin as Fin
+import GHC.Generics hiding (S)
+
+
+------------------------------------------------------------------------------
+-- Substitution class declarations
+------------------------------------------------------------------------------
+-- | Well-scoped types that can be the range of
+-- an environment. This should generally be the @Var@
+-- constructor from the syntax.
+class (Subst v v) => SubstVar (v :: Nat -> Type) where
+  var :: Fin n -> v n
+
+-- | Apply the environment throughout a term of
+-- type `c n`, replacing variables with values
+-- of type `v m`
+class (SubstVar v) => Subst v c where
+  applyE :: Env v n m -> c n -> c m
+  default applyE :: (Generic1 c, GSubst v (Rep1 c), SubstVar v) => Env v m n -> c m -> c n
+  applyE = gapplyE
+  {-# INLINE applyE #-}
+  isVar :: c n -> Maybe (v :~: c, Fin n)
+  isVar _ = Nothing
+  {-# INLINE isVar #-}
+
+-- Generic programming
+class GSubst v (e :: Nat -> Type) where
+  gsubst :: Env v m n -> e m -> e n
+
+gapplyE :: forall v c m n. (Generic1 c, GSubst v (Rep1 c), Subst v c) => Env v m n -> c m -> c n
+gapplyE r e | Just (Refl, x) <- isVar @v @c e = applyEnv r x
+gapplyE r e = applyOpt (\s x -> to1 $ gsubst s (from1 x)) r e
+{-# INLINEABLE gapplyE #-}
+
+------------------------------------------------------------------------------
+-- Environment representation as finite function
+------------------------------------------------------------------------------
+
+newtype Env (a :: Nat -> Type) (n :: Nat) (m :: Nat) =
+    Env { applyEnv :: Fin n -> a m }
+
+------------------------------------------------------------------------------
+-- Application
+------------------------------------------------------------------------------
+
+-- | Build an optimized version of applyE (does nothing here)
+applyOpt :: (Env v n m -> c n -> c m) -> (Env v n m -> c n -> c m)
+applyOpt f = f
+{-# INLINEABLE applyOpt #-}
+
+------------------------------------------------------------------------------
+-- Construction and modification
+------------------------------------------------------------------------------
+
+-- | The empty environment (zero domain)
+zeroE :: Env v Z n
+zeroE = Env $ \ x -> case x of {}
+{-# INLINEABLE zeroE #-}
+
+-- make the bound bigger, on the right, but do not change any indices.
+-- this is an identity function
+weakenER :: forall m v n. (SubstVar v) => SNat m -> Env v n (n + m)
+weakenER m = Env $ \x -> var (Fin.weakenFinRight m x)
+{-# INLINEABLE weakenER #-}
+
+-- make the bound bigger, on the left, but do not change any indices.
+-- this is an identity function
+weakenE' :: forall m v n. (SubstVar v) => SNat m -> Env v n (m + n)
+weakenE' m = Env $ \x -> var (Fin.weakenFin m x)
+{-# INLINEABLE weakenE' #-}
+
+-- | increment all free variables by m
+shiftNE :: (SubstVar v) => SNat m -> Env v n (m + n)
+shiftNE m = Env $ \x -> var (Fin.shiftN m x)
+{-# INLINEABLE shiftNE #-}
+
+-- | @cons@ -- extend an environment with a new mapping
+-- for index '0'. All existing mappings are shifted over.
+(.:) :: SubstVar v => v m -> Env v n m -> Env v (S n) m
+ty .: s = Env $ \y -> case y of
+                 FZ -> ty
+                 FS x -> applyEnv s x
+{-# INLINEABLE (.:) #-}
+
+-- | inverse of @cons@ -- remove the first mapping
+tail :: (SubstVar v) => Env v (S n) m -> Env v n m
+tail x = shiftNE s1 .>> x
+{-# INLINEABLE tail #-}
+
+-- | composition: do f then g
+(.>>) :: (Subst v v) => Env v p n -> Env v n m -> Env v p m
+(.>>) = comp
+{-# INLINEABLE (.>>) #-}
+
+-- | smart constructor for composition
+comp :: forall a m n p. SubstVar a =>
+         Env a m n -> Env a n p -> Env a m p
+comp s1 s2 = Env $ \x -> applyE s2 (applyEnv s1 x)
+{-# INLINEABLE comp #-}
+
+-- | modify an environment so that it can go under a binder
+up :: (SubstVar v) => Env v m n -> Env v (S m) (S n)
+up e = var Fin.f0 .: comp e (shiftNE s1)
+{-# INLINEABLE up #-}
+
+-- | mapping operation for range of the environment
+transform :: (forall m. a m -> b m) -> Env a n m -> Env b n m
+transform f g = Env $ \x -> f (applyEnv g x)
+{-# INLINEABLE transform #-}
diff --git a/src/Rebound/Env/Lazy.hs b/src/Rebound/Env/Lazy.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Env/Lazy.hs
@@ -0,0 +1,191 @@
+{-# LANGUAGE DefaultSignatures #-}
+{-# LANGUAGE UndecidableSuperClasses #-}
+{-# OPTIONS_HADDOCK hide #-}
+module Rebound.Env.Lazy where
+
+-- "Defunctionalized" representation of environment
+-- stored values are lazy
+-- *rest* of the environment is strict
+-- Includes optimized composition (Inc and Cons cancel)
+-- Includes Wadler's optimizations for the empty environment
+
+import Rebound.Lib
+import Data.Fin (Fin(..))
+import qualified Data.Fin as Fin
+import GHC.Generics hiding (S)
+import Control.DeepSeq (NFData (..))
+
+------------------------------------------------------------------------------
+-- Substitution class declarations
+------------------------------------------------------------------------------
+-- | Well-scoped types that can be the range of
+-- an environment. This should generally be the @Var@
+-- constructor from the syntax.
+class (Subst v v) => SubstVar (v :: Nat -> Type) where
+  var :: Fin n -> v n
+
+-- | Apply the environment throughout a term of
+-- type `c n`, replacing variables with values
+-- of type `v m`
+class (SubstVar v) => Subst v c where
+  applyE :: Env v n m -> c n -> c m
+  default applyE :: (Generic1 c, GSubst v (Rep1 c), SubstVar v) => Env v m n -> c m -> c n
+  applyE = gapplyE
+  {-# INLINE applyE #-}
+  isVar :: c n -> Maybe (v :~: c, Fin n)
+  isVar _ = Nothing
+  {-# INLINE isVar #-}
+
+-- | Generic programming variant of 'applyE'.
+gapplyE :: forall c v m n. (Generic1 c, GSubst v (Rep1 c), Subst v c) => Env v m n -> c m -> c n
+gapplyE r e | Just (Refl, x) <- isVar @v @c e = applyEnv r x
+gapplyE r e = applyOpt (\s x -> to1 $ gsubst s (from1 x)) r e
+{-# INLINEABLE gapplyE #-}
+
+-- | Generic programming support for 'Subst'.
+class GSubst v (e :: Nat -> Type) where
+  gsubst :: Env v m n -> e m -> e n
+
+
+------------------------------------------------------------------------------
+-- Environment representation
+------------------------------------------------------------------------------
+
+-- | Maps variables in scope @n@ to terms (of type @a@) in scope @m@.
+data Env (a :: Nat -> Type) (n :: Nat) (m :: Nat) where
+  Zero  :: Env a Z n
+  WeakR :: (SNat m) -> Env a n (n + m) --  weaken values in range by m
+  Weak  :: (SNat m) -> Env a n (m + n) --  weaken values in range by m
+  Inc   :: (SNat m) -> Env a n (m + n) --  increment values in range (shift) by m
+  Cons  :: (a m) -> (Env a n m) -> Env a ('S n) m --  extend a substitution (like cons)
+  (:<>) :: (Env a m n) -> (Env a n p) -> Env a m p --  compose substitutions
+
+
+instance (forall n. NFData (a n)) => NFData (Env a n m) where
+  rnf Zero = ()
+  rnf (WeakR m) = rnf m
+  rnf (Weak m) = rnf m
+  rnf (Inc m) = rnf m
+  rnf (Cons x r) = rnf x `seq` rnf r
+  rnf (r1 :<> r2) = rnf r1 `seq` rnf r2
+
+------------------------------------------------------------------------------
+-- Application
+------------------------------------------------------------------------------
+
+-- | Value of the index x in the substitution s
+
+applyEnv :: SubstVar a => Env a n m -> Fin n -> a m
+applyEnv Zero x = Fin.absurd x
+applyEnv (Inc m) x = var (Fin.shiftN m x)
+applyEnv (WeakR m) x = var (Fin.weakenFinRight m x)
+applyEnv (Weak m) x = var (Fin.weakenFin m x)
+applyEnv (Cons ty _s) FZ = ty
+applyEnv (Cons _ty s) (FS x) = applyEnv s x
+applyEnv (s1 :<> s2) x = applyE s2 (applyEnv s1 x)
+{-# INLINEABLE applyEnv #-}
+
+-- | Build an optimized version of applyE.
+-- Checks to see if we are applying the identity substitution first.
+applyOpt :: (Env v n m -> c n -> c m) -> (Env v n m -> c n -> c m)
+applyOpt f (Inc SZ) x = x
+applyOpt f (Weak SZ) x = x
+applyOpt f (WeakR SZ) (x :: c m) =
+  case axiomPlusZ @m of Refl -> x
+applyOpt f r x = f r x
+{-# INLINEABLE applyOpt #-}
+
+------------------------------------------------------------------------------
+-- Construction and modification
+------------------------------------------------------------------------------
+
+-- | The empty environment (zero domain)
+zeroE :: Env v Z n
+zeroE = Zero
+{-# INLINEABLE zeroE #-}
+
+-- | Increase the bound on free variables (on the right), without changing any free variable.
+weakenER :: forall m v n. (SubstVar v) => SNat m -> Env v n (n + m)
+weakenER = WeakR
+{-# INLINEABLE weakenER #-}
+
+-- | Increase the bound on free variables (on the left), without changing any free variable.
+weakenE' :: forall m v n. (SubstVar v) => SNat m -> Env v n (m + n)
+weakenE' = Weak
+{-# INLINEABLE weakenE' #-}
+
+-- | Shift the term, increasing every free variable as well as the bound by the provided amount.
+shiftNE :: (SubstVar v) => (SubstVar v) => SNat m -> Env v n (m + n)
+shiftNE = Inc
+{-# INLINEABLE shiftNE #-}
+
+-- | @cons@ an environment, adding a new mapping
+-- for index '0'. All keys are shifted over.
+(.:) :: v m -> Env v n m -> Env v (S n) m
+(.:) = Cons
+{-# INLINEABLE (.:) #-}
+
+-- | @uncons@ an environment, removing the mapping for index '0'.
+-- All other keys are shifted back.
+tail :: (SubstVar v) => Env v (S n) m -> Env v n m
+tail x = shiftNE s1 .>> x
+{-# INLINEABLE tail #-}
+
+-- | Compose two environments, applying them in sequence (left then right).
+-- Some optimizations will be applied to optimize the resulting environment.
+(.>>) :: (Subst v v) => Env v p n -> Env v n m -> Env v p m
+(.>>) = comp
+{-# INLINEABLE (.>>) #-}
+
+-- | Compose two environments, applying them in sequence (left then right).
+-- Some optimizations will be applied to optimize the resulting environment.
+--
+-- Some of the applied optimizations are:
+-- - Identity environments (e.g., @'shiftNE' SZ@) are eliminated
+-- - Absorbing environments on the right (i.e., 'zeroE') are eliminated
+-- - Compatible environments are fused (e.g., @'weakenER' n@ and @'weakenER' m)
+comp :: forall a m n p. SubstVar a =>
+         Env a m n -> Env a n p -> Env a m p
+comp Zero s = Zero
+comp (Weak (k1 :: SNat m1)) (Weak (k2 :: SNat m2))  =
+  case axiomAssoc @m2 @m1 @m of
+    Refl -> Weak (sPlus k2 k1)
+comp (Weak SZ) s = s
+comp s (Weak SZ) = s
+comp (WeakR (k1 :: SNat m1)) (WeakR (k2 :: SNat m2))  =
+  case axiomAssoc @m @m1 @m2 of
+    Refl -> WeakR (sPlus k1 k2)
+comp (WeakR SZ) s =
+  case axiomPlusZ @m of
+    Refl -> s
+comp s (WeakR SZ) =
+  case axiomPlusZ @n of
+    Refl -> s
+comp (Inc (k1 :: SNat m1)) (Inc (k2 :: SNat m2))  =
+  case axiomAssoc @m2 @m1 @m of
+    Refl -> Inc (sPlus k2 k1)
+comp s (Inc SZ) = s
+comp (Inc SZ) s = s
+comp (Inc (snat_ -> SS_ p1)) (Cons _t p) = comp (Inc p1) p
+comp (s1 :<> s2) s3 = comp s1 (comp s2 s3)
+comp (Cons t s1) s2 = Cons (applyE s2 t) (comp s1 s2)
+comp s1 s2 = s1 :<> s2
+{-# INLINEABLE comp #-}
+
+-- | Adapt an environment to go under a binder.
+up :: (SubstVar v) => Env v m n -> Env v (S m) (S n)
+up (Inc SZ) = Inc SZ
+up (Weak SZ) = Weak SZ
+up (WeakR SZ) = WeakR SZ
+up e = var Fin.f0 .: comp e (Inc s1)
+{-# INLINEABLE up #-}
+
+-- | Map the range of an environment. Has to preserve the scope of the range.
+transform :: (SubstVar b) => (forall m. a m -> b m) -> Env a n m -> Env b n m
+transform f Zero = Zero
+transform f (Weak x) = Weak x
+transform f (WeakR x) = WeakR x
+transform f (Inc x) = Inc x
+transform f (Cons a r) = Cons (f a) (transform f r)
+transform f (r1 :<> r2) = transform f r1 :<> transform f r2
+
diff --git a/src/Rebound/Env/LazyA.hs b/src/Rebound/Env/LazyA.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Env/LazyA.hs
@@ -0,0 +1,182 @@
+{-# LANGUAGE DefaultSignatures #-}
+{-# LANGUAGE UndecidableSuperClasses #-}
+{-# OPTIONS_HADDOCK hide #-}
+module Rebound.Env.LazyA where
+
+-- "Defunctionalized" representation of environment
+-- stored values are lazy
+-- *rest* of the environment is strict
+-- Includes optimized composition (Inc and Cons cancel)
+-- does not include Wadler's optimizations for the empty environment
+
+import Rebound.Lib
+import Data.Fin (Fin(..))
+import qualified Data.Fin as Fin
+import GHC.Generics hiding (S)
+import Control.DeepSeq (NFData (..))
+
+------------------------------------------------------------------------------
+-- Substitution class declarations
+------------------------------------------------------------------------------
+-- | Well-scoped types that can be the range of
+-- an environment. This should generally be the @Var@
+-- constructor from the syntax.
+class (Subst v v) => SubstVar (v :: Nat -> Type) where
+  var :: Fin n -> v n
+
+-- | Apply the environment throughout a term of
+-- type `c n`, replacing variables with values
+-- of type `v m`
+class (SubstVar v) => Subst v c where
+  applyE :: Env v n m -> c n -> c m
+  default applyE :: (Generic1 c, GSubst v (Rep1 c), SubstVar v) => Env v m n -> c m -> c n
+  applyE = gapplyE
+  {-# INLINE applyE #-}
+  isVar :: c n -> Maybe (v :~: c, Fin n)
+  isVar _ = Nothing
+  {-# INLINE isVar #-}
+
+gapplyE :: forall c v m n. (Generic1 c, GSubst v (Rep1 c), Subst v c) => Env v m n -> c m -> c n
+gapplyE r e | Just (Refl, x) <- isVar @v @c e = applyEnv r x
+gapplyE r e = applyOpt (\s x -> to1 $ gsubst s (from1 x)) r e
+{-# INLINEABLE gapplyE #-}
+
+-- Generic programming
+class GSubst v (e :: Nat -> Type) where
+  gsubst :: Env v m n -> e m -> e n
+
+
+
+------------------------------------------------------------------------------
+-- Environment representation
+------------------------------------------------------------------------------
+data Env (a :: Nat -> Type) (n :: Nat) (m :: Nat) where
+  Zero  :: Env a Z n
+  WeakR :: (SNat m) -> Env a n (n + m) --  weaken values in range by m
+  Weak  :: (SNat m) -> Env a n (m + n) --  weaken values in range by m
+  Inc   :: (SNat m) -> Env a n (m + n) --  increment values in range (shift) by m
+  Cons  :: (a m) -> (Env a n m) -> Env a ('S n) m --  extend a substitution (like cons)
+  (:<>) :: (Env a m n) -> (Env a n p) -> Env a m p --  compose substitutions
+
+instance (forall n. NFData (a n)) => NFData (Env a n m) where
+  rnf Zero = ()
+  rnf (WeakR m) = rnf m
+  rnf (Weak m) = rnf m
+  rnf (Inc m) = rnf m
+  rnf (Cons x r) = rnf x `seq` rnf r
+  rnf (r1 :<> r2) = rnf r1 `seq` rnf r2
+
+------------------------------------------------------------------------------
+-- Application
+------------------------------------------------------------------------------
+
+-- | Value of the index x in the substitution s
+applyEnv :: SubstVar a => Env a n m -> Fin n -> a m
+applyEnv Zero x = case x of {}
+applyEnv (Inc m) x = var (Fin.shiftN m x)
+applyEnv (WeakR m) x = var (Fin.weakenFinRight m x)
+applyEnv (Weak m) x = var (Fin.weakenFin m x)
+applyEnv (Cons ty _s) FZ = ty
+applyEnv (Cons _ty s) (FS x) = applyEnv s x
+applyEnv (s1 :<> s2) x = applyE s2 (applyEnv s1 x)
+{-# INLINEABLE applyEnv #-}
+
+-- | Build an optimized version of applyE.
+-- Checks to see if we are applying the identity substitution first.
+applyOpt :: (Env v n m -> c n -> c m) -> (Env v n m -> c n -> c m)
+{- applyOpt f (Inc SZ) x = x
+applyOpt f (Weak SZ) x = x
+applyOpt f (WeakR SZ) (x :: c m) =
+  case axiomPlusZ @m of Refl -> x -}
+applyOpt f r x = f r x
+{-# INLINEABLE applyOpt #-}
+
+------------------------------------------------------------------------------
+-- Construction and modification
+------------------------------------------------------------------------------
+
+-- | The empty environment (zero domain)
+zeroE :: Env v Z n
+zeroE = Zero
+{-# INLINEABLE zeroE #-}
+
+-- make the bound bigger, on the right, but do not change any indices.
+-- this is an identity function
+weakenER :: forall m v n. (SubstVar v) => SNat m -> Env v n (n + m)
+weakenER = WeakR
+{-# INLINEABLE weakenER #-}
+
+-- make the bound bigger, on the left, but do not change any indices.
+-- this is an identity function
+weakenE' :: forall m v n. (SubstVar v) => SNat m -> Env v n (m + n)
+weakenE' = Weak
+{-# INLINEABLE weakenE' #-}
+
+-- | increment all free variables by m
+shiftNE :: (SubstVar v) => SNat m -> Env v n (m + n)
+shiftNE = Inc
+{-# INLINEABLE shiftNE #-}
+
+-- | @cons@ -- extend an environment with a new mapping
+-- for index '0'. All existing mappings are shifted over.
+(.:) :: v m -> Env v n m -> Env v (S n) m
+(.:) = Cons
+{-# INLINEABLE (.:) #-}
+
+
+-- | inverse of @cons@ -- remove the first mapping
+tail :: (SubstVar v) => Env v (S n) m -> Env v n m
+tail x = shiftNE s1 .>> x
+{-# INLINEABLE tail #-}
+
+-- | composition: do f then g
+(.>>) :: (Subst v v) => Env v p n -> Env v n m -> Env v p m
+(.>>) = comp
+{-# INLINEABLE (.>>) #-}
+
+-- | smart constructor for composition
+comp :: forall a m n p. SubstVar a =>
+         Env a m n -> Env a n p -> Env a m p
+comp Zero s = Zero
+comp (Weak (k1 :: SNat m1)) (Weak (k2 :: SNat m2))  =
+  case axiomAssoc @m2 @m1 @m of
+    Refl -> Weak (sPlus k2 k1)
+comp (Weak SZ) s = s
+comp s (Weak SZ) = s
+comp (WeakR (k1 :: SNat m1)) (WeakR (k2 :: SNat m2))  =
+  case axiomAssoc @m @m1 @m2 of
+    Refl -> WeakR (sPlus k1 k2)
+comp (WeakR SZ) s =
+  case axiomPlusZ @m of
+    Refl -> s
+comp s (WeakR SZ) =
+  case axiomPlusZ @n of
+    Refl -> s
+comp (Inc (k1 :: SNat m1)) (Inc (k2 :: SNat m2))  =
+  case axiomAssoc @m2 @m1 @m of
+    Refl -> Inc (sPlus k2 k1)
+comp s (Inc SZ) = s
+comp (Inc SZ) s = s
+comp (Inc (snat_ -> SS_ p1)) (Cons _t p) = comp (Inc p1) p
+comp (s1 :<> s2) s3 = comp s1 (comp s2 s3)
+comp (Cons t s1) s2 = Cons (applyE s2 t) (comp s1 s2)
+comp s1 s2 = s1 :<> s2
+{-# INLINEABLE comp #-}
+
+-- | modify an environment so that it can go under a binder
+up :: (SubstVar v) => Env v m n -> Env v (S m) (S n)
+{- up (Inc SZ) = Inc SZ
+up (Weak SZ) = Weak SZ
+up (WeakR SZ) = WeakR SZ  -}
+up e = var Fin.f0 .: comp e (Inc s1)
+{-# INLINEABLE up #-}
+
+-- | mapping operation for range of the environment
+transform :: (SubstVar b) => (forall m. a m -> b m) -> Env a n m -> Env b n m
+transform f Zero = Zero
+transform f (Weak x) = Weak x
+transform f (WeakR x) = WeakR x
+transform f (Inc x) = Inc x
+transform f (Cons a r) = Cons (f a) (transform f r)
+transform f (r1 :<> r2) = transform f r1 :<> transform f r2
+
diff --git a/src/Rebound/Env/LazyB.hs b/src/Rebound/Env/LazyB.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Env/LazyB.hs
@@ -0,0 +1,153 @@
+{-# LANGUAGE DefaultSignatures #-}
+{-# LANGUAGE UndecidableSuperClasses #-}
+{-# OPTIONS_HADDOCK hide #-}
+module Rebound.Env.LazyB where
+
+-- "Defunctionalized" representation of environment
+-- stored values are lazy
+-- *rest* of the environment is lazy
+-- No optimized composition (Inc and Cons cancel)
+-- No Wadler's optimizations for the empty environment
+
+import Rebound.Lib
+import Data.Fin (Fin(..))
+import qualified Data.Fin as Fin
+import GHC.Generics hiding (S)
+import Control.DeepSeq (NFData (..))
+
+------------------------------------------------------------------------------
+-- Substitution class declarations
+------------------------------------------------------------------------------
+-- | Well-scoped types that can be the range of
+-- an environment. This should generally be the @Var@
+-- constructor from the syntax.
+class (Subst v v) => SubstVar (v :: Nat -> Type) where
+  var :: Fin n -> v n
+
+-- | Apply the environment throughout a term of
+-- type `c n`, replacing variables with values
+-- of type `v m`
+class (SubstVar v) => Subst v c where
+  applyE :: Env v n m -> c n -> c m
+  default applyE :: (Generic1 c, GSubst v (Rep1 c), SubstVar v) => Env v m n -> c m -> c n
+  applyE = gapplyE
+  {-# INLINE applyE #-}
+  isVar :: c n -> Maybe (v :~: c, Fin n)
+  isVar _ = Nothing
+  {-# INLINE isVar #-}
+
+gapplyE :: forall c v m n. (Generic1 c, GSubst v (Rep1 c), Subst v c) => Env v m n -> c m -> c n
+gapplyE r e | Just (Refl, x) <- isVar @v @c e = applyEnv r x
+gapplyE r e = applyOpt (\s x -> to1 $ gsubst s (from1 x)) r e
+{-# INLINEABLE gapplyE #-}
+
+-- Generic programming
+class GSubst v (e :: Nat -> Type) where
+  gsubst :: Env v m n -> e m -> e n
+
+------------------------------------------------------------------------------
+-- Environment representation
+------------------------------------------------------------------------------
+data Env (a :: Nat -> Type) (n :: Nat) (m :: Nat) where
+  Zero  :: Env a Z n
+  WeakR :: (SNat m) -> Env a n (n + m) --  weaken values in range by m
+  Weak  :: (SNat m) -> Env a n (m + n) --  weaken values in range by m
+  Inc   :: (SNat m) -> Env a n (m + n) --  increment values in range (shift) by m
+  Cons  :: (a m) -> (Env a n m) -> Env a ('S n) m --  extend a substitution (like cons)
+  (:<>) :: (Env a m n) -> (Env a n p) -> Env a m p --  compose substitutions
+
+
+instance (forall n. NFData (a n)) => NFData (Env a n m) where
+  rnf Zero = ()
+  rnf (WeakR m) = rnf m
+  rnf (Weak m) = rnf m
+  rnf (Inc m) = rnf m
+  rnf (Cons x r) = rnf x `seq` rnf r
+  rnf (r1 :<> r2) = rnf r1 `seq` rnf r2
+
+------------------------------------------------------------------------------
+-- Application
+------------------------------------------------------------------------------
+
+-- | Value of the index x in the substitution s
+applyEnv :: SubstVar a => Env a n m -> Fin n -> a m
+applyEnv Zero x = case x of {}
+applyEnv (Inc m) x = var (Fin.shiftN m x)
+applyEnv (WeakR m) x = var (Fin.weakenFinRight m x)
+applyEnv (Weak m) x = var (Fin.weakenFin m x)
+applyEnv (Cons ty _s) FZ = ty
+applyEnv (Cons _ty s) (FS x) = applyEnv s x
+applyEnv (s1 :<> s2) x = applyE s2 (applyEnv s1 x)
+{-# INLINEABLE applyEnv #-}
+
+-- | Build an optimized version of applyE.
+-- Checks to see if we are applying the identity substitution first.
+applyOpt :: (Env v n m -> c n -> c m) -> (Env v n m -> c n -> c m)
+applyOpt f (Inc SZ) x = x
+applyOpt f (Weak SZ) x = x
+applyOpt f (WeakR SZ) (x :: c m) =
+  case axiomPlusZ @m of Refl -> x
+applyOpt f r x = f r x
+{-# INLINEABLE applyOpt #-}
+
+------------------------------------------------------------------------------
+-- Construction and modification
+------------------------------------------------------------------------------
+
+-- | The empty environment (zero domain)
+zeroE :: Env v Z n
+zeroE = Zero
+{-# INLINEABLE zeroE #-}
+
+-- make the bound bigger, on the right, but do not change any indices.
+-- this is an identity function
+weakenER :: forall m v n. (SubstVar v) => SNat m -> Env v n (n + m)
+weakenER = WeakR
+{-# INLINEABLE weakenER #-}
+
+-- make the bound bigger, on the left, but do not change any indices.
+-- this is an identity function
+weakenE' :: forall m v n. (SubstVar v) => SNat m -> Env v n (m + n)
+weakenE' = Weak
+{-# INLINEABLE weakenE' #-}
+
+-- | increment all free variables by m
+shiftNE :: (SubstVar v) => SNat m -> Env v n (m + n)
+shiftNE = Inc
+{-# INLINEABLE shiftNE #-}
+
+-- | @cons@ -- extend an environment with a new mapping
+-- for index '0'. All existing mappings are shifted over.
+(.:) :: (SubstVar v) => v m -> Env v n m -> Env v (S n) m
+(.:) = Cons
+{-# INLINEABLE (.:) #-}
+
+
+-- | inverse of @cons@ -- remove the first mapping
+tail :: (SubstVar v) => Env v (S n) m -> Env v n m
+tail x = shiftNE s1 .>> x
+{-# INLINEABLE tail #-}
+
+-- | composition: do f then g
+-- No optimizations here
+(.>>) :: (Subst v v) => Env v p n -> Env v n m -> Env v p m
+(.>>) = (:<>)
+{-# INLINEABLE (.>>) #-}
+
+-- | modify an environment so that it can go under a binder
+up :: (SubstVar v) => Env v m n -> Env v (S m) (S n)
+up (Inc SZ) = Inc SZ
+up (Weak SZ) = Weak SZ
+up (WeakR SZ) = WeakR SZ
+up e = var Fin.f0 .: (e :<> Inc s1)
+{-# INLINEABLE up #-}
+
+-- | mapping operation for range of the environment
+transform :: (SubstVar b) => (forall m. a m -> b m) -> Env a n m -> Env b n m
+transform f Zero = Zero
+transform f (Weak x) = Weak x
+transform f (WeakR x) = WeakR x
+transform f (Inc x) = Inc x
+transform f (Cons a r) = Cons (f a) (transform f r)
+transform f (r1 :<> r2) = transform f r1 :<> transform f r2
+
diff --git a/src/Rebound/Env/Strict.hs b/src/Rebound/Env/Strict.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Env/Strict.hs
@@ -0,0 +1,188 @@
+{-# LANGUAGE DefaultSignatures #-}
+{-# LANGUAGE UndecidableSuperClasses #-}
+{-# OPTIONS_HADDOCK hide #-}
+module Rebound.Env.Strict where
+
+-- "Defunctionalized" representation of environment
+-- stored values are lazy
+-- *rest* of the environment is strict
+-- Includes optimized composition (Inc and Cons cancel)
+-- Includes Wadler's optimizations for the empty environment
+
+import Rebound.Lib
+import Data.Fin (Fin(..))
+import qualified Data.Fin as Fin
+import GHC.Generics hiding (S)
+import Control.DeepSeq (NFData (..))
+
+------------------------------------------------------------------------------
+-- Substitution class declarations
+------------------------------------------------------------------------------
+-- | Well-scoped types that can be the range of
+-- an environment. This should generally be the @Var@
+-- constructor from the syntax.
+class (Subst v v) => SubstVar (v :: Nat -> Type) where
+  var :: Fin n -> v n
+
+
+-- | Apply the environment throughout a term of
+-- type `c n`, replacing variables with values
+-- of type `v m`
+class (SubstVar v) => Subst v c where
+  applyE :: Env v n m -> c n -> c m
+  default applyE :: (Generic1 c, GSubst v (Rep1 c), SubstVar v) => Env v m n -> c m -> c n
+  applyE = gapplyE
+  {-# INLINE applyE #-}
+  isVar :: c n -> Maybe (v :~: c, Fin n)
+  isVar _ = Nothing
+  {-# INLINE isVar #-}
+
+-- Generic programming
+class GSubst v (e :: Nat -> Type) where
+  gsubst :: Env v m n -> e m -> e n
+
+gapplyE :: forall c v m n. (Generic1 c, GSubst v (Rep1 c), Subst v c) => Env v m n -> c m -> c n
+gapplyE r e | Just (Refl, x) <- isVar @v @c e = applyEnv r x
+gapplyE r e = applyOpt (\s x -> to1 $ gsubst s (from1 x)) r e
+{-# INLINEABLE gapplyE #-}
+
+------------------------------------------------------------------------------
+-- Environment representation
+------------------------------------------------------------------------------
+data Env (a :: Nat -> Type) (n :: Nat) (m :: Nat) where
+  Zero  :: Env a Z n
+  WeakR :: !(SNat m) -> Env a n (n + m) --  weaken values in range by m
+  Weak  :: !(SNat m) -> Env a n (m + n) --  weaken values in range by m
+  Inc   :: !(SNat m) -> Env a n (m + n) --  increment values in range (shift) by m
+  Cons  :: (a m) -> !(Env a n m) -> Env a ('S n) m --  extend a substitution (like cons)
+  (:<>) :: !(Env a m n) -> !(Env a n p) -> Env a m p --  compose substitutions
+
+instance (forall n. NFData (a n)) => NFData (Env a n m) where
+  rnf Zero = ()
+  rnf (WeakR m) = rnf m
+  rnf (Weak m) = rnf m
+  rnf (Inc m) = rnf m
+  rnf (Cons x r) = rnf x `seq` rnf r
+  rnf (r1 :<> r2) = rnf r1 `seq` rnf r2
+
+------------------------------------------------------------------------------
+-- Application
+------------------------------------------------------------------------------
+
+-- | Value of the index x in the substitution s
+
+applyEnv :: SubstVar a => Env a n m -> Fin n -> a m
+applyEnv Zero x = Fin.absurd x
+applyEnv (Inc m) x = var (Fin.shiftN m x)
+applyEnv (WeakR m) x = var (Fin.weakenFinRight m x)
+applyEnv (Weak m) x = var (Fin.weakenFin m x)
+applyEnv (Cons ty _s) FZ = ty
+applyEnv (Cons _ty s) (FS x) = applyEnv s x
+applyEnv (s1 :<> s2) x = applyE s2 (applyEnv s1 x)
+{-# INLINEABLE applyEnv #-}
+
+-- | Build an optimized version of applyE.
+-- Checks to see if we are applying the identity substitution first.
+applyOpt :: (Env v n m -> c n -> c m) -> (Env v n m -> c n -> c m)
+applyOpt f (Inc SZ) x = x
+applyOpt f (Weak SZ) x = x
+applyOpt f (WeakR SZ) (x :: c m) =
+  case axiomPlusZ @m of Refl -> x
+applyOpt f r x = f r x
+{-# INLINEABLE applyOpt #-}
+
+------------------------------------------------------------------------------
+-- Construction and modification
+------------------------------------------------------------------------------
+
+-- | The empty environment (zero domain)
+zeroE :: Env v Z n
+zeroE = Zero
+{-# INLINEABLE zeroE #-}
+
+-- make the bound bigger, on the right, but do not change any indices.
+-- this is an identity function
+weakenER :: forall m v n. (SubstVar v) => SNat m -> Env v n (n + m)
+weakenER = WeakR
+{-# INLINEABLE weakenER #-}
+
+-- make the bound bigger, on the left, but do not change any indices.
+-- this is an identity function
+weakenE' :: forall m v n. (SubstVar v) => SNat m -> Env v n (m + n)
+weakenE' = Weak
+{-# INLINEABLE weakenE' #-}
+
+-- | increment all free variables by m
+shiftNE :: (SubstVar v) => (SubstVar v) => SNat m -> Env v n (m + n)
+shiftNE = Inc
+{-# INLINEABLE shiftNE #-}
+
+-- | @cons@ -- extend an environment with a new mapping
+-- for index '0'. All existing mappings are shifted over.
+(.:) :: v m -> Env v n m -> Env v (S n) m
+(.:) = Cons
+{-# INLINEABLE (.:) #-}
+
+
+-- | inverse of @cons@ -- remove the first mapping
+tail :: (SubstVar v) => Env v (S n) m -> Env v n m
+tail x = shiftNE s1 .>> x
+{-# INLINEABLE tail #-}
+
+-- | composition: do f then g
+(.>>) :: (Subst v v) => Env v p n -> Env v n m -> Env v p m
+(.>>) = comp
+{-# INLINEABLE (.>>) #-}
+
+-- | smart constructor for composition
+-- Names of some cases are taken from Abadi et. al "Explicit Substitutions"
+comp :: forall a m n p. SubstVar a =>
+         Env a m n -> Env a n p -> Env a m p
+comp Zero s = Zero
+comp (Weak (k1 :: SNat m1)) (Weak (k2 :: SNat m2))  =
+  case axiomAssoc @m2 @m1 @m of
+    Refl -> Weak (sPlus k2 k1)
+comp (Weak SZ) s = s
+comp s (Weak SZ) = s
+comp (WeakR (k1 :: SNat m1)) (WeakR (k2 :: SNat m2))  =
+  case axiomAssoc @m @m1 @m2 of
+    Refl -> WeakR (sPlus k1 k2)
+comp (WeakR SZ) s =
+  case axiomPlusZ @m of
+    Refl -> s
+comp s (WeakR SZ) =
+  case axiomPlusZ @n of
+    Refl -> s
+comp (Inc (k1 :: SNat m1)) (Inc (k2 :: SNat m2))  =
+  case axiomAssoc @m2 @m1 @m of
+    Refl -> Inc (sPlus k2 k1)
+-- (sort of) ShiftId
+comp s (Inc SZ) = s
+-- IdL
+comp (Inc SZ) s = s
+-- ShiftCons
+comp (Inc (snat_ -> SS_ p1)) (Cons _t p) = comp (Inc p1) p
+-- Ass
+comp (s1 :<> s2) s3 = comp s1 (comp s2 s3)
+-- Map
+comp (Cons t s1) s2 = Cons (applyE s2 t) (comp s1 s2)
+comp s1 s2 = s1 :<> s2
+{-# INLINEABLE comp #-}
+
+-- | modify an environment so that it can go under a binder
+up :: (SubstVar v) => Env v m n -> Env v (S m) (S n)
+up (Inc SZ) = Inc SZ
+up (Weak SZ) = Weak SZ
+up (WeakR SZ) = WeakR SZ
+up e = var Fin.f0 .: comp e (Inc s1)
+{-# INLINEABLE up #-}
+
+-- | mapping operation for range of the environment
+transform :: (SubstVar b) => (forall m. a m -> b m) -> Env a n m -> Env b n m
+transform f Zero = Zero
+transform f (Weak x) = Weak x
+transform f (WeakR x) = WeakR x
+transform f (Inc x) = Inc x
+transform f (Cons a r) = Cons (f a) (transform f r)
+transform f (r1 :<> r2) = transform f r1 :<> transform f r2
+
diff --git a/src/Rebound/Env/StrictA.hs b/src/Rebound/Env/StrictA.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Env/StrictA.hs
@@ -0,0 +1,180 @@
+{-# LANGUAGE DefaultSignatures #-}
+{-# LANGUAGE UndecidableSuperClasses #-}
+{-# OPTIONS_HADDOCK hide #-}
+module Rebound.Env.StrictA where
+
+-- "Defunctionalized" representation of environment
+-- stored values are lazy
+-- *rest* of the environment is strict
+-- Includes optimized composition (Inc and Cons cancel)
+-- does not include Wadler's optimizations for the empty environment
+
+import Rebound.Lib
+import Data.Fin (Fin(..))
+import qualified Data.Fin as Fin
+import GHC.Generics hiding (S)
+import Control.DeepSeq (NFData (..))
+
+------------------------------------------------------------------------------
+-- Substitution class declarations
+------------------------------------------------------------------------------
+-- | Well-scoped types that can be the range of
+-- an environment. This should generally be the @Var@
+-- constructor from the syntax.
+class (Subst v v) => SubstVar (v :: Nat -> Type) where
+  var :: Fin n -> v n
+
+-- | Apply the environment throughout a term of
+-- type `c n`, replacing variables with values
+-- of type `v m`
+class (SubstVar v) => Subst v c where
+  applyE :: Env v n m -> c n -> c m
+  default applyE :: (Generic1 c, GSubst v (Rep1 c), SubstVar v) => Env v m n -> c m -> c n
+  applyE = gapplyE
+  {-# INLINE applyE #-}
+  isVar :: c n -> Maybe (v :~: c, Fin n)
+  isVar _ = Nothing
+  {-# INLINE isVar #-}
+
+gapplyE :: forall c v m n. (Generic1 c, GSubst v (Rep1 c), Subst v c) => Env v m n -> c m -> c n
+gapplyE r e | Just (Refl, x) <- isVar @v @c e = applyEnv r x
+gapplyE r e = applyOpt (\s x -> to1 $ gsubst s (from1 x)) r e
+{-# INLINEABLE gapplyE #-}
+
+-- Generic programming
+class GSubst v (e :: Nat -> Type) where
+  gsubst :: Env v m n -> e m -> e n
+
+------------------------------------------------------------------------------
+-- Environment representation
+------------------------------------------------------------------------------
+data Env (a :: Nat -> Type) (n :: Nat) (m :: Nat) where
+  Zero  :: Env a Z n
+  WeakR :: !(SNat m) -> Env a n (n + m) --  weaken values in range by m
+  Weak  :: !(SNat m) -> Env a n (m + n) --  weaken values in range by m
+  Inc   :: !(SNat m) -> Env a n (m + n) --  increment values in range (shift) by m
+  Cons  :: (a m) -> !(Env a n m) -> Env a ('S n) m --  extend a substitution (like cons)
+  (:<>) :: !(Env a m n) -> !(Env a n p) -> Env a m p --  compose substitutions
+
+instance (forall n. NFData (a n)) => NFData (Env a n m) where
+  rnf Zero = ()
+  rnf (WeakR m) = rnf m
+  rnf (Weak m) = rnf m
+  rnf (Inc m) = rnf m
+  rnf (Cons x r) = rnf x `seq` rnf r
+  rnf (r1 :<> r2) = rnf r1 `seq` rnf r2
+
+------------------------------------------------------------------------------
+-- Application
+------------------------------------------------------------------------------
+
+-- | Value of the index x in the substitution s
+applyEnv :: SubstVar a => Env a n m -> Fin n -> a m
+applyEnv Zero x = case x of {}
+applyEnv (Inc m) x = var (Fin.shiftN m x)
+applyEnv (WeakR m) x = var (Fin.weakenFinRight m x)
+applyEnv (Weak m) x = var (Fin.weakenFin m x)
+applyEnv (Cons ty _s) FZ = ty
+applyEnv (Cons _ty s) (FS x) = applyEnv s x
+applyEnv (s1 :<> s2) x = applyE s2 (applyEnv s1 x)
+{-# INLINEABLE applyEnv #-}
+
+-- | Build an optimized version of applyE.
+-- Checks to see if we are applying the identity substitution first.
+applyOpt :: (Env v n m -> c n -> c m) -> (Env v n m -> c n -> c m)
+{- applyOpt f (Inc SZ) x = x
+applyOpt f (Weak SZ) x = x
+applyOpt f (WeakR SZ) (x :: c m) =
+  case axiomPlusZ @m of Refl -> x -}
+applyOpt f r x = f r x
+{-# INLINEABLE applyOpt #-}
+
+------------------------------------------------------------------------------
+-- Construction and modification
+------------------------------------------------------------------------------
+
+-- | The empty environment (zero domain)
+zeroE :: Env v Z n
+zeroE = Zero
+{-# INLINEABLE zeroE #-}
+
+-- make the bound bigger, on the right, but do not change any indices.
+-- this is an identity function
+weakenER :: forall m v n. (SubstVar v) => SNat m -> Env v n (n + m)
+weakenER = WeakR
+{-# INLINEABLE weakenER #-}
+
+-- make the bound bigger, on the left, but do not change any indices.
+-- this is an identity function
+weakenE' :: forall m v n. (SubstVar v) => SNat m -> Env v n (m + n)
+weakenE' = Weak
+{-# INLINEABLE weakenE' #-}
+
+-- | increment all free variables by m
+shiftNE :: (SubstVar v) => SNat m -> Env v n (m + n)
+shiftNE = Inc
+{-# INLINEABLE shiftNE #-}
+
+-- | @cons@ -- extend an environment with a new mapping
+-- for index '0'. All existing mappings are shifted over.
+(.:) :: v m -> Env v n m -> Env v (S n) m
+(.:) = Cons
+{-# INLINEABLE (.:) #-}
+
+
+-- | inverse of @cons@ -- remove the first mapping
+tail :: (SubstVar v) => Env v (S n) m -> Env v n m
+tail x = shiftNE s1 .>> x
+{-# INLINEABLE tail #-}
+
+-- | composition: do f then g
+(.>>) :: (Subst v v) => Env v p n -> Env v n m -> Env v p m
+(.>>) = comp
+{-# INLINEABLE (.>>) #-}
+
+-- | smart constructor for composition
+comp :: forall a m n p. SubstVar a =>
+         Env a m n -> Env a n p -> Env a m p
+comp Zero s = Zero
+comp (Weak (k1 :: SNat m1)) (Weak (k2 :: SNat m2))  =
+  case axiomAssoc @m2 @m1 @m of
+    Refl -> Weak (sPlus k2 k1)
+comp (Weak SZ) s = s
+comp s (Weak SZ) = s
+comp (WeakR (k1 :: SNat m1)) (WeakR (k2 :: SNat m2))  =
+  case axiomAssoc @m @m1 @m2 of
+    Refl -> WeakR (sPlus k1 k2)
+comp (WeakR SZ) s =
+  case axiomPlusZ @m of
+    Refl -> s
+comp s (WeakR SZ) =
+  case axiomPlusZ @n of
+    Refl -> s
+comp (Inc (k1 :: SNat m1)) (Inc (k2 :: SNat m2))  =
+  case axiomAssoc @m2 @m1 @m of
+    Refl -> Inc (sPlus k2 k1)
+comp s (Inc SZ) = s
+comp (Inc SZ) s = s
+comp (Inc (snat_ -> SS_ p1)) (Cons _t p) = comp (Inc p1) p
+comp (s1 :<> s2) s3 = comp s1 (comp s2 s3)
+comp (Cons t s1) s2 = Cons (applyE s2 t) (comp s1 s2)
+comp s1 s2 = s1 :<> s2
+{-# INLINEABLE comp #-}
+
+-- | modify an environment so that it can go under a binder
+up :: (SubstVar v) => Env v m n -> Env v (S m) (S n)
+{- up (Inc SZ) = Inc SZ
+up (Weak SZ) = Weak SZ
+up (WeakR SZ) = WeakR SZ  -}
+up e = var Fin.f0 .: comp e (Inc s1)
+{-# INLINEABLE up #-}
+
+-- | mapping operation for range of the environment
+transform :: (SubstVar b) => (forall m. a m -> b m) -> Env a n m -> Env b n m
+transform f Zero = Zero
+transform f (Weak x) = Weak x
+transform f (WeakR x) = WeakR x
+transform f (Inc x) = Inc x
+transform f (Cons a r) = Cons (f a) (transform f r)
+transform f (r1 :<> r2) = transform f r1 :<> transform f r2
+
diff --git a/src/Rebound/Env/StrictB.hs b/src/Rebound/Env/StrictB.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Env/StrictB.hs
@@ -0,0 +1,151 @@
+{-# LANGUAGE DefaultSignatures #-}
+{-# LANGUAGE UndecidableSuperClasses #-}
+{-# OPTIONS_HADDOCK hide #-}
+module Rebound.Env.StrictB where
+
+-- "Defunctionalized" representation of environment
+-- stored values are lazy
+-- *rest* of the environment is strict
+-- No optimized composition (Inc and Cons cancel)
+-- No Wadler's optimizations for the empty environment
+
+import Rebound.Lib
+import Data.Fin (Fin(..))
+import qualified Data.Fin as Fin
+import GHC.Generics hiding (S)
+import Control.DeepSeq (NFData (..))
+
+------------------------------------------------------------------------------
+-- Substitution class declarations
+------------------------------------------------------------------------------
+-- | Well-scoped types that can be the range of
+-- an environment. This should generally be the @Var@
+-- constructor from the syntax.
+class (Subst v v) => SubstVar (v :: Nat -> Type) where
+  var :: Fin n -> v n
+
+-- | Apply the environment throughout a term of
+-- type `c n`, replacing variables with values
+-- of type `v m`
+class (SubstVar v) => Subst v c where
+  applyE :: Env v n m -> c n -> c m
+  default applyE :: (Generic1 c, GSubst v (Rep1 c), SubstVar v) => Env v m n -> c m -> c n
+  applyE = gapplyE
+  {-# INLINE applyE #-}
+  isVar :: c n -> Maybe (v :~: c, Fin n)
+  isVar _ = Nothing
+  {-# INLINE isVar #-}
+
+gapplyE :: forall c v m n. (Generic1 c, GSubst v (Rep1 c), Subst v c) => Env v m n -> c m -> c n
+gapplyE r e | Just (Refl, x) <- isVar @v @c e = applyEnv r x
+gapplyE r e = applyOpt (\s x -> to1 $ gsubst s (from1 x)) r e
+{-# INLINEABLE gapplyE #-}
+
+-- Generic programming
+class GSubst v (e :: Nat -> Type) where
+  gsubst :: Env v m n -> e m -> e n
+
+------------------------------------------------------------------------------
+-- Environment representation
+------------------------------------------------------------------------------
+data Env (a :: Nat -> Type) (n :: Nat) (m :: Nat) where
+  Zero  :: Env a Z n
+  WeakR :: !(SNat m) -> Env a n (n + m) --  weaken values in range by m
+  Weak  :: !(SNat m) -> Env a n (m + n) --  weaken values in range by m
+  Inc   :: !(SNat m) -> Env a n (m + n) --  increment values in range (shift) by m
+  Cons  :: (a m) -> !(Env a n m) -> Env a ('S n) m --  extend a substitution (like cons)
+  (:<>) :: !(Env a m n) -> !(Env a n p) -> Env a m p --  compose substitutions
+
+instance (forall n. NFData (a n)) => NFData (Env a n m) where
+  rnf Zero = ()
+  rnf (WeakR m) = rnf m
+  rnf (Weak m) = rnf m
+  rnf (Inc m) = rnf m
+  rnf (Cons x r) = rnf x `seq` rnf r
+  rnf (r1 :<> r2) = rnf r1 `seq` rnf r2
+------------------------------------------------------------------------------
+-- Application
+------------------------------------------------------------------------------
+
+-- | Value of the index x in the substitution s
+applyEnv :: SubstVar a => Env a n m -> Fin n -> a m
+applyEnv Zero x = case x of {}
+applyEnv (Inc m) x = var (Fin.shiftN m x)
+applyEnv (WeakR m) x = var (Fin.weakenFinRight m x)
+applyEnv (Weak m) x = var (Fin.weakenFin m x)
+applyEnv (Cons ty _s) FZ = ty
+applyEnv (Cons _ty s) (FS x) = applyEnv s x
+applyEnv (s1 :<> s2) x = applyE s2 (applyEnv s1 x)
+{-# INLINEABLE applyEnv #-}
+
+-- | Build an optimized version of applyE.
+-- Checks to see if we are applying the identity substitution first.
+applyOpt :: (Env v n m -> c n -> c m) -> (Env v n m -> c n -> c m)
+applyOpt f (Inc SZ) x = x
+applyOpt f (Weak SZ) x = x
+applyOpt f (WeakR SZ) (x :: c m) =
+  case axiomPlusZ @m of Refl -> x
+applyOpt f r x = f r x
+{-# INLINEABLE applyOpt #-}
+
+------------------------------------------------------------------------------
+-- Construction and modification
+------------------------------------------------------------------------------
+
+-- | The empty environment (zero domain)
+zeroE :: Env v Z n
+zeroE = Zero
+{-# INLINEABLE zeroE #-}
+
+-- make the bound bigger, on the right, but do not change any indices.
+-- this is an identity function
+weakenER :: forall m v n. (SubstVar v) => SNat m -> Env v n (n + m)
+weakenER = WeakR
+{-# INLINEABLE weakenER #-}
+
+-- make the bound bigger, on the left, but do not change any indices.
+-- this is an identity function
+weakenE' :: forall m v n. (SubstVar v) => SNat m -> Env v n (m + n)
+weakenE' = Weak
+{-# INLINEABLE weakenE' #-}
+
+-- | increment all free variables by m
+shiftNE :: (SubstVar v) => SNat m -> Env v n (m + n)
+shiftNE = Inc
+{-# INLINEABLE shiftNE #-}
+
+-- | @cons@ -- extend an environment with a new mapping
+-- for index '0'. All existing mappings are shifted over.
+(.:) :: (SubstVar v) => v m -> Env v n m -> Env v (S n) m
+(.:) = Cons
+{-# INLINEABLE (.:) #-}
+
+
+-- | inverse of @cons@ -- remove the first mapping
+tail :: (SubstVar v) => Env v (S n) m -> Env v n m
+tail x = shiftNE s1 .>> x
+{-# INLINEABLE tail #-}
+
+-- | composition: do f then g
+-- No optimizations here
+(.>>) :: (Subst v v) => Env v p n -> Env v n m -> Env v p m
+(.>>) = (:<>)
+{-# INLINEABLE (.>>) #-}
+
+-- | modify an environment so that it can go under a binder
+up :: (SubstVar v) => Env v m n -> Env v (S m) (S n)
+up (Inc SZ) = Inc SZ
+up (Weak SZ) = Weak SZ
+up (WeakR SZ) = WeakR SZ
+up e = var Fin.f0 .: (e :<> Inc s1)
+{-# INLINEABLE up #-}
+
+-- | mapping operation for range of the environment
+transform :: (SubstVar b) => (forall m. a m -> b m) -> Env a n m -> Env b n m
+transform f Zero = Zero
+transform f (Weak x) = Weak x
+transform f (WeakR x) = WeakR x
+transform f (Inc x) = Inc x
+transform f (Cons a r) = Cons (f a) (transform f r)
+transform f (r1 :<> r2) = transform f r1 :<> transform f r2
+
diff --git a/src/Rebound/Generics.hs b/src/Rebound/Generics.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Generics.hs
@@ -0,0 +1,125 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+module Rebound.Generics where
+    
+import GHC.Generics hiding (S)
+import Rebound.Env
+import Rebound.Classes
+import Data.Set qualified as Set
+
+--------------------------------------------
+-- Generic implementation of Subst class
+--------------------------------------------
+
+-- Constant types
+instance GSubst v (K1 i c) where
+  gsubst s (K1 c) = K1 c
+  {-# INLINE gsubst #-}
+
+instance GSubst v U1 where
+  gsubst _s U1 = U1
+  {-# INLINE gsubst #-}
+
+instance (GSubst b f) => GSubst b (M1 i c f) where
+  gsubst s = M1 . gsubst s . unM1
+  {-# INLINE gsubst #-}
+
+instance GSubst b V1 where
+  gsubst _s = error "BUG: void type"
+  {-# INLINE gsubst #-}
+
+instance (GSubst b f, GSubst b g) => GSubst b (f :*: g) where
+  gsubst s (f :*: g) = gsubst s f :*: gsubst s g
+  {-# INLINE gsubst #-}
+
+instance (GSubst b f, GSubst b g) => GSubst b (f :+: g) where
+  gsubst s (L1 f) = L1 $ gsubst s f
+  gsubst s (R1 g) = R1 $ gsubst s g
+  {-# INLINE gsubst #-}
+
+instance (Subst b g) => GSubst b (Rec1 g) where
+  gsubst s (Rec1 f) = Rec1 (applyE s f)
+  {-# INLINE gsubst #-}
+
+--------------------------------------------
+-- Generic implementation of FV class
+--------------------------------------------
+
+instance (FV t) => GFV (Rec1 t) where
+  gappearsFree s (Rec1 f) = appearsFree s f
+  {-# INLINE gappearsFree #-}
+  gfreeVars (Rec1 f) = freeVars f
+  {-# INLINE gfreeVars #-}
+
+-- Constant types
+instance GFV (K1 i c) where
+  gappearsFree s (K1 c) = False
+  {-# INLINE gappearsFree #-}
+  gfreeVars (K1 c) = Set.empty
+  {-# INLINE gfreeVars #-}
+
+instance GFV U1 where
+  gappearsFree _s U1 = False
+  {-# INLINE gappearsFree #-}
+  gfreeVars U1 = Set.empty
+
+instance GFV f => GFV (M1 i c f) where
+  gappearsFree s = gappearsFree s . unM1
+  {-# INLINE gappearsFree #-}
+  gfreeVars = gfreeVars . unM1
+  {-# INLINE gfreeVars #-}
+
+instance GFV V1 where
+  gappearsFree _s = error "BUG: void type"
+  {-# INLINE gappearsFree #-}
+  gfreeVars v = error "BUG: void type"
+  {-# INLINE gfreeVars #-}
+
+instance (GFV f, GFV g) => GFV (f :*: g) where
+  gappearsFree s (f :*: g) = gappearsFree s f && gappearsFree s g
+  {-# INLINE gappearsFree #-}
+  gfreeVars (f :*: g) = gfreeVars f <> gfreeVars g
+  {-# INLINE gfreeVars #-}
+
+instance (GFV f, GFV g) => GFV (f :+: g) where
+  gappearsFree s (L1 f) = gappearsFree s f
+  gappearsFree s (R1 g) = gappearsFree s g
+  {-# INLINE gappearsFree #-}
+
+  gfreeVars (L1 f) = gfreeVars f
+  gfreeVars (R1 g) = gfreeVars g
+  {-# INLINE gfreeVars #-}
+
+------------------------------------------------
+-- Generic implementation of Strengthening class
+------------------------------------------------
+
+
+
+instance GStrengthen (K1 i c) where
+  gstrengthenRec m n k (K1 c) = pure (K1 c)
+  {-# INLINE gstrengthenRec #-}
+
+instance GStrengthen U1 where
+  gstrengthenRec m n k U1 = pure U1
+  {-# INLINE gstrengthenRec #-}
+
+instance GStrengthen f => GStrengthen (M1 i c f) where
+  gstrengthenRec m n k x = M1 <$> gstrengthenRec m n k (unM1 x)
+  {-# INLINE gstrengthenRec #-}
+
+instance GStrengthen V1 where
+  gstrengthenRec m n k = error "BUG: void type"
+  {-# INLINE gstrengthenRec #-}
+
+instance (GStrengthen f, GStrengthen g) => GStrengthen (f :*: g) where
+  gstrengthenRec m n k (f :*: g) = (:*:) <$> gstrengthenRec m n k f <*> gstrengthenRec m n k g
+  {-# INLINE gstrengthenRec #-}
+
+instance (GStrengthen f, GStrengthen g) => GStrengthen (f :+: g) where
+  gstrengthenRec m n k (L1 f) = L1 <$> gstrengthenRec m n k f
+  gstrengthenRec m n k (R1 g) = R1 <$> gstrengthenRec m n k g
+  {-# INLINE gstrengthenRec #-}
+
+instance Strengthen t => GStrengthen (Rec1 t) where
+  gstrengthenRec k m n (Rec1 t) = Rec1 <$> strengthenRec k m n t
diff --git a/src/Rebound/Lib.hs b/src/Rebound/Lib.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Lib.hs
@@ -0,0 +1,30 @@
+-- |
+-- Description: Library for dependent types
+--
+-- Imports and re-exports libraries for Dependent Haskell
+-- Because 'Fin' and 'Vec' include definitions with the same
+-- name as Prelude functions, clients of this module should also
+-- import them this way:
+--
+-- @
+-- import 'Data.Fin' qualified as 'Fin'
+-- import 'Data.Vec' qualified as 'Vec'
+-- @
+module Rebound.Lib
+  (
+    type Type,
+    module Data.Type.Equality,
+    Fin (..),
+    Vec (..),
+    ToInt (..),
+    module Data.Nat,
+    module Data.SNat,
+  )
+where
+
+import Data.Fin (Fin (..))
+import Data.Kind (Type)
+import Data.Nat
+import Data.SNat
+import Data.Type.Equality
+import Data.Vec (Vec (..))
diff --git a/src/Rebound/MonadNamed.hs b/src/Rebound/MonadNamed.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/MonadNamed.hs
@@ -0,0 +1,86 @@
+-- |
+-- Description: Monads supporting scopes of names
+-- Stability: experimental
+--
+-- This is a simplified version of 'Rebound.MonadScoped.MonadScopedReader',
+-- where the environment is restricted to a vector of names.
+
+module Rebound.MonadNamed
+  ( Sized (..),
+    S.MonadScopedReader,
+    Scope,
+    ScopedReader (..),
+    ScopedReaderT (..),
+    scope,
+    pushVec,
+    push,
+    LocalName (..),
+    runScopedReader,
+    runScopedReaderT,
+  )
+where
+
+import Rebound hiding (fromVec)
+import Data.SNat as SNat
+import Data.Vec as Vec
+import qualified Rebound.MonadScoped as S
+import Rebound.MonadScoped (MonadScopedReader(..))
+import Data.LocalName
+
+-----------------------------------------------------------------------
+-- Scopes
+-----------------------------------------------------------------------
+
+-- | A mapping from variables in scope to a name.
+newtype Scope name n = Scope {names :: Vec n name}
+  deriving (Eq, Show)
+
+emptyScope :: Scope name Z
+emptyScope = Scope VNil
+
+fromVec :: Vec p name -> Scope name p
+fromVec v = Scope v
+
+extendScope ::
+  forall p n name.
+  (SNatI p) =>
+  Vec p name ->
+  Scope name n ->
+  Scope name (p + n)
+extendScope v (Scope s) = Scope $ Vec.append v s
+
+-----------------------------------------------------------------------
+-- MonadScoped class
+-----------------------------------------------------------------------
+
+-- | Get the name of variables in scope.
+scope :: MonadScopedReader (Scope name) m => m n (Vec n name)
+scope = readerS names
+
+-- | Add a new variable to the scope.
+push :: (MonadScopedReader (Scope name) m)
+  => name -> m (S n) a -> m n a
+push n = localS (extendScope (n ::: VNil))
+
+-- | Add a vector of new variables to the scope.
+pushVec :: (MonadScopedReader (Scope name) m)
+  => Vec p name -> m (p + n) a -> m n a
+pushVec v = withSNat (vlength v) $ localS (extendScope v)
+
+-----------------------------------------------------------------------
+-- ScopedReader monad
+-----------------------------------------------------------------------
+
+-- | A monad transformer to keep track of the name of variables in scope.
+type ScopedReaderT name m n a = S.ScopedReaderT (Scope name) m n a
+
+-- | A monad to keep track of the name of variables in scope.
+type ScopedReader name n a = S.ScopedReader (Scope name) n a
+
+-- | Run the computation with the provided vector of names.
+runScopedReaderT :: forall m n name a. ScopedReaderT name m n a -> Vec n name -> m a
+runScopedReaderT c v = S.runScopedReaderT c (Scope v)
+
+-- | Run the computation with the provided vector of names.
+runScopedReader :: forall n name a. ScopedReader name n a -> Vec n name -> a
+runScopedReader c v = S.runScopedReader c (Scope v)
diff --git a/src/Rebound/MonadScoped.hs b/src/Rebound/MonadScoped.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/MonadScoped.hs
@@ -0,0 +1,195 @@
+-- |
+-- Description: Scoped variants of some monads
+--
+-- Provides scoped variants of monads from [mtl](https://hackage.haskell.org/package/mtl).
+
+module Rebound.MonadScoped
+  ( MonadScopedReader (..),
+    ScopedReader (..),
+    ScopedReaderT (..),
+    asksS,
+    runScopedReader,
+    MonadScopedState (..),
+    ScopedState (..),
+    ScopedStateT (..),
+    evalScopedState,
+    evalScopedStateT,
+    execScopedState,
+    execScopedStateT,
+    modifyS,
+    getsS
+  )
+where
+
+import Control.Monad (liftM2, (>=>))
+import Control.Monad.Error.Class (MonadError (..))
+import Control.Monad.Identity (Identity (runIdentity))
+import Control.Monad.Reader (MonadReader (ask, local), asks)
+import Control.Monad.Writer (MonadWriter (..))
+import Data.Kind (Type)
+import Data.Nat (Nat (S))
+import Data.SNat (type (+))
+
+-----------------------------------------------------------------------
+-- Reader class
+-----------------------------------------------------------------------
+
+-- | Scoped variant of 'Control.Monad.Reader.MonadReader'.
+--
+-- __Note__: the "environment" mentioned here as nothing to do with 'Rebound.Env.Env'!
+class (forall n. Monad (m n)) => MonadScopedReader e m | m -> e where
+  {-# MINIMAL (askS | readerS), localS #-}
+  -- | Retrieve the environment.
+  askS :: m n (e n)
+  askS = readerS id
+  -- | Run a function in an altered environment.
+  localS :: (e n -> e n') -> m n' a -> m n a
+  -- | Retrieve a function of the environment.
+  readerS :: (e n -> a) -> m n a
+  readerS f = f <$> askS
+
+-- | Retrieve the environment.
+asksS :: (MonadScopedReader e m) => (e n -> a) -> m n a
+asksS = readerS
+
+-----------------------------------------------------------------------
+-- Reader monad
+-----------------------------------------------------------------------
+
+-- | Computations that need a (read-only) environment.
+type ScopedReader e n a = ScopedReaderT e Identity n a
+
+-- | Run the computation with the provided environment.
+runScopedReader :: ScopedReader e n a -> e n -> a
+runScopedReader c m = runIdentity $ runScopedReaderT c m
+
+-----------------------------------------------------------------------
+-- Reader transformer
+-----------------------------------------------------------------------
+
+-- | A scoped variant of 'Control.Monad.Reader.ReaderT'.
+newtype ScopedReaderT e m n a = ScopedReaderT {runScopedReaderT :: e n -> m a}
+  deriving (Functor)
+
+instance (Applicative m) => Applicative (ScopedReaderT e m n) where
+  pure f = ScopedReaderT $ \x -> pure f
+  ScopedReaderT f <*> ScopedReaderT x = ScopedReaderT (\e -> f e <*> x e)
+
+instance (Monad m) => Monad (ScopedReaderT e m n) where
+  ScopedReaderT m >>= k = ScopedReaderT $ \e ->
+    m e >>= (\v -> let x = k v in runScopedReaderT x e)
+
+instance (MonadReader r m) => MonadReader r (ScopedReaderT e m n) where
+  ask = ScopedReaderT $ const ask
+  local f m = ScopedReaderT (local f . runScopedReaderT m)
+
+instance (MonadError e m) => MonadError e (ScopedReaderT se m n) where
+  throwError e = ScopedReaderT $ const (throwError e)
+  catchError m k = ScopedReaderT $ \s -> runScopedReaderT m s `catchError` (\err -> runScopedReaderT (k err) s)
+
+instance (MonadWriter w m) => MonadWriter w (ScopedReaderT e m n) where
+  writer w = ScopedReaderT $ const (writer w)
+  listen m = ScopedReaderT $ \s -> listen $ runScopedReaderT m s
+  pass m = ScopedReaderT $ \s -> pass $ runScopedReaderT m s
+
+instance (Monad m) => MonadScopedReader e (ScopedReaderT e m) where
+  askS = ScopedReaderT return
+  localS f (ScopedReaderT g) = ScopedReaderT $ g . f
+
+-----------------------------------------------------------------------
+-- State class
+-----------------------------------------------------------------------
+
+-- | Scoped variant of 'Control.Monad.State.MonadState'.
+class (forall n. Monad (m n)) => MonadScopedState s m | m -> s where
+  {-# MINIMAL rescope, (stateS | (getS, putS)) #-}
+  -- | Change the scope of the environment, run a function, and change back the scope.
+  rescope :: (s n -> s n') -> (s n' -> s n) -> m n' a -> m n a
+
+  -- | Retrieve the state.
+  getS :: m n (s n)
+  getS = stateS $ \s -> (s, s)
+
+  -- | Set the state.
+  putS :: s n -> m n ()
+  putS s = stateS $ const ((), s)
+
+  -- | Lift a function into a monadic computation.
+  stateS :: (s n -> (a, s n)) -> m n a
+  stateS f = do
+    s <- getS
+    let (v, s') = f s
+    putS s'
+    return v
+
+-- | Apply a function to the state.
+modifyS :: (MonadScopedState s m) => (s n -> s n) -> m n ()
+modifyS f = do
+  s <- getS
+  putS $ f s
+
+-- | Retrieve a function of the state.
+getsS :: (MonadScopedState s m) => (s n -> a) -> m n a
+getsS f = f <$> getS
+
+-----------------------------------------------------------------------
+-- State monad
+-----------------------------------------------------------------------
+
+-- | Computations that need a state.
+type ScopedState s n a = ScopedStateT s Identity n a
+
+-- | Run the computation with the provided state, and return the result as well as the final state.
+runScopedState :: ScopedState s n a -> s n -> (a, s n)
+runScopedState m s = runIdentity $ runScopedStateT m s
+
+-- | Run the computation with the provided state, and return the result.
+evalScopedState :: ScopedState s n a -> s n -> a
+evalScopedState m s = runIdentity $ evalScopedStateT m s
+
+-- | Run the computation with the provided state, and return the final state.
+execScopedState :: ScopedState s n a -> s n -> s n
+execScopedState m s = runIdentity $ execScopedStateT m s
+
+-----------------------------------------------------------------------
+-- State transformer
+-----------------------------------------------------------------------
+
+-- | A scoped variant of 'Control.Monad.State.StateT'.
+newtype ScopedStateT s m n a = ScopedStateT {runScopedStateT :: s n -> m (a, s n)}
+  deriving (Functor)
+
+-- | Run the computation with the provided state, and return the result.
+evalScopedStateT :: (Functor m) => ScopedStateT s m n a -> s n -> m a
+evalScopedStateT m s = fst <$> runScopedStateT m s
+
+-- | Run the computation with the provided state, and return the final state.
+execScopedStateT :: (Functor m) => ScopedStateT s m n a -> s n -> m (s n)
+execScopedStateT m s = snd <$> runScopedStateT m s
+
+-- A bit disappointing, but mtl does also require m to be a monad...
+instance (Monad m) => Applicative (ScopedStateT s m n) where
+  pure f = ScopedStateT $ \s -> pure (f, s)
+  (<*>) = liftM2 (\f a -> f a)
+
+instance (Monad m) => Monad (ScopedStateT s m n) where
+  ScopedStateT m >>= k = ScopedStateT $ m >=> (\ (m', s') -> runScopedStateT (k m') s')
+
+instance (MonadReader r m) => MonadReader r (ScopedStateT s m n) where
+  ask = ScopedStateT $ \s -> asks (,s)
+  local f m = ScopedStateT (local f . runScopedStateT m)
+
+instance (MonadError e m) => MonadError e (ScopedStateT se m n) where
+  throwError e = ScopedStateT $ const (throwError e)
+  catchError m k = ScopedStateT $ \s -> runScopedStateT m s `catchError` (\err -> runScopedStateT (k err) s)
+
+instance (MonadWriter w m) => MonadWriter w (ScopedStateT s m n) where
+  writer w = ScopedStateT $ \s -> (,s) <$> writer w
+  listen m = ScopedStateT $ \s -> (\((m, s'), w) -> ((m, w), s')) <$> listen (runScopedStateT m s)
+  pass m = ScopedStateT $ \s -> pass ((\((m, r), s') -> ((m, s'), r)) <$> runScopedStateT m s)
+
+instance (Monad m) => MonadScopedState s (ScopedStateT s m) where
+  stateS f = ScopedStateT $ pure . f
+  rescope up low m = ScopedStateT $ \s -> do
+    (r, s') <- runScopedStateT m (up s)
+    return (r, low s')
diff --git a/src/Rebound/Refinement.hs b/src/Rebound/Refinement.hs
new file mode 100644
--- /dev/null
+++ b/src/Rebound/Refinement.hs
@@ -0,0 +1,85 @@
+-- |
+-- Module: Rebound.Refinement
+-- Description : Refinement from variables to terms
+--
+-- Refinements map variables in a scope to terms which live in the same scope.
+
+module Rebound.Refinement(
+    Refinement(..),
+    emptyR,
+    joinR,
+    singletonR,
+    toEnvironment,
+    fromEnvironment,
+    refine,
+    domain)
+ where
+
+import Rebound.Lib ( SNatI, SNat, type (+), Fin )
+import Rebound.Env
+import Data.Map as Map
+import Control.Monad
+import Data.Fin as Fin
+
+-- | A refinement is a special kind of substitution that does not
+-- change the scope, it just replaces all uses of a particular variable
+-- with some other term, which lives in the same scope.
+newtype Refinement v n = Refinement (Map (Fin n) (v n))
+
+-- | The empty refinement. Maps every variable to itself.
+emptyR :: Refinement v n
+emptyR = Refinement Map.empty
+
+-- | Join/merge/meld two refinements.
+-- Fails if the two refinements have overlapping domains.
+joinR ::
+  forall v n.
+  (SNatI n, Subst v v, Eq (v n)) =>
+  Refinement v n ->
+  Refinement v n ->
+  Maybe (Refinement v n)
+joinR (Refinement xs) (Refinement ys) =
+  Refinement <$> foldM f ys xs'
+  where
+    xs' = Map.toList xs
+    r = fromTable xs'
+    f :: Map.Map (Fin n) (v n) -> (Fin n, v n) -> Maybe (Map.Map (Fin n) (v n))
+    f m (k, v)
+      | Map.member k ys = Nothing
+      | otherwise =
+          let v' = applyE r v
+           in Just $ if v' == var k then m else Map.insert k (applyE r v) m
+
+-- | A singleton refinement.
+-- Maps the specified variable to the specified term, and every other variable
+-- gets mapped to itself.
+singletonR :: (SubstVar v, Eq (v n)) => (Fin n, v n) -> Refinement v n
+singletonR (x, t) =
+  if t == var x then emptyR else Refinement (Map.singleton x t)
+
+instance (Shiftable v) => Shiftable (Refinement v) where
+  shift :: forall k n. SNat k -> Refinement v n -> Refinement v (k + n)
+  shift k (Refinement (r :: Map.Map (Fin n) (v n))) = Refinement g'
+    where
+      f' = Map.mapKeysMonotonic (Fin.shiftN @k @n k) r
+      g' = Map.map (shift k) f'
+
+-- | Convert a refinement into an environment.
+toEnvironment :: (SNatI n, SubstVar v) => Refinement v n -> Env v n n
+toEnvironment (Refinement x) = fromTable (Map.toList x)
+
+-- | Convert a refinement to an environment.
+fromEnvironment :: (SNatI n, SubstVar v) => Env v n n -> Refinement v n
+fromEnvironment r = Refinement (Map.fromList (tabulate r))
+
+-- | Checks whether this refinement refines a variable.
+refines :: forall n v. (SNatI n, Subst v v, Eq (v n)) => Refinement v n -> Fin n -> Bool
+refines r i = applyE (toEnvironment r) (var @v i) /= var @v i
+
+-- | Apply the refinement to a variable.
+refine :: (SNatI n, Subst v c) => Refinement v n -> c n -> c n
+refine r = applyE (toEnvironment r)
+
+-- | Returns the domain of the environment (i.e., the list of refined variables).
+domain :: Refinement v n -> [Fin n]
+domain (Refinement m) = Map.keys m
diff --git a/test/All.hs b/test/All.hs
new file mode 100644
--- /dev/null
+++ b/test/All.hs
@@ -0,0 +1,26 @@
+import Examples.DepMatch qualified as DepMatch
+import Examples.LC qualified as LC
+import Examples.LCLet qualified as LCLet
+import Examples.PTS qualified as PTS
+import Examples.Pat qualified as Pat
+import Examples.PureSystemF qualified as PureSystemF
+import Examples.LinLC qualified as LinLC
+import Test.Tasty
+
+main :: IO ()
+main = do
+  defaultMain $
+    testGroup
+      "All"
+      [ LC.all,
+        LCLet.all,
+        Pat.all,
+        testGroup
+          "System F"
+          [ -- TODO: add System F tests
+            PureSystemF.all
+          ],
+        PTS.all,
+        DepMatch.all,
+        LinLC.all
+      ]
diff --git a/test/Examples/DepMatch.hs b/test/Examples/DepMatch.hs
new file mode 100644
--- /dev/null
+++ b/test/Examples/DepMatch.hs
@@ -0,0 +1,40 @@
+module Examples.DepMatch where
+
+import Data.Fin (f0, f1)
+import DepMatch
+import Rebound (N2, Nat (S), appearsFree, s1, snat, strengthenRec, zeroE, (.:))
+import Rebound.Bind.PatN (bind1)
+import Test.Tasty
+import Test.Tasty.HUnit
+import Utils
+
+instance Eq Err where _ == _ = False
+
+sig :: Exp n -> Exp (S n) -> Exp n
+sig l r = Sigma l (bind1 r)
+
+all :: TestTree
+all =
+  testGroup
+    "DepMatch"
+    [ testCase "Pattern-match tm0 with pat0" $
+        ((snat,) <$> patternMatch pat0 tm0) @?= Just (snat @N2, sig Star (Var f0) .: (Star .: zeroE)),
+      testCase "Is f0 free in t00?" $ appearsFree f0 t00 @?= True,
+      testCase "Is f1 free in t00?" $ appearsFree f1 t00 @?= False,
+      testCase "Weaken t00 by 1" $ weaken' s1 t00 @?= (Var f0 `App` Var f0),
+      testCase "Strengthen t00 by 1/1" $ strengthenRec s1 s1 snat t00 @?= Just (Var f0 `App` Var f0),
+      testCase "Strengthen t01 by 1/1" $ strengthenRec s1 s1 snat t01 @?= Nothing,
+      testCase "Pretty-print t0" $ show t0 @?= "λ_. 0",
+      testCase "Pretty-print t1" $ show t1 @?= "λ_. (λ_. (1 (λ_. (0 0))))",
+      testCase "Pretty-print tyid" $ show tyid @?= "Pi *. 0 -> 1",
+      testCase "Pretty-print tmid" $ show tmid @?= "λ_. (λ_. 0)",
+      testCase "Eval t1" $ eval t1 @?= lam (lam $ Var f1 `App` lam (Var f0 `App` Var f0)),
+      testCase "Eval application" $
+        eval (t1 `App` t0) @?= lam (lam (Var f0) `App` lam (Var f0 `App` Var f0)),
+      testCase "Step application" $
+        step (t1 `App` t0) @?= Just (lam (lam (Var f0) `App` lam (Var f0 `App` Var f0))),
+      testCase "Check tmid : tyid" $ checkType zeroE tmid tyid @?= Right (),
+      testCase "Type (tmid tyid)" $ case inferType zeroE (tmid `App` tyid) of
+        Left (AnnotationNeeded err) -> err @?= lam (lam (Var f0))
+        _ -> assertFailure "Expected an `AnnotationNeeded` error."
+    ]
diff --git a/test/Examples/LC.hs b/test/Examples/LC.hs
new file mode 100644
--- /dev/null
+++ b/test/Examples/LC.hs
@@ -0,0 +1,42 @@
+module Examples.LC where
+
+import HOAS qualified
+import LC
+import LCQC qualified
+import Rebound (zeroE)
+import ScopeCheck qualified
+import Test.Tasty
+import Test.Tasty.HUnit
+import Test.Tasty.QuickCheck qualified as QC
+
+all :: TestTree
+all =
+  testGroup
+    "LC"
+    [ testCase "Pretty-print t0" $ show t0 @?= "(λ. 0)",
+      testCase "Eval application" $ eval (t `App` t0) @?= t0,
+      testCase "Pretty-print t2" $ show t2 @?= "((λ. (λ. 1)) ((λ. 0) (λ. 0)))",
+      testCase "Eval t2" $ show (eval t2) @?= "(λ. (λ. 0))",
+      testCase "Step application" $ step (t0 `App` t0) @?= Just t0,
+      testCase "Eval' application" $ eval' 5 (t `App` t0) @?= Just t0,
+      testCase "WHNF" $ show (whnfEnv zeroE t) @?= "(λ. ((0 ((λ. 0) 0)) (λ. 0)))",
+      localOption (QC.QuickCheckTests 10) $
+        testGroup
+          "LCQC"
+          [ QC.testProperty "nf1 normalizes" LCQC.prop_nf1,
+            QC.testProperty "nfEnv normalizes" LCQC.prop_nfEnv
+          ],
+      testGroup
+        "ScopeCheck"
+        [ testCase "Scope idExp" $ ScopeCheck.scopeCheck ScopeCheck.idExp @?= Just t0,
+          testCase "Scope trueExp" $ ScopeCheck.scopeCheck ScopeCheck.trueExp @?= Just (lam $ lam v1),
+          testCase "Scope ill-scoped" $ ScopeCheck.scopeCheck ScopeCheck.illScoped @?= Nothing
+        ],
+      testGroup
+        "HOAS"
+        [ testCase "Convert tru" $ HOAS.cvt HOAS.tru @?= lam (lam v1),
+          testCase "Convert tru" $ HOAS.cvt HOAS.fls @?= lam (lam v0),
+          testCase "Convert app" $ HOAS.cvt HOAS.app @?= lam (lam $ v1 @@ v0),
+          testCase "Convert omega" $ HOAS.cvt HOAS.omega @?= (lam (v0 @@ v0) @@ lam (v0 @@ v0))
+        ]
+    ]
diff --git a/test/Examples/LCLet.hs b/test/Examples/LCLet.hs
new file mode 100644
--- /dev/null
+++ b/test/Examples/LCLet.hs
@@ -0,0 +1,21 @@
+module Examples.LCLet where
+
+import LCLet
+import Test.Tasty
+import Test.Tasty.HUnit
+
+all :: TestTree
+all =
+  testGroup
+    "LCLet"
+    [ testCase "Pretty-print t0" $ show t0 @?= "(λ. 0)",
+      testCase "Pretty-print t1" $ show t1 @?= "(λ. (λ. (1 ((λ. 0) 0))))",
+      testCase "Pretty-print t2" $ show t2 @?= "(let (λ. 0) in (0 0))",
+      testCase "Pretty-print t3" $ show t3 @?= "(let rec (λ. (0 (1 0))) in 0)",
+      testCase "Pretty-print t4" $ show t4 @?= "<let-tele>",
+      testCase "Eval t1" $ show (eval t1) @?= "(λ. (λ. (1 ((λ. 0) 0))))",
+      testCase "Eval application" $ show (eval (t1 @@ t0)) @?= "(λ. ((λ. 0) ((λ. 0) 0)))",
+      testCase "Eval t2" $ show (eval t2) @?= "(λ. 0)",
+      -- testCase "Eval t3" $ show (eval t3) @?= <Infinite loop>,
+      testCase "Eval t4" $ show (eval t4) @?= "(λ. 0)"
+    ]
diff --git a/test/Examples/LinLC.hs b/test/Examples/LinLC.hs
new file mode 100644
--- /dev/null
+++ b/test/Examples/LinLC.hs
@@ -0,0 +1,41 @@
+module Examples.LinLC where
+
+import Data.Vec (Vec ((:::)), empty)
+import LinLC
+import Rebound (Nat (Z))
+import Test.Tasty
+import Test.Tasty.HUnit
+
+tcS :: Exp Z -> Ty -> Assertion
+tcS t ty = runTC empty (checkType t ty) @?= Right ()
+
+tcF :: Exp Z -> Ty -> String -> Assertion
+tcF t ty msg = runTC empty (checkType t ty) @?= Left msg
+
+all :: TestTree
+all =
+  testGroup
+    "LinLC"
+    [ testCase "Check id" $
+        tcS (lam v0) (TyUnit ~> TyUnit),
+      testCase "Check app" $
+        tcS (lam $ lam $ v0 @@ v1) (TyUnit ~> (TyUnit ~> TyUnit) ~> TyUnit),
+      testCase "Check 1 unused" $
+        tcF
+          (lam $ lam v1)
+          (TyUnit ~> (TyUnit ~> TyUnit) ~> TyUnit)
+          "Variable was not used.",
+      testCase "Check type mismatch" $
+        tcF
+          (lam $ lam v0)
+          (TyUnit ~> (TyUnit ~> TyUnit) ~> TyUnit)
+          "Inferred type does not match expected type.",
+      testCase "Check 2 unused" $
+        tcF
+          (lam $ lam v0)
+          (TyUnit ~> (TyUnit ~> TyUnit) ~> TyUnit ~> TyUnit)
+          "Variable was not used.",
+      testCase "Initial scope must be used" $
+        runTC (TyUnit ::: (TyUnit ~> TyUnit) ::: TyUnit ::: empty) (checkType (v1 @@ v0) TyUnit)
+          @?= Left "Some variables in the initial scope were not used."
+    ]
diff --git a/test/Examples/PTS.hs b/test/Examples/PTS.hs
new file mode 100644
--- /dev/null
+++ b/test/Examples/PTS.hs
@@ -0,0 +1,50 @@
+module Examples.PTS where
+
+import Data.Fin (f0, f1)
+import PTS
+import Rebound (Nat (S), appearsFree, idE, s1, snat, strengthenRec, zeroE)
+import Rebound.Bind.PatN
+import Test.Tasty
+import Test.Tasty.HUnit
+import Prelude hiding (pi)
+
+lam :: Exp n -> Exp (S n) -> Exp n
+lam tTy t = Lam tTy (bind1 t)
+
+pi :: Exp n -> Exp (S n) -> Exp n
+pi tTy t = Pi tTy (bind1 t)
+
+instance Eq Err where _ == _ = False
+
+all :: TestTree
+all =
+  testGroup
+    "PTS"
+    [ testCase "Is f0 free in t00?" $ appearsFree f0 t00 @?= True,
+      testCase "Is f1 free in t00?" $ appearsFree f1 t00 @?= False,
+      testCase "Weaken t00 by 1" $ weaken' s1 t00 @?= App (Var f0) (Var f0),
+      testCase "Strengthen t00 by 1/1" $
+        strengthenRec s1 s1 snat t00 @?= Just (App (Var f0) (Var f0)),
+      testCase "Strengthen t01 by 1/1" $ strengthenRec s1 s1 snat t01 @?= Nothing,
+      testCase "Pretty-print t0" $ show t0 @?= "λ *. 0",
+      testCase "Pretty-print t1" $ show t1 @?= "λ *. λ *. 1 ((λ *. 0) 0)",
+      testCase "Pretty-print tyid" $ show tyid @?= "Pi *. 0 -> 1",
+      testCase "Pretty-print tmid" $ show tmid @?= "λ *. λ 0. 0",
+      testCase "Eval t1" $
+        eval t1 @?= lam Star (lam Star (Var f1 `App` (lam Star (Var f0) `App` Var f0))),
+      testCase "Eval application" $
+        eval (t1 `App` t0)
+          @?= lam Star (lam Star (Var f0) `App` (lam Star (Var f0) `App` Var f0)),
+      testCase "Step application" $
+        step (t1 `App` t0)
+          @?= Just (lam Star (lam Star (Var f0) `App` (lam Star (Var f0) `App` Var f0))),
+      testCase "Normalize t1" $ nf t1 @?= lam Star (lam Star (Var f1 `App` Var f0)),
+      testCase "Normalize application" $
+        nf (t1 `App` t0) @?= lam Star (Var f0),
+      testCase "EvalEnv t1" $
+        evalEnv idE t1 @?= lam Star (lam Star (Var f1 `App` (lam Star (Var f0) `App` Var f0))),
+      testCase "Type tmid" $ inferType zeroE tmid @?= Right (pi Star $ pi (Var f0) (Var f1)),
+      testCase "Type application" $
+        inferType zeroE (tmid `App` tyid)
+          @?= Right (pi (pi Star (pi (Var f0) (Var f1))) (pi Star (pi (Var f0) (Var f1))))
+    ]
diff --git a/test/Examples/Pat.hs b/test/Examples/Pat.hs
new file mode 100644
--- /dev/null
+++ b/test/Examples/Pat.hs
@@ -0,0 +1,30 @@
+module Examples.Pat where
+
+import Pat
+import Rebound (N2, Nat (Z), snat, zeroE, (.:))
+import Test.Tasty
+import Test.Tasty.HUnit
+import Utils
+
+all :: TestTree
+all =
+  testGroup
+    "Pat"
+    [ testCase "Pretty-print t0" $ show t0 @?= "λ. 0",
+      testCase "Pretty-print t1" $ show t1 @?= "λ. λ. 1 (λ. 0 0)",
+      testCase "Pretty-print t2" $ show t2 @?= "λ. case 0 of [Nil => 0,(Cons V) V => 0]",
+      testCase "Pretty-print t3" $ show t3 @?= "(cons a) ((cons b) nil)",
+      testCase "Pretty-print t4" $ show t4 @?= "λ. case 0 of [Nil => 0,(Cons V) V => 0] ((cons a) ((cons b) nil))",
+      testCase "Pattern-match e1 with p1" $
+        ((snat @N2,) <$> patternMatch p1 e1) @?= Just (snat @N2, Con "B" .: (Con "A" .: zeroE @_ @Z)),
+      testCase "Pattern-match e1 with p2" $ ((snat @N2,) <$> patternMatch p2 e1) @?= Nothing,
+      testCase "Pattern-match e2 with p1" $ ((snat @N2,) <$> patternMatch p1 e2) @?= Nothing,
+      testCase "Pattern-match e2 with p2" $
+        ((snat @N2,) <$> patternMatch p2 e2) @?= Just (snat @N2, Con "C" .: (Con "A" .: zeroE @_ @Z)),
+      testCase "Eval t1" $ show (eval t1) @?= "λ. λ. 1 (λ. 0 0)",
+      testCase "Eval application" $ show (eval (t1 `App` t0)) @?= "λ. λ. 0 (λ. 0 0)",
+      testCase "Eval t4" $ show (eval t4) @?= "case (cons a) ((cons b) nil) of [Nil => (cons a) ((cons b) nil),(Cons V) V => 0]",
+      testCase "Step application" $ show (step (t1 `App` t0)) @?= "Just (λ. λ. 0 (λ. 0 0))",
+      testCase "Normalize t1" $ show (nf t1) @?= "λ. λ. 1 0",
+      testCase "Normalize application" $ show (nf (t1 `App` t0)) @?= "λ. λ. 0 0"
+    ]
diff --git a/test/Examples/PureSystemF.hs b/test/Examples/PureSystemF.hs
new file mode 100644
--- /dev/null
+++ b/test/Examples/PureSystemF.hs
@@ -0,0 +1,46 @@
+module Examples.PureSystemF where
+
+import Data.Fin (f0, f1)
+import PureSystemF
+import Rebound (LocalName (..))
+import Rebound.Bind.Local (bind)
+import Test.Tasty
+import Test.Tasty.HUnit
+
+pureTC t = runTC emptyEnv $ inferType t
+
+bbNat = TAll $ bind (LocalName "X") $ TArr (TArr (Var f0) (Var f0)) (TArr (Var f0) (Var f0))
+
+all :: TestTree
+all =
+  testGroup
+    "SystemF"
+    [ testGroup
+        "Diadic"
+        -- TODO: add test cases
+        [],
+      testGroup
+        "Pure"
+        [ testCase "Pretty-print t0" $ show t0 @?= "ΛX. λx. x",
+          testCase "Pretty-print t1" $ show t1 @?= "ΛX. λf. λx. f [X] x",
+          testCase "Pretty-print t2" $ show t2 @?= "λX. λx. x",
+          testCase "Infer t0" $
+            pureTC t0
+              @?= Right (TAll $ bind (LocalName "X") (TArr (Var f0) (Var f0))),
+          testCase "Infer t1" $
+            pureTC t1
+              @?= Right
+                ( TAll $
+                    bind
+                      (LocalName "X")
+                      ( TArr
+                          (TAll $ bind (LocalName "Y") (TArr (Var f0) (Var f0)))
+                          (TArr (Var f0) (Var f0))
+                      )
+                ),
+          testCase "Infer t2" $ pureTC t2 @?= Left "Term variable occurs in type",
+          testCase "Infer Boehm-Berarducci Nat 0" $ pureTC bbn0 @?= Right bbNat,
+          testCase "Infer Boehm-Berarducci Nat 1" $ pureTC bbn1 @?= Right bbNat,
+          testCase "Infer Boehm-Berarducci Nat 2" $ pureTC bbn2 @?= Right bbNat
+        ]
+    ]
diff --git a/test/Utils.hs b/test/Utils.hs
new file mode 100644
--- /dev/null
+++ b/test/Utils.hs
@@ -0,0 +1,17 @@
+module Utils where
+
+import Rebound (Env, SNat (..), SNatI, SubstVar, head, snat, tail, withSNat)
+import Prelude hiding (head, tail)
+
+envEq ::
+  forall v n m.
+  (SNatI n, forall k. Eq (v k), SubstVar v) =>
+  Env v n m ->
+  Env v n m ->
+  Bool
+envEq l r = case snat @n of
+  SZ -> True
+  SS -> head l == head r && envEq (tail l) (tail r)
+
+instance {-# OVERLAPPING #-} (forall k. Eq (v k), SubstVar v) => Eq (SNat n, Env v n m) where
+  (n, l) == (_, r) = withSNat n $ envEq l r
