Stream: bugs

Topic: Broken Exercism exercises


view this post on Zulip Aurélien Geron (Jul 21 2026 at 16:29):

A bunch of Exercism exercises that used to work are now broken. Most errors are missing method is_eq. At first glance, it looks like it's nominal types (defined with :=) for unions. These used to have is_eq defined automatically, but it's no longer the case. Is this desired behavior or should I file an issue?

For example:

BinarySearchTree := [Nil, Node({ value : U64, left : BinarySearchTree, right : BinarySearchTree })].{
    ...
}

This no longer has is_eq defined automatically.

view this post on Zulip Aurélien Geron (Jul 21 2026 at 16:34):

It looks like it also concerns opaque nominal types. And they don't need to be recursive. For example:

CircularBuffer :: { data : List(I64), start : U64, length : U64 }.{ ... }

This used to have is_eq defined automatically.

view this post on Zulip Anton (Jul 21 2026 at 17:13):

This was indeed a deliberate decision, let me find the reason why...

view this post on Zulip Anton (Jul 21 2026 at 17:13):

You can just write is_eq : _, no need to implement it completely.

view this post on Zulip Anton (Jul 21 2026 at 17:27):

The reason is API-surface control for the type's author: implicit derivation on a nominal type silently makes a method part of your published API, which you then can't remove without a breaking change.

view this post on Zulip Aurélien Geron (Jul 21 2026 at 17:37):

Another exercise broke because of the error handling. Here's a simplified version:

f : I64 -> Try(I64, [Negative])
f = |x| {
    if x >= 0 Ok(2 * x) else Err(Negative)
}

g : I64 -> Try(I64, [Negative, TooBig])
g = |x| {
    y = f(x)?
    if y <= 20 Ok(x - 2) else Err(TooBig)
}

When I paste this in a roc repl, I get this error:

┌───────────────┐
│ TYPE MISMATCH ├─ This ? may return early with a type that doesn't match the function body. ─────────────────────────────────────────────────────────────────────────────────────────────┐
└┬──────────────┘                                                                                                                                                                         │
 │                                                                                                                                                                                        │
 │  y = f(x)?                                                                                                                                                                             │
 │      ‾‾‾‾‾                                                                                                                                                                             │
 └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── repl:7:9 ┘

    On error, this would return:

        Try(I64, [Negative])

    But the function body evaluates to:

        Try(I64, [Negative, TooBig])

    Hint: The error types from all ? operators and the function body must be compatible since any of them could be the actual return value.

I can fix this in a few different ways:

I don't really like any of these solutions. The first two imply that I know ahead of time how f is going to be used. The last one feels weird because the only error that f can return is Err(Negative) and we're basically saying "please turn this Err(Negative) into another Err(Negative). This confused me quite a bit at first. I feel like this should be automatic.

view this post on Zulip Aurélien Geron (Jul 21 2026 at 17:41):

