diff --git a/issues/0190-void-failable-fallthrough-uninitialized-error-slot.md b/issues/0190-void-failable-fallthrough-uninitialized-error-slot.md new file mode 100644 index 00000000..5976f50d --- /dev/null +++ b/issues/0190-void-failable-fallthrough-uninitialized-error-slot.md @@ -0,0 +1,71 @@ +# 0190 — void failable (`-> !`) implicit fall-through leaves the error slot uninitialized + +**Status:** OPEN + +## Symptom + +A `-> !` (void failable) function that exits by **implicit fall-through** +(no explicit `return;`) does not initialize its error-channel slot, so a +caller (or `main`) reads a non-zero garbage tag and reports a phantom +unhandled error. + +- Observed: `main :: () -> ! { print("ok\n"); }` prints `ok` then + `error: unhandled error reached main: error.` and exits **1**. +- Expected: exit **0** (specs.md §11: "the exit code is `0` for void / + `-> !` success"). Adding an explicit trailing `return;` makes it exit 0. + +This is the silent-uninitialized-slot failure mode: the success path +should write "no error" into the channel just like an explicit `return;` +does, but the fall-through path skips it. + +## Reproduction + +```sx +#import "modules/std.sx"; + +main :: () -> ! { + print("ok\n"); +} +``` + +Run: `./zig-out/bin/sx run repro.sx` → prints `ok`, then +`error: unhandled error reached main: error.`, exit 1 (should be 0). + +A non-`main` void failable shows the same uninitialized slot downstream: + +```sx +#import "modules/std.sx"; + +noop :: () -> ! { } // falls through, no `return;` +main :: () { + noop() catch (e) { print("phantom: {}\n", e); } // fires spuriously +} +``` + +Workaround (confirms root cause): an explicit `return;` at the end of the +`-> !` body initializes the slot and the phantom error disappears. + +## Investigation prompt + +The error channel for a `-> !` function is the last slot of the return +aggregate (specs.md §12 ABI). An explicit `return;` lowers to a write of +the "no error" sentinel into that slot; the **implicit fall-through** exit +path (end of body with no `return`) apparently omits that write, leaving +the slot whatever was on the stack. + +Likely area: the function-epilogue / failable-return lowering in +`src/ir/lower/` (the path that synthesizes the implicit return for a +body that falls off the end — search for where a void/`-> !` function's +trailing fall-through is lowered, and where the error slot's "no error" +sentinel is written on the explicit-`return;` path). The fix: the +implicit fall-through of a failable function must initialize the error +slot to "no error" exactly like `return;` does. + +Verification: the two repros above must exit 0 / not fire the catch; +`examples/errors/1026-errors-failable-main.sx` (which currently passes +only because it ends in `return;`) must keep passing. Add a regression +example: a `-> !` function (and a `main :: () -> !`) that succeeds by +fall-through with no explicit `return;`. + +(Found by adversarial review during the tuple-syntax-cutover docs pass, +commit `989e18b7`. Pre-existing — independent of the tuple change.)