Skip to content

[SE-327] Remove need for convenience for delegating initializers of an actor. #41083

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions docs/SILInitializerConventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# SIL Initializer Conventions

A nominal type can define a number of initializers, some of which may
delegate initialization to another initializer. There are specific calling
conventions for these initializers within SIL that make up a part of the ABI
for a type. This document aims to summarize the key calling conventions for
these initializers.


# Structs and Enums

The delegation status for the initializer of a struct or enum is not encoded
in the definitions of these initializers. Thus, all of these initializers
have an implicit `metatype` argument for the instance to be passed in as the
last argument to the initializer. Using `<...>` as a stand-in for other
arguments that are part of the usual function calling convention, consider this
example:

```swift
// the non-delegating init MyStruct.init(final:)
sil hidden [ossa] @$s4test8MyStructV5finalACSi_tcfC : $@convention(method) (<...>, @thin MyStruct.Type) -> MyStruct {
bb0(<...>, %meta : $@thin MyStruct.Type):
%a = alloc_box ${ var MyStruct }, var, name "self"
%b = mark_uninitialized [rootself] %a : ${ var MyStruct }
%c = begin_borrow [lexical] %b : ${ var MyStruct }
%d = project_box %c : ${ var MyStruct }, 0

// ... initialize properties, etc ...

%end = load [trivial] %d : $*MyStruct
end_borrow %c : ${ var MyStruct }
destroy_value %b : ${ var MyStruct }
return %end : $MyStruct
}


// the delegating init MyStruct.init(delegates:)
sil hidden [ossa] @$s4test8MyStructV9delegatesACyt_tcfC : $@convention(method) (<...>, @thin MyStruct.Type) -> MyStruct {
bb0(<...>, %meta : $@thin MyStruct.Type):
// Same allocation as the non-delegating:
%a = alloc_box ${ var MyStruct }, var, name "self"
%b = mark_uninitialized [rootself] %a : ${ var MyStruct }
%c = begin_borrow [lexical] %b : ${ var MyStruct }
%d = project_box %c : ${ var MyStruct }, 0

// ... delegate to MyStruct.init(final:) ...

%ctor = function_ref @$s4test8MyStructV5finalACSi_tcfC : $@convention(method) (Int, @thin MyStruct.Type) -> MyStruct
%ret = apply %ctor(<...>, %meta) : $@convention(method) (Int, @thin MyStruct.Type) -> MyStruct

assign %ret to %d : $*MyStruct
%end = load [trivial] %d : $*MyStruct
end_borrow %c : ${ var MyStruct }
destroy_value %b : ${ var MyStruct }
return %end : $MyStruct
}
```

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 may perform allocation.

# Classes

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:

```swift
// MyClass.__allocating_init(final:)
sil hidden [exact_self_class] [ossa] @$s4test7MyClassC5finalACSi_tcfC : $@convention(method) (<...>, @thick MyClass.Type) -> @owned MyClass {
bb0(%0 : $Int, %1 : $@thick MyClass.Type):
%2 = alloc_ref $MyClass
// function_ref MyClass.init(final:)
%3 = function_ref @$s4test7MyClassC5finalACSi_tcfc : $@convention(method) (Int, @owned MyClass) -> @owned MyClass
%4 = apply %3(%0, %2) : $@convention(method) (Int, @owned MyClass) -> @owned MyClass // user: %5
return %4 : $MyClass
}

// MyClass.init(final:)
sil hidden [ossa] @$s4test7MyClassC5finalACSi_tcfc : $@convention(method) (Int, @owned MyClass) -> @owned MyClass {
bb0(<...>, %1 : @owned $MyClass):
%4 = mark_uninitialized [rootself] %1 : $MyClass

// ... initialize MyClass ...

%11 = copy_value %4 : $MyClass
destroy_value %4 : $MyClass
return %11 : $MyClass
}
```

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:

