Skip to content

Commit f65c153

Browse files
committed
Add contracts docs for order-of-evaluation and contract predicates
BOOTSTRAP NOTE FOR CONTRIBUTORS: After taking this commit, just remember to build cppfront from the Cpp1 sources first as usual (see https://hsutter.github.io/cppfront/#how-do-i-get-and-build-cppfront ). Then you'll be able to compile the updated reflect.h2 in this commit just fine. Renamed the provided contract groups to lowercase descriptive names; e.g., Type -> type_safety Closes #1018 Renamed .has_handler to .is_active, which makes more sense for contract groups that don't have replaceable handlers Reordered violation test to check predicates first, before grp.is_active
1 parent 3d0a497 commit f65c153

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+264
-213
lines changed

docs/cpp2/contracts.md

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,23 @@ Notes:
1515

1616
- Optionally, `condition` may be followed by `, "message"`, a message to include if a violation occurs. For example, `pre(condition, "message")`.
1717

18-
- Optionally, a `<Group>` can be written inside `<` `>` angle brackets immediately before the `(`, to designate that this test is part of the [contract group](#contract-groups) named `Group`. If a violation occurs, `Group.report_violation()` will be called. For example, `pre<Group>(condition)`.
18+
- Optionally, a `<group, pred1, pred2>` can be written inside `<` `>` angle brackets immediately before the `(`, to designate that this test is part of the [contract group](#groups) named `group` and (also optionally) [contract predicates](#predicates) `pred1` and `pred2. If a violation occurs, `Group.report_violation()` will be called. For example, `pre<group>(condition)`.
19+
20+
The order of evaluation is:
21+
22+
- First, predicates are evaluated in order. If any predicte evaluates to `#!cpp false`, stop.
23+
24+
- Next, `group.is_active()` is evaluated. If that evaluates to `#!cpp false`, stop.
25+
26+
- Next, `condition` is evaluated. If that evaluates to `#!cpp true`, stop.
27+
28+
- Finally, if all the predicates were true and the group is active and the condition was false, `group.report_violation()` is called.
1929

2030
For example:
2131

2232
``` cpp title="Precondition and postcondition examples" hl_lines="2 3"
2333
insert_at: (container, where: int, val: int)
24-
pre<Bounds>( 0 <= where <= vec.ssize(), "position (where)$ is outside 'val'" )
34+
pre<bounds>( 0 <= where <= vec.ssize(), "position (where)$ is outside 'val'" )
2535
post ( container.ssize() == container.ssize()$ + 1 )
2636
= {
2737
_ = container.insert( container.begin()+where, val );
@@ -37,27 +47,31 @@ In this example:
3747
- The postcondition is part of the `Default` safety contract group. If the check fails, then `#!cpp cpp2::Default.report_violation()` is called.
3848
3949
40-
## <a id="contract-groups"></a> Contract groups
50+
## <a id="groups"></a> Contract groups
51+
52+
Contract groups are useful to enable or disable or [set custom handlers](#violation-handlers) independently for different groups of contracts. A contract group `grp` is just the name of an object that can be called with:
4153
42-
Contract groups are useful to enable or disable or [set custom handlers](#violation-handlers) independently for different groups of contracts. A contract group `G` is just the name of an object that can be called with `G.report_violation()` and `G.report_violation(message)`, where `message` is a `* const char` C-style text string.
54+
- `grp.report_violation()` and `grp.report_violation(message)`, where `message` is a `* const char` C-style text string
55+
56+
- `grp.is_active()`, which returns `#!cpp true` if and only if the group is enabled
4357
4458
You can create new contract groups just by creating new objects that have a `.report_violation` function. The object's name is the contract group's name. The object can be at any scope: local, global, or heap.
4559
4660
For example, here are some ways to use contract groups, for convenience using [`cpp2::contract_group`](#violation_handlers) which is a convenient group type:
4761
4862
``` cpp title="Using contract groups" hl_lines="1 4 6 10-12"
49-
GroupA: cpp2::contract_group = (); // a global group
63+
group_a: cpp2::contract_group = (); // a global group
5064
5165
func: () = {
52-
GroupB: cpp2::contract_group = (); // a local group
66+
group_b: cpp2::contract_group = (); // a local group
5367
54-
GroupC := new<cpp2::contract_group>(); // a dynamically allocated group
68+
group_c := new<cpp2::contract_group>(); // a dynamically allocated group
5569
5670
// ...
5771
58-
assert<GroupA >( some && condition );
59-
assert<GroupB >( another && condition );
60-
assert<GroupC*>( another && condition );
72+
assert<group_a >( some && condition );
73+
assert<group_g >( another || condition );
74+
assert<group_c*>( another && condition );
6175
}
6276
```
6377

@@ -81,6 +95,36 @@ function_declaration: @copyable type =
8195
```
8296

8397

98+
## <a id="predicates"></a> Contract predicates
99+
100+
Contract predicates are useful to conditionally check specific contracts as a static or dynamic property. Importantly, if any predicate is `#!cpp false`, the check's conditional expression will not be evaluated.
101+
102+
For example:
103+
104+
``` cpp title="Using contract predicates" hl_lines="1 3 4 7"
105+
is_checked_build: bool == SEE_BUILD_FLAG; // a static (compile-time) predicate
106+
107+
checking_enabled: bool = /*...*/ ; // a dynamic (run-time) predicate,
108+
// could change as the program runs
109+
110+
func: () = {
111+
assert<audit, is_checked_build, checking_enabled>( condition );
112+
}
113+
```
114+
115+
In this example, the order of evaluation is:
116+
117+
- `is_checked_build` is evaluated. Since it is a compile-time value, the evaluation can happen at compile time. If it evaluates to `#!cpp false`, then stop; the entire contract could be optimized away by the compiler.
118+
119+
- Otherwise, next `checking_enabled` is evaluated at run time. If it evaluates to `#!cpp false`, then stop.
120+
121+
- Otherwise, next `audit.is_active()` is evaluated. If it evaluates to `#!cpp false`, then stop.
122+
123+
- Otherwise, next `condition` is evaluated. If it evaluates to `#!cpp true`, then stop.
124+
125+
- Otherwise, `audit.report_violation()` is called.
126+
127+
84128
## <a id="violation-handlers"></a> `cpp2::contract_group`, and customizable violation handling
85129

86130
The contract group object could also provide additional functionality. For example, Cpp2 comes with the `cpp2::contract_group` type which allows installing a customizable handler for each object. Each object can only have one handler at a time, but the handler can change during the course of the program. `contract_group` supports:
@@ -89,34 +133,35 @@ The contract group object could also provide additional functionality. For examp
89133

90134
- `.get_handler()` returns the current handler function pointer, or null if none is installed.
91135

92-
- `.has_handler()` returns whether there is a current handler installed.
136+
- `.is_active()` returns whether there is a current handler installed.
93137

94138
- `.enforce(condition, message)` evaluates `condition`, and if it is `false` then calls `.report_violation(message)`.
95139

96140
Cpp2 comes with five predefined `contract group` global objects in namespace `cpp2`:
97141

98-
- `Default`, which is used as the default contract group for contracts that don't specify a group.
142+
- `default`, which is used as the default contract group for contracts that don't specify a group.
99143

100-
- `Type` for type safety checks.
144+
- `type_safety` for type safety checks.
101145

102-
- `Bounds` for bounds safety checks.
146+
- `bounds_safety` for bounds safety checks.
103147

104-
- `Null` for null safety checks.
148+
- `null_safety` for null safety checks.
105149

106-
- `Testing` for general test checks.
150+
- `testing` for general test checks.
107151

108152
For these groups, the default handler is `cpp2::report_and_terminate`, which prints information about the violation to `std::cerr` and then calls `std::terminate()`. But you can customize it to do anything you want, including to integrate with any third-party or in-house error reporting system your project is already using. For example:
109153

110-
``` cpp title="Example of customized contract violation handler" hl_lines="2 8-9"
154+
``` cpp title="Example of customized contract violation handler" hl_lines="2 8-9 17"
111155
main: () -> int = {
112-
cpp2::Default.set_handler(call_my_framework&);
113-
assert<Default>(false, "this is a test, this is only a test");
156+
cpp2::default.set_handler(call_my_framework&);
157+
assert<default>(false, "this is a test, this is only a test");
114158
std::cout << "done\n";
115159
}
116160

117161
call_my_framework: (msg: * const char) = {
118162
// You can do anything you like here, including arbitrary work
119-
// and integration with your current error reporting libraries
163+
// and integration with your current error reporting libraries,
164+
// log-and-continue, throw an exception, whatever is wanted...
120165
std::cout
121166
<< "sending error to my framework... ["
122167
<< msg << "]\n";

include/cpp2util.h

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -409,8 +409,7 @@ class contract_group {
409409

410410
constexpr contract_group (handler h = {}) : reporter{h} { }
411411
constexpr auto set_handler(handler h = {}) { reporter = h; }
412-
constexpr auto get_handler() const -> handler { return reporter; }
413-
constexpr auto has_handler() const -> bool { return reporter != handler{}; }
412+
constexpr auto is_active () const -> bool { return reporter != handler{}; }
414413

415414
constexpr auto enforce(bool b, CPP2_MESSAGE_PARAM msg = "" CPP2_SOURCE_LOCATION_PARAM_WITH_DEFAULT)
416415
-> void { if (!b) report_violation(msg CPP2_SOURCE_LOCATION_ARG); }
@@ -435,27 +434,27 @@ class contract_group {
435434
std::terminate();
436435
}
437436

438-
auto inline Default = contract_group(
437+
auto inline cpp2_default = contract_group(
439438
[](CPP2_MESSAGE_PARAM msg CPP2_SOURCE_LOCATION_PARAM)noexcept {
440439
report_and_terminate("Contract", msg CPP2_SOURCE_LOCATION_ARG);
441440
}
442441
);
443-
auto inline Bounds = contract_group(
442+
auto inline bounds_safety = contract_group(
444443
[](CPP2_MESSAGE_PARAM msg CPP2_SOURCE_LOCATION_PARAM)noexcept {
445444
report_and_terminate("Bounds safety", msg CPP2_SOURCE_LOCATION_ARG);
446445
}
447446
);
448-
auto inline Null = contract_group(
447+
auto inline null_safety = contract_group(
449448
[](CPP2_MESSAGE_PARAM msg CPP2_SOURCE_LOCATION_PARAM)noexcept {
450449
report_and_terminate("Null safety", msg CPP2_SOURCE_LOCATION_ARG);
451450
}
452451
);
453-
auto inline Type = contract_group(
452+
auto inline type_safety = contract_group(
454453
[](CPP2_MESSAGE_PARAM msg CPP2_SOURCE_LOCATION_PARAM)noexcept {
455454
report_and_terminate("Type safety", msg CPP2_SOURCE_LOCATION_ARG);
456455
}
457456
);
458-
auto inline Testing = contract_group(
457+
auto inline testing = contract_group(
459458
[](CPP2_MESSAGE_PARAM msg CPP2_SOURCE_LOCATION_PARAM)noexcept {
460459
report_and_terminate("Testing", msg CPP2_SOURCE_LOCATION_ARG);
461460
}
@@ -496,28 +495,28 @@ auto assert_not_null(auto&& arg CPP2_SOURCE_LOCATION_PARAM_WITH_DEFAULT) -> decl
496495
// STL iterator has the default-constructed value. So use it only for raw *...
497496
if constexpr (std::is_pointer_v<CPP2_TYPEOF(arg)>) {
498497
if (arg == CPP2_TYPEOF(arg){}) {
499-
Null.report_violation("dynamic null dereference attempt detected" CPP2_SOURCE_LOCATION_ARG);
498+
null_safety.report_violation("dynamic null dereference attempt detected" CPP2_SOURCE_LOCATION_ARG);
500499
};
501500
}
502501
else if constexpr (UniquePtr<CPP2_TYPEOF(arg)>) {
503502
if (!arg) {
504-
Null.report_violation("std::unique_ptr is empty" CPP2_SOURCE_LOCATION_ARG);
503+
null_safety.report_violation("std::unique_ptr is empty" CPP2_SOURCE_LOCATION_ARG);
505504
}
506505
}
507506
else if constexpr (SharedPtr<CPP2_TYPEOF(arg)>) {
508507
if (!arg) {
509-
Null.report_violation("std::shared_ptr is empty" CPP2_SOURCE_LOCATION_ARG);
508+
null_safety.report_violation("std::shared_ptr is empty" CPP2_SOURCE_LOCATION_ARG);
510509
}
511510
}
512511
else if constexpr (Optional<CPP2_TYPEOF(arg)>) {
513512
if (!arg.has_value()) {
514-
Null.report_violation("std::optional does not contain a value" CPP2_SOURCE_LOCATION_ARG);
513+
null_safety.report_violation("std::optional does not contain a value" CPP2_SOURCE_LOCATION_ARG);
515514
}
516515
}
517516
#ifdef __cpp_lib_expected
518517
else if constexpr (Expected<CPP2_TYPEOF(arg)>) {
519518
if (!arg.has_value()) {
520-
Null.report_violation("std::expected has an unexpected value" CPP2_SOURCE_LOCATION_ARG);
519+
null_safety.report_violation("std::expected has an unexpected value" CPP2_SOURCE_LOCATION_ARG);
521520
}
522521
}
523522
#endif
@@ -543,7 +542,7 @@ auto assert_not_null(auto&& arg CPP2_SOURCE_LOCATION_PARAM_WITH_DEFAULT) -> decl
543542
msg += "but container is empty"; \
544543
} \
545544
if (!(0 <= arg && arg < max())) { \
546-
Bounds.report_violation(msg.c_str() CPP2_SOURCE_LOCATION_ARG); \
545+
bounds_safety.report_violation(msg.c_str() CPP2_SOURCE_LOCATION_ARG); \
547546
} \
548547
return CPP2_FORWARD(x) [ arg ]; \
549548
}
@@ -591,7 +590,7 @@ auto assert_in_bounds(auto&& x, auto&& arg CPP2_SOURCE_LOCATION_PARAM_WITH_DEFAU
591590
if (msg) {
592591
err += " and the message \"" + msg + "\"";
593592
}
594-
Type.report_violation( err );
593+
type_safety.report_violation( err );
595594
std::terminate();
596595
#else
597596
throw CPP2_FORWARD(x);
@@ -609,7 +608,7 @@ inline auto Uncaught_exceptions() -> int {
609608
template<typename T>
610609
auto Dynamic_cast( [[maybe_unused]] auto&& x ) -> decltype(auto) {
611610
#ifdef CPP2_NO_RTTI
612-
Type.report_violation( "'as' dynamic casting is disabled with -fno-rtti" );
611+
type_safety.report_violation( "'as' dynamic casting is disabled with -fno-rtti" );
613612
return nullptr;
614613
#else
615614
return dynamic_cast<T>(CPP2_FORWARD(x));
@@ -619,15 +618,15 @@ auto Dynamic_cast( [[maybe_unused]] auto&& x ) -> decltype(auto) {
619618
template<typename T>
620619
auto Typeid() -> decltype(auto) {
621620
#ifdef CPP2_NO_RTTI
622-
Type.report_violation( "'any' dynamic casting is disabled with -fno-rtti" );
621+
type_safety.report_violation( "'any' dynamic casting is disabled with -fno-rtti" );
623622
#else
624623
return typeid(T);
625624
#endif
626625
}
627626

628627
auto Typeid( [[maybe_unused]] auto&& x ) -> decltype(auto) {
629628
#ifdef CPP2_NO_RTTI
630-
Type.report_violation( "'typeid' is disabled with -fno-rtti" );
629+
type_safety.report_violation( "'typeid' is disabled with -fno-rtti" );
631630
#else
632631
return typeid(CPP2_FORWARD(x));
633632
#endif
@@ -743,9 +742,9 @@ class deferred_init {
743742
public:
744743
deferred_init() noexcept { }
745744
~deferred_init() noexcept { destroy(); }
746-
auto value() noexcept -> T& { Default.enforce(init); return t(); }
745+
auto value() noexcept -> T& { cpp2_default.enforce(init); return t(); }
747746

748-
auto construct(auto&& ...args) -> void { Default.enforce(!init); new (&data) T{CPP2_FORWARD(args)...}; init = true; }
747+
auto construct(auto&& ...args) -> void { cpp2_default.enforce(!init); new (&data) T{CPP2_FORWARD(args)...}; init = true; }
749748
};
750749

751750

@@ -765,9 +764,9 @@ class out {
765764
bool called_construct_ = false;
766765

767766
public:
768-
out(T* t_) noexcept : t{ t_}, has_t{true} { Default.enforce( t); }
769-
out(deferred_init<T>* dt_) noexcept : dt{dt_}, has_t{false} { Default.enforce(dt); }
770-
out(out<T>* ot_) noexcept : ot{ot_}, has_t{ot_->has_t} { Default.enforce(ot);
767+
out(T* t_) noexcept : t{ t_}, has_t{true} { cpp2_default.enforce( t); }
768+
out(deferred_init<T>* dt_) noexcept : dt{dt_}, has_t{false} { cpp2_default.enforce(dt); }
769+
out(out<T>* ot_) noexcept : ot{ot_}, has_t{ot_->has_t} { cpp2_default.enforce(ot);
771770
if (has_t) { t = ot->t; }
772771
else { dt = ot->dt; }
773772
}
@@ -781,7 +780,7 @@ class out {
781780
// then leave it in the same state on exit (strong guarantee)
782781
~out() {
783782
if (called_construct() && uncaught_count != Uncaught_exceptions()) {
784-
Default.enforce(!has_t);
783+
cpp2_default.enforce(!has_t);
785784
dt->destroy();
786785
called_construct() = false;
787786
}
@@ -790,21 +789,21 @@ class out {
790789
auto construct(auto&& ...args) -> void {
791790
if (has_t || called_construct()) {
792791
if constexpr (requires { *t = T(CPP2_FORWARD(args)...); }) {
793-
Default.enforce( t );
792+
cpp2_default.enforce( t );
794793
*t = T(CPP2_FORWARD(args)...);
795794
}
796795
else {
797-
Default.report_violation("attempted to copy assign, but copy assignment is not available");
796+
cpp2_default.report_violation("attempted to copy assign, but copy assignment is not available");
798797
}
799798
}
800799
else {
801-
Default.enforce( dt );
800+
cpp2_default.enforce( dt );
802801
if (dt->init) {
803802
if constexpr (requires { *t = T(CPP2_FORWARD(args)...); }) {
804803
dt->value() = T(CPP2_FORWARD(args)...);
805804
}
806805
else {
807-
Default.report_violation("attempted to copy assign, but copy assignment is not available");
806+
cpp2_default.report_violation("attempted to copy assign, but copy assignment is not available");
808807
}
809808
}
810809
else {
@@ -816,11 +815,11 @@ class out {
816815

817816
auto value() noexcept -> T& {
818817
if (has_t) {
819-
Default.enforce( t );
818+
cpp2_default.enforce( t );
820819
return *t;
821820
}
822821
else {
823-
Default.enforce( dt );
822+
cpp2_default.enforce( dt );
824823
return dt->value();
825824
}
826825
}
@@ -1251,7 +1250,7 @@ auto as(X const& x CPP2_SOURCE_LOCATION_PARAM_WITH_DEFAULT) -> decltype(auto) {
12511250
}
12521251
// Signed/unsigned conversions to a not-smaller type are handled as a precondition,
12531252
// and trying to cast from a value that is in the half of the value space that isn't
1254-
// representable in the target type C is flagged as a Type safety contract violation
1253+
// representable in the target type C is flagged as a type_safety contract violation
12551254
else if constexpr (
12561255
std::is_integral_v<C> &&
12571256
std::is_integral_v<CPP2_TYPEOF(x)> &&
@@ -1260,7 +1259,7 @@ auto as(X const& x CPP2_SOURCE_LOCATION_PARAM_WITH_DEFAULT) -> decltype(auto) {
12601259
)
12611260
{
12621261
const C c = static_cast<C>(x);
1263-
Type.enforce( // precondition check: must be round-trippable => not lossy
1262+
type_safety.enforce( // precondition check: must be round-trippable => not lossy
12641263
static_cast<CPP2_TYPEOF(x)>(c) == x && (c < C{}) == (x < CPP2_TYPEOF(x){}),
12651264
"dynamic lossy narrowing conversion attempt detected" CPP2_SOURCE_LOCATION_ARG
12661265
);

regression-tests/mixed-bounds-safety-with-assert-2.cpp2

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ main: () -> int = {
99

1010
add_42_to_subrange: (inout rng:_, start:int, end:int)
1111
= {
12-
assert<Bounds>( 0 <= start );
13-
assert<Bounds>( end <= rng.ssize() );
12+
assert<bounds_safety>( 0 <= start );
13+
assert<bounds_safety>( end <= rng.ssize() );
1414

1515
count := 0;
1616
for rng

regression-tests/mixed-bounds-safety-with-assert.cpp2

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ main: () -> int = {
77
}
88

99
print_subrange: (rng:_, start:int, end:int) = {
10-
assert<Bounds>( 0 <= start );
11-
assert<Bounds>( end <= rng.ssize() );
10+
assert<bounds_safety>( 0 <= start );
11+
assert<bounds_safety>( end <= rng.ssize() );
1212

1313
count := 0;
1414
for rng

regression-tests/mixed-lifetime-safety-and-null-contracts.cpp2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#include <string>
55

66
main: () -> int = {
7-
cpp2::Null.set_handler(call_my_framework&);
7+
cpp2::null_safety.set_handler(call_my_framework&);
88
try_pointer_stuff();
99
std::cout << "done\n";
1010
}

0 commit comments

Comments
 (0)