packages feed

nova-nix-0.10.0.0: src/Nix/Parser/Expr.hs

-- | Recursive descent expression parser for the Nix language.
--
-- 14 precedence levels, lowest (top) to highest (deepest):
--
-- @
-- lambda / -> / || / && / == != / < > <= >= / //
-- ! / + - / * \\/ / ++ / ? / negate / application / selection
-- @
--
-- Close to the C++ Nix parser (parser.y), with two inert deviations: here
-- @++@ binds looser than multiplication\/addition (parser.y binds it tighter)
-- and unary negate binds looser than @?@ (parser.y makes negate tightest).
-- Both only affect mixed expressions that are type errors regardless, so no
-- program that type-checks parses differently.
-- Entirely pure. No IO, no Megaparsec, no Parsec.
module Nix.Parser.Expr
  ( -- * Entry point
    parseTopLevel,

    -- * Expression parsers (exported for testing)
    parseExpr,
  )
where

import Control.Monad (foldM, when)
import Data.ByteString (ByteString)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text.Encoding as TE
import Nix.Expr.Types
import Nix.Parser.Internal
import Nix.Parser.Lexer (Token (..))
import Nix.Parser.ParseError (ParseError (..))

-- ---------------------------------------------------------------------------
-- Top level
-- ---------------------------------------------------------------------------

-- | Parse a complete Nix file (one expression, then EOF).
parseTopLevel :: Parser Expr
parseTopLevel = do
  expr <- parseExpr
  done <- atEnd
  if done
    then pure expr
    else do
      tok <- peek
      parseError ("unexpected " <> showToken tok <> " after expression")

-- ---------------------------------------------------------------------------
-- Expression (entry point for precedence climbing)
-- ---------------------------------------------------------------------------

-- | Parse a full expression. Tries lambda first, falls back to implication.
parseExpr :: Parser Expr
parseExpr = do
  tok <- peek
  case tok of
    TokLBrace -> parseLambdaOrAttrSet
    TokIdent _ -> parseLambdaOrIdent
    _ -> parseImplication

-- ---------------------------------------------------------------------------
-- Lambda detection
-- ---------------------------------------------------------------------------

-- | Identifier at start: could be @x: body@ or @name\@{...}: body@ or just expr.
-- A lambda header (@ident :@, or @ident \@ { formals } :@) is
-- grammatically unambiguous, so once one parses we are committed and a
-- failure in the body propagates from where it happened; it must not
-- backtrack into the expression reading, whose partial success would
-- otherwise leave the outer parser complaining at the stray colon.
-- When no reading parses, the deepest failure is the one reported.
parseLambdaOrIdent :: Parser Expr
parseLambdaOrIdent = do
  simple <- tryParserKeepError trySimpleLambdaHeader
  case simple of
    Right body -> body
    Left simpleErr -> do
      named <- tryParserKeepError tryNamedSetLambdaHeader
      case named of
        Right body -> body
        Left namedErr -> do
          plain <- tryParserKeepError parseImplication
          case plain of
            Right e -> pure e
            Left plainErr ->
              failWithError (deeperError simpleErr (deeperError namedErr plainErr))

-- | The header of @x: body@; on success, returns the committed body parse.
trySimpleLambdaHeader :: Parser (Parser Expr)
trySimpleLambdaHeader = do
  name <- expectIdent
  expect TokColon
  pure ((\body -> ELambda (FormalName name) body NoCaptureInfo) <$> parseExpr)

-- | The header of @name\@{ formals }: body@; on success, returns the
-- committed body parse.
tryNamedSetLambdaHeader :: Parser (Parser Expr)
tryNamedSetLambdaHeader = do
  name <- expectIdent
  expect TokAt
  expect TokLBrace
  (formals, hasEllipsis) <- parseFormalsBody
  expect TokColon
  pure ((\body -> ELambda (FormalNamedSet name formals hasEllipsis) body NoCaptureInfo) <$> parseExpr)

