Stream: compiler development

Topic: Is this a bug?


view this post on Zulip Brendan Hansknecht (Apr 20 2024 at 15:42):

Spinoff from #beginners > combining open unions, is the following a bug?

interface Test
    exposes []
    imports []

generateOpenOkStr : Str -> [Ok Str]*
generateOpenOkStr = \s ->
    Ok s

restrictsToClosed : [Ok Str] -> Bool
restrictsToClosed = \Ok s->
    !(Str.isEmpty s)

expect
    # `in` is an open tag union, returned as `[Ok Str]*`.
    # Yet, if I type it as such, it breaks due to type mismatch
    in : [Ok Str]*
    in = generateOpenOkStr ""
    out = restrictsToClosed in
    out == Bool.false

view this post on Zulip Brendan Hansknecht (Apr 20 2024 at 15:42):

This 1st argument to restrictsToClosed has an unexpected type:

18│      out = restrictsToClosed in
                                 ^^

This in value is a:

    […]*

But restrictsToClosed needs its 1st argument to be:

    […]

view this post on Zulip Brendan Hansknecht (Apr 20 2024 at 15:43):

Works if in : [Ok Str]* is commented out.

view this post on Zulip Anton (Apr 20 2024 at 15:47):

It does work with in : [Ok Str]_ I think it's that bug, I'll check if we have an issue for that.

view this post on Zulip Anton (Apr 20 2024 at 15:54):

No, #5660 is different, perhaps this is intended behavior, given that the error message states:

(The * means something different when the tag union is an argument to a
function, though!)

view this post on Zulip Brendan Hansknecht (Apr 20 2024 at 15:57):

Yeah, it may be a proper error. That said, it definitely feels wrong as a proper error given I am returning a [Ok Str]* from the function.

view this post on Zulip Brendan Hansknecht (Apr 20 2024 at 15:58):

Am I storing the return type [Ok Str]* or the argument type [Ok Str]*? Very unclear.

view this post on Zulip Brendan Hansknecht (Apr 20 2024 at 15:58):

Whatever the case, I definitely think we need a clearer explanation here at a minimum.

view this post on Zulip timotree (Apr 20 2024 at 17:51):

I think this is a reasonable way for * and _ to work. I can reproduce this behavior with List * and List _ and I think it makes sense

constructList : List _
constructList =
    empty : List _
    empty = []
    list1 : List _
    list1 = List.append empty 1 # no error
    list1

constructList2 : List *
constructList2 =
    empty : List *
    empty = []
    list1 : List *
    list1 = List.append empty 1 # ERROR (mismatch between Num * and *)
    list1

view this post on Zulip timotree (Apr 20 2024 at 17:53):

The mental model here is that within a top-level declaration, * is a placeholder instantiated by each caller and _ is a placeholder which we get to instantiate

view this post on Zulip timotree (Apr 20 2024 at 17:54):

Any time you write myLocal = myTopLevel where myTopLevel : MyType *, you get myLocal : MyType _. This is exactly what it means that * is chosen by each caller; we are a caller of myTopLevel, so now we get to choose how to instantiate the *

view this post on Zulip Brendan Hansknecht (Apr 20 2024 at 17:56):

I agree that without function calls, that all makes sense. I think it feels the worst when returning something from a function:

getEmpty: {} -> List *
getEmpty = \{} -> []

# I know why this doesn't work, but it feels like it should work.
# `empty` is simply specifying the exact same type as what was returned by `getEmpty`
constructList : List _
constructList =
    empty : List *
    empty = getEmpty {}
    list1 = List.append empty 1 # ERROR (mismatch between Num * and *)
    list1

I wonder if it would help to have some sort of explicit input type vs return type *....not sure, but this definitely is confusing even if it can be logically explained.

view this post on Zulip timotree (Apr 20 2024 at 17:57):

But constructList can't possibly have type List * there, right? The list it returns always include a number

view this post on Zulip Brendan Hansknecht (Apr 20 2024 at 17:57):

Sorry, meant List _ or List (Num *) there.

view this post on Zulip timotree (Apr 20 2024 at 18:03):

What if we changed the meaning of * inside of type annotations for locals to mean the same thing as _?

view this post on Zulip timotree (Apr 20 2024 at 18:05):

I don't think that would result in any new type errors, because we can always assign _ to * if needed. e.g. the following typechecks

takesAnyList : List * -> {}
takesAnyList = \anyList ->
    underscoreList : List _
    underscoreList = anyList # no error
    {}

view this post on Zulip timotree (Apr 20 2024 at 18:06):

But it has the downside of losing some precision in what type annotation are expressible. You could no longer assert that a local has a type which is indeed chosen by the caller.

view this post on Zulip timotree (Apr 20 2024 at 18:11):

Ooh there is some interesting non-uniformity with how local function definitions behave here

myNested : List U32
myNested =
    myEmpty : {} -> List *
    myEmpty = \{} -> []
    List.append (myEmpty {}) 1 # no error, but if we change to `myEmpty : List *` we get an error like before

view this post on Zulip timotree (Apr 20 2024 at 18:15):

oh actually this non-uniformity is observable even without type annotations:

nestedPolymorphism : (List U32, List Str)
nestedPolymorphism =
    # myEmpty : {} -> List *
    myEmpty = \{} -> []
    (List.append (myEmpty {}) 1, List.append (myEmpty {}) "1")

