packages feed

nix-lang-0.1.0.0: src/Nix/Lang/ExactPrint.hs

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}

-- | Exact printing for annotated Nix ASTs.
--
-- This module turns an annotated tree back into source text.
--
-- Its job is not "pretty printing" in the general sense. It follows the layout,
-- token positions, and comment ownership stored in the parsed tree, so it is the
-- renderer to use after parsing or after edits from 'Nix.Lang.Edit'.
--
-- For fresh syntax trees that do not carry exact-print annotations, use
-- 'Nix.Lang.RFCPrint' or 'Nix.Lang.Outputable' instead.
--
-- Minimal example:
--
-- @
-- import Nix.Lang.ExactPrint (renderExactText)
--
-- -- expr :: Nix.Lang.Types.Ps.Expr
-- rendered = renderExactText expr
-- @
module Nix.Lang.ExactPrint
  ( ExactPrint,
    EPError (..),
    exactPrint,
    renderExactText,
    renderExactTextM,
    renderExactDoc,
  )
where

import Control.Monad (unless)
import Control.Monad.Except
import Control.Monad.State.Strict
import Data.Text (Text)
import qualified Data.Text as T
import Nix.Lang.Annotation
import Nix.Lang.ExactPrint.Operations
import Nix.Lang.ExactPrint.Prepare.Utils
import Nix.Lang.Outputable (renderToText)
import Nix.Lang.Span
import Nix.Lang.Types
import Nix.Lang.Types.Ps
import Nix.Lang.Utils
import Prettyprinter (Doc, pretty)

--------------------------------------------------------------------------------

data EPError
  = MissingTokenSpan Text AnnToken
  | EmptyAttrPath
  | MismatchedAttrPathDots Int Int
  | MismatchedSetPatCommas Int Int
  | MissingSetPatQuestion SrcSpan
  deriving (Show, Eq)

-- | The printer accumulates exact output as concrete text chunks and tracks the
-- current logical cursor used to interpret relative gaps and token deltas.
data EPState = EPState
  { epsChunks :: ![Text],
    epsCursor :: !(Maybe RenderCursor)
  }

newtype EPM a = EPM
  { runExactM :: StateT EPState (Except EPError) a
  }
  deriving newtype (Functor, Applicative, Monad, MonadError EPError, MonadState EPState)

--------------------------------------------------------------------------------

class ExactPrint a where
  exactPrintM :: a -> EPM ()

--------------------------------------------------------------------------------

exactPrint :: (ExactPrint a) => a -> Doc ann
exactPrint = either (error . show) pretty . renderExactTextM

renderExactText :: (ExactPrint a) => a -> Text
renderExactText = either (error . show) id . renderExactTextM

renderExactTextM :: (ExactPrint a) => a -> Either EPError Text
renderExactTextM x = finishPrinterState . snd <$> runExactPrinter (exactPrintM x)

renderExactDoc :: (ExactPrint a) => a -> Either EPError (Doc ann)
renderExactDoc = fmap pretty . renderExactTextM

emptyPrinterState :: EPState
emptyPrinterState = EPState [] Nothing

runExactPrinter :: EPM a -> Either EPError (a, EPState)
runExactPrinter action = runExcept (runStateT (runExactM action) emptyPrinterState)

finishPrinterState :: EPState -> Text
finishPrinterState = T.concat . reverse . epsChunks

exactPrintLocated :: (ExactPrint a) => Located a -> EPM ()
exactPrintLocated = exactPrintM . unLoc

--------------------------------------------------------------------------------

-- | Emit a raw text fragment into the printer state.
emitText :: Text -> EPM ()
emitText txt =
  modify' $ \st@EPState {epsChunks, epsCursor} ->
    st
      { epsChunks = txt : epsChunks,
        epsCursor = fmap (`advanceCursor` txt) epsCursor
      }

setCursor :: RenderCursor -> EPM ()
setCursor cursor = modify' $ \st -> st {epsCursor = Just cursor}

