Try compiling something like the following with gcc and see what happens:
int foo( struct thing *ptr ) {
const int value = thing->value;
if(ptr==NULL) return -1;
if(is_inverse(thing->data_in)) [...]
if(value == 0) [...]
}
No compiler warning, it just segfaults when run because the NULL pointer check has been removed because gcc sees the deref at the start, long before the value is used (which follows the NULL pointer check), and decides that since you've deref'd it there it can remove the NULL pointer check and emit code that it knows will crash when run.
OK, but an imaginary "literal compiler that does exactly what is written with no 'optimization'" (which again is a poorly formed concept) would _also_ segfault. Only a very weird optimizing compiler which defers evaluation of `const int value = thing->value;` to after the null check but still performs the null pointer check would return successfully here.
The code you wrote is dangerous, but not for the reason you say. The problem is that even if "thing" is NULL, "thing->value" can result in a valid memory address. And yes, this sort of thing _would_ be caught by Fil-C. Not that it would prevent a trap, since the code is defective, but if the supplied pointer is NULL, it would have empty capabilities. There would be no danger that it would inadvertently (due to pointer arithmetic) turn into a valid memory address and cause corruption to an unrelated object. Instead the code will either return -1 or trap without allowing memory corruption.
Expected behaviour: Either see that the value isn't used until after the pointer check and emit code as expected, or warn that there's a reference/check conflict in the code.
That doesn't make sense. The value *is* used before the pointer check.
In once case several folks spent some hours trying to figure out how gcc had decided to remove a NULL pointer check and could never figure it out. There was no obvious cause for it, it simply output code that had the NULL pointer check removed. The code eventually got rewritten with some volatile hack to make sure the pointer check wasn't quietly deleted.
Deciding to leave your code defective is not a good choice. The obvious reason for gcc to remove a check is that previous code already used the value, but did so silently. By plastering volatile around until it doesn't crash, you have simply decided to leave the real bug in place. There are a ton of tools available that will help diagnose the real problem, for instance ubsan would likely find it immediately. Or use a different language! Most languages don't have C/C++ style undefined behavior which is the problem here, not the compiler optimziing.
And then there's the nonnnull attribute, which in gcc does the opposite of what you'd expect: It doesn't warn if you pass in a NULL value for a nonnull parameter, but it will remove NULL pointer checks in your code.
That's funny because I that's exactly what I expect. It's an optimization flag to tell the compiler to optimize assuming the value passed is not NULL. And it _does_ emit a warning if you explicitly pass NULL to a non-null parameter if you use -Wnonnull or -Wall.
I could certainly see an argument for an attribute that requested the warnings but didn't add the optimization. But it is not what I would expect that attribute to do. In any case, nonnull is not part of C, it's a controlled non-conformity for gcc. A compiler running in a conformant mode will only remove the check when it's redundant based on the program text.
How is a compiler that rewrites for(i=0;i<10;i++) into for(i=0;i!=10;i++) preserving code semantics?
Well, to be annoyingly precise, it doesn't do that. It chooses a possible implementation of the former in the target machine code, and it does so within the constraints of the language (you aren't allowed to modify memory from an independent context). It may pick an implementation that "looks" more like the latter, but technically that's only in your imagination. And that in a nutshell is my point about people who complain about C optimizer "changing" their program. Compilers _have_ to change your program for it to be able to run.