Stream: compiler development

Topic: combining tags


view this post on Zulip Brendan Hansknecht (Aug 03 2024 at 02:39):

So, I'm not fully sure what is going wrong, but I feel like my mental model for combining tags isn't working. Currently, my sqlite PR isn't working due to breaking during compilation and it seems to be due to tag combining. Fundamentally, I have some errors that are innate to interacting with sql in various ways and some that would be user defined. I am trying to use them together in a flat union instead of adding extra unnecessary layers.

So for the sqlite result decoder, we have the base error:

SqlDecodeErr err : [FieldNotFound Str, SqlError Code Str]err
SqlDecode a err := List Str -> (Stmt -> Task a (SqlDecodeErr err))

Then it is used with various other error types and attempted to be merged together.
For example, if you query for exactly 1 result, it adds two more possible errors:

RowCountErr err : [NoRowsReturned, TooManyRowsReturned]err

So then in the full function call, we add some layers of nesting to build of the full tag as SqlDecodeErr (RowCountErr err).

This leads to the function type I would want to write:

decodeExactlyOneRow : Stmt, SqlDecode a err -> Task a (SqlDecodeErr (RowCountErr err))

Sadly, this fails to check. Cause the input err type does not know about the RowCountErrs. Roc will not expand the tag. So it is a type mismatch with unexpected types:

The argument is an anonymous function of type:

    a -> Task a [
        SqlError InternalSql.SqliteErrCode …,
        TooManyRowsReturned,
    ]

But this function needs its 2nd argument to be:

    a -> Task a [SqlError Code …, …]err

So I have to type the input error type the same as the output error type to force roc to allow them to unify happily:

decodeExactlyOneRow : Stmt, SqlDecode a (RowCountErr err) -> Task a (SqlDecodeErr (RowCountErr err))

This now passes roc check, but fails to compile:

thread '<unnamed>' panicked at crates/compiler/mono/src/ir.rs:6124:9:
assertion failed: unspecialized.is_empty()
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

My general questions are:

  1. should this work?
  2. If not, is there a similar solution that would enable this sort of error merging/propagation? I am trying to make this work like Task usually does where all of the various error types just merge.
  3. If no to both of the above, how would we expect and api like this to be written.

view this post on Zulip Sam Mohr (Aug 03 2024 at 05:56):

Have you tried making the error tag union here an open union?

    a -> Task a [
        SqlError InternalSql.SqliteErrCode …,
        TooManyRowsReturned,
    ]*

I don't think it should be necessary, but that could be a type bug

view this post on Zulip Brendan Hansknecht (Aug 03 2024 at 07:09):

I'm not sure how to make it open given it is generated from merging tags.

It is a SqlDecodeErr (RowCountErr err)

If I substitute types, that is a [FieldNotFound Str, SqlError Code Str]([NoRowsReturned, TooManyRowsReturned]err)

view this post on Zulip Richard Feldman (Aug 03 2024 at 12:01):

I think

Brendan Hansknecht said:

This leads to the function type I would want to write:

decodeExactlyOneRow : Stmt, SqlDecode a err -> Task a (SqlDecodeErr (RowCountErr err))

Sadly, this fails to check. [...] So I have to type the input error type the same as the output error type to force roc to allow them to unify happily:

decodeExactlyOneRow : Stmt, SqlDecode a (RowCountErr err) -> Task a (SqlDecodeErr (RowCountErr err))

I think the compiler is correct here; in general, a type alias means essentially "inline this type wherever you see the alias," and looking at the type signatures after inlining reveals the problem:

Stmt, SqlDecode a err -> Task a (SqlDecodeErr (RowCountErr err))
# after inlining the type aliases:
Stmt, SqlDecode a err -> Task a [FieldNotFound Str, SqlError Code Str][NoRowsReturned, TooManyRowsReturned]err

so after inlining, we can see that in SqlDecode a err the err is referring to "all the other tags in the error union except [FieldNotFound Str, SqlError Code Str][NoRowsReturned, TooManyRowsReturned]" because that's what [...]err means.

view this post on Zulip Richard Feldman (Aug 03 2024 at 12:01):

Here's inlining the other one:

Stmt, SqlDecode a (RowCountErr err) -> Task a (SqlDecodeErr (RowCountErr err))
# after inlining the type aliases:
Stmt, SqlDecode a [NoRowsReturned, TooManyRowsReturned]err -> Task a [FieldNotFound Str, SqlError Code Str][NoRowsReturned, TooManyRowsReturned]err

view this post on Zulip Richard Feldman (Aug 03 2024 at 12:03):

I'm a little surprised that one type-checks; I'd expect it to have to be:

Stmt, SqlDecode a (SqlDecodeErr (RowCountErr err)) -> Task a (SqlDecodeErr (RowCountErr err))

view this post on Zulip Richard Feldman (Aug 03 2024 at 12:07):

@Folkert de Vries and @Ayaz Hafiz probably have a more accurate mental model than I do of how these are unifying though :big_smile:

view this post on Zulip Brendan Hansknecht (Aug 03 2024 at 16:16):

The second one checks. SqlDecode adds a SqlDecodeErr internally

view this post on Zulip Brendan Hansknecht (Aug 03 2024 at 16:17):

That said, even though it type checks it crashes the compiler.

view this post on Zulip Ayaz Hafiz (Aug 03 2024 at 16:36):

I think this is a bug

view this post on Zulip Ayaz Hafiz (Aug 03 2024 at 16:36):

pretty sure the first version should work

view this post on Zulip Brendan Hansknecht (Aug 03 2024 at 17:20):

are type variable for tags open or closed? Is there anyway to control that?

Also, Richard, I think the original type should work cause SqlDecode a err is a lambda with the final lambda doing: (Stmt -> Task a (SqlDecodeErr err)). Since output tags are supposed to be open. This should return Task with an open tag output.

Oh, does the open tag in return types work if the tag is nested in another type. Like I32 -> [Even, Odd] will return an open tag. Will I32 -> Task [Even, Odd] ... return an open tag?

view this post on Zulip Brendan Hansknecht (Aug 04 2024 at 17:38):

A little update on this. The crash is due to an error with an empty tag in it. Apparently roc requires that to be typed or crash:

Builder func:

intDecoder : (I64 -> Result a err) -> (Str -> SqlDecode a [FailedToDecodeInteger err]UnexpectedTypeErr)
intDecoder = \cast ->
    decoder \val ->
        when val is
            Integer i -> cast i |> Result.mapErr FailedToDecodeInteger
            _ -> toUnexpectedTypeErr val

I64 case. This crashes if the type isn't specified. I think it is due to [].

i64 : Str -> SqlDecode I64 [FailedToDecodeInteger []]UnexpectedTypeErr
i64 = intDecoder Ok

view this post on Zulip Brendan Hansknecht (Aug 04 2024 at 17:39):

I guess I am calling mapErr on a Result I64 []. Not sure what that is expected to do.


Last updated: Jul 06 2025 at 12:14 UTC