3 ms·
C used to be a "cocky macroassembler", up until people started writing optimizing compilers for it. If your compiler merely translates a source line into a ser
by kmeisthax 4y ago
C used to be a "cocky macroassembler", up until people started writing optimizing compilers for it.
If your compiler merely translates a source line into a series of assembly mnemonics, function calls, or interpreter gotos, then the interface is the implementation. You can rely on the underlying target language to provide your program with meaning and the only people who have to care are people reimplementing your compiler for compatibility.
The moment you start talking about optimization, then this no longer works. You no longer have a correspondence between source and compiled forms of one program. You have a many-to-many relationship where one source form can be compiled into hundreds of binaries depending on how the compiler is configured, and many source forms may actually optimize to the exact same compiled form. This requires you to provide your own semantics, else compiled programs have no meaning and -O3 becomes shorthand for "make demons fly out my nose".
In the case of C they came up with a series of rules for what-not-to-do that both did not match existing language semantics and also were dangerously incomplete. There are still C programmers who insist that you can free() memory but still touch it for a "little while"[0], or access memory "off the end" of an allocation[1], for example. And ISO C still made the mistake of retaining pointers, which are a confusing mix of value and reference type. They aren't references because you are allowed to cast them to and from integers; and they can't be values because you can use them to modify other values. Because of this tension, we keep discovering new combinations of valid transformations on valid programs that cause miscompiles, and then we have to invent things like pointer provenance to fix them.
As far as I'm concerned, the only difference between Rust and C is that Rust is honest about it's cleverness. C has to pretend to be simple while also out-clevering Rust (or at least, the safe subset of Rust).
[0] Usually in an attempt to emulate automatic memory management. Manual memory management does not work when passing complex structures across an API boundary, and the only options are to either expose custom deallocators (which means no optimizations even when they are sound), tell callers how to deallocate the data (which means no changing the data), or hack the allocator to do what you really want.
[1] It works for malware developers, it should work for me, right?
- int_19h 4y ago> they can't be values because you can use them to modify other values That seems like an arbitrary criterion. They're values because they behave like values when copying them around etc. You can't even use them to modify other values implicitly - you still need to use * to get an actual dereferenceable/assignable lvalue out of the pointer.
- kmeisthax 4y agoWhat operator is used to modify the value doesn't matter, just the fact that you can do so. At the point where you use an assignable lvalue, the optimizer needs to know where that lvalue came from, which means that pointers need to be tracked like references. My point is that because pointers act like both values and references, they are neither values nor references. This makes it impossible to soundly reason about them.
- ralfj 4y agoIt is certainly possible to soundly reason about them. The C standard just does not describe how to do it. There is plenty of PL work (in particular everything considering ML with imperative features, many decades worth of work) where references are values, that works perfectly fine.
- int_19h 4y agoI still don't see why it's contradictory. They can be both values and references just fine.