Skip to content

feat: nested generic specialization + runtime preflight (0.12.0-beta.1)#87

Merged
Mx-Iris merged 24 commits into
mainfrom
feature/specialized-type-definition
May 17, 2026
Merged

feat: nested generic specialization + runtime preflight (0.12.0-beta.1)#87
Mx-Iris merged 24 commits into
mainfrom
feature/specialized-type-definition

Conversation

@Mx-Iris
Copy link
Copy Markdown
Member

@Mx-Iris Mx-Iris commented May 17, 2026

Summary

  • Add Argument.boundGeneric to SpecializationSelection, enabling nested generic specialization (Result<Foo<Bar>, Error> style).
  • Wire up runtime preflight for sameType / baseClass requirements via the runtime metadata API.
  • Surface TypeDefinition.isSpecialized and expose a DefinitionName.name(using:) overload so downstream UIs can disambiguate specialized children.
  • Perf: memoize SwiftInterface.print, pre-resolve nested boundGeneric in specialize to defuse DAG substitution explosion.
  • Fix: parse conditional invertible regions per ABI.
  • Tests: end-to-end coverage for Argument.boundGeneric, integration fixtures for SnippetUI DAG repro, sameType/baseClass preflight narrowing.
  • Chore: relocate SemanticComponents under SemanticExtensions/, move SwiftDump helpers under Utils/, 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.1 tag will be cut so RuntimeViewer can pin a versioned prerelease instead of the feature branch.

Test plan

  • swift test passes locally for SwiftInterface / SwiftInspection / SwiftDump targets.
  • Integration fixtures green for boundGeneric round-trip.
  • CI on macos-26.

Mx-Iris added 23 commits May 8, 2026 18:42
…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.
Copilot AI review requested due to automatic review settings May 17, 2026 08:26
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.boundGeneric plus a runtime metadata preflight for sameType/baseClass requirements, and surface TypeDefinition.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 (SemanticComponentsSemanticExtensions/, SwiftDump helpers → Utils/) and expand test coverage, including a new end-to-end GenericTypeNameSubstitutionTests suite.

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"
Comment on lines +13 to +15
override class var cachePath: DyldSharedCachePath { .issueCase }

override class var cacheImageName: MachOImageName { .SwiftUI }
override class var cacheImageName: MachOImageName { .SnippetUI }
Comment on lines 108 to +128
@@ -125,7 +125,7 @@ enum SwiftInterfaceBuilderTestSuite {

class MachOFileTests: MachOTestingSupport.MachOFileTests, SwiftInterfaceBuilderTests, @unchecked Sendable {
override class var fileName: MachOFileName {
.SymbolTestsCore
.issueCase
Copy link
Copy Markdown

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

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 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.
@Mx-Iris Mx-Iris merged commit 8b34efb into main May 17, 2026
2 checks passed
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.

2 participants