Skip to content

Commit 7b54c03

Browse files
committed
Remove the need for convenience on actor inits to delegate.
This is possible because actors do not support inheritance. There is one specific exception to that rule, which is that an actor can inherit from `NSObject` just to support ObjC interop. This means an actor is effectively a final class. resolves rdar://87568153
1 parent 25b69d1 commit 7b54c03

10 files changed

+256
-49
lines changed

docs/SILInitializerConventions.md

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# SIL Initializer Conventions
2+
3+
A nominal type can define a number of initializers, some of which may
4+
delegate initialization to another initializer. There are specific calling
5+
conventions for these initializers within SIL that make up a part of the ABI
6+
for a type. This document aims to summarize the key calling conventions for
7+
these initializers.
8+
9+
10+
# Structs and Enums
11+
12+
The delegation status for the initializer of a struct or enum is not encoded
13+
in the definitions of these initializers. Thus, all of these initializers
14+
have an implicit `metatype` argument for the instance to be passed in as the last
15+
argument to the initializer. Using `<...>` as a stand-in for other arguments
16+
that are part of the usual function calling convention, consider this example:
17+
18+
```
19+
// the non-delegating init MyStruct.init(final:)
20+
sil hidden [ossa] @$s4test8MyStructV5finalACSi_tcfC : $@convention(method) (<...>, @thin MyStruct.Type) -> MyStruct {
21+
bb0(<...>, %meta : $@thin MyStruct.Type):
22+
%a = alloc_box ${ var MyStruct }, var, name "self"
23+
%b = mark_uninitialized [rootself] %a : ${ var MyStruct }
24+
%c = begin_borrow [lexical] %b : ${ var MyStruct }
25+
%d = project_box %c : ${ var MyStruct }, 0
26+
27+
// ... initialize properties, etc ...
28+
29+
%end = load [trivial] %d : $*MyStruct
30+
end_borrow %c : ${ var MyStruct }
31+
destroy_value %b : ${ var MyStruct }
32+
return %end : $MyStruct
33+
}
34+
35+
36+
// the delegating init MyStruct.init(delegates:)
37+
sil hidden [ossa] @$s4test8MyStructV9delegatesACyt_tcfC : $@convention(method) (<...>, @thin MyStruct.Type) -> MyStruct {
38+
bb0(<...>, %meta : $@thin MyStruct.Type):
39+
// Same allocation as the non-delegating:
40+
%a = alloc_box ${ var MyStruct }, var, name "self"
41+
%b = mark_uninitialized [rootself] %a : ${ var MyStruct }
42+
%c = begin_borrow [lexical] %b : ${ var MyStruct }
43+
%d = project_box %c : ${ var MyStruct }, 0
44+
45+
// ... delegate to MyStruct.init(final:) ...
46+
47+
%ctor = function_ref @$s4test8MyStructV5finalACSi_tcfC : $@convention(method) (Int, @thin MyStruct.Type) -> MyStruct
48+
%ret = apply %ctor(<...>, %meta) : $@convention(method) (Int, @thin MyStruct.Type) -> MyStruct
49+
50+
assign %ret to %d : $*MyStruct
51+
%end = load [trivial] %d : $*MyStruct
52+
end_borrow %c : ${ var MyStruct }
53+
destroy_value %b : ${ var MyStruct }
54+
return %end : $MyStruct
55+
}
56+
```
57+
58+
It's important to note that all initializers take a metadata argument, regardless of whether it is a delegating initializer. There is also no separation between allocating and non-allocating initializer entrypoints. All initializers perform allocation.
59+
60+
# Classes
61+
62+
Every designated initializer has two entry-points. One performs allocation (i.e., the "allocating" entry) before continuing at the second entrypoint which does the initialization (i.e., the "initializing" entrypoint). Here's an example of `MyClass.init(final:)`, which is a designated initializer, with its two entry-points:
63+
64+
```swift
65+
// MyClass.__allocating_init(final:)
66+
sil hidden [exact_self_class] [ossa] @$s4test7MyClassC5finalACSi_tcfC : $@convention(method) (<...>, @thick MyClass.Type) -> @owned MyClass {
67+
bb0(%0 : $Int, %1 : $@thick MyClass.Type):
68+
%2 = alloc_ref $MyClass
69+
// function_ref MyClass.init(final:)
70+
%3 = function_ref @$s4test7MyClassC5finalACSi_tcfc : $@convention(method) (Int, @owned MyClass) -> @owned MyClass
71+
%4 = apply %3(%0, %2) : $@convention(method) (Int, @owned MyClass) -> @owned MyClass // user: %5
72+
return %4 : $MyClass
73+
}
74+
75+
// MyClass.init(final:)
76+
sil hidden [ossa] @$s4test7MyClassC5finalACSi_tcfc : $@convention(method) (Int, @owned MyClass) -> @owned MyClass {
77+
bb0(<...>, %1 : @owned $MyClass):
78+
%4 = mark_uninitialized [rootself] %1 : $MyClass
79+
80+
// ... initialize MyClass ...
81+
82+
%11 = copy_value %4 : $MyClass
83+
destroy_value %4 : $MyClass
84+
return %11 : $MyClass
85+
}
86+
```
87+
88+
In the mangling of these entrypoint labels, the uppercase `C` suffix indicates that it's the allocating entrypoint, whereas the lowercase `c` is the initializing entrypoint. Only the allocating entrypoint is published in the type's vtable:
89+
90+
```
91+
sil_vtable MyClass {
92+
// ...
93+
#MyClass.init!allocator: (MyClass.Type) -> (<...>) -> MyClass : @$s4test7MyClassC5finalACSi_tcfC // MyClass.__allocating_init(final:)
94+
}
95+
```
96+
97+
The initializing entrypoint is only referenced by either it's corresponding allocating entrypoint, or by a sub-class that is delegating up in a `super.init` call. For example, if we had:
98+
99+
```swift
100+
class MyClass {
101+
var x: Int
102+
init(final x: Int) {
103+
self.x = x
104+
}
105+
}
106+
107+
class MyDerivedClass: MyClass {
108+
var y: Int
109+
init(subFinal y: Int) {
110+
self.y = y
111+
super.init(final: y)
112+
}
113+
}
114+
```
115+
116+
Then the `super.init(final: y)` call directly invokes `MyClass.init(final:)`'s initializing entrypoint, bypassing its allocating init. Here's what that looks like in SIL:
117+
118+
```
119+
// MyDerivedClass.__allocating_init(final:)
120+
sil hidden [exact_self_class] [ossa] @$s4test14MyDerivedClassC5finalACSi_tcfC : $@convention(method) (Int, @thick MyDerivedClass.Type) -> @owned MyDerivedClass {
121+
// ... calls $s4test14MyDerivedClassC5finalACSi_tcfc in the usual way ...
122+
}
123+
124+
// MyDerivedClass.init(final:)
125+
sil hidden [ossa] @$s4test14MyDerivedClassC5finalACSi_tcfc : $@convention(method) (Int, @owned MyDerivedClass) -> @owned MyDerivedClass {
126+
bb0(%0 : $Int, %1 : @owned $MyDerivedClass):
127+
%2 = alloc_box ${ var MyDerivedClass }, let, name "self"
128+
%3 = mark_uninitialized [derivedself] %2 : ${ var MyDerivedClass }
129+
%4 = begin_borrow [lexical] %3 : ${ var MyDerivedClass }
130+
%5 = project_box %4 : ${ var MyDerivedClass }, 0
131+
debug_value %0 : $Int, let, name "y", argno 1
132+
store %1 to [init] %5 : $*MyDerivedClass
133+
134+
// ... initialize self.y ...
135+
136+
// perform the super call. notice the ownership transfer to the super.init.
137+
%14 = load [take] %5 : $*MyDerivedClass
138+
%15 = upcast %14 : $MyDerivedClass to $MyClass
139+
// function_ref MyClass.init(final:)
140+
%16 = function_ref @$s4test7MyClassC5finalACSi_tcfc : $@convention(method) (Int, @owned MyClass) -> @owned MyClass // user: %17
141+
%17 = apply %16(%0, %15) : $@convention(method) (Int, @owned MyClass) -> @owned MyClass // user: %18
142+
%18 = unchecked_ref_cast %17 : $MyClass to $MyDerivedClass
143+
store %18 to [init] %5 : $*MyDerivedClass // id: %19
144+
145+
// return as usual
146+
%20 = load [copy] %5 : $*MyDerivedClass
147+
end_borrow %4 : ${ var MyDerivedClass }
148+
destroy_value %3 : ${ var MyDerivedClass }
149+
return %20 : $MyDerivedClass
150+
}
151+
```
152+
153+
# Actors
154+
155+
TODO: do we want to leverage the fact that the initializing entrypoint is only ever used by its allocating init, to merge them together? This effectively closes the door on actor inheritance, and really could just be an optimization done in the future.
156+
157+
TODO: Add a pointer or quick primer on the `mark_uninitialized` kinds. Here's a quick list from the SIL.rst:
158+
159+
```
160+
The kind of mark_uninitialized instruction specifies the type of data the mark_uninitialized instruction refers to:
161+
• var: designates the start of a normal variable live range
162+
• rootself: designates self in a struct, enum, or root class
163+
• crossmodulerootself: same as rootself, but in a case where it’s not really safe to treat self as a root because the original module might add more stored properties. This is only used for Swift 4 compatibility.
164+
• derivedself: designates self in a derived (non-root) class
165+
79
166+
• derivedselfonly: designates self in a derived (non-root) class whose stored properties have already been initialized
167+
• delegatingself: designates self on a struct, enum, or class in a delegating constructor (one that calls self.init)
168+
• delegatingselfallocated: designates self on a class convenience initializer’s initializing entry point
169+
```
170+
171+
TODO: I think we need to work on extending this in the compiler for actors so that, for example, `delegatingself` can correctly appear in a designated initializer, as it can for a struct.
172+
173+
174+

