diff --git a/README.org b/README.org
--- a/README.org
+++ b/README.org
@@ -1,11 +1,11 @@
 #+OPTIONS: num:nil toc:nil
 * Axel
   Haskell + Lisp + JVM/Node/... = Profit!
-  
+
   See [[https://axellang.github.io]].
 ** Code Style
    Use ~hindent~ to format code and ~hlint~ to catch errors.
 ** Running
-   Run ~scripts/build.sh~ to build the project, and ~stack exec axel-exe~ to run ~app/Main.hs~. The executable takes one argument, the path of the Haskell program to transpile and execute.
+   Run ~scripts/build.sh~ to build the project, and ~stack exec axel-exe -- <arguments>~ to run ~app/Main.hs~. The executable takes as arguments either ~file <path to file>~ (which is the path of the Axel program to build and execute) or ~project~ (in which case an Axel project in the current directory will be built and executed).
 ** Examples
    See the ~examples~ folder for example Axel programs.
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -1,13 +1,32 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
 module Main where
 
-import Axel.Project (buildProject, runProject)
+import Axel.Error (Error)
+import qualified Axel.Error as Error (toIO)
+import Axel.Haskell.File (evalFile)
+import Axel.Haskell.Project (buildProject, runProject)
+import Axel.Parse.Args (ModeCommand(File, Project), modeCommandParser)
 
-import System.Directory (setCurrentDirectory)
-import System.Environment (getArgs)
+import Control.Monad.Except (ExceptT, MonadError)
+import Control.Monad.IO.Class (MonadIO)
 
+import Options.Applicative (execParser, info, progDesc)
+
+newtype AppM a = AppM
+  { runAppM :: ExceptT Error IO a
+  } deriving (Functor, Applicative, Monad, MonadError Error, MonadIO)
+
+runAppM' :: AppM a -> IO a
+runAppM' = Error.toIO . runAppM
+
+app :: ModeCommand -> AppM ()
+app (File filePath) = evalFile filePath
+app Project = buildProject >> runProject
+
 main :: IO ()
 main = do
-  [projectPath] <- getArgs
-  setCurrentDirectory projectPath
-  buildProject
-  runProject
+  modeCommand <-
+    execParser $ info modeCommandParser (progDesc "The command to run.")
+  runAppM' $ app modeCommand
diff --git a/axel.cabal b/axel.cabal
--- a/axel.cabal
+++ b/axel.cabal
@@ -2,10 +2,10 @@
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: deb466082a59f53f497793cec5211f873c7f26948954aad70363885d5cd568bd
+-- hash: bd98666bdad98615fa9b95dbfdbcf860ae5ad9fd5595b478e5f76d1390962147
 
 name:           axel
-version:        0.0.4
+version:        0.0.5
 synopsis:       The Axel programming language.
 description:    Haskell's semantics, plus Lisp's macros. Meet Axel – a purely functional, extensible, and powerful programming language.
 category:       Language, Lisp, Macros, Transpiler
@@ -20,15 +20,17 @@
 cabal-version:  >= 1.10
 extra-source-files:
     examples/axelTemp/Axel.hs
-    examples/do.axel
-    examples/example.axel
+    examples/doNotation.axel
+    examples/if.axel
     README.org
     scripts/build.sh
     scripts/clean.sh
+    scripts/format.sh
     scripts/ghcid.sh
     scripts/lint.sh
 data-files:
     resources/autogenerated/macros/AST.hs
+    resources/macros/MacroDefinitionAndEnvironmentFooter.hs
     resources/macros/MacroDefinitionAndEnvironmentHeader.hs
     resources/macros/Scaffold.hs
     resources/new-project-template/app/Main.axel
@@ -46,45 +48,50 @@
 
 library
   exposed-modules:
-      Axel.Parse.AST
-      Axel.Project
-  other-modules:
       Axel.AST
       Axel.Denormalize
-      Axel.Entry
       Axel.Error
-      Axel.Eval
-      Axel.GHC
+      Axel.Haskell.File
+      Axel.Haskell.GHC
+      Axel.Haskell.Prettify
+      Axel.Haskell.Project
+      Axel.Haskell.Stack
       Axel.Macros
+      Axel.Monad.Console
+      Axel.Monad.FileSystem
+      Axel.Monad.Process
+      Axel.Monad.Resource
       Axel.Normalize
       Axel.Parse
-      Axel.Quote
+      Axel.Parse.Args
+      Axel.Parse.AST
       Axel.Utils.Debug
-      Axel.Utils.Directory
       Axel.Utils.Display
       Axel.Utils.Function
       Axel.Utils.List
       Axel.Utils.Recursion
-      Axel.Utils.Resources
       Axel.Utils.String
+  other-modules:
       Paths_axel
   hs-source-dirs:
       src
-  ghc-options: -Wall -Wcpp-undef -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wmissing-import-lists -Wpartial-fields -Wredundant-constraints
+  ghc-options: -Wall -Wcpp-undef -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -Wno-simplifiable-class-constraints -Wmissing-import-lists
   build-depends:
       base >=4.11.1 && <4.12
+    , bytestring >=0.10.8 && <0.11
     , directory >=1.3 && <1.4
     , filepath >=1.4.1 && <1.5
+    , haskell-src-exts >=1.20.2 && <1.21
     , lens >=4.16.1 && <4.18
     , lens-aeson >=1.0.2 && <1.1
-    , monad-control >=1.0.2 && <1.1
     , mtl >=2.2.1 && <2.3
+    , optparse-applicative >=0.14.2 && <0.15
     , parsec >=3.1.11 && <3.2
     , process >=1.6.1 && <1.7
     , regex-pcre >=0.94.4 && <0.95
-    , split >=0.2.3 && <0.3
     , strict >=0.3.2 && <0.4
     , text >=1.2.2 && <1.3
+    , transformers >=0.5.5 && <0.6
     , typed-process >=0.2.2 && <0.3
     , vector >=0.12.0 && <0.13
     , yaml >=0.8.31 && <0.10
@@ -96,9 +103,52 @@
       Paths_axel
   hs-source-dirs:
       app
-  ghc-options: -Wall -Wcpp-undef -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wmissing-import-lists -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N
+  ghc-options: -Wall -Wcpp-undef -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -Wno-simplifiable-class-constraints -threaded -rtsopts -with-rtsopts=-N -Wmissing-import-lists
   build-depends:
       axel
     , base >=4.11.1 && <4.12
-    , directory >=1.3 && <1.4
+    , mtl >=2.2.1 && <2.3
+    , optparse-applicative >=0.14.2 && <0.15
+  default-language: Haskell2010
+
+test-suite axel-test
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  other-modules:
+      Axel.Test.ASTGen
+      Axel.Test.DenormalizeSpec
+      Axel.Test.File.FileSpec
+      Axel.Test.Haskell.GHCSpec
+      Axel.Test.Haskell.StackSpec
+      Axel.Test.MockUtils
+      Axel.Test.Monad.ConsoleMock
+      Axel.Test.Monad.ConsoleSpec
+      Axel.Test.Monad.FileSystemMock
+      Axel.Test.Monad.FileSystemSpec
+      Axel.Test.Monad.ProcessMock
+      Axel.Test.Monad.ResourceMock
+      Axel.Test.Monad.ResourceSpec
+      Axel.Test.NormalizeSpec
+      Axel.Test.Parse.ASTGen
+      Axel.Test.ParseSpec
+      Paths_axel
+  hs-source-dirs:
+      test
+  ghc-options: -Wall -Wcpp-undef -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -Wno-simplifiable-class-constraints -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      axel
+    , base >=4.11.1 && <4.12
+    , bytestring
+    , filepath
+    , hedgehog
+    , lens
+    , mtl >=2.2.1 && <2.3
+    , split
+    , tasty
+    , tasty-discover
+    , tasty-golden
+    , tasty-hedgehog
+    , tasty-hspec
+    , template-haskell
+    , transformers
   default-language: Haskell2010
diff --git a/examples/do.axel b/examples/do.axel
deleted file mode 100644
--- a/examples/do.axel
+++ /dev/null
@@ -1,44 +0,0 @@
-(module Main)
-
-(importq Axel.Parse.AST AST all)
-
-(defmacro quasiquote
-  (([(AST.SExpression xs)])
-   (let ((quasiquoteElem (fn (x) (case x
-                                   ((AST.SExpression ['unquote x])
-                                    (AST.SExpression ['list x]))
-                                   ((AST.SExpression ['unquoteSplicing x])
-                                    x)
-                                   (atom
-                                    (AST.SExpression
-                                     ['list
-                                      (AST.SExpression ['quasiquote atom])]))))))
-     (pure [(AST.SExpression ['AST.SExpression (AST.SExpression ['concat (AST.SExpression (: 'list (map quasiquoteElem xs)))])])])))
-  (([atom]) (pure [(AST.SExpression ['quote atom])])))
-
-(defmacro fnCase
-  ((cases) (<$> (fn (varId)
-                  [`(fn (~varId)
-                      (case ~varId ~@cases))])
-                AST.gensym)))
-
-(= do' (-> ([] AST.Expression) AST.Expression)
-   (() (fnCase
-        ((: var (: '<- (: val rest)))
-         `(>>= ~val (fn (~var) ~(do' rest))))
-        ((: val rest)
-         (case rest
-           ([]
-            val)
-           (_
-            `(>> ~val ~(do' rest))))))))
-
-(defmacro do
-  ((input)
-   (pure [(do' input)])))
-
-(= main (IO Unit)
-   (() (do
-         line <- getLine
-         (putStrLn line)
-         (pure unit))))
diff --git a/examples/doNotation.axel b/examples/doNotation.axel
new file mode 100644
--- /dev/null
+++ b/examples/doNotation.axel
@@ -0,0 +1,50 @@
+(importq Axel.Parse.AST AST all)
+
+-- NOTE This will eventually be automatically defined by the Axel prelude.
+(macro quasiquote ([(AST.SExpression xs)])
+       (let ((quasiquoteElem (\ (x) (case x
+                                      ((AST.SExpression ['unquote x])
+                                       (AST.SExpression ['list x]))
+                                      ((AST.SExpression ['unquoteSplicing x])
+                                       (AST.SExpression ['AST.toExpressionList x]))
+                                      (atom
+                                       (AST.SExpression
+                                        ['list
+                                         (AST.SExpression ['quasiquote atom])]))))))
+         (pure [(AST.SExpression ['AST.SExpression (AST.SExpression ['concat (AST.SExpression (: 'list (map quasiquoteElem xs)))])])])))
+(macro quasiquote ([atom])
+       (pure [(AST.SExpression ['quote atom])]))
+
+-- NOTE This will eventually be automatically defined by the Axel prelude.
+(macro fnCase (cases)
+       (<$> (\ (varId)
+             [`(\ (~varId) (case ~varId ~@cases))])
+            AST.gensym))
+
+-- NOTE This will eventually be automatically defined by the Axel prelude.
+(macro def ((: name (: typeSig cases)))
+       (pure
+        (: `(:: ~name ~typeSig)
+           (map (\ (x) `(= ~name ~@x))
+                cases))))
+
+(def mdo' (-> (List AST.Expression) AST.Expression)
+  (((: var (: '<- (: val rest))))
+   `(>>= ~val (\ (~var) ~(mdo' rest))))
+  (((: val rest))
+   (case rest
+     ([]
+      val)
+     (_
+      `(>> ~val ~(mdo' rest))))))
+
+-- NOTE This will eventually be automatically defined by the Axel prelude.
+(macro mdo (input)
+       (pure [(mdo' input)]))
+
+(def main (IO Unit)
+  (()
+   (mdo
+    line <- getLine
+    (putStrLn line)
+    (pure unit))))
diff --git a/examples/example.axel b/examples/example.axel
deleted file mode 100644
--- a/examples/example.axel
+++ /dev/null
@@ -1,30 +0,0 @@
--- This is just an example, and, since it's partial, is NOT advised for actual use.
-(defmacro' when
-  (([condition body]) (pure [`(if ~condition ~body (error "WHEN"))])))
-
-(defmacro defmacro'
-  ((exprs)
-   (pure [`(defmacro ~@exprs)])))
-
-(defmacro if
-  (([cond true false])
-   (pure [`(case ~cond
-             (True ~true)
-             (False ~false))])))
-
-(defmacro quasiquote
-  (([(AST.SExpression xs)])
-   (let ((quasiquoteElem (fn (x) (case x
-                                   ((AST.SExpression ['unquote x])
-                                    (AST.SExpression ['list x]))
-                                   ((AST.SExpression ['unquoteSplicing x])
-                                    x)
-                                   (atom
-                                    (AST.SExpression
-                                     ['list
-                                      (AST.SExpression ['quasiquote atom])]))))))
-     (pure [(AST.SExpression ['AST.SExpression (AST.SExpression ['concat (AST.SExpression (: 'list (map quasiquoteElem xs)))])])])))
-  (([atom]) (pure [(AST.SExpression ['quote atom])])))
-
-(= main (IO Unit)
-   (() (if (== 1 1) (putStrLn "Hi!") (putStrLn "wat"))))
diff --git a/examples/if.axel b/examples/if.axel
new file mode 100644
--- /dev/null
+++ b/examples/if.axel
@@ -0,0 +1,40 @@
+(importq Axel.Parse.AST AST all)
+
+-- NOTE This will eventually be automatically defined by the Axel prelude.
+(macro quasiquote ([(AST.SExpression xs)])
+       (let ((quasiquoteElem (\ (x) (case x
+                                      ((AST.SExpression ['unquote x])
+                                       (AST.SExpression ['list x]))
+                                      ((AST.SExpression ['unquoteSplicing x])
+                                       (AST.SExpression ['AST.toExpressionList x]))
+                                      (atom
+                                       (AST.SExpression
+                                        ['list
+                                         (AST.SExpression ['quasiquote atom])]))))))
+         (pure [(AST.SExpression ['AST.SExpression (AST.SExpression ['concat (AST.SExpression (: 'list (map quasiquoteElem xs)))])])])))
+(macro quasiquote ([atom])
+       (pure [(AST.SExpression ['quote atom])]))
+
+-- NOTE This will eventually be automatically defined by the Axel prelude.
+(macro fnCase (cases)
+       (<$> (\ (varId)
+             [`(\ (~varId) (case ~varId ~@cases))])
+            AST.gensym))
+
+-- NOTE This will eventually be automatically defined by the Axel prelude.
+(macro def ((: name (: typeSig cases)))
+       (pure
+        (: `(:: ~name ~typeSig)
+           (map (\ (x) `(= ~name ~@x))
+                cases))))
+
+-- NOTE This will eventually be automatically defined by the Axel prelude.
+(macro if ([cond true false])
+       (pure [`(case ~cond
+                 (True ~true)
+                 (False ~false))]))
+
+(def main (IO Unit)
+  (()
+   (putStrLn
+    (if (== 1 1) "Correct!" "Impossible..."))))
diff --git a/resources/autogenerated/macros/AST.hs b/resources/autogenerated/macros/AST.hs
--- a/resources/autogenerated/macros/AST.hs
+++ b/resources/autogenerated/macros/AST.hs
@@ -1,14 +1,13 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE InstanceSigs #-}
+
 -- NOTE Because this file will be used as the header of auto-generated macro programs,
 --      it can't have any project-specific dependencies (such as `Fix`).
 module Axel.Parse.AST where
 
-import           Data.IORef                     ( IORef
-                                                , modifyIORef
-                                                , newIORef
-                                                , readIORef
-                                                )
+import Data.IORef (IORef, modifyIORef, newIORef, readIORef)
 
-import           System.IO.Unsafe               ( unsafePerformIO )
+import System.IO.Unsafe (unsafePerformIO)
 
 -- TODO `Expression` should probably be `Traversable`, use recursion schemes, etc.
 --      I should provide `toFix` and `fromFix` functions for macros to take advantage of.
@@ -25,11 +24,11 @@
 -- Internal utilities
 -- ******************************
 toAxel :: Expression -> String
-toAxel (LiteralChar   x ) = ['\\', x]
-toAxel (LiteralInt    x ) = show x
+toAxel (LiteralChar x) = ['{', x, '}']
+toAxel (LiteralInt x) = show x
 toAxel (LiteralString xs) = "\"" ++ xs ++ "\""
-toAxel (SExpression   xs) = "(" ++ unwords (map toAxel xs) ++ ")"
-toAxel (Symbol        x ) = x
+toAxel (SExpression xs) = "(" ++ unwords (map toAxel xs) ++ ")"
+toAxel (Symbol x) = x
 
 -- ******************************
 -- Macro definition utilities
@@ -44,3 +43,20 @@
   let identifier = "aXEL_AUTOGENERATED_IDENTIFIER_" ++ show suffix
   modifyIORef gensymCounter succ
   pure $ Symbol identifier
+
+-- | This allows splice-unquoting of both `[Expression]`s and `SExpression`s, without requiring special syntax for each.
+class ToExpressionList a where
+  toExpressionList :: a -> [Expression]
+
+instance ToExpressionList [Expression] where
+  toExpressionList :: [Expression] -> [Expression]
+  toExpressionList = id
+
+-- | Because we do not have a way to statically ensure an `SExpression` is passed (and not another one of `Expression`'s constructors instead), we will error at compile-time if a macro attempts to splice-unquote inappropriately.
+instance ToExpressionList Expression where
+  toExpressionList :: Expression -> [Expression]
+  toExpressionList (SExpression xs) = xs
+  toExpressionList x =
+    error
+      (show x <>
+       " cannot be splice-unquoted, because it is not an s-expression!")
diff --git a/resources/macros/MacroDefinitionAndEnvironmentFooter.hs b/resources/macros/MacroDefinitionAndEnvironmentFooter.hs
new file mode 100644
--- /dev/null
+++ b/resources/macros/MacroDefinitionAndEnvironmentFooter.hs
@@ -0,0 +1,1 @@
+%%%MACRO_NAME%%% :: [AST.Expression] -> IO [AST.Expression]
diff --git a/resources/macros/MacroDefinitionAndEnvironmentHeader.hs b/resources/macros/MacroDefinitionAndEnvironmentHeader.hs
--- a/resources/macros/MacroDefinitionAndEnvironmentHeader.hs
+++ b/resources/macros/MacroDefinitionAndEnvironmentHeader.hs
@@ -1,3 +1,1 @@
 module MacroDefinitionAndEnvironment where
-
-import qualified Axel.Parse.AST as AST
diff --git a/resources/new-project-template/Setup.hs b/resources/new-project-template/Setup.hs
--- a/resources/new-project-template/Setup.hs
+++ b/resources/new-project-template/Setup.hs
@@ -1,3 +1,4 @@
-import Distribution.Simple(defaultMain)
+import Distribution.Simple (defaultMain)
+
 main :: (IO ())
-main  = defaultMain
+main = defaultMain
diff --git a/resources/new-project-template/app/Main.hs b/resources/new-project-template/app/Main.hs
--- a/resources/new-project-template/app/Main.hs
+++ b/resources/new-project-template/app/Main.hs
@@ -1,4 +1,4 @@
-module Main where
 import Lib
+
 main :: (IO ())
-main  = someFunc
+main = someFunc
diff --git a/resources/new-project-template/src/Lib.hs b/resources/new-project-template/src/Lib.hs
--- a/resources/new-project-template/src/Lib.hs
+++ b/resources/new-project-template/src/Lib.hs
@@ -1,3 +1,2 @@
-module Lib where
 someFunc :: (IO ())
-someFunc  = (putStrLn "someFunc")
+someFunc = (putStrLn "someFunc")
diff --git a/resources/new-project-template/test/Spec.hs b/resources/new-project-template/test/Spec.hs
--- a/resources/new-project-template/test/Spec.hs
+++ b/resources/new-project-template/test/Spec.hs
@@ -1,2 +1,2 @@
 main :: (IO ())
-main  = (putStrLn "Test suite not yet implemented")
+main = (putStrLn "Test suite not yet implemented")
diff --git a/scripts/format.sh b/scripts/format.sh
new file mode 100644
--- /dev/null
+++ b/scripts/format.sh
@@ -0,0 +1,4 @@
+#!/bin/zsh
+for f in {app,src,test}/**/*.hs; do
+  hindent "$f"
+done
diff --git a/src/Axel/AST.hs b/src/Axel/AST.hs
--- a/src/Axel/AST.hs
+++ b/src/Axel/AST.hs
@@ -9,7 +9,6 @@
 
 module Axel.AST where
 
-import Axel.Error (fatal)
 import Axel.Utils.Display
   ( Bracket(DoubleQuotes, Parentheses, SingleQuotes, SquareBrackets)
   , Delimiter(Commas, Newlines, Pipes, Spaces)
@@ -19,13 +18,13 @@
   , renderPragma
   , surround
   )
-import Axel.Utils.Recursion (Recursive(bottomUpFmap, bottomUpTraverse))
+import Axel.Utils.Recursion
+  ( Recursive(bottomUpFmap, bottomUpTraverse, topDownFmap)
+  )
 
 import Control.Arrow ((***))
 import Control.Lens.Operators ((%~), (^.))
 import Control.Lens.TH (makeFieldsNoPrefix)
-import Control.Lens.Tuple (_1, _2)
-
 import Data.Function ((&))
 import Data.Semigroup ((<>))
 
@@ -37,21 +36,21 @@
 data CaseBlock = CaseBlock
   { _expr :: Expression
   , _matches :: [(Expression, Expression)]
-  } deriving (Eq)
+  } deriving (Eq, Show)
 
 data FunctionApplication = FunctionApplication
   { _function :: Expression
   , _arguments :: [Expression]
-  } deriving (Eq)
+  } deriving (Eq, Show)
 
 newtype TopLevel = TopLevel
   { _statements :: [Statement]
-  } deriving (Eq)
+  } deriving (Eq, Show)
 
 data TypeDefinition
   = ProperType Identifier
   | TypeConstructor FunctionApplication
-  deriving (Eq)
+  deriving (Eq, Show)
 
 instance ToHaskell TypeDefinition where
   toHaskell :: TypeDefinition -> String
@@ -61,27 +60,19 @@
 data DataDeclaration = DataDeclaration
   { _typeDefinition :: TypeDefinition
   , _constructors :: [FunctionApplication]
-  } deriving (Eq)
-
-newtype ArgumentList =
-  ArgumentList [Expression]
-  deriving (Eq)
-
-instance ToHaskell ArgumentList where
-  toHaskell :: ArgumentList -> String
-  toHaskell (ArgumentList arguments) = delimit Spaces $ map toHaskell arguments
+  } deriving (Eq, Show)
 
 data FunctionDefinition = FunctionDefinition
   { _name :: Identifier
-  , _typeSignature :: FunctionApplication
-  , _definitions :: [(ArgumentList, Expression)]
-  } deriving (Eq)
+  , _arguments :: [Expression]
+  , _body :: Expression
+  } deriving (Eq, Show)
 
 data Import
   = ImportItem Identifier
   | ImportType Identifier
                [Identifier]
-  deriving (Eq)
+  deriving (Eq, Show)
 
 instance ToHaskell Import where
   toHaskell :: Import -> String
@@ -95,7 +86,7 @@
 data ImportSpecification
   = ImportAll
   | ImportOnly [Import]
-  deriving (Eq)
+  deriving (Eq, Show)
 
 instance ToHaskell ImportSpecification where
   toHaskell :: ImportSpecification -> String
@@ -106,42 +97,46 @@
 data Lambda = Lambda
   { _arguments :: [Expression]
   , _body :: Expression
-  } deriving (Eq)
-
-newtype LanguagePragma = LanguagePragma
-  { _language :: Identifier
-  } deriving (Eq)
+  } deriving (Eq, Show)
 
 data LetBlock = LetBlock
   { _bindings :: [(Expression, Expression)]
   , _body :: Expression
-  } deriving (Eq)
+  } deriving (Eq, Show)
 
-data MacroDefinition = MacroDefinition
-  { _name :: Identifier
-  , _definitions :: [(ArgumentList, Expression)]
-  } deriving (Eq)
+newtype MacroDefinition = MacroDefinition
+  { _functionDefinition :: FunctionDefinition
+  } deriving (Eq, Show)
 
+newtype Pragma = Pragma
+  { _pragmaSpecification :: String
+  } deriving (Eq, Show)
+
 data QualifiedImport = QualifiedImport
   { _moduleName :: Identifier
   , _alias :: Identifier
   , _imports :: ImportSpecification
-  } deriving (Eq)
+  } deriving (Eq, Show)
 
 data RestrictedImport = RestrictedImport
   { _moduleName :: Identifier
   , _imports :: ImportSpecification
-  } deriving (Eq)
+  } deriving (Eq, Show)
 
 data TypeclassInstance = TypeclassInstance
   { _instanceName :: Expression
   , _definitions :: [FunctionDefinition]
-  } deriving (Eq)
+  } deriving (Eq, Show)
 
+data TypeSignature = TypeSignature
+  { _name :: Identifier
+  , _typeDefinition :: Expression
+  } deriving (Eq, Show)
+
 data TypeSynonym = TypeSynonym
   { _alias :: Expression
   , _definition :: Expression
-  } deriving (Eq)
+  } deriving (Eq, Show)
 
 data Expression
   = ECaseBlock CaseBlock
@@ -151,7 +146,7 @@
   | ELambda Lambda
   | ELetBlock LetBlock
   | ELiteral Literal
-  deriving (Eq)
+  deriving (Eq, Show)
 
 instance ToHaskell Expression where
   toHaskell :: Expression -> String
@@ -169,43 +164,42 @@
 data Literal
   = LChar Char
   | LInt Int
-  | LList [Expression]
   | LString String
-  deriving (Eq)
+  deriving (Eq, Show)
 
 instance ToHaskell Literal where
   toHaskell :: Literal -> String
   toHaskell (LChar x) = surround SingleQuotes [x]
   toHaskell (LInt x) = show x
-  toHaskell (LList xs) =
-    surround SquareBrackets $ delimit Commas $ map toHaskell xs
   toHaskell (LString x) = surround DoubleQuotes x
 
 data Statement
   = SDataDeclaration DataDeclaration
   | SFunctionDefinition FunctionDefinition
-  | SLanguagePragma LanguagePragma
   | SMacroDefinition MacroDefinition
   | SModuleDeclaration Identifier
+  | SPragma Pragma
   | SQualifiedImport QualifiedImport
   | SRestrictedImport RestrictedImport
   | STopLevel TopLevel
   | STypeclassInstance TypeclassInstance
+  | STypeSignature TypeSignature
   | STypeSynonym TypeSynonym
   | SUnrestrictedImport Identifier
-  deriving (Eq)
+  deriving (Eq, Show)
 
 instance ToHaskell Statement where
   toHaskell :: Statement -> String
   toHaskell (SDataDeclaration x) = toHaskell x
   toHaskell (SFunctionDefinition x) = toHaskell x
-  toHaskell (SLanguagePragma x) = toHaskell x
+  toHaskell (SPragma x) = toHaskell x
   toHaskell (SMacroDefinition x) = toHaskell x
   toHaskell (SModuleDeclaration x) = "module " <> x <> " where"
   toHaskell (SQualifiedImport x) = toHaskell x
   toHaskell (SRestrictedImport x) = toHaskell x
   toHaskell (STopLevel xs) = toHaskell xs
   toHaskell (STypeclassInstance x) = toHaskell x
+  toHaskell (STypeSignature x) = toHaskell x
   toHaskell (STypeSynonym x) = toHaskell x
   toHaskell (SUnrestrictedImport x) = "import " <> x
 
@@ -221,12 +215,12 @@
 
 makeFieldsNoPrefix ''Lambda
 
-makeFieldsNoPrefix ''LanguagePragma
-
 makeFieldsNoPrefix ''LetBlock
 
 makeFieldsNoPrefix ''MacroDefinition
 
+makeFieldsNoPrefix ''Pragma
+
 makeFieldsNoPrefix ''QualifiedImport
 
 makeFieldsNoPrefix ''RestrictedImport
@@ -235,6 +229,8 @@
 
 makeFieldsNoPrefix ''TypeclassInstance
 
+makeFieldsNoPrefix ''TypeSignature
+
 makeFieldsNoPrefix ''TypeSynonym
 
 instance ToHaskell CaseBlock where
@@ -258,20 +254,19 @@
         toHaskell (functionApplication ^. function) <> " " <>
         delimit Spaces (map toHaskell $ functionApplication ^. arguments)
 
-functionDefinitionToHaskell ::
-     Identifier -> (ArgumentList, Expression) -> String
-functionDefinitionToHaskell functionName (pattern', definitionBody) =
-  functionName <> " " <> toHaskell pattern' <> " = " <> toHaskell definitionBody
+instance ToHaskell TypeSignature where
+  toHaskell :: TypeSignature -> String
+  toHaskell typeSignature =
+    toHaskell (EIdentifier (typeSignature ^. name)) <> " :: " <>
+    toHaskell (typeSignature ^. typeDefinition)
 
 instance ToHaskell FunctionDefinition where
   toHaskell :: FunctionDefinition -> String
-  toHaskell functionDefinition =
-    delimit Newlines $
-    (functionDefinition ^. name <> " :: " <>
-     toHaskell (functionDefinition ^. typeSignature)) :
-    map
-      (functionDefinitionToHaskell $ functionDefinition ^. name)
-      (functionDefinition ^. definitions)
+  toHaskell fnDef =
+    toHaskell (EIdentifier (fnDef ^. name)) <> " " <>
+    delimit Spaces (map toHaskell (fnDef ^. arguments)) <>
+    " = " <>
+    toHaskell (fnDef ^. body)
 
 instance ToHaskell DataDeclaration where
   toHaskell :: DataDeclaration -> String
@@ -291,10 +286,9 @@
     "\\" <> delimit Spaces (map toHaskell (lambda ^. arguments)) <> " -> " <>
     toHaskell (lambda ^. body)
 
-instance ToHaskell LanguagePragma where
-  toHaskell :: LanguagePragma -> String
-  toHaskell languagePragma =
-    renderPragma $ "LANGUAGE " <> languagePragma ^. language
+instance ToHaskell Pragma where
+  toHaskell :: Pragma -> String
+  toHaskell pragma = renderPragma (pragma ^. pragmaSpecification)
 
 instance ToHaskell LetBlock where
   toHaskell :: LetBlock -> String
@@ -309,12 +303,7 @@
 
 instance ToHaskell MacroDefinition where
   toHaskell :: MacroDefinition -> String
-  toHaskell macroDefinition =
-    delimit Newlines $
-    (macroDefinition ^. name <> " :: [AST.Expression] -> IO [AST.Expression]") :
-    map
-      (functionDefinitionToHaskell $ macroDefinition ^. name)
-      (macroDefinition ^. definitions)
+  toHaskell macroDefinition = toHaskell (macroDefinition ^. functionDefinition)
 
 instance ToHaskell QualifiedImport where
   toHaskell :: QualifiedImport -> String
@@ -352,29 +341,51 @@
     case x of
       ECaseBlock caseBlock ->
         ECaseBlock $
-        caseBlock & expr %~ bottomUpFmap f & matches %~
-        map (bottomUpFmap f *** bottomUpFmap f)
-      EEmptySExpression -> f x
+        caseBlock & expr %~ bottomUpFmap f &
+        matches %~ map (bottomUpFmap f *** bottomUpFmap f)
+      EEmptySExpression -> x
       EFunctionApplication functionApplication ->
         EFunctionApplication $
-        functionApplication & function %~ bottomUpFmap f & arguments %~
-        map (bottomUpFmap f)
+        functionApplication & function %~ bottomUpFmap f &
+        arguments %~ map (bottomUpFmap f)
       EIdentifier _ -> x
       ELambda lambda ->
         ELambda $
         lambda & arguments %~ map (bottomUpFmap f) & body %~ bottomUpFmap f
       ELetBlock letBlock ->
         ELetBlock $
-        letBlock & bindings %~
-        map ((_1 %~ bottomUpFmap f) . (_2 %~ bottomUpFmap f)) &
-        body %~
-        bottomUpFmap f
+        letBlock & bindings %~ map (bottomUpFmap f *** bottomUpFmap f) &
+        body %~ bottomUpFmap f
       ELiteral literal ->
         case literal of
           LChar _ -> x
           LInt _ -> x
-          LList exprs -> ELiteral (LList $ map (bottomUpFmap f) exprs)
           LString _ -> x
+  topDownFmap :: (Expression -> Expression) -> Expression -> Expression
+  topDownFmap f x =
+    case f x of
+      ECaseBlock caseBlock ->
+        ECaseBlock $
+        caseBlock & expr %~ bottomUpFmap f &
+        matches %~ map (bottomUpFmap f *** bottomUpFmap f)
+      EEmptySExpression -> x
+      EFunctionApplication functionApplication ->
+        EFunctionApplication $
+        functionApplication & function %~ bottomUpFmap f &
+        arguments %~ map (bottomUpFmap f)
+      EIdentifier _ -> x
+      ELambda lambda ->
+        ELambda $
+        lambda & arguments %~ map (bottomUpFmap f) & body %~ bottomUpFmap f
+      ELetBlock letBlock ->
+        ELetBlock $
+        letBlock & bindings %~ map (bottomUpFmap f *** bottomUpFmap f) &
+        body %~ bottomUpFmap f
+      ELiteral literal ->
+        case literal of
+          LChar _ -> x
+          LInt _ -> x
+          LString _ -> x
   bottomUpTraverse ::
        (Monad m) => (Expression -> m Expression) -> Expression -> m Expression
   bottomUpTraverse f x =
@@ -408,42 +419,4 @@
         case literal of
           LChar _ -> pure x
           LInt _ -> pure x
-          LList exprs ->
-            ELiteral . LList <$> traverse (bottomUpTraverse f) exprs
           LString _ -> pure x
-
-extractNameFromDefinition :: Statement -> Maybe Identifier
-extractNameFromDefinition (SDataDeclaration dataDeclaration) =
-  Just $
-  case dataDeclaration ^. typeDefinition of
-    ProperType typeName -> typeName
-    TypeConstructor fnApp ->
-      case fnApp ^. function of
-        ELiteral (LString typeName) -> typeName
-        _ -> fatal "extractNameFromDefinition" "0001"
-extractNameFromDefinition (SFunctionDefinition functionDefinition) =
-  Just $ functionDefinition ^. name
-extractNameFromDefinition (SLanguagePragma _) = Nothing
-extractNameFromDefinition (SMacroDefinition _) = Nothing
-extractNameFromDefinition (SModuleDeclaration _) = Nothing
-extractNameFromDefinition (SQualifiedImport _) = Nothing
-extractNameFromDefinition (SRestrictedImport _) = Nothing
-extractNameFromDefinition (STopLevel _) = Nothing
-extractNameFromDefinition (STypeclassInstance typeclassInstance) =
-  case typeclassInstance ^. instanceName of
-    ELiteral (LString identifier) -> Just identifier
-    _ -> Nothing
-extractNameFromDefinition (STypeSynonym typeSynonym) =
-  case typeSynonym ^. alias of
-    ELiteral (LString identifier) -> Just identifier
-    _ -> Nothing
-extractNameFromDefinition (SUnrestrictedImport _) = Nothing
-
-removeDefinitionsByName :: [String] -> [Statement] -> [Statement]
-removeDefinitionsByName namesToRemove =
-  filter
-    (\statement ->
-       not $
-       case extractNameFromDefinition statement of
-         Just definitionName -> definitionName `elem` namesToRemove
-         Nothing -> False)
diff --git a/src/Axel/Denormalize.hs b/src/Axel/Denormalize.hs
--- a/src/Axel/Denormalize.hs
+++ b/src/Axel/Denormalize.hs
@@ -1,15 +1,14 @@
 module Axel.Denormalize where
 
 import Axel.AST
-  ( ArgumentList(ArgumentList)
-  , Expression(ECaseBlock, EEmptySExpression, EFunctionApplication,
+  ( Expression(ECaseBlock, EEmptySExpression, EFunctionApplication,
            EIdentifier, ELambda, ELetBlock, ELiteral)
   , Import(ImportItem, ImportType)
   , ImportSpecification(ImportAll, ImportOnly)
-  , Literal(LChar, LInt, LList, LString)
-  , Statement(SDataDeclaration, SFunctionDefinition, SLanguagePragma,
-          SMacroDefinition, SModuleDeclaration, SQualifiedImport,
-          SRestrictedImport, STopLevel, STypeSynonym, STypeclassInstance,
+  , Literal(LChar, LInt, LString)
+  , Statement(SDataDeclaration, SFunctionDefinition, SMacroDefinition,
+          SModuleDeclaration, SPragma, SQualifiedImport, SRestrictedImport,
+          STopLevel, STypeSignature, STypeSynonym, STypeclassInstance,
           SUnrestrictedImport)
   , TopLevel(TopLevel)
   , TypeDefinition(ProperType, TypeConstructor)
@@ -22,14 +21,14 @@
   , definitions
   , expr
   , function
+  , functionDefinition
   , imports
   , instanceName
-  , language
   , matches
   , moduleName
   , name
+  , pragmaSpecification
   , typeDefinition
-  , typeSignature
   )
 import qualified Axel.Parse as Parse
   ( Expression(LiteralChar, LiteralInt, LiteralString, SExpression,
@@ -46,23 +45,22 @@
              Parse.SExpression
                [denormalizeExpression pat, denormalizeExpression res])
           (caseBlock ^. matches)
-  in Parse.SExpression $
-     Parse.Symbol "case" :
-     denormalizeExpression (caseBlock ^. expr) : denormalizedCases
+   in Parse.SExpression $ Parse.Symbol "case" :
+      denormalizeExpression (caseBlock ^. expr) :
+      denormalizedCases
 denormalizeExpression EEmptySExpression = Parse.SExpression []
 denormalizeExpression (EFunctionApplication functionApplication) =
-  Parse.SExpression $
-  denormalizeExpression (functionApplication ^. function) :
+  Parse.SExpression $ denormalizeExpression (functionApplication ^. function) :
   map denormalizeExpression (functionApplication ^. arguments)
 denormalizeExpression (EIdentifier x) = Parse.Symbol x
 denormalizeExpression (ELambda lambda) =
   let denormalizedArguments =
         Parse.SExpression $ map denormalizeExpression (lambda ^. arguments)
-  in Parse.SExpression
-       [ Parse.Symbol "fn"
-       , denormalizedArguments
-       , denormalizeExpression (lambda ^. body)
-       ]
+   in Parse.SExpression
+        [ Parse.Symbol "\\"
+        , denormalizedArguments
+        , denormalizeExpression (lambda ^. body)
+        ]
 denormalizeExpression (ELetBlock letBlock) =
   let denormalizedBindings =
         Parse.SExpression $
@@ -71,26 +69,17 @@
              Parse.SExpression
                [denormalizeExpression var, denormalizeExpression val])
           (letBlock ^. bindings)
-  in Parse.SExpression
-       [ Parse.Symbol "let"
-       , denormalizedBindings
-       , denormalizeExpression (letBlock ^. body)
-       ]
+   in Parse.SExpression
+        [ Parse.Symbol "let"
+        , denormalizedBindings
+        , denormalizeExpression (letBlock ^. body)
+        ]
 denormalizeExpression (ELiteral x) =
   case x of
     LChar char -> Parse.LiteralChar char
     LInt int -> Parse.LiteralInt int
-    LList list ->
-      Parse.SExpression $ Parse.Symbol "list" : map denormalizeExpression list
     LString string -> Parse.LiteralString string
 
-denormalizeBinding :: (ArgumentList, Expression) -> Parse.Expression
-denormalizeBinding (ArgumentList argumentList, expression) =
-  Parse.SExpression
-    [ Parse.SExpression $ map denormalizeExpression argumentList
-    , denormalizeExpression expression
-    ]
-
 denormalizeImportSpecification :: ImportSpecification -> Parse.Expression
 denormalizeImportSpecification ImportAll = Parse.Symbol "all"
 denormalizeImportSpecification (ImportOnly importList) =
@@ -107,29 +96,29 @@
           TypeConstructor typeConstructor ->
             denormalizeExpression $ EFunctionApplication typeConstructor
           ProperType properType -> Parse.Symbol properType
-  in Parse.SExpression
-       [ Parse.Symbol "data"
-       , denormalizedTypeDefinition
-       , Parse.SExpression $
+   in Parse.SExpression
+        (Parse.Symbol "data" : denormalizedTypeDefinition :
          map
            (denormalizeExpression . EFunctionApplication)
-           (dataDeclaration ^. constructors)
-       ]
-denormalizeStatement (SFunctionDefinition functionDefinition) =
-  Parse.SExpression $
-  Parse.Symbol "=" :
-  Parse.Symbol (functionDefinition ^. name) :
-  denormalizeExpression
-    (EFunctionApplication (functionDefinition ^. typeSignature)) :
-  map denormalizeBinding (functionDefinition ^. definitions)
-denormalizeStatement (SLanguagePragma languagePragma) =
+           (dataDeclaration ^. constructors))
+denormalizeStatement (SFunctionDefinition fnDef) =
   Parse.SExpression
-    [Parse.Symbol "language", Parse.Symbol $ languagePragma ^. language]
-denormalizeStatement (SMacroDefinition macroDefinition) =
-  Parse.SExpression $
-  Parse.Symbol "defmacro" :
-  Parse.Symbol (macroDefinition ^. name) :
-  map denormalizeBinding (macroDefinition ^. definitions)
+    [ Parse.Symbol "="
+    , Parse.Symbol (fnDef ^. name)
+    , Parse.SExpression (map denormalizeExpression (fnDef ^. arguments))
+    , denormalizeExpression (fnDef ^. body)
+    ]
+denormalizeStatement (SPragma pragma) =
+  Parse.SExpression
+    [Parse.Symbol "pragma", Parse.LiteralString (pragma ^. pragmaSpecification)]
+denormalizeStatement (SMacroDefinition macroDef) =
+  Parse.SExpression
+    [ Parse.Symbol "macro"
+    , Parse.Symbol (macroDef ^. functionDefinition . name)
+    , Parse.SExpression
+        (map denormalizeExpression (macroDef ^. functionDefinition . arguments))
+    , denormalizeExpression (macroDef ^. functionDefinition . body)
+    ]
 denormalizeStatement (SModuleDeclaration identifier) =
   Parse.SExpression [Parse.Symbol "module", Parse.Symbol identifier]
 denormalizeStatement (SQualifiedImport qualifiedImport) =
@@ -149,12 +138,16 @@
   Parse.SExpression $ Parse.Symbol "begin" : map denormalizeStatement statements
 denormalizeStatement (STypeclassInstance typeclassInstance) =
   Parse.SExpression
-    [ Parse.Symbol "instance"
-    , denormalizeExpression (typeclassInstance ^. instanceName)
-    , Parse.SExpression $
-      map
-        (denormalizeStatement . SFunctionDefinition)
-        (typeclassInstance ^. definitions)
+    (Parse.Symbol "instance" :
+     denormalizeExpression (typeclassInstance ^. instanceName) :
+     map
+       (denormalizeStatement . SFunctionDefinition)
+       (typeclassInstance ^. definitions))
+denormalizeStatement (STypeSignature typeSig) =
+  Parse.SExpression
+    [ Parse.Symbol "::"
+    , Parse.Symbol (typeSig ^. name)
+    , denormalizeExpression (typeSig ^. typeDefinition)
     ]
 denormalizeStatement (STypeSynonym typeSynonym) =
   Parse.SExpression
diff --git a/src/Axel/Entry.hs b/src/Axel/Entry.hs
deleted file mode 100644
--- a/src/Axel/Entry.hs
+++ /dev/null
@@ -1,85 +0,0 @@
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-module Axel.Entry where
-
-import Axel.AST (ToHaskell(toHaskell))
-import Axel.Error (Error)
-import Axel.GHC (runWithGHC)
-import Axel.Macros (exhaustivelyExpandMacros, stripMacroDefinitions)
-import Axel.Normalize (normalizeStatement)
-import Axel.Parse (Expression(Symbol), parseSource)
-import Axel.Utils.Directory (withTempDirectory)
-import Axel.Utils.Recursion (Recursive(bottomUpFmap))
-import Axel.Utils.Resources (readResource)
-import qualified Axel.Utils.Resources as Res (astDefinition)
-
-import Control.Lens.Operators ((.~))
-import Control.Monad.Except (MonadError, runExceptT, throwError)
-import Control.Monad.IO.Class (MonadIO)
-import Control.Monad.Trans.Control (MonadBaseControl)
-
-import Data.Maybe (fromMaybe)
-import Data.Semigroup ((<>))
-import qualified Data.Text as T (isSuffixOf, pack)
-
-import System.FilePath ((</>), stripExtension)
-import System.FilePath.Lens (directory)
-import qualified System.IO.Strict as S (readFile)
-
-convertList :: Expression -> Expression
-convertList =
-  bottomUpFmap $ \case
-    Symbol "List" -> Symbol "[]"
-    x -> x
-
-convertUnit :: Expression -> Expression
-convertUnit =
-  bottomUpFmap $ \case
-    Symbol "Unit" -> Symbol "()"
-    Symbol "unit" -> Symbol "()"
-    x -> x
-
-transpileSource ::
-     (MonadBaseControl IO m, MonadError Error m, MonadIO m)
-  => String
-  -> m String
-transpileSource source =
-  toHaskell . stripMacroDefinitions <$>
-  (parseSource source >>= exhaustivelyExpandMacros . convertList . convertUnit >>=
-   normalizeStatement)
-
-axelPathToHaskellPath :: FilePath -> FilePath
-axelPathToHaskellPath axelPath =
-  let basePath =
-        if ".axel" `T.isSuffixOf` T.pack axelPath
-          then fromMaybe axelPath $ stripExtension ".axel" axelPath
-          else axelPath
-  in basePath <> ".hs"
-
--- TODO Switch this to `(MonadError Error m, MonadIO m)` and do the error check in `evalFile`.
-transpileFile :: FilePath -> FilePath -> IO ()
-transpileFile path newPath = do
-  fileContents <- S.readFile path
-  result <- runExceptT $ transpileSource fileContents
-  case result of
-    Left err -> throwError $ userError $ show err
-    Right newContents -> writeFile newPath newContents
-
--- Transpile a file in place.
-transpileFile' :: FilePath -> IO FilePath
-transpileFile' path = do
-  let newPath = axelPathToHaskellPath path
-  transpileFile path newPath
-  pure newPath
-
-evalFile :: FilePath -> IO ()
-evalFile path =
-  withTempDirectory $ \tempDirectoryPath -> do
-    let astDefinitionPath = tempDirectoryPath </> "Axel.hs"
-    readResource Res.astDefinition >>= writeFile astDefinitionPath
-    let newPath = directory .~ tempDirectoryPath $ axelPathToHaskellPath path
-    transpileFile path newPath
-    evalResult <- runExceptT $ runWithGHC newPath
-    either (throwError . userError . show) putStr evalResult
diff --git a/src/Axel/Error.hs b/src/Axel/Error.hs
--- a/src/Axel/Error.hs
+++ b/src/Axel/Error.hs
@@ -1,26 +1,45 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE InstanceSigs #-}
-{-# LANGUAGE GADTs #-}
+{-# LANGUAGE ScopedTypeVariables #-}
 
 module Axel.Error where
 
 import Axel.Parse.AST (Expression, toAxel)
 
-import Data.Semigroup ((<>))
+import Control.Monad.Except (ExceptT, MonadError(throwError), runExceptT)
 
-import Text.Parsec (ParseError)
+import Data.Semigroup ((<>))
 
 data Error
-  = MacroError String
+  = EvalError String
+  | MacroError String
   | NormalizeError String
                    [Expression]
-  | ParseError ParseError
+  | ParseError String
+  | ProjectError String
 
 instance Show Error where
   show :: Error -> String
+  show (EvalError err) = err
   show (MacroError err) = err
   show (NormalizeError err context) =
     "error:\n" <> err <> "\n\n" <> "context:\n" <> unlines (map toAxel context)
-  show (ParseError err) = show err
+  show (ParseError err) = err
+  show (ProjectError err) = err
 
 fatal :: String -> String -> a
 fatal context message = error $ "[FATAL] " <> context <> " - " <> message
+
+toIO :: (MonadError IOError m) => ExceptT Error m a -> m a
+toIO f = do
+  result :: Either Error a <- runExceptT f
+  case result of
+    Left err -> throwError $ userError $ show err
+    Right x -> pure x
+
+mapError :: (MonadError e' m) => ExceptT e m a -> (e -> e') -> m a
+x `mapError` f =
+  runExceptT x >>= \case
+    Left err -> throwError (f err)
+    Right res -> pure res
diff --git a/src/Axel/Eval.hs b/src/Axel/Eval.hs
deleted file mode 100644
--- a/src/Axel/Eval.hs
+++ /dev/null
@@ -1,44 +0,0 @@
-{-# LANGUAGE FlexibleContexts #-}
-
-module Axel.Eval where
-
-import Axel.Error (Error(MacroError))
-import Axel.GHC (buildWithGHC, runWithGHC)
-import Axel.Utils.Directory (withCurrentDirectoryLifted, withTempDirectory)
-
-import Control.Monad.Except (MonadError, throwError)
-import Control.Monad.IO.Class (MonadIO, liftIO)
-import Control.Monad.Trans.Control (MonadBaseControl)
-
-import System.Directory (createDirectoryIfMissing)
-import System.FilePath ((</>))
-import System.IO (writeFile)
-
-execInterpreter :: (MonadError Error m, MonadIO m) => FilePath -> m String
-execInterpreter scaffoldFilePath = do
-  debugResult <- liftIO $ buildWithGHC scaffoldFilePath
-  case debugResult of
-    Right _ -> runWithGHC scaffoldFilePath
-    Left stderr -> throwError $ MacroError stderr
-
-evalMacro ::
-     (MonadBaseControl IO m, MonadError Error m, MonadIO m)
-  => String
-  -> String
-  -> String
-  -> m String
-evalMacro astDefinition scaffold macroDefinitionAndEnvironment =
-  withTempDirectory $ \directoryName ->
-    withCurrentDirectoryLifted directoryName $ do
-      let astDirectoryPath = "Axel" </> "Parse"
-      let macroDefinitionAndEnvironmentFileName =
-            "MacroDefinitionAndEnvironment.hs"
-      let scaffoldFileName = "Scaffold.hs"
-      liftIO $ createDirectoryIfMissing True astDirectoryPath
-      liftIO $ writeFile (astDirectoryPath </> "AST.hs") astDefinition
-      liftIO $
-        writeFile
-          macroDefinitionAndEnvironmentFileName
-          macroDefinitionAndEnvironment
-      liftIO $ writeFile scaffoldFileName scaffold
-      execInterpreter scaffoldFileName
diff --git a/src/Axel/GHC.hs b/src/Axel/GHC.hs
deleted file mode 100644
--- a/src/Axel/GHC.hs
+++ /dev/null
@@ -1,35 +0,0 @@
-{-# OPTIONS_GHC -fno-warn-orphans #-}
-{-# LANGUAGE FlexibleContexts #-}
-
-module Axel.GHC where
-
-import Axel.Error (Error(MacroError))
-
-import Control.Monad.Except (MonadError, throwError)
-import Control.Monad.IO.Class (MonadIO, liftIO)
-
-import System.Exit (ExitCode(ExitFailure, ExitSuccess))
-import System.Process (readProcessWithExitCode)
-
--- TODO Rename to `ghcCompile`
-buildWithGHC :: (MonadIO m) => FilePath -> m (Either String String)
-buildWithGHC filePath = do
-  (exitCode, stdout, stderr) <-
-    liftIO $
-    readProcessWithExitCode
-      "stack"
-      ["--resolver", "lts-12.0", "ghc", "--", "-v0", "-ddump-json", filePath]
-      ""
-  pure $
-    case exitCode of
-      ExitSuccess -> Right stdout
-      ExitFailure _ -> Left stderr
-
--- TODO Rename to `ghcInterpret`
-runWithGHC :: (MonadError Error m, MonadIO m) => FilePath -> m String
-runWithGHC filePath = do
-  (exitCode, stdout, stderr) <-
-    liftIO $ readProcessWithExitCode "stack" ["runghc", filePath] ""
-  case exitCode of
-    ExitSuccess -> pure stdout
-    ExitFailure _ -> throwError $ MacroError stderr
diff --git a/src/Axel/Haskell/File.hs b/src/Axel/Haskell/File.hs
new file mode 100644
--- /dev/null
+++ b/src/Axel/Haskell/File.hs
@@ -0,0 +1,106 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Axel.Haskell.File where
+
+import Prelude hiding (putStr, putStrLn)
+
+import Axel.AST (ToHaskell(toHaskell))
+import Axel.Error (Error(EvalError), mapError)
+import Axel.Haskell.GHC (ghcInterpret)
+import Axel.Haskell.Prettify (prettifyHaskell)
+import Axel.Macros (exhaustivelyExpandMacros, stripMacroDefinitions)
+import Axel.Monad.Console (MonadConsole(putStr), putStrLn)
+import Axel.Monad.FileSystem (MonadFileSystem)
+import qualified Axel.Monad.FileSystem as FS
+  ( MonadFileSystem(readFile, writeFile)
+  , withTemporaryDirectory
+  )
+import Axel.Monad.Process (MonadProcess)
+import Axel.Monad.Resource (MonadResource, readResource)
+import qualified Axel.Monad.Resource as Res (astDefinition)
+import Axel.Normalize (normalizeStatement)
+import Axel.Parse (Expression(Symbol), parseSource)
+import Axel.Utils.Recursion (Recursive(bottomUpFmap))
+
+import Control.Lens.Operators ((.~))
+import Control.Monad.Except (MonadError)
+
+import Data.Maybe (fromMaybe)
+import Data.Semigroup ((<>))
+import qualified Data.Text as T (isSuffixOf, pack)
+
+import System.FilePath ((</>), stripExtension, takeFileName)
+import System.FilePath.Lens (directory)
+
+convertList :: Expression -> Expression
+convertList =
+  bottomUpFmap $ \case
+    Symbol "List" -> Symbol "[]"
+    x -> x
+
+convertUnit :: Expression -> Expression
+convertUnit =
+  bottomUpFmap $ \case
+    Symbol "Unit" -> Symbol "()"
+    Symbol "unit" -> Symbol "()"
+    x -> x
+
+transpileSource ::
+     (MonadError Error m, MonadFileSystem m, MonadProcess m, MonadResource m)
+  => String
+  -> m String
+transpileSource source =
+  prettifyHaskell . toHaskell . stripMacroDefinitions <$>
+  (parseSource source >>= exhaustivelyExpandMacros . convertList . convertUnit >>=
+   normalizeStatement)
+
+axelPathToHaskellPath :: FilePath -> FilePath
+axelPathToHaskellPath axelPath =
+  let basePath =
+        if ".axel" `T.isSuffixOf` T.pack axelPath
+          then fromMaybe axelPath $ stripExtension ".axel" axelPath
+          else axelPath
+   in basePath <> ".hs"
+
+transpileFile ::
+     (MonadError Error m, MonadFileSystem m, MonadProcess m, MonadResource m)
+  => FilePath
+  -> FilePath
+  -> m ()
+transpileFile path newPath = do
+  fileContents <- FS.readFile path
+  newContents <- transpileSource fileContents
+  FS.writeFile newPath newContents
+
+-- | Transpile a file in place.
+transpileFile' ::
+     (MonadError Error m, MonadFileSystem m, MonadProcess m, MonadResource m)
+  => FilePath
+  -> m FilePath
+transpileFile' path = do
+  let newPath = axelPathToHaskellPath path
+  transpileFile path newPath
+  pure newPath
+
+evalFile ::
+     ( MonadConsole m
+     , MonadError Error m
+     , MonadFileSystem m
+     , MonadProcess m
+     , MonadResource m
+     )
+  => FilePath
+  -> m ()
+evalFile path = do
+  putStrLn ("Building " <> takeFileName path <> "...")
+  FS.withTemporaryDirectory $ \tempDirectoryPath -> do
+    let astDefinitionPath = tempDirectoryPath </> "Axel.hs"
+    readResource Res.astDefinition >>= FS.writeFile astDefinitionPath
+    let newPath = directory .~ tempDirectoryPath $ axelPathToHaskellPath path
+    transpileFile path newPath
+    putStrLn ("Running " <> takeFileName path <> "...")
+    output <- ghcInterpret newPath `mapError` EvalError
+    putStr output
diff --git a/src/Axel/Haskell/GHC.hs b/src/Axel/Haskell/GHC.hs
new file mode 100644
--- /dev/null
+++ b/src/Axel/Haskell/GHC.hs
@@ -0,0 +1,50 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE InstanceSigs #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Axel.Haskell.GHC where
+
+import Axel.Haskell.Stack (axelStackageId, stackageResolverWithAxel)
+import Axel.Monad.Process (MonadProcess(runProcess))
+
+import Control.Monad.Except (MonadError, throwError)
+
+import System.Exit (ExitCode(ExitFailure, ExitSuccess))
+
+ghcCompile :: (MonadError String m, MonadProcess m) => FilePath -> m String
+ghcCompile filePath = do
+  (exitCode, stdout, stderr) <-
+    runProcess
+      "stack"
+      [ "--resolver"
+      , stackageResolverWithAxel
+      , "ghc"
+      , "--"
+      , "-v0"
+      , "-ddump-json"
+      , filePath
+      ]
+      ""
+  case exitCode of
+    ExitSuccess -> pure stdout
+    ExitFailure _ -> throwError stderr
+
+ghcInterpret :: (MonadError String m, MonadProcess m) => FilePath -> m String
+ghcInterpret filePath = do
+  (exitCode, stdout, stderr) <-
+    runProcess
+      "stack"
+      [ "--resolver"
+      , stackageResolverWithAxel
+      , "runghc"
+      , "--package"
+      , axelStackageId
+      , "--"
+      , filePath
+      ]
+      ""
+  case exitCode of
+    ExitSuccess -> pure stdout
+    ExitFailure _ -> throwError stderr
diff --git a/src/Axel/Haskell/Prettify.hs b/src/Axel/Haskell/Prettify.hs
new file mode 100644
--- /dev/null
+++ b/src/Axel/Haskell/Prettify.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Axel.Haskell.Prettify where
+
+import Language.Haskell.Exts.Parser (ParseResult(ParseFailed, ParseOk), parse)
+import Language.Haskell.Exts.Pretty (prettyPrint)
+import Language.Haskell.Exts.SrcLoc (SrcSpanInfo)
+import Language.Haskell.Exts.Syntax (Module)
+
+prettifyHaskell :: String -> String
+prettifyHaskell input =
+  case parse input of
+    ParseOk (ast :: Module SrcSpanInfo) -> prettyPrint ast
+    ParseFailed _ _ -> input
diff --git a/src/Axel/Haskell/Project.hs b/src/Axel/Haskell/Project.hs
new file mode 100644
--- /dev/null
+++ b/src/Axel/Haskell/Project.hs
@@ -0,0 +1,75 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE InstanceSigs #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Axel.Haskell.Project where
+
+import Axel.Error (Error)
+import Axel.Haskell.File (transpileFile')
+import Axel.Haskell.Stack
+  ( addStackDependency
+  , axelStackageSpecifier
+  , buildStackProject
+  , createStackProject
+  , runStackProject
+  )
+import Axel.Monad.Console (MonadConsole)
+import Axel.Monad.FileSystem
+  ( MonadFileSystem(copyFile, getCurrentDirectory, removeFile)
+  , getDirectoryContentsRec
+  )
+import Axel.Monad.Process (MonadProcess)
+import Axel.Monad.Resource (MonadResource(getResourcePath), newProjectTemplate)
+
+import Control.Monad.Except (MonadError)
+
+import Data.Semigroup ((<>))
+import qualified Data.Text as T (isSuffixOf, pack)
+
+import System.FilePath ((</>))
+
+type ProjectPath = FilePath
+
+newProject ::
+     (MonadFileSystem m, MonadProcess m, MonadResource m) => String -> m ()
+newProject projectName = do
+  createStackProject projectName
+  addStackDependency axelStackageSpecifier projectName
+  templatePath <- getResourcePath newProjectTemplate
+  let copyAxel filePath = do
+        copyFile
+          (templatePath </> filePath <> ".axel")
+          (projectName </> filePath <> ".axel")
+        removeFile (projectName </> filePath <> ".hs")
+  mapM_ copyAxel ["Setup", "app" </> "Main", "src" </> "Lib", "test" </> "Spec"]
+
+transpileProject ::
+     (MonadError Error m, MonadFileSystem m, MonadProcess m, MonadResource m)
+  => m [FilePath]
+transpileProject = do
+  files <- getDirectoryContentsRec "."
+  let axelFiles =
+        filter (\filePath -> ".axel" `T.isSuffixOf` T.pack filePath) files
+  mapM transpileFile' axelFiles
+
+buildProject ::
+     ( MonadConsole m
+     , MonadError Error m
+     , MonadFileSystem m
+     , MonadProcess m
+     , MonadResource m
+     )
+  => m ()
+buildProject = do
+  projectPath <- getCurrentDirectory
+  hsPaths <- transpileProject
+  buildStackProject projectPath
+  mapM_ removeFile hsPaths
+
+runProject ::
+     (MonadConsole m, MonadError Error m, MonadFileSystem m, MonadProcess m)
+  => m ()
+runProject = getCurrentDirectory >>= runStackProject
diff --git a/src/Axel/Haskell/Stack.hs b/src/Axel/Haskell/Stack.hs
new file mode 100644
--- /dev/null
+++ b/src/Axel/Haskell/Stack.hs
@@ -0,0 +1,137 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE InstanceSigs #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Axel.Haskell.Stack where
+
+import Prelude hiding (putStrLn)
+
+import Axel.Error (Error(ProjectError), fatal)
+import Axel.Monad.Console (MonadConsole, putStrLn)
+import Axel.Monad.FileSystem (MonadFileSystem)
+import qualified Axel.Monad.FileSystem as FS
+  ( MonadFileSystem(readFile, writeFile)
+  , withCurrentDirectory
+  )
+import Axel.Monad.Process
+  ( MonadProcess(runProcess, runProcessInheritingStreams)
+  )
+
+import Control.Lens.Operators ((%~))
+import Control.Monad (void)
+import Control.Monad.Except (MonadError, throwError)
+
+import Data.Aeson.Lens (_Array, key)
+import qualified Data.ByteString.Char8 as B (pack, unpack)
+import Data.Function ((&))
+import Data.List (foldl')
+import qualified Data.Text as T (pack)
+import Data.Vector (cons)
+import Data.Version (showVersion)
+import qualified Data.Yaml as Yaml (Value(String), decodeEither', encode)
+
+import Paths_axel (version)
+
+import System.Exit (ExitCode(ExitFailure, ExitSuccess))
+import System.FilePath (takeFileName)
+
+import Text.Regex.PCRE ((=~), getAllTextSubmatches)
+
+type ProjectPath = FilePath
+
+type StackageId = String
+
+type StackageResolver = String
+
+type Target = String
+
+type Version = String
+
+stackageResolverWithAxel :: StackageResolver
+stackageResolverWithAxel = "nightly-2018-08-20"
+
+axelStackageVersion :: Version
+axelStackageVersion = showVersion version
+
+axelStackageId :: StackageId
+axelStackageId = "axel-" <> showVersion version
+
+axelStackageSpecifier :: StackageId
+axelStackageSpecifier = "axel ==" <> axelStackageVersion
+
+getStackProjectTargets ::
+     (Monad m, MonadFileSystem m, MonadProcess m) => ProjectPath -> m [Target]
+getStackProjectTargets projectPath =
+  FS.withCurrentDirectory projectPath $ do
+    (_, _, stderr) <- runProcess "stack" ["ide", "targets"] ""
+    pure $ lines stderr
+
+addStackDependency :: (MonadFileSystem m) => StackageId -> ProjectPath -> m ()
+addStackDependency dependencyId projectPath =
+  FS.withCurrentDirectory projectPath $ do
+    let packageConfigPath = "package.yaml"
+    packageConfigContents <- FS.readFile packageConfigPath
+    case Yaml.decodeEither' $ B.pack packageConfigContents of
+      Right contents ->
+        let newContents :: Yaml.Value =
+              contents & key "dependencies" . _Array %~
+              cons (Yaml.String $ T.pack dependencyId)
+            encodedContents = B.unpack $ Yaml.encode newContents
+         in FS.writeFile packageConfigPath encodedContents
+      Left _ -> fatal "addStackDependency" "0001"
+
+buildStackProject ::
+     (MonadConsole m, MonadError Error m, MonadFileSystem m, MonadProcess m)
+  => ProjectPath
+  -> m ()
+buildStackProject projectPath = do
+  putStrLn ("Building " <> takeFileName projectPath <> "...")
+  result <-
+    FS.withCurrentDirectory projectPath $ runProcess "stack" ["build"] ""
+  case result of
+    (ExitSuccess, _, _) -> pure ()
+    (ExitFailure _, stdout, stderr) ->
+      throwError $
+      ProjectError
+        ("Project failed to build.\n\nStdout:\n" <> stdout <> "\n\nStderr:\n" <>
+         stderr)
+
+createStackProject :: (MonadFileSystem m, MonadProcess m) => String -> m ()
+createStackProject projectName = do
+  void $ runProcess "stack" ["new", projectName, "new-template"] ""
+  setStackageResolver projectName stackageResolverWithAxel
+
+runStackProject ::
+     (MonadConsole m, MonadError Error m, MonadFileSystem m, MonadProcess m)
+  => ProjectPath
+  -> m ()
+runStackProject projectPath = do
+  targets <- getStackProjectTargets projectPath
+  case findExeTargets targets of
+    [target] -> do
+      putStrLn ("Running " <> target <> "...")
+      void $ runProcessInheritingStreams "stack" ["exec", target]
+    _ ->
+      throwError $ ProjectError "No executable target was unambiguously found!"
+  where
+    findExeTargets =
+      foldl'
+        (\acc target ->
+           case getAllTextSubmatches $ target =~
+                ("([^:]*):exe:([^:]*)" :: String) of
+             [_fullMatch, _projectName, targetName] -> targetName : acc
+             _ -> acc)
+        []
+
+setStackageResolver ::
+     (MonadFileSystem m, MonadProcess m)
+  => ProjectPath
+  -> StackageResolver
+  -> m ()
+setStackageResolver projectPath resolver =
+  void $ FS.withCurrentDirectory projectPath $
+  runProcess "stack" ["config", "set", "resolver", resolver] ""
diff --git a/src/Axel/Macros.hs b/src/Axel/Macros.hs
--- a/src/Axel/Macros.hs
+++ b/src/Axel/Macros.hs
@@ -6,22 +6,40 @@
 import Axel.AST
   ( Identifier
   , MacroDefinition
-  , Statement(SDataDeclaration, SFunctionDefinition, SLanguagePragma,
-          SMacroDefinition, SModuleDeclaration, SQualifiedImport,
-          SRestrictedImport, STopLevel, STypeSynonym, STypeclassInstance,
+  , Statement(SDataDeclaration, SFunctionDefinition, SMacroDefinition,
+          SModuleDeclaration, SPragma, SQualifiedImport, SRestrictedImport,
+          STopLevel, STypeSignature, STypeSynonym, STypeclassInstance,
           SUnrestrictedImport)
   , ToHaskell(toHaskell)
+  , functionDefinition
   , name
   , statements
   )
 import Axel.Denormalize (denormalizeStatement)
 import Axel.Error (Error(MacroError))
-import Axel.Eval (evalMacro)
+import Axel.Haskell.GHC (ghcInterpret)
+import Axel.Haskell.Prettify (prettifyHaskell)
+import Axel.Monad.FileSystem (MonadFileSystem)
+import qualified Axel.Monad.FileSystem as FS
+  ( MonadFileSystem(createDirectoryIfMissing, writeFile)
+  , withCurrentDirectory
+  , withTemporaryDirectory
+  )
+import Axel.Monad.Process (MonadProcess)
+import Axel.Monad.Resource (MonadResource, readResource)
+import qualified Axel.Monad.Resource as Res
+  ( astDefinition
+  , macroDefinitionAndEnvironmentFooter
+  , macroDefinitionAndEnvironmentHeader
+  , macroScaffold
+  )
 import Axel.Normalize (normalizeStatement)
 import qualified Axel.Parse as Parse
   ( Expression(LiteralChar, LiteralInt, LiteralString, SExpression,
            Symbol)
   , parseMultiple
+  , programToTopLevelExpressions
+  , topLevelExpressionsToProgram
   )
 import Axel.Utils.Display (Delimiter(Newlines), delimit, isOperator)
 import Axel.Utils.Function (uncurry3)
@@ -29,170 +47,179 @@
   ( Recursive(bottomUpFmap, bottomUpTraverse)
   , exhaustM
   )
-import Axel.Utils.Resources (readResource)
-import qualified Axel.Utils.Resources as Res
-  ( astDefinition
-  , macroDefinitionAndEnvironmentHeader
-  , macroScaffold
-  )
 import Axel.Utils.String (replace)
 
+import Control.Lens.Cons (snoc)
 import Control.Lens.Operators ((%~), (^.))
 import Control.Lens.Tuple (_1, _2)
 import Control.Monad (foldM)
-import Control.Monad.Except (MonadError, catchError, throwError)
-import Control.Monad.IO.Class (MonadIO, liftIO)
-import Control.Monad.Trans.Control (MonadBaseControl)
+import Control.Monad.Except (MonadError, catchError, runExceptT, throwError)
 
 import Data.Function ((&))
+import Data.List.NonEmpty (NonEmpty, nonEmpty)
+import qualified Data.List.NonEmpty as NE (head, toList)
 import Data.Semigroup ((<>))
 
+import System.FilePath ((</>))
+
 generateMacroProgram ::
-     (MonadBaseControl IO m, MonadError Error m, MonadIO m)
-  => MacroDefinition
+     (MonadError Error m, MonadFileSystem m, MonadResource m)
+  => NonEmpty MacroDefinition
   -> [Statement]
   -> [Parse.Expression]
   -> m (String, String, String)
-generateMacroProgram macroDefinition environment applicationArguments = do
-  astDefinition <- liftIO $ readResource Res.astDefinition
-  scaffold <- liftIO getScaffold
-  macroDefinitionAndEnvironment <-
-    (<>) <$> liftIO (readResource Res.macroDefinitionAndEnvironmentHeader) <*>
-    getMacroDefinitionAndEnvironmentFooter
-  pure (astDefinition, scaffold, macroDefinitionAndEnvironment)
+generateMacroProgram macroDefs env applicationArgs = do
+  astDef <- readResource Res.astDefinition
+  scaffold <- getScaffold
+  macroDefAndEnv <- (<>) <$> getMacroDefAndEnvHeader <*> getMacroDefAndEnvFooter
+  pure (astDef, scaffold, macroDefAndEnv)
   where
-    getMacroDefinitionAndEnvironmentFooter = do
-      hygenicMacroDefinition <-
-        replaceName
-          (macroDefinition ^. name)
-          newMacroName
-          (SMacroDefinition macroDefinition)
-      let source =
-            delimit Newlines $
-            map toHaskell (environment <> [hygenicMacroDefinition])
-      pure source
-    getScaffold =
-      let insertApplicationArguments =
-            let applicationArgumentsPlaceholder = "%%%ARGUMENTS%%%"
-            in replace
-                 applicationArgumentsPlaceholder
-                 (show applicationArguments)
-          insertDefinitionName =
-            let definitionNamePlaceholder = "%%%MACRO_NAME%%%"
-            in replace definitionNamePlaceholder newMacroName
-      in insertApplicationArguments . insertDefinitionName <$>
-         readResource Res.macroScaffold
+    insertDefName =
+      let defNamePlaceholder = "%%%MACRO_NAME%%%"
+       in replace defNamePlaceholder newMacroName
+    oldMacroName = NE.head macroDefs ^. functionDefinition . name
     newMacroName =
-      (macroDefinition ^. name) ++
-      if isOperator (macroDefinition ^. name)
+      oldMacroName <>
+      if isOperator oldMacroName
         then "%%%%%%%%%%"
         else "_AXEL_AUTOGENERATED_MACRO_DEFINITION"
+    getMacroDefAndEnvHeader =
+      insertDefName <$> readResource Res.macroDefinitionAndEnvironmentHeader
+    getMacroDefAndEnvFooter = do
+      hygenicMacroDefs <-
+        traverse
+          (replaceName oldMacroName newMacroName . SMacroDefinition)
+          macroDefs
+      let source =
+            prettifyHaskell $ delimit Newlines $
+            map toHaskell (env <> NE.toList hygenicMacroDefs)
+      footer <-
+        insertDefName <$> readResource Res.macroDefinitionAndEnvironmentFooter
+      pure (unlines [source, footer])
+    getScaffold :: (Monad m, MonadFileSystem m, MonadResource m) => m String
+    getScaffold =
+      let insertApplicationArgs =
+            let applicationArgsPlaceholder = "%%%ARGUMENTS%%%"
+             in replace applicationArgsPlaceholder (show applicationArgs)
+       in prettifyHaskell . insertApplicationArgs . insertDefName <$>
+          readResource Res.macroScaffold
 
 expansionPass ::
-     (MonadBaseControl IO m, MonadError Error m, MonadIO m)
+     (MonadError Error m, MonadFileSystem m, MonadProcess m, MonadResource m)
   => Parse.Expression
   -> m Parse.Expression
 expansionPass programExpr =
-  stmtExprsToProgram . map denormalizeStatement <$>
-  expandMacros (programToTopLevelExprs programExpr)
-  where
-    programToTopLevelExprs :: Parse.Expression -> [Parse.Expression]
-    programToTopLevelExprs (Parse.SExpression (Parse.Symbol "begin":stmts)) =
-      stmts
-    programToTopLevelExprs _ =
-      error "programToTopLevelExprs must be passed a top-level program!"
-    stmtExprsToProgram :: [Parse.Expression] -> Parse.Expression
-    stmtExprsToProgram stmts = Parse.SExpression (Parse.Symbol "begin" : stmts)
+  Parse.topLevelExpressionsToProgram . map denormalizeStatement <$>
+  expandMacros (Parse.programToTopLevelExpressions programExpr)
 
+programToTopLevelExpressions :: Parse.Expression -> [Parse.Expression]
+programToTopLevelExpressions (Parse.SExpression (Parse.Symbol "begin":stmts)) =
+  stmts
+programToTopLevelExpressions _ =
+  error "programToTopLevelExpressions must be passed a top-level program!"
+
+topLevelExpressionsToProgram :: [Parse.Expression] -> Parse.Expression
+topLevelExpressionsToProgram stmts =
+  Parse.SExpression (Parse.Symbol "begin" : stmts)
+
 exhaustivelyExpandMacros ::
-     (MonadBaseControl IO m, MonadError Error m, MonadIO m)
+     (MonadError Error m, MonadFileSystem m, MonadProcess m, MonadResource m)
   => Parse.Expression
   -> m Parse.Expression
 exhaustivelyExpandMacros = exhaustM expansionPass
 
--- TODO This needs heavy optimization.
+isStatementNonconflicting :: Statement -> Bool
+isStatementNonconflicting (SDataDeclaration _) = True
+isStatementNonconflicting (SFunctionDefinition _) = True
+isStatementNonconflicting (SPragma _) = True
+isStatementNonconflicting (SMacroDefinition _) = True
+isStatementNonconflicting (SModuleDeclaration _) = False
+isStatementNonconflicting (SQualifiedImport _) = True
+isStatementNonconflicting (SRestrictedImport _) = True
+isStatementNonconflicting (STopLevel _) = False
+isStatementNonconflicting (STypeclassInstance _) = True
+isStatementNonconflicting (STypeSignature _) = True
+isStatementNonconflicting (STypeSynonym _) = True
+isStatementNonconflicting (SUnrestrictedImport _) = True
+
 expandMacros ::
-     (MonadBaseControl IO m, MonadError Error m, MonadIO m)
+     (MonadError Error m, MonadFileSystem m, MonadProcess m, MonadResource m)
   => [Parse.Expression]
   -> m [Statement]
 expandMacros topLevelExprs =
   fst <$>
   foldM
     (\acc@(stmts, macroDefs) expr -> do
-       expandedExpr <- fullyExpandExpr stmts macroDefs expr
-       stmt <- normalizeStatement expandedExpr
-       pure $
-         case stmt of
-           SMacroDefinition macroDefinition ->
-             acc & _2 %~ (<> [macroDefinition])
-           _ ->
-             if isStmtNonconflicting stmt
-               then acc & _1 %~ (<> [stmt])
-               else acc)
+       expandedExprs <- fullyExpandExpr stmts macroDefs expr
+       foldM
+         (\acc' expandedExpr -> do
+            stmt <- normalizeStatement expandedExpr
+            pure $ acc' &
+              case stmt of
+                SMacroDefinition macroDefinition ->
+                  _2 %~ flip snoc macroDefinition
+                _ -> _1 %~ flip snoc stmt)
+         acc
+         expandedExprs)
     ([], [])
     topLevelExprs
   where
-    isStmtNonconflicting =
-      \case
-        SDataDeclaration _ -> True
-        SFunctionDefinition _ -> True
-        SLanguagePragma _ -> True
-        SMacroDefinition _ -> True
-        SModuleDeclaration _ -> False
-        SQualifiedImport _ -> True
-        SRestrictedImport _ -> True
-        STopLevel _ -> False
-        STypeclassInstance _ -> True
-        STypeSynonym _ -> True
-        SUnrestrictedImport _ -> True
-    fullyExpandExpr stmts macroDefs =
-      exhaustM $
-      bottomUpTraverse
-        (\case
-           Parse.SExpression xs ->
-             Parse.SExpression <$>
-             foldM
-               (\acc x ->
-                  case x of
-                    Parse.LiteralChar _ -> pure $ acc ++ [x]
-                    Parse.LiteralInt _ -> pure $ acc ++ [x]
-                    Parse.LiteralString _ -> pure $ acc ++ [x]
-                    Parse.SExpression [] -> pure $ acc ++ [x]
-                    Parse.SExpression (function:args) ->
-                      lookupMacroDefinition macroDefs function >>= \case
-                        Just macroDefinition ->
-                          (acc ++) <$>
-                          expandMacroApplication macroDefinition stmts args
-                        Nothing -> pure $ acc ++ [x]
-                    Parse.Symbol _ -> pure $ acc ++ [x])
-               []
-               xs
-           expr -> pure expr)
+    fullyExpandExpr ::
+         ( MonadError Error m
+         , MonadFileSystem m
+         , MonadProcess m
+         , MonadResource m
+         )
+      => [Statement]
+      -> [MacroDefinition]
+      -> Parse.Expression
+      -> m [Parse.Expression]
+    fullyExpandExpr stmts allMacroDefs expr = do
+      let program = Parse.topLevelExpressionsToProgram [expr]
+      expandedExpr <-
+        exhaustM
+          (bottomUpTraverse
+             (\case
+                Parse.SExpression xs ->
+                  Parse.SExpression <$>
+                  foldM
+                    (\acc x ->
+                       case x of
+                         Parse.LiteralChar _ -> pure $ acc ++ [x]
+                         Parse.LiteralInt _ -> pure $ acc ++ [x]
+                         Parse.LiteralString _ -> pure $ acc ++ [x]
+                         Parse.SExpression [] -> pure $ acc ++ [x]
+                         Parse.SExpression (function:args) ->
+                           case lookupMacroDefinitions function allMacroDefs of
+                             Just macroDefs ->
+                               (acc ++) <$>
+                               expandMacroApplication
+                                 macroDefs
+                                 (filter isStatementNonconflicting stmts)
+                                 args
+                             Nothing -> pure $ acc ++ [x]
+                         Parse.Symbol _ -> pure $ acc ++ [x])
+                    []
+                    xs
+                x -> pure x))
+          program
+      pure $ Parse.programToTopLevelExpressions expandedExpr
 
 expandMacroApplication ::
-     (MonadBaseControl IO m, MonadError Error m, MonadIO m)
-  => MacroDefinition
+     (MonadError Error m, MonadFileSystem m, MonadProcess m, MonadResource m)
+  => NonEmpty MacroDefinition
   -> [Statement]
   -> [Parse.Expression]
   -> m [Parse.Expression]
-expandMacroApplication macroDef auxEnv args = do
-  macroProgram <- generateMacroProgram macroDef auxEnv args
+expandMacroApplication macroDefs auxEnv args = do
+  macroProgram <- generateMacroProgram macroDefs auxEnv args
   newSource <- uncurry3 evalMacro macroProgram
   Parse.parseMultiple newSource
 
-lookupMacroDefinition ::
-     (MonadError Error m)
-  => [MacroDefinition]
-  -> Parse.Expression
-  -> m (Maybe MacroDefinition)
-lookupMacroDefinition macroDefs identifierExpr =
-  case filter (`isMacroBeingCalled` identifierExpr) macroDefs of
-    [] -> pure Nothing
-    [macroDef] -> pure $ Just macroDef
-    macroDef:_ ->
-      throwError
-        (MacroError $
-         "Multiple macro definitions named: `" <> macroDef ^. name <> "`!")
+lookupMacroDefinitions ::
+     Parse.Expression -> [MacroDefinition] -> Maybe (NonEmpty MacroDefinition)
+lookupMacroDefinitions identifierExpr =
+  nonEmpty . filter (`isMacroBeingCalled` identifierExpr)
 
 isMacroBeingCalled :: MacroDefinition -> Parse.Expression -> Bool
 isMacroBeingCalled macroDef identifierExpr =
@@ -201,7 +228,8 @@
     Parse.LiteralInt _ -> False
     Parse.LiteralString _ -> False
     Parse.SExpression _ -> False
-    Parse.Symbol identifier -> macroDef ^. name == identifier
+    Parse.Symbol identifier ->
+      macroDef ^. functionDefinition . name == identifier
 
 stripMacroDefinitions :: Statement -> Statement
 stripMacroDefinitions =
@@ -235,3 +263,30 @@
             then newName
             else identifier
         _ -> expr
+
+evalMacro ::
+     (MonadError Error m, MonadFileSystem m, MonadProcess m)
+  => String
+  -> String
+  -> String
+  -> m String
+evalMacro astDefinition scaffold macroDefinitionAndEnvironment =
+  FS.withTemporaryDirectory $ \directoryName ->
+    FS.withCurrentDirectory directoryName $ do
+      let astDirectoryPath = "Axel" </> "Parse"
+      let macroDefinitionAndEnvironmentFileName =
+            "MacroDefinitionAndEnvironment.hs"
+      let scaffoldFileName = "Scaffold.hs"
+      FS.createDirectoryIfMissing True astDirectoryPath
+      FS.writeFile (astDirectoryPath </> "AST.hs") astDefinition
+      FS.writeFile
+        macroDefinitionAndEnvironmentFileName
+        macroDefinitionAndEnvironment
+      FS.writeFile scaffoldFileName scaffold
+      runExceptT (ghcInterpret scaffoldFileName) >>= \case
+        Left err ->
+          throwError $
+          MacroError
+            ("Temporary directory: " <> directoryName <> "\n\n" <> "Error:\n" <>
+             err)
+        Right res -> pure res
diff --git a/src/Axel/Monad/Console.hs b/src/Axel/Monad/Console.hs
new file mode 100644
--- /dev/null
+++ b/src/Axel/Monad/Console.hs
@@ -0,0 +1,64 @@
+{-# LANGUAGE DefaultSignatures #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE InstanceSigs #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Axel.Monad.Console where
+
+import Prelude hiding (putStr)
+import qualified Prelude
+
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Control.Monad.Identity (IdentityT)
+import Control.Monad.Trans (MonadTrans, lift)
+import Control.Monad.Trans.Cont (ContT)
+import Control.Monad.Trans.Except (ExceptT)
+import Control.Monad.Trans.Maybe (MaybeT)
+import qualified Control.Monad.Trans.RWS.Lazy as LazyRWS (RWST)
+import qualified Control.Monad.Trans.RWS.Strict as StrictRWS (RWST)
+import Control.Monad.Trans.Reader (ReaderT)
+import qualified Control.Monad.Trans.State.Lazy as LazyState (StateT)
+import qualified Control.Monad.Trans.State.Strict as StrictState (StateT)
+import qualified Control.Monad.Trans.Writer.Lazy as LazyWriter (WriterT)
+import qualified Control.Monad.Trans.Writer.Strict as StrictWriter (WriterT)
+
+class (Monad m) =>
+      MonadConsole m
+  where
+  putStr :: String -> m ()
+  default putStr :: (MonadTrans t, MonadConsole m', m ~ t m') =>
+    String -> m ()
+  putStr = lift . putStr
+
+instance (MonadConsole m) => MonadConsole (ContT r m)
+
+instance (MonadConsole m) => MonadConsole (ExceptT e m)
+
+instance (MonadConsole m) => MonadConsole (IdentityT m)
+
+instance (MonadConsole m) => MonadConsole (MaybeT m)
+
+instance (MonadConsole m) => MonadConsole (ReaderT r m)
+
+instance (Monoid w, MonadConsole m) => MonadConsole (LazyRWS.RWST r w s m)
+
+instance (Monoid w, MonadConsole m) =>
+         MonadConsole (StrictRWS.RWST r w s m)
+
+instance (MonadConsole m) => MonadConsole (LazyState.StateT s m)
+
+instance (MonadConsole m) => MonadConsole (StrictState.StateT s m)
+
+instance (Monoid w, MonadConsole m) =>
+         MonadConsole (LazyWriter.WriterT w m)
+
+instance (Monoid w, MonadConsole m) =>
+         MonadConsole (StrictWriter.WriterT w m)
+
+instance {-# OVERLAPPABLE #-} (Monad m, MonadIO m) => MonadConsole m where
+  putStr :: String -> m ()
+  putStr = liftIO . Prelude.putStr
+
+putStrLn :: (MonadConsole m) => String -> m ()
+putStrLn str = putStr (str <> "\n")
diff --git a/src/Axel/Monad/FileSystem.hs b/src/Axel/Monad/FileSystem.hs
new file mode 100644
--- /dev/null
+++ b/src/Axel/Monad/FileSystem.hs
@@ -0,0 +1,162 @@
+{-# LANGUAGE DefaultSignatures #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE InstanceSigs #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Axel.Monad.FileSystem where
+
+import Prelude hiding (readFile, writeFile)
+import qualified Prelude (readFile, writeFile)
+
+import Control.Monad (forM)
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Control.Monad.Identity (IdentityT)
+import Control.Monad.Trans (MonadTrans, lift)
+import Control.Monad.Trans.Cont (ContT)
+import Control.Monad.Trans.Except (ExceptT)
+import Control.Monad.Trans.Maybe (MaybeT)
+import qualified Control.Monad.Trans.RWS.Lazy as LazyRWS (RWST)
+import qualified Control.Monad.Trans.RWS.Strict as StrictRWS (RWST)
+import Control.Monad.Trans.Reader (ReaderT)
+import qualified Control.Monad.Trans.State.Lazy as LazyState (StateT)
+import qualified Control.Monad.Trans.State.Strict as StrictState (StateT)
+import qualified Control.Monad.Trans.Writer.Lazy as LazyWriter (WriterT)
+import qualified Control.Monad.Trans.Writer.Strict as StrictWriter (WriterT)
+
+import qualified System.Directory
+  ( copyFile
+  , createDirectoryIfMissing
+  , doesDirectoryExist
+  , getCurrentDirectory
+  , getDirectoryContents
+  , getTemporaryDirectory
+  , removeFile
+  , setCurrentDirectory
+  )
+import System.FilePath ((</>))
+import qualified System.IO.Strict as S (readFile)
+
+class (Monad m) =>
+      MonadFileSystem m
+  where
+  copyFile :: FilePath -> FilePath -> m ()
+  default copyFile :: (MonadTrans t, MonadFileSystem m', m ~ t m') =>
+    FilePath -> FilePath -> m ()
+  copyFile src dest = lift $ copyFile src dest
+  createDirectoryIfMissing :: Bool -> FilePath -> m ()
+  default createDirectoryIfMissing :: ( MonadTrans t
+                                      , MonadFileSystem m'
+                                      , m ~ t m'
+                                      ) =>
+    Bool -> FilePath -> m ()
+  createDirectoryIfMissing createParents path =
+    lift $ createDirectoryIfMissing createParents path
+  doesDirectoryExist :: FilePath -> m Bool
+  default doesDirectoryExist :: (MonadTrans t, MonadFileSystem m', m ~ t m') =>
+    FilePath -> m Bool
+  doesDirectoryExist = lift . doesDirectoryExist
+  getCurrentDirectory :: m FilePath
+  default getCurrentDirectory :: (MonadTrans t, MonadFileSystem m', m ~ t m') =>
+    m FilePath
+  getCurrentDirectory = lift getCurrentDirectory
+  getDirectoryContents :: FilePath -> m [FilePath]
+  default getDirectoryContents :: (MonadTrans t, MonadFileSystem m', m ~ t m') =>
+    FilePath -> m [FilePath]
+  getDirectoryContents = lift . getDirectoryContents
+  getTemporaryDirectory :: m FilePath
+  default getTemporaryDirectory :: (MonadTrans t, MonadFileSystem m', m ~ t m') =>
+    m FilePath
+  getTemporaryDirectory = lift getTemporaryDirectory
+  readFile :: FilePath -> m String
+  default readFile :: (MonadTrans t, MonadFileSystem m', m ~ t m') =>
+    FilePath -> m String
+  readFile = lift . readFile
+  removeFile :: FilePath -> m ()
+  default removeFile :: (MonadTrans t, MonadFileSystem m', m ~ t m') =>
+    FilePath -> m ()
+  removeFile = lift . removeFile
+  setCurrentDirectory :: FilePath -> m ()
+  default setCurrentDirectory :: (MonadTrans t, MonadFileSystem m', m ~ t m') =>
+    FilePath -> m ()
+  setCurrentDirectory = lift . setCurrentDirectory
+  writeFile :: FilePath -> String -> m ()
+  default writeFile :: (MonadTrans t, MonadFileSystem m', m ~ t m') =>
+    String -> FilePath -> m ()
+  writeFile path contents = lift $ writeFile path contents
+
+instance (MonadFileSystem m) => MonadFileSystem (ContT r m)
+
+instance (MonadFileSystem m) => MonadFileSystem (ExceptT e m)
+
+instance (MonadFileSystem m) => MonadFileSystem (IdentityT m)
+
+instance (MonadFileSystem m) => MonadFileSystem (MaybeT m)
+
+instance (MonadFileSystem m) => MonadFileSystem (ReaderT r m)
+
+instance (Monoid w, MonadFileSystem m) =>
+         MonadFileSystem (LazyRWS.RWST r w s m)
+
+instance (Monoid w, MonadFileSystem m) =>
+         MonadFileSystem (StrictRWS.RWST r w s m)
+
+instance (MonadFileSystem m) => MonadFileSystem (LazyState.StateT s m)
+
+instance (MonadFileSystem m) => MonadFileSystem (StrictState.StateT s m)
+
+instance (Monoid w, MonadFileSystem m) =>
+         MonadFileSystem (LazyWriter.WriterT w m)
+
+instance (Monoid w, MonadFileSystem m) =>
+         MonadFileSystem (StrictWriter.WriterT w m)
+
+instance {-# OVERLAPPABLE #-} (Monad m, MonadIO m) => MonadFileSystem m where
+  copyFile :: FilePath -> FilePath -> m ()
+  copyFile src dest = liftIO $ System.Directory.copyFile src dest
+  createDirectoryIfMissing :: Bool -> FilePath -> m ()
+  createDirectoryIfMissing createParentDirs path =
+    liftIO $ System.Directory.createDirectoryIfMissing createParentDirs path
+  doesDirectoryExist :: FilePath -> m Bool
+  doesDirectoryExist = liftIO . System.Directory.doesDirectoryExist
+  getCurrentDirectory :: m FilePath
+  getCurrentDirectory = liftIO System.Directory.getCurrentDirectory
+  getDirectoryContents :: FilePath -> m [FilePath]
+  getDirectoryContents = liftIO . System.Directory.getDirectoryContents
+  getTemporaryDirectory :: m FilePath
+  getTemporaryDirectory = liftIO System.Directory.getTemporaryDirectory
+  readFile :: FilePath -> m String
+  readFile = liftIO . S.readFile
+  removeFile :: FilePath -> m ()
+  removeFile = liftIO . System.Directory.removeFile
+  setCurrentDirectory :: FilePath -> m ()
+  setCurrentDirectory = liftIO . System.Directory.setCurrentDirectory
+  writeFile :: FilePath -> String -> m ()
+  writeFile filePath contents = liftIO $ Prelude.writeFile filePath contents
+
+-- Adapted from http://book.realworldhaskell.org/read/io-case-study-a-library-for-searching-the-filesystem.html.
+getDirectoryContentsRec ::
+     (Monad m, MonadFileSystem m) => FilePath -> m [FilePath]
+getDirectoryContentsRec dir = do
+  names <- getDirectoryContents dir
+  let properNames = filter (`notElem` [".", ".."]) names
+  paths <-
+    forM properNames $ \name -> do
+      let path = dir </> name
+      isDirectory <- doesDirectoryExist path
+      if isDirectory
+        then getDirectoryContentsRec path
+        else pure [path]
+  pure $ concat paths
+
+withCurrentDirectory :: (MonadFileSystem m) => FilePath -> m a -> m a
+withCurrentDirectory directory f = do
+  originalDirectory <- getCurrentDirectory
+  setCurrentDirectory directory
+  result <- f
+  setCurrentDirectory originalDirectory
+  pure result
+
+withTemporaryDirectory :: (MonadFileSystem m) => (FilePath -> m a) -> m a
+withTemporaryDirectory action = getTemporaryDirectory >>= action
diff --git a/src/Axel/Monad/Process.hs b/src/Axel/Monad/Process.hs
new file mode 100644
--- /dev/null
+++ b/src/Axel/Monad/Process.hs
@@ -0,0 +1,81 @@
+{-# LANGUAGE DefaultSignatures #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE InstanceSigs #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Axel.Monad.Process where
+
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Control.Monad.Identity (IdentityT)
+import Control.Monad.Trans (MonadTrans, lift)
+import Control.Monad.Trans.Cont (ContT)
+import Control.Monad.Trans.Except (ExceptT)
+import Control.Monad.Trans.Maybe (MaybeT)
+import qualified Control.Monad.Trans.RWS.Lazy as LazyRWS (RWST)
+import qualified Control.Monad.Trans.RWS.Strict as StrictRWS (RWST)
+import Control.Monad.Trans.Reader (ReaderT)
+import qualified Control.Monad.Trans.State.Lazy as LazyState (StateT)
+import qualified Control.Monad.Trans.State.Strict as StrictState (StateT)
+import qualified Control.Monad.Trans.Writer.Lazy as LazyWriter (WriterT)
+import qualified Control.Monad.Trans.Writer.Strict as StrictWriter (WriterT)
+
+import qualified System.Environment (getArgs)
+import System.Exit (ExitCode)
+import qualified System.Process (readProcessWithExitCode)
+import qualified System.Process.Typed (proc, runProcess)
+
+class (Monad m) =>
+      MonadProcess m
+  where
+  getArgs :: m [String]
+  default getArgs :: (MonadTrans t, MonadProcess m', m ~ t m') =>
+    m [String]
+  getArgs = lift getArgs
+  runProcess :: FilePath -> [String] -> String -> m (ExitCode, String, String)
+  default runProcess :: (MonadTrans t, MonadProcess m', m ~ t m') =>
+    FilePath -> [String] -> String -> m (ExitCode, String, String)
+  runProcess cmd args stdin = lift $ runProcess cmd args stdin
+  runProcessInheritingStreams :: FilePath -> [String] -> m ExitCode
+  default runProcessInheritingStreams :: ( MonadTrans t
+                                         , MonadProcess m'
+                                         , m ~ t m'
+                                         ) =>
+    FilePath -> [String] -> m ExitCode
+  runProcessInheritingStreams cmd args =
+    lift $ runProcessInheritingStreams cmd args
+
+instance (MonadProcess m) => MonadProcess (ContT r m)
+
+instance (MonadProcess m) => MonadProcess (ExceptT e m)
+
+instance (MonadProcess m) => MonadProcess (IdentityT m)
+
+instance (MonadProcess m) => MonadProcess (MaybeT m)
+
+instance (MonadProcess m) => MonadProcess (ReaderT r m)
+
+instance (Monoid w, MonadProcess m) => MonadProcess (LazyRWS.RWST r w s m)
+
+instance (Monoid w, MonadProcess m) =>
+         MonadProcess (StrictRWS.RWST r w s m)
+
+instance (MonadProcess m) => MonadProcess (LazyState.StateT s m)
+
+instance (MonadProcess m) => MonadProcess (StrictState.StateT s m)
+
+instance (Monoid w, MonadProcess m) =>
+         MonadProcess (LazyWriter.WriterT w m)
+
+instance (Monoid w, MonadProcess m) =>
+         MonadProcess (StrictWriter.WriterT w m)
+
+instance {-# OVERLAPPABLE #-} (Monad m, MonadIO m) => MonadProcess m where
+  getArgs :: m [String]
+  getArgs = liftIO System.Environment.getArgs
+  runProcess :: FilePath -> [String] -> String -> m (ExitCode, String, String)
+  runProcess cmd args stdin =
+    liftIO $ System.Process.readProcessWithExitCode cmd args stdin
+  runProcessInheritingStreams cmd args =
+    liftIO $
+    System.Process.Typed.runProcess (System.Process.Typed.proc cmd args)
diff --git a/src/Axel/Monad/Resource.hs b/src/Axel/Monad/Resource.hs
new file mode 100644
--- /dev/null
+++ b/src/Axel/Monad/Resource.hs
@@ -0,0 +1,90 @@
+{-# LANGUAGE DefaultSignatures #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE InstanceSigs #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Axel.Monad.Resource where
+
+import Axel.Monad.FileSystem as FS (MonadFileSystem(readFile))
+
+import Control.Monad ((>=>))
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Control.Monad.Identity (IdentityT)
+import Control.Monad.Trans (MonadTrans, lift)
+import Control.Monad.Trans.Cont (ContT)
+import Control.Monad.Trans.Except (ExceptT)
+import Control.Monad.Trans.Maybe (MaybeT)
+import qualified Control.Monad.Trans.RWS.Lazy as LazyRWS (RWST)
+import qualified Control.Monad.Trans.RWS.Strict as StrictRWS (RWST)
+import Control.Monad.Trans.Reader (ReaderT)
+import qualified Control.Monad.Trans.State.Lazy as LazyState (StateT)
+import qualified Control.Monad.Trans.State.Strict as StrictState (StateT)
+import qualified Control.Monad.Trans.Writer.Lazy as LazyWriter (WriterT)
+import qualified Control.Monad.Trans.Writer.Strict as StrictWriter (WriterT)
+
+import Paths_axel (getDataFileName)
+
+import System.FilePath ((</>))
+
+newtype ResourceId =
+  ResourceId String
+
+class (Monad m) =>
+      MonadResource m
+  where
+  getResourcePath :: ResourceId -> m FilePath
+  default getResourcePath :: (MonadTrans t, MonadResource m', m ~ t m') =>
+    ResourceId -> m FilePath
+  getResourcePath = lift . getResourcePath
+
+instance (MonadResource m) => MonadResource (ContT r m)
+
+instance (MonadResource m) => MonadResource (ExceptT e m)
+
+instance (MonadResource m) => MonadResource (IdentityT m)
+
+instance (MonadResource m) => MonadResource (MaybeT m)
+
+instance (MonadResource m) => MonadResource (ReaderT r m)
+
+instance (Monoid w, MonadResource m) =>
+         MonadResource (LazyRWS.RWST r w s m)
+
+instance (Monoid w, MonadResource m) =>
+         MonadResource (StrictRWS.RWST r w s m)
+
+instance (MonadResource m) => MonadResource (LazyState.StateT s m)
+
+instance (MonadResource m) => MonadResource (StrictState.StateT s m)
+
+instance (Monoid w, MonadResource m) =>
+         MonadResource (LazyWriter.WriterT w m)
+
+instance (Monoid w, MonadResource m) =>
+         MonadResource (StrictWriter.WriterT w m)
+
+instance {-# OVERLAPPABLE #-} (Monad m, MonadIO m) => MonadResource m where
+  getResourcePath :: ResourceId -> m FilePath
+  getResourcePath (ResourceId resource) =
+    liftIO $ getDataFileName ("resources" </> resource)
+
+readResource :: (MonadFileSystem m, MonadResource m) => ResourceId -> m String
+readResource = getResourcePath >=> FS.readFile
+
+astDefinition :: ResourceId
+astDefinition = ResourceId "autogenerated/macros/AST.hs"
+
+macroDefinitionAndEnvironmentFooter :: ResourceId
+macroDefinitionAndEnvironmentFooter =
+  ResourceId "macros/MacroDefinitionAndEnvironmentFooter.hs"
+
+macroDefinitionAndEnvironmentHeader :: ResourceId
+macroDefinitionAndEnvironmentHeader =
+  ResourceId "macros/MacroDefinitionAndEnvironmentHeader.hs"
+
+macroScaffold :: ResourceId
+macroScaffold = ResourceId "macros/Scaffold.hs"
+
+newProjectTemplate :: ResourceId
+newProjectTemplate = ResourceId "new-project-template"
diff --git a/src/Axel/Normalize.hs b/src/Axel/Normalize.hs
--- a/src/Axel/Normalize.hs
+++ b/src/Axel/Normalize.hs
@@ -5,28 +5,29 @@
 module Axel.Normalize where
 
 import Axel.AST
-  ( ArgumentList(ArgumentList)
-  , CaseBlock(CaseBlock)
+  ( CaseBlock(CaseBlock)
   , DataDeclaration(DataDeclaration)
   , Expression(ECaseBlock, EEmptySExpression, EFunctionApplication,
            EIdentifier, ELambda, ELetBlock, ELiteral)
   , FunctionApplication(FunctionApplication)
   , FunctionDefinition(FunctionDefinition)
+  , Identifier
   , Import(ImportItem, ImportType)
   , ImportSpecification(ImportAll, ImportOnly)
   , Lambda(Lambda)
-  , LanguagePragma(LanguagePragma)
   , LetBlock(LetBlock)
   , Literal(LChar, LInt, LString)
   , MacroDefinition(MacroDefinition)
+  , Pragma(Pragma)
   , QualifiedImport(QualifiedImport)
   , RestrictedImport(RestrictedImport)
-  , Statement(SDataDeclaration, SFunctionDefinition, SLanguagePragma,
-          SMacroDefinition, SModuleDeclaration, SQualifiedImport,
-          SRestrictedImport, STopLevel, STypeSynonym, STypeclassInstance,
+  , Statement(SDataDeclaration, SFunctionDefinition, SMacroDefinition,
+          SModuleDeclaration, SPragma, SQualifiedImport, SRestrictedImport,
+          STopLevel, STypeSignature, STypeSynonym, STypeclassInstance,
           SUnrestrictedImport)
   , TopLevel(TopLevel)
   , TypeDefinition(ProperType, TypeConstructor)
+  , TypeSignature(TypeSignature)
   , TypeSynonym(TypeSynonym)
   , TypeclassInstance(TypeclassInstance)
   )
@@ -35,7 +36,6 @@
   ( Expression(LiteralChar, LiteralInt, LiteralString, SExpression,
            Symbol)
   )
-import Axel.Quote (quoteParseExpression)
 
 import Control.Monad.Except (MonadError, throwError)
 
@@ -54,12 +54,12 @@
                    (,) <$> normalizeExpression pat <*> normalizeExpression body
                  x -> throwError $ NormalizeError "Invalid case!" [x, expr])
               cases
-      in ECaseBlock <$>
-         (CaseBlock <$> normalizeExpression var <*> normalizedCases)
-    [Parse.Symbol "fn", Parse.SExpression args, body] ->
+       in ECaseBlock <$>
+          (CaseBlock <$> normalizeExpression var <*> normalizedCases)
+    [Parse.Symbol "\\", Parse.SExpression args, body] ->
       let normalizedArguments = traverse normalizeExpression args
-      in ELambda <$>
-         (Lambda <$> normalizedArguments <*> normalizeExpression body)
+       in ELambda <$>
+          (Lambda <$> normalizedArguments <*> normalizeExpression body)
     [Parse.Symbol "let", Parse.SExpression bindings, body] ->
       let normalizedBindings =
             traverse
@@ -69,9 +69,8 @@
                    normalizeExpression value
                  x -> throwError $ NormalizeError "Invalid pattern!" [x, expr])
               bindings
-      in ELetBlock <$>
-         (LetBlock <$> normalizedBindings <*> normalizeExpression body)
-    [Parse.Symbol "quote", expr'] -> pure $ quoteParseExpression expr'
+       in ELetBlock <$>
+          (LetBlock <$> normalizedBindings <*> normalizeExpression body)
     fn:args ->
       EFunctionApplication <$>
       (FunctionApplication <$> normalizeExpression fn <*>
@@ -79,34 +78,27 @@
     [] -> pure EEmptySExpression
 normalizeExpression (Parse.Symbol symbol) = pure $ EIdentifier symbol
 
-normalizeDefinitions ::
+normalizeFunctionDefinition ::
      (MonadError Error m)
-  => Parse.Expression
+  => Identifier
   -> [Parse.Expression]
-  -> m [(ArgumentList, Expression)]
-normalizeDefinitions ctxt =
-  traverse
-    (\case
-       Parse.SExpression [Parse.SExpression args, body] ->
-         (,) <$> (ArgumentList <$> traverse normalizeExpression args) <*>
-         normalizeExpression body
-       x -> throwError $ NormalizeError "Invalid definition!" [x, ctxt])
+  -> Parse.Expression
+  -> m FunctionDefinition
+normalizeFunctionDefinition fnName arguments body =
+  FunctionDefinition fnName <$> traverse normalizeExpression arguments <*>
+  normalizeExpression body
 
 normalizeStatement :: (MonadError Error m) => Parse.Expression -> m Statement
 normalizeStatement expr@(Parse.SExpression items) =
   case items of
-    Parse.Symbol "=":Parse.Symbol fnName:typeSig:defs ->
-      normalizeExpression typeSig >>= \case
-        EFunctionApplication normalizedTypeSig ->
-          SFunctionDefinition <$>
-          (FunctionDefinition fnName normalizedTypeSig <$>
-           normalizeDefinitions expr defs)
-        _ ->
-          throwError $ NormalizeError "Invalid type signature!" [typeSig, expr]
+    [Parse.Symbol "::", Parse.Symbol fnName, typeDef] ->
+      STypeSignature <$> (TypeSignature fnName <$> normalizeExpression typeDef)
+    [Parse.Symbol "=", Parse.Symbol fnName, Parse.SExpression arguments, body] ->
+      SFunctionDefinition <$> normalizeFunctionDefinition fnName arguments body
     Parse.Symbol "begin":stmts ->
       let normalizedStmts = traverse normalizeStatement stmts
-      in STopLevel . TopLevel <$> normalizedStmts
-    [Parse.Symbol "data", typeDef, Parse.SExpression constructors] ->
+       in STopLevel . TopLevel <$> normalizedStmts
+    Parse.Symbol "data":typeDef:constructors ->
       let normalizedConstructors =
             traverse
               (\x ->
@@ -117,27 +109,28 @@
                      throwError $
                      NormalizeError "Invalid type constructor!" [x, expr])
               constructors
-      in normalizeExpression typeDef >>= \case
-           EFunctionApplication typeConstructor ->
-             SDataDeclaration <$>
-             (DataDeclaration (TypeConstructor typeConstructor) <$>
-              normalizedConstructors)
-           EIdentifier properType ->
-             SDataDeclaration <$>
-             (DataDeclaration (ProperType properType) <$> normalizedConstructors)
-           _ -> throwError $ NormalizeError "Invalid type!" [typeDef, expr]
-    Parse.Symbol "defmacro":Parse.Symbol macroName:defs ->
-      SMacroDefinition <$>
-      (MacroDefinition macroName <$> normalizeDefinitions expr defs)
-    [Parse.Symbol "import", Parse.Symbol moduleName, Parse.SExpression imports] ->
+       in normalizeExpression typeDef >>= \case
+            EFunctionApplication typeConstructor ->
+              SDataDeclaration <$>
+              (DataDeclaration (TypeConstructor typeConstructor) <$>
+               normalizedConstructors)
+            EIdentifier properType ->
+              SDataDeclaration <$>
+              (DataDeclaration (ProperType properType) <$>
+               normalizedConstructors)
+            _ -> throwError $ NormalizeError "Invalid type!" [typeDef, expr]
+    [Parse.Symbol "macro", Parse.Symbol macroName, Parse.SExpression arguments, body] ->
+      SMacroDefinition . MacroDefinition <$>
+      normalizeFunctionDefinition macroName arguments body
+    [Parse.Symbol "import", Parse.Symbol moduleName, importSpec] ->
       SRestrictedImport <$>
-      (RestrictedImport moduleName <$> normalizeImportList expr imports)
+      (RestrictedImport moduleName <$> normalizeImportSpec expr importSpec)
     [Parse.Symbol "importq", Parse.Symbol moduleName, Parse.Symbol alias, importSpec] ->
       SQualifiedImport <$>
       (QualifiedImport moduleName alias <$> normalizeImportSpec expr importSpec)
     [Parse.Symbol "importUnrestricted", Parse.Symbol moduleName] ->
       pure $ SUnrestrictedImport moduleName
-    [Parse.Symbol "instance", instanceName, Parse.SExpression defs] ->
+    Parse.Symbol "instance":instanceName:defs ->
       let normalizedDefs =
             traverse
               (\x ->
@@ -146,17 +139,17 @@
                    _ ->
                      throwError $ NormalizeError "Invalid definition!" [x, expr])
               defs
-      in STypeclassInstance <$>
-         (TypeclassInstance <$> normalizeExpression instanceName <*>
-          normalizedDefs)
-    [Parse.Symbol "language", Parse.Symbol languageName] ->
-      pure $ SLanguagePragma (LanguagePragma languageName)
+       in STypeclassInstance <$>
+          (TypeclassInstance <$> normalizeExpression instanceName <*>
+           normalizedDefs)
+    [Parse.Symbol "pragma", Parse.LiteralString pragma] ->
+      pure $ SPragma (Pragma pragma)
     [Parse.Symbol "module", Parse.Symbol moduleName] ->
       pure $ SModuleDeclaration moduleName
     [Parse.Symbol "type", alias, def] ->
       let normalizedAlias = normalizeExpression alias
           normalizedDef = normalizeExpression def
-      in STypeSynonym <$> (TypeSynonym <$> normalizedAlias <*> normalizedDef)
+       in STypeSynonym <$> (TypeSynonym <$> normalizedAlias <*> normalizedDef)
     _ -> throwError $ NormalizeError "Invalid top-level form!" [expr]
   where
     normalizeImportSpec ctxt importSpec =
@@ -180,7 +173,7 @@
                             throwError $
                             NormalizeError "Invalid import!" [x, item, ctxt])
                        imports
-               in ImportType type' <$> normalizedImports
+                in ImportType type' <$> normalizedImports
              x -> throwError $ NormalizeError "Invalid import!" [x, item, ctxt])
         input
 normalizeStatement expr =
diff --git a/src/Axel/Parse.hs b/src/Axel/Parse.hs
--- a/src/Axel/Parse.hs
+++ b/src/Axel/Parse.hs
@@ -5,6 +5,7 @@
 --      never be imported by itself but only implicitly as part of this module.
 {-# OPTIONS_GHC -fno-warn-orphans #-}
 {-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE InstanceSigs #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
 
@@ -13,7 +14,7 @@
   , module Axel.Parse.AST
   ) where
 
-import Axel.Error (Error(ParseError))
+import Axel.Error (Error(ParseError), fatal)
 
 -- Re-exporting these so that consumers of parsed ASTs do not need
 -- to know about the internal file.
@@ -22,7 +23,9 @@
            Symbol)
   )
 import Axel.Utils.List (takeUntil)
-import Axel.Utils.Recursion (Recursive(bottomUpFmap, bottomUpTraverse))
+import Axel.Utils.Recursion
+  ( Recursive(bottomUpFmap, bottomUpTraverse, topDownFmap)
+  )
 
 import Control.Monad.Except (MonadError, throwError)
 
@@ -63,6 +66,14 @@
       LiteralString _ -> pure x
       SExpression xs -> SExpression <$> traverse (bottomUpTraverse f) xs
       Symbol _ -> pure x
+  topDownFmap :: (Expression -> Expression) -> Expression -> Expression
+  topDownFmap f x =
+    case f x of
+      LiteralChar _ -> x
+      LiteralInt _ -> x
+      LiteralString _ -> x
+      SExpression xs -> SExpression (map (topDownFmap f) xs)
+      Symbol _ -> x
 
 parseReadMacro ::
      (Stream s m Char) => String -> String -> ParsecT s u m Expression
@@ -77,14 +88,14 @@
 whitespace = many space
 
 literalChar :: (Stream s m Char) => ParsecT s u m Expression
-literalChar = LiteralChar <$> (char '\\' *> any')
+literalChar = LiteralChar <$> (char '{' *> any' <* char '}')
 
 literalInt :: (Stream s m Char) => ParsecT s u m Expression
 literalInt = LiteralInt . read <$> many1 digit
 
 literalList :: (Stream s m Char) => ParsecT s u m Expression
 literalList =
-  (SExpression . (Symbol "list" :)) <$> (char '[' *> many item <* char ']')
+  SExpression . (Symbol "list" :) <$> (char '[' *> many item <* char ']')
   where
     item = try (whitespace *> expression) <|> expression
 
@@ -125,24 +136,59 @@
   sExpression <|>
   symbol
 
-stripComments :: String -> String
-stripComments = unlines . map cleanLine . lines
+-- TODO Derive this with Template Haskell (it's really brittle, currently).
+quoteParseExpression :: Expression -> Expression
+quoteParseExpression (LiteralChar x) =
+  SExpression [Symbol "AST.LiteralChar", LiteralChar x]
+quoteParseExpression (LiteralInt x) =
+  SExpression [Symbol "AST.LiteralInt", LiteralInt x]
+quoteParseExpression (LiteralString x) =
+  SExpression [Symbol "AST.LiteralString", LiteralString x]
+quoteParseExpression (SExpression xs) =
+  SExpression
+    [ Symbol "AST.SExpression"
+    , SExpression (Symbol "list" : map quoteParseExpression xs)
+    ]
+quoteParseExpression (Symbol x) =
+  SExpression [Symbol "AST.Symbol", LiteralString (handleEscapes x)]
   where
-    cleanLine = takeUntil "--"
+    handleEscapes =
+      concatMap $ \case
+        '\\' -> "\\\\"
+        c -> [c]
 
 parseMultiple :: (MonadError Error m) => String -> m [Expression]
 parseMultiple =
-  either (throwError . ParseError) pure .
+  either (throwError . ParseError . show) (pure . map expandQuotes) .
   parse
     (many1 (optional whitespace *> expression <* optional whitespace) <* eof)
     ""
+  where
+    expandQuotes =
+      topDownFmap
+        (\case
+           SExpression [Symbol "quote", x] -> quoteParseExpression x
+           x -> x)
 
 parseSingle :: (MonadError Error m) => String -> m Expression
-parseSingle =
-  either (throwError . ParseError) pure .
-  parse (optional whitespace *> expression <* optional whitespace <* eof) ""
+parseSingle input =
+  parseMultiple input >>= \case
+    [x] -> pure x
+    _ -> throwError $ ParseError "Only one expression was expected"
 
+stripComments :: String -> String
+stripComments = unlines . map cleanLine . lines
+  where
+    cleanLine = takeUntil "--"
+
 parseSource :: (MonadError Error m) => String -> m Expression
 parseSource input = do
   statements <- parseMultiple $ stripComments input
   pure $ SExpression (Symbol "begin" : statements)
+
+programToTopLevelExpressions :: Expression -> [Expression]
+programToTopLevelExpressions (SExpression (Symbol "begin":stmts)) = stmts
+programToTopLevelExpressions _ = fatal "programToTopLevelExpressions" "0001"
+
+topLevelExpressionsToProgram :: [Expression] -> Expression
+topLevelExpressionsToProgram stmts = SExpression (Symbol "begin" : stmts)
diff --git a/src/Axel/Parse/AST.hs b/src/Axel/Parse/AST.hs
--- a/src/Axel/Parse/AST.hs
+++ b/src/Axel/Parse/AST.hs
@@ -1,3 +1,6 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE InstanceSigs #-}
+
 -- NOTE Because this file will be used as the header of auto-generated macro programs,
 --      it can't have any project-specific dependencies (such as `Fix`).
 module Axel.Parse.AST where
@@ -21,7 +24,7 @@
 -- Internal utilities
 -- ******************************
 toAxel :: Expression -> String
-toAxel (LiteralChar x) = ['\\', x]
+toAxel (LiteralChar x) = ['{', x, '}']
 toAxel (LiteralInt x) = show x
 toAxel (LiteralString xs) = "\"" ++ xs ++ "\""
 toAxel (SExpression xs) = "(" ++ unwords (map toAxel xs) ++ ")"
@@ -40,3 +43,20 @@
   let identifier = "aXEL_AUTOGENERATED_IDENTIFIER_" ++ show suffix
   modifyIORef gensymCounter succ
   pure $ Symbol identifier
+
+-- | This allows splice-unquoting of both `[Expression]`s and `SExpression`s, without requiring special syntax for each.
+class ToExpressionList a where
+  toExpressionList :: a -> [Expression]
+
+instance ToExpressionList [Expression] where
+  toExpressionList :: [Expression] -> [Expression]
+  toExpressionList = id
+
+-- | Because we do not have a way to statically ensure an `SExpression` is passed (and not another one of `Expression`'s constructors instead), we will error at compile-time if a macro attempts to splice-unquote inappropriately.
+instance ToExpressionList Expression where
+  toExpressionList :: Expression -> [Expression]
+  toExpressionList (SExpression xs) = xs
+  toExpressionList x =
+    error
+      (show x <>
+       " cannot be splice-unquoted, because it is not an s-expression!")
diff --git a/src/Axel/Parse/Args.hs b/src/Axel/Parse/Args.hs
new file mode 100644
--- /dev/null
+++ b/src/Axel/Parse/Args.hs
@@ -0,0 +1,31 @@
+module Axel.Parse.Args where
+
+import Data.Semigroup ((<>))
+
+import Options.Applicative
+  ( Parser
+  , argument
+  , command
+  , info
+  , metavar
+  , progDesc
+  , str
+  , subparser
+  )
+
+data ModeCommand
+  = File FilePath
+  | Project
+
+modeCommandParser :: Parser ModeCommand
+modeCommandParser = subparser $ projectCommand <> fileCommand
+  where
+    fileCommand =
+      command
+        "file"
+        (info (File <$> argument str (metavar "FILE")) $
+         progDesc "Build and run a single file")
+    projectCommand =
+      command
+        "project"
+        (info (pure Project) $ progDesc "Build and run the project")
diff --git a/src/Axel/Project.hs b/src/Axel/Project.hs
deleted file mode 100644
--- a/src/Axel/Project.hs
+++ /dev/null
@@ -1,92 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Axel.Project where
-
-import Axel.Entry (transpileFile')
-import Axel.Utils.Directory (getRecursiveContents)
-
-import Control.Lens ((%~))
-import Control.Monad (void)
-import Control.Monad.Except (throwError)
-import Control.Monad.IO.Class (liftIO)
-
-import Data.Aeson.Lens (_Array, key)
-import Data.Function ((&))
-import Data.List (foldl')
-import Data.Semigroup ((<>))
-import qualified Data.Text as T (append, isSuffixOf, pack)
-import Data.Vector (cons)
-import Data.Version (showVersion)
-import Data.Yaml (Value(String), decodeFileEither, encodeFile)
-
-import Paths_axel (getDataFileName, version)
-
-import System.Directory (copyFile, removeFile, setCurrentDirectory)
-import System.FilePath ((</>))
-import System.Process (readProcess, readProcessWithExitCode)
-import System.Process.Typed (proc, runProcess)
-
-import Text.Regex.PCRE ((=~), getAllTextSubmatches)
-
-newProject :: String -> IO ()
-newProject projectName = do
-  void $ readProcess "stack" ["new", projectName, "new-template"] ""
-  setCurrentDirectory projectName
-  void $
-    readProcess "stack" ["config", "set", "resolver", "nightly-2018-08-17"] ""
-  templatePath <- getDataFileName ("resources" </> "new-project-template")
-  let copyAxel filePath = do
-        copyFile
-          (templatePath </> filePath <> ".axel")
-          (projectName </> filePath <> ".axel")
-        removeFile (projectName </> filePath <> ".hs")
-  mapM_ copyAxel ["Setup", "app" </> "Main", "src" </> "Lib", "test" </> "Spec"]
-
-transpileProject :: IO [FilePath]
-transpileProject = do
-  files <- getRecursiveContents "."
-  let axelFiles =
-        filter (\filePath -> ".axel" `T.isSuffixOf` T.pack filePath) files
-  mapM transpileFile' axelFiles
-
-addAxelDependency :: IO ()
-addAxelDependency = do
-  let packageConfigPath = "package.yaml"
-  let axelHackageVersion = T.pack $ showVersion version
-  decodeResult <- decodeFileEither packageConfigPath
-  case decodeResult of
-    Right contents ->
-      let newContents :: Value
-          newContents =
-            contents & key "dependencies" . _Array %~
-            cons (String $ T.append "axel ==" axelHackageVersion)
-      in encodeFile packageConfigPath newContents
-    Left err ->
-      throwError
-        (userError $ "`package.yaml` could not be parsed: " <> show err)
-
-buildProject :: IO ()
-buildProject = do
-  hsPaths <- transpileProject
-  addAxelDependency
-  void $ readProcess "stack" ["build"] ""
-  mapM_ removeFile hsPaths
-
-runProject :: IO ()
-runProject = do
-  (_, _, stderr) <- readProcessWithExitCode "stack" ["ide", "targets"] ""
-  let targets = lines stderr
-  case findExeTargets targets of
-    [target] -> do
-      liftIO $ putStrLn ("Running " <> target <> "...")
-      void $ runProcess $ proc "stack" ["exec", target]
-    _ -> throwError (userError "No executable target was unambiguously found!")
-  where
-    findExeTargets =
-      foldl'
-        (\acc target ->
-           case getAllTextSubmatches $ target =~
-                ("([^:]*):exe:([^:]*)" :: String) of
-             [_fullMatch, _projectName, targetName] -> targetName : acc
-             _ -> acc)
-        []
diff --git a/src/Axel/Quote.hs b/src/Axel/Quote.hs
deleted file mode 100644
--- a/src/Axel/Quote.hs
+++ /dev/null
@@ -1,47 +0,0 @@
-module Axel.Quote where
-
-import qualified Axel.AST as AST
-  ( Expression(EFunctionApplication, EIdentifier, ELiteral)
-  , FunctionApplication(FunctionApplication)
-  , Literal(LChar, LInt, LString)
-  )
-import qualified Axel.Parse as Parse
-  ( Expression(LiteralChar, LiteralInt, LiteralString, SExpression,
-           Symbol)
-  )
-
-quoteList :: [Parse.Expression] -> AST.Expression
-quoteList =
-  foldr
-    (\x acc ->
-       AST.EFunctionApplication
-         (AST.FunctionApplication
-            (AST.EIdentifier ":")
-            [quoteParseExpression x, acc]))
-    (AST.EIdentifier "[]")
-
--- TODO Derive this with Template Haskell (it's really brittle, currently).
-quoteParseExpression :: Parse.Expression -> AST.Expression
-quoteParseExpression (Parse.LiteralChar x) =
-  AST.EFunctionApplication $
-  AST.FunctionApplication
-    (AST.EIdentifier "AST.LiteralChar")
-    [AST.ELiteral $ AST.LChar x]
-quoteParseExpression (Parse.LiteralInt x) =
-  AST.EFunctionApplication $
-  AST.FunctionApplication
-    (AST.EIdentifier "AST.LiteralInt")
-    [AST.ELiteral $ AST.LInt x]
-quoteParseExpression (Parse.LiteralString x) =
-  AST.EFunctionApplication $
-  AST.FunctionApplication
-    (AST.EIdentifier "AST.LiteralString")
-    [AST.ELiteral $ AST.LString x]
-quoteParseExpression (Parse.SExpression xs) =
-  AST.EFunctionApplication $
-  AST.FunctionApplication (AST.EIdentifier "AST.SExpression") [quoteList xs]
-quoteParseExpression (Parse.Symbol x) =
-  AST.EFunctionApplication $
-  AST.FunctionApplication
-    (AST.EIdentifier "AST.Symbol")
-    [AST.ELiteral $ AST.LString x]
diff --git a/src/Axel/Utils/Debug.hs b/src/Axel/Utils/Debug.hs
--- a/src/Axel/Utils/Debug.hs
+++ b/src/Axel/Utils/Debug.hs
@@ -2,8 +2,8 @@
 
 import Debug.Trace (trace, traceShow)
 
-unsafeTee :: String -> a -> a
-unsafeTee = trace
+unsafeTee :: String -> String
+unsafeTee x = trace x x
 
 unsafeTeeS :: Show a => a -> a
 unsafeTeeS x = traceShow x x
diff --git a/src/Axel/Utils/Directory.hs b/src/Axel/Utils/Directory.hs
deleted file mode 100644
--- a/src/Axel/Utils/Directory.hs
+++ /dev/null
@@ -1,41 +0,0 @@
-{-# LANGUAGE FlexibleContexts #-}
-
-module Axel.Utils.Directory where
-
-import Control.Monad (forM)
-import Control.Monad.IO.Class (MonadIO, liftIO)
-import Control.Monad.Trans.Control (MonadBaseControl, control)
-
-import System.Directory
-  ( createDirectoryIfMissing
-  , doesDirectoryExist
-  , getDirectoryContents
-  , getTemporaryDirectory
-  , withCurrentDirectory
-  )
-import System.FilePath ((</>))
-
--- Adapted from http://book.realworldhaskell.org/read/io-case-study-a-library-for-searching-the-filesystem.html.
-getRecursiveContents :: FilePath -> IO [FilePath]
-getRecursiveContents startDir = do
-  names <- getDirectoryContents startDir
-  let properNames = filter (`notElem` [".", ".."]) names
-  paths <-
-    forM properNames $ \name -> do
-      let path = startDir </> name
-      isDirectory <- doesDirectoryExist path
-      if isDirectory
-        then getRecursiveContents path
-        else pure [path]
-  pure $ concat paths
-
-withCurrentDirectoryLifted :: (MonadBaseControl IO m) => FilePath -> m a -> m a
-withCurrentDirectoryLifted directory f =
-  control $ \runInIO -> withCurrentDirectory directory (runInIO f)
-
-withTempDirectory :: (MonadIO m) => (FilePath -> m a) -> m a
-withTempDirectory f = do
-  temporaryDirectory <- liftIO getTemporaryDirectory
-  liftIO $ createDirectoryIfMissing True temporaryDirectory
-  result <- f temporaryDirectory
-  pure result
diff --git a/src/Axel/Utils/Display.hs b/src/Axel/Utils/Display.hs
--- a/src/Axel/Utils/Display.hs
+++ b/src/Axel/Utils/Display.hs
@@ -8,6 +8,7 @@
   = Commas
   | Newlines
   | Pipes
+  | Semicolons
   | Spaces
 
 delimit :: Delimiter -> [String] -> String
@@ -16,6 +17,7 @@
     lookupDelimiter Commas = ","
     lookupDelimiter Newlines = "\n"
     lookupDelimiter Pipes = "|"
+    lookupDelimiter Semicolons = ";"
     lookupDelimiter Spaces = " "
 
 -- https://stackoverflow.com/questions/10548170/what-characters-are-permitted-for-haskell-operators
@@ -27,7 +29,7 @@
 lowerFirst "" = ""
 
 renderBlock :: [String] -> String
-renderBlock = surround CurlyBraces . intercalate ";"
+renderBlock = surround CurlyBraces . delimit Semicolons
 
 renderPragma :: String -> String
 renderPragma x = "{-# " <> x <> " #-}"
diff --git a/src/Axel/Utils/Recursion.hs b/src/Axel/Utils/Recursion.hs
--- a/src/Axel/Utils/Recursion.hs
+++ b/src/Axel/Utils/Recursion.hs
@@ -13,3 +13,4 @@
   -- TODO Remove dependency on `Monad` in favor of `Applicative`
   --      (which is all the standard `traverse` requires).
   bottomUpTraverse :: (Monad m) => (a -> m a) -> a -> m a
+  topDownFmap :: (a -> a) -> a -> a
diff --git a/src/Axel/Utils/Resources.hs b/src/Axel/Utils/Resources.hs
deleted file mode 100644
--- a/src/Axel/Utils/Resources.hs
+++ /dev/null
@@ -1,28 +0,0 @@
-module Axel.Utils.Resources where
-
-import Control.Monad ((>=>))
-
-import Paths_axel (getDataFileName)
-
-import System.FilePath ((</>))
-
-import qualified System.IO.Strict as S (readFile)
-
-newtype Resource =
-  Resource String
-
-getResourcePath :: Resource -> IO FilePath
-getResourcePath (Resource resource) = getDataFileName $ "resources" </> resource
-
-readResource :: Resource -> IO String
-readResource = getResourcePath >=> S.readFile
-
-astDefinition :: Resource
-astDefinition = Resource "autogenerated/macros/AST.hs"
-
-macroDefinitionAndEnvironmentHeader :: Resource
-macroDefinitionAndEnvironmentHeader =
-  Resource "macros/MacroDefinitionAndEnvironmentHeader.hs"
-
-macroScaffold :: Resource
-macroScaffold = Resource "macros/Scaffold.hs"
diff --git a/test/Axel/Test/ASTGen.hs b/test/Axel/Test/ASTGen.hs
new file mode 100644
--- /dev/null
+++ b/test/Axel/Test/ASTGen.hs
@@ -0,0 +1,130 @@
+module Axel.Test.ASTGen where
+
+import qualified Axel.AST as AST
+
+import Hedgehog
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+
+genIdentifier :: (MonadGen m) => m AST.Identifier
+genIdentifier = Gen.string (Range.linear 1 5) Gen.alpha
+
+genLiteral :: (MonadGen m) => m AST.Literal
+genLiteral =
+  Gen.choice
+    [ AST.LChar <$> Gen.unicode
+    , AST.LInt <$> Gen.int Range.constantBounded
+    , AST.LString <$> Gen.string (Range.linear 0 5) Gen.unicode
+    ]
+
+genCaseBlock :: (MonadGen m) => m AST.CaseBlock
+genCaseBlock =
+  AST.CaseBlock <$> genExpression <*>
+  Gen.list (Range.linear 0 3) ((,) <$> genExpression <*> genExpression)
+
+genFunctionApplication :: (MonadGen m) => m AST.FunctionApplication
+genFunctionApplication =
+  AST.FunctionApplication <$> genExpression <*>
+  Gen.list (Range.linear 0 3) genExpression
+
+genLambda :: (MonadGen m) => m AST.Lambda
+genLambda =
+  AST.Lambda <$> Gen.list (Range.linear 0 3) genExpression <*> genExpression
+
+genLetBlock :: (MonadGen m) => m AST.LetBlock
+genLetBlock =
+  AST.LetBlock <$>
+  Gen.list (Range.linear 0 3) ((,) <$> genExpression <*> genExpression) <*>
+  genExpression
+
+genExpression :: (MonadGen m) => m AST.Expression
+genExpression =
+  Gen.recursive
+    Gen.choice
+    [pure AST.EEmptySExpression, AST.EIdentifier <$> genIdentifier]
+    [ AST.ECaseBlock <$> genCaseBlock
+    , AST.EFunctionApplication <$> genFunctionApplication
+    , AST.ELambda <$> genLambda
+    , AST.ELetBlock <$> genLetBlock
+    , AST.ELiteral <$> genLiteral
+    ]
+
+genTypeDefinition :: (MonadGen m) => m AST.TypeDefinition
+genTypeDefinition =
+  Gen.choice
+    [ AST.ProperType <$> genIdentifier
+    , AST.TypeConstructor <$> genFunctionApplication
+    ]
+
+genDataDeclaration :: (MonadGen m) => m AST.DataDeclaration
+genDataDeclaration =
+  AST.DataDeclaration <$> genTypeDefinition <*>
+  Gen.list (Range.linear 0 3) genFunctionApplication
+
+genFunctionDefinition :: (MonadGen m) => m AST.FunctionDefinition
+genFunctionDefinition =
+  AST.FunctionDefinition <$> genIdentifier <*>
+  Gen.list (Range.linear 0 3) genExpression <*>
+  genExpression
+
+genPragma :: (MonadGen m) => m AST.Pragma
+genPragma = AST.Pragma <$> Gen.string (Range.linear 0 10) Gen.ascii
+
+genMacroDefinition :: (MonadGen m) => m AST.MacroDefinition
+genMacroDefinition = AST.MacroDefinition <$> genFunctionDefinition
+
+genImport :: (MonadGen m) => m AST.Import
+genImport =
+  Gen.choice
+    [ AST.ImportItem <$> genIdentifier
+    , AST.ImportType <$> genIdentifier <*>
+      Gen.list (Range.linear 0 3) genIdentifier
+    ]
+
+genImportSpecification :: (MonadGen m) => m AST.ImportSpecification
+genImportSpecification =
+  Gen.choice
+    [ pure AST.ImportAll
+    , AST.ImportOnly <$> Gen.list (Range.linear 0 3) genImport
+    ]
+
+genQualifiedImport :: (MonadGen m) => m AST.QualifiedImport
+genQualifiedImport =
+  AST.QualifiedImport <$> genIdentifier <*> genIdentifier <*>
+  genImportSpecification
+
+genRestrictedImport :: (MonadGen m) => m AST.RestrictedImport
+genRestrictedImport =
+  AST.RestrictedImport <$> genIdentifier <*> genImportSpecification
+
+genTopLevel :: (MonadGen m) => m AST.TopLevel
+genTopLevel = AST.TopLevel <$> Gen.list (Range.linear 0 3) genStatement
+
+genTypeclassInstance :: (MonadGen m) => m AST.TypeclassInstance
+genTypeclassInstance =
+  AST.TypeclassInstance <$> genExpression <*>
+  Gen.list (Range.linear 0 3) genFunctionDefinition
+
+genTypeSignature :: (MonadGen m) => m AST.TypeSignature
+genTypeSignature = AST.TypeSignature <$> genIdentifier <*> genExpression
+
+genTypeSynonym :: (MonadGen m) => m AST.TypeSynonym
+genTypeSynonym = AST.TypeSynonym <$> genExpression <*> genExpression
+
+genStatement :: (MonadGen m) => m AST.Statement
+genStatement =
+  Gen.recursive
+    Gen.choice
+    [ AST.SDataDeclaration <$> genDataDeclaration
+    , AST.SFunctionDefinition <$> genFunctionDefinition
+    , AST.SPragma <$> genPragma
+    , AST.SMacroDefinition <$> genMacroDefinition
+    , AST.SModuleDeclaration <$> genIdentifier
+    , AST.SQualifiedImport <$> genQualifiedImport
+    , AST.SRestrictedImport <$> genRestrictedImport
+    , AST.STypeclassInstance <$> genTypeclassInstance
+    , AST.STypeSignature <$> genTypeSignature
+    , AST.STypeSynonym <$> genTypeSynonym
+    , AST.SUnrestrictedImport <$> genIdentifier
+    ]
+    [AST.STopLevel <$> genTopLevel]
diff --git a/test/Axel/Test/DenormalizeSpec.hs b/test/Axel/Test/DenormalizeSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Axel/Test/DenormalizeSpec.hs
@@ -0,0 +1,20 @@
+module Axel.Test.DenormalizeSpec where
+
+import Axel.Denormalize
+import Axel.Normalize
+import qualified Axel.Test.ASTGen as ASTGen
+import Axel.Test.MockUtils
+
+import Hedgehog
+
+hprop_normalizeExpression_is_the_inverse_of_denormalizeExpression :: Property
+hprop_normalizeExpression_is_the_inverse_of_denormalizeExpression =
+  property $ do
+    expr <- forAll ASTGen.genExpression
+    expr === unwrapRight (normalizeExpression (denormalizeExpression expr))
+
+hprop_normalizeStatement_is_the_inverse_of_denormalizeStatement :: Property
+hprop_normalizeStatement_is_the_inverse_of_denormalizeStatement =
+  property $ do
+    stmt <- forAll ASTGen.genStatement
+    stmt === unwrapRight (normalizeStatement (denormalizeStatement stmt))
diff --git a/test/Axel/Test/File/FileSpec.hs b/test/Axel/Test/File/FileSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Axel/Test/File/FileSpec.hs
@@ -0,0 +1,25 @@
+module Axel.Test.File.FileSpec where
+
+import Axel.Error as Error
+import Axel.Haskell.File
+import Axel.Monad.FileSystem as FS
+
+import Data.ByteString.Lazy.Char8 as C
+
+import System.FilePath
+
+import Test.Tasty
+import Test.Tasty.Golden
+
+test_transpileSource_golden :: IO TestTree
+test_transpileSource_golden = do
+  axelFiles <- findByExtension [".axel_golden"] "test/Axel/Test/File"
+  pure $
+    testGroup "`transpileSource` golden tests" $ do
+      axelFile <- axelFiles
+      let hsFile = replaceExtension axelFile ".hs_golden"
+      pure $
+        goldenVsString
+          (takeBaseName axelFile)
+          hsFile
+          (C.pack <$> Error.toIO (FS.readFile axelFile >>= transpileSource))
diff --git a/test/Axel/Test/Haskell/GHCSpec.hs b/test/Axel/Test/Haskell/GHCSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Axel/Test/Haskell/GHCSpec.hs
@@ -0,0 +1,76 @@
+{-# LANGUAGE FlexibleContexts #-}
+
+module Axel.Test.Haskell.GHCSpec where
+
+import Axel.Haskell.GHC as GHC
+import Axel.Haskell.Stack as Stack
+import qualified Axel.Test.Monad.FileSystemMock as Mock
+import qualified Axel.Test.Monad.ProcessMock as Mock
+
+import Control.Lens
+import Control.Monad.Except
+
+import System.Exit
+
+import Test.Tasty.Hspec
+
+{-# ANN module "HLint: ignore Redundant do" #-}
+
+spec_GHC :: SpecWith ()
+spec_GHC = do
+  describe "ghcCompile" $ do
+    it "compiles a file with GHC" $ do
+      let action = GHC.ghcCompile "projectFoo/app/Main.hs"
+      let origFSState = Mock.mkFSState []
+      let origProcState =
+            Mock.mkProcessState
+              []
+              [ Mock.ProcessResultT
+                  ((ExitSuccess, Just ("testStdout", "")), pure ())
+              ]
+      let expectation stdout (procState, _) = do
+            stdout `shouldBe` "testStdout"
+            procState ^. Mock.procExecutionLog `shouldBe`
+              [ ( "stack"
+                , [ "--resolver"
+                  , Stack.stackageResolverWithAxel
+                  , "ghc"
+                  , "--"
+                  , "-v0"
+                  , "-ddump-json"
+                  , "projectFoo/app/Main.hs"
+                  ]
+                , Just "")
+              ]
+      case Mock.runProcess (origProcState, origFSState) $ runExceptT action of
+        Left err -> expectationFailure err
+        Right (Left err, _) -> expectationFailure err
+        Right (Right x, state) -> expectation x state
+  describe "ghcInterpret" $ do
+    it "interprets a file with GHC" $ do
+      let action = GHC.ghcInterpret "projectFoo/app/Main.hs"
+      let origFSState = Mock.mkFSState []
+      let origProcState =
+            Mock.mkProcessState
+              []
+              [ Mock.ProcessResultT
+                  ((ExitSuccess, Just ("testStdout", "")), pure ())
+              ]
+      let expectation stdout (procState, _) = do
+            stdout `shouldBe` "testStdout"
+            procState ^. Mock.procExecutionLog `shouldBe`
+              [ ( "stack"
+                , [ "--resolver"
+                  , stackageResolverWithAxel
+                  , "runghc"
+                  , "--package"
+                  , axelStackageId
+                  , "--"
+                  , "projectFoo/app/Main.hs"
+                  ]
+                , Just "")
+              ]
+      case Mock.runProcess (origProcState, origFSState) $ runExceptT action of
+        Left err -> expectationFailure err
+        Right (Left err, _) -> expectationFailure err
+        Right (Right x, state) -> expectation x state
diff --git a/test/Axel/Test/Haskell/StackSpec.hs b/test/Axel/Test/Haskell/StackSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Axel/Test/Haskell/StackSpec.hs
@@ -0,0 +1,115 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE GADTs #-}
+
+module Axel.Test.Haskell.StackSpec where
+
+import Axel.Haskell.Stack as Stack
+import Axel.Monad.FileSystem as FS
+import Axel.Test.Monad.ConsoleMock as Mock
+import Axel.Test.Monad.FileSystemMock as Mock
+import Axel.Test.Monad.ProcessMock as Mock
+
+import Control.Lens
+import Control.Monad.Except
+
+import System.Exit
+
+import Test.Tasty.Hspec
+
+{-# ANN module "HLint: ignore Redundant do" #-}
+
+spec_Stack :: SpecWith ()
+spec_Stack = do
+  describe "addStackDependency" $ do
+    it "adds a Stackage dependency to a Stack project" $ do
+      let action = Stack.addStackDependency "foo-1.2.3.4" "project/path"
+      let origFSState =
+            Mock.mkFSState
+              [ Mock.Directory
+                  "project"
+                  [ Mock.Directory
+                      "path"
+                      [Mock.File "package.yaml" "dependencies:\n- asdf-5.6.7"]
+                  ]
+              ]
+      let expectedFSState =
+            origFSState & Mock.fsRoot . at "project/path/package.yaml" ?~
+            Mock.File
+              "package.yaml"
+              "dependencies:\n- foo-1.2.3.4\n- asdf-5.6.7\n"
+      case Mock.runFileSystem origFSState action of
+        Left err -> expectationFailure err
+        Right ((), result) -> result `shouldBe` expectedFSState
+  describe "buildStackProject" $ do
+    it "builds a Stack project" $ do
+      let action = Stack.buildStackProject "project/path"
+      let origFSState =
+            Mock.mkFSState [Mock.Directory "project" [Mock.Directory "path" []]]
+      let origProcState =
+            Mock.mkProcessState
+              []
+              [ProcessResultT ((ExitSuccess, Just ("", "")), pure ())]
+      let expectation result (procState, fsState) = do
+            result `shouldBe` ()
+            procState ^. Mock.procExecutionLog `shouldBe`
+              [("stack", ["build"], Just "")]
+            fsState ^. Mock.fsCurrentDirectory `shouldBe` "/"
+      case Mock.runProcess (origProcState, origFSState) $ runExceptT action of
+        Left err -> expectationFailure err
+        Right (Left err, _) -> expectationFailure $ show err
+        Right (Right x, state) -> expectation x state
+  describe "createStackProject" $ do
+    it "creates a new Stack project" $ do
+      let action = Stack.createStackProject "newProject"
+      let origFSState = Mock.mkFSState []
+      let origProcState =
+            Mock.mkProcessState
+              []
+              [ ProcessResultT
+                  ( (ExitSuccess, Just ("", ""))
+                  , FS.createDirectoryIfMissing False "newProject")
+              , ProcessResultT ((ExitSuccess, Just ("", "")), pure ())
+              ]
+      let expectation ((), (procState, fsState)) = do
+            procState ^. Mock.procExecutionLog `shouldBe`
+              [ ("stack", ["new", "newProject", "new-template"], Just "")
+              , ( "stack"
+                , ["config", "set", "resolver", stackageResolverWithAxel]
+                , Just "")
+              ]
+            fsState ^. Mock.fsCurrentDirectory `shouldBe` "/"
+            fsState ^. Mock.fsRoot . at "newProject" . _Just . Mock.fsPath `shouldBe`
+              "newProject"
+      case Mock.runProcess (origProcState, origFSState) action of
+        Left err -> expectationFailure err
+        Right result -> expectation result
+  describe "runStackProject" $ do
+    it "runs a Stack project" $ do
+      let action = Stack.runStackProject "project/path"
+      let origConsoleState = Mock.mkConsoleState
+      let origFSState =
+            Mock.mkFSState [Mock.Directory "project" [Mock.Directory "path" []]]
+      let origProcState =
+            Mock.mkProcessState
+              []
+              [ ProcessResultT
+                  ( ( ExitSuccess
+                    , Just ("", "foo:lib\nfoo:exe:foo-exe\nfoo:test:foo-test"))
+                  , pure ())
+              , ProcessResultT ((ExitSuccess, Nothing), pure ())
+              ]
+      let expectation result (consoleState, procState, fsState) = do
+            result `shouldBe` ()
+            procState ^. Mock.procExecutionLog `shouldBe`
+              [ ("stack", ["ide", "targets"], Just "")
+              , ("stack", ["exec", "foo-exe"], Nothing)
+              ]
+            fsState ^. Mock.fsCurrentDirectory `shouldBe` "/"
+            consoleState ^. Mock.consoleOutput `shouldBe` "Running foo-exe...\n"
+      case Mock.runProcess (origProcState, origFSState) $
+           Mock.runConsoleT origConsoleState $
+           runExceptT action of
+        Left err -> expectationFailure err
+        Right ((Left err, _), _) -> expectationFailure $ show err
+        Right ((Right x, consoleState), (procState, fsState)) ->
+          expectation x (consoleState, procState, fsState)
diff --git a/test/Axel/Test/MockUtils.hs b/test/Axel/Test/MockUtils.hs
new file mode 100644
--- /dev/null
+++ b/test/Axel/Test/MockUtils.hs
@@ -0,0 +1,35 @@
+{-# OPTIONS_GHC "-fno-warn-incomplete-patterns" #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Axel.Test.MockUtils where
+
+import Control.Monad.Except
+import Control.Monad.State.Lazy
+
+import GHC.Exts (IsString(fromString))
+
+import Language.Haskell.TH.Quote
+
+throwInterpretError ::
+     (MonadError String m, MonadState s m, Show s) => String -> String -> m a
+throwInterpretError actionName message = do
+  errorMsg <-
+    gets $ \ctxt ->
+      "\n----------\nACTION\t" <> actionName <> "\n\nMESSAGE\t" <> message <>
+      "\n\nSTATE\t" <>
+      show ctxt <>
+      "\n----------\n"
+  throwError errorMsg
+
+-- Adapted from http://hackage.haskell.org/package/string-quote-0.0.1/docs/src/Data-String-Quote.html#s.
+s :: QuasiQuoter
+s =
+  QuasiQuoter
+    ((\a -> [|fromString a|]) . filter (/= '\r'))
+    (error "Cannot use s as a pattern")
+    (error "Cannot use s as a type")
+    (error "Cannot use s as a dec")
+
+unwrapRight :: Either b a -> a
+unwrapRight (Right x) = x
diff --git a/test/Axel/Test/Monad/ConsoleMock.hs b/test/Axel/Test/Monad/ConsoleMock.hs
new file mode 100644
--- /dev/null
+++ b/test/Axel/Test/Monad/ConsoleMock.hs
@@ -0,0 +1,46 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Axel.Test.Monad.ConsoleMock where
+
+import Axel.Monad.Console as Console
+import Axel.Monad.FileSystem as FS
+import Axel.Monad.Process as Proc
+import Axel.Monad.Resource as Res
+
+import Control.Lens
+import Control.Monad.State.Lazy
+
+newtype ConsoleState = ConsoleState
+  { _consoleOutput :: String
+  } deriving (Eq, Show)
+
+makeFieldsNoPrefix ''ConsoleState
+
+mkConsoleState :: ConsoleState
+mkConsoleState = ConsoleState {_consoleOutput = ""}
+
+newtype ConsoleT m a =
+  ConsoleT (StateT ConsoleState m a)
+  deriving ( Functor
+           , Applicative
+           , Monad
+           , MonadTrans
+           , MonadFileSystem
+           , MonadProcess
+           , MonadResource
+           )
+
+type Console = ConsoleT Identity
+
+instance (Monad m) => MonadConsole (ConsoleT m) where
+  putStr str = ConsoleT $ consoleOutput <>= str
+
+runConsoleT :: ConsoleState -> ConsoleT m a -> m (a, ConsoleState)
+runConsoleT origState (ConsoleT x) = runStateT x origState
+
+runConsole :: ConsoleState -> Console a -> (a, ConsoleState)
+runConsole origState x = runIdentity $ runConsoleT origState x
diff --git a/test/Axel/Test/Monad/ConsoleSpec.hs b/test/Axel/Test/Monad/ConsoleSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Axel/Test/Monad/ConsoleSpec.hs
@@ -0,0 +1,19 @@
+module Axel.Test.Monad.ConsoleSpec where
+
+import qualified Axel.Monad.Console as Console
+import qualified Axel.Test.Monad.ConsoleMock as Mock
+
+import Test.Tasty.Hspec
+
+{-# ANN module "HLint: ignore Redundant do" #-}
+
+spec_Console :: SpecWith ()
+spec_Console =
+  describe "putStrLn" $ do
+    it "prints to the console with a trailing newline" $ do
+      let action = Console.putStrLn "line1\nline2"
+      let origState = Mock.mkConsoleState
+      let expected = Mock.ConsoleState {Mock._consoleOutput = "line1\nline2\n"}
+      case Mock.runConsoleT origState action of
+        Left err -> expectationFailure err
+        Right result -> result `shouldBe` ((), expected)
diff --git a/test/Axel/Test/Monad/FileSystemMock.hs b/test/Axel/Test/Monad/FileSystemMock.hs
new file mode 100644
--- /dev/null
+++ b/test/Axel/Test/Monad/FileSystemMock.hs
@@ -0,0 +1,278 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE InstanceSigs #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Axel.Test.Monad.FileSystemMock where
+
+import Axel.Monad.Console as Console
+import Axel.Monad.FileSystem as FS
+import Axel.Monad.Process as Proc
+import Axel.Monad.Resource as Res
+import Axel.Test.MockUtils
+
+import Control.Lens hiding (children)
+import Control.Monad.Except
+import Control.Monad.State.Lazy
+
+import Data.List.Split
+import Data.Maybe
+
+import System.FilePath
+
+data FSNode
+  = Directory FilePath
+              [FSNode]
+  | File FilePath
+         String
+  deriving (Eq, Show)
+
+fsPath :: Lens' FSNode FilePath
+fsPath =
+  lens
+    (\case
+       Directory path _ -> path
+       File path _ -> path)
+    (\node newPath ->
+       case node of
+         Directory _ children -> Directory newPath children
+         File _ contents -> File newPath contents)
+
+data FSState = FSState
+  { _fsCurrentDirectory :: FilePath
+  , _fsRoot :: FSNode
+  , _fsTempCounter :: Int
+  } deriving (Eq, Show)
+
+makeFieldsNoPrefix ''FSState
+
+mkFSState :: [FSNode] -> FSState
+mkFSState rootContents =
+  FSState
+    { _fsCurrentDirectory = "/"
+    , _fsRoot = Directory "/" rootContents
+    , _fsTempCounter = 0
+    }
+
+lookupNode :: [FilePath] -> FSNode -> Maybe FSNode
+lookupNode _ (File _ _) = Nothing
+lookupNode [] _ = Nothing
+lookupNode ["."] rootNode = pure rootNode
+lookupNode [segment] (Directory _ rootChildren) =
+  let matchingChildren =
+        filter (\child -> child ^. fsPath == segment) rootChildren
+   in case matchingChildren of
+        [child] -> pure child
+        _ -> Nothing
+lookupNode (segment:xs) rootNode@(Directory _ _) = do
+  child@(Directory _ _) <- lookupNode [segment] rootNode
+  lookupNode xs child
+
+deleteNode :: [FilePath] -> FSNode -> Maybe FSNode
+deleteNode _ (File _ _) = Nothing
+deleteNode [] _ = Nothing
+deleteNode [segment] rootNode@(Directory rootPath rootChildren) = do
+  child <- lookupNode [segment] rootNode
+  let rootChildren' = filter (/= child) rootChildren
+  pure $ Directory rootPath rootChildren'
+deleteNode (segment:xs) rootNode@(Directory rootPath rootChildren) = do
+  needle <- lookupNode [segment] rootNode
+  rootChildren' <-
+    mapM
+      (\child ->
+         case child of
+           File _ _ -> pure child
+           _ ->
+             if child == needle
+               then deleteNode xs child
+               else pure child)
+      rootChildren
+  pure $ Directory rootPath rootChildren'
+
+insertNode :: [FilePath] -> FSNode -> FSNode -> Maybe FSNode
+insertNode _ _ (File _ _) = Nothing
+insertNode [] _ _ = Nothing
+insertNode [segment] newNode (Directory rootPath rootChildren) =
+  if newNode ^. fsPath == segment
+    then let rootChildren' =
+               filter
+                 (\child -> child ^. fsPath /= newNode ^. fsPath)
+                 rootChildren
+          in pure $ Directory rootPath (newNode : rootChildren')
+    else Nothing
+insertNode (segment:xs) newNode rootNode@(Directory rootPath rootChildren) = do
+  needle <- lookupNode [segment] rootNode
+  rootChildren' <-
+    mapM
+      (\child ->
+         case child of
+           File _ _ -> pure child
+           _ ->
+             if child == needle
+               then insertNode xs newNode child
+               else pure child)
+      rootChildren
+  pure $ Directory rootPath rootChildren'
+
+type instance Index FSNode = FilePath
+
+type instance IxValue FSNode = FSNode
+
+instance Ixed FSNode where
+  ix ::
+       (Applicative f) => FilePath -> (FSNode -> f FSNode) -> FSNode -> f FSNode
+  ix path f root =
+    case lookupNode pathSegments root of
+      Just node ->
+        f node <&> \newNode -> fromJust $ insertNode pathSegments newNode root
+      Nothing -> pure root
+    where
+      pathSegments :: [FilePath]
+      pathSegments = splitDirectories path
+
+instance At FSNode where
+  at :: FilePath -> Lens' FSNode (Maybe FSNode)
+  at path =
+    let setter :: FSNode -> Maybe FSNode -> FSNode
+        setter root =
+          fromMaybe root . \case
+            Just newNode -> insertNode pathSegments newNode root
+            Nothing -> deleteNode pathSegments root
+     in lens (lookupNode pathSegments) setter
+    where
+      pathSegments :: [FilePath]
+      pathSegments = splitDirectories path
+
+absifyPath :: FilePath -> FilePath -> FilePath
+absifyPath relativePath currentDirectory =
+  let absolutePath =
+        case relativePath of
+          '/':_ -> relativePath
+          _ -> "/" </> currentDirectory </> relativePath
+      normalizedSegments =
+        concatMap
+          (\case
+             [_, ".."] -> []
+             ["."] -> []
+             xs -> xs) $
+        chunksOf 2 $
+        dropLeadingSlash $
+        splitDirectories absolutePath
+   in case joinPath normalizedSegments of
+        "" -> "/"
+        path -> path
+  where
+    dropLeadingSlash ("/":xs) = xs
+    dropLeadingSlash xs = xs
+
+absify :: (MonadState FSState f) => FilePath -> f FilePath
+absify relativePath = absifyPath relativePath <$> gets (^. fsCurrentDirectory)
+
+newtype FileSystemT m a =
+  FileSystemT (StateT FSState (ExceptT String m) a)
+  deriving ( Functor
+           , Applicative
+           , Monad
+           , MonadConsole
+           , MonadProcess
+           , MonadResource
+           )
+
+type FileSystem = FileSystemT Identity
+
+instance Eq (FileSystemT m a) where
+  _ == _ = False
+
+instance Show (FileSystemT m a) where
+  show _ = "<FileSystemT>"
+
+instance MonadTrans FileSystemT where
+  lift = FileSystemT . lift . lift
+
+instance (Monad m) => MonadFileSystem (FileSystemT m) where
+  copyFile src dest = do
+    contents <- FS.readFile src
+    FS.writeFile contents dest
+  createDirectoryIfMissing createParents relativePath =
+    FileSystemT $ do
+      path <- absify relativePath
+      parentNode <- gets (^. fsRoot . at (takeDirectory path))
+      case parentNode of
+        Just _ -> fsRoot . at path ?= Directory (takeFileName path) []
+        Nothing ->
+          if createParents
+            then fsRoot %= \origRoot ->
+                   fst
+                     (foldl
+                        (\(root, segments) pathSegment ->
+                           let newSegments = segments <> [pathSegment]
+                               newRoot =
+                                 case root ^. at (joinPath newSegments) of
+                                   Just _ -> root
+                                   Nothing ->
+                                     root & at (joinPath newSegments) ?~
+                                     Directory pathSegment []
+                            in (newRoot, newSegments))
+                        (origRoot, [])
+                        (splitDirectories path))
+            else throwInterpretError
+                   "createDirectoryIfMissing"
+                   ("Missing parents for directory: " <> path)
+  doesDirectoryExist relativePath =
+    FileSystemT $ do
+      path <- absify relativePath
+      directoryNode <- gets (^. fsRoot . at path)
+      pure $
+        case directoryNode of
+          Just (Directory _ _) -> True
+          _ -> False
+  getCurrentDirectory = FileSystemT $ gets (^. fsCurrentDirectory)
+  getDirectoryContents relativePath =
+    FileSystemT $ do
+      path <- absify relativePath
+      directoryNode <- gets (^. fsRoot . at path)
+      case directoryNode of
+        Just (Directory _ children) -> pure $ map (^. fsPath) children
+        _ ->
+          throwInterpretError
+            "getDirectoryContents"
+            ("No such directory: " <> path)
+  getTemporaryDirectory = do
+    tempCounter <- FileSystemT $ fsTempCounter <<+= 1
+    let tempDirName = "/tmp" </> show tempCounter
+    FS.createDirectoryIfMissing True tempDirName
+    pure tempDirName
+  readFile relativePath =
+    FileSystemT $ do
+      path <- absify relativePath
+      fileNode <- gets (^. fsRoot . at path)
+      case fileNode of
+        Just (File _ contents) -> pure contents
+        _ -> throwInterpretError "readFile" ("No such file: " <> path)
+  removeFile relativePath =
+    FileSystemT $ do
+      path <- absify relativePath
+      gets (deleteNode (splitDirectories path) . (^. fsRoot)) >>= \case
+        Just newRoot -> fsRoot .= newRoot
+        Nothing -> throwInterpretError "removeFile" ("No such file: " <> path)
+  setCurrentDirectory relativePath =
+    FileSystemT $ do
+      path <- absify relativePath
+      fsCurrentDirectory .= path
+  writeFile relativePath contents =
+    FileSystemT $ do
+      path <- absify relativePath
+      fsRoot . at path ?= File (takeFileName path) contents
+
+runFileSystemT :: FSState -> FileSystemT m a -> ExceptT String m (a, FSState)
+runFileSystemT origState (FileSystemT x) = runStateT x origState
+
+runFileSystem :: FSState -> FileSystem a -> Either String (a, FSState)
+runFileSystem origState x = runExcept $ runFileSystemT origState x
diff --git a/test/Axel/Test/Monad/FileSystemSpec.hs b/test/Axel/Test/Monad/FileSystemSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Axel/Test/Monad/FileSystemSpec.hs
@@ -0,0 +1,74 @@
+module Axel.Test.Monad.FileSystemSpec where
+
+import qualified Axel.Monad.FileSystem as FS
+import qualified Axel.Test.Monad.FileSystemMock as Mock
+
+import Control.Lens
+
+import System.FilePath
+
+import Test.Tasty.Hspec
+
+{-# ANN module "HLint: ignore Redundant do" #-}
+
+spec_FileSystem :: SpecWith ()
+spec_FileSystem = do
+  describe "getDirectoryContentsRec" $ do
+    it "gets the files inside a directory and all its subdirectories" $ do
+      let action = FS.getDirectoryContentsRec "dir1"
+      let origState =
+            Mock.mkFSState
+              [ Mock.Directory
+                  "dir1"
+                  [ Mock.File "file1" ""
+                  , Mock.Directory
+                      "dir2"
+                      [Mock.File "file2" "", Mock.Directory "dir3" []]
+                  ]
+              ]
+      let expected = ["dir1/file1", "dir1/dir2/file2"]
+      case Mock.runFileSystem origState action of
+        Left err -> expectationFailure err
+        Right result -> result `shouldBe` (expected, origState)
+  describe "withCurrentDirectory" $ do
+    it "changes the directory and resets it afterwards" $ do
+      let action = do
+            FS.withCurrentDirectory "inside/subdir" $
+              FS.writeFile "insideDir" "insideDirContents"
+            FS.writeFile "outsideDir" "outsideDirContents"
+      let origState =
+            Mock.mkFSState
+              [ Mock.Directory
+                  "inside"
+                  [ Mock.Directory "subdir" [Mock.File "bar" "barContents"]
+                  , Mock.File "foo" "fooContents"
+                  ]
+              ]
+      let expected =
+            origState &
+            (Mock.fsRoot . at "inside/subdir/insideDir" ?~
+             Mock.File "insideDir" "insideDirContents") &
+            (Mock.fsRoot . at "outsideDir" ?~
+             Mock.File "outsideDir" "outsideDirContents")
+      case Mock.runFileSystem origState action of
+        Left err -> expectationFailure err
+        Right result -> result `shouldBe` ((), expected)
+  describe "withTemporaryDirectory" $ do
+    it
+      "creates a temporary directory and resets the current directory afterwards" $ do
+      let action = do
+            FS.withTemporaryDirectory $ \tempDir ->
+              FS.writeFile (tempDir </> "insideTemp0") "insideTemp0Contents"
+            FS.withTemporaryDirectory $ \tempDir ->
+              FS.writeFile (tempDir </> "insideTemp1") "insideTemp1Contents"
+      let origState = Mock.mkFSState []
+      let expected =
+            origState & (Mock.fsRoot . at "tmp" ?~ Mock.Directory "tmp" []) &
+            (Mock.fsRoot . at "tmp/0" ?~
+             Mock.Directory "0" [Mock.File "insideTemp0" "insideTemp0Contents"]) &
+            (Mock.fsRoot . at "tmp/1" ?~
+             Mock.Directory "1" [Mock.File "insideTemp1" "insideTemp1Contents"]) &
+            (Mock.fsTempCounter .~ 2)
+      case Mock.runFileSystem origState action of
+        Left err -> expectationFailure err
+        Right result -> result `shouldBe` ((), expected)
diff --git a/test/Axel/Test/Monad/ProcessMock.hs b/test/Axel/Test/Monad/ProcessMock.hs
new file mode 100644
--- /dev/null
+++ b/test/Axel/Test/Monad/ProcessMock.hs
@@ -0,0 +1,128 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE PartialTypeSignatures #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Axel.Test.Monad.ProcessMock where
+
+import Axel.Monad.Console as Console
+import Axel.Monad.FileSystem as FS
+import Axel.Monad.Process as Proc
+import Axel.Monad.Resource as Res
+import Axel.Test.MockUtils
+import Axel.Test.Monad.FileSystemMock as Mock
+
+import Control.Lens
+import Control.Monad.Except
+import Control.Monad.State.Lazy
+
+import System.Exit
+
+newtype ProcessResultT m =
+  ProcessResultT ((ExitCode, Maybe (String, String)), Mock.FileSystemT m ())
+  deriving (Eq, Show)
+
+data ProcessStateT m = ProcessStateT
+  { _procMockArgs :: [String]
+  , _procExecutionLog :: [(String, [String], Maybe String)]
+  , _procMockResults :: [ProcessResultT m]
+  } deriving (Eq, Show)
+
+makeFieldsNoPrefix ''ProcessStateT
+
+type ProcessState = ProcessStateT Identity
+
+mkProcessState :: [String] -> [ProcessResultT m] -> ProcessStateT m
+mkProcessState mockArgs mockResults =
+  ProcessStateT
+    { _procMockArgs = mockArgs
+    , _procExecutionLog = []
+    , _procMockResults = mockResults
+    }
+
+newtype ProcessT m a =
+  ProcessT (StateT (ProcessStateT m) (ExceptT String (Mock.FileSystemT m)) a)
+  deriving ( Functor
+           , Applicative
+           , Monad
+           , MonadConsole
+           , MonadFileSystem
+           , MonadResource
+           )
+
+type Process = ProcessT Identity
+
+deriving instance
+         (Eq
+            (StateT (ProcessStateT m) (ExceptT String (Mock.FileSystemT m))
+               a)) =>
+         Eq (ProcessT m a)
+
+deriving instance
+         (Show
+            (StateT (ProcessStateT m) (ExceptT String (Mock.FileSystemT m))
+               a)) =>
+         Show (ProcessT m a)
+
+instance MonadTrans ProcessT where
+  lift = ProcessT . lift . lift . lift
+
+instance (Monad m) => MonadProcess (ProcessT m) where
+  getArgs = ProcessT $ gets (^. procMockArgs)
+  runProcess cmd args stdin =
+    ProcessT $ do
+      procExecutionLog %= (|> (cmd, args, Just stdin))
+      gets (uncons . (^. procMockResults)) >>= \case
+        Just (ProcessResultT (mockResult, fsAction), newMockResults) -> do
+          procMockResults .= newMockResults
+          case mockResult of
+            (exitCode, Just (stdout, stderr)) ->
+              lift (lift fsAction) >> pure (exitCode, stdout, stderr)
+            _ ->
+              throwInterpretError
+                "RunProcess"
+                ("Wrong type for mock result: " <> show mockResult)
+        Nothing -> throwInterpretError "runProcess" "No mock result available"
+  runProcessInheritingStreams cmd args =
+    ProcessT $ do
+      procExecutionLog %= (|> (cmd, args, Nothing))
+      gets (uncons . (^. procMockResults)) >>= \case
+        Just (ProcessResultT (mockResult, fsAction), newMockResults) -> do
+          procMockResults .= newMockResults
+          case mockResult of
+            (exitCode, Nothing) -> pure fsAction >> pure exitCode
+            _ ->
+              throwInterpretError
+                "runProcessInheritingStreams"
+                ("Wrong type for mock result: " <> show mockResult)
+        Nothing ->
+          throwInterpretError
+            "RunProcessInheritingStreams"
+            "No mock result available"
+
+runProcessT ::
+     forall a m. (Monad m)
+  => (ProcessStateT m, Mock.FSState)
+  -> ProcessT m a
+  -> ExceptT String m (a, (ProcessStateT m, Mock.FSState))
+runProcessT (origState, origFSState) (ProcessT x) =
+  ExceptT $
+  runExceptT (runFileSystemT origFSState $ runExceptT $ runStateT x origState) <&> \case
+    Left err -> Left err
+    Right (x'', fsState) ->
+      case x'' of
+        Left err -> Left err
+        Right (x''', procState) -> Right (x''', (procState, fsState))
+
+runProcess ::
+     (ProcessState, Mock.FSState)
+  -> Process a
+  -> Either String (a, (ProcessState, Mock.FSState))
+runProcess origState x = runExcept $ runProcessT origState x
diff --git a/test/Axel/Test/Monad/ResourceMock.hs b/test/Axel/Test/Monad/ResourceMock.hs
new file mode 100644
--- /dev/null
+++ b/test/Axel/Test/Monad/ResourceMock.hs
@@ -0,0 +1,40 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+module Axel.Test.Monad.ResourceMock where
+
+import Axel.Monad.Console as Console
+import Axel.Monad.FileSystem as FS
+import Axel.Monad.Process as Proc
+import Axel.Monad.Resource as Res
+
+import Control.Monad.Identity
+import Control.Monad.Trans
+
+import System.FilePath
+
+newtype ResourceT m a =
+  ResourceT (m a)
+  deriving ( Functor
+           , Applicative
+           , Monad
+           , MonadConsole
+           , MonadFileSystem
+           , MonadProcess
+           )
+
+type Resource = ResourceT Identity
+
+instance MonadTrans ResourceT where
+  lift = ResourceT
+
+instance (Monad m) => MonadResource (ResourceT m) where
+  getResourcePath (Res.ResourceId resourceId) =
+    ResourceT $ pure ("resources" </> resourceId)
+
+runResourceT :: ResourceT m a -> m a
+runResourceT (ResourceT x) = x
+
+runResource :: Resource a -> a
+runResource = runIdentity . runResourceT
diff --git a/test/Axel/Test/Monad/ResourceSpec.hs b/test/Axel/Test/Monad/ResourceSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Axel/Test/Monad/ResourceSpec.hs
@@ -0,0 +1,27 @@
+{-# LANGUAGE FlexibleContexts #-}
+
+module Axel.Test.Monad.ResourceSpec where
+
+import qualified Axel.Monad.Resource as Res
+import qualified Axel.Test.Monad.FileSystemMock as Mock
+import qualified Axel.Test.Monad.ResourceMock as Mock
+
+import Test.Tasty.Hspec
+
+{-# ANN module "HLint: ignore Redundant do" #-}
+
+spec_Resource :: SpecWith ()
+spec_Resource =
+  describe "readResource" $ do
+    it "reads a resource's contents from the file system" $ do
+      let action = Res.readResource (Res.ResourceId "resGroup1/res1")
+      let origFSState =
+            Mock.mkFSState
+              [ Mock.Directory
+                  "resources"
+                  [Mock.Directory "resGroup1" [Mock.File "res1" "res1Contents"]]
+              ]
+      let expected = "res1Contents"
+      case Mock.runFileSystem origFSState $ Mock.runResourceT action of
+        Left err -> expectationFailure err
+        Right result -> result `shouldBe` (expected, origFSState)
diff --git a/test/Axel/Test/NormalizeSpec.hs b/test/Axel/Test/NormalizeSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Axel/Test/NormalizeSpec.hs
@@ -0,0 +1,14 @@
+module Axel.Test.NormalizeSpec where
+
+import Axel.Denormalize
+import Axel.Normalize
+import Axel.Test.MockUtils
+import qualified Axel.Test.Parse.ASTGen as Parse.ASTGen
+
+import Hedgehog
+
+hprop_denormalizeExpression_is_the_inverse_of_normalizeExpression :: Property
+hprop_denormalizeExpression_is_the_inverse_of_normalizeExpression =
+  property $ do
+    expr <- forAll Parse.ASTGen.genExpression
+    expr === denormalizeExpression (unwrapRight (normalizeExpression expr))
diff --git a/test/Axel/Test/Parse/ASTGen.hs b/test/Axel/Test/Parse/ASTGen.hs
new file mode 100644
--- /dev/null
+++ b/test/Axel/Test/Parse/ASTGen.hs
@@ -0,0 +1,18 @@
+module Axel.Test.Parse.ASTGen where
+
+import qualified Axel.Parse.AST as AST
+
+import Hedgehog
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+
+genExpression :: (MonadGen m) => m AST.Expression
+genExpression =
+  Gen.recursive
+    Gen.choice
+    [ AST.LiteralChar <$> Gen.unicode
+    , AST.LiteralInt <$> Gen.int Range.constantBounded
+    , AST.LiteralString <$> Gen.string (Range.linear 0 5) Gen.unicode
+    , AST.Symbol <$> Gen.string (Range.linear 0 5) Gen.alpha
+    ]
+    [AST.SExpression <$> Gen.list (Range.linear 0 3) genExpression]
diff --git a/test/Axel/Test/ParseSpec.hs b/test/Axel/Test/ParseSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Axel/Test/ParseSpec.hs
@@ -0,0 +1,160 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+module Axel.Test.ParseSpec where
+
+import Axel.Parse
+import Axel.Test.MockUtils
+
+import Test.Tasty.Hspec
+
+spec_Parse :: SpecWith ()
+spec_Parse = do
+  describe "parseSingle" $ do
+    it "can parse a character literal" $ do
+      let result = LiteralChar 'a'
+      case parseSingle "{a}" of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
+    it "can parse an integer literal" $ do
+      let result = LiteralInt 123
+      case parseSingle "123" of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
+    it "can parse a list literal" $ do
+      let result = SExpression [Symbol "list", LiteralInt 1, LiteralChar 'a']
+      case parseSingle "[1 {a}]" of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
+    it "can parse a string literal" $ do
+      let result = LiteralString "a b"
+      case parseSingle "\"a b\"" of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
+    it "can parse a quasiquoted expression" $ do
+      let result =
+            SExpression
+              [Symbol "quasiquote", SExpression [Symbol "foo", Symbol "bar"]]
+      case parseSingle "`(foo bar)" of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
+    it "can parse an s-expression" $ do
+      let result = SExpression [Symbol "foo", Symbol "bar"]
+      case parseSingle "(foo bar)" of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
+    it "can parse a splice-unquoted expression" $ do
+      let result =
+            SExpression
+              [ Symbol "unquoteSplicing"
+              , SExpression [Symbol "foo", Symbol "bar"]
+              ]
+      case parseSingle "~@(foo bar)" of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
+    it "can parse a symbol" $ do
+      let result = Symbol "abc123'''"
+      case parseSingle "abc123'''" of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
+    it "can parse an unquoted expression" $ do
+      let result =
+            SExpression
+              [Symbol "unquote", SExpression [Symbol "foo", Symbol "bar"]]
+      case parseSingle "~(foo bar)" of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
+    it "can quote a character character" $ do
+      let result = SExpression [Symbol "AST.LiteralChar", LiteralChar 'a']
+      case parseSingle "'{a}" of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
+    it "can quote an integer literal" $ do
+      let result = SExpression [Symbol "AST.LiteralInt", LiteralInt 123]
+      case parseSingle "'123" of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
+    it "can quote a list literal" $ do
+      let result =
+            SExpression
+              [ Symbol "AST.SExpression"
+              , SExpression
+                  [ Symbol "list"
+                  , SExpression [Symbol "AST.Symbol", LiteralString "list"]
+                  , SExpression [Symbol "AST.LiteralInt", LiteralInt 1]
+                  , SExpression [Symbol "AST.LiteralInt", LiteralInt 2]
+                  ]
+              ]
+      case parseSingle "'[1 2]" of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
+    it "can quote a string literal" $ do
+      let result = SExpression [Symbol "AST.LiteralString", LiteralString "foo"]
+      case parseSingle "'\"foo\"" of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
+    it "can quote an s-expression" $ do
+      let result =
+            SExpression
+              [ Symbol "AST.SExpression"
+              , SExpression
+                  [ Symbol "list"
+                  , SExpression [Symbol "AST.LiteralInt", LiteralInt 1]
+                  , SExpression [Symbol "AST.LiteralInt", LiteralInt 2]
+                  ]
+              ]
+      case parseSingle "'(1 2)" of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
+    it "can quote a symbol" $ do
+      let result = SExpression [Symbol "AST.Symbol", LiteralString "foo"]
+      case parseSingle "'foo" of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
+    it "can quote a quoted expression" $ do
+      let result =
+            SExpression
+              [ Symbol "AST.SExpression"
+              , SExpression
+                  [ Symbol "list"
+                  , SExpression [Symbol "AST.Symbol", LiteralString "quote"]
+                  , SExpression [Symbol "AST.Symbol", LiteralString "foo"]
+                  ]
+              ]
+      case parseSingle "''foo" of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
+  describe "parseMultiple" $ do
+    it "can parse multiple expressions" $ do
+      let input =
+            [s|
+(foo 1 2 3)
+
+(bar
+ x
+ y
+ z)
+|]
+      let result =
+            [ SExpression
+                [Symbol "foo", LiteralInt 1, LiteralInt 2, LiteralInt 3]
+            , SExpression [Symbol "bar", Symbol "x", Symbol "y", Symbol "z"]
+            ]
+      case parseMultiple input of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
+  describe "parseSource" $ do
+    it "can parse a source file" $ do
+      let input =
+            [s|
+(foo 1 2 3) -- This is a comment
+-- Another comment! (bar x y z)
+|]
+      let result =
+            SExpression
+              [ Symbol "begin"
+              , SExpression
+                  [Symbol "foo", LiteralInt 1, LiteralInt 2, LiteralInt 3]
+              ]
+      case parseSource input of
+        Left err -> expectationFailure $ show err
+        Right x -> x `shouldBe` result
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,3 @@
+{-# OPTIONS_GHC -F -pgmF tasty-discover #-}
+
+module Main where
