-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Primops: init concatMapAttrs #13986
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Primops: init concatMapAttrs #13986
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3398,6 +3398,59 @@ static RegisterPrimOp primop_mapAttrs({ | |
.fun = prim_mapAttrs, | ||
}); | ||
|
||
|
||
static void prim_concatMapAttrs(EvalState & state, const PosIdx pos, Value ** args, Value & v) | ||
{ | ||
state.forceAttrs(*args[1], pos, | ||
"while evaluating the second argument passed to builtins.concatMapAttrs"); | ||
auto inAttrs = args[1]->attrs(); | ||
|
||
std::map<SymbolIdx, Value*> attrMap; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might make sense to use a small buffer optimization to avoid unnecessary allocations. As it stands now this will be very costly actually. |
||
|
||
for (auto &i : *inAttrs) { | ||
Value * vName = Value::toPtr(state.symbols[i.name]); | ||
Value * funApp = state.allocValue(); | ||
funApp->mkApp(args[0], vName); | ||
Value * result = state.allocValue(); | ||
result->mkApp(funApp, i.value); | ||
Comment on lines
+3414
to
+3415
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the point of allocating the result on the heap? It could just be a variable on the stack, no? |
||
state.forceAttrs(*result, pos, | ||
"while evaluating the result of the function passed to builtins.concatMapAttrs"); | ||
|
||
// Direct insertion into map - automatically handles deduplication | ||
// If duplicate keys exist, this overwrites with the later value (last-write-wins) | ||
for (auto &j : *result->attrs()) { | ||
attrMap[j.name] = j.value; | ||
} | ||
} | ||
|
||
auto attrs = state.buildBindings(attrMap.size()); | ||
for (const auto& [name, value] : attrMap) { | ||
attrs.insert(name, value); | ||
} | ||
Comment on lines
+3427
to
+3429
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. std::ranges::copy? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For large inputs, a k-way merge would make more sense. *: Nixpkgs could decide based on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think if we can expose the Bindings::iterator as a standalone k-way merge utility this is pretty easy to implement. We can resurrect your old k-way merge branch for this @roberth. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Wouldn't this also imply some overhead for the function calls? We could maybe implement an optimization for identity functions though There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think it's naturally a concatMapAttrs
(shard: type:
lib.optionalAttrs (type == "directory") (
readShard shard
)
(readDir ../by-name) No identity function needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Might be useful for other cases, idk. Not the most promising, but tracking in |
||
|
||
v.mkAttrs(attrs.alreadySorted()); | ||
} | ||
|
||
|
||
static RegisterPrimOp primop_concatMapAttrs({ | ||
.name = "__concatMapAttrs", | ||
.args = {"f", "attrset"}, | ||
.doc = R"( | ||
Apply function *f* to every element of *attrset*. For example, | ||
|
||
```nix | ||
builtins.concatMapAttrs (name: value: { ${name} = value; "${name}-${value}" = value; } ]) { foo = "fizz"; bar = "buzz"; } | ||
``` | ||
|
||
evaluates to `{ foo = "fizz"; foo-fizz = "fizz"; bar = "buzz"; bar-buzz = "buzz"; }`. | ||
|
||
If multiple applications of *f* return attribute sets with the same attribute names, | ||
the last write wins. Meaning the value from the attribute set that was processed later will be kept in the final result. | ||
)", | ||
.fun = prim_concatMapAttrs, | ||
}); | ||
|
||
|
||
static void prim_zipAttrsWith(EvalState & state, const PosIdx pos, Value ** args, Value & v) | ||
{ | ||
// we will first count how many values are present for each given key. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This must use traceable_allocator to make those allocations visible to the GC I think. Otherwise they are invisible to Boehm and those values can be freed.