Injecting new values into a comptime tuple

I would like to take a tuple like .{'1', '2', '3'} and inject a separator between the items to get, e.g. .{'1', '@', '2', '@', '3'}. The code below bombs with the following errors.

I’m focused on the separate function which is meant to do the above by creating a printer const pp1 = separate(space, .{char('@')});.

sepTypes is supposed to create the new tuple type.

I’m setting the array of values a to be of type sepTypes(sep, printers).

    comptime var a: sepTypes(sep, printers) = undefined;

Why don’t my types match?

main.zig:114:21: error: expected type 'main.Concat(struct { main.Char })', found 'main.Concat(.{ .{ ... } })'
    return Concat(a){};

The errors

❯ zig build test --summary all
test
└─ run test
   └─ zig test Debug native 2 errors
main.zig:44:25: error: type 'type' is not indexable and not a range
            inline for (printers) |p| {
                        ^~~~~~~~
main.zig:44:25: note: for loop operand must be a range, array, slice, tuple, or vector
main.zig:114:21: error: expected type 'main.Concat(struct { main.Char })', found 'main.Concat(.{ .{ ... } })'
    return Concat(a){};
           ~~~~~~~~~^~
main.zig:38:12: note: struct declared here (2 times)
    return struct {
           ^~~~~~
main.zig:101:76: note: function return type declared here
pub fn separate(comptime sep: anytype, comptime printers: anytype) Separate(sep, printers) {
                                                                   ~~~~~~~~^~~~~~~~~~~~~~~                                                                   ~~~~~~~~^~~~~~~~~~~~~~~

The code

const std = @import("std");

const io = std.io;
const mem = std.mem;
const meta = std.meta;
const testing = std.testing;

pub const Error = error{InvalidNewline} || mem.Allocator.Error;

const Empty = struct {
    const Self = @This();

    fn print(_: Self, _: anytype) Error!void {}
};

const empty = Empty{};

const Char = struct {
    c: u8,

    const Self = @This();

    fn print(self: Self, writer: anytype) Error!void {
        const s = [_]u8{self.c};
        _ = try writer.write(&s);
    }
};

pub fn char(comptime c: u8) Char {
    if (c == '\n')
        @compileError("Newline is invalid!");
    return Char{ .c = c };
}

pub const space = char(' ');

pub fn Concat(comptime printers: anytype) type {
    return struct {
        pub const T = @TypeOf(printers);

        const Self = @This();

        fn print(_: Self, writer: anytype) Error!void {
            inline for (printers) |p| {
                const PT = @TypeOf(p);
                try PT.print(p, writer);
            }
        }
    };
}

pub fn concat(comptime printers: anytype) Concat(printers) {
    return Concat(printers){};
}

test "concat" {
    const a = testing.allocator;
    const L = std.ArrayList(u8);
    var list1 = L.init(a);
    defer list1.deinit();
    const pp1 = concat(.{empty});
    try pp1.print(list1.writer());
    try testing.expectEqual(0, list1.items.len);
    var list2 = L.init(a);
    defer list2.deinit();
    const pp2 = concat(.{ empty, space, char('@') });
    try pp2.print(list2.writer());
    try testing.expectEqual(3, list2.items.len);
    try testing.expectEqualSlices(u8, " @", list2.items);
}

fn sepTypes(comptime sep: anytype, comptime printers: anytype) type {
    if (printers.len == 0)
        @compileError("No printers to separate!");

    const len = printers.len;
    const n = comptime if (len == 1) 1 else (len * 2 - 1);
    comptime var a: [n]type = undefined;

    if (len > 1) {
        const TS = @TypeOf(sep);

        for (0..len - 1) |i| {
            const T = @TypeOf(printers[i]);
            a[2 * i] = T;
            a[2 * 1 + 1] = TS;
        }
    }

    const T = @TypeOf(printers[len - 1]);
    a[n - 1] = T;

    return meta.Tuple(&a);
}

pub fn Separate(comptime sep: anytype, comptime printers: anytype) type {
    const T = sepTypes(sep, printers);
    return Concat(T);
}

pub fn separate(comptime sep: anytype, comptime printers: anytype) Separate(sep, printers) {
    const len = printers.len;
    const n = comptime if (len == 1) 1 else (len * 2 - 1);
    comptime var a: sepTypes(sep, printers) = undefined;

    if (len > 1) {
        inline for (0..len - 1) |i| {
            a[i * 2] = printers[i];
            a[i * 2 + 1] = sep;
        }
    }

    a[n - 1] = printers[len - 1];
    return Concat(a){};
}

test "separate" {
    const a = testing.allocator;
    const L = std.ArrayList(u8);
    var list = L.init(a);
    const pp1 = separate(space, .{char('@')});
    try pp1.print(list.writer());
    try testing.expectEqual(1, list.items.len);
    const expect1 = "1";
    try testing.expectEqualSlices(u8, expect1, list.items);
    list.deinit();
    // list = L.init(a);
    // const pp2 = separate(space, .{ char('1')), char('2') });
    // var state2 = State.init(0.5, 80);
    // try pp2.print(&state2, list.writer());
    // try testing.expectEqual(3, list.items.len);
    // const expect2 = "1 2";
    // try testing.expectEqualSlices(u8, expect2, list.items);
    // list.deinit();
}

pub fn main() !void {}

I tried to match the signature in the error exactly, e.g. at the bottom of separate

    return Concat(.{Char{ .c = '@' }}){};

The compiler still complains, though

main.zig:114:39: error: expected type 'main.Concat(struct { main.Char })', found 'main.Concat(.{ .{ ... } })'
    return Concat(.{Char{ .c = '@' }}){};
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~^~

It’s difficult to offer help with your question because it’s a bit bogged down by the specifics of your code, which quite franky feels overly complicated for what the question in the title asks and is difficult for readers to quickly wrap their heads around.

It might be easier to start from a blank slate. Could you clarify what your goal with this exercise is? Do you actually want a function that turns a tuple like
@as(struct { u8, i16 ?u8 }, .{ 'A', -2, null })
into
@as(struct { u8, u8, i16, u8, ?u8 }, .{ 'A', '@', -2, '@', null })
? Or do you just want to print the tuple contents with a separator inserted between elements?

The former.

I’m writing a pretty printing combinator library. separator is a combinator that takes a separator printer and a tuple of printers. Think how you would print a comma-separated list of fields in a structure, for example. I could just print a comma after every item but having a separator combinator is much cleaner.

This is an example of a combinator that invokes each combinator from the tuple in the second argument, interspersed with invocation of the separator combinator in the first argument.

const pp1 = separate(space, .{char('@')});

The idea is to turn each structure in the AST, for example, into a pretty-printing combinator by giving it the print function (as well as others I omitted). I’m stuck on modifying a comptime tuple, though.

1 Like

The implementation of sepTypes, the function that returns the type of the modified tuple, may be correct because the type main.Concat(struct { main.Char }) looks right. I feel that the problem is with the implementation of separate but for the life of me I don’t understand why the returning tuple gets this type main.Concat(.{ .{ ... } }). It is typed with the same sepTypes(sep, printers) after all!

I can’t answer why you are getting the errors you are getting because I honestly don’t understand your code. But if the goal is to insert tuple elements between all existing elements, I would try to solve it like this, by using @typeInfo on the old tuple type, creating an array of std.builtin.Type.StructField that is 2x - 1 the length of the original tuple, then populating it in a regular imperative for loop and reifying it using @Type:

const std = @import("std");

pub fn WithSeparator(Tuple: type, Separator: type) type {
    const old_info = @typeInfo(Tuple).@"struct";

    if (old_info.fields.len <= 1) return Tuple;

    var new_fields: [2 * old_info.fields.len - 1]std.builtin.Type.StructField = undefined;
    var field_index: usize = 0;
    for (old_info.fields) |old_field| {
        new_fields[field_index] = .{
            .name = std.fmt.comptimePrint("{d}", .{field_index}),
            .type = old_field.type,
            .default_value = old_field.default_value,
            .is_comptime = old_field.is_comptime,
            .alignment = old_field.alignment,
        };
        field_index += 1;
        if (field_index < new_fields.len) {
            new_fields[field_index] = .{
                .name = std.fmt.comptimePrint("{d}", .{field_index}),
                .type = Separator,
                .default_value = null,
                .is_comptime = false,
                .alignment = 0,
            };
            field_index += 1;
        }
    }

    return @Type(.{ .@"struct" = .{
        .layout = old_info.layout,
        .backing_integer = old_info.backing_integer,
        .fields = &new_fields,
        .decls = &.{},
        .is_tuple = old_info.is_tuple,
    } });
}

pub fn withSeparator(tuple: anytype, separator: anytype) WithSeparator(@TypeOf(tuple), @TypeOf(separator)) {
    var result: WithSeparator(@TypeOf(tuple), @TypeOf(separator)) = undefined;
    comptime var field_index: usize = 0;
    inline for (tuple) |x| {
        result[field_index] = x;
        field_index += 1;
        if (field_index < result.len) {
            result[field_index] = separator;
            field_index += 1;
        }
    }
    return result;
}

test withSeparator {
    var tuple: struct { u8, i16, ?u8 } = undefined;
    tuple = .{ 'A', -2, null };
    var separator: u8 = undefined;
    separator = '@';
    const result = withSeparator(tuple, separator);
    try std.testing.expectEqual(struct { u8, u8, i16, u8, ?u8 }, @TypeOf(result));
    try std.testing.expectEqual(5, result.len);
    try std.testing.expectEqual('A', result[0]);
    try std.testing.expectEqual('@', result[1]);
    try std.testing.expectEqual(-2, result[2]);
    try std.testing.expectEqual('@', result[3]);
    try std.testing.expectEqual(null, result[4]);

    const tuple0: std.meta.Tuple(&.{}) = .{}; // 0-tuple
    const result0 = withSeparator(tuple0, ',');
    try std.testing.expectEqual(0, result0.len);

    const tuple1: struct { u8 } = .{100};
    const result1 = withSeparator(tuple1, ',');
    try std.testing.expectEqual(1, result1.len);
    try std.testing.expectEqual(100, result1[0]);
}

I would also like to offer the general “use metaprogramming responsibly” advice. I personally find the idea of combining types that implement print methods just to print a list of value with separators dubious at best and would strongly suggest you to just write easy-to-understand imperative code instead. If I was multiple levels deep in nested calls to type-reifying functions and getting compile errors that I didn’t understand, I would take that as a sign that my code is overly complicated and that I should probably put effort into simplifying things.

1 Like

Let me explain…

Consider sepTypes. Where this

    @compileLog("sepTypes(space, .{char('@')})", sepTypes(space, .{char('@')}));

gives you

Compile Log Output:
@as(*const [29:0]u8, "sepTypes(space, .{char('@')})"), @as(type, struct { main.Char })

This looks correct because there’s nothing to separate with just 1 element in the printers tuple.

It first creates an array of type, one for each element of the modified tuple

    comptime var a: [n]type = undefined;

It then iterates through printers, setting each pair of elements to @TypeOf(printers[i]) for the element type and then to @TypeOf(sep) for the separator, finally returning a tuple of types.

fn sepTypes(comptime sep: anytype, comptime printers: anytype) type {
    if (printers.len == 0)
        @compileError("No printers to separate!");

    const len = printers.len;
    const n = comptime if (len == 1) 1 else (len * 2 - 1);
    comptime var a: [n]type = undefined;

    if (len > 1) {
        const TS = @TypeOf(sep);

        for (0..len - 1) |i| {
            const T = @TypeOf(printers[i]);
            a[2 * i] = T;
            a[2 * 1 + 1] = TS;
        }
    }

    const T = @TypeOf(printers[len - 1]);
    a[n - 1] = T;

    return meta.Tuple(&a);
}

It helps to cut things down to size!

The code below works. Investigating further but one of the issues seems to be with pre-allocating the type array and assigning to elements vs. concatenating the type array using a = a ++ [_]type{T};.

const std = @import("std");

const io = std.io;
const mem = std.mem;
const meta = std.meta;
const testing = std.testing;

fn sepTypes(comptime sep: anytype, comptime printers: anytype) type {
    if (printers.len == 0)
        @compileError("No printers to separate!");

    const len = printers.len;
    var a: []const type = &[_]type{};

    if (len > 1) {
        const TS = @TypeOf(sep);

        for (0..len - 1) |i| {
            const T = @TypeOf(printers[i]);
            a = a ++ [_]type{T};
            a = a ++ [_]type{TS};
        }
    }

    const T = @TypeOf(printers[len - 1]);
    a = a ++ [_]type{T};

    return meta.Tuple(a);
}

pub fn separate(comptime sep: anytype, comptime printers: anytype) sepTypes(sep, printers) {
    const len = printers.len;
    const n = comptime if (len == 1) 1 else (len * 2 - 1);
    comptime var a: sepTypes(sep, printers) = undefined;

    if (len > 1) {
        inline for (0..len - 1) |i| {
            a[i * 2] = printers[i];
            a[i * 2 + 1] = sep;
        }
    }

    a[n - 1] = printers[len - 1];

    return a;
}

test "separate" {
    const pp = separate(10, .{ 20, 30, 40 });
    try testing.expectEqual(.{ 20, 10, 30, 10, 40 }, pp);
}

pub fn main() !void {}

Modify the code somewhat, though, and it stops working (below).

The error is a bit more descriptive, though.

main.zig:130:18: error: expected type 'main.Concat(struct { comptime main.Empty = .{}, comptime main.One = .{}, main.Many, comptime main.One = .{}, comptime main.One = .{} })', found 'main.Concat(.{ undefined, undefined, .{ ... }, undefined, undefined })'
    return concat(makesep(sep, printers));
           ~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~
main.zig:45:12: note: struct declared here (2 times)
    return struct {
           ^~~~~~
main.zig:129:76: note: function return type declared here
pub fn separate(comptime sep: anytype, comptime printers: anytype) Separate(sep, printers) {
                                                                   ~~~~~~~~^~~~~~~~~~~~~~~

Why would I get undefined as a type when assigning to my newly created tuple?

const std = @import("std");

const io = std.io;
const mem = std.mem;
const meta = std.meta;
const testing = std.testing;

pub const Error = error{InvalidNewline} || mem.Allocator.Error;

const Empty = struct {
    const Self = @This();

    fn width(_: Self) usize {
        return 0;
    }
};

pub const empty = Empty{};

const One = struct {
    const Self = @This();

    fn width(_: Self) usize {
        return 1;
    }
};

pub const one = One{};

const Many = struct {
    n: usize,

    const Self = @This();

    fn width(self: Self) usize {
        return self.n;
    }
};

pub fn many(comptime n: usize) Many {
    return Many{ .n = n };
}

pub fn Concat(comptime printers: anytype) type {
    return struct {
        pub const T = @TypeOf(printers);

        const Self = @This();

        fn width(_: Self) usize {
            var w: usize = 0;
            inline for (printers) |p| {
                w += p.width();
            }
            return w;
        }
    };
}

pub fn concat(comptime printers: anytype) Concat(printers) {
    return Concat(printers){};
}

test "concat" {
    const pp = concat(.{ empty, one, many(3) });
    std.debug.print("concat: pp = {any}\n", .{@TypeOf(pp)});
    try testing.expectEqual(4, pp.width());
}

fn sepTypes(comptime sep: anytype, comptime printers: anytype) type {
    const info = switch (@typeInfo(@TypeOf(printers))) {
        .@"struct" => |s| s,
        else => @compileError("Not a tuple!"),
    };
    const len = info.fields.len;

    if (len == 0)
        @compileError("No printers to separate!");

    var a: []const type = &[_]type{};

    if (len > 1) {
        const TS = @TypeOf(sep);

        for (0..len - 1) |i| {
            const T = @TypeOf(printers[i]);
            a = a ++ [_]type{T};
            a = a ++ [_]type{TS};
        }
    }

    const T = @TypeOf(printers[len - 1]);
    a = a ++ [_]type{T};

    return meta.Tuple(a);
}

pub fn makesep(comptime sep: anytype, comptime printers: anytype) sepTypes(sep, printers) {
    const info = switch (@typeInfo(@TypeOf(printers))) {
        .@"struct" => |s| s,
        else => @compileError("Not a tuple!"),
    };
    const len = info.fields.len;
    //const len = printers.len;
    const n = comptime if (len == 1) 1 else (len * 2 - 1);
    comptime var a: sepTypes(sep, printers) = undefined;

    if (len > 1) {
        inline for (0..len - 1) |i| {
            a[i * 2] = printers[i];
            a[i * 2 + 1] = sep;
        }
    }

    a[n - 1] = printers[len - 1];

    return a;
}

test "makesep" {
    const pp = makesep(10, .{ 20, 30, 40 });
    try testing.expectEqual(.{ 10, 10, 30, 10, 40 }, pp);
}

pub fn Separate(comptime sep: anytype, comptime printers: anytype) type {
    return Concat(sepTypes(sep, printers));
}

pub fn separate(comptime sep: anytype, comptime printers: anytype) Separate(sep, printers) {
    return concat(makesep(sep, printers));
}

test "separate" {
    const pp = separate(one, .{ empty, many(3), one });
    try testing.expectEqual(5, pp.width());
}

pub fn main() !void {}

With these things I find it simplifies the code, if you make the one-element the base case and then adding separator + next-element in the induction step.

I also changed the code to convert the anytype parameters to more concrete types before passing them to the type creating function.

const std = @import("std");
const meta = std.meta;
const StructField = std.builtin.Type.StructField;

fn Separate(comptime Sep: type, comptime printers: []const StructField) type {
    if (printers.len == 0) @compileError("No printers to separate!");

    var a: []const type = &[_]type{printers[0].type};
    if (printers.len > 1) {
        for (printers[1..]) |p| {
            a = a ++ [_]type{ Sep, p.type };
        }
    }

    return meta.Tuple(a);
}

pub fn separate(comptime sep: anytype, comptime printers: anytype) Separate(@TypeOf(sep), meta.fields(@TypeOf(printers))) {
    comptime var a: Separate(@TypeOf(sep), meta.fields(@TypeOf(printers))) = undefined;
    a[0] = printers[0];

    const len = printers.len;
    if (len > 1) {
        inline for (1..len) |p| {
            const i = p * 2;
            a[i - 1] = sep;
            a[i] = printers[p];
        }
    }

    return a;
}

test "separate" {
    const t = std.testing;
    try t.expectEqual(
        .{ 20, 10, 30, 10, 40 },
        separate(10, .{ 20, 30, 40 }),
    );
    try t.expectEqual(
        .{20},
        separate(10, .{20}),
    );
}

Thanks for trying!

It still doesn’t work for more complex types passed in as printers. Please take a look at my previous message. I get pretty much the same error if I replace my implementations of Separate and separate with yours.

My implementation Injecting new values into a comptime tuple - #10 by joelreymont

Consider the primitives empty, one and many. The idea is to combine them using concat or separate into a top-level combinator pp such that pp.width() would return the total width.

You may build pp like this

    const pp = concat(.{ empty, one, many(3) });

or like this

    const pp = separate(one, .{ empty, many(3), one });

Actually, using concat works fine since the printers tuple doesn’t need to be modified. Everything breaks down when trying to modify the printers tuple and insert separators.

Partial code below. Full code in Injecting new values into a comptime tuple - #10 by joelreymont

const Empty = struct {
    const Self = @This();

    fn width(_: Self) usize {
        return 0;
    }
};

pub const empty = Empty{};

const One = struct {
    const Self = @This();

    fn width(_: Self) usize {
        return 1;
    }
};

pub const one = One{};

const Many = struct {
    n: usize,

    const Self = @This();

    fn width(self: Self) usize {
        return self.n;
    }
};

pub fn many(comptime n: usize) Many {
    return Many{ .n = n };
}

pub fn Concat(comptime printers: anytype) type {
    return struct {
        pub const T = @TypeOf(printers);

        const Self = @This();

        fn width(_: Self) usize {
            var w: usize = 0;
            inline for (printers) |p| {
                w += p.width();
            }
            return w;
        }
    };
}

pub fn concat(comptime printers: anytype) Concat(printers) {
    return Concat(printers){};
}

If I modify makesep like this

            a[i * 2] = 0; //printers[i];
            a[i * 2 + 1] = 1; //sep;

then I get the following error

main.zig:110:24: error: expected type 'main.Empty', found 'comptime_int'
            a[i * 2] = 0; //printers[i];
                       ^

So the compiler knows the appropriate type for each element of a.

How does this type become undefined in the end, though?

Printing the type of a right before returning it, e.g. at the end of makesep

    a[n - 1] = printers[len - 1];

    @compileLog("Type of a is ", @TypeOf(a));

    return a;

gives

@as(*const [13:0]u8, "Type of a is "), @as(type, struct { comptime main.Empty = .{}, comptime main.One = .{}, main.Many, comptime main.One = .{}, comptime main.One = .{} })

so the type is correct. It does become main.Concat(.{ undefined, undefined, .{ ... }, undefined, undefined }) after a is returned, though.

I think std.meta.Tuple doesn’t work for this use case because it uses CreateUniqueTuple which uses this for the fields:

.default_value = null,
.is_comptime = false,

This means that the tuples created by std.meta.Tuple aren’t equivalent to something like .{1, 2} which results in a tuple where the fields are comptime and the default value is set to the comptime values 1 and 2, instead it creates a type for runtime-tuples with no default values.

I think instead you need something like @castholm has suggested which preserves the comptime information of the original tuple fields, I made small tweaks to withSeparator so that the resulting tuple fields have is_comptime everywhere and preserve the default_value for all fields and the separator,
this is required so that the resulting tuple will actually work in an inline for loop.

So here is the adapted code:

const std = @import("std");

const io = std.io;
const mem = std.mem;
const meta = std.meta;
const testing = std.testing;

pub const Error = error{InvalidNewline} || mem.Allocator.Error;

const Empty = struct {
    const Self = @This();

    fn width(_: Self) usize {
        return 0;
    }
};

pub const empty = Empty{};

const One = struct {
    const Self = @This();

    fn width(_: Self) usize {
        return 1;
    }
};

pub const one = One{};

const Many = struct {
    n: usize,

    const Self = @This();

    fn width(self: Self) usize {
        return self.n;
    }
};

pub fn many(comptime n: usize) Many {
    return Many{ .n = n };
}

pub fn Concat(comptime printers: anytype) type {
    return struct {
        pub const T = @TypeOf(printers);

        const Self = @This();

        fn width(_: Self) usize {
            var w: usize = 0;
            inline for (printers) |p| {
                w += p.width();
            }
            return w;
        }
    };
}

pub fn concat(comptime printers: anytype) Concat(printers) {
    return Concat(printers){};
}

test "concat" {
    const pp = concat(.{ empty, one, many(3) });
    std.debug.print("concat: pp = {any}\n", .{@TypeOf(pp)});
    try testing.expectEqual(4, pp.width());
}

pub fn WithSeparator(comptime tuple: anytype, comptime separator: anytype) type {
    const Tuple = @TypeOf(tuple);
    const Separator = @TypeOf(separator);

    const old_info = @typeInfo(Tuple).Struct;

    if (old_info.fields.len <= 1) return tuple;

    var new_fields: [2 * old_info.fields.len - 1]std.builtin.Type.StructField = undefined;
    var field_index: usize = 0;
    for (old_info.fields) |old_field| {
        new_fields[field_index] = .{
            .name = std.fmt.comptimePrint("{d}", .{field_index}),
            .type = old_field.type,
            .default_value = old_field.default_value,
            .is_comptime = old_field.is_comptime,
            .alignment = old_field.alignment,
        };
        field_index += 1;
        if (field_index < new_fields.len) {
            new_fields[field_index] = .{
                .name = std.fmt.comptimePrint("{d}", .{field_index}),
                .type = Separator,
                .default_value = &separator,
                .is_comptime = true,
                .alignment = 0,
            };
            field_index += 1;
        }
    }

    return @Type(.{ .Struct = .{
        .layout = old_info.layout,
        .backing_integer = old_info.backing_integer,
        .fields = &new_fields,
        .decls = &.{},
        .is_tuple = old_info.is_tuple,
    } });
}

pub fn withSeparator(tuple: anytype, separator: anytype) WithSeparator(tuple, separator) {
    var result: WithSeparator(tuple, separator) = undefined;
    comptime var field_index: usize = 0;
    inline for (tuple) |x| {
        result[field_index] = x;
        field_index += 1;
        if (field_index < result.len) {
            result[field_index] = separator;
            field_index += 1;
        }
    }
    return result;
}

pub fn Separate(comptime sep: anytype, comptime printers: anytype) type {
    return Concat(withSeparator(printers, sep));
}

pub fn separate(comptime sep: anytype, comptime printers: anytype) Separate(sep, printers) {
    return concat(withSeparator(printers, sep));
}

test "separate" {
    const pp = separate(one, .{ empty, many(3), one });
    // try testing.expectEqual(concat(.{ empty, one, many(3), one, one }), pp); // doesn't work because of subtle type differences
    // figuring out the correct types here isn't fun anymore with error messages like this:
    // error: incompatible types: 'tuplesep2.Concat(.{ .{ ... }, .{ ... }, .{ ... }, .{ ... }, .{ ... } })' and 'tuplesep2.Concat(.{ .{ ... }, .{ ... }, .{ ... }, .{ ... }, .{ ... } })'
    //     const T = @TypeOf(expected, actual);
    //               ^~~~~~~~~~~~~~~~~~~~~~~~~
    // note: type 'tuplesep2.Concat(.{ .{ ... }, .{ ... }, .{ ... }, .{ ... }, .{ ... } })' here
    //     const T = @TypeOf(expected, actual);
    //                       ^~~~~~~~
    // note: type 'tuplesep2.Concat(.{ .{ ... }, .{ ... }, .{ ... }, .{ ... }, .{ ... } })' here
    //     const T = @TypeOf(expected, actual);
    //                                 ^~~~~~
    try testing.expectEqual(6, pp.width());
}

pub fn main() !void {}

With horrible error messages like shown in the comment it is definitely not fun to work on this anymore, but I think this is actually a good thing, because these sort of comptime shenanigans go towards using too much meta programming.

I don’t really understand what a complicated library to print things gets us?

I think it is better to just write the imperative code, instead of inventing some functional combinator thing, which then needs to use way to complex meta-programming, just to generate imperative code again in the end.

It is much better to have a little bit of repeated imperative code, than create abstractions that ultimately aren’t really needed.

I think it would be fine to create a library that helps with printing strings with separators and so on, but that could just use simple runtime code, why do that at comptime?

I found the problem.

Will post the solution shortly.

It requires changing one line of code.

I’ll make an announcement here once I finish the library, together with some examples.

1 Like

This is the old code

pub fn Separate(comptime sep: anytype, comptime printers: anytype) type {
    return Concat(sepTypes(sep, printers));
}

pub fn separate(comptime sep: anytype, comptime printers: anytype) Separate(sep, printers) {
    return concat(makesep(sep, printers));
}

Notice that I’m passing a tuple of types (sepTypes(sep, printers)) to Concat and not the original arguments. This is what strips type info down the road.

    return concat(makesep(sep, printers));

This is the new code and it works

pub fn Separate(comptime sep: anytype, comptime printers: anytype) type {
    return Concat(makesep(sep, printers));
}

pub fn separate(comptime sep: anytype, comptime printers: anytype) Separate(sep, printers) {
    return Separate(sep, printers){};
}

The key change is

    return Concat(makesep(sep, printers));

which preserves types and lets Concat do its own type introspection.

My transpiler parses one language into an AST, lifts that into IR and generates a C++ syntax tree. The printer library will let me neatly output that final C++, as well as debug the AST and the IR by printing them nicely.

Well, the type chicanery is corralled into a few small functions. The rest of the library should be simple and the type machinery completely invisible to the user. Take a look at the mecha parser combinator library if you want to see complicated!

Took me a while to grok it but I was able to extend it with location information. I now use it for parsing since there are no parser generators for Zig yet.