fix: comptime field reflection on tuples/arrays/vectors (issue 0195)

`field_count` / `field_name` were broken on every non-struct/enum aggregate:
`field_count(Tuple(i64, bool))` silently returned 0 (a missing `.tuple` arm in
the count switches), and `field_name(tuple/array/vector, i)` SEGFAULTED — the
LLVM backend built a zero-length `[0 x string]` name array for those kinds while
sizing the runtime GEP at the (often non-zero) member count, so the indexed load
ran past the array.

Root cause was three+ parallel switches that each had to know how to count an
aggregate's members, and disagreed: `field_count` lowering and `memberCount` had
struct/union/tagged_union/enum/array/vector but no `.tuple`; the backend's
`field_name_get` build + GEP sizing had neither `.tuple` nor `.array`/`.vector`.

Fix:
- add the `.tuple` arm to `field_count` lowering (src/ir/lower/call.zig) and
  `TypeTable.memberCount` (src/ir/types.zig; this also backs the COMPILER-API
  `type_field_count` VM reader).
- unify the LLVM backend onto the single source of truth: both
  `getOrBuildFieldNameArray` (reflection.zig) and `emitFieldNameGet`'s GEP sizing
  (ops.zig) now derive from `memberCount` / `memberName`, so the name-array
  length and the GEP array type can never diverge again — for any kind. A member
  with no name (positional-tuple / array / vector element) reflects as "" (one
  slot per member, always in-bounds); named-tuple elements recover their labels.

The array/vector clone was surfaced by adversarial review of the tuple-only fix.

Regression: examples/comptime/0646-comptime-field-reflect-tuple-array.sx exercises
field_count/field_name/field_type over struct, enum, positional + named tuple,
array, and vector. Full suite green (818/0). Unblocks the `race` synthesis, which
must reflect a named tuple's labels + element types.
This commit is contained in:
agra
2026-06-26 12:28:09 +03:00
parent f3f061ef00
commit 8ac6c573e8
9 changed files with 220 additions and 26 deletions

View File

@@ -2477,15 +2477,12 @@ pub const Ops = struct {
const global = self.e.reflection().getOrBuildFieldNameArray(fr.struct_type);
const idx = self.e.resolveRef(fr.index);
const string_ty = self.e.getStringStructType();
// Get struct field count for array type
const field_info = self.e.ir_mod.types.get(fr.struct_type);
const field_count: u32 = switch (field_info) {
.@"struct" => |s| @intCast(s.fields.len),
.@"union" => |u| @intCast(u.fields.len),
.tagged_union => |u| @intCast(u.fields.len),
.@"enum" => |e| @intCast(e.variants.len),
else => 0,
};
// Size the GEP's array type from the SAME single source of truth
// (`memberCount`) that `getOrBuildFieldNameArray` uses to build the name
// array, so the two can never disagree (a mismatch was issue 0195: the
// array was built zero-length for tuples/arrays while this count said N →
// an out-of-bounds GEP → segfault).
const field_count: u32 = @intCast(self.e.ir_mod.types.memberCount(fr.struct_type) orelse 0);
const array_ty = c.LLVMArrayType(string_ty, field_count);
const zero = c.LLVMConstInt(self.e.cached_i64, 0, 0);
var indices = [2]c.LLVMValueRef{ zero, idx };

View File

@@ -95,25 +95,22 @@ pub const Reflection = struct {
pub fn getOrBuildFieldNameArray(self: Reflection, struct_type: TypeId) c.LLVMValueRef {
if (self.e.field_name_arrays.get(struct_type.index())) |g| return g;
const info = self.e.ir_mod.types.get(struct_type);
// Collect name StringIds from struct fields, union fields, or enum variants
// Collect one name StringId per member, driven by the SINGLE source of
// truth `memberCount`/`memberName` (types.zig) — NOT a per-kind switch
// here. This guarantees the array length always matches `emitFieldNameGet`'s
// GEP sizing (which also derives from `memberCount`), so a kind covered by
// one but not the other can never reappear (that mismatch was issue 0195:
// tuples/arrays counted N members but built a zero-length name array → an
// out-of-bounds GEP → segfault). A member with no name (positional tuple
// element, array/vector element) yields `.empty` → "", keeping one slot
// per member so `field_name(T, i)` is always in-bounds.
const n_members: i64 = self.e.ir_mod.types.memberCount(struct_type) orelse 0;
var name_ids = std.ArrayList(StringId).empty;
defer name_ids.deinit(self.e.alloc);
switch (info) {
.@"struct" => |s| {
for (s.fields) |f| name_ids.append(self.e.alloc, f.name) catch unreachable;
},
.@"union" => |u| {
for (u.fields) |f| name_ids.append(self.e.alloc, f.name) catch unreachable;
},
.tagged_union => |u| {
for (u.fields) |f| name_ids.append(self.e.alloc, f.name) catch unreachable;
},
.@"enum" => |e| {
for (e.variants) |v| name_ids.append(self.e.alloc, v) catch unreachable;
},
else => {},
var mi: i64 = 0;
while (mi < n_members) : (mi += 1) {
const nid: StringId = self.e.ir_mod.types.memberName(struct_type, mi) orelse .empty;
name_ids.append(self.e.alloc, nid) catch unreachable;
}
const string_ty = self.e.getStringStructType();

View File

@@ -2193,6 +2193,7 @@ pub fn tryLowerReflectionCall(self: *Lowering, name: []const u8, c: *const ast.C
.@"union" => |u| @intCast(u.fields.len),
.tagged_union => |u| @intCast(u.fields.len),
.@"enum" => |e| @intCast(e.variants.len),
.tuple => |t| @intCast(t.fields.len),
.array => |a| @intCast(a.length),
.vector => |v| @intCast(v.length),
else => 0,

View File

@@ -529,6 +529,7 @@ pub const TypeTable = struct {
.@"union" => |u| @intCast(u.fields.len),
.tagged_union => |u| @intCast(u.fields.len),
.@"enum" => |e| @intCast(e.variants.len),
.tuple => |t| @intCast(t.fields.len),
.array => |a| @intCast(a.length),
.vector => |v| @intCast(v.length),
else => null,