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:
agra
2026-06-25 17:53:57 +03:00
parent c882c6c63e
commit 989e18b760
124 changed files with 941 additions and 1236 deletions

View File

@@ -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; };