Proposal: unifying division semantics

Having different division operators for different types seems like something that would be confusing for everyone no matter their programming background.

It’s a good point though that the __truediv__ dunder should probably be renamed to __div__ if that’s possible. It would make intent clearer.

Yes, this would be unexpected for many people, and I agree that “unexpected” is nothing desirable in general.

Nonetheless, every language feature is unexpected when it is first introduced, and since it is very easy to provide very good error messages for the / vs // distinction, it would not require anyone to learn a lot or to know things a priori. I do not think that “confusing” is the right word here, as the underlying rule (Ints can only have int division //) and the motivation (the code does explicitly what is written, less risk of getting things wrong here) are clear.

Whenever learning a new language, one of the things I need to do is looking up and memorizing what the / operator does. How nice would it be if this question wouldn’t need to be asked at all.

1 Like

I personally think this change is a step in the wrong direction and I’m genuinely disappointed. Leaving aside the drift from Python semantics, this is a major breaking change and the failure is silent - I have some code that I now had to fix, but I’m not even sure I added explicit casts everywhere it’s needed, not to mention that the explicit casts are ugly. If I wanted truncation I would’ve used __floordiv__. If returning a float is not desired, then __truediv__ should not be available for integer operands and cause a compiler error. Why do we even have __floordiv__ as a separate operator in this case? This is a really incredibly bad decision.

OH and guess what, int literals are behaving differently. This is madness.

I propose 2 things based on some previous comments:

  • Rename the dunder from __truediv__ to __towards_zero_div__ since we aren’t actually doing true division anymore, and the difference to floordiv is in the rounding logic; choosing a name that clearly communicates that
  • Possibly add a method fn true_div[out_dtype: DType = _float_for_dtype[Self.dtype]()](self) -> SIMD[out_dtype, Self.size]

Alternative proposals:

  • Adapt the truediv dunder to be dependent for integer types (returning floats)
  • Entirely disable truediv for integers with a where clause and add a separate method that does towards zero division for all dtypes; this would enable generic programming based on that and not the dunder

Not a huge fan of this change. In my opinion this does not solve any problem. At least one group of people will be unhappy. Either python folk will be unhappy or Cpp/Rust people will be unhappy. As mojo is following python semantic for operator overloading. I think we should follow the python semantic for this.

I have two remarks on the subject:

  • Surely this change is motivated by all the kernel programmers at Modular who obviously have Rust/CUDA/C++/C background and thus make mistakes when using `/`. It’s what currently allows the Modular company to make money, so it’s expected that their pain weight way more than the community’s pains. But what if, in the future, a high number of kernels writers come from Python/triton/cupy/CuTe/Helion/numba/TileLang ? In the mojo roadmap:

That means we must resist the urge to chase short-term wins at the expense of long-term clarity, consistency, and quality.

  • How do we teach LLMs that / in Mojo is very different from / in Python? LLMs are very sensitive to context. They see Python-looking code and try to write Python-looking code. Claude code tries to use str() instead of String() all the time, same for int(). Given how common LLM-generated code is nowadays, and given they might write the majority of the code in the future, how do we shield ourselves from those mistakes?
5 Likes

I think that’s a problematic assumption to make. Different programming languages have all sorts of subtle and not-so-subtle differences. People adapt to them.

Context for what follows: I’m very much from a systems programming background (C, C++, D, Rust), even though for some years now a lot of my programming work has been in Python. I’ve never ever got confused or had problems because the Python division operator behaves differently from C and the like. I very much doubt the devs at Modular do either.

What I can say is that, if I’m writing in a systems programming language – and Mojo is a systems programming language – I expect operations on built-in numerical types to correspond to the fast, highly optimizable hardware ops for those types, i.e. the default behaviour should be hardware driven performance. I don’t want or expect the programming language to second-guess me by choosing a different behaviour that is less efficient. If I want a different behaviour, I want to be able to specify it so that I get the most performant behaviour that satisfies my use case.

