Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions doc/manual/source/language/evaluation.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,48 @@ in f { x = throw "error"; y = throw "error"; }
=> "ok"
```

## Evaluation order

The order in which expressions are evaluated is generally unspecified, because it does not affect successful evaluation outcomes.
This allows more freedom for the evaluator to evolve and to evaluate efficiently.

Data dependencies naturally impose some ordering constraints: a value cannot be used before it is computed.
Beyond these constraints, the evaluator is free to choose any order.

The order in which side effects such as [`builtins.trace`](@docroot@/language/builtins.md#builtins-trace) output occurs is not defined, but may be expected to follow data dependencies. <!-- we may want to be more specific about this. -->

In a lazy language, evaluation order is often opposite to expectations from strict languages.
For example, in `let wrap = x: { wrapped = x; }; in wrap (1 + 2)`, the function body produces a result (`{ wrapped = ...; }`) *before* evaluating `x`.

## Infinite recursion and stack overflow

During evaluation, two types of errors can occur when expressions reference themselves or call functions too deeply:

### Infinite recursion

This error occurs when a value depends on itself through a cycle, making it impossible to compute.

```nix
let x = x; in x
=> error: infinite recursion encountered
```

Infinite recursion happens at the value level when evaluating an expression requires evaluating the same expression again.

Despite the name, infinite recursion is cheap to compute and does not involve a stack overflow.
The cycle is finite and fairly easy to detect.

### Stack overflow

This error occurs when the call depth exceeds the maximum allowed limit.

```nix
let f = x: f (x + 1);
in f 0
=> error: stack overflow; max-call-depth exceeded
```

Stack overflow happens when too many function calls are nested without returning.
The maximum call depth is controlled by the [`max-call-depth` setting](@docroot@/command-ref/conf-file.md#conf-max-call-depth).

[C API]: @docroot@/c-api.md
90 changes: 85 additions & 5 deletions doc/manual/source/language/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
| [Greater than or equal to][Comparison] | *expr* `>=` *expr* | none | 10 |
| [Equality] | *expr* `==` *expr* | none | 11 |
| Inequality | *expr* `!=` *expr* | none | 11 |
| Logical conjunction (`AND`) | *bool* `&&` *bool* | left | 12 |
| Logical disjunction (`OR`) | *bool* <code>\|\|</code> *bool* | left | 13 |
| [Logical conjunction] (`AND`) | *bool* `&&` *bool* | left | [12](#precedence-and-disjunctive-normal-form) |
| [Logical disjunction] (`OR`) | *bool* <code>\|\|</code> *bool* | left | [13](#precedence-and-disjunctive-normal-form) |
| [Logical implication] | *bool* `->` *bool* | right | 14 |
| [Pipe operator] (experimental) | *expr* `\|>` *func* | left | 15 |
| [Pipe operator] (experimental) | *func* `<\|` *expr* | right | 15 |
Expand Down Expand Up @@ -162,6 +162,9 @@ Update [attribute set] *attrset1* with names and values from *attrset2*.
The returned attribute set will have all of the attributes in *attrset1* and *attrset2*.
If an attribute name is present in both, the attribute value from the latter is taken.

This operator is [strict](@docroot@/language/evaluation.md#strictness) in both *attrset1* and *attrset2*.
That means that both arguments are evaluated to [weak head normal form](@docroot@/language/evaluation.md#values), so the attribute sets themselves are evaluated, but their attribute values are not evaluated.

[Update]: #update

## Comparison
Expand All @@ -185,18 +188,95 @@ All comparison operators are implemented in terms of `<`, and the following equi

## Equality

- [Attribute sets][attribute set] and [lists][list] are compared recursively, and therefore are fully evaluated.
- Comparison of [functions][function] always returns `false`.
- [Attribute sets][attribute set] are compared first by attribute names and then by items until a difference is found.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by attribute names

Might be a tad confusing, since attribute name order depends on the symbol table order and that can be a source of unsoundness. Not sure this is worth documenting yet.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we have an unsoundness problem, but a nondeterminism problem in terms of whether it succeeds to even evaluate or not, which is not a soundness problem if you define equality to be strict in deepSeq [a b].
But yeah. See that comment with "woeful" in it I guess.

Copy link
Contributor

@xokdvium xokdvium Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nondeterminism problem in terms of whether it succeeds to even evaluate or not

Pretty much, yeah. It's deterministic in a sense that at least it doesn't change from one run to another for a particular nix version (modulo any sort of parallel evaluation that has been floating around). But for the user it might as well be unspecified and we must be able to add stuff to the static pre-filled symbol table, which will change attribute ordering anyway. It's a very messy situation to be in tbh if one wants to compare attribute sets deterministically.

This discussion opens up the notion that changing anything around which symbols get added to the symbol table and in which order have the opportunity to change what code succeeds to evaluate and which doesn't...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Value equality is something to avoid anyway, so maybe just make it alphabetic? Slightly slower, but problem solved.

If I may apologize in advance for this writing style while I have some fun...

Why bad, you may ask?

  • Equality is deeply strict
  • Equality is slow, as a consequence
  • If things are the same, you should probably already know
  • If things are the same, why did you put them in different places at all

"But I'm not comparing for equality, but values being unequal!"

  • Have you not learned from the French Revolution?
    • which one
  • If they're unequal wouldn't you want to know why?
  • If they're equal, that was bad

"But I have a functional key that's not a string, so I can use an attrset!"

  • You should still be comparing a derived key, because comparing the whole object is foolish and error prone
  • Presumably you have a list of these things? An association list. Those have O(n) lookup time, with costly equality every step of the way.

- [Lists][list] are compared first by length and then by items until a difference is found.
- Comparison of distinct [functions][function] returns `false`, but identical functions may be subject to [value identity optimization](#value-identity-optimization).
- Numbers are type-compatible, see [arithmetic] operators.
- Floating point numbers only differ up to a limited precision.

The `==` operator is [strict](@docroot@/language/evaluation.md#strictness) in both arguments; when comparing composite types ([attribute sets][attribute set] and [lists][list]), it is partially strict in their contained values: they are evaluated until a difference is found. <!-- this is woefully underspecified, affecting which expressions evaluate correctly; not just "ordering" or error messages. -->

### Value identity optimization

Nix performs equality comparisons of nested values by pointer equality or more abstractly, _identity_.
Nix semantics ideally do not assign a unique identity to values as they are created, but equality is an exception to this rule.
The disputable benefit of this is that it is more efficient, and it allows cyclical structures to be compared, e.g. `let x = { x = x; }; in x == x` evaluates to `true`.
However, as a consequence, it makes a function equal to itself when the comparison is made in a list or attribute set, in contradiction to a simple direct comparison.

[function]: ./syntax.md#functions

[Equality]: #equality

## Logical conjunction

> **Syntax**
>
> *bool1* `&&` *bool2*

Logical AND. Equivalent to `if` *bool1* `then` *bool2* `else false`.

This operator is [strict](@docroot@/language/evaluation.md#strictness) in *bool1*, but only evaluates *bool2* if *bool1* is `true`.

> **Example**
>
> ```nix
> true && false
> => false
>
> false && throw "never evaluated"
> => false
> ```

[Logical conjunction]: #logical-conjunction

## Logical disjunction

> **Syntax**
>
> *bool1* `||` *bool2*

Logical OR. Equivalent to `if` *bool1* `then true` `else` *bool2*.

This operator is [strict](@docroot@/language/evaluation.md#strictness) in *bool1*, but only evaluates *bool2* if *bool1* is `false`.

> **Example**
>
> ```nix
> true || false
> => true
>
> true || throw "never evaluated"
> => true
> ```

[Logical disjunction]: #logical-disjunction

### Precedence and disjunctive normal form

The precedence of `&&` and `||` aligns with disjunctive normal form.
Without parentheses, an expression describes multiple "permissible situations" (connected by `||`), where each situation consists of multiple simultaneous conditions (connected by `&&`).

For example, `A || B && C || D && E` is parsed as `A || (B && C) || (D && E)`, describing three permissible situations: A holds, or both B and C hold, or both D and E hold.

## Logical implication

Equivalent to `!`*b1* `||` *b2* (or `if` *b1* `then` *b2* `else true`)
> **Syntax**
>
> *bool1* `->` *bool2*

Logical implication. Equivalent to `!`*bool1* `||` *bool2* (or `if` *bool1* `then` *bool2* `else true`).

This operator is [strict](@docroot@/language/evaluation.md#strictness) in *bool1*, but only evaluates *bool2* if *bool1* is `true`.

> **Example**
>
> ```nix
> true -> false
> => false
>
> false -> throw "never evaluated"
> => true
> ```

[Logical implication]: #logical-implication

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
false
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Distinct but not identical functions in attribute set compare as unequal
# See https://nix.dev/manual/nix/latest/language/operators#equality
{ a = (x: x); } == { a = (x: x); }
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
true
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Function comparison in attribute set uses value identity optimization
# See https://nix.dev/manual/nix/latest/language/operators#value-identity-optimization
let
f = x: x;
in
{
a = f;
} == {
a = f;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
false
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Direct comparison of distinct but not identical functions returns false
# See https://nix.dev/manual/nix/latest/language/operators#equality
(x: x) == (x: x)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
false
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Direct comparison of identical function returns false
# See https://nix.dev/manual/nix/latest/language/operators#equality
let
f = x: x;
in
f == f
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
false
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Distinct but not identical functions in list compare as unequal
# See https://nix.dev/manual/nix/latest/language/operators#equality
[ (x: x) ] == [ (x: x) ]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
true
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Function comparison in list uses value identity optimization
# See https://nix.dev/manual/nix/latest/language/operators#value-identity-optimization
let
f = x: x;
in
[ f ] == [ f ]
Loading