-- | Brace at start: could be @{ formals }: body@, @{ formals }\@name: body@, or attr set.
-- Same commitment rule as 'parseLambdaOrIdent': @... } :@ and
-- @... } \@ name :@ never follow an attr set, so a parsed lambda
-- header commits, and a body failure is reported from its own
-- position (a real error at line 410 of a formals-lambda file used to
-- surface as an attr-set complaint at line 2).  When both readings
-- fail, the deeper failure wins, so a malformed formal beats the
-- attr-set branch's earlier stumble over the first comma.
parseLambdaOrAttrSet :: Parser Expr
parseLambdaOrAttrSet = do
  headed <- tryParserKeepError trySetLambdaHeader
  case headed of
    Right body -> body
    Left lambdaErr -> do
      alt <- tryParserKeepError parseImplication
      case alt of
        Right e -> pure e
        Left attrErr -> failWithError (deeperError lambdaErr attrErr)

-- | The header of @{ a, b ? default, ... }: body@ or
-- @{ a, b }\@name: body@; on success, returns the committed body parse.
trySetLambdaHeader :: Parser (Parser Expr)
trySetLambdaHeader = do
  expect TokLBrace
  (formals, hasEllipsis) <- parseFormalsBody
  -- Check for @name after the closing brace
  tok <- peek
  case tok of
    TokColon -> do
      _ <- advance
      pure ((\body -> ELambda (FormalSet formals hasEllipsis) body NoCaptureInfo) <$> parseExpr)
    TokAt -> do
      _ <- advance
      name <- expectIdent
      expect TokColon
      pure ((\body -> ELambda (FormalNamedSet name formals hasEllipsis) body NoCaptureInfo) <$> parseExpr)
    _ -> parseError ("expected ':' or '@' after formals, got " <> showToken tok)

-- | Parse the inside of @{ ... }@ formals (comma-separated, optional defaults,
-- optional ellipsis). Expects the closing @}@.
parseFormalsBody :: Parser ([Formal], Bool)
parseFormalsBody = do
  tok <- peek
  case tok of
    TokRBrace -> do
      _ <- advance
      pure ([], False)
    TokEllipsis -> do
      _ <- advance
      expect TokRBrace
      pure ([], True)
    _ -> parseFormalsList []

parseFormalsList :: [Formal] -> Parser ([Formal], Bool)
parseFormalsList acc = do
  name <- expectIdent
  -- Upstream rejects a repeated formal name at parse time.
  when (any ((== name) . fName) acc) $
    parseError ("duplicate formal function argument '" <> name <> "'")
  -- Check for default value
  hasDefault <- match TokQuestion
  defVal <-
    if hasDefault
      then Just <$> parseExpr
      else pure Nothing
  let formal = Formal {fName = name, fDefault = defVal}
      newAcc = formal : acc
  tok <- peek
  case tok of
    TokComma -> do
      _ <- advance
      -- Check for ellipsis after comma
      next <- peek
      case next of
        TokEllipsis -> do
          _ <- advance
          expect TokRBrace
          pure (reverse newAcc, True)
        TokRBrace -> do
          _ <- advance
          pure (reverse newAcc, False)
        _ -> parseFormalsList newAcc
    TokRBrace -> do
      _ <- advance
      pure (reverse newAcc, False)
    _ -> parseError ("expected ',' or '}' in formals, got " <> showToken tok)

-- ---------------------------------------------------------------------------
-- Precedence levels (lowest to highest)
-- ---------------------------------------------------------------------------

-- | Level 1: Implication (@->@, right-associative).
parseImplication :: Parser Expr
parseImplication = do
  lhs <- parseLogicalOr
  tok <- peekMaybe
  case tok of
    Just TokImpl -> do
      _ <- advance
      EBinary OpImpl lhs <$> parseImplication
    _ -> pure lhs

