Skip to content

G#: named tuple elements — Phases A+B: front end + metadata interop (ADR-0172, #3501) - #3622

Merged
DavidObando merged 5 commits into
mainfrom
feature/named-tuple-elements
Aug 29, 2026
Merged

G#: named tuple elements — Phases A+B: front end + metadata interop (ADR-0172, #3501)#3622
DavidObando merged 5 commits into
mainfrom
feature/named-tuple-elements

Conversation

@DavidObando

@DavidObando DavidObando commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Summary

Stacked on #3621 (tuple equality) — Part 2 of the tuple-gap plan, Phases A and B of ADR-0172. Reverses ADR-0115 §B.4/T1's positional-only premise (amendment note added): G# gains named tuple elements, the readability keystone for #3501's self-migration corpus (~830 named-tuple declaration lines across ~300 files currently degrade to .ItemN soup in translated output).

Phase A — language surface & semantics

  • Types, name-first like parameters: (line int32, column int32), partial naming allowed. An identifier is a name exactly when followed by a type-clause start token; the [ case distinguishes name []T / name [3]T / name [,]T from generic List[int32], and unmanaged (…) -> R keeps its ADR-0095 function-pointer meaning. The Nullable channel type spelling is unparseable — the grammar's outer '?' on 'chan T' is unreachable #3315 grouping rule survives: no 1-tuples; a name on a single parenthesized element is GS0543 with grouping recovery.
  • Literals, colon labels: (line: 1, column: 2) (new NamedTupleElementSyntax). No C# 7.1 name inference.
  • Access: pos.line resolves positionally; ItemN and .N stay valid; emit stays positional.
  • Names are metadata (C#/Roslyn model): TupleTypeSymbol interns per (element types, names); named and unnamed same-shape tuples are distinct symbols sharing a CLR backing, related by an identity conversion (WithoutNames() recursion). Name disagreement warns GS0541; duplicates GS0540; ItemN off-position and Rest reserved (GS0542). Names propagate through generic substitution and common-type joins. Equality (ADR-0171) ignores names — tested. Diagnostics/hover render name-first.

Phase B — CLR metadata interop

  • Emit: [TupleElementNamesAttribute(string[])] (C# flattened pre-order encoding, null at unnamed slots, omitted when fully unnamed) on tuple-typed returns (sequence-0 Param row minted when needed), parameters, fields, and properties — via the same CustomAttributeEncoder hook points as the NullableAttribute machinery. Arity ≥ 8 encodes logical elements (TRest invisible); async Task<(a,b)> returns use the tuple's own array.
  • Import: TupleElementNamesReader decodes the attribute at the ClrNullability member-type entry points, rebuilding tuple occurrences in the mapped symbol (imported ValueTuple generic args flatten to TupleTypeSymbol; closed generics rebuild as constructed imported symbols). NullabilityAnnotatedTypeSymbol transfers names from its symbolic base, and named-tuple args count as substitutable — so list[i].line works through generic receivers.
  • Interop verified both directions: C#-authored named tuples surface in G# (direct returns, List<> elements, labeled args, arity-9), and csc/Roslyn decodes gsc's blob (a C# consumer binds pos.line against a G# assembly) — the strongest encoding-parity witness.
  • Bonus fix: the speculative generic-call-site scanner now accepts named tuples in type-argument position (List[(line int32, column int32)]()).

Remaining (per ADR-0172)

Phase C: cs2gs name preservation + corpus/selfmig re-baseline (the .Item2.price readability win). Phase D: LS completion polish.

Testing

  • NamedTupleElementTests (19, binder): name/ItemN/.N coexistence, labeled-literal inference, partial naming, cross-name assignment + GS0541 witness, named returns, nesting, named-vs-unnamed equality, generic substitution, GS0540/0542/0543 with recovery witnesses, display, unnamed-grammar guard.
  • NamedTupleMetadataEmitTests (6, emit+ILVerify): attribute values on return/param/field/property, omitted-when-unnamed, nested-generic flattening, arity-9, full G#→G# cross-assembly round trip incl. List element access by name.
  • Conformance sample samples/NamedTupleElements.gs + golden; full sample harness green (128/128, three hosts).
  • Full Core.Tests green (8214); emit-side regression filters green (701); coverage-matrix + PE-baseline goldens regenerated (additions only, zero existing-sample IL drift).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Nng28yiBdPVdeML7mSZphs

@DavidObando DavidObando changed the title G#: named tuple elements — Phase A front end (ADR-0172, #3501) G#: named tuple elements — Phases A+B: front end + metadata interop (ADR-0172, #3501) Aug 28, 2026
Base automatically changed from feature/tuple-equality to main August 29, 2026 00:02
DavidObando and others added 3 commits August 28, 2026 17:04
Tuple types may name elements name-first — `(line int32, column int32)`
— matching the parameter form, and tuple literals may label elements —
`(line: 1, column: 2)`. A declared name resolves member access to its
position (`pos.line` ≡ `pos.Item1`; `ItemN` and `.N` stay valid).

Names are metadata over the positional shape, the C#/Roslyn model: the
interned TupleTypeSymbol is keyed on (element types, names), a named
and an unnamed same-shape tuple are distinct symbols sharing a CLR
backing and related by an identity conversion (`WithoutNames()` gives
the canonical shape, recursively), a position-wise name disagreement
warns GS0541, equality (ADR-0171) ignores names, generic substitution
preserves them, and common-type joins keep agreeing names. Declaration
checks: GS0540 duplicate name, GS0542 reserved (`ItemN` off-position,
`Rest`), GS0543 name on a parenthesized single element (grouping per
#3315, no 1-tuples). Parser ambiguity: an identifier is a name exactly
when followed by a type-clause start; the `[` case separates `name []T`
/ `name [3]T` / `name [,]T` from generic `List[int32]`, and the
`unmanaged` function-pointer head keeps its ADR-0095 meaning.

This reverses ADR-0115 §B.4/T1's positional-only premise (amendment
note added). Phases B (TupleElementNamesAttribute emit/import), C
(cs2gs name preservation + re-baseline), and D (LS polish) follow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nng28yiBdPVdeML7mSZphs
Emit: gsc synthesizes [TupleElementNamesAttribute(string[])] — the C#
flattened DFS pre-order encoding, null at unnamed positions, omitted
when fully unnamed — on tuple-typed returns (sequence-0 Param row is
now also minted for a named-tuple return), parameters, fields, and
properties, via the same CustomAttributeEncoder hook points the
NullableAttribute machinery uses. Arity ≥ 8 contributes logical
elements only (TRest invisible). An async kickoff's Task<(a, b)>
return uses the tuple's own array — wrappers contribute no entries.

Import: TupleElementNamesReader decodes the attribute from referenced
assemblies at the four ClrNullability member-type entry points and
applies names by rebuilding each tuple occurrence in the mapped symbol
(imported ValueTuple generic arguments are flattened to TupleTypeSymbol
first; closed generics rebuild as constructed imported symbols so
receiver projection sees the names). NullabilityAnnotatedTypeSymbol's
argument derivation transfers names from its symbolic base, and a
named-tuple type argument now counts as substitutable, so
`list[i].line` works through generic receivers.

Both interop directions verified: C#-compiled named tuples surface in
G# (incl. List elements and arity-9), and csc/Roslyn decodes gsc's
blob (a C# consumer binds `pos.line` against a G# assembly) — the
strongest encoding-parity witness.

Also: the speculative generic-call-site scanner accepts named tuple
elements in type-argument position (`List[(line int32, column int32)]()`
previously fell back to an expression parse).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nng28yiBdPVdeML7mSZphs
@DavidObando
DavidObando force-pushed the feature/named-tuple-elements branch from 77ea62e to a806e04 Compare August 29, 2026 00:06
…ases C+D, #3501) (#3623)

## Summary

**Stacked on #3622** (named tuple elements, Phases A+B) — Phase C of
ADR-0172: cs2gs stops erasing C# tuple element names, delivering the
#3501 readability win across the ~830 named-tuple declaration lines in
the self-migration corpus.

- **Types** print name-first: `(int Line, int Column)` → `(Line int32,
Column int32)` (`CSharpTypeMapper` keeps Roslyn's `TupleElements` names;
a default `ItemN`-at-position counts as unnamed; the ADR-0115 §B.4
name-drop Info diagnostic is retired). Names survive the
nullability-promotion rebuild sites too (oblivious promotion, whole-type
nullable wraps).
- **Literal labels** survive: `(Count: 3, Name: "three")` via
`NameColon`.
- **Named element access stays by-name**: `item.Price * item.Quantity`
round-trips verbatim where it previously lowered to `item.Item2 *
item.Item3`; default positional `.ItemN` access still normalizes
positionally.
- `TupleTypeReference` / `TupleLiteralExpression` gain optional
element-name lists; the printer renders both forms.

### Verification

- New `Adr0172NamedTupleTranslationTests` (6): name-first types, by-name
access, positional access unchanged, literal labels, unnamed tuples
unchanged, names inside generics — every snippet re-binds through the
real gsc (exercising the #3622 front end).
- Existing tuple-behavior tests updated to the named surface (Issue1914
alias, Issue2469 nullability ×6, Issue3615 receiver probe,
oblivious-promotion sinks); coverage-matrix notes moved into the
inventory and `docs/cs2gs-coverage-matrix.md` regenerated via `cs2gs
coverage --write`.
- **Full Cs2Gs.Tests green** (2454 tests; the two last local failures
were a stale `out/bin/Release` gsc found by `FindCompiler` — rebuilt —
and a stale expectation).
- **Corpus diagnostic-run 18/20 green** with the branch gsc
(CompileGap-Library is the known wontfix; L3-Library's test-parity
failure is the documented stale-SDK-nupkg trap — the parity stage
rebuilds with the packaged gsc, which predates ADR-0172; CI builds the
SDK from the same commit). Translated L1 now reads `total += item.Price
* item.Quantity` with `List[(Name string, Price int32, Quantity
int32)]`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Nng28yiBdPVdeML7mSZphs

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…test reconciliation (ADR-0172, #3501)

gsc: an imported generic method call (LINQ FirstOrDefault et al.) whose
inference binds a type parameter to a named-tuple-bearing argument now
keeps the names on the RETURN type — `tools.FirstOrDefault(...)`.Name
previously died with GS0158 because two gates only kept the symbolic
projection when RequiresSymbolicProjection held, and a named tuple
shares its CLR backing with the unnamed shape. New
TypeSymbol.ContainsNamedTupleElements extends the anySymbolic gate in
BuildSymbolicMethodTypeArgs and the keep-projection gate in
ResolveCallReturnTypeFromSymbolicTypeArgs.

Core: TransferTupleNames rewritten from a tuple-pattern switch to plain
if/is chains — the switch shape self-migrates through the pattern
lowering's `as …!!` family, tripping the #3347/#3422 zero-noise
inventories (5 new asserted-as conversions, 5 new bangs).

cs2gs tests: reconcile the remaining positional-era expectations with
the Phase C named surface (Issue1839's NotEmpty rode on the retired
§B.4 Info diagnostic — now asserts the rendered pattern directly;
Issue2421/2490×8/2504/2579/2821, L1 declaration + e2e — the L1 ctor
line stays unnamed because the C# source spells the unnamed shape).

Local repro discipline note: the earlier "full suite green" reads were
truncated by a `| tail` pipe; suites now log to files and the stale
out/bin/Release gsc that tests' FindCompiler prefers was rebuilt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nng28yiBdPVdeML7mSZphs
@DavidObando
DavidObando merged commit bf14b47 into main Aug 29, 2026
35 of 36 checks passed
@DavidObando
DavidObando deleted the feature/named-tuple-elements branch August 29, 2026 02:08
DavidObando added a commit that referenced this pull request Aug 29, 2026
) (#3625)

## Summary

Follow-up to #3622: the code-exploder gate's `GS0158: Cannot find member
AnalysisId` family. An imported awaitable whose signature carries
reference-nullability metadata arrives wrapped in
`NullabilityAnnotatedTypeSymbol`, hiding the symbolic constructed `Task`
from `TryGetTaskElementType`'s #2195 fast path — the CLR fallback
rebuilt the element from the closed `ValueTuple` and erased its names,
so `(await store.RetryAsync(id)).AnalysisId` failed to bind. Fix: unwrap
to the symbolic base exactly when a type argument carries named-tuple
content (`ContainsNamedTupleElements` gate, matching the #3622
projection-gate family); the base's arguments already carry their
element nullability.

Verified on the pinned code-exploder `Task<(Guid AnalysisId, string?
GitRef)?>` shape reduced to a minimal cross-assembly repro (compiles +
runs), plus a new round-trip test covering `await` element access and an
if-let unwrap of an awaited nullable named tuple. Async/await suites
green (Compiler.Tests 326, Core.Tests 453).

**Known follow-up (pre-existing, reproduced without this change):** an
async-function `if let` over a *non-awaited* imported call returning
`Nullable<ValueTuple<…>>`-with-names emits invalid IL — ILVerify
`StackUnexpected: found Nullobjref, expected
Nullable<ValueTuple<int32,string>>` in `MoveNext` — filing separately.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Nng28yiBdPVdeML7mSZphs

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
DavidObando added a commit that referenced this pull request Aug 29, 2026
…fmig nightly regression) (#3631)

Fixes the systemic failure in the 2026-08-29 selfmig nightly ([run
33234797980](https://github.com/DavidObando/gsharp/actions/runs/33234797980)):
18 apps shared the same two compile fingerprints (`GS0159 Cannot find
function GetGenericArguments` / `GS0125 Variable 'closedClr' doesn't
exist`), all cascading from migrated `src/Core` failing to compile —
which dropped the gate to 28/52 green against floor 33.

## Root cause

`TranslateRecursivePattern`'s untyped branch returned `new
PropertyPattern(fields)` without ever consulting
`recursive.Designation`. A designation on a **nested** untyped property
pattern was silently dropped while the body still referenced the binder:

```csharp
// TupleElementNamesReader.cs:202 (added by #3622) — the trigger
case { ClrType: { IsGenericType: true, IsGenericTypeDefinition: false } closedClr }:
    ... closedClr.GetGenericArguments() ...
```

translated to a pattern without `closedClr`, orphaning the body
references.

## Fix

G# natively supports the after-brace designator (ADR-0166,
`PropertyPattern identifier?` in the spec grammar), and the code model +
printer already carry a `Designator` — the translator now populates it,
with reassigned-binder capture mirroring the existing var-pattern path,
and an explicit unsupported report for tuple designations. Verified that
gsc compiles and runs the nested-designation form correctly (narrowed
non-nil binding, parity with C# semantics).

## Tests

`NestedPatternDesignationTranslationTests` — switch statement (with
compile+run parity witness: generic vs nil holder), boolean
`is`-pattern, and switch expression. Full pattern-related Cs2Gs.Tests
family green (188/188).

Note: this is independent of #3630; the C# trigger shape came from #3622
(merged), so the regression exists on main regardless of the
variadic-carriers branch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Nng28yiBdPVdeML7mSZphs

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
DavidObando added a commit that referenced this pull request Aug 29, 2026
…jection gate (ADR-0172) (#3632)

Fixes the remaining systemic failure in the second 2026-08-29 selfmig
nightly ([run
33260247720](https://github.com/DavidObando/gsharp/actions/runs/33260247720),
which already included #3630/#3631): 32/52 green against floor 33, with
migrated **Cs2Gs.Translator** failing `GS0158 Cannot find member Symbol`
/ `GS0159 Cannot find function Contains` / `GS0116 not indexable` at
`CSharpToGSharpTranslator.Constructors.gs:1537-38` —
`candidate[0].Symbol` and `reach[head].Contains(symbol)` over
`List[List[(syntax …, symbol …)]]` — cascading into 11 downstream apps.

## Root cause

A named tuple nested **inside** a generic type argument shares its CLR
backing with the unnamed shape (`ValueTuple<…>`), so any keep-symbolic
gate that consults only `RequiresSymbolicProjection` (or a direct `is
TupleTypeSymbol`) collapses the constructed type to its erased CLR form
— and the element names vanish, so member access by name fails. The
#3622 projection-gate family covered *direct* named-tuple arguments
(`List[(a, b)]`, one level), but not `List[List[(a, b)]]`, `List[[](a,
b)]`, or `Dictionary[K, (a, b)]` reached through construction, indexing,
or iteration. Minimal repro (failed before, runs now):

```gs
let groups = List[List[(a int32, b string)]]()
…
groups[0][0].a          // was GS0158: Cannot find member a
```

## Fix

Widened six keep-symbolic gates with
`TypeSymbol.ContainsNamedTupleElements` (already recursive through every
constructor shape): the generic ctor-call type-argument resolution (the
erasure at the root), the constructed-generic receiver form, the indexer
element substitution + its #2365 recursive projection filter, the
generic-return construction and indexer property projection in
MemberLookup, the conversion-classifier parameter substitution, and the
type-clause `ProjectGenericArgument`. Deliberately did **not** widen
`ImportedTypeSymbol.HasSubstitutableTypeArgument` itself — it feeds
conversion/lowering decisions with broader meaning, and widening it
regressed working cases during investigation.

## Verification

- New `NestedNamedTupleProjectionTests` (4 end-to-end oracle tests) pin
all shapes, including the exact selfmig iteration+index+member chain
with an imported interface element type.
- Full Core.Tests: **8218/8218 passed**.
- Local selfmig proof over the cs2gs subtree: Cs2Gs.Translator's three
real compile errors are gone (the only remaining local failures are the
known subtree-proof artifact — `GSharpRoundTrip.gs` referencing
GSharp.Core, which the subtree run excludes from migration; the nightly
migrates it).

With this plus #3630/#3631, the next nightly should clear the
`b7a89d8f22f8`/`0a665d0d062c`/`ebe7e1f2f6f8` fingerprint family. The
`!!` ceiling breach (17516 vs 17400) is a separate ratchet question.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Nng28yiBdPVdeML7mSZphs

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant