Fix #4019: Recognize with expressions whose initializer awaits

Roslyn spills the receiver of an initializer assignment that precedes an
await into a hoisted temporary, so the clone of a with expression (and the
object of an object initializer) reaches the initializer transform through
a copy it rejects. Only copies between two hoisted compiler temporaries are
inlined: folding copies of hoisted user locals as well would turn a user's
local into an initializer.
This commit is contained in:
dualfroz committed 2026-10-10 17:52:06 +02:00
1 parent ef47a24511
commit 85285c1cea
4 files changed
+50 -4

No files matched your search

@@ -16,9 +16,9 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE. // DEALINGS IN THE SOFTWARE.
// Await shapes that still decompile to code that does not compile are tracked as #4017 (type // An await shape that still decompiles to code that does not compile is tracked as #4017 (type
// parameter with an interface constraint), #4018 (static dynamic call) and #4019 (with // parameter with an interface constraint); it is absent here because it has no correct output to
// expression); they are absent here because they have no correct output to pin yet. // pin yet.
#pragma warning disable 1998 #pragma warning disable 1998
using System; using System;
@@ -22,6 +22,9 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
#if ROSLYN2
using System.Threading.Tasks;
#endif
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.InitializerTests namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.InitializerTests
{ {
@@ -1233,6 +1236,23 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.InitializerTests
Property = 42 Property = 42
}; };
} }
#if ROSLYN2
public static async Task<C> ObjectInitializerWithAwait(Task<int> task)
{
return new C {
Z = await task
};
}
public static async Task<C> ObjectInitializerWithAwaitAfterConstant(Task<S> task)
{
return new C {
Z = 42,
Y = await task
};
}
#endif
#endregion #endregion
#region Collection initializer #region Collection initializer
@@ -3,6 +3,7 @@ using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
#endif #endif
using System.Threading.Tasks;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{ {
@@ -129,6 +130,19 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
} }
}; };
} }
public async Task<PrimaryCtor> WithAwait(PrimaryCtor input)
{
return input with {
A = await Task.FromResult(42)
};
}
public async Task<Fields> WithAwaitAfterConstant(Fields input)
{
return input with {
A = 42,
B = await Task.FromResult(3.141)
};
}
} }
public abstract record WithNestedRecords public abstract record WithNestedRecords
@@ -668,12 +668,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms
case OpCode.SwitchInstruction: case OpCode.SwitchInstruction:
return true; return true;
case OpCode.LdLoc: case OpCode.LdLoc:
if (v.StateMachineField == null && ((LdLoc)inlinedExpression).Variable.StateMachineField != null) var copiedVariable = ((LdLoc)inlinedExpression).Variable;
if (v.StateMachineField == null && copiedVariable.StateMachineField != null)
{ {
// Roslyn likes to put the result of fetching a state machine field into a temporary variable, // Roslyn likes to put the result of fetching a state machine field into a temporary variable,
// so inline more aggressively in such cases. // so inline more aggressively in such cases.
return true; return true;
} }
if (IsHoistedCompilerTemporary(v) && IsHoistedCompilerTemporary(copiedVariable))
{
return true;
}
break; break;
} }
if (inlinedExpression.ResultType == StackType.Ref) if (inlinedExpression.ResultType == StackType.Ref)
@@ -773,6 +778,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms
} }
} }
static bool IsHoistedCompilerTemporary(ILVariable v)
{
string name = v.StateMachineField?.Name;
return name != null
&& (name.StartsWith("<>7__wrap", StringComparison.Ordinal) || name.StartsWith("<>s__", StringComparison.Ordinal));
}
/// <summary> /// <summary>
/// Gets whether 'expressionBeingMoved' can be inlined into 'expr'. /// Gets whether 'expressionBeingMoved' can be inlined into 'expr'.
/// </summary> /// </summary>