-- | Level 2: Logical OR (@||@, left-associative).
parseLogicalOr :: Parser Expr
parseLogicalOr = do
  lhs <- parseLogicalAnd
  loopOr lhs
  where
    loopOr lhs = do
      found <- match TokOr
      if found
        then do
          rhs <- parseLogicalAnd
          loopOr (EBinary OpOr lhs rhs)
        else pure lhs

-- | Level 3: Logical AND (@&&@, left-associative).
parseLogicalAnd :: Parser Expr
parseLogicalAnd = do
  lhs <- parseEquality
  loopAnd lhs
  where
    loopAnd lhs = do
      found <- match TokAnd
      if found
        then do
          rhs <- parseEquality
          loopAnd (EBinary OpAnd lhs rhs)
        else pure lhs

-- | Level 4: Equality (@==@, @!=@, non-associative).
parseEquality :: Parser Expr
parseEquality = do
  lhs <- parseComparison
  tok <- peekMaybe
  case tok of
    Just TokEq -> do
      _ <- advance
      EBinary OpEq lhs <$> parseComparison
    Just TokNeq -> do
      _ <- advance
      EBinary OpNeq lhs <$> parseComparison
    _ -> pure lhs

-- | Level 5: Comparison (@<@, @>@, @<=@, @>=@, non-associative).
parseComparison :: Parser Expr
parseComparison = do
  lhs <- parseUpdate
  tok <- peekMaybe
  case tok of
    Just TokLt -> do
      _ <- advance
      EBinary OpLt lhs <$> parseUpdate
    Just TokGt -> do
      _ <- advance
      EBinary OpGt lhs <$> parseUpdate
    Just TokLte -> do
      _ <- advance
      EBinary OpLte lhs <$> parseUpdate
    Just TokGte -> do
      _ <- advance
      EBinary OpGte lhs <$> parseUpdate
    _ -> pure lhs

