Files
sx/src/ir/types.zig
agra 79419b99bd issue-0028: ?Protocol = null sentinel-shaped optional protocols
Protocol structs registered via registerProtocolDecl carry a new
is_protocol flag; the ?T paths in sizeOf/typeSizeBytes/toLLVMType
recognise it and lay out ?Protocol as the protocol struct itself
(ctx == null IS the "none" state), matching how ?Closure / ?*T are
sentinel-shaped — no extra storage.

Method dispatch on ?Protocol auto-unwraps in lowerCall's field-access
path; the unwrap is structurally a no-op so we just rebind obj_ty to
the payload type. resolveCallParamTypes extended for optional-protocol
receivers so enum-literal args (gpu.create_texture(.r8, ...)) get the
right target_type and don't silently collapse to tag=0 : s32 — same
issue-0031-class bug closed in Session 66, one type-system layer
deeper.

Library: UIRenderer / UIPipeline / GlyphCache migrated from the verbose
gpu: GPU = ---; has_gpu: bool pattern to gpu: ?GPU = null. set_gpu no
longer maintains a parallel bool flag.

Bundled: dock.sx threads delta_time as a struct field rather than via
a global pointer (cleanup unrelated to issue-0028, committed alongside).

Verified: 85/85 regression tests pass; iOS-sim chess + macOS chess
both render correctly post-migration.
2026-05-18 18:32:55 +03:00

