feat(metatype): widen type_info/define to struct types

TypeInfo gains a `struct(StructInfo) variant (StructField{name,type});
the metatype system now reflects AND constructs structs, not just enums.

- meta.sx: StructField / StructInfo / `struct TypeInfo variant.
- interp: reflectTypeInfo builds .struct (tag 1) for a source @"struct";
  define dispatches on the TypeInfo tag (defineType) -> defineEnum (0) /
  defineStruct (1). defineStruct mirrors defineEnum (dup-field-name check
  included) but completes the declare slot AS a struct via replaceKeyedInfo
  (a kind change re-keys the intern map; updatePreservingKey asserts no
  key change, true only for the enum path).
- call.zig: the lower-time type_info guard now admits @"struct".

define(declare("P"), .struct(.{ fields = .[ … ] })) builds a struct, and
define(declare("C"), type_info(SrcStruct)) round-trips one. Suite green
(682); enum path (0619) unchanged.
This commit is contained in:
agra
2026-06-17 06:54:17 +03:00
parent afb1fee252
commit aaac019715
3 changed files with 121 additions and 19 deletions

View File

@@ -2039,7 +2039,7 @@ pub const Interpreter = struct {
const handle = frame.getRef(bi.args[0]).asTypeId() orelse
return bailDetail("comptime define(): first argument is not a Type handle (use a `declare()` result)");
const info_val = frame.getRef(bi.args[1]);
return self.defineEnum(tbl, handle, info_val);
return self.defineType(tbl, handle, info_val);
},
.type_info => {
// Reflect a type INTO a `TypeInfo` value — the inverse of
@@ -2066,34 +2066,68 @@ pub const Interpreter = struct {
fn reflectTypeInfo(self: *Interpreter, tid: TypeId) InterpError!ExecResult {
var elems = std.ArrayList(Value).empty;
const info = self.module.types.get(tid);
switch (info) {
.tagged_union => |u| {
// The TypeInfo variant tag (declaration order in `meta.sx`: `enum`=0,
// `struct`=1). Each member reflects as `{ string(name), type_tag(ty) }`
// regardless of kind — payload type for an enum variant, field type for a
// struct field (a payloadless `@"enum"` variant carries `void`).
const tag: i64 = switch (info) {
.tagged_union => |u| blk: {
for (u.fields) |f| {
const nm = self.alloc.dupe(u8, self.module.types.getString(f.name)) catch return error.CannotEvalComptime;
const pair = self.alloc.dupe(Value, &.{ .{ .string = nm }, .{ .type_tag = f.ty } }) catch return error.CannotEvalComptime;
elems.append(self.alloc, .{ .aggregate = pair }) catch return error.CannotEvalComptime;
}
break :blk 0;
},
.@"enum" => |e| {
.@"enum" => |e| blk: {
for (e.variants) |vname| {
const nm = self.alloc.dupe(u8, self.module.types.getString(vname)) catch return error.CannotEvalComptime;
const pair = self.alloc.dupe(Value, &.{ .{ .string = nm }, .{ .type_tag = .void } }) catch return error.CannotEvalComptime;
elems.append(self.alloc, .{ .aggregate = pair }) catch return error.CannotEvalComptime;
}
break :blk 0;
},
else => return bailDetail("comptime type_info: only enum/tagged-union types reflect today"),
}
if (elems.items.len == 0) return bailDetail("comptime type_info: type has no variants");
.@"struct" => |s| blk: {
for (s.fields) |f| {
const nm = self.alloc.dupe(u8, self.module.types.getString(f.name)) catch return error.CannotEvalComptime;
const pair = self.alloc.dupe(Value, &.{ .{ .string = nm }, .{ .type_tag = f.ty } }) catch return error.CannotEvalComptime;
elems.append(self.alloc, .{ .aggregate = pair }) catch return error.CannotEvalComptime;
}
break :blk 1;
},
else => return bailDetail("comptime type_info: only enum / tagged-union / struct types reflect today"),
};
if (elems.items.len == 0) return bailDetail("comptime type_info: type has no members");
const variants_slice = self.alloc.dupe(Value, &.{
// Wrap: members → `{ data, len }` slice → info struct `{ members }` →
// TypeInfo `{ int(tag), info }`. Identical shape for `.enum` / `.struct`.
const members_slice = self.alloc.dupe(Value, &.{
.{ .aggregate = elems.items },
.{ .int = @intCast(elems.items.len) },
}) catch return error.CannotEvalComptime;
const einfo = self.alloc.dupe(Value, &.{.{ .aggregate = variants_slice }}) catch return error.CannotEvalComptime;
const typeinfo = self.alloc.dupe(Value, &.{ .{ .int = 0 }, .{ .aggregate = einfo } }) catch return error.CannotEvalComptime;
const inner = self.alloc.dupe(Value, &.{.{ .aggregate = members_slice }}) catch return error.CannotEvalComptime;
const typeinfo = self.alloc.dupe(Value, &.{ .{ .int = tag }, .{ .aggregate = inner } }) catch return error.CannotEvalComptime;
return .{ .value = .{ .aggregate = typeinfo } };
}
/// Complete a `declare()`d slot from a `TypeInfo` VALUE, dispatching on the
/// TypeInfo tag (`{ tag, payload }`): `0` → `.enum(EnumInfo)` (tagged_union),
/// `1` → `.struct(StructInfo)`. The tag is the variant index in `meta.sx`'s
/// `TypeInfo` enum declaration order (`enum` then `struct`).
fn defineType(self: *Interpreter, tbl: *types.TypeTable, handle: TypeId, info_val: Value) InterpError!ExecResult {
const ti_fields = switch (info_val) {
.aggregate => |f| f,
else => return bailDetail("comptime define(): info did not evaluate to a TypeInfo value"),
};
if (ti_fields.len != 2) return bailDetail("comptime define(): malformed TypeInfo value (expected `{ tag, info }`)");
const tag = ti_fields[0].asInt() orelse return bailDetail("comptime define(): TypeInfo tag is not an integer");
return switch (tag) {
0 => self.defineEnum(tbl, handle, info_val),
1 => self.defineStruct(tbl, handle, info_val),
else => bailDetail("comptime define(): unknown TypeInfo variant (only `.enum` / `.struct` are supported)"),
};
}
/// Complete a `declare()`d slot from a `TypeInfo` VALUE. The value is the
/// `.enum(EnumInfo)` tagged-union (`{ tag, EnumInfo }`), EnumInfo is
/// `{ variants }`, and each variant is `{ name: string, payload: Type }`.
@@ -2160,6 +2194,61 @@ pub const Interpreter = struct {
// Return the handle so the one-shot form chains: `T :: define(declare("T"), info)`.
return .{ .value = .{ .type_tag = handle } };
}
/// Complete a `declare()`d slot from a `.struct(StructInfo)` `TypeInfo` VALUE.
/// Mirror of `defineEnum` for structs: StructInfo is `{ fields }`, each field
/// `{ name: string, type: Type }`. Fills the (tagged_union-shaped) declare
/// slot in place as a `.@"struct"`, preserving its name + nominal id.
fn defineStruct(self: *Interpreter, tbl: *types.TypeTable, handle: TypeId, info_val: Value) InterpError!ExecResult {
// Unwrap TypeInfo `.struct(StructInfo)` → StructInfo `{ fields }`.
const ti_fields = info_val.aggregate; // defineType already checked the shape
const sinfo = ti_fields[1];
const sinfo_fields = switch (sinfo) {
.aggregate => |f| f,
else => return bailDetail("comptime define(): `.struct` payload is not a StructInfo struct value"),
};
if (sinfo_fields.len != 1) return bailDetail("comptime define(): StructInfo must have a `fields` field");
const elems = decodeVariantElements(sinfo_fields[0]) orelse
return bailDetail("comptime define(): `fields` is not a slice/array of StructField");
if (elems.len == 0) return bailDetail("comptime define(): struct has no fields");
var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
for (elems) |elem| {
const sf = switch (elem) {
.aggregate => |f| f,
else => return bailDetail("comptime define(): StructField did not evaluate to a struct value"),
};
if (sf.len != 2) return bailDetail("comptime define(): StructField must have `name` and `type`");
const fname = sf[0].asString(self) orelse return bailDetail("comptime define(): StructField `name` is not a string");
const fty = sf[1].asTypeId() orelse return bailDetail("comptime define(): StructField `type` is not a Type value");
const fname_id = tbl.internString(fname);
// Reject duplicate field names (a struct can't have two same-named
// fields). Dynamic name → set the detail directly (bailDetail is
// comptime-only); evalComptimeType renders it.
for (fields.items) |existing| {
if (existing.name == fname_id) {
last_bail_detail = std.fmt.allocPrint(self.alloc, "comptime define(): duplicate field name '{s}'", .{fname}) catch "comptime define(): duplicate field name";
return error.CannotEvalComptime;
}
}
fields.append(self.alloc, .{ .name = fname_id, .ty = fty }) catch return error.CannotEvalComptime;
}
// Complete the declare slot as a struct. It was minted as an (empty)
// tagged_union by `declare`; we keep its TypeId + name + nominal id but
// SWAP THE KIND to struct. A kind change moves the intern key, so use
// `replaceKeyedInfo` (re-keys) rather than `updatePreservingKey` (which
// asserts the key is unchanged — true for the enum path, false here).
const cur = tbl.get(handle);
if (cur != .tagged_union) return bailDetail("comptime define(): handle is not a declare()'d slot");
const full: types.TypeInfo = .{ .@"struct" = .{
.name = cur.tagged_union.name,
.fields = fields.items,
.nominal_id = cur.tagged_union.nominal_id,
} };
tbl.replaceKeyedInfo(handle, full);
return .{ .value = .{ .type_tag = handle } };
}
};
/// Normalize an interpreter value into the list of EnumVariant element values.