@@ -48,6 +48,18 @@ public func debugLog(_ s: String) {
4848@available ( SwiftStdlib 5 . 5 , * )
4949@main
5050struct 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+
5163 @inline ( never)
5264 @MainActor
5365 static func doSomething( ) async { }
@@ -293,8 +305,8 @@ struct Runner {
293305 // First access begins here.
294306 await callCallee1 ( )
295307 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.
298310 debugLog ( " ==> Exit Main " )
299311 }
300312
@@ -336,11 +348,294 @@ struct Runner {
336348 // First access begins here.
337349 await callCallee1 ( )
338350 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.
341353 debugLog ( " ==> Exit Main " )
342354 }
343355
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+
344639 await runAllTestsAsync ( )
345640 }
346641}
0 commit comments