libexpr: make thunk state shareable

this is a strong prerequisite for making values themselves copyable
without duplicating evaluation side effects. with this we can treat
`Value` the way we treated `Value *` to date and drop indirections.

Change-Id: I08f30d12697614a3ae7149615f6f1da83b13f52b
This commit is contained in:
eldritch horrors
2025-09-28 00:02:21 +02:00
parent 30b971f6e0
commit 85ed12485e
4 changed files with 98 additions and 36 deletions
+27 -16
View File
@@ -13,7 +13,7 @@ namespace nix {
inline Value::Value(app_t, EvalMemory & mem, Value & lhs, Value & rhs)
{
auto app = static_cast<Value::App *>(mem.allocBytes(sizeof(Value::App) + sizeof(Value *)));
app->_left = &lhs;
app->_left = reinterpret_cast<uintptr_t>(&lhs);
app->_n = 1;
app->_args[0] = &rhs;
raw = tag(tApp, app);
@@ -22,7 +22,7 @@ inline Value::Value(app_t, EvalMemory & mem, Value & lhs, Value & rhs)
inline Value::Value(app_t, EvalMemory & mem, Value & lhs, std::span<Value *> args)
{
auto app = static_cast<Value::App *>(mem.allocBytes(sizeof(Value::App) + args.size_bytes()));
app->_left = &lhs;
app->_left = reinterpret_cast<uintptr_t>(&lhs);
app->_n = args.size();
memcpy(app->_args, args.data(), args.size_bytes());
raw = tag(tApp, app);
@@ -31,7 +31,7 @@ inline Value::Value(app_t, EvalMemory & mem, Value & lhs, std::span<Value *> arg
inline Value::Value(thunk_t, EvalMemory & mem, Env & env, Expr & expr)
{
auto thunk = mem.allocType<Thunk>();
*thunk = {.env = &env, .expr = &expr};
*thunk = {._env = reinterpret_cast<uintptr_t>(&env), .expr = &expr};
raw = tag(tThunk, thunk);
}
@@ -112,22 +112,33 @@ Env & EvalMemory::allocEnv(size_t size)
void EvalState::forceValue(Value & v, const PosIdx pos)
{
if (v.isThunk()) {
const auto backup = v;
Env * env = v.thunk().env;
Expr & expr = *v.thunk().expr;
v = Value{NewValueAs::blackhole};
try {
expr.eval(*this, *env, v);
} catch (...) {
v = backup;
tryFixupBlackHolePos(v, pos);
throw;
auto & thunk = v.thunk();
if (thunk.resolved()) {
v = thunk.result();
} else {
const auto backup = v;
Env * env = v.thunk().env();
Expr & expr = *v.thunk().expr;
v = Value{NewValueAs::blackhole};
try {
expr.eval(*this, *env, v);
backup.thunk().resolve(v);
} catch (...) {
v = backup;
tryFixupBlackHolePos(v, pos);
throw;
}
}
} else if (v.isApp()) {
auto & app = v.app();
auto target = app.target();
if (!target->isPrimOp() || target->primOp()->arity <= app.totalArgs()) {
callFunction(*v.app().left(), v.app().args(), v, pos);
if (app.resolved()) {
v = app.result();
} else {
auto target = app.target();
if (!target->isPrimOp() || target->primOp()->arity <= app.totalArgs()) {
callFunction(*v.app().left(), v.app().args(), v, pos);
app.resolve(v);
}
}
}
}
+8 -2
View File
@@ -2136,8 +2136,14 @@ void EvalState::forceValueDeep(Value & v)
try {
// If the value is a thunk, we're evaling. Otherwise no trace necessary.
auto dts = ctx.debug && i.value->isThunk()
? makeDebugTraceStacker(*this, *i.value->thunk().expr, *i.value->thunk().env, ctx.positions[i.pos],
"while evaluating the attribute '%1%'", ctx.symbols[i.name])
? makeDebugTraceStacker(
*this,
*i.value->thunk().expr,
*i.value->thunk().env(),
ctx.positions[i.pos],
"while evaluating the attribute '%1%'",
ctx.symbols[i.name]
)
: nullptr;
recurse(*i.value);
+1 -1
View File
@@ -10,7 +10,7 @@ namespace nix {
ExprBlackHole eBlackHole;
Value::Thunk Value::blackHole{nullptr, &eBlackHole};
Value::Thunk Value::blackHole{0, &eBlackHole};
// FIXME: remove, because *symbols* are abstract and do not have a single
// textual representation; see printIdentifier()
+62 -17
View File
@@ -243,7 +243,7 @@ struct NewValueAs
struct Value
{
private:
uintptr_t raw;
mutable uintptr_t raw;
static constexpr size_t TAG_BITS = 3;
static constexpr size_t TAG_ALIGN = 1 << TAG_BITS;
@@ -606,7 +606,7 @@ public:
}
inline bool isPrimOpApp() const
{
return internalType() == tApp && app().target()->isPrimOp();
return internalType() == tApp && !app().resolved() && app().target()->isPrimOp();
}
struct alignas(TAG_ALIGN) List
@@ -658,13 +658,31 @@ public:
struct alignas(TAG_ALIGN) App
{
Value * _left;
uintptr_t _left;
size_t _n;
Value * _args[0];
bool resolved() const
{
return _n == 0;
}
void resolve(Value v)
{
_left = v.raw;
_n = 0;
}
Value * left() const
{
return _left;
return reinterpret_cast<Value *>(_left);
}
Value result() const
{
Value v;
v.raw = _left;
return v;
}
Value * target() const
@@ -722,8 +740,31 @@ public:
};
struct alignas(TAG_ALIGN) Thunk
{
Env * env;
uintptr_t _env;
Expr * expr;
bool resolved() const
{
return expr == nullptr;
}
void resolve(Value v)
{
_env = v.raw;
expr = nullptr;
}
Env * env() const
{
return reinterpret_cast<Env *>(_env);
}
Value result() const
{
Value v;
v.raw = _env;
return v;
}
};
/**
@@ -735,6 +776,7 @@ public:
*/
inline ValueType type(bool invalidIsThunk = false) const
{
again:
switch (internalType()) {
case tInt:
return nInt;
@@ -760,11 +802,19 @@ public:
case Acb::tInt:
return nInt;
}
case tThunk:
return nThunk;
case tApp:
return app().target()->isPrimOp() ? nFunction : nThunk;
case tThunk:
if (thunk().resolved()) {
raw = thunk().result().raw;
goto again;
}
return nThunk;
case tApp:
if (app().resolved()) {
raw = app().result().raw;
goto again;
}
return app().target()->isPrimOp() ? nFunction : nThunk;
}
if (invalidIsThunk)
return nThunk;
else
@@ -921,21 +971,16 @@ public:
return untag<Bindings *>();
}
const auto & thunk() const
Thunk & thunk() const
{
return *untag<const Thunk *>();
return *untag<Thunk *>();
}
App & app()
App & app() const
{
return *untag<App *>();
}
const App & app() const
{
return *untag<const App *>();
}
const auto & lambda() const
{
return *untag<const Lambda *>();