Error management is one of the great things about Roc (it's actually what made me look into Roc in the first place), but tbh it's the one thing that I think I preferred in the old Roc, mostly because of this quirk.

view this post on Zulip Anton (Jul 21 2026 at 17:48):

I'll check why this change was made

view this post on Zulip Aurélien Geron (Jul 21 2026 at 18:00):

I believe that unions used to be open by default. Perhaps this caused some issues, such as reduced performance, so now they're closed by default, and I feel like this adds friction to error management. Maybe we can keep unions closed by default, but officially recommend that all unions used for errors should be open (e.g., [NotFound, ..]).

view this post on Zulip Anton (Jul 21 2026 at 18:01):

Anton said:

I'll check why this change was made

It's a bit hard to track, Luke asked about this as well here. Can you clear this up @Richard Feldman?

view this post on Zulip Aurélien Geron (Jul 21 2026 at 18:05):

Here's another breakage. Not sure whether I should file an issue:

SimpleLinkedList :: [Nil, Cons(U64, SimpleLinkedList)].{
    from_list : List(U64) -> SimpleLinkedList
    from_list = |list| list.fold(Nil, push)

    to_list : SimpleLinkedList -> List(U64)
    to_list = |linked_list| {
        match linked_list {
            Nil => []
            Cons(head, tail) => tail.to_list().append(head)
        }
    }

    push : SimpleLinkedList, U64 -> SimpleLinkedList
    push = |linked_list, item| Cons(item, linked_list)

    reverse : SimpleLinkedList -> SimpleLinkedList
    reverse = |linked_list| {
        #help : SimpleLinkedList, SimpleLinkedList -> SimpleLinkedList
        help = |result, rest| {
            match rest {
                Nil => result
                Cons(head, tail) => help(result->push(head), tail)
            }
        }
        help(Nil, linked_list)
    }
}

main! = |_| {
    linked_list = SimpleLinkedList.from_list([1,2,3,4])
    reversed_list = linked_list.reverse()
    dbg reversed_list.to_list()
    Ok({})
}

This code used to work fine but now causes the following error:

│ ANONYMOUS RECURSION ├─ I am inferring a recursive type that has no name somewhere in help. ────────────────────────────────────────────────┐
└┬────────────────────┘                                                                                                                      │
 │                                                                                                                                           │
 │  help = |result, rest| {                                                                                                                  │
 │  ‾‾‾‾                                                                                                                                     │
 └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── test05.roc:19:3 ┘

    Here is the type I'm inferring. You will see <RecursiveType> for parts of the type that repeat.

        [Cons(U64, <RecursiveType>), Nil]

    Hint: Recursive types are only allowed through nominal types. If you need a recursive data structure, define a nominal type using :=.

If I uncomment the help type spec, everything works fine.

view this post on Zulip Aurélien Geron (Jul 21 2026 at 18:09):

Mmh, this one might actually be a compiler bug. Even if I replace the last line of reverse with: help(SimpleLinkedList.(Nil), linked_list), I still get the error, even though the compiler should know all the types.

view this post on Zulip Jared Ramirez (Jul 21 2026 at 18:09):

Aurélien Geron said:

Another exercise broke because of the error handling. Here's a simplified version:

f : I64 -> Try(I64, [Negative])
f = |x| {
    if x >= 0 Ok(2 * x) else Err(Negative)
}

g : I64 -> Try(I64, [Negative, TooBig])
g = |x| {
    y = f(x)?
    if y <= 20 Ok(x - 2) else Err(TooBig)
}

When I paste this in a roc repl, I get this error:

┌───────────────┐
│ TYPE MISMATCH ├─ This ? may return early with a type that doesn't match the function body. ─────────────────────────────────────────────────────────────────────────────────────────────┐
└┬──────────────┘                                                                                                                                                                         │
 │                                                                                                                                                                                        │
 │  y = f(x)?                                                                                                                                                                             │
 │      ‾‾‾‾‾                                                                                                                                                                             │
 └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── repl:7:9 ┘

    On error, this would return:

        Try(I64, [Negative])

    But the function body evaluates to:

        Try(I64, [Negative, TooBig])

    Hint: The error types from all ? operators and the function body must be compatible since any of them could be the actual return value.

I can fix this in a few different ways:

I don't really like any of these solutions. The first two imply that I know ahead of time how f is going to be used. The last one feels weird because the only error that f can return is Err(Negative) and we're basically saying "please turn this Err(Negative) into another Err(Negative). This confused me quite a bit at first. I feel like this should be automatic.

i'm supprised this used to work! i woul not have expected this to work at anypoint in the new compiler!

the current recommendation is your first suggestion: f : I64 -> Try(I64, [Negative, ..]) (adding the ..)

edit for additional context:

in the rust compiler, we used something called polarity which allowed the caller of f to determine if the tag union should be open or closed. we decided to try not having it in the rewrite to see how it goes!

we may at some point add it (or something like it) back to make these kinds of cases more ergonimic!

view this post on Zulip Jared Ramirez (Jul 21 2026 at 18:11):

Aurélien Geron said:

Here's another breakage. Not sure whether I should file an issue:

SimpleLinkedList :: [Nil, Cons(U64, SimpleLinkedList)].{
    from_list : List(U64) -> SimpleLinkedList
    from_list = |list| list.fold(Nil, push)

    to_list : SimpleLinkedList -> List(U64)
    to_list = |linked_list| {
        match linked_list {
            Nil => []
            Cons(head, tail) => tail.to_list().append(head)
        }
    }

    push : SimpleLinkedList, U64 -> SimpleLinkedList
    push = |linked_list, item| Cons(item, linked_list)

    reverse : SimpleLinkedList -> SimpleLinkedList
    reverse = |linked_list| {
        #help : SimpleLinkedList, SimpleLinkedList -> SimpleLinkedList
        help = |result, rest| {
            match rest {
                Nil => result
                Cons(head, tail) => help(result->push(head), tail)
            }
        }
        help(Nil, linked_list)
    }
}

main! = |_| {
    linked_list = SimpleLinkedList.from_list([1,2,3,4])
    reversed_list = linked_list.reverse()
    dbg reversed_list.to_list()
    Ok({})
}

This code used to work fine but now causes the following error:

│ ANONYMOUS RECURSION ├─ I am inferring a recursive type that has no name somewhere in help. ────────────────────────────────────────────────┐
└┬────────────────────┘                                                                                                                      │
 │                                                                                                                                           │
 │  help = |result, rest| {                                                                                                                  │
 │  ‾‾‾‾                                                                                                                                     │
 └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── test05.roc:19:3 ┘

    Here is the type I'm inferring. You will see <RecursiveType> for parts of the type that repeat.

        [Cons(U64, <RecursiveType>), Nil]

    Hint: Recursive types are only allowed through nominal types. If you need a recursive data structure, define a nominal type using :=.

If I uncomment the help type spec, everything works fine.

this is expected, previously the annotation was not needed but caused incorrect inference in other situations. instead of the annotation, you could also do SimpleLinkedList.Nil instead of just Nil in the pattern match, for example

view this post on Zulip Aurélien Geron (Jul 21 2026 at 18:20):

Thanks @Jared Ramirez . Regarding error handling, here's one function in the exercise solution:

get_user : RestApi.Database, Str -> Try(RestApi.User, [NotFound, ..])
get_user = |database, name| {
    database.users
        .find_first(|user| user.name == name).map_err(|_| NotFound)
}

The .map_err(|_| NotFound) converts a closed NotFound into an open NotFound. I feel like this will trip up newcomers. Couldn't this be automatic? Alternatively, perhaps the standard library should only return open unions for errors.

view this post on Zulip Jared Ramirez (Jul 21 2026 at 18:23):

yeah, i'm interested in seeing if we can apply that transformation automatically if you use the ? suffix or something!

i think it would be ideal for funcitons like get_user to not have to be open, but still be composable in error chains

view this post on Zulip Bryce Miller (Jul 21 2026 at 18:34):

I was encountering some similar type errors in roc-pg and haven't had time to dig in and see if I broke something or if the compiler changed.

I'm a little confused about the different types of tag unions. Do I correctly understand that [Foo, ..] means "this tag union's tags can be combined with another union's tags to make a union that contains all tags in both unions" and that the syntax for this was previously [Foo] in the Rust compiler?

If the above is correct, then what's the difference in the current compiler between [Foo] and [Foo, ..[]] ?

view this post on Zulip Bryce Miller (Jul 21 2026 at 18:38):

The documentation references "open", "extensible", and "closed" tag unions, in addition to structural vs nominal.

view this post on Zulip Bryce Miller (Jul 21 2026 at 18:40):

Maybe this question belongs in Beginners; feel free to move it if you like.

view this post on Zulip Bryce Miller (Jul 21 2026 at 18:41):

Is there a way to write an error union of Foo and Bar such that I can guarantee that my function will never return other errors, but it can also unify/combine (whatever the correct term is) with error unions from other functions?

view this post on Zulip Aurélien Geron (Jul 21 2026 at 18:57):

Bryce Miller said:

I'm a little confused about the different types of tag unions. Do I correctly understand that [Foo, ..] means "this tag union's tags can be combined with another union's tags to make a union that contains all tags in both unions" and that the syntax for this was previously [Foo] in the Rust compiler?

Yes, I believe that's correct.
Not sure about your other questions though.

view this post on Zulip Jared Ramirez (Jul 21 2026 at 18:57):

Bryce Miller said:

If the above is correct, then what's the difference in the current compiler between [Foo] and [Foo, ..[]] ?

no difference!

view this post on Zulip Jared Ramirez (Jul 21 2026 at 18:58):

Bryce Miller said:

Is there a way to write an error union of Foo and Bar such that I can guarantee that my function will never return other errors, but it can also unify/combine (whatever the correct term is) with error unions from other functions?

yes, agree!

the current solution of returning [MyErr, ..] does _mostly_ do this (you can't return anything except MyErr in the function body), but callers then have to deal with the .. (for aggregation good, for other things like tests, not as good)

i think there are some options here:

  1. polarity (let the callsite determine open/closedness)
  2. having the compiler auto-add the mapping thag Aurélien proposed above (only for operations chained with ? suffix)
  3. maybe others??

edit: i'm late to the party haha, it looks something like 2 was added then removed, but the idea here of transforming is slightly different

view this post on Zulip Aurélien Geron (Jul 21 2026 at 19:02):

Oops, it looks like the unicode package is broken because of the changes to is_eq.

view this post on Zulip Aurélien Geron (Jul 21 2026 at 19:09):

To summarise the discussion so far, 14 exercise solutions broke because of recent changes to the compiler. Perhaps we should split this topic into three separate topics?

  1. is_eq is no longer auto-generated for nominal types. You can enable the default implementation using is_eq : _. I've fixed the exercise solutions, but the unicode package needs to be fixed too (@Luke Boswell, would you like a PR for that?).

  2. Anonymous recursive types are no longer allowed. I fixed 2 exercises by adding type annotations.

  3. Some exercises were broken due to incompatible open/closed unions for errors. I fixed those but there's some discussion about error management.

view this post on Zulip Aurélien Geron (Jul 21 2026 at 19:13):

@Jared Ramirez , indeed using SimpleLinkedList.Nil in the match fixes the issue, but I don't understand why the following code does not work (triggers an anonymous recursion error):

    reverse : SimpleLinkedList -> SimpleLinkedList
    reverse = |linked_list| {
        help = |result, rest| {
            match rest {
                Nil => result
                Cons(head, tail) => help(result->push(head), tail)
            }
        }
        help(SimpleLinkedList.Nil, linked_list)
    }

The compiler has all the information it needs, doesn't it? Both arguments of help are explicitly of type SimpleLinkedList.

view this post on Zulip Richard Feldman (Jul 22 2026 at 01:13):

yeah, so errors should always use open tag unions

view this post on Zulip Richard Feldman (Jul 22 2026 at 01:17):

I forget if this was discussed earlier in the thread, but the history here is:

view this post on Zulip Richard Feldman (Jul 22 2026 at 01:18):

I'm curious what people think based on how it's gone so far

view this post on Zulip Richard Feldman (Jul 22 2026 at 01:18):

e.g. was the way things worked in the final version of the Rust compiler more or less confusing compared to the current Zig compiler when it comes to tag union openness?

view this post on Zulip Luke Boswell (Jul 22 2026 at 01:19):

I haven't had many issues with the examples in basic-cli once I made the adjustment. I did have to write some helpers to widen the error union. But I haven't really explored the use cases properly, more just throwing examples together.

view this post on Zulip Jasper Woudenberg (Jul 22 2026 at 06:48):

I got a couple of errors but they were pretty easy to fix. Maybe the error could be clearer, if a closed union is used as the second argument of a Try(..) that's a pretty clear indication we might want to use an open tag union.

view this post on Zulip Bryce Miller (Jul 22 2026 at 12:06):

Now that I understand the implications of an open tag union being in the input vs output position, I think I'm unblocked in regards to understanding current Roc code.

While I don't love the idea of having a single type annotation represent possibly different types, I do also find the input/output position thing a bit unintuitive. The type of the open union might technically be the same regardless of whether it's returned by a function or accepted as a function argument, but the behavior is different. I guess the thing that's surprising to me is that I don't need to include a catch-all pattern for open tag unions when they were returned from a function.

view this post on Zulip Bryce Miller (Jul 22 2026 at 12:27):

And I guess perhaps another surprising thing is that if I have a closed union that was returned from a function, I need to map the tags to get them to unify with tags of the same name (if I’m correctly reading this https://roc.zulipchat.com/#narrow/channel/304641-ideas/topic/handling.20err.20tag.20accumulation/with/612117635).

That mapping is something I’d expect for nominal tag unions, but I was surprised to learn that it’s sometimes necessary for structural ones too.

Not sure how often this will come up in practice if we just always use open unions for errors though.


Last updated: Jul 23 2026 at 13:15 UTC