Skip to content

[codex] Preserve nested Swift specializations#76

Merged
Mx-Iris merged 68 commits into
mainfrom
codex/fix-specialization-recursive-description
Jun 10, 2026
Merged

[codex] Preserve nested Swift specializations#76
Mx-Iris merged 68 commits into
mainfrom
codex/fix-specialization-recursive-description

Conversation

@Kyle-Ye

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

Copy link
Copy Markdown
Collaborator

Summary

  • Preserve RuntimeViewer sidebar identity for specialized Swift generic trees.
  • Keep manually-added nested specializations under their original generic child when an ancestor generic later gets specialized.
  • Add a regression test for the EventListenerPhase.Value-then-EventListenerPhase ordering.

Root Cause

The sidebar splice path updated only the cell that directly received a specialization. Ancestor cells still held stale RuntimeObject.children snapshots. When an ancestor later appended its own specialized child, rebuilding from that stale snapshot dropped descendant specializations such as EventListenerPhase.Value<Event>.

Fix

The sidebar cell now materializes its current view-model subtree before appending a specialization, so ancestor updates preserve descendant children already spliced into the UI tree. Parent lookup also uses RuntimeObjectKey, which intentionally ignores child snapshots.

Dependency

Depends on MxIris-Reverse-Engineering/MachOSwiftSection#88 for the nested specialization SPI consumed by RuntimeViewerCore.

Validation

  • USING_LOCAL_DEPENDENCIES=1 DEVELOPER_DIR=/Applications/Xcode-26.4.1.app/Contents/Developer swift test --package-path RuntimeViewer/RuntimeViewerPackages --filter SidebarRuntimeObjectCellViewModelTests
  • DEVELOPER_DIR=/Applications/Xcode-26.4.1.app/Contents/Developer xcodebuild -quiet -workspace RuntimeViewer/RuntimeViewer-Debug.xcworkspace -scheme 'RuntimeViewer macOS' -configuration Debug -destination 'platform=macOS,arch=arm64' -derivedDataPath /tmp/RuntimeViewerNestedSpecializationBuildFinal build

Mx-Iris added 30 commits May 27, 2026 23:45
Single-image Export still requires entering an image first; bulk
extraction across many frameworks was repetitive. Add a parallel
File → Export Multiple Images… wizard that picks N images up front,
then runs the per-image export concurrently (up to 8 in parallel) into
destinationURL/<imageName>/ subdirectories via the unchanged single-image
RuntimeEngine.exportInterfaces API. Progress is shown as an NSTableView
of per-image rows (queued / running with progress bar / succeeded /
failed). The wizard container (ExportingViewController + ExportingRoute)
is reused; the single-image entry remains untouched.
Folds the few remaining AppKitViewController<VM> subclasses into
UXKitViewController<VM> and gives the base an overridable contentInsets
so sheet/popover screens can inset the rooted contentView without
hand-rolling padding constraints. Roots that previously called
hierarchy {} on self now drive contentView.hierarchy {} instead.

Also restacks the batch-export image-selection cell into nested
HStack/VStack with a per-row path subtitle, switching the table to
usesAutomaticRowHeights for the taller two-line rows.
Drops the per-image objects probe that gated the ObjC/Swift format
choices behind whether a selected image actually exposed each runtime.
The probe ran in the configuration step's init before the user had
picked any images, so the radios stayed hidden on entry. Both format
groups are now always visible; objectsByImage/loading state on
BatchExportingState is no longer needed and goes away with it.
…lved

Flip UIFoundation's local checkout to `isEnabled: true` so the workspace
build no longer toggles between local and SPM-resolved copies depending
on the per-machine `usingLocalDependencies` flag; bump both
Package.resolved files to match the resulting checkout graph.
Every server-bound command used to be declared three times — once in
the public API's local/remote split, once in
setupMessageHandlerForServer, and once in RuntimeEngineProxyServer's
handler registry. Adding a new command meant editing all three call
sites, and missing the proxy registration silently broke remote
mirroring.

