[IRGen] Look for a specialized deinit when forming a call in IRGen

When outlining a destroy operation, we form direct calls to the deinit
of noncopyable types. For generic types, this was always calling into
unspecialized generics, which is... deeply unfortunate.

Look for a specialized deinit and call that instead. This eliminates a
compiler assertion in Embedded Swift, and should improve performance
with noncopyable generics elsewhere.

Fixes rdar://159054138 and #72627 / rdar://157131184.
This commit is contained in:
Doug Gregor
2025-08-25 16:00:21 -07:00
parent ae8e95d5d9
commit beeb45def1
3 changed files with 226 additions and 5 deletions

View File

@@ -26,6 +26,7 @@
#include "swift/Basic/Platform.h"
#include "swift/Basic/SourceManager.h"
#include "swift/IRGen/Linking.h"
#include "swift/SIL/GenericSpecializationMangler.h"
#include "swift/SIL/SILModule.h"
#include "llvm/IR/DerivedTypes.h"
#include "llvm/ADT/SmallString.h"
@@ -2937,11 +2938,27 @@ static bool tryEmitDeinitCall(IRGenFunction &IGF,
return true;
}
auto deinitSILFn = deinitTable->getImplementation();
// Look for a specialization of deinit that we can call.
auto substitutions = ty->getContextSubstitutionMap();
if (!substitutions.empty() &&
!substitutions.getRecursiveProperties().hasArchetype()) {
Mangle::GenericSpecializationMangler mangler(
deinitSILFn->getASTContext(), deinitSILFn,
deinitSILFn->getSerializedKind());
auto specializedName = mangler.mangleReabstracted(
substitutions, /*needAlternativeMangling=*/false);
auto specializedFn = IGF.IGM.getSILModule().lookUpFunction(specializedName);
if (specializedFn)
deinitSILFn = specializedFn;
}
// The deinit should take a single value parameter of the nominal type, either
// by @owned or indirect @in convention.
auto deinitFn = IGF.IGM.getAddrOfSILFunction(deinitTable->getImplementation(),
NotForDefinition);
auto deinitTy = deinitTable->getImplementation()->getLoweredFunctionType();
auto deinitFn = IGF.IGM.getAddrOfSILFunction(deinitSILFn, NotForDefinition);
auto deinitTy = deinitSILFn->getLoweredFunctionType();
auto deinitFP = FunctionPointer::forDirect(IGF.IGM, deinitFn,
nullptr, deinitTy);
assert(deinitTy->getNumParameters() == 1
@@ -2949,8 +2966,6 @@ static bool tryEmitDeinitCall(IRGenFunction &IGF,
&& !deinitTy->hasError()
&& "deinit should have only one parameter");
auto substitutions = ty->getContextSubstitutionMap();
CalleeInfo info(deinitTy,
deinitTy->substGenericArgs(IGF.getSILModule(),
substitutions,

View File

@@ -0,0 +1,42 @@
// RUN: %empty-directory(%t)
// RUN: %target-swift-frontend %s -g -enable-experimental-feature Embedded -c -o %t/main.o
// REQUIRES: swift_in_compiler
// REQUIRES: swift_feature_Embedded
// https://github.com/swiftlang/swift/issues/72627 - crash with noncopyable
// generic deinit.
final class Box<T: ~Copyable> {
var value: T
init(_ value: consuming T) {
self.value = value
}
}
struct ListNode<Element: ~Copyable>: ~Copyable {
typealias Link = Box<ListNode<Element>>?
var element: Element
var next: Link
}
struct List<Element: ~Copyable>: ~Copyable {
typealias Link = Box<ListNode<Element>>?
var head: Link = nil
init(head: consuming Link = nil) {
self.head = head
}
mutating func push(_ element: consuming Element) {
self = Self(head: Box(ListNode(element: element, next: self.head)))
}
}
public func main() {
var myList = List<Int>()
myList.push(1)
let _ = consume myList
}

View File

@@ -0,0 +1,164 @@
// RUN: %empty-directory(%t)
// RUN: split-file %s %t
// RUN: %target-swift-frontend %t/Library.swift -g -enable-experimental-feature Embedded -enable-experimental-feature Lifetimes -c -parse-as-library -o %t/Library.o -emit-module
// RUN: %target-swift-frontend -I %t %t/Application.swift -g -enable-experimental-feature Embedded -enable-experimental-feature Lifetimes -c -o %t/main.o
// RUN: %target-clang %target-clang-resource-dir-opt %t/main.o -o %t/a.out -dead_strip
// RUN: %target-run %t/a.out | %FileCheck %s
// REQUIRES: swift_in_compiler
// REQUIRES: executable_test
// REQUIRES: swift_feature_Embedded
// REQUIRES: swift_feature_Lifetimes
//--- Library.swift
@safe public struct UniqueBuffer<Element: ~Copyable>: ~Copyable {
@usableFromInline
let buffer: UnsafeMutableBufferPointer<Element>
private init(_uninitializedCount count: Int) {
buffer = UnsafeMutableBufferPointer.allocate(capacity: count)
}
@inline(__always)
@_alwaysEmitIntoClient
deinit {
buffer.deinitialize().deallocate()
}
/// Allocate a new buffer with `count` elements and call the given `body` function to produce an element
/// for each entry.
public init<E>(count: Int, body: (Int) throws(E) -> Element) throws(E) {
self.init(_uninitializedCount: count)
for i in 0..<count {
do throws(E) {
buffer[i] = try body(i)
} catch {
// The closure threw an error. We need to deinitialize every element we've initialized up to this point.
for j in 0 ..< i {
buffer.deinitializeElement(at: j)
}
throw error
}
}
}
/// The number of elements in the buffer.
public var count: Int { buffer.count }
/// Access the ith element in the buffer.
public subscript(_ i: Index) -> Element {
unsafeAddress {
precondition(i >= 0 && i < count)
return UnsafePointer(buffer.baseAddress! + i)
}
unsafeMutableAddress {
precondition(i >= 0 && i < count)
return buffer.baseAddress! + i
}
}
/// Index into this data structure.
public typealias Index = Int
/// Indices into this buffer.
public var indices: Range<Int> { 0..<count }
/// Produce a span covering all of the elements in the buffer.
public var span: Span<Element> {
@_lifetime(borrow self)
borrowing get {
buffer.span
}
}
/// Produce a mutable span covering all of the elements in the buffer.
public var mutableSpan: MutableSpan<Element> {
@_lifetime(&self)
mutating get {
buffer.mutableSpan
}
}
/// Run the body closure with an unsafe buffer pointer referencing the storage of this unique buffer.
///
/// Clients should prefer the `mutableSpan` property, which provides memory safety.
@unsafe public mutating func withUnsafeMutableBufferPointer<T, E>(_ body: (UnsafeMutableBufferPointer<Element>) throws(E) -> T) throws(E) -> T {
try body(buffer)
}
}
extension UniqueBuffer {
/// Allocate a buffer with `count` elements, all of which are a copy of `Element`.
public init(repeating element: Element, count: Int) {
self.init(_uninitializedCount: count)
buffer.initialize(repeating: element)
}
/// Allocate a buffer that contains a copy of the elements in the given collection.
public init(_ collection: some Collection<Element>) {
self.init(_uninitializedCount: collection.count)
_ = buffer.initialize(fromContentsOf: collection)
}
}
public enum BufferWrapper: ~Copyable {
case buffer(UniqueBuffer<Int>)
case empty
}
extension BufferWrapper {
public init(repeating: Int, count: Int) {
self = .buffer(UniqueBuffer<Int>(repeating: 17, count: 15))
}
public var count: Int {
switch self {
case .buffer(let unique): unique.count
case .empty: 0
}
}
public subscript(index: Int) -> Int {
switch self {
case .buffer(let unique): unique[index]
case .empty: fatalError("boom")
}
}
}
public struct BufferOfWrappers: ~Copyable {
let inner: UniqueBuffer<BufferWrapper>
public init() {
inner = UniqueBuffer(count: 17) { index in
.empty
}
}
public func countEm() -> Int {
return inner.count
}
}
//--- Application.swift
import Library
func test() {
let bufferWrapper = BufferWrapper(repeating: 17, count: 16)
var sum = 0
for i in 0..<bufferWrapper.count {
sum += bufferWrapper[i]
}
print(bufferWrapper.count)
print(sum)
let anotherBuffer = BufferOfWrappers()
print(anotherBuffer.countEm())
}
test()
// CHECK: 15
// CHECK: 17