From 73232ce170c8ab03b78c13bfa47dc3cfdb3caa95 Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 31 May 2026 17:39:11 +0300 Subject: [PATCH] ERR/E1.1 (slice 1): error-set type + global tag registry + decl registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First sema/types step. Implemented in the IR layer (ir/types.zig + type_bridge.zig + lower.zig), NOT src/sema.zig — lowering doesn't consume sema; the frontend Type is LSP-only. Mirrors how enums are handled. - ir/types.zig: new `.error_set` TypeInfo kind (ErrorSetInfo {name, tags: []u32}; identity = name, like enum) with a u32 runtime layout (size/align 4, LLVM i32) per the locked error-slot ABI. New TagRegistry on TypeTable (global tag pool: name -> u32, monotonic, id 0 reserved for "no error"). internTag/getTagName/errorSetType helpers; `.error_set` arms in all 7 exhaustive switches + findByName. - emit_llvm: toLLVMTypeInfo -> i32. print: writeType -> set name. - type_bridge: resolveInlineErrorSet (mirrors resolveInlineUnion) + .error_set_decl arm. - lower.zig: registerErrorSetDecl (rejects empty `error { }` with a diagnostic) wired into both top-level decl switches + the block-local one. - tests: ir/types.test (TagRegistry 0-reserved + identity; errorSetType u32 layout + named display + dedup; sorted storage) and ir/type_bridge.test (decl -> type + tag interning + re-resolve dedup). End-to-end: `Foo :: error { A, B }` + main compiles + runs (exit 0) — first ERR syntax to survive the full pipeline; empty set rejects with a diagnostic. Inferred bare `!`, error.X value, and == typing deferred to slice 2 / E1.2. zig build, zig build test, and 254/254 examples green. --- src/ir/emit_llvm.zig | 1 + src/ir/lower.zig | 21 +++++++++ src/ir/print.zig | 1 + src/ir/type_bridge.test.zig | 24 ++++++++++ src/ir/type_bridge.zig | 19 ++++++++ src/ir/types.test.zig | 59 +++++++++++++++++++++++++ src/ir/types.zig | 87 +++++++++++++++++++++++++++++++++++++ 7 files changed, 212 insertions(+) diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 7ce5aae..be292ff 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -4182,6 +4182,7 @@ pub const LLVMEmitter = struct { .f64 => self.cached_f64, .void => self.cached_void, .bool => self.cached_i1, + .error_set => self.cached_i32, // u32 tag id on the error channel .string => self.getStringStructType(), .pointer, .many_pointer, .function => self.cached_ptr, .closure => self.getClosureStructType(), diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 24a74dc..d5be070 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -477,6 +477,9 @@ pub const Lowering = struct { .union_decl => { _ = type_bridge.resolveAstType(decl, &self.module.types); }, + .error_set_decl => { + self.registerErrorSetDecl(decl); + }, .protocol_decl => { self.registerProtocolDecl(&decl.data.protocol_decl); }, @@ -710,6 +713,9 @@ pub const Lowering = struct { // Register plain union types in the type table _ = type_bridge.resolveAstType(decl, &self.module.types); }, + .error_set_decl => { + self.registerErrorSetDecl(decl); + }, .protocol_decl => { self.registerProtocolDecl(&decl.data.protocol_decl); }, @@ -1486,6 +1492,7 @@ pub const Lowering = struct { .enum_decl, .union_decl => { _ = type_bridge.resolveAstType(node, &self.module.types); }, + .error_set_decl => self.registerErrorSetDecl(node), .ufcs_alias => |ua| { self.ufcs_alias_map.put(ua.name, ua.target) catch {}; }, @@ -12063,6 +12070,20 @@ pub const Lowering = struct { // ── Type registration ─────────────────────────────────────────── /// Register a struct declaration's fields and methods in the IR type table. + /// Register a `Foo :: error { A, B }` declaration as an error-set type. + /// Rejects an empty set here (sema gate) since type_bridge has no + /// diagnostics; non-empty sets are interned via type_bridge. + fn registerErrorSetDecl(self: *Lowering, node: *const Node) void { + const esd = node.data.error_set_decl; + if (esd.tag_names.len == 0) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, node.span, "error set '{s}' must declare at least one tag", .{esd.name}); + } + return; + } + _ = type_bridge.resolveAstType(node, &self.module.types); + } + fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl) void { const table = &self.module.types; const name_id = table.internString(sd.name); diff --git a/src/ir/print.zig b/src/ir/print.zig index 9135e19..6fc0cb3 100644 --- a/src/ir/print.zig +++ b/src/ir/print.zig @@ -438,6 +438,7 @@ fn writeType(id: TypeId, tt: *const TypeTable, writer: Writer) !void { .@"union" => |u| try writer.writeAll(tt.getString(u.name)), .tagged_union => |u| try writer.writeAll(tt.getString(u.name)), .protocol => |p| try writer.writeAll(tt.getString(p.name)), + .error_set => |e| try writer.writeAll(tt.getString(e.name)), .pointer => |p| { try writer.writeByte('*'); try writeType(p.pointee, tt, writer); diff --git a/src/ir/type_bridge.test.zig b/src/ir/type_bridge.test.zig index de2380a..994ae8c 100644 --- a/src/ir/type_bridge.test.zig +++ b/src/ir/type_bridge.test.zig @@ -155,3 +155,27 @@ test "resolveAstType: TypeTable.aliases resolves named alias" { const ptr_id = type_bridge.resolveAstType(ptr_node, &table); try std.testing.expectEqual(TypeInfo{ .pointer = .{ .pointee = .u64 } }, table.get(ptr_id)); } + +test "resolveAstType: error_set_decl registers an error-set type + interns tags" { + const alloc = std.testing.allocator; + var table = TypeTable.init(alloc); + defer table.deinit(); + + const node = try alloc.create(Node); + defer alloc.destroy(node); + const tag_names = [_][]const u8{ "BadDigit", "Overflow" }; + node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .error_set_decl = .{ + .name = "ParseErr", + .tag_names = &tag_names, + } } }; + + const id = type_bridge.resolveAstType(node, &table); + const info = table.get(id); + try std.testing.expect(info == .error_set); + try std.testing.expectEqualStrings("ParseErr", table.getString(info.error_set.name)); + try std.testing.expectEqual(@as(usize, 2), info.error_set.tags.len); + // Tags were interned into the global pool (round-trip a name through it). + try std.testing.expectEqualStrings("BadDigit", table.getTagName(table.internTag("BadDigit"))); + // Re-resolving the same decl dedups to the same TypeId. + try std.testing.expectEqual(id, type_bridge.resolveAstType(node, &table)); +} diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index fbb2129..edd6bed 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -57,6 +57,7 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable) TypeId { .enum_decl => |ed| resolveInlineEnum(&ed, table), .struct_decl => |sd| resolveInlineStruct(&sd, table), .union_decl => |ud| resolveInlineUnion(&ud, table), + .error_set_decl => |esd| resolveInlineErrorSet(&esd, table), else => { // A non-type AST node reached type resolution — a caller bug. // Returning a plausible `.s64` would silently fabricate an 8-byte @@ -635,3 +636,21 @@ fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable) TypeId { table.update(id, info); return id; } + +/// `Foo :: error { A, B }` → a registered `.error_set` type. Tag names are +/// interned into the global tag pool; the set stores their (sorted) ids. The +/// caller (lowering) is responsible for rejecting an empty set, so this only +/// sees non-empty declarations. +fn resolveInlineErrorSet(esd: *const ast.ErrorSetDecl, table: *TypeTable) TypeId { + const alloc = table.alloc; + const name_id = table.internString(esd.name); + + if (table.findByName(name_id)) |existing| return existing; + + var tag_ids = std.ArrayList(u32).empty; + defer tag_ids.deinit(alloc); + for (esd.tag_names) |tn| { + tag_ids.append(alloc, table.internTag(tn)) catch unreachable; + } + return table.errorSetType(name_id, tag_ids.items); +} diff --git a/src/ir/types.test.zig b/src/ir/types.test.zig index 64d90f1..1c1c126 100644 --- a/src/ir/types.test.zig +++ b/src/ir/types.test.zig @@ -194,3 +194,62 @@ test "pack type: formatTypeName" { const empty = table.packType(&.{}); try std.testing.expectEqualStrings("pack()", table.formatTypeName(arena.allocator(), empty)); } + +// ── ERR E1.1 (Slice 1) — error sets + tag registry ── + +test "TagRegistry interns tags, id 0 reserved, global identity" { + const alloc = std.testing.allocator; + var table = TypeTable.init(alloc); + defer table.deinit(); + + const bad = table.internTag("BadDigit"); + const over = table.internTag("Overflow"); + const bad_again = table.internTag("BadDigit"); + + try std.testing.expect(bad >= 1); // id 0 reserved for "no error" + try std.testing.expect(bad != over); // distinct names → distinct ids + try std.testing.expectEqual(bad, bad_again); // same name → same id (global-flat) + try std.testing.expectEqualStrings("BadDigit", table.getTagName(bad)); + try std.testing.expectEqualStrings("Overflow", table.getTagName(over)); + try std.testing.expectEqualStrings("", table.getTagName(0)); // reserved slot +} + +test "errorSetType: u32 layout, named display, dedup by name" { + const alloc = std.testing.allocator; + var table = TypeTable.init(alloc); + defer table.deinit(); + + const name = table.internString("ParseErr"); + const tags = [_]u32{ table.internTag("BadDigit"), table.internTag("Overflow"), table.internTag("Empty") }; + const set = table.errorSetType(name, &tags); + + // u32 runtime layout (the error channel's tag value). + try std.testing.expectEqual(@as(u32, 4), table.sizeOf(set)); + try std.testing.expectEqual(@as(usize, 4), table.typeSizeBytes(set)); + try std.testing.expectEqual(@as(usize, 4), table.typeAlignBytes(set)); + // Displays by name; resolvable by name. + try std.testing.expectEqualStrings("ParseErr", table.typeName(set)); + try std.testing.expectEqual(set, table.findByName(name).?); + // Info shape. + const info = table.get(set); + try std.testing.expect(info == .error_set); + try std.testing.expectEqual(@as(usize, 3), info.error_set.tags.len); + // Identity is the name → re-constructing the same set dedups. + try std.testing.expectEqual(set, table.errorSetType(name, &tags)); +} + +test "errorSetType: tags stored sorted by global id" { + const alloc = std.testing.allocator; + var table = TypeTable.init(alloc); + defer table.deinit(); + + const name = table.internString("E"); + const c = table.internTag("C"); + const a = table.internTag("A"); + const b = table.internTag("B"); + // Pass out of order; errorSetType sorts for canonical storage. + const set = table.errorSetType(name, &[_]u32{ c, a, b }); + const stored = table.get(set).error_set.tags; + try std.testing.expectEqual(@as(usize, 3), stored.len); + try std.testing.expect(stored[0] <= stored[1] and stored[1] <= stored[2]); +} diff --git a/src/ir/types.zig b/src/ir/types.zig index 6a3d29b..7526633 100644 --- a/src/ir/types.zig +++ b/src/ir/types.zig @@ -79,6 +79,7 @@ pub const TypeInfo = union(enum) { pack: PackInfo, any, protocol: ProtocolInfo, + error_set: ErrorSetInfo, noreturn, usize, isize, @@ -191,6 +192,15 @@ pub const TypeInfo = union(enum) { sig: TypeId, // function type }; }; + + /// A declared error set `Foo :: error { A, B }`. `tags` are GLOBAL tag + /// ids from the TypeTable's `TagRegistry` (sorted, canonical). Identity is + /// the `name` (like an enum). Runtime layout is u32 — the error channel's + /// tag value; id 0 is reserved for "no error". + pub const ErrorSetInfo = struct { + name: StringId, + tags: []const u32, // sorted global tag ids + }; }; // ── StringId ──────────────────────────────────────────────────────────── @@ -256,12 +266,61 @@ pub const StringPool = struct { } }; +// ── TagRegistry ───────────────────────────────────────────────────────── +// Global error-tag pool: tag name → u32 id, monotonic, id 0 reserved for +// "no error". Tag identity is the name, program-wide — two declared sets that +// list the same tag share its id (the design's global-flat tag identity). A +// separate namespace from StringPool so tag ids stay dense (compact id→name +// table for `{}` interpolation + traces). + +pub const TagRegistry = struct { + /// tag name → id. Keys point to owned allocations in `names`. + map: std.StringHashMap(u32), + /// id → tag name. Index 0 is the reserved "" (no-error) slot. + names: std.ArrayList([]const u8), + next_id: u32, + + pub fn init(alloc: Allocator) TagRegistry { + var reg = TagRegistry{ + .map = std.StringHashMap(u32).init(alloc), + .names = std.ArrayList([]const u8).empty, + .next_id = 1, // 0 reserved for "no error" + }; + reg.names.append(alloc, "") catch unreachable; // slot 0 + return reg; + } + + pub fn deinit(self: *TagRegistry, alloc: Allocator) void { + for (self.names.items[1..]) |n| alloc.free(@constCast(n)); + self.names.deinit(alloc); + self.map.deinit(); + } + + pub fn intern(self: *TagRegistry, alloc: Allocator, name: []const u8) u32 { + if (self.map.get(name)) |id| return id; + const id = self.next_id; + self.next_id += 1; + const owned = alloc.dupe(u8, name) catch unreachable; + self.names.append(alloc, owned) catch unreachable; + self.map.put(owned, id) catch unreachable; + return id; + } + + pub fn getName(self: *const TagRegistry, id: u32) []const u8 { + if (id >= self.names.items.len) return ""; + return self.names.items[id]; + } +}; + // ── 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, + /// Global error-tag pool (string → u32 id). Populated as `error { ... }` + /// sets are registered; queried when lowering `error.X` value expressions. + tags: TagRegistry, /// Maps TypeInfo → TypeId for dedup of structural types intern_map: std.HashMap(TypeKey, TypeId, TypeKeyContext, 80), alloc: Allocator, @@ -286,6 +345,7 @@ pub const TypeTable = struct { var table = TypeTable{ .infos = std.ArrayList(TypeInfo).empty, .strings = StringPool.init(alloc), + .tags = TagRegistry.init(alloc), .intern_map = std.HashMap(TypeKey, TypeId, TypeKeyContext, 80).init(alloc), .alloc = alloc, .slice_arena = std.heap.ArenaAllocator.init(alloc), @@ -322,6 +382,7 @@ pub const TypeTable = struct { pub fn deinit(self: *TypeTable) void { self.infos.deinit(self.alloc); self.strings.deinit(self.alloc); + self.tags.deinit(self.alloc); self.intern_map.deinit(); self.slice_arena.deinit(); } @@ -361,6 +422,7 @@ pub const TypeTable = struct { .@"union" => |u| u.name, .tagged_union => |u| u.name, .@"enum" => |e| e.name, + .error_set => |e| e.name, else => null, }; if (n != null and n.? == name) return TypeId.fromIndex(@intCast(i)); @@ -425,6 +487,24 @@ pub const TypeTable = struct { return self.intern(.{ .pack = .{ .elements = owned } }); } + /// Intern an error-tag name into the global tag pool, returning its id. + pub fn internTag(self: *TypeTable, name: []const u8) u32 { + return self.tags.intern(self.alloc, name); + } + + /// Look up a tag name from its global id. + pub fn getTagName(self: *const TypeTable, id: u32) []const u8 { + return self.tags.getName(id); + } + + /// Construct (and intern) a named error-set type. `tag_ids` are global tag + /// ids (from `internTag`); they are sorted here for canonical storage. + pub fn errorSetType(self: *TypeTable, name: StringId, tag_ids: []const u32) TypeId { + const owned = self.slice_arena.allocator().dupe(u32, tag_ids) catch unreachable; + std.mem.sort(u32, owned, {}, std.sort.asc(u32)); + return self.intern(.{ .error_set = .{ .name = name, .tags = 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); @@ -485,6 +565,7 @@ pub const TypeTable = struct { return if (total == 0) 8 else total; }, .protocol => 16, // {ctx, vtable} + .error_set => 4, // u32 tag id on the error channel .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. @@ -593,6 +674,7 @@ pub const TypeTable = struct { }, .any => 2 * ptr_size, // {type_tag, data_ptr} .protocol => 2 * ptr_size, // {ctx, vtable} + .error_set => 4, // u32 tag id .@"enum" => |e| { if (e.backing_type) |bt| return self.typeSizeBytes(bt); return 8; @@ -638,6 +720,7 @@ pub const TypeTable = struct { break :blk max_a; }, .@"union", .tagged_union => 8, + .error_set => 4, // u32 tag id .@"enum" => |e| { if (e.backing_type) |bt| return self.typeAlignBytes(bt); return 8; @@ -699,6 +782,7 @@ pub const TypeTable = struct { .@"union" => |u| self.getString(u.name), .tagged_union => |u| self.getString(u.name), .protocol => |p| self.getString(p.name), + .error_set => |e| self.getString(e.name), else => "?", }; }, @@ -719,6 +803,7 @@ pub const TypeTable = struct { .@"union" => |u| self.getString(u.name), .tagged_union => |u| self.getString(u.name), .protocol => |p| self.getString(p.name), + .error_set => |e| self.getString(e.name), .pointer => |p| blk: { const inner = self.formatTypeName(alloc, p.pointee); break :blk std.fmt.allocPrint(alloc, "*{s}", .{inner}) catch "*?"; @@ -863,6 +948,7 @@ fn hashTypeInfo(h: *std.hash.Wyhash, info: TypeInfo) void { .@"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)), + .error_set => |e| h.update(std.mem.asBytes(&e.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)); @@ -915,6 +1001,7 @@ fn typeInfoEql(a: TypeInfo, b: TypeInfo) bool { .@"union" => |u| u.name == b.@"union".name, .tagged_union => |u| u.name == b.tagged_union.name, .protocol => |p| p.name == b.protocol.name, + .error_set => |e| e.name == b.error_set.name, .tuple => |t| { const u = b.tuple; if (t.fields.len != u.fields.len) return false;