Skip to content

Commit

Permalink
libexpr: experimental pipe operators
Browse files Browse the repository at this point in the history
  • Loading branch information
rhendric committed Jul 17, 2024
1 parent 621c23b commit 0f0803b
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 9 deletions.
26 changes: 26 additions & 0 deletions doc/manual/rl-next/pipe-operators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
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 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 for more examples and rationale.
27 changes: 26 additions & 1 deletion doc/manual/src/language/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@
| Logical conjunction (`AND`) | *bool* `&&` *bool* | left | 12 |
| Logical disjunction (`OR`) | *bool* <code>\|\|</code> *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 <!-- TODO(@rhendric, #10970): rationalize this -->
[number]: ./types.md#type-float
[list]: ./types.md#list
[attribute set]: ./types.md#attribute-set

<!-- TODO(@rhendric, #10970): ^ rationalize number -> int/float -->

## Attribute selection

> **Syntax**
Expand Down Expand Up @@ -182,3 +186,24 @@ Equivalent to `!`*b1* `||` *b2*.

[Logical implication]: #logical-implication

## Pipe operators

- *a* `|>` *b* is equivalent to *b* *a*
- *a* `<|` *b* is equivalent to *a* *b*

> **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
14 changes: 14 additions & 0 deletions src/libexpr/lexer.l
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}

}

Expand Down Expand Up @@ -119,6 +127,12 @@ or { return OR_KW; }
\-\> { return IMPL; }
\/\/ { return UPDATE; }
\+\+ { return CONCAT; }
\<\| { requireExperimentalFeature(Xp::PipeOperators, state->positions[CUR_POS]);
return APPLY;
}
\|\> { requireExperimentalFeature(Xp::PipeOperators, state->positions[CUR_POS]);
return APPLY_FLIPPED;
}

{ID} { yylval->id = {yytext, (size_t) yyleng}; return ID; }
{INT} { errno = 0;
Expand Down
30 changes: 23 additions & 7 deletions src/libexpr/parser.y
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}


%}

Expand All @@ -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> expr_select expr_simple expr_app
%type <e> expr_apply expr_apply_flipped
%type <list> expr_list
%type <attrs> binds
%type <formals> formals
Expand All @@ -140,6 +149,7 @@ static void setDocPosition(const LexerState & lexerState, ExprLambda * lambda, P
%token <path> PATH HPATH SPATH PATH_END
%token <uri> URI
%token IF THEN ELSE ASSERT WITH LET IN_KW REC INHERIT EQ NEQ AND OR IMPL OR_KW
%token APPLY APPLY_FLIPPED /* <| and |> */
%token DOLLAR_CURLY /* == ${ */
%token IND_STRING_OPEN IND_STRING_CLOSE
%token ELLIPSIS
Expand Down Expand Up @@ -206,9 +216,21 @@ expr_function

expr_if
: IF expr THEN expr ELSE expr { $$ = new ExprIf(CUR_POS, $2, $4, $6); }
| expr_apply
| expr_apply_flipped
| expr_op
;

expr_apply
: expr_op APPLY expr_apply { $$ = makeCall(state->at(@2), $1, $3); }
| expr_op APPLY expr_op { $$ = makeCall(state->at(@2), $1, $3); }
;

expr_apply_flipped
: expr_apply_flipped APPLY_FLIPPED expr_op { $$ = makeCall(state->at(@2), $3, $1); }
| expr_op APPLY_FLIPPED 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}); }
Expand All @@ -233,13 +255,7 @@ expr_op
;

expr_app
: expr_app expr_select {
if (auto e2 = dynamic_cast<ExprCall *>($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
;

Expand Down
10 changes: 9 additions & 1 deletion src/libutil/experimental-features.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<size_t>(Xp::VerifiedFetches);
constexpr size_t numXpFeatures = 1 + static_cast<size_t>(Xp::PipeOperators);

constexpr std::array<ExperimentalFeatureDetails, numXpFeatures> xpFeatureDetails = {{
{
Expand Down Expand Up @@ -294,6 +294,14 @@ constexpr std::array<ExperimentalFeatureDetails, numXpFeatures> 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(
Expand Down
1 change: 1 addition & 0 deletions src/libutil/experimental-features.hh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ enum struct ExperimentalFeature
ConfigurableImpureEnv,
MountedSSHStore,
VerifiedFetches,
PipeOperators,
};

/**
Expand Down
5 changes: 5 additions & 0 deletions tests/functional/lang/eval-fail-pipe-operators.err.exp
Original file line number Diff line number Diff line change
@@ -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|
1 change: 1 addition & 0 deletions tests/functional/lang/eval-fail-pipe-operators.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1 |> 2
3 changes: 3 additions & 0 deletions tests/unit/libexpr/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
54 changes: 54 additions & 0 deletions tests/unit/libexpr/trivial.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down

0 comments on commit 0f0803b

Please sign in to comment.