Skip to content

Commit

Permalink
Unify and refactor value printing
Browse files Browse the repository at this point in the history
Previously, there were two mostly-identical value printers -- one in
`libexpr/eval.cc` (which didn't force values) and one in
`libcmd/repl.cc` (which did force values and also printed ANSI color
codes).

This PR unifies both of these printers into `print.cc` and provides a
`PrintOptions` struct for controlling the output, which allows for
toggling whether values are forced, whether repeated values are tracked,
and whether ANSI color codes are displayed.

Additionally, `PrintOptions` allows tuning the maximum number of
attributes, list items, and bytes in a string that will be displayed;
this makes it ideal for contexts where printing too much output (e.g.
all of Nixpkgs) is distracting. (As requested by @roberth in
NixOS#9554 (comment))

Please read the tests for example output.

Future work:
- It would be nice to provide this function as a builtin, perhaps
  `builtins.toStringDebug` -- a printing function that never fails would
  be useful when debugging Nix code.
- It would be nice to support customizing `PrintOptions` members on the
  command line, e.g. `--option to-string-max-attrs 1000`.
  • Loading branch information
9999years committed Dec 13, 2023
1 parent 0dfa66d commit aed9930
Show file tree
Hide file tree
Showing 12 changed files with 839 additions and 308 deletions.
157 changes: 14 additions & 143 deletions src/libcmd/repl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,16 @@ struct NixRepl
void evalString(std::string s, Value & v);
void loadDebugTraceEnv(DebugTrace & dt);

typedef std::set<Value *> ValuesSeen;
std::ostream & printValue(std::ostream & str, Value & v, unsigned int maxDepth);
std::ostream & printValue(std::ostream & str, Value & v, unsigned int maxDepth, ValuesSeen & seen);
void printValue(std::ostream & str,
Value & v,
unsigned int maxDepth = std::numeric_limits<unsigned int>::max())
{
::nix::printValue(*state, str, v, PrintOptions {
.ansiColors = true,
.force = true,
.maxDepth = maxDepth
});
}
};

std::string removeWhitespace(std::string s)
Expand Down Expand Up @@ -708,7 +715,8 @@ bool NixRepl::processLine(std::string line)
else if (command == ":p" || command == ":print") {
Value v;
evalString(arg, v);
printValue(std::cout, v, 1000000000) << std::endl;
printValue(std::cout, v);
std::cout << std::endl;
}

else if (command == ":q" || command == ":quit") {
Expand Down Expand Up @@ -770,7 +778,8 @@ bool NixRepl::processLine(std::string line)
} else {
Value v;
evalString(line, v);
printValue(std::cout, v, 1) << std::endl;
printValue(std::cout, v, 1);
std::cout << std::endl;
}
}

Expand Down Expand Up @@ -892,144 +901,6 @@ void NixRepl::evalString(std::string s, Value & v)
}


std::ostream & NixRepl::printValue(std::ostream & str, Value & v, unsigned int maxDepth)
{
ValuesSeen seen;
return printValue(str, v, maxDepth, seen);
}




// FIXME: lot of cut&paste from Nix's eval.cc.
std::ostream & NixRepl::printValue(std::ostream & str, Value & v, unsigned int maxDepth, ValuesSeen & seen)
{
str.flush();
checkInterrupt();

state->forceValue(v, [&]() { return v.determinePos(noPos); });

switch (v.type()) {

case nInt:
str << ANSI_CYAN << v.integer << ANSI_NORMAL;
break;

case nBool:
str << ANSI_CYAN;
printLiteralBool(str, v.boolean);
str << ANSI_NORMAL;
break;

case nString:
str << ANSI_WARNING;
printLiteralString(str, v.string_view());
str << ANSI_NORMAL;
break;

case nPath:
str << ANSI_GREEN << v.path().to_string() << ANSI_NORMAL; // !!! escaping?
break;

case nNull:
str << ANSI_CYAN "null" ANSI_NORMAL;
break;

case nAttrs: {
seen.insert(&v);

bool isDrv = state->isDerivation(v);

if (isDrv) {
str << "«derivation ";
Bindings::iterator i = v.attrs->find(state->sDrvPath);
NixStringContext context;
if (i != v.attrs->end())
str << state->store->printStorePath(state->coerceToStorePath(i->pos, *i->value, context, "while evaluating the drvPath of a derivation"));
else
str << "???";
str << "»";
}

else if (maxDepth > 0) {
str << "{ ";

typedef std::map<std::string, Value *> Sorted;
Sorted sorted;
for (auto & i : *v.attrs)
sorted.emplace(state->symbols[i.name], i.value);

for (auto & i : sorted) {
printAttributeName(str, i.first);
str << " = ";
if (seen.count(i.second))
str << "«repeated»";
else
try {
printValue(str, *i.second, maxDepth - 1, seen);
} catch (AssertionError & e) {
str << ANSI_RED "«error: " << e.msg() << "»" ANSI_NORMAL;
}
str << "; ";
}

str << "}";
} else
str << "{ ... }";

break;
}

case nList:
seen.insert(&v);

str << "[ ";
if (maxDepth > 0)
for (auto elem : v.listItems()) {
if (seen.count(elem))
str << "«repeated»";
else
try {
printValue(str, *elem, maxDepth - 1, seen);
} catch (AssertionError & e) {
str << ANSI_RED "«error: " << e.msg() << "»" ANSI_NORMAL;
}
str << " ";
}
else
str << "... ";
str << "]";
break;

case nFunction:
if (v.isLambda()) {
std::ostringstream s;
s << state->positions[v.lambda.fun->pos];
str << ANSI_BLUE "«lambda @ " << filterANSIEscapes(s.str()) << "»" ANSI_NORMAL;
} else if (v.isPrimOp()) {
str << ANSI_MAGENTA "«primop»" ANSI_NORMAL;
} else if (v.isPrimOpApp()) {
str << ANSI_BLUE "«primop-app»" ANSI_NORMAL;
} else {
abort();
}
break;

case nFloat:
str << v.fpoint;
break;

case nThunk:
case nExternal:
default:
str << ANSI_RED "«unknown»" ANSI_NORMAL;
break;
}

return str;
}


