fix(ir): value-failable return — narrow target per-form + inline path [0097 F1/F2]

Attempt-1 narrowed lowerReturn's target to failableSuccessType(ret_ty) for
every value-carrying failable. That fixed the bare-enum success slot but
introduced two defects (attempt-2 review):

F1 — explicit full failable tuple `return (.v, error.X)` panicked. With the
target narrowed to the value type, the trailing error element no longer
resolved against the error set, leaving an `.unresolved` tuple field that
tripped "unresolved type reached LLVM emission" in backend/llvm/types.zig.

F2 — a `-> (Enum, !E)` body with a comptime parameter is inlined
(lowerComptimeCall), so its success `return .red` took the inline-return path,
which the first cut skipped: it stored `{value, undef}` (error slot undef) into
the inline slot, so the success error slot read garbage at runtime.

Fix: choose the return-expr target via failableReturnTarget(ret_ty, value_node)
— a BARE value resolves against failableSuccessType (real enum ordinal), while
an EXPLICIT full failable tuple literal (arity == full-tuple field count) keeps
the full-tuple target and is forwarded as-is. This applies on the inline path
too, and the inline value-failable return now routes through
lowerFailableSuccessReturn (whose emitTupleRet stores `{value, 0}` into the
inline slot + branches), so the success error slot is 0 there as well.

Regression: examples/1056-errors-enum-value-failable-tuple-and-comptime.sx —
F1 explicit-tuple error return + bare-value success in one fn (no panic, slot 0
on success, tag 1 on error); F2 comptime-param enum value-failable read at
runtime on the success path (cast, bare if, == error.X) + error path. Reads the
slot at runtime so an undef is caught, not masked by the `if !e` proof.
examples/1055 + the original 0097 repro still pass. Gate: zig build 0,
zig build test 0, run_examples.sh 453 ok / 0 failed / 0 timed out.
This commit is contained in:
agra
2026-06-05 22:42:12 +03:00
parent 82366a93df
commit 1151d77e96
6 changed files with 155 additions and 25 deletions

View File

@@ -12,17 +12,37 @@ with the tuple type. `lowerFailableSuccessReturn` then saw `val_ty == ret_ty` an
`constInt(0, err_ty)` was never inserted, leaving the error slot `undef` (read back as garbage
nonzero) on the success path.
**Fix:** in `lowerReturn`, when the function's return type is a value-carrying failable tuple,
narrow `target_type` to `failableSuccessType(ret_ty)` (the value type / value-tuple) before
lowering the returned expression. The enum literal then resolves to its real ordinal and is typed
as the value type, so the success-return path correctly appends the `0` error slot. The s32 case
was already correct because integer literals don't resolve variants against `target_type`.
**Fix:** in `lowerReturn`, choose the `target_type` for the returned expression via
`failableReturnTarget(ret_ty, value_node)`: for a value-carrying failable a **bare** returned
value resolves against `failableSuccessType(ret_ty)` (the value type / value-tuple) so an enum
literal gets its real ordinal and the success-return path appends the `0` error slot; an
**explicit full failable tuple** literal (`return (v..., e)`, arity == full-tuple field count)
keeps the full-tuple target so its trailing error element resolves against the error set and is
forwarded as-is. The s32 case was already correct because integer literals don't resolve variants
against `target_type`.
**Regression:** `examples/1055-errors-enum-value-failable-error-slot.sx` — reads the error slot at
runtime on the success path (`cast(s64) e`, bare `if e`, `e == error.X`), exercises a non-zero
ordinal (`.blue` = 2, which the bug also corrupted to 0), and asserts the error path still carries
the right tag + `error_tag_name`. Fails on pre-fix code, passes after. Verified `zig build`,
`zig build test`, and `bash tests/run_examples.sh` (452 ok) all green.
Two follow-up defects from the first cut of this fix were corrected (attempt-2 review):
- **F1 — explicit full tuple return panicked.** Narrowing the target to the value type for *all*
value-failables broke `return (.blue, error.Nope)`: the trailing error element no longer
resolved against the error set, leaving an `.unresolved` tuple field that tripped the
"unresolved type reached LLVM emission" panic in `src/backend/llvm/types.zig`. The
arity-aware `failableReturnTarget` keeps the full-tuple target for the explicit form, so it
lowers and forwards as before.
- **F2 — comptime-param inline return still corrupted.** A `-> (Enum, !E)` body with a comptime
parameter is inlined (`lowerComptimeCall`), so its success `return .red` took the
inline-return path (`if (self.inline_return_target)`), which the first cut skipped — it stored
`{value, undef}` (error slot `undef`) into the inline slot. That path now applies the same
target narrowing and routes a value-carrying failable through `lowerFailableSuccessReturn`
(whose `emitTupleRet` stores `{value, 0}` into the inline slot + branches), so the success
error slot is `0` there too.
**Regression:** `examples/1055-errors-enum-value-failable-error-slot.sx` (bare-enum success slot)
and `examples/1056-errors-enum-value-failable-tuple-and-comptime.sx` (F1 explicit-tuple error +
bare-value success in one fn; F2 comptime-param enum value-failable read at runtime on the success
path — `cast`, bare `if`, `== error.X`, plus the error path). Both read the slot at runtime so an
`undef` is caught, not masked by the `if !e` proof. Fail on pre-fix code, pass after. Verified
`zig build`, `zig build test`, and `bash tests/run_examples.sh` (453 ok) all green.
Below preserved as a record of the original problem.