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:
56
issues/0197-annotated-assignment-type-mismatch-no-check.md
Normal file
56
issues/0197-annotated-assignment-type-mismatch-no-check.md
Normal 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.
|
||||
Reference in New Issue
Block a user