Proposal: unifying division semantics

As part of our ongoing efforts to unify Int with SIMD, we’ve posted a proposal to address an inconsistency we’ve observed around integer division. At present, a division like 7 / 3 involving Ints returns Float64(2.333...), which while familiar to those coming from Python is surprising behavior for kernel or systems programmers. We worry that the current semantics for Int.__truediv__ (what is invoked on /) can lead to subtle numerics bugs.

We are proposing that __truediv__ should return Self for all numeric types, performing truncating division (toward zero) for integers. This then would match C/C++/Rust behavior rather than Python’s float-returning semantics.

This would be a breaking change in this aspect of Mojo’s numerics, and we recognize that moving away from Python semantics in this case could be controversial. However, we believe this is the right path forward for consistency in the language as we are unifying the Int and SIMD types. We appreciate the discussion that has already taken place in this GitHub issue, but if people have other comments and concerns feel free to add them to this thread.

4 Likes

Semantic compatibility with Python numerics is not a goal of Mojo; performance is the primary objective, and Python-style float-returning division can hinder that.

This is consistent with the stated goal of “Phase 1” of Mojo (Mojo roadmap | Modular).

I think this could be one more argument to remove def functions from Mojo 1.0. This would avoid the misconception that def functions have pythonic dynamism and semantics. def can be re-introduced to Mojo in a later phase.

I think it will result in a compiler error when the types are not the same (as in Rust). Right?

Int(2) / Float64(2.0) # compiler error

I’m not a huge fan of having this deviation from Python but it does make sense. Making the return a dependent type would bring a lot of headaches up the call-chain when dealing with generic code.

Most compute-heavy code isn’t written in Python anyway. But this will break a lof of code unless we provide some migration helpers like e.g.

extension PythonInt: # infinite precision, etc.
@deprecated(
"""using integral division to return a floating point value is discouraged
and not possible in mainline Mojo code"""
)
fn __truediv__(self, other: Self) -> PythonFloat: ...

Where people would just use them on ported code somehow like

from python.compatibility.numerics import PythonInt as int

So yeah while this change will surprise a lot of python devs, I think it’s the right (albeit painful) change

I think this is a good change. Python is one of the few languages I know of which makes int / int return a float, and I think that there is a general expectation that integer division does not change the type for many programmers.

+1 to the points about making the division type anything other than self causing issues with generics.

1 Like

Can someone clarify whether this will be the case? A compiler error here would already capture many potential errors (though it might make the code more verbose).

I wonder if we could introduce some more safety meshes for those porting Python code. For example, given that there is a specific operator for integer division, I would generally discourage the use of / if floordiv is desired. So, the only reason to apply / to Ints is generic code, where I regard having different meanings of / introducing a high risk for bugs.

Do people have examples for generic functions that use / on the input and where a switch between float and int division is actually desired? Otherwise, could we make / be constrained to non-integral types only? Or make this some kind of “unsafe” feature that needs to explicitly be enabled?

Disallowing truediv for integral types would require a comptime if to get the original behaviour. This comptime if could also be embedded into an explicit function.

As an aside, I feel like a document of all Python deviations and maybe a link to the github/forum discussions would be beneficial when 1.0 hits. I understand that it’s something that will need to be attended to, potentially forever, and that has technical burden. On the other hand, a quick reference document that says “Hey, here’s all the things that won’t work according to your expectations” will simultaneously save headaches and give context to the mojo philosophy.

4 Likes

Cython has this page: Caveats — Cython 3.3.0a0 documentation and I believe it’s really good to learn gotchas quickly

2 Likes

That’s a great suggestion, I’ll raise it internally as something we might be able to have a dedicated section for in our Mojo documentation.

1 Like

So now ‘/’ and ‘//’ do the same, which I suppose is strange. I have an idea. Make // have the Python semantics, i.e. swap the operators.

Not exactly the same: / rounds towards zero (truncate) and // rounds towards negative infinity (floor) for integral types. Python has // too and it has the same semantics as Mojo‘s // (__floordiv__).

If it were up to me, I would remove the ‘//’ operator altogether.
A division operation without hardware support and branching seems slow; I suppose this slowness can only be justified by the usefulness of exponentiation. But here, a simple method would suffice.

1 Like

Agree. At the end it‘s a matter of how „python-esque“ Mojo wants to be in this regard. AFAIK neither Rust, C, C++ nor Swift has a floordiv symbol. Looking just at Mojo alone: having two so similar functions as symbols seems unnecessary. And / has hardware support.

Removing // from Mojo would have the benefit of simplifying Mojo and directing pythonists to just use the performant / symbol for integral division that should yield an integral.

I like the idea of aliasing the / and // operators.
However, removing // seems too destructive, given that a lot of Python code relies on these semantics. Thus transition would be a pain.

I would not do this. It would be strange to have two operators doing exactly the same and would make Mojo code less consistent and more confusing. And for floating point operations it would not be meaningful to alias them. Better remove // for good to get rid of this legacy.

However, I am afraid this will render many straightforward Py→Mojo translations invalid. A lot of hardware logic is built around division to make it less than 20 cycles on modern hardware for a reason. I think it would be best to issue a warning to ensure consistency of style while maintaining compatibility.

Unfortunately I don’t think it is easy to do straight forward Py → Mojo translations except for very trivial cases - especially if you want it to be performant. Mojo already has several important semantic and syntactic differences.

For integral division the advice would simply be to just replace // with / to get maximum Mojo :rocket: speed and to have idiomatic Mojo :fire: code. But the semantic is slightly different.

And for all the non pythonists that come from Rust, C, etc., using / for integral division is familiar while // would be unknown.

This is one of the general things I consider to be neglected. If we only used the // operator, the language itself would be stranger, but it would feel more Pythonic, which I think is a strong selling point.
Fn→Def is another great example where I see a lot of potential for ‘Pythonication’ by simply removing ‘fn’, as ‘fun’ sounds too much like ‘fun’.

There is no clear right or wrong answer to this matter.
Ultimately, I think it comes down to what Chris prefers.

As the proposal has been accepted already the decision is:

  • / always returns Self. For Integral division it truncates the result (towards zero).
  • // will still be available. It rounds the result to the floor (towards negativ infinity).

As a consequence you can use both / and // for integral division. / is more performant and // retains the Python semantics. I would consider / more typical for Mojo code as it is performant and already used in many places in the Modular repo.

To get the current pythonic / semantic you need first to convert the integral types to Floats and then do the division.