From 76689a1ea622f96e56f3615af5a76c797f6565fd Mon Sep 17 00:00:00 2001 From: agra Date: Sat, 27 Jun 2026 12:31:23 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20multiple=20return=20values=20=E2=80=94?= =?UTF-8?q?=20bare-paren=20signatures,=20named=20returns,=20must-set,=20de?= =?UTF-8?q?faults?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- current/CHECKPOINT-MULTIRET.md | 100 ++++++++ current/PLAN-MULTIRET.md | 156 ++++++++++++ .../types/0202-types-void-empty-parens.sx | 21 ++ examples/types/0203-types-multi-return.sx | 31 +++ .../types/0204-types-multi-return-failable.sx | 34 +++ .../types/0205-types-named-return-locals.sx | 42 ++++ .../types/0206-types-named-return-must-set.sx | 16 ++ .../types/0207-types-named-return-defaults.sx | 26 ++ ...08-types-named-return-conditional-unset.sx | 18 ++ .../types/0209-types-multi-return-arity.sx | 8 + .../0210-types-multi-return-name-order.sx | 8 + ...0211-types-named-return-param-collision.sx | 8 + .../types/0212-types-single-return-comma.sx | 7 + .../0213-types-multi-return-as-value-type.sx | 6 + .../0202-types-void-empty-parens.exit | 1 + .../0202-types-void-empty-parens.stderr | 1 + .../0202-types-void-empty-parens.stdout | 3 + .../expected/0203-types-multi-return.exit | 1 + .../expected/0203-types-multi-return.stderr | 1 + .../expected/0203-types-multi-return.stdout | 2 + .../0204-types-multi-return-failable.exit | 1 + .../0204-types-multi-return-failable.stderr | 1 + .../0204-types-multi-return-failable.stdout | 3 + .../0205-types-named-return-locals.exit | 1 + .../0205-types-named-return-locals.stderr | 1 + .../0205-types-named-return-locals.stdout | 4 + .../0206-types-named-return-must-set.exit | 1 + .../0206-types-named-return-must-set.stderr | 11 + .../0206-types-named-return-must-set.stdout | 1 + .../0207-types-named-return-defaults.exit | 1 + .../0207-types-named-return-defaults.stderr | 1 + .../0207-types-named-return-defaults.stdout | 2 + ...-types-named-return-conditional-unset.exit | 1 + ...ypes-named-return-conditional-unset.stderr | 11 + ...ypes-named-return-conditional-unset.stdout | 1 + .../0209-types-multi-return-arity.exit | 1 + .../0209-types-multi-return-arity.stderr | 5 + .../0209-types-multi-return-arity.stdout | 1 + .../0210-types-multi-return-name-order.exit | 1 + .../0210-types-multi-return-name-order.stderr | 11 + .../0210-types-multi-return-name-order.stdout | 1 + ...11-types-named-return-param-collision.exit | 1 + ...-types-named-return-param-collision.stderr | 5 + ...-types-named-return-param-collision.stdout | 1 + .../0212-types-single-return-comma.exit | 1 + .../0212-types-single-return-comma.stderr | 5 + .../0212-types-single-return-comma.stdout | 1 + ...0213-types-multi-return-as-value-type.exit | 1 + ...13-types-multi-return-as-value-type.stderr | 5 + ...13-types-multi-return-as-value-type.stdout | 1 + ...tated-assignment-type-mismatch-no-check.md | 56 +++++ readme.md | 53 +++++ specs.md | 33 +++ src/ast.zig | 20 ++ src/ir/lower.zig | 35 +++ src/ir/lower/decl.zig | 23 ++ src/ir/lower/generic.zig | 38 +++ src/ir/lower/nominal.zig | 1 + src/ir/lower/stmt.zig | 223 ++++++++++++++++++ src/ir/semantic_diagnostics.zig | 1 + src/ir/type_bridge.zig | 5 + src/ir/type_resolver.zig | 73 +++--- src/parser.test.zig | 27 ++- src/parser.zig | 122 +++++++++- src/sema.zig | 2 + 65 files changed, 1236 insertions(+), 48 deletions(-) create mode 100644 current/CHECKPOINT-MULTIRET.md create mode 100644 current/PLAN-MULTIRET.md create mode 100644 examples/types/0202-types-void-empty-parens.sx create mode 100644 examples/types/0203-types-multi-return.sx create mode 100644 examples/types/0204-types-multi-return-failable.sx create mode 100644 examples/types/0205-types-named-return-locals.sx create mode 100644 examples/types/0206-types-named-return-must-set.sx create mode 100644 examples/types/0207-types-named-return-defaults.sx create mode 100644 examples/types/0208-types-named-return-conditional-unset.sx create mode 100644 examples/types/0209-types-multi-return-arity.sx create mode 100644 examples/types/0210-types-multi-return-name-order.sx create mode 100644 examples/types/0211-types-named-return-param-collision.sx create mode 100644 examples/types/0212-types-single-return-comma.sx create mode 100644 examples/types/0213-types-multi-return-as-value-type.sx create mode 100644 examples/types/expected/0202-types-void-empty-parens.exit create mode 100644 examples/types/expected/0202-types-void-empty-parens.stderr create mode 100644 examples/types/expected/0202-types-void-empty-parens.stdout create mode 100644 examples/types/expected/0203-types-multi-return.exit create mode 100644 examples/types/expected/0203-types-multi-return.stderr create mode 100644 examples/types/expected/0203-types-multi-return.stdout create mode 100644 examples/types/expected/0204-types-multi-return-failable.exit create mode 100644 examples/types/expected/0204-types-multi-return-failable.stderr create mode 100644 examples/types/expected/0204-types-multi-return-failable.stdout create mode 100644 examples/types/expected/0205-types-named-return-locals.exit create mode 100644 examples/types/expected/0205-types-named-return-locals.stderr create mode 100644 examples/types/expected/0205-types-named-return-locals.stdout create mode 100644 examples/types/expected/0206-types-named-return-must-set.exit create mode 100644 examples/types/expected/0206-types-named-return-must-set.stderr create mode 100644 examples/types/expected/0206-types-named-return-must-set.stdout create mode 100644 examples/types/expected/0207-types-named-return-defaults.exit create mode 100644 examples/types/expected/0207-types-named-return-defaults.stderr create mode 100644 examples/types/expected/0207-types-named-return-defaults.stdout create mode 100644 examples/types/expected/0208-types-named-return-conditional-unset.exit create mode 100644 examples/types/expected/0208-types-named-return-conditional-unset.stderr create mode 100644 examples/types/expected/0208-types-named-return-conditional-unset.stdout create mode 100644 examples/types/expected/0209-types-multi-return-arity.exit create mode 100644 examples/types/expected/0209-types-multi-return-arity.stderr create mode 100644 examples/types/expected/0209-types-multi-return-arity.stdout create mode 100644 examples/types/expected/0210-types-multi-return-name-order.exit create mode 100644 examples/types/expected/0210-types-multi-return-name-order.stderr create mode 100644 examples/types/expected/0210-types-multi-return-name-order.stdout create mode 100644 examples/types/expected/0211-types-named-return-param-collision.exit create mode 100644 examples/types/expected/0211-types-named-return-param-collision.stderr create mode 100644 examples/types/expected/0211-types-named-return-param-collision.stdout create mode 100644 examples/types/expected/0212-types-single-return-comma.exit create mode 100644 examples/types/expected/0212-types-single-return-comma.stderr create mode 100644 examples/types/expected/0212-types-single-return-comma.stdout create mode 100644 examples/types/expected/0213-types-multi-return-as-value-type.exit create mode 100644 examples/types/expected/0213-types-multi-return-as-value-type.stderr create mode 100644 examples/types/expected/0213-types-multi-return-as-value-type.stdout create mode 100644 issues/0197-annotated-assignment-type-mismatch-no-check.md diff --git a/current/CHECKPOINT-MULTIRET.md b/current/CHECKPOINT-MULTIRET.md new file mode 100644 index 00000000..5212bb8f --- /dev/null +++ b/current/CHECKPOINT-MULTIRET.md @@ -0,0 +1,100 @@ +# CHECKPOINT-MULTIRET — bare-paren multi-value + named returns + +Plan: `current/PLAN-MULTIRET.md`. Branch: `feat/multi-return`. + +## Last completed step +**Phases 0–3 implemented** (final suite + snapshot capture in progress). Examples +renumbered to the free `types` block 0202–0206 (0130/0131 already had duplicate +existing owners). +- **Phase 0** — empty `()` in the type path → `void`. (0202) +- **Phase 1** — multi-return SIGNATURES `(A, B)` / `(x: A, y: B)` / `(A, B, !)` + (≥2 value slots) parse to a `tuple_type_expr` tagged `is_multi_return`; a + single-value `(T, !)` is a plain failable (= `-> T !`). Return resolver yields + the reused tuple TypeId; `resolveParamType` rejects a multi-return tuple + (return-position-only). Consumed by destructuring. (0203, 0204) +- **Phase 2** — bare comma `return v1, v2` (positional) / `return x = v, y = w` + (named): the return parser builds the same `tuple_literal` the `.(…)` form + produces. Single positional `return v` unchanged. (used throughout 0203–0205) +- **Phase 3** — NAMED-return slots are in-scope assignable LOCALS: bound as + zero-init allocas (`bindNamedReturnSlots`), the implicit return is synthesized + from them (`synthesizeNamedReturn` → reuses `lowerReturn`), and the MUST-SET + rule errors on an unset/undefaulted slot (`bodyAssignsTo`, path-insensitive + MVP). Works with the failable error channel too. (0205 positive, 0206 negative) + +Earlier foundation: parser `collectGenericNames` descends tuple/optional/function +nodes; generic.zig `extractTypeParam` handle the `(value, !)` tuple. + +## Current state (works, verified by probes) +- `() -> ()` ≡ void; `-> (A, B)` / `-> (x: A, y: B)` / `-> (A, B, !)` multi-return. +- `return a, b` / `return x = a, y = b` bare comma; named-return locals + implicit + return + must-set diagnostic; failable named multi-return. +- `(A, B)` in a PARAM slot → loud diagnostic. +- Representation: `TupleTypeExpr.is_multi_return` flag + `Lowering.named_return_names` + state (reuses tuple ABI; no new TypeInfo variant; multi-return-ness derivable + from the FnDecl AST). + +## Post-Phase-3 changes (this session) +- **Representation refactored to a dedicated `ReturnTypeExpr` AST node** (user + preferred it over the `TupleTypeExpr.is_multi_return` flag). Resolves to a + reused `.tuple` TypeId via the shared `internTupleLike` helper. Forced + `.return_type_expr` arms onto the exhaustive `node.data` switches (sema, + semantic_diagnostics) — the coverage benefit. Param-position reject + the + named-return-locals / must-set sites now key off `.return_type_expr`. +- **Destructure-only enforcement REVERSED** (user): single-binding a multi-return + is ALLOWED. `c := f(); c.sum` works (the result is a tuple of the value slots). + For a failable multi-return, `c := f() catch …` binds only the value slots — + the error stays on the `!` channel (verified). The `callIsMultiReturn` reject + was removed. Examples 0203/0204 updated to show single-bind + field access + (output byte-identical, snapshots unchanged). + +## Phase 4 — named-return DEFAULTS (done, suite pending) +`-> (sum: i32 = 0, good: bool)`: the parser parses `= ` per slot into +`ReturnTypeExpr.field_defaults`; `bindNamedReturnSlots` seeds a defaulted slot +with its (lowered+coerced) default; a defaulted slot is EXEMPT from the must-set +rule. Also fixed `hasFnBodyAfterArrow` (the fn-def-vs-type-const lookahead) to +skip `=` + literal tokens in the return-type scan — otherwise a `=` made the decl +misread as a bodyless type-const ("expected ';'" at the body `{`). Lock: 0207. + +## Adversarial-review fixes (this session) +An adversarial review found 8 issues; fixed the soundness + silent-wrong ones: +- **#1 (segfault on a conditionally-assigned non-scalar slot)** → must-set is now + PATH-SENSITIVE definite-assignment (`definitelyAssigns`, stmt.zig): a slot not + assigned on every non-diverging path (and undefaulted) is a COMPILE ERROR, not + a runtime garbage read. `return`/`raise` count as divergence; `if` needs both + branches; `push` bodies count; `match` needs an else arm + all arms. +- **#2 (wrong-type default → segfault)** → `bindNamedReturnSlots` type-checks a + default via the coercion classifier (`.none` ⇒ diagnostic). (NOTE: the same + silent bitcast/segfault exists for ANY annotated assignment `x: i32 = "hi"` — a + broader PRE-EXISTING type-checking gap, not multi-return-specific.) +- **#3 / #8 (return arity garbage)** + **#4 (named elements ignored)** → + `validateMultiReturn` (stmt.zig, called from `lowerReturn`): rejects a bare + value where ≥2 are required, wrong arity, a comma list from a single-value fn, + and named elements out of slot order. (Reordering-by-name is a future nicety; + for now a mismatch is a loud error, never silent-wrong.) +- **#5 (slot shadows param)** → collision diagnostic in `bindNamedReturnSlots`. +- **#7 (push/defer false must-set error)** → subsumed by the DA rewrite (push + bodies count; defer correctly does NOT, as it runs after the implicit return). + +## Known limitations / next +- **#6 (design gap, NOT UB)**: a `ReturnTypeExpr` is still silently accepted in + struct-field / var-annotation / generic-arg / closure-RETURN positions (resolves + to a coherent tuple). Only the PARAM position is rejected. Rejecting the rest + needs checks at several value-resolution sites; deferred (no soundness impact). +- **Reordering named return elements by name** (vs requiring slot order) — future. +- **PRE-EXISTING**: annotated-assignment type mismatch (`x: i32 = "hi"`) segfaults + — a general type-checking gap surfaced by the review; may warrant an issue. +- Multi-return CLOSURE-TYPE values / lambda literals deferred (D3). +- Docs: readme.md / specs.md not yet updated for multi-return (docs-track rule). + +## Known issues +- (none yet) + +## Log +- Pivoted here from the Io-unification Phase 3 (true cancellation), which is + PAUSED at its blocker: capturing a failable closure into a nested closure loses + its failability (`worker() catch` → operand type 'unresolved'; repro + `.sx-tmp/pD.sx`/`pE.sx`). That capture-typing gap is unrelated to multi-return + and waits for a later session. The Io-Phase-3 stdlib edits (core/sched/io + + example 1825) were REVERTED to keep the tree green; the multi-return-relevant + compiler changes were kept. +- Foundation landed + suite green; plan + checkpoint written. diff --git a/current/PLAN-MULTIRET.md b/current/PLAN-MULTIRET.md new file mode 100644 index 00000000..f77822fc --- /dev/null +++ b/current/PLAN-MULTIRET.md @@ -0,0 +1,156 @@ +# PLAN-MULTIRET — bare-paren multi-value returns + named returns + +## Why +sx already has multi-value returns, but only in a verbose spelling: +`-> Tuple(A, B)` / `-> Tuple(x: A, y: B)` types and `return .(a, b)` / +`return .(x = a, y = b)` tuple-literal returns. Destructuring (`a, b := f()`), +named/positional field access (`r.x` / `r.0`), and value-carrying failables +(`Tuple(A, B) !E`) all work on top of the existing `.tuple` TypeId. + +The user wants the ergonomic, canonical surface: + +```sx +a :: () -> () { } // () ≡ void +two :: () -> (i32, bool) { return 42, true; } // bare-paren type + bare comma return +b :: (f1: i32, f2: i32) -> (sum: i32, good: bool) { // named returns are in-scope locals + good = true; + sum = f1 + f2; // implicit return: all named slots set +} +b2 :: (f1: i32, f2: i32) -> (sum: i32, good: bool) { + return f1 + f2, f2 > 42; // bare comma return still works +} +read :: () -> (i32, bool, !) { ... } // error channel ALWAYS the last slot +``` + +Rules (from the user): +- **`() -> ()` ≡ `() -> void`.** +- **A multi-return signature is NOT a tuple — it just REUSES the tuple machinery.** + `-> (i32, bool)` / `-> (x: i32, y: bool)` mean "this function returns multiple + values", a DISTINCT thing from `-> Tuple(i32, bool)` (which returns one tuple + value). The bare-paren form is valid ONLY as a function/closure RETURN + signature — `x: (A, B)` (a variable/param/field annotation) stays REJECTED; + `Tuple(…)` is the spelling for an actual tuple value type. +- **Consumption — destructure OR single-bind (REVISED 2026-06-27).** A + multi-return result may be DESTRUCTURED (`s, g := b2()`) OR bound to a single + name and reached by field (`c := b2(); c.sum` / `c.0`). The earlier + destructure-only rule (single-bind = error) was REVERSED by the user — single + binding is allowed; the bound value behaves like a tuple of the value slots. + - **Failable: the error stays SEPARATE.** For `-> (sum, good, !)`, a bound + value (`c := f() catch …` / `try`) holds ONLY the value slots — the error + rides the `!` channel and is NEVER part of `c` (no `c.err`). This falls out of + the existing failable machinery (catch/try strip the error before binding). +- **Failable: the error channel is always the LAST slot** (`(A, B, !)`). +- **Bare comma return**: `return v1, v2;` maps positionally to the return slots — + no `.(…)` tuple literal needed. +- **Named returns are assignable locals.** With no explicit `return`, an implicit + return at end-of-body synthesizes the result from the named locals. **A named + return that is neither assigned on the path nor given a default is a COMPILE + ERROR.** A named slot may carry a default (`(sum: i32 = 0, good: bool)`); a + defaulted slot needn't be assigned. + +## Representation (how "not a tuple, reuse machinery" is realized) — AS BUILT +- A dedicated AST node **`ReturnTypeExpr`** (`field_types` + optional + `field_names`, same shape as a tuple) is produced by the parser for a bare-paren + result list with **≥2 value slots** (`(A, B)`, `(x: A, y: B)`, `(A, B, !)`). A + single-value `(T, !)` stays a `tuple_type_expr` (a plain failable, `= -> T !`). + An EMPTY `()` parses to the `void` type. +- It resolves (type_resolver `internTupleLike`, shared with `tuple_type_expr`) to + a reused `.tuple` TypeId — full ABI / failable / destructure / field-access + machinery reuse. Its distinct MEANING lives in the AST node, not the TypeId. +- Position gating: the node is valid only in a return slot. `resolveParamType` + rejects a `ReturnTypeExpr` parameter annotation ("multi-return is return-only; + use Tuple(…)"). Being a distinct node, its mere appearance in a value-type + position is categorically an error (no flag to check) — exhaustive `switch`es + over `node.data` were forced to add a `.return_type_expr` arm (coverage). +- Consumption: destructure (`a, b := f()`) or single-bind + field access + (`c := f(); c.sum`). No single source of truth needed at call sites — the + result is just a tuple value. +- SCOPE: multi-return on `name :: (...) -> (…) { }` function declarations first. + Multi-return CLOSURE-TYPE values (`cb: Closure() -> (A, B)`) and lambda + literals are a later phase. + +## What already exists (re-use, do NOT rebuild) +- `tuple_type_expr` → `.tuple` TypeId with optional `names` (type_resolver.zig + `resolveCompound`). +- Named + positional tuple field access `r.x` / `r.0` (expr.zig + `lowerFieldAccessOnType`). +- Destructuring `a, b := f()` (`DestructureDecl`, stmt.zig). +- Value-carrying failable assembly `(T1, …, !)` (error.zig + `lowerFailableSuccessReturn` / `emitTupleRet`) — error in the last slot. +- `return .(a, b)` / `return .(x = a, y = b)` tuple-literal returns (stmt.zig + `lowerReturn`). +- Generic inference through a failable/tuple closure return (this session's + parser `collectGenericNames` + generic.zig `extractTypeParam` tuple arms). + +## Foundation already landed (uncommitted, suite-green) +- **parser.zig** — `collectGenericNames` descends tuple/optional/function nodes + (so `Closure() -> $R !` binds `$R`); the bare-paren result-list path builds a + failable `tuple_type_expr` when it ends in `!` (`(A, B, !)` parses). +- **generic.zig** — `extractTypeParam` / `matchTypeParam[Static]` handle the + `(value, !)` tuple so `$R` infers from a closure ARG's failable return. + +## Phases (each: implement → lock with an example → `zig build test` green) + +0. **`() -> ()` = void (parser).** Isolated, unambiguous. An empty `()` in the + paren type path resolves to `void`. Lock: `a :: () -> () { }`. + +1. **Multi-return signatures `-> (A, B)` / `-> (x: A, y: B)` / `-> (A, B, !)` + (parser + AST + resolution).** Add the `multi_return_type` AST node; the parser + produces it for a bare-paren result list (return position). The return resolver + lowers it to a `.tuple` TypeId and sets `Function.multi_return`; the general + resolver rejects it (return-position only). Returns still use the existing + `return .(…)` literal in this phase (bare comma is Phase 2). Consumption is + destructuring `a, b := f()` (existing machinery). Lock: positional + named + + failable multi-return examples, each destructured. + +2. **Destructure-only enforcement + bare comma `return v1, v2` (parser + lowering).** + (a) Reject using a multi-return call as a single value (`r := f()`, an arg, an + operand) — read `Function.multi_return` at the binding/use site; only + destructuring is allowed. (b) Extend the return statement to parse a + comma-separated value list and lower it to the same multi-slot return the + `.(…)` literal produces (error slot stays implicit for failables). Single-value + `return v` unchanged. Lock: `-> (i64, bool) { return 7, true; }`, a failable + variant, and a negative example (`r := f()` → diagnostic). + +3. **Named-return locals + must-set rule (sema/lowering).** For a named return + `-> (x: A, y: B)`, bind each name as an in-scope assignable local (alloca). On + a path that reaches end-of-body with NO explicit `return`, synthesize the + implicit return from the named locals. Diagnose loudly if any named slot is + neither assigned on that path nor defaulted (no silent zero-fill). Explicit + `return v1, v2` / `return .(…)` still override. Lock: the + `b :: (...) -> (sum, good) { good = true; sum = ... }` example + a negative + example (unset slot → diagnostic). + +4. **Named-return defaults `(sum: i32 = 0, good: bool)`.** A slot with a default + is exempt from the must-set rule; the default fills it at the implicit (or + partial explicit) return. Lock: an example mixing a defaulted + a required + slot. + +## Open decisions (Decisions Log) +- **D1 — multi-return is NOT a tuple; return-position-only.** *Chosen* (user + directive). Realized via a distinct **`ReturnTypeExpr` AST node** (the user + preferred a dedicated node over a `TupleTypeExpr.is_multi_return` flag — it + makes "not a tuple" true at the AST level and makes position-gating + categorical) that resolves to a reused `.tuple` TypeId. A new `.tuple`-like + TypeInfo variant was rejected — it would ripple through every exhaustive type + switch for no ABI benefit. **Destructure-only was REVERSED** (see Rules): + single-binding a multi-return result is allowed (field access on the value + slots); the failable error stays on the separate `!` channel. +- **D2 (Phase 3) — storage for named-return locals.** Lean: an alloca per named + slot bound in the function scope under its name; the implicit return reads them + into the result tuple. Revisit if the must-set analysis wants SSA-style + definite-assignment instead of an alloca + per-path check. +- **D3 — multi-return closure-type values / lambda literals.** Deferred past the + function-decl phases (needs a `ClosureInfo.multi_return` flag). Phases 0–4 cover + named function declarations only. + +## Validation (every phase) +- `zig build && zig build test` green (full corpus). +- New `examples//…` locked with snapshots; review the diff for `.ir` + churn only where expected (the prelude type table is untouched by this stream, + so churn should be minimal/none). +- Adversarial review of each phase before it lands. + +## Category for examples +Multi-return is a core type/return feature — use the `types` block (`01xx`), +next free numbers, unless a better fit emerges. diff --git a/examples/types/0202-types-void-empty-parens.sx b/examples/types/0202-types-void-empty-parens.sx new file mode 100644 index 00000000..3d8890f4 --- /dev/null +++ b/examples/types/0202-types-void-empty-parens.sx @@ -0,0 +1,21 @@ +// Empty parens `()` as a return type mean `void`: `f :: () -> () { … }` is +// exactly `f :: () -> void { … }`. The zero-parameter FUNCTION type `() -> R` +// (a callable taking no args) is unaffected — only an empty `()` in the +// return-TYPE slot folds to void. +#import "modules/std.sx"; + +// `-> ()` is void: no value returned. +greet :: () -> () { print("hi\n"); } + +// A `-> void` spelling, for contrast — identical behavior. +greet2 :: () -> void { print("bye\n"); } + +// Zero-param function-typed parameter still parses as a callable, not void. +run :: (f: () -> i64) -> i64 { return f(); } + +main :: () -> i64 { + greet(); + greet2(); + print("{}\n", run(() => 7)); + return 0; +} diff --git a/examples/types/0203-types-multi-return.sx b/examples/types/0203-types-multi-return.sx new file mode 100644 index 00000000..c5e2538f --- /dev/null +++ b/examples/types/0203-types-multi-return.sx @@ -0,0 +1,31 @@ +// Multi-return signatures: a function may return MULTIPLE values, written as a +// bare-paren list in the return slot — `-> (i64, bool)` (positional) or +// `-> (sum: i32, bigger: bool)` (named). The returned values use the bare comma +// `return a, b` form (no `.(…)` literal). +// +// The result is consumed either by DESTRUCTURING (`q, r := f()`) or by binding +// it to a single name and reaching the value slots by field (`c := f(); c.sum`). +// A multi-return is return-position-only as a TYPE: a parameter / variable +// annotation `x: (A, B)` is rejected — use `Tuple(…)` for a tuple value. +#import "modules/std.sx"; + +// Positional multi-return. +divmod :: (a: i64, b: i64) -> (i64, i64) { + return a / b, a % b; +} + +// Named multi-return — the slot names ride the signature. +stats :: (a: i32, b: i32) -> (sum: i32, bigger: bool) { + return sum = a + b, bigger = a > b; +} + +main :: () -> i64 { + // Destructure the values. + q, r := divmod(17, 5); + print("17 / 5 = {} rem {}\n", q, r); + + // …or bind once and reach the named slots by field. + c := stats(40, 2); + print("sum={} bigger={}\n", c.sum, c.bigger); + return 0; +} diff --git a/examples/types/0204-types-multi-return-failable.sx b/examples/types/0204-types-multi-return-failable.sx new file mode 100644 index 00000000..95fe481a --- /dev/null +++ b/examples/types/0204-types-multi-return-failable.sx @@ -0,0 +1,34 @@ +// A multi-return signature may carry an error channel as its LAST slot: +// `-> (i32, bool, !)` returns two values OR fails. This reuses the existing +// value-carrying-failable machinery — the error is always the final slot. A +// single-value failable `-> (T, !)` (one value + error) is exactly `-> T !`, +// NOT a multi-return. +// +// Consume it like any failable: `catch` / `or` / a guarded destructure or +// single bind. The error rides the SEPARATE `!` channel — a bound result holds +// only the VALUE slots, never the error (so `c.doubled` / `c.big`, no `c.err`). +#import "modules/std.sx"; + +CheckErr :: error { Negative } + +// Two values plus an error channel. The success values use the bare comma +// `return` form; `raise` rides the error channel as usual. +classify :: (n: i32) -> (doubled: i32, big: bool, !) { + if n < 0 { raise error.Negative; } + return doubled = n * 2, big = n > 10; +} + +main :: () -> i64 { + // Success: destructure the two value slots (the catch diverges on error). + d, b := classify(7) catch { print("unexpected error\n"); return 1; }; + print("classify(7): doubled={} big={}\n", d, b); + + // Or bind once (error stripped by `catch`) and reach the value slots by + // field — the error is NOT part of the bound value. + c := classify(21) catch { print("unexpected error\n"); return 1; }; + print("classify(21): doubled={} big={}\n", c.doubled, c.big); + + // Failure: the error path is taken. + classify(-3) catch { print("classify(-3): caught Negative\n"); return 0; }; + return 0; +} diff --git a/examples/types/0205-types-named-return-locals.sx b/examples/types/0205-types-named-return-locals.sx new file mode 100644 index 00000000..4389c098 --- /dev/null +++ b/examples/types/0205-types-named-return-locals.sx @@ -0,0 +1,42 @@ +// Named multi-return slots are in-scope assignable LOCALS. Assign each by name +// in the body; with no explicit `return`, the function implicitly returns the +// slot values once they are all set. A named slot that is never assigned (and +// has no default) is a compile error — see the companion negative example. +// +// This is sugar over the multi-return machinery: `b` below is equivalent to +// `return sum = f1 + f2, good = f1 > f2`, but reads as straight-line setup. +#import "modules/std.sx"; + +// The slot names `sum` / `good` are locals; assigning them IS the return. +combine :: (f1: i32, f2: i32) -> (sum: i32, good: bool) { + good = f1 > f2; + sum = f1 + f2; +} + +// A slot local can be read after it is set (here `hi` depends on `lo`). +bounds :: (n: i32) -> (lo: i32, hi: i32) { + lo = n; + hi = lo + 10; +} + +// Named slots work with an error channel too (the error is the last slot). +Err :: error { Negative } +roots :: (n: i32) -> (val: i32, sq: i32, !) { + if n < 0 { raise error.Negative; } + val = n; + sq = n * n; +} + +main :: () -> i64 { + s, g := combine(40, 2); + print("combine: sum={} good={}\n", s, g); + + lo, hi := bounds(5); + print("bounds: lo={} hi={}\n", lo, hi); + + v, sq := roots(7) catch { print("unexpected\n"); return 1; }; + print("roots: val={} sq={}\n", v, sq); + + roots(-1) catch { print("roots(-1): caught Negative\n"); return 0; }; + return 0; +} diff --git a/examples/types/0206-types-named-return-must-set.sx b/examples/types/0206-types-named-return-must-set.sx new file mode 100644 index 00000000..4ce976a5 --- /dev/null +++ b/examples/types/0206-types-named-return-must-set.sx @@ -0,0 +1,16 @@ +// Negative: a NAMED multi-return slot that the body never assigns (and that has +// no default) is a compile error. Here `good` is never set, so the implicit +// return cannot be synthesized — the must-set rule rejects it. (Assign every +// slot, give it a default, or end with an explicit `return`.) +#import "modules/std.sx"; + +combine :: (f1: i32, f2: i32) -> (sum: i32, good: bool) { + sum = f1 + f2; + // `good` is never assigned — must-set violation. +} + +main :: () -> i64 { + s, g := combine(1, 2); + print("{} {}\n", s, g); + return 0; +} diff --git a/examples/types/0207-types-named-return-defaults.sx b/examples/types/0207-types-named-return-defaults.sx new file mode 100644 index 00000000..d3f60872 --- /dev/null +++ b/examples/types/0207-types-named-return-defaults.sx @@ -0,0 +1,26 @@ +// A named multi-return slot may carry a DEFAULT: `-> (sum: i32 = 0, good: bool)`. +// A defaulted slot is EXEMPT from the must-set rule — if the body never assigns +// it, the default seeds it at the implicit return. A non-defaulted slot must +// still be set (see 0206). An explicit assignment overrides the default. +#import "modules/std.sx"; + +// `sum` defaults to -1; only `good` must be assigned. +classify :: (n: i32) -> (sum: i32 = -1, good: bool) { + good = n > 0; + // `sum` is left unset → the default (-1) is returned. +} + +// Both slots default; the body overrides them. +combine :: (a: i32, b: i32) -> (total: i32 = 0, ok: bool = false) { + total = a + b; + ok = true; +} + +main :: () -> i64 { + s, g := classify(5); + print("classify(5): sum={} good={}\n", s, g); // default sum -1, good true + + t, o := combine(3, 4); + print("combine(3,4): total={} ok={}\n", t, o); // overridden: 7, true + return 0; +} diff --git a/examples/types/0208-types-named-return-conditional-unset.sx b/examples/types/0208-types-named-return-conditional-unset.sx new file mode 100644 index 00000000..398ebc0f --- /dev/null +++ b/examples/types/0208-types-named-return-conditional-unset.sx @@ -0,0 +1,18 @@ +// Negative: the named-return must-set rule is PATH-SENSITIVE (definite +// assignment). A slot assigned on only SOME paths is NOT definitely set, so the +// implicit return is rejected at COMPILE time — rather than returning a stale / +// uninitialized value at run time. Here `s` is assigned only inside `if c`, so +// the `c == false` path would leave it unset. (Assign it on every path — e.g. +// add an `else`, give it a default, or use an explicit `return`.) +#import "modules/std.sx"; + +pick :: (c: bool) -> (s: i64, y: i64) { + if c { s = 10; } // `s` unset on the else path + y = 1; +} + +main :: () -> i64 { + a, b := pick(false); + print("{} {}\n", a, b); + return 0; +} diff --git a/examples/types/0209-types-multi-return-arity.sx b/examples/types/0209-types-multi-return-arity.sx new file mode 100644 index 00000000..4e1528a2 --- /dev/null +++ b/examples/types/0209-types-multi-return-arity.sx @@ -0,0 +1,8 @@ +// Negative: a multi-return's `return` must yield the right number of values. +// Returning a single value where two are required would leave the second slot +// uninitialized, so it is a compile error. +#import "modules/std.sx"; +divmod :: (a: i64, b: i64) -> (i64, i64) { + return a / b; // only one value — needs two +} +main :: () -> i64 { q, r := divmod(7, 2); print("{} {}\n", q, r); return 0; } diff --git a/examples/types/0210-types-multi-return-name-order.sx b/examples/types/0210-types-multi-return-name-order.sx new file mode 100644 index 00000000..648b0ffc --- /dev/null +++ b/examples/types/0210-types-multi-return-name-order.sx @@ -0,0 +1,8 @@ +// Negative: named return elements must be given in SLOT ORDER. A mismatched +// name would otherwise be matched positionally and silently produce the wrong +// result, so it is rejected. (Here `b` is given where slot `a` is expected.) +#import "modules/std.sx"; +pair :: (n: i32) -> (a: i32, b: i32) { + return b = n, a = n + 1; // out of slot order +} +main :: () -> i64 { x, y := pair(5); print("{} {}\n", x, y); return 0; } diff --git a/examples/types/0211-types-named-return-param-collision.sx b/examples/types/0211-types-named-return-param-collision.sx new file mode 100644 index 00000000..9b543b5c --- /dev/null +++ b/examples/types/0211-types-named-return-param-collision.sx @@ -0,0 +1,8 @@ +// Negative: a named-return slot may not share a name with a PARAMETER — the slot +// local would silently shadow the parameter. Rename one. +#import "modules/std.sx"; +inc :: (sum: i32) -> (sum: i32, ok: bool) { + ok = true; + sum = sum + 1; +} +main :: () -> i64 { s, o := inc(10); print("{} {}\n", s, o); return 0; } diff --git a/examples/types/0212-types-single-return-comma.sx b/examples/types/0212-types-single-return-comma.sx new file mode 100644 index 00000000..6643ebbb --- /dev/null +++ b/examples/types/0212-types-single-return-comma.sx @@ -0,0 +1,7 @@ +// Negative: a SINGLE-value function may not be given a comma list — the extra +// values would be silently dropped. (Did you mean to declare `-> (i64, i64)`?) +#import "modules/std.sx"; +one :: () -> i64 { + return 1, 2; // single-value return, two given +} +main :: () -> i64 { print("{}\n", one()); return 0; } diff --git a/examples/types/0213-types-multi-return-as-value-type.sx b/examples/types/0213-types-multi-return-as-value-type.sx new file mode 100644 index 00000000..c7032147 --- /dev/null +++ b/examples/types/0213-types-multi-return-as-value-type.sx @@ -0,0 +1,6 @@ +// Negative: a bare-paren `(A, B)` is a multi-return SIGNATURE — valid only as a +// return type, never as a value type. A tuple-valued field/variable/parameter +// uses `Tuple(…)`. +#import "modules/std.sx"; +Point :: struct { coords: (i64, i64); } // field value type — rejected +main :: () -> i64 { return 0; } diff --git a/examples/types/expected/0202-types-void-empty-parens.exit b/examples/types/expected/0202-types-void-empty-parens.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/types/expected/0202-types-void-empty-parens.exit @@ -0,0 +1 @@ +0 diff --git a/examples/types/expected/0202-types-void-empty-parens.stderr b/examples/types/expected/0202-types-void-empty-parens.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/types/expected/0202-types-void-empty-parens.stderr @@ -0,0 +1 @@ + diff --git a/examples/types/expected/0202-types-void-empty-parens.stdout b/examples/types/expected/0202-types-void-empty-parens.stdout new file mode 100644 index 00000000..6a94b553 --- /dev/null +++ b/examples/types/expected/0202-types-void-empty-parens.stdout @@ -0,0 +1,3 @@ +hi +bye +7 diff --git a/examples/types/expected/0203-types-multi-return.exit b/examples/types/expected/0203-types-multi-return.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/types/expected/0203-types-multi-return.exit @@ -0,0 +1 @@ +0 diff --git a/examples/types/expected/0203-types-multi-return.stderr b/examples/types/expected/0203-types-multi-return.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/types/expected/0203-types-multi-return.stderr @@ -0,0 +1 @@ + diff --git a/examples/types/expected/0203-types-multi-return.stdout b/examples/types/expected/0203-types-multi-return.stdout new file mode 100644 index 00000000..0734e08a --- /dev/null +++ b/examples/types/expected/0203-types-multi-return.stdout @@ -0,0 +1,2 @@ +17 / 5 = 3 rem 2 +sum=42 bigger=true diff --git a/examples/types/expected/0204-types-multi-return-failable.exit b/examples/types/expected/0204-types-multi-return-failable.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/types/expected/0204-types-multi-return-failable.exit @@ -0,0 +1 @@ +0 diff --git a/examples/types/expected/0204-types-multi-return-failable.stderr b/examples/types/expected/0204-types-multi-return-failable.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/types/expected/0204-types-multi-return-failable.stderr @@ -0,0 +1 @@ + diff --git a/examples/types/expected/0204-types-multi-return-failable.stdout b/examples/types/expected/0204-types-multi-return-failable.stdout new file mode 100644 index 00000000..bf2174c8 --- /dev/null +++ b/examples/types/expected/0204-types-multi-return-failable.stdout @@ -0,0 +1,3 @@ +classify(7): doubled=14 big=false +classify(21): doubled=42 big=true +classify(-3): caught Negative diff --git a/examples/types/expected/0205-types-named-return-locals.exit b/examples/types/expected/0205-types-named-return-locals.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/types/expected/0205-types-named-return-locals.exit @@ -0,0 +1 @@ +0 diff --git a/examples/types/expected/0205-types-named-return-locals.stderr b/examples/types/expected/0205-types-named-return-locals.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/types/expected/0205-types-named-return-locals.stderr @@ -0,0 +1 @@ + diff --git a/examples/types/expected/0205-types-named-return-locals.stdout b/examples/types/expected/0205-types-named-return-locals.stdout new file mode 100644 index 00000000..02a38f86 --- /dev/null +++ b/examples/types/expected/0205-types-named-return-locals.stdout @@ -0,0 +1,4 @@ +combine: sum=42 good=true +bounds: lo=5 hi=15 +roots: val=7 sq=49 +roots(-1): caught Negative diff --git a/examples/types/expected/0206-types-named-return-must-set.exit b/examples/types/expected/0206-types-named-return-must-set.exit new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/examples/types/expected/0206-types-named-return-must-set.exit @@ -0,0 +1 @@ +1 diff --git a/examples/types/expected/0206-types-named-return-must-set.stderr b/examples/types/expected/0206-types-named-return-must-set.stderr new file mode 100644 index 00000000..9c40729f --- /dev/null +++ b/examples/types/expected/0206-types-named-return-must-set.stderr @@ -0,0 +1,11 @@ +error: named return 'good' may be unset (not assigned on every path) and has no default — assign it on every path, give it a default, or end with an explicit `return` + --> examples/types/0206-types-named-return-must-set.sx:7:57 + | + 7 | combine :: (f1: i32, f2: i32) -> (sum: i32, good: bool) { + | ^ + 8 | sum = f1 + f2; + | ^^^^^^^^^^^^^^^^^^ + 9 | // `good` is never assigned — must-set violation. + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +10 | } + | ^ diff --git a/examples/types/expected/0206-types-named-return-must-set.stdout b/examples/types/expected/0206-types-named-return-must-set.stdout new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/types/expected/0206-types-named-return-must-set.stdout @@ -0,0 +1 @@ + diff --git a/examples/types/expected/0207-types-named-return-defaults.exit b/examples/types/expected/0207-types-named-return-defaults.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/types/expected/0207-types-named-return-defaults.exit @@ -0,0 +1 @@ +0 diff --git a/examples/types/expected/0207-types-named-return-defaults.stderr b/examples/types/expected/0207-types-named-return-defaults.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/types/expected/0207-types-named-return-defaults.stderr @@ -0,0 +1 @@ + diff --git a/examples/types/expected/0207-types-named-return-defaults.stdout b/examples/types/expected/0207-types-named-return-defaults.stdout new file mode 100644 index 00000000..0c2316d2 --- /dev/null +++ b/examples/types/expected/0207-types-named-return-defaults.stdout @@ -0,0 +1,2 @@ +classify(5): sum=-1 good=true +combine(3,4): total=7 ok=true diff --git a/examples/types/expected/0208-types-named-return-conditional-unset.exit b/examples/types/expected/0208-types-named-return-conditional-unset.exit new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/examples/types/expected/0208-types-named-return-conditional-unset.exit @@ -0,0 +1 @@ +1 diff --git a/examples/types/expected/0208-types-named-return-conditional-unset.stderr b/examples/types/expected/0208-types-named-return-conditional-unset.stderr new file mode 100644 index 00000000..07f66b6e --- /dev/null +++ b/examples/types/expected/0208-types-named-return-conditional-unset.stderr @@ -0,0 +1,11 @@ +error: named return 's' may be unset (not assigned on every path) and has no default — assign it on every path, give it a default, or end with an explicit `return` + --> examples/types/0208-types-named-return-conditional-unset.sx:9:39 + | + 9 | pick :: (c: bool) -> (s: i64, y: i64) { + | ^ +10 | if c { s = 10; } // `s` unset on the else path + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +11 | y = 1; + | ^^^^^^^^^^ +12 | } + | ^ diff --git a/examples/types/expected/0208-types-named-return-conditional-unset.stdout b/examples/types/expected/0208-types-named-return-conditional-unset.stdout new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/types/expected/0208-types-named-return-conditional-unset.stdout @@ -0,0 +1 @@ + diff --git a/examples/types/expected/0209-types-multi-return-arity.exit b/examples/types/expected/0209-types-multi-return-arity.exit new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/examples/types/expected/0209-types-multi-return-arity.exit @@ -0,0 +1 @@ +1 diff --git a/examples/types/expected/0209-types-multi-return-arity.stderr b/examples/types/expected/0209-types-multi-return-arity.stderr new file mode 100644 index 00000000..e29df2e0 --- /dev/null +++ b/examples/types/expected/0209-types-multi-return-arity.stderr @@ -0,0 +1,5 @@ +error: this function returns 2 values — return them as `return a, b`, not a single value + --> examples/types/0209-types-multi-return-arity.sx:6:12 + | + 6 | return a / b; // only one value — needs two + | ^^^^^ diff --git a/examples/types/expected/0209-types-multi-return-arity.stdout b/examples/types/expected/0209-types-multi-return-arity.stdout new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/types/expected/0209-types-multi-return-arity.stdout @@ -0,0 +1 @@ + diff --git a/examples/types/expected/0210-types-multi-return-name-order.exit b/examples/types/expected/0210-types-multi-return-name-order.exit new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/examples/types/expected/0210-types-multi-return-name-order.exit @@ -0,0 +1 @@ +1 diff --git a/examples/types/expected/0210-types-multi-return-name-order.stderr b/examples/types/expected/0210-types-multi-return-name-order.stderr new file mode 100644 index 00000000..28ba9199 --- /dev/null +++ b/examples/types/expected/0210-types-multi-return-name-order.stderr @@ -0,0 +1,11 @@ +error: named return element 'b' does not match the slot 'a' at position 0 — name the elements in slot order + --> examples/types/0210-types-multi-return-name-order.sx:6:5 + | + 6 | return b = n, a = n + 1; // out of slot order + | ^^^^^^^^^^^^^^^^^^^^^^^^ + +error: named return element 'a' does not match the slot 'b' at position 1 — name the elements in slot order + --> examples/types/0210-types-multi-return-name-order.sx:6:5 + | + 6 | return b = n, a = n + 1; // out of slot order + | ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/examples/types/expected/0210-types-multi-return-name-order.stdout b/examples/types/expected/0210-types-multi-return-name-order.stdout new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/types/expected/0210-types-multi-return-name-order.stdout @@ -0,0 +1 @@ + diff --git a/examples/types/expected/0211-types-named-return-param-collision.exit b/examples/types/expected/0211-types-named-return-param-collision.exit new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/examples/types/expected/0211-types-named-return-param-collision.exit @@ -0,0 +1 @@ +1 diff --git a/examples/types/expected/0211-types-named-return-param-collision.stderr b/examples/types/expected/0211-types-named-return-param-collision.stderr new file mode 100644 index 00000000..b260ef99 --- /dev/null +++ b/examples/types/expected/0211-types-named-return-param-collision.stderr @@ -0,0 +1,5 @@ +error: named return 'sum' collides with a parameter of the same name — rename one + --> examples/types/0211-types-named-return-param-collision.sx:4:22 + | + 4 | inc :: (sum: i32) -> (sum: i32, ok: bool) { + | ^^^^^^^^^^^^^^^^^^^^ diff --git a/examples/types/expected/0211-types-named-return-param-collision.stdout b/examples/types/expected/0211-types-named-return-param-collision.stdout new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/types/expected/0211-types-named-return-param-collision.stdout @@ -0,0 +1 @@ + diff --git a/examples/types/expected/0212-types-single-return-comma.exit b/examples/types/expected/0212-types-single-return-comma.exit new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/examples/types/expected/0212-types-single-return-comma.exit @@ -0,0 +1 @@ +1 diff --git a/examples/types/expected/0212-types-single-return-comma.stderr b/examples/types/expected/0212-types-single-return-comma.stderr new file mode 100644 index 00000000..a094a574 --- /dev/null +++ b/examples/types/expected/0212-types-single-return-comma.stderr @@ -0,0 +1,5 @@ +error: this function returns a single value, but a list of 2 was given + --> examples/types/0212-types-single-return-comma.sx:5:5 + | + 5 | return 1, 2; // single-value return, two given + | ^^^^^^^^^^^^ diff --git a/examples/types/expected/0212-types-single-return-comma.stdout b/examples/types/expected/0212-types-single-return-comma.stdout new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/types/expected/0212-types-single-return-comma.stdout @@ -0,0 +1 @@ + diff --git a/examples/types/expected/0213-types-multi-return-as-value-type.exit b/examples/types/expected/0213-types-multi-return-as-value-type.exit new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/examples/types/expected/0213-types-multi-return-as-value-type.exit @@ -0,0 +1 @@ +1 diff --git a/examples/types/expected/0213-types-multi-return-as-value-type.stderr b/examples/types/expected/0213-types-multi-return-as-value-type.stderr new file mode 100644 index 00000000..04d2a29e --- /dev/null +++ b/examples/types/expected/0213-types-multi-return-as-value-type.stderr @@ -0,0 +1,5 @@ +error: a bare-paren `(A, B)` is a multi-return signature, valid only as a return type; a tuple-valued field uses `Tuple(…)` + --> examples/types/0213-types-multi-return-as-value-type.sx:5:27 + | + 5 | Point :: struct { coords: (i64, i64); } // field value type — rejected + | ^^^^^^^^^^ diff --git a/examples/types/expected/0213-types-multi-return-as-value-type.stdout b/examples/types/expected/0213-types-multi-return-as-value-type.stdout new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/types/expected/0213-types-multi-return-as-value-type.stdout @@ -0,0 +1 @@ + diff --git a/issues/0197-annotated-assignment-type-mismatch-no-check.md b/issues/0197-annotated-assignment-type-mismatch-no-check.md new file mode 100644 index 00000000..e8236cae --- /dev/null +++ b/issues/0197-annotated-assignment-type-mismatch-no-check.md @@ -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. diff --git a/readme.md b/readme.md index 31fd4fb4..a24df750 100644 --- a/readme.md +++ b/readme.md @@ -132,6 +132,59 @@ v : `i2 = ---; // referenced as a type x : i2 = 3; // bare `i2` in type position is still the int type ``` +### Multiple return values + +A function can return several values with a bare-paren return signature — +positional `-> (A, B)` or named `-> (x: A, y: B)`. The empty `-> ()` is `void`, +and a trailing `!` is the error channel (always the last slot): `-> (A, B, !)`. A +multi-return is **not** a tuple value — it is a distinct return shape (so a +parameter / field / variable annotation `x: (A, B)` is rejected; use `Tuple(…)` +for an actual tuple value). + +```sx +divmod :: (a: i64, b: i64) -> (i64, i64) { + return a / b, a % b; // bare comma return — no `.( … )` literal +} + +stats :: (a: i32, b: i32) -> (sum: i32, big: bool) { + return sum = a + b, big = a > b; // named, in slot order +} +``` + +Consume the result by **destructuring** or by binding it once and reaching the +value slots by **field**: + +```sx +q, r := divmod(17, 5); // q = 3, r = 2 +c := stats(40, 2); // c.sum = 42, c.big = true +``` + +For a **failable** multi-return, the error rides the separate `!` channel — a +bound value holds only the value slots, never the error: + +```sx +classify :: (n: i32) -> (doubled: i32, big: bool, !) { + if n < 0 { raise error.Bad; } + return doubled = n * 2, big = n > 10; +} +d, b := classify(7) catch (e) { … }; // error stripped by `catch`; d, b are the values +``` + +**Named returns as locals.** Named slots are in-scope assignable locals; assigning +them *is* the return (no explicit `return` needed). A slot may carry a default, +which exempts it from the must-set rule: + +```sx +combine :: (a: i32, b: i32) -> (sum: i32 = 0, good: bool) { + good = a > b; + sum = a + b; // both slots set → implicit return +} +``` + +A named slot that is **not assigned on every path** and has no default is a +compile error (definite-assignment) — rather than returning an uninitialized +value. + ### Structs ```sx diff --git a/specs.md b/specs.md index bd0b820f..f769dddc 100644 --- a/specs.md +++ b/specs.md @@ -893,6 +893,39 @@ s := swap(1, 2); // s.0 = 2, s.1 = 1 t := wrap(42); // t.0 = 42 ``` +#### Multiple Return Values (bare-paren return signature) + +A function may return **multiple values** with a bare-paren return signature +(≥2 value slots) — positional `-> (A, B)` or named `-> (x: A, y: B)`, with an +optional trailing `!` error channel as the **last** slot (`-> (A, B, !)`). The +empty `-> ()` is `void`. A multi-return is a DISTINCT construct from a `Tuple(…)` +VALUE: it is represented internally by a reused tuple TypeId (same ABI), but it +is valid ONLY in a function/closure return position — a parameter, field, or +variable annotation `x: (A, B)` is rejected (use `Tuple(…)` for a tuple value). +A single-value `-> (T, !)` (one value + error) is NOT a multi-return; it is +exactly the failable `-> T !`. + +```sx +divmod :: (a: i64, b: i64) -> (i64, i64) { return a / b, a % b; } +stats :: (a: i32, b: i32) -> (sum: i32, big: bool) { return sum = a + b, big = a > b; } +``` + +- **`return` forms.** Bare comma list — `return a, b` (positional) or + `return x = a, y = b` (named, **in slot order**); no `.( … )` literal. A single + positional `return v` is the ordinary single-value return. The list arity must + match the value-slot count, and named elements must agree with the slots; a + mismatch is a compile error (never a silent wrong result). +- **Consumption.** Destructure (`q, r := divmod(…)`) or single-bind + field + access (`c := stats(…); c.sum`). For a failable multi-return the error rides + the separate `!` channel — a bound value (`c := f() catch … `) holds only the + value slots, never the error. +- **Named returns as locals.** Named slots are in-scope assignable locals; + assigning them all *is* the implicit return (no explicit `return` needed). A + slot may carry a default (`(sum: i32 = 0, good: bool)`), which seeds the local + and exempts it from the must-set rule. A slot that is not assigned on every + non-diverging path and has no default is a compile error (definite assignment), + and a slot name may not collide with a parameter name. + #### Representation Tuples are represented as anonymous LLVM struct types (same layout as named structs). A tuple `Tuple(i64, i64)` has LLVM type `{ i64, i64 }`. diff --git a/src/ast.zig b/src/ast.zig index db6c468d..40ce382c 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -86,6 +86,7 @@ pub const Node = struct { function_type_expr: FunctionTypeExpr, closure_type_expr: ClosureTypeExpr, tuple_type_expr: TupleTypeExpr, + return_type_expr: ReturnTypeExpr, tuple_literal: TupleLiteral, ufcs_alias: UfcsAlias, c_import_decl: CImportDecl, @@ -867,6 +868,25 @@ pub const TupleTypeExpr = struct { field_names: ?[]const []const u8, // null for positional }; +/// A bare-paren MULTI-RETURN signature `(A, B)` / `(x: A, y: B)` / `(A, B, !)` +/// (≥2 value slots, error always the LAST slot). A function with this return +/// returns MULTIPLE VALUES — a DISTINCT thing from one `Tuple(…)` value: it +/// reuses the tuple ABI under the hood (resolves to a `.tuple` TypeId), but is +/// valid ONLY as a function/closure return type (the general type resolver +/// rejects it anywhere else), and its result is consumed only by destructuring +/// (`a, b := f()`), never bound to a single value. Same shape as a tuple type so +/// the resolver can reuse the field-resolution path. The single-value `(T, !)` +/// (one value + error) is NOT this — it is a plain failable, `-> T !`. +pub const ReturnTypeExpr = struct { + field_types: []const *Node, + field_names: ?[]const []const u8, // null for positional + /// Per-slot default value expressions (`(sum: i32 = 0, good: bool)`), 1:1 + /// with `field_types`; an entry is null when that slot has no default. null + /// (the whole field) when NO slot has a default. A defaulted named slot is + /// exempt from the must-set rule — the default seeds the slot local. + field_defaults: ?[]const ?*Node = null, +}; + pub const TupleLiteral = struct { elements: []const TupleElement, // Explicit tuple type for the `Tuple(...).( ... )` typed-construction form diff --git a/src/ir/lower.zig b/src/ir/lower.zig index b80bf8e9..769520ba 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -312,6 +312,16 @@ pub const Lowering = struct { /// Cleared per function body (the `Ref` space is per-function). narrowed_refs: std.AutoHashMap(Ref, void) = undefined, force_block_value: bool = false, // set by lowerBlockValue to extract if-else values + // Set while lowering a NAMED multi-return function body (`-> (x: A, y: B)`): + // the slot names (1:1 with the return tuple's fields; a trailing "!" marks + // the failable error slot). The slots are bound as in-scope assignable locals; + // at end-of-body with no explicit `return`, `lowerValueBody` synthesizes the + // implicit return from them (must-set rule: an unset, undefaulted slot errors). + named_return_names: ?[]const []const u8 = null, + // Per-slot default exprs (1:1 with the return tuple's fields; null where the + // slot has none). A defaulted named-return slot is seeded with its default + // and exempt from the must-set rule. + named_return_defaults: ?[]const ?*const ast.Node = null, block_terminated: bool = false, // set when constant-folded if emits a return/br into current block in_lambda_body: bool = false, // true while lowering a closure-literal body; sharpens the `raise`-not-failable diagnostic (ERR E5.1: tell the user to annotate `-> (T, !)`) defer_stack: std.ArrayList(CleanupEntry) = std.ArrayList(CleanupEntry).empty, // block-scoped defer + onfail cleanup stack @@ -646,6 +656,12 @@ pub const Lowering = struct { pub fn resolveReturnType(self: *Lowering, fd: *const ast.FnDecl) TypeId { if (fd.return_type) |rt| { + // A bare-paren multi-return signature `(A, B)` is valid HERE (return + // position); it resolves to its reused tuple TypeId. Misuse as a VALUE + // type (a param / field / var annotation) is rejected at those sites + // (`resolveParamType` et al.), not in the common resolver — return + // types are re-resolved in many places (call-result typing, protocol + // impls) that a central reject would wrongly trip. return self.resolveTypeWithBindings(rt); } // No explicit annotation — the type is inferred from the body, which @@ -715,6 +731,19 @@ pub const Lowering = struct { }; } + /// A bare-paren `(A, B)` multi-return SIGNATURE is valid only as a + /// function/closure return type — never as a VALUE type (a parameter / + /// variable / field annotation), where a tuple value uses `Tuple(…)`. Emits a + /// diagnostic and returns true when `node` is a `ReturnTypeExpr`. (`what` names + /// the offending position, e.g. "parameter" / "variable" / "field".) + pub fn rejectMultiReturnValueType(self: *Lowering, node: *const ast.Node, what: []const u8) bool { + if (node.data != .return_type_expr) return false; + if (self.diagnostics) |d| { + d.addFmt(.err, node.span, "a bare-paren `(A, B)` is a multi-return signature, valid only as a return type; a tuple-valued {s} uses `Tuple(…)`", .{what}); + } + return true; + } + pub fn resolveParamType(self: *Lowering, p: *const ast.Param) TypeId { // A plain value param with no annotation can only be typed from // context (a lambda's target closure signature). When `resolveParamType` @@ -728,6 +757,9 @@ pub const Lowering = struct { } return .unresolved; } + // A bare-paren `(A, B)` is a MULTI-RETURN signature, valid only as a + // return type — not a parameter value type (use `Tuple(…)`). + if (self.rejectMultiReturnValueType(p.type_expr, "parameter")) return .unresolved; const declared_ty = self.resolveTypeWithBindings(p.type_expr); if (p.is_variadic) { // Two surface forms: @@ -1834,6 +1866,9 @@ pub const Lowering = struct { pub const lowerInlineBranch = lower_stmt.lowerInlineBranch; pub const lowerBlockValue = lower_stmt.lowerBlockValue; pub const lowerValueBody = lower_stmt.lowerValueBody; + pub const bindNamedReturnSlots = lower_stmt.bindNamedReturnSlots; + pub const synthesizeNamedReturn = lower_stmt.synthesizeNamedReturn; + pub const validateMultiReturn = lower_stmt.validateMultiReturn; pub const tryLowerAsExpr = lower_stmt.tryLowerAsExpr; pub const lowerStmt = lower_stmt.lowerStmt; pub const lowerVarDecl = lower_stmt.lowerVarDecl; diff --git a/src/ir/lower/decl.zig b/src/ir/lower/decl.zig index 567dfd4c..af5ec77a 100644 --- a/src/ir/lower/decl.zig +++ b/src/ir/lower/decl.zig @@ -2716,6 +2716,18 @@ pub fn lowerFunctionBodyInto(self: *Lowering, fd: *const ast.FnDecl, fid: FuncId scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); }; + // Named multi-return (`-> (x: A, y: B)`): bind the slots as in-scope locals + // for the body to assign; `lowerValueBody` synthesizes the implicit return. + const saved_nrn = self.named_return_names; + const saved_nrd = self.named_return_defaults; + self.named_return_names = null; + self.named_return_defaults = null; + defer { + self.named_return_names = saved_nrn; + self.named_return_defaults = saved_nrd; + } + if (fd.abi != .naked) self.bindNamedReturnSlots(fd, ret_ty, &scope); + // Inbound entry points + abi(.c) sx functions: bind current_ctx_ref // to the static default before any user code runs. if (!wants_ctx and self.implicit_ctx_enabled) { @@ -2873,6 +2885,17 @@ pub fn lowerFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, i scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true }); }; + // Named multi-return slots as in-scope locals (see the sibling body path). + const saved_nrn_lf = self.named_return_names; + const saved_nrd_lf = self.named_return_defaults; + self.named_return_names = null; + self.named_return_defaults = null; + defer { + self.named_return_names = saved_nrn_lf; + self.named_return_defaults = saved_nrd_lf; + } + if (fd.abi != .naked) self.bindNamedReturnSlots(fd, ret_ty, &scope); + // Inbound entry points + abi(.c) sx functions: bind // current_ctx_ref to &__sx_default_context. See companion comment // in `lowerFunction` for the same case. diff --git a/src/ir/lower/generic.zig b/src/ir/lower/generic.zig index 95b7101c..97c828ea 100644 --- a/src/ir/lower/generic.zig +++ b/src/ir/lower/generic.zig @@ -661,6 +661,13 @@ pub fn matchTypeParam(_: *Lowering, type_node: *const Node, tp_name: []const u8) if (ct.return_type) |rt| if (matchTypeParamStatic(rt, tp_name)) break :blk true; break :blk false; }, + // A failable closure return `Closure() -> $R !E` folds to a `(T, !)` + // tuple_type_expr, so a `$R` in the value slot lives inside the tuple's + // field_types — descend so the param is still seen as generic-bearing. + .tuple_type_expr => |tt| blk: { + for (tt.field_types) |ft| if (matchTypeParamStatic(ft, tp_name)) break :blk true; + break :blk false; + }, .parameterized_type_expr => |pt| blk: { for (pt.args) |a| if (matchTypeParamStatic(a, tp_name)) break :blk true; break :blk false; @@ -683,6 +690,12 @@ pub fn matchTypeParamStatic(type_node: *const Node, tp_name: []const u8) bool { if (ct.return_type) |rt| if (matchTypeParamStatic(rt, tp_name)) break :blk true; break :blk false; }, + // See the `matchTypeParam` tuple arm — a failable closure return folds + // to a `(T, !)` tuple_type_expr; descend into its value field(s). + .tuple_type_expr => |tt| blk: { + for (tt.field_types) |ft| if (matchTypeParamStatic(ft, tp_name)) break :blk true; + break :blk false; + }, .parameterized_type_expr => |pt| blk: { for (pt.args) |a| if (matchTypeParamStatic(a, tp_name)) break :blk true; break :blk false; @@ -764,6 +777,31 @@ pub fn extractTypeParam(self: *Lowering, type_node: *const Node, arg_ty: TypeId, } break :blk null; }, + .tuple_type_expr => |tt| blk: { + // A failable closure return `Closure() -> $R !E` folds to a `(T, !)` + // tuple_type_expr, so this arm is reached when inferring `$R` from a + // closure ARG's return type. Two arg shapes must both bind: + // - failable arg (`() -> i64 !E`): its closure `ret` is a `.tuple` + // `(i64, errset)` — match field-wise against the param tuple. + // - non-failable arg (`() -> i64`) ∅-widened into the failable slot: + // its `ret` is the bare value type `i64` — match the param's FIRST + // value field against the whole arg type. + if (!arg_ty.isBuiltin()) { + const info = self.module.types.get(arg_ty); + if (info == .tuple) { + const at = info.tuple; + for (tt.field_types, 0..) |ft, i| { + if (i >= at.fields.len) break; + if (self.extractTypeParam(ft, at.fields[i], tp_name)) |ety| break :blk ety; + } + break :blk null; + } + } + // arg is a bare value type (builtin or single non-tuple): bind it to + // the tuple's first (value) field. + if (tt.field_types.len > 0) break :blk self.extractTypeParam(tt.field_types[0], arg_ty, tp_name); + break :blk null; + }, .parameterized_type_expr => |pt| blk: { // A generic-struct param head (`Box($T)`, also reached recursively // for a pointer-wrapped `*Box($T)`): the arg is a monomorphized diff --git a/src/ir/lower/nominal.zig b/src/ir/lower/nominal.zig index ddb89db5..179c0ed5 100644 --- a/src/ir/lower/nominal.zig +++ b/src/ir/lower/nominal.zig @@ -671,6 +671,7 @@ pub fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_fil using_idx += 1; } if (field_idx < total_explicit) { + _ = self.rejectMultiReturnValueType(sd.field_types[field_idx], "field"); const field_ty = self.resolveType(sd.field_types[field_idx]); fields.append(self.alloc, .{ .name = table.internString(sd.field_names[field_idx]), diff --git a/src/ir/lower/stmt.zig b/src/ir/lower/stmt.zig index 8a0af14b..df6ac22f 100644 --- a/src/ir/lower/stmt.zig +++ b/src/ir/lower/stmt.zig @@ -163,6 +163,16 @@ pub fn lowerValueBody(self: *Lowering, body: *const Node, ret_ty: TypeId) void { return; } } + // A NAMED multi-return function (`-> (x: A, y: B)`) with no explicit + // `return`: synthesize the implicit return from the named slot LOCALS (which + // the body assigned). The must-set rule is checked here — an unset, undefaulted + // slot is a loud error, not a silent fill. This takes precedence over the + // "produces no value" diagnostic below (the body legitimately produces its + // result by assigning the slots, not via a trailing expression). + if (self.named_return_names) |names| { + self.synthesizeNamedReturn(body, ret_ty, names); + return; + } // A PURE-failable function (`-> !` / `-> !Named`, whose entire return IS // the error channel) carries no success value — a void body is a normal // success exit, not a missing value. `ensureTerminator` emits the @@ -196,6 +206,150 @@ pub fn lowerValueBody(self: *Lowering, body: *const Node, ret_ty: TypeId) void { self.ensureTerminator(ret_ty); } +/// Definite-assignment check for the named-return must-set rule: true iff every +/// non-diverging path through `node` assigns the bare identifier `name` (or +/// diverges via `return`/`raise` before reaching the implicit return). PATH- +/// SENSITIVE — a slot set in only ONE branch of an `if` (no `else`) is NOT +/// definitely assigned, so it errors instead of returning a stale/garbage value. +/// - `return`/`raise` → vacuously true (that path never reaches the implicit +/// return, so the slot need not be set on it). +/// - block → the FIRST statement that definitely assigns (or diverges) settles +/// it (sequential composition). +/// - `if` → both branches must (an `if` with no `else` cannot). +/// - `push { … }` → always runs its body. +/// - `match` → all arms must AND there is an `else` arm (exhaustiveness). +/// - `while`/`for`/`defer`/`catch` and everything else → not guaranteed. +/// Does not descend into nested function / lambda bodies (their `return`s own). +fn definitelyAssigns(node: *const Node, name: []const u8) bool { + return switch (node.data) { + .assignment => |a| a.target.data == .identifier and std.mem.eql(u8, a.target.data.identifier.name, name), + .multi_assign => |ma| blk: { + for (ma.targets) |t| { + if (t.data == .identifier and std.mem.eql(u8, t.data.identifier.name, name)) break :blk true; + } + break :blk false; + }, + // Function-level divergence — this path never reaches the implicit return. + .return_stmt, .raise_stmt => true, + .block => |blk| { + for (blk.stmts) |s| if (definitelyAssigns(s, name)) return true; + return false; + }, + .if_expr => |ie| ie.else_branch != null and + definitelyAssigns(ie.then_branch, name) and definitelyAssigns(ie.else_branch.?, name), + .push_stmt => |ps| definitelyAssigns(ps.body, name), + .match_expr => |me| blk: { + var has_else = false; + for (me.arms) |arm| { + if (arm.pattern == null) has_else = true; + if (!definitelyAssigns(arm.body, name)) break :blk false; + } + break :blk has_else; + }, + else => false, + }; +} + +/// Bind a NAMED multi-return signature's value slots (`-> (x: A, y: B)`) as +/// in-scope assignable locals, so the body's `x = …` writes to them. Each slot +/// is a zero-initialized alloca (deterministic value if a path misses it — see +/// `bodyAssignsTo`). Sets `self.named_return_names`; the caller restores it. +/// No-op for a positional multi-return (no names → use an explicit `return`). +pub fn bindNamedReturnSlots(self: *Lowering, fd: *const ast.FnDecl, ret_ty: TypeId, scope: *Scope) void { + const rt = fd.return_type orelse return; + if (rt.data != .return_type_expr) return; + const names = rt.data.return_type_expr.field_names orelse return; // positional → no locals + const defaults = rt.data.return_type_expr.field_defaults; + if (ret_ty.isBuiltin()) return; + const ti = self.module.types.get(ret_ty); + if (ti != .tuple) return; + const fields = ti.tuple.fields; + const value_count = if (self.errorChannelOf(ret_ty) != null) fields.len - 1 else fields.len; + var i: usize = 0; + while (i < value_count and i < names.len) : (i += 1) { + const nm = names[i]; + if (nm.len == 0 or std.mem.eql(u8, nm, "!")) continue; + // A named-return slot that shadows a PARAMETER of the same name would + // silently hide the parameter behind a fresh local — reject the collision. + for (fd.params) |p| { + if (std.mem.eql(u8, p.name, nm)) { + if (self.diagnostics) |d| { + d.addFmt(.err, rt.span, "named return '{s}' collides with a parameter of the same name — rename one", .{nm}); + } + } + } + const fty = fields[i]; + const slot = self.builder.alloca(fty); + // Seed the slot. A slot with a DEFAULT gets it (type-checked, lowered, + // coerced). Otherwise zero/default-init for ANY type (a deterministic + // value if the path-insensitive must-set can't prove a path sets it — + // never raw garbage; covers string / struct / float slots too). + const dflt: ?*const Node = if (defaults) |ds| (if (i < ds.len) ds[i] else null) else null; + if (dflt) |dn| { + const saved_target = self.target_type; + self.target_type = fty; + const dval = self.lowerExpr(dn); + self.target_type = saved_target; + const dval_ty = self.builder.getRefType(dval); + // Reject a default whose type has NO coercion to the slot type (e.g. + // `sum: i32 = "hi"`) — a `.none` plan would pass the value through + // unchanged and bit-mangle / segfault. (The same hole exists for any + // annotated assignment `x: i32 = "hi"` — a broader pre-existing gap.) + if (dval_ty != .unresolved and self.coercionResolver().classify(dval_ty, fty) == .none and dval_ty != fty) { + if (self.diagnostics) |d| { + d.addFmt(.err, dn.span, "named return '{s}' has a default of type '{s}' that does not match its declared type '{s}'", .{ nm, self.formatTypeName(dval_ty), self.formatTypeName(fty) }); + } + self.builder.store(slot, self.buildDefaultValue(fty)); + } else { + self.builder.store(slot, self.coerceToType(dval, dval_ty, fty)); + } + } else { + self.builder.store(slot, self.buildDefaultValue(fty)); + } + scope.put(nm, .{ .ref = slot, .ty = fty, .is_alloca = true }); + } + self.named_return_names = names; + self.named_return_defaults = defaults; +} + +/// Emit the implicit return of a NAMED multi-return body: enforce the must-set +/// rule on each value slot, then synthesize and lower `return n0 = n0, n1 = n1` +/// over the slot locals — reusing the ordinary return path (tuple build + +/// value-carrying-failable assembly), so failable named multi-returns work too. +pub fn synthesizeNamedReturn(self: *Lowering, body: *const Node, ret_ty: TypeId, names: []const []const u8) void { + const ti = self.module.types.get(ret_ty); + if (ti != .tuple) { + self.ensureTerminator(ret_ty); + return; + } + const fields = ti.tuple.fields; + const value_count = if (self.errorChannelOf(ret_ty) != null) fields.len - 1 else fields.len; + + var elems = std.ArrayList(ast.TupleElement).empty; + defer elems.deinit(self.alloc); + var i: usize = 0; + while (i < value_count and i < names.len) : (i += 1) { + const nm = names[i]; + if (nm.len == 0 or std.mem.eql(u8, nm, "!")) continue; + // Must-set: a slot not DEFINITELY assigned (on every non-diverging path) + // and with no default is an error. A defaulted slot is exempt — its + // default seeds the local in `bindNamedReturnSlots`. + const has_default = if (self.named_return_defaults) |ds| (i < ds.len and ds[i] != null) else false; + if (!has_default and !definitelyAssigns(body, nm)) { + if (self.diagnostics) |d| { + d.addFmt(.err, body.span, "named return '{s}' may be unset (not assigned on every path) and has no default — assign it on every path, give it a default, or end with an explicit `return`", .{nm}); + } + } + const id_node = self.alloc.create(Node) catch return; + id_node.* = .{ .span = body.span, .data = .{ .identifier = .{ .name = nm } } }; + elems.append(self.alloc, .{ .name = nm, .value = id_node }) catch return; + } + const tl = self.alloc.create(Node) catch return; + tl.* = .{ .span = body.span, .data = .{ .tuple_literal = .{ .elements = elems.toOwnedSlice(self.alloc) catch return } } }; + const rs = ast.ReturnStmt{ .value = tl }; + self.lowerReturn(&rs); +} + /// Try to lower a node as an expression, returning its value. /// Statement nodes are lowered as statements (returning null). pub fn tryLowerAsExpr(self: *Lowering, node: *const Node) ?Ref { @@ -295,6 +449,7 @@ pub fn lowerVarDecl(self: *Lowering, vd: *const ast.VarDecl) void { } if (vd.type_annotation) |ta| { // Explicit type annotation — resolve type first, then lower value + _ = self.rejectMultiReturnValueType(ta, "variable"); const ty = self.resolveType(ta); const slot = self.builder.alloca(ty); if (vd.value) |val| { @@ -535,12 +690,80 @@ pub fn lowerConstDecl(self: *Lowering, cd: *const ast.ConstDecl) void { } } +/// Validate an explicit `return` value against a multi-VALUE return type (≥2 +/// value slots). Emits diagnostics; does not rewrite. Covers: a bare value where +/// multiple are required (`return 5` for `-> (i64, i64)`), wrong arity (too few / +/// too many), and named elements that disagree with the slot at their position +/// (named return elements must currently be IN SLOT ORDER — reordering by name is +/// a future nicety, but a mismatch is an error, never a silent wrong result). +/// A single-value or single-failable return is left to the existing path. +pub fn validateMultiReturn(self: *Lowering, value_node: *const Node, ret_ty: TypeId) void { + const diags = self.diagnostics orelse return; + const ret_is_tuple = !ret_ty.isBuiltin() and self.module.types.get(ret_ty) == .tuple; + // A comma list / multi-element literal returned from a SINGLE-value + // (non-tuple) function would silently drop the extra values — reject it. + if (!ret_is_tuple and value_node.data == .tuple_literal) { + const els = value_node.data.tuple_literal.elements; + if (els.len > 1) { + for (els) |e| if (e.value.data == .spread_expr) return; // can't count a spread + diags.addFmt(.err, value_node.span, "this function returns a single value, but a list of {d} was given", .{els.len}); + } + return; + } + if (!ret_is_tuple) return; + const ti = self.module.types.get(ret_ty); + const fields = ti.tuple.fields; + const is_failable = self.errorChannelOf(ret_ty) != null; + const value_count = if (is_failable) fields.len - 1 else fields.len; + if (value_count < 2) return; // single value / single failable — not multi-return + if (value_node.data == .tuple_literal) { + const els = value_node.data.tuple_literal.elements; + // A spread (`..xs`) can expand to any arity — can't check statically. + for (els) |e| if (e.value.data == .spread_expr) return; + // The value-only list (n == value_count) is the bare-comma form; the full + // failable tuple (n == fields, including the error slot) is also allowed. + if (els.len != value_count and els.len != fields.len) { + diags.addFmt(.err, value_node.span, "this function returns {d} values, but {d} {s} given", .{ value_count, els.len, if (els.len == 1) @as([]const u8, "is") else @as([]const u8, "are") }); + return; + } + // Named elements must line up with the slots positionally. + if (ti.tuple.names) |slot_names| { + for (els, 0..) |e, idx| { + const en = e.name orelse continue; + if (idx >= slot_names.len) continue; + const sn = self.module.types.getString(slot_names[idx]); + if (sn.len != 0 and !std.mem.eql(u8, en, sn)) { + diags.addFmt(.err, value_node.span, "named return element '{s}' does not match the slot '{s}' at position {d} — name the elements in slot order", .{ en, sn, idx }); + } + } + } + } else { + // A bare value (not a comma list) where ≥2 are required is valid only if + // it already PRODUCES the whole multi-value tuple — forwarding another + // multi-return's result, or a multi-output `asm { … }`. Any TUPLE-typed + // value qualifies (names may differ from the slots); a non-tuple scalar + // does not — that is the `return 5` for `-> (i64, i64)` garbage case. + const vty = self.inferExprType(value_node); + const v_is_tuple = vty != .unresolved and !vty.isBuiltin() and self.module.types.get(vty) == .tuple; + if (vty != .unresolved and !v_is_tuple) { + diags.addFmt(.err, value_node.span, "this function returns {d} values — return them as `return a, b`, not a single value", .{value_count}); + } + } +} + pub fn lowerReturn(self: *Lowering, rs: *const ast.ReturnStmt) void { if (rs.value) |val| { if (val.data == .identifier and self.isPackName(val.data.identifier.name)) { _ = self.diagPackAsValue(val.data.identifier.name, val.span, .return_value); return; } + // Validate a multi-value return against the function's slots: arity, a + // bare value where multiple are required, and named-element/slot + // agreement. Catches silent garbage (`return 5` for `-> (i64, i64)`) and + // silently-wrong named returns (`return b = …, a = …` ignoring names). + if (self.builder.func) |fid| { + self.validateMultiReturn(val, self.module.functions.items[@intFromEnum(fid)].ret); + } } // Set target_type to function return type so null_literal etc. get the right type. // When inlining a comptime body, the *inlined* fn's declared return type wins diff --git a/src/ir/semantic_diagnostics.zig b/src/ir/semantic_diagnostics.zig index 1ba7335a..de6166fa 100644 --- a/src/ir/semantic_diagnostics.zig +++ b/src/ir/semantic_diagnostics.zig @@ -402,6 +402,7 @@ pub const UnknownTypeChecker = struct { .function_type_expr, .closure_type_expr, .tuple_type_expr, + .return_type_expr, => {}, } } diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index bbf153d5..a2a3e397 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -190,6 +190,10 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap // lives in PackResolver. .closure_type_expr => |ct| type_resolver.TypeResolver.resolveCompound(table, n, si) orelse resolveClosurePackShape(&ct, table, alias_map, consts), .tuple_type_expr => |tt| type_resolver.TypeResolver.resolveCompound(table, n, si) orelse resolveTupleSpreadShape(&tt, table, alias_map, consts), + // A multi-return signature resolves to its REUSED tuple TypeId — the ABI + // is a tuple; only its meaning ("multiple return values", return-only, + // destructure-only) differs, which the AST node (not the TypeId) carries. + .return_type_expr => type_resolver.TypeResolver.resolveCompound(table, n, si) orelse .unresolved, .pack_index_type_expr => { // Pack-index `$args[N]` in a type position must be resolved // against an active pack binding — `type_bridge` has no access @@ -334,6 +338,7 @@ pub fn isTypeShapedAstNode(node: *const Node, table: *TypeTable) bool { .function_type_expr, .closure_type_expr, .tuple_type_expr, + .return_type_expr, .parameterized_type_expr, .pack_index_type_expr, .comptime_pack_ref, diff --git a/src/ir/type_resolver.zig b/src/ir/type_resolver.zig index 5b04ce89..30b58e1d 100644 --- a/src/ir/type_resolver.zig +++ b/src/ir/type_resolver.zig @@ -248,43 +248,50 @@ pub const TypeResolver = struct { const ret_ty = if (ct.return_type) |rt| inner.resolveInner(rt) else TypeId.void; break :blk table.closureType(param_ids.items, ret_ty); }, - .tuple_type_expr => |tt| blk: { - // A spread field `(..xs)` expands to many fields via the pack - // state — defer to PackResolver by returning null. - for (tt.field_types) |ft| if (ft.data == .spread_expr) break :blk null; - var field_ids = std.ArrayList(TypeId).empty; - defer field_ids.deinit(table.alloc); - for (tt.field_types) |ft| { - const fid = inner.resolveInner(ft); - // A non-type tuple element (e.g. the `1` in `Tuple(i32, 1)`) - // resolves to `.unresolved`; never intern a tuple carrying it - // — that bogus type would reach LLVM emission and panic. The - // user-facing diagnostic is emitted by the literal-rejection - // arm in `resolveTypeArg` (lower.zig, the `tuple_type_expr` - // check); here we just refuse to fabricate the type, - // propagating the sentinel up. - if (fid == .unresolved) break :blk .unresolved; - field_ids.append(table.alloc, fid) catch return .unresolved; - } - // Preserve field names for a named tuple `(x: T, y: U)` when the - // name and field counts agree (so `t.x` resolves). - var name_ids: ?[]const StringId = null; - if (tt.field_names) |names| { - if (names.len == field_ids.items.len) { - var ids = std.ArrayList(StringId).empty; - for (names) |n| ids.append(table.alloc, table.internString(n)) catch return .unresolved; - name_ids = ids.toOwnedSlice(table.alloc) catch null; - } - } - break :blk table.intern(.{ .tuple = .{ - .fields = table.alloc.dupe(TypeId, field_ids.items) catch return .unresolved, - .names = name_ids, - } }); - }, + .tuple_type_expr => |tt| internTupleLike(table, tt.field_types, tt.field_names, inner), + // A multi-return signature `(A, B)` resolves to the SAME tuple TypeId + // (the ABI is a tuple); its distinct meaning lives in the AST node. + .return_type_expr => |rt| internTupleLike(table, rt.field_types, rt.field_names, inner), else => null, }; } + /// Intern a `.tuple` TypeId from a list of field-type nodes (+ optional + /// names) — the shared body of the `tuple_type_expr` and `return_type_expr` + /// resolution arms. Returns null to defer a spread to the (stateful) + /// PackResolver, `.unresolved` if any field is non-type, else the tuple. + fn internTupleLike(table: *TypeTable, field_types: []const *Node, field_names: ?[]const []const u8, inner: anytype) ?TypeId { + // A spread field `(..xs)` expands to many fields via the pack state — + // defer to PackResolver by returning null. + for (field_types) |ft| if (ft.data == .spread_expr) return null; + var field_ids = std.ArrayList(TypeId).empty; + defer field_ids.deinit(table.alloc); + for (field_types) |ft| { + const fid = inner.resolveInner(ft); + // A non-type element (e.g. the `1` in `Tuple(i32, 1)`) resolves to + // `.unresolved`; never intern a tuple carrying it — that bogus type + // would reach LLVM emission and panic. The user-facing diagnostic is + // emitted by the literal-rejection arm in `resolveTypeArg`; here we + // just refuse to fabricate the type, propagating the sentinel up. + if (fid == .unresolved) return .unresolved; + field_ids.append(table.alloc, fid) catch return .unresolved; + } + // Preserve field names for a named tuple `(x: T, y: U)` when the name and + // field counts agree (so `t.x` resolves). + var name_ids: ?[]const StringId = null; + if (field_names) |names| { + if (names.len == field_ids.items.len) { + var ids = std.ArrayList(StringId).empty; + for (names) |n| ids.append(table.alloc, table.internString(n)) catch return .unresolved; + name_ids = ids.toOwnedSlice(table.alloc) catch null; + } + } + return table.intern(.{ .tuple = .{ + .fields = table.alloc.dupe(TypeId, field_ids.items) catch return .unresolved, + .names = name_ids, + } }); + } + /// Generic type-param binding lookup (`$T`, or a bare return-type `T`). /// Reads the caller-supplied `ResolveEnv` rather than hidden `Lowering` /// state. Returns null when there are no active bindings or the name is diff --git a/src/parser.test.zig b/src/parser.test.zig index 2b4543ee..a31ffe01 100644 --- a/src/parser.test.zig +++ b/src/parser.test.zig @@ -384,20 +384,33 @@ test "parser: -> ! stays a bare error_type_expr" { try std.testing.expect(rt.data == .error_type_expr); } -// Old inline `-> (T, !)` failable form is gone — rejected with the new-form hint. -test "parser: old inline -> (T, !) is rejected" { +// Bare-paren `-> (T, !)` is a SINGLE-value failable return (= `-> T !`): one +// value slot + a trailing error channel. Parses to a `(T, !)` tuple_type_expr — +// NOT a multi-return signature (only ≥2 value slots are `return_type_expr`). +test "parser: -> (T, !) is a single-value failable, not multi-return" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); var parser = Parser.init(arena.allocator(), "f :: () -> (i64, !) { 0 }"); - try std.testing.expectError(error.ParseError, parser.parse()); + const root = try parser.parse(); + const rt = root.data.root.decls[0].data.fn_decl.return_type.?; + try std.testing.expect(rt.data == .tuple_type_expr); + const fields = rt.data.tuple_type_expr.field_types; + try std.testing.expectEqual(@as(usize, 2), fields.len); + try std.testing.expect(fields[1].data == .error_type_expr); } -// Bare-paren tuple TYPE `(A, B)` is gone — rejected (tuple types use `Tuple(...)`). -test "parser: bare-paren tuple type (A, B) is rejected" { +// A bare-paren list with ≥2 VALUE slots is a MULTI-RETURN signature: it PARSES +// to its OWN `return_type_expr` node (a distinct thing from a `Tuple(…)` value). +// Its rejection OUTSIDE a return position is a RESOLVE-time diagnostic (see the +// corpus), not a parse error. +test "parser: bare-paren (A, B) parses to a return_type_expr" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); - var parser = Parser.init(arena.allocator(), "f :: (t: (i64, i32)) { }"); - try std.testing.expectError(error.ParseError, parser.parse()); + var parser = Parser.init(arena.allocator(), "f :: () -> (i64, i32) { 0 }"); + const root = try parser.parse(); + const rt = root.data.root.decls[0].data.fn_decl.return_type.?; + try std.testing.expect(rt.data == .return_type_expr); + try std.testing.expectEqual(@as(usize, 2), rt.data.return_type_expr.field_types.len); } // Bare-paren tuple VALUE `(a, b)` is gone — rejected (tuple values use `.(...)`). diff --git a/src/parser.zig b/src/parser.zig index ad55bff9..369005a4 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -610,6 +610,11 @@ pub const Parser = struct { self.advance(); // skip '(' var param_types = std.ArrayList(*Node).empty; var param_names = std.ArrayList(?[]const u8).empty; + // Per-element default value (`(sum: i32 = 0, …)`), 1:1 with + // `param_types`; meaningful only for a multi-return signature + // (`return_type_expr`) — ignored for grouping / function / tuple forms. + var param_defaults = std.ArrayList(?*Node).empty; + var any_default = false; var has_names = false; // An error channel type (`!` / `!Named`) is only valid as the // trailing element of a result list. Reject any element after it. @@ -640,6 +645,7 @@ pub const Parser = struct { const spread = try self.createNode(spread_start, .{ .spread_expr = .{ .operand = operand } }); try param_names.append(self.allocator, null); try param_types.append(self.allocator, spread); + try param_defaults.append(self.allocator, null); continue; } // Check for optional param name: `name: Type` @@ -656,6 +662,16 @@ pub const Parser = struct { const elem = try self.parseTypeExpr(); if (elem.data == .error_type_expr) saw_error_type = true; try param_types.append(self.allocator, elem); + // Optional default value `name: Type = ` — a multi-return + // slot default. Parse it for every element (1:1 with types) and + // attach only to a `return_type_expr` below. + var elem_default: ?*Node = null; + if (self.current.tag == .equal) { + self.advance(); // skip '=' + elem_default = try self.parseExpr(); + any_default = true; + } + try param_defaults.append(self.allocator, elem_default); } try self.expect(.r_paren); if (self.current.tag == .arrow) { @@ -673,6 +689,12 @@ pub const Parser = struct { .abi = abi, } }); } + // Empty parens `()` with no `->` is the void/unit type: + // `a :: () -> () { }` is equivalent to `-> void`. (`() -> R` was the + // zero-param function type handled by the arrow branch above.) + if (param_types.items.len == 0) { + return try self.createNode(start, .{ .type_expr = .{ .name = "void" } }); + } // No '->': bare `(...)` in type position is GROUPING ONLY. A single // UNNAMED, non-spread element with NO trailing comma resolves to the // inner type. This lets `(Closure(i64,i64) -> i64)`, `?(?i64)`, etc. @@ -682,15 +704,49 @@ pub const Parser = struct { { return param_types.items[0]; } - // Anything else (a top-level comma, a `(T,)` 1-tuple, names, a - // spread) used to build a bare-paren `tuple_type_expr`. That grammar - // is gone: tuple types are written `Tuple( … )`. If the group ends in - // an error channel `!`, it is the old failable spelling `-> (T, !)`. + // A bare-paren result list classifies by VALUE-slot count (fields + // minus a trailing error channel — the error is ALWAYS the last slot): + // - ≥2 value slots → a MULTI-RETURN signature `(A, B)` / + // `(x: A, y: B)` / `(A, B, !)`: its OWN node (`return_type_expr`), + // a DISTINCT thing from a `Tuple(…)` value (not a tuple, + // return-only, destructure-only). + // - 1 value slot + error `(T, !)` → a SINGLE-value failable, exactly + // `-> T !` (NOT multi-return): the failable `tuple_type_expr`. + // - anything else (a `(T,)` 1-tuple, a stray spread) → rejected; + // a real tuple VALUE type uses `Tuple(…)`. const last_is_err = param_types.items.len > 0 and param_types.items[param_types.items.len - 1].data == .error_type_expr; - if (last_is_err) { - return self.fail("failable returns use `-> T !` or `-> Tuple(T1,T2) !`"); + const value_count = param_types.items.len - @as(usize, if (last_is_err) 1 else 0); + if (value_count >= 2 or (last_is_err and value_count == 1)) { + var fnames: ?[]const []const u8 = null; + if (has_names) { + // field_names is non-optional and must stay 1:1 with + // field_types; map an unnamed value slot to "" and a trailing + // error slot to the "!" placeholder (identified by position, + // never by this name — see errorChannelOf). + const nm = try self.allocator.alloc([]const u8, param_names.items.len); + for (param_names.items, 0..) |pn, i| { + nm[i] = pn orelse (if (last_is_err and i == param_names.items.len - 1) "!" else ""); + } + fnames = nm; + } + const field_types = try param_types.toOwnedSlice(self.allocator); + // ≥2 value slots → multi-return signature; a lone `(T, !)` is just + // a single-value failable (= `-> T !`), a plain failable tuple. + if (value_count >= 2) { + return try self.createNode(start, .{ .return_type_expr = .{ + .field_types = field_types, + .field_names = fnames, + .field_defaults = if (any_default) try param_defaults.toOwnedSlice(self.allocator) else null, + } }); + } + return try self.createNode(start, .{ .tuple_type_expr = .{ + .field_types = field_types, + .field_names = fnames, + } }); } + // Anything else (a `(T,)` 1-tuple, a spread): the bare-paren tuple + // grammar is gone — tuple VALUE types are written `Tuple( … )`. return self.fail("tuple types use `Tuple( … )` (e.g. `Tuple(A, B)`)"); } @@ -1997,13 +2053,25 @@ pub const Parser = struct { .many_pointer_type_expr => |mpte| collectGenericNames(mpte.element_type, list, allocator), .slice_type_expr => |ste| collectGenericNames(ste.element_type, list, allocator), .array_type_expr => |ate| collectGenericNames(ate.element_type, list, allocator), + .optional_type_expr => |ote| collectGenericNames(ote.inner_type, list, allocator), .parameterized_type_expr => |pte| { for (pte.args) |arg| collectGenericNames(arg, list, allocator); }, + .tuple_type_expr => |tte| { + // A failable closure return `Closure() -> $R !E` folds to a + // `(T, !)` tuple_type_expr (parseFnReturnType), so the `$R` + // binding site lives inside the tuple's field_types — descend so + // the value type's generic is still inferred from the call site. + for (tte.field_types) |ft| collectGenericNames(ft, list, allocator); + }, .closure_type_expr => |cte| { for (cte.param_types) |pt| collectGenericNames(pt, list, allocator); if (cte.return_type) |rt| collectGenericNames(rt, list, allocator); }, + .function_type_expr => |fte| { + for (fte.param_types) |pt| collectGenericNames(pt, list, allocator); + if (fte.return_type) |rt| collectGenericNames(rt, list, allocator); + }, else => {}, } } @@ -2274,9 +2342,40 @@ pub const Parser = struct { self.advance(); return try self.createNode(start, .{ .return_stmt = .{ .value = null } }); } - const value = try self.parseExpr(); + // Comma-separated return list — the bare multi-value `return` form: + // `return a, b` (positional) / `return x = a, y = b` (named), no `.(…)` + // tuple literal needed. Each element is `name = expr` (named, like the + // `.(x = v)` form) or a bare `expr` (positional). A SINGLE positional + // element is an ordinary single-value return (unchanged); a comma list + // — or any named element — is a multi-value return, synthesized as the + // same `tuple_literal` the `.(…)` form produces so the return lowering + // maps it onto the function's multi-return slots. + var ret_elems = std.ArrayList(ast.TupleElement).empty; + var ret_any_named = false; + while (true) { + if (self.isIdentLike() and self.peekNext() == .equal) { + const fname = self.tokenSlice(self.current); + self.advance(); // skip name + self.advance(); // skip '=' + const v = try self.parseExpr(); + try ret_elems.append(self.allocator, .{ .name = fname, .value = v }); + ret_any_named = true; + } else { + const v = try self.parseExpr(); + try ret_elems.append(self.allocator, .{ .name = null, .value = v }); + } + if (self.current.tag == .comma) { + self.advance(); + continue; + } + break; + } try self.expect(.semicolon); - return try self.createNode(start, .{ .return_stmt = .{ .value = value } }); + const ret_value: *Node = if (ret_elems.items.len == 1 and !ret_any_named) + ret_elems.items[0].value + else + try self.createNode(start, .{ .tuple_literal = .{ .elements = try ret_elems.toOwnedSlice(self.allocator) } }); + return try self.createNode(start, .{ .return_stmt = .{ .value = ret_value } }); } // Defer statement: defer { body } | defer ; @@ -4065,6 +4164,13 @@ pub const Parser = struct { self.current.tag == .percent or self.current.tag == .plus or self.current.tag == .minus or self.current.tag == .question or self.current.tag == .bang or + // A named multi-return slot DEFAULT (`-> (sum: i32 = 0, …)`): + // skip the `=` and the value expression's literal tokens so the + // scan keeps going to the body `{`, instead of misreading the + // decl as a bodyless function-type alias. + self.current.tag == .equal or self.current.tag == .float_literal or + self.current.tag == .string_literal or + self.current.tag == .kw_true or self.current.tag == .kw_false or self.current.tag == .colon or self.current.tag == .arrow) { self.advance(); diff --git a/src/sema.zig b/src/sema.zig index 2370b411..4e87e44b 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -1314,6 +1314,7 @@ pub const Analyzer = struct { .index_expr, .slice_expr, .tuple_type_expr, + .return_type_expr, => {}, .protocol_decl => |pd| { try self.addSymbol(pd.name, .protocol_type, null, node.span); @@ -1788,6 +1789,7 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node { .index_expr, .slice_expr, .tuple_type_expr, + .return_type_expr, .ufcs_alias, .closure_type_expr, .runtime_class_decl,