ERR/E1.8: reject dropping a failable's error slot on destructure

The error slot of a value-carrying failable can no longer be silently dropped
on a bare destructure. In lowerDestructureDecl, when the RHS is failable
(errorChannelOf(ty) != null), the error slot (always the last tuple field)
must be bound to a non-`_` name. Reject when it is omitted entirely (fewer
names than slots — e.g. `a, c := inc(5)` for `inc: -> (s32,s32,!E)`) or bound
to `_` (`v, _ := parse(5)`).

The `try` / `catch` / `or value` consumer forms all strip the error channel
(their result type is non-failable), so the check never fires on them — only a
bare failable destructure is rejected. Value-slot `_` discards stay legal
(`a, _, ae := pair()` binds the error).

This is the discard-rejection slice of E1.8; the path-sensitive flow-check
(value live only where err==null is provable) is a separate follow-up.

examples/236-failable-discard-reject.sx covers both rejected shapes (exit 1).
Gates: zig build, zig build test, 274/274 examples.
This commit is contained in:
agra
2026-06-01 00:40:05 +03:00
parent f96bcc4fe4
commit 66740fa95b
4 changed files with 59 additions and 0 deletions

View File

@@ -8313,6 +8313,23 @@ pub const Lowering = struct {
const tuple = ti.tuple;
if (dd.names.len > tuple.fields.len) return;
// E1.8 (discard rejection): when the RHS is a value-carrying failable,
// the error slot (always the LAST tuple field) cannot be dropped. It is
// dropped when the destructure omits it (fewer names than fields, so the
// trailing error slot is never reached) or binds it to `_`. The `try` /
// `catch` / `or value` consumer forms all strip the error channel (their
// result type is non-failable), so this fires only on a BARE failable
// destructure — exactly the case that would let an error vanish silently.
if (self.errorChannelOf(ty) != null) {
const err_dropped = dd.names.len < tuple.fields.len or
std.mem.eql(u8, dd.names[dd.names.len - 1], "_");
if (err_dropped) {
if (self.diagnostics) |diags| {
diags.addFmt(.err, dd.value.span, "the error slot of a failable cannot be dropped — bind it (`v, err := …`) and handle it, or use `try` / `catch`", .{});
}
}
}
// Extract each field and bind to a new variable
for (dd.names, 0..) |name, i| {
if (std.mem.eql(u8, name, "_")) continue; // discard