packages feed

nova-nix-0.9.0.0: src/Nix/Eval/Print.hs

-- | The value printers upstream writes values with.
--
-- @printValue@ (print.cc at Nix 2.24.9) quotes a value in a message.
-- @builtins.trace@ prints a value that is not a string through it, and a
-- type error that shows the offending value prints it after the type, as
-- in @expected a set but found an integer: 1@.
--
-- Upstream prints with one of two option sets: its defaults, which print
-- the whole value, and @errorPrintOptions@ (print-options.hh), which cut
-- an oversized value short so it cannot flood an error message.
--
-- Neither option set forces anything, so in 'printValue' an element or
-- attribute not yet evaluated prints as upstream's thunk marker, and which
-- ones do follows nova-nix's thunk allocation.  That is upstream's for a
-- literal written in the list or set itself, for a function's argument,
-- and for anything already forced.  It is not where upstream hands over a
-- value without a thunk and nova-nix makes one (an indented string, a
-- @~\/@ path, a variable bound to a literal by @let@ or @rec@, a global
-- such as @map@).  A function value carries neither the name nor
-- the source position upstream prints after @lambda@, so its marker holds
-- the word alone.  A set or list reached a second time prints again where
-- upstream marks it repeated, which needs value identity this printer
-- does not track.
--
-- Upstream's @printAmbiguous@ (print-ambiguous.cc) writes what
-- @nix-instantiate --eval@ prints, and 'printAmbiguous' writes what
-- @nova-nix eval@ prints.  It renders to bytes: a string's payload is
-- bytes that need not be UTF-8, and upstream writes them as they are.
module Nix.Eval.Print
  ( PrintOptions (..),
    printValue,
    printAmbiguous,
    formatXmlFloat,
  )
where

