Skip to content

[codex] Fix nested generic specialization ownership#88

Merged
Mx-Iris merged 1 commit into
MxIris-Reverse-Engineering:mainfrom
Kyle-Ye:codex/fix-specialization-recursive-description
Jun 10, 2026
Merged

[codex] Fix nested generic specialization ownership#88
Mx-Iris merged 1 commit into
MxIris-Reverse-Engineering:mainfrom
Kyle-Ye:codex/fix-specialization-recursive-description

Conversation

@Kyle-Ye

@Kyle-Ye Kyle-Ye commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add a TypeDefinition specialization path that derives nested specialized child types from an outer generic specialization.
  • Preserve manually-created nested specializations on their original generic child instead of moving them under the outer specialized node.
  • Extend recursive field dumping so optional payload metadata can be expanded in specialized descriptions.

Root Cause

RuntimeViewer needs to specialize nested Swift generic types such as EventListenerPhase<A>.Value after specializing the outer generic type. The previous model either copied/moved child specializations through the outer specialization path or lacked enough nested derivation information, which made separately-created nested specializations unstable.

Validation

  • swift test --package-path MachOSwiftSection --filter GenericTypeNameSubstitutionEndToEndTests
  • swift test --package-path MachOSwiftSection --filter SpecializedDumperFieldTypeTests

Companion RuntimeViewer PR will consume this SPI and preserve the sidebar tree identity.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Extends generic specialization support to derive nested specialized child TypeDefinitions from an outer specialization, and improves dump output for expanded field offsets by recursing through Optional/enum payload metadata.

Changes:

  • Add a TypeDefinition.specialize(...) SPI overload that can derive nested specialized children during specialization.
  • Enhance TypedDumper expanded field-offset recursion to handle Optional/enum payload metadata, not just structs.
  • Add new fixtures and tests covering nested derivation behavior and Optional payload recursion.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
Tests/SwiftInterfaceTests/GenericTypeNameSubstitutionTests.swift Adds an end-to-end test validating derived nested child specializations and preserving existing manual nested specializations.
Tests/SwiftInterfaceTests/GenericSpecializationTests.swift Adds a new nested generic fixture type used to exercise nested derivation rules.
Tests/MachOSwiftSectionTests/SpecializedDumperFieldTypeTests.swift Adds a test ensuring expanded field offsets recurse through Optional payload metadata.
Sources/SwiftInterface/Components/Definitions/TypeDefinition.swift Introduces a specialization path that derives nested specialized children and adds the nested-derivation routine.
Sources/SwiftDump/Protocols/TypedDumper.swift Refactors expanded field-offset walking to dispatch by metadata kind (struct vs enum/optional).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +262 to +278
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)
in machO: MachOImage,
depth: Int
) async throws -> [TypeDefinition] {
guard depth < 16 else { return [] }
Comment on lines 433 to 437
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 +220 to 229
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)\""
)

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request adds support for recursing through Optional and enum payload metadata during field offset dumping, and introduces recursive specialization of nested generic child types. Feedback is provided on TypeDefinition.swift to wrap the nested child specialization loop in a do-catch block, preventing a single child specialization failure from failing the entire outer type's specialization.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +251 to +300
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)
}

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
            }
        }

@Kyle-Ye Kyle-Ye marked this pull request as ready for review June 9, 2026 12:51
Mx-Iris added a commit that referenced this pull request Jun 9, 2026
…-ups

Capture the remaining review items for PR #88 that have not been
addressed in this branch yet:

  - C: request.parameters-empty case may leave grandchildren unbound
    (Copilot-flagged; assumption still needs verification against
    GenericSpecializer.makeRequest semantics).
  - D: cross-depth same-name generic parameter aliasing — the
    parameter.name lookup could collide when an Outer<A> nests an
    Inner<A> whose `A` is a distinct (depth, index) param.
  - E: `depth < 16` magic number duplicated across TypeDefinition and
    TypedDumper, and the truncation is silent.

These are not blocking PR #88. The doc lays out the assumption,
investigation steps, and proposed fix for each so a follow-up branch
can pick them up without re-reading the review history.
Mx-Iris added a commit that referenced this pull request Jun 10, 2026
Both `TypeDefinition.deriveNestedSpecializedTypeChildren` and
`TypedDumper.walkNestedExpandedFieldOffsets` previously truncated their
subtrees at a hard-coded `depth < 16` without any caller-visible signal.
Two problems compounded each other: the magic 16 was duplicated across
modules, and silent truncation made it impossible to distinguish "no
deeper nesting" from "hit the cap" in the rendered output.

Lift the bound into a named, SPI/package-exposed constant per module
(`TypeDefinition.nestedSpecializationDepthLimit`,
`nestedFieldOffsetExpansionDepthLimit`) so the value has one
authoritative source and a regression test can pin it. Annotate the
type / protocol with `@Loggable` (subsystem-scoped under
`com.machoswiftsection.swift-interface` and
`com.machoswiftsection.swift-dump` respectively) and emit a `#log(.info,
…)` line on the truncating path so callers running into the bound at
least have something to grep for in `log stream`. The diagnostic is
routed via FoundationToolbox's `@Loggable` / `#log` macros, which fall
back to the legacy `os_log` C API on macOS 10.15 — the package's
minimum deployment target precludes `os.Logger` (macOS 11+).

