diff --git a/Data/Prednote/Expressions.hs b/Data/Prednote/Expressions.hs
deleted file mode 100644
--- a/Data/Prednote/Expressions.hs
+++ /dev/null
@@ -1,83 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
--- | Handles parsing of both infix and RPN Pdct expressions.
-module Data.Prednote.Expressions
-  ( ExprDesc(..)
-  , Error
-  , Token
-  , operand
-  , opAnd
-  , opOr
-  , opNot
-  , openParen
-  , closeParen
-  , parseExpression
-  ) where
-
-import Data.Either (partitionEithers)
-import qualified Data.Text as X
-import qualified Data.Prednote.Expressions.Infix as I
-import qualified Data.Prednote.Expressions.RPN as R
-import qualified Data.Prednote.Pdct as P
-import qualified Control.Monad.Exception.Synchronous as Ex
-
--- | A single type for both RPN tokens and infix tokens.
-newtype Token a = Token { unToken :: I.InfixToken a }
-type Error = X.Text
-
--- | Creates Operands from Pdct.
-operand :: P.Pdct a -> Token a
-operand p = Token (I.TokRPN (R.TokOperand p))
-
--- | The And operator
-opAnd :: Token a
-opAnd = Token (I.TokRPN (R.TokOperator R.OpAnd))
-
--- | The Or operator
-opOr :: Token a
-opOr = Token (I.TokRPN (R.TokOperator R.OpOr))
-
--- | The Not operator
-opNot :: Token a
-opNot = Token (I.TokRPN (R.TokOperator R.OpNot))
-
--- | Open parentheses
-openParen :: Token a
-openParen = Token (I.TokParen I.Open)
-
--- | Close parentheses
-closeParen :: Token a
-closeParen = Token (I.TokParen I.Close)
-
--- | Is this an infix or RPN expression?
-data ExprDesc
-  = Infix
-  | RPN
-  deriving (Eq, Show)
-
-toksToRPN :: [Token a] -> Maybe [R.RPNToken a]
-toksToRPN toks
-  = let toEither t = case unToken t of
-          I.TokRPN tok -> Right tok
-          _ -> Left ()
-    in case partitionEithers . map toEither $ toks of
-        ([], xs) -> return xs
-        _ -> Nothing
-
--- | Parses expressions. Fails if the expression is nonsensical in
--- some way (for example, unbalanced parentheses, parentheses in an
--- RPN expression, or multiple stack values remaining.) Works by first
--- changing infix expressions to RPN ones.
-parseExpression
-  :: ExprDesc
-  -> [Token a]
-  -> Ex.Exceptional Error (P.Pdct a)
-parseExpression e toks = do
-  rpnToks <- case e of
-    Infix -> Ex.fromMaybe "unbalanced parentheses\n"
-             . I.createRPN
-             . map unToken
-             $ toks
-    RPN -> Ex.fromMaybe "parentheses in an RPN expression\n"
-           $ toksToRPN toks
-  R.parseRPN rpnToks
diff --git a/Data/Prednote/Expressions/Infix.hs b/Data/Prednote/Expressions/Infix.hs
deleted file mode 100644
--- a/Data/Prednote/Expressions/Infix.hs
+++ /dev/null
@@ -1,126 +0,0 @@
-module Data.Prednote.Expressions.Infix
-  ( InfixToken (..)
-  , Paren(..)
-  , createRPN
-  ) where
-
-import qualified Data.Prednote.Expressions.RPN as R
-import qualified Data.Foldable as Fdbl
-
-data InfixToken a
-  = TokRPN (R.RPNToken a)
-  | TokParen Paren
-
-data Paren = Open | Close
-
--- | Values on the operator stack.
-data OpStackVal
-  = StkOp R.Operator
-  | StkOpenParen
-
--- In the shunting yard algorithm, the output sequence is a queue. The
--- first values to go into the output sequence are the first to be
--- processed by the RPN parser. In this module, the output sequence is
--- implemented as a list stack, which means it must be reversed upon
--- output (this is done in the createRPN function.)
-
-processInfixToken
-  :: ([OpStackVal], [R.RPNToken a])
-  -> InfixToken a
-  -> Maybe ([OpStackVal], [R.RPNToken a])
-processInfixToken (os, ts) t = case t of
-  TokRPN tok -> return $ processRPNToken (os, ts) tok
-  TokParen p -> processParen (os, ts) p
-
-
--- | If the token is a binary operator A, then:
---
--- If A is left associative, while there is an operator B of higher or
--- equal precedence than A at the top of the stack, pop B off the
--- stack and append it to the output.
---
--- If A is right associative, while there is an operator B of higher
--- precedence than A at the top of the stack, pop B off the stack and
--- append it to the output.
---
--- Push A onto the stack.
---
--- If a token is an operand, append it to the postfix output.
---
--- And has higher precedence than Or.
-processRPNToken
-  :: ([OpStackVal], [R.RPNToken a])
-  -> R.RPNToken a
-  -> ([OpStackVal], [R.RPNToken a])
-processRPNToken (os, ts) t = case t of
-  p@(R.TokOperand _) -> (os, p:ts)
-  R.TokOperator d -> case d of
-    R.OpNot -> (StkOp R.OpNot : os, ts)
-    R.OpAnd -> (StkOp R.OpAnd : os, ts)
-    R.OpOr ->
-      let (os', ts') = popper os ts
-      in (StkOp R.OpOr : os', ts')
-
--- | Pops operators from the operator stack and places then in the
--- output queue, as long as there is an And operator on the top of the
--- operator stack.
-popper :: [OpStackVal] -> [R.RPNToken a] -> ([OpStackVal], [R.RPNToken a])
-popper os ts = case os of
-  [] -> (os, ts)
-  x:xs -> case x of
-    StkOp R.OpAnd ->
-      let os' = xs
-          ts' = R.TokOperator R.OpAnd : ts
-      in popper os' ts'
-    _ -> (os, ts)
-
--- | Pops operators off the operator stack and onto the output stack
--- as long as the top of the operator stack is not an open
--- parenthesis. When an open parenthesis is encountered, pop that too,
--- but not onto the output stack. Fails if the stack has no open
--- parentheses.
-popThroughOpen
-  :: ([OpStackVal], [R.RPNToken a])
-  -> Maybe ([OpStackVal], [R.RPNToken a])
-popThroughOpen (os, ts) = case os of
-  [] -> Nothing
-  v:vs -> case v of
-    StkOp op -> popThroughOpen (vs, R.TokOperator op : ts)
-    StkOpenParen -> return (vs, ts)
-
--- | Places an open parenthesis on the top of the operator stack. For
--- Close parenthesis, pops operators off the operator stack through
--- the next open parenthesis on the operator stack.
-processParen
-  :: ([OpStackVal], [R.RPNToken a])
-  -> Paren
-  -> Maybe ([OpStackVal], [R.RPNToken a])
-processParen (os, ts) p = case p of
-  Open -> Just (StkOpenParen : os, ts)
-  Close -> popThroughOpen (os, ts)
-
--- | Creates an RPN expression from an infix one. Fails only if there
--- are mismatched parentheses. It is possible to create a nonsensical
--- RPN expression; the RPN parser must catch this.
-createRPN
-  :: Fdbl.Foldable f
-  => f (InfixToken a)
-  -- ^ The input tokens, with the beginning of the expression on the
-  -- left side of the sequence.
-
-  -> Maybe [R.RPNToken a]
-  -- ^ The output sequence of tokens, with the beginning of the
-  -- expression on the left side of the list.
-createRPN ts = do
-  (stack, toks) <- Fdbl.foldlM processInfixToken ([], []) ts
-  fmap reverse $ popRemainingOperators stack toks
-
--- | Pops remaining items off operator stack. Fails if there is an
--- open paren left on the stack, as this indicates mismatched
--- parenthesis.
-popRemainingOperators :: [OpStackVal] -> [R.RPNToken a] -> Maybe [R.RPNToken a]
-popRemainingOperators os ts = case os of
-  [] -> return ts
-  x:xs -> case x of
-    StkOp op -> popRemainingOperators xs (R.TokOperator op : ts)
-    StkOpenParen -> Nothing
diff --git a/Data/Prednote/Expressions/RPN.hs b/Data/Prednote/Expressions/RPN.hs
deleted file mode 100644
--- a/Data/Prednote/Expressions/RPN.hs
+++ /dev/null
@@ -1,79 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
--- | Postfix, or RPN, expression parsing.
---
--- This module parses RPN expressions where the operands are
--- predicates and the operators are one of @and@, @or@, or @not@,
--- where @and@ and @or@ are binary and @not@ is unary.
-module Data.Prednote.Expressions.RPN where
-
-import qualified Control.Monad.Exception.Synchronous as Ex
-import qualified Data.Foldable as Fdbl
-import qualified Data.Prednote.Pdct as P
-import Data.Prednote.Pdct ((&&&), (|||))
-import Data.Monoid ((<>))
-import Data.Text (Text)
-import qualified Data.Text as X
-import qualified System.Console.Rainbow as C
-
-type Error = Text
-
-data RPNToken a
-  = TokOperand (P.Pdct a)
-  | TokOperator Operator
-
-data Operator
-  = OpAnd
-  | OpOr
-  | OpNot
-  deriving Show
-
-pushOperand :: P.Pdct a -> [P.Pdct a] -> [P.Pdct a]
-pushOperand p ts = p : ts
-
-pushOperator
-  :: Operator
-  -> [P.Pdct a]
-  -> Ex.Exceptional Error [P.Pdct a]
-pushOperator o ts = case o of
-  OpAnd -> case ts of
-    x:y:zs -> return $ (y &&& x) : zs
-    _ -> Ex.throw $ err "and"
-  OpOr -> case ts of
-    x:y:zs -> return $ (y ||| x) : zs
-    _ -> Ex.throw $ err "or"
-  OpNot -> case ts of
-    x:zs -> return $ P.not x : zs
-    _ -> Ex.throw $ err "not"
-  where
-    err x = "insufficient operands to apply \"" <> x
-            <> "\" operator\n"
-
-pushToken
-  :: [P.Pdct a]
-  -> RPNToken a
-  -> Ex.Exceptional Error [P.Pdct a]
-pushToken ts t = case t of
-  TokOperand p -> return $ pushOperand p ts
-  TokOperator o -> pushOperator o ts
-
-
--- | Parses an RPN expression and returns the resulting Pdct. Fails if
--- there are no operands left on the stack or if there are multiple
--- operands left on the stack; the stack must contain exactly one
--- operand in order to succeed.
-parseRPN
-  :: Fdbl.Foldable f
-  => f (RPNToken a)
-  -> Ex.Exceptional Error (P.Pdct a)
-parseRPN ts = do
-  trees <- Fdbl.foldlM pushToken [] ts
-  case trees of
-    [] -> Ex.throw $ "bad expression: no operands left on the stack\n"
-    x:[] -> return x
-    xs -> Ex.throw
-      $ "bad expression: multiple operands left on the stack:\n"
-      <> ( X.concat
-           . map C._text
-           . concatMap (P.showPdct 4 0)
-           $ xs )
-
diff --git a/Data/Prednote/Pdct.hs b/Data/Prednote/Pdct.hs
deleted file mode 100644
--- a/Data/Prednote/Pdct.hs
+++ /dev/null
@@ -1,637 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
--- | Trees of predicates.
---
--- Exports names which conflict with Prelude names, so you probably
--- want to import this module qualified.
-
-module Data.Prednote.Pdct
-  ( -- * The Pdct tree
-    Label
-  , Pdct(..)
-  , Node(..)
-  , rename
-  , always
-  , never
-
-  -- * Creating operands
-  , operand
-
-  -- * Creating Pdct from other Pdct
-  , and
-  , or
-  , not
-  , neverFalse
-  , neverTrue
-  , (&&&)
-  , (|||)
-  , boxPdct
-  , boxNode
-
-  -- * Showing and evaluating Pdct
-  , Level
-  , IndentAmt
-  , ShowDiscards
-  , showPdct
-  , eval
-  , evaluate
-  , filter
-
-  -- * Helpers for building common Pdct
-  -- ** Non-overloaded
-  , compareBy
-  , compareByMaybe
-  , greaterBy
-  , lessBy
-  , equalBy
-  , greaterEqBy
-  , lessEqBy
-  , notEqBy
-
-  -- ** Overloaded
-  , compare
-  , greater
-  , less
-  , equal
-  , greaterEq
-  , lessEq
-  , notEq
-  , parseComparer
-
-  ) where
-
-import Control.Applicative ((<*>))
-import Data.Maybe (fromMaybe, isJust, catMaybes)
-import Data.Text (Text)
-import qualified Data.Text as X
-import Data.Monoid ((<>), mconcat, mempty)
-import Data.String (fromString)
-import qualified System.Console.Rainbow as R
-import Prelude hiding (not, and, or, compare, filter)
-import qualified Prelude
-
-type Label = Text
-
--- | A tree of predicates.
-data Pdct a = Pdct Label (Node a)
-
-instance Show (Pdct a) where
-  show = X.unpack
-         . X.concat
-         . map R._text
-         . showPdct 2 0
-
--- | Renames the top level of the Pdct. The function you pass will be
--- applied to the old name.
-rename :: (Text -> Text) -> Pdct a -> Pdct a
-rename f (Pdct l n) = Pdct (f l) n
-
-data Node a
-  = And [Pdct a]
-  -- ^ None of the Pdct in list may be Just False. An empty list or
-  -- list with only Nothing is Just True.
-
-  | Or [Pdct a]
-  -- ^ At least one of the Pdct in the list must be Just True. An
-  -- empty list or list with only Nothing is Just False.
-
-  | Not (Pdct a)
-  -- ^ Just True is Just False and vice versa; Nothing remains Nothing.
-
-  | NeverFalse (Pdct a)
-  -- ^ Just True if the child is Just True; Nothing otherwise.
-
-  | NeverTrue (Pdct a)
-  -- ^ Just False if the child is Just False; Nothing otherwise.
-
-  | Operand (a -> Maybe Bool)
-  -- ^ An operand may return Just True or Just False to indicate
-  -- success or failure. It may also return Nothing to indicate a
-  -- discard.
-
--- | Given a function that un-boxes values of type b, changes a Node
--- from type a to type b.
-boxNode
-  :: (b -> a)
-  -> Node a
-  -> Node b
-boxNode f n = case n of
-  And ls -> And $ map (boxPdct f) ls
-  Or ls -> Or $ map (boxPdct f) ls
-  Not o -> Not $ boxPdct f o
-  NeverFalse o -> NeverFalse $ boxPdct f o
-  NeverTrue o -> NeverTrue $ boxPdct f o
-  Operand g -> Operand $ \b -> g (f b)
-
-
--- | Given a function that un-boxes values of type b, changes a Pdct
--- from type a to type b.
-boxPdct
-  :: (b -> a)
-  -> Pdct a
-  -> Pdct b
-boxPdct f (Pdct l n) = Pdct l $ boxNode f n
-
-and :: [Pdct a] -> Pdct a
-and = Pdct "and" . And
-
-or :: [Pdct a] -> Pdct a
-or = Pdct "or" . Or
-
-not :: Pdct a -> Pdct a
-not = Pdct "not" . Not
-
--- | Creates a new operand. The Pdct is Just True or Just False, never
--- Nothing.
-operand :: Text -> (a -> Bool) -> Pdct a
-operand t = Pdct t . Operand . fmap Just
-
--- | Turns an existing Pdct to one that never says False. If the
--- underlying predicate returns Just True, the new Pdct also returns
--- Just True. Otherwise, the Pdct returns Nothing.
-neverFalse :: Pdct a -> Pdct a
-neverFalse = Pdct "never False" . NeverFalse
-
--- | Turns an existing Pdct to one that never says True. If the
--- underlying predicate returns Just False, the new Pdct also returns
--- Just False. Otherwise, the Pdct returns Nothing.
-neverTrue :: Pdct a -> Pdct a
-neverTrue = Pdct "never True" . NeverTrue
-
-
--- | Returns a tree that is always True.
-always :: Pdct a
-always = Pdct "always True" (Operand (const (Just True)))
-
--- | Returns a tree that is always False.
-never :: Pdct a
-never = Pdct "always False" (Operand (const (Just False)))
-
--- | Forms a Pdct using 'and'.
-(&&&) :: Pdct a -> Pdct a -> Pdct a
-(&&&) x y = Pdct "and" (And [x, y])
-infixr 3 &&&
-
--- | Forms a Pdct using 'or'.
-(|||) :: Pdct a -> Pdct a -> Pdct a
-(|||) x y = Pdct "or" (Or [x, y])
-infixr 2 |||
-
--- | How many levels of indentation to use. Typically you will start
--- this at zero. It is incremented by one for each level as functions
--- descend through the tree.
-type Level = Int
-
--- | The number of spaces to use for each level of indentation.
-type IndentAmt = Int
-
--- | Indents text, and adds a newline to the end.
-indent :: IndentAmt -> Level -> [R.Chunk] -> [R.Chunk]
-indent amt lvl cs = idt : (cs ++ [nl])
-  where
-    idt = fromString (replicate (lvl * amt) ' ')
-    nl = fromString "\n"
-
--- | Creates a plain Chunk from a Text.
-plain :: Text -> R.Chunk
-plain = R.Chunk mempty
-
--- | Shows a Pdct tree without evaluating it.
-showPdct :: IndentAmt -> Level -> Pdct a -> [R.Chunk]
-showPdct amt lvl (Pdct l pd) = case pd of
-  And ls -> indent amt lvl [plain l]
-            <> mconcat (map (showPdct amt (lvl + 1)) ls)
-  Or ls -> indent amt lvl [plain l]
-           <> mconcat (map (showPdct amt (lvl + 1)) ls)
-  Not t -> indent amt lvl [plain l]
-           <> showPdct amt (lvl + 1) t
-  NeverFalse t -> indent amt lvl [plain l]
-                  <> showPdct amt (lvl + 1) t
-  NeverTrue t -> indent amt lvl [plain l]
-                 <> showPdct amt (lvl + 1) t
-  Operand _ -> indent amt lvl [plain l]
-
-
-labelBool :: Text -> Maybe Bool -> [R.Chunk]
-labelBool t b = [open, trueFalse, close, blank, txt]
-  where
-    trueFalse = case b of
-      Nothing -> "discard" <> R.f_yellow
-      Just bl -> if bl
-        then "TRUE" <> R.f_green
-        else "FALSE" <> R.f_red
-    open = "["
-    close = "]"
-    blank = plain (X.replicate blankLen " ")
-    blankLen = X.length "discard"
-               - X.length (R._text trueFalse) + 1
-    txt = plain t
-
-type ShowDiscards = Bool
-
--- | Evaluates a Pdct.
-eval :: Pdct a -> a -> Maybe Bool
-eval (Pdct _ n) a = case n of
-  And ps -> Just . Prelude.and . catMaybes $ [flip eval a] <*> ps
-  Or ps -> Just . Prelude.or . catMaybes $ [flip eval a] <*> ps
-  Not p -> fmap Prelude.not $ eval p a
-  NeverFalse p -> case eval p a of
-    Nothing -> Nothing
-    Just b -> if Prelude.not b then Nothing else Just b
-  NeverTrue p -> case eval p a of
-    Nothing -> Nothing
-    Just b -> if b then Nothing else Just b
-  Operand f -> f a
-
--- | Verbosely evaluates a Pdct.
-evaluate
-  :: IndentAmt
-  -- ^ Indent each level by this many spaces.
-
-  -> ShowDiscards
-  -- ^ If True, show discarded test results; otherwise, hide
-  -- them.
-
-  -> a
-  -- ^ The subject to evaluate
-
-  -> Level
-  -- ^ How many levels deep in the tree we are. Typically you will
-  -- start at level 0. This determines the level of indentation.
-
-  -> Pdct a
-
-  -> (Maybe Bool, [R.Chunk])
-evaluate i sd a lvl (Pdct l pd) = case pd of
-
-  And ps -> let (resBool, resTxt) = evalAnd i sd a (lvl + 1) ps
-                txt = indent i lvl (labelBool l (Just resBool))
-                        <> resTxt
-            in (Just resBool, txt)
-
-  Or ps -> let (resBool, resTxt) = evalOr i sd a (lvl + 1) ps
-               txt = indent i lvl (labelBool l (Just resBool))
-                        <> resTxt
-           in (Just resBool, txt)
-
-  Not p -> let (childMayBool, childTxt) = evaluate i sd a (lvl + 1) p
-               thisMayBool = fmap Prelude.not childMayBool
-               thisTxt = indent i lvl (labelBool l thisMayBool)
-               txt = if sd || isJust thisMayBool
-                     then thisTxt <> childTxt else mempty
-           in (thisMayBool, txt)
-
-  NeverFalse p ->
-    let (childMayBool, childTxt) = evaluate i sd a (lvl + 1) p
-        thisMayBool = case childMayBool of
-          Nothing -> Nothing
-          Just b -> if Prelude.not b then Nothing else Just b
-        thisTxt = indent i lvl (labelBool l thisMayBool)
-        txt = if sd || isJust thisMayBool
-              then thisTxt <> childTxt else mempty
-    in (thisMayBool, txt)
-
-  NeverTrue p ->
-    let (childMayBool, childTxt) = evaluate i sd a (lvl + 1) p
-        thisMayBool = case childMayBool of
-          Nothing -> Nothing
-          Just b -> if b then Nothing else Just b
-        thisTxt = indent i lvl (labelBool l thisMayBool)
-        txt = if sd || isJust thisMayBool
-              then thisTxt <> childTxt else mempty
-    in (thisMayBool, txt)
-
-  Operand p -> let res = p a
-                   txt = indent i lvl (labelBool l res)
-               in (res, if sd || isJust res then txt else mempty)
-
-evalAnd :: IndentAmt -> ShowDiscards -> a
-        -> Level -> [Pdct a] -> (Bool, [R.Chunk])
-evalAnd i sd a l ts = (Prelude.not foundFalse, txt)
-  where
-    (foundFalse, txt) = go ts (False, mempty)
-    go [] p = p
-    go (x:xs) (fndFalse, acc) =
-      if fndFalse
-      then (fndFalse, acc <> indent i l
-                             [plain "(short circuit)"])
-      else let (res, cTxt) = evaluate i sd a l x
-               fndFalse' = maybe False Prelude.not res
-           in go xs (fndFalse', acc <> cTxt)
-
-evalOr :: IndentAmt -> ShowDiscards -> a
-       -> Level -> [Pdct a] -> (Bool, [R.Chunk])
-evalOr i sd a l ts = (foundTrue, txt)
-  where
-    (foundTrue, txt) = go ts (False, mempty)
-    go [] p = p
-    go (x:xs) (fnd, acc) =
-      if fnd
-      then (fnd, acc <> indent i l
-                        [plain "(short circuit)"])
-      else let (res, cTxt) = evaluate i sd a l x
-               fnd' = fromMaybe False res
-           in go xs (fnd', acc <> cTxt)
-
--- | Filters a list of items by including only the ones for which the
--- Pdct returns Just True. Also, renames each top-level Pdct so that
--- the textual results include a description of the item being
--- evaluated.
-filter
-  :: IndentAmt
-  -- ^ Indent each level by this many spaces.
-
-  -> ShowDiscards
-  -- ^ If True, show discarded test results; otherwise, hide
-  -- them.
-
-  -> Level
-  -- ^ How many levels deep in the tree we are. Typically you will
-  -- start at level 0. This determines the level of indentation.
-
-  -> (a -> Text)
-  -- ^ How to show each item. This is used to add a description of
-  -- each item to the verbose output. This Text should be a one-line
-  -- description, without any newlines.
-
-  -> Pdct a
-  -- ^ Use this Pdct to filter
-
-  -> [a]
-  -- ^ The list to filter
-
-  -> ([a], [R.Chunk])
-  -- ^ The results of the filtering, and the verbose output indicating
-  -- what was kept and discarded and why
-
-filter ident sd lvl swr pdct items =
-  let pds = map mkPd items
-      mkPd a = rename (\x -> mconcat [x, " - ", swr a]) pdct
-      results = zipWith mkResult pds items
-      mkResult p i = (evaluate ident sd i lvl p, i)
-      folder ((maybeBool, cks), i) (as, cksOld) = (as', cks ++ cksOld)
-        where
-          as' = if fromMaybe False maybeBool
-                then i:as
-                else as
-  in foldr folder ([], []) results
-
---
--- Helpers
---
-
--- | Build a Pdct that compares items.
-compareBy
-  :: Text
-  -- ^ How to show the item being compared; used to describe the Pdct
-
-  -> Text
-  -- ^ Description of the type of thing that is being matched
-
-  -> (a -> Ordering)
-  -- ^ How to compare an item against the right hand side. Return LT
-  -- if the item is less than the right hand side; GT if greater; EQ
-  -- if equal to the right hand side.
-
-  -> Ordering
-  -- ^ When subjects are compared, this ordering must be the result in
-  -- order for the Pdct to be Just True; otherwise it is Just
-  -- False. The subject will be on the left hand side.
-
-  -> Pdct a
-
-compareBy itemDesc typeDesc cmp ord = Pdct l (Operand f)
-  where
-    l = typeDesc <> " is " <> cmpDesc <> " " <> itemDesc
-    cmpDesc = case ord of
-      LT -> "less than"
-      GT -> "greater than"
-      EQ -> "equal to"
-    f subj = Just $ cmp subj == ord
-
--- | Like 'compareBy' but allows the comparison of items that may fail
--- to return an ordering.
-compareByMaybe
-  :: Text
-  -- ^ How to show the item being compared; used to describe the Pdct
-
-  -> Text
-  -- ^ Description of the type of thing that is being matched
-
-  -> (a -> Maybe Ordering)
-  -- ^ How to compare an item against the right hand side. Return Just
-  -- LT if the item is less than the right hand side; Just GT if
-  -- greater; Just EQ if equal to the right hand side. Return Nothing
-  -- if the item cannot return an item to be compared. The result of
-  -- the evaluation of the Pdct will then be Nothing.
-
-  -> Ordering
-  -- ^ When subjects are compared, this ordering must be the result in
-  -- order for the Pdct to be Just True; otherwise it is Just False,
-  -- or Nothing if the subject does not return an ordering. The
-  -- subject will be on the left hand side.
-
-  -> Pdct a
-
-compareByMaybe itemDesc typeDesc cmp ord = Pdct l (Operand f)
-  where
-    l = typeDesc <> " is " <> cmpDesc <> " " <> itemDesc
-    cmpDesc = case ord of
-      LT -> "less than"
-      GT -> "greater than"
-      EQ -> "equal to"
-    f subj = maybe Nothing (Just . (== ord)) $ cmp subj
-
--- | Overloaded version of 'compareBy'.
-compare
-  :: (Show a, Ord a)
-  => Text
-  -- ^ Description of the type of thing being matched
-
-  -> a
-  -- ^ The right hand side of the comparison.
-
-  -> Ordering
-  -- ^ When subjects are compared, this ordering must be the result in
-  -- order for the Pdct to be Just True; otherwise it is Just
-  -- False. The subject will be on the left hand side.
-
-  -> Pdct a
-compare typeDesc a ord = compareBy itemDesc typeDesc cmp ord
-  where
-    itemDesc = X.pack . show $ a
-    cmp item = Prelude.compare item a
-
-greater
-  :: (Show a, Ord a)
-  => Text
-  -- ^ How to show the item being compared; used to describe the Pdct
-
-  -> a
-  -- ^ The right hand side of the comparison.
-
-  -> Pdct a
-greater d a = compare d a GT
-
-less
-  :: (Show a, Ord a)
-  => Text
-  -- ^ How to show the item being compared; used to describe the Pdct
-
-  -> a
-  -- ^ The right hand side of the comparison.
-
-  -> Pdct a
-less d a = compare d a LT
-
-equal
-  :: (Show a, Ord a)
-  => Text
-  -- ^ How to show the item being compared; used to describe the Pdct
-
-  -> a
-  -- ^ The right hand side of the comparison.
-
-  -> Pdct a
-equal d a = compare d a EQ
-
-greaterEq
-  :: (Show a, Ord a)
-  => Text
-  -- ^ How to show the item being compared; used to describe the Pdct
-
-  -> a
-  -- ^ The right hand side of the comparison.
-
-  -> Pdct a
-greaterEq d a = greater d a ||| equal d a
-
-lessEq
-  :: (Show a, Ord a)
-  => Text
-  -- ^ How to show the item being compared; used to describe the Pdct
-
-  -> a
-  -- ^ The right hand side of the comparison.
-
-  -> Pdct a
-lessEq d a = less d a ||| equal d a
-
-notEq
-  :: (Show a, Ord a)
-  => Text
-  -- ^ How to show the item being compared; used to describe the Pdct
-
-  -> a
-  -- ^ The right hand side of the comparison.
-
-  -> Pdct a
-notEq d a = not $ equal d a
-
-greaterBy
-  :: Text
-  -- ^ How to show the item being compared; used to describe the Pdct
-
-  -> Text
-  -- ^ Description of the type of thing that is being matched
-
-  -> (a -> Ordering)
-  -- ^ How to compare two items
-
-  -> Pdct a
-greaterBy iD tD cmp = compareBy iD tD cmp GT
-
-lessBy
-  :: Text
-  -- ^ How to show the item being compared; used to describe the Pdct
-
-  -> Text
-  -- ^ Description of the type of thing that is being matched
-
-  -> (a -> Ordering)
-  -- ^ How to compare two items
-
-  -> Pdct a
-lessBy iD tD cmp = compareBy iD tD cmp LT
-
-equalBy
-  :: Text
-  -- ^ How to show the item being compared; used to describe the Pdct
-
-  -> Text
-  -- ^ Description of the type of thing that is being matched
-
-  -> (a -> Ordering)
-  -- ^ How to compare two items
-
-  -> Pdct a
-equalBy iD tD cmp = compareBy iD tD cmp EQ
-
-greaterEqBy
-  :: Text
-  -- ^ How to show the item being compared; used to describe the Pdct
-
-  -> Text
-  -- ^ Description of the type of thing that is being matched
-
-  -> (a -> Ordering)
-  -- ^ How to compare two items
-
-  -> Pdct a
-greaterEqBy iD tD cmp =
-  greaterBy iD tD cmp ||| equalBy iD tD cmp
-
-lessEqBy
-  :: Text
-  -- ^ How to show the item being compared; used to describe the Pdct
-
-  -> Text
-  -- ^ Description of the type of thing that is being matched
-
-  -> (a -> Ordering)
-  -- ^ How to compare two items
-
-  -> Pdct a
-lessEqBy iD tD cmp =
-  lessBy iD tD cmp ||| equalBy iD tD cmp
-
-notEqBy
-  :: Text
-  -- ^ How to show the item being compared; used to describe the Pdct
-
-  -> Text
-  -- ^ Description of the type of thing that is being matched
-
-  -> (a -> Ordering)
-  -- ^ How to compare two items
-
-  -> Pdct a
-notEqBy iD tD cmp =
-  not $ equalBy iD tD cmp
-
---
--- Comparer parsers
---
-
--- | Parses a string to find the correct comparer; returns the correct
--- function to build a Pdct.
-
-parseComparer
-  :: Text
-  -- ^ The string with the comparer to be parsed
-  -> (Ordering -> Pdct a)
-  -- ^ A function that, when given an ordering, returns a Pdct
-  -> Maybe (Pdct a)
-  -- ^ If an invalid comparer string is given, Nothing; otherwise, the
-  -- Pdct.
-parseComparer t f
-  | t == ">" = Just (f GT)
-  | t == "<" = Just (f LT)
-  | t == "=" = Just (f EQ)
-  | t == "==" = Just (f EQ)
-  | t == ">=" = Just (f GT ||| f EQ)
-  | t == "<=" = Just (f LT ||| f EQ)
-  | t == "/=" = Just (not $ f EQ)
-  | t == "!=" = Just (not $ f EQ)
-  | otherwise = Nothing
-
diff --git a/Data/Prednote/TestTree.hs b/Data/Prednote/TestTree.hs
deleted file mode 100644
--- a/Data/Prednote/TestTree.hs
+++ /dev/null
@@ -1,464 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
--- | Helps you build a tree of tests that run against a series of
--- items. This is best illustrated with an example.
---
--- Let's say that you have a list of Int. You want to make sure that
--- every Int in the list is odd and that every Int is greater than
--- zero. You also want to make sure that at least 5 Ints in the list
--- are greater than 20.
---
--- 'Pdct' from "Data.Prednote.Pdct" will help you, but only so much: a
--- 'Pdct' can test individual Int, but by itself it will not help you
--- run a check against a whole list of Int. Of course you can build
--- such a test fairly easily with 'any' and 'all', but what if you
--- want to view the results of the tests verbosely? That's where this
--- module comes in.
---
--- > {-# LANGUAGE OverloadedStrings #-}
--- > import System.Console.Rainbow
--- > import Data.Prednote.TestTree
--- > import Data.Prednote.Pdct
--- >
--- > isOdd :: Pdct Int
--- > isOdd = operand "is odd" odd
--- >
--- > greaterThan0 :: Pdct Int
--- > greaterThan0 = operand "greater than zero" (> 0)
--- >
--- > greaterThan20 :: Pdct Int
--- > greaterThan20 = operand "greater than 20" (> 20)
--- >
--- > myOpts :: TestOpts Int
--- > myOpts = TestOpts
--- >   { tIndentAmt = 2
--- >   , tPassVerbosity = TrueSubjects
--- >   , tFailVerbosity = TrueSubjects
--- >   , tGroupPred = const True
--- >   , tTestPred = const True
--- >   , tShowSkippedTests = True
--- >   , tGroupVerbosity = AllGroups
--- >   , tSubjects = mySubjects
--- >   , tStopOnFail = False
--- >   }
--- >
--- > mySubjects :: [Int]
--- > mySubjects = [2, 4, 6, 8, 10, 18, 19, 20, 21, 22, 24, 26]
--- >
--- > tests :: [TestTree Int]
--- > tests = [ isOdd, greaterThan0, greaterThan20 ]
--- >
--- > main :: IO ()
--- > main = do
--- >   let (cks, passed, failed) = runTests myOpts 0 tests
--- >   t <- termFromEnv
--- >   printChunks t cks
--- >   putStrLn $ "number of tests passed: " ++ show passed
--- >   putStrLn $ "number of tests failed: " ++ show failed
-module Data.Prednote.TestTree
-  (
-  -- * The TestTree
-    Name
-  , TestFunc
-  , TestTree (..)
-  , Payload (..)
-  , test
-
-  -- * Tests
-  , eachSubjectMustBeTrue
-  , nSubjectsMustBeTrue
-
-  -- * Grouping tests
-  , group
-
-  -- * Simple test runners
-  , Verbosity(..)
-  , GroupVerbosity (..)
-  , Pt.Level
-  , PassCount
-  , FailCount
-  , runTests
-
-  -- * Showing the test tree
-  , showTestTree
-
-  -- * Tree evaluator
-  , TestOpts (..)
-  , ShortCircuit
-  , Pass
-  , evalTree
-
-  ) where
-
-import Data.Either (rights)
-import Data.Maybe (isJust)
-import Data.List (unfoldr)
-import Data.Monoid ((<>), mempty)
-import qualified Data.Text as X
-import Data.Text (Text)
-import qualified Data.List.Split as Sp
-
-import qualified System.Console.Rainbow as R
-import qualified Data.Prednote.Pdct as Pt
-
---
--- Types
---
-
-type Pass = Bool
-
--- | The name of a test or of a group.
-type Name = Text
-
--- | A tree of tests.
-data TestTree a = TestTree Name (Payload a)
-
-data Payload a
-  = Group [TestTree a]
-  | Test (TestFunc a)
-
--- | A test is a function of this type. The function must make chunks
--- in a manner that respects the applicable verbosity.
-type TestFunc a
-  = Pt.IndentAmt
-  -> Verbosity
-  -- ^ Use this verbosity for tests that pass
-
-  -> Verbosity
-  -- ^ Use this verbosity for tests that fail
-
-  -> [a]
-  -> Pt.Level
-  -> (Pass, [R.Chunk])
-
-
--- | Creates groups of tests.
-group :: Name -> [TestTree a] -> TestTree a
-group n = TestTree n . Group
-
--- | Creates tests.
-test :: Name -> TestFunc a -> TestTree a
-test n = TestTree n . Test
-
--- | How verbose to be when reporting the results of tests. It would
--- be possible to have many more knobs to control this behavior; this
--- implementation is a compromise and hopefully provides enough
--- verbosity settings without being too complex.
-data Verbosity
-
-  = Silent
-  -- ^ Show nothing at all
-
-  | PassFail
-  -- ^ Show only whether the test passed or failed
-
-  | FalseSubjects
-  -- ^ Show subjects that are False. In addition, shows all evaluation
-  -- steps that led to the subject being False; however, does not show
-  -- discarded evaluation steps. Does not show True subjects at all.
-
-  | TrueSubjects
-  -- ^ Show subjects that are True. (This is cumulative, so False
-  -- subjects are shown too, as they would be using 'FalseSubjects'.)
-  -- Shows all evaluation steps that led to the subject being True;
-  -- however, does not show discarded evaluation steps.
-
-  | Discards
-  -- ^ Shows discarded subjects. Cumulative, so also does what
-  -- 'FalseSubjects' and 'TrueSubjects' do. Also shows all discarded
-  -- evaluation steps for all subjects.
-
-  deriving (Eq, Ord, Show)
-
---
--- Helper functions
---
-
-
--- | Determines whether to show a subject, and shows it.
-showSubject
-  :: (a -> X.Text)
-  -> Verbosity
-  -> Pt.IndentAmt
-  -> Pt.Level
-  -> Pt.Pdct a
-  -> (a, Maybe Bool)
-  -> [R.Chunk]
-showSubject swr v i l p (s, b) =
-  let (showSubj, showDisc) = isSubjectAndDiscardsShown v b
-      renamer txt = X.concat [swr s, " - ", txt]
-      renamed = Pt.rename renamer p
-  in if showSubj
-     then snd $ Pt.evaluate i showDisc s l renamed
-     else []
-
--- | Given a Verbosity and a Maybe Boolean indicating whether a
--- subject is True, False, or a discard, returns whether to show the
--- subject and whether to show the discards contained within the
--- subject.
-isSubjectAndDiscardsShown :: Verbosity -> Maybe Bool -> (Bool, Bool)
-isSubjectAndDiscardsShown v b = case v of
-  Silent -> (False, False)
-  PassFail -> (False, False)
-  FalseSubjects -> (not . isTrue $ b, False)
-  TrueSubjects -> (isJust b, False)
-  Discards -> (True, True)
-
-
--- | Creates a plain Chunk from a Text.
-plain :: X.Text -> R.Chunk
-plain = R.Chunk mempty
-
-showTestTitle :: Pt.IndentAmt -> Pt.Level -> Name -> Pass -> [R.Chunk]
-showTestTitle i l n p = [idt, open, passFail, close, blank, txt, nl]
-  where
-    idt = plain (X.replicate (i * l) " ")
-    nl = plain "\n"
-    passFail =
-      if p
-      then "PASS" <> R.f_green
-      else "FAIL" <> R.f_red
-    open = plain "["
-    close = plain "]"
-    blank = plain (X.singleton ' ')
-    txt = plain n
-
-isTrue :: Maybe Bool -> Bool
-isTrue = maybe False id
-
---
--- Tests
---
-
--- | Passes if every subject is True.
-eachSubjectMustBeTrue
-  :: Name
-  -> (a -> Text)
-  -> Pt.Pdct a
-  -> TestTree a
-eachSubjectMustBeTrue n swr p = TestTree n (Test tf)
-  where
-    tf i pv fv as lvl = (pass, cks)
-      where
-        rslts = zip as (map (Pt.eval p) as)
-        pass = all (isTrue . snd) rslts
-        v = if pass then pv else fv
-        cks = tit ++ subjectChunks
-        tit = if v == Silent then [] else showTestTitle i lvl n pass
-        subjectChunks =
-          concatMap (showSubject swr v i (lvl + 1) p) rslts
-
--- | Passes if at least n subjects are True.
-nSubjectsMustBeTrue
-  :: Name
-  -> (a -> X.Text)
-  -> Int
-  -> Pt.Pdct a
-  -> TestTree a
-nSubjectsMustBeTrue n swr count p = TestTree n (Test tf)
-  where
-    tf idnt pv fv as l = (pass, cks)
-      where
-        pd (_, res) = isTrue res
-        resultList = take count
-                     . Sp.split ( Sp.keepDelimsR
-                                  (Sp.dropFinalBlank . Sp.whenElt $ pd))
-                     $ zip as (map (Pt.eval p) as)
-        pass = length resultList >= count
-        v = if pass then pv else fv
-        cks = tit ++ subjectChunks
-        tit = if v == Silent then [] else showTestTitle idnt l n pass
-        subjectChunks =
-          concatMap (showSubject swr v idnt (l + 1) p)
-          . concat $ resultList
-
-indent :: Pt.IndentAmt -> Pt.Level -> Text -> R.Chunk
-indent amt lvl t = plain txt
-  where
-    txt = X.concat [spaces, t, "\n"]
-    spaces = X.replicate (amt * lvl) " "
-
-skip :: Text -> Pt.IndentAmt -> Pt.Level -> Text -> [R.Chunk]
-skip lbl amt lvl t =
-  [ plain (X.replicate (amt * lvl) " ")
-  , plain "["
-  , plain ("skip " <> lbl) <> R.f_yellow
-  , plain "] "
-  , plain t
-  , plain "\n"
-  ]
-
--- | Shows a tree, without evaluating it.
-showTestTree
-  :: Pt.IndentAmt
-  -> Pt.Level
-  -> TestTree a
-  -> [R.Chunk]
-showTestTree amt l (TestTree n p) = indent amt l n : children
-  where
-    children = case p of
-      Group ts -> concatMap (showTestTree amt l) ts
-      Test _ -> []
-
-
--- | Options for running tests.
-data TestOpts a = TestOpts
-
-  { tIndentAmt :: Int
-    -- ^ Indent each level by this many spaces
-
-  , tPassVerbosity :: Verbosity
-    -- ^ Use this verbosity for tests that pass
-
-  , tFailVerbosity :: Verbosity
-    -- ^ Use this verbosity for tests that fail
-
-  , tGroupPred :: Name -> Bool
-    -- ^ Groups are run only if this predicate returns True.
-
-  , tTestPred :: Name -> Bool
-    -- ^ Tests are run only if this predicate returns True.
-
-  , tShowSkippedTests :: Bool
-    -- ^ Some tests might be skipped; see 'tTestPred'. This controls
-    -- whether you want to see a notification of tests that were
-    -- skipped. (Does not affect skipped groups; see 'tGroupVerbosity'
-    -- for that.)
-
-  , tGroupVerbosity :: GroupVerbosity
-    -- ^ Show group names? Even if you do not show the names of
-    -- groups, tests within the group will still be indented.
-
-  , tSubjects :: [a]
-    -- ^ The subjects to test
-
-  , tStopOnFail :: Bool
-    -- ^ If True, then tests will stop running immediately after a
-    -- single test fails. If False, all tests are always run.
-  }
-
-
--- | True if the tree returned a result without completely evaluating
--- all parts of the tree. This can occur if 'tStopOnFail' is True and
--- one of the tests in the tree failed.
-type ShortCircuit = Bool
-
--- | Evaluates a tree. This function is the basis of 'runTests', which
--- is typically a bit easier to use.
-evalTree
-
-  :: TestOpts a
-  -- ^ Most options
-
-  -> Pt.Level
-  -- ^ The tree will indented by this many levels; typically you will
-  -- want to start this at 0.
-
-  -> TestTree a
-
-  -> (ShortCircuit, [Either [R.Chunk] (Pass, [R.Chunk])])
-  -- ^ The first element of the tuple is True if the tree was not
-  -- fully evaluated. This can happen if 'tStopOnFail' is True and
-  -- one of the tests in the tree failed. The second element of the
-  -- tuple is a list of Either; each element of the list will be a
-  -- Left if that component of the tree was not a test, or a Right if
-  -- that element was a test. The Right will contain a tuple, where
-  -- the first element indicates whether the test passed or failed,
-  -- and the second element is the list of Chunk.
-
-evalTree ee l (TestTree n p) = case p of
-  Group ts -> evalGroup ee n l ts
-  Test f -> evalTest ee n l f
-
-evalGroup
-  :: TestOpts a
-  -> Name
-  -> Pt.Level
-  -> [TestTree a]
-  -> (ShortCircuit, [Either [R.Chunk] (Pass, [R.Chunk])])
-evalGroup ee n l ts = if tGroupPred ee n
-  then let ls = unfoldr (unfoldList ee l) (False, ts)
-           stop = any not . map fst $ ls
-           rslts = concat . map snd $ ls
-           groupNm = if tGroupVerbosity ee /= NoGroups
-                     then indent (tIndentAmt ee) l n
-                     else plain ""
-        in (stop, Left [groupNm] : rslts)
-  else let groupNm = if tGroupVerbosity ee == AllGroups
-                     then skip "group" (tIndentAmt ee) l n
-                     else [plain ""]
-       in (False, [Left groupNm])
-
-
-evalTest
-  :: TestOpts a
-  -> Name
-  -> Pt.Level
-  -> TestFunc a
-  -> (ShortCircuit, [Either [R.Chunk] (Pass, [R.Chunk])])
-evalTest ee n l tf = if tTestPred ee n
-  then (not p, [Right (p, cs)])
-  else (False, skipped)
-  where
-    (p, cs) = tf (tIndentAmt ee) (tPassVerbosity ee)
-              (tFailVerbosity ee) (tSubjects ee) l
-    skipped = if tShowSkippedTests ee
-              then [Left $ skip "test" (tIndentAmt ee) l n]
-              else []
-
-
---
--- Running a group of tests
---
-
-type PassCount = Int
-type FailCount = Int
-
--- | How verbose to be when showing names of groups.
-data GroupVerbosity
-
-  = NoGroups
-  -- ^ Show no group names at all. However, groups will still be
-  -- indented.
-
-  | ActiveGroups
-  -- ^ Show groups that are not skipped.
-
-  | AllGroups
-  -- ^ Show all groups, and indicate which groups are skipped.
-  deriving (Eq, Ord, Show)
-
-
--- | Runs each test in a list of tests (though each test might not run
--- if 'tStopOnFail' is True.) Reports on how many passed and how many
--- failed. (if 'tStopOnFail' is True, the FailCount will never exceed
--- 1.)
-runTests
-  :: TestOpts a
-  -> Pt.Level
-  -> [TestTree a]
-  -> ([R.Chunk], PassCount, FailCount)
-runTests ee l ts =
-  let ls = unfoldr (unfoldList ee l) (False, ts)
-      testRs = rights . concatMap snd $ ls
-      passed = length . filter id . map fst $ testRs
-      failed = length . filter (not . id) . map fst $ testRs
-      cks = concat . map (either id snd) . concatMap snd $ ls
-  in (cks, passed, failed)
-
-unfoldList
-  :: TestOpts a
-  -> Pt.Level
-  -> (ShortCircuit, [TestTree a])
-  -> Maybe ( (ShortCircuit, [Either [R.Chunk] (Pass, [R.Chunk])])
-           , (ShortCircuit, [TestTree a]))
-unfoldList ee l (seenFalse, is) =
-  if seenFalse && tStopOnFail ee
-  then Nothing
-  else case is of
-        [] -> Nothing
-        t:xs ->
-          let (short, results) = evalTree ee l t
-          in Just ((short, results), (short, xs))
-
-
diff --git a/lib/Data/Prednote/Expressions.hs b/lib/Data/Prednote/Expressions.hs
new file mode 100644
--- /dev/null
+++ b/lib/Data/Prednote/Expressions.hs
@@ -0,0 +1,83 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | Handles parsing of both infix and RPN Pdct expressions.
+module Data.Prednote.Expressions
+  ( ExprDesc(..)
+  , Error
+  , Token
+  , operand
+  , opAnd
+  , opOr
+  , opNot
+  , openParen
+  , closeParen
+  , parseExpression
+  ) where
+
+import Data.Either (partitionEithers)
+import qualified Data.Text as X
+import qualified Data.Prednote.Expressions.Infix as I
+import qualified Data.Prednote.Expressions.RPN as R
+import qualified Data.Prednote.Pdct as P
+import qualified Control.Monad.Exception.Synchronous as Ex
+
+-- | A single type for both RPN tokens and infix tokens.
+newtype Token a = Token { unToken :: I.InfixToken a }
+type Error = X.Text
+
+-- | Creates Operands from Pdct.
+operand :: P.Pdct a -> Token a
+operand p = Token (I.TokRPN (R.TokOperand p))
+
+-- | The And operator
+opAnd :: Token a
+opAnd = Token (I.TokRPN (R.TokOperator R.OpAnd))
+
+-- | The Or operator
+opOr :: Token a
+opOr = Token (I.TokRPN (R.TokOperator R.OpOr))
+
+-- | The Not operator
+opNot :: Token a
+opNot = Token (I.TokRPN (R.TokOperator R.OpNot))
+
+-- | Open parentheses
+openParen :: Token a
+openParen = Token (I.TokParen I.Open)
+
+-- | Close parentheses
+closeParen :: Token a
+closeParen = Token (I.TokParen I.Close)
+
+-- | Is this an infix or RPN expression?
+data ExprDesc
+  = Infix
+  | RPN
+  deriving (Eq, Show)
+
+toksToRPN :: [Token a] -> Maybe [R.RPNToken a]
+toksToRPN toks
+  = let toEither t = case unToken t of
+          I.TokRPN tok -> Right tok
+          _ -> Left ()
+    in case partitionEithers . map toEither $ toks of
+        ([], xs) -> return xs
+        _ -> Nothing
+
+-- | Parses expressions. Fails if the expression is nonsensical in
+-- some way (for example, unbalanced parentheses, parentheses in an
+-- RPN expression, or multiple stack values remaining.) Works by first
+-- changing infix expressions to RPN ones.
+parseExpression
+  :: ExprDesc
+  -> [Token a]
+  -> Ex.Exceptional Error (P.Pdct a)
+parseExpression e toks = do
+  rpnToks <- case e of
+    Infix -> Ex.fromMaybe "unbalanced parentheses\n"
+             . I.createRPN
+             . map unToken
+             $ toks
+    RPN -> Ex.fromMaybe "parentheses in an RPN expression\n"
+           $ toksToRPN toks
+  R.parseRPN rpnToks
diff --git a/lib/Data/Prednote/Expressions/Infix.hs b/lib/Data/Prednote/Expressions/Infix.hs
new file mode 100644
--- /dev/null
+++ b/lib/Data/Prednote/Expressions/Infix.hs
@@ -0,0 +1,126 @@
+module Data.Prednote.Expressions.Infix
+  ( InfixToken (..)
+  , Paren(..)
+  , createRPN
+  ) where
+
+import qualified Data.Prednote.Expressions.RPN as R
+import qualified Data.Foldable as Fdbl
+
+data InfixToken a
+  = TokRPN (R.RPNToken a)
+  | TokParen Paren
+
+data Paren = Open | Close
+
+-- | Values on the operator stack.
+data OpStackVal
+  = StkOp R.Operator
+  | StkOpenParen
+
+-- In the shunting yard algorithm, the output sequence is a queue. The
+-- first values to go into the output sequence are the first to be
+-- processed by the RPN parser. In this module, the output sequence is
+-- implemented as a list stack, which means it must be reversed upon
+-- output (this is done in the createRPN function.)
+
+processInfixToken
+  :: ([OpStackVal], [R.RPNToken a])
+  -> InfixToken a
+  -> Maybe ([OpStackVal], [R.RPNToken a])
+processInfixToken (os, ts) t = case t of
+  TokRPN tok -> return $ processRPNToken (os, ts) tok
+  TokParen p -> processParen (os, ts) p
+
+
+-- | If the token is a binary operator A, then:
+--
+-- If A is left associative, while there is an operator B of higher or
+-- equal precedence than A at the top of the stack, pop B off the
+-- stack and append it to the output.
+--
+-- If A is right associative, while there is an operator B of higher
+-- precedence than A at the top of the stack, pop B off the stack and
+-- append it to the output.
+--
+-- Push A onto the stack.
+--
+-- If a token is an operand, append it to the postfix output.
+--
+-- And has higher precedence than Or.
+processRPNToken
+  :: ([OpStackVal], [R.RPNToken a])
+  -> R.RPNToken a
+  -> ([OpStackVal], [R.RPNToken a])
+processRPNToken (os, ts) t = case t of
+  p@(R.TokOperand _) -> (os, p:ts)
+  R.TokOperator d -> case d of
+    R.OpNot -> (StkOp R.OpNot : os, ts)
+    R.OpAnd -> (StkOp R.OpAnd : os, ts)
+    R.OpOr ->
+      let (os', ts') = popper os ts
+      in (StkOp R.OpOr : os', ts')
+
+-- | Pops operators from the operator stack and places then in the
+-- output queue, as long as there is an And operator on the top of the
+-- operator stack.
+popper :: [OpStackVal] -> [R.RPNToken a] -> ([OpStackVal], [R.RPNToken a])
+popper os ts = case os of
+  [] -> (os, ts)
+  x:xs -> case x of
+    StkOp R.OpAnd ->
+      let os' = xs
+          ts' = R.TokOperator R.OpAnd : ts
+      in popper os' ts'
+    _ -> (os, ts)
+
+-- | Pops operators off the operator stack and onto the output stack
+-- as long as the top of the operator stack is not an open
+-- parenthesis. When an open parenthesis is encountered, pop that too,
+-- but not onto the output stack. Fails if the stack has no open
+-- parentheses.
+popThroughOpen
+  :: ([OpStackVal], [R.RPNToken a])
+  -> Maybe ([OpStackVal], [R.RPNToken a])
+popThroughOpen (os, ts) = case os of
+  [] -> Nothing
+  v:vs -> case v of
+    StkOp op -> popThroughOpen (vs, R.TokOperator op : ts)
+    StkOpenParen -> return (vs, ts)
+
+-- | Places an open parenthesis on the top of the operator stack. For
+-- Close parenthesis, pops operators off the operator stack through
+-- the next open parenthesis on the operator stack.
+processParen
+  :: ([OpStackVal], [R.RPNToken a])
+  -> Paren
+  -> Maybe ([OpStackVal], [R.RPNToken a])
+processParen (os, ts) p = case p of
+  Open -> Just (StkOpenParen : os, ts)
+  Close -> popThroughOpen (os, ts)
+
+-- | Creates an RPN expression from an infix one. Fails only if there
+-- are mismatched parentheses. It is possible to create a nonsensical
+-- RPN expression; the RPN parser must catch this.
+createRPN
+  :: Fdbl.Foldable f
+  => f (InfixToken a)
+  -- ^ The input tokens, with the beginning of the expression on the
+  -- left side of the sequence.
+
+  -> Maybe [R.RPNToken a]
+  -- ^ The output sequence of tokens, with the beginning of the
+  -- expression on the left side of the list.
+createRPN ts = do
+  (stack, toks) <- Fdbl.foldlM processInfixToken ([], []) ts
+  fmap reverse $ popRemainingOperators stack toks
+
+-- | Pops remaining items off operator stack. Fails if there is an
+-- open paren left on the stack, as this indicates mismatched
+-- parenthesis.
+popRemainingOperators :: [OpStackVal] -> [R.RPNToken a] -> Maybe [R.RPNToken a]
+popRemainingOperators os ts = case os of
+  [] -> return ts
+  x:xs -> case x of
+    StkOp op -> popRemainingOperators xs (R.TokOperator op : ts)
+    StkOpenParen -> Nothing
diff --git a/lib/Data/Prednote/Expressions/RPN.hs b/lib/Data/Prednote/Expressions/RPN.hs
new file mode 100644
--- /dev/null
+++ b/lib/Data/Prednote/Expressions/RPN.hs
@@ -0,0 +1,79 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | Postfix, or RPN, expression parsing.
+--
+-- This module parses RPN expressions where the operands are
+-- predicates and the operators are one of @and@, @or@, or @not@,
+-- where @and@ and @or@ are binary and @not@ is unary.
+module Data.Prednote.Expressions.RPN where
+
+import qualified Control.Monad.Exception.Synchronous as Ex
+import qualified Data.Foldable as Fdbl
+import qualified Data.Prednote.Pdct as P
+import Data.Prednote.Pdct ((&&&), (|||))
+import Data.Monoid ((<>))
+import Data.Text (Text)
+import qualified Data.Text as X
+import qualified System.Console.Rainbow as C
+
+type Error = Text
+
+data RPNToken a
+  = TokOperand (P.Pdct a)
+  | TokOperator Operator
+
+data Operator
+  = OpAnd
+  | OpOr
+  | OpNot
+  deriving Show
+
+pushOperand :: P.Pdct a -> [P.Pdct a] -> [P.Pdct a]
+pushOperand p ts = p : ts
+
+pushOperator
+  :: Operator
+  -> [P.Pdct a]
+  -> Ex.Exceptional Error [P.Pdct a]
+pushOperator o ts = case o of
+  OpAnd -> case ts of
+    x:y:zs -> return $ (y &&& x) : zs
+    _ -> Ex.throw $ err "and"
+  OpOr -> case ts of
+    x:y:zs -> return $ (y ||| x) : zs
+    _ -> Ex.throw $ err "or"
+  OpNot -> case ts of
+    x:zs -> return $ P.not x : zs
+    _ -> Ex.throw $ err "not"
+  where
+    err x = "insufficient operands to apply \"" <> x
+            <> "\" operator\n"
+
+pushToken
+  :: [P.Pdct a]
+  -> RPNToken a
+  -> Ex.Exceptional Error [P.Pdct a]
+pushToken ts t = case t of
+  TokOperand p -> return $ pushOperand p ts
+  TokOperator o -> pushOperator o ts
+
+
+-- | Parses an RPN expression and returns the resulting Pdct. Fails if
+-- there are no operands left on the stack or if there are multiple
+-- operands left on the stack; the stack must contain exactly one
+-- operand in order to succeed.
+parseRPN
+  :: Fdbl.Foldable f
+  => f (RPNToken a)
+  -> Ex.Exceptional Error (P.Pdct a)
+parseRPN ts = do
+  trees <- Fdbl.foldlM pushToken [] ts
+  case trees of
+    [] -> Ex.throw $ "bad expression: no operands left on the stack\n"
+    x:[] -> return x
+    xs -> Ex.throw
+      $ "bad expression: multiple operands left on the stack:\n"
+      <> ( X.concat
+           . map C._text
+           . concatMap (P.showPdct 4 0)
+           $ xs )
+
diff --git a/lib/Data/Prednote/Pdct.hs b/lib/Data/Prednote/Pdct.hs
new file mode 100644
--- /dev/null
+++ b/lib/Data/Prednote/Pdct.hs
@@ -0,0 +1,655 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | Trees of predicates.
+--
+-- Exports names which conflict with Prelude names, so you probably
+-- want to import this module qualified.
+
+module Data.Prednote.Pdct
+
+  ( -- * The Pdct tree
+    Label
+  , Hide
+  , Pdct(..)
+  , Node(..)
+
+  -- * Creating Pdct.
+  -- | All functions create Pdct that are shown by default.
+  , operand
+  , and
+  , or
+  , not
+  , (&&&)
+  , (|||)
+  , always
+  , never
+  , boxPdct
+  , boxNode
+
+  -- * Controlling whether Pdct are shown in the results
+  , hide
+  , show
+  , hideTrue
+  , hideFalse
+
+  -- * Renaming Pdct
+  , rename
+
+  -- * Result
+  , Result(..)
+  , RNode(..)
+
+  -- * Showing and evaluating Pdct
+  , evaluate
+  , evaluateNode
+  , IndentAmt
+  , Level
+  , ShowAll
+  , showResult
+  , showTopResult
+  , showPdct
+  , filter
+  , verboseFilter
+
+  -- * Helpers for building common Pdct
+  -- ** Non-overloaded
+  , compareBy
+  , compareByMaybe
+  , greaterBy
+  , lessBy
+  , equalBy
+  , greaterEqBy
+  , lessEqBy
+  , notEqBy
+
+  -- ** Overloaded
+  , compare
+  , greater
+  , less
+  , equal
+  , greaterEq
+  , lessEq
+  , notEq
+  , parseComparer
+
+  ) where
+
+
+-- # Imports
+
+import Data.Text (Text)
+import qualified Data.Text as X
+import Data.Monoid ((<>), mconcat, mempty)
+import Data.String (fromString)
+import qualified System.Console.Rainbow as R
+import Prelude hiding (not, and, or, compare, filter, show)
+import qualified Prelude
+
+-- # Pdct type
+
+type Label = Text
+
+-- | Determines whether a result is shown by default.
+type Hide = Bool
+
+-- | A predicate. Each Pdct contains a tree of Node.
+data Pdct a = Pdct
+  { pLabel :: Label
+  -- ^ Label used when showing the results
+
+  , pHide :: (Bool -> Hide)
+  -- ^ As results are computed, this function is applied to the
+  -- result. If this function returns False, then this Pdct will not
+  -- be shown by default in the results.
+
+  , pNode :: Node a
+
+  }
+
+data Node a
+  = And [Pdct a]
+  -- ^ Conjunction. If any Pdct in the list is False, the result is
+  -- False. If the list is empty, the result is True.
+
+  | Or [Pdct a]
+  -- ^ Disjunction. If at least one Pdct in the list is True, the
+  -- result it True. If the list is empty, the result is False.
+
+  | Not (Pdct a)
+  -- ^ Negation
+
+  | Operand (a -> Bool)
+  -- ^ Most basic building block.
+
+-- | Renames the top level of the Pdct. The function you pass will be
+-- applied to the old name.
+rename :: (Text -> Text) -> Pdct a -> Pdct a
+rename f p = p { pLabel = f (pLabel p) }
+
+-- | Always True
+always :: Pdct a
+always = Pdct "always True" (const False) (Operand (const True))
+
+-- | Always False
+never :: Pdct a
+never = Pdct "always False" (const False) (Operand (const False))
+
+-- | Creates and labels operands.
+operand :: Label -> (a -> Bool) -> Pdct a
+operand l = Pdct l (const False) . Operand
+
+-- | Creates And Pdct using a generic name
+and :: [Pdct a] -> Pdct a
+and = Pdct "and" (const False) . And
+
+-- | Creates Or Pdct using a generic name
+or :: [Pdct a] -> Pdct a
+or = Pdct "or" (const False) . Or
+
+-- | Creates Not Pdct using a generic name
+not :: Pdct a -> Pdct a
+not = Pdct "not" (const False) . Not
+
+-- | Changes a Pdct so it is always hidden by default.
+hide :: Pdct a -> Pdct a
+hide p = p { pHide = const True }
+
+-- | Changes a Pdct so it is always shown by default.
+show :: Pdct a -> Pdct a
+show p = p { pHide = const False }
+
+-- | Changes a Pdct so that it is hidden if its result is True.
+hideTrue :: Pdct a -> Pdct a
+hideTrue p = p { pHide = id }
+
+-- | Changes a Pdct so that it is hidden if its result is False.
+hideFalse :: Pdct a -> Pdct a
+hideFalse p = p { pHide = Prelude.not }
+
+-- | Forms a Pdct using 'and'; assigns a generic label.
+(&&&) :: Pdct a -> Pdct a -> Pdct a
+(&&&) x y = Pdct "and" (const False) (And [x, y])
+infixr 3 &&&
+
+-- | Forms a Pdct using 'or'; assigns a generic label.
+(|||) :: Pdct a -> Pdct a -> Pdct a
+(|||) x y = Pdct "or" (const False) (Or [x, y])
+infixr 2 |||
+
+-- | Given a function that un-boxes values of type b, changes a Pdct
+-- from type a to type b.
+boxPdct
+  :: (b -> a)
+  -> Pdct a
+  -> Pdct b
+boxPdct f (Pdct l d n) = Pdct l d $ boxNode f n
+
+-- | Given a function that un-boxes values of type b, changes a Node
+-- from type a to type b.
+boxNode
+  :: (b -> a)
+  -> Node a
+  -> Node b
+boxNode f n = case n of
+  And ls -> And $ map (boxPdct f) ls
+  Or ls -> Or $ map (boxPdct f) ls
+  Not o -> Not $ boxPdct f o
+  Operand g -> Operand $ \b -> g (f b)
+
+-- # Result
+
+-- | The result from evaluating a Pdct.
+data Result = Result
+  { rLabel :: Label
+  -- ^ The label from the original Pdct
+
+  , rBool :: Bool
+  -- ^ The boolean result from evaluating the node. If the node is an
+  -- operand, this is the result of applying the operand function to
+  -- the subject. Otherwise, this is the result of application of the
+  -- appropriate boolean operation to the child nodes.
+
+  , rHide :: Hide
+  -- ^ Is this result hidden in the result by default? Hiding only
+  -- affects presentation; it does not affect how this Pdct affects
+  -- any parent Pdct.
+  , rNode :: RNode
+  } deriving (Eq, Show)
+
+data RNode
+  = RAnd [Result]
+  | ROr [Result]
+  | RNot Result
+  | ROperand Bool
+  deriving (Eq, Show)
+
+-- | Applies a Pdct to a particular value, known as the subject.
+evaluate :: a -> Pdct a -> Result
+evaluate a (Pdct l d n) = Result l r d' rn
+  where
+    rn = evaluateNode a n
+    r = case rn of
+      RAnd ls -> all rBool ls
+      ROr ls -> any rBool ls
+      RNot x -> Prelude.not . rBool $ x
+      ROperand b -> b
+    d' = d r
+
+evaluateNode :: a -> Node a -> RNode
+evaluateNode a n = case n of
+  And ls -> RAnd (map (evaluate a) ls)
+  Or ls -> ROr (map (evaluate a) ls)
+  Not l -> RNot (evaluate a l)
+  Operand f -> ROperand (f a)
+
+-- # Types and functions for showing
+
+-- | The number of spaces to use for each level of indentation.
+type IndentAmt = Int
+
+-- | How many levels of indentation to use. Typically you will start
+-- this at zero. It is incremented by one for each level as functions
+-- descend through the tree.
+type Level = Int
+
+-- | Indents text, and adds a newline to the end.
+indent :: IndentAmt -> Level -> [R.Chunk] -> [R.Chunk]
+indent amt lvl cs = idt : (cs ++ [nl])
+  where
+    idt = fromString (replicate (lvl * amt) ' ')
+    nl = fromString "\n"
+
+-- # Showing Pdct
+
+-- | Creates a plain Chunk from a Text.
+plain :: Text -> R.Chunk
+plain = R.Chunk mempty
+
+-- | Shows a Pdct tree without evaluating it.
+showPdct :: IndentAmt -> Level -> Pdct a -> [R.Chunk]
+showPdct amt lvl (Pdct l _ pd) = case pd of
+  And ls -> indent amt lvl [plain ("and - " <> l)]
+            <> mconcat (map (showPdct amt (lvl + 1)) ls)
+  Or ls -> indent amt lvl [plain ("or - " <> l)]
+           <> mconcat (map (showPdct amt (lvl + 1)) ls)
+  Not t -> indent amt lvl [plain ("not - " <> l)]
+           <> showPdct amt (lvl + 1) t
+  Operand _ -> indent amt lvl [plain ("operand - " <> l)]
+
+instance Show (Pdct a) where
+  show = X.unpack
+       . X.concat
+       . map R._text
+       . showPdct 2 0
+
+
+filter :: Pdct a -> [a] -> [a]
+filter pd as
+  = map fst
+  . Prelude.filter (rBool . snd)
+  . zip as
+  . map (flip evaluate pd)
+  $ as
+
+
+-- # Showing Result
+
+labelBool :: Text -> Bool -> [R.Chunk]
+labelBool t b = [open, trueFalse, close, blank, txt]
+  where
+    trueFalse = 
+      if b then "TRUE" <> R.f_green else "FALSE" <> R.f_red
+    open = "["
+    close = "]"
+    blank = plain (X.replicate blankLen " ")
+    blankLen = X.length "discard"
+               - X.length (R._text trueFalse) + 1
+    txt = plain t
+
+type ShowAll = Bool
+
+-- | Shows a Result in a pretty way with colors and indentation.
+showResult
+  :: IndentAmt
+  -- ^ Indent each level by this many spaces
+
+  -> ShowAll
+  -- ^ If True, shows all Pdct, even ones where 'rHide' is
+  -- True. Otherwise, respects 'rHide' and does not show hidden Pdct.
+
+  -> Level
+  -- ^ How deep in the tree we are; this increments by one for each
+  -- level of descent.
+
+  -> Result
+  -- ^ The result to show
+
+  -> [R.Chunk]
+showResult amt sa lvl (Result lbl rslt hd nd)
+  | hd && Prelude.not sa = []
+  | otherwise = firstLine ++ restLines
+  where
+    firstLine = indent amt lvl $ labelBool lbl rslt
+    restLines = case nd of
+      RAnd ls -> f False ls
+      ROr ls -> f True ls
+      RNot r -> showResult amt sa (lvl + 1) r
+      ROperand _ -> []
+    f stopOn ls = concatMap sr ls' ++ end
+      where
+        ls' = takeThrough ((== stopOn) . rBool) ls
+        sr = showResult amt sa (lvl + 1)
+        end = if ls' `shorter` ls
+              then indent amt (lvl + 1) ["(short circuit)"]
+              else []
+
+-- | @shorter x y@ is True if list x is shorter than list y. Lazier
+-- than taking the length of each list and comparing the results.
+shorter :: [a] -> [a] -> Bool
+shorter [] [] = False
+shorter (_:_) [] = False
+shorter [] (_:_) = True
+shorter (_:xs) (_:ys) = shorter xs ys
+
+-- | For instance,
+-- > takeThrough odd [2,4,6,7,8] == [2,4,6,7]
+takeThrough :: (a -> Bool) -> [a] -> [a]
+takeThrough _ [] = []
+takeThrough f (x:xs) = x : if f x then [] else takeThrough f xs
+
+-- | Shows the top of a Result tree and all the child Results. Adds a
+-- short label at the top of the tree.
+showTopResult
+  :: X.Text
+  -- ^ Label to add to the top of the tree.
+  -> IndentAmt
+  -- ^ Indent each level by this many spaces
+  -> Level
+  -- ^ Indent the top by this many levels
+  -> ShowAll
+  -- ^ If True, shows all Pdct, even ones where 'rHide' is
+  -- True. Otherwise, respects 'rHide' and does not show hidden Pdct.
+
+  -> Result
+  -- ^ The result to show
+  -> [R.Chunk]
+showTopResult txt i lvl sd r = showResult i sd lvl r'
+  where
+    r' = r { rLabel = rLabel r <> " - " <> txt }
+
+
+-- | Filters a list. Also returns chunks describing the process.
+verboseFilter
+  :: (a -> X.Text)
+  -- ^ How to describe each subject
+
+  -> IndentAmt
+  -- ^ Indent each level by this many spaces
+
+  -> ShowAll
+  -- ^ If True, shows all Pdct, even ones where 'rHide' is
+  -- True. Otherwise, respects 'rHide' and does not show hidden Pdct.
+
+  -> Pdct a
+  -- ^ Used to perform the filtering
+
+  -> [a]
+  -> ([R.Chunk], [a])
+
+verboseFilter desc amt sa pd as = (chks, as')
+  where
+    rs = map (flip evaluate pd) as
+    subjAndRslts = zip as rs
+    mkChks (subj, rslt) = showTopResult (desc subj) amt 0 sa rslt
+    chks = concatMap mkChks subjAndRslts
+    as' = map fst . Prelude.filter (rBool . snd) $ subjAndRslts
+
+-- # Comparisons
+
+-- | Build a Pdct that compares items.
+compareBy
+  :: Text
+  -- ^ How to show the item being compared; used to describe the Pdct
+
+  -> Text
+  -- ^ Description of the type of thing that is being matched
+
+  -> (a -> Ordering)
+  -- ^ How to compare an item against the right hand side. Return LT
+  -- if the item is less than the right hand side; GT if greater; EQ
+  -- if equal to the right hand side.
+
+  -> Ordering
+  -- ^ When subjects are compared, this ordering must be the result in
+  -- order for the Pdct to be True; otherwise it is False. The subject
+  -- will be on the left hand side.
+
+  -> Pdct a
+
+compareBy itemDesc typeDesc cmp ord = Pdct l (const False) (Operand f)
+  where
+    l = typeDesc <> " is " <> cmpDesc <> " " <> itemDesc
+    cmpDesc = case ord of
+      LT -> "less than"
+      GT -> "greater than"
+      EQ -> "equal to"
+    f subj = cmp subj == ord
+
+-- | Overloaded version of 'compareBy'.
+compare
+  :: (Show a, Ord a)
+  => Text
+  -- ^ Description of the type of thing being matched
+
+  -> a
+  -- ^ The right hand side of the comparison.
+
+  -> Ordering
+  -- ^ When subjects are compared, this ordering must be the result in
+  -- order for the Pdct to be True; otherwise it is False. The subject
+  -- will be on the left hand side.
+
+  -> Pdct a
+compare typeDesc a ord = compareBy itemDesc typeDesc cmp ord
+  where
+    itemDesc = X.pack . Prelude.show $ a
+    cmp item = Prelude.compare item a
+
+-- | Builds a Pdct for items that might fail to return a comparison.
+compareByMaybe
+  :: Text
+  -- ^ How to show the item being compared
+
+  -> Text
+  -- ^ Description of type of thing being matched
+
+  -> (a -> Maybe Ordering)
+  -- ^ How to compare against right hand side. If Nothing, a Pdct that
+  -- always returns False is returned.
+
+  -> Ordering
+  -- ^ Ordering that must result for the Pdct to be True
+
+  -> Pdct a
+
+compareByMaybe itemDesc typeDesc cmp ord =
+  Pdct l (const False) (Operand f)
+  where
+    l = typeDesc <> " is " <> cmpDesc <> " " <> itemDesc
+    cmpDesc = case ord of
+      LT -> "less than"
+      GT -> "greater than"
+      EQ -> "equal to"
+    f subj = case cmp subj of
+      Nothing -> False
+      Just ord' -> ord == ord'
+
+greater
+  :: (Show a, Ord a)
+  => Text
+  -- ^ How to show the item being compared; used to describe the Pdct
+
+  -> a
+  -- ^ The right hand side of the comparison.
+
+  -> Pdct a
+greater d a = compare d a GT
+
+less
+  :: (Show a, Ord a)
+  => Text
+  -- ^ How to show the item being compared; used to describe the Pdct
+
+  -> a
+  -- ^ The right hand side of the comparison.
+
+  -> Pdct a
+less d a = compare d a LT
+
+equal
+  :: (Show a, Ord a)
+  => Text
+  -- ^ How to show the item being compared; used to describe the Pdct
+
+  -> a
+  -- ^ The right hand side of the comparison.
+
+  -> Pdct a
+equal d a = compare d a EQ
+
+greaterEq
+  :: (Show a, Ord a)
+  => Text
+  -- ^ How to show the item being compared; used to describe the Pdct
+
+  -> a
+  -- ^ The right hand side of the comparison.
+
+  -> Pdct a
+greaterEq d a = greater d a ||| equal d a
+
+lessEq
+  :: (Show a, Ord a)
+  => Text
+  -- ^ How to show the item being compared; used to describe the Pdct
+
+  -> a
+  -- ^ The right hand side of the comparison.
+
+  -> Pdct a
+lessEq d a = less d a ||| equal d a
+
+notEq
+  :: (Show a, Ord a)
+  => Text
+  -- ^ How to show the item being compared; used to describe the Pdct
+
+  -> a
+  -- ^ The right hand side of the comparison.
+
+  -> Pdct a
+notEq d a = not $ equal d a
+
+greaterBy
+  :: Text
+  -- ^ How to show the item being compared; used to describe the Pdct
+
+  -> Text
+  -- ^ Description of the type of thing that is being matched
+
+  -> (a -> Ordering)
+  -- ^ How to compare two items
+
+  -> Pdct a
+greaterBy iD tD cmp = compareBy iD tD cmp GT
+
+lessBy
+  :: Text
+  -- ^ How to show the item being compared; used to describe the Pdct
+
+  -> Text
+  -- ^ Description of the type of thing that is being matched
+
+  -> (a -> Ordering)
+  -- ^ How to compare two items
+
+  -> Pdct a
+lessBy iD tD cmp = compareBy iD tD cmp LT
+
+equalBy
+  :: Text
+  -- ^ How to show the item being compared; used to describe the Pdct
+
+  -> Text
+  -- ^ Description of the type of thing that is being matched
+
+  -> (a -> Ordering)
+  -- ^ How to compare two items
+
+  -> Pdct a
+equalBy iD tD cmp = compareBy iD tD cmp EQ
+
+greaterEqBy
+  :: Text
+  -- ^ How to show the item being compared; used to describe the Pdct
+
+  -> Text
+  -- ^ Description of the type of thing that is being matched
+
+  -> (a -> Ordering)
+  -- ^ How to compare two items
+
+  -> Pdct a
+greaterEqBy iD tD cmp =
+  greaterBy iD tD cmp ||| equalBy iD tD cmp
+
+lessEqBy
+  :: Text
+  -- ^ How to show the item being compared; used to describe the Pdct
+
+  -> Text
+  -- ^ Description of the type of thing that is being matched
+
+  -> (a -> Ordering)
+  -- ^ How to compare two items
+
+  -> Pdct a
+lessEqBy iD tD cmp =
+  lessBy iD tD cmp ||| equalBy iD tD cmp
+
+notEqBy
+  :: Text
+  -- ^ How to show the item being compared; used to describe the Pdct
+
+  -> Text
+  -- ^ Description of the type of thing that is being matched
+
+  -> (a -> Ordering)
+  -- ^ How to compare two items
+
+  -> Pdct a
+notEqBy iD tD cmp =
+  not $ equalBy iD tD cmp
+
+-- | Parses a string to find the correct comparer; returns the correct
+-- function to build a Pdct.
+
+parseComparer
+  :: Text
+  -- ^ The string with the comparer to be parsed
+  -> (Ordering -> Pdct a)
+  -- ^ A function that, when given an ordering, returns a Pdct
+  -> Maybe (Pdct a)
+  -- ^ If an invalid comparer string is given, Nothing; otherwise, the
+  -- Pdct.
+parseComparer t f
+  | t == ">" = Just (f GT)
+  | t == "<" = Just (f LT)
+  | t == "=" = Just (f EQ)
+  | t == "==" = Just (f EQ)
+  | t == ">=" = Just (f GT ||| f EQ)
+  | t == "<=" = Just (f LT ||| f EQ)
+  | t == "/=" = Just (not $ f EQ)
+  | t == "!=" = Just (not $ f EQ)
+  | otherwise = Nothing
+
diff --git a/lib/Data/Prednote/Test.hs b/lib/Data/Prednote/Test.hs
new file mode 100644
--- /dev/null
+++ b/lib/Data/Prednote/Test.hs
@@ -0,0 +1,216 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | Helps you build tests that run against a series of items.
+module Data.Prednote.Test
+  (
+  -- * Test data types
+    Name
+  , Verbosity(..)
+  , TrueVerbosity
+  , FalseVerbosity
+  , ShowTest(..)
+  , TestVerbosity(..)
+  , Pass
+  , Test(..)
+  , TestResult(..)
+
+  -- * Pre-built tests
+  , eachSubjectMustBeTrue
+  , nSubjectsMustBeTrue
+
+  -- * Running and showing tests
+  , evalTest
+  , showResult
+
+  ) where
+
+import Data.Maybe (fromMaybe)
+import Data.Monoid ((<>), mempty)
+import qualified Data.Text as X
+import Data.Text (Text)
+
+import qualified System.Console.Rainbow as R
+import qualified Data.Prednote.Pdct as Pt
+
+-- # Types
+
+-- | How verbose to be when showing the results of running a Pdct on a
+-- single subject.
+data Verbosity
+  = HideAll
+  -- ^ Do not show any results from the Pdct
+
+  | ShowDefaults
+  -- ^ Show results according to the default settings provided in the
+  -- Result itself
+
+  | ShowAll
+  -- ^ Show all Result
+  deriving (Eq, Show)
+
+-- | Use this verbosity for subjects that are True
+type TrueVerbosity = Verbosity
+
+-- | Use this verbosity for subjects that are False
+type FalseVerbosity = Verbosity
+
+-- | Determines whether to show any of the results from a single test.
+data ShowTest
+  = HideTest
+  -- ^ Do not show any results from this test
+
+  | ShowFirstLine TrueVerbosity FalseVerbosity
+  -- ^ Show the first line, which indicates whether the test passed or
+  -- failed and gives the label for the test. Whether to show
+  -- individual subjects is determined by the TrueVerbosity and
+  -- FalseVerbosity.
+
+  deriving (Eq, Show)
+
+-- | Determines which ShowTest to use for a particular test.
+data TestVerbosity = TestVerbosity
+  { onPass :: ShowTest
+    -- ^ Use this ShowTest when the test passes
+  , onFail :: ShowTest
+    -- ^ Use this ShowTest when the test fails
+  } deriving (Eq, Show)
+
+type Pass = Bool
+
+-- | The name of a test or of a group.
+type Name = Text
+
+-- | A single test.
+data Test a = Test
+  { testName :: Name
+  , testPass :: [Pt.Result] -> Pass
+  -- ^ Applied to the results of all applications of testFunc;
+  -- determines whether the test passes or fails.
+
+  , testFunc :: a -> Pt.Result
+  -- ^ This function is applied to each subject.
+
+  , testVerbosity :: TestVerbosity
+  -- ^ Default verbosity for the test.
+  }
+
+data TestResult a = TestResult
+  { resultName :: Name
+  , resultPass :: Pass
+  , resultSubjects :: [(a, Pt.Result)]
+  , resultDefaultVerbosity :: TestVerbosity
+  }
+
+-- # Showing tests
+
+-- | Creates a plain Chunk from a Text.
+plain :: X.Text -> R.Chunk
+plain = R.Chunk mempty
+
+showTestTitle :: Name -> Pass -> [R.Chunk]
+showTestTitle n p = [open, passFail, close, blank, txt, nl]
+  where
+    nl = plain "\n"
+    passFail =
+      if p
+      then "PASS" <> R.f_green
+      else "FAIL" <> R.f_red
+    open = plain "["
+    close = plain "]"
+    blank = plain (X.singleton ' ')
+    txt = plain n
+
+-- | Evaluates a test for a given list of subjects.
+evalTest :: Test a -> [a] -> TestResult a
+evalTest (Test n fPass fSubj vy) ls = TestResult n p ss vy
+  where
+    p = fPass results
+    results = map fSubj ls
+    ss = zip ls results
+
+-- | Shows a result with indenting.
+showResult
+  :: Pt.IndentAmt
+  -- ^ Indent each level by this many spaces
+
+  -> (a -> Text)
+  -- ^ Shows each subject. The function should return a single-line
+  -- text without a trailing newline.
+
+  -> Maybe TestVerbosity
+  -- ^ If Just, use this TestVerbosity when showing the test. If
+  -- Nothing, use the default verbosity.
+
+  -> TestResult a
+  -- ^ The result to show
+
+  -> [R.Chunk]
+showResult amt swr mayVb (TestResult n p ss dfltVb) =
+  let vb = fromMaybe dfltVb mayVb
+      tv = if p then onPass vb else onFail vb
+      firstLine = showTestTitle n p
+  in case tv of
+      HideTest -> []
+      ShowFirstLine trueV falseV ->
+        firstLine
+        ++ concatMap (showSubject p amt swr (trueV, falseV)) ss
+
+showSubject
+  :: Pass
+  -> Pt.IndentAmt
+  -> (a -> Text)
+  -> (TrueVerbosity, FalseVerbosity)
+  -> (a, Pt.Result)
+  -> [R.Chunk]
+showSubject p amt swr (tv, fv) (a, r) =
+  let txt = swr a
+      vb = if p then tv else fv
+  in case vb of
+      HideAll -> []
+      ShowDefaults -> Pt.showTopResult txt amt 1 False r
+      ShowAll -> Pt.showTopResult txt amt 1 True r
+
+-- # Pre-built tests
+
+-- | The test passes if each subject returns True.
+eachSubjectMustBeTrue :: Pt.Pdct a -> Name -> Test a
+eachSubjectMustBeTrue pd nm = Test nm pass f vy
+  where
+    vy = TestVerbosity
+      { onPass = ShowFirstLine HideAll HideAll
+      , onFail = ShowFirstLine HideAll ShowDefaults }
+    pass = all Pt.rBool
+    f = flip Pt.evaluate pd
+
+
+-- | The test passes if at least a given number of subjects are True.
+nSubjectsMustBeTrue
+  :: Pt.Pdct a
+  -> Name
+  -> Int
+  -- ^ The number of subjects that must be True. This should be a
+  -- positive number.
+  -> Test a
+nSubjectsMustBeTrue pd nm i = Test nm pass f vy
+  where
+    pass = atLeast i . filter Pt.rBool
+    f = flip Pt.evaluate pd
+    vy = TestVerbosity
+      { onPass = ShowFirstLine HideAll HideAll
+      , onFail = ShowFirstLine HideAll HideAll }
+
+
+-- # Basement
+
+-- | Returns True if the list has at least this many elements. Lazier
+-- than taking the length of the list.
+atLeast :: Int -> [a] -> Bool
+atLeast i as
+  | i < 0 = error "atLeast: negative length parameter"
+  | otherwise = go 0 as
+  where
+    go _ [] = i == 0
+    go soFar (_:xs) =
+      let nFound = soFar + 1
+      in if nFound == i then True else go nFound xs
+
diff --git a/prednote-test.hs b/prednote-test.hs
new file mode 100644
--- /dev/null
+++ b/prednote-test.hs
@@ -0,0 +1,90 @@
+{-# LANGUAGE TemplateHaskell #-}
+module Main where
+
+import Control.Applicative
+import qualified Test.QuickCheck as Q
+import Test.QuickCheck.All (quickCheckAll)
+import Test.QuickCheck.Function
+import Test.QuickCheck (Arbitrary, Gen, arbitrary)
+import qualified Data.Prednote.Pdct as P
+import Data.Prednote.Pdct ((&&&), (|||))
+import qualified Data.Text as X
+import qualified System.Exit as Exit
+
+instance Arbitrary X.Text where
+  arbitrary = fmap X.pack (Q.listOf (Q.choose ('!', '~')))
+
+instance Q.CoArbitrary a => Arbitrary (P.Pdct a) where
+  arbitrary = P.Pdct <$> arbitrary <*> arbitrary <*> arbitrary
+
+instance Q.CoArbitrary a => Arbitrary (P.Node a) where
+  arbitrary = Q.sized tree
+    where
+      tree 0 = fmap P.Operand arbitrary
+      tree n = Q.oneof
+        [ fmap P.And (Q.listOf subtree)
+        , fmap P.Or (Q.listOf subtree)
+        , fmap P.Not subtree ]
+        where
+          subtree = P.Pdct <$> arbitrary <*> arbitrary
+                    <*> tree (n `div` 2)
+
+-- | And is commutative
+prop_andCommutative :: a -> P.Pdct a -> P.Pdct a -> Bool
+prop_andCommutative a p1 p2 = P.rBool r1 == P.rBool r2
+  where
+    r1 = P.evaluate a (p1 &&& p2)
+    r2 = P.evaluate a (p2 &&& p1)
+
+-- | And is associative
+prop_andAssociative :: a -> P.Pdct a -> P.Pdct a -> P.Pdct a -> Bool
+prop_andAssociative a p1 p2 p3 = P.rBool r1 == P.rBool r2
+  where
+    r1 = P.evaluate a (p1 &&& (p2 &&& p3))
+    r2 = P.evaluate a ((p1 &&& p2) &&& p3)
+    
+-- | Or is commutative
+prop_orCommutative :: a -> P.Pdct a -> P.Pdct a -> Bool
+prop_orCommutative a p1 p2 = P.rBool r1 == P.rBool r2
+  where
+    r1 = P.evaluate a (p1 ||| p2)
+    r2 = P.evaluate a (p2 ||| p1)
+
+-- | Or is associative
+prop_orAssociative :: a -> P.Pdct a -> P.Pdct a -> P.Pdct a -> Bool
+prop_orAssociative a p1 p2 p3 = P.rBool r1 == P.rBool r2
+  where
+    r1 = P.evaluate a (p1 ||| (p2 ||| p3))
+    r2 = P.evaluate a ((p1 ||| p2) ||| p3)
+
+-- | Anything or'd with True is True
+prop_orWithTrue :: a -> P.Pdct a -> Bool
+prop_orWithTrue a p1 = P.rBool r1
+  where
+    r1 = P.evaluate a (p1 ||| P.always)
+
+-- | Anything and'ed with False is False
+prop_andWithFalse :: a -> P.Pdct a -> Bool
+prop_andWithFalse a p1 = not $ P.rBool r1
+  where
+    r1 = P.evaluate a (p1 &&& P.never)
+
+-- | And Distributitivy
+prop_andDistributivity :: a -> P.Pdct a -> P.Pdct a -> P.Pdct a -> Bool
+prop_andDistributivity x a b c = P.rBool r1 == P.rBool r2
+  where
+    r1 = P.evaluate x $ a &&& (b ||| c)
+    r2 = P.evaluate x $ (a &&& b) ||| (a &&& c)
+
+prop_orDistributivity :: a -> P.Pdct a -> P.Pdct a -> P.Pdct a -> Bool
+prop_orDistributivity x a b c = P.rBool r1 == P.rBool r2
+  where
+    r1 = P.evaluate x $ a ||| (b &&& c)
+    r2 = P.evaluate x $ (a ||| b) &&& (a ||| c)
+
+runTests = $quickCheckAll
+
+main :: IO ()
+main = do
+  b <- runTests
+  if b then Exit.exitSuccess else Exit.exitFailure
diff --git a/prednote.cabal b/prednote.cabal
--- a/prednote.cabal
+++ b/prednote.cabal
@@ -1,5 +1,5 @@
 name:                prednote
-version:             0.10.0.0
+version:             0.12.0.0
 synopsis:            Build and evaluate trees of predicates
 description:
   Build and evaluate trees of predicates. For example, you might build
@@ -32,7 +32,7 @@
     , Data.Prednote.Expressions
     , Data.Prednote.Expressions.Infix
     , Data.Prednote.Expressions.RPN
-    , Data.Prednote.TestTree
+    , Data.Prednote.Test
 
   build-depends:
       base >= 4.6 && < 5
@@ -42,3 +42,22 @@
     , text == 0.11.*
 
   ghc-options: -Wall
+  hs-source-dirs: lib
+
+executable prednote-test
+  main-is: prednote-test.hs
+  hs-source-dirs: . lib
+  if ! flag(test)
+    buildable: False
+
+  build-depends:
+      base >= 4.6 && < 5
+    , QuickCheck ==2.6.*
+    , text ==0.11.*
+    , explicit-exception ==0.1.*
+    , rainbow ==0.4.*
+    , split ==0.2.*
+
+flag test
+    Description: enables QuickCheck tests
+    default: False
