@@ -48,6 +48,18 @@ public func debugLog(_ s: String) {
48
48
@available ( SwiftStdlib 5 . 5 , * )
49
49
@main
50
50
struct Runner {
51
+ @MainActor
52
+ @inline ( never)
53
+ static func withExclusiveAccessAsync< T, U> ( to x: inout T , f: ( inout T ) async -> U ) async -> U {
54
+ await f ( & x)
55
+ }
56
+
57
+ @MainActor
58
+ @inline ( never)
59
+ static func withExclusiveAccess< T, U> ( to x: inout T , f: ( inout T ) -> U ) -> U {
60
+ f ( & x)
61
+ }
62
+
51
63
@inline ( never)
52
64
@MainActor
53
65
static func doSomething( ) async { }
@@ -293,8 +305,8 @@ struct Runner {
293
305
// First access begins here.
294
306
await callCallee1 ( )
295
307
useGlobal ( & global1) // We should not crash here since we cleaned up
296
- // the access in callCallee1 after we returned
297
- // from the await there.
308
+ // the access in callCallee1 after we returned
309
+ // from the await there.
298
310
debugLog ( " ==> Exit Main " )
299
311
}
300
312
@@ -336,11 +348,294 @@ struct Runner {
336
348
// First access begins here.
337
349
await callCallee1 ( )
338
350
useGlobal ( & global1) // We should not crash here since we cleaned up
339
- // the access in callCallee1 after we returned
340
- // from the await there.
351
+ // the access in callCallee1 after we returned
352
+ // from the await there.
341
353
debugLog ( " ==> Exit Main " )
342
354
}
343
355
356
+ // These are additional tests that used to be FileChecked but FileCheck
357
+ // was too hard to use in a concurrent context.
358
+ exclusivityTests. test ( " case1 " ) { @MainActor in
359
+ @inline ( never)
360
+ @Sendable func callee2( _ x: inout Int , _ y: inout Int , _ z: inout Int ) -> Void {
361
+ debugLog ( " ==> Enter callee2 " )
362
+ debugLog ( " ==> Exit callee2 " )
363
+ }
364
+
365
+ // We add an inline never here to make sure that we do not eliminate
366
+ // the dynamic access after inlining.
367
+ @MainActor
368
+ @inline ( never)
369
+ func callee1( ) async -> ( ) {
370
+ debugLog ( " ==> Enter callee1 " )
371
+ let handle = Task { @MainActor in
372
+ debugLog ( " ==> Enter callee1 Closure " )
373
+
374
+ // These accesses end before we await in the task.
375
+ do {
376
+ callee2 ( & global1, & global2, & global3)
377
+ }
378
+ let handle2 = Task { @MainActor in
379
+ debugLog ( " ==> Enter handle2! " )
380
+ debugLog ( " ==> Exit handle2! " )
381
+ }
382
+ await handle2. value
383
+ debugLog ( " ==> Exit callee1 Closure " )
384
+ }
385
+ await handle. value
386
+ debugLog ( " ==> Exit callee1 " )
387
+ }
388
+
389
+ debugLog ( " ==> Enter 'testCase1' " )
390
+ await callee1 ( )
391
+ debugLog ( " ==> Exit 'testCase1' " )
392
+ }
393
+
394
+ // Case 2: (F, F, T). In case 2, our task does not start with a live access
395
+ // and nothing from the outside synchronous context, but does pop with a new
396
+ // access.
397
+ //
398
+ // We use a suspend point and a withExclusiveAccessAsync(to:) to test this.
399
+ exclusivityTests. test ( " case2.filecheck.nocrash " ) { @MainActor in
400
+ debugLog ( " ==> Enter 'testCase2' " )
401
+
402
+ let handle = Task { @MainActor in
403
+ debugLog ( " ==> Inner Handle " )
404
+ await withExclusiveAccessAsync ( to: & global1) { @MainActor ( x: inout Int ) async -> Void in
405
+ let innerTaskHandle = Task { @MainActor in
406
+ // Different task, shouldn't crash.
407
+ withExclusiveAccess ( to: & global1) { _ in
408
+ debugLog ( " ==> No crash! " )
409
+ }
410
+ debugLog ( " ==> End Inner Task Handle " )
411
+ }
412
+ // This will cause us to serialize the access to global1. If
413
+ // we had an access here, we would crash.
414
+ await innerTaskHandle. value
415
+ debugLog ( " ==> After " )
416
+ }
417
+ // Accessis over. We shouldn't crash here.
418
+ withExclusiveAccess ( to: & global1) { _ in
419
+ debugLog ( " ==> No crash! " )
420
+ }
421
+ debugLog ( " ==> Inner Handle: After exclusive access " )
422
+ }
423
+
424
+ await handle. value
425
+ debugLog ( " ==> After exclusive access " )
426
+ let handle2 = Task { @MainActor in
427
+ debugLog ( " ==> Enter handle2! " )
428
+ debugLog ( " ==> Exit handle2! " )
429
+ }
430
+ await handle2. value
431
+ debugLog ( " ==> Exit 'testCase2' " )
432
+ }
433
+
434
+ exclusivityTests. test ( " case2.filecheck.crash " ) { @MainActor in
435
+ expectCrashLater ( withMessage: " Fatal access conflict detected " )
436
+ debugLog ( " ==> Enter 'testCase2' " )
437
+
438
+ let handle = Task { @MainActor in
439
+ debugLog ( " ==> Inner Handle " )
440
+ await withExclusiveAccessAsync ( to: & global1) { @MainActor ( x: inout Int ) async -> Void in
441
+ let innerTaskHandle = Task { @MainActor in
442
+ debugLog ( " ==> End Inner Task Handle " )
443
+ }
444
+ await innerTaskHandle. value
445
+ // We will crash here if we properly brought back in the
446
+ // access to global1 despite running code on a different
447
+ // task.
448
+ withExclusiveAccess ( to: & global1) { _ in
449
+ debugLog ( " ==> Got a crash! " )
450
+ }
451
+ debugLog ( " ==> After " )
452
+ }
453
+ debugLog ( " ==> Inner Handle: After exclusive access " )
454
+ }
455
+
456
+ await handle. value
457
+ debugLog ( " ==> After exclusive access " )
458
+ let handle2 = Task { @MainActor in
459
+ debugLog ( " ==> Enter handle2! " )
460
+ debugLog ( " ==> Exit handle2! " )
461
+ }
462
+ await handle2. value
463
+ debugLog ( " ==> Exit 'testCase2' " )
464
+ }
465
+
466
+ // Case 5: (T,F,F). To test case 5, we use with exclusive access to to
467
+ // create an exclusivity scope that goes over a suspension point. We are
468
+ // interesting in the case where we return after the suspension point. That
469
+ // push/pop is going to have our outer task bring in state and end it.
470
+ //
471
+ // CHECK-LABEL: ==> Enter 'testCase5'
472
+ // CHECK: ==> Task: [[TASK:0x[0-9a-f]+]]
473
+ // CHECK: Inserting new access: [[LLNODE:0x[a-z0-9]+]]
474
+ // CHECK-NEXT: Tracking!
475
+ // CHECK-NEXT: Access. Pointer: [[ACCESS:0x[a-z0-9]+]]
476
+ // CHECK: Exiting Thread Local Context. Before Swizzle. Task: [[TASK]]
477
+ // CHECK-NEXT: SwiftTaskThreadLocalContext: (FirstAccess,LastAccess): (0x0, 0x0)
478
+ // CHECK-NEXT: Access. Pointer: [[ACCESS]]. PC:
479
+ // CHECK: Exiting Thread Local Context. After Swizzle. Task: [[TASK]]
480
+ // CHECK_NEXT: SwiftTaskThreadLocalContext: (FirstAccess,LastAccess): ([[LLNODE]], [[LLNODE]])
481
+ // CHECK_NEXT: No Accesses.
482
+ //
483
+ // CHECK-NOT: Removing access:
484
+ // CHECK: ==> End Inner Task Handle
485
+ // CHECK: ==> After
486
+ // CHECK: Removing access: [[LLNODE]]
487
+ // CHECK: ==> After exclusive access
488
+ // CHECK: Exiting Thread Local Context. Before Swizzle. Task: [[TASK]]
489
+ // CHECK-NEXT: SwiftTaskThreadLocalContext: (FirstAccess,LastAccess): (0x0, 0x0)
490
+ // CHECK-NEXT: No Accesses.
491
+ // CHECK: Exiting Thread Local Context. After Swizzle. Task: [[TASK]]
492
+ // CHECK-NEXT: SwiftTaskThreadLocalContext: (FirstAccess,LastAccess): (0x0, 0x0)
493
+ // CHECK-NEXT: No Accesses.
494
+ //
495
+ // CHECK: ==> Exit 'testCase5'
496
+ exclusivityTests. test ( " case5.filecheck " ) { @MainActor in
497
+ debugLog ( " ==> Enter 'testCase5' " )
498
+
499
+ let outerHandle = Task { @MainActor in
500
+ await withExclusiveAccessAsync ( to: & global1) { @MainActor ( x: inout Int ) async -> Void in
501
+ let innerTaskHandle = Task { @MainActor in
502
+ debugLog ( " ==> End Inner Task Handle " )
503
+ }
504
+ await innerTaskHandle. value
505
+ debugLog ( " ==> After " )
506
+ }
507
+ debugLog ( " ==> After exclusive access " )
508
+ let handle2 = Task { @MainActor in
509
+ debugLog ( " ==> Enter handle2! " )
510
+ debugLog ( " ==> Exit handle2! " )
511
+ }
512
+ await handle2. value
513
+ }
514
+ await outerHandle. value
515
+ debugLog ( " ==> Exit 'testCase5' " )
516
+ }
517
+
518
+ exclusivityTests. test ( " case5.filecheck.crash " ) { @MainActor in
519
+ expectCrashLater ( withMessage: " Fatal access conflict detected " )
520
+ debugLog ( " ==> Enter 'testCase5' " )
521
+
522
+ let outerHandle = Task { @MainActor in
523
+ await withExclusiveAccessAsync ( to: & global1) { @MainActor ( x: inout Int ) async -> Void in
524
+ let innerTaskHandle = Task { @MainActor in
525
+ debugLog ( " ==> End Inner Task Handle " )
526
+ }
527
+ await innerTaskHandle. value
528
+ debugLog ( " ==> After " )
529
+ withExclusiveAccess ( to: & global1) { _ in
530
+ debugLog ( " ==> Crash here " )
531
+ }
532
+ }
533
+ debugLog ( " ==> After exclusive access " )
534
+ let handle2 = Task { @MainActor in
535
+ debugLog ( " ==> Enter handle2! " )
536
+ debugLog ( " ==> Exit handle2! " )
537
+ }
538
+ await handle2. value
539
+ }
540
+ await outerHandle. value
541
+ debugLog ( " ==> Exit 'testCase5' " )
542
+ }
543
+
544
+ // Case 6: (T, F, T). In case 6, our task starts with live accesses and is
545
+ // popped with live accesses. There are no sync accesses.
546
+ //
547
+ // We test this by looking at the behavior of the runtime after we
548
+ // finish executing handle2. In this case, we first check that things
549
+ // just work normally and as a 2nd case perform a conflicting access to
550
+ // make sure we crash.
551
+ exclusivityTests. test ( " case6.filecheck " ) { @MainActor in
552
+ let outerHandle = Task { @MainActor in
553
+ let callee2 = { @MainActor ( _ x: inout Int ) -> Void in
554
+ debugLog ( " ==> Enter callee2 " )
555
+ debugLog ( " ==> Exit callee2 " )
556
+ }
557
+
558
+ // We add an inline never here to make sure that we do not eliminate
559
+ // the dynamic access after inlining.
560
+ @MainActor
561
+ @inline ( never)
562
+ func callee1( _ x: inout Int ) async -> ( ) {
563
+ debugLog ( " ==> Enter callee1 " )
564
+ // This task is what prevents this example from crashing.
565
+ let handle = Task { @MainActor in
566
+ debugLog ( " ==> Enter callee1 Closure " )
567
+ // Second access. Different Task so it is ok.
568
+ await withExclusiveAccessAsync ( to: & global1) {
569
+ await callee2 ( & $0)
570
+ }
571
+ debugLog ( " ==> Exit callee1 Closure " )
572
+ }
573
+ await handle. value
574
+ debugLog ( " ==> callee1 after first await " )
575
+ // Force an await here so we can see that we properly swizzle.
576
+ let handle2 = Task { @MainActor in
577
+ debugLog ( " ==> Enter handle2! " )
578
+ debugLog ( " ==> Exit handle2! " )
579
+ }
580
+ await handle2. value
581
+ debugLog ( " ==> Exit callee1 " )
582
+ }
583
+
584
+ // First access begins here.
585
+ await callee1 ( & global1)
586
+ }
587
+ debugLog ( " ==> Enter 'testCase6' " )
588
+ await outerHandle. value
589
+ debugLog ( " ==> Exit 'testCase6' " )
590
+ }
591
+
592
+ exclusivityTests. test ( " case6.filecheck.crash " ) { @MainActor in
593
+ expectCrashLater ( withMessage: " Fatal access conflict detected " )
594
+ let outerHandle = Task { @MainActor in
595
+ let callee2 = { @MainActor ( _ x: inout Int ) -> Void in
596
+ debugLog ( " ==> Enter callee2 " )
597
+ debugLog ( " ==> Exit callee2 " )
598
+ }
599
+
600
+ // We add an inline never here to make sure that we do not eliminate
601
+ // the dynamic access after inlining.
602
+ @MainActor
603
+ @inline ( never)
604
+ func callee1( _ x: inout Int ) async -> ( ) {
605
+ debugLog ( " ==> Enter callee1 " )
606
+ // This task is what prevents this example from crashing.
607
+ let handle = Task { @MainActor in
608
+ debugLog ( " ==> Enter callee1 Closure " )
609
+ // Second access. Different Task so it is ok.
610
+ await withExclusiveAccessAsync ( to: & global1) {
611
+ await callee2 ( & $0)
612
+ }
613
+ debugLog ( " ==> Exit callee1 Closure " )
614
+ }
615
+ await handle. value
616
+ debugLog ( " ==> callee1 after first await " )
617
+ // Force an await here so we can see that we properly swizzle.
618
+ let handle2 = Task { @MainActor in
619
+ debugLog ( " ==> Enter handle2! " )
620
+ debugLog ( " ==> Exit handle2! " )
621
+ }
622
+ await handle2. value
623
+ // Make sure we brought back in the access to x so we crash
624
+ // here.
625
+ withExclusiveAccess ( to: & global1) { _ in
626
+ debugLog ( " ==> Will crash here! " )
627
+ }
628
+ debugLog ( " ==> Exit callee1 " )
629
+ }
630
+
631
+ // First access begins here.
632
+ await callee1 ( & global1)
633
+ }
634
+ debugLog ( " ==> Enter 'testCase6' " )
635
+ await outerHandle. value
636
+ debugLog ( " ==> Exit 'testCase6' " )
637
+ }
638
+
344
639
await runAllTestsAsync ( )
345
640
}
346
641
}
0 commit comments