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