Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,4 @@ package-lock.json

# cs2gs migration run output (generated, do not commit)
cs2gs-runs/
test/Core.Tests/Baselines/*.actual
2 changes: 1 addition & 1 deletion docs/adr/0115-csharp-to-gsharp-migration-tool.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Since issue #948, the inline field initializers the translator emits here — `p

**T4 — fieldless record → plain (`open`) `class`/`struct`.** A G# `data` type requires **at least one field** (`GS0104`, "a data type requires at least one field"). A C# **fieldless record** — typically the `abstract record Shape;` base of a closed `record` hierarchy — therefore maps to a plain `class` (or `struct`), **not** a `data class`; it is marked `open` when any case derives from it (§B.6). Two further losses are made faithfully: G# has **no `abstract` class modifier** (the keyword is not recognized by the parser; `abstract class` → `GS0125`), so C# `abstract` is **dropped** (the `open class` is subclassable but not non-instantiable); and the record-synthesized `IEquatable<Self>` interface is **dropped from the base list** because a fieldless record maps to a plain `class` that has no synthesized `Equals`, so emitting `: IEquatable[Shape]` would leave the interface unimplemented (`GS0187`). Naming the enclosing type as a base-clause type *argument* is itself legal since issue #949 (`open class Shape : IEquatable[Shape]` now compiles); the drop is a semantic-redundancy filter, not the former `GS0113` syntax limitation. Each loss is recorded as an Info diagnostic. The case records (`sealed record Circle(double Radius) : Shape`) keep the `data class Circle(Radius float64) : Shape` mapping.

**T1 — C# tuples → native G# positional tuples.** A C# value/named tuple (`(string Name, int Price, int Quantity)`) maps to the **native G# positional tuple type** `(string, int32, int32)` (spec §Type syntax), *not* to a synthesized `data struct`. G# tuples are **positional only** — the named-element spelling `(Name string, …)` does not parse — so C# element **names are dropped** at the type, and a named-element **access** `item.Price` lowers to the positional field `item.Item2` (resolved via Roslyn's `IFieldSymbol.CorrespondingTupleField`); positional `item.Item1` passes through. Tuple **construction** `(a, b, c)` maps to the G# tuple literal `(a, b, c)`. The mapping is recorded as an Info diagnostic. This was chosen over synthesizing a `data struct` per tuple shape because a `data struct` element type triggers a real compiler gap (below) and because native tuples are the genuinely canonical, round-trippable G# form.
**T1 — C# tuples → native G# positional tuples.** *Amended by ADR-0172 (2026-08-28): G# now supports named tuple elements (`(name string, price int32)` types, `(name: e)` literal labels), so the name-dropping described below is superseded — cs2gs preserves element names once its ADR-0172 Phase C lands. The remainder of this section records the original positional-only mapping.* A C# value/named tuple (`(string Name, int Price, int Quantity)`) maps to the **native G# positional tuple type** `(string, int32, int32)` (spec §Type syntax), *not* to a synthesized `data struct`. G# tuples were **positional only** — the named-element spelling `(Name string, …)` did not parse — so C# element **names were dropped** at the type, and a named-element **access** `item.Price` lowered to the positional field `item.Item2` (resolved via Roslyn's `IFieldSymbol.CorrespondingTupleField`); positional `item.Item1` passes through. Tuple **construction** `(a, b, c)` maps to the G# tuple literal `(a, b, c)`. The mapping is recorded as an Info diagnostic. This was chosen over synthesizing a `data struct` per tuple shape because a `data struct` element type triggers a real compiler gap (below) and because native tuples are the genuinely canonical, round-trippable G# form.

> **`for … in List[ownedType]` element-type erasure — discovered compiler gap.**
> Iterating a `List[T]` whose element `T` is a **user type owned by the same
Expand Down
99 changes: 99 additions & 0 deletions docs/adr/0172-named-tuple-elements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# ADR-0172: Named tuple elements

- **Status**: Accepted
- **Date**: 2026-08-28
- **Related**: issue #3501 (self-migration readability), ADR-0115 §B.4/T1 (amended by this ADR), ADR-0171 (tuple equality), ADR-0029 (data-struct members), C# §8.3.11 / `TupleElementNamesAttribute`.

## Context

G# tuples were purely positional — ADR-0115 §B.4 stated "the named-element
spelling does not parse" as a premise, and cs2gs accordingly dropped C# element
names: type `(int Line, int Column)` mapped to `(int32, int32)`, access
`t.Line` lowered to `t.Item1`, literal labels were discarded. The #3501
self-migration corpus contains roughly 830 named-tuple declaration lines
across ~300 files, so translated output was full of `item.Item2 * item.Item3`
where the C# said `item.Price * item.Quantity` — contrary to #3501's "fully
readable, maintainable" definition of done, and invisible to the selfmig
ratchet. The loss also crossed the CLR boundary in both directions: gsc
neither emitted nor imported `TupleElementNamesAttribute`.

## Decision

G# gains named tuple elements. **This reverses the ADR-0115 §B.4/T1
positional-only premise**; an amendment note there points here.

### Syntax

- **Tuple types** name elements name-first, matching the parameter form
`identifier TypeClause`:

```gs
let pos (line int32, column int32) = (3, 5)
func divmod(a int32, b int32) (quotient int32, remainder int32) { … }
```

Partial naming is allowed (`(count int32, string)`). An identifier is an
element NAME exactly when it is followed by a token that can start a type
clause; the `[` case distinguishes `name []T` / `name [3]T` / `name [,]T`
(array shapes open with `]`, a number, or a rank comma) from a generic
argument list `List[int32]`, and the `unmanaged` function-pointer head
keeps its ADR-0095 meaning. The #3315 grouping rule is preserved: there
are no 1-tuples, and a name on a parenthesized single element is error
**GS0543**, recovered as grouping.

- **Tuple literals** label elements with a colon, matching the
argument-label style:

```gs
let t = (line: 7, column: 9) // infers (line int32, column int32)
```

Labels are optional per element. A lone labeled element `(x: 1)` is
GS0543, recovered as a plain parenthesized expression. C# 7.1-style name
inference from expressions is deliberately not adopted.

### Semantics: names are metadata

- The interned `TupleTypeSymbol` is keyed on (element types, element names):
a named and an unnamed same-shape tuple are **distinct symbol instances
sharing the same CLR backing**, related by an **identity conversion**
(`WithoutNames()` computes the canonical unnamed shape, recursively).
Assignment, argument passing, and returns cross name boundaries freely;
a position where both sides declare *different* names warns **GS0541**
(the C# CS8123 analog).
- Member access resolves a declared name to its position; `ItemN` and the
numeric `.N` selectors remain valid on named tuples. Emit is unchanged —
access lowers to the positional `ItemN` field either way.
- Declaration checks: duplicate name = **GS0540**; `ItemN` at any position
other than N and `Rest` = **GS0542** (correct-position `ItemN` is
allowed, as in C#).
- Names propagate through generic substitution and merge across
common-type joins by the C# rule (keep agreeing names, drop the rest).
- **Equality (ADR-0171) ignores names** — the desugar compares shape, never
symbol identity.
- Positional deconstruction, patterns, and `for (a, b) in` are unchanged.

### Phasing

- **Phase A (this change)**: parser, symbol model, member lookup,
conversions/identity, diagnostics GS0540–GS0543, display.
- **Phase B**: metadata interop — emit `TupleElementNamesAttribute` on
tuple-typed parameters/returns/fields/properties (C# flattened pre-order
encoding) and decode it on import, so C#-authored named tuples surface
their names in G# and vice versa.
- **Phase C**: cs2gs preserves names end-to-end (type mapping, printer,
member access, literal labels) + corpus/selfmig re-baseline.
- **Phase D**: language-server polish (hover, element-name completion).

## Alternatives rejected

- **Names on references, not symbols** — G# has no annotation channel on
`BoundExpression` types; every consumer sees a bare `TypeSymbol`. The
wrapper-symbol precedent (`NullableTypeSymbol`) confirms symbols are the
annotation channel.
- **Nominal named tuples** (names part of type identity) — breaks C#
interop semantics and every existing positional conversion.

## Future work (not planned)

Deconstruction-by-name, named positional patterns, C# 7.1 name inference.
1 change: 1 addition & 0 deletions docs/coverage-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ NameOfExpression
NamedArgumentExpression
NamedDeconstructionField
NamedDeconstructionStatement
NamedTupleElement
NilKeyword
NotPattern
NullCoalescingAssignmentStatement
Expand Down
10 changes: 5 additions & 5 deletions docs/cs2gs-coverage-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Drift fails `ConstructInventoryGoldenTests`. Do not edit by hand.
| EmptyStatement | EmptyStatementSyntax | ADR-0115 §B | | tools/cs2gs/corpus/grid/G03-ControlFlow-Console/Constructs/EmptyStatement.cs | | |
| EnumDeclaration | EnumDeclarationSyntax | ADR-0115 §B.11 | | tools/cs2gs/corpus/grid/G06-Types-Console/Constructs/EnumDeclaration.cs | | Member values (explicit or implicit) and [Flags] are preserved (issue #1912, fixed). |
| EnumMemberDeclaration | EnumMemberDeclarationSyntax | ADR-0115 §B.11 | | tools/cs2gs/corpus/grid/G06-Types-Console/Constructs/EnumMemberDeclaration.cs | | Explicit/negative/[Flags] bit-shift-or/alias values resolve via the semantic model's IFieldSymbol.ConstantValue and are emitted as an explicit G# `= value` (new language feature, issue #1912, fixed). |
| EqualsExpression | BinaryExpressionSyntax | ADR-0115 §B | | tools/cs2gs/corpus/grid/G02-Operators-Console/Constructs/EqualsExpression.cs | | Tuple operands compare element-wise via gsc tuple equality (ADR-0171, issue #3501); C# element names are ignored, matching the positional G# lowering. |
| EqualsExpression | BinaryExpressionSyntax | ADR-0115 §B | | tools/cs2gs/corpus/grid/G02-Operators-Console/Constructs/EqualsExpression.cs | | Tuple operands compare element-wise via gsc tuple equality (ADR-0171, issue #3501); element names never affect equality. |
| EqualsValueClause | EqualsValueClauseSyntax | ADR-0115 §B.3 | | | | |
| EventDeclaration | EventDeclarationSyntax | ADR-0052 | | tools/cs2gs/corpus/grid/G07-Members-Console/Constructs/EventDeclaration.cs | | Explicit add/remove accessor event maps to the G# event declaration's explicit-accessor form (ADR-0052 §2); a source-declared named delegate handler type keeps its name (issue #1960 item 3) instead of the anonymous arrow form. |
| EventFieldDeclaration | EventFieldDeclarationSyntax | ADR-0052 | | tools/cs2gs/corpus/grid/G07-Members-Console/Constructs/EventFieldDeclaration.cs | | Field-like event maps to the G# field-like event declaration (ADR-0052 §2); a source-declared named delegate handler type keeps its name (issue #1960 item 3) instead of the anonymous arrow form. |
Expand Down Expand Up @@ -159,7 +159,7 @@ Drift fails `ConstructInventoryGoldenTests`. Do not edit by hand.
| NameColon | NameColonSyntax | ADR-0115 §B | | tools/cs2gs/corpus/grid/G09-Functions-Console/Constructs/NameColon.cs | | Named arguments use the G# name: value form (ADR-0080). |
| NameEquals | NameEqualsSyntax | ADR-0115 §B.16 | | | | |
| NamespaceDeclaration | NamespaceDeclarationSyntax | ADR-0115 §B.1 | | | | |
| NotEqualsExpression | BinaryExpressionSyntax | ADR-0115 §B | | tools/cs2gs/corpus/grid/G02-Operators-Console/Constructs/NotEqualsExpression.cs | | Tuple operands compare element-wise via gsc tuple equality (ADR-0171, issue #3501); C# element names are ignored, matching the positional G# lowering. |
| NotEqualsExpression | BinaryExpressionSyntax | ADR-0115 §B | | tools/cs2gs/corpus/grid/G02-Operators-Console/Constructs/NotEqualsExpression.cs | | Tuple operands compare element-wise via gsc tuple equality (ADR-0171, issue #3501); element names never affect equality. |
| NotPattern | UnaryPatternSyntax | ADR-0115 §B.22 | | tools/cs2gs/corpus/grid/G04-Patterns-Console/Constructs/NotPattern.cs | | |
| NullLiteralExpression | LiteralExpressionSyntax | ADR-0115 §B | | tools/cs2gs/corpus/grid/G01-Literals-Console/Constructs/NullLiteralExpression.cs | | |
| NullableType | NullableTypeSyntax | ADR-0115 §B.12 | | | | T? maps to G# nullable spelling. |
Expand Down Expand Up @@ -226,9 +226,9 @@ Drift fails `ConstructInventoryGoldenTests`. Do not edit by hand.
| ThrowStatement | ThrowStatementSyntax | ADR-0115 §B.27 | | tools/cs2gs/corpus/grid/G03-ControlFlow-Console/Constructs/ThrowStatement.cs | | |
| TrueLiteralExpression | LiteralExpressionSyntax | ADR-0115 §B | | tools/cs2gs/corpus/grid/G01-Literals-Console/Constructs/TrueLiteralExpression.cs | | |
| TryStatement | TryStatementSyntax | ADR-0115 §B.27 | | tools/cs2gs/corpus/grid/G03-ControlFlow-Console/Constructs/TryStatement.cs | | |
| TupleElement | TupleElementSyntax | ADR-0115 §B.12 | | | | G# tuple types (T1, T2). |
| TupleExpression | TupleExpressionSyntax | ADR-0115 §B | | tools/cs2gs/corpus/grid/G05-Collections-Console/Constructs/TupleExpression.cs | | |
| TupleType | TupleTypeSyntax | ADR-0115 §B.12 | | | | G# tuple types (T1, T2). |
| TupleElement | TupleElementSyntax | ADR-0115 §B.12 | | | | G# tuple types; ADR-0172: C# element names are preserved name-first ((Line int32, Column int32)), and named access stays by-name. |
| TupleExpression | TupleExpressionSyntax | ADR-0115 §B | | tools/cs2gs/corpus/grid/G05-Collections-Console/Constructs/TupleExpression.cs | | ADR-0172: C# element labels ((Line: 1, …)) are preserved as G# labeled elements. |
| TupleType | TupleTypeSyntax | ADR-0115 §B.12 | | | | G# tuple types; ADR-0172: element names preserved (was: dropped with an Info diagnostic). |
| TypeArgumentList | TypeArgumentListSyntax | ADR-0115 §B.7 | | tools/cs2gs/corpus/grid/G08-Generics-Console/Constructs/TypeArgumentList.cs | | |
| TypeConstraint | TypeConstraintSyntax | ADR-0115 §B.7 | | tools/cs2gs/corpus/grid/G08-Generics-Console/Constructs/TypeConstraint.cs | | |
| TypeOfExpression | TypeOfExpressionSyntax | ADR-0115 §B | | tools/cs2gs/corpus/grid/G02-Operators-Console/Constructs/TypeOfExpression.cs | | |
Expand Down
Loading
Loading