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.
753 lines
28 KiB
Zig
753 lines
28 KiB
Zig
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 0–15
|
||
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: 1–64
|
||
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 0–15, 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 0–15 (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;
|
||
},
|
||
};
|
||
}
|