diff --git a/CLAUDE.md b/CLAUDE.md index bef9e098..c4426e65 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,8 +8,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co # Build the project swift build -# Run all tests -swift test +# Run all tests (skip IntegrationTests — see note below) +swift test --skip IntegrationTests # Run specific test suites swift test --filter DemanglingTests @@ -27,6 +27,8 @@ swift run swift-section interface /path/to/binary Requires Swift 6.2+ / Xcode 26.0+. +**Test suite convention:** `Tests/IntegrationTests/` is for the maintainer's manual inspection only — it prints results with no assertions or preconditions. Agents must not run it (use `--skip IntegrationTests` when running the full suite). All other `*Tests` targets have proper assertions and required preconditions, and are safe to run. + ## Architecture Overview This is a Swift library for parsing Mach-O files to extract Swift metadata (types, protocols, conformances). It uses a custom Demangler to parse symbolic references and restore Swift Runtime logic. diff --git a/Package.swift b/Package.swift index 5b2ebcac..88e12454 100644 --- a/Package.swift +++ b/Package.swift @@ -145,7 +145,7 @@ var dependencies: [Package.Dependency] = [ .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.9.4"), .package(url: "https://github.com/MxIris-DeveloperTool/swift-clang", from: "0.2.0"), .package(url: "https://github.com/MxIris-DeveloperTool/swift-apinotes", from: "0.1.0"), - .package(url: "https://github.com/MxIris-Reverse-Engineering/swift-dyld-private", from: "1.1.0"), + .package(url: "https://github.com/MxIris-Reverse-Engineering/swift-dyld-private", from: "1.2.0"), // CLI .package(url: "https://github.com/onevcat/Rainbow", from: "4.0.0"), @@ -454,6 +454,9 @@ extension Target { .target(.SwiftInspection), .target(.SwiftDump), .target(.Utilities), + ], + exclude: [ + "GenericSpecializer/REVIEW_FIXUPS.md", ] ) @@ -596,6 +599,14 @@ extension Target { swiftSettings: testSettings ) + static let MachOCachesTests = Target.testTarget( + name: "MachOCachesTests", + dependencies: [ + .target(.MachOCaches), + ], + swiftSettings: testSettings + ) + static let SwiftInspectionTests = Target.testTarget( name: "SwiftInspectionTests", dependencies: [ @@ -723,11 +734,12 @@ let package = Package( .RegenerateBaselinesPlugin, // Testing - .MachOSymbolsTests, +// .MachOSymbolsTests, .MachOSwiftSectionTests, + .MachOCachesTests, .SwiftInspectionTests, .SwiftDumpTests, - .TypeIndexingTests, +// .TypeIndexingTests, .SwiftInterfaceTests, .MachOTestingSupportTests, .IntegrationTests, diff --git a/Sources/MachOCaches/SharedCache.swift b/Sources/MachOCaches/SharedCache.swift index 38470489..b5acdaf5 100644 --- a/Sources/MachOCaches/SharedCache.swift +++ b/Sources/MachOCaches/SharedCache.swift @@ -20,8 +20,27 @@ open class SharedCache: @unchecked Sendable { memoryPressureMonitor.startMonitoring() } + /// Per-key state: a finished build (`completed`) or an in-flight build + /// that other callers can join via the promise (`inFlight`). The + /// in-flight marker lets concurrent callers for the same key share one + /// build instead of serializing against the cache lock for the build's + /// entire duration. + private enum Entry { + case completed(Storage) + case inFlight(SharedCacheBuildPromise) + } + @Mutex - private var storageByIdentifier: [AnyHashable: Storage] = [:] + private var storageByIdentifier: [AnyHashable: Entry] = [:] + + /// Routing decision the cache lock makes on behalf of `storage(...)`: + /// either return a cached value, await someone else's in-flight build, + /// or run the build ourselves under the freshly-installed promise. + private enum Outcome { + case completed(Storage) + case wait(SharedCacheBuildPromise) + case build(SharedCacheBuildPromise) + } open func buildStorage(for machO: some MachORepresentableWithCache) -> Storage? { return nil @@ -35,33 +54,24 @@ open class SharedCache: @unchecked Sendable { /// Atomic get-or-build with a caller-provided build closure. /// - /// Unlike `storage(in:)` which uses the overridden `buildStorage(for:)`, this variant - /// lets the caller inject a custom build closure that can capture per-call context - /// (progress continuations, options, etc). The per-call context flows through closure - /// capture, never through shared instance state, so concurrent calls cannot interfere. - /// - /// The entire check-build-insert runs inside a single `withLockUnchecked` critical - /// section, guaranteeing atomicity independent of the `_modify` accessor that - /// `@Mutex` may or may not generate for the underlying property. + /// Unlike `storage(in:)` which uses the overridden `buildStorage(for:)`, + /// this variant lets the caller inject a custom build closure that can + /// capture per-call context (progress continuations, options, etc.). The + /// per-call context flows through closure capture, never through shared + /// instance state, so concurrent calls cannot interfere. /// - /// - Note: Known limitation — the `build` closure executes while the global - /// cache lock is held, so a long-running build (for example - /// `SymbolIndexStore.prepareWithProgress`) blocks concurrent cache access for - /// other Mach-O identifiers for the full duration of the build. Acceptable - /// today because concurrent cache construction is rare in practice. See - /// `KNOWN_ISSUES.md` for the tracking entry and the sketch of a per-key - /// promise-based fix. + /// The cache lock is held only long enough to look up the key and either + /// hand back a finished value, attach to an in-flight build, or install + /// our own in-flight marker. The build closure itself executes + /// **outside** the lock, so two callers building entries for different + /// Mach-O identifiers run in parallel; two callers building entries for + /// the same identifier de-duplicate via the in-flight promise. public func storage( in machO: MachO, buildUsing build: (MachO) -> Storage? ) -> Storage? { - return _storageByIdentifier.withLockUnchecked { dict -> Storage? in - let key: AnyHashable = machO.identifier - if let existing = dict[key] { return existing } - guard let new = build(machO) else { return nil } - dict[key] = new - return new - } + let key: AnyHashable = machO.identifier + return resolve(key: key) { build(machO) } } private var currentIdentifer: ObjectIdentifier { @@ -73,12 +83,62 @@ open class SharedCache: @unchecked Sendable { } open func storage() -> Storage? { - return _storageByIdentifier.withLockUnchecked { dict -> Storage? in - let key: AnyHashable = currentIdentifer - if let existing = dict[key] { return existing } - guard let new = buildStorage() else { return nil } - dict[key] = new - return new + let key: AnyHashable = currentIdentifer + return resolve(key: key) { buildStorage() } + } + + /// Shared core for both `storage(in:buildUsing:)` and `storage()`. Holds + /// the cache lock only across the dictionary lookup / marker install and + /// across the post-build dictionary update — the actual `build` call + /// runs unsynchronized so that concurrent builds for distinct keys don't + /// serialize. + /// + /// `package`-visible so the in-package test target can exercise the + /// concurrency contract directly without manufacturing a fake + /// `MachORepresentableWithCache` conformer. + package func resolve(key: AnyHashable, build: () -> Storage?) -> Storage? { + let outcome: Outcome = _storageByIdentifier.withLockUnchecked { dict in + if let entry = dict[key] { + switch entry { + case .completed(let storage): + return .completed(storage) + case .inFlight(let promise): + return .wait(promise) + } + } + let promise = SharedCacheBuildPromise() + dict[key] = .inFlight(promise) + return .build(promise) + } + + switch outcome { + case .completed(let storage): + return storage + case .wait(let promise): + return promise.wait() + case .build(let promise): + let result = build() + _storageByIdentifier.withLockUnchecked { dict in + // Only publish back if our promise is still the in-flight + // marker. `removeAll()` on memory pressure could have + // cleared the dict mid-build, and a fresh caller may have + // installed a different promise; in either case the dict is + // not ours to write — but our promise still has waiters + // attached, so we always call `fulfill(_:)` below. + if case .inFlight(let installed) = dict[key], installed === promise { + if let storage = result { + dict[key] = .completed(storage) + } else { + // Mirror the original behaviour: a build that returns + // `nil` is not cached, so subsequent callers get a + // fresh attempt instead of being permanently stuck on + // the failure. + dict[key] = nil + } + } + } + promise.fulfill(result) + return result } } } diff --git a/Sources/MachOCaches/SharedCacheBuildPromise.swift b/Sources/MachOCaches/SharedCacheBuildPromise.swift new file mode 100644 index 00000000..99bdf656 --- /dev/null +++ b/Sources/MachOCaches/SharedCacheBuildPromise.swift @@ -0,0 +1,53 @@ +import Foundation + +/// Sync rendezvous used by ``SharedCache/storage(in:buildUsing:)`` to share +/// one in-flight build between concurrent callers for the same identifier. +/// +/// The first caller installs an instance of this promise as the cache's +/// in-flight marker for the key, releases the cache lock, and runs the build +/// closure unsynchronized. Concurrent callers for the same key find the +/// in-flight marker, release the cache lock, and block in ``wait()`` on +/// `NSCondition`. The builder calls ``fulfill(_:)`` once the work is done, +/// `broadcast()` wakes every waiter at once, and `wait()` returns the same +/// value to all of them. +/// +/// `final class` because the same instance must be referenced from the cache +/// dictionary and from every waiter; `NSCondition` also requires a stable +/// memory address. `@unchecked Sendable` because the synchronization is +/// provided by `NSCondition`, not by Swift's data-race detector. +@_spi(Internals) +public final class SharedCacheBuildPromise: @unchecked Sendable { + private let condition = NSCondition() + /// Outer optional encodes "fulfilled yet?"; inner optional matches + /// `(MachO) -> Storage?` — a build that returned `nil` is a valid + /// terminal state, distinct from "still pending". + private var result: Value?? + + public init() {} + + /// Blocks until ``fulfill(_:)`` is called and returns whatever the builder + /// produced. Safe to call from any thread. + public func wait() -> Value? { + condition.lock() + defer { condition.unlock() } + while result == nil { + condition.wait() + } + // `result` is `.some(.some(v))` or `.some(.none)`; flatten the outer + // optional we used as the "fulfilled" tag back to the inner Storage?. + return result.flatMap { $0 } + } + + /// Records the result and wakes every waiter. Idempotent — a second call + /// is silently ignored, which matters when memory pressure clears the + /// cache mid-build and a fresh builder installs a new promise: the old + /// builder's `fulfill` call still has to land for its already-blocked + /// waiters, but mustn't disturb the new promise. + public func fulfill(_ value: Value?) { + condition.lock() + defer { condition.unlock() } + if result != nil { return } + result = .some(value) + condition.broadcast() + } +} diff --git a/Sources/MachOFixtureSupport/Baseline/BaselineFixturePicker.swift b/Sources/MachOFixtureSupport/Baseline/BaselineFixturePicker.swift index 18453f2e..856d9ed5 100644 --- a/Sources/MachOFixtureSupport/Baseline/BaselineFixturePicker.swift +++ b/Sources/MachOFixtureSupport/Baseline/BaselineFixturePicker.swift @@ -711,6 +711,25 @@ package enum BaselineFixturePicker { // relationship can be walked back) would let those Suites use a // live carrier. + /// Picks the SymbolTestsCore struct + /// `NestedGenerics.NestedGenericThreeLevelConstraintTest.MiddleConstrainedTest.InnerMostConstrainedTest` + /// — the deepest layer of a three-level nested generic context where each + /// outer scope adds its own protocol constraint. This is the canonical + /// counter-example for the cumulative-parent-vs-newly-introduced + /// distinction in `TargetGenericContext`: at depth ≥ 2, + /// `parentParameters.flatMap.count` and `parentParameters.last?.count` + /// diverge, exposing the bug where `currentRequirements` would otherwise + /// over-drop entries inherited from cumulative parent levels. + package static func struct_NestedThreeLevelInnerMostConstrainedTest( + in machO: some MachOSwiftSectionRepresentableWithCache + ) throws -> StructDescriptor { + try required( + try machO.swift.typeContextDescriptors.compactMap(\.struct).first(where: { descriptor in + try descriptor.name(in: machO) == "InnerMostConstrainedTest" + }) + ) + } + /// Picks the SymbolTestsCore struct /// `GenericValueFixtures.FixedSizeArray` — a generic /// type that declares one integer-value parameter (`N`) and one diff --git a/Sources/MachOSwiftSection/Models/Generic/GenericContext.swift b/Sources/MachOSwiftSection/Models/Generic/GenericContext.swift index 1c9392b4..9fd82d9c 100644 --- a/Sources/MachOSwiftSection/Models/Generic/GenericContext.swift +++ b/Sources/MachOSwiftSection/Models/Generic/GenericContext.swift @@ -46,8 +46,21 @@ public struct TargetGenericContextgetRequirementsWithInverses` for the full + /// canonical signature in scope (see `swift/lib/IRGen/GenMeta.cpp:7342`). + /// Each entry of `parentRequirements` is similarly cumulative for its + /// level, so the immediate parent's count (`parentRequirements.last?.count`) + /// is exactly the number of inherited requirements to drop. Using + /// `parentRequirements.flatMap { $0 }.count` instead would double-count + /// inherited entries at depth ≥ 2 and silently drop the requirements + /// introduced at this level. This mirrors the formula used by + /// `currentParameters`. public var currentRequirements: [GenericRequirementDescriptor] { - .init(requirements.dropFirst(parentRequirements.flatMap { $0 }.count)) + .init(requirements.dropFirst(parentRequirements.last?.count ?? 0)) } public var currentTypePacks: [GenericPackShapeDescriptor] { diff --git a/Tests/MachOSymbolsTests/SymbolIndexStoreTests.swift b/Sources/MachOTestingSupport/ProcessMemory.swift similarity index 86% rename from Tests/MachOSymbolsTests/SymbolIndexStoreTests.swift rename to Sources/MachOTestingSupport/ProcessMemory.swift index 1ee772dd..250b0200 100644 --- a/Tests/MachOSymbolsTests/SymbolIndexStoreTests.swift +++ b/Sources/MachOTestingSupport/ProcessMemory.swift @@ -1,9 +1,4 @@ import Foundation -import Testing -import MachO -@_spi(Internals) @testable import MachOSymbols -@testable import MachOTestingSupport -import MachOFixtureSupport public enum ProcessMemory { public enum Metric { @@ -82,14 +77,3 @@ public enum ProcessMemory { return result == KERN_SUCCESS ? info : nil } } - -@Suite -final class SymbolIndexStoreTests: MachOImageTests { - - override class var imageName: MachOImageName { .SwiftUI } - - @Test func main() async throws { - SymbolIndexStore.shared.prepare(in: machOImage) - ProcessMemory.report() - } -} diff --git a/Sources/SwiftDump/Extensions/GenericContext+Dump.swift b/Sources/SwiftDump/Extensions/GenericContext+Dump.swift index ee704d97..a522b7b8 100644 --- a/Sources/SwiftDump/Extensions/GenericContext+Dump.swift +++ b/Sources/SwiftDump/Extensions/GenericContext+Dump.swift @@ -86,9 +86,31 @@ extension TargetGenericContext { } } } else { - var currentValueIndex = 0 - for (offsetAndDepth, depthParameters) in allParameters.offsetEnumerated() { - for (offset, parameter) in depthParameters.offsetEnumerated() { + // `parameters` is cumulative — every nested generic context stores + // the full canonical parameter list. Naively iterating + // `allParameters` would re-emit each inherited level, producing + // duplicates at depth ≥ 2 (e.g. `` with `A` + // duplicated). Walk per-level "newly introduced" slices instead, + // mirroring the depth-aware visit order Swift's + // `forEachParam` produces. + let perLevelCounts = Self.dumpPerLevelNewParameterCounts( + parentParameters: parentParameters, + currentCount: currentParameters.count + ) + let perLevelValueCounts = Self.dumpPerLevelNewValueCounts( + parentValues: parentValues, + currentCount: currentValues.count + ) + var paramOffset = 0 + var valueOffset = 0 + var totalEmitted = 0 + let totalParameters = parameters.count + for (depthIndex, newCount) in perLevelCounts.enumerated() { + let valuesAtThisLevel = perLevelValueCounts[safe: depthIndex] ?? 0 + var currentValueIndexInLevel = 0 + for indexInLevel in 0.. [Int] { + var counts: [Int] = [] + var previous = 0 + for parentCumulative in parentParameters { + counts.append(parentCumulative.count - previous) + previous = parentCumulative.count + } + counts.append(currentCount) + return counts + } + + /// Same idea for value generics. + fileprivate static func dumpPerLevelNewValueCounts( + parentValues: [[GenericValueDescriptor]], + currentCount: Int + ) -> [Int] { + var counts: [Int] = [] + var previous = 0 + for parentCumulative in parentValues { + counts.append(parentCumulative.count - previous) + previous = parentCumulative.count + } + counts.append(currentCount) + return counts + } + @SemanticStringBuilder package func dumpGenericRequirements(resolver: DemangleResolver, in machO: MachO, isDumpCurrentLevel: Bool = true) async throws -> SemanticString { switch resolver { diff --git a/Sources/SwiftInterface/GenericSpecializer/ConformanceProvider.swift b/Sources/SwiftInterface/GenericSpecializer/ConformanceProvider.swift index e2921d95..9f1284ac 100644 --- a/Sources/SwiftInterface/GenericSpecializer/ConformanceProvider.swift +++ b/Sources/SwiftInterface/GenericSpecializer/ConformanceProvider.swift @@ -55,10 +55,20 @@ extension ConformanceProvider { // MARK: - IndexerConformanceProvider -/// ConformanceProvider implementation backed by SwiftInterfaceIndexer +/// ConformanceProvider implementation backed by SwiftInterfaceIndexer. /// -/// Note: This type is marked as @_spi(Support) because it depends on SwiftInterfaceIndexer -/// which is also SPI. Use the factory method on GenericSpecializer to create instances. +/// **Preparation contract.** The wrapped `SwiftInterfaceIndexer` must have +/// completed `prepare()` before this provider is queried — `findCandidates` +/// reads `allConformingTypesByProtocolName` / `allAllTypeDefinitions`, which +/// the indexer populates lazily during preparation. The class is marked +/// `@unchecked Sendable` to keep the API ergonomic when stored on a long- +/// lived `GenericSpecializer`, but it does *not* protect against concurrent +/// reads while preparation is still mutating the indexer's storage; callers +/// must order `await indexer.prepare()` before passing the indexer here. +/// +/// Note: This type is marked as `@_spi(Support)` because it depends on +/// `SwiftInterfaceIndexer`, which is also SPI. Use the factory initializer +/// on `GenericSpecializer` to create instances. @_spi(Support) public final class IndexerConformanceProvider: @unchecked Sendable { private let indexer: SwiftInterfaceIndexer @@ -180,73 +190,3 @@ public struct EmptyConformanceProvider: ConformanceProvider { public func typeDefinition(for typeName: TypeName) -> TypeDefinition? { nil } public func imagePath(for typeName: TypeName) -> String? { nil } } - -// MARK: - StandardLibraryConformanceProvider - -/// Provider for common standard library type conformances -/// This provides a fallback for well-known types when indexer data is not available -public struct StandardLibraryConformanceProvider: ConformanceProvider { - /// Known conformances: type name -> set of protocol names - private let knownConformances: [String: Set] - - public init() { - // Common standard library conformances - // These are simplified names without module prefix for matching - self.knownConformances = [ - "Int": ["Equatable", "Hashable", "Comparable", "Codable", "Sendable", "BitwiseCopyable"], - "Int8": ["Equatable", "Hashable", "Comparable", "Codable", "Sendable", "BitwiseCopyable"], - "Int16": ["Equatable", "Hashable", "Comparable", "Codable", "Sendable", "BitwiseCopyable"], - "Int32": ["Equatable", "Hashable", "Comparable", "Codable", "Sendable", "BitwiseCopyable"], - "Int64": ["Equatable", "Hashable", "Comparable", "Codable", "Sendable", "BitwiseCopyable"], - "UInt": ["Equatable", "Hashable", "Comparable", "Codable", "Sendable", "BitwiseCopyable"], - "UInt8": ["Equatable", "Hashable", "Comparable", "Codable", "Sendable", "BitwiseCopyable"], - "UInt16": ["Equatable", "Hashable", "Comparable", "Codable", "Sendable", "BitwiseCopyable"], - "UInt32": ["Equatable", "Hashable", "Comparable", "Codable", "Sendable", "BitwiseCopyable"], - "UInt64": ["Equatable", "Hashable", "Comparable", "Codable", "Sendable", "BitwiseCopyable"], - "Float": ["Equatable", "Hashable", "Comparable", "Codable", "Sendable", "BitwiseCopyable"], - "Double": ["Equatable", "Hashable", "Comparable", "Codable", "Sendable", "BitwiseCopyable"], - "Bool": ["Equatable", "Hashable", "Codable", "Sendable", "BitwiseCopyable"], - "String": ["Equatable", "Hashable", "Comparable", "Codable", "Sendable", "Collection", "BidirectionalCollection"], - "Character": ["Equatable", "Hashable", "Comparable", "Sendable"], - "Array": ["Equatable", "Hashable", "Collection", "MutableCollection", "RandomAccessCollection", "Codable", "Sendable"], - "Set": ["Equatable", "Hashable", "Collection", "Codable", "Sendable"], - "Dictionary": ["Equatable", "Collection", "Codable", "Sendable"], - "Optional": ["Equatable", "Hashable", "Sendable"], - "Data": ["Equatable", "Hashable", "Collection", "MutableCollection", "RandomAccessCollection", "Codable", "Sendable"], - "Date": ["Equatable", "Hashable", "Comparable", "Codable", "Sendable"], - "URL": ["Equatable", "Hashable", "Codable", "Sendable"], - "UUID": ["Equatable", "Hashable", "Codable", "Sendable"], - ] - } - - public func types(conformingTo protocolName: ProtocolName) -> [TypeName] { - // Standard library provider doesn't return type names - // It only validates conformances for known types - [] - } - - public func doesType(_ typeName: TypeName, conformTo protocolName: ProtocolName) -> Bool { - let simpleName = simplifyTypeName(typeName.name) - let simpleProto = simplifyTypeName(protocolName.name) - return knownConformances[simpleName]?.contains(simpleProto) ?? false - } - - public func conformances(of typeName: TypeName) -> [ProtocolName] { - // Would need to create ProtocolName instances which requires Node - // For now, return empty - this provider is mainly for validation - [] - } - - public var allTypeNames: [TypeName] { [] } - - public func typeDefinition(for typeName: TypeName) -> TypeDefinition? { nil } - public func imagePath(for typeName: TypeName) -> String? { nil } - - private func simplifyTypeName(_ name: String) -> String { - // Remove module prefix (e.g., "Swift.Int" -> "Int") - if let dotIndex = name.lastIndex(of: ".") { - return String(name[name.index(after: dotIndex)...]) - } - return name - } -} diff --git a/Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift b/Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift index f0e04e7d..0461cd3b 100644 --- a/Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift +++ b/Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift @@ -7,12 +7,22 @@ import OrderedCollections // MARK: - GenericSpecializer -/// Specializer for generic Swift types +/// Specializer for generic Swift types. /// /// Provides an interactive API for specializing generic types: -/// 1. Call `makeRequest(for:)` to get parameters and candidate types -/// 2. User selects concrete types for each parameter -/// 3. Call `specialize(_:with:)` to execute specialization +/// 1. Call `makeRequest(for:)` to get parameters and candidate types. +/// 2. User selects concrete types for each parameter. +/// 3. Call `specialize(_:with:)` to execute specialization. +/// +/// **MachO mode requirement.** `specialize(_:with:)`, +/// `runtimePreflight(selection:for:)`, and `resolveAssociatedTypeWitnesses` +/// are only available when `MachO == MachOImage` — they invoke runtime +/// metadata accessors (`swift_getGenericMetadata`, +/// `swift_conformsToProtocol`, `swift_getAssociatedTypeWitness`) which +/// require the type's image to be currently loaded into the running +/// process. `makeRequest(for:)` and `validate(selection:for:)` work for +/// any `MachO` — non-image specializers can still inspect parameters, +/// requirements, and candidate lists; they just cannot resolve metadata. @_spi(Support) public final class GenericSpecializer: @unchecked Sendable { @@ -47,14 +57,29 @@ extension GenericSpecializer { /// Create a specialization request for a generic type /// - /// - Parameter type: The generic type descriptor + /// - Parameters: + /// - type: The generic type descriptor. + /// - candidateOptions: Filter knobs for the per-parameter candidate + /// lists (e.g. `.excludeGenerics` to drop candidates whose own + /// descriptor is generic). /// - Returns: A request containing parameters, requirements, and candidate types /// - Throws: If the type is not generic or cannot be analyzed - public func makeRequest(for type: TypeContextDescriptorWrapper) throws -> SpecializationRequest { + public func makeRequest( + for type: TypeContextDescriptorWrapper, + candidateOptions: SpecializationRequest.CandidateOptions = .default + ) throws -> SpecializationRequest { let genericContext = try genericContext(for: type) + // Reject TypePack / Value generic parameters up front — we do not + // implement variadic generics or value generics yet, and silently + // skipping them in `buildParameters` would surface as a metadata- + // accessor argument-count mismatch deep inside `specialize()`. + if let unsupportedParameter = genericContext.parameters.first(where: { $0.kind == .typePack || $0.kind == .value }) { + throw SpecializerError.unsupportedGenericParameter(parameterKind: unsupportedParameter.kind) + } + // Build parameters from generic context - let parameters = try buildParameters(from: genericContext, for: type) + let parameters = try buildParameters(from: genericContext, for: type, candidateOptions: candidateOptions) // Build associated type requirements let associatedTypeRequirements = try buildAssociatedTypeRequirements(from: genericContext, for: type) @@ -75,25 +100,131 @@ extension GenericSpecializer { return genericContext } - /// Build parameter list from generic context - private func buildParameters(from genericContext: GenericContext, for type: TypeContextDescriptorWrapper) throws -> [SpecializationRequest.Parameter] { + /// All requirements visible to the specializer. + /// + /// `genericContext.requirements` is already cumulative — Swift's + /// `sig->getRequirementsWithInverses` covers every requirement in scope, + /// including those inherited from parent generic contexts (see + /// `swift/lib/IRGen/GenMeta.cpp:7342`). Re-flattening `allRequirements` + /// here would reintroduce the cumulative parent levels and double-count + /// inherited requirements at depth ≥ 2. + /// + /// Conditional requirements live in their own section (see + /// `addConditionalInvertedProtocols` at `GenMeta.cpp:1381`) and describe + /// the `where` clauses of `extension X: Copyable / Escapable` style + /// conformances. The section is written via + /// `addGenericRequirements(genericSig, conformance->getConditionalRequirements(), inverses)`, + /// so it can carry both: + /// - `.protocol` records — but `inverse_cannot_be_conditional_on_requirement` + /// (`DiagnosticsSema.def:8200`, enforced at `TypeCheckInvertible.cpp:198`) + /// restricts these to direct-GP `: thisInvertibleProtocol`, i.e. only + /// marker invertibles (`Copyable` / `Escapable` / `BitwiseCopyable`) + /// with `hasKeyArgument == false`. + /// - `.invertedProtocols` records — for any inverses in the conditional + /// context. + /// `collectInvertibleProtocols` is the only consumer that cares about the + /// `.invertedProtocols` half. The other consumers + /// (`collectRequirements`, `buildAssociatedTypeRequirements`, + /// `resolveAssociatedTypeWitnesses`) ignore `.invertedProtocols` by kind + /// and additionally drop marker `.protocol` records via the + /// `hasKeyArgument` filter (`collectRequirements` line 266–269 / the + /// `flags.contains(.hasKeyArgument)` guard in + /// `resolveAssociatedTypeWitnesses`), so merging conditional records here + /// is free of side effects on PWT counts. + private static func mergedRequirements( + from genericContext: GenericContext + ) -> [GenericRequirementDescriptor] { + genericContext.requirements + + genericContext.conditionalInvertibleProtocolsRequirements + } + + /// Per-level "newly introduced" parameter counts. + /// + /// `parentParameters[i]` stores the *cumulative* count visible at depth + /// `i` (Swift emits the full canonical parameter list at every nested + /// scope). Differencing successive entries yields the count of + /// parameters added at each depth; `currentParameters` already contains + /// only the new entries at the innermost scope. + private static func perLevelNewParameterCounts( + of genericContext: GenericContext + ) -> [Int] { + var counts: [Int] = [] + var previous = 0 + for parentCumulative in genericContext.parentParameters { + counts.append(parentCumulative.count - previous) + previous = parentCumulative.count + } + counts.append(genericContext.currentParameters.count) + return counts + } + + /// Pick out the `~Copyable` / `~Escapable` declaration for the generic + /// parameter at the given flat ordinal, unioning if multiple + /// `invertedProtocols` requirements target the same parameter. + /// Returns `nil` when no requirement targets this parameter. + /// + /// `flatIndex` is the parameter's absolute position in the cumulative + /// `genericContext.parameters` array — exactly the value + /// `sig->getGenericParamOrdinal(genericParam)` writes into the binary + /// (see `swift/lib/IRGen/GenMeta.cpp:7499`). + /// + /// Aggregation is union, matching IRGen's `suppressed[index].insert(...)`. + private static func collectInvertibleProtocols( + flatIndex: Int, + in genericContext: GenericContext + ) -> InvertibleProtocolSet? { + let target = UInt16(flatIndex) + var result: InvertibleProtocolSet? + for descriptor in mergedRequirements(from: genericContext) + where descriptor.layout.flags.kind == .invertedProtocols { + guard case .invertedProtocols(let inverted) = descriptor.content else { continue } + guard inverted.genericParamIndex == target else { continue } + + if let existing = result { + result = existing.union(inverted.protocols) + } else { + result = inverted.protocols + } + } + return result + } + + /// Build parameter list from generic context. + /// + /// Walks the cumulative `parameters` array level by level using the + /// per-depth "newly introduced" counts, so each parameter receives the + /// `(depth, indexInLevel)` pair that matches the demangler's canonical + /// names (`A`, `B`, `A1`, `B1`, `A2`, …). The flat ordinal of each + /// parameter — the value Swift writes into `InvertedProtocols.genericParamIndex` + /// — equals the offset into the cumulative array. + private func buildParameters( + from genericContext: GenericContext, + for type: TypeContextDescriptorWrapper, + candidateOptions: SpecializationRequest.CandidateOptions + ) throws -> [SpecializationRequest.Parameter] { var parameters: [SpecializationRequest.Parameter] = [] - // Process parameters at each depth level - for (depth, levelParams) in genericContext.allParameters.enumerated() { - for (index, param) in levelParams.enumerated() { + let cumulativeParameters = genericContext.parameters + let perLevelNewCounts = Self.perLevelNewParameterCounts(of: genericContext) + let mergedRequirements = Self.mergedRequirements(from: genericContext) + + var paramOffset = 0 + for (depth, newCount) in perLevelNewCounts.enumerated() { + for indexInLevel in 0.. [SpecializationRequest.Requirement] { var requirements: [SpecializationRequest.Requirement] = [] for genericRequirement in genericRequirements { - guard genericRequirement.flags.contains(.hasKeyArgument) else { continue } // Get the mangled param name and demangle it let mangledParamName = try genericRequirement.paramMangledName(in: machO) let paramNode = try MetadataReader.demangleType(for: mangledParamName, in: machO) - // Check if this requirement applies to our parameter - guard let dependentGenericParamType = paramNode.first(of: .dependentGenericParamType) else { - // This might be an associated type requirement - handle separately + // The requirement applies to this parameter only if its LHS is the + // generic parameter directly (not an associated-type reference like A.Element). + guard let directParamName = Self.directGenericParamName(of: paramNode), + directParamName == paramName else { continue } - guard let nodeParamName = dependentGenericParamType.text, nodeParamName == paramName else { + // ObjC-only protocol requirements have no key argument and no PWT; + // skip them. Other kinds (layout/sameType/baseClass) have no key + // argument either but should still be exposed for validation. + if genericRequirement.flags.kind == .protocol, + !genericRequirement.flags.contains(.hasKeyArgument) { continue } - // Build requirement based on kind - let requirement = try buildRequirement(from: genericRequirement) - if let requirement = requirement { + if let requirement = try buildRequirement(from: genericRequirement) { requirements.append(requirement) } } @@ -154,6 +295,87 @@ extension GenericSpecializer { return requirements } + /// Returns the parameter name when `paramNode` describes a direct generic + /// parameter (e.g. `A`). Returns `nil` when the node is an associated-type + /// reference such as `A.Element` or `A.Element.Element`. + static func directGenericParamName(of paramNode: Node) -> String? { + let typeNode = (paramNode.kind == .type) ? paramNode.firstChild : paramNode + guard let typeNode, typeNode.kind == .dependentGenericParamType else { return nil } + return typeNode.text + } + + /// Parsed associated-type access path extracted from a demangled requirement + /// LHS. The chain `A.Element.Element` produces `baseParamName == "A"` and + /// `steps == [(Element, Sequence), (Element, Sequence)]` in source order. + struct AssociatedPathInfo { + let baseParamName: String + let steps: [Step] + + struct Step { + let name: String + /// `Type` node wrapping the protocol that owns this associated type. + let protocolNode: Node + } + } + + /// Walk a demangled `LHS` node and split it into the root generic parameter + /// name plus the ordered chain of associated-type accesses. Returns `nil` + /// when the structure does not match an `A` or `A.X.Y...` reference. + /// + /// The protocol child of each `DependentAssociatedTypeRef` may be one of + /// three Demangler-emitted shapes (see + /// `swift/lib/Demangling/Demangler.cpp:2832-2845` `popAssocTypeName`): + /// - `.type` (resolver wrapped a context tree — the common case); + /// - `.protocolSymbolicReference` (resolver returned `nil` for a Swift + /// protocol — image not loaded, recursion limit, etc.); + /// - `.objectiveCProtocolSymbolicReference` (same but for an Obj-C + /// protocol). + /// We accept all three. Downstream resolution (`resolveAssociatedTypeStep`) + /// may still fail when looking the protocol up in the indexer, but the + /// parsing layer should not silently drop a structurally valid LHS. + static func extractAssociatedPath(of paramNode: Node) -> AssociatedPathInfo? { + var current: Node = paramNode + if current.kind == .type, let inner = current.firstChild { + current = inner + } + + // The OUTERMOST DependentMemberType represents the LAST step; we + // traverse outer→inner pushing each step, then reverse to get source order. + var stepsOuterToInner: [AssociatedPathInfo.Step] = [] + + while current.kind == .dependentMemberType { + guard current.numberOfChildren >= 2 else { return nil } + let baseTypeWrapper = current.children[0] + let assocRef = current.children[1] + guard assocRef.kind == .dependentAssociatedTypeRef, + assocRef.numberOfChildren >= 2, + let nameChild = assocRef.firstChild, + case .text(let stepName) = nameChild.contents else { + return nil + } + let protocolNode = assocRef.children[1] + switch protocolNode.kind { + case .type, + .protocolSymbolicReference, + .objectiveCProtocolSymbolicReference: + break + default: + return nil + } + stepsOuterToInner.append(.init(name: stepName, protocolNode: protocolNode)) + + guard baseTypeWrapper.kind == .type, let baseInner = baseTypeWrapper.firstChild else { + return nil + } + current = baseInner + } + + guard current.kind == .dependentGenericParamType, let baseName = current.text else { + return nil + } + return .init(baseParamName: baseName, steps: Array(stepsOuterToInner.reversed())) + } + /// Build a requirement from a requirement descriptor private func buildRequirement(from genericRequirement: GenericRequirementDescriptor) throws -> SpecializationRequest.Requirement? { let flags = genericRequirement.layout.flags @@ -198,89 +420,134 @@ extension GenericSpecializer { return .layout(.class) } - case .sameConformance, .sameShape, .invertedProtocols: - // These are more advanced requirements that we don't need for basic specialization + case .sameConformance: + // Derived from SameType / BaseClass; compiler forces hasKeyArgument=false, + // so it never participates in metadata accessor key arguments. + return nil + + case .sameShape: + // Pack-shape constraint between two TypePacks. Relevant only to variadic + // generics, which are out of scope for this specializer. return nil - } - } - /// Convert runtime layout kind to our model - private func convertLayoutKind(_ kind: GenericRequirementLayoutKind) -> SpecializationRequest.LayoutKind { - switch kind { - case .class: - return .class + case .invertedProtocols: + // Capability declaration (~Copyable / ~Escapable) — surfaced on + // Parameter.invertibleProtocols rather than as a Requirement, because + // it relaxes rather than constrains the parameter. + return nil } } - /// Build associated type requirements (ordered for PWT passing) + /// Build associated type requirements (ordered for PWT passing). + /// + /// Multiple constraints on the same `A.X.Y...` path are aggregated into + /// one `AssociatedTypeRequirement` whose `requirements` array preserves + /// canonical (binary) order. Aggregating by `(parameterName, path)` + /// matches the field's declared semantics — `requirements: [Requirement]` + /// is plural for a reason — and keeps consumers from having to re-group + /// duplicates themselves. private func buildAssociatedTypeRequirements( from genericContext: GenericContext, for type: TypeContextDescriptorWrapper ) throws -> [SpecializationRequest.AssociatedTypeRequirement] { - var associatedTypeRequirements: [SpecializationRequest.AssociatedTypeRequirement] = [] - let genericRequirements = genericContext.allRequirements.flatMap { $0 } + var entriesByKey: [AssociatedTypeRequirementKey: [SpecializationRequest.Requirement]] = [:] + var orderedKeys: [AssociatedTypeRequirementKey] = [] + let genericRequirements = Self.mergedRequirements(from: genericContext) for genericRequirement in genericRequirements { let mangledParamName = try genericRequirement.paramMangledName(in: machO) let paramNode = try MetadataReader.demangleType(for: mangledParamName, in: machO) - // Check for dependent member type (associated type requirement) - guard let dependentMemberType = paramNode.first(of: .dependentMemberType) else { + // Only handle dependent-member chains here; direct GP requirements + // are collected per parameter in `collectRequirements`. + guard let pathInfo = Self.extractAssociatedPath(of: paramNode), !pathInfo.steps.isEmpty else { continue } - // Get base parameter name - guard let dependentGenericParamType = dependentMemberType.first(of: .dependentGenericParamType), - let baseParamName = dependentGenericParamType.text else { + guard let requirement = try buildRequirement(from: genericRequirement) else { continue } - // Get associated type path - guard let dependentAssociatedTypeRef = dependentMemberType.first(of: .dependentAssociatedTypeRef), - let associatedTypeName = dependentAssociatedTypeRef.children.first?.text else { - continue + let key = AssociatedTypeRequirementKey( + parameterName: pathInfo.baseParamName, + path: pathInfo.steps.map(\.name) + ) + if entriesByKey[key] == nil { + orderedKeys.append(key) } + entriesByKey[key, default: []].append(requirement) + } - // Build requirement for this associated type - if let requirement = try buildRequirement(from: genericRequirement) { - associatedTypeRequirements.append(SpecializationRequest.AssociatedTypeRequirement( - parameterName: baseParamName, - path: [associatedTypeName], - requirements: [requirement] - )) - } + return orderedKeys.map { key in + SpecializationRequest.AssociatedTypeRequirement( + parameterName: key.parameterName, + path: key.path, + requirements: entriesByKey[key] ?? [] + ) } + } - return associatedTypeRequirements + /// Aggregation key for `buildAssociatedTypeRequirements` — a generic + /// type can't host a nested struct directly inside a method body, so + /// the key lives at extension scope. + /// + /// **Why no protocol identity in the key.** It would be tempting to + /// also include each step's `protocolNode` in the key — guarding + /// against the case where two unrelated protocols both declare the + /// same-named associated type (`P.Element` vs `Q.Element`) and a + /// generic parameter conforms to both. That case is **structurally + /// impossible in a well-formed binary**: the Swift compiler's + /// RequirementMachine runs a deterministic minimization pass + /// (`swift/lib/AST/RequirementMachine/MinimalConformances.cpp` + + /// `HomotopyReduction.cpp`, invoked from `getMinimalGenericSignature`) + /// before any descriptor is emitted, and that pass collapses every + /// `A.[P:Element]` / `A.[Q:Element]` reference to a single canonical + /// rooted form. The choice is fixed by `compareDependentTypesRec` + /// (`swift/lib/AST/GenericSignature.cpp:846`) — empirically the + /// lexicographically earlier protocol — and is part of type + /// checking, not an opt-in optimization. + /// + /// `dualProtocolSameNamedAssociatedTypeIsCanonicalized` in the + /// test suite exists as a belt-and-suspenders pin on this upstream + /// invariant. So long as that test stands, dropping `protocolNode` + /// from the aggregation key is safe — a future code reader noticing + /// the omission can stop here rather than re-investigating. + private struct AssociatedTypeRequirementKey: Hashable { + let parameterName: String + let path: [String] } - /// Find candidate types that satisfy all protocol constraints - private func findCandidates(satisfying protocols: [ProtocolName]) -> [SpecializationRequest.Candidate] { - guard !protocols.isEmpty else { - // No constraints - return all indexed types - return conformanceProvider.allTypeNames.compactMap { typeName -> SpecializationRequest.Candidate? in - guard conformanceProvider.typeDefinition(for: typeName) != nil else { - return nil - } - let imagePath = conformanceProvider.imagePath(for: typeName) ?? "" - return SpecializationRequest.Candidate( - typeName: typeName, - source: .image(imagePath) - ) - } + /// Find candidate types that satisfy all protocol constraints. + /// + /// Generic candidates are included by default but flagged via + /// `Candidate.isGeneric`; selecting one via `Argument.candidate` would + /// throw `candidateRequiresNestedSpecialization` from `specialize`. Pass + /// `candidateOptions: .excludeGenerics` to skip them up front when the + /// caller wants a "directly-specializable" list. + private func findCandidates( + satisfying protocols: [ProtocolName], + options: SpecializationRequest.CandidateOptions = .default + ) -> [SpecializationRequest.Candidate] { + let typeNames: [TypeName] + if protocols.isEmpty { + typeNames = conformanceProvider.allTypeNames + } else { + typeNames = conformanceProvider.types(conformingToAll: protocols) } - // Find types conforming to all protocols - let conformingTypes = conformanceProvider.types(conformingToAll: protocols) - - return conformingTypes.compactMap { typeName -> SpecializationRequest.Candidate? in - guard conformanceProvider.typeDefinition(for: typeName) != nil else { + return typeNames.compactMap { typeName -> SpecializationRequest.Candidate? in + guard let typeDefinition = conformanceProvider.typeDefinition(for: typeName) else { + return nil + } + let isGeneric = typeDefinition.type.typeContextDescriptorWrapper.typeContextDescriptor.layout.flags.isGeneric + if options.contains(.excludeGenerics), isGeneric { return nil } let imagePath = conformanceProvider.imagePath(for: typeName) ?? "" return SpecializationRequest.Candidate( typeName: typeName, - source: .image(imagePath) + source: .image(imagePath), + isGeneric: isGeneric ) } } @@ -291,30 +558,47 @@ extension GenericSpecializer { @_spi(Support) extension GenericSpecializer { - /// Validate a selection against a request + /// Validate a selection against a request — *static* checks only. /// - /// - Parameters: - /// - selection: The user's type selections - /// - request: The specialization request - /// - Returns: Validation result with any errors or warnings + /// Reports the cheap, runtime-free issues: + /// - missing parameter arguments (error) + /// - extra arguments not declared by the request (warning) + /// + /// Deliberately does *not* preempt `Argument.candidate` selections + /// flagged `isGeneric`: the existing `specialize` contract is to + /// throw a typed `SpecializerError.candidateRequiresNestedSpecialization` + /// at the candidate-resolution site, and downstream callers depend + /// on that error path. Use the request's `Candidate.isGeneric` flag + /// or the `excludeGenerics` candidate option if you want to filter + /// these earlier. + /// + /// For deeper protocol-conformance / layout checks against the + /// concrete metadata, call `runtimePreflight(selection:for:)` (only + /// available when `MachO == MachOImage`). `specialize` automatically + /// folds both validations together. public func validate(selection: SpecializationSelection, for request: SpecializationRequest) -> SpecializationValidation { let builder = SpecializationValidation.builder() - // Check all required parameters are provided for parameter in request.parameters { guard selection.hasArgument(for: parameter.name) else { builder.addError(.missingArgument(parameterName: parameter.name)) continue } - - // Validate each requirement for this parameter - // Note: We don't validate all requirements here since some require runtime resolution - // Full validation happens during specialize() } - // Check for extra arguments + let associatedTypePaths = Set(request.associatedTypeRequirements.map(\.fullPath)) + for paramName in selection.selectedParameterNames { - if !request.parameters.contains(where: { $0.name == paramName }) { + if request.parameters.contains(where: { $0.name == paramName }) { + continue + } + // Distinguish "user typed an associated-type access path" from + // "user typed a wrong/typo'd key": the former is a structurally + // recognizable mistake (associated types are derived during + // specialization) and deserves a more actionable warning. + if associatedTypePaths.contains(paramName) { + builder.addWarning(.associatedTypePathInSelection(path: paramName)) + } else { builder.addWarning(.extraArgument(parameterName: paramName)) } } @@ -323,6 +607,178 @@ extension GenericSpecializer { } } +// MARK: - Runtime Preflight + +@_spi(Support) +extension GenericSpecializer where MachO == MachOImage { + + /// Runtime-aware companion to `validate(selection:for:)`. + /// + /// Performs the checks that need an actual `Metadata`: + /// - **Protocol requirements**: every direct-GP `protocol` requirement + /// is exercised via `swift_conformsToProtocol`. A `nil` result becomes + /// `protocolRequirementNotSatisfied` instead of letting it surface + /// mid-`specialize` as `witnessTableNotFound`. + /// - **Layout (`AnyObject`) requirements**: the resolved metadata kind + /// must be class-like (`.class` / `.objcClassWrapper` / `.foreignClass`). + /// + /// Same-type / base-class / associated-type checks are intentionally + /// out of scope — they require either type-equality or chain walking + /// that we'd rather perform once inside `specialize`. Failures there + /// continue to bubble up via their typed errors. + /// + /// Argument-kind handling: + /// - `.metatype` / `.metadata` / `.specialized` are validated. The + /// `.specialized` case already carries a resolved metadata pointer + /// in the `SpecializationResult`, so it's just as cheap as the + /// direct cases. + /// - `.candidate` is skipped (its concrete metadata requires running + /// the candidate's own metadata accessor; `specialize` validates it + /// implicitly via the lookup path). + /// + /// When the indexer doesn't have a definition for a protocol referenced + /// by a requirement, the corresponding conformance check cannot be run. + /// In that case the function emits a `.protocolNotInIndexer` warning + /// (instead of silently skipping) so callers know validation is + /// incomplete and which sub-indexer is missing. + public func runtimePreflight( + selection: SpecializationSelection, + for request: SpecializationRequest + ) -> SpecializationValidation { + let builder = SpecializationValidation.builder() + + for parameter in request.parameters { + guard let argument = selection[parameter.name] else { continue } + + let metadata: Metadata + switch argument { + case .metatype(let type): + // `specialize` runs the same `Metadata.createInProcess` + // call, so a failure here will also break the accessor + // call. Surface it now as a typed error rather than a + // silent skip — the caller's selection is unusable. + do { + metadata = try Metadata.createInProcess(type) + } catch { + builder.addError(.metadataResolutionFailed( + parameterName: parameter.name, + reason: "\(error)" + )) + continue + } + case .metadata(let provided): + metadata = provided + case .specialized(let result): + // `SpecializationResult` already carries a resolved metadata + // pointer — no accessor call needed; preflight should + // exercise the same checks it does for `.metatype`. A + // failure here means the supplied result is corrupt and + // `specialize` will fail the same way; report as an error. + do { + metadata = try result.metadata() + } catch { + builder.addError(.metadataResolutionFailed( + parameterName: parameter.name, + reason: "\(error)" + )) + continue + } + case .candidate: + // The candidate's metadata still requires an accessor call; + // leave the actual conformance/layout enforcement to + // `specialize`'s candidate-resolution path. + continue + } + + for requirement in parameter.requirements { + switch requirement { + case .protocol(let info) where info.requiresWitnessTable: + guard let indexer else { + // No indexer at all — we can never check conformance. + // Surface once per missing-protocol/requirement pair + // so the caller knows validation was a no-op. + builder.addWarning(.protocolNotInIndexer( + parameterName: parameter.name, + protocolName: info.protocolName.name + )) + continue + } + guard let protocolDef = indexer.allAllProtocolDefinitions[info.protocolName] else { + // Indexer present but the protocol's defining image + // isn't included as a sub-indexer. + builder.addWarning(.protocolNotInIndexer( + parameterName: parameter.name, + protocolName: info.protocolName.name + )) + continue + } + let descriptor: MachOSwiftSection.`Protocol` + do { + descriptor = try MachOSwiftSection.`Protocol`( + descriptor: protocolDef.value.protocol.descriptor.asPointerWrapper(in: protocolDef.machO) + ) + } catch { + // Indexer found the entry but materializing the + // protocol descriptor failed — preflight cannot + // run the conformance check for this requirement. + // Distinct from `protocolNotInIndexer`: the + // protocol *is* known but unusable. + builder.addError(.protocolDescriptorResolutionFailed( + parameterName: parameter.name, + protocolName: info.protocolName.name, + reason: "\(error)" + )) + continue + } + // Distinguish "couldn't run the check" (throw) from + // "ran the check, type doesn't conform" (nil). The + // former is a warning (validation incomplete); the + // latter is the existing hard error. + let conforms: ProtocolWitnessTable? + do { + conforms = try RuntimeFunctions.conformsToProtocol( + metadata: metadata, + protocolDescriptor: descriptor.descriptor + ) + } catch { + builder.addWarning(.conformanceCheckFailed( + parameterName: parameter.name, + protocolName: info.protocolName.name, + reason: "\(error)" + )) + continue + } + if conforms == nil { + builder.addError(.protocolRequirementNotSatisfied( + parameterName: parameter.name, + protocolName: info.protocolName.name, + actualType: "\(metadata)" + )) + } + case .layout(let layoutKind): + switch layoutKind { + case .class: + let kind = metadata.kind + let isClassLike = (kind == .class || kind == .objcClassWrapper || kind == .foreignClass) + if !isClassLike { + builder.addError(.layoutRequirementNotSatisfied( + parameterName: parameter.name, + expectedLayout: layoutKind, + actualType: "\(metadata)" + )) + } + } + case .protocol, .sameType, .baseClass: + // Other kinds: skip (no PWT, or out-of-scope — see header). + continue + } + } + } + + return builder.build() + } +} + // MARK: - Specialization Execution @_spi(Support) @@ -333,18 +789,46 @@ extension GenericSpecializer where MachO == MachOImage { /// - Parameters: /// - request: The specialization request /// - selection: The user's type selections + /// - metadataRequest: Freshness state requested for the *returned* + /// metadata. Internal accessor calls used to resolve candidate + /// types and associated-type witnesses always use fully-complete + /// blocking requests, matching the semantics of + /// `swift_getGenericMetadata`. /// - Returns: Specialized metadata result /// - Throws: If specialization fails - public func specialize(_ request: SpecializationRequest, with selection: SpecializationSelection) throws -> SpecializationResult { + public func specialize( + _ request: SpecializationRequest, + with selection: SpecializationSelection, + metadataRequest: MetadataRequest = .completeAndBlocking + ) throws -> SpecializationResult { let typeDescriptor = request.typeDescriptor.asPointerWrapper(in: machO) - // Validate selection first - let validation = validate(selection: selection, for: request) - guard validation.isValid else { - let errorMessages = validation.errors.map { $0.description }.joined(separator: "; ") + // Static validation first (cheap, no runtime resolution). + let staticValidation = validate(selection: selection, for: request) + guard staticValidation.isValid else { + let errorMessages = staticValidation.errors.map { $0.description }.joined(separator: "; ") + throw SpecializerError.specializationFailed(reason: errorMessages) + } + + // Runtime preflight — verifies protocol conformance and layout + // constraints before we ever call the accessor. Surfaces + // mismatches as `SpecializationValidation.Error` values matching + // the requirement kind, instead of letting them blow up inside + // `swift_getGenericMetadata` or `RuntimeFunctions.conformsToProtocol`. + let runtimeValidation = runtimePreflight(selection: selection, for: request) + guard runtimeValidation.isValid else { + let errorMessages = runtimeValidation.errors.map { $0.description }.joined(separator: "; ") throw SpecializerError.specializationFailed(reason: errorMessages) } - // Build metadata and witness table arrays in requirement order + // Build metadata and witness table arrays in requirement order. + // + // The PWT ordering invariant (still verified by every existing + // fixture): Swift's `compareDependentTypesRec` orders all GP-rooted + // requirements before any nested-type-rooted requirement (see + // `swift/lib/AST/GenericSignature.cpp:846`). That means walking + // direct-GP requirements in parameter order, then walking associated + // requirements in canonical merged-requirement order, reconstructs + // exactly the binary's emission order without an explicit re-sort. var metadatas: [Metadata] = [] var witnessTables: [ProtocolWitnessTable] = [] var resolvedArguments: [SpecializationResult.ResolvedArgument] = [] @@ -387,8 +871,20 @@ extension GenericSpecializer where MachO == MachOImage { for: typeDescriptor, substituting: metadataByParamName ) - for (_, pwts) in associatedTypeWitnesses { - witnessTables.append(contentsOf: pwts) + witnessTables.append(contentsOf: associatedTypeWitnesses) + + // Defensive invariant — the accessor expects exactly + // `numKeyArguments` slots (metadatas first, then PWTs in canonical + // order). If `buildParameters` / `collectRequirements` / + // `buildAssociatedTypeRequirements` ever miscount, we'd send the + // wrong number of args and the runtime would fail opaquely. + // Reject up front with a typed error so the regression is + // immediately attributable. + let totalArguments = metadatas.count + witnessTables.count + guard totalArguments == request.keyArgumentCount else { + throw SpecializerError.specializationFailed( + reason: "internal: key argument count mismatch — request expects \(request.keyArgumentCount) (header.numKeyArguments), built \(totalArguments) (\(metadatas.count) metadatas + \(witnessTables.count) witness tables)" + ) } // Get metadata accessor function @@ -402,7 +898,7 @@ extension GenericSpecializer where MachO == MachOImage { // Call accessor with metadatas and witness tables let response = try accessorFunction( - request: .completeAndBlocking, + request: metadataRequest, metadatas: metadatas, witnessTables: witnessTables, ) @@ -449,9 +945,20 @@ extension GenericSpecializer where MachO == MachOImage { } let typeDefinition = typeDefinitionEntry.value + let typeContext = typeDefinition.type.typeContextDescriptorWrapper.typeContextDescriptor + + // Generic candidates need nested specialization; surface a typed error + // rather than letting the no-argument accessor call below fail with + // a generic message. + if let genericContext = try typeContext.genericContext(in: typeDefinitionEntry.machO) { + throw SpecializerError.candidateRequiresNestedSpecialization( + candidate: candidate, + parameterCount: Int(genericContext.header.numParams) + ) + } // Get accessor function from type definition's type context - let accessorFunction = try typeDefinition.type.typeContextDescriptorWrapper.typeContextDescriptor.metadataAccessorFunction(in: typeDefinitionEntry.machO) + let accessorFunction = try typeContext.metadataAccessorFunction(in: typeDefinitionEntry.machO) guard let accessorFunction else { throw SpecializerError.candidateResolutionFailed( candidate: candidate, @@ -459,7 +966,7 @@ extension GenericSpecializer where MachO == MachOImage { ) } - // For non-generic types, just call the accessor + // Non-generic: call accessor with no arguments let response = try accessorFunction(request: .completeAndBlocking) let wrapper = try response.value.resolve() return try wrapper.metadata @@ -510,24 +1017,33 @@ extension GenericSpecializer where MachO == MachOImage { @_spi(Support) extension GenericSpecializer where MachO == MachOImage { - /// Resolve associated type witness tables for a generic type's requirements + /// Resolve associated type witness tables for a generic type's requirements. + /// + /// Processes the generic requirements to find associated type constraints + /// (e.g. `A.Element: Hashable`) and resolves the corresponding witness + /// tables using runtime functions. /// - /// Processes the generic requirements to find associated type constraints (e.g., A.Element: Hashable) - /// and resolves the corresponding witness tables using runtime functions. + /// The returned array is in canonical (binary) requirement order — the + /// same order Swift's `compareDependentTypes` + /// (`swift/lib/AST/GenericSignature.cpp:846`) emits the witness-table + /// slots into the metadata accessor's argument list. Callers are + /// expected to splice this array into their PWT list **after** the + /// direct-GP PWTs. /// /// - Parameters: /// - type: The generic type descriptor /// - genericArguments: Mapping from parameter name to resolved metadata - /// - Returns: Ordered dictionary mapping associated type metadata to their witness tables + /// - Returns: Witness tables, in canonical binary order, for every + /// associated-type requirement reachable from this descriptor. func resolveAssociatedTypeWitnesses( for type: TypeContextDescriptorWrapper, substituting genericArguments: [String: Metadata] - ) throws -> OrderedDictionary { + ) throws -> [ProtocolWitnessTable] { guard let indexer else { throw AssociatedTypeResolutionError.missingIndexer } - var results: OrderedDictionary = [:] + var results: [ProtocolWitnessTable] = [] guard let genericContextInProcess = try type.genericContext() else { throw AssociatedTypeResolutionError.missingGenericContext(typeDescriptor: type) @@ -537,145 +1053,152 @@ extension GenericSpecializer where MachO == MachOImage { throw AssociatedTypeResolutionError.unsupportedGenericParameter(parameterKind: unsupportedParameter.kind) } - let requirements = try genericContextInProcess.requirements.map { try GenericRequirement(descriptor: $0) } - var conformingTypeMetadataByGenericParam: [String: Metadata] = [:] + let requirements = try Self.mergedRequirements(from: genericContextInProcess) + .map { try GenericRequirement(descriptor: $0) } let allProtocolDefinitions = indexer.allAllProtocolDefinitions for requirement in requirements { - guard let requirementProtocolDescriptor = requirement.content.protocol?.resolved, - let protocolDescriptor = requirementProtocolDescriptor.swift, - requirement.flags.contains(.hasKeyArgument) else { continue } + // Only protocol conformance requirements with key arguments produce + // associated-type witness tables; everything else is irrelevant here. + guard requirement.flags.kind == .protocol, + requirement.flags.contains(.hasKeyArgument), + let requirementProtocolDescriptor = requirement.content.protocol?.resolved, + let protocolDescriptor = requirementProtocolDescriptor.swift else { continue } let requirementProtocol = try MachOSwiftSection.`Protocol`(descriptor: protocolDescriptor) let paramNode = try MetadataReader.demangleType(for: requirement.paramManagledName) - if let dependentMemberType = paramNode.first(of: .dependentMemberType) { - // Associated type requirement (e.g., A.Element: Hashable) - guard let dependentGenericParamType = dependentMemberType.first(of: .dependentGenericParamType) else { - throw AssociatedTypeResolutionError.missingDependentGenericParamType(dependentMemberType: dependentMemberType) - } - - guard let genericParamType = dependentGenericParamType.text else { - throw AssociatedTypeResolutionError.missingGenericParamTypeText(dependentGenericParamType: dependentGenericParamType) - } - - guard let conformingTypeMetadata = conformingTypeMetadataByGenericParam[genericParamType] else { - throw AssociatedTypeResolutionError.missingConformingTypeMetadata( - genericParam: genericParamType, - availableParams: Array(conformingTypeMetadataByGenericParam.keys) - ) - } - - guard let dependentAssociatedTypeRef = dependentMemberType.first(of: .dependentAssociatedTypeRef) else { - throw AssociatedTypeResolutionError.missingDependentAssociatedTypeRef(dependentMemberType: dependentMemberType) - } - - guard let associatedTypeName = dependentAssociatedTypeRef.children.first?.text else { - throw AssociatedTypeResolutionError.missingAssociatedTypeName(dependentAssociatedTypeRef: dependentAssociatedTypeRef) - } - - guard let associatedTypeRefProtocolTypeNode = dependentAssociatedTypeRef.children.second else { - throw AssociatedTypeResolutionError.missingAssociatedTypeRefProtocolTypeNode(dependentAssociatedTypeRef: dependentAssociatedTypeRef) - } + guard let pathInfo = Self.extractAssociatedPath(of: paramNode) else { + throw AssociatedTypeResolutionError.unknownParamNodeStructure(paramNode: paramNode) + } - guard let associatedTypeRefMachOAndProtocol = allProtocolDefinitions[.init(node: associatedTypeRefProtocolTypeNode)] else { - throw AssociatedTypeResolutionError.missingAssociatedTypeRefMachOAndProtocol(protocolTypeNode: associatedTypeRefProtocolTypeNode) - } + // Direct generic parameter requirement: handled by `specialize()`, skip here. + guard !pathInfo.steps.isEmpty else { continue } - let associatedTypeRefProtocol: MachOSwiftSection.`Protocol` - do { - associatedTypeRefProtocol = try MachOSwiftSection.`Protocol`( - descriptor: associatedTypeRefMachOAndProtocol.value.protocol.descriptor.asPointerWrapper(in: associatedTypeRefMachOAndProtocol.machO) - ) - } catch { - throw AssociatedTypeResolutionError.failedToCreateAssociatedTypeRefProtocol(underlyingError: error) - } + // Walk the associated-type chain step by step starting from the + // root generic parameter's metadata. + guard var currentMetadata = genericArguments[pathInfo.baseParamName] else { + throw AssociatedTypeResolutionError.missingConformingTypeMetadata( + genericParam: pathInfo.baseParamName, + availableParams: Array(genericArguments.keys) + ) + } - let associatedTypeRefProtocolName = try associatedTypeRefProtocol.protocolName() - let availableAssociatedTypes = try associatedTypeRefProtocol.descriptor.associatedTypes() + for step in pathInfo.steps { + currentMetadata = try resolveAssociatedTypeStep( + currentMetadata: currentMetadata, + step: step, + allProtocolDefinitions: allProtocolDefinitions + ) + } - guard let associatedTypeIndex = availableAssociatedTypes.firstIndex(of: associatedTypeName) else { - throw AssociatedTypeResolutionError.missingAssociatedTypeIndex( - associatedTypeName: associatedTypeName, - protocolName: associatedTypeRefProtocolName, - availableAssociatedTypes: availableAssociatedTypes - ) - } + // The leaf metadata must conform to the requirement protocol; that + // conformance PWT is the value the runtime expects in the slot. + let currentProtocolName = try requirementProtocol.protocolName() + guard let associatedTypePWT = try? RuntimeFunctions.conformsToProtocol( + metadata: currentMetadata, + protocolDescriptor: requirementProtocol.descriptor + ) else { + throw AssociatedTypeResolutionError.associatedTypeDoesNotConformToProtocol( + associatedType: currentMetadata, + protocolName: currentProtocolName + ) + } - guard let associatedTypeBaseRequirement = associatedTypeRefProtocol.baseRequirement else { - throw AssociatedTypeResolutionError.missingAssociatedTypeBaseRequirement(protocolName: associatedTypeRefProtocolName) - } + // Append in iteration (= binary) order. A previous version + // grouped PWTs into an `OrderedDictionary` + // keyed by leaf metadata; updates kept the original key + // position, so when two distinct chains landed on the same + // leaf and a third chain in between landed on a different + // leaf, flattening the dictionary's values misordered the + // PWT slots relative to `compareDependentTypes`. See + // `associatedWitnessOrderingPreservesBinaryOrder` / + // `specializeMatchesManualBinaryOrder` for the reproduction. + results.append(associatedTypePWT) + } - let associatedTypeAccessFunctionRequirements = associatedTypeRefProtocol.requirements.filter { - $0.flags.kind.isAssociatedTypeAccessFunction - } + return results + } - guard let associatedTypeAccessFunctionRequirement = associatedTypeAccessFunctionRequirements[safe: associatedTypeIndex] else { - throw AssociatedTypeResolutionError.missingAssociatedTypeAccessFunctionRequirement( - index: associatedTypeIndex, - protocolName: associatedTypeRefProtocolName, - requirementCount: associatedTypeAccessFunctionRequirements.count - ) - } + /// Resolve a single associated-type access (`Type → Type.Step`). + /// + /// Given the current conforming type's metadata and the protocol that + /// declares the associated type, the function: + /// 1. retrieves the protocol descriptor from the indexer, + /// 2. fetches the witness table for `currentMetadata: stepProtocol`, + /// 3. locates the associated-type access function for `step.name`, + /// 4. invokes the runtime function to obtain the next metadata in the chain. + private func resolveAssociatedTypeStep( + currentMetadata: Metadata, + step: AssociatedPathInfo.Step, + allProtocolDefinitions: OrderedDictionary> + ) throws -> Metadata { + let stepProtocolName = ProtocolName(node: step.protocolNode) + guard let entry = allProtocolDefinitions[stepProtocolName] else { + throw AssociatedTypeResolutionError.missingAssociatedTypeRefMachOAndProtocol(protocolTypeNode: step.protocolNode) + } - guard let conformingTypePWT = try RuntimeFunctions.conformsToProtocol( - metadata: conformingTypeMetadata, - protocolDescriptor: associatedTypeRefProtocol.descriptor - ) else { - throw AssociatedTypeResolutionError.conformingTypeDoesNotConformToProtocol( - conformingType: conformingTypeMetadata, - protocolName: associatedTypeRefProtocolName - ) - } + let stepProtocol: MachOSwiftSection.`Protocol` + do { + stepProtocol = try MachOSwiftSection.`Protocol`( + descriptor: entry.value.protocol.descriptor.asPointerWrapper(in: entry.machO) + ) + } catch { + throw AssociatedTypeResolutionError.failedToCreateAssociatedTypeRefProtocol(underlyingError: error) + } - guard let associatedTypeMetadata = try? RuntimeFunctions.getAssociatedTypeWitness( - request: .init(), - protocolWitnessTable: conformingTypePWT, - conformingTypeMetadata: conformingTypeMetadata, - baseRequirement: associatedTypeBaseRequirement, - associatedTypeRequirement: associatedTypeAccessFunctionRequirement - ).value.resolve().metadata else { - throw AssociatedTypeResolutionError.failedToGetAssociatedTypeWitness( - conformingType: conformingTypeMetadata, - protocolName: associatedTypeRefProtocolName, - associatedTypeName: associatedTypeName - ) - } + let stepProtocolFullName = try stepProtocol.protocolName() + let availableAssociatedTypes = try stepProtocol.descriptor.associatedTypes() - let currentProtocolName = try requirementProtocol.protocolName() + guard let associatedTypeIndex = availableAssociatedTypes.firstIndex(of: step.name) else { + throw AssociatedTypeResolutionError.missingAssociatedTypeIndex( + associatedTypeName: step.name, + protocolName: stepProtocolFullName, + availableAssociatedTypes: availableAssociatedTypes + ) + } - guard let associatedTypePWT = try? RuntimeFunctions.conformsToProtocol( - metadata: associatedTypeMetadata, - protocolDescriptor: requirementProtocol.descriptor - ) else { - throw AssociatedTypeResolutionError.associatedTypeDoesNotConformToProtocol( - associatedType: associatedTypeMetadata, - protocolName: currentProtocolName - ) - } + guard let baseRequirement = stepProtocol.baseRequirement else { + throw AssociatedTypeResolutionError.missingAssociatedTypeBaseRequirement(protocolName: stepProtocolFullName) + } - results[associatedTypeMetadata, default: []].append(associatedTypePWT) + let accessFunctionRequirements = stepProtocol.requirements.filter { + $0.flags.kind.isAssociatedTypeAccessFunction + } - } else if let dependentGenericParamType = paramNode.first(of: .dependentGenericParamType) { - // Direct generic parameter - record metadata mapping - guard let genericParamType = dependentGenericParamType.text else { - throw AssociatedTypeResolutionError.missingGenericParamTypeText(dependentGenericParamType: dependentGenericParamType) - } + guard let accessFunctionRequirement = accessFunctionRequirements[safe: associatedTypeIndex] else { + throw AssociatedTypeResolutionError.missingAssociatedTypeAccessFunctionRequirement( + index: associatedTypeIndex, + protocolName: stepProtocolFullName, + requirementCount: accessFunctionRequirements.count + ) + } - guard let conformingTypeMetadata = genericArguments[genericParamType] else { - throw AssociatedTypeResolutionError.missingConformingTypeMetadata( - genericParam: genericParamType, - availableParams: Array(genericArguments.keys) - ) - } + guard let conformingPWT = try RuntimeFunctions.conformsToProtocol( + metadata: currentMetadata, + protocolDescriptor: stepProtocol.descriptor + ) else { + throw AssociatedTypeResolutionError.conformingTypeDoesNotConformToProtocol( + conformingType: currentMetadata, + protocolName: stepProtocolFullName + ) + } - conformingTypeMetadataByGenericParam[genericParamType] = conformingTypeMetadata - } else { - throw AssociatedTypeResolutionError.unknownParamNodeStructure(paramNode: paramNode) - } + guard let nextMetadata = try? RuntimeFunctions.getAssociatedTypeWitness( + request: .init(), + protocolWitnessTable: conformingPWT, + conformingTypeMetadata: currentMetadata, + baseRequirement: baseRequirement, + associatedTypeRequirement: accessFunctionRequirement + ).value.resolve().metadata else { + throw AssociatedTypeResolutionError.failedToGetAssociatedTypeWitness( + conformingType: currentMetadata, + protocolName: stepProtocolFullName, + associatedTypeName: step.name + ) } - return results + return nextMetadata } /// Errors for associated type witness resolution @@ -747,34 +1270,34 @@ extension GenericSpecializer where MachO == MachOImage { @_spi(Support) extension GenericSpecializer { /// Errors that can occur during specialization - public enum SpecializerError: Error, LocalizedError { + public enum SpecializerError: LocalizedError { case notGenericType(type: TypeContextDescriptorWrapper) - case missingGenericContext - case invalidParameterIndex(index: Int, max: Int) - case requirementParsingFailed(reason: String) case candidateResolutionFailed(candidate: SpecializationRequest.Candidate, reason: String) + case candidateRequiresNestedSpecialization( + candidate: SpecializationRequest.Candidate, + parameterCount: Int + ) case metadataCreationFailed(typeName: String, reason: String) case witnessTableNotFound(typeName: String, protocolName: String) case specializationFailed(reason: String) + case unsupportedGenericParameter(parameterKind: GenericParamKind) public var errorDescription: String? { switch self { case .notGenericType(let type): return "Type is not generic: \(type)" - case .missingGenericContext: - return "Missing generic context" - case .invalidParameterIndex(let index, let max): - return "Invalid parameter index \(index), maximum is \(max)" - case .requirementParsingFailed(let reason): - return "Failed to parse requirement: \(reason)" case .candidateResolutionFailed(let candidate, let reason): return "Failed to resolve candidate \(candidate.typeName.name): \(reason)" + case .candidateRequiresNestedSpecialization(let candidate, let parameterCount): + return "Candidate \(candidate.typeName.name) is generic with \(parameterCount) parameter(s); pass Argument.specialized(...) instead of Argument.candidate(...)" case .metadataCreationFailed(let typeName, let reason): return "Failed to create metadata for \(typeName): \(reason)" case .witnessTableNotFound(let typeName, let protocolName): return "Witness table not found for \(typeName) conforming to \(protocolName)" case .specializationFailed(let reason): return "Specialization failed: \(reason)" + case .unsupportedGenericParameter(let parameterKind): + return "Unsupported generic parameter kind: \(parameterKind). TypePack (variadic generics) and Value generics are not implemented yet." } } } diff --git a/Sources/SwiftInterface/GenericSpecializer/IMPLEMENTATION_PLAN.md b/Sources/SwiftInterface/GenericSpecializer/IMPLEMENTATION_PLAN.md deleted file mode 100644 index 9da6f219..00000000 --- a/Sources/SwiftInterface/GenericSpecializer/IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,138 +0,0 @@ -# GenericSpecializer Implementation Plan - -## Overview - -`GenericSpecializer` provides an interactive API for specializing generic Swift types at runtime. Users can query the generic parameters and requirements of a type, receive candidate types that satisfy those requirements, make selections, and obtain specialized metadata including field offsets. - -## Key Design Decisions - -1. **Only protocol requirements require PWT**: `baseClass`, `layout`, and `sameType` requirements do not need special handling - only protocol conformance requirements require passing Protocol Witness Tables (in requirement order). - -2. **Separation from Indexer**: This functionality is a separate `GenericSpecializer` class that uses `ConformanceProvider` protocol to query conformance information. - -3. **Two-step API**: First call `makeRequest()` to get parameters and candidates, then call `specialize()` with user selections. - -## Implementation Steps - -### Phase 1: Core Models ✅ -- [x] `SpecializationRequest` - Request model with parameters, requirements, candidates -- [x] `SpecializationSelection` - User selection model -- [x] `SpecializationResult` - Result model with metadata, layout, fields -- [x] `SpecializationValidation` - Validation result model - -### Phase 2: ConformanceProvider ✅ -- [x] `ConformanceProvider` protocol definition -- [x] `IndexerConformanceProvider` implementation (SPI) -- [x] `CompositeConformanceProvider` for combining multiple providers -- [x] `EmptyConformanceProvider` for testing -- [x] `StandardLibraryConformanceProvider` for common stdlib conformances - -### Phase 3: GenericSpecializer Core ✅ -- [x] `GenericSpecializer` class definition -- [x] `makeRequest(for:)` - Create specialization request -- [x] Internal: Parse generic context and build parameter list -- [x] Internal: Find candidate types for each parameter -- [x] Internal: Build requirements from generic requirements (protocol, sameType, baseClass, layout) -- [x] Internal: Build associated type requirements - -### Phase 4: Specialization Execution ✅ -- [x] `validate(selection:for:)` - Validate user selections -- [x] `specialize(_:with:)` - Execute specialization -- [x] Internal: Build metadata array and witness table array (in requirement order) -- [x] Internal: Call MetadataAccessorFunction -- [x] Internal: Extract field offsets from specialized metadata (`result.fieldOffsets()`) - -### Phase 5: Testing & Refinement -- [x] Unit tests for core models (SpecializationSelection builder, validation) -- [x] Integration tests with real generic types (TestGenericStruct with multiple constraints) -- [ ] Edge case handling (nested generics, associated types) - future enhancement - -## File Structure - -``` -Sources/SwiftInterface/GenericSpecializer/ -├── IMPLEMENTATION_PLAN.md # This file -├── GenericSpecializer.swift # Main class -├── ConformanceProvider.swift # Protocol and implementations -└── Models/ - ├── SpecializationRequest.swift - ├── SpecializationSelection.swift - ├── SpecializationResult.swift - └── SpecializationValidation.swift -``` - -## API Summary - -```swift -// Create specializer (requires MachOImage for runtime specialization) -let specializer = GenericSpecializer(indexer: indexer) - -// Step 1: Get request with parameters and candidates -let request = try specializer.makeRequest(for: .struct(descriptor)) - -// Inspect parameters and their requirements -for param in request.parameters { - print("Parameter: \(param.name) (depth=\(param.depth), index=\(param.index))") - for req in param.requirements { - switch req { - case .protocol(let info): - print(" - Protocol: \(info.protocolName.name), needsPWT: \(info.requiresWitnessTable)") - case .baseClass(let node): - print(" - BaseClass: \(node)") - case .sameType(let node): - print(" - SameType: \(node)") - case .layout(let kind): - print(" - Layout: \(kind)") - } - } - print(" Candidates: \(param.candidates.map { $0.typeName.name })") -} - -// Step 2: User makes selections -let selection: SpecializationSelection = [ - "A": .metatype(Int.self), - "B": .metatype(String.self) -] - -// Step 3: Validate (optional) -let validation = specializer.validate(selection: selection, for: request) -guard validation.isValid else { - print("Validation errors: \(validation.errors)") - return -} - -// Step 4: Execute specialization (MachOImage only) -let result = try specializer.specialize(request, with: selection) - -// Use result -let metadata = try result.metadata() -let metadataWrapper = try result.resolveMetadata() -// Access resolved arguments -for arg in result.resolvedArguments { - print("\(arg.parameterName): witnessTables=\(arg.witnessTables.count)") -} -``` - -## Requirement Handling - -| Requirement Type | Needs PWT | Handling | -|-----------------|-----------|----------| -| Protocol (`T: P`) | Yes | Pass witness table to accessor (in requirement order) | -| Same Type (`T == U`) | No | Validation only | -| Base Class (`T: C`) | No | Validation only | -| Layout (`T: AnyObject`) | No | Validation only | - -**Note**: Generic parameter names are derived from depth and index (e.g., A, B, C... for depth=0; A1, B1, C1... for depth=1) since original names are not preserved in binaries. - -## Progress Tracking - -- **Current Phase**: Complete -- **Last Updated**: 2026-01-27 -- **Status**: Core Implementation Complete with Tests - -### Completed -- Phase 1: Core Models (SpecializationRequest, SpecializationSelection, SpecializationResult, SpecializationValidation) -- Phase 2: ConformanceProvider (protocol + IndexerConformanceProvider, CompositeConformanceProvider, EmptyConformanceProvider, StandardLibraryConformanceProvider) -- Phase 3: GenericSpecializer Core (makeRequest, requirement parsing, candidate finding) -- Phase 4: Specialization Execution (validate, specialize, metadata/witness table building, fieldOffsets) -- Phase 5: Testing (unit tests, integration tests with real generic types) diff --git a/Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift b/Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift index 821f5e94..1fd0cdbf 100644 --- a/Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift +++ b/Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift @@ -49,18 +49,29 @@ extension SpecializationRequest { /// Candidate types that satisfy all requirements public var candidates: [Candidate] + /// Invertible protocols (~Copyable / ~Escapable) that the parameter + /// suppresses. The set encodes which protocols are inverted, matching + /// the binary's encoding and the existing `hasCopyable` / + /// `hasEscapable` convention — e.g. `` produces a set + /// containing `.copyable`. `nil` means the parameter has no + /// `invertedProtocols` requirement and retains every invertible + /// protocol by default (the typical Swift case). + public let invertibleProtocols: InvertibleProtocolSet? + public init( name: String, index: Int, depth: Int, requirements: [Requirement], - candidates: [Candidate] = [] + candidates: [Candidate] = [], + invertibleProtocols: InvertibleProtocolSet? = nil ) { self.name = name self.index = index self.depth = depth self.requirements = requirements self.candidates = candidates + self.invertibleProtocols = invertibleProtocols } /// Protocol requirements that require witness tables (in order) @@ -116,6 +127,31 @@ extension SpecializationRequest { } } +// MARK: - CandidateOptions + +extension SpecializationRequest { + /// Knobs that adjust how candidate lists are produced for each parameter. + public struct CandidateOptions: OptionSet, Sendable { + public let rawValue: UInt8 + + public init(rawValue: UInt8) { + self.rawValue = rawValue + } + + /// Skip candidates whose type descriptor is itself generic. + /// + /// Selecting a generic candidate via `Argument.candidate(...)` would + /// throw `candidateRequiresNestedSpecialization` at `specialize` time; + /// callers that want a "directly specializable" list can opt into + /// filtering them out at request-build time. + public static let excludeGenerics = CandidateOptions(rawValue: 1 << 0) + + /// Default behaviour: include every candidate (mirrors the + /// pre-`CandidateOptions` API). + public static let `default`: CandidateOptions = [] + } +} + // MARK: - Candidate extension SpecializationRequest { @@ -127,12 +163,19 @@ extension SpecializationRequest { /// Source of this candidate public let source: Source + /// True when the candidate's type descriptor is itself generic. + /// Selecting such a candidate via `Argument.candidate(...)` will + /// throw `candidateRequiresNestedSpecialization` from `specialize`. + public let isGeneric: Bool + public init( typeName: TypeName, source: Source, + isGeneric: Bool = false ) { self.typeName = typeName self.source = source + self.isGeneric = isGeneric } /// Source of candidate type diff --git a/Sources/SwiftInterface/GenericSpecializer/Models/SpecializationResult.swift b/Sources/SwiftInterface/GenericSpecializer/Models/SpecializationResult.swift index ffb3a8d3..1833de4e 100644 --- a/Sources/SwiftInterface/GenericSpecializer/Models/SpecializationResult.swift +++ b/Sources/SwiftInterface/GenericSpecializer/Models/SpecializationResult.swift @@ -1,10 +1,20 @@ import Foundation import MachOSwiftSection -import OrderedCollections -/// Result of generic type specialization +/// Result of generic type specialization. +/// +/// Specialized metadata is allocated by the Swift runtime's metadata +/// allocator (via `swift_getGenericMetadata`) and lives in a runtime- +/// owned heap that is **not part of any MachO image** — neither the +/// type's defining image nor `MachOImage.current()` can claim it. +/// Consequently every accessor on this type and its `ResolvedArgument` +/// pointers operates exclusively in process memory; there is no +/// file-context overload because there is no file backing the data. public struct SpecializationResult: @unchecked Sendable { - /// Specialized metadata pointer + /// Pointer to the specialized metadata, in process memory. Never + /// resolves through a MachO file reader — the metadata cache that + /// `swift_getGenericMetadata` writes into is independent of any + /// loaded image. public let metadataPointer: Pointer /// Resolved generic arguments used for specialization @@ -68,40 +78,12 @@ extension SpecializationResult { resolvedArguments.first { $0.parameterName == parameterName } } - /// Get field offsets from the specialized metadata (struct only) - /// - Returns: Array of field offsets in bytes - public func fieldOffsets() throws -> [UInt32] { - let wrapper = try resolveMetadata() - switch wrapper { - case .struct(let structMetadata): - return try structMetadata.fieldOffsets() - default: - return [] - } - } - - /// Get field offsets from the specialized metadata with MachO context - /// - Parameter machO: The MachO image - /// - Returns: Array of field offsets in bytes - public func fieldOffsets(in machO: MachO) throws -> [UInt32] { - let wrapper = try resolveMetadata() - switch wrapper { - case .struct(let structMetadata): - return try structMetadata.fieldOffsets(in: machO) - default: - return [] - } - } - - /// Get the value witness table for layout information + /// Get the value witness table for layout information. Resolves + /// in-process; specialized metadata never resides in a MachO image, + /// so the file-context overload of `MetadataWrapper.valueWitnessTable` + /// would crash with SIGBUS and is intentionally not exposed here. public func valueWitnessTable() throws -> ValueWitnessTable { let wrapper = try resolveMetadata() return try wrapper.valueWitnessTable() } - - /// Get the value witness table with MachO context - public func valueWitnessTable(in machO: MachO) throws -> ValueWitnessTable { - let wrapper = try resolveMetadata() - return try wrapper.valueWitnessTable(in: machO) - } } diff --git a/Sources/SwiftInterface/GenericSpecializer/Models/SpecializationSelection.swift b/Sources/SwiftInterface/GenericSpecializer/Models/SpecializationSelection.swift index 79704fd0..af2318bc 100644 --- a/Sources/SwiftInterface/GenericSpecializer/Models/SpecializationSelection.swift +++ b/Sources/SwiftInterface/GenericSpecializer/Models/SpecializationSelection.swift @@ -10,16 +10,6 @@ public struct SpecializationSelection: Sendable { self.arguments = arguments } - /// Convenience initializer with variadic arguments - public init(_ arguments: (String, Argument)...) { - self.arguments = Dictionary(uniqueKeysWithValues: arguments) - } - - /// Convenience initializer with dictionary literal - public init(_ arguments: [String: Argument]) { - self.arguments = arguments - } - /// Get argument for a parameter name public subscript(parameterName: String) -> Argument? { arguments[parameterName] diff --git a/Sources/SwiftInterface/GenericSpecializer/Models/SpecializationValidation.swift b/Sources/SwiftInterface/GenericSpecializer/Models/SpecializationValidation.swift index 7e27218f..fae95893 100644 --- a/Sources/SwiftInterface/GenericSpecializer/Models/SpecializationValidation.swift +++ b/Sources/SwiftInterface/GenericSpecializer/Models/SpecializationValidation.swift @@ -46,20 +46,6 @@ extension SpecializationValidation { actualType: String ) - /// Selected type does not satisfy a same-type requirement - case sameTypeRequirementNotSatisfied( - parameterName: String, - expectedType: String, - actualType: String - ) - - /// Selected type does not satisfy a base class requirement - case baseClassRequirementNotSatisfied( - parameterName: String, - expectedBaseClass: String, - actualType: String - ) - /// Selected type does not satisfy a layout requirement case layoutRequirementNotSatisfied( parameterName: String, @@ -67,30 +53,24 @@ extension SpecializationValidation { actualType: String ) - /// Could not resolve candidate type to metadata - case candidateResolutionFailed( + /// Could not resolve metadata for the parameter — preflight + /// could not run conformance/layout checks. `specialize` runs + /// the same metadata resolution path, so the failure is + /// guaranteed to surface there as well; reporting it here lets + /// the caller see the diagnostic before the accessor call. + case metadataResolutionFailed(parameterName: String, reason: String) + + /// Could not construct the protocol descriptor that a + /// requirement references — preflight could not run the + /// conformance check. Distinct from `protocolNotInIndexer` + /// (which is a warning): here the indexer *did* find the entry, + /// but materializing it as a `MachOSwiftSection.Protocol` failed. + case protocolDescriptorResolutionFailed( parameterName: String, - candidateTypeName: String, - reason: String - ) - - /// Associated type could not be resolved - case associatedTypeResolutionFailed( - parameterName: String, - associatedTypePath: [String], + protocolName: String, reason: String ) - /// The selected type is generic and requires further specialization - case requiresFurtherSpecialization( - parameterName: String, - typeName: String, - genericParameters: [String] - ) - - /// Unknown or unexpected error - case unknown(String) - public var description: String { switch self { case .missingArgument(let name): @@ -99,26 +79,14 @@ extension SpecializationValidation { case .protocolRequirementNotSatisfied(let param, let proto, let actual): return "Type '\(actual)' for parameter '\(param)' does not conform to protocol '\(proto)'" - case .sameTypeRequirementNotSatisfied(let param, let expected, let actual): - return "Type '\(actual)' for parameter '\(param)' must be same as '\(expected)'" - - case .baseClassRequirementNotSatisfied(let param, let base, let actual): - return "Type '\(actual)' for parameter '\(param)' must inherit from '\(base)'" - case .layoutRequirementNotSatisfied(let param, let layout, let actual): return "Type '\(actual)' for parameter '\(param)' does not satisfy layout requirement '\(layout)'" - case .candidateResolutionFailed(let param, let candidate, let reason): - return "Cannot resolve candidate '\(candidate)' for parameter '\(param)': \(reason)" - - case .associatedTypeResolutionFailed(let param, let path, let reason): - return "Cannot resolve associated type '\(param).\(path.joined(separator: "."))': \(reason)" - - case .requiresFurtherSpecialization(let param, let type, let genericParams): - return "Type '\(type)' for parameter '\(param)' is generic and requires specialization of: \(genericParams.joined(separator: ", "))" + case .metadataResolutionFailed(let param, let reason): + return "Could not resolve metadata for parameter '\(param)': \(reason)" - case .unknown(let message): - return "Validation error: \(message)" + case .protocolDescriptorResolutionFailed(let param, let proto, let reason): + return "Could not construct protocol descriptor for '\(proto)' (parameter '\(param)'): \(reason)" } } } @@ -129,31 +97,42 @@ extension SpecializationValidation { extension SpecializationValidation { /// Validation warning public enum Warning: Sendable, CustomStringConvertible { - /// The selected type may cause performance issues - case potentialPerformanceIssue( - parameterName: String, - reason: String - ) + /// Extra argument provided that is not needed + case extraArgument(parameterName: String) - /// The selected type is deprecated - case deprecatedType( + /// User supplied a key matching an associated-type path + /// (e.g. "A.Element"). Associated types are derived during + /// specialization and cannot be set directly; the entry is ignored. + case associatedTypePathInSelection(path: String) + + /// A parameter requirement references a protocol that the indexer + /// doesn't have a definition for, so runtime preflight cannot + /// validate conformance. Add the protocol's defining image as a + /// sub-indexer to enable the check. + case protocolNotInIndexer(parameterName: String, protocolName: String) + + /// `RuntimeFunctions.conformsToProtocol` itself threw — preflight + /// could not determine whether the parameter conforms. Distinct + /// from `protocolRequirementNotSatisfied` (an error), which fires + /// when the call ran successfully and returned `nil`. A throw + /// here usually indicates a transient runtime issue or a + /// malformed protocol descriptor pointer. + case conformanceCheckFailed( parameterName: String, - typeName: String + protocolName: String, + reason: String ) - /// Extra argument provided that is not needed - case extraArgument(parameterName: String) - public var description: String { switch self { - case .potentialPerformanceIssue(let param, let reason): - return "Parameter '\(param)' may cause performance issues: \(reason)" - - case .deprecatedType(let param, let type): - return "Type '\(type)' for parameter '\(param)' is deprecated" - case .extraArgument(let param): return "Extra argument '\(param)' is not needed for this specialization" + case .associatedTypePathInSelection(let path): + return "Selection key '\(path)' refers to an associated-type path; associated types are derived from the substituted parameter and cannot be set directly" + case .protocolNotInIndexer(let param, let proto): + return "Cannot validate conformance of parameter '\(param)' to '\(proto)': protocol descriptor not found in indexer (add the defining image as a sub-indexer to enable the check)" + case .conformanceCheckFailed(let param, let proto, let reason): + return "Conformance check for parameter '\(param)' against protocol '\(proto)' failed to run: \(reason)" } } } diff --git a/Sources/SwiftInterface/GenericSpecializer/REVIEW_FIXUPS.md b/Sources/SwiftInterface/GenericSpecializer/REVIEW_FIXUPS.md new file mode 100644 index 00000000..6cd318c0 --- /dev/null +++ b/Sources/SwiftInterface/GenericSpecializer/REVIEW_FIXUPS.md @@ -0,0 +1,51 @@ +# GenericSpecializer — Review Follow-up Fixes + +Tracks the work derived from the `feature/generic-specializer` review. +Item labels match the original review (`M1`–`M12`, `C1`–`C8`). + +## Already fixed (earlier commits on this branch) + +- **H1** — `resolveAssociatedTypeWitnesses` 的 `OrderedDictionary` 分组在不同链解析到同一 leaf metadata 时会破坏 binary PWT 顺序。改回 `[ProtocolWitnessTable]` 线性数组。 +- **C1** — `mergedRequirements` 注释修正:原文断言 conditional invertible 段"只含 `.invertedProtocols`",实际可同时含 marker `.protocol`,注释更新为说明每条过滤路径。 +- **C2** — `SpecializationResult.fieldOffsets()` / `fieldOffsets(in:)` 删除(不是 `SpecializationResult` 的职责)。 +- **C5** — `SpecializerError` 删 3 个未触发 case;`SpecializationValidation.Error` 删 6 个未发出 case;`Warning` 删 2 个未发出 case。 + +## In scope this round + +### Group 1 — zero-cost quick fixes + +- [x] **T1 (C7)** — `IndexerConformanceProvider` doc:必须先 `prepare()` 才能传给 `GenericSpecializer` +- [x] **T2 (C8)** — `GenericSpecializer` doc:`specialize` / `runtimePreflight` 仅在 `MachO == MachOImage` 时可用 +- [x] **T3 (M10)** — 测试:`makeRequest` 在非 generic 类型上应抛 `notGenericType` +- [x] **T4 (M8)** — 测试:`validate` 对未声明的参数发 `.extraArgument` warning +- [x] **T5 (C4)** — 删除 `SpecializationSelection` 的 `init(_:variadic)` 与 `init(_:unlabeled-dict)` 两个重复重载 + +### Group 2 — coverage gaps + +- [x] **T6 (M2a)** — 测试:`Argument.metadata(...)` 成功路径 +- [x] **T7 (M2b)** — 测试:`Argument.candidate(...)` 成功路径(非 generic candidate) +- [x] **T8 (M2c)** — 测试:`Argument.specialized(...)` 递归 specialize(嵌套 generic 类型作为 GP) +- [x] **T9 (M3)** — 测试:三层嵌套 `~Copyable` `specialize` 端到端(fixture 已存在,仅缺 specialize 调用) +- [x] **T10 (M5)** — 新增 `~Escapable` 与 `~Copyable & ~Escapable` fixture + 测试(dual 用空 enum,由于工具链 bug 暂去掉条件扩展) + +### Group 3 — enum / class coverage + +- [x] **T11 (M1a)** — 新增 `TestGenericEnum` fixture + makeRequest / specialize 测试 +- [x] **T12 (M1b)** — 新增 `TestGenericClass` fixture + makeRequest / specialize 测试 + +### Final + +- [ ] **T13** — 跑 `swift test --filter GenericSpecializationTests` 确认全绿;跑 `swift build` 全包确认无外部回归 + +## Deferred (not fixed this round, recorded for record) + +| 项 | 原因 | +|---|---| +| M4 (`where A == B` / sameType / baseClass non-AnyObject) | fixture 是否能 compile 本身需要验证;ROI 与风险不匹配 | +| M6 (`CompositeConformanceProvider` / `StandardLibraryConformanceProvider` 单测) | 简单 wrapper,价值有限 | +| M7 (`metadata()` / `valueWitnessTable()` / `argument(for:)` / `fullPath` 公开 API caller 测试) | 隐式被 M2 / M3 覆盖 | +| M9 (`Outer.Inner` 内层多 GP) | 路径已被 `perLevelNewParameterCounts` 覆盖;fixture 投入大 | +| M11 (marker / ObjC-only protocol silent skip) | 边角行为,目前没观察到 bug | +| M12 (`runtimePreflight` indexer 缺失协议时静默 skip) | 边角行为 | +| C3 (`StandardLibraryConformanceProvider` doc 警告) | 可在使用方加注释,非紧急 | +| C6 (`extractAssociatedPath` 防御日志) | 纯防御,未被触发 | diff --git a/Sources/SwiftInterface/SwiftInterfaceIndexer.swift b/Sources/SwiftInterface/SwiftInterfaceIndexer.swift index 6a953ed8..84baaa50 100644 --- a/Sources/SwiftInterface/SwiftInterfaceIndexer.swift +++ b/Sources/SwiftInterface/SwiftInterfaceIndexer.swift @@ -94,17 +94,42 @@ public final class SwiftInterfaceIndexer]? + var allProtocols: [MachOIndexedValue]? + var allProtocolConformances: [MachOIndexedValue]? + var allAssociatedTypes: [MachOIndexedValue]? + var allProtocolConformancesByTypeName: OrderedDictionary>>? + var allAssociatedTypesByTypeName: OrderedDictionary>>? + var allConformingTypesByProtocolName: OrderedDictionary>>? + var allRootTypeDefinitions: OrderedDictionary>? + var allAllTypeDefinitions: OrderedDictionary>? + var allRootProtocolDefinitions: OrderedDictionary>? + var allAllProtocolDefinitions: OrderedDictionary>? + var allTypeExtensionDefinitions: OrderedDictionary]>? + var allProtocolExtensionDefinitions: OrderedDictionary]>? + var allTypeAliasExtensionDefinitions: OrderedDictionary]>? + var allConformanceExtensionDefinitions: OrderedDictionary]>? + var allGlobalVariableDefinitions: [MachOIndexedValue]? + var allGlobalFunctionDefinitions: [MachOIndexedValue]? + } + public let machO: MachO @Mutex public private(set) var configuration: SwiftInterfaceIndexConfiguration = .init() - + @Mutex public private(set) var subIndexers: [SwiftInterfaceIndexer] = [] @usableFromInline let currentStorage = Storage() + @Mutex + @usableFromInline + var allStorageCache: AllStorageCache = AllStorageCache() + let eventDispatcher: SwiftInterfaceEvents.Dispatcher = .init() @Mutex @@ -128,10 +153,12 @@ public final class SwiftInterfaceIndexer) { subIndexers.append(subIndexer) + allStorageCache = AllStorageCache() } public func removeSubIndexer(at index: Int) { subIndexers.remove(at: index) + allStorageCache = AllStorageCache() } public func prepare() async throws { @@ -198,7 +225,8 @@ public final class SwiftInterfaceIndexer] { - currentStorage.types.map { .init(machO: machO, value: $0) } + subIndexers.flatMap { $0.allTypes } + if let cached = allStorageCache.allTypes { return cached } + let result = currentStorage.types.map { MachOIndexedValue(machO: machO, value: $0) } + subIndexers.flatMap { $0.allTypes } + allStorageCache.allTypes = result + return result } public var allProtocols: [MachOIndexedValue] { - currentStorage.protocols.map { .init(machO: machO, value: $0) } + subIndexers.flatMap { $0.allProtocols } + if let cached = allStorageCache.allProtocols { return cached } + let result = currentStorage.protocols.map { MachOIndexedValue(machO: machO, value: $0) } + subIndexers.flatMap { $0.allProtocols } + allStorageCache.allProtocols = result + return result } public var allProtocolConformances: [MachOIndexedValue] { - currentStorage.protocolConformances.map { .init(machO: machO, value: $0) } + subIndexers.flatMap { $0.allProtocolConformances } + if let cached = allStorageCache.allProtocolConformances { return cached } + let result = currentStorage.protocolConformances.map { MachOIndexedValue(machO: machO, value: $0) } + subIndexers.flatMap { $0.allProtocolConformances } + allStorageCache.allProtocolConformances = result + return result } public var allAssociatedTypes: [MachOIndexedValue] { - currentStorage.associatedTypes.map { .init(machO: machO, value: $0) } + subIndexers.flatMap { $0.allAssociatedTypes } + if let cached = allStorageCache.allAssociatedTypes { return cached } + let result = currentStorage.associatedTypes.map { MachOIndexedValue(machO: machO, value: $0) } + subIndexers.flatMap { $0.allAssociatedTypes } + allStorageCache.allAssociatedTypes = result + return result } public var allProtocolConformancesByTypeName: OrderedDictionary>> { + if let cached = allStorageCache.allProtocolConformancesByTypeName { return cached } var result: OrderedDictionary>> = currentStorage.protocolConformancesByTypeName.mapValues { $0.mapValues { .init(machO: machO, value: $0) } } for subIndexer in subIndexers { for (typeName, conformances) in subIndexer.allProtocolConformancesByTypeName { result[typeName, default: [:]].merge(conformances) { current, _ in current } } } + allStorageCache.allProtocolConformancesByTypeName = result return result } public var allAssociatedTypesByTypeName: OrderedDictionary>> { + if let cached = allStorageCache.allAssociatedTypesByTypeName { return cached } var result: OrderedDictionary>> = currentStorage.associatedTypesByTypeName.mapValues { $0.mapValues { .init(machO: machO, value: $0) } } for subIndexer in subIndexers { for (typeName, associatedTypes) in subIndexer.allAssociatedTypesByTypeName { result[typeName, default: [:]].merge(associatedTypes) { current, _ in current } } } + allStorageCache.allAssociatedTypesByTypeName = result return result } public var allConformingTypesByProtocolName: OrderedDictionary>> { + if let cached = allStorageCache.allConformingTypesByProtocolName { return cached } var result: OrderedDictionary>> = [:] for (protocolName, typeNames) in currentStorage.conformingTypesByProtocolName { result[protocolName] = OrderedSet(typeNames.map { .init(machO: machO, value: $0) }) @@ -810,87 +863,110 @@ extension SwiftInterfaceIndexer { result[protocolName, default: []].formUnion(typeNames) } } + allStorageCache.allConformingTypesByProtocolName = result return result } public var allRootTypeDefinitions: OrderedDictionary> { + if let cached = allStorageCache.allRootTypeDefinitions { return cached } var result = currentStorage.rootTypeDefinitions.mapValues { MachOIndexedValue(machO: machO, value: $0) } for subIndexer in subIndexers { result.merge(subIndexer.allRootTypeDefinitions) { current, _ in current } } + allStorageCache.allRootTypeDefinitions = result return result } public var allAllTypeDefinitions: OrderedDictionary> { + if let cached = allStorageCache.allAllTypeDefinitions { return cached } var result = currentStorage.allTypeDefinitions.mapValues { MachOIndexedValue(machO: machO, value: $0) } for subIndexer in subIndexers { result.merge(subIndexer.allAllTypeDefinitions) { current, _ in current } } + allStorageCache.allAllTypeDefinitions = result return result } public var allRootProtocolDefinitions: OrderedDictionary> { + if let cached = allStorageCache.allRootProtocolDefinitions { return cached } var result = currentStorage.rootProtocolDefinitions.mapValues { MachOIndexedValue(machO: machO, value: $0) } for subIndexer in subIndexers { result.merge(subIndexer.allRootProtocolDefinitions) { current, _ in current } } + allStorageCache.allRootProtocolDefinitions = result return result } public var allAllProtocolDefinitions: OrderedDictionary> { + if let cached = allStorageCache.allAllProtocolDefinitions { return cached } var result = currentStorage.allProtocolDefinitions.mapValues { MachOIndexedValue(machO: machO, value: $0) } for subIndexer in subIndexers { result.merge(subIndexer.allAllProtocolDefinitions) { prevValue, nextValue in prevValue } } + allStorageCache.allAllProtocolDefinitions = result return result } public var allTypeExtensionDefinitions: OrderedDictionary]> { + if let cached = allStorageCache.allTypeExtensionDefinitions { return cached } var result: OrderedDictionary]> = currentStorage.typeExtensionDefinitions.mapValues { $0.map { .init(machO: machO, value: $0) } } for subIndexer in subIndexers { for (extensionName, definitions) in subIndexer.allTypeExtensionDefinitions { result[extensionName, default: []].append(contentsOf: definitions) } } + allStorageCache.allTypeExtensionDefinitions = result return result } public var allProtocolExtensionDefinitions: OrderedDictionary]> { + if let cached = allStorageCache.allProtocolExtensionDefinitions { return cached } var result: OrderedDictionary]> = currentStorage.protocolExtensionDefinitions.mapValues { $0.map { .init(machO: machO, value: $0) } } for subIndexer in subIndexers { for (extensionName, definitions) in subIndexer.allProtocolExtensionDefinitions { result[extensionName, default: []].append(contentsOf: definitions) } } + allStorageCache.allProtocolExtensionDefinitions = result return result } public var allTypeAliasExtensionDefinitions: OrderedDictionary]> { + if let cached = allStorageCache.allTypeAliasExtensionDefinitions { return cached } var result: OrderedDictionary]> = currentStorage.typeAliasExtensionDefinitions.mapValues { $0.map { .init(machO: machO, value: $0) } } for subIndexer in subIndexers { for (extensionName, definitions) in subIndexer.allTypeAliasExtensionDefinitions { result[extensionName, default: []].append(contentsOf: definitions) } } + allStorageCache.allTypeAliasExtensionDefinitions = result return result } public var allConformanceExtensionDefinitions: OrderedDictionary]> { + if let cached = allStorageCache.allConformanceExtensionDefinitions { return cached } var result: OrderedDictionary]> = currentStorage.conformanceExtensionDefinitions.mapValues { $0.map { .init(machO: machO, value: $0) } } for subIndexer in subIndexers { for (extensionName, definitions) in subIndexer.allConformanceExtensionDefinitions { result[extensionName, default: []].append(contentsOf: definitions) } } + allStorageCache.allConformanceExtensionDefinitions = result return result } public var allGlobalVariableDefinitions: [MachOIndexedValue] { - currentStorage.globalVariableDefinitions.map { .init(machO: machO, value: $0) } + subIndexers.flatMap { $0.allGlobalVariableDefinitions } + if let cached = allStorageCache.allGlobalVariableDefinitions { return cached } + let result = currentStorage.globalVariableDefinitions.map { MachOIndexedValue(machO: machO, value: $0) } + subIndexers.flatMap { $0.allGlobalVariableDefinitions } + allStorageCache.allGlobalVariableDefinitions = result + return result } public var allGlobalFunctionDefinitions: [MachOIndexedValue] { - currentStorage.globalFunctionDefinitions.map { .init(machO: machO, value: $0) } + subIndexers.flatMap { $0.allGlobalFunctionDefinitions } + if let cached = allStorageCache.allGlobalFunctionDefinitions { return cached } + let result = currentStorage.globalFunctionDefinitions.map { MachOIndexedValue(machO: machO, value: $0) } + subIndexers.flatMap { $0.allGlobalFunctionDefinitions } + allStorageCache.allGlobalFunctionDefinitions = result + return result } } diff --git a/Tests/IntegrationTests/DyldCacheAssociatedTypeTests.swift b/Tests/IntegrationTests/MachOSwiftSection/DyldCacheAssociatedTypeTests.swift similarity index 100% rename from Tests/IntegrationTests/DyldCacheAssociatedTypeTests.swift rename to Tests/IntegrationTests/MachOSwiftSection/DyldCacheAssociatedTypeTests.swift diff --git a/Tests/IntegrationTests/MetadataAccessorTests.swift b/Tests/IntegrationTests/MachOSwiftSection/MetadataAccessorTests.swift similarity index 98% rename from Tests/IntegrationTests/MetadataAccessorTests.swift rename to Tests/IntegrationTests/MachOSwiftSection/MetadataAccessorTests.swift index df517adf..86aaa663 100644 --- a/Tests/IntegrationTests/MetadataAccessorTests.swift +++ b/Tests/IntegrationTests/MachOSwiftSection/MetadataAccessorTests.swift @@ -12,13 +12,6 @@ import MachOFixtureSupport import SwiftUI #endif -enum MultiPayloadEnumTests { - case closure(() -> Void) - case object(NSObject) - case tuple(a: Int, b: Double) - case empty -} - struct GenericStructNonRequirement { var field1: Double var field2: A diff --git a/Tests/IntegrationTests/OpaqueTypeTests.swift b/Tests/IntegrationTests/MachOSwiftSection/OpaqueTypeTests.swift similarity index 100% rename from Tests/IntegrationTests/OpaqueTypeTests.swift rename to Tests/IntegrationTests/MachOSwiftSection/OpaqueTypeTests.swift diff --git a/Tests/IntegrationTests/PrimitiveTypeMappingTests.swift b/Tests/IntegrationTests/MachOSwiftSection/PrimitiveTypeMappingTests.swift similarity index 100% rename from Tests/IntegrationTests/PrimitiveTypeMappingTests.swift rename to Tests/IntegrationTests/MachOSwiftSection/PrimitiveTypeMappingTests.swift diff --git a/Tests/IntegrationTests/ProtocolGenericContextTests.swift b/Tests/IntegrationTests/MachOSwiftSection/ProtocolGenericContextTests.swift similarity index 100% rename from Tests/IntegrationTests/ProtocolGenericContextTests.swift rename to Tests/IntegrationTests/MachOSwiftSection/ProtocolGenericContextTests.swift diff --git a/Tests/IntegrationTests/ProtocolRequirementSignatureTests.swift b/Tests/IntegrationTests/MachOSwiftSection/ProtocolRequirementSignatureTests.swift similarity index 100% rename from Tests/IntegrationTests/ProtocolRequirementSignatureTests.swift rename to Tests/IntegrationTests/MachOSwiftSection/ProtocolRequirementSignatureTests.swift diff --git a/Tests/MachOSymbolsTests/DyldCacheSymbolSimpleTests.swift b/Tests/IntegrationTests/MachOSymbols/DyldCacheSymbolSimpleTests.swift similarity index 100% rename from Tests/MachOSymbolsTests/DyldCacheSymbolSimpleTests.swift rename to Tests/IntegrationTests/MachOSymbols/DyldCacheSymbolSimpleTests.swift diff --git a/Tests/MachOSymbolsTests/DyldCacheSymbolTests.swift b/Tests/IntegrationTests/MachOSymbols/DyldCacheSymbolTests.swift similarity index 100% rename from Tests/MachOSymbolsTests/DyldCacheSymbolTests.swift rename to Tests/IntegrationTests/MachOSymbols/DyldCacheSymbolTests.swift diff --git a/Tests/MachOSymbolsTests/ExternalSymbolTests.swift b/Tests/IntegrationTests/MachOSymbols/ExternalSymbolTests.swift similarity index 100% rename from Tests/MachOSymbolsTests/ExternalSymbolTests.swift rename to Tests/IntegrationTests/MachOSymbols/ExternalSymbolTests.swift diff --git a/Tests/MachOSymbolsTests/MachOFileSymbolTests.swift b/Tests/IntegrationTests/MachOSymbols/MachOFileSymbolTests.swift similarity index 100% rename from Tests/MachOSymbolsTests/MachOFileSymbolTests.swift rename to Tests/IntegrationTests/MachOSymbols/MachOFileSymbolTests.swift diff --git a/Tests/IntegrationTests/MachOSymbols/SymbolIndexStoreTests.swift b/Tests/IntegrationTests/MachOSymbols/SymbolIndexStoreTests.swift new file mode 100644 index 00000000..193e2de1 --- /dev/null +++ b/Tests/IntegrationTests/MachOSymbols/SymbolIndexStoreTests.swift @@ -0,0 +1,18 @@ +import Foundation +import Testing +import MachO +@_spi(Internals) @testable import MachOSymbols +@testable import MachOTestingSupport +import MachOFixtureSupport + +@Suite +final class SymbolIndexStoreTests: MachOImageTests { + override class var imageName: MachOImageName { + .SwiftUI + } + + @Test func main() async throws { + SymbolIndexStore.shared.prepare(in: machOImage) + ProcessMemory.report() + } +} diff --git a/Tests/SwiftDumpTests/DyldCacheDumpTests.swift b/Tests/IntegrationTests/SwiftDump/DyldCacheDumpTests.swift similarity index 100% rename from Tests/SwiftDumpTests/DyldCacheDumpTests.swift rename to Tests/IntegrationTests/SwiftDump/DyldCacheDumpTests.swift diff --git a/Tests/SwiftDumpTests/MachOFileDumpTests.swift b/Tests/IntegrationTests/SwiftDump/MachOFileDumpTests.swift similarity index 100% rename from Tests/SwiftDumpTests/MachOFileDumpTests.swift rename to Tests/IntegrationTests/SwiftDump/MachOFileDumpTests.swift diff --git a/Tests/SwiftDumpTests/MachOImageDumpTests.swift b/Tests/IntegrationTests/SwiftDump/MachOImageDumpTests.swift similarity index 100% rename from Tests/SwiftDumpTests/MachOImageDumpTests.swift rename to Tests/IntegrationTests/SwiftDump/MachOImageDumpTests.swift diff --git a/Tests/SwiftDumpTests/XcodeMachOFileDumpTests.swift b/Tests/IntegrationTests/SwiftDump/XcodeMachOFileDumpTests.swift similarity index 100% rename from Tests/SwiftDumpTests/XcodeMachOFileDumpTests.swift rename to Tests/IntegrationTests/SwiftDump/XcodeMachOFileDumpTests.swift diff --git a/Tests/SwiftInspectionTests/ClassHierarchyDumpTests.swift b/Tests/IntegrationTests/SwiftInspection/ClassHierarchyDumpTests.swift similarity index 100% rename from Tests/SwiftInspectionTests/ClassHierarchyDumpTests.swift rename to Tests/IntegrationTests/SwiftInspection/ClassHierarchyDumpTests.swift diff --git a/Tests/SwiftInspectionTests/MultiPayloadEnumTests.swift b/Tests/IntegrationTests/SwiftInspection/MultiPayloadEnumTests.swift similarity index 100% rename from Tests/SwiftInspectionTests/MultiPayloadEnumTests.swift rename to Tests/IntegrationTests/SwiftInspection/MultiPayloadEnumTests.swift diff --git a/Tests/SwiftInterfaceTests/SwiftInterfaceBuilderTests.swift b/Tests/IntegrationTests/SwiftInterface/SwiftInterfaceBuilderTests.swift similarity index 100% rename from Tests/SwiftInterfaceTests/SwiftInterfaceBuilderTests.swift rename to Tests/IntegrationTests/SwiftInterface/SwiftInterfaceBuilderTests.swift diff --git a/Tests/SwiftInterfaceTests/SwiftInterfaceIndexerTests.swift b/Tests/IntegrationTests/SwiftInterface/SwiftInterfaceIndexerTests.swift similarity index 100% rename from Tests/SwiftInterfaceTests/SwiftInterfaceIndexerTests.swift rename to Tests/IntegrationTests/SwiftInterface/SwiftInterfaceIndexerTests.swift diff --git a/Tests/IntegrationTests/SymbolIndexStoreTests.swift b/Tests/IntegrationTests/SymbolIndexStoreTests.swift deleted file mode 100644 index 43125cec..00000000 --- a/Tests/IntegrationTests/SymbolIndexStoreTests.swift +++ /dev/null @@ -1,76 +0,0 @@ -import Foundation -import Testing -import Dependencies -@testable import MachOSwiftSection -@testable import MachOTestingSupport -import MachOFixtureSupport -@_spi(Internals) @testable import MachOSymbols -@_spi(Internals) @testable import MachOCaches - -final class SymbolIndexStoreTests: DyldCacheTests, @unchecked Sendable { - override class var cacheImageName: MachOImageName { - .SwiftUICore - } - - @Dependency(\.symbolIndexStore) - var symbolIndexStore - -// @Test func memberSymbols() async throws { -// let memberSymbols = SymbolIndexStore.shared.memberSymbols(of: .variableInExtension, in: machOFileInMainCache) -// for memberSymbol in memberSymbols { -// memberSymbol.demangledNode.print(using: .default).print() -// memberSymbol.demangledNode.description.print() -// print("----------------------------") -// } -// } -// -// -// @Test func dependentGenericSignature() async throws { -// for symbol in SymbolIndexStore.shared.memberSymbols(of: .allocatorInExtension, .variableInExtension, .functionInExtension, .staticVariableInExtension, .staticFunctionInExtension, in: machOFileInMainCache) { -// guard let identifier = symbol.demangledNode.identifier else { continue } -// guard let dependentGenericSignature = symbol.demangledNode.first(of: .dependentGenericSignature) else { continue } -// identifier.print() -// dependentGenericSignature.print(using: .default).print() -// let nodes = dependentGenericSignature.all(of: .requirementKinds) -// for node in nodes { -// node.print(using: .default).print() -// node.description.print() -// } -// print("----------------------------") -// } -// } - - @Test func globalSymbols() async throws { - let symbols = symbolIndexStore.globalSymbols(of: .function, in: machOFileInCache) - for symbol in symbols { - await symbol.demangledNode.print(using: .default).print() - symbol.demangledNode.description.print() - print("----------------------------") - } - } - - @available(macOS 13.0, iOS 16.0, *) - @Test func symbols() async throws { - let clock = ContinuousClock() - let machO = machOFileInCache - let duration = clock.measure { - _ = symbolIndexStore.allSymbols(in: machO) - } - print(duration) - guard let memberSymbolsByKind = symbolIndexStore.storage(in: machO)?.memberSymbolsByKind else { - return - } - for (kind, memberSymbolsByName) in memberSymbolsByKind { - print("Kind: ", kind.description) - for (name, memberSymbolsByNode) in memberSymbolsByName { - print("Name: ", name) - for (node, _) in memberSymbolsByNode { - print("Node: ") - print(node) - await print(node.print()) - } - } - print("---------------------") - } - } -} diff --git a/Tests/TypeIndexingTests/APINodesManagerTests.swift b/Tests/IntegrationTests/TypeIndexing/APINodesManagerTests.swift similarity index 100% rename from Tests/TypeIndexingTests/APINodesManagerTests.swift rename to Tests/IntegrationTests/TypeIndexing/APINodesManagerTests.swift diff --git a/Tests/TypeIndexingTests/SDKIndexerTests.swift b/Tests/IntegrationTests/TypeIndexing/SDKIndexerTests.swift similarity index 100% rename from Tests/TypeIndexingTests/SDKIndexerTests.swift rename to Tests/IntegrationTests/TypeIndexing/SDKIndexerTests.swift diff --git a/Tests/TypeIndexingTests/SourceKitManagerTests.swift b/Tests/IntegrationTests/TypeIndexing/SourceKitManagerTests.swift similarity index 100% rename from Tests/TypeIndexingTests/SourceKitManagerTests.swift rename to Tests/IntegrationTests/TypeIndexing/SourceKitManagerTests.swift diff --git a/Tests/TypeIndexingTests/SwiftInterfaceParserTests.swift b/Tests/IntegrationTests/TypeIndexing/SwiftInterfaceParserTests.swift similarity index 100% rename from Tests/TypeIndexingTests/SwiftInterfaceParserTests.swift rename to Tests/IntegrationTests/TypeIndexing/SwiftInterfaceParserTests.swift diff --git a/Tests/MachOCachesTests/SharedCacheTests.swift b/Tests/MachOCachesTests/SharedCacheTests.swift new file mode 100644 index 00000000..88bed18d --- /dev/null +++ b/Tests/MachOCachesTests/SharedCacheTests.swift @@ -0,0 +1,358 @@ +import Foundation +import Testing +import os +@_spi(Internals) import MachOCaches + +@Suite("SharedCache.resolve") +struct SharedCacheResolveTests { + /// Single-threaded sanity: a hit reuses the storage, a miss runs build, + /// and a `nil` build is not cached. + @Test func singleThreadedHitMissAndNilDontCache() { + let cache = TestCache() + + let first = cache.resolve(key: AnyHashable("a")) { 1 } + #expect(first == 1) + + // Second call hits the cache: the build closure must not run. + let second = cache.resolve(key: AnyHashable("a")) { + Issue.record("build was called for an already-cached key") + return 99 + } + #expect(second == 1) + + // A `nil` build is not cached, so the next call gets a fresh attempt. + let nilFirst: Int? = cache.resolve(key: AnyHashable("b")) { nil } + #expect(nilFirst == nil) + + let nilRetry = cache.resolve(key: AnyHashable("b")) { 7 } + #expect(nilRetry == 7) + } + + /// Concurrent calls for the **same** key must share one build — the + /// promise-based marker is the whole point of this refactor over the + /// previous "build under the global lock" implementation. + @Test func concurrentCallsForSameKeyShareOneBuild() { + let cache = TestCache() + let buildCount = OSAllocatedUnfairLock(initialState: 0) + let buildEnter = DispatchSemaphore(value: 0) + let buildRelease = DispatchSemaphore(value: 0) + let waiterCount = 32 + + // First caller installs the in-flight marker and blocks inside + // `build` until we release it. Every subsequent caller must attach + // to that marker rather than invoking `build` again. + let firstCallerDone = DispatchSemaphore(value: 0) + DispatchQueue.global().async { + let result = cache.resolve(key: AnyHashable("shared")) { + buildCount.withLock { $0 += 1 } + buildEnter.signal() + buildRelease.wait() + return 42 + } + #expect(result == 42) + firstCallerDone.signal() + } + + buildEnter.wait() // first caller is now blocked inside `build` + + // Fan out a herd of waiters; each must observe the same value and + // none of them should have triggered another build. + let herdDone = DispatchSemaphore(value: 0) + let observed = OSAllocatedUnfairLock(initialState: [Int]()) + for _ in 0 ..< waiterCount { + DispatchQueue.global().async { + let result = cache.resolve(key: AnyHashable("shared")) { + Issue.record("a waiter ran build instead of joining the in-flight promise") + return -1 + } + observed.withLock { $0.append(result ?? -1) } + herdDone.signal() + } + } + + // Give the herd a beat to all enter `resolve` and attach to the + // promise. Without this delay the test could pass spuriously if the + // first caller raced ahead of the waiters. + Thread.sleep(forTimeInterval: 0.05) + + buildRelease.signal() // unblock the first caller + firstCallerDone.wait() + for _ in 0 ..< waiterCount { herdDone.wait() } + + #expect(buildCount.withLock { $0 } == 1, "build must run exactly once for the shared key") + let values = observed.withLock { $0 } + #expect(values.count == waiterCount) + #expect(values.allSatisfy { $0 == 42 }, "every waiter must observe the builder's result") + } + + /// Concurrent calls for **different** keys must build in parallel, not + /// serialize behind one another. Verified by wall-clock: with the lock + /// held over the build, N keys × T per build = N*T; with the promise + /// fix, all N builds overlap, so wall-clock is ~T. + @Test func concurrentCallsForDifferentKeysRunInParallel() { + let cache = TestCache() + let keyCount = 8 + let perBuildSeconds: Double = 0.20 + + let allDone = DispatchSemaphore(value: 0) + let start = ContinuousClock.now + for index in 0 ..< keyCount { + DispatchQueue.global().async { + _ = cache.resolve(key: AnyHashable(index)) { + Thread.sleep(forTimeInterval: perBuildSeconds) + return index + } + allDone.signal() + } + } + for _ in 0 ..< keyCount { allDone.wait() } + let elapsed = (ContinuousClock.now - start) + let elapsedSeconds = Double(elapsed.components.seconds) + + Double(elapsed.components.attoseconds) / 1e18 + + // Allow generous slack for CI variance — the contract is "wall-clock + // is closer to one build than to N builds", not a precise multiplier. + // Serial would be ~keyCount * perBuildSeconds; we cap at half of + // that, which is still >2× the parallel ideal. + let serialCeiling = Double(keyCount) * perBuildSeconds + let parallelBudget = serialCeiling * 0.5 + #expect(elapsedSeconds < parallelBudget, + "elapsed=\(elapsedSeconds)s should be well below serial=\(serialCeiling)s") + } + + /// Cache hits stay reentrant: a build for key A may itself call + /// `resolve` for key B without deadlocking. (The fix releases the cache + /// lock around the build call, so this is straightforward — but it's + /// the whole reason the lock-during-build design was a problem to begin + /// with, worth pinning.) + @Test func buildClosureMayResolveOtherKey() { + let cache = TestCache() + let result = cache.resolve(key: AnyHashable("outer")) { + cache.resolve(key: AnyHashable("inner")) { 5 }.map { $0 * 2 } + } + #expect(result == 10) + } +} + +/// Minimal SharedCache instantiation for tests. `SharedCache.init()` is +/// `package`-visible and constructs a usable cache without any MachO +/// scaffolding because every public entry point that requires +/// `MachORepresentableWithCache` ultimately delegates to `resolve(key:build:)`. +private final class TestCache: SharedCache, @unchecked Sendable {} + +/// Mirror of ``SharedCacheResolveTests`` driven through Swift Concurrency +/// primitives (`TaskGroup`, `AsyncStream`) instead of GCD. `resolve` itself +/// is a sync function — calling it from a `Task` body still blocks the +/// cooperative thread pool while the promise's `wait()` is held, but the +/// cache contract (one build per key, parallelism across keys, reentrancy) +/// must hold regardless of how callers spawned the work. +@Suite("SharedCache.resolve under Swift Concurrency") +struct SharedCacheResolveSwiftConcurrencyTests { + @Test func sameKeyDedupViaTaskGroup() async { + let cache = TestCache() + let buildCount = OSAllocatedUnfairLock(initialState: 0) + let waiterCount = 16 + + // One-shot signal: the first task yields when it has entered the + // build closure (i.e. the in-flight marker is installed). Spawning + // waiters before this point would race the marker install and could + // false-pass the test. + let (buildEnteredStream, buildEnteredContinuation) = + AsyncStream.makeStream() + + await withTaskGroup(of: Int?.self) { group in + group.addTask { + cache.resolve(key: AnyHashable("shared")) { + buildCount.withLock { $0 += 1 } + buildEnteredContinuation.yield() + buildEnteredContinuation.finish() + // Stay inside the build long enough for the spawned + // waiter tasks to attach to the in-flight promise. + // Sync sleep is required: the build closure is sync, + // and `await Task.sleep` would compile-error here. + Thread.sleep(forTimeInterval: 0.2) + return 42 + } + } + + // Wait until the builder has entered `resolve` (deterministic). + var iterator = buildEnteredStream.makeAsyncIterator() + _ = await iterator.next() + + for _ in 0 ..< waiterCount { + group.addTask { + cache.resolve(key: AnyHashable("shared")) { + Issue.record("waiter ran build instead of joining the in-flight promise") + return -1 + } + } + } + + var values: [Int?] = [] + for await result in group { + values.append(result) + } + + #expect(buildCount.withLock { $0 } == 1, + "build must run exactly once for the shared key") + #expect(values.count == waiterCount + 1) + #expect(values.allSatisfy { $0 == 42 }, + "every Task must observe the builder's result") + } + } + + @Test func differentKeysParallelViaTaskGroup() async { + let cache = TestCache() + let keyCount = 8 + let perBuildSeconds: Double = 0.20 + + let start = ContinuousClock.now + await withTaskGroup(of: Int?.self) { group in + for index in 0 ..< keyCount { + group.addTask { + cache.resolve(key: AnyHashable(index)) { + Thread.sleep(forTimeInterval: perBuildSeconds) + return index + } + } + } + for await _ in group {} + } + let elapsed = ContinuousClock.now - start + let elapsedSeconds = Double(elapsed.components.seconds) + + Double(elapsed.components.attoseconds) / 1e18 + + let serialCeiling = Double(keyCount) * perBuildSeconds + let parallelBudget = serialCeiling * 0.5 + #expect(elapsedSeconds < parallelBudget, + "elapsed=\(elapsedSeconds)s should be well below serial=\(serialCeiling)s — TaskGroup pool size and Thread.sleep blocking the cooperative pool both factor in, so this only verifies we are not fully serial") + } + + /// async-let variant: build a small batch of distinct keys, then return + /// the dictionary of results. Validates that the structured-concurrency + /// `async let` form works the same as `TaskGroup` for distinct keys. + @Test func differentKeysParallelViaAsyncLet() async { + let cache = TestCache() + let perBuildSeconds: Double = 0.10 + + let start = ContinuousClock.now + async let a = Task.detached { + cache.resolve(key: AnyHashable("a")) { + Thread.sleep(forTimeInterval: perBuildSeconds); return 1 + } + }.value + async let b = Task.detached { + cache.resolve(key: AnyHashable("b")) { + Thread.sleep(forTimeInterval: perBuildSeconds); return 2 + } + }.value + async let c = Task.detached { + cache.resolve(key: AnyHashable("c")) { + Thread.sleep(forTimeInterval: perBuildSeconds); return 3 + } + }.value + async let d = Task.detached { + cache.resolve(key: AnyHashable("d")) { + Thread.sleep(forTimeInterval: perBuildSeconds); return 4 + } + }.value + + let results = await [a, b, c, d] + let elapsed = ContinuousClock.now - start + let elapsedSeconds = Double(elapsed.components.seconds) + + Double(elapsed.components.attoseconds) / 1e18 + + #expect(results == [1, 2, 3, 4]) + // Four 100ms builds run in parallel ⇒ wall-clock ≈ one build, well + // below the serial ceiling of 4 × 100ms = 400ms. + #expect(elapsedSeconds < 0.30) + } + + /// Reentrancy from inside a Task body: a build for one key spawns a + /// child Task that calls `resolve` for a different key, and the parent + /// awaits the child's result. The fix's lock-free build path keeps this + /// from deadlocking even though both calls share the same cache. + @Test func reentrancyFromTask() async { + let cache = TestCache() + let outerResult = await Task.detached { + cache.resolve(key: AnyHashable("outer")) { + // Spawn a nested Task that resolves a different key. We can + // only block-wait it because the outer build closure is + // sync — `await` is not allowed here. + let inner = Task.detached { + cache.resolve(key: AnyHashable("inner")) { 5 } + } + // `Task.value` is async, so we hop back through a Dispatch + // semaphore — proves reentrancy works regardless of how the + // caller chooses to bridge. + let semaphore = DispatchSemaphore(value: 0) + let result = OSAllocatedUnfairLock(initialState: nil) + Task { + let value = await inner.value + result.withLock { $0 = value } + semaphore.signal() + } + semaphore.wait() + let value = result.withLock { $0 } + return value.map { $0 * 2 } + } + }.value + #expect(outerResult == 10) + } + + /// Cancelling Tasks that are blocked inside `resolve.wait()` must not + /// corrupt cache state: the in-flight build still completes, the cache + /// still publishes the result, and a fresh post-cancellation caller + /// observes the cached value rather than re-running the build. + @Test func cancellingWaitersLeavesCacheIntact() async { + let cache = TestCache() + let buildCount = OSAllocatedUnfairLock(initialState: 0) + let (buildEnteredStream, buildEnteredContinuation) = + AsyncStream.makeStream() + + await withTaskGroup(of: Int?.self) { group in + // Builder: blocks long enough that we can spawn-and-cancel a + // herd of waiter tasks while it is still in flight. + group.addTask { + cache.resolve(key: AnyHashable("k")) { + buildCount.withLock { $0 += 1 } + buildEnteredContinuation.yield() + buildEnteredContinuation.finish() + Thread.sleep(forTimeInterval: 0.15) + return 99 + } + } + + var iterator = buildEnteredStream.makeAsyncIterator() + _ = await iterator.next() + + // Spawn waiters and immediately cancel them. `resolve` doesn't + // observe Task cancellation (it's a sync function), so they + // still complete with the builder's result; we just verify that + // the cache is not poisoned by the cancellation. + for _ in 0 ..< 8 { + let task = Task.detached { + cache.resolve(key: AnyHashable("k")) { + Issue.record("waiter ran build") + return -1 + } + } + task.cancel() + group.addTask { await task.value } + } + + for await _ in group {} + } + + #expect(buildCount.withLock { $0 } == 1, + "build must run exactly once even when waiter tasks are cancelled") + + // After the builder published, the cache should hold the result — + // a fresh caller must not trigger another build. + let post = cache.resolve(key: AnyHashable("k")) { + Issue.record("post-cancellation caller ran build, cache was corrupted") + return -1 + } + #expect(post == 99) + } +} diff --git a/Tests/MachOSwiftSectionTests/Fixtures/Generic/GenericContextTests.swift b/Tests/MachOSwiftSectionTests/Fixtures/Generic/GenericContextTests.swift index fd42fa05..a7fb36d5 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/Generic/GenericContextTests.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/Generic/GenericContextTests.swift @@ -383,6 +383,169 @@ final class GenericContextTests: MachOSwiftSectionFixtureTests, FixtureSuite, @u #expect(count == GenericContextBaseline.layoutRequirement.requirementsCount) } + // MARK: - Three-level-nested counter-examples (P0 regression guards) + // + // Every `current*` / `all*` / `parent*` derived var on `TargetGenericContext` + // also has a baseline-snapshot @Test above. Those tests pin the + // implementation against itself: the baseline literal is generated by + // running the same `currentRequirements` etc. on the live binary, so a + // *systematically wrong* implementation produces a baseline that matches + // its own (wrong) output and the snapshot tests pass. + // + // The tests below use a three-level nested generic fixture + // (`NestedGenericThreeLevelConstraintTest.MiddleConstrainedTest.InnerMostConstrainedTest`) + // — chosen because cumulative-parent storage and per-level-newly-introduced + // entries diverge starting at depth 2 — and assert against **hardcoded + // expected values derived from the source declaration**, not the + // baseline snapshot. A regression that, say, drops too many entries out + // of `currentRequirements` or that confuses `parentParameters[i]` with + // "params newly introduced at level i" surfaces here even if the + // baseline regenerator agrees with the bug. + + private func nestedThreeLevelContexts() throws -> (file: TypeGenericContext, image: TypeGenericContext) { + let file = try loadContextsFromFile { try BaselineFixturePicker.struct_NestedThreeLevelInnerMostConstrainedTest(in: $0) } + let image = try loadContextsFromImage { try BaselineFixturePicker.struct_NestedThreeLevelInnerMostConstrainedTest(in: $0) } + return (file: file, image: image) + } + + @Test("parameters is cumulative across the parent chain (3-level fixture)") + func nestedThreeLevelParametersIsCumulative() async throws { + let contexts = try nestedThreeLevelContexts() + let count = try acrossAllReaders( + file: { contexts.file.parameters.count }, + image: { contexts.image.parameters.count } + ) + // Outer + Middle + InnerMost cumulative = 3. + // numParams in the binary header matches. + #expect(count == 3) + #expect(Int(contexts.file.header.layout.numParams) == 3) + } + + @Test("requirements is cumulative across the parent chain (3-level fixture)") + func nestedThreeLevelRequirementsIsCumulative() async throws { + let contexts = try nestedThreeLevelContexts() + let count = try acrossAllReaders( + file: { contexts.file.requirements.count }, + image: { contexts.image.requirements.count } + ) + // One protocol constraint per level: Outer:Hashable, Middle:Equatable, + // InnerMost:Comparable. + #expect(count == 3) + } + + @Test("parentParameters stores cumulative counts per level — counter-example to a per-level-new interpretation") + func nestedThreeLevelParentParametersAreCumulative() async throws { + let contexts = try nestedThreeLevelContexts() + let parentCounts = try acrossAllReaders( + file: { contexts.file.parentParameters.map(\.count) }, + image: { contexts.image.parentParameters.map(\.count) } + ) + // Outer scope cumulative = [Outer]; Middle scope cumulative = [Outer, Middle]. + // If parentParameters was per-level-new, this would be [1, 1] instead. + #expect(parentCounts == [1, 2]) + } + + @Test("parentRequirements stores cumulative counts per level — counter-example to a per-level-new interpretation") + func nestedThreeLevelParentRequirementsAreCumulative() async throws { + let contexts = try nestedThreeLevelContexts() + let parentCounts = try acrossAllReaders( + file: { contexts.file.parentRequirements.map(\.count) }, + image: { contexts.image.parentRequirements.map(\.count) } + ) + // Outer scope cumulative requirements = [Outer:Hashable]; + // Middle scope cumulative requirements = [Outer:Hashable, Middle:Equatable]. + // Per-level-new would be [1, 1] — the assertion below distinguishes + // the two readings. + #expect(parentCounts == [1, 2]) + } + + @Test("currentParameters drops only the immediate-parent's cumulative count") + func nestedThreeLevelCurrentParametersHardcoded() async throws { + let contexts = try nestedThreeLevelContexts() + let count = try acrossAllReaders( + file: { contexts.file.currentParameters.count }, + image: { contexts.image.currentParameters.count } + ) + // InnerMost adds exactly one new parameter (`InnerMost`). + #expect(count == 1) + } + + @Test("currentRequirements must drop only the immediate-parent's count, not parentRequirements.flatMap.count") + func nestedThreeLevelCurrentRequirementsHardcoded() async throws { + let contexts = try nestedThreeLevelContexts() + let count = try acrossAllReaders( + file: { contexts.file.currentRequirements.count }, + image: { contexts.image.currentRequirements.count } + ) + // InnerMost adds exactly one new requirement (`InnerMost: Comparable`). + // A regression that uses `parentRequirements.flatMap{$0}.count` + // (= 1 + 2 = 3) instead of `parentRequirements.last?.count` (= 2) + // would drop everything and return 0. + #expect( + count == 1, + "currentRequirements should be [InnerMost: Comparable]; flatMap-over-cumulative-parents over-drops at depth ≥ 2." + ) + } + + @Test("flatMap-over-parents and last-of-parents diverge at depth ≥ 2 — invariant guarding the currentRequirements formula") + func nestedThreeLevelFlatMapVsLastDivergence() async throws { + let contexts = try nestedThreeLevelContexts() + let flatMapCount = try acrossAllReaders( + file: { contexts.file.parentRequirements.flatMap { $0 }.count }, + image: { contexts.image.parentRequirements.flatMap { $0 }.count } + ) + let lastCount = try acrossAllReaders( + file: { contexts.file.parentRequirements.last?.count ?? 0 }, + image: { contexts.image.parentRequirements.last?.count ?? 0 } + ) + // 1 (Outer) + 2 (Middle cumulative) = 3; immediate parent's + // cumulative count alone = 2. The two values must differ for a + // 3-level-nested fixture; if they ever stop differing, this + // counter-example has degraded and the regression surface for + // P0.1-class bugs is gone. + #expect(flatMapCount == 3) + #expect(lastCount == 2) + #expect(flatMapCount != lastCount) + } + + @Test("uniqueCurrentRequirements deduplicates by content even when parents are cumulative") + func nestedThreeLevelUniqueCurrentRequirements() async throws { + let contexts = try nestedThreeLevelContexts() + let count = try acrossAllReaders( + file: { contexts.file.uniqueCurrentRequirements(in: machOFile).count }, + image: { contexts.image.uniqueCurrentRequirements(in: machOImage).count } + ) + // InnerMost adds one requirement. uniqueCurrentRequirements walks + // `parentRequirements.flatMap { $0 }` and uses `isContentEqual` to + // skip inherited entries, so cumulative duplicates do not break it. + #expect(count == 1) + } + + @Test("allParameters has one entry per nesting level (parents + own)") + func nestedThreeLevelAllParametersLevelCount() async throws { + let contexts = try nestedThreeLevelContexts() + let count = try acrossAllReaders( + file: { contexts.file.allParameters.count }, + image: { contexts.image.allParameters.count } + ) + // Two parent generic contexts (Outer, Middle) + the current scope. + #expect(count == 3) + } + + @Test("allRequirements has one entry per nesting level (parents + own)") + func nestedThreeLevelAllRequirementsLevelCount() async throws { + let contexts = try nestedThreeLevelContexts() + let count = try acrossAllReaders( + file: { contexts.file.allRequirements.count }, + image: { contexts.image.allRequirements.count } + ) + // Same shape as allParameters: 2 parent levels + current. If + // currentRequirements regresses to empty, allRequirements collapses + // back to `parentRequirements` (count 2) — this assertion catches + // that fallback. + #expect(count == 3) + } + @Test func asGenericContext() async throws { // `asGenericContext` projects a TypeGenericContext down to the base // GenericContext shape. The offset/parameter/requirement counts must diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AnonymousContextBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AnonymousContextBaseline.swift index 7f8d3622..3552036a 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AnonymousContextBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AnonymousContextBaseline.swift @@ -12,7 +12,7 @@ enum AnonymousContextBaseline { } static let firstAnonymous = Entry( - descriptorOffset: 0x33dc4, + descriptorOffset: 0x34144, hasGenericContext: true, hasMangledName: false ) diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AnonymousContextDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AnonymousContextDescriptorBaseline.swift index 52625534..45568930 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AnonymousContextDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AnonymousContextDescriptorBaseline.swift @@ -11,7 +11,7 @@ enum AnonymousContextDescriptorBaseline { } static let firstAnonymous = Entry( - offset: 0x33dc4, + offset: 0x34144, layoutFlagsRawValue: 0xc2 ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AssociatedTypeBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AssociatedTypeBaseline.swift index a725992c..c219dc96 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AssociatedTypeBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AssociatedTypeBaseline.swift @@ -18,7 +18,7 @@ enum AssociatedTypeBaseline { } static let concreteWitnessTest = Entry( - descriptorOffset: 0x32800, + descriptorOffset: 0x32b80, recordsCount: 5, hasConformingTypeName: true, hasProtocolTypeName: true diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AssociatedTypeDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AssociatedTypeDescriptorBaseline.swift index a4fd3456..9acfbfad 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AssociatedTypeDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AssociatedTypeDescriptorBaseline.swift @@ -21,7 +21,7 @@ enum AssociatedTypeDescriptorBaseline { } static let concreteWitnessTest = Entry( - offset: 0x32800, + offset: 0x32b80, layoutNumAssociatedTypes: 5, layoutAssociatedTypeRecordSize: 8, actualSize: 56, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AssociatedTypeRecordBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AssociatedTypeRecordBaseline.swift index c3c296d5..5c07b88b 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AssociatedTypeRecordBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AssociatedTypeRecordBaseline.swift @@ -17,7 +17,7 @@ enum AssociatedTypeRecordBaseline { } static let firstRecord = Entry( - offset: 0x32810, + offset: 0x32b90, name: "First", hasSubstitutedTypeName: true ) diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/BuiltinTypeBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/BuiltinTypeBaseline.swift index d1ae8fdd..5a49b9cf 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/BuiltinTypeBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/BuiltinTypeBaseline.swift @@ -16,7 +16,7 @@ enum BuiltinTypeBaseline { } static let firstBuiltin = Entry( - descriptorOffset: 0x3aa10, + descriptorOffset: 0x3aeb0, hasTypeName: true ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/BuiltinTypeDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/BuiltinTypeDescriptorBaseline.swift index c61bb496..039ea7ee 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/BuiltinTypeDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/BuiltinTypeDescriptorBaseline.swift @@ -22,7 +22,7 @@ enum BuiltinTypeDescriptorBaseline { } static let firstBuiltin = Entry( - descriptorOffset: 0x3aa10, + descriptorOffset: 0x3aeb0, size: 0x14, alignmentAndFlags: 0x10004, stride: 0x14, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ClassBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ClassBaseline.swift index 4761fbfa..c39c0454 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ClassBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ClassBaseline.swift @@ -27,7 +27,7 @@ enum ClassBaseline { } static let classTest = Entry( - descriptorOffset: 0x338d0, + descriptorOffset: 0x33c50, hasGenericContext: false, hasResilientSuperclass: false, hasForeignMetadataInitialization: false, @@ -48,7 +48,7 @@ enum ClassBaseline { ) static let subclassTest = Entry( - descriptorOffset: 0x3394c, + descriptorOffset: 0x33ccc, hasGenericContext: false, hasResilientSuperclass: false, hasForeignMetadataInitialization: false, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ClassDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ClassDescriptorBaseline.swift index 673b6bcb..60a969db 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ClassDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ClassDescriptorBaseline.swift @@ -26,7 +26,7 @@ enum ClassDescriptorBaseline { } static let classTest = Entry( - offset: 0x338d0, + offset: 0x33c50, layoutNumFields: 0, layoutFieldOffsetVectorOffset: 10, layoutNumImmediateMembers: 9, @@ -46,7 +46,7 @@ enum ClassDescriptorBaseline { ) static let subclassTest = Entry( - offset: 0x3394c, + offset: 0x33ccc, layoutNumFields: 0, layoutFieldOffsetVectorOffset: 19, layoutNumImmediateMembers: 0, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ContextDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ContextDescriptorBaseline.swift index cde5a0f6..f46b6c98 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ContextDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ContextDescriptorBaseline.swift @@ -11,7 +11,7 @@ enum ContextDescriptorBaseline { } static let structTest = Entry( - offset: 0x36cb0, + offset: 0x37108, layoutFlagsRawValue: 0x51 ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ContextDescriptorWrapperBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ContextDescriptorWrapperBaseline.swift index f1b7757e..e9dc4273 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ContextDescriptorWrapperBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ContextDescriptorWrapperBaseline.swift @@ -33,7 +33,7 @@ enum ContextDescriptorWrapperBaseline { } static let structTest = Entry( - descriptorOffset: 0x36cb0, + descriptorOffset: 0x37108, isType: true, isStruct: true, isClass: false, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ContextWrapperBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ContextWrapperBaseline.swift index e054be9d..4f65a096 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ContextWrapperBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ContextWrapperBaseline.swift @@ -11,7 +11,7 @@ enum ContextWrapperBaseline { } static let structTest = Entry( - descriptorOffset: 0x36cb0, + descriptorOffset: 0x37108, hasParent: true ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/EnumBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/EnumBaseline.swift index bb6377fb..24ef90f5 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/EnumBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/EnumBaseline.swift @@ -18,7 +18,7 @@ enum EnumBaseline { } static let noPayloadEnumTest = Entry( - descriptorOffset: 0x34950, + descriptorOffset: 0x34cd0, hasGenericContext: false, hasForeignMetadataInitialization: false, hasSingletonMetadataInitialization: false, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/EnumDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/EnumDescriptorBaseline.swift index b1a54361..481035ab 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/EnumDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/EnumDescriptorBaseline.swift @@ -23,7 +23,7 @@ enum EnumDescriptorBaseline { } static let noPayloadEnumTest = Entry( - offset: 0x34950, + offset: 0x34cd0, layoutNumPayloadCasesAndPayloadSizeOffset: 0x0, layoutNumEmptyCases: 0x4, layoutFlagsRawValue: 0x52, @@ -40,7 +40,7 @@ enum EnumDescriptorBaseline { ) static let singlePayloadEnumTest = Entry( - offset: 0x3496c, + offset: 0x34cec, layoutNumPayloadCasesAndPayloadSizeOffset: 0x1, layoutNumEmptyCases: 0x2, layoutFlagsRawValue: 0x52, @@ -57,7 +57,7 @@ enum EnumDescriptorBaseline { ) static let multiPayloadEnumTest = Entry( - offset: 0x348f0, + offset: 0x34c70, layoutNumPayloadCasesAndPayloadSizeOffset: 0x3, layoutNumEmptyCases: 0x1, layoutFlagsRawValue: 0x52, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ExtensionContextBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ExtensionContextBaseline.swift index ae966ba1..59693c1b 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ExtensionContextBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ExtensionContextBaseline.swift @@ -12,7 +12,7 @@ enum ExtensionContextBaseline { } static let firstExtension = Entry( - descriptorOffset: 0x35638, + descriptorOffset: 0x359b8, hasGenericContext: true, hasExtendedContextMangledName: true ) diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ExtensionContextDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ExtensionContextDescriptorBaseline.swift index e3f23b41..3898b378 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ExtensionContextDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ExtensionContextDescriptorBaseline.swift @@ -11,7 +11,7 @@ enum ExtensionContextDescriptorBaseline { } static let firstExtension = Entry( - offset: 0x35638, + offset: 0x359b8, layoutFlagsRawValue: 0xc1 ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/FieldDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/FieldDescriptorBaseline.swift index 8d829b67..d254890f 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/FieldDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/FieldDescriptorBaseline.swift @@ -20,7 +20,7 @@ enum FieldDescriptorBaseline { } static let genericStructNonRequirement = Entry( - offset: 0x38744, + offset: 0x38b9c, kindRawValue: 0x0, layoutNumFields: 3, layoutFieldRecordSize: 12, @@ -29,7 +29,7 @@ enum FieldDescriptorBaseline { ) static let structTest = Entry( - offset: 0x39410, + offset: 0x398bc, kindRawValue: 0x0, layoutNumFields: 0, layoutFieldRecordSize: 12, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/FieldRecordBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/FieldRecordBaseline.swift index ac79e47a..591c416b 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/FieldRecordBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/FieldRecordBaseline.swift @@ -18,14 +18,14 @@ enum FieldRecordBaseline { } static let firstRecord = Entry( - offset: 0x38754, + offset: 0x38bac, layoutFlagsRawValue: 0x2, fieldName: "field1", hasMangledTypeName: true ) static let secondRecord = Entry( - offset: 0x38760, + offset: 0x38bb8, layoutFlagsRawValue: 0x2, fieldName: "field2", hasMangledTypeName: true diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericContextBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericContextBaseline.swift index 7deb9b65..86467f65 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericContextBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericContextBaseline.swift @@ -33,7 +33,7 @@ enum GenericContextBaseline { } static let nonRequirement = Entry( - offset: 0x34fbc, + offset: 0x3533c, size: 20, depth: 0, parametersCount: 1, @@ -60,7 +60,7 @@ enum GenericContextBaseline { ) static let layoutRequirement = Entry( - offset: 0x34fec, + offset: 0x3536c, size: 32, depth: 0, parametersCount: 1, @@ -87,7 +87,7 @@ enum GenericContextBaseline { ) static let protocolRequirement = Entry( - offset: 0x35028, + offset: 0x353a8, size: 32, depth: 0, parametersCount: 1, @@ -114,7 +114,7 @@ enum GenericContextBaseline { ) static let parameterPack = Entry( - offset: 0x35478, + offset: 0x357f8, size: 32, depth: 0, parametersCount: 1, @@ -141,7 +141,7 @@ enum GenericContextBaseline { ) static let invertibleProtocol = Entry( - offset: 0x35504, + offset: 0x35884, size: 32, depth: 0, parametersCount: 1, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericContextDescriptorHeaderBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericContextDescriptorHeaderBaseline.swift index df5328a1..238b8911 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericContextDescriptorHeaderBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericContextDescriptorHeaderBaseline.swift @@ -14,7 +14,7 @@ enum GenericContextDescriptorHeaderBaseline { } static let firstExtensionGenericHeader = Entry( - offset: 0x35644, + offset: 0x359c4, layoutNumParams: 1, layoutNumRequirements: 2, layoutNumKeyArguments: 3, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericPackShapeDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericPackShapeDescriptorBaseline.swift index eb49c4ee..acc6bf1c 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericPackShapeDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericPackShapeDescriptorBaseline.swift @@ -15,7 +15,7 @@ enum GenericPackShapeDescriptorBaseline { } static let parameterPackFirstShape = Entry( - offset: 0x35490, + offset: 0x35810, layoutKind: 0, layoutIndex: 1, layoutShapeClass: 0, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericPackShapeHeaderBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericPackShapeHeaderBaseline.swift index b0ef9efe..d909d1d0 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericPackShapeHeaderBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericPackShapeHeaderBaseline.swift @@ -12,7 +12,7 @@ enum GenericPackShapeHeaderBaseline { } static let parameterPackHeader = Entry( - offset: 0x3548c, + offset: 0x3580c, layoutNumPacks: 1, layoutNumShapeClasses: 1 ) diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericParamDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericParamDescriptorBaseline.swift index 7f0c4510..a5c84d45 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericParamDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericParamDescriptorBaseline.swift @@ -13,14 +13,14 @@ enum GenericParamDescriptorBaseline { } static let layoutRequirementParam0 = Entry( - offset: 0x34ffc, + offset: 0x3537c, layoutRawValue: 0x80, hasKeyArgument: true, kindRawValue: 0x0 ) static let parameterPackParam0 = Entry( - offset: 0x35488, + offset: 0x35808, layoutRawValue: 0x81, hasKeyArgument: true, kindRawValue: 0x1 diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericRequirementBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericRequirementBaseline.swift index d14e08a1..b352415e 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericRequirementBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericRequirementBaseline.swift @@ -11,27 +11,27 @@ enum GenericRequirementBaseline { } static let layoutRequirement = Entry( - descriptorOffset: 0x35000, + descriptorOffset: 0x35380, resolvedContentCase: "layout" ) static let swiftProtocolRequirement = Entry( - descriptorOffset: 0x3503c, + descriptorOffset: 0x353bc, resolvedContentCase: "protocol" ) static let objcProtocolRequirement = Entry( - descriptorOffset: 0x35078, + descriptorOffset: 0x353f8, resolvedContentCase: "protocol" ) static let baseClassRequirement = Entry( - descriptorOffset: 0x35414, + descriptorOffset: 0x35794, resolvedContentCase: "type" ) static let sameTypeRequirement = Entry( - descriptorOffset: 0x35384, + descriptorOffset: 0x35704, resolvedContentCase: "type" ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericRequirementDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericRequirementDescriptorBaseline.swift index a116bc63..bd250204 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericRequirementDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericRequirementDescriptorBaseline.swift @@ -13,35 +13,35 @@ enum GenericRequirementDescriptorBaseline { } static let layoutRequirement = Entry( - offset: 0x35000, + offset: 0x35380, flagsRawValue: 0x1f, kindRawValue: 0x1f, contentKindCase: "layout" ) static let swiftProtocolRequirement = Entry( - offset: 0x3503c, + offset: 0x353bc, flagsRawValue: 0x80, kindRawValue: 0x0, contentKindCase: "protocol" ) static let objcProtocolRequirement = Entry( - offset: 0x35078, + offset: 0x353f8, flagsRawValue: 0x0, kindRawValue: 0x0, contentKindCase: "protocol" ) static let baseClassRequirement = Entry( - offset: 0x35414, + offset: 0x35794, flagsRawValue: 0x2, kindRawValue: 0x2, contentKindCase: "type" ) static let sameTypeRequirement = Entry( - offset: 0x35384, + offset: 0x35704, flagsRawValue: 0x1, kindRawValue: 0x1, contentKindCase: "type" diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericValueDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericValueDescriptorBaseline.swift index 1524b11f..70a8d07a 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericValueDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericValueDescriptorBaseline.swift @@ -12,7 +12,7 @@ enum GenericValueDescriptorBaseline { } static let fixedSizeArrayFirstValue = Entry( - offset: 0x35748, + offset: 0x35ac8, layoutType: 0, typeRawValue: 0 ) diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericValueHeaderBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericValueHeaderBaseline.swift index 040e58ca..ed3f4550 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericValueHeaderBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GenericValueHeaderBaseline.swift @@ -11,7 +11,7 @@ enum GenericValueHeaderBaseline { } static let fixedSizeArrayHeader = Entry( - offset: 0x35744, + offset: 0x35ac4, layoutNumValues: 1 ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GlobalActorReferenceBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GlobalActorReferenceBaseline.swift index 8532a901..7e496976 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GlobalActorReferenceBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GlobalActorReferenceBaseline.swift @@ -11,7 +11,7 @@ enum GlobalActorReferenceBaseline { } static let firstReference = Entry( - offset: 0x29514, + offset: 0x297b4, typeNameSymbolString: "_$sScM" ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodDescriptorBaseline.swift index de561426..84842e20 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodDescriptorBaseline.swift @@ -16,7 +16,7 @@ enum MethodDescriptorBaseline { } static let firstClassTestMethod = Entry( - offset: 0x33904, + offset: 0x33c84, layoutFlagsRawValue: 0x12 ) diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodOverrideDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodOverrideDescriptorBaseline.swift index 9f1a92a4..7a05cc5d 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodOverrideDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodOverrideDescriptorBaseline.swift @@ -14,7 +14,7 @@ enum MethodOverrideDescriptorBaseline { } static let firstSubclassOverride = Entry( - offset: 0x3397c + offset: 0x33cfc ) static let subclassOverrideCount = 9 diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ModuleContextBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ModuleContextBaseline.swift index 9198978f..91d268b5 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ModuleContextBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ModuleContextBaseline.swift @@ -11,7 +11,7 @@ enum ModuleContextBaseline { } static let symbolTestsCore = Entry( - descriptorOffset: 0x33020, + descriptorOffset: 0x333a0, name: "SymbolTestsCore" ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ModuleContextDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ModuleContextDescriptorBaseline.swift index 16a9510f..caa592ce 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ModuleContextDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ModuleContextDescriptorBaseline.swift @@ -11,7 +11,7 @@ enum ModuleContextDescriptorBaseline { } static let symbolTestsCore = Entry( - offset: 0x33020, + offset: 0x333a0, layoutFlagsRawValue: 0x0 ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MultiPayloadEnumDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MultiPayloadEnumDescriptorBaseline.swift index e17181ce..5f215cce 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MultiPayloadEnumDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MultiPayloadEnumDescriptorBaseline.swift @@ -23,7 +23,7 @@ enum MultiPayloadEnumDescriptorBaseline { } static let multiPayloadEnumTest = Entry( - offset: 0x3b584, + offset: 0x3ba34, layoutSizeFlags: 0x10000, mangledTypeNameRawString: "\u{1}", contentsSizeInWord: 0x1, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ObjCProtocolPrefixBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ObjCProtocolPrefixBaseline.swift index 8976c1e5..8819e542 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ObjCProtocolPrefixBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ObjCProtocolPrefixBaseline.swift @@ -11,7 +11,7 @@ enum ObjCProtocolPrefixBaseline { } static let firstPrefix = Entry( - offset: 0x526c8, + offset: 0x52848, name: "NSObject" ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ObjCResilientClassStubInfoBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ObjCResilientClassStubInfoBaseline.swift index e54d8924..d83c3af4 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ObjCResilientClassStubInfoBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ObjCResilientClassStubInfoBaseline.swift @@ -20,8 +20,8 @@ enum ObjCResilientClassStubInfoBaseline { } static let resilientObjCStubChild = Entry( - sourceClassOffset: 0x35e68, - offset: 0x35ed4, - layoutStubRelativeOffset: 115812 + sourceClassOffset: 0x362c0, + offset: 0x3632c, + layoutStubRelativeOffset: 115084 ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/OverrideTableHeaderBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/OverrideTableHeaderBaseline.swift index a5dc5013..51e0e0db 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/OverrideTableHeaderBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/OverrideTableHeaderBaseline.swift @@ -11,7 +11,7 @@ enum OverrideTableHeaderBaseline { } static let subclassTest = Entry( - offset: 0x33978, + offset: 0x33cf8, layoutNumEntries: 9 ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolBaseRequirementBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolBaseRequirementBaseline.swift index b91ed0c4..18e4e4f8 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolBaseRequirementBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolBaseRequirementBaseline.swift @@ -10,6 +10,6 @@ enum ProtocolBaseRequirementBaseline { } static let witnessTableTest = Entry( - offset: 0x366ec + offset: 0x36b44 ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolBaseline.swift index 495a8eea..6bce6f2f 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolBaseline.swift @@ -18,7 +18,7 @@ enum ProtocolBaseline { static let protocolTest = Entry( name: "ProtocolTest", - descriptorOffset: 0x36698, + descriptorOffset: 0x36af0, protocolFlagsRawValue: 0x3, numberOfRequirements: 4, numberOfRequirementsInSignature: 1, @@ -29,7 +29,7 @@ enum ProtocolBaseline { static let protocolWitnessTableTest = Entry( name: "ProtocolWitnessTableTest", - descriptorOffset: 0x366dc, + descriptorOffset: 0x36b34, protocolFlagsRawValue: 0x3, numberOfRequirements: 5, numberOfRequirementsInSignature: 0, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolConformanceBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolConformanceBaseline.swift index 3abea1a6..32bff77f 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolConformanceBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolConformanceBaseline.swift @@ -20,7 +20,7 @@ enum ProtocolConformanceBaseline { } static let structTestProtocolTest = Entry( - descriptorOffset: 0x2f120, + descriptorOffset: 0x2f490, flagsRawValue: 0x20000, hasProtocol: true, hasWitnessTablePattern: true, @@ -34,7 +34,7 @@ enum ProtocolConformanceBaseline { ) static let conditionalFirst = Entry( - descriptorOffset: 0x2b4a0, + descriptorOffset: 0x2b740, flagsRawValue: 0x30100, hasProtocol: true, hasWitnessTablePattern: true, @@ -48,7 +48,7 @@ enum ProtocolConformanceBaseline { ) static let globalActorFirst = Entry( - descriptorOffset: 0x29504, + descriptorOffset: 0x297a4, flagsRawValue: 0x80000, hasProtocol: true, hasWitnessTablePattern: true, @@ -62,7 +62,7 @@ enum ProtocolConformanceBaseline { ) static let resilientFirst = Entry( - descriptorOffset: 0x29474, + descriptorOffset: 0x29714, flagsRawValue: 0x30000, hasProtocol: true, hasWitnessTablePattern: true, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolConformanceDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolConformanceDescriptorBaseline.swift index 1a61919f..646edc52 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolConformanceDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolConformanceDescriptorBaseline.swift @@ -15,7 +15,7 @@ enum ProtocolConformanceDescriptorBaseline { } static let structTestProtocolTest = Entry( - offset: 0x2f120, + offset: 0x2f490, layoutFlagsRawValue: 0x20000, typeReferenceKindRawValue: 0x0, hasProtocolDescriptor: true, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolDescriptorBaseline.swift index 64f23caa..01ec56a5 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolDescriptorBaseline.swift @@ -14,7 +14,7 @@ enum ProtocolDescriptorBaseline { } static let protocolTest = Entry( - offset: 0x36698, + offset: 0x36af0, layoutNumRequirementsInSignature: 1, layoutNumRequirements: 4, layoutFlagsRawValue: 0x30043, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolDescriptorRefBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolDescriptorRefBaseline.swift index 0b621d3f..d30e9a70 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolDescriptorRefBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolDescriptorRefBaseline.swift @@ -34,7 +34,7 @@ enum ProtocolDescriptorRefBaseline { ) static let liveObjc = LiveObjcEntry( - prefixOffset: 0x526c8, + prefixOffset: 0x52848, name: "NSObject" ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolRecordBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolRecordBaseline.swift index 39daf703..1852c180 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolRecordBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolRecordBaseline.swift @@ -12,8 +12,8 @@ enum ProtocolRecordBaseline { } static let firstRecord = Entry( - offset: 0x3aac4, - resolvedDescriptorOffset: 0x331b0, + offset: 0x3af64, + resolvedDescriptorOffset: 0x33530, resolvedDescriptorName: "GlobalActorIsolatedProtocolTest" ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolRequirementBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolRequirementBaseline.swift index 1e319e5f..f8a10083 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolRequirementBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolRequirementBaseline.swift @@ -12,7 +12,7 @@ enum ProtocolRequirementBaseline { } static let firstRequirement = Entry( - offset: 0x366f4, + offset: 0x36b4c, layoutFlagsRawValue: 0x11, hasDefaultImplementation: false ) diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolWitnessTableBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolWitnessTableBaseline.swift index 865f784c..7596e3f8 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolWitnessTableBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolWitnessTableBaseline.swift @@ -10,6 +10,6 @@ enum ProtocolWitnessTableBaseline { } static let firstWitnessTable = Entry( - offset: 0x2947c + offset: 0x2971c ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientSuperclassBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientSuperclassBaseline.swift index 4668805f..cc1c4460 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientSuperclassBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientSuperclassBaseline.swift @@ -19,8 +19,8 @@ enum ResilientSuperclassBaseline { } static let resilientChild = Entry( - sourceClassOffset: 0x36960, - offset: 0x3698c, - layoutSuperclassRelativeOffset: 38556 + sourceClassOffset: 0x36db8, + offset: 0x36de4, + layoutSuperclassRelativeOffset: 37444 ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientWitnessBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientWitnessBaseline.swift index b4b0ff74..4aa6a72a 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientWitnessBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientWitnessBaseline.swift @@ -13,7 +13,7 @@ enum ResilientWitnessBaseline { } static let firstWitness = Entry( - offset: 0x29488, + offset: 0x29728, hasRequirement: true, hasImplementationSymbols: true, implementationOffset: 0x1a14 diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientWitnessesHeaderBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientWitnessesHeaderBaseline.swift index 99377938..7e643690 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientWitnessesHeaderBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientWitnessesHeaderBaseline.swift @@ -11,7 +11,7 @@ enum ResilientWitnessesHeaderBaseline { } static let firstHeader = Entry( - offset: 0x29484, + offset: 0x29724, layoutNumWitnesses: 1 ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/SingletonMetadataInitializationBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/SingletonMetadataInitializationBaseline.swift index 424083fb..7fecb4a2 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/SingletonMetadataInitializationBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/SingletonMetadataInitializationBaseline.swift @@ -23,9 +23,9 @@ enum SingletonMetadataInitializationBaseline { } static let firstSingletonInit = Entry( - descriptorOffset: 0x33848, - initializationCacheRelativeOffsetBits: 0x1bcc8, - incompleteMetadataRelativeOffsetBits: 0xe334, - completionFunctionRelativeOffsetBits: 0xfffffffffffd0ab4 + descriptorOffset: 0x33bc8, + initializationCacheRelativeOffsetBits: 0x1b948, + incompleteMetadataRelativeOffsetBits: 0xdfb4, + completionFunctionRelativeOffsetBits: 0xfffffffffffd0734 ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructBaseline.swift index 045612e0..330651bb 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructBaseline.swift @@ -18,7 +18,7 @@ enum StructBaseline { } static let structTest = Entry( - descriptorOffset: 0x36cb0, + descriptorOffset: 0x37108, hasGenericContext: false, hasForeignMetadataInitialization: false, hasSingletonMetadataInitialization: false, @@ -30,7 +30,7 @@ enum StructBaseline { ) static let genericStructNonRequirement = Entry( - descriptorOffset: 0x34fa0, + descriptorOffset: 0x35320, hasGenericContext: true, hasForeignMetadataInitialization: false, hasSingletonMetadataInitialization: false, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructDescriptorBaseline.swift index 7be9b0fc..57601f2b 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructDescriptorBaseline.swift @@ -13,14 +13,14 @@ enum StructDescriptorBaseline { } static let structTest = Entry( - offset: 0x36cb0, + offset: 0x37108, layoutNumFields: 0, layoutFieldOffsetVector: 2, layoutFlagsRawValue: 0x51 ) static let genericStructNonRequirement = Entry( - offset: 0x34fa0, + offset: 0x35320, layoutNumFields: 3, layoutFieldOffsetVector: 3, layoutFlagsRawValue: 0xd1 diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeContextDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeContextDescriptorBaseline.swift index 75315098..298645cf 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeContextDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeContextDescriptorBaseline.swift @@ -14,7 +14,7 @@ enum TypeContextDescriptorBaseline { } static let structTest = Entry( - offset: 0x36cb0, + offset: 0x37108, layoutFlagsRawValue: 0x51, hasEnumDescriptor: false, hasStructDescriptor: true, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeContextDescriptorWrapperBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeContextDescriptorWrapperBaseline.swift index b1f85343..3002cc33 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeContextDescriptorWrapperBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeContextDescriptorWrapperBaseline.swift @@ -13,7 +13,7 @@ enum TypeContextDescriptorWrapperBaseline { } static let structTest = Entry( - descriptorOffset: 0x36cb0, + descriptorOffset: 0x37108, hasParent: true, hasGenericContext: false, hasTypeGenericContext: false diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeContextWrapperBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeContextWrapperBaseline.swift index 40443dd6..96de05ef 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeContextWrapperBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeContextWrapperBaseline.swift @@ -11,7 +11,7 @@ enum TypeContextWrapperBaseline { } static let structTest = Entry( - descriptorOffset: 0x36cb0, + descriptorOffset: 0x37108, isStruct: true ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeGenericContextDescriptorHeaderBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeGenericContextDescriptorHeaderBaseline.swift index 5ed45cce..ec7270ae 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeGenericContextDescriptorHeaderBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeGenericContextDescriptorHeaderBaseline.swift @@ -14,7 +14,7 @@ enum TypeGenericContextDescriptorHeaderBaseline { } static let genericStructLayoutRequirement = Entry( - offset: 0x34fec, + offset: 0x3536c, layoutNumParams: 1, layoutNumRequirements: 1, layoutNumKeyArguments: 1, diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeMetadataRecordBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeMetadataRecordBaseline.swift index 312dfe7a..3ee9dc45 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeMetadataRecordBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeMetadataRecordBaseline.swift @@ -13,9 +13,9 @@ enum TypeMetadataRecordBaseline { } static let structTestRecord = Entry( - offset: 0x3b244, - layoutRelativeOffset: -17812, + offset: 0x3b6f0, + layoutRelativeOffset: -17896, typeKindRawValue: 0x0, - contextDescriptorOffset: 0x36cb0 + contextDescriptorOffset: 0x37108 ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeReferenceBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeReferenceBaseline.swift index 3ba63dca..fc49c608 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeReferenceBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeReferenceBaseline.swift @@ -13,9 +13,9 @@ enum TypeReferenceBaseline { } static let structTestRecord = Entry( - recordFieldOffset: 0x3b244, - relativeOffset: -17812, + recordFieldOffset: 0x3b6f0, + relativeOffset: -17896, kindRawValue: 0x0, - resolvedDescriptorOffset: 0x36cb0 + resolvedDescriptorOffset: 0x37108 ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/VTableDescriptorHeaderBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/VTableDescriptorHeaderBaseline.swift index 96648058..1bd97574 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/VTableDescriptorHeaderBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/VTableDescriptorHeaderBaseline.swift @@ -12,7 +12,7 @@ enum VTableDescriptorHeaderBaseline { } static let classTest = Entry( - offset: 0x338fc, + offset: 0x33c7c, layoutVTableOffset: 10, layoutVTableSize: 9 ) diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ValueTypeDescriptorWrapperBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ValueTypeDescriptorWrapperBaseline.swift index 3411304c..4e7ed1d2 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ValueTypeDescriptorWrapperBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ValueTypeDescriptorWrapperBaseline.swift @@ -12,7 +12,7 @@ enum ValueTypeDescriptorWrapperBaseline { } static let structTest = Entry( - descriptorOffset: 0x36cb0, + descriptorOffset: 0x37108, hasParent: true, hasGenericContext: false ) diff --git a/Tests/IntegrationTests/LayoutTests.swift b/Tests/MachOSwiftSectionTests/LayoutTests.swift similarity index 100% rename from Tests/IntegrationTests/LayoutTests.swift rename to Tests/MachOSwiftSectionTests/LayoutTests.swift diff --git a/Tests/MachOSymbolsTests/DemanglingTests.swift b/Tests/MachOSymbolsTests/DemanglingTests.swift deleted file mode 100644 index f8a870d1..00000000 --- a/Tests/MachOSymbolsTests/DemanglingTests.swift +++ /dev/null @@ -1,132 +0,0 @@ -import Foundation -import Testing -@testable import Demangling -import MachOKit -import MachOFoundation -@testable import MachOTestingSupport -import MachOFixtureSupport -import Dependencies - -@MainActor -protocol DemanglingTests { - func allSymbols() async throws -> [MachOSwiftSymbol] - func mainTest() async throws -} - -extension DemanglingTests { - func mainTest() async throws { - let allSwiftSymbols = try await allSymbols() - - // Counters - var totalCount = 0 - var successCount = 0 - var knownIssueCount = 0 - var demangleFailCount = 0 - var nodeTreeMismatchCount = 0 - var remangleMismatchCount = 0 - - // Sample collection (limit per category) - let maxSamples = 10 - var demangleFailSamples: [String] = [] - var nodeTreeMismatchSamples: [String] = [] - var remangleMismatchSamples: [String] = [] - - for symbol in allSwiftSymbols { - totalCount += 1 - let mangledName = symbol.stringValue - let stdlibTree = MachOFixtureSupport.stdlib_demangleNodeTree(mangledName) - - do { - let node = try await demangleAsNode(mangledName) - var allPassed = true - - // 1. Node tree check - if let stdlibTree { - let ourTree = node.description + "\n" - if stdlibTree != ourTree { - if isOpaqueReturnTypeParentDifference(stdlibTree, ourTree) { - knownIssueCount += 1 - } else { - allPassed = false - nodeTreeMismatchCount += 1 - if nodeTreeMismatchSamples.count < maxSamples { - nodeTreeMismatchSamples.append(mangledName) - } - Issue.record("Node tree mismatch: \(mangledName)") - } - } - } - - // 2. Remangle check - let remangled = try await Demangling.mangleAsString(node) - if remangled != mangledName { - // Known issue: Md vs MD (Apple-internal lowercase 'd') - if mangledName.hasSuffix("Md"), remangled.hasSuffix("MD"), - mangledName.dropLast(2) == remangled.dropLast(2) { - knownIssueCount += 1 - } else { - allPassed = false - remangleMismatchCount += 1 - if remangleMismatchSamples.count < maxSamples { - remangleMismatchSamples.append(" \(mangledName)\n remangled: \(remangled)") - } - Issue.record("Remangle mismatch: \(mangledName)") - } - } - - if allPassed { successCount += 1 } - } catch { - if stdlibTree != nil { - // stdlib succeeded but we failed - demangleFailCount += 1 - if demangleFailSamples.count < maxSamples { - demangleFailSamples.append(" \(mangledName) — \(error)") - } - Issue.record("Demangle failed: \(mangledName): \(error)") - } else { - successCount += 1 // both failed = consistent - } - } - } - - // Print summary - print(""" - - ═══ Demangling Alignment Report ═══ - Total symbols: \(totalCount) - Passed: \(successCount) - Known issues (skip): \(knownIssueCount) - Demangle failures: \(demangleFailCount) - Node tree mismatches: \(nodeTreeMismatchCount) - Remangle mismatches: \(remangleMismatchCount) - """) - - if !demangleFailSamples.isEmpty { - print("--- Demangle Failures (first \(demangleFailSamples.count)) ---") - for sample in demangleFailSamples { - print(sample) - } - } - if !nodeTreeMismatchSamples.isEmpty { - print("--- Node Tree Mismatches (first \(nodeTreeMismatchSamples.count)) ---") - for sample in nodeTreeMismatchSamples { - print(sample) - } - } - if !remangleMismatchSamples.isEmpty { - print("--- Remangle Mismatches (first \(remangleMismatchSamples.count)) ---") - for sample in remangleMismatchSamples { - print(sample) - } - } - } - - /// Check if the difference between two tree strings is only in OpaqueReturnTypeParent lines. - private func isOpaqueReturnTypeParentDifference(_ lhs: String, _ rhs: String) -> Bool { - let filteredLhs = lhs.split(separator: "\n", omittingEmptySubsequences: false) - .filter { !$0.contains("OpaqueReturnTypeParent") } - let filteredRhs = rhs.split(separator: "\n", omittingEmptySubsequences: false) - .filter { !$0.contains("OpaqueReturnTypeParent") } - return filteredLhs == filteredRhs - } -} diff --git a/Tests/MachOSymbolsTests/DyldCacheSymbolDemanglingTests.swift b/Tests/MachOSymbolsTests/DyldCacheSymbolDemanglingTests.swift deleted file mode 100644 index e84c0848..00000000 --- a/Tests/MachOSymbolsTests/DyldCacheSymbolDemanglingTests.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import Testing -import Dependencies -import MachOKit -import MachOFoundation -@testable import Demangling -@testable import MachOTestingSupport -import MachOFixtureSupport - -@Suite -final class DyldCacheSymbolDemanglingTests: DyldCacheSymbolTests, DemanglingTests { - @Test func main() async throws { - try await mainTest() - } - - @Test func demangle() async throws { - let node = try await Demangling.demangleAsNode("_$sSis15WritableKeyPathCy17RealityFoundation23PhysicallyBasedMaterialVAE9BaseColorVGTHTm") -// try Demangling.mangleAsString(node).print() - node.description.print() - } - - @Test func stdlib_demangleNodeTree() async throws { - let mangledName = "_$s7SwiftUI11DisplayListV10PropertiesVs9OptionSetAAsAFP8rawValuex03RawI0Qz_tcfCTW" - let demangleNodeTree = MachOFixtureSupport.stdlib_demangleNodeTree(mangledName) - let stdlibNodeDescription = try #require(demangleNodeTree) - let swiftSectionNodeDescription = try await demangleAsNode(mangledName).description + "\n" - #expect(stdlibNodeDescription == swiftSectionNodeDescription) - } -} diff --git a/Tests/MachOSymbolsTests/XcodeMachOFilesSymbolDemanglingTests.swift b/Tests/MachOSymbolsTests/XcodeMachOFilesSymbolDemanglingTests.swift deleted file mode 100644 index e0872eb6..00000000 --- a/Tests/MachOSymbolsTests/XcodeMachOFilesSymbolDemanglingTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import Testing -@testable import Demangling -import MachOKit -import MachOFoundation -@testable import MachOTestingSupport -import MachOFixtureSupport -import Dependencies - -@Suite(.serialized) -final class XcodeMachOFilesSymbolDemanglingTests: DemanglingTests { - @MainActor - @Test func symbols() async throws { - try await mainTest() - } - - func allSymbols() throws -> [MachOSwiftSymbol] { - guard FileManager.default.fileExists(atPath: "/Applications/Xcode.app") else { return [] } - var symbols: [MachOSwiftSymbol] = [] - for machOFile in try XcodeMachOFileName.allCases.compactMap({ try File.loadFromFile(url: $0.url).machOFiles.first }) { - for symbol in machOFile.symbols where symbol.name.isSwiftSymbol { - symbols.append(MachOSwiftSymbol(imagePath: machOFile.imagePath, offset: symbol.offset, stringValue: symbol.name)) - } - for symbol in machOFile.exportedSymbols where symbol.name.isSwiftSymbol { - if let offset = symbol.offset { - symbols.append(MachOSwiftSymbol(imagePath: machOFile.imagePath, offset: offset, stringValue: symbol.name)) - } - } - } - return symbols - } -} diff --git a/Tests/Projects/SymbolTests/SymbolTestsCore/NestedGenerics.swift b/Tests/Projects/SymbolTests/SymbolTestsCore/NestedGenerics.swift index 34c911b9..4419ea8f 100644 --- a/Tests/Projects/SymbolTests/SymbolTestsCore/NestedGenerics.swift +++ b/Tests/Projects/SymbolTests/SymbolTestsCore/NestedGenerics.swift @@ -39,4 +39,30 @@ public enum NestedGenerics { self.elements = elements } } + + /// Three-level nesting with one direct protocol constraint per level. + /// Used as the fixture-side counter-example for the `currentRequirements`, + /// `currentParameters`, `parentRequirements`, and `parentParameters` + /// invariants — the cumulative parent storage at depth ≥ 2 is the trigger + /// for the P0.1 / P0.2 / P0.3 bugs surfaced in `GenericSpecializer`. + /// `InnerMostConstrainedTest`'s generic context surfaces: + /// - `parameters` (cumulative): [Outer, Middle, InnerMost] + /// - `requirements` (cumulative): [Outer:Hashable, Middle:Equatable, InnerMost:Comparable] + /// - `parentParameters[0]` (Outer cumulative): [Outer] + /// - `parentParameters[1]` (Middle cumulative): [Outer, Middle] + public struct NestedGenericThreeLevelConstraintTest { + public struct MiddleConstrainedTest { + public struct InnerMostConstrainedTest { + public var outer: Outer + public var middle: Middle + public var innerMost: InnerMost + + public init(outer: Outer, middle: Middle, innerMost: InnerMost) { + self.outer = outer + self.middle = middle + self.innerMost = innerMost + } + } + } + } } diff --git a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/nestedGenericsSnapshot.1.txt b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/nestedGenericsSnapshot.1.txt index 63924412..105981f6 100644 --- a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/nestedGenericsSnapshot.1.txt +++ b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/nestedGenericsSnapshot.1.txt @@ -42,4 +42,22 @@ struct SymbolTestsCore.NestedGenerics.NestedTypealiasGenericTest { /* Variable */ SymbolTestsCore.NestedGenerics.NestedTypealiasGenericTest.elements.setter : [A] SymbolTestsCore.NestedGenerics.NestedTypealiasGenericTest.elements.getter : [A] +} +struct SymbolTestsCore.NestedGenerics.NestedGenericThreeLevelConstraintTest where A: Swift.Hashable {} +struct SymbolTestsCore.NestedGenerics.NestedGenericThreeLevelConstraintTest.MiddleConstrainedTest where A1: Swift.Equatable {} +struct SymbolTestsCore.NestedGenerics.NestedGenericThreeLevelConstraintTest.MiddleConstrainedTest.InnerMostConstrainedTest where A2: Swift.Comparable { + var outer: A + var middle: A1 + var innerMost: A2 + + /* Allocator */ + SymbolTestsCore.NestedGenerics.NestedGenericThreeLevelConstraintTest.MiddleConstrainedTest.InnerMostConstrainedTest.init(outer: A, middle: A1, innerMost: A2) -> SymbolTestsCore.NestedGenerics.NestedGenericThreeLevelConstraintTest.MiddleConstrainedTest.InnerMostConstrainedTest + + /* Variable */ + SymbolTestsCore.NestedGenerics.NestedGenericThreeLevelConstraintTest.MiddleConstrainedTest.InnerMostConstrainedTest.middle.getter : A1 + SymbolTestsCore.NestedGenerics.NestedGenericThreeLevelConstraintTest.MiddleConstrainedTest.InnerMostConstrainedTest.middle.setter : A1 + SymbolTestsCore.NestedGenerics.NestedGenericThreeLevelConstraintTest.MiddleConstrainedTest.InnerMostConstrainedTest.innerMost.getter : A2 + SymbolTestsCore.NestedGenerics.NestedGenericThreeLevelConstraintTest.MiddleConstrainedTest.InnerMostConstrainedTest.innerMost.setter : A2 + SymbolTestsCore.NestedGenerics.NestedGenericThreeLevelConstraintTest.MiddleConstrainedTest.InnerMostConstrainedTest.outer.getter : A + SymbolTestsCore.NestedGenerics.NestedGenericThreeLevelConstraintTest.MiddleConstrainedTest.InnerMostConstrainedTest.outer.setter : A } \ No newline at end of file diff --git a/Tests/SwiftInterfaceTests/GenericSpecializationTests.swift b/Tests/SwiftInterfaceTests/GenericSpecializationTests.swift index 5bd43e3c..d8069a2f 100644 --- a/Tests/SwiftInterfaceTests/GenericSpecializationTests.swift +++ b/Tests/SwiftInterfaceTests/GenericSpecializationTests.swift @@ -13,181 +13,2307 @@ import MachOFixtureSupport @testable import Demangling import OrderedCollections -struct TestGenericStruct where A: Collection, B: Equatable, C: Hashable, A.Element: Hashable, A.Element: Decodable, A.Element: Encodable { - let a: A - let b: B - let c: C -} +@Suite(.serialized) +struct GenericSpecializationTests { + /// One-shot cache of the `SwiftInterfaceIndexer` shape used across all + /// nested suites. swift-testing instantiates a fresh suite struct per + /// `@Test`; the actor lets each instance share a single prepared indexer + /// instead of paying preparation cost N × suite-count times. + fileprivate actor SharedIndexerCache { + static let shared = SharedIndexerCache() -final class GenericSpecializationTests: MachOImageTests, @unchecked Sendable { - override class var imageName: MachOImageName { .SwiftUICore } + private var indexerCache: SwiftInterfaceIndexer? - @Test func main() async throws { - let machO = MachOImage.current() + enum CacheError: Error, LocalizedError { + case missingImage(name: String) - let indexer = SwiftInterfaceIndexer(in: machO) + var errorDescription: String? { + switch self { + case .missingImage(let name): + return "expected MachOImage(name: \"\(name)\") to be loadable for the test fixture" + } + } + } - try indexer.addSubIndexer(SwiftInterfaceIndexer(in: #require(MachOImage(name: "Foundation")))) - try indexer.addSubIndexer(SwiftInterfaceIndexer(in: #require(MachOImage(name: "libswiftCore")))) + /// Indexer over the current process plus Foundation and libswiftCore. + func indexer() async throws -> SwiftInterfaceIndexer { + if let indexerCache { return indexerCache } + let indexer = SwiftInterfaceIndexer(in: MachOImage.current()) + try indexer.addSubIndexer(SwiftInterfaceIndexer(in: Self.requireImage(name: "Foundation"))) + try indexer.addSubIndexer(SwiftInterfaceIndexer(in: Self.requireImage(name: "libswiftCore"))) + try await indexer.prepare() + indexerCache = indexer + return indexer + } - try await indexer.prepare() + private static func requireImage(name: String) throws -> MachOImage { + guard let image = MachOImage(name: name) else { + throw CacheError.missingImage(name: name) + } + return image + } + } - let descriptor = try #require(try machO.swift.typeContextDescriptors.first { try $0.struct?.name(in: machO) == "TestGenericStruct" }?.struct?.asPointerWrapper(in: machO)) + /// Single shared `MachOImage.current()` reference for the nested suites. + /// `.current()` already returns the same identity each call, but caching + /// it once spares the repeated function-call overhead and makes the + /// access path symmetric with `SharedIndexerCache.shared`. + fileprivate static let sharedMachO: MachOImage = .current() - let genericContext = try #require(try descriptor.genericContext()) + /// Shared environment for the nested suites. Conforming suites get a + /// `machO` (sync, backed by `sharedMachO`) and an `indexer` (async, + /// backed by `SharedIndexerCache.shared.indexer()`) for free via the + /// default implementations below — no per-suite stored properties or + /// `init` are needed. + protocol Environment { + var machO: MachOImage { get } + var indexer: SwiftInterfaceIndexer { get async throws } + } - #expect(genericContext.header.numKeyArguments == 9) + // MARK: - Fixture types + // + // All generic-shape fixtures live on the outer suite so that the + // file-scope conditional `Copyable` / `Escapable` extensions at the + // bottom of this file can keep referencing them via the canonical + // paths (e.g. `GenericSpecializationTests.NestedInvertedOuter + // .NestedInvertedMiddle.NestedInvertedInner`). Most fixtures are + // looked up by mangled-name substring on the binary at runtime, so + // the only Swift-side requirement is that the type be present + // somewhere in the test image. - let AMetatype = [Int].self - let AProtocol = (any Collection).self + struct TestGenericStruct where A.Element: Hashable, A.Element: Decodable, A.Element: Encodable { + let a: A + let b: B + let c: C + } - let BMetatype = Double.self - let BProtocol = (any Equatable).self + struct TestUnconstrainedStruct { + let a: A + } - let CMetatype = Data.self - let CProtocol = (any Hashable).self + struct TestSingleProtocolStruct { + let a: A + } - let AMetadata = try Metadata.createInProcess(AMetatype) - let BMetadata = try Metadata.createInProcess(BMetatype) - let CMetadata = try Metadata.createInProcess(CMetatype) + struct TestMultiProtocolStruct { + let a: A + } - let specializer = GenericSpecializer(indexer: indexer) + struct TestClassConstraintStruct { + let a: A + } - let associatedTypeWitnesses = try specializer.resolveAssociatedTypeWitnesses(for: .struct(descriptor), substituting: [ - "A": AMetadata, - "B": BMetadata, - "C": CMetadata, - ]) + final class TestRefClass {} - let metadataAccessorFunction = try #require(try descriptor.metadataAccessorFunction()) - let metadata = try metadataAccessorFunction( - request: .completeAndBlocking, metadatas: [ - AMetadata, - BMetadata, - CMetadata, - ], witnessTables: [ - #require(try RuntimeFunctions.conformsToProtocol(metatype: AMetatype, protocolType: AProtocol)), - #require(try RuntimeFunctions.conformsToProtocol(metatype: BMetatype, protocolType: BProtocol)), - #require(try RuntimeFunctions.conformsToProtocol(metatype: CMetatype, protocolType: CProtocol)), - ] + associatedTypeWitnesses.values.flatMap { $0 } - ) - try #expect(#require(metadata.value.resolve().struct).fieldOffsets() == [0, 8, 16]) + struct TestNestedAssociatedStruct where A.Element: Sequence, A.Element.Element: Hashable { + let a: A } - - @Test func makeRequest() async throws { - let machO = MachOImage.current() - let descriptor = try #require(try machO.swift.typeContextDescriptors.first { try $0.struct?.name(in: machO) == "TestGenericStruct" }?.struct) + struct TestDualAssociatedStruct where A.Element: Hashable, B.Element: Hashable { + let a: A + let b: B + } - let indexer = SwiftInterfaceIndexer(in: machO) - - try await indexer.prepare() - - let specializer = GenericSpecializer( - machO: machO, - conformanceProvider: IndexerConformanceProvider(indexer: indexer) - ) - let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + struct TestMixedConstraintsStruct where A.Element: Hashable { + let a: A + let b: B + } - for candidate in request.parameters[1].candidates { - candidate.typeName.name.print() + struct TestInvertedCopyableStruct: ~Copyable { + let a: A + } + + /// Two-level nested generic. **Baseline** — single-level parent nesting + /// happens to produce the right `(depth, index)` mapping because + /// `parentParameters.last.count` and `parentParameters.flatMap.count` + /// coincide when there is only one parent generic context. The matching + /// test should keep passing on the current implementation. + struct NestedGenericTwoLevelOuter { + struct NestedGenericTwoLevelInner { + let a: A + let b: B } - - // Should have 3 parameters: A, B, C - #expect(request.parameters.count == 3) + } - // Check parameter names follow depth/index naming convention - #expect(request.parameters[0].name == "A") - #expect(request.parameters[1].name == "B") - #expect(request.parameters[2].name == "C") + /// Three-level nested generic — one type parameter per level, each with + /// a different protocol constraint. Exercises **P0.1** (the + /// `currentRequirements` flatMap miscount in `GenericContext.swift:50`) + /// and **P0.2** (the `buildParameters` (depth, index) iteration over the + /// cumulative `allParameters` in `GenericSpecializer.swift:131`). + struct NestedGenericThreeLevelOuter { + struct NestedGenericThreeLevelMiddle { + struct NestedGenericThreeLevelInner { + let a: A + let b: B + let c: C + } + } + } - // Check requirements exist - #expect(request.parameters[0].hasProtocolRequirements) // A: Collection - #expect(request.parameters[1].hasProtocolRequirements) // B: Equatable - #expect(request.parameters[2].hasProtocolRequirements) // C: Hashable + /// Three-level nested generic with `~Copyable` on every type parameter. + /// Each layer's `InvertedProtocols` requirement records the suppressed + /// parameter via a *flat* `genericParamIndex` set by Swift's + /// `lib/IRGen/GenMeta.cpp:7488-7501` — `sig->getGenericParamOrdinal(genericParam)` + /// returns the parameter's position across **every** depth. + /// + /// Expected: + /// "A" → flat index 0 → invertibleProtocols == .copyable + /// "A1" → flat index 1 → invertibleProtocols == .copyable + /// "A2" → flat index 2 → invertibleProtocols == .copyable + struct NestedInvertedOuter: ~Copyable { + struct NestedInvertedMiddle: ~Copyable { + struct NestedInvertedInner: ~Copyable { + var a: A + var b: B + var c: C + } + } + } - // Check associated type requirements - #expect(!request.associatedTypeRequirements.isEmpty) + struct TestTypePackStruct { + let value: (repeat each T) } - @Test func validation() throws { - let machO = MachOImage.current() + /// Fixture with three generic parameters whose associated-type chains + /// can resolve to overlapping leaf metadata (A=[Int], B=String, + /// C=[Int] makes A.Element and C.Element both Int). Used by the + /// PWT-ordering invariant tests. + struct TestTriAssociatedSameLeafStruct + where A.Element: Hashable, B.Element: Hashable, C.Element: Hashable + { + let a: A + let b: B + let c: C + } - let descriptor = try #require(try machO.swift.typeContextDescriptors.first { try $0.struct?.name(in: machO) == "TestGenericStruct" }?.struct) + struct TestNonGenericStruct { + let value: Int + } - let specializer = GenericSpecializer( - machO: machO, - conformanceProvider: EmptyConformanceProvider() - ) - let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + enum TestInvertedEscapableEnum: ~Escapable {} + + enum TestInvertedDualEnum: ~Copyable & ~Escapable {} + + enum TestGenericEnum { + case some(A) + case none + } + + final class TestGenericClass { + let a: A + init(a: A) { self.a = a } + } + + /// Two unrelated protocols both declaring `associatedtype Element`. + /// The companion `DualElementStruct` fixture pins the Swift compiler's + /// GenericSignature minimization invariant — see the matching test + /// `dualProtocolSameNamedAssociatedTypeIsCanonicalized`. + protocol PWithElement { + associatedtype Element: Hashable + func produceElement() -> Element + } + + protocol QWithElement { + associatedtype Element: Comparable + func consumeElement(_: Element) + } + + struct DualElementStruct where A.Element: Codable { + let value: A + } + + // MARK: - Make Request + // + // `makeRequest` and request-shape inspection. Tests here verify the + // structure of the produced `SpecializationRequest` (parameters, + // requirements, candidates, invertible-protocol flags, associated-type + // path aggregation). End-to-end specialization is in the `Specialize` + // suite below. + + @Suite("Make Request") + struct MakeRequest: Environment { + @Test func basicShape() async throws { + let descriptor = try structDescriptor(named: "TestGenericStruct") + + let specializer = GenericSpecializer( + machO: machO, + conformanceProvider: IndexerConformanceProvider(indexer: try await indexer) + ) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + // Should have 3 parameters: A, B, C + #expect(request.parameters.count == 3) + + // Check parameter names follow depth/index naming convention + #expect(request.parameters[0].name == "A") + #expect(request.parameters[1].name == "B") + #expect(request.parameters[2].name == "C") + + // Check requirements exist + #expect(request.parameters[0].hasProtocolRequirements) // A: Collection + #expect(request.parameters[1].hasProtocolRequirements) // B: Equatable + #expect(request.parameters[2].hasProtocolRequirements) // C: Hashable + + // Check associated type requirements + #expect(!request.associatedTypeRequirements.isEmpty) + } + + @Test func rejectsNonGenericType() throws { + let descriptor = try structDescriptor(named: "TestNonGenericStruct") + let specializer = GenericSpecializer( + machO: machO, + conformanceProvider: EmptyConformanceProvider() + ) + + do { + _ = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + Issue.record("expected notGenericType to be thrown for a fixture without generic context") + } catch let error as GenericSpecializer.SpecializerError { + switch error { + case .notGenericType: + return + default: + Issue.record("expected notGenericType, got \(error)") + } + } + } + + @Test func rejectsTypePackParameter() throws { + let descriptor = try structDescriptor(named: "TestTypePackStruct") + + let specializer = GenericSpecializer( + machO: machO, + conformanceProvider: EmptyConformanceProvider() + ) + + do { + _ = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + Issue.record("P3: makeRequest must reject a fixture containing a TypePack parameter") + } catch let error as GenericSpecializer.SpecializerError { + switch error { + case .unsupportedGenericParameter(let kind): + #expect(kind == .typePack) + default: + Issue.record("P3: expected unsupportedGenericParameter, got \(error)") + } + } + } + + @Test func excludeGenericsFiltersGenericCandidates() async throws { + let descriptor = try structDescriptor(named: "TestSingleProtocolStruct") + let specializer = GenericSpecializer(indexer: try await indexer) + + let defaultRequest = try specializer.makeRequest( + for: TypeContextDescriptorWrapper.struct(descriptor) + ) + let defaultHasGenericCandidate = defaultRequest.parameters[0].candidates.contains { $0.isGeneric } + #expect(defaultHasGenericCandidate, "P7 baseline: default candidate list should include generic candidates") + + let filteredRequest = try specializer.makeRequest( + for: TypeContextDescriptorWrapper.struct(descriptor), + candidateOptions: .excludeGenerics + ) + let filteredHasGenericCandidate = filteredRequest.parameters[0].candidates.contains { $0.isGeneric } + #expect(!filteredHasGenericCandidate, "P7: excludeGenerics must drop every isGeneric candidate") + #expect(!filteredRequest.parameters[0].candidates.isEmpty, + "P7: there are still non-generic Hashable conformers (Int, String, …) — the filter must not empty the list") + } + + @Test func noInvertedRequirementYieldsNil() async throws { + let descriptor = try structDescriptor(named: "TestGenericStruct") + + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + #expect(request.parameters.count == 3) + for parameter in request.parameters { + #expect(parameter.invertibleProtocols == nil) + } + } + + @Test func invertedCopyableExposed() async throws { + let descriptor = try structDescriptor(named: "TestInvertedCopyableStruct") + + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + #expect(request.parameters.count == 1) + + let invertible = try #require(request.parameters[0].invertibleProtocols) + // ~Copyable only: the set encodes which protocols are suppressed, so + // the parameter declaring `~Copyable` (and not `~Escapable`) must + // produce exactly `[.copyable]` — using `==` instead of `contains` + // catches a regression where extra bits leak into the set. + #expect(invertible == .copyable) + + // Specialize with a Copyable type (Int) — the conditional Copyable + // extension makes the struct itself Copyable when A is Copyable, so + // the metadata accessor should succeed. + let result = try specializer.specialize(request, with: ["A": .metatype(Int.self)]) + let structMetadata = try #require(result.resolveMetadata().struct) + #expect(try structMetadata.fieldOffsets() == [0]) + } + + @Test func invertedEscapableExposed() async throws { + let descriptor = try enumDescriptor(named: "TestInvertedEscapableEnum") + let specializer = GenericSpecializer( + machO: machO, + conformanceProvider: EmptyConformanceProvider() + ) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.enum(descriptor)) + + #expect(request.parameters.count == 1) + let invertible = try #require(request.parameters[0].invertibleProtocols) + #expect(invertible == .escapable, "single ~Escapable should produce exactly the Escapable bit") + } + + @Test func invertedDualCopyableAndEscapable() async throws { + let descriptor = try enumDescriptor(named: "TestInvertedDualEnum") + let specializer = GenericSpecializer( + machO: machO, + conformanceProvider: EmptyConformanceProvider() + ) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.enum(descriptor)) + + #expect(request.parameters.count == 1) + let invertible = try #require(request.parameters[0].invertibleProtocols) + // Set equality — both bits must be present and nothing else. + #expect(invertible == InvertibleProtocolSet([.copyable, .escapable])) + } + + @Test func nestedAssociatedTypeShape() async throws { + let (descriptor, genericContext) = try genericStructFixture(named: "TestNestedAssociatedStruct") + + // 1 metadata + 3 PWT (A:Sequence, A.Element:Sequence, A.Element.Element:Hashable) + #expect(genericContext.header.numKeyArguments == 4) + + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + #expect(request.parameters.count == 1) + #expect(request.parameters[0].protocolRequirements.count == 1) + + // Two associated requirements: A.Element and A.Element.Element + #expect(request.associatedTypeRequirements.count == 2) + + let pathByDepth = request.associatedTypeRequirements.map(\.path) + // The single-level path "[Element]" must exist (A.Element: Sequence) + #expect(pathByDepth.contains(["Element"])) + // The two-level path "[Element, Element]" must exist (A.Element.Element: Hashable) + #expect(pathByDepth.contains(["Element", "Element"])) + } + + // P8 reproduction: AssociatedTypeRequirement aggregates by (param, path). + // + // Pre-fix every individual constraint emitted its own + // `AssociatedTypeRequirement` even when several constraints shared + // the same `(parameterName, path)` — `requirements: [Requirement]` + // was always a singleton array, despite the field being plural. + // Consumers had to re-group by hand. Post-fix the build pass + // aggregates by key and preserves canonical (binary) order inside + // each entry. + @Test func associatedTypeRequirementsAggregatedByPath() async throws { + // TestGenericStruct has three constraints on A.Element (Hashable, + // Decodable, Encodable). Pre-fix `associatedTypeRequirements` would + // hold three entries with `path == ["Element"]` each carrying one + // requirement. Post-fix it should hold a single entry whose + // `requirements` array carries all three. + let descriptor = try structDescriptor(named: "TestGenericStruct") + let specializer = GenericSpecializer( + machO: machO, + conformanceProvider: EmptyConformanceProvider() + ) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + let elementEntries = request.associatedTypeRequirements.filter { + $0.parameterName == "A" && $0.path == ["Element"] + } + #expect( + elementEntries.count == 1, + "P8: A.Element constraints must collapse into one AssociatedTypeRequirement, got \(elementEntries.count) entries" + ) + let aggregate = try #require(elementEntries.first) + #expect( + aggregate.requirements.count == 3, + "P8: aggregated entry should carry all three protocols (Hashable / Decodable / Encodable), got \(aggregate.requirements.count)" + ) + let names = aggregate.requirements.compactMap { req -> String? in + if case .protocol(let info) = req { return info.protocolName.name } + return nil + } + // Canonical order is alphabetical-by-protocol within the same LHS. + #expect(names == names.sorted(), "P8: aggregated requirements must preserve canonical (alphabetical-by-protocol) order") + } + + // ABI invariant: when two unrelated protocols both declare + // `associatedtype Element` and a generic parameter conforms to + // BOTH, `A.Element` references in the emitted requirements + // canonicalize to a SINGLE declaring protocol (RequirementMachine's + // reduction order; empirically the lexicographically earlier + // protocol). The binary never emits two distinct LHS forms + // (`A.[P:Element]` vs `A.[Q:Element]`). + // + // This invariant lets `AssociatedTypeRequirementKey = (parameterName, + // [stepName])` aggregate by name without losing protocol identity: + // there is only ever one protocol tag per (parameter, name) in the + // canonical signature, so two requirements landing on the same path + // genuinely describe constraints on the same dependent member type. + @Test func dualProtocolSameNamedAssociatedTypeIsCanonicalized() throws { + let descriptor = try structDescriptor(named: "DualElementStruct") + let genericContext = try #require(try descriptor.genericContext(in: machO)) + + // Walk every requirement's LHS, collecting the declaring protocol + // identity for any `A.Element`-rooted dependent member type. + var protocolIdentities: Set = [] + var elementRequirementCount = 0 + for req in genericContext.requirements { + let mangled = try req.paramMangledName(in: machO) + let node = try MetadataReader.demangleType(for: mangled, in: machO) + guard let path = GenericSpecializer.extractAssociatedPath(of: node), + !path.steps.isEmpty, + path.baseParamName == "A", + path.steps.first?.name == "Element" else { + continue + } + elementRequirementCount += 1 + protocolIdentities.insert( + path.steps[0].protocolNode.print(using: .interfaceTypeBuilderOnly) + ) + } + + // Sanity: with `where A.Element: Codable`, the binary emits two + // requirements (Decodable + Encodable, the two real PWT-bearing + // protocols Codable expands to). If this number changes, the + // fixture's invariant has shifted in a way the test author should + // re-verify. + #expect(elementRequirementCount == 2) + + // The actual claim: every `A.Element` requirement references the + // SAME declaring protocol after canonicalization. + #expect( + protocolIdentities.count == 1, + "GenericSignature minimization should pick one canonical protocol for A.Element; saw \(protocolIdentities)" + ) + } + } + + // MARK: - Specialize + // + // End-to-end pipeline tests that drive `makeRequest` → `specialize` → + // metadata accessor → field-offset verification. Each test pins a + // specific fixture shape (unconstrained, single protocol, multi + // protocol, class constraint, nested associated, etc.) plus the + // configuration knobs (`metadataRequest`, candidate options, argument + // case routing). + + @Suite("Specialize") + struct Specialize: Environment { + @Test func manualAccessorMatchesSpecializerWitnessOrder() async throws { + let descriptor = try inProcessStructDescriptor(named: "TestGenericStruct") + + let genericContext = try #require(try descriptor.genericContext()) + + #expect(genericContext.header.numKeyArguments == 9) + + let AMetatype = [Int].self + let AProtocol = (any Collection).self + + let BMetatype = Double.self + let BProtocol = (any Equatable).self + + let CMetatype = Data.self + let CProtocol = (any Hashable).self + + let AMetadata = try Metadata.createInProcess(AMetatype) + let BMetadata = try Metadata.createInProcess(BMetatype) + let CMetadata = try Metadata.createInProcess(CMetatype) + + let specializer = GenericSpecializer(indexer: try await indexer) + + let associatedTypeWitnesses = try specializer.resolveAssociatedTypeWitnesses(for: .struct(descriptor), substituting: [ + "A": AMetadata, + "B": BMetadata, + "C": CMetadata, + ]) + + let metadataAccessorFunction = try #require(try descriptor.metadataAccessorFunction()) + let metadata = try metadataAccessorFunction( + request: .completeAndBlocking, metadatas: [ + AMetadata, + BMetadata, + CMetadata, + ], witnessTables: [ + #require(try RuntimeFunctions.conformsToProtocol(metatype: AMetatype, protocolType: AProtocol)), + #require(try RuntimeFunctions.conformsToProtocol(metatype: BMetatype, protocolType: BProtocol)), + #require(try RuntimeFunctions.conformsToProtocol(metatype: CMetatype, protocolType: CProtocol)), + ] + associatedTypeWitnesses + ) + try #expect(#require(metadata.value.resolve().struct).fieldOffsets() == [0, 8, 16]) + } + + @Test func threeParameter() async throws { + let descriptor = try structDescriptor(named: "TestGenericStruct") + + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + let selection: SpecializationSelection = [ + "A": .metatype([Int].self), + "B": .metatype(Double.self), + "C": .metatype(Data.self), + ] + + let result = try specializer.specialize(request, with: selection) + + // Verify resolved arguments + #expect(result.resolvedArguments.count == 3) + #expect(result.resolvedArguments[0].parameterName == "A") + #expect(result.resolvedArguments[1].parameterName == "B") + #expect(result.resolvedArguments[2].parameterName == "C") + + // A: Collection requires a PWT + #expect(result.resolvedArguments[0].hasWitnessTables) + // B: Equatable requires a PWT + #expect(result.resolvedArguments[1].hasWitnessTables) + // C: Hashable requires a PWT + #expect(result.resolvedArguments[2].hasWitnessTables) + + // Verify we can resolve metadata + let metadata = try result.resolveMetadata() + let structMetadata = try #require(metadata.struct) + let fieldOffsets = try structMetadata.fieldOffsets() + #expect(fieldOffsets == [0, 8, 16]) + } + + @Test func unconstrainedParameter() async throws { + let (descriptor, genericContext) = try genericStructFixture(named: "TestUnconstrainedStruct") + + // 1 metadata, 0 PWT + #expect(genericContext.header.numKeyArguments == 1) + + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + #expect(request.parameters.count == 1) + #expect(request.parameters[0].name == "A") + #expect(!request.parameters[0].hasProtocolRequirements) + #expect(request.associatedTypeRequirements.isEmpty) + + let result = try specializer.specialize(request, with: ["A": .metatype(Int.self)]) + #expect(result.resolvedArguments.count == 1) + #expect(!result.resolvedArguments[0].hasWitnessTables) + + let metadata = try result.resolveMetadata() + let structMetadata = try #require(metadata.struct) + #expect(try structMetadata.fieldOffsets() == [0]) + } + + @Test func singleProtocolParameter() async throws { + let (descriptor, genericContext) = try genericStructFixture(named: "TestSingleProtocolStruct") + + // 1 metadata + 1 PWT (Hashable) + #expect(genericContext.header.numKeyArguments == 2) + + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + #expect(request.parameters.count == 1) + #expect(request.parameters[0].hasProtocolRequirements) + #expect(request.parameters[0].protocolRequirements.count == 1) + #expect(request.associatedTypeRequirements.isEmpty) + + let result = try specializer.specialize(request, with: ["A": .metatype(Int.self)]) + #expect(result.resolvedArguments.count == 1) + #expect(result.resolvedArguments[0].hasWitnessTables) + + let metadata = try result.resolveMetadata() + let structMetadata = try #require(metadata.struct) + #expect(try structMetadata.fieldOffsets() == [0]) + } + + @Test func multiProtocolParameter() async throws { + let (descriptor, genericContext) = try genericStructFixture(named: "TestMultiProtocolStruct") + + // 1 metadata + 3 PWT (Hashable, Decodable, Encodable) + #expect(genericContext.header.numKeyArguments == 4) + + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + #expect(request.parameters.count == 1) + #expect(request.parameters[0].protocolRequirements.count == 3) + #expect(request.associatedTypeRequirements.isEmpty) + + let result = try specializer.specialize(request, with: ["A": .metatype(Int.self)]) + #expect(result.resolvedArguments[0].witnessTables.count == 3) + + let metadata = try result.resolveMetadata() + let structMetadata = try #require(metadata.struct) + #expect(try structMetadata.fieldOffsets() == [0]) + } + + @Test func classConstraint() async throws { + let (descriptor, genericContext) = try genericStructFixture(named: "TestClassConstraintStruct") + + // 1 metadata, no PWT (layout requirement does not require WT) + #expect(genericContext.header.numKeyArguments == 1) + + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + #expect(request.parameters.count == 1) + // Layout requirement is recorded but does not need a witness table + #expect(!request.parameters[0].hasProtocolRequirements) + let hasLayout = request.parameters[0].requirements.contains { req in + if case .layout = req { return true } + return false + } + #expect(hasLayout) + + let result = try specializer.specialize(request, with: ["A": .metatype(TestRefClass.self)]) + #expect(!result.resolvedArguments[0].hasWitnessTables) + + let metadata = try result.resolveMetadata() + let structMetadata = try #require(metadata.struct) + #expect(try structMetadata.fieldOffsets() == [0]) + } + + @Test func nestedAssociatedType() async throws { + let descriptor = try structDescriptor(named: "TestNestedAssociatedStruct") + + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + // A = [[Int]] satisfies: Sequence, Element=[Int] is a Sequence, Element.Element=Int is Hashable + let result = try specializer.specialize(request, with: ["A": .metatype([[Int]].self)]) + #expect(result.resolvedArguments.count == 1) + + let metadata = try result.resolveMetadata() + let structMetadata = try #require(metadata.struct) + // Single field of type [[Int]] occupies one pointer slot + #expect(try structMetadata.fieldOffsets() == [0]) + } + + @Test func dualAssociated() async throws { + let (descriptor, genericContext) = try genericStructFixture(named: "TestDualAssociatedStruct") + + // 2 metadata + 4 PWT (A:Sequence, B:Sequence, A.Element:Hashable, B.Element:Hashable) + #expect(genericContext.header.numKeyArguments == 6) + + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + #expect(request.parameters.count == 2) + #expect(request.parameters[0].protocolRequirements.count == 1) // A: Sequence + #expect(request.parameters[1].protocolRequirements.count == 1) // B: Sequence + #expect(request.associatedTypeRequirements.count == 2) + + let pathByParam = Dictionary(grouping: request.associatedTypeRequirements, by: \.parameterName) + #expect(pathByParam["A"]?.first?.path == ["Element"]) + #expect(pathByParam["B"]?.first?.path == ["Element"]) + + // A = [Int] (Element = Int), B = [String] (Element = Character). + let result = try specializer.specialize(request, with: [ + "A": .metatype([Int].self), + "B": .metatype([String].self), + ]) + + #expect(result.resolvedArguments.count == 2) + #expect(result.resolvedArguments[0].witnessTables.count == 1) + #expect(result.resolvedArguments[1].witnessTables.count == 1) + + let metadata = try result.resolveMetadata() + let structMetadata = try #require(metadata.struct) + // [Int] occupies 8 bytes, [String] occupies 8 bytes (Array storage pointer) + #expect(try structMetadata.fieldOffsets() == [0, 8]) + } + + @Test func mixedConstraints() async throws { + let (descriptor, genericContext) = try genericStructFixture(named: "TestMixedConstraintsStruct") + + // 2 metadata + 3 PWT (A:Collection, B:Hashable, A.Element:Hashable) + #expect(genericContext.header.numKeyArguments == 5) + + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + #expect(request.parameters.count == 2) + #expect(request.parameters[0].name == "A") + #expect(request.parameters[1].name == "B") + #expect(request.parameters[0].protocolRequirements.count == 1) // Collection + #expect(request.parameters[1].protocolRequirements.count == 1) // Hashable + #expect(request.associatedTypeRequirements.count == 1) + #expect(request.associatedTypeRequirements[0].parameterName == "A") + #expect(request.associatedTypeRequirements[0].path == ["Element"]) + + let result = try specializer.specialize(request, with: [ + "A": .metatype([Int].self), + "B": .metatype(String.self), + ]) + #expect(result.resolvedArguments[0].witnessTables.count == 1) // A: Collection + #expect(result.resolvedArguments[1].witnessTables.count == 1) // B: Hashable + + let metadata = try result.resolveMetadata() + let structMetadata = try #require(metadata.struct) + // [Int] is one pointer (8 bytes), String is 16 bytes + // a at offset 0, b at offset 8 + #expect(try structMetadata.fieldOffsets() == [0, 8]) + } + + @Test func configurableMetadataRequest() async throws { + let descriptor = try structDescriptor(named: "TestGenericStruct") + + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + let selection: SpecializationSelection = [ + "A": .metatype([Int].self), + "B": .metatype(Double.self), + "C": .metatype(Data.self), + ] + + // Default request (existing behaviour) + let defaultResult = try specializer.specialize(request, with: selection) + let defaultOffsets = try #require(defaultResult.resolveMetadata().struct).fieldOffsets() + + // Explicit non-blocking complete request + let nonBlocking = MetadataRequest(state: .complete, isBlocking: false) + let explicitResult = try specializer.specialize( + request, + with: selection, + metadataRequest: nonBlocking + ) + let explicitOffsets = try #require(explicitResult.resolveMetadata().struct).fieldOffsets() + + #expect(defaultOffsets == [0, 8, 16]) + #expect(explicitOffsets == defaultOffsets) + } + + @Test func enumDescriptor() async throws { + let descriptor = try enumDescriptor(named: "TestGenericEnum") + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.enum(descriptor)) + + #expect(request.parameters.count == 1) + #expect(request.parameters[0].name == "A") + #expect(request.parameters[0].hasProtocolRequirements, + "A: Hashable must surface as a protocol requirement") + #expect(request.parameters[0].protocolRequirements.count == 1) + + let result = try specializer.specialize(request, with: ["A": .metatype(Int.self)]) + #expect(result.resolvedArguments.count == 1) + #expect(result.resolvedArguments[0].hasWitnessTables) + + // The resolved metadata must be an enum metadata kind, not a + // struct/class one — this is the assertion that proves the + // wrapper.enum case routed through the pipeline correctly. + let wrapper = try result.resolveMetadata() + _ = try #require(wrapper.enum, + "expected MetadataWrapper.enum, got \(wrapper)") + } + + @Test func classDescriptor() async throws { + let descriptor = try classDescriptor(named: "TestGenericClass") + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.class(descriptor)) + + #expect(request.parameters.count == 1) + #expect(request.parameters[0].name == "A") + #expect(request.parameters[0].hasProtocolRequirements, + "A: Hashable must surface as a protocol requirement") + #expect(request.parameters[0].protocolRequirements.count == 1) + + let result = try specializer.specialize(request, with: ["A": .metatype(Int.self)]) + #expect(result.resolvedArguments.count == 1) + #expect(result.resolvedArguments[0].hasWitnessTables) + + // The resolved metadata must be a class metadata kind. + let wrapper = try result.resolveMetadata() + _ = try #require(wrapper.class, + "expected MetadataWrapper.class, got \(wrapper)") + } + + @Test func argumentMetadataPathProducesSameMetadata() async throws { + let descriptor = try structDescriptor(named: "TestSingleProtocolStruct") + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + // Pre-resolved Metadata fed via `.metadata(...)` must produce a + // metadata pointer indistinguishable from the same selection + // expressed with `.metatype(...)` — both go through + // `Metadata.createInProcess(_:)` for the metatype case, so the + // comparison is identity at the pointer level. + let intMetadata = try Metadata.createInProcess(Int.self) + let viaMetadata = try specializer.specialize(request, with: ["A": .metadata(intMetadata)]) + let viaMetatype = try specializer.specialize(request, with: ["A": .metatype(Int.self)]) + + let metadataA = try viaMetadata.metadata() + let metadataB = try viaMetatype.metadata() + #expect(metadataA == metadataB, "Argument.metadata path must reach the same generic-metadata cache slot as Argument.metatype") + + // And the resolved argument plumbing should record the supplied + // metadata verbatim — the runtime PWT still has to be looked up, + // so `hasWitnessTables` reflects the (single, Hashable) protocol. + #expect(viaMetadata.resolvedArguments[0].metadata == intMetadata) + #expect(viaMetadata.resolvedArguments[0].hasWitnessTables) + } + + @Test func argumentCandidatePathSpecializesNonGenericCandidate() async throws { + let descriptor = try structDescriptor(named: "TestSingleProtocolStruct") + let specializer = GenericSpecializer(indexer: try await indexer) + + // Use `.excludeGenerics` so the parameter's candidate list only + // surfaces directly-specializable types. Pin the candidate to + // `Swift.Int` (matched via `currentName`) so the assertion below + // can compare against the equivalent `.metatype(Int.self)` path — + // an order-dependent `first { !$0.isGeneric }` would silently + // degrade if the indexer's iteration shifted. + let request = try specializer.makeRequest( + for: TypeContextDescriptorWrapper.struct(descriptor), + candidateOptions: .excludeGenerics + ) + let intCandidate = try #require( + request.parameters[0].candidates.first { + $0.typeName.currentName == "Int" && !$0.isGeneric + }, + "expected Swift.Int candidate after .excludeGenerics" + ) + + let viaCandidate = try specializer.specialize(request, with: ["A": .candidate(intCandidate)]) + let viaMetatype = try specializer.specialize(request, with: ["A": .metatype(Int.self)]) + + // Both paths must hit the same generic-metadata cache slot — + // candidate resolution should be a thin wrapper over the metadata + // accessor that `.metatype` already exercises. + let candidateMetadata = try viaCandidate.metadata() + let metatypeMetadata = try viaMetatype.metadata() + #expect( + candidateMetadata == metatypeMetadata, + "Argument.candidate path must reach the same metadata pointer as Argument.metatype for the same concrete type" + ) + + #expect(viaCandidate.resolvedArguments.count == 1) + #expect(viaCandidate.resolvedArguments[0].hasWitnessTables) + let structMetadata = try #require(viaCandidate.resolveMetadata().struct) + #expect(try structMetadata.fieldOffsets() == [0]) + } + + @Test func argumentSpecializedPathFeedsNestedSpecialization() async throws { + let descriptor = try structDescriptor(named: "TestUnconstrainedStruct") + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + // Step 1: build TestUnconstrainedStruct. + let inner = try specializer.specialize(request, with: ["A": .metatype(Int.self)]) + let innerMetadata = try inner.metadata() - // Test missing arguments - let emptySelection: SpecializationSelection = [:] - let validation = specializer.validate(selection: emptySelection, for: request) - #expect(!validation.isValid) - #expect(validation.errors.count == 3) // Missing A, B, C + // Step 2: feed `inner` back in as the outer's GP. Equivalent to + // TestUnconstrainedStruct>. + let outer = try specializer.specialize(request, with: ["A": .specialized(inner)]) + let outerMetadata = try outer.metadata() - // Test valid selection - let validSelection: SpecializationSelection = [ - "A": .metatype([Int].self), - "B": .metatype(Double.self), - "C": .metatype(Data.self), - ] - let validValidation = specializer.validate(selection: validSelection, for: request) - #expect(validValidation.isValid) + // The two metadatas must differ — they parameterize the same + // type with different concrete arguments. + #expect(innerMetadata != outerMetadata, "outer and inner specializations resolve to distinct generic metadata slots") + + // The resolved argument records the inner's metadata verbatim. + #expect(outer.resolvedArguments[0].metadata == innerMetadata) + + // Layout check: outer holds a single field of type + // TestUnconstrainedStruct, which is itself a single Int + // (8 bytes), so the outer field is at offset 0 and occupies one + // pointer-sized slot. + let structMetadata = try #require(outer.resolveMetadata().struct) + #expect(try structMetadata.fieldOffsets() == [0]) + } + + @Test func genericCandidateFailFast() async throws { + let descriptor = try structDescriptor(named: "TestSingleProtocolStruct") + + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + // Pin to specific stdlib types so a failure points clearly at either + // the fail-fast logic (test body) or a fixture shift (#require message), + // rather than the generic "no isGeneric candidate" mode the original + // first-matching-any-candidate form would silently degrade into. + // `currentName` strips the module prefix (e.g. "Swift.Int" → "Int"). + let genericCandidate = try #require( + request.parameters[0].candidates.first { $0.typeName.currentName == "Array" && $0.isGeneric }, + "expected Swift.Array candidate flagged isGeneric" + ) + let nonGenericCandidate = try #require( + request.parameters[0].candidates.first { $0.typeName.currentName == "Int" && !$0.isGeneric }, + "expected Swift.Int candidate flagged non-generic" + ) + + // Non-generic candidate still resolves successfully. + let okResult = try specializer.specialize( + request, + with: ["A": .candidate(nonGenericCandidate)] + ) + _ = try okResult.resolveMetadata() + + // Generic candidate throws the new typed error. + do { + _ = try specializer.specialize( + request, + with: ["A": .candidate(genericCandidate)] + ) + Issue.record("expected candidateRequiresNestedSpecialization to be thrown") + } catch let GenericSpecializer.SpecializerError.candidateRequiresNestedSpecialization(candidate, parameterCount) { + #expect(candidate.typeName == genericCandidate.typeName) + #expect(parameterCount >= 1) + } + } + + @Test func candidateErrorMessageMentionsSpecialized() async throws { + let descriptor = try structDescriptor(named: "TestSingleProtocolStruct") + + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + let genericCandidate = try #require( + request.parameters[0].candidates.first { $0.typeName.currentName == "Array" && $0.isGeneric }, + "expected Swift.Array candidate flagged isGeneric" + ) + + do { + _ = try specializer.specialize( + request, + with: ["A": .candidate(genericCandidate)] + ) + Issue.record("expected candidateRequiresNestedSpecialization to be thrown") + } catch let error as GenericSpecializer.SpecializerError { + let description = try #require(error.errorDescription) + #expect(description.contains("Argument.specialized")) + #expect(description.contains("Array")) + #expect(description.contains("generic")) + } + } + } + + // MARK: - Nested Generics + // + // Tests covering generic contexts at depth ≥ 2. The bugs reproduced here + // all stem from the same root: Swift's binary stores `parameters` and + // `requirements` cumulatively at every level of a nested generic context + // (see `swift/lib/IRGen/GenMeta.cpp:7263` — `canSig->forEachParam` walks + // every visible GP including inherited ones, and `addGenericRequirements` + // emits the full canonical signature). Single-level parent nesting works + // correctly by accident (the math falls out the same when there is + // exactly one parent generic context); the suite below pins behavior at + // depth ≥ 2 plus the SwiftDump dumper and the inverted-protocol overlay. + + @Suite("Nested Generics") + struct NestedGenerics: Environment { + @Test func twoLevelBaseline() throws { + let descriptor = try structDescriptor(named: "NestedGenericTwoLevelInner") + let genericContext = try #require(try descriptor.genericContext(in: machO)) + + // Inner sees both A (inherited from Outer) and B (its own), stored + // cumulatively. + #expect(genericContext.header.numParams == 2) + #expect(genericContext.parameters.count == 2) + #expect(genericContext.requirements.count == 2) + #expect(genericContext.parentParameters.count == 1) + #expect(genericContext.parentParameters.first?.count == 1) + #expect(genericContext.currentParameters.count == 1) + #expect(genericContext.currentRequirements.count == 1) + + let specializer = GenericSpecializer( + machO: machO, + conformanceProvider: EmptyConformanceProvider() + ) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + // Two parameters with demangler-canonical names "A" and "A1". + #expect(request.parameters.count == 2) + #expect(request.parameters.map(\.name) == ["A", "A1"]) + #expect(request.parameters[0].protocolRequirements.count == 1) // A: Hashable + #expect(request.parameters[1].protocolRequirements.count == 1) // A1 (= B): Equatable + } + + @Test func threeLevelCurrentRequirementsKeepsInnerRequirement() throws { + let descriptor = try structDescriptor(named: "NestedGenericThreeLevelInner") + let genericContext = try #require(try descriptor.genericContext(in: machO)) + + // Sanity — the binary stores parameters and requirements cumulatively. + #expect(genericContext.parameters.count == 3) // [A, B, C] + #expect(genericContext.requirements.count == 3) // [A:Hashable, B:Equatable, C:Comparable] + #expect(genericContext.parentParameters.count == 2) + #expect(genericContext.parentParameters.first?.count == 1) // Outer cumulative = [A] + #expect(genericContext.parentParameters.last?.count == 2) // Middle cumulative = [A, B] + #expect(genericContext.currentParameters.count == 1) // [C] + + // P0.1 — `currentRequirements` should be `[C: Comparable]` (1 entry). + // The current impl drops `parentRequirements.flatMap{$0}.count = 1 + 2 = 3` + // entries from a 3-element cumulative array, leaving an empty list. + // Correct behaviour mirrors `currentParameters`: drop only + // `parentRequirements.last?.count` entries. + #expect( + genericContext.currentRequirements.count == 1, + "P0.1: currentRequirements should be [C: Comparable]; flatMap-over-cumulative-parents over-drops at depth ≥ 2." + ) + } + + @Test func threeLevelMakeRequestProducesCanonicalParameterNames() throws { + let descriptor = try structDescriptor(named: "NestedGenericThreeLevelInner") + + let specializer = GenericSpecializer( + machO: machO, + conformanceProvider: EmptyConformanceProvider() + ) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + // P0.2 — `makeRequest` should expose exactly 3 type parameters whose + // canonical names match what the demangler produces for the binary's + // `qd_X` / `qd0_X` mangling: + // "A" (depth 0, idx 0) — Outer's A (mangled `x`) + // "A1" (depth 1, idx 0) — Middle's B (mangled `qd_0`) + // "A2" (depth 2, idx 0) — Inner's C (mangled `qd0_0`) + // + // The current impl iterates `allParameters` directly. Because the + // middle entry `[A, B]` is *cumulative* rather than newly-introduced, + // the loop emits an extra `Parameter` and shifts Middle's B to a wrong + // (depth, index). Concretely, names come out as ["A", "A1", "B1", "A2"]. + let names = request.parameters.map(\.name) + #expect( + request.parameters.count == 3, + "P0.2: expected 3 generic parameters, got \(request.parameters.count) named \(names)." + ) + #expect( + names == ["A", "A1", "A2"], + "P0.2: expected canonical names [A, A1, A2], got \(names)." + ) + + var parametersByName: [String: SpecializationRequest.Parameter] = [:] + for parameter in request.parameters { + parametersByName[parameter.name] = parameter + } + + // Each canonical parameter should carry exactly one direct protocol + // requirement. Under the bug, "A" sees A:Hashable twice (cumulative + // parent merge duplicates it), "A1" sees Middle's B (correctly), and + // "A2" sees nothing — Inner's C: Comparable is silently dropped by + // the buggy `currentRequirements` and never reaches `mergedRequirements`. + #expect(parametersByName["A"]?.protocolRequirements.count == 1) + #expect(parametersByName["A1"]?.protocolRequirements.count == 1) + #expect(parametersByName["A2"]?.protocolRequirements.count == 1) + } + + // P2: nested generic specialize() end-to-end coverage. + // + // The aa07d74 fix added `makeRequest` assertions for ≥ 3-level + // nested generics, but never actually called `specialize` on one. + // This test closes that gap by driving the full pipeline (request → + // specialize → metadata accessor → field offsets) on the + // `NestedGenericThreeLevelInner` fixture. + @Test func threeLevelSpecializeEndToEnd() async throws { + let descriptor = try structDescriptor(named: "NestedGenericThreeLevelInner") + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + let result = try specializer.specialize(request, with: [ + "A": .metatype(Int.self), // outer's A: Hashable + "A1": .metatype(Double.self), // middle's B: Equatable + "A2": .metatype(String.self), // inner's C: Comparable + ]) + + #expect(result.resolvedArguments.count == 3) + #expect(result.resolvedArguments[0].parameterName == "A") + #expect(result.resolvedArguments[1].parameterName == "A1") + #expect(result.resolvedArguments[2].parameterName == "A2") + // Each direct GP requirement contributes exactly one PWT. + #expect(result.resolvedArguments.allSatisfy { $0.witnessTables.count == 1 }) + + let structMetadata = try #require(result.resolveMetadata().struct) + let fieldOffsets = try structMetadata.fieldOffsets() + // Layout: a(Int) at 0, b(Double) at 8, c(String) at 16. String + // occupies 16 bytes but the field offset is the start address. + #expect(fieldOffsets == [0, 8, 16]) + } + + // P5 reproduction: SwiftDump cumulative-parameter dump. + // + // `dumpGenericParameters(isDumpCurrentLevel: false)` was iterating + // `allParameters`, whose parent levels are stored cumulatively. At + // depth ≥ 2 that would re-emit each inherited parameter (e.g. for + // our three-level `NestedGenericThreeLevelInner`, the dump produced + // `` — `A` re-appearing at depth 1, and Middle's `B` + // misnamed `B1` because the loop counted offsets by depth-cumulative + // position rather than per-level introduction). Post-fix the dumper + // walks per-level "newly introduced" slices, emitting exactly + // `` to match the demangler's canonical naming. + @Test func threeLevelDumpAllLevelsHasNoDuplicates() async throws { + let descriptor = try structDescriptor(named: "NestedGenericThreeLevelInner") + let genericContext = try #require(try descriptor.genericContext(in: machO)) + + let dumped = try await genericContext.dumpGenericParameters( + in: machO, + isDumpCurrentLevel: false + ).string + + // Expected demangler-canonical order: A, A1, A2. + let expectedNames = ["A", "A1", "A2"] + for name in expectedNames { + #expect( + dumped.contains(name), + "P5: dump must include `\(name)` (got `\(dumped)`)" + ) + } + + // The pre-fix output `` contained `B1` (Middle's `B` + // miscounted), and the bare `A` token was present *twice*. Both + // are tell-tale signs of cumulative parent re-emission. + #expect( + !dumped.contains("B1"), + "P5: pre-fix cumulative iteration produced a phantom `B1` token (got `\(dumped)`)" + ) + + // Catch the duplicate-`A` regression: a properly de-cumulated dump + // emits `A` once. The dump output has the form `A, A1, A2` (or + // similar) — split on `,` and trim, then count tokens equal to + // bare `A`. The lookbehind regex form would be cleaner but Swift + // Regex doesn't support lookbehind yet. + let tokens = dumped + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespaces) } + let bareACount = tokens.filter { $0 == "A" }.count + #expect( + bareACount == 1, + "P5: bare `A` must appear once; pre-fix cumulative iteration produced \(bareACount) occurrences in tokens \(tokens)" + ) + } + + @Test func threeLevelInvertedProtocolsPerLevel() throws { + let descriptor = try structDescriptor(named: "NestedInvertedInner") + + let specializer = GenericSpecializer( + machO: machO, + conformanceProvider: EmptyConformanceProvider() + ) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + // P0.2 prerequisite — without it the parameter list is malformed + // before we can even probe invertibleProtocols. We still run the + // P0.3 assertions below via lookup-by-name so that whichever P0 is + // fixed first surfaces a clean diagnostic. + let names = request.parameters.map(\.name) + #expect(request.parameters.count == 3, "P0.2 prerequisite: got names \(names)") + #expect(names == ["A", "A1", "A2"], "P0.2 prerequisite: got names \(names)") + + var parametersByName: [String: SpecializationRequest.Parameter] = [:] + for parameter in request.parameters { + parametersByName[parameter.name] = parameter + } + + // P0.3 — every parameter is declared `~Copyable`. + #expect( + parametersByName["A"]?.invertibleProtocols == .copyable, + "Outer's A (flat index 0) is `~Copyable`." + ) + #expect( + parametersByName["A1"]?.invertibleProtocols == .copyable, + "Middle's B (canonical A1, flat index 1) is `~Copyable`." + ) + #expect( + parametersByName["A2"]?.invertibleProtocols == .copyable, + "P0.3: Inner's C (canonical A2, flat index 2) is `~Copyable`; collectInvertibleProtocols looks it up at flat index 3 because of the cumulative parent count." + ) + } + + // M3 closure: three-level nested ~Copyable specialize end-to-end. + // + // `threeLevelInvertedProtocolsPerLevel` covers `makeRequest` for + // the same fixture, asserting that every per-level + // `invertibleProtocols` set comes out as `.copyable`. This test + // closes the gap by running the full pipeline (request → + // specialize → metadata accessor → field offsets) and verifying + // the conditional `extension … : Copyable where A: Copyable, B: + // Copyable, C: Copyable` chain is wired up correctly: binding + // every parameter to a Copyable type lets the metadata accessor + // produce a non-nil metadata pointer with the same field layout + // as the non-inverted three-level fixture. + @Test func threeLevelInvertedSpecializeEndToEnd() async throws { + let descriptor = try structDescriptor(named: "NestedInvertedInner") + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + let result = try specializer.specialize(request, with: [ + "A": .metatype(Int.self), // outer's A: ~Copyable, bound to Copyable Int + "A1": .metatype(Double.self), // middle's B: ~Copyable, bound to Copyable Double + "A2": .metatype(String.self), // inner's C: ~Copyable, bound to Copyable String + ]) + + #expect(result.resolvedArguments.count == 3) + #expect(result.resolvedArguments.map(\.parameterName) == ["A", "A1", "A2"]) + // None of the three GPs has a witness-table-bearing protocol + // requirement (they only declare `~Copyable`, which is a + // capability suppression rather than a constraint). + #expect(result.resolvedArguments.allSatisfy { !$0.hasWitnessTables }) + + // Layout matches the non-inverted three-level fixture: + // a(Int) at 0, b(Double) at 8, c(String) at 16. + let structMetadata = try #require(result.resolveMetadata().struct) + #expect(try structMetadata.fieldOffsets() == [0, 8, 16]) + } + } + + // MARK: - Validation + // + // `validate(selection:for:)` is the cheap static-only pass plus the + // public static constructors of `SpecializationValidation`. The + // runtime-aware companion `runtimePreflight` lives in its own suite + // so this group stays focused on argument-shape errors / warnings. + + @Suite("Validation") + struct Validation: Environment { + @Test func reportsMissingArguments() throws { + let descriptor = try structDescriptor(named: "TestGenericStruct") + + let specializer = GenericSpecializer( + machO: machO, + conformanceProvider: EmptyConformanceProvider() + ) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + // Test missing arguments + let emptySelection: SpecializationSelection = [:] + let validation = specializer.validate(selection: emptySelection, for: request) + #expect(!validation.isValid) + #expect(validation.errors.count == 3) // Missing A, B, C + + // Test valid selection + let validSelection: SpecializationSelection = [ + "A": .metatype([Int].self), + "B": .metatype(Double.self), + "C": .metatype(Data.self), + ] + let validValidation = specializer.validate(selection: validSelection, for: request) + #expect(validValidation.isValid) + } + + // M8: validate() reports extra-argument warnings. + // + // `validate(selection:for:)` is the cheap static-only pass and + // must not silently accept arguments for parameters the request + // does not declare. Missing arguments surface as errors; + // arguments for unknown parameters surface as `.extraArgument` + // warnings (the selection is still considered valid — + // `isValid == true` — because the extra entry is forwarded to + // no-one and cannot break the accessor call). + @Test func emitsExtraArgumentWarning() throws { + let descriptor = try structDescriptor(named: "TestSingleProtocolStruct") + let specializer = GenericSpecializer( + machO: machO, + conformanceProvider: EmptyConformanceProvider() + ) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + // `TestSingleProtocolStruct` declares a single GP "A". + // Adding "Z" to the selection should surface as a warning, not an + // error, and leave `isValid` unchanged. + let selection: SpecializationSelection = [ + "A": .metatype(Int.self), + "Z": .metatype(String.self), + ] + let validation = specializer.validate(selection: selection, for: request) + + #expect(validation.isValid, "extra arguments are warnings, not errors") + #expect(validation.errors.isEmpty) + let hasExtra = validation.warnings.contains { warning in + if case .extraArgument(let name) = warning { return name == "Z" } + return false + } + #expect(hasExtra, "validate must emit .extraArgument warning for a parameter not declared by the request") + } + + // Bug reproduction #15: validate doesn't distinguish associated-type + // path from a typo. + // + // `validate` flags any selection key not in `request.parameters` as + // `.extraArgument`. A user who mistakenly tries to set an + // associated-type path (`"A.Element"`) gets the same generic + // warning as someone who typo'd `"Z"`. The path is structured (it + // appears in `associatedTypeRequirements[*].fullPath`) so a more + // specific warning is possible. + @Test func givesSpecificWarningForAssociatedTypePath() async throws { + let descriptor = try structDescriptor(named: "TestNestedAssociatedStruct") + let specializer = GenericSpecializer( + machO: machO, + conformanceProvider: EmptyConformanceProvider() + ) + let request = try specializer.makeRequest( + for: TypeContextDescriptorWrapper.struct(descriptor) + ) + + // Sanity: this fixture has A.Element as a real associated-type path. + #expect(request.associatedTypeRequirements.contains { $0.fullPath == "A.Element" }) + + let selection: SpecializationSelection = [ + "A": .metatype([[Int]].self), + "A.Element": .metatype(Int.self), + ] + let validation = specializer.validate(selection: selection, for: request) + #expect(validation.isValid, "associated-type path is a warning, not an error") + + let hasSpecific = validation.warnings.contains { warning in + if case .associatedTypePathInSelection(let path) = warning { + return path == "A.Element" + } + return false + } + #expect( + hasSpecific, + "validate must distinguish an associated-type path (derived; not user-supplied) from a generic extra argument; got \(validation.warnings)" + ) + } + + // S3: SpecializationValidation static constructor coverage. + // + // `Builder` is the canonical construction path for the + // specializer's own `validate` / `runtimePreflight` (which need + // to accumulate a mix of errors and warnings). The three statics + // below are the ergonomic alternative for *terminal* construction + // in external callers — code that already knows whether + // validation passed and doesn't need the builder's append loop. + // None of the specializer's internal call sites use them; the + // tests pin the public surface so it doesn't drift. + + @Test func validStaticHasNoErrorsOrWarnings() throws { + let validation = SpecializationValidation.valid + #expect(validation.isValid) + #expect(validation.errors.isEmpty) + #expect(validation.warnings.isEmpty) + } + + @Test func failedStaticWithSingleErrorIsInvalid() throws { + let validation = SpecializationValidation.failed(.missingArgument(parameterName: "A")) + #expect(!validation.isValid) + #expect(validation.errors.count == 1) + #expect(validation.warnings.isEmpty) + if case .missingArgument(let name) = validation.errors[0] { + #expect(name == "A") + } else { + Issue.record("expected .missingArgument, got \(validation.errors[0])") + } + } + + @Test func failedStaticWithMultipleErrorsIsInvalid() throws { + let validation = SpecializationValidation.failed([ + .missingArgument(parameterName: "A"), + .missingArgument(parameterName: "B"), + ]) + #expect(!validation.isValid) + #expect(validation.errors.count == 2) + #expect(validation.warnings.isEmpty) + } + } + + // MARK: - Runtime Preflight + // + // `runtimePreflight(selection:for:)` exercises actual `Metadata` to + // catch protocol-conformance and class-layout mismatches before the + // accessor call. Pre-fix `validate` only checked missing/extra + // arguments; type-shape errors had to wait until `specialize` failed + // inside `RuntimeFunctions.conformsToProtocol` with the much vaguer + // `witnessTableNotFound`. + + @Suite("Runtime Preflight") + struct RuntimePreflight: Environment { + @Test func catchesProtocolMismatch() async throws { + // TestSingleProtocolStruct. Picking a Function type for + // A (Functions don't conform to Hashable) must trip the preflight. + let descriptor = try structDescriptor(named: "TestSingleProtocolStruct") + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + let badSelection: SpecializationSelection = ["A": .metatype((() -> Void).self)] + + let preflight = specializer.runtimePreflight(selection: badSelection, for: request) + #expect(!preflight.isValid) + let hasProtocolError = preflight.errors.contains { error in + if case .protocolRequirementNotSatisfied(_, let proto, _) = error { + return proto.contains("Hashable") + } + return false + } + #expect(hasProtocolError, "P6: preflight must report Hashable mismatch for () -> Void") + + // And the user-facing `specialize` should now throw with the same + // diagnostic instead of letting it surface as `witnessTableNotFound`. + do { + _ = try specializer.specialize(request, with: badSelection) + Issue.record("P6: specialize must reject the bad selection") + } catch let error as GenericSpecializer.SpecializerError { + if case .specializationFailed(let reason) = error { + #expect(reason.contains("Hashable")) + } else { + Issue.record("P6: expected specializationFailed, got \(error)") + } + } + } + + @Test func catchesLayoutMismatch() async throws { + // TestClassConstraintStruct. Picking a value type for A + // must trip the layout check. + let descriptor = try structDescriptor(named: "TestClassConstraintStruct") + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + let badSelection: SpecializationSelection = ["A": .metatype(Int.self)] + + let preflight = specializer.runtimePreflight(selection: badSelection, for: request) + #expect(!preflight.isValid) + let hasLayoutError = preflight.errors.contains { error in + if case .layoutRequirementNotSatisfied = error { + return true + } + return false + } + #expect(hasLayoutError, "P6: preflight must report layout mismatch for a value type passed where AnyObject is required") + } + + // Bug reproduction #4: runtimePreflight silently passes when the + // indexer is missing a required protocol. + // + // When the protocol referenced by a parameter requirement isn't + // in the indexer, `runtimePreflight` skips the conformance check + // entirely — it can't construct the protocol descriptor to call + // `swift_conformsToProtocol`. The fix surfaces this as a typed + // warning so the user knows to add another sub-indexer instead + // of getting a misleading `witnessTableNotFound` from `specialize`. + @Test func surfacesIndexerMissingProtocolWarning() async throws { + // Build an indexer that has the test image but NOT libswiftCore — + // so `Hashable` (defined in libswiftCore) won't be found. + let bareIndexer = SwiftInterfaceIndexer(in: machO) + try await bareIndexer.prepare() + + // Sanity: `TestSingleProtocolStruct` is in `machO`, + // so makeRequest still works (descriptors are read directly from + // the binary, not from the indexer). + let descriptor = try structDescriptor(named: "TestSingleProtocolStruct") + let specializer = GenericSpecializer(indexer: bareIndexer) + let request = try specializer.makeRequest( + for: TypeContextDescriptorWrapper.struct(descriptor) + ) + #expect(request.parameters.count == 1) + #expect(request.parameters[0].protocolRequirements.count == 1) + + // Pass a fully valid Hashable conformer. The bug: with libswiftCore + // missing from the indexer, preflight has no way to look up the + // protocol descriptor, so it skips the conformance check. That's + // fine on its own — but the user has no signal that validation is + // incomplete. + let preflight = specializer.runtimePreflight( + selection: ["A": .metatype(Int.self)], + for: request + ) + + // Expect a warning informing the user that a protocol couldn't be + // checked because it's not in the indexer. + let hasMissingProtocolWarning = preflight.warnings.contains { warning in + if case .protocolNotInIndexer(_, let proto) = warning { + return proto.contains("Hashable") + } + return false + } + #expect( + hasMissingProtocolWarning, + "preflight must warn when a parameter's protocol requirement is missing from the indexer; got warnings=\(preflight.warnings), errors=\(preflight.errors)" + ) + } + + // Bug reproduction #5: runtimePreflight skips .specialized. + // + // The pre-fix `runtimePreflight` had `.specialized` in its + // skip-list, claiming it required running an accessor to obtain + // the metadata. But a `SpecializationResult` already holds a + // resolved metadata pointer — there's no accessor to run. The + // skip silently let through specialized arguments whose result + // type didn't satisfy the target's protocol requirements. + // Failure surfaced inside `specialize` as the much vaguer + // `witnessTableNotFound`. + @Test func catchesProtocolMismatchOnSpecializedArgument() async throws { + let unconstrainedDescriptor = try structDescriptor(named: "TestUnconstrainedStruct") + let specializer = GenericSpecializer(indexer: try await indexer) + let unconstrainedRequest = try specializer.makeRequest( + for: TypeContextDescriptorWrapper.struct(unconstrainedDescriptor) + ) + + // `TestUnconstrainedStruct` does NOT conform to Hashable — + // the struct itself has no Hashable conformance, regardless of A. + let unconstrainedResult = try specializer.specialize( + unconstrainedRequest, + with: ["A": .metatype(Int.self)] + ) + + // Now feed it where Hashable is required. + let singleDescriptor = try structDescriptor(named: "TestSingleProtocolStruct") + let singleRequest = try specializer.makeRequest( + for: TypeContextDescriptorWrapper.struct(singleDescriptor) + ) + + let badSelection: SpecializationSelection = ["A": .specialized(unconstrainedResult)] + + let preflight = specializer.runtimePreflight(selection: badSelection, for: singleRequest) + #expect(!preflight.isValid, "preflight must catch Hashable mismatch on .specialized argument") + + let hasProtocolError = preflight.errors.contains { error in + if case .protocolRequirementNotSatisfied(_, let proto, _) = error { + return proto.contains("Hashable") + } + return false + } + #expect(hasProtocolError, "expected protocolRequirementNotSatisfied for Hashable") + + // And `specialize` should reject with the same diagnostic, not the + // vaguer `witnessTableNotFound` it surfaced before the fix. + do { + _ = try specializer.specialize(singleRequest, with: badSelection) + Issue.record("specialize must reject the bad .specialized argument") + } catch let error as GenericSpecializer.SpecializerError { + if case .specializationFailed(let reason) = error { + #expect(reason.contains("Hashable")) + } else { + Issue.record("expected specializationFailed, got \(error)") + } + } + } } - @Test func specialize() async throws { - let machO = MachOImage.current() + // MARK: - Invariants + // + // Tests pinning the binary-encoded ordering invariants that the + // specializer relies on. PWT slot order must match + // `compareDependentTypes` (`swift/lib/AST/GenericSignature.cpp:846`): + // GP-rooted requirements rank before nested-type-rooted ones, and + // within each block by parameter (depth, index). Diverging from that + // order routes the metadata accessor to a different cache slot or + // mis-feeds individual PWT slots. + + @Suite("Invariants") + struct Invariants: Environment { + // Associated-type PWT order with same-leaf interleaving. + // + // Regression coverage for the previously-buggy + // `GenericSpecializer.resolveAssociatedTypeWitnesses`. An earlier + // implementation returned `OrderedDictionary` + // keyed by the *leaf* metadata of each requirement chain, and + // `specialize` flattened it via `dict.values.flatMap { $0 }`. Per + // `OrderedDictionary` semantics, updating an existing key keeps + // it in its original position — so when two distinct chains + // resolved to the *same* leaf metadata but a third chain *in + // between* resolved to a different one, the flattened PWT list + // broke the binary's `compareDependentTypes` order. + // + // For the fixture below specialized with A=[Int], B=String, C=[Int]: + // A.Element = Int (M_Int) + // B.Element = Character (M_Char) + // C.Element = Int (M_Int — same as A.Element) + // + // Binary canonical order is parameter-declaration order: + // [Int_Hashable, Char_Hashable, Int_Hashable] + // + // Pre-fix flatten produced the buggy + // [Int_Hashable, Int_Hashable, Char_Hashable] + // — the slot the runtime reserved for B.Element's Hashable PWT + // got Int's Hashable PWT instead, mis-routing any associated- + // type Hashable lookup performed on the specialized type. + // `fieldOffsets()` was invariant under the re-ordering (PWT slot + // widths are uniform), so no other test caught it. Post-fix the + // function returns a flat `[ProtocolWitnessTable]` collected in + // `mergedRequirements` iteration order — which is itself the + // binary's canonical order. + @Test func associatedWitnessOrderingPreservesBinaryOrder() async throws { + // `resolveAssociatedTypeWitnesses` calls `genericContext()` (the + // in-process overload), so the descriptor must be an in-process + // pointer wrapper. + let descriptor = try inProcessStructDescriptor(named: "TestTriAssociatedSameLeafStruct") + + let specializer = GenericSpecializer(indexer: try await indexer) - let descriptor = try #require(try machO.swift.typeContextDescriptors.first { try $0.struct?.name(in: machO) == "TestGenericStruct" }?.struct) + // Bind A and C to the same array type so their A.Element / + // C.Element chains both resolve to Int's metadata; B is bound to + // String so its B.Element chain resolves to Character — distinct + // from Int. This is the exact configuration that exposes the + // pre-fix leaf-grouping bug. + let aMetadata = try Metadata.createInProcess([Int].self) + let bMetadata = try Metadata.createInProcess(String.self) + let cMetadata = try Metadata.createInProcess([Int].self) - let indexer = SwiftInterfaceIndexer(in: machO) - try indexer.addSubIndexer(SwiftInterfaceIndexer(in: #require(MachOImage(name: "Foundation")))) - try indexer.addSubIndexer(SwiftInterfaceIndexer(in: #require(MachOImage(name: "libswiftCore")))) + let resolvedWitnesses = try specializer.resolveAssociatedTypeWitnesses( + for: TypeContextDescriptorWrapper.struct(descriptor), + substituting: [ + "A": aMetadata, + "B": bMetadata, + "C": cMetadata, + ] + ) + + let intHashablePWT = try #require( + try RuntimeFunctions.conformsToProtocol( + metatype: Int.self, + protocolType: (any Hashable).self + ), + "Int must conform to Hashable in the test process" + ) + let charHashablePWT = try #require( + try RuntimeFunctions.conformsToProtocol( + metatype: Character.self, + protocolType: (any Hashable).self + ), + "Character must conform to Hashable in the test process" + ) + + // Binary requirement order — A.Element, B.Element, C.Element — + // per `compareDependentTypes`. All three share depth 0 and rank + // by parameter index, so the canonical order is exactly + // declaration order. + let expectedBinaryOrder: [ProtocolWitnessTable] = [ + intHashablePWT, // A.Element = Int + charHashablePWT, // B.Element = Character + intHashablePWT, // C.Element = Int + ] + #expect( + resolvedWitnesses == expectedBinaryOrder, + "resolveAssociatedTypeWitnesses must emit PWTs in canonical (binary) requirement order. Pre-fix leaf-keyed `OrderedDictionary` flatten produced [Int_Hashable, Int_Hashable, Char_Hashable] for this fixture, mis-feeding slot 2 (binary reserves it for B.Element: Hashable) with Int's PWT." + ) + } - try await indexer.prepare() + @Test func specializeMatchesManualBinaryOrder() async throws { + // End-to-end companion to `associatedWitnessOrderingPreservesBinaryOrder`: + // build the metadata via the API and via a hand-rolled call to + // the metadata accessor with witness tables in canonical + // (binary) order, and verify the runtime returns the same + // metadata pointer for both. Swift's generic-metadata cache keys + // on the entire `(generic args, witness tables)` tuple — feeding + // the accessor witness tables in a non-canonical order routes + // the call to a different cache slot (or, worse, populates a + // freshly allocated metadata whose internal associated-type + // witness routing is wrong). + // + // Note: `makeRequest` resolves the descriptor via the *file-context* + // `genericContext(in: machO)` overload, while + // `metadataAccessorFunction()` (no-arg) reads in-process — so we + // need a file-form descriptor for `makeRequest` and an + // in-process pointer wrapper for the manual accessor call. + let descriptor = try structDescriptor(named: "TestTriAssociatedSameLeafStruct") + let inProcessDescriptor = descriptor.asPointerWrapper(in: machO) + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) - let specializer = GenericSpecializer(indexer: indexer) - let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + let aArrayInt = [Int].self + let bString = String.self + let cArrayInt = [Int].self - let selection: SpecializationSelection = [ - "A": .metatype([Int].self), - "B": .metatype(Double.self), - "C": .metatype(Data.self), - ] + let aMetadata = try Metadata.createInProcess(aArrayInt) + let bMetadata = try Metadata.createInProcess(bString) + let cMetadata = try Metadata.createInProcess(cArrayInt) - let result = try specializer.specialize(request, with: selection) + let aSequencePWT = try #require( + try RuntimeFunctions.conformsToProtocol(metatype: aArrayInt, protocolType: (any Sequence).self), + "[Int] must conform to Sequence" + ) + let bSequencePWT = try #require( + try RuntimeFunctions.conformsToProtocol(metatype: bString, protocolType: (any Sequence).self), + "String must conform to Sequence" + ) + let cSequencePWT = try #require( + try RuntimeFunctions.conformsToProtocol(metatype: cArrayInt, protocolType: (any Sequence).self), + "[Int] must conform to Sequence" + ) + let intHashablePWT = try #require( + try RuntimeFunctions.conformsToProtocol(metatype: Int.self, protocolType: (any Hashable).self), + "Int must conform to Hashable" + ) + let charHashablePWT = try #require( + try RuntimeFunctions.conformsToProtocol(metatype: Character.self, protocolType: (any Hashable).self), + "Character must conform to Hashable" + ) - // Verify resolved arguments - #expect(result.resolvedArguments.count == 3) - #expect(result.resolvedArguments[0].parameterName == "A") - #expect(result.resolvedArguments[1].parameterName == "B") - #expect(result.resolvedArguments[2].parameterName == "C") + // Manual call: witness tables in canonical (binary) order. + // Direct-GP block: A:Sequence, B:Sequence, C:Sequence. + // Associated block: A.Element:Hashable, B.Element:Hashable, + // C.Element:Hashable. + let accessor = try #require( + try inProcessDescriptor.metadataAccessorFunction(), + "TestTriAssociatedSameLeafStruct must have a metadata accessor function" + ) + let manualResponse = try accessor( + request: .completeAndBlocking, + metadatas: [aMetadata, bMetadata, cMetadata], + witnessTables: [ + aSequencePWT, + bSequencePWT, + cSequencePWT, + intHashablePWT, // A.Element + charHashablePWT, // B.Element + intHashablePWT, // C.Element + ] + ) + let manualMetadata = try manualResponse.value.resolve().metadata - // A: Collection requires a PWT - #expect(result.resolvedArguments[0].hasWitnessTables) - // B: Equatable requires a PWT - #expect(result.resolvedArguments[1].hasWitnessTables) - // C: Hashable requires a PWT - #expect(result.resolvedArguments[2].hasWitnessTables) + // API call. + let apiResult = try specializer.specialize(request, with: [ + "A": .metatype(aArrayInt), + "B": .metatype(bString), + "C": .metatype(cArrayInt), + ]) + let apiMetadata = try apiResult.metadata() - // Verify we can resolve metadata - let metadata = try result.resolveMetadata() - let structMetadata = try #require(metadata.struct) - let fieldOffsets = try structMetadata.fieldOffsets() - #expect(fieldOffsets == [0, 8, 16]) + // The runtime's generic-metadata cache returns the same + // metadata pointer iff the `(args, PWTs)` tuple matches. + // Pre-fix `specialize` flattens its leaf-keyed dict to + // [aSequence, bSequence, cSequence, Int_H, Int_H, Char_H] + // instead of the canonical + // [aSequence, bSequence, cSequence, Int_H, Char_H, Int_H] + // — different cache key, divergent metadata pointer. + #expect( + manualMetadata == apiMetadata, + "specialize() must produce the same metadata pointer the runtime returns when invoked manually with witness tables in canonical (binary) order; divergence indicates an incorrect PWT order in the API path." + ) + } } - @Test func selectionBuilder() throws { - let selection = SpecializationSelection.builder() - .set("A", to: [Int].self) - .set("B", to: String.self) - .build() + // MARK: - Error Paths + // + // Typed-error coverage. `SpecializerError` / + // `AssociatedTypeResolutionError` carry the diagnostic messages that + // surface to callers when a specialization fails for reasons not + // catchable by the static `validate` pass — corrupt descriptors, + // missing infrastructure, key-argument count mismatches, etc. + + @Suite("Error Paths") + struct ErrorPaths: Environment { + // Bug reproduction #1: specialize doesn't self-check + // keyArgumentCount. + // + // The accessor takes `numKeyArguments` slots: `parameters.count` + // metadatas + `protocol-PWTs + assoc-PWTs`. If our parameter + // discovery or PWT collection ever miscounts (regression in + // `buildParameters` / `collectRequirements` / + // `buildAssociatedTypeRequirements`), we'd send the wrong number + // of args to the accessor and fail opaquely deep in the runtime. + // Adding a count assertion converts that silent failure into a + // typed `specializationFailed` with a clear message. + @Test func specializeRejectsMismatchedKeyArgumentCount() async throws { + let descriptor = try structDescriptor(named: "TestSingleProtocolStruct") + let specializer = GenericSpecializer(indexer: try await indexer) + let realRequest = try specializer.makeRequest( + for: TypeContextDescriptorWrapper.struct(descriptor) + ) + + // Tampered request: claims a different keyArgumentCount than + // parameters/requirements actually total. + let tamperedRequest = SpecializationRequest( + typeDescriptor: realRequest.typeDescriptor, + parameters: realRequest.parameters, + associatedTypeRequirements: realRequest.associatedTypeRequirements, + keyArgumentCount: realRequest.keyArgumentCount + 5 + ) + + do { + _ = try specializer.specialize(tamperedRequest, with: ["A": .metatype(Int.self)]) + Issue.record("specialize must reject mismatched keyArgumentCount before invoking the accessor") + } catch let error as GenericSpecializer.SpecializerError { + if case .specializationFailed(let reason) = error { + #expect( + reason.lowercased().contains("key argument") || reason.lowercased().contains("count"), + "error message should mention key argument count, got: \(reason)" + ) + } else { + Issue.record("expected specializationFailed, got \(error)") + } + } + } + + // G1: AssociatedTypeResolutionError coverage — missingIndexer. + // + // `AssociatedTypeResolutionError` carries the diagnostic + // information for every typed failure path through + // `resolveAssociatedTypeWitnesses`. Tests in this suite pin the + // most common construction triggers — a specializer built without + // an indexer, and a `substituting:` map that omits one of the + // parameters that requirement chains root into. The remaining + // cases (`missingAssociatedTypeIndex`, + // `conformingTypeDoesNotConformToProtocol`, etc.) are reachable + // only by deliberately corrupting fixture state and are deferred. + @Test func resolveAssociatedTypeWitnessesThrowsWhenIndexerIsMissing() throws { + let descriptor = try inProcessStructDescriptor(named: "TestNestedAssociatedStruct") + let specializerWithoutIndexer = GenericSpecializer( + machO: machO, + conformanceProvider: EmptyConformanceProvider() + ) + + do { + _ = try specializerWithoutIndexer.resolveAssociatedTypeWitnesses( + for: TypeContextDescriptorWrapper.struct(descriptor), + substituting: ["A": try Metadata.createInProcess([[Int]].self)] + ) + Issue.record("expected missingIndexer to throw") + } catch let error as GenericSpecializer.AssociatedTypeResolutionError { + if case .missingIndexer = error { + return + } + Issue.record("expected missingIndexer, got \(error)") + } + } + + @Test func resolveAssociatedTypeWitnessesThrowsWhenSubstitutionMissesBaseParameter() async throws { + let descriptor = try inProcessStructDescriptor(named: "TestNestedAssociatedStruct") + let specializer = GenericSpecializer(indexer: try await indexer) + + do { + _ = try specializer.resolveAssociatedTypeWitnesses( + for: TypeContextDescriptorWrapper.struct(descriptor), + substituting: [:] + ) + Issue.record("expected missingConformingTypeMetadata to throw") + } catch let error as GenericSpecializer.AssociatedTypeResolutionError { + if case .missingConformingTypeMetadata(let genericParam, _) = error { + #expect(genericParam == "A") + return + } + Issue.record("expected missingConformingTypeMetadata, got \(error)") + } + } + + // G2: candidateResolutionFailed coverage. + // + // `resolveCandidate` checks `guard let indexer` first; without + // an indexer the call must surface as a typed + // `candidateResolutionFailed` with the offending candidate in + // the payload. Exercising this path requires a request built + // with an indexer-backed specializer (so `findCandidates` can + // populate the candidate list) and a *separate* specializer + // without `indexer` for the `specialize` call. + @Test func candidatePathThrowsCandidateResolutionFailedWhenIndexerIsMissing() async throws { + let descriptor = try structDescriptor(named: "TestSingleProtocolStruct") + let indexerSpecializer = GenericSpecializer(indexer: try await indexer) + let request = try indexerSpecializer.makeRequest( + for: TypeContextDescriptorWrapper.struct(descriptor), + candidateOptions: .excludeGenerics + ) + let intCandidate = try #require( + request.parameters[0].candidates.first { + $0.typeName.currentName == "Int" && !$0.isGeneric + }, + "expected Swift.Int candidate after .excludeGenerics" + ) - #expect(selection.hasArgument(for: "A")) - #expect(selection.hasArgument(for: "B")) - #expect(!selection.hasArgument(for: "C")) - #expect(selection.selectedParameterNames.count == 2) + // The two-arg initializer leaves `indexer` nil, so the + // conformance-provider plumbing for `findCandidates` still works + // (we reuse the indexer's provider) but `resolveCandidate` finds + // no indexer and bails out cleanly. + let indexerlessSpecializer = GenericSpecializer( + machO: machO, + conformanceProvider: IndexerConformanceProvider(indexer: try await indexer) + ) + + do { + _ = try indexerlessSpecializer.specialize( + request, + with: ["A": .candidate(intCandidate)] + ) + Issue.record("expected candidateResolutionFailed when indexer is nil") + } catch let error as GenericSpecializer.SpecializerError { + if case .candidateResolutionFailed(let candidate, let reason) = error { + #expect(candidate.typeName == intCandidate.typeName) + #expect(reason.lowercased().contains("indexer")) + return + } + Issue.record("expected candidateResolutionFailed, got \(error)") + } + } + } + + // MARK: - Models + // + // Coverage of the data types around `GenericSpecializer` — + // `SpecializationSelection` (Builder + dictionary literal), + // `SpecializationResult` convenience accessors, the + // `extractAssociatedPath` parser helper, and the + // `CompositeConformanceProvider` adapter. These tests exercise the + // public API surface independent of any single specialization run. + + @Suite("Models") + struct Models: Environment { + @Test func selectionBuilderBasic() throws { + let selection = SpecializationSelection.builder() + .set("A", to: [Int].self) + .set("B", to: String.self) + .build() + + #expect(selection.hasArgument(for: "A")) + #expect(selection.hasArgument(for: "B")) + #expect(!selection.hasArgument(for: "C")) + #expect(selection.selectedParameterNames.count == 2) + } + + // G4: SpecializationSelection.Builder overload coverage. + // + // `selectionBuilderBasic` covers `set(_:to:Any.Type)`. The + // overloads for `Metadata` / `Candidate` / `SpecializationResult` + // and `remove(_:)` are public API surfaces with no other callers, + // so each gets a minimal round-trip pin: build → subscript → + // case-extract → assert. + + @Test func selectionBuilderMetadataOverloadStoresMetadataArgument() throws { + let intMetadata = try Metadata.createInProcess(Int.self) + let selection = SpecializationSelection.builder() + .set("A", to: intMetadata) + .build() + let argument = try #require(selection["A"]) + guard case .metadata(let stored) = argument else { + Issue.record("expected .metadata case, got \(argument)") + return + } + #expect(stored == intMetadata) + } + + @Test func selectionBuilderCandidateOverloadStoresCandidateArgument() async throws { + let descriptor = try structDescriptor(named: "TestSingleProtocolStruct") + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest( + for: TypeContextDescriptorWrapper.struct(descriptor), + candidateOptions: .excludeGenerics + ) + let candidate = try #require( + request.parameters[0].candidates.first { + $0.typeName.currentName == "Int" && !$0.isGeneric + }, + "expected Swift.Int candidate after .excludeGenerics" + ) + + let selection = SpecializationSelection.builder() + .set("A", to: candidate) + .build() + let argument = try #require(selection["A"]) + guard case .candidate(let stored) = argument else { + Issue.record("expected .candidate case, got \(argument)") + return + } + #expect(stored.typeName == candidate.typeName) + #expect(stored.isGeneric == candidate.isGeneric) + } + + @Test func selectionBuilderSpecializedOverloadStoresSpecializedArgument() async throws { + let descriptor = try structDescriptor(named: "TestUnconstrainedStruct") + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + let inner = try specializer.specialize(request, with: ["A": .metatype(Int.self)]) + + let selection = SpecializationSelection.builder() + .set("A", to: inner) + .build() + let argument = try #require(selection["A"]) + guard case .specialized(let stored) = argument else { + Issue.record("expected .specialized case, got \(argument)") + return + } + let storedMetadata = try stored.metadata() + let innerMetadata = try inner.metadata() + #expect(storedMetadata == innerMetadata) + } + + @Test func selectionBuilderRemoveDropsArgument() throws { + let intMetadata = try Metadata.createInProcess(Int.self) + let builder = SpecializationSelection.builder() + .set("A", to: intMetadata) + .set("B", to: String.self) + builder.remove("A") + let selection = builder.build() + #expect(!selection.hasArgument(for: "A")) + #expect(selection.hasArgument(for: "B")) + } + + // G5: SpecializationResult convenience accessor coverage. + // + // `argument(for:)` and `valueWitnessTable()` are public + // conveniences that route through `resolveMetadata()`. The smoke + // tests below pin the lookup-by-name path and the in-process VWT + // overload against a fixture whose layout we already know. There + // is intentionally no file-context VWT overload — see + // `SpecializationResult.swift` for the rationale. + + @Test func resultArgumentForLooksUpByParameterName() async throws { + let descriptor = try structDescriptor(named: "TestGenericStruct") + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + let result = try specializer.specialize(request, with: [ + "A": .metatype([Int].self), + "B": .metatype(Double.self), + "C": .metatype(Data.self), + ]) + + let argA = try #require(result.argument(for: "A")) + #expect(argA.parameterName == "A") + #expect(argA.metadata == result.resolvedArguments[0].metadata) + + #expect( + result.argument(for: "Z") == nil, + "argument(for:) should return nil for an unknown parameter name" + ) + } + + @Test func resultValueWitnessTableResolvesSizeForSimpleStruct() async throws { + let descriptor = try structDescriptor(named: "TestUnconstrainedStruct") + let specializer = GenericSpecializer(indexer: try await indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + let result = try specializer.specialize(request, with: ["A": .metatype(Int.self)]) + + // TestUnconstrainedStruct has a single Int field — 8 bytes + // on any 64-bit Apple platform. + let vwt = try result.valueWitnessTable() + #expect(vwt.layout.size == 8) + } + + // Bug reproduction #3: extractAssociatedPath returns nil for a + // bare ProtocolSymbolicReference. + // + // The Swift demangler in `popAssocTypeName` + // (`swift/lib/Demangling/Demangler.cpp:2832-2845`) accepts three + // protocol-shaped nodes for the second child of + // `DependentAssociatedTypeRef`: + // - `.type` (when the symbolic-ref resolver succeeded and + // returned a wrapped tree) + // - `.protocolSymbolicReference` (resolver returned nil) + // - `.objectiveCProtocolSymbolicReference` (resolver returned nil) + // + // The pre-fix `extractAssociatedPath` only accepted `.type`. The + // other two — which arise when MetadataReader's resolver fails — + // fell through to a `nil` return, which then became a silent + // `continue` in `buildAssociatedTypeRequirements` or a typed + // `unknownParamNodeStructure` error in + // `resolveAssociatedTypeWitnesses`. + @Test func extractAssociatedPathHandlesBareProtocolSymbolicReference() throws { + // Build the LHS for a hypothetical `A.Element: …` requirement + // whose protocol child failed to resolve symbolically. The tree + // mirrors what the demangler emits when the resolver returns nil: + // .type + // .dependentMemberType + // .type → .dependentGenericParamType (depth=0, index=0) + // .dependentAssociatedTypeRef + // .identifier "Element" + // .protocolSymbolicReference 42 ← bare, NOT in .type + let bareSymbolicRef = Node.create(kind: .protocolSymbolicReference, index: 42) + let nameNode = Node.create(kind: .identifier, text: "Element") + let assocRef = Node.create(kind: .dependentAssociatedTypeRef, children: [ + nameNode, + bareSymbolicRef, + ]) + let baseDepth = Node.create(kind: .index, index: 0) + let baseIndex = Node.create(kind: .index, index: 0) + let baseGP = Node.create(kind: .dependentGenericParamType, children: [baseDepth, baseIndex]) + let dependent = Node.create(kind: .dependentMemberType, children: [ + Node.create(kind: .type, children: [baseGP]), + assocRef, + ]) + let outer = Node.create(kind: .type, children: [dependent]) + + let path = GenericSpecializer.extractAssociatedPath(of: outer) + + // Goal of the fix: even when the protocol child is a bare + // symbolic ref, the path structure is still recoverable + // (baseParamName + step name). Downstream resolution may still + // fail at the protocol-descriptor lookup, but the parsing layer + // shouldn't punt to nil. + let recovered = try #require( + path, + "extractAssociatedPath must recover the parameter/name pair even when the protocol child is a bare ProtocolSymbolicReference; demangler legitimately emits this when its resolver returns nil" + ) + #expect(recovered.baseParamName == "A") + #expect(recovered.steps.map(\.name) == ["Element"]) + } + + // S5: extractAssociatedPath ObjC symbolic-ref coverage. + // + // `extractAssociatedPath` accepts three protocol-shaped node + // kinds for the second child of `DependentAssociatedTypeRef`: + // `.type`, `.protocolSymbolicReference`, and + // `.objectiveCProtocolSymbolicReference`. The Swift case is + // pinned by the test above; this test covers the symmetric ObjC + // fallback path. + @Test func extractAssociatedPathHandlesBareObjCProtocolSymbolicReference() throws { + let bareObjCSymbolicRef = Node.create(kind: .objectiveCProtocolSymbolicReference, index: 99) + let nameNode = Node.create(kind: .identifier, text: "Element") + let assocRef = Node.create(kind: .dependentAssociatedTypeRef, children: [ + nameNode, + bareObjCSymbolicRef, + ]) + let baseDepth = Node.create(kind: .index, index: 0) + let baseIndex = Node.create(kind: .index, index: 0) + let baseGP = Node.create(kind: .dependentGenericParamType, children: [baseDepth, baseIndex]) + let dependent = Node.create(kind: .dependentMemberType, children: [ + Node.create(kind: .type, children: [baseGP]), + assocRef, + ]) + let outer = Node.create(kind: .type, children: [dependent]) + + let path = GenericSpecializer.extractAssociatedPath(of: outer) + let recovered = try #require( + path, + "extractAssociatedPath must accept .objectiveCProtocolSymbolicReference identically to .protocolSymbolicReference" + ) + #expect(recovered.baseParamName == "A") + #expect(recovered.steps.map(\.name) == ["Element"]) + } + + // S1: CompositeConformanceProvider coverage. + // + // Two pin tests for the dedupe / first-hit semantics declared in + // `CompositeConformanceProvider`'s implementation: + // - Composing `[Empty, Real]` must behave identically to + // `Real` alone (empty contributes nothing). + // - Composing `[Real, Real]` must dedupe across providers — + // the `seen.insert(...)` guards in `types(conformingTo:)`, + // `conformances(of:)`, and `allTypeNames` exist precisely + // so callers can stack providers without producing duplicate + // entries when they overlap. + + @Test func compositeConformanceProviderEmptyPlusRealActsLikeReal() async throws { + let real = IndexerConformanceProvider(indexer: try await indexer) + let composite = CompositeConformanceProvider(providers: [ + EmptyConformanceProvider(), + real, + ]) + + #expect(composite.allTypeNames.count == real.allTypeNames.count) + + let sampleType = try #require(real.allTypeNames.first) + // First-hit semantics: empty has no entry, real does, so the + // composite must return real's value for `typeDefinition` and + // `imagePath`. + #expect(composite.typeDefinition(for: sampleType) != nil) + #expect(composite.imagePath(for: sampleType) == real.imagePath(for: sampleType)) + } + + @Test func compositeConformanceProviderDedupsAcrossDuplicateProviders() async throws { + let single = IndexerConformanceProvider(indexer: try await indexer) + let composite = CompositeConformanceProvider(providers: [single, single]) + + // Same provider twice must collapse to one provider's worth of + // results — verifies the `seen` set on every list-returning + // method, not just `allTypeNames`. + #expect(composite.allTypeNames.count == single.allTypeNames.count) + + let sampleType = try #require( + single.allTypeNames.first { !single.conformances(of: $0).isEmpty }, + "expected at least one type with at least one conformance in the indexer" + ) + let sampleProto = try #require( + single.conformances(of: sampleType).first, + "expected at least one conformance for the sample type" + ) + + #expect(composite.types(conformingTo: sampleProto).count + == single.types(conformingTo: sampleProto).count, + "types(conformingTo:) must dedupe across providers") + #expect(composite.conformances(of: sampleType).count + == single.conformances(of: sampleType).count, + "conformances(of:) must dedupe across providers") + #expect(composite.doesType(sampleType, conformTo: sampleProto), + "doesType returns true if any provider says yes") + } + } +} + +// MARK: - Default helpers for nested suites + +extension GenericSpecializationTests.Environment { + /// Sync access to the cached `MachOImage.current()` — every nested + /// suite shares the same instance. + var machO: MachOImage { + GenericSpecializationTests.sharedMachO + } + + /// Async access to the prepared indexer. The actor cache builds it once + /// per process and hands every test the same reference, so the awaiter + /// pays the preparation cost zero times after the first hit. + var indexer: SwiftInterfaceIndexer { + get async throws { + try await GenericSpecializationTests.SharedIndexerCache.shared.indexer() + } + } + + /// Resolves the first struct context descriptor whose name contains + /// `nameContains`. The fixtures live as nested types on the outer suite, + /// so a substring match against the mangled name is sufficient and + /// avoids pinning each test to the full module-qualified form. + func structDescriptor(named nameContains: String) throws -> StructDescriptor { + try #require( + try machO.swift.typeContextDescriptors.first { + try $0.struct?.name(in: machO).contains(nameContains) == true + }?.struct, + "expected a struct context descriptor whose name contains \"\(nameContains)\"" + ) + } + + /// Resolves a struct descriptor and binds it to the in-process reader via + /// `asPointerWrapper(in:)`. Required for callers that invoke the + /// no-argument overloads of descriptor methods (e.g. `genericContext()`), + /// which read through the descriptor's embedded reader rather than an + /// explicit `MachOImage` argument. + func inProcessStructDescriptor(named nameContains: String) throws -> StructDescriptor { + try structDescriptor(named: nameContains).asPointerWrapper(in: machO) + } + + /// Resolves the first enum context descriptor whose name contains + /// `nameContains`. Mirrors `structDescriptor(named:)` for enum fixtures. + func enumDescriptor(named nameContains: String) throws -> EnumDescriptor { + try #require( + try machO.swift.typeContextDescriptors.first { + try $0.enum?.name(in: machO).contains(nameContains) == true + }?.enum, + "expected an enum context descriptor whose name contains \"\(nameContains)\"" + ) + } + + /// Resolves the first class context descriptor whose name contains + /// `nameContains`. Mirrors `structDescriptor(named:)` for class fixtures. + func classDescriptor(named nameContains: String) throws -> ClassDescriptor { + try #require( + try machO.swift.typeContextDescriptors.first { + try $0.class?.name(in: machO).contains(nameContains) == true + }?.class, + "expected a class context descriptor whose name contains \"\(nameContains)\"" + ) + } + + /// Resolves the descriptor along with its generic context. Used by + /// tests that inspect the generic header (e.g. `numKeyArguments`) in + /// addition to driving `GenericSpecializer`. + func genericStructFixture( + named nameContains: String + ) throws -> (descriptor: StructDescriptor, genericContext: GenericContext) { + let descriptor = try structDescriptor(named: nameContains) + let genericContext = try #require( + try descriptor.genericContext(in: machO), + "expected genericContext on \(nameContains)" + ) + return (descriptor, genericContext) } } + +// MARK: - Conditional Copyable / Escapable extensions + +extension GenericSpecializationTests.TestInvertedCopyableStruct: Copyable where A: Copyable {} + +extension GenericSpecializationTests.NestedInvertedOuter: Copyable where A: Copyable {} +extension GenericSpecializationTests.NestedInvertedOuter.NestedInvertedMiddle: Copyable where A: Copyable, B: Copyable {} +extension GenericSpecializationTests.NestedInvertedOuter.NestedInvertedMiddle.NestedInvertedInner: Copyable where A: Copyable, B: Copyable, C: Copyable {} + +extension GenericSpecializationTests.TestInvertedEscapableEnum: Escapable where A: Escapable {} + +// Note: `TestInvertedDualEnum` deliberately ships *without* conditional +// `Copyable` / `Escapable` extensions. The fixture's purpose is to +// expose the *regular* invertible-protocol record on its single GP +// (the binary's `` declaration), which is +// what `Parameter.invertibleProtocols` reads. Adding the conditional +// extensions back in produces malformed descriptors under the current +// toolchain — the iteration over `typeContextDescriptors` throws when +// it tries to read one of them. If/when that toolchain bug is fixed, +// the conditional extensions can be reinstated to also exercise the +// merged-requirement path through `conditionalInvertibleProtocolsRequirements`. diff --git a/Tests/SwiftInterfaceTests/Snapshots/__Snapshots__/SymbolTestsCoreInterfaceSnapshotTests/interfaceSnapshot.1.txt b/Tests/SwiftInterfaceTests/Snapshots/__Snapshots__/SymbolTestsCoreInterfaceSnapshotTests/interfaceSnapshot.1.txt index 505acacf..8d11710f 100644 --- a/Tests/SwiftInterfaceTests/Snapshots/__Snapshots__/SymbolTestsCoreInterfaceSnapshotTests/interfaceSnapshot.1.txt +++ b/Tests/SwiftInterfaceTests/Snapshots/__Snapshots__/SymbolTestsCoreInterfaceSnapshotTests/interfaceSnapshot.1.txt @@ -1274,6 +1274,17 @@ enum NestedGenerics { init(elements: [A]) } + struct NestedGenericThreeLevelConstraintTest where A: Swift.Hashable { + struct MiddleConstrainedTest where A1: Swift.Equatable { + struct InnerMostConstrainedTest where A2: Swift.Comparable { + var outer: A + var middle: A1 + var innerMost: A2 + + init(outer: A, middle: A1, innerMost: A2) + } + } + } } enum Noncopyable { struct NoncopyableTest: ~Swift.Copyable { diff --git a/docs/superpowers/plans/2026-05-02-generic-specializer-cleanup.md b/docs/superpowers/plans/2026-05-02-generic-specializer-cleanup.md new file mode 100644 index 00000000..691fb858 --- /dev/null +++ b/docs/superpowers/plans/2026-05-02-generic-specializer-cleanup.md @@ -0,0 +1,894 @@ +# GenericSpecializer Cleanup Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Apply six lightweight cleanups (#5, #6, #7, #9, #10, #12) to `GenericSpecializer` per spec `docs/superpowers/specs/2026-05-02-generic-specializer-cleanup-design.md`, expanding the public API to surface `~Copyable` / `~Escapable`, merging conditional invertible requirements, splitting muddy switch arms, fail-fast on generic candidates, allowing caller-supplied `MetadataRequest`, and removing dead code. + +**Architecture:** All changes are localised to two existing files under `Sources/SwiftInterface/GenericSpecializer/` plus the test file. No new files, no new modules, no ABI-level changes. + +**Tech Stack:** Swift 6.2+, swift-testing (`@Test` / `#expect` / `#require`), SwiftPM (`swift build` / `swift test`). + +--- + +## File Structure + +**Modified:** + +- `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift` — most edits live here: the `buildRequirement` switch, `buildParameters` second pass for invertible protocols, `findCandidates` populating `isGeneric`, `resolveCandidate` fail-fast, `specialize` signature, three call sites that read requirements, deletion of `convertLayoutKind`, two new `SpecializerError` cases. +- `Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift` — `Parameter` gains `invertibleProtocols`, `Candidate` gains `isGeneric`. +- `Tests/SwiftInterfaceTests/GenericSpecializationTests.swift` — three new `@Test` methods and one new fixture. + +**Not modified:** + +- `Sources/SwiftInterface/GenericSpecializer/ConformanceProvider.swift` — no API surface change here. +- `Sources/SwiftInterface/GenericSpecializer/Models/SpecializationSelection.swift`, + `SpecializationResult.swift`, `SpecializationValidation.swift` — untouched. + +Six tasks ordered by risk and dependency: pure deletions and comment splits first, then API additions, then the test-bearing changes that build on the API. + +--- + +## Task 1: Remove dead `convertLayoutKind` (#12) + +**Files:** + +- Modify: `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift:272-277` + +- [ ] **Step 1: Confirm zero callers** + +Run: `rg -n 'convertLayoutKind' Sources/ Tests/` +Expected output: only the definition lines (272-277) — no call sites. + +- [ ] **Step 2: Delete the function** + +Remove these exact lines from `GenericSpecializer.swift`: + +```swift + /// Convert runtime layout kind to our model + private func convertLayoutKind(_ kind: GenericRequirementLayoutKind) -> SpecializationRequest.LayoutKind { + switch kind { + case .class: + return .class + } + } +``` + +- [ ] **Step 3: Build and run the specializer test class** + +Run: `swift build 2>&1 | xcsift` +Expected: build succeeds, no warnings. + +Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests 2>&1 | xcsift` +Expected: all 11 existing tests pass (`main`, `makeRequest`, `validation`, `specialize`, `selectionBuilder`, `unconstrainedSpecialize`, `singleProtocolSpecialize`, `multiProtocolSpecialize`, `classConstraintSpecialize`, `nestedAssociatedTypeRequest`, `nestedAssociatedTypeSpecialize`, `dualAssociatedSpecialize`, `mixedConstraintsSpecialize`). + +- [ ] **Step 4: Commit** + +```bash +git add Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift +git commit -m "refactor(SwiftInterface): drop unused convertLayoutKind helper" +``` + +--- + +## Task 2: Split `sameConformance` / `sameShape` / `invertedProtocols` arms (#7) + +**Files:** + +- Modify: `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift:265-268` + +- [ ] **Step 1: Replace the combined branch** + +Replace this block in `buildRequirement`: + +```swift + case .sameConformance, .sameShape, .invertedProtocols: + // These are more advanced requirements that we don't need for basic specialization + return nil + } +``` + +with three independent arms: + +```swift + case .sameConformance: + // Derived from SameType / BaseClass; compiler forces hasKeyArgument=false, + // so it never participates in metadata accessor key arguments. + return nil + + case .sameShape: + // Pack-shape constraint between two TypePacks. Relevant only to variadic + // generics, which are out of scope for this specializer. + return nil + + case .invertedProtocols: + // Capability declaration (~Copyable / ~Escapable) — surfaced on + // Parameter.invertibleProtocols rather than as a Requirement, because + // it relaxes rather than constrains the parameter. + return nil + } +``` + +- [ ] **Step 2: Build and run tests** + +Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests 2>&1 | xcsift` +Expected: all existing tests pass — this is a comment-only change. + +- [ ] **Step 3: Commit** + +```bash +git add Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift +git commit -m "refactor(SwiftInterface): split combined nil-return requirement branch" +``` + +--- + +## Task 3: Configurable `MetadataRequest` on `specialize` (#10) + +**Files:** + +- Modify: `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift:391` (signature) and `:458` (call site). +- Test: `Tests/SwiftInterfaceTests/GenericSpecializationTests.swift` + +- [ ] **Step 1: Write the failing test** + +Append to `GenericSpecializationTests.swift`, after the `selectionBuilder` test (around line 191): + +```swift + @Test func configurableMetadataRequest() async throws { + let machO = MachOImage.current() + + let descriptor = try #require(try machO.swift.typeContextDescriptors.first { + try $0.struct?.name(in: machO).contains("TestGenericStruct") == true + }?.struct) + + let indexer = SwiftInterfaceIndexer(in: machO) + try indexer.addSubIndexer(SwiftInterfaceIndexer(in: #require(MachOImage(name: "Foundation")))) + try indexer.addSubIndexer(SwiftInterfaceIndexer(in: #require(MachOImage(name: "libswiftCore")))) + try await indexer.prepare() + + let specializer = GenericSpecializer(indexer: indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + let selection: SpecializationSelection = [ + "A": .metatype([Int].self), + "B": .metatype(Double.self), + "C": .metatype(Data.self), + ] + + // Default request (existing behaviour) + let defaultResult = try specializer.specialize(request, with: selection) + let defaultOffsets = try #require(defaultResult.resolveMetadata().struct).fieldOffsets() + + // Explicit non-blocking complete request + let nonBlocking = MetadataRequest(state: .complete, isBlocking: false) + let explicitResult = try specializer.specialize( + request, + with: selection, + metadataRequest: nonBlocking + ) + let explicitOffsets = try #require(explicitResult.resolveMetadata().struct).fieldOffsets() + + #expect(defaultOffsets == [0, 8, 16]) + #expect(explicitOffsets == defaultOffsets) + } +``` + +- [ ] **Step 2: Run the test, expect a build failure** + +Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests/configurableMetadataRequest 2>&1 | xcsift` +Expected: compile error along the lines of *"extra argument 'metadataRequest' in call"* — the parameter does not exist yet. + +- [ ] **Step 3: Add the parameter to `specialize`** + +In `GenericSpecializer.swift:391` change the signature: + +```swift + public func specialize( + _ request: SpecializationRequest, + with selection: SpecializationSelection, + metadataRequest: MetadataRequest = .completeAndBlocking + ) throws -> SpecializationResult { +``` + +In the same function, find the main accessor call (currently `GenericSpecializer.swift:457-461`): + +```swift + let response = try accessorFunction( + request: .completeAndBlocking, + metadatas: metadatas, + witnessTables: witnessTables, + ) +``` + +and replace `request: .completeAndBlocking` with `request: metadataRequest`: + +```swift + let response = try accessorFunction( + request: metadataRequest, + metadatas: metadatas, + witnessTables: witnessTables, + ) +``` + +Leave `resolveCandidate` (around line 516) and `resolveAssociatedTypeStep` (around line 715) untouched — per spec §5 they keep their current internal requests. + +- [ ] **Step 4: Run the new test** + +Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests/configurableMetadataRequest 2>&1 | xcsift` +Expected: PASS. + +- [ ] **Step 5: Run the full specializer test class to confirm no regressions** + +Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests 2>&1 | xcsift` +Expected: all tests pass, including the new one. + +- [ ] **Step 6: Commit** + +```bash +git add Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift Tests/SwiftInterfaceTests/GenericSpecializationTests.swift +git commit -m "feat(SwiftInterface): allow caller-supplied MetadataRequest in specialize" +``` + +--- + +## Task 4: Merge conditional invertible protocol requirements (#6) + +**Files:** + +- Modify: `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift` — three call sites (lines 94, 285, 593). + +- [ ] **Step 1: Add the merge helper** + +Insert this private static helper inside `GenericSpecializer` (anywhere in the `extension GenericSpecializer` block that contains `buildParameters`; placing it right above `buildParameters` is clearest): + +```swift + /// All requirements visible to the specializer: the cumulative + /// `allRequirements` chain plus any conditional requirements stored + /// under `hasConditionalInvertedProtocols`. The current scope keeps + /// every candidate Copyable / Escapable, so conditional requirements + /// always evaluate active and can be merged unconditionally. + private static func mergedRequirements( + from genericContext: GenericContext + ) -> [GenericRequirementDescriptor] { + genericContext.allRequirements.flatMap { $0 } + + genericContext.conditionalInvertibleProtocolsRequirements + } +``` + +- [ ] **Step 2: Use the helper in `buildParameters`** + +In `GenericSpecializer.swift:91-97` replace: + +```swift + let requirements = try collectRequirements( + for: paramName, + from: genericContext.allRequirements.flatMap { $0 }, + parameterIndex: index, + depth: depth + ) +``` + +with: + +```swift + let requirements = try collectRequirements( + for: paramName, + from: Self.mergedRequirements(from: genericContext), + parameterIndex: index, + depth: depth + ) +``` + +- [ ] **Step 3: Use the helper in `buildAssociatedTypeRequirements`** + +In `GenericSpecializer.swift:285` replace: + +```swift + let genericRequirements = genericContext.allRequirements.flatMap { $0 } +``` + +with: + +```swift + let genericRequirements = Self.mergedRequirements(from: genericContext) +``` + +- [ ] **Step 4: Use the helper in `resolveAssociatedTypeWitnesses`** + +The third call site lives on the `MachO == MachOImage` extension (around `GenericSpecializer.swift:593`) and works with `GenericContext` already loaded into the current process via `genericContextInProcess`. Both this and the file-side `GenericContext` are the same type alias `TargetGenericContext`, so the helper applies directly. + +Replace this line: + +```swift + let requirements = try genericContextInProcess.requirements.map { try GenericRequirement(descriptor: $0) } +``` + +with: + +```swift + let requirements = try Self.mergedRequirements(from: genericContextInProcess) + .map { try GenericRequirement(descriptor: $0) } +``` + +(`mergedRequirements` is defined on `extension GenericSpecializer` without a `MachO == MachOImage` constraint, so it's reachable from both extension blocks.) + +- [ ] **Step 5: Build and run all specializer tests** + +Run: `swift build 2>&1 | xcsift` +Expected: build succeeds, no warnings. + +Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests 2>&1 | xcsift` +Expected: all existing tests still pass. None of the existing fixtures rely on `conditionalInvertibleProtocolsRequirements`, so this change is behaviour-neutral against the current suite. Direct verification is added by Task 6's fixture. + +- [ ] **Step 6: Commit** + +```bash +git add Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift +git commit -m "feat(SwiftInterface): merge conditional invertible requirements" +``` + +--- + +## Task 5: Generic-candidate fail-fast (#9) + +**Files:** + +- Modify: `Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift` — `Candidate` gains `isGeneric`. +- Modify: `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift` — new `SpecializerError` case, `findCandidates` populates `isGeneric`, `resolveCandidate` throws on generic descriptors. +- Test: `Tests/SwiftInterfaceTests/GenericSpecializationTests.swift` + +- [ ] **Step 1: Write the failing test** + +Append after the `configurableMetadataRequest` test: + +```swift + @Test func genericCandidateFailFast() async throws { + let machO = MachOImage.current() + + let descriptor = try #require(try machO.swift.typeContextDescriptors.first { + try $0.struct?.name(in: machO).contains("TestSingleProtocolStruct") == true + }?.struct) + + let indexer = SwiftInterfaceIndexer(in: machO) + try indexer.addSubIndexer(SwiftInterfaceIndexer(in: #require(MachOImage(name: "libswiftCore")))) + try await indexer.prepare() + + let specializer = GenericSpecializer(indexer: indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + // Pick a generic candidate from the candidate list (e.g. Optional or Array + // — anything generic that conforms to Hashable). We deliberately do not + // assert that *some* candidate is generic in case the candidate set + // changes; we only assert the property holds for any generic ones we find. + let genericCandidate = request.parameters[0].candidates.first { $0.isGeneric } + let nonGenericCandidate = request.parameters[0].candidates.first { !$0.isGeneric } + + // At minimum the standard library exposes both shapes for Hashable. + try #require(genericCandidate != nil, "expected at least one generic candidate") + try #require(nonGenericCandidate != nil, "expected at least one non-generic candidate") + + // Non-generic candidate still resolves successfully. + let okResult = try specializer.specialize( + request, + with: ["A": .candidate(nonGenericCandidate!)] + ) + _ = try okResult.resolveMetadata() + + // Generic candidate throws the new typed error. + do { + _ = try specializer.specialize( + request, + with: ["A": .candidate(genericCandidate!)] + ) + Issue.record("expected candidateRequiresNestedSpecialization to be thrown") + } catch let GenericSpecializer.SpecializerError.candidateRequiresNestedSpecialization(candidate, parameterCount) { + #expect(candidate.typeName == genericCandidate!.typeName) + #expect(parameterCount >= 1) + } + } +``` + +- [ ] **Step 2: Run the test, expect a build failure** + +Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests/genericCandidateFailFast 2>&1 | xcsift` +Expected: compile errors — `isGeneric` is not a member of `Candidate`, `candidateRequiresNestedSpecialization` is not a `SpecializerError` case. + +- [ ] **Step 3: Add `isGeneric` to `Candidate`** + +In `Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift`, replace the existing `Candidate` declaration (currently lines 122-143): + +```swift + /// A candidate type that can be used for specialization + public struct Candidate: Sendable, Hashable { + /// Type name + public let typeName: TypeName + + /// Source of this candidate + public let source: Source + + public init( + typeName: TypeName, + source: Source, + ) { + self.typeName = typeName + self.source = source + } + + /// Source of candidate type + public enum Source: Sendable, Hashable { + case image(String) + } + } +``` + +with: + +```swift + /// A candidate type that can be used for specialization + public struct Candidate: Sendable, Hashable { + /// Type name + public let typeName: TypeName + + /// Source of this candidate + public let source: Source + + /// True when the candidate's type descriptor is itself generic. + /// Selecting such a candidate via `Argument.candidate(...)` will + /// throw `candidateRequiresNestedSpecialization` from `specialize`. + public let isGeneric: Bool + + public init( + typeName: TypeName, + source: Source, + isGeneric: Bool = false + ) { + self.typeName = typeName + self.source = source + self.isGeneric = isGeneric + } + + /// Source of candidate type + public enum Source: Sendable, Hashable { + case image(String) + } + } +``` + +(Default value `false` keeps the existing call sites in `findCandidates` source-compatible until they are updated in Step 5.) + +- [ ] **Step 4: Add the `SpecializerError` case** + +In `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift`, find the `SpecializerError` enum (currently `:800-808`). Add the new case after `case candidateResolutionFailed(...)`: + +```swift + case candidateRequiresNestedSpecialization( + candidate: SpecializationRequest.Candidate, + parameterCount: Int + ) +``` + +Add a matching `errorDescription` arm in the `switch self` block (currently `:810-829`), after the existing `candidateResolutionFailed` arm: + +```swift + case .candidateRequiresNestedSpecialization(let candidate, let parameterCount): + return "Candidate \(candidate.typeName.name) is generic with \(parameterCount) parameter(s); pass Argument.specialized(...) instead of Argument.candidate(...)" +``` + +- [ ] **Step 5: Populate `isGeneric` in `findCandidates`** + +In `GenericSpecializer.swift:311-339`, both branches currently use `guard ... != nil` and discard the type definition. Bind it instead so we can read the descriptor's flags. + +Replace the entire `findCandidates` body: + +```swift + private func findCandidates(satisfying protocols: [ProtocolName]) -> [SpecializationRequest.Candidate] { + guard !protocols.isEmpty else { + // No constraints - return all indexed types + return conformanceProvider.allTypeNames.compactMap { typeName -> SpecializationRequest.Candidate? in + guard conformanceProvider.typeDefinition(for: typeName) != nil else { + return nil + } + let imagePath = conformanceProvider.imagePath(for: typeName) ?? "" + return SpecializationRequest.Candidate( + typeName: typeName, + source: .image(imagePath) + ) + } + } + + // Find types conforming to all protocols + let conformingTypes = conformanceProvider.types(conformingToAll: protocols) + + return conformingTypes.compactMap { typeName -> SpecializationRequest.Candidate? in + guard conformanceProvider.typeDefinition(for: typeName) != nil else { + return nil + } + let imagePath = conformanceProvider.imagePath(for: typeName) ?? "" + return SpecializationRequest.Candidate( + typeName: typeName, + source: .image(imagePath) + ) + } + } +``` + +with: + +```swift + private func findCandidates(satisfying protocols: [ProtocolName]) -> [SpecializationRequest.Candidate] { + guard !protocols.isEmpty else { + // No constraints - return all indexed types + return conformanceProvider.allTypeNames.compactMap { typeName -> SpecializationRequest.Candidate? in + guard let typeDefinition = conformanceProvider.typeDefinition(for: typeName) else { + return nil + } + let imagePath = conformanceProvider.imagePath(for: typeName) ?? "" + let isGeneric = typeDefinition.type.typeContextDescriptorWrapper.typeContextDescriptor.layout.flags.isGeneric + return SpecializationRequest.Candidate( + typeName: typeName, + source: .image(imagePath), + isGeneric: isGeneric + ) + } + } + + // Find types conforming to all protocols + let conformingTypes = conformanceProvider.types(conformingToAll: protocols) + + return conformingTypes.compactMap { typeName -> SpecializationRequest.Candidate? in + guard let typeDefinition = conformanceProvider.typeDefinition(for: typeName) else { + return nil + } + let imagePath = conformanceProvider.imagePath(for: typeName) ?? "" + let isGeneric = typeDefinition.type.typeContextDescriptorWrapper.typeContextDescriptor.layout.flags.isGeneric + return SpecializationRequest.Candidate( + typeName: typeName, + source: .image(imagePath), + isGeneric: isGeneric + ) + } + } +``` + +- [ ] **Step 6: Make `resolveCandidate` fail fast on generic candidates** + +In `GenericSpecializer.swift:487-519`, replace: + +```swift + private func resolveCandidate(_ candidate: SpecializationRequest.Candidate, parameterName: String) throws -> Metadata { + // Find the type definition from indexer + guard let indexer else { + throw SpecializerError.candidateResolutionFailed( + candidate: candidate, + reason: "Indexer not available for candidate resolution" + ) + } + + // Look up type definition + guard let typeDefinitionEntry = indexer.allAllTypeDefinitions[candidate.typeName] else { + throw SpecializerError.candidateResolutionFailed( + candidate: candidate, + reason: "Type not found in indexer" + ) + } + + let typeDefinition = typeDefinitionEntry.value + + // Get accessor function from type definition's type context + let accessorFunction = try typeDefinition.type.typeContextDescriptorWrapper.typeContextDescriptor.metadataAccessorFunction(in: typeDefinitionEntry.machO) + guard let accessorFunction else { + throw SpecializerError.candidateResolutionFailed( + candidate: candidate, + reason: "Cannot get metadata accessor function" + ) + } + + // For non-generic types, just call the accessor + let response = try accessorFunction(request: .completeAndBlocking) + let wrapper = try response.value.resolve() + return try wrapper.metadata + } +``` + +with: + +```swift + private func resolveCandidate(_ candidate: SpecializationRequest.Candidate, parameterName: String) throws -> Metadata { + // Find the type definition from indexer + guard let indexer else { + throw SpecializerError.candidateResolutionFailed( + candidate: candidate, + reason: "Indexer not available for candidate resolution" + ) + } + + // Look up type definition + guard let typeDefinitionEntry = indexer.allAllTypeDefinitions[candidate.typeName] else { + throw SpecializerError.candidateResolutionFailed( + candidate: candidate, + reason: "Type not found in indexer" + ) + } + + let typeDefinition = typeDefinitionEntry.value + let typeContext = typeDefinition.type.typeContextDescriptorWrapper.typeContextDescriptor + + // Generic candidates need nested specialization; surface a typed error + // rather than letting the no-argument accessor call below fail with + // a generic message. + if let genericContext = try typeContext.genericContext(in: typeDefinitionEntry.machO) { + throw SpecializerError.candidateRequiresNestedSpecialization( + candidate: candidate, + parameterCount: Int(genericContext.header.numParams) + ) + } + + // Get accessor function from type definition's type context + let accessorFunction = try typeContext.metadataAccessorFunction(in: typeDefinitionEntry.machO) + guard let accessorFunction else { + throw SpecializerError.candidateResolutionFailed( + candidate: candidate, + reason: "Cannot get metadata accessor function" + ) + } + + // Non-generic: call accessor with no arguments + let response = try accessorFunction(request: .completeAndBlocking) + let wrapper = try response.value.resolve() + return try wrapper.metadata + } +``` + +- [ ] **Step 7: Run the new test** + +Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests/genericCandidateFailFast 2>&1 | xcsift` +Expected: PASS. + +- [ ] **Step 8: Run the full specializer test class** + +Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests 2>&1 | xcsift` +Expected: all existing tests pass, including the new one. The default `isGeneric: Bool = false` parameter on `Candidate.init` keeps any existing positional call sites compiling unchanged. + +- [ ] **Step 9: Commit** + +```bash +git add Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift Tests/SwiftInterfaceTests/GenericSpecializationTests.swift +git commit -m "feat(SwiftInterface): fail fast on generic candidates with typed error" +``` + +--- + +## Task 6: Surface `invertibleProtocols` on `Parameter` (#5) + +**Files:** + +- Modify: `Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift` — `Parameter` gains `invertibleProtocols`. +- Modify: `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift` — `buildParameters` second pass that fills the field. +- Test: `Tests/SwiftInterfaceTests/GenericSpecializationTests.swift` — new fixture and test. + +- [ ] **Step 1: Add the test fixture and failing test** + +The fixture and `@Test` method are added inside the existing `GenericSpecializationTests` class (a `final class` is itself Copyable, so nesting a `~Copyable` struct inside it is fine). The conditional `Copyable` conformance is declared in a top-level extension because conditional conformance can only be declared at file scope. + +Append inside `GenericSpecializationTests` (after the `mixedConstraintsSpecialize` test, just before the closing `}` of the class): + +```swift + // MARK: - Inverted protocols (~Copyable) + + struct TestInvertedCopyableStruct: ~Copyable { + let a: A + } + + @Test func invertedProtocolsExposed() async throws { + let machO = MachOImage.current() + + let descriptor = try #require(try machO.swift.typeContextDescriptors.first { + try $0.struct?.name(in: machO).contains("TestInvertedCopyableStruct") == true + }?.struct) + + let indexer = SwiftInterfaceIndexer(in: machO) + try await indexer.prepare() + + let specializer = GenericSpecializer(indexer: indexer) + let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) + + #expect(request.parameters.count == 1) + + let invertible = try #require(request.parameters[0].invertibleProtocols) + // ~Copyable means the .copyable bit is not set in the surfaced set. + #expect(!invertible.contains(.copyable)) + + // Specialize with a Copyable type (Int) — the conditional Copyable + // extension makes the struct itself Copyable when A is Copyable, so + // the metadata accessor should succeed. + let result = try specializer.specialize(request, with: ["A": .metatype(Int.self)]) + let structMetadata = try #require(result.resolveMetadata().struct) + #expect(try structMetadata.fieldOffsets() == [0]) + } +``` + +Then append at file scope (after the closing `}` of the class): + +```swift +extension GenericSpecializationTests.TestInvertedCopyableStruct: Copyable where A: Copyable {} +``` + +- [ ] **Step 2: Run the test, expect a build failure** + +Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests/invertedProtocolsExposed 2>&1 | xcsift` +Expected: compile error — `invertibleProtocols` is not a member of `Parameter`. + +- [ ] **Step 3: Add `invertibleProtocols` to `Parameter`** + +In `Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift`, modify the `Parameter` struct (currently lines 36-78). The new field plus an updated initialiser: + +```swift + public struct Parameter: Sendable { + /// Parameter name (e.g., "A", "B", "A1" - based on depth and index) + public let name: String + + /// Parameter index in generic signature + public let index: Int + + /// Depth level (for nested generic contexts) + public let depth: Int + + /// Requirements on this parameter (ordered - PWT passed in this order) + public let requirements: [Requirement] + + /// Candidate types that satisfy all requirements + public var candidates: [Candidate] + + /// Invertible protocols (~Copyable / ~Escapable) that the parameter + /// declares. The set carries the bits that ARE present — e.g. + /// `` produces a set without `.copyable`. `nil` means + /// the parameter has no `invertedProtocols` requirement and retains + /// every invertible protocol by default (the typical Swift case). + public let invertibleProtocols: InvertibleProtocolSet? + + public init( + name: String, + index: Int, + depth: Int, + requirements: [Requirement], + candidates: [Candidate] = [], + invertibleProtocols: InvertibleProtocolSet? = nil + ) { + self.name = name + self.index = index + self.depth = depth + self.requirements = requirements + self.candidates = candidates + self.invertibleProtocols = invertibleProtocols + } + + /// Protocol requirements that require witness tables (in order) + public var protocolRequirements: [Requirement] { + requirements.filter { + if case .protocol = $0 { return true } + return false + } + } + + /// Whether this parameter has any protocol requirements + public var hasProtocolRequirements: Bool { + !protocolRequirements.isEmpty + } + } +``` + +`MachOSwiftSection` is already imported by this file (line 3), which exposes `InvertibleProtocolSet` — no new import needed. + +- [ ] **Step 4: Fill the field from `buildParameters`** + +In `GenericSpecializer.swift:79-120` extend the body of `buildParameters` so that after collecting requirements and candidates a second pass extracts any `.invertedProtocols` requirement targeting the parameter being constructed. + +Replace this block at the existing tail of the inner loop: + +```swift + parameters.append(SpecializationRequest.Parameter( + name: paramName, + index: index, + depth: depth, + requirements: requirements, + candidates: candidates + )) +``` + +with: + +```swift + let invertibleProtocols = Self.collectInvertibleProtocols( + for: index, + depth: depth, + in: genericContext + ) + + parameters.append(SpecializationRequest.Parameter( + name: paramName, + index: index, + depth: depth, + requirements: requirements, + candidates: candidates, + invertibleProtocols: invertibleProtocols + )) +``` + +Add the new helper as a `private static` function inside the same `extension GenericSpecializer`, just after `mergedRequirements`: + +```swift + /// Pick out the `~Copyable` / `~Escapable` declaration for the + /// generic parameter at `(depth, index)`, intersecting if multiple + /// `invertedProtocols` requirements target the same parameter. + /// Returns `nil` when no requirement targets this parameter. + private static func collectInvertibleProtocols( + for index: Int, + depth: Int, + in genericContext: GenericContext + ) -> InvertibleProtocolSet? { + // The binary stores the parameter index as a flat 16-bit value + // across all depth levels: it equals the cumulative count of + // parameters in prior depths plus the current depth's index. + let priorDepthParameterCount = genericContext.allParameters + .prefix(depth) + .reduce(0) { $0 + $1.count } + let flatIndex = UInt16(priorDepthParameterCount + index) + + var result: InvertibleProtocolSet? + for descriptor in mergedRequirements(from: genericContext) + where descriptor.layout.flags.kind == .invertedProtocols { + guard case .invertedProtocols(let inverted) = descriptor.content else { continue } + guard inverted.genericParamIndex == flatIndex else { continue } + + if let existing = result { + result = existing.intersection(inverted.protocols) + } else { + result = inverted.protocols + } + } + return result + } +``` + +- [ ] **Step 5: Run the new test** + +Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests/invertedProtocolsExposed 2>&1 | xcsift` +Expected: PASS — `invertibleProtocols` is non-`nil` for the fixture, `.copyable` is absent, specialization with `Int` succeeds. + +- [ ] **Step 6: Run the full specializer test class** + +Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests 2>&1 | xcsift` +Expected: every test passes — the new optional field defaults to `nil` for fixtures without `invertedProtocols`, so the existing tests are unaffected. + +- [ ] **Step 7: Commit** + +```bash +git add Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift Tests/SwiftInterfaceTests/GenericSpecializationTests.swift +git commit -m "feat(SwiftInterface): surface invertible protocols on Parameter" +``` + +--- + +## Done + +After Task 6 the working tree should have six commits, each scoped to one numbered fix from the spec. Final verification: + +- [ ] **Step 1: Final full-suite run** + +Run: `swift test --filter SwiftInterfaceTests 2>&1 | xcsift` +Expected: all tests in `SwiftInterfaceTests` pass. + +- [ ] **Step 2: Confirm spec coverage** + +| Spec section | Plan task | Status | +|---|---|---| +| Design §1 invertibleProtocols on Parameter | Task 6 | ✓ | +| Design §2 conditional invertible merge | Task 4 | ✓ | +| Design §3 split nil-return arms | Task 2 | ✓ | +| Design §4 generic candidate fail-fast | Task 5 | ✓ | +| Design §5 configurable MetadataRequest | Task 3 | ✓ | +| Design §6 remove dead code | Task 1 | ✓ | +| Testing §3.1 inverted protocols exposure | Task 6 step 1 | ✓ | +| Testing §3.2 fail-fast on generic candidate | Task 5 step 1 | ✓ | +| Testing §3.3 configurable MetadataRequest | Task 3 step 1 | ✓ | +| Testing §3.4 conditional invertible (e2e) | Task 6 fixture's conditional `Copyable` extension exercises §2 merge | ✓ | diff --git a/docs/superpowers/reviews/2026-05-06-generic-specializer-bug-review.md b/docs/superpowers/reviews/2026-05-06-generic-specializer-bug-review.md new file mode 100644 index 00000000..4ec7b90d --- /dev/null +++ b/docs/superpowers/reviews/2026-05-06-generic-specializer-bug-review.md @@ -0,0 +1,349 @@ +# GenericSpecializer 代码审查与修复总结 + +**审查日期:** 2026-05-06 +**审查对象:** `Sources/SwiftInterface/GenericSpecializer/`、`Tests/SwiftInterfaceTests/GenericSpecializationTests.swift`、`Sources/SwiftDump/Extensions/GenericContext+Dump.swift` +**审查方法:** 对照 Swift 编译器源码(`/Volumes/SwiftProjects/swift-project/swift/lib/IRGen/GenMeta.cpp`、`lib/AST/GenericSignature.cpp`、`lib/AST/RequirementMachine/RequirementBuilder.cpp`、`lib/AST/Requirement.cpp`、`include/swift/AST/DiagnosticsSema.def`)逐项验证当前实现的正确性。 +**约束:** generic type pack 与 value generics 已声明为暂不实现的功能,不在审查范围内。 + +--- + +## 一、最终落地的修复(4 项) + +| 编号 | 主题 | 类型 | 复现测试 | +| ---- | ---- | ---- | -------- | +| P3 | TypePack/Value 参数早抛 | bug fix | `typePackParameterThrowsEarly` | +| P5 | SwiftDump 累积参数 dump | bug fix(潜伏) | `nestedThreeLevelDumpAllLevelsHasNoDuplicates` | +| P6 | `validate()` 运行时预检 | API 增强 + 错误时机修正 | `runtimePreflightCatches{ProtocolMismatch,LayoutMismatch}` | +| P8 | AssociatedTypeRequirement 按 (param, path) 聚合 | 语义修正 | `associatedTypeRequirementsAggregatedByPath` | + +附加: + +- P2:补 `nestedThreeLevelSpecializeEndToEnd` 端到端测试,关闭 commit `aa07d74` 留下的覆盖缺口(不是新 bug,是测试缺口)。 +- P7:新增 `SpecializationRequest.CandidateOptions.excludeGenerics`,UX 增强(不是 bug)。 + +每条复现测试都在临时撤回相应修复后实测确认会失败: + +| 修复 | 撤回后观察到的错误 | +| ---- | ----------------- | +| P3 | `Issue.record("makeRequest must reject…")` —— 旧实现静默跳过 TypePack | +| P5 | dumped 字符串等于 `A, A1, B1, A2`,phantom `B1` 被复现 | +| P6 | `specialize` 抛 `witnessTableNotFound`,而非 `specializationFailed` | +| P8 | `elementEntries.count == 3 ≠ 1`,每个 requirement 一条独立记录 | + +--- + +## 二、撤回的修复(3 项,附原因) + +### P1(已撤):mergedRequirements 过滤 conditional 中的非-invertible + +**最初的判断:** Conditional invertible 区段经由 `addGenericRequirements` 写入,理论上可以含 `Conformance` 类型记录(例如 `where T: Hashable`),合并后会污染 base 参数列表。 + +**对照源码后发现:** Swift 前端在 sema 阶段就会拒绝这种写法。 + +``` +DiagnosticsSema.def:8200 inverse_cannot_be_conditional_on_requirement +"conditional conformance to suppressible cannot depend on '<:|==|has same shape as>'" +``` + +实测尝试 `extension X: Copyable where A: Copyable, A: Equatable {}` 直接编译失败,所以条件区段在合法 Swift 二进制里**只可能**含 `.invertedProtocols`。 + +**当前代码的工作机理:** 即便 conditional 区段确实只含 `.invertedProtocols`,原本的代码: + +- `collectInvertibleProtocols` 通过 `genericParamIndex == flatIndex` 过滤——条件区段里"正向 Copyable"形式的 `genericParamIndex == 0xFFFF`,永远不匹配真实参数 ordinal,自动跳过; +- `buildRequirement` 对 `.invertedProtocols` 返回 `nil`,不会进入 `parameter.requirements`; +- `resolveAssociatedTypeWitnesses` 仅看 `.protocol` kind。 + +所以即使源码层面允许该输入,下游也已正确过滤。**P1 是个幽灵 bug。** + +**结论:** 撤回过滤代码,更新 `mergedRequirements` 注释解释这个不变量(Swift sema 强制 + 下游 kind 过滤双重保障)。 + +### P4(已撤):specialize 单次 canonical 遍历 + +**最初的判断:** 现有 2 阶段写法(先按参数顺序收 direct PWT、再追加 associated PWT)依赖一个隐式的不变量"GP 优先于 nested type",不直接对应二进制布局规则。 + +**对照源码后发现:** Swift `compareDependentTypesRec`(`GenericSignature.cpp:846`)对 (GP, nested-type) pair 总是返回 GP 在前;且 weight 在普通泛型场景下都是 0,不会 fall through 出非 GP-优先的顺序。也就是说,2 阶段写法的输出与 canonical 顺序**逐字节相等**,不是巧合,是 Swift 排序规则的直接推论。 + +**结论:** P4 不是 bug fix,是代码风格 refactor。撤回新写法,保留 2 阶段,但在注释里把不变量与对应 Swift 源码位置写清楚。 + +#### 我曾认为更稳健的方案(保留以备后续参考) + +如果未来 Swift 改了 `compareDependentTypesRec`、或我们要支持 type pack / value generics 让 weight 出现非零情形,可以切换到下面的单次 canonical 遍历。它把"按 binary canonical 顺序遍历"做成显式逻辑,不依赖排序推论: + +```swift +public func specialize(...) throws -> SpecializationResult { + let typeDescriptor = request.typeDescriptor.asPointerWrapper(in: machO) + + let staticValidation = validate(selection: selection, for: request) + guard staticValidation.isValid else { /* throw */ } + let runtimeValidation = runtimePreflight(selection: selection, for: request) + guard runtimeValidation.isValid else { /* throw */ } + + // Phase 1 — metadata in declaration order. + var metadatas: [Metadata] = [] + var metadataByName: [String: Metadata] = [:] + for parameter in request.parameters { + let argument = selection[parameter.name]! // validate 已保证存在 + let metadata = try resolveMetadata(for: argument, parameterName: parameter.name) + metadatas.append(metadata) + metadataByName[parameter.name] = metadata + } + + // Phase 2 — 一次性按 binary canonical 顺序产 PWT。 + let genericContext = try requireGenericContextInProcess(for: request.typeDescriptor) + let mergedReqs = Self.mergedRequirements(from: genericContext) + var witnessTables: [ProtocolWitnessTable] = [] + var perParamPWTs: [String: [ProtocolWitnessTable]] = [:] + + for requirement in mergedReqs { + guard requirement.flags.kind == .protocol, + requirement.flags.contains(.hasKeyArgument) else { continue } + + let resolvedContent = try requirement.resolvedContent() + guard case .protocol(let protocolRef) = resolvedContent, + let resolved = protocolRef.resolved, + let swiftDescriptor = resolved.swift else { continue } + let proto = try MachOSwiftSection.Protocol(descriptor: swiftDescriptor) + + let paramNode = try MetadataReader.demangleType(for: requirement.paramMangledName()) + let targetMetadata: Metadata + let directParamName: String? + + if let directName = Self.directGenericParamName(of: paramNode) { + targetMetadata = metadataByName[directName]! + directParamName = directName + } else if let pathInfo = Self.extractAssociatedPath(of: paramNode), !pathInfo.steps.isEmpty { + guard let indexer else { throw AssociatedTypeResolutionError.missingIndexer } + var current = metadataByName[pathInfo.baseParamName]! + for step in pathInfo.steps { + current = try resolveAssociatedTypeStep( + currentMetadata: current, + step: step, + allProtocolDefinitions: indexer.allAllProtocolDefinitions + ) + } + targetMetadata = current + directParamName = nil + } else { + continue + } + + guard let pwt = try? RuntimeFunctions.conformsToProtocol( + metadata: targetMetadata, + protocolDescriptor: proto.descriptor + ) else { + throw SpecializerError.witnessTableNotFound(/* … */) + } + + witnessTables.append(pwt) + if let name = directParamName { + perParamPWTs[name, default: []].append(pwt) + } + } + + var resolvedArguments: [SpecializationResult.ResolvedArgument] = [] + for parameter in request.parameters { + resolvedArguments.append(.init( + parameterName: parameter.name, + metadata: metadataByName[parameter.name]!, + witnessTables: perParamPWTs[parameter.name] ?? [] + )) + } + + let response = try /* accessor */(request: metadataRequest, metadatas: metadatas, witnessTables: witnessTables) + return SpecializationResult(metadataPointer: response.value, resolvedArguments: resolvedArguments) +} +``` + +**好处:** + +- "PWT 顺序 = mergedRequirements 顺序" 写在循环里而非靠不变量推导。 +- 不再需要 `resolveAssociatedTypeWitnesses` 这条单独路径(虽然它对外仍是 `@_spi(Support)` 保留 API,给 `main()` 测试用)。 +- 与 `swift/lib/IRGen/GenMeta.cpp:7351` `addGenericRequirements` 的发射顺序一一对应。 + +**触发切换的信号:** + +- 新增 type pack / value generics 支持,weight 不再恒等; +- Swift 改 `Requirement::compare` 或 `compareDependentTypesRec` 的排序规则; +- 想统一处理 same-type / base-class 的 PWT-less 校验路径。 + +不触发上述任一条之前,2 阶段实现工作正确且更短。 + +### P7(保留):CandidateOptions.excludeGenerics + +严格说不是 bug,是 UX 改进。原始候选列表会包含 `Array`、`Dictionary` 等带 `isGeneric: true` 的条目,调用方选了再调 `specialize` 才会拿到 `candidateRequiresNestedSpecialization`。新增的 `.excludeGenerics` 让调用方在 `makeRequest` 阶段就把这类剔除掉。 + +保留是因为它不破坏任何契约(默认值是 `.default` 即旧行为),追加的字段是 `OptionSet` 形式可向前扩展。 + +--- + +## 三、各项 bug 及修复细节 + +### P3:TypePack / Value 参数静默跳过 + +**问题:** `buildParameters` 用 `guard param.hasKeyArgument, param.kind == .type else { continue }` 跳过 typePack/value 参数。结果: + +1. `request.parameters.count` 少于 `genericContext.header.numKeyArguments` 暗含的真实参数数; +2. `specialize` 循环迭代 `request.parameters` 把 metadata pointer 数组做齐,但少塞; +3. metadata accessor 拿到不齐的数组,最终在运行时崩溃或返回错值。 + +**修复:** `makeRequest` 入口检查 `genericContext.parameters` 里是否有 `.typePack` 或 `.value`,若有立即抛 `SpecializerError.unsupportedGenericParameter(parameterKind:)`。 + +**复现测试:** `typePackParameterThrowsEarly`,fixture 是 `struct TestTypePackStruct { let value: (repeat each T) }`。撤回修复后测试落到 `Issue.record("makeRequest must reject…")`。 + +### P5:SwiftDump 累积参数 dump(≥ 3 层嵌套时输出错位) + +**问题:** `dumpGenericParameters(isDumpCurrentLevel: false)` 遍历 `allParameters`,但 `parentParameters` 每一层是**累积存储**(每层都包含父级的所有参数)。原代码: + +```swift +for (offsetAndDepth, depthParameters) in allParameters.offsetEnumerated() { + for (offset, parameter) in depthParameters.offsetEnumerated() { + try Standard(genericParameterName(depth: offsetAndDepth.index, index: offset.index)) + ... + } +} +``` + +对三层嵌套 `Outer.Middle.Inner`,`allParameters = [[A], [A, B], [C]]`,输出 ``: + +- depth 0 输出 `A` —— 正确; +- depth 1 输出 `A1, B1` —— `A1` 实际是 Outer 的 A 在 cumulative 形式下被重命名,`B1` 是 phantom 槽; +- depth 2 输出 `A2` —— Inner 的 C,正确。 + +正确输出应是 demangler-canonical 名 ``。 + +**修复:** 切换到 per-level "新增"切片走法,与 `GenericSpecializer.perLevelNewParameterCounts` 同源: + +```swift +let perLevelCounts = Self.dumpPerLevelNewParameterCounts( + parentParameters: parentParameters, + currentCount: currentParameters.count +) +var paramOffset = 0 +for (depthIndex, newCount) in perLevelCounts.enumerated() { + for indexInLevel in 0..` 选 `() -> Void`(不 Hashable),断言 `runtimePreflight().errors` 含 Hashable 错误,且 `specialize` 抛 `specializationFailed` 而非 `witnessTableNotFound`。撤回修复后测试拿到 `witnessTableNotFound`,不匹配 `specializationFailed` 分支。 +- `runtimePreflightCatchesLayoutMismatch`:`TestClassConstraintStruct` 选 `Int`,断言 `runtimePreflight` 报 `layoutRequirementNotSatisfied`。 + +### P8:AssociatedTypeRequirement 按 (param, path) 聚合 + +**问题:** 原 `buildAssociatedTypeRequirements` 为每个 requirement 单独生成一条 `AssociatedTypeRequirement`。`TestGenericStruct where A.Element: Hashable, A.Element: Decodable, A.Element: Encodable` 会产生三条 `path == ["Element"]` 的记录,每条 `requirements` 里只有一项。但字段类型是 `requirements: [Requirement]`(复数),暗示应聚合,调用方却必须自己再 group by。 + +**修复:** 用 `(parameterName, path)` 作为聚合 key 收集 requirement 列表,按首次出现顺序产出,所以同一 path 多个约束按 binary canonical 顺序进入同一个 `requirements` 数组: + +```swift +private func buildAssociatedTypeRequirements(...) throws -> [...] { + var entriesByKey: [AssociatedTypeRequirementKey: [SpecializationRequest.Requirement]] = [:] + var orderedKeys: [AssociatedTypeRequirementKey] = [] + for genericRequirement in genericRequirements { + guard let pathInfo = Self.extractAssociatedPath(of: paramNode), !pathInfo.steps.isEmpty else { continue } + guard let requirement = try buildRequirement(from: genericRequirement) else { continue } + let key = AssociatedTypeRequirementKey( + parameterName: pathInfo.baseParamName, + path: pathInfo.steps.map(\.name) + ) + if entriesByKey[key] == nil { orderedKeys.append(key) } + entriesByKey[key, default: []].append(requirement) + } + return orderedKeys.map { ... } +} + +private struct AssociatedTypeRequirementKey: Hashable { + let parameterName: String + let path: [String] +} +``` + +注意 `Key` 不能嵌在泛型方法里(Swift 限制),所以提到 extension 里。 + +**复现测试:** `associatedTypeRequirementsAggregatedByPath`: + +- 期望 `path == ["Element"]` 的条目数为 1; +- 期望该唯一条目的 `requirements.count == 3`; +- 期望 requirements 按字母序保留 canonical(Decodable < Encodable < Hashable)。 + +撤回修复后测试报 `count → 3 ≠ 1` 和 `requirements.count → 1 ≠ 3`。 + +--- + +## 四、新增的覆盖测试(非 bug fix) + +### P2:nested generic specialize 端到端 + +之前 commit `aa07d74` 修了三层嵌套泛型在 `makeRequest` / `currentRequirements` / `invertibleProtocols` 这几个点的 bug,但加的测试都只断言到 `request` 这个层面。`specialize` 调用 metadata accessor 这条完整链路对 ≥ 2 层嵌套从来没跑过。 + +补 `nestedThreeLevelSpecializeEndToEnd`:用 `NestedGenericThreeLevelInner`(满足 Hashable / Equatable / Comparable)跑完 `specialize`,断言: + +- `resolvedArguments` 顺序 `["A", "A1", "A2"]`; +- 每个直接 GP 约束贡献一个 PWT; +- `fieldOffsets() == [0, 8, 16]`(Int + Double + String 布局)。 + +这条测试在 aa07d74 之前会失败(参数名错位导致 metadata accessor 调用失败),现在通过;之后就是抗回归保险。 + +--- + +## 五、不在本次审查范围内的 follow-up + +留给后续 PR: + +1. `mergedRequirements` 现在是 `genericContext.requirements + conditionalInvertibleProtocolsRequirements`,依赖 sema 保证 conditional 区段只含 `.invertedProtocols`。如果将来要保护 against 手工伪造的二进制(非 swiftc 产出),可以加 `kind == .invertedProtocols` 过滤,把当前的隐式信任写成显式。 +2. P4 列出的"单次 canonical 遍历"方案在引入 type pack / value generics 时切换。同一 PR 顺手把 `resolveAssociatedTypeWitnesses` 标 deprecated 或合并掉。 +3. SwiftDump 端如果将来要支持完整的 `` dump(目前所有调用者都是 `current`),P5 修过的 false 分支路径就可以正式走起来——届时应同步把现有 fixtures 跑一遍 baseline 重生。 + +--- + +## 六、本次提交 surface + +**修改文件:** + +- `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift`(P3 makeRequest 入口检查、P6 runtimePreflight + specialize 联动、P7 candidateOptions、P8 聚合 key、`SpecializerError.unsupportedGenericParameter`) +- `Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift`(P7 `CandidateOptions`) +- `Sources/SwiftDump/Extensions/GenericContext+Dump.swift`(P5 per-level walker + 两个 helper) +- `Tests/SwiftInterfaceTests/GenericSpecializationTests.swift`(P2 + P3 + P5 + P6 ×2 + P7 + P8 共 7 条测试 + 1 个 fixture) + +**API 增加:** + +- `SpecializerError.unsupportedGenericParameter(parameterKind: GenericParamKind)` +- `GenericSpecializer.makeRequest(for:candidateOptions:)`(兼容旧无参形式,默认值 `.default`) +- `SpecializationRequest.CandidateOptions`(OptionSet:`.default` / `.excludeGenerics`) +- `GenericSpecializer.runtimePreflight(selection:for:)`(仅 `MachO == MachOImage`) + +**API 行为变化:** + +- `validate(selection:for:)` 文档更新(强调静态检查,不再误导调用方"specialize 会替你跑深度校验")。 +- `specialize` 现在会先静态校验、再运行时预检,最后才进 metadata 阶段;预检失败抛 `specializationFailed`。 +- `request.associatedTypeRequirements` 同 path 多约束聚合到一条记录(行为改变,调用方需重新 group 的代码可以删掉)。 diff --git a/docs/superpowers/specs/2026-05-02-generic-specializer-cleanup-design.md b/docs/superpowers/specs/2026-05-02-generic-specializer-cleanup-design.md new file mode 100644 index 00000000..03b6991d --- /dev/null +++ b/docs/superpowers/specs/2026-05-02-generic-specializer-cleanup-design.md @@ -0,0 +1,304 @@ +# GenericSpecializer Cleanup and API Polish + +**Date:** 2026-05-02 +**Status:** Approved, pending implementation +**Branch:** `feature/generic-specializer` + +## Problem + +`GenericSpecializer`'s main path — Type generic parameters, direct protocol +constraints, and multi-level associated-type witness tables — is functional +and covered by `Tests/SwiftInterfaceTests/GenericSpecializationTests.swift`. +A diff against the Swift Runtime ABI nevertheless surfaces several lightweight +gaps that hurt API quality without affecting current passing tests: + +- `~Copyable` / `~Escapable` parameter capability declarations + (`GenericRequirementKind.invertedProtocols`) are silently dropped: + `buildRequirement` in + `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift:265-268` + returns `nil` for `.sameConformance`, `.sameShape`, and `.invertedProtocols` + in one combined branch, so callers cannot tell whether a parameter allows + non-Copyable types. +- `GenericContext` reads + `conditionalInvertibleProtocolsRequirements` + (`Sources/MachOSwiftSection/Models/Generic/GenericContext.swift:30`) but + `GenericSpecializer.makeRequest` only consults `genericContext.allRequirements`, + so any conditional requirement stored under that header is invisible to the + specializer. +- A private helper `convertLayoutKind` + (`GenericSpecializer.swift:272-277`) has no callers. +- `MetadataRequest` is hard-coded to `.completeAndBlocking` at + `GenericSpecializer.swift:458`, leaving callers no way to request + `.complete` or `.abstract` when needed (e.g. to avoid blocking inside + recursive specialization). +- A generic `Candidate` (e.g. `Array`, `Optional`) cannot be resolved by + `resolveCandidate` — line 516 calls + `accessorFunction(request: .completeAndBlocking)` with no arguments, which + is correct only for non-generic types. The resulting failure surfaces as a + generic "Cannot get metadata accessor function" message, not as an + actionable error directing the caller toward + `Argument.specialized(...)` or nested specialization. +- The combined `case .sameConformance, .sameShape, .invertedProtocols` + branch reads as "everything we don't support yet", but the three kinds have + very different reasons for being skipped. The single-line comment makes + future maintenance harder. + +This document scopes a self-contained cleanup that surfaces missing +information, removes dead code, and improves error and request signatures. + +## Goals + +- Surface `~Copyable` / `~Escapable` parameter capability information on + `SpecializationRequest.Parameter`. +- Include `conditionalInvertibleProtocolsRequirements` in the requirement set + consumed by the specializer. +- Split `sameConformance` / `sameShape` / `invertedProtocols` into individual + `case` arms with intent-revealing comments. +- Let callers pass a `MetadataRequest` to `specialize(...)`. +- Detect generic candidates eagerly in `resolveCandidate` and throw a typed, + actionable error. +- Remove the unused `convertLayoutKind` helper. + +## Non-Goals + +- Variadic generic parameters (`each T`, `GenericParamKind.typePack`). +- Value generic parameters (`let N: Int`, `GenericParamKind.value`). +- `isPackRequirement` / `isValueRequirement` flag handling. +- `validate(...)` substantive validation (typed errors for `.protocol`, + `.sameType`, `.baseClass`, `.layout`); tracked separately. +- PWT caching across `RuntimeFunctions.conformsToProtocol` calls; tracked + separately. +- Querying whether a candidate type itself conforms to `Copyable` / + `Escapable` (would require new indexer surface). +- Full nested-candidate specialization (would require `Candidate` to carry + sub-arguments). The fail-fast in this spec is the prerequisite for that + future work. + +## Design + +### 1. Surface inverted protocols on `Parameter` (#5) + +`SpecializationRequest.Parameter` gains one optional field: + +```swift +public struct Parameter: Sendable { + // existing fields... + public let invertibleProtocols: InvertibleProtocolSet? +} +``` + +The set carries the **bits that ARE present** in the type (e.g. a parameter +declared `` produces a set that does **not** contain +`.copyable`). `nil` means the parameter has no `invertedProtocols` +requirement at all (i.e. it is a normal parameter that retains every +invertible protocol by default). + +Population: at the end of `buildParameters`, iterate the merged requirement +list a second time and pick out `kind == .invertedProtocols`. Each such +requirement carries `genericParamIndex: UInt16` and an +`InvertibleProtocolSet`. Match the index against the parameter's flat depth/ +index pair and write the set onto the corresponding `Parameter`. If multiple +inverted requirements target the same parameter (theoretically possible +across enclosing contexts), intersect the sets. + +`Requirement` enum is **not** changed. The existing +`case .invertedProtocols` branch in `buildRequirement` keeps returning `nil` +but with a comment explaining that the information is surfaced one level up. + +### 2. Merge conditional invertible protocol requirements (#6) + +Introduce a single private helper: + +```swift +private static func mergedRequirements(from genericContext: GenericContext) + -> [GenericRequirementDescriptor] +{ + genericContext.allRequirements.flatMap { $0 } + + genericContext.conditionalInvertibleProtocolsRequirements +} +``` + +Replace the three current call sites in `GenericSpecializer.swift` that read +`genericContext.allRequirements.flatMap { $0 }`: + +- `buildParameters` (line 94) +- `buildAssociatedTypeRequirements` (line 285) +- `resolveAssociatedTypeWitnesses` (line 593, via + `genericContextInProcess.requirements`) + +The third call site lives on the `MachO == MachOImage` extension and reads +`genericContextInProcess.requirements` (a flat array, not nested). To stay +consistent we apply the same merge inline: + +```swift +let mergedDescriptors = genericContextInProcess.requirements + + genericContextInProcess.conditionalInvertibleProtocolsRequirements +let requirements = try mergedDescriptors.map { + try GenericRequirement(descriptor: $0) +} +``` + +This treats every conditional requirement as active. The current scope rules +out non-Copyable / non-Escapable candidates (#1-#4 are out of scope), so all +candidates retain Copyable / Escapable by default and the conditional +predicates always evaluate true. When future work introduces non-default +candidates, the merge can be made conditional on the candidate's invertible +set. + +### 3. Split `.sameConformance` / `.sameShape` / `.invertedProtocols` (#7) + +Replace +`GenericSpecializer.swift:265-268`: + +```swift +case .sameConformance, .sameShape, .invertedProtocols: + // These are more advanced requirements that we don't need for basic specialization + return nil +``` + +with three independent arms: + +```swift +case .sameConformance: + // Derived from SameType / BaseClass; compiler forces hasKeyArgument = false, + // so it never participates in metadata accessor key arguments. + return nil + +case .sameShape: + // Pack-shape constraint between two TypePacks. Relevant only to variadic + // generics, which are out of scope for this specializer. + return nil + +case .invertedProtocols: + // Capability declaration (~Copyable / ~Escapable) — surfaced one level up + // on Parameter.invertibleProtocols rather than as a Requirement, because + // it relaxes rather than constrains the parameter. + return nil +``` + +No behavioural change. + +### 4. Generic candidate fail-fast (#9) + +`SpecializationRequest.Candidate` gains a `Bool` field: + +```swift +public struct Candidate: Sendable, Hashable { + public let typeName: TypeName + public let source: Source + public let isGeneric: Bool +} +``` + +`findCandidates` populates `isGeneric` by reading +`typeDefinition.type.typeContextDescriptorWrapper.typeContextDescriptor.flags.isGeneric` +(provided by `ContextDescriptorFlags` at +`Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptorFlags.swift:65`). +The field is informational — it lets callers gray-out generic candidates in +UI before attempting to use them. + +`GenericSpecializer.SpecializerError` gains: + +```swift +case candidateRequiresNestedSpecialization( + candidate: SpecializationRequest.Candidate, + parameterCount: Int +) +``` + +`resolveCandidate` checks +`try descriptor.genericContext(in: typeDefinitionEntry.machO) != nil` before +calling `metadataAccessorFunction`. If the candidate is generic, it throws +`candidateRequiresNestedSpecialization` carrying the candidate and the count +of generic parameters from the descriptor's generic context header. The +existing fall-through to a no-argument `accessorFunction(request:)` call is +removed for generic candidates. + +`parameterCount` lets callers preallocate UI for the nested selection step. + +### 5. Configurable `MetadataRequest` on `specialize` (#10) + +`specialize` signature changes: + +```swift +public func specialize( + _ request: SpecializationRequest, + with selection: SpecializationSelection, + metadataRequest: MetadataRequest = .completeAndBlocking +) throws -> SpecializationResult +``` + +The new parameter is forwarded only to the **main** accessor invocation at +`GenericSpecializer.swift:458`. Internal calls keep their original requests: + +- `resolveCandidate`'s `accessorFunction(request: .completeAndBlocking)` + stays — candidate metadata must be complete to be used as a key argument. +- `resolveAssociatedTypeStep`'s `getAssociatedTypeWitness(request: .init(), + ...)` stays — abstract is correct for type-witness extraction. + +This matches the semantics of `swift_getGenericMetadata`'s `request` +parameter: the caller controls only the freshness state of the **returned** +metadata, not transitive runtime calls. + +### 6. Remove dead code (#12) + +Delete `convertLayoutKind` at `GenericSpecializer.swift:272-277`. No callers. + +## Testing + +All new tests live in +`Tests/SwiftInterfaceTests/GenericSpecializationTests.swift`. The other four +items (#6 merge, #7 comments, #10 default parameter, #12 dead code) are +behaviour-preserving refactors covered by the existing test suite. + +### Inverted protocols exposure (#5) + +```swift +struct TestNonCopyableStruct { let a: A } +``` + +Tests: + +- `request.parameters[0].invertibleProtocols` is non-`nil`. +- The set does **not** contain `.copyable`. +- `specialize` with `A = Int` (a Copyable type) still succeeds. + +### Generic candidate fail-fast (#9) + +Set up a request whose candidates include a generic standard-library type +(e.g. `Array` against `A: Collection`). Assertions: + +- The matching `Candidate.isGeneric == true`. +- Calling `specialize(request, with: ["A": .candidate(arrayCandidate)])` + throws `candidateRequiresNestedSpecialization`, not + `candidateResolutionFailed`. + +### Configurable `MetadataRequest` (#10) + +Run `TestGenericStruct` specialization with the default request, then +again with `metadataRequest: .complete` (non-blocking). Both runs must +produce identical `fieldOffsets() == [0, 8, 16]`. + +### Conditional invertible requirements (#6) + +If a fixture exposing `conditionalInvertibleProtocolsRequirements` can be +authored within Swift 5.9+ language constraints (e.g. +`struct S: ~Copyable where A: P { ... }`), the test asserts the +resulting `Parameter.requirements` includes the merged conditional entries. +If `~Copyable` placement constraints prevent a minimal example, +this test degrades to an end-to-end specialization that exercises the merge +path without directly inspecting the merged list. + +## Risks and Migration + +- **`Candidate` and `Parameter` shape changes are public API.** Both + structures live under `@_spi(Support)` indirectly via + `SpecializationRequest`, but the new fields are sources-compatible only + for callers that use the synthesised memberwise initialiser positionally. + Existing call sites in tests use named arguments, so the impact is minimal. +- **Conditional merge is unconditional.** As noted in §2, this is correct + for the current candidate set. The merge helper is the natural extension + point when non-default candidates are introduced. +- **No ABI-level change.** No new key arguments are passed; no metadata + accessor invocation order is altered. The main accessor still receives + `[metadatas...] + [witnessTables...]` in the same order as today.