-- | Level 6: Update (@//@, right-associative).
parseUpdate :: Parser Expr
parseUpdate = do
  lhs <- parseNot
  tok <- peekMaybe
  case tok of
    Just TokUpdate -> do
      _ <- advance
      EBinary OpUpdate lhs <$> parseUpdate
    _ -> pure lhs

-- | Level 7: Logical NOT (@!@, prefix).
parseNot :: Parser Expr
parseNot = do
  tok <- peek
  case tok of
    TokNot -> do
      _ <- advance
      EUnary OpNot <$> parseNot
    _ -> parseConcat

-- | Level 8: List concatenation (@++@, right-associative).
parseConcat :: Parser Expr
parseConcat = do
  lhs <- parseAddSub
  tok <- peekMaybe
  case tok of
    Just TokConcat -> do
      _ <- advance
      EBinary OpConcat lhs <$> parseConcat
    _ -> pure lhs

-- | Level 9: Addition and subtraction (@+@, @-@, left-associative).
parseAddSub :: Parser Expr
parseAddSub = do
  lhs <- parseMulDiv
  loopAddSub lhs
  where
    loopAddSub lhs = do
      tok <- peekMaybe
      case tok of
        Just TokPlus -> do
          _ <- advance
          rhs <- parseMulDiv
          loopAddSub (EBinary OpAdd lhs rhs)
        Just TokMinus -> do
          _ <- advance
          rhs <- parseMulDiv
          loopAddSub (EBinary OpSub lhs rhs)
        _ -> pure lhs

-- | Level 10: Multiplication and division (@*@, @/@, left-associative).
parseMulDiv :: Parser Expr
parseMulDiv = do
  lhs <- parseNegate
  loopMulDiv lhs
  where
    loopMulDiv lhs = do
      tok <- peekMaybe
      case tok of
        Just TokStar -> do
          _ <- advance
          rhs <- parseNegate
          loopMulDiv (EBinary OpMul lhs rhs)
        Just TokSlash -> do
          _ <- advance
          rhs <- parseNegate
          loopMulDiv (EBinary OpDiv lhs rhs)
        _ -> pure lhs

-- | Level 11: Arithmetic negation (@-@, prefix).
parseNegate :: Parser Expr
parseNegate = do
  tok <- peek
  case tok of
    TokMinus -> do
      _ <- advance
      EUnary OpNegate <$> parseNegate
    _ -> parseHasAttr

-- | Level 12: Has-attribute (@?@, non-associative).
-- Right operand is an attr path, not a full expression.
parseHasAttr :: Parser Expr
parseHasAttr = do
  lhs <- parseApp
  tok <- peekMaybe
  case tok of
    Just TokQuestion -> do
      _ <- advance
      EHasAttr lhs <$> parseAttrPath
    _ -> pure lhs

-- | Level 13: Function application (juxtaposition, left-associative).
parseApp :: Parser Expr
parseApp = do
  func <- parseSelect
  loopApp func
  where
    loopApp func = do
      tok <- peekMaybe
      case tok of
        Just t | canStartAtom t -> do
          arg <- parseSelect
          loopApp (EApp func arg)
        _ -> pure func

-- | Level 14: Attribute selection (@e.a@, @e.a or default@).
parseSelect :: Parser Expr
parseSelect = do
  expr <- parseAtom
  selectLoop expr

selectLoop :: Expr -> Parser Expr
selectLoop expr = do
  tok <- peekMaybe
  case tok of
    Just TokDot -> do
      _ <- advance
      path <- parseAttrPath
      -- Check for 'or' default
      orDefault <- tryParser parseOrDefault
      selectLoop (ESelect expr path orDefault)
    _ -> pure expr

-- | Parse the @or default@ part after @e.path@.
parseOrDefault :: Parser Expr
parseOrDefault = do
  tok <- peek
  case tok of
    TokIdent "or" -> do
      _ <- advance
      parseSelect
    _ -> parseError "expected 'or'"

-- ---------------------------------------------------------------------------
-- Atoms
-- ---------------------------------------------------------------------------

parseAtom :: Parser Expr
parseAtom = do
  tok <- peek
  case tok of
    TokInt n -> advance >> pure (ELit (NixInt n))
    TokFloat d -> advance >> pure (ELit (NixFloat d))
    TokTrue -> advance >> pure (ELit (NixBool True))
    TokFalse -> advance >> pure (ELit (NixBool False))
    TokNull -> advance >> pure (ELit NixNull)
    TokUri u -> advance >> pure (ELit (NixUri u))
    TokPath p -> advance >> pure (ELit (NixPath p))
    TokPathInterpStart headPiece -> advance >> EPathStr headPiece <$> parsePathParts
    TokSearchPath p -> advance >> pure (ESearchPath p)
    TokIdent name -> advance >> pure (EVar name)
    TokStringOpen -> parseString
    TokIndStringOpen -> parseIndString
    TokLParen -> parseParen
    TokLBracket -> parseList
    TokLBrace -> do
      _ <- advance
      parseAttrSet False
    TokRec -> do
      _ <- advance
      expect TokLBrace
      parseAttrSet True
    TokLet -> parseLet
    TokIf -> parseIf
    TokWith -> parseWith
    TokAssert -> parseAssert
    _ -> parseError ("unexpected " <> showToken tok)

-- ---------------------------------------------------------------------------
-- Strings
-- ---------------------------------------------------------------------------

parseString :: Parser Expr
parseString = do
  expect TokStringOpen
  parts <- parseStringParts TokStringClose
  pure (EStr parts)

parseIndString :: Parser Expr
parseIndString = do
  expect TokIndStringOpen
  parts <- parseStringParts TokIndStringClose
  pure (EIndStr parts)

parseStringParts :: Token -> Parser [StringPart]
parseStringParts closer = go []
  where
    go acc = do
      tok <- peek
      case tok of
        _ | tok == closer -> do
          _ <- advance
          pure (reverse acc)
        TokStringLit txt -> do
          _ <- advance
          go (StrLit txt : acc)
        TokStringEsc txt -> do
          _ <- advance
          go (StrEsc txt : acc)
        TokInterpOpen -> do
          _ <- advance
          expr <- parseExpr
          expect TokInterpClose
          go (StrInterp expr : acc)
        _ -> parseError ("unexpected " <> showToken tok <> " in string")

-- | The pieces of an interpolated path literal, after the opening head
-- piece: literal chunks and interpolations until the lexer's
-- synthesized 'TokPathEnd'.  The lexer emits nothing else between the
-- opener and the end, so the trailing case is a defect signal, not a
-- user-facing grammar error.
parsePathParts :: Parser [StringPart]
parsePathParts = go []
  where
    go acc = do
      tok <- peek
      case tok of
        TokPathEnd -> do
          _ <- advance
          pure (reverse acc)
        TokPathLit txt -> do
          _ <- advance
          go (StrLit (TE.encodeUtf8 txt) : acc)
        TokInterpOpen -> do
          _ <- advance
          expr <- parseExpr
          expect TokInterpClose
          go (StrInterp expr : acc)
        _ -> parseError ("unexpected " <> showToken tok <> " in path")

-- ---------------------------------------------------------------------------
-- Compound expressions
-- ---------------------------------------------------------------------------

parseParen :: Parser Expr
parseParen = do
  expect TokLParen
  expr <- parseExpr
  expect TokRParen
  pure expr

parseList :: Parser Expr
parseList = do
  expect TokLBracket
  EList <$> parseListElems

parseListElems :: Parser [Expr]
parseListElems = go []
  where
    go acc = do
      tok <- peek
      case tok of
        TokRBracket -> do
          _ <- advance
          pure (reverse acc)
        _ | canStartAtom tok -> do
          elem_ <- parseSelect
          go (elem_ : acc)
        _ -> parseError ("unexpected " <> showToken tok <> " in list")

parseAttrSet :: Bool -> Parser Expr
parseAttrSet isRec =
  (\bs -> EAttrs isRec bs NoCaptureInfo) <$> parseBindings

parseBindings :: Parser [Binding]
parseBindings = go [] >>= normalizeBindings
  where
    go acc = do
      tok <- peek
      case tok of
        TokRBrace -> do
          _ <- advance
          pure (reverse acc)
        TokInherit -> do
          inherited <- parseInherit
          go (reverse inherited ++ acc)
        _ -> do
          binding <- parseNamedBinding
          go (binding : acc)

-- ---------------------------------------------------------------------------
-- Binding normalization (upstream parser.y addAttr semantics)
-- ---------------------------------------------------------------------------

-- | Accumulated definition while normalizing one binding list.
data NormEntry
  = -- | @name = value@ at its first definition's position - mergeable when
    -- the value is a plain attrset literal.
    StaticEntry !SrcPos !Text !Expr
  | -- | An @inherit@ - its names are never mergeable.
    InheritEntry !Binding
  | -- | A dynamic-key binding - collisions surface at eval time.
    DynamicEntry !Binding

-- | How a name was defined, for duplicate checking.
data DefKind = DefStatic | DefInherit

-- | Normalize a binding list the way upstream's parser (@addAttr@) does,
-- over bindings 'parseNamedBinding' has already hoisted to one key each:
--
-- * two definitions of one key merge recursively when BOTH values are
--   attrset literals - so @a.b = 1; a.c = 2;@ and @a = { b = 1; }; a.c = 2;@
--   both work, with inner duplicates checked recursively.  A @rec@ marker
--   on the existing set governs the merged result; one on the new set is
--   discarded, exactly as upstream.  The key keeps its first definition's
--   position and each merged binding its own, as upstream's do;
-- * any other duplicate definition is a parse error, as upstream
--   (@a = 1; a = 2;@, an @inherit@ name reused, merging into a
--   non-literal value).
--
-- After normalization every static top-level key appears exactly once -
-- which also makes the positional (slot-based) eval path applicable to
-- sets and lets that use nested attrpaths.
normalizeBindings :: [Binding] -> Parser [Binding]
normalizeBindings bindings =
  case normalizeBindingList bindings of
    Left err -> parseError err
    Right normalized -> pure normalized

-- | Pure core of 'normalizeBindings'; recurses into merged literals.
normalizeBindingList :: [Binding] -> Either Text [Binding]
normalizeBindingList bindings = do
  (entriesRev, _defined) <- foldM step ([], Map.empty) bindings
  pure (map renderEntry (reverse entriesRev))
  where
    step :: ([NormEntry], Map Text DefKind) -> Binding -> Either Text ([NormEntry], Map Text DefKind)
    step (entries, defined) binding = case binding of
      NamedBinding pos (StaticKey key) value ->
        case Map.lookup key defined of
          Nothing ->
            Right (StaticEntry pos key value : entries, Map.insert key DefStatic defined)
          Just DefStatic -> do
            updated <- mergeIntoEntries key value entries
            Right (updated, defined)
          Just DefInherit -> Left (duplicateAttr key)
      b@(NamedBinding {}) -> Right (DynamicEntry b : entries, defined)
      b@(Inherit _ name _) -> inheritEntry b [name]
      b@(InheritFrom _ names) -> inheritEntry b [name | InheritedName _ name <- names]
      where
        -- One name at a time, so a name repeated within the clause is a
        -- duplicate too, as upstream's per-name dupAttr check makes it.
        inheritEntry b names = do
          updated <- foldM defineInherited defined names
          Right (InheritEntry b : entries, updated)
        defineInherited acc name
          | Map.member name acc = Left (duplicateAttr name)
          | otherwise = Right (Map.insert name DefInherit acc)

    -- Replace the existing entry for @key@ with the merged value.
    mergeIntoEntries key newValue entries = case entries of
      [] -> Left (duplicateAttr key)
      (StaticEntry existingPos existingKey existingValue : rest)
        | existingKey == key -> do
            merged <- mergeAttrValues key existingValue newValue
            Right (StaticEntry existingPos existingKey merged : rest)
      (entry : rest) -> (entry :) <$> mergeIntoEntries key newValue rest

    -- Upstream's addAttr (parser-state.hh) merges ANY two attrset
    -- literals without consulting ->recursive on either side: the
    -- EXISTING set's rec marker governs the merged result and the new
    -- set's marker is DISCARDED (upstream's own comment documents the
    -- discard).  The merged binding list is re-normalized so inner
    -- duplicates are caught.
    mergeAttrValues _ (EAttrs existingRec existingBindings _) (EAttrs _ newBindings _) =
      (\merged -> EAttrs existingRec merged NoCaptureInfo)
        <$> normalizeBindingList (existingBindings ++ newBindings)
    mergeAttrValues key _ _ = Left (duplicateAttr key)

    renderEntry (StaticEntry pos key value) = NamedBinding pos (StaticKey key) value
    renderEntry (InheritEntry b) = b
    renderEntry (DynamicEntry b) = b

    duplicateAttr key = "attribute '" <> key <> "' already defined"

