Stream: beginners

Topic: error unification


view this post on Zulip Anton (Jul 02 2024 at 17:46):

This should work right?

list : Task (List Str) *
list =
    Effect.args
    |> Effect.map Ok
    |> InternalTask.fromEffect

foo =
    Task.await list \argList ->
        Task.err (Exit 0 "")

Error message:

── TYPE MISMATCH in Arg.roc ────────────────────────────────────────────────────

This 2nd argument to await has an unexpected type:

15│>      Task.await list \argList ->
16│>          Task.err (Exit 0 "")

The argument is an anonymous function of type:

    List Str -> Task * [Exit (Num *) Str]

But await needs its 2nd argument to be:

    List Str -> Task * *

view this post on Zulip Anton (Jul 02 2024 at 17:47):

I don't understand how this function can work but the code I posted does not.

view this post on Zulip Brendan Hansknecht (Jul 02 2024 at 19:14):

No, I don't think it should work based on our current type system
......
But man, I hate * a lot of the time.

view this post on Zulip Brendan Hansknecht (Jul 02 2024 at 19:14):

I mean I love the clarity of it, but I hate how untuitive it can make bugs

view this post on Zulip Brendan Hansknecht (Jul 02 2024 at 19:16):

Task.await: Task a b, (a -> Task c b) -> Task c b

view this post on Zulip Brendan Hansknecht (Jul 02 2024 at 19:18):

Task a b ~ Task (List Str) *
a = List Str
b = * (note, this is the not in an output position, it is closed and can not merge with anything)

a -> Task c b ~ \argList -> Task.err (Exit 0 "")
c = *
b = [Exit (Num *) Str]

# we have just declared `b` as two different types...bug

view this post on Zulip Brendan Hansknecht (Jul 02 2024 at 19:19):

I honestly think that * should be restricted to only being allowed to be used in function signatures.

view this post on Zulip Brendan Hansknecht (Jul 02 2024 at 19:21):

Then list would be:
list : Task (List Str) [].
The error becomes more clear. And you would probably change the type to:
list : Task (List Str) _
Or more accurately:
list : Task (List Str) []*

view this post on Zulip Brendan Hansknecht (Jul 02 2024 at 19:24):

I don't understand how this function can work but the code I posted does not.

I think it works due to a compiler bug. I bet that we are interpreting [] as an open tag instead of a closed tag.

view this post on Zulip Brendan Hansknecht (Jul 02 2024 at 19:25):

Arg.list should be list: Task (List Str) []*

view this post on Zulip Anton (Jul 03 2024 at 12:44):

Thanks for the explanation @Brendan Hansknecht :heart:

With []* I still can't get it to work, I've further simplified my code:

app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br" }

import pf.Stdout
import pf.Task exposing [Task]

okStr : Task Str []*
okStr =
    Task.ok "aa"

foo =
    Task.await okStr \str ->
        Task.err (MyErr str)

main =
    Stdout.line! "Hello, World!"

This is the error message:

TYPE MISMATCH

This 2nd argument to `await` has an unexpected type:

11│>      Task.await okStr \str ->
12│>          Task.err (MyErr str)

The argument is an anonymous function of type:

    Str -> Task * [MyErr Str]

But `await` needs its 2nd argument to be:

    Str -> Task * []*

Tip: Looks like a closed tag union does not have the `MyErr`
tag.

Tip: Closed tag unions can't grow, because that might change
the size in memory. Can you use an open tag union?

From the error message it seems like the compiler just erroneously detected a closed tag union?

I can use okStr : Task Str []_, but type inference makes this okStr : Task Str [MyErr Str]. This would require everyone who uses okStr to handle the MyErr Str error which can not actually happen.

view this post on Zulip Sam Mohr (Jul 03 2024 at 12:55):

Anton said:

I can use okStr : Task Str []_, but type inference makes this okStr : Task Str [MyErr Str]. This would require everyone who uses okStr to handle the MyErr Str error which can not actually happen.

If you write the same thing using Results, which are just an alias instead of an opaque type, can you still not use []*?

