Skip to content

Commit

Permalink
moved to having just 2 assert macros from 3
Browse files Browse the repository at this point in the history
  • Loading branch information
nessan committed Jul 26, 2024
1 parent 79e19d1 commit 2c238a6
Show file tree
Hide file tree
Showing 18 changed files with 68 additions and 129 deletions.
4 changes: 1 addition & 3 deletions docs/content/_common.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ $$
:::

<!-- Our check/assert macros and compiler flags that control them -->
[`BIT_DEBUG`]: /content/bit_assert/index.qmd#compiler-flags
[`BIT_NDEBUG`]: /content/bit_assert/index.qmd#compiler-flags
[`bit_assert`]: /content/bit_assert/index.qmd
[`BIT_DEBUG`]: /content/bit_assert/index.qmd
[`bit_debug_assert`]: /content/bit_assert/index.qmd
[`bit_always_assert`]: /content/bit_assert/index.qmd

Expand Down
80 changes: 24 additions & 56 deletions docs/content/bit_assert/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,24 @@ title: Assertions

## Introduction

The `bit_assert.h` header has three replacements for the standard [`assert`] macro --- they all allow for an additional string output that you can use to print the values of the variables that triggered any failure.
The `bit_assert.h` header has two replacements for the standard [`assert`] macro --- they allow for an additional string output that you can use to print the values of the variables that triggered any failure.

