diff --git a/examples/236-failable-discard-reject.sx b/examples/236-failable-discard-reject.sx new file mode 100644 index 0000000..c48fe9c --- /dev/null +++ b/examples/236-failable-discard-reject.sx @@ -0,0 +1,30 @@ +// Failable error-slot discard rejection (ERR step E1.8 — discard slice). The +// error slot of a value-carrying failable cannot be dropped on a bare +// destructure: it must be bound (`v, err := …`) and handled, or the failure +// routed through `try` / `catch` / `or value` (all of which strip the error +// channel, so they don't reach this check). Two rejected shapes here: +// (1) omitting the error slot entirely (fewer names than slots), and +// (2) binding it to `_`. +// This file is expected to FAIL compilation (exit 1). +// +// Run: ./zig-out/bin/sx run examples/236-failable-discard-reject.sx + +#import "modules/std.sx"; + +E :: error { Bad, Empty } + +pair :: (n: s32) -> (s32, s32, !E) { + if n < 0 { raise error.Bad; } + return (n, n + 1); +} + +parse :: (n: s32) -> (s32, !E) { + if n < 0 { raise error.Bad; } + return n * 2; +} + +main :: () -> s32 { + a, b := pair(5); // ERROR: error slot omitted (3 slots, 2 names) + v, _ := parse(5); // ERROR: error slot discarded with `_` + return a + b + v; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 0e8855f..0842d43 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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 diff --git a/tests/expected/236-failable-discard-reject.exit b/tests/expected/236-failable-discard-reject.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/tests/expected/236-failable-discard-reject.exit @@ -0,0 +1 @@ +1 diff --git a/tests/expected/236-failable-discard-reject.txt b/tests/expected/236-failable-discard-reject.txt new file mode 100644 index 0000000..f365ed3 --- /dev/null +++ b/tests/expected/236-failable-discard-reject.txt @@ -0,0 +1,11 @@ +error: the error slot of a failable cannot be dropped — bind it (`v, err := …`) and handle it, or use `try` / `catch` + --> /Users/agra/projects/sx/examples/236-failable-discard-reject.sx:27:13 + | +27 | a, b := pair(5); // ERROR: error slot omitted (3 slots, 2 names) + | ^^^^^^^ + +error: the error slot of a failable cannot be dropped — bind it (`v, err := …`) and handle it, or use `try` / `catch` + --> /Users/agra/projects/sx/examples/236-failable-discard-reject.sx:28:13 + | +28 | v, _ := parse(5); // ERROR: error slot discarded with `_` + | ^^^^^^^^