Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 61 additions & 18 deletions Sources/SwiftDump/Protocols/TypedDumper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}

Expand All @@ -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() {
Expand All @@ -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)
}
Comment on lines 433 to 437
}
}
}
}

/// 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<T>`.
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<M: ValueMetadataProtocol>(for mangledTypeName: MangledName, parentMetadata: M) -> Any.Type? {
if let substituted = try? RuntimeFunctions.getTypeByMangledNameInContext(mangledTypeName, specializedFrom: parentMetadata) {
return substituted
}
Expand All @@ -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<M: ValueMetadataProtocol>(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),
Expand Down
109 changes: 108 additions & 1 deletion Sources/SwiftInterface/Components/Definitions/TypeDefinition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<MachOImage>,
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)
Expand All @@ -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<MachOImage>,
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)
Comment on lines +262 to +278
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)
}
Comment on lines +251 to +300

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If any nested child type fails to specialize (for example, due to unsupported generic parameters or a constraint mismatch), the entire deriveNestedSpecializedTypeChildren call will throw and fail the specialization of the outer type. Wrapping the inner loop body in a do-catch block and skipping the failed child allows the outer type and other valid nested types to still be specialized and displayed successfully.

        for child in typeChildren {
            guard child.type.typeContextDescriptorWrapper.typeContextDescriptor.layout.flags.isGeneric else {
                continue
            }

            do {
                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)
            } catch {
                continue
            }
        }

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
Expand Down
24 changes: 24 additions & 0 deletions Tests/MachOSwiftSectionTests/SpecializedDumperFieldTypeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Fixtures.SingleParameterBox<Int>>.self

let descriptor = try structDescriptor(named: "OptionalGenericFieldStruct")
let structValue = try Struct(descriptor: descriptor, in: machO)
let specializedMetadata = try StructMetadata.createInProcess(
Fixtures.OptionalGenericFieldStruct<Fixtures.SingleParameterBox<Int>>.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<Int>.value; got: \(body)")
}

// MARK: - Bound declaration semantic styling

@Test("specialized name keeps inner type arguments at .name (not .declaration)")
Expand Down
15 changes: 15 additions & 0 deletions Tests/SwiftInterfaceTests/GenericSpecializationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,21 @@ struct GenericSpecializationTests {
}
}

struct NestedGenericInheritedOnlyOuter<A: Hashable> {
struct FailureReason {
let value: A
}

struct Value {
let value: A
}

struct NeedsOwnParameter<B: Equatable> {
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`)
Expand Down
Loading
Loading