Stream: bugs

Topic: Annotation dependent inference when destructuring opaque


view this post on Zulip Jonathan (Jul 17 2026 at 16:59):

I'm not sure if there's some syntax I don't yet know, but I'm getting a type error only when I don't annotate push or pop from the following:

Stack(v) :: {s: List(v)}.{
    push : Stack(v), v -> Stack(v)
    push = |{s}, v| {
        {s: s.append(v)}
    }

    pop : Stack(v) -> (Stack(v), Try(v, [ListWasEmpty]))
    pop = |s| {
        last = s.last()
        dropped = s.drop_last(1)
        (Stack.{s: dropped}, last)
    }
    new = || {Stack.{s: []}}
}

In both push and pop, the opaque backing type is destructured to get the list. If I use without annotations, I get the following error:

┌───────────────┐
│ TYPE MISMATCH ├─ The push method on Stack has an incompatible type. ────────────────────────────┐
└┬──────────────┘                                                                                 │
 │                                                                                                │
 │  $s = $s.push(2)                                                                               │
 │       ‾‾                                                                                       │
 └───────────────────────────────────────────── main.roc:54:10 ┘

    The method push has the type:

        { s: a }, b -> { s: c } where [a.append : a, b -> c]

    But I need it to have the type:

        Stack(v), a -> _ret
          where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]

Adding in annotations the error goes away. Here is an example use for reproduction

main! = |_| {
    var $s = Stack.new()
    $s = $s.push(2)
    Ok({})
}

view this post on Zulip Jared Ramirez (Jul 17 2026 at 17:39):

Instead of Stack.{s}, you can use Stack.({x})! But I think that pop would still not work without the annotation (the compiler needs some way to know that s is Stack)

view this post on Zulip Jared Ramirez (Jul 17 2026 at 17:41):

Jonathan said:

view this post on Zulip Jared Ramirez (Jul 17 2026 at 17:42):

More broadly, this is a known (and not really solvable) constraint. In many cases, the compiler can figure out how to lift structural values (eg { ... } or a tag) into a nominal (eg Stack), but not always. In these cases, either annotation or explicitly using the nominal constructor is necessary (eg Stack.({ ... }))

view this post on Zulip Jared Ramirez (Jul 17 2026 at 17:44):

All that said, Stack.{s} should work, so that is definitely a bug. I will make an issue & fix

view this post on Zulip Jonathan (Jul 17 2026 at 17:54):

Stack.({s})

Ahh :face_palm: Thank you, that's bitten me a few times now.

Stack.{s} should work

Is it conceptually different from Stack(.. or just equivalent sugar?

has no special access to Stack internals

I don't follow here, isn't this the point of opaque? From the langref:

outside its defining module the backing representation is hidden, so the type can only be created and inspected through the methods that module exposes.

view this post on Zulip Jared Ramirez (Jul 17 2026 at 18:03):

equivalent sugar!

then sorry, my wording was confusing there:

so more on that second point: for all accessible nominal types (eg an opaque nominal in the defining module OR a transparent Nominal anywhere), the compiler can coerce that nominal's backing type into the nominal itself, is it's clear from surrounding context. eg:

xxx : Stack
xxx = { s: [ ... ] }

works, because we know that xxx should be Stack, so we can coerce the plain record

what I meant was that inside a nominal types method block (Stack := { s: List }.{ ... }), there no extra coercion logic, just the regular coercion I just described

view this post on Zulip Jared Ramirez (Jul 17 2026 at 18:14):

I have a PR up for |Stack.{s}|! https://github.com/roc-lang/roc/pull/10190

view this post on Zulip Jonathan (Jul 17 2026 at 19:20):

Jared Ramirez said:

what I meant was that inside a nominal types method block (Stack := { s: List }.{ ... }), there no extra coercion logic, just the regular coercion I just described

ahh, gotcha. Thanks!

view this post on Zulip Jonathan (Jul 17 2026 at 19:31):

Thank you, that's bitten me a few times now.

And also, I'm not sure what to suggest, but I have found it a bit easy to mix up the similar syntaxes |S.({x})|/|S.{x}| and |S({x})| for two distinct types. I don't imagine I'll be the last, perhaps an error hint for this kind of thing?


Last updated: Jul 23 2026 at 13:15 UTC