fix(ir): value-failable returning an enum zeroes the success error slot [0097]

A `-> (Enum, !E)` `return .variant` lowered the enum literal with
`target_type` set to the full failable tuple `(Enum, !E)` instead of the
success value type. The bare literal resolves its tag against `target_type`;
against a tuple it matched no variant (silent tag 0) and was stamped with the
tuple type, so `lowerFailableSuccessReturn` saw `val_ty == ret_ty` and took the
forwarding branch — returning the half-built `{value, undef}` aggregate and
never appending the `0` error slot. Every runtime read of the slot on the
success path (`cast(s64) e`, bare `if e`, `e == error.X`) saw garbage nonzero;
only the compile-time `if !e` proof masked it. The s32 case was already correct
because integer literals don't resolve variants against `target_type`.

Fix: in lowerReturn, narrow `target_type` to `failableSuccessType(ret_ty)` for
a value-carrying failable before lowering the returned expression. The enum
literal then resolves to its real ordinal and is typed as the value type, so
the success path correctly appends `0`. Forwarding (`return call()` / explicit
`(v, e)`) is unaffected — those still yield a value typed equal to the tuple.

Regression: examples/1055-errors-enum-value-failable-error-slot.sx reads the
error slot at runtime on the success path (cast, bare if, == error.X), checks a
non-zero ordinal (.blue=2, also corrupted to 0 pre-fix), and asserts the error
path still carries the right tag + error_tag_name. Fails pre-fix, passes after.
This commit is contained in:
agra
2026-06-05 22:10:14 +03:00
parent e466bd5ddf
commit 82366a93df
6 changed files with 171 additions and 1 deletions

View File

@@ -2216,7 +2216,23 @@ pub const Lowering = struct {
self.module.functions.items[@intFromEnum(fid)].ret
else
TypeId.s64;
if (ret_ty_for_target != .void) self.target_type = ret_ty_for_target;
// A value-carrying failable (`-> (T..., !)`) returns its VALUE part; the
// success error slot is appended below by lowerFailableSuccessReturn.
// Resolve the returned expression against that value type, NOT the
// failable tuple: a bare enum literal `.variant` resolves its tag against
// `target_type`, and against the tuple it matches no variant (tag 0) and
// is stamped with the tuple type — which lowerFailableSuccessReturn then
// treats as a forwarded full tuple, dropping the appended `0` error slot.
// Forwarding (`return call()` / explicit `(v, e)`) still yields a value
// typed equal to the full tuple, so it is unaffected.
const target_for_value: TypeId = if (self.inline_return_target == null and
!ret_ty_for_target.isBuiltin() and
self.module.types.get(ret_ty_for_target) == .tuple and
self.errorChannelOf(ret_ty_for_target) != null)
self.failableSuccessType(ret_ty_for_target)
else
ret_ty_for_target;
if (target_for_value != .void) self.target_type = target_for_value;
// Evaluate return value first (before defers)
const ret_val = if (rs.value) |val| self.lowerExpr(val) else null;
self.target_type = old_target;