ERR/E2.1b: value-carrying failable consumers (try / catch)
The consumer side of the error-channel tuple ABI. A value-carrying `-> (T, !E)`
failable can now be consumed by `try` and `catch` (not just destructured).
Single-value; multi-value `-> (T1, T2, !)` consumers bail (E2).
- lowerTry: a value-carrying callee returns `{v, err}`. Extract `err`
(tuple_get field 1), branch; on success the try value is `tuple_get(field 0)`,
on error propagate via emitErrorReturn (pure caller → `ret(tag)`;
value-carrying caller → `ret {undef..., tag}`). Widening now runs for
value-carrying callees too. Retires the two value-carrying bails.
- lowerCatch: a value-carrying LHS merges through a block-param phi — the
success edge feeds `tuple_get(field 0)`, the handler edge feeds the body's
value (coerced to the success type). runCatchBody factors the bound-tag body
lowering (force_block_value for the value case). Pure-failable catch
unchanged.
- A non-diverging value-carrying catch body that yields no value is now a
clean diagnostic ("`catch` body must produce a value … or diverge") instead
of coercing `void` into a bad ref / failing LLVM verification — caught by an
adversarial review of the lowering.
Tests: examples/229-value-failable-consume.sx (try in value-carrying + pure
callers, catch block/bare/match-body/diverging bodies; exit 32),
examples/230-value-failable-reject.sx (void catch body rejected; exit 1).
Gates: zig build, zig build test, 268/268 examples.
This commit is contained in:
166
src/ir/lower.zig
166
src/ir/lower.zig
@@ -15263,16 +15263,12 @@ pub const Lowering = struct {
|
||||
return self.builder.constInt(0, .void);
|
||||
};
|
||||
|
||||
// E1.4a scope guards — bail loudly on the shapes E2/E1.4b own.
|
||||
if (op_ty != callee_set) {
|
||||
// Value-carrying callee: `try` must bind the value slot(s), which
|
||||
// needs the error-channel tuple ABI (E2).
|
||||
return self.bailTry(span, "a value-carrying failable callee (`-> (T..., !)`)");
|
||||
}
|
||||
if (caller_ret != caller_set) {
|
||||
// Propagating from a value-carrying caller needs undef value slots
|
||||
// alongside the error slot (E2's tuple ABI).
|
||||
return self.bailTry(span, "a value-carrying failable function");
|
||||
// A value-carrying callee (`-> (T, !)`) returns a tuple `{v, err}`; a
|
||||
// pure-failable callee (`-> !`) returns the bare error tag. Multi-value
|
||||
// callees (`-> (T1, T2, !)`) need multi-slot extraction — deferred.
|
||||
const callee_value_carrying = op_ty != callee_set;
|
||||
if (callee_value_carrying and self.module.types.get(op_ty).tuple.fields.len != 2) {
|
||||
return self.bailTry(span, "a multi-value failable callee (`-> (T1, T2, !)`)");
|
||||
}
|
||||
|
||||
// (3) Widening: the callee's escape set must be ⊆ the caller's named
|
||||
@@ -15293,28 +15289,64 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// (4) Lower: evaluate the call (→ the error tag, 0 = success), branch.
|
||||
const err_val = self.lowerExpr(operand);
|
||||
// (4) Lower: evaluate the operand, then branch on its error tag (which
|
||||
// is the bare result for a pure callee, or the last tuple slot for
|
||||
// a value-carrying one).
|
||||
const result = self.lowerExpr(operand);
|
||||
const err_val = if (callee_value_carrying)
|
||||
self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 1, .base_type = op_ty } }, callee_set)
|
||||
else
|
||||
result;
|
||||
const err_ty = self.builder.getRefType(err_val);
|
||||
const zero = self.builder.constInt(0, err_ty);
|
||||
const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = zero } }, .bool);
|
||||
const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_ty) } }, .bool);
|
||||
|
||||
const prop_bb = self.freshBlock("try.prop");
|
||||
const ok_bb = self.freshBlock("try.ok");
|
||||
self.builder.condBr(is_err, prop_bb, &.{}, ok_bb, &.{});
|
||||
|
||||
// Propagation: run the function's defers, then return the error tag
|
||||
// (widened to the caller's set — global-flat tag ids carry across).
|
||||
// Propagation: run the function's defers, then return the caller's
|
||||
// failure carrying this tag (pure caller → `ret(tag)`; value-carrying
|
||||
// caller → `ret {undef..., tag}`).
|
||||
self.builder.switchToBlock(prop_bb);
|
||||
self.emitBlockDefers(self.func_defer_base);
|
||||
const widened = if (err_ty != caller_set) self.coerceToType(err_val, err_ty, caller_set) else err_val;
|
||||
self.builder.ret(widened, caller_set);
|
||||
self.emitErrorReturn(caller_ret, caller_set, err_val);
|
||||
|
||||
// Success: continue. A pure-failable callee has no value slots.
|
||||
// Success: a value-carrying callee yields its value slot; a
|
||||
// pure-failable callee has no value (void).
|
||||
self.builder.switchToBlock(ok_bb);
|
||||
if (callee_value_carrying) {
|
||||
const succ_ty = self.module.types.get(op_ty).tuple.fields[0];
|
||||
return self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 0, .base_type = op_ty } }, succ_ty);
|
||||
}
|
||||
return self.builder.constInt(0, .void);
|
||||
}
|
||||
|
||||
/// Return the enclosing function's failure carrying error tag `err`. A
|
||||
/// pure-failable caller (`-> !`) returns the tag directly; a value-carrying
|
||||
/// caller (`-> (T..., !)`) returns `{undef value slots..., tag}`. Honors
|
||||
/// inline-comptime return targets. The caller emits defers first.
|
||||
fn emitErrorReturn(self: *Lowering, caller_ret: TypeId, caller_set: TypeId, err: Ref) void {
|
||||
const ety = self.builder.getRefType(err);
|
||||
const coerced = if (ety != caller_set) self.coerceToType(err, ety, caller_set) else err;
|
||||
if (caller_ret == caller_set) {
|
||||
if (self.inline_return_target) |iri| {
|
||||
self.builder.store(iri.slot, coerced);
|
||||
self.builder.br(iri.done_bb, &.{});
|
||||
} else {
|
||||
self.builder.ret(coerced, caller_set);
|
||||
}
|
||||
} else {
|
||||
const fields = self.module.types.get(caller_ret).tuple.fields;
|
||||
var undefs = std.ArrayList(Ref).empty;
|
||||
defer undefs.deinit(self.alloc);
|
||||
for (fields[0 .. fields.len - 1]) |vty| {
|
||||
undefs.append(self.alloc, self.builder.constUndef(vty)) catch unreachable;
|
||||
}
|
||||
const tup = self.buildFailableTuple(caller_ret, undefs.items, coerced);
|
||||
self.emitTupleRet(caller_ret, tup);
|
||||
}
|
||||
}
|
||||
|
||||
fn diagTryNotFailable(self: *Lowering, span: ast.Span) void {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, span, "`try` is only valid inside a failable function (a return type with `!` or `!Named`)", .{});
|
||||
@@ -15338,46 +15370,90 @@ pub const Lowering = struct {
|
||||
}
|
||||
return self.builder.constInt(0, .void);
|
||||
};
|
||||
if (op_ty != err_set) {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, span, "`catch` on a value-carrying failable (`-> (T..., !)`) is not yet lowered — pending the error-channel tuple ABI (ERR E2)", .{});
|
||||
}
|
||||
// Pure-failable LHS (`-> !`): no success value. Run the body on the
|
||||
// error path; both paths fall through to a value-less merge.
|
||||
if (op_ty == err_set) {
|
||||
const err_val = self.lowerExpr(ce.operand);
|
||||
const err_ty = self.builder.getRefType(err_val);
|
||||
const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_ty) } }, .bool);
|
||||
const handle_bb = self.freshBlock("catch.handle");
|
||||
const merge_bb = self.freshBlock("catch.merge");
|
||||
self.builder.condBr(is_err, handle_bb, &.{}, merge_bb, &.{});
|
||||
self.builder.switchToBlock(handle_bb);
|
||||
_ = self.runCatchBody(ce, err_val, err_set, null);
|
||||
if (!self.currentBlockHasTerminator()) self.builder.br(merge_bb, &.{});
|
||||
self.builder.switchToBlock(merge_bb);
|
||||
return self.builder.constInt(0, .void);
|
||||
}
|
||||
|
||||
// Evaluate the operand → the error tag (0 = success), then branch.
|
||||
const err_val = self.lowerExpr(ce.operand);
|
||||
const err_ty = self.builder.getRefType(err_val);
|
||||
const zero = self.builder.constInt(0, err_ty);
|
||||
const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = zero } }, .bool);
|
||||
// Value-carrying LHS (`-> (T, !)`): on success the catch yields the
|
||||
// value slot; on error it yields the handler body's value. The paths
|
||||
// merge through a block-parameter (phi). Multi-value is deferred.
|
||||
const fields = self.module.types.get(op_ty).tuple.fields;
|
||||
if (fields.len != 2) {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, span, "`catch` on a multi-value failable (`-> (T1, T2, !)`) is not yet lowered — pending the multi-value error-channel ABI (ERR E2)", .{});
|
||||
}
|
||||
return self.builder.constInt(0, .void);
|
||||
}
|
||||
const succ_ty = fields[0];
|
||||
const result = self.lowerExpr(ce.operand);
|
||||
const err_val = self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 1, .base_type = op_ty } }, err_set);
|
||||
const succ_val = self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 0, .base_type = op_ty } }, succ_ty);
|
||||
const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_set) } }, .bool);
|
||||
|
||||
const handle_bb = self.freshBlock("catch.handle");
|
||||
const merge_bb = self.freshBlock("catch.merge");
|
||||
// On success (err == 0) jump straight to the merge; on error, handle.
|
||||
self.builder.condBr(is_err, handle_bb, &.{}, merge_bb, &.{});
|
||||
const merge_bb = self.freshBlockWithParams("catch.merge", &.{succ_ty});
|
||||
// Success → merge with the value slot; error → run the handler.
|
||||
self.builder.condBr(is_err, handle_bb, &.{}, merge_bb, &.{succ_val});
|
||||
|
||||
// Handle: bind the tag (typed as the error set) and run the body in a
|
||||
// child scope. The body diverges (terminates the block) or falls
|
||||
// through to the merge.
|
||||
self.builder.switchToBlock(handle_bb);
|
||||
const body_val = self.runCatchBody(ce, err_val, err_set, succ_ty);
|
||||
if (!self.currentBlockHasTerminator()) {
|
||||
// A non-diverging handler must produce a value of the success type.
|
||||
// A value-less (void) body is a type error — diagnose and feed an
|
||||
// undef placeholder so the merge phi stays well-typed (rather than
|
||||
// coercing `void` into a bad ref).
|
||||
const bv: Ref = blk: {
|
||||
if (body_val) |v| {
|
||||
const vty = self.builder.getRefType(v);
|
||||
if (vty != .void) break :blk self.coerceToType(v, vty, succ_ty);
|
||||
}
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, span, "`catch` body must produce a value of type '{s}' (or diverge with `return` / `raise`)", .{self.formatTypeName(succ_ty)});
|
||||
}
|
||||
break :blk self.builder.constUndef(succ_ty);
|
||||
};
|
||||
self.builder.br(merge_bb, &.{bv});
|
||||
}
|
||||
|
||||
self.builder.switchToBlock(merge_bb);
|
||||
return self.builder.blockParam(merge_bb, 0, succ_ty);
|
||||
}
|
||||
|
||||
/// Lower a `catch` body in a child scope that binds the error tag to the
|
||||
/// catch binding (if any). When `want_ty` is non-null (value-carrying
|
||||
/// catch), returns the body's value (or null if the body diverged); when
|
||||
/// null (pure-failable catch), runs the body for effect and returns null.
|
||||
fn runCatchBody(self: *Lowering, ce: *const ast.CatchExpr, err_val: Ref, err_set: TypeId, want_ty: ?TypeId) ?Ref {
|
||||
var handle_scope = Scope.init(self.alloc, self.scope);
|
||||
const saved_scope = self.scope;
|
||||
self.scope = &handle_scope;
|
||||
defer {
|
||||
self.scope = saved_scope;
|
||||
handle_scope.deinit();
|
||||
}
|
||||
if (ce.binding) |name| {
|
||||
handle_scope.put(name, .{ .ref = err_val, .ty = err_set, .is_alloca = false });
|
||||
}
|
||||
if (ce.body.data == .block) {
|
||||
self.lowerBlock(ce.body);
|
||||
} else {
|
||||
_ = self.lowerExpr(ce.body);
|
||||
if (want_ty == null) {
|
||||
if (ce.body.data == .block) self.lowerBlock(ce.body) else _ = self.lowerExpr(ce.body);
|
||||
return null;
|
||||
}
|
||||
self.scope = saved_scope;
|
||||
handle_scope.deinit();
|
||||
if (!self.currentBlockHasTerminator()) self.builder.br(merge_bb, &.{});
|
||||
|
||||
// Merge (success path + non-diverging handle). Pure-failable → void.
|
||||
self.builder.switchToBlock(merge_bb);
|
||||
return self.builder.constInt(0, .void);
|
||||
const saved_fbv = self.force_block_value;
|
||||
self.force_block_value = true;
|
||||
defer self.force_block_value = saved_fbv;
|
||||
return if (ce.body.data == .block) self.lowerBlockValue(ce.body) else self.lowerExpr(ce.body);
|
||||
}
|
||||
|
||||
fn bailTry(self: *Lowering, span: ast.Span, comptime what: []const u8) Ref {
|
||||
|
||||
Reference in New Issue
Block a user