From 7f1ebde7b84ad6d418eae7890fda840071d89891 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 30 May 2024 02:31:33 +0200 Subject: [PATCH] primops: Move functions to primops/list.cc Moved builtins: all, any, concatLists, concatMap, elem, elemAt, filter, foldl', head, length, listToAttrs, map, partition, sort, tail The CompareValues struct has been moved to primops.hh Change-Id: Ifc5457298215fd20c96aa8acac65749ed42c28dd --- src/libexpr/meson.build | 1 + src/libexpr/primops.cc | 597 +------------------------------ src/libexpr/primops.hh | 96 ++++- src/libexpr/primops/list.cc | 680 ++++++++++++++++++++++++++++++++++++ 4 files changed, 772 insertions(+), 602 deletions(-) create mode 100644 src/libexpr/primops/list.cc diff --git a/src/libexpr/meson.build b/src/libexpr/meson.build index 3c942a5d1..67743f0f5 100644 --- a/src/libexpr/meson.build +++ b/src/libexpr/meson.build @@ -94,6 +94,7 @@ libexpr_sources = files( 'primops/fetchMercurial.cc', 'primops/fetchTree.cc', 'primops/fromTOML.cc', + 'primops/list.cc', 'primops/string.cc', 'value/context.cc', ) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index d8dcb43f2..d8351087c 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -558,63 +558,6 @@ template } } -struct CompareValues -{ - EvalState & state; - const PosIdx pos; - const std::string_view errorCtx; - - CompareValues(EvalState & state, const PosIdx pos, const std::string_view && errorCtx) : state(state), pos(pos), errorCtx(errorCtx) { }; - - bool operator () (Value * v1, Value * v2) const - { - return (*this)(v1, v2, errorCtx); - } - - bool operator () (Value * v1, Value * v2, std::string_view errorCtx) const - { - try { - if (v1->type() == nFloat && v2->type() == nInt) - return v1->fpoint < v2->integer; - if (v1->type() == nInt && v2->type() == nFloat) - return v1->integer < v2->fpoint; - if (v1->type() != v2->type()) - state.error("cannot compare %s with %s", showType(*v1), showType(*v2)).debugThrow(); - // Allow selecting a subset of enum values - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wswitch-enum" - switch (v1->type()) { - case nInt: - return v1->integer < v2->integer; - case nFloat: - return v1->fpoint < v2->fpoint; - case nString: - return strcmp(v1->string.s, v2->string.s) < 0; - case nPath: - return strcmp(v1->_path, v2->_path) < 0; - case nList: - // Lexicographic comparison - for (size_t i = 0;; i++) { - if (i == v2->listSize()) { - return false; - } else if (i == v1->listSize()) { - return true; - } else if (!state.eqValues(*v1->listElems()[i], *v2->listElems()[i], pos, errorCtx)) { - return (*this)(v1->listElems()[i], v2->listElems()[i], "while comparing two list elements"); - } - } - default: - state.error("cannot compare %s with %s; values of that type are incomparable", showType(*v1), showType(*v2)).debugThrow(); - #pragma GCC diagnostic pop - } - } catch (Error & e) { - if (!errorCtx.empty()) - e.addTrace(nullptr, errorCtx); - throw; - } - } -}; - static void prim_genericClosure(EvalState & state, const PosIdx pos, Value * * args, Value & v) { state.forceAttrs(*args[0], noPos, "while evaluating the first argument passed to builtins.genericClosure"); @@ -2293,61 +2236,6 @@ static RegisterPrimOp primop_isAttrs({ "nameN"; value = valueN;}] is transformed to {name1 = value1; ... nameN = valueN;}. In case of duplicate occurrences of the same name, the first takes precedence. */ -static void prim_listToAttrs(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - state.forceList(*args[0], pos, "while evaluating the argument passed to builtins.listToAttrs"); - - auto attrs = state.buildBindings(args[0]->listSize()); - - std::set seen; - - for (auto v2 : args[0]->listItems()) { - state.forceAttrs(*v2, pos, "while evaluating an element of the list passed to builtins.listToAttrs"); - - Bindings::iterator j = getAttr(state, state.sName, v2->attrs, "in a {name=...; value=...;} pair"); - - auto name = state.forceStringNoCtx(*j->value, j->pos, "while evaluating the `name` attribute of an element of the list passed to builtins.listToAttrs"); - - auto sym = state.symbols.create(name); - if (seen.insert(sym).second) { - Bindings::iterator j2 = getAttr(state, state.sValue, v2->attrs, "in a {name=...; value=...;} pair"); - attrs.insert(sym, j2->value, j2->pos); - } - } - - v.mkAttrs(attrs); -} - -static RegisterPrimOp primop_listToAttrs({ - .name = "__listToAttrs", - .args = {"e"}, - .doc = R"( - Construct a set from a list specifying the names and values of each - attribute. Each element of the list should be a set consisting of a - string-valued attribute `name` specifying the name of the attribute, - and an attribute `value` specifying its value. - - In case of duplicate occurrences of the same name, the first - takes precedence. - - Example: - - ```nix - builtins.listToAttrs - [ { name = "foo"; value = 123; } - { name = "bar"; value = 456; } - { name = "bar"; value = 420; } - ] - ``` - - evaluates to - - ```nix - { foo = 123; bar = 456; } - ``` - )", - .fun = prim_listToAttrs, -}); static void prim_functionArgs(EvalState & state, const PosIdx pos, Value * * args, Value & v) { @@ -2409,499 +2297,16 @@ static RegisterPrimOp primop_isList({ .fun = prim_isList, }); -static void elemAt(EvalState & state, const PosIdx pos, Value & list, int n, Value & v) -{ - state.forceList(list, pos, "while evaluating the first argument passed to builtins.elemAt"); - if (n < 0 || (unsigned int) n >= list.listSize()) - state.error( - "list index %1% is out of bounds", - n - ).atPos(pos).debugThrow(); - state.forceValue(*list.listElems()[n], pos); - v = *list.listElems()[n]; -} - -/* Return the n-1'th element of a list. */ -static void prim_elemAt(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - elemAt(state, pos, *args[0], state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.elemAt"), v); -} - -static RegisterPrimOp primop_elemAt({ - .name = "__elemAt", - .args = {"xs", "n"}, - .doc = R"( - Return element *n* from the list *xs*. Elements are counted starting - from 0. A fatal error occurs if the index is out of bounds. - )", - .fun = prim_elemAt, -}); - /* Return the first element of a list. */ -static void prim_head(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - elemAt(state, pos, *args[0], 0, v); -} -static RegisterPrimOp primop_head({ - .name = "__head", - .args = {"list"}, - .doc = R"( - Return the first element of a list; abort evaluation if the argument - isn’t a list or is an empty list. You can test whether a list is - empty by comparing it with `[]`. - )", - .fun = prim_head, -}); - -/* Return a list consisting of everything but the first element of - a list. Warning: this function takes O(n) time, so you probably - don't want to use it! */ -static void prim_tail(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.tail"); - if (args[0]->listSize() == 0) - state.error("'tail' called on an empty list").atPos(pos).debugThrow(); - - state.mkList(v, args[0]->listSize() - 1); - for (unsigned int n = 0; n < v.listSize(); ++n) - v.listElems()[n] = args[0]->listElems()[n + 1]; -} - -static RegisterPrimOp primop_tail({ - .name = "__tail", - .args = {"list"}, - .doc = R"( - Return the second to last elements of a list; abort evaluation if - the argument isn’t a list or is an empty list. - - > **Warning** - > - > This function should generally be avoided since it's inefficient: - > unlike Haskell's `tail`, it takes O(n) time, so recursing over a - > list by repeatedly calling `tail` takes O(n^2) time. - )", - .fun = prim_tail, -}); /* Apply a function to every element of a list. */ -static void prim_map(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.map"); - - if (args[1]->listSize() == 0) { - v = *args[1]; - return; - } - - state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.map"); - - state.mkList(v, args[1]->listSize()); - for (unsigned int n = 0; n < v.listSize(); ++n) - (v.listElems()[n] = state.allocValue())->mkApp( - args[0], args[1]->listElems()[n]); -} - -static RegisterPrimOp primop_map({ - .name = "map", - .args = {"f", "list"}, - .doc = R"( - Apply the function *f* to each element in the list *list*. For - example, - - ```nix - map (x: "foo" + x) [ "bar" "bla" "abc" ] - ``` - - evaluates to `[ "foobar" "foobla" "fooabc" ]`. - )", - .fun = prim_map, -}); - -/* Filter a list using a predicate; that is, return a list containing - every element from the list for which the predicate function - returns true. */ -static void prim_filter(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.filter"); - - if (args[1]->listSize() == 0) { - v = *args[1]; - return; - } - - state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.filter"); - - SmallValueVector vs(args[1]->listSize()); - size_t k = 0; - - bool same = true; - for (unsigned int n = 0; n < args[1]->listSize(); ++n) { - Value res; - state.callFunction(*args[0], *args[1]->listElems()[n], res, noPos); - if (state.forceBool(res, pos, "while evaluating the return value of the filtering function passed to builtins.filter")) - vs[k++] = args[1]->listElems()[n]; - else - same = false; - } - - if (same) - v = *args[1]; - else { - state.mkList(v, k); - for (unsigned int n = 0; n < k; ++n) v.listElems()[n] = vs[n]; - } -} - -static RegisterPrimOp primop_filter({ - .name = "__filter", - .args = {"f", "list"}, - .doc = R"( - Return a list consisting of the elements of *list* for which the - function *f* returns `true`. - )", - .fun = prim_filter, -}); - -/* Return true if a list contains a given element. */ -static void prim_elem(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - bool res = false; - state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.elem"); - for (auto elem : args[1]->listItems()) - if (state.eqValues(*args[0], *elem, pos, "while searching for the presence of the given element in the list")) { - res = true; - break; - } - v.mkBool(res); -} - -static RegisterPrimOp primop_elem({ - .name = "__elem", - .args = {"x", "xs"}, - .doc = R"( - Return `true` if a value equal to *x* occurs in the list *xs*, and - `false` otherwise. - )", - .fun = prim_elem, -}); - -/* Concatenate a list of lists. */ -static void prim_concatLists(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.concatLists"); - state.concatLists(v, args[0]->listSize(), args[0]->listElems(), pos, "while evaluating a value of the list passed to builtins.concatLists"); -} - -static RegisterPrimOp primop_concatLists({ - .name = "__concatLists", - .args = {"lists"}, - .doc = R"( - Concatenate a list of lists into a single list. - )", - .fun = prim_concatLists, -}); /* Return the length of a list. This is an O(1) time operation. */ -static void prim_length(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.length"); - v.mkInt(args[0]->listSize()); -} - -static RegisterPrimOp primop_length({ - .name = "__length", - .args = {"e"}, - .doc = R"( - Return the length of the list *e*. - )", - .fun = prim_length, -}); - -/* Reduce a list by applying a binary operator, from left to - right. The operator is applied strictly. */ -static void prim_foldlStrict(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.foldlStrict"); - state.forceList(*args[2], pos, "while evaluating the third argument passed to builtins.foldlStrict"); - - if (args[2]->listSize()) { - Value * vCur = args[1]; - - for (auto [n, elem] : enumerate(args[2]->listItems())) { - Value * vs []{vCur, elem}; - vCur = n == args[2]->listSize() - 1 ? &v : state.allocValue(); - state.callFunction(*args[0], 2, vs, *vCur, pos); - } - state.forceValue(v, pos); - } else { - state.forceValue(*args[1], pos); - v = *args[1]; - } -} - -static RegisterPrimOp primop_foldlStrict({ - .name = "__foldl'", - .args = {"op", "nul", "list"}, - .doc = R"( - Reduce a list by applying a binary operator, from left to right, - e.g. `foldl' op nul [x0 x1 x2 ...] = op (op (op nul x0) x1) x2) - ...`. For example, `foldl' (x: y: x + y) 0 [1 2 3]` evaluates to 6. - The return value of each application of `op` is evaluated immediately, - even for intermediate values. - )", - .fun = prim_foldlStrict, -}); - -static void anyOrAll(bool any, EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - state.forceFunction(*args[0], pos, std::string("while evaluating the first argument passed to builtins.") + (any ? "any" : "all")); - state.forceList(*args[1], pos, std::string("while evaluating the second argument passed to builtins.") + (any ? "any" : "all")); - - std::string_view errorCtx = any - ? "while evaluating the return value of the function passed to builtins.any" - : "while evaluating the return value of the function passed to builtins.all"; - - Value vTmp; - for (auto elem : args[1]->listItems()) { - state.callFunction(*args[0], *elem, vTmp, pos); - bool res = state.forceBool(vTmp, pos, errorCtx); - if (res == any) { - v.mkBool(any); - return; - } - } - - v.mkBool(!any); -} -static void prim_any(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - anyOrAll(true, state, pos, args, v); -} - -static RegisterPrimOp primop_any({ - .name = "__any", - .args = {"pred", "list"}, - .doc = R"( - Return `true` if the function *pred* returns `true` for at least one - element of *list*, and `false` otherwise. - )", - .fun = prim_any, -}); - -static void prim_all(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - anyOrAll(false, state, pos, args, v); -} - -static RegisterPrimOp primop_all({ - .name = "__all", - .args = {"pred", "list"}, - .doc = R"( - Return `true` if the function *pred* returns `true` for all elements - of *list*, and `false` otherwise. - )", - .fun = prim_all, -}); - -static void prim_genList(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - auto len = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.genList"); - - if (len < 0) - state.error("cannot create list of size %1%", len).atPos(pos).debugThrow(); - - // More strict than striclty (!) necessary, but acceptable - // as evaluating map without accessing any values makes little sense. - state.forceFunction(*args[0], noPos, "while evaluating the first argument passed to builtins.genList"); - - state.mkList(v, len); - for (unsigned int n = 0; n < (unsigned int) len; ++n) { - auto arg = state.allocValue(); - arg->mkInt(n); - (v.listElems()[n] = state.allocValue())->mkApp(args[0], arg); - } -} - -static RegisterPrimOp primop_genList({ - .name = "__genList", - .args = {"generator", "length"}, - .doc = R"( - Generate list of size *length*, with each element *i* equal to the - value returned by *generator* `i`. For example, - - ```nix - builtins.genList (x: x * x) 5 - ``` - - returns the list `[ 0 1 4 9 16 ]`. - )", - .fun = prim_genList, -}); - -static void prim_lessThan(EvalState & state, const PosIdx pos, Value * * args, Value & v); -static void prim_sort(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.sort"); - - auto len = args[1]->listSize(); - if (len == 0) { - v = *args[1]; - return; - } - - state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.sort"); - - state.mkList(v, len); - for (unsigned int n = 0; n < len; ++n) { - state.forceValue(*args[1]->listElems()[n], pos); - v.listElems()[n] = args[1]->listElems()[n]; - } - - auto comparator = [&](Value * a, Value * b) { - /* Optimization: if the comparator is lessThan, bypass - callFunction. */ - /* TODO: (layus) this is absurd. An optimisation like this - should be outside the lambda creation */ - if (args[0]->isPrimOp()) { - auto ptr = args[0]->primOp->fun.target(); - if (ptr && *ptr == prim_lessThan) - return CompareValues(state, noPos, "while evaluating the ordering function passed to builtins.sort")(a, b); - } - - Value * vs[] = {a, b}; - Value vBool; - state.callFunction(*args[0], 2, vs, vBool, noPos); - return state.forceBool(vBool, pos, "while evaluating the return value of the sorting function passed to builtins.sort"); - }; - - /* FIXME: std::sort can segfault if the comparator is not a strict - weak ordering. What to do? std::stable_sort() seems more - resilient, but no guarantees... */ - std::stable_sort(v.listElems(), v.listElems() + len, comparator); -} - -static RegisterPrimOp primop_sort({ - .name = "__sort", - .args = {"comparator", "list"}, - .doc = R"( - Return *list* in sorted order. It repeatedly calls the function - *comparator* with two elements. The comparator should return `true` - if the first element is less than the second, and `false` otherwise. - For example, - - ```nix - builtins.sort builtins.lessThan [ 483 249 526 147 42 77 ] - ``` - - produces the list `[ 42 77 147 249 483 526 ]`. - - This is a stable sort: it preserves the relative order of elements - deemed equal by the comparator. - )", - .fun = prim_sort, -}); - -static void prim_partition(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.partition"); - state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.partition"); - - auto len = args[1]->listSize(); - - ValueVector right, wrong; - - for (unsigned int n = 0; n < len; ++n) { - auto vElem = args[1]->listElems()[n]; - state.forceValue(*vElem, pos); - Value res; - state.callFunction(*args[0], *vElem, res, pos); - if (state.forceBool(res, pos, "while evaluating the return value of the partition function passed to builtins.partition")) - right.push_back(vElem); - else - wrong.push_back(vElem); - } - - auto attrs = state.buildBindings(2); - - auto & vRight = attrs.alloc(state.sRight); - auto rsize = right.size(); - state.mkList(vRight, rsize); - if (rsize) - memcpy(vRight.listElems(), right.data(), sizeof(Value *) * rsize); - - auto & vWrong = attrs.alloc(state.sWrong); - auto wsize = wrong.size(); - state.mkList(vWrong, wsize); - if (wsize) - memcpy(vWrong.listElems(), wrong.data(), sizeof(Value *) * wsize); - - v.mkAttrs(attrs); -} - -static RegisterPrimOp primop_partition({ - .name = "__partition", - .args = {"pred", "list"}, - .doc = R"( - Given a predicate function *pred*, this function returns an - attrset containing a list named `right`, containing the elements - in *list* for which *pred* returned `true`, and a list named - `wrong`, containing the elements for which it returned - `false`. For example, - - ```nix - builtins.partition (x: x > 10) [1 23 9 3 42] - ``` - - evaluates to - - ```nix - { right = [ 23 42 ]; wrong = [ 1 9 3 ]; } - ``` - )", - .fun = prim_partition, -}); - -static void prim_concatMap(EvalState & state, const PosIdx pos, Value * * args, Value & v) -{ - state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.concatMap"); - state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.concatMap"); - auto nrLists = args[1]->listSize(); - - // List of returned lists before concatenation. References to these Values must NOT be persisted. - SmallTemporaryValueVector lists(nrLists); - size_t len = 0; - - for (unsigned int n = 0; n < nrLists; ++n) { - Value * vElem = args[1]->listElems()[n]; - state.callFunction(*args[0], *vElem, lists[n], pos); - state.forceList(lists[n], lists[n].determinePos(args[0]->determinePos(pos)), "while evaluating the return value of the function passed to builtins.concatMap"); - len += lists[n].listSize(); - } - - state.mkList(v, len); - auto out = v.listElems(); - for (unsigned int n = 0, pos = 0; n < nrLists; ++n) { - auto l = lists[n].listSize(); - if (l) - memcpy(out + pos, lists[n].listElems(), l * sizeof(Value *)); - pos += l; - } -} - -static RegisterPrimOp primop_concatMap({ - .name = "__concatMap", - .args = {"f", "list"}, - .doc = R"( - This function is equivalent to `builtins.concatLists (map f list)` - but is more efficient. - )", - .fun = prim_concatMap, -}); /************************************************************* @@ -3048,7 +2453,7 @@ static RegisterPrimOp primop_bitXor({ .fun = prim_bitXor, }); -static void prim_lessThan(EvalState & state, const PosIdx pos, Value * * args, Value & v) +void prim_lessThan(EvalState & state, const PosIdx pos, Value * * args, Value & v) { state.forceValue(*args[0], pos); state.forceValue(*args[1], pos); diff --git a/src/libexpr/primops.hh b/src/libexpr/primops.hh index 00ffb019a..b326cfd59 100644 --- a/src/libexpr/primops.hh +++ b/src/libexpr/primops.hh @@ -4,7 +4,6 @@ #include "eval.hh" #include -#include #include namespace nix { @@ -52,6 +51,8 @@ void prim_importNative(EvalState & state, const PosIdx pos, Value ** args, Value */ void prim_exec(EvalState & state, const PosIdx pos, Value ** args, Value & v); +void prim_lessThan(EvalState & state, const PosIdx pos, Value ** args, Value & v); + void makePositionThunks(EvalState & state, const PosIdx pos, Value & line, Value & column); #if HAVE_BOEHMGC @@ -64,7 +65,8 @@ typedef std::list ValueList; * getAttr wrapper */ -Bindings::iterator getAttr(EvalState & state, Symbol attrSym, Bindings * attrSet, std::string_view errorCtx); +Bindings::iterator +getAttr(EvalState & state, Symbol attrSym, Bindings * attrSet, std::string_view errorCtx); /** * Struct definitions @@ -79,18 +81,100 @@ struct RegexCache std::regex get(std::string_view re) { auto it = cache.find(re); - if (it != cache.end()) + if (it != cache.end()) { return it->second; + } keys.emplace_back(re); - return cache.emplace(keys.back(), std::regex(keys.back(), std::regex::extended)).first->second; + return cache.emplace(keys.back(), std::regex(keys.back(), std::regex::extended)) + .first->second; } }; -struct RealisePathFlags { +struct CompareValues +{ + EvalState & state; + const PosIdx pos; + const std::string_view errorCtx; + + CompareValues(EvalState & state, const PosIdx pos, const std::string_view && errorCtx) + : state(state) + , pos(pos) + , errorCtx(errorCtx){}; + + bool operator()(Value * v1, Value * v2) const + { + return (*this)(v1, v2, errorCtx); + } + + bool operator()(Value * v1, Value * v2, std::string_view errorCtx) const + { + try { + if (v1->type() == nFloat && v2->type() == nInt) { + return v1->fpoint < v2->integer; + } + if (v1->type() == nInt && v2->type() == nFloat) { + return v1->integer < v2->fpoint; + } + if (v1->type() != v2->type()) { + state.error("cannot compare %s with %s", showType(*v1), showType(*v2)) + .debugThrow(); + } +// Allow selecting a subset of enum values +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch-enum" + switch (v1->type()) { + case nInt: + return v1->integer < v2->integer; + case nFloat: + return v1->fpoint < v2->fpoint; + case nString: + return strcmp(v1->string.s, v2->string.s) < 0; + case nPath: + return strcmp(v1->_path, v2->_path) < 0; + case nList: + // Lexicographic comparison + for (size_t i = 0;; i++) { + if (i == v2->listSize()) { + return false; + } else if (i == v1->listSize()) { + return true; + } else if (!state.eqValues( + *v1->listElems()[i], *v2->listElems()[i], pos, errorCtx + )) + { + return (*this)( + v1->listElems()[i], + v2->listElems()[i], + "while comparing two list elements" + ); + } + } + default: + state + .error( + "cannot compare %s with %s; values of that type are incomparable", + showType(*v1), + showType(*v2) + ) + .debugThrow(); +#pragma GCC diagnostic pop + } + } catch (Error & e) { + if (!errorCtx.empty()) { + e.addTrace(nullptr, errorCtx); + } + throw; + } + } +}; + +struct RealisePathFlags +{ // Whether to check that the path is allowed in pure eval mode bool checkForPureEval = true; }; -SourcePath realisePath(EvalState & state, const PosIdx pos, Value & v, const RealisePathFlags flags = {}); +SourcePath +realisePath(EvalState & state, const PosIdx pos, Value & v, const RealisePathFlags flags = {}); } diff --git a/src/libexpr/primops/list.cc b/src/libexpr/primops/list.cc new file mode 100644 index 000000000..110e2077b --- /dev/null +++ b/src/libexpr/primops/list.cc @@ -0,0 +1,680 @@ +#include "gc-small-vector.hh" +#include "primops.hh" + +namespace nix { + +static void anyOrAll(bool any, EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + const std::string fun = any ? "builtins.any" : "builtins.all"; + state.forceFunction( + *args[0], pos, std::string("while evaluating the first argument passed to " + fun) + ); + state.forceList( + *args[1], pos, std::string("while evaluating the second argument passed to " + fun) + ); + Value vTmp; + for (auto elem : args[1]->listItems()) { + state.callFunction(*args[0], *elem, vTmp, pos); + bool res = state.forceBool( + vTmp, pos, "while evaluating the return value of the function passed to " + fun + ); + if (res == any) { + v.mkBool(any); + return; + } + } + v.mkBool(!any); +} + +static void elemAt(EvalState & state, const PosIdx pos, Value & list, int n, Value & v) +{ + state.forceList(list, pos, "while evaluating the first argument passed to builtins.elemAt"); + if (n < 0 || (unsigned int) n >= list.listSize()) { + state.error("list index %1% is out of bounds", n).atPos(pos).debugThrow(); + } + state.forceValue(*list.listElems()[n], pos); + v = *list.listElems()[n]; +} + +/** + * builtins.all + */ + +static void prim_all(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + anyOrAll(false, state, pos, args, v); +} + +static RegisterPrimOp primop_all({ + .name = "__all", + .args = {"pred", "list"}, + .doc = R"( + Return `true` if the function *pred* returns `true` for all elements + of *list*, and `false` otherwise. + )", + .fun = prim_all, +}); + +/** + * builtins.any + */ + +static void prim_any(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + anyOrAll(true, state, pos, args, v); +} + +static RegisterPrimOp primop_any({ + .name = "__any", + .args = {"pred", "list"}, + .doc = R"( + Return `true` if the function *pred* returns `true` for at least one + element of *list*, and `false` otherwise. + )", + .fun = prim_any, +}); + +/** + * builtins.concatLists + */ + +static void prim_concatLists(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + state.forceList( + *args[0], pos, "while evaluating the first argument passed to builtins.concatLists" + ); + state.concatLists( + v, + args[0]->listSize(), + args[0]->listElems(), + pos, + "while evaluating a value of the list passed to builtins.concatLists" + ); +} + +static RegisterPrimOp primop_concatLists({ + .name = "__concatLists", + .args = {"lists"}, + .doc = R"( + Concatenate a list of lists into a single list. + )", + .fun = prim_concatLists, +}); + +/** + * builtins.concatMap + */ + +static void prim_concatMap(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + state.forceFunction( + *args[0], pos, "while evaluating the first argument passed to builtins.concatMap" + ); + state.forceList( + *args[1], pos, "while evaluating the second argument passed to builtins.concatMap" + ); + auto nrLists = args[1]->listSize(); + + // List of returned lists before concatenation. References to these Values must NOT be + // persisted. + SmallTemporaryValueVector lists(nrLists); + size_t len = 0; + + for (unsigned int n = 0; n < nrLists; ++n) { + Value * vElem = args[1]->listElems()[n]; + state.callFunction(*args[0], *vElem, lists[n], pos); + state.forceList( + lists[n], + lists[n].determinePos(args[0]->determinePos(pos)), + "while evaluating the return value of the function passed to builtins.concatMap" + ); + len += lists[n].listSize(); + } + + state.mkList(v, len); + auto out = v.listElems(); + for (unsigned int n = 0, pos = 0; n < nrLists; ++n) { + auto l = lists[n].listSize(); + if (l) { + memcpy(out + pos, lists[n].listElems(), l * sizeof(Value *)); + } + pos += l; + } +} + +static RegisterPrimOp primop_concatMap({ + .name = "__concatMap", + .args = {"f", "list"}, + .doc = R"( + This function is equivalent to `builtins.concatLists (map f list)` + but is more efficient. + )", + .fun = prim_concatMap, +}); + +/** + * builtins.elem + */ + +static void prim_elem(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + bool res = false; + state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.elem"); + for (auto elem : args[1]->listItems()) { + if (state.eqValues( + *args[0], + *elem, + pos, + "while searching for the presence of the given element in the list" + )) + { + res = true; + break; + } + } + v.mkBool(res); +} + +static RegisterPrimOp primop_elem({ + .name = "__elem", + .args = {"x", "xs"}, + .doc = R"( + Return `true` if a value equal to *x* occurs in the list *xs*, and + `false` otherwise. + )", + .fun = prim_elem, +}); + +/** + * builtins.elemAt + */ + +static void prim_elemAt(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + elemAt( + state, + pos, + *args[0], + state.forceInt( + *args[1], pos, "while evaluating the second argument passed to builtins.elemAt" + ), + v + ); +} + +static RegisterPrimOp primop_elemAt({ + .name = "__elemAt", + .args = {"xs", "n"}, + .doc = R"( + Return element *n* from the list *xs*. Elements are counted starting + from 0. A fatal error occurs if the index is out of bounds. + )", + .fun = prim_elemAt, +}); + +/** + * builtins.filter + */ + +static void prim_filter(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + state.forceList( + *args[1], pos, "while evaluating the second argument passed to builtins.filter" + ); + + if (args[1]->listSize() == 0) { + v = *args[1]; + return; + } + + state.forceFunction( + *args[0], pos, "while evaluating the first argument passed to builtins.filter" + ); + + SmallValueVector vs(args[1]->listSize()); + size_t k = 0; + + bool same = true; + for (unsigned int n = 0; n < args[1]->listSize(); ++n) { + Value res; + state.callFunction(*args[0], *args[1]->listElems()[n], res, noPos); + if (state.forceBool( + res, + pos, + "while evaluating the return value of the filtering function passed to " + "builtins.filter" + )) + { + vs[k++] = args[1]->listElems()[n]; + } else { + same = false; + } + } + + if (same) { + v = *args[1]; + } else { + state.mkList(v, k); + for (unsigned int n = 0; n < k; ++n) { + v.listElems()[n] = vs[n]; + } + } +} + +/** + * builtins.foldl' + */ + +static void prim_foldlStrict(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + state.forceFunction( + *args[0], pos, "while evaluating the first argument passed to builtins.foldlStrict" + ); + state.forceList( + *args[2], pos, "while evaluating the third argument passed to builtins.foldlStrict" + ); + if (args[2]->listSize()) { + Value * vCur = args[1]; + for (auto [n, elem] : enumerate(args[2]->listItems())) { + Value * vs[]{vCur, elem}; + vCur = n == args[2]->listSize() - 1 ? &v : state.allocValue(); + state.callFunction(*args[0], 2, vs, *vCur, pos); + } + state.forceValue(v, pos); + } else { + state.forceValue(*args[1], pos); + v = *args[1]; + } +} + +static RegisterPrimOp primop_foldlStrict({ + .name = "__foldl'", + .args = {"op", "nul", "list"}, + .doc = R"( + Reduce a list by applying a binary operator, from left to right, + e.g. `foldl' op nul [x0 x1 x2 ...] = op (op (op nul x0) x1) x2) + ...`. For example, `foldl' (x: y: x + y) 0 [1 2 3]` evaluates to 6. + The return value of each application of `op` is evaluated immediately, + even for intermediate values. + )", + .fun = prim_foldlStrict, +}); + +/** + * builtins.genList + */ + +static void prim_genList(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + auto len = state.forceInt( + *args[1], pos, "while evaluating the second argument passed to builtins.genList" + ); + if (len < 0) { + state.error("cannot create list of size %1%", len).atPos(pos).debugThrow(); + } + // More strict than striclty (!) necessary, but acceptable + // as evaluating map without accessing any values makes little sense. + state.forceFunction( + *args[0], noPos, "while evaluating the first argument passed to builtins.genList" + ); + state.mkList(v, len); + for (unsigned int n = 0; n < (unsigned int) len; ++n) { + auto arg = state.allocValue(); + arg->mkInt(n); + (v.listElems()[n] = state.allocValue())->mkApp(args[0], arg); + } +} + +static RegisterPrimOp primop_genList({ + .name = "__genList", + .args = {"generator", "length"}, + .doc = R"( + Generate list of size *length*, with each element *i* equal to the + value returned by *generator* `i`. For example, + ```nix + builtins.genList (x: x * x) 5 + ``` + returns the list `[ 0 1 4 9 16 ]`. + )", + .fun = prim_genList, +}); + +/** + * builtins.head + */ + +static void prim_head(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + elemAt(state, pos, *args[0], 0, v); +} + +static RegisterPrimOp primop_head({ + .name = "__head", + .args = {"list"}, + .doc = R"( + Return the first element of a list; abort evaluation if the argument + isn’t a list or is an empty list. You can test whether a list is + empty by comparing it with `[]`. + )", + .fun = prim_head, +}); + +/** + * builtins.length + */ + +static void prim_length(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.length"); + v.mkInt(args[0]->listSize()); +} +static RegisterPrimOp primop_length({ + .name = "__length", + .args = {"e"}, + .doc = R"( + Return the length of the list *e*. + )", + .fun = prim_length, +}); + +static RegisterPrimOp primop_filter({ + .name = "__filter", + .args = {"f", "list"}, + .doc = R"( + Return a list consisting of the elements of *list* for which the + function *f* returns `true`. + )", + .fun = prim_filter, +}); + +/** + * builtins.listToAttrs + */ + +static void prim_listToAttrs(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + state.forceList(*args[0], pos, "while evaluating the argument passed to builtins.listToAttrs"); + + auto attrs = state.buildBindings(args[0]->listSize()); + + std::set seen; + + for (auto v2 : args[0]->listItems()) { + state.forceAttrs( + *v2, pos, "while evaluating an element of the list passed to builtins.listToAttrs" + ); + + Bindings::iterator j = + getAttr(state, state.sName, v2->attrs, "in a {name=...; value=...;} pair"); + + auto name = state.forceStringNoCtx( + *j->value, + j->pos, + "while evaluating the `name` attribute of an element of the list passed to " + "builtins.listToAttrs" + ); + + auto sym = state.symbols.create(name); + if (seen.insert(sym).second) { + Bindings::iterator j2 = + getAttr(state, state.sValue, v2->attrs, "in a {name=...; value=...;} pair"); + attrs.insert(sym, j2->value, j2->pos); + } + } + + v.mkAttrs(attrs); +} + +static RegisterPrimOp primop_listToAttrs({ + .name = "__listToAttrs", + .args = {"e"}, + .doc = R"( + Construct a set from a list specifying the names and values of each + attribute. Each element of the list should be a set consisting of a + string-valued attribute `name` specifying the name of the attribute, + and an attribute `value` specifying its value. + + In case of duplicate occurrences of the same name, the first + takes precedence. + + Example: + + ```nix + builtins.listToAttrs + [ { name = "foo"; value = 123; } + { name = "bar"; value = 456; } + { name = "bar"; value = 420; } + ] + ``` + + evaluates to + + ```nix + { foo = 123; bar = 456; } + ``` + )", + .fun = prim_listToAttrs, +}); + +/** + * builtins.map + */ + +static void prim_map(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.map"); + + if (args[1]->listSize() == 0) { + v = *args[1]; + return; + } + + state.forceFunction( + *args[0], pos, "while evaluating the first argument passed to builtins.map" + ); + + state.mkList(v, args[1]->listSize()); + for (unsigned int n = 0; n < v.listSize(); ++n) { + (v.listElems()[n] = state.allocValue())->mkApp(args[0], args[1]->listElems()[n]); + } +} + +static RegisterPrimOp primop_map({ + .name = "map", + .args = {"f", "list"}, + .doc = R"( + Apply the function *f* to each element in the list *list*. For + example, + + ```nix + map (x: "foo" + x) [ "bar" "bla" "abc" ] + ``` + + evaluates to `[ "foobar" "foobla" "fooabc" ]`. + )", + .fun = prim_map, +}); + +/** + * builtins.partition + */ + +static void prim_partition(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + state.forceFunction( + *args[0], pos, "while evaluating the first argument passed to builtins.partition" + ); + state.forceList( + *args[1], pos, "while evaluating the second argument passed to builtins.partition" + ); + + auto len = args[1]->listSize(); + + ValueVector right, wrong; + + for (unsigned int n = 0; n < len; ++n) { + auto vElem = args[1]->listElems()[n]; + state.forceValue(*vElem, pos); + Value res; + state.callFunction(*args[0], *vElem, res, pos); + if (state.forceBool( + res, + pos, + "while evaluating the return value of the partition function passed to " + "builtins.partition" + )) + { + right.push_back(vElem); + } else { + wrong.push_back(vElem); + } + } + + auto attrs = state.buildBindings(2); + + auto & vRight = attrs.alloc(state.sRight); + auto rsize = right.size(); + state.mkList(vRight, rsize); + if (rsize) { + memcpy(vRight.listElems(), right.data(), sizeof(Value *) * rsize); + } + + auto & vWrong = attrs.alloc(state.sWrong); + auto wsize = wrong.size(); + state.mkList(vWrong, wsize); + if (wsize) { + memcpy(vWrong.listElems(), wrong.data(), sizeof(Value *) * wsize); + } + + v.mkAttrs(attrs); +} + +static RegisterPrimOp primop_partition({ + .name = "__partition", + .args = {"pred", "list"}, + .doc = R"( + Given a predicate function *pred*, this function returns an + attrset containing a list named `right`, containing the elements + in *list* for which *pred* returned `true`, and a list named + `wrong`, containing the elements for which it returned + `false`. For example, + + ```nix + builtins.partition (x: x > 10) [1 23 9 3 42] + ``` + + evaluates to + + ```nix + { right = [ 23 42 ]; wrong = [ 1 9 3 ]; } + ``` + )", + .fun = prim_partition, +}); + +/** + * builtins.sort + */ + +static void prim_sort(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.sort"); + + auto len = args[1]->listSize(); + if (len == 0) { + v = *args[1]; + return; + } + + state.forceFunction( + *args[0], pos, "while evaluating the first argument passed to builtins.sort" + ); + + state.mkList(v, len); + for (unsigned int n = 0; n < len; ++n) { + state.forceValue(*args[1]->listElems()[n], pos); + v.listElems()[n] = args[1]->listElems()[n]; + } + + auto comparator = [&](Value * a, Value * b) { + /* Optimization: if the comparator is lessThan, bypass + callFunction. */ + /* TODO: (layus) this is absurd. An optimisation like this + should be outside the lambda creation */ + if (args[0]->isPrimOp()) { + auto ptr = args[0]->primOp->fun.target(); + if (ptr && *ptr == prim_lessThan) { + return CompareValues(state, noPos, "while evaluating the ordering function passed to builtins.sort")(a, b); + } + } + + Value * vs[] = {a, b}; + Value vBool; + state.callFunction(*args[0], 2, vs, vBool, noPos); + return state.forceBool( + vBool, + pos, + "while evaluating the return value of the sorting function passed to builtins.sort" + ); + }; + + /* FIXME: std::sort can segfault if the comparator is not a strict + weak ordering. What to do? std::stable_sort() seems more + resilient, but no guarantees... */ + std::stable_sort(v.listElems(), v.listElems() + len, comparator); +} + +static RegisterPrimOp primop_sort({ + .name = "__sort", + .args = {"comparator", "list"}, + .doc = R"( + Return *list* in sorted order. It repeatedly calls the function + *comparator* with two elements. The comparator should return `true` + if the first element is less than the second, and `false` otherwise. + For example, + + ```nix + builtins.sort builtins.lessThan [ 483 249 526 147 42 77 ] + ``` + + produces the list `[ 42 77 147 249 483 526 ]`. + + This is a stable sort: it preserves the relative order of elements + deemed equal by the comparator. + )", + .fun = prim_sort, +}); + +/** + * builtins.tail + */ + +static void prim_tail(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.tail"); + if (args[0]->listSize() == 0) { + state.error("'tail' called on an empty list").atPos(pos).debugThrow(); + } + + state.mkList(v, args[0]->listSize() - 1); + for (unsigned int n = 0; n < v.listSize(); ++n) { + v.listElems()[n] = args[0]->listElems()[n + 1]; + } +} + +static RegisterPrimOp primop_tail({ + .name = "__tail", + .args = {"list"}, + .doc = R"( + Return the second to last elements of a list; abort evaluation if + the argument isn’t a list or is an empty list. + + > **Warning** + > + > This function should generally be avoided since it's inefficient: + > unlike Haskell's `tail`, it takes O(n) time, so recursing over a + > list by repeatedly calling `tail` takes O(n^2) time. + )", + .fun = prim_tail, +}); +}