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:
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();
|
||||
|
||||
Reference in New Issue
Block a user