From beeb45def12fa063e6f7471d1ce2c0a45545f4fd Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 25 Aug 2025 16:00:21 -0700 Subject: [PATCH] [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. --- lib/IRGen/GenType.cpp | 25 +++- test/embedded/deinit-noncopyable-2.swift | 42 ++++++ test/embedded/deinit-noncopyable.swift | 164 +++++++++++++++++++++++ 3 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 test/embedded/deinit-noncopyable-2.swift create mode 100644 test/embedded/deinit-noncopyable.swift diff --git a/lib/IRGen/GenType.cpp b/lib/IRGen/GenType.cpp index 26dc3e4b5f277..0d3b16a77111a 100644 --- a/lib/IRGen/GenType.cpp +++ b/lib/IRGen/GenType.cpp @@ -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, diff --git a/test/embedded/deinit-noncopyable-2.swift b/test/embedded/deinit-noncopyable-2.swift new file mode 100644 index 0000000000000..f998e505da233 --- /dev/null +++ b/test/embedded/deinit-noncopyable-2.swift @@ -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 { + var value: T + init(_ value: consuming T) { + self.value = value + } +} + +struct ListNode: ~Copyable { + typealias Link = Box>? + + var element: Element + var next: Link +} + +struct List: ~Copyable { + typealias Link = Box>? + + 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() + myList.push(1) + let _ = consume myList +} + diff --git a/test/embedded/deinit-noncopyable.swift b/test/embedded/deinit-noncopyable.swift new file mode 100644 index 0000000000000..2aa2b448c1989 --- /dev/null +++ b/test/embedded/deinit-noncopyable.swift @@ -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: ~Copyable { + @usableFromInline + let buffer: UnsafeMutableBufferPointer + + 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(count: Int, body: (Int) throws(E) -> Element) throws(E) { + self.init(_uninitializedCount: count) + + for i in 0.. 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 { 0.. { + @_lifetime(borrow self) + borrowing get { + buffer.span + } + } + + /// Produce a mutable span covering all of the elements in the buffer. + public var mutableSpan: MutableSpan { + @_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(_ body: (UnsafeMutableBufferPointer) 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) { + self.init(_uninitializedCount: collection.count) + _ = buffer.initialize(fromContentsOf: collection) + } +} + +public enum BufferWrapper: ~Copyable { +case buffer(UniqueBuffer) +case empty +} + +extension BufferWrapper { + public init(repeating: Int, count: Int) { + self = .buffer(UniqueBuffer(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 + + 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..