Collapse the trio into a single RuntimeEngineRequest protocol whose
conformers describe the wire name and own a perform(on:) local
implementation. RuntimeEngine.dispatch(_:) routes through the same
conformer for both client (sendMessage) and local arms, and a single
registerSharedHandlers(on:engine:) call wires every shared command on
both the server arm and the proxy server. engineList stays server-only
(no proxy equivalent), iconRequest stays proxy-only, and the
objectsInImage progress-pumping variant remains a server-side
override.

Adding a new command is now: declare an XxxRequest conformer + append
one register(...) line — both server entry points pick it up
automatically.
…legate

Both BatchExportingImageSelectionViewController and
SpecializationViewController previously blocked the table/outline
selection by hooking shouldSelectItem in an NSOutlineViewDelegate.
Setting selectionHighlightStyle = .none achieves the same visual
result without the delegate plumbing, so retire the delegate
extensions. SpecializationViewController also drops the now-redundant
rx.setDelegate(self) wiring; the image-selection sheet gains a small
inset around its content stack while we are touching the layout.
The completion step used to render a single concatenated multi-line
summary string and a centered checkmark hero. Swap that for a
horizontally laid-out header (compact icon + title + destination
subtitle) and four stat cards (Interfaces, Images, ObjC·Swift,
Duration) driven by a typed Summary struct on the ViewModel. The
ViewModel now formats and partitions the numbers up front (failure
ratios, locale-aware integer grouping, tilde-abbreviated destination
path) so the view stays a thin renderer.
…s gotcha

Codify the project rule that any custom AppKit view needing layer-level
visuals (corner radius, border, background, shadow) MUST inherit
UIFoundationAppKit.LayerBackedView rather than hand-rolling
wantsLayer + layer?.xxx. Call out the borderPositions = [] default
that silently swallows non-zero borderWidth + non-nil borderColor so
future contributors don't lose an afternoon to it.
Replace the SwiftUI @AppStorage-backed includeMetadata flag with the
project @userdefault wrapper and persist the ObjC/Swift format choices
as well, so export layout preferences survive across sessions for both
single and batch export. Make Format Codable so @userdefault can store
it, and centralize the defaults keys in ExportingDefaultsKey.
Adopt the current .repeat(.periodic) symbol-effect API and convey
activity with a rotating SF Symbol: restore the ImageView wrapper for
the indexing row icon and spin a refresh glyph while a batch export row
is running.
…lView

Drop the hand-rolled wantsLayer + layer?.backgroundColor handling in
favor of LayerBackedTableCellView's backgroundColor property, matching
the project's layer-backed view conventions.
Fold the two-stage filtered/selection combineLatest into a single map so
cell view models are built in one pass, reading selection state directly
from exportingState.
MachInjector 0.2.0 → 0.3.0, RunningApplicationKit 0.3.2 → 0.3.3,
SwiftyXPC 0.5.100 → 0.5.102. Point swift-mobile-gestalt and
swift-navigation at MxIris-Library-Forks (0.5.0 / 2.8.100) for the
in-progress patches the upstream repos don't carry. Pick up
swift-helper-service 0.1.3 and swift-identified-collections 1.1.1
(transitive from the helper service); drop NSAttributedStringBuilder
which no longer has any callers.
The server-only progress-bearing override for runtimeObjectsInImage was
declaring its payload as a bare String, but every caller —
dispatch(ObjectsInImageRequest:), _remoteObjectsWithProgress, and the
shared handler this override replaces — sends the structured
ObjectsInImageRequest envelope. The mismatch failed every objects(in:)
call against a remote engine with NSCocoaErrorDomain 4864.

Send ObjectsInImageRequest from _remoteObjectsWithProgress and decode it
the same way on the server arm so both objects(in:) and
objectsWithProgress(in:) hit a symmetric wire form.

Also drop the unused `try` on IsImageIndexedRequest.perform — its
underlying _isImageIndexed is non-throwing.
C++ template type names routinely produce display names longer than the
255-byte NAME_MAX on APFS/HFS+, which made the export pipeline throw
NSFileWriteInvalidFileNameError (Cocoa 642) for fully-spelled
std::unordered_map<…> and friends.

