Restarting the `int/uint` Discussion

A 32-bit type for enumerate isn’t enough for lots of common use cases like numbering lines or bytes from a file.

2 Likes

Who said anything about encouraging it? We're just recognizing reality.

Recognizing reality means using big integers when bounds aren't known. The reality of big integers in Rust is that they're far more painful to use than in C++ and the number libraries were deliberately redesigned to drop all support for them... there are plans to make them even more painful to use by changing Add and Sub so that any generic numeric code is a PITA to write.

A wrapper doing a smaller integer optimization means that big integers only need to allocate when the alternative would have been overflow or crash. The overhead is 2 statically predictable branches for every operation compared to 1 branch for overflow checking with no meaningful error handling. It's even more expensive than overflow checking alone, but it has the virtue of being correct - which is not true of crashing on overflow.

3 Likes

Here’s (going to be) int fallback for i32 and I don’t see how this is relevant, since let a = 0 is always easier to write than let a : int = 0 or let a = 0i. The fallback also serves as a perfect way to guide not-so-experienced peers.

I’m not a fan of having multiple ways in the core language to do exactly the same thing. That would be another con for design 2.

Yeah, performance and the like are not my main concern, if you’re into that kind of micro-optimization you’ll probably won’t mind writing platform-dependent code to take advantage of the register sizes and available instruction set for instance.

It’s more about correctness and portabilty. In particular what bothers me is this broken promise of a “good enough” type to use as a default integer type without 2nd thought.

If we look at C the default types have varying width depending on the architecture. It makes it hard to write portable code but at least you know that when you’re using int you have a reasonably fast integer type, long might be bigger but slower, short is probably shorter etc… Basically you lose portability but might have better performance (and back when C was created it was probably very necessary, having a one-size-fits-all would probably have been completely intolerable).

That being said, any project that needed to be portable pre-C99 just ended up typedef’ing their own fixed width types in their config.h and C99 made stdint.h mandatory.

So giving a special status to 32bit integer seems like a lose-lose. You can’t tell the coder "int is generally fast" like C does and by hiding the fact that it’s really just a 32bit integer you take the risk that the coder won’t take the range into account while choosing which type to use.

Look at the snippet of code on the very homepage of the language: http://www.rust-lang.org/

Would you say that using int for the accumulator variable makes sense right now? Would it make more sense if int was i32 assuming that program is external input and not hardcoded? It’s still pretty broken, you should definitely check for overflow in a real world scenario.

Lastly, since it seems that churn was a concern (I’m not sure why, but still), changing the width of int would subtly change the behavior of many programs that used it while still allowing them to build. That dummy calculator code is one such example. Removing the type altogether would mean that all that code would break and need to be retyped which also has the advantage of forcing people to fix their code if they didn’t mean to use pointer-sized integers.

4 Likes

I don’t like the alias either- seems pointless, nobody will default to int if the language doesn’t HAVE it. However, the point about ‘int means 32 bit’ being rare is wrong; “int” is a fixed 32-bit type in both Java and C#, which are very common languages that set a lot of peoples’ expectations.

The argument against ‘int’ in rust is pretty convincing. It’s a systems language, people should think carefully about bounds, i32 already exists, etc. But if ‘int’ existed and meant i32, it wouldn’t be that surprising to many.

2 Likes

within that context, something else worth mentioning: c# and java are not (ordinarily) bare-metal languages, but they do require some level of performance, and deal with big data structures; having a 32-bit index type, both are hard-limited to 4GB or 2GB arrays. this is not a problem in practice. i’ve written software in both languages that handles much larger quantities of data, using bespoke data structures - you would want them anyway for that kind of work.

so, i agree with valloric that rust could get away without having a 64-bit array index at all, should that simplify the design.

That’s interesting, I ignored that Java and C# set a precedent.

It’s not just about array indexes though, being able to safely cast between integers and raw pointers in low level code is absolutely necessary. Think about accessing hardware registers and things like that. There needs to be an intptr_t in one form or an other. In that context it makes sense to use that type to index arrays, while limit yourself for no good reason?