```cpp
bit_assert(condition, message) // <1>
bit_debug_assert(condition, message) // <2>
bit_always_assert(condition, message) // <3>
bit_debug_assert(condition, message) // <1>
bit_always_assert(condition, message) // <2>
```
1. assertions of this type are verified **unless** you set the `BIT_NDEBUG` flag at compile time.
This version is closest in spirit to the standard [`assert`] macro.
1. assertions of this type are **only** verified if you set the `BIT_DEBUG` flag at compile time.
2. assertions of this type are **always** verified and cannot be turned off with a compiler flag.
1. Assertions of this type are **only** verified if you set the `BIT_DEBUG` flag at compile time.
2. Assertions of this type are **always** verified and cannot be turned off with a compiler flag.
Assuming the asserts are "on," in all cases, if `condition` evaluates to `false`, we print an error message to `std::cerr`, and the program will **exit**.
Assuming an assertion is "on," in all cases, if `condition` evaluates to `false`, we print an error message to `std::cerr`, and the program will **exit**.
The error message always includes the location of the failure and an extra dynamic payload typically used to print the values of the variables that triggered the failure.
The payload can be anything that can be formatted using the facilities in [`std::format`].
These look like functions but are macros, and the level of assertion checking performed is controlled by setting compiler flags at build time:
## Compiler Flags
Macro | Description
----------- | -----------
`BIT_DEBUG` | If set, we will perform demanding but potentially useful safety assertions on indices, size equality checks, etc.
`BIT_NDEBUG` | If set, we turn off even relatively innocuous safety assertions for maximum performance.
: {.bordered .hover .responsive tbl-colwidths="[30,70]"}
::: {.callout-note}
# Consistency is enforced
If you set the `BIT_NDEBUG` flag, the library will ensure that `BIT_DEBUG` is not set.
# Compiler Flag
If you set the `BIT_DEBUG` flag, the library will perform demanding but potentially useful safety assertions on indices, size equality checks, etc.
Otherwise all the `bit_debug_assert` calls are no-ops.
:::
::: {.callout-tip}
Expand All @@ -56,14 +44,14 @@ constexpr bit::vector &set(std::size_t i)
...
}
```
Here `m_size` is holds the size of the vector---so we must have `i < m_size`
Here `m_size` is holds the size of the vector --- so we must have `i < m_size`

To check every element access, set the `BIT_DEBUG` flag during compiles.
If the assertion fails, the program exits with an error message that gives the offending values.

The `bit_debug_assert` line expands to _nothing_ if the `BIT_DEBUG` flag is not set during compiles.
The `bit_debug_assert` line expands to *nothing* if the `BIT_DEBUG` flag is not set during compiles.

[Example---Message from an assertion failure]{.bt}
[Example --- Message from an assertion failure]{.bt}
```cpp
#include <bit/bit.h>
int main()
Expand All @@ -75,14 +63,14 @@ int main()
}
```
1. Construct a vector of size 12 and then attempt to set the "last" element.
2. A deliberate but typical _off-by-one_ index error as the valid indices are from 0 to `n-1`, which is 11.
2. A deliberate but typical *off-by-one* index error as the valid indices are from 0 to `n-1`, which is 11.

Compile the sample program with the `BIT_DEBUG` flag set and get:

[Output]{.bt}
```sh
BIT ASSERTION FAILURE:
Function 'set' (vector.h, line 893):
Function 'set' (vector.h, line 910):
Statement 'i < m_size' is NOT true: Index i = 12 must be < `m_size` = 12
```
The program will then exit.
Expand Down Expand Up @@ -110,53 +98,33 @@ However, enabling these sorts of checks may be very useful during development.
The `bit_debug_assert(...)` macro expands to nothing **unless** you set the `BIT_DEBUG` flag at compile time.
### `bit_assert`
### `bit_always_assert`
On the other hand, some checks are pretty cheap, especially when you compare the cost to the actual work done by the function.
The `bit_assert(...)` form is helpful for those cheaper verifications.
There may be other checks you never want to be turned off.
The `bit_always_assert(...)` form accomplishes those tasks --- it is unaffected by compiler flags.
For example, a pre-condition for a matrix inversion method is that the input matrix must be square.
Here is how you might do that check in an `invert(const Matrix& M)` function:
Here is how we do that check in an `invert(const Matrix& M)` function:
```cpp
bit_assert(M.is_square(), "Cannot invert a {} x {} NON-square matrix!", M.rows(), M.cols());
bit_always_assert(M.is_square(), "Cannot invert a {} x {} NON-square matrix!", M.rows(), M.cols());
```
We can only invert square matrices.
The `M.is_square()` call checks that condition and, on failure, throws an exception with a helpful message.

This particular check is always on by default, and the programmer needs to do something special (i.e., define the `BIT_NDEBUG` flag at compile time) to deactivate it.

The `bit_assert(...)` macro expands to nothing **only if** you set the `BIT_NDEBUG` flag at compile time --- the behavior is the same as the standard [`assert`] macro but allows for adding a formatted error message.
The `M.is_square()` call checks that condition and, on failure, exits the program with a helpful message.

### `bit_always_assert`

There may be checks you never want to be turned off.
The final form `bit_always_assert(...)` accomplishes those tasks --- it is unaffected by compiler flags.
Here the cost of the check is very slight compared to the work done by the `invert(...)` method, so leaving it on even in production code is not a problem.

For instance, in that last example, the check cost is very slight compared to the work done by the `invert(...)` method, so leaving it on even in production code is probably not a problem.
You might well want to use the `bit_always_assert(...)` version so the check never gets disabled:
```cpp
bit_always_assert(M.is_square(), "Cannot invert a {} x {} NON-square matrix!", M.rows(), M.cols());
```
The decision to use one of these forms vs. another depends on the cost of doing the check versus the work done by the method in question.
The decision to use one form vs. the other depends on the cost of doing the check versus the work done by the method in question.
A primary use case for `bit_debug_assert` is to do things like bounds checking on indices --- from experience, this is vital during development.
However, bounds-checking every index operation incurs a considerable performance penalty and can slow down numerical code by orders of magnitude.
So it makes sense to have the checks in place for development but to ensure they are never there in release builds.

In the development cycle, asserting range indices and so on is helpful.
However, those assertions are expensive and can slow down numerical code by orders of magnitude.
Therefore, we don't want there to be any chance that those verifications are accidentally left "on" in our production code.
The first form, `bit_debug_assert(...)`, covers these asserts.
The first form, `bit_debug_assert(...)`, covers these types of checks.
Turning on `bit_debug_assert` asserts requires the programmer to take a specific action, namely, setting the `BIT_DEBUG` flag during compile time.

On the other hand, some assertions are relatively cheap, especially compared to the work done by the containing function.
For example, a pre-condition for the [`matrix::invert`] method is that the input bit-matrix is square.
There is probably no harm if we always do that assert, which is very cheap compared to the typical cost of inverting a bit-matrix.
The second form, `bit_assert(...)`, is suitable for those cheaper verifications.
Turning off even those assertions is possible, but the programmer must take a specific action. She must set the `BIT_NDEBUG` flag during compile time.
Finally, you may wish that some assertions are always checked.
The final form above accomplishes those tasks.
The second form, `bit_always_assert(...)`, is suitable for those cheaper verifications.

::: {.callout-note}
# Macro-land
Expand Down
2 changes: 0 additions & 2 deletions docs/content/matrix/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,7 @@ Method | Description
Macro | Description
----- | -----------
[`BIT_DEBUG`] | This compile-time flag enables extra safety checks.
[`BIT_NDEBUG`] | This compile-time flag turns off most safety checks.
[`bit_debug_assert`] | These assertions are only checked if the `BIT_DEBUG` flag is set at compile time.
[`bit_assert`] | These assertions are checked unless the `BIT_NDEBUG` flag is set at compile time.
[`bit_always_assert`] | Use this form for checks that must always be performed.
: {.bordered .striped .hover .responsive tbl-colwidths="[35,65]"}

Expand Down
3 changes: 1 addition & 2 deletions docs/content/matrix/invert.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ Contrast that to matrices over the reals where, mathematically at least, matrice
::: {.callout-warning}
# The bit-matrix argument must be square
The input matrix must be square, and the [`bit_assert`] macro checks that pre-condition.
Setting the `BIT_NDEBUG` flag at compile time turns off that check.
The input matrix must be square, and the [`bit_always_assert`] macro checks that pre-condition.
:::
[Example]{.bt}
Expand Down
3 changes: 1 addition & 2 deletions docs/content/matrix/pow.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ It is also worth noting that all arithmetic in $\FF$ is mod 2, so there are no o
::: {.callout-warning}
# The bit-matrix argument must be square
The input matrix must be square, and the [`bit_assert`] macro checks that pre-condition.
Setting the `BIT_NDEBUG` flag at compile time turns off that check.
The input matrix must be square, and the [`bit_always_assert`] macro checks that pre-condition.
:::
[Example]{.bt}
Expand Down
13 changes: 3 additions & 10 deletions docs/content/notes/design.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -218,14 +218,7 @@ Our assertions come with the ability to print an explanation of what caused any

The most commonly used form in the library is `bit_debug_assert(...)`.
This form expands to nothing *unless* the programmer sets the `BIT_DEBUG` flag at compile time.
That is typically done automatically _only_ for debug software builds and is _never_ done for release/optimized builds.
That is typically done automatically *only* for debug software builds and is *never* done for release/optimized builds.

There are a couple of other versions of our assert macros.

The `bit_assert(...)` macro is always on unless the programmer sets the `BIT_NDEBUG` flag at compile time.
Typically, we employ this form if the assertion check is relatively cheap compared to the work done in the function call.
For example, the bit-matrix inversion function uses this form of assertion to check that the input bit-matrix is square.
That check cost is negligible compared to the typical cost of inverting a bit-matrix, but to extract every ounce of efficiency, you can set the `BIT_NDEBUG` flag during compile time.

Finally, some checks must always be performed.
The `bit_always_assert(...)` form handles those cases.
There is also a version `bit_always_assert(...)` for checks that should always be carried out no matter what flags are passed to the compiler.
Typically these are for assertions where the cost of the check is cheap compared to the cost of the work done in the method.
3 changes: 1 addition & 2 deletions docs/content/polynomial/evaluation.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ The sum uses [Horner's method](https://en.wikipedia.org/wiki/Horner%27s_method).

::: {.callout-warning}
# The bit-matrix argument must be square
The input matrix must be square, and the [`bit_assert`] macro checks that pre-condition.
Setting the `BIT_NDEBUG` flag at compile time turns off that check.
The input matrix must be square, and the [`bit_always_assert`] macro checks that pre-condition.
:::

[Example]{.bt}
Expand Down
6 changes: 2 additions & 4 deletions docs/content/polynomial/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,13 @@ Method | Description

### Debugging

You can set some compile-time flags that enable extra safety checks.
You might set these flags in `DEBUG` builds where performance is not the critical concern.
You can set a compile-time flag to enable extra safety checks.
These checks can have a severe performance penalty so typically are only turned on for development.

Macro | Description
----- | -----------
[`BIT_DEBUG`] | This compile-time flag enables extra safety checks.
[`BIT_NDEBUG`] | This compile-time flag turns off most safety checks.
[`bit_debug_assert`] | These assertions are **only** checked if you set the `BIT_DEBUG` flag at compile time.
[`bit_assert`] | These assertions are checked **unless** you set the `BIT_NDEBUG` flag at compile time.
[`bit_always_assert`] | These assertions are always checked.
: {.bordered .striped .hover .responsive tbl-colwidths="[35,65]"}

Expand Down
6 changes: 2 additions & 4 deletions docs/content/vector/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -224,15 +224,13 @@ Method | Description

### Debugging

You can set some compile-time flags that enable range checking and so on.
You might set these flags in `DEBUG` builds where performance is not the critical concern.
You can set a compile-time flag [`BIT_DEBUG`] to enable range checking and other assertions.
These checks can have a substantial performance impact so typically are only used during development.

Macro | Description
----- | -----------
[`BIT_DEBUG`] | This compile-time flag enables extra safety checks.
[`BIT_NDEBUG`] | This compile-time flag turns off most safety checks.
[`bit_debug_assert`] | These assertions are **only** checked if you set the `BIT_DEBUG` flag at compile time.
[`bit_assert`] | These assertions are checked **unless** you set the `BIT_NDEBUG` flag at compile time.
[`bit_always_assert`] | These assertions are always checked.
: {.bordered .striped .hover .responsive tbl-colwidths="[30,70]"}

Expand Down
2 changes: 1 addition & 1 deletion examples/pow01.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ template<std::unsigned_integral Block, typename Allocator>
constexpr matrix<Block, Allocator>
old(const matrix<Block, Allocator>& M, std::size_t n)
{
bit_assert(M.is_square(), "Matrix is {} x {} but it should be square!", M.rows(), M.cols());
bit_always_assert(M.is_square(), "Matrix is {} x {} but it should be square!", M.rows(), M.cols());

// Note the M^0 is the identity matrix so we start with that
matrix<Block, Allocator> retval = matrix<Block, Allocator>::identity(M.rows());
Expand Down
20 changes: 9 additions & 11 deletions examples/scratch01.cpp
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@


#ifndef BIT_DEBUG
#define BIT_DEBUG
#endif
#include <bit/bit.h>
int main()
{
// lambda: Turns the degree of a polynomial into a string.
auto deg = [](auto& p) { return p.degree() == bit::polynomial<>::ndeg ? "NONE" : std::format("{}", p.degree()); };

auto p0 = bit::polynomial<>::random(0);
std::cout << std::format("p0(x) = {} has degree: {}.\n", p0, deg(p0));

auto p1 = bit::polynomial<>::random(7);
std::cout << std::format("p0(x) = {} has degree: {}.\n", p1, deg(p1));

auto p2 = bit::polynomial<>::random(7, 0.9);
std::cout << std::format("p0(x) = {} has degree: {}.\n", p2, deg(p2));
std::size_t n = 12; // <1>
bit::vector<> v(n);
v.set(n); // <2>
std::cout << v << "\n";
}
17 changes: 3 additions & 14 deletions include/bit/bit_assert.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// @brief Three replacements for the standard `assert(condition)` macro that add an informational message.
/// @brief Two replacements for the standard `assert(condition)` macro that add an informational message.
/// @link https://nessan.github.io/bit
/// SPDX-FileCopyrightText: 2024 Nessan Fitzmaurice <[email protected]>
/// SPDX-License-Identifier: MIT
Expand All @@ -9,32 +9,21 @@
#include <iostream>
#include <string>

/// @brief Exit using the bit::exit(...) method automatically adding location information to the payload.
/// @brief This is called if an assertion fails -- exits the program using the @c bit::exit(...) method.
/// @note This is a macro that automatically adds the needed location information to the payload.
#define bit_assertion_failed(...) bit::exit(__func__, __FILE__, __LINE__, std::format(__VA_ARGS__))

/// @def The `bit_always_assert` macro cannot be switched off with compiler flags.
#define bit_always_assert(cond, ...) \
if (!(cond)) bit_assertion_failed("Statement '{}' is NOT true: {}\n", #cond, std::format(__VA_ARGS__))

// If BIT_NDEBUG is set then BIT_DEBUG should *not* be set.
#if defined(BIT_NDEBUG)
#undef BIT_DEBUG
#endif

/// @def The `bit_debug_assert` macro expands to a no-op *unless* the `BIT_DEBUG` flag is set.
#ifdef BIT_DEBUG
#define bit_debug_assert(cond, ...) bit_always_assert(cond, __VA_ARGS__)
#else
#define bit_debug_assert(cond, ...) void(0)
#endif

/// @def The `bit_assert`macro expands to a no-op *only if* the `BIT_NDEBUG` flag is set.
#ifdef BIT_NDEBUG
#define bit_assert(cond, ...) void(0)
#else
#define bit_assert(cond, ...) bit_always_assert(cond, __VA_ARGS__)
#endif

namespace bit {

/// @brief Given a path like `/home/jj/dev/project/src/foo.cpp` this returns its basename `foo.cpp`
Expand Down
8 changes: 5 additions & 3 deletions include/bit/danilevsky.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ characteristic_polynomial(const matrix<Block, Allocator>& A)
// Matrix needs to be non-empty and square.
bit_always_assert(A.is_square(), "Matrix is {} x {} but it needs to be square!", A.rows(), A.cols());

// Get the Frobenius form of A as a vector of companion matrices (matrix gets destroyed doing that so use a copy).
// Make working copy of A.
matrix<Block, Allocator> A_copy{A};
auto companion_matrices = compact_frobenius_form(A_copy);

// Get the Frobenius form of A as a vector of companion matrices (matrix gets destroyed doing that so use a copy).
auto companion_matrices = compact_frobenius_form(A_copy);
bit_always_assert(companion_matrices.size() > 0, "Something went wrong--the Frobenius form of A is empty!");

auto retval = companion_matrix_characteristic_polynomial(companion_matrices[0]);
Expand Down Expand Up @@ -97,7 +99,7 @@ danilevsky(matrix<Block, Allocator>& A, std::size_t n = 0)
if (n == 0) n = N;

// If we were asked to look at a specific sub-matrix it better fit.
bit_always_assert(n <= N, "Asked to look at {} rows but matrix has only {} of them!", n, N);
bit_debug_assert(n <= N, "Asked to look at {} rows but matrix has only {} of them!", n, N);

// Handle an edge case.
if (n == 1) {
Expand Down
4 changes: 2 additions & 2 deletions include/bit/gauss.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class gauss {
/// If there are free variables this will be a random choice over any of the possible 2^f variants.
vector_type operator()() const
{
// If we are asked for a solution and there are none then we throw an exception
// If we are asked for a solution and there are none then barf.
bit_always_assert(is_consistent(), "System is inconsistent so has NO solutions");

// Default all the elements of x to random values.
Expand All @@ -117,7 +117,7 @@ class gauss {
/// @note The numbering of the solutions is certainly not unique
vector_type operator()(std::size_t ns) const
{
// If we are asked for a solution and there are none then we throw an exception
// If we are asked for a non-existent solution we barf.
bit_always_assert(ns < m_count, "Argument ns = {} is not less than solution count = {}!", ns, m_count);

// Our solution will have the free variables set bases on the bit-pattern in ns.
Expand Down
Loading

0 comments on commit 2c238a6

Please sign in to comment.