[embedded] Introduce class-bound existentials into Embedded Swift

Motivated by need for protocol-based dynamic dispatch, which hasn't been possible in Embedded Swift due to a full ban on existentials. This lifts that restriction but only for class-bound existentials: Class-bound existentials are already (even in desktop Swift) much more lightweight than full existentials, as they don't need type metadata, their containers are typically 2 words only (reference + wtable pointer), don't incur copies (only retains+releases).

Included in this PR:
[x] Non-generic class-bound existentials, executable tests for those.
[x] Extension methods on protocols and using those from a class-bound existential.
[x] RuntimeEffects now differentiate between Existential and ExistentialClassBound.
[x] PerformanceDiagnostics don't flag ExistentialClassBound in Embedded Swift.
[x] WTables are generated in IRGen when needed.

Left for follow-up PRs:
[ ] Generic classes support
This commit is contained in:
Kuba Mracek
2024-09-19 07:49:50 -07:00
parent c9c962bbd1
commit 6b9a3051e3
16 changed files with 306 additions and 32 deletions

View File

@@ -89,9 +89,23 @@ inline bool isEmbedded(CanType t) {
return t->getASTContext().LangOpts.hasFeature(Feature::Embedded);
}
// Metadata is not generated and not allowed to be referenced in Embedded Swift,
// expect for classes (both generic and non-generic), dynamic self, and
// class-bound existentials.
inline bool isMetadataAllowedInEmbedded(CanType t) {
return isa<ClassType>(t) || isa<BoundGenericClassType>(t) ||
isa<DynamicSelfType>(t);
if (isa<ClassType>(t) || isa<BoundGenericClassType>(t) ||
isa<DynamicSelfType>(t)) {
return true;
}
if (auto existentialTy = dyn_cast<ExistentialType>(t)) {
if (existentialTy->requiresClass())
return true;
}
if (auto archeTy = dyn_cast<ArchetypeType>(t)) {
if (archeTy->requiresClass())
return true;
}
return false;
}
inline bool isEmbedded(Decl *d) {
@@ -1062,7 +1076,10 @@ public:
}
static LinkEntity forProtocolWitnessTable(const RootProtocolConformance *C) {
assert(!isEmbedded(C));
if (isEmbedded(C)) {
assert(C->getProtocol()->requiresClass());
}
LinkEntity entity;
entity.setForProtocolConformance(Kind::ProtocolWitnessTable, C);
return entity;

View File

@@ -55,6 +55,9 @@ enum class RuntimeEffect : unsigned {
/// Witness methods, boxing, unboxing, initializing, etc.
Existential = 0x80,
/// Class-bound only existential
ExistentialClassBound = 0x200,
/// Not modelled currently.
Concurrency = 0x0,

View File

@@ -1142,8 +1142,9 @@ void IRGenModule::emitGlobalLists() {
// Eagerly emit functions that are externally visible. Functions that are
// dynamic replacements must also be eagerly emitted.
static bool isLazilyEmittedFunction(SILFunction &f, SILModule &m) {
// Embedded Swift only emits specialized function, so don't emit generic
// functions, even if they're externally visible.
// Embedded Swift only emits specialized function (except when they are
// protocol witness methods). So don't emit generic functions, even if they're
// externally visible.
if (f.getASTContext().LangOpts.hasFeature(Feature::Embedded) &&
f.getLoweredFunctionType()->getSubstGenericSignature()) {
return true;
@@ -1332,7 +1333,7 @@ void IRGenerator::emitLazyDefinitions() {
assert(LazyFieldDescriptors.empty());
// LazyFunctionDefinitions are allowed, but they must not be generic
for (SILFunction *f : LazyFunctionDefinitions) {
assert(!f->isGeneric());
assert(hasValidSignatureForEmbedded(f));
}
assert(LazyWitnessTables.empty());
assert(LazyCanonicalSpecializedMetadataAccessors.empty());
@@ -1482,7 +1483,7 @@ void IRGenerator::addLazyFunction(SILFunction *f) {
// Embedded Swift doesn't expect any generic functions to be referenced.
if (SIL.getASTContext().LangOpts.hasFeature(Feature::Embedded)) {
assert(!f->isGeneric());
assert(hasValidSignatureForEmbedded(f));
}
assert(!FinishedEmittingLazyDefinitions);
@@ -3473,6 +3474,24 @@ llvm::CallBase *swift::irgen::emitCXXConstructorCall(
return result;
}
// For a SILFunction to be legal in Embedded Swift, it must be either
// - non-generic
// - generic with parameters thar are either
// - fully specialized (concrete)
// - a class-bound archetype (class-bound existential)
bool swift::irgen::hasValidSignatureForEmbedded(SILFunction *f) {
auto s = f->getLoweredFunctionType()->getInvocationGenericSignature();
for (auto genParam : s.getGenericParams()) {
auto mappedParam = f->getGenericEnvironment()->mapTypeIntoContext(genParam);
if (auto archeTy = dyn_cast<ArchetypeType>(mappedParam)) {
if (archeTy->requiresClass())
continue;
}
return false;
}
return true;
}
StackProtectorMode IRGenModule::shouldEmitStackProtector(SILFunction *f) {
const SILOptions &opts = IRGen.SIL.getOptions();
return (opts.EnableStackProtection && f->needsStackProtection()) ?
@@ -4352,6 +4371,9 @@ static bool conformanceIsVisibleViaMetadata(
void IRGenModule::addProtocolConformance(ConformanceDescription &&record) {
if (Context.LangOpts.hasFeature(Feature::Embedded)) {
return;
}
emitProtocolConformance(record);

View File

@@ -76,6 +76,8 @@ namespace irgen {
llvm::FunctionType *ctorFnType,
llvm::Constant *ctorAddress,
llvm::ArrayRef<llvm::Value *> args);
bool hasValidSignatureForEmbedded(SILFunction *f);
}
}

View File

@@ -1791,11 +1791,6 @@ static void forEachProtocolWitnessTable(
assert(protocols.size() == witnessConformances.size() &&
"mismatched protocol conformances");
// Don't emit witness tables in embedded Swift.
if (srcType->getASTContext().LangOpts.hasFeature(Feature::Embedded)) {
return;
}
for (unsigned i = 0, e = protocols.size(); i < e; ++i) {
assert(protocols[i] == witnessConformances[i].getRequirement());
auto table = emitWitnessTableRef(IGF, srcType, srcMetadataCache,

View File

@@ -1512,6 +1512,13 @@ public:
/// Add reference to the protocol conformance descriptor that generated
/// this table.
void addProtocolConformanceDescriptor() {
// In Embedded Swift, there are no protocol conformance descriptors. Emit
// a null pointer instead to keep the same layout as regular Swift.
if (IGM.Context.LangOpts.hasFeature(Feature::Embedded)) {
Table.addNullPointer(IGM.Int8PtrTy);
return;
}
auto descriptor =
IGM.getAddrOfProtocolConformanceDescriptor(&Conformance);
if (isRelative)
@@ -2560,6 +2567,8 @@ static void addWTableTypeMetadata(IRGenModule &IGM,
void IRGenModule::emitSILWitnessTable(SILWitnessTable *wt) {
if (Context.LangOpts.hasFeature(Feature::Embedded)) {
// In Embedded Swift, only class-bound wtables are allowed.
if (!wt->getConformance()->getProtocol()->requiresClass())
return;
}
@@ -3560,9 +3569,13 @@ llvm::Value *irgen::emitWitnessTableRef(IRGenFunction &IGF,
CanType srcType,
llvm::Value **srcMetadataCache,
ProtocolConformanceRef conformance) {
assert(!srcType->getASTContext().LangOpts.hasFeature(Feature::Embedded));
auto proto = conformance.getRequirement();
// In Embedded Swift, only class-bound wtables are allowed.
if (srcType->getASTContext().LangOpts.hasFeature(Feature::Embedded)) {
assert(proto->requiresClass());
}
assert(Lowering::TypeConverter::protocolRequiresWitnessTable(proto)
&& "protocol does not have witness tables?!");

View File

@@ -1407,7 +1407,10 @@ bool IRGenerator::canEmitWitnessTableLazily(SILWitnessTable *wt) {
}
void IRGenerator::addLazyWitnessTable(const ProtocolConformance *Conf) {
assert(!SIL.getASTContext().LangOpts.hasFeature(Feature::Embedded));
// In Embedded Swift, only class-bound wtables are allowed.
if (SIL.getASTContext().LangOpts.hasFeature(Feature::Embedded)) {
assert(Conf->getProtocol()->requiresClass());
}
if (auto *wt = SIL.lookUpWitnessTable(Conf)) {
// Add it to the queue if it hasn't already been put there.

View File

@@ -2478,10 +2478,6 @@ void IRGenModule::emitSILFunction(SILFunction *f) {
if (f->isExternalDeclaration())
return;
if (Context.LangOpts.hasFeature(Feature::Embedded) &&
f->getLoweredFunctionType()->isPolymorphic())
return;
// Do not emit bodies of public_external or package_external functions.
if (hasPublicOrPackageVisibility(f->getLinkage(),
f->getASTContext().SILOpts.EnableSerializePackage) &&

View File

@@ -671,9 +671,13 @@ RuntimeEffect swift::getRuntimeEffect(SILInstruction *inst, SILType &impactType)
RuntimeEffect::MetaData | RuntimeEffect::Existential;
case SILInstructionKind::InitExistentialRefInst:
impactType = inst->getOperand(0)->getType();
return RuntimeEffect::MetaData | RuntimeEffect::ExistentialClassBound;
case SILInstructionKind::InitExistentialMetatypeInst:
impactType = inst->getOperand(0)->getType();
return RuntimeEffect::MetaData | RuntimeEffect::Existential;
case SILInstructionKind::ObjCToThickMetatypeInst:
impactType = inst->getOperand(0)->getType();
return RuntimeEffect::MetaData;
@@ -693,14 +697,8 @@ RuntimeEffect swift::getRuntimeEffect(SILInstruction *inst, SILType &impactType)
return RuntimeEffect::Existential;
case SILInstructionKind::OpenExistentialRefInst: {
SILType opType = cast<OpenExistentialRefInst>(inst)->getOperand()->getType();
impactType = opType;
if (opType.getASTType()->isObjCExistentialType()) {
return RuntimeEffect::MetaData | RuntimeEffect::Existential;
}
return RuntimeEffect::MetaData | RuntimeEffect::Existential;
// TODO: should be Existential
//return RuntimeEffect::Existential;
impactType = inst->getOperand(0)->getType();
return RuntimeEffect::MetaData | RuntimeEffect::ExistentialClassBound;
}
case SILInstructionKind::UnconditionalCheckedCastInst:
@@ -963,7 +961,7 @@ RuntimeEffect swift::getRuntimeEffect(SILInstruction *inst, SILType &impactType)
rt |= RuntimeEffect::ObjectiveC | RuntimeEffect::MetaData;
break;
case SILFunctionTypeRepresentation::WitnessMethod:
rt |= RuntimeEffect::MetaData | RuntimeEffect::Existential;
rt |= RuntimeEffect::MetaData; // ???
break;
case SILFunctionTypeRepresentation::CFunctionPointer:
case SILFunctionTypeRepresentation::CXXMethod:

View File

@@ -534,7 +534,8 @@ bool PerformanceDiagnostics::visitInst(SILInstruction *inst,
LocWithParent loc(inst->getLoc().getSourceLoc(), parentLoc);
if (perfConstr == PerformanceConstraints::NoExistentials &&
(impact & RuntimeEffect::Existential)) {
((impact & RuntimeEffect::Existential) ||
(impact & RuntimeEffect::ExistentialClassBound))) {
PrettyStackTracePerformanceDiagnostics stackTrace("existential", inst);
if (impactType) {
diagnose(loc, diag::perf_diag_existential_type, impactType.getASTType());
@@ -556,6 +557,8 @@ bool PerformanceDiagnostics::visitInst(SILInstruction *inst,
}
if (module.getOptions().EmbeddedSwift) {
// Explicitly don't detect RuntimeEffect::ExistentialClassBound - those are
// allowed in Embedded Swift.
if (impact & RuntimeEffect::Existential) {
PrettyStackTracePerformanceDiagnostics stackTrace("existential", inst);
if (impactType) {

View File

@@ -457,7 +457,13 @@ void addFunctionPasses(SILPassPipelinePlan &P,
P.addMem2Reg();
// Run the existential specializer Pass.
if (!P.getOptions().EmbeddedSwift) {
// MandatoryPerformanceOptimizations already took care of all specializations
// in embedded Swift mode, running the existential specializer might introduce
// more generic calls from non-generic functions, which breaks the assumptions
// of embedded Swift.
P.addExistentialSpecializer();
}
// Cleanup, which is important if the inliner has restarted the pass pipeline.
P.addPerformanceConstantPropagation();

View File

@@ -0,0 +1,43 @@
// RUN: %target-run-simple-swift(-enable-experimental-feature Embedded -parse-as-library -wmo) | %FileCheck %s
// RUN: %target-run-simple-swift(-enable-experimental-feature Embedded -parse-as-library -wmo -O) | %FileCheck %s
// RUN: %target-run-simple-swift(-enable-experimental-feature Embedded -parse-as-library -wmo -Osize) | %FileCheck %s
// REQUIRES: swift_in_compiler
// REQUIRES: executable_test
// REQUIRES: optimized_stdlib
// REQUIRES: OS=macosx || OS=linux-gnu
protocol ClassBound: AnyObject {
func foo()
func bar()
}
class MyClass {}
extension MyClass: ClassBound {
func foo() { print("MyClass.foo()") }
func bar() { print("MyClass.bar()") }
}
class MyOtherClass {}
extension MyOtherClass: ClassBound {
func foo() { print("MyOtherClass.foo()") }
func bar() { print("MyOtherClass.bar()") }
}
func test(existential: any ClassBound) {
existential.foo()
existential.bar()
}
@main
struct Main {
static func main() {
test(existential: MyClass())
// CHECK: MyClass.foo()
// CHECK: MyClass.bar()
test(existential: MyOtherClass())
// CHECK: MyOtherClass.foo()
// CHECK: MyOtherClass.bar()
}
}

View File

@@ -0,0 +1,50 @@
// RUN: %target-run-simple-swift(-enable-experimental-feature Embedded -parse-as-library -wmo) | %FileCheck %s
// REQUIRES: swift_in_compiler
// REQUIRES: executable_test
// REQUIRES: optimized_stdlib
// REQUIRES: OS=macosx || OS=linux-gnu
protocol ClassBound: AnyObject {
func foo()
func bar()
}
extension ClassBound {
func extensionMethod() {
self.foo()
self.bar()
}
}
class MyClass {}
extension MyClass: ClassBound {
func foo() { print("MyClass.foo()") }
func bar() { print("MyClass.bar()") }
}
class MyOtherClass {}
extension MyOtherClass: ClassBound {
func foo() { print("MyOtherClass.foo()") }
func bar() { print("MyOtherClass.bar()") }
}
@main
struct Main {
static func main() {
var array: [any ClassBound] = []
array.append(MyClass())
array.append(MyOtherClass())
for e in array {
e.extensionMethod()
}
// CHECK: MyClass.foo()
// CHECK: MyClass.bar()
// CHECK: MyOtherClass.foo()
// CHECK: MyOtherClass.bar()
}
}

View File

@@ -0,0 +1,44 @@
// RUN: %target-run-simple-swift(-enable-experimental-feature Embedded -parse-as-library -wmo) | %FileCheck %s
// REQUIRES: swift_in_compiler
// REQUIRES: executable_test
// REQUIRES: optimized_stdlib
// REQUIRES: OS=macosx || OS=linux-gnu
protocol ClassBound: AnyObject {
func foo()
func bar()
}
class MyClass {}
extension MyClass: ClassBound {
func foo() { print("MyClass.foo()") }
func bar() { print("MyClass.bar()") }
}
class MyOtherClass {}
extension MyOtherClass: ClassBound {
func foo() { print("MyOtherClass.foo()") }
func bar() { print("MyOtherClass.bar()") }
}
@main
struct Main {
static func main() {
var array: [any ClassBound] = []
array.append(MyClass())
array.append(MyOtherClass())
for e in array {
e.foo()
e.bar()
}
// CHECK: MyClass.foo()
// CHECK: MyClass.bar()
// CHECK: MyOtherClass.foo()
// CHECK: MyOtherClass.bar()
}
}

View File

@@ -0,0 +1,44 @@
// RUN: %target-run-simple-swift(-enable-experimental-feature Embedded -parse-as-library -wmo) | %FileCheck %s
// REQUIRES: swift_in_compiler
// REQUIRES: executable_test
// REQUIRES: optimized_stdlib
// REQUIRES: OS=macosx || OS=linux-gnu
// Generic classes don't work yet.
// XFAIL: *
protocol ClassBound: AnyObject {
func foo()
func bar()
}
class MyGenericClass<T> {
var typ: String
init(typ: String) { self.typ = typ }
}
extension MyGenericClass: ClassBound {
func foo() { print("MyGenericClass<\(typ)>.foo()") }
func bar() { print("MyGenericClass<\(typ)>.bar()") }
}
@main
struct Main {
static func main() {
var array: [any ClassBound] = []
array.append(MyGenericClass<Int>(typ: "Int"))
array.append(MyGenericClass<String>(typ: "String"))
for e in array {
e.foo()
e.bar()
}
// CHECK: MyGenericClass<Int>.foo()
// CHECK: MyGenericClass<Int>.bar()
// CHECK: MyGenericClass<String>.foo()
// CHECK: MyGenericClass<String>.bar()
}
}

View File

@@ -0,0 +1,35 @@
// RUN: %target-swift-emit-ir -parse-as-library -module-name main -verify %s -enable-experimental-feature Embedded -wmo
// REQUIRES: swift_in_compiler
// REQUIRES: optimized_stdlib
// REQUIRES: OS=macosx || OS=linux-gnu
protocol ClassBound: AnyObject {
func foo()
}
protocol NotClassBound {
func foo()
}
class MyClass {}
extension MyClass: ClassBound, NotClassBound {
func foo() { print("MyClass.foo()") }
}
func test(existential: any ClassBound) {
existential.foo() // ok
}
func test(existential: any NotClassBound) {
existential.foo() // expected-error {{cannot use a value of protocol type in embedded Swift}}
// expected-note@+3 {{called from here}}
}
@main
struct Main {
static func main() {
test(existential: MyClass() as (any ClassBound)) // ok
test(existential: MyClass() as (any NotClassBound)) // expected-error {{cannot use a value of protocol type 'any NotClassBound' in embedded Swift}}
}
}