-- | @a.b.c = v;@ as one binding under its first key, the rest of the
-- path hoisted into nested set literals, one per level: @a = { b = { c =
-- v; }; };@, as upstream's @addAttr@ builds them, every level at the
-- path's position.
parseNamedBinding :: Parser Binding
parseNamedBinding = do
  pos <- currentPos
  firstKey <- parseAttrKey
  rest <- dottedKeys
  expect TokAssign
  val <- parseExpr
  expect TokSemicolon
  let nestUnder [] value = value
      nestUnder (key : keys) value = EAttrs False [NamedBinding pos key (nestUnder keys value)] NoCaptureInfo
  pure (NamedBinding pos firstKey (nestUnder rest val))

-- | An @inherit@ clause: one 'InheritFrom' for @inherit (from) x y;@, one
-- 'Inherit' per name for @inherit x y;@, each holding the variable it copies.
-- Each name is at its own position, as upstream's parser.y records them.
parseInherit :: Parser [Binding]
parseInherit = do
  expect TokInherit
  tok <- peek
  case tok of
    TokLParen -> do
      _ <- advance
      from <- parseExpr
      expect TokRParen
      names <- parseInheritNames
      expect TokSemicolon
      pure [InheritFrom from names]
    _ -> do
      names <- parseInheritNames
      expect TokSemicolon
      pure [Inherit pos name (EVar name) | InheritedName pos name <- names]

