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-10-05 14:56:56 +02:00
parent 57333a0e60
commit b19bbdfee1
4 changed files with 146 additions and 73 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 = &env, .expr = &expr};
raw = tag(tThunk, thunk);
}
@@ -113,22 +113,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
@@ -2154,8 +2154,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
@@ -11,7 +11,7 @@ namespace nix {
ExprBlackHole eBlackHole;
static Env nullEnv;
Value::Thunk Value::blackHole{&nullEnv, &eBlackHole};
Value::Thunk Value::blackHole{{&nullEnv}, &eBlackHole};
// FIXME: remove, because *symbols* are abstract and do not have a single
// textual representation; see printIdentifier()
+110 -54
View File
@@ -245,7 +245,7 @@ struct NewValueAs
struct Value
{
private:
uintptr_t raw;
mutable uintptr_t raw;
public:
static constexpr size_t TAG_BITS = 3;
@@ -613,7 +613,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
@@ -665,13 +665,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
@@ -727,11 +745,6 @@ public:
Env * env;
ExprLambda * fun;
};
struct alignas(TAG_ALIGN) Thunk
{
Env * env;
Expr * expr;
};
/**
* Returns the normal type of a Value. This only returns nThunk if
@@ -740,43 +753,7 @@ public:
* @param invalidIsThunk Instead of aborting an an invalid (probably
* 0, so uninitialized) internal type, return `nThunk`.
*/
inline ValueType type(bool invalidIsThunk = false) const
{
switch (internalType()) {
case tInt:
return nInt;
case tBool:
return nBool;
case tString:
return untag<const String *>()->isPath() ? nPath : nString;
case tAttrs:
return nAttrs;
case tList:
return nList;
case tAuxiliary:
switch (untag<const Acb *>()->type) {
case Acb::tExternal:
return nExternal;
case Acb::tFloat:
return nFloat;
case Acb::tNull:
return nNull;
case Acb::tPrimOp:
case Acb::tLambda:
return nFunction;
case Acb::tInt:
return nInt;
}
case tThunk:
return nThunk;
case tApp:
return app().target()->isPrimOp() ? nFunction : nThunk;
}
if (invalidIsThunk)
return nThunk;
else
abort();
}
inline ValueType type(bool invalidIsThunk = false) const;
inline void mkInt(NixInt::Inner n)
{
@@ -928,21 +905,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 *>();
@@ -972,6 +944,90 @@ public:
}
};
struct alignas(Value::TAG_ALIGN) Value::Thunk
{
union {
Env * _env;
Value _result;
};
Expr * expr;
bool resolved() const
{
return expr == nullptr;
}
void resolve(Value v)
{
_result = v;
expr = nullptr;
}
Env * env() const
{
return _env;
}
Value result() const
{
return _result;
}
};
/**
* Returns the normal type of a Value. This only returns nThunk if
* the Value hasn't been forceValue'd
*
* @param invalidIsThunk Instead of aborting an an invalid (probably
* 0, so uninitialized) internal type, return `nThunk`.
*/
inline ValueType Value::type(bool invalidIsThunk) const
{
again:
switch (internalType()) {
case tInt:
return nInt;
case tBool:
return nBool;
case tString:
return untag<const String *>()->isPath() ? nPath : nString;
case tAttrs:
return nAttrs;
case tList:
return nList;
case tAuxiliary:
switch (untag<const Acb *>()->type) {
case Acb::tExternal:
return nExternal;
case Acb::tFloat:
return nFloat;
case Acb::tNull:
return nNull;
case Acb::tPrimOp:
case Acb::tLambda:
return nFunction;
case Acb::tInt:
return nInt;
}
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
abort();
}
using ValueVector = GcVector<Value *>;
using PrimOp = Value::PrimOp;