Contracts, Undefined Behavior, and Defensive,Programming

Johannes Pfau nospam at example.com
Sat Jun 13 15:38:03 UTC 2020


Am Sat, 13 Jun 2020 14:41:19 +0000 schrieb Jesse Phillips:

> On Saturday, 13 June 2020 at 08:52:18 UTC, Johannes Pfau wrote:
>>
>>
>> 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.
> 
> The article talks about this. It states that soft undefined behavior can
> lead to hard UB.
> 
> I don't think it tries to argue this is OK. 

I think you're refering to the strlen example on page 4? It is true that 
soft UB **may** lead to hard UB in general. If this is caused by 
application code, as in that example, it is something @safe has to (and 
can) take care of.

In application code, soft UB may be limited to local effects. Consider an 
online banking application showing the current date somewhere: If some 
code manages to assign 25 to the hours part of the date, that certainly 
violates some semantic guarantees. But if the date is only shown to the 
user, the worst thing that'll happen is that the user may see an 
incorrect date. And if all code is @safe, we're still guaranteed not to 
get memory corruption (hard UB).

But now if I add an assert(day <= 24) somewhere with best defensive 
programming intentions, the compiler may turn that into hard UB now. And 
hard UB could even include memory corruption setting your account balance 
to a negative value.


As assert "[...] checks (along with any action taken should a check fail) 
are, however,never a part of a function’s contract, cannot be relied upon 
by the clients..." they subvert @safe checking when used as assumes:

An example:

void a(int n) @safe
{
    assert (n > 0);
}

void b() @safe
{
    a(-1);
}

The compiler can not prevent the a(-1) call in b, as it does not know the 
semantic invariant demanded by a(). However, in a(), the assert (n > 0) 
get's ransformed to assume(n > 0) in release mode, which will lead to 
safe code causing hard UB.

Without assume, points where soft UB can turn into hard UB should all be 
covered by @safe (at least for memory corruption).


To summarize and rephrase this: the encapsulation unit of @safe is the 
function boundary. The parameter definitions (including this pointer) and 
return type and their modifiers are the information the compiler uses to 
implement it's safety checks. For all possible combinations of these 
input values, the @safe function may not cause memory corruption.

asserts then narrow the function contract by describing semantic 
limitations and @safe is not able to enforce this, as this information is 
not part of the function signature. This is fine, as @safe will still 
prevent soft UB from escalating to hard UB. But if you now use assert() 
as assume(), you introduce hard UB for some possible input combinations, 
subverting @safe guarantees.



> [...]
I agree with everything else you said ;-)





-- 
Johannes


More information about the Digitalmars-d mailing list