From 5a24a1177d5d8611a600f004820f77d3d117d299 Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 31 May 2026 18:30:22 +0300 Subject: [PATCH] =?UTF-8?q?ERR/E1.2:=20failable=20signatures=20=E2=80=94?= =?UTF-8?q?=20resolve=20the=20`!`=20/=20`!Named`=20error=20channel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the `.error_type_expr` arm to type_bridge.resolveAstType (the gating site that still returned `.unresolved`): - `!Named` → resolveTypeName(name) → the declared error set (E1.1). - bare `!` → a shared inferred placeholder error set (reserved name "!", empty tags), refined per failable function by the E1.4 SCC pass. The error channel then falls out of the existing multi-return + tuple machinery: `-> (s32, !Named)` is a tuple_type_expr whose last field is the error_type_expr → resolves to a tuple {s32, error_set} — exactly the locked ABI (error slot = last return slot, u32). `-> !Named` resolves to the set. Verified end-to-end via scratch: `parse :: (n) -> (s32, !ParseErr) { ...; return (n, e); }` compiles + runs, `v, err := parse(5)` destructures (err typed as the error set), `err == error.X` works; `-> !Named` single return too. 3 unit tests in type_bridge.test.zig (!Named, bare ! placeholder, tuple ending in the error set). No examples/ — the only current usage path (return (value, error)) will be flow-check-rejected at E1.8; the blessed example waits for E1.3 (raise) + try/catch consumption. zig build, zig build test (275), and 256/256 examples green. --- src/ir/type_bridge.test.zig | 66 +++++++++++++++++++++++++++++++++++++ src/ir/type_bridge.zig | 16 +++++++++ 2 files changed, 82 insertions(+) diff --git a/src/ir/type_bridge.test.zig b/src/ir/type_bridge.test.zig index 994ae8c..26075ab 100644 --- a/src/ir/type_bridge.test.zig +++ b/src/ir/type_bridge.test.zig @@ -179,3 +179,69 @@ test "resolveAstType: error_set_decl registers an error-set type + interns tags" // Re-resolving the same decl dedups to the same TypeId. try std.testing.expectEqual(id, type_bridge.resolveAstType(node, &table)); } + +// ── ERR E1.2 — failable-signature error channel resolution ── + +test "resolveAstType: `!Named` resolves to the declared error set" { + const alloc = std.testing.allocator; + var table = TypeTable.init(alloc); + defer table.deinit(); + + // Register `ParseErr :: error { BadDigit }` directly. + const set = table.errorSetType(table.internString("ParseErr"), &[_]u32{table.internTag("BadDigit")}); + + // `!ParseErr` (an error_type_expr with a name) resolves to that set. + const node = try alloc.create(Node); + defer alloc.destroy(node); + node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .error_type_expr = .{ .name = "ParseErr" } } }; + try std.testing.expectEqual(set, type_bridge.resolveAstType(node, &table)); +} + +test "resolveAstType: bare `!` resolves to a shared inferred placeholder set" { + const alloc = std.testing.allocator; + var table = TypeTable.init(alloc); + defer table.deinit(); + + const a = try alloc.create(Node); + defer alloc.destroy(a); + a.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .error_type_expr = .{ .name = null } } }; + const b = try alloc.create(Node); + defer alloc.destroy(b); + b.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .error_type_expr = .{ .name = null } } }; + + const ia = type_bridge.resolveAstType(a, &table); + const ib = type_bridge.resolveAstType(b, &table); + try std.testing.expect(table.get(ia) == .error_set); + try std.testing.expectEqualStrings("!", table.getString(table.get(ia).error_set.name)); + try std.testing.expectEqual(@as(usize, 0), table.get(ia).error_set.tags.len); // empty until E1.4 SCC + try std.testing.expectEqual(ia, ib); // all bare `!` share the placeholder for now +} + +test "resolveAstType: `(s32, !Named)` result list is a tuple ending in the error set" { + // resolveTupleType allocates its field slice via `table.alloc` (the real + // compiler backs the table with an arena), so use one here. + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var table = TypeTable.init(alloc); + + const set = table.errorSetType(table.internString("IoErr"), &[_]u32{table.internTag("Eof")}); + + const val_ty = try alloc.create(Node); + defer alloc.destroy(val_ty); + val_ty.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "s32" } } }; + const err_ty = try alloc.create(Node); + defer alloc.destroy(err_ty); + err_ty.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .error_type_expr = .{ .name = "IoErr" } } }; + const fields = [_]*Node{ val_ty, err_ty }; + const tuple = try alloc.create(Node); + defer alloc.destroy(tuple); + tuple.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .tuple_type_expr = .{ .field_types = &fields, .field_names = null } } }; + + const id = type_bridge.resolveAstType(tuple, &table); + const info = table.get(id); + try std.testing.expect(info == .tuple); + try std.testing.expectEqual(@as(usize, 2), info.tuple.fields.len); + try std.testing.expectEqual(TypeId.s32, info.tuple.fields[0]); + try std.testing.expectEqual(set, info.tuple.fields[1]); // error channel = last slot +} diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index edd6bed..9594b73 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -58,6 +58,7 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable) TypeId { .struct_decl => |sd| resolveInlineStruct(&sd, table), .union_decl => |ud| resolveInlineUnion(&ud, table), .error_set_decl => |esd| resolveInlineErrorSet(&esd, table), + .error_type_expr => |ete| resolveErrorType(&ete, table), else => { // A non-type AST node reached type resolution — a caller bug. // Returning a plausible `.s64` would silently fabricate an 8-byte @@ -654,3 +655,18 @@ fn resolveInlineErrorSet(esd: *const ast.ErrorSetDecl, table: *TypeTable) TypeId } return table.errorSetType(name_id, tag_ids.items); } + +/// The error channel of a failable signature: `!Named` → the declared error +/// set (registered by `resolveInlineErrorSet`); bare `!` → a shared inferred +/// placeholder set. The placeholder's members are refined per failable +/// function by the whole-program SCC pass (E1.4); for now every bare `!` +/// resolves to the same empty inferred set, which is correct while no +/// function raises (E1.3+). +fn resolveErrorType(ete: *const ast.ErrorTypeExpr, table: *TypeTable) TypeId { + if (ete.name) |name| return resolveTypeName(name, table); + // `!` is not a legal type/identifier name, so this reserved StringId can + // never collide with a user-declared set. + const name_id = table.internString("!"); + if (table.findByName(name_id)) |existing| return existing; + return table.errorSetType(name_id, &.{}); +}