feat: multiple return values — bare-paren signatures, named returns, must-set, defaults

A function may return multiple values via a bare-paren return signature:
`-> (A, B)` / `-> (x: A, y: B)` / `-> (A, B, !)` (error always the last slot),
and `-> ()` is `void`. This is DISTINCT from a `Tuple(…)` value — return-position
only (a dedicated `ReturnTypeExpr` AST node resolving to a reused `.tuple`
TypeId); a parameter / field / variable annotation `x: (A, B)` is rejected. A
single-value `-> (T, !)` stays a plain failable (= `-> T !`).

Returns use the bare comma form `return a, b` / `return x = a, y = b` (no `.( … )`
literal). Consume by destructuring (`a, b := f()`) or single-bind + field access
(`c := f(); c.sum`); a failable bound value holds only the value slots (the error
stays on the `!` channel).

Named return slots are in-scope assignable locals; with no explicit `return` the
implicit return is synthesized from them. Path-sensitive definite-assignment
enforces the must-set rule, and a slot may carry a default that exempts it.
Validation rejects arity mismatches, out-of-slot-order named elements, a
slot/parameter name collision, a comma list from a single-value function, and a
multi-return signature used as a value type.

Examples 0202-0213; readme + specs updated. issues/0197 files a pre-existing
annotated-assignment type-check gap (`x: i32 = "hi"` segfaults) surfaced by the
adversarial review.
This commit is contained in:
agra
2026-06-27 12:31:23 +03:00
parent c94f878e7e
commit 76689a1ea6
65 changed files with 1236 additions and 48 deletions

View File

@@ -0,0 +1,56 @@
# 0197 — annotated assignment with an incompatible type is unchecked (segfaults)
**Symptom** — A variable / constant declared with an explicit type annotation and
an initializer of an INCOMPATIBLE type is accepted with no diagnostic; the value
is passed through unchanged (a `.none` coercion plan), bit-mangling the slot and
segfaulting at run time.
- Observed: `x : i32 = "hi";` compiles, then crashes (`Segmentation fault`).
- Expected: a compile-time diagnostic — `cannot initialize 'x' of type 'i32'
with a value of type 'string'` (or similar), exit code 1, no crash.
This is a GENERAL type-checking gap, not specific to any one feature. It was
surfaced while reviewing the multi-return feature (a named-return slot default
`-> (sum: i32 = "hi", …)` hit the same path; that site now has its own guard, but
the underlying annotated-assignment hole remains).
## Reproduction
```sx
#import "modules/std.sx";
main :: () -> i64 {
x : i32 = "hi"; // string initializer for an i32 slot — no diagnostic
print("{}\n", x); // garbage, then SIGSEGV
return 0;
}
```
`./zig-out/bin/sx run repro.sx` → prints garbage then `Segmentation fault`.
`./zig-out/bin/sx ir repro.sx` does NOT crash (it lowers fine) — the bad coercion
blows up only at run time.
## Investigation prompt
The annotated var/const-decl lowering stores the initializer into the slot
WITHOUT checking that the initializer's type can actually reach the annotated
type. The store goes through `coerceToType` → `coerceMode`
(`src/ir/lower/coerce.zig:596,606`), whose classifier
(`coercionResolver().classify`, `src/ir/conversions.zig:54`) returns `.none` for
an incompatible pair — and `coerceMode`'s `.no_op, .none => return val` arm
(coerce.zig ~614) then passes the value through unchanged, so a 16-byte `string`
lands in a 4-byte `i32` slot (and vice-versa), corrupting memory.
The fix likely belongs at the annotated var-decl / const-decl store sites
(`src/ir/lower/stmt.zig` `lowerVarDecl` ~line 450, and the const-decl path) and
anywhere else a value is stored into an explicitly-annotated slot: when
`classify(src_ty, dst_ty) == .none` and `src_ty != dst_ty`, emit a diagnostic
(`self.diagnostics.addFmt(.err, span, "...", ...)`) instead of silently coercing.
(The multi-return default site already does exactly this — see the
`coercionResolver().classify(...) == .none` guard in `bindNamedReturnSlots`,
`src/ir/lower/stmt.zig` — that pattern can be lifted to a shared helper and reused
at the assignment sites.)
Verification: `./zig-out/bin/sx run repro.sx` should print a type-mismatch
diagnostic and exit non-zero, NOT segfault. Add a `examples/diagnostics/` or
`examples/types/` negative example once fixed.