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 +20/−0
- Setup.hs +2/−0
- simple-nix.cabal +34/−0
- src/Nix.hs +7/−0
- src/Nix/Common.hs +120/−0
- src/Nix/Expr.hs +179/−0
- src/Nix/Parser.hs +324/−0
+ 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