Maybe D was wrong on contracts
Quirin Schroll
qs.il.paperinik at gmail.com
Mon Aug 17 18:57:04 UTC 2026
TL;DR: D’s contracts are theoretically unsound, but there’s an
easy fix.
---
I just read
[P3097](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3097r3.pdf), an accepted proposal for C++29. It allows for pre- and postconditions on virtual member functions. Its design goals and discussion sections (§§ 3–4) are really interesting as they discuss (among others) the design of D’s contracts. (You don’t need to know any C++ to follow most of the proposal sections.)
Here’s the, in my opinion, most crucial paragraph for the D
community. “It” refers to the design of prior proposals that were
similar to D’s and Eiffel’s contracts, which closely follow the
Liskov substitution principle:
> [It] assumes that contract assertions can express the entire
> plain-language contract across the whole program, while in
> reality they will only ever express a subset of it, and only in
> some components. Our proposed C++ design accounts for this
> fundamental limitation, ensures that introducing contract
> assertions cannot cause remote breakage of correct code, and
> enables the user to adopt precondition and postcondition
> assertions on virtual functions incrementally.
>
> […]
>
> More fundamentally, OR-ing two sequences of *contract
> assertions* is not equivalent to OR-ing the two contracts that
> those assertions check. As we saw above, contract assertions
> can only validate a subset of a function’s plain-language
> contract. Therefore, assuming that one side of such a
> disjunction is satisfied merely because none of the associated
> assertions failed is logically unsound.
In § 4.7, P3097 exemplifies how the mantra of OR-ing and AND-ing
is incorrect (overly narrow) if the goal is to enforce the
substitution principle. I won’t bore you with the example; in
short: If a virtual function has a precondition *P* and
postcondition *C* (read *C* as conclusion), any implementation
must satisfy the implication *P* ⇒ *C.* Thus, for the
substitution principle to hold, any overriding function must also
satisfy *P* ⇒ *C,* but the current state of the D language
requires it to actually satisfy *P* ∨ *Q* ⇒ *C,* where *Q* is the
conjunction of the preconditions of the overrider. Notably, *Q*
might be `true` when the preconditions of the overrider are the
empty set.
Maybe C++ got something right, or rather, D got something wrong
that C++ got right-er:
*A virtual function’s postconditions don’t get limit what
overriders do when invoked with arguments that don’t satisfy its
preconditions.*
The violation of this principle might be the reason why D’s
contracts never took off.
It might be worth considering moving closer to C++’s semantics of
contracts, not because of interoperability or compatibility, but
because they’re closer to correct.
D should validate contracts differently and in-line with the
following principle:
*The postconditions of a function need only be met by its
implementation if its precondition was satisfied.*
For non-virtual functions, the violation of preconditions simply
leads to an `Error` being thrown, thus skipping the code that
checks the postconditions, thus coincidentally satisfying the
principle. It fails only for virtual functions.
Example:
```d
class Base { int f(int x) in(x > 0) out(r; r >
0); }
class Derived : Base { override int f(int x) in(true) out(r;
true ); }
```
If we assume both implementations are just `{ return x; }`, all
contracts should be satisfied because with `r == x`, all
individual contracts are of the form *X* implies *X,* which is
obviously true.
Where D is going astray is not that when calling `Derived.f`, it
checks if `Base.f.in` is satisfied at all, but what it does with
the result. Current behavior is: `Derived.f` cannot assume
anything (because `Derived.f.in` is `true`) and must definitively
establish `r > 0` because `Base.f.out` says so and in the current
language semantics, all postconditions have to be met
unconditionally. This is logically incorrect because `Base.f.out`
need only be satisfied if `Base.f.in` was satisfied to begin with
(that’s what a contract actually means); if it wasn’t satisfied,
`Derived.f` should not be bound by `Base.f.out` in any way.
I have no idea how DMD implements contract checking, but I assume
it wouldn’t be too difficult to skip checking postconditions if
the corresponding preconditions hadn’t been met. At worst, it has
to store a `bool` to remember that. I’d consider this a miniscule
price to pay (in terms of performance) for a contract semantics
that is theoretically sound.
C++29, according to the proposal, given an object with a static
type of `C`, the language wouldn’t consider the contracts of
`Base.f` at all. The reasoning is: If we *know for sure* we have
a `C` object, only contracts specified on `C` concern us, as well
as contracts of the dynamically invoked function; this
(intentionally) allows for violations of the substitution
principle for cases where no plain substitution takes place. I’m
not proposing that at all, or rather, if anything, for
`extern(C++)` functions only.
More information about the Digitalmars-d
mailing list