view this post on Zulip Sam Mohr (Jul 03 2024 at 12:58):

I've been running into this issue with Tasks for basic-cli, the current solution hack is to do

runTask : Task ok {}

runTask |> Task.result! |> Result.withDefault foo

view this post on Zulip Sam Mohr (Jul 03 2024 at 13:00):

It feels like opaque types prevent full type constraining from happening, in that Task doesn't actually look like a single tag Union to the compiler, though I'm not sure on this.

view this post on Zulip Richard Feldman (Jul 03 2024 at 13:20):

is this related to let polymorphism? What happens if you do okStr : {} -> Task Str []* instead?

view this post on Zulip Sam Mohr (Jul 03 2024 at 13:57):

Richard Feldman said:

is this related to let polymorphism? What happens if you do okStr : {} -> Task Str []* instead?

If I change the Arg.list signature to list : U64 -> Task (List Str) []* I still get an error:

── TYPE MISMATCH in ../basic-cli/examples/../platform/Arg.roc ──────────────────

Something is off with the body of the parse definition:

83│  parse : CliParser state -> Task state [Exit I32 Str, StdoutErr Stdout.Err]
84│  parse = \parser ->
85│      # Stdout.line! "Parsing args..."
86│      arguments = list! 0
                     ^^^^^^^

This await call produces:

    Task state []*

But the type annotation on parse says it should be:

    Task state [
        Exit I32 Str,
        StdoutErr Stdout.Err,
    ]

Tip: Looks like a closed tag union does not have the StdoutErr and
Exit tags.

Tip: Closed tag unions can't grow, because that might change the size
in memory. Can you use an open tag union?

view this post on Zulip Anton (Jul 03 2024 at 14:28):

If you write the same thing using Results, which are just an alias instead of an opaque type, can you still not use []*?

Yeah, I get the same thing using Result

view this post on Zulip Anton (Jul 03 2024 at 14:40):

What happens if you do okStr : {} -> Task Str []* instead?

That works!

view this post on Zulip Richard Feldman (Jul 03 2024 at 15:52):

Sam Mohr said:

Richard Feldman said:

is this related to let polymorphism? What happens if you do okStr : {} -> Task Str []* instead?

If I change the Arg.list signature to list : U64 -> Task (List Str) []* I still get an error:

this one surprises me - @Ayaz Hafiz any ideas why it would be saying those don't unify?

view this post on Zulip Ayaz Hafiz (Jul 03 2024 at 17:44):

I'm not sure, it's probably worth expanding out the types under the Task alias

view this post on Zulip Brendan Hansknecht (Jul 04 2024 at 00:24):

Related issue I think:

The effect:

ffiLoad : Str  -> Effect (Result U64 Str)
ffiClose : U64 -> Effect (Result {} *)
ffiCall : U64, Str -> Effect (Result {} *)

With call : Lib, Str -> Task {} *. Which should work based on ffiClose working and being close : Lib -> Task {} *:

Something is off with the body of the call definition:

46│   call : Lib, Str -> Task {} *
47│   call = \@Lib lib, fnName ->
48│>      Effect.ffiCall lib fnName
49│>      |> InternalTask.fromEffect

This InternalTask.fromEffect call produces:

    InternalTask.Task {} []

But the type annotation on call says it should be:

    Task.Task {} *

Changing to Task {} [] cause that seems to be what the error is telling me to do.

Something is off with the body of the call definition:

46│   call : Lib, Str -> Task {} []
47│   call = \@Lib lib, fnName ->
48│>      Effect.ffiCall lib fnName
49│>      |> InternalTask.fromEffect

This InternalTask.fromEffect call produces:

    InternalTask.Task {} err

But the type annotation on call says it should be:

    Task.Task {} []

view this post on Zulip Brendan Hansknecht (Jul 04 2024 at 01:52):

Oh, I think I just realized something!

So if you dump the ir of the examples/tcp-client.roc, the call to roc_fx_tcpClose is incorrect. We can not have open tags in effects. They mean that the output has a dynamic layout that the platform is unaware of.

