diff --git a/Changelog.md b/Changelog.md index 52c23e6e38d..07b74ff344b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,27 @@ * motoko (`moc`) + * Added pipe operator ` |> ` and placeholder expression `_` (#3987). + For example: + + ``` motoko + Iter.range(0, 10) |> + Iter.toList _ |> + List.filter(_, func n { n % 3 == 0 }) |> + { multiples = _ }; + ``` + + may, according to taste, be a more readable rendition of: + + ``` motoko + { multiples = + List.filter( + Iter.toList(Iter.range(0, 10)), + func n { n % 3 == 0 }) }; + ``` + + However, beware the change of evaluation order for code with side-effects. + * BREAKING CHANGE (Minor): New keyword `composite` allows one to declare Internet Computer *composite queries* (#4003). diff --git a/doc/md/examples/Piped.mo b/doc/md/examples/Piped.mo new file mode 100644 index 00000000000..a88ed7df7f5 --- /dev/null +++ b/doc/md/examples/Piped.mo @@ -0,0 +1,7 @@ +import Iter "mo:base/Iter"; +import List "mo:base/List"; + +Iter.range(0, 10) |> + Iter.toList _ |> + List.filter(_, func n { n % 3 == 0 }) |> + { multiples = _ }; diff --git a/doc/md/examples/Unpiped.mo b/doc/md/examples/Unpiped.mo new file mode 100644 index 00000000000..cb70631dca0 --- /dev/null +++ b/doc/md/examples/Unpiped.mo @@ -0,0 +1,7 @@ +import Iter "mo:base/Iter"; +import List "mo:base/List"; + +{ multiples = + List.filter( + Iter.toList(Iter.range(0, 10)), + func n { n % 3 == 0 }) }; diff --git a/doc/md/examples/grammar.txt b/doc/md/examples/grammar.txt index 2d791878427..d4eaa41cebc 100644 --- a/doc/md/examples/grammar.txt +++ b/doc/md/examples/grammar.txt @@ -157,6 +157,7 @@ + '_' ::= @@ -188,6 +189,7 @@ 'and' 'or' ':' + '|>' ::= diff --git a/doc/md/language-manual.md b/doc/md/language-manual.md index 25e35fef778..df13027bc17 100644 --- a/doc/md/language-manual.md +++ b/doc/md/language-manual.md @@ -53,21 +53,21 @@ In the definition of some lexemes, the quick reference uses the symbol `␣` to Single line comments are all characters following `//` until the end of the same line. -``` motoko +``` motoko no-repl // single line comment x = 1 ``` Single or multi-line comments are any sequence of characters delimited by `/*` and `*/`: -``` motoko +``` motoko no-repl /* multi-line comments look like this, as in C and friends */ ``` Comments delimited by `/*` and `*/` may be nested, provided the nesting is well-bracketed. -``` motoko +``` motoko no-repl /// I'm a documentation comment /// for a function ``` @@ -217,7 +217,6 @@ Some types have several categories. For example, type `Int` is both arithmetic ( | `-` | A | numeric negation | | `+` | A | numeric identity | | `^` | B | bitwise negation | -| `!` | | null break | ### Relational operators @@ -304,6 +303,7 @@ The following table defines the relative precedence and associativity of operato | (higher) | none | `else`, `while` | | (higher) | right | `:=`, `+=`, `-=`, `*=`, `/=`, `%=`, `**=`, `#=`, `&=`, `\|=`, `^=`, `<<=`, `>>=`, `<<>=`, `<>>=`, `+%=`, `-%=`, `*%=`, `**%=` | | (higher) | left | `:` | +| (higher) | left | `|>` | | (higher) | left | `or` | | (higher) | left | `and` | | (higher) | none | `==`, `!=`, `<`, `>`, `<=`, `>`, `>=` | @@ -469,6 +469,8 @@ The syntax of an *expression* is as follows: unary operator binary operator binary relational operator + _ placeholder expression + |> pipe operator ( ,* ) tuple . tuple projection ? option injection @@ -1769,6 +1771,63 @@ Otherwise, `r1` and `r2` are values `v1` and `v2` and the expression returns the For equality and inequality, the meaning of `v1 v2` depends on the compile-time, static choice of `T` (not the run-time types of `v1` and `v2`, which, due to subtyping, may be more precise). +### Pipe operators and placeholder expressions + +The pipe expression ` |> ` binds the value of `` to the special placeholder expression `_`, that can be referenced in `` (and recursively in ``). +Referencing the placeholder expression outside of a pipe operation is a compile-time error. + +The pipe expression ` |> ` is just syntactic sugar for a `let` binding to a +placeholder identifier, `p`: + +``` bnf +do { let p = ; } +``` + +The placeholder expression `_` is just syntactic sugar for the expression referencing the placeholder identifier: + +``` bnf +p +``` + +The placeholder identifier, `p`, is a fixed, reserved identifier that cannot be bound by any other expression or pattern other than a pipe operation, +and can only be referenced using the placeholder expression `_`. + +`|>` has lowest precedence amongst all operators except `:` and associates to the left. + +Judicious use of the pipe operator allows one to express a more complicated nested expression by piping arguments of that expression into their nested positions within that expression. + +For example: + +``` motoko no-repl +Iter.range(0, 10) |> + Iter.toList _ |> + List.filter(_, func n { n % 3 == 0 }) |> + { multiples = _ }; +``` + +may, according to taste, be a more readable rendition of: + +``` motoko no-repl +{ multiples = + List.filter( + Iter.toList(Iter.range(0, 10)), + func n { n % 3 == 0 }) }; +``` + +Above, each occurence of `_` refers to the value of the left-hand-size of the nearest enclosing +pipe operation, after associating nested pipes to the left. + +Note that the evaluation order of the two examples is different, but consistently left-to-right. + +:::note + +Although syntactically identical, the placeholder expression is +semantically distinct from, and should not be confused with, the wildcard pattern `_`. +Occurrences of the forms can be distinguished by their syntactic role as pattern or +expression. + +::: + ### Tuples Tuple expression `(, …​, )` has tuple type `(T1, …​, Tn)`, provided ``, …​, `` have types `T1`, …​, `Tn`. diff --git a/doc/md/pipes.md b/doc/md/pipes.md new file mode 100644 index 00000000000..89c42c98f5b --- /dev/null +++ b/doc/md/pipes.md @@ -0,0 +1,46 @@ +# Piping values into expressions + +It can sometimes be hard to read deeply nested expressions involving several function applications. + +``` motoko file=./examples/Unpiped.mo#L1-L8 +``` + +This expression take the range of numbers `0`..`10`, converts it to a list, filters the list for multiples of three and returns a record containing the result. + +To make such expressions more readable, you can use Motoko's pipe operator ` |> `. +The operator evaluates the first argument ``, and lets you refer to its value in `` using the special placeholder expression `_`. + +Using this, you can write the former expression as: + +``` motoko file=./examples/Piped.mo#L1-L8 +``` + +Now, the textual order of operations corresponds to our English explanation above: "this expression takes the range of numbers `0`..`10`, converts it to a list, filters the list for multiples of three and returns a record containing the result". + + +The pipe expression ` |> ` is just syntactic sugar for the following block binding `` to a reserved +placeholder identifier, `p`, before returning ``: + +``` bnf +do { let p = ; } +``` + +The otherwise inaccessible placeholder identifier `p` can only referenced by the placeholder expression `_`. +Multiple references to `_` are allowed and refer to the same value within the same pipe operation. + + +Note that using `_` as an expression outside of a pipe operation, where it is undefined, is an error. + +For example: + +``` motoko +let x = _; +``` + +produces the compile-time error "type error [M0057], unbound variable _". + +(Internally, the compiler uses the reserved identifier `_` as the name for the placeholder called `p` above, so this `let` is just referencing an undefined variable). + + +See [here](language-manual.md#pipe-operators-and-placeholder-expressions) for more details. + diff --git a/src/gen-grammar/grammar.sed b/src/gen-grammar/grammar.sed index 209e5f7433b..66ca48c061f 100644 --- a/src/gen-grammar/grammar.sed +++ b/src/gen-grammar/grammar.sed @@ -73,6 +73,7 @@ s/RBRACKET/\']\'/g s/QUEST/\'?\'/g s/BANG/\'!\'/g s/QUERY/\'query\'/g +s/PIPE/\'|>\'/g s/PUBLIC/\'public\'/g s/PRIVATE/\'private\'/g s/POWOP/\'**\'/g diff --git a/src/mo_frontend/error_reporting.ml b/src/mo_frontend/error_reporting.ml index 6cbf1ebdfe4..78cf5e56ffe 100644 --- a/src/mo_frontend/error_reporting.ml +++ b/src/mo_frontend/error_reporting.ml @@ -132,3 +132,4 @@ let terminal2token (type a) (symbol : a terminal) : token = | T_WRAPSUBASSIGN -> WRAPSUBASSIGN | T_WRAPMULASSIGN -> WRAPMULASSIGN | T_WRAPPOWASSIGN -> WRAPPOWASSIGN + | T_PIPE -> PIPE diff --git a/src/mo_frontend/parser.mly b/src/mo_frontend/parser.mly index d941c3c64f0..abf7ecfba06 100644 --- a/src/mo_frontend/parser.mly +++ b/src/mo_frontend/parser.mly @@ -233,6 +233,7 @@ and objblock s dec_fields = %token BOOL %token ID %token TEXT +%token PIPE %token PRIM %token UNDERSCORE %token COMPOSITE @@ -243,6 +244,7 @@ and objblock s dec_fields = %nonassoc ELSE WHILE %left COLON +%left PIPE %left OR %left AND %nonassoc EQOP NEQOP LEOP LTOP GTOP GEOP @@ -579,6 +581,8 @@ exp_nullary(B) : { VarE(x) @? at $sloc } | PRIM s=TEXT { PrimE(s) @? at $sloc } + | UNDERSCORE + { VarE ("_" @@ at $sloc) @? at $sloc } exp_post(B) : | e=exp_nullary(B) @@ -642,6 +646,13 @@ exp_un(B) : { OrE(e1, e2) @? at $sloc } | e=exp_bin(B) COLON t=typ_nobin { AnnotE(e, t) @? at $sloc } + | e1=exp_bin(B) PIPE e2=exp_bin(ob) + { let x = "_" @@ e1.at in + BlockE [ + LetD (VarP x @! x.at, e1, None) @? e1.at; + ExpD e2 @? e2.at + ] @? at $sloc } + %public exp_nondec(B) : | e=exp_bin(B) diff --git a/src/mo_frontend/printers.ml b/src/mo_frontend/printers.ml index b68799c040a..3e114afed0d 100644 --- a/src/mo_frontend/printers.ml +++ b/src/mo_frontend/printers.ml @@ -139,6 +139,7 @@ let string_of_symbol = function | X (T T_ADDOP) -> unop "+" | X (T T_ACTOR) -> "actor" | X (T T_INVARIANT) -> "invariant" + | X (T T_PIPE) -> "|>" (* non-terminals *) | X (N N_bl) -> "" | X (N N_case) -> "" diff --git a/src/mo_frontend/source_lexer.mll b/src/mo_frontend/source_lexer.mll index 5d6c8fd310f..2e57329d2c6 100644 --- a/src/mo_frontend/source_lexer.mll +++ b/src/mo_frontend/source_lexer.mll @@ -139,6 +139,7 @@ rule token mode = parse | "**" { POWOP } | "&" { ANDOP } | "|" { OROP } + | "|>" { PIPE } | "^" { XOROP } | "<<" { SHLOP } | "<<>" { ROTLOP } diff --git a/src/mo_frontend/source_token.ml b/src/mo_frontend/source_token.ml index c0a0926797b..01568a63e07 100644 --- a/src/mo_frontend/source_token.ml +++ b/src/mo_frontend/source_token.ml @@ -126,6 +126,7 @@ type token = | SPACE of int | TAB of int (* shudders *) | COMMENT of string + | PIPE let to_parser_token : token -> (Parser.token, line_feed trivia) result = function @@ -248,6 +249,7 @@ let to_parser_token : | UNDERSCORE -> Ok Parser.UNDERSCORE | COMPOSITE -> Ok Parser.COMPOSITE | INVARIANT -> Ok Parser.INVARIANT + | PIPE -> Ok Parser.PIPE (*Trivia *) | SINGLESPACE -> Error (Space 1) | SPACE n -> Error (Space n) @@ -379,6 +381,7 @@ let string_of_parser_token = function | Parser.INVARIANT -> "INVARIANT" | Parser.IMPLIES -> "IMPLIES" | Parser.OLD -> "OLD" + | Parser.PIPE -> "PIPE" let is_lineless_trivia : token -> void trivia option = function | SINGLESPACE -> Some (Space 1) diff --git a/test/fail/ok/pipe-ill-typed.tc.ok b/test/fail/ok/pipe-ill-typed.tc.ok new file mode 100644 index 00000000000..0b805740c95 --- /dev/null +++ b/test/fail/ok/pipe-ill-typed.tc.ok @@ -0,0 +1,12 @@ +pipe-ill-typed.mo:7.33-7.34: type error [M0096], expression of type + {#B} +cannot produce expected type + {#A} +pipe-ill-typed.mo:11.37-11.38: type error [M0096], expression of type + {#A} +cannot produce expected type + {#B} +pipe-ill-typed.mo:16.33-16.34: type error [M0096], expression of type + {#A} +cannot produce expected type + (A__9, B) diff --git a/test/fail/ok/pipe-ill-typed.tc.ret.ok b/test/fail/ok/pipe-ill-typed.tc.ret.ok new file mode 100644 index 00000000000..69becfa16f9 --- /dev/null +++ b/test/fail/ok/pipe-ill-typed.tc.ret.ok @@ -0,0 +1 @@ +Return code 1 diff --git a/test/fail/ok/syntax2.tc.ok b/test/fail/ok/syntax2.tc.ok index d475b13c05a..dd5b1ecf199 100644 --- a/test/fail/ok/syntax2.tc.ok +++ b/test/fail/ok/syntax2.tc.ok @@ -5,6 +5,7 @@ syntax2.mo:2.1-2.4: syntax error [M0001], unexpected token 'let', expected one o ; seplist(,) + |> or implies diff --git a/test/fail/ok/syntax3.tc.ok b/test/fail/ok/syntax3.tc.ok index 028eebd7729..130c9403e6a 100644 --- a/test/fail/ok/syntax3.tc.ok +++ b/test/fail/ok/syntax3.tc.ok @@ -4,6 +4,7 @@ syntax3.mo:1.3-1.4: syntax error [M0001], unexpected token ';', expected one of ! + |> or implies diff --git a/test/fail/ok/syntax5.tc.ok b/test/fail/ok/syntax5.tc.ok index 95550080467..a882586ced3 100644 --- a/test/fail/ok/syntax5.tc.ok +++ b/test/fail/ok/syntax5.tc.ok @@ -4,6 +4,7 @@ syntax5.mo:3.1: syntax error [M0001], unexpected end of input, expected one of t ! + |> or implies diff --git a/test/fail/pipe-ill-typed.mo b/test/fail/pipe-ill-typed.mo new file mode 100644 index 00000000000..17a3412ae4b --- /dev/null +++ b/test/fail/pipe-ill-typed.mo @@ -0,0 +1,17 @@ +type A = {#A}; +type B = {#B}; + +func f2(x1 : T1, x2 : T2) : (T1, T2) { (x1, x2) }; + +do { + let (#A, #B) = #B |> f2(_, #B); // type error +}; + +do { + let (#A, #B) = #A |> f2(#A, _); // type error +}; + + +do { + let (#A, #B) = #A |> f2 _; // type error +}; diff --git a/test/run/ok/pipes.run-ir.ok b/test/run/ok/pipes.run-ir.ok new file mode 100644 index 00000000000..bb588193f4d --- /dev/null +++ b/test/run/ok/pipes.run-ir.ok @@ -0,0 +1,23 @@ +1: +#A +f +#B +2: +#B +f +#A +3: +#A +f +#B +#C +4: +#B +f +#A +#C +5: +#C +f +#A +#B diff --git a/test/run/ok/pipes.run-low.ok b/test/run/ok/pipes.run-low.ok new file mode 100644 index 00000000000..bb588193f4d --- /dev/null +++ b/test/run/ok/pipes.run-low.ok @@ -0,0 +1,23 @@ +1: +#A +f +#B +2: +#B +f +#A +3: +#A +f +#B +#C +4: +#B +f +#A +#C +5: +#C +f +#A +#B diff --git a/test/run/ok/pipes.run.ok b/test/run/ok/pipes.run.ok new file mode 100644 index 00000000000..bb588193f4d --- /dev/null +++ b/test/run/ok/pipes.run.ok @@ -0,0 +1,23 @@ +1: +#A +f +#B +2: +#B +f +#A +3: +#A +f +#B +#C +4: +#B +f +#A +#C +5: +#C +f +#A +#B diff --git a/test/run/ok/pipes.tc.ok b/test/run/ok/pipes.tc.ok new file mode 100644 index 00000000000..38c38904268 --- /dev/null +++ b/test/run/ok/pipes.tc.ok @@ -0,0 +1,8 @@ +pipes.mo:76.5-76.8: warning [M0145], this pattern of type + Nat32 +does not cover value + 0 or 1 or _ +pipes.mo:78.5-78.8: warning [M0145], this pattern of type + Nat32 +does not cover value + 0 or 1 or _ diff --git a/test/run/ok/pipes.wasm-run.ok b/test/run/ok/pipes.wasm-run.ok new file mode 100644 index 00000000000..bb588193f4d --- /dev/null +++ b/test/run/ok/pipes.wasm-run.ok @@ -0,0 +1,23 @@ +1: +#A +f +#B +2: +#B +f +#A +3: +#A +f +#B +#C +4: +#B +f +#A +#C +5: +#C +f +#A +#B diff --git a/test/run/pipes.mo b/test/run/pipes.mo new file mode 100644 index 00000000000..57fa1f12025 --- /dev/null +++ b/test/run/pipes.mo @@ -0,0 +1,115 @@ +import Prim "mo:⛔"; + +type A = {#A}; +type B = {#B}; +type C = {#C}; +func f0() {}; +func f1(x : T) : T { x }; +func f2(x1 : T1, x2 : T2) : (T1, T2) { (x1, x2) }; +func f3(x1 : T1, x2 : T2, x3 : T3) : (T1, T2, T3) { (x1, x2, x3) }; + + +() |> f0 _; + +() |> f0 (_); + +let (#A, #B) = #A |> f2(_, #B); +let (#A, #B) = #B |> f2(#A, _); +let (#A, #B, #C) = #A |> f3(_, #B, #C); +let (#A, #B, #C) = #B |> f3(#A, _, #C); +let (#A, #B, #C) = #C |> f3(#A, #B, _); + +/* left associative nesting */ + +let ((#A, #B),#C) = #A |> f2(_, #B) |> f2(_, #C); +let (#C,(#A, #B)) = #A |> f2(_, #B) |> f2(#C, _); + + +let ((#A, #B),#C) = (#A |> f2(_, #B)) |> f2(_, #C); +let (#C,(#A, #B)) = (#A |> f2(_, #B)) |> f2(#C, _); + +/* eval order */ + +func A() : A { Prim.debugPrint(debug_show(#A)); #A }; +func B() : B { Prim.debugPrint(debug_show(#B)); #B }; +func C() : C { Prim.debugPrint(debug_show(#C)); #C }; + +func f(f : F) : F { + Prim.debugPrint("f"); f +}; + +Prim.debugPrint("1:"); +let (#A, #B) = A() |> (f f2)(_, B()); +Prim.debugPrint("2:"); +let (#A, #B) = B() |> (f f2)(A(), _); +Prim.debugPrint("3:"); +let (#A, #B, #C) = A() |> (f f3)(_, B(), C()); +Prim.debugPrint("4:"); +let (#A, #B, #C) = B() |> (f f3)(A(), _, C()); +Prim.debugPrint("5:"); +let (#A, #B, #C) = C() |> (f f3)(A(), B(), _); + +/* contrived example */ + +type Iter = { next : () -> ?T }; + +func sum(ns : Iter) : Nat32 { + var sum : Nat32 = 0; + for (n in ns) { sum += n }; + sum + }; + +func map(xs : Iter, f : A -> B) : Iter = object { + public func next() : ?B { + switch (xs.next()) { + case (?next) { + ?f(next) + }; + case (null) { + null + } + } + } + }; + + +let 532 = "hello".chars() |> map(_, Prim.charToNat32) |> sum _; + +let 532 = Prim.charToNat32 |> map("hello".chars(), _) |> sum (_); + + +/* eval order, continued */ + +do { + // check eval order preserved, even for piped lvalues + var i = 0; + func f() : Nat -> Nat { + i := 1; + func (j : Nat) : Nat { j }; + }; + + // should read i before executing f() + assert (i |> (f())(_) == 0); +}; + +/* option blocks */ + + +let _ = do ? { + let none = null; + (none |> f1(_)) ! +}; + +let _ = do ? { + let some = ??(); + (some ! |> f1(_))!; +}; + + +/* non-linear, free-form piping */ + +let five = 2 |> _ * _ |> _ + 1; + +assert (five == 5); + +