Avoiding that sort of an error at compile-time is right there at the heart of the core premise of Rust, so Rust should optimize for it.
This boils down to two things: 1. What do we think is correct at any given time for any given program? The compiler can't know so we have to tell it, which brings us to 2 2. How do we write down in our programming language what is supposed to be correct?
The problem with floats is that sometimes you care about precision, sometimes you don't sometimes you care about overflows, sometimes you don't. Sometimes you care about inverse operations, sometimes you don't. Commercial programming language which only expose a "float" types usually are unable to deal with the greater complexity of _ensuring_ that some property you care about isn't broken. That is why compilers let you operate on floats and shoot yourself in the foot when you divide and then multiply back.
On the other hand, one could imagine a future programming language (and some academic experiments already exist) where you can tell the computer "in this part of the code, it's important that I never overflow" or "in this function, I expect multiplication and division to be inverses of each other". In which case the compiler will display diagnostic information if you do something that break those properties.
It's not clear yet what is the most convenient user interface to write down and check those properties. But many believe it by using even more advanced type systems than Rust. Many other believe that we can add static analysis atop existing programming languages to obtain the same result.
TLDR: Rust allows it for now because 50-100years in the future we'll have to tools to tell when it's ok and when it's not ok to multiply floats together. Right now we're still smacking rocks together to make fire.
> "Compilers (and type systems) are written by and for humans..."
The abstraction that is the type system might be designed for humans, but the underlying physical layers are totally non-abstract, tangible things with their own constraints. If I'm sending data to a DAC over the i2c protocol, the protocol has very real constraints at the electrical level. In order to interact with such protocols in a meaningful way we need low-level semantics to control the shape of data at the level of individual bits.
Consider the following:
uint32_t a = 1234567;
float b = 0.34;
uint8_t c = a * b;
fwrite(&c, 1, 1, stream);
What value gets written to the stream here?> "first tell me the use case for multiplying a float by an int that does not lead to a float"
Should the Rust compiler forbid all multiplication between the two types that doesn't get stored in a float?
That being said, multiplying by 2 not working is pretty funny because that's always exact until you overflow the float. It's pretty annoying the compiler didn't just figure out there was no precision loss anywhere here so just do it instead of complaining.
You're getting warmer. The reality is compilers default floating point is wrong for most uses. You should be able to represent things like pi and 1/3 exactly by default. That that's not the case in JS is criminal.