libexpr: tag Value::Acb

Value is already tagged. Value::Acb blocks are allocated for lambdas (so
we can fit the value tag into the three bits we have available), but the
current layout is rather wasteful for this purpose. the type bits can be
stored together with parts of pointers, which in the lambda case will be
the scope the lambda captures. the expr could also be used, but Env is a
gc-allocated item and thus guaranteed to be aligned properly for tagging

Change-Id: Ia685875387c7795bc4a00d73d1ce3cfea84e7297
This commit is contained in:
eldritch horrors
2025-10-05 14:56:56 +02:00
parent b19bbdfee1
commit 8108e8a760
5 changed files with 73 additions and 27 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ inline Value::Value(thunk_t, EvalMemory & mem, Env & env, Expr & expr)
inline Value::Value(lambda_t, EvalMemory & mem, Env & env, ExprLambda & lambda)
{
auto lp = mem.allocType<Lambda>();
*lp = Lambda{{Acb::tLambda}, &env, &lambda};
new (lp) Lambda{env, lambda};
raw = tag(tAuxiliary, lp);
}
+11 -5
View File
@@ -118,7 +118,7 @@ std::string showType(const Value & v)
case tAuxiliary:
#pragma GCC diagnostic push
#pragma GCC diagnostic error "-Wswitch-enum"
switch (v.auxiliary()->type) {
switch (v.auxiliary()->type()) {
case Value::Acb::tExternal:
return v.external()->showType();
case Value::Acb::tFloat:
@@ -1624,7 +1624,7 @@ void EvalState::callFunction(Value & fun, std::span<Value *> args, Value & vRes,
ExprLambda & lambda(*vCur.lambda().fun);
Env & env2 = lambda.pattern->match(lambda, *this, *vCur.lambda().env, args[0], pos);
Env & env2 = lambda.pattern->match(lambda, *this, *vCur.lambda().env(), args[0], pos);
ctx.stats.nrFunctionCalls++;
if (ctx.stats.countCalls) ctx.stats.addCall(lambda);
@@ -1821,12 +1821,18 @@ void EvalState::autoCallFunction(Bindings & args, Value & fun, Value & res, PosI
if (j) {
attrs.insert(*j);
} else if (!i.def) {
ctx.errors.make<MissingArgumentError>(R"(cannot evaluate a function that has an argument without a value ('%1%')
ctx.errors
.make<MissingArgumentError>(
R"(cannot evaluate a function that has an argument without a value ('%1%')
Lix attempted to evaluate a function as a top level expression; in
this case it must have its arguments supplied either by default
values, or passed explicitly with '--arg' or '--argstr'. See
https://docs.lix.systems/manual/lix/stable/language/constructs.html#functions)", ctx.symbols[i.name])
.atPos(i.pos).withFrame(*fun.lambda().env, *fun.lambda().fun).debugThrow();
https://docs.lix.systems/manual/lix/stable/language/constructs.html#functions)",
ctx.symbols[i.name]
)
.atPos(i.pos)
.withFrame(*fun.lambda().env(), *fun.lambda().fun)
.debugThrow();
}
}
}
+1 -1
View File
@@ -63,7 +63,7 @@ struct Constant
using ValMap = GcMap<std::string, Value *>;
struct Env
struct alignas(Value::Acb::TAG_ALIGN) Env
{
Env * up;
Value * values[0];
+7
View File
@@ -20,6 +20,13 @@ static_assert(alignof(Value::List) >= Value::TAG_ALIGN);
static_assert(alignof(Value::Thunk) >= Value::TAG_ALIGN);
static_assert(alignof(Value::App) >= Value::TAG_ALIGN);
static_assert(alignof(Value::External) >= Value::Acb::TAG_ALIGN);
static_assert(alignof(Value::Float) >= Value::Acb::TAG_ALIGN);
static_assert(alignof(Value::Null) >= Value::Acb::TAG_ALIGN);
static_assert(alignof(Value::PrimOp) >= Value::Acb::TAG_ALIGN);
static_assert(alignof(Value::Int) >= Value::Acb::TAG_ALIGN);
static_assert(alignof(Value::Lambda) >= Value::Acb::TAG_ALIGN);
static void copyContextToValue(Value::String & s, const NixStringContext & context)
{
if (!context.empty()) {
+53 -20
View File
@@ -348,7 +348,7 @@ public:
raw = tInt | (uintptr_t(i.value) << TAG_BITS);
} else {
auto ip = gcAllocType<Int>();
ip->type = Acb::tInt;
ip->raw = Acb::tInt;
ip->value = i;
raw = tag(tAuxiliary, ip);
}
@@ -359,7 +359,7 @@ public:
Value(floating_t, NixFloat f)
{
auto fp = gcAllocType<Float>();
fp->type = Acb::tFloat;
fp->raw = Acb::tFloat;
fp->value = f;
raw = tag(tAuxiliary, fp);
}
@@ -540,7 +540,7 @@ public:
Value(external_t, ExternalValueBase & external)
{
auto ext = gcAllocType<External>();
ext->type = Acb::tExternal;
ext->raw = Acb::tExternal;
ext->external = &external;
raw = tag(tAuxiliary, ext);
}
@@ -605,11 +605,11 @@ public:
// type() == nFunction
inline bool isLambda() const
{
return internalType() == tAuxiliary && auxiliary()->type == Acb::tLambda;
return internalType() == tAuxiliary && auxiliary()->type() == Acb::tLambda;
};
inline bool isPrimOp() const
{
return internalType() == tAuxiliary && auxiliary()->type == Acb::tPrimOp;
return internalType() == tAuxiliary && auxiliary()->type() == Acb::tPrimOp;
}
inline bool isPrimOpApp() const
{
@@ -712,14 +712,48 @@ public:
/// these blocks are usually heap-allocated in GC memory space.
struct alignas(TAG_ALIGN) Acb
{
enum {
// NOTE value.cc contains alignment assertions for pointers tagged thusly.
// *always* ensure that these assertions match the tag types declared here
enum Type {
tExternal,
tFloat,
tNull,
tPrimOp,
tLambda,
tInt,
} type;
};
uintptr_t raw;
static constexpr size_t TAG_BITS = 3;
static constexpr size_t TAG_ALIGN = 1 << TAG_BITS;
static constexpr uintptr_t TAG_MASK = (1 << TAG_BITS) - 1;
static uintptr_t tag(Type t, auto v)
{
if constexpr (std::is_null_pointer_v<decltype(v)>) {
return t;
} else if constexpr (std::is_pointer_v<decltype(v)>) {
return (reinterpret_cast<uintptr_t>(v)) | t;
} else {
return (static_cast<uintptr_t>(v) << TAG_BITS) | t;
}
}
template<typename T>
T untag() const
{
if constexpr (std::is_pointer_v<T>) {
return reinterpret_cast<T>(raw & ~TAG_MASK);
} else {
return static_cast<T>((raw & ~TAG_MASK) >> TAG_BITS);
}
}
Type type() const
{
return Type(raw & TAG_MASK);
}
};
struct External : Acb
{
@@ -742,8 +776,14 @@ public:
struct Lambda : Acb
{
Env * env;
ExprLambda * fun;
Lambda(Env & env, ExprLambda & fun) : Acb{tag(tLambda, &env)}, fun(&fun) {}
Env * env() const
{
return untag<Env *>();
}
};
/**
@@ -885,7 +925,7 @@ public:
memcpy(&tmp, &raw, sizeof(tmp));
return NixInt(tmp >> 3);
} else {
assert(internalType() == tAuxiliary && untag<const Acb *>()->type == Acb::tInt);
assert(internalType() == tAuxiliary && untag<const Acb *>()->type() == Acb::tInt);
return untag<const Int *>()->value;
}
}
@@ -922,19 +962,19 @@ public:
const PrimOp * primOp() const
{
assert(internalType() == tAuxiliary && untag<const Acb *>()->type == Acb::tPrimOp);
assert(internalType() == tAuxiliary && untag<const Acb *>()->type() == Acb::tPrimOp);
return untag<const PrimOp *>();
}
const ExternalValueBase * external() const
{
assert(internalType() == tAuxiliary && untag<const Acb *>()->type == Acb::tExternal);
assert(internalType() == tAuxiliary && untag<const Acb *>()->type() == Acb::tExternal);
return untag<const External *>()->external;
}
NixFloat fpoint() const
{
assert(internalType() == tAuxiliary && untag<const Acb *>()->type == Acb::tFloat);
assert(internalType() == tAuxiliary && untag<const Acb *>()->type() == Acb::tFloat);
return untag<const Float *>()->value;
}
@@ -974,13 +1014,6 @@ struct alignas(Value::TAG_ALIGN) Value::Thunk
}
};
/**
* 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:
@@ -996,7 +1029,7 @@ again:
case tList:
return nList;
case tAuxiliary:
switch (untag<const Acb *>()->type) {
switch (untag<const Acb *>()->type()) {
case Acb::tExternal:
return nExternal;
case Acb::tFloat: