Stream: contributing

Topic: Await bang desugaring rules


view this post on Zulip Kiryl Dziamura (Jun 29 2024 at 10:53):

I'm currently looking into an issue with await bang desugaring in defs and want to clarify my understanding:

If suffixed expression is part of another expression - it should be desugared like this:

main =
    a b!
main =
    Task.await b \#!a0 ->
        a #!a0

But if it's a top level bang - it should return an empty record:

main =
    a!
main =
    Task.await a \{} ->
        Task.ok {}

Meaning, the following two snippets have different type signatures (let's assume a is Task U8):

main : Task {}
main =
    a!
main : Task U8
main =
    a

Currently it works inconsistently (can't provide an exhaustive list of problems unfortunately, but here's the ticket I'm working on). I just want to clarify that my intuition is correct so I can fix this logic in parser and desugaring.

view this post on Zulip Luke Boswell (Jun 29 2024 at 12:25):

But if it's a top level bang - it should return an empty record:

I think in this case it won't be unwrapped, but just ignored.

view this post on Zulip Luke Boswell (Jun 29 2024 at 12:28):

In the implementation of apply_task_await there are two functions which should detect this case is_matching_empty_record and is_matching_intermediate_answer and just make this a NOOP

view this post on Zulip Luke Boswell (Jun 29 2024 at 12:30):

So for these two examples you have shown, the first;

main =
    Task.await b \#!a0 ->
        a #!a0

Should hit the is_matching_intermediate_answer and just return the b expr.

For the second;

main =
    Task.await a \{} ->
        Task.ok {}

This should hit the is_matching_empty_record and just return the a expr.

view this post on Zulip Kiryl Dziamura (Jun 29 2024 at 12:48):

Luke Boswell said:

For the second;

main =
    Task.await a \{} ->
        Task.ok {}

This should hit the is_matching_empty_record and just return the a expr.

But it’s not the same. If I wrote it manually, I wouldn’t expect Task.ok {} would become a

The first

Could you please elaborate?

view this post on Zulip Kiryl Dziamura (Jun 29 2024 at 14:36):

Another way to think about it. I think it should work this way:

Invalid

f : Bool, Task a, Task b -> Task {}
f = \x, a, b ->
    if x then
        a
    else
        b

Valid

f : Bool, Task a, Task b -> Task {}
f = \x, a, b ->
    if x then
        a!
    else
        b!

view this post on Zulip Brendan Hansknecht (Jun 29 2024 at 16:07):

Ah, this is a question if bang should throw away the result

view this post on Zulip Brendan Hansknecht (Jun 29 2024 at 16:09):

I think this is a case where the exact semantics of bang are a bit complex

view this post on Zulip Brendan Hansknecht (Jun 29 2024 at 16:09):

And arguably inconsistent

view this post on Zulip Brendan Hansknecht (Jun 29 2024 at 16:11):

I think the expected behaviour above is a type mismatch in both cases

view this post on Zulip Brendan Hansknecht (Jun 29 2024 at 16:14):

These would be correct though
I think this is the expected behaviour:

f : Bool, Task a, Task a -> Task a
f = \x, a0, a1 ->
    if x then
        a0!
    else
        a1!

g : Task a -> Task a
g = \a ->
    a!

h : Bool, Task a, Task a, Task b -> Task b
h = \x, a0, a1, b ->
    if x then
        a0!
    else
        a1!

    b!

view this post on Zulip Brendan Hansknecht (Jun 29 2024 at 16:15):

I think the correct mental model is that ! always tries to return a result. It does not end with a Task.ok {}

That said, a following bang will throw away the result of the previous bang.

view this post on Zulip Kiryl Dziamura (Jun 29 2024 at 16:19):

My take is that bang is expression if it’s part of another expression. If it’s top-level (in place of assignment) - it’s a destructuring statement. I.e:

main =
    a!
    b!
    c!
main =
    {} = a!
    {} = b!
    {} = c!
    {}

view this post on Zulip Brendan Hansknecht (Jun 29 2024 at 16:22):

Desugared with the redundant await (which wouldn't actually be emitted, but I think make the desugaring clearer) these would be:

f : Bool, Task a, Task a -> Task a
f = \x, a0, a1 ->
    Task. await (
        if x then
            a0
        else
            a1
    ) \a -> Task.ok a

g : Task a -> Task a
g = \a ->
    Task. await a \a2 -> Task.ok a2

h : Bool, Task a, Task a, Task b -> Task b
h = \x, a0, a1, b ->
    Task. await (
        if x then
            a0
        else
            a1
    ) \a ->
        # It can be imgained that there is an igorned `_ = Task.ok a` here.
        Task.await b \b2 -> Task.ok b2

view this post on Zulip Brendan Hansknecht (Jun 29 2024 at 16:24):

My take is that bang is expression if it’s part of another expression

This can be debated in ideas, but the current design is to explicitly allow a trailing ! which essentially doesn't actually desugar.

view this post on Zulip Brendan Hansknecht (Jun 29 2024 at 16:24):

Should create the semantic I show in the desugaring directly above.

view this post on Zulip Kiryl Dziamura (Jun 29 2024 at 16:26):

https://github.com/roc-lang/roc/issues/6851

So this fix was correct then?

view this post on Zulip Brendan Hansknecht (Jun 29 2024 at 16:33):

I don't know that code enough to tell for sure.

Fundamentally, with the current design, ending a function with a! or with a should result in the exact same final result.

view this post on Zulip Kiryl Dziamura (Jun 29 2024 at 17:27):

So in theory, to return an empty record, I’d do it explicitly? (I’m from mobile rn, can’t check)

main =
    a!
    b!
    c!
    {}

view this post on Zulip Luke Boswell (Jun 29 2024 at 20:15):

I think supporting the final or trailing expression so it doesnt get unwrapped is why we have EmptyDefsFinal.

view this post on Zulip Brendan Hansknecht (Jun 29 2024 at 20:36):

It currently would need to be:

main =
    a!
    b!
    c!
    Task.ok {}

We talked about removing the need for the Task.ok, don't recall which thread, but I don't think anything was decided around that.

view this post on Zulip Brendan Hansknecht (Jun 29 2024 at 20:37):

Assuming c! returns a Task c where c is not {}

view this post on Zulip Kiryl Dziamura (Jun 29 2024 at 20:38):

From what I saw in the code, EmptyDefsFinal unwraps into Task.ok {} and is used only if the last statement in defs is the bang. Thus why I started this thread - I removed EmptyDefsFinal completely and it works as @Brendan Hansknecht outlined. But then I looked at the code again and understood that it was probably a different intention.

view this post on Zulip Kiryl Dziamura (Jun 29 2024 at 20:49):

Explicit {} return is reasonable because a! desugared to a is intuitive. As I said, that’s how I expected it to work initially. However, the other way makes sense as well, just another mental model. I’ll describe my thoughts in #ideas later. The current thread is for fixing the bug without introducing breaking changes.


Last updated: Jul 06 2025 at 12:14 UTC