Declare const or var depending on comptime const

I need ideally a (global) const to be var depending on a comptime boolean.
Is that possible in 1 declaration?

pub const we_can_change: bool = false;

pub const value: i32 = 42; // if we_can_change is false
pub var value: i32 = 42; // if we_can_change is true

The one way I can think of right now would be something like

const std = @import("std");

// Compile error, "cannot assign to constant"
// const we_can_change = false;

// Runs just fine!
const we_can_change = true;

pub const wrapper = if (we_can_change) struct {
    pub var value: i32 = 42;
} else struct {
    pub const value: i32 = 42;
};

// Skipping the wrapper by using a pointer
// is another option
// pub const value = &wrapper.value;

pub fn main() !void {
    wrapper.value = 12;

    std.debug.print("{d}\n", .{wrapper.value});
}

Edit: This of course also works if you define wrapper inside main instead, though that would probably defeat the purpose.

Edit 2: A more direct way to go the pointer route, the sacrifice being a second declaration:

var backing: i32 = 42;
pub const value: if (we_can_change) *i32 else *const i32 = &backing;
4 Likes

good ideas. fooling around with it now.

But if we go the pointer way, we need allocations right?
Edit: in my case it is more complicated. mutable type = 4 fields in a struct, unmutable type is a hard constant.

@Fitti’s code works verbatim, you can point to global memory.

1 Like

I came up with the following… This is about what i need.

pub const spsa: bool = false; // or true :)

pub const current_terms = if (spsa) &tunable_terms else &default_terms;
pub const default_terms: Terms = .{};
pub var tunable_terms: Terms = if (spsa) default_terms else void;

pub const Terms = struct {
    reversed_futility_pruning: _reversed_futility_pruning = .{},

    pub const _reversed_futility_pruning = struct {
        max_depth: Tunable(i32) = tunable(i32, 6, 2, 8, 1),
        base_margin: Tunable(i32) =  tunable(i32, 21, 10, 60, 10),
        improving_margin: Tunable(i32) =  tunable(i32, 40, 20, 100, 10),
        not_improving_margin: Tunable(i32) = tunable(i32, 74, 20, 120, 10),
    };
};

inline fn tunable(comptime T: type, comptime value: T, min: T,  max: T, step: T) Tunable(T) {
    return switch (spsa) {
        false => Tunable(T).init(value),
        true => Tunable(T).init(value, min, max, step),
    };
}

pub fn Tunable(comptime T: type) type {
    return switch (spsa) {
        false => ConstantEntry(T),
        true => TunableEntry(T),
    };
}

pub fn ConstantEntry(comptime T: type) type {
   return struct {
        const Self = @This();

        v: T,

        inline fn init(value: T) Self {
            return .{ .v = value };
        }
   };
}

pub fn TunableEntry(comptime T: type) type {
    return struct {
        const Self = @This();

        v: T,
        min: T,
        max: T,
        step: T,

        inline fn init(value: T, min: T, max: T, step: T) Self {
            return .{ . v = value, .min = min, .max = max, .step = step };
        }
    };
}

main

    std.debug.print("tunable size {}\n\n", .{ @sizeOf(Tunable(i32)) } );
    std.debug.print("before \n{}\n", . { current_terms.reversed_futility_pruning });
    current_terms.reversed_futility_pruning.max_depth.v = 424242; // does not compile when spsa == false
    std.debug.print("after \n{}\n", . { current_terms.reversed_futility_pruning });

You could leverage Zig’s lazy analysis and also do something like this, if you want to make sure to never use the one that shouldn’t be used.

pub const current_terms = if (spsa) &tunable_terms else &default_terms;
pub const default_terms: Terms = if (!spsa) .{} else @compileError("Using tunable terms");
pub var tunable_terms: Terms = if (spsa) .{} else @compileError("Using default terms");

This could save you from bugs introduced by typos. Alternatively think about whether the bottom two should really be pub.

2 Likes

True. thank you to point that out.,
Still i think it is a bit of a mess :slight_smile: Don’t like it.

From what I understand, TunableEntry and ConstantEntry never coexist, so you do not need to have two distinct types, but instead have a single Entry struct that is adapted according to spsa. This way you get rid of some types:

pub const spsa: bool = true; // or true :)

