Skip to content

Add a refactoring between reference and struct tuples - #20548

Open
xperiandri wants to merge 5 commits into
dotnet:mainfrom
xperiandri:feature/tuple-struct-refactoring
Open

xperiandri wants to merge 5 commits into
dotnet:mainfrom
xperiandri:feature/tuple-struct-refactoring

Conversation

@xperiandri

@xperiandri xperiandri commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Offers Ctrl+. on a tuple expression, pattern or annotated tuple type to switch it between a reference and a struct tuple. The value is followed through annotations, patterns, record fields, function results and call arguments across the solution, so the result still type-checks. What can't be followed (fst, snd, generic collections) is left for the compiler to report.

Not offered for method argument lists, the only argument of a member (that is its parameter list), inside quotations, or when the file has a paired .fsi.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Change path Release notes path Description
`vsintegration/src` docs/release-notes/.VisualStudio/18.vNext.md

Ctrl+. on a tuple expression, pattern or annotated tuple type converts it
to the other kind and follows the value through the solution: annotations
of values, parameters, record fields and function results it flows
through, tuple patterns taking it apart, and the arguments and values
flowing into it. Uses that cannot be followed (fst, snd, generic
collections) are left for the compiler to report.

CreateWithCodeAndDependency now tells FCS about both files, so the second
file can be type-checked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the feature/tuple-struct-refactoring branch from fb6c03d to 512e8cb Compare September 14, 2026 14:36
xperiandri and others added 2 commits September 14, 2026 17:02
Adding the second document to a single-file solution left Find All
References unable to see either file, so a chain through a function's
call sites stopped at the first file. The synthetic project gives both
files to the checker the way AddReturnTypeTests and FindReferencesTests
set up theirs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The synthetic project's checker reads the other file from disk, so
checking the refactored document saw the old definition; a struct tuple
pattern happens to accept a reference tuple, which hid that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri and others added 2 commits September 14, 2026 19:59
…ents with their calls

The only argument of a member or constructor is its parameter list, not
a tuple, so it is no longer offered. A tuple that is a whole curried
argument of a function or member now converts the matching argument at
every call, and `struct` no longer runs into a name the parenthesis
follows (`f(a, b)`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e of its section

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@xperiandri
xperiandri marked this pull request as ready for review September 14, 2026 19:40
@xperiandri
xperiandri requested a review from a team as a code owner September 14, 2026 19:40
@github-actions github-actions Bot added the ⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager label Sep 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Design-Time
Affects-Design-Time: Modifies Visual Studio code executed while projects are open.

Generated by PR Tooling Safety Check · gpt56 1.8M ·

@xperiandri xperiandri changed the title Refactoring between reference and struct tuples Add a refactoring between reference and struct tuples Sep 15, 2026
@xperiandri

Copy link
Copy Markdown
Contributor Author

Covered cases

Converts

  • A tuple used nowhere else converts on its own, and back: printfn "%A" (1, 2)printfn "%A" struct (1, 2); a type argument list<int * int>list<struct (int * int)>
  • A value converts together with the tuple patterns that take it apart and the annotations it flows into, both ways: let pair = (1, 2) + let (a, b) = pair + let copy: int * int = pairlet pair = struct (1, 2) + let struct (a, b) = pair + let copy: struct (int * int) = pair
  • A parameter annotation converts the patterns on the parameter and every argument passed to it, both literals and values, both ways: let sum (p: int * int) + let (a, b) = p + let pair = (3, 4) + sum pair + sum (1, 2)let sum (p: struct (int * int)) + let struct (a, b) = p + let pair = struct (3, 4) + sum pair + sum struct (1, 2)
  • A parameter that is not first converts the matching argument of each call, both ways: curried let scale (factor: int) (p: int * int) + scale 2 (1, 2)scale 2 struct (1, 2); a tuple inside a tupled parameter list let area (p: int * int, factor: int) + area ((2, 3), 4)area (struct (2, 3), 4)
  • The tuple argument of a curried function or member converts with its calls, both ways: let add (a, b) c + add (1, 2) 3let add struct (a, b) c + add struct (1, 2) 3; member _.Add (a: int, b: int) (c: int) + Calc().Add (1, 2) 3member _.Add struct (a: int, b: int) (c: int) + Calc().Add struct (1, 2) 3
  • A space is added between struct and a name the parenthesis directly follows: let add(a, b) clet add struct (a, b) c
  • A return type converts the result and the patterns that take it apart, both ways: let origin () : int * int = (0, 0) + let (x, y) = origin ()let origin () : struct (int * int) = struct (0, 0) + let struct (x, y) = origin ()
  • A record field converts only its own values and the patterns on it, both ways: { Start: int * int; Finish: int * int } + { Start = (0, 0); Finish = (1, 1) } + let (sx, sy) = line.Start → only Start becomes struct (int * int), struct (0, 0) and let struct (sx, sy); Finish is not changed
  • A value declared in a second file converts in that file: the caret on let (a, b) = A.pair in module B also rewrites let pair = (1, 2) in A to struct (1, 2); the result is checked as a new project and has no errors
  • A use the refactoring cannot follow is left for the compiler to report: let pair = (1, 2) + let first = fst pairlet pair = struct (1, 2), fst pair is unchanged and gives exactly one error on its line
  • The action titles name the target kind: Convert to struct tuple and Convert to reference tuple
  • Every result is re-type-checked and asserted to have no errors

Not offered

  • The caret is not on a tuple: let x = 1
  • The argument list of a .NET method call: System.Math.Max(1, 2)
  • A tuple inside a quotation: <@ (1, 2) @>
  • A member's parameter list, here with an optional parameter: member _.Greet(name: string, ?greeting: string)
  • A struct tuple that is a member's only parameter: member _.Add struct (a: int, b: int) = a + b
  • A constructor's parameter list: new(x: int, y: int, z: int) = Point(x + z, y)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

1 participant