fix: initialize the error-channel slot on every failable implicit success return (issue 0190)
A failable function that returned by IMPLICIT success (no explicit
`return`) left its error-tag slot uninitialized, so a caller's `catch` /
`or` (or `main`) read a garbage tag and reported a phantom unhandled
error — and for value-carrying failables the success value was dropped.
The "no error" sentinel was only written on the explicit-`return;` path.
Unified all function-body-return lowering so the failable-success slot
is always written:
- void `-> !` fall-through: `ensureTerminator` (control_flow.zig) now
emits `ret constInt(0)` for a pure-failable end-of-body.
- value-failable trailing-expression success: `lowerValueBody`
(stmt.zig) routes through `lowerFailableSuccessReturn`.
- generic + pack-fn instances: `monomorphizeFunction` (generic.zig) and
`monomorphizePackFn` (pack.zig) now DELEGATE their body-return to
`lowerValueBody` instead of hand-rolling a `coerce`+`ret` that drifted
(covers generic/pack value-failables).
Also fixes the missing-value diagnostic guard added here: it now counts
`.err`-level diagnostics (new `DiagnosticList.errorCount`) rather than the
total list length, so a warning/note emitted while lowering the body
(e.g. an ObjC selector arity warning) can no longer suppress a genuine
"body produces no value" error — which previously shipped an
uninitialized return at exit 0.
Regressions: examples/errors/1061 (void fall-through), 1062 (value-failable
trailing expr), 1063 (generic value-failable trailing expr).
This commit is contained in:
@@ -1,6 +1,70 @@
|
||||
# 0190 — void failable (`-> !`) implicit fall-through leaves the error slot uninitialized
|
||||
|
||||
**Status:** OPEN
|
||||
**Status:** RESOLVED
|
||||
|
||||
> **Root cause:** `ensureTerminator` (the unified implicit fall-through /
|
||||
> epilogue synthesis in `src/ir/lower/control_flow.zig`) handled `void` and
|
||||
> `noreturn`, but for a pure-failable return type (an `.error_set`) it fell
|
||||
> through to the generic `else` arm and emitted `ret const_undef(ret_ty)`,
|
||||
> leaving the error-channel slot undefined. The bare-`return;` path in
|
||||
> `lowerReturn` (`src/ir/lower/stmt.zig`) already wrote `constInt(0, ret_ty)`
|
||||
> ("no error") for the same case, so adding an explicit `return;` masked the
|
||||
> bug.
|
||||
>
|
||||
> **Fix:** `src/ir/lower/control_flow.zig` `ensureTerminator` — added an arm
|
||||
> that, for a pure-failable (`!ret_ty.isBuiltin() and types.get(ret_ty) ==
|
||||
> .error_set`) end-of-body fall-through, emits `ret constInt(0, ret_ty)`,
|
||||
> matching the explicit-`return;` success path. This covers ALL failable
|
||||
> functions that fall through, not just `main`.
|
||||
>
|
||||
> **Sibling fix (value-failable trailing success expression):** adversarial
|
||||
> review found the SAME uninitialized-error-slot bug on a value-carrying
|
||||
> failable (`-> T !E` / `-> Tuple(A,B) !E`) whose body ends in a trailing
|
||||
> success EXPRESSION (no explicit `return`). `lowerValueBody`
|
||||
> (`src/ir/lower/stmt.zig`) blindly `coerceToType`+`ret`'d the bare success
|
||||
> value to the full failable tuple type, leaving the success error-tag slot
|
||||
> uninitialized → phantom `catch`/`or` on SUCCESS (and a dropped value /
|
||||
> `ret { ... } undef` for string + multi-value returns). **Fix:** before the
|
||||
> generic `coerceToType`+`ret`, mirror the explicit-`return EXPR;` branch — when
|
||||
> `ret_ty` is a value-failable tuple (`!ret_ty.isBuiltin() and
|
||||
> types.get(ret_ty) == .tuple and self.errorChannelOf(ret_ty) != null`) call
|
||||
> `self.lowerFailableSuccessReturn(val, ret_ty, span)` so the success error slot
|
||||
> is set to 0. The pure-failable fall-through (above) and the missing-value
|
||||
> error case (`-> i64 !E { }`) are untouched.
|
||||
>
|
||||
> **Generic + pack-instance fix (unification):** the same trailing-value
|
||||
> body-return was hand-rolled (`coerceToType`+`ret`, no failable-success
|
||||
> routing) in TWO more places — `monomorphizeFunction`
|
||||
> (`src/ir/lower/generic.zig`) and `monomorphizePackFn`
|
||||
> (`src/ir/lower/pack.zig`). A generic value-failable `($T) -> T !E { v }`
|
||||
> instantiated at i64 / string / struct shipped an uninitialized error slot →
|
||||
> phantom `catch` on success and `or` silently yielding the fallback (value
|
||||
> corruption). **Fix:** both sites now DELEGATE the trailing-value return to the
|
||||
> shared `lowerValueBody` (the same helper the decl path uses) instead of
|
||||
> re-implementing it, so the value-failable success routing, the pure-failable
|
||||
> fall-through, and the missing-value diagnostic are all handled in one place
|
||||
> and can't drift again. With this, every body-return path that can carry a
|
||||
> failable channel (decl, generic, pack-instance, closure/lambda) routes the
|
||||
> trailing-success value through `lowerFailableSuccessReturn`. (The JNI
|
||||
> native-method entry wrapper in `ffi.zig` still hand-rolls its body-return,
|
||||
> but a JNI export crosses the C-ABI boundary where the error channel is
|
||||
> forbidden by the ERR E5.1 FFI-boundary rule, so it can never be value-failable.)
|
||||
>
|
||||
> **Regression tests:**
|
||||
> - `examples/errors/1061-errors-void-failable-fallthrough.sx` — a `-> !` callee
|
||||
> that succeeds by fall-through (its `catch` must not fire) called from a
|
||||
> `main :: () -> !` that also falls through (exit 0).
|
||||
> - `examples/errors/1062-errors-value-failable-trailing-expr.sx` — value-failable
|
||||
> trailing-expression successes (`-> i64 !E { 99 }`, `-> string !E { "hi" }`,
|
||||
> `-> Tuple(i64,i64) !E { .(1,2) }`) each `catch`-handled (catch must not fire,
|
||||
> value correct), plus a real `raise` still firing the caller's catch.
|
||||
> - `examples/errors/1063-errors-generic-value-failable-trailing-expr.sx` — a
|
||||
> generic value-failable `($T) -> T !E { v }` instantiated at i64 / string /
|
||||
> struct (each `catch`-handled, catch must not fire, value correct), the
|
||||
> `or`-form yielding the real value not the fallback, plus a generic that
|
||||
> `raise`s still firing the caller's catch.
|
||||
>
|
||||
> Full suite green (examples: 813 ran, 0 failed).
|
||||
|
||||
## Symptom
|
||||
|
||||
|
||||
Reference in New Issue
Block a user