This is the effect: tcpClose : U64 -> Effect (Result {} *)
This is what rust expects: (u64) -> RocResult<(), ()>
This is the signiture that llvm is generating: declare void @roc_fx_tcpClose(ptr sret({ [0 x i64], [6 x i64], i8, [7 x i8] }), i64)
So the first arg is the result type and the second arg is the actual function input.
That means, it expects a result type of { [0 x i64], [6 x i64], i8, [7 x i8] }
i8, [7 x i8] at the end is a tag for Ok/Err.
[0 x i64] contains no data, so we don't care about it.
[6 x i64] is 48 bytes of unaccounted for data.
What does that data represent? I'm pretty sure it's the union of all of the error types that build up in the task chain. In this case, the largest potential payload is TcpPerformErr (TcpReadErr (Unrecognized Str)) which is a Str (3 x i64) wrapped in a tag (i8, [7 x i8] aka 1xi64), wrapped in a tag (1xi64), wrapped in a tag (1xi64). For a total of 6 x i64.

This means that roc_fx_tcpClose is returning a different type from rust than llvm is interpreting it as. There definitely needs to be a mapping generated here.

As a note, at some point we might want an optimization to collapse those tags together or avoid padding in some way.

view this post on Zulip Richard Feldman (Jul 04 2024 at 02:04):

whoa, great find!

view this post on Zulip Luke Boswell (Jul 04 2024 at 02:41):

Nice, so we should replace the wildcards in our effects/platformTasks with the unit type I guess.

view this post on Zulip Luke Boswell (Jul 04 2024 at 02:45):

So have a fixed closed tag provided to the host (e.g. effect/platform task), and to the app we can still have an open tag.

view this post on Zulip Brendan Hansknecht (Jul 04 2024 at 02:46):

Yeah, this is probably something that needs to be codified as a restriction in the compiler

view this post on Zulip Richard Feldman (Jul 04 2024 at 12:14):

oh I think we definitely want to support it

view this post on Zulip Richard Feldman (Jul 04 2024 at 12:14):

I think we need to fix the code gen :big_smile:

view this post on Zulip Brendan Hansknecht (Jul 04 2024 at 13:29):

How would we support it? I think that we might need to build a restricted type in rust and then have roc explicitly map the error/expand the type.

view this post on Zulip Brendan Hansknecht (Jul 04 2024 at 13:30):

Like I dont think rust can construct the error tag in general here. Cause it only knows the local errors, not the full set of errors that the app unified with.

view this post on Zulip Richard Feldman (Jul 04 2024 at 13:34):

oh sorry, is this in the platform?

view this post on Zulip Richard Feldman (Jul 04 2024 at 13:34):

yeah the platform shouldn’t be able to tell the host it has an unbound variable

view this post on Zulip Brendan Hansknecht (Jul 04 2024 at 13:37):

The platform specifies an effect like:
Effect (Result {} *)

Roc is generate code that is expecting it to return a Result {} UnifiedErrorTag

view this post on Zulip Richard Feldman (Jul 04 2024 at 14:53):

ahh yeah, I see what you mean avoiding restricting it now - yeah, that should be a compile-time error

view this post on Zulip Anton (Jul 05 2024 at 12:41):

I'm making the suggested changes to tcpClose. Given that we always return ok here, should we not be returning {} instead, just like roc_fx_ttyModeCanonical?

#[no_mangle]
pub extern "C" fn roc_fx_tcpClose(stream_id: u64) -> RocResult<(), ()> {
    TCP_STREAMS.with(|tcp_streams_local| {
        tcp_streams_local.borrow_mut().remove(&stream_id);
    });

    RocResult::ok(())
}

view this post on Zulip Anton (Jul 05 2024 at 14:25):

Sidenote: For those messing with this general error unification problem, you may hit a case where you can not remove the * in []* even though it is reported as unnecessary. I made #6873 for that issue.


Last updated: Jul 05 2025 at 12:14 UTC