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:
@@ -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.
|
// Re-resolving the same decl dedups to the same TypeId.
|
||||||
try std.testing.expectEqual(id, type_bridge.resolveAstType(node, &table));
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable) TypeId {
|
|||||||
.struct_decl => |sd| resolveInlineStruct(&sd, table),
|
.struct_decl => |sd| resolveInlineStruct(&sd, table),
|
||||||
.union_decl => |ud| resolveInlineUnion(&ud, table),
|
.union_decl => |ud| resolveInlineUnion(&ud, table),
|
||||||
.error_set_decl => |esd| resolveInlineErrorSet(&esd, table),
|
.error_set_decl => |esd| resolveInlineErrorSet(&esd, table),
|
||||||
|
.error_type_expr => |ete| resolveErrorType(&ete, table),
|
||||||
else => {
|
else => {
|
||||||
// A non-type AST node reached type resolution — a caller bug.
|
// A non-type AST node reached type resolution — a caller bug.
|
||||||
// Returning a plausible `.s64` would silently fabricate an 8-byte
|
// 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);
|
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, &.{});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user