Proposal: unifying division semantics

I’m curious, what are some examples of the subtle bugs that the current behaviour can lead to?

Secondly, how would I get the current behaviour if the division return type is Self? Because mathematically the correct answer to 5/2 is 2.5, not 2, and I’m certain there are situations where this correctness is important.

Some ported code will be slower and some faster.
But what about overflow or undefined division by zero or -1?

As long as you use IntLiteral, nothing changes:

var res = 5/2 # 2.5, a float

This will be the new semantic for Int division:

var res = Int(5)/Int(2) # 2, an Int

This is already the case for SIMD based integral types like Int32.

To get the current semantic back for Int (or any integral types):

var a = Int(5)
var b = Int(2)
var res = Float64(a)/Float64(b) # 2.5, a float
1 Like

Can you explain the logic of this choice in a bit more detail? It feels counter-intuitive that this one case of integer division will result in a float, but none of the others. If T / T -> T for any other type, why should it be different for IntLiteral?

I would recommend not relying on IntLiteral here as I could see this changing too.
@melodyogonna - to keep the old “float” semantics, I would recommend using float literals or explicitly casting to float.
E.g.

var res = 5.0 / 2.0
# or
var res = Float64(5) / Float64(2)
1 Like

The proposal doc doesn’t really motivate the proposed change. It’s too short.

It should aim to answer the following question in detail: Why do we need to diverge from Python? What problems would occur by sticking with the Python model?

Being more like C++ and Rust is not a compelling argument, because Mojo’s syntax and operators are currently completely derived from Python, and everyone expects code that looks like Python to have behaviour reminiscent of Python.

2 Likes

While non-trivial to implement, I think there may be a better long-term solution than allowing Python to constrain syntax or behaviour: implement a compile-time function that takes a string of Python code as input and translates it to equivalent mojo code.

That would make it easy for mojo to diverge where it’s useful, while allowing people to easily copy-paste from Python, and slowly migrate the resulting code.

A lot of the changes to Mojo recently makes it a bit harder to reason about than if it were its own independent language. The Python similarities are going to be very deceptive

The proposal mentions that IntLiteral will not be affected and remain as is. Do you intend to change the proposal to include IntLiteral?

Consider the following code. Comments describe what gets printed with the current stable release of Mojo (which doesn’t yet have the proposed change):

fn main() -> None:
    var si = SIMD[DType.int, 1](10)
    print("SIMD int: ", si / 3)         # 3

    var i: Int = 10
    print("Int: ", i / 3)               # 3.333333333

    var su = SIMD[DType.uint, 1](10)
    print("SIMD uint: ", su / 3)        # 3

    var u: UInt = 10
    print("UInt: ", u / 3)              # 3

So straight up we have some nasty inconsistencies in behaviour between different integer types. If I write some code that performs division, and I switch out the type of integer being used, then the behaviour of my code may change in unexpected ways. That gets worse if I’m writing, say, a generic function that accepts input of arbitrary numerical types.

That’s a good question and it will depend on you to define what “correct” means for you.

The short answer is that given integer values a and b that don’t divide evenly, you can use Float64(a) / Float64(b)to get Python-like behaviour. But that’s just one possibility depending on your requirements.

Consider 10/3 as a good example. Python would give you a floating-point value, 3.33333…, which is a finite approximation to the true mathematical answer. Some other programming languages (e.g. various Lisp variants) will represent this via a data structure explicitly defining a rational number, so that you don’t get hit by floating point approximation. Both have performance implications if you’re doing a lot of numerical calculations.

Either might be preferable for you as the programmer, depending on your use-case. Python is second guessing us with the assumption that most users will be satisfied with a floating-point value approximating the true mathematical result. Systems programming languages (C, C++, D, Rust, and others) will typically default to the hardware defined ops for the type (which are most performant), hence the truncate-towards-centre behaviour of integer division, and expect the programmer to specify when they want alternative (usually more expensive) behaviour, like the float conversion above.

Given that Mojo is aiming to be a systems programming language, it makes sense to follow that design principle, especially because it avoids divergence between the behaviour of built-in and SIMD integer types.

I would state it this way: “Mojo has syntactic similarities to Python with type annotations but semantically it‘s a very different language.” At least for Phase 1 (Mojo 1.x).

Seems like a problem of the language more than it is of the behaviour, but I understand now why the consistency is required.

There are a lot of places where the semantics are similar with just subtle differences; the mental overhead of this will add up.

I think there’s a small but significant typo in the proposal in the Semantic Examples section where floating point behaviour is described:

# Float __floordiv__ follows the same semantics, but still returns Self
7.0 / 3.0     # = 2.0
7.0 // -3.0   # = -3.0 (floor toward -infinity)

Shouldn’t the first division example be either:

7.0 // 3.0   # = 2.0

or else:

7.0 / 3.0    # = 2.33333333333

… ?

I think one of the main problems is that very few CPUs have the ability to do int / int -> float directly, and it makes the “default” kind of division very surprising to anyone coming from languages other than python. This behavior in python has been a point of surprise for many people I’ve talked to, and python somewhat hides it behind the default lack of types.

I’m not sure this is doable, given that Python can do dynamic things that Mojo cannot (ex: exec(sys.stdin.readline()))

I think there are quite some important and non subtle semantical differences already, e.g.:

  • Mojo value semantic vs. Python reference semantic.
  • Mojo immutable default argument convention vs. Python mutable convention.
  • Mojo strong compile-time typing vs. Python dynamics nature.

It might be a good idea to have a dedicated place in the documentation to highlight the difference as proposed on Discord.

1 Like

Sorry, I wasn’t precise enough in explaining what I meant. It wouldn’t be about completely translating code (impossible given that language and library features don’t fully match) but about translating syntax. I think that should be achievable in principle.