Can’t understand how comptime works in my code

I have a simple enum, that has a method to “convert” it to a colored string, but whenever I try to use it, I get an error “unable to resolve comptime value”, though this code is not intended to be ran on comptime, but even if zig is calculating all possible return values at comptime (since there is a very finite number of those) of this function:

  1. I don’t remember asking to do so :smile:
  2. All values are actually comptime known, so there shouldn’t be any errors anyway.

Minimal example:

const std = @import("std");
const fmt = std.fmt.comptimePrint;

const ESC = "\x1B[";

const FORE: u8 = 38;
const BACK: u8 = 48;

const Field = enum {
    A,
    B,

    fn to_string(self: Field) u8 {
        return switch (self) {
            .A => 'A',
            .B => 'B',
        };
    }

    fn to_colored_string(self: Field) []const u8 {
        return switch (self) {
            .A => setFore("A", "FF0000"),
            .B => setFore("B", "0000FF"),
        };
    }
};

pub fn setFore(str: []const u8, comptime color: []const u8) []const u8 {
    const x = convertHexColorToRGB(color);
    return getRGB(FORE, x[0], x[1], x[2]) ++ str ++ getReset(FORE);
}
pub fn convertHexColorToRGB(comptime hex: []const u8) []u8 {
    var result: [3]u8 = undefined;

    for (0..3) |x| {
        result[x] = hexStrToDec(hex[x * 2]) * 16 + hexStrToDec(hex[x * 2 + 1]);
    }

    return &result;
}

fn hexStrToDec(hex: u8) u8 {
    if (hex >= 'A' and hex <= 'F') {
        return hex - 'A' + 10;
    } else if (hex >= 'a' and hex <= 'f') {
        return hex - 'a' + 10;
    } else if (hex >= '0' and hex <= '9') {
        return hex - '0';
    }
    return 0;
}

fn getRGB(comptime mode: u8, r: u8, g: u8, b: u8) []const u8 {
    return fmt("{s}{d};2;{d};{d};{d}m", .{ ESC, mode, r, g, b });
}

fn getReset(comptime mode: u8) []const u8 {
    return fmt("{s}{d}m", .{ ESC, mode + 1 });
}

const stdout = std.io.getStdOut().writer();

pub fn main() void {
    const field = Field.A;

    // this works
    stdout.print("{c}", .{field.to_string()}) catch return;
    // and this doesn't
    stdout.print("{s}", .{field.to_colored_string()}) catch return;
}

I also tried using comptimePrint instead of concat (since some errors explicitly mention concat as the main issue), but it didn’t change much. Replacing comptime operations with print that uses an allocator would probably fix the issue, but I don’t want to hassle with them in such a simple use-case.

Please help me understand why zig is trying to execute it at compile time, even though it shouldn’t, why it errors at comptime though all values are actually comptime-known in this example, atleast, and how to fix it for both when values are comptime known and when they aren’t.

I am new to zig, so bad code and stupid questions are definitely there, sorry for this :upside_down_face:.

Hi @DeNice-r Welcome to Ziggit,

So running the code I see this error message:

src/main.zig:30:18: error: unable to resolve comptime value
    return getRGB(FORE, x[0], x[1], x[2]) ++ str ++ getReset(FORE);
           ~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~
src/main.zig:30:18: note: slice value being concatenated must be comptime-known
/home/tyler/.local/bin/zigc/zig-0.12.0/lib/std/fmt.zig:1827:89: error: unable to resolve comptime value

The Note is helpful. Concantenation works only with comptime known arrays, and the error is indicating that getRGB doesn’t return a comptime value. This is because r g and b parameters are all runtime values, and not comptime ones.

2 Likes

From what I can see the main reason why it wants everything to be comptime is this:

return getRGB(FORE, x[0], x[1], x[2]) ++ str ++ getReset(FORE);

