Contracts, Undefined Behavior, and Defensive,Programming
Johannes Pfau
nospam at example.com
Sat Jun 13 08:52:18 UTC 2020
Am Fri, 12 Jun 2020 23:46:38 -0400 schrieb Andrei Alexandrescu:
> A short and sweet paper:
>
> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1743r0.pdf
>
> It echoes Walter's opinions on contracts vs. runtime checking, to a tee.
> He has been derided on occasion in this group for such views. The paper
> is a good rationale from an independent source.
A good article. Although there have sometimes been discussions about it,
I think the difference between input validation and contract checking is
now well understood by most people participating in discussions here.
However, this article also gives some vocabulary which might allow us to
discuss the assert/assume conflict, where there is no uniform position in
the community, in a clearer and more precise way. Consider this example:
------------------------------------------------------------------------
/**
* returns (1 + 2 + ... + n) / n
*/
double sumDiv(ubyte n) @safe
{
double accum = 0;
for (size_t i = 0; i <= n; i++)
{
accum += i;
if (i == n)
return accum / i;
}
// DMD requires this to compile the code, cause it does not detect
// the above loop covers all possible inputs.
// GDC and LDC optimizers remove this assert.
assert(0);
}
------------------------------------------------------------------------
For n = 0, this function causes soft (library) undefined behavior
according to the article: 0.0/0 is defined to give NaN, but it's not a
valid result for the algorithm above. Please note that the if block in
the loop will return for all possible input values. The assert(0) is only
needed because of compiler limitations, but never executed.
So we apply defensive programming and add this contract:
----------------------------
double sumDiv(ubyte n)
in (n > 0)
----------------------------
This now nicely detects the error in debug mode. However, the D spec says
"The compiler is free to assume the assert expression is true and optimize
subsequent code accordingly." and "Undefined Behavior: The subsequent
execution of the program after an assert contract is false.": https://
dlang.org/spec/contracts.html
I didn't remember that the spec was that upfront about this. Let's see
how this affects our example:
* If the compiler interprets in(n > 0) as assume(n > 0), this means that
the first iteration of the for loop with i = 0 has no effect: The if
condition is always false (n != 0) and the accum += 0 has no effect.
* So it may rewrite the loop bounds to start at i = 1
* If this optimized function is called with n = 0, the loop body and the
return is never executed
* Remember that the assert(0) at the end could already be removed by the
optimizer
* I guess all bets are off what happens now
So using definsive programming we turned soft UB into hard UB which will
probably crash the program. It could also cause memory corruption, run
into an infinite loop, .... And please note that all this even happens in
a @safe function.
So because of this, I think it's a really bad idea to conflate assume and
assert meanings.
--
Johannes
More information about the Digitalmars-d
mailing list