diff --git a/src/libexpr/eval-inline.hh b/src/libexpr/eval-inline.hh index 30f6ec7db2e..d539e8119bf 100644 --- a/src/libexpr/eval-inline.hh +++ b/src/libexpr/eval-inline.hh @@ -29,16 +29,21 @@ LocalNoInlineNoReturn(void throwTypeError(const Pos & pos, const char * s, const }); } - -void EvalState::forceValue(Value & v, const Pos & pos) +void EvalState::evalValueWithStrategy(Value & v, EvalStrategy & strat, const Pos & pos) { if (v.type == tThunk) { Env * env = v.thunk.env; Expr * expr = v.thunk.expr; try { + // tBlackhole indicates that any further forcing of this value should throw inf rec + // However, this only causes inf rec when the forcing happens *before* the value is assigned its final value + // Meaning that within the expr->eval you'd have to do `; v.type = ` + // However, if expressions implement their own infinite recursion check (like ExprOpUpdate!), it can do + // `v.type = ; ` + // Which means that the infinite recursion detection from this forceValue is prevented, since the tBlackhole is unset before the potentially recursive evaluations v.type = tBlackhole; //checkInterrupt(); - expr->eval(*this, *env, v); + expr->evalWithStrategy(*this, *env, v, strat); } catch (...) { v.type = tThunk; v.thunk.env = env; @@ -46,12 +51,30 @@ void EvalState::forceValue(Value & v, const Pos & pos) throw; } } + else if (v.type == tAttrs) + strat.handleAttrs(*this, v); + else if (v.type == tLazyUpdate || v.type == tLazyUpdateLeftBlackhole) + // TODO: positions are not as precise as they could be + reevalLazyUpdateWithStrategy(v, strat, pos, pos); else if (v.type == tApp) - callFunction(*v.app.left, *v.app.right, v, noPos); + callFunctionWithStrategy(*v.app.left, *v.app.right, v, strat, pos); else if (v.type == tBlackhole) - throwEvalError(pos, "infinite recursion encountered"); + throwEvalError(pos, "infinite recursion encountered (tBlackhole in forceValue)"); } +void EvalState::forceValue(Value & v, const Pos & pos) +{ + evalValueWithStrategy(v, ForceEvalStrategy::getInstance(), pos); +} + +Attr * EvalState::evalValueAttr(Value & v, const Symbol & name, const Pos & pos) +{ + // No need to set tBlackhole's here, because evaluating attributes of values doesn't require evaluation, and inf rec within lazyBinOps is handled by them directly + + auto strat = AttrEvalStrategy(name); + evalValueWithStrategy(v, strat, pos); + return strat.getAttr(); +} inline void EvalState::forceAttrs(Value & v) { diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index d6366050c7f..7a1b358588e 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -119,6 +119,7 @@ static void printValue(std::ostream & str, std::set & active, con break; case tThunk: case tApp: + case tLazyUpdate: str << ""; break; case tLambda: @@ -180,6 +181,8 @@ string showType(ValueType type) case tPrimOpApp: return "a partially applied built-in function"; case tExternal: return "an external value"; case tFloat: return "a float"; + case tLazyUpdate: return "a lazy attribute update"; + case tLazyUpdateLeftBlackhole: return "a lazy attribute update with left being a blackhole"; } abort(); } @@ -509,7 +512,7 @@ Value * EvalState::addConstant(const string & name, Value & v) Value * EvalState::addPrimOp(const string & name, - size_t arity, PrimOpFun primOp) + size_t arity, PrimOpForceFun primOp) { auto name2 = string(name, 0, 2) == "__" ? string(name, 2) : name; Symbol sym = symbols.create(name2); @@ -731,6 +734,25 @@ inline Value * EvalState::lookupVar(Env * env, const ExprVar & var, bool noEval) } +void ForceEvalStrategy::handleAttrs(EvalState & state, Value & v) +{ + // The force eval strategy is never "done" early, since it needs + // to fully evaluate the Value +} + +void AttrEvalStrategy::handleAttrs(EvalState & state, Value & v) +{ + attr = v.attrs->find(name); + if (attr == v.attrs->end()) { + attr = nullptr; + // We haven't found the attribute value yet, so this evaluation strategy + // isn't "done" yet + } else { + // We did find the attribute value, exit early + done = true; + } +} + std::atomic nrValuesFreed{0}; void finalizeValue(void * obj, void * data) @@ -746,7 +768,6 @@ Value * EvalState::allocValue() return v; } - Env & EvalState::allocEnv(size_t size) { nrEnvs++; @@ -817,6 +838,10 @@ Value * Expr::maybeThunk(EvalState & state, Env & env) return v; } +void Expr::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) +{ + abort(); +} unsigned long nrAvoided = 0; @@ -854,19 +879,25 @@ Value * ExprPath::maybeThunk(EvalState & state, Env & env) return &v; } - void EvalState::evalFile(const Path & path_, Value & v, bool mustBeTrivial) +{ + evalFileWithStrategy(path_, v, ForceEvalStrategy::getInstance(), mustBeTrivial); +} + +void EvalState::evalFileWithStrategy(const Path & path_, Value & v, EvalStrategy & strat, bool mustBeTrivial) { auto path = checkSourcePath(path_); FileEvalCache::iterator i; if ((i = fileEvalCache.find(path)) != fileEvalCache.end()) { + evalValueWithStrategy(i->second, strat, noPos); v = i->second; return; } Path path2 = resolveExprPath(path); if ((i = fileEvalCache.find(path2)) != fileEvalCache.end()) { + evalValueWithStrategy(i->second, strat, noPos); v = i->second; return; } @@ -889,7 +920,8 @@ void EvalState::evalFile(const Path & path_, Value & v, bool mustBeTrivial) if (mustBeTrivial && !(dynamic_cast(e))) throw Error("file '%s' must be an attribute set", path); - eval(e, v); + + evalWithStrategy(e, v, strat); } catch (Error & e) { addErrorTrace(e, "while evaluating the file '%1%':", path2); throw; @@ -906,10 +938,14 @@ void EvalState::resetFileCache() fileParseCache.clear(); } +void EvalState::evalWithStrategy(Expr * e, Value & v, EvalStrategy & strat) +{ + e->evalWithStrategy(*this, baseEnv, v, strat); +} void EvalState::eval(Expr * e, Value & v) { - e->eval(*this, baseEnv, v); + evalWithStrategy(e, v, ForceEvalStrategy::getInstance()); } @@ -943,34 +979,38 @@ inline void EvalState::evalAttrs(Env & env, Expr * e, Value & v) void Expr::eval(EvalState & state, Env & env, Value & v) { - abort(); + evalWithStrategy(state, env, v, ForceEvalStrategy::getInstance()); } -void ExprInt::eval(EvalState & state, Env & env, Value & v) +Attr * Expr::evalAttr(EvalState & state, Env & env, Value & v, const Symbol & name) { - v = this->v; + auto strat = AttrEvalStrategy(name); + evalWithStrategy(state, env, v, strat); + return strat.getAttr(); } - -void ExprFloat::eval(EvalState & state, Env & env, Value & v) +void ExprInt::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { v = this->v; } -void ExprString::eval(EvalState & state, Env & env, Value & v) +void ExprFloat::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { v = this->v; } - -void ExprPath::eval(EvalState & state, Env & env, Value & v) +void ExprString::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { v = this->v; } +void ExprPath::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) +{ + v = this->v; +} -void ExprAttrs::eval(EvalState & state, Env & env, Value & v) +void ExprAttrs::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { state.mkAttrs(v, attrs.size() + dynamicAttrs.size()); Env *dynamicEnv = &env; @@ -1049,10 +1089,11 @@ void ExprAttrs::eval(EvalState & state, Env & env, Value & v) v.attrs->push_back(Attr(nameSym, i.valueExpr->maybeThunk(state, *dynamicEnv), &i.pos)); v.attrs->sort(); // FIXME: inefficient } + strat.handleAttrs(state, v); } -void ExprLet::eval(EvalState & state, Env & env, Value & v) +void ExprLet::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { /* Create a new environment that contains the attributes in this `let'. */ @@ -1066,26 +1107,23 @@ void ExprLet::eval(EvalState & state, Env & env, Value & v) for (auto & i : attrs->attrs) env2.values[displ++] = i.second.e->maybeThunk(state, i.second.inherited ? env : env2); - body->eval(state, env2, v); + body->evalWithStrategy(state, env2, v, strat); } - -void ExprList::eval(EvalState & state, Env & env, Value & v) +void ExprList::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { state.mkList(v, elems.size()); for (size_t n = 0; n < elems.size(); ++n) v.listElems()[n] = elems[n]->maybeThunk(state, env); } - -void ExprVar::eval(EvalState & state, Env & env, Value & v) +void ExprVar::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { Value * v2 = state.lookupVar(&env, *this, false); - state.forceValue(*v2, pos); + state.evalValueWithStrategy(*v2, strat, pos); v = *v2; } - static string showAttrPath(EvalState & state, Env & env, const AttrPath & attrPath) { std::ostringstream out; @@ -1105,40 +1143,62 @@ static string showAttrPath(EvalState & state, Env & env, const AttrPath & attrPa unsigned long nrLookups = 0; -void ExprSelect::eval(EvalState & state, Env & env, Value & v) +void ExprSelect::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { - Value vTmp; + // TODO: Probably use pos2 throughout this function Pos * pos2 = 0; - Value * vAttrs = &vTmp; - e->eval(state, env, vTmp); + Value vTmp; + Value * vAttrs = &vTmp; try { - for (auto & i : attrPath) { - nrLookups++; - Bindings::iterator j; - Symbol name = getName(i, state, env); + auto i = attrPath.begin(); + + // In the first loop iteration, we evaluate an attribute of an Expr, and not a Value + // So by unrolling this, we can avoid a thunk allocation by using e->evalAttr instead of evalValueAttr + nrLookups++; + Symbol name = getName(*i, state, env); + Attr * attr = e->evalAttr(state, env, vTmp, name); + if (!attr) { if (def) { - state.forceValue(*vAttrs, pos); - if (vAttrs->type != tAttrs || - (j = vAttrs->attrs->find(name)) == vAttrs->attrs->end()) - { - def->eval(state, env, v); - return; - } + return def->evalWithStrategy(state, env, v, strat); } else { + // Depending on the reason of j being null we throw an error + // If it wasn't an attribute set, this should trigger state.forceAttrs(*vAttrs, pos); - if ((j = vAttrs->attrs->find(name)) == vAttrs->attrs->end()) + // Otherwise the attribute set will have missed the name we wanted + throwEvalError(pos, "attribute '%1%' missing", name); + } + } + vAttrs = attr->value; + pos2 = attr->pos; + if (state.countCalls && pos2) state.attrSelects[*pos2]++; + + ++i; + + + + for (;i < attrPath.end(); ++i) { + nrLookups++; + Symbol name = getName(*i, state, env); + Attr * attr = state.evalValueAttr(*vAttrs, name, pos); + if (!attr) { + if (def) { + return def->evalWithStrategy(state, env, v, strat); + } else { + // Depending on the reason of j being null we throw an error + // If it wasn't an attribute set, this should trigger + state.forceAttrs(*vAttrs, pos); + // Otherwise the attribute set will have missed the name we wanted throwEvalError(pos, "attribute '%1%' missing", name); + } } - vAttrs = j->value; - pos2 = j->pos; + vAttrs = attr->value; + pos2 = attr->pos; if (state.countCalls && pos2) state.attrSelects[*pos2]++; } - state.forceValue(*vAttrs, ( pos2 != NULL ? *pos2 : this->pos ) ); - } catch (Error & e) { if (pos2 && pos2->file != state.sDerivationNix) addErrorTrace(e, *pos2, "while evaluating the attribute '%1%'", @@ -1146,11 +1206,13 @@ void ExprSelect::eval(EvalState & state, Env & env, Value & v) throw; } + // TODO: Pass pos + state.evalValueWithStrategy(*vAttrs, strat, pos2 != NULL ? *pos2 : pos); v = *vAttrs; } - -void ExprOpHasAttr::eval(EvalState & state, Env & env, Value & v) +// TODO: Use evalValueAttr to make this lazy, like ExprSelect +void ExprOpHasAttr::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { Value vTmp; Value * vAttrs = &vTmp; @@ -1174,25 +1236,22 @@ void ExprOpHasAttr::eval(EvalState & state, Env & env, Value & v) mkBool(v, true); } - -void ExprLambda::eval(EvalState & state, Env & env, Value & v) +void ExprLambda::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { v.type = tLambda; v.lambda.env = &env; v.lambda.fun = this; } - -void ExprApp::eval(EvalState & state, Env & env, Value & v) +void ExprApp::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { /* FIXME: vFun prevents GCC from doing tail call optimisation. */ Value vFun; e1->eval(state, env, vFun); - state.callFunction(vFun, *(e2->maybeThunk(state, env)), v, pos); + state.callFunctionWithStrategy(vFun, *(e2->maybeThunk(state, env)), v, strat, pos); } - -void EvalState::callPrimOp(Value & fun, Value & arg, Value & v, const Pos & pos) +void EvalState::callPrimOp(Value & fun, Value & arg, Value & v, EvalStrategy & strat, const Pos & pos) { /* Figure out the number of arguments still needed. */ size_t argsDone = 0; @@ -1218,7 +1277,14 @@ void EvalState::callPrimOp(Value & fun, Value & arg, Value & v, const Pos & pos) /* And call the primop. */ nrPrimOpCalls++; if (countCalls) primOpCalls[primOp->primOp->name]++; - primOp->primOp->fun(*this, pos, vArgs, v); + if (primOp->primOp->fun) { + primOp->primOp->fun(*this, pos, vArgs, v); + if (v.type == tAttrs) { + strat.handleAttrs(*this, v); + } + } else { + primOp->primOp->funStrat(*this, pos, vArgs, v, strat); + } } else { Value * fun2 = allocValue(); *fun2 = fun; @@ -1229,14 +1295,18 @@ void EvalState::callPrimOp(Value & fun, Value & arg, Value & v, const Pos & pos) } void EvalState::callFunction(Value & fun, Value & arg, Value & v, const Pos & pos) +{ + callFunctionWithStrategy(fun, arg, v, ForceEvalStrategy::getInstance(), pos); +} + +void EvalState::callFunctionWithStrategy(Value & fun, Value & arg, Value & v, EvalStrategy & strat, const Pos & pos) { auto trace = evalSettings.traceFunctionCalls ? std::make_unique(pos) : nullptr; forceValue(fun, pos); if (fun.type == tPrimOp || fun.type == tPrimOpApp) { - callPrimOp(fun, arg, v, pos); - return; + return callPrimOp(fun, arg, v, strat, pos); } if (fun.type == tAttrs) { @@ -1251,7 +1321,7 @@ void EvalState::callFunction(Value & fun, Value & arg, Value & v, const Pos & po /* !!! Should we use the attr pos here? */ Value v2; callFunction(*found->value, fun2, v2, pos); - return callFunction(v2, arg, v, pos); + return callFunctionWithStrategy(v2, arg, v, strat, pos); } } @@ -1312,7 +1382,7 @@ void EvalState::callFunction(Value & fun, Value & arg, Value & v, const Pos & po catching exceptions makes this function not tail-recursive. */ if (loggerSettings.showTrace.get()) try { - lambda.body->eval(*this, env2, v); + lambda.body->evalWithStrategy(*this, env2, v, strat); } catch (Error & e) { addErrorTrace(e, lambda.pos, "while evaluating %s", (lambda.name.set() @@ -1322,7 +1392,7 @@ void EvalState::callFunction(Value & fun, Value & arg, Value & v, const Pos & po throw; } else - fun.lambda.fun->body->eval(*this, env2, v); + fun.lambda.fun->body->evalWithStrategy(*this, env2, v, strat); } @@ -1381,7 +1451,7 @@ void EvalState::autoCallFunction(Bindings & args, Value & fun, Value & res) } -void ExprWith::eval(EvalState & state, Env & env, Value & v) +void ExprWith::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { Env & env2(state.allocEnv(1)); env2.up = &env; @@ -1389,79 +1459,66 @@ void ExprWith::eval(EvalState & state, Env & env, Value & v) env2.type = Env::HasWithExpr; env2.values[0] = (Value *) attrs; - body->eval(state, env2, v); + body->evalWithStrategy(state, env2, v, strat); } - -void ExprIf::eval(EvalState & state, Env & env, Value & v) +void ExprIf::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { - (state.evalBool(env, cond, pos) ? then : else_)->eval(state, env, v); + (state.evalBool(env, cond, pos) ? then : else_)->evalWithStrategy(state, env, v, strat); } - -void ExprAssert::eval(EvalState & state, Env & env, Value & v) +void ExprAssert::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { if (!state.evalBool(env, cond, pos)) { std::ostringstream out; cond->show(out); throwAssertionError(pos, "assertion '%1%' failed at %2%", out.str()); } - body->eval(state, env, v); + body->evalWithStrategy(state, env, v, strat); } - -void ExprOpNot::eval(EvalState & state, Env & env, Value & v) +void ExprOpNot::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { mkBool(v, !state.evalBool(env, e)); } - -void ExprOpEq::eval(EvalState & state, Env & env, Value & v) +void ExprOpEq::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { Value v1; e1->eval(state, env, v1); Value v2; e2->eval(state, env, v2); mkBool(v, state.eqValues(v1, v2)); } - -void ExprOpNEq::eval(EvalState & state, Env & env, Value & v) +void ExprOpNEq::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { Value v1; e1->eval(state, env, v1); Value v2; e2->eval(state, env, v2); mkBool(v, !state.eqValues(v1, v2)); } - -void ExprOpAnd::eval(EvalState & state, Env & env, Value & v) +void ExprOpAnd::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { mkBool(v, state.evalBool(env, e1, pos) && state.evalBool(env, e2, pos)); } - -void ExprOpOr::eval(EvalState & state, Env & env, Value & v) +void ExprOpOr::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { mkBool(v, state.evalBool(env, e1, pos) || state.evalBool(env, e2, pos)); } - -void ExprOpImpl::eval(EvalState & state, Env & env, Value & v) +void ExprOpImpl::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { mkBool(v, !state.evalBool(env, e1, pos) || state.evalBool(env, e2, pos)); } - -void ExprOpUpdate::eval(EvalState & state, Env & env, Value & v) +void EvalState::updateAttrs(const Value & v1, const Value & v2, Value & v) { - Value v1, v2; - state.evalAttrs(env, e1, v1); - state.evalAttrs(env, e2, v2); - - state.nrOpUpdates++; + nrOpUpdates++; if (v1.attrs->size() == 0) { v = v2; return; } if (v2.attrs->size() == 0) { v = v1; return; } - state.mkAttrs(v, v1.attrs->size() + v2.attrs->size()); + mkAttrs(v, 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. */ @@ -1482,11 +1539,57 @@ void ExprOpUpdate::eval(EvalState & state, Env & env, Value & v) while (i != v1.attrs->end()) v.attrs->push_back(*i++); while (j != v2.attrs->end()) v.attrs->push_back(*j++); - state.nrOpUpdateValuesCopied += v.attrs->size(); + nrOpUpdateValuesCopied += v.attrs->size(); } +void ExprOpUpdate::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) +{ + Value v1, v2; + state.evalAttrs(env, e1, v1); + state.evalAttrs(env, e2, v2); + state.updateAttrs(v1, v2, v); + strat.handleAttrs(state, v); + + // Lazy version + //state.createLazyUpdate(*e1->maybeThunk(state, env), *e2->maybeThunk(state, env), v); + //state.reevalLazyUpdateWithStrategy(v, strat, e1->getPos(), e2->getPos()); +} -void ExprOpConcatLists::eval(EvalState & state, Env & env, Value & v) +void EvalState::createLazyUpdate(Value & v1, Value & v2, Value & v) +{ + v.type = tLazyUpdate; + v.lazyUpdate.left = &v1; + v.lazyUpdate.right = &v2; +} +// TODO: Only pass one position argument (corresponding to the left side) +void EvalState::reevalLazyUpdateWithStrategy(Value & v, EvalStrategy & strat, const Pos & pos1, const Pos & pos2) +{ + ValueType orig = v.type; + + // TODO: Try catch same as in eval-inline? + v.type = tBlackhole; + evalValueWithStrategy(*v.lazyUpdate.right, strat, pos2); + + if (!strat.done) { + + if (orig == tLazyUpdateLeftBlackhole) { + throwEvalError(pos1, "infinite recursion encountered while recursing into the left side of a lazy binop"); + } + // TODO: Try catch same as in eval-inline? + v.type = tLazyUpdateLeftBlackhole; + evalValueWithStrategy(*v.lazyUpdate.left, strat, pos1); + } + + // TODO: This allows nonsense like `1 // 2`. We need to check the type of both sides + if (v.lazyUpdate.left->type == tAttrs && v.lazyUpdate.right->type == tAttrs) { + updateAttrs(*v.lazyUpdate.left, *v.lazyUpdate.right, v); + } else { + v.type = orig; + } +} + + +void ExprOpConcatLists::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { Value v1; e1->eval(state, env, v1); Value v2; e2->eval(state, env, v2); @@ -1494,7 +1597,6 @@ void ExprOpConcatLists::eval(EvalState & state, Env & env, Value & v) state.concatLists(v, 2, lists, pos); } - void EvalState::concatLists(Value & v, size_t nrLists, Value * * lists, const Pos & pos) { nrListConcats++; @@ -1524,7 +1626,7 @@ void EvalState::concatLists(Value & v, size_t nrLists, Value * * lists, const Po } -void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v) +void ExprConcatStrings::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { PathSet context; std::ostringstream s; @@ -1581,13 +1683,12 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v) mkString(v, s.str(), context); } - -void ExprPos::eval(EvalState & state, Env & env, Value & v) +void ExprPos::evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat) { state.mkPos(v, &pos); + strat.handleAttrs(state, v); } - void EvalState::forceValueDeep(Value & v) { std::set seen; diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index 0e1f61baab3..4c289bb3e5d 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -21,12 +21,15 @@ class StorePath; enum RepairFlag : bool; -typedef void (* PrimOpFun) (EvalState & state, const Pos & pos, Value * * args, Value & v); +typedef void (* PrimOpForceFun) (EvalState & state, const Pos & pos, Value * * args, Value & v); + +typedef void (* PrimOpFun) (EvalState & state, const Pos & pos, Value * * args, Value & v, EvalStrategy & strat); struct PrimOp { - PrimOpFun fun; + PrimOpForceFun fun; + PrimOpFun funStrat; size_t arity; Symbol name; std::vector args; @@ -161,6 +164,7 @@ public: /* Evaluate an expression read from the given file to normal form. Optionally enforce that the top-level expression is trivial (i.e. doesn't require arbitrary computation). */ + void evalFileWithStrategy(const Path & path, Value & v, EvalStrategy & strat, bool mustBeTrivial = false); void evalFile(const Path & path, Value & v, bool mustBeTrivial = false); void resetFileCache(); @@ -174,6 +178,7 @@ public: /* Evaluate an expression to normal form, storing the result in value `v'. */ + void evalWithStrategy(Expr * e, Value & v, EvalStrategy & strat); void eval(Expr * e, Value & v); /* Evaluation the expression, then verify that it has the expected @@ -187,6 +192,10 @@ public: application, call the function and overwrite `v' with the result. Otherwise, this is a no-op. */ inline void forceValue(Value & v, const Pos & pos = noPos); + inline void evalValueWithStrategy(Value & v, EvalStrategy & strat, const Pos & pos = noPos); + + /* Get an attribute of a value, or null if it doesn't exist */ + inline Attr * evalValueAttr(Value & v, const Symbol & name, const Pos & pos); /* Force a value, then recursively force list elements and attributes. */ @@ -244,7 +253,7 @@ private: Value * addConstant(const string & name, Value & v); Value * addPrimOp(const string & name, - size_t arity, PrimOpFun primOp); + size_t arity, PrimOpForceFun primOp); Value * addPrimOp(PrimOp && primOp); @@ -282,8 +291,9 @@ public: bool isFunctor(Value & fun); + void callFunctionWithStrategy(Value & fun, Value & arg, Value & v, EvalStrategy & strat, const Pos & pos); void callFunction(Value & fun, Value & arg, Value & v, const Pos & pos); - void callPrimOp(Value & fun, Value & arg, Value & v, const Pos & pos); + void callPrimOp(Value & fun, Value & arg, Value & v, EvalStrategy & strat, const Pos & pos); /* Automatically call a function for which each argument has a default value or has a binding in the `args' map. */ @@ -305,6 +315,11 @@ public: void concatLists(Value & v, size_t nrLists, Value * * lists, const Pos & pos); + void updateAttrs(const Value & v1, const Value & v2, Value & v); + + void createLazyUpdate(Value & v1, Value & v2, Value & v); + void reevalLazyUpdateWithStrategy(Value & v, EvalStrategy & strat, const Pos & pos1, const Pos & pos2); + /* Print statistics. */ void printStats(); @@ -337,13 +352,55 @@ private: typedef std::map AttrSelects; AttrSelects attrSelects; - friend struct ExprOpUpdate; friend struct ExprOpConcatLists; friend struct ExprSelect; friend void prim_getAttr(EvalState & state, const Pos & pos, Value * * args, Value & v); friend void prim_match(EvalState & state, const Pos & pos, Value * * args, Value & v); }; +// An evaluation strategy, used to implement lazy attribute names +// For lazy expressions, evaluation will run the strategy sequentially +// on all its parts, and exit as soon as true is returned +class EvalStrategy +{ +public: + bool done = false; + + // How this evaluation strategy handles attribute sets + // The passed v is a tAttrs and can be modified by this function + // Returns whether the value caused the evaluation strategy to be "done" + virtual void handleAttrs(EvalState & state, Value & v) = 0; +}; + +// An evaluation strategy that forces values into weak head normal form, so no +// thunks or unapplied applications +class ForceEvalStrategy : public EvalStrategy +{ +private: + ForceEvalStrategy() { }; + +public: + static ForceEvalStrategy & getInstance() { + static ForceEvalStrategy instance; + return instance; + }; + void handleAttrs(EvalState & state, Value & v) override; +}; + +// An evaluation strategy that tries to only evaluate a specific attribute +// and gives access to the result +class AttrEvalStrategy : public EvalStrategy +{ +private: + const Symbol & name; + Attr * attr = nullptr; + +public: + AttrEvalStrategy(const Symbol & name) : name(name) { }; + void handleAttrs(EvalState & state, Value & v) override; + // Returns the attribute value if found, otherwise nullptr + Attr * getAttr() { return attr; }; +}; /* Return a string representing the type of the value `v'. */ string showType(ValueType type); diff --git a/src/libexpr/nixexpr.hh b/src/libexpr/nixexpr.hh index e4cbc660fc5..7814359eebe 100644 --- a/src/libexpr/nixexpr.hh +++ b/src/libexpr/nixexpr.hh @@ -55,7 +55,9 @@ std::ostream & operator << (std::ostream & str, const Pos & pos); struct Env; struct Value; class EvalState; +class EvalStrategy; struct StaticEnv; +struct Attr; /* An attribute path is a sequence of attribute names. */ @@ -74,12 +76,28 @@ string showAttrPath(const AttrPath & attrPath); /* Abstract syntax of Nix expressions. */ + struct Expr { virtual ~Expr() { }; virtual void show(std::ostream & str) const; virtual void bindVars(const StaticEnv & env); - virtual void eval(EvalState & state, Env & env, Value & v); + + // Evaluates an expression with a given evaluation strategy + // The result should be put into v, which is uninitialized at first + // See EvalStrategy for what the return value indicates + virtual void evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat); + + // Evaluates an expression with the ForceEvalStrategy evaluation strategy + // TODO: Inline? + void eval(EvalState & state, Env & env, Value & v); + + // Evaluates an expression with the AttrEvalStrategy evaluation strategy, + // returning the resulting attribute value + // TODO: Inline? + Attr * evalAttr(EvalState & state, Env & env, Value & v, const Symbol & name); + + virtual Value * maybeThunk(EvalState & state, Env & env); virtual void setName(Symbol & name); }; @@ -88,7 +106,7 @@ std::ostream & operator << (std::ostream & str, const Expr & e); #define COMMON_METHODS \ void show(std::ostream & str) const; \ - void eval(EvalState & state, Env & env, Value & v); \ + void evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat); \ void bindVars(const StaticEnv & env); struct ExprInt : Expr @@ -302,7 +320,7 @@ struct ExprOpNot : Expr { \ e1->bindVars(env); e2->bindVars(env); \ } \ - void eval(EvalState & state, Env & env, Value & v); \ + void evalWithStrategy(EvalState & state, Env & env, Value & v, EvalStrategy & strat); \ }; MakeBinOp(ExprApp, "") diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 2b304aab0fb..1387911b6cf 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -627,6 +627,18 @@ static RegisterPrimOp primop_genericClosure(RegisterPrimOp::Info { .fun = prim_genericClosure, }); +static void prim_lazyAttrUpdate(EvalState & state, const Pos & pos, Value * * args, Value & v, EvalStrategy & strat) +{ + state.createLazyUpdate(*args[0], *args[1], v); + state.reevalLazyUpdateWithStrategy(v, strat, pos, pos); +} + +static RegisterPrimOp primop_lazyAttrUpdate(RegisterPrimOp::Info { + .name = "__lazyAttrUpdate", + .arity = 2, + .funStrat = prim_lazyAttrUpdate, +}); + static RegisterPrimOp primop_abort({ .name = "abort", .args = {"s"}, @@ -1989,10 +2001,9 @@ static RegisterPrimOp primop_attrValues({ void prim_getAttr(EvalState & state, const Pos & pos, Value * * args, Value & v) { string attr = state.forceStringNoCtx(*args[0], pos); - state.forceAttrs(*args[1], pos); // !!! Should we create a symbol here or just do a lookup? - Bindings::iterator i = args[1]->attrs->find(state.symbols.create(attr)); - if (i == args[1]->attrs->end()) + Attr * i = state.evalValueAttr(*args[1], state.symbols.create(attr), pos); + if (!i) throw EvalError({ .hint = hintfmt("attribute '%1%' missing", attr), .errPos = pos @@ -3482,7 +3493,7 @@ static RegisterPrimOp primop_splitVersion({ RegisterPrimOp::PrimOps * RegisterPrimOp::primOps; -RegisterPrimOp::RegisterPrimOp(std::string name, size_t arity, PrimOpFun fun, +RegisterPrimOp::RegisterPrimOp(std::string name, size_t arity, PrimOpForceFun fun, std::optional requiredFeature) { if (!primOps) primOps = new PrimOps; @@ -3569,6 +3580,7 @@ void EvalState::createBaseEnv() if (!primOp.requiredFeature || settings.isExperimentalFeatureEnabled(*primOp.requiredFeature)) addPrimOp({ .fun = primOp.fun, + .funStrat = primOp.funStrat, .arity = std::max(primOp.args.size(), primOp.arity), .name = symbols.create(primOp.name), .args = std::move(primOp.args), diff --git a/src/libexpr/primops.hh b/src/libexpr/primops.hh index 9d42d65398d..5e050603491 100644 --- a/src/libexpr/primops.hh +++ b/src/libexpr/primops.hh @@ -16,7 +16,8 @@ struct RegisterPrimOp size_t arity = 0; const char * doc; std::optional requiredFeature; - PrimOpFun fun; + PrimOpForceFun fun; + PrimOpFun funStrat; }; typedef std::vector PrimOps; @@ -28,7 +29,7 @@ struct RegisterPrimOp RegisterPrimOp( std::string name, size_t arity, - PrimOpFun fun, + PrimOpForceFun fun, std::optional requiredFeature = {}); RegisterPrimOp(Info && info); diff --git a/src/libexpr/value.hh b/src/libexpr/value.hh index fe11bb2ed24..4e63d74fc35 100644 --- a/src/libexpr/value.hh +++ b/src/libexpr/value.hh @@ -26,7 +26,9 @@ typedef enum { tPrimOp, tPrimOpApp, tExternal, - tFloat + tFloat, + tLazyUpdate, + tLazyUpdateLeftBlackhole, } ValueType; @@ -91,6 +93,7 @@ std::ostream & operator << (std::ostream & str, const ExternalValueBase & v); struct Value { ValueType type; + union { NixInt integer; @@ -132,6 +135,10 @@ struct Value Env * env; Expr * expr; } thunk; + struct { + Value * left; + Value * right; + } lazyUpdate; struct { Value * left, * right; } app;