When you write Java and C# you target the Java and .Net VM so the language designers are free to make the assumption they want since they control the whole memory abstraction, basically.

2 Likes

I'd like to second this; I have almost never seen overflow in my career. I have seen underflow bugs relating to incorrect use of unsigned types, but that's irrelevant to this discussion. I've seen plenty of memory safety bugs relating to overflow deliberately caused by an attacker, but those are also irrelevant to Rust—I spent some time going through all security-critical bugs in Firefox related to this and all of them would have been prevented by Rust's bounds checks and/or iterators.

I fully admit that I am possibly biased by my primary focus in user interface code, layout, and graphics, where minor correctness bugs in edge cases simply don't have very large consequences. A phrase I stole from roc is "if layout goes wrong [aside from an exploit or crash], the worst that can happen is that a box is in the wrong place." In other words, I'm not writing code to pilot the Mars rover here, and I might have a different opinion if that were the case. But my instinct tells me that you and I are in the majority here—most people aren't writing code to pilot the Mars rover either.

1 Like

Option #1, please.

I have been, and remain, of the opinion that allowing an architecture specific int size to occupy the prime name real estate of int/uint is a huge mistake.

That, I believe, is very wrong. It would make perfect sense for the error message for trying to use an (undefined) int type be something like "there is no default integer type in rust. if you want to use a a pointer sized type, use intp [or whatever we decide on], otherwise use a specific type such as u32 or u64 that will be guaranteed to be large enough (and not overflow) for your particular use case".

This approach provides explicitness (a rust goal), helpfull guidance to newcomers, and helps spread awareness that there is no one-size-fits-all type.

It also reserves int/uint for possible future use, e.g. a non-breaking migration to design 4, should there ever be a consensus that a mathematically pure(r) int type could be a good option.

5 Likes

Total anecdote again, but: I ran in to a 32bit overflow issue in my first hours using Rust.

https://github.com/rust-lang/rust/issues/17795

I’ve done “big data” stuff for the past several years, mostly in C#, and we’ve seen plenty of potential overflow issues there. I can name at least two systems that went with i32s for their graph identifiers, and now probably feel pretty silly.

Only point is that overflow does exist. It isn’t going to get any less common in the coming 5 years. I have a pile of code I’m looking at right now that moves about 6GB of data around using uint indices, and if I accidentally got 32bit indices I’d be sad. No clue if that is enough reason to do A vs B, but did want to point out that it is a thing that happens.

1 Like

I work on a system that over the last couple of years has grown from hundreds of millions to tens of billions requests per day. We pretty regularly (every few months or so) find a spot where someone used a 32-bit int for a counter that they never thought would need a long. Most of the time these are fairly benign, just a dashboard light going red for no reason, but there’ve been a couple showstoppers as well.

Please note that I don’t consider this an argument for Design 3 or 4, just an argument against Design 2.

1 Like

I definitely sympathize with your approach but I guess if it's not a problem in your use case the question would be: would preventing this issue for people who do have this problem impact you in a negative way?

In this particular instance, do you think that forcing you to chose one of the sized u* types instead of having a u32 default would be a step back in usability?

I have seen underflow bugs relating to incorrect use of unsigned types, but that's irrelevant to this discussion

Actually, that's not completely irrelevant as one of the other abuses of int I've seen very often in C is people abusing int for unsigned quantities. The reason is that unsigned and unsigned int are much longer to type and int doesn't really carry the idea of signedness. In rust uint is not much longer but if we wanted to be really coherent in the naming int should be called sint anyway. I also always thought that C got it backwards, it should be unsigned by default IMHO, I use a whole lot more unsigned quantities than signed ones. So that's an other reason I don't really like having a defaultish int type that doesn't invite you to reason about the size or the signedness of your variable.

1 Like

In this particular instance, do you think that forcing you to chose one of the sized u* types instead of having a u32 default would be a step back in usability?