include/swift/AST/DiagnosticsSema.def

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3572,7 +3572,7 @@ ERROR(designated_init_in_extension,none,
35723572
ERROR(cfclass_designated_init_in_extension,none,
35733573
"designated initializer cannot be declared in an extension of %0",
35743574
(DeclName))
3575-
ERROR(enumstruct_convenience_init,none,
3575+
ERROR(no_convenience_keyword_init,none,
35763576
"initializers in %0 are not marked with 'convenience'",
35773577
(StringRef))
35783578
ERROR(nonclass_convenience_init,none,

lib/Sema/TypeCheckDecl.cpp

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -379,40 +379,64 @@ CtorInitializerKind
379379
InitKindRequest::evaluate(Evaluator &evaluator, ConstructorDecl *decl) const {
380380
auto &diags = decl->getASTContext().Diags;
381381

382-
// Convenience inits are only allowed on classes and in extensions thereof.
383-
if (decl->getAttrs().hasAttribute<ConvenienceAttr>()) {
384-
if (auto nominal = decl->getDeclContext()->getSelfNominalTypeDecl()) {
385-
auto classDecl = dyn_cast<ClassDecl>(nominal);
386-
387-
// Forbid convenience inits on Foreign CF types, as Swift does not yet
388-
// support user-defined factory inits.
389-
if (classDecl &&
390-
classDecl->getForeignClassKind() == ClassDecl::ForeignKind::CFType) {
391-
diags.diagnose(decl->getLoc(), diag::cfclass_convenience_init);
392-
}
382+
if (auto nominal = decl->getDeclContext()->getSelfNominalTypeDecl()) {
383+
384+
// Convenience inits are only allowed on classes and in extensions thereof.
385+
if (auto convenAttr = decl->getAttrs().getAttribute<ConvenienceAttr>()) {
386+
if (auto classDecl = dyn_cast<ClassDecl>(nominal)) {
387+
if (classDecl->isAnyActor()) {
388+
// For an actor "convenience" is not required, but we'll honor it.
389+
diags.diagnose(decl->getLoc(),
390+
diag::no_convenience_keyword_init, "actors")
391+
.fixItRemove(convenAttr->getLocation())
392+
.warnUntilSwiftVersion(6);
393+
394+
} else { // not an actor
395+
// Forbid convenience inits on Foreign CF types, as Swift does not yet
396+
// support user-defined factory inits.
397+
if (classDecl->getForeignClassKind() == ClassDecl::ForeignKind::CFType)
398+
diags.diagnose(decl->getLoc(), diag::cfclass_convenience_init);
399+
}
393400

394-
if (!classDecl) {
395-
auto ConvenienceLoc =
396-
decl->getAttrs().getAttribute<ConvenienceAttr>()->getLocation();
401+
} else { // not a ClassDecl
402+
auto ConvenienceLoc = convenAttr->getLocation();
397403

398-
// Produce a tailored diagnostic for structs and enums.
404+
// Produce a tailored diagnostic for structs and enums. They should
405+
// not have `convenience`.
399406
bool isStruct = dyn_cast<StructDecl>(nominal) != nullptr;
400407
if (isStruct || dyn_cast<EnumDecl>(nominal)) {
401-
diags.diagnose(decl->getLoc(), diag::enumstruct_convenience_init,
408+
diags.diagnose(decl->getLoc(), diag::no_convenience_keyword_init,
402409
isStruct ? "structs" : "enums")
403410
.fixItRemove(ConvenienceLoc);
404411
} else {
405-
diags.diagnose(decl->getLoc(), diag::nonclass_convenience_init,
406-
nominal->getName())
412+
diags.diagnose(decl->getLoc(), diag::no_convenience_keyword_init,
413+
nominal->getName().str())
407414
.fixItRemove(ConvenienceLoc);
408415
}
409416
return CtorInitializerKind::Designated;
410417
}
418+
419+
return CtorInitializerKind::Convenience;
411420
}
412421

413-
return CtorInitializerKind::Convenience;
422+
// if there is no `convenience` keyword...
423+
424+
// actors infer whether they are `convenience` from their body kind.
425+
if (auto classDecl = dyn_cast<ClassDecl>(nominal)) {
426+
if (classDecl->isAnyActor()) {
427+
auto kind = decl->getDelegatingOrChainedInitKind();
428+
switch (kind.initKind) {
429+
case BodyInitKind::ImplicitChained:
430+
case BodyInitKind::Chained:
431+
case BodyInitKind::None:
432+
return CtorInitializerKind::Designated;
433+
434+
case BodyInitKind::Delegating:
435+
return CtorInitializerKind::Convenience;
436+
}
437+
}
438+
}
414439

415-
} else if (auto nominal = decl->getDeclContext()->getSelfNominalTypeDecl()) {
416440
// A designated init for a class must be written within the class itself.
417441
//
418442
// This is because designated initializers of classes get a vtable entry,
@@ -437,10 +461,11 @@ InitKindRequest::evaluate(Evaluator &evaluator, ConstructorDecl *decl) const {
437461
return CtorInitializerKind::Convenience;
438462
}
439463
}
464+
} // end of Nominal context
440465

441-
if (decl->getDeclContext()->getExtendedProtocolDecl()) {
442-
return CtorInitializerKind::Convenience;
443-
}
466+
// initializers in protocol extensions must be convenience inits
467+
if (decl->getDeclContext()->getExtendedProtocolDecl()) {
468+
return CtorInitializerKind::Convenience;
444469
}
445470

446471
return CtorInitializerKind::Designated;

lib/Sema/TypeCheckDeclPrimary.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2832,6 +2832,13 @@ class DeclChecker : public DeclVisitor<DeclChecker> {
28322832
return false;
28332833
}
28342834

2835+
// do not skip the body of an actor initializer.
2836+
// they are checked to determine delegation status
2837+
if (auto *ctor = dyn_cast<ConstructorDecl>(AFD))
2838+
if (auto *nom = ctor->getParent()->getSelfNominalTypeDecl())
2839+
if (nom->isAnyActor())
2840+
return false;
2841+
28352842
// Skipping all bodies won't serialize anything, so can skip regardless
28362843
if (getASTContext().TypeCheckerOpts.SkipFunctionBodies ==
28372844
FunctionBodySkipping::All)

lib/Sema/TypeCheckStmt.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1702,6 +1702,7 @@ static void checkClassConstructorBody(ClassDecl *classDecl,
17021702
ASTContext &ctx = classDecl->getASTContext();
17031703
bool wantSuperInitCall = false;
17041704
bool isDelegating = false;
1705+
17051706
auto initKindAndExpr = ctor->getDelegatingOrChainedInitKind();
17061707
switch (initKindAndExpr.initKind) {
17071708
case BodyInitKind::Delegating:

test/Concurrency/actor_isolation.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -861,14 +861,14 @@ actor SomeActorWithInits {
861861
self.nonisolated()
862862
}
863863

864-
convenience init(i3: Bool) {
864+
convenience init(i3: Bool) { // expected-warning{{initializers in actors are not marked with 'convenience'; this is an error in Swift 6}}{{3-15=}}
865865
self.init(i1: i3)
866866
_ = mutableState // expected-error{{actor-isolated property 'mutableState' can not be referenced from a non-isolated context}}
867867
self.isolated() // expected-error{{actor-isolated instance method 'isolated()' can not be referenced from a non-isolated context}}
868868
self.nonisolated()
869869
}
870870

871-
convenience init(i4: Bool) async {
871+
convenience init(i4: Bool) async { // expected-warning{{initializers in actors are not marked with 'convenience'; this is an error in Swift 6}}{{3-15=}}
872872
self.init(i1: i4)
873873
_ = mutableState
874874
self.isolated()
@@ -894,14 +894,14 @@ actor SomeActorWithInits {
894894
_ = mutableState // will be caught by flow-isolation
895895
}
896896

897-
@MainActor convenience init(i7: Bool) {
897+
@MainActor convenience init(i7: Bool) { // expected-warning{{initializers in actors are not marked with 'convenience'; this is an error in Swift 6}}{{14-26=}}
898898
self.init(i1: i7)
899899
_ = mutableState // expected-error{{actor-isolated property 'mutableState' can not be referenced from the main actor}}
900900
self.isolated() // expected-error{{actor-isolated instance method 'isolated()' can not be referenced from the main actor}}
901901
self.nonisolated()
902902
}
903903

904-
@MainActor convenience init(i8: Bool) async {
904+
@MainActor convenience init(i8: Bool) async { // expected-warning{{initializers in actors are not marked with 'convenience'; this is an error in Swift 6}}{{14-26=}}
905905
self.init(i1: i8)
906906
_ = await mutableState
907907
await self.isolated()
@@ -1244,9 +1244,9 @@ actor Counter {
12441244

12451245
}
12461246

1247-
convenience init(material: Int) async {
1247+
init(material: Int) async {
12481248
self.init()
1249-
self.counter = 10 // FIXME: this should work, and also needs to work in definite initialization by injecting hops
1249+
self.counter = 10
12501250
}
12511251
}
12521252

@@ -1273,17 +1273,17 @@ actor Butterfly {
12731273

12741274
nonisolated init(async: Void) async {}
12751275

1276-
nonisolated convenience init(icecream: Void) { // expected-warning {{'nonisolated' on an actor's synchronous initializer is invalid; this is an error in Swift 6}} {{3-15=}}
1276+
nonisolated init(icecream: Void) { // expected-warning {{'nonisolated' on an actor's synchronous initializer is invalid; this is an error in Swift 6}} {{3-15=}}
12771277
self.init()
12781278
self.flapsPerSec += 1 // expected-error {{actor-isolated property 'flapsPerSec' can not be mutated from a non-isolated context}}
12791279
}
12801280

1281-
nonisolated convenience init(cookies: Void) async {
1281+
nonisolated init(cookies: Void) async {
12821282
self.init()
12831283
self.flapsPerSec += 1 // expected-error {{actor-isolated property 'flapsPerSec' can not be mutated from a non-isolated context}}
12841284
}
12851285

1286-
convenience init(brownies: Void) {
1286+
init(brownies: Void) {
12871287
self.init()
12881288
self.flapsPerSec = 0 // expected-error {{actor-isolated property 'flapsPerSec' can not be mutated from a non-isolated context}}
12891289
}

0 commit comments

Comments
 (0)