www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Maybe D was wrong on contracts

reply Quirin Schroll <qs.il.paperinik gmail.com> writes:
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.
Aug 17
next sibling parent "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
Two known problems with contracts:

1.   =inclusiveincontracts 'in' contracts of overridden methods must be 
a superset of parent contract 
(https://dlang.org/changelog/2.095.0.html#inclusive-incontracts)

2. the decision to emit must be done by the caller, not the callee's 
compilation flags. Nobody has wanted to fix this yet (although approved).

To quote Mike:

"We approved enabling the following switches in a future edition: 
fieldwise, nosharedaccess, inclusiveincontracts, fiximmutableconv, and 
systemvariables."
Aug 17
prev sibling next sibling parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 8/17/26 20:57, Quirin Schroll wrote:
 TL;DR: D’s contracts are theoretically unsound, but there’s an easy fix.
 
 ....
 
 Maybe C++ got something right, or rather, D got something wrong that C++ 
 got right-er:
 ....
It's not like I didn't try: https://issues.dlang.org/bugs/7584/
 ...
 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.
 ...
Continuing when an `in` contract fails needs to catch an `AssertError`. (This is already a problem.)
Aug 17
parent Meta <jared771 gmail.com> writes:
On Monday, 17 August 2026 at 19:10:57 UTC, Timon Gehr wrote:
 Continuing when an `in` contract fails needs to catch an 
 `AssertError`. (This is already a problem.)
What's the best solution to this? Changing the throwing of an AssertError to something like ContractViolatedException?
Aug 18
prev sibling next sibling parent "H. S. Teoh" <hsteoh qfbox.info> writes:
On Mon, Aug 17, 2026 at 06:57:04PM +0000, Quirin Schroll via Digitalmars-d
wrote:
 TL;DR: D’s contracts are theoretically unsound, but there’s an easy fix.
[...]
 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.
[...] IMO, the reason D's contracts never took off is because they are too complex to reason about when mixed with inheritance / polymorphism. I do use them in my own code, but rarely in class hierarchies, if at all. For simple domain checking of function inputs / verification of outputs, they can be pretty convenient for catching refactoring mistakes that break existing code. But mixing them with inheritance leads to mind-boggling complexities about who/what can/cannot override what/when/how, and what the resulting semantics are "supposed" to be -- it's just wayyy too much mental effort for marginal benefits. T -- Let X be the set not defined by this sentence...
Aug 17
prev sibling parent Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Monday, August 17, 2026 1:23:31 PM Mountain Daylight Time H. S. Teoh via
Digitalmars-d wrote:
 On Mon, Aug 17, 2026 at 06:57:04PM +0000, Quirin Schroll via Digitalmars-d
wrote:
 TL;DR: D’s contracts are theoretically unsound, but there’s an easy fix.
[...]
 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.
[...] IMO, the reason D's contracts never took off is because they are too complex to reason about when mixed with inheritance / polymorphism. I do use them in my own code, but rarely in class hierarchies, if at all. For simple domain checking of function inputs / verification of outputs, they can be pretty convenient for catching refactoring mistakes that break existing code. But mixing them with inheritance leads to mind-boggling complexities about who/what can/cannot override what/when/how, and what the resulting semantics are "supposed" to be -- it's just wayyy too much mental effort for marginal benefits.
100%, though I don't think that contracts get used much in D ever without considering classes. As far as classes go, even assuming that D's implementation for them is correct, actually reasoning about them in the face of inheritance is simply too hard to be worth even considering in the vast majority of cases. Arguably, if you actually need that level of checks, them maybe you should rethink your design. And if you're just trying to catch bugs, well, at that point, you're getting into the issue of whether your checks are even correct. It's basically the same as writing proofs for code correctness. Past a certain point of complexity, they're not even vaguely worth it in the vast majority of cases, because you then not only have to debug and verify your program, but you have to debug and verify your proof. It's simply not worth it. Maybe it would make sense for someone like Boeing on NASA, but for anything approaching a normal program, it's really not. As for contracts in general (separate from classes and inheritance), D's current implementation makes them borderline pointless anyway. For them to be truly valuable, they need to be inserted at the call site based on how the caller is compiled rather than be compiled into the function itself and called based on how that function is compiled. As things stand, for in contracts, you might as well just put the assertions at the top of the function and not bother with the in contract. And out contracts are just pretty much useless in general regardless. Occasionally, they make sense, but in the vast majority of cases, what the result of the function should be depends on the input, and that doesn't work with out contracts (at least not with how D implements them). So, for the most part, unit tests are a far better way to verify what you might theoretically want to put in an out contract. The result of all of this is that contracts in D as currently implemented are pretty pointless. They do have some value with inheritance in the case where the conditions are simple enough to be reasonable (since you can't just put the assertions at the top of the function when inheritance is involved), but that's going to be rare for most folks. And classes are used pretty sparingly in the average D codebase anyway. And on top of that, you have the issue of whether assertions or exceptions are a better solution when it comes to validating function input. There are plenty of cases where assertions make good sense (and thus where in contracts could be useful if they were implemented properly), but with public-facing APIs, there's always the argument that defensive programming is be a better approach, because then it's impossible for bad input to make it past the checks at the top of the function instead of relying on the caller to have done their due dilligence (be it by compiling the code with contracts enabled or by simply verifying the conditions before calling the function). And of course, being able to make that decision properly requires that the programmer properly understand what assertions and exceptions are for and when it makes sense to use one or the other, and that seems to be bizarrely hard for some folks to grasp for some reason. All in all, I think that contracts are a nice idea, but D's implementation needs some work for them to make sense there. Even then though, I doubt that I would use them much. I'd use them more than I do now, but I doubt that I'd use them heavily, and I'd likely still never use them with classes, because I rarely use classes, and the logic for using contracts with them correctly is just too complicated. - Jonathan M Davis
Aug 20