You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: parenthetical syntax for message sends (cycles, timeout etc.) (#4608)
This PR introduces new syntax (parenthetical notes, or simply _parentheticals_) for `async`-valued calls and `async` self-sends. Two attributes are recognised therein: `cycles : Nat` and `timeout : Nat32`. The type-checker enforces these types (when attributes present) and warns on unknown ones. Having a `timeout` attribute present will make the message send a bounded-wait one (also called best-effort) according to the new replica semantics. Specifying `cycles` works like the (soon to be deprecated) `Cycles.add<system>`, ~but one has to decide which to use (either `Cycles.add` or a parenthetical) since there is no mix-and-match~.
-----------
~`ICCallPrim` (and friends) should carry the fragment to set the `SystemCyclesAddPrim` while the call is being set up towards the replica.~ This now happens in the desugarer by assigning to variables (`@cycles` and `@timeout`).
Parentheticals
- now: `(with cycles = 42_000_000) Actor.call(param)`
- also now: `(with timeout = 10)`
`ic0.call_with_best_effort_response : (timeout_seconds : i32) -> ()`
- maybe: `(with receiveMax = 50_000)` (to limit the response size)
- maybe: `(with resend = { tries = 5; delay = 3 })` when `SYS_UNKNOWN` reject response (like [Unix `EINTR`](https://unix.stackexchange.com/questions/253349/what-is-the-rationale-behind-eintr), but saves cycles by not re-encoding the arguments)
- support dfinity/ic#1158
- usecase `(with memoryLimit = 1G) ActorClass(<args>)`
- default/suppress call-meta-attributes: `func() : async () = (with) async { ... }` — document this!
TODOs:
- [x] `Changelog.md`
- [x] documentation
- [x] test one-ways
- typecheck
- [x] warn when an attribute is moot
- [x] `cycles : Nat` for canister sends (_self_ and _raw_ sends too)
- [x] `timeout : Nat32` for best-effort
- [x] parenthetical should have no send cap (test still missing)
- [x] `async` blocks
- [x] `ICCallPrim`, see top
- `ICCallRawPrim`, not directly annotatable (desisting for now, but see #4868)
- [ ] `FIXME`s, `TODO`s
- [x] `ICCallPrim` should have a non-`option` setup
- [ ] break out unrelated PRs (optimisations?) and beautifications — see #4890
- [ ] warn on queries?
- [x] best-effort: new error code when deadline passed?
- [x] doc/md/base/Error.md -> `motoko-base`, see dfinity/motoko-base#686
- [x] visit all `git grep system_fatal` and amend `#system_unknown`
- [x] decide whether `(with cycles) async` or `async (with cycles)` — decided: keep as prefix!
- [x] write a temporary test that `Cycles.add` still works
- [x] test that parenthetical have no send capability
- [x] test `(with cycles) (func () -> async () {})()` direct application
- [x] test `(with cycles) (system Lib.Class)(...)()`
- [x] test error on `async*` (and call)
Copy file name to clipboardexpand all lines: doc/md/canister-maintenance/cycles.md
+5-2
Original file line number
Diff line number
Diff line change
@@ -13,11 +13,14 @@ In Motoko programs deployed on ICP, each actor represents a canister and has an
13
13
14
14
Callees can accept all, some, or none of the available cycles up to limit determined by their actor’s current balance. Any remaining cycles are refunded to the caller. If a call traps, all its accompanying cycles are automatically refunded to the caller without loss.
15
15
16
-
In future, we may see Motoko adopt dedicated syntax and types to support safer programming with cycles. For now, we provide a temporary way to manage cycles through a low-level imperative API provided by the [ExperimentalCycles](../base/ExperimentalCycles.md) library in package `base`.
16
+
Motoko is adopting dedicated syntax and types to support safer programming with cycles. Users can now attach `(where cycles = <amount>)` as a prefix to message sends and async expressions.
17
+
This new syntax will eventually obsolete the use of `ExperimentalCycles.add<system>(cycles)` in the examples that follow.
18
+
19
+
For now (and until officially deprecating it), we provide a temporary way to manage cycles through a low-level imperative API provided by the [ExperimentalCycles](../base/ExperimentalCycles.md) library in package `base`.
17
20
18
21
:::note
19
22
20
-
This library is subject to change and likely to be replaced by more high-level support for cycles in later versions of Motoko.
23
+
This library is subject to change and likely to be replaced by more high-level support for cycles in later versions of Motoko. See [Async data](../writing-motoko/async-data.md) for further usage information about parentheticals (such as attaching cycles) on message sends.
async* <block-or-exp> Delay an asynchronous computation
526
526
await* <block-or-exp> Await a delayed computation (only in async)
@@ -746,6 +746,8 @@ type ErrorCode = {
746
746
#system_fatal;
747
747
// Transient error.
748
748
#system_transient;
749
+
// Response unknown due to missed deadline.
750
+
#system_unknown;
749
751
// Destination invalid.
750
752
#destination_invalid;
751
753
// Explicit reject by canister code.
@@ -2188,7 +2190,7 @@ Otherwise,
2188
2190
2189
2191
### Function calls
2190
2192
2191
-
The function call expression `<exp1> <T0,…,Tn>? <exp2>` has type `T` provided:
2193
+
The function call expression `<parenthetical>? <exp1> <T0,…,Tn>? <exp2>` has type `T` provided:
2192
2194
2193
2195
- The function `<exp1>` has function type `<shared>? < X0 <: V0, ..., Xn <: Vn > U1-> U2`.
2194
2196
@@ -2206,6 +2208,10 @@ Otherwise, `exp2` is evaluated to a result `r2`. If `r2` is `trap`, the expressi
2206
2208
2207
2209
Otherwise, `r1` is a function value, `<shared-pat>? func <X0 <: V0, …, n <: Vn> <pat1> { <exp> }` (for some implicit environment), and `r2` is a value `v2`. If `<shared-pat>` is present and of the form `shared <query>? <pat>` then evaluation continues by matching the record value `{caller = p}` against `<pat>`, where `p` is the [`Principal`](../base/Principal.md) invoking the function, typically a user or canister. Matching continues by matching `v1` against `<pat1>`. If pattern matching succeeds with some bindings, then evaluation returns the result of `<exp>` in the environment of the function value not shown extended with those bindings. Otherwise, some pattern match has failed and the call results in `trap`.
2208
2210
2211
+
A `<parenthetical>`, when present, modifies dynamic attributes of the message send (provided that the return type `T` is of form `async U`, i.e. a future). The recognized attributes are
2212
+
-`cycles : Nat` to attach cycles
2213
+
-`timeout : Nat32` to introduce a timeout for best-effort message execution.
2214
+
2209
2215
:::note
2210
2216
2211
2217
The exhaustiveness side condition on `shared` function expressions ensures that argument pattern matching cannot fail (see [functions](#functions)).
@@ -2478,7 +2484,7 @@ The `return` expression exits the corresponding dynamic function invocation or c
2478
2484
2479
2485
### Async
2480
2486
2481
-
The async expression `async <block-or-exp>` has type `async T` provided:
2487
+
The async expression `<parenthetical>? async <block-or-exp>` has type `async T` provided:
2482
2488
2483
2489
-`<block-or-exp>` has type `T`.
2484
2490
@@ -2490,6 +2496,10 @@ The implicit return type in `<block-or-exp>` is `T`. That is, the return express
2490
2496
2491
2497
Evaluation of `async <block-or-exp>` queues a message to evaluate `<block-or-exp>` in the nearest enclosing or top-level actor. It immediately returns a future of type `async T` that can be used to `await` the result of the pending evaluation of `<exp>`.
2492
2498
2499
+
The presence of `<parenthetical>` modifies the semantics of the async expression to
2500
+
- attach cycles with attribute `cycles : Nat`
2501
+
- impose a timeout (observed when awaiting the result) with attribute `timeout : Nat32`.
2502
+
2493
2503
:::note
2494
2504
2495
2505
Because it involves messaging, evaluating an `async` expression can fail due to a lack of canister resources.
@@ -2788,4 +2798,4 @@ In general, this means that an expression of a more specific type may appear whe
2788
2798
-**IEEE Standard for Floating-Point Arithmetic**, in IEEE Std 754-2019 (Revision of IEEE 754-2008), vol., no., pp.1-84, 22 July 2019, doi: 10.1109/IEEESTD.2019.8766229.
Copy file name to clipboardexpand all lines: doc/md/writing-motoko/async-data.md
+22-2
Original file line number
Diff line number
Diff line change
@@ -14,6 +14,8 @@ Like several other languages, Motoko offers `async` and `await` to support conve
14
14
In Motoko, executing an asynchronous expression, whether a call to a shared function, or just a local `async` expression, produces a future, an object of type `async T`, for some result type `T`.
15
15
Instead of blocking the caller until the call has returned, the message is enqueued on the callee and the future representing that pending request is immediately returned to the caller. The future is a placeholder for the eventual result of the request that the caller can later query.
16
16
17
+
Every operation resulting in a future (i.e. sends to other `actor`s/canisters or self sends with `async` or by function calls) can be prefixed by a _parenthetical_ of the form `(base with attr₁ = v₁; attr₂ = v₂; …)` where `base` is an optional record containing (e.g. default) attributes. Accepted attributes are currently `cycles : Nat`, specifying the amount of cycles to be sent along with the message, and `timeout : Nat32` to modify the deadline and restrict the time span while the receiver can reply.
18
+
17
19
The syntax `await` synchronizes on a future, and suspends computation until the future is completed by its producer.
18
20
19
21
Between issuing the request and deciding to wait for the result, the caller is free to do other work. Once the callee has processed the request, the future is completed and its result made available to the caller. If the caller is waiting on the future, its execution can resume with the result, otherwise the result is simply stored in the future for later use.
@@ -37,7 +39,6 @@ The `Counter` actor declares one field and three public, shared functions:
37
39
38
40
- Function `bump()` asynchronously increments and reads the counter.
39
41
40
-
41
42
The only way to read or modify the state (`count`) of the `Counter` actor is through its shared functions.
42
43
43
44
## Using `await` to consume async futures
@@ -69,6 +70,25 @@ Unlike a local function call, which blocks the caller until the callee has retur
69
70
70
71
Awaiting a future a second time will just produce the same result, including re-throwing any error stored in the future. Suspension occurs even if the future is already complete; this ensures state changes and message sends prior to every `await` are committed.
71
72
73
+
## Using parentheticals to modify message send modalities
74
+
75
+
In the above examples all messages sent to the `Counter` actor do not transmit cycles and will never timeout when their results are awaited. Both of these
76
+
aspects can be configured by syntactically adding a parenthetical and thus modifying the dynamic attributes of the message send.
77
+
78
+
To add cycles to the send one would write
79
+
```motoko no-repl
80
+
let a = (with cycles = 42_000_000) Counter.bump();
81
+
```
82
+
Similarly, one can specify an explicit timeout to apply when awaiting the result of the message. This is useful in applications that prefer best-effort over guaranteed response message delivery:
83
+
```motoko no-repl
84
+
let a = (with timeout = 25) Counter.bump();
85
+
```
86
+
Custom defaults for these attributes can be defined in a record that is used as the base expression of a parenthetical:
87
+
```motoko no-repl
88
+
let boundedWait = { timeout = 25 };
89
+
let a = (boundedWait with cycles = 42_000_000) Counter.bump();
90
+
```
91
+
72
92
:::danger
73
93
74
94
A function that does not `await` in its body is guaranteed to execute atomically. In particular, the environment cannot change the state of the actor while the function is executing. If a function performs an `await`, however, atomicity is no longer guaranteed. Between suspension and resumption around the `await`, the state of the enclosing actor may change due to concurrent processing of other incoming actor messages. It is the programmer’s responsibility to guard against non-synchronized state changes. A programmer may, however, rely on any state change prior to the await being committed.
@@ -92,4 +112,4 @@ Each `await` suspends execution, allowing an interloper to change the state of t
92
112
93
113
-[`rxmo`](https://mops.one/rxmo): A library for reactive programming using observables, making it easier to compose asynchronous or callback-based code.
0 commit comments