@@ -395,6 +395,106 @@ private bool SkipGeneratedBranchesForExceptionHandlers(MethodDefinition methodDe
395
395
return _compilerGeneratedBranchesToExclude [ methodDefinition . FullName ] . Contains ( instruction . Offset ) ;
396
396
}
397
397
398
+ private bool SkipAwaitUsingBranches ( List < Instruction > instructions , Instruction instruction )
399
+ {
400
+ /*
401
+ Suppose we have an "await using" statement like this one:
402
+
403
+ await using (var ms = new MemoryStream(Encoding.ASCII.GetBytes("abc")))
404
+ {
405
+ }
406
+
407
+ The asynchronously disposable object is stored in a compiler-generated
408
+ field with the name <xxx>5__#, where "xxx" is the name of the variable
409
+ and "#" is an increasing index number for compiler-generated variables
410
+ (so, in this case, it would be <ms>5__1). Where it increases from 1 is
411
+ when "await using" is nested.
412
+
413
+ There are three kinds of branches that the compiler generates that we
414
+ should skip.
415
+ */
416
+
417
+ int currentIndex = instructions . BinarySearch ( instruction , new InstructionByOffsetComparer ( ) ) ;
418
+
419
+ /*
420
+ The first kind of branch we want to skip is a check that the variable
421
+ holding the asychronously disposable object is null. In the presence
422
+ of nested "await using" statements, all of these variables are checked
423
+ in various places, but the IL for each one follows the same pattern.
424
+
425
+ if (<ms>5__1 == null) [<--- we want to skip this]
426
+ {
427
+ goto IL_00b9;
428
+ }
429
+
430
+ IL_0048: ldarg.0
431
+ IL_0049: ldfld class [System.Private.CoreLib]System.IO.MemoryStream AwaitUsing/'<AsyncAwait>d__0'::'<ms>5__1'
432
+ IL_004e: brfalse.s IL_00b9 [<--- first kind of branch to skip]
433
+ */
434
+ if ( currentIndex >= 2 &&
435
+ instructions [ currentIndex - 2 ] . OpCode == OpCodes . Ldarg_0 &&
436
+ instructions [ currentIndex - 1 ] . OpCode == OpCodes . Ldfld &&
437
+ instructions [ currentIndex - 1 ] . Operand is FieldReference disposableRef &&
438
+ disposableRef . Name . StartsWith ( "<" ) && disposableRef . Name . Contains ( ">5__" ) )
439
+ {
440
+ return true ;
441
+ }
442
+ /*
443
+ The second and third kinds of branches we want to skip both appear in a
444
+ compiler-generated block that checks if an exception has been thrown by a
445
+ task and needs to be re-thrown.
446
+
447
+ obj2 = <>s__2;
448
+ if (obj2 != null) [<--- second kind of branch to skip]
449
+ {
450
+ Exception ex = obj2 as Exception;
451
+ if (ex == null) [<--- third kind of branch to skip]
452
+ {
453
+ throw obj2;
454
+ }
455
+ ExceptionDispatchInfo.Capture(ex).Throw();
456
+ }
457
+
458
+ IL_00b9: ldarg.0
459
+ IL_00ba: ldfld object AwaitUsing/'<AsyncAwait>d__0'::'<>s__2'
460
+ IL_00bf: stloc.1
461
+ IL_00c0: ldloc.1
462
+ IL_00c1: brfalse.s IL_00de [<--- second branch to skip]
463
+ IL_00c3: ldloc.1
464
+ IL_00c4: isinst [System.Private.CoreLib]System.Exception
465
+ IL_00c9: stloc.s 5
466
+ IL_00cb: ldloc.s 5
467
+ IL_00cd: brtrue.s IL_00d1 [<--- third branch to skip]
468
+ */
469
+ else if ( currentIndex >= 3 &&
470
+ instructions [ currentIndex - 3 ] . OpCode == OpCodes . Ldfld &&
471
+ instructions [ currentIndex - 3 ] . Operand is FieldReference exceptionRef && exceptionRef . Name . Contains ( "<>s__" ) &&
472
+ instructions [ currentIndex - 2 ] . OpCode == OpCodes . Stloc_1 &&
473
+ instructions [ currentIndex - 1 ] . OpCode == OpCodes . Ldloc_1 )
474
+ {
475
+ return true ;
476
+ }
477
+ else if ( currentIndex >= 4 &&
478
+ instructions [ currentIndex - 4 ] . OpCode == OpCodes . Ldloc_1 &&
479
+ instructions [ currentIndex - 3 ] . OpCode == OpCodes . Isinst &&
480
+ instructions [ currentIndex - 3 ] . Operand is TypeReference exceptionType && exceptionType . FullName == "System.Exception" &&
481
+ instructions [ currentIndex - 2 ] . OpCode == OpCodes . Stloc_S &&
482
+ instructions [ currentIndex - 2 ] . Operand is VariableReference variableStore && variableStore . Index == 5 &&
483
+ instructions [ currentIndex - 1 ] . OpCode == OpCodes . Ldloc_S &&
484
+ instructions [ currentIndex - 1 ] . Operand is VariableReference variableLoad && variableLoad . Index == 5 )
485
+ {
486
+ return true ;
487
+ }
488
+
489
+ /*
490
+ The "await using" state machine also contains a compiler-generated check
491
+ that the awaiter is completed, but that's being handled already by
492
+ SkipIsCompleteAwaiters.
493
+ */
494
+
495
+ return false ;
496
+ }
497
+
398
498
public IReadOnlyList < BranchPoint > GetBranchPoints ( MethodDefinition methodDefinition )
399
499
{
400
500
var list = new List < BranchPoint > ( ) ;
@@ -435,7 +535,9 @@ public IReadOnlyList<BranchPoint> GetBranchPoints(MethodDefinition methodDefinit
435
535
436
536
if ( isMoveNextInsideAsyncStateMachineProlog )
437
537
{
438
- if ( SkipMoveNextPrologueBranches ( instruction ) || SkipIsCompleteAwaiters ( instruction ) )
538
+ if ( SkipMoveNextPrologueBranches ( instruction ) ||
539
+ SkipIsCompleteAwaiters ( instruction ) ||
540
+ SkipAwaitUsingBranches ( instructions , instruction ) )
439
541
{
440
542
continue ;
441
543
}
0 commit comments