diff --git a/lix/libexpr/eval.cc b/lix/libexpr/eval.cc index e26c4bf8e..52d8406e2 100644 --- a/lix/libexpr/eval.cc +++ b/lix/libexpr/eval.cc @@ -39,6 +39,7 @@ #include #include #include +#include #include #include @@ -1303,113 +1304,140 @@ static std::string showAttrPath(EvalState & state, Env & env, const AttrPath & a return out.str(); } +/** 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( + "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 const allAttrNames = *vCurrent.attrs() + | std::views::transform([&state](auto const & attr) { + return std::string{state.ctx.symbols[attr.name]}; + }) + | std::ranges::to(); + + auto suggestions = Suggestions::bestMatches(allAttrNames, state.ctx.symbols[attrSym]); + state.ctx.errors.make("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; +} void ExprSelect::eval(EvalState & state, Env & env, Value & v) { - Value vFirst; - - // Pointer to the current attrset Value in this select chain. - Value * vCurrent = &vFirst; // 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 { - e->eval(state, env, vFirst); + // Evaluate the original thing we're selecting on. + e->eval(state, env, baseSelectee); } catch (Error & e) { - assert(this->e != nullptr); - e.addTrace( - state.ctx.positions[getPos()], + // clang-format off + e.addTrace(state.ctx.positions[getPos()], HintFmt( "while evaluating an expression to select '%s' on it", - showAttrPath(state.ctx.symbols, this->attrPath) - ); + showAttrPath(state.ctx.symbols, attrPath) + )); + // clang-format on throw; } try { - for (auto const & [partIdx, currentAttrName] : enumerate(attrPath)) { + // With the original selectee evaluated, we'll walk the selection path starting + // with the evaluated original selectee. + std::reference_wrapper curSelectee = std::ref(baseSelectee); + for (AttrName const & attrName : attrPath) { state.ctx.stats.nrLookups++; - Symbol const name = getName(currentAttrName, state, env); - - try { - state.forceValue(*vCurrent, pos); - } catch (Error & e) { - e.addTrace( - state.ctx.positions[currentAttrName.pos], - "while evaluating an expression to select '%s' on it", - state.ctx.symbols[name] - ); - throw; + // Select `attrName` on `curSelectee`. + auto const attr = selectSingleAttr(state, env, attrName, curSelectee.get()); + if (!attr) { + // Use default. + this->def->eval(state, env, v); + return; } - if (vCurrent->type() != nAttrs) { + // 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); - // If we have an `or` provided default, - // then this is allowed to not be an attrset. - if (def != nullptr) { - this->def->eval(state, env, v); - return; - } - - // Otherwise, we must type error. - state.ctx.errors.make( - "expected a set but found %s: %s", - showType(*vCurrent), - ValuePrinter(state, *vCurrent, errorPrintOptions) - ).addTrace( - currentAttrName.pos, - HintFmt("while selecting '%s'", state.ctx.symbols[name]) - ).debugThrow(); + posCurrent = attr->pos; + posCurrentSyntax = attrName.pos; + if (state.ctx.stats.countCalls) { + state.ctx.stats.attrSelects[posCurrent]++; } - - // Now that we know this is actually an attrset, try to find an attr - // with the selected name. - auto attrIt = vCurrent->attrs()->get(name); - if (!attrIt) { - - // If we have an `or` provided default, then we'll use that. - if (def != nullptr) { - this->def->eval(state, env, v); - return; - } - - // Otherwise, missing attr error. - std::set allAttrNames; - for (auto const & attr : *vCurrent->attrs()) { - allAttrNames.emplace(state.ctx.symbols[attr.name]); - } - auto suggestions = Suggestions::bestMatches(allAttrNames, state.ctx.symbols[name]); - state.ctx.errors.make("attribute '%s' missing", state.ctx.symbols[name]) - .atPos(currentAttrName.pos) - .withSuggestions(suggestions) - .withFrame(env, *this) - .debugThrow(); - } - - // If we're here, then we successfully found the attribute. - // Set our currently operated-on attrset to this one, and keep going. - vCurrent = &attrIt->value; - posCurrent = attrIt->pos; - posCurrentSyntax = currentAttrName.pos; - if (state.ctx.stats.countCalls) state.ctx.stats.attrSelects[posCurrent]++; } - state.forceValue(*vCurrent, (posCurrent ? posCurrent : posCurrentSyntax)); + state.forceValue(curSelectee.get(), posCurrent ? posCurrent : posCurrentSyntax); + + v = curSelectee.get(); + + } catch (Error & err) { + auto const & lastPos = state.ctx.positions[posCurrent]; + if (lastPos && !std::get_if(&lastPos.origin)) { + err.addTrace( + lastPos, "while evaluating the attribute '%s'", showAttrPath(state, env, attrPath) + ); + } - } catch (Error & e) { - auto pos2r = state.ctx.positions[posCurrent]; - if (pos2r && !std::get_if(&pos2r.origin)) - e.addTrace(pos2r, "while evaluating the attribute '%1%'", - showAttrPath(state, env, attrPath)); throw; } - - v = *vCurrent; } - void ExprOpHasAttr::eval(EvalState & state, Env & env, Value & v) { Value vTmp; diff --git a/lix/libexpr/nixexpr.hh b/lix/libexpr/nixexpr.hh index 8cfccfeaf..fc258e192 100644 --- a/lix/libexpr/nixexpr.hh +++ b/lix/libexpr/nixexpr.hh @@ -289,6 +289,8 @@ struct ExprInheritFrom : Expr void accept(ExprVisitor & ev, std::unique_ptr & ptr) override { ev.visit(*this, ptr); } }; +struct Attr; + struct ExprSelect : Expr { /** The expression attributes are being selected on. e.g. `foo` in `foo.bar.baz`. */ @@ -307,6 +309,9 @@ struct ExprSelect : Expr JSON toJSON(const SymbolTable & symbols) const override; void eval(EvalState & state, Env & env, Value & v) override; void accept(ExprVisitor & ev, std::unique_ptr & ptr) override { ev.visit(*this, ptr); } + +private: + Attr const * selectSingleAttr(EvalState & state, Env & env, AttrName const & attrName, Value & vCurrent); }; struct ExprOpHasAttr : Expr