-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Scoped attributes for checked arithmetic #146
RFC: Scoped attributes for checked arithmetic #146
Conversation
program's meaning, as in C. The programmer would not be allowed to rely on a | ||
specific, or any, result being returned on overflow, but the compiler would | ||
also not be allowed to assume that overflow won't happen. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to clarify: the below loop would be allowed to print "hello" any number of times from 127 to infinity (inclusive), and may print "hello" a different number of times on different runs of the program, and may print "hedgehogs" but may not print "penguins"?
fn main() {
let mut i: i8 = 1;
while i > 0 {
println!("hello");
i = i + 1;
if i != i { println!("penguins"); }
if i + 1 != i + 1 { println!("hedgehogs"); }
}
}
(Thus, for example, LLVM's undef
could not be used to represent the value of 100i8 + 100
, because undef
can be two different values in different usage points. http://llvm.org/releases/3.4/docs/LangRef.html#undefined-values : "two ‘undef‘ operands are not necessarily the same. This can be surprising to people (and also matches C semantics) where they assume that “X^X” is always zero, even if X is undefined. This isn’t true for a number of reasons, but the short answer is that an ‘undef‘ “variable” can arbitrarily change its value over its “live range”.")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think that's right. Or in any case, if it precludes undef
that's a good thing. :)
Presumably this is clear to you, but for anyone else reading:
- The idea is that this is very rarely a program you intentionally want to write. If you do want to write it, you should explicitly use
.wrapping_add(1)
instead of+ 1
. And if you write it accidentally, turning on checks will catch it. - If the compiler saw
100i8 + 100
, it would be free to represent it asfail!("overflow")
, to issue a warning, or possibly even an error. - The goal is not to introduce unpredictability or to allow compiler optimizations, only to keep programmers honest. I would already be very surprised if, on an actual implementation, the above program did anything other than print "hello" 127 times, and I would be shocked out of my skin if it ever actually printed hedgehogs. But it doesn't make sense to explicitly define the semantics of overflow with checks off as wrapping when the whole point is that overflow shouldn't happen. Turning
overflow_checks
on or off should only produce an observable difference in the behavior of the program, beyond its runtime, if the program has an overflow bug. - In C's case, the program would be allowed to do anything, from nothing at all, to printing "LIONS, TIGERS, AND BEARS", to
cat /dev/urandom > /dev/sda
, to initiating the nuclear launch sequence.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good :)
I would be pleased if we could tighten the definition of what is allowed even more, without limiting what our desired implementations are allowed to do (such as an As-if-Infinitely-Ranged checker). I haven't thought of a good way though. For example, this is too restrictive: "On any given program run, given bit patterns X
and Y
, X + Y
evaluates to the same value [or the same lack of a value, e.g., fail!()] everywhere X
and Y
are added in the program." I suppose a stern warning to compiler authors will do.
(Also, for anyone who doesn't know: Though it's undesirable here, LLVM's undef
is not quite as bad as undefined behavior. "[T]he LLVM optimizer generally takes much less liberty with undefined behavior than it could." ... "[In Clang/LLVM,] Arithmetic that operates on undefined values is considered to produce a undefined value instead of producing undefined behavior. The distinction is that undefined values can't format your hard drive or produce other undesirable effects." http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_21.html )
Thank you for writing this up! It appears to accurately reflect the current thinking from the mailing list. Personally, I like this proposal a lot. I think we should follow C# and make the default not check for overflow. The perf hit is too large to have it on all the time. People who always want the check can easily turn it on on a crate-level basis. |
I am in favor of having overflow check for the operators off by default. People have a mental model for how fast or slow |
Good work!
|
|
One minor variation could be whether to have separate (I went with separate traits when writing the RFC because I just based them on the existing |
(Merged ops into |
I don't really like overflow returning "unspecified results" - then it could potentially "explode" within an unsafe block. A better way to deal with this is to call overflow a "strict conformance violation"(Bikeshed) and require that high-quality code be "strictly conforming" - with maybe a compiler option to add checks (or, if you're feeling evil, add an option to let the compiler C-style-assume that the program is - but this would make all code unsafe - so...). Additionally, I don't think overflow attributes are the right way to deal with this problem - a 3n-type solution (signed integers, unsigned integers, elements of Z/2^nZ) would probably be the best (I prefer still having "overflow checking" on unsigned ints, and I think "must be >=0" ints are horrible - the type-system exists for a reason). |
So can today's integer types. In fact, as far as I'm aware, it's happened.
Isn't this the same idea under a different cloak?
Not sure I understand this right - by "signed integers" and "unsigned integers" are you implying unbounded ones, while "elements of Z/2^nZ" are the existing fixed-size ones? If so, we do have |
How can Maybe the The behaviour of out-of-range literals should also be discussed—would something like |
|
Actually, that's just because LLVM specifies it to be undefined so that it is possible to use a division and remainder instruction to compute it (e.g. on x86 idiv is used for that, and it triggers an exception for overflow). Mathematically, modulus can never overflow and Not sure if it makes sense for Rust to adopt LLVM's rule or if it should just generate a check for this condition (division is already very slow, so the branch is not that bad). |
I mentioned It says
Yes. This looks great but may I suggest changing
|
glaebhoerl: By "exploding", I mean that a calculation within safe code overflows, and interacts badly with an inlined unsafe function or macro. Of course unsafe code needs to be careful with integers - that's why you shouldn't write lots of it. The thing is that I don't think attributes are the right solution to the problem - the thing we want is to ensure the code does not have unintentional integer overflows during testing, but still have well-defined safe behaviour in production (because safe code shouldn't be able to behave in a non-well-defined manner). Attributes, as a per-code-block option, aren't particularly supportive of this. About uN/iN/zN - again, I don't think that attributes are the right way to handle intentional integer wrapping (if its intentional then its not overflow) - wrapping arithmetic is not the same thing as integer arithmetic. My proposal was to have "non-strictly-conforming-overflow" uN and iN types + a wrapping zN type (maybe the uN and zN types could be merged C-style - but I like unsigned types and prefer that code use them without being forced to use wraparound arithmetic). This means that addition would go like (a as z32) + 2 (or (b as z32) + (c as z32)), which is slightly ugly (maybe allow zN+int addition)? Another proposal is to put wraparound arithmetic via ugly intristics wrapping_add(i, j). Actually, that may not be as ugly as I thought. Thinking of it, I think the best solution is "strict semantics" + intristics for quotient ring arithmetic (ring_add, ring_sub, ...) - I think "ring_XYZ" would be good names - they're short, and if you don't know what a ring is you probably shouldn't be using them. |
The reason I don't think this is the way to go is that the official semantics of a type shouldn't magically change based on context. The way it's written in the RFC, the official semantics of arithmetic on the built-in integers are specified such that overflow is a program error, but because of regrettable performance constraints, it's left up to the implementation whether to return an unspecified result, or to terminate normal execution in some fashion. And the implementation, in this case If overflow is normal behavior, and e.g. wraparound is required, then different operations, e.g. For more background on the meaning of "unspecified result" see this mini-conversation with @idupree. @P1start
I think it should behave the same in both instances: it's a program error either way, and the attribute is only a runtime thing the programmer decides based on whether or not the performance cost of checking is acceptable. Even in the @bill-myers (
I don't really mind either way. I'll update the RFC if there's a consensus around either option. (Or this change can be proposed in a separate RFC - I assume we currently do whatever LLVM does.)
If code isn't safe for all possible inputs, then it's not safe. Therefore, if it's a function, it has to be an Anyways, my previous remark still holds. Currently the integer types are defined to wrap around on overflow. Outside of a few specialized cases like hash functions and checksums, this doesn't make any sense, and programs aren't prepared to handle it. If an integer overflows, it wraps around, and if it's used in an
I agree, and this is why the RFC has a
Yes, the RFC specifies I encourage you to read the mailing list thread, in case you haven't, and maybe give the RFC another look, because many of the things you wrote have already been discussed (several times), and in some cases addressed. |
@glaebhoerl Agreed. Sorry for my brain fart about
|
|
||
## Semantics of overflow with the built-in types | ||
|
||
Currently, the built-in arithmetic operators `+`, `-`, `*`, `/`, and `%` on the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this operator list should include as
conversion to an integer type. Otherwise computing a value in one numeric type then converting would accidentally bypass the overflow check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, I'll add that shortly.
I've updated the RFC with points from the discussion here. @idupree @bill-myers: I've lifted parts of your comments into the text wholesale. I hope this is OK! |
I just want to say that this is a wonderfully written RFC and it was a joy to read. |
Thank you! |
@glaebhoerl Using my writing is okay with me! |
This doesn't have a lot of precedent in Rust, but using attributes on types is another solution (similar to . Much like C's API compatibility hazards could be avoided by letting the types be easily converted between each other- there would be relatively little danger or unsafety compared to the proposed solution, and purposeful uses of different styles of arithmetic would be more obvious than the current situation. |
6357402
to
e0acdf4
Compare
It is clarified in the next sentence:
The language can provide tools for detecting overflow independently of the performance of overflow detection.
Could you expand on this in more detail? What part of your idea of Rust is it violating?
This RFC acknowledges that many/most people do not want overflows, or only want overflows in small pieces of their code (e.g. it is likely true that the only place in rustc where overflow is desired is hashing computations and possibly constant evaluation, and everywhere else overflow would be incorrect). In any case, it seems to me that the features as written in this RFC do not lead to either of those things, since one can just add |
Coming to this late, but I've deliberately used wrapping operations in places besides those named. An embedded RTOS can record the current time as the number of timer ticks that have occurred since startup. When the timer ticks every millisecond (a very common configuration), and this number is stored in a u32, this will wrap about once every 50 days. If you want to know the number of milliseconds that have elapsed since an event, the operation This is just one example of where I've relied on wrapping arithmetic, though they basically all take this form: wrapping operations are useful for working with deltas against monotonically increasing values, where the range of the value can be expressed as |
For what it's worth, I'd second rpjohnst's comment above - this feels like a more natural fit in the type system than in specific expressions. For example, when working with hashing operations where wrapping operations are desirable, it is a mistake to use a non-wrapping operation. I'd never implement something like this, but consider the following:
There are too many places to keep track of which type of operation should be performed, it's more bug-prone than if we could record the attribute against a newtype |
@aidancully Thanks, more use cases for wrapping arithmetic are definitely good to know about. However, this didn't make me change my opinion about any of the decisions in the RFC - was it intended to? The fact that wrapping operations work well for deltas as you describe is a nifty but nontrivial fact: you have to think about what wrapping will do, and whether or not it will do what you want, so the fact that wrapping is explicitly desired here should be reflected in the source, whether as a |
@glaebhoerl Sorry, I misread the RFC earlier - I didn't notice the Wrapping<?> types. If I had noticed them, I wouldn't have made the point the same way. That said, I haven't seen (and have a hard time imagining) a use-case where wrapping operations should sometimes be used against a particular variable, while bounds-checking operations should be used against the same variable at other times. For example, I can't imagine why you'd ever do something like:
It seems to me that, for a given variable, it should always use the same type of arithmetic operation (where by "type of operation" I mean, saturating, bounds-checking, or what-have-you), and that it's generally nonsensical for that variable to use different types of operation in different places. At the least, this is likely the most common case - it would almost always be an error for the same variable to use, say, a saturating operation in one place and a wrapping operation in another. As such, I think the choice of what type of operations one wants to use belongs in the type system, rather than by multiplying the number of operators that one can choose from when using a single type. (That is, I'd like to continue using If we have what I think is the uncommon case, and different types of operations are necessary against the same underlying variable, then the desired type of operation can be chosen via type-casts: Thanks |
This sounds plausible, but do you perhaps have a theoretical explanation of why this is the case (if it is)? The other issue is that the LHS and RHS operands would often be different types. What should happen then? Should the overflow mode also "infect" the types of the function's arguments? Or should the function body cast them appropriately? IIRC Jerry Morrison (@1fish2) had some good arguments and examples regarding this subject in the original mailing list discussion (linked from the text). |
I don't have a hard-and-fast rationale. Heuristically, can you (or anyone) think of an instance where that argument doesn't hold? Hash calculations should always use wrapping operations, array indices should always use bounds-checked operations, some graphics operations (lighting calculation, perhaps?) should always use saturating operations (can't get a brighter color in common 24-bit RGB representation than 0xFFFFFF, or darker than 0x000000). The difficulty in coming up with cases where we'd reasonably mix types of operations is (to me) a strong indication that, in general, if types of operations are mixed, it's likely to be a bug, and the compiler can likely catch it. If it isn't a bug, then there's still an escape hatch available by type-casting intermediates to use other types of operation. For a soft theoretical explanation, I would say that any variable is a representation of some domain concept. It's the domain concept which dictates what type of arithmetic is appropriate. For example, that a hash value is a number calculated through arithmetic operations is essentially an implementation detail. The concept is that we want a function mapping a large domain to a small range, with very low chance of collision. In this case, arithmetic operations are just a convenient way of defining such a function. We don't particularly care about how much sense the operations make in isolation (like, why would someone in general want to multiply a number by some arbitrary constant, then add another arbitrary constant? the reason we do so here is related to how the hash calculation function does not make sense outside of hash calculation), but we do care that the range is not artificially constrained in our calculation operation. Wrapping operations are always the correct choice for hash calculation, Similarly, for an RGB value, our domain dictates that an individual color can't be less than 0, nor greater than 255, so saturating operations seem more appropriate. For representing time since system boot, it's important that the time value keep increasing, so supporting wrapping is necessary. In all of these cases, it's the domain concept (hash value, color intensity, time since boot) that dictates what type of arithmetic operation to use... And representing the operations that are valid against a domain concept is what a type system is for. Sorry to be so wordy... I hope that was helpful.
I'd compare it to |
Let's take an example from the Effective Java section on how to write a good hash method (for a
These arithmetic operations are meant to be wrapping operations, so in Rust you might declare But After computing a hash, what do you do with it? Generally you pass it around, compare it for equality, and index into a hash table So the hashing domain concept doesn't suffice to make the hash computation wrap-around, nor does it fit the other things you do with a hash value. |
(BTW I consider the term "overflow" to include underflow. I.e. compute an out-of-range value.) |
@1fish2, I actually think there are three domain concepts at work, in the example, and each concept should support different operations:
What arithmetic operations are natural against a telephone number? In general, I'd say the answer is none, it's nonsensical to write
I see that you argued against this form in the email @glaebhoerl referenced from the RFC, arguing that this way to write the function is more bug-prone (at least in Java) than it is to use an explicit wrapping operator. That might be true in Java (though I'm not yet convinced so), but I'm pretty convinced that using the type system would be more robust in rust: It is always a mistake to use a non-wrapping operation when computing a hash. Keeping the operation selection in the type system prevents incorrectly using a bounds-checking or (heaven-forbid) saturating operation when calculating the hash. It is harder to write buggy code when the type system prevents you from doing so. The program should fail to type-check if you attempt to use the wrong form of number in calculating the operation. I don't consider it that onerous to typecast the telephone number's input fields to use them as hash intermediate values... That sort of thing will be necessary any time you're hashing something that isn't a number (like, say, a string-slice name), and believe it or not, I kind of like the parallelism of requiring a similar conversion to hash calculation domain no matter the input type. In any event, it's certainly a price I'm willing to pay for increased safety. |
@aidancully @1fish2 Thanks for the discussion, this is somewhat persuasive to me. It's certainly good design to use newtypes to distinguish values with equal representations but different meanings, and I can see how this can extend to overflow semantics as well. That said, it still doesn't make me think that the design in the RFC should be changed in any way. A single |
I know I started reading the wrong version of the RFC at first (I have to apologize, I'm still getting used to github after doing almost entirely closed-source development for a very long time)... I mostly don't object to the technical decisions in the RFC (though I have different biases, and probably wouldn't have made the same decisions), but I will say that text in the RFC like this:
(describing the intention behind |
Yeah, that's basically what I was trying to say: some of the motivating text, "filler" might turn out different if I were to write it again, but the decisions themselves still feel sound. What the actual text under the (I think a |
It's a major performance loss for safe code. |
@thestinger, why is it a performance loss? My assumption is that |
|
Rust already has a lot of friction, and stuff like this is going to make it a lot worse. The interest of system programmers dies out more and more as changes like opt-in Copy keep landing. |
@aidancully I'm with you on thinking in terms of domain concepts but skeptical that static types will help manage those differences. Operations happen to the values in transition between domains. (I once tried using types to track dimensional units. It didn't work at all.) Anyway, go ahead and implement The relevant point for this RFC is: When we want numeric wraparound, we must tell the compiler. Do not rely on it to ignore overflow by doing wraparound. We could use wraparound types, methods, or operators. One should selectively turn off overflow checks (that's what the scoped attributes are for) for performance, not to request wraparound behavior.
That's a fun question. There's at least equals and hash code for comparison and table lookup. Also div/mod to convert to a string and as part of testing if an areaCode is a toll-free number. |
To say that in a less tangled way: To get numeric wraparound, use wraparound types, methods, or operators. To turn off overflow checking for performance, use scoped attributes. |
@thestinger, AFAICT, you didn't actually address why the performance would be worse. At the most, your argument reads like you may end up with duplicated code (if I'm also skeptical that comparing for type representational equality is that computationally expensive... (Incidentally, what is the "n" in your O(n) for comparing And, FWIW, I work in embedded systems programming. What's attractive to me about Rust is that it makes a lot of the abstractions and safety measures developed for higher-level languages finally available for use in this domain. I don't claim to speak for anyone but myself, but my interest actually increases as more high-level ideas are made usable in this domain (where "usable", to me, requires things like performance being easy to reason about, and maintaining a reasonably clear relationship between generated object code and input source). @1fish2, yes, I agree with that summary, that's where I am now, too. Thank you! |
Can we rely on If so, we could address your concerns by providing functions in the library that re-borrow |
I think if |
IIRC, there have been proposals for more newtyping support, which would allow one to do |
Closing in favor of #560. |
Pretty