Skip to content

Instantly share code, notes, and snippets.

@jcouv
Last active October 22, 2019 21:03
Show Gist options
  • Save jcouv/a35c30a220c3920229d09331ebd97c76 to your computer and use it in GitHub Desktop.
Save jcouv/a35c30a220c3920229d09331ebd97c76 to your computer and use it in GitHub Desktop.

(Taken from dotnet/roslyn#20648)

At one time the implementation of tuples recognized that tuples and their underlying types are distinct types. Later, the LDM decided that these should not be distinct types. We (mostly) modified the compiler’s language behavior to respect the LDM decision, but we have not modified the APIs accordingly.

Our compiler guidelines include this design point:

As a rule, the public APIs exposed by Roslyn should reflect the language view of the program, not the lowered or implementation view. For example, a Symbol's Name property should reflect the name from the language point of view if there is one; we have a separate MetadataName (internal) property to represent the implementation view.

In order to align our APIs with the language view for tuples, I propose to make the following changes:

  • The INamedTypeSymbol APIs are changed as follows:
    • IsTupleType: Any type that is “tuple-compatible” shall be considered a tuple type (including original types such as ValueTuple<T1, T2>), in which case this API returns true.
    • Construct: A constructed type that is a tuple type (e.g., the construction of ValueTuple<T1,T2> with the types {Int32, Int32}) shall be considered a tuple type. This currently works in the language, but not in the APIs. The APIs will be fixed so that INamedTypeSymbol.Construct returns tuple types when appropriate.
    • TupleUnderlyingType: for a tuple type, it shall return a corresponding tuple type with no user-added names (at the top level). For example, given the type (int a, int b), its underlying type is (int, int), also known as System.ValueTuple<int, int>. For non-tuple types, it behaves as before.
    • Arity: Tuple types with fewer than 8 elements have the same arity as the number of elements. For example, (int, int) as arity 2. Tuple types with 8 or more elements have arity 8. This is a straightforward consequence of a tuple type without named elements having itself as its underlying type.
    • TypeArguments: This is now the same as the type arguments of the tuple’s underlying type (i.e., its own type arguments for unnamed tuple types).
    • ConstructedFrom: taken from the underlying type
    • OriginalDefinition: taken from the underlying type
    • Locations: taken from the underlying type.
    • TypeSubstitution and other internal APIs: taken from the underlying type
  • Instances of ValueTuple<T> (length one tuples) are considered a tuple type (even though there is no surface syntax for them in the language)
  • FieldSymbol APIs are changed as follows:
    • OriginalDefinition: For fields originating in the underlying type, that underlying field. For compiler-synthesized fields of long tuples, itself. For user-named elements, the same as today.
    • Locations: For fields originating in the underlying type, that underlying field’s location. For compiler-synthesized fields of long tuples, empty. For user-named elements, the location where a name was provided.
    • IsTupleField: true if the field is an instance field of a type (ContainingType) for which IsTupleType is true.
    • IsVirtualTupleField, IsDefaultTupleElement, TupleUnderlyingField (ie. VT'1.Item1 for (...).Item8), CorrespondingTupleField (ie. (...).Item1 for (...).Alice), TupleElementIndex: as today.

Some interesting effects are on long tuples.

  • ValueTuple<T1, T2, T3, T4, T5, T6, T7, TRest> is not a tuple type. Its field Item1 and Rest have property IsTupleField false. This is existing behavior.
  • When it is instantiated or substituted with a tuple type as TRest, the result is a tuple type that has additional synthetic fields for Item8 and beyond.
  • The TRest field has the property IsTupleField that returns true when accessed from a long tuple type. This is existing behavior.

In C# 6 and prior, a constructed type always had the same number of members as the type it was constructed from. This is no longer the case in C# 7, as long tuple types have additional members that are not members of the type from which they are constructed (e.g. Item8). This change is reflected in the compiler APIs, which will no longer support the obsolete invariant. I haven’t yet found any code that depends on this invariant.


  1. ToDisplayString and ToDisplayFormat and diagnostics use tuple format
  2. Missing ValueTuple type results in an error type (rather than a struct type)
  3. If explicitly specifying default tuple names, as in (int Item1, int Item2), those fields are just the default tuple fields. This is equivalent to (int, int) and has no element names.
  4. All members that are added are definitions, implicitly declared,
  5. Tuple types have the name ValueTuple and an arity up to 8 (ValueTuple'8).
  6. Both (int, int) and (int a, int b) are constructed from (T1, T2) (ie. ValueTuple<T1, T2>).
  7. Both (int, ..., int) and (int a, ..., int g) are constructed from ValueTuple<T1, ..., TRest>.
  8. Tests use ValueTuple format in VerifyIL
  9. Still some details to figure out for DeclaringSyntaxReferences and Locations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment