ERR/E1.2: failable signatures — resolve the ! / !Named error channel

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.
This commit is contained in:
agra
2026-05-31 18:30:22 +03:00
parent f5974e5846
commit 5a24a1177d
2 changed files with 82 additions and 0 deletions

View File

@@ -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
}

View File

@@ -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, &.{});
}