How to return a comptime known array from a function with 0.12.0?

I have following function

    pub fn genSchema(comptime ent_type_tuple: anytype) []const TableSchema {
        comptime {
            var table_schemas: [max_capacity]TableSchema = undefined;

            var table_schema_gen_count: usize = 0;
            for (0..ent_type_tuple.len) |i| {
                table_schemas[i] = calcTableSchema(ent_type_tuple[i]);
                table_schema_gen_count += 1;
            } // mark 3

            var table_schemas_final: [table_schema_gen_count]TableSchema = undefined; // mark 2
            for (0..table_schema_gen_count) |i| {
                table_schemas_final[i] = table_schemas[i];
            }

            const table_schemas_const = table_schemas_final; // mark 1
            return &table_schemas_const;
        }
    }

The idea is that it will try to calculate an array of TableSchema struct in comptime based on ent_type_tuple, it is part of my own ORM code :). It can only know how many table schemas it will generate in comptime after computing (mark 3), but all info is comptime known, no run time thing. Later I will use like const my_schemas = comptime genSchema({ ... }). But with zig 0.12.0’s new comptime var rule, I see error like

error: function called at runtime cannot return value at comptime
            return &table_schemas_const;

My understanding is that with mark 2, I have already made a comptime fully known array, and then prompted in mark 1 (as instructed by 0.12.0 release notes), it should be safe enough to just return it (as hard code thing), but it can not.

I guess the problem is still on the return type, it is a slice now, and I should return an array. But the problem is that the size of the array can only be known after comptime calculation, how to write here? so confused.

Please enlighting me on how to write in 0.12.0 (as it used to work before 0.12.0). Thanks.

This answer doesn’t attend the issue of why the copy to a const doesn’t work, which I would also like to know why it doesn’t. But I think that geiven ent_type_tuple is comptime known, then ent_type_tuple.len is comptime known too and thus your return type can be the array [ent_type_tuple.len]TableSchema?

Resulting code:

pub fn genSchema(
    comptime ent_type_tuple: anytype,
) [ent_type_tuple.len]TableSchema {
    var table_schemas: [ent_type_tuple.len]TableSchema = undefined;

    for (0..ent_type_tuple.len) |i| {
        table_schemas[i] = calcTableSchema(ent_type_tuple[i]);
    } // mark 3

    return table_schemas;
}

If the .len causes an error (haven’t tried this code) you could then add an extra comptime parameter for the length:

pub fn genSchema(
    comptime ent_type_tuple: anytype,
    comptime len: comptime_int,
) [len]TableSchema {
//...

I think you need to use inline for instead of just for:

For loops can be inlined. This causes the loop to be unrolled, which allows the code to do some things which only work at compile time, such as use types as first class values. The capture value and iterator value of inlined for loops are compile-time known.

Also you can use @compileLog to print the different values, if it encounters a runtime known value it will print something like @as(u32, [runtime value]).
Often times you can use inline for.


In other cases you can use if or switch to branch on comptime known values for example if(comptime some_condition) doing this makes it true and comptime known in that branch, often times this makes it possible to lift things from being only known at run time, to also being known at compile time (in that branch).

(Because if you switch on the type/tag with for example an inline else prong in a switch, the type/tag becomes known because that branch will only be taken for that type/tag) Well actually the branch gets compiled into the instantiation/specialization of the generic function, while the non matching ones disappear.

thanks for the suggestion, but I am afraid that the ent_type_tuple.len does not always equal to the final table schemas. A simple example in relational db is when to build a multi to multi relationship, 2 items in ent_type_tuple will result 3 tables. So still in comptime the returned array len is got after calculation.

also thanks first. The whole body of this function is wrapped in comptime block, so I guess the for inside is already inline for, infact if place an inline keyword before my for loop, zig will complain it is unnecessary.

Hmm ok. Then what about printing everything with @compileLog to see where the runtime value comes from?

Just curious, but how do you call this function? Are you calling it in a comptime scope? Or are you looking to only do the comptime calculations at comptime and call the function at runtime - if that makes sense?

Seems like it may not like the fact that the return is in the comptime block (and potentially being called at runtime)? What if you moved the return to the outside of the comptime scope since pointers are inherently a runtime thing, are they not? If you needed to copy the table_schemas_const wouldn’t you just return it without the &? I assumed that slice coercion in this case happened without the &, but I’m just shooting blind here. Ignore this if it doesn’t pertain to your problem, or if I am misunderstanding any Zig syntax.

here are 3 lines from my testing code

const tables = comptime SchemaUtil.genSchema(.{ User, Company, Skill });
const user_table = tables[0];
try testing.expectEqualSlices(u8, "User", user_table.table_name);

and in the code, User, Company, Skill are comptime known types (struct), which is used for analysis and generate Table Schema definition and later for helping generating SQL. So I was expecting the tables are just comptime calculated constants as they should be in that way.

This compiles and runs. See if it works for your program.

const std = @import("std");

const max_capacity = 10;

const TableSchema = struct {};

const User = struct {};
const Company = struct {};

fn calcTableSchema(comptime T: type) TableSchema {
    return switch (T) {
        User => .{},
        Company => .{},
        else => @compileError("WTF?"),
    };
}

pub fn genSchema(comptime ent_type_tuple: anytype) []const TableSchema {
    const table_schemas_const = blk: {
        var table_schemas: [max_capacity]TableSchema = undefined;

        comptime var table_schema_gen_count: usize = 0;
        inline for (0..ent_type_tuple.len) |i| {
            table_schemas[i] = calcTableSchema(ent_type_tuple[i]);
            table_schema_gen_count += 1;
        } // mark 3

        var table_schemas_final: [table_schema_gen_count]TableSchema = undefined; // mark 2
        for (0..table_schema_gen_count) |i| {
            table_schemas_final[i] = table_schemas[i];
        }

        break :blk table_schemas_final;
    };

    return &table_schemas_const;
}

pub fn main() !void {
    const schemas = genSchema(.{ User, Company });
    std.debug.print("{any}\n", .{schemas});
}

thanks you all. I have figured out what’s wrong in my code. The original way of returning comptime array as slice has no problem, the problem is mostly recursive: which means if anything nested in depth of the last returned struct or struct inside array, will be reported as comptime value used in runtime. Somehow zig 0.12.0 now can not track this in a more helpful way, so any place if missed one assigning comptime var to comptime const before returning could results this. Later I will write a more detailed analysis of this.

1 Like