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/nix#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`.

(cherry picked from commit 0fa08b4, )

===

Restore ambiguous value printer for `nix-instantiate`

The Nix team has requested that this output format remain unchanged.
I've added a warning to the man page explaining that `nix-instantiate
--eval` output will not parse correctly in many situations.

(cherry picked from commit df84dd4)

Change-Id: I7cca6b4b53cd0642f2d49af657d5676a8554c9f8
  • Loading branch information
eldritch horrors committed Mar 9, 2024
1 parent 0e8f505 commit 512c1f0
Show file tree
Hide file tree
Showing 20 changed files with 1,351 additions and 296 deletions.
80 changes: 59 additions & 21 deletions doc/manual/src/command-ref/nix-instantiate.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,50 @@ standard input.

- `--parse`\
Just parse the input files, and print their abstract syntax trees on
standard output in ATerm format.
standard output as a Nix expression.

- `--eval`\
Just parse and evaluate the input files, and print the resulting
values on standard output. No instantiation of store derivations
takes place.

> **Warning**
>
> This option produces ambiguous output which is not suitable for machine
> consumption. For example, these two Nix expressions print the same result
> despite having different types:
>
> ```console
> $ nix-instantiate --eval --expr '{ a = {}; }'
> { a = <CODE>; }
> $ nix-instantiate --eval --expr '{ a = <CODE>; }'
> { a = <CODE>; }
> ```
>
> For human-readable output, `nix eval` (experimental) is more informative:
>
> ```console
> $ nix-instantiate --eval --expr 'a: a'
> <LAMBDA>
> $ nix eval --expr 'a: a'
> «lambda @ «string»:1:1»
> ```
>
> For machine-readable output, the `--xml` option produces unambiguous
> output:
>
> ```console
> $ nix-instantiate --eval --xml --expr '{ foo = <CODE>; }'
> <?xml version='1.0' encoding='utf-8'?>
> <expr>
> <attrs>
> <attr column="3" line="1" name="foo">
> <unevaluated />
> </attr>
> </attrs>
> </expr>
> ```
- `--find-file`\
Look up the given files in Nix’s search path (as specified by the
`NIX_PATH` environment variable). If found, print the corresponding
Expand All @@ -61,11 +98,11 @@ standard input.
- `--json`\
When used with `--eval`, print the resulting value as an JSON
representation of the abstract syntax tree rather than as an ATerm.
representation of the abstract syntax tree rather than as a Nix expression.
- `--xml`\
When used with `--eval`, print the resulting value as an XML
representation of the abstract syntax tree rather than as an ATerm.
representation of the abstract syntax tree rather than as a Nix expression.
The schema is the same as that used by the [`toXML`
built-in](../language/builtins.md).
Expand Down Expand Up @@ -133,28 +170,29 @@ $ nix-instantiate --eval --xml --expr '1 + 2'
The difference between non-strict and strict evaluation:

```console
$ nix-instantiate --eval --xml --expr 'rec { x = "foo"; y = x; }'
...
<attr name="x">
<string value="foo" />
</attr>
<attr name="y">
<unevaluated />
</attr>
...
$ nix-instantiate --eval --xml --expr '{ x = {}; }'
<?xml version='1.0' encoding='utf-8'?>
<expr>
<attrs>
<attr column="3" line="1" name="x">
<unevaluated />
</attr>
</attrs>
</expr>
```

Note that `y` is left unevaluated (the XML representation doesn’t
attempt to show non-normal forms).

```console
$ nix-instantiate --eval --xml --strict --expr 'rec { x = "foo"; y = x; }'
...
<attr name="x">
<string value="foo" />
</attr>
<attr name="y">
<string value="foo" />
</attr>
...
$ nix-instantiate --eval --xml --strict --expr '{ x = {}; }'
<?xml version='1.0' encoding='utf-8'?>
<expr>
<attrs>
<attr column="3" line="1" name="x">
<attrs>
</attrs>
</attr>
</attrs>
</expr>
```
158 changes: 15 additions & 143 deletions src/libcmd/repl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,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 @@ -713,7 +721,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 @@ -775,7 +784,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 @@ -897,144 +907,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.s);
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
Loading

0 comments on commit 512c1f0

Please sign in to comment.