Add builtins.toStringDebug

Added `builtins.toStringDebug`, which formats a value as a string for
debugging purposes. Unlike `builtins.toString`, `builtins.toStringDebug`
will never error and will always produce human-readable, pretty-printed
output (including for expressions that error). This makes it ideal for
interpolation into `builtins.trace` calls and `assert` messages.

(cherry picked from commit 3af61fec55b1bf882d67cc81d874c76c555d058a)

Upstream-PR: https://github.com/NixOS/nix/pull/10206
Change-Id: I2c778d3dea3c797a2eda8a4be5cf0e944ab54225
This commit is contained in:
Rebecca Turner 2024-03-09 18:03:26 -08:00 committed by Qyriad
parent 76b45b4861
commit 8a666d24aa
8 changed files with 86 additions and 4 deletions

View file

@ -0,0 +1,10 @@
---
synopsis: Add `builtins.toStringDebug`
prs:
---
Added `builtins.toStringDebug`, which formats a value as a string for debugging
purposes. Unlike `builtins.toString`, `builtins.toStringDebug` will never error
and will always produce human-readable, pretty-printed output (including for
expressions that error). This makes it ideal for interpolation into
`builtins.trace` calls and `assert` messages.

View file

@ -9,6 +9,7 @@
#include "json-to-value.hh" #include "json-to-value.hh"
#include "names.hh" #include "names.hh"
#include "path-references.hh" #include "path-references.hh"
#include "print-options.hh"
#include "store-api.hh" #include "store-api.hh"
#include "util.hh" #include "util.hh"
#include "value-to-json.hh" #include "value-to-json.hh"
@ -979,7 +980,7 @@ static void prim_trace(EvalState & state, const PosIdx pos, Value * * args, Valu
if (args[0]->type() == nString) if (args[0]->type() == nString)
printError("trace: %1%", args[0]->string.s); printError("trace: %1%", args[0]->string.s);
else else
printError("trace: %1%", ValuePrinter(state, *args[0])); printError("trace: %1%", ValuePrinter(state, *args[0], debugPrintOptions));
if (evalSettings.builtinsTraceDebugger && state.debugRepl && !state.debugTraces.empty()) { if (evalSettings.builtinsTraceDebugger && state.debugRepl && !state.debugTraces.empty()) {
const DebugTrace & last = state.debugTraces.front(); const DebugTrace & last = state.debugTraces.front();
state.runDebugRepl(nullptr, last.env, last.expr); state.runDebugRepl(nullptr, last.env, last.expr);

View file

@ -0,0 +1,32 @@
#include "primops.hh"
#include "print-options.hh"
namespace nix {
static void prim_toStringDebug(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
v.mkString(printValue(state, *args[0], debugPrintOptions));
}
static RegisterPrimOp primop_toStringDebug({
.name = "toStringDebug",
.args = {"value"},
.doc = R"(
Format a value as a string for debugging purposes.
Unlike [`toString`](@docroot@/language/builtins.md#builtins-toString),
`toStringDebug` will never error and will always produce human-readable
output (including for values that throw errors). For this reason,
`toStringDebug` is ideal for interpolation into messages in
[`trace`](@docroot@/language/builtins.md#builtins-trace)
calls and [`assert`](@docroot@/language/constructs.html#assertions)
statements.
Output will be pretty-printed and include ANSI escape sequences.
If the value contains too many values (for instance, more than 32
attributes or list items), some values will be elided.
)",
.fun = prim_toStringDebug,
});
}

View file

@ -117,4 +117,21 @@ static PrintOptions errorPrintOptions = PrintOptions {
.maxStringLength = 1024, .maxStringLength = 1024,
}; };
/**
* `PrintOptions` for unknown and therefore potentially large values in
* debugging contexts, to avoid printing "too much" output.
*
* This is like `errorPrintOptions`, but prints more values.
*/
static PrintOptions debugPrintOptions = PrintOptions {
.ansiColors = true,
.force = true,
.derivationPaths = true,
.maxDepth = 15,
.maxAttrs = 32,
.maxListItems = 32,
.maxStringLength = 1024,
.prettyIndent = 2
};
} }

View file

@ -554,11 +554,24 @@ public:
} }
}; };
/**
* Print the given value to `output`.
*/
void printValue(EvalState & state, std::ostream & output, Value & v, PrintOptions options) void printValue(EvalState & state, std::ostream & output, Value & v, PrintOptions options)
{ {
Printer(output, state, options).print(v); Printer(output, state, options).print(v);
} }
/**
* Print the given value to a new string.
*/
std::string printValue(EvalState & state, Value & v, PrintOptions options)
{
std::ostringstream output;
printValue(state, output, v, options);
return output.str();
}
std::ostream & operator<<(std::ostream & output, const ValuePrinter & printer) std::ostream & operator<<(std::ostream & output, const ValuePrinter & printer)
{ {
printValue(printer.state, output, printer.value, printer.options); printValue(printer.state, output, printer.value, printer.options);

View file

@ -47,6 +47,8 @@ std::ostream & printIdentifier(std::ostream & o, std::string_view s);
void printValue(EvalState & state, std::ostream & str, Value & v, PrintOptions options = PrintOptions {}); void printValue(EvalState & state, std::ostream & str, Value & v, PrintOptions options = PrintOptions {});
std::string printValue(EvalState & state, Value & v, PrintOptions options = PrintOptions {});
/** /**
* A partially-applied form of `printValue` which can be formatted using `<<` * A partially-applied form of `printValue` which can be formatted using `<<`
* without allocating an intermediate string. * without allocating an intermediate string.

View file

@ -28,10 +28,15 @@ expectStderr 1 nix-instantiate --show-trace --eval -E 'builtins.addErrorContext
expectStderr 1 nix-instantiate --show-trace --eval -E 'builtins.addErrorContext "Hello %" (throw "Foo")' | grepQuiet 'Hello %' expectStderr 1 nix-instantiate --show-trace --eval -E 'builtins.addErrorContext "Hello %" (throw "Foo")' | grepQuiet 'Hello %'
nix-instantiate --eval -E 'let x = builtins.trace { x = x; } true; in x' \ nix-instantiate --eval -E 'let x = builtins.trace { x = x; } true; in x' \
2>&1 | grepQuiet -E 'trace: { x = «potential infinite recursion»; }' 2>&1 | grepQuiet -F "trace: {
x = «potential infinite recursion»;
}"
nix-instantiate --eval -E 'let x = { repeating = x; tracing = builtins.trace x true; }; in x.tracing'\ nix-instantiate --eval -E 'let x = { repeating = x; tracing = builtins.trace x true; }; in x.tracing'\
2>&1 | grepQuiet -F 'trace: { repeating = «repeated»; tracing = «potential infinite recursion»; }' 2>&1 | grepQuiet -F "trace: {
repeating = «repeated»;
tracing = «potential infinite recursion»;
}"
set +x set +x

View file

@ -1 +1,3 @@
trace: [ «thunk» ] trace: [
2
]