pub const current_terms = if (spsa) &tunable_terms else &default_terms;
const default_terms: Terms = if (!spsa) .{} else @compileError("Using tunable terms");
var tunable_terms: Terms = if (spsa) .{} else @compileError("Using default terms");

pub const Terms = struct {
    reversed_futility_pruning: _reversed_futility_pruning = .{},

    pub const _reversed_futility_pruning = struct {
        max_depth: Entry(i32) = init(i32, 6, 2, 8, 1),
        base_margin: Entry(i32) = init(i32, 21, 10, 60, 10),
        improving_margin: Entry(i32) = init(i32, 40, 20, 100, 10),
        not_improving_margin: Entry(i32) = init(i32, 74, 20, 120, 10),
    };
};

pub fn Entry(T: type) type {
    comptime if (spsa) {
        return struct {
            v: T,
            min: T,
            max: T,
            step: T,
        };
    } else {
        return struct {
            v: T,
        };
    };
}

pub fn init(T: type, value: T, min: T, max: T, step: T) Entry(T) {
    comptime if (spsa) {
        return .{ .v = value, .min = min, .max = max, .step = step };
    } else {
        return .{ .v = value };
    };
}

BTW @pzittlau’s suggestion already helped me, because I was printing tunable_terms instead of current_terms to the console when testing this code :+1:

2 Likes

Oho. that is much better!

Currently I have just global constants defined, sub-structured only for readability btw:

pub const terms = struct {
    pub const reversed_futility_pruning = struct {
        pub const max_depth: i32 = 6;
        pub const base_margin: i32 = 21;
        pub const improving_margin: i32 = 40;
        pub const not_improving_margin: i32 = 74;
    };
};

Now the actual Search code using this stuff:

const rfp = terms.reversed_futility_pruning; // alias for shortness
if (
   depth <= rfp.max_depth and 
   node.eval < mate_threshold and 
   node.eval - rfp.base_margin >= beta
) {
    // do smart things here.
}

When defining spsa = false do we lose any speed accessing the entries? Or is the ‘constness’ kinda guaranteed?

Two inconveniences left are:

  1. I have to access the v field instead of a direct value.

  2. And the definition itself

max_depth: Entry(i32) = init(i32, 6, 2, 8, 1),

I could contemplate

pub var max_depth = init(i32, 6, 2, 8, 1),

But then we lose the constness…

Edit: another “inconvenience” is to declare the structs as types and as fields.

The ideal declaration would still be const. I may have hundreds of these values and i want them declared at one place…
Is it somehow possible to stuff things @comptime in a memory buffer?

pub const x = if (spsa) just_a_struct else create_in_buffer_and_return_pointer();

I think what you might want to do is simply


pub const Range = struct {
    min: i32,
    max: i32,
    step: i32,
};

pub const terms = struct {
    pub const reversed_futility_pruning = struct {
        pub const max_depth: i32 = 6;
        pub var max_depth_tuning: Range = .{2, 8, 1},
        pub const base_margin: i32 = 21;
        pub var base_margin_tuning: Range = .{10, 60, 10}, 
        ...
    };
};

The values are kept constant, no extra .v field access, and no comptime magic necessary :slight_smile:
As namespace variables are lazy evaluated the tuning variables are probably stripped by the compiler when not used, but I am not sure about that.

Not really. It would also double the number of variables.
The ideal situation is one declaration for a const or a var.
where the const is just one value and the var 4 values.

(edit: and the only way to have a pub const be variable is a pointer)

Ah ok sorry, then I did not fully understand your messages, the “still const” part is confusing to me.

You can initialize current_terms with default_terms directly instead of taking a pointer:

pub const current_terms = if (spsa) &tunable_terms else default_terms;

Yes… It is possible doing that for sure.
My main point is to not write any duplicate code, because the list of used features is quite long.

Ok. This is reasonably nice. Using anonymous structs and std.mem.zeroInit for defaults. And one default declaration.
I uploaded it to my “temporary stuff” on github.
here

Now final question: when using one of the structs inside the Terms (just for shorter code at the callsite)
should i do this:

const rfp = terms.reversed_futility_pruning;
// read rfp fields

or this:

const rfp = &terms.reversed_futility_pruning;
// read rfp fields

performance-wise?

Interesting question! Putting the example code in compiler explorer shows, that the compiler can determine you are referencing constant memory in both cases. E.g. using some value in a comparison, it directly plugs in the value into the compare instruction. So in this specific case it does not matter.

1 Like