```swift
sil_vtable MyClass {
// ...
#MyClass.init!allocator: (MyClass.Type) -> (<...>) -> MyClass : @$s4test7MyClassC5finalACSi_tcfC // MyClass.__allocating_init(final:)
}
```

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:

```swift
class MyClass {
var x: Int
init(final x: Int) {
self.x = x
}
}

class MyDerivedClass: MyClass {
var y: Int
init(subFinal y: Int) {
self.y = y
super.init(final: y)
}
}
```

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:

```
// MyDerivedClass.__allocating_init(final:)
sil hidden [exact_self_class] [ossa] @$s4test14MyDerivedClassC5finalACSi_tcfC : $@convention(method) (Int, @thick MyDerivedClass.Type) -> @owned MyDerivedClass {
// ... calls $s4test14MyDerivedClassC5finalACSi_tcfc in the usual way ...
}

// MyDerivedClass.init(final:)
sil hidden [ossa] @$s4test14MyDerivedClassC5finalACSi_tcfc : $@convention(method) (Int, @owned MyDerivedClass) -> @owned MyDerivedClass {
bb0(%0 : $Int, %1 : @owned $MyDerivedClass):
%2 = alloc_box ${ var MyDerivedClass }, let, name "self"
%3 = mark_uninitialized [derivedself] %2 : ${ var MyDerivedClass }
%4 = begin_borrow [lexical] %3 : ${ var MyDerivedClass }
%5 = project_box %4 : ${ var MyDerivedClass }, 0
debug_value %0 : $Int, let, name "y", argno 1
store %1 to [init] %5 : $*MyDerivedClass

// ... initialize self.y ...

// perform the super call. notice the ownership transfer to the super.init.
%14 = load [take] %5 : $*MyDerivedClass
%15 = upcast %14 : $MyDerivedClass to $MyClass
// function_ref MyClass.init(final:)
%16 = function_ref @$s4test7MyClassC5finalACSi_tcfc : $@convention(method) (Int, @owned MyClass) -> @owned MyClass // user: %17
%17 = apply %16(%0, %15) : $@convention(method) (Int, @owned MyClass) -> @owned MyClass // user: %18
%18 = unchecked_ref_cast %17 : $MyClass to $MyDerivedClass
store %18 to [init] %5 : $*MyDerivedClass // id: %19

// return as usual
%20 = load [copy] %5 : $*MyDerivedClass
end_borrow %4 : ${ var MyDerivedClass }
destroy_value %3 : ${ var MyDerivedClass }
return %20 : $MyDerivedClass
}
```

# Actors

There does not exist a sub-actor that inherits from some other actor in the type
system. As a result, the `convenience` keyword is not required for actor
initializers in the source code. Without inheritance, only the allocating
entry-points can ever be used by an actor.

Nevertheless, internally the compiler will still differentiate between
convenience and designated initializers. So everything discussed
earlier for classes also apply to actors. The body of the initializer determines
whether the compiler internally treats it as `convenience` or not. For example,
an internally designated initializer for an actor still emits two entry-points,
but the initializing entrypoint is exclusively used by its corresponding
allocating entrypoint.



