Stream: contributing

Topic: non-Result Math operations


view this post on Zulip Nikita Mounier (Apr 10 2022 at 15:52):

Hi everyone!
@Kevin Gillette and I are going to work on making math operations no longer return Result. Specifically, these functions are: div, divFloor, divCeil, mod, rem, log, and sqrt.
Here's the corresponding GitHub issue: https://github.com/rtfeldman/roc/issues/2826

view this post on Zulip Kevin Gillette (Apr 10 2022 at 15:53):

@Nikita Mounier so we can divide work, which functions are you considering to be div-related?

view this post on Zulip Nikita Mounier (Apr 10 2022 at 15:55):

Sorry you're right, I meant div, divFloor, and divCeil. It's true that mod and rem are also division-related.

view this post on Zulip Nikita Mounier (Apr 10 2022 at 15:55):

so we can divide work

I see what you did there

view this post on Zulip Kevin Gillette (Apr 10 2022 at 15:56):

Yes, I completely meant that as a joke. That I thought about ahead of time. Indeed.

view this post on Zulip Kevin Gillette (Apr 10 2022 at 15:58):

Okay, sounds good. So you'll handle div, divFloor, and divCeil, and I'll handle mod, rem, log, and sqrt?

Are you planning to rename the existing functions to divChecked and so on, per https://github.com/rtfeldman/roc/issues/2826#issuecomment-1094166537, and then produce panicking versions using the original versions?

view this post on Zulip Nikita Mounier (Apr 10 2022 at 16:05):

Sure, if everyone else is cool with that.

view this post on Zulip Nikita Mounier (Apr 10 2022 at 16:06):

