ERR/E1.1 (slice 1): error-set type + global tag registry + decl registration

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.
This commit is contained in:
agra
2026-05-31 17:39:11 +03:00
parent fdeab0efd4
commit 73232ce170
7 changed files with 212 additions and 0 deletions

View File

@@ -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 015, 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;