Literal string coercion to []const u8 in generic function

I have this init function that should handle different types

hlstring: t.HlString,
chars: t.Chars,
visible: bool,

pub fn init(alc: mem.Allocator, args: anytype) !TextChunk {
    const T = @TypeOf(args[0]);
    const value: T = args[0];

    switch (T) {
        []const u8 => stuff(),

        // more types...

        else => {
            switch (@typeInfo(T)) {
                .pointer => {
                    if (value.len > 0 and @TypeOf(value[0]) == u8) {
                        stuff();
                    }
                    else {
                        @compileError("invalid type: " ++ @typeName(T));
                    }
                },
                else => @compileError("invalid type: " ++ @typeName(T)),
            }
        },
    }
    return ...;
}

and testing it like this

test "init TextChunk" {
    var da = std.heap.DebugAllocator(.{}){};
    defer _ = da.deinit();
    const alc = da.allocator();

    var tc1 = try TextChunk.init(alc, .{"aaa"});
    defer tc1.deinit(alc);
    try expect(mem.eql(u8, tc1.chars.items, "\x1b[22;23;24;27;39;49maaa"));
}

This works but I can’t manage to coerce that string literal "aaa" to a []const u8, I need that switch over @typeInfo(T) to find out it’s a string.

If I call it like this

    var tc1 = try TextChunk.init(alc, .{@as([]const u8, "aaa")});

then it works also without the @typeInfo switch, but I’d like to avoid that cast at every call site. I have the impression I’m doing something wrong, is there another way to coerce that string literal into []const u8?

Ok this works and seems better

pub fn init(alc: mem.Allocator, comptime T: type, args: anytype) !TextChunk {
    const value: T = args[0];

    switch (T) {
        []const u8 => {
            // ...
        },

        // more types...

        else => @compileError("invalid type: " ++ @typeName(T)),
    }
    return .{
        // ...
    };
}

test "init TextChunk" {
    var da = std.heap.DebugAllocator(.{}){};
    defer _ = da.deinit();
    const alc = da.allocator();

    var tc1 = try TextChunk.init(alc, []const u8, .{ "aaa" });
    defer tc1.deinit(alc);
    try expect(mem.eql(u8, tc1.chars.items, "\x1b[22;23;24;27;39;49maaa"));
}

You can use @compileLog to see what type you get from string literals when accepting anytype, that should explain why expecting a slice wasn’t working. Also be aware that sometimes somebody can also give you a [] u8 (which won’t match []const u8, which will also not match [:0]u8 nor [:0]const u8).

You might want have a dedicated function that only deals with strings so that you can simplify some generic code.

Checking if an argument of some unknown type is string-like and coercible to []const u8 requires a bit of reflection effort, since it could be a single-item pointer to an array instead of an actual slice, and/or be mutable and/or have a 0 sentinel.

pub fn asString(x: anytype) ?[]const u8 {
    switch (@typeInfo(@TypeOf(x))) {
        .pointer => |ptr_info| {
            if (ptr_info.is_volatile or
                ptr_info.alignment != 1 or
                ptr_info.address_space != .generic or
                ptr_info.is_allowzero)
            {
                return null;
            }
            switch (ptr_info.size) {
                .one => switch (@typeInfo(ptr_info.child)) {
                    .array => |array_info| {
                        if (array_info.child == u8 and (array_info.sentinel() orelse 0) == 0) {
                            return x;
                        }
                    },
                    else => {},
                },
                .slice => {
                    if (ptr_info.child == u8 and (ptr_info.sentinel() orelse 0) == 0) {
                        return x;
                    }
                },
                else => {},
            }
        },
        else => {},
    }
    return null;
}

test asString {
    var string = [_:0]u8{ 'a', 'b', 'c' };
    try std.testing.expect(asString(@as([]const u8, &string)) != null);
    try std.testing.expect(asString(@as([]u8, &string)) != null);
    try std.testing.expect(asString(@as([:0]const u8, &string)) != null);
    try std.testing.expect(asString(@as([:0]u8, &string)) != null);
    try std.testing.expect(asString(@as(*const [3]u8, &string)) != null);
    try std.testing.expect(asString(@as(*[3]u8, &string)) != null);
    try std.testing.expect(asString(@as(*const [3:0]u8, &string)) != null);
    try std.testing.expect(asString(@as(*[3:0]u8, &string)) != null);
}
2 Likes

Thanks. For now I resolved to pass the type as argument so that it doesn’t have to guess it. I’ll see if I run into trouble with that.

I don’t think this is applicable to your case, but if you just want a compiler error if it’s not coercible, you can just assign it to a variable with an explicit type.