Dump build options provided by build.zig in ZON format

I am building the same executable with 2^4 (and later more !) different configurations. I would like to dump the results of my runs to .zon format. It looks like the options generated by build.zig use pub const, and therefore can’t be formatted to .zon.
Example:

    const RunData = struct {
        numCycles: usize,
        maxAllocated: usize,
        maxCollected: usize,
        maxGrayStackSize: usize,
        file: []const u8,
        config: @TypeOf(@import("config"))
    };

    const data = RunData{
        .numCycles = vm.gc.numCycles,
        .maxAllocated = vm.gc.maxAllocated,
        .maxCollected = vm.gc.maxCollected,
        .maxGrayStackSize = vm.gc.maxGrayStackSize,
        .file = path,
        .config = @import("config"),
    };

How can I serialize them without writing the name of each configuration variable explicitely in source code ?

I might have a plan: generate a .zon file at build time, and then import it inside my executable. The only problem is that I must give it a known name, because @import only works with string litterals. Is there a way to make a module out of a .zon file ? This way I could call it using @import("config") inside my main.zig without having to know the name, like when I import a module.

Can you describe what you actually have in config?
I am unsure whether I fully understand what you are trying to do, so please clarify.

Here is a bit of example code:
main.zig:

const std = @import("std");
const data = @import("zonexample.zon");
const gendata = @import("generated");

pub fn main() !void {
    std.debug.print("import of zon file: {s} {}\n", .{ data.name, data.value });
    std.debug.print("import of generated zon file: {s} {}\n", .{ gendata.name, gendata.value });
}

zonexample.zon:

.{
    .name = "hello",
    .value = 123,
}

build.zig:

const std = @import("std");

pub fn build(b: *std.Build) !void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const root = b.createModule(.{
        .root_source_file = b.path("main.zig"),
        .target = target,
        .optimize = optimize,
    });
    const exe = b.addExecutable(.{
        .name = "zonexample",
        .root_module = root,
    });

    const files = b.addWriteFiles();
    const generate_zon = files.add("generated.zon",
        \\.{
        \\    .name = "generated",
        \\    .value = 33,
        \\}
    );
    root.addAnonymousImport("generated", .{
        .root_source_file = generate_zon,
    });

    b.installArtifact(exe);

    const run_exe = b.addRunArtifact(exe);
    run_exe.addArgs(b.args orelse &.{});
    run_exe.step.dependOn(b.getInstallStep());

    const run = b.step("run", "Run the app");
    run.dependOn(&run_exe.step);
}

generate_zon is a lazy path, so you also could for example write a program that can run as a runStep that generates the content of that file, the build system guide has an example for that Zig Build System ⚡ Zig Programming Language

Why would you need/want to do that?
If you answer this it may clear up what you are trying to do and whether there is a simpler way to achieve something similar.

example of a config file:

.{
    .smallJumps = false,
    .vlaTypes = false,
    .storeHash = false,
    .internString = true,
}

My build.zig currently parses the .zon file, and uses .addOption to add all of the fields to the Vm module, which imports them. This should change as the number of options grows: I would like to generate all of the configs with code at build time instead. These configs are used at compile time to affect layout of structs, method behaviour, so I can’t pass them to the executable at runtime via a config file cli argument.

As the name of the options maybe suggest, I want to measure the impacts of string interning in my Lox implementation in term of speed, memory consumption, etc…, and I don’t want to have a runtime if branch each time I create a String instance. This is especially important when I will measure executable size: I don’t want to have the runtime if branch inside the executable, it would clutter my measurements.

So that’s why I want to dump compile time config from my executable !
I believe your approach could work, but I really need to generate a lot of configs so I want a way to get “the config” without knowing it’s name in advance. I am currently trying another aproach: embedding the formatted zon string inside the options. I would then parse the string at the end and then add the data to my results struct.

1 Like

It worked ! Here is a sketch of my solution:

