diff options
-rw-r--r-- | doc/manual/change-authors.yml | 5 | ||||
-rw-r--r-- | doc/manual/rl-next/pipe-operator.md | 10 | ||||
-rw-r--r-- | doc/manual/src/language/operators.md | 32 | ||||
-rw-r--r-- | src/libexpr/parser/grammar.hh | 7 | ||||
-rw-r--r-- | src/libexpr/parser/parser.cc | 25 | ||||
-rw-r--r-- | src/libstore/build/derivation-goal.cc | 54 | ||||
-rw-r--r-- | src/libstore/build/derivation-goal.hh | 3 | ||||
-rw-r--r-- | src/libstore/build/drv-output-substitution-goal.cc | 12 | ||||
-rw-r--r-- | src/libstore/build/goal.cc | 7 | ||||
-rw-r--r-- | src/libstore/build/goal.hh | 20 | ||||
-rw-r--r-- | src/libstore/build/local-derivation-goal.cc | 15 | ||||
-rw-r--r-- | src/libstore/build/substitution-goal.cc | 16 | ||||
-rw-r--r-- | src/libstore/build/worker.cc | 20 | ||||
-rw-r--r-- | src/libstore/build/worker.hh | 74 | ||||
-rw-r--r-- | src/libutil/experimental-features.cc | 10 | ||||
-rw-r--r-- | src/libutil/experimental-features.hh | 1 | ||||
-rw-r--r-- | tests/unit/libexpr/trivial.cc | 36 |
17 files changed, 244 insertions, 103 deletions
diff --git a/doc/manual/change-authors.yml b/doc/manual/change-authors.yml index 630af29ff..e18abada1 100644 --- a/doc/manual/change-authors.yml +++ b/doc/manual/change-authors.yml @@ -103,6 +103,11 @@ midnightveil: ncfavier: github: ncfavier +piegames: + display_name: piegames + forgejo: piegames + github: piegamesde + puck: display_name: puck forgejo: puck diff --git a/doc/manual/rl-next/pipe-operator.md b/doc/manual/rl-next/pipe-operator.md new file mode 100644 index 000000000..49dc01308 --- /dev/null +++ b/doc/manual/rl-next/pipe-operator.md @@ -0,0 +1,10 @@ +--- +synopsis: Pipe operator `|>` (experimental) +issues: [fj#438] +cls: [1654] +category: Features +credits: [piegames, horrors] +--- + +Implementation of the pipe operator (`|>`) in the language as described in [RFC 148](https://github.com/NixOS/rfcs/pull/148). +The feature is still marked experimental, enable `--extra-experimental-features pipe-operator` to use it. diff --git a/doc/manual/src/language/operators.md b/doc/manual/src/language/operators.md index 6dcdc6eb0..2d4707814 100644 --- a/doc/manual/src/language/operators.md +++ b/doc/manual/src/language/operators.md @@ -26,6 +26,8 @@ | Logical conjunction (`AND`) | *bool* `&&` *bool* | left | 12 | | Logical disjunction (`OR`) | *bool* <code>\|\|</code> *bool* | left | 13 | | [Logical implication] | *bool* `->` *bool* | none | 14 | +| \[Experimental\] [Function piping] | *expr* |> *func* | left | 15 | +| \[Experimental\] [Function piping] | *expr* <| *func* | right | 16 | [string]: ./values.md#type-string [path]: ./values.md#type-path @@ -215,3 +217,33 @@ nix-repl> let f = x: 1; s = { func = f; }; in [ (f == f) (s == s) ] Equivalent to `!`*b1* `||` *b2*. [Logical implication]: #logical-implication + +## \[Experimental\] Function piping + +*This language feature is still experimental and may change at any time. Enable `--extra-experimental-features pipe-operator` to use it.* + +Pipes are a dedicated operator for function application, but with reverse order and a lower binding strength. +This allows you to chain function calls together in way that is more natural to read and requires less parentheses. + +`a |> f b |> g` is equivalent to `g (f b a)`. +`g <| f b <| a` is equivalent to `g (f b a)`. + +Example code snippet: + +```nix +defaultPrefsFile = defaultPrefs + |> lib.mapAttrsToList ( + key: value: '' + // ${value.reason} + pref("${key}", ${builtins.toJSON value.value}); + '' + ) + |> lib.concatStringsSep "\n" + |> pkgs.writeText "nixos-default-prefs.js"; +``` + +Note how `mapAttrsToList` is called with two arguments (the lambda and `defaultPrefs`), +but moving the last argument in front of the rest improves the reading flow. +This is common for functions with long first argument, including all `map`-like functions. + +[Function piping]: #experimental-function-piping diff --git a/src/libexpr/parser/grammar.hh b/src/libexpr/parser/grammar.hh index 82df63bc5..2c5a3d1be 100644 --- a/src/libexpr/parser/grammar.hh +++ b/src/libexpr/parser/grammar.hh @@ -434,6 +434,8 @@ struct op { struct and_ : _op<TAO_PEGTL_STRING("&&"), 12> {}; struct or_ : _op<TAO_PEGTL_STRING("||"), 13> {}; struct implies : _op<TAO_PEGTL_STRING("->"), 14, kind::rightAssoc> {}; + struct pipe_right : _op<TAO_PEGTL_STRING("|>"), 15> {}; + struct pipe_left : _op<TAO_PEGTL_STRING("<|"), 16, kind::rightAssoc> {}; }; struct _expr { @@ -521,6 +523,7 @@ struct _expr { app > {}; + /* Order matters here. The order is the parsing order, not the precedence order: '<=' must be parsed before '<'. */ struct _binary_operator : sor< operator_<op::implies>, operator_<op::update>, @@ -529,6 +532,8 @@ struct _expr { operator_<op::minus>, operator_<op::mul>, operator_<op::div>, + operator_<op::pipe_right>, + operator_<op::pipe_left>, operator_<op::less_eq>, operator_<op::greater_eq>, operator_<op::less>, @@ -649,6 +654,8 @@ struct operator_semantics { grammar::op::minus, grammar::op::mul, grammar::op::div, + grammar::op::pipe_right, + grammar::op::pipe_left, has_attr > op; }; diff --git a/src/libexpr/parser/parser.cc b/src/libexpr/parser/parser.cc index 68aa3ddc5..6d496d141 100644 --- a/src/libexpr/parser/parser.cc +++ b/src/libexpr/parser/parser.cc @@ -113,6 +113,29 @@ struct ExprState return std::make_unique<ExprCall>(pos, std::make_unique<ExprVar>(fn), std::move(args)); } + std::unique_ptr<Expr> pipe(PosIdx pos, State & state, bool flip = false) + { + if (!state.xpSettings.isEnabled(Xp::PipeOperator)) + throw ParseError({ + .msg = HintFmt("Pipe operator is disabled"), + .pos = state.positions[pos] + }); + + // Reverse the order compared to normal function application: arg |> fn + std::unique_ptr<Expr> fn, arg; + if (flip) { + fn = popExprOnly(); + arg = popExprOnly(); + } else { + arg = popExprOnly(); + fn = popExprOnly(); + } + std::vector<std::unique_ptr<Expr>> args{1}; + args[0] = std::move(arg); + + return std::make_unique<ExprCall>(pos, std::move(fn), std::move(args)); + } + std::unique_ptr<Expr> order(PosIdx pos, bool less, State & state) { return call(pos, state.s.lessThan, !less); @@ -162,6 +185,8 @@ struct ExprState [&] (Op::concat) { return applyBinary<ExprOpConcatLists>(pos); }, [&] (has_attr & a) { return applyUnary<ExprOpHasAttr>(std::move(a.path)); }, [&] (Op::unary_minus) { return negate(pos, state); }, + [&] (Op::pipe_right) { return pipe(pos, state, true); }, + [&] (Op::pipe_left) { return pipe(pos, state); }, })(op) }; } diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index 17a2b04f1..aa89f9e7d 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -174,10 +174,9 @@ Goal::WorkResult DerivationGoal::getDerivation() return loadDerivation(); } - addWaitee(worker.makePathSubstitutionGoal(drvPath)); state = &DerivationGoal::loadDerivation; - return StillAlive{}; + return WaitForGoals{{worker.makePathSubstitutionGoal(drvPath)}}; } @@ -268,11 +267,12 @@ Goal::WorkResult DerivationGoal::haveDerivation() /* We are first going to try to create the invalid output paths through substitutes. If that doesn't work, we'll build them. */ + WaitForGoals result; if (settings.useSubstitutes && parsedDrv->substitutesAllowed()) for (auto & [outputName, status] : initialOutputs) { if (!status.wanted) continue; if (!status.known) - addWaitee( + result.goals.insert( worker.makeDrvOutputSubstitutionGoal( DrvOutput{status.outputHash, outputName}, buildMode == bmRepair ? Repair : NoRepair @@ -280,18 +280,18 @@ Goal::WorkResult DerivationGoal::haveDerivation() ); else { auto * cap = getDerivationCA(*drv); - addWaitee(worker.makePathSubstitutionGoal( + result.goals.insert(worker.makePathSubstitutionGoal( status.known->path, buildMode == bmRepair ? Repair : NoRepair, cap ? std::optional { *cap } : std::nullopt)); } } - if (waitees.empty()) { /* to prevent hang (no wake-up event) */ + if (result.goals.empty()) { /* to prevent hang (no wake-up event) */ return outputsSubstitutionTried(); } else { state = &DerivationGoal::outputsSubstitutionTried; - return StillAlive{}; + return result; } } @@ -362,6 +362,8 @@ Goal::WorkResult DerivationGoal::outputsSubstitutionTried() produced using a substitute. So we have to build instead. */ Goal::WorkResult DerivationGoal::gaveUpOnSubstitution() { + WaitForGoals result; + /* At this point we are building all outputs, so if more are wanted there is no need to restart. */ needRestart = NeedRestartForMoreOutputs::BuildInProgressWillNotNeed; @@ -373,7 +375,7 @@ Goal::WorkResult DerivationGoal::gaveUpOnSubstitution() addWaiteeDerivedPath = [&](ref<SingleDerivedPath> inputDrv, const DerivedPathMap<StringSet>::ChildNode & inputNode) { if (!inputNode.value.empty()) - addWaitee(worker.makeGoal( + result.goals.insert(worker.makeGoal( DerivedPath::Built { .drvPath = inputDrv, .outputs = inputNode.value, @@ -418,14 +420,14 @@ Goal::WorkResult DerivationGoal::gaveUpOnSubstitution() if (!settings.useSubstitutes) throw Error("dependency '%s' of '%s' does not exist, and substitution is disabled", worker.store.printStorePath(i), worker.store.printStorePath(drvPath)); - addWaitee(worker.makePathSubstitutionGoal(i)); + result.goals.insert(worker.makePathSubstitutionGoal(i)); } - if (waitees.empty()) {/* to prevent hang (no wake-up event) */ + if (result.goals.empty()) {/* to prevent hang (no wake-up event) */ return inputsRealised(); } else { state = &DerivationGoal::inputsRealised; - return StillAlive{}; + return result; } } @@ -466,6 +468,7 @@ Goal::WorkResult DerivationGoal::repairClosure() } /* Check each path (slow!). */ + WaitForGoals result; for (auto & i : outputClosure) { if (worker.pathContentsGood(i)) continue; printError( @@ -473,9 +476,9 @@ Goal::WorkResult DerivationGoal::repairClosure() worker.store.printStorePath(i), worker.store.printStorePath(drvPath)); auto drvPath2 = outputsToDrv.find(i); if (drvPath2 == outputsToDrv.end()) - addWaitee(worker.makePathSubstitutionGoal(i, Repair)); + result.goals.insert(worker.makePathSubstitutionGoal(i, Repair)); else - addWaitee(worker.makeGoal( + result.goals.insert(worker.makeGoal( DerivedPath::Built { .drvPath = makeConstantStorePathRef(drvPath2->second), .outputs = OutputsSpec::All { }, @@ -483,12 +486,12 @@ Goal::WorkResult DerivationGoal::repairClosure() bmRepair)); } - if (waitees.empty()) { + if (result.goals.empty()) { return done(BuildResult::AlreadyValid, assertPathValidity()); } state = &DerivationGoal::closureRepaired; - return StillAlive{}; + return result; } @@ -580,10 +583,9 @@ Goal::WorkResult DerivationGoal::inputsRealised() resolvedDrvGoal = worker.makeDerivationGoal( pathResolved, wantedOutputs, buildMode); - addWaitee(resolvedDrvGoal); state = &DerivationGoal::resolvedFinished; - return StillAlive{}; + return WaitForGoals{{resolvedDrvGoal}}; } std::function<void(const StorePath &, const DerivedPathMap<StringSet>::ChildNode &)> accumInputPaths; @@ -648,8 +650,7 @@ Goal::WorkResult DerivationGoal::inputsRealised() slot to become available, since we don't need one if there is a build hook. */ state = &DerivationGoal::tryToBuild; - worker.wakeUp(shared_from_this()); - return StillAlive{}; + return ContinueImmediately{}; } Goal::WorkResult DerivationGoal::started() @@ -701,8 +702,7 @@ Goal::WorkResult DerivationGoal::tryToBuild() if (!actLock) actLock = std::make_unique<Activity>(*logger, lvlWarn, actBuildWaiting, fmt("waiting for lock on %s", Magenta(showPaths(lockFiles)))); - worker.waitForAWhile(shared_from_this()); - return StillAlive{}; + return WaitForAWhile{}; } actLock.reset(); @@ -753,9 +753,8 @@ Goal::WorkResult DerivationGoal::tryToBuild() if (!actLock) actLock = std::make_unique<Activity>(*logger, lvlTalkative, actBuildWaiting, fmt("waiting for a machine to build '%s'", Magenta(worker.store.printStorePath(drvPath)))); - worker.waitForAWhile(shared_from_this()); outputLocks.unlock(); - return StillAlive{}; + return WaitForAWhile{}; case rpDecline: /* We should do it ourselves. */ break; @@ -765,8 +764,7 @@ Goal::WorkResult DerivationGoal::tryToBuild() actLock.reset(); state = &DerivationGoal::tryLocalBuild; - worker.wakeUp(shared_from_this()); - return StillAlive{}; + return ContinueImmediately{}; } Goal::WorkResult DerivationGoal::tryLocalBuild() { @@ -1508,10 +1506,6 @@ Goal::Finished DerivationGoal::done( buildResult.status = status; if (ex) buildResult.errorMsg = fmt("%s", Uncolored(ex->info().msg)); - if (buildResult.status == BuildResult::TimedOut) - worker.timedOut = true; - if (buildResult.status == BuildResult::PermanentFailure) - worker.permanentFailure = true; mcExpectedBuilds.reset(); mcRunningBuilds.reset(); @@ -1537,6 +1531,10 @@ Goal::Finished DerivationGoal::done( return Finished{ .result = buildResult.success() ? ecSuccess : ecFailed, .ex = ex ? std::make_unique<Error>(std::move(*ex)) : nullptr, + .permanentFailure = buildResult.status == BuildResult::PermanentFailure, + .timedOut = buildResult.status == BuildResult::TimedOut, + .hashMismatch = anyHashMismatchSeen, + .checkMismatch = anyCheckMismatchSeen, }; } diff --git a/src/libstore/build/derivation-goal.hh b/src/libstore/build/derivation-goal.hh index c43e2aed5..257282308 100644 --- a/src/libstore/build/derivation-goal.hh +++ b/src/libstore/build/derivation-goal.hh @@ -107,6 +107,9 @@ struct DerivationGoal : public Goal */ NeedRestartForMoreOutputs needRestart = NeedRestartForMoreOutputs::OutputsUnmodifedDontNeed; + bool anyHashMismatchSeen = false; + bool anyCheckMismatchSeen = false; + /** * See `retrySubstitution`; just for that field. */ diff --git a/src/libstore/build/drv-output-substitution-goal.cc b/src/libstore/build/drv-output-substitution-goal.cc index 62e86e1cc..b41dae5d6 100644 --- a/src/libstore/build/drv-output-substitution-goal.cc +++ b/src/libstore/build/drv-output-substitution-goal.cc @@ -41,8 +41,7 @@ Goal::WorkResult DrvOutputSubstitutionGoal::tryNext() if maxSubstitutionJobs == 0, we still allow a substituter to run. This prevents infinite waiting. */ if (worker.runningSubstitutions >= std::max(1U, settings.maxSubstitutionJobs.get())) { - worker.waitForBuildSlot(shared_from_this()); - return StillAlive{}; + return WaitForSlot{}; } maintainRunningSubstitutions = @@ -101,6 +100,7 @@ Goal::WorkResult DrvOutputSubstitutionGoal::realisationFetched() return tryNext(); } + WaitForGoals result; for (const auto & [depId, depPath] : outputInfo->dependentRealisations) { if (depId != id) { if (auto localOutputInfo = worker.store.queryRealisation(depId); @@ -116,17 +116,17 @@ Goal::WorkResult DrvOutputSubstitutionGoal::realisationFetched() ); return tryNext(); } - addWaitee(worker.makeDrvOutputSubstitutionGoal(depId)); + result.goals.insert(worker.makeDrvOutputSubstitutionGoal(depId)); } } - addWaitee(worker.makePathSubstitutionGoal(outputInfo->outPath)); + result.goals.insert(worker.makePathSubstitutionGoal(outputInfo->outPath)); - if (waitees.empty()) { + if (result.goals.empty()) { return outPathValid(); } else { state = &DrvOutputSubstitutionGoal::outPathValid; - return StillAlive{}; + return result; } } diff --git a/src/libstore/build/goal.cc b/src/libstore/build/goal.cc index f26c2c671..a4e66d989 100644 --- a/src/libstore/build/goal.cc +++ b/src/libstore/build/goal.cc @@ -11,13 +11,6 @@ bool CompareGoalPtrs::operator() (const GoalPtr & a, const GoalPtr & b) const { } -void Goal::addWaitee(GoalPtr waitee) -{ - waitees.insert(waitee); - waitee->waiters.insert(shared_from_this()); -} - - void Goal::trace(std::string_view s) { debug("%1%: %2%", name, s); diff --git a/src/libstore/build/goal.hh b/src/libstore/build/goal.hh index dd29b9fc4..5d7bb72b8 100644 --- a/src/libstore/build/goal.hh +++ b/src/libstore/build/goal.hh @@ -106,12 +106,28 @@ struct Goal : public std::enable_shared_from_this<Goal> public: struct [[nodiscard]] StillAlive {}; + struct [[nodiscard]] WaitForSlot {}; + struct [[nodiscard]] WaitForAWhile {}; + struct [[nodiscard]] ContinueImmediately {}; + struct [[nodiscard]] WaitForGoals { + Goals goals; + }; struct [[nodiscard]] Finished { ExitCode result; std::unique_ptr<Error> ex; + bool permanentFailure = false; + bool timedOut = false; + bool hashMismatch = false; + bool checkMismatch = false; }; - struct [[nodiscard]] WorkResult : std::variant<StillAlive, Finished> + struct [[nodiscard]] WorkResult : std::variant< + StillAlive, + WaitForSlot, + WaitForAWhile, + ContinueImmediately, + WaitForGoals, + Finished> { WorkResult() = delete; using variant::variant; @@ -133,8 +149,6 @@ public: virtual WorkResult work() = 0; - void addWaitee(GoalPtr waitee); - virtual void waiteeDone(GoalPtr waitee) { } virtual WorkResult handleChildOutput(int fd, std::string_view data) diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index c435a3c00..8b640e4ad 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -158,9 +158,8 @@ Goal::WorkResult LocalDerivationGoal::tryLocalBuild() unsigned int curBuilds = worker.getNrLocalBuilds(); if (curBuilds >= settings.maxBuildJobs) { state = &DerivationGoal::tryToBuild; - worker.waitForBuildSlot(shared_from_this()); outputLocks.unlock(); - return StillAlive{}; + return WaitForSlot{}; } assert(derivationType); @@ -202,8 +201,7 @@ Goal::WorkResult LocalDerivationGoal::tryLocalBuild() if (!actLock) actLock = std::make_unique<Activity>(*logger, lvlWarn, actBuildWaiting, fmt("waiting for a free build user ID for '%s'", Magenta(worker.store.printStorePath(drvPath)))); - worker.waitForAWhile(shared_from_this()); - return StillAlive{}; + return WaitForAWhile{}; } } @@ -237,8 +235,9 @@ Goal::WorkResult LocalDerivationGoal::tryLocalBuild() } catch (BuildError & e) { outputLocks.unlock(); buildUser.reset(); - worker.permanentFailure = true; - return done(BuildResult::InputRejected, {}, std::move(e)); + auto report = done(BuildResult::InputRejected, {}, std::move(e)); + report.permanentFailure = true; + return report; } /* This state will be reached when we get EOF on the child's @@ -2197,7 +2196,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() if (wanted != got) { /* Throw an error after registering the path as valid. */ - worker.hashMismatch = true; + anyHashMismatchSeen = true; // XXX: shameless layering violation hack that makes the hash mismatch error at least not utterly worthless auto guessedUrl = getOr(drv->env, "urls", getOr(drv->env, "url", "(unknown)")); delayedException = std::make_exception_ptr( @@ -2284,7 +2283,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() if (!worker.store.isValidPath(newInfo.path)) continue; ValidPathInfo oldInfo(*worker.store.queryPathInfo(newInfo.path)); if (newInfo.narHash != oldInfo.narHash) { - worker.checkMismatch = true; + anyCheckMismatchSeen = true; if (settings.runDiffHook || settings.keepFailed) { auto dst = worker.store.toRealPath(finalDestPath + checkSuffix); deletePath(dst); diff --git a/src/libstore/build/substitution-goal.cc b/src/libstore/build/substitution-goal.cc index fb2949fd0..67a5f20bb 100644 --- a/src/libstore/build/substitution-goal.cc +++ b/src/libstore/build/substitution-goal.cc @@ -152,15 +152,16 @@ Goal::WorkResult PathSubstitutionGoal::tryNext() /* To maintain the closure invariant, we first have to realise the paths referenced by this one. */ + WaitForGoals result; for (auto & i : info->references) if (i != storePath) /* ignore self-references */ - addWaitee(worker.makePathSubstitutionGoal(i)); + result.goals.insert(worker.makePathSubstitutionGoal(i)); - if (waitees.empty()) {/* to prevent hang (no wake-up event) */ + if (result.goals.empty()) {/* to prevent hang (no wake-up event) */ return referencesValid(); } else { state = &PathSubstitutionGoal::referencesValid; - return StillAlive{}; + return result; } } @@ -181,8 +182,7 @@ Goal::WorkResult PathSubstitutionGoal::referencesValid() assert(worker.store.isValidPath(i)); state = &PathSubstitutionGoal::tryToRun; - worker.wakeUp(shared_from_this()); - return StillAlive{}; + return ContinueImmediately{}; } @@ -194,8 +194,7 @@ Goal::WorkResult PathSubstitutionGoal::tryToRun() if maxSubstitutionJobs == 0, we still allow a substituter to run. This prevents infinite waiting. */ if (worker.getNrSubstitutions() >= std::max(1U, (unsigned int) settings.maxSubstitutionJobs)) { - worker.waitForBuildSlot(shared_from_this()); - return StillAlive{}; + return WaitForSlot{}; } maintainRunningSubstitutions = std::make_unique<MaintainCount<uint64_t>>(worker.runningSubstitutions); @@ -256,8 +255,7 @@ Goal::WorkResult PathSubstitutionGoal::finished() /* Try the next substitute. */ state = &PathSubstitutionGoal::tryNext; - worker.wakeUp(shared_from_this()); - return StillAlive{}; + return ContinueImmediately{}; } worker.markContentsGood(storePath); diff --git a/src/libstore/build/worker.cc b/src/libstore/build/worker.cc index 30c4ed944..a27cb0076 100644 --- a/src/libstore/build/worker.cc +++ b/src/libstore/build/worker.cc @@ -22,10 +22,6 @@ Worker::Worker(Store & store, Store & evalStore) nrLocalBuilds = 0; nrSubstitutions = 0; lastWokenUp = steady_time_point::min(); - permanentFailure = false; - timedOut = false; - hashMismatch = false; - checkMismatch = false; } @@ -146,6 +142,11 @@ void Worker::goalFinished(GoalPtr goal, Goal::Finished & f) assert(!goal->exitCode.has_value()); goal->exitCode = f.result; + permanentFailure |= f.permanentFailure; + timedOut |= f.timedOut; + hashMismatch |= f.hashMismatch; + checkMismatch |= f.checkMismatch; + if (f.ex) { if (!goal->waiters.empty()) logError(f.ex->info()); @@ -188,6 +189,15 @@ void Worker::handleWorkResult(GoalPtr goal, Goal::WorkResult how) std::visit( overloaded{ [&](Goal::StillAlive) {}, + [&](Goal::WaitForSlot) { waitForBuildSlot(goal); }, + [&](Goal::WaitForAWhile) { waitForAWhile(goal); }, + [&](Goal::ContinueImmediately) { wakeUp(goal); }, + [&](Goal::WaitForGoals & w) { + for (auto & dep : w.goals) { + goal->waitees.insert(dep); + dep->waiters.insert(goal); + } + }, [&](Goal::Finished & f) { goalFinished(goal, f); }, }, how @@ -522,7 +532,7 @@ void Worker::waitForInput() if (rd == 0 || (rd == -1 && errno == EIO)) { debug("%1%: got EOF", goal->getName()); goal->handleEOF(k); - wakeUp(goal); + handleWorkResult(goal, Goal::ContinueImmediately{}); j->fds.erase(k); } else if (rd == -1) { if (errno != EINTR) diff --git a/src/libstore/build/worker.hh b/src/libstore/build/worker.hh index 5af93b49e..a741b2672 100644 --- a/src/libstore/build/worker.hh +++ b/src/libstore/build/worker.hh @@ -105,35 +105,59 @@ private: */ std::map<StorePath, bool> pathContentsGoodCache; - void goalFinished(GoalPtr goal, Goal::Finished & f); - void handleWorkResult(GoalPtr goal, Goal::WorkResult how); - -public: - - const Activity act; - const Activity actDerivations; - const Activity actSubstitutions; - /** * Set if at least one derivation had a BuildError (i.e. permanent * failure). */ - bool permanentFailure; + bool permanentFailure = false; /** * Set if at least one derivation had a timeout. */ - bool timedOut; + bool timedOut = false; /** * Set if at least one derivation fails with a hash mismatch. */ - bool hashMismatch; + bool hashMismatch = false; /** * Set if at least one derivation is not deterministic in check mode. */ - bool checkMismatch; + bool checkMismatch = false; + + void goalFinished(GoalPtr goal, Goal::Finished & f); + void handleWorkResult(GoalPtr goal, Goal::WorkResult how); + + /** + * Put `goal` to sleep until a build slot becomes available (which + * might be right away). + */ + void waitForBuildSlot(GoalPtr goal); + + /** + * Wait for a few seconds and then retry this goal. Used when + * waiting for a lock held by another process. This kind of + * polling is inefficient, but POSIX doesn't really provide a way + * to wait for multiple locks in the main select() loop. + */ + void waitForAWhile(GoalPtr goal); + + /** + * Wake up a goal (i.e., there is something for it to do). + */ + void wakeUp(GoalPtr goal); + + /** + * Wait for input to become available. + */ + void waitForInput(); + +public: + + const Activity act; + const Activity actDerivations; + const Activity actSubstitutions; Store & store; Store & evalStore; @@ -206,11 +230,6 @@ public: void removeGoal(GoalPtr goal); /** - * Wake up a goal (i.e., there is something for it to do). - */ - void wakeUp(GoalPtr goal); - - /** * Return the number of local build processes currently running (but not * remote builds via the build hook). */ @@ -234,29 +253,10 @@ public: void childTerminated(Goal * goal); /** - * Put `goal` to sleep until a build slot becomes available (which - * might be right away). - */ - void waitForBuildSlot(GoalPtr goal); - - /** - * Wait for a few seconds and then retry this goal. Used when - * waiting for a lock held by another process. This kind of - * polling is inefficient, but POSIX doesn't really provide a way - * to wait for multiple locks in the main select() loop. - */ - void waitForAWhile(GoalPtr goal); - - /** * Loop until the specified top-level goals have finished. */ void run(const Goals & topGoals); - /** - * Wait for input to become available. - */ - void waitForInput(); - /*** * The exit status in case of failure. * diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc index 3a834293a..15a18c770 100644 --- a/src/libutil/experimental-features.cc +++ b/src/libutil/experimental-features.cc @@ -167,6 +167,16 @@ constexpr std::array<ExperimentalFeatureDetails, numXpFeatures> xpFeatureDetails )", }, { + .tag = Xp::PipeOperator, + .name = "pipe-operator", + .description = R"( + Enable new operators for function application to "pipe" arguments through a chain of functions similar to `lib.pipe`. + This implementation is based on Nix [RFC 148](https://github.com/NixOS/rfcs/pull/148). + + Tracking issue: https://git.lix.systems/lix-project/lix/issues/438 + )", + }, + { .tag = Xp::FetchClosure, .name = "fetch-closure", .description = R"( diff --git a/src/libutil/experimental-features.hh b/src/libutil/experimental-features.hh index 38889e7bc..121318d23 100644 --- a/src/libutil/experimental-features.hh +++ b/src/libutil/experimental-features.hh @@ -21,6 +21,7 @@ enum struct ExperimentalFeature NixCommand, RecursiveNix, NoUrlLiterals, + PipeOperator, FetchClosure, ReplFlake, AutoAllocateUids, diff --git a/tests/unit/libexpr/trivial.cc b/tests/unit/libexpr/trivial.cc index 19b62aff8..c984657fd 100644 --- a/tests/unit/libexpr/trivial.cc +++ b/tests/unit/libexpr/trivial.cc @@ -210,4 +210,40 @@ namespace nix { TEST_F(TrivialExpressionTest, orCantBeUsed) { ASSERT_THROW(eval("let or = 1; in or"), Error); } + + // pipes are gated behind an experimental feature flag + TEST_F(TrivialExpressionTest, pipeDisabled) { + ASSERT_THROW(eval("let add = l: r: l + r; in ''a'' |> add ''b''"), Error); + ASSERT_THROW(eval("let add = l: r: l + r; in ''a'' <| add ''b''"), Error); + } + + TEST_F(TrivialExpressionTest, pipeRight) { + ExperimentalFeatureSettings mockXpSettings; + mockXpSettings.set("experimental-features", "pipe-operator"); + + auto v = eval("let add = l: r: l + r; in ''a'' |> add ''b''", true, mockXpSettings); + ASSERT_THAT(v, IsStringEq("ba")); + v = eval("let add = l: r: l + r; in ''a'' |> add ''b'' |> add ''c''", true, mockXpSettings); + ASSERT_THAT(v, IsStringEq("cba")); + } + + TEST_F(TrivialExpressionTest, pipeLeft) { + ExperimentalFeatureSettings mockXpSettings; + mockXpSettings.set("experimental-features", "pipe-operator"); + + auto v = eval("let add = l: r: l + r; in add ''a'' <| ''b''", true, mockXpSettings); + ASSERT_THAT(v, IsStringEq("ab")); + v = eval("let add = l: r: l + r; in add ''a'' <| add ''b'' <| ''c''", true, mockXpSettings); + ASSERT_THAT(v, IsStringEq("abc")); + } + + TEST_F(TrivialExpressionTest, pipeMixed) { + ExperimentalFeatureSettings mockXpSettings; + mockXpSettings.set("experimental-features", "pipe-operator"); + + auto v = eval("let add = l: r: l + r; in add ''a'' <| ''b'' |> add ''c''", true, mockXpSettings); + ASSERT_THAT(v, IsStringEq("acb")); + v = eval("let add = l: r: l + r; in ''a'' |> add <| ''c''", true, mockXpSettings); + ASSERT_THAT(v, IsStringEq("ac")); + } } /* namespace nix */ |