The Modular team has just published a proposal we’ve been working on to standardize Mojo APIs around Int. I refer you to that proposal for all the details. At a high level, our plan is to go from today’s mixture of various integer types across the Mojo standard library to a consistent use of a single type: Int.
This is a precedent in other languages like Swift:
Unless you need to work with a specific size of integer, always use Int for integer values in your code. This aids code consistency and interoperability.
While we have been discussing this for a while, the recent work to weed out subtle numerical errors by removing implicit casts between Int and UInt has further motivated us to standardize on a single type. There will always be exceptions, but having a canonical integer type for almost all interfaces will cut down on the number of casts that your typical Mojo developer will encounter in regular usage. Overall, we feel it will provide a cleaner and more ergonomic experience.
A previous discussion about integer types for indexing is present in this forum thread, but we wanted to highlight this new proposal in its own thread. We know that there are strong opinions on various sides here, but for the reasons detailed in the above-linked proposal we believe that coalescing on Int as the default provides the best user experience while preserving our goal of performance.
Suppose I wanted over/underflow to trap, but only in debug mode; how would I achieve this? Seems to me that supporting customization like this isn’t really possible without a system like @owenhilyard‘s effect handler proposal.
I will would like to, once again, state that Rust should have the worst possible version of the problem Swift is talking about here, since it forces explicit casts for everything and has an integer type which is basically only used for indexing. Even before the removal of implicit Int ↔ UInt casts, the top 1000 Rust crates had a lower density of casts than both the Mojo stdlib and MAX. To me, this indicates there is a solution to that problem which doesn’t sacrifice using “the most specific type” for integers, which I suspect is closely related to not having a type called Int.
Once SIMD unification happens, we could potentially provide a way to have this be configurable at build time. Internally the SIMD type could perform these checks.
We can provide explicit functions on SIMD such as checked_add or checked_sub for example.
We can provide explicit wrapper types that have different behavior. CheckedSIMD, NoOverflowSIMD, WrappingSIMD, etc…
I think all 3 are desirable for different use-cases. For example, anyone doing math that is likely to overflow may want to use a CheckedSIMD, whereas others might want to only check particular operations. Having a global switch for what the defaults are is good for debugging without needing to have some weird typedefs.
Sure, Mojo makes it easy to define your own integer type, and implement it with such behavior. If there is a specific reason to add it in the future, we can add a new DType that provides such behavior in SIMD, but I’m not sure how that would be efficiently implementable for SIMD’s.
Please note that Int or Int8 are fully defined on over/underflow to 2’s complement wrapping. As such, we cannot and should not make them trap on overflow. This is unlike int in C, which is undefined behavior on overflow.
I think that efficency is a matter of perspective here. In a debug mode, I care more that I can run the program once or twice and have the compiler point me to the place where overflow shows up and that will likely be faster than manually hunting for where overflow could occur even with a 2x slowdown for all SIMD operations. I see this as mostly a debug feature, although perhaps it could be a parameter on some _InnerSIMD and the current SIMD could be comptime SIMD[dtype: DType, width: Int] = _InnerSIMD[dtype, width, OverflowBehavior.WRAPPING]. This would mean minimal extra work to support SaturatingSIMD and CheckedSIMD (especially since we have parametric raises now), and the function-level versions (checked_add, wrapping_add, saturating_add, etc) could be implemented with a bitcast to the relevant SIMD type.
Please note that Int or Int8 are fully defined on over/underflow to 2’s complement wrapping. As such, we cannot and should not make them trap on overflow. This is unlike int in C, which is undefined behavior on overflow.
What about user-selected behavior on overflow? Mojo can default to wrapping in release mode (O3 or possibly some other switch) and checked in debug mode (O0/1), with a flag to explicitly set the behavior for people who want O3 optimizations with overflow checks. I know this makes CI harder, and would require code which intentionally wraps to use a WrappingSIMD type, but I can’t think of many use-cases that would expect wrapping behavior to function properly, although I’m open to examples, so I think that making some code use .wrapping_fma or use WrappingSIMD shouldn’t be a large burden if it means easier debugging.
I also prefer some way to defer decisions like this as much as possible to whoever’s writing top-level code.
The problem with using custom types and explicit operations like checked_ like Nate and Chris suggested is that library writers could use e.g. an Int that doesn’t check for overflow (even in debug mode), which a top-level user might not prefer. Now, there’s no way for the user to make sure that the internal library code traps on overflow.
I think the general guidance would be to use the types where it’s controlled by the application author unless you have a very good reason to do something else (ex: cryptographic code sometimes depends on wrapping). We can’t stop people from doing things “wrong” but we can help shape ecosystem expectations.
I love this idea a lot (ref). Though I think it’s going to be a bit difficult to actually do this through the stdlib. Some issues: we’d need to introduce ancillary structs; ownership management can become tricky if the type itself is funny; etc.
It would be nice if we could have some language support for this. For what it’s worth, I think named extensions work with this. We could implement __getitem__ in an extension named unchecked, which would bypass the need for an ancillary completely.
I think that we should try to keep to the basic definitions Rust uses for unsafe, since there is a lot of mindshare around those definitions. I think some refining of unsafe is welcome, for example unsafe-security for things like using non-literal strings in a SQL library where it won’t cause UB but might cause a security issue, or unsafe-bounds for things which aren’t bounds checked, etc.
For negative indexing, if we want it I think that we need to nail down how it works precisely and make everything use that, because right now some things use modulus and -1 * len(list) is equally as valid for indexing as -1, and others will reject that. Personally, I think that negative indexing should be a separate trait to positive indexing since it may not be reasonable to implement for all datatypes (ex: some kinds of intrusive linked lists).
My 2 cents is that this is a mistake / missed opportunity. For the rare cases where a programmer actually wants their integers to wrap (this is like 0.01% of integer variables? Hashing is the primary use case), Mojo should offer a Bin64/Bit64 data type for “opaque binary data”, i.e. the kind of data that you want to shift >> around and have 2’s complement wrapping.
When an “everyday integer quantity” overflows this is almost always a programming error, and it would be very helpful for some of these errors to be detected at runtime, at least in debug builds. This doesn’t prevent you from promising that in release builds, the value will wrap instead of being caught. But this would purely be for performance reasons; this wouldn’t make integer overflow valid/correct.
I agree with this in principle. But then again, 2’s complement wrapping is the default behavior in numpy as well and when such massively utilized libraries do things this way it makes me assume that people much smarter than myself had a good reason for implementing it that way. Perhaps the logic is that you might want to allow calculations where intermediate steps may over/underflow gracefully so that the final result (which could be in the appropriate range) is still calculable?
Regardless, would maintaining consistent behavior with things like Numpy be a reason to prefer this over making them trap? If not, would it be better to handle the wrapping conditions at the type level (Bin64/Bit64) or at the operator level (+./-. /etc. operators to “use wrapping”)?
Nobody “implements” 2’s complement per se. The underlying hardware works that way, so it’s what everybody gets by default. And the hardware only works this way because it requires fewer transistors.
This doesn’t prevent a programming language from deciding that overflow is not valid behavior. That’s trivial to do, you can just decree “this is not valid”. The tricky part is deciding what to do in cases where the behavior occurs. You have to give the programmer some assurance of what is going to happen. IMO the best way to handle this is to let the programmer decide from a few options, while choosing sensible defaults. For debug builds a good default is for the program to abort with a descriptive error in cases where the overflow is detectable. For release builds, the only reasonable default is to let the integers wrap, with a disclaimer that “this means your program is erroneous, so you should never overflow your integers intentionally”.
would maintaining consistent behavior with things like Numpy be a reason to prefer this over making them trap?
If your Mojo program is calling into Numpy (a Python library), Numpy is going to continue doing whatever Numpy already does. For brand new programs written in Mojo, we can make different choices.
If not, would it be better to handle the wrapping conditions at the type level (Bin64/Bit64) or at the operator level (+./-. /etc. operators to “use wrapping”)?
In most cases where somebody wants 2’s complement wrapping, they’re not operating on an everyday integer quantity (e.g. “I have 100 customers”), they’re operating on a special bucket of bits. That’s why I think having a separate data type is a clean and unsurprising solution.
In most cases where somebody wants 2’s complement wrapping, they’re not operating on an everyday integer quantity (e.g. “I have 100 customers”), they’re operating on a special bucket of bits. That’s why I think having a separate data type is a clean and unsurprising solution.
I agree with your idea but not your naming. I think that we should use something like WrappingSIMD/SaturatingSIMD/CheckedSIMD, and have normal SIMD decide which one of those it is via a compiler switch so that the user can flip code between “help me figure out why this is blowing up” mode and “flops go brr” mode.
I’m not sure if this should really be called SIMD because there is no vpaddoq instruction that reports overflow to a mask register in one or two cycles. This is without performing a ‘reduce or(any)’ operation, which is necessary for raising add.
@llvm.sadd.with.overflow.v8i64 and the Znver5 target.