From ef4f59ea9912c7016654a02c7c5fac574462a9e3 Mon Sep 17 00:00:00 2001 From: Rebecca Turner Date: Sat, 9 Mar 2024 18:03:26 -0800 Subject: [PATCH 1/2] 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 --- doc/manual/rl-next/to-string-debug.md | 11 +++++++ 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, 87 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 000000000..560cad59a --- /dev/null +++ b/doc/manual/rl-next/to-string-debug.md @@ -0,0 +1,11 @@ +--- +synopsis: Add `builtins.toStringDebug` +prs: 10206 +cls: 967 +--- + +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 33a2688f1..73e1d2d06 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 "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) printError("trace: %1%", args[0]->string.s); 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 000000000..5287bc94c --- /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 080ba26b8..56e2ec713 100644 --- a/src/libexpr/print-options.hh +++ b/src/libexpr/print-options.hh @@ -117,4 +117,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 231bde0a0..201260267 100644 --- a/src/libexpr/print.cc +++ b/src/libexpr/print.cc @@ -554,11 +554,24 @@ public: } }; +/** + * 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 42826d94d..aa223cf92 100644 --- a/src/libexpr/print.hh +++ b/src/libexpr/print.hh @@ -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 {}); +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 94c00bad0..e7a9147f1 100755 --- a/tests/functional/lang.sh +++ b/tests/functional/lang.sh @@ -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 %' 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 80aa17c6e..6129a3892 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 +] From 62f670e11533dc29767b0cae2826296f1532e24e Mon Sep 17 00:00:00 2001 From: Rebecca Turner Date: Sat, 9 Mar 2024 19:37:11 -0800 Subject: [PATCH 2/2] Add `builtins.toStringDebugOptions` (cherry picked from commit adcb2637ca71d3d4a37bbf131a21b15e4044fa59) Upstream-PR: https://github.com/NixOS/nix/pull/10206 Change-Id: I373ecf9d107052152df703c3a1208364a9842305 --- doc/manual/rl-next/to-string-debug.md | 3 + src/libexpr/primops/toStringDebug.cc | 63 +++++++++++++++++++- src/libexpr/print-options.cc | 85 +++++++++++++++++++++++++++ src/libexpr/print-options.hh | 10 ++++ 4 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 src/libexpr/print-options.cc diff --git a/doc/manual/rl-next/to-string-debug.md b/doc/manual/rl-next/to-string-debug.md index 560cad59a..873bf4ae5 100644 --- a/doc/manual/rl-next/to-string-debug.md +++ b/doc/manual/rl-next/to-string-debug.md @@ -9,3 +9,6 @@ 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. + +A variant, `builtins.toStringDebugOptions`, accepts as its first argument a set +of options for additional control over the output. diff --git a/src/libexpr/primops/toStringDebug.cc b/src/libexpr/primops/toStringDebug.cc index 5287bc94c..c81323be2 100644 --- a/src/libexpr/primops/toStringDebug.cc +++ b/src/libexpr/primops/toStringDebug.cc @@ -1,5 +1,6 @@ #include "primops.hh" #include "print-options.hh" +#include "value.hh" namespace nix { @@ -22,11 +23,67 @@ static RegisterPrimOp primop_toStringDebug({ 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. + Output may change in future Nix versions. Currently, output is + 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, }); +static void prim_toStringDebugOptions(EvalState & state, const PosIdx pos, Value * * args, Value & v) +{ + auto options = PrintOptions::fromValue(state, *args[0]); + v.mkString(printValue(state, *args[1], options)); +} + +static RegisterPrimOp primop_toStringDebugOptions({ + .name = "toStringDebugOptions", + .args = {"options", "value"}, + .doc = R"( + Format a value as a string for debugging purposes. + + Like + [`toStringDebug`](@docroot@/language/builtins.md#builtins-toStringDebug) + but accepts an additional attribute set of arguments as its first value: + + - `ansiColors` (boolean, default `true`): Whether or not to include ANSI + escapes for coloring in the output. + - `force` (boolean, default `true`): Whether or not to force values while + printing output. + - `derivationPaths` (boolean, default `true`): If `force` is set, print + derivations as `.drv` paths instead of as attribute sets. + - `trackRepeated` (boolean, default `true`): Whether or not to track + repeated values while printing output. This will help avoid excessive + output while printing self-referential structures. The specific cycle + detection algorithm may not detect all repeated values and may change + between releases. + - `maxDepth` (integer, default 15): The maximum depth to print values to. + Depth is increased when printing nested lists and attribute sets. If + `maxDepth` is -1, values will be printed to unlimited depth (or until + Nix crashes). + - `maxAttrs` (integer, default 32): The maximum number of attributes to + print in attribute sets. Further attributes will be replaced with a + `«234 attributes elided»` message. Note that this is the maximum number + of attributes to print for the entire `toStringDebugOptions` call (if + it were per-attribute set, it would be possible for + `toStringDebugOptions` to produce essentially unbounded output). If + `maxAttrs` is -1, all attributes will be printed. + - `maxListItems` (integer, default 32): The maximum number of list items to + print. Further items will be replaced with a `«234 items elided»` + message. If `maxListItems` is -1, all items will be printed. + - `maxStringLength` (integer, default 1024): The maximum number of bytes + to print of strings. Further data will be replaced with a `«234 bytes + elided»` message. If `maxStringLength` is -1, full strings will be + printed. + - `prettyIndent` (integer, default 2): The number of spaces of indent to + use when pretty-printing values. If `prettyIndent` is 0, values will be + printed on a single line. + + Missing attributes will be substituted with a default value. Default + values may change between releases. + )", + .fun = prim_toStringDebugOptions, +}); + } diff --git a/src/libexpr/print-options.cc b/src/libexpr/print-options.cc new file mode 100644 index 000000000..09a8cbd6f --- /dev/null +++ b/src/libexpr/print-options.cc @@ -0,0 +1,85 @@ +#include + +#include "print-options.hh" +#include "value.hh" +#include "eval.hh" + +namespace nix { + +namespace { +static std::string_view ERROR_CONTEXT = "while constructing printing options"; +} + +size_t nixIntToSizeT(EvalState & state, Value & v, NixInt i, bool minusOneIsMax) +{ + if (minusOneIsMax && i == -1) { + return std::numeric_limits::max(); + } + + try { + return boost::numeric_cast(i); + } catch (boost::numeric::bad_numeric_cast & e) { + state.error( + "Failed to convert integer to `size_t`: %1%", e.what() + ) + .atPos(v) + .debugThrow(); + } +} + +bool boolAttr(EvalState & state, Value & v, std::string_view attrName, bool defaultValue) +{ + auto attr = v.attrs->find(state.symbols.create(attrName)); + if (attr != v.attrs->end()) { + return state.forceBool(*attr->value, attr->pos, ERROR_CONTEXT); + } else { + return defaultValue; + } +} + +size_t intAttr(EvalState & state, Value & v, std::string_view attrName, size_t defaultValue, bool minusOneIsMax) +{ + auto attr = v.attrs->find(state.symbols.create(attrName)); + if (attr != v.attrs->end()) { + return nixIntToSizeT( + state, + v, + state.forceInt(*attr->value, attr->pos, ERROR_CONTEXT), + minusOneIsMax + ); + } else { + return defaultValue; + } +} + +PrintOptions PrintOptions::fromValue(EvalState & state, Value & v) +{ + state.forceAttrs( + v, [v]() { return v.determinePos(noPos); }, ERROR_CONTEXT); + + auto ansiColors = boolAttr(state, v, "ansiColors", true); + auto force = boolAttr(state, v, "force", true); + auto derivationPaths = boolAttr(state, v, "derivationPaths", true); + auto trackRepeated = boolAttr(state, v, "trackRepeated", true); + + auto maxDepth = intAttr(state, v, "trackRepeated", 15, true); + auto maxAttrs = intAttr(state, v, "maxAttrs", 32, true); + auto maxListItems = intAttr(state, v, "maxListItems", 32, true); + auto maxStringLength = intAttr(state, v, "maxStringLength", 1024, true); + + auto prettyIndent = intAttr(state, v, "prettyIndent", 2, false); + + return PrintOptions { + .ansiColors = ansiColors, + .force = force, + .derivationPaths = derivationPaths, + .trackRepeated = trackRepeated, + .maxDepth = maxDepth, + .maxAttrs = maxAttrs, + .maxListItems = maxListItems, + .maxStringLength = maxStringLength, + .prettyIndent = prettyIndent, + }; +} + +} diff --git a/src/libexpr/print-options.hh b/src/libexpr/print-options.hh index 56e2ec713..e24bf14ec 100644 --- a/src/libexpr/print-options.hh +++ b/src/libexpr/print-options.hh @@ -8,6 +8,9 @@ namespace nix { +struct Value; +class EvalState; + /** * How errors should be handled when printing values. */ @@ -36,6 +39,13 @@ enum class ErrorPrintBehavior { */ struct PrintOptions { + /** + * Construct `PrintOptions` from a Nix `Value`. + * + * See `builtins.toStringDebugOptions` for details on the format. + */ + static PrintOptions fromValue(EvalState & state, Value & v); + /** * If true, output ANSI color sequences. */