By contrast Python’s numerical types are an abstraction over the underlying hardware-supported types, an abstraction designed to provide calculations that are closer to (but not the same as) mathematics. (Both __truediv__ and __floordiv__ are motivated by maths use cases.) Nothing stops Mojo from implementing, say, a Number type whose operations behave the same way, which can be used when easy maths is more important than number crunching speed. But when it’s doing operations on built-in, hardware-supported types, Mojo should follow systems programming norms and give you the efficient, hardware-implemented ops.

The Mojo devs aren’t making this choice because “That’s how C, C++ and Rust do it.” They’re making this choice (as those languages did before them) because systems developers, and hence systems programming languages, need these features.

1 Like

Remember that for floating point numbers the semantic of / (__truediv__) stays the same and does not round/truncate to integers. The difference to // (__floordiv__) is significant for floating point numbers.

1 Like

Coming from Java, I have no problem with the new definition of / . If I want the result not to be an integer, well I use the appropriate floating point or decimal type. On the other hand, it is quite difficult to have consistent result types in languages that support many different numerical types (e.g. Is 6.0/2.0 yielding 3 or 3.0? And what type exactly, Int64, Float64, Int8, …?).

Requiring both operands and the result to be of the same type makes / division conceptually easy to understand and allows for generalization.

I agree with Joseph, this is a “what does the hardware do?” decision, and a vanishingly small amount of hardware provides int / int -> float division, especially for high precision integers which is what I expect most people to be doing division with. For example, x86 only provides int / int -> int division with SIMD. While I don’t think we should break with python lightly, “almost every other programming languages, and all of the relevant systems programming languages do something else” seems like a fairly good reason to break from Python.

If there are different hardware operations for float / float -> float and int / int -> int, then this is another motivation for using different operators. My point is that truncating-towards-zero-division is a different operation than actual division, and that it uses the same operator as float division is not set in stone.

almost every other programming languages, and all of the relevant systems programming languages do something else

Most high-level languages (at least Python, R, JavaScript, PhP) return floats. And with R and Python, we have most data analysis workflows covered. Yes, Mojo is a systems language, but it is supposed to have a low barrier for “practicioners”, and it is supposed to be used in close integration with Python. Note that / applied to a Python Int having a different meaning than the operator applied to a Mojo Int and both being in the same code base is not ideal.

I think that with two operators and constraints via where clauses, we have all the tools necessary to reconcile both needs.

1 Like

I would consider Java to be quite a high level language (at least it is not a systems programming language) that does not return floats. And JavaScript is special because all numbers are represented as Float64 by default, so there is no real integer division in JavaScript.

JavaScript recently introduced BigInt as a numerical type and it matches this proposal:

  • 5n / 2n yields 2n
  • 5n / 2 is an error because a BigInt can only be divided by a BigInt

I’m honestly a bit baffled by the idea that folks capable of performing sophisticated data analysis are going to have a problem understanding that Mojo isn’t Python, and that there are some differences you have to adapt to.

I mean, it’s not that hard (or even uncommon) to write C/C++/D/Rust that interfaces with Python, especially where fast number crunching is needed. The same issues apply there. People cope.

What I think would be really hard to adapt to, though, is implementing things in a way that is both different to Python and different to any other programming language I can think of.

As I said before, I think it’ll be a lot easier just to implement a higher-level Number type that abstracts away the hardware types and behaves more in line with mathematical expectations. After all, that’s what Python and R, at least, are doing.

1 Like

I’m honestly a bit baffled by the idea that folks capable of performing sophisticated data analysis are going to have a problem understanding that Mojo isn’t Python

It is not about understanding. It is about making mistakes, and about consistency.

Yes, people will cope either way. We are just looking for the best way of helping them and reducing errors.

I should add, both int and float division are the same truncating integer division under the hood. The difference is on the interpretation placed on those integer values. See e.g.

1 Like