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:
agra
2026-05-31 19:09:32 +03:00
parent 5a24a1177d
commit 9984fa6b96
8 changed files with 241 additions and 11 deletions

View File

@@ -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 => "+",

View File

@@ -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();