Nix Expression Language non-determinism example
Today Vladimir Kryachko found a very curious nixpkgs bug!
Here it’s simplified version:
Good:
$ nix-instantiate -E 'let n = import <nixpkgs> {}; in n.pkgs.nix'
/nix/store/6gzxax0rl0k1n3hg0s22jnj6c1c0aj3b-nix-2.15.1.drv
Bad:
$ nix-instantiate -E 'let n = import <nixpkgs> {}; extensions = []; in n.pkgs.nix'
error:
… while calling the 'derivationStrict' builtin
at /builtin/derivation.nix:9:12: (source not available)
… while evaluating derivation 'nix-2.15.1'
whose name attribute is located at /nix/store/21nmswlqvkpbv9ykprrf7x6hvzi5djhf-source/pkgs/stdenv/generic/make-derivation.nix:300:7
… while evaluating attribute 'configureFlags' of derivation 'nix-2.15.1'
at /nix/store/21nmswlqvkpbv9ykprrf7x6hvzi5djhf-source/pkgs/stdenv/generic/make-derivation.nix:358:7:
357| # This parameter is sometimes a string, sometimes null, and sometimes a list, yuck
358| configureFlags = let inherit (lib) optional elem; in
| ^
359| configureFlags
error: assertion '(final).hasSharedLibraries' failed
How can extensions = []
affect anything? It should not!
Quick quiz: Why does crash happen? Is it a nix
bug, nixpkgs
bug or
neither?
Nix Expression Language
nix package manager uses nix expression language.
It’s a pure (immutable values) lazy (call-by-need) dynamically typed language. It is simple and short on builtin primitives. The simpler expressions are usually easy to read and reason about.
nix repl
provides great interactive environment to poke at things:
$ nix repl
nix-repl> let a = 1; b = 2; in a + b
3
nix-repl> (a: b: a + b) 1 2
3
nix-repl> 1 == 2
false
The larger expressions require some experience and care to figure out.
It has first class anonymous functions (lambdas) and attribute sets
(tables). Attribute sets are key-value pairs one can access via .
dot
operator. Attribute sets also provide merge and other operations:
nix-repl> { a = 1; b = 2; } // { b = 4; c = 5; }
{ a = 1; b = 4; c = 5; }
nix-repl> { a = 1; b = 2;}.b
2
nix-repl> { a = 1; b = 2; } == { b = 2; a = 1; }
true
nix-repl> { b = 2; a = 1; }
{ a = 1; b = 2; }
They are called sets because attribute name order is not supposed to matter.
Nix Expression Language Exceptions
It’s so simple! What could possibly go wrong?
Lazy languages frequently have a special property: among other things it’s hard to reason about code’s performance! As performance depends not just on the code that constructs the value, but also on the code that inspects the value.
It might sound like a minor optimization problem. But in practice it allows (and even encourages) to employ lazy evaluation as a flow control mechanism: infinite lists, partially initialized data structures and similar techniques are ubiquitous idioms.
Laziness can be observed in the following example:
nix-repl> let f = x: f x; in f 1
error: stack overflow (possible infinite recursion)
nix-repl> { unused = let f = x: f x; in f 1; used = 1;}.used
1
nix-repl> { unused = let f = x: f x; in f 1; used = 1;}.unused
error: stack overflow (possible infinite recursion)
Here we defined function f
which calls itself indefinitely:
f (f (f (... f (1) ) ) ... )
as long as we don’t try to print or access that unused
field with infinite recursion things work just fine.
nix expression language
also allows you to generate exceptions out of
pure code by constructing special values via throw
or assert
keywords:
nix-repl> throw "gah"
error:
… while calling the 'throw' builtin
error: gah
nix-repl> assert true; 42
42
nix-repl> assert false; 42
error: assertion 'false' failed
The actual problem
Apparently the above is enough to get non-deterministic evaluation!
The example would be a comparison of two sets with unevaluated exceptions:
nix-repl> { a = 1; b = throw "meh"; } == { a = 2; b = 1; }
false
nix-repl> { a = 1; b = throw "meh"; } == { a = 1; b = 1; }
error:
error: meh
Note how b
is not causing any troubles in the first case but fails in
the second case.
And more: if we just rename a
and b
in the first case the
behaviour will change (needs an interpreter restart):
nix-repl> { b = 1; a = throw "meh"; } == { b = 2; a = 1; }
error:
error: meh
It means that comparison operator somehow orders sets internally. If they were ordered by an attribute name it would not be too bad (the failure would be deterministic) but still annoying: alpha conversion (renames) causing evaluation difference in pure lazy languages is an unexpected property.
Let’s drop into a one-liner evaluator. How about this one:
$ nix-instantiate --eval -E '{ a = 1; b = throw "meh"; } == { a = 2; b = 1; }'
false
$ nix-instantiate --eval -E 'let b = 1; in { a = 1; b = throw "meh"; } == { a = 2; b = 1; }'
error:
error: meh
Aren’t these supposed to be literally the same thing?
Apparently nix
evaluator “interns” all new symbols (immutable strings,
de-duplication mechanism) in the order it encounters them. If b
happens to be the first it will affect all the attribute set traversals
after it!
The workarounds time!
So how would you work the thing around in nixpkgs
if you already
happen to define extensions
string early? Simple! Define also
isStatic
symbol somewhere as well:
Good:
$ nix-instantiate -E 'let n = import <nixpkgs> {}; in n.pkgs.nix'
/nix/store/6gzxax0rl0k1n3hg0s22jnj6c1c0aj3b-nix-2.15.1.drv
Bad:
nix-instantiate -E 'let n = import <nixpkgs> {}; extensions = []; in n.pkgs.nix'
error:
error: assertion '(final).hasSharedLibraries' failed
And good again:
$ nix-instantiate -E 'let n = import <nixpkgs> {}; isStatic = true; extensions = []; in n.pkgs.nix'
/nix/store/6gzxax0rl0k1n3hg0s22jnj6c1c0aj3b-nix-2.15.1.drv
It’s not a very practical workaround. But I find it funny.
To see why it works one needs to know where the extensions
attribute
name comes from.
It comes from the following nixpkgs
code in the guts
of pkgsStatic.*
package definitions:
nix-instantiate --eval -E 'let n = import <nixpkgs> {}; in n.pkgs.stdenv.hostPlatform == n.pkgsStatic.stdenv.hostPlatform'
false
hostPlatform
is a big attribute set with the main difference in
isStatic
field:
nix-repl> pkgs.stdenv.hostPlatform.isStatic
false
nix-repl> pkgsStatic.stdenv.hostPlatform.isStatic
true
Usually that is the cutoff when we compare two attrsets. But if we add
an extensions
into the picture:
nix-repl> pkgs.stdenv.hostPlatform.extensions
{ executable = ""; library = ".so"; sharedLibrary = ".so"; staticLibrary = ".a"; }
nix-repl> pkgsStatic.stdenv.hostPlatform.extensions
{ executable = ""; library = ".a"; sharedLibrary = «error: error: assertion '(final).hasSharedLibraries' failed
Note how sharedLibrary
always fails to evaluate.
How could we fix and prevent it?
I think it would be reasonable to have at least the optional mode in
nix
evaluator to perform attrset comparisons eagerly to uncover
potential evaluation instability like that.
I proposed one in PR nix/8711. It manages to catch this infelicity as is:
$ NIX_VALIDATE_EVAL_NONDETERMINISM=1 nix-instantiate --eval -E 'let n = import <nixpkgs> {}; in n.pkgs.stdenv.hostPlatform == n.pkgsStatic.stdenv.hostPlatform'
error:
...
error: assertion '(final).hasSharedLibraries' failed
On the nixpkgs
side no comparable attrsets should contain any
exception values. It’s better not to include the attribute at all than
have it throw like that.
It would be a good idea to cut down amount of abstraction layers in
lib/systems/default.nix
so errors would be not as cryptic for
newcomers.
Parting words
Pure lazy evaluation has it’s own caveats and causes non-deterministic
evaluation. With luck some form of
PR nix/8711 will enter nix
and one would be able to add CI checks against such problems.
Otherwise local patches would have to do.
Have fun!