feat: multiple return values — bare-paren signatures, named returns, must-set, defaults
A function may return multiple values via a bare-paren return signature: `-> (A, B)` / `-> (x: A, y: B)` / `-> (A, B, !)` (error always the last slot), and `-> ()` is `void`. This is DISTINCT from a `Tuple(…)` value — return-position only (a dedicated `ReturnTypeExpr` AST node resolving to a reused `.tuple` TypeId); a parameter / field / variable annotation `x: (A, B)` is rejected. A single-value `-> (T, !)` stays a plain failable (= `-> T !`). Returns use the bare comma form `return a, b` / `return x = a, y = b` (no `.( … )` literal). Consume by destructuring (`a, b := f()`) or single-bind + field access (`c := f(); c.sum`); a failable bound value holds only the value slots (the error stays on the `!` channel). Named return slots are in-scope assignable locals; with no explicit `return` the implicit return is synthesized from them. Path-sensitive definite-assignment enforces the must-set rule, and a slot may carry a default that exempts it. Validation rejects arity mismatches, out-of-slot-order named elements, a slot/parameter name collision, a comma list from a single-value function, and a multi-return signature used as a value type. Examples 0202-0213; readme + specs updated. issues/0197 files a pre-existing annotated-assignment type-check gap (`x: i32 = "hi"` segfaults) surfaced by the adversarial review.
This commit is contained in:
100
current/CHECKPOINT-MULTIRET.md
Normal file
100
current/CHECKPOINT-MULTIRET.md
Normal file
@@ -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 `= <expr>` 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.
|
||||
156
current/PLAN-MULTIRET.md
Normal file
156
current/PLAN-MULTIRET.md
Normal file
@@ -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/<category>/…` 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.
|
||||
21
examples/types/0202-types-void-empty-parens.sx
Normal file
21
examples/types/0202-types-void-empty-parens.sx
Normal file
@@ -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;
|
||||
}
|
||||
31
examples/types/0203-types-multi-return.sx
Normal file
31
examples/types/0203-types-multi-return.sx
Normal file
@@ -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;
|
||||
}
|
||||
34
examples/types/0204-types-multi-return-failable.sx
Normal file
34
examples/types/0204-types-multi-return-failable.sx
Normal file
@@ -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;
|
||||
}
|
||||
42
examples/types/0205-types-named-return-locals.sx
Normal file
42
examples/types/0205-types-named-return-locals.sx
Normal file
@@ -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;
|
||||
}
|
||||
16
examples/types/0206-types-named-return-must-set.sx
Normal file
16
examples/types/0206-types-named-return-must-set.sx
Normal file
@@ -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;
|
||||
}
|
||||
26
examples/types/0207-types-named-return-defaults.sx
Normal file
26
examples/types/0207-types-named-return-defaults.sx
Normal file
@@ -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;
|
||||
}
|
||||
18
examples/types/0208-types-named-return-conditional-unset.sx
Normal file
18
examples/types/0208-types-named-return-conditional-unset.sx
Normal file
@@ -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;
|
||||
}
|
||||
8
examples/types/0209-types-multi-return-arity.sx
Normal file
8
examples/types/0209-types-multi-return-arity.sx
Normal file
@@ -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; }
|
||||
8
examples/types/0210-types-multi-return-name-order.sx
Normal file
8
examples/types/0210-types-multi-return-name-order.sx
Normal file
@@ -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; }
|
||||
@@ -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; }
|
||||
7
examples/types/0212-types-single-return-comma.sx
Normal file
7
examples/types/0212-types-single-return-comma.sx
Normal file
@@ -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; }
|
||||
6
examples/types/0213-types-multi-return-as-value-type.sx
Normal file
6
examples/types/0213-types-multi-return-as-value-type.sx
Normal file
@@ -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; }
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
hi
|
||||
bye
|
||||
7
|
||||
1
examples/types/expected/0203-types-multi-return.exit
Normal file
1
examples/types/expected/0203-types-multi-return.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
examples/types/expected/0203-types-multi-return.stderr
Normal file
1
examples/types/expected/0203-types-multi-return.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
2
examples/types/expected/0203-types-multi-return.stdout
Normal file
2
examples/types/expected/0203-types-multi-return.stdout
Normal file
@@ -0,0 +1,2 @@
|
||||
17 / 5 = 3 rem 2
|
||||
sum=42 bigger=true
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
classify(7): doubled=14 big=false
|
||||
classify(21): doubled=42 big=true
|
||||
classify(-3): caught Negative
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
combine: sum=42 good=true
|
||||
bounds: lo=5 hi=15
|
||||
roots: val=7 sq=49
|
||||
roots(-1): caught Negative
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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 | }
|
||||
| ^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
classify(5): sum=-1 good=true
|
||||
combine(3,4): total=7 ok=true
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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 | }
|
||||
| ^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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
|
||||
| ^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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) {
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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
|
||||
| ^^^^^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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
|
||||
| ^^^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
56
issues/0197-annotated-assignment-type-mismatch-no-check.md
Normal file
56
issues/0197-annotated-assignment-type-mismatch-no-check.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 0197 — annotated assignment with an incompatible type is unchecked (segfaults)
|
||||
|
||||
**Symptom** — A variable / constant declared with an explicit type annotation and
|
||||
an initializer of an INCOMPATIBLE type is accepted with no diagnostic; the value
|
||||
is passed through unchanged (a `.none` coercion plan), bit-mangling the slot and
|
||||
segfaulting at run time.
|
||||
|
||||
- Observed: `x : i32 = "hi";` compiles, then crashes (`Segmentation fault`).
|
||||
- Expected: a compile-time diagnostic — `cannot initialize 'x' of type 'i32'
|
||||
with a value of type 'string'` (or similar), exit code 1, no crash.
|
||||
|
||||
This is a GENERAL type-checking gap, not specific to any one feature. It was
|
||||
surfaced while reviewing the multi-return feature (a named-return slot default
|
||||
`-> (sum: i32 = "hi", …)` hit the same path; that site now has its own guard, but
|
||||
the underlying annotated-assignment hole remains).
|
||||
|
||||
## Reproduction
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
|
||||
main :: () -> i64 {
|
||||
x : i32 = "hi"; // string initializer for an i32 slot — no diagnostic
|
||||
print("{}\n", x); // garbage, then SIGSEGV
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
`./zig-out/bin/sx run repro.sx` → prints garbage then `Segmentation fault`.
|
||||
`./zig-out/bin/sx ir repro.sx` does NOT crash (it lowers fine) — the bad coercion
|
||||
blows up only at run time.
|
||||
|
||||
## Investigation prompt
|
||||
|
||||
The annotated var/const-decl lowering stores the initializer into the slot
|
||||
WITHOUT checking that the initializer's type can actually reach the annotated
|
||||
type. The store goes through `coerceToType` → `coerceMode`
|
||||
(`src/ir/lower/coerce.zig:596,606`), whose classifier
|
||||
(`coercionResolver().classify`, `src/ir/conversions.zig:54`) returns `.none` for
|
||||
an incompatible pair — and `coerceMode`'s `.no_op, .none => return val` arm
|
||||
(coerce.zig ~614) then passes the value through unchanged, so a 16-byte `string`
|
||||
lands in a 4-byte `i32` slot (and vice-versa), corrupting memory.
|
||||
|
||||
The fix likely belongs at the annotated var-decl / const-decl store sites
|
||||
(`src/ir/lower/stmt.zig` `lowerVarDecl` ~line 450, and the const-decl path) and
|
||||
anywhere else a value is stored into an explicitly-annotated slot: when
|
||||
`classify(src_ty, dst_ty) == .none` and `src_ty != dst_ty`, emit a diagnostic
|
||||
(`self.diagnostics.addFmt(.err, span, "...", ...)`) instead of silently coercing.
|
||||
(The multi-return default site already does exactly this — see the
|
||||
`coercionResolver().classify(...) == .none` guard in `bindNamedReturnSlots`,
|
||||
`src/ir/lower/stmt.zig` — that pattern can be lifted to a shared helper and reused
|
||||
at the assignment sites.)
|
||||
|
||||
Verification: `./zig-out/bin/sx run repro.sx` should print a type-mismatch
|
||||
diagnostic and exit non-zero, NOT segfault. Add a `examples/diagnostics/` or
|
||||
`examples/types/` negative example once fixed.
|
||||
53
readme.md
53
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
|
||||
|
||||
33
specs.md
33
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 }`.
|
||||
|
||||
|
||||
20
src/ast.zig
20
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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -402,6 +402,7 @@ pub const UnknownTypeChecker = struct {
|
||||
.function_type_expr,
|
||||
.closure_type_expr,
|
||||
.tuple_type_expr,
|
||||
.return_type_expr,
|
||||
=> {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 `.(...)`).
|
||||
|
||||
122
src/parser.zig
122
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 = <expr>` — 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 <expr>;
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user