Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
fcb9be7
fix(SwiftInterface): support multi-level associated types in specializer
Mx-Iris Apr 29, 2026
c57db57
chore(deps): bump swift-dyld-private to 1.2.0
Mx-Iris Apr 29, 2026
25cc4de
test(SwiftInterface): nest specialization fixtures next to their tests
Mx-Iris Apr 29, 2026
2d01bd2
docs(SwiftInterface): drop completed GenericSpecializer plan
Mx-Iris May 1, 2026
db0effc
docs(SwiftInterface): add GenericSpecializer cleanup design doc
Mx-Iris May 1, 2026
b865fa2
docs(SwiftInterface): add GenericSpecializer cleanup implementation plan
Mx-Iris May 1, 2026
cc2877f
refactor(SwiftInterface): drop unused convertLayoutKind helper
Mx-Iris May 1, 2026
82efb7a
refactor(SwiftInterface): split combined nil-return requirement branch
Mx-Iris May 1, 2026
690e894
feat(SwiftInterface): allow caller-supplied MetadataRequest in specia…
Mx-Iris May 1, 2026
bcef3ea
docs(SwiftInterface): clarify metadataRequest scope on specialize
Mx-Iris May 1, 2026
f931b00
feat(SwiftInterface): merge conditional invertible requirements
Mx-Iris May 1, 2026
849a2d4
feat(SwiftInterface): fail fast on generic candidates with typed error
Mx-Iris May 2, 2026
325a0b1
test(SwiftInterface): pin genericCandidateFailFast to Array/Int
Mx-Iris May 2, 2026
6118e92
feat(SwiftInterface): surface invertible protocols on Parameter
Mx-Iris May 2, 2026
28de281
docs(SwiftInterface): correct invertibleProtocols semantics
Mx-Iris May 2, 2026
e9feca9
fix(SwiftInterface): union invertibleProtocols across requirements
Mx-Iris May 2, 2026
9c473e1
test(SwiftInterface): cover invertibleProtocols nil case and candidat…
Mx-Iris May 2, 2026
8b09bab
test(SwiftInterface): hoist indexer setup into actor-cached init
Mx-Iris May 2, 2026
746a9fd
test(SwiftInterface): drop SharedIndexerCache, build fullIndexer in init
Mx-Iris May 2, 2026
951a413
perf(SwiftInterface): cache merged sub-indexer aggregates
Mx-Iris May 2, 2026
a1fa396
test(SwiftInterface): extract struct fixture lookup helpers
Mx-Iris May 2, 2026
aa07d74
fix(SwiftInterface): correct nested-generic handling in GenericSpecia…
Mx-Iris May 5, 2026
a54b9aa
test(MachOSwiftSection): add three-level nested-generic counter-examples
Mx-Iris May 5, 2026
9e626d5
test(MachOSwiftSection): regen baselines and snapshots for new fixture
Mx-Iris May 5, 2026
4c30b53
fix(SwiftDump): correct cumulative-parent dump for nested generics
Mx-Iris May 5, 2026
9f1fca7
fix(SwiftInterface): address GenericSpecializer code-review findings
Mx-Iris May 5, 2026
d3f731a
docs(SwiftInterface): record GenericSpecializer review and rationale
Mx-Iris May 5, 2026
519eec5
refactor(SwiftInterface): tighten GenericSpecializer API surface and …
Mx-Iris May 6, 2026
dea5165
test(SwiftInterface): broaden GenericSpecializer coverage to enum/cla…
Mx-Iris May 6, 2026
6b4a681
feat(MachOTestingSupport): add ProcessMemory helper for runtime memor…
Mx-Iris May 6, 2026
795fde4
test: relocate non-asserting tests under Tests/IntegrationTests/<modu…
Mx-Iris May 6, 2026
1b1469b
chore(SwiftPM): drop stale IMPLEMENTATION_PLAN.md exclude
Mx-Iris May 6, 2026
a4f402c
fix(SwiftInterface): harden GenericSpecializer validation per audit f…
Mx-Iris May 6, 2026
aeed373
feat(MachOCaches): de-duplicate concurrent cache builds via in-flight…
Mx-Iris May 7, 2026
72c8697
fix(SwiftInterface): surface preflight failures as typed diagnostics …
Mx-Iris May 7, 2026
fdb4310
test(SwiftInterface): reorganize GenericSpecializer tests into nested…
Mx-Iris May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
# Build the project
swift build

# Run all tests
swift test
# Run all tests (skip IntegrationTests — see note below)
swift test --skip IntegrationTests

