1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
//! Attrset builtins: attrNames, attrValues, hasAttr, getAttr, intersectAttrs,
//! mapAttrs, listToAttrs, catAttrs, removeAttrs, filterAttrs, zipAttrsWith.
use super::*;
pub(crate) fn register(builtins: &mut NixAttrs) {
// attrNames: iterates BTreeMap keys (sorted). String clone per key
// (typically small interned identifiers).
register_builtin(builtins, "attrNames", |args| {
let attrs = args[0].to_attrs()?;
Ok(Value::List(Rc::new(attrs.keys().map(|k| Value::string(k.clone())).collect())))
});
// attrValues: iterates BTreeMap values. Each `.cloned()` is an Rc
// bump for heap-backed Value variants (no deep copy).
register_builtin(builtins, "attrValues", |args| {
let attrs = args[0].to_attrs()?;
Ok(Value::List(Rc::new(NixList::new(attrs.values().cloned().collect()))))
});
register_builtin(builtins, "hasAttr", |args| {
let name = args[0].as_string()?.to_string();
Ok(Value::Builtin(Box::new(BuiltinFn {
name: "hasAttr<partial>",
func: Rc::new(move |args2| {
let attrs = args2[0].to_attrs()?;
Ok(Value::Bool(attrs.contains_key(&name)))
}),
})))
});
register_builtin(builtins, "getAttr", |args| {
let name = args[0].as_string()?.to_string();
Ok(Value::Builtin(Box::new(BuiltinFn {
name: "getAttr<partial>",
func: Rc::new(move |args2| {
let attrs = args2[0].to_attrs()?;
attrs.get(&name).cloned().ok_or_else(|| EvalError::AttrNotFound(name.clone()))
}),
})))
});
register_builtin(builtins, "intersectAttrs", |args| {
let a = args[0].to_attrs()?.clone();
Ok(Value::Builtin(Box::new(BuiltinFn {
name: "intersectAttrs<partial>",
func: Rc::new(move |args2| {
let b = args2[0].to_attrs()?;
let mut result = NixAttrs::new();
// SYM-KEYED end to end (lever 1 of the 20s campaign,
// 2026-07-21). The previous `iter_unsorted` + `contains_key`
// + `insert` loop was the single hottest code shape in the
// whole cid eval — live sampling attributed 63/70 interner
// leaves to THIS closure: String-per-key materialization,
// re-intern in contains_key, third intern in insert. The
// Symbols never needed to leave symbol space at all.
// Byte-neutral: result order is re-derived at observation
// time via sorted_entries, same as before.
for (sym, v) in b.iter_syms() {
if a.contains_key_sym(&sym) {
result.insert_sym(sym, v.clone());
}
}
Ok(Value::Attrs(Rc::new(result)))
}),
})))
});
// filterAttrs
register_builtin(builtins, "filterAttrs", |args| {
let pred = args[0].clone();
Ok(Value::Builtin(Box::new(BuiltinFn {
name: "filterAttrs<partial>",
func: Rc::new(move |args2| {
let attrs = args2[0].to_attrs()?;
let mut result = NixAttrs::new();
// Fresh result map — insertion order is discarded (re-derived
// sorted at any sink), so the sorted `iter()` was dead work.
//
// NEEDS-PER-SITE-VERIFICATION (PERF-ARSENAL C-filter), argument
// discharged: byte-neutral on the SUCCESS path, verified by two
// obligations —
// (a) NO drvPath on the error path: this builtin only ever
// builds a fresh plain attrset; it constructs no derivation
// and produces no drvPath. On a predicate throw it returns
// `Err` with no value, so the sui-parity byte axis (drvPath)
// is unreachable when order could matter.
// (b) NO cross-entry force dependency: the predicate is applied
// per entry as `apply(pred, k)` then `apply_and_force(_, v)`
// — a fresh partial application each time over an immutable
// shared `pred`. One entry's force neither changes another
// entry's boolean outcome nor whether it throws.
// Residual (NOT rounded up): under iter_unsorted, WHICH throwing
// entry errors FIRST is hasher-seed-nondeterministic, so the
// error *message* is order-sensitive — success-neutral, not
// error-deterministic. Byte-parity is unaffected (no drvPath on
// error); this stays NEEDS-VERIFICATION-with-argument, not
// PROVABLY-NEUTRAL.
// Sym-keyed (lever 1): the String is still materialized — the
// predicate lambda needs it — but exactly ONCE, moved into the
// lambda arg; the insert stays in symbol space and the per-call
// Vec collect is gone.
for (sym, v) in attrs.iter_syms() {
let k = sui_intern::resolve(sym);
let partial = crate::eval::apply(pred.clone(), Value::string(k))?;
if crate::eval::apply_and_force(partial, v.clone())?.as_bool()? {
result.insert_sym(sym, v.clone());
}
}
Ok(Value::Attrs(Rc::new(result)))
}),
})))
});
// Attrset higher-order operations
register_builtin(builtins, "mapAttrs", |args| {
let func = args[0].clone();
Ok(Value::Builtin(Box::new(BuiltinFn {
name: "mapAttrs<partial>",
func: Rc::new(move |args2| {
let attrs = args2[0].to_attrs()?;
let mut result = NixAttrs::new();
// Fresh result map; each value is an independent lazy thunk,
// so per-entry mapping is order-independent and the sorted
// iter was dead work. Byte-neutral.
// Sym-keyed (lever 1): one resolve for the lambda's key arg,
// zero-intern insert, no per-call Vec collect.
for (sym, v) in attrs.iter_syms() {
let f = func.clone();
let key = sui_intern::resolve(sym);
let val = v.clone();
let thunk = Thunk::new_native(move || {
let partial = crate::eval::apply(f, Value::string(key))?;
crate::eval::apply(partial, val)
});
result.insert_sym(sym, Value::Thunk(thunk));
}
Ok(Value::Attrs(Rc::new(result)))
}),
})))
});
register_builtin(builtins, "listToAttrs", |args| {
let list = args[0].to_list()?;
let mut attrs = NixAttrs::new();
for item in list {
let item_attrs = item.to_attrs()?;
let name = item_attrs.get("name")
.ok_or_else(|| EvalError::AttrNotFound("name".to_string()))?
.to_str()?;
let value = item_attrs.get("value")
.ok_or_else(|| EvalError::AttrNotFound("value".to_string()))?
.clone();
// Nix `listToAttrs` semantics: on a duplicate `name`, the FIRST
// occurrence wins (later duplicates are ignored). cppnix builds
// the attrset with an ordered insert that refuses to overwrite an
// existing key. `NixAttrs::insert` is last-wins, so guard with an
// explicit first-wins skip. (Byte-parity root: a Cargo.lock that
// lists a crate twice — a registry entry then a git entry of the
// same name+version — must resolve to the FIRST/registry source,
// exactly as nix does; last-wins picked the git source and
// produced a structurally different rust_<crate> derivation.)
if !attrs.contains_key(&name) {
attrs.insert(name, value);
}
}
Ok(Value::Attrs(Rc::new(attrs)))
});
register_builtin(builtins, "catAttrs", |args| {
let name = args[0].as_string()?.to_string();
Ok(Value::Builtin(Box::new(BuiltinFn {
name: "catAttrs<partial>",
func: Rc::new(move |args2| {
let list = args2[0].to_list()?;
let mut result = Vec::new();
for item in &list {
if let Ok(attrs) = item.to_attrs()
&& let Some(v) = attrs.get(&name) {
result.push(v.clone());
}
}
Ok(Value::List(Rc::new(NixList::new(result))))
}),
})))
});
register_builtin(builtins, "removeAttrs", |args| {
let set = args[0].to_attrs()?.clone();
Ok(Value::Builtin(Box::new(BuiltinFn {
name: "removeAttrs<partial>",
func: Rc::new(move |args2| {
let names = args2[0].to_list()?;
let remove: Vec<String> = names.iter()
.filter_map(|v| v.to_str().ok())
.collect();
let mut result = set.clone();
for name in &remove {
result.remove(name);
}
Ok(Value::Attrs(Rc::new(result)))
}),
})))
});
// zipAttrsWith — zip attrsets with a combining function
register_builtin(builtins, "zipAttrsWith", |args| {
let func = args[0].clone();
Ok(Value::Builtin(Box::new(BuiltinFn {
name: "zipAttrsWith<partial>",
func: Rc::new(move |args2| {
let list = args2[0].to_list()?;
// Collect all keys and their values across all attrsets
let mut collected: std::collections::BTreeMap<String, Vec<Value>> =
std::collections::BTreeMap::new();
for item in &list {
let attrs = item.to_attrs()?;
// Feeds a BTreeMap keyed by name — the sort re-imposed by
// `iter()` is redundant with the BTreeMap's own ordering.
for (k, v) in attrs.iter_unsorted() {
collected.entry(k.clone()).or_default().push(v.clone());
}
}
let mut result = NixAttrs::new();
for (k, vs) in collected {
// CRITICAL: Wrap each merge result in a native thunk.
// CppNix's zipAttrsWith produces a lazy attrset where
// each key's merge result is independently evaluable.
// Eagerly applying the merge function forces ALL keys,
// which breaks the nixpkgs module system's fixpoint:
// pushedDownDefinitionsByName uses zipAttrsWith, and
// eagerly merging ALL definitions forces config values
// while config is still being computed (blackhole).
let f = func.clone();
let key = k.clone();
let thunk = Thunk::new_native(move || {
let partial = crate::eval::apply(
f,
Value::string(key),
)?;
crate::eval::apply(partial, Value::List(Rc::new(NixList::new(vs))))
});
result.insert(k, Value::Thunk(thunk));
}
Ok(Value::Attrs(Rc::new(result)))
}),
})))
});
}