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:
@@ -2216,22 +2216,18 @@ pub const Lowering = struct {
|
||||
self.module.functions.items[@intFromEnum(fid)].ret
|
||||
else
|
||||
TypeId.s64;
|
||||
// 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
|
||||
// A value-carrying failable (`-> (T..., !)`) returns its VALUE part and
|
||||
// the success error slot (0) is appended by lowerFailableSuccessReturn.
|
||||
// Resolve a BARE returned value 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;
|
||||
// is stamped with the tuple type — which the success-return path then
|
||||
// mistakes for a forwarded full tuple, dropping the appended `0` slot.
|
||||
// An explicit full failable tuple return (`return (v..., e)`) keeps the
|
||||
// full-tuple target so its trailing error element resolves against the
|
||||
// error set; it is then forwarded as-is. Applies to the inlined
|
||||
// comptime-body return path too (iri.ret_ty is the failable tuple there).
|
||||
const target_for_value = self.failableReturnTarget(ret_ty_for_target, rs.value);
|
||||
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;
|
||||
@@ -2251,6 +2247,20 @@ pub const Lowering = struct {
|
||||
// terminator do its job.
|
||||
if (self.inline_return_target) |iri| {
|
||||
if (ret_val) |ref| {
|
||||
// Value-carrying failable inlined body: append the success error
|
||||
// slot (0) exactly like the real-return path below.
|
||||
// lowerFailableSuccessReturn routes through emitTupleRet, which
|
||||
// stores into iri.slot and branches to iri.done_bb for an inline
|
||||
// target. Defers first, so the returned SSA value is materialized
|
||||
// before they run (matching the real-return ordering).
|
||||
if (!iri.ret_ty.isBuiltin() and
|
||||
self.module.types.get(iri.ret_ty) == .tuple and
|
||||
self.errorChannelOf(iri.ret_ty) != null)
|
||||
{
|
||||
self.emitBlockDefers(self.func_defer_base);
|
||||
self.lowerFailableSuccessReturn(ref, iri.ret_ty, rs.value.?.span);
|
||||
return;
|
||||
}
|
||||
const val_ty = self.builder.getRefType(ref);
|
||||
const coerced = if (val_ty != iri.ret_ty)
|
||||
self.coerceToType(ref, val_ty, iri.ret_ty)
|
||||
@@ -15185,6 +15195,25 @@ pub const Lowering = struct {
|
||||
} });
|
||||
}
|
||||
|
||||
/// The `target_type` to lower a returned expression against. For a
|
||||
/// value-carrying failable (`-> (T..., !)`) a BARE returned value resolves
|
||||
/// against the success value type (so a bare enum literal gets its real
|
||||
/// ordinal); an EXPLICIT full failable tuple literal (`return (v..., e)`,
|
||||
/// arity == full-tuple field count) keeps the failable-tuple target so its
|
||||
/// trailing error element resolves against the error set and is forwarded
|
||||
/// as-is. Every other return type passes through unchanged.
|
||||
fn failableReturnTarget(self: *Lowering, ret_ty: TypeId, value_node: ?*const Node) TypeId {
|
||||
if (ret_ty.isBuiltin()) return ret_ty;
|
||||
if (self.module.types.get(ret_ty) != .tuple) return ret_ty;
|
||||
if (self.errorChannelOf(ret_ty) == null) return ret_ty;
|
||||
if (value_node) |vn| {
|
||||
if (vn.data == .tuple_literal and
|
||||
vn.data.tuple_literal.elements.len == self.module.types.get(ret_ty).tuple.fields.len)
|
||||
return ret_ty;
|
||||
}
|
||||
return self.failableSuccessType(ret_ty);
|
||||
}
|
||||
|
||||
/// Extract the success value from an evaluated value-carrying failable
|
||||
/// tuple `result` (type `op_ty`): the lone value slot for single-value,
|
||||
/// or an assembled value-tuple (typed `succ_ty`) for multi-value.
|
||||
|
||||
Reference in New Issue
Block a user