Errors · Lesson 9.16

Assert

Source
Check what the program relies on with Assert, kept in every build, and DebugAssert, which a release build removes.
You'll need: Panic, Slice

An assertion states something the program relies on, and checks it while the program runs. It is a panic with a condition attached: when the condition holds, nothing happens; when it does not, the program stops.

Assertions are for the same kind of problem as Panic — a bug, not bad input. They make an assumption visible in the code, and make sure that if it is ever wrong, the program says so at once instead of carrying on with nonsense.

Assert

The median of a list is its middle score, and an empty list has no middle:

Assert(scores.length > 0, "a median needs at least one score");

Call Median with an empty slice and the program stops exactly like a Panic, reporting the message and where the assertion is:

Assertion failed: a median needs at least one score
  at Median (Src/Main.rux:43:5)

Write the message as what was expected, since the failing value is not printed. "a median needs at least one score" tells the reader the rule that was broken; "empty list" would only describe the symptom.

DebugAssert

Core has two assertions, and the difference is the build:

DebugAssert(InOrder(scores), "the scores are sorted");
AssertDebugAssert
Debug build (rux run)checkedchecked
Release build (rux run --release)checkedremoved — the condition is not evaluated
Use it fora promise that must hold in the program you shipa check worth making while developing, too slow to pay for when shipped

InOrder is a whole pass over the scores. That is worth doing while developing, but it would turn a cheap lookup into a slow one on every call in a shipped program — so it sits in a DebugAssert.

Removed means not evaluated

InOrder prints a line so you can see the difference. Run the program both ways and the line appears only in the debug build — in a release build the call never happens at all.

That has two consequences. An expensive check costs nothing when shipped. And a condition with a side effect loses that effect: anything a DebugAssert's condition does is gone from the release build.

It also means a release build no longer catches the mistake the check was there for. Give Median the unsorted scores [3, 8, 5, 13, 21], and the debug build stops with Assertion failed: the scores are sorted, while the release build quietly prints median: 5.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// An assertion states something the program relies on, and checks it while
// the program runs:
//
//     Assert(scores.length > 0, "a median needs at least one score");
//
// When the condition holds, nothing happens. When it does not, the program
// stops exactly like a `Panic`, reporting the message and where it was:
//
//     Assertion failed: a median needs at least one score
//       at Median (Src/Main.rux:43:5)
//
// Write the message as what was expected, since the failing value is not
// printed.
//
// `Core` has two of them, and the difference is the build:
//
// - `Assert` is checked in every build. Use it for a promise that must hold
//   in the program you ship.
// - `DebugAssert` is checked in a debug build (`rux run`) and removed from a
//   release build (`rux run --release`). Removed means its condition is not
//   even evaluated, so an expensive check costs nothing when shipped — and a
//   condition with a side effect loses that effect.
//
// `InOrder` below prints a line so you can see that difference: run the
// program both ways and the line appears only in the debug build.
import Core::{ Assert, DebugAssert };
import Io::PrintLine;

// A whole pass over the scores: worth checking while developing, too slow to
// pay for on every call in a shipped program.
func InOrder(scores: int[..]) -> bool {
    PrintLine("(checking that the scores are in order)");
    for i in 1..scores.length {
        if scores[i - 1] > scores[i] {
            return false;
        }
    }
    return true;
}

// The middle score of a sorted list.
func Median(scores: int[..]) -> int {
    Assert(scores.length > 0, "a median needs at least one score");
    DebugAssert(InOrder(scores), "the scores are sorted");
    return scores[scores.length / 2];
}

func Main() -> int {
    let scores = [3, 5, 8, 13, 21];
    PrintLine("median: {}", Median(scores));
    return 0;
}

Besides Io, its Rux.toml lists Core under [Dependencies].

Run it

cd Examples/Errors/Assert
rux run
(checking that the scores are in order)
median: 8

A release build drops the DebugAssert, so its check never runs:

rux run --release
median: 8

Common mistakes

Work inside a DebugAssert.
A condition such as DebugAssert(Load(settings), "...") does its loading in a debug build and not at all in a release build. Do the work first, store the result, and assert on the stored value.
A condition that is not a bool.
Assert(scores.length, "...") is error: argument 1 to 'Assert' has type 'uint64', but parameter 'condition' requires 'bool8'. Write the comparison out: scores.length > 0.
Asserting on user input.
Like Panic, an assertion is for bugs. Input that can be wrong in a working program deserves a fallible and a helpful message, not a stopped program.

Try it yourself

  1. Call Median(scores[0..0]) — an empty slice of the scores — and read the assertion message.
  2. Change the scores to [3, 8, 5, 13, 21] and run with rux run and rux run --release. Compare the two.
  3. Add an Assert that every score is between 0 and 100, and decide whether it should be an Assert or a DebugAssert.

Learn more

  • Assert and #build in the API reference
  • Build mode — debug and release builds, and how a program can tell them apart
  • rux run — the --release flag
  • Panic — the same stop, without a condition