Skip to content

Commit 11f3da7

Browse files
committed
libexpr: add builtins.filterAttrs with tombstone optimization
Add a new builtin function filterAttrs that filters attribute sets based on a predicate function. The predicate receives both the attribute name and value, returning true for attributes to keep. Implementation uses tombstones for large attrsets (>= 8 elements) when layer capacity is available. Instead of copying all matching attributes, it layers tombstones for non-matching attributes on top of the source. This is beneficial when most attributes pass the filter (common case). Fast paths: - Empty attrset: return unchanged - All attributes pass: return unchanged - No attributes pass: return emptyBindings Falls back to copy-based approach for small attrsets or when the layer list is full. Example usage: builtins.filterAttrs (name: value: value > 5) { a = 3; b = 6; c = 10; } => { b = 6; c = 10; }
1 parent 4870830 commit 11f3da7

File tree

5 files changed

+99
-0
lines changed

5 files changed

+99
-0
lines changed

src/libexpr/primops.cc

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3565,6 +3565,93 @@ static RegisterPrimOp primop_mapAttrs({
35653565
.fun = prim_mapAttrs,
35663566
});
35673567

3568+
static void prim_filterAttrs(EvalState & state, const PosIdx pos, Value ** args, Value & v)
3569+
{
3570+
state.forceAttrs(*args[1], pos, "while evaluating the second argument passed to builtins.filterAttrs");
3571+
3572+
auto * source = args[1]->attrs();
3573+
3574+
// Fast path: empty attrset
3575+
if (source->empty()) {
3576+
v = *args[1];
3577+
return;
3578+
}
3579+
3580+
state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.filterAttrs");
3581+
3582+
// For large attrsets with available layer capacity, use tombstones.
3583+
// This is beneficial when most attrs pass the filter (common case).
3584+
constexpr size_t tombstoneThreshold = 8;
3585+
if (source->size() >= tombstoneThreshold && !source->isLayerListFull()) {
3586+
// Collect attrs that DON'T pass the filter (will become tombstones)
3587+
boost::container::small_vector<Symbol, 64> toRemove;
3588+
3589+
for (auto & attr : *source) {
3590+
Value * vName = Value::toPtr(state.symbols[attr.name]);
3591+
Value * callArgs[] = {vName, attr.value};
3592+
Value res;
3593+
state.callFunction(*args[0], callArgs, res, noPos);
3594+
if (!state.forceBool(
3595+
res,
3596+
pos,
3597+
"while evaluating the return value of the filtering function passed to builtins.filterAttrs"))
3598+
toRemove.push_back(attr.name);
3599+
}
3600+
3601+
// If nothing to remove, return original
3602+
if (toRemove.empty()) {
3603+
v = *args[1];
3604+
return;
3605+
}
3606+
3607+
// If everything filtered out, return empty
3608+
if (toRemove.size() == source->size()) {
3609+
v.mkAttrs(&Bindings::emptyBindings);
3610+
return;
3611+
}
3612+
3613+
// Layer tombstones on top
3614+
auto attrs = state.buildBindings(toRemove.size());
3615+
for (auto sym : toRemove)
3616+
attrs.insertTombstone(sym);
3617+
attrs.layerOnTopOf(*source);
3618+
v.mkAttrs(attrs.finish());
3619+
return;
3620+
}
3621+
3622+
// Copy approach for small attrsets or when layer list is full
3623+
auto attrs = state.buildBindings(source->size());
3624+
3625+
for (auto & attr : *source) {
3626+
Value * vName = Value::toPtr(state.symbols[attr.name]);
3627+
Value * callArgs[] = {vName, attr.value};
3628+
Value res;
3629+
state.callFunction(*args[0], callArgs, res, noPos);
3630+
if (state.forceBool(
3631+
res, pos, "while evaluating the return value of the filtering function passed to builtins.filterAttrs"))
3632+
attrs.insert(attr.name, attr.value);
3633+
}
3634+
3635+
v.mkAttrs(attrs.alreadySorted());
3636+
}
3637+
3638+
static RegisterPrimOp primop_filterAttrs({
3639+
.name = "__filterAttrs",
3640+
.args = {"f", "attrset"},
3641+
.doc = R"(
3642+
Return an attribute set consisting of the attributes in *attrset* for which
3643+
the function *f* returns `true`. The function *f* is called with two arguments:
3644+
the name of the attribute and the value of the attribute. For example,
3645+
3646+
```nix
3647+
builtins.filterAttrs (name: value: name == "foo") { foo = 1; bar = 2; }
3648+
```
3649+
3650+
evaluates to `{ foo = 1; }`.
3651+
)",
3652+
.fun = prim_filterAttrs,
3653+
});
3654+
35683655
static void prim_zipAttrsWith(EvalState & state, const PosIdx pos, Value ** args, Value & v)
35693656
{
35703657
// we will first count how many values are present for each given key.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ a = 3; }
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
builtins.filterAttrs (name: value: name == "a") {
2+
a = 3;
3+
b = 6;
4+
c = 10;
5+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ b = 6; c = 10; }
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
builtins.filterAttrs (name: value: value > 5) {
2+
a = 3;
3+
b = 6;
4+
c = 10;
5+
}

0 commit comments

Comments
 (0)