parseInheritNames :: Parser [InheritedName]
parseInheritNames = go []
  where
    go acc = do
      pos <- currentPos
      tok <- peek
      case tok of
        TokIdent name -> do
          _ <- advance
          go (InheritedName pos name : acc)
        -- Keywords are valid as inherit names: inherit if then else;
        _ | Just name <- keywordToText tok -> do
          _ <- advance
          go (InheritedName pos name : acc)
        -- Quoted strings for keywords used as attribute names: inherit "or";
        TokStringOpen -> do
          _ <- advance
          name <- parseQuotedInheritName pos
          go (InheritedName pos name : acc)
        _ -> pure (reverse acc)

-- | Parse a quoted inherit name: a simple string literal (no interpolation),
-- the string opening at the position given.  Used for keywords like @"or"@
-- in @inherit (self.trivial) "or";@.
parseQuotedInheritName :: SrcPos -> Parser Text
parseQuotedInheritName pos = do
  tok <- peek
  case tok of
    TokStringLit s -> do
      _ <- advance
      expect TokStringClose
      attrNameText pos s
    TokStringClose -> do
      _ <- advance
      pure ""
    _ -> parseError ("expected string literal in inherit, got " <> showToken tok)

parseAttrPath :: Parser AttrPath
parseAttrPath = do
  first <- parseAttrKey
  rest <- dottedKeys
  pure (first : rest)

