libexpr: experimental pipe operators

This commit is contained in:
Ryan Hendrickson 2024-07-24 13:17:28 -04:00
parent 874c1bdbbf
commit e086d5d899
10 changed files with 174 additions and 9 deletions

View File

@ -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

View File

@ -26,13 +26,17 @@
| Logical conjunction (`AND`) | *bool* `&&` *bool* | left | 12 | | Logical conjunction (`AND`) | *bool* `&&` *bool* | left | 12 |
| Logical disjunction (`OR`) | *bool* <code>\|\|</code> *bool* | left | 13 | | Logical disjunction (`OR`) | *bool* <code>\|\|</code> *bool* | left | 13 |
| [Logical implication] | *bool* `->` *bool* | right | 14 | | [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 [string]: ./types.md#type-string
[path]: ./types.md#type-path [path]: ./types.md#type-path
[number]: ./types.md#type-float <!-- TODO(@rhendric, #10970): rationalize this --> [number]: ./types.md#type-float
[list]: ./types.md#list [list]: ./types.md#list
[attribute set]: ./types.md#attribute-set [attribute set]: ./types.md#attribute-set
<!-- TODO(@rhendric, #10970): ^ rationalize number -> int/float -->
## Attribute selection ## Attribute selection
> **Syntax** > **Syntax**
@ -182,3 +186,34 @@ Equivalent to `!`*b1* `||` *b2*.
[Logical implication]: #logical-implication [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

View File

@ -67,6 +67,14 @@ static StringToken unescapeStr(SymbolTable & symbols, char * s, size_t length)
return {result, size_t(t - result)}; 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 IMPL; }
\/\/ { return UPDATE; } \/\/ { return UPDATE; }
\+\+ { return CONCAT; } \+\+ { 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; } {ID} { yylval->id = {yytext, (size_t) yyleng}; return ID; }
{INT} { errno = 0; {INT} { errno = 0;

View File

@ -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<ExprCall *>(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 <e> start expr expr_function expr_if expr_op %type <e> start expr expr_function expr_if expr_op
%type <e> expr_select expr_simple expr_app %type <e> expr_select expr_simple expr_app
%type <e> expr_pipe_from expr_pipe_into
%type <list> expr_list %type <list> expr_list
%type <attrs> binds %type <attrs> binds
%type <formals> formals %type <formals> formals
@ -140,6 +149,7 @@ static void setDocPosition(const LexerState & lexerState, ExprLambda * lambda, P
%token <path> PATH HPATH SPATH PATH_END %token <path> PATH HPATH SPATH PATH_END
%token <uri> URI %token <uri> URI
%token IF THEN ELSE ASSERT WITH LET IN_KW REC INHERIT EQ NEQ AND OR IMPL OR_KW %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 DOLLAR_CURLY /* == ${ */
%token IND_STRING_OPEN IND_STRING_CLOSE %token IND_STRING_OPEN IND_STRING_CLOSE
%token ELLIPSIS %token ELLIPSIS
@ -206,9 +216,21 @@ expr_function
expr_if expr_if
: IF expr THEN expr ELSE expr { $$ = new ExprIf(CUR_POS, $2, $4, $6); } : IF expr THEN expr ELSE expr { $$ = new ExprIf(CUR_POS, $2, $4, $6); }
| expr_pipe_from
| expr_pipe_into
| expr_op | 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
: '!' expr_op %prec NOT { $$ = new ExprOpNot($2); } : '!' expr_op %prec NOT { $$ = new ExprOpNot($2); }
| '-' expr_op %prec NEGATE { $$ = new ExprCall(CUR_POS, new ExprVar(state->s.sub), {new ExprInt(0), $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_app expr_select { : expr_app expr_select { $$ = makeCall(CUR_POS, $1, $2); }
if (auto e2 = dynamic_cast<ExprCall *>($1)) {
e2->args.push_back($2);
$$ = $1;
} else
$$ = new ExprCall(CUR_POS, $1, {$2});
}
| expr_select | expr_select
; ;

View File

@ -24,7 +24,7 @@ struct ExperimentalFeatureDetails
* feature, we either have no issue at all if few features are not added * 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. * at the end of the list, or a proper merge conflict if they are.
*/ */
constexpr size_t numXpFeatures = 1 + static_cast<size_t>(Xp::VerifiedFetches); constexpr size_t numXpFeatures = 1 + static_cast<size_t>(Xp::PipeOperators);
constexpr std::array<ExperimentalFeatureDetails, numXpFeatures> xpFeatureDetails = {{ constexpr std::array<ExperimentalFeatureDetails, numXpFeatures> xpFeatureDetails = {{
{ {
@ -294,6 +294,14 @@ constexpr std::array<ExperimentalFeatureDetails, numXpFeatures> xpFeatureDetails
)", )",
.trackingUrl = "https://github.com/NixOS/nix/milestone/48", .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( static_assert(

View File

@ -35,6 +35,7 @@ enum struct ExperimentalFeature
ConfigurableImpureEnv, ConfigurableImpureEnv,
MountedSSHStore, MountedSSHStore,
VerifiedFetches, VerifiedFetches,
PipeOperators,
}; };
/** /**

View File

@ -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|

View File

@ -0,0 +1 @@
1 |> 2

View File

@ -34,6 +34,9 @@ int main (int argc, char **argv) {
setEnv("_NIX_TEST_NO_SANDBOX", "1"); setEnv("_NIX_TEST_NO_SANDBOX", "1");
#endif #endif
// For pipe operator tests in trivial.cc
experimentalFeatureSettings.set("experimental-features", "pipe-operators");
::testing::InitGoogleTest(&argc, argv); ::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS(); return RUN_ALL_TESTS();
} }

View File

@ -182,6 +182,60 @@ namespace nix {
ASSERT_THAT(v, IsIntEq(15)); 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) { TEST_F(TrivialExpressionTest, bindOr) {
auto v = eval("{ or = 1; }"); auto v = eval("{ or = 1; }");
ASSERT_THAT(v, IsAttrsOfSize(1)); ASSERT_THAT(v, IsAttrsOfSize(1));