Yes, I do think so. We performed the experiment and the result was integer suffixes littered throughout small programs. This creates unnecessary friction (to borrow Jonathan Blow's term) for many use cases, in which overflow simply isn't a huge concern.

I’m not sure I understand, which suffixes are you talking about? I thought that had been resolved by defaulting integers to i32.

I think you're reading a bit too much into the justifications for option 1.

The first design implies that int is a default, but there is no current default in the language.

There is an accepted RFC to add a default. Furthermore, the entire point of renaming is that the name "int" creates a "de facto default", by dint of having a familiar name.

The i32 type is being added as a fallback for inference which is the closest thing to a default.

It is a default, in all senses of the word. When type inference fails to establish a hard constraint, it falls back onto a default.

It claims that there's an opportunity to provide a good decision / guide design but fails to substantiate that claim. The usual design suggestion is to use the integer type that's large enough for the use case - and while you can provide guidelines for some common cases, there is no sane "default" choice.

That in itself is a design guideline. With option 1 we would need to emphasize that one must think carefully about bounds.

2 Likes

I'm not sure I understand, which suffixes are you talking about? I thought that had been resolved by defaulting integers to i32.

Well, I thought that default was what you were objecting to.

Oh no, I object to having a “defaultish” integer type. That’s much worse in my opinion because it contaminates things like library interfaces and whatnot.

I’m still not entirely convinced that the default type coercion was really necessary (as I still maintain that it’s not doing the programmer any service to dissuade them from thinking about the size of their integer types) but I can see the argument for usability on small programs and stuff like for _ in range(0, 10).

I don’t think it’s easy to make the same case for having int = i32 since it’s exactly the same amount of typing anyway. And u32 is actually shorter than uint and it matches the naming convention of all other integer and floating point types in the language.

Do you really think people learning rust would be distraught by the lack of a type called int? I don’t really see it myself.

Well at least we’re moving forward, it seems everybody agrees that using int as intptr_t is a bad idea.

6 Likes

I think big integers incur too much code bloat for a systems language, even not counting the branches. I'm also concerned about the fact that they could just replace overflow failures with OOMs—there are always limits, and multiplications/exponentiation can cause you to hit those limits pretty quickly. They also hurt the ability to reason about performance because they affect asymptotic complexity (as arithmetic operations become O(length of integer)). Most importantly, though, the amount of work we'd need to do to get optimizations working with them prohibits their widespread use in practice.

If big integers were introduced more pervasively into the language I'd probably have to ban them from both Servo and sprocketnes via lints or whatnot on performance competitiveness grounds. (Of course, neither project is representative of everyone's use case, but it's what I have to go by.)

The rationale given against Design 1 seems very weak to me. It assumes that there’s something magical about the name ‘int’ that frees people from having to “figure out what the best size for that usage is”, but designs 2, 3, and half of 4 immediately go back to placing that burden on the programmer.

‘int’ is desirable as a name first b/c it’s the most natural abbreviation for “integer”, and second b/c it’s familiar from other languages.

Unless it’s actually implemented as a big integer, the ‘it’s an integer’ argument is misleading (and always has been in languages that restrict its size).

The ‘it’s familiar’ justification isn’t rock solid in the case of Design 2 (on the basis of ‘to whom?’ - C and C++ programmers know you can’t actually portably rely on it being 32-bit, while Java and C# programmers know you can), and completely falls apart to the point of being damagingly misleading in the case of designs 3 or 4.

So the only places where ‘int’ as a name actually makes any sense at all are designs 2 (b/c it’s mostly familiar, with caveats) or 4-bigint (b/c it actually behaves as an integer).

But I am strongly against Design 2, b/c, in addition to the listed drawbacks, it creates pointless redundancy with the equally terse and more explicit i32 while perpetuating the historical folly of ints not actually being integers.

I would be in favor of Design 1 or Design 4-bigint, with a strong preference for Design 1. ‘int’ isn’t a sacred name, just kill it.

6 Likes