packages feed

nova-nix-0.1.4.0: src/Nix/Eval/StringInterp.hs

-- | String interpolation evaluation for Nix.
--
-- Handles both regular strings (double-quoted) and indented strings
-- (double single-quoted).
-- Takes the evaluator as a parameter to break the import cycle with
-- @Nix.Eval@.
module Nix.Eval.StringInterp
  ( evalStringParts,
    evalIndStringParts,
    coerceToString,
  )
where

import Data.Text (Text)
import qualified Data.Text as T
import Nix.Eval.Context (concatStrings)
import Nix.Eval.Types (Env, MonadEval (..), NixValue (..), StringContext, emptyContext, typeName)
import Nix.Expr.Types (Expr, StringPart (..))

-- | The evaluator function, passed as a parameter to avoid cyclic imports.
type Eval m = Env -> Expr -> m NixValue

-- | Evaluate the parts of a regular string (double-quoted).
-- Returns the concatenated text and the merged context from all parts.
evalStringParts :: (MonadEval m) => Eval m -> Env -> [StringPart] -> m (Text, StringContext)
evalStringParts evalFn env parts = do
  chunks <- mapM (evalOnePart evalFn env) parts
  pure (concatStrings chunks)

-- | Evaluate the parts of an indented string (double single-quoted).
--
-- After interpolation, strips the common leading whitespace from all
-- non-empty lines (the standard Nix indented-string semantics).
-- Context is preserved through indentation stripping.
evalIndStringParts :: (MonadEval m) => Eval m -> Env -> [StringPart] -> m (Text, StringContext)
evalIndStringParts evalFn env parts = do
  (raw, ctx) <- evalStringParts evalFn env parts
  pure (stripIndentation raw, ctx)

-- | Evaluate a single string part, returning its text and context.
evalOnePart :: (MonadEval m) => Eval m -> Env -> StringPart -> m (Text, StringContext)
evalOnePart _ _ (StrLit txt) = pure (txt, emptyContext)
evalOnePart evalFn env (StrInterp expr) = do
  val <- evalFn env expr
  coerceToString val

-- | Coerce a Nix value to a string for interpolation.
--
-- Strict coercion: strings, ints, floats, paths, null, bools.
-- Lists, sets (without @__toString@/@outPath@), and functions are errors.
-- Used by string interpolation (@"${...}"@).
coerceToString :: (MonadEval m) => NixValue -> m (Text, StringContext)
coerceToString val = case val of
  VStr s ctx -> pure (s, ctx)
  VInt n -> pure (T.pack (show n), emptyContext)
  VFloat n -> pure (T.pack (show n), emptyContext)
  VPath p -> pure (p, emptyContext)
  VNull -> pure ("", emptyContext)
  VBool True -> pure ("1", emptyContext)
  VBool False -> pure ("", emptyContext)
  other ->
    throwEvalError ("cannot coerce " <> typeName other <> " to a string")

-- ---------------------------------------------------------------------------
-- Indented string whitespace stripping
-- ---------------------------------------------------------------------------

-- | Strip the common leading whitespace from an indented string.
--
-- Algorithm (matching C++ Nix):
-- 1. Split into lines.
-- 2. Find the minimum indentation of all non-empty lines (excluding
--    the first line, which has no leading whitespace in double single-quoted).
-- 3. Strip that many spaces/tabs from the front of each line.
-- 4. Drop a single leading newline if present.
-- 5. Drop a single trailing newline if present.
stripIndentation :: Text -> Text
stripIndentation raw
  | T.null raw = raw
  | otherwise =
      let withLeadingStripped = stripLeadingNewline raw
          lns = T.splitOn "\n" withLeadingStripped
          minIndent = minimumIndent lns
          stripped = map (stripPrefix minIndent) lns
          joined = T.intercalate "\n" stripped
       in stripTrailingNewline joined

-- | Count leading spaces on a line (tabs count as one space).
countIndent :: Text -> Int
countIndent = T.length . T.takeWhile (\c -> c == ' ' || c == '\t')

-- | Find the minimum indentation across all non-empty lines.
minimumIndent :: [Text] -> Int
minimumIndent lns =
  let nonEmpty = filter (not . T.null) lns
      indents = map countIndent nonEmpty
   in case indents of
        [] -> 0
        xs -> minimum xs

-- | Strip up to @n@ leading whitespace characters from a line.
stripPrefix :: Int -> Text -> Text
stripPrefix 0 t = t
stripPrefix n t = case T.uncons t of
  Just (c, rest)
    | c == ' ' || c == '\t' -> stripPrefix (n - 1) rest
  _ -> t

-- | Drop a single leading newline.
stripLeadingNewline :: Text -> Text
stripLeadingNewline t = case T.uncons t of
  Just ('\n', rest) -> rest
  _ -> t

-- | Drop a single trailing newline.
stripTrailingNewline :: Text -> Text
stripTrailingNewline t = case T.unsnoc t of
  Just (prefix, '\n') -> prefix
  _ -> t