packages feed

simple-nix (empty) → 0.1.0.0

raw patch · 7 files changed

+686/−0 lines, 7 filesdep +MissingHdep +basedep +classy-preludesetup-changed

Dependencies added: MissingH, base, classy-prelude, error-list, mtl, parsec, system-filepath, text, text-render, unordered-containers

Files

+ LICENSE view
@@ -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.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ simple-nix.cabal view
@@ -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
+ src/Nix.hs view
@@ -0,0 +1,7 @@+module Nix (+    module Nix.Expr,+    module Nix.Parser+  ) where++import Nix.Expr+import Nix.Parser
+ src/Nix/Common.hs view
@@ -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
+ src/Nix/Expr.hs view
@@ -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']
+ src/Nix/Parser.hs view
@@ -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