MandatoryPerformanceOptimizations: support default methods for class existentials

For example:
```
protocol P: AnyObject {
  func foo()
}
extension P {
  func foo() {}
}
class C: P {}

let e: any P = C()
```

Such default methods are SILGen'd with a generic self argument. Therefore we need to specialize such witness methods, even if the conforming type is not generic.

rdar://145855851
This commit is contained in:
Erik Eckstein
2025-04-17 16:06:10 +02:00
parent 7a8a50a2b3
commit d222cf20f1
6 changed files with 242 additions and 66 deletions

View File

@@ -79,7 +79,7 @@ private struct VTableSpecializer {
}
private func specializeEntries(of vTable: VTable, _ notifyNewFunction: (Function) -> ()) -> [VTable.Entry] {
return vTable.entries.compactMap { entry in
return vTable.entries.map { entry in
if !entry.implementation.isGeneric {
return entry
}
@@ -91,8 +91,7 @@ private struct VTableSpecializer {
context.loadFunction(function: entry.implementation, loadCalleesRecursively: true),
let specializedMethod = context.specialize(function: entry.implementation, for: methodSubs) else
{
context.diagnosticEngine.diagnose(.non_final_generic_class_function, at: entry.methodDecl.location)
return nil
return entry
}
notifyNewFunction(specializedMethod)
@@ -106,16 +105,29 @@ private struct VTableSpecializer {
}
}
func specializeWitnessTable(forConformance conformance: Conformance,
errorLocation: Location,
/// Specializes a witness table of `conformance` for the concrete type of the conformance.
func specializeWitnessTable(for conformance: Conformance,
_ context: ModulePassContext,
_ notifyNewWitnessTable: (WitnessTable) -> ())
{
let genericConformance = conformance.genericConformance
guard let witnessTable = context.lookupWitnessTable(for: genericConformance) else {
if let existingSpecialization = context.lookupWitnessTable(for: conformance),
existingSpecialization.isSpecialized
{
return
}
let baseConf = conformance.isInherited ? conformance.inheritedConformance: conformance
if !baseConf.isSpecialized {
var visited = Set<Conformance>()
specializeDefaultMethods(for: conformance, visited: &visited, context, notifyNewWitnessTable)
return
}
guard let witnessTable = context.lookupWitnessTable(for: baseConf.genericConformance) else {
fatalError("no witness table found")
}
assert(witnessTable.isDefinition, "No witness table available")
let substitutions = baseConf.specializedSubstitutions
let newEntries = witnessTable.entries.map { origEntry in
switch origEntry {
@@ -125,13 +137,14 @@ func specializeWitnessTable(forConformance conformance: Conformance,
guard let origMethod = witness else {
return origEntry
}
let methodSubs = conformance.specializedSubstitutions.getMethodSubstitutions(for: origMethod)
let methodSubs = substitutions.getMethodSubstitutions(for: origMethod,
// Generic self types need to be handled specially (see `getMethodSubstitutions`)
selfType: origMethod.hasGenericSelf(context) ? conformance.type.canonical : nil)
guard !methodSubs.conformances.contains(where: {!$0.isValid}),
context.loadFunction(function: origMethod, loadCalleesRecursively: true),
let specializedMethod = context.specialize(function: origMethod, for: methodSubs) else
{
context.diagnosticEngine.diagnose(.cannot_specialize_witness_method, requirement, at: errorLocation)
return origEntry
}
return .method(requirement: requirement, witness: specializedMethod)
@@ -139,7 +152,7 @@ func specializeWitnessTable(forConformance conformance: Conformance,
let baseConf = context.getSpecializedConformance(of: witness,
for: conformance.type,
substitutions: conformance.specializedSubstitutions)
specializeWitnessTable(forConformance: baseConf, errorLocation: errorLocation, context, notifyNewWitnessTable)
specializeWitnessTable(for: baseConf, context, notifyNewWitnessTable)
return .baseProtocol(requirement: requirement, witness: baseConf)
case .associatedType(let requirement, let witness):
let substType = witness.subst(with: conformance.specializedSubstitutions)
@@ -150,15 +163,104 @@ func specializeWitnessTable(forConformance conformance: Conformance,
let concreteAssociateConf = conformance.getAssociatedConformance(ofAssociatedType: requirement.rawType,
to: assocConf.protocol)
if concreteAssociateConf.isSpecialized {
specializeWitnessTable(forConformance: concreteAssociateConf,
errorLocation: errorLocation,
context, notifyNewWitnessTable)
specializeWitnessTable(for: concreteAssociateConf, context, notifyNewWitnessTable)
}
return .associatedConformance(requirement: requirement,
witness: concreteAssociateConf)
}
}
let newWT = context.createWitnessTable(entries: newEntries,conformance: conformance,
linkage: .shared, serialized: false)
let newWT = context.createSpecializedWitnessTable(entries: newEntries,conformance: conformance,
linkage: .shared, serialized: false)
notifyNewWitnessTable(newWT)
}
/// Specializes the default methods of a non-generic witness table.
/// Default implementations (in protocol extentions) of non-generic protocol methods have a generic
/// self argument. Specialize such methods with the concrete type. Note that it is important to also
/// specialize inherited conformances so that the concrete self type is correct, even for derived classes.
private func specializeDefaultMethods(for conformance: Conformance,
visited: inout Set<Conformance>,
_ context: ModulePassContext,
_ notifyNewWitnessTable: (WitnessTable) -> ())
{
// Avoid infinite recursion, which may happen if an associated conformance is the conformance itself.
guard visited.insert(conformance).inserted,
let witnessTable = context.lookupWitnessTable(for: conformance.rootConformance)
else {
return
}
assert(witnessTable.isDefinition, "No witness table available")
var specialized = false
let newEntries = witnessTable.entries.map { origEntry in
switch origEntry {
case .invalid:
return WitnessTable.Entry.invalid
case .method(let requirement, let witness):
guard let origMethod = witness,
// Is it a generic method where only self is generic (= a default witness method)?
origMethod.isGeneric, origMethod.isNonGenericWitnessMethod(context)
else {
return origEntry
}
// Replace the generic self type with the concrete type.
let methodSubs = SubstitutionMap(genericSignature: origMethod.genericSignature,
replacementTypes: [conformance.type])
guard !methodSubs.conformances.contains(where: {!$0.isValid}),
context.loadFunction(function: origMethod, loadCalleesRecursively: true),
let specializedMethod = context.specialize(function: origMethod, for: methodSubs) else
{
return origEntry
}
specialized = true
return .method(requirement: requirement, witness: specializedMethod)
case .baseProtocol(_, let witness):
specializeDefaultMethods(for: witness, visited: &visited, context, notifyNewWitnessTable)
return origEntry
case .associatedType:
return origEntry
case .associatedConformance(_, let assocConf):
specializeDefaultMethods(for: assocConf, visited: &visited, context, notifyNewWitnessTable)
return origEntry
}
}
// If the witness table does not contain any default methods, there is no need to create a
// specialized witness table.
if specialized {
let newWT = context.createSpecializedWitnessTable(entries: newEntries,conformance: conformance,
linkage: .shared, serialized: false)
notifyNewWitnessTable(newWT)
}
}
private extension Function {
// True, if this is a non-generic method which might have a generic self argument.
// Default implementations (in protocol extentions) of non-generic protocol methods have a generic
// self argument.
func isNonGenericWitnessMethod(_ context: some Context) -> Bool {
switch loweredFunctionType.invocationGenericSignatureOfFunction.genericParameters.count {
case 0:
return true
case 1:
return hasGenericSelf(context)
default:
return false
}
}
// True, if the self argument is a generic parameter.
func hasGenericSelf(_ context: some Context) -> Bool {
let convention = FunctionConvention(for: loweredFunctionType,
hasLoweredAddresses: context.moduleHasLoweredAddresses)
if convention.hasSelfParameter,
let selfParam = convention.parameters.last,
selfParam.type.isGenericTypeParameter
{
return true
}
return false
}
}