import Control.Monad.State.Strict (State, evalState, gets, modify')
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Char8 as BC
import qualified Data.ByteString.Lazy as BL
import Data.Char (isAsciiLower, isAsciiUpper, isDigit)
import Data.List (partition)
import Data.String (IsString)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Nix.Eval.Types (AttrSet, NixValue (..), Thunk (..), attrSetToAscList, bytesToTextLossy, clistThunks, readThunkValue, thunkUnderEvaluation)

-- | Which of upstream's option sets a print follows.
data PrintOptions
  = -- | Upstream's defaults: the whole value, as @builtins.trace@ prints it.
    PrintInFull
  | -- | @errorPrintOptions@: nesting, attributes, list items and string
    -- bytes past its limits are cut, and what was cut is counted instead.
    PrintForError

-- | A bound one of the option sets puts on a print.
data Limit = Unlimited | AtMost !Int

-- | Whether a count is still inside a bound.
within :: Int -> Limit -> Bool
within _ Unlimited = True
within count (AtMost bound) = count < bound

-- | @errorPrintOptions@ (print-options.hh at 2.24.9): how deep a print
-- goes, and how many attributes, list items and string bytes it shows.
errorMaxDepth, errorMaxAttrs, errorMaxListItems, errorMaxStringLength :: Int
errorMaxDepth = 10
errorMaxAttrs = 10
errorMaxListItems = 10
errorMaxStringLength = 1024

depthLimit, attrLimit, itemLimit, stringLimit :: PrintOptions -> Limit
depthLimit = limitedTo errorMaxDepth
attrLimit = limitedTo errorMaxAttrs
itemLimit = limitedTo errorMaxListItems
stringLimit = limitedTo errorMaxStringLength

-- | The defaults bound nothing; @errorPrintOptions@ bounds at its value.
limitedTo :: Int -> PrintOptions -> Limit
limitedTo _ PrintInFull = Unlimited
limitedTo bound PrintForError = AtMost bound

-- | How many attributes and list items one print has shown.  Upstream's
-- bounds on them count over the whole print, not per set or list.
data Shown = Shown {shownAttrs :: !Int, shownItems :: !Int}

type Printer = State Shown

-- | Render a value as upstream's @printValue@ renders it under the given
-- options.
printValue :: PrintOptions -> NixValue -> Text
printValue options val = evalState (printAt options 0 val) (Shown 0 0)

-- | A value at a nesting depth, the top-level value being at 0.
printAt :: PrintOptions -> Int -> NixValue -> Printer Text
printAt options depth val = case val of
  VInt n -> pure (T.pack (show n))
  VFloat f -> pure (formatXmlFloat f)
  VBool True -> pure "true"
  VBool False -> pure "false"
  VNull -> pure "null"
  VStr bytes _ -> pure (printString options bytes)
  VPath p -> pure p
  VList cl -> printList options depth (map Thunk (clistThunks cl))
  VAttrs attrs -> printAttrs options depth attrs
  -- What upstream prints for a lambda whose expression it cannot reach.
  VLambda {} -> pure (marker "lambda")
  VBuiltin name [] -> pure (marker ("primop " <> name))
  VBuiltin name _ -> pure (marker ("partially applied primop " <> name))
  -- A compiled pattern exists only among the arguments of a partially
  -- applied match or split and names neither, so it prints as upstream
  -- prints an application whose primop it cannot name.
  VCompiledRegex _ -> pure (marker "partially applied primop")

-- | Upstream's @printAttrs@.  A set's attributes count toward the bound
-- after the attributes of any set inside them, as upstream counts one
-- only once its value has printed.
printAttrs :: PrintOptions -> Int -> AttrSet -> Printer Text
printAttrs options depth attrs
  | within depth (depthLimit options) =
      bracketed "{" "}" <$> printBounded (attrLimit options) shownAttrs countAttr elidedAttrs (map printAttr (orderedAttrs options attrs))
  | otherwise = pure "{ ... }"
  where
    printAttr (name, thunk) = do
      shown <- printThunk options (depth + 1) thunk
      pure (builderText (printIdentifier (TE.encodeUtf8 name)) <> " = " <> shown <> ";")
    countAttr counts = counts {shownAttrs = shownAttrs counts + 1}
    elidedAttrs left = elided left "attribute" "attributes"

-- | Upstream's @printList@.
printList :: PrintOptions -> Int -> [Thunk] -> Printer Text
printList options depth thunks
  | within depth (depthLimit options) =
      bracketed "[" "]" <$> printBounded (itemLimit options) shownItems countItem elidedItems (map (printThunk options (depth + 1)) thunks)
  | otherwise = pure "[ ... ]"
  where
    countItem counts = counts {shownItems = shownItems counts + 1}
    elidedItems left = elided left "item" "items"

-- | Print the entries of one set or list while the print's running count
-- is inside the bound, then, in place of the rest, how many were left.
printBounded :: Limit -> (Shown -> Int) -> (Shown -> Shown) -> (Int -> Text) -> [Printer Text] -> Printer [Text]
printBounded bound counted count elision = go
  where
    go :: [Printer Text] -> Printer [Text]
    go [] = pure []
    go entries@(entry : rest) = gets counted >>= continueAt
      where
        continueAt sofar
          | within sofar bound = do
              shown <- entry
              modify' count
              (shown :) <$> go rest
          | otherwise = pure [elision (length entries)]

-- | A set's attributes in upstream's order: by name, with @_type@ and
-- @type@ ahead of the rest wherever attributes are bounded (print.cc sorts
-- by its @ImportantFirstAttrNameCmp@ then).
orderedAttrs :: PrintOptions -> AttrSet -> [(Text, Thunk)]
orderedAttrs options attrs = case attrLimit options of
  Unlimited -> byName
  AtMost _ -> important <> rest
  where
    byName = attrSetToAscList attrs
    (important, rest) = partition ((`elem` importantAttrNames) . fst) byName

-- | Upstream's @isImportantAttrName@.
importantAttrNames :: [Text]
importantAttrNames = ["_type", "type"]

-- | An element or attribute value, read without forcing it: its value
-- once it has one, and otherwise upstream's marker for a thunk, or for a
-- thunk whose force is still running.
printThunk :: PrintOptions -> Int -> Thunk -> Printer Text
printThunk options depth thunk = case readThunkValue thunk of
  Just val -> printAt options depth val
  Nothing
    | thunkUnderEvaluation thunk -> pure (marker "potential infinite recursion")
    | otherwise -> pure (marker "thunk")

-- | Upstream's @printLiteralString@: quoted and escaped so the lexer reads
-- the same string back.  The cut and the count of what it left out are in
-- bytes, as upstream's are.  A message is 'Text', so a multi-byte
-- character the cut splits decodes to U+FFFD where upstream writes the
-- partial bytes raw.
printString :: PrintOptions -> ByteString -> Text
printString options bytes = builderText (quotedBytes shown bytes) <> elision
  where
    total = BS.length bytes
    shown = case stringLimit options of
      Unlimited -> total
      AtMost bound -> min bound total
    elision
      | total == shown = ""
      | otherwise = " " <> elided (total - shown) "byte" "bytes"

-- | The first @count@ bytes as a string literal.
quotedBytes :: Int -> ByteString -> BB.Builder
quotedBytes count bytes = "\"" <> escapeBytes count bytes <> "\""

-- | The first @count@ bytes, escaped.  The test for @${@ looks one byte
-- past the cut, as upstream's does, so a @$@ shown last is escaped when
-- the @{@ after it was cut.
escapeBytes :: Int -> ByteString -> BB.Builder
escapeBytes count bytes = case BC.uncons bytes of
  Just (c, rest) | count > 0 -> escapeChar c rest <> escapeBytes (count - 1) rest
  _ -> mempty

escapeChar :: Char -> ByteString -> BB.Builder
escapeChar c rest = case c of
  '"' -> "\\\""
  '\\' -> "\\\\"
  '\n' -> "\\n"
  '\r' -> "\\r"
  '\t' -> "\\t"
  '$' | "{" `BS.isPrefixOf` rest -> "\\$"
  _ -> BB.char8 c

-- | Upstream's @printElided@, with its @pluralize@: how many of a thing a
-- bound left out.
elided :: Int -> Text -> Text -> Text
elided 1 single _ = marker ("1 " <> single <> " elided")
elided count _ plural = marker (T.pack (show count) <> " " <> plural <> " elided")

-- | Upstream brackets its markers in guillemets (U+00AB, U+00BB), written
-- as escapes to keep the source ASCII.
marker :: Text -> Text
marker inner = "\x00AB" <> inner <> "\x00BB"

-- | Rendered bytes as message text.
builderText :: BB.Builder -> Text
builderText = bytesToTextLossy . BL.toStrict . BB.toLazyByteString

-- ---------------------------------------------------------------------------
-- The eval result
-- ---------------------------------------------------------------------------

-- | Render a value as @nix-instantiate --eval@ prints its result, through
-- upstream's @printAmbiguous@ (print-ambiguous.cc at 2.24.9), without the
-- newline the command writes after it.  A string prints as its own bytes,
-- escaped as 'printValue' escapes it and never cut, and a float in a
-- default ostream's six-digit form, as 'formatXmlFloat' writes it.
printAmbiguous :: NixValue -> BB.Builder
printAmbiguous val = case val of
  VInt n -> BB.int64Dec n
  VFloat f -> TE.encodeUtf8Builder (formatXmlFloat f)
  VBool True -> "true"
  VBool False -> "false"
  VNull -> "null"
  VStr bytes _ -> quotedBytes (BS.length bytes) bytes
  VPath p -> TE.encodeUtf8Builder p
  VList cl -> bracketed "[" "]" (map (printElement . Thunk) (clistThunks cl))
  VAttrs attrs -> bracketed "{" "}" (map printAttribute (attrSetToAscList attrs))
  VLambda {} -> "<LAMBDA>"
  VBuiltin _ [] -> "<PRIMOP>"
  VBuiltin _ _ -> "<PRIMOP-APP>"
  -- A compiled pattern exists only among the arguments of a partially
  -- applied match or split, so it prints as that application does.
  VCompiledRegex _ -> "<PRIMOP-APP>"
  where
    printAttribute (name, thunk) =
      printIdentifier (TE.encodeUtf8 name) <> " = " <> printElement thunk <> ";"

-- | Upstream follows the opening bracket and every item with a space, so
-- an empty list is @[ ]@.  Both printers bracket this way.
bracketed :: (IsString s, Monoid s) => s -> s -> [s] -> s
bracketed open close items = open <> " " <> foldMap (<> " ") items <> close

-- | A list element or attribute value: its value when it has one, and
-- upstream's marker for a thunk when it was never forced.
printElement :: Thunk -> BB.Builder
printElement thunk = maybe "<CODE>" printAmbiguous (readThunkValue thunk)

-- | Upstream's @printIdentifier@ (print.cc), which @printAmbiguous@ writes
-- an attribute name through (nixexpr.cc's @operator<<@ on a symbol): a
-- name the lexer reads back as an identifier prints bare, and any other
-- prints as a string literal.  The test is on bytes, so a byte past ASCII
-- always quotes.
printIdentifier :: ByteString -> BB.Builder
printIdentifier name
  | isBareIdentifier name = BB.byteString name
  | otherwise = quotedBytes (BS.length name) name

isBareIdentifier :: ByteString -> Bool
isBareIdentifier name = case BC.uncons name of
  Just (first, rest) ->
    identifierStart first
      && BC.all identifierChar rest
      && name `notElem` reservedKeywords
  Nothing -> False
  where
    identifierStart c = isAsciiLower c || isAsciiUpper c || c == '_'
    identifierChar c = identifierStart c || isDigit c || c == '\'' || c == '-'

-- | Upstream's @isReservedKeyword@ set (print.cc).  @or@ is not in it: the
-- lexer accepts @or@ as an attribute name.
reservedKeywords :: [ByteString]
reservedKeywords = ["if", "then", "else", "assert", "with", "let", "in", "rec", "inherit"]

-- ---------------------------------------------------------------------------
-- Floats
-- ---------------------------------------------------------------------------

-- | Exponent suffix of the XML float layout, as printf writes it: sign
-- always present, magnitude zero-padded to at least two digits (@+05@,
-- @-21@).
signedExponent :: Int -> String
signedExponent e
  | e < 0 = '-' : padded (negate e)
  | otherwise = '+' : padded e
  where
    padded n
      | n < 10 = '0' : show n
      | otherwise = show n

-- | Format a float as upstream @toXML@ and @printValue@ render one - C++
-- @operator<<@ on a default-format ostream: 6 significant digits, trailing
-- zeros stripped, plain decimal only for decimal exponents in [-4, 5],
-- otherwise @d.ddde+XX@ with a signed exponent of at least two digits.
-- Rounding is half-even on the exact binary value, matching a
-- correctly-rounded printf.
formatXmlFloat :: Double -> Text
formatXmlFloat d
  | isNaN d = "nan"
  | isInfinite d = if d > 0 then "inf" else "-inf"
  | d == 0 = if isNegativeZero d then "-0" else "0"
  | d < 0 = "-" <> formatXmlFloat (negate d)
  | otherwise = T.pack (xmlFloatPositive d)

-- | 6-significant-digit @%g@ layout of a positive finite double.
xmlFloatPositive :: Double -> String
xmlFloatPositive d =
  let exact = toRational d
      roughExp = decimalExponentOf exact
      rounded = round (exact * 10 ^^ (xmlSigDigits - 1 - roughExp)) :: Integer
      -- Rounding can carry into a new leading digit (999999.9 -> 1000000).
      (sigDigits, pointExp) =
        if rounded >= 10 ^ xmlSigDigits
          then (rounded `div` 10, roughExp + 1)
          else (rounded, roughExp)
      digits = show sigDigits
   in if xmlMinFixedExp <= pointExp && pointExp < xmlSigDigits
        then fixedForm digits pointExp
        else sciForm digits pointExp
  where
    fixedForm digits pointExp
      | pointExp >= 0 =
          let (intPart, fracPart) = splitAt (pointExp + 1) digits
           in joinFraction intPart (stripTrailingZeros fracPart)
      | otherwise =
          joinFraction "0" (stripTrailingZeros (replicate (negate pointExp - 1) '0' <> digits))
    sciForm digits pointExp =
      joinFraction (take 1 digits) (stripTrailingZeros (drop 1 digits))
        <> "e"
        <> signedExponent pointExp
    joinFraction intPart fracPart
      | null fracPart = intPart
      | otherwise = intPart <> "." <> fracPart
    stripTrailingZeros = reverse . dropWhile (== '0') . reverse

-- | @%g@ default precision: 6 significant digits.
xmlSigDigits :: Int
xmlSigDigits = 6

-- | @%g@ switches to scientific below a decimal exponent of -4.
xmlMinFixedExp :: Int
xmlMinFixedExp = -4

-- | The decimal exponent @e@ of a positive rational: the unique @e@ with
-- @10^e <= r < 10^(e+1)@.  A float log gives the estimate; the exact
-- comparisons correct it, since the log is off by one near powers of ten.
decimalExponentOf :: Rational -> Int
decimalExponentOf r = correct (floor (logBase 10 (fromRational r :: Double)))
  where
    correct e
      | 10 ^^ e > r = correct (e - 1)
      | 10 ^^ (e + 1) <= r = correct (e + 1)
      | otherwise = e