I’ll try and explain what I need to get working.
In the base code I work with the Terms array.
Now I am making a function which has to extract the name of one of the arrays, when passing the address of one ScorePair argument using the global var terms.
pub fn using_scorepair(sp: *const ScorePair) void {
// in which array resides this sp argument (kind of doable if the compiled order is the same)
// what is the declaration name of that array inside Terms (without if if if if)
}
pub const terms: *const Terms = &default_terms; // default_terms not included here.
Why not use an array with one items instead of using sometimes arrays, sometime the struct directly ? It would reduce the complexity of your monster, with no runtime costs since the array will be embedded in the struct anyways.
I think you’re reading it incorrectly? It’s actually always just an O(1) array access at runtime. The comptime monster above is just code that generates that array.
EDIT: Oh I’m actually the one reading your comment incorrectly. I can’t just use a flat array of ScorePair, I think? I need to get the field names from somewhere.
Hmm, some of the code doesn’t actually need to be there, but it checks the assumptions that make it work. Specifically that the parent struct only consists of fields of type ScorePair or (n-dimensional) arrays thereof, and that we haven’t been given a bogus pointer to ScorePair.
I could have made the code generic (while actually being simpler, code-wise), using an array that maps to individual bytes of the struct. The field types wouldn’t matter then, but the array would be 4x bigger.
Come to think of it, it still makes an assumption that Zig will actually just pack the structs together and not add some magic u16s in between, which would throw off the alignment. It would probably be better to change Terms and ScorePair into extern structs to guarantee the assumed layout (should have no runtime cost, as the structs are basically arrays with named fields).
That is very true. Currently everything aligns by luck.
And what if the struct needs types which are not allowed in them? Like a zig-enum.
Is a struct actually controllable regarding layout? Without packing.
No, the layout of a Zig struct is not controllable, basically whatsoever, apart from being able to use align to control the alignment of certain fields – it’s explicitly made so Zig can do whatever to make the layout optimal.
Controlling the layout is what extern struct is for. As for enum, I believe if you set a tag type for them (e.g. enum(u8)), you can then put them in extern structs.
Well, there are multiple solutions:
If extern struct is good enough for you, then you just change these structs to that and use the original code I put here;
OR, you can set ScorePair’s alignment to align(4), which should make the original assumption hold as well, I believe;
OR, you can edit the code so that it actually only assumes u16’s natural alignment (makes the array 2x bigger);
zig still has to respect the alignment of fields, including the default type alignment, otherwise you couldn’t reliably get a valid pointer to fields since zig pointers depend on a comptime known (minimum) alignment.
Modifying the code to not rely on the layout is a preferable solution over making the type extern.
I’ll make a fixed version tomorrow, if no one else has
Yes, but my original solution relies on ScorePair being aligned to 4 bytes (as it would in an extern struct), while it’s only guaranteed to be aligned to 2.
I think you can reduce the static executable size by splitting it into two arrays one with the strings and another with the indices which then can use a smaller type.
fn scorePairName(terms: *const Terms, pair: *const ScorePair) []const u8 {
const names = std.meta.fieldNames(Terms);
const indices = comptime blk: {
var count: usize = 0;
for (std.meta.fields(Terms)) |field| {
const FT = if (field.type == ScorePair) [1]ScorePair else field.type;
if (@typeInfo(FT) != .array)
@compileError("Field '" ++ field.name ++ "' is not a (array of) ScorePair");
count += @as(FT, undefined).len;
}
if (count == 0) @compileError("Empty struct not supported");
const Index = std.math.IntFittingRange(0, count - 1);
var indices: [count]Index = undefined;
var current: Index = 0;
for (std.meta.fields(Terms), 0..) |field, i| {
const FT = if (field.type == ScorePair) [1]ScorePair else field.type;
const start_idx = current;
current += @as(FT, undefined).len;
const end_idx = current;
indices[start_idx..end_idx].* = @splat(i);
}
break :blk indices;
};
const start = @intFromPtr(terms);
const current = @intFromPtr(pair);
std.debug.assert(current >= start);
const idx = (current - start) / @sizeOf(ScorePair);
std.debug.assert(idx < indices.len);
return names[indices[idx]];
}
You probably also could go further by generating a bitfield that has a 1 set for every position where the index increases and then you could do a popcount of all bits up to the queried position to calculate the index. I think that would also make it easy to write an iterator that enumerates all ScorePairs while also keeping the index of the current field name.
If you would go with the iterator, you also could create the array of names with the array of counts and then when accessing a specific index you would create the iterator and let it count (it would internally have a field index and sub index for the field within that field’s array and a running total of the previous counts).