From 28ae24f3f738a50903bd9b9bf8eece84a1075c30 Mon Sep 17 00:00:00 2001 From: piegames Date: Thu, 11 Jul 2024 10:49:15 +0200 Subject: [PATCH] libexpr: Add experimental pipe operator The |> operator is a reverse function operator with low binding strength to replace lib.pipe. Implements RFC 148, see the RFC text for more details. Closes #438. Change-Id: I21df66e8014e0d4dd9753dd038560a2b0b7fd805 --- doc/manual/change-authors.yml | 5 ++++ doc/manual/rl-next/pipe-operator.md | 10 ++++++++ doc/manual/src/language/operators.md | 32 +++++++++++++++++++++++++ src/libexpr/parser/grammar.hh | 7 ++++++ src/libexpr/parser/parser.cc | 25 +++++++++++++++++++ src/libutil/experimental-features.cc | 10 ++++++++ src/libutil/experimental-features.hh | 1 + tests/unit/libexpr/trivial.cc | 36 ++++++++++++++++++++++++++++ 8 files changed, 126 insertions(+) create mode 100644 doc/manual/rl-next/pipe-operator.md 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* \|\| *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 {}; struct or_ : _op {}; struct implies : _op"), 14, kind::rightAssoc> {}; + struct pipe_right : _op"), 15> {}; + struct pipe_left : _op {}; }; 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_, operator_, @@ -529,6 +532,8 @@ struct _expr { operator_, operator_, operator_, + operator_, + operator_, operator_, operator_, operator_, @@ -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(pos, std::make_unique(fn), std::move(args)); } + std::unique_ptr 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 fn, arg; + if (flip) { + fn = popExprOnly(); + arg = popExprOnly(); + } else { + arg = popExprOnly(); + fn = popExprOnly(); + } + std::vector> args{1}; + args[0] = std::move(arg); + + return std::make_unique(pos, std::move(fn), std::move(args)); + } + std::unique_ptr order(PosIdx pos, bool less, State & state) { return call(pos, state.s.lessThan, !less); @@ -162,6 +185,8 @@ struct ExprState [&] (Op::concat) { return applyBinary(pos); }, [&] (has_attr & a) { return applyUnary(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/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 @@ -166,6 +166,16 @@ constexpr std::array xpFeatureDetails may confuse external tooling. )", }, + { + .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", 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 */