diff --git a/doc/manual/rl-next/pipe-operators.md b/doc/manual/rl-next/pipe-operators.md
new file mode 100644
index 000000000..b4cbe30e3
--- /dev/null
+++ b/doc/manual/rl-next/pipe-operators.md
@@ -0,0 +1,28 @@
+---
+synopsis: "Add `pipe-operators` experimental feature"
+prs:
+- 11131
+---
+
+This is a draft implementation of [RFC 0148](https://github.com/NixOS/rfcs/pull/148).
+
+The `pipe-operators` experimental feature adds [`<|` and `|>` operators][pipe operators] to the Nix language.
+*a* `|>` *b* is equivalent to the function application *b* *a*, and
+*a* `<|` *b* is equivalent to the function application *a* *b*.
+
+For example:
+
+```
+nix-repl> 1 |> builtins.add 2 |> builtins.mul 3
+9
+
+nix-repl> builtins.add 1 <| builtins.mul 2 <| 3
+7
+```
+
+`<|` and `|>` are right and left associative, respectively, and have lower precedence than any other operator.
+These properties may change in future releases.
+
+See [the RFC](https://github.com/NixOS/rfcs/pull/148) for more examples and rationale.
+
+[pipe operators]: @docroot@/language/operators.md#pipe-operators
diff --git a/doc/manual/src/language/operators.md b/doc/manual/src/language/operators.md
index d2476c413..e96a28988 100644
--- a/doc/manual/src/language/operators.md
+++ b/doc/manual/src/language/operators.md
@@ -26,13 +26,17 @@
| Logical conjunction (`AND`) | *bool* `&&` *bool* | left | 12 |
| Logical disjunction (`OR`) | *bool* \|\|
*bool* | left | 13 |
| [Logical implication] | *bool* `->` *bool* | right | 14 |
+| [Pipe operator] (experimental) | *expr* `\|>` *func* | left | 15 |
+| [Pipe operator] (experimental) | *func* `<\|` *expr* | right | 15 |
[string]: ./types.md#type-string
[path]: ./types.md#type-path
-[number]: ./types.md#type-float
+[number]: ./types.md#type-float
[list]: ./types.md#list
[attribute set]: ./types.md#attribute-set
+
+
## Attribute selection
> **Syntax**
@@ -176,3 +180,34 @@ Equivalent to `!`*b1* `||` *b2*.
[Logical implication]: #logical-implication
+## Pipe operators
+
+- *a* `|>` *b* is equivalent to *b* *a*
+- *a* `<|` *b* is equivalent to *a* *b*
+
+> **Example**
+>
+> ```
+> nix-repl> 1 |> builtins.add 2 |> builtins.mul 3
+> 9
+>
+> nix-repl> builtins.add 1 <| builtins.mul 2 <| 3
+> 7
+> ```
+
+> **Warning**
+>
+> This syntax is part of an
+> [experimental feature](@docroot@/contributing/experimental-features.md)
+> and may change in future releases.
+>
+> To use this syntax, make sure the
+> [`pipe-operators` experimental feature](@docroot@/contributing/experimental-features.md#xp-feature-pipe-operators)
+> is enabled.
+> For example, include the following in [`nix.conf`](@docroot@/command-ref/conf-file.md):
+>
+> ```
+> extra-experimental-features = pipe-operators
+> ```
+
+[Pipe operator]: #pipe-operators
diff --git a/src/libexpr/lexer.l b/src/libexpr/lexer.l
index 58401be8e..eb1825b7c 100644
--- a/src/libexpr/lexer.l
+++ b/src/libexpr/lexer.l
@@ -67,6 +67,14 @@ static StringToken unescapeStr(SymbolTable & symbols, char * s, size_t length)
return {result, size_t(t - result)};
}
+static void requireExperimentalFeature(const ExperimentalFeature & feature, const Pos & pos)
+{
+ if (!experimentalFeatureSettings.isEnabled(feature))
+ throw ParseError(ErrorInfo{
+ .msg = HintFmt("experimental Nix feature '%1%' is disabled; add '--extra-experimental-features %1%' to enable it", showExperimentalFeature(feature)),
+ .pos = pos,
+ });
+}
}
@@ -119,6 +127,12 @@ or { return OR_KW; }
\-\> { return IMPL; }
\/\/ { return UPDATE; }
\+\+ { return CONCAT; }
+\<\| { requireExperimentalFeature(Xp::PipeOperators, state->positions[CUR_POS]);
+ return PIPE_FROM;
+ }
+\|\> { requireExperimentalFeature(Xp::PipeOperators, state->positions[CUR_POS]);
+ return PIPE_INTO;
+ }
{ID} { yylval->id = {yytext, (size_t) yyleng}; return ID; }
{INT} { errno = 0;
diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y
index 8ea176b24..9ad41c148 100644
--- a/src/libexpr/parser.y
+++ b/src/libexpr/parser.y
@@ -99,6 +99,14 @@ static void setDocPosition(const LexerState & lexerState, ExprLambda * lambda, P
}
}
+static Expr * makeCall(PosIdx pos, Expr * fn, Expr * arg) {
+ if (auto e2 = dynamic_cast(fn)) {
+ e2->args.push_back(arg);
+ return fn;
+ }
+ return new ExprCall(pos, fn, {arg});
+}
+
%}
@@ -123,6 +131,7 @@ static void setDocPosition(const LexerState & lexerState, ExprLambda * lambda, P
%type start expr expr_function expr_if expr_op
%type expr_select expr_simple expr_app
+%type expr_pipe_from expr_pipe_into
%type expr_list
%type binds
%type formals
@@ -140,6 +149,7 @@ static void setDocPosition(const LexerState & lexerState, ExprLambda * lambda, P
%token PATH HPATH SPATH PATH_END
%token URI
%token IF THEN ELSE ASSERT WITH LET IN_KW REC INHERIT EQ NEQ AND OR IMPL OR_KW
+%token PIPE_FROM PIPE_INTO /* <| and |> */
%token DOLLAR_CURLY /* == ${ */
%token IND_STRING_OPEN IND_STRING_CLOSE
%token ELLIPSIS
@@ -206,9 +216,21 @@ expr_function
expr_if
: IF expr THEN expr ELSE expr { $$ = new ExprIf(CUR_POS, $2, $4, $6); }
+ | expr_pipe_from
+ | expr_pipe_into
| expr_op
;
+expr_pipe_from
+ : expr_op PIPE_FROM expr_pipe_from { $$ = makeCall(state->at(@2), $1, $3); }
+ | expr_op PIPE_FROM expr_op { $$ = makeCall(state->at(@2), $1, $3); }
+ ;
+
+expr_pipe_into
+ : expr_pipe_into PIPE_INTO expr_op { $$ = makeCall(state->at(@2), $3, $1); }
+ | expr_op PIPE_INTO expr_op { $$ = makeCall(state->at(@2), $3, $1); }
+ ;
+
expr_op
: '!' expr_op %prec NOT { $$ = new ExprOpNot($2); }
| '-' expr_op %prec NEGATE { $$ = new ExprCall(CUR_POS, new ExprVar(state->s.sub), {new ExprInt(0), $2}); }
@@ -233,13 +255,7 @@ expr_op
;
expr_app
- : expr_app expr_select {
- if (auto e2 = dynamic_cast($1)) {
- e2->args.push_back($2);
- $$ = $1;
- } else
- $$ = new ExprCall(CUR_POS, $1, {$2});
- }
+ : expr_app expr_select { $$ = makeCall(CUR_POS, $1, $2); }
| expr_select
;
diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc
index 1c080e372..a0c955816 100644
--- a/src/libutil/experimental-features.cc
+++ b/src/libutil/experimental-features.cc
@@ -24,7 +24,7 @@ struct ExperimentalFeatureDetails
* feature, we either have no issue at all if few features are not added
* at the end of the list, or a proper merge conflict if they are.
*/
-constexpr size_t numXpFeatures = 1 + static_cast(Xp::VerifiedFetches);
+constexpr size_t numXpFeatures = 1 + static_cast(Xp::PipeOperators);
constexpr std::array xpFeatureDetails = {{
{
@@ -294,6 +294,14 @@ constexpr std::array xpFeatureDetails
)",
.trackingUrl = "https://github.com/NixOS/nix/milestone/48",
},
+ {
+ .tag = Xp::PipeOperators,
+ .name = "pipe-operators",
+ .description = R"(
+ Add `|>` and `<|` operators to the Nix language.
+ )",
+ .trackingUrl = "https://github.com/NixOS/nix/milestone/55",
+ },
}};
static_assert(
diff --git a/src/libutil/experimental-features.hh b/src/libutil/experimental-features.hh
index 6ffbc0c10..e65e51280 100644
--- a/src/libutil/experimental-features.hh
+++ b/src/libutil/experimental-features.hh
@@ -35,6 +35,7 @@ enum struct ExperimentalFeature
ConfigurableImpureEnv,
MountedSSHStore,
VerifiedFetches,
+ PipeOperators,
};
/**
diff --git a/tests/functional/lang/eval-fail-pipe-operators.err.exp b/tests/functional/lang/eval-fail-pipe-operators.err.exp
new file mode 100644
index 000000000..49f3fa8ad
--- /dev/null
+++ b/tests/functional/lang/eval-fail-pipe-operators.err.exp
@@ -0,0 +1,5 @@
+error: experimental Nix feature 'pipe-operators' is disabled; add '--extra-experimental-features pipe-operators' to enable it
+ at /pwd/lang/eval-fail-pipe-operators.nix:1:3:
+ 1| 1 |> 2
+ | ^
+ 2|
diff --git a/tests/functional/lang/eval-fail-pipe-operators.nix b/tests/functional/lang/eval-fail-pipe-operators.nix
new file mode 100644
index 000000000..433e0fd7f
--- /dev/null
+++ b/tests/functional/lang/eval-fail-pipe-operators.nix
@@ -0,0 +1 @@
+1 |> 2
diff --git a/tests/unit/libexpr/main.cc b/tests/unit/libexpr/main.cc
index cf7fcf5a3..e3412d9ef 100644
--- a/tests/unit/libexpr/main.cc
+++ b/tests/unit/libexpr/main.cc
@@ -34,6 +34,9 @@ int main (int argc, char **argv) {
setEnv("_NIX_TEST_NO_SANDBOX", "1");
#endif
+ // For pipe operator tests in trivial.cc
+ experimentalFeatureSettings.set("experimental-features", "pipe-operators");
+
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
diff --git a/tests/unit/libexpr/trivial.cc b/tests/unit/libexpr/trivial.cc
index 61ea71a0f..e455a571b 100644
--- a/tests/unit/libexpr/trivial.cc
+++ b/tests/unit/libexpr/trivial.cc
@@ -182,6 +182,60 @@ namespace nix {
ASSERT_THAT(v, IsIntEq(15));
}
+ TEST_F(TrivialExpressionTest, forwardPipe) {
+ auto v = eval("1 |> builtins.add 2 |> builtins.mul 3");
+ ASSERT_THAT(v, IsIntEq(9));
+ }
+
+ TEST_F(TrivialExpressionTest, backwardPipe) {
+ auto v = eval("builtins.add 1 <| builtins.mul 2 <| 3");
+ ASSERT_THAT(v, IsIntEq(7));
+ }
+
+ TEST_F(TrivialExpressionTest, forwardPipeEvaluationOrder) {
+ auto v = eval("1 |> null |> (x: 2)");
+ ASSERT_THAT(v, IsIntEq(2));
+ }
+
+ TEST_F(TrivialExpressionTest, backwardPipeEvaluationOrder) {
+ auto v = eval("(x: 1) <| null <| 2");
+ ASSERT_THAT(v, IsIntEq(1));
+ }
+
+ TEST_F(TrivialExpressionTest, differentPipeOperatorsDoNotAssociate) {
+ ASSERT_THROW(eval("(x: 1) <| 2 |> (x: 3)"), ParseError);
+ }
+
+ TEST_F(TrivialExpressionTest, differentPipeOperatorsParensLeft) {
+ auto v = eval("((x: 1) <| 2) |> (x: 3)");
+ ASSERT_THAT(v, IsIntEq(3));
+ }
+
+ TEST_F(TrivialExpressionTest, differentPipeOperatorsParensRight) {
+ auto v = eval("(x: 1) <| (2 |> (x: 3))");
+ ASSERT_THAT(v, IsIntEq(1));
+ }
+
+ TEST_F(TrivialExpressionTest, forwardPipeLowestPrecedence) {
+ auto v = eval("false -> true |> (x: !x)");
+ ASSERT_THAT(v, IsFalse());
+ }
+
+ TEST_F(TrivialExpressionTest, backwardPipeLowestPrecedence) {
+ auto v = eval("(x: !x) <| false -> true");
+ ASSERT_THAT(v, IsFalse());
+ }
+
+ TEST_F(TrivialExpressionTest, forwardPipeStrongerThanElse) {
+ auto v = eval("if true then 1 else 2 |> 3");
+ ASSERT_THAT(v, IsIntEq(1));
+ }
+
+ TEST_F(TrivialExpressionTest, backwardPipeStrongerThanElse) {
+ auto v = eval("if true then 1 else 2 <| 3");
+ ASSERT_THAT(v, IsIntEq(1));
+ }
+
TEST_F(TrivialExpressionTest, bindOr) {
auto v = eval("{ or = 1; }");
ASSERT_THAT(v, IsAttrsOfSize(1));