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 */