libexpr: add multi-arg app nodes

these behave like the old chains of app nodes, but they can store more
than one argument per node. for tApp values themselves this is not all
that useful, but if we could share tApp and tPrimOpApp backing storage
we could avoid creating and traversing the linked lists of values that
are currently needed to represent partially applied builtin functions.

Change-Id: I5a2a02d9733e1e0be5443459e2998d62fd3b9a5b
This commit is contained in:
eldritch horrors
2025-09-29 17:56:05 +02:00
parent 3350ab8a56
commit 0b5b14ddc7
5 changed files with 69 additions and 21 deletions
+20 -3
View File
@@ -5,9 +5,27 @@
#include "lix/libexpr/eval.hh"
#include "lix/libexpr/eval-error.hh"
#include "lix/libexpr/gc-alloc.hh"
#include "value.hh"
#include <cstdint>
namespace nix {
inline Value::Value(app_t, EvalMemory & mem, Value & lhs, std::span<Value *> args)
: internalType(tApp)
{
if (args.size() == 1) {
_app._left = reinterpret_cast<uintptr_t>(&lhs);
_app._right = args[0];
} else {
auto app =
static_cast<Value::AppN *>(mem.allocBytes(sizeof(Value::AppN) + args.size_bytes()));
app->nargs = args.size();
memcpy(app->args, args.data(), args.size_bytes());
_app._left = reinterpret_cast<uintptr_t>(&lhs) | 1;
_app._appn = app;
}
}
[[gnu::always_inline]]
void * EvalMemory::allocBytes(size_t size)
{
@@ -89,12 +107,11 @@ void EvalState::forceValue(Value & v, const PosIdx pos)
tryFixupBlackHolePos(v, pos);
throw;
}
} else if (v.isApp()) {
callFunction(*v.app().left(), v.app().args(), v, pos);
}
else if (v.isApp())
callFunction(*v.app().left, *v.app().right, v, pos);
}
[[gnu::always_inline]]
inline void EvalState::forceAttrs(Value & v, const PosIdx pos, std::string_view errorCtx)
{
+1
View File
@@ -157,6 +157,7 @@ void initLibExpr()
/* Don't look for interior pointers. This reduces the odds of
misdetection a bit. */
GC_set_all_interior_pointers(0);
GC_REGISTER_DISPLACEMENT(1);
/* We don't have any roots in data segments, so don't scan from
there. */
+6 -8
View File
@@ -1955,10 +1955,9 @@ static void prim_mapAttrs(EvalState & state, Value * * args, Value & v)
auto attrs = state.ctx.buildBindings(args[1]->attrs()->size());
for (auto & i : *args[1]->attrs()) {
Value * vFun2 = state.ctx.mem.allocValue();
auto vName = const_cast<Value *>(state.ctx.symbols[i.name].toValuePtr());
vFun2->mkApp(args[0], vName);
attrs.alloc(i.name).mkApp(vFun2, i.value);
Value * appArgs[] = {vName, i.value};
attrs.alloc(i.name) = {NewValueAs::app, state.ctx.mem, *args[0], appArgs};
}
v.mkAttrs(attrs.alreadySorted());
@@ -1997,13 +1996,12 @@ static void prim_zipAttrsWith(EvalState & state, Value * * args, Value & v)
/* Construct a `fn name list` function call value. */
auto name = const_cast<Value *>(state.ctx.symbols[sym].toValuePtr());
auto call1 = state.ctx.mem.allocValue();
call1->mkApp(args[0], name);
auto call2 = state.ctx.mem.allocValue();
call2->mkApp(call1, list);
Value * callArgs[] = {name, list};
auto call = state.ctx.mem.allocValue();
*call = {NewValueAs::app, state.ctx.mem, *args[0], callArgs};
/* Insert it inside the returned attribute set. */
attrs.insert(sym, call2);
attrs.insert(sym, call);
}
/* Populate the lists inside the attribute set */
+41 -9
View File
@@ -3,6 +3,7 @@
#include <cassert>
#include <climits>
#include <cstdint>
#include <functional>
#include <ranges>
#include <span>
@@ -19,6 +20,7 @@
namespace nix {
class BindingsBuilder;
class EvalMemory;
typedef enum {
tInt = 1,
@@ -440,8 +442,13 @@ public:
/// lazy partial application of another lambda.
Value(app_t, Value & lhs, Value & rhs)
: internalType(tApp)
, _app({ .left = &lhs, .right = &rhs })
{ }
, _app{._left = reinterpret_cast<uintptr_t>(&lhs), ._right = &rhs}
{
}
/// Constructs a nix language value of type "lambda", which represents a
/// lazy partial application of another lambda.
Value(app_t, EvalMemory & mem, Value & lhs, std::span<Value *> args);
/// Constructs a nix language value of type "external", which is only used
/// by plugins. Do any existing plugins even use this mechanism?
@@ -562,6 +569,17 @@ public:
}
};
struct AppN
{
size_t nargs;
Value * args[0];
std::span<Value *> argsSpan()
{
return {args, nargs};
}
};
union
{
/// Dummy field, which takes up as much space as the largest union variants
@@ -589,9 +607,25 @@ public:
Expr * expr;
} _thunk;
struct {
Value * left, * right;
uintptr_t _left;
union
{
Value * _right;
AppN * _appn;
};
Value * left() const
{
return reinterpret_cast<Value *>(_left & ~uintptr_t(1));
}
std::span<Value *> args()
{
return _left & 1 ? _appn->argsSpan() : std::span{&_right, 1};
}
} _app;
struct {
struct
{
Env * env;
ExprLambda * fun;
} _lambda;
@@ -647,7 +681,7 @@ public:
*/
inline void clearValue()
{
_app.left = _app.right = 0;
_empty[0] = _empty[1] = 0;
}
inline void mkInt(NixInt::Inner n)
@@ -716,9 +750,7 @@ public:
inline void mkApp(Value * l, Value * r)
{
internalType = tApp;
_app.left = l;
_app.right = r;
*this = {NewValueAs::app, *l, *r};
}
inline void mkLambda(Env * e, ExprLambda * f)
@@ -849,7 +881,7 @@ public:
return _thunk;
}
const auto & app() const
auto & app()
{
return _app;
}
+1 -1
View File
@@ -102,7 +102,7 @@ TEST_F(ValuePrintingTests, vThunk)
TEST_F(ValuePrintingTests, vApp)
{
Value vApp;
vApp.mkApp(nullptr, nullptr);
vApp.mkApp(&vApp, &vApp);
test(vApp, "«thunk»");
}