lang 2.1: Pack as a type-system value

Add a `pack` variant to IR `TypeInfo` — an ordered, interned sequence of
per-position element types (`PackInfo { elements: []const TypeId }`) — with
constructor (`packType`), structural equality + hashing, and a `pack(T0, …)`
printer. A pack is comptime-only: it lowers to flat positional args before
codegen and has no runtime layout, so `sizeOf` and `toLLVMType` bail loudly
rather than inventing a size. 5 unit tests (N=0/1/3, dedup, order/arity
distinctness, distinct-from-tuple, printer).

Also: give TypeTable an arena for the slices its constructors dupe (freed at
deinit), and add the missing `usize`/`isize` arms to `sizeOf` (a latent
non-exhaustive switch) so types.test.zig compiles and runs leak-free.
This commit is contained in:
agra
2026-05-29 15:24:46 +03:00
parent 98526ab9b4
commit 92638ae9b5
3 changed files with 132 additions and 4 deletions

View File

@@ -4286,6 +4286,9 @@ pub const LLVMEmitter = struct {
return self.cached_ptr;
},
.usize, .isize => if (self.target_config.isWasm32()) self.cached_i32 else self.cached_i64,
// Comptime-only: a pack is expanded to flat positional args before
// codegen, so it must never reach LLVM type emission.
.pack => @panic("pack type has no LLVM representation (comptime-only)"),
};
}

View File

@@ -119,3 +119,78 @@ test "typeName for builtins" {
try std.testing.expectEqualStrings("void", table.typeName(.void));
try std.testing.expectEqualStrings("Any", table.typeName(.any));
}
// ── Pack type (Feature 1, Step 2.1) ──────────────────────────────────
test "pack type: construct, element access, intern dedup (N=3)" {
const alloc = std.testing.allocator;
var table = TypeTable.init(alloc);
defer table.deinit();
const elems = &[_]TypeId{ .bool, .s32, .string };
const p1 = table.packType(elems);
const p2 = table.packType(elems);
try std.testing.expectEqual(p1, p2); // structural dedup
const info = table.get(p1);
try std.testing.expect(info == .pack);
try std.testing.expectEqual(@as(usize, 3), info.pack.elements.len);
try std.testing.expectEqual(TypeId.bool, info.pack.elements[0]);
try std.testing.expectEqual(TypeId.s32, info.pack.elements[1]);
try std.testing.expectEqual(TypeId.string, info.pack.elements[2]);
}
test "pack type: empty pack (N=0)" {
const alloc = std.testing.allocator;
var table = TypeTable.init(alloc);
defer table.deinit();
const empty1 = table.packType(&.{});
const empty2 = table.packType(&.{});
try std.testing.expectEqual(empty1, empty2);
const info = table.get(empty1);
try std.testing.expect(info == .pack);
try std.testing.expectEqual(@as(usize, 0), info.pack.elements.len);
}
test "pack type: single element (N=1)" {
const alloc = std.testing.allocator;
var table = TypeTable.init(alloc);
defer table.deinit();
const p = table.packType(&[_]TypeId{.f64});
const info = table.get(p);
try std.testing.expectEqual(@as(usize, 1), info.pack.elements.len);
try std.testing.expectEqual(TypeId.f64, info.pack.elements[0]);
}
test "pack type: distinct element lists are distinct types" {
const alloc = std.testing.allocator;
var table = TypeTable.init(alloc);
defer table.deinit();
const a = table.packType(&[_]TypeId{ .bool, .s32 });
const b = table.packType(&[_]TypeId{ .s32, .bool }); // order matters
const c = table.packType(&[_]TypeId{.bool}); // arity matters
try std.testing.expect(a != b);
try std.testing.expect(a != c);
try std.testing.expect(b != c);
// A pack is distinct from the tuple of the same elements.
const tup = table.intern(.{ .tuple = .{ .fields = &[_]TypeId{ .bool, .s32 }, .names = null } });
try std.testing.expect(a != tup);
}
test "pack type: formatTypeName" {
const alloc = std.testing.allocator;
var table = TypeTable.init(alloc);
defer table.deinit();
var arena = std.heap.ArenaAllocator.init(alloc);
defer arena.deinit();
const p = table.packType(&[_]TypeId{ .bool, .s32, .string });
try std.testing.expectEqualStrings("pack(bool, s32, string)", table.formatTypeName(arena.allocator(), p));
const empty = table.packType(&.{});
try std.testing.expectEqualStrings("pack()", table.formatTypeName(arena.allocator(), empty));
}

View File

