Files
sx/issues/0200-named-generic-multi-return-implicit-return.md
agra b322dcfe61 fix: type-safe stores + Any unbox/eq; finish multi-return deferrals
Type-checking gaps (segfault/corruption → compile errors):

- 0197: reject a store into an annotated slot whose value has no modeled
  coercion AND a different byte width (a 16-byte string into a 4-byte i32
  overran the slot and segfaulted). New checkAssignable / noneReinterpretIsUnsafe
  (coerce.zig, width via the LLVM-accurate typeSizeBytes) wired into every store
  site: var/const-decl, single + multi assignment (identifier/field/index/
  element/deref), named-return defaults. Same-width reinterpretations (*T→[*]T,
  i64→isize, fn-ref) and explicit xx/cast stay allowed; cascades suppressed via
  externalErrorsExist. Examples 1205, 1206.
- 0198: an implicit `Any → T` unbox is now a compile error (it blindly
  reinterpreted the boxed payload — silent garbage for a wrong scalar, a segfault
  for an aggregate). xx and compiler-generated match/pack unboxes are unaffected.
  Example 1207.
- 0199: `Any == <concrete>` (one operand Any) aborted the LLVM verifier — the
  comparison arm now fires when either operand is Any, boxing the concrete side
  first. Example 0654.

Multi-return deferrals (PLAN-MULTIRET #6 + named-order + D3 + generic):

- Reorder named return elements by name instead of requiring slot order; error on
  unknown/duplicate/missing (value-only AND full-failable-tuple forms). Examples
  0210, 0214.
- Reject a bare-paren (A, B) multi-return signature in generic-arg position
  (return-position-only). Example 0215.
- Multi-return closure types / lambda literals work via the reused tuple
  machinery (destructure, single-bind+field, lambda arg). Example 0216.
- Generic multi-return: positional works (0217); 0200: the named-slot
  implicit-return form now works for generic free fns + struct methods —
  monomorphizeFunction now calls bindNamedReturnSlots. Example 0218.

readme.md documents the annotated-store coercion rule; CHECKPOINT-MULTIRET.md
updated. Full corpus green (850/0).
2026-06-27 17:28:27 +03:00

4.0 KiB
Raw Blame History

RESOLVED (2026-06-27). Root cause exactly as hypothesized: the generic monomorph path monomorphizeFunction (src/ir/lower/generic.zig) bound params and lowered the body via lowerValueBody, but NEVER called bindNamedReturnSlots — so named_return_names stayed null and the implicit-return synthesis (lowerValueBody, stmt.zig) didn't fire. (The non-generic decl path lowerFunctionBodyInto already called it.) Fix: call bindNamedReturnSlots(fd, ret_ty, &scope) in monomorphizeFunction after param-binding, with the same named_return_names/named_return_defaults save/restore. Covers generic free functions AND generic struct methods (the instance-method path shares the monomorph), with defaults and the failable error channel. Regression test: examples/types/0218-types-multi-return-generic-named.sx.

0200 — named-return locals don't synthesize the implicit return in a GENERIC multi-return function

Symptom — A generic function with a NAMED multi-return (-> (first: $T, second: $U)) that relies on the implicit return (assigns the named slot locals, no explicit return) fails to compile: the named-return-locals synthesis does not fire for the monomorphized instance, so it reports "body produces no value".

  • Observed: pair :: (a: $T, b: $U) -> (first: T, second: U) { first = a; second = b; }error: function returns '(first: i64, second: bool)' but its body produces no value — end it with a trailing expression (no ';') or an explicit 'return'.
  • Expected: the named slot locals (first, second) are bound and the implicit return is synthesized from them, exactly as for a NON-generic named multi-return.

Note the diagnostic shows the return type RESOLVED to concrete types ((first: i64, second: bool)) — so binding/return-type resolution ran; only the named-return-LOCALS path (bindNamedReturnSlotsself.named_return_names) did not take effect for the generic instance.

WORKS (so this is narrow): the POSITIONAL generic multi-return with an explicit return is fine — (a: $T, b: $U) -> (T, U) { return a, b; } and explicit-type pair(i32, bool, 7, true) both run correctly. Only the named-slot IMPLICIT-return form × generic monomorph is broken. Workaround: use an explicit return a, b.

Reproduction

#import "modules/std.sx";

pair :: (a: $T, b: $U) -> (first: T, second: U) {
    first = a;
    second = b;          // implicit return from named slots — never synthesized
}

main :: () -> i64 {
    x, y := pair(7, true);
    print("{} {}\n", x, y);
    return 0;
}

./zig-out/bin/sx run repro.sx → the "produces no value" error, exit 1.

Investigation prompt

The implicit-return-from-named-slots synthesis (lowerValueBody in src/ir/lower/stmt.zig ~line 172: if (self.named_return_names) |names| { … }) only fires when self.named_return_names is set by bindNamedReturnSlots (src/ir/lower/stmt.zig ~258). That binder is called from lowerFunctionBodyInto (src/ir/lower/decl.zig:2729). bindNamedReturnSlots early-returns unless fd.return_type.?.data == .return_type_expr.

The likely cause: the generic-FREE-function monomorph lowers the instance with a SUBSTITUTED return-type node (the $T/$U resolved into a concrete tuple_type_expr or a resolved TypeId), so fd.return_type.data is no longer .return_type_exprbindNamedReturnSlots early-returns → named_return_names stays null → the implicit return isn't synthesized. Confirm by checking the generic free-function instantiation path (search instantiateGeneric / lazyLowerFunction / the monomorph that rewrites fd for free functions): does it preserve the original ReturnTypeExpr AST node (binding via type_bindings), or rewrite it? The fix likely keys bindNamedReturnSlots off the ORIGINAL template fd.return_type (which carries field_names), or threads the field-names through the monomorph. Generic STRUCT methods may have the same gap — test Box(T) with a named multi-return method.

Verify: the repro prints 7 true, exit 0. Add a positive generics example.