Should the panic be at the zig level (using the `@panic("...") builtin function I saw elsewhere?

view this post on Zulip Nikita Mounier (Apr 10 2022 at 16:09):

This is how the div function inside builtins/bitcode/src/dec.zig handles denominators equal to 0:

if (denominator_i128 == 0) {
    // The compiler frontend does the `denominator == 0` check for us,
    // therefore this case is unreachable from roc user code
    unreachable;
}

So should I remove the compiler frontend check and instead have zig panic? Or should the panic be higher up?

view this post on Zulip Folkert de Vries (Apr 10 2022 at 16:11):

no a zig panic is quite different

view this post on Zulip Folkert de Vries (Apr 10 2022 at 16:12):

oh, actually

view this post on Zulip Folkert de Vries (Apr 10 2022 at 16:12):

we overload zig's @panic with our roc_panic

view this post on Zulip Folkert de Vries (Apr 10 2022 at 16:13):

so I guess indeed you can do a @panic("some message") there

view this post on Zulip Nikita Mounier (Apr 10 2022 at 16:13):

Right, I saw @panic being used for overflow which feels similar to dividing by 0. Cool!

view this post on Zulip Kevin Gillette (Apr 10 2022 at 16:14):

Okay, so an @panic in Zig will compile to a roc_panic call?

view this post on Zulip Nikita Mounier (Apr 10 2022 at 16:14):

Could you point me to the directory where the compiler frontend would do the denominator == 0 check? I'm still not super familiar with the codebase structure

view this post on Zulip Ayaz Hafiz (Apr 10 2022 at 16:16):

here's an example https://github.com/rtfeldman/roc/blob/e477813be41eed53a8df6e517fbbae8d01088dee/compiler/can/src/builtins.rs#L4298-L4362

view this post on Zulip Nikita Mounier (Apr 10 2022 at 16:17):

Okay, so an @panic in Zig will compile to a roc_panic call?

Looks like it

view this post on Zulip Nikita Mounier (Apr 10 2022 at 16:17):

Great, that's what I was looking for. Thanks!

view this post on Zulip Kevin Gillette (Apr 10 2022 at 16:31):

What are people's thoughts about the wording of the panics. It looks like all @panic calls within latest trunk start with the word "TODO". It also doesn't look like there are any explicit panics for integer overflow to reference (are those handled by rust/zig intrinsics)?

view this post on Zulip Kevin Gillette (Apr 10 2022 at 16:34):

Oh, I see. It's handled via throw_on_overflow

view this post on Zulip Nikita Mounier (Apr 10 2022 at 17:00):

Btw it might be a good idea if we were to both put all the checked variants inside compiler/module/src/symbol.rs, where all the symbols are listed in a macro with a number followed by its name followed by the corresponding function.
For instance, there's:

39 NUM_DIV_FLOAT: "div"
40 NUM_DIV_INT: "divFloor"

Which we'd want to turn into:

39 NUM_DIV_FLOAT: "div"
40 NUM_DIV_FLOAT_CHECKED: "divChecked"
41 NUM_DIV_INT: "divFloor"
42 NUM_DIV_INT_CHECKED: "divFloorChecked"

Since we gotta offset the number on the left each time we add a new symbol, we might get some nasty merge conflicts if we don't take each others' checked functions into account. Do you see where I'm coming from?

view this post on Zulip Folkert de Vries (Apr 10 2022 at 17:09):

that's hard to prevent

view this post on Zulip Folkert de Vries (Apr 10 2022 at 17:10):

because there cannot be gaps in those lists

view this post on Zulip Kevin Gillette (Apr 10 2022 at 17:14):

I follow. I'm reading through compiler/builtins/README.md now.

Side note: are the integers critical for humans to manage (is there any bespoke "42ness" to "42"), or does other human-managed code refer to the ID by number instead of symbol? At some point could those integer identifiers by assigned by position, thus minimizing conflicts?

view this post on Zulip Nikita Mounier (Apr 10 2022 at 17:20):

Here, I've created a gist with the symbol list so that you don't need to go through the same time-consuming process: https://gist.github.com/nikitamounier/776a49dfc84d07347d7669b80775c030

view this post on Zulip Nikita Mounier (Apr 10 2022 at 17:20):

Lmk if there's anything missing or if you notice any gaps

view this post on Zulip Kevin Gillette (Apr 10 2022 at 17:40):

Thanks! I've got some stuff to take care of over the next several hours, so it sounds like you may well have code I can reference (or at least will touch relevant files before I do).

Something that's not entirely clear to me at this time: why do some operations have _INT or _FLOAT suffices? NUM_DIV_INT makes sense to me, given that it is, iirc, a separate operator, but NUM_MOD_INT vs NUM_MOD_FLOAT is not clear to me given that NUM_REM has no such variants.

view this post on Zulip Nikita Mounier (Apr 10 2022 at 18:25):

Thanks! I've got some stuff to take care of over the next several hours, so it sounds like you may well have code I can reference (or at least will touch relevant files before I do).

Sure! I can already tell you that some of the files you'll want to modify are compiler/builtins/roc/Num.roc, compiler/builtins/bitcode/src/num.zig, compiler/builtins/bitcode/src/dec.zig, compiler/can/src/builtins.rs, compiler/module/src/low_level.rs, compiler/module/src/symbol.rs, and compiler/builtins/src/std.rs

view this post on Zulip Nikita Mounier (Apr 10 2022 at 18:57):

There's one thing I'm slightly confused by, if anyone could give me some context. From compiler/module/src/symbol.rs, divFloor is associated to the symbol NUM_DIV_INT. However, in compiler/can/src/builtins.src, NUM_DIV_INT is associated to the num_div_int function, which, in the branch where the denominator is not equal to 0, is identical to num_div_float's success branch. What's weirder, the comment above num_div_int defines it as:

/// Num.div : Int a , Int a -> Result (Int a) [ DivByZero ]*

Instead of what should logically be:

/// Num.divFloor: Int a , Int a -> Result (Int a) [ DivByZero ]*

Any idea what I'm missing here?

view this post on Zulip Nikita Mounier (Apr 10 2022 at 19:02):

Kevin Gillette said:

NUM_DIV_INT makes sense to me, given that it is, iirc, a separate operator, but NUM_MOD_INT vs NUM_MOD_FLOAT is not clear to me given that NUM_REM has no such variants.

IIRC, modulus for integer and modulus for floats works quite differently. For instance, C++ has % for integer modulus and fmod for floating-point modulus. Most newer languages unified the two though, even though the implementation works differently under the hood whether it's integer or float.

view this post on Zulip Brendan Hansknecht (Apr 10 2022 at 19:16):

I haven't looked at it, but their is a chance that float div is just a generic llvm division and not actually specific to floating point, thus int can call it

view this post on Zulip Brendan Hansknecht (Apr 10 2022 at 19:16):

Also, I think our modulus only works with ints.

view this post on Zulip Brendan Hansknecht (Apr 10 2022 at 19:20):

Also int division is like normal int division in other languages where it is not a floor. It also goes towards zero. Floor goes towards -infinity

view this post on Zulip Nikita Mounier (Apr 10 2022 at 19:24):

Brendan Hansknecht said:

Also int division is like normal int division in other languages where it is not a floor. It also goes towards zero. Floor goes towards -infinity

Got it. So why is NUM_DIV_INT associated to "divFloor" here? https://github.com/rtfeldman/roc/blob/trunk/compiler/module/src/symbol.rs#L949

view this post on Zulip Kevin Gillette (Apr 10 2022 at 19:41):

@Nikita Mounier my guess is that this is the difference between the / operator (floating point division) and the // operator (flooring division).

view this post on Zulip Brendan Hansknecht (Apr 10 2022 at 20:12):

Got it. So why is NUM_DIV_INT associated to "divFloor" here?

Bug? Differing opinion around rounding direction? :shrug:
Probably just wasn't thought about.

view this post on Zulip Brendan Hansknecht (Apr 10 2022 at 20:13):

A lot of the symbols were added just to give standard features, they may not be fully correct/thought through. For example, we recently had a large discussion about bit shifting

view this post on Zulip Nikita Mounier (Apr 10 2022 at 23:31):

Alrighty, I've made a WIP pull request, let me know what you think:
https://github.com/rtfeldman/roc/pull/2832

view this post on Zulip Nikita Mounier (Apr 11 2022 at 18:59):

I'm not sure I fully understand the test error CI is giving me about f64 not implementing PartialEq<{integer}. What do you think?

view this post on Zulip Ayaz Hafiz (Apr 11 2022 at 19:01):

In Rust, you can't compare two floats via equality

view this post on Zulip Ayaz Hafiz (Apr 11 2022 at 19:02):

oh wait nvm that's not it

view this post on Zulip Ayaz Hafiz (Apr 11 2022 at 19:03):

Try making it -1.0 rather than -1 in the test. I think otherwise Rust infers it to be an integer

view this post on Zulip Nikita Mounier (Apr 11 2022 at 19:44):

Yeah you're probably right. Let's see what CI says...

view this post on Zulip Kevin Gillette (Apr 12 2022 at 03:04):

Brendan Hansknecht said:

Also, I think our modulus only works with ints.

Direction-wise, is this the design intent, or something we intend to change? As I'm working on a PR to handle make mod panic, I might as well either get rid of float mod, or alternatively look into implementing it for floats (or at least add clarifying comments in the code) depending on the answer to this question.

view this post on Zulip Kevin Gillette (Apr 12 2022 at 04:28):

A bit more clarity needed: % and %% each appear to inconsistently be referred to as mod and rem/modFloor.

| `a % b`           | `Num.rem a b`    |
| `a %% b`          | `Num.mod a b`    |

https://github.com/rtfeldman/roc/blob/a7c37a4b7a635cb6122234f67d03669d71c87c09/TUTORIAL.md?plain=1#L1946-L1947


`a % b` is shorthand for `Num.mod a b`.

https://github.com/rtfeldman/roc/blob/a7c37a4b7a635cb6122234f67d03669d71c87c09/compiler/builtins/docs/Num.roc#L1104


`a %% b` is shorthand for `Int.modFloor a b`.

https://github.com/rtfeldman/roc/blob/a7c37a4b7a635cb6122234f67d03669d71c87c09/compiler/builtins/docs/Num.roc#L818


iiuc, the primary difference between integer rem and mod is in the handling of negative operands. Is that Roc's interpretation as well? What then is the intended behavior of modFloor as defined on integers (should it be called mod or rem instead)? And which operator is truly meant to correspond to which named function?

view this post on Zulip Brendan Hansknecht (Apr 12 2022 at 15:14):

Direction-wise, is this the design intent, or something we intend to change?

Mostly my assumption that could be wrong. I personally don't think it should work with floats. They can produce quite surprising results when you use mod due to their inaccuracies. I think that in many languages, % doesn't work with float types, but they still have an explicity fmod function somewhere in the standard library.

view this post on Zulip Nikita Mounier (Apr 12 2022 at 16:25):

Hmmmmm, I'm getting a very weird test failure:

fn gen_div_checked_by_zero_dec() {
    assert_evals_to!(
        indoc!(
            r#"
                    x : Dec
                    x = 10

                    y : Dec
                    y = 0

                    when Num.divChecked x y is
                        Ok val -> val
                        Err _ -> -1
                "#
        ),
        -1,
        i128
    );
}

CI says it's returning -1000000000000000000 instead of -1. Any idea why that could be happening?

view this post on Zulip Brendan Hansknecht (Apr 12 2022 at 16:27):

A dec is an int value times 10^-18

view this post on Zulip Brendan Hansknecht (Apr 12 2022 at 16:28):

In rust you are viewing the raw dec value, so it it 10^18 times bigger than you expect.

view this post on Zulip Nikita Mounier (Apr 12 2022 at 16:33):

I see – should I replace -1 with something like RocDec::from_str_to_i128_unsafe("-1") then?

view this post on Zulip Brendan Hansknecht (Apr 12 2022 at 16:33):

Yeah, that or the explicit value. I would just follow whatever other dec tests do.

view this post on Zulip Nikita Mounier (Apr 12 2022 at 16:36):

Yeah, the other dec tests do RocDec::from_str_to_i128_unsafe so I'll just stick with that. Thanks!

view this post on Zulip Richard Feldman (Apr 12 2022 at 17:16):

I spent a bunch of time looking into remainder and mod at some point; let me look some things up to refresh my memory on the subject :big_smile:

view this post on Zulip Richard Feldman (Apr 12 2022 at 17:19):

so one of my goals for Roc in general is to try to have the "performance ceiling" be as high as possible without breaking referential transparency or memory safety. That is, try to make there be some way to get the fastest combination of machine instructions into the binary, so that if you find that your Roc program isn't running fast enough, you have some way to solve that problem - even if it involves writing code in a way you normally wouldn't

view this post on Zulip Richard Feldman (Apr 12 2022 at 17:20):

that goal guides builtin design; for example, if the CPU supports a particular arithmetic operation (like float mod), by default I want to expose that as a primitive so that if someone really needs that for their particular use case to run fast, they have access to it

view this post on Zulip Richard Feldman (Apr 12 2022 at 17:20):

because if the operation isn't an effect and isn't exposed in the builtins, there's really no other way to access it

view this post on Zulip Richard Feldman (Apr 12 2022 at 17:22):

so if mod didn't accept Num, then I'd want to have a separate modFloat that only works on floats...which maybe is fine, if mod on floats is indeed a footgun, but it would have the downside that you could no longer do mod on floats using an infix operator, so I'd want to learn more about that before splitting one function into two! :big_smile:

view this post on Zulip Richard Feldman (Apr 12 2022 at 17:37):

ok, looking it up, it appears that there actually is no CPU instruction for modulo or remainder for floats, so let's drop that operation for now :thumbs_up: - we can revisit adding it again in the future if there's demand, and discuss the pros/cons then

view this post on Zulip Richard Feldman (Apr 12 2022 at 17:38):

so basically:

view this post on Zulip Richard Feldman (Apr 12 2022 at 17:38):

sound good?

view this post on Zulip Nikita Mounier (Apr 12 2022 at 18:51):

All tests passed :tada::tada::champagne:!

view this post on Zulip Nikita Mounier (Apr 12 2022 at 18:53):

Now I can take off the [WIP] from my PR :sunglasses:

view this post on Zulip Nikita Mounier (Apr 12 2022 at 18:53):

I'll make the necessary changes to the documentation, then I think it's ready to merge...

view this post on Zulip Nikita Mounier (Apr 12 2022 at 19:01):

Actually looks like there's nothing else to change in the docs! Merge away

view this post on Zulip Kevin Gillette (Apr 12 2022 at 21:02):

@Richard Feldman so that means Roc will no longer have an %% operator? % will desugar to Num.rem, and Num.mod is only available by name?

view this post on Zulip Richard Feldman (Apr 12 2022 at 21:04):

right!

view this post on Zulip Jared Cone (Apr 12 2022 at 22:06):

Just wanted to mention some inconsistency that / does float division and // does integer division, but % does int remainder with this proposal?

view this post on Zulip Brendan Hansknecht (Apr 12 2022 at 22:28):

Just wondering, does / ever cast int to floats, or is it always float division with float inputs. If it never casts, can't we just have / for both float and int division. It will just fail to type check if you use and int with a float?

view this post on Zulip Kevin Gillette (Apr 12 2022 at 23:28):

Python 3 broke compatibility with Python 2 by changing / to always mean "float division" (previously it mean "flooring division" for ints and "regular division" for floats), while // was introduced to always mean "flooring division". I feel this clarity makes sense.

In Python 3 the types are preserved though for //, such that 3.0 // 2.0 yields 1.0, yet / always produces a float even from int inputs.

I'm not sure about what typing choice is reasonable for Roc. For Elm's tradeoffs (less concerned with access to low-level performance control), I'd think (//) : Num, Num -> Int makes sense, but for Roc, it's not clear how to turn a Num a into an Int b. Also, if someone doing performance sensitive floating point code wanted a truncated float result, forcing them to explicitly convert back to float would be undesirable (even if LLVM could optimize that into remaining a float the whole time).

I would expect divFloor : Num a, Num a -> Num a is reasonable (no type changes).

I don't know what to think about the result type of div however due to the type correspondence question. Do we just prohibit div and / on integers?

view this post on Zulip Kevin Gillette (Apr 12 2022 at 23:29):

(and does that imply that a function operating on Num can't access /, since not every Num variant would have access to non-flooring division?)

view this post on Zulip Richard Feldman (Apr 13 2022 at 00:59):

the main motivation for that split is that it's easy to get surprising answers with int division

view this post on Zulip Richard Feldman (Apr 13 2022 at 01:00):

e.g. in x / y, if x is 3 and y is 2, then if they're both floats the answer is 1.5 but if they're both ints the answer is 1

view this post on Zulip Richard Feldman (Apr 13 2022 at 01:01):

and 3 / 2 returning 1 is a surprising result because that's not what it is in math

view this post on Zulip Richard Feldman (Apr 13 2022 at 01:02):

3 // 2 is a visual indicator that it's not doing normal division

view this post on Zulip Richard Feldman (Apr 13 2022 at 01:02):

so it's less error prone

view this post on Zulip Kevin Gillette (Apr 13 2022 at 04:39):

Looking through some of the code, I see:

I128 : Num (Integer Signed128)
I64 : Num (Integer Signed64)
I32 : Num (Integer Signed32)
I16 : Num (Integer Signed16)
I8 : Int Signed8

https://github.com/rtfeldman/roc/blob/f39f7eda03a2488c1a89ee2b58ab3c6ca97ccaee/compiler/builtins/roc/Num.roc#L169-L173

Of these two styles, which is preferred? It looks like the I8 style was introduced more recently. Would it be desirable to align these declarations on one style or another?

view this post on Zulip Ayaz Hafiz (Apr 13 2022 at 04:55):

They resolve to the same underlying type (Int is Int a : Num (Integer a)), so there is no functional difference, also because the end user will only ever see I8 or I16 anyway, not the whole expanded type.

In any case these definitions will change relatively soon with opaque types and abilities.


Last updated: Jul 05 2025 at 12:14 UTC