ERR/E1.3: raise sema + pure-failable lowering
`raise EXPR` now terminates a failable function via the error channel. Scope (Option 2): full raise sema checks + lowering for the pure-failable shape (`-> !` / `-> !Named`); the value-carrying `-> (T..., !)` shape bails loudly, deferred to E2's error-channel tuple ABI. - lowerStmt + tryLowerAsExpr: `.raise_stmt` -> lowerRaise (also routes a raise that is a block's last statement, which previously hit unknown_expr) - lowerRaise: failable-context check (effectiveReturnType + errorChannelOf); literal membership via lowerErrorTagLiteral; variable form subset-checked via checkErrorSetSubset; pure-failable emits ret(tag) - lowerErrorTagLiteral skips membership for the bare-`!` inferred placeholder - plain `return;` in a pure-failable fn emits ret(0) (success / no error) - parser: in_defer_body flag rejects `raise` inside a `defer` body Tests: examples/219-raise.sx (positive, exit 8), examples/220-raise-rejections.sx (3 sema rejections, exit 1), inline parser test for raise-in-defer. Gates: zig build, zig build test, 258/258 examples.
This commit is contained in:
156
src/ir/lower.zig
156
src/ir/lower.zig
@@ -1443,7 +1443,7 @@ pub const Lowering = struct {
|
||||
/// Statement nodes are lowered as statements (returning null).
|
||||
fn tryLowerAsExpr(self: *Lowering, node: *const Node) ?Ref {
|
||||
return switch (node.data) {
|
||||
.var_decl, .const_decl, .fn_decl, .return_stmt, .assignment, .defer_stmt, .push_stmt, .multi_assign, .destructure_decl => {
|
||||
.var_decl, .const_decl, .fn_decl, .return_stmt, .raise_stmt, .assignment, .defer_stmt, .push_stmt, .multi_assign, .destructure_decl => {
|
||||
self.lowerStmt(node);
|
||||
return null;
|
||||
},
|
||||
@@ -1457,6 +1457,7 @@ pub const Lowering = struct {
|
||||
.const_decl => |cd| self.lowerConstDecl(&cd),
|
||||
.fn_decl => |fd| self.lowerLocalFnDecl(&fd),
|
||||
.return_stmt => |rs| self.lowerReturn(&rs),
|
||||
.raise_stmt => |rs| self.lowerRaise(&rs, node.span),
|
||||
.assignment => |asgn| self.lowerAssignment(&asgn),
|
||||
.defer_stmt => |ds| self.lowerDefer(&ds),
|
||||
.push_stmt => |ps| self.lowerPush(&ps),
|
||||
@@ -1734,7 +1735,18 @@ pub const Lowering = struct {
|
||||
self.builder.ret(coerced, ret_ty);
|
||||
}
|
||||
} else {
|
||||
self.builder.retVoid();
|
||||
// A bare `return;` in a pure failable function (`-> !` / `-> !Named`,
|
||||
// whose return type IS the error set) is the success exit — the
|
||||
// error slot carries 0 ("no error"). Everything else is a void return.
|
||||
const ret_ty = if (self.builder.func) |fid|
|
||||
self.module.functions.items[@intFromEnum(fid)].ret
|
||||
else
|
||||
TypeId.void;
|
||||
if (!ret_ty.isBuiltin() and self.module.types.get(ret_ty) == .error_set) {
|
||||
self.builder.ret(self.builder.constInt(0, ret_ty), ret_ty);
|
||||
} else {
|
||||
self.builder.retVoid();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4744,16 +4756,21 @@ pub const Lowering = struct {
|
||||
if (!t.isBuiltin()) {
|
||||
const info = self.module.types.get(t);
|
||||
if (info == .error_set) {
|
||||
var in_set = false;
|
||||
for (info.error_set.tags) |member| {
|
||||
if (member == tag_id) {
|
||||
in_set = true;
|
||||
break;
|
||||
// The bare-`!` inferred placeholder (reserved name "!") accepts
|
||||
// any tag — its members aren't known until the whole-program SCC
|
||||
// pass (E1.4) folds in every raised tag. Skip membership for it.
|
||||
if (!std.mem.eql(u8, self.module.types.getString(info.error_set.name), "!")) {
|
||||
var in_set = false;
|
||||
for (info.error_set.tags) |member| {
|
||||
if (member == tag_id) {
|
||||
in_set = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!in_set) {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, span, "error tag 'error.{s}' is not in error set '{s}'", .{ tag_name, self.module.types.getString(info.error_set.name) });
|
||||
if (!in_set) {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, span, "error tag 'error.{s}' is not in error set '{s}'", .{ tag_name, self.module.types.getString(info.error_set.name) });
|
||||
}
|
||||
}
|
||||
}
|
||||
return self.builder.constInt(@as(i64, @intCast(tag_id)), t);
|
||||
@@ -14934,6 +14951,123 @@ pub const Lowering = struct {
|
||||
self.builder.emit(.{ .cmp_ne = .{ .lhs = lv, .rhs = rv } }, .bool);
|
||||
}
|
||||
|
||||
/// The declared return type of the function currently being lowered (the
|
||||
/// inlined body's type wins while inlining a comptime call), or null when
|
||||
/// there is no enclosing function.
|
||||
fn effectiveReturnType(self: *Lowering) ?TypeId {
|
||||
if (self.inline_return_target) |iri| return iri.ret_ty;
|
||||
if (self.builder.func) |fid| return self.module.functions.items[@intFromEnum(fid)].ret;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// If `ret_ty` belongs to a failable function, the TypeId of its error
|
||||
/// channel; else null. `-> !Named` / `-> !` resolve the error set directly;
|
||||
/// `-> (T..., !)` carries it as the last tuple field (the locked ABI).
|
||||
fn errorChannelOf(self: *Lowering, ret_ty: TypeId) ?TypeId {
|
||||
if (ret_ty.isBuiltin()) return null;
|
||||
switch (self.module.types.get(ret_ty)) {
|
||||
.error_set => return ret_ty,
|
||||
.tuple => |t| {
|
||||
if (t.fields.len == 0) return null;
|
||||
const last = t.fields[t.fields.len - 1];
|
||||
if (last.isBuiltin()) return null;
|
||||
return if (self.module.types.get(last) == .error_set) last else null;
|
||||
},
|
||||
else => return null,
|
||||
}
|
||||
}
|
||||
|
||||
/// True for the bare-`!` inferred placeholder error set (reserved name "!").
|
||||
fn isInferredErrorSet(self: *Lowering, set: TypeId) bool {
|
||||
if (set.isBuiltin()) return false;
|
||||
const info = self.module.types.get(set);
|
||||
if (info != .error_set) return false;
|
||||
return std.mem.eql(u8, self.module.types.getString(info.error_set.name), "!");
|
||||
}
|
||||
|
||||
/// Diagnose every tag of `src` that is not also a member of `dst` (the
|
||||
/// enclosing function's named error set). Both must be `.error_set` types.
|
||||
fn checkErrorSetSubset(self: *Lowering, src: TypeId, dst: TypeId, span: ast.Span) void {
|
||||
if (src.isBuiltin() or dst.isBuiltin()) return;
|
||||
const src_info = self.module.types.get(src);
|
||||
const dst_info = self.module.types.get(dst);
|
||||
if (src_info != .error_set or dst_info != .error_set) return;
|
||||
for (src_info.error_set.tags) |tag| {
|
||||
var found = false;
|
||||
for (dst_info.error_set.tags) |d| {
|
||||
if (d == tag) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, span, "error tag 'error.{s}' is not in caller's error set '{s}'", .{ self.module.types.getTagName(tag), self.module.types.getString(dst_info.error_set.name) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `raise EXPR;` — terminate the enclosing failable function via the error
|
||||
/// channel. E1.3 lowers the **pure-failable** shape (`-> !` / `-> !Named`,
|
||||
/// whose return type IS the error set): emit `ret(EXPR)`. The value-carrying
|
||||
/// shape (`-> (T..., !)`) needs the value slots set to `undef` alongside the
|
||||
/// error slot — that tuple ABI lands in E2.1/E2.2, so we bail loudly here
|
||||
/// rather than ship a half-built return that silently corrupts value slots.
|
||||
fn lowerRaise(self: *Lowering, rs: *const ast.RaiseStmt, span: ast.Span) void {
|
||||
// (1) `raise` is legal only inside a failable function.
|
||||
const ret_ty = self.effectiveReturnType() orelse {
|
||||
self.diagRaiseNotFailable(span);
|
||||
return;
|
||||
};
|
||||
const err_set = self.errorChannelOf(ret_ty) orelse {
|
||||
self.diagRaiseNotFailable(span);
|
||||
return;
|
||||
};
|
||||
const inferred = self.isInferredErrorSet(err_set);
|
||||
|
||||
// (2) Set check. Lowering EXPR with the function's error set as the
|
||||
// target type makes a literal `raise error.X` validate `X ∈ set`
|
||||
// inside lowerErrorTagLiteral (the inferred placeholder accepts any
|
||||
// tag). The variable form `raise e` is subset-checked below.
|
||||
const saved_target = self.target_type;
|
||||
self.target_type = err_set;
|
||||
const tag_ref = self.lowerExpr(rs.tag);
|
||||
self.target_type = saved_target;
|
||||
|
||||
if (!inferred and !isErrorTagLiteralNode(rs.tag)) {
|
||||
if (self.errorSetTypeOf(rs.tag)) |src_set| {
|
||||
self.checkErrorSetSubset(src_set, err_set, span);
|
||||
}
|
||||
}
|
||||
|
||||
// (3) Emit the failure return. Pure-failable: the return type IS the
|
||||
// error set, so return the tag value directly.
|
||||
if (ret_ty == err_set) {
|
||||
self.emitBlockDefers(self.func_defer_base);
|
||||
const tag_ty = self.builder.getRefType(tag_ref);
|
||||
const coerced = if (tag_ty != err_set) self.coerceToType(tag_ref, tag_ty, err_set) else tag_ref;
|
||||
if (self.inline_return_target) |iri| {
|
||||
self.builder.store(iri.slot, coerced);
|
||||
self.builder.br(iri.done_bb, &.{});
|
||||
} else {
|
||||
self.builder.ret(coerced, err_set);
|
||||
}
|
||||
} else {
|
||||
// Value-carrying `-> (T..., !)`: needs undef value slots + the error
|
||||
// slot, assembled per the error-channel tuple ABI (ERR E2.1/E2.2).
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, span, "`raise` in a value-carrying failable function (`-> (T..., !)`) is not yet lowered — pending the error-channel tuple ABI (ERR E2); use a `-> !` / `-> !Named` signature for now", .{});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn diagRaiseNotFailable(self: *Lowering, span: ast.Span) void {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, span, "`raise` is only valid inside a failable function (a return type with `!` or `!Named`)", .{});
|
||||
}
|
||||
}
|
||||
|
||||
fn binOpSymbol(op: ast.BinaryOp.Op) []const u8 {
|
||||
return switch (op) {
|
||||
.add => "+",
|
||||
|
||||
@@ -32,6 +32,11 @@ pub const Parser = struct {
|
||||
/// rejected — an error during cleanup has no propagation target. E1.7
|
||||
/// extends this to the full {try, return, break, continue} set.
|
||||
in_onfail_body: bool = false,
|
||||
/// When true (set while parsing a `defer` body), a `raise` statement is
|
||||
/// rejected — same reason as `onfail`: cleanup runs while the function is
|
||||
/// already exiting, so there is nothing to propagate to. E1.7 extends this
|
||||
/// to the full {try, return, break, continue} set.
|
||||
in_defer_body: bool = false,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, source: [:0]const u8) Parser {
|
||||
var lexer = Lexer.init(source);
|
||||
@@ -2010,6 +2015,9 @@ pub const Parser = struct {
|
||||
if (self.current.tag == .kw_defer) {
|
||||
const start = self.current.loc.start;
|
||||
self.advance();
|
||||
const saved_defer = self.in_defer_body;
|
||||
self.in_defer_body = true;
|
||||
defer self.in_defer_body = saved_defer;
|
||||
const deferred = try self.parseExpr();
|
||||
try self.expect(.semicolon);
|
||||
return try self.createNode(start, .{ .defer_stmt = .{ .expr = deferred } });
|
||||
@@ -2021,6 +2029,9 @@ pub const Parser = struct {
|
||||
if (self.in_onfail_body) {
|
||||
return self.fail("`raise` is not allowed inside an `onfail` body — an error during cleanup has no propagation target");
|
||||
}
|
||||
if (self.in_defer_body) {
|
||||
return self.fail("`raise` is not allowed inside a `defer` body — an error during cleanup has no propagation target");
|
||||
}
|
||||
self.advance();
|
||||
const tag_expr = try self.parseExpr();
|
||||
try self.expect(.semicolon);
|
||||
@@ -4247,6 +4258,13 @@ test "E0.2 raise rejected inside an onfail body" {
|
||||
try std.testing.expectError(error.ParseError, parser.parse());
|
||||
}
|
||||
|
||||
test "E1.3 raise rejected inside a defer body" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () { defer { raise error.X; } }");
|
||||
try std.testing.expectError(error.ParseError, parser.parse());
|
||||
}
|
||||
|
||||
test "E0.2 onfail with binding and block body" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
Reference in New Issue
Block a user