Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
cb2d3a9
tweak
ggreif Jun 4, 2025
dabcb73
more tweaks
ggreif Jun 4, 2025
2c22f59
tweak
ggreif Jun 4, 2025
5e49632
simplify
ggreif Jun 4, 2025
2c9b66e
fix types and the coercion
ggreif Jun 4, 2025
29a5626
accept
ggreif Jun 4, 2025
5464929
test
ggreif Jun 4, 2025
cead471
indent
ggreif Jun 5, 2025
45dc3fa
simplify
ggreif Jun 5, 2025
c342af0
simplify, return to previous types
ggreif Jun 5, 2025
a053eb8
connect loose ends
ggreif Jun 5, 2025
57c340e
no need to assert
ggreif Jun 5, 2025
7701980
restore
ggreif Jun 5, 2025
b8c54a8
WIP
ggreif Jun 5, 2025
891ae89
accept
ggreif Jun 5, 2025
421bba0
accept
ggreif Jun 5, 2025
f4432e2
remove cruft
ggreif Jun 5, 2025
8491185
accept
ggreif Jun 7, 2025
19f48fc
fix
ggreif Jun 7, 2025
e4c35ca
Merge branch 'master' into gabor/fastpath
ggreif Jun 7, 2025
8123e9f
Update src/ir_def/check_ir.ml
ggreif Jun 10, 2025
b9a64cd
Update src/ir_def/check_ir.ml
ggreif Jun 10, 2025
923099c
Merge branch 'master' into gabor/fastpath
ggreif Jun 10, 2025
eda49f2
Update Changelog.md
ggreif Jun 10, 2025
1f4bb4d
Merge branch 'master' into gabor/fastpath
ggreif Jun 10, 2025
0274330
Update Changelog.md
ggreif Jun 10, 2025
633b1c0
test semantic difference of a commit point
ggreif Jun 11, 2025
cdbc376
accept
ggreif Jun 11, 2025
7dd2f45
send queue idea
ggreif Jun 11, 2025
09ce9d7
Update Changelog.md
ggreif Jun 11, 2025
32f6f2b
Apply suggestions from code review
ggreif Jun 11, 2025
fcca72a
Merge branch 'master' into gabor/fastpath
ggreif Jun 11, 2025
824eb80
introduce and use `await?` (#5250)
ggreif Jun 11, 2025
c43a13e
review comments
ggreif Jun 11, 2025
ac3ca6e
Update Changelog.md
ggreif Jun 11, 2025
7b7e7dc
pass flag to `await`
ggreif Jun 11, 2025
7adf9b4
interpreter: short-circuit the continuations when required
ggreif Jun 11, 2025
a7dd642
fix tracing
ggreif Jun 11, 2025
aa4e687
Update Changelog.md
ggreif Jun 11, 2025
b0334e6
maintain the trace depth invariant
ggreif Jun 12, 2025
21b28b4
fix the IR interpreter
ggreif Jun 13, 2025
307681d
refactor
ggreif Jun 13, 2025
85378ab
tweak
ggreif Jun 13, 2025
0bae09f
Merge branch 'master' into gabor/fastpath
ggreif Jun 13, 2025
982e6e9
add section to the language manual about `await?`
ggreif Jun 13, 2025
7f5b78c
add blurb to tutorial
ggreif Jun 13, 2025
93367d6
doc: `await?` doc tweaks (#5266)
crusso Jun 16, 2025
582f570
Merge branch 'master' into gabor/fastpath
ggreif Jun 16, 2025
05593e2
Update src/ir_interpreter/interpret_ir.ml
ggreif Jun 17, 2025
33e65e0
Merge branch 'master' into gabor/fastpath
ggreif Jun 17, 2025
a23a938
Update doc/md/fundamentals/5-actors-async.md
crusso Jun 17, 2025
ecab9f4
Merge branch 'master' into gabor/fastpath
ggreif Jun 17, 2025
087c2c7
add test that short-circuiting also works for the `throw` case
ggreif Jun 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
# Motoko compiler changelog

## 0.14.13 (FUTURE)

* motoko (`moc`)

* optimization: accelerate IR type checking with caching of sub, lub and check_typ tests (#5260). Reduces need for `-no-check-ir` flag.
* Introduce `await?` to synchronize `async` futures, avoiding the commit point when already fulfilled (#5215).

* optimization: accelerate IR type checking with caching of sub, lub and check_typ tests (#5260).
Reduces need for `-no-check-ir` flag.

## 0.14.12 (2025-06-12)

* motoko (`moc`)
* Added the `rootKey` primitive (#4994).

* optimization: for `--enhanced-orthogonal-persistence`, reduce code-size and compile-time by sharing more static allocations (#5233, #5242).

* bugfix: fix `-fshared-code` bug (#5230).
* bugfix: avoid stack overflow and reduce code complexity for large eop canisters (#5218).
* Added the `rootKey` primitive (#4994).

* bugfix: avoid stack overflow and reduce code complexity for large EOP canisters (#5218).

## 0.14.11 (2025-05-16)

Expand Down
29 changes: 28 additions & 1 deletion doc/md/15-language-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ The following keywords are reserved and may not be used as identifiers:

``` bnf

actor and assert async async* await await* break case catch class
actor and assert async async* await await? await* break case catch class
composite continue debug debug_show do else false flexible finally for
from_candid func if ignore import in module not null persistent object or label
let loop private public query return shared stable switch system throw
Expand Down Expand Up @@ -522,6 +522,7 @@ The syntax of an expression is as follows:
return <exp>? Return
<parenthetical>? async <block-or-exp> Async expression
await <block-or-exp> Await future (only in async)
await? <block-or-exp> Await future (only in async) without state commit when completed
async* <block-or-exp> Delay an asynchronous computation
await* <block-or-exp> Await a delayed computation (only in async)
throw <exp> Raise an error (only in async)
Expand Down Expand Up @@ -2575,6 +2576,32 @@ Earlier versions of Motoko would trap in such situations, making it difficult fo

:::

### Await?

Similar to `await`, the `await?` expression `await? <exp>` has type `T` provided:

- `<exp>` has type `async T`.

- `T` is shared.

- The `await?` is explicitly enclosed by an `async` expression or appears in the body of a `shared` function.

Expression `await? <exp>` evaluates `<exp>` to a result `r`. If `r` is `trap`, evaluation returns `trap`. Otherwise `r` is a future. If the `future` is incomplete, that is, its evaluation is still pending, `await? <exp>` suspends evaluation of the neared enclosing `async` or `shared` function, adding the suspension to the wait-queue of the `future`. Execution of the suspension is resumed once the future is completed, if ever.
If the future is complete with value `v`, then `await? <exp>` immediately continues execution with the value `v`.
If the future is complete with thrown error value `e`, then `await? <exp>` immediately continues execution by re-throwing the error `e`.

Thus `await?` behaves like `await` on an incomplete future, but does not suspend execution and simply continues when the future is already complete.

This conditional suspension, dependent on the state of the future, enables performance optimization in cases where a definite commit point is not required.

:::danger

As with `await`, between suspension and resumption of a computation, 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.

Using `await?` signals that the computation may commit its current state and suspend execution, potentially allowing concurrent modification of state.

:::


### Async*

Expand Down
1 change: 1 addition & 0 deletions doc/md/examples/grammar.txt
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@
<parenthetical>? 'async' <exp_nest>
'async*' <exp_nest>
'await' <exp_nest>
'await?' <exp_nest>
'await*' <exp_nest>
'assert' <exp_nest>
'label' <id> (':' <typ>)? <exp_nest>
Expand Down
25 changes: 23 additions & 2 deletions doc/md/fundamentals/5-actors-async.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ In Motoko, actors have dedicated syntax and type rules that support asynchronous

* A **future**, `f`, has the type `async T` and represents a value of type `T` that will be available later.

* To retrieve the result of a future, use `await f`, which pauses execution until the future is resolved and returns a value of type `T`.
* To retrieve the result of a future, use `await f`, which pauses execution until the future is resolved and returns a value of type `T`. `await? f` can be used when the the future is likely resolved and the state commit semantics is irrelevant.

* These restrictions help prevent shared mutable state from being introduced via messaging. Only immutable, shared data can be sent between actors through shared functions.

Expand Down Expand Up @@ -95,7 +95,7 @@ The `Counter` actor declares one field and three public, shared functions:

The only way to read or modify the state (`count`) of the `Counter` actor is through its shared functions.

## Using `await` to consume async futures
## Using `await` to consume `async` futures

The caller of a shared function typically receives a future, a value of type `async T` for some `T`.

Expand Down Expand Up @@ -124,6 +124,27 @@ Unlike a local function call, which waits for the result before continuing, a sh

If you `await` the same future again, it just returns the same result or error. Even if the future is already done, `await` will briefly suspend all pending state changes and outgoing messages. This means that you can rely on every `await` to commit state, whether its future is still in progress or already completed.

## Using `await?` to efficiently await concurrent futures

An `await` will always suspend execution and commit state, even if its future is already complete.

When several futures are issued in parallel and racing to complete, it can be more efficient to opt out of the unconditional behavior of `await` and immediately continue with a result when it is available:

``` motoko no-repl
let a : async Nat = CounterA.read();
let b : async Nat = CounterB.read();
let sum : Nat = (await a) + (await? b);
```

Here the futures `a` and `b` are racing to complete, and it is likely that the first `await` on `a` will resume with `b` already completed.
Using `await? b` ensures that `b`'s result can be used immediately, if available, without an unnecessary suspension.

:::danger

Since a commit of global state may not happen when using `await?`, this construct should be only used when the commit can be safely omitted.

:::


## Using parentheticals to modify message send modalities

Expand Down
1 change: 1 addition & 0 deletions src/gen-grammar/grammar.sed
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ s/ROTLASSIGN/\'<<>=\'/g
s/RETURN/\'return\'/g
s/RCURLY/\'}\'/g
s/RBRACKET/\']\'/g
s/AWAITQUEST/\'await?\'/g
s/QUEST/\'?\'/g
s/BANG/\'!\'/g
s/QUERY/\'query\'/g
Expand Down
10 changes: 6 additions & 4 deletions src/ir_def/arrange_ir.ml
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ and prim = function
| GetLastArrayOffset -> Atom "GetLastArrayOffset"
| BreakPrim i -> "BreakPrim" $$ [id i]
| RetPrim -> Atom "RetPrim"
| AwaitPrim Type.Fut -> Atom "AwaitPrim"
| AwaitPrim Type.Cmp -> Atom "AwaitPrim*"
| AwaitPrim (Type.AwaitFut false) -> Atom "AwaitPrim"
| AwaitPrim (Type.AwaitFut true) -> Atom "AwaitPrim?"
| AwaitPrim Type.AwaitCmp -> Atom "AwaitPrim*"
| AssertPrim -> Atom "AssertPrim"
| ThrowPrim -> Atom "ThrowPrim"
| ShowPrim t -> "ShowPrim" $$ [typ t]
Expand All @@ -108,8 +109,9 @@ and prim = function
| SetCertifiedData -> Atom "SetCertifiedData"
| GetCertificate -> Atom "GetCertificate"
| OtherPrim s -> Atom s
| CPSAwait (Type.Fut, t) -> "CPSAwait" $$ [typ t]
| CPSAwait (Type.Cmp, t) -> "CPSAwait*" $$ [typ t]
| CPSAwait (Type.AwaitFut false, t) -> "CPSAwait" $$ [typ t]
| CPSAwait (Type.AwaitFut true, t) -> "CPSAwait?" $$ [typ t]
| CPSAwait (Type.AwaitCmp, t) -> "CPSAwait*" $$ [typ t]
| CPSAsync (Type.Fut, t) -> "CPSAsync" $$ [typ t]
| CPSAsync (Type.Cmp, t) -> "CPSAsync*" $$ [typ t]
| ICArgDataPrim -> Atom "ICArgDataPrim"
Expand Down
6 changes: 4 additions & 2 deletions src/ir_def/check_ir.ml
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ let rec check_exp env (exp:Ir.exp) : unit =
| Some c -> T.Con(c, [])
| None -> error env exp.at "misplaced await" in
let t1 = T.promote (typ exp1) in
let (t2, t3) = try T.as_async_sub s t0 t1
let (t2, t3) = try T.as_async_sub (to_async_sort s) t0 t1
with Invalid_argument _ ->
error env exp1.at "expected async type, but expression has type\n %s"
(T.string_of_typ_expand t1)
Expand Down Expand Up @@ -600,7 +600,7 @@ let rec check_exp env (exp:Ir.exp) : unit =
T.Opt (T.seq ots) <: t
| CPSAwait (s, cont_typ), [a; krb] ->
let (_, t1) =
try T.as_async_sub s T.Non (T.normalize (typ a))
try T.as_async_sub (to_async_sort s) T.Non (T.normalize (typ a))
with _ -> error env exp.at "CPSAwait expect async arg, found %s" (T.string_of_typ (typ a))
in
(match cont_typ with
Expand Down Expand Up @@ -1195,6 +1195,8 @@ and gather_dec env scope dec : scope =
let val_info = {typ = t; const = false; loc_known = false} in
{ val_env = T.Env.add id val_info scope.val_env }

and to_async_sort = Type.(function | AwaitCmp -> Cmp | AwaitFut _ -> Fut)

(* Programs *)

let check_comp_unit env = function
Expand Down
2 changes: 1 addition & 1 deletion src/ir_def/construct.ml
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ let asyncE s typ_bind e typ1 =
}

let awaitE s e =
let (s, _ , typ) = T.as_async (T.normalize (typ e)) in
let (_, _ , typ) = T.as_async (T.normalize (typ e)) in
{ it = PrimE (AwaitPrim s, [e]);
at = no_region;
note = Note.{ def with typ; eff = T.Await }
Expand Down
4 changes: 2 additions & 2 deletions src/ir_def/construct.mli
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ val primE : Ir.prim -> exp list -> exp
val selfRefE : typ -> exp
val assertE : exp -> exp
val asyncE : async_sort -> typ_bind -> exp -> typ -> exp
val awaitE : async_sort -> exp -> exp
val awaitE : await_sort -> exp -> exp
val cps_asyncE : async_sort -> typ -> typ -> exp -> exp
val cps_awaitE : async_sort -> typ -> exp -> exp -> exp
val cps_awaitE : await_sort -> typ -> exp -> exp -> exp
val ic_replyE : typ list -> exp -> exp
val ic_rejectE : exp -> exp
val ic_callE : exp -> exp -> exp -> exp -> exp -> exp
Expand Down
4 changes: 2 additions & 2 deletions src/ir_def/ir.ml
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ and prim =
| IdxBlobPrim (* blob indexing *)
| BreakPrim of id (* break *)
| RetPrim (* return *)
| AwaitPrim of Type.async_sort (* await/await* *)
| AwaitPrim of Type.await_sort (* await/await?/await* *)
| AssertPrim (* assertion *)
| ThrowPrim (* throw *)
| ShowPrim of Type.typ (* debug_show *)
Expand Down Expand Up @@ -170,7 +170,7 @@ and prim =

| OtherPrim of string (* Other primitive operation, no custom typing rule *)
(* backend stuff *)
| CPSAwait of Type.async_sort * Type.typ
| CPSAwait of Type.await_sort * Type.typ
(* typ is the current continuation type of cps translation *)
| CPSAsync of Type.async_sort * Type.typ
| ICPerformGC
Expand Down
26 changes: 17 additions & 9 deletions src/ir_interpreter/interpret_ir.ml
Original file line number Diff line number Diff line change
Expand Up @@ -179,16 +179,24 @@ let async env at (f: (V.value V.cont) -> (V.value V.cont) -> unit) (k : V.value
);
k (V.Async async)

let await env at async k =
if env.flags.trace then trace "=> await %s" (string_of_region at);
let await env at short async k r =
let adorn, schedule = if short then "?", (|>) () else "", Scheduler.queue in
if env.flags.trace then trace "=> await%s %s" adorn (string_of_region at);
decr trace_depth;
get_async async (fun v ->
Scheduler.queue (fun () ->
schedule (fun () ->
if env.flags.trace then
trace "<- await %s%s" (string_of_region at) (string_of_arg env v);
trace "<- await%s %s%s" adorn (string_of_region at) (string_of_arg env v);
incr trace_depth;
k v)
)
(fun v ->
schedule (fun () ->
if env.flags.trace then
trace "<- await%s %s threw %s" adorn (string_of_region at) (string_of_arg env v);
incr trace_depth;
r v)
)


(* queue a lowered oneway or replying function that no longer does AsyncE on entry *)
Expand Down Expand Up @@ -369,10 +377,10 @@ and interpret_exp_mut env exp (k : V.value V.cont) =
| BreakPrim id, [v1] -> find id env.labs v1
| RetPrim, [v1] -> Option.get env.rets v1
| ThrowPrim, [v1] -> Option.get env.throws v1
| AwaitPrim Type.Fut, [v1] ->
| AwaitPrim (T.AwaitFut short), [v1] ->
assert env.flavor.has_await;
await env exp.at (V.as_async v1) k (Option.get env.throws)
| AwaitPrim Type.Cmp, [v1] ->
await env exp.at short (V.as_async v1) k (Option.get env.throws)
| AwaitPrim T.AwaitCmp, [v1] ->
assert env.flavor.has_await;
(V.as_comp v1) k (Option.get env.throws)
| AssertPrim, [v1] ->
Expand Down Expand Up @@ -401,13 +409,13 @@ and interpret_exp_mut env exp (k : V.value V.cont) =
k
| _ -> assert false
end
| CPSAwait _, [v1; v2] ->
| CPSAwait (sort, _), [v1; v2] ->
assert (not env.flavor.has_await && env.flavor.has_async_typ);
begin match V.as_tup v2 with
| [vf; vr] ->
let (_, f) = V.as_func vf in
let (_, r) = V.as_func vr in
await env exp.at (V.as_async v1)
await env exp.at (sort <> T.AwaitFut false) (V.as_async v1)
(fun v -> f (context env) v k)
(fun e -> r (context env) e k) (* TBR *)
| _ -> assert false
Expand Down
27 changes: 15 additions & 12 deletions src/ir_passes/async.ml
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ let transform prog =
| VarE (_, _) -> exp'
| AssignE (exp1, exp2) ->
AssignE (t_lexp exp1, t_exp exp2)
| PrimE (CPSAwait (Fut, cont_typ), [a; krb]) ->
| PrimE (CPSAwait (AwaitFut short, cont_typ), [a; krb]) ->
begin match cont_typ with
| Func(_, _, [], _, []) ->
(* unit answer type, from await in `async {}` *)
Expand All @@ -265,19 +265,22 @@ let transform prog =
switch_variantE (t_exp a -*- varE vkrb)
[ ("suspend", wildP,
unitE()); (* suspend *)
("schedule", varP schedule, (* resume later *)
(* try await async (); schedule() catch e -> r(e) *)
(let v = fresh_var "call" unit in
letE v
(selfcallE [] (ic_replyE [] (unitE())) (varE schedule) (projE (varE vkrb) 1)
([] -->* (projE (varE vkrb) 2 -*- unitE ())))
(check_call_perform_status (varE v) (fun e -> projE (varE vkrb) 1 -*- e))))
("schedule", varP schedule,
(if short then (* resume immediately *)
varE schedule -*- unitE ()
else (* resume later *)
(* try await async (); schedule() catch e -> r(e) *)
let v = fresh_var "call" unit in
letE v
(selfcallE [] (ic_replyE [] (unitE())) (varE schedule) (projE (varE vkrb) 1)
([] -->* (projE (varE vkrb) 2 -*- unitE ())))
(check_call_perform_status (varE v) (fun e -> projE (varE vkrb) 1 -*- e))))
]
unit
)).it
| _ -> assert false
end
| PrimE (CPSAwait (Cmp, cont_typ), [a; krb]) ->
| PrimE (CPSAwait (AwaitCmp, cont_typ), [a; krb]) ->
begin match cont_typ with
| Func(_, _, [], _, []) ->
(t_exp a -*- t_exp krb).it
Expand All @@ -296,7 +299,7 @@ let transform prog =
letP (tupP [varP nary_async; varP nary_reply; varP reject; varP clean]) def;
let ic_reply = (* flatten v, here and below? *)
let v = fresh_var "v" (T.seq ts1) in
v --> (ic_replyE ts1 (varE v)) in
v --> ic_replyE ts1 (varE v) in
let ic_reject =
let e = fresh_var "e" catch in
e --> ic_rejectE (errorMessageE (varE e)) in
Expand Down Expand Up @@ -382,7 +385,7 @@ let transform prog =
| FuncE (x, s, c, typbinds, args, ret_tys, exp) ->
begin
match s with
| Local ->
| Local ->
FuncE (x, s, c, t_typ_binds typbinds, t_args args, List.map t_typ ret_tys, t_exp exp)
| Shared s' ->
begin
Expand All @@ -403,7 +406,7 @@ let transform prog =
| t -> assert false in
let k =
let v = fresh_var "v" t1 in
v --> (ic_replyE ret_tys (varE v)) in
v --> ic_replyE ret_tys (varE v) in
let r =
let e = fresh_var "e" catch in
e --> ic_rejectE (errorMessageE (varE e)) in
Expand Down
10 changes: 3 additions & 7 deletions src/ir_passes/await.ml
Original file line number Diff line number Diff line change
Expand Up @@ -462,12 +462,8 @@ and c_exp' context exp k =
in
k' -@- cps_async
| PrimE (AwaitPrim s, [exp1]) ->
let r = match LabelEnv.find_opt Throw context with
| Some (Cont r) -> r
| _ -> assert false
in
let b = match LabelEnv.find_opt Cleanup context with
| Some (Cont r) -> r
let r, b = match LabelEnv.find_opt Throw context, LabelEnv.find_opt Cleanup context with
| Some (Cont r), Some (Cont b) -> r, b
| _ -> assert false
in
letcont k (fun k ->
Expand All @@ -477,7 +473,7 @@ and c_exp' context exp k =
cps_awaitE s (typ_of_var k) (t_exp context exp1) krb
| T.Await ->
c_exp context exp1
(meta (typ exp1) (fun v1 -> (cps_awaitE s (typ_of_var k) (varE v1) krb)))
(meta (typ exp1) (fun v1 -> cps_awaitE s (typ_of_var k) (varE v1) krb))
)
| DeclareE (id, typ, exp1) ->
unary context k (fun v1 -> e (DeclareE (id, typ, varE v1))) exp1
Expand Down
Loading
Loading