diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 75347a5e3..a59e50abf 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -4438,11 +4438,29 @@ namespace ICSharpCode.Decompiler.CSharp value = value.UnwrapChild(((DirectionExpression)value.Expression).Expression); } var callBuilder = new CallBuilder(this, typeSystem, settings); - if (expectedType != null - && inst.GetAwaiterMethod != null - && !callBuilder.CheckSimpleCall(value.ResolveResult, inst.GetAwaiterMethod, inst.GetAwaiterCallOpCode)) + if (expectedType != null && inst.GetAwaiterMethod != null) { - value = value.ConvertTo(expectedType, this); + // An operand boxed for the GetAwaiter call is typed 'object', which hides the receiver + // from member lookup. C# boxes the operand of an `await` implicitly, so the box need + // not appear in the output as long as the unboxed operand still binds the same + // GetAwaiter. Look through the box for that question only; UnwrapChild detaches the + // operand from the AST, so it must not run before the answer is known. + Expression? boxedOperand = null; + var lookupTarget = value.ResolveResult; + if (value.ResolveResult is ConversionResolveResult { Conversion.IsBoxingConversion: true } boxing + && value.Expression is CastExpression boxCast) + { + boxedOperand = boxCast.Expression; + lookupTarget = boxing.Input; + } + if (!callBuilder.CheckSimpleCall(lookupTarget, inst.GetAwaiterMethod, inst.GetAwaiterCallOpCode)) + { + value = value.ConvertTo(expectedType, this); + } + else if (boxedOperand != null) + { + value = value.UnwrapChild(boxedOperand); + } } return new UnaryOperatorExpression(UnaryOperatorType.Await, value.Expression) .WithILInstruction(inst)