Stream: beginners

Topic: Surprising crash due to inferred int type


view this post on Zulip Eric Rogstad (Jun 11 2024 at 18:06):

I was playing around with writing my first roc code, and came across a surprising (to me) crash due to integer overflow. I was able to narrow it down to the following minimal(ish) test case:

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

printNoOverflow =
    rows = 3

    rows
    - 5
    |> List.repeat rows
    |> Task.forEach \row ->
        row
        |> Num.toStr
        |> Stdout.line

printAndOverflow = \rows ->
    rows
    - 5
    |> List.repeat rows
    |> Task.forEach \row ->
        row
        |> Num.toStr
        |> Stdout.line

main =
    # works, printing:
    # -2
    # -2
    # -2
    Stdout.line! "working version:"
    printNoOverflow!

    # crashes with:
    # Integer subtraction overflowed!
    Stdout.line! "crashing version:"
    printAndOverflow 3

What appears to be happening is that List.repeat takes a U64, so rows is being inferred as an U64 in the crashing version, both in the List.repeat rows expression and also in the rows - 5 expression, with the latter causing the crash.

Whereas, in the non-crashing version, rows is being inferred as an U64 just in the List.repeat rows expression, and as a Num * in the rows - 5 expression.

My question is: is this behavior expected? And/or is there some basic pattern I should be following to avoid the crash?

view this post on Zulip Ian McLerran (Jun 11 2024 at 20:26):

Hmm... this is definitely strange... rows should indeed be inferred to be U64 because of the List.repeat call. I would expect you to get an overflow in both cases if anything.

What happens if you do:

printAndOverflow = \rows ->
    Num.toI64 rows
     - 5
    |> List.repeat rows
    #...

view this post on Zulip Eric Rogstad (Jun 11 2024 at 20:33):

Ah, that works

view this post on Zulip Eric Rogstad (Jun 11 2024 at 20:43):

So I guess the lesson is that the compiler won't necessarily infer for me that any given integer could be negative, and I have to think through each case myself and decide whether it needs to be converted to an IXX. Does that sound right?

view this post on Zulip Ian McLerran (Jun 11 2024 at 20:48):

Correct. The other alternative is that you use Num.subChecked to catch any potential overflows, without type casting to a signed type:

subChecked : Num a, Num a -> Result (Num a) [Overflow]

view this post on Zulip Ian McLerran (Jun 11 2024 at 22:47):

Oh, probably even better than using a type cast for the subtract operation, you could define the type of your function like:

printAndOverflow : I64 -> Task {} _ # or I32 etc

This would enforce rows as a signed type, and when you try to use it as a U64 you would get a compile error telling you that you that your types conflict. Then typecast into the List.repeat call.

view this post on Zulip Brendan Hansknecht (Jun 12 2024 at 05:39):

Locals for numbers are polymorphic I believe

view this post on Zulip Brendan Hansknecht (Jun 12 2024 at 05:39):

So they can be multiple types

view this post on Zulip Brendan Hansknecht (Jun 12 2024 at 05:39):

It is a special case

view this post on Zulip Brendan Hansknecht (Jun 12 2024 at 05:39):

That's at least my guess

view this post on Zulip Brendan Hansknecht (Jun 12 2024 at 05:41):

So I think row being polymorphoc leads to row as a U64 and row as a Num a. The default Num a being an I64

view this post on Zulip Brendan Hansknecht (Jun 12 2024 at 05:41):

So it's really the use as a function arg that forces them to unify

view this post on Zulip Brendan Hansknecht (Jun 12 2024 at 05:49):

Actual roc ir for this:

procedure : `#UserApp.printNoOverflow` [<r>C *self {I64, {}}, C [C [C Str I32, C [C , C , C Str, C , C , C , C ]], C {}]]
procedure = `#UserApp.printNoOverflow` ():
    let `#UserApp.40` : I64 = 3i64;
    let `#UserApp.rows` : U64 = 3i64;
    let `#UserApp.41` : I64 = 5i64;
    let `#UserApp.39` : I64 = CallByName `Num.sub` `#UserApp.40` `#UserApp.41`;
    let `#UserApp.35` : List I64 = CallByName `List.repeat` `#UserApp.39` `#UserApp.rows`;
    let `#UserApp.36` : {} = Struct {};
    let `#UserApp.34` : [<r>C *self {I64, {}}, C [C [C Str I32, C [C , C , C Str, C , C , C , C ]], C {}]] = CallByName `pf.Task.forEach` `#UserApp.35` `#UserApp.36`;
    ret `#UserApp.34`;

Yeah, two versions of the local.

view this post on Zulip Brendan Hansknecht (Jun 12 2024 at 05:51):

And failing version where row has to have single type due to being one arg

procedure : `#UserApp.printAndOverflow` [<r>C *self {U64, {}}, C [C [C Str I32, C [C , C , C Str, C , C , C , C ]], C {}]]
procedure = `#UserApp.printAndOverflow` (`#UserApp.rows`: U64):
    let `#UserApp.32` : U64 = 5i64;
    let `#UserApp.31` : U64 = CallByName `Num.sub` `#UserApp.rows` `#UserApp.32`;
    let `#UserApp.27` : List U64 = CallByName `List.repeat` `#UserApp.31` `#UserApp.rows`;
    let `#UserApp.28` : {} = Struct {};
    let `#UserApp.26` : [<r>C *self {U64, {}}, C [C [C Str I32, C [C , C , C Str, C , C , C , C ]], C {}]] = CallByName `pf.Task.forEach` `#UserApp.27` `#UserApp.28`;
    ret `#UserApp.26`;

view this post on Zulip Brendan Hansknecht (Jun 12 2024 at 05:53):

@Richard Feldman general question: do we have a better way to expain this to users/clarify expectations. The local acting different from a function arg feel very strange.

view this post on Zulip Brendan Hansknecht (Jun 12 2024 at 05:53):

It feels like the local has implicit type casting (which it essentially does, but with compile time errors on cast losing precisions or failing)

view this post on Zulip Richard Feldman (Jun 12 2024 at 12:50):

@Ayaz Hafiz might have an idea on how to explain it better, but my first thought is that the simplest explanation is something like "constants get inlined"

view this post on Zulip Richard Feldman (Jun 12 2024 at 12:51):

so here:

    rows = 3

    rows - 5
    |> List.repeat rows

rows is a constant that's known at compile time, so it gets inlined to:

    3 - 5
    |> List.repeat 3

...which explains the rest of the behavior

view this post on Zulip Richard Feldman (Jun 12 2024 at 12:52):

\rows -> is different in that rows is not a constant, so it doesn't get inlined

view this post on Zulip Ian McLerran (Jun 12 2024 at 14:34):

That explanation makes perfect sense! This is really good information to know!

view this post on Zulip Eric Rogstad (Jun 12 2024 at 18:02):

Thanks for the explanations, all! :pray:

view this post on Zulip Brendan Hansknecht (Jun 12 2024 at 18:17):

We have to be careful though, cause it is special for constant numbers specifically, iiuc

view this post on Zulip Richard Feldman (Jun 12 2024 at 20:57):

currently yes, although we’d like to extend it to other literals

view this post on Zulip Brendan Hansknecht (Jun 12 2024 at 23:45):

You sure? I thought that was the whole point of the let's not document.

view this post on Zulip Richard Feldman (Jun 13 2024 at 02:35):

yeah, e.g. if you have a record of number literals, we have all the info necessary at compile time to inline all of that just like we do if all the number literals were in separate defs


Last updated: Jul 06 2025 at 12:14 UTC