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