2 changes: 1 addition & 1 deletion include/swift/AST/DiagnosticsSema.def
Original file line number Diff line number Diff line change
Expand Up @@ -3572,7 +3572,7 @@ ERROR(designated_init_in_extension,none,
ERROR(cfclass_designated_init_in_extension,none,
"designated initializer cannot be declared in an extension of %0",
(DeclName))
ERROR(enumstruct_convenience_init,none,
ERROR(no_convenience_keyword_init,none,
"initializers in %0 are not marked with 'convenience'",
(StringRef))
ERROR(nonclass_convenience_init,none,
Expand Down
8 changes: 6 additions & 2 deletions lib/AST/ASTContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3066,10 +3066,14 @@ AnyFunctionType::Param swift::computeSelfParam(AbstractFunctionDecl *AFD,
}

// Convenience initializers have a dynamic 'self' in '-swift-version 5'.
//
// NOTE: it's important that we check if it's a convenience init only after
// confirming it's not semantically final, or else there can be a request
// evaluator cycle to determine the init kind for actors, which are final.
if (Ctx.isSwiftVersionAtLeast(5)) {
if (wantDynamicSelf && CD->isConvenienceInit())
if (wantDynamicSelf)
if (auto *classDecl = selfTy->getClassOrBoundGenericClass())
if (!classDecl->isSemanticallyFinal())
if (!classDecl->isSemanticallyFinal() && CD->isConvenienceInit())
isDynamicSelf = true;
}
} else if (isa<DestructorDecl>(AFD)) {
Expand Down
4 changes: 2 additions & 2 deletions lib/AST/Decl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3075,13 +3075,13 @@ void ValueDecl::setIsObjC(bool value) {
bool ValueDecl::isSemanticallyFinal() const {
// Actor types are semantically final.
if (auto classDecl = dyn_cast<ClassDecl>(this)) {
if (classDecl->isActor())
if (classDecl->isAnyActor())
return true;
}

// As are members of actor types.
if (auto classDecl = getDeclContext()->getSelfClassDecl()) {
if (classDecl->isActor())
if (classDecl->isAnyActor())
return true;
}

Expand Down
11 changes: 6 additions & 5 deletions lib/SIL/IR/SILDeclRef.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -669,13 +669,14 @@ IsSerialized_t SILDeclRef::isSerialized() const {
return IsSerialized;

// The allocating entry point for designated initializers are serialized
// if the class is @usableFromInline or public.
// if the class is @usableFromInline or public. Actors are excluded because
// whether the init is designated is not clearly reflected in the source code.
if (kind == SILDeclRef::Kind::Allocator) {
auto *ctor = cast<ConstructorDecl>(d);
if (ctor->isDesignatedInit() &&
ctor->getDeclContext()->getSelfClassDecl()) {
if (!ctor->hasClangNode())
return IsSerialized;
if (auto classDecl = ctor->getDeclContext()->getSelfClassDecl()) {
if (!classDecl->isAnyActor() && ctor->isDesignatedInit())
if (!ctor->hasClangNode())
return IsSerialized;
}
}

Expand Down
71 changes: 48 additions & 23 deletions lib/Sema/TypeCheckDecl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -379,40 +379,64 @@ CtorInitializerKind
InitKindRequest::evaluate(Evaluator &evaluator, ConstructorDecl *decl) const {
auto &diags = decl->getASTContext().Diags;

// Convenience inits are only allowed on classes and in extensions thereof.
if (decl->getAttrs().hasAttribute<ConvenienceAttr>()) {
if (auto nominal = decl->getDeclContext()->getSelfNominalTypeDecl()) {
auto classDecl = dyn_cast<ClassDecl>(nominal);

// Forbid convenience inits on Foreign CF types, as Swift does not yet
// support user-defined factory inits.
if (classDecl &&
classDecl->getForeignClassKind() == ClassDecl::ForeignKind::CFType) {
diags.diagnose(decl->getLoc(), diag::cfclass_convenience_init);
}
if (auto nominal = decl->getDeclContext()->getSelfNominalTypeDecl()) {

// Convenience inits are only allowed on classes and in extensions thereof.
if (auto convenAttr = decl->getAttrs().getAttribute<ConvenienceAttr>()) {
if (auto classDecl = dyn_cast<ClassDecl>(nominal)) {
if (classDecl->isAnyActor()) {
// For an actor "convenience" is not required, but we'll honor it.
diags.diagnose(decl->getLoc(),
diag::no_convenience_keyword_init, "actors")
.fixItRemove(convenAttr->getLocation())
.warnUntilSwiftVersion(6);

} else { // not an actor
// Forbid convenience inits on Foreign CF types, as Swift does not yet
// support user-defined factory inits.
if (classDecl->getForeignClassKind() == ClassDecl::ForeignKind::CFType)
diags.diagnose(decl->getLoc(), diag::cfclass_convenience_init);
}

if (!classDecl) {
auto ConvenienceLoc =
decl->getAttrs().getAttribute<ConvenienceAttr>()->getLocation();
} else { // not a ClassDecl
auto ConvenienceLoc = convenAttr->getLocation();

// Produce a tailored diagnostic for structs and enums.
// Produce a tailored diagnostic for structs and enums. They should
// not have `convenience`.
bool isStruct = dyn_cast<StructDecl>(nominal) != nullptr;
if (isStruct || dyn_cast<EnumDecl>(nominal)) {
diags.diagnose(decl->getLoc(), diag::enumstruct_convenience_init,
diags.diagnose(decl->getLoc(), diag::no_convenience_keyword_init,
isStruct ? "structs" : "enums")
.fixItRemove(ConvenienceLoc);
} else {
diags.diagnose(decl->getLoc(), diag::nonclass_convenience_init,
nominal->getName())
diags.diagnose(decl->getLoc(), diag::no_convenience_keyword_init,
nominal->getName().str())
.fixItRemove(ConvenienceLoc);
}
return CtorInitializerKind::Designated;
}

return CtorInitializerKind::Convenience;
}

return CtorInitializerKind::Convenience;
// if there is no `convenience` keyword...

// actors infer whether they are `convenience` from their body kind.
if (auto classDecl = dyn_cast<ClassDecl>(nominal)) {
if (classDecl->isAnyActor()) {
auto kind = decl->getDelegatingOrChainedInitKind();
switch (kind.initKind) {
case BodyInitKind::ImplicitChained:
case BodyInitKind::Chained:
case BodyInitKind::None:
return CtorInitializerKind::Designated;

case BodyInitKind::Delegating:
return CtorInitializerKind::Convenience;
}
}
}

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

if (decl->getDeclContext()->getExtendedProtocolDecl()) {
return CtorInitializerKind::Convenience;
}
// initializers in protocol extensions must be convenience inits
if (decl->getDeclContext()->getExtendedProtocolDecl()) {
return CtorInitializerKind::Convenience;
}

return CtorInitializerKind::Designated;
Expand Down
7 changes: 7 additions & 0 deletions lib/Sema/TypeCheckDeclPrimary.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2832,6 +2832,13 @@ class DeclChecker : public DeclVisitor<DeclChecker> {
return false;
}

// do not skip the body of an actor initializer.
// they are checked to determine delegation status
if (auto *ctor = dyn_cast<ConstructorDecl>(AFD))
if (auto *nom = ctor->getParent()->getSelfNominalTypeDecl())
if (nom->isAnyActor())
return false;

// Skipping all bodies won't serialize anything, so can skip regardless
if (getASTContext().TypeCheckerOpts.SkipFunctionBodies ==
FunctionBodySkipping::All)
Expand Down
1 change: 1 addition & 0 deletions lib/Sema/TypeCheckStmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1702,6 +1702,7 @@ static void checkClassConstructorBody(ClassDecl *classDecl,
ASTContext &ctx = classDecl->getASTContext();
bool wantSuperInitCall = false;
bool isDelegating = false;

auto initKindAndExpr = ctor->getDelegatingOrChainedInitKind();
switch (initKindAndExpr.initKind) {
case BodyInitKind::Delegating:
Expand Down
19 changes: 19 additions & 0 deletions test/Concurrency/Runtime/Inputs/MysteryInit.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

// This input is useful to ensure the delegation status of
// an actor's initializer does not affect ABI stability
public actor BigFoot {

public let name: String

private init(withName name: String) {
self.name = name
}

public init?() {
#if DELEGATES
self.init(withName: "Sasquatch")
#else
return nil
#endif
}
}
Loading