feat: tuple syntax cutover — Tuple(...) type + .(...) value
Replace the bare-paren tuple grammar with explicit, position-unambiguous
forms, mirroring how structs work:
type `(A, B)` -> `Tuple(A, B)` (named keeps `:`)
value `(a, b)` -> `.(a, b)` (named uses `=`)
typed (new) -> `Tuple(A, B).(a, b)` (like `Point.{...}`)
failable `-> (T, !)` -> `-> T !`
`-> (T1, T2, !)`-> `-> Tuple(T1, T2) !` (channel outside Tuple)
Bare `(...)` is now grouping only, everywhere; a comma in bare parens is a
hard error with a migration hint. Grouping, function types `(A, B) -> R`,
param lists, lambdas, and match bindings are unaffected.
`Tuple(...)` is strictly a TYPE in every position (including `size_of` /
`type_info` args); a tuple VALUE comes only from `.(...)` (anonymous) or
`Tuple(...).(...)` (explicitly typed). A bare `Tuple(1, 2)` is a tuple
type with non-type elements -> rejected.
The ~110 tuple-bearing corpus files were migrated with a one-shot
AST-aware migrator (the `sx migrate` tool from the prior commit, removed
here). New examples: 0130 (new syntax), 0131 (typed construction), 1060
(named-tuple failable return). 1116 golden updated for the new hint text.
This commit is contained in:
396
src/parser.zig
396
src/parser.zig
@@ -431,6 +431,68 @@ pub const Parser = struct {
|
||||
return self.fail("expected ':', '=', ';', or 'extern' after type annotation");
|
||||
}
|
||||
|
||||
/// Parse a function/method/lambda return type, folding a trailing `!`
|
||||
/// error channel that sits OUTSIDE the value type into the same
|
||||
/// representation the inline `(T, !)` form produces.
|
||||
///
|
||||
/// `-> T !` ⇒ tuple_type_expr { [T, error_type_expr] } (== `(T, !)`)
|
||||
/// `-> Tuple(A, B) !` ⇒ tuple_type_expr { [A, B, error_type_expr] } (== `(A, B, !)`)
|
||||
/// `-> !` ⇒ error_type_expr (bare; handled by parseTypeExpr directly)
|
||||
///
|
||||
/// The old inline `(T, !)` / `(T1, T2, !)` forms keep working unchanged —
|
||||
/// this only ADDS the trailing-`!`-after-the-type spelling.
|
||||
fn parseFnReturnType(self: *Parser) anyerror!*Node {
|
||||
const start = self.current.loc.start;
|
||||
const ty = try self.parseTypeExpr();
|
||||
|
||||
// A trailing `!` (optionally `!Named`) after the return TYPE denotes the
|
||||
// error channel sitting OUTSIDE the value type. A bare `-> !` is already
|
||||
// an error_type_expr (no value), so a `!` after one would be a doubled
|
||||
// error channel — leave it for the normal "unexpected token" path.
|
||||
if (self.current.tag != .bang or ty.data == .error_type_expr) return ty;
|
||||
|
||||
self.advance(); // skip '!'
|
||||
var set_name: ?[]const u8 = null;
|
||||
if (self.current.tag == .identifier) {
|
||||
set_name = self.tokenSlice(self.current);
|
||||
self.advance();
|
||||
}
|
||||
const err_node = try self.createNode(start, .{ .error_type_expr = .{ .name = set_name } });
|
||||
|
||||
// Build the value+error result list. If the value type is itself a
|
||||
// tuple — `Tuple(A, B)` (positional) or `Tuple(x: A, y: B)` (named) —
|
||||
// flatten its fields and append the error channel, so `-> Tuple(A,B) !`
|
||||
// is identical to `(A, B, !)` and `-> Tuple(x: A, y: B) !` keeps the
|
||||
// `x`/`y` names on the flattened value fields. The value-return lowering
|
||||
// inserts the value slots FLAT into the result tuple, so the result type
|
||||
// must list the value fields flat too — wrapping a named tuple as
|
||||
// `{ {A,B}, err }` would miscompile the flat 2-tuple insert. Otherwise
|
||||
// wrap the single value as `(T, !)`.
|
||||
var fields = std.ArrayList(*Node).empty;
|
||||
var names = std.ArrayList([]const u8).empty;
|
||||
var has_names = false;
|
||||
if (ty.data == .tuple_type_expr) {
|
||||
const tt = ty.data.tuple_type_expr;
|
||||
for (tt.field_types) |f| try fields.append(self.allocator, f);
|
||||
if (tt.field_names) |fn_names| {
|
||||
has_names = true;
|
||||
for (fn_names) |nm| try names.append(self.allocator, nm);
|
||||
// The trailing error channel needs a placeholder name so the
|
||||
// names slice stays 1:1 with field_types. It is identified by
|
||||
// position (last field), never by this name (see
|
||||
// lower/error.zig errorChannelOf).
|
||||
try names.append(self.allocator, "!");
|
||||
}
|
||||
} else {
|
||||
try fields.append(self.allocator, ty);
|
||||
}
|
||||
try fields.append(self.allocator, err_node);
|
||||
return try self.createNode(start, .{ .tuple_type_expr = .{
|
||||
.field_types = try fields.toOwnedSlice(self.allocator),
|
||||
.field_names = if (has_names) try names.toOwnedSlice(self.allocator) else null,
|
||||
} });
|
||||
}
|
||||
|
||||
fn parseTypeExpr(self: *Parser) anyerror!*Node {
|
||||
const start = self.current.loc.start;
|
||||
|
||||
@@ -597,9 +659,12 @@ pub const Parser = struct {
|
||||
}
|
||||
try self.expect(.r_paren);
|
||||
if (self.current.tag == .arrow) {
|
||||
// '->' present: function type
|
||||
// '->' present: function type. Accept a trailing `!`/`!Named`
|
||||
// error channel after the return type (`(i64) -> i64 !E`), folded
|
||||
// to the SAME `(T, !)` / `(A, B, !)` representation the inline form
|
||||
// produces — the old `-> (T, !)` spelling keeps working too.
|
||||
self.advance(); // skip '->'
|
||||
const return_type = try self.parseTypeExpr();
|
||||
const return_type = try self.parseFnReturnType();
|
||||
const abi = try self.parseOptionalAbi();
|
||||
return try self.createNode(start, .{ .function_type_expr = .{
|
||||
.param_types = try param_types.toOwnedSlice(self.allocator),
|
||||
@@ -608,33 +673,25 @@ pub const Parser = struct {
|
||||
.abi = abi,
|
||||
} });
|
||||
}
|
||||
// No '->': GROUPING vs tuple. Mirror value position (`(expr)` groups,
|
||||
// `(expr,)` is a 1-tuple): a single UNNAMED, non-spread element with
|
||||
// NO trailing comma is a grouping — resolve to the inner type. This
|
||||
// lets `(Closure(i64,i64) -> i64)`, `?(?i64)`, etc. parenthesize a
|
||||
// type for grouping/readability. A 1-tuple type now requires the
|
||||
// trailing comma `(T,)`; named `(x: T)` and spread `(..Ts)` stay
|
||||
// tuples.
|
||||
// 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.
|
||||
// parenthesize a type for readability.
|
||||
if (param_types.items.len == 1 and !had_trailing_comma and !has_names and
|
||||
param_types.items[0].data != .spread_expr)
|
||||
{
|
||||
return param_types.items[0];
|
||||
}
|
||||
// Tuple type. Keep field names for a named tuple `(x: T, y: U)` so
|
||||
// `t.x` resolves. `field_names` is non-optional per slot, so
|
||||
// synthesize `_<i>` for any unnamed one.
|
||||
var field_names: ?[]const []const u8 = null;
|
||||
if (has_names) {
|
||||
var fns = std.ArrayList([]const u8).empty;
|
||||
for (param_names.items, 0..) |pn, i| {
|
||||
try fns.append(self.allocator, pn orelse try std.fmt.allocPrint(self.allocator, "_{d}", .{i}));
|
||||
}
|
||||
field_names = try fns.toOwnedSlice(self.allocator);
|
||||
// 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, !)`.
|
||||
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) !`");
|
||||
}
|
||||
return try self.createNode(start, .{ .tuple_type_expr = .{
|
||||
.field_types = try param_types.toOwnedSlice(self.allocator),
|
||||
.field_names = field_names,
|
||||
} });
|
||||
return self.fail("tuple types use `Tuple( … )` (e.g. `Tuple(A, B)`)");
|
||||
}
|
||||
|
||||
if (self.current.tag.isTypeKeyword() or self.isIdentLike()) {
|
||||
@@ -668,6 +725,17 @@ pub const Parser = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Tuple type: `Tuple(A, B)` / `Tuple(T)` / `Tuple()` /
|
||||
// named `Tuple(x: A, y: B)` / pack `Tuple(..Ts)` / `Tuple(..F(Ts))`.
|
||||
// Magic contextual id — only a `Tuple` IMMEDIATELY followed by `(`
|
||||
// builds a tuple type; a bare `Tuple` stays an ordinary identifier
|
||||
// (mirrors `Closure`). Lowers to the SAME `tuple_type_expr` the
|
||||
// inline `(A, B)` / `(x: A, y: B)` / `(..Ts)` forms produce.
|
||||
// Unlike `Closure`, a trailing `->` is REJECTED (no return type).
|
||||
if (std.mem.eql(u8, name, "Tuple") and self.current.tag == .l_paren) {
|
||||
return self.parseTupleTypeBody(start);
|
||||
}
|
||||
|
||||
// Closure type: Closure(params...) -> R
|
||||
// Variadic-pack trailing form: `Closure(Prefix..., ..$pack) -> R`
|
||||
// binds `pack` to a heterogeneous comptime type list at impl
|
||||
@@ -724,7 +792,11 @@ pub const Parser = struct {
|
||||
var return_type: ?*Node = null;
|
||||
if (self.current.tag == .arrow) {
|
||||
self.advance();
|
||||
return_type = try self.parseTypeExpr();
|
||||
// Accept a trailing `!`/`!Named` error channel after the
|
||||
// closure return type (`Closure(i64) -> i64 !E`, `... -> T !`),
|
||||
// folded to the same `(T, !)` / `(A, B, !)` representation; the
|
||||
// old `-> (T, !)` form keeps working.
|
||||
return_type = try self.parseFnReturnType();
|
||||
}
|
||||
return try self.createNode(start, .{ .closure_type_expr = .{
|
||||
.param_types = try param_types.toOwnedSlice(self.allocator),
|
||||
@@ -1267,7 +1339,7 @@ pub const Parser = struct {
|
||||
var return_type: ?*Node = null;
|
||||
if (self.current.tag == .arrow) {
|
||||
self.advance();
|
||||
return_type = try self.parseTypeExpr();
|
||||
return_type = try self.parseFnReturnType();
|
||||
}
|
||||
|
||||
// Optional body (default method) or semicolon
|
||||
@@ -1565,7 +1637,7 @@ pub const Parser = struct {
|
||||
var return_type: ?*Node = null;
|
||||
if (self.current.tag == .arrow) {
|
||||
self.advance();
|
||||
return_type = try self.parseTypeExpr();
|
||||
return_type = try self.parseFnReturnType();
|
||||
}
|
||||
|
||||
// Optional `#jni_method_descriptor("(Sig)Ret")` — explicit JNI descriptor override.
|
||||
@@ -1974,7 +2046,7 @@ pub const Parser = struct {
|
||||
var return_type: ?*Node = null;
|
||||
if (self.current.tag == .arrow) {
|
||||
self.advance();
|
||||
return_type = try self.parseTypeExpr();
|
||||
return_type = try self.parseFnReturnType();
|
||||
}
|
||||
|
||||
// Optional `#get` / `#set` property-accessor marker:
|
||||
@@ -2601,7 +2673,18 @@ pub const Parser = struct {
|
||||
expr = try self.createNode(expr.span.start, .{ .call = .{ .callee = expr, .args = try args.toOwnedSlice(self.allocator) } });
|
||||
} else if (self.current.tag == .dot) {
|
||||
self.advance();
|
||||
if (self.current.tag == .l_brace) {
|
||||
if (self.current.tag == .l_paren and expr.data == .tuple_type_expr) {
|
||||
// Typed tuple-value construction: `Tuple(A, B).( v1, v2 )`
|
||||
// — the `Tuple(...)` type followed by a `.( ... )`
|
||||
// initializer, exactly like `Name.{ ... }` for structs.
|
||||
// Same element rules as the anonymous `.( ... )` form
|
||||
// (positional, named `=`, spread); the resulting
|
||||
// `tuple_literal` carries the explicit tuple type so it
|
||||
// lowers against that type instead of self-typing.
|
||||
const lit = try self.parseDotTupleLiteral(expr.span.start);
|
||||
lit.data.tuple_literal.type_expr = expr;
|
||||
expr = lit;
|
||||
} else if (self.current.tag == .l_brace) {
|
||||
// Struct literal: Type.{ ... }
|
||||
if (expr.data == .identifier) {
|
||||
// Simple name: Vec4.{ ... }
|
||||
@@ -2988,6 +3071,19 @@ pub const Parser = struct {
|
||||
.identifier => {
|
||||
const name = self.tokenSlice(self.current);
|
||||
const is_raw = self.current.is_raw;
|
||||
// `Tuple(...)` in expression position is a tuple TYPE node —
|
||||
// identical to the one the type parser produces — NOT a value.
|
||||
// It is first-class in `size_of` / `type_info` / generic type
|
||||
// args (a non-type element like `Tuple(i32, 1)` is rejected at
|
||||
// the type-demanding site), can stand alone as a `Type` value,
|
||||
// and a postfix `.( ... )` (handled in `parsePostfix`)
|
||||
// constructs a typed tuple VALUE of that type — exactly like
|
||||
// `Name.{ ... }` for structs. A bare `Tuple` not followed by `(`
|
||||
// (or a backtick-raw `` `Tuple ``) stays an ordinary identifier.
|
||||
if (!is_raw and std.mem.eql(u8, name, "Tuple") and self.peekNext() == .l_paren) {
|
||||
self.advance(); // skip `Tuple`; `current` is now `(`
|
||||
return self.parseTupleTypeBody(start);
|
||||
}
|
||||
// A backtick raw identifier (`` `i2 ``) is NEVER type-classified —
|
||||
// it is always a value identifier, bypassing the reserved-type-name
|
||||
// rule. Only a bare spelling is checked for a type name
|
||||
@@ -3027,11 +3123,20 @@ pub const Parser = struct {
|
||||
try self.expect(.r_bracket);
|
||||
return try self.createNode(start, .{ .array_literal = .{ .elements = try elements.toOwnedSlice(self.allocator) } });
|
||||
}
|
||||
// Tuple value literal: .( ... )
|
||||
// positional `.(a, b)` / 1-tuple `.(x)` / empty `.()` /
|
||||
// named `.(x = a, y = b)` (uses `=`, like struct-literal field
|
||||
// init) / spread `.(..xs)` / nested `.( .(a,b), c )`.
|
||||
// Produces the SAME `tuple_literal` node the inline `(a, b)`
|
||||
// form produces, so it self-types structurally with no target.
|
||||
if (self.current.tag == .l_paren) {
|
||||
return self.parseDotTupleLiteral(start);
|
||||
}
|
||||
// Enum literal: .variant_name. A reserved keyword is a valid
|
||||
// variant name here — the leading dot disambiguates (`.enum`,
|
||||
// `.struct`), so no backtick escape is needed.
|
||||
const name = self.dotMemberName() orelse
|
||||
return self.fail("expected variant name, '{', or '[' after '.'");
|
||||
return self.fail("expected variant name, '{', '[', or '(' after '.'");
|
||||
// Enum literal: .variant_name — parsePostfix handles optional (...) as a call
|
||||
return try self.createNode(start, .{ .enum_literal = .{ .name = name } });
|
||||
},
|
||||
@@ -3052,34 +3157,25 @@ pub const Parser = struct {
|
||||
self.in_for_header = false;
|
||||
defer self.in_for_header = saved_hdr_grp;
|
||||
|
||||
// Check for named tuple: (name: expr, ...)
|
||||
// Bare `(...)` is GROUPING ONLY. Tuple VALUES are written
|
||||
// `.( … )` / `Tuple(T..).( … )`. A named element, an empty group,
|
||||
// a leading spread, or a top-level comma all used to build a
|
||||
// bare-paren `tuple_literal`; that grammar is gone.
|
||||
if (self.current.tag == .identifier and self.peekNext() == .colon) {
|
||||
return self.parseTupleLiteralNamed(start);
|
||||
return self.fail("tuple values use `.( … )` (e.g. `.(a, b)`) or `Tuple(T..).( … )`");
|
||||
}
|
||||
|
||||
// Empty parens or first expression
|
||||
if (self.current.tag == .r_paren) {
|
||||
self.advance();
|
||||
// () — empty tuple
|
||||
return try self.createNode(start, .{ .tuple_literal = .{ .elements = &.{} } });
|
||||
return self.fail("tuple values use `.( … )` (e.g. `.(a, b)`) or `Tuple(T..).( … )`");
|
||||
}
|
||||
|
||||
// Leading pack/tuple spread: `(..xs)` / `(..xs.field)` materializes
|
||||
// a tuple from a pack. The spread reuses `spread_expr`; its operand
|
||||
// carries the projection (`xs.field`) shape.
|
||||
if (self.current.tag == .dot_dot) {
|
||||
const spread_start = self.current.loc.start;
|
||||
self.advance(); // skip '..'
|
||||
const operand = try self.parseExpr();
|
||||
const spread = try self.createNode(spread_start, .{ .spread_expr = .{ .operand = operand } });
|
||||
return self.finishTupleAfterFirst(start, spread);
|
||||
return self.fail("tuple values use `.( … )` (e.g. `.(a, b)`) or `Tuple(T..).( … )`");
|
||||
}
|
||||
|
||||
const first = try self.parseExpr();
|
||||
|
||||
// Check for comma → tuple
|
||||
// A top-level comma was a tuple; now it is an error.
|
||||
if (self.current.tag == .comma) {
|
||||
return self.finishTupleAfterFirst(start, first);
|
||||
return self.fail("tuple values use `.( … )` (e.g. `.(a, b)`) or `Tuple(T..).( … )`");
|
||||
}
|
||||
|
||||
// No comma → grouping
|
||||
@@ -3581,45 +3677,156 @@ pub const Parser = struct {
|
||||
return try self.createNode(start_pos, .{ .match_expr = .{ .subject = subject, .arms = try arms.toOwnedSlice(self.allocator) } });
|
||||
}
|
||||
|
||||
/// Parse a named tuple literal: (name: expr, name: expr, ...)
|
||||
/// Called after '(' has been consumed and we've verified identifier + colon pattern.
|
||||
fn parseTupleLiteralNamed(self: *Parser, start: u32) anyerror!*Node {
|
||||
var elements = std.ArrayList(ast.TupleElement).empty;
|
||||
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
||||
if (self.current.tag != .identifier) {
|
||||
return self.fail("expected field name in named tuple");
|
||||
}
|
||||
const name = self.tokenSlice(self.current);
|
||||
self.advance();
|
||||
try self.expect(.colon);
|
||||
const value = try self.parseExpr();
|
||||
try elements.append(self.allocator, .{ .name = name, .value = value });
|
||||
if (self.current.tag == .comma) {
|
||||
self.advance();
|
||||
if (self.current.tag == .r_paren) break;
|
||||
} else break;
|
||||
|
||||
/// Parse a `.( ... )` tuple value literal. `current` is the `(`.
|
||||
/// A token that can only begin a VALUE literal, never a type. Used to give
|
||||
/// `Tuple(...)` a precise lowering-time "element is not a type" diagnostic
|
||||
/// (instead of a generic parse error) when a literal is supplied as a tuple
|
||||
/// element.
|
||||
fn currentTokenIsValueLiteral(self: *Parser) bool {
|
||||
switch (self.current.tag) {
|
||||
.int_literal,
|
||||
.float_literal,
|
||||
.string_literal,
|
||||
.raw_string_literal,
|
||||
.kw_true,
|
||||
.kw_false,
|
||||
.kw_null,
|
||||
=> return true,
|
||||
// A signed numeric literal — `Tuple(i32, -1)` / `Tuple(i32, +2)` —
|
||||
// is value-shaped too, so the precise "tuple type element is not a
|
||||
// type" diagnostic fires instead of the generic "expected type
|
||||
// name" parse error. Only a leading sign DIRECTLY before a number
|
||||
// counts (not `-T`, which is never a valid type anyway).
|
||||
.minus, .plus => {
|
||||
const next = self.peekNext();
|
||||
return next == .int_literal or next == .float_literal;
|
||||
},
|
||||
else => return false,
|
||||
}
|
||||
try self.expect(.r_paren);
|
||||
return try self.createNode(start, .{ .tuple_literal = .{ .elements = try elements.toOwnedSlice(self.allocator) } });
|
||||
}
|
||||
|
||||
/// Finish parsing a tuple after the first positional element and a comma.
|
||||
/// Called with first element already parsed and current token is ','.
|
||||
fn finishTupleAfterFirst(self: *Parser, start: u32, first: *Node) anyerror!*Node {
|
||||
var elements = std.ArrayList(ast.TupleElement).empty;
|
||||
try elements.append(self.allocator, .{ .name = null, .value = first });
|
||||
while (self.current.tag == .comma) {
|
||||
self.advance(); // skip ','
|
||||
if (self.current.tag == .r_paren) break; // trailing comma: (42,)
|
||||
// Spread element: `(a, ..xs, b)` — reuses `spread_expr`.
|
||||
/// Parse a `Tuple(...)` tuple-TYPE body. On entry `current` is the `(`
|
||||
/// immediately after the `Tuple` contextual id (the caller has already
|
||||
/// consumed `Tuple`). Used in BOTH type position (the type parser) and
|
||||
/// expression position (`parsePrimary`'s `.identifier` arm) — `Tuple(...)`
|
||||
/// always denotes a TYPE, never a value. A postfix `.( ... )` after the
|
||||
/// returned `tuple_type_expr` constructs a typed tuple VALUE (handled in
|
||||
/// `parsePostfix`, mirroring `Name.{ ... }` typed struct literals).
|
||||
/// `Tuple(A, B)` / `Tuple(T)` / `Tuple()` / named `Tuple(x: A, y: B)` /
|
||||
/// pack `Tuple(..Ts)` / `Tuple(..F(Ts))`. Lowers to the SAME
|
||||
/// `tuple_type_expr` the inline `(A, B)` / `(x: A, y: B)` / `(..Ts)`
|
||||
/// forms produce. Unlike `Closure`, a trailing `->` is REJECTED.
|
||||
fn parseTupleTypeBody(self: *Parser, start: u32) anyerror!*Node {
|
||||
self.advance(); // skip '('
|
||||
var field_types = std.ArrayList(*Node).empty;
|
||||
var field_name_opt = std.ArrayList(?[]const u8).empty;
|
||||
var has_names = false;
|
||||
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
||||
if (field_types.items.len > 0) {
|
||||
try self.expect(.comma);
|
||||
if (self.current.tag == .r_paren) break; // trailing comma ok
|
||||
}
|
||||
// Pack-spread field: `Tuple(..Ts)` / `Tuple(..F(Ts))` /
|
||||
// `Tuple(..Ts.Arg)`. Reuses `spread_expr` (same machinery as
|
||||
// the inline tuple-type and Closure pack paths).
|
||||
if (self.current.tag == .dot_dot) {
|
||||
const spread_start = self.current.loc.start;
|
||||
const sp_start = self.current.loc.start;
|
||||
self.advance(); // skip '..'
|
||||
const operand = try self.parseTypeExpr();
|
||||
try field_name_opt.append(self.allocator, null);
|
||||
try field_types.append(self.allocator, try self.createNode(sp_start, .{ .spread_expr = .{ .operand = operand } }));
|
||||
continue;
|
||||
}
|
||||
// Named field: `name: Type` (keeps `:`).
|
||||
if (self.isIdentLike() and self.peekNext() == .colon) {
|
||||
const fname = self.tokenSlice(self.current);
|
||||
self.advance(); // skip name
|
||||
self.advance(); // skip ':'
|
||||
try field_name_opt.append(self.allocator, fname);
|
||||
has_names = true;
|
||||
} else {
|
||||
try field_name_opt.append(self.allocator, null);
|
||||
}
|
||||
// A literal element (`Tuple(i32, 1)`) is NOT a type. Parse it as a
|
||||
// value expression so the lowering type-arg check rejects it with
|
||||
// the precise "tuple type element is not a type" diagnostic, rather
|
||||
// than `parseTypeExpr` bailing here with a generic "expected type
|
||||
// name" parse error. Type-shaped elements still go through the type
|
||||
// parser (so `*T`, `[N]T`, `Tuple(...)`, names all parse).
|
||||
if (self.currentTokenIsValueLiteral()) {
|
||||
// A leading `+` on a signed literal (`Tuple(i32, +1)`) has no
|
||||
// unary-op parse; consume it so the number parses as a bare
|
||||
// value literal. `parseUnary` handles the `-` case and falls
|
||||
// through to `parsePrimary` for an unsigned literal.
|
||||
if (self.current.tag == .plus) self.advance();
|
||||
try field_types.append(self.allocator, try self.parseUnary());
|
||||
} else {
|
||||
try field_types.append(self.allocator, try self.parseTypeExpr());
|
||||
}
|
||||
}
|
||||
try self.expect(.r_paren);
|
||||
// A `Tuple(...)` has NO return type — reject `-> R` loudly rather
|
||||
// than silently swallowing it the way `Closure` consumes it.
|
||||
if (self.current.tag == .arrow) {
|
||||
return self.fail("`Tuple` has no return type — remove the `->`");
|
||||
}
|
||||
// Per-slot field names are non-optional in the AST; synthesize
|
||||
// `_<i>` for any unnamed slot (mirrors the inline named-tuple path).
|
||||
var field_names: ?[]const []const u8 = null;
|
||||
if (has_names) {
|
||||
var fns = std.ArrayList([]const u8).empty;
|
||||
for (field_name_opt.items, 0..) |fn_opt, i| {
|
||||
try fns.append(self.allocator, fn_opt orelse try std.fmt.allocPrint(self.allocator, "_{d}", .{i}));
|
||||
}
|
||||
field_names = try fns.toOwnedSlice(self.allocator);
|
||||
}
|
||||
return try self.createNode(start, .{ .tuple_type_expr = .{
|
||||
.field_types = try field_types.toOwnedSlice(self.allocator),
|
||||
.field_names = field_names,
|
||||
} });
|
||||
}
|
||||
|
||||
/// Builds the SAME `tuple_literal` node as the inline `(a, b)` form so the
|
||||
/// two are structurally identical (operators / splat / `.0` all work with no
|
||||
/// target type). Supports positional, 1-tuple, empty, named (`x = a`, using
|
||||
/// `=`), spread (`..xs` / `..xs.field`) and nesting.
|
||||
fn parseDotTupleLiteral(self: *Parser, start: u32) anyerror!*Node {
|
||||
self.advance(); // skip '('
|
||||
|
||||
// `.(` always opens a tuple/grouping, so calls inside parse normally even
|
||||
// within a for header (mirrors the bare `(` primary path).
|
||||
const saved_hdr_grp = self.in_for_header;
|
||||
self.in_for_header = false;
|
||||
defer self.in_for_header = saved_hdr_grp;
|
||||
|
||||
var elements = std.ArrayList(ast.TupleElement).empty;
|
||||
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
||||
if (elements.items.len > 0) {
|
||||
try self.expect(.comma);
|
||||
if (self.current.tag == .r_paren) break; // trailing comma ok
|
||||
}
|
||||
// Spread element: `.(..xs)` / `.(a, ..xs, b)` — reuses `spread_expr`,
|
||||
// whose operand carries any `xs.field` projection.
|
||||
if (self.current.tag == .dot_dot) {
|
||||
const sp_start = self.current.loc.start;
|
||||
self.advance(); // skip '..'
|
||||
const operand = try self.parseExpr();
|
||||
const spread = try self.createNode(spread_start, .{ .spread_expr = .{ .operand = operand } });
|
||||
const spread = try self.createNode(sp_start, .{ .spread_expr = .{ .operand = operand } });
|
||||
try elements.append(self.allocator, .{ .name = null, .value = spread });
|
||||
continue;
|
||||
}
|
||||
// Named field: `name = value` (uses `=`, distinct from the inline
|
||||
// named-tuple value form which uses `:`). The name is stored on the
|
||||
// TupleElement exactly like the `:` path.
|
||||
if (self.isIdentLike() and self.peekNext() == .equal) {
|
||||
const fname = self.tokenSlice(self.current);
|
||||
self.advance(); // skip name
|
||||
self.advance(); // skip '='
|
||||
const value = try self.parseExpr();
|
||||
try elements.append(self.allocator, .{ .name = fname, .value = value });
|
||||
continue;
|
||||
}
|
||||
const value = try self.parseExpr();
|
||||
try elements.append(self.allocator, .{ .name = null, .value = value });
|
||||
}
|
||||
@@ -3751,7 +3958,7 @@ pub const Parser = struct {
|
||||
var return_type: ?*Node = null;
|
||||
if (self.current.tag == .arrow) {
|
||||
self.advance();
|
||||
return_type = try self.parseTypeExpr();
|
||||
return_type = try self.parseFnReturnType();
|
||||
}
|
||||
|
||||
// Optional ABI annotation: abi(.c) / abi(.zig) / abi(.naked)
|
||||
@@ -4569,8 +4776,8 @@ test "parse comptime type-pack is NOT a protocol pack (..$args)" {
|
||||
// type-application); closure-sig packs use ClosureTypeExpr.pack_name +
|
||||
// pack_projection. Arrow bodies wrap the expression in a block.
|
||||
|
||||
test "parse pack expansion: tuple value (..xs)" {
|
||||
const source = "f :: () => (..xs);";
|
||||
test "parse pack expansion: tuple value .(..xs)" {
|
||||
const source = "f :: () => .(..xs);";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), source);
|
||||
@@ -4585,8 +4792,8 @@ test "parse pack expansion: tuple value (..xs)" {
|
||||
try std.testing.expectEqualStrings("xs", el.data.spread_expr.operand.data.identifier.name);
|
||||
}
|
||||
|
||||
test "parse pack expansion: tuple value projection (..xs.value)" {
|
||||
const source = "f :: () => (..xs.value);";
|
||||
test "parse pack expansion: tuple value projection .(..xs.value)" {
|
||||
const source = "f :: () => .(..xs.value);";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), source);
|
||||
@@ -4601,8 +4808,8 @@ test "parse pack expansion: tuple value projection (..xs.value)" {
|
||||
try std.testing.expectEqualStrings("xs", op.data.field_access.object.data.identifier.name);
|
||||
}
|
||||
|
||||
test "parse pack expansion: tuple type (..F(Ts))" {
|
||||
const source = "g :: (x: (..F(Ts))) => x;";
|
||||
test "parse pack expansion: tuple type Tuple(..F(Ts))" {
|
||||
const source = "g :: (x: Tuple(..F(Ts))) => x;";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), source);
|
||||
@@ -4707,8 +4914,8 @@ test "parse bare failable return: named `!Foo`" {
|
||||
try std.testing.expectEqualStrings("ParseErr", rt.data.error_type_expr.name.?);
|
||||
}
|
||||
|
||||
test "parse multi-return with inferred `!` as trailing element" {
|
||||
const source = "f :: () -> (i32, !) { 0; }";
|
||||
test "parse failable with inferred `!` (new `-> T !` form)" {
|
||||
const source = "f :: () -> i32 ! { 0; }";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), source);
|
||||
@@ -4723,8 +4930,8 @@ test "parse multi-return with inferred `!` as trailing element" {
|
||||
try std.testing.expect(fields[1].data.error_type_expr.name == null);
|
||||
}
|
||||
|
||||
test "parse multi-return with named `!Foo` as trailing element" {
|
||||
const source = "f :: () -> (i32, i64, !ParseErr) { 0; }";
|
||||
test "parse failable with named `!Foo` (new `-> Tuple(...) !` form)" {
|
||||
const source = "f :: () -> Tuple(i32, i64) !ParseErr { 0; }";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), source);
|
||||
@@ -4737,7 +4944,7 @@ test "parse multi-return with named `!Foo` as trailing element" {
|
||||
try std.testing.expectEqualStrings("ParseErr", fields[2].data.error_type_expr.name.?);
|
||||
}
|
||||
|
||||
test "parse error type rejected when not the trailing result element" {
|
||||
test "parse old bare-paren failable `-> (!, i32)` is rejected" {
|
||||
const source = "f :: () -> (!, i32) { 0; }";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
@@ -4745,7 +4952,7 @@ test "parse error type rejected when not the trailing result element" {
|
||||
try std.testing.expectError(error.ParseError, parser.parse());
|
||||
}
|
||||
|
||||
test "parse error type rejected in the middle of a result list" {
|
||||
test "parse old bare-paren failable `-> (i32, !, i64)` is rejected" {
|
||||
const source = "f :: () -> (i32, !, i64) { 0; }";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
@@ -4764,8 +4971,11 @@ test "round-trip print: error-set decl" {
|
||||
try std.testing.expectEqualStrings(source, aw.writer.toArrayList().items);
|
||||
}
|
||||
|
||||
test "round-trip print: multi-return result list with pointer + named error" {
|
||||
const source = "open :: () -> (*Handle, !IoErr) { 0; }";
|
||||
test "print: failable result list with pointer + named error folds to tuple repr" {
|
||||
// New `-> T !` form: a single value + named error channel folds to the SAME
|
||||
// internal `tuple_type_expr` the old `(*Handle, !IoErr)` spelling produced,
|
||||
// so printType still renders the canonical tuple representation.
|
||||
const source = "open :: () -> *Handle !IoErr { 0; }";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), source);
|
||||
@@ -5115,7 +5325,7 @@ test "E0.3 or value-terminator: parse(s) or 0" {
|
||||
|
||||
test "E0.3 full failable function parses end-to-end (all E0 forms)" {
|
||||
const source =
|
||||
\\parse :: (s: string) -> (i32, !ParseErr) {
|
||||
\\parse :: (s: string) -> i32 !ParseErr {
|
||||
\\ onfail (e) { cleanup(s); }
|
||||
\\ v := try inner(s) or 0;
|
||||
\\ w := other(s) catch (e2) { return 0; };
|
||||
|
||||
Reference in New Issue
Block a user