While here, document the `typeChildren` / `specializedChildren`
maintenance contract that PR #88 introduced asymmetrically:
`outer.specialize(...derivingNestedSpecializationsWith:...)` updates
`outer.specializedChildren` and replaces `outerSpecialized.typeChildren`
with the derived nested children, but deliberately does *not* fold
those derived children into the generic nested child's
`specializedChildren` — that field is reserved for user-initiated
specialization, and the regression test
`outerSpecializationDerivesNestedChildSpecializationsWithoutMovingExistingChildSpecializations`
pins the contract. The doc comments now explain the asymmetry and give
callers the dual-walk template for assembling a full inventory.
Mx-Iris added a commit that referenced this pull request Jun 10, 2026
Close out the last remaining PR #88 follow-up:

- Mark items C and D in the original follow-ups report as
  research-complete with assumptions disproved. C (Copilot's
  grandchildren-lose-outer-binding hypothesis) is incompatible with
  `GenericContext.parameters` being cumulative — `request.parameters`
  always carries every inherited param, so the inner loop never starves
  for outer bindings. D (cross-depth same-name param ambiguity) is
  incompatible with `genericParameterName(depth:index:)` appending the
  depth suffix to every non-zero depth — `Outer<A>.Inner<A>` reads as
  `A` and `A1` at the Specializer layer, never colliding.

- Add a dedicated report for item E covering the constant extraction,
  the `@Loggable` / `#log` wiring, the macOS 10.15 minimum that drove
  the choice over `os.Logger`, and why a 16+ layer fixture-driven
  truncation test was skipped in favor of the contract pin.
@Mx-Iris Mx-Iris merged commit 4c08cae into MxIris-Reverse-Engineering:main Jun 10, 2026
2 checks passed
Mx-Iris added a commit that referenced this pull request Jun 10, 2026
…-ups

Capture the remaining review items for PR #88 that have not been
addressed in this branch yet:

  - C: request.parameters-empty case may leave grandchildren unbound
    (Copilot-flagged; assumption still needs verification against
    GenericSpecializer.makeRequest semantics).
  - D: cross-depth same-name generic parameter aliasing — the
    parameter.name lookup could collide when an Outer<A> nests an
    Inner<A> whose `A` is a distinct (depth, index) param.
  - E: `depth < 16` magic number duplicated across TypeDefinition and
    TypedDumper, and the truncation is silent.

These are not blocking PR #88. The doc lays out the assumption,
investigation steps, and proposed fix for each so a follow-up branch
can pick them up without re-reading the review history.
Mx-Iris added a commit that referenced this pull request Jun 10, 2026
Both `TypeDefinition.deriveNestedSpecializedTypeChildren` and
`TypedDumper.walkNestedExpandedFieldOffsets` previously truncated their
subtrees at a hard-coded `depth < 16` without any caller-visible signal.
Two problems compounded each other: the magic 16 was duplicated across
modules, and silent truncation made it impossible to distinguish "no
deeper nesting" from "hit the cap" in the rendered output.

Lift the bound into a named, SPI/package-exposed constant per module
(`TypeDefinition.nestedSpecializationDepthLimit`,
`nestedFieldOffsetExpansionDepthLimit`) so the value has one
authoritative source and a regression test can pin it. Annotate the
type / protocol with `@Loggable` (subsystem-scoped under
`com.machoswiftsection.swift-interface` and
`com.machoswiftsection.swift-dump` respectively) and emit a `#log(.info,
…)` line on the truncating path so callers running into the bound at
least have something to grep for in `log stream`. The diagnostic is
routed via FoundationToolbox's `@Loggable` / `#log` macros, which fall
back to the legacy `os_log` C API on macOS 10.15 — the package's
minimum deployment target precludes `os.Logger` (macOS 11+).

While here, document the `typeChildren` / `specializedChildren`
maintenance contract that PR #88 introduced asymmetrically:
`outer.specialize(...derivingNestedSpecializationsWith:...)` updates
`outer.specializedChildren` and replaces `outerSpecialized.typeChildren`
with the derived nested children, but deliberately does *not* fold
those derived children into the generic nested child's
`specializedChildren` — that field is reserved for user-initiated
specialization, and the regression test
`outerSpecializationDerivesNestedChildSpecializationsWithoutMovingExistingChildSpecializations`
pins the contract. The doc comments now explain the asymmetry and give
callers the dual-walk template for assembling a full inventory.
Mx-Iris added a commit that referenced this pull request Jun 10, 2026
Close out the last remaining PR #88 follow-up:

- Mark items C and D in the original follow-ups report as
  research-complete with assumptions disproved. C (Copilot's
  grandchildren-lose-outer-binding hypothesis) is incompatible with
  `GenericContext.parameters` being cumulative — `request.parameters`
  always carries every inherited param, so the inner loop never starves
  for outer bindings. D (cross-depth same-name param ambiguity) is
  incompatible with `genericParameterName(depth:index:)` appending the
  depth suffix to every non-zero depth — `Outer<A>.Inner<A>` reads as
  `A` and `A1` at the Specializer layer, never colliding.

- Add a dedicated report for item E covering the constant extraction,
  the `@Loggable` / `#log` wiring, the macOS 10.15 minimum that drove
  the choice over `os.Logger`, and why a 16+ layer fixture-driven
  truncation test was skipped in favor of the contract pin.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants