Skip to content

Commit a7c2053

Browse files
committed
Added missing curried or uncurried syntax variants; syntax_extensions.md update
1 parent b1e31fd commit a7c2053

File tree

4 files changed

+122
-40
lines changed

4 files changed

+122
-40
lines changed

lib/ppx_cd.ml

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ let translate (expr : expression) : result =
414414
( [%expr Shape.Pointwise_tern],
415415
Ast_builder.Default.pexp_extension ~loc
416416
@@ Location.error_extensionf ~loc
417-
"ppx_ocannl %%cd: expected a ternary operator, one of: %s" "where, fma" ))
417+
"ppx_ocannl %%cd: expected a ternary operator, one of: where, fma" ))
418418
in
419419
(* FIXME: collapse these (code reuse) *)
420420
let process_assign_ternop ~accu_op ~lhs ~tern_op ~rhs1 ~rhs2 ~rhs3 ?projections ~proj_in_scope
@@ -882,6 +882,19 @@ let translate (expr : expression) : result =
882882
embedded_nodes = __comment_block.Arrayjit.Assignments.embedded_nodes;
883883
}];
884884
}
885+
| [%expr
886+
[%e? { pexp_desc = Pexp_ident { txt = Lident accu_op; _ }; _ }]
887+
[%e? lhs]
888+
([%e? { pexp_desc = Pexp_ident { txt = Lident bin_op; _ }; _ }]
889+
([%e? rhs1], [%e? rhs2])
890+
~projections:[%e? projections])]
891+
| [%expr
892+
[%e? { pexp_desc = Pexp_ident { txt = Lident accu_op; _ }; _ }]
893+
[%e? lhs]
894+
([%e? { pexp_desc = Pexp_ident { txt = Lident bin_op; _ }; _ }]
895+
[%e? rhs1]
896+
[%e? rhs2]
897+
~projections:[%e? projections])]
885898
| [%expr
886899
[%e? { pexp_desc = Pexp_ident { txt = Lident accu_op; _ }; _ }]
887900
[%e? lhs]
@@ -896,6 +909,14 @@ let translate (expr : expression) : result =
896909
[%e? lhs]
897910
([%e? { pexp_desc = Pexp_ident { txt = Lident tern_op; _ }; _ }]
898911
([%e? rhs1], [%e? rhs2], [%e? rhs3])
912+
~projections:[%e? projections])]
913+
| [%expr
914+
[%e? { pexp_desc = Pexp_ident { txt = Lident accu_op; _ }; _ }]
915+
[%e? lhs]
916+
([%e? { pexp_desc = Pexp_ident { txt = Lident tern_op; _ }; _ }]
917+
[%e? rhs1]
918+
[%e? rhs2]
919+
[%e? rhs3]
899920
~projections:[%e? projections])] ->
900921
process_assign_ternop ~accu_op ~lhs ~tern_op ~rhs1 ~rhs2 ~rhs3 ~projections
901922
~proj_in_scope:true ()
@@ -908,15 +929,10 @@ let translate (expr : expression) : result =
908929
| [%expr
909930
[%e? { pexp_desc = Pexp_ident { txt = Lident accu_op; _ }; _ }]
910931
[%e? lhs]
911-
(([%e? { pexp_desc = Pexp_ident { txt = Lident un_op; _ }; _ }] [%e? rhs])
912-
~projections:[%e? projections])]
913-
| [%expr
914-
[%e? { pexp_desc = Pexp_ident { txt = Lident accu_op; _ }; _ }]
915-
[%e? lhs]
932+
(* FIXME: this was never needed as prefix operators bind tighter? *)
916933
([%e? { pexp_desc = Pexp_ident { txt = Lident un_op; _ }; _ }]
917934
([%e? rhs] ~projections:[%e? projections]))]
918935
when Hashtbl.mem unary_ops un_op ->
919-
(* Handle both un_op priority levels -- where application binds tighter and less tight. *)
920936
process_assign_unop ~accu_op ~lhs ~un_op ~rhs ~projections ~proj_in_scope:true ()
921937
| [%expr
922938
[%e? { pexp_desc = Pexp_ident { txt = Lident accu_op; _ }; _ }]
@@ -929,6 +945,13 @@ let translate (expr : expression) : result =
929945
([%e? { pexp_desc = Pexp_ident { txt = Lident bin_op; _ }; _ }]
930946
([%e? rhs1], [%e? rhs2])
931947
~logic:[%e? { pexp_desc = Pexp_constant (Pconst_string (spec, s_loc, _)); _ } as logic])]
948+
| [%expr
949+
[%e? { pexp_desc = Pexp_ident { txt = Lident accu_op; _ }; _ }]
950+
[%e? lhs]
951+
([%e? { pexp_desc = Pexp_ident { txt = Lident bin_op; _ }; _ }]
952+
[%e? rhs1]
953+
[%e? rhs2]
954+
~logic:[%e? { pexp_desc = Pexp_constant (Pconst_string (spec, s_loc, _)); _ } as logic])]
932955
| [%expr
933956
[%e? { pexp_desc = Pexp_ident { txt = Lident accu_op; _ }; _ }]
934957
[%e? lhs]
@@ -951,6 +974,14 @@ let translate (expr : expression) : result =
951974
[%e? lhs]
952975
([%e? { pexp_desc = Pexp_ident { txt = Lident tern_op; _ }; _ }]
953976
([%e? rhs1], [%e? rhs2], [%e? rhs3])
977+
~logic:[%e? { pexp_desc = Pexp_constant (Pconst_string (spec, s_loc, _)); _ }])]
978+
| [%expr
979+
[%e? { pexp_desc = Pexp_ident { txt = Lident accu_op; _ }; _ }]
980+
[%e? lhs]
981+
([%e? { pexp_desc = Pexp_ident { txt = Lident tern_op; _ }; _ }]
982+
[%e? rhs1]
983+
[%e? rhs2]
984+
[%e? rhs3]
954985
~logic:[%e? { pexp_desc = Pexp_constant (Pconst_string (spec, s_loc, _)); _ }])] ->
955986
let logic =
956987
let loc = s_loc in
@@ -973,6 +1004,13 @@ let translate (expr : expression) : result =
9731004
| [%expr
9741005
[%e? { pexp_desc = Pexp_ident { txt = Lident accu_op; _ }; _ }]
9751006
[%e? lhs]
1007+
([%e? { pexp_desc = Pexp_ident { txt = Lident unop_ident; _ }; _ }]
1008+
[%e? rhs]
1009+
~logic:[%e? { pexp_desc = Pexp_constant (Pconst_string (spec, s_loc, _)); _ } as logic])]
1010+
| [%expr
1011+
[%e? { pexp_desc = Pexp_ident { txt = Lident accu_op; _ }; _ }]
1012+
[%e? lhs]
1013+
(* FIXME: this was never needed as prefix operators bind tighter? *)
9761014
([%e? { pexp_desc = Pexp_ident { txt = Lident unop_ident; _ }; _ }]
9771015
([%e? rhs]
9781016
~logic:
@@ -1002,6 +1040,13 @@ let translate (expr : expression) : result =
10021040
[%e? lhs]
10031041
([%e? { pexp_desc = Pexp_ident { txt = Lident tern_op; _ }; _ }]
10041042
([%e? rhs1], [%e? rhs2], [%e? rhs3]))]
1043+
| [%expr
1044+
[%e? { pexp_desc = Pexp_ident { txt = Lident accu_op; _ }; _ }]
1045+
[%e? lhs]
1046+
([%e? { pexp_desc = Pexp_ident { txt = Lident tern_op; _ }; _ }]
1047+
[%e? rhs1]
1048+
[%e? rhs2]
1049+
[%e? rhs3])]
10051050
when is_assignment accu_op && Hashtbl.mem ternary_ops tern_op && proj_in_scope ->
10061051
process_assign_ternop ~accu_op ~lhs ~tern_op ~rhs1 ~rhs2 ~rhs3 ~proj_in_scope ()
10071052
| [%expr
@@ -1029,6 +1074,13 @@ let translate (expr : expression) : result =
10291074
[%e? lhs]
10301075
([%e? { pexp_desc = Pexp_ident { txt = Lident tern_op; _ }; _ }]
10311076
([%e? rhs1], [%e? rhs2], [%e? rhs3]))]
1077+
| [%expr
1078+
[%e? { pexp_desc = Pexp_ident { txt = Lident accu_op; _ }; _ }]
1079+
[%e? lhs]
1080+
([%e? { pexp_desc = Pexp_ident { txt = Lident tern_op; _ }; _ }]
1081+
[%e? rhs1]
1082+
[%e? rhs2]
1083+
[%e? rhs3])]
10321084
when is_assignment accu_op && Hashtbl.mem ternary_ops tern_op ->
10331085
let logic, tern_op = ternary_op tern_op in
10341086
process_raw_ternop ~accu_op ~lhs ~tern_op ~rhs1 ~rhs2 ~rhs3 ~logic

lib/ppx_op.ml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,15 @@ let rec translate ~num_configs ~is_toplevel ~has_config ?label expr =
164164
let vbs2, e2 = loop expr2 in
165165
let vbs3, e3 = loop expr3 in
166166
(reduce_vbss [ vbs2; vbs3 ], [%expr [%e e1] [%e e2] [%e e3]])
167+
| [%expr
168+
[%e? { pexp_desc = Pexp_ident { txt = Lident op_ident; _ }; _ }]
169+
([%e? expr2], [%e? expr3], [%e? expr4])]
170+
when Hashtbl.mem ternary_ops op_ident ->
171+
let e1 = [%expr [%e expr] ?label:[%e opt_expr ~loc label]] in
172+
let vbs2, e2 = loop expr2 in
173+
let vbs3, e3 = loop expr3 in
174+
let vbs4, e4 = loop expr4 in
175+
(reduce_vbss [ vbs2; vbs3; vbs4 ], [%expr [%e e1] [%e e2] [%e e3] [%e e4]])
167176
| [%expr [%e? expr1] [%e? expr2] [%e? expr3]] ->
168177
let vbs1, e1 = loop ?label expr1 in
169178
let vbs2, e2 = loop expr2 in

lib/ppx_shared.ml

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -114,36 +114,6 @@ let is_assignment ident =
114114
&& Char.equal ident.[0] '='
115115
&& (not @@ List.mem [ "=="; "==="; "=>"; "==>"; "=>>" ] ident ~equal:String.equal)
116116

117-
(* let binary_op expr = (* This and is_binary_op should stay in sync with
118-
Arrayjit.Ops.binop_cd_syntax. *) (* FIXME: get rid of this and use binary_ops table instead. *)
119-
let loc = expr.pexp_loc in match expr with | [%expr ( + )] -> ([%expr Shape.Pointwise_bin],
120-
[%expr Arrayjit.Ops.Add]) | [%expr ( - )] -> ([%expr Shape.Pointwise_bin], [%expr
121-
Arrayjit.Ops.Sub]) | [%expr ( * )] -> ( Ast_builder.Default.pexp_extension ~loc @@
122-
Location.error_extensionf ~loc "No default compose type for binary `*`, try e.g. ~logic:\".\" for
123-
pointwise, %s" "~logic:\"@\" for matrix multiplication", [%expr Arrayjit.Ops.Mul] ) | [%expr ( /
124-
)] -> ( Ast_builder.Default.pexp_extension ~loc @@ Location.error_extensionf ~loc "For clarity,
125-
no default compose type for binary `/`, use ~logic:\".\" for pointwise \ division", [%expr
126-
Arrayjit.Ops.Div] ) | [%expr ( ** )] -> ([%expr Shape.Pointwise_bin], [%expr
127-
Arrayjit.Ops.ToPowOf]) | [%expr ( -?/ )] -> ([%expr Shape.Pointwise_bin], [%expr
128-
Arrayjit.Ops.Relu_gate]) | [%expr ( -/> )] -> ([%expr Shape.Pointwise_bin], [%expr
129-
Arrayjit.Ops.Arg2]) | [%expr ( -@> )] -> ([%expr Shape.Pointwise_bin], [%expr Arrayjit.Ops.Arg1])
130-
| [%expr ( < )] -> ([%expr Shape.Pointwise_bin], [%expr Arrayjit.Ops.Cmplt]) | [%expr ( <> )] ->
131-
([%expr Shape.Pointwise_bin], [%expr Arrayjit.Ops.Cmpne]) | [%expr ( || )] -> ([%expr
132-
Shape.Pointwise_bin], [%expr Arrayjit.Ops.Or]) | [%expr ( && )] -> ([%expr Shape.Pointwise_bin],
133-
[%expr Arrayjit.Ops.And]) | [%expr ( % )] -> ([%expr Shape.Pointwise_bin], [%expr
134-
Arrayjit.Ops.Mod]) | [%expr ( @^ )] -> ([%expr Shape.Pointwise_bin], [%expr Arrayjit.Ops.Max]) |
135-
[%expr ( ^^ )] -> ([%expr Shape.Pointwise_bin], [%expr Arrayjit.Ops.Min]) | _ -> ( [%expr
136-
Shape.Pointwise_bin], Ast_builder.Default.pexp_extension ~loc @@ Location.error_extensionf ~loc
137-
"ppx_ocannl %%cd: expected a binary operator, one of: %s" "+ (Add), - (Sub), * (Mul), / (Div), **
138-
(ToPowOf), -?/ (Relu_gate), -/> (Arg2), < \ (Cmplt), <> (Cmpne), || (Or), && (And), % (Mod), @^
139-
(Max), ^^ (Min)" ) *)
140-
(* let ternary_op expr = let loc =
141-
expr.pexp_loc in match expr with | [%expr where] -> ([%expr Shape.Pointwise_tern], [%expr
142-
Arrayjit.Ops.Where]) | [%expr fma] -> ([%expr Shape.Compose_accumulate], [%expr
143-
Arrayjit.Ops.FMA]) | _ -> ( [%expr Shape.Pointwise_bin], Ast_builder.Default.pexp_extension ~loc
144-
@@ Location.error_extensionf ~loc "ppx_ocannl %%cd: expected a ternary operator, one of: %s"
145-
"where, fma" ) *)
146-
147117
(** Binary primitive ops, both infix operator and function name variants. *)
148118
let binary_ops =
149119
Hashtbl.of_alist_exn

lib/syntax_extensions.md

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- Table of contents
44
- [Preliminaries](#preliminaries)
5+
- [Primitive operations](#primitive-operations)
56
- [The syntax for %op](#the-syntax-for-op)
67
- [The syntax for %cd](#the-syntax-for-cd)
78
- [Numeric and N-dimensional array literals](#numeric-and-n-dimensional-array-literals)
@@ -26,7 +27,7 @@
2627

2728
## Preliminaries
2829

29-
OCANNL, and arrayjit specifically, is built around a fixed number of numeric operations, declared in `arrayjit/ops.ml`. We assign lexical operators to many of the operations, inventing novel operators if needed. For example, Rectified Linear Unit `Relu` operation, which computes `f(x) = max(0,x)`, gets the operator `relu`, and the ReLU-Gate `Relu_gate` operation, which computes `f(x,y) = if x > 0.0 then y else 0.0`, gets the operator `-?/`. These built-in numeric operations are used to construct assignments (`Assignments.t` packaged as `Assignments.comp`). The syntax `%cd` is needed to build assignments concisely. On the other hand, while the syntax `%op` helps build tensors (`Tensor.t`), they can be expressed concisely in pure OCaml. Unlike for assignments, the building blocks for tensor expressions are easy to extend. The meaningful basic ones are provided in `lib/operation.ml`.
30+
OCANNL, and arrayjit specifically, is built around a fixed number of numeric operations, declared in `arrayjit/ops.ml`. We assign lexical operators to the binary operations, inventing novel operators if needed. For example, Rectified Linear Unit `Relu` operation, which computes `f(x) = max(0,x)`, is called `relu`, while the ReLU-Gate `Relu_gate` operation, which computes `f(x,y) = if x > 0.0 then y else 0.0`, gets the operator `-?/` in addition to name `relu_gate`. These built-in numeric operations are used to construct assignments (`Assignments.t` packaged as `Assignments.comp`). The syntax `%cd` is needed to build assignments concisely, and the assignment operators always start with `=` (unlike in C where they end with `=`). On the other hand, while the syntax `%op` helps build tensors (`Tensor.t`), they can be expressed concisely in pure OCaml. Unlike for assignments, the building blocks for tensor expressions are easy to extend. The meaningful basic ones are provided in `lib/operation.ml`.
3031

3132
In OCANNL, we call a tensor that is prohibited from propagating gradients, does not have a gradient node nor backprop code, a _non-differentiable tensor_. Accordingly we can call the "plain" tensors with a gradient node _differentiable tensors_. Expressions in the `%cd` syntax will sometimes build new non-differentiable tensors as components of assignments (they will never build new differentiable tensors). The syntax extensions make the following assumption:
3233

@@ -37,6 +38,56 @@ Functions inside `Operation.NTDSL` use `~grad_spec:Prohibit_grad` when calling i
3738

3839
The extension points open `NTDSL.O`, resp. `TDSL.O`, for the scope of the extension point, to expose the corresponding operators.
3940

41+
## Primitive operations
42+
43+
To accomodate stylistic preferences, OCANNL supports both curried and uncurried syntaxes for primitive operation application. Binary operators are associated with infix operators, in addition to having alphabetic identifiers. This stems from the following restriction: in the `%cd` syntax, the assignment is always an infix operator, and it needs to pick the accumulation operation.
44+
45+
The unary primitive operations:
46+
47+
| Identifier | Default projection | Constructor in `Arrayjit.Ops` |
48+
|------------|--------------------|-------------|
49+
| `id` | pointwise | `Identity` |
50+
| `relu` | pointwise | `Relu` |
51+
| `sat01` | pointwise | `Satur01` |
52+
| `exp` | pointwise | `Exp` |
53+
| `log` | pointwise | `Log` |
54+
| `exp2` | pointwise | `Exp2` |
55+
| `log2` | pointwise | `Log2` |
56+
| `sin` | pointwise | `Sin` |
57+
| `cos` | pointwise | `Cos` |
58+
| `sqrt` | pointwise | `Sqrt` |
59+
| `recip` | pointwise | `Recip` |
60+
| `recip_sqrt` | pointwise | `Recip_sqrt` |
61+
| `neg` | pointwise | `Neg` |
62+
| `tanh` | pointwise | `Tanh_approx` |
63+
64+
The binary primitive operations:
65+
66+
| Identifier | Infix operator | Default projection | Constructor in `Arrayjit.Ops` | Assignments |
67+
|------------|----------------|--------------------|-------------|-------------|
68+
| `fst` | `-@>` | pointwise | `Arg1` | none |
69+
| `snd` | `-/>` | pointwise | `Arg2` | `=:` |
70+
| `add` | `+` | pointwise | `Add` | `=+`, `=:+` |
71+
| `sub` | `-` | pointwise | `Sub` | `=-`, `=:-` |
72+
| `mul` | `*` | none | `Mul` | `=*`, `=:*` |
73+
| `div` | `/` | none | `Div` | `=/`, `=:/` |
74+
| `pow` | `**` | pointwise | `ToPowOf` | `=**`, `=:**` |
75+
| `relu_gate` | `-?/` | pointwise | `Relu_gate` | `=?/`, `=:?/` |
76+
| `lt` | `<` | pointwise | `Cmplt` | none |
77+
| `ne` | `<>` | pointwise | `Cmpne` | none |
78+
| `or_` | `\|\|` | pointwise | `Or` | `=\|\|`, `=:\|\|` |
79+
| `and_` | `&&` | pointwise | `And` | `=&&`, `=:&&` |
80+
| `mod_` | `%` | pointwise | `Mod` | `=%`, `=:%` |
81+
| `max` | `@^` | pointwise | `Max` | `=@^`, `=:@^` |
82+
| `min` | `^^` | pointwise | `Min` | `=^^`, `=:^^` |
83+
84+
The ternary primitive operations:
85+
86+
| Identifier | Default projection | Constructor in `Arrayjit.Ops` |
87+
|------------|--------------------|-------------|
88+
| `where` | pointwise | `Where` |
89+
| `fma` | compose-accumulate | `FMA` |
90+
4091
## The syntax for %op
4192

4293
The `%op` syntax is simpler than the `%cd` syntax since it relies more on regular OCaml expressions. For example, we can write without syntax extensions:
@@ -99,9 +150,9 @@ type Assignments.t =
99150

100151
For example the binary case in pseudocode: `if initialize_neutral then lhs = 0; lhs = lhs accum (rhs1 op rhs2)` (assuming the neutral element of `accum` is 0). The representation also has a field `projections` which determines which loops should be run and how the tensor nodes should be indexed to perform the computation.
101152

102-
The basic `%cd` syntax for binary operator assignments has the form: `<lhs> <asgn-op> <rhs1> <op> <rhs2>` (or `<lhs> <asgn-op> <op> <rhs1> <rhs2>` when `<op>` is not an operator). The binary operators in the `<rhs1> <op> <rhs2>` part have a straightfowrad syntax: `<op>` is one of `+`, `-`, `*`, `/`, `**` (to-power-of), `-?/` (ReLU-Gate). `<asgn-op>` starts with `=`, followed by `:` only if `initialize_neutral` is true, then followed by one of `+`, `-`, `*`, `/`, `**`, `relu`. The fields `<lhs>`, `<rhs1>`, `<rhs2>` will often be either special-purpose identifiers (e.g. `t`, `t1`, `t2`, `g`, `g1`, `g2`) or identifiers bound to tensors. `<rhs1>`, `<rsh2>` will also often be (non-differentiable) tensor expressions. The notation `<tensor>.grad` stands for the gradient node of the given tensor. For more about "slot fillers", and to learn about the operators `*+` and `++`, see the section [further features of the syntax extension %cd](#further-features-of-the-syntax-extension-cd).
153+
The basic `%cd` syntax for assignments has the form: `<lhs> <asgn-op> <primitive-op-application[rhs1, rhs2?, rhs3?]>`. See [Primitive operations](#primitive-operations) for the syntax of primitive operation application, where `<rhs1>`, `<rhs2>` (for binary and ternary ops), `<rhs3>` (for ternary ops) are subexpressions. `<asgn-op>` starts with `=`, followed by `:` only if `initialize_neutral` is true, then followed by the operator syntax variant of a binary primitive operation. The fields `<lhs>`, `<rhs1>`, `<rhs2>`, `<rhs3>` will often be either special-purpose identifiers (e.g. `t`, `t1`, `t2`, `t3`, `g`, `g1`, `g2`, `g3`) or identifiers bound to tensors. `<rhs1>`, `<rsh2>`, `<rsh3>` will also often be (non-differentiable) tensor expressions. The notation `<tensor>.grad` stands for the gradient node of the given tensor. For more about "slot fillers", and to learn about the operators `*+` and `++`, see the section [further features of the syntax extension %cd](#further-features-of-the-syntax-extension-cd).
103154

104-
How is the `projections` field determined? `projections` can be given explicitly as a labeled argument `~projections`. If they aren't but `%cd` realizes there is a `~projections` parameter in scope, it uses it -- see `lib/operation.ml` where this option is used to define tensor operations. If instead of `~projections` a `~logic` labeled argument is given, the string passed is used to determine projections. `~logic:"."` means a pointwise operation. `~logic:"@"` means an "output axes of rhs2 match input axes of rhs1" operation (matrix multiplication is a special case). `~logic:"T"` means transpose of input and output axes. The string passed to `~logic` can also use OCANNL's generalization of the einsum notation, allowing arbitrary permutations and reductions of axes. If no information is given, the default is a pointwise operation.
155+
How is the `projections` field determined? `projections` can be given explicitly as a labeled argument `~projections`. If they aren't but `%cd` realizes there is a `~projections` parameter in scope, it uses it -- see `lib/operation.ml` where this option is used to define tensor operations. If instead of `~projections` a `~logic` labeled argument is given, the string passed is used to determine projections. `~logic:"."` means a pointwise operation. `~logic:"@"` means an "output axes of rhs2 match input axes of rhs1" operation (matrix multiplication is a special case). `~logic:"T"` means transpose of input and output axes. The string passed to `~logic` can also use OCANNL's generalization of the einsum notation, allowing arbitrary permutations and reductions of axes. If no information is given, the default depends on the primitive operation, but it is almost always a pointwise operation.
105156

106157
Here we see an example of tensor multiplication -- extending matrix multiplication to arbitrary number of axes -- multiplying `a` by `b` to get `c`. In `=:+`, `=` is required to separate the assigned-to part from the computation, `:` clears-out `c` before the computation, `+` selects addition to accumulate the results.
107158

0 commit comments

Comments
 (0)