[codex] Fix nested generic specialization ownership#88
Conversation
There was a problem hiding this comment.
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
TypedDumperexpanded field-offset recursion to handleOptional/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.
| 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 [] } |
| 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) | ||
| } |
| 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)\"" | ||
| ) |
There was a problem hiding this comment.
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.
| 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) | ||
| } |
There was a problem hiding this comment.
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
}
}…-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.
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.
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.
…-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.
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.
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.
Summary
Root Cause
RuntimeViewer needs to specialize nested Swift generic types such as
EventListenerPhase<A>.Valueafter 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 GenericTypeNameSubstitutionEndToEndTestsswift test --package-path MachOSwiftSection --filter SpecializedDumperFieldTypeTestsCompanion RuntimeViewer PR will consume this SPI and preserve the sidebar tree identity.