-- | The keys after an attribute path's first, each after a dot.  A dot
-- commits to the key after it, as upstream's LR grammar does (the
-- @attrpath@ rule, parser.y at 2.24.9), so a key that fails to parse
-- reports its own error, a string key that is not UTF-8 among them,
-- rather than backtracking to blame the dot.
dottedKeys :: Parser [AttrKey]
dottedKeys = go []
  where
    go acc = do
      dotted <- match TokDot
      if dotted
        then do
          key <- parseAttrKey
          go (key : acc)
        else pure (reverse acc)

parseAttrKey :: Parser AttrKey
parseAttrKey = do
  tok <- peek
  case tok of
    TokIdent name -> advance >> pure (StaticKey name)
    -- Keywords are valid as attribute names in Nix:
    -- { if = 1; }, a.then, { else = 2; }, etc.
    _ | Just name <- keywordToText tok -> advance >> pure (StaticKey name)
    TokStringOpen -> do
      pos <- currentPos
      strExpr <- parseString
      -- A string key with no interpolation is static, as in upstream Nix's
      -- parser (the string becomes a symbol at parse time) - which is also
      -- what makes @let "x" = 1; in x@ legal.
      case literalStringKey strExpr of
        Just bytes -> StaticKey <$> attrNameText pos bytes
        Nothing -> pure (DynamicKey strExpr)
    TokInterpOpen -> do
      _ <- advance
      expr <- parseExpr
      -- In expression context, the lexer doesn't track interpolation
      -- mode - } is TokRBrace rather than TokInterpClose.
      expect TokRBrace
      pure (DynamicKey expr)
    _ -> parseError ("expected attribute key, got " <> showToken tok)

-- | The bytes of a string expression that contains no interpolation; any
-- 'StrInterp' makes the key dynamic.
literalStringKey :: Expr -> Maybe ByteString
literalStringKey (EStr parts) = mconcat <$> traverse literalPart parts
  where
    literalPart (StrLit bytes) = Just bytes
    literalPart (StrEsc bytes) = Just bytes
    literalPart (StrInterp _) = Nothing
