Skip to content

Convert New ValueTuple(Of ...)(args) to a tuple literal - #7

Draft
GrahamTheCoder wants to merge 9 commits into
masterfrom
claude/fix-1027-valuetuple-construction
Draft

GrahamTheCoder wants to merge 9 commits into
masterfrom
claude/fix-1027-valuetuple-construction

Conversation

@GrahamTheCoder

@GrahamTheCoder GrahamTheCoder commented Nov 10, 2026 •

Copy link
Copy Markdown
Owner

Fixes icsharpcode#1027

Root cause

VisitObjectCreationExpression converted the VB type syntax ValueTuple(Of Double, Double) through the symbol-based type conversion, and Roslyn's SyntaxGenerator.TypeExpression writes a tuple type with tuple syntax (double, double). C# forbids tuple syntax after new (CS8181), so new (double, double)(1d, 2d) didn't compile. Only object creation is affected: new (int, int)[3], typeof((int, int)), default((int, int)) and List<(int, int)> are all valid C#.

Design (simplest correct form first, uglier fallbacks only where needed)

ValueTupleCreationConverter (new) picks the form. If it declines, VisitObjectCreationExpression names the tuple type as ValueTuple<...> (CommonConversions.GetValueTupleTypeSyntax), so every remaining case compiles.

