How to create arrays during compile time in zig

962 Views Asked by At

I'm very new to zig and finding hard to create array during compile time. I want to create error type during compile time. Below is a sample code.

const std = @import("std");

pub fn get_error() !type {
    // Cannot initialize with a fixed length array as the error names are fetched from a different file.
    var GP = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = GP.allocator();
    var error_codes = std.ArrayList(std.builtin.Type.Error).init(allocator);
    
    try error_codes.append(.{ .name = "Error1" }); // Think Error1 is the name fetched from the file.

    return @Type(.{ .ErrorSet = try error_codes.toOwnedSlice() });
}

test "error_codes" {
    const error_type = comptime get_error();
    _ = error_type;
}

Click test after opening this link for running above code

1

There are 1 best solutions below

5
On BEST ANSWER

From the comments in the posted code it looks like OP wants to read a file containing error names and from this create an error set type. It seems that OP would like to use comptime to accomplish this at compile time.

There is a fundamental problem with this plan: it is not possible to do comptime I\O operations in Zig. In addition to this problem, Zig doesn't currently have comptime allocators (as of the current v0.12.0 development branch; it looks like this may be coming in the future).

There are at least two paths forward. One option is to just create an error type in another file and import it. OP must have some reason to avoid this, but let's look at it anyway to see what the error set type looks like:

// error_codes.zig
pub const ErrorCodes = error{
    ErrorCodeRed,
    ErrorCodeYellow,
    ErrorCodeGreen,
};
// import_errors.zig
const std = @import("std");
const ErrorCodes = @import("./error_codes.zig").ErrorCodes;

pub fn main() void {
    std.debug.print("{}\n", .{ErrorCodes});
    std.debug.print("{}\n", .{@typeInfo(ErrorCodes)});
}

Run this program to inspect the error set type:

> zig build-exe import_errors.zig
> ./import_errors
error{ErrorCodeRed,ErrorCodeYellow,ErrorCodeGreen}
builtin.Type{ .ErrorSet = { builtin.Type.Error{ .name = { ... } }, builtin.Type.Error{ .name = { ... } }, builtin.Type.Error{ .name = { ... } } } }

It is also possible to do this using a file containing error names at compile time, but not using comptime with I\O operations. That is, even if Zig had allocators working at compile time, the standard library file I/O operations can't be used at compile time. Instead you can use @embedFile to load the contents of the file containing error names at compile time into a compile time array. @embedFile returns the equivalent of a string literal containing the contents of the file.

Since you have this data at compile time, you can count the number of error names and create another compile time array to hold the errors. This array can be converted to a slice and used to initialize the ErrorSet field of a Type to create an error set type via the @Type builtin function.

The program which follows assumes that the file containing error names follows a strict format: each line must contain exactly one error name and nothing else. Here is the error_codes.dat file:

ErrorCodeRed
ErrorCodeYellow
ErrorCodeGreen

This program embeds the error_codes.dat file, uses it to create an array of errors from the error names in the file, and uses the array of errors to create an error type, all at compile time:

// embed_errors.zig
const std = @import("std");
const mem = std.mem;
const Error = std.builtin.Type.Error;

const err_data = @embedFile("./error_codes.dat");

fn load_error_codes(comptime errs: []Error, comptime data: []const u8) type {
    var names = mem.tokenizeAny(u8, data, "\r\n");
    for (0..errs.len) |i| {
        errs[i] = .{ .name = names.next().? };
    }
    return @Type(.{ .ErrorSet = errs });
}

pub fn main() void {
    const err_count = comptime mem.count(u8, err_data, "\n");
    comptime var errors: [err_count]Error = undefined;
    const ErrorCodes = comptime load_error_codes(&errors, err_data);

    std.debug.print("{}\n", .{ErrorCodes});
    std.debug.print("{}\n", .{@typeInfo(ErrorCodes)});
}

Running this program shows that it creates an error set type identical to the one created by the first program:

> zig build-exe embed_errors.zig
> ./embed_errors
error{ErrorCodeRed,ErrorCodeYellow,ErrorCodeGreen}
builtin.Type{ .ErrorSet = { builtin.Type.Error{ .name = { ... } }, builtin.Type.Error{ .name = { ... } }, builtin.Type.Error{ .name = { ... } } } }

Does This Really Happen at Compile Time?

OP has suggested that @embedFile will not work because they "...need to process and generate error codes during comptime."

That is exactly what this solution does. It wouldn't even work if @embedFile didn't provide the data at comptime. You can look at the assembly code emitted by the compiler to see that the data file has been encoded as a string literal:

; ...

embed_errors.err_data__anon_3100:
    .asciz  "ErrorCodeRed\r\nErrorCodeYellow\r\nErrorCodeGreen\r\n"

;...

And the ErrorCodes error set is comptime because it is a const which is initialized by calling the load_error_codes function at comptime.

Changing Requirements

OP now says that the errors need to be prefixed with some additional context information, and that "...this demands an explicit allocator for string concatenation."

This is not true. If the prefixes are known at compile time (and they must be since it would be impossible to add unknown prefixes at compile time) the code can use the ++ operator to concatenate the strings.

Here is an example which adds a prefix to the errors. The prefix is passed to the (comptime) call to load_error_codes_with_context and it is simply concatenated with each of the error names as the individual errors are constructed. Obviously this may need to be altered to suit OP's particular use case, but the exact requirements have not been made clear. So long as the prefixes are known or can be calculated at compile time this idea should work.

// embed_errors_with_context.zig
const std = @import("std");
const mem = std.mem;
const Error = std.builtin.Type.Error;

const err_data = @embedFile("./error_codes.dat");

fn load_error_codes_with_context(comptime context: []const u8, comptime errs: []Error, comptime data: []const u8) type {
    var names = mem.tokenizeAny(u8, data, "\r\n");
    for (errs) |*e| {
        e.* = .{ .name = context ++ names.next().? };
    }
    return @Type(.{ .ErrorSet = errs });
}

pub fn main() void {
    const err_count = comptime mem.count(u8, err_data, "\n");
    comptime var errors: [err_count]Error = undefined;
    const ErrorCodes =
        comptime load_error_codes_with_context(
        "MyPrefix",
        &errors,
        err_data,
    );

    std.debug.print("{}\n", .{ErrorCodes});
    std.debug.print("{}\n", .{@typeInfo(ErrorCodes)});
}

Running this program shows that the errors are now prefixed:

> zig build-exe .\embed_errors_with_context.zig
> .\embed_errors_with_context
error{MyPrefixErrorCodeRed,MyPrefixErrorCodeYellow,MyPrefixErrorCodeGreen}
builtin.Type{ .ErrorSet = { builtin.Type.Error{ .name = { ... } }, builtin.Type.Error{ .name = { ... } }, builtin.Type.Error{ .name = { ... } } } }