diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
 # dickinson
 
+## 0.1.0.1
+
+  * Fix source distribution so nix builds it automatically
+  * Fix a bug in the evaluator that caused valid expressions to be rejected
+    in the REPL
+  * Show an error in the REPL when trying to `:view` a name not in scope.
+  * Throw error when tuple pattern doesn't match type of expression
+  * Fix `$` within strings so it doesn't need to be escaped
+
 ## 0.1.0.0
 
 Initial release
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@
 
 ### Examples
 
-An riff on the Unix fortune program is available
+A riff on the Unix fortune program is available
 [here](https://github.com/vmchale/dickinson/blob/master/examples/fortune.dck).
 Try
 
diff --git a/bench/Bench.hs b/bench/Bench.hs
--- a/bench/Bench.hs
+++ b/bench/Bench.hs
@@ -55,9 +55,11 @@
                     ]
                 , bgroup "pipeline"
                     [ bench "examples/shakespeare.dck" $ nfIO (pipeline [] "examples/shakespeare.dck")
+                    , bench "examples/fortune.dck" $ nfIO (pipeline [] "examples/fortune.dck")
                     ]
                 , bgroup "tcFile"
                     [ bench "examples/hotTake.dck" $ nfIO (tcFile [] "examples/hotTake.dck")
+                    , bench "examples/fortune.dck" $ nfIO (tcFile [] "examples/fortune.dck")
                     ]
                 ]
 
diff --git a/examples/fionaBot.dck b/examples/fionaBot.dck
new file mode 100644
--- /dev/null
+++ b/examples/fionaBot.dck
@@ -0,0 +1,12 @@
+%-
+
+(:def fiona
+  (:oneof
+    (| "You forgot the difference between equanimity and passivity.")
+    (| "You're more likely to get cut with a dull tool than a sharp one.")
+    (| "Hunger hurts but starving works.")
+    (| "I ran out of white doves' feathers\nTo soak up the hot piss that comes from your mouth")
+    (| "Just tolerate my little fist\nTugging on your forest chest")))
+
+(:def main
+  fiona)
diff --git a/examples/fortune.dck b/examples/fortune.dck
--- a/examples/fortune.dck
+++ b/examples/fortune.dck
@@ -19,8 +19,12 @@
     (| "Wash your hands with soap and water for at least 20 seconds.")
     (| "Cleanliness is next to godliness.")
     (| "Do not complain.")
+    (| "Guilt redeems, not love.")
     (| "There is no such thing as metaphors.")
     (| "Excess is a sin.")
+    (| "Spurn your family.")
+    (| "Salvation is finality.")
+    (| "Love animals.")
     (| "Sex wastes the male body.")
     (| "Cultivate weakness.")
     (| "Beauty is a moral imperative.")
diff --git a/examples/hotTake.dck b/examples/hotTake.dck
deleted file mode 100644
--- a/examples/hotTake.dck
+++ /dev/null
@@ -1,67 +0,0 @@
-%-
-
-(:def unpopularize
-  (:lambda opinion text
-    (:branch
-      (| 0.9 opinion)
-      (| 0.1 "Unpopular opinion: ${opinion}"))))
-
-(:def slur
-  (:let
-    [badWord
-      (:oneof
-        (| "crackhead")
-        (| "'Privileged'"))]
-    "${badWord} is a slur"))
-
-; queering/ _ is queer
-
-(:def normalize
-  (:let
-    [norm
-      (:oneof
-        (| "bidets")
-        (| "breastfeeding your boyfriend")
-        (| "not wearing deodorant")
-        (| "being a virgin")
-        (| "DWIs"))]
-    "Normalize ${norm}"))
-
-(:def abolish
-  (:let
-    [target
-      (:oneof
-        (| "credit scores")
-        (| "the stock market")
-        (| "parking tickets"))]
-    "Abolish ${target}"))
-
-(:def problematic
-  (:let
-    [bad
-      (:oneof
-        (| "Grades are")
-        (| "The GRE is")
-        (| "Masks are"))]
-      "${bad} problematic"))
-
-(:def toxic
-  (:let
-    [badThing
-      (:oneof
-        (| "brushing your teeth is")
-        (| "masculinity is")
-        (| "museums are"))]
-    "${badThing} toxic."))
-
-; cancel _
-
-(:def main
-  $ unpopularize
-    (:flatten
-      (:oneof
-        (| normalize)
-        (| toxic)
-        (| problematic)
-        (| slur)
-        (| abolish))))
diff --git a/language-dickinson.cabal b/language-dickinson.cabal
--- a/language-dickinson.cabal
+++ b/language-dickinson.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.0
 name:               language-dickinson
-version:            0.1.0.0
+version:            0.1.0.1
 license:            BSD3
 license-file:       LICENSE
 copyright:          Copyright: (c) 2020 Vanessa McHale
