feat: nested generic specialization + runtime preflight (0.12.0-beta.1)#87
Conversation
…izationResult Add a `metadata: MetadataWrapper?` field and a `specializedTypeDefinitions` array on `TypeDefinition`, plus a `specialize(with:in:image:)` method that validates kind compatibility and descriptor identity before appending a new specialized definition. Plumb the metadata through `printTypeDefinition` into the dumper, replacing the now-redundant `genericParamSpecializations` parameter on `TypeContextWrapper.dumper`. The indexer stays agnostic of specialization state — `printRoot` walks each TypeDefinition's own array.
The .omc/ directory holds local oh-my-claudecode session state and should never be tracked.
…ronment into shared module The actor-backed indexer cache, MachOImage handle, and descriptor-lookup helpers used to live inline inside GenericSpecializationTests as a private Environment protocol. A second SwiftInterfaceTests suite now needs the same wiring, so promote it to GenericSpecializationTestingEnvironment in MachOTestingSupport (with package-level visibility) and have the existing nested suites adopt it. Also wires MachOSwiftSection and SwiftInterface into MachOTestingSupport's target deps so the protocol can reference the relevant types directly.
…TypeDefinition.typeName
Building on the earlier specialized-TypeDefinition support, every
specialization currently inherits the unbound TypeName (e.g. printing
and mangling as `Box<A>`), which means two specializations of the same
type are indistinguishable downstream. Add `boundGenericTypeName(...)`
to wrap the unbound `Type → Structure/Class/Enum` node in a
`BoundGeneric{Class,Structure,Enum}` carrying the chosen type
arguments — mirroring the shape Swift's demangler emits — so each
specialized definition prints as the bound form and round-trips through
mangleAsString uniquely.
Also collapse the redundant `(in: machO, image: machOImage)` parameter
pair down to a single `MachOImage` argument: validation always needs
the in-process descriptor, so accepting only `MachOImage` removes the
generic-over-MachO surface that callers couldn't usefully vary. Drops
the long-tracked GenericSpecializer/REVIEW_FIXUPS.md (every in-scope
item shipped; deferred items are tracked outside the source tree).
…tadata Adds `getTypeByMangledNameInContext(_:specializedFrom:in:)` overloads (and in-process variants) that derive the descriptor pointer plus inline generic-arguments array from a specialized in-process metadata. For struct and enum the offset is the constant `sizeof(metadata header)/sizeof(StoredPointer)`; for class metadata it follows `TargetClassDescriptor::getGenericArgumentOffset` — non-resilient case derives from the immediate-members offset, resilient case reads the runtime-populated `StoredClassMetadataBounds.immediateMembersOffset`. Also fixes a longstanding bug where the existing `getTypeByMangledNameInContext` / `InEnvironment` wrappers accepted `genericContext` and `genericArguments` parameters but discarded them at the C ABI boundary, hardcoding `nil, nil`. Generic resolution requests that should have substituted parameters silently returned nil.
…tadata
When a dumper runs against an in-process specialized metadata (e.g.
`Box<Int>`), every mangled name read from the descriptor still references
the unbound generic parameters. This change threads the specialized
metadata through the dumper rendering path so the user-facing output
shows the concrete type arguments everywhere.
Surface changes:
- `name` renders the bound generic form (`Box<Int>`) instead of the
unbound shape, with declaration styling kept on the outer head only.
Type arguments inside `<...>` retain `.name` styling so they look
semantically identical to any other type reference in the output.
The `interfaceName` used for symbol-index lookups stays unbound.
- `declaration` skips the `<A: …>` generic-signature clause when the
bound name already carries the concrete arguments, avoiding
`Box<Int><A: Hashable>` duplication.
- Field rendering, `printTypeLayout`, last-field `endOffset`, and the
`expandedFieldOffsets` walker substitute through the specialized
metadata. The walker also recurses with each nested struct's
metadata so multi-level fields (e.g. `inner: Bar<A>` whose `value: A`
needs Bar's specialization context) all resolve.
Implementation notes:
- `MetadataContext` is renamed to `DumperMetadataContext` and extracted
into its own file, so the type isn't shadowed by the per-dumper
`Metadata` typealiases.
- Class support is added to the `metadataContext` plumbing in
`TypeWrapper+Dumper.swift` (alongside the existing struct/enum), so
`ClassDumper` can carry a specialized `ClassMetadataObjCInterop`.
- `TypedDumper` declares `metadataContext`, `resolveFieldMetatype`, and
`boundDumpedMetatype` as protocol requirements with `nil`-returning
defaults; constrained extensions for `Metadata: ValueMetadataProtocol`
and `Metadata == ClassMetadataObjCInterop` provide the real
implementations. Dynamic dispatch through the protocol is required —
plain extension methods would static-bind to the defaults.
- The expanded-offset walker keeps its kind-checked
`Metadata.createInProcess(...).asMetadataWrapper().struct` guard,
skipping non-struct field types (class / enum / builtin) cleanly
rather than mis-reading them as `StructMetadata` and trapping on
`descriptor().struct!`.
… output Adds package-level fixtures in `MachOTestingSupport` (struct, enum, and class shapes with various generic parameter counts and field compositions) so tests in any target can drive the same descriptors through `MachOImage.current()` lookups. `SpecializedMangledNameResolutionTests` pins the runtime helper layer: single-/multi-parameter substitution, positional ordering across two specializations of the same descriptor, parameters embedded in `Array<A>` / `Optional<A>` / `[Key: Value]`, enum payload substitution, class metadata (root, two-parameter, generic subclass of generic parent), and the negative path where a generic-bearing mangled name returns nil without specialization context. `SpecializedDumperFieldTypeTests` pins the dumper-side wiring: field-type substitution for struct / enum / class, declaration rendering in bound generic form with the generic clause suppressed, semantic context preservation (`Int` inside `Box<Int>` stays `.type(_, .name)` rather than getting upgraded to `.declaration`), expanded-field-offset recursion through nested specialized metadata, and a regression test for the kind-check that prevents the walker from trapping on a class-typed nested field.
swift_getGenericMetadata does not verify sameType or baseClass constraints (Metadata.cpp:810 onward), so the prior preflight - which downgraded RHS-as-associated-path to warnings and never even saw LHS-as-associated-path - could let invalid selections through to mint a corrupt metadata cache slot. Resolve LHS and RHS through swift_getTypeByMangledNameInContext using the canonical key arguments buffer instead, mirroring Swift runtime's own _checkGenericRequirements (ProtocolConformance.cpp:1846). Covers GP-vs-GP, GP-vs-concrete, and any depth of dependent-member chain (A.Element == B, A.Element.Index == B.Index) under one mechanism. Also narrow <T: BaseClass> candidate lists to the base class plus its subclasses via a new ConformanceProvider.subclasses(of:) hook; sameType candidates remain unfiltered by design and are checked at validate time.
Pin the new unified-resolution preflight against `where A == B.Element` fixtures (consistent and inconsistent selections) and the baseClass candidate narrowing against a three-level class chain. Replaces the older test that asserted associated-path RHS downgrades to a warning - that downgrade no longer applies under the runtime-substitution path.
…sSpecialized specialize(with:in:) was the only caller mutating typeName post-init, which forced a public internal(set) var. Move the bound-generic name computation up-front and feed it through a new internal designated init so external callers can't bypass the canonical typeName(in:) derivation. Replace the previously implicit "specialized iff metadata != nil" check with an explicit isSpecialized flag set at construction. Also rename specializedTypeDefinitions to specializedChildren - the shorter name pairs naturally with typeChildren / protocolChildren and matches how the printer iterates them.
Callers that need a non-default demangle profile (e.g. a stable fully-qualified rendering for hashing or cross-source comparison) previously had to bypass DefinitionName and drop down to node.print(using:) directly. Surface the same routing as the default name property but parameterized.
Captures Approach 2 — `Argument.boundGeneric` — plus the two follow-up sketches (runtime-direct mangled / tree-shaped Candidate) for future reference. Keeps the design rationale (cross-image specializer spawn, preflight metadata participation in runUnifiedConstraintCheck, dotted- path error forwarding, soft maxBindingDepth guard) in the tree so the implementation commit can stay focused on code.
…lection Lets callers express `Outer<T>` with `T = Array<Int>` / `Dictionary<String, Array<Int>>` declaratively without ping-ponging through manual `specialize` calls and `Argument.specialized(...)`. The specializer recurses on each `.boundGeneric` level using an inner `GenericSpecializer` bound to the candidate's defining image (so cross-image bindings like `Array` from stdlib resolve descriptor offsets against the right Mach-O). To make the new case integrate with every existing validation pass: - `internalValidate` / `internalRuntimePreflight` / `internalSpecialize` thread `parameterPathPrefix` + `depth`, surfacing inner errors and warnings as flat dotted paths against the same outer builder. - `collectBoundGenericValidation` runs the inner validate + preflight + specialize so `runUnifiedConstraintCheck`'s runtime-substitution path sees a fully-formed metadata for sameType / baseClass checks (bail-out only triggers on naked `.candidate`). - `ResolvedArgument.innerResult` exposes the resolved binding tree so renderers and snapshot builders can walk it without re-deriving from the original selection. - `SpecializerError.boundGenericInnerFailed(parameterName:underlying:)` preserves the inner cause for direct-SPI callers that bypass preflight; `maxBindingDepth` (default 16) guards runaway recursion.
…ants
New `BoundGeneric` suite pins the contract from the roadmap test plan:
- Array<Int> via .boundGeneric resolves to the same canonical runtime
metadata as the .metatype([Int].self) path; inner result is populated.
- Two-level Dictionary<String, Array<Int>> nests through .boundGeneric
and the binding tree is walkable via ResolvedArgument.innerResult.
- buildKeyArgumentsBuffer's PWT slot-count invariant holds (inner PWTs
don't leak into the outer buffer).
- preflight surfaces typed .protocolRequirementNotSatisfied('Hashable')
when the resolved Array<NonHashable> fails the outer constraint.
- runUnifiedConstraintCheck participates on .boundGeneric (instead of
the .candidate-style bail-out), validating sameType via
swift_getTypeByMangledNameInContext substitution.
- Inner failures keep typed identity through SpecializerError
.boundGenericInnerFailed; specializationFailed's joined reason still
mentions the inner cause.
- ResolvedArgument.innerResult is populated for .boundGeneric /
.specialized and nil for .metatype / .candidate.
- maxBindingDepth guard fires with a recognizable reason when depth
exceeds the (test-tightened) ceiling.
Pure file moves, no content changes. Pull cross-cutting helpers (DemangleResolver, DumperConfiguration, DumperMetadataContext, ParentClassVTableCache, SwiftAttribute) out of the SwiftDump top level and Dumper/ directory into a dedicated Utils/ subdirectory so the module's primary entry points stay obvious.
Pure file move, no content changes. Relocate SemanticComponents.swift out of the generic Components/ folder into a dedicated SemanticExtensions/ directory, matching how the rest of the module groups Semantic-typed additions.
specialize() previously ran each nested .boundGeneric inner level twice — once inside internalRuntimePreflight (collectBoundGenericValidation resolves inner metadata for cross-parameter constraint checks) and again in the main path via recursivelySpecializeBoundGeneric. Both branches recursed into their own preflight + main path, so binding depth N degraded to O(2^N) inner specializations; profiling T -> Array<Set<Int>> showed ~70% of total time spent inside preflight alone. Pre-resolve at the specialize entry point: depth-first walk the selection, specialize each .boundGeneric once via the inner specializer, and replace it with .specialized(SpecializationResult). internalSpecialize then sees only .specialized arguments — preflight and buildKeyArgumentsBuffer no longer recurse, so cost becomes linear in depth. Inner failures still surface as SpecializerError.specializationFailed with the same "Could not resolve metadata for parameter '<name>': ..." wording collectBoundGenericValidation produced, so existing pattern matches in tests continue to fire.
Conditional invertible protocols region was decoded as `<set: UInt16, count: UInt16, GenericRequirementDescriptor[count]>`, but the upstream layout in `swift/include/swift/ABI/GenericContext.h` is `<set: UInt16, count[popcount(set.rawBits)]: UInt16, padding, GenericRequirementDescriptor[counts.back()]>`. Counts are cumulative, the trailing requirement array needs 4-byte alignment, and the count array's length scales with the number of inverted protocols in the set rather than being a fixed singleton. Types whose conditional set has popcount <= 1 happened to read correctly because the cursor stayed 4-byte aligned and the single read covered the only count entry. Anything with popcount >= 2 (e.g. `Swift.Result`, which carries both Copyable and Escapable conditional inverses) lost 4 bytes off the cursor — every following `GenericRequirementDescriptor` decoded into garbage, eventually feeding the demangler a random byte sequence and tripping `matchFailed(wanted: "(read test function to succeed)", at: 0)` when the specializer reached `MetadataReader.demangleType` for `paramMangledName`. Rewrite both `GenericContext` init paths (Readable and ReadingContext) to read `popcount(set.rawBits)` cumulative UInt16 counts, retain the last one as the running total, align to 4 bytes, then read `total` requirement descriptors. The public field stays a single `InvertibleProtocolsRequirementCount?` whose value is now the cumulative total, matching `numTrailingObjects(GenericConditional InvertibleProtocolRequirement) == counts.back().count` upstream. Add a regression test that drives `GenericSpecializer.makeRequest` on stdlib's `Swift.Result` end-to-end via the cross-image aggregate.
Capture the investigation that traced `matchFailed(wanted: "(read test function to succeed)", at: 0)` on `Swift.Result` specialization back to a layout mismatch in `GenericContext`'s conditional invertible region — count array length, cumulative semantics, and 4-byte alignment around the trailing `GenericRequirementDescriptor` array. Includes the byte- level walkthrough that diagnostic dumps surfaced so the next time something similar shows up the cursor-evolution comparison is in the repo rather than scattered across chat transcripts.
Demangler returns the same Node instance for every back-reference, so a mangling with substitutions is a DAG, not a tree. The conformers walked children naively and re-expanded shared subtrees, turning one real-world SnippetUI record into ~394k node visits (effectively a hang). Mirror the Apple NodePrinter recursion guard (MaxDepth=768) and add a per-print SemanticString memoization keyed by ObjectIdentifier(node). Only cache default-context calls so output that depends on caller state (asPrefixContext / custom context / active dependentMemberType chain) stays correct. Cost drops from exponential-in-DAG to O(unique nodes).
Register the SnippetUI/SiriOntology image names and the iOS 12 dyld cache + SnippetUI Mach-O paths used to reproduce the DAG-print hang, then point the DyldCache and MachOFile integration suites at them so the maintainer can re-run the original repro without local edits.
There was a problem hiding this comment.
Pull request overview
Targets a 0.12.0-beta.1 release that unblocks RuntimeViewer v2.1.0-beta.1's user-driven generic specialization. The PR extends SpecializationSelection with nested boundGeneric arguments, adds runtime preflight for sameType / baseClass requirements, fixes an ABI parsing bug for conditional invertible regions, and adds memoization for SwiftInterface.print. It also relocates some helpers, expands tests/fixtures heavily, and refreshes docs.
Changes:
- Add
Argument.boundGenericplus a runtime metadata preflight for sameType/baseClass requirements, and surfaceTypeDefinition.isSpecialized/DefinitionName.name(using:)for downstream consumers. - Fix conditional-invertible-region parsing per ABI; memoize interface print path; pre-resolve nested boundGeneric in
specialize. - Restructure source layout (
SemanticComponents→SemanticExtensions/, SwiftDump helpers →Utils/) and expand test coverage, including a new end-to-endGenericTypeNameSubstitutionTestssuite.
Reviewed changes
Copilot reviewed 47 out of 53 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| Sources/MachOFixtureSupport/MachOFileName.swift | Adds issueCase enum with a hardcoded /Users/JH/... path — non-portable. |
| Sources/MachOFixtureSupport/DyldSharedCachePath.swift | Adds issueCase enum with a hardcoded /Users/JH/... path — non-portable. |
| Sources/MachOFixtureSupport/MachOImageName.swift | Adds SnippetUI / SiriOntology image-name cases. |
| Tests/IntegrationTests/SwiftDump/DyldCacheDumpTests.swift | Switches fixtures to .issueCase / .SnippetUI, breaking portability. |
| Tests/IntegrationTests/SwiftInterface/SwiftInterfaceBuilderTests.swift | Switches fixtures to .SiriOntology / .issueCase, breaking portability. |
| Tests/SwiftInterfaceTests/GenericSpecializationTests.swift | Hoists shared environment out of the suite; adds sameType / baseClass preflight fixtures and a Swift.Result regression test. |
| Tests/SwiftInterfaceTests/GenericTypeNameSubstitutionTests.swift | New helper + end-to-end coverage for TypeDefinition.boundGenericTypeName and specialize(...) mangle/demangle round-trip. |
| Sources/SwiftInterface/... (specialize, print memoization, preflight) | Implements the new specialization + preflight surface area. |
| Sources/MachOSwiftSection (GenericContext ABI) | Fixes conditional-invertible-region parsing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| case SymbolTests = "../../Tests/Projects/SymbolTests/DerivedData/SymbolTests/Build/Products/Release/SymbolTests.framework/Versions/A/SymbolTests" | ||
| case SymbolTestsCore = "../../Tests/Projects/SymbolTests/DerivedData/SymbolTests/Build/Products/Release/SymbolTestsCore.framework/Versions/A/SymbolTestsCore" | ||
| case issueCase = "/Users/JH/Downloads/SnippetUI/SnippetUI" |
| case macOS_15_5 = "/Volumes/RE/Dyld-Shared-Cache/macOS/15.5/dyld_shared_cache_arm64e" | ||
| case iOS_18_5 = "/Volumes/Generic/iOS Systems/22F76__iPhone17,5/dyld_shared_cache_arm64e" | ||
| case iOS_26_1 = "/Volumes/Generic/iOS Systems/23B85__iPhone17,5/dyld_shared_cache_arm64e" | ||
| case issueCase = "/Users/JH/Downloads/19E241__iPhone11,2_4_6_iPhone12,3_5/dyld_shared_cache_arm64e" |
| override class var cachePath: DyldSharedCachePath { .issueCase } | ||
|
|
||
| override class var cacheImageName: MachOImageName { .SwiftUI } | ||
| override class var cacheImageName: MachOImageName { .SnippetUI } |
| @@ -125,7 +125,7 @@ enum SwiftInterfaceBuilderTestSuite { | |||
|
|
|||
| class MachOFileTests: MachOTestingSupport.MachOFileTests, SwiftInterfaceBuilderTests, @unchecked Sendable { | |||
| override class var fileName: MachOFileName { | |||
| .SymbolTestsCore | |||
| .issueCase | |||
There was a problem hiding this comment.
Code Review
This pull request introduces significant enhancements to the generic specialization engine and Swift interface printing logic. Key improvements include fixing a long-standing ABI parsing bug for conditional invertible protocols, implementing support for bound generic candidates to enable recursive specialization, and adding a memoization mechanism to the node printer to prevent performance degradation when processing complex type hierarchies. Additionally, the PR updates class, enum, and struct dumpers to handle specialized metadata and incorporates extensive new testing infrastructure and documentation. I have no feedback to provide as no review comments were included in the input.
memoize print path added in ccc7b80 calls NodePrinterTarget.append(_:) which only exists from swift-demangling 0.4.0 onward.
Summary
Argument.boundGenerictoSpecializationSelection, enabling nested generic specialization (Result<Foo<Bar>, Error>style).sameType/baseClassrequirements via the runtime metadata API.TypeDefinition.isSpecializedand expose aDefinitionName.name(using:)overload so downstream UIs can disambiguate specialized children.SwiftInterface.print, pre-resolve nested boundGeneric inspecializeto defuse DAG substitution explosion.Argument.boundGeneric, integration fixtures for SnippetUI DAG repro, sameType/baseClass preflight narrowing.SemanticComponentsunderSemanticExtensions/, move SwiftDump helpers underUtils/, bump xcscheme markers to Xcode 26.5.Why now
RuntimeViewer v2.1.0-beta.1 ships user-driven generic specialization and depends on these APIs. After this lands, a
0.12.0-beta.1tag will be cut so RuntimeViewer can pin a versioned prerelease instead of the feature branch.Test plan
swift testpasses locally for SwiftInterface / SwiftInspection / SwiftDump targets.