# Run specific test suites
swift test --filter DemanglingTests
Expand All @@ -27,6 +27,8 @@ swift run swift-section interface /path/to/binary

Requires Swift 6.2+ / Xcode 26.0+.

**Test suite convention:** `Tests/IntegrationTests/` is for the maintainer's manual inspection only — it prints results with no assertions or preconditions. Agents must not run it (use `--skip IntegrationTests` when running the full suite). All other `*Tests` targets have proper assertions and required preconditions, and are safe to run.

## Architecture Overview

This is a Swift library for parsing Mach-O files to extract Swift metadata (types, protocols, conformances). It uses a custom Demangler to parse symbolic references and restore Swift Runtime logic.
Expand Down
18 changes: 15 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ var dependencies: [Package.Dependency] = [
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.9.4"),
.package(url: "https://github.com/MxIris-DeveloperTool/swift-clang", from: "0.2.0"),
.package(url: "https://github.com/MxIris-DeveloperTool/swift-apinotes", from: "0.1.0"),
.package(url: "https://github.com/MxIris-Reverse-Engineering/swift-dyld-private", from: "1.1.0"),
.package(url: "https://github.com/MxIris-Reverse-Engineering/swift-dyld-private", from: "1.2.0"),

// CLI
.package(url: "https://github.com/onevcat/Rainbow", from: "4.0.0"),
Expand Down Expand Up @@ -454,6 +454,9 @@ extension Target {
.target(.SwiftInspection),
.target(.SwiftDump),
.target(.Utilities),
],
exclude: [
"GenericSpecializer/REVIEW_FIXUPS.md",
]
)

Expand Down Expand Up @@ -596,6 +599,14 @@ extension Target {
swiftSettings: testSettings
)

static let MachOCachesTests = Target.testTarget(
name: "MachOCachesTests",
dependencies: [
.target(.MachOCaches),
],
swiftSettings: testSettings
)