753 lines
28 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const std = @import("std");
const Allocator = std.mem.Allocator;
// ── TypeId ──────────────────────────────────────────────────────────────
// Opaque handle into the TypeTable. First 16 slots are reserved for builtins.
pub const TypeId = enum(u32) {
// Builtin slots 015
void = 0,
bool = 1,
s8 = 2,
s16 = 3,
s32 = 4,
s64 = 5,
u8 = 6,
u16 = 7,
u32 = 8,
u64 = 9,
f32 = 10,
f64 = 11,
string = 12, // [:0]u8
any = 13,
noreturn = 14,
isize = 15,
usize = 16,
_, // user-defined types start at 17
pub const first_user: u32 = 17;
pub fn index(self: TypeId) u32 {
return @intFromEnum(self);
}
pub fn fromIndex(i: u32) TypeId {
return @enumFromInt(i);
}
pub fn isBuiltin(self: TypeId) bool {
return self.index() < first_user;
}
};
// ── TypeInfo ────────────────────────────────────────────────────────────
// Resolved type information stored in the TypeTable.
// Unlike the AST-level `types.Type` which uses string names for references,
// TypeInfo uses TypeId handles, making it fully resolved and internable.
pub const TypeInfo = union(enum) {
signed: u8, // bit width: 164
unsigned: u8,
f32,
f64,
void,
bool,
string, // [:0]u8 — fat pointer {ptr, len}
@"struct": StructInfo,
@"enum": EnumInfo,
@"union": UnionInfo,
tagged_union: TaggedUnionInfo,
array: ArrayInfo,
slice: SliceInfo,
pointer: PointerInfo,
many_pointer: ManyPointerInfo,
vector: VectorInfo,
function: FunctionInfo,
closure: ClosureInfo,
optional: OptionalInfo,
tuple: TupleInfo,
any,
protocol: ProtocolInfo,
noreturn,
usize,
isize,
pub const StructInfo = struct {
name: StringId,
fields: []const Field,
// True iff this struct backs a protocol value (registered via
// `registerProtocolDecl`). Used by the optional code path: a
// `?Protocol` is sentinel-shaped (the protocol struct itself,
// null ctx == none) rather than the standard `{T, i1}` discriminated
// layout — matching how `?Closure` works.
is_protocol: bool = false,
pub const Field = struct {
name: StringId,
ty: TypeId,
};
};
pub const EnumInfo = struct {
name: StringId,
variants: []const StringId,
is_flags: bool = false,
explicit_values: ?[]const i64 = null, // for flags (power-of-2) or custom values
backing_type: ?TypeId = null, // e.g. u32 for `enum u32 { ... }`
};
pub const UnionInfo = struct {
name: StringId,
fields: []const StructInfo.Field,
};
pub const TaggedUnionInfo = struct {
name: StringId,
fields: []const StructInfo.Field,
tag_type: TypeId, // tag integer type (e.g. .u32, .s64)
backing_type: ?TypeId = null, // enum struct backing (e.g. { tag: u32; _: u32; payload: [30]u32; })
explicit_tag_values: ?[]const i64 = null, // explicit variant values (e.g., quit :: 0x100)
};
pub const ArrayInfo = struct {
element: TypeId,
length: u32,
};
pub const SliceInfo = struct {
element: TypeId,
};
pub const PointerInfo = struct {
pointee: TypeId,
};
pub const ManyPointerInfo = struct {
element: TypeId,
};
pub const VectorInfo = struct {
element: TypeId,
length: u32,
};
pub const FunctionInfo = struct {
params: []const TypeId,
ret: TypeId,
call_conv: CallConv = .default,
};
pub const CallConv = enum { default, c };
pub const ClosureInfo = struct {
params: []const TypeId,
ret: TypeId,
};
pub const OptionalInfo = struct {
child: TypeId,
};
pub const TupleInfo = struct {
fields: []const TypeId,
names: ?[]const StringId,
};
pub const ProtocolInfo = struct {
name: StringId,
methods: []const Method,
pub const Method = struct {
name: StringId,
sig: TypeId, // function type
};
};
};
// ── StringId ────────────────────────────────────────────────────────────
pub const StringId = enum(u32) {
empty = 0,
_,
pub fn index(self: StringId) u32 {
return @intFromEnum(self);
}
};
// ── StringPool ──────────────────────────────────────────────────────────
// Intern strings for type/field/variant names. Deduplicates by content.
pub const StringPool = struct {
/// Maps string content → StringId for dedup. Keys point to owned allocations in `strings`.
map: std.StringHashMap(StringId),
/// Owned string data indexed by StringId. Each entry is separately heap-allocated.
strings: std.ArrayList([]const u8),
next_id: u32,
pub fn init(alloc: Allocator) StringPool {
var pool = StringPool{
.map = std.StringHashMap(StringId).init(alloc),
.strings = std.ArrayList([]const u8).empty,
.next_id = 1, // 0 is reserved for empty
};
// Slot 0 = empty string (not heap-allocated)
pool.strings.append(alloc, "") catch unreachable;
return pool;
}
pub fn deinit(self: *StringPool, alloc: Allocator) void {
// Free heap-allocated strings (skip slot 0 which is a string literal)
for (self.strings.items[1..]) |s| {
alloc.free(@constCast(s));
}
self.strings.deinit(alloc);
self.map.deinit();
}
pub fn intern(self: *StringPool, alloc: Allocator, str: []const u8) StringId {
if (str.len == 0) return .empty;
if (self.map.get(str)) |id| return id;
const id: StringId = @enumFromInt(self.next_id);
self.next_id += 1;
// Allocate a stable copy — used as both map key and lookup value
const owned = alloc.dupe(u8, str) catch unreachable;
self.strings.append(alloc, owned) catch unreachable;
self.map.put(owned, id) catch unreachable;
return id;
}
pub fn get(self: *const StringPool, id: StringId) []const u8 {
const idx = id.index();
if (idx >= self.strings.items.len) return "";
return self.strings.items[idx];
}
};
// ── TypeTable ───────────────────────────────────────────────────────────
// Holds all resolved types. Builtins in slots 015, user types interned from 16+.
pub const TypeTable = struct {
infos: std.ArrayList(TypeInfo),
strings: StringPool,
/// Maps TypeInfo → TypeId for dedup of structural types
intern_map: std.HashMap(TypeKey, TypeId, TypeKeyContext, 80),
alloc: Allocator,
/// Target pointer size in bytes (4 for wasm32, 8 for 64-bit targets).
pointer_size: u8 = 8,
pub fn init(alloc: Allocator) TypeTable {
var table = TypeTable{
.infos = std.ArrayList(TypeInfo).empty,
.strings = StringPool.init(alloc),
.intern_map = std.HashMap(TypeKey, TypeId, TypeKeyContext, 80).init(alloc),
.alloc = alloc,
};
// Pre-populate builtin slots 015 (must match TypeId enum order)
const builtins = [_]TypeInfo{
.void, // 0
.bool, // 1
.{ .signed = 8 }, // 2: s8
.{ .signed = 16 }, // 3: s16
.{ .signed = 32 }, // 4: s32
.{ .signed = 64 }, // 5: s64
.{ .unsigned = 8 }, // 6: u8
.{ .unsigned = 16 }, // 7: u16
.{ .unsigned = 32 }, // 8: u32
.{ .unsigned = 64 }, // 9: u64
.f32, // 10
.f64, // 11
.string, // 12
.any, // 13
.noreturn, // 14
.isize, // 15: isize (pointer-sized signed)
.usize, // 16: usize (pointer-sized unsigned)
};
for (&builtins) |info| {
table.infos.append(alloc, info) catch unreachable;
}
return table;
}
pub fn deinit(self: *TypeTable) void {
self.infos.deinit(self.alloc);
self.strings.deinit(self.alloc);
self.intern_map.deinit();
}
/// Look up the TypeInfo for a given TypeId.
pub fn get(self: *const TypeTable, id: TypeId) TypeInfo {
return self.infos.items[id.index()];
}
/// Intern a TypeInfo, returning the existing TypeId if structurally equal.
pub fn intern(self: *TypeTable, info: TypeInfo) TypeId {
const key = TypeKey{ .info = info };
if (self.intern_map.get(key)) |existing| {
return existing;
}
const id = TypeId.fromIndex(@intCast(self.infos.items.len));
self.infos.append(self.alloc, info) catch unreachable;
self.intern_map.putNoClobber(key, id) catch unreachable;
return id;
}
/// Update the TypeInfo for an existing TypeId. Used when a forward-declared
/// type (e.g., struct with empty fields) gets its full definition later.
pub fn update(self: *TypeTable, id: TypeId, info: TypeInfo) void {
const idx = id.index();
if (idx < self.infos.items.len) {
self.infos.items[idx] = info;
}
}
/// Find a named type (struct/union/enum) by its StringId name.
/// Returns the TypeId if found, null otherwise.
pub fn findByName(self: *const TypeTable, name: StringId) ?TypeId {
for (self.infos.items, 0..) |info, i| {
const n: ?StringId = switch (info) {
.@"struct" => |s| s.name,
.@"union" => |u| u.name,
.tagged_union => |u| u.name,
.@"enum" => |e| e.name,
else => null,
};
if (n != null and n.? == name) return TypeId.fromIndex(@intCast(i));
}
return null;
}
// ── Convenience constructors ────────────────────────────────────────
pub fn ptrTo(self: *TypeTable, pointee: TypeId) TypeId {
return self.intern(.{ .pointer = .{ .pointee = pointee } });
}
pub fn manyPtrTo(self: *TypeTable, element: TypeId) TypeId {
return self.intern(.{ .many_pointer = .{ .element = element } });
}
pub fn sliceOf(self: *TypeTable, element: TypeId) TypeId {
return self.intern(.{ .slice = .{ .element = element } });
}
pub fn arrayOf(self: *TypeTable, element: TypeId, length: u32) TypeId {
return self.intern(.{ .array = .{ .element = element, .length = length } });
}
pub fn optionalOf(self: *TypeTable, child: TypeId) TypeId {
return self.intern(.{ .optional = .{ .child = child } });
}
pub fn functionType(self: *TypeTable, params: []const TypeId, ret: TypeId) TypeId {
return self.functionTypeCC(params, ret, .default);
}
pub fn functionTypeCC(self: *TypeTable, params: []const TypeId, ret: TypeId, cc: TypeInfo.CallConv) TypeId {
const owned_params = self.alloc.dupe(TypeId, params) catch unreachable;
return self.intern(.{ .function = .{ .params = owned_params, .ret = ret, .call_conv = cc } });
}
pub fn closureType(self: *TypeTable, params: []const TypeId, ret: TypeId) TypeId {
const owned_params = self.alloc.dupe(TypeId, params) catch unreachable;
return self.intern(.{ .closure = .{ .params = owned_params, .ret = ret } });
}
pub fn vectorOf(self: *TypeTable, element: TypeId, length: u32) TypeId {
return self.intern(.{ .vector = .{ .element = element, .length = length } });
}
/// Size in bytes for a type (pointer-sized = 8 on 64-bit).
pub fn sizeOf(self: *const TypeTable, id: TypeId) u32 {
const info = self.get(id);
return switch (info) {
.void, .noreturn => 0,
.bool => 1,
.signed => |w| @max(1, w / 8),
.unsigned => |w| @max(1, w / 8),
.f32 => 4,
.f64 => 8,
.string => 16, // {ptr, len}
.pointer, .many_pointer, .function => 8,
.closure => 16, // {fn_ptr, env}
.optional => |opt| blk: {
// Sentinel-shaped optionals (pointer/closure/protocol) cost
// no extra storage — null reuses the payload's null state.
const child_info = self.get(opt.child);
if (child_info == .pointer or child_info == .many_pointer or child_info == .function) break :blk 8;
if (child_info == .closure) break :blk 16;
if (child_info == .@"struct" and child_info.@"struct".is_protocol) break :blk self.sizeOf(opt.child);
// Discriminated form: payload + has_value flag (8-aligned).
break :blk self.sizeOf(opt.child) + 8;
},
.slice => 16, // {ptr, len}
.array => |arr| arr.length * self.sizeOf(arr.element),
.vector => |vec| vec.length * self.sizeOf(vec.element),
.any => 16, // {type_tag, data_ptr}
.@"struct" => |s| {
var total: u32 = 0;
for (s.fields) |f| total += @max(self.sizeOf(f.ty), 8);
return if (total == 0) 8 else total;
},
.@"union" => |u| {
var max_field: u32 = 0;
for (u.fields) |f| {
const sz = self.sizeOf(f.ty);
if (sz > max_field) max_field = sz;
}
return @max(max_field, 8);
},
.tagged_union => |u| {
if (u.backing_type) |bt| return self.sizeOf(bt);
var max_field: u32 = 0;
for (u.fields) |f| {
const sz = self.sizeOf(f.ty);
if (sz > max_field) max_field = sz;
}
const tag_sz = @as(u32, @intCast(self.typeSizeBytes(u.tag_type)));
return tag_sz + @max(max_field, 8);
},
.@"enum" => |e| {
if (e.backing_type) |bt| return self.sizeOf(bt);
return 8;
},
.tuple => |t| {
var total: u32 = 0;
for (t.fields) |f| total += @max(self.sizeOf(f), 8);
return if (total == 0) 8 else total;
},
.protocol => 16, // {ctx, vtable}
};
}
/// Compute the ABI size in bytes for a type, matching LLVM's struct layout rules.
/// This is the authoritative size computation used for closure env sizing and
/// verified against LLVMABISizeOfType.
fn intAbiBytes(w: u16) usize {
// LLVM ABI size for iN: round w up to the next power of 2, then /8.
// Sub-byte widths (i1, i2, ..., i7) are 1 byte.
if (w <= 8) return 1;
if (w <= 16) return 2;
if (w <= 32) return 4;
return 8;
}
pub fn typeSizeBytes(self: *const TypeTable, ty: TypeId) usize {
const ptr_size: usize = self.pointer_size;
if (ty == .void) return 0;
if (ty == .bool) return 1;
if (ty == .u8 or ty == .s8) return 1;
if (ty == .u16 or ty == .s16) return 2;
if (ty == .s32 or ty == .u32 or ty == .f32) return 4;
if (ty == .s64 or ty == .u64 or ty == .f64) return 8;
if (ty == .usize or ty == .isize) return ptr_size;
if (ty == .string) return 16; // {ptr, i64} — always 16 (i64 alignment pads on wasm32)
if (ty.isBuiltin()) return ptr_size; // default for unknown builtins
const info = self.get(ty);
return switch (info) {
.pointer, .many_pointer, .function => ptr_size,
.slice => 16, // {ptr, i64} — same layout as string
.closure => 2 * ptr_size, // {fn_ptr, env_ptr}
.optional => |o| blk: {
const child_info = self.get(o.child);
if (child_info == .pointer or child_info == .many_pointer or child_info == .function)
break :blk ptr_size;
if (child_info == .closure)
break :blk 2 * ptr_size;
if (child_info == .@"struct" and child_info.@"struct".is_protocol)
break :blk self.typeSizeBytes(o.child);
const cs = self.typeSizeBytes(o.child);
const ca = self.typeAlignBytes(o.child);
// { T, i1 } — i1 goes right after T, then pad to struct alignment
const unpadded = cs + 1;
break :blk (unpadded + ca - 1) & ~(ca - 1);
},
.@"struct" => |s| blk: {
var offset: usize = 0;
var max_a: usize = 1;
for (s.fields) |f| {
const fs = self.typeSizeBytes(f.ty);
const fa = self.typeAlignBytes(f.ty);
if (fa > max_a) max_a = fa;
offset = (offset + fa - 1) & ~(fa - 1);
offset += fs;
}
break :blk if (offset == 0) 0 else (offset + max_a - 1) & ~(max_a - 1);
},
.@"union" => |u| blk: {
var max_payload: usize = 0;
for (u.fields) |f| {
const fs = self.typeSizeBytes(f.ty);
if (fs > max_payload) max_payload = fs;
}
break :blk if (max_payload == 0) 8 else max_payload;
},
.tagged_union => |u| blk: {
if (u.backing_type) |bt| break :blk self.typeSizeBytes(bt);
var max_payload: usize = 0;
for (u.fields) |f| {
const fs = self.typeSizeBytes(f.ty);
if (fs > max_payload) max_payload = fs;
}
const tag_size = self.typeSizeBytes(u.tag_type);
const raw = max_payload + tag_size;
break :blk (raw + 7) & ~@as(usize, 7);
},
.array => |a| blk: {
const elem_size = self.typeSizeBytes(a.element);
break :blk elem_size * @as(usize, @intCast(a.length));
},
.vector => |v| blk: {
const elem_size = self.typeSizeBytes(v.element);
const raw = elem_size * @as(usize, @intCast(v.length));
// LLVM vectors round ABI size up to next power of 2
break :blk std.math.ceilPowerOfTwo(usize, raw) catch raw;
},
.tuple => |t| blk: {
var offset: usize = 0;
var max_a: usize = 1;
for (t.fields) |f| {
const fs = self.typeSizeBytes(f);
const fa = self.typeAlignBytes(f);
if (fa > max_a) max_a = fa;
offset = (offset + fa - 1) & ~(fa - 1);
offset += fs;
}
break :blk if (offset == 0) 0 else (offset + max_a - 1) & ~(max_a - 1);
},
.any => 2 * ptr_size, // {type_tag, data_ptr}
.protocol => 2 * ptr_size, // {ctx, vtable}
.@"enum" => |e| {
if (e.backing_type) |bt| return self.typeSizeBytes(bt);
return 8;
},
// LLVM rounds arbitrary-width integers up to the next power-of-2
// width before computing ABI size (i12 → 2 bytes, i24 → 4 bytes).
.signed => |w| intAbiBytes(w),
.unsigned => |w| intAbiBytes(w),
else => 8,
};
}
/// Compute the ABI alignment in bytes for a type, matching LLVM's rules.
pub fn typeAlignBytes(self: *const TypeTable, ty: TypeId) usize {
const ptr_align: usize = self.pointer_size;
if (ty == .void) return 1;
if (ty == .bool) return 1;
if (ty == .u8 or ty == .s8) return 1;
if (ty == .u16 or ty == .s16) return 2;
if (ty == .s32 or ty == .u32 or ty == .f32) return 4;
if (ty == .s64 or ty == .u64 or ty == .f64) return 8;
if (ty == .usize or ty == .isize) return ptr_align;
if (ty == .string) return 8; // i64 drives alignment
if (ty.isBuiltin()) return ptr_align;
const info = self.get(ty);
return switch (info) {
.pointer, .many_pointer, .function => ptr_align,
.slice => 8, // i64 drives alignment
.closure => ptr_align, // {ptr, ptr}
.optional => |o| blk: {
const child_info = self.get(o.child);
if (child_info == .pointer or child_info == .many_pointer or child_info == .function or child_info == .closure)
break :blk ptr_align;
break :blk self.typeAlignBytes(o.child);
},
.@"struct" => |s| blk: {
var max_a: usize = 1;
for (s.fields) |f| {
const fa = self.typeAlignBytes(f.ty);
if (fa > max_a) max_a = fa;
}
break :blk max_a;
},
.@"union", .tagged_union => 8,
.@"enum" => |e| {
if (e.backing_type) |bt| return self.typeAlignBytes(bt);
return 8;
},
.array => |a| self.typeAlignBytes(a.element),
.vector => |v| self.typeAlignBytes(v.element),
.tuple => |t| blk: {
var max_a: usize = 1;
for (t.fields) |f| {
const fa = self.typeAlignBytes(f);
if (fa > max_a) max_a = fa;
}
break :blk max_a;
},
.signed => |w| intAbiBytes(w),
.unsigned => |w| intAbiBytes(w),
else => 8,
};
}
/// Intern a string into the pool.
pub fn internString(self: *TypeTable, str: []const u8) StringId {
return self.strings.intern(self.alloc, str);
}
/// Look up a string from its id.
pub fn getString(self: *const TypeTable, id: StringId) []const u8 {
return self.strings.get(id);
}
/// Format a TypeId for display (e.g., "s32", "*bool", "[]u8").
pub fn typeName(self: *const TypeTable, id: TypeId) []const u8 {
// Fast path for builtins
return switch (id) {
.void => "void",
.bool => "bool",
.s8 => "s8",
.s16 => "s16",
.s32 => "s32",
.s64 => "s64",
.u8 => "u8",
.u16 => "u16",
.u32 => "u32",
.u64 => "u64",
.f32 => "f32",
.f64 => "f64",
.string => "string",
.any => "Any",
.noreturn => "noreturn",
.isize => "isize",
.usize => "usize",
else => {
// User types — format from TypeInfo
const info = self.get(id);
return switch (info) {
.@"struct" => |s| self.getString(s.name),
.@"enum" => |e| self.getString(e.name),
.@"union" => |u| self.getString(u.name),
.tagged_union => |u| self.getString(u.name),
.protocol => |p| self.getString(p.name),
else => "?",
};
},
};
}
};
// ── Intern map support ──────────────────────────────────────────────────
// We use a custom hash/eql context so structurally identical types dedup.
const TypeKey = struct {
info: TypeInfo,
};
const TypeKeyContext = struct {
pub fn hash(_: TypeKeyContext, key: TypeKey) u64 {
var h = std.hash.Wyhash.init(0);
hashTypeInfo(&h, key.info);
return h.final();
}
pub fn eql(_: TypeKeyContext, a: TypeKey, b: TypeKey) bool {
return typeInfoEql(a.info, b.info);
}
};
fn hashTypeInfo(h: *std.hash.Wyhash, info: TypeInfo) void {
// Hash the tag
const tag: u8 = @intFromEnum(std.meta.activeTag(info));
h.update(&.{tag});
switch (info) {
.signed => |w| h.update(&.{w}),
.unsigned => |w| h.update(&.{w}),
.f32, .f64, .void, .bool, .string, .any, .noreturn, .usize, .isize => {},
.pointer => |p| h.update(std.mem.asBytes(&p.pointee)),
.many_pointer => |p| h.update(std.mem.asBytes(&p.element)),
.slice => |s| h.update(std.mem.asBytes(&s.element)),
.array => |a| {
h.update(std.mem.asBytes(&a.element));
h.update(std.mem.asBytes(&a.length));
},
.vector => |v| {
h.update(std.mem.asBytes(&v.element));
h.update(std.mem.asBytes(&v.length));
},
.optional => |o| h.update(std.mem.asBytes(&o.child)),
.function => |f| {
for (f.params) |p| h.update(std.mem.asBytes(&p));
h.update(std.mem.asBytes(&f.ret));
const cc_byte: u8 = @intFromEnum(f.call_conv);
h.update(&.{cc_byte});
},
.closure => |c| {
for (c.params) |p| h.update(std.mem.asBytes(&p));
h.update(std.mem.asBytes(&c.ret));
},
.@"struct" => |s| h.update(std.mem.asBytes(&s.name)),
.@"enum" => |e| h.update(std.mem.asBytes(&e.name)),
.@"union" => |u| h.update(std.mem.asBytes(&u.name)),
.tagged_union => |u| h.update(std.mem.asBytes(&u.name)),
.protocol => |p| h.update(std.mem.asBytes(&p.name)),
.tuple => |t| {
for (t.fields) |f| h.update(std.mem.asBytes(&f));
if (t.names) |ns| for (ns) |n| h.update(std.mem.asBytes(&n));
},
}
}
fn typeInfoEql(a: TypeInfo, b: TypeInfo) bool {
const Tag = std.meta.Tag(TypeInfo);
const a_tag: Tag = a;
const b_tag: Tag = b;
if (a_tag != b_tag) return false;
return switch (a) {
.signed => |w| w == b.signed,
.unsigned => |w| w == b.unsigned,
.f32, .f64, .void, .bool, .string, .any, .noreturn, .usize, .isize => true,
.pointer => |p| p.pointee == b.pointer.pointee,
.many_pointer => |p| p.element == b.many_pointer.element,
.slice => |s| s.element == b.slice.element,
.array => |ar| ar.element == b.array.element and ar.length == b.array.length,
.vector => |v| v.element == b.vector.element and v.length == b.vector.length,
.optional => |o| o.child == b.optional.child,
.function => |f| {
const g = b.function;
if (f.params.len != g.params.len) return false;
for (f.params, g.params) |fp, gp| {
if (fp != gp) return false;
}
if (f.call_conv != g.call_conv) return false;
return f.ret == g.ret;
},
.closure => |c| {
const d = b.closure;
if (c.params.len != d.params.len) return false;
for (c.params, d.params) |cp, dp| {
if (cp != dp) return false;
}
return c.ret == d.ret;
},
.@"struct" => |s| s.name == b.@"struct".name,
.@"enum" => |e| e.name == b.@"enum".name,
.@"union" => |u| u.name == b.@"union".name,
.tagged_union => |u| u.name == b.tagged_union.name,
.protocol => |p| p.name == b.protocol.name,
.tuple => |t| {
const u = b.tuple;
if (t.fields.len != u.fields.len) return false;
for (t.fields, u.fields) |tf, uf| {
if (tf != uf) return false;
}
if ((t.names == null) != (u.names == null)) return false;
if (t.names) |tn| {
const un = u.names.?;
if (tn.len != un.len) return false;
for (tn, un) |tna, una| if (tna != una) return false;
}
return true;
},
};
}