Clamp every export file name to 255 UTF-8 bytes, truncating the base on
a Character boundary so the result stays valid UTF-8 and never splits a
multi-byte scalar. Append an FNV-1a-based ~8-hex disambiguator computed
over the *full* base name so two distinct long names that share a
truncated prefix don't collide on disk, and the same object always maps
to the same file name across runs (Hashable.hashValue is per-run seeded
and unsuitable). The suffix (extension / category tag) is never
dropped.
A "succeeded" image whose object count silently included a non-zero
failure tally was indistinguishable from a fully clean export — the row
showed the green checkmark and only the small "N failed" detail tag.

Capture .objectFailed events from RuntimeInterfaceExportReporter into a
new BatchExportingObjectFailure list, thread it through the per-image
outcome, and render partial failures distinctly:
- progress + completion rows switch to an orange triangle icon and an
  orange detail color when failed > 0
- both views attach a multi-line tooltip listing each failed object and
  its error (capped at 50 entries with "…and N more" overflow)

Also pre-flight the image with isImageLoaded / loadImage before
exporting so an unloaded image fails the row up front with a real error
description instead of silently producing zero objects.
Replace the single-shot Back toolbar item with a Previous/Next navigation
group whose visibility, enablement, and target object are derived from
`DocumentState.selectionStack` and a new `selectionIndex` cursor.

Sidebar clicks now `.push` onto the history instead of replacing the
root, so the user can walk backward and forward through every object
they've inspected (browser-style). New `.backward`/`.forward` routes
move the cursor without mutating the array; `.push` from mid-history
truncates the abandoned forward branch first; `.pop` keeps its prior
removeLast semantics for callers that need to shrink the history.

Content/Inspector coordinators now read `selectedRuntimeObject`
(stack[cursor]) instead of `stack.last`, and SidebarRuntimeObjectList's
visual selection follows the cursor so previous/next also re-highlight
the matching sidebar row when the object lives in the current image.
The cell view is no longer Sidebar-specific — moving it into Base keeps
it reachable from non-sidebar surfaces (Open Quickly, popovers) without
crossing module folders.
Replace ad-hoc `.centerY` / `.left` / `.vStackCenter` alignments with
the standard `.center` / `.leading` set so the call sites match the
axis-aware naming the stack view wrappers expose elsewhere.
…encies

Hoist the usingLocalDependencies flag into the LocalSearchPath.package
default so every local checkout no longer has to repeat
isEnabled: usingLocalDependencies, and uncomment the secondary RxAppKit
local path now that the default applies.
…iews

Replace the isReorderable-driven ternary between rx.reorderableNodes
and rx.nodes with the new rx.nodes(options:) overload, keeping the
binding site uniform regardless of whether reordering is enabled.
- Bump MachOSwiftSection exact pin to 0.12.0-beta.2 (specialization fixes)
- Refresh RuntimeViewerCore / RuntimeViewerPackages Package.resolved against
  new dependency tags (RxAppKit 0.4.0, MachOObjCSection 0.7.102,
  FrameworkToolbox 0.7.1, Semaphore 0.1.1, swift-demangling 0.4.1,
  swift-dyld-private 1.2.1, swift-objc-dump 0.8.101).
- Refresh workspace-level Package.resolved for both Debug and Distribution
  workspaces so CI builds against the same pins as the local Xcode session.
- Add Changelogs/v2.1.0-beta.2.md.
Wrap `SelectionRouter` and `EmptyRouteTransitionContext` in DocumentState
behind `#if os(macOS)` since they conform to CocoaCoordinator's
`TransitionContext` (which XCoordinator does not provide on iOS), and gate
the three remaining cross-platform call sites — InspectorRelationships,
ContentText, Sidebar — that still triggered `documentState.selectionRouter`
without a guard.

Wrap `InspectorSwiftSpecializationViewModel` in `#if os(macOS)` because it
addresses `InspectorRuntimeObjectRoute.requestSpecializationSheet`, which
only exists in the macOS branch of that route enum.

Guard the `settings.general.sidebarMaxExpansionDepth` lookup in
SidebarRootViewModel behind `#if canImport(RuntimeViewerSettings)` — the
Settings target is macOS-only, so iOS falls back to `.max`.
…form

