Rust 1.45(blog.rust-lang.org) |
Rust 1.45(blog.rust-lang.org) |
Rust 1.45 will be the Rocket Release. It unblocks Rocket running on stable as tracked here https://github.com/SergioBenitez/Rocket/issues/19
This is so excellent, and I love seeing long term, multiyear goals get completed. It isn't just this release, but all the releases in between. The Rust team and community is amazing.
Or maybe it's an explosive weapon crafted with metal pipe and gunpowder. https://rust.fandom.com/wiki/Rocket
I spent a few years as a scientific programmer and this is exactly the sort of thing that just bites you on the behind in C/C++/Fortran: the undefined behaviour can actually manifest as noise in your output, or just really hard to track down, intermittent problems. A big win to get rid of it.
The overflow-is-a-bug-saturating as would be the default, and there would be a separate sat_as for "I know this saturates, it isn't a bug.". ::sigh:: Rust went through this same debate for integers, initially rejecting the argument I'm giving here but switching back to it after silently-defined-integer-overflow concealed severe memory unsafty bugs in the standard library.
Well-defining something like saturation actually reduces the power of static and dynamic program analysis because it can no longer tell if the overflow was a programmer-intended use of saturation or a bug.
Having it undefined was better from a tools perspective, even if worse at runtime, because if a tool could prove that overflow would happen (statically) or that it does happen (dynamically) that would always be a bug, and always be worth bringing to the user's attention.
So now you still get "noise" in your output, but it's the harder to detect noise of consistent saturation, and you've lost the ability to have instrumentation that tells you that you have a flaw in your code.
So I think this is again an example of rust making a decision that hurts program correctness.
> So I think this is again an example of rust making a decision that hurts program correctness.
Could you expand on other examples?
This looks very dangerous, because it essentially does the "nearest to right" thing. Say, you cast 256 to a u8, it's then saturated to 255. That's almost right, and a result might be wrong only by 0.5%. Much harder to detect than if it is set to 0.
It’s not supposed to. Type casting with ‘as’ is supposed to be lightweight and always succeed; there is no room in the type system to return an error. In case lossless casting is not possible, some value still has to be returned. Until now, this was outright UB — meaning the compiler is not even obligated to keep it consistent from one build to another. Saturating, while still not optimal, is at least deterministic.
> This looks very dangerous, because it essentially does the "nearest to right" thing.
That’s why the intention is to introduce more robust approximate conversion functions and eventually probably deprecate ‘as’ casts altogether. There has been a number of discussions about this; current disagreements seem to be about how to handle the various possible rounding modes.
For better or worse, Rust 1.0 released with the philosophy that the `as` operator is for "fast and loose" conversions where accuracy is not prioritized; e.g. casting a u32 to a u8 would always risk silently truncating in the event the value was too large to represent. Over the years the language has added a lot of standard library support for bypassing the `as` operator entirely, and I think the prevailing opinion at this point might be that if they had the to do it all over again they might not have had the `as` operator at all, instead making do with a combination of ordinary error-checked conversion methods and the YOLO unsafe unchecked methods as seen here.
Which is to say: it's not that they technically couldn't have gone with the panic approach, but (performance implications aside) I think they'd rather just start moving away from `as` in general wherever possible.
NaN to 0 is a bit more concerning, but inconvienence and compatibility need to be weighed against catching every error (no type system will catch every bug).
Not sure what you mean here, and I don't have the standard at hand ATM, but I'm quite sure this is undefined behaviour in Fortran.
But yes, I agree defined behaviour is good. Undefined behaviour is occasionally good for optimization, at the cost of gray hairs for users.
Some things are well specified. Some things are mostly specified. Some things are still very much up in the air.
Though I try to always scrutinize any floating-point / integer conversions during code reviews. The default casting of a floating point value to integer is frequently not what you want, however. In the code we do, for example, you will usually round to the nearest integer instead, we don't normally need the more fancy rounding schemes.
To be honest... struggling to see why you'd do the former, outside of situations where you're happy with saturation, though I haven't thought about it a lot. Agreed that a consistent behaviour is a big help - I can work with "Rust does x in this scenario".
99% of my development work these days is C with the target being Linux/ARM with a small-ish memory model. Think 64 or 128MB of DDR. Does this fit within Rust's world?
I've noticed that stripped binary sizes for a simple "Hello, World!" example are significantly larger with Rust. Is this just the way things are and the "cost of protection"? For reference, using rustc version 1.41.0, the stripped binary was 199KiB and the same thing in C (gcc 9.3) was 15KiB.
At least i+1 elements, right? Or am I getting caught up by one of the three hardest problems again?
https://github.com/rust-lang/blog.rust-lang.org/commit/fe241... (should roll out in a few minutes)
1. Naming things.
2. Cache invalidation.
3. Off by one errors.
Number 3 apparently the hardest one.
———-
Original question: Out of curiosity, why the +1?
But I understand it could be more fun for the devs :-)
Features like CORS are available via third party fairings, but I could see them being incorporated in the future in the `rocket_contrib` portion. I think the goal is to keep the overall framework pretty light and put more things into the `rocket_contrib` portion.
In general, stable proc macros is an awesome step for Rust.
Maybe take a look at this I2C lib: https://github.com/rust-embedded/rust-i2cdev
I use C++ at work, which admittedly isn't the language I use most, and macros are used quite a bit in the code base. I find they just make the code harder to read, reason about, debug, and sometimes even write. I don't see them really living up to their claimed value.
Is there something different about Rust's macros that make them better?
easy: far easier than gradle and maven, imho even easier than go. Definitely easier than cmake.
- Having to learn weird syntax and constantly look up the reference manual (CMake) - Having to manually add source files (qmake) - Sometimes it just needs a clean and nobody knows why (Visual Studio) - Having to remember to set up debug and release builds and decide your directory layout for everything and figure out what 'a shadow build' is and who gives a crap since it all takes too much HDD space either way (qmake, CMake, make)
Also having tests built-in is really nice. Rust is the only language where I bother writing tests. Everything else makes it too hard, as if entry points into your binary are supposed to be rare and expensive.
The new API to cast in an unsafe manner is:
let x: f32 = 1.0;
let y: u8 = unsafe { x.to_int_unchecked() };
But as always, you should only use this method as a last resort. Just like with array access, the compiler can often optimize the checks away, making the safe and unsafe versions equivalent when the compiler can prove it.
I believe for array access you can elide the bounds checking with an assert like assert!(len(arr) <= 255)
let mut sum = 0;
for i in 0..255 {
sum += arr[i];//this access doesn't emit bounds checks in the compiled code
}
I'm guessing it would work like this with casts? assert!(x <= 255. && x >= 0);
let y: u8 = x as u8; // no checkHere's an example of how when it can detect it, it does the right thing: https://godbolt.org/z/hPqf69
I am not an expert in these hints, maybe someone else knows!
Something so lossy and ill-conceived should not be a two-letter operator.
I would say that my personal take of the temperature is "vaguely pro but not a slam dunk", at least from the opinions I've seen. Only one way to find out.
The numeric casts are the easy part of this.
Rust has great libraries to make life easy, eg https://docs.rs/fixed/1.0.0/fixed/ (Note: I haven't benchmarked the 4 or 5+ fixed precision libraries Rust offers to see which is best.)
That's not really true. It's more pulling from a database or csvs as backtesting is the most important part, which is also why the person you were replying to was asking about backtesting specifically.
Most firms roll out their own programming language, because before Rust existed there wasn't really a language that was a good choice for algo trading. Algo trading needs a few things:
1) It needs a financial number data type. That is, base 10 precision. Floats and doubles will not cut it when dealing with money.
2) The language needs to not implicitly do type conversions, so your types do not accidentally get converted to doubles.
3) You want provability. That is, you want guarantees that your program will run exactly the way you intended or you could lose a lot of money.
4) You hopefully want it to go fast, or backtesting could take ages. Historically super computers have been preferred, but that is probably not the case today. (This isn't even for HFT, just scalping and swing trading.)
Most in house languages in the industry are functional programming paradigm, because it allows guarantees. What you see is what you get. It allows one to write in a more mathematical way.
Today Rust takes the cake, as it is the only mainstream language that meets all the criteria, despite not being a functional programming paradigm language.
well done rust team
Can it "often" solve the halting problem as well?
The hope that this kind of optimization will happen sounds a bit fanciful for any non-trivial part of a program.
I ported a small C function to Rust recently that involved some looping, and all of the bounds checking was completely eliminated, even once I took the line-by-line port and turned it into a slightly higher level one with slices and iterators instead of pointer + length.
That is a bit extreme but it demonstrates the lower bound.
There's a lot of things you can do to drop sizes, depending on the specifics of what you're doing and the tradeoffs you want to make.
Architecture support is where stuff gets tougher than size, to be honest. ARM stuff is well supported though, and is only going to get better in the future. The sort of default "get started" board is the STM32F4 discovery, which has 1 meg of flash and 192k of RAM. Seems like you're well above that.
FYI Rust (and Go) currently don’t work on the new Apple ARM macs.
Especially in your case you'll find Rust to be a joy to use: you'll have way more confidence in your code being able to run for months without segfaults or memory leaks. And if you have a good understanding of the C memory model using Rust will be a breeze.
Worth noting that a lot of the code size is a constant addition that won't really scale with your program code.
I really hope that more enlightened vendors (hi Nordic Semiconductor) will start supporting Rust on their platforms.
I guess with younger and younger kids learning programming these, may be there can handle more? I am not sure if my son would understand all of the intricacies in his first semester.
[1] https://www.cs.umd.edu/class/spring2020/cmsc330/
[2] https://www.cs.umd.edu/class/spring2020/cmsc330/lectures/25-...
Maybe the professor for this class could assign a non-trivial project in C at the start of the semester then the same project again at the end of the semester except in Rust.
Of course they won't be able to grasp everything that Rust has to offer, but that is true of any language. I think Rust will expose them to many theoretical and practical CS concepts that they will be glad to have at least heard of during their studies.
In our degree, the first year students learn to program with Python, Racket (or OCaml depending on which teacher they get), C, Prolog, Bash, … Each of these language have way more to offer than what they can grasp. But each of them offers a different approach to programming and help the students to actually learn to program (rather than learning to write Java code, for example).
The course in question is actually called "Advanced programming". I want to experiment with a Rust course in second year as a kind of followup to both the functional programming course (the Racket/OCaml one) and the imperative programming course (the C one) that they have during the first year. If it really doesn't work, we'll change for something else or simply swap back to it being "Advanced C programming" for instance. But first, let's try to make the Rust experiment work. I really think it can benefits our students!
This is an eternal debate about Rust. I don't think it's required though. Can you appreciate functions without understanding assembly and calling conventions? I believe the answer is yes :)
Ouch. Modern C++ has alternatives to C macros. Is it an old code base, or is it just written in the C++98 style?
Today people will typically use constexpr instead of #define. While macros can possibly do some funky things constexpr can't, you'd be hard pressed to find those things. (C++ supports constexpr if, constexpr functions, constexpr lambdas, math, and so on.)
If you have the time and effort, it may be worthwhile to slowly start modernizing the code base, bit by bit.
The advantage of modern day C++ is it catches errors at compile time that older versions did not and would crash while the software was running. You might improve the stability of the code base if you help out.
C Macros are lacking because they are very primitive, e.g. they have not type system. They are also hardly turing complete. Its extremely hard to write a meaningful algorithm in them. IMHO the real macros of the C++ language are the templates and constexpr, althou they are limited in other ways. E.g. its hard to extend the syntax using them or do certain things like making the calling function return. They grow ever more powerful, with their own type system (concepts) and things like std::embed and static refection so they finally feel like a real language, alsbei a clumsy, pure functional language that feels alien a C++ programmer without exposure to haskell.
Rust macros are actually meant to feel like Rust, not some ad-hoc bolted on language.
I think this may be why I'm having a hard time appreciating them. Probably half the macros I see could just be a function call. The majority of those that don't are hiding a conditional return or goto, which I find to be a net negative.
I'll probably have to use a language with good macros before I can appreciate them.
When I say hygienic, I mean that when a macro uses a variable that isn't in the parameters or isn't static, it will be a compile error as it cannot know the scope. Any variables defined inside are scoped. Only parameters parses in can be referenced from outside the scope of the macro
They're also notated different to regular functions and they can't appear anywhere so it's obvious when you see a macro that it will expand into some code that won't break any of the other code in your function.
Of course people can write really terrible macros but typically macros serve a single very simple task and should be well documented and in my experience they often are.
As for procedural macros. They are just ast in, ast out functions, but written as a library in regular rust rather than some language thrown on top. That makes it easy to reason about the code and make it safe. If you write them well, they can be very good at reporting errors in usage to users.
The std lib also provides some very straightforward yet useful macros to act as inspiration. String formatting is an inline macro. vec is a macro to quickly define Vectors. Derive debug, partial equality, default values are all procedural macros and they are very straightforward yet tedious tasks to do on your own all the time.
Given the macros people publish, I would say they've done a good job at securing them as a useful feature. I've not seen many instances in actual code bases of messy macros. For examples of great macros, see serde[0], clap[1], inline-python[2]
[0]: https://github.com/serde-rs/serde [1]: https://github.com/clap-rs/clap [2]: https://docs.rs/inline-python/0.5.3/inline_python/
It wasn't hard, but yeah it was not fun either.
I can only imagine it's worse if you're actually writing the libraries and not just a CRUD app like I am
[1] Apparently the postgres crate was a wrapper around tokio_postgres all along and I didn't notice. So to remove a dependency I switched to using tokio_postgres directly
[0] https://doc.rust-lang.org/stable/rust-by-example/flow_contro... [1] https://play.rust-lang.org/?version=stable&mode=debug&editio...
I feel like static typing would get in the way there, making code more verbose and more difficult to prototype while running possibly thousands of backtests (e.g, to tune hyperparameters of your model) but if that your preference, by all means, Rust should be usable for that.
If the array size is definitely greater than or equal to 255 then all the array accesses in the loop will be in bounds and no further bounds check is required.
If required to an an "unsound" label, that might be enough push for some developers to add their own explicit "if" guards or use another conversion method without having to deprecate "as".
As for requiring an additional speedbump (like e.g. a "lossy" label) here to guard against misuse, I think this proposal is overlooking something: Rust can't just abruptly break all code that currently uses `as` in order to demand that something like `lossy as` be used instead. Any removal would have to first have a very long period where `lossy as` is syntactically valid and where the compiler instead warns for people using raw `as`. But if the compiler is already emitting a mere warning for `as` that suggests a better alternative, then it could just as easily suggest a method like `.try_into()`, which exists today. And once you're having the compiler warn about changing `as` into something else, that's already indistinguishable from deprecating `as` in those instances, so there's no point trying to avoid it.
Alternatively one could just deprecate `as` for numeric conversions, although there are still some library holes that would need to be patched up (e.g. other than `as` I don't know of an existing single method to say that you want a fast integer conversion routine that merely truncates a u64 into a u32; right now the alternative is `try_into`, which does a runtime check and returns a Result). It might also exacerbate tensions with some people who for quite a while have been grumpy that Rust requires frequent explicit numeric conversions when doing things like indexing (which always requires a `usize` type, so you see `foo[bar as usize]` unless people want to always work with usizes directly); would these people be happier if that were `foo[bar.as_usize()]` instead, or would this all be blocked on a discussion about being more lenient with numeric conversions?
However, there is one more thing a function can't do: borrowing arguments. Formatting never moves arguments because the format_args! macro generates references to them, which it then creates std::fmt::Argument out of.
Yes, that particular breed of C macros would likely manifest in Rust as people just defining a new function. In Rust, you tend to see macros in places where "just make a new function" doesn't suffice for whatever reason; for example, maybe you need to define a dozen different structs that only differ by the type of one field, so instead of actually defining the struct a dozen times, you could just define the struct inside the macro and then `define_my_struct!(u8); define_my_struct!(u16);` and so on.
You also can't use Rust macros to "redefine" other unrelated pieces of code, so that's one less thing to worry about.
fn foo<T: Trait>(_: T){}
fn foo(_: impl Trait) {}
fn foo(_: &Trait) {}
These three different fn definitions have two different behaviors and affect both the speed of the code and the speed of compilation and it depends entirely on how they are called.The first one is what the language calls generics: they are always monomorphized, which means that if you have three calls to `foo` with different types (that implement Trait) the compiler will expand three different functions with different types (code expansion).
The second one is a separate syntax level feature (impl Trait) which was mainly added to introduce a new feature which is static opaque types, where the function determines what the underlying the return type will be, but the caller can only interact with it using the trait's API.
[Aside] This is useful for cases like the following:
fn it() -> impl Iterator<Item = i32> {
vec![1, 2, 3].into_iter()
}
where you would otherwise have to specify the specific type: fn it() -> std::vec::IntoIter<i32> {
vec![1, 2, 3].into_iter()
}
This example doesn't seem like much, but if you want to add a `map()` call to this you start to see the benefit: fn it() -> impl Iterator<Item = i32> {
vec![1, 2, 3].into_iter().map(|x| x * x)
}
fn it() -> std::iter::Map<std::vec::IntoIter<i32>, fn(i32) -> i32> {
vec![1, 2, 3].into_iter().map(|x| x * x)
}
The more types you nest the more the benefits come into play. [end of aside]Now, with that out of the way, the type of an impl Trait in argument is decided by the caller (not the function), so they are implemented internally exactly the same as type generics. The only difference is arguable nicer syntax in the definition and not being able to specify a type using the turbofish. For all intents and purposes, those two are the same feature.
The third function is different, it uses a virtual table, with everything that implies: there's type erasure, there's only a single function in the expanded code (which makes compilation faster because the compiler doesn't need to do work), calling this function can be slower because the final executable has to perform some pointer chasing to call methods, instead of directly knowing where to call them.
All of this to say: if you use `fn foo<T: Trait>(_: T)` or `fn foo(_: &Trait)` affects compilation and execute time, so you have to be aware of their distinction. This means that if you're not aware you might have slower code than you would with a compiler (like Swift, for example) which relies on heuristics to decide to do static or dynamic dispatch, but it also means that your code's performance characteristics won't change all of a sudden because you modified a tangentially related part of the code and suddenly crossed some threshold.
Another example can be `.clone()`: is it slow? The answer is always "it depends". You might be cloning an `Arc`, which is cheap, you could be cloning a 10MB string, which is slow. But because we train ourselves to see clone as slow we might be worried or annoyed by `Arc`. We could make it `Copy`, but if we did that then you have less control over where the `Arc` gets copied which would make it harder to keep track of where the RC gets incremented. The language also doesn't automatically implement `Copy` for small structs, even though it could, which would make it easier to learn that part of the language (you don't learn to add derives early on), at the cost of baffling behavior (you might add a field and suddenly your struct isn't considered "small" anymore).
Yet another example, you also have access to `Cow<'_, str>`, which lets you deal with both static and heap allocated strings in the same way in your code, but it pollutes your code, where the naïve thing to do would be to use `String` everywhere.
My personal wish is for Rust to remain explicit as much as possible, but use lints to emit suggestions for the cases where a more "magic" language would change the emitted code. That way the code documents its behavior with fewer surprises.
> [...] it's not even 1.0 yet. Thus, it isn't stable enough for production use.
API stability (i.e. how the API will change in future) is largely unrelated to the question of whether you can trust it to work in production.
Maybe for some people API stability is a "must have" for production use, for the sake of minimizing churn when upgrading dependencies, but that's far from a universal principal.
I think people often get confused about different meanings of "stable". I've worked with plenty of libraries with stable APIs that are buggy piles of hacks. And I've worked with plenty of libraries with unstable APIs that are rock-solid in production. They're different concerns, but people seem to conflate them a lot.
So, to clarify, any statement like "not even 1.0 [...] isn't stable enough for production use", made without qualification, is a non sequitur.
E.g. the API:
char *gets(char *s);
has not changed in probably forty years. Rock stable!If we agree on the definition, then it generally follow that being productive (e.g ergonomic API), optimised (polishing work), not having rough edges, and having implemented the many required features (a server framework actually require a LOT) all those steps are done AFTER the foundational work of pre 1.0 Those after work will probably sometimes break API stability as we are not omniscient and forward compatibility is non trivial hence the needs for production ready frameworks to have had many breaking change releases so more like 3.x than 1.x Hence, it follows that the temporary but general API stability guarantees from a 1.0 are insufficient and general means that is can begin to be used, not that it should be used.
Rocket and any other rust server framework are not production ready as soon as you go beyond trivial use cases.they have dangerous foundational bugs and show stopper missing features. I have built a startup product with actix web (but studied rocket too) and those are dangerous economic bombs.
There is a curious parallel to the rise of Go which had no versions in their ecosystem as the language was adopted.
Rust has a linter called Clippy which absolutely does call out things like that. It can do so because there are other mechanisms in the language which provide the same functionality without ambiguity. I believe casting with `as` is linted against by Clippy.
[1]: https://doc.rust-lang.org/nomicon/what-unsafe-does.html
[2]: https://nora.codes/post/what-is-rusts-unsafe/
[3]: https://play.rust-lang.org/?version=nightly&mode=debug&editi...
[4]: https://rust-lang.github.io/rust-clippy/master/#cast_lossles...
[5]: https://rust-lang.github.io/rust-clippy/master/#cast_possibl...
[6]: https://rust-lang.github.io/rust-clippy/master/#checked_conv...
[7]: https://rust-lang.github.io/rust-clippy/master/#char_lit_as_...
[8]: https://rust-lang.github.io/rust-clippy/master/#cast_sign_lo...
[9]: https://rust-lang.github.io/rust-clippy/master/#cast_precisi...
[10]: https://rust-lang.github.io/rust-clippy/master/#cast_possibl...
Yes it is. It's used all the time. There's a reason as_conversions defaults to Allow in clippy.
Of course, there are lots of situations where `as` is the wrong tool for the job, but I think it's a bit of a stretch to call `as` "not idiomatic". It's perfectly idiomatic in lots of situations.
Whether or not it should be idiomatic is a separate question.
IOW, is it currently unidiomatic? No, code reviewers won't generally look sideways at it. But it's certainly getting less idiomatic over time, and that trend doesn't seem likely to stop soon.
The docs, the reference, and the RFCs all regards “as” as a mistake that should not be used, and _many_ features have been introduced over the years to reduce the cases in which “as” is the only alternative.
There are cases in which there is currently no alternative, so people “must” use “as”. This does not imply that it is idiomatic to do so.
The monomorphization-vs-dynamic dispatch thing feels natural from a C++ perspective, as it completely mirrors the choice of achieving 'polymorphism' via templates or virtual methods (though of course the Rust syntax is way nicer!, using traits for both, whereas in C++ you have either a class definition or...nothing, just ungodly compile errors ("compile-time dynamic typing")).
That's interesting re Swift. It seems similar in a way to using heuristics to decide whether to inline a function or not.
I _think_ C# does monomorphization for value types ("struct") and vtables for reference types ("class"), though I wouldn't bet on it...
> fn it() -> impl Iterator<Item = i32> {
One of the things that impressed me w/ rust was being able to write really concise code using ".map()" and friends and finding that it all ended up running just as fast as raw loops.
(The thing that has most impressed me about rust was the crossbeam crate + type system + derive stuff, which let me parallelize board search in an incredibly easy fashion. I found it much nicer to work w/ than Go channels, which is supposedly one of Go's big tricks!)
https://github.com/rust-lang/rust/issues/73908 has the full details.
(Also, not to be super pedantic, but this (among other things) is partially why I said "and is only going to get better in the future," that is, the support is generally good (I know, I am literally doing some of that in another tab right now) but not flawless.)
Some probably won't support it years after it's available for actual commercial products...
Not sure also what you mean by giving it a rest, this is the first time I’ve made a comment on this point.
Update: thanks for editing your comment to be less offensive.
Someone else was saying the same thing in the comment you linked. I'll summarize it as:
"Rust takes time to port to a platform which is not even commercially available".
These attributes change in response to events in the game. When a civilization would reach the modern era, it's aggression attribute would be reduced by 2. Gandhi's aggression was reduced from 1 to 255 (underflow). He would rain down nukes on everything in sight, somewhat uncharacteristic of him.
Fans liked this so much that Firaxis maintained the same behaviour in later games.
Especially back in the 1980s and 1990s you'd get awful code that did things like averaging the two streams because wrapping sounds awful and the authors were ignorant of the theory and/or unaware that saturated addition is a thing.
You can tell when somebody did this because it means playing silence makes everything else quieter, or worse there's an arbitrary limit on how many streams are played and playing any one thing is very quiet because it's attenuated so as to never wrap.
Haiku the 1990s-style operating system did this for years, as did various Amiga music software.
Way worse than that. The compiler wasn't obligated to act like anything at all. It would be totally legal to compile it so that the first time the value was accessed you got 0, the next time you got 1 - within the same program execution, with no mutation of the value. That is the sort of thing that is observed behavior of UB in the worst cases, and why it's so terrible to just pretend that UB is innocuous.
I guess I’ll have to go dig up the RFC discussion on this one; it should make for interesting reading.
There is: you panic, like in the array case.
That would have been much robust (at the cost of performance).
As you snarkily imply, the contrapositive should be common sense and not need to be stated. Yet your brain failed to see that your comment on gets() is exactly such a needless truism.
Way worse than even that (you might be noticing a theme here...). Once the optimizer has removed as dead code any branch that, if taken, would provably lead to UB at some arbitrary future point of execution, it can conclude that the other branch is now the only possible execution, and call it unconditionally, even if that leads to removing all your files (the classic example is https://kristerw.blogspot.com/2017/09/why-undefined-behavior...).
main()
x = get_from_some_external_data_source()
if x:
print("Hello World")
trigger_ub()
You might expect this code to always print if x is true but the optimizer can look at this and say "welp, if x is true then it would trigger ub, therefore it must be false, and since x must always be false we can just remove that entire branch."2. This might (I haven't profiled it) introduce performance regressions in ways which should not happen.
3. Besides in some usages around `dyn` other usages of `as` get increasingly more alternatives. It's just a question of time until `as` (for int/float casts) is recommended to not be used at all, maybe even linted against.
4. Given precedence of many other programming languages people don't expect a "simple" float to int cast to be failable. (The new methods replacing `as` make the fallibility clear, as it's e.g. `u64::try_from(bigf64)`).
5. It's udef-ness is only detected/handled in llvm, _I don't know_ if llvm provides similar well integrated mechanisms for this as it does for integer overflows. If not that would be another problem.
So, instead of this being traditional UB, it was a combination of two separate issues:
- Rustc erroneously emitting code that exercised an LLVM UB case, and
- Imprecise Rust documentation around the exact behavior of float -> int ‘as’ casts
Because casting to floats is not UB in the Rust spec, it's UB in LLVM. That's the whole reason this was an issue in the first place.
Now, Rust could have chosen to define the behavior to panic, but so far it's been a hard and fast rule in Rust that as casts do not panic. You would have to have a much better reason to change that then "well, it was UB before" since (1) nobody wanted it to be UB before, and (2) the actual implementation never panicked (and people absolutely rely on the fact that casts don't panic in unsafe code).
Without knowing this case, I'd wager a guess: it's about performance. Panicking introduces a branch and side-effects which, again, affects negstively optimization potential and performance. The saturating cast affects performance too, but less. If some old code has a lot of number crunching containing these operations, a big performance regression would be nasty.
https://github.com/rust-lang/rust/issues/10184#issuecomment-...
I don't disagree that making more obvious errors into panics would be nice, but the performance implications are often quite unforgiving.
It seems strange to have a cast as a safe operation and yet return results that are almost surely a bug. This means I will avoid casts altogether in my code, and I do hope they get rid of them at some point as some have suggested.
Rust tries hard not to cause regressions in users without good cause, which includes avoiding runtime performance regressions. One of the reasons that this fix took so long was that benchmarking prospective solutions revealed unacceptable performance regressions in users, even those who were already "doing the right thing" by manually upholding the proper invariants (no NaN, and value within range). The tension is that something still needed to be done, because at the end of the day the Rust creed is still "no undefined behavior without `unsafe`".
> This means I will avoid casts altogether in my code
Indeed, this is hardly discouraged wherever possible. For converting, say, a u64 to a u8, use `foo.try_into()` in order to get a conversion that follows the usual Rust conventions around Result-based error-handling (which didn't exist back when `as` was first conceived, or when this bug was originally filed). Casting floating point types to integers is already a rather rare use case in the first place.