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 Jan 4, 2024
1 parent 4858671 commit d6a3ee5
Show file tree
Hide file tree
Showing 14 changed files with 1,033 additions and 274 deletions.
158 changes: 15 additions & 143 deletions src/libcmd/repl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,17 @@ 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,
.derivationPaths = true,
.maxDepth = maxDepth
});
}
};

std::string removeWhitespace(std::string s)
Expand Down Expand Up @@ -708,7 +716,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 +779,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 +902,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, 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
126 changes: 26 additions & 100 deletions src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -104,117 +104,23 @@ 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:
if (!isBlackhole()) {
str << "<CODE>";
} else {
// 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;
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;
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);
}

// Pretty print types for assertion errors
std::ostream & operator << (std::ostream & os, const ValueType t) {
os << showType(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();
}

void Value::print(EvalState & state, std::ostream & str, PrintOptions options)
{
printValue(state, str, *this, options);
}

const Value * getPrimOp(const Value &v) {
const Value * primOp = &v;
Expand Down Expand Up @@ -709,6 +615,26 @@ void PrimOp::check()
}


std::ostream & operator<<(std::ostream & output, PrimOp & primOp)
{
output << "primop " << primOp.name;
return output;
}


PrimOp * Value::primOpAppPrimOp() const
{
Value * left = primOpApp.left;
while (left && !left->isPrimOp()) {
left = left->primOpApp.left;
}

if (!left)
return nullptr;
return left->primOp;
}


void Value::mkPrimOp(PrimOp * p)
{
p->check();
Expand Down
4 changes: 3 additions & 1 deletion src/libexpr/eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ struct PrimOp
void check();
};

std::ostream & operator<<(std::ostream & output, PrimOp & primOp);

/**
* Info about a constant
*/
Expand Down Expand Up @@ -127,7 +129,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
Loading

0 comments on commit d6a3ee5

Please sign in to comment.