Or in particular: If you only have rules that say what UB is , how do you tell that a program isn’t UB; this is the kind of question you really really want to make as easy to answer as possible, as it’s the question of how to write programs that don’t UB, the question that every programmer using the language has to answer every time they use the language (or in cases like Rust, every time they use unsafe
Rust).
By designing the set of allowable transformations such that if the inputs to a function meet certain constraints, the outputs will be precisely specified, and if the inputs meet looser constraints, the outputs will be less strongly specified but still meet some constraints. Program correctness could thus be demonstrated either by showing that the inputs meet the tighter constraints (and the downstream paths expect the precisely specified output), or that the downstream paths will be satisfied by the more loosely specified outputs that may result from the more loosely specified inputs.
If a language were to specify that integer computations which overflow generally use quiet two's-complement wraparound semantics, but also specified that a compiler may perform certain transfomrs such as replacing an expression which is of the form (a*N)/N
[for positive integer N] with a
, or (a*N)/(b*n)
with a/b
, one could demonstrate mathematically that such transformations could not affect program behavior in cases where none of the original expressions would overflow. In many cases, it might be easier to demonstrate that a computation like (a*60)/30
would never be performed when a
is large enough to make a*60
overflow, and then assume that the expression will behave precisely as written, than to examine all of the ways a compiler might rewrite it (e.g. (a*10)/5
, (a*4)/2
, a*6/3
, etc.) and confirm that all of them meet requirements.
On the other hand, there are situations where processing a mixture of meaningful and meaningless computations may be more efficient than trying to determine beforehand which computations will be meaningful. In such cases, even if overflows could occur in meaningless computations, a programmer could prove that the transforms won't affect program correctness by showing that overflows will never occur in meaningful computations, and that the only requirement for the meaningless computations is that they have no side effect beyond yielding a meaningless result (a condition which the transforms would not affect). That seems more practical, and would allow the generation of more efficient code, than having to write the code in a way that would prevent overflow from occurring in the meaningless expressions (which would in turn prevent the compiler from performing the above transformations).
If you would have to consider “whether certain reorderings of operations might affect program behavior” instead, you would exponentially increase the things you need to validate. You don’t only have to consider every possible program input, but additionally also every possible way to apply “reorderings” that are defined/axiomized to be legal in order to rule out UB from your programs.
In most cases, the easiest way for a programmer to ensure that allowable reorderings wouldn't prevent a program from meeting requirements would be to avoid any constructs where reorderings could have even localized effects on program behavior. On the other hand, it may sometimes be easier to show that the only things affect by a reordering will be values that will end up being ignored by the current program, or which--if they are included in a blob of binary data that gets output by the program, will be ignored by whatever programs are going to read that blob.
One semantics definition which then must be shown to actually support these reorderings. Of course after the reordering are shown to be derivable from that semantics, it makes absolutely no sense anymore to keep the reordering rules be the language specification.
A major problem here is that it is impossible to write a semantics specification which will regard as UB all of the corner cases where an allowable reordering would turn a program which would have met requirements into one that doesn't, without regarding as UB any situations where no allowable reordering would do so. Any practical set of rules will end up putting more restrictions on programmers and/or compilers than one which regards the transforms as axioms.
Well said. Isn't this the trap that attempts to more or less formally describe how concurrent/multithreaded code behaves fell into? I vaguely remember the official formalizations of Java concurrency were written in terms of what re-orderings JVM/hardware is allowed to do?
The optimal way for for programs to achieve required semantics will often vary depending upon what guarantees the underlying hardware platform can offer. Programs that are designed to exploit a particular hardware memory model can often be more efficient than programs that are written in hardware-agnostic fashion, but may be extremely difficult to port to hardware with a different model. I don't recall the JVM as being defined in terms of allowable transformations; I haven't looked at it in ages, but I recall it was defined in terms of non-transitive ordering relationships, and it of course avoided allowing programmers any abilities that wouldn't be universally supportable.
BTW, returning somewhat to the subject of the original post, I find myself wondering why high-performance computing would be done with a language which allows pointer arithmetic beyond "form non-persistable reference to sub-object", much less arbitrary conversions between integers and pointers. Support for more generalized pointer arithmetic is helpful if one is trying to use a simple compiler to generate efficient code for a processor with limited addressing modes, or if one needs to perform memory management within a language rather than using an outside library, but from what I understand, high-performance computing generally doesn't require doing any of those things. Allowing more generalized pointer arithmetic or--even worse--arbitrary conversions between integers and pointers, makes it impossible for the language to support optimization without adding a lot of complexity to both the language specification and to implementations. What benefit do high-performance computing applications actually receive in exchange for this complexity?