static let SwiftInspectionTests = Target.testTarget(
name: "SwiftInspectionTests",
dependencies: [
Expand Down Expand Up @@ -723,11 +734,12 @@ let package = Package(
.RegenerateBaselinesPlugin,

// Testing
.MachOSymbolsTests,
// .MachOSymbolsTests,
.MachOSwiftSectionTests,
.MachOCachesTests,
.SwiftInspectionTests,
.SwiftDumpTests,
.TypeIndexingTests,
// .TypeIndexingTests,
.SwiftInterfaceTests,
.MachOTestingSupportTests,
.IntegrationTests,
Expand Down
118 changes: 89 additions & 29 deletions Sources/MachOCaches/SharedCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,27 @@ open class SharedCache<Storage>: @unchecked Sendable {
memoryPressureMonitor.startMonitoring()
}

/// Per-key state: a finished build (`completed`) or an in-flight build
/// that other callers can join via the promise (`inFlight`). The
/// in-flight marker lets concurrent callers for the same key share one
/// build instead of serializing against the cache lock for the build's
/// entire duration.
private enum Entry {
case completed(Storage)
case inFlight(SharedCacheBuildPromise<Storage>)
}

@Mutex
private var storageByIdentifier: [AnyHashable: Storage] = [:]
private var storageByIdentifier: [AnyHashable: Entry] = [:]

/// Routing decision the cache lock makes on behalf of `storage(...)`:
/// either return a cached value, await someone else's in-flight build,
/// or run the build ourselves under the freshly-installed promise.
private enum Outcome {
case completed(Storage)
case wait(SharedCacheBuildPromise<Storage>)
case build(SharedCacheBuildPromise<Storage>)
}

open func buildStorage(for machO: some MachORepresentableWithCache) -> Storage? {
return nil
Expand All @@ -35,33 +54,24 @@ open class SharedCache<Storage>: @unchecked Sendable {

/// Atomic get-or-build with a caller-provided build closure.
///
/// Unlike `storage(in:)` which uses the overridden `buildStorage(for:)`, this variant
/// lets the caller inject a custom build closure that can capture per-call context
/// (progress continuations, options, etc). The per-call context flows through closure
/// capture, never through shared instance state, so concurrent calls cannot interfere.
///
/// The entire check-build-insert runs inside a single `withLockUnchecked` critical
/// section, guaranteeing atomicity independent of the `_modify` accessor that
/// `@Mutex` may or may not generate for the underlying property.
/// Unlike `storage(in:)` which uses the overridden `buildStorage(for:)`,
/// this variant lets the caller inject a custom build closure that can
/// capture per-call context (progress continuations, options, etc.). The
/// per-call context flows through closure capture, never through shared
/// instance state, so concurrent calls cannot interfere.
///
/// - Note: Known limitation — the `build` closure executes while the global
/// cache lock is held, so a long-running build (for example
/// `SymbolIndexStore.prepareWithProgress`) blocks concurrent cache access for
/// other Mach-O identifiers for the full duration of the build. Acceptable
/// today because concurrent cache construction is rare in practice. See
/// `KNOWN_ISSUES.md` for the tracking entry and the sketch of a per-key
/// promise-based fix.
/// The cache lock is held only long enough to look up the key and either
/// hand back a finished value, attach to an in-flight build, or install
/// our own in-flight marker. The build closure itself executes
/// **outside** the lock, so two callers building entries for different
/// Mach-O identifiers run in parallel; two callers building entries for
/// the same identifier de-duplicate via the in-flight promise.
public func storage<MachO: MachORepresentableWithCache>(
in machO: MachO,
buildUsing build: (MachO) -> Storage?
) -> Storage? {
return _storageByIdentifier.withLockUnchecked { dict -> Storage? in
let key: AnyHashable = machO.identifier
if let existing = dict[key] { return existing }
guard let new = build(machO) else { return nil }
dict[key] = new
return new
}
let key: AnyHashable = machO.identifier
return resolve(key: key) { build(machO) }
}

private var currentIdentifer: ObjectIdentifier {
Expand All @@ -73,12 +83,62 @@ open class SharedCache<Storage>: @unchecked Sendable {
}

open func storage() -> Storage? {
return _storageByIdentifier.withLockUnchecked { dict -> Storage? in
let key: AnyHashable = currentIdentifer
if let existing = dict[key] { return existing }
guard let new = buildStorage() else { return nil }
dict[key] = new
return new
let key: AnyHashable = currentIdentifer
return resolve(key: key) { buildStorage() }
}

/// Shared core for both `storage(in:buildUsing:)` and `storage()`. Holds
/// the cache lock only across the dictionary lookup / marker install and
/// across the post-build dictionary update — the actual `build` call
/// runs unsynchronized so that concurrent builds for distinct keys don't
/// serialize.
///
/// `package`-visible so the in-package test target can exercise the
/// concurrency contract directly without manufacturing a fake
/// `MachORepresentableWithCache` conformer.
package func resolve(key: AnyHashable, build: () -> Storage?) -> Storage? {
let outcome: Outcome = _storageByIdentifier.withLockUnchecked { dict in
if let entry = dict[key] {
switch entry {
case .completed(let storage):
return .completed(storage)
case .inFlight(let promise):
return .wait(promise)
}
}
let promise = SharedCacheBuildPromise<Storage>()
dict[key] = .inFlight(promise)
return .build(promise)
}

switch outcome {
case .completed(let storage):
return storage
case .wait(let promise):
return promise.wait()
case .build(let promise):
let result = build()
_storageByIdentifier.withLockUnchecked { dict in
// Only publish back if our promise is still the in-flight
// marker. `removeAll()` on memory pressure could have
// cleared the dict mid-build, and a fresh caller may have
// installed a different promise; in either case the dict is
// not ours to write — but our promise still has waiters
// attached, so we always call `fulfill(_:)` below.
if case .inFlight(let installed) = dict[key], installed === promise {
if let storage = result {
dict[key] = .completed(storage)
} else {
// Mirror the original behaviour: a build that returns
// `nil` is not cached, so subsequent callers get a
// fresh attempt instead of being permanently stuck on
// the failure.
dict[key] = nil
}
}
}
promise.fulfill(result)
return result
}
}
}
53 changes: 53 additions & 0 deletions Sources/MachOCaches/SharedCacheBuildPromise.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Foundation

/// Sync rendezvous used by ``SharedCache/storage(in:buildUsing:)`` to share
/// one in-flight build between concurrent callers for the same identifier.
///
/// The first caller installs an instance of this promise as the cache's
/// in-flight marker for the key, releases the cache lock, and runs the build
/// closure unsynchronized. Concurrent callers for the same key find the
/// in-flight marker, release the cache lock, and block in ``wait()`` on
/// `NSCondition`. The builder calls ``fulfill(_:)`` once the work is done,
/// `broadcast()` wakes every waiter at once, and `wait()` returns the same
/// value to all of them.
///
/// `final class` because the same instance must be referenced from the cache
/// dictionary and from every waiter; `NSCondition` also requires a stable
/// memory address. `@unchecked Sendable` because the synchronization is
/// provided by `NSCondition`, not by Swift's data-race detector.
@_spi(Internals)
public final class SharedCacheBuildPromise<Value>: @unchecked Sendable {
private let condition = NSCondition()
/// Outer optional encodes "fulfilled yet?"; inner optional matches
/// `(MachO) -> Storage?` — a build that returned `nil` is a valid
/// terminal state, distinct from "still pending".
private var result: Value??

public init() {}

/// Blocks until ``fulfill(_:)`` is called and returns whatever the builder
/// produced. Safe to call from any thread.
public func wait() -> Value? {
condition.lock()
defer { condition.unlock() }
while result == nil {
condition.wait()
}
// `result` is `.some(.some(v))` or `.some(.none)`; flatten the outer
// optional we used as the "fulfilled" tag back to the inner Storage?.
return result.flatMap { $0 }
}

/// Records the result and wakes every waiter. Idempotent — a second call
/// is silently ignored, which matters when memory pressure clears the
/// cache mid-build and a fresh builder installs a new promise: the old
/// builder's `fulfill` call still has to land for its already-blocked
/// waiters, but mustn't disturb the new promise.
public func fulfill(_ value: Value?) {
condition.lock()
defer { condition.unlock() }
if result != nil { return }
result = .some(value)
condition.broadcast()
}
}
19 changes: 19 additions & 0 deletions Sources/MachOFixtureSupport/Baseline/BaselineFixturePicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,25 @@ package enum BaselineFixturePicker {
// relationship can be walked back) would let those Suites use a
// live carrier.

/// Picks the SymbolTestsCore struct
/// `NestedGenerics.NestedGenericThreeLevelConstraintTest.MiddleConstrainedTest.InnerMostConstrainedTest<InnerMost: Comparable>`
/// — the deepest layer of a three-level nested generic context where each
/// outer scope adds its own protocol constraint. This is the canonical
/// counter-example for the cumulative-parent-vs-newly-introduced
/// distinction in `TargetGenericContext`: at depth ≥ 2,
/// `parentParameters.flatMap.count` and `parentParameters.last?.count`
/// diverge, exposing the bug where `currentRequirements` would otherwise
/// over-drop entries inherited from cumulative parent levels.
package static func struct_NestedThreeLevelInnerMostConstrainedTest(
in machO: some MachOSwiftSectionRepresentableWithCache
) throws -> StructDescriptor {
try required(
try machO.swift.typeContextDescriptors.compactMap(\.struct).first(where: { descriptor in
try descriptor.name(in: machO) == "InnerMostConstrainedTest"
})
)
}

/// Picks the SymbolTestsCore struct
/// `GenericValueFixtures.FixedSizeArray<let N: Int, T>` — a generic
/// type that declares one integer-value parameter (`N`) and one
Expand Down
15 changes: 14 additions & 1 deletion Sources/MachOSwiftSection/Models/Generic/GenericContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,21 @@ public struct TargetGenericContext<Header: GenericContextDescriptorHeaderProtoco
return .init(parameters.dropFirst(inheritedCount))
}

/// Requirements newly introduced at this scope (excludes those inherited
/// from parent generic contexts).
///
/// `requirements` is cumulative across the entire parent chain — the
/// Swift compiler emits `sig->getRequirementsWithInverses` for the full
/// canonical signature in scope (see `swift/lib/IRGen/GenMeta.cpp:7342`).
/// Each entry of `parentRequirements` is similarly cumulative for its
/// level, so the immediate parent's count (`parentRequirements.last?.count`)
/// is exactly the number of inherited requirements to drop. Using
/// `parentRequirements.flatMap { $0 }.count` instead would double-count
/// inherited entries at depth ≥ 2 and silently drop the requirements
/// introduced at this level. This mirrors the formula used by
/// `currentParameters`.
public var currentRequirements: [GenericRequirementDescriptor] {
.init(requirements.dropFirst(parentRequirements.flatMap { $0 }.count))
.init(requirements.dropFirst(parentRequirements.last?.count ?? 0))
}

public var currentTypePacks: [GenericPackShapeDescriptor] {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import Foundation
import Testing
import MachO
@_spi(Internals) @testable import MachOSymbols
@testable import MachOTestingSupport
import MachOFixtureSupport

public enum ProcessMemory {
public enum Metric {
Expand Down Expand Up @@ -82,14 +77,3 @@ public enum ProcessMemory {
return result == KERN_SUCCESS ? info : nil
}
}

@Suite
final class SymbolIndexStoreTests: MachOImageTests {

override class var imageName: MachOImageName { .SwiftUI }

@Test func main() async throws {
SymbolIndexStore.shared.prepare(in: machOImage)
ProcessMemory.report()
}
}
Loading
Loading