From 3af61fec55b1bf882d67cc81d874c76c555d058a Mon Sep 17 00:00:00 2001 From: Rebecca Turner Date: Sat, 9 Mar 2024 18:03:26 -0800 Subject: [PATCH] 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. --- doc/manual/rl-next/to-string-debug.md | 10 ++++++ src/libexpr/primops.cc | 3 +- src/libexpr/primops/toStringDebug.cc | 32 +++++++++++++++++++ src/libexpr/print-options.hh | 17 ++++++++++ src/libexpr/print.cc | 13 ++++++++ src/libexpr/print.hh | 2 ++ tests/functional/lang.sh | 9 ++++-- tests/functional/lang/eval-okay-print.err.exp | 4 ++- 8 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 doc/manual/rl-next/to-string-debug.md create mode 100644 src/libexpr/primops/toStringDebug.cc diff --git a/doc/manual/rl-next/to-string-debug.md b/doc/manual/rl-next/to-string-debug.md new file mode 100644 index 00000000000..dd75176dde3 --- /dev/null +++ b/doc/manual/rl-next/to-string-debug.md @@ -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. diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index bc2a70496e6..9010022c4d9 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -9,6 +9,7 @@ #include "json-to-value.hh" #include "names.hh" #include "path-references.hh" +#include "print-options.hh" #include "store-api.hh" #include "util.hh" #include "processes.hh" @@ -1000,7 +1001,7 @@ static void prim_trace(EvalState & state, const PosIdx pos, Value * * args, Valu if (args[0]->type() == nString) printError("trace: %1%", args[0]->string_view()); else - printError("trace: %1%", ValuePrinter(state, *args[0])); + printError("trace: %1%", ValuePrinter(state, *args[0], debugPrintOptions)); if (evalSettings.builtinsTraceDebugger && state.debugRepl && !state.debugTraces.empty()) { const DebugTrace & last = state.debugTraces.front(); state.runDebugRepl(nullptr, last.env, last.expr); diff --git a/src/libexpr/primops/toStringDebug.cc b/src/libexpr/primops/toStringDebug.cc new file mode 100644 index 00000000000..5287bc94cb9 --- /dev/null +++ b/src/libexpr/primops/toStringDebug.cc @@ -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, +}); + +} diff --git a/src/libexpr/print-options.hh b/src/libexpr/print-options.hh index 6c5e80c61d8..e868790e060 100644 --- a/src/libexpr/print-options.hh +++ b/src/libexpr/print-options.hh @@ -89,4 +89,21 @@ static PrintOptions errorPrintOptions = PrintOptions { .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 +}; + } diff --git a/src/libexpr/print.cc b/src/libexpr/print.cc index 9d280f6239c..d0f7c6f8c9d 100644 --- a/src/libexpr/print.cc +++ b/src/libexpr/print.cc @@ -592,11 +592,24 @@ class Printer } }; +/** + * Print the given value to `output`. + */ void printValue(EvalState & state, std::ostream & output, Value & v, PrintOptions options) { 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) { printValue(printer.state, output, printer.value, printer.options); diff --git a/src/libexpr/print.hh b/src/libexpr/print.hh index 7ddda81b88f..8279998a503 100644 --- a/src/libexpr/print.hh +++ b/src/libexpr/print.hh @@ -62,6 +62,8 @@ std::ostream & printIdentifier(std::ostream & o, std::string_view s); 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 `<<` * without allocating an intermediate string. diff --git a/tests/functional/lang.sh b/tests/functional/lang.sh index 12df32c8770..a6e6fbdcd3b 100755 --- a/tests/functional/lang.sh +++ b/tests/functional/lang.sh @@ -26,10 +26,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 %' 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'\ - 2>&1 | grepQuiet -F 'trace: { repeating = «repeated»; tracing = «potential infinite recursion»; }' + 2>&1 | grepQuiet -F "trace: { + repeating = «repeated»; + tracing = «potential infinite recursion»; +}" set +x diff --git a/tests/functional/lang/eval-okay-print.err.exp b/tests/functional/lang/eval-okay-print.err.exp index 80aa17c6e1d..6129a389252 100644 --- a/tests/functional/lang/eval-okay-print.err.exp +++ b/tests/functional/lang/eval-okay-print.err.exp @@ -1 +1,3 @@ -trace: [ «thunk» ] +trace: [ + 2 +]