@@ -23,6 +23,9 @@
     test/data/*.pretty
     test/data/*.dck
     test/data/*.rename
+    test/eval/*.dck
+    test/error/*.dck
+    test/demo/*.dck
     examples/*.dck
 
 extra-doc-files:
@@ -98,8 +101,8 @@
     default-language: Haskell2010
     other-extensions:
         DeriveAnyClass DeriveFunctor DeriveGeneric FlexibleContexts
-        GeneralizedNewtypeDeriving OverloadedStrings StandaloneDeriving
-        TupleSections
+        FlexibleInstances GeneralizedNewtypeDeriving OverloadedStrings
+        StandaloneDeriving TupleSections
 
     ghc-options:      -Wall -O2
     build-depends:
@@ -146,8 +149,10 @@
     other-modules:
         REPL
         REPL.Save
+        REPL.Completions
 
     default-language: Haskell2010
+    other-extensions: FlexibleContexts OverloadedStrings TupleSections
     ghc-options:      -Wall -rtsopts -with-rtsopts=-A4M
     build-depends:
         base -any,
@@ -192,6 +197,7 @@
         TypeCheck
 
     default-language: Haskell2010
+    other-extensions: OverloadedStrings
     ghc-options:      -threaded -rtsopts "-with-rtsopts=-N -K1K" -Wall
     build-depends:
         base -any,
diff --git a/man/emd.1 b/man/emd.1
--- a/man/emd.1
+++ b/man/emd.1
@@ -41,6 +41,8 @@
 \f[B]:t\f[R] \f[B]:type\f[R] - Display the type of an expression
 .PP
 \f[B]:v\f[R] \f[B]:view\f[R] - Show the definition of a name
+.PP
+\f[B]:h\f[R] \f[B]:help\f[R] - Display help
 .SH OPTIONS
 .TP
 \f[B]-h\f[R] \f[B]--help\f[R]
diff --git a/public/Language/Dickinson.hs b/public/Language/Dickinson.hs
--- a/public/Language/Dickinson.hs
+++ b/public/Language/Dickinson.hs
@@ -1,4 +1,4 @@
--- | This modules contains some bits and pieces to work with Dickinson code.
+-- | This module contains some bits and pieces to work with Dickinson code.
 module Language.Dickinson ( -- * Parser
                             parse
                           , ParseError (..)
diff --git a/run/Main.hs b/run/Main.hs
--- a/run/Main.hs
+++ b/run/Main.hs
@@ -19,8 +19,6 @@
 main :: IO ()
 main = run =<< execParser wrapper
 
--- TODO: cache/"compile"?
-
 act :: Parser Act
 act = hsubparser
     (command "run" (info runP (progDesc "Execute a file"))
diff --git a/run/REPL.hs b/run/REPL.hs
--- a/run/REPL.hs
+++ b/run/REPL.hs
@@ -1,5 +1,6 @@
 {-# LANGUAGE FlexibleContexts  #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TupleSections     #-}
 
 module REPL ( dickinsonRepl
             ) where
@@ -33,8 +34,10 @@
 import           Language.Dickinson.Unique
 import           Lens.Micro                            (_1)
 import           Lens.Micro.Mtl                        (use, (.=))
+import           REPL.Completions
 import           REPL.Save
-import           System.Console.Haskeline              (InputT, defaultSettings, getInputLine, historyFile, runInputT)
+import           System.Console.Haskeline              (InputT, completeFilename, defaultSettings, fallbackCompletion,
+                                                        getInputLine, historyFile, runInputT, setComplete)
 import           System.Directory                      (getHomeDirectory)
 import           System.FilePath                       ((</>))
 import           System.Random                         (newStdGen, randoms)
@@ -49,7 +52,8 @@
     g <- newStdGen
     emdDir <- (</> ".emd_history") <$> getHomeDirectory
     let initSt = EvalSt (randoms g) mempty initRenames mempty alexInitUserState emptyTyEnv
-    let emdSettings = defaultSettings { historyFile = Just emdDir }
+    let myCompleter = emdCompletions `fallbackCompletion` completeFilename
+    let emdSettings = setComplete myCompleter $ defaultSettings { historyFile = Just emdDir }
     flip evalStateT initSt $ runInputT emdSettings x
 
 loop :: Repl AlexPosn ()
@@ -65,7 +69,6 @@
         Just (":r":fp:_)    -> loadReplSt fp *> loop
         Just (":type":e:_)  -> typeExpr e *> loop
         Just (":t":e:_)     -> typeExpr e *> loop
-        Just (":v":n:_)     -> bindDisplay (T.pack n) *> loop
         Just (":view":n:_)  -> bindDisplay (T.pack n) *> loop
         Just [":q"]         -> pure ()
         Just [":quit"]      -> pure ()
@@ -85,8 +88,8 @@
     , helpOption ":load, :l" "<file>" "Load file contents"
     , helpOption ":r" "<file>" "Restore REPL state from a file"
     , helpOption ":type, :t" "<expression>" "Display the type of an expression"
-    , helpOption ":view, :v" "<name>" "Show the value of a name"
-    , helpOption ":quite, :q" "" "Quit REPL"
+    , helpOption ":view" "<name>" "Show the value of a name"
+    , helpOption ":quit, :q" "" "Quit REPL"
     , helpOption ":list" "" "List all names that are in scope"
     ]
 
@@ -114,7 +117,7 @@
 listNames = liftIO . traverse_ TIO.putStrLn =<< names
 
 names :: Repl AlexPosn [T.Text]
-names = lift $ gets (M.keys . topLevel)
+names = lift $ namesState
 
 bindDisplay :: T.Text -> Repl AlexPosn ()
 bindDisplay t = do
@@ -126,7 +129,7 @@
             case IM.lookup i exprs of
                 Just e  -> liftIO $ putDoc (pretty e <> hardline)
                 Nothing -> error "Internal error."
-        Nothing -> pure () -- TODO: error
+        Nothing -> liftIO $ putDoc $ (<> hardline) $ pretty (NoText t :: DickinsonError AlexPosn)
 
 setSt :: AlexUserState -> Repl AlexPosn ()
 setSt newSt = lift $ do
@@ -183,7 +186,6 @@
 putErr (Right x) f = f x
 putErr (Left y) _  = liftIO $ putDoc (pretty y <> hardline)
 
--- TODO: check
 loadFile :: FilePath -> Repl AlexPosn ()
 loadFile fp = do
     pathMod <- liftIO defaultLibPath
diff --git a/run/REPL/Completions.hs b/run/REPL/Completions.hs
new file mode 100644
--- /dev/null
+++ b/run/REPL/Completions.hs
@@ -0,0 +1,57 @@
+{-# LANGUAGE TupleSections #-}
+
+module REPL.Completions ( emdCompletions
+                        , namesState
+                        ) where
+
+import           Control.Monad.State      (StateT, gets)
+import qualified Data.Map                 as M
+import qualified Data.Text                as T
+import           Language.Dickinson.Eval
+import           System.Console.Haskeline (Completion, CompletionFunc, simpleCompletion)
+
+namesState :: StateT (EvalSt a) IO [T.Text]
+namesState = gets (M.keys . topLevel)
+
+namesStr :: StateT (EvalSt a) IO [String]
+namesStr = fmap T.unpack <$> gets (M.keys . topLevel)
+
+cyclicSimple :: [String] -> [Completion]
+cyclicSimple [] = []
+cyclicSimple xs = cycle $ fmap simpleCompletion xs
+
+emdCompletions :: CompletionFunc (StateT (EvalSt a) IO)
+emdCompletions (":","")       = pure (":", cyclicSimple [ "help", "h", "save", "load", "l", "r", "type", "t", "view", "quit", "q", "list" ])
+emdCompletions ("l:", "")     = pure ("l:", cyclicSimple [ "oad", "", "ist" ])
+emdCompletions ("ol:", "")    = pure ("ol:", [simpleCompletion "ad"])
+emdCompletions ("aol:", "")   = pure ("aol:", [simpleCompletion "d"])
+emdCompletions ("daol:", "")  = pure ("daol:", [simpleCompletion ""])
+emdCompletions ("il:", "")    = pure ("il:", [simpleCompletion "st"])
+emdCompletions ("sil:", "")   = pure ("sil:", [simpleCompletion "t"])
+emdCompletions ("tsil:", "")  = pure ("tsil:", [simpleCompletion ""])
+emdCompletions ("h:", "")     = pure ("h:", cyclicSimple ["elp", ""])
+emdCompletions ("eh:", "")    = pure ("eh:", [simpleCompletion "lp"])
+emdCompletions ("leh:", "")   = pure ("leh:", [simpleCompletion "p"])
+emdCompletions ("pleh:", "")  = pure ("pleh:", [simpleCompletion ""])
+emdCompletions ("s:", "")     = pure ("s:", [simpleCompletion "ave"])
+emdCompletions ("as:", "")    = pure ("as:", [simpleCompletion "ve"])
+emdCompletions ("vas:", "")   = pure ("vas:", [simpleCompletion "e"])
+emdCompletions ("evas:", "")  = pure ("evas:", [simpleCompletion ""])
+emdCompletions ("r:", "")     = pure ("r:", [simpleCompletion ""])
+emdCompletions ("t:", "")     = pure ("t:", cyclicSimple ["ype", ""])
+emdCompletions ("yt:", "")    = pure ("yt:", [simpleCompletion "pe"])
+emdCompletions ("pyt:", "")   = pure ("pyt:", [simpleCompletion "e"])
+emdCompletions ("epyt:", "")  = pure ("epyt:", [simpleCompletion ""])
+emdCompletions ("v:", "")     = pure ("v:", [simpleCompletion "iew"])
+emdCompletions ("iv:", "")    = pure ("iv:", [simpleCompletion "ew"])
+emdCompletions ("eiv:", "")   = pure ("eiv:", [simpleCompletion "w"])
+emdCompletions ("weiv:", "")  = pure ("weiv:", [simpleCompletion ""])
+emdCompletions ("q:", "")     = pure ("q:", cyclicSimple ["uit", ""])
+emdCompletions ("uq:", "")    = pure ("uq:", [simpleCompletion "it"])
+emdCompletions ("iuq:", "")   = pure ("iuq:", [simpleCompletion "t"])
+emdCompletions ("tiuq:", "")  = pure ("tiuq:", [simpleCompletion ""])
+emdCompletions (" weiv:", "") = do { ns <- namesStr ; pure (" weiv:", cyclicSimple ns) } -- TODO: when it matches part of the identifiers!
+emdCompletions (" epyt:", "") = do { ns <- namesStr ; pure (" epyt:", cyclicSimple ns) }
+emdCompletions (" t:", "")    = do { ns <- namesStr ; pure (" t:", cyclicSimple ns) }
+emdCompletions ("", "")       = ("",) . cyclicSimple <$> namesStr
+emdCompletions _              = pure (undefined, [])
diff --git a/src/Language/Dickinson/Check.hs b/src/Language/Dickinson/Check.hs
--- a/src/Language/Dickinson/Check.hs
+++ b/src/Language/Dickinson/Check.hs
@@ -16,7 +16,8 @@
           announce _       = Nothing
 
 -- runs after the renamer
--- | Checks that there are not name clashes at the top level.
+-- | Checks that there are not name clashes at the top level or within let
+-- bindings.
 checkMultiple :: [Declaration a] -> Maybe (DickinsonWarning a)
 checkMultiple ds =
         checkNames (mapMaybe defNameM ds)
@@ -35,19 +36,21 @@
 collectConstructors (TyDecl _ _ cs) = toList cs
 
 checkMultipleExpr :: Expression a -> Maybe (DickinsonWarning a)
-checkMultipleExpr Var{}            = Nothing
-checkMultipleExpr Literal{}        = Nothing
-checkMultipleExpr StrChunk{}       = Nothing
-checkMultipleExpr (Interp _ es)    = foldMapAlternative checkMultipleExpr es
-checkMultipleExpr (Apply _ e e')   = checkMultipleExpr e <|> checkMultipleExpr e'
-checkMultipleExpr (Match _ e _ e') = checkMultipleExpr e <|> checkMultipleExpr e'
-checkMultipleExpr (Choice _ brs)   = foldMapAlternative (checkMultipleExpr . snd) brs
-checkMultipleExpr (Concat _ es)    = foldMapAlternative checkMultipleExpr es
-checkMultipleExpr (Tuple _ es)     = foldMapAlternative checkMultipleExpr es
-checkMultipleExpr (Lambda _ _ _ e) = checkMultipleExpr e
-checkMultipleExpr (Flatten _ e)    = checkMultipleExpr e
-checkMultipleExpr (Let _ bs e)     =
+checkMultipleExpr Var{}              = Nothing
+checkMultipleExpr Literal{}          = Nothing
+checkMultipleExpr StrChunk{}         = Nothing
+checkMultipleExpr (Interp _ es)      = foldMapAlternative checkMultipleExpr es
+checkMultipleExpr (MultiInterp _ es) = foldMapAlternative checkMultipleExpr es
+checkMultipleExpr (Apply _ e e')     = checkMultipleExpr e <|> checkMultipleExpr e'
+checkMultipleExpr (Match _ e _ e')   = checkMultipleExpr e <|> checkMultipleExpr e'
+checkMultipleExpr (Choice _ brs)     = foldMapAlternative (checkMultipleExpr . snd) brs
+checkMultipleExpr (Concat _ es)      = foldMapAlternative checkMultipleExpr es
+checkMultipleExpr (Tuple _ es)       = foldMapAlternative checkMultipleExpr es
+checkMultipleExpr (Lambda _ _ _ e)   = checkMultipleExpr e
+checkMultipleExpr (Flatten _ e)      = checkMultipleExpr e
+checkMultipleExpr (Let _ bs e)       =
         checkNames (toList $ fmap fst bs)
     <|> foldMapAlternative checkMultipleExpr (snd <$> bs)
     <|> checkMultipleExpr e
 checkMultipleExpr (Annot _ e _)    = checkMultipleExpr e
+checkMultipleExpr Constructor{}    = Nothing
diff --git a/src/Language/Dickinson/Check/Internal.hs b/src/Language/Dickinson/Check/Internal.hs
--- a/src/Language/Dickinson/Check/Internal.hs
+++ b/src/Language/Dickinson/Check/Internal.hs
@@ -11,6 +11,7 @@
 import           Lens.Micro.Mtl            (use)
 
 -- TODO: sanity check for the lexer
+-- | Sanity check for the renamer.
 sanityCheck :: (HasRenames s, MonadState s m) => [Declaration a] -> m ()
 sanityCheck d = do
     storedMax <- use (rename.maxLens)
@@ -35,6 +36,7 @@
 maxUniqueExpression StrChunk{}                            = 0
 maxUniqueExpression (Var _ (Name _ (Unique i) _))         = i
 maxUniqueExpression (Choice _ pes)                        = maximum (maxUniqueExpression . snd <$> pes)
+maxUniqueExpression (MultiInterp _ es)                    = maximum (fmap maxUniqueExpression es)
 maxUniqueExpression (Interp _ es)                         = maximum (fmap maxUniqueExpression es)
 maxUniqueExpression (Concat _ es)                         = maximum (fmap maxUniqueExpression es)
 maxUniqueExpression (Apply _ e e')                        = max (maxUniqueExpression e) (maxUniqueExpression e')
diff --git a/src/Language/Dickinson/DuplicateCheck.hs b/src/Language/Dickinson/DuplicateCheck.hs
--- a/src/Language/Dickinson/DuplicateCheck.hs
+++ b/src/Language/Dickinson/DuplicateCheck.hs
@@ -16,7 +16,7 @@
     where announce (_:(l, y):_) = Just $ DuplicateStr l y
           announce _            = Nothing
 
--- | Check that there are no duplicate names as the top-level
+-- | Check that there are not duplicates in branches.
 checkDuplicates :: [Declaration a] -> Maybe (DickinsonWarning a)
 checkDuplicates = foldMapAlternative checkDeclDuplicates
 
@@ -31,20 +31,19 @@
 collectText :: [(b, Expression a)] -> [(a, T.Text)]
 collectText = mapMaybe (extrText . snd)
 
--- TODO: check duplicate tydecl names!!
-
 checkExprDuplicates :: Expression a -> Maybe (DickinsonWarning a)
-checkExprDuplicates Var{}            = Nothing
-checkExprDuplicates Literal{}        = Nothing
-checkExprDuplicates StrChunk{}       = Nothing
-checkExprDuplicates (Interp _ es)    = foldMapAlternative checkExprDuplicates es
-checkExprDuplicates (Concat _ es)    = foldMapAlternative checkExprDuplicates es
-checkExprDuplicates (Tuple _ es)     = foldMapAlternative checkExprDuplicates es
-checkExprDuplicates (Apply _ e e')   = checkExprDuplicates e <|> checkExprDuplicates e'
-checkExprDuplicates (Choice _ brs)   = checkNames (collectText $ toList brs)
-checkExprDuplicates (Let _ brs es)   = foldMapAlternative checkExprDuplicates (snd <$> brs) <|> checkExprDuplicates es
-checkExprDuplicates (Lambda _ _ _ e) = checkExprDuplicates e
-checkExprDuplicates (Match _ e _ e') = checkExprDuplicates e <|> checkExprDuplicates e'
-checkExprDuplicates (Flatten _ e)    = checkExprDuplicates e
-checkExprDuplicates (Annot _ e _)    = checkExprDuplicates e
-checkExprDuplicates Constructor{}    = Nothing
+checkExprDuplicates Var{}              = Nothing
+checkExprDuplicates Literal{}          = Nothing
+checkExprDuplicates StrChunk{}         = Nothing
+checkExprDuplicates (Interp _ es)      = foldMapAlternative checkExprDuplicates es
+checkExprDuplicates (MultiInterp _ es) = foldMapAlternative checkExprDuplicates es
+checkExprDuplicates (Concat _ es)      = foldMapAlternative checkExprDuplicates es
+checkExprDuplicates (Tuple _ es)       = foldMapAlternative checkExprDuplicates es
+checkExprDuplicates (Apply _ e e')     = checkExprDuplicates e <|> checkExprDuplicates e'
+checkExprDuplicates (Choice _ brs)     = checkNames (collectText $ toList brs)
+checkExprDuplicates (Let _ brs es)     = foldMapAlternative checkExprDuplicates (snd <$> brs) <|> checkExprDuplicates es
+checkExprDuplicates (Lambda _ _ _ e)   = checkExprDuplicates e
+checkExprDuplicates (Match _ e _ e')   = checkExprDuplicates e <|> checkExprDuplicates e'
+checkExprDuplicates (Flatten _ e)      = checkExprDuplicates e
+checkExprDuplicates (Annot _ e _)      = checkExprDuplicates e
+checkExprDuplicates Constructor{}      = Nothing
diff --git a/src/Language/Dickinson/Error.hs b/src/Language/Dickinson/Error.hs
--- a/src/Language/Dickinson/Error.hs
+++ b/src/Language/Dickinson/Error.hs
@@ -25,10 +25,10 @@
                       | ModuleNotFound a (Name a)
                       | TypeMismatch (Expression a) (DickinsonTy a) (DickinsonTy a)
                       | ExpectedLambda (Expression a) (DickinsonTy a)
-                      | MultiBind a (Name a) (Pattern a) -- When a variable is bound more than once in a pattern...
+                      | MultiBind a (Name a) (Pattern a) -- When a variable is bound more than once in a pattern
                       | MalformedTuple a
-                      | InternalError
                       | UnfoundConstructor a (TyName a)
+                      | UnfoundType a (Name a)
                       deriving (Generic, NFData)
 
 data DickinsonWarning a = MultipleNames a (Name a) -- TODO: throw both?
@@ -51,8 +51,8 @@
     pretty (ExpectedLambda e ty)     = "Expected" <+> squotes (pretty e) <+> "to be of function type, found type" <+> pretty ty
     pretty (MultiBind l n p)         = pretty l <+> "Name" <+> pretty n <+> "is bound more than once in" <+> pretty p
     pretty (MalformedTuple l)        = pretty l <+> "Malformed tuple"
-    pretty InternalError             = "Internal error. Please report this as a bug: https://github.com/vmchale/dickinson/issues"
     pretty (UnfoundConstructor l tn) = pretty l <+> "Constructor" <+> pretty tn <+> "not found"
+    pretty (UnfoundType l ty)        = pretty l <+> "Type" <+> pretty ty <+> "not found"
 
 instance (Pretty a, Typeable a) => Exception (DickinsonError a)
 
diff --git a/src/Language/Dickinson/Eval.hs b/src/Language/Dickinson/Eval.hs
--- a/src/Language/Dickinson/Eval.hs
+++ b/src/Language/Dickinson/Eval.hs
@@ -17,9 +17,9 @@
 
 import           Control.Composition           (thread)
 import           Control.Monad                 ((<=<))
-import           Control.Monad.Except          (ExceptT, MonadError, runExceptT, throwError)
+import           Control.Monad.Except          (MonadError, throwError)
 import qualified Control.Monad.Ext             as Ext
-import           Control.Monad.State.Lazy      (MonadState, State, evalState, get, gets, modify, put)
+import           Control.Monad.State.Lazy      (MonadState, get, gets, modify, put)
 import           Data.Foldable                 (toList, traverse_)
 import qualified Data.IntMap                   as IM
 import           Data.List.NonEmpty            (NonEmpty, (<|))
@@ -46,7 +46,7 @@
     , renameCtx     :: Renames
     -- TODO: map to uniques or an expression?
     , topLevel      :: M.Map T.Text Unique
-    -- For imports & such.
+    -- Used in the REPL, for instance
     , lexerState    :: AlexUserState
     -- For error messages
     , tyEnv         :: (TyEnv a)
@@ -147,6 +147,7 @@
               -> m ()
 loadDickinson = traverse_ addDecl
 
+-- Used in the REPL
 balanceMax :: (HasRenames s, HasLexerState s) => MonadState s m => m ()
 balanceMax = do
     m0 <- use (rename.maxLens)
@@ -165,6 +166,8 @@
 extrText (StrChunk _ t) = pure t
 extrText e              = do { ty <- typeOf e ; throwError $ TypeMismatch e (TyText $ exprAnn e) ty }
 
+-- Work with a temporary state, handling the max sensibly so as to prevent name
+-- collisions
 withSt :: (HasRenames s, MonadState s m) => (s -> s) -> m b -> m b
 withSt modSt act = do
     preSt <- get
@@ -177,10 +180,10 @@
 bindPattern :: (MonadError (DickinsonError a) m, MonadState (EvalSt a) m) => Pattern a -> Expression a -> m (EvalSt a -> EvalSt a)
 bindPattern (PatternVar _ n) e               = pure $ nameMod n e
 bindPattern Wildcard{} _                     = pure id
-bindPattern (PatternTuple _ ps) (Tuple _ es) = thread <$> Ext.zipWithM bindPattern ps es
+bindPattern (PatternTuple _ ps) (Tuple _ es) = thread <$> Ext.zipWithM bindPattern ps es -- don't need to verify length because in theory typechecker already did
 bindPattern (PatternTuple l _) _             = throwError $ MalformedTuple l
 
--- To partially apply lambdas
+-- To partially apply lambdas (needed for curried functions)
 tryEvalExpressionM :: (MonadState (EvalSt a) m, MonadError (DickinsonError a) m) => Expression a -> m (Expression a)
 tryEvalExpressionM e@Literal{}    = pure e
 tryEvalExpressionM e@StrChunk{}   = pure e
@@ -233,7 +236,7 @@
     withSt modSt $
         evalExpressionM e'
 evalExpressionM (Flatten _ e) = do
-    e' <- resolveExpressionM e
+    e' <- resolveFlattenM e
     evalExpressionM ({-# SCC "mapChoice.setFrequency" #-} mapChoice setFrequency e')
 evalExpressionM (Annot _ e _) = evalExpressionM e
 
@@ -265,12 +268,45 @@
 resolveDeclarationM :: (MonadState (EvalSt a) m, MonadError (DickinsonError a) m) => Declaration a -> m (Declaration a)
 resolveDeclarationM (Define l n e) = Define l n <$> resolveExpressionM e
 
--- | Resolve let bindings and such; no not perform choices or concatenations.
+-- | To aid the @:flatten@ function: resolve an expression, leaving
+-- choices/branches intact.
+resolveFlattenM :: (MonadState (EvalSt a) m, MonadError (DickinsonError a) m) => Expression a -> m (Expression a)
+resolveFlattenM e@Literal{}     = pure e
+resolveFlattenM e@StrChunk{}    = pure e
+resolveFlattenM e@Constructor{} = pure e
+resolveFlattenM (Var _ n)       = lookupName n
+resolveFlattenM (Choice l pes) = do
+    let ps = fst <$> pes
+    es <- traverse resolveFlattenM (snd <$> pes)
+    pure $ Choice l (NE.zip ps es)
+resolveFlattenM (Interp l es) = Interp l <$> traverse resolveFlattenM es
+resolveFlattenM (Concat l es) = Concat l <$> traverse resolveFlattenM es
+resolveFlattenM (Tuple l es) = Tuple l <$> traverse resolveFlattenM es
+resolveFlattenM (Let _ bs e) = do
+    let stMod = thread $ fmap (uncurry nameMod) bs
+    withSt stMod $
+        resolveFlattenM e
+resolveFlattenM (Apply _ e e') = do
+    e'' <- resolveFlattenM e
+    case e'' of
+        Lambda _ n _ e''' ->
+            withSt (nameMod n e') $
+                resolveFlattenM e'''
+        _ -> error "Ill-typed expression"
+resolveFlattenM e@Lambda{} = pure e
+resolveFlattenM (Match _ e p e') =
+    (bindPattern p =<< resolveFlattenM e) *>
+    resolveFlattenM e'
+resolveFlattenM (Flatten l e) =
+    Flatten l <$> resolveFlattenM e
+resolveFlattenM (Annot _ e _) = resolveFlattenM e
+
+-- | Resolve let bindings and such; do not perform choices or concatenations.
 resolveExpressionM :: (MonadState (EvalSt a) m, MonadError (DickinsonError a) m) => Expression a -> m (Expression a)
 resolveExpressionM e@Literal{}     = pure e
 resolveExpressionM e@StrChunk{}    = pure e
 resolveExpressionM e@Constructor{} = pure e
-resolveExpressionM (Var _ n)       = resolveExpressionM =<< lookupName n
+resolveExpressionM v@(Var _ n)     = maybe (pure v) resolveExpressionM =<< tryLookupName n
 resolveExpressionM (Choice l pes) = do
     let ps = fst <$> pes
     es <- traverse resolveExpressionM (snd <$> pes)
@@ -282,17 +318,16 @@
     let stMod = thread $ fmap (uncurry nameMod) bs
     withSt stMod $
         resolveExpressionM e
-resolveExpressionM (Apply _ e e') = do
+resolveExpressionM (Apply l e e') = do
     e'' <- resolveExpressionM e
     case e'' of
         Lambda _ n _ e''' ->
             withSt (nameMod n e') $
-                resolveExpressionM e''' -- TODO: is this right?
-        _ -> error "Ill-typed expression"
-resolveExpressionM e@Lambda{} = pure e -- TODO: is this right?
-resolveExpressionM (Match _ e p e') =
-    (bindPattern p =<< resolveExpressionM e) *>
-    resolveExpressionM e'
+                resolveExpressionM e'''
+        _ -> Apply l e'' <$> resolveExpressionM e'
+resolveExpressionM (Lambda l n ty e) = Lambda l n ty <$> resolveExpressionM e
+resolveExpressionM (Match l e p e') =
+    Match l <$> resolveExpressionM e <*> pure p <*> resolveExpressionM e'
 resolveExpressionM (Flatten l e) =
     Flatten l <$> resolveExpressionM e
 resolveExpressionM (Annot _ e _) = resolveExpressionM e
diff --git a/src/Language/Dickinson/File.hs b/src/Language/Dickinson/File.hs
--- a/src/Language/Dickinson/File.hs
+++ b/src/Language/Dickinson/File.hs
@@ -8,6 +8,7 @@
                                , amalgamateRenameM
                                , fmtFile
                                , pipeline
+                               , resolveFile
                                ) where
 
 import           Control.Applicative                   ((<|>))
@@ -91,6 +92,9 @@
 
 evalFile :: [FilePath] -> FilePath -> IO T.Text
 evalFile is = fmap eitherThrow . evalIO . (evalDickinsonAsMain <=< amalgamateRenameM is)
+
+resolveFile :: [FilePath] -> FilePath -> IO [Declaration AlexPosn]
+resolveFile is = fmap eitherThrow . evalIO . (traverse resolveDeclarationM <=< amalgamateRenameM is)
 
 pipeline :: [FilePath] -> FilePath -> IO T.Text
 pipeline is fp = fmap eitherThrow $ evalIO $ do
diff --git a/src/Language/Dickinson/Lexer.x b/src/Language/Dickinson/Lexer.x
--- a/src/Language/Dickinson/Lexer.x
+++ b/src/Language/Dickinson/Lexer.x
@@ -52,9 +52,9 @@
 @escape_str = \\ [$str_special n]
 
 -- single-line string
-@string = \" ([^ $str_special] | @escape_str)* \"
+@string = \" ([^ $str_special] | "$" [^\{\"\$] | @escape_str)* (\" | \$\")
 
-$str_chunk = [^ \"\\\$]
+$str_chunk = [^\"\\\$]
 
 @str_interp_in = ($str_chunk | @escape_str)+
 
@@ -66,48 +66,63 @@
 @name = (@lower_name \.)* @lower_name
 @tyname = [A-Z] @follow_char*
 
+@multi_str_in = ([^\'] | $white)*
+
 tokens :-
 
-    <0> $white+                    ;
-    <0> ";".*                      ;
+    <0> {
 
-    <0> \(                         { mkSym LParen }
-    <0> \)                         { mkSym RParen }
-    <0> \>                         { mkSym RBracket }
-    <0> \|                         { mkSym VBar }
-    <0> \[                         { mkSym LSqBracket }
-    <0> \]                         { mkSym RSqBracket }
-    <0> \$                         { mkSym DollarSign }
-    <0> \,                         { mkSym Comma }
-    <0> \_                         { mkSym Underscore }
-    <0> "⟶"                        { mkSym Arrow }
-    <0> "->"                       { mkSym Arrow }
-    <0> \:                         { mkSym Colon }
-    <0> \%\-                       { mkSym DeclBreak }
-    <0> "="                        { mkSym Eq }
+        $white+                    ;
+        ";".*                      ;
 
-    -- keywords
-    <0> ":let"                     { mkKeyword KwLet }
-    <0> ":branch"                  { mkKeyword KwBranch }
-    <0> ":oneof"                   { mkKeyword KwOneof }
-    <0> ":def"                     { mkKeyword KwDef }
-    <0> ":include"                 { mkKeyword KwInclude }
-    <0> ":lambda"                  { mkKeyword KwLambda }
-    <0> "text"                     { mkKeyword KwText }
-    <0> ":match"                   { mkKeyword KwMatch }
-    <0> ":flatten"                 { mkKeyword KwFlatten }
-    <0> "tydecl"                   { mkKeyword KwTyDecl }
+        \(                         { mkSym LParen }
+        \)                         { mkSym RParen }
+        \>                         { mkSym RBracket }
+        \|                         { mkSym VBar }
+        \[                         { mkSym LSqBracket }
+        \]                         { mkSym RSqBracket }
+        \$                         { mkSym DollarSign }
+        \,                         { mkSym Comma }
+        \_                         { mkSym Underscore }
+        "⟶"                        { mkSym Arrow }
+        "->"                       { mkSym Arrow }
+        \:                         { mkSym Colon }
+        \%\-                       { mkSym DeclBreak }
+        "="                        { mkSym Eq }
 
-    <0> @name                      { tok (\p s -> TokIdent p <$> newIdentAlex p (mkText s)) }
-    <0> @tyname                    { tok (\p s -> TokTyCons p <$> newIdentAlex p (mkText s)) }
+        -- keywords
+        ":let"                     { mkKeyword KwLet }
+        ":branch"                  { mkKeyword KwBranch }
+        ":oneof"                   { mkKeyword KwOneof }
+        ":def"                     { mkKeyword KwDef }
+        ":include"                 { mkKeyword KwInclude }
+        ":lambda"                  { mkKeyword KwLambda }
+        "text"                     { mkKeyword KwText }
+        ":match"                   { mkKeyword KwMatch }
+        ":flatten"                 { mkKeyword KwFlatten }
+        "tydecl"                   { mkKeyword KwTyDecl }
 
-    -- strings
+        -- identifiers
+        @name                      { tok (\p s -> TokIdent p <$> newIdentAlex p (mkText s)) }
+        @tyname                    { tok (\p s -> TokTyCons p <$> newIdentAlex p (mkText s)) }
+
+    }
+
+    -- interpolated strings
     <0> \"                         { mkSym StrBegin `andBegin` string }
     <string> @str_interp_in        { tok (\p s -> alex $ TokStrChunk p (escReplace $ mkText s)) }
     <string> @interp               { mkSym BeginInterp `andBegin` 0 }
+    <string> "$"                   { tok (\p s -> alex $ TokStrChunk p (mkText s)) }
     <0> \}                         { mkSym EndInterp `andBegin` string }
     <string> \"                    { mkSym StrEnd `andBegin` 0 }
 
+    -- TODO: track "depth" in interpolations
+
+    <0> "'''"                      { mkSym MultiStrBegin `andBegin` multiStr }
+    <multiStr> @multi_str_in       { tok (\p s -> alex $ TokStrChunk p (mkText s)) }
+    <multiStr> \' [^\']            { tok (\p s -> alex $ TokStrChunk p (mkText s)) }
+    <multiStr> "'''"               { mkSym MultiStrEnd `andBegin` 0 }
+
     -- strings
     <0> @string                    { tok (\p s -> alex $ TokString p (escReplace . T.tail . T.init $ mkText s)) }
 
@@ -123,7 +138,7 @@
     . T.replace "\\$" "$"
 
 mkText :: BSL.ByteString -> T.Text
-mkText = decodeUtf8 . BSL.toStrict
+mkText = {-# SCC "mkText" #-} decodeUtf8 . BSL.toStrict
 
 alex :: a -> Alex a
 alex = pure
@@ -184,6 +199,8 @@
          | EndInterp
          | StrBegin
          | StrEnd
+         | MultiStrBegin
+         | MultiStrEnd
          | Arrow
          | DollarSign
          | Comma
@@ -204,6 +221,8 @@
     pretty EndInterp     = rbrace
     pretty StrBegin      = dquote
     pretty StrEnd        = dquote
+    pretty MultiStrBegin = "'''"
+    pretty MultiStrEnd   = "'''"
     pretty Arrow         = "⟶"
     pretty DollarSign    = "$"
     pretty Comma         = comma
@@ -249,9 +268,9 @@
              | TokIdent { loc :: a, ident :: Name a }
              | TokTyCons { loc :: a, tyIdent :: TyName a }
              | TokDouble { loc :: a, double :: Double }
+             | TokStrChunk { loc :: a, str :: T.Text }
              -- separate tok for full strings for sake of speed
              | TokString { loc :: a, str :: T.Text }
-             | TokStrChunk { loc :: a, str :: T.Text }
              | TokKeyword { loc :: a, kw :: Keyword }
              | TokSym { loc :: a, sym :: Sym }
              deriving (Eq, Generic, NFData)
diff --git a/src/Language/Dickinson/Lib/Get.hs b/src/Language/Dickinson/Lib/Get.hs
--- a/src/Language/Dickinson/Lib/Get.hs
+++ b/src/Language/Dickinson/Lib/Get.hs
@@ -31,7 +31,7 @@
     (st, x) <- fAct lSt
     (lexerStateLens .= st) $> x
 
--- Parse an import. Does NOT perform renaming!
+-- | Parse an import. Does not perform renaming!
 parseImport :: (MonadError (DickinsonError AlexPosn) m, MonadIO m)
             => [FilePath] -- ^ Include path
             -> Import AlexPosn
diff --git a/src/Language/Dickinson/Name.hs b/src/Language/Dickinson/Name.hs
--- a/src/Language/Dickinson/Name.hs
+++ b/src/Language/Dickinson/Name.hs
@@ -23,6 +23,7 @@
 
 type TyName a = Name a
 
+-- TODO: separate type for module name
 -- | A (possibly qualified) name.
 data Name a = Name { name   :: NonEmpty T.Text
                    , unique :: !Unique
diff --git a/src/Language/Dickinson/Parser.y b/src/Language/Dickinson/Parser.y
--- a/src/Language/Dickinson/Parser.y
+++ b/src/Language/Dickinson/Parser.y
@@ -52,6 +52,8 @@
     rsqbracket { TokSym $$ RSqBracket }
     rbracket { TokSym $$ RBracket }
     strBegin { TokSym $$ StrBegin }
+    multiStrBegin { TokSym $$ MultiStrBegin }
+    multiStrEnd { TokSym $$ MultiStrEnd }
     strEnd { TokSym $$ StrEnd }
     arrow { TokSym $$ Arrow }
     dollar { TokSym $$ DollarSign }
@@ -146,6 +148,7 @@
            | ident { Var (loc $1) (ident $1) }
            | stringLiteral { Literal (loc $1) (str $1) }
            | strBegin some(Interp) strEnd { Interp $1 (toList $ NE.reverse $2) }
+           | multiStrBegin some(Interp) multiStrEnd { MultiInterp $1 (processMultiChunks $ toList $ NE.reverse $2) }
            | rbracket many(Expression) { Concat $1 (reverse $2) }
            | dollar Expression Expression { Apply $1 $2 $3 }
            | lparen sepBy(Expression,comma) rparen { Tuple $1 (NE.reverse $2) }
@@ -167,6 +170,27 @@
                         | Declaration { Left $1 }
 
 {
+
+countSpaces :: T.Text -> Int
+countSpaces = T.length . T.takeWhile (== ' ')
+
+stripMulti :: T.Text -> T.Text
+stripMulti t =
+    let ls = T.lines t
+        in let sp = minimum (maxBound : (fmap countSpaces $ tail ls))
+            in T.unlines (fmap (T.drop sp) ls)
+
+mapStrChunk :: (T.Text -> T.Text) -> Expression a -> Expression a
+mapStrChunk f (StrChunk l t) = StrChunk l (f t)
+mapStrChunk _ e = e
+
+squishStrChunks :: [Expression a] -> [Expression a]
+squishStrChunks (StrChunk l t:StrChunk _ t':ts) = squishStrChunks (StrChunk l (t <> t') : ts)
+squishStrChunks (t:ts)                          = t : squishStrChunks ts
+squishStrChunks []                              = []
+
+processMultiChunks :: [Expression a] -> [Expression a]
+processMultiChunks = fmap (mapStrChunk stripMulti) . squishStrChunks
 
 weight :: NonEmpty (Expression a) -> NonEmpty (Double, Expression a)
 weight es = (recip, ) <$> es
diff --git a/src/Language/Dickinson/Rename.hs b/src/Language/Dickinson/Rename.hs
--- a/src/Language/Dickinson/Rename.hs
+++ b/src/Language/Dickinson/Rename.hs
@@ -81,28 +81,33 @@
     u' <- replaceUnique u
     pure $ Name n u' l
 
+-- exported so we can test it alone
 renameDickinson :: Int -> Dickinson a -> (Dickinson a, Int)
 renameDickinson m ds = runRenameM m $ renameDickinsonM ds
 
+-- | The renamer ensures global uniqueness and is used during evaluation to
+-- clone expressions with bound variables.
 renameDickinsonM :: (MonadState s m, HasRenames s) => Dickinson a -> m (Dickinson a)
 renameDickinsonM (Dickinson i d) = Dickinson i <$> renameDeclarationsM d
 
 renameDeclarationsM :: (MonadState s m, HasRenames s) => [Declaration a] -> m [Declaration a]
 renameDeclarationsM = traverse renameDeclarationM <=< traverse insDeclM
 
--- broadcast first...
+-- broadcast first... This allows definitions to be declared in any order.
 insDeclM :: (MonadState s m, HasRenames s) => Declaration a -> m (Declaration a)
 insDeclM (Define p n e) = do
     (n', modR) <- withName n
     modifying rename modR
     pure $ Define p n' e
-insDeclM d@TyDecl{} = pure d -- FIXME: scoping!! (two type decls should be illegal?)
+insDeclM d@TyDecl{} = pure d -- TODO: decide on spec for scoping. (two type decls should be illegal)
 
 renameDeclarationM :: (MonadState s m, HasRenames s) => Declaration a -> m (Declaration a)
 renameDeclarationM (Define p n e) =
     Define p n <$> renameExpressionM e
 renameDeclarationM d@TyDecl{} = pure d
 
+-- allows us to work with a temporary change to the renamer state, tracking the
+-- max sensibly
 withRenames :: (HasRenames s, MonadState s m) => (Renames -> Renames) -> m a -> m a
 withRenames modSt act = do
     preSt <- use rename
@@ -146,6 +151,7 @@
                 in let es = fmap snd branches
                     in NE.zip ds <$> traverse renameExpressionM es
 renameExpressionM (Interp p es) = Interp p <$> traverse renameExpressionM es
+renameExpressionM (MultiInterp p es) = MultiInterp p <$> traverse renameExpressionM es
 renameExpressionM (Concat p es) = Concat p <$> traverse renameExpressionM es
 renameExpressionM (Tuple p es)  = Tuple p <$> traverse renameExpressionM es
 renameExpressionM (Apply p e e') = Apply p <$> renameExpressionM e <*> renameExpressionM e'
diff --git a/src/Language/Dickinson/ScopeCheck.hs b/src/Language/Dickinson/ScopeCheck.hs
--- a/src/Language/Dickinson/ScopeCheck.hs
+++ b/src/Language/Dickinson/ScopeCheck.hs
@@ -42,24 +42,42 @@
 checkDickinson d = traverse_ insDecl d *> mapSumM checkDecl d
 
 insDecl :: Declaration a -> CheckM ()
-insDecl (Define _ n _) = insertName n
+insDecl (Define _ n _)   = insertName n
+insDecl (TyDecl _ n tys) = insertName n *> traverse_ insertName tys
 
 checkDecl :: Declaration a -> CheckM (Maybe (DickinsonError a))
 checkDecl (Define _ _ e) = checkExpr e
+checkDecl TyDecl{}       = pure Nothing
 
+checkType :: DickinsonTy a -> CheckM (Maybe (DickinsonError a))
+checkType TyText{}         = pure Nothing
+checkType (TyFun _ ty ty') = (<|>) <$> checkType ty <*> checkType ty'
+checkType (TyTuple _ tys)  = mapSumM checkType tys
+checkType (TyNamed l n@(Name _ (Unique k) _)) = do
+    b <- get
+    if k `IS.member` b
+        then pure Nothing
+        else pure $ Just (UnfoundType l n)
+
 checkExpr :: Expression a -> CheckM (Maybe (DickinsonError a))
 checkExpr Literal{}      = pure Nothing
 checkExpr StrChunk{}     = pure Nothing
 checkExpr (Apply _ e e') = (<|>) <$> checkExpr e <*> checkExpr e'
 checkExpr (Interp _ es)  = mapSumM checkExpr es
+checkExpr (MultiInterp _ es)  = mapSumM checkExpr es
 checkExpr (Choice _ brs) = mapSumM checkExpr (snd <$> brs)
 checkExpr (Concat _ es)  = mapSumM checkExpr es
 checkExpr (Tuple _ es)   = mapSumM checkExpr es
 checkExpr (Flatten _ e)  = checkExpr e
-checkExpr (Annot _ e _)  = checkExpr e
+checkExpr (Annot _ e ty) = (<|>) <$> checkExpr e <*> checkType ty
 checkExpr (Lambda _ n _ e) = do
     insertName n
     checkExpr e <* deleteName n
+checkExpr (Constructor _ n@(Name _ (Unique i) l)) = do
+    b <- get
+    if i `IS.member` b
+        then pure Nothing
+        else pure $ Just (UnfoundConstructor l n)
 checkExpr (Var _ n@(Name _ (Unique i) l)) = do
     b <- get
     if i `IS.member` b
diff --git a/src/Language/Dickinson/Type.hs b/src/Language/Dickinson/Type.hs
--- a/src/Language/Dickinson/Type.hs
+++ b/src/Language/Dickinson/Type.hs
@@ -18,8 +18,9 @@
 import qualified Data.List.NonEmpty            as NE
 import           Data.Semigroup                ((<>))
 import qualified Data.Text                     as T
-import           Data.Text.Prettyprint.Doc     (Doc, Pretty (pretty), brackets, colon, concatWith, dquotes, group,
-                                                hardline, hsep, indent, parens, pipe, rangle, tupled, vsep, (<+>))
+import           Data.Text.Prettyprint.Doc     (Doc, Pretty (pretty), align, brackets, colon, concatWith, dquotes,
+                                                group, hardline, hsep, indent, parens, pipe, rangle, tupled, vsep,
+                                                (<+>))
 import           Data.Text.Prettyprint.Doc.Ext (hardSep, (<#*>), (<#>), (<^>))
 import           GHC.Generics                  (Generic)
 import           Language.Dickinson.Name
@@ -52,34 +53,35 @@
 data Expression a = Literal { exprAnn :: a, litText :: T.Text }
                   | StrChunk { exprAnn :: a, chunkText :: T.Text }
                   | Choice { exprAnn :: a
-                           , choices :: (NonEmpty (Double, Expression a))
+                           , choices :: NonEmpty (Double, Expression a)
                            }
                   | Let { exprAnn  :: a
-                        , letBinds :: (NonEmpty (Name a, Expression a))
-                        , letExpr  :: (Expression a)
+                        , letBinds :: NonEmpty (Name a, Expression a)
+                        , letExpr  :: Expression a
                         }
-                  | Var { exprAnn :: a, exprVar :: (Name a) }
+                  | Var { exprAnn :: a, exprVar :: Name a }
                   | Interp { exprAnn :: a, exprInterp :: [Expression a] }
+                  | MultiInterp { exprAnn :: a, exprMultiInterp :: [Expression a] }
                   | Lambda { exprAnn    :: a
-                           , lambdaVar  :: (Name a)
-                           , lambdaTy   :: (DickinsonTy a)
-                           , lambdaExpr :: (Expression a)
+                           , lambdaVar  :: Name a
+                           , lambdaTy   :: DickinsonTy a
+                           , lambdaExpr :: Expression a
                            }
                   | Apply { exprAnn :: a
-                          , exprFun :: (Expression a)
-                          , exprArg :: (Expression a)
+                          , exprFun :: Expression a
+                          , exprArg :: Expression a
                           }
                   | Concat { exprAnn :: a, exprConcats :: [Expression a] }
-                  | Tuple { exprAnn :: a, exprTup :: (NonEmpty (Expression a)) }
+                  | Tuple { exprAnn :: a, exprTup :: NonEmpty (Expression a) }
                   | Match { exprAnn   :: a
-                          , exprMatch :: (Expression a)
-                          , exprPat   :: (Pattern a)
-                          , exprIn    :: (Expression a)
+                          , exprMatch :: Expression a
+                          , exprPat   :: Pattern a
+                          , exprIn    :: Expression a
                           }
-                  | Flatten { exprAnn :: a, exprFlat :: (Expression a) }
+                  | Flatten { exprAnn :: a, exprFlat :: Expression a }
                   | Annot { exprAnn :: a
-                          , expr    :: (Expression a)
-                          , exprTy  :: (DickinsonTy a)
+                          , expr    :: Expression a
+                          , exprTy  :: DickinsonTy a
                           }
                   | Constructor { exprAnn :: a, constructorName :: TyName a }
                   deriving (Generic, NFData, Binary, Functor, Show)
@@ -100,7 +102,7 @@
 
 instance Pretty (Declaration a) where
     pretty (Define _ n e)  = parens (":def" <+> pretty n <#> indent 2 (pretty e))
-    pretty (TyDecl _ n cs) = "tydecl" <+> pretty n <+> "=" <+> (concatWith (\x y -> x <+> pipe <+> y)) (toList (pretty <$> cs))
+    pretty (TyDecl _ n cs) = "tydecl" <+> pretty n <+> "=" <+> concatWith (\x y -> x <+> pipe <+> y) (toList (pretty <$> cs))
 
 instance Pretty (Import a) where
     pretty (Import _ n)   = parens (":include" <+> pretty n)
@@ -114,21 +116,26 @@
 prettyChoiceBranch :: (Double, Expression a) -> Doc b
 prettyChoiceBranch (d, e) = parens (pipe <+> pretty d <+> pretty e)
 
-
 prettyInterp :: Expression a -> Doc b
 prettyInterp (StrChunk _ t) = pretty (escReplace t)
 prettyInterp e              = "${" <> pretty e <> "}"
 
+prettyMultiInterp :: [Expression a] -> Doc b
+prettyMultiInterp = concatWith (<#>) . concatMap prettyChunk
+    where prettyChunk (StrChunk _ t) = fmap pretty (T.lines t) -- TODO: squish StrChunks together (so that ' blocks are not separated...)
+          prettyChunk _              = undefined -- TODO: handle this when interpolated multiline strings hit
+
 instance Pretty (Pattern a) where
     pretty (PatternVar _ n)    = pretty n
     pretty (PatternTuple _ ps) = tupled (toList (pretty <$> ps))
     pretty Wildcard{}          = "_"
+    pretty (PatternCons _ c)   = pretty c
 
 escReplace :: T.Text -> T.Text
 escReplace =
       T.replace "\"" "\\\""
     . T.replace "\n" "\\n"
-    . T.replace "$" "\\$"
+    . T.replace "${" "\\${"
 
 -- figure out indentation
 instance Pretty (Expression a) where
@@ -141,6 +148,7 @@
     pretty (Lambda _ n ty e)  = parens (":lambda" <+> pretty n <+> pretty ty <#*> pretty e)
     pretty (Apply _ e e')     = parens ("$" <+> pretty e <+> pretty e')
     pretty (Interp _ es)      = group (dquotes (foldMap prettyInterp es))
+    pretty (MultiInterp _ es) = group (align ("'''" <> prettyMultiInterp es <> "'''"))
     pretty (Concat _ es)      = parens (rangle <+> hsep (pretty <$> es))
     pretty StrChunk{}         = error "Internal error: naked StrChunk"
     pretty (Tuple _ es)       = tupled (toList (pretty <$> es))
diff --git a/src/Language/Dickinson/TypeCheck.hs b/src/Language/Dickinson/TypeCheck.hs
--- a/src/Language/Dickinson/TypeCheck.hs
+++ b/src/Language/Dickinson/TypeCheck.hs
@@ -33,7 +33,7 @@
     unless (ty' == ty) $
         throwError (TypeMismatch e ty ty')
 
-newtype TyEnv a = TyEnv { unTyEnv :: (IM.IntMap (DickinsonTy a)) }
+newtype TyEnv a = TyEnv { unTyEnv :: IM.IntMap (DickinsonTy a) }
     deriving (Binary)
 
 class HasTyEnv f where
@@ -71,10 +71,12 @@
 tyAddDecl Define{}         = pure ()
 tyAddDecl (TyDecl l tn cs) = traverse_ (\c -> tyInsert c (TyNamed l tn)) cs
 
-bindPattern :: (MonadState (s a) m, HasTyEnv s, MonadError (DickinsonError a) m) => Pattern a -> (DickinsonTy a) -> m ()
+bindPattern :: (MonadState (s a) m, HasTyEnv s, MonadError (DickinsonError a) m) => Pattern a -> DickinsonTy a -> m ()
 bindPattern (PatternVar _ n) ty                 = tyInsert n ty
 bindPattern Wildcard{} _                        = pure ()
-bindPattern (PatternTuple _ ps) (TyTuple _ tys) = Ext.zipWithM_ bindPattern ps tys -- FIXME: length ps = length tys
+bindPattern (PatternTuple l ps) (TyTuple _ tys)
+    | length ps == length tys = Ext.zipWithM_ bindPattern ps tys
+    | otherwise = throwError $ MalformedTuple l
 bindPattern (PatternTuple l _) _                = throwError $ MalformedTuple l
 
 -- run after global renamer
@@ -87,6 +89,8 @@
     case IM.lookup i tyEnv of
         Just ty -> pure ty
         Nothing -> throwError $ UnfoundName l n
+typeOf (MultiInterp l es) =
+    traverse_ (tyAssert (TyText undefined)) es $> TyText l
 typeOf (Interp l es) =
     traverse_ (tyAssert (TyText undefined)) es $> TyText l
 typeOf (Concat l es) =
diff --git a/test/Eval.hs b/test/Eval.hs
--- a/test/Eval.hs
+++ b/test/Eval.hs
@@ -17,18 +17,27 @@
     , resultCase "test/demo/tyAnnot.dck"
     , resultCase "test/data/quoteify.dck"
     , resultCase "test/data/hangIndefinitely.dck"
+    , resolveCase "test/data/hangIndefinitely.dck"
     ]
 
-forceText :: a -> Assertion
-forceText = (`seq` pure ())
+forceResult :: a -> Assertion
+forceResult = (`seq` pure ())
 
 resultCase :: FilePath -> TestTree
 resultCase fp = testCase fp $ result fp
 
+resolveCase :: FilePath -> TestTree
+resolveCase fp = testCase fp $ resolve fp
+
+resolve :: FilePath -> Assertion
+resolve fp = do
+    res <- resolveFile ["prelude", "lib"] fp
+    forceResult res
+
 result :: FilePath -> Assertion
 result fp = do
     res <- evalFile ["prelude", "lib"] fp
-    forceText res
+    forceResult res
 
 constEval :: Assertion
 constEval = do
diff --git a/test/Golden.hs b/test/Golden.hs
--- a/test/Golden.hs
+++ b/test/Golden.hs
@@ -31,6 +31,8 @@
         , renameDckFile "test/data/tupleRename.dck"
         , renameDckFile "test/eval/match.dck"
         , renameDckFile "test/data/quoteify.dck"
+        , withDckFile "test/data/lexDollarSign.dck"
+        , withDckFile "test/data/multiStr.dck"
         ]
 
 prettyBSL :: Dickinson a -> BSL.ByteString
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -43,16 +43,22 @@
         , parseNoError "lib/color.dck"
         , parseNoError "lib/birds.dck"
         , lexNoError "test/data/import.dck"
+        , lexNoError "test/data/lexDollarSign.dck"
         , parseNoError "test/data/import.dck"
         , detectDuplicate "test/data/multiple.dck"
         , detectDuplicate "test/error/double.dck"
         , detectDuplicate "test/error/tyDouble.dck"
         , detectDuplicate "test/error/tyConsDouble.dck"
         , lexNoError "examples/shakespeare.dck"
+        , lexNoError "test/data/multiStr.dck"
+        , parseNoError "test/data/multiStr.dck"
         , parseNoError "examples/shakespeare.dck"
         , detectScopeError "test/demo/improbableScope.dck"
         , detectBadBranch "test/demo/sillyOption.dck"
         , noScopeError "test/demo/circular.dck"
+        , noScopeError "test/data/adt.dck"
+        , detectScopeError "test/error/tyScope.dck"
+        , detectScopeError "test/error/constructorScope.dck"
         , findPath
         ]
 
diff --git a/test/data/adt.dck b/test/data/adt.dck
--- a/test/data/adt.dck
+++ b/test/data/adt.dck
@@ -1,9 +1,9 @@
 %-
 
-tydecl sex = Boy | Girl
-
 (:def boy   
   Boy : sex)
+
+tydecl sex = Boy | Girl
 
 (:def main
   "Hello, World!")
diff --git a/test/data/adt.pretty b/test/data/adt.pretty
--- a/test/data/adt.pretty
+++ b/test/data/adt.pretty
@@ -1,9 +1,9 @@
 %-
 
-tydecl sex = Boy | Girl
-
 (:def boy
   Boy : sex)
+
+tydecl sex = Boy | Girl
 
 (:def main
   "Hello, World!")
diff --git a/test/data/lexDollarSign.dck b/test/data/lexDollarSign.dck
new file mode 100644
--- /dev/null
+++ b/test/data/lexDollarSign.dck
@@ -0,0 +1,11 @@
+%-
+
+(:def main
+  (:oneof
+    (| "$")
+    (| "$a")
+    (| "$$")
+    (| "$${a}")
+    (| "$\${a}")
+    (| "$a${a}")
+    (| "${a}$")))
diff --git a/test/data/lexDollarSign.pretty b/test/data/lexDollarSign.pretty
new file mode 100644
--- /dev/null
+++ b/test/data/lexDollarSign.pretty
@@ -0,0 +1,11 @@
+%-
+
+(:def main
+  (:branch
+    (| 0.14285714285714285 "$")
+    (| 0.14285714285714285 "$a")
+    (| 0.14285714285714285 "$$")
+    (| 0.14285714285714285 "$${a}")
+    (| 0.14285714285714285 "$\${a}")
+    (| 0.14285714285714285 "$a${a}")
+    (| 0.14285714285714285 "${a}$")))
diff --git a/test/data/multiStr.dck b/test/data/multiStr.dck
new file mode 100644
--- /dev/null
+++ b/test/data/multiStr.dck
@@ -0,0 +1,16 @@
+%-
+
+(:def strangeChar
+  '''
+  I saw my boss' dog today
+  ''')
+
+(:def empty
+  '''
+  ''')
+
+(:def main
+  '''
+  This is a multi-line string
+  '''
+  )
diff --git a/test/data/multiStr.pretty b/test/data/multiStr.pretty
new file mode 100644
--- /dev/null
+++ b/test/data/multiStr.pretty
@@ -0,0 +1,15 @@
+%-
+
+(:def strangeChar
+  '''
+  I saw my boss' dog today
+  ''')
+
+(:def empty
+  '''
+  ''')
+
+(:def main
+  '''
+  This is a multi-line string
+  ''')
diff --git a/test/demo/animal.dck b/test/demo/animal.dck
new file mode 100644
--- /dev/null
+++ b/test/demo/animal.dck
@@ -0,0 +1,6 @@
+(:include animal)
+
+%-
+
+(:def main
+  animal)
diff --git a/test/demo/circular.dck b/test/demo/circular.dck
new file mode 100644
--- /dev/null
+++ b/test/demo/circular.dck
@@ -0,0 +1,7 @@
+%-
+
+(:def main
+  (a))
+
+(:def a
+  "a")
diff --git a/test/demo/improbableScope.dck b/test/demo/improbableScope.dck
new file mode 100644
--- /dev/null
+++ b/test/demo/improbableScope.dck
@@ -0,0 +1,13 @@
+(:include color)
+
+%-
+
+(:def invalid
+  (:let 
+    [yes "yes"]
+      (:branch
+        (| 1.0 yes)
+        (| 0.001 no))))
+
+(:def main
+  ("hello ${invalid}"))
diff --git a/test/demo/mutual.dck b/test/demo/mutual.dck
new file mode 100644
--- /dev/null
+++ b/test/demo/mutual.dck
@@ -0,0 +1,14 @@
+%-
+
+(:def a
+  (:branch
+    (| 0.75 "a")
+    (| 0.25 (> "a" b))))
+
+(:def b
+  (:branch
+    (| 0.75 "b")
+    (| 0.25 (> "b" a))))
+
+(:def main
+  (> a b))
diff --git a/test/demo/sillyOption.dck b/test/demo/sillyOption.dck
new file mode 100644
--- /dev/null
+++ b/test/demo/sillyOption.dck
@@ -0,0 +1,6 @@
+%-
+
+(:def silly
+  (:branch
+    (| 1.0 "hello")
+    (| 1.0 "hello")))
diff --git a/test/demo/tyAnnot.dck b/test/demo/tyAnnot.dck
new file mode 100644
--- /dev/null
+++ b/test/demo/tyAnnot.dck
@@ -0,0 +1,6 @@
+(:include animal)
+
+%-
+
+(:def main
+  (animal : text))
diff --git a/test/error/constructorScope.dck b/test/error/constructorScope.dck
new file mode 100644
--- /dev/null
+++ b/test/error/constructorScope.dck
@@ -0,0 +1,6 @@
+%-
+
+tydecl sex = Boy | Girl
+
+(:def boy   
+  NonBinary : sex)
diff --git a/test/error/double.dck b/test/error/double.dck
new file mode 100644
--- /dev/null
+++ b/test/error/double.dck
@@ -0,0 +1,7 @@
+%-
+
+(:def one
+  "name")
+
+(:def one
+  "name")
diff --git a/test/error/illTyped.dck b/test/error/illTyped.dck
new file mode 100644
--- /dev/null
+++ b/test/error/illTyped.dck
@@ -0,0 +1,8 @@
+%-
+
+(:def silly
+  (:lambda func (-> text text)
+    ("Helo, ${func}")))
+
+(:def main
+  silly)
diff --git a/test/error/lexErr.dck b/test/error/lexErr.dck
new file mode 100644
--- /dev/null
+++ b/test/error/lexErr.dck
@@ -0,0 +1,4 @@
+&$
+
+(:def main
+  "hello")
diff --git a/test/error/tyConsDouble.dck b/test/error/tyConsDouble.dck
new file mode 100644
--- /dev/null
+++ b/test/error/tyConsDouble.dck
@@ -0,0 +1,5 @@
+%-
+
+tydecl sex = Boy | Girl
+
+tydecl gender = Boy | Girl | NonBinary
diff --git a/test/error/tyDouble.dck b/test/error/tyDouble.dck
new file mode 100644
--- /dev/null
+++ b/test/error/tyDouble.dck
@@ -0,0 +1,5 @@
+%-
+
+tydecl sex = Boy | Girl
+
+tydecl sex = Boy | Girl | NonBinary
diff --git a/test/error/tyScope.dck b/test/error/tyScope.dck
new file mode 100644
--- /dev/null
+++ b/test/error/tyScope.dck
@@ -0,0 +1,6 @@
+%-
+
+tydecl sex = Boy | Girl
+
+(:def boy   
+  Boy : gender)
diff --git a/test/eval/choice.dck b/test/eval/choice.dck
new file mode 100644
--- /dev/null
+++ b/test/eval/choice.dck
@@ -0,0 +1,6 @@
+%-
+
+(:def main
+    (:oneof
+        (| "woman")
+        (| "man")))
diff --git a/test/eval/context.dck b/test/eval/context.dck
new file mode 100644
--- /dev/null
+++ b/test/eval/context.dck
@@ -0,0 +1,15 @@
+%-
+
+(:def letBinding
+    (:let
+        [a "woman"]
+        (:branch
+            (| 1.0 a)
+            (| 1.0 a))))
+
+(:def main
+    (:let
+        [a letBinding]
+        (:branch
+            (| 1.0 letBinding)
+            (| 1.0 a))))
diff --git a/test/eval/local.dck b/test/eval/local.dck
new file mode 100644
--- /dev/null
+++ b/test/eval/local.dck
@@ -0,0 +1,9 @@
+%-
+
+(:def main
+    (:let
+        [a "man"]
+        [b "woman"]
+        (:branch
+            (| 1.0 a)
+            (| 1.0 b))))
diff --git a/test/eval/main.dck b/test/eval/main.dck
new file mode 100644
--- /dev/null
+++ b/test/eval/main.dck
@@ -0,0 +1,5 @@
+%-
+
+(:def main
+    (:oneof
+        (| "man")))
diff --git a/test/eval/match.dck b/test/eval/match.dck
new file mode 100644
--- /dev/null
+++ b/test/eval/match.dck
@@ -0,0 +1,27 @@
+%-
+
+(:def fst
+  (:lambda xy (text, text)
+    (:match xy (x, _)
+      x)))
+
+(:def snd
+  (:lambda xy (text, text)
+    (:match xy (_, y)
+      y)))
+
+; TODO tydecl sex = Boy | Girl
+
+; see https://github.com/vmchale/doggo-command-line/blob/master/src/main.rs
+(:def greeter
+  (:lambda dog (text, text)
+    (:let
+      [name ($fst dog)]
+      [pronoun ($snd dog)]
+        (:oneof
+          (| "${name} is a heckin' fine floofer")
+          (| "${name} is a good woofer")
+          (| "${name} eats toilet paper sometimes but ${pronoun} tries.")))))
+
+(:def main
+  ($ greeter ("Maxine", "she")))