VB C#
Return New ValueTuple(Of Double, Double)(1, 2) return (1d, 2d);
Dim t = New ValueTuple(Of Long, Short)(x, s) (x Integer) var t = ((long)x, s);
New ValueTuple(Of Double, Long, Byte)(1, 2, 3) (1d, 2L, (byte)3)
New ValueTuple(Of Short, Integer)(s * s, s * s) ((short)(s * s), s * s)
New ValueTuple(Of String, Integer)(Nothing, Nothing) ((string)null, default(int))
New ValueTuple(Of Integer, String)(o, o) (Option Strict Off, o Object) (Conversions.ToInteger(o), Conversions.ToString(o))
New ValueTuple(Of Colour, Integer)(c, c) / (Of Colour, Colour)(0, c) (c, (int)c) / ((Colour)0, c)
New ValueTuple(Of Object, Object)(s, 1) new ValueTuple<object, object>(s, 1)
New ValueTuple(Of Integer?, Integer)(x, 1) new ValueTuple<int?, int>(x, 1)
New ValueTuple(Of Func(Of Integer), Integer)(Function() x, 2) new ValueTuple<Func<int>, int>(() => x, 2)
New ValueTuple(Of Integer, String)(item2:=s, item1:=x) new ValueTuple<int, string>(item2: s, item1: x)
New ValueTuple(Of Integer, Integer) With {.Item1 = x} new ValueTuple<int, int>() { Item1 = x }
New ValueTuple(Of Integer)(x) new ValueTuple<int>(x)
New ValueTuple(Of Integer, String)() / no parentheses default((int, string)) (default where target-typed)
New ValueTuple(Of Integer)() default(ValueTuple<int>)
New ValueTuple(Of 7×Integer, ValueTuple(Of Integer))(1, …, 7, New ValueTuple(Of Integer)(8)) (1, 2, 3, 4, 5, 6, 7, 8)
same with ValueTuple(Of Integer, Long) rest (8, 9) (1, …, 7, 8, 9L)
TRest passed as a variable, created without arguments, with an initializer, or converted (ValueTuple(Of Integer) into ValueTuple(Of Long)) new ValueTuple<…, ValueTuple<…>>(…)
inside an expression tree new ValueTuple<int, int>(x, 2) (tuple literals aren't allowed: CS8143)
ValueTuple.Create(1, 2), New Tuple(Of Integer, Integer)(1, 2), arrays, GetType, List(Of (Integer, Integer)) unchanged

Literal rule. A tuple literal's element types come from its target type if it has one. With no target type (e.g. var), they come from the elements. So a literal is used only when every converted argument already has its element's type. That's TypeConversionAnalyzer.KeepsConvertedTypeWithoutTargetType: either the source type is the element type, or the conversion the analyzer adds is explicit (cast, Conversions.ToX, or an exactly typed literal such as 1d). Implicit C# conversions (boxing, int to int?, lambda to delegate) would give the literal a different type under var, so those keep the constructor. When the literal is target-typed, the simplifier removes casts it doesn't need.

Decisions (accepted)

  • When an element needs a target type, the creation falls back to new ValueTuple<object, object>(s, 1) rather than ((object)s, (object)1). Target-typed contexts aren't detected, so they also keep the constructor.
  • No arguments → default((T1, T2)) rather than new ValueTuple<T1, T2>().

Review

The blind review converged after 1 round: it found only nitpicks. Its quality points (a shared plain-tuple-creation check, and a plain HasNoConversion body) and its test gaps (Option Strict Off and enum elements) were addressed in separate commits.

Out of scope (pre-existing, for separate issues)

  • Named-argument evaluation order. VB evaluates named arguments in parameter order, but C# evaluates them in source order. A self-verifying test showed New ValueTuple(Of Integer, Integer)(item2:=A(), item1:=B()) runs B then A in VB, and the converted C# runs A then B. This affects every call with reordered named arguments. The self-verifying test here only asserts values for named arguments.
  • Redundant double cast (int)(short)1 from existing literal conversions.

Tests

  • Conversion tests in ExpressionTests.cs: ValueTupleConstructorBecomesTupleLiteral, …BecomesTupleLiteralWithElementsOfTheTypeArguments, …WithConvertedElementsBecomesTupleLiteral, …WithoutArgumentsBecomesDefault, …WithInlineRestBecomesFlatTupleLiteral, …KeepsConstructorWhenTupleLiteralWouldChangeMeaning, …InExpressionTreeKeepsConstructor.
  • Self-verifying Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb (10 facts). They check runtime GetType() of var-inferred results, so a wrongly typed literal fails. They also check values, converted and enum elements, evaluation order, long tuples with Rest, defaults, and an expression tree that is compiled and invoked.
  • Full suite (net10.0, Linux) at 207efa1: 1440 passed, 5 skipped, 0 failed. Master baseline is 1425 / 5 / 0, and no existing expectations changed. After the review commits, the ExpressionTests + SelfVerifyingTests filter gives 800 passed, 1 skipped, 0 failed.

Coverage of changed lines

Measured with --collect:"XPlat Code Coverage" on the tuple and self-verifying tests. Every changed line is executed. Partially covered branches:

  • the vbConvertedType is null guard in KeepsConvertedTypeWithoutTargetType (error recovery only; it stops Equals(null, null) from returning true);
  • the defensive checks on a TRest in GetTupleElementsOrNull: a non-tuple type, and named arguments or no parentheses.

Two branches that coverage showed to be unreachable were removed in their own commit. Mutation testing was not run.

🤖 Generated with Claude Code

https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg

C# reports CS8181 for `new (int, int)(...)`, so an object creation of a
tuple type now uses `new ValueTuple<int, int>(...)`. This makes every
ValueTuple construction compile; simpler forms follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
Fixes icsharpcode#1027: `New ValueTuple(Of Double, Double)(1, 2)`
now becomes `(1d, 2d)`.

A tuple literal only takes its element types from a target type when it
has one (not when assigned to var), so it's used only when every converted
argument already has its element's type. Otherwise (boxing, int to int?,
lambdas), and for 1-tuples, named arguments, initializers and expression
trees (CS8143), the creation stays `new ValueTuple<...>(...)`.
A TRest created inline is flattened, as C# lays out longer literals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
A struct's parameterless creation is its default value, and
`default((int, string))` names the tuple type without constructing it,
so it also simplifies to `default` where the type is inferred.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
`New ValueTuple(Of T1..T7, ValueTuple(Of T8))(..., New ValueTuple(Of T8)())`
flattened to a 7-element literal, a different type. It now keeps the
constructor, with the empty rest as `default`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
A VB tuple type always has a TupleUnderlyingType, and a literal-suffix
conversion never has identical source and converted types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
@GrahamTheCoder GrahamTheCoder changed the title WIP: Convert New ValueTuple(Of ...)(args) to a tuple literal Convert New ValueTuple(Of ...)(args) to a tuple literal Nov 11, 2026
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.

VB -> C#: Conversion of ValueTuple causes compile error

2 participants