G#: named tuple elements — Phases A+B: front end + metadata interop (ADR-0172, #3501) - #3622
Merged
Conversation
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
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nng28yiBdPVdeML7mSZphs
DavidObando
force-pushed
the
feature/named-tuple-elements
branch
from
August 29, 2026 00:06
77ea62e to
a806e04
Compare
…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>
DavidObando
enabled auto-merge (squash)
August 29, 2026 00:11
…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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
.ItemNsoup in translated output).Phase A — language surface & semantics
(line int32, column int32), partial naming allowed. An identifier is a name exactly when followed by a type-clause start token; the[case distinguishesname []T/name [3]T/name [,]Tfrom genericList[int32], andunmanaged (…) -> Rkeeps 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.(line: 1, column: 2)(newNamedTupleElementSyntax). No C# 7.1 name inference.pos.lineresolves positionally;ItemNand.Nstay valid; emit stays positional.TupleTypeSymbolinterns 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;ItemNoff-position andRestreserved (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
[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 sameCustomAttributeEncoderhook points as theNullableAttributemachinery. Arity ≥ 8 encodes logical elements (TRestinvisible); asyncTask<(a,b)>returns use the tuple's own array.TupleElementNamesReaderdecodes the attribute at theClrNullabilitymember-type entry points, rebuilding tuple occurrences in the mapped symbol (importedValueTuplegeneric args flatten toTupleTypeSymbol; closed generics rebuild as constructed imported symbols).NullabilityAnnotatedTypeSymboltransfers names from its symbolic base, and named-tuple args count as substitutable — solist[i].lineworks through generic receivers.List<>elements, labeled args, arity-9), and csc/Roslyn decodes gsc's blob (a C# consumer bindspos.lineagainst a G# assembly) — the strongest encoding-parity witness.List[(line int32, column int32)]()).Remaining (per ADR-0172)
Phase C: cs2gs name preservation + corpus/selfmig re-baseline (the
.Item2→.pricereadability win). Phase D: LS completion polish.Testing
NamedTupleElementTests(19, binder): name/ItemN/.Ncoexistence, 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.Listelement access by name.samples/NamedTupleElements.gs+ golden; full sample harness green (128/128, three hosts).🤖 Generated with Claude Code
https://claude.ai/code/session_01Nng28yiBdPVdeML7mSZphs