From 1039b6719bb906af6a58316b758b203d975c5188 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 7 Dec 2025 13:46:02 +0100 Subject: [PATCH 1/3] doc: Document "evaluation order", some strictness, equality quirk Correct and clarify evaluation semantics including to help users understand Nix language behavior without unnecessarily pinning down the implementation. --- doc/manual/source/language/evaluation.md | 44 +++++++++++++ doc/manual/source/language/operators.md | 83 ++++++++++++++++++++++-- 2 files changed, 122 insertions(+), 5 deletions(-) diff --git a/doc/manual/source/language/evaluation.md b/doc/manual/source/language/evaluation.md index 980942c92..dff429776 100644 --- a/doc/manual/source/language/evaluation.md +++ b/doc/manual/source/language/evaluation.md @@ -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. + +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 diff --git a/doc/manual/source/language/operators.md b/doc/manual/source/language/operators.md index ab74e8a99..f2a7f0baa 100644 --- a/doc/manual/source/language/operators.md +++ b/doc/manual/source/language/operators.md @@ -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* \|\| *bool* | left | 13 | +| [Logical conjunction] (`AND`) | *bool* `&&` *bool* | left | 12 | +| [Logical disjunction] (`OR`) | *bool* \|\| *bool* | left | 13 | | [Logical implication] | *bool* `->` *bool* | right | 14 | | [Pipe operator] (experimental) | *expr* `\|>` *func* | left | 15 | | [Pipe operator] (experimental) | *func* `<\|` *expr* | right | 15 | @@ -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 @@ -185,18 +188,88 @@ 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. +- [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. + +### 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 + ## 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 From 97a60c1fab7165c209da3bc51b701645c35c2718 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 7 Dec 2025 14:10:16 +0100 Subject: [PATCH 2/3] doc: Precedence aligns with disjunctive normal form --- doc/manual/source/language/operators.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/doc/manual/source/language/operators.md b/doc/manual/source/language/operators.md index f2a7f0baa..dad3e1e8d 100644 --- a/doc/manual/source/language/operators.md +++ b/doc/manual/source/language/operators.md @@ -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* \|\| *bool* | left | 13 | +| [Logical conjunction] (`AND`) | *bool* `&&` *bool* | left | [12](#precedence-and-disjunctive-normal-form) | +| [Logical disjunction] (`OR`) | *bool* \|\| *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 | @@ -251,6 +251,13 @@ This operator is [strict](@docroot@/language/evaluation.md#strictness) in *bool1 [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 > **Syntax** From 6fb5276e7bc7f2a0d81a5194ebac4ce95b6f87d6 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 7 Dec 2025 14:43:46 +0100 Subject: [PATCH 3/3] test: add tests for function equality behavior Add tests for function equality covering both direct comparisons and comparisons within composite types (lists and attribute sets). Tests verify: - Direct function comparisons always return false - Value identity optimization in composite types allows identical functions to compare as equal when both references point to the same function value --- ...al-okay-equal-function-attrset-distinct-similar.exp | 1 + ...al-okay-equal-function-attrset-distinct-similar.nix | 3 +++ .../eval-okay-equal-function-attrset-identical.exp | 1 + .../eval-okay-equal-function-attrset-identical.nix | 10 ++++++++++ ...val-okay-equal-function-direct-distinct-similar.exp | 1 + ...val-okay-equal-function-direct-distinct-similar.nix | 3 +++ .../lang/eval-okay-equal-function-direct-identical.exp | 1 + .../lang/eval-okay-equal-function-direct-identical.nix | 6 ++++++ .../eval-okay-equal-function-list-distinct-similar.exp | 1 + .../eval-okay-equal-function-list-distinct-similar.nix | 3 +++ .../lang/eval-okay-equal-function-list-identical.exp | 1 + .../lang/eval-okay-equal-function-list-identical.nix | 6 ++++++ 12 files changed, 37 insertions(+) create mode 100644 tests/functional/lang/eval-okay-equal-function-attrset-distinct-similar.exp create mode 100644 tests/functional/lang/eval-okay-equal-function-attrset-distinct-similar.nix create mode 100644 tests/functional/lang/eval-okay-equal-function-attrset-identical.exp create mode 100644 tests/functional/lang/eval-okay-equal-function-attrset-identical.nix create mode 100644 tests/functional/lang/eval-okay-equal-function-direct-distinct-similar.exp create mode 100644 tests/functional/lang/eval-okay-equal-function-direct-distinct-similar.nix create mode 100644 tests/functional/lang/eval-okay-equal-function-direct-identical.exp create mode 100644 tests/functional/lang/eval-okay-equal-function-direct-identical.nix create mode 100644 tests/functional/lang/eval-okay-equal-function-list-distinct-similar.exp create mode 100644 tests/functional/lang/eval-okay-equal-function-list-distinct-similar.nix create mode 100644 tests/functional/lang/eval-okay-equal-function-list-identical.exp create mode 100644 tests/functional/lang/eval-okay-equal-function-list-identical.nix diff --git a/tests/functional/lang/eval-okay-equal-function-attrset-distinct-similar.exp b/tests/functional/lang/eval-okay-equal-function-attrset-distinct-similar.exp new file mode 100644 index 000000000..c508d5366 --- /dev/null +++ b/tests/functional/lang/eval-okay-equal-function-attrset-distinct-similar.exp @@ -0,0 +1 @@ +false diff --git a/tests/functional/lang/eval-okay-equal-function-attrset-distinct-similar.nix b/tests/functional/lang/eval-okay-equal-function-attrset-distinct-similar.nix new file mode 100644 index 000000000..13c30d9f7 --- /dev/null +++ b/tests/functional/lang/eval-okay-equal-function-attrset-distinct-similar.nix @@ -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); } diff --git a/tests/functional/lang/eval-okay-equal-function-attrset-identical.exp b/tests/functional/lang/eval-okay-equal-function-attrset-identical.exp new file mode 100644 index 000000000..27ba77dda --- /dev/null +++ b/tests/functional/lang/eval-okay-equal-function-attrset-identical.exp @@ -0,0 +1 @@ +true diff --git a/tests/functional/lang/eval-okay-equal-function-attrset-identical.nix b/tests/functional/lang/eval-okay-equal-function-attrset-identical.nix new file mode 100644 index 000000000..830267c82 --- /dev/null +++ b/tests/functional/lang/eval-okay-equal-function-attrset-identical.nix @@ -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; +} diff --git a/tests/functional/lang/eval-okay-equal-function-direct-distinct-similar.exp b/tests/functional/lang/eval-okay-equal-function-direct-distinct-similar.exp new file mode 100644 index 000000000..c508d5366 --- /dev/null +++ b/tests/functional/lang/eval-okay-equal-function-direct-distinct-similar.exp @@ -0,0 +1 @@ +false diff --git a/tests/functional/lang/eval-okay-equal-function-direct-distinct-similar.nix b/tests/functional/lang/eval-okay-equal-function-direct-distinct-similar.nix new file mode 100644 index 000000000..f3a931c6b --- /dev/null +++ b/tests/functional/lang/eval-okay-equal-function-direct-distinct-similar.nix @@ -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) diff --git a/tests/functional/lang/eval-okay-equal-function-direct-identical.exp b/tests/functional/lang/eval-okay-equal-function-direct-identical.exp new file mode 100644 index 000000000..c508d5366 --- /dev/null +++ b/tests/functional/lang/eval-okay-equal-function-direct-identical.exp @@ -0,0 +1 @@ +false diff --git a/tests/functional/lang/eval-okay-equal-function-direct-identical.nix b/tests/functional/lang/eval-okay-equal-function-direct-identical.nix new file mode 100644 index 000000000..f91a39fb8 --- /dev/null +++ b/tests/functional/lang/eval-okay-equal-function-direct-identical.nix @@ -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 diff --git a/tests/functional/lang/eval-okay-equal-function-list-distinct-similar.exp b/tests/functional/lang/eval-okay-equal-function-list-distinct-similar.exp new file mode 100644 index 000000000..c508d5366 --- /dev/null +++ b/tests/functional/lang/eval-okay-equal-function-list-distinct-similar.exp @@ -0,0 +1 @@ +false diff --git a/tests/functional/lang/eval-okay-equal-function-list-distinct-similar.nix b/tests/functional/lang/eval-okay-equal-function-list-distinct-similar.nix new file mode 100644 index 000000000..cd6182770 --- /dev/null +++ b/tests/functional/lang/eval-okay-equal-function-list-distinct-similar.nix @@ -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) ] diff --git a/tests/functional/lang/eval-okay-equal-function-list-identical.exp b/tests/functional/lang/eval-okay-equal-function-list-identical.exp new file mode 100644 index 000000000..27ba77dda --- /dev/null +++ b/tests/functional/lang/eval-okay-equal-function-list-identical.exp @@ -0,0 +1 @@ +true diff --git a/tests/functional/lang/eval-okay-equal-function-list-identical.nix b/tests/functional/lang/eval-okay-equal-function-list-identical.nix new file mode 100644 index 000000000..5156ffc47 --- /dev/null +++ b/tests/functional/lang/eval-okay-equal-function-list-identical.nix @@ -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 ]