Files
lix/lix/libexpr/eval-expr.cc
T
skye 51c6d6a2e8 libexpr: Remove various default constructions of Values
Progress towards #744

Change-Id: I138ecf7ab712ea570ecbf506c7b6f6be6a6a6964
2026-03-17 15:48:12 -04:00

729 lines
24 KiB
C++

#include "eval.hh"
#include "primops.hh"
#include "gc-small-vector.hh"
/// This file contains all implementations of `Expr::eval`, and some other helper functions defined by
/// `Expr` subtypes. Note that some of the evaluation helper functions on `EvalState` that do the heavy
/// lifting are not in this file but kept in `eval.cc`. In the future, more logic from here will be factored
/// out into helpers over at `eval.cc` until this file contains a readable and high-level implementation of
/// the evaluator.
namespace nix {
/* Create a thunk for the delayed computation of the given expression
in the given environment. But if the expression is a variable,
then look it up right away. This significantly reduces the number
of thunks allocated. */
Value Expr::maybeThunk(EvalState & state, Env & env)
{
state.ctx.stats.nrThunks++;
return {NewValueAs::thunk, state.ctx.mem, env, *this};
}
Value ExprVar::maybeThunk(EvalState & state, Env & env)
{
Value * v = state.lookupVar(&env, *this, true);
/* The value might not be initialised in the environment yet.
In that case, ignore it. */
if (v && !v->isInvalid()) {
state.ctx.stats.nrAvoided++;
return *v;
}
return Expr::maybeThunk(state, env);
}
Value ExprLiteral::maybeThunk(EvalState & state, Env & env)
{
state.ctx.stats.nrAvoided++;
return v;
}
Value ExprList::maybeThunk(EvalState & state, Env & env)
{
if (elems.empty()) {
return Value::EMPTY_LIST;
}
return Expr::maybeThunk(state, env);
}
Value Expr::eval(EvalState & state, Env & env)
{
abort();
}
Value ExprLiteral::eval(EvalState & state, Env & env)
{
return this->v;
}
Value ExprInheritFrom::eval(EvalState & state, Env & env)
{
Value & v2 = env.values[displ];
state.forceValue(v2, pos);
return v2;
}
Env * ExprAttrs::buildInheritFromEnv(EvalState & state, Env & up)
{
Env & inheritEnv = state.ctx.mem.allocEnv(inheritFromExprs->size());
inheritEnv.up = &up;
Displacement displ = 0;
for (auto & from : *inheritFromExprs) {
inheritEnv.values[displ++] = from->maybeThunk(state, up);
}
return &inheritEnv;
}
Value ExprSet::eval(EvalState & state, Env & env)
{
Bindings::Size capacity = attrs.size() + dynamicAttrs.size();
Value v = {NewValueAs::attrs, state.ctx.buildBindings(capacity).finish()};
auto dynamicEnv = &env;
if (recursive) {
/* Create a new environment that contains the attributes in
this `rec'. */
Env & env2(state.ctx.mem.allocEnv(attrs.size()));
env2.up = &env;
dynamicEnv = &env2;
Env * inheritEnv = inheritFromExprs ? buildInheritFromEnv(state, env2) : nullptr;
ExprAttrs::AttrDefs::iterator overrides = attrs.find(state.ctx.symbols.sym___overrides);
bool hasOverrides = overrides != attrs.end();
/* The recursive attributes are evaluated in the new
environment, while the inherited attributes are evaluated
in the original environment. */
Displacement displ = 0;
for (auto & i : attrs) {
Value vAttr;
if (hasOverrides && i.second.kind != ExprAttrs::AttrDef::Kind::Inherited) {
vAttr = {
NewValueAs::thunk,
state.ctx.mem,
*i.second.chooseByKind(&env2, &env, inheritEnv),
*i.second.e
};
state.ctx.stats.nrThunks++;
} else {
vAttr = i.second.e->maybeThunk(state, *i.second.chooseByKind(&env2, &env, inheritEnv));
}
env2.values[displ++] = vAttr;
v.attrs()->push_back(Attr(i.first, vAttr, i.second.pos));
}
/* If the rec contains an attribute called `__overrides', then
evaluate it, and add the attributes in that set to the rec.
This allows overriding of recursive attributes, which is
otherwise not possible. (You can use the // operator to
replace an attribute, but other attributes in the rec will
still reference the original value, because that value has
been substituted into the bodies of the other attributes.
Hence we need __overrides.) */
if (hasOverrides) {
Value & vOverrides = (*v.attrs())[overrides->second.displ].value;
state.forceAttrs(vOverrides, noPos, "while evaluating the `__overrides` attribute");
Bindings * newBnds = state.ctx.mem.allocBindings(capacity + vOverrides.attrs()->size());
for (auto & i : *v.attrs()) {
newBnds->push_back(i);
}
for (auto & i : *vOverrides.attrs()) {
ExprAttrs::AttrDefs::iterator j = attrs.find(i.name);
if (j != attrs.end()) {
(*newBnds)[j->second.displ] = i;
env2.values[j->second.displ] = i.value;
} else {
newBnds->push_back(i);
}
}
newBnds->sort();
v = {NewValueAs::attrs, newBnds};
}
}
else {
Env * inheritEnv = inheritFromExprs ? buildInheritFromEnv(state, env) : nullptr;
for (auto & i : attrs) {
v.attrs()->push_back(Attr(
i.first,
i.second.e->maybeThunk(state, *i.second.chooseByKind(&env, &env, inheritEnv)),
i.second.pos
));
}
}
/* Dynamic attrs apply *after* rec and __overrides. */
for (auto & i : dynamicAttrs) {
/* Before evaluating dynamic attrs, we blackhole the output attrset and only restore it after the operation.
* This is to avoid exposing the partially constructed set as a value, see
* http://github.com/NixOS/nix/issues/7012. Any accesses to the output attrset will thus infrec.
*/
Value vBackup = v;
Symbol nameSym;
{
KJ_DEFER(v = vBackup);
v = Value{NewValueAs::blackhole};
Value nameVal = i.nameExpr->eval(state, *dynamicEnv);
state.forceValue(nameVal, i.pos);
if (nameVal.type() == nNull) {
continue;
}
state.forceStringNoCtx(nameVal, i.pos, "while evaluating the name of a dynamic attribute");
nameSym = state.ctx.symbols.create(nameVal.str());
}
auto j = v.attrs()->get(nameSym);
if (j) {
state.ctx.errors
.make<EvalError>(
"dynamic attribute '%1%' already defined at %2%",
state.ctx.symbols[nameSym],
state.ctx.positions[j->pos]
)
.atPos(i.pos)
.withFrame(env, *this)
.debugThrow();
}
i.valueExpr->setName(nameSym);
/* Keep sorted order so find can catch duplicates */
v.attrs()->push_back(Attr(nameSym, i.valueExpr->maybeThunk(state, *dynamicEnv), i.pos));
v.attrs()->sort(); // FIXME: inefficient
}
v.attrs()->pos = pos;
return v;
}
Value ExprLet::eval(EvalState & state, Env & env)
{
/* Create a new environment that contains the attributes in this
`let'. */
Env & env2(state.ctx.mem.allocEnv(attrs.size()));
env2.up = &env;
Env * inheritEnv = inheritFromExprs ? buildInheritFromEnv(state, env2) : nullptr;
/* The recursive attributes are evaluated in the new environment,
while the inherited attributes are evaluated in the original
environment. */
Displacement displ = 0;
for (auto & i : attrs) {
env2.values[displ++] = i.second.e->maybeThunk(state, *i.second.chooseByKind(&env2, &env, inheritEnv));
}
return body->eval(state, env2);
}
Value ExprList::eval(EvalState & state, Env & env)
{
auto result = state.ctx.mem.newList(elems.size());
Value v = {NewValueAs::list, result};
for (auto && [n, v2] : enumerate(result->span())) {
v2 = elems[n]->maybeThunk(state, env);
}
return v;
}
Value ExprVar::eval(EvalState & state, Env & env)
{
Value * v2 = state.lookupVar(&env, *this, false);
try {
state.forceValue(*v2, pos);
} catch (Error & e) {
/* `name` can be invalid if we are an ExprInheritFrom */
if (name) {
e.addTrace(state.ctx.positions[getPos()], "while evaluating %s", state.ctx.symbols[name]);
}
throw;
}
return *v2;
}
Value ExprWith::eval(EvalState & state, Env & env)
{
Env & env2(state.ctx.mem.allocEnv(1));
env2.up = &env;
env2.values[0] = attrs->maybeThunk(state, env);
return body->eval(state, env2);
}
Value ExprIf::eval(EvalState & state, Env & env)
{
Value vCond = cond->eval(state, env);
return (state.checkBool(vCond, env, *cond) ? *then : *else_).eval(state, env);
}
Value ExprAssert::eval(EvalState & state, Env & env)
{
Value vCond = cond->eval(state, env);
if (!state.checkBool(vCond, env, *cond)) {
state.ctx.errors.make<AssertionError>("assertion failed")
.atPos(pos)
.withFrame(env, *this)
.debugThrow();
}
return body->eval(state, env);
}
Value ExprOpNot::eval(EvalState & state, Env & env)
{
Value vInner = e->eval(state, env);
return {NewValueAs::boolean, !state.checkBool(vInner, env, *e)};
}
Value ExprOpEq::eval(EvalState & state, Env & env)
{
Value v1 = e1->eval(state, env);
Value v2 = e2->eval(state, env);
return {NewValueAs::boolean, state.eqValues(v1, v2, pos, "while testing two values for equality")};
}
Value ExprOpNEq::eval(EvalState & state, Env & env)
{
Value v1 = e1->eval(state, env);
Value v2 = e2->eval(state, env);
return {NewValueAs::boolean, !state.eqValues(v1, v2, pos, "while testing two values for inequality")};
}
Value ExprOpAnd::eval(EvalState & state, Env & env)
{
Value v1 = e1->eval(state, env);
/* Explicitly short-circuit */
if (!state.checkBool(v1, env, *e1)) {
return {NewValueAs::boolean, false};
}
Value v2 = e2->eval(state, env);
return {NewValueAs::boolean, state.checkBool(v2, env, *e2)};
}
Value ExprOpOr::eval(EvalState & state, Env & env)
{
Value v1 = e1->eval(state, env);
/* Explicitly short-circuit */
if (state.checkBool(v1, env, *e1)) {
return {NewValueAs::boolean, true};
}
Value v2 = e2->eval(state, env);
return {NewValueAs::boolean, state.checkBool(v2, env, *e2)};
}
Value ExprOpImpl::eval(EvalState & state, Env & env)
{
Value v1 = e1->eval(state, env);
/* Explicitly short-circuit (ex falso quodlibet) */
if (!state.checkBool(v1, env, *e1)) {
return {NewValueAs::boolean, true};
}
Value v2 = e2->eval(state, env);
return {NewValueAs::boolean, state.checkBool(v2, env, *e2)};
}
Value ExprOpUpdate::eval(EvalState & state, Env & env)
{
Value v1 = e1->eval(state, env);
state.checkAttrs(v1, env, *e1);
Value v2 = e2->eval(state, env);
state.checkAttrs(v2, env, *e2);
state.ctx.stats.nrOpUpdates++;
if (v1.attrs()->size() == 0) {
return v2;
}
if (v2.attrs()->size() == 0) {
return v1;
}
auto attrs = state.ctx.buildBindings(v1.attrs()->size() + v2.attrs()->size());
/* Merge the sets, preferring values from the second set. Make
sure to keep the resulting vector in sorted order. */
Bindings::iterator i = v1.attrs()->begin();
Bindings::iterator j = v2.attrs()->begin();
while (i != v1.attrs()->end() && j != v2.attrs()->end()) {
if (i->name == j->name) {
attrs.insert(*j);
++i;
++j;
} else if (i->name < j->name) {
attrs.insert(*i++);
} else {
attrs.insert(*j++);
}
}
while (i != v1.attrs()->end()) {
attrs.insert(*i++);
}
while (j != v2.attrs()->end()) {
attrs.insert(*j++);
}
Value v = {NewValueAs::attrs, attrs.alreadySorted()};
state.ctx.stats.nrOpUpdateValuesCopied += v.attrs()->size();
return v;
}
Value ExprOpConcatLists::eval(EvalState & state, Env & env)
{
state.ctx.stats.nrListConcats++;
/* We don't call into `concatLists` as that loses the position information of the expressions. */
Value v1 = e1->eval(state, env);
state.checkList(v1, env, *e1);
Value v2 = e2->eval(state, env);
state.checkList(v2, env, *e2);
size_t l1 = v1.listSize(), l2 = v2.listSize(), len = l1 + l2;
if (l1 == 0) {
return v2;
} else if (l2 == 0) {
return v1;
} else {
auto list = state.ctx.mem.newList(len);
auto out = list->elems;
std::copy(v1.listElems(), v1.listElems() + l1, out);
std::copy(v2.listElems(), v2.listElems() + l2, out + l1);
return {NewValueAs::list, list};
}
}
Value ExprConcatStrings::eval(EvalState & state, Env & env)
{
NixStringContext context;
std::vector<BackedStringView> s;
size_t sSize = 0;
NixInt n{0};
NixFloat nf = 0;
bool first = !isInterpolation;
ValueType firstType = nString;
const auto str = [&] {
std::string result;
result.reserve(sSize);
for (const auto & part : s) {
result += *part;
}
return result;
};
/* build a gc'd value string directly instead of going through str()
and mkString to save an allocation and copy */
const auto gcStr = [&] {
auto result = Value::Str::gcAlloc(sSize);
char * tmp = result->contents;
for (const auto & part : s) {
memcpy(tmp, part->data(), part->size());
tmp += part->size();
}
return result;
};
// List of returned strings. References to these Values must NOT be persisted.
SmallTemporaryValueVector<conservativeStackReservation> values;
values.reserve(es.size());
for (auto & [i_pos, i] : es) {
values.push_back(i->eval(state, env));
Value & vTmp = values.back();
/* If the first element is a path, then the result will also
be a path, we don't copy anything (yet - that's done later,
since paths are copied when they are used in a derivation),
and none of the strings are allowed to have contexts. */
if (first) {
firstType = vTmp.type();
}
if (firstType == nInt) {
if (vTmp.type() == nInt) {
auto newN = n + vTmp.integer();
if (auto checked = newN.valueChecked(); checked.has_value()) {
n = NixInt(*checked);
} else {
state.ctx.errors
.make<EvalError>("integer overflow in adding %1% + %2%", n, vTmp.integer())
.atPos(i_pos)
.debugThrow();
}
} else if (vTmp.type() == nFloat) {
// Upgrade the type from int to float;
firstType = nFloat;
nf = n.value;
nf += vTmp.fpoint();
} else {
state.ctx.errors.make<EvalError>("cannot add %1% to an integer", showType(vTmp))
.atPos(i_pos)
.withFrame(env, *this)
.debugThrow();
}
} else if (firstType == nFloat) {
if (vTmp.type() == nInt) {
nf += vTmp.integer().value;
} else if (vTmp.type() == nFloat) {
nf += vTmp.fpoint();
} else {
state.ctx.errors.make<EvalError>("cannot add %1% to a float", showType(vTmp))
.atPos(i_pos)
.withFrame(env, *this)
.debugThrow();
}
} else {
if (s.empty()) {
s.reserve(es.size());
}
/* If we are coercing inside of an interpolation, we may allow slightly more comfort by coercing
* things like integers. */
auto coercionMode = isInterpolation && featureSettings.isEnabled(Xp::CoerceIntegers)
? StringCoercionMode::Interpolation
: StringCoercionMode::Strict;
/* skip canonization of first path, which would only be not
canonized in the first place if it's coming from a ./${foo} type
path */
auto part = state.coerceToString(
i_pos,
vTmp,
context,
"while evaluating a path segment",
coercionMode,
firstType == nString,
!first
);
sSize += part->size();
s.emplace_back(std::move(part));
}
first = false;
}
if (firstType == nInt) {
return {NewValueAs::integer, n};
} else if (firstType == nFloat) {
return {NewValueAs::floating, nf};
} else if (firstType == nPath) {
if (!context.empty()) {
state.ctx.errors
.make<EvalError>("a string that refers to a store path cannot be appended to a path")
.atPos(pos)
.withFrame(env, *this)
.debugThrow();
}
return {NewValueAs::path, CanonPath(canonPath(str()))};
} else {
return {NewValueAs::string, gcStr(), context};
}
}
Value ExprPos::eval(EvalState & state, Env & env)
{
return state.mkPos(pos);
}
Value ExprBlackHole::eval(EvalState & state, Env & env)
{
state.ctx.errors.make<InfiniteRecursionError>("infinite recursion encountered").debugThrow();
}
Value ExprDebugFrame::eval(EvalState & state, Env & env)
{
auto dts = makeDebugTraceStacker(state, *inner, env, state.ctx.positions[pos], message);
return inner->eval(state, env);
}
/** Returns `nullptr` if we should be using a default instead. */
Attr const *
ExprSelect::selectSingleAttr(EvalState & state, Env & env, AttrName const & attrName, Value & vCurrent)
{
Symbol const attrSym = getName(attrName, state, env);
try {
state.forceValue(vCurrent, pos);
} catch (Error & e) {
// clang-format off
e.addTrace(state.ctx.positions[attrName.pos], HintFmt(
"while evaluating an expression to select '%s' on it", state.ctx.symbols[attrSym]
));
// clang-format on
throw;
}
if (vCurrent.type() != nAttrs) {
// If we have an `or` provided default, then it doesn't have to be an attrset.
// Let the caller know there's no attr value here.
if (def != nullptr) {
return nullptr;
}
// Otherwise, we must type error.
// clang-format off
state.ctx.errors.make<TypeError>(
"expected a set but found %s: %s",
showType(vCurrent),
ValuePrinter(state, vCurrent, errorPrintOptions)
).addTrace(
attrName.pos,
HintFmt("while selecting '%s'", state.ctx.symbols[attrSym])
).debugThrow();
// clang-format on
}
// Now that we know it's an attrset, we can actually look for the name.
auto const attrIt = vCurrent.attrs()->get(attrSym);
if (!attrIt) {
// Again if we have an `or` provided default, then missing attr is not an error.
if (def != nullptr) {
return nullptr;
}
// Otherwise, we collect all attr names and throw an attr missing error.
std::set<std::string> const allAttrNames = *vCurrent.attrs()
| std::views::transform([&state](auto const & attr) {
return std::string{state.ctx.symbols[attr.name]};
})
| std::ranges::to<std::set>();
auto suggestions = Suggestions::bestMatches(allAttrNames, state.ctx.symbols[attrSym]);
state.ctx.errors.make<EvalError>("attribute '%s' missing", state.ctx.symbols[attrSym])
.atPos(attrName.pos)
.withSuggestions(suggestions)
.withFrame(env, *this)
.debugThrow();
}
// If we made it here, then we successfully found the attribute.
// Return it to our caller!
return attrIt;
}
Value ExprSelect::eval(EvalState & state, Env & env)
{
// Position for the current attrset Value in this select chain.
PosIdx posCurrent;
// Position for the current selector in this select chain.
PosIdx posCurrentSyntax;
Value baseSelectee = [&]() {
try {
// Evaluate the original thing we're selecting on.
return e->eval(state, env);
} catch (Error & e) {
// clang-format off
e.addTrace(state.ctx.positions[getPos()], HintFmt(
"while evaluating an expression to select '%s' on it",
showAttrPath(state.ctx.symbols, attrPath)
));
// clang-format on
throw;
}
}();
try {
// With the original selectee evaluated, we'll walk the selection path starting
// with the evaluated original selectee.
std::reference_wrapper<Value> curSelectee = std::ref(baseSelectee);
for (AttrName const & attrName : attrPath) {
state.ctx.stats.nrLookups++;
// Select `attrName` on `curSelectee`.
auto const attr = selectSingleAttr(state, env, attrName, curSelectee.get());
if (!attr) {
// Use default.
try {
return this->def->eval(state, env);
} catch (Error & err) {
err.addTrace(
state.ctx.positions[this->def->pos],
"while evaluating fallback for missing attribute '%s'",
state.ctx.symbols[getName(attrName, state, env)]
);
throw;
}
}
// The selection worked. If we have another iteration, then we use `attr->value`
// as the thing to select on. If this is the last iteration, then `attr->value`
// is the final value this ExprSelect evaluated to.
curSelectee = std::ref(attr->value);
posCurrent = attr->pos;
posCurrentSyntax = attrName.pos;
if (state.ctx.stats.countCalls) {
state.ctx.stats.attrSelects[posCurrent]++;
}
}
state.forceValue(curSelectee.get(), posCurrent ? posCurrent : posCurrentSyntax);
return curSelectee.get();
} catch (Error & err) {
auto const & lastPos = state.ctx.positions[posCurrent];
if (lastPos && !std::get_if<Pos::Hidden>(&lastPos.origin)) {
err.addTrace(lastPos, "while evaluating the attribute '%s'", showAttrPath(state, env, attrPath));
}
throw;
}
}
Value ExprOpHasAttr::eval(EvalState & state, Env & env)
{
Value vTmp = e->eval(state, env);
Value * vAttrs = &vTmp;
for (auto & i : attrPath) {
state.forceValue(*vAttrs, getPos());
const Attr * j;
auto name = getName(i, state, env);
if (vAttrs->type() != nAttrs || (j = vAttrs->attrs()->get(name)) == nullptr) {
return {NewValueAs::boolean, false};
} else {
vAttrs = &j->value;
}
}
return {NewValueAs::boolean, true};
}
Value ExprLambda::eval(EvalState & state, Env & env)
{
return {NewValueAs::lambda, state.ctx.mem, env, *this};
}
Value ExprCall::eval(EvalState & state, Env & env)
{
Value vFun = fun->eval(state, env);
// Empirical arity of Nixpkgs lambdas by regex e.g. ([a-zA-Z]+:(\s|(/\*.*\/)|(#.*\n))*){5}
// 2: over 4000
// 3: about 300
// 4: about 60
// 5: under 10
// This excluded attrset lambdas (`{...}:`). Contributions of mixed lambdas appears insignificant at ~150
// total.
SmallValueVector<4> vArgs;
vArgs.reserve(args.size());
for (size_t i = 0; i < args.size(); ++i) {
vArgs.push_back(args[i]->maybeThunk(state, env));
}
return state.callFunction(vFun, vArgs, pos);
}
}