libstore: bring back some pointer equality nonsense

the change to shareable thunks also removed a few cases of pointer
equality checks that allowed structures containing functions to be
considered equal to other sets containing the same functions, even
if the sets themselves were pointer-equal themselves. *so* busted.

Change-Id: If87fdab658f9037ce2a654f69a9e3da6ae2f53e5
This commit is contained in:
eldritch horrors
2025-11-14 02:56:53 +00:00
parent 78bdc60c11
commit 2fa40c9de4
4 changed files with 147 additions and 2 deletions
+48
View File
@@ -0,0 +1,48 @@
---
synopsis: "Function equality semantics are more consistent, but still bad"
cls: [4556, 4244]
issues: []
category: "Breaking Changes"
credits: [horrors]
---
Lix has inherited a historic misfeature from CppNix in the form of pointer
equality checks built into the `==` operator. These checks were originally
meant to optimize comparison for large sets, but they have the unfortunate
side effect of producing unexpected results when sets containing functions
are compared. **Lix 2.93 and earlier** behave as shown in the repl session
```
Lix 2.93.3
Type :? for help.
nix-repl> f = x: x
Added f.
nix-repl> f == f
false
nix-repl> let s.f = f; in s.f == s.f
false
nix-repl> # however!
{ inherit f; } == { inherit f; }
true
nix-repl> [ f ] == [ f ]
true
nix-repl> # and, in another twist:
[ f ] == map f [ f ]
false
```
Nixpkgs relies on sets containing functions being comparable, so we cannot
simply deprecate this behavior. Due to changes to the object model used by
Lix ***all* comparisons above now evaluate to `true`**. This is considered
a breaking change because eval results may differ, but we also consider it
minor because the optimization is unsound (c.f. `let l = [NaN]; in l == l`
evaluates to `true` even though floating point `NaN` is incomparable). Lix
intends to remove this optimization altogether in the future, but until we
can do that we instead make it slightly less broken to allow other, *real*
optimizations. Function equality comparison remains **undefined behavior**
and should not be relied upon in Nixlang code that intends to be portable.
+2 -2
View File
@@ -2680,9 +2680,9 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v
return true;
}
/* Functions are incomparable. */
/* Functions are incomparable, except for identity (see note above about this nonsense). */
case nFunction:
return false;
return pointerEq();
case nExternal:
if (pointerEq()) return true;
@@ -0,0 +1 @@
{ functionsDirect = [ true false false true ]; functionsSelected = [ true false false true ]; lists = { bigList1 = { bigList1 = true; bigList1' = false; bigList2 = true; bigList2' = false; bigList3 = true; bigList3' = false; list1 = false; list1' = false; list2 = false; list2' = false; }; bigList1' = { bigList1 = false; bigList1' = true; bigList2 = false; bigList2' = true; bigList3 = false; bigList3' = true; list1 = false; list1' = false; list2 = false; list2' = false; }; bigList2 = { bigList1 = true; bigList1' = false; bigList2 = true; bigList2' = false; bigList3 = true; bigList3' = false; list1 = false; list1' = false; list2 = false; list2' = false; }; bigList2' = { bigList1 = false; bigList1' = true; bigList2 = false; bigList2' = true; bigList3 = false; bigList3' = true; list1 = false; list1' = false; list2 = false; list2' = false; }; bigList3 = { bigList1 = true; bigList1' = false; bigList2 = true; bigList2' = false; bigList3 = true; bigList3' = false; list1 = false; list1' = false; list2 = false; list2' = false; }; bigList3' = { bigList1 = false; bigList1' = true; bigList2 = false; bigList2' = true; bigList3 = false; bigList3' = true; list1 = false; list1' = false; list2 = false; list2' = false; }; list1 = { bigList1 = false; bigList1' = false; bigList2 = false; bigList2' = false; bigList3 = false; bigList3' = false; list1 = true; list1' = false; list2 = true; list2' = false; }; list1' = { bigList1 = false; bigList1' = false; bigList2 = false; bigList2' = false; bigList3 = false; bigList3' = false; list1 = false; list1' = true; list2 = false; list2' = true; }; list2 = { bigList1 = false; bigList1' = false; bigList2 = false; bigList2' = false; bigList3 = false; bigList3' = false; list1 = true; list1' = false; list2 = true; list2' = false; }; list2' = { bigList1 = false; bigList1' = false; bigList2 = false; bigList2' = false; bigList3 = false; bigList3' = false; list1 = false; list1' = true; list2 = false; list2' = true; }; }; nans = { list = { list = true; plain = false; set = false; }; plain = { list = false; plain = false; set = false; }; set = { list = false; plain = false; set = true; }; }; sets = { bigSet1 = { bigSet1 = true; bigSet1' = false; bigSet2 = true; bigSet2' = false; bigSet3 = true; bigSet3' = false; set1 = false; set1' = false; set2 = false; set2' = false; }; bigSet1' = { bigSet1 = false; bigSet1' = true; bigSet2 = false; bigSet2' = true; bigSet3 = false; bigSet3' = true; set1 = false; set1' = false; set2 = false; set2' = false; }; bigSet2 = { bigSet1 = true; bigSet1' = false; bigSet2 = true; bigSet2' = false; bigSet3 = true; bigSet3' = false; set1 = false; set1' = false; set2 = false; set2' = false; }; bigSet2' = { bigSet1 = false; bigSet1' = true; bigSet2 = false; bigSet2' = true; bigSet3 = false; bigSet3' = true; set1 = false; set1' = false; set2 = false; set2' = false; }; bigSet3 = { bigSet1 = true; bigSet1' = false; bigSet2 = true; bigSet2' = false; bigSet3 = true; bigSet3' = false; set1 = false; set1' = false; set2 = false; set2' = false; }; bigSet3' = { bigSet1 = false; bigSet1' = true; bigSet2 = false; bigSet2' = true; bigSet3 = false; bigSet3' = true; set1 = false; set1' = false; set2 = false; set2' = false; }; set1 = { bigSet1 = false; bigSet1' = false; bigSet2 = false; bigSet2' = false; bigSet3 = false; bigSet3' = false; set1 = true; set1' = false; set2 = true; set2' = false; }; set1' = { bigSet1 = false; bigSet1' = false; bigSet2 = false; bigSet2' = false; bigSet3 = false; bigSet3' = false; set1 = false; set1' = true; set2 = false; set2' = true; }; set2 = { bigSet1 = false; bigSet1' = false; bigSet2 = false; bigSet2' = false; bigSet3 = false; bigSet3' = false; set1 = true; set1' = false; set2 = true; set2' = false; }; set2' = { bigSet1 = false; bigSet1' = false; bigSet2 = false; bigSet2' = false; bigSet3 = false; bigSet3' = false; set1 = false; set1' = true; set2 = false; set2' = true; }; }; }
@@ -0,0 +1,96 @@
let
f1 = x: x;
f2 = x: x;
inf = let f = n: if n == 0 then 1.0 else 10 * f (n - 1); in f 1000;
nan = inf * 0;
# all of these sets and lists are assumed to have different *addresses* as long as CSE does not occur
sets = rec {
set1 = {
f = f1;
};
set1' = {
f = f2;
};
set2 = {
f = f1;
};
set2' = {
f = f2;
};
bigSet1 = {
f = f1;
meow = true;
};
bigSet1' = {
f = f2;
meow = true;
};
bigSet2 = bigSet1 // {
meow = true;
};
bigSet2' = bigSet1' // {
meow = true;
};
bigSet3 = {
f = f1;
meow = true;
};
bigSet3' = {
f = f2;
meow = true;
};
};
lists = rec {
list1 = [ f1 ];
list1' = [ f2 ];
list2 = [ f1 ];
list2' = [ f2 ];
bigList1 = [ f1 true ];
bigList1' = [ f2 true ];
bigList2 = map (e: if builtins.isBool e then true else e) bigList1;
bigList2'= map (e: if builtins.isBool e then true else e) bigList1';
bigList3 = [ f1 true ];
bigList3' = [ f2 true ];
};
nans = rec {
plain = nan;
list = [ nan ];
set = { inherit nan; };
};
compareAllPairs = cases:
let
names = builtins.attrNames cases;
in
builtins.listToAttrs (map
(n1: {
name = n1;
value = builtins.listToAttrs (map
(n2: { name = n2; value = cases.${n1} == cases.${n2}; })
names);
})
names);
results = {
functionsDirect = [
(f1 == f1)
(f1 == f2)
(f2 == f1)
(f2 == f2)
];
functionsSelected = [
(sets.set1.f == sets.set1.f)
(sets.set1.f == sets.set1'.f)
(sets.set1'.f == sets.set1.f)
(sets.set1'.f == sets.set1'.f)
];
sets = compareAllPairs sets;
lists = compareAllPairs lists;
nans = compareAllPairs nans;
};
in
builtins.deepSeq results results