Assert but only in paranoid mode

I am testing some routines which I need to assert, but I only want this if the program (debug compile) is in ‘paranoid’ mode.
These paranoid assertions should only be executed when my global const paranoid is true.

How can I best write a routine paranoid_assert() of which I am sure it is totally ignored in release mode?
Normal asserts are ignored in releasemode.

(this is kinda needed because the paranoid checks in some cases are slowing down very much).

EDIT: I don’t think this actually works

Summary

Something like:

fn paranoid_assert(ok: bool) void {
    if (paranoid and !ok) unreachable
}

the simplest way is to import builtin and to check if your program is being built in debug mode, and only perform the check in that case. for more granular control you can add a build-time setting (see Zig Build System ⚡ Zig Programming Language) but the gist is the same.

const builtin = @import("builtin");

fn debugAssert(ok: bool) void {
  if (builtin.mode != .Debug) return;
  if (!ok) unreachable;
}
2 Likes

In build.zig:

const paranoid = b.option(bool, "paranoid", "Whether to enable paranoid assertions") orelse false;
const options = b.addOptions();
options.addOption(bool, "paranoid", paranoid);
const exe = b.addExecutable(
  // Add the usual stuff
  .imports = &.{ .{ .name = "paranoid", .module = option.createModule() },
);

In your program:

const paranoid = @import("paranoid").paranoid;

// Whenever you need to assert something in paranoid mode:
if(comptime paranoid) std.debug.assert(assertion);

You can’t simple pass the assertion into the function, because the work to compute the parameter would have already been done, so you need to wrap it in a conditional at the call site.

6 Likes

You can’t simple pass the assertion into the function, because the work to compute the parameter would have already been done, so you need to wrap it in a conditional at the call site.

Hmm that’s a good point i didn’t think about. I was kind thinking that if the condition was dependent on a comptime known value it would be removed.

Yes good point, sometimes you will have to “hoist” the setting check.

But with inline you also could get the if to be inserted at the call site.

1 Like

The computation needs to be inside the escope of the if clause.

Consider this:

inline fn paranoidAssert(ok: bool) void{
  if(comptime paranoid) std.debug.assert(ok);
}

// Call site
paranoidAssert(expensiveComputation());

After inlining, it would become:

const ok = expensiveComputation();
if(comptime paranoid) std.debug.assert(ok);

Even though ok is a dead value, in debug builds dead values are not eliminated, so you’d pay the cost the same way.

1 Like

Maybe it is easiest to do this?

fn somewhere_in_my_program() void {
    assert(my_expensive_check());
}

fn my_expensive_check() bool {
   if (!globals.paranoid) return true;
   // do my checks
}

Yes, that would work as well, but only for expensiveCheck. Regardless of whichever method you choose, for every check that you want to make conditional, you need a way to divert control flow before reaching the computation. You can put it in a function or do an inline block of code. What you can’t do is have a function that takes an arbitrary statement as a parameter and conditionally executes it. This goes agaisn’t the principal of “no hidden control flow”. Consider what would happen if such a function existed:

paranoidAssert(functionWithSideEffects());

Without knowing what paranoidAssert is doing internally, one would expect the side effects to happen. It would be very surprising and hidden if it could make the entire call to functionWithSideEffects disappear.

Taking a page out of asynchronous programming, what you could do is pass a function object:

/// `assertion` must have a method `call`
/// that returns a bool, and will be called
/// if paranoid assertions are enabled.
fn paranoidAssert(assertion: anytype) void{
  if(comptime paranoid) 
    std.debug.assert(assertion.call());
}

// Call site
paranoidAssertion(struct{
  // arguments
  arg1: u8,

  pub fn call(self: @This()) bool{
    //Do the checks
  }
}{ .arg1 = 1});

But it’s a bit clumsy. Better to just do if(comptime paranoid) thing.

if (paranoid) assert(expensiveCheck())

This fine and probably the most concise way of eliding the expensive checks when assertions are disabled. However, you could also consider something like the following, where the assertion function is an optional that you have to unwrap:

const paranoid = false; // from build options

const assertParanoid: ?fn (ok: bool) void = if (paranoid)
    struct {
        fn assert(ok: bool) void {
            std.debug.assert(ok);
        }
    }.assert
else
    null;

pub fn main() void {
    if (assertParanoid) |assert| assert(expensiveCheck());
}

fn expensiveCheck() bool {
    std.debug.print("checked!\n", .{});
    return true;
}

This is slightly more verbose, but leaves less room for programmer mistakes since the captured assert is guaranteed to only be available in that scope.

If you want to make certain that this is a comptime operation (which it should be anyway) you can just say

if (comptime paranoid)
    assert(expensiveCheck());

That will get you a compile error if paranoid isn’t comptime-known for some reason.

ah yes. that is clearer and better readable. no hidden things.

1 Like