`SelectionRouter` no longer conforms to the coordinator framework's
`Router` protocol — its purpose is to mutate `DocumentState` and emit a
route, not perform a UI transition, so the coordinator machinery (and
the `TransitionContext` / `TransitionProtocol` divergence between
CocoaCoordinator and XCoordinator) was buying nothing. It's now a plain
`@MainActor` class with a public `trigger(_:)`. Drop
`EmptyRouteTransitionContext` accordingly.

`RuntimeViewerSettings` was already platform-agnostic in its sources
(only imports Foundation/Observation/MetaCodable/RuntimeViewerCore), so
the `condition: .when(platforms: appkitPlatforms)` on the
`RuntimeViewerApplication` dependency was unnecessarily fencing iOS off
from settings access. Make it an unconditional dependency and drop the
matching `#if canImport(RuntimeViewerSettings)` guards in `ViewModel`.

Drop the `#if os(macOS)` around `InspectorRuntimeObjectRoute` — every
case references only `RuntimeObject` (cross-platform) so the iOS
`typealias InspectorRuntimeObjectRoute = InspectorRoute` workaround is
no longer necessary.

Revert the temporary platform guards added in 5335d1a around
`documentState.selectionRouter`, `settings.general.sidebarMaxExpansionDepth`,
and the whole `InspectorSwiftSpecializationViewModel` — they all build
on iOS now.
…h iOS XCoordinator parity

Revert the standalone `SelectionRouter` class introduced in 6ac542f back
to a real `Router` conformance — macOS keeps its CocoaCoordinator-based
implementation untouched, and iOS gets a parallel XCoordinator-based one
using the equivalent types: `Router<RouteType>` instead of
`Router<Route>`, and `EmptyRouteTransitionContext: TransitionProtocol`
in place of `TransitionContext` (XCoordinator merges the
`TransitionContext.presentables` surface into `TransitionProtocol`).

XCoordinator's `Router` extends `Presentable`, so the iOS branch adds a
deliberately no-op `viewController`/`router(for:)` surface — this router
mutates state and emits a route, it never actually presents anything.

Public surface is unchanged: callers see `any Router<SelectionRoute>`,
so `documentState.selectionRouter.trigger(.push(x))` works the same on
both platforms.
Mx-Iris added 17 commits June 5, 2026 01:40
0fdca62 already eliminated the @Mutex `_modify` coroutine accessor that
bec9f4b's `processReceivedData` snapshot and `RuntimeNetworkConnection`
AsyncStream consumer were guarding against. With the real trigger gone the
guards are dead weight, and the AsyncStream consumer's strictly serial
drain was incidentally compounding latency when a peer echoed handler
errors at high rate.

The v2.1.0-beta.4 sim-side image-loading regression turned out not to be a
channel-layer issue — it was a background-indexing handler echoing
`DyldOpenError` for the peer's own main executable, triggering a peer-error
ping-pong on `sendSemaphore`. That requires a separate fix on the indexing
handler; see v2.1.0-beta.4 changelog known-issue note.
…indexing

If the remote engine serving as the Server side has background indexing on,
the indexer's `LoadImageForBackgroundIndexingRequest` handler raises
`DyldOpenError` on the peer's own main exec, the error response carries no
`identifier`, the peer envelope-decodes it as `keyNotFound`, and both sides
ping-pong on the shared `sendSemaphore` — image-list pushes and progress
events starve out. Workaround: disable background indexing on the
remote-Server side. Two-part fix targeted at the next build.
The old sendRequest held sendSemaphore from wait-to-resume, so a slow
peer handler (e.g. 20 s of section parsing during background indexing)
monopolized the channel and blocked every other send-in-flight at the
local outbox. Stamp a UUID nonce per round trip, key pendingRequests by
nonce, and release the semaphore as soon as the write returns;
concurrent sendRequests now wait on responses in parallel and
identifier-keyed collisions on the routing table are impossible.

Adjacent fix: the receive catch arm was echoing a bare
RuntimeNetworkRequestError carrying no identifier when envelope decode
failed, which caused the peer to also fail decode and echo back — both
sides ping-ponging on the same semaphore and starving every other
request. Split into two arms: decode failures swallow silently;
handler failures echo via the same nonce-keyed envelope used by
successful responses, so the peer's sendRequest routes the error back
to its matching pending entry instead of triggering a re-echo loop.

