Contracts, Undefined Behavior, and Defensive,Programming

Stanislav Blinov stanislav.blinov at gmail.com
Sat Jun 13 15:04:45 UTC 2020


On Saturday, 13 June 2020 at 08:52:18 UTC, Johannes Pfau wrote:

> ------------------------------------------------------------------------
> /**
>  * 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.

According to the algorithm, as presented, NaN is a valid result, 
and this so-called "soft" UB does not exist. It is the 
documentation of `sumDiv` that is incorrect, or rather, 
incomplete - it presupposes `n` being non-zero, but otherwise 
does not make any statement wrt the interface (i.e. usage) of the 
function, so we shall turn to the interface. The function is 
@safe, so it promises to handle all safe inputs, which certainly 
include 0, without causing UB, and only return safe values, which 
NaN is. Optimizers, therefore, must not "optimize" this function 
into causing "hard" UB (such as removing the 0th iteration 
without inserting a `ret`).

If we only saw the declaration, without definition, i.e.:

/**
  * returns (1 + 2 + ... + n) / n
  */
double sumDiv(ubyte n) @safe;

Nowhere is it stated what happens if input is 0. But still, it 
has a safe interface, it is @safe, and so must return a safe 
value, which NaN is. No UB, "soft" or "hard".

If `sumDiv` expects *the callers* to *never* pass 0, it must 
either:

- throw an Exception (which its interface allows)
- throw an Error (and become nothrow)
- profess so via its interface:

> double sumDiv(NonZero!ubyte n) @safe @nogc nothrow;
> //or, if it must never return a NaN:
> NeverNaN!double sumDiv(NonZero!ubyte n) @safe @nogc 
> /+/*perhaps*/nothrow+/;

...given hypothetical `NonZero` and `NeverNaN` types. Then the 
responsibility of checking for 0 is delegated away from the 
function body, and optimizers may indeed @safe-ly remove the 0th 
iteration.

> So we apply defensive programming and add this contract:
> ----------------------------
> double sumDiv(ubyte n)
> in (n > 0)

Dropping @safe? Then yes, there may be UB. But if @safe is to be 
kept, there's a case to be made to error out on that contract as 
being an invalid contract.

There's a "but" though, which concerns that stipulation on 
`assert` which you quoted from the spec. But that "but" has to do 
with semantics of `sumDiv` being available during compilation, 
which is not always possible because compilers cop out to 
"opaque" object files too early (i.e. in case if it's a library 
function, compiled into a separate object file, with only the 
declaration present during compilation). However, since, 
according to Walter, I "misunderstand" what linkers do (nevermind 
the fact that it's very much still about compilation and not 
linking), I'll say no more on that particular subject. Suffice it 
to say that in such a case, indeed, conflating `assert` and 
`assume` is just wrong.


More information about the Digitalmars-d mailing list