Comptime code block selection

I have some code like this:

    if (comptime builtin.mode == .Debug or builtin.mode == .ReleaseSafe) {
        var gpa = std.heap.GeneralPurposeAllocator(.{}){};
        defer std.debug.assert(gpa.deinit() == .ok);
        const alloca = gpa.allocator();
    } else {
        var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
        defer arena.deinit();
        const alloca = arena.allocator();
    }

and then later I want to use alloca for ArrayLists, etc. Obviously this doesn’t work for a few reasons, mostly related to scoping, I think? const alloca is reported as an unused local constant in both branches of the if, and alloca is reported as undeclared at all use sites. All I want is for conditionally including the blocks of code in the comptime if… Any ideas on how to go about this? I looked around at similar topics and couldn’t find anything close enough to spark enlightenment, but maybe I’m missing something… Thanks!

This should do the job:

    const safe = builtin.mode == .Debug or builtin.mode == .ReleaseSafe;
    var base_allocator = if (safe) {
        std.heap.GeneralPurposeAllocator(.{}){};
    } else {
        std.heap.ArenaAllocator.init(std.heap.page_allocator);
    };
    defer if (safe) {
        std.debug.assert(base_allocator.deinit() == .ok);
    } else {
        base_allocator.deinit();
    };
    const allocator = base_allocator.allocator();
3 Likes

That indeed does do the job; thanks! It does, however require splitting the comptime test in two. In this instance, that’s no problem at all, and I’ll implement something like this and move on with my day. However, in general is there some way to conditionally dump a block of code into a scope? If not, I guess that’s a restriction of comptime that I can work around (there was a blog post by Mitchell Hashimoto that seems relevant here) but it sure would be nice sometimes…

Here are two ways you could wrap the logic in a struct:

const std = @import("std");
const builtin = @import("builtin");

const AllocatorWrapper = struct {
    const safe = builtin.mode == .Debug or builtin.mode == .ReleaseSafe;

    const GPA = if (safe) std.heap.GeneralPurposeAllocator(.{}) else void;
    const Arena = if (safe) void else std.heap.ArenaAllocator;
    gpa: GPA,
    arena: Arena,

    pub fn init() AllocatorWrapper {
        return .{
            .gpa = if (safe) .{} else {},
            .arena = if (safe) {} else Arena.init(std.heap.page_allocator),
        };
    }
    pub fn deinit(self: *AllocatorWrapper) void {
        if (safe) std.debug.assert(self.gpa.deinit() == .ok) else self.arena.deinit();
    }

    pub fn allocator(self: *AllocatorWrapper) @TypeOf(if (safe) self.gpa.allocator() else self.arena.allocator()) {
        return if (safe) self.gpa.allocator() else self.arena.allocator();
    }
};

const AllocatorWrapper2 = struct {
    const safe = builtin.mode == .Debug or builtin.mode == .ReleaseSafe;

    const Allocator = if (safe) std.heap.GeneralPurposeAllocator(.{}) else std.heap.ArenaAllocator;
    wrapped: Allocator,

    pub fn init() AllocatorWrapper2 {
        return .{
            .wrapped = if (safe) .{} else Allocator.init(std.heap.page_allocator),
        };
    }
    pub fn deinit(self: *AllocatorWrapper2) void {
        if (safe) std.debug.assert(self.wrapped.deinit() == .ok) else self.wrapped.deinit();
    }

    pub fn allocator(self: *AllocatorWrapper2) @TypeOf(self.wrapped.allocator()) {
        return self.wrapped.allocator();
    }
};

pub fn main() !void {
    var wrapper = AllocatorWrapper.init();
    defer wrapper.deinit();

    const allocator = wrapper.allocator();

    const numbers = try allocator.alloc(u32, 5);
    defer allocator.free(numbers);

    for (numbers, 10..) |*n, i| n.* = @intCast(i);

    std.debug.print("{any} {s}\n", .{ numbers, @tagName(builtin.mode) });
}

It is a bit more wordy but you could reuse this wrapper and it makes the main function more clutter free, I think I prefer the first wrapper.

1 Like

Those are nice wrappers, and I may use one or the other if I find I need this sort of thing a lot. In particular it does clean up my main a bit. However, in your first wrapper you now have six(!) instances of if (safe) and I really am wondering if it could just be one.

1 Like

Good that you said this, I forgot that I had actually another idea for wrapping it and this is the best way:

const GPAWrapper = struct {
    const GPA = std.heap.GeneralPurposeAllocator(.{});
    gpa: GPA,

    pub fn init() GPAWrapper {
        return .{ .gpa = .{} };
    }
    pub fn deinit(self: *GPAWrapper) void {
        std.debug.assert(self.gpa.deinit() == .ok);
    }

    pub fn allocator(self: *GPAWrapper) @TypeOf(self.gpa.allocator()) {
        return self.gpa.allocator();
    }
};

const ArenaWrapper = struct {
    const Arena = std.heap.ArenaAllocator;
    arena: Arena,

    pub fn init() ArenaWrapper {
        return .{
            .arena = Arena.init(std.heap.page_allocator),
        };
    }
    pub fn deinit(self: *ArenaWrapper) void {
        self.arena.deinit();
    }

    pub fn allocator(self: *ArenaWrapper) @TypeOf(self.arena.allocator()) {
        return self.arena.allocator();
    }
};

pub fn main() !void {
    const safe = builtin.mode == .Debug or builtin.mode == .ReleaseSafe;
    const Wrapper = if (safe) GPAWrapper else ArenaWrapper;
    var wrapper = Wrapper.init();
    defer wrapper.deinit();

    const allocator = wrapper.allocator();
    ...
}

So I think in Zig you can use one if to switch between two different implementations of a type or namespace which both have the same methods or declarations, basically using static duck typing.

With the example you provided the main issue is that the allocator implementation needs to be placed in memory that isn’t temporary, I don’t think there is any way to use a single block for that.

Also I don’t know how that would work with newly introduced identifiers, I think that would be somewhat similar to the old implementation of usingnamespace where the introduced declarations where accessible locally and that was removed so that it is clearer where identifiers are introduced.

this solution probably isn’t smart enough to meet your use case, but sometimes i’ll do something like

const value_to_later_assert = if (builtin.mode == .Debug) createTheValue();
defer if (builtin.mode == .Debug) std.debug.assert(value_to_later_assert == otherValue());

or something like this

// allocation
var gpa: if (builtin.mode == .Debug) std.heap.GeneralPurposeAllocator(.{
    .stack_trace_frames = 20,
}) else void = if (builtin.mode == .Debug) .{};
defer if (builtin.mode == .Debug) _ = gpa.deinit();
const allocator = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator;

std.debug.assert

In ReleaseFast and ReleaseSmall modes, calls to this function are optimized away, and in fact the optimizer is able to use the assertion in its heuristics.

I don’t understand why you avoid the assert in ReleaseSafe, when you execute asserts (which you should because it is a safe mode), the manual test for the build mode is unnecessary, because it is already part of what assert does.

Also assert only takes one parameter, what is going on here?

the point is that the call to createTheValue() is not free. in my particular example it calls into a C library. it’s not particularly expensive either, i’ll own, but no point in even making the call if i don’t otherwise need the value

1 Like