feat: parenthesized type grouping — (T) groups, (T,) is a 1-tuple (issue 0177)

In type position, parentheses now mirror value position: (T) (a single
unnamed element, no trailing comma) is a GROUPING that resolves to the
inner type; (T,) is a 1-tuple; (A, B) a 2-tuple; named (x: T) and spread
(..Ts) stay tuples; (...) -> R stays a function type. This lets a
closure/optional/function type be parenthesized for readability without
silently becoming a 1-tuple:
  [1](Closure(i64,i64) -> i64)   // array of closures (issue 0177) -> 7
  ?(?i64)                        // genuine nested optional (issue 0165 intent)

Parser: src/parser.zig returns the inner node for a single unnamed
non-spread no-trailing-comma parenthesized type. formatTypeName (both
generic.zig diagnostics + types.zig reflection) now render a 1-tuple as
(T,) so the spelling is unambiguous and diagnostics are self-consistent.
The 0165 coerce/stmt note reworded accordingly.

specs.md §Type Syntax updated; basic/0036 wrap return -> (i64,); obsolete
diagnostic 1195 removed (?(?i64) now compiles); regression
examples/types/0201-types-parenthesized-type-grouping.sx added; 0414 .ir
golden regenerated for the (T,) rendering. Resolves 0177; updates
0165/0170. Verified by 3 adversarial reviews; suite 792/0.
This commit is contained in:
agra
2026-06-23 10:43:47 +03:00
parent c41f51aed3
commit 555ccdc024
18 changed files with 120 additions and 40 deletions

View File

@@ -552,10 +552,17 @@ pub const Parser = struct {
// An error channel type (`!` / `!Named`) is only valid as the
// trailing element of a result list. Reject any element after it.
var saw_error_type = false;
// Track an explicit trailing comma so a single-element `(T,)` stays a
// 1-tuple while `(T)` (no comma) is a GROUPING — see the grouping
// return below.
var had_trailing_comma = false;
while (self.current.tag != .r_paren and self.current.tag != .eof) {
if (param_types.items.len > 0) {
try self.expect(.comma);
if (self.current.tag == .r_paren) break; // trailing comma ok
if (self.current.tag == .r_paren) {
had_trailing_comma = true;
break; // trailing comma ok
}
}
if (saw_error_type) {
return self.fail("error type '!' must be the last element of a result list");
@@ -601,9 +608,21 @@ pub const Parser = struct {
.abi = abi,
} });
}
// No '->': tuple type (even for single element). 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.
// 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.
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;