View compile time values in a debugger

I’m getting my feet wet in zig with a toy game project. As is it very early, I end up with lots of compilation time values.

Is there an idiomatic way to make those available to a debugger (e.g. RAD Debugger, RemedyBG), i.e. in a watch window expression or usable in a conditional breakpoint.

I expect I would have to write something in code, because if it’s not used, it’s not compiled so it doesn’t have a value, but could I force the evaluation by giving some function a type (e.g. main), and all the value constants of that type would be returned as variables in a new struct type.

Is there already something in the lib that would have the same effect?

Only if they end up available at runtime, at which point it’s just a normal variable.

If you want to print debug your comptime code, there is @compileLog.

A while ago, someone did mention a comptime debugger was planned/wip, but I have not found anything to verify that.

1 Like

If anybody is interested, I found a way to get what I wanted.

A friend helped me with this, and to fit my purpose, I had it pass a type:

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

fn CreateDebugType(TConstants: type) type {
    const decls = std.meta.declarations(TConstants);
    var fields: [decls.len]std.builtin.Type.StructField = undefined;

    for (decls, 0..) |decl, i| {
        const val = @field(TConstants, decl.name);
        const TField = @TypeOf(val);

        // Convert comptime types to runtime types so they can be stored
        const RuntimeT = switch (@typeInfo(TField)) {
            .comptime_int => i64,
            .comptime_float => f64,
            else => TField,
        };

        // Create null-terminated name for the field
        const name_z = decl.name ++ "\x00";
        const name_ptr: [:0]const u8 = name_z[0..decl.name.len :0];

        fields[i] = .{
            .name = name_ptr,
            .type = RuntimeT,
            .default_value_ptr = null,
            .is_comptime = false,
            .alignment = @alignOf(RuntimeT),
        };
    }

    return @Type(.{ .@"struct" = .{
        .layout = .auto,
        .fields = &fields,
        .decls = &.{},
        .is_tuple = false,
    } });
}

pub fn createDebugValue(TConstants: type) CreateDebugType(TConstants) {
    const ConstantsDebugType = CreateDebugType(TConstants);
    return blk: {
        var val: ConstantsDebugType = undefined;
        for (std.meta.declarations(TConstants)) |decl| {
            const c_val = @field(TConstants, decl.name);
            const TField = @TypeOf(c_val);
            const field_val = switch (@typeInfo(TField)) {
                .comptime_int => @as(i64, c_val),
                .comptime_float => @as(f64, c_val),
                else => c_val,
            };
            @field(val, decl.name) = field_val;
        }
        break :blk val;
    };
}

So a simple way to use it is in the main, you can pass it your imported structs:

const std = @import("std");
const debug_const = @import("debug_const.zig");
const root = @import("root.zig");
pub const _r = debug_const.createDebugValue(root);
pub fn main() anyerror!void {
    std.mem.doNotOptimizeAway(&_r);
    try root.main();
}

Then you can put _r in the watch window to observe your pub const of the root.zig file, so you can observe your non-trivial comptime values more easily, without a printf-compile loop.

2 Likes

That is near, but I’m curious as to what you are doing that makes that easier than using std.debug.print or @compileLog. ofc if you learnt something then it’s certainly worth it.

Nothing special. I just started my project and had some segfault and wanted to reason about my index arithmetic, what value wasn’t what I expected, I don’t remember the details.

My main question was how people in the community usually go about it: many pub var, write a test to show the values, std.debug.print the values, some debugging feature I don’t know about, some compiler output that can be enable.

I thought of this solution because I didn’t want to keep track of my symbols in more than one place (i.e. where I need it for compilation + where I need make it observable at runtime), and I didn’t want to lose the comptime just to observe it in a debugger. Now I need to put the struct in a variable and do the std.mem call and add pub to the const, but it’s good enough for me as I expect the project structure will not change as much as adding/removing/renaming const.