failedPolymorhpism : (List U32, List Str)
failedPolymorhpism =
    # myEmpty : List _
    myEmpty = []
    (List.append myEmpty 1, List.append myEmpty "1") # ERROR (mismatch between Str and Num *)

I guess local function definitions can be polymorphic but local values cannot

view this post on Zulip Brendan Hansknecht (Apr 20 2024 at 18:19):

Yeah, requiring functions for polymorphism was something @Ayaz Hafiz did for complexity and compile time reasons.

Top levels are special to allow polymorphism. They are always turned into functions.

view this post on Zulip timotree (Apr 20 2024 at 18:27):

Oh wait, it seems like * in local annotations is currently completely useless, because it doesn't even specify which caller-chosen variable it's referring to

twoEmptyLists : (List *, List *)
twoEmptyLists =
    empty1 : List * # The two `*` in the signature are distinct variables. Which one are we referring to?
    empty1 = []
    empty2 : List *
    empty2 = []
    (empty1, empty2) # ERROR (mismatch between (List *, List *) and (List *, List *))

In fact, you get the same error (albeit with a better message) even if there's only one * in the signature

myEmpty : List *
myEmpty =
    empty : List *
    empty = []
    empty # ERROR

view this post on Zulip timotree (Apr 20 2024 at 18:29):

I think the only way to use * in a type annotation for a local successfully is if the enclosing function has _s in its type

myEmpty2 : List _
myEmpty2 =
    empty : List *
    empty = []
    empty # no error

view this post on Zulip Brendan Hansknecht (Apr 20 2024 at 18:30):

Yeah, so maybe the error should say "Putting * in a type definition is not really useful. Consider using _ instead." The only cases where * is really useful is for function inputs.

view this post on Zulip Brendan Hansknecht (Apr 20 2024 at 18:31):

That may not be a totally fair statement, but I do think it is mostly accurate

view this post on Zulip timotree (Apr 20 2024 at 21:31):

Brendan Hansknecht said:

Top levels are special to allow polymorphism. They are always turned into functions.

Is this accurate? I just read Let-generalization: Let’s not? and it seems to specify that only lambdas and numerals can be polymorphic, regardless of whether they're top-level or local. Isn't the fact that top-levels constants can't always be polymorphic exactly why we have Dict.empty {} instead of just Dict.empty?

view this post on Zulip timotree (Apr 20 2024 at 21:33):

I think the bug tracked at https://github.com/roc-lang/roc/issues/5536 is the same as the bug in this thread. In that example it's actually a top-level definition being annotated!

view this post on Zulip Brendan Hansknecht (Apr 20 2024 at 21:37):

I thought we made all top levels into thunks to help with this

view this post on Zulip Brendan Hansknecht (Apr 20 2024 at 21:37):

But I don't know all the details

view this post on Zulip Ayaz Hafiz (Apr 20 2024 at 22:13):

only values that are syntactic functions or numbers can be polymorphic, regardless of whether they're on the toplevel or not

view this post on Zulip Ayaz Hafiz (Apr 20 2024 at 22:13):

And in nested contexts, only syntactic functions can be polymorphic

view this post on Zulip Ayaz Hafiz (Apr 20 2024 at 22:16):

To the original post, I think the program should fail to compile, but the bug is that you should not be able to type in : [Ok Str]*. That type doesn't make any sense in Roc, since it claims that in is polymorphic, but it's not.

view this post on Zulip Brendan Hansknecht (Apr 20 2024 at 23:04):

Thanks for the clarity

view this post on Zulip Richard Feldman (Apr 20 2024 at 23:32):

ok cool, so it sounds like the fix here is to make a new compiler error for annotations of this form, yeah?

view this post on Zulip Ayaz Hafiz (Apr 20 2024 at 23:39):

yeah I think so. I'm kind of surprised such a check isn't already around

view this post on Zulip Anton (Apr 22 2024 at 09:06):

I want to make an issue for this, can someone summarize in which cases an annotation containing ]* needs to error?

view this post on Zulip timotree (Apr 22 2024 at 15:43):

If an annotation contains * or introduces a new type variable, the associated definition must be either a syntactic function or a number. So e.g.

# OK. It's a number
three : Int *
three = 3

# OK. It's a function
returnEmpty : {} -> List *
returnEmpty \{} -> []

# Error. It's a list literal
empty : List *
empty = []

# Error. It's a tag
red : [Red, Green, Blue]*
red = Red

# Error. It's a division operation
pi : Frac *
pi = 22 / 7

view this post on Zulip timotree (Apr 22 2024 at 15:46):

Let-generalization: Let’s not? has an error message we could use as a starting point for all these cases:

-- NUMBER IS NOT POLYMORPHIC --

The type annotation on `piApprox` suggests that it can be used polymorphically:

1 | piApprox : Frac *
2 | piApprox = 22 / 7

Unfortunately, I can't use `piApprox` as any fractional type! I can only use it
as exactly one of `Dec`, `F32`, or `F64`.

If you want me to infer the fractional type that should be used, you can use an
inference annotation instead of `*`:

  piApprox : Frac _

If you explicitly want `piApprox` to be able to be used polymorphically, consider
making it a thunk:

  piApprox : {} -> Frac *
  piApprox = \{} -> 22 / 7

view this post on Zulip Anton (Apr 22 2024 at 16:33):

Lovely thanks @timotree! I made #6663


Last updated: Jul 06 2025 at 12:14 UTC