std::unique_ptr<AbstractNixRepl> AbstractNixRepl::create(
const SearchPath & searchPath, nix::ref<Store> store, ref<EvalState> state,
std::function<AnnotatedValues()> getValues)
Expand Down
102 changes: 5 additions & 97 deletions src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -103,101 +103,9 @@ RootValue allocRootValue(Value * v)
#endif
}

void Value::print(const SymbolTable &symbols, std::ostream &str,
std::set<const void *> *seen, int depth) const

{
checkInterrupt();

if (depth <= 0) {
str << "«too deep»";
return;
}
switch (internalType) {
case tInt:
str << integer;
break;
case tBool:
printLiteralBool(str, boolean);
break;
case tString:
printLiteralString(str, string_view());
break;
case tPath:
str << path().to_string(); // !!! escaping?
break;
case tNull:
str << "null";
break;
case tAttrs: {
if (seen && !attrs->empty() && !seen->insert(attrs).second)
str << "«repeated»";
else {
str << "{ ";
for (auto & i : attrs->lexicographicOrder(symbols)) {
str << symbols[i->name] << " = ";
i->value->print(symbols, str, seen, depth - 1);
str << "; ";
}
str << "}";
}
break;
}
case tList1:
case tList2:
case tListN:
if (seen && listSize() && !seen->insert(listElems()).second)
str << "«repeated»";
else {
str << "[ ";
for (auto v2 : listItems()) {
if (v2)
v2->print(symbols, str, seen, depth - 1);
else
str << "(nullptr)";
str << " ";
}
str << "]";
}
break;
case tThunk:
case tApp:
str << "<CODE>";
break;
case tLambda:
str << "<LAMBDA>";
break;
case tPrimOp:
str << "<PRIMOP>";
break;
case tPrimOpApp:
str << "<PRIMOP-APP>";
break;
case tExternal:
str << *external;
break;
case tFloat:
str << fpoint;
break;
case tBlackhole:
// Although we know for sure that it's going to be an infinite recursion
// when this value is accessed _in the current context_, it's likely
// that the user will misinterpret a simpler «infinite recursion» output
// as a definitive statement about the value, while in fact it may be
// a valid value after `builtins.trace` and perhaps some other steps
// have completed.
str << "«potential infinite recursion»";
break;
default:
printError("Nix evaluator internal error: Value::print(): invalid value type %1%", internalType);
abort();
}
}

void Value::print(const SymbolTable &symbols, std::ostream &str,
bool showRepeated, int depth) const {
std::set<const void *> seen;
print(symbols, str, showRepeated ? nullptr : &seen, depth);
void Value::print(EvalState &state, std::ostream &str,
PrintOptions options) {
printValue(state, str, *this, options);
}

// Pretty print types for assertion errors
Expand All @@ -206,10 +114,10 @@ std::ostream & operator << (std::ostream & os, const ValueType t) {
return os;
}

std::string printValue(const EvalState & state, const Value & v)
std::string printValue(EvalState & state, Value & v)
{
std::ostringstream out;
v.print(state.symbols, out);
v.print(state, out);
return out.str();
}

Expand Down
2 changes: 1 addition & 1 deletion src/libexpr/eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ std::unique_ptr<ValMap> mapStaticEnvBindings(const SymbolTable & st, const Stati
void copyContext(const Value & v, NixStringContext & context);


std::string printValue(const EvalState & state, const Value & v);
std::string printValue(EvalState & state, Value & v);
std::ostream & operator << (std::ostream & os, const ValueType t);


Expand Down
45 changes: 45 additions & 0 deletions src/libexpr/print-options.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#pragma once
/**
* @file
* @brief Options for printing Nix values.
*/

namespace nix {

/**
* Options for printing Nix values.
*/
struct PrintOptions
{
/**
* If true, output ANSI color sequences.
*/
bool ansiColors = false;
/**
* If true, force values.
*/
bool force = false;
/**
* If true, track which values have been printed and skip them on
* subsequent encounters. Useful for self-referential values.
*/
bool trackRepeated = true;
/**
* Maximum depth to evaluate to.
*/
unsigned int maxDepth = std::numeric_limits<unsigned int>::max();
/**
* Maximum number of attributes in an attribute set to print.
*/
unsigned int maxAttrs = std::numeric_limits<unsigned int>::max();
/**
* Maximum number of list items to print.
*/
unsigned int maxListItems = std::numeric_limits<unsigned int>::max();
/**
* Maximum string length to print.
*/
unsigned int maxStringLength = std::numeric_limits<unsigned int>::max();
};

}
Loading

0 comments on commit aed9930

Please sign in to comment.