In other languages string + string implicitely allocates memory for you, to store the result string.
In Zig this just isn’t possible because there is no default allocator, and there is also no RAII or garbage collection to automatically clean it up. An operation like ++ can only work at comptime (or on arrays with comptime-known length). If you want to avoid it, then I can use runtime printing like bufPrint or allocPrint. An ArrayList could also work for the concatenation part.

The error in this particular case is happening because functions calls are executed at runtime, unless you are in a comptime block. So in this case the compiler will attempt to run getRGB at runtime, giving you an error. To fix it you can just write comptime in front of the function. Here is the fixed version:

pub fn setFore(comptime str: []const u8, comptime color: []const u8) []const u8 {
    const x = comptime convertHexColorToRGB(color);
    return comptime getRGB(FORE, x[0], x[1], x[2]) ++ str ++ getReset(FORE);
}
4 Likes

Hi

I am learning zig as well. I am running through your code.

Please would you explain fn convertHexColorToRGB() to me?

You initialize a Var called “result”. Which has 3 bytes allocated to it in an array.
You return &result. Which is a reference. I assume a slice.

Okay. Where does this reference point to once the function finishes and “result” goes out of scope?

Thanks

pub fn convertHexColorToRGB(comptime hex: []const u8) []u8 {
    var result: [3]u8 = undefined;

    for (0..3) |x| {
        result[x] = hexStrToDec(hex[x * 2]) * 16 + hexStrToDec(hex[x * 2 + 1]);
    }

    return &result;
}
2 Likes

Replying to my own Reply. Which is never a good idea.

The good news is I got the code working. I am guessing you were expecting a Red A.
The bad news is it is somewhat butchered it to get it to work. Sorry.

Essentially I have added an arena allocator for the u8 slices to point to. The protocol I used was to rem out your code and replace it with mine. So you can see what I have been up to. It’s not pretty and I am not proud of it, but it may give you some ideas on how to proceed.

Here we go …

const std = @import("std");
//const fmt = std.fmt.comptimePrint;

const ESC = "\x1B[";

const FORE: u8 = 38;
const BACK: u8 = 48;

const Field = enum {
    A,
    B,

    fn to_string(self: Field) u8 {
        return switch (self) {
            .A => 'A',
            .B => 'B',
        };
    }

    //fn to_colored_string(self: Field) []const u8 {
    fn to_colored_string(self: Field, allocator:std.mem.Allocator ) void {
        return switch (self) {
            .A => setFore("A", "FF0000",allocator),
            .B => setFore("B", "0000FF",allocator),
        };
    }
};

//pub fn setFore(str: []const u8, comptime color: []const u8) []const u8 {
pub fn setFore(str: []const u8, comptime color: []const u8, allocator:std.mem.Allocator) void {

    //const x = convertHexColorToRGB(color);
    var result: [3]u8 = undefined;

    result[0]=hexStrToDec(color[0]) * 16 + hexStrToDec(color[1]);
    result[1]=hexStrToDec(color[2]) * 16 + hexStrToDec(color[3]);
    result[2]=hexStrToDec(color[4]) * 16 + hexStrToDec(color[5]);

    //return getRGB(FORE, x[0], x[1], x[2]) ++ str ++ getReset(FORE);
    stdout.print("{s}", .{ getRGB(FORE, result[0], result[1], result[2],allocator)} ) catch return;
    stdout.print("{s}", .{ str }) catch return;
    stdout.print("{s}", .{ getReset(FORE ,allocator)} ) catch return;

    return;

}

//pub fn convertHexColorToRGB(comptime hex: []const u8) []u8 {
//    var result: [3]u8 = undefined;
//
//    for (0..3) |x| {
//        result[x] = hexStrToDec(hex[x * 2]) * 16 + hexStrToDec(hex[x * 2 + 1]);
//    }
//
//    return &result;
//}

fn hexStrToDec(comptime hex: u8) u8 {
    if (hex >= 'A' and hex <= 'F') {
        return hex - 'A' + 10;
    } else if (hex >= 'a' and hex <= 'f') {
        return hex - 'a' + 10;
    } else if (hex >= '0' and hex <= '9') {
        return hex - '0';
    }
    return 0;
}

