From 4c08cae577a57c486c67f0e8817693bb9caf6aee Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 9 Jun 2026 18:32:40 +0800 Subject: [PATCH] Fix nested generic specialization ownership --- Sources/SwiftDump/Protocols/TypedDumper.swift | 79 ++++++++++--- .../Definitions/TypeDefinition.swift | 109 +++++++++++++++++- .../SpecializedDumperFieldTypeTests.swift | 24 ++++ .../GenericSpecializationTests.swift | 15 +++ .../GenericTypeNameSubstitutionTests.swift | 77 ++++++++++++- 5 files changed, 283 insertions(+), 21 deletions(-) diff --git a/Sources/SwiftDump/Protocols/TypedDumper.swift b/Sources/SwiftDump/Protocols/TypedDumper.swift index d4c32c93..7727ddd3 100644 --- a/Sources/SwiftDump/Protocols/TypedDumper.swift +++ b/Sources/SwiftDump/Protocols/TypedDumper.swift @@ -387,9 +387,27 @@ extension TypedDumper { } else { topMetatype = try? RuntimeFunctions.getTypeByMangledNameInContext(mangledTypeName) } - if let topMetatype, - let topStructMetadata = structMetadata(forMetatype: topMetatype) { - walkNestedExpandedFieldOffsets(of: topStructMetadata, baseOffset: baseOffset, baseIndentation: baseIndentation, ancestors: ancestors) + if let topMetatype { + walkNestedExpandedFieldOffsets(of: topMetatype, baseOffset: baseOffset, baseIndentation: baseIndentation, ancestors: ancestors) + } + } + + /// Dispatches recursive expansion by metadata kind. Structs expose their + /// stored fields directly; Optional and other enum wrappers expose payload + /// case records that can themselves carry specialized struct payloads. + @SemanticStringBuilder + private func walkNestedExpandedFieldOffsets(of metatype: Any.Type, baseOffset: Int, baseIndentation: Int, ancestors: [Bool], depth: Int = 0) -> SemanticString { + if depth < 16, + let wrapper = try? Metadata.createInProcess(metatype).asMetadataWrapper() { + switch wrapper { + case .struct(let metadata): + walkNestedStructFieldOffsets(of: metadata, baseOffset: baseOffset, baseIndentation: baseIndentation, ancestors: ancestors, depth: depth) + case .enum(let metadata), + .optional(let metadata): + walkNestedEnumPayloadFieldOffsets(of: metadata, baseOffset: baseOffset, baseIndentation: baseIndentation, ancestors: ancestors, depth: depth) + default: + SemanticString() + } } } @@ -399,7 +417,7 @@ extension TypedDumper { /// (the one that treats `mangledTypeName.startOffset` as an absolute /// in-process pointer). @SemanticStringBuilder - private func walkNestedExpandedFieldOffsets(of metadata: StructMetadata, baseOffset: Int, baseIndentation: Int, ancestors: [Bool]) -> SemanticString { + private func walkNestedStructFieldOffsets(of metadata: StructMetadata, baseOffset: Int, baseIndentation: Int, ancestors: [Bool], depth: Int) -> SemanticString { if let descriptor = try? metadata.structDescriptor(), let nestedFieldOffsets = try? metadata.fieldOffsets(for: descriptor), let nestedFieldRecords = try? descriptor.fieldDescriptor().records() { @@ -414,32 +432,57 @@ extension TypedDumper { if let nestedMangledTypeName, let resolvedMetatype = resolveNestedMetatype(for: nestedMangledTypeName, parentMetadata: metadata), - let nestedStructMetadata = structMetadata(forMetatype: resolvedMetatype) { - walkNestedExpandedFieldOffsets(of: nestedStructMetadata, baseOffset: absoluteOffset, baseIndentation: baseIndentation, ancestors: ancestors + [isLastField]) + hasExpandableMetadata(forMetatype: resolvedMetatype) { + walkNestedExpandedFieldOffsets(of: resolvedMetatype, baseOffset: absoluteOffset, baseIndentation: baseIndentation, ancestors: ancestors + [isLastField], depth: depth + 1) } } } } } - /// Returns a `StructMetadata` only when the in-process metadata for - /// `metatype` actually has struct kind. Filtering on - /// `MetadataWrapper.struct` is critical because callers later reach - /// `metadata.structDescriptor()` whose internal `descriptor().struct!` - /// force-unwraps — handing it a misinterpreted class / enum / builtin - /// metadata would crash. Returning `nil` here cleanly skips the - /// recursion for non-struct field types. - private func structMetadata(forMetatype metatype: Any.Type) -> StructMetadata? { + /// Recursive walk over payload cases for Optional and enum wrappers. + /// Payloads all begin at the enum payload area, offset 0 relative to the + /// enum value, so the child offset starts at `baseOffset`. + @SemanticStringBuilder + private func walkNestedEnumPayloadFieldOffsets(of metadata: EnumMetadata, baseOffset: Int, baseIndentation: Int, ancestors: [Bool], depth: Int) -> SemanticString { + if let descriptor = try? metadata.enumDescriptor(), + descriptor.hasPayloadCases, + let records = try? descriptor.fieldDescriptor().records() { + let payloadRecords = Array(records.prefix(descriptor.numberOfPayloadCases)) + for (payloadIndex, payloadRecord) in payloadRecords.enumerated() { + if let mangledTypeName = try? payloadRecord.mangledTypeName(), + !mangledTypeName.isEmpty, + let resolvedMetatype = resolveNestedMetatype(for: mangledTypeName, parentMetadata: metadata), + hasExpandableMetadata(forMetatype: resolvedMetatype) { + let fieldName = (try? payloadRecord.fieldName()) ?? "payload" + let typeName = nestedTypeName(for: mangledTypeName, parentMetadata: metadata) + let isLastPayload = payloadIndex == payloadRecords.count - 1 + configuration.expandedFieldOffsetComment(fieldName: fieldName, typeName: typeName, offset: baseOffset, baseIndentation: baseIndentation, ancestors: ancestors, isLast: isLastPayload) + walkNestedExpandedFieldOffsets(of: resolvedMetatype, baseOffset: baseOffset, baseIndentation: baseIndentation, ancestors: ancestors + [isLastPayload], depth: depth + 1) + } + } + } + } + + /// Returns true only for metadata kinds the recursive walker knows how + /// to inspect safely. This preserves the old class/builtin guard while + /// allowing enum payload containers such as `Optional`. + private func hasExpandableMetadata(forMetatype metatype: Any.Type) -> Bool { guard let wrapper = try? Metadata.createInProcess(metatype).asMetadataWrapper() else { - return nil + return false + } + switch wrapper { + case .struct, .enum, .optional: + return true + default: + return false } - return wrapper.struct } /// Resolves a nested field's mangled name to its concrete `Any.Type`, /// substituting generic parameters via the parent struct's specialized /// metadata. Falls back to the bare resolver for fully-resolved names. - private func resolveNestedMetatype(for mangledTypeName: MangledName, parentMetadata: StructMetadata) -> Any.Type? { + private func resolveNestedMetatype(for mangledTypeName: MangledName, parentMetadata: M) -> Any.Type? { if let substituted = try? RuntimeFunctions.getTypeByMangledNameInContext(mangledTypeName, specializedFrom: parentMetadata) { return substituted } @@ -451,7 +494,7 @@ extension TypedDumper { /// print the bound type via `_mangledTypeName` round-trip; otherwise /// we fall through to the unbound demangling, which keeps the legacy /// behavior for non-generic / unresolvable names. - private func nestedTypeName(for mangledTypeName: MangledName?, parentMetadata: StructMetadata) -> String { + private func nestedTypeName(for mangledTypeName: MangledName?, parentMetadata: M) -> String { guard let mangledTypeName else { return "" } if #available(macOS 11, iOS 14, tvOS 14, watchOS 7, *), let resolvedMetatype = resolveNestedMetatype(for: mangledTypeName, parentMetadata: parentMetadata), diff --git a/Sources/SwiftInterface/Components/Definitions/TypeDefinition.swift b/Sources/SwiftInterface/Components/Definitions/TypeDefinition.swift index c4dc1cae..f485e5d3 100644 --- a/Sources/SwiftInterface/Components/Definitions/TypeDefinition.swift +++ b/Sources/SwiftInterface/Components/Definitions/TypeDefinition.swift @@ -166,6 +166,50 @@ public final class TypeDefinition: Definition { typeArgumentNodes: [Node]? = nil, in machO: MachOImage, ) async throws -> TypeDefinition { + let specialized = try makeSpecializedDefinition( + with: specializationResult, + typeArgumentNodes: typeArgumentNodes, + in: machO + ) + specializedChildren.append(specialized) + return specialized + } + + @_spi(Support) + @discardableResult + public func specialize( + with specializationResult: SpecializationResult, + typeArgumentNodes: [Node]? = nil, + derivingNestedSpecializationsWith specializer: GenericSpecializer, + selection: SpecializationSelection, + typeArgumentNodesByParameter: [String: Node], + in machO: MachOImage + ) async throws -> TypeDefinition { + let specialized = try makeSpecializedDefinition( + with: specializationResult, + typeArgumentNodes: typeArgumentNodes, + in: machO + ) + specialized.typeChildren = try await deriveNestedSpecializedTypeChildren( + using: specializer, + selection: selection, + typeArgumentNodesByParameter: typeArgumentNodesByParameter, + inheritedTypeArgumentNodes: typeArgumentNodes ?? [], + in: machO, + depth: 0 + ) + for child in specialized.typeChildren { + child.parent = specialized + } + specializedChildren.append(specialized) + return specialized + } + + private func makeSpecializedDefinition( + with specializationResult: SpecializationResult, + typeArgumentNodes: [Node]?, + in machO: MachOImage + ) throws -> TypeDefinition { let metadata = try specializationResult.resolveMetadata() try validateSpecialization(metadata: metadata, in: machO) @@ -190,10 +234,73 @@ public final class TypeDefinition: Definition { let specialized = TypeDefinition(type: type, typeName: finalTypeName, isSpecialized: true) specialized.metadata = metadata - specializedChildren.append(specialized) return specialized } + private func deriveNestedSpecializedTypeChildren( + using specializer: GenericSpecializer, + selection: SpecializationSelection, + typeArgumentNodesByParameter: [String: Node], + inheritedTypeArgumentNodes: [Node], + in machO: MachOImage, + depth: Int + ) async throws -> [TypeDefinition] { + guard depth < 16 else { return [] } + + var derivedChildren: [TypeDefinition] = [] + for child in typeChildren { + guard child.type.typeContextDescriptorWrapper.typeContextDescriptor.layout.flags.isGeneric else { + continue + } + + let request = try specializer.makeRequest(for: child.type.typeContextDescriptorWrapper) + var childArguments: [String: SpecializationSelection.Argument] = [:] + var childArgumentNodes: [Node] = [] + var childNodesByParameter: [String: Node] = [:] + var hasCompleteBinding = true + + for parameter in request.parameters { + guard let argument = selection.arguments[parameter.name], + let node = typeArgumentNodesByParameter[parameter.name] + else { + hasCompleteBinding = false + break + } + childArguments[parameter.name] = argument + childArgumentNodes.append(node) + childNodesByParameter[parameter.name] = node + } + + guard hasCompleteBinding else { + continue + } + + let childSelection = SpecializationSelection(arguments: childArguments) + let childResult = try specializer.specialize(request, with: childSelection) + let effectiveChildArgumentNodes = childArgumentNodes.isEmpty + ? inheritedTypeArgumentNodes + : childArgumentNodes + let childSpecialized = try child.makeSpecializedDefinition( + with: childResult, + typeArgumentNodes: effectiveChildArgumentNodes, + in: machO + ) + childSpecialized.typeChildren = try await child.deriveNestedSpecializedTypeChildren( + using: specializer, + selection: childSelection, + typeArgumentNodesByParameter: childNodesByParameter, + inheritedTypeArgumentNodes: effectiveChildArgumentNodes, + in: machO, + depth: depth + 1 + ) + for grandchild in childSpecialized.typeChildren { + grandchild.parent = childSpecialized + } + derivedChildren.append(childSpecialized) + } + return derivedChildren + } + /// Build a bound-generic `TypeName` by wrapping the supplied unbound /// (`Type → Structure(...)` / `Class(...)` / `Enum(...)`) form with a /// `BoundGeneric{Class,Structure,Enum}` node carrying the concrete type diff --git a/Tests/MachOSwiftSectionTests/SpecializedDumperFieldTypeTests.swift b/Tests/MachOSwiftSectionTests/SpecializedDumperFieldTypeTests.swift index 5ed744e7..4853f558 100644 --- a/Tests/MachOSwiftSectionTests/SpecializedDumperFieldTypeTests.swift +++ b/Tests/MachOSwiftSectionTests/SpecializedDumperFieldTypeTests.swift @@ -311,6 +311,30 @@ struct SpecializedDumperFieldTypeTests { "expected nested expanded line to substitute A → Double; got: \(body)") } + @Test("expanded field offsets recurse through Optional payload metadata") + func expandedFieldOffsetsRecurseThroughOptionalPayloadMetadata() async throws { + _ = Fixtures.OptionalGenericFieldStruct>.self + + let descriptor = try structDescriptor(named: "OptionalGenericFieldStruct") + let structValue = try Struct(descriptor: descriptor, in: machO) + let specializedMetadata = try StructMetadata.createInProcess( + Fixtures.OptionalGenericFieldStruct>.self + ) + let metadataContext = DumperMetadataContext(metadata: specializedMetadata, readingContext: InProcessContext.shared) + + var expandedConfig = configuration + expandedConfig.printFieldOffset = true + expandedConfig.printExpandedFieldOffsets = true + + let dumper = StructDumper(structValue, metadataContext: metadataContext, using: expandedConfig, in: machO) + let body = try await dumper.body.string + + #expect(body.contains("some ("), + "expected Optional payload case to be emitted before payload recursion; got: \(body)") + #expect(body.contains("value (Swift.Int):") || body.contains("value (Int):"), + "expected Optional payload recursion to expand SingleParameterBox.value; got: \(body)") + } + // MARK: - Bound declaration semantic styling @Test("specialized name keeps inner type arguments at .name (not .declaration)") diff --git a/Tests/SwiftInterfaceTests/GenericSpecializationTests.swift b/Tests/SwiftInterfaceTests/GenericSpecializationTests.swift index aa62888e..4eff1d5c 100644 --- a/Tests/SwiftInterfaceTests/GenericSpecializationTests.swift +++ b/Tests/SwiftInterfaceTests/GenericSpecializationTests.swift @@ -80,6 +80,21 @@ struct GenericSpecializationTests { } } + struct NestedGenericInheritedOnlyOuter { + struct FailureReason { + let value: A + } + + struct Value { + let value: A + } + + struct NeedsOwnParameter { + let a: A + let b: B + } + } + /// 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`) diff --git a/Tests/SwiftInterfaceTests/GenericTypeNameSubstitutionTests.swift b/Tests/SwiftInterfaceTests/GenericTypeNameSubstitutionTests.swift index 894b97df..e4285a2a 100644 --- a/Tests/SwiftInterfaceTests/GenericTypeNameSubstitutionTests.swift +++ b/Tests/SwiftInterfaceTests/GenericTypeNameSubstitutionTests.swift @@ -217,11 +217,13 @@ struct GenericTypeNameSubstitutionEndToEndTests: GenericSpecializationTestingEnv /// raw descriptor here was crashing inside `MetadataReader.demangleContext` /// because the file-form descriptors from `machO.swift.typeContextDescriptors` /// require additional in-process context the test wasn't supplying. - private func resolveTypeDefinition(named substring: String) async throws -> TypeDefinition { + private func resolveTypeDefinition(named substring: String, excluding excludedSubstring: String? = nil) async throws -> TypeDefinition { let resolvedIndexer = try await indexer return try #require( resolvedIndexer.allTypeDefinitions.first(where: { entry in - entry.key.name.contains(substring) + let includesName = entry.key.name.contains(substring) + let isNotExcluded = excludedSubstring.map { !entry.key.name.contains($0) } ?? true + return includesName && isNotExcluded })?.value, "expected indexer to have a TypeDefinition whose typeName contains \"\(substring)\"" ) @@ -349,4 +351,75 @@ struct GenericTypeNameSubstitutionEndToEndTests: GenericSpecializationTestingEnv #expect(!mangledInt.isEmpty) #expect(!mangledString.isEmpty) } + + @Test("outer specialization derives nested child specializations without moving existing child specializations") + func outerSpecializationDerivesNestedChildSpecializationsWithoutMovingExistingChildSpecializations() async throws { + _ = GenericSpecializationTests.NestedGenericInheritedOnlyOuter.self + _ = GenericSpecializationTests.NestedGenericInheritedOnlyOuter.self + + let resolvedIndexer = try await indexer + let baseDefinition = try #require( + resolvedIndexer.allTypeDefinitions.first(where: { entry in + entry.value.typeName.currentName == "NestedGenericInheritedOnlyOuter" + })?.value, + "expected indexer to have the root outer fixture definition" + ) + let valueChild = try #require( + baseDefinition.typeChildren.first { $0.typeName.name.contains("Value") }, + "expected outer generic fixture to have its nested Value type before specialization" + ) + let specializer = GenericSpecializer(indexer: try await indexer) + + let valueRequest = try specializer.makeRequest(for: valueChild.type.typeContextDescriptorWrapper) + let valueStringResult = try specializer.specialize(valueRequest, with: ["A": .metatype(String.self)]) + let stringNode = makeSwiftStdLibTypeNode(name: "String") + let manuallySpecializedValue = try await valueChild.specialize( + with: valueStringResult, + typeArgumentNodes: [stringNode], + in: machO + ) + #expect(valueChild.specializedChildren.contains { $0 === manuallySpecializedValue }) + + let outerRequest = try specializer.makeRequest(for: baseDefinition.type.typeContextDescriptorWrapper) + let outerSelection = SpecializationSelection(arguments: ["A": .metatype(Int.self)]) + let outerResult = try specializer.specialize(outerRequest, with: outerSelection) + let intNode = makeSwiftStdLibTypeNode(name: "Int") + + let specialized = try await baseDefinition.specialize( + with: outerResult, + typeArgumentNodes: [intNode], + derivingNestedSpecializationsWith: specializer, + selection: outerSelection, + typeArgumentNodesByParameter: ["A": intNode], + in: machO + ) + + #expect(valueChild.specializedChildren.count == 1) + #expect(valueChild.specializedChildren.contains { $0 === manuallySpecializedValue }, + "outer specialization must not move or duplicate existing manual nested specializations") + + let specializedChildNames = specialized.typeChildren.map(\.typeName.name) + let specializedFailureReason = try #require( + specialized.typeChildren.first { + $0.isSpecialized + && $0.typeName.name.contains("FailureReason") + }, + "expected specialized outer to derive FailureReason; got \(specializedChildNames)" + ) + let specializedValue = try #require( + specialized.typeChildren.first { + $0.isSpecialized + && $0.typeName.name.contains("Value") + }, + "expected specialized outer to derive Value; got \(specializedChildNames)" + ) + #expect(specializedFailureReason.parent === specialized) + #expect(specializedValue.parent === specialized) + #expect(specializedFailureReason.metadata != nil) + #expect(specializedValue.metadata != nil) + #expect(!specializedChildNames.contains { $0.contains("NeedsOwnParameter") }, + "nested child requiring its own B parameter should be ignored when only the outer A binding is available") + #expect(!specialized.typeChildren.contains { $0 === valueChild }, + "specialized outer must contain detached child specializations, not the canonical generic child") + } }