What a Compiler Is Allowed to Do
Observable behavior, the as-if rule, and undefined behavior as a licence to assume rather than a promise to crash. Semantics decide what is legal, not cleverness.
An optimization is valid only if it preserves the language's defined observable behavior. Every pass, every flag and every argument about undefined behavior in this domain is a consequence of that one sentence.
The list the whole domain depends on: input and output, volatile accesses, whether the program terminates, and the order the language sequences those in. Elapsed time, memory used, chosen registers and instruction counts are not on it — which is exactly why the compiler may change them.
The compiler may transform the program however it likes, provided the observable behavior of the result follows the rules of the language's abstract machine. It is not a loophole — it is the clause that makes any optimization at all legal.
Undefined behavior is not a run-time error and not a promise of a crash. It is a licence for the compiler to assume the program never does it — which turns a source-level mistake into a premise the optimizer reasons from.
The canonical case, worked properly: a null check placed after a dereference is deleted, because dereferencing already implied the pointer was non-null. Not a compiler being malicious — ordinary branch simplification applied to a fact the language supplied.
Can integer overflow occur? Can two references alias? Can a function have hidden side effects? The answers are properties of the language, and they decide what its compiler may do — which is why the same transformation is routine in C, forbidden in Java and unnecessary in Rust.
What `-O0` through `-O3`, `-Os` and `-Oz` actually select, why a higher number is not automatically faster, and why the only way to choose between two of them for your program is to measure both.
The middle-end is a sequence: IR in, pass, IR out, repeat. Passes come in three kinds — analyses that compute facts, transformations that rewrite, and cleanups that make the next pass's job possible — and the pipeline is how a compiler is actually organised.
The same passes in a different order produce different code, and no order is best for every program. Constant propagation, branch simplification and dead-code elimination are the canonical cascade — and running the pipeline to a fixed point is what a compiler does instead of solving the problem.