//fn getRGB(comptime mode: u8, r: u8, g: u8, b: u8) []const u8 {
fn getRGB(mode: u8,r: u8,g: u8,b: u8, allocator:std.mem.Allocator) []u8 {
    //return fmt("{s}{d};3;{d};{d};{d}m", .{ ESC, mode, r, g, b });

    const return_value=std.fmt.allocPrint(allocator,"{s}{d};2;{d};{d};{d}m", .{ ESC, mode, r, g, b }) catch {std.process.exit(1); };
    return return_value;
}

fn getReset(comptime mode: u8,allocator:std.mem.Allocator) []u8 {
    //return fmt("{s}{d}m", .{ ESC, mode + 1 });

    const return_value=std.fmt.allocPrint(allocator,"{s}{d}m", .{ ESC, mode + 1 }) catch {std.process.exit(1); };
    return return_value;
}

const stdout = std.io.getStdOut().writer();

pub fn main() void {
    const field = Field.A;

    // this works
    stdout.print("{c}", .{field.to_string()}) catch return;
    // and this doesn't
    //stdout.print("{s}", .{field.to_colored_string()}) catch return;

    // But this does ......

    // setup an arena allocation to give slices to point to.
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit(); const allocator = arena.allocator();

    // lets run your code with some memory
    field.to_colored_string(allocator);

}

small nitpick

Concantenation works only with comptime known arrays

It works with runtime known arrays as well. But only on comptime known slices since ++ needs to know size.

4 Likes

I used return result at first, but compiler explicitly states that it should have & (or that is how I read it):

array literal requires address-of operator (&) to coerce to slice type '[]u8'
    return result;
           ^~~~~~

Couldn’t make it work otherwise, so left the way it was recommended by the error message.

Regarding your question: it coerces to a slice (instead of returning bare pointer), as the error suggests. But honestly I don’t understand it any better :slightly_smiling_face:

As to your reply, thanks for the effort. I had great hopes to avoid allocators and especially passing them around for this use case, because it feels bad :sweat_smile: . I’ve got another suggestion, to use buffer as it may be a good compromise between comptime and runtime. I’ll try it first since I can’t justify for myself using allocators in this case :innocent:

Thanks, didn’t know you could do that! That pushed me forward a bit :sparkling_heart:

I don’t understand why you return a slice, why not just return [3]u8 instead?

pub fn convertHexColorToRGB(comptime hex: []const u8) [3]u8 {
    var result: [3]u8 = undefined;

    for (0..3) |x| {
        result[x] = hexStrToDec(hex[x * 2]) * 16 + hexStrToDec(hex[x * 2 + 1]);
    }

    return result;
}

Then the caller can use it more easily with comptime known length in the type and you don’t enter that whole uncertain area, of having to worry about where that temporary pointer may end up pointing and whether the memory it is pointing to is still correct or not.

Instead of using slices everywhere just use the same return type that comptimePrint has *const [count(fmt, args):0]u8

const rgb_fmt = "{s}{d};2;{d};{d};{d}m";
fn getRGB(comptime mode: u8, r: u8, g: u8, b: u8) *const [std.fmt.count(rgb_fmt, .{ ESC, mode, r, g, b }):0]u8 {
    return fmt(rgb_fmt, .{ ESC, mode, r, g, b });
}

fn getReset(comptime mode: u8) *const [std.fmt.count("{s}{d}m", .{ ESC, mode + 1 }):0]u8 {
    return fmt("{s}{d}m", .{ ESC, mode + 1 });
}

Additionally I am not quite sure of your overall intent. For example why do you store your color as hex string and convert it, instead of using decimals to begin with? (maybe some other code requires that)

Mostly I find it difficult to tell what is part of the problem you are trying to solve and what might be things you don’t really need, but just things you have tried.

I think it would be good if you clarify your goal, inputs, outputs and whether those are runtime or comptime values.

