packages feed

nova-nix-0.1.6.0: src/Nix/Parser.hs

-- | Hand-rolled Nix language parser.
--
-- == How Nix parsing works
--
-- A @.nix@ file is a single expression. There is no top-level module
-- declaration, no import list, no @main@.  The file IS the expression.
--
-- @
-- -- default.nix
-- { pkgs ? import \<nixpkgs\> {} }:
-- pkgs.mkShell { buildInputs = [ pkgs.ghc ]; }
-- @
--
-- This is a lambda that takes an attribute set with a default, and
-- returns a call to @mkShell@. The whole file is one 'Expr'.
--
-- == Why hand-rolled
--
-- hnix used Megaparsec and it was 10x slower than C++ Nix (38% of runtime
-- in GC from parser allocations). We use a hand-rolled recursive descent
-- parser with 'Data.Text' slicing to minimize allocations.
--
-- == Operator precedence (lowest to highest)
--
-- @
-- ->            (right)    implication
-- ||            (left)     logical or
-- &&            (left)     logical and
-- == !=         (none)     equality
-- \< \> \<= \>=      (none)     comparison
-- //            (right)    attribute set merge
-- !             (prefix)   logical not
-- ++ (list)     (right)    list concatenation
-- + -           (left)     addition, subtraction
-- * /           (left)     multiplication, division
-- -             (prefix)   arithmetic negation
-- f x           (left)     function application
-- e.a           (left)     attribute selection
-- @
module Nix.Parser
  ( -- * Parsing
    parseNix,
    parseNixFile,

    -- * Errors
    ParseError (..),
  )
where

import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import Nix.Expr.Resolve (resolveVars)
import Nix.Expr.Types (Expr)
import Nix.Parser.Expr (parseTopLevel)
import Nix.Parser.Internal (ParseState (..), runParser)
import Nix.Parser.Lexer (tokenize)
import Nix.Parser.ParseError (ParseError (..))

-- | Parse a Nix expression from source text.
--
-- The input is the full file contents. The file name is used only for
-- error messages.  Strips a leading UTF-8 BOM if present — Windows
-- editors (Notepad, PowerShell) commonly add one.
parseNix :: Text -> Text -> Either ParseError Expr
parseNix fileName source = do
  tokens <- tokenize fileName (stripBOM source)
  let st = ParseState {psTokens = tokens, psFile = fileName}
  (expr, _remaining) <- runParser parseTopLevel st
  pure (resolveVars expr)

-- | Strip a leading UTF-8 byte order mark (U+FEFF) if present.
stripBOM :: Text -> Text
stripBOM t = case T.uncons t of
  Just ('\xFEFF', rest) -> rest
  _ -> t

-- | Parse a @.nix@ file from disk.
parseNixFile :: FilePath -> IO (Either ParseError Expr)
parseNixFile path = do
  source <- TIO.readFile path
  let fileName = packFilePath path
  pure $ parseNix fileName source

-- | Convert a FilePath to Text for error reporting.
packFilePath :: FilePath -> Text
packFilePath = T.pack