(this is not specific to Rust)
Modern compilers could do almost any optimization; the bigger issue is in some reasonable time to figure out whether the optimization is valid and (almost always) benefitial.
Propagation of constants and dead code elimination are always benefitial, so you can rely on them.
Inlining of functions is often also very simple. It is very difficult to figure out what would be optimal to inline, so inliners use some simple heuristic such as "if we inline foo into bar, how much larger will bar become? Taking the best candidates with some budget".
In this heuristic, if inlining foo into bar will make bar smaller, it will always be inlined - this is generally true for simple functions that access a field, or wrap a call to single function.
If your function is otherwise small, it is still very likely to be inlined, but it is no longer guaranteed - and without inlining, compilers are much worse at reasoning across function boundaries.
Loops are a lot more complicated.
For simple example of two slices with the same length:
for i in 0..a.len() {
b[i] = a[i];
}
Naively in each iteration it will check whether i < b.len() and i < a.len(). The a check is trivially known from the loop limits, but what about b? The compiler will either have to leave the check there, or split it into something like this:
if a.len() <= b.len() {
for i in 0..a.len() {
*b.get_unchecked_mut(i) = *a.get_unchecked(i);
}
} else {
for i in 0..a.len() {
assert!(i < b.len());
*b.get_unchecked_mut(i) = *a.get_unchecked(i);
}
}
In this simple case the compiler does split it, but with more complicated loops it may just keep the bounds checks with heavy performance penalties.
In either case it will create much better code if you add assert_eq!(b.len(), a.len()) before the loop.
So for:
try to think what invariants would you need to optimize the code, and add them as asserts if they are not obvious from something else.
Especially at start of functions this is not only good for the compiler, but also benefitial as documentation.
If you have some hot loop, try to use less abstractions, they may sometimes confuse the compiler (resulting in seemingly "roll of a dice").
But for any non-trivial changes, meassure the difference, idealy try to figure out when it breaks.
In general, create code that is easy to reason about (for strict compilers, and humans).