While it is useful to do certain things at comptime, doing everything at comptime seems unpractical. I think things become way easier when you stop converting things to a string which you then print and instead use formatting functions that write to a writer directly.

While there are a lot of things that you can do at comptime, I think it is best to first do something simple and then figure out where comptime is really useful/beneficial.

So here is an example where I changed a bunch of stuff:

const std = @import("std");

pub const console = struct {
    // https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
    // ansi escape codes for coloring text output
    const ESC = "\u{001B}";
    pub const RESET = ESC ++ "[0m";
    pub const BLACK = ESC ++ "[1;30m";
    pub const RED = ESC ++ "[1;31m";
    pub const GREEN = ESC ++ "[1;32m";
    pub const YELLOW = ESC ++ "[1;33m";
    pub const BLUE = ESC ++ "[1;34m";
    pub const MAGENTA = ESC ++ "[1;34m";
    pub const CYAN = ESC ++ "[1;36m";
    pub const WHITE = ESC ++ "[1;37m";

    const rgb_format = ESC ++ "[38;2;{d};{d};{d}m";
    fn rgb(args: anytype) *const [std.fmt.count(rgb_format, args):0]u8 {
        if (comptime args.len != 3) @compileError("rgb expects a tuple of 3 arguments of u8");
        return std.fmt.comptimePrint(rgb_format, args);
    }

    pub const add = rgb(.{ 120, 200, 120 }); // green
    pub const remove = rgb(.{ 200, 120, 120 }); // red

    pub const clear_line_right = ESC ++ "[0K";
    pub const clear_line_left = ESC ++ "[1K";
    pub const clear_line = ESC ++ "[2K";

    pub const previous_line = ESC ++ "[F";
};

const Field = enum {
    A,
    B,

    fn color(self: Field) std.fmt.Formatter(formatColor) {
        return .{ .data = self };
    }

    fn formatColor(
        self: Field,
        comptime _: []const u8,
        _: std.fmt.FormatOptions,
        writer: anytype,
    ) !void {
        const c: [:0]const u8 = switch (self) {
            .A => console.rgb(.{ 255, 0, 0 }),
            .B => console.rgb(.{ 0, 0, 255 }),
        };
        try writer.print("{s}{s}{s}", .{ c, @tagName(self), console.RESET });
    }

    pub fn format(
        self: Field,
        comptime _: []const u8,
        _: std.fmt.FormatOptions,
        writer: anytype,
    ) !void {
        try writer.print("{s}", .{@tagName(self)});
    }
};

const stdout = std.io.getStdOut().writer();

pub fn main() !void {
    const field = Field.A;

    try stdout.print("{}\n", .{field});
    try stdout.print("{}\n", .{field.color()});
}

Also take a look at: Idiomatic Complex Formatting - #5 by castholm

3 Likes

First of all: huge thank you for this answer. It is effectively a goldmine of zig knowledge for me :smiley:. So many small and big things, especially the fact that you could write your own format func, I sure did like it in python and it is there, too!

I don’t really have many requirements to this code other than that I’d like if it worked :sweat_smile:, and answer to most of your why X instead of Y questions is that I had no idea Y existed or couldn’t figure it out.

I would like all values to be runtime, but colors will be comptime 100%, I just wanted to make it work with any (comp- or run-time) values, but thrown a bunch of comptime’s in there to make it work at least somehow.

Mostly all things that are unnecessary (like converting hex in a string to u8 rgb) are self-imposed challenges, since, again, I’m just learning and trying to come by as many things as I can handle.

Misuse of slices mostly caused by that I had no idea it was so easy to go from an array to a slice while typing a value, and frankly thought const declares immutability of a value :sweat_smile:.

I managed to solve my issue by changing my value requirement, just figured that if I’m going to print the value - it has to be comptime-known, so I settled by adding comptime here and there, and my final code resembles zig libraries that I found on github that do the same, so I’m more or less satisfied with the result.

Thanks for a very valuable answer, you really helped me understand zig much better.

1 Like