@@ -67,6 +67,7 @@ pub const TypeInfo = union(enum) {
closure: ClosureInfo,
optional: OptionalInfo,
tuple: TupleInfo,
pack: PackInfo,
any,
protocol: ProtocolInfo,
noreturn,
@@ -162,6 +163,14 @@ pub const TypeInfo = union(enum) {
names: ?[]const StringId,
};
/// A heterogeneous variadic pack as a first-class type-system value: an
/// ordered sequence of per-position element types. Comptime-only — a pack
/// lowers to flat positional args before codegen and has NO runtime layout
/// (sizeOf panics). `elements.len == 0` is a valid empty pack.
pub const PackInfo = struct {
elements: []const TypeId,
};
pub const ProtocolInfo = struct {
name: StringId,
methods: []const Method,
@@ -245,6 +254,12 @@ pub const TypeTable = struct {
/// Maps TypeInfo → TypeId for dedup of structural types
intern_map: std.HashMap(TypeKey, TypeId, TypeKeyContext, 80),
alloc: Allocator,
/// Owns the element/param slices duped by the type constructors
/// (`functionType*`, `closureType*`, `packType`). Freed wholesale in
/// `deinit` — these slices live as long as the table, so an arena avoids
/// per-slice bookkeeping and the owned-vs-borrowed ambiguity that blocks
/// freeing them individually.
slice_arena: std.heap.ArenaAllocator,
/// Target pointer size in bytes (4 for wasm32, 8 for 64-bit targets).
pointer_size: u8 = 8,
/// Borrowed pointer to Lowering's `type_alias_map`. When set,
@@ -262,6 +277,7 @@ pub const TypeTable = struct {
.strings = StringPool.init(alloc),
.intern_map = std.HashMap(TypeKey, TypeId, TypeKeyContext, 80).init(alloc),
.alloc = alloc,
.slice_arena = std.heap.ArenaAllocator.init(alloc),
};
// Pre-populate builtin slots 015 (must match TypeId enum order)
@@ -295,6 +311,7 @@ pub const TypeTable = struct {
self.infos.deinit(self.alloc);
self.strings.deinit(self.alloc);
self.intern_map.deinit();
self.slice_arena.deinit();
}
/// Look up the TypeInfo for a given TypeId.
@@ -366,22 +383,22 @@ pub const TypeTable = struct {
}
pub fn functionTypeCC(self: *TypeTable, params: []const TypeId, ret: TypeId, cc: TypeInfo.CallConv) TypeId {
const owned_params = self.alloc.dupe(TypeId, params) catch unreachable;
const owned_params = self.slice_arena.allocator().dupe(TypeId, params) catch unreachable;
return self.intern(.{ .function = .{ .params = owned_params, .ret = ret, .call_conv = cc } });
}
pub fn functionTypePack(self: *TypeTable, params: []const TypeId, ret: TypeId, cc: TypeInfo.CallConv, pack_start: u32) TypeId {
const owned_params = self.alloc.dupe(TypeId, params) catch unreachable;
const owned_params = self.slice_arena.allocator().dupe(TypeId, params) catch unreachable;
return self.intern(.{ .function = .{ .params = owned_params, .ret = ret, .call_conv = cc, .pack_start = pack_start } });
}
pub fn closureType(self: *TypeTable, params: []const TypeId, ret: TypeId) TypeId {
const owned_params = self.alloc.dupe(TypeId, params) catch unreachable;
const owned_params = self.slice_arena.allocator().dupe(TypeId, params) catch unreachable;
return self.intern(.{ .closure = .{ .params = owned_params, .ret = ret } });
}
pub fn closureTypePack(self: *TypeTable, params: []const TypeId, ret: TypeId, pack_start: u32) TypeId {
const owned_params = self.alloc.dupe(TypeId, params) catch unreachable;
const owned_params = self.slice_arena.allocator().dupe(TypeId, params) catch unreachable;
return self.intern(.{ .closure = .{ .params = owned_params, .ret = ret, .pack_start = pack_start } });
}
@@ -389,6 +406,13 @@ pub const TypeTable = struct {
return self.intern(.{ .vector = .{ .element = element, .length = length } });
}
/// Construct (and intern) a heterogeneous pack type from an ordered
/// element-type list. `elements.len == 0` yields the empty pack.
pub fn packType(self: *TypeTable, elements: []const TypeId) TypeId {
const owned = self.slice_arena.allocator().dupe(TypeId, elements) catch unreachable;
return self.intern(.{ .pack = .{ .elements = owned } });
}
/// 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);
@@ -449,6 +473,10 @@ pub const TypeTable = struct {
return if (total == 0) 8 else total;
},
.protocol => 16, // {ctx, vtable}
.usize, .isize => 8, // pointer-sized (this path is not target-aware; see typeSizeBytes)
// Comptime-only: a pack must be expanded to flat positional args
// before codegen. Reaching runtime layout means a pack leaked.
.pack => @panic("pack type has no runtime layout (comptime-only)"),
};
}
@@ -740,6 +768,17 @@ pub const TypeTable = struct {
buf.append(alloc, ')') catch break :blk "(?)";
break :blk buf.toOwnedSlice(alloc) catch "(?)";
},
.pack => |pk| blk: {
var buf = std.ArrayList(u8).empty;
defer buf.deinit(alloc);
buf.appendSlice(alloc, "pack(") catch break :blk "pack(?)";
for (pk.elements, 0..) |e, i| {
if (i > 0) buf.appendSlice(alloc, ", ") catch break :blk "pack(?)";
buf.appendSlice(alloc, self.formatTypeName(alloc, e)) catch break :blk "pack(?)";
}
buf.append(alloc, ')') catch break :blk "pack(?)";
break :blk buf.toOwnedSlice(alloc) catch "pack(?)";
},
.signed => |w| std.fmt.allocPrint(alloc, "s{d}", .{w}) catch "s?",
.unsigned => |w| std.fmt.allocPrint(alloc, "u{d}", .{w}) catch "u?",
else => self.typeName(id),
@@ -812,6 +851,9 @@ fn hashTypeInfo(h: *std.hash.Wyhash, info: TypeInfo) void {
for (t.fields) |f| h.update(std.mem.asBytes(&f));
if (t.names) |ns| for (ns) |n| h.update(std.mem.asBytes(&n));
},
.pack => |p| {
for (p.elements) |e| h.update(std.mem.asBytes(&e));
},
}
}
@@ -871,5 +913,13 @@ fn typeInfoEql(a: TypeInfo, b: TypeInfo) bool {
}
return true;
},
.pack => |p| {
const q = b.pack;
if (p.elements.len != q.elements.len) return false;
for (p.elements, q.elements) |pe, qe| {
if (pe != qe) return false;
}
return true;
},
};
}