Skip to content

Commit 59f3f16

Browse files
committed
[Executors] Make move to ExecutorJob binary compatible; deprecate Job
1 parent 1808850 commit 59f3f16

File tree

9 files changed

+244
-48
lines changed

9 files changed

+244
-48
lines changed

include/swift/AST/Decl.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3879,8 +3879,11 @@ class NominalTypeDecl : public GenericTypeDecl, public IterableDeclContext {
38793879
/// Find the 'RemoteCallArgument(label:name:value:)' initializer function.
38803880
ConstructorDecl *getDistributedRemoteCallArgumentInitFunction() const;
38813881

3882-
/// Get the move-only `enqueue(Job)` protocol requirement function on the `Executor` protocol.
3882+
/// Get the move-only `enqueue(ExecutorJob)` protocol requirement function on the `Executor` protocol.
38833883
AbstractFunctionDecl *getExecutorOwnedEnqueueFunction() const;
3884+
/// This method should be deprecated and removed
3885+
/// Get the move-only `enqueue(Job)` protocol requirement function on the `Executor` protocol.
3886+
AbstractFunctionDecl *getExecutorLegacyOwnedEnqueueFunction() const;
38843887

38853888
/// Collect the set of protocols to which this type should implicitly
38863889
/// conform, such as AnyObject (for classes).

include/swift/AST/DiagnosticsSema.def

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6517,7 +6517,11 @@ WARNING(hashvalue_implementation,Deprecation,
65176517

65186518
WARNING(executor_enqueue_unowned_implementation,Deprecation,
65196519
"'Executor.enqueue(UnownedJob)' is deprecated as a protocol requirement; "
6520-
"conform type %0 to 'Executor' by implementing 'func enqueue(Job)' instead",
6520+
"conform type %0 to 'Executor' by implementing 'func enqueue(ExecutorJob)' instead",
6521+
(Type))
6522+
WARNING(executor_enqueue_deprecated_owned_job_implementation,Deprecation,
6523+
"'Executor.enqueue(Job)' is deprecated as a protocol requirement; "
6524+
"conform type %0 to 'Executor' by implementing 'func enqueue(ExecutorJob)' instead",
65216525
(Type))
65226526

65236527
//------------------------------------------------------------------------------

lib/AST/Decl.cpp

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5305,6 +5305,9 @@ VarDecl *NominalTypeDecl::getGlobalActorInstance() const {
53055305
AbstractFunctionDecl *
53065306
NominalTypeDecl::getExecutorOwnedEnqueueFunction() const {
53075307
auto &C = getASTContext();
5308+
StructDecl *executorJobDecl = C.getExecutorJobDecl();
5309+
if (!executorJobDecl)
5310+
return nullptr;
53085311

53095312
auto proto = dyn_cast<ProtocolDecl>(this);
53105313
if (!proto)
@@ -5322,11 +5325,51 @@ NominalTypeDecl::getExecutorOwnedEnqueueFunction() const {
53225325
continue;
53235326

53245327
if (auto *funcDecl = dyn_cast<AbstractFunctionDecl>(candidate)) {
5325-
if (funcDecl->getParameters()->size() != 1)
5328+
auto params = funcDecl->getParameters();
5329+
5330+
if (params->size() != 1)
53265331
continue;
53275332

5333+
if (params->get(0)->getSpecifier() == ParamSpecifier::LegacyOwned && // TODO: make this Consuming
5334+
params->get(0)->getInterfaceType()->isEqual(executorJobDecl->getDeclaredInterfaceType())) {
5335+
return funcDecl;
5336+
}
5337+
}
5338+
}
5339+
5340+
return nullptr;
5341+
}
5342+
5343+
AbstractFunctionDecl *
5344+
NominalTypeDecl::getExecutorLegacyOwnedEnqueueFunction() const {
5345+
auto &C = getASTContext();
5346+
StructDecl *legacyJobDecl = C.getJobDecl();
5347+
if (!legacyJobDecl)
5348+
return nullptr;
5349+
5350+
auto proto = dyn_cast<ProtocolDecl>(this);
5351+
if (!proto)
5352+
return nullptr;
5353+
5354+
llvm::SmallVector<ValueDecl *, 2> results;
5355+
lookupQualified(getSelfNominalTypeDecl(),
5356+
DeclNameRef(C.Id_enqueue),
5357+
NL_ProtocolMembers,
5358+
results);
5359+
5360+
for (auto candidate: results) {
5361+
// we're specifically looking for the Executor protocol requirement
5362+
if (!isa<ProtocolDecl>(candidate->getDeclContext()))
5363+
continue;
5364+
5365+
if (auto *funcDecl = dyn_cast<AbstractFunctionDecl>(candidate)) {
53285366
auto params = funcDecl->getParameters();
5329-
if (params->get(0)->getSpecifier() == ParamSpecifier::LegacyOwned) { // TODO: make this Consuming
5367+
5368+
if (params->size() != 1)
5369+
continue;
5370+
5371+
if (params->get(0)->getSpecifier() == ParamSpecifier::LegacyOwned && // TODO: make this Consuming
5372+
params->get(0)->getType()->isEqual(legacyJobDecl->getDeclaredInterfaceType())) {
53305373
return funcDecl;
53315374
}
53325375
}

lib/Sema/TypeCheckConcurrency.cpp

Lines changed: 53 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,8 +1269,9 @@ void swift::tryDiagnoseExecutorConformance(ASTContext &C,
12691269
// enqueue(_:)
12701270
auto enqueueDeclName = DeclName(C, DeclBaseName(C.Id_enqueue), { Identifier() });
12711271

1272-
FuncDecl *unownedEnqueueRequirement = nullptr;
12731272
FuncDecl *moveOnlyEnqueueRequirement = nullptr;
1273+
FuncDecl *legacyMoveOnlyEnqueueRequirement = nullptr; // TODO: preferably we'd want to remove handling of `enqueue(Job)` when able to
1274+
FuncDecl *unownedEnqueueRequirement = nullptr;
12741275
for (auto req: proto->getProtocolRequirements()) {
12751276
auto *funcDecl = dyn_cast<FuncDecl>(req);
12761277
if (!funcDecl)
@@ -1282,62 +1283,74 @@ void swift::tryDiagnoseExecutorConformance(ASTContext &C,
12821283
// look for the first parameter being a Job or UnownedJob
12831284
if (funcDecl->getParameters()->size() != 1)
12841285
continue;
1286+
12851287
if (auto param = funcDecl->getParameters()->front()) {
1288+
StructDecl *executorJobDecl = C.getExecutorJobDecl();
1289+
StructDecl *legacyJobDecl = C.getJobDecl();
12861290
StructDecl *unownedJobDecl = C.getUnownedJobDecl();
1287-
StructDecl *jobDecl = nullptr;
1288-
if (auto executorJobDecl = C.getExecutorJobDecl()) {
1289-
jobDecl = executorJobDecl;
1290-
} else if (auto plainJobDecl = C.getJobDecl()) {
1291-
// old standard library, before we introduced the `typealias Job = ExecutorJob`
1292-
jobDecl = plainJobDecl;
1293-
}
12941291

1295-
if (jobDecl &&
1296-
param->getType()->isEqual(jobDecl->getDeclaredInterfaceType())) {
1292+
if (executorJobDecl && param->getType()->isEqual(executorJobDecl->getDeclaredInterfaceType())) {
12971293
assert(moveOnlyEnqueueRequirement == nullptr);
12981294
moveOnlyEnqueueRequirement = funcDecl;
1299-
} else if (unownedJobDecl &&
1300-
param->getType()->isEqual(unownedJobDecl->getDeclaredInterfaceType())) {
1295+
} else if (legacyJobDecl && param->getType()->isEqual(legacyJobDecl->getDeclaredInterfaceType())) {
1296+
assert(legacyMoveOnlyEnqueueRequirement == nullptr);
1297+
legacyMoveOnlyEnqueueRequirement = funcDecl;
1298+
} else if (unownedJobDecl && param->getType()->isEqual(unownedJobDecl->getDeclaredInterfaceType())) {
13011299
assert(unownedEnqueueRequirement == nullptr);
13021300
unownedEnqueueRequirement = funcDecl;
13031301
}
13041302
}
13051303

1306-
// if we found both, we're done here and break out of the loop
1307-
if (unownedEnqueueRequirement && moveOnlyEnqueueRequirement)
1304+
// if we found all potential requirements, we're done here and break out of the loop
1305+
if (unownedEnqueueRequirement &&
1306+
moveOnlyEnqueueRequirement &&
1307+
legacyMoveOnlyEnqueueRequirement)
13081308
break; // we're done looking for the requirements
13091309
}
13101310

13111311
auto conformance = module->lookupConformance(nominalTy, proto);
13121312
auto concreteConformance = conformance.getConcrete();
13131313
assert(unownedEnqueueRequirement && "could not find the enqueue(UnownedJob) requirement, which should be always there");
1314-
ConcreteDeclRef unownedEnqueueWitness = concreteConformance->getWitnessDeclRef(unownedEnqueueRequirement);
13151314

1316-
if (auto enqueueUnownedDecl = unownedEnqueueWitness.getDecl()) {
1317-
// Old UnownedJob based impl is present, warn about it suggesting the new protocol requirement.
1318-
if (enqueueUnownedDecl->getLoc().isValid()) {
1319-
diags.diagnose(enqueueUnownedDecl->getLoc(), diag::executor_enqueue_unowned_implementation, nominalTy);
1320-
}
1321-
}
1322-
1323-
if (auto unownedEnqueueDecl = unownedEnqueueWitness.getDecl()) {
1324-
if (moveOnlyEnqueueRequirement) {
1325-
ConcreteDeclRef moveOnlyEnqueueWitness = concreteConformance->getWitnessDeclRef(moveOnlyEnqueueRequirement);
1326-
if (auto moveOnlyEnqueueDecl = moveOnlyEnqueueWitness.getDecl()) {
1327-
if (unownedEnqueueDecl && unownedEnqueueDecl->getLoc().isInvalid() &&
1328-
moveOnlyEnqueueDecl && moveOnlyEnqueueDecl->getLoc().isInvalid()) {
1329-
// Neither old nor new implementation have been found, but we provide default impls for them
1330-
// that are mutually recursive, so we must error and suggest implementing the right requirement.
1331-
auto ownedRequirement = C.getExecutorDecl()->getExecutorOwnedEnqueueFunction();
1332-
nominal->diagnose(diag::type_does_not_conform, nominalTy, proto->getDeclaredInterfaceType());
1333-
ownedRequirement->diagnose(diag::no_witnesses,
1334-
getProtocolRequirementKind(ownedRequirement),
1335-
ownedRequirement->getName(),
1336-
proto->getDeclaredInterfaceType(),
1337-
/*AddFixIt=*/true);
1338-
}
1339-
}
1340-
}
1315+
// try to find at least a single implementations of enqueue(_:)
1316+
ConcreteDeclRef unownedEnqueueWitness = concreteConformance->getWitnessDeclRef(unownedEnqueueRequirement);
1317+
ValueDecl *unownedEnqueueWitnessDecl = unownedEnqueueWitness.getDecl();
1318+
ValueDecl *moveOnlyEnqueueWitnessDecl = nullptr;
1319+
ValueDecl *legacyMoveOnlyEnqueueWitnessDecl = nullptr;
1320+
1321+
if (moveOnlyEnqueueRequirement) {
1322+
moveOnlyEnqueueWitnessDecl = concreteConformance->getWitnessDeclRef(
1323+
moveOnlyEnqueueRequirement).getDecl();
1324+
}
1325+
if (legacyMoveOnlyEnqueueRequirement) {
1326+
legacyMoveOnlyEnqueueWitnessDecl = concreteConformance->getWitnessDeclRef(
1327+
legacyMoveOnlyEnqueueRequirement).getDecl();
1328+
}
1329+
1330+
// --- Diagnose warnings and errors
1331+
1332+
// Old UnownedJob based impl is present, warn about it suggesting the new protocol requirement.
1333+
if (unownedEnqueueWitnessDecl && unownedEnqueueWitnessDecl->getLoc().isValid()) {
1334+
diags.diagnose(unownedEnqueueWitnessDecl->getLoc(), diag::executor_enqueue_unowned_implementation, nominalTy);
1335+
}
1336+
// Old Job based impl is present, warn about it suggesting the new protocol requirement.
1337+
if (legacyMoveOnlyEnqueueWitnessDecl && legacyMoveOnlyEnqueueWitnessDecl->getLoc().isValid()) {
1338+
diags.diagnose(legacyMoveOnlyEnqueueWitnessDecl->getLoc(), diag::executor_enqueue_deprecated_owned_job_implementation, nominalTy);
1339+
}
1340+
1341+
if ((!unownedEnqueueWitnessDecl || unownedEnqueueWitnessDecl->getLoc().isInvalid()) &&
1342+
(!moveOnlyEnqueueWitnessDecl || moveOnlyEnqueueWitnessDecl->getLoc().isInvalid()) &&
1343+
(!legacyMoveOnlyEnqueueWitnessDecl || legacyMoveOnlyEnqueueWitnessDecl->getLoc().isInvalid())) {
1344+
// Neither old nor new implementation have been found, but we provide default impls for them
1345+
// that are mutually recursive, so we must error and suggest implementing the right requirement.
1346+
auto ownedRequirement = C.getExecutorDecl()->getExecutorOwnedEnqueueFunction();
1347+
nominal->diagnose(diag::type_does_not_conform, nominalTy, proto->getDeclaredInterfaceType());
1348+
ownedRequirement->diagnose(diag::no_witnesses,
1349+
getProtocolRequirementKind(ownedRequirement),
1350+
ownedRequirement->getName(),
1351+
ownedRequirement->getParameters()->get(0)->getInterfaceType(),
1352+
/*AddFixIt=*/true);
1353+
return;
13411354
}
13421355
}
13431356

stdlib/public/Concurrency/Executor.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ public protocol Executor: AnyObject, Sendable {
2424
#endif // !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
2525
func enqueue(_ job: UnownedJob)
2626

27+
#if !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
28+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
29+
@available(*, deprecated, message: "Use enqueue(ExecutorJob) instead")
30+
func enqueue(_ job: __owned Job)
31+
#endif // !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
32+
2733
#if !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
2834
@available(SwiftStdlib 5.9, *)
2935
func enqueue(_ job: __owned ExecutorJob)
@@ -46,6 +52,17 @@ public protocol SerialExecutor: Executor {
4652
#endif // !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
4753
func enqueue(_ job: UnownedJob)
4854

55+
#if !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
56+
// This requirement is repeated here as a non-override so that we
57+
// get a redundant witness-table entry for it. This allows us to
58+
// avoid drilling down to the base conformance just for the basic
59+
// work-scheduling operation.
60+
@_nonoverride
61+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
62+
@available(*, deprecated, message: "Use enqueue(ExecutorJob) instead")
63+
func enqueue(_ job: __owned Job)
64+
#endif // !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
65+
4966
#if !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
5067
// This requirement is repeated here as a non-override so that we
5168
// get a redundant witness-table entry for it. This allows us to
@@ -92,6 +109,10 @@ extension Executor {
92109
public func enqueue(_ job: __owned ExecutorJob) {
93110
self.enqueue(UnownedJob(job))
94111
}
112+
113+
public func enqueue(_ job: __owned Job) {
114+
self.enqueue(UnownedJob(job))
115+
}
95116
}
96117
#endif // !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
97118

stdlib/public/Concurrency/PartialAsyncTask.swift

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ public struct UnownedJob: Sendable {
4242
self.context = context
4343
}
4444

45+
#if !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
46+
/// Create an `UnownedJob` whose lifetime must be managed carefully until it is run exactly once.
47+
@available(SwiftStdlib 5.9, *)
48+
public init(_ job: __owned Job) {
49+
self.context = job.context
50+
}
51+
#endif // !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
52+
4553
#if !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
4654
/// Create an `UnownedJob` whose lifetime must be managed carefully until it is run exactly once.
4755
@available(SwiftStdlib 5.9, *)
@@ -106,9 +114,76 @@ extension UnownedJob: CustomStringConvertible {
106114

107115
#if !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
108116

117+
/// Deprecated equivalent of ``ExecutorJob``.
118+
///
119+
/// A unit of scheduleable work.
120+
///
121+
/// Unless you're implementing a scheduler,
122+
/// you don't generally interact with jobs directly.
109123
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
110124
@available(*, deprecated, renamed: "ExecutorJob")
111-
public typealias Job = ExecutorJob
125+
@frozen
126+
@_moveOnly
127+
public struct Job: Sendable {
128+
internal var context: Builtin.Job
129+
130+
@usableFromInline
131+
internal init(context: __owned Builtin.Job) {
132+
self.context = context
133+
}
134+
135+
public init(_ job: UnownedJob) {
136+
self.context = job._context
137+
}
138+
139+
public init(_ job: __owned ExecutorJob) {
140+
self.context = job.context
141+
}
142+
143+
public var priority: JobPriority {
144+
let raw = _swift_concurrency_jobPriority(UnownedJob(context: self.context))
145+
return JobPriority(rawValue: raw)
146+
}
147+
148+
// TODO: move only types cannot conform to protocols, so we can't conform to CustomStringConvertible;
149+
// we can still offer a description to be called explicitly though.
150+
public var description: String {
151+
let id = _getJobTaskId(UnownedJob(context: self.context))
152+
/// Tasks are always assigned an unique ID, however some jobs may not have it set,
153+
/// and it appearing as 0 for _different_ jobs may lead to misunderstanding it as
154+
/// being "the same 0 id job", we specifically print 0 (id not set) as nil.
155+
if (id > 0) {
156+
return "\(Self.self)(id: \(id))"
157+
} else {
158+
return "\(Self.self)(id: nil)"
159+
}
160+
}
161+
}
162+
163+
@available(SwiftStdlib 5.9, *)
164+
extension Job {
165+
166+
/// Run this job on the passed in executor.
167+
///
168+
/// This operation runs the job on the calling thread and *blocks* until the job completes.
169+
/// The intended use of this method is for an executor to determine when and where it
170+
/// wants to run the job and then call this method on it.
171+
///
172+
/// The passed in executor reference is used to establish the executor context for the job,
173+
/// and should be the same executor as the one semantically calling the `runSynchronously` method.
174+
///
175+
/// This operation consumes the job, preventing it accidental use after it has ben run.
176+
///
177+
/// Converting a `ExecutorJob` to an ``UnownedJob`` and invoking ``UnownedJob/runSynchronously(_:)` on it multiple times is undefined behavior,
178+
/// as a job can only ever be run once, and must not be accessed after it has been run.
179+
///
180+
/// - Parameter executor: the executor this job will be semantically running on.
181+
@_alwaysEmitIntoClient
182+
@inlinable
183+
__consuming public func runSynchronously(on executor: UnownedSerialExecutor) {
184+
_swiftJobRun(UnownedJob(self), executor)
185+
}
186+
}
112187

113188
/// A unit of scheduleable work.
114189
///
@@ -129,6 +204,10 @@ public struct ExecutorJob: Sendable {
129204
self.context = job._context
130205
}
131206

207+
public init(_ job: __owned Job) {
208+
self.context = job.context
209+
}
210+
132211
public var priority: JobPriority {
133212
let raw = _swift_concurrency_jobPriority(UnownedJob(context: self.context))
134213
return JobPriority(rawValue: raw)

stdlib/public/Distributed/DistributedDefaultExecutor.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ internal final class DistributedRemoteActorReferenceExecutor: SerialExecutor {
2424
internal init() {}
2525

2626
@inlinable
27-
public func enqueue(_ job: __owned ExecutorJob) {
27+
public func enqueue(_ job: __owned Job) {
2828
let jobDescription = job.description
2929
fatalError("Attempted to enqueue \(ExecutorJob.self) (\(jobDescription)) on executor of remote distributed actor reference!")
3030
}

test/Concurrency/Runtime/custom_executors_moveOnly_job.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
// REQUIRES: concurrency_runtime
1111

1212
final class InlineExecutor: SerialExecutor, CustomStringConvertible {
13+
public func enqueue(_ job: UnownedJob) {
14+
job.runSynchronously(on: self.asUnownedSerialExecutor())
15+
}
16+
public func enqueue(_ job: __owned Job) {
17+
job.runSynchronously(on: self.asUnownedSerialExecutor())
18+
}
1319
public func enqueue(_ job: __owned ExecutorJob) {
1420
job.runSynchronously(on: self.asUnownedSerialExecutor())
1521
}

0 commit comments

Comments
 (0)