From 92638ae9b5c51f38ca14e80ea14b7620ce5cb375 Mon Sep 17 00:00:00 2001 From: agra Date: Fri, 29 May 2026 15:24:46 +0300 Subject: [PATCH] lang 2.1: Pack as a type-system value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/ir/emit_llvm.zig | 3 ++ src/ir/types.test.zig | 75 +++++++++++++++++++++++++++++++++++++++++++ src/ir/types.zig | 58 ++++++++++++++++++++++++++++++--- 3 files changed, 132 insertions(+), 4 deletions(-) diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index dea0126..129a7e0 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -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)"), }; } diff --git a/src/ir/types.test.zig b/src/ir/types.test.zig index 91aa065..64d90f1 100644 --- a/src/ir/types.test.zig +++ b/src/ir/types.test.zig @@ -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)); +} diff --git a/src/ir/types.zig b/src/ir/types.zig index 5f93ff3..f1c8e01 100644 --- a/src/ir/types.zig +++ b/src/ir/types.zig @@ -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 0–15 (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; + }, }; }