diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2015 Allen Nelson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/simple-nix.cabal b/simple-nix.cabal
new file mode 100644
--- /dev/null
+++ b/simple-nix.cabal
@@ -0,0 +1,34 @@
+-- Initial simplenix.cabal generated by cabal init.  For further
+-- documentation, see http://haskell.org/cabal/users-guide/
+
+name:                simple-nix
+version:             0.1.0.0
+synopsis:            Simple parsing/pretty printing for Nix expressions
+-- description:
+homepage:            https://github.com/adnelson/simple-nix
+license:             MIT
+license-file:        LICENSE
+author:              Allen Nelson
+maintainer:          anelson@narrativescience.com
+-- copyright:
+category:            Nix
+build-type:          Simple
+-- extra-source-files:
+cabal-version:       >=1.10
+
+library
+  exposed-modules:     Nix, Nix.Expr, Nix.Parser
+  other-modules:       Nix.Common
+  -- other-extensions:
+  build-depends:       base >=4.8 && <4.9
+                     , classy-prelude
+                     , text
+                     , mtl
+                     , unordered-containers
+                     , parsec
+                     , MissingH
+                     , error-list
+                     , text-render
+                     , system-filepath
+  hs-source-dirs:      src
+  default-language:    Haskell2010
diff --git a/src/Nix.hs b/src/Nix.hs
new file mode 100644
--- /dev/null
+++ b/src/Nix.hs
@@ -0,0 +1,7 @@
+module Nix (
+    module Nix.Expr,
+    module Nix.Parser
+  ) where
+
+import Nix.Expr
+import Nix.Parser
diff --git a/src/Nix/Common.hs b/src/Nix/Common.hs
new file mode 100644
--- /dev/null
+++ b/src/Nix/Common.hs
@@ -0,0 +1,120 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoMonomorphismRestriction #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE ViewPatterns #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE TypeFamilies #-}
+module Nix.Common (
+    module ClassyPrelude,
+    module Control.Applicative,
+    module Control.Exception,
+    module Control.Exception.ErrorList,
+    module Control.Monad,
+    module Control.Monad.Except,
+    module Control.Monad.Identity,
+    module Control.Monad.State.Strict,
+    module Control.Monad.Reader,
+    module Control.Monad.Trans,
+    module Data.Char,
+    module Data.HashMap.Strict,
+    module Data.Either,
+    module Data.Maybe,
+    module Data.List,
+    module Data.String.Utils,
+    module Filesystem.Path.CurrentOS,
+    module GHC.Exts,
+    module GHC.IO.Exception,
+    module Text.Render,
+    Name, Record,
+    tuple, tuple3, fromRight, pathToText,
+    putStrsLn, putStrs, dropSuffix, maybeIf,
+    joinBy, mapJoinBy
+  ) where
+
+import ClassyPrelude hiding (assert, asList, find, FilePath, bracket,
+                             maximum, maximumBy, try)
+import qualified Prelude as P
+import Control.Monad (when)
+import Control.Monad.Trans (MonadIO(..), lift)
+import Control.Monad.Reader (ReaderT(..), MonadReader(..), (<=<), (>=>), ask,
+                             asks)
+import Control.Monad.State.Strict (MonadState, StateT, State, get, gets,
+                                   modify, put, liftM, liftIO, runState,
+                                   runStateT, execState, execStateT,
+                                   evalState, evalStateT)
+import Control.Monad.Except (ExceptT, MonadError(..), throwError, runExceptT)
+import Control.Exception.ErrorList
+import Control.Monad.Identity (Identity(..))
+import Control.Applicative hiding (empty, optional)
+import Data.Char (isDigit, isAlpha)
+import Data.List (maximum, maximumBy)
+import Data.HashMap.Strict (HashMap, (!))
+import qualified Data.HashMap.Strict as H
+import Data.Maybe (fromJust, isJust, isNothing)
+import Data.Either (isRight, isLeft)
+import Data.String.Utils hiding (join)
+import qualified Data.Text as T
+import GHC.Exts (IsList)
+import GHC.IO.Exception
+import Control.Exception (bracket)
+import Text.Render hiding (renderParens)
+import Filesystem.Path.CurrentOS (FilePath, fromText, toText, collapse)
+
+-- | Indicates that the text is some identifier.
+type Name = Text
+
+-- | A record is a lookup table with string keys.
+type Record = HashMap Name
+
+-- | Takes two applicative actions and returns their result as a 2-tuple.
+tuple :: Applicative f => f a -> f b -> f (a, b)
+tuple action1 action2 = (,) <$> action1 <*> action2
+
+-- | Takes three applicative actions and returns their result as a 3-tuple.
+tuple3 :: Applicative f => f a -> f b -> f c -> f (a, b, c)
+tuple3 action1 action2 action3 = (,,) <$> action1 <*> action2 <*> action3
+
+-- | Creates a new hashmap by applying a function to every key in it.
+alterKeys :: (Eq k, Hashable k, Eq k', Hashable k') =>
+             (k -> k') -> HashMap k v -> HashMap k' v
+alterKeys f mp = do
+  let pairs = H.toList mp
+  let newPairs = P.map (\(k, v) -> (f k, v)) pairs
+  let newMap = H.fromList newPairs
+  newMap
+
+fromRight :: Either a b -> b
+fromRight (Right x) = x
+fromRight (Left err) = error "Expected `Right` value"
+
+putStrsLn :: MonadIO m => [Text] -> m ()
+putStrsLn = putStrLn . concat
+
+putStrs :: MonadIO m => [Text] -> m ()
+putStrs = putStr . concat
+
+dropSuffix :: String -> String -> String
+dropSuffix suffix s | s == suffix = ""
+dropSuffix suffix (c:cs) = c : dropSuffix suffix cs
+dropSuffix suffix "" = ""
+
+maybeIf :: Bool -> a -> Maybe a
+maybeIf True x = Just x
+maybeIf False _ = Nothing
+
+grab :: (Hashable k, Eq k) => k -> HashMap k v -> v
+grab k = fromJust . H.lookup k
+
+joinBy :: Text -> [Text] -> Text
+joinBy = T.intercalate
+
+mapJoinBy :: Text -> (a -> Text) -> [a] -> Text
+mapJoinBy sep func = joinBy sep . map func
+
+pathToText :: FilePath -> Text
+pathToText pth = case toText pth of
+  Left p -> p
+  Right p -> p
diff --git a/src/Nix/Expr.hs b/src/Nix/Expr.hs
new file mode 100644
--- /dev/null
+++ b/src/Nix/Expr.hs
@@ -0,0 +1,179 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE ViewPatterns #-}
+module Nix.Expr where
+
+import Nix.Common
+import qualified Data.HashMap.Strict as H
+import qualified Data.HashSet as HS
+import qualified Data.Text as T
+
+data FuncArgs
+  = Arg Name
+  | Kwargs (HashMap Name (Maybe NixExpr)) Bool (Maybe Name)
+  deriving (Show, Eq)
+
+data NixExpr
+  = Var Name
+  | NixPathVar Name
+  | Num Int
+  | Bool Bool
+  | Null
+  | OneLineString NixString
+  | MultiLineString NixString
+  | Path FilePath
+  | List [NixExpr]
+  | Set Bool [NixAssign]
+  | Let [NixAssign] NixExpr
+  | Function FuncArgs NixExpr
+  | Apply NixExpr NixExpr
+  | With NixExpr NixExpr
+  | If NixExpr NixExpr NixExpr
+  | Dot NixExpr [NixString] (Maybe NixExpr)
+  | BinOp NixExpr Text NixExpr
+  | Not NixExpr
+  | Assert NixExpr NixExpr
+  deriving (Show, Eq)
+
+data NixAssign
+  = Assign [NixString] NixExpr
+  | Inherit (Maybe NixExpr) (HashSet Name)
+  deriving (Show, Eq)
+
+data NixString
+  = Plain Text
+  | Antiquote NixString NixExpr NixString
+  deriving (Show, Eq)
+
+instance IsString NixString where
+  fromString = Plain . fromString
+
+instance IsString NixExpr where
+  fromString = OneLineString . fromString
+
+(=$=) :: Name -> NixExpr -> NixAssign
+k =$= v = Assign [Plain k] v
+
+str :: Text -> NixExpr
+str = OneLineString . Plain
+
+--assignsToMap :: [NixAssign] -> Record NixExpr
+--assignsToMap asns = H.fromList $ map totuple asns where
+--  totuple (Assign [])
+
+toKwargs :: [(Name, Maybe NixExpr)] -> FuncArgs
+toKwargs stuff = Kwargs (H.fromList stuff) False Nothing
+
+isValidIdentifier :: Name -> Bool
+isValidIdentifier "" = False
+isValidIdentifier (unpack -> c:cs) = validFirst c && validRest cs
+  where validFirst c = isAlpha c || c == '-' || c == '_'
+        validRest (c:cs) = (validFirst c || isDigit c) && validRest cs
+        validRest "" = True
+
+renderPath :: [NixString] -> Text
+renderPath = mapJoinBy "." ren where
+  ren (Plain txt) | isValidIdentifier txt = txt
+  ren txt = renderOneLineString txt
+
+renderAssign :: NixAssign -> Text
+renderAssign (Assign p e) = renderPath p <> " = " <> renderNixExpr e <> ";"
+renderAssign (Inherit maybE names) = do
+  let ns = joinBy " " $ HS.toList names
+      e = maybe "" (\e -> " (" <> renderNixExpr e <> ") ") maybE
+  "inherit " <> e <> ns <> ";"
+
+renderOneLineString :: NixString -> Text
+renderOneLineString s = "\"" <> escape escapeSingle s <> "\""
+
+renderMultiLineString :: NixString -> Text
+renderMultiLineString s = "''" <> escape escapeMulti s <> "''"
+
+renderParens e | isTerm e = renderNixExpr e
+renderParens e = "(" <> renderNixExpr e <> ")"
+
+renderKwargs :: [(Name, Maybe NixExpr)] -> Bool -> Text
+renderKwargs ks dotdots = case (ks, dotdots) of
+  ([], True) -> "{...}"
+  ([], False) -> "{}"
+  (ks, True) -> "{" <> ren ks <> ", ...}"
+  (ks, False) -> "{" <> ren ks <> "}"
+  where ren ks = mapJoinBy ", " ren' ks
+        ren' (k, Nothing) = k
+        ren' (k, Just e) = k <> " ? " <> renderNixExpr e
+
+renderFuncArgs :: FuncArgs -> Text
+renderFuncArgs (Arg a) = a
+renderFuncArgs (Kwargs k dotdots mname) =
+  let args = renderKwargs (H.toList k) dotdots
+  in args <> maybe "" (\n -> " @ " <> n) mname
+
+renderDot :: NixExpr -> [NixString] -> Maybe NixExpr -> Text
+renderDot e pth alt = renderParens e <> rpth <> ralt where
+  rpth = case pth of {[] -> ""; _ -> "." <> renderPath pth}
+  ralt = case alt of {Nothing -> ""; Just e' -> " or " <> renderNixExpr e'}
+
+-- | A "term" is something which does not need to be enclosed in
+-- parentheses.
+isTerm :: NixExpr -> Bool
+isTerm (Var _) = True
+isTerm (Num _) = True
+isTerm (Bool _) = True
+isTerm Null = True
+isTerm (Path p) = True
+isTerm (OneLineString _) = True
+isTerm (MultiLineString _) = True
+isTerm (List _) = True
+isTerm (Set _ _) = True
+isTerm (Dot _ _ Nothing) = True
+isTerm (NixPathVar _) = True
+isTerm _ = False
+
+renderNixExpr :: NixExpr -> Text
+renderNixExpr = \case
+  Var name -> name
+  Num n -> pack $ show n
+  Bool True -> "true"
+  Bool False -> "false"
+  Null -> "null"
+  NixPathVar v -> "<" <> v <> ">"
+  OneLineString s -> renderOneLineString s
+  MultiLineString s -> renderMultiLineString s
+  Path pth -> pathToText pth
+  List es -> "[" <> mapJoinBy " " renderNixExpr es <> "]"
+  Set True asns -> "rec " <> renderNixExpr (Set False asns)
+  Set False asns -> "{" <> concatMap renderAssign asns <> "}"
+  Let asns e -> concat ["let ", concatMap renderAssign asns, " in ",
+                        renderNixExpr e]
+  Function arg e -> renderFuncArgs arg <> ": " <> renderNixExpr e
+  Apply e1@(Apply _ _) e2 -> renderNixExpr e1 <> " " <> renderNixExpr e2
+  Apply e1 e2 -> renderNixExpr e1 <> " " <> renderParens e2
+  With e1 e2 -> "with " <> renderNixExpr e1 <> "; " <> renderNixExpr e2
+  Assert e1 e2 -> "assert " <> renderNixExpr e1 <> "; " <> renderNixExpr e2
+  If e1 e2 e3 -> "if " <> renderNixExpr e1 <> " then "
+                       <> renderNixExpr e2 <> " else " <> renderNixExpr e3
+  Dot e pth alt -> renderDot e pth alt
+  BinOp e1 op e2 -> renderParens e1 <> " " <> op <> " " <> renderParens e2
+  Not e -> "!" <> renderNixExpr e
+
+escapeSingle :: String -> String
+escapeSingle s = case s of
+  '$':'{':s' -> '\\':'$':'{':escapeSingle s'
+  '\n':s' -> '\\':'n':escapeSingle s'
+  '\t':s' -> '\\':'t':escapeSingle s'
+  '\r':s' -> '\\':'r':escapeSingle s'
+  '\b':s' -> '\\':'b':escapeSingle s'
+  c:s' -> c : escapeSingle s'
+  "" -> ""
+
+escapeMulti :: String -> String
+escapeMulti s = case s of
+  '$':'{':s' -> '\\':'$':'{':escapeMulti s
+  c:s' -> c : escapeMulti s'
+  "" -> ""
+
+escape :: (String -> String) -> NixString -> Text
+escape esc (Plain s) = pack $ esc $ unpack s
+escape esc (Antiquote s e s') = concat [escape esc s, "${", renderNixExpr e,
+                                        "}", escape esc s']
diff --git a/src/Nix/Parser.hs b/src/Nix/Parser.hs
new file mode 100644
--- /dev/null
+++ b/src/Nix/Parser.hs
@@ -0,0 +1,324 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE TypeFamilies #-}
+module Nix.Parser where
+
+import qualified Prelude as P
+import Text.Parsec hiding (many, (<|>), spaces, parse, State, uncons)
+import qualified Text.Parsec as Parsec
+
+import qualified Data.HashSet as HS
+import qualified Data.HashMap.Strict as H
+
+import Nix.Common
+import Nix.Expr
+
+type Parser = ParsecT String () Identity
+
+-- | Given a parser and a string, attempts to parse the string.
+parse :: Parser a -> Text -> Either ParseError a
+parse p = Parsec.parse p "" . unpack
+
+parseFull :: Parser a -> Text -> Either ParseError a
+parseFull p = Parsec.parse (p <* eof) "" . unpack
+
+comment :: Parser ()
+comment = lineComment <|> blockComment >> return ()
+  where
+    lineComment = do
+      char '#'
+      manyTill anyChar (choice [char '\n' >> return (), eof])
+    blockComment = try $ do
+      string "/*"
+      manyTill anyChar (choice [try $ string "*/" >> return (), eof])
+
+-- | Consumes any spaces (not other whitespace).
+spaces :: Parser ()
+spaces = many (oneOf "\n\t " *> return () <|> comment) >> return ()
+
+-- | Consumes at least one space (not other whitespace).
+spaces1 :: Parser ()
+spaces1 = char ' ' >> spaces
+
+-- | Parses the given string and any trailing spaces.
+sstring :: String -> Parser String
+sstring = lexeme . string
+
+-- | Parses the given character and any trailing spaces.
+schar :: Char -> Parser Char
+schar = lexeme . char
+
+-- | Parses `p` and any trailing spaces.
+lexeme :: Parser a -> Parser a
+lexeme p = p <* spaces
+
+-- | Parses an integer.
+pInt :: Parser Int
+pInt = lexeme $ P.read <$> many1 digit
+
+-- | Parses the given string. Does not fail if it's a keyword.
+keyword :: String -> Parser String
+keyword = try . sstring
+
+-- | Parses an identifier
+pIdentifier :: Parser Text
+pIdentifier = lexeme pIdentifier'
+
+pIdentifier' :: Parser Text
+pIdentifier' = notKeyword $ do
+  first <- letter <|> char '_'
+  rest <- many $ letter <|> digit <|> char '_' <|> char '-'
+  return $ pack $ first : rest
+
+-- | Parses `p`, but fails if the result is a reserved word.
+notKeyword :: Parser Text -> Parser Text
+notKeyword p = try $ do
+  ident <- p
+  if ident `member` keywords then unexpected $ "keyword " <> show ident
+  else return ident
+
+-- | Set of keywords.
+keywords :: HashSet Text
+keywords = HS.fromList ["if", "then", "else", "null", "inherit", "rec",
+                        "true", "false", "let", "in", "with", "assert"]
+
+-- | Set of operators.
+operators :: HashSet String
+operators = HS.fromList ["+", "-", "*", "/", "++", "&&", "||", "//", "?",
+                         "==", "!=", ">=", "<=", ">", "<"]
+
+-- | Characters found in operators.
+opChars :: [Char]
+opChars = "+-*/&|?=><!$"
+
+addLeft :: Text -> NixString -> NixString
+addLeft txt (Plain p) = Plain (txt <> p)
+addLeft txt (Antiquote s e s') = Antiquote (addLeft txt s) e s'
+
+-- | Parses an interpolated string, without parsing quotes.
+pInterp :: Parser NixString
+pInterp = do
+  let stopChars = "$\\\""
+  plain <- pack <$> many (noneOf stopChars)
+  let stop = return $ Plain plain
+      continue = pInterp
+      -- Append `s` to what we're building, and keep parsing.
+      continueWith s = fmap (addLeft (plain <> s)) continue
+      consume c = char c >> continueWith (singleton c)
+  option (Plain plain) $ do
+    lookAhead (oneOf stopChars) >>= \case
+      '$' -> char '$' >> lookAhead anyChar >>= \case
+        -- If it's an open parens, grab what's in the parens.
+        '{' -> Antiquote (Plain plain) <$> curlies <*> continue
+        -- If it's another dollar sign, grab both dollar signs.
+        '$' -> char '$' >> continueWith "$$"
+        -- Otherwise, just keep going.
+        c -> continueWith "$"
+      -- If there's a backslash, we're escaping whatever's next.
+      '\\' -> char '\\' >> anyChar >>= \case
+        'n' -> continueWith "\n"
+        'r' -> continueWith "\r"
+        't' -> continueWith "\t"
+        'b' -> continueWith "\b"
+        c -> continueWith $ singleton c
+      -- If it's a quote and we're not in a multi-line string, return.
+      -- Note that we're not consuming the quote;
+      -- that happens in the outer parser calling this function.
+      '"' -> stop
+  where curlies = between (schar '{') (char '}') pNixExpr
+
+-- | Parses an interpolated string, without parsing quotes. Counts spaces
+-- at beginning of lines.
+pInterpMultiLine :: Parser NixString
+pInterpMultiLine = do
+  let stopChars = "$\\'"
+  plain <- pack <$> many (noneOf stopChars)
+  let stop = return (Plain plain)
+      continue = pInterpMultiLine
+      -- Append `s` to what we're building, and keep parsing.
+      continueWith s = fmap (addLeft (plain <> s)) continue
+      consume c = char c >> continueWith (singleton c)
+  option (Plain plain) $ do
+    lookAhead (oneOf stopChars) >>= \case
+      '$' -> char '$' >> lookAhead anyChar >>= \case
+        -- If it's an open parens, grab what's in the parens.
+        '{' -> Antiquote (Plain plain) <$> curlies <*> continue
+        -- If it's another dollar sign, grab both dollar signs.
+        '$' -> char '$' >> continueWith "$$"
+        -- Otherwise, just keep going.
+        c -> continueWith "$"
+      -- If there's a backslash, we're escaping whatever's next.
+      '\\' -> char '\\' >> singleton <$> anyChar >>= continueWith
+      -- If we see a single quote followed by another one, we stop here.
+      '\'' -> choice [lookAhead (string "''") >> stop,
+                      consume '\'']
+  where curlies = between (schar '{') (char '}') pNixExpr
+
+pOneLineString :: Parser NixString
+pOneLineString = between (char '"') (schar '"') pInterp
+
+pMultiLineString :: Parser NixString
+pMultiLineString = between (string "''") (sstring "''") pInterpMultiLine
+
+pString :: Parser NixExpr
+pString = choice [OneLineString <$> pOneLineString,
+                  MultiLineString <$> pMultiLineString]
+
+pVar :: Parser NixExpr
+pVar = try $ fmap Var $ pIdentifier <* notFollowedBy (char ':')
+
+pBool :: Parser NixExpr
+pBool = choice (map keyword ["true", "false"]) >>= \case
+  "true" -> return $ Bool True
+  "false" -> return $ Bool False
+
+pNull :: Parser NixExpr
+pNull = keyword "null" >> return Null
+
+pFuncArgs :: Parser FuncArgs
+pFuncArgs = choice [Arg <$> pIdentifier, pKwargs]
+
+-- | Gets all of the arguments for a function.
+pKwargs :: Parser FuncArgs
+pKwargs = do
+  (args, dotdots) <- between (schar '{') (schar '}') _getKwargs
+  argsNname <- optionMaybe $ schar '@' >> pIdentifier
+  return $ Kwargs (H.fromList args) dotdots argsNname
+  where
+    _getKwargs :: Parser ([(Name, Maybe NixExpr)], Bool)
+    _getKwargs = go [] where
+      -- Attempt to parse `...`. If this succeeds, stop and return True.
+      -- Otherwise, attempt to parse an argument, optionally with a
+      -- default. If this fails, then return what has been accumulated
+      -- so far.
+      go acc = (sstring "..." >> return (acc, True)) <|> getMore acc
+      getMore acc = do
+        -- Could be nothing, in which just return what we have so far.
+        option (acc, False) $ do
+          -- Get an argument name and an optional default.
+          pair <- liftA2 (,) pIdentifier (optionMaybe $ schar '?' >> pNixExpr)
+          -- Either return this, or attempt to get a comma and restart.
+          option (acc `snoc` pair, False) $ do
+            schar ','
+            go (acc `snoc` pair)
+
+pFunction :: Parser NixExpr
+pFunction = try $ do
+  args <- pFuncArgs
+  sstring ":"
+  body <- pNixExpr
+  return $ Function args body
+
+pSet :: Parser NixExpr
+pSet = try $ do
+  isRec <- isJust <$> optionMaybe (keyword "rec")
+  assigns <- between (schar '{') (schar '}') (many pNixAssign)
+  return $ Set isRec assigns
+
+pLet :: Parser NixExpr
+pLet = liftA2 Let (between (keyword "let") (keyword "in") $ many pNixAssign)
+                  pNixExpr
+
+pNixPathVar :: Parser Name
+pNixPathVar = try $ between (char '<') (schar '>') pIdentifier'
+
+pNixAssign :: Parser NixAssign
+pNixAssign = choice [inherit, assign] <* schar ';' where
+  inherit = keyword "inherit" >> do
+    from <- optionMaybe pParens
+    names <- HS.fromList <$> many pIdentifier
+    return $ Inherit from names
+  assign = do
+    assignee <- pKeyPath
+    schar '='
+    val <- pNixExpr
+    return $ Assign assignee val
+
+pPath :: Parser NixExpr
+pPath = lexeme $ try $ do
+  dots <- option "" $ try (string "..") <|> string "."
+  rest <- try $ do
+    char '/' <* notFollowedBy (oneOf "*/")
+    path <- many1 (noneOf "\n\t ;#(){}")
+    return $ '/' : path
+  return $ Path $ fromString $ dots <> rest
+
+pList :: Parser NixExpr
+pList = fmap List $ between (schar '[') (schar ']') $ many pDot
+
+pParens :: Parser NixExpr
+pParens = between (schar '(') (schar ')') pNixExpr
+
+pNixTerm :: Parser NixExpr
+pNixTerm = choice [pString, pUriString, pVar, Num <$> pInt,
+                   NixPathVar <$> pNixPathVar,
+                   pBool, pNull, pList, pParens, pSet, pPath]
+
+pKeyPath :: Parser [NixString]
+pKeyPath = (Plain <$> pIdentifier <|> pOneLineString) `sepBy1` dot
+  where dot = try $ schar '.' <* notFollowedBy (char '.')
+
+pDot :: Parser NixExpr
+pDot = do
+  term <- pNixTerm
+  option term $ try $ do
+    schar '.'
+    keypath <- pKeyPath
+    alt <- optionMaybe (keyword "or" >> pNixExpr)
+    return $ Dot term keypath alt
+
+pNot :: Parser NixExpr
+pNot = do
+  optnot <- optionMaybe $ schar '!'
+  expr <- pApply
+  case optnot of
+    Nothing -> return expr
+    Just _ -> return $ Not expr
+
+pNixExpr :: Parser NixExpr
+pNixExpr = choice [pBinary, pFunction, pLet, pAssert, pWith, pIf, pNot]
+
+pApply :: Parser NixExpr
+pApply = pDot `chainl1` (pure Apply)
+
+-- | Two expressions joined by a binary operator.
+pBinary :: Parser NixExpr
+pBinary = pNot `chainl1` fmap (flip BinOp) op where
+  op = try $ do
+    oper <- lexeme $ many1 (oneOf opChars)
+    if not $ HS.member oper operators
+    then unexpected $ "Invalid operator " <> show oper
+    else return $ pack oper
+
+pAssert :: Parser NixExpr
+pAssert = liftA2 Assert (keyword "assert" *> pNixExpr) (schar ';' *> pNixExpr)
+
+pWith :: Parser NixExpr
+pWith = liftA2 With (keyword "with" *> pNixExpr) (schar ';' *> pNixExpr)
+
+pIf :: Parser NixExpr
+pIf = liftA3 If (keyword "if" *> pNixExpr)
+                (keyword "then" *> pNixExpr)
+                (keyword "else" *> pNixExpr)
+
+pUriString :: Parser NixExpr
+pUriString = lexeme $ try $ do
+  scheme <- (:) <$> letter <*> many (letter <|> digit <|> char '+')
+  string ":"
+  rest <- many1 $ noneOf "\n\t ;"
+  return $ OneLineString $ Plain $ pack $ scheme <> ":" <> rest
+
+pTopLevel :: Parser NixExpr
+pTopLevel = pNixExpr
+
+parseFile :: String -> IO (Either ParseError NixExpr)
+parseFile = parseFileWith pTopLevel
+
+parseFileWith :: Parser a -> String -> IO (Either ParseError a)
+parseFileWith p path = parseFull (spaces >> p) <$> readFile path
+
+parseNix :: Text -> Either ParseError NixExpr
+parseNix = parseFull pTopLevel
+
+parseNixA = parseFull pNixAssign