-- | Ensure the printer has a current cursor, initializing it when rendering is
-- starting from an otherwise anchor-free context.
ensureCursor :: RenderCursor -> EPM RenderCursor
ensureCursor fallback = do
  mCursor <- gets epsCursor
  case mCursor of
    Just cursor -> pure cursor
    Nothing -> setCursor fallback >> pure fallback

-- | Move the output cursor to the start of a target span by emitting the exact
-- gap implied by the current cursor and that span.
emitGapToSpan :: SrcSpan -> EPM ()
emitGapToSpan span' = do
  mCursor <- gets epsCursor
  case mCursor of
    Nothing -> setCursor (cursorAtSpanStart span')
    Just cursor -> emitText (renderGapFromCursorToSpanText cursor span')

-- | Emit text whose first character is anchored at a particular span start.
emitAtSpan :: SrcSpan -> Text -> EPM ()
emitAtSpan span' txt = emitGapToSpan span' >> emitText txt

emitDelta :: DeltaPos -> EPM ()
emitDelta delta = do
  _ <- ensureCursor (RenderCursor 1 1)
  emitText (renderGapFromDeltaText delta)

-- | Recover the emitted surface text for a token from its annotation identity.
tokenTextM :: Text -> AnnToken -> EPM Text
tokenTextM label tok =
  case showToken (annToken tok) of
    Just txt -> pure txt
    Nothing -> throwError $ MissingTokenSpan (label <> ": token text unavailable") tok

-- | Emit a token using the canonical text associated with its annotation.
emitToken :: Text -> AnnToken -> EPM ()
emitToken label tok = do
  txt <- tokenTextM label tok
  emitTokenText tok txt

-- | Emit a token using explicit text supplied by the caller.
emitTokenText :: AnnToken -> Text -> EPM ()
emitTokenText tok txt = do
  case annTokenPos tok of
    AnnSpan src -> emitGapToSpan src
    AnnDelta delta -> emitDelta delta
  emitText txt

--------------------------------------------------------------------------------

emitCommentAt :: Located Comment -> EPM ()
emitCommentAt (L span' comment) = emitAtSpan span' (renderCommentText comment)

emitComments :: [Located Comment] -> EPM ()
emitComments = mapM_ emitCommentAt

emitPriorCommentsTo :: SrcSpan -> [Located Comment] -> EPM ()
emitPriorCommentsTo target comments = do
  emitComments comments
  unless (null comments) (emitGapToSpan target)

emitFollowingComments :: [Located Comment] -> EPM ()
emitFollowingComments = emitComments

-- | Emit a node together with the comments it owns.
emitWrappedNode :: (HasAnnCommon a) => SrcSpan -> a -> EPM () -> EPM ()
emitWrappedNode target ann body = do
  let comments = annComments ann
  emitPriorCommentsTo target (priorComments comments)
  body
  emitFollowingComments (followingComments comments)

--------------------------------------------------------------------------------

instance ExactPrint Expr where
  exactPrintM = \case
    NixVar ann ident -> emitWrappedNode (getLoc ident) ann $ emitAtSpan (getLoc ident) (unLoc ident)
    NixLit ann lit -> emitWrappedNode (getLoc lit) ann $ emitGapToSpan (getLoc lit) >> exactPrintM (unLoc lit)
    NixPar ann (L _ x) -> renderParM ann x
    NixString ann str -> emitWrappedNode (getLoc str) ann $ emitAtSpan (getLoc str) (renderStringText (unLoc str))
    NixPath ann path -> emitWrappedNode (getLoc path) ann $ emitAtSpan (getLoc path) (renderPathText (unLoc path))
    NixEnvPath ann path -> renderEnvPathM ann path
    NixLam ann (L _ pat) (L _ x) -> renderLamM ann pat x
    NixApp ann (L _ f) (L _ x) -> emitWrappedNode (exprSpan f) ann $ exactPrintM f >> exactPrintM x
    NixBinApp ann op (L _ x) (L _ y) -> renderBinAppM ann op x y
    NixNotApp ann (L _ x) -> renderPrefixAppM ann "!" x
    NixNegApp ann (L _ x) -> renderPrefixAppM ann "-" x
    NixList ann xs -> renderListM ann xs
    NixSet ann NixSetRecursive (L _ bindings) -> renderSetM ann True bindings
    NixSet ann NixSetNonRecursive (L _ bindings) -> renderSetM ann False bindings
    NixLet ann (L _ bindings) (L _ x) -> renderLetM ann bindings x
    NixHasAttr ann (L _ x) (L _ p) -> renderHasAttrM ann x p
    NixSelect ann (L _ x) (L _ p) mx -> renderSelectM ann x p mx
    NixIf ann (L _ cond) (L _ t) (L _ f) -> renderIfM ann cond t f
    NixWith ann (L _ scope) (L _ x) -> renderWithM ann scope x
    NixAssert ann (L _ assertion) (L _ x) -> renderAssertM ann assertion x

instance ExactPrint Lit where
  exactPrintM = emitText . renderLitText

instance ExactPrint AttrPath where
  exactPrintM path@(NixAttrPath ann keys) =
    emitWrappedNode (attrPathSpan path) ann (renderAttrPathM ann keys)

instance ExactPrint AttrKey where
  exactPrintM = emitText . renderAttrKeyText

instance ExactPrint Binding where
  exactPrintM = \case
    NixNormalBinding ann (L _ path) (L _ x) -> renderNormalBindingM ann path x
    NixInheritBinding ann mScope names -> renderInheritBindingM ann mScope names

instance ExactPrint SetPatAs where
  exactPrintM = renderSetPatAsM

instance ExactPrint FuncPat where
  exactPrintM = \case
    NixVarPat ann ident -> emitWrappedNode (getLoc ident) ann $ emitAtSpan (getLoc ident) (unLoc ident)
    pat@(NixSetPat ann ellipses mAs params) ->
      emitWrappedNode (funcPatBodySpan pat) ann $ renderSetPatM ann ellipses mAs params

--------------------------------------------------------------------------------

renderLitText :: Lit -> Text
renderLitText = \case
  NixUri _ uri -> uri
  NixInteger _ int -> T.pack (show int)
  NixFloat _ float -> T.pack (show float)
  NixBoolean _ True -> "true"
  NixBoolean _ False -> "false"
  NixNull _ -> "null"

renderStringText :: NString -> Text
renderStringText = \case
  NixDoubleQuotesString src _ -> renderDoubleQuotedSourceText src
  NixDoubleSingleQuotesString src _ -> renderIndentedStringSourceText src

renderPathText :: Path -> Text
renderPathText = \case
  NixLiteralPath _ path -> path
  NixInterpolPath _ parts -> renderInterpolatedPathText parts

renderAttrKeyText :: AttrKey -> Text
renderAttrKeyText = \case
  NixStaticAttrKey _ (L _ x) -> x
  NixDynamicStringAttrKey _ parts -> renderDoubleQuotedPartsText parts
  NixDynamicInterpolAttrKey _ expr -> renderInterpolatedExprText expr

renderInterpolatedPathText :: [LNixStringPart Ps] -> Text
renderInterpolatedPathText = T.concat . fmap (renderPathPartText . unLoc)

renderDoubleQuotedPartsText :: [LNixStringPart Ps] -> Text
renderDoubleQuotedPartsText parts = "\"" <> renderQuotedPartsText parts <> "\""

renderQuotedPartsText :: [LNixStringPart Ps] -> Text
renderQuotedPartsText = T.concat . fmap (renderQuotedPartText . unLoc)

renderPathPartText :: NixStringPart Ps -> Text
renderPathPartText = \case
  NixStringLiteral _ txt -> txt
  NixStringInterpol _ expr -> renderInterpolatedExprText expr

renderQuotedPartText :: NixStringPart Ps -> Text
renderQuotedPartText = \case
  NixStringLiteral _ txt -> txt
  NixStringInterpol _ expr -> renderInterpolatedExprText expr

renderInterpolatedExprText :: LExpr -> Text
renderInterpolatedExprText expr = "${" <> renderToText (unLoc expr) <> "}"

renderSetPatAsM :: SetPatAs -> EPM ()
renderSetPatAsM NixSetPatAs {..} =
  emitWrappedNode (setPatAsRenderSpan nspaAnn nspaVar nspaLocation) nspaAnn $ case nspaLocation of
    NixSetPatAsLeading -> do
      emitAtSpan (getLoc nspaVar) (unLoc nspaVar)
      emitToken "set pattern at" (aspaAt nspaAnn)
    NixSetPatAsTrailing -> do
      emitToken "set pattern at" (aspaAt nspaAnn)
      emitAtSpan (getLoc nspaVar) (unLoc nspaVar)

instance ExactPrint SetPatBinding where
  exactPrintM NixSetPatBinding {..} = do
    emitAtSpan (getLoc nspbVar) (unLoc nspbVar)
    case nspbDefault of
      Nothing -> pure ()
      Just defExpr -> case aspbQuestion nspbAnn of
        Just qTok -> emitToken "set pattern question" qTok >> exactPrintLocated defExpr
        Nothing -> throwError $ MissingSetPatQuestion (getLoc nspbVar)

--------------------------------------------------------------------------------

renderListM :: AnnListNode -> [LExpr] -> EPM ()
renderListM ann xs = do
  let comments = annComments ann
  emitPriorCommentsTo (expectTokenSpan "list open bracket" (alnOpenS ann)) (priorComments comments)
  emitToken "list open bracket" (alnOpenS ann)
  mapM_ exactPrintLocated xs
  emitFollowingComments (followingComments comments)
  emitToken "list close bracket" (alnCloseS ann)

--------------------------------------------------------------------------------

renderSetM :: AnnSet -> Bool -> [LBinding] -> EPM ()
renderSetM ann _ bindings = do
  let comments = annComments ann
      openAnchor = maybe (expectTokenSpan "set open brace" (asOpenC ann)) (expectTokenSpan "rec keyword") (asRec ann)
  emitPriorCommentsTo openAnchor (priorComments comments)
  case asRec ann of
    Just recTok -> emitToken "rec keyword" recTok
    Nothing -> pure ()
  emitToken "set open brace" (asOpenC ann)
  mapM_ exactPrintLocated bindings
  emitFollowingComments (followingComments comments)
  emitToken "set close brace" (asCloseC ann)

--------------------------------------------------------------------------------

renderEnvPathM :: AnnEnvPathNode -> Located Text -> EPM ()
renderEnvPathM ann path = do
  let comments = annComments ann
  emitPriorCommentsTo (expectTokenSpan "env path open" (aenvOpen ann)) (priorComments comments)
  emitToken "env path open" (aenvOpen ann)
  emitAtSpan (getLoc path) (unLoc path)
  emitFollowingComments (followingComments comments)
  emitToken "env path close" (aenvClose ann)

--------------------------------------------------------------------------------

renderLamM :: AnnLamNode -> FuncPat -> Expr -> EPM ()
renderLamM ann pat body = do
  let comments = annComments ann
  emitPriorCommentsTo (funcPatRenderSpan pat) (priorComments comments)
  exactPrintM pat
  emitToken "lambda colon" (alamColon ann)
  exactPrintM body
  emitFollowingComments (followingComments comments)

--------------------------------------------------------------------------------

renderBinAppM :: AnnBinAppNode -> BinaryOp -> Expr -> Expr -> EPM ()
renderBinAppM ann op lhs rhs = do
  let comments = annComments ann
  emitPriorCommentsTo (exprSpan lhs) (priorComments comments)
  exactPrintM lhs
  emitTokenText (abinOperator ann) (showBinOP op)
  exactPrintM rhs
  emitFollowingComments (followingComments comments)

--------------------------------------------------------------------------------

renderPrefixAppM :: AnnPrefixNode -> Text -> Expr -> EPM ()
renderPrefixAppM ann tok expr = do
  let comments = annComments ann
  emitPriorCommentsTo (expectTokenSpan "prefix operator" (apfxToken ann)) (priorComments comments)
  emitTokenText (apfxToken ann) tok
  exactPrintM expr
  emitFollowingComments (followingComments comments)

--------------------------------------------------------------------------------

renderAttrPathM :: AnnAttrPath -> [LAttrKey] -> EPM ()
renderAttrPathM ann keys =
  case keys of
    [] -> throwError EmptyAttrPath
    first : rest ->
      case aapDots ann of
        dotTok : moreDots | length (aapDots ann) == length keys -> do
          emitToken "attr path dot" dotTok
          emitGapToSpan (getLoc first)
          exactPrintM (unLoc first)
          emitAttrPathTail moreDots rest
        dots -> do
          emitGapToSpan (getLoc first)
          exactPrintM (unLoc first)
          emitAttrPathTail dots rest
  where
    emitAttrPathTail [] [] = pure ()
    emitAttrPathTail (dotTok : moreDots) (key : moreKeys) = do
      emitToken "attr path dot" dotTok
      emitGapToSpan (getLoc key)
      exactPrintM (unLoc key)
      emitAttrPathTail moreDots moreKeys
    emitAttrPathTail dots moreKeys = throwError $ MismatchedAttrPathDots (length dots) (length moreKeys)

--------------------------------------------------------------------------------

renderNormalBindingM :: AnnNormalBinding -> AttrPath -> Expr -> EPM ()
renderNormalBindingM ann path expr = do
  let comments = annComments ann
  emitPriorCommentsTo (attrPathRenderSpan path) (priorComments comments)
  exactPrintM path
  emitToken "binding equals" (anbEqual ann)
  exactPrintM expr
  emitToken "binding semicolon" (anbSemicolon ann)
  emitFollowingComments (followingComments comments)

renderInheritBindingM :: AnnInheritBinding -> Maybe LExpr -> [LAttrKey] -> EPM ()
renderInheritBindingM ann mScope names = do
  let comments = annComments ann
      inheritSpan = expectTokenSpan "inherit keyword" (aibInherit ann)
  emitPriorCommentsTo inheritSpan (priorComments comments)
  emitToken "inherit keyword" (aibInherit ann)
  maybe (pure ()) exactPrintLocated mScope
  mapM_ emitName names
  emitToken "inherit semicolon" (aibSemicolon ann)
  emitFollowingComments (followingComments comments)
  where
    emitName name = emitGapToSpan (getLoc name) >> exactPrintM (unLoc name)

--------------------------------------------------------------------------------

renderParM :: AnnParNode -> Expr -> EPM ()
renderParM ann expr = do
  let comments = annComments ann
  emitPriorCommentsTo (expectTokenSpan "paren open" (apnOpenP ann)) (priorComments comments)
  emitToken "paren open" (apnOpenP ann)
  exactPrintM expr
  emitFollowingComments (followingComments comments)
  emitToken "paren close" (apnCloseP ann)

--------------------------------------------------------------------------------

renderLetM :: AnnLetNode -> [LBinding] -> Expr -> EPM ()
renderLetM ann bindings expr = do
  let comments = annComments ann
      letSpan = expectTokenSpan "let keyword" (alLet ann)
  emitPriorCommentsTo letSpan (priorComments comments)
  emitToken "let keyword" (alLet ann)
  mapM_ exactPrintLocated bindings
  emitToken "in keyword" (alIn ann)
  exactPrintM expr
  emitFollowingComments (followingComments comments)

--------------------------------------------------------------------------------

renderIfM :: AnnIfNode -> Expr -> Expr -> Expr -> EPM ()
renderIfM ann cond thenExpr elseExpr = do
  let comments = annComments ann
      ifSpan = expectTokenSpan "if keyword" (aifIf ann)
  emitPriorCommentsTo ifSpan (priorComments comments)
  emitToken "if keyword" (aifIf ann)
  exactPrintM cond
  emitToken "then keyword" (aifThen ann)
  exactPrintM thenExpr
  emitToken "else keyword" (aifElse ann)
  exactPrintM elseExpr
  emitFollowingComments (followingComments comments)

--------------------------------------------------------------------------------

renderWithM :: AnnWithNode -> Expr -> Expr -> EPM ()
renderWithM ann scope expr = do
  let comments = annComments ann
      withSpan = expectTokenSpan "with keyword" (awWith ann)
  emitPriorCommentsTo withSpan (priorComments comments)
  emitToken "with keyword" (awWith ann)
  exactPrintM scope
  emitToken "with semicolon" (awSemicolon ann)
  exactPrintM expr
  emitFollowingComments (followingComments comments)

--------------------------------------------------------------------------------

renderAssertM :: AnnAssertNode -> Expr -> Expr -> EPM ()
renderAssertM ann assertion expr = do
  let comments = annComments ann
      assertSpan = expectTokenSpan "assert keyword" (aaAssert ann)
  emitPriorCommentsTo assertSpan (priorComments comments)
  emitToken "assert keyword" (aaAssert ann)
  exactPrintM assertion
  emitToken "assert semicolon" (aaSemicolon ann)
  exactPrintM expr
  emitFollowingComments (followingComments comments)

--------------------------------------------------------------------------------

renderHasAttrM :: AnnHasAttr -> Expr -> AttrPath -> EPM ()
renderHasAttrM ann expr path = do
  let comments = annComments ann
  emitPriorCommentsTo (exprSpan expr) (priorComments comments)
  exactPrintM expr
  emitToken "has-attr question" (ahaQuestion ann)
  exactPrintM path
  emitFollowingComments (followingComments comments)

--------------------------------------------------------------------------------

renderSelectM :: AnnSelect -> Expr -> AttrPath -> Maybe LExpr -> EPM ()
renderSelectM ann expr path def = do
  let comments = annComments ann
  emitPriorCommentsTo (exprSpan expr) (priorComments comments)
  exactPrintM expr
  exactPrintM path
  case def of
    Nothing -> pure ()
    Just defExpr -> case aslOr ann of
      Just orTok -> emitTokenText orTok "or" >> exactPrintLocated defExpr
      -- fall back to plain text
      Nothing -> emitText " or " >> exactPrintLocated defExpr
  emitFollowingComments (followingComments comments)

--------------------------------------------------------------------------------

renderSetPatM :: AnnSetPatNode -> NixSetPatEllipses -> Maybe LSetPatAs -> [LSetPatBinding] -> EPM ()
renderSetPatM ann ellipses mAs params = do
  case mAs of
    Just (L _ asPat@NixSetPatAs {nspaLocation = NixSetPatAsLeading}) -> exactPrintM asPat
    _ -> pure ()
  emitToken "set pattern open" (aspOpenC ann)
  renderSetPatEntriesM params (aspCommas ann) ellipses (aspEllipsis ann)
  emitToken "set pattern close" (aspCloseC ann)
  case mAs of
    Just (L _ asPat@NixSetPatAs {nspaLocation = NixSetPatAsTrailing}) -> exactPrintM asPat
    _ -> pure ()

renderSetPatEntriesM :: [LSetPatBinding] -> [AnnToken] -> NixSetPatEllipses -> Maybe AnnToken -> EPM ()
renderSetPatEntriesM params commas ellipses ellipsisTok = do
  let (separatorCommas, trailingComma) = splitAt (max 0 (length params - 1)) commas
  renderBindings params separatorCommas
  case (ellipses, ellipsisTok, trailingComma) of
    (NixSetPatIsEllipses, Just tok, commaTok : _) -> emitToken "set pattern comma" commaTok >> emitToken "set pattern ellipsis" tok
    (NixSetPatIsEllipses, Just tok, []) -> emitToken "set pattern ellipsis" tok
    (NixSetPatIsEllipses, Nothing, _) -> pure ()
    (_, _, commaTok : _) -> emitToken "set pattern trailing comma" commaTok
    _ -> pure ()
  where
    renderBindings [] [] = pure ()
    renderBindings (param : rest) commaToks = do
      exactPrintM (unLoc param)
      case (rest, commaToks) of
        (next : more, commaTok : remaining) -> emitToken "set pattern comma" commaTok >> renderBindings (next : more) remaining
        ([], []) -> pure ()
        _ -> throwError $ MismatchedSetPatCommas (length commaToks) (length rest)
    renderBindings [] remaining = throwError $ MismatchedSetPatCommas (length remaining) 0

--------------------------------------------------------------------------------

renderCommentText :: Comment -> Text
renderCommentText = \case
  LineComment txt -> "#" <> txt
  BlockComment txt -> "/*" <> txt <> "*/"