Wire format is backward compatible: nonce is optional, missing values
fall back to identifier-keyed routing.

See Changelogs/v2.1.0-beta.4.md "Remote-Server background indexing
stalls image loading" for the failure mode this addresses.
Two changes that together let background indexing work on non-local
(XPC / Bonjour / iOS Simulator) engines.

1. _loadImageForBackgroundIndexing now skips dlopen when imageList
   already contains the canonical path. The BFS routinely visits the
   peer's own main executable, which dlopen refuses to re-open by
   definition; on iOS Simulator it also hits system images whose
   canonical path differs from dyld's runtime form due to
   DYLD_ROOT_PATH rewriting, resolving to (no such file) on disk.
   The previous-commit transport hardening makes the spurious
   DyldOpenError no longer fatal, but guarding at source is cheaper.

2. canOpenImage / rpaths / dependencies now dispatch instead of
   reading the client process's dyld directly. The protocol's
   "Pure local check" assumption only ever worked on the local
   engine; remote engines were silently returning empty dependency
   lists from the client-side DyldUtilities.machOImage lookup,
   so BFS stuck at the root and batches only ever indexed the
   main executable instead of the full dependency closure.

Wire-format addition: RuntimeDependencyEntry struct replaces the
tuple-typed dependencies return value at the dispatch seam (tuples
aren't Codable). The manager-facing API still uses tuples and
needs no changes.
…ustom modes

Replace the single `BackgroundMode` toggle with a three-level switch:

- `isEnabled` (master) — overall on/off; when off, neither sub-mode runs.
- `heuristic.isEnabled` — main-executable BFS at document open / engine swap.
- `custom.isEnabled` — always-index list (entries moved into `custom.entries`).

`maxConcurrency` is now shared between sub-modes; `depth` lives under
`heuristic`. The coordinator reconciles each toggle independently in
`handleSettingsChange` and uses the new `cancelBatches(matching:)` Manager
API to scope cancellation to one sub-mode without disturbing the other.

Drop the dyld add-image pump entirely — heuristic discovery now relies
solely on document-open / fullReload triggers, matching the new sub-mode's
documented semantics ("images dlopen'd after the initial sweep are not
auto-indexed").

Popover groups batches by `RuntimeIndexingBatchReason.Category` so
HEURISTIC / ALWAYS INDEX / MANUAL each get their own collapsible header.
Always-index groups flatten one level (entry → item) so single-image
entries render as a flat list instead of an empty intermediate batch row.
`imageNodes` is now a `nonisolated` getter backed by the underlying
`CurrentValueSubject`, with all mutation routed through the existing
actor-isolated `setImageNodes(_:)`. Callers no longer need an `await`
hop just to read the current snapshot.

Use this to:

- Synchronously gate the "Export Multiple Images" menu item in
  `MainWindowController.validateMenuItem` on `imageNodes.count >= 2` —
  `responds(to:)` runs on the main thread and can't suspend.
- Drop the redundant `await` in `BatchExportingCoordinator.loadAvailableImages`.
- Drop the redundant `await` in `RuntimeEngineProxyServer.sendInitialData`.
- `RuntimeMessageChannel`: discard the `yield` result explicitly so the
  compiler doesn't warn about an unused `Discarding` value.
- `RuntimeViewerPackages/Package.swift`: comment out the local-path
  branches for `UXKitCoordinator` and `OpenUXKit` so the repo resolves
  against the OpenUXKit remote unconditionally. The local checkouts those
  pointed at no longer exist for most contributors.
Both bumps are semver-compatible with the existing `from:` pins and
synchronize the declared floor with the latest published tags:

- UIFoundation 0.10.2 adds an opt-out for the visual-effect host on
  `UXKitViewController` and respects safeAreaLayoutGuide on macOS 11+.
- RxAppKit 0.5.0 drives `rx.itemSelected` (NSTableView / NSOutlineView)
  off `selectionDidChangeNotification` so keyboard / type-select
  selection events surface alongside clicks — already relied on by
  the type-select navigation debounce shipped in 28fd4c4.
1.13.0 declares the trait set (Clocks / CombineSchedulers / Foundation /
FoundationNetworking) the resolved graph enables; 1.12.0 doesn't, so
release-channel resolves under USING_LOCAL_DEPENDENCIES=0 fail with
"declares no traits". Catches up the main-branch pin with the floor that
already shipped on the v2.1.0-beta.4 release tag (0e7b9f2).
- Add Changelogs/v2.1.0-beta.5.md.

Clears beta.4's known-issue ping-pong on remote-Server background
indexing (b276703 + 1d9becf), re-rolls the Always Index list that
beta.4 had to revert (453644c stayed on main), and reorganizes
Background Indexing settings into master + heuristic + custom.
Includes the imageNodes nonisolated refactor + UIFoundation /
RxAppKit / swift-dependencies pin catch-up.
Introduce a `@MainActor`-isolated `CellViewModel` base in
RuntimeViewerApplication and rebase the three batch-export row/cell view
models on it, replacing the per-class `NSObject, @unchecked Sendable`
boilerplate. `BatchExportingImageSelectionCellViewModel`'s `Differentiable`
conformance is now declared `@MainActor` to match.

Also refresh Package.resolved across the workspace and per-package
manifests to pick up new dependency revisions.
….plist

- `ITSAppUsesNonExemptEncryption=false` skips the per-build App Store
  Connect export-compliance prompt.
- `NSLocalNetworkUsageDescription` explains the optional Bonjour mirror
  feature; required by iOS before `_runtimeviewer._tcp` discovery can
  prompt for local-network access.
RuntimeEngine no longer caches an XPC listener endpoint or downcasts
its connection to a transport-specific protocol. Announcing the listener
endpoint to the Mach Service is now self-contained in
RuntimeXPCServerConnection — the only place that already knows it is an
XPC server with an endpoint to publish — so neither the engine nor the
injected RuntimeViewerServer entry point needs to mention XPC.

Collapse the ad-hoc bonjourEndpoint: / xpcServerEndpoint: parameters of
connect(...) into a single credential: RuntimeConnectionCredential, so
session-scoped endpoints stop being passed around as untyped
(any Sendable) and the call sites describe their intent (.bonjour /
.xpcServer) directly.

@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 introduces an identityPath property to RuntimeObject and RuntimeObjectKey to uniquely identify nested types under different tree paths, preventing duplicate entries and preserving nested specializations in the sidebar. It also updates layout alignments in several AppKit view controllers and adds corresponding unit tests. Feedback on these changes highlights two issues: first, the parentIdentityPath is not passed to makeRuntimeObject within the specialize method, resulting in flat identity paths for dynamically specialized nested types; second, the newly introduced specializedDefinitionByObject cache is never cleared when the index configuration is updated, which could lead to memory leaks or stale lookups.

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 641 to 648
let specializedDefinition = try await baseTypeDefinition.specialize(
with: result,
typeArgumentNodes: typeArgumentNodes.count == upstreamRequest.parameters.count ? typeArgumentNodes : nil,
derivingNestedSpecializationsWith: specializer,
selection: upstreamSelection,
typeArgumentNodesByParameter: resolved.nodesByParameter,
in: machO
)

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

In specialize(for:with:), when makeRuntimeObject is called to create the specialized RuntimeObject (around line 663), the parentIdentityPath parameter is not provided. This causes the newly created specialized object to have a flat identityPath (just its own mangled name) instead of being nested under the parent's identityPath (object.identityPath).

Since RuntimeObjectKey uses identityPath to distinguish same-named objects under different tree paths, failing to pass parentIdentityPath: object.identityPath breaks the identity of dynamically specialized nested types and can lead to lookup mismatches or duplicate entries in the sidebar.

Please update the makeRuntimeObject call to pass parentIdentityPath: object.identityPath.

// both maps and throw `invalidRuntimeObject`.
private var interfaceByObject: OrderedDictionary<RuntimeObjectKey, RuntimeObjectInterface> = [:]

private var specializedDefinitionByObject: [RuntimeObjectKey: TypeDefinition] = [:]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The new specializedDefinitionByObject cache is populated when specialized types are created, but it is never cleared when the index is re-prepared or updated.

In updateConfiguration(using:transformer:), when newIndexConfiguration.showCImportedTypes != oldIndexConfiguration.showCImportedTypes, interfaceDefinitionNameByObject is cleared, but specializedDefinitionByObject is not. This can lead to a memory leak of stale TypeDefinition objects and potential lookup mismatches. Please ensure specializedDefinitionByObject.removeAll() is called whenever interfaceDefinitionNameByObject is cleared.

@Kyle-Ye Kyle-Ye marked this pull request as ready for review June 9, 2026 12:51
Kyle-Ye and others added 6 commits June 9, 2026 21:55
…isonStep<Self> form

The `ComparableDefinition<Self> { makeComparable { … } }` form requires a
newer FrameworkToolbox than batch-export currently pins, and is unrelated
to the nested-specialization fix that follows. Roll the three call sites
back to the `some ComparisonStep<Self>` form already supported in the
pinned dependency set so the bugfix branch can build against
batch-export's deps without forcing an unrelated SPM bump.
… cell viewmodel

The preceding two commits introduced an identityPath String on every
RuntimeObject to disambiguate the same Swift metadata appearing under
different sidebar paths (e.g. EventListenerPhase.Value<Event> reached
via the inner generic vs. via Phase<Event>). That coupling makes the
data-layer model carry a UI concern and costs ~30-80MB on large indexes
where N RuntimeObject instances each pay for a per-node path String.

Drop identityPath from RuntimeObject and RuntimeObjectKey so the latter
returns to its metadata-identity role. Recreate the disambiguation on
SidebarRuntimeObjectCellViewModel.StableID by folding a recursive parent
fingerprint into the cell's own (imagePath, name, kind). The fingerprint
is not cached: stableID reads it on demand so late-binding parent
changes show up automatically and runtimeObject.children is excluded so
splicing a child does not flip the parent's fingerprint.

The regression scenario still holds — EventListenerPhase.Value<Event>
survives an ancestor EventListenerPhase<PanEvent> specialization, as
covered by the existing ancestorSpecializationPreservesExistingNestedSpecialization
test. The new stableIDDistinguishesSameObjectUnderDifferentParents test
takes over the role of the deleted runtimeObjectKeyDiscriminatesIdentityPath
test by asserting that two cells whose runtimeObject.key matches still
hash to distinct StableIDs when their sidebar parents differ.

Also fix RuntimeObject.withImagePath dropping the properties field —
isGeneric / isSpecialized would otherwise be silently cleared on any
imagePath rewrite.
…cated case

Upstream's `derivingNestedSpecializationsWith` produces specialized
nested children (e.g. `Phase<Event>.Value` from specializing
`Phase<Event>`) whose generic ancestor is `Phase.Value`, not
addressable from the derived parent. `makeRuntimeObject` used to
register these as `.specializedType(unspecialized: self, specialized:
self)`, which is incoherent — `unspecialized` should be the canonical
generic parent, not the same bound typeName.

The two current consumers (`interface(for:)`, `memberAddresses(...)`)
mask this by checking `specializedDefinitionByObject` first, but any
third consumer reading `unspecialized` would hit
`invalidRuntimeObject`.

Introduce `.derivedSpecializedType(TypeName)` as the dedicated case —
docs make the contract explicit (only addressable via
`specializedDefinitionByObject`). Route both existing consumers
through the cache directly.

Also clear `specializedDefinitionByObject` alongside
`interfaceDefinitionNameByObject` on `showCImportedTypes` reconfig,
otherwise the section keeps `TypeDefinition` references no consumer
can reach.
@Mx-Iris Mx-Iris force-pushed the codex/fix-specialization-recursive-description branch from a6b51bd to 9b955da Compare June 10, 2026 07:24
@Mx-Iris Mx-Iris merged commit 7a3f86b into main Jun 10, 2026
@Mx-Iris Mx-Iris deleted the codex/fix-specialization-recursive-description branch June 10, 2026 07:27
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.

Recursive descriptions miss types that require derived specializations Specializing a generic parent drops nested generic specializations

2 participants