@@ -172,62 +172,6 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
172
172
return FunctionParameterClauseSyntax ( parameters: parameterList)
173
173
}
174
174
175
- /// Create a closure capture list used to capture the arguments to a function
176
- /// when calling it from its corresponding thunk function.
177
- ///
178
- /// - Parameters:
179
- /// - parametersWithLabels: A sequence of tuples containing parameters to
180
- /// the original function and their corresponding identifiers as used by
181
- /// the thunk function.
182
- ///
183
- /// - Returns: A closure capture list syntax node representing the arguments
184
- /// to the thunk function.
185
- ///
186
- /// We need to construct a capture list when calling a synchronous test
187
- /// function because of the call to `__ifMainActorIsolationEnforced(_:else:)`
188
- /// that we insert. That function theoretically captures all arguments twice,
189
- /// which is not allowed for arguments marked `borrowing` or `consuming`. The
190
- /// capture list forces those arguments to be copied, side-stepping the issue.
191
- ///
192
- /// - Note: We do not support move-only types as arguments yet. Instances of
193
- /// move-only types cannot be used with generics, so they cannot be elements
194
- /// of a `Collection`.
195
- private static func _createCaptureListExpr(
196
- from parametersWithLabels: some Sequence < ( DeclReferenceExprSyntax , FunctionParameterSyntax ) >
197
- ) -> ClosureCaptureClauseSyntax {
198
- let specifierKeywordsNeedingCopy : [ TokenKind ] = [ . keyword( . borrowing) , . keyword( . consuming) , ]
199
- let closureCaptures = parametersWithLabels. lazy. map { label, parameter in
200
- var needsCopy = false
201
- if let parameterType = parameter. type. as ( AttributedTypeSyntax . self) {
202
- needsCopy = parameterType. specifiers. contains { specifier in
203
- guard case let . simpleTypeSpecifier( specifier) = specifier else {
204
- return false
205
- }
206
- return specifierKeywordsNeedingCopy. contains ( specifier. specifier. tokenKind)
207
- }
208
- }
209
-
210
- if needsCopy {
211
- return ClosureCaptureSyntax (
212
- name: label. baseName,
213
- equal: . equalToken( ) ,
214
- expression: CopyExprSyntax (
215
- copyKeyword: . keyword( . copy) . with ( \. trailingTrivia, . space) ,
216
- expression: label
217
- )
218
- )
219
- } else {
220
- return ClosureCaptureSyntax ( expression: label)
221
- }
222
- }
223
-
224
- return ClosureCaptureClauseSyntax {
225
- for closureCapture in closureCaptures {
226
- closureCapture
227
- }
228
- }
229
- }
230
-
231
175
/// The `static` keyword, if `typeName` is not `nil`.
232
176
///
233
177
/// - Parameters:
@@ -269,7 +213,6 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
269
213
// needed, so it's lazy.
270
214
let forwardedParamsExpr = _createForwardedParamsExpr ( from: parametersWithLabels)
271
215
let thunkParamsExpr = _createThunkParamsExpr ( from: parametersWithLabels)
272
- lazy var captureListExpr = _createCaptureListExpr ( from: parametersWithLabels)
273
216
274
217
// How do we call a function if we don't know whether it's `async` or
275
218
// `throws`? Yes, we know if the keywords are on the function, but it could
@@ -290,7 +233,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
290
233
// If the function is noasync *and* main-actor-isolated, we'll call through
291
234
// MainActor.run to invoke it. We do not have a general mechanism for
292
235
// detecting isolation to other global actors.
293
- lazy var isMainActorIsolated = !functionDecl. attributes ( named: " MainActor " , inModuleNamed: " Swift " ) . isEmpty
236
+ lazy var isMainActorIsolated = !functionDecl. attributes ( named: " MainActor " , inModuleNamed: " _Concurrency " ) . isEmpty
294
237
var forwardCall : ( ExprSyntax ) -> ExprSyntax = {
295
238
" try await Testing.__requiringTry(Testing.__requiringAwait( \( $0) )) "
296
239
}
@@ -315,7 +258,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
315
258
if functionDecl. isStaticOrClass {
316
259
thunkBody = " _ = \( forwardCall ( " \( typeName) . \( functionDecl. name. trimmed) \( forwardedParamsExpr) " ) ) "
317
260
} else {
318
- let instanceName = context. makeUniqueName ( thunking : functionDecl )
261
+ let instanceName = context. makeUniqueName ( " " )
319
262
let varOrLet = functionDecl. isMutating ? " var " : " let "
320
263
thunkBody = """
321
264
\( raw: varOrLet) \( raw: instanceName) = \( forwardInit ( " \( typeName) () " ) )
@@ -344,16 +287,45 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
344
287
thunkBody = " _ = \( forwardCall ( " \( functionDecl. name. trimmed) \( forwardedParamsExpr) " ) ) "
345
288
}
346
289
347
- // If this function is synchronous and is not explicitly isolated to the
348
- // main actor, it may still need to run main-actor-isolated depending on the
349
- // runtime configuration in the test process.
350
- if functionDecl. signature. effectSpecifiers? . asyncSpecifier == nil && !isMainActorIsolated {
290
+ // If this function is synchronous, is not explicitly nonisolated, and is
291
+ // not explicitly isolated to some actor, it should run in the configured
292
+ // default isolation context. If the suite type is an actor, this will cause
293
+ // a hop off the actor followed by an immediate hop back on, but otherwise
294
+ // should be harmless. Note that we do not support specifying an `isolated`
295
+ // parameter on a test function at this time.
296
+ //
297
+ // We use a second, inner thunk function here instead of just adding the
298
+ // isolation parameter to the "real" thunk because adding it there prevents
299
+ // correct tuple desugaring of the "real" arguments to the thunk.
300
+ if functionDecl. signature. effectSpecifiers? . asyncSpecifier == nil && !isMainActorIsolated && !functionDecl. isNonisolated {
301
+ // Get a unique name for this secondary thunk. We don't need it to be
302
+ // uniqued against functionDecl because it's interior to the "real" thunk,
303
+ // so its name can't conflict with any other names visible in this scope.
304
+ let isolationThunkName = context. makeUniqueName ( " " )
305
+
306
+ // Insert a (defaulted) isolated argument. If we emit a closure (or inner
307
+ // function) that captured the arguments to the "real" thunk, the compiler
308
+ // has trouble reasoning about the lifetime of arguments to that closure
309
+ // especially if those arguments are borrowed or consumed, which results
310
+ // in hard-to-avoid compile-time errors. Fortunately, forwarding the full
311
+ // argument list is straightforward.
312
+ let thunkParamsExprCopy = FunctionParameterClauseSyntax {
313
+ for thunkParam in thunkParamsExpr. parameters {
314
+ thunkParam
315
+ }
316
+ FunctionParameterSyntax (
317
+ modifiers: [ DeclModifierSyntax ( name: . keyword( . isolated) ) ] ,
318
+ firstName: . wildcardToken( ) ,
319
+ type: " isolated (any Actor)? " as TypeSyntax ,
320
+ defaultValue: InitializerClauseSyntax ( value: " Testing.__defaultSynchronousIsolationContext " as ExprSyntax )
321
+ )
322
+ }
323
+
351
324
thunkBody = """
352
- try await Testing.__ifMainActorIsolationEnforced { \( captureListExpr) in
353
- \( thunkBody)
354
- } else: { \( captureListExpr) in
325
+ @Sendable func \( isolationThunkName) \( thunkParamsExprCopy) async throws {
355
326
\( thunkBody)
356
327
}
328
+ try await \( isolationThunkName) \( forwardedParamsExpr)
357
329
"""
358
330
}
359
331
0 commit comments