literalStringKey _ = Nothing

-- | The name a string attribute key spells, the string opening at the
-- position given.  Attribute names are text here, so a key whose bytes are
-- not UTF-8 is a syntax error at the string, where upstream makes a symbol
-- of any bytes (the @attrpath@ and @attrs@ rules, parser.y at 2.24.9).
attrNameText :: SrcPos -> ByteString -> Parser Text
attrNameText (SrcPos origin line col) bytes = case TE.decodeUtf8' bytes of
  Right name -> pure name
  Left _ ->
    failWithError
      ParseError
        { peFile = sourceName origin,
          peLine = line,
          peCol = col,
          peMessage = "attribute name is not valid UTF-8"
        }

-- | Convert keyword tokens to their text for use as attribute names.
-- All Nix keywords are valid as attr keys in binding and select position.
keywordToText :: Token -> Maybe Text
keywordToText TokIf = Just "if"
keywordToText TokThen = Just "then"
keywordToText TokElse = Just "else"
keywordToText TokLet = Just "let"
keywordToText TokIn = Just "in"
keywordToText TokWith = Just "with"
keywordToText TokAssert = Just "assert"
keywordToText TokRec = Just "rec"
keywordToText TokInherit = Just "inherit"
keywordToText TokTrue = Just "true"
keywordToText TokFalse = Just "false"
keywordToText TokNull = Just "null"
keywordToText _ = Nothing

parseLet :: Parser Expr
parseLet = do
  expect TokLet
  bindings <- parseLetBindings
  expect TokIn
  (\body -> ELet bindings body NoCaptureInfo) <$> parseExpr

parseLetBindings :: Parser [Binding]
parseLetBindings = go [] >>= normalizeBindings
  where
    go acc = do
      tok <- peek
      case tok of
        TokIn -> pure (reverse acc)
        TokInherit -> do
          inherited <- parseInherit
          go (reverse inherited ++ acc)
        _ -> do
          binding <- parseNamedBinding
          -- Upstream rejects a dynamic TOP-LEVEL key in a let at parse time;
          -- nested dynamic keys (let a.${k} = 1) live in a nested attrset
          -- and are fine.
          case binding of
            NamedBinding _ (DynamicKey _) _ ->
              parseError "dynamic attributes not allowed in let"
            _ -> go (binding : acc)

parseIf :: Parser Expr
parseIf = do
  expect TokIf
  cond <- parseExpr
  expect TokThen
  thenExpr <- parseExpr
  expect TokElse
  EIf cond thenExpr <$> parseExpr

parseWith :: Parser Expr
parseWith = do
  expect TokWith
  env <- parseExpr
  expect TokSemicolon
  EWith env <$> parseExpr

parseAssert :: Parser Expr
parseAssert = do
  expect TokAssert
  cond <- parseExpr
  expect TokSemicolon
  EAssert cond <$> parseExpr

-- ---------------------------------------------------------------------------
-- Helpers
-- ---------------------------------------------------------------------------

-- | Can this token start an atomic expression (for application parsing)?
canStartAtom :: Token -> Bool
canStartAtom (TokIdent _) = True
canStartAtom (TokInt _) = True
canStartAtom (TokFloat _) = True
canStartAtom TokTrue = True
canStartAtom TokFalse = True
canStartAtom TokNull = True
canStartAtom (TokUri _) = True
canStartAtom (TokPath _) = True
canStartAtom (TokPathInterpStart _) = True
canStartAtom (TokSearchPath _) = True
canStartAtom TokStringOpen = True
canStartAtom TokIndStringOpen = True
canStartAtom TokLParen = True
canStartAtom TokLBrace = True
canStartAtom TokLBracket = True
canStartAtom TokRec = True
canStartAtom TokLet = True
canStartAtom TokIf = True
canStartAtom TokWith = True
canStartAtom TokAssert = True
canStartAtom _ = False