But this is what I meant about choosing to suit your project.
I’m sure as my code gets older and older I’ll end up with some assertions that are so slow/pedantic my only options are “let’s just delete this, it never fails” or “well let’s leave it on only in debug mode at least, we’re still usable even in that mode”.
I agree though that until you realise you have this requirement it’s probably best to go with the grain of “Debug/ReleaseSafe/ReleaseFast”
As far as I can tell, since Zig doesn’t have macros there is no way to avoid the inline if (dbg_mode), as any attempt to abstract this to a function will potentially evaluate the parameter.
While that works, it is far less readable than a simple if(dbg_mode) //check. So I wouldn’t recommend it.
It also only works when all the logic is in a function which returns the value you want to check, more complex checks would have to be wrapped in a function which is not very desirable.
Edit: passing the function and the arguments to the function as separate arguments is the key to why this does work. I missed that earlier.
Original reply:
That won’t work. The assumption is that the add function here is the expensive part, not the assert; exiting the assert early does not matter. Try adding a print to your add function and observe that it is always called.
(Technically the print statement forces add to always be called because it introduces a side effect, and the non side-effecting function could have been optimized away, but I think you should write your code in a way that it doesn’t matter whether or not the function has side effects if it is important that it is not included in certain build modes)
This is because there are no macros in Zig, as the OP said. The std.debug.assert function call being optimized away does not mean that functions called as arguments are optimized away.
No it doesn’t print it through my_assert. That would be very weird. Please look at the slighty reformatted code.
const std = @import("std");
const dbg_mode = @import("builtin").mode == .Debug or @import("builtin").mode == .ReleaseSafe;
// Same as std.debug.assert
fn assert(ok: bool) void {
if (!ok) unreachable; // assertion failure
}
fn my_assert(check_fn: anytype, args: anytype, check: anytype) void {
if (dbg_mode) {
std.debug.print("run my_assert\n", .{});
if (!(@call(.auto, check_fn, args) == check)) unreachable;
// It would be very weird if the above would be called, no?
}
}
pub fn main() !void {
std.debug.print("dbg_mode: {}\n", .{dbg_mode});
assert(add(1, 2) == 3); // Comment this line and run again
my_assert(add, .{ 1, 2 }, 3);
}
fn add(a: u64, b: u64) u64 {
std.debug.print("in add\n", .{});
return a + b;
}
I get these outputs:
// zig run main.zig
dbg_mode: true
in add
run my_assert
in add
// zig run main.zig -O ReleaseFast
dbg_mode: false
in add
// Now comment out the `assert(add(1, 2) == 3)` line
// zig run main.zig
dbg_mode: true
run my_assert
in add
// zig run main.zig -O ReleaseFast
dbg_mode: false
The problem with approaches like these is that they only really work for the trivial case, you’re only kicking the can down the road. What if you want to do my_assert(add, .{ getValue(), getOtherValue() }, getExpected())? The functions will still be evaluated for side effects, so you’d need to add support for another level of indirection with my_assert(add, .{ .{ getValue, .{} }, .{ getOtherValue, .{} } }, .{ getExpected, .{} }) and at a certain point it looks like a huge mess and like you’ve reinvented Lisp but with curly braces
if (assertions_enabled) assert(sideEffects()) is the only way to eliminate evaluations that scales indefinitely. If you prefer a different style you can of course get a bit creative and try something like if (getAsserter()) |assert| assert(sideEffects()) but you can’t escape from the fact that the “assertions enabled/disabled” conditional control flow needs to occur on the caller side.