// VmConfig.zig
/// Log the stack from the current callFrame.
logStack: bool,

/// Log the callStack.
logCallStack: bool,

/// Log each instructions.
logInstructions: bool,

/// Log each garbage collection cycle.
logGCCycles: bool,

/// Log all allocations made by the Vm.
logAllocations: bool,

/// Each call to the alloc functions triggers a GC Cycle.
stressGC: bool,

/// Jump offsets are encoded using 1 byte instead of 2.
smallJumps: bool,

/// Embed arrays of known length within allocations of types.
vlaTypes: bool,

/// Embed hash inside string struct.
storeHash: bool,

/// Cache strings to prevent duplicates.
internString: bool,
// build.zig
fn optionFrom(b: *std.Build, comptime T: type, values: *const T) anyerror!*std.Build.Step.Options {
    const options = b.addOptions();
    inline for (@typeInfo(T).@"struct".fields) |field| {
        options.addOption(@FieldType(T, field.name), field.name, @field(values, field.name));
    }

    var writer = std.Io.Writer.Allocating.init(b.allocator);
    defer writer.deinit();

    try std.zon.stringify.serialize(values, .{ .whitespace = false }, &writer.writer);
    try writer.writer.writeByte(0);
    const formatted: []const u8 = writer.written();
    const res = formatted[0 .. formatted.len - 1 :0];

    options.addOption([:0]const u8, "formatted", res);
    return options;
}
// main.zig
    const RunData = struct {
        numCycles: usize,
        maxAllocated: usize,
        maxCollected: usize,
        maxGrayStackSize: usize,
        file: []const u8,
        config: VmConfig,
    };

    const config = try std.zon.parse.fromSlice(VmConfig, allocator, Vm.config.formatted, null, .{
        .free_on_error = true,
        .ignore_unknown_fields = false,
    });

    const data = RunData{
        .numCycles = vm.gc.numCycles,
        .maxAllocated = vm.gc.maxAllocated,
        .maxCollected = vm.gc.maxCollected,
        .maxGrayStackSize = vm.gc.maxGrayStackSize,
        .file = path,
        .config = config,
    };

    try std.zon.stringify.serialize(data, .{}, results.writer().?);

It’s interesting to see how many people are using Zig for implementing other languages like Lox.
We could use a special category “Crafting Interpreters” …

I think a tag would be enough

1 Like

May I disagree ? Often, ‘Crafting interpreters’ is just the context, and knowledge of the book is not needed to answer most posts.

I think for language implementations we could maybe have a tag like ‘language-dev’, if you create a language based on crafting interpreters it would be fine to have tags ‘language-dev’ and ‘Crafting Interpreters’

I think it makes sense to use tags to group context, if people tag all the posts related to ‘Crafting Interpreters’ with a common tag it is easier to find for people who care about it in particular.

That said, this doesn’t mean that all the help topics need to do this, I think my conversation with @hvbargen was a bit more general about how “Crafting Interpreters” could have its niche in this forum.

If somebody wants to collect different posts about a particular topic, we also can create a specific topic for it and then turn it into a wiki post, so the community can turn it into a sort of evolving index about the topic.

+1 for a tag language-dev.
There are some details in several language-dev related topics where Zig is offers more natural ways or OTOH can be a bit tricky, or offers better performance.
I think of how to implement memory management including garbage collection, NAN-tagging, writing a lexer, writing a byte-code interpreter loop, even simple things like strings, error handling.
When I worked through the book, I tried to do it as close to the C source used in the book, which helped me to understand what to add where.
Later, after finishing, I changed a lot to make it more Zig-like, eg using errors instead of return codes/booleans, or make use of the amazing comptime compilation to avoid boilerplate code.
It’s still a lot of fun, unfortunately I don’t have as much time as I want for it, because I have to work for a living. So many ideas and so little time …

1 Like

Yes, I believe a more general tag like langage-dev is better suited for this forum, because it’s more generic.