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

@@ -950,6 +950,38 @@ pub const Lowering = struct {
}
}
}
// A `Tuple(...)` element must denote a TYPE; a VALUE-literal element —
// e.g. the `1` in `Tuple(i32, 1)` — is a user error. Diagnose it loudly
// here (the same message the `.( ... )`-in-type path emits) BEFORE
// `resolveCompound` would intern a tuple carrying an `.unresolved`
// field. Only the unambiguous value literals are rejected: an
// `error_type_expr` element (`-> Tuple(A, B) !` desugaring), names,
// and the structural type shapes are all legitimate tuple elements.
if (node.data == .tuple_type_expr) {
for (node.data.tuple_type_expr.field_types) |ft| {
// A signed numeric literal (`Tuple(i32, -1)`) arrives as a
// `negate` unary over an int/float literal — reject it as the
// literal it wraps, not as a generic non-type.
const probe = if (ft.data == .unary_op and ft.data.unary_op.op == .negate)
ft.data.unary_op.operand
else
ft;
switch (probe.data) {
.int_literal,
.float_literal,
.string_literal,
.bool_literal,
.null_literal,
=> {
if (self.diagnostics) |diags| {
diags.addFmt(.err, ft.span, "tuple type element is not a type (found `{s}`); a tuple used as a type must list only types, e.g. `Tuple(i32, i32)`", .{@tagName(probe.data)});
}
return .unresolved;
},
else => {},
}
}
}
// Structural type shapes — `*T`, `[*]T`, `[]T`, `?T`, `[N]T`, functions,
// PLAIN closures, and PLAIN tuples — are owned by
// `TypeResolver.resolveCompound` (A2.3b). Element types recurse through