diff --git a/.github/workflows/macOS.yml b/.github/workflows/macOS.yml index 13da9601..e6a10e1d 100644 --- a/.github/workflows/macOS.yml +++ b/.github/workflows/macOS.yml @@ -40,7 +40,7 @@ jobs: uses: actions/cache@v4 with: path: Tests/Projects/SymbolTests/DerivedData - key: symboltests-${{ matrix.xcode-version }}-${{ hashFiles('Tests/Projects/SymbolTests/**/*.swift', 'Tests/Projects/SymbolTests/**/*.pbxproj') }} + key: symboltests-${{ matrix.xcode-version }}-${{ hashFiles('Tests/Projects/SymbolTests/**/*.swift', 'Tests/Projects/SymbolTests/**/*.pbxproj', '.github/workflows/macOS.yml') }} - name: Build SymbolTestsCore fixture working-directory: ${{ github.workspace }} @@ -53,32 +53,43 @@ jobs: -derivedDataPath Tests/Projects/SymbolTests/DerivedData \ -destination 'generic/platform=macOS' \ -quiet \ - CODE_SIGNING_ALLOWED=NO \ ARCHS=arm64 \ + CODE_SIGN_IDENTITY=- \ + CODE_SIGNING_REQUIRED=NO \ build - - name: Normalize SymbolTestsCore fixture path + - name: Normalize fixture framework paths run: | set -eo pipefail - expected="Tests/Projects/SymbolTests/DerivedData/SymbolTests/Build/Products/Release/SymbolTestsCore.framework" - echo "--- find SymbolTestsCore.framework under DerivedData ---" - find Tests/Projects/SymbolTests/DerivedData -name "SymbolTestsCore.framework" -type d 2>/dev/null || true - if [ -d "$expected" ]; then - echo "Framework already at expected path; nothing to do." - exit 0 - fi - # Prefer a Release build product (skip Intermediates and Debug). - found=$(find Tests/Projects/SymbolTests/DerivedData -path '*/Build/Products/Release/SymbolTestsCore.framework' -type d 2>/dev/null | head -1) - if [ -z "$found" ]; then - echo "ERROR: SymbolTestsCore.framework not found at any Build/Products/Release/ path." - echo "--- DerivedData top 3 levels ---" - find Tests/Projects/SymbolTests/DerivedData -maxdepth 3 -type d - exit 1 - fi - mkdir -p "$(dirname "$expected")" - ln -sfn "$(realpath "$found")" "$expected" - echo "Linked $found -> $expected" - ls -la "$expected/Versions/A/SymbolTestsCore" + # The tests look for the frameworks under DerivedData/SymbolTests/..., + # while xcodebuild writes them to DerivedData/Build/Products/Release. + # Link every framework the tests need, not just SymbolTestsCore: + # DependencyClosureTests resolves SymbolTestsHelper through the same + # expected path, and omitting it fails that suite with + # "The file SymbolTestsHelper doesn't exist". + root="Tests/Projects/SymbolTests/DerivedData" + expected_dir="$root/SymbolTests/Build/Products/Release" + echo "--- frameworks found under DerivedData ---" + find "$root" -path '*/Build/Products/Release/*.framework' -maxdepth 6 -type d 2>/dev/null || true + mkdir -p "$expected_dir" + for framework in SymbolTestsCore SymbolTestsHelper; do + expected="$expected_dir/$framework.framework" + if [ -d "$expected" ] && [ ! -L "$expected" ]; then + echo "$framework already at expected path; nothing to do." + continue + fi + # Prefer a Release build product (skip Intermediates and Debug). + found=$(find "$root" -path "*/Build/Products/Release/$framework.framework" -type d 2>/dev/null | head -1) + if [ -z "$found" ]; then + echo "ERROR: $framework.framework not found at any Build/Products/Release/ path." + echo "--- DerivedData top 3 levels ---" + find "$root" -maxdepth 3 -type d + exit 1 + fi + ln -sfn "$(realpath "$found")" "$expected" + echo "Linked $found -> $expected" + ls -la "$expected/Versions/A/$framework" + done - name: Upload xcodebuild logs on failure if: failure() @@ -106,4 +117,4 @@ jobs: -c ${{ matrix.configuration }} \ --enable-experimental-prebuilts \ --build-path .build-test-${{ matrix.configuration }} \ - --filter '\.(SymbolTestsCoreDumpSnapshotTests|SymbolTestsCoreInterfaceSnapshotTests|SymbolTestsCoreCoverageInvariantTests|STCoreE2ETests|STCoreTests|GenericSpecializationTests|MultiPayloadEnumTests|MetadataReaderDemanglingTests)(/|$)' + --filter '\.(SymbolTestsCoreDumpSnapshotTests|SymbolTestsCoreInterfaceSnapshotTests|SymbolTestsCoreCoverageInvariantTests|STCoreE2ETests|STCoreTests|GenericSpecializationTests|MultiPayloadEnumTests|MetadataReaderDemanglingTests|DependencyLoadNameTests|DependencyClosureTests|FileDependencyLocatorTests|SwiftInterfaceBuilderDependenciesTests|ManglingPrefixTests|MethodDescriptorTests|MethodOverrideDescriptorTests|MethodDefaultOverrideDescriptorTests|ProtocolRequirementTests|ResilientWitnessTests|LargeStackTaskExecutionTests|BoundedConcurrentMapTests|SwiftEvolutionInterfaceBuilderTests|EvolutionCommandValidationTests|DiffCommandValidationTests)(/|$)' diff --git a/AGENTS.md b/AGENTS.md index 28b7d02f..723bb459 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,14 +67,30 @@ swift-section (CLI) └── SwiftDeclaration (shared declaration model) └── SwiftDump └── SwiftInspection - └── MachOSwiftSection - └── MachOFoundation - └── MachOSymbols, MachOPointers + └── MachOSwiftSection (ABI model — depends on MachOBase ONLY) + └── MachOBase (umbrella: reading / resolving / pointers) + └── MachOPointers └── MachOReading, MachOResolving - └── MachOCaches - └── MachOKitExtensions (external), MachOKit (external) + └── MachOKitExtensions (external), MachOKit (external) + +SwiftInspection and everything above it also import + MachOFoundation (umbrella = MachOBase + MachOSymbols + MachODependencies) + └── MachOSymbols (symbol index + demangling), MachODependencies + └── MachOCaches, MachOBase ``` +The ABI model is **self-contained** (evolution proposal `self-contained-abi-layer`): +`MachOSwiftSection` reaches neither `MachOSymbols` nor `Demangling`. Descriptors +expose an implementation's **offset / context address** (`implementationOffset`, +`implementationAddress(in:)`, `defaultImplementationOffset`); attributing symbol +names to that offset is `SwiftInspection`'s `implementationSymbols(in:)` / +`defaultImplementationSymbols(in:)` extension, one layer up. The symbol *value* +types (`Symbol`, `Symbols`, `SymbolOrElement`) live in `MachOResolving`, the +bind-aware `SymbolOrElementPointer` in `MachOPointers`; the index-backed lookup +(`symbols(offset:)`, `Symbol.resolve(from:in:)`, `resolvesSymbolUsingIndexStore`) +stays in `MachOSymbols`. A module that needs the index imports `MachOFoundation` +(or `MachOSymbols`) itself — `MachOSwiftSection` no longer re-exports it. + `SwiftLayout` (static field-offset engine) is a peer that depends on `SwiftInspection` + `MachOSwiftSection` (+ `MachOObjCSection` for ObjC-ancestor instance sizes). It backs the static ABI-analysis path and is consumed by @@ -104,19 +120,22 @@ See [Documentations/Internal/FieldLayoutRendererReaderSpecialization.md](Documen - `MachOFile.Swift` / `MachOImage.Swift` - Entry point via `.swift` property - Models for descriptors: `TypeContextDescriptor`, `ProtocolDescriptor`, `ProtocolConformanceDescriptor` - Relative pointer resolution for Swift's position-independent metadata +- **Self-contained** (evolution proposal `self-contained-abi-layer`): depends on `MachOBase` only — no symbol index, no demangler. The five implementation-pointer descriptors (`MethodDescriptor`, `MethodOverrideDescriptor`, `MethodDefaultOverrideDescriptor`, `ProtocolRequirement`, `ResilientWitness`) carry a `RelativeDirectRawPointer` and expose `implementationOffset: Int?` (pure pointer arithmetic, `nil` for a null pointer) plus `implementationAddress(in context:)`; the former `RelativeDirectPointer` fields made a plain ABI read build the whole image's demangled symbol index on first touch, and their `ReadingContext` leg — with no symbol service to consult — read the implementation's machine code as a `Symbols` value. Symbol attribution is `SwiftInspection`'s `implementationSymbols(in:)` extensions. The mangling-prefix check (`hasSwiftManglingPrefix` / `strippingSwiftManglingPrefix`) and the `__C` / `__C_Synthesized` module names (`CImportedModuleNames`) are local copies of demangler facts, pinned equal by `ManglingPrefixTests`. See [Documentations/Internal/SelfContainedABILayer.md](Documentations/Internal/SelfContainedABILayer.md). **SwiftDump** - High-level type wrappers - `Struct`, `Enum`, `Class`, `Protocol`, `ProtocolConformance`, `AssociatedType` - `DemangleResolver` - Resolves mangled names using the Demangler +- **Vtable slot attribution** (evolution proposal `vtable-slot-attribution-via-method-descriptor-symbols`): which member a vtable slot belongs to comes from the method descriptor's own `Tq` symbol (`SwiftInspection`'s `attributedMemberNode(in:)`), NOT from the symbols at its implementation address. Identical code folding merges byte-identical bodies onto one address, so that mapping has no inverse — SwiftUICore's empty-`ret` address carries 2878 symbols, and `SwiftUI.GraphHost`'s four folded vtable methods used to print as one right name plus three coroutine resume functions of its NESTED `GraphHost.Data` struct. A `Tq` symbol is a per-member data symbol at the descriptor's own address, immune to folding (proposal 0006 already used it as negative evidence for `final`; this is the same fact used positively). The implementation-address route stays as the fallback for the ~28% of slots carrying no `Tq` symbol, and when that fallback runs against a folded address the dump says so (`// Attribution: ambiguous — N symbols folded at this address`) instead of presenting a guess as fact — though `N` is currently the raw folded-symbol count rather than the number of candidates that actually belong to this type, a defect due to be corrected in the same fix batch. A null implementation means **dead-method elimination removed the implementation body** while the slot stayed for layout: IRGen's `buildMethodDescriptorFields` writes the relative address when the SIL vtable has an entry and null when it does not, and null is its only other branch — so the marker is the compiler's, not this library's inference. The declaration itself survives (source, `Tq` symbol and descriptor all remain); what is gone is the body, which is why the comment must not read as "the API was deleted". It happens to members that are `internal` or narrower (`private`, `fileprivate`, function-local types — `package` is still an anchor) under whole-module optimization: a `public` type's undecorated `init()` is `internal`, hence not a dead-function-elimination anchor. Outside whole-module builds `internal` remains an anchor, so this is essentially a Release-build phenomenon — and a common one, not an exotic one (measured: 341 such bind SITES in SwiftUICore and 11680 in Xcode's SourceEditor against 4894 `Tq` symbols; sites exceed deleted methods because a subclass's metadata re-emits the inherited slots). **Static** class metadata fills those slots with `swift_deletedMethodError` (calling one traps); the discriminator is `ClassLayoutFlags::HasStaticVTable`, which IRGen sets for the Singleton / Update / FixedOrUpdate strategies alike, so only generic classes and the Resilient strategy rebuild a vtable at runtime — and there `initClassVTable` copies the descriptor's null through unchanged. The slot renders as `// No implementation in this image (deleted method — slot retained for ABI)` above the name the `Tq` symbol still supplies; that wording overstates the cause and is due to be replaced in the fix batch tracked by `Roadmaps/2026-09-06-pr123-review-findings.md`. The same evidence order is mirrored in `TypeDefinition.index`, so the interface path's vtable-offset comments agree. Candidate matching in the fallback goes through `NodeReference.declarationContextNode` (the member's DIRECT declaration context), never `first(of: .class)` — the latter finds the first class node anywhere in the tree and so accepts a nested type's member as the enclosing class's own. **Protocol-side attribution is deliberately untouched**: `ResilientWitness` / `ProtocolRequirement` have no per-member descriptor symbol, and a context-based match drops their non-member symbols outright (measured: 1033 lines of SwiftUICore protocol output degraded to `[Stripped Symbol]`). Pinned by `VTableSlotAttributionTests` (on-the-fly fixture forced to fold with `-Xlinker -deduplicate`) and `GraphHostVTableAttributionTests` (the reported binary, simulator-runtime-gated). The interface generation is split into layered peer modules over a shared `SwiftDeclaration` base model (`SwiftInterface` orchestrates them): **SwiftDeclaration** - Shared declaration model (base layer for the Swift* modules) - `TypeDefinition`, `ProtocolDefinition`, `ExtensionDefinition`, `FunctionDefinition`, names, kinds, `DefinitionBuilder` - The model retains **descriptor references, not parsed wrappers** (evolution proposal 0002): `TypeDefinition.typeContextDescriptorWrapper`, `ExtensionDefinition.protocolConformanceDescriptor`, `ProtocolDefinition.protocolDescriptor`. The full wrappers (`TypeContextWrapper` / `ProtocolConformance` / `Protocol`, trailing objects included) are rebuilt on demand via `materializedTypeContext(in:)` / `materializedProtocolConformance(in:)` / `materializedProtocol(in:)`. **Materialization discipline**: at most one materialization per operation (index it / print it / specialize it), threaded through as a local variable; never a per-access computed property, and the result is never cached on the definition — caching would re-accumulate, in browse order, the memory the slimming reclaimed. `DeclarationModelInstanceSizeTests` pins the instance-size ceilings. -- `SwiftIndexEvents` - event namespace (Payload/Dispatcher/Handler) emitted by both indexer and printer. **Library code never writes to a process stream** (evolution proposal 0005): every degradation — a dropped definition, a skipped descriptor, a dependency that would not load — is dispatched as an event, and the *host* decides where it lands (a GUI attaches `OSLogEventHandler`, the CLI attaches `ConsoleEventHandler`, which reports on **stderr**; stdout carries the generated Swift / JSON, so writing there corrupts the product output — issue #102). `Dispatcher.dispatch` has a **floor**: with no handler attached it reports failures through `#log` rather than dropping them, so forgetting to attach a sink degrades to "somewhere findable" instead of silence. What counts as a failure is `Payload.unhandledFailureDescription`, an exhaustive `switch` on purpose — a new failure case must opt *in* explicitly, since the alternative silently escapes the floor. Two modules sit below the event layer and cannot reach it (`SwiftDeclaration` depends on `SwiftDeclarationRendering`, so naming the event types there would cycle): `Node+OpaqueType` takes an injected `OpaqueTypeDegradationReporter` closure, `MultiPayloadEnumDescriptorCache` logs directly; both land on the same `#log` floor. Never use `FileHandle.standardError/Output.write(_:)` — that overload raises an uncatchable ObjC exception on a closed or broken stream and aborts the host; use `fputs` / `fwrite` (`write(contentsOf:)` needs macOS 10.15.4, above this package's 10.15 floor). Pinned by a source scan (`PrintFailureEventTests.libraryModulesWriteToNoProcessStream`) carrying an explicit, shrink-only list of baseline offenders. See [Documentations/Internal/EventBasedDegradationReporting.md](Documentations/Internal/EventBasedDegradationReporting.md) +- `SwiftIndexEvents` - event namespace (Payload/Dispatcher/Handler) emitted by both indexer and printer. **Library code never writes to a process stream** (evolution proposal 0005): every degradation — a dropped definition, a skipped descriptor, a dependency that would not load — is dispatched as an event, and the *host* decides where it lands (a GUI attaches `OSLogEventHandler`, the CLI attaches `ConsoleEventHandler`, which reports on **stderr**; stdout carries the generated Swift / JSON, so writing there corrupts the product output — issue #102). `Dispatcher.dispatch` has a **floor**: with no handler attached it reports failures through `#log` rather than dropping them, so forgetting to attach a sink degrades to "somewhere findable" instead of silence. Handler invocation is **serialized process-wide** (one recursive lock across every dispatcher, evolution proposal `large-stack-executor-and-cross-version-parallelism`): `Handler` has no `Sendable` requirement and cross-version preparation shares one host handler across N dispatchers on N tasks, so the lock is what keeps a stateful handler single-threaded — delivery ORDER across dispatchers is still first-come. `ConsoleEventHandler(label:)` prefixes `[label]` for that same reason (the CLI tags `old` / `new` and each evolution version). What counts as a failure is `Payload.unhandledFailureDescription`, an exhaustive `switch` on purpose — a new failure case must opt *in* explicitly, since the alternative silently escapes the floor. Two modules sit below the event layer and cannot reach it (`SwiftDeclaration` depends on `SwiftDeclarationRendering`, so naming the event types there would cycle): `Node+OpaqueType` takes an injected `OpaqueTypeDegradationReporter` closure, `MultiPayloadEnumDescriptorCache` logs directly; both land on the same `#log` floor. Never use `FileHandle.standardError/Output.write(_:)` — that overload raises an uncatchable ObjC exception on a closed or broken stream and aborts the host; use `fputs` / `fwrite` (`write(contentsOf:)` needs macOS 10.15.4, above this package's 10.15 floor). Pinned by a source scan (`PrintFailureEventTests.libraryModulesWriteToNoProcessStream`) carrying an explicit, shrink-only list of baseline offenders. See [Documentations/Internal/EventBasedDegradationReporting.md](Documentations/Internal/EventBasedDegradationReporting.md) **SwiftIndexing** - Builds the `SwiftDeclaration` model from a Mach-O image +- `SwiftDeclarationIndexer.prepare()` runs its body on the demangler's large-stack task executor through `LargeStackTaskExecution.run` (evolution proposal `large-stack-executor-and-cross-version-parallelism`; see the `MachOSymbols` entry) — as do `SwiftInterfaceBuilder.prepare()` / `printRoot()`, `SwiftDiffableInterfaceBuilder.prepare()`, the evolution builder's `prepare` / render entries, the diff renderer's two entries, the printer's four per-definition entries and the six `Dumpable.dump(using:in:)` conformers. Output is independent of the executor; the wrap is about where the task runs, not what it produces - `SwiftDeclarationIndexer` - Indexes types, extensions, conformances. Its `deinit` cleans up the three per-image caches (symbol store, interned-name store, demangle memo) via `PerImageCacheEvictionRegistry`, under two rules: eviction is claimed **per CACHE** (each of the three sampled separately at `prepare()`, by whichever indexer's `prepare()` found it absent and therefore built it) and performed by the image's LAST live indexer, so an earlier-deinitializing indexer never wipes the caches out from under a live sibling. The per-cache split matters because only the symbol store is necessarily an indexer's: the interned-name store and the demangle memo are also populated by SwiftLayout, `SwiftDeclarationRendering` and `SwiftSpecialization` through `MetadataReader`, so a "dump, then build the interface" sequence fills those two with no symbol store at all — a single claim sampled from the symbol store alone read that state backwards and evicted live non-indexer work's caches. Registration is keyed on indexer identity (`ObjectIdentifier`), not counted, so a concurrent double `prepare()` (its `isPrepared` guard is a plain check-then-set on an async entry point) cannot strand the population above zero and leak all three caches for the process lifetime. Entries built by non-indexer callers are never claimed and never evicted (pinned by `PerImageCacheEvictionTests`, including `indexerDoesNotEvictCachesItDidNotBuild`) - The section-wrapper populations the index passes consume (`types` / `protocols` / `protocolConformances` / `associatedTypes` and the parsed-value keyed conformance maps) are **indexing transients** since proposal 0002 — released when `prepare()` finishes, with no public projection. The retained conformance facts are the name-level maps `conformingProtocolNamesByTypeName` / `conformingTypesByProtocolName` (+ their merged `all*` variants), which is all any post-indexing consumer (including `SwiftSpecialization`'s `ConformanceProvider`) reads. - `SwiftIndexEventReporter`, `OSLogEventHandler`, `ConsoleEventHandler` - event handlers @@ -133,17 +152,18 @@ The interface generation is split into layered peer modules over a shared `Swift - Instance members print the recovered `final` keyword (evolution proposal 0006, `isFinal` on the member definitions + `FieldDefinition`) — the mirror image of the `class`/`static` fact: inside a non-actor class whose vtable header is readable, a member with no vtable method descriptor was declared `final`, gated four ways so the keyword is never wrong (accessor symbols must actually have joined; `@objc` without a descriptor means `@objc dynamic`, objc_msgSend-dispatched and overridable, so excluded; stored `let`s are not overridable and stay unmarked; a member name with a `Tq` method-descriptor symbol provably has a vtable entry and is never marked — the negative evidence that survives identical-code-folding, where SourceEditor folds 1128 empty implementations onto one address and the descriptor→symbol join cannot pair them, pinned by the Xcode-gated `FinalKeywordICFRegressionTests`). The stored-`var` accessor groups `DefinitionBuilder` used to discard on field-name dedup are folded back onto `FieldDefinition.accessors`, which also gives stored `var`s their getter/setter vtable-offset comments (under `--emit-vtable-offsets`) and lazy fields their caller-facing getter type (`lazy var x: String`, not the `Optional` storage type; the dump path deliberately keeps the storage truth). The descriptor→symbol join keys through `memberJoinKey`, which strips the `Tu` async-function-pointer marker — before that fix async members never joined (no vtable comment, missing `override`, and a would-be false `final`). Members of a `final` class get member-level `final` (class-level `final` has no ABI bit; the init's vtable entry keeps the header present), which is dispatch-honest and exactly what a reconstruction needs to link. See [Documentations/Internal/FinalKeywordAndLazyAccessorTypeRecovery.md](Documentations/Internal/FinalKeywordAndLazyAccessorTypeRecovery.md). - Specialized definitions (`TypeDefinition.isSpecialized`) render **bound**: the header prints the concrete-argument name (`Box`, generic-signature clause skipped) via `BoundDumpedTypeNameRenderer`, and each field's type node is substituted through the specialized runtime metadata via `SpecializedMetadataNodeSubstitution` — both live in `SwiftDeclarationRendering` so the dump path (`TypedDumper`, which keeps its own copies/forwarders) stays independent. See [Documentations/Internal/SpecializedInterfaceBoundRenderingRestoration.md](Documentations/Internal/SpecializedInterfaceBoundRenderingRestoration.md). - Interface header + export-status annotations (evolution proposal 0008, both flag-gated and default-off so default output stays byte-identical): `InterfaceHeaderInfo`/`InterfaceHeaderBlock` (public — RuntimeViewer's per-type export bypasses `printRoot`, so the header is a standalone component; generator identity is caller-supplied, the optional date defaults to absent for snapshot byte-stability, and the library-evolution line words the dispatch-thunk count as "detected / not detected", never an assertion) render ahead of `ImportsBlock` when `SwiftInterfaceBuilderConfiguration.interfaceHeaderInfo` is set; `--emit-export-status` prints `// not exported` on members none of whose symbols have an export-trie entry, via `isExportedIncludingDerivedSymbols` — the bare name is NOT sufficient evidence (a library-evolution build keeps implementation symbols local while exporting the `Tj` dispatch thunk, so the bare query flags every public member), and `override` / `@objc` members are exempt (reachable through the parent's thunk / objc_msgSend with zero own exported symbols; conformance witnesses deliberately are NOT exempt — their zero-export fact honestly means "not statically callable"). See [Documentations/Internal/InterfaceHeaderAndExportStatusAnnotations.md](Documentations/Internal/InterfaceHeaderAndExportStatusAnnotations.md). +- Exported-only filtering (evolution proposal 0016; `SwiftDeclarationPrintConfiguration.printExportedDeclarationsOnly`, CLI `swift-section interface --exported-only`, default off) is the annotation's FILTERING counterpart, print-time only (the indexed model stays complete; diff / evolution / RuntimeViewer browsing unaffected; `dump` out of scope). Same fact, same rule: a declaration is dropped only on a definitive `false`, every `nil` keeps it. Types / protocols are ruled by their descriptor symbol (`…Mn` / `…Mp`) — FIRST the symbol found AT the descriptor's offset (the compiler's own spelling), and only when none exists by a remangled name against the trie, which refuses `.extension` contexts: a public type nested in a constrained extension mangles only the extension's own requirement while the model's name node carries the full signature, so the remangle-only first version dropped an exported type (pinned by `publicTypeNestedInConstrainedExtensionIsKept`). Members reuse `exportVerdict(forSymbolNames:)` with the `override` / `@objc` exemptions; stored fields use their accessor group (no accessors ⇒ not checkable ⇒ kept); enum cases are never filtered. Extensions have no descriptor symbol: they drop when their target or conforming protocol is an in-image non-exported declaration per `ExportFilterScope`, which `printRoot()` builds from the indexer's complete tables — a stripped image has NO symbol for a non-exported type, so a symbol-derived "in-image" test would keep every extension of a dropped private type; hosts bypassing `printRoot` call `installExportFilterScope(types:protocols:)` themselves or the extension leg fails open. An emptied plain extension is dropped whole, an emptied conformance extension keeps its clause as `{}` (the `.swiftinterface` shape for synthesized conformances). The three print entries are split into a non-builder filter shell + `printIncluded…` builder body (result builders cannot early-return); a filtered definition emits no print events, an emptied extension emits a paired start/completion around empty output; `renderModelFields` pre-selects the rendered fields so survivors keep their ORIGINAL record index. Known: references to dropped types are not rewritten. See [Documentations/Internal/ExportedOnlyInterfaceFiltering.md](Documentations/Internal/ExportedOnlyInterfaceFiltering.md). - The main interface path's stored-field / enum-case rendering (`renderModelFields` → `printThrowingField` / `printThrowingEnumCase`) carries the **pre-leaf-migration error contract**: record reads, metadata comments, and type printing propagate errors (a failing field fails the whole type), and an enum case's payload presence follows the field record's mangled type name (captured at index time as `FieldFlags.hasMangledTypeName`, so rendering never re-reads the record positionally) — a `Void` payload prints `case a()` exactly like the dump path, while a payload whose node *renders empty* degrades to the bare case in both paths (`case a()` around nothing is invalid Swift; the interface printers also render kind-9 accessor-function symbolic references as the honest `accessor function at ` fallback — see [Documentations/Internal/AccessorFunctionReferenceRendering.md](Documentations/Internal/AccessorFunctionReferenceRendering.md)). At the TOP level the contract inverts: `printRoot` (and `printThrowingProtocol`'s trailing default-implementation extensions) catch per definition — one type/protocol/extension whose printing throws drops only itself, never its whole block (a block-level catch once blanked every type of a legacy binary's interface; pinned by `LegacyDyldInfoBindTests`). The same per-definition contract extends into the NESTED children loops (`printTypeDefinition` / `printExtensionDefinition`): a nested child whose printing throws drops only itself, never the enclosing definition (pinned by `corruptNestedChildDropsOnlyItself`). The diff renderer's `printField` / `printEnumCase` keep their own per-member-catch, rendered-text-gating contract (that is their original design, needed for standalone `+`/`-` members). The shared comment engine's `FieldLayoutRenderer.storedFieldComments` / `enumCaseComments` are `throws` for the same reason, and multi-payload enum descriptors resolve through `MultiPayloadEnumDescriptorCache` in `SwiftDeclarationRendering` (built once per image as a *partial* map — one bad descriptor only degrades its own enum to the tagged projection). See [Documentations/Internal/LeafMigrationRegressionFixes.md](Documentations/Internal/LeafMigrationRegressionFixes.md). **SwiftSpecialization** - Runtime generic specialization (see implementation plan below) - `GenericSpecializer`, `ConformanceProvider` - `TypeDefinition` specialization behavior (`specialize(...)`, `specializedChildren`) -**SwiftInterface** - Thin orchestrator tying indexing + printing into a full interface dump -- `SwiftInterfaceBuilder` - Main builder, call `prepare()` then `printRoot()` -- `.swiftinterface` file types (`SwiftInterfaceFile`, `SwiftInterfaceParser`, …) -- `InterfaceUnionWalker` - the SHARED structure walk behind both comparison renderers (evolution proposal draft-unify-interface-renderers): N versions erased as `[any InterfaceVersionRendering]` (`InterfaceVersionUnit` = per-version `SwiftDiffableInterfaceBuilder` + a printer sharing its dispatcher) in, block-grouped lines out. The walker owns the STRUCTURE — matching and union ordering (newest version's order as spine, absent declarations appended from their last-carrying version; keys first-wins per side, EMISSION INCLUDED, mirroring `ABIDiffer.keyed` — a later same-keyed element is never emitted twice, which the header-failure tests' replace-not-append injections rely on), extension-container splitting + header construction (`ABIDiffer.extensionContainerKey`, one source of truth), member construction (`UnionRenderableMember`, identity/payload keys from the same `MemberRecord` projections the differ freezes), the `MemberCategory.allCases` schedule, and body composition order — while an `InterfaceUnionEmitting` strategy owns the PRESENTATION. `SwiftDiffableInterfaceRenderer` is a public shell whose generics erase at construction: `DiffUnionStrategy` keeps the genuinely two-sided semantics (`HeaderOutcome` pairing with failed-side stand-in, `-`/`+` pairs for modified members with the identical-rendering collapse, `DiffContainerAssembler` markers); `SwiftEvolutionInterfaceRenderer` is the N-way annotation strategy (latest-renderable header, `EvolutionAnnotationIndex` lookups, anchor rules). Members render at printer level 0 IN THE WALKER: the variable/subscript printers bake accessor-block interior indentation ABSOLUTELY from `level` while both format layers indent every line by its own indentLevel — real-level rendering double-indented `get`/`}` (the diff path carried exactly that artifact until the unification; pinned by `DiffMemberIndentationTests`). The format layers stay split on purpose (`DiffMarking`/`EvolutionMarking` + the two assemblers): markers apply per line, annotations anchor per unit — genuinely different semantics, not duplication -- `SwiftEvolutionInterfaceBuilder` - N ≥ 2 versions rendered as ONE **union interface with lifecycle annotations** (evolution proposal draft-swift-evolution-interface-builder; the N-way analogue of the `SwiftDiffableInterfaceBuilder`+`Renderer` pair). Division of labor is the load-bearing rule: annotation facts come SOLELY from `ABIEvolutionBuilder` over the versions' frozen snapshots (`EvolutionAnnotationIndex` joins on the same `ABIKey`/`MemberRecord` constructions `ABIDiffer` freezes — a lookup miss IS the "present throughout, never changed" verdict and renders bare), while text renders from the live models: each declaration from the LAST version that has it, so a modified member shows one line (newest generation) with the old shape in the phrase (`modified in 26.0: old → new`; an identical arrow collapses to the bare phrase). Union order = newest version's order as spine, absent declarations appended in their last-carrying version's order (the shared `InterfaceUnionWalker`'s matching — see its entry above). Format layer (`EvolutionMarking`): trailing `// [●●○] removed in 26.0` comments (bitmap + phrases, legend header mapping positions to labels), per-block column alignment capped at 72 (overflow → own line one level deeper), annotation anchored on the unit's DECLARATION line (members: first line, attributes print inline; container headers: last line, the one with the brace — a computed property's annotation never sinks to its accessor block's closing brace), reporter-mirrored warnings tail. Members render at printer level 0 — enforced in the shared walker for BOTH comparison paths, see the `InterfaceUnionWalker` entry. The public surface is TWO types: `AnySwiftEvolutionInterfaceBuilder`, the type-ERASED runtime-N workhorse (each version erases at construction via `InterfaceVersionRendering`/`InterfaceVersionUnit`; homogeneous `[MachO]` init + heterogeneous pack init, both available everywhere — packs in *function* position need no availability gate), and `SwiftEvolutionInterfaceBuilder`, the pack-generic façade for compile-time-fixed axes (`@available(macOS 14…)` — packs in a TYPE's generic parameter list DO need the Swift 5.9 runtime; constructs-then-erases, behavior byte-identical by construction, pinned by `packGenericFacadeMatchesTheErasedBuilder`). A pack's arity is compile-time, so runtime-N (the CLI, RuntimeViewer's user-picked versions) always goes through the erased type; same-element requirements (`repeat each MachO == M`) are not yet supported by the toolchain, so the array init cannot live on the pack type. All inputs must be binaries (snapshots carry no renderable interface); protocols' stripped `pwtslot:` records are not rendered (no declaration — same as `diff --interface`). CLI: `swift-section evolution --interface` (mutually exclusive with `--json`/`--summary-only`; colorizes by event kind at the CLI layer). Structured stream for hosts: `@_spi(Support) annotatedBlocks()` → `[[EvolutionLine]]` +**SwiftInterface** - Thin orchestrator tying indexing + printing into a full interface dump (module reference: [Documentations/Internal/Modules/SwiftInterface.md](Documentations/Internal/Modules/SwiftInterface.md)) +- `SwiftInterfaceBuilder` - Main builder, call `prepare()` then `printRoot()`; both run on the large-stack task executor (`LargeStackTaskExecution.run`, see `MachOSymbols`) +- Cross-version preparation is **parallel** (evolution proposal `large-stack-executor-and-cross-version-parallelism`): `AnySwiftEvolutionInterfaceBuilder.prepare(maximumConcurrentPreparations:)` (default = processor count; `1` = the serial oldest-first order; below 1 clamps) indexes the versions through `Collection.concurrentMap(maximumConcurrency:_:)` (`Utilities`, a windowed task group: source-ordered results, first failure rethrown, pending elements never started), and the CLI's `diff` / `evolution` index their inputs the same way under `--jobs N`. Safe because versions are different files — caches key on UUID, descriptor reads go through a memory mapping — and the window is capped at the processor count because a preparation occupies its executor thread. The result is byte-identical to serial (pinned by `parallelPreparationMatchesSerialPreparation`); only event delivery interleaves. Intra-version (per-definition) parallelism is deliberately NOT done: MachOKit's `MachOFile` reads share one `FileHandle` (seek + read), and `index(in:)`'s `isIndexed` guard is a plain check-then-set +- `InterfaceUnionWalker` - the SHARED structure walk behind both comparison renderers (evolution proposal 0014): N versions erased as `[any InterfaceVersionRendering]` (`InterfaceVersionUnit` = per-version `SwiftDiffableInterfaceBuilder` + a printer sharing its dispatcher) in, block-grouped lines out. The walker owns the STRUCTURE — matching and union ordering (newest version's order as spine, absent declarations appended from their last-carrying version; keys first-wins per side, EMISSION INCLUDED, mirroring `ABIDiffer.keyed` — a later same-keyed element is never emitted twice, which the header-failure tests' replace-not-append injections rely on), extension-container splitting + header construction (`ABIDiffer.extensionContainerKey`, one source of truth), member construction (`UnionRenderableMember`, identity/payload keys from the same `MemberRecord` projections the differ freezes), the `MemberCategory.allCases` schedule, and body composition order — while an `InterfaceUnionEmitting` strategy owns the PRESENTATION. `SwiftDiffableInterfaceRenderer` is a public shell whose generics erase at construction: `DiffUnionStrategy` keeps the genuinely two-sided semantics (`HeaderOutcome` pairing with failed-side stand-in, `-`/`+` pairs for modified members with the identical-rendering collapse, `DiffContainerAssembler` markers); `SwiftEvolutionInterfaceRenderer` is the N-way annotation strategy (latest-renderable header, `EvolutionAnnotationIndex` lookups, anchor rules). Members render at printer level 0 IN THE WALKER: the variable/subscript printers bake accessor-block interior indentation ABSOLUTELY from `level` while both format layers indent every line by its own indentLevel — real-level rendering double-indented `get`/`}` (the diff path carried exactly that artifact until the unification; pinned by `DiffMemberIndentationTests`). The format layers stay split on purpose (`DiffMarking`/`EvolutionMarking` + the two assemblers): markers apply per line, annotations anchor per unit — genuinely different semantics, not duplication +- `SwiftEvolutionInterfaceBuilder` - N ≥ 2 versions rendered as ONE **union interface with lifecycle annotations** (evolution proposal 0013; the N-way analogue of the `SwiftDiffableInterfaceBuilder`+`Renderer` pair). Division of labor is the load-bearing rule: annotation facts come SOLELY from `ABIEvolutionBuilder` over the versions' frozen snapshots (`EvolutionAnnotationIndex` joins on the same `ABIKey`/`MemberRecord` constructions `ABIDiffer` freezes — a lookup miss IS the "present throughout, never changed" verdict and renders bare), while text renders from the live models: each declaration from the LAST version that has it, so a modified member shows one line (newest generation) with the old shape in the phrase (`modified in 26.0: old → new`; an identical arrow collapses to the bare phrase). Union order = newest version's order as spine, absent declarations appended in their last-carrying version's order (the shared `InterfaceUnionWalker`'s matching — see its entry above). Format layer (`EvolutionMarking`): trailing `// [●●○] removed in 26.0` comments (bitmap + phrases, legend header mapping positions to labels), per-block column alignment capped at 72 (overflow → own line one level deeper), annotation anchored on the unit's DECLARATION line (members: first line, attributes print inline; container headers: last line, the one with the brace — a computed property's annotation never sinks to its accessor block's closing brace), reporter-mirrored warnings tail. Members render at printer level 0 — enforced in the shared walker for BOTH comparison paths, see the `InterfaceUnionWalker` entry. The public surface is TWO types: `AnySwiftEvolutionInterfaceBuilder`, the type-ERASED runtime-N workhorse (each version erases at construction via `InterfaceVersionRendering`/`InterfaceVersionUnit`; homogeneous `[MachO]` init + heterogeneous pack init, both available everywhere — packs in *function* position need no availability gate), and `SwiftEvolutionInterfaceBuilder`, the pack-generic façade for compile-time-fixed axes (`@available(macOS 14…)` — packs in a TYPE's generic parameter list DO need the Swift 5.9 runtime; constructs-then-erases, behavior byte-identical by construction, pinned by `packGenericFacadeMatchesTheErasedBuilder`). A pack's arity is compile-time, so runtime-N (the CLI, RuntimeViewer's user-picked versions) always goes through the erased type; same-element requirements (`repeat each MachO == M`) are not yet supported by the toolchain, so the array init cannot live on the pack type. All inputs must be binaries (snapshots carry no renderable interface); protocols' stripped `pwtslot:` records are not rendered (no declaration — same as `diff --interface`). CLI: `swift-section evolution --interface` (mutually exclusive with `--json`/`--summary-only`; colorizes by event kind at the CLI layer). Structured stream for hosts: `@_spi(Support) annotatedBlocks()` → `[[EvolutionLine]]` - `SwiftInterfaceBuilderOpaqueTypeProvider` - Expands `some` return types from the opaque type descriptor's generic requirements. Primary-associated-type arguments are **attributed per protocol** (evolution proposal 0011), never distributed blindly across the composition: a same-type constraint attaches to the protocol its **anchor** names (the associated type's declaring protocol, kept by the demangler in `dependentAssociatedTypeRef` — identity comparison only, so it works offline via bind symbols), to a protocol whose refine closure contains the anchor, or — for compiler-collapsed equivalence classes like `TestCollection<[A]>` — by name fallback (only when no anchor matched, the protocol itself declares that associated-type name, the candidate is unique, and the anchor lies **outside** the composition; a collapsed pin and a never-pinned same-named member are byte-identical in the descriptor, so an in-composition anchor forbids the fallback). Protocol facts (refine signature + associated-type names) resolve through `ProtocolFactsResolver`: a reachable descriptor (same image on both readers; **any** image in-process — `MachOImage` dereferences the requirement's indirect pointer cross-image) merged with `BuiltinStandardLibraryProtocolFacts` (the only source of primary names/order — SE-0346 leaves no runtime trace — and the offline fallback for bind-only externals). Unknown facts attach nothing (a missed parameter beats a fabricated one), so `MachOFile` and `MachOImage` output depth may differ — accepted, in-process is strictly additive. Composition order is the descriptor's canonical order; source order is not recoverable. See [Documentations/Internal/OpaquePrimaryAssociatedTypeAttribution.md](Documentations/Internal/OpaquePrimaryAssociatedTypeAttribution.md) (implementation note) and [Documentations/Internal/OpaqueReturnTypeResolution.md](Documentations/Internal/OpaqueReturnTypeResolution.md) (domain deep-dive: descriptor encoding, anchor/collapse mechanics, byte-level debugging). Printing and indexing are peers — neither depends on the other. @@ -170,6 +190,7 @@ Printing and indexing are peers — neither depends on the other. - `Transformer.SwiftEnumLayout` (in `OutputTransformer`, bridged here) - Token-template rendering for enum-layout comments: three template levels (strategy line / per-case block / per-fixed-byte line) with `${token}` placeholders, plus five presets — `detailed` (the built-in default, unit-test-guaranteed identical to `EnumCaseProjection.description`, which is implemented over it), `explained` (partially-fixed bytes narrated as bit ranges: `bits 7-4 are always 0100; the other bits (3-0) hold payload data`), `standard` (no per-byte lines), `inline` (one line per case with the byte summary inline after the header: `` Case 1 `implicit` (empty case #0): bytes[0x8..<0x10] = 0x1 ``, via the colon-friendly `${fixedBytesPhrase}` token), `compact` (one line per case, no byte information). `SwiftInspection`'s bridge (`Transformer+EnumLayoutProjection.swift`) builds the template inputs from `LayoutResult`/`EnumCaseProjection`; wiring goes through `applyTransformers` (see the `OutputTransformer` module below) and the CLI's `--enum-layout-style` / `--enum-layout-template` / `--enum-layout-case-template` / `--enum-layout-byte-template`. Conditional content uses line-tokens (`${encodingLine}`, `${patternNoteLine}`) — lines left empty after substitution are dropped; a case template referencing no byte tokens gets the note/byte lines auto-appended (`appendsOmittedDetails`, the historical RuntimeViewer behavior), and a mask-unaware per-byte template never renders a partially-fixed byte (the engine falls back to the mask-scoped built-in wording) - `ClassHierarchyDumper` - Dumps class inheritance hierarchies - `MetadataReader` - Reads runtime metadata from MachOImage +- `RuntimeMetadataTypeBuilder` - The first production conformer of swift-demangling's `TypeBuilder` protocol (its `TypeDecoder` is the ported upstream walker): decodes a demangled `Node` tree into **live in-process metadata**, mirroring the runtime's `DecodedMetadataBuilder` (`MetadataLookup.cpp`) over the same entry points instead of remangling to a string for `swift_getTypeByMangledNameInContext`. Nominal construction gathers key arguments with `_gatherGenericParameters` semantics — parent written args read back from the parent metadata's generic-argument area (value 16-byte header / class resilient-or-not immediate-members branch), key type params' metadata first, then one PWT per key protocol requirement in requirement order via `swift_conformsToProtocol`, requirement subjects (`A`, `A.Element`) self-decoded through a nested builder bound to the written args — then calls the descriptor's metadata accessor at `MetadataState.abstract` (cycle-tolerant, like the runtime); the public entry (`metadataType(for:)` / `metadata(for:)`) forces completion via `swift_checkMetadataState`. Structural types go through `swift_getTupleTypeMetadata` / `swift_getFunctionTypeMetadata`(+ weak-linked extended variant) / `swift_getExistentialTypeMetadata` / `swift_getMetatypeMetadata`, bridged in `MachOSwiftSectionC` (plain C decls; `ProtocolClassConstraint` is ABI-inverted — Class = 0). Named nominal nodes are the NORM, not the fallback (`MetadataReader` resolves symbolic references into named context trees, never `.typeSymbolicReference`): resolution is injected `nominalTypeDescriptorResolver` seam → runtime name lookup for non-generics → a built-in stdlib generic descriptor table (`Array`/`Dictionary`/`Optional`/…, located through known instantiations). An ObjC class builds as the **realized class pointer itself** (`swift_getInitializedObjCClass`) — `swift_getObjCClassMetadata`'s wrapper is a distinct metadata identity that splits generic caches (and printing it crashed outright). Honest typed-`TypeLookupError` rejections (no fabricated values): SIL types, parameter packs, value generics (`InlineArray<5, _>`), opaque returns, constrained/extended existentials, dynamic Self, non-key parent params. Pinned by round-trip parity: `RuntimeMetadataTypeBuilderTests` demangles `_mangledTypeName(T.self)` and requires the rebuilt `Any.Type` to be pointer-identical to `T.self`. See evolution proposal 0012. **SwiftLayout** - Static aggregate-layout engine (offline field offsets, no runtime) - `StaticLayoutCalculator` - Entry point: computes struct/class stored-property field offsets from a Mach-O file without loading the process or calling the metadata accessor. `fieldLayout(of:)` lays out a non-generic descriptor; `fieldLayout(of:genericArguments:)` lays out a **concrete generic instantiation** (`Foo`) by supplying its depth-0 type-argument `Node`s, and `fieldLayout(forInstantiationMangledName:)` does the same from a binary's bound-generic mangled reference (resolving the descriptor in its defining image). All share one environment-threaded per-field path (`accumulateFieldLayout`, default `.empty` ⇒ unchanged non-generic behavior) with per-field degradation @@ -182,7 +203,7 @@ Printing and indexing are peers — neither depends on the other. - `ObjCClassIndex` - Phase-4 Objective-C ancestor support: reads a class's instance `class_ro_t.instanceSize` from `__objc_classlist` (bare name → start layout), resolving the realized `class_rw_t` form for classes dyld has realized in-process. Uses `instanceSize` (where a Swift subclass's first field begins), **not** `instanceStart`; value matches `ObjCClass.info(in:).instanceSize` without parsing methods/ivars. Also indexes every statically-emitted **Swift** class's own `class_ro_t.instanceStart` (legacy `_TtC…` runtime names demangled back to the qualified-name key): the class's field block starts there, slid by the ObjC runtime when the actual ancestor outgrows it (objc4 `moveIvars`: slide rounded up to the class's max own-ivar alignment) — a dyld-cache image carries the pre-slid final value. Classlist membership discriminates the mode: generic / singleton-initialized classes are absent and keep the Swift-runtime rule (exact superclass size, per-field alignment). This fixed the `AppKitTableHeaderCell`-class survey mismatches (fields at 248, not the raw `NSTableHeaderCell` 241; see `ObjCAncestorSlideLayoutTests`). `objc.classes64`/ro accessors are concrete `MachOFile`/`MachOImage` overloads, so the builder is split per reader - `ObjCProtocolIndex` - Phase-8 `@objc` protocol support: indexes `__objc_protolist` by Swift qualified name (parsing the legacy `_TtP_` mangling; native ObjC protocols' plain names are skipped — they demangle as `__C` references and never reach the lookup). Recognition is the whole payload: a Swift-declared `@objc` protocol emits no `__swift5_protos` descriptor, is always class-bound, and contributes no witness table. Reader-split like `ObjCClassIndex` - `ImageUniverse` / `ImageReference` - Type/protocol/ObjC-class/assocty-witness/ObjC-protocol lookup seam (five resolvers: `resolveType`, `resolveProtocolClassConstraint`, `resolveObjCClassInstanceSize`, `resolveAssociatedTypeWitness`, `isObjCProtocolDeclared`). `ImageReference` indexes one image's type descriptors (`__swift5_types`), protocol class constraints (`__swift5_protos`), ObjC class instance sizes (`__objc_classlist`), associated-type witnesses (`__swift5_assocty`, keyed `conforming|protocol|assoc`), and `@objc` protocol declarations (`__objc_protolist`); `ImageUniverse` is either single-image (`singleImage`) or a **dependency closure** (`dependencyClosure`) that merges a root plus its transitive dependencies, **indexing each dependency lazily** (root eager, dependencies folded in resolution order only when a lookup misses, all five indexes merged together) so a several-hundred-image OS closure is not eagerly demangled -- `ImageUniverse+DependencyClosure` - Closure factories: in-process (`dependencyClosure(root: MachOImage)`, resolves dependencies through the active dyld) and offline (`dependencyClosure(root: MachOFile, searchPaths:)`, resolves through explicit on-disk paths + the dyld shared cache, the latter indexed once by bare name). `LayoutDependencySearchPath` is SwiftLayout-local (no `SwiftInterface` dependency). Dependency load names are matched by **bare name** (`MachOImage(name:)` semantics); `MachOFile.imagePath` is the install name, not a filesystem path +- `ImageUniverse+DependencyClosure` - Closure factories, all thin wrappers over `MachODependencies.DependencyClosure` (evolution proposal 0017): in-process (`dependencyClosure(root: MachOImage)`, through the active dyld), offline (`dependencyClosure(root: MachOFile, searchPaths:)`, explicit on-disk files + dyld shared caches), and `dependencyClosure(_ closure:)` for a host that resolved the closure once and shares it with interface generation. The transitive walk is breadth-first ON PURPOSE — the universe indexes dependencies lazily in that order and stops at the first hit. `LayoutDependencySearchPath` is a deprecated typealias of `DependencySearchPath`; `MachOFile.imagePath` is the install name, not a filesystem path (the explicit search path for a sibling framework must be computed by the caller) - `GenericArgumentEnvironment` - Phase-5/6 concrete bound-generic field substitution: a non-generic type with a `MyBox` field resolves it by capturing the instantiated node's `(depth, index) → Node` argument map (`make(forInstantiatedTypeNode:)`) and deep-rewriting the base type's `dependentGenericParamType` field nodes (purely syntactic — no metadata accessor / PWT, so no new `SwiftSpecialization`/`SwiftGenericSupport` dependency). Arguments may be plain types, **value arguments** (`Foo<5>`, bound as `.integer`/`.negativeInteger` nodes, SE-0452), or **flat packs** (SE-0393). Substitution is a hand-rolled **top-down** recursion (not the bottom-up `Node.Rewriter` — pack expansion is context-sensitive: instance `i` of an expansion resolves a pack-bound parameter to its `i`-th element, which a bottom-up pass cannot distinguish from literal packs in shapes like `(repeat Pair)`): concrete pack expansions expand in place inside `.tuple` (flattened elements, empty pack → empty tuple, single-unlabeled-element result collapses to the element itself, matching the runtime's one-tuple identity) and inside `.pack` literals (the `Foo` forwarding shape flattens). Only a pack argument still containing an unexpanded expansion degrades the environment. **Arguments are collected per level along the nominal parent chain** (outermost bound-generic level = depth 0), so a nested type of a specialized parent whose fields *use* the parent's parameter — `Environment.Content`, a plain `.enum` node with no argument list of its own — binds the parent's arguments, and a two-level instantiation (`Outer.Inner`) binds each level at its own depth; only parameters of contexts the mangling genuinely does not carry (a local type in a generic function) stay degraded. Instantiations memoize under a remangled instantiation key (`memoizedInstantiationLayout`, skipping the frozen table); a leading bare-name `KnownLayoutTable` check keeps `Array`/`UnsafePointer` argument-independent. `superclassStartLayout` substitutes the superclass reference first (`class Sub: Base`). Also fixes a latent single-payload-enum bug (the payload reads the correct parameter, not blindly the first type argument). `make(forDepthZeroTypeArguments:)` builds the same depth-0 map directly from a caller-supplied argument-`Node` list (backing `StaticLayoutCalculator`'s top-level generic-instantiation entries), not only from a `boundGeneric*` node. Compiler-enforced simplifications: a generic *type* declares at most one type pack, and enums cannot declare one at all - `ClassBoundGenericParameterAnalysis` - Phase-9 **unspecialized** requirement-signature layout mining: derives, in one pass over `genericContext.requirements`, the `RequirementSignatureLayoutFacts` a generic descriptor's signature pins about each parameter **without any argument** — both (a) **class-bound parameters** and (b) **concrete same-type pins**. (a) A generic parameter constrained to a class layout (`Element: AnyObject`), a superclass (`Element: SomeClass`, a `baseClass` requirement), or a class-bound protocol (`Element: SomeClassBoundProtocol`, Swift or imported/`@objc` ObjC) is necessarily a single object reference, so a field typed by it — **and every field after it** — lays out exactly even when the type is dumped with no generic arguments; class-boundness is classified from the requirement kind + resolved content (`layout(.class)` / `baseClass` / a class-bound `protocol`; a cross-image protocol symbol is recovered by name through the universe's `resolveProtocolClassConstraint` + `isObjCProtocolDeclared` seams), the parameter rewrites to a placeholder `.class` node the resolver lays out as `.pointerSized` (`swift_getHeapObjectExtraInhabitantCount`, saturated 0x7FFF_FFFF on 64-bit Darwin). (b) A parameter pinned to a **concrete** type by a `sameType` requirement (`Value == Foundation.Date` / `== Range`, contributed by a **constrained extension** — a type nested in `extension Foo where Value == Date` inherits the requirement) is that type in every valid use, so its unwrapped RHS node becomes a genuine **substitution** (only when the RHS is fully concrete — a `sameType` RHS that references another parameter / dependent member cannot stand alone and is skipped; a dependent-member *subject* like `T.Element == X` says nothing about `T` and is skipped too). Both facts read from bare-parameter subjects carrying absolute `(depth, index)` (matching field records, so nested contexts need no bookkeeping). Seeded via `GenericArgumentEnvironment.augmented(withRequirementFacts:)` at every field-reading entry point (`StaticLayoutCalculator.fieldLayout(ofStruct:/ofClass:)` + the resolver's `computeStructLayout` / `computeClassLayout` / `computeEnumLayout` choke points, so generic superclasses and ObjC-ancestor subclasses benefit too), all through the same top-down substitution (reaching inside optionals/tuples/`Array`/function types). A genuine instantiation argument always wins over both fallbacks (it cannot contradict a same-type pin). Empirically same-type pinning is small (only ~23 of 579 `sameType` requirements across 5 frameworks are bare-param-to-concrete — 75% are dependent-member subjects the assocty bridge covers, 21% are abstract param-to-param), but it is a correct completion: the two facts together are everything the requirement signature determines without arguments. `sameType`-to-a-concrete-class (a representation-and-value pin) flows through as a substitution just like any other concrete pin. Same-**value** pins (`extension Foo where count == 5` — a `sameType` requirement with `isValueRequirement` set whose RHS mangles an integer) flow through the same extraction with zero extra code: the `.integer` RHS passes the fully-concrete check and binds like a phase-6 value argument, so a nested type's `InlineArray` field resolves unspecialized (and the interface printer now renders SE-0452 integer nodes — `SwiftPrinting.NodePrintable` previously dropped them, printing `ValueGenericBuffer<>` / `where A == `) - Fixed arrays / value generics: the resolver dispatches `builtinFixedArray` (`Builtin.FixedArray`: count ≤ 0 → empty; else `size == stride == element.stride × count` with no tail-padding reclamation even at count 1, alignment/bitwise-takability from the element, **XI from the first element** — ported from `swift_getFixedArrayTypeMetadata`, consistent with IRGen `convertBuiltinFixedArrayType` and RemoteInspection `ArrayTypeInfo`), and special-cases `Swift.InlineArray` onto the same formula (layout-identical to its only stored field; its descriptor lives in the stdlib, like `Optional`'s, so single-image scopes work). Tuple XI is the max over elements (runtime `swift_getTupleTypeMetadata` semantics; previously hardcoded 0). Zero-sized fields report offset 0, mirroring the compiler-emitted vector (IRGen `ElementLayout::completeEmpty`), not the accumulator position `performBasicLayout` would report. Note the official offline lowering (RemoteInspection `TypeLowering.cpp`) rejects packs outright — this path is validated directly against runtime substitution semantics @@ -203,12 +224,14 @@ Printing and indexing are peers — neither depends on the other. ### MachO Infrastructure Modules -- **MachOFoundation** - Combines reading, symbols, pointers +- **MachOBase** - Umbrella over the reader / resolver / pointer layer (`MachOKitExtensions`, `MachOReading`, `MachOResolving`, `MachOPointers`, `Utilities`): everything the ABI model may depend on. `MachOSwiftSection` re-exports it, so `import MachOSwiftSection` still brings the pointer and reader types — but not the symbol index +- **MachOFoundation** - `MachOBase` plus `MachOSymbols` and `MachODependencies` — the umbrella for everything above the ABI model - **MachOReading** - File reading abstractions -- **MachOResolving** - Address/offset resolution -- **MachOSymbols** - Symbol table parsing and demangling. `SymbolIndexStore`'s offset and member indexes hold their row lists in `SymbolRowBucket` (evolution proposal 0003): the dominant single-row case stays inline in the dictionary slot, only a bucket that collects a second row allocates an array; iteration order is insertion order, so query output is byte-identical to the former `[UInt32]` buckets. The member/typeInfo/thunk-attribute indexes are keyed by printed type name FIRST and interned context node SECOND, and the name key is NOT injective — it is printed with `.interfaceTypeBuilderOnly`, which strips private discriminators, so same-named private types from different files share one name bucket (issue #115: the dump path's name-only lookups merged both types' members into each declaration). Any consumer resolving *one* type's members/info/attributes must use the node-taking overloads (`memberSymbols(of:for:node:in:)`, `methodDescriptorMemberSymbols(of:for:node:in:)`, `typeInfo(for:node:in:)`, `thunkAttributeMembers(of:for:node:in:)`); the name-only forms deliberately flatten every sub-bucket and are only for "all types printing as this name" aggregation. See [Documentations/Internal/PrivateTypeMemberAttribution.md](Documentations/Internal/PrivateTypeMemberAttribution.md). `Storage` also carries the image's **export facts** (evolution proposal 0008): both symtab collection legs filter on `!nlist.isExternal`, so exported symbols' rows come only from the export-trie leg, whose row minting is conditional — trie membership is therefore collected explicitly in the same pass (a per-row bitmap plus a fallback name set for offset-less re-exports), backing the tri-state `isExported(name:in:)` (`nil` = the image has no export information; never annotate then) and `isExportedIncludingDerivedSymbols(name:in:)` (extends over the `Tj`/`Tq`/`Tu`/`TjTu` appended-suffix forms) -- **MachOPointers** - Pointer types (relative, indirect, etc.) +- **MachOResolving** - Address/offset resolution; also home of the symbol **value** types `Symbol` (offset + name + `isExternal`), `Symbols` (every name at one offset — identical code folding leaves several) and `SymbolOrElement` (a bind-table symbol or a resolved element). They carry no lookup behavior: `Symbols` is deliberately not `Resolvable`, because "the symbols at this offset" is a query against the symbol index, not a read +- **MachOSymbols** - Symbol table parsing and demangling — the symbol *index*, one layer above the ABI model; the value types it vends live in `MachOResolving`. Also home of `LargeStackTaskExecution` (evolution proposal `large-stack-executor-and-cross-version-parallelism`): `run(_:)` sets swift-demangling's 16 MB `LargeStackTaskExecutor` (`StackSafeExecutor.taskExecutor`, `@_spi(Internals)`, 0.6.3+) as the task executor preference around a library entry point. The demangler decides per call whether to hop to its 8 MB pool by probing the CALLING thread's remaining stack against a 2 MB floor — cooperative and libdispatch threads carry 512 KB, so an async print loop paid one thread round trip + semaphore wait per printed symbol (8–21 µs, 1.14–2.28×); on an executor thread the probe passes at every entry, synchronous callees included, so the whole pipeline runs inline (the effect of `withLargeStack` extended to a whole task). Nesting is a no-op (already on the executor = no switch), an unstructured `Task {}` does NOT inherit the preference (SE-0417 — never start one inside a wrapped entry; child tasks and default actors do inherit), the main actor keeps its own executor (its 8 MB stack passes the probe anyway), and below macOS 15 / iOS 18 (or off Darwin) `run` executes the body unchanged. Process-wide off switch `isEnabled` (hosts with their own executor) seeded by `MACHO_SWIFT_SECTION_LARGE_STACK_EXECUTOR` (`0` / `false` / `no` / `off` = off, anything else or unset = on; the A/B and timing runs compare one binary both ways). `concurrentMap(maximumConcurrency:)` submits through `addTaskUnlessCancelled`: cancelling the caller stops pending submissions and fails the call with `CancellationError`, never a partial array. See [Documentations/Internal/LargeStackTaskExecutorAdoption.md](Documentations/Internal/LargeStackTaskExecutorAdoption.md). Index-backed lookups: `symbols(offset:)` on `MachORepresentableWithCache`, `Symbol.resolve(from:in:)` and the process-wide `Symbol.resolvesSymbolUsingIndexStore` switch. `SymbolIndexStore`'s offset and member indexes hold their row lists in `SymbolRowBucket` (evolution proposal 0003): the dominant single-row case stays inline in the dictionary slot, only a bucket that collects a second row allocates an array; iteration order is insertion order, so query output is byte-identical to the former `[UInt32]` buckets. The member/typeInfo/thunk-attribute indexes are keyed by printed type name FIRST and interned context node SECOND, and the name key is NOT injective — it is printed with `.interfaceTypeBuilderOnly`, which strips private discriminators, so same-named private types from different files share one name bucket (issue #115: the dump path's name-only lookups merged both types' members into each declaration). Any consumer resolving *one* type's members/info/attributes must use the node-taking overloads (`memberSymbols(of:for:node:in:)`, `methodDescriptorMemberSymbols(of:for:node:in:)`, `typeInfo(for:node:in:)`, `thunkAttributeMembers(of:for:node:in:)`); the name-only forms deliberately flatten every sub-bucket and are only for "all types printing as this name" aggregation. See [Documentations/Internal/PrivateTypeMemberAttribution.md](Documentations/Internal/PrivateTypeMemberAttribution.md). `Storage` also carries the image's **export facts** (evolution proposal 0008): both symtab collection legs filter on `!nlist.isExternal`, so exported symbols' rows come only from the export-trie leg, whose row minting is conditional — trie membership is therefore collected explicitly in the same pass (a per-row bitmap plus a fallback name set for offset-less re-exports), backing the tri-state `isExported(name:in:)` (`nil` = the image has no export information; never annotate then) and `isExportedIncludingDerivedSymbols(name:in:)` (extends over the `Tj`/`Tq`/`Tu`/`TjTu` appended-suffix forms) +- **MachOPointers** - Pointer types (relative, indirect, etc.), including `SymbolOrElementPointer` (absorbed the former `MachOSymbolPointers` target): an indirectable target that resolves either to a bind-table symbol (`Symbol`, via `MachOBindRebaseResolving`) or to an element - **MachOCaches** - dyld shared cache support +- **MachODependencies** - The one dependency-resolution implementation every feature shares (evolution proposal 0017; re-exported by `MachOFoundation`, so nothing above it needs an extra import). `DependencyClosure` walks a root's `LC_LOAD_DYLIB`-family load commands — `.direct` (the root's own list, load-command order) or `.transitive` (breadth-first, so a lazily indexing consumer meets the root's direct dependencies first) — deduplicated by **bare image name** (`DependencyLoadName.bareImageName(of:)`: last path component, FIRST extension stripped — `libobjc.A.dylib` → `libobjc`; the exact rule `MachOImage(name:)` matches on, which is why handing that lookup a raw load path resolves nothing — the bug `SwiftInterfaceBuilderDependencies`'s image initializer carried until this module), root excluded, misses recorded in `unresolvedLoadNames` rather than dropped. Locators: `InProcessDependencyLocator` (active dyld) and `FileDependencyLocator` (`DependencySearchPath`s: explicit files + dyld caches; **exact install path first, ranked bare name second** via `DyldCacheImageSearchMode.matchRank`, so a macOS cache's `/System/iOSSupport` Catalyst SwiftUI never shadows the native one; each cache indexed ONCE lazily, since a per-lookup `machOFile(by:)` scan is `O(dependencies × cache size)`; fat explicit files contribute the root's architecture; unopenable search paths land in `searchPathLoadFailures`, never thrown). Consumers: `SwiftLayout.ImageUniverse` (transitive), `SwiftInterface.SwiftInterfaceBuilderDependencies` (DIRECT on purpose — TypeIndexing generates one SourceKit interface per dependency module, so the transitive closure would bring back the whole-SDK generation that once disabled the target; it dispatches `searchPathLoadFailures` as `renderingDegraded(.dependencyLoad)` events and exposes `unresolvedLoadNames`), `swift-section interface --resolve-c-module-names`. This module sits below the event layer: failures are data, never logged here. See [Documentations/Internal/Modules/MachODependencies.md](Documentations/Internal/Modules/MachODependencies.md) - **MachOKitExtensions** (external package, `../MachOKitExtensions`) - Extensions to MachOKit types. This used to be an in-repo `MachOExtensions` target; it was extracted so `MachOObjCSection` can depend on it too (MachOSwiftSection depends on MachOObjCSection, so the ObjC side could never depend back on an in-package target without a package-level cycle). Two behaviors this repo's tests still pin live there: - `resolveBind(fileOffset:)` resolves bind slots from chained fixups AND, when those are absent, from the legacy `LC_DYLD_INFO(_ONLY)` bind opcode streams (pre-macOS 12 / iOS 16 deployment targets, e.g. iOS 15.5 simulator frameworks) via a lazily built file-offset → symbol-name index; `isBind(_:)` splits on the same discriminator so the two public APIs always agree on a slot. The opcode stream is treated as hostile input: every slot is bounds-checked against its segment's file size before recording and a repeat run terminates at the segment end (a raw uleb count can no longer spin the loop; a wrapped offset can no longer claim a foreign file offset). The arm64e threaded legacy format is deliberately not indexed. Pinned by `LegacyDyldInfoBindTests`, whose fixture is compiled on the fly with `-target arm64-apple-macosx11.0` to force the legacy format. - `DyldCacheImageSearchMode.matchRank(forImagePath:)` ranks name lookups instead of taking the first hit — leaf names are not unique inside a shared cache (`SwiftUI.framework/SwiftUI` vs `SwiftUI.axbundle/SwiftUI`, and a macOS cache's Mac Catalyst builds under `/System/iOSSupport` share the native build's leaf name). Ranks accumulate across every cache file (main plus subcaches, each scanned once) so a low-ranked hit in the first file cannot shadow the framework binary in a subcache; only a native canonical framework reaches `bestMatchRank`, which is what makes the early exit sound. Pinned by `DyldCacheImageSearchTests`. @@ -268,6 +291,17 @@ final class Foo { Tests use `MACHO_SWIFT_SECTION_SILENT_TEST=1` to suppress verbose output. +**Swift Testing runs test bodies on 512 KB cooperative threads.** A test that +calls a library entry point gets the large-stack executor through the entry's +own `LargeStackTaskExecution.run` (macOS 15+; CI runs macOS 26), but a test +that drives the demangler or printer DIRECTLY in a deep recursion runs on the +cooperative thread and hops per call exactly as a host would; wrap such a body +in `LargeStackTaskExecution.run` if the hop cost or the 8 MB pool depth limit +(`KnownIssues.md` #4 upstream) is what is being measured. Do not read a "the +suite passed" as "the executor was used": `LargeStackTaskExecutionTests` pins +the executor behavior by thread identity, everything else is executor-agnostic +by design. + **On-the-fly-compiled fixture dylibs need a class.** A struct-only fixture module compiles to a dylib with NO `__DATA` segment, and the pinned MachOKit release mis-walks that layout's chained-fixup pages during `resolveBind` — @@ -308,6 +342,8 @@ Tests read Mach-O files from Xcode frameworks and dyld shared cache for real-wor ## Fixture-Based Test Coverage (MachOSwiftSection) +Design rationale and history of the whole fixture/CI system (why SymbolTestsCore only, path anchoring, the sentinel trust crisis, CI whitelist): [Documentations/Internal/FixtureTestingAndContinuousIntegration.md](Documentations/Internal/FixtureTestingAndContinuousIntegration.md). This section is the operational contract. + `MachOSwiftSection/Models/` is exhaustively covered by `Tests/MachOSwiftSectionTests/Fixtures/`. Suites mirror the source directory and assert one of: - **Cross-reader equality** across MachOFile/MachOImage/InProcess + their ReadingContext counterparts (via `acrossAllReaders` / `acrossAllContexts` helpers), plus per-method ABI literal values from `__Baseline__/*Baseline.swift` — this is the standard depth. @@ -391,7 +427,60 @@ Rule out both before attributing red tests to a code change: `packageRef.kind` reads `fileSystem` (sibling) vs `remoteSourceControl` (fallback). Export `USING_LOCAL_DEPENDENCIES=1` for every build that must use siblings — including detached/`nohup` runs like the rendering A/B - script. + script. The remote fallback has its own drift: two FRESH scratch paths + resolve the newest matching remote versions independently, so an A/B whose + sides were resolved minutes apart can compare different upstream versions + (observed 2026-09-02: a candidate scratch picked up swift-demangling 0.6.1 + + FrameworkToolbox 0.11.0 over the baseline's 0.6.0 + 0.10.0 and ran the + layout dump 2.5× slower — a phantom regression). Copy the baseline's + `Package.resolved` into the candidate checkout before building and confirm + both `workspace-state.json` files agree. (Bisected the same day: the + slowdown is swift-demangling 0.6.1 alone — its `StackSafeExecutor` now + re-ranks the large-stack worker's QoS per hop and parks idle workers at + background, and every `demangle` / `print` / `remangle` call is one hop — + and it hits the DEFAULT `dump` / `interface` paths too (SwiftUICore: 50 s → + 150–210 s), not only the layout path. Do not bump past 0.6.0 until upstream + addresses it; see the 2026-09-02 MachODependencies task report.) + +3. **Fixture build settings drifting from the ones the baselines were generated + with.** The ABI literal baselines under + `Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/` record absolute + implementation offsets, and those offsets move when the fixture is built with + different settings — not only when its sources change. Measured 2026-09-07: + passing `CODE_SIGNING_ALLOWED=NO` shifts every implementation offset in + `SymbolTestsCore` by +16 bytes (`MethodDescriptorTests` 5624 → 5640, + `MethodOverrideDescriptorTests` 16404 → 16420, `ResilientWitnessTests` + 9100 → 9116, `ProtocolRequirementTests` 45256 → 45272), turning four suites + red with no code change. The setting does not even do what its name suggests + here — the product carries an `LC_CODE_SIGNATURE` either way; what changes is + the layout. `ARCHS=arm64` is harmless, and the `-derivedDataPath` value + (hence the absolute path length) does NOT affect the offsets — both were + ruled out by bisecting one variable at a time. + + The mechanism is alignment padding, not the signature: both products carry + an `LC_CODE_SIGNATURE` and identical headers (35 load commands, 5000 bytes), + but the padding between the load commands and `__text` is 64 bytes in a + signed build and 80 in a `CODE_SIGNING_ALLOWED=NO` one, so `__text` starts at + `0x13e8` versus `0x13f8` and every function offset moves with it. + + **CI therefore builds the fixture ad-hoc signed** (`CODE_SIGN_IDENTITY=-` + plus `CODE_SIGNING_REQUIRED=NO`), which needs no certificate — a GitHub + runner has none, and simply dropping `CODE_SIGNING_ALLOWED=NO` makes the + build fail with *No signing certificate "Mac Development" found* — while + reproducing the signed layout exactly (`__text` at `0x13e8`, the five suites + 28/28 green). Do not "fix" a future mismatch by switching CI back to + `CODE_SIGNING_ALLOWED=NO`; that trades a build failure for four silently + wrong suites. + + The consequence for regeneration: **the fixture build command in CI and the + one used to regenerate baselines must produce the same layout.** A local + regeneration with a real signing identity matches CI's ad-hoc build; a + regeneration with `CODE_SIGNING_ALLOWED=NO` does not. This bit once already — + CI carried `CODE_SIGNING_ALLOWED=NO` while the baselines were generated + without it, and because the CI workflow only triggers on `main` (`branches: + [main]` for both `push` and `pull_request`), the mismatch stayed invisible + through every `→ next` PR and surfaced only at the 0.19.0 release PR, on 27 + commits at once. ## Work In Progress diff --git a/Changelogs/0.18.0.md b/Changelogs/0.18.0.md new file mode 100644 index 00000000..d59e357c --- /dev/null +++ b/Changelogs/0.18.0.md @@ -0,0 +1,30 @@ +# 0.18.0 + +A minor release on top of `0.17.1`. One change, and it is a layering change with a breaking API surface: the ABI model (`MachOSwiftSection`) no longer depends on the symbol index or the demangler. Evolution proposal `self-contained-abi-layer`; implementation note `Documentations/Internal/SelfContainedABILayer.md`. + +## What changed + +1. **Descriptors expose an implementation's address, not its symbols.** `MethodDescriptor`, `MethodOverrideDescriptor`, `MethodDefaultOverrideDescriptor`, `ProtocolRequirement` and `ResilientWitness` carry a `RelativeDirectRawPointer` and expose `implementationOffset: Int?` (pure pointer arithmetic, `nil` for a null pointer) and `implementationAddress(in context:) -> Context.Address?` (`defaultImplementationOffset` / `defaultImplementationAddress(in:)` on `ProtocolRequirement`). The former `RelativeDirectPointer` fields made a plain ABI read build the whole image's demangled symbol index on first touch, and their `ReadingContext` overload — with no symbol service to consult — read the implementation's machine code as a `Symbols` value. +2. **Symbol attribution moved up to `SwiftInspection`.** `implementationSymbols(in:)` / `defaultImplementationSymbols(in:)` are extensions there now, non-throwing, MachO-backed only: the symbol index's answer for the offset the ABI layer reports. +3. **The symbol value types moved down.** `Symbol`, `Symbols` and `SymbolOrElement` live in `MachOResolving`; `SymbolOrElementPointer` lives in `MachOPointers`, which absorbed the `MachOSymbolPointers` target. `Symbols` is no longer `Resolvable`. The index-backed lookups (`symbols(offset:)`, `Symbol.resolve(from:in:)`, `Symbol.resolvesSymbolUsingIndexStore`) stay in `MachOSymbols`. +4. **New umbrella `MachOBase`** (`MachOKitExtensions` + `MachOReading` + `MachOResolving` + `MachOPointers` + `Utilities`) is all `MachOSwiftSection` depends on and re-exports. `MachOFoundation` is `MachOBase` plus `MachOSymbols` and `MachODependencies`. + +## Compatibility + +Breaking, source level (this package is source-distributed; every downstream recompiles): + +- `descriptor.implementationSymbols(in: machO)`: drop the `try`, add `import SwiftInspection`. +- `descriptor.implementationSymbols(in: context)`: removed; use `try descriptor.implementationAddress(in: context)` for the location. +- `Symbols.resolve(from:in:)`: removed; call `machO.symbols(offset:)` (`MachOSymbols`). +- `symbols(offset:) async`: removed (identical to the synchronous form, and an `async` caller bound to it and demanded `await`). +- `MachOSymbols.Symbol` / `.Symbols` / `.SymbolOrElement` qualified names: now `MachOResolving.…` (or unqualified). +- `import MachOSymbolPointers`: remove; the type is in `MachOPointers`. +- `ResilientWitness.implementationOffset` is `Int?`; `ResilientWitness.implementationAddress(in: machO)` returns `String?`. +- A file that reached `SymbolIndexStore`, `DemangledSymbol` or `DependencyClosure` through `import MachOSwiftSection` alone now needs `import MachOFoundation`, and the **target** that contains it must depend on the new `MachOFoundation` library product (`.product(name: "MachOFoundation", package: "MachOSwiftSection")`): those types used to arrive through `MachOSwiftSection`'s re-export, so no product existed for them. `MachOBase` is a product too. Reaching a module through the build directory without declaring it works by accident in a monolithic SwiftPM build and fails under explicit modules. +- Snapshot `formatVersion` unchanged. Pinned dependencies unchanged from `0.17.1`. +- Rendered output is byte-identical under the default flags (dump and interface, verified with the rendering A/B harness). One deliberate change under `--emit-member-addresses`: a resilient witness whose implementation pointer is null no longer gets an address comment. The old line resolved a null relative pointer, which yields the pointer field's own position, and printed that as if it were code — never a real address. A two-sided comparison with `--emit-member-addresses` over SwiftUICore / SwiftUI / SwiftData / Combine (host dyld cache, dump and interface, 8 pairs) was byte-identical: none of those frameworks carries such a witness, so the change is reachable only through a null implementation pointer. + +## Requirements + +- Swift 6.2+ +- Xcode 26.0+ diff --git a/Changelogs/0.19.0.md b/Changelogs/0.19.0.md new file mode 100644 index 00000000..bedf79de --- /dev/null +++ b/Changelogs/0.19.0.md @@ -0,0 +1,38 @@ +# 0.19.0 + +A minor release on top of `0.18.0`, carrying two independent pieces of work. Two output-neutral performance changes (evolution proposal 0019): the library's async entry points now run on swift-demangling's large-stack task executor, and multi-version preparation (`diff` / `evolution`) is parallel — implementation note `Documentations/Internal/LargeStackTaskExecutorAdoption.md`. And one correctness fix that **does change rendered output** (evolution proposal 0020): a class vtable slot is attributed by its method descriptor's own symbol rather than by the symbols at its implementation address, which identical code folding makes ambiguous. + +## What changed + +1. **Async entry points run on a 16 MB task executor.** The demangler decides per call whether to hop to its 8 MB pool by probing the calling thread's remaining stack; Swift Concurrency's cooperative threads carry 512 KB, so an async print loop paid one thread round trip per printed symbol. `MachOSymbols.LargeStackTaskExecution.run` sets swift-demangling 0.6.3's `LargeStackTaskExecutor` as the task executor preference around every library entry — indexer `prepare()`, `SwiftInterfaceBuilder.prepare()` / `printRoot()`, `SwiftDiffableInterfaceBuilder.prepare()`, the evolution builder's `prepare` / render entries, the diff renderer, the printer's four per-definition entries and the six `Dumpable.dump(using:in:)` conformers — so the whole pipeline demangles, prints and remangles inline. Hosts change nothing. Requires macOS 15 / iOS 18 / tvOS 18 / watchOS 11 / visionOS 2 at runtime; below that (or off Darwin) the body runs exactly as before. `LargeStackTaskExecution.isEnabled` (seeded by `MACHO_SWIFT_SECTION_LARGE_STACK_EXECUTOR=0`) turns it off process-wide. +2. **Cross-version preparation is parallel.** `AnySwiftEvolutionInterfaceBuilder.prepare(maximumConcurrentPreparations:)` (and the pack-generic façade) indexes the versions concurrently, defaulting to the processor count; `1` is the former oldest-first serial order. `swift-section diff` and `swift-section evolution` index their inputs the same way and gained `--jobs N`. The result is byte-identical to serial preparation; only event delivery interleaves. Intra-version parallelism is out of scope (MachOKit's file reads share one `FileHandle`). +3. **`Collection.concurrentMap(maximumConcurrency:_:)`** (`Utilities`): a windowed task-group map — source-ordered results, first failure rethrown, pending elements never started; cancelling the calling task stops submission and fails the call with `CancellationError`. +4. **Event delivery is serialized process-wide.** `SwiftIndexEvents.Dispatcher` takes one recursive lock around handler invocation, so a host handler shared by concurrently prepared versions is never entered concurrently (`Handler` still has no `Sendable` requirement). +5. **Labeled console diagnostics.** `ConsoleEventHandler(label:)` prints `[label]` after the timestamp; `diff` tags its sides `old` / `new`, `evolution` tags each version with its axis label (`AnySwiftEvolutionInterfaceBuilder.init` gained `eventHandlersPerVersion:`), snapshot inputs use their provenance label or file name. stderr only. +6. `MACHO_SWIFT_SECTION_LARGE_STACK_EXECUTOR` accepts `0` / `false` / `no` / `off` (case-insensitive) as off; anything else, or unset, is on. +7. **Vtable slots are attributed by their method descriptor's symbol.** Which member a class vtable slot belongs to used to be read from the symbols at the slot's implementation address — a mapping identical code folding destroys, since the linker merges every byte-identical body onto one address (SwiftUICore's empty-`ret` address carries 2878 symbols). `SwiftUI.GraphHost`'s four folded vtable methods printed as one correct name plus three coroutine resume functions of its *nested* `GraphHost.Data` struct. Attribution now reads the descriptor's own `Tq` method-descriptor symbol — one per member, at the descriptor's own address, so folding cannot reach it — and falls back to the implementation address only for the slots that carry no `Tq` (~28%), annotating that fallback when it lands on a folded address instead of presenting a guess as fact. The same evidence order is mirrored in the interface path, so its vtable-offset comments agree. Candidate matching in the fallback now goes through the member's direct declaration context, which stops a nested type's member from being accepted as the enclosing class's own. A slot whose implementation pointer is null is annotated rather than rendered as a bare lookup failure: dead-method elimination removed the body while the slot stayed for layout. Measured on SwiftUICore (iOS 18.5 arm64): 828 declaration lines change, all 7 nested-type bleeds gone with none introduced, `Symbol not found` 358 → 0, and no line regressed from a name to a `sub_` address. + +## Measured + +Release binaries, host dyld cache (macOS 26.5.2, 10-core Apple Silicon), two runs each; outputs byte-identical across every configuration. + +| Configuration | SwiftUICore dump | SwiftUICore interface | SwiftUI dump | SwiftUI interface | +|---|---|---|---|---| +| 0.18.0 (swift-demangling 0.6.0) | 48.8 s / 48.6 s | 56.4 s / 57.0 s | 79.4 s / 79.9 s | 87.5 s / 89.4 s | +| 0.19.0, executor off | 48.6 s / 48.7 s | 56.4 s / 55.7 s | 78.0 s / 79.7 s | 88.4 s / 89.7 s | +| 0.19.0, executor on (default) | **40.6 s / 40.4 s** | **47.0 s / 47.1 s** | **61.3 s / 60.2 s** | **71.2 s / 70.9 s** | + +`swift-section evolution` over three archived SwiftUI caches (macOS 15.5 / 26.5.2 / 27.0 beta 6): `--interface` 306.7 s → 151.9 s, lineage report 282.4 s → 139.4 s (executor off + `--jobs 1` → executor on + default parallelism), identical output. + +## Compatibility + +- Additive at the source level: no existing signature changed; `prepare()` keeps working through the new parameter's default. +- **Dependency floor**: swift-demangling `0.6.3 ..< 0.7.0` (was `0.6.0 ..< 0.7.0`). 0.6.1 re-ranked the demangler's pool QoS per hop and slowed `dump` / `interface` 3–4×; 0.6.2 fixed that; 0.6.3 adds the executor. Other pins unchanged. +- Rendered output is byte-identical **for the executor and parallelism changes** (dump and interface, verified with the rendering A/B harness, executor on and off). The vtable attribution fix deliberately changes output: slot names that were wrong under identical code folding are now right, and null-implementation slots carry a new comment. Anything diffing this release's `dump` / `interface` against 0.18.0's should expect those lines to move. +- Known rough edges in the new comments, tracked for the next release (`Roadmaps/2026-09-06-pr123-review-findings.md`): the null-implementation comment's wording overstates the cause (the declaration survives; only the body was eliminated), the ambiguity comment's count is the raw folded-symbol total rather than the number of candidates belonging to the type, and neither comment has a configuration switch yet. +- Snapshot `formatVersion` unchanged. + +## Requirements + +- Swift 6.2+ +- Xcode 26.0+ diff --git a/Documentations/Evolutions/0012-in-process-metadata-type-builder.md b/Documentations/Evolutions/0012-in-process-metadata-type-builder.md new file mode 100644 index 00000000..a22202aa --- /dev/null +++ b/Documentations/Evolutions/0012-in-process-metadata-type-builder.md @@ -0,0 +1,36 @@ +# 0012 - RuntimeMetadataTypeBuilder:TypeBuilder 的首个生产 conformer(node → 进程内活 metadata) + +- **状态**: Implemented +- **创建日期**: 2026-08-30 +- **最后更新**: 2026-08-31 + +## 摘要 + +swift-demangling 已完整移植上游 `TypeDecoder.h` 的遍历器(`TypeDecoder` + `TypeBuilder` 协议,含 `NodeReference` store-backed 变体与栈安全契约),但生产侧至今没有任何 conformer(唯一实现是测试里的 `StringTypeBuilder`)。本提案实现第一个生产 conformer `RuntimeMetadataTypeBuilder`——对标 Swift 运行时 `MetadataLookup.cpp` 里 `swift_getTypeByMangledName` 的核心 `DecodedMetadataBuilder`:把 demangle 出的 `Node` 树直接构建成进程内的活 metadata(`BuiltType == Metadata`)。这替代了目前「node remangle 成字符串 → `swift_getTypeByMangledNameInContext`」的往返(该入口还要求 mangled 字节位于可寻址内存、上下文与实参按运行时约定摆放),并为后续让 `SwiftSpecialization` 接受任意类型表达式实参(用户输入 `[Int: String]?` 之类)打底。 + +## 方案 + +以下为默认档下自行敲定的假设,未获反对即按此执行: + +- **位置与命名**:`Sources/SwiftInspection/RuntimeMetadataTypeBuilder.swift`(SwiftInspection 是「运行时 metadata 分析」的既有归属,`MetadataReader` 在此;它已依赖 Demangling 与 MachOSwiftSection,无需新增依赖边)。关联类型:`BuiltType = Metadata`、`BuiltTypeDecl` = 类型上下文描述符 wrapper、`BuiltProtocolDecl` = Swift/ObjC 双态的 protocol descriptor 引用。仅进程内(`MachOImage` / in-process)可用,与现有运行时路径一致。 +- **结构类型走运行时官方入口**,在 `MachOSwiftSectionC` 补齐 C 桥接(现仅有 `swift_getTypeByMangledName*` / `swift_conformsToProtocol` / `swift_getAssociatedTypeWitness`):tuple → `swift_getTupleTypeMetadata`,function → `swift_getFunctionTypeMetadata`,existential → `swift_getExistentialTypeMetadata`,metatype → `swift_getMetatypeMetadata` / `swift_getExistentialMetatypeMetadata`,ObjC class → `objc_getClass` + `swift_getObjCClassMetadata`。sugar 节点(Optional / Array / Dictionary / InlineArray)按 stdlib 类型的 bound generic 处理。 +- **nominal 类型**:`createTypeDecl` 把节点解析为描述符——symbolic reference 节点直接持有描述符指针(`node.index` 即进程内地址,上游 `getIndex()` 约定);命名节点走三级解析:调用方注入的 `nominalTypeDescriptorResolver` seam(有索引的宿主接进来)→ 非泛型命名节点 remangle 后经 `swift_getTypeByMangledNameInEnvironment` 按名查询(运行时自己的全镜像搜索)→ 标准库常用泛型(Array / Dictionary / Optional / Range 等 17 个)从内置表取描述符(经任一已知实例化的 metadata 反查,进程内一次性缓存)。`createNominalType` 统一走 `createBoundGenericType`(上游同构);key-argument 收集按 `_gatherGenericParameters` + `_checkGenericRequirements` 语义在 builder 内自实现:全 cumulative 参数列表的 written args(父级实参从 parent metadata 的 generic-argument 区读回,class 的 resilient/非 resilient 偏移分支与 `RuntimeFunctions` 同构)、key 参数 metadata 先行、key protocol requirement 的 PWT 按 requirement 顺序经 `swift_conformsToProtocol` 追加,requirement subject(`A` / `A.Element`)用携带 written-args 绑定环境的嵌套 builder 自举解码。 +- **泛型参数**:builder 可携带一个可选绑定环境(`(depth, index) → Metadata`);无绑定时 `createGenericTypeParameterType` 抛 typed `TypeLookupError`——诚实降级,绝不造值。 +- **首版明确拒绝**(typed error,不 fabricate):SIL 系(`createImplFunctionType` / SILBox)、pack expansion、`resolveOpaqueType`。拒绝面与运行时 `DecodedMetadataBuilder` 一致或更窄,后续按需求逐项放开。 +- **验证**:往返 parity 测试(`RuntimeMetadataTypeBuilderTests`)——对活类型取 `_mangledTypeName`,demangle 成节点经 builder 重建,产物必须与原类型指针相等(运行时 metadata 全局唯一化,指针相等即语义相等;期望值是独立的类型字面量,非重算)。覆盖:标准库泛型、tuple / 函数 / metatype / existential、ObjC class 与协议 existential、resolver seam 下的约束泛型(Hashable PWT)、dependent-member requirement subject(assocty witness)、嵌套泛型的父级实参合并、绑定环境替换,以及无绑定参数 / 无 resolver 命名泛型的 typed error 拒绝。 +- **本提案不动 `GenericSpecializer` 现有路径**。接线(如 `SpecializationSelection.Argument` 新增 `.typeExpression(Node)` case)是后续独立批次,届时在本提案决策日志登记或另开提案。 + +## 决策日志 + +| 日期 | 决定 | 理由 | +|------|------|------| +| 2026-08-30 | Created as Draft | 用户定向:TypeBuilder 的第一个生产 conformer 做 in-process metadata builder | +| 2026-08-30 | 不从 Swift 源码搬移三个 builder(ASTBuilder / TypeRefBuilder / DecodedMetadataBuilder),只在本项目实现协议 | 遍历器已由 swift-demangling 移植并按上游审计;ASTBuilder 依赖编译器 AST 无场景,TypeRefBuilder 对应的离线布局本项目已有更强实现 | +| 2026-08-30 | 离线 layout 引擎不重构到 TypeDecoder 上 | `StaticTypeLayoutResolver` 在 spare-bits XI、parameter pack 等处已强于官方 `TypeLowering`;重构触发强制渲染 A/B 验证,churn 大收益低 | +| 2026-08-30 | 用户批准方案(默认档假设无异议),状态 Draft → In Progress | 轻量档:点头即动手 | +| 2026-08-30 | 不复用 `GenericSpecializer` 做 bound-generic 构建,key-argument 收集在 builder 内按运行时语义自实现 | `makeRequest` 会为每个参数急切枚举候选(无约束参数 = 全镜像每个类型一个 Candidate),且 PWT 解析硬依赖 indexer——交互流形状,不适合逐节点解码;进程内 `swift_conformsToProtocol` 即是运行时自己的解析路径 | +| 2026-08-30 | 命名泛型增设标准库描述符内置表 + `nominalTypeDescriptorResolver` seam;本项目 `MetadataReader` 从不产出 `.typeSymbolicReference`(都解析成命名树),命名路径是主路径而非回退 | 字符串 mangling 里 `Sa` / `SD` 等标准替换展开成命名节点,没有表则 `Array` 都建不出;索引级解析留给有索引的宿主注入 | +| 2026-08-30 | `createObjCClassType` 返回 realized class 指针本身(`swift_getInitializedObjCClass`),不走 `swift_getObjCClassMetadata` | 实测 canonical `Any.Type`(`NSObject.self`、按名查询结果)就是 class 指针;wrapper 是另一个 metadata 身份,会分裂泛型实例化缓存,且在测试进程里打印 wrapper 触发 SIGSEGV | +| 2026-08-30 | 首版拒绝面在方案基础上补列 constrained existential(上游 DecodedMetadataBuilder 同样拒绝)与 value generic 实参(`InlineArray<5, _>`)、非 key 或非 type 的父级参数 | 诚实降级:typed error 优于错值;后续按需求逐项放开 | +| 2026-08-30 | 实现完成:`RuntimeMetadataTypeBuilderTests` 17 用例全绿;全量 `swift test --skip IntegrationTests` 1572 测试 / 294 suite 通过(原始退出码 0)。本批次以远端依赖构建(本地 `MachOObjCSection` 兄弟副本被 pin 在 0.7.103,缺 `ObjCIndexing`,未动它) | 待与代码同批合入共享分支时置 `Implemented` 并取号 | +| 2026-08-31 | In Progress → Implemented,编号 draft → 0012 | 随 `next` rebase 到 `main` 之上、与代码同批落入共享分支,兑现上一行「待与代码同批合入共享分支时置 `Implemented` 并取号」。取号按 README 规则:fetch 全部远程分支后取 `Evolutions/` 编号全局最大值 0011 + 1(取号当时 `draft-swift-evolution-interface-builder` / `draft-unify-interface-renderers` 两份虽已 `Implemented` 却仍未取号,按规则 `draft-` 不占号;该两份随后于同日补取 0013 / 0014)。同批完成互链改名:ProjectEvolutionLog 第 52 节(标题占位「落地时定节号」一并兑现)、AGENTS.md、任务报告 | diff --git a/Documentations/Evolutions/draft-swift-evolution-interface-builder.md b/Documentations/Evolutions/0013-swift-evolution-interface-builder.md similarity index 97% rename from Documentations/Evolutions/draft-swift-evolution-interface-builder.md rename to Documentations/Evolutions/0013-swift-evolution-interface-builder.md index 2ff95c92..9780bb7b 100644 --- a/Documentations/Evolutions/draft-swift-evolution-interface-builder.md +++ b/Documentations/Evolutions/0013-swift-evolution-interface-builder.md @@ -1,9 +1,9 @@ -# Draft - SwiftEvolutionInterfaceBuilder:ABI 演进的并集注解接口渲染 +# 0013 - SwiftEvolutionInterfaceBuilder:ABI 演进的并集注解接口渲染 - **状态**: Implemented - **作者**: JH - **创建日期**: 2026-08-25 -- **最后更新**: 2026-08-25 +- **最后更新**: 2026-08-31 - **所属愿景**: 无 - **关联提案**: 无(与 `0006` 之前落地的 SwiftDiffing 系列同域:`ABIDiffer` / `ABIEvolutionBuilder` / `SwiftDiffableInterfaceRenderer` 是其直接前作) - **实现分支 / PR**: `feature/swift-evolution-interface-builder` @@ -378,3 +378,4 @@ swift-section evolution --interface --dyld-shared-cache -n SwiftUICore cache17 c | 2026-08-26 | 属性打印修正(用户实机反馈) | SwiftUI 三缓存轴 dump 暴露两处成员多行渲染缺陷:① accessor 块双重缩进——variable/subscript printer 按 `level` 给块内行烘焙**绝对**缩进,而 evolution 格式层又按行加层级缩进;修法:成员一律以 printer level 0 渲染(块变相对缩进),格式层统一缩进即精确。② 注解沉到 accessor 块收尾 `}`——「附着末行」规则对多行成员选错行;修法:锚点分靶(成员锚**首行**即声明行——属性内联无前置行;容器 header 锚末行即带 `{` 的声明行)。两侧 `diff --interface` 路径仍带同源缺陷 ①(本批不动,另行处理);opaque `some` 裸打印为 diff 路径既有缺口(printer 未接 `addExtraDataProvider` 的 opaque 展开线),同样另行立项。 | | 2026-08-25 | 收尾裁决:术语表 | 新术语「union interface(并集接口)」「lifecycle annotation(生命周期注解)」已登记进项目术语表(同批次)。 | | 2026-08-25 | 提问结论(第二轮) | 注解格式定为位图 + 事件短语 + 头部图例(否:纯短语、伪 @available);未变声明渲染但不注解(否:每行位图;`--changes-only` 未采纳、记入将来方向);modified 只渲染最新代际 + 变更注解(否:逐代际多行);泛型形态定为 pack 异构 init(@available 门控)+ 同质数组 init 双轨(用户自定答案),实现收敛为非泛型公开类 + 内部擦除。 | +| 2026-08-31 | 补取编号 draft → 0013 | 本案 2026-08-25 已置 `Implemented`,但落地 commit 漏了 README 规定的「合入共享分支时取号」一步,文件名与状态表编号列一直停在 `draft`。本次连同 0012 的取号一并补齐:fetch 全部远程分支后按落地先后排号(本案 08-25 落地在前取 0013,0014 的统一渲染器提案 08-26 在后)。同批完成互链改名:AGENTS.md、Glossary、ProjectEvolutionLog 第 47 节、ABIEvolutionDesign、任务报告、0014 的「关联提案」行 | diff --git a/Documentations/Evolutions/draft-unify-interface-renderers.md b/Documentations/Evolutions/0014-unify-interface-renderers.md similarity index 95% rename from Documentations/Evolutions/draft-unify-interface-renderers.md rename to Documentations/Evolutions/0014-unify-interface-renderers.md index 398b6020..5fc5f3c4 100644 --- a/Documentations/Evolutions/draft-unify-interface-renderers.md +++ b/Documentations/Evolutions/0014-unify-interface-renderers.md @@ -1,11 +1,11 @@ -# Draft - 统一 diff / evolution 接口渲染器的结构遍历核心 +# 0014 - 统一 diff / evolution 接口渲染器的结构遍历核心 - **状态**: Implemented - **作者**: JH - **创建日期**: 2026-08-26 -- **最后更新**: 2026-08-26 +- **最后更新**: 2026-08-31 - **所属愿景**: 无 -- **关联提案**: [draft-swift-evolution-interface-builder](draft-swift-evolution-interface-builder.md)(被统一的两条渲染路之一即其产物;其决策日志 2026-08-26「属性打印修正」条目记录的 diff 路径同源缺陷,本提案顺带修复) +- **关联提案**: [0013-swift-evolution-interface-builder](0013-swift-evolution-interface-builder.md)(被统一的两条渲染路之一即其产物;其决策日志 2026-08-26「属性打印修正」条目记录的 diff 路径同源缺陷,本提案顺带修复) - **实现分支 / PR**: `feature/swift-evolution-interface-builder`(PR #114 同分支追加) - **配套文档**: [TaskReports/2026-08-26-unify-interface-renderers.md](../Internal/TaskReports/2026-08-26-unify-interface-renderers.md)(过程复盘);不另立实现说明,裁决见决策日志 @@ -174,3 +174,4 @@ variable / subscript)的块内缩进从双重变为正确;单行成员、hea | 2026-08-26 | 顺手修正:协议 header 失败事件的 kind | diff 路原硬编码 `.type`(协议 header 失败也报 `.type`);策略化后如实传 `.protocol`,与 evolution 路一致。无测试钉旧值。 | | 2026-08-26 | In Progress → Implemented | 落地三步完成:核心迁移(evolution e2e 字节钉子原样通过)→ diff 策略 + 缩进修正(修前必红的 `DiffMemberIndentationTests` 转绿)→ 文档批次。SwiftInterface + SwiftDiffing + SwiftSectionCommand 三 target 209 tests / 34 suites 全绿(原始退出码 0)。 | | 2026-08-26 | 收尾裁决:配套文档与术语表 | 不另立实现说明——发射策略接缝的取舍已完整落在遍历器/策略的代码文档注释与 AGENTS.md `InterfaceUnionWalker` 条目里,单独成篇只会复述(反向判据命中);过程复盘在任务报告。术语表登记「emission strategy(发射策略)」。 | +| 2026-08-31 | 补取编号 draft → 0014 | 与 0013 同因:本案 2026-08-26 已置 `Implemented`,落地 commit 漏了 README 规定的取号一步。按落地先后排在 0013(其被统一的渲染路之一即 0013 的产物)之后。同批完成互链改名:AGENTS.md、Glossary、ProjectEvolutionLog 第 49 节、ReviewAdjudications A20、任务报告 | diff --git a/Documentations/Evolutions/0015-type-name-resolver-role-split.md b/Documentations/Evolutions/0015-type-name-resolver-role-split.md new file mode 100644 index 00000000..7471e765 --- /dev/null +++ b/Documentations/Evolutions/0015-type-name-resolver-role-split.md @@ -0,0 +1,145 @@ +# 0015 - TypeNameResolvable 角色化拆分:printer 查询解析器按能力分协议 + +- **状态**: Implemented +- **作者**: JH +- **创建日期**: 2026-09-01 +- **最后更新**: 2026-09-01 +- **所属愿景**: 无 +- **关联提案**: [0009-type-indexing-revival](0009-type-indexing-revival.md)(`swiftName(forCName:category:)` 在该案加入本协议,是协议持续增宽的实例之一)、[0011-opaque-primary-associated-type-attribution](0011-opaque-primary-associated-type-attribution.md)(`opaqueType(forNode:index:)` 的消费语义) +- **实现分支 / PR**: `next` 直落 +- **配套文档**: [Internal/Modules/SwiftInterface.md](../Internal/Modules/SwiftInterface.md)(`SwiftInterfaceBuilderExtraDataProvider` 条目同批改写);不另立实现说明,裁决见决策日志 + +## 摘要 + +`SwiftPrinting.TypeNameResolvable` 是 printer 外挂查询解析器的注册协议,三个方法 +(`moduleName(forTypeName:)` / `swiftName(forCName:category:)` / `opaqueType(forNode:index:)`) +全部带默认 `nil` 实现。本提案把它拆成一个空标记协议 `TypeNameResolving` + 三个单方法角色协议 +(`ModuleNameResolving` / `CImportedNameResolving` / `OpaqueTypeResolving`,**无默认实现**), +provider 只声明自己真正服务的角色;`SwiftDeclarationPrinter` 在注册时按角色分箱 +(`as?` 只发生在 `addTypeNameResolver` 里,打印热路径零转型),每个 delegate 查询只走 +能回答它的那一箱。`SwiftInterfaceBuilderExtraDataProvider` 同步与 resolver 概念解耦: +退化为纯生命周期钩子(`Sendable` + `setup()`),`addExtraDataProvider` 改为按能力转发。 +输出逐字节不变。 + +## 动机 + +用户指出该协议同时违反接口隔离原则(ISP)与开闭原则(OCP),核实成立: + +- **ISP:没有任何一个真实 provider 实现全部三个方法。** `SwiftInterfaceBuilderOpaqueTypeProvider` + 只实现 `opaqueType`,`TypeIndexing.SwiftInterfaceBuilderTypeNameProvider` 只实现两个名字方法, + 其余全靠默认 `nil` 实现凑数;唯一「全实现」的 `SwiftDeclarationPrinter` 是多路复用器,不算数。 +- **OCP:每加一个查询能力都要修改公共契约。** git 历史证实协议从 opaque-type 起步, + 先后增宽 `moduleName`(TypeIndexing 一期)与 `swiftName` + `category` 参数(提案 0009), + 每次都要同时动协议定义、默认扩展、printer 的三处转发,且波及继承它的 + `SwiftInterfaceBuilderExtraDataProvider` 整条线。 +- **默认实现是静默失效的温床。** 用 protocol extension 默认返回 `nil` 模拟「可选 delegate 方法」, + 意味着 conformer 签名漂移(如 `swiftName` 加 `category:` 那次)不会编译报错,只是永远不被调用。 +- **热路径无效 fan-out。** delegate 方法按「每个打印出来的标识符」调用,每次 `moduleName` + 查询都异步跳一遍全部 resolver,包括只会返回 `nil` 的 opaque provider。 + +## 前期调研 + +- 全部消费面已枚举:`NodePrintable.printModule`(`moduleName`,带 `Ref` 后缀剥除回退)、 + `TypeNodePrintable`(`swiftName` / `opaqueType`)、`SwiftDeclarationPrinter` 的三处 + `asyncFirstNonNil` fan-out、`SwiftInterfaceBuilder.addExtraDataProvider` 的注册转发。 + `Sources` 内对旧协议名的引用仅此数处;`Tests` 无直接引用(全部经 `addExtraDataProvider`)。 +- 消费端聚合接口(内部协议 `NodePrintableDelegate`,node printer 持有的 weak delegate) + **不是病灶**:printer 是唯一 conformer 且真要回答全部查询,胖在消费端是职责面本身。 +- `SwiftDeclarationPrinter` 为 `@_spi(Support)`,其 `typeNameResolvers` 公开属性无外部读取方, + 可安全改为私有分箱存储。 + +## 提议方案 + +1. **角色协议**(`SwiftPrinting/NodePrintables/TypeNameResolving.swift`,`CImportedTypeNameCategory` + 随迁):空标记协议 `TypeNameResolving: Sendable` 作注册入口;三个单方法角色协议 + `ModuleNameResolving` / `CImportedNameResolving` / `OpaqueTypeResolving` 各自 refine 标记协议, + **不带默认实现**——签名漂移在 conformer 处编译期报错,而非静默脱钩。 +2. **注册分箱**:`SwiftDeclarationPrinter.addTypeNameResolver(_ resolver: any TypeNameResolving)` + 注册时逐角色 `as?` 分箱进私有 `TypeNameResolverRegistry`(三个角色数组);命中零角色触发 + debug `assert`(注册了却永远不被咨询几乎必是忘了声明角色 conformance)。三个 delegate + 查询各走自己那一箱,打印热路径零转型、零无效跳转。 +3. **`SwiftInterfaceBuilderExtraDataProvider` 解耦**:不再继承 resolver 协议,只余 + `Sendable` + 默认为空的 `setup()`(生命周期钩子);`addExtraDataProvider` 改为 + `as? any TypeNameResolving` 命中才转发给 printer。纯 setup 型 provider(如只预热缓存) + 从此是合法形态;builder 将来长出别的能力面走同一分发路数,provider 协议不再被动。 +4. **`TypeNameResolvable` 删名不留 typealias**:留一个指向标记协议的别名会让旧 conformer + 静默编译通过但永远不被调用(恰是要消灭的失效形态);删名让下游在编译期撞见、有意识迁移。 +5. 现有两个 provider 只改 conformance 声明,方法体零改动。 + +### 非目标 + +- 不合并 `moduleName` 与 `swiftName` 两角色。二者虽同源(都由 `TypeDatabase` 回答、同属 C + 导入名归属),但打印侧是两个独立调用点,分开更彻底、合并收益只是少一个协议名。 +- 不做完全开放的查询注册表(泛型 `Query → Answer` 的 type-erased registry)。对三个方法的 + 表面积而言,type erasure + async + `Sendable` 的机械成本远超收益,且「系统支持哪些查询」 + 会从协议定义可见退化为翻注册代码才知道。新查询能力在 printer 里的提问点无论如何省不掉, + 角色协议已消掉了其余全部修改面(协议、旧 conformer、分发基础设施)。 + +## 替代方案考量 + +- **保持现状(Cocoa optional-delegate 惯用法)**:默认实现确实让「加方法」对既有 conformer + 源码兼容,但代价是把编译期检查换成静默失效,且 ISP 违反持续累积。否。 +- **enum 查询 + 单方法**(`resolve(_ query: TypeNameQuery) async -> ...`):加 case 对 + exhaustive switch 的 conformer 同样是修改,还丢掉每查询的专属签名。否。 +- **`SwiftInterfaceBuilderExtraDataProvider` 继承标记协议**(初版草图):转发可无条件、 + 忘声明角色会撞注册 assert;但强迫「所有 extra-data provider 必是 resolver」,与原病灶同构。 + 用户点破后改为按能力转发,接受「setup-only 合法化后忘声明角色退回静默」的已知代价 + (写 provider 必跑一次即暴露)。 + +## 影响 + +### 源码兼容性(source compatibility) + +**破坏性**:`TypeNameResolvable` 删除;`SwiftDeclarationPrinter.typeNameResolvers` 公开属性 +(`@_spi(Support)`)移除;`SwiftInterfaceBuilderExtraDataProvider` 不再自带三个查询方法。 +下游自定义 provider 需把 conformance 从旧协议改为所服务的角色协议——编译期报错,机械迁移。 +`addTypeNameResolver` / `addExtraDataProvider` / `removeAll*` 签名语义不变。 + +### ABI 兼容性(条件项) + +不适用——源码分发,无 ABI 承诺。 + +### 下游影响 + +RuntimeViewer 等下游若有自定义 provider,重编译时收到编译错误,按角色声明即可; +仅用库内两个 provider 的调用方零改动。 + +### 文档与示例 + +`Internal/Modules/SwiftInterface.md` 的 `SwiftInterfaceBuilderExtraDataProvider` 条目同批改写; +AGENTS.md 架构节未提及旧协议名(provider 挂接 API 不变),判定无需同步。 + +## API 演进与废弃策略 + +删名即迁移信号,不设废弃期(源码分发、下游可数)。将来新增查询能力的既定路径: +新角色协议 refine `TypeNameResolving` + registry 加一箱 + printer 加一个转发方法 + 提问点, +旧协议与旧 conformer 零触碰。 + +## 验收 + +- `swift build` 绿(原始退出码 0)。 +- `SwiftInterfaceTests` 131/131 全绿,含字节级 interface 快照 + (`SymbolTestsCoreInterfaceSnapshotTests`)与 opaque provider / TypeName provider 的 e2e—— + 新分发路径下输出逐字节不变的直接证据。 +- `SwiftDumpTests` + `SwiftSectionCommandTests` 97/97 全绿。 +- 不跑 rendering A/B 全量:改动为小规模机械重构(7 文件、零行为变化),字节级快照钉子已覆盖 + 经新分发路径的打印主路(与 0014 同判据)。 +- 过程记录:首轮测试 25 处失败经 AGENTS.md 环境漂移程序甄别为 fixture 二进制过期 + (`FinalMembersTest` 在源码而不在 8 月 6 日的共享二进制里,`strings` 计数 0),按既定 + xcodebuild 程序重建后与本改动无关的失败全部消失。 + +## 落地步骤 + +一批完成:角色协议新文件 → printer 分箱 → builder 解耦转发 → 两 provider 改 conformance → +测试验证 → 文档批次(本提案 + Evolutions/README + 模块文档 + ProjectEvolutionLog)→ 提交推送。 + +## 决策日志 + +| 日期 | 变更 | 说明 | +|------|------|------| +| 2026-09-01 | Created | 用户指出 `public protocol TypeNameResolvable: Sendable` 违反 SOLID(ISP + OCP)。对话中三轮定稿:角色拆分方向 → 完整改法草图(含删默认实现、注册分箱、删名不留别名)→ 用户点破 `SwiftInterfaceBuilderExtraDataProvider` 应与 resolver 概念完全解耦。 | +| 2026-09-01 | Accepted(对话批准,流程降档) | 用户「可以,改吧,改完直接提交推送就好」——按全局规则降档直做,完整澄清提问以对话讨论代偿;提案与代码同批落地,直取 Implemented。 | +| 2026-09-01 | 关键裁决:删默认实现 | 角色协议单方法、conformer 只认领所服务的角色后,默认实现失去存在理由;删掉换回编译期检查。 | +| 2026-09-01 | 关键裁决:ExtraDataProvider 不继承标记协议 | 初版草图令其继承;用户指出这与原病灶同构(强迫所有 provider 是 resolver)。改为 `addExtraDataProvider` 按能力 `as?` 转发,setup-only provider 合法化;接受「忘声明角色退回静默」的已知代价。 | +| 2026-09-01 | In Progress → Implemented | 实现 + 验证完成(构建绿、131 + 97 tests 全绿、字节级快照不变);落地取号 0015(全局最大 0014 + 1)。 | +| 2026-09-01 | 收尾裁决:配套文档与术语表 | 不另立实现说明——设计取舍完整落在本提案与角色协议/registry 的代码文档注释里,单独成篇只会复述;模块文档 `SwiftInterface.md` 条目同批改写。无新造术语(标记协议/角色协议为通用概念),术语表不登记。 | diff --git a/Documentations/Evolutions/0016-exported-only-interface.md b/Documentations/Evolutions/0016-exported-only-interface.md new file mode 100644 index 00000000..9e9a6d04 --- /dev/null +++ b/Documentations/Evolutions/0016-exported-only-interface.md @@ -0,0 +1,66 @@ +# 0016 - Interface 只打印导出声明(`--exported-only`) + +- **状态**: Implemented +- **创建日期**: 2026-09-02 +- **最后更新**: 2026-09-02 +- **关联提案**: [0008](0008-interface-header-and-export-status-annotations.md)(导出状态标注——本提案复用它的导出事实层与成员判定) +- **实现分支**: `feature/exported-only-interface` +- **配套文档**: [ExportedOnlyInterfaceFiltering.md](../Internal/ExportedOnlyInterfaceFiltering.md)(实现说明) + +## 摘要 + +给 `SwiftInterfaceBuilder` / `SwiftDeclarationPrinter` 加一个打印期过滤开关 +`SwiftDeclarationPrintConfiguration.printExportedDeclarationsOnly`(CLI:`swift-section interface --exported-only`), +打开后 interface 只输出**镜像导出**的声明:类型 / 协议按各自描述符符号(`…Mn` / `…Mp`)是否在 export trie 里裁决, +成员沿用提案 0008 的成员判定(本体或 `Tj`/`Tq`/`Tu`/`TjTu` 派生符号任一导出即算导出), +扩展按「被扩展类型 / 遵循协议是否为本镜像内未导出声明」裁决。提案 0008 只做**标注**(`// not exported`), +本提案是它的**过滤**形态;默认关闭,默认输出字节不变。 + +## 方案 + +**语义仍是符号表事实,不是访问级别猜测**(与 0008 一致):只在裁决为「确定未导出」(`false`)时删; +镜像没有导出信息、成员没有符号证据、重整名失败等一切拿不到证据的情形(`nil`)一律保留——过滤绝不靠猜。 +因此 `-enable-testing` 构建里的 `internal` 声明、`@usableFromInline` 类型会被保留,这是导出表的真实状态。 + +裁决规则: + +| 对象 | 判据 | 备注 | +|------|------|------| +| 类型 | `_$sMn`(nominal type descriptor)在 export trie | 从 `TypeName.node` 重整(剥 `.type` 壳与 bound-generic 壳);C 导入类型的外来描述符不导出,`--show-c-imported-types` 下会被过滤 | +| 协议 | `_$sMp`(protocol descriptor)在 export trie | 同上 | +| 成员(函数 / 计算属性 / 下标 / 协议 requirement) | 0008 的 `exportVerdict(forSymbolNames:)`:任一符号(含派生形态)导出即保留 | `override` / `@objc` 成员豁免(保留);协议 requirement 经 `Tq` 派生形态命中 | +| 存储属性 | accessor 组的同一判定 | 没有 accessor 符号的字段「不可检」→ 保留;`override` / 有 `To` ObjC 入口的保留 | +| 枚举 case | 不判 | 没有符号;枚举被保留则 case 全保留 | +| 全局变量 / 函数 | 成员判定 | | +| 扩展 | 被扩展类型或遵循协议是**本镜像内**未导出声明 ⇒ 整个删除 | 目标在其它镜像的一律保留;fixture 上 44 个未导出 `Mc` 全涉及私有类型,无需再查 conformance 描述符 | +| 空扩展 | 普通扩展过滤后没有任何成员 / 嵌套声明 ⇒ 删除 | conformance 扩展即使空体也保留(它本身就是声明) | +| 嵌套类型 / 协议、特化子类型、协议默认实现扩展块 | 各按自身判据;父级被删则整块消失 | | + +**落点在打印期**(用户选定):索引出的模型保持完整,RuntimeViewer 浏览、ABI diff / snapshot / evolution 不受影响。 +三个打印入口(`printTypeDefinition` / `printProtocolDefinition` / `printExtensionDefinition`)拆成「过滤壳 + builder 体」, +成员循环、字段循环、`printRoot` 的全局块各加一处 `where` 过滤。扩展的「本镜像内」判定需要索引器的类型 / 协议表, +打印器不持有索引器,所以引入 `ExportFilterScope`(本镜像内未导出的 `TypeName` / `ProtocolName` 集合), +由 `SwiftInterfaceBuilder.printRoot()` 在开关打开时从索引器构建并装到打印器上;绕过 `printRoot` 的宿主自行调用 +`installExportFilterScope(types:protocols:)`,不装则扩展过滤退化为「保留」(fail-open)。 + +**已知限制**:过滤按声明进行,不改写引用——导出成员的签名里仍可能引用被过滤掉的类型 +(fixture 里 `typealias Body = Structs.PrivateProtocolTest` 就是一例),与真实二进制里 `some P` 解析到私有类型的情形同构。 +`dump` 路径不在本提案范围(用户选定)。 + +**验证**:`SymbolTestsCore`(library-evolution Release,`ENABLE_TESTABILITY` 打开,故只有 `private` 声明未导出)上的端到端测试 +钉住类型 / 协议 / 协议默认实现扩展 / conformance 扩展 / 嵌套私有类型 / 成员 / 全局各一例的删除,以及 `Tj` 导出、`@objc`、`override` +三类保留;另用即时编译的 library-evolution fixture(无 `-enable-testing`)钉住 `internal` 类型 / 成员 / 存储属性 / 空扩展规则。 +CLI flag 解析测试与默认关闭。 + +## 决策日志 + +| 日期 | 决定 | 理由 | +|------|------|------| +| 2026-09-02 | Created;一轮澄清(三题) | 用户原话「SwiftInterface实现只打印exported的方法和类型」 | +| 2026-09-02 | 过滤放打印期,不放索引期 | 用户选定;模型完整、diff / RuntimeViewer 不受影响,改动集中在打印器 | +| 2026-09-02 | 存储属性按 accessor 判定过滤,无证据者保留 | 用户选定;与 0008 标注一致,接近 `.swiftinterface` 对 resilient 类型的做法 | +| 2026-09-02 | 只做 `interface`,`dump` 不动 | 用户选定 | +| 2026-09-02 | 类型级判据用 `Mn` / `Mp` 描述符符号,而非 `Ma` 或 offset→符号反查 | fixture 上导出 `Mn` 集合与 `Ma` 完全一致;按名字查 trie 在 strip 过的镜像上依然给出确定的 `false`,offset 反查在 strip 后只能答 `nil` | +| 2026-09-02 | 三题均选推荐项,视为批准,直接进入 In Progress | 轻量档流程:一轮提问、点头即动手 | +| 2026-09-02 | 类型级判据改为「先反查描述符 offset 处的符号,重整名只兜底且拒绝 `.extension` 上下文」 | fixture 交叉验证发现带约束扩展里的公开嵌套类型被纯重整名判据误删——编译器只 mangle 扩展自身的 requirement,模型节点带完整签名;上一行「按名字查优于 offset 反查」的理由仍成立,故 offset 反查为第一腿、名字查询降为兜底。详见实现说明「与提案的差异」 | +| 2026-09-02 | Implemented;落地编号 0016(远端 `next` 最大 0015)。配套实现说明 [ExportedOnlyInterfaceFiltering.md](../Internal/ExportedOnlyInterfaceFiltering.md) 已登记;新术语「exported-only 过滤」已入 Glossary | 三套 22 测试 + 回归 290 测试全绿,fixture 全量交叉验证零残留 | diff --git a/Documentations/Evolutions/0017-macho-dependencies-module.md b/Documentations/Evolutions/0017-macho-dependencies-module.md new file mode 100644 index 00000000..2d29887c --- /dev/null +++ b/Documentations/Evolutions/0017-macho-dependencies-module.md @@ -0,0 +1,74 @@ +# 0017 - 依赖闭包下沉为 MachODependencies 模块:两套依赖加载合一 + +- **状态**: Implemented +- **作者**: JH +- **创建日期**: 2026-09-02 +- **最后更新**: 2026-09-02 +- **所属愿景**: 无 +- **关联提案**: [0009-type-indexing-revival](0009-type-indexing-revival.md)(`SwiftInterfaceBuilderDependencies` 的消费者 TypeIndexing 为何只能吃直接依赖) +- **实现分支 / PR**: `feature/macho-dependencies-module`(worktree `.worktrees/MachOSwiftSection-MachODependencies`)→ `next` +- **配套文档**: [Internal/Modules/MachODependencies.md](../Internal/Modules/MachODependencies.md)(模块参考)、[Internal/TaskReports/2026-09-02-macho-dependencies-module.md](../Internal/TaskReports/2026-09-02-macho-dependencies-module.md)(任务报告) + +## 摘要 + +仓库里目前有**两套**「找依赖二进制」的实现,互不复用、规则也不一致: + +- `SwiftLayout/ImageUniverse+DependencyClosure.swift` —— **传递闭包**:BFS 递归解析每个镜像的 `LC_LOAD_DYLIB`,按 bare name 去重,dyld cache 首查时一次性建索引。搜索路径枚举 `LayoutDependencySearchPath` 是公开的,但遍历函数与 `MachOFileDependencyLocator` 都是文件私有,别的功能拿不到。 +- `SwiftInterface/SwiftInterfaceBuilderDependencies.swift` + `DependencyPath.swift` —— **只取一层直接依赖**,按 install path 精确匹配 cache 镜像,供 TypeIndexing 的 `--resolve-c-module-names` 使用。绑定在接口构建器的类型上。 + +本提案新建底层 target **`MachODependencies`**(只依赖 MachOKit + MachOKitExtensions + Utilities,不碰 Swift metadata),把「搜索路径 → 定位器 → 遍历(直接 / 传递)→ 依赖镜像集合 + 未解析清单」抽成独立可复用的 API;上述两处消费者改为薄包装,各自语义保持(layout 传递、interface 直接)。默认输出逐字节不变。 + +## 方案 + +### 新模块 API(`Sources/MachODependencies/`) + +- `DependencySearchPath`:`.machOFile(path:)` / `.dyldSharedCache(path:)` / `.systemDyldSharedCache`,沿用 SwiftLayout 的拼写。 +- `DependencyLoadName.bareImageName(of:)`:load name(`@rpath/Foo.framework/Versions/A/Foo`、`/usr/lib/swift/libswiftCore.dylib`)→ bare name(`Foo`、`libswiftCore`)。规则与 MachOKit `MachOImage(name:)` 完全一致(末段路径去首个扩展名),这是与 MachOKit 的契约。 +- `DependencyLocating` 协议 + 两个实现:`InProcessDependencyLocator`(经活动 dyld,`MachOImage(name:)`,**先归一 bare name**)、`FileDependencyLocator`(显式文件 + dyld cache,cache 首查时一次性建索引,避免 `O(依赖数 × cache 大小)`)。 +- `DependencyTraversal`:`.direct` / `.transitive`。 +- `DependencyClosure`:`root`、`images`(解析顺序:direct 为 load command 顺序,transitive 为 BFS;按 bare name 去重、不含 root)、`unresolvedLoadNames`、`searchPathLoadFailures`。便利构造 `init(root: MachOImage, traversal:)`、`init(root: MachOFile, searchPaths:, traversal:)`;底层 `init(root:traversal:locator:)` 供注入自定义定位器与测试。 +- `MachOFoundation` 加 `@_exported import MachODependencies`(与其它 MachO* 基础 target 一致),下游模块零 import 变更即可用;同时作为独立 library product 暴露。 + +### 匹配规则(合并时必须二选一,取并集) + +- 定位顺序:**install path 精确匹配优先**(SwiftInterface 现行规则)→ **bare name 排序匹配兜底**(SwiftLayout 现行规则,但改用 MachOKitExtensions 的 `DyldCacheImageSearchMode.matchRank`:canonical framework > 普通 dylib > bundle,`/System/iOSSupport` 下的 Catalyst 构建降级)。SwiftLayout 现行的 cache 索引是「枚举顺序首写者胜」,macOS cache 上同名 Catalyst 构建可能先被枚举到——潜在错配,合并后消除。 +- fat 依赖文件取与 root 同架构的 slice,没有再退到第一个(两处现行都是无条件 `.first`)。 +- 定位不到**不抛错**:记入 `unresolvedLoadNames`;搜索路径本身加载失败记入 `searchPathLoadFailures`。模块位于事件层之下,只回传数据、不落日志,由上层决定报告方式。 + +### 既有消费者 + +- **SwiftLayout**:`ImageUniverse.dependencyClosure(root:searchPaths:)` / `(root:)` 改为构造 `DependencyClosure` 再把 `images` 喂给既有的 `dependencyClosure(root:dependencyImages:)`;`ImageUniverse` 本身(惰性索引、五个 resolve seam)**不动**。`LayoutDependencySearchPath` 保留为 deprecated typealias。 +- **SwiftDeclarationRendering**:`StaticLayoutDependencyResolution.dependencyClosure(searchPaths:)` 关联值换成 `[DependencySearchPath]`(typealias 保证源码兼容)。 +- **SwiftInterface**:`SwiftInterfaceBuilderDependencies` 变薄包装——新增 `init(machO:searchPaths:eventHandlers:)` 与 `init(closure:)`,旧 `init(machO:paths:eventHandlers:)` 与 `DependencyPath` 标 deprecated 并转发;`searchPathLoadFailures` 继续派发 `renderingDegraded(.dependencyLoad)` 事件;新增 `unresolvedLoadNames` 透出。**保持只取直接依赖**——TypeIndexing 按依赖清单逐模块生成 SourceKit 接口,换成传递闭包就退回提案 0009 之前「全 SDK 生成」的开销。`MachOImage` 版改走 `DependencyClosure(root:traversal: .direct)`,顺带修一个静默 bug:现行把完整 load path 喂给按 bare name 匹配的 `MachOImage(name:)`,结果永远解析为空(MachOKit `Dylib.name` 是 "library's path name",`MachOImage(name:)` 比较的是末段去扩展名)。仓库内与下游(RuntimeViewer / MachOKitUI / SymbolViewer)均无该 init 的调用方。 +- **CLI** `interface --resolve-c-module-names`:改用新 init;「resolved no dependency images」警告改为按 `unresolvedLoadNames` 精确提示。 + +### 测试 + +- 新 `MachODependenciesTests`:bare name 归一;in-process direct(含 `SymbolTestsHelper` 命中——即上述 bug 的回归测试)与 transitive(direct ⊂ transitive、BFS 前缀、去重);offline 显式路径 + 系统 cache;无搜索路径时全部进 `unresolvedLoadNames`;坏路径进 `searchPathLoadFailures` 且不抛;host cache 上 install path 精确优先与 `iOSSupport` 降级。 +- 既有 `SwiftLayoutTests.DependencyClosureLayoutTests` 作端到端锚点不动;`SwiftInterfaceTests` 加 direct 语义与 image 版非空的锚点。 +- 验证:`swift build`、相关 filter 套件、全量(跳 IntegrationTests);渲染 A/B(默认输出不经闭包——`SwiftDeclarationPrinter` 只在 `--emit-field-offsets` 等布局注释开启时才构建 provider)+ 手工对一个系统框架跑 `dump --emit-field-offsets` 前后 diff。 + +### 文档 + +新 `Internal/Modules/MachODependencies.md`(模块参考:定位规则、一次性 cache 索引、BFS 顺序为何对惰性消费者重要、install name ≠ 磁盘路径、`MachOImage(name:)` 的 bare name 契约);`Modules/README.md` 与 `Documentations/README.md` 登记;AGENTS.md 模块图与条目;`StaticLayoutDependencyClosure.md` 加迁移指引;Glossary 登记「依赖闭包」「bare name」;ProjectEvolutionLog 落地时加节;任务报告。不动 Changelog(不升版本)。 + +### 未问而定的假设 + +1. 模块名 `MachODependencies`、类型名如上;产品同时作为独立 library 暴露。 +2. deprecated 过渡保留一个版本再删(无下游使用者,成本极低)。 +3. 依赖种类不过滤(load / weak / reexport / upward / lazy 全收,与现行一致)。 +4. `@rpath` / `@loader_path` / `@executable_path` 仍不展开(与现行一致,列为后续)。 + +## 决策日志 + +| 日期 | 决定 | 理由 | +|------|------|------| +| 2026-09-02 | Created as Draft | 用户:「把目前的依赖闭包抽出来,方便给其他功能使用」 | +| 2026-09-02 | 两套实现合并;落点为本仓库新 target `MachODependencies` | 用户在澄清一轮中选定(备选:只抽 SwiftLayout 一套;放 sibling 包 MachOKitExtensions) | +| 2026-09-02 | SwiftInterface 保持只取直接依赖 | TypeIndexing 的接口生成成本随依赖清单线性增长(提案 0009) | +| 2026-09-02 | 匹配规则取并集:精确路径优先、bare name 排序兜底 | 修 Catalyst 同名错配,两侧现行行为都是新规则的子集 | +| 2026-09-02 | 走轻量档 | 无破坏性 API(旧名 deprecated 转发),用户未要求升档 | +| 2026-09-02 | Draft → Accepted | 用户批准(「开个worktree开工」),提案 0016 已落地,本案落地时编号取 0017 | +| 2026-09-02 | `SwiftInterface` 只透出 `unresolvedLoadNames`,不新增事件 case | `Payload.unhandledFailureDescription` 是有意的穷举 switch,加 case 要动 `SwiftDeclaration`;CLI 自己打 warning 已够用 | +| 2026-09-02 | Accepted → Implemented | 26 定向测试 + 全量 1612 测试(仅 2 个已知 flaky 并发用例,单独跑通过)全绿;带布局注释的 dump / interface 对 SwiftUI / SwiftUICore / SwiftData / Combine 双侧逐字节一致;同依赖版本下耗时持平。配套文档:模块参考 `Modules/MachODependencies.md`;术语「bare image name」「dependency closure」已登记 Glossary。落地 `next` 时取号(0016 已占,预计 0017)并在同一 commit 改名 | +| 2026-09-02 | 落地 `next` 取号 0017 | 远程共享分支 `Evolutions/` 最大号为 0016(`origin/next`),+1;文件改名与全部互链同批(PR 分支上完成) | diff --git a/Documentations/Evolutions/0018-self-contained-abi-layer.md b/Documentations/Evolutions/0018-self-contained-abi-layer.md new file mode 100644 index 00000000..2c5b0adf --- /dev/null +++ b/Documentations/Evolutions/0018-self-contained-abi-layer.md @@ -0,0 +1,235 @@ +# 0018 - ABI 层自包含:MachOSwiftSection 不再依赖符号索引 + +- **状态**: Implemented +- **作者**: JH +- **创建日期**: 2026-09-03 +- **最后更新**: 2026-09-04 +- **所属愿景**: 无 +- **关联提案**: [0019-large-stack-executor-and-cross-version-parallelism](0019-large-stack-executor-and-cross-version-parallelism.md)(同一轮调研的另一产物,互不依赖,可独立落地) +- **实现分支 / PR**: `feature/self-contained-abi-layer`(worktree `.worktrees/MachOSwiftSection-SelfContainedABI`),[PR #121](https://github.com/MxIris-Reverse-Engineering/MachOSwiftSection/pull/121) +- **配套文档**: [SelfContainedABILayer.md](../Internal/SelfContainedABILayer.md)(实现说明)、[TaskReports/2026-09-03-self-contained-abi-layer.md](../Internal/TaskReports/2026-09-03-self-contained-abi-layer.md)(过程复盘) + +## 摘要 + +`MachOSwiftSection` 是这个库的 ABI 层:把 `__swift5_*` 段里的描述符按运行时布局读出来。它今天反向依赖着符号索引:五个描述符的 Layout 里声明了 `RelativeDirectPointer` 字段,而 `Symbols` 的 `Resolvable` 实现会走 `SymbolIndexStore.shared`,第一次访问就触发整个镜像十几万个符号的扫描与 demangle;`SymbolOrElementPointer` 又把 `MachOSymbols.Symbol` 值类型带进了 ABI 层的每一种上下文指针。本提案把 ABI 层收成真正自包含的一层:描述符只暴露实现的**地址**,符号查询作为扩展上移到 `SwiftInspection`;`Symbol` / `Symbols` / `SymbolOrElement` 这几个纯值类型下沉到 `MachOResolving`,`MachOSymbolPointers` 并入 `MachOPointers`;`MachOSwiftSection` 的依赖收成 `MachOReading` / `MachOResolving` / `MachOPointers`(加 MachOKit 与 MachOKitExtensions),同时摘掉只为一处前缀判断而存在的 `Demangling` 依赖。顺带消除 `ReadingContext` 那条腿把机器码字节当 `Symbols` 结构体读出来的隐患。 + +## 动机 + +### 1. 一个 ABI 访问器背后跑着整镜像的符号扫描 + +`MethodDescriptor.Layout.implementation` 的类型是 `RelativeDirectPointer`(`Sources/MachOSwiftSection/Models/Type/Class/Method/MethodDescriptor.swift:8`),访问器 `implementationSymbols(in:)`(`:22`)就是对这个指针调 `resolve(from:in:)`。`Symbols` 的 `resolve`(`Sources/MachOSymbols/Symbols.swift:21`)调 `machO.symbols(offset:)`(`Sources/MachOSymbols/MachO+Symbol.swift:5-7`),后者是 `SymbolIndexStore.shared.symbols(for:in:)`,查询前先 `storage(in:)`:没建表就当场建(`Sources/MachOCaches/SharedCache.swift` 的 `resolve(key:build:)`),建表就是 `buildStorageSweep` 对全部符号逐个 demangle(SwiftUI iOS 18.5 是 185,988 行)。同样的结构复制在 `MethodOverrideDescriptor`、`MethodDefaultOverrideDescriptor`、`ProtocolRequirement`、`ResilientWitness` 四个描述符上。 + +也就是说,「读一个 vtable 槽指向哪里」这种 ABI 层面的操作,会把符号索引、demangler、`SharedCache`、内存压力监听整套东西拉起来。下游已经为此付过代价:MachOKitUI 在渲染 Swift section 之前把进程全局开关 `MachOSymbols.Symbol.resolvesSymbolUsingIndexStore` 强行置 `false`(`MachOKitUI/Sources/MachOKitUICore/Builder/MachOSwiftSectionDetailBuilder.swift:18-20`),就是为了让一个 Mach-O 浏览器翻描述符时别把整张符号表建起来。一个只想读 ABI 的消费者不该需要知道这个开关。 + +### 2. 类型依赖把 ABI 层钉在符号索引模块下面 + +即使不谈行为,`MachOSwiftSection` 也在类型层面离不开 `MachOSymbols`:`SymbolOrElementPointer`(`Sources/MachOSymbolPointers/SymbolOrElementPointer.swift:14-17`)的 `.symbol` 载荷是 `MachOSymbols.Symbol`,而 `ContextPointer`(`Sources/MachOSwiftSection/Pointer/ContextPointer.swift:4`)、`RelativeContextPointer` / `RelativeMethodDescriptorPointer` / `RelativeProtocolRequirementPointer`(`Pointer/RelativePointers.swift:3-9`)、`ProtocolConformanceDescriptor.protocolDescriptor`(`Models/ProtocolConformance/ProtocolConformanceDescriptor.swift:7`)、`TypeReference.indirectObjCClass`(`Models/Type/TypeReference.swift:8`)、`RelativeProtocolDescriptorPointer`(`Pointer/RelativeProtocolDescriptorPointer.swift:5-6`)全部建立在它之上,`ContextDescriptorProtocol.parent(in:)` 返回 `SymbolOrElement`。这些 `Symbol` 值只是 bind 表解析出来的「偏移加名字」(`SymbolOrElementPointer.swift:77-86` 的 `resolveBind`),从不碰索引库,但它们的类型住在装着 `SymbolIndexStore`、缓存与 `Demangling` 依赖的模块里。`Package.swift:355-362` 因此让 `MachOSwiftSection` 依赖 `MachOFoundation` 伞模块,而伞模块 `@_exported` 了 `MachOSymbols` 与 `MachOSymbolPointers`(`Sources/MachOFoundation/Exported.swift`)。 + +### 3. `ReadingContext` 那条腿在读垃圾内存 + +`implementationSymbols(in context:)`(`MethodDescriptor.swift:30`,其余四个描述符同形)对 `RelativeDirectPointer` 调 `resolve(at:in:)`。`Symbols` 没有为 `ReadingContext` 提供任何实现,于是落到 `Resolvable` 的默认实现 `context.readElement(at:)`(`Sources/MachOResolving/Resolvable.swift`),最终是 `assumingMemoryBound(to: Symbols.self).pointee`(`Sources/MachOReading/Readable/UnsafeRawPointer+Readable.swift:60-62`):把函数入口处的机器码按「一个 `Int` 加一个数组引用」的结构体原样读出来。它没有崩溃只是因为读出的假数组指针恰好是非规范地址,运行时的 retain / release 会跳过它;三处 fixture 测试只断言 `!= nil`(`Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Method/MethodDescriptorTests.swift:64`、`MethodOverrideDescriptorTests.swift:90`、`Fixtures/Protocol/ProtocolRequirementTests.swift:60`,`imageContext` 是 `MachOContext`,见 `Sources/MachOTestingSupport/MachOSwiftSectionFixtureTests.swift:15`),非 Optional 路径永远不为 nil,所以永远绿。`Sources/` 里没有生产代码走这条腿。这是「符号查询伪装成内存读取」这个设计的必然结果:`MachO` 那条腿有索引库可查,`ReadingContext` 这条腿没有任何符号服务,只能读字节。 + +## 前期调研 + +- **两条依赖通道的完整清单**:行为通道是五个描述符的 `RelativeDirectPointer` 字段与 `implementationSymbols` / `defaultImplementationSymbols` 访问器(`Models/Type/Class/Method/MethodDescriptor.swift:8,22,30`、`MethodOverrideDescriptor.swift:9,35,51`、`MethodDefaultOverrideDescriptor.swift:9,31,57`、`Models/Protocol/ProtocolRequirement.swift:7,21,43`、`Models/Protocol/ResilientWitness.swift:8,41,53`);类型通道是 `SymbolOrElement` / `SymbolOrElementPointer`(上文动机 §2)。`MachOSwiftSection` 里显式 `import MachOSymbols` 的两个文件(`Models/Type/TypeContextWrapper.swift:3`、`Models/BuiltinType/BuiltinType.swift:2`)实际没有用到该模块的任何名字。 +- **`Demangling` 依赖只剩一处**:`Sources/MachOSwiftSection/Extensions/String+.swift:18` 用了 `String.isSwiftSymbol`(上游 `Sources/Demangling/Utils/Extensions.swift:26`,一个 mangling 前缀判断);`Models/OpaqueType/OpaqueType.swift` 与 `Models/ContextDescriptor/ContextDescriptorProtocol.swift:3` 的 `import Demangling` 没有实际引用。`MangledName` 是本模块自己的类型(`Models/Mangling/MangledName.swift:5`),不来自上游。 +- **值类型的真实职责**:`Symbol`(`Sources/MachOSymbols/Symbol.swift:8-23`)是 `offset` / `name` / `isExternal` 三个字段的 struct;只有 `resolve(from:in:)`(`:25-35`)与 `@Mutex static var resolvesSymbolUsingIndexStore`(`:71-72`)牵扯索引库。`Symbols`(`Symbols.swift`)是 `[Symbol]` 的集合包装,其 `AsyncResolvable` 实现是全部耦合所在。`SymbolOrElement`(`Sources/MachOSymbols/SymbolOrElement.swift:6-8`)是 `symbol` / `element` 二选一的枚举。 +- **`ResilientWitness` 已经有地址形态的访问器**:`implementationOffset`(`ResilientWitness.swift:30-32`,用 `resolveDirectOffset(from:)` 纯算术得出,不需要 reader)与 `implementationAddress(in:)`(`:37-39`)。本提案就是把这个形态推广到五个描述符。`MachOPointers` 已有 `RelativeDirectRawPointer`(`Sources/MachOPointers/RelativePointers.swift:9`)。 +- **上层调用方**(都在本仓库内,`grep implementationSymbols|defaultImplementationSymbols|Symbols\.resolve|Symbol\.resolve`):`SwiftDump` 的 `ClassDumper`(7 处)、`ProtocolConformanceDumper`(4)、`ProtocolDumper`(2);`SwiftDeclaration` 的 `TypeDefinition`(3)、`ExtensionDefinition`(3)、`ProtocolDefinition`(2);`SwiftInspection/MetadataReader.swift:162`(`MachOContext.lookupSymbol` 走 `Symbol.resolve`);`SwiftDeclarationRendering/Extensions/OpaqueType+.swift:12`;`MachOFixtureSupport` 三个 baseline 生成器;`MachOSwiftSectionTests` 四个 fixture 套件。它们要的都是「这个偏移上有哪些符号」,多数已经直接写 `Symbols.resolve(from: x.offset, in: machO)`。 +- **下游**:RuntimeViewer 两个文件 `import MachOSwiftSection`,没有经它间接使用 `MachOSymbols` / `MachODependencies` 的类型;MachOKitUI 有三个文件经再导出拿到了 `MachOSymbols` 的名字,其中 `MachOSwiftSectionDetailBuilder.swift:18-20` 用限定名 `MachOSymbols.Symbol` 且没有 `import MachOSymbols`;SymbolViewer 八个文件直接 `import MachOSymbols`,不经 `MachOSwiftSection`。 +- **来历**:`RelativeDirectPointer` 由 2025-06-29 的 commit `a9f019c1`「Handling the issue of multiple symbols with the same offset」引入——同地址多名字(identical code folding)需要一个集合类型,当时顺手让集合 `Resolvable`,字段就直接「解析成符号列表」了。集合本身仍是对的,错的是把查询做成读取。 +- **覆盖不变量**:`MachOSwiftSectionCoverageInvariantTests` 要求 `Models/` 下每个公开方法都有登记测试;五个访问器登记在 `Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/*Baseline.swift` 的 `registeredTestMethodNames`。方法移出 `Models/` 后登记随之迁移。 + +## 提议方案 + +1. **ABI 层只说地址。** 五个描述符 Layout 的 `RelativeDirectPointer` 改为 `RelativeDirectRawPointer`;访问器统一为 `implementationOffset: Int?`(空指针为 nil)与 `implementationAddress(in context:)`;`ProtocolRequirement` 用 `defaultImplementationOffset` 命名。`implementationSymbols` 家族从 `MachOSwiftSection` 删除。 +2. **符号查询作为扩展上移到 `SwiftInspection`。** 在 `SwiftInspection` 里给五个描述符补同名扩展 `implementationSymbols(in machO:)`,实现是拿偏移问 `machO.symbols(offset:)`。只保留 `MachO` 那条腿;`ReadingContext` 腿不再提供(没有符号服务可用,也没有生产调用方)。 +3. **值与服务分家。** `Symbol` / `Symbols` / `SymbolOrElement` 移到 `MachOResolving`;`SymbolOrElementPointer` 连同 `MachOSymbolPointers` 整个并入 `MachOPointers`,删除该 target。`Symbols` 不再是 `Resolvable`;`Symbol` 走索引库的 `resolve(from:in:)` 与 `resolvesSymbolUsingIndexStore` 留在 `MachOSymbols` 作为扩展。 +4. **依赖收窄。** `MachOSwiftSection` 的 target 依赖改为 MachOKit、MachOKitExtensions、`MachOReading`、`MachOResolving`、`MachOPointers`、`MachOSwiftSectionC`、`Utilities`;`Sources/MachOSwiftSection/Exported.swift` 改为再导出这几个底层模块而非伞模块;`isSwiftSymbol` 在本模块就地实现,摘掉 `Demangling` 依赖;删掉两处无用的 `import MachOSymbols`。`MachOFoundation` 伞模块继续服务上层,去掉 `MachOSymbolPointers` 一行。 + +### 非目标 + +- 不改 `SymbolIndexStore` 的任何行为与 API;不改 `MetadataReader` 的符号查找机制(它在 `SwiftInspection`,位置本来就对)。 +- 不动 `resolvesSymbolUsingIndexStore` 开关的语义;它只影响 `MachOSymbols` 里留下的 `Symbol.resolve`。 +- 不给 `ReadingContext` 增加符号服务抽象。 +- 不处理 `Demangling` 以外的其它模块边界(`MachOSwiftSectionC`、`Utilities` 保持)。 + +## 详细设计 + +### ABI 层(`MachOSwiftSection`) + +```swift +public struct MethodDescriptor: ResolvableLocatableLayoutWrapper { + public struct Layout: LayoutProtocol { + public let flags: MethodDescriptorFlags + public let implementation: RelativeDirectRawPointer + } + public var layout: Layout + public let offset: Int +} + +extension MethodDescriptor { + /// File offset of the implementation, or `nil` for a null pointer + /// (a method with no implementation). Pure pointer arithmetic on the + /// descriptor's own offset, so it needs no reader. + public var implementationOffset: Int? { + guard layout.implementation.isValid else { return nil } + return layout.implementation.resolveDirectOffset(from: offset(of: \.implementation)) + } + + /// The same target as an address in `context`. + public func implementationAddress(in context: Context) throws -> Context.Address? { + guard let implementationOffset else { return nil } + return try context.addressFromOffset(implementationOffset) + } +} +``` + +`MethodOverrideDescriptor`、`MethodDefaultOverrideDescriptor`、`ResilientWitness` 同形;`ProtocolRequirement` 的字段与访问器名为 `defaultImplementation` / `defaultImplementationOffset` / `defaultImplementationAddress(in:)`。`ResilientWitness` 现有的 `implementationOffset: Int` 改为 `Int?`(空指针语义原来靠调用方 `isValid` 自查,现在统一)。 + +### 符号查询扩展(`SwiftInspection`) + +```swift +import MachOSwiftSection +import MachOSymbols + +extension MethodDescriptor { + public func implementationSymbols(in machO: MachO) -> Symbols? { + guard let implementationOffset else { return nil } + return machO.symbols(offset: implementationOffset) + } +} +``` + +其余四个同形。返回类型从 `throws -> Symbols?` 变为 `-> Symbols?`(查询本身不抛)。 + +### 值类型下沉 + +```swift +// MachOResolving +public struct Symbol: Hashable, Sendable { + public let offset: Int + public let name: String + public let isExternal: Bool + public init(offset: Int, name: String, isExternal: Bool = false) + public func addressString(format: AddressFormat, in machO: some MachORepresentableWithCache) -> String +} + +public struct Symbols: RandomAccessCollection, MutableCollection { /* 现状,去掉 AsyncResolvable */ } + +public enum SymbolOrElement: Resolvable { + case symbol(Symbol) + case element(Element) +} + +// MachOPointers(吸收 MachOSymbolPointers 全部内容) +public enum SymbolOrElementPointer: RelativeIndirectType { /* 现状 */ } + +// MachOSymbols 留下的部分 +extension Symbol { + @Mutex public static var resolvesSymbolUsingIndexStore: Bool + public static func resolve(from offset: Int, in machO: MachO) throws -> Self? +} +extension MachORepresentableWithCache { + public func symbols(offset: Int) -> Symbols? +} +``` + +`MachOPointers` 需要新增对 `MachOKitExtensions` product 的依赖(`SymbolOrElementPointer` 用 `MachOBindRebaseResolving`)。`MachOFoundation/Exported.swift` 去掉 `MachOSymbolPointers`。 + +### 调用方迁移 + +| 现在 | 之后 | +|---|---| +| `try Symbols.resolve(from: x.offset, in: machO)` | `machO.symbols(offset: x.offset)` | +| `try Symbol.resolve(from: offset, in: machO)`(`MetadataReader.swift:162`) | `machO.symbols(offset: offset)?.first`(保留 `resolvesSymbolUsingIndexStore` 分支时仍调 `MachOSymbols` 留下的 `Symbol.resolve`) | +| `try descriptor.implementationSymbols(in: machO)` | `descriptor.implementationSymbols(in: machO)`(需 `import SwiftInspection`) | +| `descriptor.implementationSymbols(in: context)` | 删除;无生产调用方 | + +### 测试 + +- **先钉住垃圾读**:对旧 API 写一条会红的测试——`implementationSymbols(in: imageContext)` 的结果应与 `implementationSymbols(in: machOImage)` 一致,今天不一致。它作为动机 §3 的证据进提案分支;旧 API 删除后由下一条永久替代。 +- **永久回归**:四个 fixture 套件的 `implementationSymbols` 测试改为跨 reader 比对 `implementationOffset` 的真值并对照 baseline 字面量,`imageContext` 腿比对 `implementationAddress(in:)` 与偏移一致;`registeredTestMethodNames` 登记新成员名,`regen-baselines` 重生成。 +- **分层本身由编译器守**:`Package.swift` 里 `MachOSwiftSection` 的依赖列表不含 `MachOSymbols` / `Demangling`,回归就是编译错误,不另加源码扫描。 +- **渲染 A/B**:本提案触碰 reader 栈与索引路径,必须跑 `Scripts/run-rendering-ab-verification.py` 逐字节对比。 + +## 替代方案考量 + +- **只在代码里不调索引库,保留伞模块依赖(代码级别自包含)**:不必移动任何类型,下游零改动。否决:依赖仍在包图里,clean build 时 ABI 层仍要等 `Demangling` 编完,而「别再把索引库钩回 ABI 层」只能靠 review 守。2026-09-03 用户明确选包图级别。 +- **在 `MachOReading` 定义符号查找协议,让 `ReadingContext` / `MachO` 类型 retroactive 遵循**:能保住 `ReadingContext` 腿。否决:为一个没有生产调用方的入口引入一层抽象,且 retroactive conformance 落在 `MachOSymbols` 就意味着谁 import 了它谁的 ABI 访问器行为就变,隐式耦合换了个地方。 +- **给 `MachOSymbols` 留 `typealias Symbol = MachOResolving.Symbol` 保住限定名**:否决:`MachOFoundation` 同时再导出两个模块,同名 typealias 会让不限定的 `Symbol` 在所有 `import MachOFoundation` 的文件里产生歧义风险;限定名 `MachOSymbols.Symbol` 的已知调用方只有 MachOKitUI 一处,改一行即可。 +- **把值类型全部并进 `MachOPointers`**:少一层,但 `MachOPointers` 的职责从「相对指针」变成「指针加符号值」。`Symbol` 是「地址解析」的结果,`MachOResolving` 更贴切,且 `Resolvable` 就在那里。 +- **扩展放新建的小 target `MachOSwiftSectionSymbols`**:分层最纯,多一个模块要维护;`SwiftInspection` 已经是「ABI + 符号 + demangle」的交汇层。2026-09-03 用户选 `SwiftInspection`。 + +## 影响 + +### 源码兼容性(source compatibility) + +**有破坏**,逐条: + +| 调用点 | 改前 | 改后 | +|---|---|---| +| 描述符符号访问器 | `try d.implementationSymbols(in: machO)`(`MachOSwiftSection`) | `d.implementationSymbols(in: machO)`,需 `import SwiftInspection` | +| `ReadingContext` 腿 | `try d.implementationSymbols(in: context)` | 删除,无替代(读的是垃圾) | +| Layout 字段类型 | `RelativeDirectPointer` | `RelativeDirectRawPointer` | +| `ResilientWitness.implementationOffset` | `Int` | `Int?` | +| `Symbols.resolve(from:in:)` / `Symbol.resolve(from:in:)` 作为 `Resolvable` 要求 | 存在 | `Symbols` 的删除;`Symbol` 的保留为 `MachOSymbols` 扩展方法 | +| 限定名 `MachOSymbols.Symbol` / `MachOSymbols.Symbols` / `MachOSymbols.SymbolOrElement` | 可用 | 改为 `MachOResolving.…` 或不限定 | +| `import MachOSymbolPointers` | 可用 | 模块并入 `MachOPointers` | +| 经 `import MachOSwiftSection` 间接得到 `MachOSymbols` / `MachODependencies` 的名字 | 可用 | 需显式 import | + +`@available(*, deprecated, renamed:)` 能覆盖的只有 `symbols(offset:) async` 这种同模块内的改名;类型跨模块移动与字段类型变更无法平滑过渡,按 minor 版本一次性完成。 + +### ABI 兼容性 + +不适用 —— 本库以 SPM 源码分发,使用方每次重新编译。 + +### 下游影响 + +本仓库:`MachOSwiftSection`、`MachOSymbols`、`MachOResolving`、`MachOPointers`、`MachOSymbolPointers`(删除)、`MachOFoundation`、`SwiftInspection`、`SwiftDeclaration`、`SwiftDump`、`SwiftDeclarationRendering`、`MachOFixtureSupport`、`MachOSwiftSectionTests`。 + +跨仓库:MachOKitUI 三个文件补 `import MachOSymbols`(其中一处改限定名);RuntimeViewer 无影响;SymbolViewer 无影响(直接 import `MachOSymbols`,用的是索引库)。 + +### 文档与示例 + +AGENTS.md 的模块依赖图与 `MachOSwiftSection` / `MachOSymbols` 条目;`Documentations/README.md`、`Documentations/Internal/Modules/` 的模块参考;`ProjectEvolutionLog.md` 新节;`Changelogs/` 随版本;Glossary 视是否引入新术语(预计不引入)。 + +## API 演进与废弃策略 + +- `symbols(offset:) async`(与同步版逐字相同)标 deprecated 一个版本后删除。 +- 其余破坏项无法转发,随 minor 版本(0.18.0)一次性完成,Changelog 逐条列出改前 / 改后。 +- 不需要 semver major:本库 0.x 阶段,且下游全部在本人控制之下。 + +## 落地步骤 + +1. 对旧 API 写红测试钉住 `ReadingContext` 腿的垃圾读(动机 §3),确认修前失败。 +2. 值类型下沉:`Symbol` / `Symbols` / `SymbolOrElement` 移到 `MachOResolving`,`MachOSymbolPointers` 并入 `MachOPointers`,`Package.swift` 与 `MachOFoundation/Exported.swift` 同步;`MachOSymbols` 留下 `Symbol.resolve` 与开关。全仓库构建通过。 +3. 描述符字段改 `RelativeDirectRawPointer`,补 `implementationOffset` / `implementationAddress(in:)`,删 `implementationSymbols` 家族。 +4. `SwiftInspection` 补五个扩展;迁移 `SwiftDeclaration` / `SwiftDump` / `MetadataReader` / `OpaqueType+` / baseline 生成器的调用点。 +5. `MachOSwiftSection` 依赖收窄:`Exported.swift`、就地实现 `isSwiftSymbol`、删无用 import、`Package.swift` 依赖列表。 +6. fixture 测试改比对偏移真值,登记 `registeredTestMethodNames`,`regen-baselines`,覆盖不变量全绿;步骤 1 的红测试由新 API 上的等价断言永久替代。 +7. 全量测试(`--skip IntegrationTests`)与渲染 A/B 逐字节验证。 +8. 文档同批:AGENTS.md、README 索引、模块参考、演进账本、Changelog 与 `Version.swift`;MachOKitUI 侧补 import 的 PR。 + +**收尾时判断**:实现说明——写(「为什么 `ReadingContext` 腿没有符号服务」「为什么不留 typealias」是从签名看不出的决策);术语——预计不引入。 + +## 决策日志 + +| 日期 | 变更 | 说明 | +|------|------|------| +| 2026-09-03 | Created as Draft | 用户提出「ABI 不能反向依赖 SymbolIndexStore,ABI 接口应该自包含」,本提案由当日调研直接产出 | +| 2026-09-03 | 自包含到包图级别 | 澄清提问第一轮:`MachOSwiftSection` 只依赖三个底层模块,不再再导出伞模块;代码级别方案否决(依赖仍在包图里,防回归靠 review) | +| 2026-09-03 | 扩展落点 `SwiftInspection` | 同轮:否决新建小 target 与「不提供扩展」 | +| 2026-09-03 | 值类型下沉 `MachOResolving`,`MachOSymbolPointers` 并入 `MachOPointers` | 第二、三轮:用户先反问「下沉有意义吗」,答复是不下沉则包图级别不成立;确认下沉 | +| 2026-09-03 | 未问自定 | deprecated 只覆盖可转发项,minor 版本一次完成;`ReadingContext` 腿先红测试后删除;顺手摘掉 `Demangling` 依赖 | +| 2026-09-03 | Accepted | 用户:「swift-demangling 那边的给它的 agent 就行了,你这边弄 ABI」——上游提案 0014 交由该仓库的 agent,本提案获准开工 | +| 2026-09-03 | In Progress | 在 worktree `MachOSwiftSection-SelfContainedABI`(分支 `feature/self-contained-abi-layer`,基于 `next` f3782248)实施;async 提案 draft 随分支携带,不在本批实现 | +| 2026-09-03 | 红测试证实垃圾读 | 对旧 API 比对两条腿的 `offset`:context 腿 -2999674702252736512,MachO 腿 5624(`MethodDescriptorTests`,修前失败);修后由新 API 上的 `implementationAddress(in: context) == implementationOffset` 永久替代 | +| 2026-09-03 | 实现期决定:新增底层伞模块 `MachOBase` | 163 个文件的 `import MachOFoundation` 一行换成 `import MachOBase`,比每个文件写四五行显式 import 干净;`MachOFoundation` = `MachOBase` + `MachOSymbols` + `MachODependencies` | +| 2026-09-03 | 实现期决定:`symbols(offset:) async` 删除而非废弃 | 方案原想标 deprecated 留一版;实测 async 上下文优先绑定 async 重载并要求 `await`,`if let symbols = machO.symbols(offset:)` 四处直接报错,两个重载不能共存 | +| 2026-09-03 | 实现期决定:`Demangling` 依赖多摘出两处 | 除 `isSwiftSymbol` 外还有 `stripManglePrefix`(`MangledName.typeString`)与 `cModule` / `objcModule`(`ContextDescriptorProtocol`);分别本地化为 `strippingSwiftManglingPrefix` 与 `CImportedModuleNames`,前两者由 `ManglingPrefixTests` 钉住与 demangler 一致 | +| 2026-09-03 | 实现期发现:16 个 target 靠传递依赖拿 `MachOFoundation` | `SwiftInspection`、`SwiftDump`、`SwiftDeclaration`、`SwiftIndexing`、`SwiftPrinting`、`swift-section` 与十个测试 target 从未声明 `.target(.MachOFoundation)`,全靠 `MachOSwiftSection` 的再导出;本批补齐声明,另有 12 个源码文件与若干测试文件补显式 import | +| 2026-09-03 | 验证通过 | `MachOSwiftSectionTests` 723 测试 / 161 套件全绿;全量 1617 测试 / 302 套件仅两个已知 flaky 的墙钟并行度断言假失败、单独重跑全绿;渲染 A/B 78 对输出逐字节一致(系统 dyld cache、四个模拟器运行时、进程内 MachOImage)。细节见任务报告 | +| 2026-09-04 | Review 修复批次(PR #121 review,12 条全真) | A–H 与 I(Utilities)、K、L 修复:补 `MachOBase` / `MachOFoundation` library product(下游能声明依赖)、changelog 限定「逐字节一致」为默认 flag 并说明 `--emit-member-addresses` 下空 witness 不再打假地址、`ManglingPrefixTests` 钉住 `CImportedModuleNames`、`ProtocolRequirementTests` 加有默认实现的第二个 picker、`MethodOverrideDescriptor` baseline 发射 `implementationOffset`、四个 target 补依赖、CI filter 补 `MethodDefaultOverrideDescriptorTests`、`ProtocolConformanceDumper` 统一到新 accessor、image 腿独立断言、前缀单次扫描。延后:A23(既有未声明 import)、A24(全量迁移)。清单见 `Roadmaps/2026-09-04-pr121-review-findings.md` | +| 2026-09-03 | 收尾判断 | 配套文档:写了实现说明 [SelfContainedABILayer.md](../Internal/SelfContainedABILayer.md)(登记在头部);术语:不引入新术语(`MachOBase` 是模块名,不入术语表)。待合入 `next` 时按落地规则取编号并置 Implemented | +| 2026-09-04 | Implemented;落地编号 0018 | `origin/next` 的 `Evolutions/` 最大编号 0017,本线取 0018;改名、标题、状态表与同仓链接同批完成,代码与 fixture 注释按规则继续引用 slug | diff --git a/Documentations/Evolutions/0019-large-stack-executor-and-cross-version-parallelism.md b/Documentations/Evolutions/0019-large-stack-executor-and-cross-version-parallelism.md new file mode 100644 index 00000000..d8d95368 --- /dev/null +++ b/Documentations/Evolutions/0019-large-stack-executor-and-cross-version-parallelism.md @@ -0,0 +1,198 @@ +# 0019 - 大栈任务执行器接入与跨版本并行准备 + +- **状态**: Implemented +- **作者**: JH +- **创建日期**: 2026-09-03 +- **最后更新**: 2026-09-07 +- **所属愿景**: 无 +- **关联提案**: swift-demangling 提案 0014「大栈 TaskExecutor」(上游前置,执行器本体在那边;本提案只做接入);[0018-self-contained-abi-layer](0018-self-contained-abi-layer.md)(同一轮调研产物,互不依赖) +- **实现分支 / PR**: `feature/large-stack-executor-and-cross-version-parallelism`,[PR #122](https://github.com/MxIris-Reverse-Engineering/MachOSwiftSection/pull/122)(堆叠在 ABI 提案的 PR #121 之上,base 随其合并切到 `next`) +- **配套文档**: [LargeStackTaskExecutorAdoption.md](../Internal/LargeStackTaskExecutorAdoption.md)(实现说明);任务报告 [2026-09-03-large-stack-executor-and-cross-version-parallelism.md](../Internal/TaskReports/2026-09-03-large-stack-executor-and-cross-version-parallelism.md) + +## 摘要 + +这个库的 async 是「签名上的 async」:306 个 async 函数、72 个公开 async 入口,但除了 `TypeIndexing` 的两个 actor 之外没有任何 actor,真正会挂起任务的地方只有三处。真正的开销在打印路径:每次 `printSemantic` 都跳到 swift-demangling 的 8 MB 大栈线程再用信号量停住协作线程,每次固定多付 8–21 µs;索引路径已经用 `withLargeStack` 包住整趟符号扫描摊掉了跳转,打印循环因为是 async 包不住。本提案做两件事:**接入上游提供的大栈 `TaskExecutor`**——`StackSafeExecutor` 按线程剩余栈空间探测,任务只要跑在 16 MB 线程上,demangle / print / remangle 全部内联、零跳转,同步被调方一并受益;**把 diff / evolution 的多版本准备改成并行**——各版本是不同文件、缓存按 UUID 键控、读取走 mmap,实测三版本并行约 2 倍。两者都不改任何输出,逐字节由渲染 A/B 守住。版本内逐定义并行不在本提案内。 + +## 动机 + +### 1. 打印路径每次调用付一次线程往返,而且现在包不住 + +`Node.printSemantic(using:)`(`Sources/SwiftDeclarationRendering/Extensions/Node+.swift:117-122`)走 `DemanglingPrinter.print`,上游那里是 `StackSafeExecutor.execute`:探测当前线程剩余栈是否 ≥ 2 MB(`swift-demangling/Sources/Demangling/Utils/StackSafeExecutor.swift:161-169`,用 `pthread_get_stackaddr_np` / `pthread_get_stacksize_np` 算),不够就提交到 8 MB 池线程并在信号量上等(`:183-200`)。Darwin 给协作线程和 libdispatch 线程的栈都是 512 KB,探测永远不通过。实测(`Documentations/Internal/Reviews/2026-07-31-node-store-migration-review.md:80-96`,release): + +| 场景 | 每次调用固定开销 | 放大倍数 | +|---|---|---| +| 小树打印 | 8.2 µs | 2.28× | +| 916 字符真实符号 | 20.8 µs | 1.14× | + +索引侧的同类问题已修:`SymbolIndexStore.buildStorageImpl` 把整趟扫描包进 `withLargeStack`(`Sources/MachOSymbols/SymbolIndexStore.swift:456-482`),10 万符号 1317 → 701 ms。打印侧做不到,评审记录 `:128` 写明:「打印侧的循环在 `SwiftDeclarationPrinter` 里,是 async,同步的 `withLargeStack` 无法包裹。真要摊销需要自定义一个跑在 8 MB 线程上的 `SerialExecutor`,或把打印批次改成同步。」`Node+.swift:79,109` 的注释还引用着上游已不存在的 `executeWithUncheckedSendability`。 + +### 2. 多版本准备是串行的,而它们彼此独立 + +`DiffCommand` 先 `oldBuilder.prepare()` 再 `newBuilder.prepare()`(`Sources/swift-section/Commands/DiffCommand.swift:82-87`);`AnySwiftEvolutionInterfaceBuilder.prepare()` 用 for 循环逐版本 `await`(`Sources/SwiftInterface/AnySwiftEvolutionInterfaceBuilder.swift:96-100`)。2026-09-02 的实测(debug,SwiftUI 三个归档 cache):单版本索引 202 s 墙钟 / 182 CPU 秒,单线程;三版本三进程并行 396 s 墙钟,约 2 倍加速。85% 的时间在 `SwiftDiffableInterfaceBuilder.prepare()` 的逐定义 `index(in:)`。 + +### 3. 「全库 async 化」不是答案 + +2026-09-03 的调研结论:把剩下的同步模块也改成 async 只是再套一层壳,且撞硬墙——`deinit`、属性 getter、`Hashable` / `Codable`、`for … where` 子句不能 `await`;MachOKit 与 Swift 运行时调用是同步的;`Node` 与三个 Definition 是非 `Sendable` 的 class;协作线程 512 KB 栈让 async 化后探测 100% 不通过;符号扫描改逐个 `await` 反而比现在的批量内联慢。收益在「并行」和「让 async 代码跑在大栈上」,不在「挂起」。 + +## 前期调研 + +- **SE-0417 任务执行器偏好**(Swift 6.0 实现,运行时 macOS 15 / iOS 18 / tvOS 18 / watchOS 11 / visionOS 2):`withTaskExecutorPreference(_:operation:)` 内的 `nonisolated async` 函数、子任务与默认 actor 都跑在指定执行器上;非结构化 `Task {}` 不继承。本包部署下限 macOS 10.15 / iOS 13(`Package.swift:975`),需要 `#available` 门控。 +- **上游探测机制与本提案的契合**:探测看的是剩余栈空间而非线程身份,所以任何 16 MB 线程上 `execute` / `executeAsync` 都直接内联(`StackSafeExecutor.swift:84-86,118-120`)。上游已有 `LargeStackThreadPool`(按 QoS 分五个子池、`pthread_create` + `pthread_attr_setstacksize` 建线程、`NSCondition` 停车,`:358-746`);`@_spi(Internals) public enum StackSafeExecutor`(`:41-42`)。已登记问题:打印器 768 层上限 × 每层约 11.6 KB ≈ 8.9 MB,超过 8 MB worker(上游 `Documentations/KnownIssues.md` #4)——上游提案 0014 给执行器线程开 16 MB。 +- **swift-demangling 版本**:本仓库钉 0.6.0;0.6.1 的 QoS 改动让 dump 慢 3–4 倍(2026-09-02 二分);0.6.2 含分池修复,2026-09-03 用户实测速度已恢复;**0.6.3 已于 2026-09-03 打 tag 推到远端(tag 指向 `8f32e30`,含提案 0014 的实现 `eaf7e76`)**。 +- **上游 0.6.3 的实际接口**(由 swift-demangling 会话确认,与本提案详细设计一致):`@_spi(Internals) import Demangling` 后用 `StackSafeExecutor.taskExecutor`,类型 `LargeStackTaskExecutor: TaskExecutor`,`@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)`;`withTaskExecutorPreference(StackSafeExecutor.taskExecutor) { … }` 或 `Task(executorPreference: StackSafeExecutor.taskExecutor, priority: …) { … }`(`executorPreference` 在 `priority` 之前);非 Darwin 平台没有这个符号。上游落地时用户批准了四处修订,接入侧无需改动:优先级映射直接用 `JobPriority` 原始值(它本身就是 Darwin QoS 类数值,`unspecified` 归 `DEFAULT`);提交只用稳态额度(每类 `max(2, 核数)`),`enqueue` 从不阻塞;池建不出线程时回退到一次性 16 MB 专用线程,再回退到 `DispatchQueue.global(qos:)`,绝不在 `enqueue` 里就地跑;执行器线程 16 MB——实测 8 MB 线程上打印器 380 层、remangler 200 层先于计数器 SIGBUS,16 MB 上同深度完整完成、计数器先触发(打印器 383 层起 `<>`,remangler 260 层起 `.tooComplex`),所以 `KnownIssues` #4 在执行器路径上对打印器与 remangler 关闭;`TypeDecoder` 需要约 30 MB、仍会先爆栈(它本就不经 `StackSafeExecutor`)。上游文档:`Documentations/StackSafety.md` 第八节。 +- **接入时要守的两条上游契约**:非结构化 `Task {}` 不继承偏好,库入口若在内部起 `Task` 必须显式传 `executorPreference`;一个 job 阻塞线程等同类另一个 job 会耗尽该类 worker,契约与协作线程池相同——`SharedCache` 的 `NSCondition` 等待正是这种阻塞,跨版本并行的并发上限因此不应超过核数。 +- **跨版本并行的安全性**:`MachOFile.identifier` 按 LC_UUID 键控(`MachOKitExtensions/Sources/MachOKitExtensions/MachORepresentableWithCache.swift:16-33`),五个 `SharedCache` 单例全部按此键分片;本仓库描述符读取走 `MemoryMappedFile`(`Sources/MachOReading/Extensions/MachOFile+.swift:21-31`);demangler 池按核数扩。三进程实测已证明可并行;进程内并行只多了共享 `SharedCache` 字典锁与 `PerImageCacheEvictionRegistry` 的 `NSLock`,都是短临界区。 +- **版本内并行为什么不做**:MachOKit 自己的读取有 124 处 `fileHandle.seek` + `read`(`MachOFile.swift`、`DyldCache.swift`、`_DyldCacheFileRepresentable.swift` 等)共用一个句柄,两个线程交错就读错位置;`index(in:)` 的 `guard !isIndexed` 是非原子的检查加赋值(`Sources/SwiftDeclaration/Components/Definitions/TypeDefinition.swift:166`、`ExtensionDefinition.swift:176`、`ProtocolDefinition.swift:158`)。前者不在本仓库,需要 MachOKit 改 mmap 读或每个分片独立 `MachOFile` 实例,另起提案。 +- **内存**:`evolution --interface` 路径 N 个索引器本来就同时常驻,约 190 MB / 版本;lineage / JSON 路径现在逐版本释放(三版本峰值 308 MB),并行后同时常驻的索引器数等于并发上限。 +- **已有的真挂起点**(并行改造不会碰):`SymbolIndexStore.prepareWithProgress` 的 `AsyncStream`(`SymbolIndexStore.swift:1264-1276`)、上游 `executeAsync` 的 continuation、`TypeIndexing` 的 actor。 +- **顺带发现**:`SwiftDeclarationIndexer.swift:782` 的 `await symbolIndexStore.memberSymbols(...)` 等的是同步函数,是空的 `await`;`Utilities/ConcurrentMap.swift` 零调用方。 + +## 提议方案 + +1. **执行器由上游提供,本仓库在库入口自装偏好。** 新增一个小工具(放 `MachOSymbols`,它已 `@_spi(Internals) import Demangling` 且位于所有消费者之下): + + ```swift + public enum LargeStackTaskExecution { + /// Process-wide switch; hosts that manage their own executor set it to false. + @Mutex public static var isEnabled: Bool = true + + /// Runs `body` with the demangler's large-stack task executor as the task + /// executor preference when the runtime has one; otherwise runs `body` + /// unchanged. Output is identical either way; only where the work runs differs. + public static func run(_ body: () async throws -> Success) async rethrows -> Success + } + ``` + + 以下入口在函数体最外层包一次 `LargeStackTaskExecution.run`:`SwiftDeclarationIndexer.prepare()` / `updateConfiguration(_:)`;`SwiftInterfaceBuilder.prepare()` / `printRoot()`;`SwiftDiffableInterfaceBuilder.prepare()`;`AnySwiftEvolutionInterfaceBuilder.prepare()` / `printAnnotatedInterface()` / `annotatedBlocks()`;`SwiftDiffableInterfaceRenderer.printAnnotatedInterface(format:)` / `annotatedDiffBlocks()`;`SwiftDeclarationPrinter.printTypeDefinition` / `printProtocolDefinition` / `printExtensionDefinition` / `printDefinition`(RuntimeViewer 逐类型打印绕过 `printRoot` 的路径);`SwiftDump` 的 `Dumpable.dump(using:in:)` 家族。已在该执行器上的嵌套包裹不产生跳转。宿主零改动。 +2. **macOS 15 / iOS 18 以下静默回退**:`run` 里 `#available` 不满足或执行器不可用就直接执行 `body`,行为与今天完全一致。 +3. **跨版本并行**:`AnySwiftEvolutionInterfaceBuilder.prepare(maximumConcurrentPreparations:)`(默认 `min(版本数, activeProcessorCount)`),用 `withThrowingTaskGroup` 按窗口并发、结果按版本序落位;`DiffCommand` 两个 builder 用 `async let`;`EvolutionCommand` 的 lineage 输入加载同样按窗口并行;CLI 增加 `--jobs N`(`--jobs 1` 即串行)。事件 handler 的 stderr 输出会交错,属可接受。 +4. **前置条件**:上游提案 0014 已随 0.6.3 落地(2026-09-03);本提案获准后第一步把 swift-demangling pin 抬到 0.6.3(跳过 0.6.1),并在同一份 `Package.resolved` 下做一次 0.6.0 vs 0.6.3 的 SwiftUICore `dump` / `interface` 计时作为基线。 + +### 非目标 + +- 版本内逐定义并行(等 MachOKit 侧的读取线程安全,另起提案)。 +- `SharedCache` / `SymbolIndexStore` 的 async 建表路径(2026-09-03 评估收益小,不做)。 +- 恢复符号扫描的多路并行(扫描只占导出约 1%)。 +- 抬部署下限。 +- 任何输出格式变化。 + +## 详细设计 + +### `LargeStackTaskExecution.run` + +```swift +@_spi(Internals) import Demangling + +public enum LargeStackTaskExecution { + @Mutex public static var isEnabled: Bool = true + + public static func run(_ body: () async throws -> Success) async rethrows -> Success { + if isEnabled, #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + return try await withTaskExecutorPreference(StackSafeExecutor.taskExecutor, operation: body) + } + return try await body() + } +} +``` + +`StackSafeExecutor.taskExecutor` 由上游 0.6.3 以 `@_spi(Internals)` 提供(上游提案 0014)。`withTaskExecutorPreference` 要求 `Success: Sendable`:`SemanticString`、`[[EvolutionLine]]`、`[[DiffLine]]`、`Void` 均满足。 + +### 入口包裹 + +每个入口的改动形态一致,以 `printRoot` 为例: + +```swift +public func printRoot() async throws -> SemanticString { + try await LargeStackTaskExecution.run { + try await printRootContents() + } +} +``` + +`@Dependency` 的 task-local 在执行器上正常传播(不再有 pthread 跳转),`SharedCache` 的 `NSCondition` 等待与 `os_unfair_lock` 会占住执行器线程,与今天占住协作线程等价。 + +### 跨版本并行 + +```swift +extension AnySwiftEvolutionInterfaceBuilder { + public func prepare(maximumConcurrentPreparations: Int = ProcessInfo.processInfo.activeProcessorCount) async throws +} +``` + +实现:`withThrowingTaskGroup` 中最多 `maximumConcurrentPreparations` 个在飞,每个子任务 `try await versionUnit.prepare()`;子任务继承执行器偏好。`prepare()` 无参形态保留为默认值调用。`DiffCommand`: + +```swift +async let oldPrepared: Void = oldBuilder.prepare() +async let newPrepared: Void = newBuilder.prepare() +try await (oldPrepared, newPrepared) +``` + +### 验证 + +- 渲染 A/B(`Scripts/run-rendering-ab-verification.py`)逐字节:执行器开 / 关各一次。 +- 计时表:SwiftUI 与 SwiftUICore 的 `dump` 与 `interface`,执行器关 vs 开;`evolution` 三版本 `--jobs 1` vs 默认。 +- 测试:`LargeStackTaskExecution.run` 在支持的系统上把体跑在上游执行器(以上游提供的探测 / 计数钩子断言零跳转)、不支持时原样执行;并行 `prepare` 与串行 `prepare` 产出的 snapshot 逐字节相同;`--jobs` 解析。 + +## 替代方案考量 + +- **全库 async 化**:见动机 §3,否决。 +- **把打印批次改回同步再包 `withLargeStack`**:不依赖 macOS 15,但砍掉现有 async API,RuntimeViewer 全部调用点重写。否决。 +- **本仓库自建执行器**:不等上游发版,但进程里两套大栈线程池,栈策略分散在两个仓库。2026-09-03 用户选上游。 +- **宿主自己装偏好**:更显式,但每个宿主都要改,漏装就回到逐次跳转。用户选库入口自装。 +- **SE-0392 自定义 `SerialExecutor` 的 actor 给 macOS 14 兜底**:多一套机制维护,覆盖的只是一个系统版本。用户选静默回退。 +- **默认串行、`--jobs` 显式打开**:内存行为与今天一致,但默认拿不到加速。用户选默认并行、上限取核数。 + +## 影响 + +### 源码兼容性(source compatibility) + +**纯新增**:`LargeStackTaskExecution`、`prepare(maximumConcurrentPreparations:)`、CLI `--jobs`。所有既有签名不变;入口包裹对调用方透明。 + +### ABI 兼容性 + +不适用 —— 本库以 SPM 源码分发,使用方每次重新编译。 + +### 下游影响 + +本仓库:`MachOSymbols`、`SwiftIndexing`、`SwiftInterface`、`SwiftPrinting`、`SwiftDump`、`swift-section`。依赖:swift-demangling ≥ 0.6.3。 + +跨仓库:RuntimeViewer、MachOKitUI、SymbolViewer 无需改动即受益;RuntimeViewer 若自行管理执行器可置 `LargeStackTaskExecution.isEnabled = false`。 + +### 文档与示例 + +AGENTS.md(`SwiftInterface` / `SwiftIndexing` 条目补执行器一句;测试环境节补「协作线程 512 KB」的对策);`Documentations/Internal/Reviews/2026-07-31-node-store-migration-review.md:128` 那条待办标记已解决;`Node+.swift` 过期注释修正;演进账本;Changelog。 + +## API 演进与废弃策略 + +无废弃项。`prepare()` 无参形态保留。 + +## 落地步骤 + +1. 抬 swift-demangling pin 到 0.6.3(上游 0014 已 Implemented),跑一次基线计时(SwiftUI / SwiftUICore dump 与 interface)作为对照。 +2. 库内所有在 `prepare` / `printRoot` 等入口内部起的非结构化 `Task {}` 逐一核对,显式传 `executorPreference`。 +3. `MachOSymbols` 加 `LargeStackTaskExecution` 与测试。 +4. 逐入口包裹(索引器 → interface builder → printer 入口 → diff / evolution → dump),每一步全量测试。 +5. 跨版本并行:库 API、`DiffCommand`、`EvolutionCommand`、`--jobs`,串行 / 并行 snapshot 等价测试。 +6. 渲染 A/B 逐字节 + 计时表写入实现说明。 +7. 文档同批:AGENTS.md、评审记录待办、`Node+.swift` 注释、演进账本、Changelog 与 `Version.swift`。 + +**收尾时判断**:实现说明——写(执行器为什么按剩余栈探测就能生效、哪些入口包裹了、计时数据);术语——「大栈执行器」视落地时是否在多处出现再决定是否入术语表。 + +## 决策日志 + +| 日期 | 变更 | 说明 | +|------|------|------| +| 2026-09-03 | Created as Draft | 用户问「整个库都使用 async 环境是否可行」;调研结论是全库 async 化不可取,收益在大栈执行器与并行 | +| 2026-09-03 | 范围:执行器 + 跨版本并行 | 澄清提问第一轮:版本内并行另起提案(卡在 MachOKit 共享 FileHandle) | +| 2026-09-03 | 执行器归上游 swift-demangling | 同轮:复用 `LargeStackThreadPool`,进程内一个池;上游提案 0014 | +| 2026-09-03 | 库入口自装偏好;macOS 15 以下静默回退;默认并行上限取核数 | 第二轮 | +| 2026-09-03 | 上游提案由本人在 sibling 仓库起草;上游发版号 0.6.3 | 第三轮与收尾确认;用户告知 0.6.2 已实测恢复 | +| 2026-09-03 | 上游 0.6.3 已发版,接口与四处修订对齐 | swift-demangling 会话通知:tag `8f32e30`、实现 `eaf7e76`;`StackSafeExecutor.taskExecutor` / `LargeStackTaskExecutor`;优先级映射改用 `JobPriority` 原始值、只用稳态额度、双级回退、16 MB 实测深度。状态仍为 Draft,等用户置 Accepted 后再抬 pin 与开工 | +| 2026-09-03 | Accepted → In Progress | 用户指示「基于上一个 PR 实现 async 提案」,视为批准;分支自 ABI 提案的分支切出,第一步抬 swift-demangling pin 到 0.6.3 | +| 2026-09-03 | 落地偏差:`isEnabled` 初值读环境变量 | 提案只有静态开关;加 `MACHO_SWIFT_SECTION_LARGE_STACK_EXECUTOR=0` 是为了让渲染 A/B 与计时用同一个二进制比较开 / 关,宿主不必重编 | +| 2026-09-03 | 落地偏差:并行用通用 `concurrentMap(maximumConcurrency:)` 而非 `async let` | `DiffCommand` 与 evolution 的 lineage 输入同样需要窗口化,一个 `Utilities` 帮手三处复用,`--jobs 1` 与默认走同一条代码路径 | +| 2026-09-03 | 落地偏差:CLI 的 `dump` 循环不再额外包一层 | 六个 `Dumpable.dump` 已各自包裹,CLI 逐类型进出执行器约一万跳、零点几秒,远小于原来逐符号跳转 | +| 2026-09-03 | 核对:库内零处非结构化 `Task {}` | 唯一的 `withTaskGroup` 在 `TypeIndexing.TypeDatabase`,结构化、继承偏好;落地步骤 2 无需改动 | +| 2026-09-03 | 验证:全量 1637 测试通过;计时 −16% ~ −23%(单版本)、2.0×(三版本 evolution);四种配置输出逐字节一致 | 数据见实现说明「实测数据」;0.6.0 → 0.6.3 仅抬 pin 持平,证明 0.6.1 的回归未带入 | +| 2026-09-04 | Review 修复批次(PR #122 review,15 条:真缺陷 5、误报 1、取舍 9) | 修:`concurrentMap` 取消语义(`addTaskUnlessCancelled` + 抛 `CancellationError`)、执行器测试同时挡 `isEnabled`、环境变量接受 `0/false/no/off`、并行等价测试先跑并行、三方 barrier 钉窗口宽度;用户裁定:F2 用 Dispatcher 进程级递归锁串行化投递(不改 `Handler` API)、F6 保持核数、F7 加 `ConsoleEventHandler(label:)` 与 `eventHandlersPerVersion`;F9 重复门判为不可消除(A32);其余登记 A25–A33。清单见 `Roadmaps/2026-09-04-pr122-review-findings.md` | +| 2026-09-03 | 收尾判断:写实现说明;「大栈执行器」入术语表 | 实现说明记录探测机制为何免改调用点、入口清单与嵌套免费、回退与开关、并行安全性与不做版本内并行的原因、计时表;术语在 AGENTS.md / 提案 / 实现说明 / 账本多处出现,登记 `Documentations/Glossary.md` | diff --git a/Documentations/Evolutions/0020-vtable-slot-attribution-via-method-descriptor-symbols.md b/Documentations/Evolutions/0020-vtable-slot-attribution-via-method-descriptor-symbols.md new file mode 100644 index 00000000..229c4713 --- /dev/null +++ b/Documentations/Evolutions/0020-vtable-slot-attribution-via-method-descriptor-symbols.md @@ -0,0 +1,111 @@ +# 0020 - vtable 槽归属改用 method descriptor 符号:ICF 折叠下的错名修正与墓碑槽还原 + +- **状态**: Implemented +- **作者**: JH +- **创建日期**: 2026-09-06 +- **最后更新**: 2026-09-07 +- **所属愿景**: 无 +- **关联提案**: [0006-final-keyword-and-lazy-accessor-type-recovery](0006-final-keyword-and-lazy-accessor-type-recovery.md)(首次把 `Tq` 符号当作 ICF 免疫证据用于 `final` 判定,但只用作否定证据,没有用于正向归属——本提案补上那一步) +- **实现分支 / PR**: `feature/vtable-slot-attribution`(worktree `.worktrees/MachOSwiftSection-VTableSlotAttribution`),[PR #123](https://github.com/MxIris-Reverse-Engineering/MachOSwiftSection/pull/123) +- **配套文档**: [TaskReports/2026-09-06-vtable-slot-attribution.md](../Internal/TaskReports/2026-09-06-vtable-slot-attribution.md)(过程复盘) + +## 摘要 + +class vtable 每个槽打印成哪个成员,今天是靠「实现地址反查符号」决定的:从 `MethodDescriptor` 的 implementation 相对指针算出偏移,再问符号索引这个偏移上有哪些符号。这个映射在 linker 做过 identical code folding(ICF,把字节相同的函数体合并到同一地址)之后就是一对多的,反查不回去。SwiftUICore(iOS 18.5 arm64)里 `SwiftUI.GraphHost` 的四个空实现方法全部折叠在 `0x9330`,那个地址上有 2878 个符号,于是 dump 把 vtable 槽 26–29 印成了 `isHiddenForReuseDidChange()` 加三个属于嵌套 struct `GraphHost.Data` 的协程 resume 函数,而真正的 `instantiateOutputs()` / `uninstantiateOutputs()` / `timeDidChange()` 一个都没出现在 vtable 列表里。 + +每个 method descriptor 自己带一个 `Tq` 符号(method descriptor symbol),它是 `S` 类型的全局数据符号,一个成员一个唯一地址,ICF 完全影响不到。本提案把 vtable 槽的归属**主源**从「实现地址反查」换成「descriptor 自身地址查 `Tq`」,实现地址反查降级为回退;顺带修掉筛选条件 `node.first(of: .class)` 把嵌套类型成员误判成本类成员的问题,并让 `Tq` 还原出那些实现已被删除、槽位为 ABI 保留的墓碑槽的名字。 + +## 方案 + +### 现状与真值(`SwiftUI.GraphHost` 实测) + +类描述符 `0x98c9c8`,`vTableOffset = 20`、`vTableSize = 10`,十条 method descriptor 连续排在 `0x98c9fc`–`0x98ca44`: + +| slot | descriptor | `Tq` 符号给出的真值 | 当前 dump 输出 | +|---|---|---|---| +| 20 | `0x98c9fc` | getter,metadata 里绑到 `swift_deletedMethodError` | `Symbol not found` | +| 21 | `0x98ca04` | setter,同上 | `Symbol not found` | +| 22 | `0x98ca0c` | modify,同上 | `Symbol not found` | +| 23 | `0x98ca14` | `init(data:)` | 一致 | +| 24 | `0x98ca1c` | `graphDelegate.getter` | 一致 | +| 25 | `0x98ca24` | `parentHost.getter` | 一致 | +| 26 | `0x98ca2c` | `instantiateOutputs()` | `isHiddenForReuseDidChange()` | +| 27 | `0x98ca34` | `uninstantiateOutputs()` | `Data.graph.modify … .resume.0` | +| 28 | `0x98ca3c` | `timeDidChange()` | `Data.globalSubgraph.modify … .resume.0` | +| 29 | `0x98ca44` | `isHiddenForReuseDidChange()` | `Data.rootSubgraph.modify … .resume.0` | + +前三槽在 class metadata(`0xaaa000`)里是 chained-fixup bind 槽,import ordinal `0xc1c` 解出来是 `_swift_deletedMethodError`:某个 overridable 属性被删除了,槽位作为 ABI 墓碑保留,调用即 trap。descriptor 侧 implementation 为 null,正是这个原因。 + +### 两条根因 + +**一、归属主源选错。** `Sources/SwiftDump/Dumper/ClassDumper.swift:239` 用 `descriptor.implementationSymbols(in: machO)` 取名字。ICF 之下一个地址对应多个成员,这个方向的映射不存在逆。 + +**二、候选筛选过松。** `Sources/SwiftDump/Dumper/ClassDumper.swift:618` 用 `node.first(of: .class)` 深度优先找第一个 class 节点来判断「这个符号属不属于本类」。`GraphHost.Data.graph.modify` 的 context 链是 `class GraphHost → struct Data`,`first(of: .class)` 命中 `GraphHost`,于是嵌套类型的成员被认作本类的 vtable 方法。这一条独立于 ICF 也是错的。 + +### 影响面(SwiftUICore iOS 18.5 arm64,171 个非泛型带 vtable 的类 / 512 个槽) + +| 情况 | 槽数 | 占比 | 本提案后 | +|---|---|---|---| +| implementation 符号唯一 + 有 `Tq` | 224 | 43.8% | 已正确,`Tq` 只是加固 | +| implementation 为 null(墓碑)+ 有 `Tq` | 79 | 15.4% | **从 `Symbol not found` 变成有名字** | +| implementation 多符号(ICF)+ 有 `Tq` | 64 | 12.5% | **从错名变正确名**(GraphHost 属于此类) | +| implementation 符号唯一 + 无 `Tq` | 27 | 5.3% | 已正确,走回退 | +| implementation 多符号(ICF)+ 无 `Tq` | 19 | 3.7% | 仍不可归属,标注为不可靠 | +| implementation 为 null(墓碑)+ 无 `Tq` | 91 | 17.8% | 仍无名,标注为无实现 | +| implementation 无符号 | 8 | 1.6% | 仍无名 | + +泛型类的 vtable 不在这份统计里(统计脚本跳过了 trailing object 布局较复杂的泛型描述符),但归属机制与非泛型类完全相同,修复同样覆盖。 + +### 改动 + +1. **`Sources/SwiftInspection/Extensions/Descriptor+MethodDescriptorSymbols.swift`**(新文件)——`MethodDescriptor.methodDescriptorSymbols(in:)` 拿 descriptor **自身的偏移**查符号索引,`attributedMemberNode(in:)` 把 `global(methodDescriptor())` 还原成 printer 期望的 `global()` 形状。放在 `SwiftInspection` 而不是 ABI 层,与提案 0018 定下的分工一致:`MachOSwiftSection` 只暴露地址,符号归属属于上一层。 + + **只给 `MethodDescriptor`,不给两个 override descriptor**(最初写了,实测后撤回,见决策日志):override descriptor 自己没有 `Tq`,它指向的是**父类**的 descriptor,用那个身份回答的是另一个问题——dump 要打印的是本类的实现符号,`TypeDefinition.index` 要 join 的是本类的成员符号。override 槽的槽号本来就来自 `ParentClassVTableCache`,从不依赖符号归属。 + +2. **`Sources/SwiftDump/Dumper/ClassDumper.swift` 的 vtable 主循环**——归属改为三级:先 `Tq`,取不到再走实现地址反查,都取不到才落到地址或墓碑文案。两个 override 循环保持原样。 + +3. **`Sources/SwiftDump/Dumper/ClassDumper.swift` 的 `validNode`**——把「节点里含有本类的 class 节点」换成「成员的直接 context 就是本类」(`NodeReference.declarationContextNode`,新文件 `Node+DeclarationContext.swift`),堵住嵌套类型串味。`SwiftDeclaration` 的 `demangledOverrideSymbol` 同样处理。`ProtocolDumper` 的同名 helper 试改后回退(见决策日志)。 + +4. **`Sources/SwiftDeclaration/Components/Definitions/TypeDefinition.swift`**——interface 路径的 `methodDescriptors` 循环同样优先走 descriptor 符号(两个 override 循环同上,保持原样)。这条路径修复前的症状比 dump 轻但同样不对:GraphHost 的 slot 26 一样错标到 `isHiddenForReuseDidChange`,而 `instantiateOutputs` / `uninstantiateOutputs` / `timeDidChange` 三个真 vtable 方法完全没有 vtable 注释。 + +5. **两种渲染档**(本轮已定,见决策日志),落在 `DeclarationRenderConfiguration` 的 `ambiguousAttributionComment` / `deletedMethodSlotComment`: + - 不可归属槽(无 `Tq` 且实现地址被折叠)仍打印猜测名,但加注释说明该地址折叠了多少个符号、归属不可确定。 + - 墓碑槽(implementation 为 null)打印 `Tq` 还原出的声明,并注释说明该槽在本镜像内无实现、调用会 trap;拿不到 `Tq` 的墓碑槽渲染为 ``。 + +### 明确不做 + +- **protocol witness / resilient witness 的归属不动。** `ResilientWitness` 与 `ProtocolRequirement` 的默认实现没有对应的独立 descriptor 数据符号,`Tq` 这条路子在那边不存在,只能继续靠实现地址反查。它们同样受 ICF 影响,但那是另一个问题,需要另外的证据源,不在本提案范围。 +- **`SwiftDiffing` 的 snapshot 格式不动。** 已确认 `MemberRecord` 的 identityKey / payloadKey 都不含 vtable offset(`Sources/SwiftDiffing/` 全目录无 vtable 引用),`formatVersion` 不需要 bump,历史 baseline 不失效。 +- **不改 vtable 槽号的计算方式。** `vTableOffset + index` 经 GraphHost 实测与 class metadata 的 immediate members 布局吻合(`numImmediateMembers = 20` = field offset vector 10 + vtable 10,`fieldOffsetVectorOffset = 10`,故 vtable 起于 word 20),这部分本来就是对的。 + +### 验证(已执行,数据为 SwiftUICore iOS 18.5 arm64 实测) + +- **单元回归**(`Tests/SwiftDumpTests/VTableSlotAttributionTests.swift`):on-the-fly 编译的 fixture,`open class Host` 三个空方法加一个嵌套 `class Nested` 的空方法,全部用 `-Xlinker -deduplicate` 强制折叠到同一地址。修复前 dump 出 `beta` / `alpha` / `gamma`(符号表顺序),修复后 `alpha` / `beta` / `gamma`(`Tq` 地址顺序)。fixture 带 class 满足 `__DATA` 段要求。 + - 嵌套串味在这个规模的 fixture 上**复现不出来**:是否串味取决于 linker 把嵌套成员排在符号表的哪个位置,这里外层类的三个成员排在前面、槽位先被取完。相关的两条测试因此是防御性的(修复前也绿),已在测试注释里写明,实证复现在下面的 GraphHost 套件。 +- **真实二进制回归**(`GraphHostVTableAttributionTests`):钉住 GraphHost 的四个折叠槽(26–29 对应 `instantiateOutputs` / `uninstantiateOutputs` / `timeDidChange` / `isHiddenForReuseDidChange`)、`!output.contains("resume")`、以及 20–22 的墓碑注释。simruntime 路径按目录扫描发现,缺失即跳过。 +- **红/绿证明**:源码改动整体回退后,5 条测试红 3 条(9 个 issue);改动恢复后 5 条全绿。 +- **全库 A/B**:828 条声明行变化。7 处 `.resume.` 串味全部消失、零新增;`Symbol not found` 358 → 0(其中 195 条拿到真名,163 条转为带墓碑注释的 ``);**零**行从有名字退化成 `sub_` 地址。 +- **机械比对**:把 1199 个 `Tq` 符号按地址排序作为真值,与 dump 输出的槽序列逐类比对——55 个类同时具备 `Tq` 真值与 vtable 输出,其中 54 个相对顺序与 `Tq` 地址顺序完全一致,**0 个顺序错配**(余下 1 个是比对脚本的类名前缀归属误判,把嵌套类 `ResolvedStyledText.TextLayoutManager` 的成员算给了外层类)。 +- **渲染 A/B 脚本**:`Scripts/run-rendering-ab-verification.py` 的逐字节判据不适用于本提案(改变输出正是目的),故以上述定量比对替代,结论同样写入任务报告。 + +### 已知遗留 + +- `ClassDumper.vtableAccessorFieldNames`(`final` 关键字还原的证据源之一)仍按实现地址收集访问器字段名,ICF 下会把折叠地址上所有符号的字段名一并收进来。后果是**少标** `final`(保守方向,不会错标),且它还有 `Tq` method descriptor 符号作为第二证据源。改用 `Tq` 主源会改变 `final` 输出,属于提案 0006 的领域,未纳入本批次。 + +## 决策日志 + +| 日期 | 决定 | 理由 | +|------|------|------| +| 2026-09-06 | Created as Draft | 用户报告 Hopper 看到的 `SwiftUI.GraphHost` vtable 布局与 dump 输出不符;调研确认 dump 错,根因是 ICF 下实现地址反查符号不可逆 | +| 2026-09-06 | 归属主源改用 method descriptor 自身的 `Tq` 符号,实现地址反查降级为回退 | `Tq` 是 `S` 类型全局数据符号,每个成员一个唯一地址,ICF 免疫;提案 0006 落地时(commit 83a4308c)已确认过这条性质,但只把它用作 `final` 判定的否定证据,没有用于正向归属 | +| 2026-09-06 | 不可归属槽(无 `Tq` 且实现地址折叠,实测 3.7%)仍打印猜测名,但加注释标明折叠符号数与归属不可确定 | 保留线索的同时不误导读者;完全不给名字会让这批槽失去全部可读性 | +| 2026-09-06 | 墓碑槽打印 `Tq` 还原的声明并注明「本镜像内无实现」 | descriptor 的实现被删除但符号仍在,能还原出被删的是哪个方法(实测 79 槽 / 15.4%),比现状的 `Symbol not found` 信息量高;同时必须讲清它没有实现,否则读者会以为是普通成员 | +| 2026-09-06 | 走轻量档:不套完整模板,不做完整拷问 | 归属机制的局部修正,不涉及架构变更或破坏性 API;新增的 `descriptorSymbols(in:)` 是纯增量 API | +| 2026-09-06 | 用户批准,直接进入 In Progress | 轻量档提案,用户看过方案后指示开工;`In Review` 阶段跳过 | +| 2026-09-06 | ProtocolDumper 的同类修改试做后回退 | 协议侧符号不只是成员:`base conformance descriptor for P: Q` 等要求描述符没有 entity 节点,按声明上下文匹配会整批丢弃(实测 SwiftUICore 协议输出 1033 行退化为 `[Stripped Symbol]`)。协议侧需要自己的证据模型,不在本提案范围 | +| 2026-09-06 | `validNode` 的上下文修复保留,尽管在本二进制上零影响 | `Tq` 主源已覆盖所有会出问题的槽,隔离 A/B 显示该修复单独作用时输出逐字节不变;保留是因为它在 `Tq` 缺失的回退路径上仍是正确性前提 | +| 2026-09-06 | `vtableAccessorFieldNames` 不改 | 同一 ICF 根因,但后果是保守的少标 `final`,且改动会牵动提案 0006 的输出,混入本批次会让 A/B 审查失去焦点 | +| 2026-09-06 | `Tq` 主源**只用于类自己的 `MethodDescriptor`**,两个 override descriptor 撤回 | 先按「override 也能经父类 descriptor 拿 `Tq`」实现,结果 `override` 关键字从输出里整个消失——`TypeDefinition.index` 的 joinKey 要跟本类成员符号对上,父类形状的节点匹配不到任何东西(`SymbolTestsCoreE2ETests.outputContainsOverrideKeyword` 抓住)。撤回后另有一处残留在 `dumpMethodDeclaration`,被 override 循环的 `.element` 腿调用时把 `override ResilientChild.init()` 打成 `override ResilientBase.init()`,并丢掉实现符号带的 `vtable thunk … dispatching to …` 细节(快照 diff 审查抓住)。教训:`Tq` 回答的是「这个 descriptor 声明了谁」,override 槽问的是「本类的实现是谁」,不是同一个问题 | +| 2026-09-06 | 更新 10 份快照基线(9 份 dump + 1 份 interface) | 逐条审查确认全部为修正:16 条墓碑注释新增、10 条 `[Init] Symbol not found` 拿到真名、6 条降级为 ``、4 条去掉 `async function pointer to` 前缀(vtable 槽显示成员本身而非 `Tu` 常量)、`FinalMembersTest` 三条 kind 与名字的系统性错位修正(`[Setter]` 配 `plainMethod()` 这类)、interface 的 `static func classMethod()` → `class func classMethod()`(fixture 源码写的就是 `public class func`,此前因归属错位没 join 上 descriptor 而误印 `static`) | +| 2026-09-07 | `/code-review xhigh` 跑完 PR #123,15 条发现全部按四问裁决:真缺陷 4、建议同批修 3、低优先级 3、误报或已有裁决 3、流程 2 | 清单与逐条论证见 [`Roadmaps/2026-09-06-pr123-review-findings.md`](../../Roadmaps/2026-09-06-pr123-review-findings.md);「不修 / 误报 / 延后」的终审登记为 A34–A39。本轮**只落记录,代码未改**,修复批次另起 | +| 2026-09-07 | 上面 2026-09-06「墓碑槽」那条决定**成立**,只有措辞要改(本行取代同日一条判它「因果前提被推翻」的记录,那条判断经复核有误,已撤回) | 初判依据是「fixture 里 `TestsObjects` 的 `init()` 明明存在却被标成 deleted」,错在把「声明存在」当成「实现存在」。IRGen 的 `buildMethodDescriptorFields`(`lib/IRGen/GenMeta.cpp` 约 340–364 行)只有两个分支,写 null 那支的原注释即 "The method is removed by dead method elimination."——null 是编译器唯一的写入路径,不是本库的推断。真实根因是**访问级别**:public 类型里不写修饰符的 `init()` 默认 internal,整模块优化下不是死函数消除的 anchor,没人调就被删实现体;fixture 里被标记的全是 internal 或函数内局部类成员,未标记的全是显式 `public init`(`AsyncInitializerActorTest` 幸免是因为 public,与 async 无关)。独立探针确证:`-O -wmo -enable-library-evolution` 下 internal init / 访问器只剩 `Tq` 无函数符号,`dyld_info` 数出的 `_swift_deletedMethodError` bind 数与预期精确吻合。故 SwiftUICore 33% 的比例可信。待修:注释措辞改为 `Implementation removed by dead-method elimination; vtable slot kept for layout (calling it traps)`,并在文档补上「`swift_deletedMethodError` 只填静态 metadata,运行时实例化路径 null 保持 null」这一限定 | diff --git a/Documentations/Evolutions/README.md b/Documentations/Evolutions/README.md index 1ea9e2d1..6e553dd1 100644 --- a/Documentations/Evolutions/README.md +++ b/Documentations/Evolutions/README.md @@ -19,5 +19,12 @@ | [0009](0009-type-indexing-revival.md) | TypeIndexing 重启:`__C` 类型模块归属解析的索引管线修复与重构(两线合并时由 0008 重排至 0009,见提案「编号说明」) | Implemented | | [0010](0010-community-type-mapping-bundles.md) | 补充类型映射:私有框架 `__C` 类型的用户自备 APINotes 加载(AttributeGraph 等;合并时由 0009 重排) | Implemented | | [0011](0011-opaque-primary-associated-type-attribution.md) | opaque 返回类型的 primary associated type 归属:anchor 协议裁决 + 协议事实解析链(main 直落线并入 next 时由 0006 重排) | Implemented | -| [draft](draft-swift-evolution-interface-builder.md) | SwiftEvolutionInterfaceBuilder:ABI 演进的并集注解接口渲染(`evolution --interface`) | Implemented | -| [draft](draft-unify-interface-renderers.md) | 统一 diff / evolution 接口渲染器的结构遍历核心(顺带修 diff accessor 双重缩进) | Implemented | +| [0012](0012-in-process-metadata-type-builder.md) | RuntimeMetadataTypeBuilder:TypeBuilder 的首个生产 conformer,node → 进程内活 metadata | Implemented | +| [0013](0013-swift-evolution-interface-builder.md) | SwiftEvolutionInterfaceBuilder:ABI 演进的并集注解接口渲染(`evolution --interface`) | Implemented | +| [0014](0014-unify-interface-renderers.md) | 统一 diff / evolution 接口渲染器的结构遍历核心(顺带修 diff accessor 双重缩进) | Implemented | +| [0015](0015-type-name-resolver-role-split.md) | TypeNameResolvable 角色化拆分:printer 查询解析器按能力分协议 | Implemented | +| [0017](0017-macho-dependencies-module.md) | 依赖闭包下沉为 MachODependencies 模块:两套依赖加载合一 | Implemented | +| [0016](0016-exported-only-interface.md) | Interface 只打印导出声明(`--exported-only`):提案 0008 标注的过滤形态,打印期按描述符 / 派生符号 / 扩展目标裁决 | Implemented | +| [0018](0018-self-contained-abi-layer.md) | ABI 层自包含:MachOSwiftSection 不再依赖符号索引——描述符只暴露实现地址,符号查询上移 SwiftInspection,值类型下沉 MachOResolving | Implemented | +| [0019](0019-large-stack-executor-and-cross-version-parallelism.md) | 大栈任务执行器接入与跨版本并行准备:打印路径零线程跳转(执行器本体在 swift-demangling 0014),diff / evolution 多版本并行 | Implemented | +| [0020](0020-vtable-slot-attribution-via-method-descriptor-symbols.md) | vtable 槽归属改用 method descriptor 符号:ICF 折叠下的错名修正与墓碑槽还原 | Implemented | diff --git a/Documentations/Glossary.md b/Documentations/Glossary.md index 9371b2a6..a09a89c8 100644 --- a/Documentations/Glossary.md +++ b/Documentations/Glossary.md @@ -30,6 +30,21 @@ - **主要出现在**:`Scripts/run-rendering-ab-verification.py` - **延伸阅读**:[SystemFrameworkRenderingVerification.md](Internal/SystemFrameworkRenderingVerification.md) +### ABI 墓碑(ABI tombstone) + +实现体已被优化器删除、槽位仍保留在 vtable 里的槽。**被删的是函数体,不是声明**——源码、`Tq` 符号、method descriptor 都还在,所以「被删掉实现的是哪个成员」仍然可知;`Tq` 也没有时才退化为 ``。 + +判据是 method descriptor 的 implementation 相对指针为 null,而这是**编译器的权威标记**:IRGen 的 `buildMethodDescriptorFields` 只有两个分支,SIL vtable 有 entry 就写相对地址,没有就写 null,后者的原注释即 "The method is removed by dead method elimination."。 + +**成因是访问级别,不是「API 被删除」**:public 类型里不写修饰符的 `init()` 默认是 internal,在整模块优化(whole-module optimization)下 internal 成员不是 dead function elimination 的 anchor,没人调用(或调用点内联后独立函数体死掉)即被摘掉 vtable entry。OS 框架里多数 vtable 成员是 internal,所以这个现象常见而非罕见——实测 SwiftUICore(iOS 18.5 arm64)341 处、Xcode 自带 SourceEditor.framework 11680 处。 + +**两种 metadata 形态要分开**:静态 class metadata 对这类槽填 `swift_deletedMethodError`,调用即 trap;而运行时实例化的 metadata(泛型类、resilient 父类的 relocate 路径)由 `initClassVTable` 把 descriptor 的 null 原样拷入,**保持 null,不会变成那个函数**。判别标志是 `ClassLayoutFlags::HasStaticVTable`——IRGen 对 Singleton / Update / FixedOrUpdate 三种策略都会设它,所以「自身字段依赖 resilient 类型、但祖先固定」的 Singleton 类 vtable 仍是静态的,照样填 `swift_deletedMethodError`;真正在运行时重建 vtable 的只有泛型类与 Resilient 策略两类。 + +**这也是不要靠 bind 来判定墓碑的原因之一**:离线读一个 Resilient 策略的类,`MachOFile` 里只有 metadata pattern,根本没有 vtable word 可读——判据必须回到 descriptor 的 null 本身。 + +- **主要出现在**:`ClassDumper` 的 vtable 循环、`DeclarationRenderConfiguration.deletedMethodSlotComment` +- **延伸阅读**:[提案 vtable-slot-attribution](Evolutions/0020-vtable-slot-attribution-via-method-descriptor-symbols.md)、[PR #123 review findings 第 1 条](../Roadmaps/2026-09-06-pr123-review-findings.md) + ### anchor 协议(anchor protocol) 一条 same-type 约束的 subject 里,关联类型所**限定的声明协议**——`τ_1_0.[Swift.Sequence]Element == [A]` 的 anchor 是 `Swift.Sequence`(mangling 层限定形式,demangle 后保留在 `dependentAssociatedTypeRef` 的第二个 child)。注意 anchor 是 canonicalization 后**继承链最上层的原始声明者**,不一定是源码 sugar 写在哪个协议上(`Collection<[A]>` 的约束 anchor 是 Sequence),也不一定在 opaque 组合成员之内。opaque 尖括号参数的归属裁决以它为第一信号。 @@ -37,6 +52,13 @@ - **主要出现在**:`Sources/SwiftInterface/OpaqueSameTypeConstraint.swift`、`SwiftInterfaceBuilderOpaqueTypeProvider` - **延伸阅读**:[提案 0011](Evolutions/0011-opaque-primary-associated-type-attribution.md)、[OpaqueReturnTypeResolution.md](Internal/OpaqueReturnTypeResolution.md) §2.2 +### bare image name(裸镜像名) + +一个 dylib load name(`@rpath/Foo.framework/Versions/A/Foo`、`/usr/lib/libobjc.A.dylib`)归约成的镜像名:末段路径去**第一个**扩展名(`Foo`、`libobjc`)。这是与 MachOKit 的契约——`MachOImage(name:)` 对进程内每个镜像的路径做同一归约再比较,把未归约的 load name 喂给它永远匹配不到。它也是所有依赖集合的去重键:同一个库会被不同镜像以不同拼写链接,只有裸名跨拼写稳定。 + +- **主要出现在**:`Sources/MachODependencies/DependencyLoadName.swift`、`DependencyClosure`、`FileDependencyLocator` +- **延伸阅读**:[Modules/MachODependencies.md](Internal/Modules/MachODependencies.md) §2 + ### bucket(桶) 分类索引里「一个键对应的一组符号表行号」(如 `symbolRowsByOffset` 的值、`MemberSymbolRows` 的叶子)。旧形态是 `[UInt32]` 小数组——绝大多数桶只有一个元素,却各付一次堆分配;提案 0003 落地后值形态为 `SymbolRowBucket`(单元素内联于字典槽,第二个元素起才落堆数组),迭代序保持插入序。 @@ -44,6 +66,13 @@ - **主要出现在**:`Sources/MachOSymbols/SymbolIndexStore.swift`、`Sources/MachOSymbols/SymbolRowBucket.swift` - **延伸阅读**:[提案 0003](Evolutions/0003-symbol-row-bucket-flattening.md) +### dependency closure(依赖闭包) + +一个 root 二进制经 `LC_LOAD_DYLIB` 家族 load command 解析出的依赖镜像集合(`MachODependencies.DependencyClosure`)。本项目里的「闭包」默认指**传递**闭包:BFS 递归、按裸镜像名去重、root 排除、解析顺序是契约的一部分(`SwiftLayout.ImageUniverse` 按此顺序惰性索引、命中即停)。同一类型也承载 `.direct` 遍历(只取 root 自己的一层),`SwiftInterfaceBuilderDependencies` 用的是这一种——名字里的「闭包」在那里只是复用同一个结果类型。定位不到的依赖记入 `unresolvedLoadNames`,不算失败。 + +- **主要出现在**:`Sources/MachODependencies/DependencyClosure.swift`、`Sources/SwiftLayout/ImageUniverse+DependencyClosure.swift` +- **延伸阅读**:[Modules/MachODependencies.md](Internal/Modules/MachODependencies.md)、[StaticLayoutDependencyClosure.md](Internal/StaticLayoutDependencyClosure.md) + ### derived symbol forms(派生符号形态) 一个成员实现符号经追加后缀派生出的入口符号:`Tj`(dispatch thunk)、`Tq`(method descriptor)、`Tu`(async function pointer)、`TjTu`。library-evolution 构建的常态是**实现符号不导出、`Tj` 导出**(外部调用方经 thunk 派发),所以判断成员导出状态必须查全形态(`isExportedIncludingDerivedSymbols`)——裸查实现符号会把整个 resilient 库误判为未导出。 @@ -72,12 +101,33 @@ Requirement Machine 最小化泛型签名时,把 pin 到同一具体类型的 - **主要出现在**:`Sources/SwiftPrinting/SwiftDeclarationPrinter.swift`(`renderMember`)、三个 Dumper 的 member-symbol 循环 - **延伸阅读**:[提案 0008](Evolutions/0008-interface-header-and-export-status-annotations.md)、[InterfaceHeaderAndExportStatusAnnotations.md](Internal/InterfaceHeaderAndExportStatusAnnotations.md) +### exported-only 过滤(`--exported-only`,`printExportedDeclarationsOnly`) + +export status 的**过滤形态**:interface 只输出镜像导出的声明。类型 / 协议按描述符符号(`…Mn` / `…Mp`,优先取描述符 offset 处的符号,重整名只兜底)裁决,成员沿用 export status 的派生形态判定,扩展按「被扩展类型 / 遵循协议是否为本镜像内未导出声明」裁决(依据 **`ExportFilterScope`**——`printRoot` 从索引器表算出的本镜像内未导出 `TypeName` / `ProtocolName` 集合)。只在判定为 `false` 时删,`nil` 一律保留(绝不靠猜删);普通扩展被清空则整块删,conformance 扩展留 `{}`。与 export status 是同一个事实的两种呈现:删除条件即标注条件,两开关同开输出零标注。 + +- **主要出现在**:`Sources/SwiftPrinting/SwiftDeclarationPrinter+ExportFilter.swift`、`SwiftInterfaceBuilder.printRoot()` +- **延伸阅读**:[提案 0016](Evolutions/0016-exported-only-interface.md)、[ExportedOnlyInterfaceFiltering.md](Internal/ExportedOnlyInterfaceFiltering.md) + ### emission strategy(发射策略) diff / evolution 两条对比渲染路共享结构遍历核心(`InterfaceUnionWalker`)之后各自剩下的那一半:遍历器负责**结构**(N 路匹配与并集排序、extension 容器拆分、成员构造、类别调度、body 组合序),策略(`InterfaceUnionEmitting`)负责**呈现**——同一个匹配结果如何变成行(`+`/`-` 标记 vs 生命周期注解)、容器 header 如何裁决(两侧配对 vs 最新可渲染)、容器如何装配。真正语义不同的部分(`HeaderOutcome` 配对、注解锚点、两套格式层)只住在策略里,绝不上浮进遍历器。 - **主要出现在**:`Sources/SwiftInterface/InterfaceUnionWalker.swift`(协议与遍历器)、`SwiftDiffableInterfaceRenderer.swift`(`DiffUnionStrategy`)、`SwiftEvolutionInterfaceRenderer.swift`(evolution 策略) -- **延伸阅读**:[提案 draft-unify-interface-renderers](Evolutions/draft-unify-interface-renderers.md) +- **延伸阅读**:[提案 0014](Evolutions/0014-unify-interface-renderers.md) + +### identical code folding(ICF,相同代码折叠) + +linker 把字节相同的函数体合并到同一地址的优化。后果是「地址 → 符号」不再是单射:SwiftUICore 里空 `ret` 那一个地址上挂着 **2878** 个符号。任何「拿实现地址反查这是谁」的逻辑在折叠面前都没有逆——vtable 槽归属因此改用 method descriptor 自身的 `Tq` 符号(每成员一个、位于 descriptor 自身地址,折叠够不着它),实现地址反查只作回退,且回退撞上折叠地址时输出会注明归属不确定。同一事实也是 `final` 关键字还原(提案 0006)必须用 `Tq` 作否定证据的原因。fixture 里可用 `-Xlinker -deduplicate` 强制触发。 + +- **主要出现在**:`Descriptor+MethodDescriptorSymbols.swift`、`ClassDumper`、`TypeDefinition.index`、`FinalKeywordICFRegressionTests`、`VTableSlotAttributionTests` +- **延伸阅读**:[提案 vtable-slot-attribution](Evolutions/0020-vtable-slot-attribution-via-method-descriptor-symbols.md)、[提案 0006](Evolutions/0006-final-keyword-and-lazy-accessor-type-recovery.md) + +### large-stack executor(大栈执行器,`LargeStackTaskExecution`) + +swift-demangling 0.6.3 起提供的 `TaskExecutor`(`StackSafeExecutor.taskExecutor`,线程栈 16 MB,`@_spi(Internals)`)。demangler 每次 demangle / print / remangle 都按**调用线程的剩余栈**决定要不要跳到它的 8 MB 线程池——协作线程只有 512 KB,探针永远不过,async 打印循环因此每打印一个符号付一次线程往返;task 跑在大栈执行器的线程上时探针每个入口都通过,全程原地执行、零跳转。本库通过 `MachOSymbols.LargeStackTaskExecution.run` 在库入口(索引器 prepare、interface builder、printer 逐定义入口、diff / evolution、dump)自装偏好,宿主零改动;macOS 15 / iOS 18 以下静默回退为原样执行。与「跳转池」(demangler 自己的 8 MB `LargeStackThreadPool`,同步 `withLargeStack` 批次用)是两个池:执行器的 job 是整段 task,会占线程上百秒,不能挤占同步跳转的额度。 + +- **主要出现在**:`Sources/MachOSymbols/LargeStackTaskExecution.swift`、各 async 入口的 `LargeStackTaskExecution.run { … }` +- **延伸阅读**:[LargeStackTaskExecutorAdoption.md](Internal/LargeStackTaskExecutorAdoption.md)、提案 [0019-large-stack-executor-and-cross-version-parallelism](Evolutions/0019-large-stack-executor-and-cross-version-parallelism.md)、上游 swift-demangling `Documentations/StackSafety.md` 第八节 ### late-name 路径(`lateDemangledNode(forName:)`) @@ -96,7 +146,7 @@ sweep 覆盖范围之外的名字走的旁路:demangle 后 intern 进 `Storage 演进并集接口里每条「变过的」声明行尾的注释:`// [●●○] removed in 26.0` —— 存在位图(每版本一位,文件头图例映射位置到版本标签)+ 按 ` · ` 连接的事件短语(added / removed / modified in 版本;modified 带 `旧签名 → 新签名`,两侧文本相同时省略箭头段)。**没有注解本身就是信息**:全程存在且从未变化。注解事实唯一来源是 `ABIEvolution` 的 lineage 查表,渲染器不自行推导。 - **主要出现在**:`Sources/SwiftInterface/EvolutionMarking.swift`、`EvolutionAnnotationIndex.swift` -- **延伸阅读**:[提案 draft-swift-evolution-interface-builder](Evolutions/draft-swift-evolution-interface-builder.md) +- **延伸阅读**:[提案 0013](Evolutions/0013-swift-evolution-interface-builder.md) ### materialize(物化) @@ -169,7 +219,7 @@ Swift runtime 的 descriptor 布局惯例:固定头之后按 flags 跟着可 `evolution --interface` 的输出形态:N 个版本所有声明的**并集**只渲染一次的 Swift 接口——每条声明由「最后一个拥有它的版本」的模型与 printer 渲染文本,变化写进生命周期注解,同一成员改签名不裂成多行。与「逐 transition 串联 diff」(同一声明重复出现 N−1 次)和「只渲染最新版 + since 注解」(丢中间版本细节)相对。排序规则:最新版本声明序为脊柱,不在最新版的声明按其最后存在版本的顺序追加。 - **主要出现在**:`Sources/SwiftInterface/SwiftEvolutionInterfaceRenderer.swift`(`matchAcrossVersions`) -- **延伸阅读**:[提案 draft-swift-evolution-interface-builder](Evolutions/draft-swift-evolution-interface-builder.md) +- **延伸阅读**:[提案 0013](Evolutions/0013-swift-evolution-interface-builder.md) ### wrapper vs descriptor(高层包装 vs 描述符) diff --git a/Documentations/Internal/ABIEvolutionDesign.md b/Documentations/Internal/ABIEvolutionDesign.md index d7399004..9623cd96 100644 --- a/Documentations/Internal/ABIEvolutionDesign.md +++ b/Documentations/Internal/ABIEvolutionDesign.md @@ -251,7 +251,7 @@ diff 的 verdict 交叉核对行。 ## 首轮落地后的增量(第五批)—— 并集注解接口(`evolution --interface`) -提案:`Evolutions/draft-swift-evolution-interface-builder.md`。把 N 版本演进渲染成 +提案:`Evolutions/0013-swift-evolution-interface-builder.md`。把 N 版本演进渲染成 **一份带生命周期注解的 Swift 接口**,取代 lineage 清单成为主要人读视图。落点在 `SwiftInterface`(不是本模块——渲染需要活模型与 printer),但设计与本模块强耦合, 维护时会踩的决策记录在这里: diff --git a/Documentations/Internal/ExportedOnlyInterfaceFiltering.md b/Documentations/Internal/ExportedOnlyInterfaceFiltering.md new file mode 100644 index 00000000..e6ee0e57 --- /dev/null +++ b/Documentations/Internal/ExportedOnlyInterfaceFiltering.md @@ -0,0 +1,118 @@ +# Interface 只打印导出声明(`--exported-only`)的实现说明 + +> 配套提案见 [0016](../Evolutions/0016-exported-only-interface.md)。 +> 本文记录**实际落地的实现**、与提案的差异,以及当前覆盖范围与已知降级。面向维护者。 +> 它是 [InterfaceHeaderAndExportStatusAnnotations.md](InterfaceHeaderAndExportStatusAnnotations.md)(提案 0008,`// not exported` 标注)的续篇:标注回答「这个成员导出了吗」,本文的过滤回答「只看导出的东西是什么样」。 + +## 背景与目标 + +`SwiftDeclarationPrintConfiguration.printExportedDeclarationsOnly`(CLI `swift-section interface --exported-only`)打开后, +interface 只输出镜像导出的声明。「导出」始终是 **export trie 的事实**,不是访问级别推断——`internal` / `private` 在二进制里不可恢复 +(roadmap L-16),而 `-enable-testing` 构建的 `internal` 与 `@usableFromInline` 类型确实导出,过滤后它们照样在。 +边界:只做 interface 路径;`dump` 的三个 Dumper 不在范围内。 + +## 关键设计决策 + +**过滤在打印期,不在索引期。** 用户选定。索引出的模型保持完整,RuntimeViewer 浏览、ABI diff / snapshot / evolution +全部不受影响;代价是打印器多了一组判定入口。索引期过滤本可顺带给 diff 一个「只比对导出面」的能力,但要在 +`DefinitionBuilder` 和四个 extension 桶上做更深的手术,且提案 0008 的标注本来就是打印期的事实——两者同层最自然。 + +**绝不靠猜删东西。** 每个判定都是三态:`false` 才删,`true` 与 `nil` 都保留。`nil` 覆盖「镜像没有导出信息」「成员没有 join 上任何符号」 +「重整名不可信」「宿主没装 `ExportFilterScope`」全部情形。这和标注的「绝不靠猜打标」是同一条原则的两面, +也是 `filteredOutputCarriesNoAnnotation` 这个结构性不变量成立的原因:过滤的删除条件**就是**标注的发射条件,两个 flag 同开输出零标注。 + +**类型 / 协议判据:先反查描述符 offset 处的符号,重整名只做兜底。** 提案原定按 `TypeName.node` 重整出 `_$s…Mn` / `…Mp` +查 trie。第一版这么做,fixture 当场暴露一个真实误删:`extension GenericRequirementTest where T: RawRepresentable { public struct RawRepresentableNestedStruct {} }` +——编译器给嵌套在**带约束扩展**里的类型 mangle 的上下文只含扩展自己的 requirement(`…VAASYRzrlE28RawRepresentableNestedStructVMn`), +而模型的名字节点带着类型的完整签名(interface 头部印出 `where A: RawRepresentable, A: ProtocolTest` 两条),重整结果与真实符号不等, +trie 查不到就成了假阴性,一个导出类型被删掉。因此 `exportVerdict(descriptorOffset:nameNode:…)` 分两腿: + +1. `symbolIndexStore.symbols(for: descriptor.offset)` 取**描述符所在位置**的符号(按 `Mn` / `Mp` 后缀挑),拿编译器自己的拼法查导出位图。 + 导出描述符必有 trie 行、未 strip 的镜像对未导出描述符也有本地 symtab 行,所以这一腿几乎总能答。 +2. 只有描述符处没有任何符号(strip 过的镜像里的未导出类型)才用重整名查 trie——trie 在 symtab 被 strip 后依然完整, + 一次 miss 就是真阴性。但**含 `.extension` 上下文的名字拒绝兜底**(返回 `nil` → 保留):那正是第 1 腿存在的理由。 + +**扩展的判据要靠索引器的表,符号推不出「本镜像内」。** 扩展没有自己的描述符符号,能判的只有「被扩展类型 / 遵循协议是不是本镜像内未导出声明」。 +「本镜像内」不能从符号存储推断——strip 过的镜像里一个未导出的类型**一个符号都没有**,用 `typeInfo` / `containsSymbol` 判会把被删私有类型的 +conformance 扩展全部漏下来。所以 `SwiftInterfaceBuilder.printRoot()` 在开关打开时从 `indexer.allTypeDefinitions` / +`allProtocolDefinitions` 算出 `ExportFilterScope`(本镜像内未导出的 `TypeName` / `ProtocolName` 集合,结构哈希,跨 store 也能匹配) +装到打印器上;`ExtensionName` 经 `TypeName(node:kind:)` / `ProtocolName(node:)` 转换后查集合,`conformingProtocolName` 再查一次协议集合。 +不必再查 conformance 描述符 `…Mc`:fixture 上 44 个未导出 `Mc` 全都涉及私有类型或协议,一个 conformance 的导出性就是它两方的导出性。 + +**空扩展:普通扩展删,conformance 扩展留 `{}`。** 成员全被过滤后的 `extension Foo {}` 是噪音;但 `extension Foo: Equatable {}` +本身就是声明——合成的 `==` witness 不可静态调用(0008 故意不豁免 witness),删掉 witness 后剩下的正是 `.swiftinterface` 对合成 conformance 的印法。 +「空」按过滤后的实际内容算(成员、嵌套类型 / 协议、关联类型记录),原本就空的普通扩展桶(如 `where A: ~Copyable {}`)也一并消失。 + +**三个打印入口拆成「过滤壳 + builder 体」。** `@SemanticStringBuilder` 函数里不能 early return,所以 `printTypeDefinition` / +`printProtocolDefinition` / `printExtensionDefinition` 变成普通函数做判定,原体改名为 `printIncluded…`。被过滤的定义返回空 `SemanticString`, +`BlockList` / `NestedDeclaration` 对空项整体跳过,不会留下孤零零的换行——这是为什么输出里没有 `\n\n\n`。 +**事件契约**:被过滤的定义在 start 事件之前就返回,不发任何 print 事件(它没被打印);被**清空**的普通扩展例外——空不空要先 +`index(in:)` 才知道,而索引必须在 start 事件之后(否则失败事件无 start 可配对,见 `printProtocolDefinition` 的注释), +所以它发一对 start / completed 包住空结果。 + +**字段循环先筛后印,保留原始下标。** `renderModelFields` 的字段记录与布局注释都按原始位置取(`fieldRecords[safe:]`、 +`storedFieldComments(forFieldAtIndex:)`),而尾部换行跟着「最后一个实际渲染的字段」走。所以先把 `fields.enumerated()` 过滤成 +`renderedFields`,再用 `offsetEnumerated()` 遍历——`fieldIndex` 取原始下标,`offset.isEnd` 取渲染序列的末尾。枚举 case 没有符号,永远不筛。 + +## 模块结构 + +``` +Sources/SwiftPrinting/ +├── SwiftDeclarationPrintConfiguration.swift # printExportedDeclarationsOnly +├── SwiftDeclarationPrinter+ExportFilter.swift # ExportFilterScope、installExportFilterScope、两级 verdict、全部 isExcludedByExportFilter 判定 +├── SwiftDeclarationPrinter.swift # 三个入口的过滤壳 + printIncluded… 体;成员循环 where 过滤;exportFilterScope 存储 +└── SwiftDeclarationPrinter+Headers.swift # renderModelFields 的字段预筛 +Sources/SwiftInterface/SwiftInterfaceBuilder.swift # printRoot 装 scope;全局块 where 过滤 +Sources/swift-section/Commands/InterfaceCommand.swift # --exported-only +``` + +## 核心算法与数据流 + +`printRoot()` → 开关打开则 `installExportFilterScope(types:protocols:)`(对每个定义跑一次类型级 verdict)→ `printRootContents()`: + +| 对象 | 判定入口 | 依据 | +|------|----------|------| +| 全局变量 / 函数 | `isExcludedByExportFilter(globalSymbolNames:)` | `exportVerdict(forSymbolNames:)`(0008 的派生形态查询) | +| 类型 / 协议(含嵌套、特化子类型) | 入口壳 → `exportVerdict(forTypeDefinition:/forProtocolDefinition:)` | 描述符 offset 处的符号 → 重整名兜底 | +| 扩展 | 入口壳 → `isExcludedByExportFilter(_ extension)`;索引后 `isEmptiedByExportFilter` | `ExportFilterScope` 集合;过滤后内容是否为空 | +| 成员 | `printMembersByOffset` / `ByCategory` 的 `where` | 派生形态查询;`override` / `@objc` 豁免 | +| 存储属性 | `renderModelFields` 预筛 `isExcludedByExportFilter(field:)` | accessor 组的派生形态查询;无 accessor 符号 / `override` / `To` 入口豁免 | + +特化子类型的名字是 bound-generic 节点,verdict 的两腿都落在未绑定描述符上(第 1 腿直接用描述符 offset,第 2 腿剥 bound-generic 壳)。 + +## 与提案的差异 + +- **类型级判据加了 offset 反查腿。** 提案决策日志写的是「按名字查 trie 优于 offset 反查」,理由是 strip 后 offset 反查只能答 `nil`; + 实现保留了这个理由(第 2 腿),但把 offset 反查提到第 1 腿——原因是上文的带约束扩展重整名不等价问题,提案写作时未预见。 +- 其余与提案一致。 + +## 验证 + +- `Tests/SwiftInterfaceTests/ExportedOnlyInterfaceTests.swift`(`SymbolTestsCore`,12 例):私有类型 / 私有协议及其默认实现扩展 / + 私有类型的 conformance 扩展 / 嵌套私有类型(父级与公开兄弟保留)各一例删除;带约束扩展里的公开嵌套类型保留(回归); + 未导出成员删而类型留;未导出全局删;`Tj` 导出、`@objc`、`override` 三类保留;清空的 conformance 扩展留 `{}`; + 双 flag 同开零标注;无空行残留;默认输出与从未听说过该开关的 builder 逐字节相同。每条否定断言都先在默认输出上断言其存在。 +- `Tests/SwiftInterfaceTests/ExportedOnlyLibraryEvolutionFixtureTests.swift`(即时编译,`-enable-library-evolution` 且无 `-enable-testing`,7 例): + `SymbolTestsCore` 因 `ENABLE_TESTABILITY` 导出全部 `internal`,所以 `internal` 形态在这里钉:类型(顶层 + 嵌套)、协议及其默认实现、 + 公开类型对内部协议的 conformance 扩展、方法、存储属性(accessor 以本地符号 join 上)、全局;被清空的普通扩展容器整块删、有公开成员的容器保留;枚举 case 保留。 +- `Tests/SwiftSectionCommandTests/ExportedOnlyFlagTests.swift`:flag 解析、默认关、`dump` 无此 flag。 +- 落地前的人工交叉验证(fixture 全量输出):默认 3824 行 → 过滤后 2839 行;标注模式下 378 处 `// not exported` 对应的声明在过滤输出里**零残留**; + 过滤输出相对默认输出的新增行只有 conformance 扩展的 `{` → `{}` 改写;无 `\n\n\n`、无 `{\n}`。 +- 默认路径零行为改动,不构成 AGENTS.md 意义上的 large refactor,未跑渲染 A/B 脚本;由既有 interface 快照与 `defaultOutputIsUnchanged` 钉住。 + +## 已知降级 + +- **引用不改写。** 过滤按声明进行,导出成员的签名里仍可能引用被删的类型(fixture:`typealias Body = Structs.PrivateProtocolTest`)。 + 这与真实二进制里 `some P` 解析到私有类型的情形同构,属于「输出忠实于二进制」的一面。 +- **`--show-c-imported-types` 下 C 导入类型会被过滤。** `__C.CMTime` 之类外来描述符的 `Mn` 是本地符号(它们不是本镜像导出的),判定为 `false`。 +- **绕过 `printRoot` 的宿主要自己装 scope。** RuntimeViewer 的 per-type 导出若不调 `installExportFilterScope`,类型 / 协议 / 成员照常过滤, + 只有扩展这一腿退化为「全部保留」——fail-open,静默。 +- **镜像没有导出信息时什么都不过滤**(三态 `nil`),静默。目前没有能构造这种镜像的 fixture(与 0008 同一个缺口)。 +- **被清空的普通扩展仍会派发一对 print 事件**(见上文事件契约),事件消费者会看到一个 start / completed 之间零输出的扩展。 + +## 延伸阅读 + +- 配套提案:[0016](../Evolutions/0016-exported-only-interface.md) +- 前篇:[InterfaceHeaderAndExportStatusAnnotations.md](InterfaceHeaderAndExportStatusAnnotations.md)(导出事实层、派生符号形态、两个豁免的来历) +- 模块参考:[Modules/SwiftInterface.md](Modules/SwiftInterface.md)(`printRoot()` 的段落与 catch 契约) +- 术语:[Glossary.md](../Glossary.md) 的「export status」与「exported-only 过滤」 diff --git a/Documentations/Internal/FixtureTestingAndContinuousIntegration.md b/Documentations/Internal/FixtureTestingAndContinuousIntegration.md new file mode 100644 index 00000000..4e169d8c --- /dev/null +++ b/Documentations/Internal/FixtureTestingAndContinuousIntegration.md @@ -0,0 +1,160 @@ +# Fixture 测试体系与 CI 的设计来历 + +> 本文整合自 2026-03 → 2026-05 的四份设计 spec 与 CI 落地记录(原存于已删除的 +> `docs/superpowers/` 目录,完整原文见 git 历史)。**现行操作规程以 +> [AGENTS.md](../../AGENTS.md) 的「Fixture-Based Test Coverage (MachOSwiftSection)」与 +> 「Build and Test Commands」章节为准**;本文记录的是设计动机、机制原理与踩坑史—— +> 代码和规程里看不出来的那部分。 + +整个体系分三层,按落地时间排列: + +1. **快照层**(2026-03):`SwiftDumpTests` / `SwiftInterfaceTests` 对 fixture 输出做逐字节快照; +2. **集成/E2E 层**(2026-04):对解析出的 `TypeDefinition` 模型与 `SwiftInterfaceBuilder` 输出做结构化断言; +3. **ABI 覆盖层**(2026-05):`MachOSwiftSectionTests/Fixtures/` 对 `Models/` 每个 public 方法做跨 reader 一致性 + baseline 字面量断言,配覆盖不变量守护。 + +三层共用同一个 fixture:`Tests/Projects/SymbolTests` 编出的 `SymbolTestsCore.framework`。 + +## 为什么 fixture 只用 SymbolTestsCore + +CI 上可复现性是唯一判据: + +| 来源 | CI 可复现? | 漂移维度 | +|---|---|---| +| 系统 dyld cache | 否——随每个 macOS 补丁变 | macOS 版本、cache 布局、框架更新 | +| Xcode 自带框架 | 否——随每个 Xcode 发布变 | Xcode / swiftlang 版本 | +| `SymbolTestsCore`(检入源码编译) | **是**——pinned 源码 + pinned Xcode | 仅 pinned 工具链版本 | + +坍缩到单一确定性来源后:快照直接进 git、无外部 fixtures 包、无自动重录 workflow,快照 diff 就是「本库对某 Swift 构造发出的元数据变了」的直接信号。系统 cache / Xcode 框架的旧快照套件在快照层落地时删除,那些二进制仅供本地手动探查。 + +## 路径锚定——fixture 二进制怎么被找到(易踩坑) + +`MachOFileName.SymbolTestsCore` 存的是相对路径 +`../../Tests/Projects/SymbolTests/DerivedData/...`,**不是**对 CWD 解析,而是对 +`Sources/MachOTestingSupport/Extensions.swift` 的 `#filePath` 解析——`../../` 爬回仓库根再下行。 +而构建端 `xcodebuild -derivedDataPath Tests/Projects/SymbolTests/DerivedData` 是对 +`xcodebuild` 自己的 CWD 解析。两者对齐**当且仅当**构建从仓库根发起——任何从子目录跑 +`xcodebuild` 的 CI 步骤都会悄悄打破对齐。CI 还遇到过第三种形态:runner 上 `xcodebuild` +把产物放在 `DerivedData/Build/Products/...`(本地是 `DerivedData/SymbolTests/Build/Products/...`), +workflow 里的 "Normalize SymbolTestsCore fixture path" 步骤用符号链接补齐。 + +本地一键构建入口是 `Scripts/build-test-fixtures.sh`。 + +## Fixture 源码约定 + +- 每个 `.swift` 文件 = 一个语言/ABI 特性类目,以 `public enum <文件名> { … }` 做命名空间, + 内部全 `public`(保证 descriptor 落进二进制)。快照测试按「根命名空间 == 文件名」过滤归属。 +- 项目用 `PBXFileSystemSynchronizedRootGroup`——新文件放进目录即入编译,无需改 `project.pbxproj`。 +- 偏差与边界(约定的例外要么改写成约定、要么在此登记): + - `AsyncSequence.swift` / `Codable.swift` / `StringInterpolation.swift` 的 enum 名与文件名不同 + (`AsyncSequenceTests` / `CodableTests` / `StringInterpolations`),为避开 stdlib 同名类型; + - `GlobalDeclarations.swift` 只有全局声明、不发 TypeContextDescriptor,per-category dump 有意为空, + 覆盖靠全模块 interface 快照; + - `NeverExtensions.swift` 全是 `extension Never`,descriptor 归属 `Swift.Never`, + 用显式 Never 谓词而非命名空间归属——唯一的非命名空间归属规则。 +- ProtocolConformance 按**遵循方类型**的根命名空间归属(与浏览 dump 输出的习惯一致)。 +- 2026-04-13 的扩展批把 fixture 从 18 个文件扩到 54+,分三类:通用语言特性(KeyPaths、 + Codable 合成、property observers …)、扩展特性(protocol composition、class-bound generics、 + marker protocols …)、以及**专为二进制元数据形态设计**的一批——后者与目标 section 的对应关系 + 是选 fixture 样本时的检索表: + +| 文件 | 目标 section / descriptor | +|---|---| +| `FieldDescriptorVariants.swift` | `__swift5_fieldmd` 字段形态全集 | +| `GenericRequirementVariants.swift` | `TargetGenericRequirementDescriptor` 全 requirement kind(含 `~Copyable`/`~Escapable`) | +| `VTableEntryVariants.swift` | class `VTableDescriptorHeader` 各 entry flag | +| `ConditionalConformanceVariants.swift` | `__swift5_proto` 条件 requirement 表 | +| `DefaultImplementationVariants.swift` | `__swift5_protos` 默认实现扩展(含 `where Self:` 约束) | +| `FrozenResilienceContrast.swift` | 同布局 `@frozen` vs resilient 的 descriptor 形态对照 | +| `AssociatedTypeWitnessPatterns.swift` | `__swift5_assocty` 五种 witness 模式 | +| `BuiltinTypeFields.swift` | builtin 类型字段 | + +后续 2026-05-05 批又为 sentinel 消化加了 default-override table、resilient class、 +ObjC class wrapper、canonical specialized metadata、foreign types、value generics 等形态 +(见下文 ABI 覆盖层)。 + +## 集成/E2E 层(2026-04-10) + +两层分工:`SymbolTestsCoreIntegrationTests` 加载二进制后在 **`TypeDefinition` 模型层**断言 +(类型/字段/conformance/override/嵌套/关联类型/属性推断/vtable 与 PWT 排序); +`SymbolTestsCoreE2ETests` 走 `SwiftInterfaceBuilder` 在**输出字符串层**断言 +(`@propertyWrapper` 等 attribute 出现、vtable offset 注释升序、`@retroactive` / `override` 关键字在场)。 +同批为 attribute 推断加了专用 fixture 类型(`PropertyWrapperStruct` / `ResultBuilderStruct` / +`DynamicMemberLookupStruct` / `DynamicCallableStruct` / `ObjCAttributeClass`)。 + +## ABI 覆盖层(2026-05-03 设计 + 2026-05-05 收紧) + +### 四支柱架构 + +``` +fixture.framework (SymbolTestsCore) + ├─[disk]──── MachOFile ──┐ + ├─[dlopen]── MachOImage ─┼─→ 3 个 ReadingContext ─→ Fixtures/ 各 Suite + └─[ptr]───── InProcess ──┘ │ + ├─→ ① 跨 reader 一致性 #expect + └─→ ② ABI baseline 字面量 #expect ←─ baseline-generator 生成 + MachOSwiftSectionCoverageInvariantTests 守护(源码静态扫描 vs 注册名单) +``` + +关键设计决策: + +- **generator 只走 MachOFile 单一路径生成 baseline**(单路径易审计);MachOImage / InProcess 的 + 正确性由跨 reader 一致性独立验证,不依赖 baseline——两道防线互为兜底(三家同错一个 bug 时 + baseline 字面量兜住;generator 自身出错时一致性断言兜住)。 +- **数值进制约定**:offset / size / flags 用 hex(便于和反汇编工具对照),count / index 用十进制。 +- **重载合并**:同名方法的 `(in: MachO)` / `(in: Context)` / `()` InProcess 三家重载共享一个 + `MethodKey`,在单个 `@Test` 内一并验证;覆盖守护按 `(typeName, memberName)` 比对不区分重载。 +- baseline 头部记录 toolchain 版本 + 生成日期;`--suite ` 支持局部重生成以缩小 review 面。 + +### 信任危机与 sentinel 收紧(2026-05-05) + +原始落地被 review 发现**系统性失真**:157 个 suite 里 88 个(56%)从不调用 +`acrossAllReaders`/`acrossAllContexts`,687 个声明覆盖的方法里 277 个(40%)只是挂在 +baseline 字符串集合里的 sentinel——`registeredTestMethodNames` 是手工名单, +「registered == expected」的不变量对这些 suite 是空挡。原设计要求「没有合适样本就进 +allowlist 并填 reason」,实施时被偷换成永远通过的 sentinel 测试,绕开了 reason 强制。 + +修复沿三条路径(机制细节现录于 AGENTS.md): + +- **A — sentinel 一等公民化**:`SentinelReason` 类型化三档(`runtimeOnly` / `needsFixtureExtension` + / `pureDataUtility`),`SuiteBehaviorScanner` 用 SwiftSyntax 按 `@Test` 函数体内的调用痕迹判定 + 实际行为,新增两段不变量——**liarSentinel**(标了 sentinel 但实际在真测 → 标签过期)与 + **unmarkedSentinel**(行为是 sentinel 但没登记 → 堵住 silent sentinel)。行为事实最难撒谎, + 故选全自动扫描而非手工 marker。 +- **B — 扩 fixture 消化 `needsFixtureExtension`**:每种缺失的 metadata 形态一个新 fixture 文件 + 一个 commit(重编 fixture 会引发整片 baseline 漂移,故批间先做 baseline 对齐 commit)。 +- **C — runtime-only 转 InProcess 真测**:运行时现场分配的 metadata(metatype / tuple / function / + existential …)在其他 reader 上拿不到数据,强求跨 reader 是另一种 sentinel——所以走 + `usingInProcessOnly` 单 reader + baseline 字面量。样本来源分流:stdlib 类型直接 + `unsafeBitCast(T.self, …)`,fixture nominal 类型取其 metadata 指针,header/bounds 类从既有 + metadata 指针偏移读取。无法稳定构造的(`swift_allocBox` 产物等)保留 `runtimeOnly` 永久 sentinel。 + +## CI(2026-04-18 设计 + 落地反馈) + +- **白名单而非黑名单**:CI 只跑不依赖开发机环境(Xcode 框架、模拟器 runtime、系统 dyld cache、 + 运行时加载镜像)的 fixture 套件。白名单正面表达意图(「CI 只跑可复现的 fixture 测试」), + 黑名单会让新测试被默默包含。代价是新增可上 CI 的套件要记得进名单。 +- **过滤器是一条锚定 regex**:`\.(SuiteA|SuiteB|…)(/|$)`——前导 `\.` 锚住模块前缀,尾部 `(/|$)` + 是词边界,防 `STCoreTests` 匹配进 `STCoreE2ETests` 这类子串陷阱(多个 `--filter` 分开传没有 + 这层保护)。当前名单与 runner/Xcode 版本以 [`.github/workflows/macOS.yml`](../../.github/workflows/macOS.yml) + 为活权威(名单在过滤层落地后已多次扩充)。 +- **首轮 CI 踩过的五个坑**(都已修进 workflow,重配 CI 时先对照这张表): + +| 坑 | 修法 | +|---|---| +| `xcodebuild` 因缺开发者证书失败 | `CODE_SIGNING_ALLOWED=NO` | +| `generic/platform=macOS` 产 universal slice,x86_64 `.swiftinterface` 验证失败(项目 ARM-only) | `ARCHS=arm64` | +| Xcode 26.2(Swift 6.2.3)发出的 `.swiftinterface` 含 `nonisolated(nonsending)` 后自己拒绝验证(编译器 bug) | 跳到 Xcode 26.4(Swift 6.3 修复)——这就是当年 pin 26.4 而非需求档案里 26.2 的原因 | +| 远端依赖 pin 版本缺新 API | 升 `Package.swift` pin | +| runner 上 fixture 产物路径与本地布局不同 | "Normalize SymbolTestsCore fixture path" 符号链接步骤 | + +- **无自动重录 workflow**:快照漂移永远由开发者本地 `SNAPSHOT_TESTING_RECORD=all` 重录、 + 人工 review 后与触发变更同 PR 提交。CI 的 `xcode-version` 永远显式 pin,不用 `latest-stable`—— + 任何 bump 都应是有意的、可 review 的变更(工具链升级可能合法地改变发出的元数据与 section 顺序, + 后者由 linker 决定,同工具链内稳定)。 + +## 相关文档 + +- [AGENTS.md](../../AGENTS.md) —— 覆盖体系的现行操作规程(invariants、regen-baselines、环境漂移排查) +- [Reviews/2026-05-06-generic-specializer-bug-review.md](Reviews/2026-05-06-generic-specializer-bug-review.md) —— 同期 GenericSpecializer 审查(复现测试纪律的样板) +- [ProjectEvolutionLog.md](ProjectEvolutionLog.md) §6–§7 —— 这两个工作弧在演进账本里的条目 +- 原始四份 spec 与逐步执行 plan 的完整原文:git 历史中的 `docs/superpowers/`(2026-09-01 移除) diff --git a/Documentations/Internal/LargeStackTaskExecutorAdoption.md b/Documentations/Internal/LargeStackTaskExecutorAdoption.md new file mode 100644 index 00000000..42b302f5 --- /dev/null +++ b/Documentations/Internal/LargeStackTaskExecutorAdoption.md @@ -0,0 +1,123 @@ +# 大栈任务执行器接入与跨版本并行 + +提案:[0019-large-stack-executor-and-cross-version-parallelism](../Evolutions/0019-large-stack-executor-and-cross-version-parallelism.md)。上游执行器本体:swift-demangling 提案 0014(`Documentations/StackSafety.md` 第八节)。本文记录落地后的形态、那些从签名上看不出来的决策,以及计时数据。 + +## 改了什么 + +| 之前 | 之后 | +|---|---| +| 打印路径每次 `printSemantic` / demangle / remangle 都由 `StackSafeExecutor` 探测调用线程剩余栈;协作线程 512 KB 永远不过,每次调用跳到 8 MB 池线程再用信号量停住(release 实测每次 8–21 µs) | 库的 async 入口用 `LargeStackTaskExecution.run` 把整段 task 放到 swift-demangling 0.6.3 的 16 MB `LargeStackTaskExecutor` 上;探针在每个入口都通过,全程原地执行,零跳转 | +| swift-demangling pin `0.6.0 ..< 0.7.0`(实际锁在 0.6.0,因为 0.6.1 的 QoS 改动慢 3–4 倍) | pin `0.6.3 ..< 0.7.0`,跳过 0.6.1 / 0.6.2 | +| `AnySwiftEvolutionInterfaceBuilder.prepare()` 逐版本串行 `await`;`DiffCommand` 先 old 后 new;`EvolutionCommand` 的 lineage 输入逐个加载 | `prepare(maximumConcurrentPreparations:)`(默认核数、`1` 即旧顺序);`diff` / `evolution` 的输入按窗口并行索引,CLI 新增 `--jobs N` | +| 无通用的「限并发 map」 | `Utilities` 的 `Collection.concurrentMap(maximumConcurrency:_:)`:窗口化 task group,结果按源序,首错重抛 | +| `Node+.swift` 的注释引用上游已不存在的 `executeWithUncheckedSendability` | 改为如实描述 `execute` 与执行器路径的关系 | + +输出不变:渲染 A/B 逐字节一致(见「实测数据」)。 + +## 从签名看不出来的决策 + +### 执行器为什么不用改 demangler 的任何调用点 + +`StackSafeExecutor` 的探针(`currentThreadHasSufficientStack`)用 `pthread_get_stackaddr_np` / `pthread_get_stacksize_np` 算调用线程剩余栈是否 ≥ 2 MB,看的是**栈**不是**线程身份**。所以只要 task 跑在一条 16 MB 线程上,`execute` / `executeAsync` 的探针在每一层都通过、直接内联——同步被调方(`printSemantic`、`demangleAsNodeTransient`、remangle)一并受益,一个调用点都不用碰。这也是为什么本库这边的改动只有「包一层」:`withTaskExecutorPreference(StackSafeExecutor.taskExecutor) { body }`。 + +### 为什么在库入口自装,而不是让宿主装 + +宿主漏装一处就回到逐次跳转,而且每个宿主(RuntimeViewer、MachOKitUI、SymbolViewer、CLI)都得改。库入口自装后宿主零改动;想自己管执行器的宿主置 `LargeStackTaskExecution.isEnabled = false`。这是提案第二轮澄清时用户的选择。 + +### 包了哪些入口 + +| 模块 | 入口 | +|---|---| +| `SwiftIndexing` | `SwiftDeclarationIndexer.prepare()`(`updateConfiguration(_:)` 经它) | +| `SwiftInterface` | `SwiftInterfaceBuilder.prepare()` / `printRoot()`;`SwiftDiffableInterfaceBuilder.prepare()`;`AnySwiftEvolutionInterfaceBuilder.prepare(maximumConcurrentPreparations:)` / `printAnnotatedInterface()` / `annotatedBlocks()`(pack façade 委托);`SwiftDiffableInterfaceRenderer.printAnnotatedInterface(format:)` / `annotatedDiffBlocks()` | +| `SwiftPrinting` | `SwiftDeclarationPrinter.printTypeDefinition` / `printProtocolDefinition` / `printExtensionDefinition` / `printDefinition`(RuntimeViewer 逐类型导出绕过 `printRoot` 的路径) | +| `SwiftDump` | 六个 `Dumpable.dump(using:in:)` 遵循者(`Struct` / `Class` / `Enum` / `Protocol` / `ProtocolConformance` / `AssociatedType`) | + +**嵌套免费**:`printRoot` 里再进 `printTypeDefinition`、父类型的嵌套子类型循环再进 `printTypeDefinition`,都是「已在执行器上再包一层」。SE-0417 的 `withTaskExecutorPreference` 在当前执行器就是目标执行器时不切换,`nestedRunsStayOnTheSameThread` 钉住线程不变。CLI 的 `dump` 循环按类型逐个调 `dump`,每个类型进出执行器各一跳,SwiftUICore 五千余类型约一万跳、总计零点几秒,远小于原来每个符号一跳,故 CLI 侧没有再包一层。 + +**壳 + 体的拆分**:`printExtensionDefinition` 与 `printDefinition` 原本函数体就是整段逻辑(后者还是 `@SemanticStringBuilder`),result builder 的函数体不能直接套一个 `run { }` 闭包再 return,所以拆成非 builder 的壳(过滤 + `run`)与 builder 的体(`printExtensionDefinitionContents` / `printDefinitionContents`),与提案 0016 拆 `printIncluded…` 的手法相同。注意 `printIncludedExtensionDefinition` 这个名字已被 0016 的 builder 体占用,新壳体用了 `…Contents` 后缀。 + +### 非结构化 `Task {}` 的核对结果 + +SE-0417:非结构化 `Task {}` 不继承执行器偏好。核对 `Sources/` 全部:库内**零处**非结构化 `Task`(唯一的 `withTaskGroup` 在 `TypeIndexing.TypeDatabase`,是结构化子任务,继承偏好)。`childTasksInheritTheExecutor` 钉住子任务继承这一前提,跨版本并行靠它。以后若在包裹的入口内起 `Task {}`,必须显式传 `executorPreference:`。 + +### 静默回退、`isEnabled` 与环境变量 + +`run` 里 `#available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)` 不满足、非 Darwin、或 `isEnabled == false`,都直接 `try await body()`——与接入前完全一致的行为(跳转照付)。`isEnabled` 的初值读环境变量 `MACHO_SWIFT_SECTION_LARGE_STACK_EXECUTOR`(等于 `"0"` 即关),用途是让渲染 A/B 与计时能用**同一个二进制**比较开 / 关两种状态,宿主不必为此重编。它是提案未列出的一个小补充。 + +### 主 actor 调用方 + +本包的库 target 没有开启 SE-0461 `NonisolatedNonsendingByDefault`(`Package.swift` 里定义了这些 upcoming feature 常量,但只有测试 target 接 `testSettings`,且它几乎为空),所以库的 async 入口是经典 `nonisolated`:从 `@MainActor` 调用会离开主 actor。接入前它落在协作线程(512 KB,逐次跳转),接入后落在执行器线程——对 RuntimeViewer 这类从主线程发起导出的宿主是纯收益。若将来开启 SE-0461,主 actor 调用会留在主线程(主 actor 有自己的 executor,偏好不生效),主线程栈 8 MB 探针本就通过,也无损失;从默认 actor 调用则仍在执行器上(SE-0417:默认 actor 继承偏好)。 + +### 跨版本并行为什么安全、上限为什么取核数 + +各版本是不同文件:`MachOFile.identifier` 按 LC_UUID 键控,五个 `SharedCache` 单例全部按此分片;描述符读取走 `MemoryMappedFile`;demangler 池按核数扩。2026-09-02 已用三个进程并行证明可行,进程内并行只多了 `SharedCache` 字典锁与 `PerImageCacheEvictionRegistry` 的 `NSLock`,都是短临界区。窗口上限取核数的原因来自上游契约:一个 `prepare` 是整段 task,占住执行器的一条线程直到结束;执行器每个 QoS 类的稳态额度是 `max(2, 核数)`,窗口超过它只会排队,不会更快。`parallelPreparationMatchesSerialPreparation` 钉住并行与串行的接口、结构流、evolution JSON 逐字节一致。 + +### 版本内并行为什么不做 + +MachOKit 自己的读取有上百处 `fileHandle.seek` + `read` 共用一个句柄(`MachOFile.swift`、`DyldCache.swift` 等),两个线程交错就读错位置;`index(in:)` 的 `guard !isIndexed` 是非原子的检查加赋值。前者不在本仓库,需要 MachOKit 改 mmap 读或每个分片独立 `MachOFile` 实例,另起提案。 + +### `concurrentMap(maximumConcurrency:)` 的错误语义 + +窗口化 `withThrowingTaskGroup`:先提交 `window` 个,每完成一个再提交一个,结果按源序落位。首个错误经 `group.next()` 抛出,task group 在作用域退出时取消并等待在飞的子任务(`prepare` 不检查取消,所以在飞的会跑完,结果丢弃),**尚未启动的元素永远不启动**(`theFirstFailureIsRethrownAndPendingElementsNeverStart`)。哪个错误先到是调度决定的——串行时固定是最旧版本的错误,并行时不一定;只影响错误报文,不影响成功路径。 + +### 取消语义 + +`concurrentMap` 用 `addTaskUnlessCancelled` 提交:调用方的 task 被取消后,尚未启动的元素永远不启动,调用抛 `CancellationError`,绝不返回残缺数组(第一版用 `addTask`,取消后的 group 照收子任务,一次被取消的多版本准备会把剩余版本全部索引到底;审查者独立编译复现了这一点)。在飞的 transform 会跑完——索引链路本身不检查取消——结果丢弃。取消发生在最后一个元素提交之后则什么也不改变,结果照常返回。`TypeIndexing.TypeDatabase.index` 的 task group 是同形的旧代码,缺注入缝写不出复现测试,延后(裁决 A33)。 + +### 事件投递串行化 + +`SwiftIndexEvents.Handler` 没有 `Sendable` 约束,而并行准备把宿主传进来的同一个 handler 实例塞给 N 个版本的 dispatcher、在 N 个任务上调用。`Dispatcher.dispatch` 现在用一把**进程级** `NSRecursiveLock` 包住 handler 调用:有状态的宿主 handler 保持单线程,console handler 的一行不会被截断,代价是每个事件一次无竞争锁;递归锁让 handler 里再 dispatch 不死锁。跨 dispatcher 的投递**顺序**仍由谁先到决定。这是 F2 三个选项(加 `Sendable` 约束是破坏性 API 变更、只写文档等于把风险留给宿主)里用户选的。 + +### stderr 归属 + +`ConsoleEventHandler(label:)` 在时间戳后加 `[label]`:`diff` 用 `old` / `new`,`evolution` 每个版本用它的 axis label(通过新 init 参数 `eventHandlersPerVersion(versionIndex, label)`,共享 handler 照旧),snapshot 输入用 provenance label 或文件名。只改 stderr,不碰产品输出。 + +### 环境变量的接受值 + +`MACHO_SWIFT_SECTION_LARGE_STACK_EXECUTOR` 的 `0` / `false` / `no` / `off`(大小写与首尾空白不敏感)为关,其余(含未设置)为开——第一版只比对字面量 `"0"`,`=false` 会把执行器测两遍然后得出「执行器不要钱」的错误结论。`LargeStackTaskExecutionTests` 的执行器线程断言同时挡 `isSupported` 与 `isEnabled`,在该变量关闭的环境下跳过而不是假红。 + +### `--jobs` 在 CLI 校验而库端 clamp + +命令行上的 `--jobs 0` 是笔误,`ValidationError("--jobs must be at least 1.")` 立刻报;库 API 的 `maximumConcurrentPreparations` 小于 1 则按 1 处理(`preparationWindowIsClampedNotValidated`),因为宿主可能直接把「核数 - 1」之类的算式传进来,为一个下界抛错不值得。 + +## 实测数据 + +release 二进制,宿主 dyld cache(macOS 26.5.2,10 核 Apple Silicon),每项跑两次取两次的值;四个配置的输出逐字节一致(`cmp`)。 + +**单版本 `dump` / `interface`(`--uses-system-dyld-shared-cache -n `,秒)** + +| 配置 | SwiftUICore dump | SwiftUICore interface | SwiftUI dump | SwiftUI interface | +|---|---|---|---|---| +| swift-demangling 0.6.0(ABI 分支,接入前) | 48.8 / 48.6 | 56.4 / 57.0 | 79.4 / 79.9 | 87.5 / 89.4 | +| 0.6.3 仅抬 pin(接入前) | 48.5 / 48.4 | 55.8 / 56.2 | 77.2 / 79.7 | 89.3 / 89.6 | +| 0.6.3 + 执行器**关**(`MACHO_SWIFT_SECTION_LARGE_STACK_EXECUTOR=0`) | 48.6 / 48.7 | 56.4 / 55.7 | 78.0 / 79.7 | 88.4 / 89.7 | +| 0.6.3 + 执行器**开**(默认) | **40.6 / 40.4** | **47.0 / 47.1** | **61.3 / 60.2** | **71.2 / 70.9** | + +执行器带来 16–23% 的墙钟缩短(SwiftUICore −17% / −16%,SwiftUI −23% / −20%);0.6.0 → 0.6.3 本身无差别(0.6.1 的回归已在 0.6.2 修掉)。剩下的时间是索引与打印本身——跳转只是每次调用的固定开销,它在 SwiftUI 这种符号更多的镜像上占比更大。 + +**三版本 `evolution`(SwiftUI,归档 cache 15.5 / 26.5.2 / 27.0-beta.6,秒)** + +| 配置 | `--interface` | lineage 报告 | +|---|---|---| +| 执行器关 + `--jobs 1`(接入前的形态) | 306.7 | 282.4 | +| 执行器开 + `--jobs 1` | 242.6 | 233.8 | +| 执行器开 + 默认并行(3 个版本同时) | **151.9** | **139.4** | + +并行本身 1.6–1.7×(三版本不等长,最慢的那个定墙钟),叠加执行器 2.0×;`--jobs 1` 与默认的输出逐字节一致。 + +**渲染 A/B**(`Scripts/run-rendering-ab-verification.py`,基线 = ABI 分支 `feature/self-contained-abi-layer`,候选 = 本分支;基线 swift-demangling 0.6.0、候选 0.6.3,其余 pin 一致):执行器**开**与**关**(`MACHO_SWIFT_SECTION_LARGE_STACK_EXECUTOR=0`)各跑一轮,**两轮均 78 对输出逐字节一致,0 差异**。覆盖当前系统 dyld cache(归档 cache 目录名与脚本期望不符,按文档回退到系统 cache)、模拟器运行时 iOS 15.5 / 18.5 / 18.6 / 26.5、进程内 MachOImage 三条路径的 dump 与 interface。脚本自己记录的墙钟也印证了收益:候选侧 SwiftUI dump 60 s / interface 71 s 对基线 80 s / 88 s,关掉执行器后候选与基线持平(48 s vs 48 s)。 + +## 测试锚点 + +- `LargeStackTaskExecutionTests`(MachOSymbolsTests):执行器线程栈 ≥ 16 MB 且线程名前缀 `swift-demangling.task-executor.`;`execute` / `executeAsync` 在体内不跳线程;嵌套 `run` 不换线程;子任务继承;`isEnabled = false` 时留在调用线程;值与错误透传。 +- `BoundedConcurrentMapTests`(SwiftInterfaceTests):源序、窗口不超、窗口 1 严格串行、窗口内真并发(rendezvous,超时即失败)、首错语义、空输入。 +- `SwiftEvolutionInterfaceBuilderTests.parallelPreparationMatchesSerialPreparation` / `preparationWindowIsClampedNotValidated`。 +- `DiffCommandValidationTests` / `EvolutionCommandValidationTests`:`--jobs` 解析与下界校验。 + +## 已知限制 + +- 执行器只在 macOS 15 / iOS 18 起可用;以下系统行为不变。 +- `KnownIssues.md` #4(上游)在执行器路径上对打印器与 remangler 关闭,`TypeDecoder`(需约 30 MB)仍会先爆栈——它本就不经 `StackSafeExecutor`。 +- 事件 handler 的 stderr 输出在并行 `prepare` 时会交错。 +- 并行 `prepare` 期间同时常驻的索引器数等于窗口大小,内存峰值随之上升(`evolution --interface` 路径本来就全部常驻,lineage / JSON 路径从「逐版本释放」变为「窗口内同时常驻」)。 diff --git a/Documentations/Internal/Modules/MachODependencies.md b/Documentations/Internal/Modules/MachODependencies.md new file mode 100644 index 00000000..4156b69b --- /dev/null +++ b/Documentations/Internal/Modules/MachODependencies.md @@ -0,0 +1,94 @@ +# MachODependencies 模块 + +> 模块参考文档(module reference),随代码维护。读者:维护者。 +> 提案:[0017-macho-dependencies-module](../../Evolutions/0017-macho-dependencies-module.md)。 + +## 模块定位 + +MachODependencies 回答一个问题:**一个二进制链接了哪些镜像,去哪里把它们找出来。** 它只读 `LC_LOAD_DYLIB` 家族的 load command,不碰 Swift metadata,因此和 `MachOCaches` / `MachOReading` 一样位于 MachO* 基础层,由 `MachOFoundation` 统一 re-export——凡是 `import MachOSwiftSection` 的模块都直接可用,不必再加 import。 + +它取代了两套各自为政的实现:`SwiftLayout` 里文件私有的传递闭包(BFS + bare name 去重 + cache 一次性索引),和 `SwiftInterface` 里绑定在 `SwiftInterfaceBuilderDependencies` 上的一层直接依赖加载(按 install path 精确匹配)。两处现在都是薄包装,各自的语义保持不变:静态布局要传递闭包,`__C` 类型归属只要直接依赖。 + +下游消费者:`SwiftLayout.ImageUniverse`(三个 `dependencyClosure` 工厂)、`SwiftInterface.SwiftInterfaceBuilderDependencies`(供 TypeIndexing)、`swift-section interface --resolve-c-module-names`。 + +## 文件 → 子系统对照 + +| 子系统 | 文件 | +|---|---| +| 1. 搜索路径与失败记录 | `DependencySearchPath`(含 `DependencySearchPathError` / `DependencySearchPathLoadFailure`) | +| 2. load name 归一 | `DependencyLoadName` | +| 3. 定位器 | `DependencyLocating`(协议 + `InProcessDependencyLocator`)、`FileDependencyLocator` | +| 4. 闭包遍历 | `DependencyClosure`(含 `DependencyTraversal`) | + +## 1. 搜索路径 + +`DependencySearchPath` 三种:显式 Mach-O 文件、显式 dyld shared cache 文件、宿主系统的 cache。**`@rpath` / `@loader_path` / `@executable_path` 不展开**——一个不在 cache 里的依赖(sibling framework、测试 helper)必须由调用方以 `.machOFile(path:)` 显式给出。这是从 SwiftLayout 阶段 3 继承的 MVP 边界,未变。 + +打不开的搜索路径**不抛错**,记进 `DependencySearchPathLoadFailure`(附原始 error;系统 cache 不可用时是 `systemDyldSharedCacheUnavailable`)。理由有二:一条坏路径不该让整个解析失败;本模块在事件层(`SwiftIndexEvents`)之下,无法派发事件,只能把失败当数据回传,由上层决定落点——`SwiftInterfaceBuilderDependencies` 把它们派发为 `renderingDegraded(.dependencyLoad)` 事件,CLI 经 `ConsoleEventHandler` 落到 stderr。 + +## 2. load name 归一(`DependencyLoadName.bareImageName(of:)`) + +load name → bare image name:取末段路径、去**第一个**扩展名(`libobjc.A.dylib` → `libobjc`,`libc++.1.dylib` → `libc++`)。这条规则是**与 MachOKit 的契约**:`MachOImage(name:)` 对进程内每个镜像的路径做同样的归约再比较。把未归一的 load name(dyld 报告的都是绝对路径)直接喂给它永远匹配不到——`SwiftInterfaceBuilderDependencies` 的 `MachOImage` 版初始化器就是这么写的,从诞生起解析结果一直为空,仓库内无人调用所以没被发现(`DependencyLoadNameTests.bareImageNameIsWhatMachOImageLookupMatches` 与 `SwiftInterfaceBuilderDependenciesTests.imageInitializerResolvesTheMappedDirectDependencies` 锁定)。 + +bare name 同时是所有依赖集合的**去重键**:同一个库会被不同镜像以不同拼写链接(sibling 用 `@rpath/…`,系统框架用绝对路径),只有 bare name 跨拼写稳定。 + +## 3. 定位器 + +`DependencyLocating` 只有一个方法 `locate(loadName:)`,收到的是 load command 里的**原始拼写**,归一由实现自己做。这让遍历与「镜像从哪来」解耦:进程内、磁盘搜索路径、测试里手搭的表,都是一个实现。 + +- **`InProcessDependencyLocator`**:归一后走 `MachOImage(name:)`。系统框架天然从 cache 解析;`@rpath` 依赖只要已映射进进程也能解析;弱链接但未映射的(如 `libswiftCoreAudio`)解析不到,进 `unresolvedLoadNames`。 +- **`FileDependencyLocator`**:两步查找,顺序固定: + 1. **install path 精确匹配**——系统框架的 load name 就是 cache 镜像的 `imagePath`,命中即是编译器自己的答案。显式文件同时以「传入的磁盘路径」和「文件的 install name(`LC_ID_DYLIB`,通常 `@rpath/…`)」两种拼写登记,因为 `MachOFile.imagePath` 是 install name 而非磁盘路径。 + 2. **bare name 排序兜底**——`@rpath/…` 或 cache 不认识的路径拼写落到这里。cache 里 leaf name 不唯一:macOS cache 在 `/System/iOSSupport` 下带着 Mac Catalyst 版 SwiftUI,iOS cache 有同名 `.axbundle`。候选按 MachOKitExtensions 的 `DyldCacheImageSearchMode.matchRank` 排序(canonical framework > 普通 dylib > bundle,support root 降级),取最优。**旧的 SwiftLayout 定位器是「枚举顺序首写者胜」**,在 macOS cache 上可能选中 Catalyst 构建——这是合并时消除的潜在错配(`FileDependencyLocatorTests.bareNameFallbackPrefersTheNativeCanonicalFramework` 锁定)。`matchRank` 对多点 leaf(`libc++.1.dylib`)返回 `nil`,此时记为最差 rank 但仍可解析。 + + cache 索引**首次查询时一次性建成**(一遍 `machOFiles()`,同时建 install path 表与 bare name 最优表),之后 O(1)。逐次 `machOFile(by:)` 是 `O(依赖数 × cache 大小)` 的全扫描,阶段 3 实测 551 镜像闭包要 21 秒。`NSLock` 保护惰性索引,定位器可跨任务共享。 + + fat 显式文件取与 root 同架构的 slice(`preferredCPU`:先比 `cpu.type` + 掩掉 capability 位后的 `cpu.subtype`,能分开 arm64 / arm64e;再只比 type;最后 `.first`——旧两处实现都无条件取 `.first`)。注意 MachOKit 的 `CPU ==` 比的是原始值,versioned-ABI 的 arm64e 切片会和普通 arm64e 判不等,所以不能直接比 `header.cpu`。 + +## 4. 闭包遍历(`DependencyClosure`) + +`DependencyClosure(root:traversal:locator:)` 是唯一的遍历实现,两个便利初始化器只是选定位器:`init(root: MachOImage, traversal:)` 与 `init(root: MachOFile, searchPaths:, traversal:)`。 + +- **`.direct`** 只走 root 自己的 load command;**`.transitive`** BFS 递归。 +- **顺序是契约的一部分**:direct 为 load command 顺序,transitive 为 BFS(root 的直接依赖全部在前)。`SwiftLayout.ImageUniverse` 按这个顺序惰性索引依赖、命中即停;DFS 会把 Foundation 整棵子树排在 root 的第二个 Swift 依赖前面(`DependencyClosureTests.inProcessTransitiveClosureExtendsTheDirectPrefixBreadthFirst` 锁定 direct 是 transitive 的前缀)。 +- 按 bare name 去重,root 自身排除(以 root 的 `imagePath` 归一后预置进 visited 集合);**再按镜像身份去重**(`MachORepresentableWithCache.identifier`,文件是 `LC_UUID` 键)——文件定位器把一个显式文件登记在磁盘路径、install name、bare name 三种拼法下,root 若以两个 load name 链到同一个二进制,只按 bare name 去重会把它收两次(`sameImageReachedUnderTwoLoadNamesIsCollectedOnce` 锁定)。 +- 定位不到的 load name 进 `unresolvedLoadNames`(按遇到顺序,同样按 bare name 去重),遍历继续。`images` 与 `unresolvedLoadNames` 恰好是 root 直接依赖的二分(direct 模式下,`DependencyClosureTests.inProcessDirectClosureResolvesMappedDependencies` 锁定)。 + +**为什么 SwiftInterface 保持 `.direct`**:TypeIndexing 按依赖清单逐模块生成 SourceKit 接口,成本随清单线性增长;OS 框架的传递闭包有几百个镜像,会退回提案 0009 之前「全 SDK 生成」的开销。要传递集合的宿主自己构造 `DependencyClosure(…, traversal: .transitive)` 再喂 `init(closure:)`。 + +## 消费入口速查 + +```swift +// 静态布局:传递闭包(默认) +let universe = try ImageUniverse.dependencyClosure(root: machOFile, searchPaths: [.machOFile(path: helperPath), .systemDyldSharedCache]) +// 或先建闭包再共享 +let closure = DependencyClosure(root: machOFile, searchPaths: [.systemDyldSharedCache]) +let universe = try ImageUniverse.dependencyClosure(closure) +let providerDependencies = SwiftInterfaceBuilderDependencies(closure: closure) // 注意:这里是传递集合 + +// __C 归属:直接依赖 +let providerDependencies = SwiftInterfaceBuilderDependencies(machO: machOFile, searchPaths: [.systemDyldSharedCache], eventHandlers: [ConsoleEventHandler()]) +providerDependencies.unresolvedLoadNames // 精确报告解析不到的依赖 +``` + +已废弃(保留一个版本):`SwiftLayout.LayoutDependencySearchPath`(typealias)、`SwiftInterface.DependencyPath`(case 拼写不同,经 `searchPath` 转换)与 `SwiftInterfaceBuilderDependencies.init(machO:paths:eventHandlers:)`。 + +## 测试锚点 + +- `Tests/MachODependenciesTests/DependencyLoadNameTests.swift` — 归一规则表 + 与 `MachOImage(name:)` 的契约。 +- `Tests/MachODependenciesTests/DependencyClosureTests.swift` — direct / transitive 语义、BFS 前缀、去重、未解析报告、坏搜索路径不抛、自定义定位器收到原始 load name。 +- `Tests/MachODependenciesTests/FileDependencyLocatorTests.swift` — 宿主 cache 上的精确路径优先与 Catalyst 降级(无宿主 cache 时跳过)。 +- `Tests/SwiftInterfaceTests/SwiftInterfaceBuilderDependenciesTests.swift` — 薄包装的 direct 语义、image 版非空回归、`init(closure:)` 保留调用方遍历。 +- `Tests/SwiftLayoutTests/DependencyClosureLayoutTests.swift` — 端到端:闭包驱动的跨模块字段偏移(未改动)。 + +## 已知边界 + +- `@rpath` 等不展开(见 §1)。 +- 依赖种类不过滤:load / weak / reexport / upward / lazy 全收。 +- cache 的 bare name 兜底对多点 leaf 名(`libc++.1.dylib`)只能给最差 rank。 +- `MachODependenciesTests` 不在 CI 的 filter 子集里,只在本地全量跑。 + +## 相关文档 + +- [StaticLayoutDependencyClosure.md](../StaticLayoutDependencyClosure.md) — SwiftLayout 阶段 3 的原始设计与「落地实测」(惰性索引、BFS、一次性 cache 索引等结论的出处)。 +- [TypeIndexingPipeline.md](../TypeIndexingPipeline.md) — 直接依赖清单在 `__C` 归属管线里的用法。 diff --git a/Documentations/Internal/Modules/README.md b/Documentations/Internal/Modules/README.md new file mode 100644 index 00000000..f28b6ad2 --- /dev/null +++ b/Documentations/Internal/Modules/README.md @@ -0,0 +1,43 @@ +# 模块参考文档(Modules/) + +本目录是**按模块组织的参考文档**系列:每个库模块一篇,回答「这个模块是什么、内部分几个子系统、每个子系统的分工与关键契约是什么、细节去哪里看」。它是各模块的**权威入口**——专题文档(迁移记录、审计、提案实现说明)继续留在 `Internal/` 平铺层与 `Evolutions/`,模块文档负责把它们串起来。 + +写作约定: + +- **一个模块一篇**,文件名与模块目录名一致(PascalCase + `.md`)。模块内子系统多的,每个子系统在文中占一个完整章节;已有专题文档覆盖的子系统写导读并链接,不复述。 +- 内容以「代码里看不出来的东西」为主:子系统边界、跨文件契约、决策的为什么、测试锚点。逐行复述源码注释的内容不写。 +- 与代码同批维护:模块的文件增删、子系统重组,模块文档在同一批次更新。 +- 每新增一篇,同批更新本表与 [`Documentations/README.md`](../../README.md) 索引。 + +## 覆盖状态 + +| 模块 | 文档 | 说明 | +|---|---|---| +| SwiftInterface | [SwiftInterface.md](SwiftInterface.md) | ✅ 已写 | +| swift-section (CLI) | — | 待写 | +| SwiftIndexing | — | 待写 | +| SwiftPrinting | — | 待写 | +| SwiftSpecialization | — | 待写 | +| SwiftAttributeInference | — | 待写 | +| SwiftDeclaration | — | 待写 | +| SwiftDeclarationRendering | — | 待写 | +| SwiftDump | — | 待写 | +| SwiftInspection | — | 待写 | +| SwiftLayout | — | 待写(专题:[StaticLayoutEngine.md](../StaticLayoutEngine.md)、[StaticLayoutDependencyClosure.md](../StaticLayoutDependencyClosure.md)) | +| SwiftDiffing | — | 待写(专题:[ABIDiffDesignAndLimitations.md](../ABIDiffDesignAndLimitations.md)、[ABIEvolutionDesign.md](../ABIEvolutionDesign.md)) | +| TypeIndexing | — | 待写(专题:[TypeIndexingPipeline.md](../TypeIndexingPipeline.md)) | +| SwiftOutputTransformer | — | 待写(专题:[OutputTransformerMigration.md](../OutputTransformerMigration.md)) | +| MachOSwiftSection | — | 待写 | +| MachOFoundation | — | 待写 | +| MachOSymbols | — | 待写(专题:[SymbolIndexStoreMemoryOptimization.md](../SymbolIndexStoreMemoryOptimization.md)) | +| MachOBase | — | 待写(伞模块:ABI 层允许看到的全部——reading / resolving / pointers;见 [SelfContainedABILayer.md](../SelfContainedABILayer.md)) | +| MachOPointers | — | 待写(`SymbolOrElementPointer` 自 `MachOSymbolPointers` 并入,见 [SelfContainedABILayer.md](../SelfContainedABILayer.md)) | +| MachOReading / MachOResolving | — | 待写 | +| MachOCaches | — | 待写 | +| MachODependencies | [MachODependencies.md](MachODependencies.md) | ✅ 已写 | +| MachOSwiftSectionC | — | 待写 | +| MachOMacros | — | 待写 | +| Utilities | — | 待写 | +| MachOFixtureSupport / MachOTestingSupport(C) / baseline-generator | — | 待写(测试基础设施,可合并成一篇) | + +(`Demangling` / `Semantic` 来自上游包 swift-demangling / swift-semantic-string,`MachOKitExtensions` / `MachOObjCSection` 是 sibling 外部包,文档归各自仓库。) diff --git a/Documentations/Internal/Modules/SwiftInterface.md b/Documentations/Internal/Modules/SwiftInterface.md new file mode 100644 index 00000000..5c678f07 --- /dev/null +++ b/Documentations/Internal/Modules/SwiftInterface.md @@ -0,0 +1,111 @@ +# SwiftInterface 模块 + +> 模块参考文档(module reference),随代码维护。读者:维护者。 +> 细节文档见文末[「相关文档」](#相关文档);本文负责全貌与分工,不复述细节。 + +## 模块定位 + +SwiftInterface 是接口生成的**编排层**(thin orchestrator):它自己不索引、不打印、不算 diff,而是把下层的 `SwiftIndexing`(建声明模型)、`SwiftPrinting`(渲染声明)、`SwiftDiffing`(ABI 键与 lineage 事实)组合成三种**面向人读的输出产品**: + +1. **单版本完整 interface** —— `SwiftInterfaceBuilder.printRoot()`,`swift-section interface` 与 RuntimeViewer 的主路径; +2. **两版本 diff interface** —— `SwiftDiffableInterfaceRenderer`,整份接口逐行标 `+`/`-`/` `,`swift-section diff --interface`; +3. **N 版本 evolution interface** —— `AnySwiftEvolutionInterfaceBuilder`,并集接口 + 生命周期注解(`// [●●○] removed in 26.0`),`swift-section evolution --interface`。 + +产品 2 与 3 共享同一套结构遍历核心 `InterfaceUnionWalker`(演进提案 0014 的统一),只在「呈现策略」上分叉。另有一个不渲染的旁支:`SwiftDiffableInterfaceBuilder` 把索引结果**冻结**成 `ABIModule` / `ABISnapshot`,是 `SwiftDiffing` 数据管线(change list / JSON / lineage 报告)的输入生产者——渲染路径和纯数据路径吃的是同一份冻结事实,这是两边永不打架的根基。 + +下游消费者:`swift-section` CLI(`InterfaceCommand` / `DiffCommand` / `EvolutionCommand`)、`TypeIndexing`(它的 provider 实现本模块的注入协议)、RuntimeViewer 等宿主(走 `@_spi(Support)` 的结构化流)。 + +## 文件 → 子系统对照 + +| 子系统 | 文件 | +|---|---| +| 1. 单版本接口生成 | `SwiftInterfaceBuilder`、`SwiftInterfaceBuilderConfiguration`、`SwiftInterfaceBuilderDependencies`、`DependencyPath`、`SwiftInterfaceBuilderExtraDataProvider` | +| 2. Opaque 返回类型解析 | `SwiftInterfaceBuilderOpaqueTypeProvider`、`ProtocolFactsResolver`、`BuiltinStandardLibraryProtocolFacts`、`OpaqueSameTypeConstraint` | +| 3. 共享 union 走查 | `InterfaceUnionWalker`、`InterfaceVersionRendering` | +| 4. 两侧 diff 渲染 | `SwiftDiffableInterfaceBuilder`、`SwiftDiffableInterfaceRenderer`(含 `DiffUnionStrategy`)、`DiffMarking`(含 `DiffMarker`/`DiffLine`)、`DiffContainerAssembler`、`DiffFormat`、`UnifiedDiffFormatter`、`SwiftDeclarationPrinter+DiffRendering` | +| 5. N 路 evolution 渲染 | `AnySwiftEvolutionInterfaceBuilder`、`SwiftEvolutionInterfaceBuilder`(pack façade)、`SwiftEvolutionInterfaceRenderer`、`EvolutionAnnotationIndex`、`EvolutionLine`(含 `EvolutionAnnotation`)、`EvolutionMarking`(含 `EvolutionContainerAssembler`) | + +## 子系统 1:单版本接口生成 + +`SwiftInterfaceBuilder` 持有一对 `SwiftDeclarationIndexer` + `SwiftDeclarationPrinter`(`@_spi(Support)` 暴露,宿主可直接触达),生命周期是两步:`prepare()` 然后 `printRoot()`。 + +**`prepare()` 的顺序与失败语义**:先逐个 `extraDataProvider.setup()`(失败**降级**为 `renderingDegraded` 事件,不阻断——外挂数据源坏了不该毁掉整份接口),再 `indexer.prepare()`(失败**抛出**),最后 `collectModules()`(失败**抛出**)。全程用 `phaseTransition` 事件汇报阶段。 + +**运行线程**:`prepare()` 与 `printRoot()` 的函数体都包在 `LargeStackTaskExecution.run` 里(提案 `large-stack-executor-and-cross-version-parallelism`),整段 task 跑在 demangler 的 16 MB 大栈执行器上,打印路径每个符号不再付一次线程往返;输出与执行器无关。`SwiftDiffableInterfaceBuilder.prepare()`、evolution builder 的 `prepare` / 渲染入口、diff renderer 的两个入口同样如此。见 [LargeStackTaskExecutorAdoption.md](../LargeStackTaskExecutorAdoption.md)。 + +**`collectModules()`**:import 列表不来自 load command,而是扫全部符号的 demangle 树收 `.module` 节点——binary 里真正被引用的模块才进 import。过滤 `__C` / `__ObjC` / stdlib 三个伪模块;`internalModules`(`Swift`、`_Concurrency`、`_StringProcessing`、`_SwiftConcurrencyShims`)恒定并入。 + +**`printRoot()` 的段落与 catch 契约**:组成顺序是 header(提案 0008,flag-gated 默认缺席)→ imports → 全局变量 → 全局函数 → 根类型 → 特化变体(`specializedChildren` 挂在各 `TypeDefinition` 上,indexer 对用户驱动的特化保持无知,所以这里全量走查 `allTypeDefinitions`)→ 根协议 → **嵌套**协议的 default-implementation 扩展块(extension 不能嵌进父体,顶层补印;这个循环在提案 0007 之前是死代码)→ 四桶 extension(`isAttachedToProtocolDefinition` 的已附着定义被过滤,避免 issue #106 §5 的重复块)。贯穿全部段落的契约是**逐定义 catch**:一个定义打印抛错只丢它自己,绝不空掉整块(历史上块级 catch 让一个旧 binary 的全部类型被抹白;由 `LegacyDyldInfoBindTests` 与 `corruptNestedChildDropsOnlyItself` 钉住)。两个全局块例外地不带定义上下文——`printVariable`/`printFunction` 本身不抛、各自派发过失败事件,块级包裹只是保险带。 + +**exported-only 过滤的接线**(提案 0016):`printRoot()` 现在是一个普通函数壳,开关 +`printExportedDeclarationsOnly` 打开时先用 `indexer.allTypeDefinitions` / `allProtocolDefinitions` 调 +`printer.installExportFilterScope(types:protocols:)`,再进入 builder 体 `printRootContents()`。类型 / 协议 / 成员由 printer 自行裁决, +扩展的「目标是不是本镜像内未导出声明」只有索引器的表能回答(strip 过的镜像里未导出类型零符号),所以 scope 在这里装; +两个全局块各带一个 `where !printer.isExcludedByExportFilter(globalSymbolNames:)`。绕过 `printRoot` 的宿主要自己装 scope,否则扩展一腿 fail-open。 +详见 [ExportedOnlyInterfaceFiltering.md](../ExportedOnlyInterfaceFiltering.md)。 + +**`SwiftInterfaceBuilderExtraDataProvider`**:外部数据源的注入缝,纯生命周期钩子——`Sendable` + 一个默认为空的 `setup()`(提案 0015 之前它还继承 printer 的 resolver 协议,强迫所有 provider 都是类型名解析器)。「会不会回答 printer 查询」是正交能力:provider 按需另行声明 `SwiftPrinting` 的角色协议(`ModuleNameResolving` / `CImportedNameResolving` / `OpaqueTypeResolving`,均 refine 空标记协议 `TypeNameResolving`),`addExtraDataProvider(_:)` 用 `as? any TypeNameResolving` 命中才把它转发给 printer——所以一个 resolver 型 provider 一头挂在 builder 的 prepare 生命周期上,一头挂在打印热路径上,而纯 setup 型 provider(只预热缓存之类)也是合法形态。两个已知实现:`TypeIndexing.SwiftInterfaceBuilderTypeNameProvider`(`__C` 模块归属,跨模块,声明两个名字角色)和本模块的 `SwiftInterfaceBuilderOpaqueTypeProvider`(声明 `OpaqueTypeResolving`,见子系统 2)。 + +**`SwiftInterfaceBuilderDependencies` + `DependencyPath`**:把「主 binary + 它的依赖镜像」凑成一组,按 reader 分特化——`MachOFile` 版从 `DependencyPath`(单个 Mach-O 路径 / dyld cache 路径 / 宿主系统 cache)按 install name 匹配装载,装载失败走 `renderingDegraded` 事件(默认无 handler 也有 os_log 地板);`MachOImage` 版直接按名字向 dyld 要。消费者是 CLI 的 `InterfaceCommand` 与 `TypeIndexing`(依赖过滤 + 惰性 ObjC 元数据索引都需要依赖镜像清单)。 + +## 子系统 2:Opaque 返回类型解析 + +把 `some P` 的占位还原成带 primary associated type 实参的完整拼写(`some Collection & Sendable`)。领域细节已有两篇专文——[OpaqueReturnTypeResolution.md](../OpaqueReturnTypeResolution.md)(descriptor 编码、anchor/塌缩机制、字节级调试)与 [OpaquePrimaryAssociatedTypeAttribution.md](../OpaquePrimaryAssociatedTypeAttribution.md)(提案 0011 的实现说明)——本节只给文件分工: + +- **`SwiftInterfaceBuilderOpaqueTypeProvider`**:入口,也是一个 `ExtraDataProvider`(挂到 builder 上,printer 打印 `some` 返回类型时经 `opaqueType(forNode:index:)` 回查)。从符号表定位 opaque type descriptor,把 generic requirements 拆成协议项与 same-type 项,逐协议调用归属判定(anchor 直接命中 → refine 闭包 → 名字兜底,兜底四条件缺一不可——宁可少一个实参也不捏造一个)。 +- **`OpaqueSameTypeConstraint`** / **`OpaqueDependentMemberProjection`**:从 requirement 节点里挖出来的单条 same-type 约束(区分正向 pin `τ.Name == X` 与反向 pin `outer == τ.Name`,后者渲染期经 `SubstitutionMap` 回溯)及其解析器。 +- **`ProtocolFactsResolver`**:按「可达 descriptor 优先、内置表兜底」的链条解析协议事实(自声明的 associated type 名、refine 闭包),`refineClosureContainsAnchor` 对不完整闭包返回三态(命中 / 完整排除 / `nil` 不可证)。 +- **`BuiltinStandardLibraryProtocolFacts`**:冻结的 stdlib 协议表——**primary associated type 名单与顺序的唯一来源**(SE-0346 不留运行时痕迹),也是离线 bind-only 外部协议的兜底。无 associated type 的协议也登记空条目,让归属能说「确定不附着」而非降级。 + +## 子系统 3:共享 union 走查(提案 0014) + +diff 与 evolution 两条比较渲染路径的公共结构核心。分工是这个子系统的全部要点: + +- **`InterfaceUnionWalker` 拥有结构**:N 版本按 `ABIKey` 匹配(与 `ABIDiffer` 冻结进 snapshot 的同一套键构造,所以渲染视图与数据视图的匹配永远一致);并集排序 = 最新版本的声明顺序做脊柱,缺席声明按「最后携带它的版本」的顺序追加;每版本键 first-wins(**含发射**——同键后来者不会被发射第二次)。extension 桶按 `ABIDiffer.extensionContainerKey` 拆成 per-(target, protocol, where, retroactive) 容器;成员经 `UnionRenderableMember`(identity/payload 键取自 differ 冻结的同一 `MemberRecord` 投影)构造;类别调度走 `MemberCategory.allCases`;body 组装顺序镜像 `printTypeDefinition`。 +- **`InterfaceUnionEmitting` 策略拥有呈现**:五个定制点——type/protocol header 解析(返回 `nil` 则整个声明连体丢弃:没有 header 行的成员不是合法 Swift)、extension header 包装、成员发射(一个 match 出零到多个 unit)、容器组装。真正双侧语义(`HeaderOutcome` 配对)留在 diff 策略、注解锚定留在 evolution 策略,绝不上提进 walker。 +- **`InterfaceVersionRendering` / `InterfaceVersionUnit`**:版本抽象缝。每个版本 = 一个 `SwiftDiffableInterfaceBuilder` + 一个共享其事件 dispatcher 的 printer(dispatcher 共享是修过的坑:裸 `.init(in:)` 的 printer 没有 sink,diff 路径曾整条静默吞失败)。reader 泛型在此擦除且无损——walker 对 printer 的全部消费就是「把这个成员渲染成 `SemanticString`」,没有任何 `MachO` 类型的值跨缝。 +- **成员一律在 printer level 0 渲染**:变量/下标 printer 会按 `level` 绝对烘焙 accessor 块内部缩进,而两个格式层又按行自缩进——真实 level 渲染会让 `get`/`}` 双重缩进(diff 路径在统一前正是带着这个缺陷;`DiffMemberIndentationTests` 钉住)。 + +## 子系统 4:两侧 diff 渲染 + +数据流:`SwiftDiffableInterfaceBuilder` ×2(prepare)→ `SwiftDiffableInterfaceRenderer`(包成 `[old, new]` 双元素轴)→ walker + `DiffUnionStrategy` → 分类流 `[[DiffLine]]` → `DiffFormat` → 最终文本。 + +- **`SwiftDiffableInterfaceBuilder`**:`SwiftInterfaceBuilder` 的 ABI-diff 对应物——索引后不打印而是冻结。`prepare()` 必须自己驱动逐定义的 `index(in:)`:成员索引平时由 printer 惰性触发,differ 不打印,没人替它触发。`abiModule()` 是 indexer 属性的纯投影;`snapshot()` 直通 `Codable` 快照(存基线、离线 diff)。 +- **`DiffUnionStrategy`**:三路成员发射(unchanged 发新侧 ` `、added/removed 发单侧、modified 发 `-` 旧行 + `+` 新行),带**同文塌缩**——payload 键(remangle)变了但两侧渲染逐字节相同(symbolic reference、私有判别符被 `.default` 打印抹掉的场合)就塌成一条上下文行,change list 里仍记录键变。header 走 `HeaderOutcome` 三态(absent / rendered / failed,**不是** `SemanticString?`——「本侧没有这个声明」和「有但渲染失败」曾被同一个值表示,混同的后果是空 header 顶着成员出场);两侧**总是都尝试**,单侧失败由另一侧顶替站位,失败在失败侧自己的 dispatcher 上派发。 +- **分类流 / 格式层分离**:renderer 永不把 `+`/`-` 符号烤进文本,只产 `[[DiffLine]]`(marker + 裸单行内容 + indentLevel;`@_spi(Support) annotatedDiffBlocks()` 直接暴露给宿主)。`DiffFormat` 是唯一的符号化缝:`inline`(git-diff 风格,marker 占 0 列 + 一格 gutter)、`markdownFenced`(```` ```diff ````围栏,围栏长度自适应内容里的反引号串)、`unified(contextLines:)`(真 unified diff,`git apply` 可消费,gutter 为空)、`perLine` 最小扩展点。`UnifiedDiffFormatter` 独立成文件做行号 / hunk 分组。 +- **`DiffMarking` / `DiffContainerAssembler`**:纯函数格式工具。`markLines`(急切成串)与 `markedLines`(结构化)共享同一条 per-line 规则,防两路漂移;`splitIntoLines` 故意开 internal 给 `EvolutionMarking` 复用。assembler 管容器组装:added/removed 容器整体带 marker、common 容器 header 变了才 `-`/`+` 成对、空 body 内联 ` {}`。 +- **`SwiftDeclarationPrinter+DiffRendering`**:diff 专用的 `package` 打印入口(无 body 的 type/protocol header、独立 `deinit` / `associatedtype` 行)。放本模块而非 `SwiftPrinting` 是刻意的——只服务 diff 路径的帮手不该混在共享渲染原语旁边;但 header 渲染义务上要与 `printTypeDefinition` / `printProtocolDefinition` 的 header 部分**保持同步**(源码注释里有 keep-in-sync 标记)。 + +## 子系统 5:N 路 evolution 渲染(提案 0013) + +N ≥ 2 版本渲染成**一份**并集接口,声明尾注生命周期注解。承重决策是**事实与文本的分工**: + +- **注解事实只来自 `ABIEvolution`**:`prepare(maximumConcurrentPreparations:)` **并行**索引各版本(窗口默认取核数、`1` 即旧的串行顺序;各版本是不同文件、缓存按 UUID 键控,结果与窗口无关,由 `parallelPreparationMatchesSerialPreparation` 钉住)→ 冻结 snapshot → `ABIEvolutionBuilder` 建 lineage 矩阵;渲染期经 `EvolutionAnnotationIndex` 按键查询,**查不到即是「全程在场、从未变化」的裁决**(`ABIEvolution` 只物化有变化的 lineage),渲染为无注解。策略自己绝不重推事件,所以注解接口、lineage 报告、JSON 三个视图永不打架。 +- **渲染文本来自活模型**:每个声明由最后携带它的版本的 printer 渲染(modified 成员只显示最新一代,旧形态进注解短语 `modified in 26.0: old → new`;箭头两侧相同则塌回裸短语)。header 解析是「最新可渲染」:从新到旧找第一个渲染成功的版本,每次失败都在其版本自己的 dispatcher 上派发,全部失败才整体丢弃(diff 的 drop-whole 规则推广到 N 侧)。 +- **公开面是两个类型**:`AnySwiftEvolutionInterfaceBuilder` 是类型擦除的 runtime-N 主力(同构数组 init + 异构 pack init 都全平台可用——pack 在*函数*位不需要可用性门槛),CLI 与宿主的用户选版场景都走它;`SwiftEvolutionInterfaceBuilder` 是 pack 泛型 façade(类型位的 pack 需要 Swift 5.9 运行时,故 `@available(macOS 14…)`;构造即擦除,行为逐字节一致,由 `packGenericFacadeMatchesTheErasedBuilder` 钉住)。工具链尚不支持 `repeat each MachO == M` 的同元素约束,所以数组 init 上不了 pack 类型——这是两个类型并存的直接原因。 +- **格式层 `EvolutionMarking`**(+ `EvolutionContainerAssembler`):legend 头两行(轴 + bitmap 位置对照)、注解列按块对齐、上限 72 列(超限换行缩一级)、锚点规则(成员注解锚**首行**——attribute 内联,computed property 的注解不能沉到 accessor 块闭括号;容器 header 锚**末行**——带 `{` 的那行)、镜像 `ABIEvolutionReporter` 措辞的 warnings 尾巴。与 `DiffMarking` 故意不合并:marker 按行、注解按 unit 锚定,是真不同语义。 +- **结构化流**:`@_spi(Support) annotatedBlocks()` → `[[EvolutionLine]]`(`EvolutionAnnotation` 是纯数据——presence bitmap + `LineageEvent`s),宿主可自行着色/折叠。 + +限制:输入必须全是 binary(snapshot 没有可渲染接口);协议的 `pwtslot:` 记录不渲染(没有对应声明,与 `diff --interface` 一致,变化仍见于 lineage 报告与 JSON)。 + +## 消费入口速查 + +| 入口 | 路径 | +|---|---| +| `swift-section interface` | `SwiftInterfaceBuilder`(+ `--resolve-c-module-names` 挂 TypeIndexing provider,opaque provider 默认挂) | +| `swift-section diff --interface` | `SwiftDiffableInterfaceBuilder` ×2 + `SwiftDiffableInterfaceRenderer` | +| `swift-section evolution --interface` | `AnySwiftEvolutionInterfaceBuilder`(与 `--json`/`--summary-only` 互斥) | +| `swift-section diff` / `snapshot` / `evolution`(数据路径) | `SwiftDiffableInterfaceBuilder.abiModule()/snapshot()` → SwiftDiffing | +| RuntimeViewer 等宿主 | `@_spi(Support)`:indexer/printer 直达、`annotatedDiffBlocks()`、`annotatedBlocks()`;`InterfaceHeaderBlock` 是独立组件(per-type 导出不走 `printRoot`) | + +## 相关文档 + +- [Evolutions/0013](../../Evolutions/0013-swift-evolution-interface-builder.md) —— evolution 渲染的提案(决策记录,含 API 全貌与决策日志) +- [Evolutions/0014](../../Evolutions/0014-unify-interface-renderers.md) —— union walker 统一的提案 +- [Evolutions/0011](../../Evolutions/0011-opaque-primary-associated-type-attribution.md) / [OpaquePrimaryAssociatedTypeAttribution.md](../OpaquePrimaryAssociatedTypeAttribution.md) / [OpaqueReturnTypeResolution.md](../OpaqueReturnTypeResolution.md) —— opaque 子系统 +- [InterfaceHeaderAndExportStatusAnnotations.md](../InterfaceHeaderAndExportStatusAnnotations.md) —— 接口头部与导出状态标注(提案 0008;header 组件在本模块消费) +- [ExportedOnlyInterfaceFiltering.md](../ExportedOnlyInterfaceFiltering.md) —— exported-only 过滤(提案 0016;`printRoot` 装 `ExportFilterScope`) +- [DiffableInterfacePlan.md](../DiffableInterfacePlan.md) —— diff 接口的原始实现计划(历史文档,统一前的形态) +- [ABIDiffDesignAndLimitations.md](../ABIDiffDesignAndLimitations.md) / [ABIEvolutionDesign.md](../ABIEvolutionDesign.md) —— 下层 SwiftDiffing 的键方案与 lineage 模型 +- [LeafMigrationRegressionFixes.md](../LeafMigrationRegressionFixes.md) —— `printRoot` 逐定义 catch 契约的来历 +- [SwiftModularizationMigration.md](../SwiftModularizationMigration.md) —— SwiftInterface 单体拆成分层对等模块的迁移记录 diff --git a/Documentations/Internal/ProjectEvolutionLog.md b/Documentations/Internal/ProjectEvolutionLog.md index b841089f..494f3359 100644 --- a/Documentations/Internal/ProjectEvolutionLog.md +++ b/Documentations/Internal/ProjectEvolutionLog.md @@ -62,8 +62,8 @@ - **落地**:`SwiftSpecialization`:`GenericSpecializer` 两步 API(`makeRequest` → `specialize`)、`ConformanceProvider`、PWT 按 requirement 顺序传递的关键不变量。 后续加入 `Argument.boundGeneric` 嵌套绑定(Roadmap 2026-05-11 的 Approach 2)。 -- **文档**:[../../docs/superpowers/specs/2026-05-02-generic-specializer-cleanup-design.md](../../docs/superpowers/specs/2026-05-02-generic-specializer-cleanup-design.md)、 - [../../docs/superpowers/reviews/2026-05-06-generic-specializer-bug-review.md](../../docs/superpowers/reviews/2026-05-06-generic-specializer-bug-review.md)、 +- **文档**:[Reviews/2026-05-06-generic-specializer-bug-review.md](Reviews/2026-05-06-generic-specializer-bug-review.md) + (含同期 cleanup 六项的提交面记录;cleanup 的原始设计 spec 在 git 历史的 `docs/superpowers/`)、 [../../Roadmaps/2026-05-11-bound-generic-candidates.md](../../Roadmaps/2026-05-11-bound-generic-candidates.md)、 TaskReports [2026-06-10-pr88-nested-generic-specialization-followups.md](TaskReports/2026-06-10-pr88-nested-generic-specialization-followups.md) / [2026-06-10-pr88-nested-recursion-depth-limit.md](TaskReports/2026-06-10-pr88-nested-recursion-depth-limit.md)。 @@ -74,8 +74,8 @@ - **时间**:2026-03-12 → 2026-04-18(`0.8.x`–`0.9.x`) - **动机**:dump / interface 输出需要可回归的快照测试,且要能在 CI 上跑。 - **落地**:snapshot 测试管线 + CI 设计。 -- **文档**:[../../docs/superpowers/specs/2026-03-15-ci-snapshot-testing-design.md](../../docs/superpowers/specs/2026-03-15-ci-snapshot-testing-design.md)、 - [../../docs/superpowers/specs/2026-04-18-ci-test-filter-design.md](../../docs/superpowers/specs/2026-04-18-ci-test-filter-design.md)。 +- **文档**:[FixtureTestingAndContinuousIntegration.md](FixtureTestingAndContinuousIntegration.md) + (整合原快照测试与 CI 过滤两份 spec;原文在 git 历史的 `docs/superpowers/`)。 ## 7. SymbolTestsCore fixtures / 覆盖率体系 @@ -84,11 +84,9 @@ 并对 `MachOSwiftSection/Models` 建立「每个 public 方法必有测试或 allowlist」的覆盖不变量。 - **落地**:`MachOFixtureSupport`、`baseline-generator` + `RegenerateBaselinesPlugin`、 `MachOSwiftSectionCoverageInvariantTests` 四不变量、`SuiteBehaviorScanner`。 -- **文档**:[../../docs/superpowers/specs/2026-04-10-symboltestscore-integration-tests-design.md](../../docs/superpowers/specs/2026-04-10-symboltestscore-integration-tests-design.md)、 - [../../docs/superpowers/specs/2026-04-13-symboltestscore-fixture-expansion-design.md](../../docs/superpowers/specs/2026-04-13-symboltestscore-fixture-expansion-design.md)、 - [../../docs/superpowers/specs/2026-05-03-machoswift-section-fixture-tests-design.md](../../docs/superpowers/specs/2026-05-03-machoswift-section-fixture-tests-design.md)、 - [../../docs/superpowers/specs/2026-05-05-fixture-coverage-tightening-design.md](../../docs/superpowers/specs/2026-05-05-fixture-coverage-tightening-design.md)。 - 测试约定见 [AGENTS.md](../../AGENTS.md)。 +- **文档**:[FixtureTestingAndContinuousIntegration.md](FixtureTestingAndContinuousIntegration.md) + (整合原集成测试、fixture 扩展、ABI 覆盖体系、覆盖收紧四份 spec;原文在 git 历史的 + `docs/superpowers/`)。测试约定见 [AGENTS.md](../../AGENTS.md)。 ## 8. ReadingContext 读取抽象 @@ -96,8 +94,9 @@ - **动机**:统一 `MachOFile` / `MachOImage` / InProcess 三种读取方式的 API 面,让上层代码 对 reader 泛化。 - **落地**:`MachOReading.ReadingContext` 一族 + 全库适配。 -- **文档**:[ReadingContextAbstraction.md](ReadingContextAbstraction.md)、 - [../../docs/superpowers/specs/2026-05-02-reading-context-api-design.md](../../docs/superpowers/specs/2026-05-02-reading-context-api-design.md)。 +- **文档**:[ReadingContextAbstraction.md](ReadingContextAbstraction.md) + (其「Model Coverage Completion Pass」一节整合了原覆盖补全 spec;原文在 git 历史的 + `docs/superpowers/`)。 ## 9. SwiftInterface ABI 解析 / 打印路径修复 @@ -968,7 +967,7 @@ --- -## 47. 演进并集注解接口 SwiftEvolutionInterfaceBuilder(提案 draft-swift-evolution-interface-builder) +## 47. 演进并集注解接口 SwiftEvolutionInterfaceBuilder(提案 0013) - **时间段**:2026-08-25。 - **动机**:`swift-section evolution` 唯一的人读输出是 `ABIEvolutionReporter` 的 @@ -998,7 +997,7 @@ `EvolutionLine`)、`swift-section`(`evolution --interface` + 事件类别着色); 测试为格式层/注解索引单测 + 三版本即时编译 fixture 的端到端 suite + CLI 校验规则钉子。 -- **文档**:[draft-swift-evolution-interface-builder.md](../Evolutions/draft-swift-evolution-interface-builder.md)、 +- **文档**:[0013-swift-evolution-interface-builder.md](../Evolutions/0013-swift-evolution-interface-builder.md)、 [ABIEvolutionDesign.md](ABIEvolutionDesign.md)(第五批增量一节)、 [TaskReports/2026-08-25-swift-evolution-interface-builder.md](TaskReports/2026-08-25-swift-evolution-interface-builder.md)、 README `evolution` 一节、术语表新增「union interface」「lifecycle annotation」。 @@ -1042,7 +1041,7 @@ --- -## 49. 统一 diff / evolution 接口渲染器的结构遍历核心(提案 draft-unify-interface-renderers) +## 49. 统一 diff / evolution 接口渲染器的结构遍历核心(提案 0014) - **时间段**:2026-08-26。 - **动机**:`SwiftDiffableInterfaceRenderer`(602 行)与 `SwiftEvolutionInterfaceRenderer` @@ -1071,7 +1070,7 @@ 库侧净 −256 行(671+/927−,且两渲染器只剩策略)。附带测试基建发现:纯 struct 的即时编译 fixture dylib 没有 `__DATA` 段,pinned MachOKit 解析其 chained fixups 会越界崩溃——fixture 必须带 至少一个 class(已记入 AGENTS.md Test Environment)。 -- **文档**:[draft-unify-interface-renderers.md](../Evolutions/draft-unify-interface-renderers.md)、 +- **文档**:[0014-unify-interface-renderers.md](../Evolutions/0014-unify-interface-renderers.md)、 [TaskReports/2026-08-26-unify-interface-renderers.md](TaskReports/2026-08-26-unify-interface-renderers.md)、 AGENTS.md(`InterfaceUnionWalker` 条目 + fixture 地雷)、术语表新增「emission strategy」。 - **对应版本**:`0.17.0`。 @@ -1127,6 +1126,201 @@ --- +## 52. RuntimeMetadataTypeBuilder —— TypeBuilder 的首个生产 conformer + +- **时间段**:2026-08-30。 +- **动机**:swift-demangling 已完整移植上游 `TypeDecoder.h`(`TypeDecoder` + 遍历器 + `TypeBuilder` 协议),但生产侧没有任何 conformer。本项目进程内的 node → 活 + metadata 一直靠「remangle 成字符串 → `swift_getTypeByMangledNameInContext`」往返,该入口 + 要求 mangled 字节在可寻址内存、上下文与实参按运行时约定摆放,且无法表达任意合成的类型 + 表达式节点。 +- **关键决策**: + - **对标运行时 `DecodedMetadataBuilder`(`MetadataLookup.cpp`)逐路径移植语义**,不发明 + 新模型:nominal 统一走 bound-generic 路径、key-argument 收集按 `_gatherGenericParameters` + + `_checkGenericRequirements` 形状(父级 written args 从 parent metadata 读回、key 参数 + metadata 先行、PWT 按 requirement 顺序经 `swift_conformsToProtocol`)、组合期用 + `MetadataState.abstract`(容环)、顶层 `swift_checkMetadataState` 强制补全。 + - **不复用 `GenericSpecializer`**:其 `makeRequest` 为每个参数急切枚举候选(无约束参数 = + 全镜像每类型一个 Candidate)、PWT 解析硬依赖 indexer——交互流形状,不适合逐节点解码。 + - **命名节点是主路径**:本项目 `MetadataReader` 从不产出 `.typeSymbolicReference`(context + 引用都解析成命名树)。解析链 = 注入的 `nominalTypeDescriptorResolver` seam → 非泛型走 + 运行时按名查询 → 标准库常用泛型内置描述符表(经已知实例化的 metadata 反查)。 + - **ObjC class 的 canonical 形态是 realized class 指针本身**(`swift_getInitializedObjCClass`): + `swift_getObjCClassMetadata` 的 wrapper 是另一个 metadata 身份,会分裂泛型实例化缓存, + 且在测试进程里打印 wrapper 直接 SIGSEGV——探针实测按名查询返回的就是 class 指针。 + - **诚实拒绝面**(typed `TypeLookupError`,绝不造值):SIL 系、parameter pack、value + generic 实参、opaque 返回、constrained/extended existential、dynamic Self、非 key 父级参数。 + - requirement subject(`A` / `A.Element`)用携带 written-args 绑定环境的嵌套 builder + **自举解码**,dependent member 经 `swift_getAssociatedTypeWitness` 解析。 +- **落地模块**:`SwiftInspection`(`RuntimeMetadataTypeBuilder`)、`MachOSwiftSectionC` + (补桥 `swift_checkMetadataState` / `swift_getTupleTypeMetadata` / `swift_getFunctionTypeMetadata` + + weak 的 extended 变体 / `swift_getMetatypeMetadata` / `swift_getExistentialMetatypeMetadata` + / `swift_getExistentialTypeMetadata`)。验证:`RuntimeMetadataTypeBuilderTests` 往返 parity + (`_mangledTypeName(T.self)` → demangle → builder → 与 `T.self` 指针相等),覆盖标准库泛型、 + 结构类型、ObjC、约束泛型 PWT、assocty witness、嵌套泛型父级实参与 typed error 拒绝。 +- **文档**:[../Evolutions/0012-in-process-metadata-type-builder.md](../Evolutions/0012-in-process-metadata-type-builder.md)、 + [TaskReports/2026-08-30-runtime-metadata-type-builder.md](TaskReports/2026-08-30-runtime-metadata-type-builder.md)。 +- **对应版本**:未发布(0.17.0 之后)。 + +--- + +## 53. TypeNameResolvable 角色化拆分(提案 0015) + +- **时间段**:2026-09-01,单日单批。 +- **动机**:用户指出 printer 外挂查询解析器的注册协议 `TypeNameResolvable`(三方法全带默认 + `nil` 实现)违反 ISP 与 OCP,核实成立——没有任何真实 provider 实现全部三个方法,且每加 + 一个查询能力都要修改公共契约并波及继承链。 +- **关键决策**:拆成空标记协议 `TypeNameResolving` + 三个**无默认实现**的单方法角色协议 + (`ModuleNameResolving` / `CImportedNameResolving` / `OpaqueTypeResolving`),签名漂移 + 从静默脱钩变回编译期报错;printer 注册时按角色分箱(`as?` 不进打印热路径,`moduleName` + 查询不再逐个问只会回 `nil` 的 opaque provider);`SwiftInterfaceBuilderExtraDataProvider` + 与 resolver 概念解耦为纯生命周期钩子(用户点破「继承标记协议」与原病灶同构),setup-only + provider 合法化;旧协议删名不留 typealias(别名会让旧 conformer 静默编译通过但永远不被 + 调用)。消费端聚合接口 `NodePrintableDelegate` 有意保持胖——printer 是唯一 conformer 且 + 真要回答全部查询。 +- **落地模块**:`SwiftPrinting`(角色协议 + 注册分箱)、`SwiftInterface`(provider 协议 + 解耦 + 按能力转发)、`TypeIndexing`(conformance 声明)。验证:构建绿, + `SwiftInterfaceTests` 131/131(含字节级 interface 快照)+ `SwiftDumpTests` / + `SwiftSectionCommandTests` 97/97,输出逐字节不变。 +- **文档**:[../Evolutions/0015-type-name-resolver-role-split.md](../Evolutions/0015-type-name-resolver-role-split.md)、 + [Modules/SwiftInterface.md](Modules/SwiftInterface.md)(`SwiftInterfaceBuilderExtraDataProvider` 条目同批改写)。 +- **对应版本**:未发布(0.17.0 之后)。 + +--- + +## 54. Interface 只打印导出声明(提案 0016) + +- **时间段**:2026-09-02,单日单批。 +- **动机**:用户要求「SwiftInterface 只打印 exported 的方法和类型」。提案 0008 只在成员上打 + `// not exported` 标注,类型 / 协议 / 扩展层面没有任何导出判断,打印链路也没有过滤钩子。 +- **关键决策**:过滤放**打印期**(用户选定;模型完整、diff / RuntimeViewer 不受影响), + `SwiftDeclarationPrintConfiguration.printExportedDeclarationsOnly` + CLI `--exported-only`,只做 + interface。语义仍是 export trie 事实:`false` 才删、`nil` 一律留。类型 / 协议按描述符符号裁决, + **先反查描述符 offset 处的符号、重整名只兜底且拒绝 `.extension` 上下文**——第一版纯重整名把带约束 + 扩展里的公开嵌套类型误删(编译器只 mangle 扩展自己的 requirement,模型节点带完整签名)。扩展靠 + `printRoot` 从索引器表算出的 `ExportFilterScope` 裁决(strip 过的镜像里未导出类型零符号,符号推不出 + 「本镜像内」);普通扩展被清空整块删、conformance 扩展留 `{}`。三个打印入口拆成「过滤壳 + builder + 体」,被过滤定义不发 print 事件,被清空的扩展发成对事件。存储属性按 accessor 判定(用户选定)。 +- **落地模块**:`SwiftPrinting`(`+ExportFilter.swift` 新文件 + 三入口 + 成员 / 字段循环)、`SwiftInterface` + (`printRoot` 装 scope + 全局块过滤)、`swift-section`(flag)。验证:三套 22 测试全绿(`SymbolTestsCore` + 端到端 12 例 + 即时编译 library-evolution fixture 7 例覆盖 `internal` 形态 + CLI 3 例);fixture 全量交叉验证 + 378 处标注声明零残留、新增行仅 `{` → `{}`;`SwiftInterfaceTests` / `SwiftSectionCommandTests` / + `SwiftDiffingTests` 回归绿,默认输出逐字节不变。 +- **文档**:[../Evolutions/0016-exported-only-interface.md](../Evolutions/0016-exported-only-interface.md)、 + [ExportedOnlyInterfaceFiltering.md](ExportedOnlyInterfaceFiltering.md)、 + [TaskReports/2026-09-02-exported-only-interface.md](TaskReports/2026-09-02-exported-only-interface.md)、 + Glossary 新术语「exported-only 过滤」。 +- **对应版本**:未发布(0.17.0 之后)。 + +--- + +## 55. 依赖闭包下沉为 MachODependencies 模块(提案 0017) + +- **时间段**:2026-09-02,单日单批。 +- **动机**:用户要求「把目前的依赖闭包抽出来,方便给其他功能使用」。仓库里有两套互不复用的「找依赖二进制」 + 实现:`SwiftLayout` 的传递闭包(BFS + bare name 去重 + cache 一次性索引,遍历与定位器文件私有)与 + `SwiftInterface.SwiftInterfaceBuilderDependencies`(一层直接依赖、按 install path 精确匹配,供 TypeIndexing)。 +- **关键决策**:两套合并(用户选定),落点为本仓库新底层 target `MachODependencies`(用户选定;备选 sibling 包 + MachOKitExtensions),只依赖 MachOKit + MachOKitExtensions,由 `MachOFoundation` re-export。API:`DependencySearchPath`、 + `DependencyLoadName.bareImageName(of:)`、`DependencyLocating` + `InProcessDependencyLocator` / `FileDependencyLocator`、 + `DependencyClosure`(`.direct` / `.transitive`,顺序是契约,未解析进 `unresolvedLoadNames`,坏搜索路径进 + `searchPathLoadFailures`,不抛不记日志——模块在事件层之下)。匹配规则取两侧并集:**install path 精确优先,bare name + 排序兜底**(`DyldCacheImageSearchMode.matchRank`),消除旧 SwiftLayout 定位器在 macOS cache 上可能先选中 Catalyst + SwiftUI 的错配;fat 显式文件取 root 同架构 slice。SwiftInterface **保持只取直接依赖**(TypeIndexing 成本随清单线性增长), + 并顺带修掉其 `MachOImage` 版把完整 load path 喂给按 bare name 匹配的 `MachOImage(name:)`、从诞生起解析为空的静默 + bug(仓库内与下游均无调用方)。旧名 `LayoutDependencySearchPath`(typealias)、`DependencyPath`(`searchPath` 转换) + 与 `init(machO:paths:eventHandlers:)` 标 deprecated 保留一个版本。 +- **落地模块**:`MachODependencies`(新,5 文件)、`MachOFoundation`(re-export)、`SwiftLayout`(三个工厂改薄包装 + + `dependencyClosure(_ closure:)`)、`SwiftDeclarationRendering`(`StaticLayoutDependencyResolution` 关联值换型)、 + `SwiftInterface`(薄包装 + `init(closure:)` + `unresolvedLoadNames`)、`swift-section`(`--resolve-c-module-names` + 精确报告未解析依赖)。测试:新 `MachODependenciesTests`(归一规则与 `MachOImage(name:)` 契约、direct / transitive / + BFS 前缀 / 去重 / 未解析 / 坏路径 / 自定义定位器、宿主 cache 精确优先与 Catalyst 降级)、 + `SwiftInterfaceBuilderDependenciesTests`(image 版非空回归、direct 语义、`init(closure:)` 保留遍历);既有 + `DependencyClosureLayoutTests` 端到端不动。验证:全量 1612 测试仅 2 个已知 flaky;带布局注释的 dump / interface + 对 SwiftUI / SwiftUICore / SwiftData / Combine 双侧 8 组逐字节一致;同依赖版本下耗时持平(首轮 2.5× 的「回归」 + 是候选 scratch 解析到更新的 swift-demangling 0.6.1 / FrameworkToolbox 0.11.0 所致;二分确认为 swift-demangling 0.6.1 的 + `StackSafeExecutor` QoS 改动,且默认 dump / interface 路径同样慢 3–4 倍,升级前须上游先修——教训进 AGENTS.md)。 +- **文档**:[../Evolutions/0017-macho-dependencies-module.md](../Evolutions/0017-macho-dependencies-module.md)、 + [Modules/MachODependencies.md](Modules/MachODependencies.md)、 + [TaskReports/2026-09-02-macho-dependencies-module.md](TaskReports/2026-09-02-macho-dependencies-module.md)、 + Glossary 新术语「bare image name」「dependency closure」、AGENTS.md 模块图与条目、 + `StaticLayoutDependencyClosure.md` 迁移指引。 +- **对应版本**:未发布(0.17.0 之后)。 + +--- + +## 56. ABI 层自包含(提案 0018) + +- **时间段**:2026-09-03。 +- **动机**:`MachOSwiftSection`(ABI 模型)反向依赖符号索引——五个描述符的 Layout 里是 + `RelativeDirectPointer`,`Symbols` 的 `Resolvable` 实现走 `SymbolIndexStore.shared`, + 一次 ABI 访问就触发整镜像的符号扫描与 demangle;`SymbolOrElementPointer` 又把 `MachOSymbols.Symbol` + 带进每一种上下文指针。下游 MachOKitUI 为此把进程全局开关 `resolvesSymbolUsingIndexStore` 强行置 + false。顺带发现 `ReadingContext` 那条腿在把机器码当 `Symbols` 结构体读(红测试:context 腿 + `offset` 为 -2999674702252736512,MachO 腿 5624)。 +- **关键决策**:自包含到包图级别(用户定);描述符只暴露 `implementationOffset` / + `implementationAddress(in:)`,符号归属作为扩展上移 `SwiftInspection`;`Symbol` / `Symbols` / + `SymbolOrElement` 下沉 `MachOResolving`,`MachOSymbolPointers` 并入 `MachOPointers`;新增底层伞模块 + `MachOBase`(163 个文件的一行 import 替换);`Demangling` 依赖一并摘掉(前缀判断与 `__C` 常量本地化, + `ManglingPrefixTests` 钉住等价);async 版 `symbols(offset:)` 删除而非废弃(async 上下文会优先绑定它)。 +- **落地模块**:`MachOResolving`、`MachOPointers`、`MachOBase`(新)、`MachOSymbols`、`MachOFoundation`、 + `MachOSwiftSection`、`SwiftInspection`、`SwiftDeclaration`、`SwiftDump`、`MachOFixtureSupport`, + `MachOSymbolPointers` 删除;16 个 target 补上原本只靠传递拿到的 `MachOFoundation` 声明。 +- **关联文档**:[提案](../Evolutions/0018-self-contained-abi-layer.md)、 + [SelfContainedABILayer.md](SelfContainedABILayer.md)、 + [TaskReports/2026-09-03-self-contained-abi-layer.md](TaskReports/2026-09-03-self-contained-abi-layer.md)。 +- **对应版本**:0.18.0(破坏性 API 变更,见 Changelog)。 + +## 2026-09-03 大栈任务执行器接入与跨版本并行(提案 large-stack-executor-and-cross-version-parallelism;节号落地时取) + +- **时间段**:2026-09-03。 +- **动机**:用户问「整个库都使用 async 环境是否可行」。调研结论:这个库的 async 是签名上的 async, + 真正的开销在打印路径——每次 `printSemantic` 都跳到 swift-demangling 的 8 MB 大栈线程再用信号量 + 停住协作线程(release 每次 8–21 µs,1.14–2.28×);索引侧已用同步 `withLargeStack` 摊掉,打印循环 + 因为是 async 包不住。多版本 `prepare` 串行而各版本彼此独立。全库 async 化不是答案(撞 `deinit` / + getter / `Hashable` 不能 `await`、MachOKit 同步、`Node` 非 `Sendable`,且协作线程 512 KB 让探测 + 100% 不过)。 +- **关键决策**:执行器归上游(swift-demangling 提案 0014,随 0.6.3 发版:`StackSafeExecutor.taskExecutor`, + 16 MB 线程、与 8 MB 跳转池分池共码);本库在库入口自装偏好(`MachOSymbols.LargeStackTaskExecution.run`), + 宿主零改动;macOS 15 以下静默回退;跨版本并行默认开、窗口取核数(`prepare(maximumConcurrentPreparations:)`、 + CLI `--jobs`),版本内并行不做(MachOKit 共享 FileHandle);pin 抬到 0.6.3 跳过 0.6.1。 +- **落地模块**:`MachOSymbols`(`LargeStackTaskExecution`)、`Utilities`(`concurrentMap(maximumConcurrency:)`)、 + `SwiftIndexing`、`SwiftInterface`、`SwiftPrinting`、`SwiftDump`、`swift-section`。 +- **关联文档**:[提案](../Evolutions/0019-large-stack-executor-and-cross-version-parallelism.md)、 + [LargeStackTaskExecutorAdoption.md](LargeStackTaskExecutorAdoption.md)、 + [TaskReports/2026-09-03-large-stack-executor-and-cross-version-parallelism.md](TaskReports/2026-09-03-large-stack-executor-and-cross-version-parallelism.md)、 + Glossary 新术语「large-stack executor(大栈执行器)」。 +- **对应版本**:0.19.0(依赖下限 swift-demangling ≥ 0.6.3,见 Changelog)。 + +--- + +## 2026-09-06 vtable 槽归属改用 method descriptor 符号(提案 vtable-slot-attribution-via-method-descriptor-symbols;节号落地时取) + +- **时间段**:2026-09-06。 +- **动机**:用户在 Hopper 里看 `SwiftUI.GraphHost`(iOS 18.5 simruntime 的 SwiftUICore)的 vtable, + 与 `dump` 输出对不上。查证属实:槽 26–29 四条全错,真值是 `instantiateOutputs` / + `uninstantiateOutputs` / `timeDidChange` / `isHiddenForReuseDidChange`,dump 打的是 + `isHiddenForReuseDidChange` 加三条属于嵌套 struct `GraphHost.Data` 的协程 resume 函数。根因是归属 + 主源选错——靠实现地址反查符号,而 identical code folding 把字节相同的函数体折叠到一个地址 + (`0x9330` 上 2878 个符号),这个映射没有逆;叠加 `first(of: .class)` 把嵌套类型的成员认作本类的。 + 影响面:171 个非泛型带 vtable 的类 / 512 个槽里,16.2% 的槽实现地址上有多个符号,14.1% 与别的槽 + 共享同一地址(必然至少错一个)。 +- **关键决策**:归属主源改用 method descriptor 自身的 `Tq` 符号(每成员一个、位于 descriptor 自身 + 地址、ICF 免疫;提案 0006 已确认这条性质,但只当否定证据用),实现地址反查降级为回退。**只用于类 + 自己的 `MethodDescriptor`**——override descriptor 指向的是父类 descriptor,用那个身份会让 + `override` 关键字整个消失(joinKey 匹配的是本类成员符号),实测撤回。协议侧同类修改也撤回: + `base conformance descriptor` 这类要求描述符没有 entity 节点,按声明上下文匹配会整批丢弃 + (SwiftUICore 协议输出 1033 行退化)。不可归属槽保留猜测名但加注归属不确定;implementation 为 null + 的槽是 ABI 墓碑(metadata 绑到 `swift_deletedMethodError`),打印 `Tq` 还原的名字并注明无实现。 +- **落地模块**:`SwiftInspection`(`Descriptor+MethodDescriptorSymbols`、`Node+DeclarationContext`)、 + `SwiftDump`(`ClassDumper`)、`SwiftDeclaration`(`TypeDefinition` / `OverrideSymbolMatcher`)、 + `SwiftDeclarationRendering`(两条新注释)。 +- **关联文档**:[提案](../Evolutions/0020-vtable-slot-attribution-via-method-descriptor-symbols.md)、 + [TaskReports/2026-09-06-vtable-slot-attribution.md](TaskReports/2026-09-06-vtable-slot-attribution.md)。 +- **对应版本**:未发布(待 bump)。 + +--- + ## 维护约定 1. **每个非平凡批次结束时必须在本文追加/更新一节**(新工作弧新增一节;延续既有弧则在该节 diff --git a/Documentations/Internal/ReadingContextAbstraction.md b/Documentations/Internal/ReadingContextAbstraction.md index 49747a21..e5176372 100644 --- a/Documentations/Internal/ReadingContextAbstraction.md +++ b/Documentations/Internal/ReadingContextAbstraction.md @@ -288,6 +288,47 @@ Sources/ 4. **Backward Compatible**: Existing code continues to work unchanged 5. **Consistent with Swift Runtime**: Follows established patterns from Apple's implementation +## Model Coverage Completion Pass (2026-05) + +When the abstraction landed, only ~15 of the ~60 files under +`Sources/MachOSwiftSection/Models/` that expose a MachO-based API also exposed a +`ReadingContext` overload — any caller adopting the abstraction had to drop back +to the MachO/InProcess APIs for the rest. A dedicated completion pass (original +spec: `docs/superpowers/specs/2026-05-02-reading-context-api-design.md`, now in +git history only) added the missing overloads across all of `Models/`, purely +additive, batched per sub-directory with one passing build per batch. The +mechanical substitution rule: every `machO.read*(offset: o)` becomes +`context.read*(at: try context.addressFromOffset(o))`, every +`pointer.resolve(from: o, in: machO)` becomes +`pointer.resolve(at: try context.addressFromOffset(o), in: context)`; local +`Int` offset arithmetic stays unchanged — the translation to a context-specific +address happens at the read site. + +One capability was added to support runtime-pointer-returning methods +(`metadataAccessorFunction` is the canonical case, which only makes sense when +the reader is mapped into the current process): + +```swift +extension ReadingContext { + /// nil unless the context is mapped into the current process. + public func runtimePointer(at address: Address) throws -> UnsafeRawPointer? { nil } +} +// InProcessContext returns the address itself; MachOContext returns +// machO.ptr + address when its MachO is a MachOImage, else nil. +``` + +Two deliberate decisions worth keeping in mind: + +- `runtimePointer(at:)` is an **extension method with a default, not a protocol + requirement** — keeping it out of the requirement set makes the addition + non-breaking for external conformers. A future conformer that needs it must + override the extension, not implement a witness. +- The `machO as? MachOImage` runtime cast inside `MachOContext`'s override is + unavoidable: the generic parameter is unconstrained at the conformance site, + so a `where MachO == MachOImage` specialization would not produce a witness. + For `MachOContext` the method returns `nil`, matching the + pre-existing MachO overload's behavior. + ## Future Considerations 1. **Async Support**: Add `AsyncReadingContext` for async reading operations diff --git a/Documentations/Internal/ReviewAdjudications.md b/Documentations/Internal/ReviewAdjudications.md index 53c05951..82277b9c 100644 --- a/Documentations/Internal/ReviewAdjudications.md +++ b/Documentations/Internal/ReviewAdjudications.md @@ -237,7 +237,7 @@ - **裁决**:误报 / 有意行为,不修(2026-08-27)。 - **发现**:`InterfaceUnionWalker.matchAcrossVersions` 用 `seen.insert(elementKey).inserted` 门控 emission,而它替换掉的 `SwiftDiffableInterfaceRenderer.diffMembers`(main `:428`)与 `matchByKey`(main `:556`)遍历新侧全部元素——identity key 碰撞时旧路径两个都渲染,新路径只渲染第一个。review 据此判定 `swift-section diff --interface` 会静默少渲染成员。 -- **复现 / 是否误报**:行为差异属实,但**定性错误**。walker 的文档注释明写 "Keys are first-wins within each version (emission included…) mirroring `ABIDiffer.keyed`",`draft-unify-interface-renderers.md` 的决策日志(2026-08-26)专条记载:实现中确认旧 diff 发射循环对同 key 重复项重复发射,与其**自身查表字典**和注释声明的 first-wins 相矛盾,判定为漏网,统一后连发射也 first-wins;恰好依赖旧行为的测试 `unrenderableHeaderIsReportedAsAnEvent` 同批改成 replace 注入。旧行为也并非「更正确」——第二个重复项是与 first-wins 的旧侧条目**错配比较**后发射的。 +- **复现 / 是否误报**:行为差异属实,但**定性错误**。walker 的文档注释明写 "Keys are first-wins within each version (emission included…) mirroring `ABIDiffer.keyed`",`0014-unify-interface-renderers.md` 的决策日志(2026-08-26)专条记载:实现中确认旧 diff 发射循环对同 key 重复项重复发射,与其**自身查表字典**和注释声明的 first-wins 相矛盾,判定为漏网,统一后连发射也 first-wins;恰好依赖旧行为的测试 `unrenderableHeaderIsReportedAsAnEvent` 同批改成 replace 注入。旧行为也并非「更正确」——第二个重复项是与 first-wins 的旧侧条目**错配比较**后发射的。 - **与 main 基线对比**:行为变化确由本 PR 引入,但项目已把旧行为定性为 bug,故不是回归。 - **既往修复**:无。这是首次把发射对齐 first-wins 的 deliberate 改动。 - **残余关切(不构成缺陷)**:`--interface` 模式直接从 live model 渲染、不经 `ABIDiff`,所以 `keyCollisions()` 诊断在该视图无处输出。**main 同样如此**,属可选增强而非本 PR 缺陷。 @@ -270,3 +270,172 @@ - **为什么仍然修**:改动是严格更安全的收紧,与本 PR 自己在相邻代码里声明的不变量一致,且无碰撞时零行为差异;留着一个「已知按名字查、只是恰好没人能触发」的查询是下一次回归的种子。 - **测试**:`FinalMemberRecoveryTests.sameNamedPrivateClassesGetIndependentFinalVerdicts` 是**防回归钉子而非复现**(夹具 `PrivateDoppelgangerClass` 对,一边非 final、一边 `final`),测试注释与本条目互引。 - **复审条件**:① 在真实框架二进制上观察到同名私有类型且其中一方贡献了 `Tq` 或存储属性访问器符号;② 上游工具链改变私有类型的符号发射策略(例如为 `private` class 也发 `Tq`)——届时本条的三条理由需重测。 + +--- + +## A23 — `MachOSwiftSection` 对 `FoundationToolbox` / `SwiftStdlibToolbox` / `MachOReading` 的 import 未在 manifest 声明(PR #121 review 发现 I,部分) + +- **裁决**:本 PR 只补新丢的 `.target(.Utilities)`;其余三个模块的未声明 import **延后**到独立清理批次(2026-09-04)。 +- **发现**:`Sources/MachOSwiftSection` 里 `import FoundationToolbox` ×5、`import SwiftStdlibToolbox` ×6、`import MachOReading` ×5,target 依赖列表里都没有;今天能编是因为 `MachOBase` 再导出 `MachOReading`,而 FrameworkToolbox 的两个模块经 `MachOKitExtensions` 等传递可见。 +- **复现 / 是否误报**:属实。`git diff f3782248 -- Package.swift` 证明 `Utilities` 是本 PR 从依赖列表里删掉的(`MachOFoundation` → `MachOBase` 替换时一并丢失),其余三个在 `next` 上就没声明。 +- **与 main 基线对比**:`Utilities` 一条为本 PR 引入(已补);其余为既有状态,非回归。 +- **为什么延后**:整仓同类问题不止这一个 target(PR #121 自己就补了 16 个 target 的 `MachOFoundation` 声明),应当一次性用「每个 import 都有直接声明」的脚本扫全仓并统一修,而不是每个 PR 顺手补几条。零行为影响,无用户可见后果。 +- **复审条件**:① SwiftPM 或 Xcode 启用 explicit modules 后编译失败——届时立刻修;② 做全仓 import / 依赖一致性清理时一并处理。 + +--- + +## A24 — 约 14 处旧写法 `implementation.resolveDirectOffset(from: offset(of:))` 未迁移到 `implementationOffset`(PR #121 review 发现 J,全量部分) + +- **裁决**:本 PR 只统一 `ProtocolConformanceDumper` 一个文件(同一函数内新旧两种写法并存,见发现原文);`ClassDumper`(229/272/287/318/332/543 附近)、`TypeDefinition`(244–257 四个循环)、`ExtensionDefinition`(233/235/241)的迁移**延后**为独立批次(2026-09-04)。 +- **发现**:全仓 19 处 `implementation.resolveDirectOffset` / `defaultImplementation.resolveDirectOffset`,5 处是新 accessor 本体,其余散落在上述文件,都是 `!isNull` 守卫 + 裸算术的旧形状。 +- **复现 / 是否误报**:属实,但**语义等价**——新 accessor 的实现就是 `guard isValid` + 同一条 `resolveDirectOffset`,两种写法在每个输入上给出相同结果;纯风格问题,不改输出。 +- **与 main 基线对比**:旧写法在 `next` 上就存在,本 PR 只是新增了权威取值口而没有收编。 +- **为什么延后**:14 处分布在三个模块的热路径上,每处都要过一遍渲染 A/B 才敢合;与本 PR 的目标(分层)无关,捆进来只会拖大 review 面。已确认不产生功能分裂:两种写法等价,分裂只是「读代码时要认两种形状」。 +- **复审条件**:单独开一个「收编 `implementationOffset`」的清理批次(轻量档提案即可),逐字节 A/B 后合入;届时本条关闭。 + +--- + +## A25 — 「六个 `import MachOFoundation` 冗余」(PR #122 review 发现 F11,**误报**) + +- **裁决**:误报(2026-09-04)。 +- **发现**:审查者依据「`Sources/MachOSwiftSection/Exported.swift` 是 `@_exported import MachOFoundation`」判定 `SwiftDump/Dumpable/*+Dumpable.swift` 六处新增的 `import MachOFoundation` 多余。 +- **为什么是误报**:提案 0018 把 ABI 层的再导出收窄到 `MachOBase`(注释原文 "the ABI layer deliberately stops here"),`LargeStackTaskExecution` 住在 `MachOSymbols`,只有 `MachOFoundation` 再导出它;去掉那六行编译不过。审查者读的是 0018 之前的状态。 +- **附带子主张**:① `MachOSymbols` target 未声明 `FoundationToolbox` product 而 `LargeStackTaskExecution.swift` import 它——属实但基线既有(`SymbolIndexStore.swift` / `Symbol.swift` 同样如此),归 A23 同类清理批次;② `AnySwiftEvolutionInterfaceBuilder.swift` 新加的 `import Utilities` 冗余(`MachOSwiftSection → MachOBase → Utilities`)——属实,已删。 +- **复审条件**:无。 + +--- + +## A26 — 打印器四个逐定义入口每次调用都包一层 `LargeStackTaskExecution.run`(PR #122 review 发现 F13) + +- **裁决**:不修(2026-09-04)。 +- **发现**:`printTypeDefinition` / `printProtocolDefinition` / `printExtensionDefinition` / `printDefinition` 每次调用读一次 `isEnabled`(`@Mutex`,`os_unfair_lock`)并进一次 `withTaskExecutorPreference`;全仓 26 个 `run` 调用点,新增入口要同步维护。 +- **为什么不修**:效率论据被本 PR 自己的数据推翻——无竞争 `os_unfair_lock` 约 20 ns,十万次合计约 2 ms,而实测整体快 16–23%;已在执行器上的嵌套 `withTaskExecutorPreference` 不切换(`nestedRunsStayOnTheSameThread` 钉住)。包在逐定义入口是有意的:RuntimeViewer 逐类型导出绕过 `printRoot`,只包 `printRoot` 会漏掉它。 +- **复审条件**:profiling 显示 `run` 的开销在某条路径上可观;或出现第 27 个入口时考虑把「入口 = 包裹」写成 lint 检查。 + +--- + +## A27 — 新 `Collection.concurrentMap(maximumConcurrency:)` 与既有 `Array.concurrentMap(_:)` 同名而语义不同(PR #122 review 发现 F10) + +- **裁决**:不修(2026-09-04)。 +- **发现**:`Sources/Utilities/ConcurrentMap.swift` 的 `concurrentMap(_:)` 是 `DispatchQueue.concurrentPerform` 的同步阻塞版;新函数是 async、窗口化、可抛错。参数标签不同、无重载歧义,纯可读性。 +- **为什么不修**:两者的调用形态(`await` + `try` + `maximumConcurrency:` 标签)已把区别写在调用点上;改名或合并文件是纯搬动。既有同步版零调用方(2026-09-03 调研已记录),更合适的动作是下次清理批次删掉它。 +- **复审条件**:同步版被删或被重新启用时一并统一命名。 + +--- + +## A28 — `LargeStackTaskExecution.run` 未转发 `isolation: isolated (any Actor)? = #isolation`(PR #122 review 发现 F12) + +- **裁决**:不修(2026-09-04)。 +- **发现**:标准的「透传隔离」写法会加一个 `#isolation` 参数再转给 `withTaskExecutorPreference`;`run` 没有。 +- **为什么不修**:`body` 是非 `@Sendable` 闭包,在 actor 隔离上下文里字面量继承调用方隔离,实现说明里「主 actor 保持自己的 executor」仍然成立;差别只是多一次跳转。库 target 未开 SE-0461,从 `@MainActor` 调用本就离开主 actor(见实现说明「主 actor 调用方」)。 +- **复审条件**:库 target 开启 `NonisolatedNonsendingByDefault` 时重议。 + +--- + +## A29 — `@Suite(.serialized)` 不足以保护进程级开关 `isEnabled`(PR #122 review 发现 F4) + +- **裁决**:不修(2026-09-04)。 +- **发现**:`disabledRunsTheBodyOnTheCallersExecutor` 翻转 `LargeStackTaskExecution.isEnabled`,`.serialized` 只序列化本套件;其他套件与之并行时在翻转窗口内静默失去执行器。 +- **为什么不修**:只影响那几毫秒里其他套件跑在哪条线程上,不影响任何断言的正确性(全仓其他套件对执行器不敏感,AGENTS.md Test Environment 节写明)。审查者称「测试 trap 会跳过 defer」不成立——Swift Testing 的 `#expect` 失败不 trap,`defer` 正常恢复。横向排查:35 个 `.serialized` 套件里只有这一个翻转进程级开关。 +- **复审条件**:出现第二个断言线程身份的套件。 + +--- + +## A30 — 执行器关闭 / 不可用时并行窗口 = 核数会占满协作线程池(PR #122 review 发现 F8) + +- **裁决**:不修(2026-09-04)。 +- **发现**:`StackSafeExecutor` 探测失败时用 `DispatchSemaphore.wait()` 阻塞调用线程;macOS 14 或宿主关掉开关时,N 个并行 `prepare` 同时阻塞 N 条协作线程,宿主其他 async 工作会饿住(不会死锁:8 MB 跳转池是另一个池)。 +- **为什么不修**:这正是接入前每一次 `prepare` 的行为,并行只是把它乘以窗口;关掉执行器是宿主的显式选择(A/B 与计时配置),macOS 14 以下的用户面很小。宿主可用 `maximumConcurrentPreparations: 1` / `--jobs 1` 回到旧形态。 +- **复审条件**:有 macOS 14 宿主反馈饿死;届时可让 `run` 在不支持时把窗口自动收窄到 1。 + +--- + +## A31 — lineage / JSON 路径默认并行窗口取核数,峰值内存从 1 个索引镜像变为核数个(PR #122 review 发现 F6) + +- **裁决**:保持(2026-09-04,用户裁定)。 +- **发现**:`ABISnapshotInputLoader.loadDocument` 索引完即丢 builder,旧循环峰值一个镜像;新默认 `min(N, 核数)` 个(约 32 MB / 版本)。 +- **为什么保持**:提案第二轮澄清用户选「默认并行上限取核数」,`--jobs` 帮助文本写明代价;`--interface` 路径本来就全部常驻。 +- **复审条件**:出现内存受限的宿主场景(例如 CI 上几十个版本)时给 lineage 路径单独的默认值。 + +--- + +## A32 — `run` 与 `isSupported` 各写一遍平台 + 可用性门(PR #122 review 发现 F9) + +- **裁决**:不修(2026-09-04)。 +- **发现**:`#if canImport(Darwin)` + `#available(macOS 15…)` 在两处重复。 +- **为什么不修**:`#available` 必须在使用 `StackSafeExecutor.taskExecutor` 的词法位置出现,编译器不接受「`isSupported` 为真」作为可用性证明;把 `isSupported` 加进 `run` 的条件只是第三次重复。两处各有必要,已在 `run` 的注释说明。 +- **复审条件**:Swift 提供可用性谓词的抽象手段。 + +--- + +## A33 — `TypeIndexing.TypeDatabase.index` 的 task group 用 `addTask`,取消后仍提交剩余模块(PR #122 review 发现 1 的横向同类) + +- **裁决**:延后(2026-09-04)。 +- **发现**:`Sources/TypeIndexing/TypeDatabase.swift:76` 与 `concurrentMap` 修复前同形;基线既有,非本 PR 引入。 +- **为什么延后**:正确修法是 `addTaskUnlessCancelled` + 注册前 `Task.checkCancellation()`(否则取消会把残缺索引静默登记进去),而 `index(dependencies:moduleFilter:)` 直接构造 `SDKIndexer` / `ModuleInterfaceIndexer`(需要 SourceKit 与 SDK),没有注入缝可以写单元级复现测试;按「修复必带能变红的测试」规则,先补注入缝再修。 +- **复审条件**:`TypeDatabase` 获得 indexer 注入缝时一并修,或 GUI 宿主报告取消 `--resolve-c-module-names` 后 CPU 仍被占用。 + + +--- + +## A34 — dump 的 vtable 段落输出 `class func static X.classMethod()`(PR #123 review 发现 11,**已有裁决**) + +- **裁决**:不修(2026-09-07,沿用 2026-07 的既有决定)。 +- **发现**:`dumpMethodKeyword`(`ClassDumper.swift:529`)对类型级成员输出 `class`,而 demangler 对 `.static` 节点无条件打印 `static ` 前缀,合起来是自相矛盾的 `class func static ...`;新基线 `vTableEntryVariantsSnapshot.1.txt:142` 收录了这一行。 +- **复现 / 是否误报**:输出属实,但**不是本 PR 引入的缺陷**。本 PR 只是让该槽第一次正确解析到 `classMethod`(基线上它错解析成一个 subscript setter),既有形态首次出现在这个 fixture 上。 +- **与 main 基线对比**:形态在 `next` 上就存在,只是此 fixture 未触发。 +- **既往修复 / 当时为什么这样做**:[ClassMemberKeywordRecovery.md](ClassMemberKeywordRecovery.md) 第「dump 路径里 override table 的 `static` 前缀不动」节(修 issue #99 时写的)已明确裁决:dump 输出里的 `static` 来自 **demangler 对符号的忠实还原**,「dump 本就是符号列表而非可编译 Swift 源码,改掉它等于篡改 demangle 结果,明确不动」;该文原文即写着「原输出形如 `static func static Foo...`,现为 `class func static Foo...`」。理由今日仍成立。 +- **复审条件**:dump 输出的定位从「符号列表」改为「可编译 Swift 源码」;或 demangler 提供抑制 `static` 前缀的 `DemangleOptions`(目前没有)。 + +--- + +## A35 — `entityNodeKinds` 未收录 `.boundGenericFunction`(PR #123 review 发现 12,**误报**) + +- **裁决**:误报(2026-09-07)。 +- **发现**:审查认为 `Node+DeclarationContext.swift:25` 的 `entityNodeKinds` 漏掉 `.boundGenericFunction`(`NodePrinter.swift:431` 与 `.function` 同样走 `printEntity(hasName: true)`),导致这类符号走不到 entity、被当作候选丢弃。 +- **为什么是误报**:`.boundGenericFunction` 的子节点布局是 `[functionOrConstructorNode, typeList]`(`Demangler.swift:1288`:`createNode(kind: .boundGenericFunction, children: [n, args])`;`NodePrinter.swift:1954` 也是这么解包的),**第一个子节点不是声明上下文**,而是 `.function` / `.constructor` 节点本身。把它排除在 entity 集合之外、让遍历**穿过**它落到里面那个 `.function` 上,拿到的才是正确的上下文——收录它反而会把函数节点当成上下文。当前写法正确。 +- **与 main 基线对比**:新增代码,无基线对照。 +- **复审条件**:上游改变 `.boundGenericFunction` 的子节点布局。 + +--- + +## A36 — `VTableSlotAttributionTests` 的 fixture 前提硬失败(PR #123 review 发现 13,**误报 + 有意设计**) + +- **裁决**:不修(2026-09-07)。 +- **发现**:审查提出两点——(a) `swiftc -emit-library` 不带 `-target`,在 Intel 主机上产出的 fat 文件没有 arm64 slice,`#require(machOFile, "fixture unexpectedly missing an arm64 slice")` 会让 5 个测试全红;(b) `foldedImplementationAddress` 用 `#require` 断言折叠发生,linker 不再折叠时 3 个测试因环境原因变红。 +- **复现 / 是否误报**:(a) **误报**——`swiftc` 默认产出宿主架构的 **thin** 文件,`loadFromFile` 走 `.machO` 分支,`.fat` 分支根本不会走到。(b) 属实,但**是测试自己写明的设计**:文件头文档注释写着 `foldedImplementationAddress` 是「a REQUIRED premise of every test here rather than a soft check」——前提没了测试即空转,红掉正是想要的信号。 +- **与 main 基线对比**:新增测试,无基线。 +- **复审条件**:(b) 若工具链侧真的停止支持 `-deduplicate`,改为 `.enabled(if:)` 跳过而非红;届时需同时在 `KnownIssues` 侧记下「本套测试已失去前提」,不能静默跳过。 + +--- + +## A37 — 两条新注释不走 OutputTransformer token-template 机制(PR #123 review 发现 4,部分) + +- **裁决**:模板槽不修(2026-09-07,沿用本表 A14);**布尔开关待修,不在本条覆盖范围内**。 +- **发现**:`deletedMethodSlotComment()` / `ambiguousAttributionComment(foldedSymbolCount:)` 既无 `printXxx` 开关,也无 `…Transformer` 闭包槽,与 `DeclarationRenderConfiguration` 里其他每一种注释都不同。 +- **为什么模板槽不修**:A14(2026-08-23,`not exported` 注释)已就同形问题裁决——transformer 机制的价值在**带变量 token 的注释**,零参数的固定事实陈述模板化只能改文案,而文案恰是承重部分。`deletedMethodSlotComment()` 是零 token,直接适用。 +- **A14 未覆盖的两点**:`ambiguousAttributionComment(foldedSymbolCount:)` **带一个变量 token**,A14 的理由不适用;且 A14 明确写过「若需要开关,`printExportStatus` 这一个 Bool 就是全部所需表面」,而这两条注释连那个 Bool 都没有。**加开关是真缺陷**,记在 [PR #123 findings 第 4 条](../../Roadmaps/2026-09-06-pr123-review-findings.md),随修复批次处理。 +- **复审条件**:歧义注释的计数语义定稿后(见 findings 第 2 条),若宿主提出自定义措辞需求,再给它补单 token 模块。 + +--- + +## A38 — `Tq` 归属分支不做声明上下文校验(PR #123 review 发现 8) + +- **裁决**:延后(2026-09-07)。 +- **发现**:`TypeDefinition.swift:275` 与 `ClassDumper.swift:242` 的 `Tq` 分支跳过了回退路径强制的两道闸——声明上下文必须匹配本类(`demangledOverrideSymbol` / `ClassDumper.validNode` 都做)、`visitedNodes` 去重——却仍向 `visitedNodes` 追加。 +- **复现 / 是否误报**:机制属实,**但构造不出触发场景**。`attributedMemberNode` 只接受能 demangle 成 `.methodDescriptor` 的符号,而 method descriptor 在一个镜像内地址唯一,同地址出现别的类的 `Tq` 符号在实践中不成立;dyld 共享缓存的偏移规范化理论上留了口子(AGENTS.md 记有「raw 与 adjusted offset 共用一行」),本轮未能构造出实例。 +- **与 main 基线对比**:新增代码路径。 +- **为什么延后**:正确修法是加一句与回退路径同样的上下文断言(便宜),但按「修复必带能变红的测试」规则,需要先构造出能触发的镜像;在构造出来之前加断言等于加一条永远不执行的分支。 +- **复核补充(2026-09-07)**:「构造不出触发镜像」**不等于**「构造不出变红的测试」——`MethodDescriptorAttribution.memberNode(forMethodDescriptorSymbols:in:)` 收的是一个 `Symbols` 值,手造一个含别的类的 `Tq` 名字的 `Symbols` 即可在单元级复现并变红。因此「没有复现测试」不再是延后的理由,修复批次顺手加断言时把这条测试一并补上。 +- **复审条件**:修复批次加上下文断言(连同上述单元测试),或构造出跨镜像 `Tq` 偏移碰撞的真实实例。 + +--- + +## A39 — `vtableAccessorFieldNames` 按折叠地址扫描且无本类过滤(PR #123 review 发现 15,**基线既有**) + +- **裁决**:延后为独立批次(2026-09-07)。 +- **发现**:`ClassDumper.swift:561-571` 仍按实现地址收集访问器名字,内层 `for symbol in symbols` 无本类过滤、无早退,把找到的每个 `.variable` 名字都塞进集合。折叠地址上是每个访问器 descriptor ~2878 次 `demangleSymbolReference`(有 memo 缓存兜底),且会收进嵌套类型的同名字段,从而抑制本类同名字段的 `final` 标记——正是本 PR 在别处修掉的那种跨类型串味。 +- **与 main 基线对比**:`next` 上一模一样,本 PR 未触及该函数。 +- **既往修复 / 当时为什么这样做**:来自提案 0006 的 `final` 关键字还原(commit `da9b8be2`,其后 `83a4308c` 补了「有 `Tq` 就绝不标 `final`」的否定证据)。当时的证据模型就是实现地址反查,本 PR 才把「实现地址在折叠下不可逆」确立为项目事实。 +- **为什么延后**:错误方向保守(少标 `final` 而非错标),且修法与 findings 第 5 条(遍历去重)、第 6 条(快路径不白算)同属「把 `Tq` 优先的证据顺序推广到 `final` 恢复路径」,捆在一起做才不会来回改同一段。 +- **复审条件**:`final` 恢复路径的证据模型统一批次;或出现「`final` 在真实框架上被系统性漏标」的报告。 diff --git a/docs/superpowers/reviews/2026-05-06-generic-specializer-bug-review.md b/Documentations/Internal/Reviews/2026-05-06-generic-specializer-bug-review.md similarity index 100% rename from docs/superpowers/reviews/2026-05-06-generic-specializer-bug-review.md rename to Documentations/Internal/Reviews/2026-05-06-generic-specializer-bug-review.md diff --git a/Documentations/Internal/Reviews/2026-07-31-node-store-migration-review.md b/Documentations/Internal/Reviews/2026-07-31-node-store-migration-review.md index 2f43f4f1..da0004ec 100644 --- a/Documentations/Internal/Reviews/2026-07-31-node-store-migration-review.md +++ b/Documentations/Internal/Reviews/2026-07-31-node-store-migration-review.md @@ -126,6 +126,7 @@ libdispatch worker reported_size= 524 KB remaining= 523 KB >=2MB: no (HOP 每符号省 6.2 µs。值得注意的是跳转成本(615.7 ms)几乎与 demangle 本身(701.5 ms)等价——**近一半时间花在线程往返上**。主线程两组数据一致,再次确认探测通过时批量边界不产生任何作用,也印证上文对原实测表的更正。 - **`Node+.swift` 的 `printSemantic` 注释**(本次改动):原注释断言 `print(_:options:)` "runs the recursion inline against a stack floor and pays for a worker only for a tree that actually reaches it",与代码相反——它内部就是 `executeWithUncheckedSendability`,与 `execute` 是同一段逻辑,仅少了 `Sendable` 约束。注释已改为如实描述,并说明为何摊销点不在此处。**代码未动**。 - **渲染循环暂不处理**:打印侧的循环在 `SwiftDeclarationPrinter` 里,是 `async`,同步的 `withLargeStack` 无法包裹。真要摊销需要自定义一个跑在 8 MB 线程上的 `SerialExecutor`,或把打印批次改成同步——两者都是独立的重构。先量清楚真实导出中的调用次数与总开销,再决定是否值得。 + **已解决(2026-09-03,提案 `large-stack-executor-and-cross-version-parallelism`)**:走的是第三条路——上游 swift-demangling 0.6.3 提供 16 MB 线程的 `TaskExecutor`,本库的 async 入口经 `LargeStackTaskExecution.run` 把整个 task 放到它上面,探针在每个入口都通过,打印路径零跳转;见 `Documentations/Internal/LargeStackTaskExecutorAdoption.md`。 > 探针为一次性测量代码,测完已删除。若需长期守护该性质,应整理为正式 benchmark。 diff --git a/Documentations/Internal/SelfContainedABILayer.md b/Documentations/Internal/SelfContainedABILayer.md new file mode 100644 index 00000000..52d42d7a --- /dev/null +++ b/Documentations/Internal/SelfContainedABILayer.md @@ -0,0 +1,64 @@ +# ABI 层自包含:MachOSwiftSection 与符号索引分家 + +提案:[0018-self-contained-abi-layer](../Evolutions/0018-self-contained-abi-layer.md)。本文记录落地后的形态,以及那些从签名上看不出来、下次维护会踩的决策。 + +## 改了什么 + +| 之前 | 之后 | +|---|---| +| `MachOSwiftSection` 依赖 `MachOFoundation` 伞模块(含 `MachOSymbols`、`MachODependencies`)和 `Demangling`,并把伞模块整个再导出 | 只依赖新的底层伞模块 `MachOBase`(`MachOKitExtensions` + `MachOReading` + `MachOResolving` + `MachOPointers` + `Utilities`)并再导出它;`Package.swift` 上看不到符号索引与 demangler | +| 五个描述符的 Layout 字段是 `RelativeDirectPointer`,访问器 `implementationSymbols(in:)` 一调就建整镜像符号索引 | 字段是 `RelativeDirectRawPointer`,访问器 `implementationOffset: Int?`(纯指针算术)与 `implementationAddress(in context:)`;`ProtocolRequirement` 用 `defaultImplementation…` 前缀 | +| `implementationSymbols(in:)` 家族在 ABI 层 | 同名扩展在 `SwiftInspection`(`Extensions/Descriptor+ImplementationSymbols.swift`),实现是拿偏移问 `machO.symbols(offset:)`;只有 MachO 一条腿 | +| `Symbol` / `Symbols` / `SymbolOrElement` 在 `MachOSymbols`,`Symbols` 是 `AsyncResolvable` | 三个纯值类型在 `MachOResolving`;`Symbols` 不再是 `Resolvable`;`Symbol` 走索引库的 `resolve(from:in:)` 与 `resolvesSymbolUsingIndexStore` 作为扩展留在 `MachOSymbols` | +| `SymbolOrElementPointer` 独占一个 `MachOSymbolPointers` target | 并入 `MachOPointers`,target 删除 | +| `String.isSwiftSymbol` / `stripManglePrefix`、`cModule` / `objcModule` 来自 `Demangling` | 本模块自有 `hasSwiftManglingPrefix` / `strippingSwiftManglingPrefix`(`Extensions/String+.swift`)与 `CImportedModuleNames`(`Models/Mangling/`),`ManglingPrefixTests` 钉住与 demangler 一致 | + +分层由编译器守:`MachOSwiftSection` 的依赖列表里没有 `MachOSymbols` 与 `Demangling`,回归就是编译错误。 + +## 从签名看不出来的决策 + +### 为什么多了一个 `MachOBase` 伞模块 + +`MachOSwiftSection` 里 163 个文件写着 `import MachOFoundation`。把它们改成四五行显式 import 是同样的信息量却多了几百行 diff,而且以后每个新文件都要重复一遍。`MachOBase` 把「ABI 层允许看到的一切」定义成一个名字,`MachOFoundation` 变成 `MachOBase` 加符号索引加依赖解析。下游 `import MachOSwiftSection` 仍能拿到指针与 reader 类型(`MachOSwiftSection` 再导出 `MachOBase`),拿不到的只有 `MachOSymbols` 与 `MachODependencies`。 + +### 为什么 `ReadingContext` 那条腿没有符号形态 + +`ReadingContext` 没有任何符号服务可用。旧的 `implementationSymbols(in context:)` 因此落到 `Resolvable` 的默认 `context.readElement(at:)`,把函数入口的机器码按 `Symbols` 结构体(一个 `Int` 加一个数组引用)读出来。修前的红测试(`MethodDescriptorTests`,对旧 API 比对两条腿的 `offset`)给出的数字:context 腿 -2999674702252736512,MachO 腿 5624。它没崩只是因为读出的假数组指针恰好是非规范地址,运行时的 retain / release 会跳过它。三处 fixture 测试只断言「不为 nil」,永远绿。现在 context 腿给的是 `implementationAddress(in:) -> Context.Address?`,`MachOContext` 上就是偏移本身,测试断言它等于 `implementationOffset`。 + +### 为什么不给 `MachOSymbols` 留 `typealias Symbol` + +`MachOFoundation` 同时再导出 `MachOSymbols` 与 `MachOResolving`。若 `MachOSymbols` 里有 `public typealias Symbol = MachOResolving.Symbol`,所有 `import MachOFoundation` 的文件里不限定的 `Symbol` 都有歧义风险。限定名 `MachOSymbols.Symbol` 的已知调用方只有 MachOKitUI 一处(`MachOSwiftSectionDetailBuilder.swift`),改一行即可。 + +### 为什么 `symbols(offset:) async` 是删除而不是废弃 + +它与同步版逐字相同,本想标 deprecated 留一版。但 async 上下文里编译器优先绑定 async 重载并要求 `await`,`if let symbols = machO.symbols(offset:)` 这种链式条件在 `ProtocolDefinition.index` 等四处直接报错。两个重载不能共存,只能删。 + +### `Symbol.resolve(from:in:)` 为什么还在 + +`MetadataReader` 的 `MachOContext.lookupSymbol` 与 `SwiftDeclarationRendering` 的 `OpaqueType+` 仍用它,且它承载 `resolvesSymbolUsingIndexStore` 开关(MachOKitUI 会置 false)。它现在是 `MachOSymbols` 里 `Symbol` 的静态扩展方法,不再是 `Resolvable` 要求:语义是「查」不是「读」。 + +### `ResilientWitness.implementationAddress(in:)` 的两个形态 + +原有的 MachO 版是调试用的地址字符串格式化器,随 `implementationOffset` 变 `Int?` 一起变成 `String?`。新增的 context 版返回 `Context.Address?`。两者同名、靠参数类型区分(`MachOFile` / `MachOImage` 不是 `ReadingContext`,`MachOContext` / `InProcessContext` 不是 `MachOSwiftSectionRepresentableWithCache`),覆盖不变量按成员名登记一次即可。 + +### 靠再导出拿到符号模块的文件 + +`MachOSwiftSection` 不再带出 `MachOSymbols` / `MachODependencies` 后,仓库内 12 个源码文件与若干测试文件补了显式 `import MachOFoundation` / `import MachOResolving`,`Package.swift` 里 16 个 target 补上原本只靠传递拿到的 `.target(.MachOFoundation)`(`SwiftInspection`、`SwiftDump`、`SwiftDeclaration`、`SwiftIndexing`、`SwiftPrinting`、`swift-section` 与十个测试 target)。这些依赖本来就在用,只是没声明。 + +## 下游迁移 + +| 调用点 | 改法 | +|---|---| +| `try d.implementationSymbols(in: machO)` | 去掉 `try`,文件补 `import SwiftInspection` | +| `d.implementationSymbols(in: context)` | 删除;要位置用 `try d.implementationAddress(in: context)` | +| `try Symbols.resolve(from: offset, in: machO)` | `machO.symbols(offset: offset)`(`MachOSymbols`) | +| `MachOSymbols.Symbol` 限定名 | 不限定,或 `MachOResolving.Symbol` | +| `import MachOSymbolPointers` | 删除(并入 `MachOPointers`) | +| 经 `import MachOSwiftSection` 间接用 `SymbolIndexStore` 等 | 补 `import MachOFoundation` | +| `ResilientWitness.implementationOffset` | 现在是 `Int?` | + +## 验证 + +- `MachOSwiftSectionTests` 723 测试 / 161 套件全绿(五个重生成的 baseline、覆盖不变量、`ManglingPrefixTests`);全量 1617 测试仅两个已知 flaky 的墙钟并行度断言假失败,单独重跑全绿。 +- 渲染 A/B 78 对输出逐字节一致:系统 dyld cache、iOS 15.5 / 18.5 / 18.6 / 26.5 模拟器运行时、进程内 MachOImage 三条路径的 dump 与 interface。默认输出与布局注释均不经过被改动的符号路径以外的逻辑,结果符合预期。 +- 过程复盘:[TaskReports/2026-09-03-self-contained-abi-layer.md](TaskReports/2026-09-03-self-contained-abi-layer.md)。 diff --git a/Documentations/Internal/StaticLayoutDependencyClosure.md b/Documentations/Internal/StaticLayoutDependencyClosure.md index a521fa29..3471c5be 100644 --- a/Documentations/Internal/StaticLayoutDependencyClosure.md +++ b/Documentations/Internal/StaticLayoutDependencyClosure.md @@ -3,6 +3,8 @@ > 承接 [`StaticLayoutEngine.md`](StaticLayoutEngine.md)(单镜像引擎 + existential/actor 支持)。本文把静态 field-offset 引擎从「单镜像」扩展为「依赖闭包」,使字段类型 / 父类 / 协议位于**其他镜像**时也能解析。面向维护者。 > > **状态:已落地。** 下文设计/步骤为原始计划,末尾「落地实测」记录与计划的差异;与实现冲突处以「落地实测」为准。 +> +> **2026-09-02 更新**:本文描述的定位器(`MachOFileDependencyLocator`)、BFS 遍历与 `LayoutDependencySearchPath` 已下沉为共享模块 `MachODependencies`(提案 [0017](../Evolutions/0017-macho-dependencies-module.md));`ImageUniverse.dependencyClosure(root:…)` 工厂现在是 `DependencyClosure` 的薄包装,cache 的 bare name 匹配也从「枚举顺序首写者胜」改为「install path 精确优先、排序兜底」。现行契约以 [Modules/MachODependencies.md](Modules/MachODependencies.md) 为准,本文保留为阶段 3 的设计与实测记录。 ## 背景与目标 diff --git a/Documentations/Internal/TaskReports/2026-08-25-swift-evolution-interface-builder.md b/Documentations/Internal/TaskReports/2026-08-25-swift-evolution-interface-builder.md index 03463162..2fbea854 100644 --- a/Documentations/Internal/TaskReports/2026-08-25-swift-evolution-interface-builder.md +++ b/Documentations/Internal/TaskReports/2026-08-25-swift-evolution-interface-builder.md @@ -35,7 +35,7 @@ 7. modified:只渲染最新代际 + 变更注解(否:逐代际多行)。 8. 泛型形态:pack 异构 init(门控)+ 同质数组 init 双轨,实现收敛为非泛型类 + 擦除。 -提案:`Documentations/Evolutions/draft-swift-evolution-interface-builder.md`。 +提案:`Documentations/Evolutions/0013-swift-evolution-interface-builder.md`。 ## 实际执行 diff --git a/Documentations/Internal/TaskReports/2026-08-26-unify-interface-renderers.md b/Documentations/Internal/TaskReports/2026-08-26-unify-interface-renderers.md index 52404f8b..464f39a2 100644 --- a/Documentations/Internal/TaskReports/2026-08-26-unify-interface-renderers.md +++ b/Documentations/Internal/TaskReports/2026-08-26-unify-interface-renderers.md @@ -1,6 +1,6 @@ # 2026-08-26 统一 diff / evolution 接口渲染器的结构遍历核心 -- **提案**: [draft-unify-interface-renderers](../../Evolutions/draft-unify-interface-renderers.md) +- **提案**: [0014-unify-interface-renderers](../../Evolutions/0014-unify-interface-renderers.md) - **分支 / PR**: `feature/swift-evolution-interface-builder`(PR #114 同分支追加) ## 问题 diff --git a/Documentations/Internal/TaskReports/2026-08-30-runtime-metadata-type-builder.md b/Documentations/Internal/TaskReports/2026-08-30-runtime-metadata-type-builder.md new file mode 100644 index 00000000..5b2aef19 --- /dev/null +++ b/Documentations/Internal/TaskReports/2026-08-30-runtime-metadata-type-builder.md @@ -0,0 +1,36 @@ +# 2026-08-30 RuntimeMetadataTypeBuilder:TypeBuilder 的首个生产 conformer + +## 问题 + +用户最初问「Swift 源码里有几个符合 TypeDecoder 的类型,我们需要搬过来吗」。调研结论是三个具体 Builder(`ASTBuilder` / `TypeRefBuilder` / `DecodedMetadataBuilder`)都不该搬——遍历器本身已由 swift-demangling 完整移植(`TypeDecoder` + `TypeBuilder` 协议,且按上游逐项审计过),但生产侧没有任何 conformer(唯一实现是测试里的 `StringTypeBuilder`)。用户定向:第一个生产 conformer 做 in-process metadata builder(对标运行时 `DecodedMetadataBuilder`),走轻量档提案 `0012-in-process-metadata-type-builder`(当时为 `draft-` 未取号)。 + +## 调研 + +- **上游语义**(`swift/stdlib/public/runtime/MetadataLookup.cpp`):`createNominalType` 统一走 `createBoundGenericType`;key-argument 收集是 `_gatherGenericParameters`(接受「仅最内层实参 + parent metadata 补外层」或「完整扁平实参且无 parent」两种形状;父级 written args 从 parent metadata 的 generic-argument 区读回)+ `_checkGenericRequirements`(PWT 按 requirement 顺序追加);accessor 用 `MetadataState::Abstract` 请求(容环),顶层 `swift_checkMetadataState` 补全;tuple 单元素无标签解包、labels 用空格终止串;`createConstrainedExistentialType` 上游自己也拒绝。 +- **项目侧关键事实**(探查 agent 清点): + - `MetadataReader` **从不产出** `.typeSymbolicReference`——context 引用都解析成命名树。命名节点解析是主路径,不是回退。 + - `GenericSpecializer.makeRequest` 为每个参数**急切枚举候选**(无约束参数 = 全镜像每类型一个 Candidate),PWT 解析硬依赖 indexer——不适合逐节点解码复用,故 key-argument 收集在 builder 内自实现。 + - swift-demangling 的 `FunctionTypeFlags` 位布局与运行时 ABI 字 1:1,可透传;`TypeLookupErrorOr = Result` 现成,作为 `BuiltType` 承载失败(`create*` 协议方法不抛错)。 + - 进程内 wrapper 约定:`offset` 即指针位模式,`asPointer` / `readWrapperElement` 纯往返。 +- **兄弟依赖状态**:本地 `MachOObjCSection` 被 pin 在 0.7.103(detached,main 在 `.claude/worktrees` 里),带 `USING_LOCAL_DEPENDENCIES=1` 构建会因缺 `ObjCIndexing` product 失败;远端 swift-demangling 0.6.1 已含 TypeDecoder。**本批次全程用远端依赖构建**,未动被 pin 的兄弟仓库。 + +## 最终方案 + +见提案 `Documentations/Evolutions/0012-in-process-metadata-type-builder.md`(方案节 + 决策日志为准)。要点:`Sources/SwiftInspection/RuntimeMetadataTypeBuilder.swift`,`BuiltType = TypeLookupErrorOr`;`MachOSwiftSectionC` 补桥六个运行时入口(extended function 变体 weak-linked);命名节点三级解析(注入 seam → 运行时按名 → 标准库泛型内置表);诚实拒绝面 typed error。 + +## 实际执行 + +1. `Functions.h` 补 C 桥(含 ``、`ProtocolClassConstraint` ABI 反转注释、existential 协议数组会被原地排序的注释)。 +2. `RuntimeMetadataTypeBuilder.swift`(约 700 行):全协议实现 + `_gatherGenericParameters` 语义的 `keyArguments(of:ownArguments:parentType:)` + requirement subject 自举解码 + `writtenGenericArguments(ofParent:)`(value 16 字节头 / class resilient 分支)+ 标准库泛型描述符表 + `runtimeTypeByName`(remangle 剥前缀喂 `swift_getTypeByMangledNameInEnvironment`)。 +3. `RuntimeMetadataTypeBuilderTests`:往返 parity(oracle = 类型字面量本身,非重算)。 + +## 验证 + +- `RuntimeMetadataTypeBuilderTests` 17 个用例全绿:标准库泛型(含 `Dictionary` 的 Hashable PWT)、tuple / 函数 / metatype / existential / 组合、ObjC class 与协议 existential、resolver seam 下的约束泛型、`Sequence.Element: Equatable` 的 assocty-witness requirement subject、`OuterGeneric.Inner` 与 `OuterGeneric.InnerPair` 的父级实参合并、绑定环境替换(`x` / `SayxG`)、两类 typed error 拒绝。 +- 全量 `swift test --skip IntegrationTests` 结果见提案决策日志(本报告写作时在后台运行)。 + +## 偏差与踩坑 + +- **ObjC class 崩溃**:首版按上游用 `swift_getObjCClassMetadata`,NSObject / NSString 用例 SIGSEGV——崩在测试打印重建类型时(`objc_class::demangledName` 读 0x303)。探针实测:canonical `Any.Type`(`NSObject.self`、按名查询返回值)就是 realized class 指针本身,wrapper 是另一个 metadata 身份。改用 `swift_getInitializedObjCClass` 直接返回 class 指针。上游源码(本机 checkout 6.3.2)的 decode 路径按字面读应产出 wrapper、而已装运行时按名查询返回 class 指针——机制差异未再深挖,以行为事实为准。 +- **标准库泛型表是实现期补的**:方案原文只写了 seam + 运行时按名回退,实测 `SaySiG` 这类字符串 mangling 的标准替换展开成命名节点后 `Array` 都建不出,遂加表(17 个常用泛型,经已知实例化反查描述符)。 +- 首版拒绝面比方案多列 constrained existential 与 value generics(决策日志有记)。 diff --git a/Documentations/Internal/TaskReports/2026-09-02-exported-only-interface.md b/Documentations/Internal/TaskReports/2026-09-02-exported-only-interface.md new file mode 100644 index 00000000..d30e9ba5 --- /dev/null +++ b/Documentations/Internal/TaskReports/2026-09-02-exported-only-interface.md @@ -0,0 +1,43 @@ +# 2026-09-02 Interface 只打印导出声明(`--exported-only`) + +## 问题 + +用户要求「SwiftInterface 只打印 exported 的方法和类型」。现状:提案 0008 只在成员上打 `// not exported` 标注;类型、协议、扩展层面没有任何导出判断(`Sources/` 里对 `…Mn` / `…Mp` 描述符符号零查询),`printRoot` 与三个打印入口没有任何过滤钩子。提案 [0016](../../Evolutions/0016-exported-only-interface.md)。 + +## 调研 + +- `SymbolIndexStore` 的导出事实层(`isExported` 三态、`isExportedIncludingDerivedSymbols` 的 `Tj`/`Tq`/`Tu`/`TjTu` 派生形态)与 `SwiftDeclarationPrinter.exportVerdict(forSymbolNames:)` 可直接复用;`renderMember` 三 case + `renderModelFields` 字段腿 + `printRoot` 两个全局块是全部发射点。 +- fixture(`SymbolTestsCore`,library-evolution Release + `ENABLE_TESTABILITY`)实测:`Mn` 378 导出 / 19 本地(全是 `private` 类型 + `__C` 外来描述符),`Mp` 54 / 2,`Mc` 147 / 44——44 个未导出 conformance 全涉及私有类型,扩展不必再查 `Mc`;导出 `Mn` 集合与 `Ma` 集合完全一致。 +- Remangler 对 `.global` 节点发 `_$s` 前缀,与存储里的符号名形态一致;`BlockList` / `NestedDeclaration` 对空项整体跳过,被过滤的定义返回空串即可不留空行。 +- `TypeDefinition.extensions` 在索引器里从未被填充,扩展与类型的关联只能经名字(`ExtensionName` ↔ `TypeName` 结构相等)建立。 + +## 澄清提问(一轮三题,均选推荐项) + +1. 过滤层级:**打印期**(模型完整、diff / RuntimeViewer 不受影响)vs 索引期。 +2. 存储属性:**按 accessor 判定过滤**(与标注一致)vs 一律保留。 +3. 范围:**只做 interface** vs 连 dump 一起。 + +## 实际执行 + +1. `SwiftPrinting`:`printExportedDeclarationsOnly`;新文件 `SwiftDeclarationPrinter+ExportFilter.swift`(`ExportFilterScope`、`installExportFilterScope`、类型 / 协议两腿 verdict、全部 `isExcludedByExportFilter` 判定、`isEmptiedByExportFilter`);三个打印入口拆成过滤壳 + `printIncluded…` builder 体;成员循环 `where` 过滤;`renderModelFields` 字段预筛保原始下标。 +2. `SwiftInterface`:`printRoot()` 拆壳装 scope,全局块 `where` 过滤。 +3. CLI:`interface --exported-only`。 +4. 文档:提案(轻量档三段)、实现说明、README(公开 + 文档索引)、Evolutions 状态表、Glossary、演进账本、AGENTS.md、模块参考。 + +## 过程中的一次纠错(真实二进制暴露) + +第一版类型级判据纯靠重整名(`TypeName.node` → `_$s…Mn`)查 trie。全量输出交叉比对发现 `GenericRequirementTest where A: RawRepresentable` 下的公开嵌套类型 `RawRepresentableNestedStruct` 被误删:编译器给带约束扩展里的类型 mangle 的上下文只含扩展自己的 requirement(`…VAASYRzrlE…`),模型节点却带类型完整签名(interface 头部印两条 `where`),重整结果与真实符号不等价。改为**先反查描述符 offset 处的符号**(编译器自己的拼法),只在描述符处无符号时才用重整名兜底、且含 `.extension` 上下文的名字拒绝兜底。回归钉在 `publicTypeNestedInConstrainedExtensionIsKept`。 + +另一处测试侧的返工:即时编译 fixture 里想用「两个带约束扩展」造两个容器,实际上带约束扩展成员都渲染进同一个 `extension Foo { … where … }` 容器(每个成员自带 where 子句),改用第二个泛型类型 `PublicPair` 承载「只有内部成员的容器」。 + +## 验证 + +- 新增三套 22 测试全绿:`ExportedOnlyInterfaceTests`(12)、`ExportedOnlyLibraryEvolutionFixtureTests`(7,`-enable-library-evolution` 无 `-enable-testing`,覆盖 `internal` 形态与存储属性)、`ExportedOnlyFlagTests`(3)。 +- fixture 全量交叉验证:3824 → 2839 行;标注模式 378 处 `// not exported` 对应声明在过滤输出里零残留;新增行只有 conformance 扩展 `{` → `{}`;无 `\n\n\n` / `{\n}`。 +- 回归:`SwiftInterfaceTests` / `SwiftSectionCommandTests` / `SwiftDiffingTests` / `SwiftPrintingTests` 过滤子集全绿(见下方备注);默认输出由既有快照与 `defaultOutputIsUnchanged` 钉住。 +- 默认路径零行为改动,不构成 large refactor,未跑渲染 A/B 脚本。 + +## 与计划的偏离 + +- 类型级判据加了 offset 反查腿(提案决策日志原本主张按名字查优于 offset 反查),原因见上文纠错;记入实现说明「与提案的差异」。 +- 被清空的普通扩展会派发一对 start / completed 事件(索引必须在 start 之后才能判空),被过滤的定义则不发事件——提案未涉及事件契约,实现说明补记。 diff --git a/Documentations/Internal/TaskReports/2026-09-02-macho-dependencies-module.md b/Documentations/Internal/TaskReports/2026-09-02-macho-dependencies-module.md new file mode 100644 index 00000000..10076726 --- /dev/null +++ b/Documentations/Internal/TaskReports/2026-09-02-macho-dependencies-module.md @@ -0,0 +1,54 @@ +# 2026-09-02 依赖闭包下沉为 MachODependencies 模块 + +## 问题 + +用户要求「把目前的依赖闭包抽出来,方便给其他功能使用」。现状:仓库里有两套互不复用的「找依赖二进制」实现——`SwiftLayout/ImageUniverse+DependencyClosure.swift` 的传递闭包(BFS 递归、bare name 去重、dyld cache 一次性索引;遍历函数与 `MachOFileDependencyLocator` 都是文件私有,只有 `LayoutDependencySearchPath` 公开),以及 `SwiftInterface/SwiftInterfaceBuilderDependencies.swift` + `DependencyPath.swift`(一层直接依赖、按 install path 精确匹配 cache 镜像,供 TypeIndexing 的 `--resolve-c-module-names`)。提案 [0017](../../Evolutions/0017-macho-dependencies-module.md)。 + +## 调研 + +- 两套实现的规则不一致:SwiftLayout 按 bare name(`MachOImage(name:)` 语义),cache 索引「枚举顺序首写者胜」——在 macOS cache 上同名的 Mac Catalyst 构建(`/System/iOSSupport/…/SwiftUI`)可能先被枚举到;SwiftInterface 按 `imagePath` 精确匹配,`@rpath/…` 依赖永远解析不到。MachOKitExtensions 已有 `DyldCacheImageSearchMode.matchRank(forImagePath:)`(canonical framework > dylib > bundle,support root 降级)可直接复用。 +- SwiftInterface 的 `MachOImage` 版初始化器把完整 load path(MachOKit `Dylib.name` 是 "library's path name")喂给按 bare name 比较的 `MachOImage(name:)`,从诞生起解析结果恒为空。`git log` 追到它随 `SwiftInterface` 拆分(47b5961f)一起出现,仓库内与下游(RuntimeViewer / MachOKitUI / SymbolViewer)均无调用方,所以没被发现。 +- TypeIndexing 按依赖清单逐模块生成 SourceKit 接口(提案 0009 把「全 SDK 生成」改成「只生成依赖过滤选中的模块」),因此 SwiftInterface 那套**必须**保持只取直接依赖。 +- 默认输出不经闭包:`SwiftDeclarationPrinter.staticFieldLayoutProvider()` 只在 `printFieldOffset` / `printTypeLayout` / `printEnumLayout` / `printExpandedFieldOffsets` 任一开启时才构建 provider;渲染 A/B 脚本的 dump / interface 不传这些 flag。 +- 下游三个仓库都不用 `LayoutDependencySearchPath` / `DependencyPath` / `SwiftInterfaceBuilderDependencies`,公开 API 改名的源码兼容风险为零,但仍按库项目惯例保留 deprecated 转发一个版本。 +- 本 worktree 复制了集成 worktree 的 `.package.env`,但 agent 的 shell 不读它,`USING_LOCAL_DEPENDENCIES` 实为 unset,所有构建都走远程 pin(两侧 `workspace-state.json` 均为 `remoteSourceControl`);`.worktrees/` 下的依赖符号链接仍按 `create-worktree` skill 递归补齐(新增 `swift-objc-dump`、`FrameworkToolbox`)。 + +## 澄清提问(一轮两题,均选推荐项) + +1. 范围:**两套合并**(共享模块同时提供 direct / transitive,SwiftInterface 改薄包装保持 direct 语义)vs 只抽 SwiftLayout 一套。 +2. 落点:**本仓库新建 `MachODependencies` target**(MachOFoundation re-export)vs 放 sibling 包 MachOKitExtensions。 + +未问而定:模块与类型命名;匹配规则取并集(install path 精确优先、bare name 排序兜底);fat 显式文件取 root 同架构 slice;deprecated 转发保留一个版本;依赖种类不过滤;`@rpath` 不展开。 + +## 实际执行 + +1. 新 target `Sources/MachODependencies/`(只依赖 MachOKit + MachOKitExtensions):`DependencySearchPath`(含 `DependencySearchPathError` / `DependencySearchPathLoadFailure`)、`DependencyLoadName.bareImageName(of:)`、`DependencyLocating` + `InProcessDependencyLocator`、`FileDependencyLocator`(两级查找,cache 索引 `NSLock` 保护一次性建成)、`DependencyClosure`(`DependencyTraversal.direct / .transitive`,`images` / `unresolvedLoadNames` / `searchPathLoadFailures`)。`MachOFoundation` 加 `@_exported import`,同时作为独立 library product。 +2. `SwiftLayout`:三个 `ImageUniverse.dependencyClosure` 工厂改薄包装,新增 `dependencyClosure(_ closure:)`;`LayoutDependencySearchPath` → deprecated typealias。 +3. `SwiftDeclarationRendering`:`StaticLayoutDependencyResolution.dependencyClosure(searchPaths:)` 关联值换成 `[DependencySearchPath]`。 +4. `SwiftInterface`:`SwiftInterfaceBuilderDependencies` 改薄包装(`init(closure:)`、`init(machO:searchPaths:eventHandlers:)`、`unresolvedLoadNames`;`searchPathLoadFailures` 继续派发 `renderingDegraded(.dependencyLoad)` 事件,subject 保持裸路径);`DependencyPath` + 旧 init 标 deprecated 转发;`MachOImage` 版改走闭包(顺带修 bug)。 +5. CLI `interface --resolve-c-module-names`:改用新 init,新增按 `unresolvedLoadNames` 逐项点名的 warning。 +6. 测试:新 `Tests/MachODependenciesTests/`(三文件)、`Tests/SwiftInterfaceTests/SwiftInterfaceBuilderDependenciesTests.swift`;`IntegrationTests/TypeNameProviderTests` 改用新 init。 +7. 文档:提案(轻量档三段 + 决策日志)、模块参考 `Internal/Modules/MachODependencies.md`、`Modules/README.md` 与 `Documentations/README.md` 索引、AGENTS.md(模块图 + 基础模块条目 + SwiftLayout 条目)、Glossary(「bare image name」「dependency closure」)、`StaticLayoutDependencyClosure.md` 迁移指引、演进账本第 55 节、本报告。不升版本,不动 Changelog。 + +## 验证 + +- 定向套件:`MachODependenciesTests`(3 套件 15 测试)+ `SwiftInterfaceBuilderDependenciesTests`(3)+ 既有 `DependencyClosureLayoutTests`(4)+ `FieldLayoutRendererReaderSpecializationTests`(4)——26 测试 6 套件全绿(`swift test --filter …`,退出码 0)。宿主 cache 存在,`FileDependencyLocatorTests` 的 Catalyst 精确路径分支实际执行。 +- 全量(`swift test --skip IntegrationTests`)与 release 双侧输出对比:见文末「验证结果补记」。 + +## 与计划的偏离 + +- 提案写的是「`SwiftInterface` 派发 `unresolvedLoadNames` 事件」的可能性;实现只透出数据、不新增事件 case(`Payload.unhandledFailureDescription` 是有意的穷举 switch,加 case 要动 `SwiftDeclaration`),由 CLI 自己打 warning。 +- `DependencyClosure` 的 `MachOFile` 便利初始化器不再 `throws`(旧 `ImageUniverse.dependencyClosure(root:searchPaths:)` 的 `throws` 其实从不抛,失败全是 `try?` 吞掉的);`ImageUniverse` 工厂仍 `throws`,因为 root 的 `ImageReference` 构建会抛。 + +## 验证结果补记 + +- 全量 `swift test --skip IntegrationTests`:1612 测试 / 301 套件,仅 `SharedCache.resolve under Swift Concurrency` 的 `differentKeysParallelViaTaskGroup` / `differentKeysParallelViaAsyncLet` 失败——已知 flaky(用墙钟断言并行度,全量跑假失败),单独重跑两者通过。 +- 带布局注释的双侧对比(release 二进制,宿主 dyld cache):`dump --emit-field-offsets --emit-type-layout --emit-enum-layout` 与 `interface --emit-offset-comments --emit-type-layout --emit-enum-layout` 对 SwiftUI(142067 / 138936 行)、SwiftUICore、SwiftData、Combine 共 8 组输出**逐字节一致**。这是闭包真正参与的路径;默认输出不经闭包,未跑完整渲染 A/B 脚本(与 0016 的做法一致)。 +- **一次差点误判的性能回归**:首轮计时候选版慢 2.5 倍(SwiftUICore 60s → 145–197s)。逐项排查:闭包内容与顺序几乎相同(604 镜像,仅 `libcrypto` / `libssl` 两个多点号 dylib 的版本选择不同),闭包构建 < 0.2s;最终发现两侧 scratch 各自新鲜解析远程 pin,候选版拿到了当天新发布的 swift-demangling **0.6.1** 与 FrameworkToolbox **0.11.0**,基线是 0.6.0 / 0.10.0。把候选版的 `Package.resolved` 换成基线的重建后,SwiftUICore 计时 51 / 52 s 对 52 / 52 s,**完全持平**。结论:回归来自上游版本,与本次改动无关;A/B 双侧必须共用同一份 `Package.resolved`,此教训已补进 AGENTS.md 的环境漂移条目。 +- **二分(用户追问后补做)**:只把 swift-demangling 钉到 0.6.1、FrameworkToolbox 保持 0.10.0(直接改 `Package.resolved` 的 `version` / `revision`,`swift package resolve --version` 对该包名报 not found),SwiftUICore 布局 dump 77–92 s → 321–430 s,**普通 `dump` 50–59 s → 153–211 s、普通 `interface` 65 s → 186 s**,输出全部一致——默认路径同样中招,不限于布局。FrameworkToolbox 0.11.0 只改 re-export 与包拓扑,排除。swift-demangling 0.6.0..0.6.1 唯一代码改动是 `StackSafeExecutor`(commit `a3477d3`,为消 Thread Performance Checker 的优先级反转报告):worker 取到每个任务前 `pthread_set_qos_class_self_np` 到提交方 QoS、停车前降到 `QOS_CLASS_BACKGROUND`。demangle / print / remangle 每次调用都是一跳,跳数以十万计,每跳多两次 QoS 系统调用并且唤醒的是 background 线程(能效核 + 节流),累加成分钟级。机制由 diff 推断,未做进程采样(采样步骤因后台命令超时未执行)。**在上游修复前 MachOSwiftSection 不应升到 swift-demangling ≥ 0.6.1**。 +- 本 worktree 的 `Package.resolved`(gitignored)现与集成 worktree 一致(swift-demangling 0.6.0 / FrameworkToolbox 0.10.0)。 + +## code-review 后续修正(PR #120,另一会话审查、本会话落地) + +审查报 15 条,四问过后本 PR 动手 5 处:① 代码注释里的提案引用从 `draft-macho-dependencies-module` 改成 slug `macho-dependencies-module`(规则是代码注释只用 slug,`draft-` 是创建期文件名前缀而非 slug;审查原建议改编号,核实规则后否决);② 四个新套件加进 CI filter;③ `SwiftInterfaceBuilderDependenciesTests` 补 `ExclusiveImageAccess(.SymbolTestsHelper)`(它经 `InProcessDependencyLocator` 对 helper 调 `MachOImage(name:)`);④ **本 PR 新引入的真问题**:文件定位器三键登记后同一镜像可能以两个 bare name 各进 `images` 一次,加按 `identifier` 去重 + 复现测试(修前 `images.count == 2`);⑤ 切片选择比 `cpu.type` + 掩码后 `cpu.subtype`(审查会话指出 `CPU ==` 不掩 capability 位)。另开不进本 PR:CLI `--resolve-c-module-names` 的「解析为空」守卫判据本就写错(应判平台不匹配,3e8f78ae 选错判据,非本次回归,且需非 macOS fixture 才能写红测试)。登记不修:SwiftLayout 丢弃 `searchPathLoadFailures`(与 next 一致,宿主可自行 resolve 再喂 `dependencyClosure(_:)`)、一次性 cache 索引开销(与 A6 同性质)。 + diff --git a/Documentations/Internal/TaskReports/2026-09-03-large-stack-executor-and-cross-version-parallelism.md b/Documentations/Internal/TaskReports/2026-09-03-large-stack-executor-and-cross-version-parallelism.md new file mode 100644 index 00000000..7a007678 --- /dev/null +++ b/Documentations/Internal/TaskReports/2026-09-03-large-stack-executor-and-cross-version-parallelism.md @@ -0,0 +1,57 @@ +# 2026-09-03 大栈任务执行器接入与跨版本并行 + +## 问题 + +用户问「整个库都使用 async 环境是否可行」。核实结果:306 个 async 函数、72 个公开 async 入口,但除 `TypeIndexing` 的两个 actor 外没有 actor,真正会挂起的地方只有三处;真正的开销在打印路径——每次 `printSemantic` 都由 swift-demangling 的 `StackSafeExecutor` 探测调用线程剩余栈,协作线程 512 KB 永远不过,每次调用跳到 8 MB 池线程再用信号量停住(release 每次 8–21 µs,1.14–2.28×)。索引侧已用同步 `withLargeStack` 摊掉;打印循环是 async,包不住。此外 diff / evolution 的多版本 `prepare` 串行,而各版本彼此独立。 + +## 调研 + +- 全库 async 化不可取:`deinit` / getter / `Hashable` / `for … where` 不能 `await`;MachOKit 与运行时调用同步;`Node` 与三个 Definition 是非 `Sendable` 的 class;async 化后协作线程 512 KB 让探测 100% 不通过;符号扫描改逐个 `await` 反而比批量内联慢。收益在「并行」和「让 async 代码跑在大栈上」。 +- 上游探测看的是**剩余栈**不是线程身份,所以 16 MB 线程上的 task 每个入口都内联——只需一个 `TaskExecutor`(SE-0417,macOS 15 / iOS 18 起)。上游已有 `LargeStackThreadPool`(按 QoS 分池、`pthread_attr_setstacksize`)可复用。 +- swift-demangling 版本线:0.6.1 的 QoS 改动慢 3–4 倍(2026-09-02 二分);0.6.2 分池修复、用户实测恢复;0.6.3 含执行器(提案 0014,`StackSafeExecutor.taskExecutor`,`@_spi(Internals)`)。 +- 跨版本并行安全:`MachOFile.identifier` 按 LC_UUID 键控,`SharedCache` 全按此分片,描述符读取走 mmap;三进程并行实测约 2 倍。版本内并行卡在 MachOKit 共享 `FileHandle` 的 seek + read。 +- 库内非结构化 `Task {}`:零处。 + +## 澄清提问(完整档,四轮 + 收尾) + +1. 范围:执行器 + 跨版本并行;版本内并行另起提案。 +2. 执行器归上游 swift-demangling,进程内一个池(后改为分池共码)。 +3. 库入口自装偏好;macOS 15 以下静默回退;默认并行上限取核数。 +4. 上游提案由本人在 sibling 仓库起草;执行器线程 16 MB、跳转池 8 MB;`@_spi(Internals)`;发版 0.6.3。 +5. 收尾:用户确认「其他没问题」,0.6.2 已实测恢复;之后指示「基于上一个 PR 实现 async 提案」,视为 Accepted。 + +## 最终方案 + +见提案与实现说明。要点:`MachOSymbols.LargeStackTaskExecution.run` 包住索引器 `prepare`、interface builder 的 `prepare` / `printRoot`、diffable builder 的 `prepare`、evolution builder 的 `prepare` / 两个渲染入口、diff renderer 的两个入口、printer 的四个逐定义入口、六个 `Dumpable.dump`;`AnySwiftEvolutionInterfaceBuilder.prepare(maximumConcurrentPreparations:)`(默认核数);`diff` / `evolution` 的输入按窗口并行,CLI `--jobs`;`Utilities.concurrentMap(maximumConcurrency:)`;pin 抬到 0.6.3。 + +## 实际执行 + +worktree `.worktrees/MachOSwiftSection-LargeStackExecutor`,分支 `feature/large-stack-executor-and-cross-version-parallelism`,基于 `feature/self-contained-abi-layer`(ABI 提案的分支,PR #121 未合并时堆叠)。 + +1. **抬 pin**:`"0.6.3" ..< "0.7.0"`,`Package.resolved` 解析到 0.6.3(`8f32e30`);其余 pin 不动。同一份 `Package.resolved` 下先做 0.6.0 vs 0.6.3 的 release 计时(见下)。 +2. **`LargeStackTaskExecution`**(`Sources/MachOSymbols/LargeStackTaskExecution.swift`):`isEnabled`(`@Mutex`,初值读 `MACHO_SWIFT_SECTION_LARGE_STACK_EXECUTOR`)、`isSupported`、`run`。`withTaskExecutorPreference` 在当前工具链接受非 `Sendable` 的 `operation`(用 `swiftc -typecheck -swift-version 6 -strict-concurrency=complete` 探针确认),所以 `run` 的 `body` 不必 `@Sendable`——各入口闭包捕获非 `Sendable` 的 Definition class 才能过编译。 +3. **逐入口包裹**:按提案清单。`printExtensionDefinition` / `printDefinition` 拆壳 + 体;`printIncludedExtensionDefinition` 名字已被提案 0016 的 builder 体占用(首次编译撞名),壳体改名 `…Contents`。 +4. **并行**:`Utilities/BoundedConcurrentMap.swift`;evolution builder 的 `prepare` 加参数;pack façade 跟随;`DiffCommand` 两侧、`EvolutionCommand` 两条路径都走 `concurrentMap`;`--jobs` 校验 ≥ 1。 +5. **测试**:`LargeStackTaskExecutionTests`(6)、`BoundedConcurrentMapTests`(7)、evolution builder 的并行等价 + clamp(2)、`DiffCommandValidationTests`(3)、`EvolutionCommandValidationTests` 加 `--jobs`(2);CI filter 加入五个套件。首次跑:`EvolutionLine` 的属性是 `content` 不是 `text`,改后 32 个全过。 +6. **文档同批**:实现说明、术语表、AGENTS.md(`MachOSymbols` / `SwiftIndexing` / `SwiftInterface` 条目 + 测试环境节)、`Modules/SwiftInterface.md`、README 索引、评审记录待办标记已解决、`Node+.swift` 过期注释、演进账本、Changelog 0.19.0 + `Version.swift`。 + +## 验证 + +- 新增 / 改动的五个套件 32 个测试通过;全量 `swift test --skip IntegrationTests`:**1637 个测试、305 个套件全部通过**(385 s),含以往偶发的 `SharedCache` 并发墙钟测试。 +- 计时(release,宿主 cache;详表见实现说明):SwiftUICore dump 48.6 → 40.5 s、interface 56 → 47 s;SwiftUI dump 79 → 61 s、interface 89 → 71 s(执行器关 → 开,−16% 到 −23%);0.6.0 → 0.6.3 仅抬 pin 持平。三版本 SwiftUI `evolution --interface` 306.7 s(关 + `--jobs 1`)→ 242.6 s(开 + `--jobs 1`)→ 151.9 s(开 + 默认并行);lineage 282.4 → 233.8 → 139.4 s。 +- 输出:单版本四种配置(0.6.0 / 0.6.3 pin-only / 执行器关 / 执行器开)的 dump 与 interface 逐字节一致;evolution 的 `--jobs 1` 与默认并行、执行器开与关逐字节一致。 +- 渲染 A/B(`Scripts/run-rendering-ab-verification.py`,基线 = ABI 分支,候选 = 本分支):执行器开、关各一轮,**两轮均 78 对逐字节一致,0 差异**(当前系统 cache、模拟器运行时 iOS 15.5 / 18.5 / 18.6 / 26.5、进程内 MachOImage)。 + +## 与计划的偏差 + +- `isEnabled` 初值读环境变量(提案只有静态开关)——为 A/B 与计时服务。 +- 并行用通用 `concurrentMap(maximumConcurrency:)` 而非 `async let`。 +- CLI `dump` 循环不再额外包一层(逐类型跳转代价可忽略)。 +- 主 actor 段落:库 target 未开启 SE-0461,async 入口从 `@MainActor` 调用会离开主 actor 落到执行器线程(实现说明已按事实写)。 + +## Review 修复批次(2026-09-04) + +并行 review 会话对 PR #122 给出 15 条发现(原文与处置见 [Roadmaps/2026-09-04-pr122-review-findings.md](../../../Roadmaps/2026-09-04-pr122-review-findings.md))。最严重的一条已被审查者独立复现:`concurrentMap` 用 `addTask`,取消后剩余元素全部启动。修复:`addTaskUnlessCancelled`、被拒即抛 `CancellationError`(不能返回残缺数组,`result!` 会崩)。其余四条真缺陷是测试层面的:执行器线程断言没挡 `isEnabled`、环境变量只认 `"0"`、并行等价测试先串行焐热缓存、窗口断言退化成串行也绿。用户三项裁定:Dispatcher 进程级递归锁串行化 handler 调用、lineage 默认窗口保持核数、stderr 加输入标签(`ConsoleEventHandler(label:)` + `eventHandlersPerVersion`)。审查者对 F9「重复门」的修法在实际中不可行(`#available` 必须出现在使用点),登记 A32;`TypeDatabase` 的同形 `addTask` 缺注入缝写不出复现测试,登记 A33。 + +验证:受影响 7 个套件 43 个测试通过;突变检查——把 `addTaskUnlessCancelled` 改回 `addTask`、去掉 dispatcher 锁、测试 guard 只看 `isSupported`,并在 `MACHO_SWIFT_SECTION_LARGE_STACK_EXECUTOR=0` 下跑对应四个测试:`cancellationStopsSubmittingPendingElements`(元素 1 启动、不抛错)、`aHandlerSharedByConcurrentDispatchersIsNeverEnteredConcurrently`(handler 被并发进入)、`bodyRunsOnAnExecutorThreadWhenSupported` 与 `demanglerEntriesInsideTheBodyDoNotHop`(环境变量关闭时假红)四个测试共 7 处失败;改回后同一环境下全绿。全量 `swift test --skip IntegrationTests` 结果见下一行。全量 1649 测试 / 307 套件,仅 `SharedCache.resolve under Swift Concurrency` 的两个墙钟并行度断言在全量并行时假失败(已知),单独重跑通过。 + diff --git a/Documentations/Internal/TaskReports/2026-09-03-self-contained-abi-layer.md b/Documentations/Internal/TaskReports/2026-09-03-self-contained-abi-layer.md new file mode 100644 index 00000000..573ed603 --- /dev/null +++ b/Documentations/Internal/TaskReports/2026-09-03-self-contained-abi-layer.md @@ -0,0 +1,61 @@ +# 2026-09-03 ABI 层自包含:MachOSwiftSection 与符号索引分家 + +## 问题 + +用户在一次「整个库能否全 async 化」的调研中顺手提出:「ABI 不能反向依赖 SymbolIndexStore,ABI 接口应该自包含」。核实结果:`MachOSwiftSection` 对 `MachOSymbols` 有两条通道——五个描述符的 `RelativeDirectPointer` 字段经 `Symbols` 的 `Resolvable` 实现走 `SymbolIndexStore.shared`,一次 ABI 访问触发整镜像符号扫描;`SymbolOrElementPointer` 把 `MachOSymbols.Symbol` 带进每一种上下文指针。提案 [0018-self-contained-abi-layer](../../Evolutions/0018-self-contained-abi-layer.md)。 + +## 调研 + +- 五个描述符(`MethodDescriptor`、`MethodOverrideDescriptor`、`MethodDefaultOverrideDescriptor`、`ProtocolRequirement`、`ResilientWitness`)的字段与访问器;`Symbols.resolve` → `machO.symbols(offset:)` → `SymbolIndexStore.shared.symbols(for:in:)` → 未建表就当场建。 +- `ReadingContext` 腿没有符号服务,落到 `Resolvable` 默认的 `context.readElement(at:)`,把机器码按 `Symbols` 结构体读出来;三处 fixture 测试只断言「不为 nil」。 +- MachOKitUI 在渲染 Swift section 前把 `MachOSymbols.Symbol.resolvesSymbolUsingIndexStore` 强行置 false(`MachOSwiftSectionDetailBuilder.swift:18-20`)——下游为此付过代价的证据。 +- `MachOSwiftSection` 对 `Demangling` 的依赖只剩三处字符串帮手:`isSwiftSymbol`、`stripManglePrefix`、`cModule` / `objcModule`。 +- 上层调用方:`SwiftDump` 三个 dumper、`SwiftDeclaration` 三个定义类、`MetadataReader` 一处、`OpaqueType+` 一处、三个 baseline 生成器、四个 fixture 套件。下游 RuntimeViewer 无影响,SymbolViewer 无影响(不用搬走的值类型),MachOKitUI 一行。 + +## 澄清提问(完整档,四轮) + +1. 自包含到**包图级别**(否决代码级别);`implementationSymbols` 扩展落 **`SwiftInspection`**。 +2. 用户反问「下沉有意义吗」,答复:不下沉则包图级别不成立;随后确认**下沉** `MachOResolving`,`MachOSymbolPointers` 并入 `MachOPointers`。 +3. 上游 swift-demangling 的执行器提案由本人起草(属另一提案)。 +4. 用户口头批准「你这边弄 ABI」,状态置 Accepted → In Progress。 + +## 实际执行 + +worktree `.worktrees/MachOSwiftSection-SelfContainedABI`,分支 `feature/self-contained-abi-layer`,基于 `next` f3782248。 + +1. **先写红测试**:`MethodDescriptorTests` 对旧 API 比对两条腿的 `offset`——context 腿 -2999674702252736512,MachO 腿 5624,修前失败。 +2. **值类型下沉**:`Symbol` / `Symbols` / `SymbolOrElement` 移到 `MachOResolving`;`Symbols` 去掉 `AsyncResolvable`,`init` 改 `package`;`Symbol` 的索引库 `resolve` 与 `resolvesSymbolUsingIndexStore` 留在 `MachOSymbols` 作扩展;`SymbolOrElementPointer` 并入 `MachOPointers`,`MachOSymbolPointers` target 删除。 +3. **新伞模块 `MachOBase`**(`MachOKitExtensions` + `MachOReading` + `MachOResolving` + `MachOPointers` + `Utilities`);`MachOFoundation` 改为 `MachOBase` + `MachOSymbols` + `MachODependencies`;`MachOSwiftSection` 163 个文件 `import MachOFoundation` → `import MachOBase`,`Exported.swift` 只再导出 `MachOBase`。 +4. **描述符改地址接口**:字段 `RelativeDirectRawPointer`,`implementationOffset: Int?`,`implementationAddress(in context:)`;`ResilientWitness.implementationAddress(in machO:)` 随之变 `String?`。 +5. **`SwiftInspection` 补五个同名扩展**(`Extensions/Descriptor+ImplementationSymbols.swift`),调用方去掉 `try` / `Symbols.resolve`。 +6. **摘 `Demangling`**:`hasSwiftManglingPrefix` / `strippingSwiftManglingPrefix` / `CImportedModuleNames` 本地化。 +7. **依赖声明补齐**:16 个 target 补 `.target(.MachOFoundation)`(原本只靠传递),12 个源码文件与若干测试文件补显式 import。 +8. **测试**:四个 fixture 套件改比对 `implementationOffset` 真值与 context 地址等价;`MethodDefaultOverrideDescriptor` 的哨兵登记与覆盖 allowlist 同步;新增 `ManglingPrefixTests`(与 demangler 逐字符串对照);红测试由新 API 上的等价断言永久替代;五个 baseline 用插件重生成(`MethodDescriptor` 的实现偏移钉为 `0x15f8`)。 +9. **文档**:提案决策日志、实现说明 `SelfContainedABILayer.md`、AGENTS.md 模块图与条目、`Documentations/README.md`、模块参考覆盖表、演进账本(节号落地时取)、`Changelogs/0.18.0.md`、`Version.swift` 0.17.1 → 0.18.0、CI 过滤清单加五个套件。 + +## 验证 + +- `swift build --build-tests`:全部 target 编译通过,`Sources/` 零新增警告。 +- `swift test --filter MachOSwiftSectionTests`:723 测试 / 161 套件全绿,退出码 0。 +- 全量 `swift test --skip IntegrationTests` 与渲染 A/B:见文末补记。 + +## 与计划的偏离 + +- 提案写 `symbols(offset:) async` 标 deprecated 留一版;实测两个重载不能共存,改为删除。 +- 提案只点名 `isSwiftSymbol` 一处 `Demangling` 用法;实际还有 `stripManglePrefix` 与 `cModule` / `objcModule`,一并本地化。 +- 提案没预见 `MachOBase`;它是「163 个文件换一行 import」与「包图级别」两个要求的交点。 +- 提案没预见 16 个 target 从未声明对 `MachOFoundation` 的依赖。 + +## 验证结果补记 + +- 全量 `swift test --skip IntegrationTests`:1617 测试 / 302 套件,仅 `SharedCache.resolve under Swift Concurrency` 的 `differentKeysParallelViaTaskGroup` / `differentKeysParallelViaAsyncLet` 失败——已知 flaky(墙钟断言并行度,当时渲染 A/B 的 release 构建正在同机跑),单独重跑 5 测试全绿、退出码 0。 +- 渲染 A/B(`Scripts/run-rendering-ab-verification.py`,基线 = 集成 worktree 的 `next` f3782248,候选 = 本分支;两侧 `Package.resolved` 逐项一致,均为远程 pin):**78 对输出逐字节一致,0 差异**。覆盖当前系统 dyld cache(归档 cache 目录名与脚本期望不符,按文档回退到系统 cache)、模拟器运行时 iOS 15.5 / 18.5 / 18.6 / 26.5(15.5 上 SwiftUICore / SwiftData / ActivityKit 不在运行时内,按规则跳过)、进程内 MachOImage 三条路径的 dump 与 interface。 +- 红测试证据留档:修前 `MethodDescriptorTests.implementationSymbolsContextLegAgreesWithMachOLeg` 失败(context 腿 `offset` -2999674702252736512 vs MachO 腿 5624);修后由 `implementationAddress` 测试的 `imageAddress == implementationOffset == 0x15f8` 永久替代。 +- 未做:模拟器 UI 验证(不适用);下游 MachOKitUI 的一行改动(`MachOSymbols.Symbol.resolvesSymbolUsingIndexStore` → `Symbol.resolvesSymbolUsingIndexStore` + `import MachOSymbols`)待其仓库跟进。 + +## Review 修复批次(2026-09-04) + +并行 review 会话对 PR #121 给出 12 条发现(原文与处置见 [Roadmaps/2026-09-04-pr121-review-findings.md](../../../Roadmaps/2026-09-04-pr121-review-findings.md)),全部属实。最要紧的两条:manifest 没有 `MachOFoundation` product,下游按 changelog 改 import 也声明不了依赖(已补 `MachOBase` / `MachOFoundation` 两个 product);`--emit-member-addresses` 下空 resilient witness 不再打一行假地址(旧代码把空相对指针解析成字段自身位置),changelog 原来无限定的「逐字节一致」改为限定默认 flag。其余是测试空转(`ProtocolRequirementTests` 的 nil == nil、`MethodOverrideDescriptor` baseline 不发射字面量、`MethodDescriptorTests` image 腿借 file 的 offset)、文档与测试不符(`ManglingPrefixTests` 未钉 `CImportedModuleNames`)、四个 target 漏声明依赖、CI filter 漏一个套件,以及 `ProtocolConformanceDumper` 新旧写法并存。延后两项登记为 ReviewAdjudications A23 / A24。 + +验证(2026-09-04):受影响的 9 个套件 98 个测试通过;突变检查——把 `ProtocolRequirement.defaultImplementationOffset` 与 `MethodOverrideDescriptor.implementationOffset` 改成返回指针字段自身位置后,`ProtocolRequirementTests` / `MethodOverrideDescriptorTests` 共 4 处断言变红,改回后全绿(修复前的两个测试对这种错误不敏感);全量 `swift test --skip IntegrationTests` 1618 测试 / 302 套件,仅 `SharedCache.resolve under Swift Concurrency` 的两个墙钟并行度断言在与 release 构建、A/B 同时跑时假失败,单独重跑全绿;带 `--emit-member-addresses` 的双侧对比(基线 `next` f3782248 的 release 二进制 vs 本分支):SwiftUICore / SwiftUI / SwiftData / Combine 的 dump 与 interface 8 对逐字节一致——这四个框架里没有 implementation 为空的 resilient witness,所以 B 的行为变化只在空指针那条分支上可达,changelog 已如实限定。 + diff --git a/Documentations/Internal/TaskReports/2026-09-06-vtable-slot-attribution.md b/Documentations/Internal/TaskReports/2026-09-06-vtable-slot-attribution.md new file mode 100644 index 00000000..8b632fd7 --- /dev/null +++ b/Documentations/Internal/TaskReports/2026-09-06-vtable-slot-attribution.md @@ -0,0 +1,58 @@ +# 2026-09-06 vtable 槽归属改用 method descriptor 符号 + +对应提案:[0020-vtable-slot-attribution-via-method-descriptor-symbols](../../Evolutions/0020-vtable-slot-attribution-via-method-descriptor-symbols.md) + +## 问题 + +用户在 Hopper 里看 `SwiftUI.GraphHost`(iOS 18.5 simruntime 的 SwiftUICore,arm64)的 vtable 布局,发现与 `swift-section dump` 的输出对不上。 + +## 调研 + +直接解析二进制取真值,不依赖工具自身的输出: + +- 类描述符在 `0x98c9c8`,`VTableDescriptorHeader` 给出 `vTableOffset = 20`、`vTableSize = 10`,十条 method descriptor 连续排在 `0x98c9fc`–`0x98ca44`。 +- 每条 descriptor 自带一个 `Tq` 符号(`nm` 里是 `S` 型全局数据符号),demangle 后就是这个槽的真实成员。 +- 与 dump 输出对照:槽 23/24/25 正确,槽 26–29 **四条全错**——真值是 `instantiateOutputs` / `uninstantiateOutputs` / `timeDidChange` / `isHiddenForReuseDidChange`,dump 打的是 `isHiddenForReuseDidChange` 加三条属于嵌套 struct `GraphHost.Data` 的协程 resume 函数。 +- 槽 20/21/22 打 `Symbol not found`。读 class metadata(`0xaaa000`)的对应 word 发现它们是 chained-fixup **bind** 槽,import ordinal `0xc1c` 解出来是 `_swift_deletedMethodError`:成员被删除、槽位为 ABI 保留、调用即 trap。descriptor 侧 implementation 为 null 正是这个缘故。 + +根因两条: + +1. 归属主源选错。`ClassDumper` 用实现地址反查符号,而 linker 的 identical code folding 把字节相同的函数体折叠到一个地址——`0x9330`(空 `ret`)上有 **2878** 个符号,这个映射没有逆。 +2. 候选筛选过松。`node.first(of: .class)` 找的是树里**任意位置**的第一个 class 节点,`GraphHost.Data.graph.modify` 的 context 链是 `class GraphHost → struct Data`,于是嵌套类型的成员被当成本类的。 + +影响面(171 个非泛型带 vtable 的类 / 512 个槽):16.2% 的槽实现地址上有多个符号,14.1% 与别的槽共享同一地址(必然至少错一个),71.7% 的槽 descriptor 自带 `Tq` 可精确归属。 + +## 方案 + +`Tq` 是每个成员一个、位于 descriptor 自身地址的数据符号,ICF 影响不到——提案 0006 修 `final` 误判时已经确认过这条性质,但只用作否定证据。这次把它用作正向归属的主源,实现地址反查降级为回退。 + +## 实际执行 + +按提案做完主 vtable 循环 + interface 索引路径 + `validNode` 上下文修正后,有两处必须靠测试才发现的错误: + +1. **override 不能走 `Tq`**。最初把 `attributedMemberNode` 也给了 `MethodOverrideDescriptor` / `MethodDefaultOverrideDescriptor`(经它们指向的父类 descriptor 取 `Tq`)。结果 `override` 关键字从 interface 输出里整个消失:`TypeDefinition.index` 的 joinKey 是要跟**本类**成员符号对上的,父类形状的节点匹配不到任何东西。`SymbolTestsCoreE2ETests.outputContainsOverrideKeyword` 抓住了。 +2. **`dumpMethodDeclaration` 里的残留**。撤回后仍在这个 helper 里留了 `Tq` 主源,而 override 循环的 `.element` 腿会用**父类的** descriptor 调它,于是 `override ResilientChild.init()` 被打成 `override ResilientBase.init()`,还丢掉了实现符号携带的 `vtable thunk … dispatching to …` 细节。这一处单元测试没覆盖,是逐条审查快照 diff 时发现的。 + +教训是同一个:`Tq` 回答的是「这个 descriptor 声明了谁」,override 槽问的是「本类的实现是谁」,两者不是同一个问题。 + +另有一处主动放弃:`ProtocolDumper.validNode` 按同样思路收窄上下文匹配后,`base conformance descriptor for P: Q` 这类**没有 entity 节点**的要求描述符被整批丢弃,SwiftUICore 的协议输出 1033 行退化为 `[Stripped Symbol]`。协议侧符号不只是成员,需要自己的证据模型,已回退并在代码注释里写明原因。 + +## 验证 + +- **红/绿**:源码改动整体回退后新增的 5 条测试红 3 条(9 个 issue),恢复后 5 条全绿。 +- **fixture 复现**:`open class Host` 三个空方法 + 一个嵌套 `class Nested` 的空方法,`-Xlinker -deduplicate` 强制折叠。修复前 dump 出 `beta`/`alpha`/`gamma`(符号表顺序),修复后 `alpha`/`beta`/`gamma`(`Tq` 地址顺序)。嵌套串味在这个规模复现不出来(取决于 linker 把嵌套成员排在符号表的哪个位置),相关两条测试是防御性的,已在注释里写明。 +- **全库 A/B**(SwiftUICore):828 条声明行变化;7 处 `.resume.` 串味全部消失、零新增;`Symbol not found` 358 → 0(195 条拿到真名,163 条转为带墓碑注释的 ``);零行从有名字退化成 `sub_` 地址。 +- **机械比对**:1199 个 `Tq` 符号按地址排序作真值与 dump 槽序列逐类比对,55 个可比对的类里 54 个相对顺序完全一致,0 个顺序错配(余下 1 个是比对脚本把嵌套类成员按名字前缀算给了外层类)。 +- **`validNode` 隔离 A/B**:单独回退这一处,输出逐字节不变——`Tq` 主源已覆盖所有出问题的槽,它是纯加固。 +- **快照**:10 份基线更新,逐条审查确认全部为修正(详见提案决策日志)。其中两条意外收获:`FinalMembersTest` 的 kind 注释与名字系统性错位(`[Setter]` 配 `plainMethod()`)得到修正;interface 里 `classMethod` 从 `static func` 修正为 `class func`——fixture 源码写的就是 `public class func`,此前因归属错位没 join 上 descriptor 而误印。 +- **全量测试**:`swift test --skip IntegrationTests` 通过(退出码取自 `swift test` 本身,不看 xcsift 摘要)。 + +## 与提案的偏离 + +- 提案原计划三类 descriptor 都用 `Tq`,实际收窄到只有 `MethodDescriptor`,理由见上。 +- 提案原计划一并修 `ProtocolDumper` 的同类模式,实测退化后回退。 +- 提案说渲染 A/B 脚本的逐字节判据「会红且属预期」,实际改用上面的定量比对(`Tq` 真值机械核对 + 分类统计)作为验收证据,比人工看 diff 更可证伪。 + +## 环境备忘 + +新建的 worktree 里 `Tests/Projects/SymbolTests/DerivedData` 不存在(gitignore 从不检出),fixture 绑定的测试会在毫秒内全部失败并报 `NSCocoaErrorDomain Code=4 "The file 'SymbolTestsCore' doesn't exist."`。本次按 AGENTS.md 的规程处理:确认分支相对 `next` 在 `Tests/Projects/` 下无 diff、且仓库根已有的 fixture 二进制比源码新,然后符号链接过去,未重新构建。 diff --git a/Documentations/README.md b/Documentations/README.md index ab4484f4..4e685e12 100644 --- a/Documentations/README.md +++ b/Documentations/README.md @@ -38,6 +38,11 @@ Everything under [`Internal/`](Internal/) is maintainer-facing. Design notes, migration guides, refactor write-ups, and per-task reports for contributors to this repository. Not part of the public documentation surface (mixed Chinese / English). +**Per-module reference docs** live in [`Internal/Modules/`](Internal/Modules/README.md) — one doc +per library module (what it is, its subsystems, their contracts, and where the detail docs are), +the authoritative entry point for each module. Coverage status is tracked in that directory's +README; topic docs stay in the flat `Internal/` layer and are linked from the module docs. + **Start here for history:** [ProjectEvolutionLog.md](Internal/ProjectEvolutionLog.md) is the chronological ledger of the library's own evolution — one section per work arc (period, motivation, key decisions, landed modules, doc links, version range), maintained on every @@ -48,6 +53,9 @@ required by `Version.swift`'s bump contract). | Doc | What it covers | |---|---| +| [Modules/](Internal/Modules/README.md) | **按模块组织的参考文档系列**:每个库模块一篇权威入口(定位 / 子系统分工 / 跨文件契约 / 细节文档指路);该目录 README 是覆盖状态表。 | +| [Modules/MachODependencies.md](Internal/Modules/MachODependencies.md) | MachODependencies 模块参考:所有功能共用的依赖解析——搜索路径、load name 归一(与 `MachOImage(name:)` 的契约)、两种定位器(进程内 / 文件:install path 精确优先、bare name 排序兜底、cache 一次性索引)、direct / transitive 遍历与顺序契约、未解析清单;SwiftLayout 与 SwiftInterface 两处薄包装的语义边界与测试锚点。 | +| [Modules/SwiftInterface.md](Internal/Modules/SwiftInterface.md) | SwiftInterface 模块参考:编排层定位与三种输出产品(单版本 interface / 两侧 diff / N 路 evolution),五个子系统(核心 builder、opaque 解析、共享 union 走查、diff 渲染、evolution 渲染)的分工、契约与测试锚点,消费入口速查。 | | [ProjectEvolutionLog.md](Internal/ProjectEvolutionLog.md) | 编年演进账本:逐工作弧(Foundation 解析 → demangler → 模块化 → SwiftLayout → SwiftDiffing/ABI evolution …)的时间段/动机/关键决策/落地文档/版本对应,含每批次必须追加的维护约定。 | | [ReviewAdjudications.md](Internal/ReviewAdjudications.md) | Review 已裁决清单:判定为「不修 / 误报」的发现及结论、理由、复审条件;每轮 code review 先对照此表,已裁决且理由仍成立的直接跳过。 | | [SwiftModularizationMigration.md](Internal/SwiftModularizationMigration.md) | The `SwiftInterface` monolith → layered peer modules refactor; where everything moved. | @@ -57,7 +65,7 @@ required by `Version.swift`'s bump contract). | [NestedFieldOffsetCycleGuard.md](Internal/NestedFieldOffsetCycleGuard.md) | 嵌套字段偏移展开在**有环**类型图上的指数级路径枚举(`DVTIconKit` "死循环")与两道守卫:`indirect` case 是堆 box 指针故报告但不下钻(值类型字段图唯一的成环途径,实际消除爆炸的那道),以及**路径作用域**的已打开类型集合(运行时按 metatype 指针、静态按打印类型名)作为解析误判造成假环的纵深防御。含"为什么深度上限约束不了路径数"、按路径而非全局的取舍、运行时/静态两条实现各自的回归套件,以及 2026-05-16 打印路径 DAG 爆炸修复的教训为何没能横移过来的查证。 | | [StaticFieldOffsetComputation.md](Internal/StaticFieldOffsetComputation.md) | Research + implementation guide for computing stored-property field offsets statically (offline, no runtime): fixed-layout vs resilient, the `performBasicLayout` algorithm, `MetadataInitialization` triage, the dependency-closure type resolver, ObjC ancestors via MachOObjCSection, and a generics difficulty assessment. | | [StaticLayoutEngine.md](Internal/StaticLayoutEngine.md) | The shipped `SwiftLayout` module: what was actually built for static field-offset computation (recompute via `performBasicLayout` rather than reading the vector), the file structure, the runtime-accessor-vs-static validation suite, empirical findings that diverged from the research, and the known per-field degradations. Existentials (opaque / class-bound / error / metatype), the default-actor storage builtin, cross-module field/superclass/protocol types (via the dependency closure), ObjC-ancestor classes (Phase 4 — a Swift class deriving from `NSObject` et al. starts its fields at the ObjC ancestor's `instanceSize`, read via `MachOObjCSection`), multi-payload enums + imported C value types (via `__swift5_builtin` whole-type layouts), imported-ObjC-protocol existentials (`any NSCopying`), C-function-pointer / ObjC-block fields, and concrete bound-generic instantiations as fields (Phase 5 — purely syntactic `dependentGenericParamType` substitution via `GenericArgumentEnvironment`, depth-0 type parameters) are resolved; only a top-level generic type's own unsubstituted parameters, value/pack arguments, and depth>0 nested-context parameters remain degraded. | -| [StaticLayoutDependencyClosure.md](Internal/StaticLayoutDependencyClosure.md) | Phase-3 (**shipped**): extends `SwiftLayout` from single-image to a dependency closure (`LC_LOAD_DYLIB` + dyld shared cache) so cross-module field/superclass/protocol types resolve, with zero resolver changes. Covers the homogeneous-per-root typing decision, the `ImageUniverse.dependencyClosure` factory, the resilient-class static-computability boundary (and why their runtime field-offset vector is empty), and the validation strategy — plus a "落地实测" section recording where the implementation diverged from the plan (lazy per-image indexing over a 551-image closure, bare-name matching, missing-section tolerance, one-shot cache indexing, literal pinning for resilient classes that emit no `…Wvd` global). ObjC ancestors were resolved by Phase 4 (`ObjCClassIndex` + a third `resolveObjCClassInstanceSize` seam; see StaticLayoutEngine.md). | +| [StaticLayoutDependencyClosure.md](Internal/StaticLayoutDependencyClosure.md) | Phase-3 (**shipped**): extends `SwiftLayout` from single-image to a dependency closure (`LC_LOAD_DYLIB` + dyld shared cache) so cross-module field/superclass/protocol types resolve, with zero resolver changes. Covers the homogeneous-per-root typing decision, the `ImageUniverse.dependencyClosure` factory, the resilient-class static-computability boundary (and why their runtime field-offset vector is empty), and the validation strategy — plus a "落地实测" section recording where the implementation diverged from the plan (lazy per-image indexing over a 551-image closure, bare-name matching, missing-section tolerance, one-shot cache indexing, literal pinning for resilient classes that emit no `…Wvd` global). ObjC ancestors were resolved by Phase 4 (`ObjCClassIndex` + a third `resolveObjCClassInstanceSize` seam; see StaticLayoutEngine.md). **2026-09-02 起**定位器与遍历已下沉为共享模块 `MachODependencies`(见 [Modules/MachODependencies.md](Internal/Modules/MachODependencies.md)),本文保留为阶段 3 的设计与实测记录。 | | [LeafMigrationPlan.md](Internal/LeafMigrationPlan.md) | Plan for making `SwiftDump` a leaf module. | | [SpecializedInterfaceBoundRenderingRestoration.md](Internal/SpecializedInterfaceBoundRenderingRestoration.md) | 修复 leaf 迁移引入的回归:interface 路径重新对特化定义做绑定渲染——头部打印 `Box`(跳过泛型签名子句)、字段经特化 metadata 替换;机制经 `SpecializedMetadataNodeSubstitution` + 下移的 `BoundDumpedTypeNameRenderer` 落在 `SwiftDeclarationRendering`,dump 路径零变化。 | | [LeafMigrationRegressionAudit.md](Internal/LeafMigrationRegressionAudit.md) | 对 `aa233bc` leaf 迁移线的全面回归审计:三路逐行比对方法、7 项问题清单(多 payload 枚举容错丢失、深度截断诊断静默 + 测试钉死常量、Void payload case 两路不一致等——已于 2026-07-31 全部修复,见 LeafMigrationRegressionFixes.md)、已修复的历史断裂记录(metadata 注释全丢 / SIGBUS / 绑定渲染回归)与已核对干净的面。 | @@ -76,17 +84,26 @@ required by `Version.swift`'s bump contract). | [EnumLayoutAuditFixes.md](Internal/EnumLayoutAuditFixes.md) | 对照 Swift 官方源码(`EnumImpl.h` / `Enum.cpp` / `GenEnum.cpp` / `TypeLowering.cpp`)的枚举布局全面审计与五项修复:indirect 单 payload 的 heap-pointer XI(曾被误判为 overflow 布局)、枚举自身 VWT 的 size 交叉校验与 payloadXI 精确反推、spare-bits payload case 的位级 `fixedBitMasks`(不再整字节过度声明)、empty case 判别区完整记录(tagged 零扩展 + spare-bits 全位固定)、no-payload XI 封顶;runtime 对拍测试增量与 RuntimeViewerCore token 同步。 | | [OutputTransformerMigration.md](Internal/OutputTransformerMigration.md) | `Transformer` 模板机制的 Swift 侧(注释 token 模板 + 预设)从 RuntimeViewerCore 迁入库侧的新 `OutputTransformer` 模块(ObjC 侧 CType/ivarOffset 暂留 RV):架构(模块清单、宽容 Codable 持久化契约、SwiftInspection 桥接、闭包工厂 + `applyTransformers` 接线)、RV 兼容语义(auto-append、partial-mask 安全回退)、RV 侧收编为 `@_exported` shim + 一行接线。 | | [CLITransformerTemplateInterface.md](Internal/CLITransformerTemplateInterface.md) | `swift-section` 的注释模板命令行入口:三层配置(`--transformer-config` JSON 文件 / `--enum-layout-style` 整模块预设 / 逐模块模板选项)与其优先级、"内置模板名 vs 字面模板" 的解析规则(未知名字报错而非退化)、"启用的模块自动打开对应注释开关" 规则、`transformer tokens/templates/config` 发现性子命令,以及 `interface` 补齐 `--emit-type-layout` / `--emit-enum-layout`。 | -| [ReadingContextAbstraction.md](Internal/ReadingContextAbstraction.md) | The `ReadingContext` reading-abstraction design. | +| [ReadingContextAbstraction.md](Internal/ReadingContextAbstraction.md) | The `ReadingContext` reading-abstraction design, including the 2026-05 model-coverage completion pass (`runtimePointer(at:)` extension-not-requirement decision). | +| [FixtureTestingAndContinuousIntegration.md](Internal/FixtureTestingAndContinuousIntegration.md) | **Fixture 测试体系与 CI 的设计来历**(整合自已删除的 `docs/superpowers/` 四份 spec + CI 落地记录):为什么 fixture 只用 SymbolTestsCore、`#filePath` 路径锚定陷阱、命名空间约定与边界规则、ABI 覆盖四支柱与 sentinel 信任危机(88/157 suite 失真)的收紧、CI 白名单 regex 与首轮五坑。现行操作规程仍以 AGENTS.md 为准。 | | [ClassMemberKeywordRecovery.md](Internal/ClassMemberKeywordRecovery.md) | `class` / `static` 成员关键字的还原:mangling 层面两者不可区分,判据是「类型级成员有 vtable method descriptor ⇒ 源码是 `class`」(`static` 隐式 final、不进 vtable);模型侧 `isClassMember` 计算属性 + 三个 node printer 接线 + dump vtable 段落关键字,顺带消灭非法的 `override static` 输出;`final class func` 等四类 ABI 上与 `static` 完全一致,保守输出语义等价的 `static`。 | +| [SelfContainedABILayer.md](Internal/SelfContainedABILayer.md) | ABI 层自包含(提案 `self-contained-abi-layer` 的实现说明):`MachOSwiftSection` 只依赖新伞模块 `MachOBase`,描述符只暴露 `implementationOffset` / `implementationAddress(in:)`,符号归属上移 `SwiftInspection`,值类型下沉 `MachOResolving`;记录为什么多一个伞模块、为什么 `ReadingContext` 腿没有符号形态(旧腿在读机器码)、为什么不留 typealias、为什么 async 重载只能删;附下游迁移表。 | +| [LargeStackTaskExecutorAdoption.md](Internal/LargeStackTaskExecutorAdoption.md) | 大栈任务执行器接入与跨版本并行(提案 `large-stack-executor-and-cross-version-parallelism` 的实现说明):为什么打印路径每个符号付一次线程往返、为什么执行器按剩余栈探测就能让整个 task 内联、`LargeStackTaskExecution.run` 包了哪些入口与嵌套为何免费、macOS 15 以下的静默回退、跨版本并行为什么安全而版本内并行为什么不做(MachOKit 共享 FileHandle)、`--jobs` 与 `concurrentMap(maximumConcurrency:)` 的语义;附 0.6.0 / 0.6.3 / 执行器开关的计时表与渲染 A/B 结论。 | | [ExtensionContainerUnification.md](Internal/ExtensionContainerUnification.md) | Extension 容器统一(提案 0007 的实现说明):双产线重复(协议尾随 descriptor 副本 + 符号扫描桶副本)的「附着 + 打印抑制」消解——桶是 ABI 快照的直接输入故不可移除,附着对象留桶打标、顶层打印跳过;descriptor 合成降级为 fallback(ICF 地址上丢成员);桶内同身份合并、空 requirement 签名桶折叠、`updateConfiguration` no-op 修复、嵌套协议扩展块死循环修复;裸头 typealias 块并存为 P1-9 残余(格式冻结约束下不合并)。 | | [FinalKeywordAndLazyAccessorTypeRecovery.md](Internal/FinalKeywordAndLazyAccessorTypeRecovery.md) | `final` 成员关键字还原与 lazy var 访问器类型修正(提案 0006 的实现说明):核心是停止丢弃 `DefinitionBuilder` 已解析的 stored-var accessor→vtable 归属;三层证据门(非 actor class 有 vtable header / accessor 组确实 join 上 / `@objc` 排除)宁缺勿错;`memberJoinKey` 剥 `Tu` async-function-pointer 标记(顺带修复 async 成员一直缺失的 `override` 与 vtable 注释);final class 的成员级 `final`、stored `let` 不标、dump 路径 lazy 保持存储真相等决策与降级。 | | [InterfaceHeaderAndExportStatusAnnotations.md](Internal/InterfaceHeaderAndExportStatusAnnotations.md) | Interface 文件头部与导出状态标注(提案 0008 的实现说明):导出集为何必须在构建扫描里显式旁路收集(symtab 两腿只收本地符号、trie 腿建行有条件);裸查实现符号在 evolution 构建上全量假阳性 → 派生符号形态查询(`Tj`/`Tq`/`Tu`/`TjTu`);`override` / `@objc` 发射豁免与 conformance witness 故意不豁免的边界;头部组件的调用方传入 generator 身份、日期缺席字节稳定、evolution 行 detected/not detected 措辞;dump 路径的符号级语义收窄与已知残留。 | +| [ExportedOnlyInterfaceFiltering.md](Internal/ExportedOnlyInterfaceFiltering.md) | Interface 只打印导出声明(提案 0016 的实现说明,0008 标注的过滤形态):为什么过滤放打印期;类型级判据为何「先反查描述符 offset 处的符号、重整名只兜底且拒绝扩展上下文」(带约束扩展里的公开嵌套类型被第一版误删);扩展判据为何必须靠索引器的表(`ExportFilterScope`)而非符号推断;空扩展的两条规则;三个打印入口的「过滤壳 + builder 体」拆分与事件配对契约;字段循环先筛后印保原始下标;已知降级(引用不改写、C 导入类型被过滤、宿主须自装 scope)。 | | [NodeStoreMigrationPlan.md](Internal/NodeStoreMigrationPlan.md) | NodeStore 迁移(把符号 demangle 结果从每节点 48 字节的 class `Node` 树,换成 12 字节/节点的扁平 arena 存储)的分期计划与逐批实施记录。看点:为什么分五个阶段、每阶段的实测收益,以及 Stage 5a 踩过的大坑——跨 store 的 `NodeReference` 字典键静默失配导致 `override` 关键字丢失,教训固化成 `StructuralNodeReferenceKey` 键规则。 | | [SharedNodeStoreMigration.md](Internal/SharedNodeStoreMigration.md) | **Implemented(2026-08-08)**:三条各自新建私有小 store 的流水线(`InternedNodeReferenceCache` / `TypeDefinition` 字段树 / `lateDemangledNode`)合并到共享的 `SharedNodeStore`(上游 swift-demangling 提案 0010),驻留 store 数从上万收敛到每镜像个位数。记录改了哪里、哪些故意没改、验证结果与和方案的差异。 | | [MetadataReaderCacheRetirement.md](Internal/MetadataReaderCacheRetirement.md) | **Implemented(2026-08-08)**:`MetadataReader` 的三张缓存字典从持有 class `Node` 树改持 `NodeReference`——它们是 NodeStore 迁移后残留 18.4 万个 `Node` 的持有主体。公开 API 与 103 处调用点零改动;RuntimeViewer 实测存活 `Node` 从 207,489 降到 44(−99.98%)。 | | [Evolutions/0001-symbol-name-offsetization.md](Evolutions/0001-symbol-name-offsetization.md) | **提案 0001(Implemented)**:符号名不再拷成 `String` 常驻内存(49.4 万个 / 68.7 MiB,当时堆内最大单项),改存字符串表位置、用时现读;查名字典整个退役,换名字序二分。RuntimeViewer 稳态 445 → 322 MB(−28%),公开 API 仅一处破坏(`Symbol.nlist` → `isExternal`)。提案头部有一页导读。 | | [Evolutions/0002-declaration-model-descriptor-slimming.md](Evolutions/0002-declaration-model-descriptor-slimming.md) | **提案 0002(Implemented)**:声明模型 descriptor 化——`TypeDefinition` / `ExtensionDefinition` / `ProtocolDefinition` 不再终身驻留急切解析的胖 wrapper(`TypeContextWrapper` 472 B × 2 份、`ProtocolConformance` 及其 `[ResilientWitness]`、`Protocol`),改存几十字节级 descriptor 引用,trailing 解析在惰性 `index()` / 打印期临时物化;`parentContext` 降级为索引期局部载体。0001 后堆内新头部(声明模型 41.3 + MachOSwiftSection 簇 33.4 MiB)的对症案,预估再省 30–45 MiB;破坏性 API 变更(三处属性换形态,机械迁移)。 | | [Evolutions/0003-symbol-row-bucket-flattening.md](Evolutions/0003-symbol-row-bucket-flattening.md) | **提案 0003(Implemented)**:符号索引里 45 万个小数组桶(绝大多数只装一个元素,却各付一次堆分配)换成单元素内联的 `SymbolRowBucket`,38.8 → 7.2 MiB。零 API 变化,输出逐字节不变。提案头部有一页导读。 | +| [Evolutions/0016-exported-only-interface.md](Evolutions/0016-exported-only-interface.md) | **提案 0016(Implemented)**:Interface 只打印导出声明(`--exported-only`)——提案 0008 标注的过滤形态,打印期按类型 / 协议描述符符号、成员派生符号、扩展目标是否为本镜像内未导出声明裁决;`false` 才删、`nil` 一律留;普通扩展被清空整块删、conformance 扩展留 `{}`;只做 interface。 | +| [Evolutions/0017-macho-dependencies-module.md](Evolutions/0017-macho-dependencies-module.md) | **提案 0017(Implemented)**:依赖闭包下沉为底层 `MachODependencies` 模块——把 SwiftLayout 的传递闭包(BFS + bare name 去重 + cache 一次性索引)与 SwiftInterface 的直接依赖加载合成一套可复用 API(`DependencyClosure` / `DependencySearchPath` / 定位器),两处消费者改薄包装、各自语义保持;匹配规则取并集(install path 精确优先、bare name 排序兜底),顺带修 `MachOImage` 版永远解析为空的静默 bug。 | +| [Evolutions/0018-self-contained-abi-layer.md](Evolutions/0018-self-contained-abi-layer.md) | **提案 draft(In Progress)**:ABI 层自包含——五个描述符的 `RelativeDirectPointer` 字段让一次 ABI 访问触发整镜像符号扫描,`ReadingContext` 腿还在把机器码当 `Symbols` 结构体读;改为只暴露 `implementationOffset`,符号查询作为扩展上移 `SwiftInspection`,`Symbol` / `Symbols` / `SymbolOrElement` 下沉 `MachOResolving`、`MachOSymbolPointers` 并入 `MachOPointers`,`MachOSwiftSection` 只依赖三个底层模块并摘掉 `Demangling`。 | +| [Evolutions/0019-large-stack-executor-and-cross-version-parallelism.md](Evolutions/0019-large-stack-executor-and-cross-version-parallelism.md) | **提案 0019(Implemented)**:大栈任务执行器接入与跨版本并行——库入口用 `withTaskExecutorPreference` 让整个任务跑在 swift-demangling 0.6.3 提供的 16 MB 线程上,demangle / print 探测直接通过、零跳转(macOS 15 以下静默回退);diff / evolution 的多版本准备改 task group 并行,上限取核数、CLI `--jobs`。全库 async 化被否决的理由在动机里。 | +| [Evolutions/0020-vtable-slot-attribution-via-method-descriptor-symbols.md](Evolutions/0020-vtable-slot-attribution-via-method-descriptor-symbols.md) | **提案 0020(Implemented)**:vtable 槽归属改用 method descriptor 符号——今天靠「实现地址反查符号」定名,identical code folding 之下这个映射不可逆(`SwiftUI.GraphHost` 四个空实现折叠在一个地址上,那里有 2878 个符号,槽 26–29 全部错名、三条串到嵌套 `GraphHost.Data` 的协程 resume 函数);改用每个 descriptor 自带的、ICF 免疫的 `Tq` 符号作归属主源,实现地址反查降级为回退,顺带修 `first(of: .class)` 把嵌套类型成员当本类成员的筛选漏洞,并还原墓碑槽(实现已删、槽位为 ABI 保留)的名字。 | | [Glossary.md](Glossary.md) | **项目术语表**:sweep、腿(reader-split leg)、名字来源、detach、物化、permutation 二分、store-identity vs 结构相等、wrapper vs descriptor、桶、trailing objects 等本项目自造词与特定用法;跨项目通用术语在全局表(iCloud Global),不重复登记。提案与专题文档引入新术语时同批登记。 | | [SymbolIndexStoreMemoryOptimization.md](Internal/SymbolIndexStoreMemoryOptimization.md) | **`SymbolIndexStore` 内存优化专题——读这批内存文档先读这篇**:三波优化各自解决什么、今天的存储模型长什么样、付出了哪些约束、实测收益全曲线(RuntimeViewer 五镜像稳态 842 → 262 MB)。 | | [DeclarationModelMemoryFootprint.md](Internal/DeclarationModelMemoryFootprint.md) | NodeStore 迁移后的声明模型内存足迹量测:`TypeDefinition` 1272 字节的逐属性构成(两份 `TypeContextWrapper` 占 74%)、`TypeContextWrapper` 按最大 case `Class` 定尺的原因、`parentContext` 是被当成永久字段的临时值、mini-store 增殖与 `MetadataReaderCache` 仍持 `Node` 树;含四项可回收估算(合计 ~8–10%)与「当前不建议实施、应先剖析其余 90%」的结论。 | diff --git a/Package.swift b/Package.swift index 3bb859b4..e23cc527 100644 --- a/Package.swift +++ b/Package.swift @@ -159,7 +159,7 @@ extension Package.Dependency { ), remote: .package( url: "https://github.com/MxIris-Reverse-Engineering/swift-demangling", - "0.6.0" ..< "0.7.0", + "0.6.3" ..< "0.7.0", ), ) @@ -266,6 +266,20 @@ extension Target { ], ) + /// Dependency resolution shared by every feature that needs a binary's + /// linked images (evolution proposal macho-dependencies-module): + /// search paths, the in-process / on-disk locators, and the direct or + /// transitive `DependencyClosure` walk over `LC_LOAD_DYLIB`. Knows nothing + /// about Swift metadata, so it sits with the other MachO* leaf targets and + /// is re-exported by `MachOFoundation`. + static let MachODependencies = Target.target( + name: "MachODependencies", + dependencies: [ + .product(.MachOKit), + .product(.MachOKitExtensions), + ], + ) + static let MachOReading = Target.target( name: "MachOReading", dependencies: [ @@ -301,20 +315,24 @@ extension Target { name: "MachOPointers", dependencies: [ .product(.MachOKit), + .product(.MachOKitExtensions), .target(.MachOReading), .target(.MachOResolving), .target(.Utilities), ], ) - static let MachOSymbolPointers = Target.target( - name: "MachOSymbolPointers", + /// The reader / resolver / pointer layer as one import: everything the + /// ABI model is allowed to depend on. `MachOFoundation` adds the symbol + /// index and dependency resolution on top (evolution proposal + /// `self-contained-abi-layer`). + static let MachOBase = Target.target( + name: "MachOBase", dependencies: [ - .product(.MachOKit), + .product(.MachOKitExtensions), .target(.MachOReading), .target(.MachOResolving), .target(.MachOPointers), - .target(.MachOSymbols), .target(.Utilities), ], ) @@ -323,13 +341,9 @@ extension Target { name: "MachOFoundation", dependencies: [ .product(.MachOKit), - .target(.MachOReading), - .product(.MachOKitExtensions), - .target(.MachOPointers), + .target(.MachOBase), .target(.MachOSymbols), - .target(.MachOResolving), - .target(.MachOSymbolPointers), - .target(.Utilities), + .target(.MachODependencies), ], ) @@ -337,14 +351,16 @@ extension Target { name: "MachOSwiftSectionC", ) + /// The ABI model. Depends on the reader / resolver / pointer layer only: + /// no symbol index, no demangler (evolution proposal + /// `self-contained-abi-layer`). static let MachOSwiftSection = Target.target( name: "MachOSwiftSection", dependencies: [ .product(.MachOKit), - .product(.Demangling), - .target(.MachOFoundation), - .target(.MachOSwiftSectionC), + .target(.MachOBase), .target(.Utilities), + .target(.MachOSwiftSectionC), ], ) @@ -378,6 +394,7 @@ extension Target { .target(.MachOSwiftSectionC), .target(.Utilities), .target(.SwiftOutputTransformer), + .target(.MachOFoundation), ], ) @@ -393,6 +410,8 @@ extension Target { .product(.MachOKit), .product(.MachOObjCSection), .product(.Demangling), + .target(.MachODependencies), + .target(.MachOFoundation), .target(.MachOSwiftSection), .target(.SwiftInspection), .target(.Utilities), @@ -414,6 +433,8 @@ extension Target { .product(.Demangling), .product(name: "FoundationToolbox", package: "FrameworkToolbox"), .target(.MachOCaches), + .target(.MachODependencies), + .target(.MachOFoundation), .target(.MachOSwiftSection), .target(.Utilities), .target(.SwiftOutputTransformer), @@ -433,6 +454,7 @@ extension Target { .target(.Utilities), .target(.SwiftInspection), .target(.SwiftDeclarationRendering), + .target(.MachOFoundation), ], ) @@ -457,6 +479,7 @@ extension Target { .target(.SwiftInspection), .target(.SwiftDeclarationRendering), .target(.Utilities), + .target(.MachOFoundation), ], ) @@ -474,6 +497,7 @@ extension Target { .target(.SwiftInspection), .target(.Utilities), .target(.SwiftDeclaration), + .target(.MachOFoundation), ], ) @@ -525,6 +549,7 @@ extension Target { .target(.Utilities), .target(.SwiftDeclaration), .target(.SwiftAttributeInference), + .target(.MachOFoundation), ], ) @@ -558,6 +583,8 @@ extension Target { .product(.MachOObjCSection), .product(.Semantic), .product(.Demangling), + .target(.MachODependencies), + .target(.MachOFoundation), .target(.MachOSwiftSection), .target(.SwiftInspection), .target(.SwiftDeclarationRendering), @@ -602,6 +629,7 @@ extension Target { .target(.TypeIndexing), .product(name: "Rainbow", package: "Rainbow"), .product(name: "ArgumentParser", package: "swift-argument-parser"), + .target(.MachOFoundation), ], ) @@ -712,6 +740,7 @@ extension Target { .target(.MachOTestingSupport), .target(.MachOFixtureSupport), .product(.Demangling), + .target(.MachOResolving), ], swiftSettings: testSettings, ) @@ -723,6 +752,9 @@ extension Target { .target(.MachOTestingSupport), .target(.MachOFixtureSupport), .target(.SwiftDump), + .target(.SwiftInspection), + .target(.MachOFoundation), + .product(.Demangling), ], swiftSettings: testSettings, ) @@ -736,6 +768,18 @@ extension Target { swiftSettings: testSettings, ) + static let MachODependenciesTests = Target.testTarget( + name: "MachODependenciesTests", + dependencies: [ + .target(.MachODependencies), + .target(.MachOTestingSupport), + .target(.MachOFixtureSupport), + .product(.MachOKit), + .product(.MachOKitExtensions), + ], + swiftSettings: testSettings, + ) + static let SwiftOutputTransformerTests = Target.testTarget( name: "SwiftOutputTransformerTests", dependencies: [ @@ -765,6 +809,7 @@ extension Target { .target(.MachOTestingSupport), .target(.MachOFixtureSupport), .product(.Demangling), + .target(.MachOFoundation), ], swiftSettings: testSettings, ) @@ -779,6 +824,7 @@ extension Target { .product(.Semantic), .product(.Demangling), .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), + .target(.MachOFoundation), ], swiftSettings: testSettings, ) @@ -803,6 +849,7 @@ extension Target { .target(.MachOTestingSupport), .target(.MachOFixtureSupport), .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), + .target(.MachOFoundation), ], swiftSettings: testSettings, ) @@ -816,6 +863,7 @@ extension Target { .target(.SwiftDump), .target(.MachOTestingSupport), .target(.MachOFixtureSupport), + .target(.MachOFoundation), ], swiftSettings: testSettings, ) @@ -831,6 +879,7 @@ extension Target { .target(.MachOFixtureSupport), .product(.Semantic), .product(.Demangling), + .target(.MachOFoundation), ], swiftSettings: testSettings, ) @@ -843,6 +892,7 @@ extension Target { .target(.SwiftAttributeInference), .target(.MachOTestingSupport), .target(.MachOFixtureSupport), + .target(.MachOFoundation), ], swiftSettings: testSettings, ) @@ -852,6 +902,7 @@ extension Target { dependencies: [ .target(.SwiftDeclaration), .target(.SwiftDiffing), + .target(.MachOFoundation), ], swiftSettings: testSettings, ) @@ -878,6 +929,7 @@ extension Target { .target(.SwiftInspection), .target(.MachOTestingSupport), .target(.MachOFixtureSupport), + .target(.MachOFoundation), ], swiftSettings: testSettings, ) @@ -891,6 +943,7 @@ extension Target { .target(.SwiftSpecialization), .target(.MachOTestingSupport), .target(.MachOFixtureSupport), + .target(.MachOFoundation), ], swiftSettings: testSettings, ) @@ -917,7 +970,6 @@ extension Target { .target(.MachOResolving), .target(.MachOSymbols), .target(.MachOPointers), - .target(.MachOSymbolPointers), .target(.MachOFoundation), .target(.MachOSwiftSection), .target(.SwiftInspection), @@ -945,6 +997,13 @@ let package = Package( platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .visionOS(.v1)], products: [ .library(.MachOSwiftSection), + // The ABI model no longer re-exports the symbol index (evolution + // proposal `self-contained-abi-layer`), so a downstream target that + // uses `SymbolIndexStore` / `DemangledSymbol` / `DependencyClosure` + // depends on `MachOFoundation` (or the lower `MachOBase`) explicitly. + .library(.MachOBase), + .library(.MachOFoundation), + .library(.MachODependencies), .library(.SwiftOutputTransformer), .library(.SwiftInspection), .library(.SwiftLayout), @@ -966,11 +1025,12 @@ let package = Package( .Utilities, .SwiftOutputTransformer, .MachOCaches, + .MachODependencies, .MachOReading, .MachOResolving, .MachOSymbols, .MachOPointers, - .MachOSymbolPointers, + .MachOBase, .MachOFoundation, .MachOSwiftSectionC, .MachOSwiftSection, @@ -1002,6 +1062,7 @@ let package = Package( .MachOSymbolsTests, .MachOSwiftSectionTests, .MachOCachesTests, + .MachODependenciesTests, .SwiftInspectionTests, .SwiftOutputTransformerTests, .SwiftLayoutTests, diff --git a/README.md b/README.md index ccd7845e..fa84fafa 100644 --- a/README.md +++ b/README.md @@ -290,9 +290,18 @@ swift-section interface --emit-header /path/to/binary # `@objc` members are exempt — they are reachable through the parent's # dispatch thunk / objc_msgSend without any exported symbol of their own. swift-section interface --emit-export-status /path/to/binary + +# Print only the declarations the image exports — the filtering counterpart of +# --emit-export-status. Types and protocols are ruled by their descriptor +# symbol's export-trie entry, extensions by whether their target is an +# in-image non-exported declaration, members by the same derived-form verdict +# the annotation uses. Still a symbol-table fact, never an access-level guess: +# anything without export evidence (and every `override` / `@objc` member) is +# kept, so an `-enable-testing` build keeps its `internal` declarations. +swift-section interface --exported-only /path/to/binary ``` -Both flags default to off, keeping default output byte-identical. +All three flags default to off, keeping default output byte-identical. **Working with dyld shared cache:** diff --git a/Roadmaps/2026-09-04-pr121-review-findings.md b/Roadmaps/2026-09-04-pr121-review-findings.md new file mode 100644 index 00000000..af82c921 --- /dev/null +++ b/Roadmaps/2026-09-04-pr121-review-findings.md @@ -0,0 +1,32 @@ +# PR #121 review findings(ABI 层自包含,2026-09-04) + +并行 review 会话(machoswiftsection-71,xhigh code-review)对 PR #121(`feature/self-contained-abi-layer` → `next`,merge-base `f3782248`)的 12 条发现,全部复核为真、无误报。本表是原始清单与处置状态;「延后」的终审条目收录进 [ReviewAdjudications.md](../Documentations/Internal/ReviewAdjudications.md)(A23、A24)。 + +用户裁定:A–H 与 I、K、L 全修;J 只统一 `ProtocolConformanceDumper` 一个文件,全量迁移延后;async 提案草稿保留在本 PR 内。 + +## 合并前必修(5 条) + +| # | 位置 | 问题 | 状态 | +|---|---|---|---| +| A | `Package.swift` products | `MachOSwiftSection` 不再再导出 `MachOFoundation`,但 manifest 没有 `MachOFoundation` / `MachOBase` / `MachOSymbols` 的 library product,changelog 让下游 `import MachOFoundation` 却无法在下游 manifest 里声明;explicit modules 下直接失败 | **已修**:新增 `.library(.MachOBase)`、`.library(.MachOFoundation)`;changelog 兼容性一节写明下游要加的 product | +| B | `ProtocolConformanceDumper.swift:105` | 空 witness 的地址注释被去掉(旧代码把空指针解析成字段自身位置当地址打印),方向正确但 changelog「输出逐字节一致」未限定 flag,A/B 三条腿都不带 `--emit-member-addresses` | **已修**:changelog 限定为默认 flag 并说明该变化;补跑一次带 `--emit-member-addresses` 的双侧对比(见任务报告) | +| C | `AGENTS.md:123`、`SelfContainedABILayer.md:14`、`String+.swift` 注释 | 声称 `ManglingPrefixTests` 钉住 `CImportedModuleNames`,实际测试只比前缀两个帮手 | **已修**:`ManglingPrefixTests.cImportedModuleNamesMatchTheDemangler` 断言两常量与 demangler 的 `objcModule` / `cModule` 相等 | +| D | `ProtocolRequirementTests.swift:65` | 两个新测试是 `nil == nil` 空转(选中的第一个 requirement 没有默认实现) | **已修**:新增 picker `protocol_BasicDefaultProtocol`,baseline 加 `firstDefaultedRequirement`(第一个 `defaultImplementation.isValid` 的 requirement),两个测试同时断言 nil 例与非 nil 字面量,并断言结果不等于指针字段自身位置;修复前用突变验证会红 | +| E | `Evolutions/README.md:28` | 提案仍 In Progress、未编号;捆绑 async 草稿 | **部分修**:落地 commit 编号 0018 并置 Implemented(与 0017 做法一致);async 草稿按用户裁定保留 | + +## 同批修(3 条) + +| # | 位置 | 问题 | 状态 | +|---|---|---|---| +| F | `Package.swift` | `SwiftLayout` / `SwiftDeclarationRendering` / `SwiftInterface` 新增 `import MachOFoundation`、`MachOSwiftSectionTests` 新增 `import SwiftInspection`,target 依赖未声明 | **已修**:四个 target 补声明 | +| G | `MethodOverrideDescriptorBaselineGenerator.swift:54` | registered 含 `implementationOffset` 但 Entry 不发射,测试只能 `!= nil` | **已修**:Entry 加 `implementationOffset: Int?`,baseline 重生成,测试比字面量;过期头注释改掉 | +| H | `.github/workflows/macOS.yml:109` | CI filter 漏 `MethodDefaultOverrideDescriptorTests` | **已修** | + +## 延后 / 单独处理(4 条) + +| # | 位置 | 问题 | 状态 | +|---|---|---|---| +| I | `Package.swift:357` | `MachOSwiftSection` 丢了 `.target(.Utilities)`;`FoundationToolbox` / `SwiftStdlibToolbox` / `MachOReading` 在 next 上就未声明 | **Utilities 已补**;其余三个延后,见 A23 | +| J | `ClassDumper` / `TypeDefinition` / `ExtensionDefinition` / `ProtocolConformanceDumper` 约 14 处 | 旧写法未迁移到 `implementationOffset` | **ProtocolConformanceDumper 已统一**;其余延后,见 A24 | +| K | `MethodDescriptorTests.swift:89` | image 腿用 file 的 offset 查、无非空守卫,可能 `nil == nil` | **已修**:image 腿用自己的 offset,两侧都断言非空 | +| L | `String+.swift:55` | `strippingSwiftManglingPrefix` 扫两遍前缀 | **已修**:一次扫描 | diff --git a/Roadmaps/2026-09-04-pr122-review-findings.md b/Roadmaps/2026-09-04-pr122-review-findings.md new file mode 100644 index 00000000..c080f991 --- /dev/null +++ b/Roadmaps/2026-09-04-pr122-review-findings.md @@ -0,0 +1,37 @@ +# PR #122 review findings(大栈执行器接入与跨版本并行,2026-09-04) + +并行 review 会话(machoswiftsection-cf,xhigh code-review)对 PR #122(`feature/large-stack-executor-and-cross-version-parallelism` → `feature/self-contained-abi-layer`)的 15 条发现,已按四问逐条裁决:真缺陷 5、误报 1、设计取舍 / 风格 9。本表是原始清单与处置状态;「不修 / 误报 / 延后」的终审条目收录进 [ReviewAdjudications.md](../Documentations/Internal/ReviewAdjudications.md)(A25–A33)。 + +用户裁定:5 条真缺陷全修,另修重复门(后判为不可消除,A32)与冗余 import;F2 用 Dispatcher 进程级锁;F6 保持核数;F7 加输入标签。 + +## 真缺陷(5 条) + +| # | 位置 | 问题 | 状态 | +|---|---|---|---| +| 1 | `Utilities/BoundedConcurrentMap.swift` | `addTask` 不响应取消:宿主取消后剩余元素全部启动(审查者独立编译复现:8 元素窗口 2 全部启动) | **已修**:`addTaskUnlessCancelled`,被拒即抛 `CancellationError`(不返回残缺数组——否则 `result!` 崩溃);`cancellationStopsSubmittingPendingElements` 修复前红(元素 1 启动且不抛错) | +| 2 | `LargeStackTaskExecutionTests` | 四个执行器线程测试只挡 `isSupported`,`MACHO_SWIFT_SECTION_LARGE_STACK_EXECUTOR=0` 下必红 | **已修**:`executorIsActive` 同时看 `isEnabled`;修复前该环境变量下两个测试红,修复后绿 | +| 3 | `LargeStackTaskExecution.swift` | 环境变量只认字面量 `"0"`,`=false` 静默保持开启 | **已修**:`isEnabled(fromEnvironmentValue:)`——`0` / `false` / `no` / `off`(大小写、首尾空白不敏感)为关,其余为开;参数化测试 | +| 4 | `SwiftEvolutionInterfaceBuilderTests` | 先串行再并行,并行那轮全是缓存命中 | **已修**:并行先跑 | +| 5 | `BoundedConcurrentMapTests` | `maximumInFlight <= 3` 退化成串行也绿 | **已修**:新增三方 barrier 测试 `theWindowAdmitsItsFullWidth`(窗口不足 3 即挂起、超时失败) | + +## 误报(1 条) + +| # | 位置 | 结论 | 状态 | +|---|---|---|---| +| F11 | `SwiftDump/Dumpable/*` 六处 `import MachOFoundation` | 审查者按 0018 之前的再导出状态判断;实际必需 | **误报**,见 A25(附带子主张:`FoundationToolbox` 未声明属基线既有归 A23;`AnySwiftEvolutionInterfaceBuilder` 的 `import Utilities` 冗余已删) | + +## 设计取舍 / 风格(9 条) + +| # | 位置 | 问题 | 状态 | +|---|---|---|---| +| F2 | `AnySwiftEvolutionInterfaceBuilder` | 同一 handler 被 N 个版本的任务并发调用,`Handler` 无 `Sendable` | **已修**:`Dispatcher.dispatch` 用进程级 `NSRecursiveLock` 串行化 handler 调用(`EventDeliverySerializationTests`:修复前 4 个 dispatcher 并发投递时 handler 被并发进入,修复后最大在飞 1;可重入不死锁) | +| F6 | `EvolutionCommand` lineage 路径 | 默认窗口取核数,峰值内存倍增 | **保持**,见 A31 | +| F7 | `DiffCommand` / `EvolutionCommand` | stderr 诊断交错、无输入归属 | **已修**:`ConsoleEventHandler(label:)`,`diff` 用 `old` / `new`,`evolution` 每版本用其 label(新 init 参数 `eventHandlersPerVersion`),snapshot 输入用 label 或文件名;`ConsoleEventHandlerLineTests` | +| F8 | 执行器关闭时窗口占满协作线程池 | 属实 | **不修**,见 A30 | +| F4 | `.serialized` 不保护进程级开关 | 属实但无正确性影响 | **不修**,见 A29 | +| F9 | `run` / `isSupported` 双重可用性门 | 属实但不可消除 | **不修**,见 A32 | +| F12 | `run` 未转发 `#isolation` | 惯用法 | **不修**,见 A28 | +| F10 | 与既有 `concurrentMap(_:)` 同名 | 可读性 | **不修**,见 A27 | +| F13 | 逐定义入口每次包一层 | 维护性 | **不修**,见 A26 | + +横向同类:`TypeDatabase.swift:76` 的裸 `addTask`(基线既有)延后,见 A33。 diff --git a/Roadmaps/2026-09-06-pr123-review-findings.md b/Roadmaps/2026-09-06-pr123-review-findings.md new file mode 100644 index 00000000..4847580f --- /dev/null +++ b/Roadmaps/2026-09-06-pr123-review-findings.md @@ -0,0 +1,155 @@ +# PR #123 review findings(vtable 槽归属改用 method descriptor 符号,2026-09-06) + +`/code-review xhigh` 对 PR #123(`feature/vtable-slot-attribution` → `next`,2 个 commit,7 个源文件 + 10 份快照基线 + 1 个新测试文件 + 文档)的 15 条发现,已按四问(复现 / 基线对比 / 值不值得修 / 既往修复)逐条裁决:**真缺陷 4、建议同批修 3、低优先级 3、误报或早有裁决 3、流程 2**。 + +本表是原始清单与处置状态。「不修 / 误报 / 延后」的终审条目收录进 [ReviewAdjudications.md](../Documentations/Internal/ReviewAdjudications.md)(A34–A39)。 + +**当前状态:只落记录,代码未改。** 用户裁定先把审查结论记下来,修复批次另起。 + +对比基线:`git diff next...feature/vtable-slot-attribution`。 + +## 复核(2026-09-07) + +本清单初版落地后交 `MachOSwiftSection-Fable` 复核,两条结论改判,均已就地改写: + +1. **第 1 条改判**:初版判「墓碑注释的因果断言站不住」,实际**断言成立**(IRGen 只有一条写 null 的路径,注释原文就是 dead method elimination),站不住的是措辞;真实根因是**访问级别**(internal 成员在整模块优化下被删实现体),不是初版猜测的 async。已跑独立探针确证,数据写在该条里。SwiftUICore 那 33% 因此**可信**,Glossary 与 AGENTS.md 的对应表述改为死方法消除的措辞而非删除。 +2. **第 5 条升级**:初版只当性能问题(遍历无去重),复核指出它同时是**正确性缺陷**——遍历无差别下降,把闭包 / 默认参数 / 变量初始化表达式的宿主报成声明上下文。白名单下降的修法同时解决两者。 + +复核同时确认 A34 / A35 / A36 三条误报裁决成立,并为 A38 补了一条可行的测试路径。 + +## 一、真缺陷(4 条,待修) + +四条全部集中在本 PR 新增的两行注释上,建议一批改完。 + +### 1. 墓碑注释用词误导(初版判为「说假话」,2026-09-07 改判) + +`Sources/SwiftDeclarationRendering/DeclarationRenderConfiguration.swift:211` + +新注释 `// No implementation in this image (deleted method — slot retained for ABI)` 默认开启、无法关闭。 + +**改判说明**:本条初版判定「因果断言站不住」,依据是 fixture 里源码明确存在的 `init()` 也被打上了这个标签。经 `MachOSwiftSection-Fable` 复核并跑独立探针确证,**因果断言在编译器层面成立**,站不住的只是措辞。初版的错误在于把「声明存在」当成了「实现存在」——被删掉的是函数体,不是声明。 + +- **能复现吗 / 是不是误报**:注释出现的位置属实,但它描述的机制是**真的**,不是本库的推断。IRGen 写 method descriptor 实现指针的 `buildMethodDescriptorFields`(swiftlang/swift,`lib/IRGen/GenMeta.cpp` 约 340–364 行)只有两个分支:SIL vtable 有 entry 就写相对地址,没有就写 null,后者的原注释即 "The method is removed by dead method elimination. It should be never called."。**null 是唯一写入路径**。override descriptor 的 builder(约 2413–2431 行)同形;静态 class metadata 的 `addReifiedVTableEntry`(约 4770–4800 行)对同一情况填 `swift_deletedMethodError`(async / coroutine 各有变体)。 +- **真实根因是访问级别,不是 async**:public 类型里不写修饰符的 `init()` 默认是 **internal**。在 whole-module optimization(整模块优化)的 Release 构建下,internal 成员不是 dead function elimination(死函数消除)的 anchor——`SILLinkage::Hidden` 经 `isPossiblyUsedExternally` 返回 `!wholeModule`(`include/swift/SIL/SILLinkage.h:274`),于是没人调用(或调用点内联后独立函数体死掉)的成员被 `removeDeadEntriesFromTables` 摘掉 vtable entry,IRGen 随即写 null。fixture 的 Xcode 配置正是 `SWIFT_COMPILATION_MODE = wholemodule` + `BUILD_LIBRARY_FOR_DISTRIBUTION = YES`。 + - **被标记的**全是 internal 或函数内局部类的成员:8 个类的隐式 `init()`、`ReferenceFieldTest.init(reference:)`(显式但无修饰符 ⇒ internal)、`ClassSubscriptTest` 的 `private var elements` 三个访问器(旁边 public subscript 的三个访问器都有名字)、`LocalClass` 的四个成员;`SubclassTest` / `FinalClassTest` 的 `override ` 是对那个 internal init 的 override。 + - **未标记的**全是显式 `public init`。初版观察到「唯一幸免的是 async 的 `AsyncInitializerActorTest`」——幸免的原因是它写了 `public init(identifier:) async`,**public 才是变量,async 是巧合**。 + - **独立探针**(2026-09-07 实测,`xcrun swiftc -O -wmo -enable-library-evolution -emit-library`):`public final class A {}` 的隐式 init、`public class C { var x = 0 }` 的三个访问器**只有 `Tq` 符号、没有函数符号**;`public final class B { public init() {} }` 的 `__allocating_init` 函数符号在 `0x8a0` 且带 dispatch thunk。`dyld_info -fixups` 数出 **5** 处 `_swift_deletedMethodError` bind = A 的 init 1 + C 的 init / getter / setter / modify 4,与预期精确吻合。 +- **与基线对比**:`next` 上这些槽输出 `Symbol not found`,用的是同一个 `implementation.isNull` 判定;本 PR 新增的是对该事实的解释,而该解释正确。**SwiftUICore 那 33% 因此可信**——OS 框架里多数 vtable 成员是 internal,整模块优化下 devirtualize + inline 之后独立函数体死掉。复核实测:SwiftUICore(iOS 18.5 arm64)有 341 处该 bind,Xcode 自带 SourceEditor.framework 有 11680 处(对应 4894 个 `Tq`)。 +- **仍然要改的**:措辞。「deleted method」在编译器语境里指「优化器删掉了实现体」,读者会读成「声明 / API 被删除」,而声明还在(源码在、`Tq` 在、descriptor 在)。建议措辞 `// Implementation removed by dead-method elimination; vtable slot kept for layout (calling it traps)`。 +- **文档必须补上的限定**:`swift_deletedMethodError` **只填进静态 metadata**。泛型类与 resilient 父类走运行时实例化路径时,`initClassVTable`(`stdlib/public/runtime/Metadata.cpp` 约 4157 行)把 `methodDescription.getImpl()` 原样拷入,null 保持 null,不会变成那个函数。 +- **值不值得修**:值得,但严重程度从「输出在说假话」降为「用词误导」。 +- **既往修复**:无。 +- **明确不要做的**:不要在产品代码里验证 metadata 的 bind 来佐证墓碑判定。descriptor 的 null 已是唯一写入路径的权威标记,bind 只是它在静态 metadata 里的后果,泛型类根本不存在——验证会产生假阴性,还要为每个 null 槽多做一次 classlist 查找。离线可行性本身没问题(`SwiftLayout.ObjCClassIndex` 已有 `MachOFile` 版 `__objc_classlist` 读取,`MachOKitExtensions.resolveBind(fileOffset:)` 能解 bind),但只值得放进 `GraphHostVTableAttributionTests`:用 `$s7SwiftUI9GraphHostCN` 定位 metadata、验三个 word 等于 `_swift_deletedMethodError`,让那条测试的文档注释名副其实(复核已核对:GraphHost 的 `0xAAA0A0 / A8 / B0` 三个 word 确实 bind 到 `libswiftCore/_swift_deletedMethodError`)。 +- **顺带核出的提案笔误**:提案表格里「`Tq` 符号给出的真值:getter / setter / modify」三行实际来自 descriptor flags——GraphHost 只有 7 个 `Tq`(从 `0x98ca14` 起),槽 20–22 并没有 `Tq`。 + +### 2. 歧义注释的判据用错了数,且在没有名字时照打 + +`Sources/SwiftDump/Dumper/ClassDumper.swift:250` + +触发条件是 `implementationSymbols.count > 1`——折叠地址上的**原始符号总数**,不是「有几个候选真的属于本类」。 + +- **能复现吗**:能,两种形态。(a) 某地址折叠 3 个符号、其中仅 1 个是本类成员时,归属其实毫无歧义,却仍打印「3 symbols folded」。(b) `validNode` 返回 nil(无任何折叠符号的声明上下文匹配本类)时输出退化为 `sub_XXXX` 地址,注释却仍称「下面这个名字是最佳候选」——下面根本没有名字,与 `ambiguousAttributionComment` 自身的文档注释直接矛盾。SwiftUICore 上该行会打印「2878 symbols folded」,度量的是折叠桶大小而非候选集。 +- **与基线对比**:新增代码,基线无。 +- **值不值得修**:值得,改动小。 +- **修法(复核给出)**:让 `validNode` 顺带返回「匹配本类且未被认领」的候选数,按它三分——**≥ 2** 才打歧义注释,措辞改成 `M of N symbols at this address are members of this type`(同时说明分子分母各是什么);**= 1** 归属其实唯一,不打;**= 0** 输出 `sub_` 地址,此时更不该声称「下面这个名字是最佳候选」。`implementationSymbols` 与计数一起移进 `attributedMethodNode == nil` 分支,第 6 条随之一并解决。 +- **注意(第二轮复核提出)**:本条要改的是歧义注释的**判据与措辞**,而代码此刻仍在输出字面的 `// Attribution: ambiguous — N symbols folded at this address`。`AGENTS.md` 与本条都保留这个字面文本作为「当前行为」的记录,修复批次落地时再一并替换为 `M of N …` 的新措辞——文档不得抢先描述尚未存在的输出。 +- **既往修复**:无。 + +### 3. 同一个「实现为 null」在一个函数里有三种渲染 + +`Sources/SwiftDump/Dumper/ClassDumper.swift:248` / `323` / `354` + +- method descriptor 循环:墓碑注释 + `` +- override 循环的 `.element` 分支:**无**墓碑注释,只有 `override `(见 `classesSnapshot.1.txt:87` 的 `SubclassTest`、`:111` 的 `FinalClassTest`) +- override / default-override 的 `else` 分支:仍是 method 循环刚淘汰的 `Error("Symbol not found")` + +- **能复现吗**:能,提交的基线里直接可见。 +- **与基线对比**:`next` 上三处一致(都是 `Symbol not found`),**不一致是本 PR 引入的**。 +- **值不值得修**:值得。除输出不一致外有具体隐患:`GraphHostVTableAttributionTests.deletedMethodSlotsAreMarkedAsTombstones` 断言 `!output.contains("Symbol not found")`,目前能过仅因 GraphHost 恰好没有 null 实现的 override 槽。 +- **修法(复核细化)**:一个 helper 算出「归属 + 注释」两件事,三个循环共用。null 实现的 override 槽建议渲染 `override ` 并附 `// overrides Parent.init()`——父类的 `Tq` 在 override 循环里本来就拿到了(`descriptor.methodDescriptor(in:)`),措辞是「覆盖了谁」而不是「是谁」,因此不会重蹈当初把 `override ResilientChild.init()` 印成 `override ResilientBase.init()` 的坑。两处残留的 `Error("Symbol not found")` 必须清掉,否则 `deletedMethodSlotsAreMarkedAsTombstones` 里的 `!contains("Symbol not found")` 只是碰巧绿。 +- **既往修复**:无。 + +### 4. 两条新注释没有开关 + +`Sources/SwiftDeclarationRendering/DeclarationRenderConfiguration.swift:200`–`225` + +`DeclarationRenderConfiguration` 里其他每种注释都有 `printXxx` 布尔开关(`printVTableOffset` / `printMemberAddress` / `printExportStatus` / `printFieldOffset`),新增两条一个都没有,无条件输出。RuntimeViewer、`swift-section dump` 与所有快照消费者被迫接收;在第 1 条的措辞问题解决前,宿主连「我不同意这个断言」都无法表达。 + +- **能复现吗**:属实。 +- **与基线对比**:新增。 +- **值不值得修**:**开关要加;transformer 模板槽不加**。[A14](../Documentations/Internal/ReviewAdjudications.md)(2026-08-23,`not exported` 注释)已裁决过「新注释不走 transformer 模板机制」——理由是模板机制的价值在**带变量 token 的注释**,零参数的固定陈述模板化只能改措辞,而措辞正是承重部分。该先例覆盖零 token 的 `deletedMethodSlotComment()`,**不覆盖**带一个变量的 `ambiguousAttributionComment(foldedSymbolCount:)`,也**不覆盖开关本身**——A14 明确写了「若需要开关,一个 Bool 就是全部所需表面」,而这两条连那个 Bool 都没有。详见 A37。 +- **既往修复**:A14 是同形先例,见上。 +- **默认值(2026-09-07 用户裁定)**:走折中——**歧义注释默认开**(猜出来的名字必须带 caveat,这是它存在的理由),**墓碑注释默认关**,跟随 CLI 的 `--emit-vtable-offsets` 一起打开(沿用 `Field offset: unknown ()` 挂 field-offset 家族、`protocol-extension default` 挂 member-address 家族的先例)。两者都不违反 A14。 + +## 二、建议同批修(3 条) + +### 5. `declarationContextNode` 的遍历既无差别下降、又没有去重(2026-09-07 补入正确性缺陷) + +`Sources/SwiftInspection/Extensions/Node+DeclarationContext.swift:47` + +手写广度优先遍历,`queue.append(contentsOf: candidate.children)`,既不筛选下降路径,也**没有 visited 集合**。 + +- **正确性问题(复核补入,比性能问题更重)**:遍历会穿过**任何**非 entity 包装节点,于是 `closure #1 in Foo.bar()`、`default argument 0 of Foo.bar()`、`variable initialization expression of Foo.x` 都会走到里层的 `.function` / `.variable` 并把 `Foo` 报成声明上下文——这些符号因此被当作 `Foo` 的成员候选接受。对照组是符号索引自己的口径:`SymbolIndexStore.processMemberSymbol` **只接受** `.static` 与访问器包装。本 PR 的初衷正是堵住「把不属于本类的符号当本类成员」,这里等于开了一个新口子。 +- **性能问题**:节点树是 hash-consed 的有向无环图(相同子树共享同一实例),无 visited 集合的遍历枚举的是路径数而非节点数。上游 swift-demangling 的 `DemanglingNode+Sequence.swift:245-251` 正是为此把 `first(of:)` 换成去重版,注释附实测:「on a shared DAG that one costs 2^N... Measured: 18.2s on a 22-level doubling DAG」,并指出**查不到东西的那次最贵**(无可短路)。这正是此处的常见情形:`validNode` 每个候选符号调一次,输入在 identical code folding(相同代码折叠)下是该地址上的全部符号——SwiftUICore 为 2878 个,其中多数是 metadata accessor、outlined function、witness table 这类根本没有 entity 节点的符号,每个都要走完整棵树才返回 nil。 +- **与基线对比**:新增代码。基线用的是上游已去重的 `first(of: .class)`,两个问题都属**新引入**。 +- **修法(一箭双雕)**:只沿白名单包装下降,并且**不进 type 子树**。闭包 / 默认参数 / 变量初始化表达式因此不再被误判,同时下降路径收敛成一条链,DAG 爆炸随之消失,**连 visited 集合都不需要**。初版建议的「改用上游 `first(of: 多个 kind)`」只解决性能、不解决误判,已废弃。 +- **白名单(2026-09-07 第二轮复核修正后,已对 swift-demangling 源码逐条核过)**: + - **可下降**:`global`(遍历全部子节点)、`static`、八个访问器 kind——`getter` / `setter` / `modifyAccessor` / `modify2Accessor` / `readAccessor` / `read2Accessor` / `unsafeAddressor` / `unsafeMutableAddressor`(`Node+Kind.swift`;**不是** `modify` / `read`,那两个 kind 名不存在)、`boundGenericFunction`(`[n, args]`,只降 `children[0]`)、`vTableThunk`(只降 `children[0]`)。 + - **必须含 `vTableThunk`**,否则 override 循环的回退会退化成 `override `:`vtable thunk for Base.f() dispatching to Sub.f()` 是 override 槽合法的实现符号(`ResilientClasses` 快照里就有)。`Demangler.swift:1744` 建的是 `children: [derived, base]`,而 `printVTableThunk` 把 `children[1]`(base)印在 "vtable thunk for" 之后、`children[0]`(derived)印在 "dispatching to" 之后——**要的是 derived**。现行 BFS 没出问题纯粹因为 `children[0]` 先入队。 + - **跳过(是叶子标记,不是包装)**:`mergedFunction` / `asyncFunctionPointer` / `coroFunctionPointer` / `objCAttribute`。它们由 `NodeFactory` 造成**无子节点**的单例(`Node(kind: .mergedFunction)`),作为 `global` 的兄弟子节点出现,遍历 `global` 的全部子节点就已覆盖,不该列进「可下降」。 + - **`methodDescriptor` 不列入**:两个调用方(`ClassDumper.validNode`、`OverrideSymbolMatcher.demangledOverrideSymbol`)的输入都是**实现地址**上的符号,而 `Tq` 是数据符号、不会出现在代码地址;`attributedMemberNode` 自己用 `first(of: .methodDescriptor)` 解包;A38 的上下文断言也落在解包后的 `global(entity)` 上。去掉它使这个 API 的契约收窄为「实现符号树」。 + - 其余一律不下降。 +- **横向排查**:全仓搜过,无第二处手写节点子树遍历,此为唯一一例。 +- **既往修复**:上游 0.5.x 已就同一 DAG 形状做过修复,本仓库这次是重新引入;误判那一半是本 PR 独有。 + +### 6. 快路径上白算一次 `implementationSymbols` + +`Sources/SwiftDump/Dumper/ClassDumper.swift:241` + +无条件调用 `descriptor.implementationSymbols(in: machO)`,但它只在 `Tq` 查不到时的回退分支用得上。`Symbols` 是实打实的 `[Symbol]` 数组(`Sources/MachOResolving/Symbols.swift:12`,每个 `Symbol` 为 32 字节 eager value)。按 PR 自身测量 71.7% 的槽有 `Tq` 符号,这些槽白建一次数组;在 PR 描述的折叠地址上是 2878 × 32 ≈ 92 KB 建了就扔。 + +- **与基线对比**:基线也调一次,但基线**需要**它;本 PR 使其变成可避免的开销。 +- **值不值得修**:值得。把调用挪进 `if resolvedMethodNode == nil` 分支即可,歧义计数一并挪入(与第 2 条的修法合并)。 + +### 7. 模型 / interface 路只抄了归属顺序,没抄诚实标注 + +`Sources/SwiftDeclaration/Components/Definitions/TypeDefinition.swift:275` + +采用了同样的「先 `Tq` 后实现地址」证据顺序,但回退命中折叠地址时**不发任何事件、不渲染任何标记**。那 3.7%「折叠且无 `Tq`」的槽位,在 interface 输出里照样带 vtable offset 注释与 `override` / `class` 关键字,而 dump 路径对同一槽会标注归属不确定。 + +- **能复现吗**:属实,diff 直接可见——`SwiftIndexEvents` 无新事件,`SwiftPrinting` 无新标记。 +- **值不值得修**:中等。该路径喂给 interface、diff、evolution 三个输出,影响面比 dump 大;但属于「诚实性没做全」而非「输出变错」,可作独立小批次。 + +## 三、低优先级 / 硬化(3 条) + +| # | 位置 | 结论 | 处置 | +|---|---|---|---| +| 8 | `TypeDefinition.swift:275`、`ClassDumper.swift:242` | `Tq` 分支跳过回退路径的两道闸(声明上下文匹配、`visitedNodes` 去重)。**基本是理论风险**:`attributedMemberNode` 只接受能 demangle 成 `.methodDescriptor` 的符号,而 descriptor 在一个镜像内地址唯一,同地址出现别类 `Tq` 的场景构造不出;dyld 共享缓存的偏移规范化理论上留了口子,未能构造实例 | 建议加一句与回退路径同样的上下文断言作便宜硬化,不急。见 A38。**复核补充**:「构造不出触发镜像」不等于「构造不出变红的测试」——`MethodDescriptorAttribution.memberNode(forMethodDescriptorSymbols:in:)` 收的是 `Symbols` 值,手造一个含别类 `Tq` 名字的 `Symbols` 即可在单元级变红,修复批次顺手加断言时测试是有的 | +| 9 | `Tests/SwiftDumpTests/VTableSlotAttributionTests.swift` | 新增 5 个测试全部驱动 `ClassDumper`;`TypeDefinition.index` / `OverrideSymbolMatcher` 那一半的全部证据是 `interfaceSnapshot.1.txt:3157` 改了一行。PR 说明自记:本改动的早期版本曾让 `override` 从 interface 输出中**整个消失**,那种失败模式现有测试抓不住 | 待补:至少钉住 `OverrideSymbolMatcher` 从 `first(of: .class)` 换成 `declarationContextNode` | +| 10 | `Sources/SwiftInspection/Extensions/Descriptor+MethodDescriptorSymbols.swift:61` | `MethodDescriptorAttribution` 是包一个静态函数的公开命名空间,其解包 `SymbolIndexStore.swift:695` 已做过一遍(结果形状不同:`global(entity)` vs 裸 `entity`),今后靠人手同步;`methodDescriptorSymbols(in:)` 是一行 `machO.symbols(offset:)`,按理应与既有四个 `implementationSymbols(in:)` 重载同文件 | 纯结构问题,不影响行为,随修复批次顺手整理 | + +## 四、误报或早有裁决(3 条,不动) + +| # | 位置 | 结论 | 状态 | +|---|---|---|---| +| 11 | `vTableEntryVariantsSnapshot.1.txt:142` 的 `class func static X.classMethod()` | **已裁决**:[ClassMemberKeywordRecovery.md:73-81](../Documentations/Internal/ClassMemberKeywordRecovery.md) 明确记过这个决定,原文即写着「现为 `class func static Foo...`」。本 PR 只是让该槽第一次正确解析到 `classMethod`(基线上错解析成 subscript setter),既有形态首次出现在此 fixture | 不动,见 A34 | +| 12 | `Node+DeclarationContext.swift:25` 的 `entityNodeKinds` 缺 `.boundGenericFunction` | **误报**:`Demangler.swift:1288` 构造它时是 `children: [n, args]`,第一个子节点是 `.function` / `.constructor` 节点本身而**非声明上下文**(`NodePrinter.swift:1954` 同样如此解包)。把它排除、让遍历**穿过**它落到里面的 `.function`,拿到的才是正确上下文 | 不动,见 A35 | +| 13 | `VTableSlotAttributionTests.swift:107` 的前提硬失败 | **误报(前半)+ 有意设计(后半)**:`swiftc` 不带 `-target` 默认产出宿主架构 thin 文件,走 `.machO` 分支,`.fat` 分支不会走到;「linker 不折叠即红」是测试自己写明的设计(文件头注释:a REQUIRED premise rather than a soft check) | 不动,见 A36 | + +## 五、流程(2 条) + +### 14. 提案状态三处不一致,且从未到过 `Accepted` + +- `Documentations/Evolutions/0020-vtable-slot-attribution-via-method-descriptor-symbols.md:3`:`In Progress` +- `Documentations/Evolutions/README.md:30`:`Draft` +- `Documentations/README.md:106`:`Draft` + +三个来源两种答案,无一为 `Accepted`,而实现代码已在同一 commit 落地。文件名仍带 `draft-` 前缀(约定是落地时才分配 `NNNN-` 编号)。**待办:修复批次落地前把状态改为 `Accepted` 并分配编号,三处对齐。** + +### 15. `vtableAccessorFieldNames` 的折叠地址扫描 —— 基线既有,非本 PR 引入 + +`Sources/SwiftDump/Dumper/ClassDumper.swift:561-571` 仍按实现地址收集访问器名字,内层循环无本类过滤、无早退,把每个 `.variable` 的名字都塞进集合。折叠地址上是每个访问器 descriptor ~2878 次 demangle 查询(有 memo 缓存兜底),且会把嵌套类型的同名字段一并收进,从而抑制本类同名字段的 `final` 标记——正是本 PR 在别处修掉的那种跨类型串味。 + +- **与基线对比**:`next` 上一模一样,本 PR 未触及。 +- **既往修复**:来自提案 0006(commit `da9b8be2` / `83a4308c`)。PR 记为后续项,理由是错误方向保守(少标 `final` 而非错标)。 +- **处置**:同意作独立批次,见 A39。 diff --git a/Sources/MachOBase/Exported.swift b/Sources/MachOBase/Exported.swift new file mode 100644 index 00000000..99637261 --- /dev/null +++ b/Sources/MachOBase/Exported.swift @@ -0,0 +1,10 @@ +// The reader / resolver / pointer layer as one import — everything the ABI +// model (`MachOSwiftSection`) is allowed to depend on, and nothing above it. +// `MachOFoundation` re-exports this plus the symbol index and dependency +// resolution; the ABI layer deliberately stops here (evolution proposal +// `self-contained-abi-layer`). +@_exported import MachOKitExtensions +@_exported import MachOReading +@_exported import MachOResolving +@_exported import MachOPointers +@_exported import Utilities diff --git a/Sources/MachODependencies/DependencyClosure.swift b/Sources/MachODependencies/DependencyClosure.swift new file mode 100644 index 00000000..4e202592 --- /dev/null +++ b/Sources/MachODependencies/DependencyClosure.swift @@ -0,0 +1,107 @@ +import MachOKit +import MachOKitExtensions + +/// How far a `DependencyClosure` follows the load commands. +public enum DependencyTraversal: Sendable, Hashable { + /// Only the root's own `LC_LOAD_DYLIB`-family entries. + case direct + /// The root's dependencies, their dependencies, and so on — breadth-first. + case transitive +} + +/// The dependency images of a root binary, resolved through a +/// `DependencyLocating` strategy. +/// +/// `images` is in **resolution order**: the load-command order for a direct +/// traversal, breadth-first for a transitive one (the root's direct +/// dependencies first, then theirs, …). The order is part of the contract, not +/// an implementation detail — a consumer that indexes dependencies lazily +/// (`SwiftLayout.ImageUniverse`) folds them in this order and stops at the +/// first hit, and depth-first would put Foundation's whole subtree ahead of the +/// root's own second Swift dependency. +/// +/// Images are deduplicated twice: by bare image name (`DependencyLoadName`), +/// never by load-name spelling, and by image identity +/// (`MachORepresentableWithCache.identifier`, `LC_UUID`-keyed for a file) — +/// the file locator registers an explicit file under its on-disk path, its +/// install name and its bare name, so a root that links the same binary under +/// two load names reaches one image twice, and it is collected once. The root +/// itself is excluded. A dependency the locator cannot find is not an error: +/// its load name is recorded in `unresolvedLoadNames` and traversal +/// continues, so the result degrades per dependency rather than failing +/// whole. +public struct DependencyClosure: Sendable { + public let root: MachO + public let traversal: DependencyTraversal + /// The resolved dependency images in resolution order (see the type + /// documentation), root excluded. + public let images: [MachO] + /// Load names no locator could resolve, in encounter order, deduplicated by + /// bare image name. + public let unresolvedLoadNames: [String] + /// Search paths the file locator could not open (always empty for a locator + /// that has no search paths, such as the in-process one). + public let searchPathLoadFailures: [DependencySearchPathLoadFailure] + + /// Resolves the closure of `root` through `locator`. The general entry + /// point; the reader-specific initializers below pick the locator. + public init(root: MachO, traversal: DependencyTraversal = .transitive, locator: some DependencyLocating) { + self.init(root: root, traversal: traversal, locator: locator, searchPathLoadFailures: []) + } + + init(root: MachO, traversal: DependencyTraversal, locator: some DependencyLocating, searchPathLoadFailures: [DependencySearchPathLoadFailure]) { + var visitedBareImageNames: Set = [DependencyLoadName.bareImageName(of: root.imagePath)] + var collectedImageIdentifiers: Set = [root.identifier] + var images: [MachO] = [] + var unresolvedLoadNames: [String] = [] + var frontier: [MachO] = [root] + + while !frontier.isEmpty { + var nextFrontier: [MachO] = [] + for image in frontier { + for loadName in image.dependencies.map(\.dylib.name) { + let bareImageName = DependencyLoadName.bareImageName(of: loadName) + guard !bareImageName.isEmpty, visitedBareImageNames.insert(bareImageName).inserted else { continue } + guard let dependencyImage = locator.locate(loadName: loadName) else { + unresolvedLoadNames.append(loadName) + continue + } + // Resolved, but to an image already collected under another + // spelling: nothing to add, and nothing to report. + guard collectedImageIdentifiers.insert(dependencyImage.identifier).inserted else { continue } + images.append(dependencyImage) + nextFrontier.append(dependencyImage) + } + } + frontier = traversal == .transitive ? nextFrontier : [] + } + + self.root = root + self.traversal = traversal + self.images = images + self.unresolvedLoadNames = unresolvedLoadNames + self.searchPathLoadFailures = searchPathLoadFailures + } +} + +// MARK: - In-process (MachOImage) + +extension DependencyClosure where MachO == MachOImage { + /// Resolves an in-process image's dependencies through the active dyld + /// (`InProcessDependencyLocator`). + public init(root: MachOImage, traversal: DependencyTraversal = .transitive) { + self.init(root: root, traversal: traversal, locator: InProcessDependencyLocator()) + } +} + +// MARK: - Offline (MachOFile) + +extension DependencyClosure where MachO == MachOFile { + /// Resolves a file-backed image's dependencies through `searchPaths` + /// (`FileDependencyLocator`). Fat explicit files contribute the slice + /// matching the root's architecture. + public init(root: MachOFile, searchPaths: [DependencySearchPath] = [.systemDyldSharedCache], traversal: DependencyTraversal = .transitive) { + let locator = FileDependencyLocator(searchPaths: searchPaths, preferredCPU: root.header.cpu) + self.init(root: root, traversal: traversal, locator: locator, searchPathLoadFailures: locator.loadFailures) + } +} diff --git a/Sources/MachODependencies/DependencyLoadName.swift b/Sources/MachODependencies/DependencyLoadName.swift new file mode 100644 index 00000000..0424b34a --- /dev/null +++ b/Sources/MachODependencies/DependencyLoadName.swift @@ -0,0 +1,20 @@ +/// Helpers over the dylib load names recorded in `LC_LOAD_DYLIB`-family load +/// commands (`@rpath/Foo.framework/Versions/A/Foo`, +/// `/usr/lib/swift/libswiftCore.dylib`, …). +public enum DependencyLoadName { + /// The bare image name a load name identifies: the last path component with + /// its first extension component stripped (`Foo`, `libswiftCore`). + /// + /// This is deliberately the same rule `MachOImage(name:)` applies to the + /// images dyld has mapped, so a load name normalized here can be handed + /// straight to the in-process lookup — passing the un-normalized load name + /// never matches, because dyld reports absolute paths and the lookup + /// compares bare names. It is also the key every dependency set is + /// deduplicated on: the same library is linked under different spellings + /// by different images (`@rpath/…` by a sibling, an absolute path by a + /// system framework), and only the bare name is stable across them. + public static func bareImageName(of loadName: String) -> String { + let lastPathComponent = loadName.components(separatedBy: "/").last ?? loadName + return lastPathComponent.components(separatedBy: ".").first ?? lastPathComponent + } +} diff --git a/Sources/MachODependencies/DependencyLocating.swift b/Sources/MachODependencies/DependencyLocating.swift new file mode 100644 index 00000000..5ff96127 --- /dev/null +++ b/Sources/MachODependencies/DependencyLocating.swift @@ -0,0 +1,28 @@ +import MachOKit +import MachOKitExtensions + +/// Turns a dependency load name into a concrete image of the root's reader +/// type — the one seam `DependencyClosure` needs, so the traversal is +/// independent of *where* images come from (the active dyld, a set of search +/// paths, or a test's hand-built table). +public protocol DependencyLocating { + associatedtype MachO: MachORepresentableWithCache + + /// The image `loadName` refers to, or `nil` when this locator cannot find + /// it. `loadName` is the raw load-command spelling; implementations + /// normalize it themselves (see `DependencyLoadName.bareImageName(of:)`). + func locate(loadName: String) -> MachO? +} + +/// Resolves dependencies through the active dyld: system frameworks resolve +/// from the shared cache automatically, and locally loaded frameworks resolve +/// as long as they are already mapped into this process. +public struct InProcessDependencyLocator: DependencyLocating, Sendable { + public init() {} + + public func locate(loadName: String) -> MachOImage? { + let bareImageName = DependencyLoadName.bareImageName(of: loadName) + guard !bareImageName.isEmpty else { return nil } + return MachOImage(name: bareImageName) + } +} diff --git a/Sources/MachODependencies/DependencySearchPath.swift b/Sources/MachODependencies/DependencySearchPath.swift new file mode 100644 index 00000000..9f9de5a5 --- /dev/null +++ b/Sources/MachODependencies/DependencySearchPath.swift @@ -0,0 +1,50 @@ +import Foundation + +/// Where an offline (`MachOFile`) dependency locator may look for a dependency +/// binary. Cache-resident system frameworks (the stdlib, Foundation, the rest +/// of the OS) resolve through a dyld shared cache; anything else — a sibling +/// framework reached through `@rpath`, a test helper next to the root binary — +/// has to be handed over as an explicit file, because `@rpath` / +/// `@loader_path` / `@executable_path` are not expanded. +public enum DependencySearchPath: Sendable, Hashable, CustomStringConvertible { + /// An explicit on-disk path to a Mach-O (or fat) binary file. + case machOFile(path: String) + /// An explicit path to a dyld shared cache file. + case dyldSharedCache(path: String) + /// The running system's active dyld shared cache. + case systemDyldSharedCache + + public var description: String { + switch self { + case .machOFile(let path): + return "machOFile(\(path))" + case .dyldSharedCache(let path): + return "dyldSharedCache(\(path))" + case .systemDyldSharedCache: + return "systemDyldSharedCache" + } + } +} + +/// Why a search path contributed nothing. Recorded on the closure rather than +/// thrown: one unusable search path must not fail the whole resolution, and +/// this module sits below the event layer, so the host decides where the +/// degradation is reported. +public enum DependencySearchPathError: Error, Sendable, Equatable { + /// The file loaded but yielded no Mach-O slice. + case noMachOSlice(path: String) + /// `FullDyldCache.host` returned `nil` — the platform exposes no shared + /// cache file to this process. + case systemDyldSharedCacheUnavailable +} + +/// A search path that could not be opened, paired with the reason. +public struct DependencySearchPathLoadFailure: Sendable { + public let searchPath: DependencySearchPath + public let error: any Error + + public init(searchPath: DependencySearchPath, error: any Error) { + self.searchPath = searchPath + self.error = error + } +} diff --git a/Sources/MachODependencies/FileDependencyLocator.swift b/Sources/MachODependencies/FileDependencyLocator.swift new file mode 100644 index 00000000..cd48d200 --- /dev/null +++ b/Sources/MachODependencies/FileDependencyLocator.swift @@ -0,0 +1,169 @@ +import Foundation +import MachOKit +import MachOKitExtensions + +/// Locates dependency `MachOFile`s across a set of search paths, for a root +/// binary read from disk (no running process to ask). +/// +/// Two lookups, in order: +/// +/// 1. **Exact install path.** A system framework's load name is the absolute +/// path the cache image also reports as its `imagePath`, so this is the +/// compiler's own answer whenever it applies. +/// 2. **Bare name, ranked.** Load names that are not absolute (`@rpath/…`) or +/// that spell a path the cache does not use fall back to the bare image +/// name. Leaf names are not unique inside a shared cache — a macOS cache +/// carries the Mac Catalyst build of SwiftUI under `/System/iOSSupport` +/// next to the native one, and iOS caches ship an `.axbundle` wearing the +/// framework's name — so candidates are ranked with +/// `DyldCacheImageSearchMode.matchRank(forImagePath:)` (canonical framework +/// binary, then plain dylib, then bundle; support-root builds demoted) +/// instead of taking whichever image the cache enumerates first. +/// +/// Explicit files are indexed eagerly at construction. Each dyld shared cache +/// is indexed **once**, lazily, on the first lookup that reaches the caches — +/// one pass over `machOFiles()` rather than a fresh per-lookup scan, which +/// would cost `O(dependencies × cache size)` (measured at 21 s over a +/// 551-image closure before this was made one-shot). +public final class FileDependencyLocator: DependencyLocating, @unchecked Sendable { + /// Search paths that could not be opened. Never thrown: one bad path must + /// not fail the whole resolution. + public let loadFailures: [DependencySearchPathLoadFailure] + + private let explicitFilesByInstallPath: [String: MachOFile] + private let explicitFilesByBareName: [String: MachOFile] + private let caches: [FullDyldCache] + private let cacheIndexLock = NSLock() + private var cacheIndex: CacheImageIndex? + + /// - Parameters: + /// - searchPaths: Consulted in order; the first explicit file registered + /// under a name wins, and the first cache image with the best rank wins. + /// - preferredCPU: For a fat explicit file, the slice to pick — the root + /// binary's own architecture, so a universal dependency is laid out + /// for the same target as the root. Matched on CPU type plus subtype + /// (so arm64 and arm64e are told apart), then on type alone, then the + /// first slice. + public init(searchPaths: [DependencySearchPath], preferredCPU: CPU? = nil) { + var explicitFilesByInstallPath: [String: MachOFile] = [:] + var explicitFilesByBareName: [String: MachOFile] = [:] + var caches: [FullDyldCache] = [] + var loadFailures: [DependencySearchPathLoadFailure] = [] + + for searchPath in searchPaths { + switch searchPath { + case .machOFile(let path): + do { + let slices = try File.loadFromFile(url: URL(fileURLWithPath: path)).machOFiles + guard let machOFile = Self.preferredSlice(among: slices, preferredCPU: preferredCPU) else { + throw DependencySearchPathError.noMachOSlice(path: path) + } + // A file's `imagePath` is its install name (`LC_ID_DYLIB`, + // typically `@rpath/…`), not its on-disk path, so both + // spellings are registered for the exact lookup, and the + // supplied path's bare name for the fallback. + for installPath in [path, machOFile.imagePath] where explicitFilesByInstallPath[installPath] == nil { + explicitFilesByInstallPath[installPath] = machOFile + } + let bareImageName = DependencyLoadName.bareImageName(of: path) + if !bareImageName.isEmpty, explicitFilesByBareName[bareImageName] == nil { + explicitFilesByBareName[bareImageName] = machOFile + } + } catch { + loadFailures.append(.init(searchPath: searchPath, error: error)) + } + case .dyldSharedCache(let path): + do { + caches.append(try FullDyldCache(url: URL(fileURLWithPath: path))) + } catch { + loadFailures.append(.init(searchPath: searchPath, error: error)) + } + case .systemDyldSharedCache: + if let hostCache = FullDyldCache.host { + caches.append(hostCache) + } else { + loadFailures.append(.init(searchPath: searchPath, error: DependencySearchPathError.systemDyldSharedCacheUnavailable)) + } + } + } + + self.explicitFilesByInstallPath = explicitFilesByInstallPath + self.explicitFilesByBareName = explicitFilesByBareName + self.caches = caches + self.loadFailures = loadFailures + } + + public func locate(loadName: String) -> MachOFile? { + if let explicitFile = explicitFilesByInstallPath[loadName] { + return explicitFile + } + let bareImageName = DependencyLoadName.bareImageName(of: loadName) + guard !bareImageName.isEmpty else { return nil } + if let explicitFile = explicitFilesByBareName[bareImageName] { + return explicitFile + } + guard !caches.isEmpty else { return nil } + let index = builtCacheIndex() + if let cacheImage = index.imagesByInstallPath[loadName] { + return cacheImage + } + return index.bestImagesByBareName[bareImageName]?.machOFile + } + + /// Compared on `CPU.type` and `CPU.subtype` rather than `CPU ==`: the + /// struct's synthesized equality includes the raw subtype's capability + /// bits (the arm64e pointer-authentication ABI version and versioned-ABI + /// flag under `CPU_SUBTYPE_MASK`), so a versioned-ABI arm64e slice would + /// compare unequal to a plain arm64e root and silently fall through to + /// the type-only match — the arm64 / arm64e confusion this exists to + /// avoid. `subtype` masks those bits. + private static func preferredSlice(among slices: [MachOFile], preferredCPU: CPU?) -> MachOFile? { + guard let preferredCPU else { return slices.first } + if let exactSlice = slices.first(where: { $0.header.cpu.type == preferredCPU.type && $0.header.cpu.subtype == preferredCPU.subtype }) { + return exactSlice + } + if let sameTypeSlice = slices.first(where: { $0.header.cpu.type == preferredCPU.type }) { + return sameTypeSlice + } + return slices.first + } + + // MARK: - One-shot cache index + + private struct CacheImageIndex { + var imagesByInstallPath: [String: MachOFile] = [:] + var bestImagesByBareName: [String: (rank: Int, machOFile: MachOFile)] = [:] + } + + /// Rank given to a cache image whose path the `DyldCacheImageSearchMode` + /// name rule does not recognize under its bare name (a multi-dotted leaf + /// such as `libc++.1.dylib`, whose `deletingPathExtension` form is not the + /// bare name). It still resolves — every entry under a key legitimately + /// carries that bare name — it just loses to any recognized shape. + private static let unrankedShapeRank = Int.max + + private func builtCacheIndex() -> CacheImageIndex { + cacheIndexLock.lock() + defer { cacheIndexLock.unlock() } + if let cacheIndex { return cacheIndex } + + var index = CacheImageIndex() + for cache in caches { + for machOFile in cache.machOFiles() { + let installPath = machOFile.imagePath + if index.imagesByInstallPath[installPath] == nil { + index.imagesByInstallPath[installPath] = machOFile + } + let bareImageName = DependencyLoadName.bareImageName(of: installPath) + guard !bareImageName.isEmpty else { continue } + let rank = DyldCacheImageSearchMode.name(bareImageName).matchRank(forImagePath: installPath) ?? Self.unrankedShapeRank + if let existing = index.bestImagesByBareName[bareImageName], existing.rank <= rank { + continue + } + index.bestImagesByBareName[bareImageName] = (rank, machOFile) + } + } + cacheIndex = index + return index + } +} diff --git a/Sources/MachOFixtureSupport/Baseline/BaselineEmitter.swift b/Sources/MachOFixtureSupport/Baseline/BaselineEmitter.swift index eb6bf474..bf6512b9 100644 --- a/Sources/MachOFixtureSupport/Baseline/BaselineEmitter.swift +++ b/Sources/MachOFixtureSupport/Baseline/BaselineEmitter.swift @@ -17,6 +17,13 @@ package enum BaselineEmitter { return "0x\(String(unsigned, radix: 16))" } + /// Emit `0x` for a present value and the literal `nil` + /// otherwise — for `Int?` baseline fields such as an implementation + /// offset that may be a null pointer. + package static func optionalHex(_ value: T?) -> String { + value.map(hex) ?? "nil" + } + /// Emit `[0x..., 0x..., ...]` for an array of binary integers. package static func hexArray(_ values: [T]) -> String { "[\(values.map(hex).joined(separator: ", "))]" diff --git a/Sources/MachOFixtureSupport/Baseline/BaselineFixturePicker.swift b/Sources/MachOFixtureSupport/Baseline/BaselineFixturePicker.swift index 027b94f6..d485ba60 100644 --- a/Sources/MachOFixtureSupport/Baseline/BaselineFixturePicker.swift +++ b/Sources/MachOFixtureSupport/Baseline/BaselineFixturePicker.swift @@ -354,6 +354,21 @@ package enum BaselineFixturePicker { ) } + /// Picks `DefaultImplementationVariants.BasicDefaultProtocol` from the + /// `SymbolTestsCore` fixture: `required()` has no default implementation, + /// `withDefault()` and `withDefaultAndGeneric(_:)` have one — the shape + /// that exercises both states of `ProtocolRequirement`'s + /// `defaultImplementation` pointer. + package static func protocol_BasicDefaultProtocol( + in machO: some MachOSwiftSectionRepresentableWithCache + ) throws -> ProtocolDescriptor { + try required( + try machO.swift.protocolDescriptors.first(where: { descriptor in + try descriptor.name(in: machO) == "BasicDefaultProtocol" + }) + ) + } + /// Picks `Protocols.BaseProtocolTest` from the `SymbolTestsCore` /// fixture. Used as the base side of the inheritance fixture pair /// (`BaseProtocolTest` / `DerivedProtocolTest`). Has a single diff --git a/Sources/MachOFixtureSupport/Baseline/Generators/Class/MethodDefaultOverrideDescriptorBaselineGenerator.swift b/Sources/MachOFixtureSupport/Baseline/Generators/Class/MethodDefaultOverrideDescriptorBaselineGenerator.swift index 4db8344d..c8626d87 100644 --- a/Sources/MachOFixtureSupport/Baseline/Generators/Class/MethodDefaultOverrideDescriptorBaselineGenerator.swift +++ b/Sources/MachOFixtureSupport/Baseline/Generators/Class/MethodDefaultOverrideDescriptorBaselineGenerator.swift @@ -17,7 +17,8 @@ package enum MethodDefaultOverrideDescriptorBaselineGenerator { // Public members declared directly in MethodDefaultOverrideDescriptor.swift. // Overload pairs collapse to single MethodKey entries via the scanner. let registered = [ - "implementationSymbols", + "implementationAddress", + "implementationOffset", "layout", "offset", "originalMethodDescriptor", diff --git a/Sources/MachOFixtureSupport/Baseline/Generators/Class/MethodDescriptorBaselineGenerator.swift b/Sources/MachOFixtureSupport/Baseline/Generators/Class/MethodDescriptorBaselineGenerator.swift index 3a9821af..6290a735 100644 --- a/Sources/MachOFixtureSupport/Baseline/Generators/Class/MethodDescriptorBaselineGenerator.swift +++ b/Sources/MachOFixtureSupport/Baseline/Generators/Class/MethodDescriptorBaselineGenerator.swift @@ -9,8 +9,8 @@ import MachOFoundation /// `MethodDescriptor` is the row type for a class's vtable. We pick the /// first vtable entry from the `Classes.ClassTest` picker — which has a /// non-empty vtable — and record the `flags.rawValue` plus the descriptor -/// offset. Live `Symbols?` payloads aren't embedded as literals; the Suite -/// uses cross-reader equality at runtime to assert agreement. +/// offset, plus the implementation offset the descriptor's relative pointer +/// resolves to (a pure-arithmetic value, identical across readers). package enum MethodDescriptorBaselineGenerator { package static func generate( in machO: some MachOSwiftSectionRepresentableWithCache, @@ -24,10 +24,12 @@ package enum MethodDescriptorBaselineGenerator { let methodCount = classWrapper.methodDescriptors.count // Public members declared directly in MethodDescriptor.swift. - // The two `implementationSymbols(in:)` overloads collapse to a - // single MethodKey under PublicMemberScanner's name-only key. + // Symbol attribution (`implementationSymbols(in:)`) moved to + // SwiftInspection (evolution proposal `self-contained-abi-layer`); + // the ABI layer exposes the implementation's offset and address. let registered = [ - "implementationSymbols", + "implementationAddress", + "implementationOffset", "layout", "offset", ] @@ -37,10 +39,9 @@ package enum MethodDescriptorBaselineGenerator { // Regenerate via: Scripts/regen-baselines.sh // Source fixture: SymbolTestsCore.framework // - // Method descriptors carry a `Symbols?` implementation pointer; live - // payloads aren't embedded as literals. The companion Suite - // (MethodDescriptorTests) verifies cross-reader agreement at - // runtime. + // The implementation offset is pure relative-pointer arithmetic, so + // it is pinned as a literal; the companion Suite + // (MethodDescriptorTests) also verifies cross-reader agreement. """ let file: SourceFileSyntax = """ @@ -52,6 +53,7 @@ package enum MethodDescriptorBaselineGenerator { struct Entry { let offset: Int let layoutFlagsRawValue: UInt32 + let implementationOffset: Int? } static let firstClassTestMethod = \(raw: entryExpr) @@ -68,11 +70,13 @@ package enum MethodDescriptorBaselineGenerator { private static func emitEntryExpr(for method: MethodDescriptor) -> String { let offset = method.offset let flagsRaw = method.layout.flags.rawValue + let implementationOffset = method.implementationOffset let expr: ExprSyntax = """ Entry( offset: \(raw: BaselineEmitter.hex(offset)), - layoutFlagsRawValue: \(raw: BaselineEmitter.hex(flagsRaw)) + layoutFlagsRawValue: \(raw: BaselineEmitter.hex(flagsRaw)), + implementationOffset: \(raw: BaselineEmitter.optionalHex(implementationOffset)) ) """ return expr.description diff --git a/Sources/MachOFixtureSupport/Baseline/Generators/Class/MethodOverrideDescriptorBaselineGenerator.swift b/Sources/MachOFixtureSupport/Baseline/Generators/Class/MethodOverrideDescriptorBaselineGenerator.swift index 45432a8a..a8a84fa2 100644 --- a/Sources/MachOFixtureSupport/Baseline/Generators/Class/MethodOverrideDescriptorBaselineGenerator.swift +++ b/Sources/MachOFixtureSupport/Baseline/Generators/Class/MethodOverrideDescriptorBaselineGenerator.swift @@ -9,8 +9,10 @@ import MachOFoundation /// `MethodOverrideDescriptor` is the row type for a class's override table. /// We pick the first override entry from `Classes.SubclassTest` (which /// overrides several methods inherited from `ClassTest`) and record the -/// descriptor offset. Resolved class/method/symbols pointers aren't -/// embedded as literals; the Suite uses cross-reader equality at runtime. +/// descriptor offset and the implementation offset — pure relative-pointer +/// arithmetic, so both pin as literals. The resolved class / method +/// descriptors are not embedded; the Suite checks their presence across +/// readers at runtime. package enum MethodOverrideDescriptorBaselineGenerator { package static func generate( in machO: some MachOSwiftSectionRepresentableWithCache, @@ -28,7 +30,8 @@ package enum MethodOverrideDescriptorBaselineGenerator { // collapse to a single MethodKey under PublicMemberScanner. let registered = [ "classDescriptor", - "implementationSymbols", + "implementationAddress", + "implementationOffset", "layout", "methodDescriptor", "offset", @@ -39,9 +42,10 @@ package enum MethodOverrideDescriptorBaselineGenerator { // Regenerate via: Scripts/regen-baselines.sh // Source fixture: SymbolTestsCore.framework // - // MethodOverrideDescriptor carries three relative pointers (class / - // method / implementation Symbols). Live payloads aren't embedded; - // the Suite verifies cross-reader agreement at runtime. + // The implementation offset is pure relative-pointer arithmetic, so + // it is pinned as a literal (like MethodDescriptor's); the class / + // method descriptor pointers resolve to live wrappers and are + // checked for presence across readers at runtime instead. """ let file: SourceFileSyntax = """ @@ -52,6 +56,7 @@ package enum MethodOverrideDescriptorBaselineGenerator { struct Entry { let offset: Int + let implementationOffset: Int? } static let firstSubclassOverride = \(raw: entryExpr) @@ -67,10 +72,12 @@ package enum MethodOverrideDescriptorBaselineGenerator { private static func emitEntryExpr(for override: MethodOverrideDescriptor) -> String { let offset = override.offset + let implementationOffset = override.implementationOffset let expr: ExprSyntax = """ Entry( - offset: \(raw: BaselineEmitter.hex(offset)) + offset: \(raw: BaselineEmitter.hex(offset)), + implementationOffset: \(raw: BaselineEmitter.optionalHex(implementationOffset)) ) """ return expr.description diff --git a/Sources/MachOFixtureSupport/Baseline/Generators/Protocol/ProtocolRequirementBaselineGenerator.swift b/Sources/MachOFixtureSupport/Baseline/Generators/Protocol/ProtocolRequirementBaselineGenerator.swift index a916a514..e5410503 100644 --- a/Sources/MachOFixtureSupport/Baseline/Generators/Protocol/ProtocolRequirementBaselineGenerator.swift +++ b/Sources/MachOFixtureSupport/Baseline/Generators/Protocol/ProtocolRequirementBaselineGenerator.swift @@ -15,7 +15,13 @@ import MachOFoundation /// /// Picker: `Protocols.ProtocolWitnessTableTest` — its 5 method /// requirements (`a`/`b`/`c`/`d`/`e`) flesh out the trailing array; we -/// pick the first requirement and exercise its accessors. +/// pick the first requirement and exercise its accessors. None of them has +/// a default implementation, so a second entry comes from +/// `DefaultImplementationVariants.BasicDefaultProtocol`: the first +/// requirement whose `defaultImplementation` pointer is non-null (a +/// protocol-extension default), so the non-`nil` arithmetic of +/// `defaultImplementationOffset` is pinned as a literal too — not just the +/// `nil` case. /// /// The companion `ProtocolBaseRequirement` type (declared in the same /// `ProtocolRequirement.swift` file) gets its own baseline / Suite @@ -31,11 +37,21 @@ package enum ProtocolRequirementBaselineGenerator { let firstRequirementExpr = try emitRequirementEntryExpr(for: firstRequirement, in: machO) + let defaultedDescriptor = try BaselineFixturePicker.protocol_BasicDefaultProtocol(in: machO) + let defaultedProtocol = try `Protocol`(descriptor: defaultedDescriptor, in: machO) + let firstDefaultedRequirement = try required(defaultedProtocol.requirements.first { $0.layout.defaultImplementation.isValid }) + let firstDefaultedRequirementExpr = try emitRequirementEntryExpr(for: firstDefaultedRequirement, in: machO) + // Public members declared on `ProtocolRequirement` (the first struct // in ProtocolRequirement.swift). `init(layout:offset:)` is filtered // as memberwise-synthesized. + // Symbol attribution (`defaultImplementationSymbols(in:)`) lives in + // SwiftInspection since evolution proposal `self-contained-abi-layer`; + // the ABI layer exposes the default implementation's offset and + // context address. let registered = [ - "defaultImplementationSymbols", + "defaultImplementationAddress", + "defaultImplementationOffset", "layout", "offset", ] @@ -55,10 +71,12 @@ package enum ProtocolRequirementBaselineGenerator { struct Entry { let offset: Int let layoutFlagsRawValue: UInt32 - let hasDefaultImplementation: Bool + let defaultImplementationOffset: Int? } static let firstRequirement = \(raw: firstRequirementExpr) + + static let firstDefaultedRequirement = \(raw: firstDefaultedRequirementExpr) } """ @@ -73,13 +91,13 @@ package enum ProtocolRequirementBaselineGenerator { ) throws -> String { let offset = requirement.offset let layoutFlagsRawValue = requirement.layout.flags.rawValue - let hasDefaultImplementation = (try requirement.defaultImplementationSymbols(in: machO)) != nil + let defaultImplementationOffset = requirement.defaultImplementationOffset let expr: ExprSyntax = """ Entry( offset: \(raw: BaselineEmitter.hex(offset)), layoutFlagsRawValue: \(raw: BaselineEmitter.hex(layoutFlagsRawValue)), - hasDefaultImplementation: \(literal: hasDefaultImplementation) + defaultImplementationOffset: \(raw: BaselineEmitter.optionalHex(defaultImplementationOffset)) ) """ return expr.description diff --git a/Sources/MachOFixtureSupport/Baseline/Generators/Protocol/ResilientWitnessBaselineGenerator.swift b/Sources/MachOFixtureSupport/Baseline/Generators/Protocol/ResilientWitnessBaselineGenerator.swift index 1d28e6e1..0d857f7c 100644 --- a/Sources/MachOFixtureSupport/Baseline/Generators/Protocol/ResilientWitnessBaselineGenerator.swift +++ b/Sources/MachOFixtureSupport/Baseline/Generators/Protocol/ResilientWitnessBaselineGenerator.swift @@ -13,8 +13,8 @@ import MachOFoundation /// /// Picker: the first `ProtocolConformance` from the fixture with a /// non-empty `resilientWitnesses` array. We pin the resolved offset of -/// the first witness's `requirement(in:)` and the boolean presence of -/// `implementationSymbols(in:)`. `implementationAddress(in:)` is a +/// the first witness's `requirement(in:)` and its `implementationOffset` +/// (pure relative-pointer arithmetic). `implementationAddress(in:)` is a /// MachO-only debug formatter (see `ResilientWitness.swift` doc-comment) /// — we register the name but do not assert on the live address string /// (it's a base-16 representation of an in-memory pointer). @@ -28,26 +28,24 @@ package enum ResilientWitnessBaselineGenerator { let requirement = try firstWitness.requirement(in: machO) let hasRequirement = requirement != nil - let hasImplementationSymbols = (try firstWitness.implementationSymbols(in: machO)) != nil let implementationOffset = firstWitness.implementationOffset let entryExpr = emitEntryExpr( offset: firstWitness.offset, hasRequirement: hasRequirement, - hasImplementationSymbols: hasImplementationSymbols, implementationOffset: implementationOffset ) // Public members declared directly in ResilientWitness.swift. - // The `requirement(in:)` and `implementationSymbols(in:)` overloads - // (MachO + InProcess + ReadingContext) collapse to single MethodKeys - // under the scanner's name-based deduplication. - // `implementationAddress(in:)` is a MachO-only debug formatter — - // tracked here, exercised for type-correctness in the Suite. + // The `requirement(in:)` overloads (MachO + InProcess + + // ReadingContext) collapse to a single MethodKey under the scanner's + // name-based deduplication, as do the two `implementationAddress(in:)` + // forms (the MachO-only debug formatter and the ReadingContext + // address). Symbol attribution (`implementationSymbols(in:)`) lives in + // SwiftInspection since evolution proposal `self-contained-abi-layer`. let registered = [ "implementationAddress", "implementationOffset", - "implementationSymbols", "layout", "offset", "requirement", @@ -68,8 +66,7 @@ package enum ResilientWitnessBaselineGenerator { struct Entry { let offset: Int let hasRequirement: Bool - let hasImplementationSymbols: Bool - let implementationOffset: Int + let implementationOffset: Int? } static let firstWitness = \(raw: entryExpr) @@ -84,15 +81,13 @@ package enum ResilientWitnessBaselineGenerator { private static func emitEntryExpr( offset: Int, hasRequirement: Bool, - hasImplementationSymbols: Bool, - implementationOffset: Int + implementationOffset: Int? ) -> String { let expr: ExprSyntax = """ Entry( offset: \(raw: BaselineEmitter.hex(offset)), hasRequirement: \(literal: hasRequirement), - hasImplementationSymbols: \(literal: hasImplementationSymbols), - implementationOffset: \(raw: BaselineEmitter.hex(implementationOffset)) + implementationOffset: \(raw: BaselineEmitter.optionalHex(implementationOffset)) ) """ return expr.description diff --git a/Sources/MachOFoundation/Exported.swift b/Sources/MachOFoundation/Exported.swift index 586a80d3..48103864 100644 --- a/Sources/MachOFoundation/Exported.swift +++ b/Sources/MachOFoundation/Exported.swift @@ -1,7 +1,3 @@ -@_exported import MachOKitExtensions -@_exported import MachOReading -@_exported import MachOPointers +@_exported import MachOBase +@_exported import MachODependencies @_exported import MachOSymbols -@_exported import MachOResolving -@_exported import MachOSymbolPointers -@_exported import Utilities diff --git a/Sources/MachOSymbolPointers/SymbolOrElementPointer.swift b/Sources/MachOPointers/SymbolOrElementPointer.swift similarity index 99% rename from Sources/MachOSymbolPointers/SymbolOrElementPointer.swift rename to Sources/MachOPointers/SymbolOrElementPointer.swift index 1abd59d5..8910f786 100644 --- a/Sources/MachOSymbolPointers/SymbolOrElementPointer.swift +++ b/Sources/MachOPointers/SymbolOrElementPointer.swift @@ -1,7 +1,5 @@ import MachOKit import MachOReading -import MachOPointers -import MachOSymbols import MachOResolving import MachOKitExtensions diff --git a/Sources/MachOResolving/Symbol.swift b/Sources/MachOResolving/Symbol.swift new file mode 100644 index 00000000..d5d2de77 --- /dev/null +++ b/Sources/MachOResolving/Symbol.swift @@ -0,0 +1,51 @@ +import MachOKit +import MachOKitExtensions + +/// A symbol as the resolution layer knows it: the offset it sits at, its +/// name, and whether the symbol-table entry was an undefined external +/// import. +/// +/// This is a plain value — a bind-table entry read back through +/// `SymbolOrElementPointer`, or a row a symbol index vends — and it carries +/// no lookup behavior of its own. Looking a symbol *up* by offset is the +/// job of `MachOSymbols` (`symbols(offset:)`), deliberately one layer above +/// the ABI model (evolution proposal `self-contained-abi-layer`). +public struct Symbol: Hashable, Sendable { + public let offset: Int + + public let name: String + + /// Whether the symbol-table entry was flagged as an undefined external + /// import (`N_EXT` with `N_UNDF` type). Extracted from the `nlist` entry + /// at collection time; the entry itself is not retained. + public let isExternal: Bool + + public init(offset: Int, name: String, isExternal: Bool = false) { + self.offset = offset + self.name = name + self.isExternal = isExternal + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(offset) + hasher.combine(name) + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.offset == rhs.offset && lhs.name == rhs.name + } + + public enum AddressFormat { + case hex + case decimal + } + + public func addressString(format: AddressFormat, in machO: some MachORepresentableWithCache) -> String { + switch format { + case .hex: + return "0x" + String(machO.address(forOffset: offset), radix: 16, uppercase: true) + case .decimal: + return String(machO.address(forOffset: offset), radix: 10) + } + } +} diff --git a/Sources/MachOSymbols/SymbolOrElement.swift b/Sources/MachOResolving/SymbolOrElement.swift similarity index 99% rename from Sources/MachOSymbols/SymbolOrElement.swift rename to Sources/MachOResolving/SymbolOrElement.swift index 3b8bb2ed..d7834c43 100644 --- a/Sources/MachOSymbols/SymbolOrElement.swift +++ b/Sources/MachOResolving/SymbolOrElement.swift @@ -1,6 +1,5 @@ import MachOKit import MachOReading -import MachOResolving import MachOKitExtensions public enum SymbolOrElement: Resolvable { diff --git a/Sources/MachOResolving/Symbols.swift b/Sources/MachOResolving/Symbols.swift new file mode 100644 index 00000000..0a6b19b3 --- /dev/null +++ b/Sources/MachOResolving/Symbols.swift @@ -0,0 +1,51 @@ +/// Every symbol found at one offset. +/// +/// Identical code folding leaves several names at one address, so a +/// symbol lookup answers with a collection rather than a single `Symbol`. +/// The resolution layer only defines the collection; `MachOSymbols` +/// populates it from its per-image index. +public struct Symbols: Sendable { + public let offset: Int + + private var _storage: [Symbol] = [] + + package init(offset: Int, symbols: [Symbol]) { + self.offset = offset + self._storage = symbols + } +} + +extension Symbols: RandomAccessCollection { + public typealias Element = Symbol + + public var startIndex: Int { _storage.startIndex } + + public var endIndex: Int { _storage.endIndex } + + public func index(after i: Int) -> Int { + _storage.index(after: i) + } +} + +extension Symbols: MutableCollection { + public subscript(position: Int) -> Symbol { + get { + _storage[position] + } + set { + _storage[position] = newValue + } + } + + public mutating func append(_ newElement: Symbol) { + _storage.append(newElement) + } + + public mutating func remove(at index: Int) { + _storage.remove(at: index) + } + + public mutating func removeAll() { + _storage.removeAll() + } +} diff --git a/Sources/MachOSwiftSection/Exported.swift b/Sources/MachOSwiftSection/Exported.swift index 45461596..d0e38e77 100644 --- a/Sources/MachOSwiftSection/Exported.swift +++ b/Sources/MachOSwiftSection/Exported.swift @@ -1 +1 @@ -@_exported import MachOFoundation +@_exported import MachOBase diff --git a/Sources/MachOSwiftSection/Extensions/String+.swift b/Sources/MachOSwiftSection/Extensions/String+.swift index b5afc379..3bb6d049 100644 --- a/Sources/MachOSwiftSection/Extensions/String+.swift +++ b/Sources/MachOSwiftSection/Extensions/String+.swift @@ -1,5 +1,3 @@ -import Demangling - extension String { var countedString: String { guard !isEmpty else { return "" } @@ -15,7 +13,7 @@ extension String { } var insertManglePrefix: String { - guard !isSwiftSymbol else { return self } + guard !hasSwiftManglingPrefix else { return self } return "_$s" + self } @@ -26,4 +24,40 @@ extension String { var stripDuplicateProtocolMangleType: String { replacingOccurrences(of: "_p_p", with: "_p") } + + /// Length of the Swift mangling prefix the string starts with, `0` for + /// none. + /// + /// The same prefix list as `Demangling.getManglingPrefixLength` (and + /// `MachOSymbols`' byte-level `nameBytesHaveSwiftManglingPrefix`), kept + /// local so the ABI model does not depend on the demangler for a prefix + /// check; `ManglingPrefixTests` pins `hasSwiftManglingPrefix` / + /// `strippingSwiftManglingPrefix` equal to the demangler's answers (and + /// `CImportedModuleNames` equal to its module-name constants). + var swiftManglingPrefixLength: Int { + let utf8Bytes = utf8 + if utf8Bytes.starts(with: "_T0".utf8) || utf8Bytes.starts(with: "_$S".utf8) || utf8Bytes.starts(with: "_$s".utf8) || utf8Bytes.starts(with: "_$e".utf8) { + return 3 + } else if utf8Bytes.starts(with: "$S".utf8) || utf8Bytes.starts(with: "$s".utf8) || utf8Bytes.starts(with: "$e".utf8) { + return 2 + } else if utf8Bytes.starts(with: "@__swiftmacro_".utf8) { + return 14 + } + return 0 + } + + /// Whether the string starts with a Swift mangling prefix. + var hasSwiftManglingPrefix: Bool { + swiftManglingPrefixLength > 0 + } + + /// The string without its Swift mangling prefix; unchanged when it has + /// none. + var strippingSwiftManglingPrefix: String { + // One prefix scan, not two: `hasSwiftManglingPrefix` would compute + // the same length and throw it away. + let prefixLength = swiftManglingPrefixLength + guard prefixLength > 0 else { return self } + return String(dropFirst(prefixLength)) + } } diff --git a/Sources/MachOSwiftSection/MachOFile+Swift.swift b/Sources/MachOSwiftSection/MachOFile+Swift.swift index 603067df..5b2d13b9 100644 --- a/Sources/MachOSwiftSection/MachOFile+Swift.swift +++ b/Sources/MachOSwiftSection/MachOFile+Swift.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase extension MachOFile { public struct Swift { diff --git a/Sources/MachOSwiftSection/MachOImage+Swift.swift b/Sources/MachOSwiftSection/MachOImage+Swift.swift index 0a5334c6..fa6b1308 100644 --- a/Sources/MachOSwiftSection/MachOImage+Swift.swift +++ b/Sources/MachOSwiftSection/MachOImage+Swift.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase extension MachOImage { public struct Swift { diff --git a/Sources/MachOSwiftSection/Models/Anonymous/AnonymousContext.swift b/Sources/MachOSwiftSection/Models/Anonymous/AnonymousContext.swift index 7713da32..5cc0a353 100644 --- a/Sources/MachOSwiftSection/Models/Anonymous/AnonymousContext.swift +++ b/Sources/MachOSwiftSection/Models/Anonymous/AnonymousContext.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct AnonymousContext: TopLevelType, ContextProtocol { public let descriptor: AnonymousContextDescriptor diff --git a/Sources/MachOSwiftSection/Models/Anonymous/AnonymousContextDescriptor.swift b/Sources/MachOSwiftSection/Models/Anonymous/AnonymousContextDescriptor.swift index 30a3261d..86bb0f64 100644 --- a/Sources/MachOSwiftSection/Models/Anonymous/AnonymousContextDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/Anonymous/AnonymousContextDescriptor.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct AnonymousContextDescriptor: AnonymousContextDescriptorProtocol { public struct Layout: AnonymousContextDescriptorLayout { diff --git a/Sources/MachOSwiftSection/Models/Anonymous/AnonymousContextDescriptorProtocol.swift b/Sources/MachOSwiftSection/Models/Anonymous/AnonymousContextDescriptorProtocol.swift index 880df665..81eecfcc 100644 --- a/Sources/MachOSwiftSection/Models/Anonymous/AnonymousContextDescriptorProtocol.swift +++ b/Sources/MachOSwiftSection/Models/Anonymous/AnonymousContextDescriptorProtocol.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase public protocol AnonymousContextDescriptorProtocol: ContextDescriptorProtocol where Layout: AnonymousContextDescriptorLayout {} diff --git a/Sources/MachOSwiftSection/Models/AssociatedType/AssociatedType.swift b/Sources/MachOSwiftSection/Models/AssociatedType/AssociatedType.swift index fdc02bdb..a4638907 100644 --- a/Sources/MachOSwiftSection/Models/AssociatedType/AssociatedType.swift +++ b/Sources/MachOSwiftSection/Models/AssociatedType/AssociatedType.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct AssociatedType: TopLevelType { public let descriptor: AssociatedTypeDescriptor diff --git a/Sources/MachOSwiftSection/Models/AssociatedType/AssociatedTypeDescriptor.swift b/Sources/MachOSwiftSection/Models/AssociatedType/AssociatedTypeDescriptor.swift index 9537ab0b..4170c055 100644 --- a/Sources/MachOSwiftSection/Models/AssociatedType/AssociatedTypeDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/AssociatedType/AssociatedTypeDescriptor.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct AssociatedTypeDescriptor: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/AssociatedType/AssociatedTypeRecord.swift b/Sources/MachOSwiftSection/Models/AssociatedType/AssociatedTypeRecord.swift index ed860e61..b51bd581 100644 --- a/Sources/MachOSwiftSection/Models/AssociatedType/AssociatedTypeRecord.swift +++ b/Sources/MachOSwiftSection/Models/AssociatedType/AssociatedTypeRecord.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct AssociatedTypeRecord: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/BuiltinType/BuiltinType.swift b/Sources/MachOSwiftSection/Models/BuiltinType/BuiltinType.swift index b3856ad3..3a2202bd 100644 --- a/Sources/MachOSwiftSection/Models/BuiltinType/BuiltinType.swift +++ b/Sources/MachOSwiftSection/Models/BuiltinType/BuiltinType.swift @@ -1,7 +1,6 @@ import Foundation -import MachOSymbols import MachOKit -import MachOFoundation +import MachOBase public struct BuiltinType: TopLevelType { public let descriptor: BuiltinTypeDescriptor diff --git a/Sources/MachOSwiftSection/Models/BuiltinType/BuiltinTypeDescriptor.swift b/Sources/MachOSwiftSection/Models/BuiltinType/BuiltinTypeDescriptor.swift index 9b0e3e38..93466f1a 100644 --- a/Sources/MachOSwiftSection/Models/BuiltinType/BuiltinTypeDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/BuiltinType/BuiltinTypeDescriptor.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct BuiltinTypeDescriptor: ResolvableLocatableLayoutWrapper, TopLevelDescriptor { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptor.swift b/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptor.swift index 63bdc0a6..4361ec4c 100644 --- a/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptor.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct ContextDescriptor: ContextDescriptorProtocol { public struct Layout: ContextDescriptorLayout { diff --git a/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptorLayout.swift b/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptorLayout.swift index d6135fb4..a708af2f 100644 --- a/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptorLayout.swift +++ b/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptorLayout.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase @Layout public protocol ContextDescriptorLayout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptorProtocol.swift b/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptorProtocol.swift index 57666e18..9930c690 100644 --- a/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptorProtocol.swift +++ b/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptorProtocol.swift @@ -1,6 +1,5 @@ import MachOKit -import MachOFoundation -import Demangling +import MachOBase @dynamicMemberLookup public protocol ContextDescriptorProtocol: ResolvableLocatableLayoutWrapper where Layout: ContextDescriptorLayout { @@ -56,7 +55,7 @@ extension ContextDescriptorProtocol { public func isCImportedContextDescriptor(in machO: MachO) throws -> Bool { guard let moduleContextDescriptor = try moduleContextDescriptor(in: machO) else { return false } let moduleName = try moduleContextDescriptor.name(in: machO) - return moduleName == cModule || moduleName == objcModule + return moduleName == CImportedModuleNames.cSynthesized || moduleName == CImportedModuleNames.objectiveC } } @@ -89,7 +88,7 @@ extension ContextDescriptorProtocol { public func isCImportedContextDescriptor() throws -> Bool { guard let moduleContextDescriptor = try moduleContextDescriptor() else { return false } let moduleName = try moduleContextDescriptor.name() - return moduleName == cModule || moduleName == objcModule + return moduleName == CImportedModuleNames.cSynthesized || moduleName == CImportedModuleNames.objectiveC } } @@ -126,6 +125,6 @@ extension ContextDescriptorProtocol { public func isCImportedContextDescriptor(in context: Context) throws -> Bool { guard let moduleContextDescriptor = try moduleContextDescriptor(in: context) else { return false } let moduleName = try moduleContextDescriptor.name(in: context) - return moduleName == cModule || moduleName == objcModule + return moduleName == CImportedModuleNames.cSynthesized || moduleName == CImportedModuleNames.objectiveC } } diff --git a/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptorWrapper.swift b/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptorWrapper.swift index 362b1604..91453200 100644 --- a/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptorWrapper.swift +++ b/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptorWrapper.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase import SwiftStdlibToolbox public enum ContextDescriptorWrapper { diff --git a/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextProtocol.swift b/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextProtocol.swift index 0dfc3d47..8e952152 100644 --- a/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextProtocol.swift +++ b/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextProtocol.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public protocol ContextProtocol: Sendable { associatedtype Descriptor: ContextDescriptorProtocol diff --git a/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextWrapper.swift b/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextWrapper.swift index cbf9ce03..ba394922 100644 --- a/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextWrapper.swift +++ b/Sources/MachOSwiftSection/Models/ContextDescriptor/ContextWrapper.swift @@ -1,7 +1,7 @@ import Foundation import MachOKit import FoundationToolbox -import MachOFoundation +import MachOBase @CaseCheckable(.public) @AssociatedValue(.public) diff --git a/Sources/MachOSwiftSection/Models/ContextDescriptor/NamedContextDescriptorLayout.swift b/Sources/MachOSwiftSection/Models/ContextDescriptor/NamedContextDescriptorLayout.swift index 173da094..ffa3ce22 100644 --- a/Sources/MachOSwiftSection/Models/ContextDescriptor/NamedContextDescriptorLayout.swift +++ b/Sources/MachOSwiftSection/Models/ContextDescriptor/NamedContextDescriptorLayout.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase @Layout public protocol NamedContextDescriptorLayout: ContextDescriptorLayout { diff --git a/Sources/MachOSwiftSection/Models/ContextDescriptor/NamedContextDescriptorProtocol.swift b/Sources/MachOSwiftSection/Models/ContextDescriptor/NamedContextDescriptorProtocol.swift index af3763e3..a8804876 100644 --- a/Sources/MachOSwiftSection/Models/ContextDescriptor/NamedContextDescriptorProtocol.swift +++ b/Sources/MachOSwiftSection/Models/ContextDescriptor/NamedContextDescriptorProtocol.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase public protocol NamedContextDescriptorProtocol: ContextDescriptorProtocol where Layout: NamedContextDescriptorLayout {} diff --git a/Sources/MachOSwiftSection/Models/DispatchClass/DispatchClassMetadata.swift b/Sources/MachOSwiftSection/Models/DispatchClass/DispatchClassMetadata.swift index 5584acee..9f57543b 100644 --- a/Sources/MachOSwiftSection/Models/DispatchClass/DispatchClassMetadata.swift +++ b/Sources/MachOSwiftSection/Models/DispatchClass/DispatchClassMetadata.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct DispatchClassMetadata: HeapMetadataProtocol { public struct Layout: HeapMetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/ExistentialType/ExistentialMetatypeMetadata.swift b/Sources/MachOSwiftSection/Models/ExistentialType/ExistentialMetatypeMetadata.swift index ee547689..4796a30e 100644 --- a/Sources/MachOSwiftSection/Models/ExistentialType/ExistentialMetatypeMetadata.swift +++ b/Sources/MachOSwiftSection/Models/ExistentialType/ExistentialMetatypeMetadata.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct ExistentialMetatypeMetadata: MetadataProtocol { public struct Layout: MetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/ExistentialType/ExistentialTypeFlags.swift b/Sources/MachOSwiftSection/Models/ExistentialType/ExistentialTypeFlags.swift index 2caddce9..adc78d30 100644 --- a/Sources/MachOSwiftSection/Models/ExistentialType/ExistentialTypeFlags.swift +++ b/Sources/MachOSwiftSection/Models/ExistentialType/ExistentialTypeFlags.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct ExistentialTypeFlags: OptionSet, Sendable { public typealias RawValue = UInt32 diff --git a/Sources/MachOSwiftSection/Models/ExistentialType/ExistentialTypeMetadata.swift b/Sources/MachOSwiftSection/Models/ExistentialType/ExistentialTypeMetadata.swift index e4aeb40f..30d5e163 100644 --- a/Sources/MachOSwiftSection/Models/ExistentialType/ExistentialTypeMetadata.swift +++ b/Sources/MachOSwiftSection/Models/ExistentialType/ExistentialTypeMetadata.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct ExistentialTypeMetadata: MetadataProtocol { public struct Layout: ExistentialTypeMetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/ExistentialType/ExistentialTypeMetadataLayout.swift b/Sources/MachOSwiftSection/Models/ExistentialType/ExistentialTypeMetadataLayout.swift index 81c53b6e..c572ef37 100644 --- a/Sources/MachOSwiftSection/Models/ExistentialType/ExistentialTypeMetadataLayout.swift +++ b/Sources/MachOSwiftSection/Models/ExistentialType/ExistentialTypeMetadataLayout.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase @Layout public protocol ExistentialTypeMetadataLayout: MetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/ExistentialType/ExtendedExistentialTypeMetadata.swift b/Sources/MachOSwiftSection/Models/ExistentialType/ExtendedExistentialTypeMetadata.swift index 432a04d3..118eab7e 100644 --- a/Sources/MachOSwiftSection/Models/ExistentialType/ExtendedExistentialTypeMetadata.swift +++ b/Sources/MachOSwiftSection/Models/ExistentialType/ExtendedExistentialTypeMetadata.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct ExtendedExistentialTypeMetadata: MetadataProtocol { public struct Layout: MetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/ExistentialType/ExtendedExistentialTypeShape.swift b/Sources/MachOSwiftSection/Models/ExistentialType/ExtendedExistentialTypeShape.swift index 5025c13d..3d659cd1 100644 --- a/Sources/MachOSwiftSection/Models/ExistentialType/ExtendedExistentialTypeShape.swift +++ b/Sources/MachOSwiftSection/Models/ExistentialType/ExtendedExistentialTypeShape.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct ExtendedExistentialTypeShape: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/ExistentialType/NonUniqueExtendedExistentialTypeShape.swift b/Sources/MachOSwiftSection/Models/ExistentialType/NonUniqueExtendedExistentialTypeShape.swift index bfbe04ca..62cebaaf 100644 --- a/Sources/MachOSwiftSection/Models/ExistentialType/NonUniqueExtendedExistentialTypeShape.swift +++ b/Sources/MachOSwiftSection/Models/ExistentialType/NonUniqueExtendedExistentialTypeShape.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct NonUniqueExtendedExistentialTypeShape: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Extension/ExtensionContext.swift b/Sources/MachOSwiftSection/Models/Extension/ExtensionContext.swift index bc8de062..1fb74d2a 100644 --- a/Sources/MachOSwiftSection/Models/Extension/ExtensionContext.swift +++ b/Sources/MachOSwiftSection/Models/Extension/ExtensionContext.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct ExtensionContext: TopLevelType, ContextProtocol { public let descriptor: ExtensionContextDescriptor diff --git a/Sources/MachOSwiftSection/Models/Extension/ExtensionContextDescriptor.swift b/Sources/MachOSwiftSection/Models/Extension/ExtensionContextDescriptor.swift index a09bab62..fb1f4bb2 100644 --- a/Sources/MachOSwiftSection/Models/Extension/ExtensionContextDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/Extension/ExtensionContextDescriptor.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct ExtensionContextDescriptor: ExtensionContextDescriptorProtocol { public struct Layout: ExtensionContextDescriptorLayout { diff --git a/Sources/MachOSwiftSection/Models/Extension/ExtensionContextDescriptorLayout.swift b/Sources/MachOSwiftSection/Models/Extension/ExtensionContextDescriptorLayout.swift index 7fb3f03b..bb7c40b3 100644 --- a/Sources/MachOSwiftSection/Models/Extension/ExtensionContextDescriptorLayout.swift +++ b/Sources/MachOSwiftSection/Models/Extension/ExtensionContextDescriptorLayout.swift @@ -1,4 +1,4 @@ -import MachOFoundation +import MachOBase @Layout public protocol ExtensionContextDescriptorLayout: ContextDescriptorLayout { diff --git a/Sources/MachOSwiftSection/Models/FieldDescriptor/FieldDescriptor.swift b/Sources/MachOSwiftSection/Models/FieldDescriptor/FieldDescriptor.swift index 106e7cbf..446706eb 100644 --- a/Sources/MachOSwiftSection/Models/FieldDescriptor/FieldDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/FieldDescriptor/FieldDescriptor.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct FieldDescriptor: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/FieldRecord/FieldRecord.swift b/Sources/MachOSwiftSection/Models/FieldRecord/FieldRecord.swift index 4b19236b..c687a9ef 100644 --- a/Sources/MachOSwiftSection/Models/FieldRecord/FieldRecord.swift +++ b/Sources/MachOSwiftSection/Models/FieldRecord/FieldRecord.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct FieldRecord: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/ForeignType/ForeignClassMetadata.swift b/Sources/MachOSwiftSection/Models/ForeignType/ForeignClassMetadata.swift index 23553115..e8f237d6 100644 --- a/Sources/MachOSwiftSection/Models/ForeignType/ForeignClassMetadata.swift +++ b/Sources/MachOSwiftSection/Models/ForeignType/ForeignClassMetadata.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct ForeignClassMetadata: MetadataProtocol { public struct Layout: ForeignClassMetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/ForeignType/ForeignClassMetadataLayout.swift b/Sources/MachOSwiftSection/Models/ForeignType/ForeignClassMetadataLayout.swift index 6e91117b..287e4104 100644 --- a/Sources/MachOSwiftSection/Models/ForeignType/ForeignClassMetadataLayout.swift +++ b/Sources/MachOSwiftSection/Models/ForeignType/ForeignClassMetadataLayout.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase @Layout public protocol ForeignClassMetadataLayout: MetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/ForeignType/ForeignReferenceTypeMetadata.swift b/Sources/MachOSwiftSection/Models/ForeignType/ForeignReferenceTypeMetadata.swift index a08513e8..fc7845ac 100644 --- a/Sources/MachOSwiftSection/Models/ForeignType/ForeignReferenceTypeMetadata.swift +++ b/Sources/MachOSwiftSection/Models/ForeignType/ForeignReferenceTypeMetadata.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct ForeignReferenceTypeMetadata: MetadataProtocol { public struct Layout: ForeignReferenceTypeMetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/ForeignType/ForeignReferenceTypeMetadataLayout.swift b/Sources/MachOSwiftSection/Models/ForeignType/ForeignReferenceTypeMetadataLayout.swift index cf76a2cd..0952b091 100644 --- a/Sources/MachOSwiftSection/Models/ForeignType/ForeignReferenceTypeMetadataLayout.swift +++ b/Sources/MachOSwiftSection/Models/ForeignType/ForeignReferenceTypeMetadataLayout.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase @Layout public protocol ForeignReferenceTypeMetadataLayout: MetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Function/FunctionTypeMetadata.swift b/Sources/MachOSwiftSection/Models/Function/FunctionTypeMetadata.swift index 6c390315..f4ebf740 100644 --- a/Sources/MachOSwiftSection/Models/Function/FunctionTypeMetadata.swift +++ b/Sources/MachOSwiftSection/Models/Function/FunctionTypeMetadata.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct FunctionTypeMetadata: MetadataProtocol { public struct Layout: MetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Generic/GenericContext.swift b/Sources/MachOSwiftSection/Models/Generic/GenericContext.swift index c387eff7..0b44c5fa 100644 --- a/Sources/MachOSwiftSection/Models/Generic/GenericContext.swift +++ b/Sources/MachOSwiftSection/Models/Generic/GenericContext.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase import MemberwiseInit public typealias GenericContext = TargetGenericContext diff --git a/Sources/MachOSwiftSection/Models/Generic/GenericEnvironment.swift b/Sources/MachOSwiftSection/Models/Generic/GenericEnvironment.swift index 07b08632..c6d6d608 100644 --- a/Sources/MachOSwiftSection/Models/Generic/GenericEnvironment.swift +++ b/Sources/MachOSwiftSection/Models/Generic/GenericEnvironment.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct GenericEnvironment: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Generic/GenericPackShapeDescriptor.swift b/Sources/MachOSwiftSection/Models/Generic/GenericPackShapeDescriptor.swift index a0bdc530..22cc4b6f 100644 --- a/Sources/MachOSwiftSection/Models/Generic/GenericPackShapeDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/Generic/GenericPackShapeDescriptor.swift @@ -1,4 +1,4 @@ -import MachOFoundation +import MachOBase public struct GenericPackShapeDescriptor: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Generic/GenericPackShapeHeader.swift b/Sources/MachOSwiftSection/Models/Generic/GenericPackShapeHeader.swift index 4a522ae5..746f2e08 100644 --- a/Sources/MachOSwiftSection/Models/Generic/GenericPackShapeHeader.swift +++ b/Sources/MachOSwiftSection/Models/Generic/GenericPackShapeHeader.swift @@ -1,4 +1,4 @@ -import MachOFoundation +import MachOBase public struct GenericPackShapeHeader: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Generic/GenericParamDescriptor.swift b/Sources/MachOSwiftSection/Models/Generic/GenericParamDescriptor.swift index e9d74fd8..72141792 100644 --- a/Sources/MachOSwiftSection/Models/Generic/GenericParamDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/Generic/GenericParamDescriptor.swift @@ -1,4 +1,4 @@ -import MachOFoundation +import MachOBase public struct GenericParamDescriptor: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Generic/GenericRequirement.swift b/Sources/MachOSwiftSection/Models/Generic/GenericRequirement.swift index 8fc41088..adc2cc3d 100644 --- a/Sources/MachOSwiftSection/Models/Generic/GenericRequirement.swift +++ b/Sources/MachOSwiftSection/Models/Generic/GenericRequirement.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct GenericRequirement: Sendable, TopLevelType { public let descriptor: GenericRequirementDescriptor diff --git a/Sources/MachOSwiftSection/Models/Generic/GenericRequirementContent.swift b/Sources/MachOSwiftSection/Models/Generic/GenericRequirementContent.swift index eaf1b15c..5745f6e6 100644 --- a/Sources/MachOSwiftSection/Models/Generic/GenericRequirementContent.swift +++ b/Sources/MachOSwiftSection/Models/Generic/GenericRequirementContent.swift @@ -1,4 +1,4 @@ -import MachOFoundation +import MachOBase import FoundationToolbox @CaseCheckable(.public) diff --git a/Sources/MachOSwiftSection/Models/Generic/GenericRequirementDescriptor.swift b/Sources/MachOSwiftSection/Models/Generic/GenericRequirementDescriptor.swift index e680fcbf..9f63202c 100644 --- a/Sources/MachOSwiftSection/Models/Generic/GenericRequirementDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/Generic/GenericRequirementDescriptor.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct GenericRequirementDescriptor: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Generic/GenericValueDescriptor.swift b/Sources/MachOSwiftSection/Models/Generic/GenericValueDescriptor.swift index 32f08703..c85ff69a 100644 --- a/Sources/MachOSwiftSection/Models/Generic/GenericValueDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/Generic/GenericValueDescriptor.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct GenericValueDescriptor: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Generic/GenericValueHeader.swift b/Sources/MachOSwiftSection/Models/Generic/GenericValueHeader.swift index 76042093..a4d09eb8 100644 --- a/Sources/MachOSwiftSection/Models/Generic/GenericValueHeader.swift +++ b/Sources/MachOSwiftSection/Models/Generic/GenericValueHeader.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct GenericValueHeader: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Generic/GenericWitnessTable.swift b/Sources/MachOSwiftSection/Models/Generic/GenericWitnessTable.swift index e60b6532..0adf3c23 100644 --- a/Sources/MachOSwiftSection/Models/Generic/GenericWitnessTable.swift +++ b/Sources/MachOSwiftSection/Models/Generic/GenericWitnessTable.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct GenericWitnessTable: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Generic/TypeGenericContextDescriptorHeader.swift b/Sources/MachOSwiftSection/Models/Generic/TypeGenericContextDescriptorHeader.swift index e39e2b04..38e681b8 100644 --- a/Sources/MachOSwiftSection/Models/Generic/TypeGenericContextDescriptorHeader.swift +++ b/Sources/MachOSwiftSection/Models/Generic/TypeGenericContextDescriptorHeader.swift @@ -1,4 +1,4 @@ -import MachOFoundation +import MachOBase public struct TypeGenericContextDescriptorHeader: GenericContextDescriptorHeaderProtocol { public struct Layout: GenericContextDescriptorHeaderLayout { diff --git a/Sources/MachOSwiftSection/Models/Heap/GenericBoxHeapMetadata.swift b/Sources/MachOSwiftSection/Models/Heap/GenericBoxHeapMetadata.swift index 5e147f96..40485bba 100644 --- a/Sources/MachOSwiftSection/Models/Heap/GenericBoxHeapMetadata.swift +++ b/Sources/MachOSwiftSection/Models/Heap/GenericBoxHeapMetadata.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct GenericBoxHeapMetadata: HeapMetadataProtocol { public struct Layout: HeapMetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Heap/HeapLocalVariableMetadata.swift b/Sources/MachOSwiftSection/Models/Heap/HeapLocalVariableMetadata.swift index 6b7801cf..8b62404e 100644 --- a/Sources/MachOSwiftSection/Models/Heap/HeapLocalVariableMetadata.swift +++ b/Sources/MachOSwiftSection/Models/Heap/HeapLocalVariableMetadata.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct HeapLocalVariableMetadata: HeapMetadataProtocol { public struct Layout: HeapMetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Mangling/CImportedModuleNames.swift b/Sources/MachOSwiftSection/Models/Mangling/CImportedModuleNames.swift new file mode 100644 index 00000000..a5725c9c --- /dev/null +++ b/Sources/MachOSwiftSection/Models/Mangling/CImportedModuleNames.swift @@ -0,0 +1,12 @@ +/// The module names the compiler gives C-imported declarations, as the +/// runtime spells them in module context descriptors: `__C` for +/// Objective-C / C declarations and `__C_Synthesized` for the wrappers it +/// synthesizes around them. +/// +/// These are ABI facts, so they live here rather than being borrowed from +/// the demangler's `objcModule` / `cModule` constants — the ABI model does +/// not depend on `Demangling` (evolution proposal `self-contained-abi-layer`). +enum CImportedModuleNames { + static let objectiveC = "__C" + static let cSynthesized = "__C_Synthesized" +} diff --git a/Sources/MachOSwiftSection/Models/Mangling/MangledName.swift b/Sources/MachOSwiftSection/Models/Mangling/MangledName.swift index 3514e93e..b6ec4408 100644 --- a/Sources/MachOSwiftSection/Models/Mangling/MangledName.swift +++ b/Sources/MachOSwiftSection/Models/Mangling/MangledName.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct MangledName: Sendable, Hashable { package enum Element: Sendable, Hashable { @@ -73,7 +73,7 @@ public struct MangledName: Sendable, Hashable { public var symbolString: String { guard !elements.isEmpty else { return "" } let rawStringValue = rawString - if rawStringValue.isSwiftSymbol { + if rawStringValue.hasSwiftManglingPrefix { return rawStringValue } else { return rawStringValue.insertManglePrefix @@ -83,8 +83,8 @@ public struct MangledName: Sendable, Hashable { public var typeString: String { guard !elements.isEmpty else { return "" } let rawStringValue = rawString - if rawStringValue.isSwiftSymbol { - return rawStringValue.stripManglePrefix + if rawStringValue.hasSwiftManglingPrefix { + return rawStringValue.strippingSwiftManglingPrefix } else { return rawStringValue } diff --git a/Sources/MachOSwiftSection/Models/Metadata/CanonicalSpecializedMetadataAccessorsListEntry.swift b/Sources/MachOSwiftSection/Models/Metadata/CanonicalSpecializedMetadataAccessorsListEntry.swift index 7c8c4bff..90fc9e9d 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/CanonicalSpecializedMetadataAccessorsListEntry.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/CanonicalSpecializedMetadataAccessorsListEntry.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct CanonicalSpecializedMetadataAccessorsListEntry: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Metadata/CanonicalSpecializedMetadatasCachingOnceToken.swift b/Sources/MachOSwiftSection/Models/Metadata/CanonicalSpecializedMetadatasCachingOnceToken.swift index 8f4f4dba..a1d28f2b 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/CanonicalSpecializedMetadatasCachingOnceToken.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/CanonicalSpecializedMetadatasCachingOnceToken.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public typealias SwiftOnceToken = intptr_t diff --git a/Sources/MachOSwiftSection/Models/Metadata/CanonicalSpecializedMetadatasListEntry.swift b/Sources/MachOSwiftSection/Models/Metadata/CanonicalSpecializedMetadatasListEntry.swift index 15ca1491..cc84c7bd 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/CanonicalSpecializedMetadatasListEntry.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/CanonicalSpecializedMetadatasListEntry.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct CanonicalSpecializedMetadatasListEntry: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Metadata/FixedArrayTypeMetadata.swift b/Sources/MachOSwiftSection/Models/Metadata/FixedArrayTypeMetadata.swift index 63a7e146..72972682 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/FixedArrayTypeMetadata.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/FixedArrayTypeMetadata.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase import MachOKit public struct FixedArrayTypeMetadata: MetadataProtocol { diff --git a/Sources/MachOSwiftSection/Models/Metadata/FixedArrayTypeMetadataLayout.swift b/Sources/MachOSwiftSection/Models/Metadata/FixedArrayTypeMetadataLayout.swift index e76583e6..e7f50c49 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/FixedArrayTypeMetadataLayout.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/FixedArrayTypeMetadataLayout.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase @Layout public protocol FixedArrayTypeMetadataLayout: MetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Metadata/FullMetadata.swift b/Sources/MachOSwiftSection/Models/Metadata/FullMetadata.swift index b579245b..5094e1f0 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/FullMetadata.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/FullMetadata.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct FullMetadata: ResolvableLocatableLayoutWrapper { @dynamicMemberLookup diff --git a/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeader.swift b/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeader.swift index c03c946b..07899b6f 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeader.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeader.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct HeapMetadataHeader: HeapMetadataHeaderProtocol { public struct Layout: HeapMetadataHeaderLayout { diff --git a/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderLayout.swift b/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderLayout.swift index 94fb94a8..f3d06f24 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderLayout.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderLayout.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase @Layout public protocol HeapMetadataHeaderLayout: TypeMetadataLayoutPrefixLayout, HeapMetadataHeaderPrefixLayout, TypeMetadataHeaderBaseLayout {} diff --git a/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderPrefix.swift b/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderPrefix.swift index b94e2876..b86172ea 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderPrefix.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderPrefix.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct HeapMetadataHeaderPrefix: HeapMetadataHeaderPrefixProtocol { public struct Layout: HeapMetadataHeaderPrefixLayout { diff --git a/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderPrefixLayout.swift b/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderPrefixLayout.swift index 0a6b0d8c..8ecc527d 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderPrefixLayout.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderPrefixLayout.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase @Layout public protocol HeapMetadataHeaderPrefixLayout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderPrefixProtocol.swift b/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderPrefixProtocol.swift index 3ad5cc94..bb763cbc 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderPrefixProtocol.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderPrefixProtocol.swift @@ -1,5 +1,5 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public protocol HeapMetadataHeaderPrefixProtocol: ResolvableLocatableLayoutWrapper where Layout: HeapMetadataHeaderPrefixLayout {} diff --git a/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderProtocol.swift b/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderProtocol.swift index 9e2a02a5..18e1460a 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderProtocol.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/Headers/HeapMetadataHeaderProtocol.swift @@ -1,5 +1,5 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public protocol HeapMetadataHeaderProtocol: TypeMetadataLayoutPrefixProtocol, HeapMetadataHeaderPrefixProtocol, TypeMetadataHeaderBaseProtocol where Layout: HeapMetadataHeaderLayout {} diff --git a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeader.swift b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeader.swift index 5efd5645..82de267c 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeader.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeader.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct TypeMetadataHeader: TypeMetadataHeaderProtocol { public struct Layout: TypeMetadataHeaderLayout { diff --git a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderBase.swift b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderBase.swift index fcf6ac62..0622e6f3 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderBase.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderBase.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct TypeMetadataHeaderBase: TypeMetadataHeaderBaseProtocol { public struct Layout: TypeMetadataHeaderBaseLayout { diff --git a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderBaseLayout.swift b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderBaseLayout.swift index 00e4fcef..f8ab4e21 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderBaseLayout.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderBaseLayout.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase @Layout public protocol TypeMetadataHeaderBaseLayout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderBaseProtocol.swift b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderBaseProtocol.swift index 192bde9a..5909f89d 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderBaseProtocol.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderBaseProtocol.swift @@ -1,5 +1,5 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public protocol TypeMetadataHeaderBaseProtocol: ResolvableLocatableLayoutWrapper where Layout: TypeMetadataHeaderBaseLayout {} diff --git a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderLayout.swift b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderLayout.swift index 91229bd0..38ea898c 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderLayout.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderLayout.swift @@ -1,5 +1,5 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public protocol TypeMetadataHeaderLayout: TypeMetadataLayoutPrefixLayout, TypeMetadataHeaderBaseLayout {} diff --git a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderProtocol.swift b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderProtocol.swift index 47bccb76..1a7f82d9 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderProtocol.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataHeaderProtocol.swift @@ -1,5 +1,5 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public protocol TypeMetadataHeaderProtocol: TypeMetadataLayoutPrefixProtocol, TypeMetadataHeaderBaseProtocol where Layout: TypeMetadataHeaderLayout {} diff --git a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataLayoutPrefix.swift b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataLayoutPrefix.swift index 1c70ccfc..b2b68ad7 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataLayoutPrefix.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataLayoutPrefix.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct TypeMetadataLayoutPrefix: TypeMetadataLayoutPrefixProtocol { public struct Layout: TypeMetadataLayoutPrefixLayout { diff --git a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataLayoutPrefixLayout.swift b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataLayoutPrefixLayout.swift index b77b8119..19604521 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataLayoutPrefixLayout.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataLayoutPrefixLayout.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase @Layout public protocol TypeMetadataLayoutPrefixLayout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataLayoutPrefixProtocol.swift b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataLayoutPrefixProtocol.swift index a9d6754c..6493ddbc 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataLayoutPrefixProtocol.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/Headers/TypeMetadataLayoutPrefixProtocol.swift @@ -1,5 +1,5 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public protocol TypeMetadataLayoutPrefixProtocol: ResolvableLocatableLayoutWrapper where Layout: TypeMetadataLayoutPrefixLayout {} diff --git a/Sources/MachOSwiftSection/Models/Metadata/MetadataAccessorFunction.swift b/Sources/MachOSwiftSection/Models/Metadata/MetadataAccessorFunction.swift index d7543fe9..731e434b 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/MetadataAccessorFunction.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/MetadataAccessorFunction.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase import MachOKit import MachOSwiftSectionC diff --git a/Sources/MachOSwiftSection/Models/Metadata/MetadataBoundsLayout.swift b/Sources/MachOSwiftSection/Models/Metadata/MetadataBoundsLayout.swift index c8f199f1..622dfb7f 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/MetadataBoundsLayout.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/MetadataBoundsLayout.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase @Layout public protocol MetadataBoundsLayout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Metadata/MetadataInitialization/ForeignMetadataInitialization.swift b/Sources/MachOSwiftSection/Models/Metadata/MetadataInitialization/ForeignMetadataInitialization.swift index e051bb6c..0dd9af6f 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/MetadataInitialization/ForeignMetadataInitialization.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/MetadataInitialization/ForeignMetadataInitialization.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct ForeignMetadataInitialization: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Metadata/MetadataInitialization/SingletonMetadataInitialization.swift b/Sources/MachOSwiftSection/Models/Metadata/MetadataInitialization/SingletonMetadataInitialization.swift index 98609292..187ef443 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/MetadataInitialization/SingletonMetadataInitialization.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/MetadataInitialization/SingletonMetadataInitialization.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct SingletonMetadataInitialization: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Metadata/MetadataLayout.swift b/Sources/MachOSwiftSection/Models/Metadata/MetadataLayout.swift index c9f7e494..6f632d16 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/MetadataLayout.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/MetadataLayout.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase @Layout public protocol MetadataLayout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Metadata/MetadataResponse.swift b/Sources/MachOSwiftSection/Models/Metadata/MetadataResponse.swift index ccbab413..d66879c9 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/MetadataResponse.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/MetadataResponse.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct MetadataResponse { public let value: Pointer diff --git a/Sources/MachOSwiftSection/Models/Metadata/MetadataWrapper.swift b/Sources/MachOSwiftSection/Models/Metadata/MetadataWrapper.swift index 07d85a0b..c04347a4 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/MetadataWrapper.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/MetadataWrapper.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase import SwiftStdlibToolbox @CaseCheckable(.public) diff --git a/Sources/MachOSwiftSection/Models/Metadata/MetatypeMetadata.swift b/Sources/MachOSwiftSection/Models/Metadata/MetatypeMetadata.swift index c0d120c8..2c4dfb72 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/MetatypeMetadata.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/MetatypeMetadata.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct MetatypeMetadata: MetadataProtocol { public struct Layout: MetatypeMetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Metadata/MetatypeMetadataLayout.swift b/Sources/MachOSwiftSection/Models/Metadata/MetatypeMetadataLayout.swift index 40d22ae4..7f9b13b0 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/MetatypeMetadataLayout.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/MetatypeMetadataLayout.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase @Layout public protocol MetatypeMetadataLayout: MetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Metadata/SingletonMetadataPointer.swift b/Sources/MachOSwiftSection/Models/Metadata/SingletonMetadataPointer.swift index bd113f65..e013c0cf 100644 --- a/Sources/MachOSwiftSection/Models/Metadata/SingletonMetadataPointer.swift +++ b/Sources/MachOSwiftSection/Models/Metadata/SingletonMetadataPointer.swift @@ -1,4 +1,4 @@ -import MachOFoundation +import MachOBase public struct SingletonMetadataPointer: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Module/ModuleContext.swift b/Sources/MachOSwiftSection/Models/Module/ModuleContext.swift index 4ebe1065..bc06926b 100644 --- a/Sources/MachOSwiftSection/Models/Module/ModuleContext.swift +++ b/Sources/MachOSwiftSection/Models/Module/ModuleContext.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct ModuleContext: TopLevelType, ContextProtocol { public let descriptor: ModuleContextDescriptor diff --git a/Sources/MachOSwiftSection/Models/Module/ModuleContextDescriptor.swift b/Sources/MachOSwiftSection/Models/Module/ModuleContextDescriptor.swift index c468dee1..b62e4e32 100644 --- a/Sources/MachOSwiftSection/Models/Module/ModuleContextDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/Module/ModuleContextDescriptor.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct ModuleContextDescriptor: ModuleContextDescriptorProtocol { public struct Layout: ModuleContextDescriptorLayout { diff --git a/Sources/MachOSwiftSection/Models/OpaqueType/OpaqueMetadata.swift b/Sources/MachOSwiftSection/Models/OpaqueType/OpaqueMetadata.swift index 052b574c..42b71af7 100644 --- a/Sources/MachOSwiftSection/Models/OpaqueType/OpaqueMetadata.swift +++ b/Sources/MachOSwiftSection/Models/OpaqueType/OpaqueMetadata.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct OpaqueMetadata: MetadataProtocol { public typealias HeaderType = TypeMetadataHeaderBase diff --git a/Sources/MachOSwiftSection/Models/OpaqueType/OpaqueType.swift b/Sources/MachOSwiftSection/Models/OpaqueType/OpaqueType.swift index b8f78371..607ea738 100644 --- a/Sources/MachOSwiftSection/Models/OpaqueType/OpaqueType.swift +++ b/Sources/MachOSwiftSection/Models/OpaqueType/OpaqueType.swift @@ -1,7 +1,6 @@ import Foundation import MachOKit -import MachOFoundation -import Demangling +import MachOBase public struct OpaqueType: TopLevelType, ContextProtocol { public let descriptor: OpaqueTypeDescriptor diff --git a/Sources/MachOSwiftSection/Models/OpaqueType/OpaqueTypeDescriptor.swift b/Sources/MachOSwiftSection/Models/OpaqueType/OpaqueTypeDescriptor.swift index da76e2be..7d2119c0 100644 --- a/Sources/MachOSwiftSection/Models/OpaqueType/OpaqueTypeDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/OpaqueType/OpaqueTypeDescriptor.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct OpaqueTypeDescriptor: OpaqueTypeDescriptorProtocol { public struct Layout: OpaqueTypeDescriptorLayout { diff --git a/Sources/MachOSwiftSection/Models/Protocol/ObjC/ObjCProtocolPrefix.swift b/Sources/MachOSwiftSection/Models/Protocol/ObjC/ObjCProtocolPrefix.swift index a2617909..fbdff2e4 100644 --- a/Sources/MachOSwiftSection/Models/Protocol/ObjC/ObjCProtocolPrefix.swift +++ b/Sources/MachOSwiftSection/Models/Protocol/ObjC/ObjCProtocolPrefix.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct ObjCProtocolPrefix: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Protocol/ObjC/RelativeObjCProtocolPrefix.swift b/Sources/MachOSwiftSection/Models/Protocol/ObjC/RelativeObjCProtocolPrefix.swift index 2ab0704e..300e02c7 100644 --- a/Sources/MachOSwiftSection/Models/Protocol/ObjC/RelativeObjCProtocolPrefix.swift +++ b/Sources/MachOSwiftSection/Models/Protocol/ObjC/RelativeObjCProtocolPrefix.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct RelativeObjCProtocolPrefix: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Protocol/Protocol.swift b/Sources/MachOSwiftSection/Models/Protocol/Protocol.swift index 2aeca908..ba5a3403 100644 --- a/Sources/MachOSwiftSection/Models/Protocol/Protocol.swift +++ b/Sources/MachOSwiftSection/Models/Protocol/Protocol.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase // using TrailingObjects // = swift::ABI::TrailingObjects< diff --git a/Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptor.swift b/Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptor.swift index c619c275..1a4b9866 100644 --- a/Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptor.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase /// A protocol descriptor. /// diff --git a/Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptorLayout.swift b/Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptorLayout.swift index c4feea35..31a44ee4 100644 --- a/Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptorLayout.swift +++ b/Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptorLayout.swift @@ -1,4 +1,4 @@ -import MachOFoundation +import MachOBase public protocol ProtocolDescriptorLayout: NamedContextDescriptorLayout { var numRequirementsInSignature: UInt32 { get } diff --git a/Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptorRef.swift b/Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptorRef.swift index 20420a78..02fc9a4a 100644 --- a/Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptorRef.swift +++ b/Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptorRef.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct ProtocolDescriptorRef { public let storage: StoredPointer diff --git a/Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptorWithObjCInterop.swift b/Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptorWithObjCInterop.swift index dd4bb86a..8bb1b463 100644 --- a/Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptorWithObjCInterop.swift +++ b/Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptorWithObjCInterop.swift @@ -1,6 +1,6 @@ import Foundation import FoundationToolbox -import MachOFoundation +import MachOBase @AssociatedValue(.public) @CaseCheckable(.public) diff --git a/Sources/MachOSwiftSection/Models/Protocol/ProtocolRecord.swift b/Sources/MachOSwiftSection/Models/Protocol/ProtocolRecord.swift index 418f43e9..82610251 100644 --- a/Sources/MachOSwiftSection/Models/Protocol/ProtocolRecord.swift +++ b/Sources/MachOSwiftSection/Models/Protocol/ProtocolRecord.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase /// Mirrors `TargetProtocolRecord` from /// `swift/include/swift/ABI/Metadata.h:2766`. One entry per 4-byte slot of diff --git a/Sources/MachOSwiftSection/Models/Protocol/ProtocolRequirement.swift b/Sources/MachOSwiftSection/Models/Protocol/ProtocolRequirement.swift index 8b0044ac..c2437597 100644 --- a/Sources/MachOSwiftSection/Models/Protocol/ProtocolRequirement.swift +++ b/Sources/MachOSwiftSection/Models/Protocol/ProtocolRequirement.swift @@ -1,10 +1,10 @@ import MachOKit -import MachOFoundation +import MachOBase public struct ProtocolRequirement: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { public let flags: ProtocolRequirementFlags - public let defaultImplementation: RelativeDirectPointer + public let defaultImplementation: RelativeDirectRawPointer } public let offset: Int @@ -18,9 +18,13 @@ public struct ProtocolRequirement: ResolvableLocatableLayoutWrapper { } extension ProtocolRequirement { - public func defaultImplementationSymbols(in machO: MachO) throws -> Symbols? { + /// File offset of the requirement's default implementation, or `nil` + /// when the requirement has none. Pure pointer arithmetic on the + /// descriptor's own offset; symbol attribution is `SwiftInspection`'s + /// `defaultImplementationSymbols(in:)`, one layer up. + public var defaultImplementationOffset: Int? { guard layout.defaultImplementation.isValid else { return nil } - return try layout.defaultImplementation.resolve(from: offset(of: \.defaultImplementation), in: machO) + return layout.defaultImplementation.resolveDirectOffset(from: offset(of: \.defaultImplementation)) } } @@ -40,8 +44,10 @@ public struct ProtocolBaseRequirement: ResolvableLocatableLayoutWrapper { // MARK: - ReadingContext Support extension ProtocolRequirement { - public func defaultImplementationSymbols(in context: Context) throws -> Symbols? { - guard layout.defaultImplementation.isValid else { return nil } - return try layout.defaultImplementation.resolve(at: try context.addressFromOffset(offset(of: \.defaultImplementation)), in: context) + /// The default implementation's location as an address in `context`, or + /// `nil` when the requirement has none. + public func defaultImplementationAddress(in context: Context) throws -> Context.Address? { + guard let defaultImplementationOffset else { return nil } + return try context.addressFromOffset(defaultImplementationOffset) } } diff --git a/Sources/MachOSwiftSection/Models/Protocol/ProtocolWitnessTable.swift b/Sources/MachOSwiftSection/Models/Protocol/ProtocolWitnessTable.swift index 3cda632e..1bc4714e 100644 --- a/Sources/MachOSwiftSection/Models/Protocol/ProtocolWitnessTable.swift +++ b/Sources/MachOSwiftSection/Models/Protocol/ProtocolWitnessTable.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct ProtocolWitnessTable: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Protocol/ResilientWitness.swift b/Sources/MachOSwiftSection/Models/Protocol/ResilientWitness.swift index 33309a48..2ccb61a6 100644 --- a/Sources/MachOSwiftSection/Models/Protocol/ResilientWitness.swift +++ b/Sources/MachOSwiftSection/Models/Protocol/ResilientWitness.swift @@ -1,11 +1,11 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct ResilientWitness: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { public let requirement: RelativeProtocolRequirementPointer - public let implementation: RelativeDirectPointer + public let implementation: RelativeDirectRawPointer } public let offset: Int @@ -27,19 +27,23 @@ extension ResilientWitness { return try layout.requirement.resolve(from: pointer(of: \.requirement)).asOptional } - public var implementationOffset: Int { - layout.implementation.resolveDirectOffset(from: offset(of: \.implementation)) + /// File offset of the witness implementation, or `nil` for a null + /// pointer. Pure pointer arithmetic on the descriptor's own offset; + /// symbol attribution is `SwiftInspection`'s `implementationSymbols(in:)`, + /// one layer up. + public var implementationOffset: Int? { + guard layout.implementation.isValid else { return nil } + return layout.implementation.resolveDirectOffset(from: offset(of: \.implementation)) } - - /// MachO-only debug formatter; no `ReadingContext` mirror exists because - /// `addressString(forOffset:)` is a MachO display helper (not a data read) - /// and has no counterpart on the unified `ReadingContext` abstraction. - public func implementationAddress(in machO: some MachOSwiftSectionRepresentableWithCache) -> String { - return machO.addressString(forOffset: implementationOffset) - } - - public func implementationSymbols(in machO: MachO) throws -> Symbols? { - return try layout.implementation.resolve(from: offset(of: \.implementation), in: machO) + + /// MachO-only debug formatter (`nil` for a null pointer); no + /// `ReadingContext` mirror exists because `addressString(forOffset:)` is a + /// MachO display helper (not a data read) and has no counterpart on the + /// unified `ReadingContext` abstraction — the context-flavored + /// ``implementationAddress(in:)-swift.method`` below returns the typed + /// address instead. + public func implementationAddress(in machO: some MachOSwiftSectionRepresentableWithCache) -> String? { + return implementationOffset.map { machO.addressString(forOffset: $0) } } } @@ -50,7 +54,10 @@ extension ResilientWitness { return try layout.requirement.resolve(at: try context.addressFromOffset(offset(of: \.requirement)), in: context).asOptional } - public func implementationSymbols(in context: Context) throws -> Symbols? { - return try layout.implementation.resolve(at: try context.addressFromOffset(offset(of: \.implementation)), in: context) + /// The witness implementation's location as an address in `context`, or + /// `nil` for a null pointer. + public func implementationAddress(in context: Context) throws -> Context.Address? { + guard let implementationOffset else { return nil } + return try context.addressFromOffset(implementationOffset) } } diff --git a/Sources/MachOSwiftSection/Models/Protocol/ResilientWitnessesHeader.swift b/Sources/MachOSwiftSection/Models/Protocol/ResilientWitnessesHeader.swift index 0add70db..ab4f3632 100644 --- a/Sources/MachOSwiftSection/Models/Protocol/ResilientWitnessesHeader.swift +++ b/Sources/MachOSwiftSection/Models/Protocol/ResilientWitnessesHeader.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct ResilientWitnessesHeader: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/ProtocolConformance/GlobalActorReference.swift b/Sources/MachOSwiftSection/Models/ProtocolConformance/GlobalActorReference.swift index ea7698d3..af869d7a 100644 --- a/Sources/MachOSwiftSection/Models/ProtocolConformance/GlobalActorReference.swift +++ b/Sources/MachOSwiftSection/Models/ProtocolConformance/GlobalActorReference.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase /// Trailing object of `TargetProtocolConformanceDescriptor` carrying the global /// actor that isolates a conformance (e.g. `extension X: @MainActor P`). diff --git a/Sources/MachOSwiftSection/Models/ProtocolConformance/ProtocolConformance.swift b/Sources/MachOSwiftSection/Models/ProtocolConformance/ProtocolConformance.swift index 0958cef2..02b564a0 100644 --- a/Sources/MachOSwiftSection/Models/ProtocolConformance/ProtocolConformance.swift +++ b/Sources/MachOSwiftSection/Models/ProtocolConformance/ProtocolConformance.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase // using TrailingObjects = swift::ABI::TrailingObjects< // TargetProtocolConformanceDescriptor, diff --git a/Sources/MachOSwiftSection/Models/ProtocolConformance/ProtocolConformanceDescriptor.swift b/Sources/MachOSwiftSection/Models/ProtocolConformance/ProtocolConformanceDescriptor.swift index bef6fe7b..9af363f4 100644 --- a/Sources/MachOSwiftSection/Models/ProtocolConformance/ProtocolConformanceDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/ProtocolConformance/ProtocolConformanceDescriptor.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct ProtocolConformanceDescriptor: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/TupleType/TupleTypeMetadata.swift b/Sources/MachOSwiftSection/Models/TupleType/TupleTypeMetadata.swift index 1c8bd964..018c2fae 100644 --- a/Sources/MachOSwiftSection/Models/TupleType/TupleTypeMetadata.swift +++ b/Sources/MachOSwiftSection/Models/TupleType/TupleTypeMetadata.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct TupleTypeMetadata: MetadataProtocol { public typealias HeaderType = TypeMetadataHeaderBase diff --git a/Sources/MachOSwiftSection/Models/TupleType/TupleTypeMetadataElementLayout.swift b/Sources/MachOSwiftSection/Models/TupleType/TupleTypeMetadataElementLayout.swift index b7ba72c4..e2d8e175 100644 --- a/Sources/MachOSwiftSection/Models/TupleType/TupleTypeMetadataElementLayout.swift +++ b/Sources/MachOSwiftSection/Models/TupleType/TupleTypeMetadataElementLayout.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase @Layout public protocol TupleTypeMetadataElementLayout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/TupleType/TupleTypeMetadataLayout.swift b/Sources/MachOSwiftSection/Models/TupleType/TupleTypeMetadataLayout.swift index b2118bd8..872c7749 100644 --- a/Sources/MachOSwiftSection/Models/TupleType/TupleTypeMetadataLayout.swift +++ b/Sources/MachOSwiftSection/Models/TupleType/TupleTypeMetadataLayout.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase @Layout public protocol TupleTypeMetadataLayout: MetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Class.swift b/Sources/MachOSwiftSection/Models/Type/Class/Class.swift index 77ebe8d2..b5d81652 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Class.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Class.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase // template // class swift_ptrauth_struct_context_descriptor(ClassDescriptor) diff --git a/Sources/MachOSwiftSection/Models/Type/Class/ClassDescriptor.swift b/Sources/MachOSwiftSection/Models/Type/Class/ClassDescriptor.swift index 5abd5c0d..3711159a 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/ClassDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/ClassDescriptor.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct ClassDescriptor: TypeContextDescriptorProtocol { public struct Layout: ClassDescriptorLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/Class/ClassDescriptorLayout.swift b/Sources/MachOSwiftSection/Models/Type/Class/ClassDescriptorLayout.swift index a5548172..52f46b91 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/ClassDescriptorLayout.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/ClassDescriptorLayout.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase @Layout public protocol ClassDescriptorLayout: TypeContextDescriptorLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadata/AnyClassMetadata.swift b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadata/AnyClassMetadata.swift index 9de60a29..e3a027e2 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadata/AnyClassMetadata.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadata/AnyClassMetadata.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct AnyClassMetadata: AnyClassMetadataProtocol { public struct Layout: AnyClassMetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadata/AnyClassMetadataLayout.swift b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadata/AnyClassMetadataLayout.swift index 89e129ee..076ac7a7 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadata/AnyClassMetadataLayout.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadata/AnyClassMetadataLayout.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase @Layout public protocol AnyClassMetadataLayout: HeapMetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadata/AnyClassMetadataProtocol.swift b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadata/AnyClassMetadataProtocol.swift index edd0581c..ff5434d8 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadata/AnyClassMetadataProtocol.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadata/AnyClassMetadataProtocol.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public protocol AnyClassMetadataProtocol: HeapMetadataProtocol where Layout: AnyClassMetadataLayout {} diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadataObjCInterop/AnyClassMetadataObjCInterop.swift b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadataObjCInterop/AnyClassMetadataObjCInterop.swift index 98951d9e..99f2f876 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadataObjCInterop/AnyClassMetadataObjCInterop.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadataObjCInterop/AnyClassMetadataObjCInterop.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct AnyClassMetadataObjCInterop: AnyClassMetadataObjCInteropProtocol { public struct Layout: AnyClassMetadataObjCInteropLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadataObjCInterop/AnyClassMetadataObjCInteropLayout.swift b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadataObjCInterop/AnyClassMetadataObjCInteropLayout.swift index 31546728..78cd8785 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadataObjCInterop/AnyClassMetadataObjCInteropLayout.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadataObjCInterop/AnyClassMetadataObjCInteropLayout.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase @Layout public protocol AnyClassMetadataObjCInteropLayout: HeapMetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadataObjCInterop/AnyClassMetadataObjCInteropProtocol.swift b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadataObjCInterop/AnyClassMetadataObjCInteropProtocol.swift index d920fe95..7166d390 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadataObjCInterop/AnyClassMetadataObjCInteropProtocol.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadataObjCInterop/AnyClassMetadataObjCInteropProtocol.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public protocol AnyClassMetadataObjCInteropProtocol: HeapMetadataProtocol where Layout: AnyClassMetadataObjCInteropLayout {} diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/Bounds/StoredClassMetadataBounds.swift b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/Bounds/StoredClassMetadataBounds.swift index ca45d10a..c4c909b5 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/Bounds/StoredClassMetadataBounds.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/Bounds/StoredClassMetadataBounds.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct StoredClassMetadataBounds: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadata/ClassMetadata.swift b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadata/ClassMetadata.swift index 288c978c..a660b373 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadata/ClassMetadata.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadata/ClassMetadata.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase public struct ClassMetadata: ClassMetadataProtocol { public struct Layout: ClassMetadataLayout, FinalClassMetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadata/ClassMetadataLayout.swift b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadata/ClassMetadataLayout.swift index ccbbbcf0..590b1acb 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadata/ClassMetadataLayout.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadata/ClassMetadataLayout.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase @Layout public protocol ClassMetadataLayout: AnyClassMetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadata/ClassMetadataProtocol.swift b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadata/ClassMetadataProtocol.swift index cc4ac440..e849d7fe 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadata/ClassMetadataProtocol.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadata/ClassMetadataProtocol.swift @@ -1,4 +1,4 @@ import MachOKit -import MachOFoundation +import MachOBase public protocol ClassMetadataProtocol: AnyClassMetadataProtocol, FinalClassMetadataProtocol {} diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadataObjCInterop/ClassMetadataObjCInterop.swift b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadataObjCInterop/ClassMetadataObjCInterop.swift index 8af769d6..665a0e61 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadataObjCInterop/ClassMetadataObjCInterop.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadataObjCInterop/ClassMetadataObjCInterop.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase public struct ClassMetadataObjCInterop: ClassMetadataObjCInteropProtocol { public struct Layout: ClassMetadataObjCInteropLayout, FinalClassMetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadataObjCInterop/ClassMetadataObjCInteropLayout.swift b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadataObjCInterop/ClassMetadataObjCInteropLayout.swift index 2dff3aa3..a4cd7cda 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadataObjCInterop/ClassMetadataObjCInteropLayout.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadataObjCInterop/ClassMetadataObjCInteropLayout.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase @Layout public protocol ClassMetadataObjCInteropLayout: AnyClassMetadataObjCInteropLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadataObjCInterop/ClassMetadataObjCInteropProtocol.swift b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadataObjCInterop/ClassMetadataObjCInteropProtocol.swift index fa02b100..a50f884e 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadataObjCInterop/ClassMetadataObjCInteropProtocol.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ClassMetadataObjCInterop/ClassMetadataObjCInteropProtocol.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase public protocol ClassMetadataObjCInteropProtocol: AnyClassMetadataObjCInteropProtocol, FinalClassMetadataProtocol where Layout: ClassMetadataObjCInteropLayout {} diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/FinalClassMetadataLayout.swift b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/FinalClassMetadataLayout.swift index f2255555..639e24bd 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/FinalClassMetadataLayout.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/FinalClassMetadataLayout.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public protocol FinalClassMetadataLayout { var descriptor: Pointer { get } diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/FinalClassMetadataProtocol.swift b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/FinalClassMetadataProtocol.swift index f997b3e8..829726f8 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/FinalClassMetadataProtocol.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/FinalClassMetadataProtocol.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public protocol FinalClassMetadataProtocol: HeapMetadataProtocol {} diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ObjCClassWrapperMetadata.swift b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ObjCClassWrapperMetadata.swift index 0eb8f383..2801985b 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ObjCClassWrapperMetadata.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Metadata/ObjCClassWrapperMetadata.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct ObjCClassWrapperMetadata: MetadataProtocol { public struct Layout: MetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodDefaultOverrideDescriptor.swift b/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodDefaultOverrideDescriptor.swift index 99379ce5..ae9dee78 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodDefaultOverrideDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodDefaultOverrideDescriptor.swift @@ -1,12 +1,12 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct MethodDefaultOverrideDescriptor: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { public let replacement: RelativeMethodDescriptorPointer public let original: RelativeMethodDescriptorPointer - public let implementation: RelativeDirectPointer + public let implementation: RelativeDirectRawPointer } public var layout: Layout @@ -28,8 +28,11 @@ extension MethodDefaultOverrideDescriptor { return try layout.replacement.resolve(from: offset(of: \.replacement), in: machO).asOptional } - public func implementationSymbols(in machO: MachO) throws -> Symbols? { - return try layout.implementation.resolve(from: offset(of: \.implementation), in: machO) + /// File offset of the default-override implementation, or `nil` for a + /// null pointer. See `MethodDescriptor.implementationOffset`. + public var implementationOffset: Int? { + guard layout.implementation.isValid else { return nil } + return layout.implementation.resolveDirectOffset(from: offset(of: \.implementation)) } } @@ -54,7 +57,10 @@ extension MethodDefaultOverrideDescriptor { return try layout.replacement.resolve(at: try context.addressFromOffset(offset(of: \.replacement)), in: context).asOptional } - public func implementationSymbols(in context: Context) throws -> Symbols? { - return try layout.implementation.resolve(at: try context.addressFromOffset(offset(of: \.implementation)), in: context) + /// The default-override implementation's location as an address in + /// `context`, or `nil` for a null pointer. + public func implementationAddress(in context: Context) throws -> Context.Address? { + guard let implementationOffset else { return nil } + return try context.addressFromOffset(implementationOffset) } } diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodDefaultOverrideTableHeader.swift b/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodDefaultOverrideTableHeader.swift index 3623a2d2..7472d22e 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodDefaultOverrideTableHeader.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodDefaultOverrideTableHeader.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct MethodDefaultOverrideTableHeader: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodDescriptor.swift b/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodDescriptor.swift index d8c4963b..951b13bb 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodDescriptor.swift @@ -1,11 +1,11 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct MethodDescriptor: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { public let flags: MethodDescriptorFlags - public let implementation: RelativeDirectPointer + public let implementation: RelativeDirectRawPointer } public var layout: Layout @@ -19,15 +19,25 @@ public struct MethodDescriptor: ResolvableLocatableLayoutWrapper { } extension MethodDescriptor { - public func implementationSymbols(in machO: MachO) throws -> Symbols? { - return try layout.implementation.resolve(from: offset(of: \.implementation), in: machO) + /// File offset of the method's implementation, or `nil` when the pointer + /// is null (an abstract method, or one whose implementation is not in + /// this image). Pure pointer arithmetic on the descriptor's own offset — + /// no reader involved. Attributing symbol names to that offset is + /// `SwiftInspection`'s `implementationSymbols(in:)`, one layer up. + public var implementationOffset: Int? { + guard layout.implementation.isValid else { return nil } + return layout.implementation.resolveDirectOffset(from: offset(of: \.implementation)) } } // MARK: - ReadingContext Support extension MethodDescriptor { - public func implementationSymbols(in context: Context) throws -> Symbols? { - return try layout.implementation.resolve(at: try context.addressFromOffset(offset(of: \.implementation)), in: context) + /// The implementation's location as an address in `context` (a file + /// offset for `MachOContext`, a pointer in-process), or `nil` for a null + /// pointer. + public func implementationAddress(in context: Context) throws -> Context.Address? { + guard let implementationOffset else { return nil } + return try context.addressFromOffset(implementationOffset) } } diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodImplementationPointer.swift b/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodImplementationPointer.swift index c161bdba..17464f90 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodImplementationPointer.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodImplementationPointer.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public enum MethodImplementationPointer { case implementation(RelativeDirectRawPointer) diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodOverrideDescriptor.swift b/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodOverrideDescriptor.swift index be4847ed..30b55e0f 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodOverrideDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Method/MethodOverrideDescriptor.swift @@ -1,12 +1,12 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct MethodOverrideDescriptor: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { public let `class`: RelativeContextPointer public let method: RelativeMethodDescriptorPointer - public let implementation: RelativeDirectPointer + public let implementation: RelativeDirectRawPointer } public var layout: Layout @@ -32,8 +32,11 @@ extension MethodOverrideDescriptor { return try layout.method.resolve(from: pointer(of: \.method)).asOptional } - public func implementationSymbols(in machO: MachO) throws -> Symbols? { - return try layout.implementation.resolve(from: offset(of: \.implementation), in: machO) + /// File offset of the overriding implementation, or `nil` for a null + /// pointer. See `MethodDescriptor.implementationOffset`. + public var implementationOffset: Int? { + guard layout.implementation.isValid else { return nil } + return layout.implementation.resolveDirectOffset(from: offset(of: \.implementation)) } } @@ -48,7 +51,10 @@ extension MethodOverrideDescriptor { return try layout.method.resolve(at: try context.addressFromOffset(offset(of: \.method)), in: context).asOptional } - public func implementationSymbols(in context: Context) throws -> Symbols? { - return try layout.implementation.resolve(at: try context.addressFromOffset(offset(of: \.implementation)), in: context) + /// The overriding implementation's location as an address in `context`, + /// or `nil` for a null pointer. + public func implementationAddress(in context: Context) throws -> Context.Address? { + guard let implementationOffset else { return nil } + return try context.addressFromOffset(implementationOffset) } } diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Method/OverrideTableHeader.swift b/Sources/MachOSwiftSection/Models/Type/Class/Method/OverrideTableHeader.swift index 799a038d..4d9b6612 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Method/OverrideTableHeader.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Method/OverrideTableHeader.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct OverrideTableHeader: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Method/VTableDescriptorHeader.swift b/Sources/MachOSwiftSection/Models/Type/Class/Method/VTableDescriptorHeader.swift index 6b29178f..90e3bce4 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Method/VTableDescriptorHeader.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Method/VTableDescriptorHeader.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct VTableDescriptorHeader: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Resilient/ObjCResilientClassStubInfo.swift b/Sources/MachOSwiftSection/Models/Type/Class/Resilient/ObjCResilientClassStubInfo.swift index 3d8a6716..5d811487 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Resilient/ObjCResilientClassStubInfo.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Resilient/ObjCResilientClassStubInfo.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct ObjCResilientClassStubInfo: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Type/Class/Resilient/ResilientSuperclass.swift b/Sources/MachOSwiftSection/Models/Type/Class/Resilient/ResilientSuperclass.swift index 5c037081..06bfdccc 100644 --- a/Sources/MachOSwiftSection/Models/Type/Class/Resilient/ResilientSuperclass.swift +++ b/Sources/MachOSwiftSection/Models/Type/Class/Resilient/ResilientSuperclass.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct ResilientSuperclass: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Type/Enum/Enum.swift b/Sources/MachOSwiftSection/Models/Type/Enum/Enum.swift index 2b2fa6d7..2bb2ee6f 100644 --- a/Sources/MachOSwiftSection/Models/Type/Enum/Enum.swift +++ b/Sources/MachOSwiftSection/Models/Type/Enum/Enum.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase // template // class swift_ptrauth_struct_context_descriptor(EnumDescriptor) diff --git a/Sources/MachOSwiftSection/Models/Type/Enum/EnumDescriptor.swift b/Sources/MachOSwiftSection/Models/Type/Enum/EnumDescriptor.swift index bf1b1da9..809e7284 100644 --- a/Sources/MachOSwiftSection/Models/Type/Enum/EnumDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/Type/Enum/EnumDescriptor.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase import SwiftStdlibToolbox public struct EnumDescriptor: TypeContextDescriptorProtocol { diff --git a/Sources/MachOSwiftSection/Models/Type/Enum/Metadata/EnumMetadata.swift b/Sources/MachOSwiftSection/Models/Type/Enum/Metadata/EnumMetadata.swift index 96be99bf..aad3a8d3 100644 --- a/Sources/MachOSwiftSection/Models/Type/Enum/Metadata/EnumMetadata.swift +++ b/Sources/MachOSwiftSection/Models/Type/Enum/Metadata/EnumMetadata.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct EnumMetadata: EnumMetadataProtocol { public struct Layout: EnumMetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/Enum/Metadata/EnumMetadataLayout.swift b/Sources/MachOSwiftSection/Models/Type/Enum/Metadata/EnumMetadataLayout.swift index 24aaef3f..a42ce8c3 100644 --- a/Sources/MachOSwiftSection/Models/Type/Enum/Metadata/EnumMetadataLayout.swift +++ b/Sources/MachOSwiftSection/Models/Type/Enum/Metadata/EnumMetadataLayout.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase @Layout public protocol EnumMetadataLayout: ValueMetadataLayout {} diff --git a/Sources/MachOSwiftSection/Models/Type/Enum/Metadata/EnumMetadataProtocol.swift b/Sources/MachOSwiftSection/Models/Type/Enum/Metadata/EnumMetadataProtocol.swift index c19fd96d..192b1189 100644 --- a/Sources/MachOSwiftSection/Models/Type/Enum/Metadata/EnumMetadataProtocol.swift +++ b/Sources/MachOSwiftSection/Models/Type/Enum/Metadata/EnumMetadataProtocol.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase public protocol EnumMetadataProtocol: ValueMetadataProtocol where Layout: EnumMetadataLayout {} diff --git a/Sources/MachOSwiftSection/Models/Type/Enum/MultiPayloadEnumDescriptor.swift b/Sources/MachOSwiftSection/Models/Type/Enum/MultiPayloadEnumDescriptor.swift index 5d772762..83e4dba6 100644 --- a/Sources/MachOSwiftSection/Models/Type/Enum/MultiPayloadEnumDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/Type/Enum/MultiPayloadEnumDescriptor.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase public struct MultiPayloadEnumDescriptor: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Models/Type/Struct/Struct.swift b/Sources/MachOSwiftSection/Models/Type/Struct/Struct.swift index 3f957cf8..64132680 100644 --- a/Sources/MachOSwiftSection/Models/Type/Struct/Struct.swift +++ b/Sources/MachOSwiftSection/Models/Type/Struct/Struct.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct Struct: TopLevelType, ContextProtocol { public let descriptor: StructDescriptor diff --git a/Sources/MachOSwiftSection/Models/Type/Struct/StructDescriptor.swift b/Sources/MachOSwiftSection/Models/Type/Struct/StructDescriptor.swift index c5abf2a5..f14efab5 100644 --- a/Sources/MachOSwiftSection/Models/Type/Struct/StructDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/Type/Struct/StructDescriptor.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct StructDescriptor: TypeContextDescriptorProtocol { public struct Layout: StructDescriptorLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/Struct/StructMetadata.swift b/Sources/MachOSwiftSection/Models/Type/Struct/StructMetadata.swift index eb60f69c..1153999e 100644 --- a/Sources/MachOSwiftSection/Models/Type/Struct/StructMetadata.swift +++ b/Sources/MachOSwiftSection/Models/Type/Struct/StructMetadata.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase public struct StructMetadata: StructMetadataProtocol { public struct Layout: StructMetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/Struct/StructMetadataLayout.swift b/Sources/MachOSwiftSection/Models/Type/Struct/StructMetadataLayout.swift index ac7dc146..d189f61d 100644 --- a/Sources/MachOSwiftSection/Models/Type/Struct/StructMetadataLayout.swift +++ b/Sources/MachOSwiftSection/Models/Type/Struct/StructMetadataLayout.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase @Layout public protocol StructMetadataLayout: ValueMetadataLayout {} diff --git a/Sources/MachOSwiftSection/Models/Type/Struct/StructMetadataProtocol.swift b/Sources/MachOSwiftSection/Models/Type/Struct/StructMetadataProtocol.swift index 96502841..f6435956 100644 --- a/Sources/MachOSwiftSection/Models/Type/Struct/StructMetadataProtocol.swift +++ b/Sources/MachOSwiftSection/Models/Type/Struct/StructMetadataProtocol.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase public protocol StructMetadataProtocol: ValueMetadataProtocol where Layout: StructMetadataLayout {} diff --git a/Sources/MachOSwiftSection/Models/Type/TypeContextDescriptor.swift b/Sources/MachOSwiftSection/Models/Type/TypeContextDescriptor.swift index b9af1f10..113ab20d 100644 --- a/Sources/MachOSwiftSection/Models/Type/TypeContextDescriptor.swift +++ b/Sources/MachOSwiftSection/Models/Type/TypeContextDescriptor.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public struct TypeContextDescriptor: TypeContextDescriptorProtocol { public struct Layout: TypeContextDescriptorLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/TypeContextDescriptorLayout.swift b/Sources/MachOSwiftSection/Models/Type/TypeContextDescriptorLayout.swift index c203126b..d78b4011 100644 --- a/Sources/MachOSwiftSection/Models/Type/TypeContextDescriptorLayout.swift +++ b/Sources/MachOSwiftSection/Models/Type/TypeContextDescriptorLayout.swift @@ -1,5 +1,5 @@ -import MachOFoundation +import MachOBase @Layout public protocol TypeContextDescriptorLayout: NamedContextDescriptorLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/TypeContextDescriptorProtocol.swift b/Sources/MachOSwiftSection/Models/Type/TypeContextDescriptorProtocol.swift index 0a425d40..f1b108c9 100644 --- a/Sources/MachOSwiftSection/Models/Type/TypeContextDescriptorProtocol.swift +++ b/Sources/MachOSwiftSection/Models/Type/TypeContextDescriptorProtocol.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase public protocol TypeContextDescriptorProtocol: NamedContextDescriptorProtocol where Layout: TypeContextDescriptorLayout {} diff --git a/Sources/MachOSwiftSection/Models/Type/TypeContextDescriptorWrapper.swift b/Sources/MachOSwiftSection/Models/Type/TypeContextDescriptorWrapper.swift index 8b8e5f20..00b080a5 100644 --- a/Sources/MachOSwiftSection/Models/Type/TypeContextDescriptorWrapper.swift +++ b/Sources/MachOSwiftSection/Models/Type/TypeContextDescriptorWrapper.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase import SwiftStdlibToolbox @CaseCheckable(.public) diff --git a/Sources/MachOSwiftSection/Models/Type/TypeContextWrapper.swift b/Sources/MachOSwiftSection/Models/Type/TypeContextWrapper.swift index 55cd0f61..35468ada 100644 --- a/Sources/MachOSwiftSection/Models/Type/TypeContextWrapper.swift +++ b/Sources/MachOSwiftSection/Models/Type/TypeContextWrapper.swift @@ -1,6 +1,5 @@ import Foundation import MachOKit -import MachOSymbols import SwiftStdlibToolbox @AssociatedValue(.public) diff --git a/Sources/MachOSwiftSection/Models/Type/TypeMetadataRecord.swift b/Sources/MachOSwiftSection/Models/Type/TypeMetadataRecord.swift index e3b78333..3ba36f14 100644 --- a/Sources/MachOSwiftSection/Models/Type/TypeMetadataRecord.swift +++ b/Sources/MachOSwiftSection/Models/Type/TypeMetadataRecord.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase /// Mirrors `TargetTypeMetadataRecord` from /// `swift/include/swift/ABI/Metadata.h:2720`. One entry per 4-byte slot of diff --git a/Sources/MachOSwiftSection/Models/Type/TypeReference.swift b/Sources/MachOSwiftSection/Models/Type/TypeReference.swift index 4aae2f67..91d87de2 100644 --- a/Sources/MachOSwiftSection/Models/Type/TypeReference.swift +++ b/Sources/MachOSwiftSection/Models/Type/TypeReference.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase public enum TypeReference: Sendable { case directTypeDescriptor(RelativeDirectPointer) diff --git a/Sources/MachOSwiftSection/Models/Type/ValueMetadata.swift b/Sources/MachOSwiftSection/Models/Type/ValueMetadata.swift index 5e2a2aa4..81ec8992 100644 --- a/Sources/MachOSwiftSection/Models/Type/ValueMetadata.swift +++ b/Sources/MachOSwiftSection/Models/Type/ValueMetadata.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase public struct ValueMetadata: ValueMetadataProtocol { public struct Layout: StructMetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/Type/ValueMetadataLayout.swift b/Sources/MachOSwiftSection/Models/Type/ValueMetadataLayout.swift index 178498ce..108b7f93 100644 --- a/Sources/MachOSwiftSection/Models/Type/ValueMetadataLayout.swift +++ b/Sources/MachOSwiftSection/Models/Type/ValueMetadataLayout.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase @Layout public protocol ValueMetadataLayout: MetadataLayout { diff --git a/Sources/MachOSwiftSection/Models/ValueWitnessTable/TypeLayout.swift b/Sources/MachOSwiftSection/Models/ValueWitnessTable/TypeLayout.swift index a8d4b1b6..6df1a14c 100644 --- a/Sources/MachOSwiftSection/Models/ValueWitnessTable/TypeLayout.swift +++ b/Sources/MachOSwiftSection/Models/ValueWitnessTable/TypeLayout.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase @dynamicMemberLookup public struct TypeLayout { diff --git a/Sources/MachOSwiftSection/Models/ValueWitnessTable/ValueWitnessTable.swift b/Sources/MachOSwiftSection/Models/ValueWitnessTable/ValueWitnessTable.swift index e13979a7..c14f38bc 100644 --- a/Sources/MachOSwiftSection/Models/ValueWitnessTable/ValueWitnessTable.swift +++ b/Sources/MachOSwiftSection/Models/ValueWitnessTable/ValueWitnessTable.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct ValueWitnessTable: ResolvableLocatableLayoutWrapper { public struct Layout: LayoutProtocol { diff --git a/Sources/MachOSwiftSection/Pointer/ContextPointer.swift b/Sources/MachOSwiftSection/Pointer/ContextPointer.swift index 24e7645e..473f7237 100644 --- a/Sources/MachOSwiftSection/Pointer/ContextPointer.swift +++ b/Sources/MachOSwiftSection/Pointer/ContextPointer.swift @@ -1,4 +1,4 @@ import MachOKit -import MachOFoundation +import MachOBase public typealias ContextPointer = SymbolOrElementPointer diff --git a/Sources/MachOSwiftSection/Pointer/RelativePointers.swift b/Sources/MachOSwiftSection/Pointer/RelativePointers.swift index 36c1c548..a6946c31 100644 --- a/Sources/MachOSwiftSection/Pointer/RelativePointers.swift +++ b/Sources/MachOSwiftSection/Pointer/RelativePointers.swift @@ -1,4 +1,4 @@ -import MachOFoundation +import MachOBase public typealias RelativeMethodDescriptorPointer = RelativeSymbolOrElementPointer diff --git a/Sources/MachOSwiftSection/Pointer/RelativeProtocolDescriptorPointer.swift b/Sources/MachOSwiftSection/Pointer/RelativeProtocolDescriptorPointer.swift index 6b96ef20..17a3b920 100644 --- a/Sources/MachOSwiftSection/Pointer/RelativeProtocolDescriptorPointer.swift +++ b/Sources/MachOSwiftSection/Pointer/RelativeProtocolDescriptorPointer.swift @@ -1,5 +1,5 @@ import MachOKit -import MachOFoundation +import MachOBase public enum RelativeProtocolDescriptorPointer: Sendable, Equatable { case objcPointer(RelativeSymbolOrElementPointerIntPair) diff --git a/Sources/MachOSwiftSection/Runtime/RuntimeFunctions.swift b/Sources/MachOSwiftSection/Runtime/RuntimeFunctions.swift index 9b056251..7d256fe5 100644 --- a/Sources/MachOSwiftSection/Runtime/RuntimeFunctions.swift +++ b/Sources/MachOSwiftSection/Runtime/RuntimeFunctions.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase import FoundationToolbox import MachOSwiftSectionC diff --git a/Sources/MachOSwiftSection/Utils/AnyLocatableLayoutWrapper.swift b/Sources/MachOSwiftSection/Utils/AnyLocatableLayoutWrapper.swift index ee856d13..ccfd9617 100644 --- a/Sources/MachOSwiftSection/Utils/AnyLocatableLayoutWrapper.swift +++ b/Sources/MachOSwiftSection/Utils/AnyLocatableLayoutWrapper.swift @@ -1,5 +1,5 @@ import Foundation -import MachOFoundation +import MachOBase public struct AnyLocatableLayoutWrapper: ResolvableLocatableLayoutWrapper { public var layout: Layout diff --git a/Sources/MachOSwiftSection/Utils/MachOSwiftSectionError.swift b/Sources/MachOSwiftSection/Utils/MachOSwiftSectionError.swift index b9f96736..d85cbd4d 100644 --- a/Sources/MachOSwiftSection/Utils/MachOSwiftSectionError.swift +++ b/Sources/MachOSwiftSection/Utils/MachOSwiftSectionError.swift @@ -1,6 +1,6 @@ import Foundation import MachOKit -import MachOFoundation +import MachOBase public enum MachOSwiftSectionError: LocalizedError, Sendable { case sectionNotFound(section: MachOSwiftSectionName, allSectionNames: [String]) diff --git a/Sources/MachOSwiftSection/Utils/ResolvableLocatableLayoutWrapper.swift b/Sources/MachOSwiftSection/Utils/ResolvableLocatableLayoutWrapper.swift index 458873e2..46c17036 100644 --- a/Sources/MachOSwiftSection/Utils/ResolvableLocatableLayoutWrapper.swift +++ b/Sources/MachOSwiftSection/Utils/ResolvableLocatableLayoutWrapper.swift @@ -1,4 +1,4 @@ import Foundation -import MachOFoundation +import MachOBase public typealias ResolvableLocatableLayoutWrapper = LocatableLayoutWrapper & Resolvable diff --git a/Sources/MachOSwiftSectionC/include/Functions.h b/Sources/MachOSwiftSectionC/include/Functions.h index 016fc6d9..bef4f168 100644 --- a/Sources/MachOSwiftSectionC/include/Functions.h +++ b/Sources/MachOSwiftSectionC/include/Functions.h @@ -34,6 +34,7 @@ #include #include +#include #include "CallAccessor.h" @@ -117,6 +118,59 @@ extern const void *swift_getTypeByMangledNameInEnvironment(const char *typeNameS extern const MetadataResponse swift_getAssociatedTypeWitness(size_t request, const void *wtable, const void *conformingType, const void *reqBase, const void *assocType); +//===----------------------------------------------------------------------===// +// Metadata Construction +//===----------------------------------------------------------------------===// + +// MetadataResponse swift_checkMetadataState(MetadataRequest request, const Metadata *type); +extern const MetadataResponse swift_checkMetadataState(size_t request, const void *type); + +// const FunctionTypeMetadata * +// swift_getFunctionTypeMetadata(FunctionTypeFlags flags, const Metadata *const *parameters, +// const uint32_t *parameterFlags, const Metadata *result); +extern const void *swift_getFunctionTypeMetadata(size_t flags, const void *const *parameters, + const uint32_t *parameterFlags, const void *result); + +// const FunctionTypeMetadata * +// swift_getExtendedFunctionTypeMetadata(FunctionTypeFlags flags, +// FunctionMetadataDifferentiabilityKind diffKind, +// const Metadata *const *parameters, +// const uint32_t *parameterFlags, const Metadata *result, +// const Metadata *globalActor, +// ExtendedFunctionTypeFlags extFlags, +// const Metadata *thrownError); +// Weak: only present in newer Swift runtimes; check the symbol's address before calling. +extern const void *swift_getExtendedFunctionTypeMetadata(size_t flags, size_t diffKind, + const void *const *parameters, + const uint32_t *parameterFlags, + const void *result, const void *globalActor, + uint32_t extFlags, const void *thrownError) + __attribute__((weak_import)); + +// const Metadata *swift_getMetatypeMetadata(const Metadata *instanceType); +extern const void *swift_getMetatypeMetadata(const void *instanceType); + +// const ExistentialMetatypeMetadata *swift_getExistentialMetatypeMetadata(const Metadata *instanceType); +extern const void *swift_getExistentialMetatypeMetadata(const void *instanceType); + +// const ExistentialTypeMetadata * +// swift_getExistentialTypeMetadata(ProtocolClassConstraint classConstraint, +// const Metadata *superclassConstraint, +// size_t numProtocols, const ProtocolDescriptorRef *protocols); +// The runtime sorts the protocols array in place — always pass a mutable copy. +// ProtocolClassConstraint is ABI-inverted: Class = 0, Any = 1. +extern const void *swift_getExistentialTypeMetadata(uint8_t classConstraint, + const void *superclassConstraint, + size_t numProtocols, void *protocols); + +// MetadataResponse swift_getTupleTypeMetadata(MetadataRequest request, TupleTypeFlags flags, +// const Metadata *const *elements, const char *labels, +// const ValueWitnessTable *proposedWitnesses); +extern const MetadataResponse swift_getTupleTypeMetadata(size_t request, size_t flags, + const void *const *elements, + const char *labels, + const void *proposedWitnesses); + //===----------------------------------------------------------------------===// // Obj-C Support //===----------------------------------------------------------------------===// diff --git a/Sources/MachOSymbols/DemangledSymbol.swift b/Sources/MachOSymbols/DemangledSymbol.swift index c653a81c..335b0453 100644 --- a/Sources/MachOSymbols/DemangledSymbol.swift +++ b/Sources/MachOSymbols/DemangledSymbol.swift @@ -1,3 +1,4 @@ +import MachOResolving import Demangling /// A symbol paired with the handle of its demangled tree. diff --git a/Sources/MachOSymbols/LargeStackTaskExecution.swift b/Sources/MachOSymbols/LargeStackTaskExecution.swift new file mode 100644 index 00000000..d0e15367 --- /dev/null +++ b/Sources/MachOSymbols/LargeStackTaskExecution.swift @@ -0,0 +1,90 @@ +import Foundation +import FoundationToolbox +@_spi(Internals) import Demangling + +/// Runs a library entry point on the demangler's large-stack task executor +/// (evolution proposal `large-stack-executor-and-cross-version-parallelism`). +/// +/// `StackSafeExecutor` decides per demangle / print / remangle whether to hop +/// to an 8MB pool thread by probing the CALLING thread's remaining stack, not +/// its identity. Swift Concurrency's cooperative threads (and libdispatch's) +/// carry 512KB, so on them the probe never passes and every call pays a +/// thread round trip plus a semaphore wait — 8–21 µs each in release, 1.14–2.28× +/// the work itself. The synchronous indexing sweep amortizes that with one +/// `withLargeStack` batch (`SymbolIndexStore.buildStorageImpl`); an `async` +/// print loop cannot be enclosed in a synchronous batch. The upstream answer +/// (swift-demangling proposal 0014, shipped in 0.6.3) is a `TaskExecutor` +/// whose threads carry 16MB: a task running on it passes the probe at every +/// entry point, synchronous callees included, so the whole pipeline runs +/// inline with zero hops. +/// +/// ``run(_:)`` is the one place this library adopts it. The async entry points +/// — indexer preparation, interface building and printing, the printer's +/// per-definition entries, diff / evolution preparation and rendering, and +/// the dump family — wrap their bodies in it, so a host gets the executor +/// without changing a line. Nesting is free: a task already on the executor +/// does not switch, so an entry point reached from another wrapped entry +/// point pays nothing. +/// +/// Output is identical either way; only the thread the work runs on differs. +/// Where the runtime has no task executors (below macOS 15 / iOS 18 / tvOS 18 +/// / watchOS 11 / visionOS 2, or off Darwin) the body runs unchanged on the +/// caller's executor — the pre-adoption behavior, hops included. +/// +/// Two upstream contracts the wrapping honors: an unstructured `Task {}` +/// does not inherit the preference (SE-0417), so the library starts none +/// inside a wrapped entry point (child tasks and default actors do inherit); +/// and a job that blocks its thread waiting on another job of the same +/// quality-of-service class can exhaust that class's workers, exactly as it +/// would exhaust the cooperative pool — which is why cross-version +/// parallelism caps its window at the processor count. +public enum LargeStackTaskExecution { + /// Process-wide switch. A host that manages its own executor sets it to + /// `false`. The environment variable + /// `MACHO_SWIFT_SECTION_LARGE_STACK_EXECUTOR` seeds it for a process that + /// cannot be recompiled — the rendering A/B and the timing runs compare + /// the same binary with the executor on and off through it. See + /// ``isEnabled(fromEnvironmentValue:)`` for the accepted spellings. + @Mutex + public static var isEnabled: Bool = isEnabled(fromEnvironmentValue: ProcessInfo.processInfo.environment["MACHO_SWIFT_SECTION_LARGE_STACK_EXECUTOR"]) + + /// The verdict `MACHO_SWIFT_SECTION_LARGE_STACK_EXECUTOR` seeds: + /// `0` / `false` / `no` / `off` (case-insensitive, surrounding whitespace + /// ignored) turn the executor off, every other value — including an + /// unset variable — leaves it on. The first version compared the raw + /// value against the literal `"0"`, so `=false` silently measured the + /// executor twice. + public static func isEnabled(fromEnvironmentValue value: String?) -> Bool { + guard let value else { return true } + let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + return !["0", "false", "no", "off"].contains(normalized) + } + + /// Whether this process can run work on the executor at all: Darwin, on a + /// runtime with SE-0417 task executors. Independent of ``isEnabled``. + public static var isSupported: Bool { + #if canImport(Darwin) + if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + return true + } + #endif + return false + } + + /// Runs `body` with the large-stack executor as the task executor + /// preference when ``isEnabled`` and ``isSupported``; otherwise runs + /// `body` unchanged on the caller's executor. + /// + /// The preference governs nonisolated async code, child tasks and default + /// actors inside `body`. An actor with its own executor (the main actor) + /// keeps its executor — the main thread's 8MB stack already passes the + /// probe, so nothing is lost there. + public static func run(_ body: () async throws -> Success) async rethrows -> Success { + #if canImport(Darwin) + if isEnabled, #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + return try await withTaskExecutorPreference(StackSafeExecutor.taskExecutor, operation: body) + } + #endif + return try await body() + } +} diff --git a/Sources/MachOSymbols/MachO+Symbol.swift b/Sources/MachOSymbols/MachO+Symbol.swift index 24b0cfb8..2b527460 100644 --- a/Sources/MachOSymbols/MachO+Symbol.swift +++ b/Sources/MachOSymbols/MachO+Symbol.swift @@ -1,18 +1,18 @@ import MachOKit import MachOKitExtensions +import MachOResolving extension MachORepresentableWithCache { - public func symbols(offset: Int) -> MachOSymbols.Symbols? { - return SymbolIndexStore.shared.symbols(for: offset, in: self) - } - - public func symbols(offset: Int) async -> MachOSymbols.Symbols? { + /// Every symbol the image's index knows at `offset`, building the index + /// on first use. `nil` when the image has no symbol at that offset (or + /// no index could be built). + public func symbols(offset: Int) -> MachOResolving.Symbols? { return SymbolIndexStore.shared.symbols(for: offset, in: self) } } extension MachORepresentable { - public var swiftSymbols: [MachOSymbols.Symbol] { + public var swiftSymbols: [MachOResolving.Symbol] { symbols.filter { $0.name.isSwiftSymbol }.map { .init(offset: $0.offset, name: $0.name) } } } diff --git a/Sources/MachOSymbols/Symbol.swift b/Sources/MachOSymbols/Symbol.swift index b07a0ab2..88209f62 100644 --- a/Sources/MachOSymbols/Symbol.swift +++ b/Sources/MachOSymbols/Symbol.swift @@ -5,27 +5,24 @@ import MachOKitExtensions @_spi(Internals) import Demangling import FoundationToolbox -public struct Symbol: AsyncResolvable, SymbolProtocol, Hashable, Sendable { - public let offset: Int - - public let name: String - - /// Whether the symbol-table entry was flagged as an undefined external - /// import (`N_EXT` with `N_UNDF` type). Extracted from the `nlist` entry - /// at collection time; the entry itself is not retained (a 40-byte - /// existential per symbol copy that nothing else consumed). - public let isExternal: Bool - - public init(offset: Int, name: String, isExternal: Bool = false) { - self.offset = offset - self.name = name - self.isExternal = isExternal - } +extension Symbol { + /// Whether ``resolve(from:in:)`` answers from `SymbolIndexStore` (every + /// name at the offset, index built on first use) or from MachOKit's own + /// symbol table (one name, no index). Process-wide. + @Mutex + public static var resolvesSymbolUsingIndexStore: Bool = true + /// Looks the symbol at `offset` up — `SymbolIndexStore` or MachOKit's + /// symbol table depending on ``resolvesSymbolUsingIndexStore`` — and + /// throws when there is none. + /// + /// A lookup, not a read: this is why it lives here and not on the value + /// type in `MachOResolving` (evolution proposal `self-contained-abi-layer`). public static func resolve(from offset: Int, in machO: MachO) throws -> Self { try required(resolve(from: offset, in: machO)) } + /// Optional form of ``resolve(from:in:)-swift.type.method``. public static func resolve(from offset: Int, in machO: MachO) throws -> Self? { if resolvesSymbolUsingIndexStore { return machO.symbols(offset: offset)?.first @@ -33,46 +30,10 @@ public struct Symbol: AsyncResolvable, SymbolProtocol, Hashable, Sendable { return machO.symbol(for: offset, inSection: 0, isGlobalOnly: false)?.asCurrentSymbol } } - - public static func resolve(from offset: Int, in machO: MachO) async throws -> Self { - try await required(resolve(from: offset, in: machO)) - } - - public static func resolve(from offset: Int, in machO: MachO) async throws -> Self? { - if resolvesSymbolUsingIndexStore { - return await machO.symbols(offset: offset)?.first - } else { - return machO.symbol(for: offset, inSection: 0, isGlobalOnly: false)?.asCurrentSymbol - } - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(offset) - hasher.combine(name) - } - - public static func == (lhs: Self, rhs: Self) -> Bool { - return lhs.offset == rhs.offset && lhs.name == rhs.name - } - - public enum AddressFormat { - case hex - case decimal - } - - public func addressString(format: AddressFormat, in machO: some MachORepresentableWithCache) -> String { - switch format { - case .hex: - return "0x" + String(machO.address(forOffset: offset), radix: 16, uppercase: true) - case .decimal: - return String(machO.address(forOffset: offset), radix: 10) - } - } - - @Mutex - public static var resolvesSymbolUsingIndexStore: Bool = true } +extension Symbol: MachOSymbols.SymbolProtocol {} + public protocol SymbolProtocol { var name: String { get } } @@ -85,9 +46,8 @@ extension MachOSymbols.SymbolProtocol { } } - extension MachOKit.SymbolProtocol { - fileprivate var asCurrentSymbol: MachOSymbols.Symbol { + fileprivate var asCurrentSymbol: MachOResolving.Symbol { .init(offset: offset, name: name, isExternal: nlist.isExternal) } } diff --git a/Sources/MachOSymbols/SymbolIndexStore.swift b/Sources/MachOSymbols/SymbolIndexStore.swift index 3721d245..e7f4d152 100644 --- a/Sources/MachOSymbols/SymbolIndexStore.swift +++ b/Sources/MachOSymbols/SymbolIndexStore.swift @@ -2,6 +2,7 @@ import Foundation import FoundationToolbox import MachOKit import MachOKitExtensions +import MachOResolving @_spi(Internals) import Demangling import OrderedCollections import Utilities diff --git a/Sources/MachOSymbols/SymbolTable.swift b/Sources/MachOSymbols/SymbolTable.swift index ce2bc02e..59b6ba32 100644 --- a/Sources/MachOSymbols/SymbolTable.swift +++ b/Sources/MachOSymbols/SymbolTable.swift @@ -1,3 +1,4 @@ +import MachOResolving import Foundation import MachOKit diff --git a/Sources/MachOSymbols/Symbols.swift b/Sources/MachOSymbols/Symbols.swift deleted file mode 100644 index b68c469f..00000000 --- a/Sources/MachOSymbols/Symbols.swift +++ /dev/null @@ -1,66 +0,0 @@ -import MachOKit -import MachOReading -import MachOResolving -import MachOKitExtensions - -public struct Symbols: AsyncResolvable { - public let offset: Int - - private var _storage: [Symbol] = [] - - internal init(offset: Int, symbols: [Symbol]) { - self.offset = offset - self._storage = symbols - } - - public static func resolve(from offset: Int, in machO: MachO) throws -> Self { - try required(resolve(from: offset, in: machO)) - } - - public static func resolve(from offset: Int, in machO: MachO) throws -> Self? { - return machO.symbols(offset: offset) - } - - public static func resolve(from offset: Int, in machO: MachO) async throws -> Self { - try await required(resolve(from: offset, in: machO)) - } - - public static func resolve(from offset: Int, in machO: MachO) async throws -> Self? { - return await machO.symbols(offset: offset) - } -} - -extension Symbols: RandomAccessCollection { - public typealias Element = Symbol - - public var startIndex: Int { _storage.startIndex } - - public var endIndex: Int { _storage.endIndex } - - public func index(after i: Int) -> Int { - _storage.index(after: i) - } -} - -extension Symbols: MutableCollection { - public subscript(position: Int) -> Symbol { - get { - _storage[position] - } - set { - _storage[position] = newValue - } - } - - public mutating func append(_ newElement: Symbol) { - _storage.append(newElement) - } - - public mutating func remove(at index: Int) { - _storage.remove(at: index) - } - - public mutating func removeAll() { - _storage.removeAll() - } -} diff --git a/Sources/SwiftDeclaration/Components/Definitions/ExtensionDefinition.swift b/Sources/SwiftDeclaration/Components/Definitions/ExtensionDefinition.swift index 34ed5a95..1b63b073 100644 --- a/Sources/SwiftDeclaration/Components/Definitions/ExtensionDefinition.swift +++ b/Sources/SwiftDeclaration/Components/Definitions/ExtensionDefinition.swift @@ -208,7 +208,7 @@ public final class ExtensionDefinition: Definition, MutableDefinition { var defaultImplementationSymbolNames: Set = [] for resilientWitness in protocolConformance.resilientWitnesses { - if let symbols = try resilientWitness.implementationSymbols(in: machO), let symbol = try _symbol(for: symbols, typeName: extensionName.name, visitedNodes: visitedNodes) { + if let symbols = resilientWitness.implementationSymbols(in: machO), let symbol = try _symbol(for: symbols, typeName: extensionName.name, visitedNodes: visitedNodes) { _ = visitedNodes.append(StructuralNodeReferenceKey(symbol.demangledNode)) addSymbol(.init(symbol), memberSymbolsByKind: &memberSymbolsByKind, inExtension: true) } else if let requirement = try resilientWitness.requirement(in: machO) { @@ -218,10 +218,10 @@ public final class ExtensionDefinition: Definition, MutableDefinition { addSymbol(.init(.init(symbol: symbol, demangledNode: demangledNode)), memberSymbolsByKind: &memberSymbolsByKind, inExtension: true) } case .element(let element): - if let symbols = try await Symbols.resolve(from: element.offset, in: machO), let symbol = try _symbol(for: symbols, typeName: extensionName.name, visitedNodes: visitedNodes) { + if let symbols = machO.symbols(offset: element.offset), let symbol = try _symbol(for: symbols, typeName: extensionName.name, visitedNodes: visitedNodes) { _ = visitedNodes.append(StructuralNodeReferenceKey(symbol.demangledNode)) addSymbol(.init(symbol), memberSymbolsByKind: &memberSymbolsByKind, inExtension: true) - } else if let defaultImplementationSymbols = try element.defaultImplementationSymbols(in: machO), let symbol = try _symbol(for: defaultImplementationSymbols, typeName: extensionName.name, visitedNodes: visitedNodes) { + } else if let defaultImplementationSymbols = element.defaultImplementationSymbols(in: machO), let symbol = try _symbol(for: defaultImplementationSymbols, typeName: extensionName.name, visitedNodes: visitedNodes) { _ = visitedNodes.append(StructuralNodeReferenceKey(symbol.demangledNode)) // The witness resolved through the requirement's // DEFAULT implementation — the code lives in a diff --git a/Sources/SwiftDeclaration/Components/Definitions/FunctionDefinition.swift b/Sources/SwiftDeclaration/Components/Definitions/FunctionDefinition.swift index 22207347..c54d4ea1 100644 --- a/Sources/SwiftDeclaration/Components/Definitions/FunctionDefinition.swift +++ b/Sources/SwiftDeclaration/Components/Definitions/FunctionDefinition.swift @@ -1,6 +1,7 @@ import MemberwiseInit import Demangling import MachOSwiftSection +import MachOFoundation import Utilities @MemberwiseInit(.public) diff --git a/Sources/SwiftDeclaration/Components/Definitions/OverrideSymbolMatcher.swift b/Sources/SwiftDeclaration/Components/Definitions/OverrideSymbolMatcher.swift index cce60928..a84e303d 100644 --- a/Sources/SwiftDeclaration/Components/Definitions/OverrideSymbolMatcher.swift +++ b/Sources/SwiftDeclaration/Components/Definitions/OverrideSymbolMatcher.swift @@ -4,8 +4,15 @@ import OrderedCollections @_spi(Internals) import MachOSymbols @_spi(Internals) import SwiftInspection -/// Finds the implementation symbol whose demangled `.class` node matches -/// `typeNode`'s class node, skipping already-visited nodes. +/// Finds the implementation symbol declared in `typeNode`'s class, skipping +/// already-visited nodes. +/// +/// The match is on the member's DIRECT declaration context. Matching on +/// `first(of: .class)` — the first class node anywhere in the tree — accepts +/// members of NESTED types as members of the enclosing class +/// (`GraphHost.Data.graph.modify` reports `GraphHost`), which under identical +/// code folding is how a vtable slot acquires a name belonging to something +/// else entirely. /// /// `visitedNodes` is keyed structurally: `demangledNodeReference(for:)` can /// hand back references from different stores, and under store-identity @@ -47,8 +54,8 @@ package func demangledOverrideSymbol SemanticString { + indentString + Comment("No implementation in this image (deleted method — slot retained for ABI)") + BreakLine() + } + + /// Builds the comment marking a vtable slot whose member could not be + /// proven. + /// + /// The descriptor carries no `Tq` symbol and its implementation address is + /// shared by identical-code-folded siblings, so the name that follows is + /// the best available candidate rather than an established fact. + @SemanticStringBuilder + package func ambiguousAttributionComment(foldedSymbolCount: Int) -> SemanticString { + indentString + Comment("Attribution: ambiguous — \(foldedSymbolCount) symbols folded at this address") + BreakLine() + } + /// Builds an enum layout per-case comment block for the given case projection. @SemanticStringBuilder package func enumLayoutCaseComment(caseProjection: EnumLayoutCalculator.EnumCaseProjection) -> SemanticString { diff --git a/Sources/SwiftDeclarationRendering/Extensions/Node+.swift b/Sources/SwiftDeclarationRendering/Extensions/Node+.swift index 53ce6463..263794aa 100644 --- a/Sources/SwiftDeclarationRendering/Extensions/Node+.swift +++ b/Sources/SwiftDeclarationRendering/Extensions/Node+.swift @@ -76,24 +76,30 @@ extension DemanglingNode { /// overflow the stack where the identical `NodeReference` call would not. /// /// The guard is the engine's own `print(_:options:)`, which routes through - /// `StackSafeExecutor.executeWithUncheckedSendability` — the same probe - /// and the same 2MB floor as `execute`, only without the `Sendable` - /// checking a generic `Target` cannot satisfy. An earlier version of this - /// comment framed the two as a choice, claiming the engine's entry point - /// "runs the recursion inline and pays for a worker only for a tree that - /// actually reaches it"; it does not. Darwin gives every thread but the - /// main one a 512KB stack, so on a cooperative or libdispatch worker the - /// probe never passes and *every* call hops to a large-stack worker and - /// blocks on a semaphore — whichever wrapper the engine happens to use. + /// `StackSafeExecutor.execute` — probe the calling thread's remaining + /// stack against a 2MB floor, run inline when it passes, hop to an 8MB + /// pool worker and block on a semaphore when it does not. An earlier + /// version of this comment claimed the engine's entry point "runs the + /// recursion inline and pays for a worker only for a tree that actually + /// reaches it"; it does not. Darwin gives every thread but the main one a + /// 512KB stack, so on a cooperative or libdispatch worker the probe never + /// passes and *every* call hops. /// /// That cost is upstream's deliberate trade (`swift-demangling` 7b86137): /// before it, the printer recursed unguarded and a deeply nested generic /// really did overflow a 512KB worker. It is paid down at a *batch* /// boundary, never here — a `withLargeStack` around this single call would /// save exactly the one hop it adds. `SymbolIndexStore.buildStorageImpl` - /// is where the repo does that, because it owns a loop; the printer's own - /// loop lives in `SwiftDeclarationPrinter` and is `async`, which a - /// synchronous wrapper cannot enclose. + /// does that for the synchronous sweep, because it owns a loop; the + /// printer's loop lives in `SwiftDeclarationPrinter` and is `async`, which + /// a synchronous wrapper cannot enclose. The async side's batch boundary + /// is the TASK instead (evolution proposal + /// `large-stack-executor-and-cross-version-parallelism`): the library's + /// async entry points run on the demangler's 16MB `LargeStackTaskExecutor` + /// through `LargeStackTaskExecution.run`, where the probe passes at every + /// entry and this call runs inline. The hop is what happens OFF that path + /// — a host that disabled the executor, or a runtime without task + /// executors (below macOS 15). /// /// **Do not "modernize" this onto `runPrintWalk(using:)`.** Upstream added /// that protocol requirement as the dispatch hook behind @@ -105,11 +111,10 @@ extension DemanglingNode { /// it. For a non-`String` target the engine's static /// `DemanglingPrinter.print(_:options:)` is the only /// entry point, and will remain so. It is byte-for-byte unchanged across - /// the `runPrintWalk` introduction — including the - /// `StackSafeExecutor.executeWithUncheckedSendability` wrapper this comment - /// exists to explain (verified upstream on a deliberately 512KB-stacked - /// thread against 600 levels of nested generics, which survives only - /// because of that hop). + /// the `runPrintWalk` introduction — including the `StackSafeExecutor` + /// wrapper this comment exists to explain (verified upstream on a + /// deliberately 512KB-stacked thread against 600 levels of nested + /// generics, which survives only because of that hop). /// /// Worth noting *why* the shadowing hazard above is a recurring shape /// rather than a one-off: upstream hit the mirror image of it in the same diff --git a/Sources/SwiftDeclarationRendering/StaticFieldLayoutProvider.swift b/Sources/SwiftDeclarationRendering/StaticFieldLayoutProvider.swift index a1b79ce3..a58adbf8 100644 --- a/Sources/SwiftDeclarationRendering/StaticFieldLayoutProvider.swift +++ b/Sources/SwiftDeclarationRendering/StaticFieldLayoutProvider.swift @@ -1,6 +1,8 @@ import Foundation import MachOKit +import MachODependencies import MachOSwiftSection +import MachOFoundation import SwiftLayout @_spi(Internals) import SwiftInspection @@ -16,7 +18,7 @@ public enum StaticLayoutDependencyResolution: Sendable, Equatable, Hashable { /// stdlib / Foundation / the rest of the OS). Cross-module field / /// superclass / protocol types resolve, and resilient classes are laid out /// against their dependencies' actual binaries ("this specific deployment"). - case dependencyClosure(searchPaths: [LayoutDependencySearchPath]) + case dependencyClosure(searchPaths: [DependencySearchPath]) /// The default resolution: the full transitive closure over the system dyld /// shared cache. diff --git a/Sources/SwiftDump/Dumpable/AssociatedType+Dumpable.swift b/Sources/SwiftDump/Dumpable/AssociatedType+Dumpable.swift index e1d99f9c..57debb70 100644 --- a/Sources/SwiftDump/Dumpable/AssociatedType+Dumpable.swift +++ b/Sources/SwiftDump/Dumpable/AssociatedType+Dumpable.swift @@ -1,6 +1,7 @@ import Foundation import MachOKit import MachOSwiftSection +import MachOFoundation import Semantic import Utilities import SwiftDeclarationRendering @@ -15,6 +16,8 @@ extension AssociatedType: ConformedDumpable { } public func dump(using configuration: DumperConfiguration, in machO: MachO) async throws -> SemanticString { - try await AssociatedTypeDumper(self, using: configuration, in: machO).body + try await LargeStackTaskExecution.run { + try await AssociatedTypeDumper(self, using: configuration, in: machO).body + } } } diff --git a/Sources/SwiftDump/Dumpable/Class+Dumpable.swift b/Sources/SwiftDump/Dumpable/Class+Dumpable.swift index 2b568528..09e705dc 100644 --- a/Sources/SwiftDump/Dumpable/Class+Dumpable.swift +++ b/Sources/SwiftDump/Dumpable/Class+Dumpable.swift @@ -2,6 +2,7 @@ import Semantic import Demangling import MachOKit import MachOSwiftSection +import MachOFoundation import Utilities import SwiftDeclarationRendering @@ -11,6 +12,8 @@ extension Class: NamedDumpable { } public func dump(using configuration: DumperConfiguration, in machO: MachO) async throws -> SemanticString { - try await ClassDumper(self, using: configuration, in: machO).body + try await LargeStackTaskExecution.run { + try await ClassDumper(self, using: configuration, in: machO).body + } } } diff --git a/Sources/SwiftDump/Dumpable/Enum+Dumpable.swift b/Sources/SwiftDump/Dumpable/Enum+Dumpable.swift index 3ab756e0..42c55fe5 100644 --- a/Sources/SwiftDump/Dumpable/Enum+Dumpable.swift +++ b/Sources/SwiftDump/Dumpable/Enum+Dumpable.swift @@ -1,6 +1,7 @@ import Foundation import MachOKit import MachOSwiftSection +import MachOFoundation import Semantic import Utilities import SwiftDeclarationRendering @@ -11,6 +12,8 @@ extension Enum: NamedDumpable { } public func dump(using configuration: DumperConfiguration, in machO: MachO) async throws -> SemanticString { - try await EnumDumper(self, using: configuration, in: machO).body + try await LargeStackTaskExecution.run { + try await EnumDumper(self, using: configuration, in: machO).body + } } } diff --git a/Sources/SwiftDump/Dumpable/Protocol+Dumpable.swift b/Sources/SwiftDump/Dumpable/Protocol+Dumpable.swift index 106a111d..25280ab2 100644 --- a/Sources/SwiftDump/Dumpable/Protocol+Dumpable.swift +++ b/Sources/SwiftDump/Dumpable/Protocol+Dumpable.swift @@ -1,6 +1,7 @@ import Foundation import MachOKit import MachOSwiftSection +import MachOFoundation import Semantic import Utilities import Demangling @@ -13,6 +14,8 @@ extension MachOSwiftSection.`Protocol`: NamedDumpable { } public func dump(using configuration: DumperConfiguration, in machO: MachO) async throws -> SemanticString { - try await ProtocolDumper(self, using: configuration, in: machO).body + try await LargeStackTaskExecution.run { + try await ProtocolDumper(self, using: configuration, in: machO).body + } } } diff --git a/Sources/SwiftDump/Dumpable/ProtocolConformance+Dumpable.swift b/Sources/SwiftDump/Dumpable/ProtocolConformance+Dumpable.swift index 33535a1b..bceeeadd 100644 --- a/Sources/SwiftDump/Dumpable/ProtocolConformance+Dumpable.swift +++ b/Sources/SwiftDump/Dumpable/ProtocolConformance+Dumpable.swift @@ -1,6 +1,7 @@ import Foundation import MachOKit import MachOSwiftSection +import MachOFoundation import Semantic import Demangling import Utilities @@ -17,6 +18,8 @@ extension ProtocolConformance: ConformedDumpable { } public func dump(using configuration: DumperConfiguration, in machO: MachO) async throws -> SemanticString { - try await ProtocolConformanceDumper(self, using: configuration, in: machO).body + try await LargeStackTaskExecution.run { + try await ProtocolConformanceDumper(self, using: configuration, in: machO).body + } } } diff --git a/Sources/SwiftDump/Dumpable/Struct+Dumpable.swift b/Sources/SwiftDump/Dumpable/Struct+Dumpable.swift index c06a21e3..3e687a6e 100644 --- a/Sources/SwiftDump/Dumpable/Struct+Dumpable.swift +++ b/Sources/SwiftDump/Dumpable/Struct+Dumpable.swift @@ -1,6 +1,7 @@ import Foundation import MachOKit import MachOSwiftSection +import MachOFoundation import Semantic import Utilities import SwiftDeclarationRendering @@ -11,6 +12,8 @@ extension Struct: NamedDumpable { } public func dump(using configuration: DumperConfiguration, in machO: MachO) async throws -> SemanticString { - try await StructDumper(self, using: configuration, in: machO).body + try await LargeStackTaskExecution.run { + try await StructDumper(self, using: configuration, in: machO).body + } } } diff --git a/Sources/SwiftDump/Dumper/ClassDumper.swift b/Sources/SwiftDump/Dumper/ClassDumper.swift index 5faa0058..6983932d 100644 --- a/Sources/SwiftDump/Dumper/ClassDumper.swift +++ b/Sources/SwiftDump/Dumper/ClassDumper.swift @@ -231,15 +231,28 @@ package struct ClassDumper: TypedDumper { configuration.memberAddressComment(offset: implOffset, addressString: machO.addressString(forOffset: implOffset)) } - Indent(level: 1) + // Attribution, in order of evidence: the descriptor's own `Tq` + // symbol (one per member, at the descriptor's own address, so + // identical code folding cannot reach it), then the symbols at + // the implementation address (not invertible under folding — + // the fallback, not the source of truth). Pre-resolved before + // any keyword is emitted because the distributed check reads + // the node. + let implementationSymbols = descriptor.implementationSymbols(in: machO) + let attributedMethodNode = descriptor.attributedMemberNode(in: machO) + var resolvedMethodNode = attributedMethodNode + if resolvedMethodNode == nil, let implementationSymbols { + resolvedMethodNode = try? await validNode(for: implementationSymbols, visitedNodes: methodVisitedNodes) + } - // Pre-resolve the method node so we can check distributed status - // before deciding which keywords to emit. - var resolvedMethodNode: NodeReference? = nil - if let symbols = try? descriptor.implementationSymbols(in: machO) { - resolvedMethodNode = try? await validNode(for: symbols, visitedNodes: methodVisitedNodes) + if descriptor.implementation.isNull { + configuration.deletedMethodSlotComment() + } else if attributedMethodNode == nil, let implementationSymbols, implementationSymbols.count > 1 { + configuration.ambiguousAttributionComment(foldedSymbolCount: implementationSymbols.count) } + Indent(level: 1) + let isDistributedMethod: Bool = { guard descriptor.flags.kind == .method, let root = resolvedMethodNode, @@ -278,7 +291,11 @@ package struct ClassDumper: TypedDumper { let methodDescriptor = try descriptor.methodDescriptor(in: machO) - if let symbols = try? descriptor.implementationSymbols(in: machO), let node = try await validNode(for: symbols, visitedNodes: methodOverrideVisitedNodes) { + // An override slot keeps the implementation-address route: its + // descriptor has no `Tq` symbol, and the parent's descriptor + // names the parent's member, not this class's implementation. + // See the note in `Descriptor+MethodDescriptorSymbols.swift`. + if let symbols = descriptor.implementationSymbols(in: machO), let node = try await validNode(for: symbols, visitedNodes: methodOverrideVisitedNodes) { dumpMethodKind(for: methodDescriptor?.resolved) Keyword(.override) Space() @@ -326,7 +343,9 @@ package struct ClassDumper: TypedDumper { Space() - if let symbols = try? descriptor.implementationSymbols(in: machO), let node = try await validNode(for: symbols, visitedNodes: methodDefaultOverrideVisitedNodes) { + // Implementation-address route, same reason as the override + // loop above. + if let symbols = descriptor.implementationSymbols(in: machO), let node = try await validNode(for: symbols, visitedNodes: methodDefaultOverrideVisitedNodes) { try await demangleResolver.resolve(for: node) _ = methodDefaultOverrideVisitedNodes.append(StructuralNodeReferenceKey(node)) } else if !descriptor.implementation.isNull { @@ -453,14 +472,14 @@ package struct ClassDumper: TypedDumper { private func collectOverrideImplementationSymbolNames() -> Set { var names: Set = [] for descriptor in dumped.methodOverrideDescriptors { - if let symbols = try? descriptor.implementationSymbols(in: machO) { + if let symbols = descriptor.implementationSymbols(in: machO) { for overrideSymbol in symbols { names.insert(overrideSymbol.name) } } } for descriptor in dumped.methodDefaultOverrideDescriptors { - if let symbols = try? descriptor.implementationSymbols(in: machO) { + if let symbols = descriptor.implementationSymbols(in: machO) { for overrideSymbol in symbols { names.insert(overrideSymbol.name) } @@ -528,10 +547,18 @@ package struct ClassDumper: TypedDumper { @SemanticStringBuilder private func dumpMethodDeclaration(for descriptor: MethodDescriptor, resolvedNode: NodeReference? = nil, visitedNodes: inout OrderedSet) async throws -> SemanticString { + // No `Tq` lookup here on purpose. The vtable loop resolves attribution + // itself and passes the result in as `resolvedNode`; the OTHER caller + // is the override loop's `.element` leg, where `descriptor` is the + // PARENT class's method descriptor — attributing through its `Tq` + // symbol would rename the overriding member to the overridden one + // (`override ResilientChild.init()` → `override ResilientBase.init()`) + // and drop the vtable-thunk dispatch detail the implementation symbol + // carries. let node: NodeReference? if let resolvedNode { node = resolvedNode - } else if let symbols = try? descriptor.implementationSymbols(in: machO) { + } else if let symbols = descriptor.implementationSymbols(in: machO) { node = try await validNode(for: symbols, visitedNodes: visitedNodes) } else { node = nil @@ -543,7 +570,10 @@ package struct ClassDumper: TypedDumper { } else if !descriptor.implementation.isNull { FunctionDeclaration(machO.addressString(forOffset: descriptor.implementation.resolveDirectOffset(from: descriptor.offset(of: \.implementation))).insertSubFunctionPrefix) } else { - Error("Symbol not found") + // A null implementation with no `Tq` symbol to name it: the slot is + // an ABI tombstone (see `deletedMethodSlotComment`) whose member + // name this image does not carry. + Error("") } } @@ -562,7 +592,7 @@ package struct ClassDumper: TypedDumper { var names: Set = [] let accessorKinds: Set = [.getter, .setter, .modifyCoroutine, .readCoroutine] for descriptor in dumped.methodDescriptors where accessorKinds.contains(descriptor.flags.kind) { - guard let symbols = try? descriptor.implementationSymbols(in: machO) else { continue } + guard let symbols = descriptor.implementationSymbols(in: machO) else { continue } for symbol in symbols { guard let node = MetadataReader.demangleSymbolReference(for: symbol, in: machO), let variableName = node.first(of: .variable)?.identifier else { continue } @@ -612,12 +642,24 @@ package struct ClassDumper: TypedDumper { return names } + /// The first symbol among `symbols` that is a member OF THIS CLASS and has + /// not been claimed yet. + /// + /// The match is on the member's DIRECT declaration context, not on + /// `first(of: .class)`: the latter finds the first class node anywhere in + /// the tree, so a member of a nested type (`GraphHost.Data.graph.modify`) + /// reports the enclosing class and passes as that class's own member. Under + /// identical code folding — where this whole function's input is one + /// address' worth of unrelated folded symbols — that is a wrong name, not + /// merely a missed one. package func validNode(for symbols: Symbols, visitedNodes: borrowing OrderedSet = []) async throws -> NodeReference? { let currentInterfaceName = try await _name(using: .options(.interfaceType)).string for symbol in symbols { - if let node = MetadataReader.demangleSymbolReference(for: symbol, in: machO), let classNode = node.first(of: .class), await classNode.print(using: .interfaceType) == currentInterfaceName, !visitedNodes.contains(StructuralNodeReferenceKey(node)) { - return node - } + guard let node = MetadataReader.demangleSymbolReference(for: symbol, in: machO), + let declarationContextNode = node.declarationContextNode, + await declarationContextNode.print(using: .interfaceType) == currentInterfaceName, + !visitedNodes.contains(StructuralNodeReferenceKey(node)) else { continue } + return node } return nil } diff --git a/Sources/SwiftDump/Dumper/ProtocolConformanceDumper.swift b/Sources/SwiftDump/Dumper/ProtocolConformanceDumper.swift index 2128dce9..20f03699 100644 --- a/Sources/SwiftDump/Dumper/ProtocolConformanceDumper.swift +++ b/Sources/SwiftDump/Dumper/ProtocolConformanceDumper.swift @@ -102,13 +102,13 @@ package struct ProtocolConformanceDumper: Conforme for resilientWitness in dumped.resilientWitnesses { BreakLine() - if configuration.printMemberAddress { - configuration.memberAddressComment(offset: resilientWitness.implementationOffset, addressString: resilientWitness.implementationAddress(in: machO)) + if configuration.printMemberAddress, let implementationOffset = resilientWitness.implementationOffset, let implementationAddressString = resilientWitness.implementationAddress(in: machO) { + configuration.memberAddressComment(offset: implementationOffset, addressString: implementationAddressString) } Indent(level: 1) - if let symbols = try resilientWitness.implementationSymbols(in: machO), let node = Self.demangledSymbol(for: symbols, typeName: typeNameString, visitedNodes: visitedNodes, in: machO)?.demangledNode { + if let symbols = resilientWitness.implementationSymbols(in: machO), let node = Self.demangledSymbol(for: symbols, typeName: typeNameString, visitedNodes: visitedNodes, in: machO)?.demangledNode { _ = visitedNodes.append(StructuralNodeReferenceKey(node)) try await demangleResolver.resolve(for: node) } else if let requirement = try resilientWitness.requirement(in: machO) { @@ -117,10 +117,10 @@ package struct ProtocolConformanceDumper: Conforme case .symbol(let symbol): try await MetadataReader.demangleSymbolReference(for: symbol, in: machO).asyncMap { try await demangleResolver.resolve(for: $0) } case .element(let element): - if let symbols = try await Symbols.resolve(from: element.offset, in: machO), let node = Self.demangledSymbol(for: symbols, typeName: typeNameString, visitedNodes: visitedNodes, in: machO)?.demangledNode { + if let symbols = machO.symbols(offset: element.offset), let node = Self.demangledSymbol(for: symbols, typeName: typeNameString, visitedNodes: visitedNodes, in: machO)?.demangledNode { _ = visitedNodes.append(StructuralNodeReferenceKey(node)) try await demangleResolver.resolve(for: node) - } else if let defaultImplementationSymbols = try element.defaultImplementationSymbols(in: machO), let node = Self.demangledSymbol(for: defaultImplementationSymbols, typeName: typeNameString, visitedNodes: visitedNodes, in: machO)?.demangledNode { + } else if let defaultImplementationSymbols = element.defaultImplementationSymbols(in: machO), let node = Self.demangledSymbol(for: defaultImplementationSymbols, typeName: typeNameString, visitedNodes: visitedNodes, in: machO)?.demangledNode { _ = visitedNodes.append(StructuralNodeReferenceKey(node)) // Qualifies the address comment above // (evolution proposal 0007): this witness @@ -132,20 +132,16 @@ package struct ProtocolConformanceDumper: Conforme Space() } try await demangleResolver.resolve(for: node) - } else if !element.defaultImplementation.isNull { - FunctionDeclaration(machO.addressString(forOffset: element.defaultImplementation.resolveDirectOffset(from: element.offset(of: \.defaultImplementation))).insertSubFunctionPrefix) - } else if !resilientWitness.implementation.isNull { -// do { -// try demangleResolver.resolve(for: MetadataReader.demangle(for: MangledName.resolve(from: resilientWitness.implementation.resolveDirectOffset(from: resilientWitness.offset(of: \.implementation)) - 1, in: machO), in: machO)) -// } catch { - FunctionDeclaration(machO.addressString(forOffset: resilientWitness.implementation.resolveDirectOffset(from: resilientWitness.offset(of: \.implementation))).insertSubFunctionPrefix) -// } + } else if let defaultImplementationOffset = element.defaultImplementationOffset { + FunctionDeclaration(machO.addressString(forOffset: defaultImplementationOffset).insertSubFunctionPrefix) + } else if let implementationOffset = resilientWitness.implementationOffset { + FunctionDeclaration(machO.addressString(forOffset: implementationOffset).insertSubFunctionPrefix) } else { Error("Symbol not found") } } - } else if !resilientWitness.implementation.isNull { - FunctionDeclaration(machO.addressString(forOffset: resilientWitness.implementation.resolveDirectOffset(from: resilientWitness.offset(of: \.implementation))).insertSubFunctionPrefix) + } else if let implementationOffset = resilientWitness.implementationOffset { + FunctionDeclaration(machO.addressString(forOffset: implementationOffset).insertSubFunctionPrefix) } else { Error("Symbol not found") } @@ -185,7 +181,7 @@ package struct ProtocolConformanceDumper: Conforme } private func _requirementName(for requirement: ProtocolRequirement) async throws -> String? { - guard let symbols = try await Symbols.resolve(from: requirement.offset, in: machO) else { return nil } + guard let symbols = machO.symbols(offset: requirement.offset) else { return nil } for symbol in symbols { if let node = MetadataReader.demangleSymbolReference(for: symbol, in: machO) { return await node.print(using: typeNameOptions) diff --git a/Sources/SwiftDump/Dumper/ProtocolDumper.swift b/Sources/SwiftDump/Dumper/ProtocolDumper.swift index 2a583ec2..7a565b46 100644 --- a/Sources/SwiftDump/Dumper/ProtocolDumper.swift +++ b/Sources/SwiftDump/Dumper/ProtocolDumper.swift @@ -97,13 +97,13 @@ package struct ProtocolDumper: NamedDumper { for (offset, requirement) in dumped.requirements.offsetEnumerated() { BreakLine() Indent(level: configuration.indentation) - if let symbols = try await Symbols.resolve(from: requirement.offset, in: machO), let validNode = try await validNode(for: symbols) { + if let symbols = machO.symbols(offset: requirement.offset), let validNode = try await validNode(for: symbols) { try await demangleResolver.resolve(for: validNode) } else { InlineComment("[Stripped Symbol]") } - if let symbols = try requirement.defaultImplementationSymbols(in: machO), let defaultImplementation = try await validNode(for: symbols, visitedNode: defaultImplementations) { + if let symbols = requirement.defaultImplementationSymbols(in: machO), let defaultImplementation = try await validNode(for: symbols, visitedNode: defaultImplementations) { _ = defaultImplementations.append(StructuralNodeReferenceKey(defaultImplementation)) } @@ -146,6 +146,17 @@ package struct ProtocolDumper: NamedDumper { } } + /// The first symbol among `symbols` mentioning THIS protocol and not yet + /// claimed. + /// + /// Deliberately NOT narrowed to the declaration-context match + /// `ClassDumper.validNode` uses. A protocol's symbols are not only members: + /// `base conformance descriptor for P: Q` and the other requirement + /// descriptors carry no entity node at all, so a context-based match drops + /// them outright (measured: 1033 lines of SwiftUICore's protocol output + /// degraded to `[Stripped Symbol]`). Protocol-side attribution needs its + /// own evidence model and is out of scope for the vtable-attribution + /// proposal — the folding ambiguity documented there applies here too. private func validNode(for symbols: Symbols, visitedNode: borrowing OrderedSet = []) async throws -> NodeReference? { let currentInterfaceName = try await _name(using: .options(.interfaceType)).string for symbol in symbols { diff --git a/Sources/SwiftIndexing/SwiftDeclarationIndexer.swift b/Sources/SwiftIndexing/SwiftDeclarationIndexer.swift index f1902561..ca09ec3f 100644 --- a/Sources/SwiftIndexing/SwiftDeclarationIndexer.swift +++ b/Sources/SwiftIndexing/SwiftDeclarationIndexer.swift @@ -263,9 +263,20 @@ public final class SwiftDeclarationIndexer String? { + let prefix = "[\(timestamp)]" + (label.map { " [\($0)]" } ?? "") switch event { case .extractionCompleted(let result): - report("[\(timestamp)] [INFO] Extracted \(result.count) \(sectionName(result.section))") + return "\(prefix) [INFO] Extracted \(result.count) \(sectionName(result.section))" case .typeIndexingCompleted(let result): - report("[\(timestamp)] [INFO] Types: \(result.successful) successful, \(result.failed) failed, \(result.cImportedSkipped) C-imported skipped, \(result.nestedTypes) nested, \(result.extensionTypes) in extensions") + return "\(prefix) [INFO] Types: \(result.successful) successful, \(result.failed) failed, \(result.cImportedSkipped) C-imported skipped, \(result.nestedTypes) nested, \(result.extensionTypes) in extensions" case .protocolIndexingCompleted(let result): - report("[\(timestamp)] [INFO] Protocols: \(result.successful) successful, \(result.failed) failed") + return "\(prefix) [INFO] Protocols: \(result.successful) successful, \(result.failed) failed" case .conformanceIndexingCompleted(let result): - report("[\(timestamp)] [INFO] Conformances: \(result.extensionCount) extensions, \(result.failedConformances + result.failedAssociatedTypes + result.failedExtensions) failed") + return "\(prefix) [INFO] Conformances: \(result.extensionCount) extensions, \(result.failedConformances + result.failedAssociatedTypes + result.failedExtensions) failed" case .extensionIndexingCompleted(let result): - report("[\(timestamp)] [INFO] Extensions: \(result.typeExtensions) type, \(result.protocolExtensions) protocol, \(result.typeAliasExtensions) typealias, \(result.failed) failed") + return "\(prefix) [INFO] Extensions: \(result.typeExtensions) type, \(result.protocolExtensions) protocol, \(result.typeAliasExtensions) typealias, \(result.failed) failed" case .moduleCollectionCompleted(let result): - report("[\(timestamp)] [INFO] Found \(result.moduleCount) modules to import") + return "\(prefix) [INFO] Found \(result.moduleCount) modules to import" case .phaseTransition(let phase, let state): let phaseName = phaseName(phase) switch state { case .completed: - report("[\(timestamp)] [SUCCESS] \(phaseName.capitalized) completed") + return "\(prefix) [SUCCESS] \(phaseName.capitalized) completed" case .failed(let error): - report("[\(timestamp)] [ERROR] \(phaseName.capitalized) failed: \(String(describing: error))") + return "\(prefix) [ERROR] \(phaseName.capitalized) failed: \(String(describing: error))" case .started: - break // Ignore started events for console output + return nil // Ignore started events for console output } case .definitionPrintFailed(let context, let error): - report("[\(timestamp)] [ERROR] Failed to print \(context.kind.description) '\(context.name)': \(String(describing: error))") + return "\(prefix) [ERROR] Failed to print \(context.kind.description) '\(context.name)': \(String(describing: error))" case .renderingDegraded(let context, let error): let subject = context.subject.map { " for \($0)" } ?? "" - report("[\(timestamp)] [ERROR] Degraded \(context.source)\(subject): \(String(describing: error))") + return "\(prefix) [ERROR] Degraded \(context.source)\(subject): \(String(describing: error))" default: - break // Ignore other detailed events + return nil // Ignore other detailed events } } diff --git a/Sources/SwiftInspection/Extensions/Descriptor+ImplementationSymbols.swift b/Sources/SwiftInspection/Extensions/Descriptor+ImplementationSymbols.swift new file mode 100644 index 00000000..ddaf349c --- /dev/null +++ b/Sources/SwiftInspection/Extensions/Descriptor+ImplementationSymbols.swift @@ -0,0 +1,61 @@ +import MachOKit +import MachOFoundation +import MachOSwiftSection + +// Symbol attribution for the ABI layer's implementation pointers. +// +// `MachOSwiftSection` only knows an implementation's *offset* +// (`implementationOffset`); which symbol names sit at that offset is a +// symbol-index question, answered here where `MachOSymbols` is in reach +// (evolution proposal `self-contained-abi-layer`). Several names can share +// one offset under identical code folding, hence `Symbols`, not `Symbol`. +// Only the MachO-backed form exists: a `ReadingContext` carries no symbol +// service to consult. + +extension MethodDescriptor { + /// The symbols the image's index finds at the implementation's offset; + /// `nil` when the descriptor has no implementation or the index knows + /// nothing at that offset. + public func implementationSymbols(in machO: MachO) -> Symbols? { + guard let implementationOffset else { return nil } + return machO.symbols(offset: implementationOffset) + } +} + +extension MethodOverrideDescriptor { + /// The symbols the image's index finds at the overriding implementation's + /// offset; `nil` for a null pointer or an offset the index does not know. + public func implementationSymbols(in machO: MachO) -> Symbols? { + guard let implementationOffset else { return nil } + return machO.symbols(offset: implementationOffset) + } +} + +extension MethodDefaultOverrideDescriptor { + /// The symbols the image's index finds at the default-override + /// implementation's offset; `nil` for a null pointer or an offset the + /// index does not know. + public func implementationSymbols(in machO: MachO) -> Symbols? { + guard let implementationOffset else { return nil } + return machO.symbols(offset: implementationOffset) + } +} + +extension ProtocolRequirement { + /// The symbols the image's index finds at the requirement's default + /// implementation; `nil` when the requirement has none or the index does + /// not know the offset. + public func defaultImplementationSymbols(in machO: MachO) -> Symbols? { + guard let defaultImplementationOffset else { return nil } + return machO.symbols(offset: defaultImplementationOffset) + } +} + +extension ResilientWitness { + /// The symbols the image's index finds at the witness implementation's + /// offset; `nil` for a null pointer or an offset the index does not know. + public func implementationSymbols(in machO: MachO) -> Symbols? { + guard let implementationOffset else { return nil } + return machO.symbols(offset: implementationOffset) + } +} diff --git a/Sources/SwiftInspection/Extensions/Descriptor+MethodDescriptorSymbols.swift b/Sources/SwiftInspection/Extensions/Descriptor+MethodDescriptorSymbols.swift new file mode 100644 index 00000000..26545088 --- /dev/null +++ b/Sources/SwiftInspection/Extensions/Descriptor+MethodDescriptorSymbols.swift @@ -0,0 +1,97 @@ +import MachOKit +import MachOFoundation +import MachOSwiftSection +@_spi(Internals) import Demangling +@_spi(Internals) import MachOSymbols + +// Vtable slot attribution — deciding which member a slot belongs to. +// +// The answer comes from the method descriptor's OWN `Tq` symbol, not from the +// symbols sitting at its implementation address. The implementation route is +// not invertible: identical code folding merges every byte-identical function +// body onto a single address, and asking the index which symbols live there +// answers with all of them at once. SwiftUICore's `0x9330` — the empty `ret` +// body — carries 2878, which is how `SwiftUI.GraphHost`'s four empty vtable +// methods came to print as one correct name plus three coroutine resume +// functions belonging to its nested `GraphHost.Data` struct. +// +// A `Tq` method-descriptor symbol is a data symbol at the descriptor's own +// address, one per member, so folding cannot reach it. Evolution proposal 0006 +// already relied on that property as negative evidence for the `final` keyword +// (a member with a `Tq` symbol provably owns a vtable entry); this is the same +// fact used positively. +// +// It is not always available — a stripped image, or a member whose descriptor +// symbol the compiler never emitted — so every entry point here returns `nil` +// rather than throwing, and callers fall back to the implementation route. + +extension MethodDescriptor { + /// The symbols the image's index finds at the descriptor's own offset — + /// the `Tq` method-descriptor symbol, when the image carries one. + public func methodDescriptorSymbols(in machO: MachO) -> Symbols? { + machO.symbols(offset: offset) + } + + /// The member this vtable slot belongs to, recovered from the descriptor's + /// own `Tq` symbol. + /// + /// The result is member-shaped (`global()`) — the same shape the + /// implementation-symbol route yields — so printers and join keys need no + /// special case for it. + public func attributedMemberNode(in machO: MachO) -> NodeReference? { + guard let symbols = methodDescriptorSymbols(in: machO) else { return nil } + return MethodDescriptorAttribution.memberNode(forMethodDescriptorSymbols: symbols, in: machO) + } +} + +// Deliberately NOT extended to `MethodOverrideDescriptor` / +// `MethodDefaultOverrideDescriptor`. Those carry no `Tq` symbol of their own — +// they are re-bindings, not declarations — and the descriptor they point at +// belongs to the PARENT class. Attributing an override slot through it answers +// a different question than either consumer asks: the dump prints the +// overriding class's own implementation symbol, and `TypeDefinition.index` +// joins on the subclass's member symbols, so a parent-shaped node matches +// nothing and the `override` keyword disappears from the output entirely +// (caught by `SymbolTestsCoreE2ETests.outputContainsOverrideKeyword`). +// Override slots keep the implementation-address route, which is correct for +// them modulo the same folding ambiguity; their slot NUMBERS come from +// `ParentClassVTableCache` and never depended on symbol attribution. + +/// The shared unwrapping behind `attributedMemberNode`. +public enum MethodDescriptorAttribution { + /// The first symbol among `symbols` that demangles to a method descriptor, + /// unwrapped to its member. + /// + /// A descriptor's own address holds exactly one `Tq` symbol in practice, + /// but the query is written to skip anything else that may share the + /// address rather than to assume the first entry is the right one. + public static func memberNode( + forMethodDescriptorSymbols symbols: Symbols, + in machO: MachO + ) -> NodeReference? { + for symbol in symbols { + guard let node = MetadataReader.demangleSymbolReference(for: symbol, in: machO), + let memberNode = memberNode(unwrappingMethodDescriptorNode: node, in: machO) else { continue } + return memberNode + } + return nil + } + + /// Rewrites `global(methodDescriptor())` into `global()`. + /// + /// Printed as-is, a method-descriptor tree reads "method descriptor for + /// X" rather than X; every consumer wants the member itself. The rebuilt + /// tree is interned through `InternedNodeReferenceCache` (never a bare + /// `NodeReference(interning:)`) so it shares the image's store, and the + /// intermediate tree is transient because it is consumed by that interning + /// and dropped. + static func memberNode( + unwrappingMethodDescriptorNode node: NodeReference, + in machO: MachO + ) -> NodeReference? { + guard let methodDescriptorNode = node.first(of: .methodDescriptor), + let entityNode = methodDescriptorNode.children.first else { return nil } + let memberTree = Node.createTransient(kind: .global, child: entityNode.materialize()) + return InternedNodeReferenceCache.shared.reference(interning: memberTree, in: machO) + } +} diff --git a/Sources/SwiftInspection/Extensions/Node+DeclarationContext.swift b/Sources/SwiftInspection/Extensions/Node+DeclarationContext.swift new file mode 100644 index 00000000..2ca29e31 --- /dev/null +++ b/Sources/SwiftInspection/Extensions/Node+DeclarationContext.swift @@ -0,0 +1,66 @@ +@_spi(Internals) import Demangling + +// The type a member symbol is declared IN. +// +// `first(of: .class)` is NOT that question. It finds the first class node +// ANYWHERE in the tree, so a member of a NESTED type answers with the +// enclosing class and reads as that class's own member: the symbol +// `SwiftUI.GraphHost.Data.graph.modify` has the context chain +// `class GraphHost → struct Data`, and `first(of: .class)` happily returns +// `GraphHost`. Combined with identical code folding — where a whole address' +// worth of folded symbols is offered as candidates for one vtable slot — that +// is how three of `GraphHost`'s vtable methods came to print as coroutine +// resume functions of its nested `Data` struct. +// +// The declaration context is the DIRECT context of the tree's outermost +// entity, which is what the symbol index keys its member buckets on +// (`SymbolIndexStore.processThunkAttributeSymbol` extracts the same thing). + +/// Node kinds that carry `(context, identifier, type)` — the shape whose first +/// child is the declaration context. +/// +/// Accessor wrappers (`getter` / `setter` / `modify` / `read`) are deliberately +/// absent: their first child is the `variable` or `subscript` they wrap, not a +/// context, so they must be walked THROUGH rather than treated as the entity. +private let entityNodeKinds: Set = [ + .function, + .variable, + .subscript, + .constructor, + .allocator, + .destructor, + .deallocator, +] + +extension NodeReference { + /// The type this member symbol is declared in, or `nil` when the tree + /// carries no entity whose context can be read. + /// + /// Searched breadth-first so the OUTERMOST entity wins: a function type + /// nested in a member's signature (a closure parameter, say) is deeper + /// than the member itself and must never be mistaken for it. + public var declarationContextNode: NodeReference? { + guard let entityNode = outermostEntityNode else { return nil } + return entityNode.children.first.map { $0.unwrappingExtensionContext } + } + + private var outermostEntityNode: NodeReference? { + var queue: [NodeReference] = [self] + var queueIndex = 0 + while queueIndex < queue.count { + let candidate = queue[queueIndex] + queueIndex += 1 + if entityNodeKinds.contains(candidate.kind) { return candidate } + queue.append(contentsOf: candidate.children) + } + return nil + } + + /// An `extension` context stands for the type it extends: the extension + /// node's layout is `extension(module, extendedType, ?genericSignature)`, + /// so the extended type is the second child. Any other node is itself. + private var unwrappingExtensionContext: NodeReference { + guard kind == .extension, let extendedType = children.second else { return self } + return extendedType + } +} diff --git a/Sources/SwiftInspection/MetadataReader.swift b/Sources/SwiftInspection/MetadataReader.swift index 6c713b53..6eb6804e 100644 --- a/Sources/SwiftInspection/MetadataReader.swift +++ b/Sources/SwiftInspection/MetadataReader.swift @@ -154,17 +154,17 @@ extension MetadataReader { // MARK: - Symbol Lookup Protocol private protocol SymbolLookupContext { - func lookupSymbol(at offset: Int) -> MachOSymbols.Symbol? + func lookupSymbol(at offset: Int) -> Symbol? } extension MachOContext: SymbolLookupContext { - func lookupSymbol(at offset: Int) -> MachOSymbols.Symbol? { + func lookupSymbol(at offset: Int) -> Symbol? { try? Symbol.resolve(from: offset, in: machO) } } extension InProcessContext: SymbolLookupContext { - func lookupSymbol(at offset: Int) -> MachOSymbols.Symbol? { + func lookupSymbol(at offset: Int) -> Symbol? { guard let ptr = UnsafeRawPointer(bitPattern: offset) else { return nil } guard let result = MachOImage.symbol(for: ptr) else { return nil } return Symbol(offset: offset, name: result.1.name) diff --git a/Sources/SwiftInspection/RuntimeMetadataTypeBuilder.swift b/Sources/SwiftInspection/RuntimeMetadataTypeBuilder.swift new file mode 100644 index 00000000..f8813ad6 --- /dev/null +++ b/Sources/SwiftInspection/RuntimeMetadataTypeBuilder.swift @@ -0,0 +1,1096 @@ +import Foundation +@_spi(Internals) import Demangling +import MachOKit +import MachOFoundation +import MachOSwiftSection +import MachOSwiftSectionC +#if canImport(ObjectiveC) +import ObjectiveC +#endif + +/// In-process `TypeBuilder` conformer: decodes a demangled `Node` tree into +/// live runtime metadata, mirroring the runtime's own `DecodedMetadataBuilder` +/// (`stdlib/public/runtime/MetadataLookup.cpp`) over the same runtime entry +/// points — descriptor metadata accessors, `swift_conformsToProtocol`, +/// `swift_getTupleTypeMetadata`, `swift_getFunctionTypeMetadata`, +/// `swift_getExistentialTypeMetadata` — instead of remangling the node back to +/// a string for `swift_getTypeByMangledNameInContext`. +/// +/// Construction requests use `MetadataState.abstract` while composing (the +/// runtime does the same to stay cycle-tolerant); the public entry point +/// forces completion through `swift_checkMetadataState` before returning. +/// +/// First-version rejection surface (each returns a typed `TypeLookupError`, +/// never a fabricated value): SIL function/box types, parameter packs, value +/// generic arguments (`InlineArray<5, _>`), opaque return types, constrained +/// and extended existential shapes, and dynamic `Self`. This is the same or a +/// narrower rejection set than the runtime builder's. +public struct RuntimeMetadataTypeBuilder: TypeBuilder { + /// Absolute `(depth, index)` of a generic parameter, matching the + /// demangler's `dependentGenericParamType` coordinates. + public struct GenericParameterPosition: Hashable, Sendable { + public let depth: Int + public let index: Int + + public init(depth: Int, index: Int) { + self.depth = depth + self.index = index + } + } + + /// Substitutions for `dependentGenericParamType` nodes reached during + /// decoding. Empty by default — an unbound generic parameter then fails + /// with a typed error rather than guessing. + public var genericParameterMetadataTypes: [GenericParameterPosition: Any.Type] + + /// Resolution seam for nominal declaration nodes that carry a name rather + /// than a resolved symbolic reference. Return the type context + /// descriptor's in-process address, or `nil` to fall through to the + /// builder's own fallbacks (runtime name lookup for non-generic nominals, + /// known stdlib descriptors for the sugar types). + public var nominalTypeDescriptorResolver: (@Sendable (Node) -> UnsafeRawPointer?)? + + public init( + genericParameterMetadataTypes: [GenericParameterPosition: Any.Type] = [:], + nominalTypeDescriptorResolver: (@Sendable (Node) -> UnsafeRawPointer?)? = nil + ) { + self.genericParameterMetadataTypes = genericParameterMetadataTypes + self.nominalTypeDescriptorResolver = nominalTypeDescriptorResolver + } + + // MARK: - Public entry point + + /// Decodes `node` into complete in-process runtime metadata. + public func metadataType(for node: Node) throws(TypeLookupError) -> Any.Type { + let decoder = TypeDecoder(builder: self) + let abstractType = try decoder.decodeMangledType(node: node, forRequirement: false).get() + return try Self.completedMetadataType(of: abstractType) + } + + /// Same as `metadataType(for:)`, returning the project's `Metadata` + /// wrapper for callers composing with the inspection APIs. + public func metadata(for node: Node) throws -> Metadata { + try Metadata.createInProcess(metadataType(for: node)) + } + + private static func completedMetadataType(of type: Any.Type) throws(TypeLookupError) -> Any.Type { + let response = swift_checkMetadataState( + MetadataRequest(state: .complete, isBlocking: true).rawValue.cast(), + metadataPointer(of: type) + ) + guard let completedPointer = response.Metadata else { + throw TypeLookupError("swift_checkMetadataState returned no metadata") + } + return anyType(fromMetadataPointer: completedPointer) + } + + // MARK: - Associated types + + public typealias BuiltType = TypeLookupErrorOr + + /// A nominal declaration reached by the decoder: either a resolved type + /// context descriptor address, or the declaration node itself when only + /// the name is known (resolution then happens at metadata-construction + /// time, where the argument shape is known). + public enum NominalTypeDeclaration { + case descriptor(UnsafeRawPointer) + case named(Node) + } + + public typealias BuiltTypeDecl = NominalTypeDeclaration + public typealias BuiltProtocolDecl = ProtocolDescriptorRef + + /// Placeholder for the builder projections this conformer cannot realize + /// as runtime metadata (SIL boxes, generic signatures, requirement + /// values used only by constrained existentials). Constructible so the + /// non-throwing protocol requirements can return it; every `BuiltType` + /// composed from one fails with a typed error instead. + public struct UnsupportedProjection {} + + public typealias BuiltSILBoxField = UnsupportedProjection + public typealias BuiltSubstitution = UnsupportedProjection + public typealias BuiltRequirement = UnsupportedProjection + public typealias BuiltInverseRequirement = UnsupportedProjection + public typealias BuiltLayoutConstraint = UnsupportedProjection + public typealias BuiltGenericSignature = UnsupportedProjection + public typealias BuiltSubstitutionMap = UnsupportedProjection + + // MARK: - TypeBuilder core + + public func getManglingFlavor() -> ManglingFlavor { .default } + + public func decodeMangledType(node: Node?, forRequirement: Bool) throws(TypeLookupError) -> BuiltType { + guard let node else { throw TypeLookupError("nil node") } + let decoder = TypeDecoder(builder: self) + return try decoder.decodeMangledType(node: node, forRequirement: forRequirement) + } + + // MARK: - Type declarations + + public func createTypeDecl(node: Node, typeAlias: inout Bool) -> NominalTypeDeclaration? { + typeAlias = node.kind == .typeAlias || node.kind == .boundGenericTypeAlias + if node.kind == .typeSymbolicReference { + guard let address = node.index, let pointer = UnsafeRawPointer(bitPattern: UInt(address)) else { + return nil + } + return .descriptor(pointer) + } + if let resolvedPointer = nominalTypeDescriptorResolver?(node) { + return .descriptor(resolvedPointer) + } + return .named(node) + } + + public func createProtocolDecl(node: Node) -> ProtocolDescriptorRef? { + switch node.kind { + case .protocolSymbolicReference: + guard let address = node.index else { return nil } + return .forSwift(StoredPointer(address)) + case .objectiveCProtocolSymbolicReference: + guard let address = node.index else { return nil } + return .forObjC(StoredPointer(address)) + default: + break + } + if let resolvedPointer = nominalTypeDescriptorResolver?(node) { + return .forSwift(StoredPointer(UInt(bitPattern: resolvedPointer))) + } + // Named Swift protocol: resolve through the runtime by building the + // single-protocol existential's metadata from the node's bare type + // mangling and reading the descriptor reference back out of it. + guard let existentialType = Self.runtimeTypeByName( + of: Node.createTransient(kind: .type, child: Node.createTransient( + kind: .protocolList, + child: Node.createTransient(kind: .typeList, child: Node.createTransient(kind: .type, child: node)) + )) + ) else { return nil } + guard let existentialMetadata = try? ExistentialTypeMetadata.createInProcess(existentialType), + let reference = try? existentialMetadata.protocols().first else { + return nil + } + return reference + } + + #if canImport(ObjectiveC) + public func createObjCProtocolDecl(name: String) -> ProtocolDescriptorRef { + guard let objcProtocol = objc_getProtocol(name) else { + return .forSwift(0) + } + return .forObjC(StoredPointer(UInt(bitPattern: unsafeBitCast(objcProtocol, to: UnsafeRawPointer.self)))) + } + + public func createObjCClassType(name: String) -> BuiltType { + guard let objcClass = objc_lookUpClass(name) else { + return .failure(TypeLookupError("Objective-C class '\(name)' is not present in this process")) + } + // The canonical `Any.Type` for an Objective-C class is the realized + // class object itself (`NSObject.self` bitcasts to the class + // pointer); `swift_getObjCClassMetadata`'s wrapper is a distinct + // metadata identity and would split generic instantiation caches. + let realizedClass = swift_getInitializedObjCClass(objcClass) + return .success(Self.anyType(fromMetadataPointer: unsafeBitCast(realizedClass, to: UnsafeRawPointer.self))) + } + + public func createBoundGenericObjCClassType(name: String, args: [BuiltType]) -> BuiltType { + // Generic arguments of lightweight Objective-C generic classes are + // not reified in metadata — same as the runtime builder. + createObjCClassType(name: name) + } + #endif + + // MARK: - Nominal types + + public func createNominalType(typeDecl: NominalTypeDeclaration, parent: BuiltType?) -> BuiltType { + createBoundGenericType(typeDecl: typeDecl, args: [], parent: parent) + } + + public func createTypeAliasType(typeDecl: NominalTypeDeclaration, parent: BuiltType?) -> BuiltType { + // No way to resolve a typealias's underlying type from a binary; some + // CF types are mangled as typealiases, so treat it as nominal (the + // runtime builder does the same). + createNominalType(typeDecl: typeDecl, parent: parent) + } + + public func createBoundGenericType(typeDecl: NominalTypeDeclaration, args: [BuiltType], parent: BuiltType?) -> BuiltType { + let ownArguments: [Any.Type] + switch Self.unwrap(args) { + case .success(let unwrapped): ownArguments = unwrapped + case .failure(let error): return .failure(error) + } + let parentType: Any.Type? + switch parent { + case .none: parentType = nil + case .success(let type): parentType = type + case .failure(let error): return .failure(error) + } + + switch typeDecl { + case .descriptor(let descriptorPointer): + return buildNominalMetadataType( + descriptorPointer: descriptorPointer, + ownArguments: ownArguments, + parentType: parentType + ) + case .named(let node): + if ownArguments.isEmpty, parentType == nil { + // A concrete non-generic nominal resolves through the + // runtime's own name lookup, which searches every loaded + // image's type section with its conformance caches. + guard let resolvedType = Self.runtimeTypeByName(of: Node.createTransient(kind: .type, child: node)) else { + return .failure(TypeLookupError(node: node, message: "runtime name lookup found no type descriptor")) + } + return .success(resolvedType) + } + // A named generic declaration needs its descriptor. The standard + // library's generic types come from textual manglings (`Sa`, + // `SD`, …), so their descriptors are kept on hand. + if let qualifiedName = Self.qualifiedDeclarationName(of: node), + let descriptorPointer = Self.standardLibraryGenericDescriptorPointersByQualifiedName[qualifiedName] { + return buildNominalMetadataType( + descriptorPointer: descriptorPointer, + ownArguments: ownArguments, + parentType: parentType + ) + } + return .failure(TypeLookupError( + node: node, + message: "cannot resolve a named generic declaration to its descriptor; supply nominalTypeDescriptorResolver" + )) + } + } + + private func buildNominalMetadataType( + descriptorPointer: UnsafeRawPointer, + ownArguments: [Any.Type], + parentType: Any.Type? + ) -> BuiltType { + let contextWrapper: ContextDescriptorWrapper + do { + contextWrapper = try ContextDescriptorWrapper.resolve(from: descriptorPointer) + } catch { + return .failure(TypeLookupError("cannot read context descriptor: \(error)")) + } + + // A protocol descriptor in type position denotes the bare existential + // (the runtime builder's `_getSimpleProtocolTypeMetadata`). + if contextWrapper.isProtocol { + var protocolReferences = [ProtocolDescriptorRef.forSwift(StoredPointer(UInt(bitPattern: descriptorPointer)))] + return Self.existentialMetadataType( + protocolReferences: &protocolReferences, + superclassType: nil, + isClassBound: false + ) + } + + guard let typeWrapper = contextWrapper.typeContextDescriptorWrapper else { + return .failure(TypeLookupError("context descriptor at \(descriptorPointer) is not a type context")) + } + + let genericContext: GenericContext? + do { + genericContext = try typeWrapper.genericContext() + } catch { + return .failure(TypeLookupError("cannot read generic context: \(error)")) + } + + var keyMetadataTypes: [Any.Type] = [] + var witnessTables: [ProtocolWitnessTable] = [] + + if let genericContext { + switch keyArguments(of: genericContext, ownArguments: ownArguments, parentType: parentType) { + case .success(let gathered): + keyMetadataTypes = gathered.metadataTypes + witnessTables = gathered.witnessTables + case .failure(let error): + return .failure(error) + } + } else if !ownArguments.isEmpty { + return .failure(TypeLookupError("generic arguments supplied to the non-generic declaration at \(descriptorPointer)")) + } + + do { + guard let accessorFunction = try typeWrapper.typeContextDescriptor.metadataAccessorFunction() else { + return .failure(TypeLookupError("type context descriptor at \(descriptorPointer) has no metadata accessor")) + } + let keyMetadatas = try keyMetadataTypes.map { try Metadata.createInProcess($0) } + let response = try accessorFunction( + request: MetadataRequest(state: .abstract, isBlocking: false), + metadatas: keyMetadatas, + witnessTables: witnessTables + ) + let responseMetadata = try response.value.resolve() + return .success(Self.anyType(fromMetadataPointer: try responseMetadata.metadata.asPointer)) + } catch { + return .failure(TypeLookupError("metadata accessor invocation failed: \(error)")) + } + } + + /// The gathered key-argument buffer contents for one generic context, in + /// the runtime's canonical order: every key type parameter's metadata + /// first, then one witness table per key protocol requirement in + /// requirement order — the `_gatherGenericParameters` + + /// `_checkGenericRequirements` shape. + private struct GatheredKeyArguments { + var metadataTypes: [Any.Type] + var witnessTables: [ProtocolWitnessTable] + } + + private func keyArguments( + of genericContext: GenericContext, + ownArguments: [Any.Type], + parentType: Any.Type? + ) -> TypeLookupErrorOr { + let cumulativeParameters = genericContext.parameters + + if let unsupportedParameter = cumulativeParameters.first(where: { $0.kind != .type }) { + return .failure(TypeLookupError("generic parameter kind \(unsupportedParameter.kind) (pack or value) is not supported yet")) + } + + // Written arguments cover the whole cumulative parameter list. Either + // the caller supplied only the innermost level (parent metadata fills + // the outer levels), or the complete flattened list with no parent — + // the same two acceptable shapes as `_gatherGenericParameters`. + var writtenArguments: [Any.Type] + if ownArguments.count == genericContext.currentParameters.count { + switch Self.writtenGenericArguments(ofParent: parentType) { + case .success(let parentArguments): + writtenArguments = parentArguments + ownArguments + case .failure(let error): + return .failure(error) + } + } else if ownArguments.count == cumulativeParameters.count, parentType == nil { + writtenArguments = ownArguments + } else { + return .failure(TypeLookupError( + "incorrect number of generic arguments: have \(ownArguments.count), context declares \(genericContext.currentParameters.count) local / \(cumulativeParameters.count) total" + )) + } + + guard writtenArguments.count == cumulativeParameters.count else { + return .failure(TypeLookupError( + "written generic arguments (\(writtenArguments.count)) do not cover the cumulative parameter list (\(cumulativeParameters.count))" + )) + } + + var gathered = GatheredKeyArguments(metadataTypes: [], witnessTables: []) + for (parameter, argument) in zip(cumulativeParameters, writtenArguments) where parameter.hasKeyArgument { + gathered.metadataTypes.append(argument) + } + + // Witness tables for the key protocol requirements. Requirement + // subjects (`A`, `A.Element`, …) decode through a nested builder + // whose bindings are the written arguments. + var subjectBuilder = self + subjectBuilder.genericParameterMetadataTypes = Self.bindings( + of: genericContext, + writtenArguments: writtenArguments + ) + + for requirementDescriptor in genericContext.requirements { + let flags = requirementDescriptor.layout.flags + guard flags.kind == .protocol, flags.contains(.hasKeyArgument) else { continue } + + let requirement: GenericRequirement + do { + requirement = try GenericRequirement(descriptor: requirementDescriptor) + } catch { + return .failure(TypeLookupError("cannot read generic requirement: \(error)")) + } + guard case .protocol(let protocolReference) = requirement.content, + let resolvedProtocol = protocolReference.resolved, + let protocolDescriptor = resolvedProtocol.swift else { + return .failure(TypeLookupError("key protocol requirement's descriptor did not resolve")) + } + + let subjectNode: Node + do { + subjectNode = try MetadataReader.demangleType(for: requirement.paramManagledName) + } catch { + return .failure(TypeLookupError("cannot demangle requirement subject: \(error)")) + } + + let subjectType: Any.Type + switch Result(catching: { () throws(TypeLookupError) in + try subjectBuilder.decodeMangledType(node: subjectNode, forRequirement: true).get() + }) { + case .success(let type): subjectType = type + case .failure(let error): return .failure(TypeLookupError("cannot resolve requirement subject: \(error)")) + } + + do { + guard let witnessTable = try RuntimeFunctions.conformsToProtocol( + metadata: Metadata.createInProcess(subjectType), + protocolDescriptor: protocolDescriptor + ) else { + return .failure(TypeLookupError("\(subjectType) does not conform to the required protocol")) + } + gathered.witnessTables.append(witnessTable) + } catch { + return .failure(TypeLookupError("conformance lookup failed: \(error)")) + } + } + + // The same final honesty check the runtime performs: the accessor's + // argument buffer must hold exactly the declared key-argument count — + // a mismatch means a requirement kind this builder does not model. + let declaredKeyArgumentCount = Int(genericContext.header.layout.numKeyArguments) + let gatheredKeyArgumentCount = gathered.metadataTypes.count + gathered.witnessTables.count + guard gatheredKeyArgumentCount == declaredKeyArgumentCount else { + return .failure(TypeLookupError( + "gathered \(gatheredKeyArgumentCount) key arguments but the generic context declares \(declaredKeyArgumentCount)" + )) + } + + return .success(gathered) + } + + /// Maps every cumulative generic parameter position to its written + /// argument, using the per-level "newly introduced" counts so `(depth, + /// index)` matches the demangler's coordinates. + private static func bindings( + of genericContext: GenericContext, + writtenArguments: [Any.Type] + ) -> [GenericParameterPosition: Any.Type] { + var perLevelCounts: [Int] = [] + var previousCumulativeCount = 0 + for parentLevel in genericContext.parentParameters { + perLevelCounts.append(parentLevel.count - previousCumulativeCount) + previousCumulativeCount = parentLevel.count + } + perLevelCounts.append(genericContext.currentParameters.count) + + var result: [GenericParameterPosition: Any.Type] = [:] + var flatIndex = 0 + for (depth, count) in perLevelCounts.enumerated() { + for indexInLevel in 0 ..< count { + guard flatIndex < writtenArguments.count else { return result } + result[GenericParameterPosition(depth: depth, index: indexInLevel)] = writtenArguments[flatIndex] + flatIndex += 1 + } + } + return result + } + + /// Reads a parent context's written generic arguments back out of its + /// live metadata (the outer levels of a nested instantiation). `nil` or a + /// non-generic parent contributes nothing. + private static func writtenGenericArguments(ofParent parentType: Any.Type?) -> TypeLookupErrorOr<[Any.Type]> { + guard let parentType else { return .success([]) } + do { + let parentMetadata = try Metadata.createInProcess(parentType) + guard let parentWrapper = try parentMetadata.typeContextDescriptorWrapper() else { + return .success([]) + } + guard let parentGenericContext = try parentWrapper.genericContext() else { + return .success([]) + } + // Reconstructing written arguments for non-key parameters (ones a + // same-type constraint erased from the key buffer) needs the + // requirement solver the runtime hides in + // `_gatherWrittenGenericParameters`; reject rather than guess. + if let nonKeyParameter = parentGenericContext.parameters.first(where: { !$0.hasKeyArgument || $0.kind != .type }) { + return .failure(TypeLookupError("parent context has a non-key or non-type generic parameter (\(nonKeyParameter.kind)); not supported yet")) + } + + let argumentCount = parentGenericContext.parameters.count + let argumentsPointer = try genericArgumentsPointer(ofParentMetadata: parentMetadata) + let arguments = (0 ..< argumentCount).map { argumentIndex in + anyType(fromMetadataPointer: argumentsPointer.load( + fromByteOffset: argumentIndex * MemoryLayout.size, + as: UnsafeRawPointer.self + )) + } + return .success(arguments) + } catch { + return .failure(TypeLookupError("cannot read parent generic arguments: \(error)")) + } + } + + private static func genericArgumentsPointer(ofParentMetadata parentMetadata: Metadata) throws -> UnsafeRawPointer { + let metadataPointer = try parentMetadata.asPointer + switch parentMetadata.kind { + case .struct, .enum, .optional: + return metadataPointer.advanced(by: MemoryLayout.size) + case .class: + let classMetadata = try ClassMetadataObjCInterop.resolve(from: metadataPointer) + guard let classDescriptor = try classMetadata.descriptor() else { + throw TypeLookupError("parent class metadata has no Swift descriptor") + } + let immediateMembersOffsetInWords: Int + if classDescriptor.hasResilientSuperclass { + let bounds = try classDescriptor.resilientMetadataBounds() + immediateMembersOffsetInWords = Int(bounds.layout.immediateMembersOffset) / MemoryLayout.size + } else { + immediateMembersOffsetInWords = Int(classDescriptor.nonResilientImmediateMembersOffset) + } + return metadataPointer.advanced(by: immediateMembersOffsetInWords * MemoryLayout.size) + default: + throw TypeLookupError("parent metadata kind \(parentMetadata.kind) cannot carry generic arguments") + } + } + + // MARK: - Generic parameters + + public func createGenericTypeParameterType(depth: Int, index: Int) -> BuiltType { + guard let boundType = genericParameterMetadataTypes[GenericParameterPosition(depth: depth, index: index)] else { + return .failure(TypeLookupError("generic parameter (depth \(depth), index \(index)) has no bound metadata")) + } + return .success(boundType) + } + + public func createDependentMemberType(member: String, base: BuiltType) -> BuiltType { + .failure(TypeLookupError("unbound dependent member type '\(member)' cannot be resolved")) + } + + public func createDependentMemberType(member: String, base: BuiltType, protocol protocolDecl: ProtocolDescriptorRef) -> BuiltType { + let baseType: Any.Type + switch base { + case .success(let type): baseType = type + case .failure(let error): return .failure(error) + } + if protocolDecl.isObjC { + return .failure(TypeLookupError("associated type '\(member)' of an Objective-C protocol cannot be resolved")) + } + do { + let protocolDescriptor = try protocolDecl.swiftProtocol() + let declaredProtocol = try MachOSwiftSection.Protocol(descriptor: protocolDescriptor) + let associatedTypeNames = try protocolDescriptor.associatedTypes() + guard let associatedTypeIndex = associatedTypeNames.firstIndex(of: member) else { + return .failure(TypeLookupError("protocol declares no associated type named '\(member)'")) + } + guard let baseRequirement = declaredProtocol.baseRequirement else { + return .failure(TypeLookupError("protocol has no requirement base descriptor")) + } + let accessFunctionRequirements = declaredProtocol.requirements.filter { + $0.flags.kind.isAssociatedTypeAccessFunction + } + guard associatedTypeIndex < accessFunctionRequirements.count else { + return .failure(TypeLookupError("associated type index \(associatedTypeIndex) exceeds the requirement list")) + } + let baseMetadata = try Metadata.createInProcess(baseType) + guard let witnessTable = try RuntimeFunctions.conformsToProtocol( + metadata: baseMetadata, + protocolDescriptor: protocolDescriptor + ) else { + return .failure(TypeLookupError("\(baseType) does not conform to the protocol declaring '\(member)'")) + } + let response = try RuntimeFunctions.getAssociatedTypeWitness( + request: MetadataRequest(state: .abstract, isBlocking: false), + protocolWitnessTable: witnessTable, + conformingTypeMetadata: baseMetadata, + baseRequirement: baseRequirement, + associatedTypeRequirement: accessFunctionRequirements[associatedTypeIndex] + ) + let witnessMetadata = try response.value.resolve().metadata + return .success(Self.anyType(fromMetadataPointer: try witnessMetadata.asPointer)) + } catch { + return .failure(TypeLookupError("associated type witness resolution failed: \(error)")) + } + } + + // MARK: - Metatypes + + public func createMetatypeType(instance: BuiltType, repr: ImplMetatypeRepresentation?) -> BuiltType { + instance.map { instanceType in + Self.anyType(fromMetadataPointer: swift_getMetatypeMetadata(Self.metadataPointer(of: instanceType))) + } + } + + public func createExistentialMetatypeType(instance: BuiltType, repr: ImplMetatypeRepresentation?) -> BuiltType { + let instanceType: Any.Type + switch instance { + case .success(let type): instanceType = type + case .failure(let error): return .failure(error) + } + let instanceKind = (try? Metadata.createInProcess(instanceType).kind) ?? .opaque + guard instanceKind == .existential || instanceKind == .existentialMetatype else { + return .failure(TypeLookupError("existential metatype instance is neither an existential nor an existential metatype")) + } + return .success(Self.anyType(fromMetadataPointer: swift_getExistentialMetatypeMetadata(Self.metadataPointer(of: instanceType)))) + } + + // MARK: - Existentials + + public func createProtocolCompositionType(protocols: [ProtocolDescriptorRef], superclass: BuiltType?, isClassBound: Bool, forRequirement: Bool) -> BuiltType { + let superclassType: Any.Type? + switch superclass { + case .none: superclassType = nil + case .success(let type): superclassType = type + case .failure(let error): return .failure(error) + } + guard !protocols.contains(where: { $0.storage == 0 }) else { + return .failure(TypeLookupError("a protocol in the composition did not resolve")) + } + var protocolReferences = protocols + return Self.existentialMetadataType( + protocolReferences: &protocolReferences, + superclassType: superclassType, + isClassBound: isClassBound + ) + } + + public func createProtocolCompositionType(protocol protocolDecl: ProtocolDescriptorRef, superclass: BuiltType?, isClassBound: Bool, forRequirement: Bool) -> BuiltType { + createProtocolCompositionType(protocols: [protocolDecl], superclass: superclass, isClassBound: isClassBound, forRequirement: forRequirement) + } + + private static func existentialMetadataType( + protocolReferences: inout [ProtocolDescriptorRef], + superclassType: Any.Type?, + isClassBound: Bool + ) -> BuiltType { + var classConstraint = ProtocolClassConstraint.any + if isClassBound || superclassType != nil { + classConstraint = .class + } else { + for reference in protocolReferences where classConstraint == .any { + if reference.isObjC { + classConstraint = .class + } else if let descriptor = try? reference.swiftProtocol(), + descriptor.flags.kindSpecificFlags?.protocolFlags?.classConstraint == .class { + classConstraint = .class + } + } + } + let superclassPointer = superclassType.map { metadataPointer(of: $0) } + // The runtime sorts the protocol array in place; hand it scratch storage. + let existentialPointer = protocolReferences.withUnsafeMutableBufferPointer { buffer in + swift_getExistentialTypeMetadata( + classConstraint.rawValue, + superclassPointer, + buffer.count, + buffer.baseAddress + ) + } + guard let existentialPointer else { + return .failure(TypeLookupError("swift_getExistentialTypeMetadata returned nil")) + } + return .success(anyType(fromMetadataPointer: existentialPointer)) + } + + public func createConstrainedExistentialType(base: BuiltType, requirements: [UnsupportedProjection], inverseRequirements: [UnsupportedProjection]) -> BuiltType { + // The runtime builder rejects these too ("FIXME: Runtime plumbing"). + .failure(TypeLookupError("constrained existential types are not supported")) + } + + public func createSymbolicExtendedExistentialType(shapeNode: Node, args: [BuiltType]) -> BuiltType { + .failure(TypeLookupError("extended existential shapes are not supported yet")) + } + + // MARK: - Functions + + public func createFunctionType( + parameters: [FunctionParam], + result: BuiltType, + flags: Demangling.FunctionTypeFlags, + extFlags: ExtendedFunctionTypeFlags, + diffKind: FunctionMetadataDifferentiabilityKind, + globalActorType: BuiltType?, + thrownErrorType: BuiltType? + ) -> BuiltType { + let resultType: Any.Type + switch result { + case .success(let type): resultType = type + case .failure(let error): return .failure(error) + } + + var parameterPointers: [UnsafeRawPointer?] = [] + var parameterFlags: [UInt32] = [] + parameterPointers.reserveCapacity(parameters.count) + for parameter in parameters { + switch parameter.type { + case .success(let parameterType): + parameterPointers.append(Self.metadataPointer(of: parameterType)) + case .failure(let error): + return .failure(error) + case .none: + return .failure(TypeLookupError("function parameter carries no type")) + } + parameterFlags.append(parameter.flags.rawValue) + } + + let globalActor: Any.Type? + switch globalActorType { + case .none: globalActor = nil + case .success(let type): globalActor = type + case .failure(let error): return .failure(error) + } + let thrownError: Any.Type? + switch thrownErrorType { + case .none: thrownError = nil + case .success(let type): thrownError = type + case .failure(let error): return .failure(error) + } + + let needsExtendedEntryPoint = extFlags.rawValue != 0 || diffKind.isDifferentiable || globalActor != nil || thrownError != nil + + let functionMetadataPointer: UnsafeRawPointer? = parameterPointers.withUnsafeBufferPointer { parameterBuffer in + parameterFlags.withUnsafeBufferPointer { flagsBuffer in + let flagsPointer = flags.hasParameterFlags ? flagsBuffer.baseAddress : nil + if needsExtendedEntryPoint { + guard hasExtendedFunctionMetadataEntryPoint else { return nil } + return swift_getExtendedFunctionTypeMetadata( + Int(flags.rawValue).cast(), + Self.runtimeDifferentiabilityKindValue(of: diffKind), + parameterBuffer.baseAddress, + flagsPointer, + Self.metadataPointer(of: resultType), + globalActor.map { Self.metadataPointer(of: $0) }, + extFlags.rawValue, + thrownError.map { Self.metadataPointer(of: $0) } + ) + } + return swift_getFunctionTypeMetadata( + Int(flags.rawValue).cast(), + parameterBuffer.baseAddress, + flagsPointer, + Self.metadataPointer(of: resultType) + ) + } + } + guard let functionMetadataPointer else { + return .failure(TypeLookupError( + needsExtendedEntryPoint + ? "this Swift runtime has no swift_getExtendedFunctionTypeMetadata (needed for global actor / typed throws / differentiability)" + : "swift_getFunctionTypeMetadata returned nil" + )) + } + return .success(Self.anyType(fromMetadataPointer: functionMetadataPointer)) + } + + private var hasExtendedFunctionMetadataEntryPoint: Bool { + // The symbol is weak-imported; an older runtime does not have it. + dlsym(UnsafeMutableRawPointer(bitPattern: -2) /* RTLD_DEFAULT */, "swift_getExtendedFunctionTypeMetadata") != nil + } + + private static func runtimeDifferentiabilityKindValue(of diffKind: FunctionMetadataDifferentiabilityKind) -> Int { + switch diffKind { + case .nonDifferentiable: return 0 + case .forward: return 1 + case .reverse: return 2 + case .normal: return 3 + case .linear: return 4 + } + } + + public func createImplFunctionType( + calleeConvention: ImplParameterConvention, + coroutineKind: ImplCoroutineKind, + parameters: [ImplFunctionParam], + yields: [ImplFunctionYield], + results: [ImplFunctionResult], + errorResult: ImplFunctionResult?, + flags: ImplFunctionTypeFlags + ) -> BuiltType { + .failure(TypeLookupError("SIL function types have no runtime metadata")) + } + + // MARK: - Tuples and packs + + public func createTupleType(elements: [BuiltType], labels: [String?]) -> BuiltType { + let elementTypes: [Any.Type] + switch Self.unwrap(elements) { + case .success(let unwrapped): elementTypes = unwrapped + case .failure(let error): return .failure(error) + } + // The runtime unwraps unlabeled one-element tuples to the element. + if elementTypes.count == 1, labels.first ?? nil == nil { + return .success(elementTypes[0]) + } + + var labelsString = "" + for (labelIndex, label) in labels.enumerated() { + guard let label, !label.isEmpty else { + if !labelsString.isEmpty { labelsString.append(" ") } + continue + } + if labelsString.isEmpty { + labelsString.append(String(repeating: " ", count: labelIndex)) + } + labelsString.append(label) + labelsString.append(" ") + } + + let tupleFlagsNonConstantLabelsMask = 0x10000 + let tupleFlags = elementTypes.count | (labelsString.isEmpty ? 0 : tupleFlagsNonConstantLabelsMask) + + let elementPointers: [UnsafeRawPointer?] = elementTypes.map { Self.metadataPointer(of: $0) } + let response = elementPointers.withUnsafeBufferPointer { elementBuffer in + labelsString.isEmpty + ? swift_getTupleTypeMetadata( + MetadataRequest(state: .abstract, isBlocking: false).rawValue.cast(), + tupleFlags.cast(), + elementBuffer.baseAddress, + nil, + nil + ) + : labelsString.withCString { labelsPointer in + swift_getTupleTypeMetadata( + MetadataRequest(state: .abstract, isBlocking: false).rawValue.cast(), + tupleFlags.cast(), + elementBuffer.baseAddress, + labelsPointer, + nil + ) + } + } + guard let tuplePointer = response.Metadata else { + return .failure(TypeLookupError("swift_getTupleTypeMetadata returned nil")) + } + return .success(Self.anyType(fromMetadataPointer: tuplePointer)) + } + + public func createPackType(elements: [BuiltType]) -> BuiltType { + .failure(TypeLookupError("parameter packs are not supported yet")) + } + + public func createSILPackType(elements: [BuiltType], isElementAddress: Bool) -> BuiltType { + .failure(TypeLookupError("lowered SIL pack types cannot be built")) + } + + public func createExpandedPackElement(type: BuiltType) -> BuiltType { + type + } + + public func beginPackExpansion(countType: BuiltType) -> Int { + 0 + } + + public func advancePackExpansion(index: Int) {} + + public func endPackExpansion() {} + + public func pushGenericParams(parameterPacks: [(Int, Int)]) {} + + public func popGenericParams() {} + + // MARK: - Reference storage + + public func createUnownedStorageType(base: BuiltType) -> BuiltType { + // Reference-storage qualifiers do not change metadata identity; the + // runtime builder records ownership out of band and returns the base. + base + } + + public func createUnmanagedStorageType(base: BuiltType) -> BuiltType { + base + } + + public func createWeakStorageType(base: BuiltType) -> BuiltType { + base + } + + // MARK: - SIL boxes + + public func createSILBoxField(type: BuiltType, isMutable: Bool) -> UnsupportedProjection { + UnsupportedProjection() + } + + public func createSILBoxType(base: BuiltType) -> BuiltType { + .failure(TypeLookupError("SIL box types have no runtime metadata")) + } + + public func createSILBoxTypeWithLayout( + fields: [UnsupportedProjection], + substitutions: [UnsupportedProjection], + requirements: [UnsupportedProjection], + inverseRequirements: [UnsupportedProjection] + ) -> BuiltType { + .failure(TypeLookupError("SIL box types have no runtime metadata")) + } + + // MARK: - Special types + + public func createDynamicSelfType(base: BuiltType) -> BuiltType { + .failure(TypeLookupError("dynamic Self cannot appear in a free-standing type")) + } + + public func resolveOpaqueType(descriptor: Node, genericArgs: [ArraySlice], ordinal: UInt64) -> BuiltType { + .failure(TypeLookupError("opaque return types are not supported yet")) + } + + public func createBuiltinType(name: String, mangledName: String) -> BuiltType { + // Builtin metadata is exported as "$sN" symbols from the + // runtime (the runtime builder reads the same table statically). + guard let metadataAddress = dlsym(UnsafeMutableRawPointer(bitPattern: -2) /* RTLD_DEFAULT */, "$s\(mangledName)N") else { + return .failure(TypeLookupError("builtin type '\(name)' has no exported metadata symbol")) + } + return .success(Self.anyType(fromMetadataPointer: UnsafeRawPointer(metadataAddress))) + } + + // MARK: - Sugar + + public func createOptionalType(base: BuiltType) -> BuiltType { + boundGenericStandardLibraryType(of: Optional.self, arguments: [base]) + } + + public func createArrayType(element: BuiltType) -> BuiltType { + boundGenericStandardLibraryType(of: [Int].self, arguments: [element]) + } + + public func createDictionaryType(key: BuiltType, value: BuiltType) -> BuiltType { + boundGenericStandardLibraryType(of: [Int: Int].self, arguments: [key, value]) + } + + public func createInlineArrayType(count: BuiltType, element: BuiltType) -> BuiltType { + .failure(TypeLookupError("InlineArray needs a value generic argument; not supported yet")) + } + + private func boundGenericStandardLibraryType(of instantiation: Any.Type, arguments: [BuiltType]) -> BuiltType { + let descriptorPointer: UnsafeRawPointer + do { + guard let wrapper = try Metadata.createInProcess(instantiation).typeContextDescriptorWrapper() else { + return .failure(TypeLookupError("cannot locate the standard library descriptor via \(instantiation)")) + } + descriptorPointer = try wrapper.typeContextDescriptor.asPointer + } catch { + return .failure(TypeLookupError("cannot locate the standard library descriptor via \(instantiation): \(error)")) + } + return createBoundGenericType(typeDecl: .descriptor(descriptorPointer), args: arguments, parent: nil) + } + + // MARK: - Integer generic arguments + + public func createIntegerType(value: Int) -> BuiltType { + .failure(TypeLookupError("value generic arguments are not supported yet")) + } + + public func createNegativeIntegerType(value: Int) -> BuiltType { + .failure(TypeLookupError("value generic arguments are not supported yet")) + } + + public func createBuiltinFixedArrayType(size: BuiltType, element: BuiltType) -> BuiltType { + .failure(TypeLookupError("Builtin.FixedArray needs a value generic argument; not supported yet")) + } + + // MARK: - Requirements (constrained existentials only; rejected above) + + public func createRequirement(kind: RequirementKind, subjectType: BuiltType, constraintType: BuiltType) -> UnsupportedProjection { + UnsupportedProjection() + } + + public func createRequirement(kind: RequirementKind, subjectType: BuiltType, layout: UnsupportedProjection) -> UnsupportedProjection { + UnsupportedProjection() + } + + public func createSubstitution(firstType: BuiltType, secondType: BuiltType) -> UnsupportedProjection { + UnsupportedProjection() + } + + public func createInverseRequirement(subjectType: BuiltType, kind: Demangling.InvertibleProtocolKind) -> UnsupportedProjection { + UnsupportedProjection() + } + + public func getLayoutConstraint(kind: LayoutConstraintKind) -> UnsupportedProjection { + UnsupportedProjection() + } + + public func getLayoutConstraintWithSizeAlign(kind: LayoutConstraintKind, size: Int, alignment: Int) -> UnsupportedProjection { + UnsupportedProjection() + } + + // MARK: - Queries + + public func isExistential(type: BuiltType) -> Bool { + guard case .success(let builtType) = type, + let kind = try? Metadata.createInProcess(builtType).kind else { + return false + } + return kind == .existential || kind == .existentialMetatype + } + + // MARK: - Shared helpers + + private static func unwrap(_ builtTypes: [BuiltType]) -> TypeLookupErrorOr<[Any.Type]> { + var unwrapped: [Any.Type] = [] + unwrapped.reserveCapacity(builtTypes.count) + for builtType in builtTypes { + switch builtType { + case .success(let type): unwrapped.append(type) + case .failure(let error): return .failure(error) + } + } + return .success(unwrapped) + } + + private static func metadataPointer(of type: Any.Type) -> UnsafeRawPointer { + unsafeBitCast(type, to: UnsafeRawPointer.self) + } + + private static func anyType(fromMetadataPointer pointer: UnsafeRawPointer) -> Any.Type { + unsafeBitCast(pointer, to: Any.Type.self) + } + + /// Runtime name lookup: remangles `typeNode` to its bare type mangling and + /// asks `swift_getTypeByMangledNameInEnvironment` — the entry behind + /// `_typeByName`, covering every image the runtime has registered. + private static func runtimeTypeByName(of typeNode: Node) -> Any.Type? { + guard let mangledSymbol = try? mangleAsString(typeNode) else { return nil } + var bareTypeMangling = Substring(mangledSymbol) + if let prefixLength = manglingPrefixLength(of: mangledSymbol), prefixLength > 0 { + bareTypeMangling = bareTypeMangling.dropFirst(prefixLength) + } + var utf8Bytes = Array(bareTypeMangling.utf8) + return utf8Bytes.withUnsafeMutableBufferPointer { buffer -> Any.Type? in + guard let baseAddress = buffer.baseAddress else { return nil } + return baseAddress.withMemoryRebound(to: CChar.self, capacity: buffer.count) { characterPointer in + guard let resolved = swift_getTypeByMangledNameInEnvironment(characterPointer, buffer.count, nil, nil) else { + return nil + } + return anyType(fromMetadataPointer: resolved) + } + } + } + + private static func manglingPrefixLength(of mangledSymbol: String) -> Int? { + for prefix in ["$s", "_$s", "$S", "_$S", "$e", "_$e"] where mangledSymbol.hasPrefix(prefix) { + return prefix.count + } + return nil + } + + /// `Module.Name` of a depth-1 nominal declaration node; `nil` for nested + /// declarations (their resolution goes through the resolver seam). + private static func qualifiedDeclarationName(of node: Node) -> String? { + guard node.children.count >= 2, + let moduleNode = node.children.first, moduleNode.kind == .module, + let moduleName = moduleNode.text else { return nil } + for child in node.children.dropFirst() where child.kind == .identifier { + if let declarationName = child.text { + return "\(moduleName).\(declarationName)" + } + } + return nil + } + + /// Descriptors of the standard library's common generic types, located + /// through an arbitrary known instantiation's metadata — the same trick + /// the sugar paths use, cached process-wide. + private static nonisolated(unsafe) let standardLibraryGenericDescriptorPointersByQualifiedName: [String: UnsafeRawPointer] = { + let knownInstantiations: [String: Any.Type] = [ + "Swift.Array": [Int].self, + "Swift.ContiguousArray": ContiguousArray.self, + "Swift.ArraySlice": ArraySlice.self, + "Swift.Dictionary": [Int: Int].self, + "Swift.Set": Set.self, + "Swift.Optional": Int?.self, + "Swift.Range": Range.self, + "Swift.ClosedRange": ClosedRange.self, + "Swift.PartialRangeFrom": PartialRangeFrom.self, + "Swift.PartialRangeUpTo": PartialRangeUpTo.self, + "Swift.PartialRangeThrough": PartialRangeThrough.self, + "Swift.Result": Result.self, + "Swift.Unmanaged": Unmanaged.self, + "Swift.UnsafePointer": UnsafePointer.self, + "Swift.UnsafeMutablePointer": UnsafeMutablePointer.self, + "Swift.UnsafeBufferPointer": UnsafeBufferPointer.self, + "Swift.UnsafeMutableBufferPointer": UnsafeMutableBufferPointer.self, + ] + var descriptorPointers: [String: UnsafeRawPointer] = [:] + for (qualifiedName, instantiation) in knownInstantiations { + guard let wrapper = try? Metadata.createInProcess(instantiation).typeContextDescriptorWrapper(), + let descriptorPointer = try? wrapper.typeContextDescriptor.asPointer else { continue } + descriptorPointers[qualifiedName] = descriptorPointer + } + return descriptorPointers + }() +} diff --git a/Sources/SwiftInterface/AnySwiftEvolutionInterfaceBuilder.swift b/Sources/SwiftInterface/AnySwiftEvolutionInterfaceBuilder.swift index 1e0272d1..81c116e1 100644 --- a/Sources/SwiftInterface/AnySwiftEvolutionInterfaceBuilder.swift +++ b/Sources/SwiftInterface/AnySwiftEvolutionInterfaceBuilder.swift @@ -6,6 +6,7 @@ import SwiftDiffing import MachOSwiftSection import Semantic import SwiftStdlibToolbox +import MachOSymbols /// Renders one module's ABI across N ≥ 2 ordered binary versions as a single /// **union interface with lifecycle annotations** — the N-way, human-readable @@ -45,17 +46,29 @@ public final class AnySwiftEvolutionInterfaceBuilder: Sendable { /// Homogeneous construction: N versions of the same reader type, count /// decided at runtime. /// + /// `eventHandlers` are shared by every version; `eventHandlersPerVersion` + /// adds handlers built for one version (index and label), so a host can + /// attribute the diagnostics of concurrently prepared versions — the CLI + /// attaches a `ConsoleEventHandler(label:)` per version through it. + /// Handler invocation is serialized process-wide by the dispatcher, so a + /// shared handler is never called concurrently. + /// /// - Throws: `ABIEvolutionError.fewerThanTwoVersions` / /// `.labelCountMismatch` on invalid input shapes. public init( configuration: SwiftDeclarationIndexConfiguration = .init(), eventHandlers: [SwiftIndexEvents.Handler] = [], + eventHandlersPerVersion: ((_ versionIndex: Int, _ label: String) -> [SwiftIndexEvents.Handler])? = nil, versions: [MachO], labels: [String] ) throws { try Self.validate(versionCount: versions.count, labelCount: labels.count) - self.versionUnits = versions.map { - InterfaceVersionUnit(configuration: configuration, eventHandlers: eventHandlers, machO: $0) + self.versionUnits = versions.enumerated().map { versionIndex, machO in + InterfaceVersionUnit( + configuration: configuration, + eventHandlers: eventHandlers + (eventHandlersPerVersion?(versionIndex, labels[versionIndex]) ?? []), + machO: machO + ) } self.labels = labels } @@ -93,13 +106,32 @@ public final class AnySwiftEvolutionInterfaceBuilder: Sendable { /// Indexes every version (full member indexing included), freezes each /// into a snapshot, and builds the `ABIEvolution` annotation matrix. Must /// complete before any rendering entry point. - public func prepare() async throws { - for versionUnit in versionUnits { - try await versionUnit.prepare() + /// + /// Versions index **concurrently**, at most `maximumConcurrentPreparations` + /// at a time (evolution proposal + /// `large-stack-executor-and-cross-version-parallelism`): each version is + /// a different file whose caches key on its own UUID and whose descriptor + /// reads go through a memory mapping, so versions never share mutable + /// state — three archived SwiftUI caches prepared in parallel measured + /// about 2× over serial. The window defaults to the processor count and + /// never exceeds it usefully: a preparation occupies its thread, and the + /// executor's per-class worker count is the processor count. Pass 1 for + /// the serial order (oldest first). Values below 1 count as 1. The + /// result is independent of the window; only event delivery interleaves + /// across concurrently indexed versions. + /// + /// Runs on the demangler's large-stack task executor + /// (`LargeStackTaskExecution.run`); the per-version child tasks inherit + /// it. + public func prepare(maximumConcurrentPreparations: Int = ProcessInfo.processInfo.activeProcessorCount) async throws { + try await LargeStackTaskExecution.run { + _ = try await versionUnits.concurrentMap(maximumConcurrency: maximumConcurrentPreparations) { versionUnit in + try await versionUnit.prepare() + } + let snapshots = versionUnits.map { $0.snapshot() } + let versionDescriptors = labels.map { ABIVersionDescriptor(label: $0) } + preparedEvolution = try ABIEvolutionBuilder().evolution(of: snapshots, versions: versionDescriptors) } - let snapshots = versionUnits.map { $0.snapshot() } - let versionDescriptors = labels.map { ABIVersionDescriptor(label: $0) } - preparedEvolution = try ABIEvolutionBuilder().evolution(of: snapshots, versions: versionDescriptors) } /// The evolution built by `prepare()` — the same value the lineage report @@ -117,8 +149,10 @@ public final class AnySwiftEvolutionInterfaceBuilder: Sendable { /// called before `prepare()`. public func printAnnotatedInterface() async throws -> SemanticString { let evolution = try requirePrepared() - let blocks = await makeRenderer(for: evolution).annotatedBlocks() - return EvolutionMarking.renderInterface(blocks: blocks, evolution: evolution) + return await LargeStackTaskExecution.run { + let blocks = await makeRenderer(for: evolution).annotatedBlocks() + return EvolutionMarking.renderInterface(blocks: blocks, evolution: evolution) + } } /// The structured line stream behind ``printAnnotatedInterface()``: the @@ -129,7 +163,10 @@ public final class AnySwiftEvolutionInterfaceBuilder: Sendable { /// renderer's `annotatedDiffBlocks()`. @_spi(Support) public func annotatedBlocks() async throws -> [[EvolutionLine]] { - try await makeRenderer(for: requirePrepared()).annotatedBlocks() + let evolution = try requirePrepared() + return await LargeStackTaskExecution.run { + await makeRenderer(for: evolution).annotatedBlocks() + } } private func makeRenderer(for evolution: ABIEvolution) -> SwiftEvolutionInterfaceRenderer { diff --git a/Sources/SwiftInterface/DependencyPath.swift b/Sources/SwiftInterface/DependencyPath.swift index f66717e7..7bf63eb4 100644 --- a/Sources/SwiftInterface/DependencyPath.swift +++ b/Sources/SwiftInterface/DependencyPath.swift @@ -1,6 +1,11 @@ -import SwiftDeclaration -import SwiftIndexing -import SwiftPrinting +import MachODependencies + +/// Superseded by `MachODependencies.DependencySearchPath`, which every +/// dependency-resolving feature now shares (evolution proposal +/// macho-dependencies-module). Kept one release for source +/// compatibility; the case spellings differ, so this is a conversion rather +/// than a typealias. +@available(*, deprecated, message: "Use DependencySearchPath (MachODependencies); convert with `searchPath`.") public enum DependencyPath: CustomStringConvertible { /// A path to a specific Mach-O binary file case machO(String) @@ -19,4 +24,16 @@ public enum DependencyPath: CustomStringConvertible { return "usesSystemDyldSharedCache" } } + + /// The equivalent shared search path. + public var searchPath: DependencySearchPath { + switch self { + case .machO(let path): + return .machOFile(path: path) + case .dyldSharedCache(let path): + return .dyldSharedCache(path: path) + case .usesSystemDyldSharedCache: + return .systemDyldSharedCache + } + } } diff --git a/Sources/SwiftInterface/SwiftDiffableInterfaceBuilder.swift b/Sources/SwiftInterface/SwiftDiffableInterfaceBuilder.swift index f3b99cc8..573312f4 100644 --- a/Sources/SwiftInterface/SwiftDiffableInterfaceBuilder.swift +++ b/Sources/SwiftInterface/SwiftDiffableInterfaceBuilder.swift @@ -3,6 +3,7 @@ import SwiftDeclaration import SwiftDeclarationRendering import SwiftDiffing import MachOSwiftSection +import MachOSymbols /// The ABI-diff analogue of ``SwiftInterfaceBuilder``. /// @@ -38,7 +39,18 @@ public final class SwiftDiffableInterfaceBuilder: /// here. (`index(in:)` is `package`-scoped and idempotent, so this is safe /// and cheap to re-enter; it is callable because this builder lives in the /// same package as the model.) + /// + /// Runs on the demangler's large-stack task executor + /// (`LargeStackTaskExecution.run`), so the per-definition indexing pass + /// — the bulk of a diff's cost — demangles inline instead of hopping to + /// a pool thread per symbol. public func prepare() async throws { + try await LargeStackTaskExecution.run { + try await prepareContents() + } + } + + private func prepareContents() async throws { try await indexer.prepare() for typeDefinition in indexer.allTypeDefinitions.values { diff --git a/Sources/SwiftInterface/SwiftDiffableInterfaceRenderer.swift b/Sources/SwiftInterface/SwiftDiffableInterfaceRenderer.swift index c85140ee..1fa9d9d1 100644 --- a/Sources/SwiftInterface/SwiftDiffableInterfaceRenderer.swift +++ b/Sources/SwiftInterface/SwiftDiffableInterfaceRenderer.swift @@ -8,6 +8,7 @@ import MachOSwiftSection import Semantic import Demangling import OrderedCollections +import MachOSymbols /// Renders a **full Swift interface annotated with diff markers** — a git-diff /// style view of how the new binary's ABI surface differs from the old. @@ -58,8 +59,13 @@ public final class SwiftDiffableInterfaceRenderer< /// ``DiffFormat/inline``, the git-diff-style `+`/`-`/` ` markers with a /// one-space gutter). The classified stream comes from /// ``annotatedDiffBlocks()``; the format turns it into the final string. + /// + /// Runs on the demangler's large-stack task executor + /// (`LargeStackTaskExecution.run`), as does ``annotatedDiffBlocks()``. public func printAnnotatedInterface(format: DiffFormat = .inline) async -> SemanticString { - await format.render(annotatedDiffBlocks()) + await LargeStackTaskExecution.run { + await format.render(annotatedDiffBlocks()) + } } /// The full classified diff as a block-grouped, single-line-split stream: the @@ -70,7 +76,9 @@ public final class SwiftDiffableInterfaceRenderer< /// than a rendered string. @_spi(Support) public func annotatedDiffBlocks() async -> [[DiffLine]] { - await InterfaceUnionWalker(versions: versions, strategy: DiffUnionStrategy(versions: versions)).blocks() + await LargeStackTaskExecution.run { + await InterfaceUnionWalker(versions: versions, strategy: DiffUnionStrategy(versions: versions)).blocks() + } } } diff --git a/Sources/SwiftInterface/SwiftEvolutionInterfaceBuilder.swift b/Sources/SwiftInterface/SwiftEvolutionInterfaceBuilder.swift index 69b88673..60ece49f 100644 --- a/Sources/SwiftInterface/SwiftEvolutionInterfaceBuilder.swift +++ b/Sources/SwiftInterface/SwiftEvolutionInterfaceBuilder.swift @@ -57,9 +57,9 @@ public final class SwiftEvolutionInterfaceBuilder: Sendable public func addExtraDataProvider(_ extraDataProvider: some SwiftInterfaceBuilderExtraDataProvider) { extraDataProviders.append(extraDataProvider) - printer.addTypeNameResolver(extraDataProvider) + if let typeNameResolver = extraDataProvider as? any TypeNameResolving { + printer.addTypeNameResolver(typeNameResolver) + } } public func removeAllExtraDataProviders() { @@ -92,8 +94,21 @@ public final class SwiftInterfaceBuilder: Sendable /// - Building cross-reference maps for conformances and associated types /// - Collecting all required module imports /// + /// Runs on the demangler's large-stack task executor + /// (`LargeStackTaskExecution.run`, evolution proposal + /// `large-stack-executor-and-cross-version-parallelism`), as does + /// `printRoot()`: every demangle / print / remangle inside runs inline + /// instead of hopping to a pool thread per call. Output is independent + /// of where it runs. + /// /// - Throws: An error if indexing fails or if required data cannot be extracted. public func prepare() async throws { + try await LargeStackTaskExecution.run { + try await prepareContents() + } + } + + private func prepareContents() async throws { eventDispatcher.dispatch(.phaseTransition(phase: .preparation, state: .started)) for extraDataProvider in extraDataProviders { @@ -123,8 +138,24 @@ public final class SwiftInterfaceBuilder: Sendable eventDispatcher.dispatch(.phaseTransition(phase: .preparation, state: .completed)) } - @SemanticStringBuilder public func printRoot() async throws -> SemanticString { + // Exported-only filter (evolution proposal + // `exported-only-interface`): the printer rules on types, + // protocols and members by itself, but an `extension`'s verdict needs + // the indexer's complete in-image tables — a stripped image carries no + // symbol for a non-exported type, so only the index can say whether an + // extension's target is a dropped in-image declaration. Installed per + // print so a configuration update between prints is honored. + if printer.configuration.printExportedDeclarationsOnly { + printer.installExportFilterScope(types: indexer.allTypeDefinitions.values, protocols: indexer.allProtocolDefinitions.values) + } + return try await LargeStackTaskExecution.run { + try await printRootContents() + } + } + + @SemanticStringBuilder + private func printRootContents() async throws -> SemanticString { // Leading header (evolution proposal 0008), deliberately independent // of the imports block below — flag-gated, default absent. if let interfaceHeaderInfo = configuration.interfaceHeaderInfo { @@ -141,7 +172,7 @@ public final class SwiftInterfaceBuilder: Sendable // they report as `.definitionBlock` degradations instead. await printCatchedThrowing(dispatchingTo: eventDispatcher, degradationSource: .definitionBlock) { await BlockList { - for variable in indexer.globalVariableDefinitions { + for variable in indexer.globalVariableDefinitions where !printer.isExcludedByExportFilter(globalSymbolNames: variable.accessors.map(\.symbol.name)) { printer.globalExportStatusComment(forSymbolNames: variable.accessors.map(\.symbol.name)) await printer.printVariable(variable, level: 0) } @@ -150,7 +181,7 @@ public final class SwiftInterfaceBuilder: Sendable await printCatchedThrowing(dispatchingTo: eventDispatcher, degradationSource: .definitionBlock) { await BlockList { - for function in indexer.globalFunctionDefinitions { + for function in indexer.globalFunctionDefinitions where !printer.isExcludedByExportFilter(globalSymbolNames: [function.symbol.name]) { printer.globalExportStatusComment(forSymbolNames: [function.symbol.name]) await printer.printFunction(function, level: 0) } diff --git a/Sources/SwiftInterface/SwiftInterfaceBuilderDependencies.swift b/Sources/SwiftInterface/SwiftInterfaceBuilderDependencies.swift index dddedf0f..48aaa9aa 100644 --- a/Sources/SwiftInterface/SwiftInterfaceBuilderDependencies.swift +++ b/Sources/SwiftInterface/SwiftInterfaceBuilderDependencies.swift @@ -3,88 +3,93 @@ import SwiftDeclaration import SwiftIndexing import SwiftPrinting @preconcurrency import MachOKit +import MachODependencies import MachOSwiftSection +import MachOFoundation +/// The images a provider such as TypeIndexing's +/// `SwiftInterfaceBuilderTypeNameProvider` works against: the root plus its +/// **direct** dependencies only. +/// +/// Direct on purpose. TypeIndexing generates a SourceKit module interface per +/// dependency module, so its cost is linear in this list; the transitive +/// closure of an OS framework runs to hundreds of images and would bring back +/// the whole-SDK generation that once made that target unusable (evolution +/// proposal 0009). A caller that genuinely wants the transitive set builds a +/// `DependencyClosure` with `.transitive` and hands it to `init(closure:)`. @dynamicMemberLookup public struct SwiftInterfaceBuilderDependencies: Sendable { public let machO: MachO public let dependencies: [MachO] + /// Load names of the root's dependencies no locator could resolve — the + /// exact complement of `dependencies`, so a host can say *which* images + /// its attribution will miss instead of guessing from an empty list. + public let unresolvedLoadNames: [String] + public subscript(dynamicMember keyPath: KeyPath) -> Value { self[keyPath: keyPath] } + + /// Wraps an already-resolved closure — any traversal, any locator — so a + /// host that resolved dependencies once can share them with every consumer. + public init(closure: DependencyClosure) { + self.machO = closure.root + self.dependencies = closure.images + self.unresolvedLoadNames = closure.unresolvedLoadNames + } } extension SwiftInterfaceBuilderDependencies { - /// - Parameter eventHandlers: Sinks for the dependencies that fail to load. - /// Defaulted, so existing call sites are unaffected — and passing none is - /// safe rather than silent: `SwiftIndexEvents.Dispatcher` reports an - /// unhandled failure to os_log rather than dropping it. - public init(machO: MachO, paths: [DependencyPath], eventHandlers: [SwiftIndexEvents.Handler] = []) { - var dependencies: [MachOFile] = [] - let dependencyPaths = Set(machO.dependencies.map(\.dylib.name)) - let eventDispatcher = SwiftIndexEvents.Dispatcher() - eventDispatcher.addHandlers(eventHandlers) - - for searchPath in paths { - switch searchPath { - case .machO(let path): - do { - if let machOFile = try File.loadFromFile(url: .init(fileURLWithPath: path)).machOFiles.first { - dependencies.append(machOFile) - } else {} - } catch { - eventDispatcher.dispatch( - .renderingDegraded( - context: .init(source: .dependencyLoad, subject: path), - error: error - ) - ) - } - case .dyldSharedCache(let path): - do { - let fullDyldCache = try FullDyldCache(url: .init(fileURLWithPath: path)) - var foundCount = 0 - for machOFile in fullDyldCache.machOFiles() where dependencyPaths.contains(machOFile.imagePath) { - dependencies.append(machOFile) - foundCount += 1 - } - } catch { - eventDispatcher.dispatch( - .renderingDegraded( - context: .init(source: .dependencyLoad, subject: path), - error: error - ) + /// Resolves the root's direct dependencies through `searchPaths` + /// (`FileDependencyLocator`: exact install path first, ranked bare-name + /// match second). + /// + /// - Parameter eventHandlers: Sinks for the search paths that fail to + /// open. Defaulted, so existing call sites are unaffected — and passing + /// none is safe rather than silent: `SwiftIndexEvents.Dispatcher` reports + /// an unhandled failure to os_log rather than dropping it. + public init(machO: MachO, searchPaths: [DependencySearchPath], eventHandlers: [SwiftIndexEvents.Handler] = []) { + let closure = DependencyClosure(root: machO, searchPaths: searchPaths, traversal: .direct) + if !closure.searchPathLoadFailures.isEmpty { + let eventDispatcher = SwiftIndexEvents.Dispatcher() + eventDispatcher.addHandlers(eventHandlers) + for loadFailure in closure.searchPathLoadFailures { + eventDispatcher.dispatch( + .renderingDegraded( + context: .init(source: .dependencyLoad, subject: Self.eventSubject(for: loadFailure.searchPath)), + error: loadFailure.error ) - } - case .usesSystemDyldSharedCache: - if let hostDyldCache = FullDyldCache.host { - var foundCount = 0 - for machOFile in hostDyldCache.machOFiles() where dependencyPaths.contains(machOFile.imagePath) { - dependencies.append(machOFile) - foundCount += 1 - } - } + ) } } - self.machO = machO - self.dependencies = dependencies + self.init(closure: closure) } -} -extension SwiftInterfaceBuilderDependencies { - public init(machO: MachO) { - var dependencies: [MachO] = [] - let dependencyNames = machO.dependencies.map(\.dylib.name) + @available(*, deprecated, renamed: "init(machO:searchPaths:eventHandlers:)") + public init(machO: MachO, paths: [DependencyPath], eventHandlers: [SwiftIndexEvents.Handler] = []) { + self.init(machO: machO, searchPaths: paths.map(\.searchPath), eventHandlers: eventHandlers) + } - for dependencyPath in dependencyNames { - if let machO = MachOImage(name: dependencyPath) { - dependencies.append(machO) - } + /// The event subject stays the bare path for file and cache entries, as it + /// was before the search paths were unified. + private static func eventSubject(for searchPath: DependencySearchPath) -> String { + switch searchPath { + case .machOFile(let path), .dyldSharedCache(let path): + return path + case .systemDyldSharedCache: + return searchPath.description } + } +} - self.machO = machO - self.dependencies = dependencies +extension SwiftInterfaceBuilderDependencies { + /// Resolves the root's direct dependencies through the active dyld + /// (`InProcessDependencyLocator`, which normalizes each load name to the + /// bare image name `MachOImage(name:)` matches on — handing it the raw + /// load path resolved nothing). + public init(machO: MachO) { + self.init(closure: DependencyClosure(root: machO, traversal: .direct)) } } diff --git a/Sources/SwiftInterface/SwiftInterfaceBuilderExtraDataProvider.swift b/Sources/SwiftInterface/SwiftInterfaceBuilderExtraDataProvider.swift index 48922341..f1d708d0 100644 --- a/Sources/SwiftInterface/SwiftInterfaceBuilderExtraDataProvider.swift +++ b/Sources/SwiftInterface/SwiftInterfaceBuilderExtraDataProvider.swift @@ -3,7 +3,14 @@ import SwiftIndexing import SwiftPrinting import Demangling -public protocol SwiftInterfaceBuilderExtraDataProvider: TypeNameResolvable { +/// A builder-lifecycle hook: attached via `addExtraDataProvider(_:)`, its +/// `setup()` runs during `prepare()`. Resolver-ness is an orthogonal +/// capability — a provider that answers printer queries additionally conforms +/// to the `TypeNameResolving` role protocols (`ModuleNameResolving`, +/// `CImportedNameResolving`, `OpaqueTypeResolving`), and `addExtraDataProvider` +/// forwards it to the printer only then. A setup-only provider is a legitimate +/// conformer. +public protocol SwiftInterfaceBuilderExtraDataProvider: Sendable { func setup() async throws } diff --git a/Sources/SwiftInterface/SwiftInterfaceBuilderOpaqueTypeProvider.swift b/Sources/SwiftInterface/SwiftInterfaceBuilderOpaqueTypeProvider.swift index 64f64637..2b359194 100644 --- a/Sources/SwiftInterface/SwiftInterfaceBuilderOpaqueTypeProvider.swift +++ b/Sources/SwiftInterface/SwiftInterfaceBuilderOpaqueTypeProvider.swift @@ -11,7 +11,7 @@ import SwiftStdlibToolbox import SwiftDeclarationRendering @_spi(Internals) import SwiftInspection -public struct SwiftInterfaceBuilderOpaqueTypeProvider: SwiftInterfaceBuilderExtraDataProvider, Sendable { +public struct SwiftInterfaceBuilderOpaqueTypeProvider: SwiftInterfaceBuilderExtraDataProvider, OpaqueTypeResolving, Sendable { public let machO: MachO public init(machO: MachO) { diff --git a/Sources/SwiftLayout/ImageUniverse+DependencyClosure.swift b/Sources/SwiftLayout/ImageUniverse+DependencyClosure.swift index 7bfe8fab..f89f3d78 100644 --- a/Sources/SwiftLayout/ImageUniverse+DependencyClosure.swift +++ b/Sources/SwiftLayout/ImageUniverse+DependencyClosure.swift @@ -1,21 +1,25 @@ -import Foundation import MachOKit +import MachODependencies import MachOSwiftSection - -/// Where the `MachOFile` dependency-closure factory may locate a dependency -/// binary. Mirrors the resolution strategies used elsewhere in the package, but -/// kept local so `SwiftLayout` does not depend on the higher-level -/// `SwiftInterface` module that defines the analogous `DependencyPath`. -public enum LayoutDependencySearchPath: Sendable, Equatable, Hashable { - /// An explicit on-disk path to a Mach-O (or fat) binary file. Used for - /// non-cache dependencies such as a sibling framework reached through - /// `@rpath` (the MVP does not expand `@rpath` itself). - case machOFile(path: String) - /// An explicit path to a dyld shared cache file. - case dyldSharedCache(path: String) - /// The system's active dyld shared cache (covers stdlib / Foundation / - /// Distributed and the rest of the OS frameworks). - case systemDyldSharedCache +import MachOFoundation + +/// The search-path enum moved down to `MachODependencies` so every feature +/// resolves dependencies the same way (evolution proposal +/// macho-dependencies-module). Kept one release for source +/// compatibility; the cases are spelled identically. +@available(*, deprecated, renamed: "DependencySearchPath") +public typealias LayoutDependencySearchPath = DependencySearchPath + +extension ImageUniverse { + /// Builds a universe over an already-resolved `DependencyClosure` — the + /// entry point for a host that resolves the closure once and shares it with + /// other consumers (interface generation, `__C` attribution). The closure's + /// resolution order is preserved: the universe indexes dependencies lazily + /// in exactly that order and stops at the first hit, which is why the + /// closure walks breadth-first. + public static func dependencyClosure(_ closure: DependencyClosure) throws -> ImageUniverse { + try dependencyClosure(root: closure.root, dependencyImages: closure.images) + } } // MARK: - In-process closure (MachOImage) @@ -29,10 +33,7 @@ extension ImageUniverse where MachO == MachOImage { /// are skipped — their types simply degrade per field rather than failing /// the whole closure. public static func dependencyClosure(root: MachOImage) throws -> ImageUniverse { - let collectedDependencies = transitiveDependencies(of: root) { bareName in - MachOImage(name: bareName) - } - return try dependencyClosure(root: root, dependencyImages: collectedDependencies) + try dependencyClosure(DependencyClosure(root: root, traversal: .transitive)) } } @@ -40,127 +41,20 @@ extension ImageUniverse where MachO == MachOImage { extension ImageUniverse where MachO == MachOFile { /// Builds an offline dependency closure for a file-backed image. Each - /// dependency is located by its bare name through the supplied search paths - /// (explicit on-disk files first, then the dyld shared cache), recursively, - /// deduped by bare name. Dependencies that cannot be located are skipped. + /// dependency is located through the supplied search paths (explicit + /// on-disk files, then the dyld shared caches — exact install path first, + /// ranked bare-name match second; see `FileDependencyLocator`), + /// recursively, deduped by bare name. Dependencies that cannot be located + /// are skipped. /// - /// `@rpath` / `@loader_path` / `@executable_path` are not expanded in this - /// MVP: a non-cache dependency must be reachable through an explicit - /// `.machOFile(path:)` entry. Cache-resident system frameworks resolve by - /// bare name automatically. + /// `@rpath` / `@loader_path` / `@executable_path` are not expanded: a + /// non-cache dependency must be reachable through an explicit + /// `.machOFile(path:)` entry. Cache-resident system frameworks resolve + /// automatically. public static func dependencyClosure( root: MachOFile, - searchPaths: [LayoutDependencySearchPath] = [.systemDyldSharedCache] + searchPaths: [DependencySearchPath] = [.systemDyldSharedCache] ) throws -> ImageUniverse { - let locator = try MachOFileDependencyLocator(searchPaths: searchPaths) - let collectedDependencies = transitiveDependencies(of: root) { bareName in - locator.locate(bareName: bareName) - } - return try dependencyClosure(root: root, dependencyImages: collectedDependencies) - } -} - -/// Collects the transitive dependency closure of `root` in breadth-first order -/// (direct dependencies first, then their dependencies, …), deduped by bare -/// image name, using `locate` to turn each `LC_LOAD_DYLIB` bare name into a -/// concrete image. Breadth-first ordering matters because the universe indexes -/// dependencies lazily in this order: the binary's own direct Swift -/// dependencies — the ones most field types resolve against — come first. -private func transitiveDependencies( - of root: MachO, - locate: (String) -> MachO? -) -> [MachO] { - var visitedBareNames: Set = [bareImageName(fromDependencyLoadName: root.imagePath)] - var collected: [MachO] = [] - var frontier: [MachO] = [root] - - while !frontier.isEmpty { - var nextFrontier: [MachO] = [] - for image in frontier { - for dependencyLoadName in image.dependencies.map(\.dylib.name) { - let bareName = bareImageName(fromDependencyLoadName: dependencyLoadName) - guard !bareName.isEmpty, visitedBareNames.insert(bareName).inserted else { continue } - guard let dependencyImage = locate(bareName) else { continue } - collected.append(dependencyImage) - nextFrontier.append(dependencyImage) - } - } - frontier = nextFrontier - } - return collected -} - -/// Locates dependency `MachOFile`s by bare name across a set of search paths. -/// Explicit on-disk files are indexed eagerly by bare name (keyed by the -/// supplied path's bare name, since a file's `imagePath` is its install name, -/// not its on-disk path). Each dyld shared cache is indexed lazily by bare name -/// the first time a cache lookup is needed — one full pass over the cache, -/// rather than a fresh per-lookup scan (which would be `O(dependencies × cache -/// size)`). -private final class MachOFileDependencyLocator { - private let explicitFilesByBareName: [String: MachOFile] - private let caches: [FullDyldCache] - private var cacheFilesByBareName: [String: MachOFile]? - - init(searchPaths: [LayoutDependencySearchPath]) throws { - var explicitFilesByBareName: [String: MachOFile] = [:] - var caches: [FullDyldCache] = [] - for searchPath in searchPaths { - switch searchPath { - case .machOFile(let path): - guard let machOFile = try? File.loadFromFile(url: URL(fileURLWithPath: path)).machOFiles.first else { continue } - // Key by the supplied path's bare name: `machOFile.imagePath` - // resolves to the install name (`@rpath/Foo.framework/.../Foo`), - // whose bare name matches a dependent's load name anyway. - let bareName = bareImageName(fromDependencyLoadName: path) - if explicitFilesByBareName[bareName] == nil { - explicitFilesByBareName[bareName] = machOFile - } - case .dyldSharedCache(let path): - if let cache = try? FullDyldCache(url: URL(fileURLWithPath: path)) { - caches.append(cache) - } - case .systemDyldSharedCache: - if let hostCache = FullDyldCache.host { - caches.append(hostCache) - } - } - } - self.explicitFilesByBareName = explicitFilesByBareName - self.caches = caches - } - - func locate(bareName: String) -> MachOFile? { - if let explicit = explicitFilesByBareName[bareName] { - return explicit - } - guard !caches.isEmpty else { return nil } - return cacheIndex()[bareName] + try dependencyClosure(DependencyClosure(root: root, searchPaths: searchPaths, traversal: .transitive)) } - - /// Builds (once) and returns the bare-name → cache image index across every - /// configured cache, first writer wins. - private func cacheIndex() -> [String: MachOFile] { - if let cacheFilesByBareName { return cacheFilesByBareName } - var index: [String: MachOFile] = [:] - for cache in caches { - for machOFile in cache.machOFiles() { - let bareName = bareImageName(fromDependencyLoadName: machOFile.imagePath) - if !bareName.isEmpty, index[bareName] == nil { - index[bareName] = machOFile - } - } - } - cacheFilesByBareName = index - return index - } -} - -/// Reduces a dylib load name (`@rpath/Foo.framework/Versions/A/Foo`, -/// `/usr/lib/swift/libswiftDistributed.dylib`, a bare module name) to the bare -/// image name `MachOImage(name:)` and the dyld-cache `.name` matcher use — the -/// last path component with its first extension component stripped. -func bareImageName(fromDependencyLoadName dependencyLoadName: String) -> String { - let lastPathComponent = dependencyLoadName.components(separatedBy: "/").last ?? dependencyLoadName - return lastPathComponent.components(separatedBy: ".").first ?? lastPathComponent } diff --git a/Sources/SwiftLayout/ImageUniverse.swift b/Sources/SwiftLayout/ImageUniverse.swift index 5c3bca23..aff99ced 100644 --- a/Sources/SwiftLayout/ImageUniverse.swift +++ b/Sources/SwiftLayout/ImageUniverse.swift @@ -1,4 +1,5 @@ import MachOSwiftSection +import MachOFoundation /// The set of images the layout engine may resolve types against, plus the /// resolution entry point the resolver uses to map a fully-qualified type name diff --git a/Sources/SwiftPrinting/NodePrintables/NodePrintableDelegate.swift b/Sources/SwiftPrinting/NodePrintables/NodePrintableDelegate.swift index 6ac7b1ed..d05840bb 100644 --- a/Sources/SwiftPrinting/NodePrintables/NodePrintableDelegate.swift +++ b/Sources/SwiftPrinting/NodePrintables/NodePrintableDelegate.swift @@ -1,42 +1,13 @@ -import SwiftDeclaration import Demangling -import Foundation -/// The declaration category of a C-imported type reference, as the mangling -/// records it. A Swift-spelling lookup is only well-defined per category: an -/// ObjC protocol and class may share one C name (`NSObject`), and only the -/// protocol is renamed (`NSObjectProtocol`) — a category-blind lookup would -/// rewrite class references with the protocol's rename. -public enum CImportedTypeNameCategory: Sendable { - case objcClass - case objcProtocol - case valueType - case other - - public init(nodeKind: Node.Kind) { - switch nodeKind { - case .class: - self = .objcClass - case .protocol: - self = .objcProtocol - case .enum, .structure: - self = .valueType - default: - self = .other - } - } -} - -public protocol TypeNameResolvable: Sendable { +/// The aggregate query surface the node printers ask their answers from. +/// +/// Deliberately fat on this, the consumer side: ``SwiftDeclarationPrinter`` is +/// the sole conformer and genuinely serves every query, fanning each one out +/// to the role-scoped resolvers registered with it (see `TypeNameResolving` +/// and the role protocols beside it for the provider side). +protocol NodePrintableDelegate: AnyObject, Sendable { func moduleName(forTypeName typeName: String) async -> String? func swiftName(forCName cName: String, category: CImportedTypeNameCategory) async -> String? func opaqueType(forNode node: Node, index: Int?) async -> String? } - -extension TypeNameResolvable { - public func moduleName(forTypeName typeName: String) async -> String? { nil } - public func swiftName(forCName cName: String, category: CImportedTypeNameCategory) async -> String? { nil } - public func opaqueType(forNode node: Node, index: Int?) async -> String? { nil } -} - -protocol NodePrintableDelegate: TypeNameResolvable, AnyObject {} diff --git a/Sources/SwiftPrinting/NodePrintables/TypeNameResolving.swift b/Sources/SwiftPrinting/NodePrintables/TypeNameResolving.swift new file mode 100644 index 00000000..ffaa8299 --- /dev/null +++ b/Sources/SwiftPrinting/NodePrintables/TypeNameResolving.swift @@ -0,0 +1,51 @@ +import Demangling + +/// The declaration category of a C-imported type reference, as the mangling +/// records it. A Swift-spelling lookup is only well-defined per category: an +/// ObjC protocol and class may share one C name (`NSObject`), and only the +/// protocol is renamed (`NSObjectProtocol`) — a category-blind lookup would +/// rewrite class references with the protocol's rename. +public enum CImportedTypeNameCategory: Sendable { + case objcClass + case objcProtocol + case valueType + case other + + public init(nodeKind: Node.Kind) { + switch nodeKind { + case .class: + self = .objcClass + case .protocol: + self = .objcProtocol + case .enum, .structure: + self = .valueType + default: + self = .other + } + } +} + +/// Marker for a resolver registrable with ``SwiftDeclarationPrinter``. Conform +/// to the role protocols below for the queries the resolver actually serves — +/// a resolver conforming to none of them is never consulted, and the roles +/// deliberately carry no default implementations, so a signature drift breaks +/// the conformer at compile time instead of silently unhooking it. +public protocol TypeNameResolving: Sendable {} + +/// Resolves the real module of a type printed under a placeholder module +/// (`__C.NSString` → `Foundation`). +public protocol ModuleNameResolving: TypeNameResolving { + func moduleName(forTypeName typeName: String) async -> String? +} + +/// Resolves a C-imported identifier to its Swift spelling (APINotes renames, +/// the CF `Ref`-strip bridge rule), per declaration category. +public protocol CImportedNameResolving: TypeNameResolving { + func swiftName(forCName cName: String, category: CImportedTypeNameCategory) async -> String? +} + +/// Expands an opaque return type (`some P`) from its descriptor's generic +/// requirements. +public protocol OpaqueTypeResolving: TypeNameResolving { + func opaqueType(forNode node: Node, index: Int?) async -> String? +} diff --git a/Sources/SwiftPrinting/SwiftDeclarationPrintConfiguration.swift b/Sources/SwiftPrinting/SwiftDeclarationPrintConfiguration.swift index 838aca3b..4046319a 100644 --- a/Sources/SwiftPrinting/SwiftDeclarationPrintConfiguration.swift +++ b/Sources/SwiftPrinting/SwiftDeclarationPrintConfiguration.swift @@ -25,6 +25,18 @@ public struct SwiftDeclarationPrintConfiguration: Equatable, Sendable { /// FACT, not an access-level guess; nothing is emitted when the image /// carries no export information. public var printExportStatus: Bool = false + + /// Print only the declarations the image EXPORTS (evolution proposal + /// `exported-only-interface`) — the filtering counterpart of + /// `printExportStatus`. Types / protocols are ruled by their descriptor + /// symbol (`…Mn` / `…Mp`) in the export trie, members by the same + /// derived-form verdict the annotation uses, extensions by whether their + /// target is an in-image non-exported declaration (see + /// `ExportFilterScope`). Still a symbol-table FACT: a declaration is + /// dropped only on a definitive negative — anything without evidence + /// (no export information, no joined symbols, `override` / `@objc` + /// members) is kept, so the filter never drops on a guess. + public var printExportedDeclarationsOnly: Bool = false public var memberSortOrder: SwiftDeclarationMemberSortOrder = .byCategory public var printTypeLayout: Bool = false public var printEnumLayout: Bool = false diff --git a/Sources/SwiftPrinting/SwiftDeclarationPrinter+ExportFilter.swift b/Sources/SwiftPrinting/SwiftDeclarationPrinter+ExportFilter.swift new file mode 100644 index 00000000..30ce27ec --- /dev/null +++ b/Sources/SwiftPrinting/SwiftDeclarationPrinter+ExportFilter.swift @@ -0,0 +1,261 @@ +import Foundation +import SwiftDeclaration +@_spi(Internals) import Demangling +import Dependencies +import MachOSwiftSection +@_spi(Internals) import MachOSymbols + +/// The in-image declarations the exported-only filter (evolution proposal +/// `exported-only-interface`) needs in order to rule on an +/// `extension`: an extension has no descriptor symbol of its own, so it is +/// dropped exactly when it targets — or declares conformance to — a +/// declaration of THIS image whose descriptor is provably not exported. +/// Targets living in other images are unknowable here and are always kept. +/// +/// Built by ``SwiftDeclarationPrinter/installExportFilterScope(types:protocols:)`` +/// from the indexer's complete type / protocol tables — the symbol store +/// cannot stand in for those: a stripped image carries no symbol at all for +/// a non-exported type, so a symbol-derived "is this type in-image" test +/// would keep every extension of a dropped private type. +public struct ExportFilterScope: Sendable, Equatable { + /// Names of the in-image types whose nominal type descriptor is not in + /// the export trie. Structural (`TypeName`'s `Hashable`), so an + /// `ExtensionName` minted into a different node store still matches. + public var nonExportedTypeNames: Set + + /// Names of the in-image protocols whose protocol descriptor is not in + /// the export trie. + public var nonExportedProtocolNames: Set + + public static let empty = ExportFilterScope(nonExportedTypeNames: [], nonExportedProtocolNames: []) + + public init(nonExportedTypeNames: Set, nonExportedProtocolNames: Set) { + self.nonExportedTypeNames = nonExportedTypeNames + self.nonExportedProtocolNames = nonExportedProtocolNames + } +} + +// MARK: - Scope installation + +extension SwiftDeclarationPrinter { + /// Computes the ``ExportFilterScope`` for the given in-image definitions + /// and installs it on this printer. `SwiftInterfaceBuilder.printRoot()` + /// calls this with the indexer's complete tables whenever + /// `printExportedDeclarationsOnly` is set; a host that drives the printer + /// per definition (bypassing `printRoot`) installs its own scope the same + /// way. With no scope installed the extension leg of the filter + /// degrades to "keep" — the filter never drops on a guess. + public func installExportFilterScope(types: some Sequence, protocols: some Sequence) { + var scope = ExportFilterScope.empty + for typeDefinition in types where exportVerdict(forTypeDefinition: typeDefinition) == false { + scope.nonExportedTypeNames.insert(typeDefinition.typeName) + } + for protocolDefinition in protocols where exportVerdict(forProtocolDefinition: protocolDefinition) == false { + scope.nonExportedProtocolNames.insert(protocolDefinition.protocolName) + } + exportFilterScope = scope + } + + public func removeExportFilterScope() { + exportFilterScope = .empty + } +} + +// MARK: - Declaration-level verdicts + +extension SwiftDeclarationPrinter { + /// Whether the type's nominal type descriptor (`…Mn`) has an export-trie + /// entry: `true` / `false` are trie facts, `nil` means no verdict (the + /// image carries no export information, or no evidence could be found). + /// The descriptor is the one symbol every nominal type owns regardless + /// of what else got exported — on the `SymbolTestsCore` fixture the + /// exported `Mn` and `Ma` (metadata accessor) sets coincide exactly. + public func exportVerdict(forTypeDefinition typeDefinition: TypeDefinition) -> Bool? { + exportVerdict( + descriptorOffset: typeDefinition.typeContextDescriptorWrapper.typeContextDescriptor.offset, + nameNode: typeDefinition.typeName.node, + descriptorKind: .nominalTypeDescriptor, + descriptorSuffix: "Mn" + ) + } + + /// Whether the protocol's descriptor (`…Mp`) has an export-trie entry; + /// same tri-state as ``exportVerdict(forTypeDefinition:)``. + public func exportVerdict(forProtocolDefinition protocolDefinition: ProtocolDefinition) -> Bool? { + exportVerdict( + descriptorOffset: protocolDefinition.protocolDescriptor.offset, + nameNode: protocolDefinition.protocolName.node, + descriptorKind: .protocolDescriptor, + descriptorSuffix: "Mp" + ) + } + + /// Two legs, authoritative first: + /// + /// 1. The symbol actually located AT the descriptor. Every exported + /// descriptor has a trie row at its offset, and an unstripped image + /// also carries a local symtab row for a non-exported one, so this leg + /// answers with the compiler's own spelling of the name — which is + /// what makes it authoritative: a type nested in a CONSTRAINED + /// extension (`extension Foo where A: P { public struct Nested {} }`) + /// mangles only the extension's own requirements into its context, + /// while the model's name node carries the full signature, so a + /// remangled name misses the trie and would drop an exported type. + /// 2. Only when no symbol sits at the descriptor (a stripped image's + /// non-exported type): the remangled descriptor name against the trie, + /// which is complete even when the symtab is not. Restricted to + /// canonical contexts — a name involving an `.extension` context is + /// exactly the shape leg 1 exists for, and yields no verdict here. + private func exportVerdict(descriptorOffset: Int, nameNode: NodeReference, descriptorKind: Node.Kind, descriptorSuffix: String) -> Bool? { + @Dependency(\.symbolIndexStore) var symbolIndexStore + if let symbolsAtDescriptor = symbolIndexStore.symbols(for: descriptorOffset, in: machO), + let descriptorSymbol = symbolsAtDescriptor.first(where: { $0.name.isSwiftSymbol && $0.name.hasSuffix(descriptorSuffix) }) { + return symbolIndexStore.isExported(name: descriptorSymbol.name, in: machO) + } + guard let symbolName = Self.descriptorSymbolName(for: nameNode, descriptorKind: descriptorKind) else { return nil } + return symbolIndexStore.isExported(name: symbolName, in: machO) + } + + /// Remangles a name node into the symbol name of its `descriptorKind` + /// descriptor (`_$sMn` / `_$sMp`), or `nil` when the + /// spelling cannot be trusted. The name node may arrive wrapped in a + /// `.type` envelope, and a specialized definition's name is a + /// bound-generic node (`Box`) — the descriptor belongs to the + /// unbound nominal, so both wrappers are peeled first. A context chain + /// through an `.extension` node is refused (see the verdict's leg 2). + /// The remangling walks a transient tree materialized from the + /// reference; nothing is interned or cached. + static func descriptorSymbolName(for node: NodeReference, descriptorKind: Node.Kind) -> String? { + var contextNode = node.materialize() + if contextNode.kind == .type, let wrappedNode = contextNode.children.first { + contextNode = wrappedNode + } + switch contextNode.kind { + case .boundGenericStructure, .boundGenericClass, .boundGenericEnum, .boundGenericProtocol, .boundGenericOtherNominalType, .boundGenericTypeAlias: + guard var nominalNode = contextNode.children.first else { return nil } + if nominalNode.kind == .type, let wrappedNode = nominalNode.children.first { + nominalNode = wrappedNode + } + contextNode = nominalNode + default: + break + } + guard !containsExtensionContext(contextNode) else { return nil } + let descriptorNode = Node.createTransient(kind: descriptorKind, children: [contextNode]) + let globalNode = Node.createTransient(kind: .global, children: [descriptorNode]) + return try? mangleAsString(globalNode) + } + + private static func containsExtensionContext(_ node: Node) -> Bool { + if node.kind == .extension { return true } + return node.children.contains { containsExtensionContext($0) } + } +} + +// MARK: - Exclusion predicates + +extension SwiftDeclarationPrinter { + var isExportFilterEnabled: Bool { + configuration.printExportedDeclarationsOnly + } + + /// A type is dropped only on a definitive negative verdict; `nil` keeps it. + package func isExcludedByExportFilter(_ typeDefinition: TypeDefinition) -> Bool { + isExportFilterEnabled && exportVerdict(forTypeDefinition: typeDefinition) == false + } + + package func isExcludedByExportFilter(_ protocolDefinition: ProtocolDefinition) -> Bool { + isExportFilterEnabled && exportVerdict(forProtocolDefinition: protocolDefinition) == false + } + + /// An extension is dropped when its target (the extended type or + /// protocol) or the protocol it declares conformance to is an in-image + /// declaration the installed ``ExportFilterScope`` recorded as not + /// exported. Targets outside the scope — other images' types, or any + /// target when no scope is installed — keep the extension. + /// + /// The fixture survey behind this rule: every non-exported conformance + /// descriptor (`…Mc`) in `SymbolTestsCore` involves a private type or + /// protocol, so the conformance descriptor itself needs no separate + /// query — a conformance of an exported type to an exported (or + /// external) protocol is as exported as its parties. + package func isExcludedByExportFilter(_ extensionDefinition: ExtensionDefinition) -> Bool { + guard isExportFilterEnabled else { return false } + let scope = exportFilterScope + let extensionName = extensionDefinition.extensionName + switch extensionName.kind { + case .type(let typeKind): + if scope.nonExportedTypeNames.contains(TypeName(node: extensionName.node, kind: typeKind)) { + return true + } + case .protocol: + if scope.nonExportedProtocolNames.contains(ProtocolName(node: extensionName.node)) { + return true + } + case .typeAlias: + break + } + if let conformingProtocolName = extensionDefinition.conformingProtocolName, + scope.nonExportedProtocolNames.contains(conformingProtocolName) { + return true + } + return false + } + + /// A NON-conformance extension every member and nested declaration of + /// which the filter drops renders as `extension Foo {}` — pure noise, so + /// the whole block goes. A conformance extension is kept even with an + /// empty body: the conformance clause is itself the declaration. + /// Requires the definition to be indexed (members come from `index(in:)`). + package func isEmptiedByExportFilter(_ extensionDefinition: ExtensionDefinition) -> Bool { + guard isExportFilterEnabled, + extensionDefinition.protocolConformanceDescriptor == nil, + extensionDefinition.conformingProtocolName == nil else { return false } + if !extensionDefinition.associatedTypes.isEmpty { return false } + if extensionDefinition.orderedMembers.contains(where: { !isExcludedByExportFilter($0) }) { return false } + if extensionDefinition.types.contains(where: { !isExcludedByExportFilter($0) }) { return false } + if extensionDefinition.protocols.contains(where: { !isExcludedByExportFilter($0) }) { return false } + return true + } + + /// The member rule is the annotation rule of proposal 0008 turned into a + /// drop: excluded only when EVERY symbol of the member (derived forms + /// included) provably lacks a trie entry. `override` (reachable through + /// the parent's dispatch thunk) and `@objc` (objc_msgSend) members carry + /// zero exported symbols of their own while being perfectly callable, + /// so both are kept, exactly as they are never annotated. + package func isExcludedByExportFilter(_ member: OrderedMember) -> Bool { + guard isExportFilterEnabled else { return false } + switch member { + case .allocator(let function), .function(let function): + return isExcludedByExportFilter(isOverride: function.isOverride, isObjC: function.attributes.contains(.objc), symbolNames: [function.symbol.name]) + case .variable(let variable): + return isExcludedByExportFilter(isOverride: variable.isOverride, isObjC: variable.attributes.contains(.objc), symbolNames: variable.accessors.map(\.symbol.name)) + case .subscript(let `subscript`): + return isExcludedByExportFilter(isOverride: `subscript`.isOverride, isObjC: `subscript`.attributes.contains(.objc), symbolNames: `subscript`.accessors.map(\.symbol.name)) + } + } + + /// Stored-field leg, mirroring the annotation's field leg in + /// `renderModelFields`: a field whose accessor group never joined has no + /// evidence and is kept ("not checkable", never "confirmed exported"); + /// `FieldDefinition` carries no attributes, so `@objc` is recognized by + /// the accessor's `To` thunk in the symbol population. Enum cases never + /// reach here — they own no symbols. + package func isExcludedByExportFilter(field: FieldDefinition) -> Bool { + guard isExportFilterEnabled, !field.accessors.isEmpty, !field.isOverride else { return false } + @Dependency(\.symbolIndexStore) var symbolIndexStore + let hasObjCEntryPoint = field.accessors.contains { symbolIndexStore.containsSymbol(named: $0.symbol.name + "To", in: machO) } + return !hasObjCEntryPoint && exportVerdict(forSymbolNames: field.accessors.map(\.symbol.name)) == false + } + + /// Top-level globals: no `override` / `@objc` at top level, so the bare + /// symbol verdict decides. + package func isExcludedByExportFilter(globalSymbolNames symbolNames: [String]) -> Bool { + isExportFilterEnabled && exportVerdict(forSymbolNames: symbolNames) == false + } + + private func isExcludedByExportFilter(isOverride: Bool, isObjC: Bool, symbolNames: [String]) -> Bool { + !isOverride && !isObjC && exportVerdict(forSymbolNames: symbolNames) == false + } +} diff --git a/Sources/SwiftPrinting/SwiftDeclarationPrinter+Headers.swift b/Sources/SwiftPrinting/SwiftDeclarationPrinter+Headers.swift index 98901c6d..562f0871 100644 --- a/Sources/SwiftPrinting/SwiftDeclarationPrinter+Headers.swift +++ b/Sources/SwiftPrinting/SwiftDeclarationPrinter+Headers.swift @@ -352,9 +352,18 @@ extension SwiftDeclarationPrinter { await fieldLayoutRenderer.enumPrefixComments(enumLayout: enumLayout) } - for (offset, field) in typeDefinition.fields.offsetEnumerated() { + // Exported-only filter (evolution proposal + // `exported-only-interface`): the rendered fields are selected + // up front, so a dropped field keeps every survivor's ORIGINAL index + // (field records and layout comments are positional) and the trailing + // break still follows the last field actually rendered. Enum cases + // own no symbols and are never filtered. + let renderedFields = Array(typeDefinition.fields.enumerated()).filter { isEnum || !isExcludedByExportFilter(field: $0.element) } + for (offset, indexedField) in renderedFields.offsetEnumerated() { + let fieldIndex = indexedField.offset + let field = indexedField.element BreakLine() - let fieldRecord = fieldRecords[safe: offset.index] + let fieldRecord = fieldRecords[safe: fieldIndex] let mangledTypeName = try fieldRecord?.mangledTypeName(in: machO) // Per-record metadata comments (single source of truth with the // `SwiftDump` dumpers): struct/class fields get the offset + @@ -362,9 +371,9 @@ extension SwiftDeclarationPrinter { // block. if let mangledTypeName { if isEnum { - try await fieldLayoutRenderer.enumCaseComments(forCaseAtIndex: offset.index, mangledTypeName: mangledTypeName, enumLayout: enumLayout) + try await fieldLayoutRenderer.enumCaseComments(forCaseAtIndex: fieldIndex, mangledTypeName: mangledTypeName, enumLayout: enumLayout) } else { - try await fieldLayoutRenderer.storedFieldComments(forFieldAtIndex: offset.index, mangledTypeName: mangledTypeName, fieldOffsets: fieldOffsets) + try await fieldLayoutRenderer.storedFieldComments(forFieldAtIndex: fieldIndex, mangledTypeName: mangledTypeName, fieldOffsets: fieldOffsets) } } // A non-final stored `var`'s getter/setter occupy vtable slots diff --git a/Sources/SwiftPrinting/SwiftDeclarationPrinter.swift b/Sources/SwiftPrinting/SwiftDeclarationPrinter.swift index c8c5abea..d936fb44 100644 --- a/Sources/SwiftPrinting/SwiftDeclarationPrinter.swift +++ b/Sources/SwiftPrinting/SwiftDeclarationPrinter.swift @@ -22,8 +22,24 @@ public final class SwiftDeclarationPrinter: Sendab @Mutex public private(set) var configuration: SwiftDeclarationPrintConfiguration = .init() + /// Resolvers binned per role at registration time (`addTypeNameResolver`), + /// so each delegate query walks only the resolvers that can answer it and + /// the print path never runs a conformance cast. + private struct TypeNameResolverRegistry: Sendable { + var moduleNameResolvers: [any ModuleNameResolving] = [] + var cImportedNameResolvers: [any CImportedNameResolving] = [] + var opaqueTypeResolvers: [any OpaqueTypeResolving] = [] + } + @Mutex - public private(set) var typeNameResolvers: [any TypeNameResolvable] = [] + private var typeNameResolverRegistry: TypeNameResolverRegistry = .init() + + /// The in-image non-exported declaration names the exported-only filter + /// consults for `extension` targets — installed by + /// `installExportFilterScope(types:protocols:)`, consulted only while + /// `configuration.printExportedDeclarationsOnly` is set. + @Mutex + var exportFilterScope: ExportFilterScope = .empty /// `package` so in-package renderers that drive this printer — notably /// ``SwiftDiffableInterfaceRenderer`` — report their own degradations into @@ -124,16 +140,53 @@ public final class SwiftDeclarationPrinter: Sendab _memoizedStaticFieldLayoutProvider.withLock { $0 = .uncomputed } } - public func addTypeNameResolver(_ resolver: any TypeNameResolvable) { - typeNameResolvers.append(resolver) + public func addTypeNameResolver(_ resolver: any TypeNameResolving) { + _typeNameResolverRegistry.withLock { registry in + var matchedAnyRole = false + if let moduleNameResolver = resolver as? any ModuleNameResolving { + registry.moduleNameResolvers.append(moduleNameResolver) + matchedAnyRole = true + } + if let cImportedNameResolver = resolver as? any CImportedNameResolving { + registry.cImportedNameResolvers.append(cImportedNameResolver) + matchedAnyRole = true + } + if let opaqueTypeResolver = resolver as? any OpaqueTypeResolving { + registry.opaqueTypeResolvers.append(opaqueTypeResolver) + matchedAnyRole = true + } + assert(matchedAnyRole, "resolver conforms to no role protocol and would never be consulted") + } } public func removeAllTypeNameResolvers() { - typeNameResolvers.removeAll() + typeNameResolverRegistry = .init() } - @SemanticStringBuilder + /// Exported-only gate (evolution proposal `exported-only-interface`): + /// a type whose descriptor provably is not exported renders as NOTHING — + /// an empty result every enclosing `BlockList` / `NestedDeclaration` + /// skips without a stray break. Ruled before the start event, so a + /// filtered definition leaves no unpaired `definitionPrintStarted`. + /// + /// The four per-definition entries (`printTypeDefinition` / + /// `printProtocolDefinition` / `printExtensionDefinition` / + /// `printDefinition`) run on the demangler's large-stack task executor + /// (`LargeStackTaskExecution.run`, evolution proposal + /// `large-stack-executor-and-cross-version-parallelism`): a host printing + /// one declaration at a time — RuntimeViewer's per-type export bypasses + /// `printRoot` — gets inline demangling too. Reached from `printRoot` or + /// from a parent's nested-children loop the task is already on the + /// executor and the wrap is a no-op. public func printTypeDefinition(_ typeDefinition: TypeDefinition, level: Int = 1, displayParentName: Bool = false) async throws -> SemanticString { + guard !isExcludedByExportFilter(typeDefinition) else { return SemanticString() } + return try await LargeStackTaskExecution.run { + try await printIncludedTypeDefinition(typeDefinition, level: level, displayParentName: displayParentName) + } + } + + @SemanticStringBuilder + private func printIncludedTypeDefinition(_ typeDefinition: TypeDefinition, level: Int, displayParentName: Bool) async throws -> SemanticString { let printingContext = SwiftIndexEvents.PrintingContext(name: typeDefinition.typeName.name, kind: .type) eventDispatcher.dispatch(.definitionPrintStarted(context: printingContext)) @@ -206,8 +259,18 @@ public final class SwiftDeclarationPrinter: Sendab eventDispatcher.dispatch(.definitionPrintCompleted(context: printingContext)) } - @SemanticStringBuilder + /// Exported-only gate, same contract as `printTypeDefinition`: a protocol + /// whose descriptor provably is not exported renders as nothing — its + /// trailing default-implementation extensions go with it. public func printProtocolDefinition(_ protocolDefinition: ProtocolDefinition, level: Int = 1, displayParentName: Bool = false) async throws -> SemanticString { + guard !isExcludedByExportFilter(protocolDefinition) else { return SemanticString() } + return try await LargeStackTaskExecution.run { + try await printIncludedProtocolDefinition(protocolDefinition, level: level, displayParentName: displayParentName) + } + } + + @SemanticStringBuilder + private func printIncludedProtocolDefinition(_ protocolDefinition: ProtocolDefinition, level: Int, displayParentName: Bool) async throws -> SemanticString { // Context and start event FIRST, exactly like `printTypeDefinition`. // The materialization below throws (proposal 0002 rebuilt the wrapper // from its descriptor), and a failure that precedes the start event @@ -265,8 +328,22 @@ public final class SwiftDeclarationPrinter: Sendab eventDispatcher.dispatch(.definitionPrintCompleted(context: printingContext)) } - @SemanticStringBuilder + /// Exported-only gate for extensions. Two rules, ruled at different + /// points: an extension TARGETING an in-image non-exported declaration + /// is dropped before any event (like a filtered type); a non-conformance + /// extension the filter EMPTIES needs its members, which only + /// `index(in:)` provides, so that rule is evaluated after the start + /// event and the index — and reports a paired completion around an + /// empty result, keeping the event stream's start/completion pairing + /// intact on that path too. public func printExtensionDefinition(_ extensionDefinition: ExtensionDefinition, level: Int = 1) async throws -> SemanticString { + guard !isExcludedByExportFilter(extensionDefinition) else { return SemanticString() } + return try await LargeStackTaskExecution.run { + try await printExtensionDefinitionContents(extensionDefinition, level: level) + } + } + + private func printExtensionDefinitionContents(_ extensionDefinition: ExtensionDefinition, level: Int) async throws -> SemanticString { let printingContext = SwiftIndexEvents.PrintingContext(name: extensionDefinition.extensionName.name, kind: .extension) eventDispatcher.dispatch(.definitionPrintStarted(context: printingContext)) @@ -274,6 +351,19 @@ public final class SwiftDeclarationPrinter: Sendab try await extensionDefinition.index(in: machO) } + let rendered: SemanticString + if isEmptiedByExportFilter(extensionDefinition) { + rendered = SemanticString() + } else { + rendered = try await printIncludedExtensionDefinition(extensionDefinition, level: level) + } + + eventDispatcher.dispatch(.definitionPrintCompleted(context: printingContext)) + return rendered + } + + @SemanticStringBuilder + private func printIncludedExtensionDefinition(_ extensionDefinition: ExtensionDefinition, level: Int) async throws -> SemanticString { try await DeclarationBlock(level: level) { try await printExtensionHeader(extensionDefinition, level: level) } body: { @@ -314,8 +404,6 @@ public final class SwiftDeclarationPrinter: Sendab try await printDefinition(extensionDefinition, level: 1) } - - eventDispatcher.dispatch(.definitionPrintCompleted(context: printingContext)) } /// Renders an extension's header line (`extension Foo : Bar where …`) with no @@ -393,8 +481,14 @@ public final class SwiftDeclarationPrinter: Sendab } } - @SemanticStringBuilder public func printDefinition(_ definition: some Definition, level: Int = 1) async throws -> SemanticString { + try await LargeStackTaskExecution.run { + try await printDefinitionContents(definition, level: level) + } + } + + @SemanticStringBuilder + private func printDefinitionContents(_ definition: some Definition, level: Int) async throws -> SemanticString { if let mutableDefinition = definition as? MutableDefinition, !mutableDefinition.isIndexed { try await mutableDefinition.index(in: machO) } @@ -419,7 +513,7 @@ public final class SwiftDeclarationPrinter: Sendab let vtableTransformerClosure = vtableOffsetTransformerClosure await MemberList(level: level) { - for member in definition.orderedMembers { + for member in definition.orderedMembers where !isExcludedByExportFilter(member) { await renderMember(member, level: level, offsetCommentPrefix: offsetCommentPrefix, emitOffsetComment: emitOffsetComment, printVTableOffset: printVTableOffset, printMemberAddress: printMemberAddress, printExportStatus: printExportStatus, vtableTransformerClosure: vtableTransformerClosure) } @@ -452,7 +546,7 @@ public final class SwiftDeclarationPrinter: Sendab for category in MemberCategory.allCases { await MemberList(level: level) { - for member in definition.members(in: category) { + for member in definition.members(in: category) where !isExcludedByExportFilter(member) { await renderMember(member, level: level, offsetCommentPrefix: offsetCommentPrefix, emitOffsetComment: emitOffsetComment, printVTableOffset: printVTableOffset, printMemberAddress: printMemberAddress, printExportStatus: printExportStatus, vtableTransformerClosure: vtableTransformerClosure) } } @@ -729,14 +823,14 @@ package func printCatchedThrowing( extension SwiftDeclarationPrinter: NodePrintableDelegate { public func moduleName(forTypeName typeName: String) async -> String? { - await typeNameResolvers.asyncFirstNonNil { await $0.moduleName(forTypeName: typeName) } + await typeNameResolverRegistry.moduleNameResolvers.asyncFirstNonNil { await $0.moduleName(forTypeName: typeName) } } public func swiftName(forCName cName: String, category: CImportedTypeNameCategory) async -> String? { - await typeNameResolvers.asyncFirstNonNil { await $0.swiftName(forCName: cName, category: category) } + await typeNameResolverRegistry.cImportedNameResolvers.asyncFirstNonNil { await $0.swiftName(forCName: cName, category: category) } } public func opaqueType(forNode node: Node, index: Int?) async -> String? { - await typeNameResolvers.asyncFirstNonNil { await $0.opaqueType(forNode: node, index: index) } + await typeNameResolverRegistry.opaqueTypeResolvers.asyncFirstNonNil { await $0.opaqueType(forNode: node, index: index) } } } diff --git a/Sources/TypeIndexing/SwiftInterfaceBuilderTypeNameProvider.swift b/Sources/TypeIndexing/SwiftInterfaceBuilderTypeNameProvider.swift index 2ddac2f3..6e2c8e52 100644 --- a/Sources/TypeIndexing/SwiftInterfaceBuilderTypeNameProvider.swift +++ b/Sources/TypeIndexing/SwiftInterfaceBuilderTypeNameProvider.swift @@ -13,7 +13,7 @@ import SwiftPrinting /// `Foundation.NSString` through a ``TypeDatabase`` built for the indexed /// binary's platform and dependency set. @available(macOS 13.0, *) -public final class SwiftInterfaceBuilderTypeNameProvider: SwiftInterfaceBuilderExtraDataProvider, Sendable { +public final class SwiftInterfaceBuilderTypeNameProvider: SwiftInterfaceBuilderExtraDataProvider, ModuleNameResolving, CImportedNameResolving, Sendable { public let machO: MachO private let typeDatabase: TypeDatabase diff --git a/Sources/Utilities/BoundedConcurrentMap.swift b/Sources/Utilities/BoundedConcurrentMap.swift new file mode 100644 index 00000000..9c0e6523 --- /dev/null +++ b/Sources/Utilities/BoundedConcurrentMap.swift @@ -0,0 +1,68 @@ +extension Collection where Element: Sendable { + /// Transforms every element with at most `maximumConcurrency` transforms + /// in flight, returning the results in source order. + /// + /// Submission is windowed: the first `maximumConcurrency` elements start + /// at once and each completion admits the next, so a bounded number of + /// transforms ever runs concurrently — the shape cross-version preparation + /// needs (evolution proposal + /// `large-stack-executor-and-cross-version-parallelism`), where each + /// in-flight transform holds an indexed image's memory and a thread. A + /// `maximumConcurrency` of 1 runs the elements strictly one after the + /// other, in order; values below 1 count as 1. + /// + /// Two things stop the submission of elements not yet started, and in + /// both the pending elements never start: the first failure, which is + /// rethrown; and cancellation of the calling task, after which the call + /// throws `CancellationError` — never a partial array. Transforms already + /// in flight run to completion first (a task group waits for its + /// children, and the transforms this library passes do not observe + /// cancellation), and their results are discarded. A cancellation that + /// arrives after the last element was submitted changes nothing: the + /// work is done, so the results are returned. + /// + /// Child tasks inherit the caller's task executor preference, so under + /// `LargeStackTaskExecution.run` every transform runs on the large-stack + /// executor. + public func concurrentMap( + maximumConcurrency: Int, + _ transform: @escaping @Sendable (Element) async throws -> Result + ) async throws -> [Result] { + let window = Swift.max(1, maximumConcurrency) + return try await withThrowingTaskGroup(of: (index: Int, result: Result).self) { group in + var results = [Result?](repeating: nil, count: count) + var pending = enumerated().makeIterator() + + // `addTaskUnlessCancelled`, not `addTask`: a cancelled group still + // accepts children through `addTask`, which is how a cancelled + // multi-version preparation used to index every remaining version + // to the end. A refused submission means the calling task was + // cancelled; throwing here is what makes the group cancel and + // drain its in-flight children and the call fail as a whole + // (returning `results` with holes would trap on the unwrap below). + var started = 0 + while started < window, let (index, element) = pending.next() { + guard group.addTaskUnlessCancelled(operation: { (index, try await transform(element)) }) else { + throw CancellationError() + } + started += 1 + } + + while let (index, result) = try await group.next() { + results[index] = result + if let (nextIndex, nextElement) = pending.next() { + guard group.addTaskUnlessCancelled(operation: { (nextIndex, try await transform(nextElement)) }) else { + throw CancellationError() + } + } + } + + return results.map { result in + // Every index is filled once `next()` returns nil without + // throwing: each submitted task reports exactly one index, and + // a refused submission threw above. + result! + } + } + } +} diff --git a/Sources/swift-section/Commands/DiffCommand.swift b/Sources/swift-section/Commands/DiffCommand.swift index e24f21cc..2b56c27b 100644 --- a/Sources/swift-section/Commands/DiffCommand.swift +++ b/Sources/swift-section/Commands/DiffCommand.swift @@ -60,6 +60,16 @@ struct DiffCommand: AsyncParsableCommand { @Option(name: .shortAndLong, help: "Write the report to this path instead of stdout.", completion: .file()) var outputPath: String? + @Option(name: .long, help: "How many inputs to index at once (default: the processor count). Pass 1 to index the old side, then the new side.") + var jobs: Int? + + /// The concurrency window for indexing the two sides (evolution proposal + /// `large-stack-executor-and-cross-version-parallelism`): both inputs are + /// independent files, so by default they index in parallel. + private var maximumConcurrentPreparations: Int { + jobs ?? ProcessInfo.processInfo.activeProcessorCount + } + func run() async throws { let abiDiff: ABIDiff? if interface { @@ -78,13 +88,15 @@ struct DiffCommand: AsyncParsableCommand { // to its printers, so this is what puts a dropped declaration on // stderr instead of leaving it to `Dispatcher`'s os_log floor, which // a CLI operator never sees. - log("Indexing old binary…") - let oldBuilder = SwiftDiffableInterfaceBuilder(eventHandlers: [ConsoleEventHandler()], in: oldMachO) - try await oldBuilder.prepare() - - log("Indexing new binary…") - let newBuilder = SwiftDiffableInterfaceBuilder(eventHandlers: [ConsoleEventHandler()], in: newMachO) - try await newBuilder.prepare() + let oldBuilder = SwiftDiffableInterfaceBuilder(eventHandlers: [ConsoleEventHandler(label: "old")], in: oldMachO) + let newBuilder = SwiftDiffableInterfaceBuilder(eventHandlers: [ConsoleEventHandler(label: "new")], in: newMachO) + // Old side first in the window, so `--jobs 1` is the historical + // order; with a wider window the two index side by side and their + // diagnostics interleave on stderr. + log(maximumConcurrentPreparations > 1 ? "Indexing old and new binaries…" : "Indexing old binary, then new binary…") + _ = try await [oldBuilder, newBuilder].concurrentMap(maximumConcurrency: maximumConcurrentPreparations) { builder in + try await builder.prepare() + } // Only the `--fail-on-breaking` CI gate needs the ABI diff on the // annotated-interface path. @@ -109,8 +121,10 @@ struct DiffCommand: AsyncParsableCommand { // The change-list path is snapshot-based either way, so each side // may be a binary (indexed and frozen here) or a persisted // baseline (decoded, with its format version validated). - let oldDocument = try await loadDocument(at: oldPath) - let newDocument = try await loadDocument(at: newPath) + let documents = try await [(path: oldPath, side: "old"), (path: newPath, side: "new")].concurrentMap(maximumConcurrency: maximumConcurrentPreparations) { input in + try await loadDocument(at: input.path, consoleLabel: input.side) + } + let (oldDocument, newDocument) = (documents[0], documents[1]) log("Diffing…") let diff = ABIDiffer().diff(old: oldDocument, new: newDocument) @@ -147,6 +161,9 @@ struct DiffCommand: AsyncParsableCommand { if format != nil, !interface { throw ValidationError("--format only applies to the annotated interface; pass --interface.") } + if let jobs, jobs < 1 { + throw ValidationError("--jobs must be at least 1.") + } if cacheImageName != nil, cacheImagePath != nil { throw ValidationError("--cache-image-name and --cache-image-path are mutually exclusive; pass only one.") } @@ -178,7 +195,7 @@ struct DiffCommand: AsyncParsableCommand { /// Loads one change-list-path input: a snapshot JSON is decoded, a binary /// is indexed and frozen (with provenance stamped). - private func loadDocument(at path: String) async throws -> ABISnapshotDocument { + private func loadDocument(at path: String, consoleLabel: String) async throws -> ABISnapshotDocument { try await ABISnapshotInputLoader.loadDocument( path: path, architecture: architecture, @@ -186,6 +203,7 @@ struct DiffCommand: AsyncParsableCommand { cacheImageName: cacheImageName, cacheImagePath: cacheImagePath, label: nil, + consoleLabel: consoleLabel, log: log ) } diff --git a/Sources/swift-section/Commands/EvolutionCommand.swift b/Sources/swift-section/Commands/EvolutionCommand.swift index 1ab510b6..1885859e 100644 --- a/Sources/swift-section/Commands/EvolutionCommand.swift +++ b/Sources/swift-section/Commands/EvolutionCommand.swift @@ -51,6 +51,18 @@ struct EvolutionCommand: AsyncParsableCommand { @Option(name: .shortAndLong, help: "Write the report to this path instead of stdout.", completion: .file()) var outputPath: String? + @Option(name: .long, help: "How many inputs to index at once (default: the processor count). Pass 1 to index the versions one after the other, oldest first.") + var jobs: Int? + + /// The concurrency window for indexing the inputs (evolution proposal + /// `large-stack-executor-and-cross-version-parallelism`): every input is + /// an independent file, so by default up to one per processor index at + /// once. Each in-flight input holds its indexed image in memory, which is + /// what `--jobs` trades against. + private var maximumConcurrentPreparations: Int { + jobs ?? ProcessInfo.processInfo.activeProcessorCount + } + func run() async throws { let explicitLabels = try ABISnapshotInputLoader.parseLabels(labels, inputCount: inputPaths.count) @@ -59,9 +71,8 @@ struct EvolutionCommand: AsyncParsableCommand { return } - var documents: [ABISnapshotDocument] = [] - for (index, inputPath) in inputPaths.enumerated() { - let document = try await ABISnapshotInputLoader.loadDocument( + let documents = try await Array(inputPaths.enumerated()).concurrentMap(maximumConcurrency: maximumConcurrentPreparations) { index, inputPath in + try await ABISnapshotInputLoader.loadDocument( path: inputPath, architecture: architecture, isDyldSharedCache: isDyldSharedCache, @@ -70,7 +81,6 @@ struct EvolutionCommand: AsyncParsableCommand { label: explicitLabels[index], log: log ) - documents.append(document) } // Snapshot inputs may already carry a provenance label; binaries fall @@ -128,12 +138,12 @@ struct EvolutionCommand: AsyncParsableCommand { // builder, because the version count is a runtime value here — the // pack-generic SwiftEvolutionInterfaceBuilder's arity is compile-time. let builder = try AnySwiftEvolutionInterfaceBuilder( - eventHandlers: [ConsoleEventHandler()], + eventHandlersPerVersion: { _, label in [ConsoleEventHandler(label: label)] }, versions: machOFiles, labels: resolvedLabels ) - log("Indexing \(machOFiles.count) versions…") - try await builder.prepare() + log("Indexing \(machOFiles.count) versions (\(min(maximumConcurrentPreparations, machOFiles.count)) at a time)…") + try await builder.prepare(maximumConcurrentPreparations: maximumConcurrentPreparations) log("Rendering annotated interface…") let annotated = try await builder.printAnnotatedInterface() try emitInterface(annotated.string) @@ -208,6 +218,9 @@ struct EvolutionCommand: AsyncParsableCommand { if json, summaryOnly { throw ValidationError("--json and --summary-only are mutually exclusive.") } + if let jobs, jobs < 1 { + throw ValidationError("--jobs must be at least 1.") + } if cacheImageName != nil, cacheImagePath != nil { throw ValidationError("--cache-image-name and --cache-image-path are mutually exclusive; pass only one.") } diff --git a/Sources/swift-section/Commands/InterfaceCommand.swift b/Sources/swift-section/Commands/InterfaceCommand.swift index be43d198..2c6bd1dc 100644 --- a/Sources/swift-section/Commands/InterfaceCommand.swift +++ b/Sources/swift-section/Commands/InterfaceCommand.swift @@ -65,6 +65,9 @@ struct InterfaceCommand: AsyncParsableCommand { @Flag(help: "Annotate members none of whose symbols have an export-trie entry with a `not exported` comment") var emitExportStatus: Bool = false + @Flag(name: .customLong("exported-only"), help: "Print only the declarations the image exports: types and protocols whose descriptor symbol has an export-trie entry, extensions targeting them, and members with at least one exported symbol (dispatch-thunk and other derived forms included). `override` / `@objc` members and anything without export evidence are kept.") + var exportedOnly: Bool = false + @Option(name: .shortAndLong, help: "The color scheme for the output.") var colorScheme: SemanticColorScheme = .none @@ -80,6 +83,7 @@ struct InterfaceCommand: AsyncParsableCommand { printMemberAddress: emitMemberAddresses, printVTableOffset: emitVtableOffsets, printExportStatus: emitExportStatus, + printExportedDeclarationsOnly: exportedOnly, memberSortOrder: sortMembersByOffset ? .byOffset : .byCategory, printTypeLayout: emitTypeLayout, printEnumLayout: emitEnumLayout @@ -122,15 +126,18 @@ struct InterfaceCommand: AsyncParsableCommand { if #available(macOS 13.0, *) { let providerDependencies = SwiftInterfaceBuilderDependencies( machO: machOFile, - paths: [.usesSystemDyldSharedCache], + searchPaths: [.systemDyldSharedCache], eventHandlers: [ConsoleEventHandler()] ) // Dependency resolution against the HOST dyld cache matches - // install names exactly; a non-macOS binary's paths mostly - // miss, which silently guts the SDK-interface source. Say so - // instead of degrading quietly. + // install names exactly, then bare names; a non-macOS binary's + // paths mostly miss both, which silently guts the SDK-interface + // source. Say so — and name the misses — instead of degrading + // quietly. if providerDependencies.dependencies.isEmpty { fputs("warning: --resolve-c-module-names resolved no dependency images against this host (non-macOS binary?); attribution will be limited to SDK APINotes and supplementary files\n", stderr) + } else if !providerDependencies.unresolvedLoadNames.isEmpty { + fputs("warning: --resolve-c-module-names could not resolve \(providerDependencies.unresolvedLoadNames.count) dependency image(s) against this host; their types will not be attributed: \(providerDependencies.unresolvedLoadNames.joined(separator: ", "))\n", stderr) } // Bad supplementary paths are otherwise only os_log'd by the // library floor; a CLI user who mistyped a path or handed a diff --git a/Sources/swift-section/Utilities/ABISnapshotInputLoader.swift b/Sources/swift-section/Utilities/ABISnapshotInputLoader.swift index e5e86903..5f2220c7 100644 --- a/Sources/swift-section/Utilities/ABISnapshotInputLoader.swift +++ b/Sources/swift-section/Utilities/ABISnapshotInputLoader.swift @@ -27,7 +27,10 @@ enum ABISnapshotInputLoader { /// Load one input as a frozen snapshot document. A JSON path decodes (with /// the format-version check); a binary path is loaded, indexed, and frozen, /// with provenance stamped from the load parameters. `label` overrides the - /// document's provenance label either way. + /// document's provenance label either way. `consoleLabel` tags the + /// input's stderr diagnostics (inputs index concurrently under `--jobs`, + /// so an unlabeled line cannot be attributed); it defaults to `label`, + /// then to the input's file name. static func loadDocument( path: String, architecture: Architecture?, @@ -35,6 +38,7 @@ enum ABISnapshotInputLoader { cacheImageName: String?, cacheImagePath: String?, label: String?, + consoleLabel: String? = nil, log: (String) -> Void ) async throws -> ABISnapshotDocument { if try isSnapshotDocument(atPath: path) { @@ -59,7 +63,10 @@ enum ABISnapshotInputLoader { ) // Shared by `snapshot` / `evolution` / `diff`'s snapshot inputs, so this // is where those three get a stderr sink for anything indexing drops. - let builder = SwiftDiffableInterfaceBuilder(eventHandlers: [ConsoleEventHandler()], in: machOFile) + let builder = SwiftDiffableInterfaceBuilder( + eventHandlers: [ConsoleEventHandler(label: consoleLabel ?? label ?? defaultLabel(forPath: path))], + in: machOFile + ) try await builder.prepare() let cacheImageSuffix = [cacheImageName, cacheImagePath].compactMap { $0 }.first.map { " (\($0))" } ?? "" let provenance = ABIProvenance( diff --git a/Sources/swift-section/Version.swift b/Sources/swift-section/Version.swift index 9b812f0a..d16524ba 100644 --- a/Sources/swift-section/Version.swift +++ b/Sources/swift-section/Version.swift @@ -2,5 +2,5 @@ // When bumping: also add Changelogs/.md, then tag the release with the same string. // Verified by .github/workflows/version-check.yml (PR) and .github/workflows/release.yml (tag). enum BundledVersion { - static let value = "0.17.1" + static let value = "0.19.0" } diff --git a/Tests/IntegrationTests/MachOSwiftSection/OpaqueTypeTests.swift b/Tests/IntegrationTests/MachOSwiftSection/OpaqueTypeTests.swift index 693451da..f748eb3f 100644 --- a/Tests/IntegrationTests/MachOSwiftSection/OpaqueTypeTests.swift +++ b/Tests/IntegrationTests/MachOSwiftSection/OpaqueTypeTests.swift @@ -1,4 +1,5 @@ import Foundation +import MachOResolving import Testing import SwiftDeclarationRendering import Demangling @@ -53,7 +54,7 @@ final class OpaqueTypeDyldCacheTests: DyldCacheTests, OpaqueTypeTests, @unchecke let machO = machOFileInCache print(machO.startOffset) try print(OpaqueType(descriptor: .resolve(from: 895065692, in: machO), in: machO)) - try await print(Symbols.resolve(from: 895065692, in: machO) as Symbols) + print(machO.symbols(offset: 895065692) as Symbols?) } } diff --git a/Tests/IntegrationTests/TypeIndexing/TypeNameProviderTests.swift b/Tests/IntegrationTests/TypeIndexing/TypeNameProviderTests.swift index 7f4c8225..cd61b531 100644 --- a/Tests/IntegrationTests/TypeIndexing/TypeNameProviderTests.swift +++ b/Tests/IntegrationTests/TypeIndexing/TypeNameProviderTests.swift @@ -28,7 +28,7 @@ final class TypeNameProviderMachOFileTests: MachOFileTests, @unchecked Sendable in: machOFile ) if resolvingCModuleNames { - let providerDependencies = SwiftInterfaceBuilderDependencies(machO: machOFile, paths: [.usesSystemDyldSharedCache]) + let providerDependencies = SwiftInterfaceBuilderDependencies(machO: machOFile, searchPaths: [.systemDyldSharedCache]) if let typeNameProvider = SwiftInterfaceBuilderTypeNameProvider(machO: machOFile, dependencies: providerDependencies) { builder.addExtraDataProvider(typeNameProvider) } else { diff --git a/Tests/MachODependenciesTests/DependencyClosureTests.swift b/Tests/MachODependenciesTests/DependencyClosureTests.swift new file mode 100644 index 00000000..1c136e32 --- /dev/null +++ b/Tests/MachODependenciesTests/DependencyClosureTests.swift @@ -0,0 +1,194 @@ +import Foundation +import MachOFixtureSupport +import MachOKit +import MachOKitExtensions +import Testing +@testable import MachODependencies +@testable import MachOTestingSupport + +/// Covers the traversal contract of `DependencyClosure` on the +/// `SymbolTestsCore` fixture, whose load commands mix every shape the +/// locators must handle: an `@rpath` sibling framework (`SymbolTestsHelper`, +/// resolvable in-process and through an explicit file, never through the +/// cache), absolute system frameworks (`Foundation`), and absolute Swift +/// runtime dylibs, some weakly linked and not necessarily mapped in the test +/// process. +/// +/// Declares `SymbolTestsHelper` because the offline closure reaches that +/// binary by a hand-built path — the sharing rule is about every suite that +/// touches the image, not only the ones asserting on its caches. +@Suite(ExclusiveImageAccess(.SymbolTestsHelper)) +final class DependencyClosureTests: MachOSwiftSectionFixtureTests, @unchecked Sendable { + /// On-disk path of the helper framework binary, derived from this file's + /// location: a `MachOFile`'s `imagePath` is its install name + /// (`@rpath/…`), so the explicit search path cannot come from the root. + private static let symbolTestsHelperOnDiskPath: String = { + URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() // Tests/MachODependenciesTests + .deletingLastPathComponent() // Tests + .deletingLastPathComponent() // repository root + .appendingPathComponent("Tests/Projects/SymbolTests/DerivedData/SymbolTests/Build/Products/Release/SymbolTestsHelper.framework/Versions/A/SymbolTestsHelper") + .standardizedFileURL.path + }() + + private static let symbolTestsHelperLoadName = "@rpath/SymbolTestsHelper.framework/Versions/A/SymbolTestsHelper" + private static let swiftCoreLoadName = "/usr/lib/swift/libswiftCore.dylib" + + private func bareImageNames(of images: [MachO]) -> [String] { + images.map { DependencyLoadName.bareImageName(of: $0.imagePath) } + } + + private func directLoadNameBareImageNames(of root: MachO) -> [String] { + var seen: Set = [] + return root.dependencies.map { DependencyLoadName.bareImageName(of: $0.dylib.name) }.filter { seen.insert($0).inserted } + } + + // MARK: - In-process + + /// The image initializer must resolve the dependencies dyld has mapped — + /// including the ones linked by absolute path, which only resolve once + /// the load name is normalized to a bare name. + @MainActor + @Test func inProcessDirectClosureResolvesMappedDependencies() throws { + let root = machOImage + let closure = DependencyClosure(root: root, traversal: .direct) + + let resolvedBareImageNames = bareImageNames(of: closure.images) + #expect(resolvedBareImageNames.contains("SymbolTestsHelper"), "the @rpath sibling is mapped by the fixture's dlopen") + #expect(resolvedBareImageNames.contains("libswiftCore"), "an absolute load path must resolve after bare-name normalization") + #expect(resolvedBareImageNames.contains("Foundation")) + + let directBareImageNames = Set(directLoadNameBareImageNames(of: root)) + #expect(Set(resolvedBareImageNames).isSubset(of: directBareImageNames), "a direct traversal never follows a dependency's own load commands") + #expect(Set(closure.unresolvedLoadNames.map(DependencyLoadName.bareImageName(of:))).isDisjoint(with: resolvedBareImageNames)) + #expect(resolvedBareImageNames.count + closure.unresolvedLoadNames.count == directBareImageNames.count, "every direct load name is either resolved or reported, exactly once") + #expect(!resolvedBareImageNames.contains("SymbolTestsCore"), "the root is never its own dependency") + #expect(closure.searchPathLoadFailures.isEmpty, "the in-process locator has no search paths to fail") + } + + /// Transitive resolution extends the direct set without reordering it: + /// the direct dependencies come first, in load-command order, then the + /// next level. A lazily indexing consumer relies on that prefix. + @MainActor + @Test func inProcessTransitiveClosureExtendsTheDirectPrefixBreadthFirst() throws { + let root = machOImage + let direct = DependencyClosure(root: root, traversal: .direct) + let transitive = DependencyClosure(root: root, traversal: .transitive) + + let directBareImageNames = bareImageNames(of: direct.images) + let transitiveBareImageNames = bareImageNames(of: transitive.images) + #expect(Array(transitiveBareImageNames.prefix(directBareImageNames.count)) == directBareImageNames) + #expect(transitiveBareImageNames.count > directBareImageNames.count, "Foundation alone pulls in images the fixture does not link directly") + #expect(Set(transitiveBareImageNames).count == transitiveBareImageNames.count, "deduplicated by bare image name") + #expect(!transitiveBareImageNames.contains("SymbolTestsCore")) + } + + // MARK: - Offline + + @MainActor + @Test func offlineClosureResolvesExplicitFilesAndCacheImages() throws { + guard FullDyldCache.host != nil else { + print("skipped: no host dyld shared cache") + return + } + let root = machOFile + let closure = DependencyClosure( + root: root, + searchPaths: [.machOFile(path: Self.symbolTestsHelperOnDiskPath), .systemDyldSharedCache], + traversal: .direct + ) + + #expect(closure.searchPathLoadFailures.isEmpty) + let helper = try #require(closure.images.first { DependencyLoadName.bareImageName(of: $0.imagePath) == "SymbolTestsHelper" }) + #expect(helper.imagePath == Self.symbolTestsHelperLoadName, "the explicit file is the one the @rpath load name names") + let swiftCore = try #require(closure.images.first { DependencyLoadName.bareImageName(of: $0.imagePath) == "libswiftCore" }) + #expect(swiftCore.imagePath == Self.swiftCoreLoadName, "a cache image resolves by its exact install path") + #expect(!closure.unresolvedLoadNames.contains(Self.symbolTestsHelperLoadName)) + } + + /// Without the explicit file the `@rpath` sibling is exactly the kind of + /// dependency the cache cannot answer — it must be reported, not dropped. + @MainActor + @Test func offlineClosureReportsWhatTheCacheCannotAnswer() throws { + guard FullDyldCache.host != nil else { + print("skipped: no host dyld shared cache") + return + } + let closure = DependencyClosure(root: machOFile, searchPaths: [.systemDyldSharedCache], traversal: .direct) + #expect(closure.unresolvedLoadNames.contains(Self.symbolTestsHelperLoadName)) + #expect(!bareImageNames(of: closure.images).contains("SymbolTestsHelper")) + } + + @MainActor + @Test func offlineClosureWithNoSearchPathsReportsEveryDependency() throws { + let root = machOFile + let closure = DependencyClosure(root: root, searchPaths: [], traversal: .transitive) + #expect(closure.images.isEmpty) + #expect(closure.unresolvedLoadNames.map(DependencyLoadName.bareImageName(of:)) == directLoadNameBareImageNames(of: root), "reported in load-command order, deduplicated by bare name") + } + + @MainActor + @Test func unopenableSearchPathIsRecordedRatherThanThrown() throws { + let missingPath = "/nonexistent/MachODependenciesTests/Missing.framework/Missing" + let closure = DependencyClosure(root: machOFile, searchPaths: [.machOFile(path: missingPath)], traversal: .direct) + #expect(closure.searchPathLoadFailures.count == 1) + #expect(closure.searchPathLoadFailures.first?.searchPath == .machOFile(path: missingPath)) + #expect(closure.images.isEmpty) + } + + // MARK: - Locator seam + + /// A locator conforming to `DependencyLocating` receives every load name + /// in its raw load-command spelling, exactly once per bare name. + private final class RecordingLocator: DependencyLocating { + private(set) var receivedLoadNames: [String] = [] + private let imagesByBareImageName: [String: MachOFile] + + init(imagesByBareImageName: [String: MachOFile]) { + self.imagesByBareImageName = imagesByBareImageName + } + + func locate(loadName: String) -> MachOFile? { + receivedLoadNames.append(loadName) + return imagesByBareImageName[DependencyLoadName.bareImageName(of: loadName)] + } + } + + /// One image reached under two spellings is collected once. The file + /// locator registers an explicit file under its on-disk path, its install + /// name and its bare name, so a root that links the same binary under two + /// load names would otherwise get it twice: bare-name deduplication sees + /// two names, but the resolved image is one, and `ImageUniverse` would + /// index it twice. Identity is `MachORepresentableWithCache.identifier` + /// (`LC_UUID`-keyed for a file), so two `MachOFile` values over the same + /// binary compare equal. + @MainActor + @Test func sameImageReachedUnderTwoLoadNamesIsCollectedOnce() throws { + let root = machOFile + let helperFile = try #require(File.loadFromFile(url: URL(fileURLWithPath: Self.symbolTestsHelperOnDiskPath)).machOFiles.first) + // Two of the root's direct load names, both answered by the same file. + let locator = RecordingLocator(imagesByBareImageName: ["SymbolTestsHelper": helperFile, "libswiftCore": helperFile]) + + let closure = DependencyClosure(root: root, traversal: .direct, locator: locator) + + #expect(closure.images.count == 1, "the same image must not be collected under a second load name") + #expect(closure.images.first?.imagePath == helperFile.imagePath) + #expect(!closure.unresolvedLoadNames.contains(Self.symbolTestsHelperLoadName)) + #expect(!closure.unresolvedLoadNames.contains(Self.swiftCoreLoadName), "a load name answered by an already-collected image is resolved, not reported") + } + + @MainActor + @Test func customLocatorReceivesRawLoadNamesAndSuppliesTheImages() throws { + let root = machOFile + let helperFile = try #require(File.loadFromFile(url: URL(fileURLWithPath: Self.symbolTestsHelperOnDiskPath)).machOFiles.first) + let locator = RecordingLocator(imagesByBareImageName: ["SymbolTestsHelper": helperFile]) + + let closure = DependencyClosure(root: root, traversal: .direct, locator: locator) + + #expect(locator.receivedLoadNames.contains(Self.symbolTestsHelperLoadName), "the locator sees the @rpath spelling, not a pre-normalized name") + #expect(locator.receivedLoadNames == directLoadNameBareImageNames(of: root).map { bareImageName in root.dependencies.first { DependencyLoadName.bareImageName(of: $0.dylib.name) == bareImageName }!.dylib.name }) + #expect(closure.images.count == 1) + #expect(closure.images.first?.imagePath == helperFile.imagePath) + #expect(closure.unresolvedLoadNames.count == locator.receivedLoadNames.count - 1) + } +} diff --git a/Tests/MachODependenciesTests/DependencyLoadNameTests.swift b/Tests/MachODependenciesTests/DependencyLoadNameTests.swift new file mode 100644 index 00000000..4909aa11 --- /dev/null +++ b/Tests/MachODependenciesTests/DependencyLoadNameTests.swift @@ -0,0 +1,39 @@ +import Foundation +import MachOKit +import Testing +@testable import MachODependencies + +/// Pins the load-name → bare-image-name rule. It is a contract with MachOKit, +/// not a convenience: `MachOImage(name:)` compares the *same* reduction of +/// every mapped image's path, so any drift here silently turns the in-process +/// locator into a no-op (which is exactly how `SwiftInterfaceBuilderDependencies`'s +/// image initializer resolved nothing for its whole life — it handed the raw +/// load path to a bare-name lookup). +@Suite +struct DependencyLoadNameTests { + @Test(arguments: [ + ("@rpath/SymbolTestsHelper.framework/Versions/A/SymbolTestsHelper", "SymbolTestsHelper"), + ("/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation", "Foundation"), + ("/usr/lib/swift/libswiftCore.dylib", "libswiftCore"), + // The FIRST extension component is stripped, not the last — the rule + // MachOKit applies, so `libobjc.A.dylib` keys as `libobjc`. + ("/usr/lib/libobjc.A.dylib", "libobjc"), + ("libc++.1.dylib", "libc++"), + ("Foundation", "Foundation"), + ("", ""), + ]) + func bareImageNameStripsDirectoriesAndTheFirstExtension(loadName: String, expectedBareImageName: String) { + #expect(DependencyLoadName.bareImageName(of: loadName) == expectedBareImageName) + } + + /// The reduction must agree with `MachOImage(name:)` on a real mapped + /// image: the stdlib is loaded in every Swift process, so its absolute + /// load path — never matched raw — must resolve once normalized. + @Test func bareImageNameIsWhatMachOImageLookupMatches() { + let loadName = "/usr/lib/swift/libswiftCore.dylib" + #expect(MachOImage(name: loadName) == nil, "the raw load path must NOT match — that is the trap this rule exists for") + let image = MachOImage(name: DependencyLoadName.bareImageName(of: loadName)) + #expect(image != nil) + #expect(image?.imagePath == loadName) + } +} diff --git a/Tests/MachODependenciesTests/FileDependencyLocatorTests.swift b/Tests/MachODependenciesTests/FileDependencyLocatorTests.swift new file mode 100644 index 00000000..eb4e9030 --- /dev/null +++ b/Tests/MachODependenciesTests/FileDependencyLocatorTests.swift @@ -0,0 +1,75 @@ +import Foundation +import MachOKit +import MachOKitExtensions +import Testing +@testable import MachODependencies + +/// Pins the two-step lookup of `FileDependencyLocator` against the host's own +/// dyld shared cache: exact install path first, ranked bare name second. +/// +/// The ranking matters on macOS specifically. The macOS cache carries the Mac +/// Catalyst build of SwiftUI under `/System/iOSSupport` next to the native +/// framework, so a first-writer-wins bare-name index (the pre-unification +/// SwiftLayout locator) resolved whichever the cache enumerated first. +@Suite +struct FileDependencyLocatorTests { + private static let nativeSwiftUIPath = "/System/Library/Frameworks/SwiftUI.framework/Versions/A/SwiftUI" + private static let catalystSwiftUIPath = "/System/iOSSupport/System/Library/Frameworks/SwiftUI.framework/Versions/A/SwiftUI" + + private func hostLocator() -> FileDependencyLocator? { + guard FullDyldCache.host != nil else { + print("skipped: no host dyld shared cache") + return nil + } + return FileDependencyLocator(searchPaths: [.systemDyldSharedCache]) + } + + @Test func exactInstallPathIsAnsweredVerbatim() throws { + guard let locator = hostLocator() else { return } + let native = try #require(locator.locate(loadName: Self.nativeSwiftUIPath)) + #expect(native.imagePath == Self.nativeSwiftUIPath) + + // The Catalyst build shares SwiftUI's bare name; asked for by exact + // path it must come back as itself, never as the native winner. + if let catalyst = locator.locate(loadName: Self.catalystSwiftUIPath) { + #expect(catalyst.imagePath == Self.catalystSwiftUIPath) + } else { + print("note: this host cache carries no Catalyst SwiftUI; exact-path half of the check skipped") + } + } + + @Test func bareNameFallbackPrefersTheNativeCanonicalFramework() throws { + guard let locator = hostLocator() else { return } + let resolved = try #require(locator.locate(loadName: "@rpath/SwiftUI.framework/SwiftUI")) + #expect(resolved.imagePath == Self.nativeSwiftUIPath) + #expect(!resolved.imagePath.hasPrefix("/System/iOSSupport")) + } + + @Test func bareNameFallbackResolvesAbsolutePathsTheCacheDoesNotSpell() throws { + guard let locator = hostLocator() else { return } + // A plausible load name whose exact path is not in the cache (no + // `Versions/A`), so only the bare-name step can answer. + let resolved = try #require(locator.locate(loadName: "/System/Library/Frameworks/Foundation.framework/Foundation")) + #expect(DependencyLoadName.bareImageName(of: resolved.imagePath) == "Foundation") + } + + @Test func unknownNameResolvesToNil() { + guard let locator = hostLocator() else { return } + #expect(locator.locate(loadName: "@rpath/NoSuchLibrary.framework/NoSuchLibrary") == nil) + #expect(locator.locate(loadName: "") == nil) + } + + @Test func unopenableCachePathIsRecorded() { + let missingPath = "/nonexistent/dyld_shared_cache_arm64e" + let locator = FileDependencyLocator(searchPaths: [.dyldSharedCache(path: missingPath)]) + #expect(locator.loadFailures.count == 1) + #expect(locator.loadFailures.first?.searchPath == .dyldSharedCache(path: missingPath)) + #expect(locator.locate(loadName: "/usr/lib/swift/libswiftCore.dylib") == nil) + } + + @Test func searchPathDescriptionsAreStable() { + #expect(DependencySearchPath.machOFile(path: "/a").description == "machOFile(/a)") + #expect(DependencySearchPath.dyldSharedCache(path: "/b").description == "dyldSharedCache(/b)") + #expect(DependencySearchPath.systemDyldSharedCache.description == "systemDyldSharedCache") + } +} diff --git a/Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift b/Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift index 8cf58e2e..913ef555 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift @@ -297,7 +297,7 @@ enum CoverageAllowlistEntries { private static let needsFixtureExtensionEntries: [CoverageAllowlistEntry] = [ CoverageAllowlistHelpers.sentinelGroup( typeName: "MethodDefaultOverrideDescriptor", - members: ["originalMethodDescriptor", "replacementMethodDescriptor", "implementationSymbols", "layout", "offset"], + members: ["originalMethodDescriptor", "replacementMethodDescriptor", "implementationAddress", "implementationOffset", "layout", "offset"], reason: .needsFixtureExtension(detail: "MethodDefaultOverrideTable requires experimental CoroutineAccessors (read2/modify2) on a resilient open class; macOS Swift runtime does not yet export _swift_deletedCalleeAllocatedCoroutineMethodErrorTwc, so the fixture cannot be built. Defer until ABI stabilizes.") ), CoverageAllowlistHelpers.sentinelGroup( diff --git a/Tests/MachOSwiftSectionTests/Fixtures/Protocol/ProtocolRequirementTests.swift b/Tests/MachOSwiftSectionTests/Fixtures/Protocol/ProtocolRequirementTests.swift index d8598809..48e1665f 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/Protocol/ProtocolRequirementTests.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/Protocol/ProtocolRequirementTests.swift @@ -9,7 +9,11 @@ import MachOFixtureSupport /// /// Picker: `Protocols.ProtocolWitnessTableTest` — its 5 method /// requirements (`a`/`b`/`c`/`d`/`e`) flesh out the trailing array; we -/// pick the first requirement and exercise its accessors. +/// pick the first requirement and exercise its accessors. None of them has +/// a default implementation, so the default-implementation accessors are +/// ALSO exercised on `DefaultImplementationVariants.BasicDefaultProtocol`'s +/// first defaulted requirement — a `nil == nil` on the first picker alone +/// never ran the relative-pointer arithmetic. /// /// `ProtocolBaseRequirement` (the second struct in the same file) gets /// its own Suite (`ProtocolBaseRequirementTests`). @@ -30,6 +34,16 @@ final class ProtocolRequirementTests: MachOSwiftSectionFixtureTests, FixtureSuit return (file: file, image: image) } + private func loadFirstDefaultedRequirements() throws -> (file: ProtocolRequirement, image: ProtocolRequirement) { + let fileDescriptor = try BaselineFixturePicker.protocol_BasicDefaultProtocol(in: machOFile) + let imageDescriptor = try BaselineFixturePicker.protocol_BasicDefaultProtocol(in: machOImage) + let fileProtocol = try MachOSwiftSection.`Protocol`(descriptor: fileDescriptor, in: machOFile) + let imageProtocol = try MachOSwiftSection.`Protocol`(descriptor: imageDescriptor, in: machOImage) + let file = try required(fileProtocol.requirements.first { $0.layout.defaultImplementation.isValid }) + let image = try required(imageProtocol.requirements.first { $0.layout.defaultImplementation.isValid }) + return (file: file, image: image) + } + @Test func offset() async throws { let (file, image) = try loadFirstRequirements() let result = try acrossAllReaders( @@ -48,16 +62,46 @@ final class ProtocolRequirementTests: MachOSwiftSectionFixtureTests, FixtureSuit #expect(result == ProtocolRequirementBaseline.firstRequirement.layoutFlagsRawValue) } - @Test func defaultImplementationSymbols() async throws { + /// `defaultImplementationOffset` is pure relative-pointer arithmetic: + /// pinned as a literal — `nil` for a requirement without a default, a + /// real offset for a defaulted one — and identical across readers. + @Test func defaultImplementationOffset() async throws { let (file, image) = try loadFirstRequirements() let result = try acrossAllReaders( - file: { (try file.defaultImplementationSymbols(in: machOFile)) != nil }, - image: { (try image.defaultImplementationSymbols(in: machOImage)) != nil } + file: { file.defaultImplementationOffset }, + image: { image.defaultImplementationOffset } ) - #expect(result == ProtocolRequirementBaseline.firstRequirement.hasDefaultImplementation) + #expect(result == nil) + #expect(result == ProtocolRequirementBaseline.firstRequirement.defaultImplementationOffset) + + let (defaultedFile, defaultedImage) = try loadFirstDefaultedRequirements() + let defaultedResult = try acrossAllReaders( + file: { defaultedFile.defaultImplementationOffset }, + image: { defaultedImage.defaultImplementationOffset } + ) + #expect(defaultedResult != nil) + #expect(defaultedResult == ProtocolRequirementBaseline.firstDefaultedRequirement.defaultImplementationOffset) + // The literal is the resolved target, never the pointer field's own + // position: an implementation that returned `offset(of:)` instead of + // resolving through it would land exactly there. + #expect(defaultedResult != defaultedFile.offset(of: \.defaultImplementation)) + } + + /// The `ReadingContext` leg reports the same location as a context + /// address (a file offset for `MachOContext`), `nil` included. + @Test func defaultImplementationAddress() async throws { + let (file, image) = try loadFirstRequirements() + let fileAddress = try file.defaultImplementationAddress(in: fileContext) + let imageAddress = try image.defaultImplementationAddress(in: imageContext) + #expect(fileAddress == nil) + #expect(imageAddress == nil) - // ReadingContext overload also exercised. - let imageContextResult = (try image.defaultImplementationSymbols(in: imageContext)) != nil - #expect(imageContextResult == ProtocolRequirementBaseline.firstRequirement.hasDefaultImplementation) + let (defaultedFile, defaultedImage) = try loadFirstDefaultedRequirements() + let defaultedFileAddress = try defaultedFile.defaultImplementationAddress(in: fileContext) + let defaultedImageAddress = try defaultedImage.defaultImplementationAddress(in: imageContext) + #expect(defaultedFileAddress != nil) + #expect(defaultedFileAddress == defaultedFile.defaultImplementationOffset) + #expect(defaultedImageAddress == defaultedImage.defaultImplementationOffset) + #expect(defaultedImageAddress == ProtocolRequirementBaseline.firstDefaultedRequirement.defaultImplementationOffset) } } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/Protocol/ResilientWitnessTests.swift b/Tests/MachOSwiftSectionTests/Fixtures/Protocol/ResilientWitnessTests.swift index 84faf39f..0e6bc5f6 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/Protocol/ResilientWitnessTests.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/Protocol/ResilientWitnessTests.swift @@ -9,11 +9,11 @@ import MachOFixtureSupport /// /// Picker: the first `ProtocolConformance` from the fixture with a /// non-empty `resilientWitnesses` array. We pick its first witness and -/// exercise the `requirement(in:)` and `implementationSymbols(in:)` -/// resolution paths (each MachO + ReadingContext overload) plus the -/// `implementationOffset` derived var. `implementationAddress(in:)` is -/// a MachO-only debug formatter — we exercise its type-correctness by -/// calling it and checking it returns a non-empty hex string. +/// exercise the `requirement(in:)` resolution path (MachO + ReadingContext +/// overloads), the `implementationOffset` derived var (pinned as a +/// literal), and the two `implementationAddress(in:)` forms: the +/// ReadingContext address must equal the offset, and the MachO-only debug +/// formatter must produce a non-empty hex string. @Suite final class ResilientWitnessTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable { static let testedTypeName = "ResilientWitness" @@ -69,18 +69,6 @@ final class ResilientWitnessTests: MachOSwiftSectionFixtureTests, FixtureSuite, #expect(result == ResilientWitnessBaseline.firstWitness.implementationOffset) } - @Test func implementationSymbols() async throws { - let (file, image) = try loadFirstWitnesses() - // MachOFile + MachOImage exercise the two main code paths; the - // ReadingContext overloads are exercised by other Suites in - // this group (e.g. ResilientWitnessTests.requirement) via the - // imageContext. - let fileResult = (try file.implementationSymbols(in: machOFile)) != nil - let imageResult = (try image.implementationSymbols(in: machOImage)) != nil - #expect(fileResult == ResilientWitnessBaseline.firstWitness.hasImplementationSymbols) - #expect(imageResult == ResilientWitnessBaseline.firstWitness.hasImplementationSymbols) - } - /// `implementationAddress(in:)` is a MachO-only debug formatter — we /// don't pin the address string (it differs between MachOFile vs /// MachOImage by file vs in-memory base), but we verify it produces @@ -89,7 +77,14 @@ final class ResilientWitnessTests: MachOSwiftSectionFixtureTests, FixtureSuite, let (file, image) = try loadFirstWitnesses() let fileAddress = file.implementationAddress(in: machOFile) let imageAddress = image.implementationAddress(in: machOImage) - #expect(!fileAddress.isEmpty) - #expect(!imageAddress.isEmpty) + #expect(fileAddress?.isEmpty == false) + #expect(imageAddress?.isEmpty == false) + + // The ReadingContext form is the typed location, not a string. + let fileContextAddress = try file.implementationAddress(in: fileContext) + let imageContextAddress = try image.implementationAddress(in: imageContext) + #expect(fileContextAddress == file.implementationOffset) + #expect(imageContextAddress == image.implementationOffset) + #expect(imageContextAddress == ResilientWitnessBaseline.firstWitness.implementationOffset) } } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Method/MethodDefaultOverrideDescriptorTests.swift b/Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Method/MethodDefaultOverrideDescriptorTests.swift index fa16bccb..809b519e 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Method/MethodDefaultOverrideDescriptorTests.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Method/MethodDefaultOverrideDescriptorTests.swift @@ -27,6 +27,7 @@ final class MethodDefaultOverrideDescriptorTests: MachOSwiftSectionFixtureTests, // Invariant test. #expect(MethodDefaultOverrideDescriptorBaseline.registeredTestMethodNames.contains("originalMethodDescriptor")) #expect(MethodDefaultOverrideDescriptorBaseline.registeredTestMethodNames.contains("replacementMethodDescriptor")) - #expect(MethodDefaultOverrideDescriptorBaseline.registeredTestMethodNames.contains("implementationSymbols")) + #expect(MethodDefaultOverrideDescriptorBaseline.registeredTestMethodNames.contains("implementationOffset")) + #expect(MethodDefaultOverrideDescriptorBaseline.registeredTestMethodNames.contains("implementationAddress")) } } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Method/MethodDescriptorTests.swift b/Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Method/MethodDescriptorTests.swift index 1e72a225..3e00280a 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Method/MethodDescriptorTests.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Method/MethodDescriptorTests.swift @@ -1,6 +1,7 @@ import Foundation import Testing import MachOFoundation +import SwiftInspection @testable import MachOSwiftSection @testable import MachOTestingSupport import MachOFixtureSupport @@ -8,9 +9,10 @@ import MachOFixtureSupport /// Fixture-based Suite for `MethodDescriptor`. /// /// The Suite picks the first vtable entry from `Classes.ClassTest`, then -/// asserts cross-reader equality on the descriptor's offset and the -/// `flags.rawValue`. The `implementationSymbols` accessor is exercised -/// via cross-reader presence. +/// asserts cross-reader equality on the descriptor's offset, the +/// `flags.rawValue`, and the implementation offset the relative pointer +/// resolves to (pinned as a baseline literal; the ReadingContext leg must +/// report the same location as a context address). @Suite final class MethodDescriptorTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable { static let testedTypeName = "MethodDescriptor" @@ -48,20 +50,49 @@ final class MethodDescriptorTests: MachOSwiftSectionFixtureTests, FixtureSuite, #expect(flagsRaw == MethodDescriptorBaseline.firstClassTestMethod.layoutFlagsRawValue) } - /// `implementationSymbols(in:)` returns the resolved Symbols (or nil). - /// Exercise cross-reader presence; the underlying Symbols object is - /// not cheaply Equatable so we don't compare values directly. - @Test func implementationSymbols() async throws { + /// `implementationOffset` is pure relative-pointer arithmetic: pinned as + /// a literal and identical across readers. + @Test func implementationOffset() async throws { let methods = try loadFirstMethods() - let presence = try acrossAllReaders( - file: { (try methods.file.implementationSymbols(in: machOFile)) != nil }, - image: { (try methods.image.implementationSymbols(in: machOImage)) != nil } + let result = try acrossAllReaders( + file: { methods.file.implementationOffset }, + image: { methods.image.implementationOffset } ) - // The first vtable entry of ClassTest resolves to a real symbol. - #expect(presence == true) + #expect(result == MethodDescriptorBaseline.firstClassTestMethod.implementationOffset) + } + + /// The `ReadingContext` leg reports the same location as a context + /// address (a file offset for `MachOContext`). Its predecessor, + /// `implementationSymbols(in: context)`, had no symbol service to consult + /// and read the implementation's machine code as a `Symbols` value — + /// its `offset` came back as the first eight code bytes (evolution + /// proposal `self-contained-abi-layer`). This equality is what that + /// leg could never satisfy. + @Test func implementationAddress() async throws { + let methods = try loadFirstMethods() + let fileAddress = try methods.file.implementationAddress(in: fileContext) + let imageAddress = try methods.image.implementationAddress(in: imageContext) + #expect(fileAddress == methods.file.implementationOffset) + #expect(imageAddress == methods.image.implementationOffset) + #expect(imageAddress == MethodDescriptorBaseline.firstClassTestMethod.implementationOffset) + } + + /// Symbol attribution lives one layer up (SwiftInspection): it is the + /// symbol index's answer for the offset the ABI layer reports, nothing + /// more. The first vtable entry of ClassTest resolves to a real symbol. + @Test func implementationSymbolsAttributeTheReportedOffset() async throws { + let methods = try loadFirstMethods() + let fileOffset = try #require(methods.file.implementationOffset) + let expectedFileNames = machOFile.symbols(offset: fileOffset)?.map(\.name) + #expect(expectedFileNames?.isEmpty == false) + #expect(methods.file.implementationSymbols(in: machOFile)?.map(\.name) == expectedFileNames) - // ReadingContext-based overload. - let imageCtxPresence = (try methods.image.implementationSymbols(in: imageContext)) != nil - #expect(imageCtxPresence == true) + // The image leg on its own evidence: its own offset and a non-empty + // answer, so a `nil == nil` from an image index that returned + // nothing cannot pass silently. + let imageOffset = try #require(methods.image.implementationOffset) + let expectedImageNames = machOImage.symbols(offset: imageOffset)?.map(\.name) + #expect(expectedImageNames?.isEmpty == false) + #expect(methods.image.implementationSymbols(in: machOImage)?.map(\.name) == expectedImageNames) } } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Method/MethodOverrideDescriptorTests.swift b/Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Method/MethodOverrideDescriptorTests.swift index 2fe2537f..81795e6e 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Method/MethodOverrideDescriptorTests.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Method/MethodOverrideDescriptorTests.swift @@ -8,8 +8,9 @@ import MachOFixtureSupport /// Fixture-based Suite for `MethodOverrideDescriptor`. /// /// The Suite picks the first override entry from `Classes.SubclassTest`, -/// then asserts cross-reader equality on the descriptor's offset and -/// presence-flags for the resolved class/method/symbols pointers. +/// then asserts cross-reader equality on the descriptor's offset, the +/// implementation offset, and presence-flags for the resolved class/method +/// pointers. @Suite final class MethodOverrideDescriptorTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable { static let testedTypeName = "MethodOverrideDescriptor" @@ -76,18 +77,26 @@ final class MethodOverrideDescriptorTests: MachOSwiftSectionFixtureTests, Fixtur #expect(presence == true) } - /// `implementationSymbols(in:)` returns the resolved override - /// implementation Symbols. - @Test func implementationSymbols() async throws { + /// `implementationOffset` is pure relative-pointer arithmetic: pinned as + /// a literal (an override always has an implementation, so never `nil`) + /// and identical across readers. + @Test func implementationOffset() async throws { let overrides = try loadFirstOverrides() - let presence = try acrossAllReaders( - file: { (try overrides.file.implementationSymbols(in: machOFile)) != nil }, - image: { (try overrides.image.implementationSymbols(in: machOImage)) != nil } + let result = try acrossAllReaders( + file: { overrides.file.implementationOffset }, + image: { overrides.image.implementationOffset } ) - #expect(presence == true) + #expect(result != nil) + #expect(result == MethodOverrideDescriptorBaseline.firstSubclassOverride.implementationOffset) + } - // ReadingContext-based overload. - let imageCtxPresence = (try overrides.image.implementationSymbols(in: imageContext)) != nil - #expect(imageCtxPresence == true) + /// The `ReadingContext` leg reports the same location as a context + /// address (a file offset for `MachOContext`). + @Test func implementationAddress() async throws { + let overrides = try loadFirstOverrides() + let fileAddress = try overrides.file.implementationAddress(in: fileContext) + let imageAddress = try overrides.image.implementationAddress(in: imageContext) + #expect(fileAddress == overrides.file.implementationOffset) + #expect(imageAddress == overrides.image.implementationOffset) } } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodDefaultOverrideDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodDefaultOverrideDescriptorBaseline.swift index 26540a74..8d0ade9d 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodDefaultOverrideDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodDefaultOverrideDescriptorBaseline.swift @@ -9,5 +9,5 @@ // missing runtime coverage. enum MethodDefaultOverrideDescriptorBaseline { - static let registeredTestMethodNames: Set = ["implementationSymbols", "layout", "offset", "originalMethodDescriptor", "replacementMethodDescriptor"] + static let registeredTestMethodNames: Set = ["implementationAddress", "implementationOffset", "layout", "offset", "originalMethodDescriptor", "replacementMethodDescriptor"] } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodDescriptorBaseline.swift index 985e3f54..d807d247 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodDescriptorBaseline.swift @@ -2,22 +2,23 @@ // Regenerate via: Scripts/regen-baselines.sh // Source fixture: SymbolTestsCore.framework // -// Method descriptors carry a `Symbols?` implementation pointer; live -// payloads aren't embedded as literals. The companion Suite -// (MethodDescriptorTests) verifies cross-reader agreement at -// runtime. +// The implementation offset is pure relative-pointer arithmetic, so +// it is pinned as a literal; the companion Suite +// (MethodDescriptorTests) also verifies cross-reader agreement. enum MethodDescriptorBaseline { - static let registeredTestMethodNames: Set = ["implementationSymbols", "layout", "offset"] + static let registeredTestMethodNames: Set = ["implementationAddress", "implementationOffset", "layout", "offset"] struct Entry { let offset: Int let layoutFlagsRawValue: UInt32 + let implementationOffset: Int? } static let firstClassTestMethod = Entry( offset: 0x40960, - layoutFlagsRawValue: 0x12 + layoutFlagsRawValue: 0x12, + implementationOffset: 0x15f8 ) static let classTestMethodCount = 9 diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodOverrideDescriptorBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodOverrideDescriptorBaseline.swift index e51f5a88..bd4483a8 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodOverrideDescriptorBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodOverrideDescriptorBaseline.swift @@ -2,19 +2,22 @@ // Regenerate via: Scripts/regen-baselines.sh // Source fixture: SymbolTestsCore.framework // -// MethodOverrideDescriptor carries three relative pointers (class / -// method / implementation Symbols). Live payloads aren't embedded; -// the Suite verifies cross-reader agreement at runtime. +// The implementation offset is pure relative-pointer arithmetic, so +// it is pinned as a literal (like MethodDescriptor's); the class / +// method descriptor pointers resolve to live wrappers and are +// checked for presence across readers at runtime instead. enum MethodOverrideDescriptorBaseline { - static let registeredTestMethodNames: Set = ["classDescriptor", "implementationSymbols", "layout", "methodDescriptor", "offset"] + static let registeredTestMethodNames: Set = ["classDescriptor", "implementationAddress", "implementationOffset", "layout", "methodDescriptor", "offset"] struct Entry { let offset: Int + let implementationOffset: Int? } static let firstSubclassOverride = Entry( - offset: 0x409d8 + offset: 0x409d8, + implementationOffset: 0x4014 ) static let subclassOverrideCount = 9 diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolRequirementBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolRequirementBaseline.swift index 818d7b2d..d6a19700 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolRequirementBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolRequirementBaseline.swift @@ -3,17 +3,23 @@ // Source fixture: SymbolTestsCore.framework enum ProtocolRequirementBaseline { - static let registeredTestMethodNames: Set = ["defaultImplementationSymbols", "layout", "offset"] + static let registeredTestMethodNames: Set = ["defaultImplementationAddress", "defaultImplementationOffset", "layout", "offset"] struct Entry { let offset: Int let layoutFlagsRawValue: UInt32 - let hasDefaultImplementation: Bool + let defaultImplementationOffset: Int? } static let firstRequirement = Entry( offset: 0x4493c, layoutFlagsRawValue: 0x11, - hasDefaultImplementation: false + defaultImplementationOffset: nil + ) + + static let firstDefaultedRequirement = Entry( + offset: 0x41290, + layoutFlagsRawValue: 0x11, + defaultImplementationOffset: 0xb0c8 ) } diff --git a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientWitnessBaseline.swift b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientWitnessBaseline.swift index 22e916ae..b3d57d57 100644 --- a/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientWitnessBaseline.swift +++ b/Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientWitnessBaseline.swift @@ -3,19 +3,17 @@ // Source fixture: SymbolTestsCore.framework enum ResilientWitnessBaseline { - static let registeredTestMethodNames: Set = ["implementationAddress", "implementationOffset", "implementationSymbols", "layout", "offset", "requirement"] + static let registeredTestMethodNames: Set = ["implementationAddress", "implementationOffset", "layout", "offset", "requirement"] struct Entry { let offset: Int let hasRequirement: Bool - let hasImplementationSymbols: Bool - let implementationOffset: Int + let implementationOffset: Int? } static let firstWitness = Entry( offset: 0x384a4, hasRequirement: true, - hasImplementationSymbols: true, implementationOffset: 0x238c ) } diff --git a/Tests/MachOSwiftSectionTests/ManglingPrefixTests.swift b/Tests/MachOSwiftSectionTests/ManglingPrefixTests.swift new file mode 100644 index 00000000..aea64098 --- /dev/null +++ b/Tests/MachOSwiftSectionTests/ManglingPrefixTests.swift @@ -0,0 +1,26 @@ +import Testing +import Demangling +@testable import MachOSwiftSection + +/// The ABI layer keeps its own mangling-prefix check and its own copies of +/// the C-imported module names so that it does not depend on the demangler +/// for them (evolution proposal `self-contained-abi-layer`). This pins both +/// to the demangler's own values: a drift in the prefix list would +/// misclassify symbols, a drift in the module names would make every +/// `isCImportedContextDescriptor` answer `false` for the `__C` module. +@Suite +struct ManglingPrefixTests { + @Test func cImportedModuleNamesMatchTheDemangler() { + #expect(CImportedModuleNames.objectiveC == objcModule) + #expect(CImportedModuleNames.cSynthesized == cModule) + } + + @Test(arguments: [ + "_T0Si", "_$SSi", "_$sSi", "_$eSi", "$SSi", "$sSi", "$eSi", "@__swiftmacro_33main4fooV", + "Si", "_Si", "$", "_$", "", "T0Si", "swiftmacro_", "__C", "_$t", "$x", + ]) + func agreesWithTheDemangler(_ string: String) { + #expect(string.hasSwiftManglingPrefix == string.isSwiftSymbol) + #expect(string.strippingSwiftManglingPrefix == string.stripManglePrefix) + } +} diff --git a/Tests/MachOSymbolsTests/LargeStackTaskExecutionTests.swift b/Tests/MachOSymbolsTests/LargeStackTaskExecutionTests.swift new file mode 100644 index 00000000..0bfc5cd9 --- /dev/null +++ b/Tests/MachOSymbolsTests/LargeStackTaskExecutionTests.swift @@ -0,0 +1,139 @@ +import Foundation +import Testing +@_spi(Internals) import Demangling +@testable import MachOSymbols + +/// Pins `LargeStackTaskExecution.run` (evolution proposal +/// `large-stack-executor-and-cross-version-parallelism`): on a runtime with +/// task executors the body runs on one of the demangler's 16MB executor +/// threads and every demangler entry inside it runs inline (no hop to the +/// 8MB pool); nested runs do not switch threads; disabled or unsupported, the +/// body runs where the caller was. The upstream executor's own behavior +/// (thread size, QoS class, fallbacks) is pinned by swift-demangling's +/// `LargeStackTaskExecutorTests`; these tests cover the adoption seam only. +/// +/// Serialized: `disabledRunsTheBodyOnTheCallersExecutor` flips the +/// process-wide switch, and a parallel sibling asserting on the executor +/// thread would read it mid-flip. +@Suite(.serialized) +struct LargeStackTaskExecutionTests { + /// The upstream executor names its workers after itself + /// (`swift-demangling.task-executor.`); the prefix is the observable + /// identity of "an executor thread" from this side of the module boundary. + private static let executorThreadNamePrefix = "swift-demangling.task-executor." + + private static func currentThreadName() -> String { + var buffer = [CChar](repeating: 0, count: 128) + pthread_getname_np(pthread_self(), &buffer, buffer.count) + return String(cString: buffer) + } + + private static func currentThread() -> mach_port_t { + pthread_mach_thread_np(pthread_self()) + } + + /// `run` puts the body on the executor only when the runtime supports it + /// AND the process-wide switch is on — a run under + /// `MACHO_SWIFT_SECTION_LARGE_STACK_EXECUTOR=0` (the A/B and timing + /// configuration) takes the pass-through path, so the executor-thread + /// assertions below would be false for a reason the test does not + /// control. Guard both, not just support. + private static var executorIsActive: Bool { + LargeStackTaskExecution.isSupported && LargeStackTaskExecution.isEnabled + } + + @Test func bodyRunsOnAnExecutorThreadWhenSupported() async { + guard Self.executorIsActive else { return } + let (stackSize, threadName) = await LargeStackTaskExecution.run { + (pthread_get_stacksize_np(pthread_self()), Self.currentThreadName()) + } + #expect(stackSize >= 16 * 1024 * 1024, "stack was \(stackSize) bytes") + #expect(threadName.hasPrefix(Self.executorThreadNamePrefix), "ran on \(threadName)") + } + + /// The property the adoption exists for: inside the body, the demangler's + /// stack probe passes, so its blocking and suspending entries stay on the + /// task's thread instead of hopping to a pool worker. + @Test func demanglerEntriesInsideTheBodyDoNotHop() async { + guard Self.executorIsActive else { return } + let observation = await LargeStackTaskExecution.run { + let taskThread = Self.currentThread() + let blockingCallThread: mach_port_t = StackSafeExecutor.execute { Self.currentThread() } + let suspendingCallThread: mach_port_t = await StackSafeExecutor.executeAsync { Self.currentThread() } + return (taskThread, blockingCallThread, suspendingCallThread) + } + #expect(observation.1 == observation.0, "execute hopped off the executor thread") + #expect(observation.2 == observation.0, "executeAsync hopped off the executor thread") + } + + /// Every wrapped entry point reached from another wrapped entry point + /// (`printRoot` → `printTypeDefinition`, a parent's nested-children loop) + /// nests a run inside a run; the inner one must not move the task. + @Test func nestedRunsStayOnTheSameThread() async { + guard Self.executorIsActive else { return } + let (outerThread, innerThread) = await LargeStackTaskExecution.run { + let outer = Self.currentThread() + let inner = await LargeStackTaskExecution.run { Self.currentThread() } + return (outer, inner) + } + #expect(innerThread == outerThread) + } + + /// Child tasks inherit the preference (SE-0417) — the cross-version + /// parallel preparation relies on this. + @Test func childTasksInheritTheExecutor() async { + guard Self.executorIsActive else { return } + let childThreadNames = await LargeStackTaskExecution.run { + await withTaskGroup(of: String.self) { group in + for _ in 0 ..< 3 { + group.addTask { Self.currentThreadName() } + } + return await group.reduce(into: [String]()) { $0.append($1) } + } + } + #expect(childThreadNames.count == 3) + for threadName in childThreadNames { + #expect(threadName.hasPrefix(Self.executorThreadNamePrefix), "child ran on \(threadName)") + } + } + + @Test func disabledRunsTheBodyOnTheCallersExecutor() async { + let wasEnabled = LargeStackTaskExecution.isEnabled + LargeStackTaskExecution.isEnabled = false + defer { LargeStackTaskExecution.isEnabled = wasEnabled } + + let callerThread = Self.currentThread() + let (bodyThread, threadName) = await LargeStackTaskExecution.run { (Self.currentThread(), Self.currentThreadName()) } + // No switch at all: a non-suspending body on the caller's executor + // completes on the very thread that entered it. + #expect(bodyThread == callerThread) + #expect(!threadName.hasPrefix(Self.executorThreadNamePrefix), "ran on \(threadName)") + } + + /// The environment seed: the four off spellings, case-insensitive and + /// whitespace-tolerant, turn the executor off; anything else — including + /// an unset variable — leaves it on. The first version compared against + /// the literal `"0"`, so `=false` silently kept the executor on. + @Test(arguments: [ + ("0", false), ("false", false), ("FALSE", false), ("no", false), ("off", false), (" 0 ", false), ("Off\n", false), + ("1", true), ("true", true), ("yes", true), ("on", true), ("", true), ("2", true), ("disabled", true), + ]) + func environmentValueParsing(value: String, expected: Bool) { + #expect(LargeStackTaskExecution.isEnabled(fromEnvironmentValue: value) == expected) + } + + @Test func unsetEnvironmentValueLeavesTheExecutorOn() { + #expect(LargeStackTaskExecution.isEnabled(fromEnvironmentValue: nil)) + } + + @Test func valuesAndErrorsPassThrough() async throws { + struct Failure: Error, Equatable {} + + let value = await LargeStackTaskExecution.run { 42 } + #expect(value == 42) + + await #expect(throws: Failure.self) { + try await LargeStackTaskExecution.run { throw Failure() } + } + } +} diff --git a/Tests/MachOSymbolsTests/PackedNameReferenceBudgetTests.swift b/Tests/MachOSymbolsTests/PackedNameReferenceBudgetTests.swift index 1a599c32..bdf7d70a 100644 --- a/Tests/MachOSymbolsTests/PackedNameReferenceBudgetTests.swift +++ b/Tests/MachOSymbolsTests/PackedNameReferenceBudgetTests.swift @@ -1,4 +1,5 @@ import Foundation +import MachOResolving import Testing import Demangling @_spi(Internals) @testable import MachOSymbols diff --git a/Tests/MachOSymbolsTests/SymbolIndexStoreFixtureTests.swift b/Tests/MachOSymbolsTests/SymbolIndexStoreFixtureTests.swift index 99b7dc2d..f92ce523 100644 --- a/Tests/MachOSymbolsTests/SymbolIndexStoreFixtureTests.swift +++ b/Tests/MachOSymbolsTests/SymbolIndexStoreFixtureTests.swift @@ -1,4 +1,5 @@ import Foundation +import MachOResolving import Testing @_spi(Internals) import Demangling @_spi(Internals) @testable import MachOSymbols diff --git a/Tests/SwiftAttributeInferenceTests/TypeAttributeInferrerTests.swift b/Tests/SwiftAttributeInferenceTests/TypeAttributeInferrerTests.swift index 4ff77c66..f7830c89 100644 --- a/Tests/SwiftAttributeInferenceTests/TypeAttributeInferrerTests.swift +++ b/Tests/SwiftAttributeInferenceTests/TypeAttributeInferrerTests.swift @@ -1,4 +1,5 @@ @_spi(Support) @testable import SwiftDeclaration +import MachOFoundation @testable import SwiftAttributeInference import Testing import SwiftDump diff --git a/Tests/SwiftDiffingTests/ABIDifferTests.swift b/Tests/SwiftDiffingTests/ABIDifferTests.swift index 53e67acb..850041cb 100644 --- a/Tests/SwiftDiffingTests/ABIDifferTests.swift +++ b/Tests/SwiftDiffingTests/ABIDifferTests.swift @@ -1,4 +1,5 @@ @_spi(Support) @testable import SwiftDeclaration +import MachOFoundation @testable import SwiftDiffing import Testing import Foundation diff --git a/Tests/SwiftDiffingTests/ABIExtensionAttributionTests.swift b/Tests/SwiftDiffingTests/ABIExtensionAttributionTests.swift index be6b6ac2..cf39d6b2 100644 --- a/Tests/SwiftDiffingTests/ABIExtensionAttributionTests.swift +++ b/Tests/SwiftDiffingTests/ABIExtensionAttributionTests.swift @@ -1,4 +1,5 @@ @_spi(Support) @testable import SwiftDeclaration +import MachOFoundation @testable import SwiftDiffing import Testing import Foundation diff --git a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/actorsSnapshot.1.txt b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/actorsSnapshot.1.txt index d4efb2e5..6cee6657 100644 --- a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/actorsSnapshot.1.txt +++ b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/actorsSnapshot.1.txt @@ -11,7 +11,8 @@ actor SymbolTestsCore.Actors.ActorTest { /* [Method] */ func SymbolTestsCore.Actors.ActorTest.mutateState() -> () /* [Method] */ func SymbolTestsCore.Actors.ActorTest.readState() -> Swift.Int /* [Method] */ func SymbolTestsCore.Actors.ActorTest.nonisolatedMethod() -> Swift.String - /* [ Init ] */ Symbol not found + // No implementation in this image (deleted method — slot retained for ABI) + /* [ Init ] */ SymbolTestsCore.Actors.ActorTest.__allocating_init() -> SymbolTestsCore.Actors.ActorTest /* Deallocator */ SymbolTestsCore.Actors.ActorTest.__deallocating_deinit @@ -44,7 +45,8 @@ actor SymbolTestsCore.Actors.ActorTest { actor SymbolTestsCore.Actors.CustomGlobalActor { var $defaultActor: Builtin.DefaultActorStorage - /* [ Init ] */ Symbol not found + // No implementation in this image (deleted method — slot retained for ABI) + /* [ Init ] */ SymbolTestsCore.Actors.CustomGlobalActor.__allocating_init() -> SymbolTestsCore.Actors.CustomGlobalActor /* Deallocator */ SymbolTestsCore.Actors.CustomGlobalActor.__deallocating_deinit @@ -71,7 +73,8 @@ class SymbolTestsCore.Actors.GlobalActorAnnotatedClass { /* [Setter] */ SymbolTestsCore.Actors.GlobalActorAnnotatedClass.value.setter : Swift.Int /* [Modify] */ SymbolTestsCore.Actors.GlobalActorAnnotatedClass.value.modify : Swift.Int /* [Method] */ func SymbolTestsCore.Actors.GlobalActorAnnotatedClass.method() -> Swift.Int - /* [ Init ] */ Symbol not found + // No implementation in this image (deleted method — slot retained for ABI) + /* [ Init ] */ SymbolTestsCore.Actors.GlobalActorAnnotatedClass.__allocating_init() -> SymbolTestsCore.Actors.GlobalActorAnnotatedClass /* Deallocator */ SymbolTestsCore.Actors.GlobalActorAnnotatedClass.__deallocating_deinit @@ -104,7 +107,8 @@ class SymbolTestsCore.Actors.MainActorAnnotatedTest { /* [Modify] */ SymbolTestsCore.Actors.MainActorAnnotatedTest.value.modify : Swift.Int /* [Method] */ func SymbolTestsCore.Actors.MainActorAnnotatedTest.method() -> Swift.Int /* [Method] */ func SymbolTestsCore.Actors.MainActorAnnotatedTest.nonisolatedMethod() -> Swift.String - /* [ Init ] */ Symbol not found + // No implementation in this image (deleted method — slot retained for ABI) + /* [ Init ] */ SymbolTestsCore.Actors.MainActorAnnotatedTest.__allocating_init() -> SymbolTestsCore.Actors.MainActorAnnotatedTest /* Deallocator */ SymbolTestsCore.Actors.MainActorAnnotatedTest.__deallocating_deinit diff --git a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/basicTypesSnapshot.1.txt b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/basicTypesSnapshot.1.txt index 3149b945..74e1f92d 100644 --- a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/basicTypesSnapshot.1.txt +++ b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/basicTypesSnapshot.1.txt @@ -2,7 +2,8 @@ enum SymbolTestsCore.BasicTypes {} class SymbolTestsCore.BasicTypes.TestsObjects { - /* [ Init ] */ Symbol not found + // No implementation in this image (deleted method — slot retained for ABI) + /* [ Init ] */ SymbolTestsCore.BasicTypes.TestsObjects.__allocating_init() -> SymbolTestsCore.BasicTypes.TestsObjects /* Deallocator */ SymbolTestsCore.BasicTypes.TestsObjects.__deallocating_deinit diff --git a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/classesSnapshot.1.txt b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/classesSnapshot.1.txt index f5ff5429..89738996 100644 --- a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/classesSnapshot.1.txt +++ b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/classesSnapshot.1.txt @@ -43,7 +43,8 @@ class SymbolTestsCore.Classes.ClassTest { /* [Setter] */ SymbolTestsCore.Classes.ClassTest.dynamicVariable.setter : Swift.Bool /* [Modify] */ SymbolTestsCore.Classes.ClassTest.dynamicVariable.modify : Swift.Bool /* [Method] */ func SymbolTestsCore.Classes.ClassTest.dynamicMethod() -> () - /* [ Init ] */ Symbol not found + // No implementation in this image (deleted method — slot retained for ABI) + /* [ Init ] */ SymbolTestsCore.Classes.ClassTest.__allocating_init() -> SymbolTestsCore.Classes.ClassTest /* Deallocator */ SymbolTestsCore.Classes.ClassTest.__deallocating_deinit @@ -83,7 +84,7 @@ class SymbolTestsCore.Classes.SubclassTest: SymbolTestsCore.Classes.ClassTest { /* [Setter] */ override SymbolTestsCore.Classes.SubclassTest.dynamicVariable.setter : Swift.Bool /* [Modify] */ override SymbolTestsCore.Classes.SubclassTest.instanceVariable.modify : Swift.Bool /* [Method] */ override SymbolTestsCore.Classes.SubclassTest.dynamicVariable.modify : Swift.Bool - /* [ Init ] */ override Symbol not found + /* [ Init ] */ override /* Deallocator */ SymbolTestsCore.Classes.SubclassTest.__deallocating_deinit @@ -107,7 +108,7 @@ class SymbolTestsCore.Classes.FinalClassTest: SymbolTestsCore.Classes.SubclassTe /* [Setter] */ override SymbolTestsCore.Classes.FinalClassTest.dynamicVariable.modify : Swift.Bool /* [Modify] */ override SymbolTestsCore.Classes.FinalClassTest.dynamicVariable.modify : Swift.Bool /* [Method] */ override SymbolTestsCore.Classes.FinalClassTest.dynamicMethod() -> () - /* [ Init ] */ override Symbol not found + /* [ Init ] */ override /* Deallocator */ SymbolTestsCore.Classes.FinalClassTest.__deallocating_deinit diff --git a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/fieldDescriptorVariantsSnapshot.1.txt b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/fieldDescriptorVariantsSnapshot.1.txt index 12568c70..fdde0aed 100644 --- a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/fieldDescriptorVariantsSnapshot.1.txt +++ b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/fieldDescriptorVariantsSnapshot.1.txt @@ -40,7 +40,8 @@ class SymbolTestsCore.FieldDescriptorVariants.ReferenceFieldTest { /* [Getter] */ SymbolTestsCore.FieldDescriptorVariants.ReferenceFieldTest.strongVarField.getter : Swift.AnyObject /* [Setter] */ SymbolTestsCore.FieldDescriptorVariants.ReferenceFieldTest.strongVarField.setter : Swift.AnyObject /* [Modify] */ SymbolTestsCore.FieldDescriptorVariants.ReferenceFieldTest.strongVarField.modify : Swift.AnyObject - /* [ Init ] */ Symbol not found + // No implementation in this image (deleted method — slot retained for ABI) + /* [ Init ] */ SymbolTestsCore.FieldDescriptorVariants.ReferenceFieldTest.__allocating_init(reference: Swift.AnyObject) -> SymbolTestsCore.FieldDescriptorVariants.ReferenceFieldTest /* Deallocator */ SymbolTestsCore.FieldDescriptorVariants.ReferenceFieldTest.__deallocating_deinit diff --git a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/functionFeaturesSnapshot.1.txt b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/functionFeaturesSnapshot.1.txt index 7940fe37..ba9ec5ce 100644 --- a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/functionFeaturesSnapshot.1.txt +++ b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/functionFeaturesSnapshot.1.txt @@ -24,7 +24,8 @@ class SymbolTestsCore.FunctionFeatures.ClosureParameterTest { /* [Method] */ func SymbolTestsCore.FunctionFeatures.ClosureParameterTest.acceptEscaping(() -> ()) -> () /* [Method] */ func SymbolTestsCore.FunctionFeatures.ClosureParameterTest.acceptAutoclosure(@autoclosure () -> Swift.Bool) -> Swift.Bool /* [Method] */ func SymbolTestsCore.FunctionFeatures.ClosureParameterTest.acceptEscapingAutoclosure(@autoclosure () -> Swift.Bool) -> () - /* [ Init ] */ Symbol not found + // No implementation in this image (deleted method — slot retained for ABI) + /* [ Init ] */ SymbolTestsCore.FunctionFeatures.ClosureParameterTest.__allocating_init() -> SymbolTestsCore.FunctionFeatures.ClosureParameterTest /* Deallocator */ SymbolTestsCore.FunctionFeatures.ClosureParameterTest.__deallocating_deinit diff --git a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/initializersSnapshot.1.txt b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/initializersSnapshot.1.txt index 1ecfcc09..819af3ca 100644 --- a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/initializersSnapshot.1.txt +++ b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/initializersSnapshot.1.txt @@ -108,7 +108,7 @@ actor SymbolTestsCore.Initializers.AsyncInitializerActorTest { var $defaultActor: Builtin.DefaultActorStorage let identifier: Swift.Int - /* [ Init ] */ async function pointer to SymbolTestsCore.Initializers.AsyncInitializerActorTest.__allocating_init(identifier: Swift.Int) async -> SymbolTestsCore.Initializers.AsyncInitializerActorTest + /* [ Init ] */ SymbolTestsCore.Initializers.AsyncInitializerActorTest.__allocating_init(identifier: Swift.Int) async -> SymbolTestsCore.Initializers.AsyncInitializerActorTest /* Allocator */ SymbolTestsCore.Initializers.AsyncInitializerActorTest.__allocating_init(identifier: Swift.Int) async -> SymbolTestsCore.Initializers.AsyncInitializerActorTest diff --git a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/nestedFunctionsSnapshot.1.txt b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/nestedFunctionsSnapshot.1.txt index e8d2135a..c74c7275 100644 --- a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/nestedFunctionsSnapshot.1.txt +++ b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/nestedFunctionsSnapshot.1.txt @@ -8,8 +8,12 @@ struct SymbolTestsCore.NestedFunctions.NestedFunctionHolderTest { class SymbolTestsCore.NestedFunctions.NestedFunctionHolderTest.LocalClass { var label: Swift.String - /* [Getter] */ Symbol not found - /* [Setter] */ Symbol not found - /* [Modify] */ Symbol not found - /* [ Init ] */ Symbol not found + // No implementation in this image (deleted method — slot retained for ABI) + /* [Getter] */ + // No implementation in this image (deleted method — slot retained for ABI) + /* [Setter] */ + // No implementation in this image (deleted method — slot retained for ABI) + /* [Modify] */ + // No implementation in this image (deleted method — slot retained for ABI) + /* [ Init ] */ } \ No newline at end of file diff --git a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/subscriptsSnapshot.1.txt b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/subscriptsSnapshot.1.txt index ec30f02b..14af5edd 100644 --- a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/subscriptsSnapshot.1.txt +++ b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/subscriptsSnapshot.1.txt @@ -17,13 +17,17 @@ struct SymbolTestsCore.Subscripts.SubscriptStaticTest { class SymbolTestsCore.Subscripts.ClassSubscriptTest { var elements: [Swift.Int] - /* [Getter] */ Symbol not found - /* [Setter] */ Symbol not found - /* [Modify] */ Symbol not found + // No implementation in this image (deleted method — slot retained for ABI) + /* [Getter] */ + // No implementation in this image (deleted method — slot retained for ABI) + /* [Setter] */ + // No implementation in this image (deleted method — slot retained for ABI) + /* [Modify] */ /* [Getter] */ SymbolTestsCore.Subscripts.ClassSubscriptTest.subscript.getter : (Swift.Int) -> Swift.Int /* [Setter] */ SymbolTestsCore.Subscripts.ClassSubscriptTest.subscript.setter : (Swift.Int) -> Swift.Int /* [Modify] */ SymbolTestsCore.Subscripts.ClassSubscriptTest.subscript.modify : (Swift.Int) -> Swift.Int - /* [ Init ] */ Symbol not found + // No implementation in this image (deleted method — slot retained for ABI) + /* [ Init ] */ SymbolTestsCore.Subscripts.ClassSubscriptTest.__allocating_init() -> SymbolTestsCore.Subscripts.ClassSubscriptTest /* Deallocator */ SymbolTestsCore.Subscripts.ClassSubscriptTest.__deallocating_deinit diff --git a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/vTableEntryVariantsSnapshot.1.txt b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/vTableEntryVariantsSnapshot.1.txt index 095e5b54..e2a7679d 100644 --- a/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/vTableEntryVariantsSnapshot.1.txt +++ b/Tests/SwiftDumpTests/Snapshots/__Snapshots__/SymbolTestsCoreDumpSnapshotTests/vTableEntryVariantsSnapshot.1.txt @@ -4,13 +4,13 @@ enum SymbolTestsCore.VTableEntryVariants {} class SymbolTestsCore.VTableEntryVariants.VTableBaseTest { /* [Method] */ func SymbolTestsCore.VTableEntryVariants.VTableBaseTest.normalMethod() -> () /* [Method] */ func SymbolTestsCore.VTableEntryVariants.VTableBaseTest.overridableMethod() -> Swift.Int - /* [Method] */ func async function pointer to SymbolTestsCore.VTableEntryVariants.VTableBaseTest.asyncMethod() async -> Swift.Int + /* [Method] */ func SymbolTestsCore.VTableEntryVariants.VTableBaseTest.asyncMethod() async -> Swift.Int /* [Method] */ func SymbolTestsCore.VTableEntryVariants.VTableBaseTest.throwingMethod() throws -> Swift.Int - /* [Method] */ func async function pointer to SymbolTestsCore.VTableEntryVariants.VTableBaseTest.asyncThrowingMethod() async throws -> Swift.Int + /* [Method] */ func SymbolTestsCore.VTableEntryVariants.VTableBaseTest.asyncThrowingMethod() async throws -> Swift.Int /* [Getter] */ SymbolTestsCore.VTableEntryVariants.VTableBaseTest.normalProperty.getter : Swift.Int /* [Setter] */ SymbolTestsCore.VTableEntryVariants.VTableBaseTest.normalProperty.setter : Swift.Int /* [Modify] */ SymbolTestsCore.VTableEntryVariants.VTableBaseTest.normalProperty.modify : Swift.Int - /* [Getter] */ async function pointer to SymbolTestsCore.VTableEntryVariants.VTableBaseTest.asyncProperty.getter : Swift.Int + /* [Getter] */ SymbolTestsCore.VTableEntryVariants.VTableBaseTest.asyncProperty.getter : Swift.Int /* [Getter] */ SymbolTestsCore.VTableEntryVariants.VTableBaseTest.throwingProperty.getter : Swift.Int /* [ Init ] */ SymbolTestsCore.VTableEntryVariants.VTableBaseTest.__allocating_init() -> SymbolTestsCore.VTableEntryVariants.VTableBaseTest @@ -135,11 +135,11 @@ class SymbolTestsCore.VTableEntryVariants.FinalMembersTest { /* [Getter] */ SymbolTestsCore.VTableEntryVariants.FinalMembersTest.plainComputedProperty.getter : Swift.Int /* [Setter] */ SymbolTestsCore.VTableEntryVariants.FinalMembersTest.plainComputedProperty.setter : Swift.Int /* [Modify] */ SymbolTestsCore.VTableEntryVariants.FinalMembersTest.plainComputedProperty.modify : Swift.Int - /* [Method] */ func SymbolTestsCore.VTableEntryVariants.FinalMembersTest.plainComputedProperty.modify : Swift.Int + /* [Method] */ func SymbolTestsCore.VTableEntryVariants.FinalMembersTest.plainMethod() -> () /* [Getter] */ SymbolTestsCore.VTableEntryVariants.FinalMembersTest.subscript.getter : (plainIndex: Swift.Int) -> Swift.Int - /* [Setter] */ SymbolTestsCore.VTableEntryVariants.FinalMembersTest.plainMethod() -> () + /* [Setter] */ SymbolTestsCore.VTableEntryVariants.FinalMembersTest.subscript.setter : (plainIndex: Swift.Int) -> Swift.Int /* [Modify] */ SymbolTestsCore.VTableEntryVariants.FinalMembersTest.subscript.modify : (plainIndex: Swift.Int) -> Swift.Int - /* [Method] */ class func SymbolTestsCore.VTableEntryVariants.FinalMembersTest.subscript.setter : (plainIndex: Swift.Int) -> Swift.Int + /* [Method] */ class func static SymbolTestsCore.VTableEntryVariants.FinalMembersTest.classMethod() -> () /* [ Init ] */ SymbolTestsCore.VTableEntryVariants.FinalMembersTest.__allocating_init() -> SymbolTestsCore.VTableEntryVariants.FinalMembersTest /* Allocator */ diff --git a/Tests/SwiftDumpTests/VTableSlotAttributionTests.swift b/Tests/SwiftDumpTests/VTableSlotAttributionTests.swift new file mode 100644 index 00000000..ccc67674 --- /dev/null +++ b/Tests/SwiftDumpTests/VTableSlotAttributionTests.swift @@ -0,0 +1,348 @@ +import Foundation +import Testing +import MachOKit +import MachOFoundation +@testable import MachOSwiftSection +@testable import SwiftDump +import SwiftDeclarationRendering +@_spi(Internals) @testable import MachOSymbols +@testable import MachOTestingSupport + +/// Vtable slot attribution under identical code folding. +/// +/// Which member a slot belongs to is decided by the method descriptor's own +/// `Tq` symbol, not by the symbols at its implementation address: the linker +/// folds byte-identical function bodies onto one address, so an +/// implementation-address query answers with every folded member at once and +/// the mapping cannot be inverted. Before the fix the slots were handed +/// whichever matching symbol the table listed first, which reordered them +/// (and, when a NESTED type's member folded in too, took names from outside +/// the class entirely). +/// +/// The fixture compiles with `-Xlinker -deduplicate` to force the folding — +/// without it the four empty bodies stay at four addresses and the scenario +/// does not exist, which is why `foldedImplementationAddress` is a REQUIRED +/// premise of every test here rather than a soft check. +@Suite(.serialized) +struct VTableSlotAttributionTests { + private enum FixtureWorkingDirectoryCleanup { + nonisolated(unsafe) static var directories: [URL] = [] + static let registration: Void = { + atexit { + for directory in FixtureWorkingDirectoryCleanup.directories { + try? FileManager.default.removeItem(at: directory) + } + } + }() + } + + /// `alpha` / `beta` / `gamma` have byte-identical (empty) bodies and fold + /// onto one address. `Nested.nestedNoop` folds onto the SAME address and is + /// the cross-type trap: its demangled tree's first class node is `Host`, so + /// a `first(of: .class)` match accepts it as a member of `Host` — the shape + /// that put `GraphHost.Data`'s coroutine resume functions in `GraphHost`'s + /// vtable. `Nested` must be a class, not a struct: a struct method's body + /// differs in calling convention and does not fold. + /// + /// The class also satisfies the `__DATA`-segment requirement every + /// on-the-fly fixture in this repository carries (see + /// `DiffMemberIndentationTests`). + private static let fixtureSource = """ + open class Host { + open func alpha() {} + open func beta() {} + open func gamma() {} + public class Nested { + public func nestedNoop() {} + } + } + """ + + private static let fixtureCompilationResult: Result = { + Result { + let workingDirectory = FileManager.default.temporaryDirectory + .appendingPathComponent("VTableAttributionFixture-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: workingDirectory, withIntermediateDirectories: true) + _ = FixtureWorkingDirectoryCleanup.registration + FixtureWorkingDirectoryCleanup.directories.append(workingDirectory) + + let sourceURL = workingDirectory.appendingPathComponent("VTableAttributionFixture.swift") + let libraryURL = workingDirectory.appendingPathComponent("libVTableAttributionFixture.dylib") + try fixtureSource.write(to: sourceURL, atomically: true, encoding: .utf8) + + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun") + process.arguments = [ + "swiftc", "-emit-library", "-module-name", "VTableAttributionFixture", + sourceURL.path, "-o", libraryURL.path, + // ld64's identical-code-folding switch: without it the empty + // bodies keep four distinct addresses and nothing is ambiguous. + "-Xlinker", "-deduplicate", + ] + let standardErrorPipe = Pipe() + process.standardError = standardErrorPipe + try process.run() + // Drain BEFORE waitUntilExit — see LegacyDyldInfoBindTests. + let diagnosticsData = standardErrorPipe.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + guard process.terminationStatus == 0 else { + throw FixtureCompilationError(diagnostics: String(decoding: diagnosticsData, as: UTF8.self)) + } + return libraryURL + } + }() + + private struct FixtureCompilationError: Swift.Error, CustomStringConvertible { + let diagnostics: String + var description: String { "vtable-attribution fixture compilation failed:\n\(diagnostics)" } + } + + private func loadFixtureMachOFile() throws -> MachOFile { + let libraryURL = try Self.fixtureCompilationResult.get() + switch try MachOKit.loadFromFile(url: libraryURL) { + case .machO(let machOFile): + return machOFile + case .fat(let fatFile): + let machOFile = try fatFile.machOFiles().first { $0.header.cpuType == .arm64 } + return try #require(machOFile, "fixture unexpectedly missing an arm64 slice") + } + } + + private func classDescriptor(named name: String, in machOFile: MachOFile) throws -> ClassDescriptor { + for typeContextDescriptor in try machOFile.swift.typeContextDescriptors { + guard case .class(let classDescriptor) = typeContextDescriptor else { continue } + guard try classDescriptor.name(in: machOFile) == name else { continue } + return classDescriptor + } + Issue.record("fixture is missing the class \(name)") + throw FixtureCompilationError(diagnostics: "class \(name) not found") + } + + /// The vtable slot declarations of a dumped class, in slot order, as + /// `(slot, declaration)` pairs with the kind comment stripped. + private func vtableSlots(inDumpOf classDescriptor: ClassDescriptor, in machOFile: MachOFile) async throws -> [(slot: Int, declaration: String)] { + var configuration = DumperConfiguration.demangleOptions(.test) + configuration.printVTableOffset = true + let classType = try Class(descriptor: classDescriptor, in: machOFile) + let output = try await classType.dump(using: configuration, in: machOFile).string + + var slots: [(slot: Int, declaration: String)] = [] + let lines = output.split(separator: "\n", omittingEmptySubsequences: false) + for (index, line) in lines.enumerated() { + let trimmed = line.trimmingCharacters(in: .whitespaces) + guard trimmed.hasPrefix("// VTable offset: "), + let slot = Int(trimmed.dropFirst("// VTable offset: ".count)) else { continue } + for followingLine in lines[(index + 1)...] { + let candidate = followingLine.trimmingCharacters(in: .whitespaces) + if candidate.hasPrefix("//") || candidate.isEmpty { continue } + let declaration = candidate.replacingOccurrences( + of: "^/\\*[^*]*\\*/\\s*", + with: "", + options: .regularExpression + ) + slots.append((slot, declaration)) + break + } + } + return slots + } + + /// The single address `alpha` / `beta` / `gamma` / `nestedNoop` folded onto + /// — the premise the whole suite rests on. + private func foldedImplementationAddress(in machOFile: MachOFile) throws -> Int { + let hostDescriptor = try classDescriptor(named: "Host", in: machOFile) + let hostClass = try Class(descriptor: hostDescriptor, in: machOFile) + let methodImplementationOffsets = hostClass.methodDescriptors + .filter { $0.flags.kind == .method } + .compactMap(\.implementationOffset) + let distinctOffsets = Set(methodImplementationOffsets) + #expect(methodImplementationOffsets.count == 3, "fixture should contribute three plain vtable methods") + return try #require( + distinctOffsets.count == 1 ? distinctOffsets.first : nil, + """ + the fixture's empty method bodies did NOT fold onto one address \ + (found \(distinctOffsets.count) distinct: \(distinctOffsets.sorted().map { String($0, radix: 16) })). \ + This suite cannot test attribution ambiguity without the folding — \ + check that the toolchain's linker still honours `-deduplicate`. + """ + ) + } + + /// Every folded slot keeps ITS OWN member, in declaration order. + /// + /// The pre-fix output for exactly this fixture was `beta` / `alpha` / + /// `gamma` — the symbol table's order at the folded address, not the + /// vtable's. + @Test func foldedSlotsKeepTheirOwnMembers() async throws { + let machOFile = try loadFixtureMachOFile() + _ = try foldedImplementationAddress(in: machOFile) + + let hostDescriptor = try classDescriptor(named: "Host", in: machOFile) + let slots = try await vtableSlots(inDumpOf: hostDescriptor, in: machOFile) + let methodSlots = slots.filter { $0.declaration.contains("func ") } + + #expect(methodSlots.map(\.slot) == methodSlots.map(\.slot).sorted(), "slots must be emitted in slot order") + #expect( + methodSlots.map(\.declaration) == [ + "func VTableAttributionFixture.Host.alpha() -> ()", + "func VTableAttributionFixture.Host.beta() -> ()", + "func VTableAttributionFixture.Host.gamma() -> ()", + ], + "folded slots must be attributed by their own `Tq` symbols, in declaration order; got \(methodSlots)" + ) + } + + /// A nested type's member never surfaces as a slot of the enclosing class, + /// even though it folded onto the same address and its demangled tree's + /// first class node IS the enclosing class. + /// + /// DEFENSIVE, not a reproduction: this passes on the pre-fix code too. + /// Whether a nested member is actually mis-attributed depends on where the + /// linker happens to place it in the symbol table relative to the enclosing + /// class's own members — here `Host`'s three members are listed first and + /// exhaust the slots before `nestedNoop` is reached. Attempts to force the + /// unfavourable order in a fixture this size did not reproduce it. The + /// live reproduction is `GraphHostVTableAttributionTests`, where three + /// slots really did print `GraphHost.Data`'s resume functions. + @Test func nestedTypeMembersNeverOccupyTheEnclosingClassVTable() async throws { + let machOFile = try loadFixtureMachOFile() + _ = try foldedImplementationAddress(in: machOFile) + + let hostDescriptor = try classDescriptor(named: "Host", in: machOFile) + let slots = try await vtableSlots(inDumpOf: hostDescriptor, in: machOFile) + + #expect( + !slots.contains { $0.declaration.contains("nestedNoop") }, + "a nested type's folded member must not be attributed to the enclosing class; got \(slots)" + ) + } + + /// The nested class's own vtable is attributed to the nested class — the + /// counterpart to the test above, so "drop everything ambiguous" cannot + /// pass both. Defensive in the same sense: it also passes pre-fix. + @Test func nestedClassKeepsItsOwnVTableMember() async throws { + let machOFile = try loadFixtureMachOFile() + _ = try foldedImplementationAddress(in: machOFile) + + let nestedDescriptor = try classDescriptor(named: "Nested", in: machOFile) + let slots = try await vtableSlots(inDumpOf: nestedDescriptor, in: machOFile) + + #expect( + slots.contains { $0.declaration == "func VTableAttributionFixture.Host.Nested.nestedNoop() -> ()" }, + "the nested class's own folded member must still be attributed to it; got \(slots)" + ) + } +} + +/// The reported case, pinned end to end against the real binary: `dump`ing +/// `SwiftUI.GraphHost` from an iOS 18.5 simulator runtime's SwiftUICore. +/// +/// Its four empty vtable methods fold onto `0x9330`, an address carrying 2878 +/// symbols, and three of the four slots used to print coroutine resume +/// functions of the nested `GraphHost.Data` struct. Slots 20–22 are a second +/// fact worth pinning: their implementation pointers are null (the class +/// metadata binds them to `swift_deletedMethodError`), so they are ABI +/// tombstones rather than members. +/// +/// Environment-gated: skipped wherever no simulator runtime ships SwiftUICore. +/// Where the gated suite below finds its binary. +/// +/// Deliberately a separate type: a `@Suite(.enabled(if:))` condition that reads +/// a static of the suite it decorates is a circular macro reference and does +/// not compile. +enum SimulatorRuntimeSwiftUICore { + /// The first SwiftUICore an installed simulator runtime provides. The + /// runtime volumes are versioned per install (`iOS_22F77`, …), so the path + /// is discovered rather than written down. + static let url: URL? = { + let fileManager = FileManager.default + let runtimeSearchRoots = [ + "/Library/Developer/CoreSimulator/Volumes", + "/Library/Developer/CoreSimulator/Profiles/Runtimes", + ] + let frameworkSuffix = "Contents/Resources/RuntimeRoot/System/Library/Frameworks/SwiftUICore.framework/SwiftUICore" + for searchRoot in runtimeSearchRoots { + guard let enumerator = try? fileManager.contentsOfDirectory(atPath: searchRoot) else { continue } + for entry in enumerator.sorted() { + let entryURL = URL(fileURLWithPath: searchRoot).appendingPathComponent(entry) + // A volume nests one more level: /Library/Developer/CoreSimulator/Profiles/Runtimes/. + let candidateRoots = [ + entryURL, + entryURL.appendingPathComponent("Library/Developer/CoreSimulator/Profiles/Runtimes"), + ] + for candidateRoot in candidateRoots { + if fileManager.fileExists(atPath: candidateRoot.appendingPathComponent(frameworkSuffix).path) { + return candidateRoot.appendingPathComponent(frameworkSuffix) + } + guard let runtimes = try? fileManager.contentsOfDirectory(atPath: candidateRoot.path) else { continue } + for runtime in runtimes.sorted() { + let candidate = candidateRoot.appendingPathComponent(runtime).appendingPathComponent(frameworkSuffix) + if fileManager.fileExists(atPath: candidate.path) { return candidate } + } + } + } + } + return nil + }() +} + +@Suite(.serialized, .enabled(if: SimulatorRuntimeSwiftUICore.url != nil)) +struct GraphHostVTableAttributionTests { + private func graphHostDump() async throws -> String { + let url = try #require(SimulatorRuntimeSwiftUICore.url) + let machOFile: MachOFile + switch try MachOKit.loadFromFile(url: url) { + case .machO(let file): + machOFile = file + case .fat(let fatFile): + machOFile = try #require(try fatFile.machOFiles().first { $0.header.cpuType == .arm64 }) + } + + for typeContextDescriptor in try machOFile.swift.typeContextDescriptors { + guard case .class(let classDescriptor) = typeContextDescriptor, + try classDescriptor.name(in: machOFile) == "GraphHost" else { continue } + var configuration = DumperConfiguration.demangleOptions(.test) + configuration.printVTableOffset = true + let classType = try Class(descriptor: classDescriptor, in: machOFile) + return try await classType.dump(using: configuration, in: machOFile).string + } + Issue.record("SwiftUICore does not contain a GraphHost class") + return "" + } + + /// The four folded slots, each with the member its `Tq` symbol names. + /// Pre-fix, slot 26 printed `isHiddenForReuseDidChange` (slot 29's member) + /// and 27–29 printed `GraphHost.Data`'s `.resume.0` functions. + @Test func foldedSlotsMatchTheirMethodDescriptorSymbols() async throws { + let output = try await graphHostDump() + + for (slot, member) in [ + (26, "instantiateOutputs"), + (27, "uninstantiateOutputs"), + (28, "timeDidChange"), + (29, "isHiddenForReuseDidChange"), + ] { + #expect( + output.contains("// VTable offset: \(slot)\n /* [Method] */ func SwiftUI.GraphHost.\(member)() -> ()"), + "vtable slot \(slot) must be attributed to \(member)" + ) + } + + #expect(!output.contains("resume"), "no coroutine resume function belongs in GraphHost's vtable") + } + + /// Slots 20–22 carry no implementation: deleted members whose slots stay + /// for ABI stability. They must say so rather than read as ordinary + /// members or as a bare lookup failure. + @Test func deletedMethodSlotsAreMarkedAsTombstones() async throws { + let output = try await graphHostDump() + + for slot in 20...22 { + #expect( + output.contains("// VTable offset: \(slot)\n // No implementation in this image (deleted method — slot retained for ABI)"), + "vtable slot \(slot) must be marked as an ABI tombstone" + ) + } + #expect(!output.contains("Symbol not found"), "the tombstone slots must not degrade to a bare lookup failure") + } +} diff --git a/Tests/SwiftIndexingTests/ConsoleEventHandlerLineTests.swift b/Tests/SwiftIndexingTests/ConsoleEventHandlerLineTests.swift new file mode 100644 index 00000000..a09b8e91 --- /dev/null +++ b/Tests/SwiftIndexingTests/ConsoleEventHandlerLineTests.swift @@ -0,0 +1,40 @@ +import Foundation +import Testing +import SwiftDeclaration +@testable import SwiftIndexing + +/// Pins `ConsoleEventHandler`'s line format, in particular the input label +/// (evolution proposal `large-stack-executor-and-cross-version-parallelism`): +/// `diff` / `evolution` index their inputs concurrently, so every stderr line +/// carries `[label]` after the timestamp when a label is set, and nothing +/// extra when it is not. +@Suite +struct ConsoleEventHandlerLineTests { + private let completedExtraction = SwiftIndexEvents.Payload.extractionCompleted( + result: SwiftIndexEvents.ExtractionResult(section: .swiftTypes, count: 42) + ) + + @Test func labeledLinesCarryTheLabelAfterTheTimestamp() { + let handler = ConsoleEventHandler(label: "26.0") + #expect(handler.line(for: completedExtraction, timestamp: "12:00:00") == "[12:00:00] [26.0] [INFO] Extracted 42 Swift types") + } + + @Test func unlabeledLinesAreUnchanged() { + let handler = ConsoleEventHandler() + #expect(handler.label == nil) + #expect(handler.line(for: completedExtraction, timestamp: "12:00:00") == "[12:00:00] [INFO] Extracted 42 Swift types") + } + + @Test func unreportedEventsProduceNoLine() { + let handler = ConsoleEventHandler(label: "old") + #expect(handler.line(for: .moduleCollectionStarted, timestamp: "12:00:00") == nil) + #expect(handler.line(for: .phaseTransition(phase: .indexing, state: .started), timestamp: "12:00:00") == nil) + } + + @Test func failuresKeepTheLabelToo() { + struct Failure: Error, CustomStringConvertible { var description: String { "boom" } } + let handler = ConsoleEventHandler(label: "new") + let line = handler.line(for: .phaseTransition(phase: .indexing, state: .failed(Failure())), timestamp: "12:00:00") + #expect(line == "[12:00:00] [new] [ERROR] Indexing failed: boom") + } +} diff --git a/Tests/SwiftIndexingTests/OverrideRecoveryPredicateTests.swift b/Tests/SwiftIndexingTests/OverrideRecoveryPredicateTests.swift index 89257874..5c37ded4 100644 --- a/Tests/SwiftIndexingTests/OverrideRecoveryPredicateTests.swift +++ b/Tests/SwiftIndexingTests/OverrideRecoveryPredicateTests.swift @@ -1,4 +1,5 @@ import Foundation +import MachOResolving import Testing import Demangling import MachOSymbols diff --git a/Tests/SwiftInspectionTests/RuntimeMetadataTypeBuilderTests.swift b/Tests/SwiftInspectionTests/RuntimeMetadataTypeBuilderTests.swift new file mode 100644 index 00000000..6fdac323 --- /dev/null +++ b/Tests/SwiftInspectionTests/RuntimeMetadataTypeBuilderTests.swift @@ -0,0 +1,197 @@ +import Foundation +import Testing +import Demangling +@testable import MachOSwiftSection +@testable import SwiftInspection + +// MARK: - Fixtures (internal on purpose: plain identifiers keep the test +// resolver's name matching trivial) + +struct HashableConstrainedBox { + var element: Element +} + +struct UnconstrainedBox { + var element: Element +} + +struct ElementEquatableBox where Elements.Element: Equatable { + var elements: Elements +} + +final class OuterGenericFixture { + struct Inner { + var value: Int + } + + struct InnerPair { + var first: First? + var second: Second? + } +} + +// MARK: - Tests + +@Suite +struct RuntimeMetadataTypeBuilderTests { + /// The oracle loop: a live type's own mangled name, decoded through the + /// builder, must come back as the identical runtime metadata (metadata is + /// process-unique, so `==` on `Any.Type` is semantic equality). + private func expectRoundTrip( + _ expectedType: Any.Type, + builder: RuntimeMetadataTypeBuilder = RuntimeMetadataTypeBuilder(), + sourceLocation: SourceLocation = #_sourceLocation + ) throws { + let mangledTypeName = try #require(_mangledTypeName(expectedType), sourceLocation: sourceLocation) + let typeNode = try demangleAsNode(mangledTypeName, isType: true) + let rebuiltType = try builder.metadataType(for: typeNode) + #expect( + ObjectIdentifier(rebuiltType) == ObjectIdentifier(expectedType), + "mangling \(mangledTypeName): rebuilt \(rebuiltType), expected \(expectedType)", + sourceLocation: sourceLocation + ) + } + + // MARK: Concrete structural types (no descriptor resolver needed) + + @Test(arguments: [ + [Int].self, + ContiguousArray.self, + [String: Int].self, + Set.self, + Int?.self, + String??.self, + Range.self, + ClosedRange.self, + Result.self, + UnsafePointer.self, + UnsafeMutableBufferPointer.self, + ] as [Any.Type]) + func standardLibraryGenericRoundTrips(expectedType: Any.Type) throws { + try expectRoundTrip(expectedType) + } + + @Test(arguments: [ + Int.self, + String.self, + Double.self, + StaticString.self, + (Int, String).self, + (first: Int, second: String).self, + ((Int) -> String).self, + ((Int, String) throws -> Void).self, + (@Sendable (Int) async -> String).self, + Int.Type.self, + [Int].Type.self, + Any.self, + AnyObject.self, + Any.Type.self, + (any Error).self, + (any CustomStringConvertible).self, + (any CustomStringConvertible & AnyObject).self, + ] as [Any.Type]) + func concreteTypeRoundTrips(expectedType: Any.Type) throws { + try expectRoundTrip(expectedType) + } + + @Test func objectiveCClassRoundTrips() throws { + try expectRoundTrip(NSObject.self) + } + + @Test func bridgedObjectiveCClassRoundTrips() throws { + try expectRoundTrip(NSString.self) + } + + @Test func objectiveCProtocolExistentialRoundTrips() throws { + try expectRoundTrip((any NSCopying).self) + } + + // MARK: Generic fixtures through the descriptor-resolver seam + + /// Resolves the fixture declarations above by their plain identifier, + /// handing back the descriptor read from a known instantiation's + /// metadata — the seam a host with a real index would fill. + private static let fixtureDescriptorResolver: @Sendable (Node) -> UnsafeRawPointer? = { declarationNode in + let knownInstantiations: [String: Any.Type] = [ + "HashableConstrainedBox": HashableConstrainedBox.self, + "UnconstrainedBox": UnconstrainedBox.self, + "ElementEquatableBox": ElementEquatableBox<[Int]>.self, + "OuterGenericFixture": OuterGenericFixture.self, + "Inner": OuterGenericFixture.Inner.self, + "InnerPair": OuterGenericFixture.InnerPair.self, + ] + var declarationName: String? + for child in declarationNode.children.reversed() where child.kind == .identifier { + declarationName = child.text + break + } + guard let declarationName, let instantiation = knownInstantiations[declarationName] else { return nil } + guard let wrapper = try? Metadata.createInProcess(instantiation).typeContextDescriptorWrapper() else { return nil } + return try? wrapper.typeContextDescriptor.asPointer + } + + private var fixtureBuilder: RuntimeMetadataTypeBuilder { + RuntimeMetadataTypeBuilder(nominalTypeDescriptorResolver: Self.fixtureDescriptorResolver) + } + + @Test func unconstrainedGenericInstantiates() throws { + try expectRoundTrip(UnconstrainedBox.self, builder: fixtureBuilder) + } + + @Test func conformanceConstrainedGenericResolvesItsWitnessTable() throws { + try expectRoundTrip(HashableConstrainedBox.self, builder: fixtureBuilder) + } + + @Test func dependentMemberRequirementSubjectResolvesThroughAssociatedTypeWitness() throws { + try expectRoundTrip(ElementEquatableBox<[Int]>.self, builder: fixtureBuilder) + } + + @Test func nestedTypeInheritsParentGenericArguments() throws { + try expectRoundTrip(OuterGenericFixture.Inner.self, builder: fixtureBuilder) + } + + @Test func nestedGenericCombinesParentAndOwnArguments() throws { + try expectRoundTrip(OuterGenericFixture.InnerPair.self, builder: fixtureBuilder) + } + + @Test func genericArgumentsComposeStructurally() throws { + try expectRoundTrip(HashableConstrainedBox<[Int?]>.self, builder: fixtureBuilder) + } + + // MARK: Generic parameter bindings + + @Test func boundGenericParameterSubstitutes() throws { + let parameterNode = try demangleAsNode("x", isType: true) + let builder = RuntimeMetadataTypeBuilder( + genericParameterMetadataTypes: [.init(depth: 0, index: 0): Int.self] + ) + #expect(try ObjectIdentifier(builder.metadataType(for: parameterNode)) == ObjectIdentifier(Int.self)) + } + + @Test func boundGenericParameterComposesIntoStructuralTypes() throws { + let arrayOfParameterNode = try demangleAsNode("SayxG", isType: true) + let builder = RuntimeMetadataTypeBuilder( + genericParameterMetadataTypes: [.init(depth: 0, index: 0): String.self] + ) + #expect(try ObjectIdentifier(builder.metadataType(for: arrayOfParameterNode)) == ObjectIdentifier([String].self)) + } + + // MARK: Honest rejections + + @Test func unboundGenericParameterFailsWithTypedError() throws { + let parameterNode = try demangleAsNode("x", isType: true) + let builder = RuntimeMetadataTypeBuilder() + #expect(throws: TypeLookupError.self) { + try builder.metadataType(for: parameterNode) + } + } + + @Test func namedGenericWithoutResolverFailsWithTypedError() throws { + let mangledTypeName = try #require(_mangledTypeName(UnconstrainedBox.self)) + let typeNode = try demangleAsNode(mangledTypeName, isType: true) + let builder = RuntimeMetadataTypeBuilder() + #expect(throws: TypeLookupError.self) { + try builder.metadataType(for: typeNode) + } + } +} diff --git a/Tests/SwiftInterfaceTests/BoundedConcurrentMapTests.swift b/Tests/SwiftInterfaceTests/BoundedConcurrentMapTests.swift new file mode 100644 index 00000000..4f639e97 --- /dev/null +++ b/Tests/SwiftInterfaceTests/BoundedConcurrentMapTests.swift @@ -0,0 +1,203 @@ +import Foundation +import Testing +import MachOFoundation + +/// Pins `concurrentMap(maximumConcurrency:_:)` (evolution proposal +/// `large-stack-executor-and-cross-version-parallelism`), the windowed +/// scheduler behind cross-version preparation: source-ordered results, a +/// window that is never exceeded, a window of 1 that is strictly serial, +/// genuine concurrency inside the window, and first-failure semantics that +/// never start the elements still pending. +/// +/// No wall-clock assertions: the ordering and window facts are recorded +/// through locks and rendezvous, so a loaded machine cannot fail them. +@Suite +struct BoundedConcurrentMapTests { + /// Records start/end events and the in-flight high-water mark. + private final class Ledger: @unchecked Sendable { + private let lock = NSLock() + private var inFlight = 0 + private(set) var maximumInFlight = 0 + private(set) var events: [String] = [] + + func start(_ element: Int) { + lock.withLock { + inFlight += 1 + maximumInFlight = max(maximumInFlight, inFlight) + events.append("start \(element)") + } + } + + func end(_ element: Int) { + lock.withLock { + inFlight -= 1 + events.append("end \(element)") + } + } + } + + /// A rendezvous for a fixed number of participants: `arrive()` suspends + /// until every participant has arrived, so it completes only if that many + /// transforms are in flight at the same time. + private actor Barrier { + private let participantCount: Int + private var arrivedCount = 0 + private var waiters: [CheckedContinuation] = [] + + init(participantCount: Int) { + self.participantCount = participantCount + } + + func arrive() async { + arrivedCount += 1 + if arrivedCount >= participantCount { + for waiter in waiters { waiter.resume() } + waiters.removeAll() + return + } + await withCheckedContinuation { waiters.append($0) } + } + } + + /// A one-shot gate: `wait()` suspends until `open()` — a rendezvous that + /// completes only if the waiter and the opener run concurrently. + private actor Gate { + private var isOpen = false + private var waiters: [CheckedContinuation] = [] + + func wait() async { + if isOpen { return } + await withCheckedContinuation { waiters.append($0) } + } + + func open() { + isOpen = true + for waiter in waiters { waiter.resume() } + waiters.removeAll() + } + } + + @Test func resultsKeepSourceOrderWhateverTheCompletionOrder() async throws { + let elements = Array(0 ..< 12) + let results = try await elements.concurrentMap(maximumConcurrency: 4) { element in + // Later elements finish first when they can. + try await Task.sleep(nanoseconds: UInt64(12 - element) * 1_000_000) + return element * 10 + } + #expect(results == elements.map { $0 * 10 }) + } + + @Test func theWindowIsNeverExceeded() async throws { + let ledger = Ledger() + _ = try await Array(0 ..< 10).concurrentMap(maximumConcurrency: 3) { element in + ledger.start(element) + try await Task.sleep(nanoseconds: 5_000_000) + ledger.end(element) + } + #expect(ledger.maximumInFlight <= 3) + #expect(ledger.events.count == 20) + } + + @Test func aWindowOfOneIsStrictlySerialInSourceOrder() async throws { + let ledger = Ledger() + _ = try await Array(0 ..< 5).concurrentMap(maximumConcurrency: 1) { element in + ledger.start(element) + await Task.yield() + ledger.end(element) + } + let expected = (0 ..< 5).flatMap { ["start \($0)", "end \($0)"] } + #expect(ledger.events == expected) + #expect(ledger.maximumInFlight == 1) + } + + @Test func valuesBelowOneCountAsOne() async throws { + let ledger = Ledger() + _ = try await Array(0 ..< 3).concurrentMap(maximumConcurrency: 0) { element in + ledger.start(element) + await Task.yield() + ledger.end(element) + } + #expect(ledger.maximumInFlight == 1) + } + + /// Two elements in a window of two must run at the same time: the first + /// waits on a gate only the second opens. A serial scheduler would never + /// reach the opener — hence the time limit, which is the failure mode. + @Test(.timeLimit(.minutes(1))) func elementsInsideTheWindowRunConcurrently() async throws { + let gate = Gate() + let results = try await [0, 1].concurrentMap(maximumConcurrency: 2) { element in + if element == 0 { + await gate.wait() + } else { + await gate.open() + } + return element + } + #expect(results == [0, 1]) + } + + /// The window admits exactly its width, not fewer: three elements in a + /// window of three must all be in flight at once (a three-way barrier that + /// only releases when all three have arrived). A window that admitted two + /// would leave the third pending forever — hence the time limit. + @Test(.timeLimit(.minutes(1))) func theWindowAdmitsItsFullWidth() async throws { + let barrier = Barrier(participantCount: 3) + let results = try await [0, 1, 2].concurrentMap(maximumConcurrency: 3) { element in + await barrier.arrive() + return element + } + #expect(results == [0, 1, 2]) + } + + /// Cancelling the calling task stops the submission of pending elements + /// and fails the call with `CancellationError` — never a partial array. + /// Element 0 holds the (width-1) window open until the test has cancelled + /// the task; element 1 must then never start. The first version used + /// `addTask`, which a cancelled group still accepts, so every remaining + /// version of a cancelled multi-version preparation indexed to the end. + @Test(.timeLimit(.minutes(1))) func cancellationStopsSubmittingPendingElements() async { + let ledger = Ledger() + let elementZeroStarted = Gate() + let elementZeroMayFinish = Gate() + + let task = Task { + try await Array(0 ..< 4).concurrentMap(maximumConcurrency: 1) { element in + ledger.start(element) + if element == 0 { + await elementZeroStarted.open() + await elementZeroMayFinish.wait() + } + ledger.end(element) + } + } + + await elementZeroStarted.wait() + task.cancel() + await elementZeroMayFinish.open() + + let outcome = await task.result + #expect(throws: CancellationError.self) { try outcome.get() } + #expect(ledger.events == ["start 0", "end 0"]) + } + + @Test func theFirstFailureIsRethrownAndPendingElementsNeverStart() async { + struct Failure: Error, Equatable { + let element: Int + } + let ledger = Ledger() + await #expect(throws: Failure(element: 1)) { + _ = try await Array(0 ..< 5).concurrentMap(maximumConcurrency: 1) { element in + ledger.start(element) + defer { ledger.end(element) } + if element == 1 { throw Failure(element: element) } + } + } + // Serial window: element 0 completed, element 1 threw, 2…4 never started. + #expect(ledger.events == ["start 0", "end 0", "start 1", "end 1"]) + } + + @Test func emptyInputYieldsEmptyOutput() async throws { + let results = try await [Int]().concurrentMap(maximumConcurrency: 4) { $0 } + #expect(results.isEmpty) + } +} diff --git a/Tests/SwiftInterfaceTests/EventDeliverySerializationTests.swift b/Tests/SwiftInterfaceTests/EventDeliverySerializationTests.swift new file mode 100644 index 00000000..ddb2bab6 --- /dev/null +++ b/Tests/SwiftInterfaceTests/EventDeliverySerializationTests.swift @@ -0,0 +1,82 @@ +import Foundation +import Testing +import SwiftDeclaration + +/// Pins the dispatcher's process-wide serialization of handler invocation +/// (evolution proposal `large-stack-executor-and-cross-version-parallelism`): +/// `SwiftIndexEvents.Handler` carries no `Sendable` requirement, and +/// cross-version preparation shares one host handler across N dispatchers on +/// N tasks, so without the lock a stateful handler is entered concurrently. +@Suite +struct EventDeliverySerializationTests { + /// Counts concurrent entries into `handle`; a short spin inside makes an + /// overlap near-certain when delivery is not serialized. + private final class OverlapDetectingHandler: SwiftIndexEvents.Handler, @unchecked Sendable { + private let lock = NSLock() + private var inFlight = 0 + private(set) var maximumInFlight = 0 + private(set) var eventCount = 0 + + func handle(event: SwiftIndexEvents.Payload) { + lock.withLock { + inFlight += 1 + maximumInFlight = max(maximumInFlight, inFlight) + eventCount += 1 + } + usleep(50) + lock.withLock { inFlight -= 1 } + } + } + + @Test func aHandlerSharedByConcurrentDispatchersIsNeverEnteredConcurrently() async { + let handler = OverlapDetectingHandler() + let dispatcherCount = 4 + let eventsPerDispatcher = 200 + let dispatchers = (0 ..< dispatcherCount).map { _ in + let dispatcher = SwiftIndexEvents.Dispatcher() + dispatcher.addHandler(handler) + return dispatcher + } + + await withTaskGroup(of: Void.self) { group in + for dispatcher in dispatchers { + group.addTask { + for _ in 0 ..< eventsPerDispatcher { + dispatcher.dispatch(.moduleCollectionStarted) + } + } + } + } + + #expect(handler.eventCount == dispatcherCount * eventsPerDispatcher) + #expect(handler.maximumInFlight == 1, "handler was entered \(handler.maximumInFlight) times concurrently") + } + + /// The lock is recursive: a handler that dispatches from inside `handle` + /// (a host forwarding events into its own dispatcher) must not deadlock. + private final class ReentrantHandler: SwiftIndexEvents.Handler, @unchecked Sendable { + let inner = SwiftIndexEvents.Dispatcher() + private let lock = NSLock() + private(set) var outerCount = 0 + + func handle(event: SwiftIndexEvents.Payload) { + lock.withLock { outerCount += 1 } + if case .moduleCollectionStarted = event { + inner.dispatch(.moduleCollectionCompleted(result: .init(moduleCount: 0, modules: []))) + } + } + } + + @Test(.timeLimit(.minutes(1))) func reentrantDispatchFromAHandlerDoesNotDeadlock() { + let handler = ReentrantHandler() + let innerHandler = OverlapDetectingHandler() + handler.inner.addHandler(innerHandler) + let outer = SwiftIndexEvents.Dispatcher() + outer.addHandler(handler) + + outer.dispatch(.moduleCollectionStarted) + + #expect(handler.outerCount == 1) + #expect(innerHandler.eventCount == 1) + } +} diff --git a/Tests/SwiftInterfaceTests/ExportedOnlyInterfaceTests.swift b/Tests/SwiftInterfaceTests/ExportedOnlyInterfaceTests.swift new file mode 100644 index 00000000..5ce9d3f8 --- /dev/null +++ b/Tests/SwiftInterfaceTests/ExportedOnlyInterfaceTests.swift @@ -0,0 +1,210 @@ +import Foundation +import Testing +import MachOKit +@_spi(Internals) @testable import MachOSymbols +@_spi(Support) @testable import SwiftDeclaration +@_spi(Support) @testable import SwiftPrinting +@_spi(Support) @testable import SwiftInterface +@testable import MachOTestingSupport +import MachOFixtureSupport + +/// End-to-end coverage for the exported-only filter (evolution proposal +/// `exported-only-interface`) on the `SymbolTestsCore` fixture — a +/// library-evolution Release build with `ENABLE_TESTABILITY`, so its +/// `internal` declarations ARE exported and only `private` ones are not. +/// That makes it the right fixture for the declaration-level rules +/// (private types / protocols / their extensions) and the member rule's +/// three exemptions; the `internal` shapes are pinned separately by +/// `ExportedOnlyLibraryEvolutionFixtureTests` on a compile-on-the-fly module. +/// +/// Every negative assertion is paired with the same assertion against the +/// DEFAULT output, so "absent" provably means "dropped by the filter" and not +/// "never rendered in the first place". +@Suite(.serialized) +final class ExportedOnlyInterfaceTests: MachOFileTests, @unchecked Sendable { + override class var fileName: MachOFileName { .SymbolTestsCore } + + private func buildOutput(exportedOnly: Bool, annotate: Bool = false) async throws -> String { + var printConfiguration = SwiftDeclarationPrintConfiguration() + printConfiguration.printExportedDeclarationsOnly = exportedOnly + printConfiguration.printExportStatus = annotate + let configuration = SwiftInterfaceBuilderConfiguration( + indexConfiguration: .init(showCImportedTypes: false), + printConfiguration: printConfiguration + ) + let unsafeMachOFile = machOFile + let builder = try SwiftInterfaceBuilder(configuration: configuration, eventHandlers: [], in: unsafeMachOFile) + try await builder.prepare() + return try await builder.printRoot().string + } + + /// A top-level `private struct`: its nominal type descriptor is a local + /// symbol, so the whole declaration goes — while the `public enum` anchor + /// declared in the same file stays. + @Test func privateTypeIsDropped() async throws { + let unsafeMachOFile = machOFile + let descriptorName = "_$s15SymbolTestsCore19PrivateDoppelganger33_1282F137F8790A6AF4B7E2738A640142LLVMn" + #expect(SymbolIndexStore.shared.isExported(name: descriptorName, in: unsafeMachOFile) == false) + + let defaultOutput = try await buildOutput(exportedOnly: false) + #expect(defaultOutput.contains("struct PrivateDoppelganger {")) + + let filteredOutput = try await buildOutput(exportedOnly: true) + #expect(!filteredOutput.contains("struct PrivateDoppelganger {")) + #expect(!filteredOutput.contains("class PrivateDoppelgangerClass {")) + #expect(filteredOutput.contains("enum PrivateDoppelgangerFirstFileAnchors {")) + } + + /// A `private protocol` goes by its protocol descriptor, and the + /// protocol-extension default implementations attached to it (rendered + /// trailing the declaration, evolution proposal 0007) go with it. + @Test func privateProtocolAndItsDefaultImplementationsAreDropped() async throws { + let unsafeMachOFile = machOFile + let descriptorName = "_$s15SymbolTestsCore27PrivateDoppelgangerProtocol33_1282F137F8790A6AF4B7E2738A640142LLMp" + #expect(SymbolIndexStore.shared.isExported(name: descriptorName, in: unsafeMachOFile) == false) + + let defaultOutput = try await buildOutput(exportedOnly: false) + #expect(defaultOutput.contains("protocol PrivateDoppelgangerProtocol {")) + #expect(defaultOutput.contains("var alphaDefaultProperty: Swift.Int64 {")) + + let filteredOutput = try await buildOutput(exportedOnly: true) + #expect(!filteredOutput.contains("protocol PrivateDoppelgangerProtocol {")) + #expect(!filteredOutput.contains("extension SymbolTestsCore.PrivateDoppelgangerProtocol {")) + #expect(!filteredOutput.contains("alphaDefaultProperty")) + } + + /// A conformance extension whose target is a private type — an + /// extension owns no descriptor symbol, so the verdict comes from the + /// installed `ExportFilterScope`'s in-image tables. + @Test func conformanceExtensionOfPrivateTypeIsDropped() async throws { + let conformanceHeader = "extension SymbolTestsCore.AlphaProtocolWitness: SymbolTestsCore.PrivateDoppelgangerProtocol {}" + let defaultOutput = try await buildOutput(exportedOnly: false) + #expect(defaultOutput.contains(conformanceHeader)) + + let filteredOutput = try await buildOutput(exportedOnly: true) + #expect(!filteredOutput.contains(conformanceHeader)) + #expect(!filteredOutput.contains("AlphaProtocolWitness")) + } + + /// A private type NESTED in a public one drops alone: the parent and its + /// public sibling stay, and so does nothing of the nested type's + /// conformance extensions (their target is the dropped nested type). + @Test func nestedPrivateTypeIsDroppedWhileParentStays() async throws { + let unsafeMachOFile = machOFile + let descriptorName = "_$s15SymbolTestsCore7StructsO19PrivateProtocolTest33_930E68B58AC9D850D1A6B7A3A1786E37LLOMn" + #expect(SymbolIndexStore.shared.isExported(name: descriptorName, in: unsafeMachOFile) == false) + + let defaultOutput = try await buildOutput(exportedOnly: false) + #expect(defaultOutput.contains(" enum PrivateProtocolTest {")) + #expect(defaultOutput.contains("extension SymbolTestsCore.Structs.PrivateProtocolTest: Swift.Hashable {")) + + let filteredOutput = try await buildOutput(exportedOnly: true) + #expect(filteredOutput.contains("enum Structs {")) + #expect(filteredOutput.contains(" struct StructTest {")) + #expect(!filteredOutput.contains("enum PrivateProtocolTest {")) + #expect(!filteredOutput.contains("extension SymbolTestsCore.Structs.PrivateProtocolTest")) + } + + /// The regression the descriptor-offset leg exists for: a public type + /// nested in a CONSTRAINED extension mangles only the extension's own + /// requirement into its context (`…VAASYRzrlE28RawRepresentableNestedStructVMn`), + /// while the model's name node carries the type's full signature — a + /// remangled name misses the trie and the first implementation dropped + /// this exported type. The verdict must come from the symbol AT the + /// descriptor, which spells the name the compiler's way. + @Test func publicTypeNestedInConstrainedExtensionIsKept() async throws { + let unsafeMachOFile = machOFile + let descriptorName = "_$s15SymbolTestsCore8GenericsO22GenericRequirementTestVAASYRzrlE28RawRepresentableNestedStructVMn" + #expect(SymbolIndexStore.shared.isExported(name: descriptorName, in: unsafeMachOFile) == true) + + let filteredOutput = try await buildOutput(exportedOnly: true) + #expect(filteredOutput.contains("struct RawRepresentableNestedStruct {")) + } + + /// The member rule: `GenericAsyncSequenceTest.AsyncIterator`'s implicit + /// `init()` is `internal` with no exported form (the true positive of + /// `ExportStatusAnnotationTests`), so it goes — while the struct that + /// declares it stays with its exported `next()`. + @Test func nonExportedMemberIsDroppedWhileItsTypeStays() async throws { + let filteredOutput = try await buildOutput(exportedOnly: true) + let typeBlock = try #require(filteredOutput.range(of: "struct GenericAsyncSequenceTest")) + let iteratorBlock = try #require(filteredOutput.range(of: "struct AsyncIterator {", range: typeBlock.upperBound ..< filteredOutput.endIndex)) + let lines = filteredOutput[iteratorBlock.upperBound...].split(separator: "\n", omittingEmptySubsequences: false) + let closingIndex = lines.firstIndex { $0.trimmingCharacters(in: .whitespaces) == "}" } ?? lines.endIndex + let blockLines = lines[.. Swift.String")) + } + + /// A conformance extension is kept even when the filter empties it: a + /// synthesized `Equatable`'s `==` witness is not statically callable + /// (proposal 0008 deliberately does not exempt witnesses), so the body + /// collapses to `{}` — the same shape a `.swiftinterface` prints for a + /// synthesized conformance — and the conformance clause itself remains. + @Test func emptiedConformanceExtensionKeepsItsClause() async throws { + let defaultOutput = try await buildOutput(exportedOnly: false) + #expect(defaultOutput.contains("extension SymbolTestsCore.Enums.RawValueEnumTest: Swift.Equatable {\n")) + + let filteredOutput = try await buildOutput(exportedOnly: true) + #expect(filteredOutput.contains("extension SymbolTestsCore.Enums.RawValueEnumTest: Swift.Equatable {}")) + } + + /// The filter's drop condition IS the annotation's emit condition, so + /// with both flags on the output carries zero `not exported` comments — + /// a structural invariant, not a fixture coincidence. + @Test func filteredOutputCarriesNoAnnotation() async throws { + let annotatedOutput = try await buildOutput(exportedOnly: false, annotate: true) + #expect(annotatedOutput.contains("// not exported")) + + let filteredOutput = try await buildOutput(exportedOnly: true, annotate: true) + #expect(!filteredOutput.contains("// not exported")) + } + + /// Dropped definitions render as EMPTY results that every `BlockList` / + /// `NestedDeclaration` skips outright — no orphaned separator breaks. + @Test func filteredOutputHasNoBlankLineArtifacts() async throws { + let filteredOutput = try await buildOutput(exportedOnly: true) + #expect(!filteredOutput.contains("\n\n\n")) + #expect(!filteredOutput.contains("{\n}")) + } + + /// Flag off (the default): byte-identical to a builder that never heard + /// of the filter. + @Test func defaultOutputIsUnchanged() async throws { + let defaultOutput = try await buildOutput(exportedOnly: false) + let unsafeMachOFile = machOFile + let builder = try SwiftInterfaceBuilder(configuration: .init(indexConfiguration: .init(showCImportedTypes: false)), eventHandlers: [], in: unsafeMachOFile) + try await builder.prepare() + let untouchedOutput = try await builder.printRoot().string + #expect(defaultOutput == untouchedOutput) + } +} diff --git a/Tests/SwiftInterfaceTests/ExportedOnlyLibraryEvolutionFixtureTests.swift b/Tests/SwiftInterfaceTests/ExportedOnlyLibraryEvolutionFixtureTests.swift new file mode 100644 index 00000000..7c1940e3 --- /dev/null +++ b/Tests/SwiftInterfaceTests/ExportedOnlyLibraryEvolutionFixtureTests.swift @@ -0,0 +1,267 @@ +import Foundation +import Testing +import MachOKit +import Semantic +@_spi(Internals) @testable import MachOSymbols +@testable import MachOSwiftSection +@_spi(Support) @testable import SwiftPrinting +@_spi(Support) @testable import SwiftInterface + +/// The exported-only filter (evolution proposal +/// `exported-only-interface`) over `internal` declarations — the +/// shape `SymbolTestsCore` cannot pin, because that fixture is built with +/// `ENABLE_TESTABILITY` and exports its internals. This module is compiled +/// on the fly with `-enable-library-evolution` and WITHOUT `-enable-testing`, +/// so every `internal` declaration is a local symbol and the filter has a +/// definitive negative to act on for: a type, a nested type, a protocol and +/// its default implementation, a method, a stored property, a global, a +/// conformance to an internal protocol, and a constrained extension whose +/// members are all internal. +@Suite(.serialized) +struct ExportedOnlyLibraryEvolutionFixtureTests { + private enum FixtureWorkingDirectoryCleanup { + nonisolated(unsafe) static var directories: [URL] = [] + static let registration: Void = { + atexit { + for directory in FixtureWorkingDirectoryCleanup.directories { + try? FileManager.default.removeItem(at: directory) + } + } + }() + } + + /// The `Anchor` class is load-bearing fixture ballast: a struct-only + /// module compiles to a dylib with no `__DATA` segment, whose + /// chained-fixup pages the pinned MachOKit release mis-walks (see + /// `DiffMemberIndentationTests`). `@inline(never)` keeps the internal + /// members' symbols from being folded away. + private static let fixtureSource = """ + public struct PublicShape { + public var title: String + var internalNote: String + public init(title: String, internalNote: String) { + self.title = title + self.internalNote = internalNote + } + public func publicMethod() {} + @inline(never) func internalMethod() {} + public struct PublicNested {} + struct InternalNested {} + } + struct InternalShape { + var value: Int + } + extension InternalShape { + @inline(never) func orphanMethod() {} + } + public protocol PublicContract { + func requirement() + } + protocol InternalContract { + func hidden() + } + extension InternalContract { + @inline(never) func hiddenDefault() {} + } + extension PublicShape: PublicContract { + public func requirement() {} + } + extension PublicShape: InternalContract { + @inline(never) func hidden() {} + } + public struct PublicBox { + public var element: Element + public init(element: Element) { self.element = element } + } + extension PublicBox where Element == Int { + @inline(never) func internalConstrainedMember() {} + } + extension PublicBox where Element == String { + public func publicConstrainedMember() {} + } + public struct PublicPair { + public var first: Element + public var second: Element + public init(first: Element, second: Element) { + self.first = first + self.second = second + } + } + extension PublicPair where Element == Int { + @inline(never) func internalOnlyConstrainedMember() {} + } + public enum PublicEnum { + case alpha + case beta + } + public class Anchor { + public func run() {} + } + public func publicGlobal() {} + @inline(never) func internalGlobal() {} + """ + + // `Swift.Error` spelled out: the `Semantic` import brings its own `Error` + // type into scope, which would otherwise win the lookup. + private static let fixtureCompilationResult: Result = { + Result { + let workingDirectory = FileManager.default.temporaryDirectory + .appendingPathComponent("ExportedOnlyFixture-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: workingDirectory, withIntermediateDirectories: true) + _ = FixtureWorkingDirectoryCleanup.registration + FixtureWorkingDirectoryCleanup.directories.append(workingDirectory) + + let sourceURL = workingDirectory.appendingPathComponent("ExportedOnlyFixture.swift") + let libraryURL = workingDirectory.appendingPathComponent("libExportedOnlyFixture.dylib") + try fixtureSource.write(to: sourceURL, atomically: true, encoding: .utf8) + + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun") + process.arguments = [ + "swiftc", "-emit-library", "-enable-library-evolution", + "-module-name", "ExportedOnlyFixture", + sourceURL.path, "-o", libraryURL.path, + ] + let standardErrorPipe = Pipe() + process.standardError = standardErrorPipe + try process.run() + // Drain BEFORE waitUntilExit — see LegacyDyldInfoBindTests. + let diagnosticsData = standardErrorPipe.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + guard process.terminationStatus == 0 else { + let diagnostics = String(decoding: diagnosticsData, as: UTF8.self) + throw FixtureCompilationError(diagnostics: diagnostics) + } + return libraryURL + } + }() + + private struct FixtureCompilationError: Swift.Error, CustomStringConvertible { + let diagnostics: String + var description: String { "Exported-only fixture compilation failed:\n\(diagnostics)" } + } + + private func loadFixtureMachOFile() throws -> MachOFile { + let libraryURL = try Self.fixtureCompilationResult.get() + let file = try MachOKit.loadFromFile(url: libraryURL) + switch file { + case .machO(let machOFile): + return machOFile + case .fat(let fatFile): + let machOFile = try fatFile.machOFiles().first { $0.header.cpuType == .arm64 } + return try #require(machOFile, "fixture unexpectedly missing an arm64 slice") + } + } + + private func buildOutput(exportedOnly: Bool) async throws -> (output: String, machOFile: MachOFile) { + let machOFile = try loadFixtureMachOFile() + var printConfiguration = SwiftDeclarationPrintConfiguration() + printConfiguration.printExportedDeclarationsOnly = exportedOnly + let builder = try SwiftInterfaceBuilder( + configuration: .init(printConfiguration: printConfiguration), + eventHandlers: [], + in: machOFile + ) + try await builder.prepare() + return (try await builder.printRoot().string, machOFile) + } + + /// The premise every assertion below rests on: without `-enable-testing` + /// the internal descriptors are local symbols and the public ones are + /// in the trie — the store must answer `false` / `true`, never `nil`. + @Test func fixtureExportsExactlyItsPublicDescriptors() async throws { + let (_, machOFile) = try await buildOutput(exportedOnly: false) + #expect(SymbolIndexStore.shared.isExported(name: "_$s19ExportedOnlyFixture11PublicShapeVMn", in: machOFile) == true) + #expect(SymbolIndexStore.shared.isExported(name: "_$s19ExportedOnlyFixture13InternalShapeVMn", in: machOFile) == false) + #expect(SymbolIndexStore.shared.isExported(name: "_$s19ExportedOnlyFixture14PublicContractMp", in: machOFile) == true) + #expect(SymbolIndexStore.shared.isExported(name: "_$s19ExportedOnlyFixture16InternalContractMp", in: machOFile) == false) + } + + @Test func internalTypesAreDroppedAtEveryNestingLevel() async throws { + let (defaultOutput, _) = try await buildOutput(exportedOnly: false) + #expect(defaultOutput.contains("struct InternalShape {")) + #expect(defaultOutput.contains("struct InternalNested {")) + + let (filteredOutput, _) = try await buildOutput(exportedOnly: true) + #expect(filteredOutput.contains("struct PublicShape {")) + #expect(filteredOutput.contains("struct PublicNested {")) + #expect(!filteredOutput.contains("struct InternalShape {")) + #expect(!filteredOutput.contains("struct InternalNested {")) + // The internal type's extension goes with its target. + #expect(!filteredOutput.contains("extension ExportedOnlyFixture.InternalShape")) + #expect(!filteredOutput.contains("orphanMethod")) + } + + @Test func internalProtocolItsDefaultsAndConformancesToItAreDropped() async throws { + let (defaultOutput, _) = try await buildOutput(exportedOnly: false) + #expect(defaultOutput.contains("protocol InternalContract {")) + #expect(defaultOutput.contains("extension ExportedOnlyFixture.PublicShape: ExportedOnlyFixture.InternalContract")) + + let (filteredOutput, _) = try await buildOutput(exportedOnly: true) + #expect(filteredOutput.contains("protocol PublicContract {")) + #expect(filteredOutput.contains("extension ExportedOnlyFixture.PublicShape: ExportedOnlyFixture.PublicContract")) + #expect(filteredOutput.contains("func requirement()")) + #expect(!filteredOutput.contains("protocol InternalContract {")) + #expect(!filteredOutput.contains("hiddenDefault")) + // A PUBLIC type's conformance to an INTERNAL protocol: the target is + // exported, the conforming protocol is not — the extension goes. + #expect(!filteredOutput.contains("extension ExportedOnlyFixture.PublicShape: ExportedOnlyFixture.InternalContract")) + #expect(!filteredOutput.contains("func hidden()")) + } + + /// Members and STORED properties of a kept type: the internal method and + /// the internal stored `var` (whose accessor group joined as local + /// symbols) go, the public ones stay. + @Test func internalMembersAndStoredPropertiesAreDropped() async throws { + let (defaultOutput, _) = try await buildOutput(exportedOnly: false) + #expect(defaultOutput.contains("var internalNote: Swift.String")) + #expect(defaultOutput.contains("func internalMethod()")) + + let (filteredOutput, _) = try await buildOutput(exportedOnly: true) + #expect(filteredOutput.contains("var title: Swift.String")) + #expect(filteredOutput.contains("func publicMethod()")) + #expect(filteredOutput.contains("init(title: Swift.String, internalNote: Swift.String)")) + #expect(!filteredOutput.contains("var internalNote")) + #expect(!filteredOutput.contains("func internalMethod()")) + } + + /// Constrained-extension members render inside ONE plain + /// `extension Foo { … }` container per type, each member carrying its + /// own `where` clause. `PublicPair`'s container holds only an internal + /// member: the filter EMPTIES it and, being a plain extension, the whole + /// block goes. `PublicBox`'s container keeps its public member and loses + /// the internal one. + @Test func emptiedPlainExtensionIsDroppedAndPopulatedOneKept() async throws { + let (defaultOutput, _) = try await buildOutput(exportedOnly: false) + #expect(defaultOutput.contains("extension ExportedOnlyFixture.PublicPair {")) + #expect(defaultOutput.contains("func internalOnlyConstrainedMember() where A == Swift.Int")) + #expect(defaultOutput.contains("func internalConstrainedMember() where A == Swift.Int")) + + let (filteredOutput, _) = try await buildOutput(exportedOnly: true) + #expect(filteredOutput.contains("struct PublicPair {")) + #expect(!filteredOutput.contains("extension ExportedOnlyFixture.PublicPair")) + #expect(!filteredOutput.contains("internalOnlyConstrainedMember")) + #expect(filteredOutput.contains("extension ExportedOnlyFixture.PublicBox {")) + #expect(filteredOutput.contains("func publicConstrainedMember() where A == Swift.String")) + #expect(!filteredOutput.contains("internalConstrainedMember")) + } + + /// Globals follow the member rule; enum cases own no symbols and are + /// never touched. + @Test func internalGlobalIsDroppedAndEnumCasesAreKept() async throws { + let (defaultOutput, _) = try await buildOutput(exportedOnly: false) + #expect(defaultOutput.contains("func internalGlobal()")) + + let (filteredOutput, _) = try await buildOutput(exportedOnly: true) + #expect(filteredOutput.contains("func publicGlobal()")) + #expect(!filteredOutput.contains("func internalGlobal()")) + #expect(filteredOutput.contains("case alpha")) + #expect(filteredOutput.contains("case beta")) + } + + @Test func filteredOutputHasNoBlankLineArtifacts() async throws { + let (filteredOutput, _) = try await buildOutput(exportedOnly: true) + #expect(!filteredOutput.contains("\n\n\n")) + #expect(!filteredOutput.contains("{\n}")) + } +} diff --git a/Tests/SwiftInterfaceTests/Snapshots/__Snapshots__/SymbolTestsCoreInterfaceSnapshotTests/interfaceSnapshot.1.txt b/Tests/SwiftInterfaceTests/Snapshots/__Snapshots__/SymbolTestsCoreInterfaceSnapshotTests/interfaceSnapshot.1.txt index dfc6484a..978cc2b1 100644 --- a/Tests/SwiftInterfaceTests/Snapshots/__Snapshots__/SymbolTestsCoreInterfaceSnapshotTests/interfaceSnapshot.1.txt +++ b/Tests/SwiftInterfaceTests/Snapshots/__Snapshots__/SymbolTestsCoreInterfaceSnapshotTests/interfaceSnapshot.1.txt @@ -3154,7 +3154,7 @@ enum VTableEntryVariants { get } - static func classMethod() + class func classMethod() static func staticMethod() deinit diff --git a/Tests/SwiftInterfaceTests/SwiftEvolutionInterfaceBuilderTests.swift b/Tests/SwiftInterfaceTests/SwiftEvolutionInterfaceBuilderTests.swift index d640976c..f4b73e2d 100644 --- a/Tests/SwiftInterfaceTests/SwiftEvolutionInterfaceBuilderTests.swift +++ b/Tests/SwiftInterfaceTests/SwiftEvolutionInterfaceBuilderTests.swift @@ -3,6 +3,7 @@ import Testing import MachOKit import Semantic import SwiftDiffing +import SwiftDeclaration @testable import MachOSwiftSection @_spi(Support) @testable import SwiftInterface @@ -135,15 +136,103 @@ struct SwiftEvolutionInterfaceBuilderTests { } } - private func preparedBuilder() async throws -> AnySwiftEvolutionInterfaceBuilder { + private func preparedBuilder(maximumConcurrentPreparations: Int = ProcessInfo.processInfo.activeProcessorCount) async throws -> AnySwiftEvolutionInterfaceBuilder { let builder = try AnySwiftEvolutionInterfaceBuilder( versions: try loadFixtureMachOFiles(), labels: ["1.0", "2.0", "3.0"] ) - try await builder.prepare() + try await builder.prepare(maximumConcurrentPreparations: maximumConcurrentPreparations) return builder } + // MARK: - Cross-version parallel preparation + + /// `prepare(maximumConcurrentPreparations:)` (evolution proposal + /// `large-stack-executor-and-cross-version-parallelism`) indexes the + /// versions concurrently; the annotated interface, the structured stream + /// and the evolution's JSON must be byte-identical to the serial + /// (`1`) preparation — the window is a scheduling knob, never a semantic + /// one. + /// + /// The PARALLEL builder prepares first: the per-image caches key on the + /// file's identity, so a serial run first would leave the parallel run + /// with warm caches and no concurrent cold build — the one thing this + /// test exists to exercise. + @Test func parallelPreparationMatchesSerialPreparation() async throws { + let parallelBuilder = try await preparedBuilder(maximumConcurrentPreparations: 3) + let serialBuilder = try await preparedBuilder(maximumConcurrentPreparations: 1) + + let serialInterface = try await serialBuilder.printAnnotatedInterface().string + let parallelInterface = try await parallelBuilder.printAnnotatedInterface().string + #expect(serialInterface == parallelInterface) + #expect(serialInterface.contains("removed in 2.0")) + + let serialBlocks = try await serialBuilder.annotatedBlocks() + let parallelBlocks = try await parallelBuilder.annotatedBlocks() + #expect(serialBlocks.map { $0.map(\.content.string) } == parallelBlocks.map { $0.map(\.content.string) }) + + let encoder = ABIJSON.encoder() + let serialEvolution = try encoder.encode(try #require(serialBuilder.evolution)) + let parallelEvolution = try encoder.encode(try #require(parallelBuilder.evolution)) + #expect(serialEvolution == parallelEvolution) + } + + /// A recording handler: which version labels reported through it, and + /// how many events it saw. Reference type on purpose — the assertion is + /// about the instance each version was handed. + private final class RecordingHandler: SwiftIndexEvents.Handler, @unchecked Sendable { + private let lock = NSLock() + private(set) var eventCount = 0 + let label: String + + init(label: String) { + self.label = label + } + + func handle(event: SwiftIndexEvents.Payload) { + lock.withLock { eventCount += 1 } + } + } + + /// `eventHandlersPerVersion` hands each version its own handlers (built + /// from the version's index and label), on top of the shared ones — the + /// seam the CLI uses to label each version's stderr diagnostics. + @Test func perVersionEventHandlersReachTheirOwnVersion() async throws { + let shared = RecordingHandler(label: "shared") + var perVersion: [RecordingHandler] = [] + let perVersionLock = NSLock() + let builder = try AnySwiftEvolutionInterfaceBuilder( + eventHandlers: [shared], + eventHandlersPerVersion: { versionIndex, label in + let handler = RecordingHandler(label: "\(versionIndex):\(label)") + perVersionLock.withLock { perVersion.append(handler) } + return [handler] + }, + versions: try loadFixtureMachOFiles(), + labels: ["1.0", "2.0", "3.0"] + ) + try await builder.prepare(maximumConcurrentPreparations: 3) + + #expect(perVersion.map(\.label).sorted() == ["0:1.0", "1:2.0", "2:3.0"]) + for handler in perVersion { + #expect(handler.eventCount > 0, "\(handler.label) saw no events") + } + // The shared handler hears every version: at least the sum of what + // the per-version handlers saw individually. + #expect(shared.eventCount >= perVersion.map(\.eventCount).max() ?? 0) + #expect(shared.eventCount == perVersion.map(\.eventCount).reduce(0, +)) + } + + /// A window wider than the version count and a window below 1 are both + /// clamped, not rejected. + @Test func preparationWindowIsClampedNotValidated() async throws { + let wideBuilder = try await preparedBuilder(maximumConcurrentPreparations: 64) + let narrowBuilder = try await preparedBuilder(maximumConcurrentPreparations: 0) + let wideInterface = try await wideBuilder.printAnnotatedInterface().string + let narrowInterface = try await narrowBuilder.printAnnotatedInterface().string + #expect(wideInterface == narrowInterface) + } + // MARK: - The annotated interface @Test func annotatedInterfaceCarriesEveryLifecycleAnnotation() async throws { diff --git a/Tests/SwiftInterfaceTests/SwiftInterfaceBuilderDependenciesTests.swift b/Tests/SwiftInterfaceTests/SwiftInterfaceBuilderDependenciesTests.swift new file mode 100644 index 00000000..79177b15 --- /dev/null +++ b/Tests/SwiftInterfaceTests/SwiftInterfaceBuilderDependenciesTests.swift @@ -0,0 +1,71 @@ +import Foundation +import MachOFixtureSupport +import MachOKit +import MachOKitExtensions +import MachODependencies +import Testing +@testable import MachOTestingSupport +@testable import SwiftInterface + +/// Pins what `SwiftInterfaceBuilderDependencies` promises now that it is a +/// thin wrapper over `DependencyClosure` (evolution proposal +/// macho-dependencies-module): direct-only semantics for both readers, +/// and an image initializer that actually resolves something. +/// +/// Declares `SymbolTestsHelper` for the same reason `DependencyClosureTests` +/// does: the image initializer resolves the fixture's `@rpath` sibling through +/// `MachOImage(name:)`. Nothing here indexes it or touches a per-image cache — +/// the declaration is what makes the sharing greppable, as AGENTS.md asks of +/// every suite touching that image. +@Suite(ExclusiveImageAccess(.SymbolTestsHelper)) +final class SwiftInterfaceBuilderDependenciesTests: MachOSwiftSectionFixtureTests, @unchecked Sendable { + private func bareImageNames(of images: [MachO]) -> Set { + Set(images.map { DependencyLoadName.bareImageName(of: $0.imagePath) }) + } + + private func directBareImageNames(of root: MachO) -> Set { + Set(root.dependencies.map { DependencyLoadName.bareImageName(of: $0.dylib.name) }) + } + + /// Regression: the image initializer used to hand each raw load path to + /// `MachOImage(name:)`, which compares bare names, so it resolved nothing + /// for its whole life. Nothing in-tree called it, which is how it survived. + @MainActor + @Test func imageInitializerResolvesTheMappedDirectDependencies() { + let root = machOImage + let dependencies = SwiftInterfaceBuilderDependencies(machO: root) + let resolved = bareImageNames(of: dependencies.dependencies) + #expect(resolved.contains("libswiftCore")) + #expect(resolved.contains("Foundation")) + #expect(resolved.isSubset(of: directBareImageNames(of: root)), "direct dependencies only") + } + + @MainActor + @Test func fileInitializerKeepsDirectSemanticsAndReportsMisses() { + guard FullDyldCache.host != nil else { + print("skipped: no host dyld shared cache") + return + } + let root = machOFile + let dependencies = SwiftInterfaceBuilderDependencies(machO: root, searchPaths: [.systemDyldSharedCache]) + let resolved = bareImageNames(of: dependencies.dependencies) + #expect(resolved.contains("libswiftCore")) + #expect(resolved.isSubset(of: directBareImageNames(of: root)), "the cache-backed initializer never walks a dependency's own load commands") + #expect(dependencies.unresolvedLoadNames.contains("@rpath/SymbolTestsHelper.framework/Versions/A/SymbolTestsHelper"), "the @rpath sibling is not in the cache and must be named, not dropped") + } + + /// A host that resolved a transitive closure once can hand it over + /// unchanged — the wrapper imposes no traversal of its own. + @MainActor + @Test func closureInitializerPreservesTheCallersTraversal() { + guard FullDyldCache.host != nil else { + print("skipped: no host dyld shared cache") + return + } + let root = machOFile + let direct = SwiftInterfaceBuilderDependencies(machO: root, searchPaths: [.systemDyldSharedCache]) + let transitive = SwiftInterfaceBuilderDependencies(closure: DependencyClosure(root: root, searchPaths: [.systemDyldSharedCache], traversal: .transitive)) + #expect(transitive.dependencies.count > direct.dependencies.count) + #expect(transitive.machO.imagePath == root.imagePath) + } +} diff --git a/Tests/SwiftSectionCommandTests/DiffCommandValidationTests.swift b/Tests/SwiftSectionCommandTests/DiffCommandValidationTests.swift new file mode 100644 index 00000000..8d46d7c9 --- /dev/null +++ b/Tests/SwiftSectionCommandTests/DiffCommandValidationTests.swift @@ -0,0 +1,45 @@ +import Foundation +import Testing +import ArgumentParser +@testable import swift_section + +/// Pins `swift-section diff`'s `--jobs` option (evolution proposal +/// `large-stack-executor-and-cross-version-parallelism`): the two sides index +/// concurrently by default, `--jobs 1` restores the old-then-new order, and a +/// window below 1 is rejected at validation time rather than clamped silently +/// — on the command line a zero is a typo, not a request. +@Suite +struct DiffCommandValidationTests { + private func expectValidationFailure(_ arguments: [String], messagePart: String) { + #expect( + "parsing \(arguments) should fail validation", + performing: { + _ = try DiffCommand.parse(arguments) + }, + throws: { error in + DiffCommand.message(for: error).contains(messagePart) + } + ) + } + + @Test func jobsBelowOneIsRejected() { + expectValidationFailure( + ["--jobs", "0", "old.dylib", "new.dylib"], + messagePart: "--jobs must be at least 1" + ) + } + + @Test func jobsParsesAndDefaultsToAbsent() throws { + let explicit = try DiffCommand.parse(["--jobs", "1", "old.dylib", "new.dylib"]) + #expect(explicit.jobs == 1) + let implicit = try DiffCommand.parse(["old.dylib", "new.dylib"]) + #expect(implicit.jobs == nil) + } + + @Test func jobsParsesAlongsideTheInterfaceOptions() throws { + let command = try DiffCommand.parse(["--interface", "--format", "unified", "--jobs", "2", "old.dylib", "new.dylib"]) + #expect(command.interface) + #expect(command.format == .unified) + #expect(command.jobs == 2) + } +} diff --git a/Tests/SwiftSectionCommandTests/EvolutionCommandValidationTests.swift b/Tests/SwiftSectionCommandTests/EvolutionCommandValidationTests.swift index 26148c24..88b937d2 100644 --- a/Tests/SwiftSectionCommandTests/EvolutionCommandValidationTests.swift +++ b/Tests/SwiftSectionCommandTests/EvolutionCommandValidationTests.swift @@ -41,6 +41,22 @@ struct EvolutionCommandValidationTests { ) } + // MARK: - `--jobs` (evolution proposal `large-stack-executor-and-cross-version-parallelism`) + + @Test func jobsBelowOneIsRejected() { + expectValidationFailure( + ["--jobs", "0", "old.dylib", "new.dylib"], + messagePart: "--jobs must be at least 1" + ) + } + + @Test func jobsParsesAndDefaultsToAbsent() throws { + let explicit = try EvolutionCommand.parse(["--jobs", "2", "old.dylib", "new.dylib"]) + #expect(explicit.jobs == 2) + let implicit = try EvolutionCommand.parse(["old.dylib", "new.dylib"]) + #expect(implicit.jobs == nil) + } + @Test func interfaceParsesAlongsideTheSharedOptions() throws { let command = try EvolutionCommand.parse([ "--interface", "--labels", "1.0,2.0", "--fail-on-breaking", "old.dylib", "new.dylib", diff --git a/Tests/SwiftSectionCommandTests/ExportedOnlyFlagTests.swift b/Tests/SwiftSectionCommandTests/ExportedOnlyFlagTests.swift new file mode 100644 index 00000000..ebec5de8 --- /dev/null +++ b/Tests/SwiftSectionCommandTests/ExportedOnlyFlagTests.swift @@ -0,0 +1,27 @@ +import Foundation +import Testing +import ArgumentParser +@testable import swift_section + +/// Command-line surface for evolution proposal +/// `exported-only-interface`: `swift-section interface --exported-only` +/// exists and defaults to off — the contract that keeps default output +/// byte-identical. `dump` deliberately has no such flag (out of scope). +@Suite +struct ExportedOnlyFlagTests { + @Test func interfaceFlagDefaultsOff() throws { + let command = try InterfaceCommand.parse(["/tmp/example"]) + #expect(command.exportedOnly == false) + } + + @Test func interfaceFlagParses() throws { + let command = try InterfaceCommand.parse(["/tmp/example", "--exported-only"]) + #expect(command.exportedOnly == true) + } + + @Test func dumpHasNoSuchFlag() throws { + #expect(throws: (any Error).self) { + try DumpCommand.parse(["/tmp/example", "--exported-only"]) + } + } +} diff --git a/docs/superpowers/plans/2026-04-10-symboltestscore-integration-tests.md b/docs/superpowers/plans/2026-04-10-symboltestscore-integration-tests.md deleted file mode 100644 index a976d854..00000000 --- a/docs/superpowers/plans/2026-04-10-symboltestscore-integration-tests.md +++ /dev/null @@ -1,812 +0,0 @@ -# SymbolTestsCore Integration Tests Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Extend SymbolTestsCore with attribute-focused types and add comprehensive integration tests covering both new and existing functionality with structured assertions. - -**Architecture:** Add new Swift types to the existing SymbolTestsCore Xcode framework project, rebuild it, then write two test files — one middle-layer test using `SwiftInterfaceIndexer`/`TypeDefinition` APIs directly, and one end-to-end test using `SwiftInterfaceBuilder` output strings. Tests load the compiled `SymbolTestsCore.framework` binary via the existing `MachOFileTests` base class. - -**Tech Stack:** Swift Testing framework (`@Test`, `#expect`, `#require`), `SwiftInterfaceIndexer`, `TypeAttributeInferrer`, `MemberAttributeInferrer`, `OrderedMember`, `SwiftInterfaceBuilder`. - ---- - -### Task 1: Add New Types to SymbolTestsCore - -**Files:** -- Modify: `Tests/Projects/SymbolTests/SymbolTestsCore/SymbolTestsCore.swift` - -- [ ] **Step 1: Add PropertyWrapperStruct** - -Append to end of `SymbolTestsCore.swift`: - -```swift -@propertyWrapper -public struct PropertyWrapperStruct { - public var wrappedValue: Value - public var projectedValue: ClosedRange - - public init(wrappedValue: Value, range: ClosedRange) { - self.projectedValue = range - self.wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound) - } -} -``` - -- [ ] **Step 2: Add ResultBuilderStruct** - -Append: - -```swift -@resultBuilder -public struct ResultBuilderStruct { - public static func buildBlock(_ components: Element...) -> [Element] { components } - public static func buildOptional(_ component: [Element]?) -> [Element] { component ?? [] } -} -``` - -- [ ] **Step 3: Add DynamicMemberLookupStruct** - -Append: - -```swift -@dynamicMemberLookup -public struct DynamicMemberLookupStruct { - public subscript(dynamicMember member: String) -> Int { 0 } -} -``` - -- [ ] **Step 4: Add DynamicCallableStruct** - -Append: - -```swift -@dynamicCallable -public struct DynamicCallableStruct { - public func dynamicallyCall(withArguments arguments: [Int]) -> Int { - arguments.reduce(0, +) - } - - public func dynamicallyCall(withKeywordArguments arguments: KeyValuePairs) -> Int { 0 } -} -``` - -- [ ] **Step 5: Add ObjCAttributeClass** - -Append: - -```swift -public class ObjCAttributeClass: NSObject { - @objc public func objcMethod() {} - @nonobjc public func nonobjcMethod() {} - @objc public dynamic func objcDynamicMethod() {} -} -``` - -- [ ] **Step 6: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/SymbolTestsCore.swift -git commit -m "feat: add attribute-focused types to SymbolTestsCore" -``` - ---- - -### Task 2: Rebuild SymbolTests Xcode Project - -**Files:** -- Output: `Tests/Projects/SymbolTests/DerivedData/SymbolTests/Build/Products/Release/SymbolTestsCore.framework/Versions/A/SymbolTestsCore` - -- [ ] **Step 1: Build SymbolTests in Release configuration** - -```bash -xcodebuild -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj \ - -scheme SymbolTests \ - -configuration Release \ - -derivedDataPath Tests/Projects/SymbolTests/DerivedData \ - build 2>&1 | tail -5 -``` - -Expected: `** BUILD SUCCEEDED **` - -- [ ] **Step 2: Verify built binary contains new types** - -```bash -nm Tests/Projects/SymbolTests/DerivedData/SymbolTests/Build/Products/Release/SymbolTestsCore.framework/Versions/A/SymbolTestsCore | grep -c "PropertyWrapperStruct\|ResultBuilderStruct\|DynamicMemberLookupStruct\|DynamicCallableStruct\|ObjCAttributeClass" -``` - -Expected: Non-zero count (new symbols present in binary). - -- [ ] **Step 3: Commit built binary** - -```bash -git add Tests/Projects/SymbolTests/DerivedData/ -git commit -m "build: rebuild SymbolTests with new attribute-focused types" -``` - ---- - -### Task 3: Write Middle-Layer Integration Tests — Type Parsing and Fields - -**Files:** -- Create: `Tests/SwiftInterfaceTests/SymbolTestsCoreIntegrationTests.swift` - -- [ ] **Step 1: Create test file with shared setup** - -Create `Tests/SwiftInterfaceTests/SymbolTestsCoreIntegrationTests.swift`: - -```swift -import Foundation -import Testing -import MachOKit -import Dependencies -@_spi(Internals) import MachOSymbols -@_spi(Internals) import MachOCaches -@_spi(Support) @testable import SwiftInterface -@testable import MachOSwiftSection -@testable import MachOTestingSupport -@testable import SwiftDump - -// MARK: - Shared Setup - -@Suite(.serialized) -final class SymbolTestsCoreIntegrationTests: MachOFileTests, @unchecked Sendable { - override class var fileName: MachOFileName { .SymbolTestsCore } - - private func preparedIndexer() async throws -> SwiftInterfaceIndexer { - let indexer = SwiftInterfaceIndexer(in: machOFile) - try await indexer.prepare() - return indexer - } - - private func findTypeDefinition(named typeName: String, in indexer: SwiftInterfaceIndexer) -> TypeDefinition? { - indexer.allTypeDefinitions.values.first { $0.typeName.name.hasSuffix(typeName) } - } - - private func findProtocolDefinition(named protocolName: String, in indexer: SwiftInterfaceIndexer) -> ProtocolDefinition? { - indexer.allProtocolDefinitions.values.first { $0.protocolName.name.hasSuffix(protocolName) } - } -} - -// MARK: - Type Parsing - -extension SymbolTestsCoreIntegrationTests { - @Test func parsedTypeNamesContainExpectedTypes() async throws { - let indexer = try await preparedIndexer() - let typeNames = Set(indexer.allTypeDefinitions.values.map { $0.typeName.currentName }) - - #expect(typeNames.contains("StructTest")) - #expect(typeNames.contains("ClassTest")) - #expect(typeNames.contains("SubclassTest")) - #expect(typeNames.contains("FinalClassTest")) - #expect(typeNames.contains("MultiPayloadEnumTests")) - #expect(typeNames.contains("GenericRequirementTest")) - #expect(typeNames.contains("GenericPackTest")) - #expect(typeNames.contains("GenericValueTest")) - #expect(typeNames.contains("OpaqueReturnTypeTest")) - #expect(typeNames.contains("PropertyWrapperStruct")) - #expect(typeNames.contains("ResultBuilderStruct")) - #expect(typeNames.contains("DynamicMemberLookupStruct")) - #expect(typeNames.contains("DynamicCallableStruct")) - #expect(typeNames.contains("ObjCAttributeClass")) - } - - @Test func typeKindsAreCorrect() async throws { - let indexer = try await preparedIndexer() - - let structTest = try #require(findTypeDefinition(named: "StructTest", in: indexer)) - #expect(structTest.typeName.kind == .struct) - - let classTest = try #require(findTypeDefinition(named: "ClassTest", in: indexer)) - #expect(classTest.typeName.kind == .class) - - let multiPayloadEnumTests = try #require(findTypeDefinition(named: "MultiPayloadEnumTests", in: indexer)) - #expect(multiPayloadEnumTests.typeName.kind == .enum) - } - - @Test func parsedProtocolNamesContainExpectedProtocols() async throws { - let indexer = try await preparedIndexer() - let protocolNames = Set(indexer.allProtocolDefinitions.values.map { $0.protocolName.currentName }) - - #expect(protocolNames.contains("ProtocolTest")) - #expect(protocolNames.contains("ProtocolWitnessTableTest")) - #expect(protocolNames.contains("TestCollection")) - #expect(protocolNames.contains("ProtocolPrimaryAssociatedTypeTest")) - } -} - -// MARK: - Fields and Stored Properties - -extension SymbolTestsCoreIntegrationTests { - @Test func storedPropertyFieldsAreCorrect() async throws { - let indexer = try await preparedIndexer() - let typeDefinition = try #require(findTypeDefinition(named: "GenericStructNonRequirement", in: indexer)) - try await typeDefinition.index(in: machOFile) - - let fieldNames = typeDefinition.fields.map(\.name) - #expect(fieldNames == ["field1", "field2", "field3"]) - } -} -``` - -- [ ] **Step 2: Run tests to verify they pass** - -```bash -swift test --filter SymbolTestsCoreIntegrationTests 2>&1 | tail -20 -``` - -Expected: All tests pass. - -- [ ] **Step 3: Commit** - -```bash -git add Tests/SwiftInterfaceTests/SymbolTestsCoreIntegrationTests.swift -git commit -m "test: add type parsing and fields integration tests" -``` - ---- - -### Task 4: Add Protocol Conformance and Retroactive Tests - -**Files:** -- Modify: `Tests/SwiftInterfaceTests/SymbolTestsCoreIntegrationTests.swift` - -- [ ] **Step 1: Add protocol conformance tests** - -Append to `SymbolTestsCoreIntegrationTests.swift`: - -```swift -// MARK: - Protocol Conformances - -extension SymbolTestsCoreIntegrationTests { - @Test func structTestConformsToExpectedProtocols() async throws { - let indexer = try await preparedIndexer() - let conformancesByType = indexer.protocolConformancesByTypeName - - let structTestConformances = conformancesByType.first { $0.key.name.hasSuffix("StructTest") } - let protocolNames = try #require(structTestConformances?.value.keys.map(\.name)) - - #expect(protocolNames.contains(where: { $0.hasSuffix("ProtocolTest") })) - #expect(protocolNames.contains(where: { $0.hasSuffix("ProtocolWitnessTableTest") })) - } - - @Test func genericRequirementTestConformsToProtocolTest() async throws { - let indexer = try await preparedIndexer() - let conformancesByType = indexer.protocolConformancesByTypeName - - let genericConformances = conformancesByType.first { $0.key.name.hasSuffix("GenericRequirementTest") } - let protocolNames = try #require(genericConformances?.value.keys.map(\.name)) - - #expect(protocolNames.contains(where: { $0.hasSuffix("ProtocolTest") })) - } - - @Test func retroactiveConformanceIsDetected() async throws { - let indexer = try await preparedIndexer() - let conformanceExtensions = indexer.conformanceExtensionDefinitions - - let neverExtensions = conformanceExtensions.filter { $0.key.name == "Swift.Never" } - let hasRetroactive = neverExtensions.values.flatMap { $0 }.contains { $0.isRetroactive } - - #expect(hasRetroactive) - } -} -``` - -- [ ] **Step 2: Run tests** - -```bash -swift test --filter SymbolTestsCoreIntegrationTests 2>&1 | tail -20 -``` - -Expected: All tests pass. - -- [ ] **Step 3: Commit** - -```bash -git add Tests/SwiftInterfaceTests/SymbolTestsCoreIntegrationTests.swift -git commit -m "test: add protocol conformance and retroactive integration tests" -``` - ---- - -### Task 5: Add Class Hierarchy, Nested Types, and Associated Types Tests - -**Files:** -- Modify: `Tests/SwiftInterfaceTests/SymbolTestsCoreIntegrationTests.swift` - -- [ ] **Step 1: Add class hierarchy and override tests** - -Append to `SymbolTestsCoreIntegrationTests.swift`: - -```swift -// MARK: - Class Hierarchy and Override - -extension SymbolTestsCoreIntegrationTests { - @Test func classTestOwnMethodsAreNotOverride() async throws { - let indexer = try await preparedIndexer() - let classTest = try #require(findTypeDefinition(named: "ClassTest", in: indexer)) - try await classTest.index(in: machOFile) - - for function in classTest.functions { - #expect(!function.isOverride, "ClassTest.\(function.name) should not be override") - } - } - - @Test func subclassTestHasOverrideMethods() async throws { - let indexer = try await preparedIndexer() - let subclassTest = try #require(findTypeDefinition(named: "SubclassTest", in: indexer)) - try await subclassTest.index(in: machOFile) - - let instanceMethod = subclassTest.functions.first { $0.name == "instanceMethod" } - #expect(instanceMethod?.isOverride == true) - } - - @Test func finalClassTestHasOverrideMethods() async throws { - let indexer = try await preparedIndexer() - let finalClassTest = try #require(findTypeDefinition(named: "FinalClassTest", in: indexer)) - try await finalClassTest.index(in: machOFile) - - let instanceMethod = finalClassTest.functions.first { $0.name == "instanceMethod" } - #expect(instanceMethod?.isOverride == true) - } -} - -// MARK: - Nested Types - -extension SymbolTestsCoreIntegrationTests { - @Test func genericRequirementTestHasNestedType() async throws { - let indexer = try await preparedIndexer() - let genericRequirementTest = try #require(findTypeDefinition(named: "GenericRequirementTest", in: indexer)) - - let childNames = genericRequirementTest.typeChildren.map { $0.typeName.currentName } - #expect(childNames.contains("RawRepresentableNestedStruct")) - } - - @Test func rawRepresentableNestedStructHasNestedType() async throws { - let indexer = try await preparedIndexer() - let genericRequirementTest = try #require(findTypeDefinition(named: "GenericRequirementTest", in: indexer)) - - let rawRepresentableNested = genericRequirementTest.typeChildren.first { $0.typeName.currentName == "RawRepresentableNestedStruct" } - let nestedChildren = try #require(rawRepresentableNested?.typeChildren.map { $0.typeName.currentName }) - #expect(nestedChildren.contains("NestedStruct")) - } -} - -// MARK: - Associated Types - -extension SymbolTestsCoreIntegrationTests { - @Test func protocolTestHasAssociatedTypeBody() async throws { - let indexer = try await preparedIndexer() - let protocolTest = try #require(findProtocolDefinition(named: "ProtocolTest", in: indexer)) - try await protocolTest.index(in: machOFile) - - #expect(protocolTest.associatedTypes.contains("Body")) - } -} -``` - -- [ ] **Step 2: Run tests** - -```bash -swift test --filter SymbolTestsCoreIntegrationTests 2>&1 | tail -20 -``` - -Expected: All tests pass. - -- [ ] **Step 3: Commit** - -```bash -git add Tests/SwiftInterfaceTests/SymbolTestsCoreIntegrationTests.swift -git commit -m "test: add class hierarchy, nested types, associated types integration tests" -``` - ---- - -### Task 6: Add Type Attribute Inference Integration Tests - -**Files:** -- Modify: `Tests/SwiftInterfaceTests/SymbolTestsCoreIntegrationTests.swift` - -- [ ] **Step 1: Add type attribute inference tests** - -Append to `SymbolTestsCoreIntegrationTests.swift`: - -```swift -// MARK: - Type Attributes (Integration) - -extension SymbolTestsCoreIntegrationTests { - @Test func propertyWrapperStructInfersPropertyWrapperAttribute() async throws { - let indexer = try await preparedIndexer() - let typeDefinition = try #require(findTypeDefinition(named: "PropertyWrapperStruct", in: indexer)) - try await typeDefinition.index(in: machOFile) - - let inferrer = TypeAttributeInferrer(resilienceAwareAttributes: false) - let attributes = inferrer.infer(for: typeDefinition) - - #expect(attributes.contains(.propertyWrapper)) - } - - @Test func resultBuilderStructInfersResultBuilderAttribute() async throws { - let indexer = try await preparedIndexer() - let typeDefinition = try #require(findTypeDefinition(named: "ResultBuilderStruct", in: indexer)) - try await typeDefinition.index(in: machOFile) - - let inferrer = TypeAttributeInferrer(resilienceAwareAttributes: false) - let attributes = inferrer.infer(for: typeDefinition) - - #expect(attributes.contains(.resultBuilder)) - } - - @Test func dynamicMemberLookupStructInfersDynamicMemberLookupAttribute() async throws { - let indexer = try await preparedIndexer() - let typeDefinition = try #require(findTypeDefinition(named: "DynamicMemberLookupStruct", in: indexer)) - try await typeDefinition.index(in: machOFile) - - let inferrer = TypeAttributeInferrer(resilienceAwareAttributes: false) - let attributes = inferrer.infer(for: typeDefinition) - - #expect(attributes.contains(.dynamicMemberLookup)) - } - - @Test func dynamicCallableStructInfersDynamicCallableAttribute() async throws { - let indexer = try await preparedIndexer() - let typeDefinition = try #require(findTypeDefinition(named: "DynamicCallableStruct", in: indexer)) - try await typeDefinition.index(in: machOFile) - - let inferrer = TypeAttributeInferrer(resilienceAwareAttributes: false) - let attributes = inferrer.infer(for: typeDefinition) - - #expect(attributes.contains(.dynamicCallable)) - } - - @Test func structTestDoesNotInferAnyTypeAttribute() async throws { - let indexer = try await preparedIndexer() - let typeDefinition = try #require(findTypeDefinition(named: "StructTest", in: indexer)) - try await typeDefinition.index(in: machOFile) - - let inferrer = TypeAttributeInferrer(resilienceAwareAttributes: false) - let attributes = inferrer.infer(for: typeDefinition) - - #expect(!attributes.contains(.propertyWrapper)) - #expect(!attributes.contains(.resultBuilder)) - #expect(!attributes.contains(.dynamicMemberLookup)) - #expect(!attributes.contains(.dynamicCallable)) - } -} -``` - -- [ ] **Step 2: Run tests** - -```bash -swift test --filter SymbolTestsCoreIntegrationTests 2>&1 | tail -20 -``` - -Expected: All tests pass. - -- [ ] **Step 3: Commit** - -```bash -git add Tests/SwiftInterfaceTests/SymbolTestsCoreIntegrationTests.swift -git commit -m "test: add type attribute inference integration tests" -``` - ---- - -### Task 7: Add Member Attribute Integration Tests - -**Files:** -- Modify: `Tests/SwiftInterfaceTests/SymbolTestsCoreIntegrationTests.swift` - -- [ ] **Step 1: Add member attribute tests** - -Append to `SymbolTestsCoreIntegrationTests.swift`: - -```swift -// MARK: - Member Attributes (Integration) - -extension SymbolTestsCoreIntegrationTests { - @Test func classTestDynamicMembersHaveDynamicAttribute() async throws { - let indexer = try await preparedIndexer() - let classTest = try #require(findTypeDefinition(named: "ClassTest", in: indexer)) - try await classTest.index(in: machOFile) - - let dynamicVariable = classTest.variables.first { $0.name == "dynamicVariable" } - #expect(dynamicVariable?.attributes.contains(.dynamic) == true) - - let dynamicMethod = classTest.functions.first { $0.name == "dynamicMethod" } - #expect(dynamicMethod?.attributes.contains(.dynamic) == true) - } - - @Test func objcAttributeClassMemberAttributes() async throws { - let indexer = try await preparedIndexer() - let objcClass = try #require(findTypeDefinition(named: "ObjCAttributeClass", in: indexer)) - try await objcClass.index(in: machOFile) - - let objcMethod = objcClass.functions.first { $0.name == "objcMethod" } - #expect(objcMethod?.attributes.contains(.objc) == true) - - let nonobjcMethod = objcClass.functions.first { $0.name == "nonobjcMethod" } - #expect(nonobjcMethod?.attributes.contains(.nonobjc) == true) - - let objcDynamicMethod = objcClass.functions.first { $0.name == "objcDynamicMethod" } - #expect(objcDynamicMethod?.attributes.contains(.objc) == true) - #expect(objcDynamicMethod?.attributes.contains(.dynamic) == true) - } -} -``` - -- [ ] **Step 2: Run tests** - -```bash -swift test --filter SymbolTestsCoreIntegrationTests 2>&1 | tail -20 -``` - -Expected: All tests pass. - -- [ ] **Step 3: Commit** - -```bash -git add Tests/SwiftInterfaceTests/SymbolTestsCoreIntegrationTests.swift -git commit -m "test: add member attribute integration tests" -``` - ---- - -### Task 8: Add VTable Offset and Member Ordering Tests - -**Files:** -- Modify: `Tests/SwiftInterfaceTests/SymbolTestsCoreIntegrationTests.swift` - -- [ ] **Step 1: Add vtable offset and PWT ordering tests** - -Append to `SymbolTestsCoreIntegrationTests.swift`: - -```swift -// MARK: - VTable Offset and Member Ordering - -extension SymbolTestsCoreIntegrationTests { - @Test func classTestVTableMembersAreSortedByVTableOffset() async throws { - let indexer = try await preparedIndexer() - let classTest = try #require(findTypeDefinition(named: "ClassTest", in: indexer)) - try await classTest.index(in: machOFile) - - let orderedMembers = OrderedMember.classOrdered(OrderedMember.allMembers(from: classTest)) - let vtableOffsets = orderedMembers.compactMap(\.minVTableOffset) - - // vtable offsets should be in ascending order - #expect(!vtableOffsets.isEmpty) - for index in 1.. \(vtableOffsets[index])") - } - - // All vtable members should come before non-vtable members - let hasVTable = orderedMembers.map { $0.minVTableOffset != nil } - let lastVTableIndex = hasVTable.lastIndex(of: true) ?? -1 - let firstNonVTableIndex = hasVTable.firstIndex(of: false) ?? orderedMembers.count - #expect(lastVTableIndex < firstNonVTableIndex) - } - - @Test func subclassTestOverrideMembersHaveVTableOffset() async throws { - let indexer = try await preparedIndexer() - let subclassTest = try #require(findTypeDefinition(named: "SubclassTest", in: indexer)) - try await subclassTest.index(in: machOFile) - - let overrideMethods = subclassTest.functions.filter { $0.isOverride } - #expect(!overrideMethods.isEmpty) - - for method in overrideMethods { - #expect(method.vtableOffset != nil, "Override method \(method.name) should have vtable offset") - } - } -} - -// MARK: - PWT Offset Ordering - -extension SymbolTestsCoreIntegrationTests { - @Test func protocolWitnessTableTestMembersAreSortedByPWTOffset() async throws { - let indexer = try await preparedIndexer() - let protocolDefinition = try #require(findProtocolDefinition(named: "ProtocolWitnessTableTest", in: indexer)) - try await protocolDefinition.index(in: machOFile) - - let pwtOffsets = protocolDefinition.orderedMembers.compactMap(\.pwtOffset) - - #expect(!pwtOffsets.isEmpty) - for index in 1.. \(pwtOffsets[index])") - } - } -} -``` - -- [ ] **Step 2: Run tests** - -```bash -swift test --filter SymbolTestsCoreIntegrationTests 2>&1 | tail -20 -``` - -Expected: All tests pass. - -- [ ] **Step 3: Commit** - -```bash -git add Tests/SwiftInterfaceTests/SymbolTestsCoreIntegrationTests.swift -git commit -m "test: add vtable offset and PWT ordering integration tests" -``` - ---- - -### Task 9: Write End-to-End Tests - -**Files:** -- Create: `Tests/SwiftInterfaceTests/SymbolTestsCoreE2ETests.swift` - -- [ ] **Step 1: Create E2E test file** - -Create `Tests/SwiftInterfaceTests/SymbolTestsCoreE2ETests.swift`: - -```swift -import Foundation -import Testing -import MachOKit -import Dependencies -@_spi(Internals) import MachOSymbols -@_spi(Internals) import MachOCaches -@_spi(Support) @testable import SwiftInterface -@testable import MachOSwiftSection -@testable import MachOTestingSupport -@testable import SwiftDump - -@Suite(.serialized) -final class SymbolTestsCoreE2ETests: MachOFileTests, @unchecked Sendable { - override class var fileName: MachOFileName { .SymbolTestsCore } - - private func buildOutput(memberSortOrder: SwiftInterfaceMemberSortOrder = .byOffset) async throws -> String { - let configuration = SwiftInterfaceBuilderConfiguration( - indexConfiguration: .init(showCImportedTypes: false), - printConfiguration: .init( - printStrippedSymbolicItem: true, - printFieldOffset: true, - printMemberAddress: false, - printVTableOffset: true, - printPWTOffset: true, - memberSortOrder: memberSortOrder, - printTypeLayout: false, - printEnumLayout: false, - resilienceAwareAttributes: false - ) - ) - let builder = try SwiftInterfaceBuilder(configuration: configuration, eventHandlers: [], in: machOFile) - try await builder.prepare() - let result = try await builder.printRoot() - return result.string - } -} - -// MARK: - E2E: Type Attributes in Output - -extension SymbolTestsCoreE2ETests { - @Test func outputContainsPropertyWrapperAttribute() async throws { - let output = try await buildOutput() - #expect(output.contains("@propertyWrapper")) - } - - @Test func outputContainsResultBuilderAttribute() async throws { - let output = try await buildOutput() - #expect(output.contains("@resultBuilder")) - } - - @Test func outputContainsDynamicMemberLookupAttribute() async throws { - let output = try await buildOutput() - #expect(output.contains("@dynamicMemberLookup")) - } - - @Test func outputContainsDynamicCallableAttribute() async throws { - let output = try await buildOutput() - #expect(output.contains("@dynamicCallable")) - } -} - -// MARK: - E2E: Member Attributes in Output - -extension SymbolTestsCoreE2ETests { - @Test func outputContainsObjcAttribute() async throws { - let output = try await buildOutput() - #expect(output.contains("@objc")) - } - - @Test func outputContainsDynamicKeyword() async throws { - let output = try await buildOutput() - #expect(output.contains("dynamic")) - } -} - -// MARK: - E2E: VTable Offset in Output - -extension SymbolTestsCoreE2ETests { - @Test func outputContainsVTableOffsetComments() async throws { - let output = try await buildOutput(memberSortOrder: .byOffset) - #expect(output.contains("vtable offset")) - } -} - -// MARK: - E2E: Structure Completeness - -extension SymbolTestsCoreE2ETests { - @Test func outputContainsAllExpectedTypeDeclarations() async throws { - let output = try await buildOutput() - - #expect(output.contains("struct StructTest")) - #expect(output.contains("class ClassTest")) - #expect(output.contains("class SubclassTest")) - #expect(output.contains("class FinalClassTest")) - #expect(output.contains("enum MultiPayloadEnumTests")) - #expect(output.contains("protocol ProtocolTest")) - #expect(output.contains("protocol ProtocolWitnessTableTest")) - #expect(output.contains("struct GenericRequirementTest")) - #expect(output.contains("struct PropertyWrapperStruct")) - #expect(output.contains("struct ResultBuilderStruct")) - #expect(output.contains("struct DynamicMemberLookupStruct")) - #expect(output.contains("struct DynamicCallableStruct")) - #expect(output.contains("class ObjCAttributeClass")) - } - - @Test func outputContainsOverrideKeyword() async throws { - let output = try await buildOutput() - #expect(output.contains("override")) - } - - @Test func outputContainsRetroactiveAnnotation() async throws { - let output = try await buildOutput() - #expect(output.contains("@retroactive")) - } - - @Test func outputContainsConditionalConformanceWhereClause() async throws { - let output = try await buildOutput() - // GenericRequirementTest: Equatable where T: Equatable - #expect(output.contains("where")) - } -} -``` - -- [ ] **Step 2: Run tests** - -```bash -swift test --filter SymbolTestsCoreE2ETests 2>&1 | tail -20 -``` - -Expected: All tests pass. - -- [ ] **Step 3: Commit** - -```bash -git add Tests/SwiftInterfaceTests/SymbolTestsCoreE2ETests.swift -git commit -m "test: add end-to-end integration tests for SwiftInterfaceBuilder output" -``` - ---- - -### Task 10: Final Verification - -- [ ] **Step 1: Run all SwiftInterfaceTests** - -```bash -swift test --filter SwiftInterfaceTests 2>&1 | tail -30 -``` - -Expected: All tests pass, including existing tests that were not modified. - -- [ ] **Step 2: Run full test suite to check for regressions** - -```bash -swift test 2>&1 | tail -30 -``` - -Expected: No regressions in any test target. - -- [ ] **Step 3: Commit plan doc** - -```bash -git add docs/superpowers/plans/2026-04-10-symboltestscore-integration-tests.md -git commit -m "docs: add SymbolTestsCore integration tests implementation plan" -``` diff --git a/docs/superpowers/plans/2026-04-13-symboltestscore-fixture-expansion.md b/docs/superpowers/plans/2026-04-13-symboltestscore-fixture-expansion.md deleted file mode 100644 index 9054ea6d..00000000 --- a/docs/superpowers/plans/2026-04-13-symboltestscore-fixture-expansion.md +++ /dev/null @@ -1,2875 +0,0 @@ -# SymbolTestsCore Fixture Expansion Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Expand the `SymbolTestsCore` test fixture with 44 new Swift source files and 4 edits to existing files so the MachOSwiftSection / SwiftDump / SwiftInterface parsing pipeline gets broader coverage of Swift mangling and `__swift5_*` metadata shapes. - -**Architecture:** Each new file is a self-contained `public enum Feature { ... }` namespace containing `public` nested declarations that emit distinctive type context descriptors and mangled symbols. There are no modifications to parsing code or new test assertions — the deliverable is the fixture itself. Files are grouped into 19 tasks by theme: each task writes 2–4 files, rebuilds `SymbolTestsCore.framework` via `xcodebuild`, and commits. Task 19 regenerates the single snapshot baseline that depends on the fixture. - -**Tech Stack:** Swift 6.2, Xcode 26, `xcodebuild`, `xcsift` (build output formatter), `swift-snapshot-testing`. - ---- - -## File Structure - -**New files (44)** in `Tests/Projects/SymbolTests/SymbolTestsCore/`: - -Category 1 (general features, 24 files): `KeyPaths.swift`, `Typealiases.swift`, `Extensions.swift`, `DefaultArguments.swift`, `PropertyObservers.swift`, `Initializers.swift`, `Codable.swift`, `AccessLevels.swift`, `Availability.swift`, `DistributedActors.swift`, `StringInterpolation.swift`, `NestedGenerics.swift`, `Tuples.swift`, `FunctionTypes.swift`, `NestedFunctions.swift`, `MetatypeUsage.swift`, `ExistentialAny.swift`, `SameTypeRequirements.swift`, `OptionSetAndRawRepresentable.swift`, `DiamondInheritance.swift`, `WeakUnownedReferences.swift`, `ErrorTypes.swift`, `ResultBuilderDSL.swift`, `RethrowingFunctions.swift`. - -Category 2 (extended features, 12 files): `ProtocolComposition.swift`, `OverloadedMembers.swift`, `UnsafePointers.swift`, `AsyncSequence.swift`, `PropertyWrapperVariants.swift`, `CustomLiterals.swift`, `StaticMembers.swift`, `ClassBoundGenerics.swift`, `MarkerProtocols.swift`, `DependentTypeAccess.swift`, `DeinitVariants.swift`, `CollectionConformances.swift`. - -Category 3 (binary metadata variants, 8 files): `FieldDescriptorVariants.swift`, `GenericRequirementVariants.swift`, `VTableEntryVariants.swift`, `ConditionalConformanceVariants.swift`, `DefaultImplementationVariants.swift`, `FrozenResilienceContrast.swift`, `AssociatedTypeWitnessPatterns.swift`, `BuiltinTypeFields.swift`. - -**Edits to existing files (4):** `Classes.swift`, `Enums.swift`, `FunctionFeatures.swift`, `Protocols.swift`. - -**Snapshot regeneration:** `Tests/SwiftInterfaceTests/Snapshots/__Snapshots__/MachOFileInterfaceSnapshotTests/interfaceSnapshot.1.txt` will be regenerated in Task 19 because its content is derived from the fixture. - -**No pbxproj changes** — `SymbolTestsCore` target uses `PBXFileSystemSynchronizedRootGroup`, so any `.swift` file in the folder is automatically compiled. - ---- - -## Build Verification Command - -Every task ends with this command to verify the fixture still compiles: - -```bash -xcodebuild \ - -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj \ - -scheme SymbolTests \ - -configuration Release \ - -derivedDataPath Tests/Projects/SymbolTests/DerivedData \ - build 2>&1 | xcsift --quiet -``` - -**Expected:** exits with code 0. xcsift prints a short summary; no error output. If a file fails to compile, fix the offending source inline before committing. Do not commit a broken state. - ---- - -## Task 1: KeyPaths, Typealiases, Extensions - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/KeyPaths.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/Typealiases.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/Extensions.swift` - -- [ ] **Step 1: Write `KeyPaths.swift`** - -```swift -import Foundation - -public enum KeyPaths { - public struct KeyPathHolderTest { - public var readOnlyKeyPath: KeyPath - public var writableKeyPath: WritableKeyPath - public var referenceWritableKeyPath: ReferenceWritableKeyPath - public var partialKeyPath: PartialKeyPath - public var anyKeyPath: AnyKeyPath - public var value: Int - public var text: String - - public init( - readOnlyKeyPath: KeyPath, - writableKeyPath: WritableKeyPath, - referenceWritableKeyPath: ReferenceWritableKeyPath, - partialKeyPath: PartialKeyPath, - anyKeyPath: AnyKeyPath, - value: Int, - text: String - ) { - self.readOnlyKeyPath = readOnlyKeyPath - self.writableKeyPath = writableKeyPath - self.referenceWritableKeyPath = referenceWritableKeyPath - self.partialKeyPath = partialKeyPath - self.anyKeyPath = anyKeyPath - self.value = value - self.text = text - } - } - - public class KeyPathReferenceTest { - public var mutableText: String = "" - public var mutableInteger: Int = 0 - public init() {} - } - - public struct KeyPathFactoryTest { - public var keyPathProducer: (Root) -> KeyPath - - public init(keyPathProducer: @escaping (Root) -> KeyPath) { - self.keyPathProducer = keyPathProducer - } - } -} -``` - -- [ ] **Step 2: Write `Typealiases.swift`** - -```swift -import Foundation - -public enum Typealiases { - public typealias IntegerAlias = Int - public typealias CompletionHandler = (Int, Error?) -> Void - public typealias ResultHandler = (Result) -> Void - public typealias EquatablePair = (left: Element, right: Element) - - public struct TypealiasContainerTest { - public typealias NestedAlias = Element - public typealias NestedCollection = Array - public typealias NestedHandler = (Element) -> Void - - public var element: NestedAlias - public var collection: NestedCollection - public var handler: NestedHandler - - public init(element: NestedAlias, collection: NestedCollection, handler: @escaping NestedHandler) { - self.element = element - self.collection = collection - self.handler = handler - } - } - - public struct ConstrainedTypealiasTest where Element: Comparable { - public typealias ConstrainedRange = ClosedRange - public var range: ConstrainedRange - - public init(range: ConstrainedRange) { - self.range = range - } - } -} -``` - -- [ ] **Step 3: Write `Extensions.swift`** - -```swift -import Foundation - -public enum Extensions { - public struct ExtensionBaseStruct { - public var element: Element - public init(element: Element) { - self.element = element - } - } - - public struct ExtensionConstrainedStruct { - public var element: Element - public init(element: Element) { - self.element = element - } - } - - public protocol ExtensionProtocol { - associatedtype Item - var item: Item { get } - } -} - -extension Extensions.ExtensionBaseStruct where Element: Equatable { - public func isEqualTo(_ other: Self) -> Bool { - element == other.element - } -} - -extension Extensions.ExtensionBaseStruct where Element: Comparable { - public func isLessThan(_ other: Self) -> Bool { - element < other.element - } -} - -extension Extensions.ExtensionBaseStruct where Element: Hashable & Sendable { - public func computeHash() -> Int { - element.hashValue - } -} - -extension Extensions.ExtensionConstrainedStruct: Extensions.ExtensionProtocol where Element: Hashable { - public var item: Element { element } -} - -extension Extensions.ExtensionProtocol where Item: Equatable { - public func matches(_ other: Item) -> Bool { - item == other - } -} - -extension Extensions.ExtensionProtocol where Item: Comparable { - public func isLessThan(_ other: Item) -> Bool { - item < other - } -} -``` - -- [ ] **Step 4: Build fixture to verify** - -Run the Build Verification Command from the top of this plan. -Expected: xcodebuild exits 0 with no errors reported by xcsift. - -- [ ] **Step 5: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/KeyPaths.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/Typealiases.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/Extensions.swift -git commit -m "test(fixture): add KeyPaths, Typealiases, Extensions" -``` - ---- - -## Task 2: DefaultArguments, PropertyObservers - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/DefaultArguments.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/PropertyObservers.swift` - -- [ ] **Step 1: Write `DefaultArguments.swift`** - -```swift -import Foundation - -public enum DefaultArguments { - public struct DefaultArgumentMethodTest { - public func greet(name: String = "World", repeated: Int = 1, punctuation: Character = "!") -> String { - String(repeating: "\(name)\(punctuation) ", count: repeated) - } - - public func append(_ value: Int, to collection: [Int] = []) -> [Int] { - collection + [value] - } - - public static func createDefault(label: String = "default", value: Int = 0) -> DefaultArgumentMethodTest { - DefaultArgumentMethodTest() - } - } - - public struct DefaultArgumentInitializerTest { - public var name: String - public var count: Int - public var enabled: Bool - - public init(name: String = "default", count: Int = 0, enabled: Bool = true) { - self.name = name - self.count = count - self.enabled = enabled - } - } - - public struct DefaultArgumentSubscriptTest { - public subscript(index: Int = 0, fallback fallback: String = "") -> String { - fallback - } - } - - public class DefaultArgumentClassTest { - public func process(value: Int = 42, multiplier: Double = 1.0) -> Double { - Double(value) * multiplier - } - - public init(initial: Int = 0, scale: Double = 1.0) {} - } -} -``` - -- [ ] **Step 2: Write `PropertyObservers.swift`** - -```swift -import Foundation - -public enum PropertyObservers { - public class PropertyObserverClassTest { - public var observedValue: Int = 0 { - willSet { - print("willSet: \(newValue)") - } - didSet { - print("didSet: \(oldValue)") - } - } - - public var observedName: String = "" { - willSet(newName) { - _ = newName - } - didSet(oldName) { - _ = oldName - } - } - - public var computedBacking: Int { - get { observedValue } - set { observedValue = newValue } - } - - public init() {} - } - - public struct PropertyObserverStructTest { - public var observedField: Double = 0.0 { - willSet { - _ = newValue - } - didSet { - _ = oldValue - } - } - - public init() {} - } -} -``` - -- [ ] **Step 3: Build fixture** - -Run the Build Verification Command. -Expected: build succeeds. - -- [ ] **Step 4: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/DefaultArguments.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/PropertyObservers.swift -git commit -m "test(fixture): add DefaultArguments and PropertyObservers" -``` - ---- - -## Task 3: Initializers, Codable - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/Initializers.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/Codable.swift` - -- [ ] **Step 1: Write `Initializers.swift`** - -```swift -import Foundation - -public enum Initializers { - public struct CustomInitializerError: Error { - public let reason: String - public init(reason: String) { - self.reason = reason - } - } - - public class ConvenienceInitializerTest { - public let primaryValue: Int - public let secondaryValue: String - - public init(primaryValue: Int, secondaryValue: String) { - self.primaryValue = primaryValue - self.secondaryValue = secondaryValue - } - - public convenience init(primaryValue: Int) { - self.init(primaryValue: primaryValue, secondaryValue: "") - } - - public convenience init() { - self.init(primaryValue: 0, secondaryValue: "") - } - } - - public class RequiredInitializerTest { - public let value: Int - - public required init(value: Int) { - self.value = value - } - - public required convenience init() { - self.init(value: 0) - } - } - - public class RequiredInitializerSubclass: RequiredInitializerTest { - public let extraValue: String - - public required init(value: Int) { - self.extraValue = "" - super.init(value: value) - } - - public required convenience init() { - self.init(value: 0) - } - } - - public struct FailableInitializerTest { - public let value: Int - - public init?(value: Int) { - guard value >= 0 else { return nil } - self.value = value - } - - public init!(unsafe value: Int) { - self.value = value - } - } - - public struct TypedThrowingInitializerTest { - public let value: Int - - public init(value: Int) throws(CustomInitializerError) { - guard value >= 0 else { - throw CustomInitializerError(reason: "negative") - } - self.value = value - } - } - - public actor AsyncInitializerActorTest { - public let identifier: Int - - public init(identifier: Int) async { - self.identifier = identifier - } - } -} -``` - -- [ ] **Step 2: Write `Codable.swift`** - -```swift -import Foundation - -public enum CodableTests { - public struct SynthesizedCodableTest: Codable { - public var identifier: Int - public var name: String - public var optionalValue: Double? - - public init(identifier: Int, name: String, optionalValue: Double?) { - self.identifier = identifier - self.name = name - self.optionalValue = optionalValue - } - } - - public struct CustomCodableTest: Codable { - public var displayName: String - public var hiddenCount: Int - - private enum CodingKeys: String, CodingKey { - case displayName = "display_name" - case hiddenCount = "count" - } - - public init(displayName: String, hiddenCount: Int) { - self.displayName = displayName - self.hiddenCount = hiddenCount - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.displayName = try container.decode(String.self, forKey: .displayName) - self.hiddenCount = try container.decode(Int.self, forKey: .hiddenCount) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(displayName, forKey: .displayName) - try container.encode(hiddenCount, forKey: .hiddenCount) - } - } - - public class CodableClassTest: Codable { - public var identifier: Int - public var label: String - - public init(identifier: Int, label: String) { - self.identifier = identifier - self.label = label - } - } - - public enum CodableEnumTest: Codable { - case empty - case withValue(Int) - case withPair(left: String, right: Int) - } - - public struct GenericCodableTest: Codable { - public var element: Element - public var metadata: [String: String] - - public init(element: Element, metadata: [String: String]) { - self.element = element - self.metadata = metadata - } - } -} -``` - -- [ ] **Step 3: Build fixture** - -Run the Build Verification Command. Expected: build succeeds. - -- [ ] **Step 4: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/Initializers.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/Codable.swift -git commit -m "test(fixture): add Initializers and Codable" -``` - ---- - -## Task 4: AccessLevels, Availability - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/AccessLevels.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/Availability.swift` - -- [ ] **Step 1: Write `AccessLevels.swift`** - -```swift -import Foundation - -public enum AccessLevels { - public struct PublicAccessLevelTest { - public var publicField: Int - package var packageField: Int - internal var internalField: Int - fileprivate var fileprivateField: Int - private var privateField: Int - - public init(publicField: Int, packageField: Int, internalField: Int, fileprivateField: Int, privateField: Int) { - self.publicField = publicField - self.packageField = packageField - self.internalField = internalField - self.fileprivateField = fileprivateField - self.privateField = privateField - } - - public func publicMethod() {} - package func packageMethod() {} - internal func internalMethod() {} - fileprivate func fileprivateMethod() {} - private func privateMethod() {} - } - - open class OpenAccessLevelTest { - open var openField: Int = 0 - public var publicField: Int = 0 - - open func openMethod() {} - public func publicMethod() {} - - public init() {} - } - - public class SubclassOfOpenAccessLevel: OpenAccessLevelTest { - open override func openMethod() {} - public override var openField: Int { - get { 0 } - set {} - } - } -} -``` - -- [ ] **Step 2: Write `Availability.swift`** - -```swift -import Foundation - -public enum Availability { - @available(macOS 12.0, iOS 15.0, *) - public struct MultiPlatformAvailableTest { - public var value: Int - public init(value: Int) { - self.value = value - } - } - - @available(macOS, deprecated: 13.0, message: "Use RenamedAvailabilityNewTest instead") - public struct DeprecatedAvailabilityTest { - public var value: Int - public init(value: Int) { - self.value = value - } - } - - public struct RenamedAvailabilityNewTest { - public init() {} - } - - @available(macOS, introduced: 10.15, deprecated: 14.0, obsoleted: 15.0, message: "Obsoleted in macOS 15") - public struct ObsoletedAvailabilityTest { - public init() {} - } - - public struct AvailabilityMemberTest { - @available(macOS 13.0, *) - public var modernField: Int { - 0 - } - - @available(macOS, deprecated: 13.0) - public func deprecatedMethod() {} - - @available(*, unavailable, message: "No longer supported") - public func unavailableMethod() {} - - public init() {} - } -} -``` - -- [ ] **Step 3: Build fixture** - -Run the Build Verification Command. Expected: build succeeds. - -- [ ] **Step 4: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/AccessLevels.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/Availability.swift -git commit -m "test(fixture): add AccessLevels and Availability" -``` - ---- - -## Task 5: DistributedActors, StringInterpolation - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/DistributedActors.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/StringInterpolation.swift` - -- [ ] **Step 1: Write `DistributedActors.swift`** - -```swift -import Foundation -import Distributed - -public enum DistributedActors { - public distributed actor DistributedActorTest { - public typealias ActorSystem = LocalTestingDistributedActorSystem - - public distributed func remoteMethod(value: Int) -> Int { - value * 2 - } - - public distributed func remoteThrowingMethod() throws -> String { - "result" - } - - public nonisolated var nonisolatedProperty: String { - "nonisolated" - } - - public distributed func parameterizedMethod(label: String, count: Int) -> String { - String(repeating: label, count: count) - } - } - - public distributed actor GenericDistributedActorTest { - public typealias ActorSystem = LocalTestingDistributedActorSystem - - public distributed func process(element: Element) -> Element { - element - } - } -} -``` - -- [ ] **Step 2: Write `StringInterpolation.swift`** - -```swift -import Foundation - -public enum StringInterpolations { - public struct CustomStringInterpolationTest: ExpressibleByStringLiteral, ExpressibleByStringInterpolation { - public var storage: String - - public init(stringLiteral value: String) { - self.storage = value - } - - public init(stringInterpolation: StringInterpolation) { - self.storage = stringInterpolation.accumulator - } - - public struct StringInterpolation: StringInterpolationProtocol { - public var accumulator: String - - public init(literalCapacity: Int, interpolationCount: Int) { - self.accumulator = "" - self.accumulator.reserveCapacity(literalCapacity + interpolationCount) - } - - public mutating func appendLiteral(_ literal: String) { - accumulator.append(literal) - } - - public mutating func appendInterpolation(_ value: Int) { - accumulator.append(String(value)) - } - - public mutating func appendInterpolation(_ value: String) { - accumulator.append(value) - } - - public mutating func appendInterpolation(_ value: Value) { - accumulator.append(value.description) - } - - public mutating func appendInterpolation(formatted value: Double, precision: Int) { - accumulator.append(String(format: "%.\(precision)f", value)) - } - } - } -} -``` - -- [ ] **Step 3: Build fixture** - -Run the Build Verification Command. -Expected: build succeeds. If `LocalTestingDistributedActorSystem` is unavailable, replace with a minimal custom `DistributedActorSystem` conformance (follow compiler diagnostics). - -- [ ] **Step 4: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/DistributedActors.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/StringInterpolation.swift -git commit -m "test(fixture): add DistributedActors and StringInterpolation" -``` - ---- - -## Task 6: NestedGenerics, Tuples, FunctionTypes - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/NestedGenerics.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/Tuples.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/FunctionTypes.swift` - -- [ ] **Step 1: Write `NestedGenerics.swift`** - -```swift -import Foundation - -public enum NestedGenerics { - public struct OuterGenericTest { - public struct InnerGenericTest { - public struct InnerMostGenericTest { - public var outer: Outer - public var inner: Inner - public var innerMost: InnerMost - - public init(outer: Outer, inner: Inner, innerMost: InnerMost) { - self.outer = outer - self.inner = inner - self.innerMost = innerMost - } - } - } - } - - public struct NestedGenericWithConstraintsTest { - public struct InnerConstrainedTest where Outer: Sendable { - public var outer: Outer - public var inner: Inner - - public init(outer: Outer, inner: Inner) { - self.outer = outer - self.inner = inner - } - } - } - - public struct NestedTypealiasGenericTest { - public typealias ElementArray = [Element] - public typealias ElementDictionary = [Key: Element] - - public var elements: ElementArray - - public init(elements: ElementArray) { - self.elements = elements - } - } -} -``` - -- [ ] **Step 2: Write `Tuples.swift`** - -```swift -import Foundation - -public enum Tuples { - public struct TupleFieldTest { - public var namedTuple: (first: Int, second: String) - public var unnamedTuple: (Int, Double, Bool) - public var nestedTuple: ((Int, Int), (String, String)) - - public init( - namedTuple: (first: Int, second: String), - unnamedTuple: (Int, Double, Bool), - nestedTuple: ((Int, Int), (String, String)) - ) { - self.namedTuple = namedTuple - self.unnamedTuple = unnamedTuple - self.nestedTuple = nestedTuple - } - } - - public struct TupleFunctionTest { - public func acceptTuple(_ value: (Int, String)) -> (Bool, Double) { - (true, 0.0) - } - - public func acceptNamedTuple(_ value: (identifier: Int, label: String)) -> (result: Bool, score: Double) { - (result: true, score: 0.0) - } - - public func returnLargeTuple() -> (Int, Double, String, Bool, Int, Double) { - (0, 0.0, "", true, 0, 0.0) - } - } - - public struct GenericTupleTest { - public var pair: (First, Second) - public var labeled: (left: First, right: Second) - - public init(pair: (First, Second), labeled: (left: First, right: Second)) { - self.pair = pair - self.labeled = labeled - } - } -} -``` - -- [ ] **Step 3: Write `FunctionTypes.swift`** - -```swift -import Foundation - -public enum FunctionTypes { - public struct FunctionFieldTest { - public var simpleFunction: (Int) -> Int - public var multiArgumentFunction: (Int, String, Bool) -> Double - public var throwingFunction: () throws -> Int - public var asyncFunction: () async -> String - public var asyncThrowingFunction: () async throws -> Int - - public init( - simpleFunction: @escaping (Int) -> Int, - multiArgumentFunction: @escaping (Int, String, Bool) -> Double, - throwingFunction: @escaping () throws -> Int, - asyncFunction: @escaping () async -> String, - asyncThrowingFunction: @escaping () async throws -> Int - ) { - self.simpleFunction = simpleFunction - self.multiArgumentFunction = multiArgumentFunction - self.throwingFunction = throwingFunction - self.asyncFunction = asyncFunction - self.asyncThrowingFunction = asyncThrowingFunction - } - } - - public struct HigherOrderFunctionTest { - public func acceptFunctionReturningFunction(_ producer: @escaping (Int) -> (Double) -> String) -> String { - producer(0)(0.0) - } - - public func returnFunctionReturningFunction() -> (Int) -> (Double) -> String { - { _ in { _ in "" } } - } - - public func curriedFunction(_ firstArgument: Int) -> (Double) -> (String) -> Bool { - { _ in { _ in false } } - } - } - - public struct FunctionTypealiasTest { - public typealias Transformer = (Input) -> Output - public typealias Predicate = (Value) -> Bool - public typealias BiFunction = (First, Second) -> Result - - public var transformer: Transformer - public var predicate: Predicate - public var biFunction: BiFunction - - public init( - transformer: @escaping Transformer, - predicate: @escaping Predicate, - biFunction: @escaping BiFunction - ) { - self.transformer = transformer - self.predicate = predicate - self.biFunction = biFunction - } - } -} -``` - -- [ ] **Step 4: Build fixture** - -Run the Build Verification Command. Expected: build succeeds. - -- [ ] **Step 5: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/NestedGenerics.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/Tuples.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/FunctionTypes.swift -git commit -m "test(fixture): add NestedGenerics, Tuples, FunctionTypes" -``` - ---- - -## Task 7: NestedFunctions, MetatypeUsage, ExistentialAny - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/NestedFunctions.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/MetatypeUsage.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/ExistentialAny.swift` - -- [ ] **Step 1: Write `NestedFunctions.swift`** - -```swift -import Foundation - -public enum NestedFunctions { - public struct NestedFunctionHolderTest { - public func outerFunction(parameter: Int) -> Int { - func innerFunction(inner: Int) -> Int { - inner * 2 - } - - func secondInnerFunction(first: Int, second: Int) -> Int { - first + second - } - - return innerFunction(inner: parameter) + secondInnerFunction(first: parameter, second: parameter) - } - - public func outerGenericFunction(element: Element) -> [Element] { - func innerGenericFunction(item: Item) -> [Item] { - [item, item] - } - - return innerGenericFunction(item: element) - } - - public func outerWithLocalType() -> Int { - struct LocalStruct { - var value: Int - } - - let local = LocalStruct(value: 42) - return local.value - } - - public func outerWithLocalClass() -> String { - class LocalClass { - var label: String = "" - } - - let instance = LocalClass() - return instance.label - } - } -} -``` - -- [ ] **Step 2: Write `MetatypeUsage.swift`** - -```swift -import Foundation - -public enum MetatypeUsage { - public struct MetatypeFieldTest { - public var concreteMetatype: Int.Type - public var anyMetatype: Any.Type - public var protocolMetatype: any Protocols.ProtocolTest.Type - public var anyObjectMetatype: AnyObject.Type - - public init( - concreteMetatype: Int.Type, - anyMetatype: Any.Type, - protocolMetatype: any Protocols.ProtocolTest.Type, - anyObjectMetatype: AnyObject.Type - ) { - self.concreteMetatype = concreteMetatype - self.anyMetatype = anyMetatype - self.protocolMetatype = protocolMetatype - self.anyObjectMetatype = anyObjectMetatype - } - } - - public struct MetatypeFunctionTest { - public func acceptMetatype(_ type: Element.Type) -> Element.Type { - type - } - - public func acceptProtocolMetatype(_ type: any Protocols.ProtocolTest.Type) -> String { - String(describing: type) - } - - public func returnMetatype() -> Self.Type { - Self.self - } - - public func dynamicType(of value: Element) -> Element.Type { - type(of: value) - } - } -} -``` - -- [ ] **Step 3: Write `ExistentialAny.swift`** - -```swift -import Foundation - -public enum ExistentialAny { - public struct ExistentialFieldTest { - public var simpleExistential: any Protocols.ProtocolTest - public var compositionExistential: any Protocols.ProtocolTest & Sendable - public var optionalExistential: (any Protocols.ProtocolTest)? - public var existentialArray: [any Protocols.ProtocolTest] - public var existentialDictionary: [String: any Protocols.ProtocolTest] - public var existentialFunction: (any Protocols.ProtocolTest) -> Void - - public init( - simpleExistential: any Protocols.ProtocolTest, - compositionExistential: any Protocols.ProtocolTest & Sendable, - optionalExistential: (any Protocols.ProtocolTest)?, - existentialArray: [any Protocols.ProtocolTest], - existentialDictionary: [String: any Protocols.ProtocolTest], - existentialFunction: @escaping (any Protocols.ProtocolTest) -> Void - ) { - self.simpleExistential = simpleExistential - self.compositionExistential = compositionExistential - self.optionalExistential = optionalExistential - self.existentialArray = existentialArray - self.existentialDictionary = existentialDictionary - self.existentialFunction = existentialFunction - } - } - - public struct ExistentialClassBoundTest { - public var classBound: any Protocols.ClassBoundProtocolTest - public var anyObjectReference: AnyObject - - public init(classBound: any Protocols.ClassBoundProtocolTest, anyObjectReference: AnyObject) { - self.classBound = classBound - self.anyObjectReference = anyObjectReference - } - } -} -``` - -- [ ] **Step 4: Build fixture** - -Run the Build Verification Command. Expected: build succeeds. - -- [ ] **Step 5: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/NestedFunctions.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/MetatypeUsage.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/ExistentialAny.swift -git commit -m "test(fixture): add NestedFunctions, MetatypeUsage, ExistentialAny" -``` - ---- - -## Task 8: SameTypeRequirements, OptionSetAndRawRepresentable, DiamondInheritance - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/SameTypeRequirements.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/OptionSetAndRawRepresentable.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/DiamondInheritance.swift` - -- [ ] **Step 1: Write `SameTypeRequirements.swift`** - -```swift -import Foundation - -public enum SameTypeRequirements { - public struct SameTypeElementTest where First.Element == Second.Element { - public var first: First - public var second: Second - - public init(first: First, second: Second) { - self.first = first - self.second = second - } - } - - public struct NestedSameTypeTest< - First: Collection, - Second: Collection - > where First.Element == Second.Element, First.Index == Int, Second.Index == Int { - public var first: First - public var second: Second - - public init(first: First, second: Second) { - self.first = first - self.second = second - } - } - - public struct ChainedSameTypeTest< - First: Protocols.ProtocolTest, - Second: Protocols.ProtocolTest, - Third: Protocols.ProtocolTest - > where First.Body == Second, Second.Body == Third { - public var first: First - public var second: Second - public var third: Third - - public init(first: First, second: Second, third: Third) { - self.first = first - self.second = second - self.third = third - } - } -} -``` - -- [ ] **Step 2: Write `OptionSetAndRawRepresentable.swift`** - -```swift -import Foundation - -public enum OptionSetAndRawRepresentable { - public struct OptionSetTest: OptionSet { - public let rawValue: UInt - - public init(rawValue: UInt) { - self.rawValue = rawValue - } - - public static let first = OptionSetTest(rawValue: 1 << 0) - public static let second = OptionSetTest(rawValue: 1 << 1) - public static let third = OptionSetTest(rawValue: 1 << 2) - public static let all: OptionSetTest = [.first, .second, .third] - } - - public struct StringRawRepresentableTest: RawRepresentable { - public let rawValue: String - - public init?(rawValue: String) { - self.rawValue = rawValue - } - } - - public struct IntRawRepresentableTest: RawRepresentable { - public var rawValue: Int - - public init(rawValue: Int) { - self.rawValue = rawValue - } - } - - public struct GenericRawRepresentableTest: RawRepresentable { - public var rawValue: Raw - - public init(rawValue: Raw) { - self.rawValue = rawValue - } - } -} -``` - -- [ ] **Step 3: Write `DiamondInheritance.swift`** - -```swift -import Foundation - -public enum DiamondInheritance { - public protocol DiamondBaseProtocol { - func baseMethod() -> String - } - - public protocol DiamondLeftProtocol: DiamondBaseProtocol { - func leftMethod() -> Int - } - - public protocol DiamondRightProtocol: DiamondBaseProtocol { - func rightMethod() -> Double - } - - public protocol DiamondBottomProtocol: DiamondLeftProtocol, DiamondRightProtocol { - func bottomMethod() -> Bool - } - - public struct DiamondImplementationTest: DiamondBottomProtocol { - public func baseMethod() -> String { "" } - public func leftMethod() -> Int { 0 } - public func rightMethod() -> Double { 0.0 } - public func bottomMethod() -> Bool { false } - - public init() {} - } - - public protocol TriDiamondRootProtocol { - func root() -> String - } - - public protocol TriDiamondFirstProtocol: TriDiamondRootProtocol { - func first() -> Int - } - - public protocol TriDiamondSecondProtocol: TriDiamondRootProtocol { - func second() -> Int - } - - public protocol TriDiamondThirdProtocol: TriDiamondRootProtocol { - func third() -> Int - } - - public protocol TriDiamondLeafProtocol: TriDiamondFirstProtocol, TriDiamondSecondProtocol, TriDiamondThirdProtocol { - func leaf() -> Int - } -} -``` - -- [ ] **Step 4: Build fixture** - -Run the Build Verification Command. Expected: build succeeds. - -- [ ] **Step 5: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/SameTypeRequirements.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/OptionSetAndRawRepresentable.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/DiamondInheritance.swift -git commit -m "test(fixture): add SameTypeRequirements, OptionSet/RawRepresentable, DiamondInheritance" -``` - ---- - -## Task 9: WeakUnownedReferences, ErrorTypes, ResultBuilderDSL, RethrowingFunctions - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/WeakUnownedReferences.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/ErrorTypes.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/ResultBuilderDSL.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/RethrowingFunctions.swift` - -- [ ] **Step 1: Write `WeakUnownedReferences.swift`** - -```swift -import Foundation - -public enum WeakUnownedReferences { - public class ReferenceTargetTest { - public var value: Int = 0 - public init() {} - } - - public class WeakReferenceHolderTest { - public weak var weakReference: ReferenceTargetTest? - public weak var weakAnyObject: AnyObject? - public init() {} - } - - public class UnownedReferenceHolderTest { - public unowned var unownedReference: ReferenceTargetTest - public unowned(safe) var unownedSafeReference: ReferenceTargetTest - public unowned(unsafe) var unownedUnsafeReference: ReferenceTargetTest - - public init(target: ReferenceTargetTest) { - self.unownedReference = target - self.unownedSafeReference = target - self.unownedUnsafeReference = target - } - } - - public class MixedReferenceHolderTest { - public weak var weakReference: ReferenceTargetTest? - public unowned var unownedReference: ReferenceTargetTest - public var strongReference: ReferenceTargetTest - - public init(target: ReferenceTargetTest) { - self.unownedReference = target - self.strongReference = target - } - } -} -``` - -- [ ] **Step 2: Write `ErrorTypes.swift`** - -```swift -import Foundation - -public enum ErrorTypes { - public enum SimpleErrorTest: Error { - case notFound - case invalid - case unknown - } - - public enum AssociatedValueErrorTest: Error { - case withMessage(String) - case withCode(Int) - case withContext(message: String, code: Int, underlying: (any Error)?) - } - - public struct LocalizedErrorTest: LocalizedError { - public let errorDescription: String? - public let failureReason: String? - public let recoverySuggestion: String? - public let helpAnchor: String? - - public init(description: String, reason: String, suggestion: String, helpAnchor: String) { - self.errorDescription = description - self.failureReason = reason - self.recoverySuggestion = suggestion - self.helpAnchor = helpAnchor - } - } - - public struct CustomNSErrorTest: CustomNSError { - public static let errorDomain: String = "com.test.CustomNSErrorTest" - public let errorCode: Int - public let errorUserInfo: [String: Any] - - public init(errorCode: Int, errorUserInfo: [String: Any]) { - self.errorCode = errorCode - self.errorUserInfo = errorUserInfo - } - } - - public struct SendableErrorTest: Error, Sendable { - public let identifier: Int - public let descriptionText: String - - public init(identifier: Int, descriptionText: String) { - self.identifier = identifier - self.descriptionText = descriptionText - } - } -} -``` - -- [ ] **Step 3: Write `ResultBuilderDSL.swift`** - -```swift -import Foundation - -public enum ResultBuilderDSL { - @resultBuilder - public struct FullResultBuilderTest { - public static func buildExpression(_ expression: Int) -> [Int] { - [expression] - } - - public static func buildExpression(_ expression: [Int]) -> [Int] { - expression - } - - public static func buildBlock(_ components: [Int]...) -> [Int] { - components.flatMap { $0 } - } - - public static func buildOptional(_ component: [Int]?) -> [Int] { - component ?? [] - } - - public static func buildEither(first component: [Int]) -> [Int] { - component - } - - public static func buildEither(second component: [Int]) -> [Int] { - component - } - - public static func buildArray(_ components: [[Int]]) -> [Int] { - components.flatMap { $0 } - } - - public static func buildLimitedAvailability(_ component: [Int]) -> [Int] { - component - } - - public static func buildFinalResult(_ component: [Int]) -> [Int] { - component - } - } - - @resultBuilder - public struct GenericResultBuilderTest { - public static func buildBlock(_ components: [Element]...) -> [Element] { - components.flatMap { $0 } - } - - public static func buildExpression(_ expression: Element) -> [Element] { - [expression] - } - - public static func buildOptional(_ component: [Element]?) -> [Element] { - component ?? [] - } - } -} -``` - -- [ ] **Step 4: Write `RethrowingFunctions.swift`** - -```swift -import Foundation - -public enum RethrowingFunctions { - public struct RethrowingHolderTest { - public func rethrowing(_ body: () throws -> Int) rethrows -> Int { - try body() - } - - public func asyncRethrowing(_ body: () async throws -> Int) async rethrows -> Int { - try await body() - } - - public func rethrowingMap(_ elements: [Element], transform: (Element) throws -> Int) rethrows -> [Int] { - try elements.map(transform) - } - - public func rethrowingWithDefault(_ body: () throws -> Int, defaultValue: Int = 0) rethrows -> Int { - try body() - } - - public func rethrowingGeneric(_ input: Input, transform: (Input) throws -> Output) rethrows -> Output { - try transform(input) - } - } -} -``` - -- [ ] **Step 5: Build fixture** - -Run the Build Verification Command. Expected: build succeeds. - -- [ ] **Step 6: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/WeakUnownedReferences.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/ErrorTypes.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/ResultBuilderDSL.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/RethrowingFunctions.swift -git commit -m "test(fixture): add WeakUnowned, ErrorTypes, ResultBuilderDSL, Rethrowing" -``` - ---- - -## Task 10: ProtocolComposition, OverloadedMembers, UnsafePointers - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/ProtocolComposition.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/OverloadedMembers.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/UnsafePointers.swift` - -- [ ] **Step 1: Write `ProtocolComposition.swift`** - -```swift -import Foundation - -public enum ProtocolComposition { - public protocol ComposeFirstProtocol { - func first() -> Int - } - - public protocol ComposeSecondProtocol { - func second() -> String - } - - public protocol ComposeThirdProtocol { - func third() -> Double - } - - public struct ProtocolCompositionFieldTest { - public var twoComposition: any ComposeFirstProtocol & ComposeSecondProtocol - public var threeComposition: any ComposeFirstProtocol & ComposeSecondProtocol & ComposeThirdProtocol - public var classBoundComposition: any AnyObject & ComposeFirstProtocol - public var sendableComposition: any Sendable & ComposeFirstProtocol - - public init( - twoComposition: any ComposeFirstProtocol & ComposeSecondProtocol, - threeComposition: any ComposeFirstProtocol & ComposeSecondProtocol & ComposeThirdProtocol, - classBoundComposition: any AnyObject & ComposeFirstProtocol, - sendableComposition: any Sendable & ComposeFirstProtocol - ) { - self.twoComposition = twoComposition - self.threeComposition = threeComposition - self.classBoundComposition = classBoundComposition - self.sendableComposition = sendableComposition - } - } - - public struct ProtocolCompositionFunctionTest { - public func acceptComposition(_ value: any ComposeFirstProtocol & ComposeSecondProtocol) {} - - public func returnComposition() -> any ComposeFirstProtocol & ComposeSecondProtocol { - fatalError() - } - - public func genericCompositionParameter(_ element: Element) {} - } -} -``` - -- [ ] **Step 2: Write `OverloadedMembers.swift`** - -```swift -import Foundation - -public enum OverloadedMembers { - public struct OverloadedMethodTest { - public func process(_ value: Int) -> Int { value } - public func process(_ value: Double) -> Double { value } - public func process(_ value: String) -> String { value } - public func process(_ first: Int, _ second: Int) -> Int { first + second } - public func process(_ first: Int, label: String) -> String { label } - public func process(_ value: Element) -> Element { value } - public func process(equatable value: Element) -> Bool { false } - } - - public struct OverloadedSubscriptTest { - public subscript(index: Int) -> Int { 0 } - public subscript(key: String) -> String { "" } - public subscript(range: Range) -> [Int] { [] } - public subscript(element element: Element) -> Int { 0 } - } - - public struct OverloadedInitializerTest { - public init(_ value: Int) {} - public init(_ value: String) {} - public init(_ value: Double) {} - public init(first: Int, second: Int) {} - public init(element: Element) {} - } -} -``` - -- [ ] **Step 3: Write `UnsafePointers.swift`** - -```swift -import Foundation - -public enum UnsafePointers { - public struct UnsafePointerFieldTest { - public var readPointer: UnsafePointer - public var mutablePointer: UnsafeMutablePointer - public var rawPointer: UnsafeRawPointer - public var mutableRawPointer: UnsafeMutableRawPointer - public var bufferPointer: UnsafeBufferPointer - public var mutableBufferPointer: UnsafeMutableBufferPointer - public var rawBufferPointer: UnsafeRawBufferPointer - public var opaquePointer: OpaquePointer - - public init( - readPointer: UnsafePointer, - mutablePointer: UnsafeMutablePointer, - rawPointer: UnsafeRawPointer, - mutableRawPointer: UnsafeMutableRawPointer, - bufferPointer: UnsafeBufferPointer, - mutableBufferPointer: UnsafeMutableBufferPointer, - rawBufferPointer: UnsafeRawBufferPointer, - opaquePointer: OpaquePointer - ) { - self.readPointer = readPointer - self.mutablePointer = mutablePointer - self.rawPointer = rawPointer - self.mutableRawPointer = mutableRawPointer - self.bufferPointer = bufferPointer - self.mutableBufferPointer = mutableBufferPointer - self.rawBufferPointer = rawBufferPointer - self.opaquePointer = opaquePointer - } - } - - public struct UnmanagedFieldTest { - public var unmanagedReference: Unmanaged - - public init(unmanagedReference: Unmanaged) { - self.unmanagedReference = unmanagedReference - } - } - - public struct AutoreleasingPointerFieldTest { - public var autoreleasing: AutoreleasingUnsafeMutablePointer - - public init(autoreleasing: AutoreleasingUnsafeMutablePointer) { - self.autoreleasing = autoreleasing - } - } -} -``` - -- [ ] **Step 4: Build fixture** - -Run the Build Verification Command. Expected: build succeeds. - -- [ ] **Step 5: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/ProtocolComposition.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/OverloadedMembers.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/UnsafePointers.swift -git commit -m "test(fixture): add ProtocolComposition, OverloadedMembers, UnsafePointers" -``` - ---- - -## Task 11: AsyncSequence, PropertyWrapperVariants, CustomLiterals - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/AsyncSequence.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/PropertyWrapperVariants.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/CustomLiterals.swift` - -- [ ] **Step 1: Write `AsyncSequence.swift`** - -```swift -import Foundation - -public enum AsyncSequenceTests { - public struct AsyncSequenceTest: AsyncSequence { - public typealias Element = Int - - public struct AsyncIterator: AsyncIteratorProtocol { - public mutating func next() async -> Element? { - nil - } - } - - public func makeAsyncIterator() -> AsyncIterator { - AsyncIterator() - } - } - - public struct ThrowingAsyncSequenceTest: AsyncSequence { - public typealias Element = String - - public struct AsyncIterator: AsyncIteratorProtocol { - public mutating func next() async throws -> Element? { - nil - } - } - - public func makeAsyncIterator() -> AsyncIterator { - AsyncIterator() - } - } - - public struct GenericAsyncSequenceTest: AsyncSequence { - public struct AsyncIterator: AsyncIteratorProtocol { - public mutating func next() async -> Element? { - nil - } - } - - public func makeAsyncIterator() -> AsyncIterator { - AsyncIterator() - } - } -} -``` - -- [ ] **Step 2: Write `PropertyWrapperVariants.swift`** - -```swift -import Foundation - -public enum PropertyWrapperVariants { - @propertyWrapper - public struct ProjectedValueWrapperTest { - private var storage: Value - public var wrappedValue: Value { - get { storage } - set { storage = newValue } - } - public var projectedValue: ProjectedValueWrapperTest { - self - } - - public init(wrappedValue: Value) { - self.storage = wrappedValue - } - } - - @propertyWrapper - public struct DefaultInitializableWrapperTest { - public var wrappedValue: Int - - public init() { - self.wrappedValue = 0 - } - - public init(wrappedValue: Int) { - self.wrappedValue = wrappedValue - } - } - - @propertyWrapper - public struct StaticSubscriptWrapperTest { - public static subscript( - _enclosingInstance instance: Enclosing, - wrapped wrappedKeyPath: ReferenceWritableKeyPath, - storage storageKeyPath: ReferenceWritableKeyPath - ) -> Value { - get { - instance[keyPath: storageKeyPath].storage - } - set { - instance[keyPath: storageKeyPath].storage = newValue - } - } - - @available(*, unavailable) - public var wrappedValue: Value { - get { fatalError() } - set { fatalError() } - } - - private var storage: Value - - public init(wrappedValue: Value) { - self.storage = wrappedValue - } - } -} -``` - -- [ ] **Step 3: Write `CustomLiterals.swift`** - -```swift -import Foundation - -public enum CustomLiterals { - public struct IntegerLiteralTest: ExpressibleByIntegerLiteral { - public let value: Int64 - public init(integerLiteral value: Int64) { - self.value = value - } - } - - public struct StringLiteralTest: ExpressibleByStringLiteral, ExpressibleByUnicodeScalarLiteral, ExpressibleByExtendedGraphemeClusterLiteral { - public let value: String - - public init(stringLiteral value: String) { - self.value = value - } - - public init(unicodeScalarLiteral value: String) { - self.value = value - } - - public init(extendedGraphemeClusterLiteral value: String) { - self.value = value - } - } - - public struct ArrayLiteralTest: ExpressibleByArrayLiteral { - public let elements: [Int] - - public init(arrayLiteral elements: Int...) { - self.elements = elements - } - } - - public struct DictionaryLiteralTest: ExpressibleByDictionaryLiteral { - public let elements: [String: Int] - - public init(dictionaryLiteral elements: (String, Int)...) { - var dictionary: [String: Int] = [:] - for (key, value) in elements { - dictionary[key] = value - } - self.elements = dictionary - } - } - - public struct BooleanLiteralTest: ExpressibleByBooleanLiteral { - public let value: Bool - public init(booleanLiteral value: Bool) { - self.value = value - } - } - - public struct FloatLiteralTest: ExpressibleByFloatLiteral { - public let value: Double - public init(floatLiteral value: Double) { - self.value = value - } - } - - public struct NilLiteralTest: ExpressibleByNilLiteral { - public init(nilLiteral: ()) {} - } -} -``` - -- [ ] **Step 4: Build fixture** - -Run the Build Verification Command. Expected: build succeeds. - -- [ ] **Step 5: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/AsyncSequence.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/PropertyWrapperVariants.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/CustomLiterals.swift -git commit -m "test(fixture): add AsyncSequence, PropertyWrapperVariants, CustomLiterals" -``` - ---- - -## Task 12: StaticMembers, ClassBoundGenerics, MarkerProtocols - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/StaticMembers.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/ClassBoundGenerics.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/MarkerProtocols.swift` - -- [ ] **Step 1: Write `StaticMembers.swift`** - -```swift -import Foundation - -public enum StaticMembers { - public struct StaticMemberStructTest { - public static let storedConstant: Int = 0 - public static var storedMutable: String = "" - public static var computedProperty: Int { - get { 0 } - set {} - } - - public static func staticMethod() -> Int { 0 } - public static func staticGenericMethod(_ element: Element) -> Element { element } - - public static subscript(index: Int) -> String { - String(index) - } - } - - public class StaticMemberClassTest { - public static let storedConstant: Int = 0 - public static var storedMutable: String = "" - public class var classComputed: Int { 0 } - - public static func staticMethod() -> Int { 0 } - public class func classMethod() -> Int { 0 } - - public init() {} - } - - public class StaticMemberSubclassTest: StaticMemberClassTest { - public override class var classComputed: Int { 1 } - public override class func classMethod() -> Int { 1 } - } -} -``` - -- [ ] **Step 2: Write `ClassBoundGenerics.swift`** - -```swift -import Foundation - -public enum ClassBoundGenerics { - public struct AnyObjectBoundTest { - public var element: Element - public init(element: Element) { - self.element = element - } - } - - public struct AnyObjectAndProtocolBoundTest where Element: AnyObject, Element: Protocols.ProtocolTest { - public var element: Element - public init(element: Element) { - self.element = element - } - } - - public class ClassBoundGenericClassTest { - public var element: Element - public init(element: Element) { - self.element = element - } - } - - public protocol ClassBoundGenericProtocol: AnyObject { - associatedtype Item: AnyObject - var item: Item { get } - } - - public struct ClassBoundFunctionTest { - public func acceptClassBound(_ element: Element) -> Element { - element - } - - public func acceptClassAndProtocol(_ element: Element) -> Element where Element: AnyObject, Element: Protocols.ProtocolTest { - element - } - } -} -``` - -- [ ] **Step 3: Write `MarkerProtocols.swift`** - -```swift -import Foundation - -public enum MarkerProtocols { - public protocol MarkerProtocolTest {} - - public protocol EmptyMarkerProtocolTest {} - - public protocol ClassBoundMarkerProtocol: AnyObject {} - - public protocol InheritingMarkerProtocol: MarkerProtocolTest {} - - public struct MarkerConformingStructTest: MarkerProtocolTest, EmptyMarkerProtocolTest { - public var value: Int - public init(value: Int) { - self.value = value - } - } - - public class MarkerConformingClassTest: ClassBoundMarkerProtocol, InheritingMarkerProtocol { - public var label: String - public init(label: String) { - self.label = label - } - } - - public enum MarkerConformingEnumTest: MarkerProtocolTest { - case first - case second - } -} -``` - -- [ ] **Step 4: Build fixture** - -Run the Build Verification Command. Expected: build succeeds. - -- [ ] **Step 5: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/StaticMembers.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/ClassBoundGenerics.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/MarkerProtocols.swift -git commit -m "test(fixture): add StaticMembers, ClassBoundGenerics, MarkerProtocols" -``` - ---- - -## Task 13: DependentTypeAccess, DeinitVariants, CollectionConformances - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/DependentTypeAccess.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/DeinitVariants.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/CollectionConformances.swift` - -- [ ] **Step 1: Write `DependentTypeAccess.swift`** - -```swift -import Foundation - -public enum DependentTypeAccess { - public protocol DependentProtocol { - associatedtype First - associatedtype Second: Collection where Second.Element == First - } - - public struct DependentAccessTest { - public var iteratorElement: Element.Iterator.Element? - public var indicesIndex: Element.Indices.Element? - public var subSequenceIndex: Element.SubSequence.Index? - - public init( - iteratorElement: Element.Iterator.Element?, - indicesIndex: Element.Indices.Element?, - subSequenceIndex: Element.SubSequence.Index? - ) { - self.iteratorElement = iteratorElement - self.indicesIndex = indicesIndex - self.subSequenceIndex = subSequenceIndex - } - } - - public struct DeepDependentAccessTest where Element.SubSequence: Collection { - public var deepElement: Element.SubSequence.SubSequence.Element? - - public init(deepElement: Element.SubSequence.SubSequence.Element?) { - self.deepElement = deepElement - } - } - - public struct DependentFunctionTest { - public func acceptDependent( - _ element: Element, - iteratorElement: Element.Iterator.Element, - indicesElement: Element.Indices.Element - ) -> Element.SubSequence { - element[element.startIndex.. { - public var element: Element - public init(element: Element) { - self.element = element - } - deinit {} - } -} -``` - -Note: `isolated deinit` was considered but omitted because it is still experimental. Only include it if `swift-syntax` tooling confirms stable support on the current toolchain. - -- [ ] **Step 3: Write `CollectionConformances.swift`** - -```swift -import Foundation - -public enum CollectionConformances { - public struct CustomSequenceTest: Sequence { - public struct Iterator: IteratorProtocol { - public mutating func next() -> Int? { nil } - } - - public func makeIterator() -> Iterator { - Iterator() - } - } - - public struct CustomCollectionTest: Collection { - public var startIndex: Int { 0 } - public var endIndex: Int { 0 } - - public subscript(position: Int) -> Int { 0 } - - public func index(after index: Int) -> Int { - index + 1 - } - } - - public struct CustomBidirectionalCollectionTest: BidirectionalCollection { - public var startIndex: Int { 0 } - public var endIndex: Int { 0 } - - public subscript(position: Int) -> String { "" } - - public func index(after index: Int) -> Int { - index + 1 - } - - public func index(before index: Int) -> Int { - index - 1 - } - } - - public struct CustomRandomAccessCollectionTest: RandomAccessCollection { - public var startIndex: Int { 0 } - public var endIndex: Int { 0 } - - public subscript(position: Int) -> Double { 0.0 } - - public func index(after index: Int) -> Int { - index + 1 - } - - public func index(before index: Int) -> Int { - index - 1 - } - } -} -``` - -- [ ] **Step 4: Build fixture** - -Run the Build Verification Command. Expected: build succeeds. - -- [ ] **Step 5: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/DependentTypeAccess.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/DeinitVariants.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/CollectionConformances.swift -git commit -m "test(fixture): add DependentTypeAccess, DeinitVariants, CollectionConformances" -``` - ---- - -## Task 14: FieldDescriptorVariants, GenericRequirementVariants - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/FieldDescriptorVariants.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/GenericRequirementVariants.swift` - -- [ ] **Step 1: Write `FieldDescriptorVariants.swift`** - -```swift -import Foundation - -public enum FieldDescriptorVariants { - public struct VarLetFieldTest { - public var mutableField: Int - public let immutableField: String - public var mutableOptional: Double? - public let immutableOptional: Int? - - public init(mutableField: Int, immutableField: String, mutableOptional: Double?, immutableOptional: Int?) { - self.mutableField = mutableField - self.immutableField = immutableField - self.mutableOptional = mutableOptional - self.immutableOptional = immutableOptional - } - } - - public class ReferenceFieldTest { - public weak var weakField: AnyObject? - public unowned var unownedField: AnyObject - public unowned(unsafe) var unownedUnsafeField: AnyObject - public var strongField: AnyObject - - public init(reference: AnyObject) { - self.unownedField = reference - self.unownedUnsafeField = reference - self.strongField = reference - } - } - - public struct MangledNameVariantsTest { - public var concreteInt: Int - public var concreteString: String - public var genericElement: Element - public var arrayOfElement: [Element] - public var dictionaryOfElement: [String: Element] - public var optionalElement: Element? - public var tupleField: (Int, Element) - public var functionField: (Element) -> Int - - public init( - concreteInt: Int, - concreteString: String, - genericElement: Element, - arrayOfElement: [Element], - dictionaryOfElement: [String: Element], - optionalElement: Element?, - tupleField: (Int, Element), - functionField: @escaping (Element) -> Int - ) { - self.concreteInt = concreteInt - self.concreteString = concreteString - self.genericElement = genericElement - self.arrayOfElement = arrayOfElement - self.dictionaryOfElement = dictionaryOfElement - self.optionalElement = optionalElement - self.tupleField = tupleField - self.functionField = functionField - } - } -} -``` - -- [ ] **Step 2: Write `GenericRequirementVariants.swift`** - -```swift -import Foundation - -public enum GenericRequirementVariants { - public struct ProtocolRequirementTest { - public var element: Element - public init(element: Element) { self.element = element } - } - - public struct SameTypeRequirementTest where First == Second { - public var first: First - public var second: Second - - public init(first: First, second: Second) { - self.first = first - self.second = second - } - } - - public class GenericBaseClassForRequirementTest { - public var baseField: Int = 0 - public init() {} - } - - public struct BaseClassRequirementTest { - public var element: Element - public init(element: Element) { self.element = element } - } - - public struct LayoutAnyObjectRequirementTest { - public var element: Element - public init(element: Element) { self.element = element } - } - - public struct SameShapePackRequirementTest { - public var first: (repeat each First) - public var second: (repeat each Second) - - public init(first: (repeat each First), second: (repeat each Second)) { - self.first = first - self.second = second - } - } - - public struct InvertibleProtocolRequirementTest: ~Copyable { - public var element: Element - - public init(element: consuming Element) { - self.element = element - } - } -} -``` - -- [ ] **Step 3: Build fixture** - -Run the Build Verification Command. -Expected: build succeeds. If the parameter pack declaration produces a "same-shape inference" diagnostic, add an explicit `where (repeat (each First, each Second)): Any` constraint. - -- [ ] **Step 4: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/FieldDescriptorVariants.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/GenericRequirementVariants.swift -git commit -m "test(fixture): add FieldDescriptorVariants and GenericRequirementVariants" -``` - ---- - -## Task 15: VTableEntryVariants, ConditionalConformanceVariants - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/VTableEntryVariants.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/ConditionalConformanceVariants.swift` - -- [ ] **Step 1: Write `VTableEntryVariants.swift`** - -```swift -import Foundation - -public enum VTableEntryVariants { - public class VTableBaseTest { - public func normalMethod() {} - public func overridableMethod() -> Int { 0 } - public final func finalMethod() {} - public func asyncMethod() async -> Int { 0 } - public func throwingMethod() throws -> Int { 0 } - public func asyncThrowingMethod() async throws -> Int { 0 } - - public var normalProperty: Int { - get { 0 } - set {} - } - - public var asyncProperty: Int { - get async { 0 } - } - - public var throwingProperty: Int { - get throws { 0 } - } - - public init() {} - } - - public class VTableOverrideTest: VTableBaseTest { - public override func overridableMethod() -> Int { 1 } - public override func asyncMethod() async -> Int { 1 } - public override func throwingMethod() throws -> Int { 1 } - } - - public final class VTableFinalOverrideTest: VTableBaseTest { - public override func overridableMethod() -> Int { 2 } - } - - public class VTableDeepOverrideTest: VTableOverrideTest { - public override func overridableMethod() -> Int { 3 } - public override func asyncMethod() async -> Int { 3 } - } -} -``` - -- [ ] **Step 2: Write `ConditionalConformanceVariants.swift`** - -```swift -import Foundation - -public enum ConditionalConformanceVariants { - public struct ConditionalContainerTest { - public var element: Element - public init(element: Element) { self.element = element } - } - - public protocol ConditionalFirstProtocol {} - public protocol ConditionalSecondProtocol {} - public protocol ConditionalThirdProtocol {} -} - -extension ConditionalConformanceVariants.ConditionalContainerTest: Equatable where Element: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.element == rhs.element - } -} - -extension ConditionalConformanceVariants.ConditionalContainerTest: Hashable where Element: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(element) - } -} - -extension ConditionalConformanceVariants.ConditionalContainerTest: Comparable where Element: Comparable { - public static func < (lhs: Self, rhs: Self) -> Bool { - lhs.element < rhs.element - } -} - -extension ConditionalConformanceVariants.ConditionalContainerTest: Sendable where Element: Sendable {} - -extension ConditionalConformanceVariants.ConditionalContainerTest: ConditionalConformanceVariants.ConditionalFirstProtocol -where Element: ConditionalConformanceVariants.ConditionalFirstProtocol {} - -extension ConditionalConformanceVariants.ConditionalContainerTest: ConditionalConformanceVariants.ConditionalSecondProtocol -where Element: ConditionalConformanceVariants.ConditionalFirstProtocol & ConditionalConformanceVariants.ConditionalSecondProtocol {} - -extension ConditionalConformanceVariants.ConditionalContainerTest: ConditionalConformanceVariants.ConditionalThirdProtocol -where Element: ConditionalConformanceVariants.ConditionalFirstProtocol, - Element: ConditionalConformanceVariants.ConditionalSecondProtocol, - Element: ConditionalConformanceVariants.ConditionalThirdProtocol {} -``` - -- [ ] **Step 3: Build fixture** - -Run the Build Verification Command. Expected: build succeeds. - -- [ ] **Step 4: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/VTableEntryVariants.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/ConditionalConformanceVariants.swift -git commit -m "test(fixture): add VTableEntryVariants and ConditionalConformanceVariants" -``` - ---- - -## Task 16: DefaultImplementationVariants, FrozenResilienceContrast - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/DefaultImplementationVariants.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/FrozenResilienceContrast.swift` - -- [ ] **Step 1: Write `DefaultImplementationVariants.swift`** - -```swift -import Foundation - -public enum DefaultImplementationVariants { - public protocol BasicDefaultProtocol { - func required() -> Int - func withDefault() -> String - func withDefaultAndGeneric(_ element: Element) -> Element - } - - public protocol ConstrainedDefaultProtocol { - associatedtype Element - var element: Element { get } - } -} - -extension DefaultImplementationVariants.BasicDefaultProtocol { - public func withDefault() -> String { - "default" - } - - public func withDefaultAndGeneric(_ element: Element) -> Element { - element - } -} - -extension DefaultImplementationVariants.ConstrainedDefaultProtocol where Element: Equatable { - public func isEqualTo(_ other: Element) -> Bool { - element == other - } -} - -extension DefaultImplementationVariants.ConstrainedDefaultProtocol where Element: Comparable { - public func isLessThan(_ other: Element) -> Bool { - element < other - } -} - -extension DefaultImplementationVariants.ConstrainedDefaultProtocol where Element: Hashable & Sendable { - public func computeHash() -> Int { - element.hashValue - } -} - -extension DefaultImplementationVariants.ConstrainedDefaultProtocol where Element: AnyObject { - public func identityCheck(_ other: Element) -> Bool { - element === other - } -} -``` - -- [ ] **Step 2: Write `FrozenResilienceContrast.swift`** - -```swift -import Foundation - -public enum FrozenResilienceContrast { - @frozen - public struct FrozenStructTest { - public var firstField: Int - public var secondField: Double - public var thirdField: String - - public init(firstField: Int, secondField: Double, thirdField: String) { - self.firstField = firstField - self.secondField = secondField - self.thirdField = thirdField - } - } - - public struct ResilientStructTest { - public var firstField: Int - public var secondField: Double - public var thirdField: String - - public init(firstField: Int, secondField: Double, thirdField: String) { - self.firstField = firstField - self.secondField = secondField - self.thirdField = thirdField - } - } - - @frozen - public enum FrozenEnumContrastTest { - case empty - case integer(Int) - case string(String) - case pair(Int, Double) - } - - public enum ResilientEnumContrastTest { - case empty - case integer(Int) - case string(String) - case pair(Int, Double) - } - - @frozen - public struct FrozenGenericTest { - public var element: Element - public var count: Int - - public init(element: Element, count: Int) { - self.element = element - self.count = count - } - } - - public struct ResilientGenericTest { - public var element: Element - public var count: Int - - public init(element: Element, count: Int) { - self.element = element - self.count = count - } - } -} -``` - -- [ ] **Step 3: Build fixture** - -Run the Build Verification Command. Expected: build succeeds. - -- [ ] **Step 4: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/DefaultImplementationVariants.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/FrozenResilienceContrast.swift -git commit -m "test(fixture): add DefaultImplementationVariants and FrozenResilienceContrast" -``` - ---- - -## Task 17: AssociatedTypeWitnessPatterns, BuiltinTypeFields - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/AssociatedTypeWitnessPatterns.swift` -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/BuiltinTypeFields.swift` - -- [ ] **Step 1: Write `AssociatedTypeWitnessPatterns.swift`** - -```swift -import Foundation - -public enum AssociatedTypeWitnessPatterns { - public protocol AssociatedPatternProtocol { - associatedtype First - associatedtype Second: Collection - associatedtype Third - associatedtype Fourth - associatedtype Fifth - } - - public struct ConcreteWitnessTest: AssociatedPatternProtocol { - public typealias First = Int - public typealias Second = [String] - public typealias Third = Double - public typealias Fourth = Bool - public typealias Fifth = Character - } - - public struct NestedWitnessTest: AssociatedPatternProtocol { - public struct NestedFirst {} - public struct NestedThird {} - - public typealias First = NestedFirst - public typealias Second = [NestedFirst] - public typealias Third = NestedThird - public typealias Fourth = (NestedFirst, NestedThird) - public typealias Fifth = NestedFirst? - } - - public struct GenericParameterWitnessTest: AssociatedPatternProtocol { - public typealias First = Element - public typealias Second = [Element] - public typealias Third = Element? - public typealias Fourth = (Element, Element) - public typealias Fifth = [String: Element] - } - - public struct RecursiveWitnessTest: AssociatedPatternProtocol { - public typealias First = RecursiveWitnessTest - public typealias Second = [RecursiveWitnessTest] - public typealias Third = RecursiveWitnessTest? - public typealias Fourth = (RecursiveWitnessTest, RecursiveWitnessTest) - public typealias Fifth = [String: RecursiveWitnessTest] - } - - public struct DependentWitnessTest: AssociatedPatternProtocol { - public typealias First = Element.Element - public typealias Second = Element - public typealias Third = Element.Iterator - public typealias Fourth = Element.Index - public typealias Fifth = Element.SubSequence - } -} -``` - -- [ ] **Step 2: Write `BuiltinTypeFields.swift`** - -```swift -import Foundation - -public enum BuiltinTypeFields { - public struct IntegerTypesTest { - public var intField: Int - public var int8Field: Int8 - public var int16Field: Int16 - public var int32Field: Int32 - public var int64Field: Int64 - public var uintField: UInt - public var uint8Field: UInt8 - public var uint16Field: UInt16 - public var uint32Field: UInt32 - public var uint64Field: UInt64 - - public init( - intField: Int, - int8Field: Int8, - int16Field: Int16, - int32Field: Int32, - int64Field: Int64, - uintField: UInt, - uint8Field: UInt8, - uint16Field: UInt16, - uint32Field: UInt32, - uint64Field: UInt64 - ) { - self.intField = intField - self.int8Field = int8Field - self.int16Field = int16Field - self.int32Field = int32Field - self.int64Field = int64Field - self.uintField = uintField - self.uint8Field = uint8Field - self.uint16Field = uint16Field - self.uint32Field = uint32Field - self.uint64Field = uint64Field - } - } - - public struct FloatingTypesTest { - public var floatField: Float - public var doubleField: Double - public var float32Field: Float32 - public var float64Field: Float64 - - public init(floatField: Float, doubleField: Double, float32Field: Float32, float64Field: Float64) { - self.floatField = floatField - self.doubleField = doubleField - self.float32Field = float32Field - self.float64Field = float64Field - } - } - - public struct PrimitiveTypesTest { - public var boolField: Bool - public var characterField: Character - public var stringField: String - - public init(boolField: Bool, characterField: Character, stringField: String) { - self.boolField = boolField - self.characterField = characterField - self.stringField = stringField - } - } - - public struct TupleBuiltinTest { - public var pairField: (Int, Double) - public var tripleField: (Int, Double, Bool) - public var quadrupleField: (Int8, Int16, Int32, Int64) - - public init(pairField: (Int, Double), tripleField: (Int, Double, Bool), quadrupleField: (Int8, Int16, Int32, Int64)) { - self.pairField = pairField - self.tripleField = tripleField - self.quadrupleField = quadrupleField - } - } -} -``` - -- [ ] **Step 3: Build fixture** - -Run the Build Verification Command. Expected: build succeeds. - -- [ ] **Step 4: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/AssociatedTypeWitnessPatterns.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/BuiltinTypeFields.swift -git commit -m "test(fixture): add AssociatedTypeWitnessPatterns and BuiltinTypeFields" -``` - ---- - -## Task 18: Edits to existing files - -**Files:** -- Modify: `Tests/Projects/SymbolTests/SymbolTestsCore/Classes.swift` -- Modify: `Tests/Projects/SymbolTests/SymbolTestsCore/Enums.swift` -- Modify: `Tests/Projects/SymbolTests/SymbolTestsCore/FunctionFeatures.swift` -- Modify: `Tests/Projects/SymbolTests/SymbolTestsCore/Protocols.swift` - -- [ ] **Step 1: Extend `Classes.swift`** - -Add these nested types inside the existing `public enum Classes { ... }` block (place before the closing `}`): - -```swift - public class RequiredInitClassTest { - public let identifier: Int - public required init(identifier: Int) { - self.identifier = identifier - } - } - - public class DefaultParameterClassTest { - public func method(first: Int = 0, second: String = "default") -> Int { - first - } - - public class func classMethod(value: Int = 42) -> Int { - value - } - - public init() {} - } -``` - -- [ ] **Step 2: Extend `Enums.swift`** - -Add these nested enums inside the existing `public enum Enums { ... }` block: - -```swift - @frozen - public enum LargeFrozenEnumTest { - case alpha - case beta - case gamma - case delta - case epsilon - case zeta - case eta - case theta - case iota - case kappa - } - - public enum GenericPayloadEnumTest { - case first(Element) - case second(Element, Element) - case empty - } - - public enum FunctionReferenceCaseTest { - case first(Int) - case second(String) - - public static func selectFirst() -> (Int) -> FunctionReferenceCaseTest { - FunctionReferenceCaseTest.first - } - } -``` - -- [ ] **Step 3: Extend `FunctionFeatures.swift`** - -Add these nested structs inside the existing `public enum FunctionFeatures { ... }` block: - -```swift - public struct MainActorClosureTest { - public func acceptMainActorClosure(_ callback: @MainActor () -> Void) {} - public func acceptMainActorAsync(_ callback: @MainActor () async -> Void) {} - } - - public struct DefaultParameterFunctionTest { - public func defaultMethod(value: Int = 0, label: String = "default", flag: Bool = true) -> String { - label - } - - public static func staticDefault(first: Int = 0, second: Int = 1) -> Int { - first + second - } - } -``` - -- [ ] **Step 4: Extend `Protocols.swift`** - -Add these protocols inside the existing `public enum Protocols { ... }` block: - -```swift - public protocol SelfConstraintProtocolTest where Self: AnyObject, Self: Sendable { - func method() -> Self - } - - public protocol MultiPrimaryAssociatedTypeTest { - associatedtype First - associatedtype Second - associatedtype Third - } -``` - -- [ ] **Step 5: Build fixture** - -Run the Build Verification Command. Expected: build succeeds. - -- [ ] **Step 6: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/Classes.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/Enums.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/FunctionFeatures.swift \ - Tests/Projects/SymbolTests/SymbolTestsCore/Protocols.swift -git commit -m "test(fixture): extend Classes, Enums, FunctionFeatures, Protocols" -``` - ---- - -## Task 19: Regenerate snapshot and run full SwiftInterfaceTests - -The `MachOFileInterfaceSnapshotTests` compares the full SwiftInterface output of `SymbolTestsCore` against a stored snapshot. Because every prior task added new symbols, this snapshot is now stale. - -**Files:** -- Delete: `Tests/SwiftInterfaceTests/Snapshots/__Snapshots__/MachOFileInterfaceSnapshotTests/interfaceSnapshot.1.txt` -- Regenerate: same path, from test run -- Run: `swift test --filter SwiftInterfaceTests` - -- [ ] **Step 1: Update package dependencies** - -Run: `swift package update` -Expected: resolves dependencies; no errors. - -- [ ] **Step 2: Delete the stale snapshot** - -Run: -```bash -rm Tests/SwiftInterfaceTests/Snapshots/__Snapshots__/MachOFileInterfaceSnapshotTests/interfaceSnapshot.1.txt -``` - -- [ ] **Step 3: Run the snapshot test to regenerate** - -Run: `swift test --filter MachOFileInterfaceSnapshotTests 2>&1 | xcsift` -Expected: the test "fails" the first time with a `.missing` record mode notice and writes a new `interfaceSnapshot.1.txt` file. Re-run the same command; the second run should pass against the newly-generated snapshot. - -- [ ] **Step 4: Inspect the regenerated snapshot** - -Run: `wc -l Tests/SwiftInterfaceTests/Snapshots/__Snapshots__/MachOFileInterfaceSnapshotTests/interfaceSnapshot.1.txt` -Expected: substantially larger than the original 930 lines (now should be ~2500–3500 lines including all new types). - -Spot-check the snapshot to confirm a sampling of new types are present: - -```bash -grep -E "KeyPaths|DistributedActors|FieldDescriptorVariants|OverloadedMethodTest" \ - Tests/SwiftInterfaceTests/Snapshots/__Snapshots__/MachOFileInterfaceSnapshotTests/interfaceSnapshot.1.txt | head -20 -``` -Expected: each pattern matched at least once. - -- [ ] **Step 5: Run the complete SwiftInterfaceTests suite** - -Run: `swift test --filter SwiftInterfaceTests 2>&1 | xcsift` -Expected: all tests pass. If any existing test fails because it hard-codes a condition only true on the old fixture shape (e.g., expected type count), fix the test to be robust to the new shape rather than reverting fixture changes. - -- [ ] **Step 6: Commit regenerated snapshot** - -```bash -git add Tests/SwiftInterfaceTests/Snapshots/__Snapshots__/MachOFileInterfaceSnapshotTests/interfaceSnapshot.1.txt -git commit -m "test(snapshot): regenerate MachOFileInterfaceSnapshot for expanded fixture" -``` - ---- - -## Final verification checklist - -After Task 19 is committed, do a quick sanity check on the whole branch: - -- [ ] Run the full test suite: `swift test 2>&1 | xcsift`. -- [ ] Confirm no test target regressed beyond `MachOFileInterfaceSnapshotTests` (whose snapshot was intentionally updated). -- [ ] `git log --oneline feature/vtable-offset-and-member-ordering` should show 19 new test-fixture commits. -- [ ] Spot-check that `Tests/Projects/SymbolTests/SymbolTestsCore/` now contains 62 files (18 pre-existing + 44 new). diff --git a/docs/superpowers/plans/2026-04-18-ci-test-filter.md b/docs/superpowers/plans/2026-04-18-ci-test-filter.md deleted file mode 100644 index 7ecf528a..00000000 --- a/docs/superpowers/plans/2026-04-18-ci-test-filter.md +++ /dev/null @@ -1,407 +0,0 @@ -# CI Test Filter and macOS 26 Upgrade Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Restrict CI test runs to five `SymbolTestsCore`-based test classes and bump both workflows to macOS 26 + Xcode 26.4. - -**Architecture:** Pure CI configuration change. No source-code edits. Apply a `--filter` regex to `swift test` in `.github/workflows/macOS.yml`, and bump the runner image and Xcode version in both `macOS.yml` and `release.yml`. Verify the regex selects exactly the intended five test classes, then commit each workflow change separately. - -**Tech Stack:** GitHub Actions, `xcodebuild`, SwiftPM `swift test --filter`. - -**Spec:** `docs/superpowers/specs/2026-04-18-ci-test-filter-design.md` - ---- - -## File Structure - -| File | Change | -|---|---| -| `.github/workflows/macOS.yml` | Modify `matrix.os`, `matrix.xcode-version`, and the two `swift test` invocations (add `--filter`) | -| `.github/workflows/release.yml` | Modify `runs-on` and the `Setup Xcode` `xcode-version` | - -No new files. No source files touched. - ---- - -## Task 1: Verify the `--filter` regex locally - -**Files:** none modified. Verification only. - -**Goal:** Confirm the proposed regex matches exactly the five target test classes and nothing else: - -- `SwiftDumpTests.SymbolTestsCoreDumpSnapshotTests` -- `SwiftInterfaceTests.SymbolTestsCoreInterfaceSnapshotTests` -- `SwiftDumpTests.SymbolTestsCoreCoverageInvariantTests` -- `SwiftInterfaceTests.STCoreE2ETests` -- `SwiftInterfaceTests.STCoreTests` - -The proposed regex is: - -``` -\.(SymbolTestsCoreDumpSnapshotTests|SymbolTestsCoreInterfaceSnapshotTests|SymbolTestsCoreCoverageInvariantTests|STCoreE2ETests|STCoreTests)(/|$) -``` - -Note on tooling: in the current Swift toolchain, `swift test --list-tests` is deprecated and `swift test list` does not accept `--filter`. We therefore enumerate every test ID with `swift test list`, then apply the regex with local `grep -E` to simulate what SwiftPM's `--filter` will admit at CI time. - -- [ ] **Step 1: Confirm the SymbolTestsCore fixture framework exists** - -Run: - -``` -ls Tests/Projects/SymbolTests/DerivedData/SymbolTests/Build/Products/Release/SymbolTestsCore.framework/Versions/A/SymbolTestsCore -``` - -Expected: the path lists. If it does not, build it once before continuing: - -``` -xcodebuild \ - -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj \ - -scheme SymbolTestsCore \ - -configuration Release \ - -derivedDataPath Tests/Projects/SymbolTests/DerivedData \ - -destination 'generic/platform=macOS' \ - build -``` - -- [ ] **Step 2: Enumerate every test ID** - -Run from repo root (allow up to 10 minutes — SwiftPM may build the entire test target graph on first run): - -``` -swift test list 2>&1 | tee /tmp/macho-all-tests.txt -``` - -Expected: the file ends with hundreds of lines of the form `./` (plus some build-progress noise lines that the later grep ignores). - -- [ ] **Step 3: Apply the proposed regex and capture matches** - -Run: - -``` -grep -E '\.(SymbolTestsCoreDumpSnapshotTests|SymbolTestsCoreInterfaceSnapshotTests|SymbolTestsCoreCoverageInvariantTests|STCoreE2ETests|STCoreTests)(/|$)' /tmp/macho-all-tests.txt | tee /tmp/macho-filter-check.txt | wc -l -``` - -Expected: a non-zero count. Each line in `/tmp/macho-filter-check.txt` should be a fully qualified test ID whose class component is one of the five targets above. - -- [ ] **Step 4: Confirm `STCoreE2ETests` and `STCoreTests` are both present and distinct** - -Run: - -``` -grep -E '\.STCoreE2ETests/' /tmp/macho-filter-check.txt | head -3 -grep -E '\.STCoreTests/' /tmp/macho-filter-check.txt | head -3 -``` - -Expected: Both commands return at least one line each (the `(/|$)` anchor in the regex correctly distinguishes `STCoreTests` from `STCoreE2ETests`). - -- [ ] **Step 5: Confirm no environment-dependent classes leak through** - -Run: - -``` -grep -E '\.(DyldCache|XcodeMachOFile|MachOImage)' /tmp/macho-filter-check.txt || echo "no env-dependent classes matched" -``` - -Expected output: `no env-dependent classes matched`. - -- [ ] **Step 6: Confirm the unique class set is exactly the five expected** - -Run: - -``` -awk -F'/' '{print $1}' /tmp/macho-filter-check.txt | sort -u -``` - -Expected output (exactly these five lines, possibly in a different sort order — they will sort alphabetically): - -``` -SwiftDumpTests.SymbolTestsCoreCoverageInvariantTests -SwiftDumpTests.SymbolTestsCoreDumpSnapshotTests -SwiftInterfaceTests.STCoreE2ETests -SwiftInterfaceTests.STCoreTests -SwiftInterfaceTests.SymbolTestsCoreInterfaceSnapshotTests -``` - -If any other class name appears, **stop**: the regex is admitting something it should not. Report and let the controller decide. - -If a name from the expected set is missing, **stop**: the regex is too restrictive. Report. - -- [ ] **Step 7: No commit — verification only** - -Optionally `rm /tmp/macho-all-tests.txt /tmp/macho-filter-check.txt` to clean up. - ---- - -## Task 2: Update `.github/workflows/macOS.yml` — runner, Xcode version, and `--filter` - -**Files:** -- Modify: `.github/workflows/macOS.yml` (4 textual changes; exact line numbers may shift) - -**Goal:** Bump `macos-15` → `macos-26`, `"16.3"` → `"26.4"`, and add `--filter` to both `swift test` invocations. Leave the existing `Resolve SPM dependencies`, `Cache SymbolTests DerivedData`, `Build SymbolTestsCore fixture`, and `Upload xcodebuild logs on failure` steps untouched. - -- [ ] **Step 1: Bump the matrix runner image** - -Apply this edit: - -``` -old_string: - os: [macos-15] -new_string: - os: [macos-26] -``` - -- [ ] **Step 2: Bump the Xcode version** - -Apply this edit: - -``` -old_string: - xcode-version: ["16.3"] -new_string: - xcode-version: ["26.4"] -``` - -- [ ] **Step 3: Add `--filter` to the Debug `swift test` step** - -Apply this edit: - -``` -old_string: - - name: Build and run tests in debug mode - run: | - swift test \ - -c debug \ - --build-path .build-test-debug -new_string: - - name: Build and run tests in debug mode - run: | - swift test \ - -c debug \ - --build-path .build-test-debug \ - --filter '\.(SymbolTestsCoreDumpSnapshotTests|SymbolTestsCoreInterfaceSnapshotTests|SymbolTestsCoreCoverageInvariantTests|STCoreE2ETests|STCoreTests)(/|$)' -``` - -- [ ] **Step 4: Add `--filter` to the Release `swift test` step** - -Apply this edit: - -``` -old_string: - - name: Build and run tests in release mode - run: | - swift test \ - -c release \ - --build-path .build-test-release -new_string: - - name: Build and run tests in release mode - run: | - swift test \ - -c release \ - --build-path .build-test-release \ - --filter '\.(SymbolTestsCoreDumpSnapshotTests|SymbolTestsCoreInterfaceSnapshotTests|SymbolTestsCoreCoverageInvariantTests|STCoreE2ETests|STCoreTests)(/|$)' -``` - -- [ ] **Step 5: Visually inspect the diff** - -Run: - -``` -git diff .github/workflows/macOS.yml -``` - -Expected: Exactly the four textual changes above. No reflowing of unrelated lines, no accidental whitespace changes elsewhere. - -- [ ] **Step 6: Validate YAML parses** - -Run (Python is preinstalled on macOS): - -``` -python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/macOS.yml'))" && echo "yaml OK" -``` - -Expected output: `yaml OK`. If parsing fails, fix the indentation or quoting before continuing. - -- [ ] **Step 7: Commit** - -``` -git add .github/workflows/macOS.yml -git commit -m "$(cat <<'EOF' -ci(macOS): pin to macos-26 + Xcode 26.2 and filter to fixture tests - -Restrict swift test runs to the five SymbolTestsCore-based test classes -(SymbolTestsCoreDumpSnapshotTests, SymbolTestsCoreInterfaceSnapshotTests, -SymbolTestsCoreCoverageInvariantTests, STCoreE2ETests, STCoreTests) so -the CI runner only executes tests that do not depend on a developer- -machine environment. - -Spec: docs/superpowers/specs/2026-04-18-ci-test-filter-design.md -EOF -)" -``` - ---- - -## Task 3: Update `.github/workflows/release.yml` — runner and Xcode version - -**Files:** -- Modify: `.github/workflows/release.yml` (2 textual changes) - -**Goal:** Match the runner and Xcode version of the test workflow. No other changes. - -- [ ] **Step 1: Bump the runner image** - -Apply this edit: - -``` -old_string: - runs-on: macos-15 -new_string: - runs-on: macos-26 -``` - -- [ ] **Step 2: Bump the Xcode version** - -Apply this edit: - -``` -old_string: - xcode-version: "16.3" -new_string: - xcode-version: "26.4" -``` - -- [ ] **Step 3: Visually inspect the diff** - -Run: - -``` -git diff .github/workflows/release.yml -``` - -Expected: Exactly two changed lines (`macos-15` → `macos-26`, `"16.3"` → `"26.4"`). Nothing else. - -- [ ] **Step 4: Validate YAML parses** - -Run: - -``` -python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/release.yml'))" && echo "yaml OK" -``` - -Expected output: `yaml OK`. - -- [ ] **Step 5: Commit** - -``` -git add .github/workflows/release.yml -git commit -m "$(cat <<'EOF' -ci(release): bump runner to macos-26 + Xcode 26.2 - -Aligns the release workflow with the macOS test workflow. -EOF -)" -``` - ---- - -## Task 4: Push branch and open PR - -**Files:** none modified. - -**Goal:** Get the changes onto a remote branch and open a PR so CI runs end-to-end on a real `macos-26` runner. - -- [ ] **Step 1: Confirm branch identity and commit list** - -Run: - -``` -git log --oneline origin/main..HEAD -``` - -Expected: At least four commits on `chore/ci-only-fixture-tests`: - -1. `Add CI test filter and macOS 26.2 upgrade spec` (initial spec) -2. `Refine CI filter spec to current state and add implementation plan` (spec correction + plan) -3. `ci(macOS): pin to macos-26 + Xcode 26.2 and filter to fixture tests` -4. `ci(release): bump runner to macos-26 + Xcode 26.2` - -Plus possibly an extra spec/plan correction commit (e.g. the regex update from four to five classes once that landed). - -- [ ] **Step 2: Push the branch** - -Run: - -``` -git push -u origin chore/ci-only-fixture-tests -``` - -- [ ] **Step 3: Open the PR** - -Run: - -``` -gh pr create --title "ci: filter to SymbolTestsCore fixture tests + bump to macOS 26.2" --body "$(cat <<'EOF' -## Summary -- Restrict `swift test` runs in `.github/workflows/macOS.yml` to the five - `SymbolTestsCore`-based test classes (`SymbolTestsCoreDumpSnapshotTests`, - `SymbolTestsCoreInterfaceSnapshotTests`, - `SymbolTestsCoreCoverageInvariantTests`, `STCoreE2ETests`, `STCoreTests`). - Other tests depend on developer-machine resources (Xcode frameworks, - iOS Simulator runtimes, dyld shared cache) that don't exist on the CI - runner. -- Bump both `macOS.yml` and `release.yml` to `macos-26` + Xcode `26.2`. - -## Test plan -- [ ] CI run on this PR completes the `Build SymbolTestsCore fixture` step. -- [ ] `Build and run tests in debug mode` and `Build and run tests in release mode` each report exactly the five whitelisted test classes (visible in the test log). -- [ ] No `DyldCache*`, `Xcode*`, `MachOImage*`, or non-snapshot `*DumpTests` classes appear in the test log. - -Spec: `docs/superpowers/specs/2026-04-18-ci-test-filter-design.md` -EOF -)" -``` - -- [ ] **Step 4: Watch the CI run** - -Use the URL printed by `gh pr create`, or: - -``` -gh pr checks --watch -``` - -Expected: Both Debug and Release `swift test` steps pass. - -If a step fails, do **not** patch it blindly. Read the failure log, identify root cause, and decide whether the fix belongs in this PR (adjust filter regex, fix YAML) or is a separate concern (real test breakage on macOS 26.2). For test breakage, file a follow-up issue rather than disabling the failing test in this PR. - ---- - -## Self-Review Notes - -- **Spec coverage:** Filter regex (Tasks 1, 2), env upgrade for `macOS.yml` (Task 2), env upgrade for `release.yml` (Task 3). Fixture build is already in place — explicitly noted in the spec, no task needed. -- **No placeholders:** Every textual edit is given as a concrete `old_string`/`new_string` pair; the regex is identical across all uses. -- **Type consistency:** The five test class names are spelled identically in Tasks 1, 2, the commit message, and the PR body. The `--filter` regex is byte-identical in Steps 3 and 4 of Task 2, in Task 1 verification, and in the spec's "Filter test runs" section. - ---- - -## Implementation Outcome (2026-04-18, after CI feedback) - -The Task 1-4 commits above landed as planned and pushed to PR #65, but -the first CI runs surfaced four issues that needed unplanned follow-up -fixes. All resolved on the same branch, all included in PR #65. - -| # | Issue | Fix | Commit | -|---|---|---|---| -| 1 | `xcodebuild` failed on missing developer certificate (team `D5Q73692VW`) | Pass `CODE_SIGNING_ALLOWED=NO` build setting to `xcodebuild` | `84e695a` | -| 2 | `generic/platform=macOS` produced a universal slice; the x86_64 `.swiftinterface` failed verification (CLAUDE.md notes the project is ARM-only) | Add `ARCHS=arm64` (and `SWIFT_VERIFY_EMITTED_MODULE_INTERFACE=NO`, which turned out not to suppress the Xcode-26 explicit-module verifier) | `cdb84a4` | -| 3 | Swift 6.2.3 in Xcode 26.2 emits a `.swiftinterface` containing `nonisolated(nonsending)` then refuses to verify its own output (compiler bug) | Bump CI Xcode pin from `26.2` to `26.4` (Swift 6.3 fixed it) | `715e330` | -| 4 | `swift-demangling 0.1.0` (the remote pin) lacks `DemangleOptions.removeReferenceStoragePrefix`, breaking SwiftDump compilation in CI | Bump `Package.swift` pin from `0.1.0` to `0.1.1` (newly published) | `783656d` | -| 5 | `xcodebuild` on the CI runner emits the fixture at `DerivedData/Build/Products/Release/`, but `MachOFileName.SymbolTestsCore` reads it from `DerivedData/SymbolTests/Build/Products/Release/` (the layout xcodebuild produces locally) | Add a `Normalize SymbolTestsCore fixture path` step that finds the produced framework and symlinks it to the expected path | `90219b1` | - -CI on the macos-26 runner with Xcode 26.4 finishes in ~15 minutes and -runs exactly the five whitelisted suites in both Debug and Release -configs. No environment-dependent classes leak through. - -The `macos-26` runner had Xcode 26.4 preinstalled, so no further -setup-xcode workaround was needed. The `macOS` workflow had been in -`disabled_manually` state on origin (since 2025-10-20); it was -re-enabled during Task 4 to allow the new run to fire. diff --git a/docs/superpowers/plans/2026-05-02-generic-specializer-cleanup.md b/docs/superpowers/plans/2026-05-02-generic-specializer-cleanup.md deleted file mode 100644 index 691fb858..00000000 --- a/docs/superpowers/plans/2026-05-02-generic-specializer-cleanup.md +++ /dev/null @@ -1,894 +0,0 @@ -# GenericSpecializer Cleanup Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Apply six lightweight cleanups (#5, #6, #7, #9, #10, #12) to `GenericSpecializer` per spec `docs/superpowers/specs/2026-05-02-generic-specializer-cleanup-design.md`, expanding the public API to surface `~Copyable` / `~Escapable`, merging conditional invertible requirements, splitting muddy switch arms, fail-fast on generic candidates, allowing caller-supplied `MetadataRequest`, and removing dead code. - -**Architecture:** All changes are localised to two existing files under `Sources/SwiftInterface/GenericSpecializer/` plus the test file. No new files, no new modules, no ABI-level changes. - -**Tech Stack:** Swift 6.2+, swift-testing (`@Test` / `#expect` / `#require`), SwiftPM (`swift build` / `swift test`). - ---- - -## File Structure - -**Modified:** - -- `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift` — most edits live here: the `buildRequirement` switch, `buildParameters` second pass for invertible protocols, `findCandidates` populating `isGeneric`, `resolveCandidate` fail-fast, `specialize` signature, three call sites that read requirements, deletion of `convertLayoutKind`, two new `SpecializerError` cases. -- `Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift` — `Parameter` gains `invertibleProtocols`, `Candidate` gains `isGeneric`. -- `Tests/SwiftInterfaceTests/GenericSpecializationTests.swift` — three new `@Test` methods and one new fixture. - -**Not modified:** - -- `Sources/SwiftInterface/GenericSpecializer/ConformanceProvider.swift` — no API surface change here. -- `Sources/SwiftInterface/GenericSpecializer/Models/SpecializationSelection.swift`, - `SpecializationResult.swift`, `SpecializationValidation.swift` — untouched. - -Six tasks ordered by risk and dependency: pure deletions and comment splits first, then API additions, then the test-bearing changes that build on the API. - ---- - -## Task 1: Remove dead `convertLayoutKind` (#12) - -**Files:** - -- Modify: `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift:272-277` - -- [ ] **Step 1: Confirm zero callers** - -Run: `rg -n 'convertLayoutKind' Sources/ Tests/` -Expected output: only the definition lines (272-277) — no call sites. - -- [ ] **Step 2: Delete the function** - -Remove these exact lines from `GenericSpecializer.swift`: - -```swift - /// Convert runtime layout kind to our model - private func convertLayoutKind(_ kind: GenericRequirementLayoutKind) -> SpecializationRequest.LayoutKind { - switch kind { - case .class: - return .class - } - } -``` - -- [ ] **Step 3: Build and run the specializer test class** - -Run: `swift build 2>&1 | xcsift` -Expected: build succeeds, no warnings. - -Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests 2>&1 | xcsift` -Expected: all 11 existing tests pass (`main`, `makeRequest`, `validation`, `specialize`, `selectionBuilder`, `unconstrainedSpecialize`, `singleProtocolSpecialize`, `multiProtocolSpecialize`, `classConstraintSpecialize`, `nestedAssociatedTypeRequest`, `nestedAssociatedTypeSpecialize`, `dualAssociatedSpecialize`, `mixedConstraintsSpecialize`). - -- [ ] **Step 4: Commit** - -```bash -git add Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift -git commit -m "refactor(SwiftInterface): drop unused convertLayoutKind helper" -``` - ---- - -## Task 2: Split `sameConformance` / `sameShape` / `invertedProtocols` arms (#7) - -**Files:** - -- Modify: `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift:265-268` - -- [ ] **Step 1: Replace the combined branch** - -Replace this block in `buildRequirement`: - -```swift - case .sameConformance, .sameShape, .invertedProtocols: - // These are more advanced requirements that we don't need for basic specialization - return nil - } -``` - -with three independent arms: - -```swift - case .sameConformance: - // Derived from SameType / BaseClass; compiler forces hasKeyArgument=false, - // so it never participates in metadata accessor key arguments. - return nil - - case .sameShape: - // Pack-shape constraint between two TypePacks. Relevant only to variadic - // generics, which are out of scope for this specializer. - return nil - - case .invertedProtocols: - // Capability declaration (~Copyable / ~Escapable) — surfaced on - // Parameter.invertibleProtocols rather than as a Requirement, because - // it relaxes rather than constrains the parameter. - return nil - } -``` - -- [ ] **Step 2: Build and run tests** - -Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests 2>&1 | xcsift` -Expected: all existing tests pass — this is a comment-only change. - -- [ ] **Step 3: Commit** - -```bash -git add Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift -git commit -m "refactor(SwiftInterface): split combined nil-return requirement branch" -``` - ---- - -## Task 3: Configurable `MetadataRequest` on `specialize` (#10) - -**Files:** - -- Modify: `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift:391` (signature) and `:458` (call site). -- Test: `Tests/SwiftInterfaceTests/GenericSpecializationTests.swift` - -- [ ] **Step 1: Write the failing test** - -Append to `GenericSpecializationTests.swift`, after the `selectionBuilder` test (around line 191): - -```swift - @Test func configurableMetadataRequest() async throws { - let machO = MachOImage.current() - - let descriptor = try #require(try machO.swift.typeContextDescriptors.first { - try $0.struct?.name(in: machO).contains("TestGenericStruct") == true - }?.struct) - - let indexer = SwiftInterfaceIndexer(in: machO) - try indexer.addSubIndexer(SwiftInterfaceIndexer(in: #require(MachOImage(name: "Foundation")))) - try indexer.addSubIndexer(SwiftInterfaceIndexer(in: #require(MachOImage(name: "libswiftCore")))) - try await indexer.prepare() - - let specializer = GenericSpecializer(indexer: indexer) - let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) - - let selection: SpecializationSelection = [ - "A": .metatype([Int].self), - "B": .metatype(Double.self), - "C": .metatype(Data.self), - ] - - // Default request (existing behaviour) - let defaultResult = try specializer.specialize(request, with: selection) - let defaultOffsets = try #require(defaultResult.resolveMetadata().struct).fieldOffsets() - - // Explicit non-blocking complete request - let nonBlocking = MetadataRequest(state: .complete, isBlocking: false) - let explicitResult = try specializer.specialize( - request, - with: selection, - metadataRequest: nonBlocking - ) - let explicitOffsets = try #require(explicitResult.resolveMetadata().struct).fieldOffsets() - - #expect(defaultOffsets == [0, 8, 16]) - #expect(explicitOffsets == defaultOffsets) - } -``` - -- [ ] **Step 2: Run the test, expect a build failure** - -Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests/configurableMetadataRequest 2>&1 | xcsift` -Expected: compile error along the lines of *"extra argument 'metadataRequest' in call"* — the parameter does not exist yet. - -- [ ] **Step 3: Add the parameter to `specialize`** - -In `GenericSpecializer.swift:391` change the signature: - -```swift - public func specialize( - _ request: SpecializationRequest, - with selection: SpecializationSelection, - metadataRequest: MetadataRequest = .completeAndBlocking - ) throws -> SpecializationResult { -``` - -In the same function, find the main accessor call (currently `GenericSpecializer.swift:457-461`): - -```swift - let response = try accessorFunction( - request: .completeAndBlocking, - metadatas: metadatas, - witnessTables: witnessTables, - ) -``` - -and replace `request: .completeAndBlocking` with `request: metadataRequest`: - -```swift - let response = try accessorFunction( - request: metadataRequest, - metadatas: metadatas, - witnessTables: witnessTables, - ) -``` - -Leave `resolveCandidate` (around line 516) and `resolveAssociatedTypeStep` (around line 715) untouched — per spec §5 they keep their current internal requests. - -- [ ] **Step 4: Run the new test** - -Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests/configurableMetadataRequest 2>&1 | xcsift` -Expected: PASS. - -- [ ] **Step 5: Run the full specializer test class to confirm no regressions** - -Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests 2>&1 | xcsift` -Expected: all tests pass, including the new one. - -- [ ] **Step 6: Commit** - -```bash -git add Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift Tests/SwiftInterfaceTests/GenericSpecializationTests.swift -git commit -m "feat(SwiftInterface): allow caller-supplied MetadataRequest in specialize" -``` - ---- - -## Task 4: Merge conditional invertible protocol requirements (#6) - -**Files:** - -- Modify: `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift` — three call sites (lines 94, 285, 593). - -- [ ] **Step 1: Add the merge helper** - -Insert this private static helper inside `GenericSpecializer` (anywhere in the `extension GenericSpecializer` block that contains `buildParameters`; placing it right above `buildParameters` is clearest): - -```swift - /// All requirements visible to the specializer: the cumulative - /// `allRequirements` chain plus any conditional requirements stored - /// under `hasConditionalInvertedProtocols`. The current scope keeps - /// every candidate Copyable / Escapable, so conditional requirements - /// always evaluate active and can be merged unconditionally. - private static func mergedRequirements( - from genericContext: GenericContext - ) -> [GenericRequirementDescriptor] { - genericContext.allRequirements.flatMap { $0 } - + genericContext.conditionalInvertibleProtocolsRequirements - } -``` - -- [ ] **Step 2: Use the helper in `buildParameters`** - -In `GenericSpecializer.swift:91-97` replace: - -```swift - let requirements = try collectRequirements( - for: paramName, - from: genericContext.allRequirements.flatMap { $0 }, - parameterIndex: index, - depth: depth - ) -``` - -with: - -```swift - let requirements = try collectRequirements( - for: paramName, - from: Self.mergedRequirements(from: genericContext), - parameterIndex: index, - depth: depth - ) -``` - -- [ ] **Step 3: Use the helper in `buildAssociatedTypeRequirements`** - -In `GenericSpecializer.swift:285` replace: - -```swift - let genericRequirements = genericContext.allRequirements.flatMap { $0 } -``` - -with: - -```swift - let genericRequirements = Self.mergedRequirements(from: genericContext) -``` - -- [ ] **Step 4: Use the helper in `resolveAssociatedTypeWitnesses`** - -The third call site lives on the `MachO == MachOImage` extension (around `GenericSpecializer.swift:593`) and works with `GenericContext` already loaded into the current process via `genericContextInProcess`. Both this and the file-side `GenericContext` are the same type alias `TargetGenericContext`, so the helper applies directly. - -Replace this line: - -```swift - let requirements = try genericContextInProcess.requirements.map { try GenericRequirement(descriptor: $0) } -``` - -with: - -```swift - let requirements = try Self.mergedRequirements(from: genericContextInProcess) - .map { try GenericRequirement(descriptor: $0) } -``` - -(`mergedRequirements` is defined on `extension GenericSpecializer` without a `MachO == MachOImage` constraint, so it's reachable from both extension blocks.) - -- [ ] **Step 5: Build and run all specializer tests** - -Run: `swift build 2>&1 | xcsift` -Expected: build succeeds, no warnings. - -Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests 2>&1 | xcsift` -Expected: all existing tests still pass. None of the existing fixtures rely on `conditionalInvertibleProtocolsRequirements`, so this change is behaviour-neutral against the current suite. Direct verification is added by Task 6's fixture. - -- [ ] **Step 6: Commit** - -```bash -git add Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift -git commit -m "feat(SwiftInterface): merge conditional invertible requirements" -``` - ---- - -## Task 5: Generic-candidate fail-fast (#9) - -**Files:** - -- Modify: `Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift` — `Candidate` gains `isGeneric`. -- Modify: `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift` — new `SpecializerError` case, `findCandidates` populates `isGeneric`, `resolveCandidate` throws on generic descriptors. -- Test: `Tests/SwiftInterfaceTests/GenericSpecializationTests.swift` - -- [ ] **Step 1: Write the failing test** - -Append after the `configurableMetadataRequest` test: - -```swift - @Test func genericCandidateFailFast() async throws { - let machO = MachOImage.current() - - let descriptor = try #require(try machO.swift.typeContextDescriptors.first { - try $0.struct?.name(in: machO).contains("TestSingleProtocolStruct") == true - }?.struct) - - let indexer = SwiftInterfaceIndexer(in: machO) - try indexer.addSubIndexer(SwiftInterfaceIndexer(in: #require(MachOImage(name: "libswiftCore")))) - try await indexer.prepare() - - let specializer = GenericSpecializer(indexer: indexer) - let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) - - // Pick a generic candidate from the candidate list (e.g. Optional or Array - // — anything generic that conforms to Hashable). We deliberately do not - // assert that *some* candidate is generic in case the candidate set - // changes; we only assert the property holds for any generic ones we find. - let genericCandidate = request.parameters[0].candidates.first { $0.isGeneric } - let nonGenericCandidate = request.parameters[0].candidates.first { !$0.isGeneric } - - // At minimum the standard library exposes both shapes for Hashable. - try #require(genericCandidate != nil, "expected at least one generic candidate") - try #require(nonGenericCandidate != nil, "expected at least one non-generic candidate") - - // Non-generic candidate still resolves successfully. - let okResult = try specializer.specialize( - request, - with: ["A": .candidate(nonGenericCandidate!)] - ) - _ = try okResult.resolveMetadata() - - // Generic candidate throws the new typed error. - do { - _ = try specializer.specialize( - request, - with: ["A": .candidate(genericCandidate!)] - ) - Issue.record("expected candidateRequiresNestedSpecialization to be thrown") - } catch let GenericSpecializer.SpecializerError.candidateRequiresNestedSpecialization(candidate, parameterCount) { - #expect(candidate.typeName == genericCandidate!.typeName) - #expect(parameterCount >= 1) - } - } -``` - -- [ ] **Step 2: Run the test, expect a build failure** - -Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests/genericCandidateFailFast 2>&1 | xcsift` -Expected: compile errors — `isGeneric` is not a member of `Candidate`, `candidateRequiresNestedSpecialization` is not a `SpecializerError` case. - -- [ ] **Step 3: Add `isGeneric` to `Candidate`** - -In `Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift`, replace the existing `Candidate` declaration (currently lines 122-143): - -```swift - /// A candidate type that can be used for specialization - public struct Candidate: Sendable, Hashable { - /// Type name - public let typeName: TypeName - - /// Source of this candidate - public let source: Source - - public init( - typeName: TypeName, - source: Source, - ) { - self.typeName = typeName - self.source = source - } - - /// Source of candidate type - public enum Source: Sendable, Hashable { - case image(String) - } - } -``` - -with: - -```swift - /// A candidate type that can be used for specialization - public struct Candidate: Sendable, Hashable { - /// Type name - public let typeName: TypeName - - /// Source of this candidate - public let source: Source - - /// True when the candidate's type descriptor is itself generic. - /// Selecting such a candidate via `Argument.candidate(...)` will - /// throw `candidateRequiresNestedSpecialization` from `specialize`. - public let isGeneric: Bool - - public init( - typeName: TypeName, - source: Source, - isGeneric: Bool = false - ) { - self.typeName = typeName - self.source = source - self.isGeneric = isGeneric - } - - /// Source of candidate type - public enum Source: Sendable, Hashable { - case image(String) - } - } -``` - -(Default value `false` keeps the existing call sites in `findCandidates` source-compatible until they are updated in Step 5.) - -- [ ] **Step 4: Add the `SpecializerError` case** - -In `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift`, find the `SpecializerError` enum (currently `:800-808`). Add the new case after `case candidateResolutionFailed(...)`: - -```swift - case candidateRequiresNestedSpecialization( - candidate: SpecializationRequest.Candidate, - parameterCount: Int - ) -``` - -Add a matching `errorDescription` arm in the `switch self` block (currently `:810-829`), after the existing `candidateResolutionFailed` arm: - -```swift - case .candidateRequiresNestedSpecialization(let candidate, let parameterCount): - return "Candidate \(candidate.typeName.name) is generic with \(parameterCount) parameter(s); pass Argument.specialized(...) instead of Argument.candidate(...)" -``` - -- [ ] **Step 5: Populate `isGeneric` in `findCandidates`** - -In `GenericSpecializer.swift:311-339`, both branches currently use `guard ... != nil` and discard the type definition. Bind it instead so we can read the descriptor's flags. - -Replace the entire `findCandidates` body: - -```swift - private func findCandidates(satisfying protocols: [ProtocolName]) -> [SpecializationRequest.Candidate] { - guard !protocols.isEmpty else { - // No constraints - return all indexed types - return conformanceProvider.allTypeNames.compactMap { typeName -> SpecializationRequest.Candidate? in - guard conformanceProvider.typeDefinition(for: typeName) != nil else { - return nil - } - let imagePath = conformanceProvider.imagePath(for: typeName) ?? "" - return SpecializationRequest.Candidate( - typeName: typeName, - source: .image(imagePath) - ) - } - } - - // Find types conforming to all protocols - let conformingTypes = conformanceProvider.types(conformingToAll: protocols) - - return conformingTypes.compactMap { typeName -> SpecializationRequest.Candidate? in - guard conformanceProvider.typeDefinition(for: typeName) != nil else { - return nil - } - let imagePath = conformanceProvider.imagePath(for: typeName) ?? "" - return SpecializationRequest.Candidate( - typeName: typeName, - source: .image(imagePath) - ) - } - } -``` - -with: - -```swift - private func findCandidates(satisfying protocols: [ProtocolName]) -> [SpecializationRequest.Candidate] { - guard !protocols.isEmpty else { - // No constraints - return all indexed types - return conformanceProvider.allTypeNames.compactMap { typeName -> SpecializationRequest.Candidate? in - guard let typeDefinition = conformanceProvider.typeDefinition(for: typeName) else { - return nil - } - let imagePath = conformanceProvider.imagePath(for: typeName) ?? "" - let isGeneric = typeDefinition.type.typeContextDescriptorWrapper.typeContextDescriptor.layout.flags.isGeneric - return SpecializationRequest.Candidate( - typeName: typeName, - source: .image(imagePath), - isGeneric: isGeneric - ) - } - } - - // Find types conforming to all protocols - let conformingTypes = conformanceProvider.types(conformingToAll: protocols) - - return conformingTypes.compactMap { typeName -> SpecializationRequest.Candidate? in - guard let typeDefinition = conformanceProvider.typeDefinition(for: typeName) else { - return nil - } - let imagePath = conformanceProvider.imagePath(for: typeName) ?? "" - let isGeneric = typeDefinition.type.typeContextDescriptorWrapper.typeContextDescriptor.layout.flags.isGeneric - return SpecializationRequest.Candidate( - typeName: typeName, - source: .image(imagePath), - isGeneric: isGeneric - ) - } - } -``` - -- [ ] **Step 6: Make `resolveCandidate` fail fast on generic candidates** - -In `GenericSpecializer.swift:487-519`, replace: - -```swift - private func resolveCandidate(_ candidate: SpecializationRequest.Candidate, parameterName: String) throws -> Metadata { - // Find the type definition from indexer - guard let indexer else { - throw SpecializerError.candidateResolutionFailed( - candidate: candidate, - reason: "Indexer not available for candidate resolution" - ) - } - - // Look up type definition - guard let typeDefinitionEntry = indexer.allAllTypeDefinitions[candidate.typeName] else { - throw SpecializerError.candidateResolutionFailed( - candidate: candidate, - reason: "Type not found in indexer" - ) - } - - let typeDefinition = typeDefinitionEntry.value - - // Get accessor function from type definition's type context - let accessorFunction = try typeDefinition.type.typeContextDescriptorWrapper.typeContextDescriptor.metadataAccessorFunction(in: typeDefinitionEntry.machO) - guard let accessorFunction else { - throw SpecializerError.candidateResolutionFailed( - candidate: candidate, - reason: "Cannot get metadata accessor function" - ) - } - - // For non-generic types, just call the accessor - let response = try accessorFunction(request: .completeAndBlocking) - let wrapper = try response.value.resolve() - return try wrapper.metadata - } -``` - -with: - -```swift - private func resolveCandidate(_ candidate: SpecializationRequest.Candidate, parameterName: String) throws -> Metadata { - // Find the type definition from indexer - guard let indexer else { - throw SpecializerError.candidateResolutionFailed( - candidate: candidate, - reason: "Indexer not available for candidate resolution" - ) - } - - // Look up type definition - guard let typeDefinitionEntry = indexer.allAllTypeDefinitions[candidate.typeName] else { - throw SpecializerError.candidateResolutionFailed( - candidate: candidate, - reason: "Type not found in indexer" - ) - } - - let typeDefinition = typeDefinitionEntry.value - let typeContext = typeDefinition.type.typeContextDescriptorWrapper.typeContextDescriptor - - // Generic candidates need nested specialization; surface a typed error - // rather than letting the no-argument accessor call below fail with - // a generic message. - if let genericContext = try typeContext.genericContext(in: typeDefinitionEntry.machO) { - throw SpecializerError.candidateRequiresNestedSpecialization( - candidate: candidate, - parameterCount: Int(genericContext.header.numParams) - ) - } - - // Get accessor function from type definition's type context - let accessorFunction = try typeContext.metadataAccessorFunction(in: typeDefinitionEntry.machO) - guard let accessorFunction else { - throw SpecializerError.candidateResolutionFailed( - candidate: candidate, - reason: "Cannot get metadata accessor function" - ) - } - - // Non-generic: call accessor with no arguments - let response = try accessorFunction(request: .completeAndBlocking) - let wrapper = try response.value.resolve() - return try wrapper.metadata - } -``` - -- [ ] **Step 7: Run the new test** - -Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests/genericCandidateFailFast 2>&1 | xcsift` -Expected: PASS. - -- [ ] **Step 8: Run the full specializer test class** - -Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests 2>&1 | xcsift` -Expected: all existing tests pass, including the new one. The default `isGeneric: Bool = false` parameter on `Candidate.init` keeps any existing positional call sites compiling unchanged. - -- [ ] **Step 9: Commit** - -```bash -git add Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift Tests/SwiftInterfaceTests/GenericSpecializationTests.swift -git commit -m "feat(SwiftInterface): fail fast on generic candidates with typed error" -``` - ---- - -## Task 6: Surface `invertibleProtocols` on `Parameter` (#5) - -**Files:** - -- Modify: `Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift` — `Parameter` gains `invertibleProtocols`. -- Modify: `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift` — `buildParameters` second pass that fills the field. -- Test: `Tests/SwiftInterfaceTests/GenericSpecializationTests.swift` — new fixture and test. - -- [ ] **Step 1: Add the test fixture and failing test** - -The fixture and `@Test` method are added inside the existing `GenericSpecializationTests` class (a `final class` is itself Copyable, so nesting a `~Copyable` struct inside it is fine). The conditional `Copyable` conformance is declared in a top-level extension because conditional conformance can only be declared at file scope. - -Append inside `GenericSpecializationTests` (after the `mixedConstraintsSpecialize` test, just before the closing `}` of the class): - -```swift - // MARK: - Inverted protocols (~Copyable) - - struct TestInvertedCopyableStruct: ~Copyable { - let a: A - } - - @Test func invertedProtocolsExposed() async throws { - let machO = MachOImage.current() - - let descriptor = try #require(try machO.swift.typeContextDescriptors.first { - try $0.struct?.name(in: machO).contains("TestInvertedCopyableStruct") == true - }?.struct) - - let indexer = SwiftInterfaceIndexer(in: machO) - try await indexer.prepare() - - let specializer = GenericSpecializer(indexer: indexer) - let request = try specializer.makeRequest(for: TypeContextDescriptorWrapper.struct(descriptor)) - - #expect(request.parameters.count == 1) - - let invertible = try #require(request.parameters[0].invertibleProtocols) - // ~Copyable means the .copyable bit is not set in the surfaced set. - #expect(!invertible.contains(.copyable)) - - // Specialize with a Copyable type (Int) — the conditional Copyable - // extension makes the struct itself Copyable when A is Copyable, so - // the metadata accessor should succeed. - let result = try specializer.specialize(request, with: ["A": .metatype(Int.self)]) - let structMetadata = try #require(result.resolveMetadata().struct) - #expect(try structMetadata.fieldOffsets() == [0]) - } -``` - -Then append at file scope (after the closing `}` of the class): - -```swift -extension GenericSpecializationTests.TestInvertedCopyableStruct: Copyable where A: Copyable {} -``` - -- [ ] **Step 2: Run the test, expect a build failure** - -Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests/invertedProtocolsExposed 2>&1 | xcsift` -Expected: compile error — `invertibleProtocols` is not a member of `Parameter`. - -- [ ] **Step 3: Add `invertibleProtocols` to `Parameter`** - -In `Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift`, modify the `Parameter` struct (currently lines 36-78). The new field plus an updated initialiser: - -```swift - public struct Parameter: Sendable { - /// Parameter name (e.g., "A", "B", "A1" - based on depth and index) - public let name: String - - /// Parameter index in generic signature - public let index: Int - - /// Depth level (for nested generic contexts) - public let depth: Int - - /// Requirements on this parameter (ordered - PWT passed in this order) - public let requirements: [Requirement] - - /// Candidate types that satisfy all requirements - public var candidates: [Candidate] - - /// Invertible protocols (~Copyable / ~Escapable) that the parameter - /// declares. The set carries the bits that ARE present — e.g. - /// `` produces a set without `.copyable`. `nil` means - /// the parameter has no `invertedProtocols` requirement and retains - /// every invertible protocol by default (the typical Swift case). - public let invertibleProtocols: InvertibleProtocolSet? - - public init( - name: String, - index: Int, - depth: Int, - requirements: [Requirement], - candidates: [Candidate] = [], - invertibleProtocols: InvertibleProtocolSet? = nil - ) { - self.name = name - self.index = index - self.depth = depth - self.requirements = requirements - self.candidates = candidates - self.invertibleProtocols = invertibleProtocols - } - - /// Protocol requirements that require witness tables (in order) - public var protocolRequirements: [Requirement] { - requirements.filter { - if case .protocol = $0 { return true } - return false - } - } - - /// Whether this parameter has any protocol requirements - public var hasProtocolRequirements: Bool { - !protocolRequirements.isEmpty - } - } -``` - -`MachOSwiftSection` is already imported by this file (line 3), which exposes `InvertibleProtocolSet` — no new import needed. - -- [ ] **Step 4: Fill the field from `buildParameters`** - -In `GenericSpecializer.swift:79-120` extend the body of `buildParameters` so that after collecting requirements and candidates a second pass extracts any `.invertedProtocols` requirement targeting the parameter being constructed. - -Replace this block at the existing tail of the inner loop: - -```swift - parameters.append(SpecializationRequest.Parameter( - name: paramName, - index: index, - depth: depth, - requirements: requirements, - candidates: candidates - )) -``` - -with: - -```swift - let invertibleProtocols = Self.collectInvertibleProtocols( - for: index, - depth: depth, - in: genericContext - ) - - parameters.append(SpecializationRequest.Parameter( - name: paramName, - index: index, - depth: depth, - requirements: requirements, - candidates: candidates, - invertibleProtocols: invertibleProtocols - )) -``` - -Add the new helper as a `private static` function inside the same `extension GenericSpecializer`, just after `mergedRequirements`: - -```swift - /// Pick out the `~Copyable` / `~Escapable` declaration for the - /// generic parameter at `(depth, index)`, intersecting if multiple - /// `invertedProtocols` requirements target the same parameter. - /// Returns `nil` when no requirement targets this parameter. - private static func collectInvertibleProtocols( - for index: Int, - depth: Int, - in genericContext: GenericContext - ) -> InvertibleProtocolSet? { - // The binary stores the parameter index as a flat 16-bit value - // across all depth levels: it equals the cumulative count of - // parameters in prior depths plus the current depth's index. - let priorDepthParameterCount = genericContext.allParameters - .prefix(depth) - .reduce(0) { $0 + $1.count } - let flatIndex = UInt16(priorDepthParameterCount + index) - - var result: InvertibleProtocolSet? - for descriptor in mergedRequirements(from: genericContext) - where descriptor.layout.flags.kind == .invertedProtocols { - guard case .invertedProtocols(let inverted) = descriptor.content else { continue } - guard inverted.genericParamIndex == flatIndex else { continue } - - if let existing = result { - result = existing.intersection(inverted.protocols) - } else { - result = inverted.protocols - } - } - return result - } -``` - -- [ ] **Step 5: Run the new test** - -Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests/invertedProtocolsExposed 2>&1 | xcsift` -Expected: PASS — `invertibleProtocols` is non-`nil` for the fixture, `.copyable` is absent, specialization with `Int` succeeds. - -- [ ] **Step 6: Run the full specializer test class** - -Run: `swift test --filter SwiftInterfaceTests.GenericSpecializationTests 2>&1 | xcsift` -Expected: every test passes — the new optional field defaults to `nil` for fixtures without `invertedProtocols`, so the existing tests are unaffected. - -- [ ] **Step 7: Commit** - -```bash -git add Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift Sources/SwiftInterface/GenericSpecializer/Models/SpecializationRequest.swift Tests/SwiftInterfaceTests/GenericSpecializationTests.swift -git commit -m "feat(SwiftInterface): surface invertible protocols on Parameter" -``` - ---- - -## Done - -After Task 6 the working tree should have six commits, each scoped to one numbered fix from the spec. Final verification: - -- [ ] **Step 1: Final full-suite run** - -Run: `swift test --filter SwiftInterfaceTests 2>&1 | xcsift` -Expected: all tests in `SwiftInterfaceTests` pass. - -- [ ] **Step 2: Confirm spec coverage** - -| Spec section | Plan task | Status | -|---|---|---| -| Design §1 invertibleProtocols on Parameter | Task 6 | ✓ | -| Design §2 conditional invertible merge | Task 4 | ✓ | -| Design §3 split nil-return arms | Task 2 | ✓ | -| Design §4 generic candidate fail-fast | Task 5 | ✓ | -| Design §5 configurable MetadataRequest | Task 3 | ✓ | -| Design §6 remove dead code | Task 1 | ✓ | -| Testing §3.1 inverted protocols exposure | Task 6 step 1 | ✓ | -| Testing §3.2 fail-fast on generic candidate | Task 5 step 1 | ✓ | -| Testing §3.3 configurable MetadataRequest | Task 3 step 1 | ✓ | -| Testing §3.4 conditional invertible (e2e) | Task 6 fixture's conditional `Copyable` extension exercises §2 merge | ✓ | diff --git a/docs/superpowers/plans/2026-05-02-reading-context-api.md b/docs/superpowers/plans/2026-05-02-reading-context-api.md deleted file mode 100644 index afc27cb4..00000000 --- a/docs/superpowers/plans/2026-05-02-reading-context-api.md +++ /dev/null @@ -1,772 +0,0 @@ -# ReadingContext API Coverage Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add `ReadingContext`-based overloads to every method in `Sources/MachOSwiftSection/Models/` that currently exposes only the MachO/InProcess API pair, plus the small `runtimePointer(at:)` extension needed to express runtime-pointer-returning methods. - -**Architecture:** Mechanical mirroring per the substitution table in the design doc. New overloads sit in `// MARK: - ReadingContext Support` sections. Existing MachO/InProcess code is left untouched. One small protocol extension (`runtimePointer(at:)`) is added to `MachOReading` to support `metadataAccessorFunction`. - -**Tech Stack:** Swift 6.2+ / Xcode 26.0+, Swift Package Manager. Build via `swift build`, test via `swift test`. Reference doc: `docs/superpowers/specs/2026-05-02-reading-context-api-design.md`. - ---- - -## Working agreements (apply to every task) - -These are rules the implementer follows for every batch — the per-task steps below assume these: - -- **Reuse the per-method pattern** documented in the design doc, section "Pattern for the common case". The substitution table is the contract; do not invent a new style. -- **Place new overloads in a dedicated `// MARK: - ReadingContext Support` extension** at the bottom of the file. If the file already has one, append to it. -- **Do not modify existing MachO or InProcess code** in any task except Task 1. -- **Mirror the MachO overload exactly:** same return type, same nullability, same `throws`, same parent helpers (e.g. `try someMethod(in: context)` instead of `try someMethod(in: machO)`). -- **Local offset arithmetic on `Int`** (`currentOffset.offset(of:)`, `currentOffset.align(to:)`, `currentOffset += ...`) stays untouched. Only the *read site* uses `try context.addressFromOffset(currentOffset)`. -- **For partial files** (already have some `` methods), add only the methods that are *missing* relative to the file's MachO API surface. Do not duplicate existing ReadingContext methods. -- **Build after every batch:** `swift package update && swift build 2>&1 | xcsift`. Must succeed before commit. -- **Commit message convention:** `feat(MachOSwiftSection): add ReadingContext API for ` for content batches; `feat(MachOReading): add runtimePointer extension` for Task 1. -- **One commit per batch.** Do not stack multiple batches in one commit. - ---- - -## Task 1: Add `runtimePointer(at:)` extension to `MachOReading` - -**Files:** -- Modify: `Sources/MachOReading/ReadingContext/ReadingContext.swift` (append extension at bottom) -- Modify: `Sources/MachOReading/ReadingContext/MachOContext.swift` (append extension) -- Modify: `Sources/MachOReading/ReadingContext/InProcessContext.swift` (append extension) - -- [x] **Step 1: Read the three target files end-to-end** - -Read all three files completely so the extension placement matches existing style (imports, doc comment style, ordering). - -```bash -# verify imports — MachOContext already imports MachOKit, so MachOImage is in scope -grep -n "import" Sources/MachOReading/ReadingContext/MachOContext.swift -``` - -- [x] **Step 2: Append default extension to `ReadingContext.swift`** - -After the existing `extension ReadingContext { ... bindRebaseResolver default ... }` block, add: - -```swift -extension ReadingContext { - /// Converts a context-specific address to a runtime `UnsafeRawPointer`, - /// when this context is mapped into the current process. - /// - /// - `InProcessContext`: returns the address itself (already a pointer). - /// - `MachOContext`: returns `machO.ptr + address`. - /// - `MachOContext` / other readers: returns `nil`. - /// - /// This is an extension method (not a protocol requirement) so adding - /// new `ReadingContext` conformers does not become a breaking change. - /// Concrete contexts that *can* vend a runtime pointer override this in - /// their own files. - public func runtimePointer(at address: Address) throws -> UnsafeRawPointer? { - nil - } -} -``` - -- [x] **Step 3: Append override to `MachOContext.swift`** - -At the bottom of the file (after the `Convenience Extensions` MARK): - -```swift -// MARK: - Runtime Pointer Support - -extension MachOContext { - /// Returns the runtime pointer for the given file offset when the - /// underlying reader is a `MachOImage` mapped into the current process. - /// Returns `nil` for `MachOFile` and other non-resident readers. - public func runtimePointer(at address: Int) throws -> UnsafeRawPointer? { - if let machOImage = machO as? MachOImage { - return machOImage.ptr + UnsafeRawPointer.Stride(address) - } - return nil - } -} -``` - -- [x] **Step 4: Append override to `InProcessContext.swift`** - -At the bottom of the file: - -```swift -// MARK: - Runtime Pointer Support - -extension InProcessContext { - /// In-process addresses are already runtime pointers, so this returns - /// the address unchanged. - public func runtimePointer(at address: UnsafeRawPointer) throws -> UnsafeRawPointer? { - address - } -} -``` - -- [x] **Step 5: Build** - -```bash -swift package update && swift build 2>&1 | xcsift -``` - -Expected: build succeeds. - -- [x] **Step 6: Commit** - -```bash -git add Sources/MachOReading/ReadingContext/ReadingContext.swift \ - Sources/MachOReading/ReadingContext/MachOContext.swift \ - Sources/MachOReading/ReadingContext/InProcessContext.swift -git commit -m "feat(MachOReading): add runtimePointer extension for ReadingContext - -Default returns nil. MachOContext returns machO.ptr + address when the -underlying reader is a MachOImage; InProcessContext returns the address -itself. Enables runtime-pointer-returning methods (e.g. -metadataAccessorFunction) to be expressed under the unified ReadingContext -abstraction without per-call type dispatch." -``` - ---- - -## Task 2: `Anonymous/`, `Module/`, `Extension/` (top-level Context wrappers) - -**Files:** -- Modify: `Sources/MachOSwiftSection/Models/Anonymous/AnonymousContext.swift` -- Modify: `Sources/MachOSwiftSection/Models/Module/ModuleContext.swift` -- Modify: `Sources/MachOSwiftSection/Models/Extension/ExtensionContext.swift` - -These three files each have a MachO `init(descriptor:in:)` and an InProcess `init(descriptor:)`. Add a third `init(descriptor:in:Context)` mirroring the MachO version. - -- [x] **Step 1: Add `init(descriptor:in:Context)` to `AnonymousContext.swift`** - -Append at bottom: - -```swift -extension AnonymousContext { - public init(descriptor: AnonymousContextDescriptor, in context: Context) throws { - self.descriptor = descriptor - var currentOffset = descriptor.offset + descriptor.layoutSize - - let genericContext = try descriptor.genericContext(in: context) - if let genericContext { - currentOffset += genericContext.size - } - self.genericContext = genericContext - - if descriptor.hasMangledName { - let mangledNamePointerAddress = try context.addressFromOffset(currentOffset) - let mangledNamePointer: RelativeDirectPointer = try context.readElement(at: mangledNamePointerAddress) - self.mangledName = try mangledNamePointer.resolve(at: mangledNamePointerAddress, in: context) - currentOffset += MemoryLayout>.size - } else { - self.mangledName = nil - } - } -} -``` - -- [x] **Step 2: Add `init(descriptor:in:Context)` to `ModuleContext.swift`** - -Append at bottom: - -```swift -extension ModuleContext { - public init(descriptor: ModuleContextDescriptor, in context: Context) throws { - self.descriptor = descriptor - self.name = try descriptor.name(in: context) - } -} -``` - -(`name(in: Context)` already exists on `NamedContextDescriptorProtocol`.) - -- [x] **Step 3: Add `init(descriptor:in:Context)` to `ExtensionContext.swift`** - -Append at bottom: - -```swift -extension ExtensionContext { - public init(descriptor: ExtensionContextDescriptor, in context: Context) throws { - self.descriptor = descriptor - self.extendedContextMangledName = try descriptor.extendedContext(in: context) - self.genericContext = try descriptor.genericContext(in: context) - } -} -``` - -If `extendedContext(in: Context)` does not yet exist on `ExtensionContextDescriptor`, add it first inside `Sources/MachOSwiftSection/Models/Extension/ExtensionContextDescriptor.swift` mirroring the MachO version, then complete this step. - -- [x] **Step 4: Build** - -```bash -swift build 2>&1 | xcsift -``` - -- [x] **Step 5: Commit** - -```bash -git add Sources/MachOSwiftSection/Models/Anonymous/AnonymousContext.swift \ - Sources/MachOSwiftSection/Models/Module/ModuleContext.swift \ - Sources/MachOSwiftSection/Models/Extension/ExtensionContext.swift \ - Sources/MachOSwiftSection/Models/Extension/ExtensionContextDescriptor.swift -git commit -m "feat(MachOSwiftSection): add ReadingContext API for top-level contexts - -Mirror the MachO init(descriptor:in:) overloads on AnonymousContext, -ModuleContext, and ExtensionContext under the ReadingContext abstraction." -``` - ---- - -## Task 3: `ContextDescriptor/` (`ContextProtocol.swift`, `ContextWrapper.swift`) - -**Files:** -- Modify: `Sources/MachOSwiftSection/Models/ContextDescriptor/ContextProtocol.swift` -- Modify: `Sources/MachOSwiftSection/Models/ContextDescriptor/ContextWrapper.swift` - -`ContextProtocol` exposes `parent(in: machO)`. `ContextWrapper` exposes `parent(in: machO)` and `forContextDescriptorWrapper(_:in:)`. Mirror both. - -- [x] **Step 1: Add ReadingContext extension to `ContextProtocol.swift`** - -Append at bottom: - -```swift -// MARK: - ReadingContext Support - -extension ContextProtocol { - public func parent(in context: Context) throws -> SymbolOrElement? { - try descriptor.parent(in: context)?.map { try ContextWrapper.forContextDescriptorWrapper($0, in: context) } - } -} -``` - -- [x] **Step 2: Add ReadingContext methods to `ContextWrapper.swift`** - -Append at bottom (after the existing `parent()` method): - -```swift -// MARK: - ReadingContext Support - -extension ContextWrapper { - public static func forContextDescriptorWrapper(_ contextDescriptorWrapper: ContextDescriptorWrapper, in context: Context) throws -> Self { - switch contextDescriptorWrapper { - case .type(let typeContextDescriptorWrapper): - switch typeContextDescriptorWrapper { - case .enum(let enumDescriptor): - return try .type(.enum(.init(descriptor: enumDescriptor, in: context))) - case .struct(let structDescriptor): - return try .type(.struct(.init(descriptor: structDescriptor, in: context))) - case .class(let classDescriptor): - return try .type(.class(.init(descriptor: classDescriptor, in: context))) - } - case .protocol(let protocolDescriptor): - return try .protocol(.init(descriptor: protocolDescriptor, in: context)) - case .anonymous(let anonymousContextDescriptor): - return try .anonymous(.init(descriptor: anonymousContextDescriptor, in: context)) - case .extension(let extensionContextDescriptor): - return try .extension(.init(descriptor: extensionContextDescriptor, in: context)) - case .module(let moduleContextDescriptor): - return try .module(.init(descriptor: moduleContextDescriptor, in: context)) - case .opaqueType(let opaqueTypeDescriptor): - return try .opaqueType(.init(descriptor: opaqueTypeDescriptor, in: context)) - } - } - - public func parent(in context: Context) throws -> SymbolOrElement? { - switch self { - case .type(let typeWrapper): - switch typeWrapper { - case .enum(let `enum`): - return try `enum`.descriptor.parent(in: context)?.map { try ContextWrapper.forContextDescriptorWrapper($0, in: context) } - case .struct(let `struct`): - return try `struct`.descriptor.parent(in: context)?.map { try ContextWrapper.forContextDescriptorWrapper($0, in: context) } - case .class(let `class`): - return try `class`.descriptor.parent(in: context)?.map { try ContextWrapper.forContextDescriptorWrapper($0, in: context) } - } - case .protocol(let `protocol`): - return try `protocol`.descriptor.parent(in: context)?.map { try ContextWrapper.forContextDescriptorWrapper($0, in: context) } - case .anonymous(let anonymousContext): - return try anonymousContext.descriptor.parent(in: context)?.map { try ContextWrapper.forContextDescriptorWrapper($0, in: context) } - case .extension(let extensionContext): - return try extensionContext.descriptor.parent(in: context)?.map { try ContextWrapper.forContextDescriptorWrapper($0, in: context) } - case .module(let moduleContext): - return try moduleContext.descriptor.parent(in: context)?.map { try ContextWrapper.forContextDescriptorWrapper($0, in: context) } - case .opaqueType(let opaqueType): - return try opaqueType.descriptor.parent(in: context)?.map { try ContextWrapper.forContextDescriptorWrapper($0, in: context) } - } - } -} -``` - -`ContextWrapper.forContextDescriptorWrapper(_:in:Context)` depends on every concrete Context type having a `init(descriptor:in:Context)` overload. Tasks 2, 4, 5, 6, 7 add those; this task can compile only after they all land — **so move this task's commit to be the last commit in the series** (after Task 7), or split into two: stub today, fill in once the dependents land. - -**Recommended:** Land *only* the `parent(in:context:)` portion now (which depends on `descriptor.parent(in:context:)` already provided by `ContextDescriptorProtocol`), and defer `forContextDescriptorWrapper(_:in:context:)` to a later task (Task 11) once all `init(descriptor:in:context:)` overloads exist. - -- [x] **Step 3: Build** - -```bash -swift build 2>&1 | xcsift -``` - -- [x] **Step 4: Commit** - -```bash -git add Sources/MachOSwiftSection/Models/ContextDescriptor/ContextProtocol.swift \ - Sources/MachOSwiftSection/Models/ContextDescriptor/ContextWrapper.swift -git commit -m "feat(MachOSwiftSection): add ReadingContext API for ContextProtocol/ContextWrapper - -Adds parent(in:context:) on both. forContextDescriptorWrapper(_:in:context:) -is deferred until concrete Context types have their ReadingContext init -overloads (Task 11)." -``` - -Combined with Task 11 below into a single commit `d5d1d74` since the dependency only resolves once all concrete `init(descriptor:in:Context)` overloads exist. - ---- - -## Task 4: `Type/Class/` (descriptor + class type + class metadata protocols + method descriptors) - -**Files:** -- Modify: `Sources/MachOSwiftSection/Models/Type/Class/Class.swift` -- Modify: `Sources/MachOSwiftSection/Models/Type/Class/ClassDescriptor.swift` -- Modify: `Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadata/AnyClassMetadataProtocol.swift` -- Modify: `Sources/MachOSwiftSection/Models/Type/Class/Metadata/AnyClassMetadataObjCInterop/AnyClassMetadataObjCInteropProtocol.swift` -- Modify: `Sources/MachOSwiftSection/Models/Type/Class/Metadata/FinalClassMetadataProtocol.swift` -- Modify: `Sources/MachOSwiftSection/Models/Type/Class/Method/MethodDescriptor.swift` -- Modify: `Sources/MachOSwiftSection/Models/Type/Class/Method/MethodOverrideDescriptor.swift` -- Modify: `Sources/MachOSwiftSection/Models/Type/Class/Method/MethodDefaultOverrideDescriptor.swift` - -- [x] **Step 1: Read each file end-to-end** - -For each of the 8 files, list every method with a `(in machO: MachO)` signature. Those are the methods that need a sibling `(in context: Context)` overload. - -- [x] **Step 2: Add ReadingContext overloads to each file** - -Apply the substitution table from the design doc to each MachO method. Place new overloads in a `// MARK: - ReadingContext Support` extension at the bottom of each file. - -For `Class.swift` (the highest-level wrapper): mirror the `init(descriptor:in:)` and any other MachO-parameterized methods, calling `.someMethod(in: context)` for nested calls instead of `.someMethod(in: machO)`. - -For `ClassDescriptor.swift`: mirror every descriptor method one-by-one. - -For metadata protocols: mirror every method that reads through `machO`. - -- [x] **Step 3: Build** - -```bash -swift build 2>&1 | xcsift -``` - -- [x] **Step 4: Commit** - -```bash -git add Sources/MachOSwiftSection/Models/Type/Class/ -git commit -m "feat(MachOSwiftSection): add ReadingContext API for class types - -Mirror the MachO overloads on Class, ClassDescriptor, the AnyClassMetadata -and FinalClassMetadata protocols, and the Method*Descriptor types. Call -sites pass the context through to nested methods so the unified path is -end-to-end." -``` - ---- - -## Task 5: `Type/Enum/`, `Type/Struct/` - -**Files:** -- Modify: `Sources/MachOSwiftSection/Models/Type/Enum/Enum.swift` -- Modify: `Sources/MachOSwiftSection/Models/Type/Enum/Metadata/EnumMetadataProtocol.swift` -- Modify: `Sources/MachOSwiftSection/Models/Type/Enum/MultiPayloadEnumDescriptor.swift` -- Modify: `Sources/MachOSwiftSection/Models/Type/Struct/Struct.swift` -- Modify: `Sources/MachOSwiftSection/Models/Type/Struct/StructMetadataProtocol.swift` - -- [x] **Step 1: Add ReadingContext overloads to each file** - -For each file, list every method whose signature uses `(in machO: MachO)` and add a sibling `(in context: Context)` overload using the substitution table from the design doc. Place new overloads in a `// MARK: - ReadingContext Support` extension at the bottom of each file. - -Specifics for this batch: -- `Enum.swift` and `Struct.swift`: mirror `init(descriptor:in:)` so the value-type wrappers can be built from a `ReadingContext`. Pass `in: context` to all nested `descriptor.someMethod(in: ...)` calls. -- `EnumMetadataProtocol` and `StructMetadataProtocol`: mirror metadata-reading methods (e.g. `payloadCases`, `typeDescriptor`). -- `MultiPayloadEnumDescriptor`: mirror payload-tag and case helpers. - -- [x] **Step 2: Build** - -```bash -swift build 2>&1 | xcsift -``` - -- [x] **Step 3: Commit** - -```bash -git add Sources/MachOSwiftSection/Models/Type/Enum/ \ - Sources/MachOSwiftSection/Models/Type/Struct/ -git commit -m "feat(MachOSwiftSection): add ReadingContext API for enum/struct types - -Mirror init(descriptor:in:) and metadata helpers on Enum, Struct, -EnumMetadataProtocol, StructMetadataProtocol, and -MultiPayloadEnumDescriptor." -``` - ---- - -## Task 6: `Type/` root files (descriptor, references, value metadata) - -**Files:** -- Modify: `Sources/MachOSwiftSection/Models/Type/TypeContextDescriptor.swift` -- Modify: `Sources/MachOSwiftSection/Models/Type/TypeContextWrapper.swift` -- Modify: `Sources/MachOSwiftSection/Models/Type/TypeReference.swift` -- Modify: `Sources/MachOSwiftSection/Models/Type/ValueMetadataProtocol.swift` -- Modify (partial top-up): `Sources/MachOSwiftSection/Models/Type/TypeContextDescriptorProtocol.swift` - -- [x] **Step 1: Add ReadingContext overloads to the four uncovered files** - -Same procedure as prior batches. - -- [x] **Step 2: Top up `TypeContextDescriptorProtocol.swift`** - -The file already has `genericContext(in:Context)` and `typeGenericContext(in:Context)`. Add the missing `fieldDescriptor(in:Context)` and `metadataAccessorFunction(in:Context)`: - -```swift -extension TypeContextDescriptorProtocol { - public func fieldDescriptor(in context: Context) throws -> FieldDescriptor { - let address = try context.addressFromOffset(offset + layout.offset(of: .fieldDescriptor)) - return try layout.fieldDescriptor.resolve(at: address, in: context) - } - - public func metadataAccessorFunction(in context: Context) throws -> MetadataAccessorFunction? { - let fieldAddress = try context.addressFromOffset(offset + layout.offset(of: .accessFunctionPtr)) - let relativeOffset: Int32 = try context.readElement(at: fieldAddress) - let targetAddress = context.advanceAddress(fieldAddress, by: Int(relativeOffset)) - return try context.runtimePointer(at: targetAddress).map { MetadataAccessorFunction(ptr: $0) } - } -} -``` - -The `metadataAccessorFunction(in:Context)` returns `nil` for `MachOContext` (matching the existing MachO overload), and returns the function pointer for `InProcessContext` and `MachOContext`. - -- [x] **Step 3: Build** - -```bash -swift build 2>&1 | xcsift -``` - -- [x] **Step 4: Commit** - -```bash -git add Sources/MachOSwiftSection/Models/Type/TypeContextDescriptor.swift \ - Sources/MachOSwiftSection/Models/Type/TypeContextWrapper.swift \ - Sources/MachOSwiftSection/Models/Type/TypeReference.swift \ - Sources/MachOSwiftSection/Models/Type/ValueMetadataProtocol.swift \ - Sources/MachOSwiftSection/Models/Type/TypeContextDescriptorProtocol.swift -git commit -m "feat(MachOSwiftSection): add ReadingContext API for Type root descriptors - -Mirror MachO overloads on TypeContextDescriptor, TypeContextWrapper, -TypeReference, and ValueMetadataProtocol. Add the missing -fieldDescriptor(in:context:) and metadataAccessorFunction(in:context:) -overloads on TypeContextDescriptorProtocol — the latter uses the new -runtimePointer(at:) extension to return the function pointer for -InProcess and MachOImage contexts and nil for MachOFile. - -Also retarget EnumMetadataProtocol.enumDescriptor and -StructMetadataProtocol.structDescriptor to call descriptor(in:context:) -now that the helper exists." -``` - ---- - -## Task 7: `Protocol/`, `ProtocolConformance/` - -**Files:** -- Modify: `Sources/MachOSwiftSection/Models/Protocol/Protocol.swift` -- Modify: `Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptor.swift` -- Modify: `Sources/MachOSwiftSection/Models/Protocol/ProtocolDescriptorRef.swift` -- Modify: `Sources/MachOSwiftSection/Models/Protocol/ProtocolRecord.swift` -- Modify: `Sources/MachOSwiftSection/Models/Protocol/ProtocolRequirement.swift` -- Modify: `Sources/MachOSwiftSection/Models/Protocol/ResilientWitness.swift` -- Modify: `Sources/MachOSwiftSection/Models/ProtocolConformance/GlobalActorReference.swift` -- Modify: `Sources/MachOSwiftSection/Models/ProtocolConformance/ProtocolConformance.swift` -- Modify: `Sources/MachOSwiftSection/Models/ProtocolConformance/ProtocolConformanceDescriptor.swift` - -- [x] **Step 1: Add ReadingContext overloads to each file** - -For each file, list every method whose signature uses `(in machO: MachO)` and add a sibling `(in context: Context)` overload using the substitution table from the design doc. Place new overloads in a `// MARK: - ReadingContext Support` extension at the bottom of each file. - -Note: `Protocol.swift` and `ProtocolConformance.swift` are the highest-level wrappers — their `init(descriptor:in:Context)` overloads are required by Task 11 (`ContextWrapper.forContextDescriptorWrapper(_:in:context:)`), so do not skip them. - -- [x] **Step 2: Build** - -```bash -swift build 2>&1 | xcsift -``` - -- [x] **Step 3: Commit** - -```bash -git add Sources/MachOSwiftSection/Models/Protocol/ \ - Sources/MachOSwiftSection/Models/ProtocolConformance/ -git commit -m "feat(MachOSwiftSection): add ReadingContext API for protocol/conformance types - -Mirror MachO overloads on Protocol, ProtocolDescriptor, ProtocolDescriptorRef, -ProtocolRecord, ProtocolRequirement, ResilientWitness, GlobalActorReference, -ProtocolConformance, and ProtocolConformanceDescriptor." -``` - -The implementation also added two prerequisite mirrors needed by `Protocol.swift` and `ProtocolDescriptorRef.swift`: -- `GenericRequirement.init` (would otherwise be in Task 8). -- `ObjCProtocolPrefix.name` (a partial file from the Task 12 audit list). - ---- - -## Task 8: `Generic/`, `FieldDescriptor/`, `FieldRecord/`, `AssociatedType/` - -**Files:** -- Modify: `Sources/MachOSwiftSection/Models/Generic/GenericRequirement.swift` -- Modify: `Sources/MachOSwiftSection/Models/FieldDescriptor/FieldDescriptor.swift` -- Modify: `Sources/MachOSwiftSection/Models/FieldRecord/FieldRecord.swift` -- Modify: `Sources/MachOSwiftSection/Models/AssociatedType/AssociatedType.swift` -- Modify: `Sources/MachOSwiftSection/Models/AssociatedType/AssociatedTypeDescriptor.swift` -- Modify: `Sources/MachOSwiftSection/Models/AssociatedType/AssociatedTypeRecord.swift` - -- [x] **Step 1: Add ReadingContext overloads to each file** - -For each file, list every method whose signature uses `(in machO: MachO)` and add a sibling `(in context: Context)` overload using the substitution table from the design doc. Place new overloads in a `// MARK: - ReadingContext Support` extension at the bottom of each file. - -Note: `GenericRequirement.swift` only needs an `init(descriptor:in:Context)` mirror — `paramMangledName(in:Context)` and `resolvedContent(in:Context)` already exist on `GenericRequirementDescriptor` (one of the partial files). Already added in Task 7 as a prerequisite, so this batch covers only the field/associated-type files. - -- [x] **Step 2: Build** - -```bash -swift build 2>&1 | xcsift -``` - -- [x] **Step 3: Commit** - -```bash -git add Sources/MachOSwiftSection/Models/FieldDescriptor/ \ - Sources/MachOSwiftSection/Models/FieldRecord/ \ - Sources/MachOSwiftSection/Models/AssociatedType/ -git commit -m "feat(MachOSwiftSection): add ReadingContext API for generic/field/associated-type - -Mirror MachO overloads on FieldDescriptor, FieldRecord, and the -AssociatedType family. - -GenericRequirement.init was added as a prerequisite in the -preceding protocol/conformance batch." -``` - ---- - -## Task 9: `Metadata/` (protocols and wrappers) - -**Files:** -- Modify: `Sources/MachOSwiftSection/Models/Metadata/MetadataProtocol.swift` -- Modify: `Sources/MachOSwiftSection/Models/Metadata/MetadataWrapper.swift` - -- [x] **Step 1: Add ReadingContext overloads to each file** - -For each file, list every method whose signature uses `(in machO: MachO)` and add a sibling `(in context: Context)` overload using the substitution table from the design doc. Place new overloads in a `// MARK: - ReadingContext Support` extension at the bottom of each file. - -Specifics for this batch: -- `MetadataProtocol`: mirror metadata-reading methods (e.g. `valueWitnessTable`, `typeContextDescriptor`, kind-specific accessors). -- `MetadataWrapper`: mirror the static `forMetadata(_:in:)` factory (the switch over metadata kinds) and any wrapper-level methods that take `in: machO`. - -- [x] **Step 2: Build** - -```bash -swift build 2>&1 | xcsift -``` - -- [x] **Step 3: Commit** - -```bash -git add Sources/MachOSwiftSection/Models/Metadata/MetadataProtocol.swift \ - Sources/MachOSwiftSection/Models/Metadata/MetadataWrapper.swift -git commit -m "feat(MachOSwiftSection): add ReadingContext API for Metadata protocol/wrapper" -``` - ---- - -## Task 10: `ExistentialType/`, `ForeignType/`, `TupleType/`, `OpaqueType/`, `BuiltinType/` - -**Files:** -- Modify: `Sources/MachOSwiftSection/Models/ExistentialType/ExistentialTypeMetadata.swift` -- Modify: `Sources/MachOSwiftSection/Models/ForeignType/ForeignClassMetadata.swift` -- Modify: `Sources/MachOSwiftSection/Models/ForeignType/ForeignReferenceTypeMetadata.swift` -- Modify: `Sources/MachOSwiftSection/Models/TupleType/TupleTypeMetadata.swift` -- Modify: `Sources/MachOSwiftSection/Models/OpaqueType/OpaqueType.swift` -- Modify: `Sources/MachOSwiftSection/Models/BuiltinType/BuiltinType.swift` -- Modify: `Sources/MachOSwiftSection/Models/BuiltinType/BuiltinTypeDescriptor.swift` - -- [ ] **Step 1: Add ReadingContext overloads to each file** - -For each file, list every method whose signature uses `(in machO: MachO)` and add a sibling `(in context: Context)` overload using the substitution table from the design doc. Place new overloads in a `// MARK: - ReadingContext Support` extension at the bottom of each file. - -These are the remaining metadata wrappers — most have one to three methods each. Read each file end-to-end before mirroring. - -- [x] **Step 2: Build** - -```bash -swift build 2>&1 | xcsift -``` - -- [x] **Step 3: Commit** - -```bash -git add Sources/MachOSwiftSection/Models/ExistentialType/ \ - Sources/MachOSwiftSection/Models/TupleType/ \ - Sources/MachOSwiftSection/Models/OpaqueType/ \ - Sources/MachOSwiftSection/Models/BuiltinType/ -git commit -m "feat(MachOSwiftSection): add ReadingContext API for remaining metadata types - -Mirror MachO overloads on ExistentialTypeMetadata, TupleTypeMetadata, -OpaqueType, BuiltinType, and BuiltinTypeDescriptor. - -ForeignClassMetadata.classDescriptor and -ForeignReferenceTypeMetadata.classDescriptor were added as prerequisites -in the preceding Metadata batch." -``` - ---- - -## Task 11: `ContextWrapper.forContextDescriptorWrapper(_:in:context:)` - -**Files:** -- Modify: `Sources/MachOSwiftSection/Models/ContextDescriptor/ContextWrapper.swift` - -This was deferred from Task 3 because it depends on every concrete Context type having a `init(descriptor:in:Context)` overload, which Tasks 2/4/5/6/7 add. - -- [x] **Step 1: Add `forContextDescriptorWrapper(_:in:Context)` to `ContextWrapper.swift`** - -Inside the existing `// MARK: - ReadingContext Support` extension (added in Task 3), add: - -```swift -public static func forContextDescriptorWrapper(_ contextDescriptorWrapper: ContextDescriptorWrapper, in context: Context) throws -> Self { - switch contextDescriptorWrapper { - case .type(let typeContextDescriptorWrapper): - switch typeContextDescriptorWrapper { - case .enum(let enumDescriptor): - return try .type(.enum(.init(descriptor: enumDescriptor, in: context))) - case .struct(let structDescriptor): - return try .type(.struct(.init(descriptor: structDescriptor, in: context))) - case .class(let classDescriptor): - return try .type(.class(.init(descriptor: classDescriptor, in: context))) - } - case .protocol(let protocolDescriptor): - return try .protocol(.init(descriptor: protocolDescriptor, in: context)) - case .anonymous(let anonymousContextDescriptor): - return try .anonymous(.init(descriptor: anonymousContextDescriptor, in: context)) - case .extension(let extensionContextDescriptor): - return try .extension(.init(descriptor: extensionContextDescriptor, in: context)) - case .module(let moduleContextDescriptor): - return try .module(.init(descriptor: moduleContextDescriptor, in: context)) - case .opaqueType(let opaqueTypeDescriptor): - return try .opaqueType(.init(descriptor: opaqueTypeDescriptor, in: context)) - } -} -``` - -Also retrofit the `parent(in:context:)` method added in Task 3 to call this new helper instead of inlining it (search for the eight `forContextDescriptorWrapper($0, in: context)` call sites — they should already be calling this name; this step just adds the actual implementation). - -- [x] **Step 2: Build** - -```bash -swift build 2>&1 | xcsift -``` - -- [x] **Step 3: Commit** - -Combined with Task 3 above into a single commit `d5d1d74` with body: - -``` -feat(MachOSwiftSection): wire ContextProtocol/ContextWrapper for ReadingContext - -Add parent(in:context:) on ContextProtocol and ContextWrapper, and the -forContextDescriptorWrapper(_:in:context:) static factory on ContextWrapper. - -These three methods were deferred from earlier batches because -forContextDescriptorWrapper(_:in:context:) depends on every concrete -Context type having an init(descriptor:in:context:) overload — and those -landed across Tasks 2, 4, 5, 6, and 7. Now that all dependencies exist, -this commit closes the dependency and completes the parent traversal API -under the unified ReadingContext abstraction. -``` - ---- - -## Task 12: Audit partial files, sweep for missed methods, full test pass - -**Files:** -- Audit (no expected modifications): all 15 files that already had partial ReadingContext support, listed below. - -The 15 partially-implemented files were: - -``` -Models/Anonymous/AnonymousContextDescriptorProtocol.swift -Models/ContextDescriptor/ContextDescriptorProtocol.swift -Models/ContextDescriptor/ContextDescriptorWrapper.swift -Models/ContextDescriptor/NamedContextDescriptorProtocol.swift -Models/ExistentialType/ExtendedExistentialTypeShape.swift -Models/ExistentialType/NonUniqueExtendedExistentialTypeShape.swift -Models/Extension/ExtensionContextDescriptor.swift -Models/Generic/GenericContext.swift -Models/Generic/GenericRequirementDescriptor.swift -Models/Mangling/MangledName.swift -Models/Protocol/ObjC/ObjCProtocolPrefix.swift -Models/Protocol/ObjC/RelativeObjCProtocolPrefix.swift -Models/Type/TypeContextDescriptorProtocol.swift (already topped up in Task 6) -Models/Type/TypeContextDescriptorWrapper.swift -Models/Type/TypeMetadataRecord.swift -``` - -- [x] **Step 1: Per-file audit** - -For each of the 15 files, list its `(in machO: MachO)` methods and its `(in context: Context)` methods. Any MachO method without a sibling ReadingContext method is a gap — add it using the same substitution table. Skip files with no gap. - -Run this command to surface gaps quickly: - -```bash -for f in $(grep -l "Context: ReadingContext" Sources/MachOSwiftSection/Models/ -r); do - echo "=== $f ===" - grep -E "func .*<(MachO|Context):" "$f" | sed -E 's/.*func ([a-zA-Z]+)<(MachO|Context).*/\1 \2/' -done -``` - -Look for method names that appear with `MachO` but not with `Context`. - -- [x] **Step 2: Add missing ReadingContext overloads found in Step 1** - -Apply the substitution table. If no gaps were found, this step is a no-op. - -- [x] **Step 3: Full test pass** - -```bash -swift package update && swift test 2>&1 | xcsift -``` - -Expected: existing tests pass (`MachOSwiftSectionTests`, `DemanglingTests`, `SwiftDumpTests`, `SwiftInterfaceTests`). No new tests are introduced by this work. - -If a test fails: read the failure carefully. The most likely cause is a typo in a substitution (e.g. forgot `try`, wrong type cast). Fix in place and re-run. - -- [x] **Step 4: Final coverage check** - -Run the gap query from the design doc one more time: - -```bash -grep -lL "ReadingContext" $(grep -l "MachOSwiftSectionRepresentableWithCache" \ - Sources/MachOSwiftSection/Models/ -r) -``` - -Expected: empty output (every model file with a MachO API now has a ReadingContext API). - -- [x] **Step 5: Commit (only if Step 2 produced changes; otherwise skip)** - -```bash -git add Sources/MachOSwiftSection/Models/ -git commit -m "feat(MachOSwiftSection): close ReadingContext gaps in partial files - -Audit pass over the 15 files that previously had partial ReadingContext -coverage. Adds the few overloads that were missing relative to each -file's MachO API surface." -``` - ---- - -## Done - -After Task 12, the entire `Sources/MachOSwiftSection/Models/` tree exposes a complete `ReadingContext` API mirror, the `runtimePointer(at:)` extension is in place, and `swift test` passes. diff --git a/docs/superpowers/plans/2026-05-03-machoswift-section-fixture-tests.md b/docs/superpowers/plans/2026-05-03-machoswift-section-fixture-tests.md deleted file mode 100644 index e5edaacb..00000000 --- a/docs/superpowers/plans/2026-05-03-machoswift-section-fixture-tests.md +++ /dev/null @@ -1,2194 +0,0 @@ -# MachOSwiftSection Fixture-Based Test Coverage Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Establish fixture-based tests covering every `public`/`open` `func`/`var`/`init` declared under `Sources/MachOSwiftSection/Models/`, with cross-reader equality assertions (MachOFile/MachOImage/InProcess + their `ReadingContext` equivalents) and full ABI literal expected values pinned in git. - -**Architecture:** Four pillars. (1) `MachOSwiftSectionFixtureTests` base loads `SymbolTestsCore.framework` from disk *and* `dlopen`s it into the test process, exposing `machOFile`, `machOImage`, three `ReadingContext` instances. (2) Per-method `@Test` Suites under `Tests/MachOSwiftSectionTests/Fixtures/` mirror the `Models/` directory; each `@Test` does cross-reader equality + reference to a baseline literal. (3) `baseline-generator` executable target reads fixture via MachOFile path, emits `__Baseline__/Baseline.swift` literal data files committed to git. (4) `MachOSwiftSectionCoverageInvariantTests` uses SwiftSyntax to scan `Models/` source and reflects all `FixtureSuite`-conforming Suites; missing/extra members fail the build. - -**Tech Stack:** Swift 6.2 / Xcode 26.0+, swift-testing, SwiftSyntax + SwiftParser + SwiftSyntaxBuilder (already a Package.swift dep), MachOKit, dlopen/dlfcn, ArgumentParser. - -**Code generation strategy:** Baseline files are produced via SwiftSyntaxBuilder's string-interpolation form (`SourceFileSyntax(stringLiteral:)` + `\(literal:)` / `\(raw:)`). SwiftSyntax parses the interpolated source, rejecting any malformed syntax at generation time, and `.formatted()` normalizes indentation/whitespace. A small `BaselineEmitter` helper covers cases `\(literal:)` doesn't natively support (hex literals — Int default to decimal in `\(literal:)`). - -**Branch:** `feature/machoswift-section-fixture-tests` (already created from `feature/reading-context-api`). - -**Spec reference:** `docs/superpowers/specs/2026-05-03-machoswift-section-fixture-tests-design.md`. - -**Prerequisites:** Before any task, run `swift package update` from the repo root to ensure SPM dependencies are current (per CLAUDE.md). Confirm the fixture is built: `xcodebuild -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj -scheme SymbolTestsCore -configuration Release build` if `Tests/Projects/SymbolTests/DerivedData/.../SymbolTestsCore.framework` is absent. - ---- - -## File Structure - -### Sources/MachOTestingSupport/ (existing target — extend) -- **Modify** `MachOImageName.swift` — add `SymbolTestsCore`, `SymbolTests` cases mirroring `MachOFileName` -- **Create** `MachOSwiftSectionFixtureTests.swift` — base test class -- **Create** `FixtureLoadError.swift` — error type -- **Create** `Coverage/MethodKey.swift` — `(typeName, memberName)` struct -- **Create** `Coverage/FixtureSuite.swift` — protocol Suite types conform to -- **Create** `Coverage/PublicMemberScanner.swift` — SwiftSyntax static scan -- **Create** `Coverage/CoverageAllowlist.swift` — allowlist data type (entries supplied by test target) -- **Create** `Baseline/BaselineEmitter.swift` — small helper (`hex`/`hexArray`) for emitting hex integer literals as `\(raw:)` interpolations; strings/bools/decimal ints/optionals/arrays-of-strings handled directly by SwiftSyntaxBuilder's `\(literal:)` -- **Create** `Baseline/BaselineGenerator.swift` — top-level orchestration -- **Create** `Baseline/BaselineFixturePicker.swift` — selects "main + variants" per descriptor type -- **Create** `Baseline/Generators/BaselineGenerator.swift` — one per descriptor family (added incrementally per Phase 2 task) - -### Sources/baseline-generator/ (NEW executable target) -- **Create** `main.swift` — ArgumentParser front, invokes `BaselineGenerator` - -### Tests/MachOSwiftSectionTests/Fixtures/ (NEW) -- **Create** subdirectories mirroring `Sources/MachOSwiftSection/Models/` -- **Create** `Tests.swift` Suite files (added incrementally per Phase 2 task) -- **Create** `__Baseline__/Baseline.swift` (auto-generated; committed) -- **Create** `__Baseline__/AllFixtureSuites.swift` (auto-generated) -- **Create** `MachOSwiftSectionCoverageInvariantTests.swift` (Phase 3) -- **Create** `CoverageAllowlistEntries.swift` — concrete allowlist entries with reasons - -### Modified Package.swift -- Add `baseline-generator` executable target with deps `[MachOTestingSupport, ArgumentParser]` -- Add `MachOTestingSupport` deps `[SwiftSyntax]` (for the scanner) - ---- - -## Task 1: Test Infrastructure Foundation - -**Files:** -- Modify: `Sources/MachOTestingSupport/MachOImageName.swift` -- Create: `Sources/MachOTestingSupport/FixtureLoadError.swift` -- Create: `Sources/MachOTestingSupport/MachOSwiftSectionFixtureTests.swift` -- Create: `Tests/MachOSwiftSectionTests/Fixtures/FixtureLoadingProbeTests.swift` (smoke test only) - -- [ ] **Step 1: Add `SymbolTestsCore` and `SymbolTests` cases to `MachOImageName`** - -Read the current file, then append the two cases mirroring `MachOFileName`: - -```swift -// Sources/MachOTestingSupport/MachOImageName.swift (existing file, append cases) -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" -``` - -- [ ] **Step 2: Build to verify no break** - -Run: `swift build 2>&1 | xcsift` -Expected: clean build. - -- [ ] **Step 3: Write `FixtureLoadError`** - -Create `Sources/MachOTestingSupport/FixtureLoadError.swift`: - -```swift -import Foundation - -package enum FixtureLoadError: Error, CustomStringConvertible { - case fixtureFileMissing(path: String) - case imageNotFoundAfterDlopen(path: String, dlerror: String?) - - package var description: String { - switch self { - case .fixtureFileMissing(let path): - return """ - Fixture binary not found at \(path). - Build it with: - xcodebuild -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj \\ - -scheme SymbolTestsCore -configuration Release build - """ - case .imageNotFoundAfterDlopen(let path, let dlerror): - return """ - dlopen succeeded but MachOImage(named:) returned nil for \(path). - dlerror: \(dlerror ?? "") - """ - } - } -} -``` - -- [ ] **Step 4: Write `MachOSwiftSectionFixtureTests` base class** - -Create `Sources/MachOTestingSupport/MachOSwiftSectionFixtureTests.swift`: - -```swift -import Foundation -import Testing -import MachOKit -import MachOFoundation -import MachOReading -import MachOResolving - -@MainActor -package class MachOSwiftSectionFixtureTests: Sendable { - package let machOFile: MachOFile - package let machOImage: MachOImage - - package let fileContext: MachOContext - package let imageContext: MachOContext - package let inProcessContext: InProcessContext - - package class var fixtureFileName: MachOFileName { .SymbolTestsCore } - package class var fixtureImageName: MachOImageName { .SymbolTestsCore } - package class var preferredArchitecture: CPUType { .arm64 } - - package init() async throws { - // 1) Load MachO from disk. - let file = try loadFromFile(named: Self.fixtureFileName) - switch file { - case .fat(let fatFile): - self.machOFile = try required( - fatFile.machOFiles().first(where: { $0.header.cpuType == Self.preferredArchitecture }) - ?? fatFile.machOFiles().first - ) - case .machO(let machO): - self.machOFile = machO - @unknown default: - fatalError() - } - - // 2) Ensure fixture is dlopen'd into the test process so MachOImage(named:) succeeds. - try Self.ensureFixtureLoaded() - guard let image = MachOImage(named: Self.fixtureImageName) else { - throw FixtureLoadError.imageNotFoundAfterDlopen( - path: Self.fixtureImageName.rawValue, - dlerror: Self.lastDlerror() - ) - } - self.machOImage = image - - // 3) Three ReadingContext instances over the same fixture. - self.fileContext = MachOContext(machO: machOFile) - self.imageContext = MachOContext(machO: machOImage) - self.inProcessContext = InProcessContext() - } - - private static let dlopenOnce: Void = { - let absolute = resolveFixturePath(MachOImageName.SymbolTestsCore.rawValue) - _ = absolute.withCString { dlopen($0, RTLD_LAZY) } - }() - - private static func ensureFixtureLoaded() throws { - _ = dlopenOnce - } - - /// Resolve a relative MachOImageName path (rooted at the package-relative `../../Tests/...` - /// convention) to an absolute filesystem path. Uses the same anchor strategy as - /// `loadFromFile` for parity. - private static func resolveFixturePath(_ relativePath: String) -> String { - if relativePath.hasPrefix("/") { return relativePath } - let anchor = URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() // MachOTestingSupport/ - .deletingLastPathComponent() // Sources/ - return anchor.appendingPathComponent(relativePath).standardizedFileURL.path - } - - private static func lastDlerror() -> String? { - guard let cString = dlerror() else { return nil } - return String(cString: cString) - } -} - -extension MachOSwiftSectionFixtureTests { - /// Run `body` against each (label, reader) pair, asserting all results equal the first. - /// Returns the unique value. Fails fast with the label of the first mismatching reader. - package func acrossAllReaders( - file fileWork: () throws -> T, - image imageWork: () throws -> T, - inProcess inProcessWork: (() throws -> T)? = nil, - sourceLocation: SourceLocation = #_sourceLocation - ) throws -> T { - let fromFile = try fileWork() - let fromImage = try imageWork() - #expect(fromFile == fromImage, "MachOFile vs MachOImage diverged", sourceLocation: sourceLocation) - if let inProcessWork { - let fromInProcess = try inProcessWork() - #expect(fromFile == fromInProcess, "MachOFile vs InProcess diverged", sourceLocation: sourceLocation) - } - return fromFile - } - - /// Run `body` against each ReadingContext (file/image/inProcess), asserting all equal. - package func acrossAllContexts( - file fileWork: () throws -> T, - image imageWork: () throws -> T, - inProcess inProcessWork: (() throws -> T)? = nil, - sourceLocation: SourceLocation = #_sourceLocation - ) throws -> T { - let fromFileCtx = try fileWork() - let fromImageCtx = try imageWork() - #expect(fromFileCtx == fromImageCtx, "fileContext vs imageContext diverged", sourceLocation: sourceLocation) - if let inProcessWork { - let fromInProcessCtx = try inProcessWork() - #expect(fromFileCtx == fromInProcessCtx, "fileContext vs inProcessContext diverged", sourceLocation: sourceLocation) - } - return fromFileCtx - } -} -``` - -- [ ] **Step 5: Write a smoke test verifying the fixture loads from all three readers** - -Create `Tests/MachOSwiftSectionTests/Fixtures/FixtureLoadingProbeTests.swift`: - -```swift -import Foundation -import Testing -import MachOKit -@testable import MachOSwiftSection -@testable import MachOTestingSupport - -@Suite -final class FixtureLoadingProbeTests: MachOSwiftSectionFixtureTests, @unchecked Sendable { - @Test func machOFileSwiftSectionParses() async throws { - let typeContextDescriptors = try machOFile.swift.typeContextDescriptors - #expect(!typeContextDescriptors.isEmpty, "fixture must contain at least one type") - } - - @Test func machOImageSwiftSectionParses() async throws { - let typeContextDescriptors = try machOImage.swift.typeContextDescriptors - #expect(!typeContextDescriptors.isEmpty, "fixture image must contain at least one type") - } - - @Test func threeReadersSeeSameTypeCount() async throws { - let fileCount = try machOFile.swift.typeContextDescriptors.count - let imageCount = try machOImage.swift.typeContextDescriptors.count - #expect(fileCount == imageCount, "MachOFile and MachOImage disagree on type count") - } -} -``` - -- [ ] **Step 6: Build and run smoke test** - -Run: `swift build 2>&1 | xcsift` -Expected: clean build. - -Run: `swift test --filter FixtureLoadingProbeTests 2>&1 | xcsift` -Expected: 3 tests pass. - -If MachOImage count differs from MachOFile count, that itself is a finding worth investigating — but most likely they agree because both read the same `__swift5_types` section. - -- [ ] **Step 7: Commit** - -```bash -git add Sources/MachOTestingSupport/MachOImageName.swift \ - Sources/MachOTestingSupport/FixtureLoadError.swift \ - Sources/MachOTestingSupport/MachOSwiftSectionFixtureTests.swift \ - Tests/MachOSwiftSectionTests/Fixtures/FixtureLoadingProbeTests.swift -git commit -m "$(cat <<'EOF' -test(MachOTestingSupport): add MachOSwiftSectionFixtureTests base + dlopen fixture loader - -Loads SymbolTestsCore.framework from disk, dlopens it once into the test -process, and exposes machOFile/machOImage plus three ReadingContext -instances (fileContext/imageContext/inProcessContext). Adds smoke probe -to verify all three readers see the fixture's swift5_types section. -EOF -)" -``` - ---- - -## Task 2: BaselineEmitter (hex helper) + SwiftSyntaxBuilder dep - -**Files:** -- Modify: `Package.swift` — add `SwiftSyntaxBuilder` to `MachOTestingSupport` deps; declare `MachOTestingSupportTests` test target if it doesn't already exist -- Create: `Sources/MachOTestingSupport/Baseline/BaselineEmitter.swift` -- Create: `Tests/MachOTestingSupportTests/Baseline/BaselineEmitterTests.swift` - -**Background.** Most ABI baseline data (strings, bools, decimal ints, arrays of strings, optionals) will be emitted via SwiftSyntaxBuilder's `\(literal:)` interpolation, which auto-escapes and parses-validates at generation time. The exception is **hex literals** — `\(literal: 0x10)` produces `16` (decimal), not `0x10`. We emit hex via `\(raw:)` and a small `BaselineEmitter` helper that returns the hex literal string. That helper, plus its hex-array variant, is the entirety of `BaselineEmitter`. - -- [ ] **Step 1: Add `SwiftSyntaxBuilder` to `MachOTestingSupport` target deps** - -Inspect `Package.swift`. The `MachOTestingSupport` target currently depends on `SwiftSyntax`/`SwiftParser` (added in Task 3). Add `SwiftSyntaxBuilder` alongside: - -```swift -// In Package.swift, MachOTestingSupport target dependencies: -.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), -``` - -And in `extension Target.Dependency` near the other SwiftSyntax aliases: - -```swift -static let SwiftSyntaxBuilder = Target.Dependency.product( - name: "SwiftSyntaxBuilder", - package: "swift-syntax" -) -``` - -(Note: Task 3 also adds `SwiftSyntax`/`SwiftParser` deps. If executing Task 2 before Task 3, add all three at once and Task 3 Step 1 becomes a no-op.) - -- [ ] **Step 2: Confirm `MachOTestingSupportTests` test target exists in `Package.swift`** - -If a `MachOTestingSupportTests` target is not present, add this declaration alongside the other testTargets (mirror `MachOSwiftSectionTests` style): - -```swift -// Sources/Package.swift (extension Target, near other testTargets) -static let MachOTestingSupportTests = Target.testTarget( - name: "MachOTestingSupportTests", - dependencies: [ - .target(.MachOTestingSupport), - ], - swiftSettings: testSettings -) -``` - -And register it in the `targets:` array of the `Package(...)` declaration. - -- [ ] **Step 3: Build to verify deps wire up** - -Run: `swift package update && swift build 2>&1 | xcsift` -Expected: clean build. - -- [ ] **Step 4: Write failing emitter test** - -Create `Tests/MachOTestingSupportTests/Baseline/BaselineEmitterTests.swift`: - -```swift -import Foundation -import Testing -@testable import MachOTestingSupport - -@Suite -struct BaselineEmitterTests { - @Test func emitsIntHex() { - #expect(BaselineEmitter.hex(0x10) == "0x10") - } - - @Test func emitsZeroHex() { - #expect(BaselineEmitter.hex(0) == "0x0") - } - - @Test func emitsUInt32Hex() { - #expect(BaselineEmitter.hex(UInt32(0x40000051)) == "0x40000051") - } - - @Test func emitsNegativeIntAsTwosComplementHex() { - // Negative Int sign-extends to UInt64 representation. - #expect(BaselineEmitter.hex(Int(-1)) == "0xffffffffffffffff") - } - - @Test func emitsHexArray() { - #expect(BaselineEmitter.hexArray([0x10, 0x18, 0x28]) == "[0x10, 0x18, 0x28]") - } - - @Test func emitsEmptyHexArray() { - #expect(BaselineEmitter.hexArray([Int]()) == "[]") - } -} -``` - -- [ ] **Step 5: Run test to verify it fails** - -Run: `swift test --filter BaselineEmitterTests 2>&1 | xcsift` -Expected: FAIL — `BaselineEmitter` not defined. - -- [ ] **Step 6: Implement `BaselineEmitter`** - -Create `Sources/MachOTestingSupport/Baseline/BaselineEmitter.swift`: - -```swift -import Foundation - -/// Tiny helper providing the few literal forms that SwiftSyntaxBuilder's -/// `\(literal:)` does NOT produce in the form we want for ABI baselines. -/// -/// Specifically: integers via `\(literal:)` come out as decimal Swift literals, -/// but baseline files emit offsets/sizes/flags as hex (`0x...`) for parity with -/// `otool` / Hopper output. Use these helpers with `\(raw:)` in the -/// SwiftSyntaxBuilder source string. -/// -/// For everything else — strings, bools, decimal ints, arrays of strings, -/// optionals — use `\(literal:)` directly; SwiftSyntaxBuilder handles escaping. -package enum BaselineEmitter { - /// Emit `0x` for any binary integer (sign-extends to UInt64). - package static func hex(_ value: T) -> String { - let unsigned = UInt64(truncatingIfNeeded: value) - return "0x\(String(unsigned, radix: 16))" - } - - /// Emit `[0x..., 0x..., ...]` for an array of binary integers. - package static func hexArray(_ values: [T]) -> String { - "[\(values.map(hex).joined(separator: ", "))]" - } -} -``` - -- [ ] **Step 7: Run test to verify it passes** - -Run: `swift test --filter BaselineEmitterTests 2>&1 | xcsift` -Expected: 6 tests pass. - -- [ ] **Step 8: Commit** - -```bash -git add Package.swift \ - Sources/MachOTestingSupport/Baseline/BaselineEmitter.swift \ - Tests/MachOTestingSupportTests/Baseline/BaselineEmitterTests.swift -git commit -m "$(cat <<'EOF' -test(MachOTestingSupport): add BaselineEmitter hex helper + SwiftSyntaxBuilder dep - -Adds two-function helper (hex/hexArray) for emitting integer literals as -hex (`0x...`) for ABI baseline files. Strings/bools/decimal ints/arrays of -strings/optionals are emitted via SwiftSyntaxBuilder's `\(literal:)` -interpolation directly. Hex needs a helper because `\(literal: 0x10)` outputs -`16` (decimal). Wires SwiftSyntaxBuilder into MachOTestingSupport deps. -EOF -)" -``` - ---- - -## Task 3: PublicMemberScanner + Coverage Framework - -**Files:** -- Create: `Sources/MachOTestingSupport/Coverage/MethodKey.swift` -- Create: `Sources/MachOTestingSupport/Coverage/FixtureSuite.swift` -- Create: `Sources/MachOTestingSupport/Coverage/CoverageAllowlist.swift` -- Create: `Sources/MachOTestingSupport/Coverage/PublicMemberScanner.swift` -- Modify: `Package.swift` — add SwiftSyntax dep to `MachOTestingSupport` target if not present -- Create: `Tests/MachOTestingSupportTests/Coverage/PublicMemberScannerTests.swift` -- Create: `Tests/MachOTestingSupportTests/Coverage/Fixtures/SampleSource.swift` — input fixture for scanner test - -- [ ] **Step 1: Add SwiftSyntax to MachOTestingSupport target deps if missing** - -Inspect `Package.swift` `MachOTestingSupport` target. If it doesn't already depend on `SwiftSyntax` and `SwiftParser`, add: - -```swift -.product(.SwiftSyntax), -.product(.SwiftParser), -``` - -to the `dependencies:` array of the `MachOTestingSupport` target declaration. - -- [ ] **Step 2: Build to verify deps wire up** - -Run: `swift build 2>&1 | xcsift` -Expected: clean build. - -- [ ] **Step 3: Create `MethodKey`** - -`Sources/MachOTestingSupport/Coverage/MethodKey.swift`: - -```swift -import Foundation - -package struct MethodKey: Hashable, Comparable, CustomStringConvertible { - package let typeName: String - package let memberName: String - - package init(typeName: String, memberName: String) { - self.typeName = typeName - self.memberName = memberName - } - - package static func < (lhs: MethodKey, rhs: MethodKey) -> Bool { - if lhs.typeName != rhs.typeName { return lhs.typeName < rhs.typeName } - return lhs.memberName < rhs.memberName - } - - package var description: String { - "\(typeName).\(memberName)" - } -} -``` - -- [ ] **Step 4: Create `FixtureSuite` protocol** - -`Sources/MachOTestingSupport/Coverage/FixtureSuite.swift`: - -```swift -import Foundation - -/// Conformance contract for fixture-based test suites participating in coverage tracking. -/// -/// Each Suite type provides: -/// - `testedTypeName`: the source-code Type whose public members the Suite covers -/// (e.g. "StructDescriptor"). Must match the type name exactly as it appears in -/// `Sources/MachOSwiftSection/Models/`. -/// - `registeredTestMethodNames`: the member names covered by `@Test` methods in this Suite. -/// For each entry "foo", the Coverage Invariant test expects a public member -/// `.foo` (any overload group) to exist in the source. -package protocol FixtureSuite { - static var testedTypeName: String { get } - static var registeredTestMethodNames: Set { get } -} -``` - -- [ ] **Step 5: Create `CoverageAllowlist`** - -`Sources/MachOTestingSupport/Coverage/CoverageAllowlist.swift`: - -```swift -import Foundation - -/// A single entry exempting one (typeName, memberName) pair from coverage requirements. -/// Each entry MUST carry a human-readable reason. -package struct CoverageAllowlistEntry: Hashable, CustomStringConvertible { - package let key: MethodKey - package let reason: String - - package init(typeName: String, memberName: String, reason: String) { - self.key = MethodKey(typeName: typeName, memberName: memberName) - self.reason = reason - } - - package var description: String { - "\(key) // \(reason)" - } -} -``` - -- [ ] **Step 6: Create scanner skeleton (no SwiftSyntax integration yet)** - -`Sources/MachOTestingSupport/Coverage/PublicMemberScanner.swift`: - -```swift -import Foundation -import SwiftSyntax -import SwiftParser - -/// Scans a directory of Swift source files and extracts the set of public/open -/// `func`, `var`, and `init` members, keyed by `(typeName, memberName)`. -/// -/// Skipped: -/// - `internal`, `private`, `fileprivate` declarations -/// - `@_spi(...)` declarations (treated as non-public) -/// - members on types whose name ends with `Layout` (covered by LayoutTests) -/// - `init(layout:offset:)` synthesized by `@MemberwiseInit` -/// - extensions on enums whose name ends with `Kind`/`Flags` and similar pure-data utilities -/// (handled via allowlist if they slip through) -package struct PublicMemberScanner { - package let sourceRoot: URL - - package init(sourceRoot: URL) { - self.sourceRoot = sourceRoot - } - - package func scan(applyingAllowlist allowlist: Set = []) throws -> Set { - let files = try collectSwiftFiles(under: sourceRoot) - var result: Set = [] - for fileURL in files { - let source = try String(contentsOf: fileURL, encoding: .utf8) - let tree = Parser.parse(source: source) - let visitor = PublicMemberVisitor(viewMode: .sourceAccurate) - visitor.walk(tree) - for key in visitor.collected { - if allowlist.contains(key) { continue } - result.insert(key) - } - } - return result - } - - private func collectSwiftFiles(under root: URL) throws -> [URL] { - let fileManager = FileManager.default - let enumerator = fileManager.enumerator(at: root, includingPropertiesForKeys: nil) - var files: [URL] = [] - while let url = enumerator?.nextObject() as? URL { - if url.pathExtension == "swift" { files.append(url) } - } - return files - } -} - -private final class PublicMemberVisitor: SyntaxVisitor { - private(set) var collected: [MethodKey] = [] - private var typeStack: [String] = [] - - override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { - typeStack.append(node.name.text) - return .visitChildren - } - override func visitPost(_ node: ClassDeclSyntax) { - typeStack.removeLast() - } - - override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { - typeStack.append(node.name.text) - return .visitChildren - } - override func visitPost(_ node: StructDeclSyntax) { - typeStack.removeLast() - } - - override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { - typeStack.append(node.name.text) - return .visitChildren - } - override func visitPost(_ node: EnumDeclSyntax) { - typeStack.removeLast() - } - - override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { - typeStack.append(node.name.text) - return .visitChildren - } - override func visitPost(_ node: ProtocolDeclSyntax) { - typeStack.removeLast() - } - - override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { - // Push the extended type as the current scope. - typeStack.append(node.extendedType.trimmedDescription) - return .visitChildren - } - override func visitPost(_ node: ExtensionDeclSyntax) { - typeStack.removeLast() - } - - override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { - guard isPublicLike(node.modifiers, attributes: node.attributes) else { return .skipChildren } - guard let typeName = currentTypeName() else { return .skipChildren } - if shouldSkip(typeName: typeName) { return .skipChildren } - collected.append(MethodKey(typeName: typeName, memberName: node.name.text)) - return .skipChildren - } - - override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { - guard isPublicLike(node.modifiers, attributes: node.attributes) else { return .skipChildren } - guard let typeName = currentTypeName() else { return .skipChildren } - if shouldSkip(typeName: typeName) { return .skipChildren } - for binding in node.bindings { - if let pattern = binding.pattern.as(IdentifierPatternSyntax.self) { - collected.append(MethodKey(typeName: typeName, memberName: pattern.identifier.text)) - } - } - return .skipChildren - } - - override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { - guard isPublicLike(node.modifiers, attributes: node.attributes) else { return .skipChildren } - guard let typeName = currentTypeName() else { return .skipChildren } - if shouldSkip(typeName: typeName) { return .skipChildren } - if isMemberwiseSynthesizedInit(node) { return .skipChildren } - let signature = node.signature.parameterClause.parameters.map { $0.firstName.text }.joined(separator: ":") - let memberName = signature.isEmpty ? "init" : "init(\(signature):)" - collected.append(MethodKey(typeName: typeName, memberName: memberName)) - return .skipChildren - } - - private func currentTypeName() -> String? { - typeStack.last - } - - private func shouldSkip(typeName: String) -> Bool { - if typeName.hasSuffix("Layout") { return true } - return false - } - - private func isPublicLike(_ modifiers: DeclModifierListSyntax, attributes: AttributeListSyntax) -> Bool { - // Reject if any @_spi attribute is present. - for attribute in attributes { - if let attr = attribute.as(AttributeSyntax.self), - attr.attributeName.trimmedDescription == "_spi" { - return false - } - } - // Accept only if `public` or `open` modifier exists. - for modifier in modifiers { - let name = modifier.name.text - if name == "public" || name == "open" { return true } - } - return false - } - - private func isMemberwiseSynthesizedInit(_ node: InitializerDeclSyntax) -> Bool { - // Detect explicit synthesis when authoring class declares @MemberwiseInit; - // the macro expands to init(layout: ..., offset: ...). - let names = node.signature.parameterClause.parameters.map { $0.firstName.text } - return names == ["layout", "offset"] || names == ["offset", "layout"] - } -} -``` - -- [ ] **Step 7: Write a sample-source fixture for the scanner test** - -Create `Tests/MachOTestingSupportTests/Coverage/Fixtures/SampleSource.swift`: - -```swift -// Sample source consumed by PublicMemberScannerTests via on-disk reads. -// Not actually compiled — file extension must remain `.swift` but content is -// read from disk by the test, so it'll go through SwiftSyntax parser, not the -// build's Swift compiler. Scope matches typical Models/ patterns. - -public struct SampleDescriptor { - public func name() -> String { "" } - public var nameOptional: String? { nil } - public init(layout: SampleLayout, offset: Int) {} - public init(custom: Int) {} - internal func internalHelper() {} - private var hidden: Int { 0 } -} - -extension SampleDescriptor { - public func sectionedFoo() -> Int { 0 } -} - -@_spi(Internals) -extension SampleDescriptor { - public func spiHidden() -> Int { 0 } -} - -public struct SampleLayout { - public static func offset(of field: PartialKeyPath) -> Int { 0 } -} -``` - -Note: this file must be excluded from the build target. Place it under -`Tests/MachOTestingSupportTests/Coverage/Fixtures/` and ensure the test target -doesn't compile it (Xcode/SPM will compile any `.swift` under `Tests/`, so -prefix the file name with `_` would help — but cleaner is to rename the -extension to something other than `.swift`). Use `.swiftsample` and read -explicitly: - -Actually rename to `SampleSource.swift.txt` and update the test path. The -scanner reads files by URL anyway. - -Re-create `Tests/MachOTestingSupportTests/Coverage/Fixtures/SampleSource.swift.txt` with the content above. - -- [ ] **Step 8: Write the failing scanner test** - -Create `Tests/MachOTestingSupportTests/Coverage/PublicMemberScannerTests.swift`: - -```swift -import Foundation -import Testing -@testable import MachOTestingSupport - -@Suite -struct PublicMemberScannerTests { - private var fixtureRoot: URL { - URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() // Coverage/ - .appendingPathComponent("Fixtures") - } - - /// Scanner reads `.swift` files in the directory. We renamed our test source to - /// `.swift.txt` to avoid build inclusion, then rename a tmp copy to `.swift` for the scan. - private func makeScanRoot() throws -> URL { - let tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) - let source = try String(contentsOf: fixtureRoot.appendingPathComponent("SampleSource.swift.txt")) - let dest = tempDir.appendingPathComponent("SampleSource.swift") - try source.write(to: dest, atomically: true, encoding: .utf8) - return tempDir - } - - @Test func collectsPublicMembers() throws { - let root = try makeScanRoot() - defer { try? FileManager.default.removeItem(at: root) } - let scanner = PublicMemberScanner(sourceRoot: root) - let result = try scanner.scan() - - #expect(result.contains(MethodKey(typeName: "SampleDescriptor", memberName: "name"))) - #expect(result.contains(MethodKey(typeName: "SampleDescriptor", memberName: "nameOptional"))) - #expect(result.contains(MethodKey(typeName: "SampleDescriptor", memberName: "init(custom:)"))) - #expect(result.contains(MethodKey(typeName: "SampleDescriptor", memberName: "sectionedFoo"))) - } - - @Test func skipsInternalAndPrivate() throws { - let root = try makeScanRoot() - defer { try? FileManager.default.removeItem(at: root) } - let scanner = PublicMemberScanner(sourceRoot: root) - let result = try scanner.scan() - - #expect(!result.contains(MethodKey(typeName: "SampleDescriptor", memberName: "internalHelper"))) - #expect(!result.contains(MethodKey(typeName: "SampleDescriptor", memberName: "hidden"))) - } - - @Test func skipsSPI() throws { - let root = try makeScanRoot() - defer { try? FileManager.default.removeItem(at: root) } - let scanner = PublicMemberScanner(sourceRoot: root) - let result = try scanner.scan() - - #expect(!result.contains(MethodKey(typeName: "SampleDescriptor", memberName: "spiHidden"))) - } - - @Test func skipsMemberwiseInit() throws { - let root = try makeScanRoot() - defer { try? FileManager.default.removeItem(at: root) } - let scanner = PublicMemberScanner(sourceRoot: root) - let result = try scanner.scan() - - // The 2-arg `init(layout:offset:)` should be filtered as MemberwiseInit synthesized. - #expect(!result.contains(MethodKey(typeName: "SampleDescriptor", memberName: "init(layout:offset:)"))) - } - - @Test func skipsLayoutTypes() throws { - let root = try makeScanRoot() - defer { try? FileManager.default.removeItem(at: root) } - let scanner = PublicMemberScanner(sourceRoot: root) - let result = try scanner.scan() - - #expect(!result.contains(MethodKey(typeName: "SampleLayout", memberName: "offset"))) - } - - @Test func appliesAllowlist() throws { - let root = try makeScanRoot() - defer { try? FileManager.default.removeItem(at: root) } - let scanner = PublicMemberScanner(sourceRoot: root) - let allowlist: Set = [MethodKey(typeName: "SampleDescriptor", memberName: "name")] - let result = try scanner.scan(applyingAllowlist: allowlist) - #expect(!result.contains(MethodKey(typeName: "SampleDescriptor", memberName: "name"))) - } -} -``` - -- [ ] **Step 9: Run scanner test to verify it fails** - -Run: `swift test --filter PublicMemberScannerTests 2>&1 | xcsift` -Expected: at least the path-fixture-not-found assertion or compile error indicating fixture is missing — fix by creating the fixture. - -Then re-run; expected: scanner tests pass. - -- [ ] **Step 10: Run all coverage tests + emitter tests** - -Run: `swift test --filter MachOTestingSupportTests 2>&1 | xcsift` -Expected: all pass. - -- [ ] **Step 11: Commit** - -```bash -git add Sources/MachOTestingSupport/Coverage/ \ - Tests/MachOTestingSupportTests/Coverage/ \ - Package.swift -git commit -m "$(cat <<'EOF' -test(MachOTestingSupport): add coverage framework — MethodKey, FixtureSuite, scanner - -PublicMemberScanner walks SwiftSyntax to extract public/open func/var/init from a -source root, keyed by (typeName, memberName). Skips internal/private/fileprivate, -@_spi(...), Layout-suffixed types, and @MemberwiseInit-synthesized -init(layout:offset:). FixtureSuite protocol exposes testedTypeName + -registeredTestMethodNames for the Coverage Invariant test wiring up later. -EOF -)" -``` - ---- - -## Task 4: Reference Suite — `Type/Struct/` end-to-end - -Pilot the full pattern on `Sources/MachOSwiftSection/Models/Type/Struct/` (5 files: `Struct.swift`, `StructDescriptor.swift`, `StructDescriptorLayout.swift`, `StructMetadata.swift`, `StructMetadataLayout.swift`, `StructMetadataProtocol.swift`). `*Layout.swift` files are scanner-skipped; the 4 testable files yield ~3-5 Suites total. - -This task delivers the *first* Suite and its corresponding sub-generator end-to-end, locking in the pattern reused by Tasks 5-15. - -**Files:** -- Create: `Sources/MachOTestingSupport/Baseline/Generators/StructDescriptorBaselineGenerator.swift` -- Create: `Sources/MachOTestingSupport/Baseline/BaselineFixturePicker.swift` (skeleton) -- Create: `Tests/MachOSwiftSectionTests/Fixtures/Type/Struct/StructTests.swift` -- Create: `Tests/MachOSwiftSectionTests/Fixtures/Type/Struct/StructDescriptorTests.swift` -- Create: `Tests/MachOSwiftSectionTests/Fixtures/Type/Struct/StructMetadataTests.swift` -- Create: `Tests/MachOSwiftSectionTests/Fixtures/Type/Struct/StructMetadataProtocolTests.swift` -- Create: `Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructDescriptorBaseline.swift` (auto-generated) -- (and matching baseline files for Struct, StructMetadata, StructMetadataProtocol) - -- [ ] **Step 1: Inventory `Type/Struct/` public surface** - -Run from the repo root to enumerate public methods (cross-check with the scanner once it lands later in this Task): - -```bash -rg -n "^ public (func|var|init)" Sources/MachOSwiftSection/Models/Type/Struct/ -t swift -``` - -Expected output: list of public funcs/vars/inits across `Struct.swift`, `StructDescriptor.swift`, `StructMetadata.swift`, `StructMetadataProtocol.swift`. Save the list (paste into a scratch buffer) — this is the master list of `@Test func`s you must produce. - -- [ ] **Step 2: Pick the fixture targets** - -In `SymbolTestsCore/Structs.swift` we have `public struct Structs.StructTest`. In `SymbolTestsCore/GenericFieldLayout.swift` we have `public struct GenericFieldLayout.GenericStructNonRequirement`. We'll use: - -| variant key | fixture target | rationale | -|---|---|---| -| `structTest` | `SymbolTestsCore.Structs.StructTest` | concrete (no generics) | -| `genericStructNonRequirement` | `SymbolTestsCore.GenericFieldLayout.GenericStructNonRequirement` | generic struct, exercises generic context paths | - -- [ ] **Step 3: Write `BaselineFixturePicker` skeleton** - -`Sources/MachOTestingSupport/Baseline/BaselineFixturePicker.swift`: - -```swift -import Foundation -import MachOFoundation -@testable import MachOSwiftSection - -/// Centralizes the "pick (main + variants) fixture entities for each descriptor type" -/// logic, ensuring Suites and their corresponding BaselineGenerators look at the -/// same set of entities. -package enum BaselineFixturePicker { - package static func struct_StructTest( - in machO: some MachOSwiftSectionRepresentableWithCache - ) throws -> StructDescriptor { - try required( - try machO.swift.typeContextDescriptors.compactMap(\.struct).first(where: { - try $0.name(in: machO) == "StructTest" - && (try? $0.parent(in: machO)?.dumpName(using: .default, in: machO).string).flatMap { $0 == "Structs" } == true - }) - ) - } - - package static func struct_GenericStructNonRequirement( - in machO: some MachOSwiftSectionRepresentableWithCache - ) throws -> StructDescriptor { - try required( - try machO.swift.typeContextDescriptors.compactMap(\.struct).first(where: { - try $0.name(in: machO) == "GenericStructNonRequirement" - }) - ) - } -} -``` - -If `dumpName` is not available at this layer, use `parent(in:)` chasing to walk up the context chain; the simplest robust check is by exact `name(in:)` since both `StructTest` and `GenericStructNonRequirement` are unique names within the fixture. - -- [ ] **Step 4: Run baseline-generator manually for StructDescriptor — first cut** - -We don't have the executable target yet (Task 17). Use a temporary `@Test`-shaped shim or write a one-shot Swift script that: - -1. Loads `SymbolTestsCore.framework` MachOFile (via the already-existing `loadFromFile`). -2. Picks the two struct variants via `BaselineFixturePicker`. -3. For each public member of `StructDescriptor`, reads the value through the `MachOFile` reader. -4. Emits the Swift literal data into `Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructDescriptorBaseline.swift`. - -Sketch script (commit to `Sources/baseline-generator/main.swift` as a stub Task 17 will polish): - -```swift -import Foundation -import ArgumentParser -import MachOTestingSupport - -// Phase-1 stub: invokes BaselineGenerator.generateAll(); proper CLI in Task 17. -@main -struct BaselineGeneratorMain: AsyncParsableCommand { - func run() async throws { - try await BaselineGenerator.generateAll( - outputDirectory: URL(fileURLWithPath: "Tests/MachOSwiftSectionTests/Fixtures/__Baseline__") - ) - } -} -``` - -(`BaselineGenerator.generateAll()` will start with just StructDescriptor and grow per-task.) - -- [ ] **Step 5: Implement `BaselineGenerator` (dispatcher pattern from the start) + `StructDescriptorBaselineGenerator`** - -`Sources/MachOTestingSupport/Baseline/BaselineGenerator.swift`: - -```swift -import Foundation -import MachOFoundation -import MachOKit -@testable import MachOSwiftSection - -package enum BaselineGenerator { - package static func generateAll(outputDirectory: URL) async throws { - try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) - let machOFile = try loadFixtureMachOFile() - // Add one call per Suite as it lands in Tasks 5-15. Keep deterministic ordering. - try dispatchSuite("StructDescriptor", in: machOFile, outputDirectory: outputDirectory) - } - - package static func generate(suite name: String, outputDirectory: URL) async throws { - try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) - let machOFile = try loadFixtureMachOFile() - try dispatchSuite(name, in: machOFile, outputDirectory: outputDirectory) - } - - private static func dispatchSuite(_ name: String, in machOFile: MachOFile, outputDirectory: URL) throws { - switch name { - case "StructDescriptor": - try StructDescriptorBaselineGenerator.generate(in: machOFile, outputDirectory: outputDirectory) - // Add cases here as Tasks 5-15 land. - default: - throw BaselineGeneratorError.unknownSuite(name) - } - } - - private static func loadFixtureMachOFile() throws -> MachOFile { - let file = try loadFromFile(named: .SymbolTestsCore) - switch file { - case .fat(let fat): - return try required( - fat.machOFiles().first(where: { $0.header.cpuType == .arm64 }) - ?? fat.machOFiles().first - ) - case .machO(let machO): - return machO - @unknown default: - fatalError() - } - } -} - -package enum BaselineGeneratorError: Error, CustomStringConvertible { - case unknownSuite(String) - package var description: String { - switch self { - case .unknownSuite(let name): - return "Unknown suite: \(name). Use --help for the list of valid suites." - } - } -} -``` - -Now Tasks 5-15 each add **one line** to `dispatchSuite` and **one line** to `generateAll`, plus their sub-generator file. - -`Sources/MachOTestingSupport/Baseline/Generators/StructDescriptorBaselineGenerator.swift`: - -```swift -import Foundation -import SwiftSyntax -import SwiftSyntaxBuilder -import MachOFoundation -@testable import MachOSwiftSection - -package enum StructDescriptorBaselineGenerator { - package static func generate( - in machO: some MachOSwiftSectionRepresentableWithCache, - outputDirectory: URL - ) throws { - let toolchain = "Swift 6.2" - let date = ISO8601DateFormatter().string(from: Date()) - - // Pick fixture entities. - let structTest = try BaselineFixturePicker.struct_StructTest(in: machO) - let genericStruct = try BaselineFixturePicker.struct_GenericStructNonRequirement(in: machO) - - // Read ABI fields per variant. Each helper returns the precise Swift - // initializer expression as a SourceFileSyntax-compatible string. - let structTestExpr = try emitEntryExpr(for: structTest, in: machO) - let genericStructExpr = try emitEntryExpr(for: genericStruct, in: machO) - - let registered = memberNames().sorted() - - // SwiftSyntaxBuilder string-interpolation form. SwiftSyntax parses this - // string at construction time — any malformed Swift fails immediately. - let header = """ - // AUTO-GENERATED — DO NOT EDIT. - // Regenerate via: swift run baseline-generator --suite StructDescriptor - // Source fixture: SymbolTestsCore.framework - // Toolchain: \(toolchain) - // Generated: \(date) - """ - - let file: SourceFileSyntax = """ - \(raw: header) - - enum StructDescriptorBaseline { - static let registeredTestMethodNames: Set = \(literal: registered) - - struct Entry { - let name: String - let numberOfFields: Int - let fieldNames: [String] - let fieldOffsets: [Int] - let isGeneric: Bool - let flagsRawValue: UInt32 - // ... extend per StructDescriptor public member - } - - static let structTest = \(raw: structTestExpr) - - static let genericStructNonRequirement = \(raw: genericStructExpr) - } - """ - - // `.formatted()` normalizes indentation/whitespace so re-runs produce - // byte-identical output (idempotency; verified in Task 4 Step 12). - let formatted = file.formatted().description + "\n" - - let outputURL = outputDirectory.appendingPathComponent("StructDescriptorBaseline.swift") - try formatted.write(to: outputURL, atomically: true, encoding: .utf8) - } - - /// Build the `Entry(...)` initializer expression as a Swift source fragment. - /// Plain values use `\(literal:)`; hex values use `\(raw:)` + `BaselineEmitter.hex`. - private static func emitEntryExpr( - for descriptor: StructDescriptor, - in machO: some MachOSwiftSectionRepresentableWithCache - ) throws -> String { - let name = try descriptor.name(in: machO) - let numFields = Int(descriptor.layout.numFields) - let fields = try descriptor.fields(in: machO) - let fieldNames = try fields.records.map { try $0.fieldName(in: machO) } - let fieldOffsets = try fields.records.map { try $0.fieldOffset(in: machO) } - let isGeneric = descriptor.layout.flags.isGeneric - let flagsRaw = descriptor.layout.flags.rawValue - - // We build this expression as an ExprSyntax to get string-interpolation - // ergonomics, then return its description (the resulting source fragment - // is later embedded into the SourceFileSyntax above). - let expr: ExprSyntax = """ - Entry( - name: \(literal: "SymbolTestsCore." + name), - numberOfFields: \(literal: numFields), - fieldNames: \(literal: fieldNames), - fieldOffsets: \(raw: BaselineEmitter.hexArray(fieldOffsets)), - isGeneric: \(literal: isGeneric), - flagsRawValue: \(raw: BaselineEmitter.hex(flagsRaw)) - ) - """ - return expr.description - } - - /// Hand-curated member name list mirroring StructDescriptor public surface. - /// Each entry must correspond to a `@Test func ` in - /// StructDescriptorTests.swift. The Coverage Invariant test (Task 16) - /// verifies this matches the static scan output. - private static func memberNames() -> [String] { - [ - "name", - "fields", - "genericContext", - "numberOfFields", - "fieldOffsetVectorOffset", - // ... extend per StructDescriptor public surface inventoried in Step 1 - ] - } -} -``` - -(Adapt method calls to actual `StructDescriptor` public API — Step 1's inventory is the source of truth.) - -**SwiftSyntaxBuilder primer:** -- `\(literal: x)` — `x` is `ExpressibleByLiteralSyntax`-conforming (`String`, `Int`, `Bool`, `[String]`, `Optional`, etc.). Output is a properly escaped/formatted Swift literal token. SwiftSyntax parses + validates at construction time. -- `\(raw: string)` — inserts `string` as raw Swift source. Use when `\(literal:)` doesn't apply (e.g. hex literals, pre-built expressions). -- `SourceFileSyntax`/`ExprSyntax` accept multi-line string literals and parse them; malformed Swift throws at construction site. -- `.formatted()` returns a copy with normalized trivia (indentation, whitespace, newlines). - -- [ ] **Step 6: Add `baseline-generator` executable target stub to Package.swift** - -Add to `Package.swift` `extension Target`: - -```swift -static let baseline_generator = Target.executableTarget( - name: "baseline-generator", - dependencies: [ - .target(.MachOTestingSupport), - .product(name: "ArgumentParser", package: "swift-argument-parser"), - ], - swiftSettings: testSettings -) -``` - -Add to the `Package(...)` `targets:` array: - -```swift -.baseline_generator, -``` - -Build: - -Run: `swift build 2>&1 | xcsift` -Expected: clean build. - -- [ ] **Step 7: Run baseline-generator to produce StructDescriptorBaseline.swift** - -```bash -swift run baseline-generator -``` - -Expected: `Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructDescriptorBaseline.swift` is created. Inspect it visually: - -```bash -cat Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructDescriptorBaseline.swift -``` - -Confirm names/offsets look plausible. If a value looks suspicious (e.g. `numberOfFields: 0`, `fieldOffsets: []`), recheck `BaselineFixturePicker` — likely picked wrong type. - -Note: SwiftSyntax's `.formatted()` normalizes whitespace, so the actual layout might differ slightly from the source string template — that's expected and desirable (idempotent re-runs produce byte-identical output). - -If `swift run baseline-generator` itself crashes with a SwiftSyntax parse error, the source string template has malformed Swift; SwiftSyntax catches this at construction time so the message will point at the offending line. - -- [ ] **Step 8: Write `StructDescriptorTests` Suite using the baseline** - -`Tests/MachOSwiftSectionTests/Fixtures/Type/Struct/StructDescriptorTests.swift`: - -```swift -import Foundation -import Testing -import MachOFoundation -@testable import MachOSwiftSection -@testable import MachOTestingSupport - -@Suite -final class StructDescriptorTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable { - static let testedTypeName = "StructDescriptor" - static var registeredTestMethodNames: Set { - StructDescriptorBaseline.registeredTestMethodNames - } - - @Test func name() async throws { - let fileSubject = try BaselineFixturePicker.struct_StructTest(in: machOFile) - let imageSubject = try BaselineFixturePicker.struct_StructTest(in: machOImage) - - let result = try acrossAllReaders( - file: { try fileSubject.name(in: machOFile) }, - image: { try imageSubject.name(in: machOImage) }, - inProcess: { try imageSubject.asPointerWrapper(in: machOImage).name() } - ) - _ = try acrossAllContexts( - file: { try fileSubject.name(in: fileContext) }, - image: { try imageSubject.name(in: imageContext) } - ) - - #expect("SymbolTestsCore." + result == StructDescriptorBaseline.structTest.name) - } - - @Test func numberOfFields() async throws { - let fileSubject = try BaselineFixturePicker.struct_StructTest(in: machOFile) - let imageSubject = try BaselineFixturePicker.struct_StructTest(in: machOImage) - - let result = try acrossAllReaders( - file: { fileSubject.layout.numFields }, - image: { imageSubject.layout.numFields } - ) - - #expect(Int(result) == StructDescriptorBaseline.structTest.numberOfFields) - } - - @Test func fields() async throws { - let fileSubject = try BaselineFixturePicker.struct_StructTest(in: machOFile) - let imageSubject = try BaselineFixturePicker.struct_StructTest(in: machOImage) - - let fileFieldNames = try fileSubject.fields(in: machOFile).records.map { try $0.fieldName(in: machOFile) } - let imageFieldNames = try imageSubject.fields(in: machOImage).records.map { try $0.fieldName(in: machOImage) } - let inProcessFieldNames = try imageSubject.asPointerWrapper(in: machOImage).fields().records.map { try $0.fieldName() } - - #expect(fileFieldNames == imageFieldNames) - #expect(fileFieldNames == inProcessFieldNames) - #expect(fileFieldNames == StructDescriptorBaseline.structTest.fieldNames) - - let fileFieldOffsets = try fileSubject.fields(in: machOFile).records.map { try $0.fieldOffset(in: machOFile) } - #expect(fileFieldOffsets == StructDescriptorBaseline.structTest.fieldOffsets) - } - - // ... one @Test per entry in StructDescriptorBaseline.registeredTestMethodNames -} -``` - -Repeat the pattern for every entry in `registeredTestMethodNames`. The body of each `@Test` follows the template: - -```swift -@Test func () async throws { - let fileSubject = try BaselineFixturePicker.struct_StructTest(in: machOFile) - let imageSubject = try BaselineFixturePicker.struct_StructTest(in: machOImage) - // 1) Cross-reader equality (omit inProcess block if no InProcess overload exists) - let result = try acrossAllReaders( - file: { try fileSubject.(in: machOFile) }, - image: { try imageSubject.(in: machOImage) }, - inProcess: { try imageSubject.asPointerWrapper(in: machOImage).() } - ) - // 2) Baseline literal - #expect((result) == StructDescriptorBaseline.structTest.) -} -``` - -- [ ] **Step 9: Run StructDescriptorTests** - -Run: `swift test --filter StructDescriptorTests 2>&1 | xcsift` -Expected: all tests pass. If a test fails: - -- **mismatch with baseline**: investigate whether the baseline value or the reader is wrong. If the baseline is wrong (generator bug), fix generator and rerun `swift run baseline-generator`. If the reader is wrong, fix the reader. -- **cross-reader mismatch**: a real bug in one of the three readers — investigate which one disagrees. - -- [ ] **Step 10: Repeat Steps 5-9 for `Struct`, `StructMetadata`, `StructMetadataProtocol`** - -Apply the same pattern to the other 3 testable Type/Struct/ files. For each: - -1. Inventory public members (`rg "^ public (func|var|init)" Sources/MachOSwiftSection/Models/Type/Struct/.swift -t swift`). -2. Add to `BaselineFixturePicker` if needed (e.g. `struct_StructTest_metadata` etc.). -3. Add a sub-generator under `Sources/MachOTestingSupport/Baseline/Generators/`. -4. Wire the sub-generator call into `BaselineGenerator.generateAll()`. -5. Run `swift run baseline-generator`; visually inspect the new baseline file. -6. Write the corresponding `Tests.swift` Suite, one `@Test` per registered member name. -7. Run `swift test --filter Tests`. - -For `StructMetadata`, fixture targets are picked by calling `metadataAccessorFunction()` on the descriptor in MachOImage and resolving — this only works for `MachOImage`, so the cross-reader equality block omits InProcess and treats `imageContext` differently. Document the asymmetry in the Suite comment. - -- [ ] **Step 11: Run all Type/Struct tests** - -Run: `swift test --filter "Type/Struct" 2>&1 | xcsift` - -Expected: all 4 (or however many) Suite files pass. - -- [ ] **Step 12: Confirm baseline-generator is idempotent** - -Run: `swift run baseline-generator && git status Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/` - -Expected: no modified files. If diffs appear, fix the generator (likely a non-deterministic field iteration order — sort). - -- [ ] **Step 13: Commit** - -```bash -git add Sources/MachOTestingSupport/Baseline/ \ - Sources/baseline-generator/ \ - Tests/MachOSwiftSectionTests/Fixtures/Type/Struct/ \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructDescriptorBaseline.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructBaseline.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructMetadataBaseline.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructMetadataProtocolBaseline.swift \ - Package.swift -git commit -m "$(cat <<'EOF' -test(MachOSwiftSection): add fixture-based Suite + baseline for Type/Struct - -Reference implementation locking the pattern reused by remaining Models/ -subdirectories: per-file BaselineGenerator (MachOFile path) writes a literal -__Baseline__/Baseline.swift, and per-file Tests Suite asserts both -cross-reader equality (file/image/inProcess + ReadingContext variants) and -baseline literal equality. Picks Structs.StructTest + GenericStructNonRequirement -as fixture variants. -EOF -)" -``` - ---- - -## Tasks 5–15: Per-Subdirectory Suite Migration - -Each task in this phase follows the exact same shape as Task 4 Steps 1-13, applied to a different `Models/` subdirectory. The deliverable per task is: - -1. **Inventory**: `rg "^ public (func|var|init)" Sources/MachOSwiftSection/Models// -t swift` — produces the list of `@Test func`s required. -2. **Picker entries**: extend `BaselineFixturePicker` with `_` static methods. -3. **Sub-generator(s)**: under `Sources/MachOTestingSupport/Baseline/Generators/`, one per testable file. -4. **Wire into `BaselineGenerator`**: add a `case "":` to `dispatchSuite(_:in:outputDirectory:)` calling the new sub-generator, AND a matching `try dispatchSuite("", ...)` line in `generateAll(outputDirectory:)`. Both edits are required so `swift run baseline-generator` and `swift run baseline-generator --suite ` both produce the new baseline. -5. **Run generator, eyeball diff**: `swift run baseline-generator && git diff Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/`. -6. **Suite(s)**: under `Tests/MachOSwiftSectionTests/Fixtures//`, one Suite per testable file, conforming to `MachOSwiftSectionFixtureTests` and `FixtureSuite`. -7. **`@Test` per registered member**: full cross-reader equality + baseline literal block per Task 4 Step 8 template. -8. **Run + commit** per Task 4 Steps 11-13. - -If a `Models//.swift` only declares enums/flags/protocols/layouts with no public func/var/init that needs MachO state, skip it; the scanner will not produce expected entries either. - -If `BaselineFixturePicker` cannot find a fixture entity for a given variant — log it, add a `CoverageAllowlistEntry` to `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift` (created in Task 16) with reason `needs fixture extension`, and proceed. - -The fixture variants chosen per task are documented inline below. - -### Task 5: `Anonymous/`, `Module/`, `Extension/` - -**Files (testable):** -- `Anonymous/AnonymousContext.swift`, `AnonymousContextDescriptor.swift`, `AnonymousContextDescriptorProtocol.swift`, `AnonymousContextDescriptorFlags.swift` -- `Module/ModuleContext.swift`, `ModuleContextDescriptor.swift`, `ModuleContextDescriptorProtocol.swift` -- `Extension/ExtensionContext.swift`, `ExtensionContextDescriptor.swift`, `ExtensionContextDescriptorProtocol.swift` - -**Fixture variants:** -- `Anonymous`: anonymous context arises from generic param scopes — pick from any generic struct's parent chain (e.g. `GenericFieldLayout.GenericStructNonRequirement`). -- `Module`: pick the `SymbolTestsCore` module context itself (from any descriptor's parent chain). -- `Extension`: pick the extension on `Structs.StructTest` for `Protocols.ProtocolWitnessTableTest` (in `SymbolTestsCore/Structs.swift`). - -- [ ] **Step 1: Apply Task 4 Steps 1-13 to Anonymous/** - -For each file in `Models/Anonymous/`: -- Inventory public members. -- Extend `BaselineFixturePicker` with `anonymous_*` accessors. -- Add `Anonymous*BaselineGenerator.swift` sub-generators. -- Wire into `BaselineGenerator`: add `case ""` to `dispatchSuite` + matching call in `generateAll`. -- Run `swift run baseline-generator`; verify baselines look reasonable. -- Write `Anonymous*Tests.swift` Suites under `Tests/MachOSwiftSectionTests/Fixtures/Anonymous/`. -- `swift test --filter Anonymous`. - -- [ ] **Step 2: Apply Task 4 Steps 1-13 to Module/** - -Same as Step 1, scoped to `Models/Module/`. - -- [ ] **Step 3: Apply Task 4 Steps 1-13 to Extension/** - -Same as Step 1, scoped to `Models/Extension/`. - -- [ ] **Step 4: Confirm idempotence + run all three sub-Suite groups** - -```bash -swift run baseline-generator && git status Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ -swift test --filter "Anonymous|Module|Extension" 2>&1 | xcsift -``` - -Expected: no baseline diffs, all tests pass. - -- [ ] **Step 5: Commit** - -```bash -git add Sources/MachOTestingSupport/Baseline/ \ - Tests/MachOSwiftSectionTests/Fixtures/Anonymous/ \ - Tests/MachOSwiftSectionTests/Fixtures/Module/ \ - Tests/MachOSwiftSectionTests/Fixtures/Extension/ \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ -git commit -m "$(cat <<'EOF' -test(MachOSwiftSection): add fixture-based Suites for Anonymous/Module/Extension - -Cover Anonymous/Module/Extension context wrappers and descriptors via -SymbolTestsCore fixture: anonymous (generic param scopes), module -(SymbolTestsCore module context), extension (Structs.StructTest extension on -Protocols.ProtocolWitnessTableTest). Each public member gets a @Test with -cross-reader equality + baseline literal. -EOF -)" -``` - -### Task 6: `ContextDescriptor/` - -**Files (testable):** -- `ContextDescriptor.swift`, `ContextDescriptorProtocol.swift`, `ContextDescriptorWrapper.swift`, `ContextProtocol.swift`, `ContextWrapper.swift`, `NamedContextDescriptorProtocol.swift` - -(Skip: `*Layout.swift`, `*Flags.swift`, `*Kind.swift`, `KindSpecificFlags.swift` — pure data types.) - -**Fixture variants:** Use `Structs.StructTest` ContextDescriptor for testing flags/parent/name; use `SymbolTestsCore` module context for `ContextWrapper.parent`/`forContextDescriptorWrapper`. - -- [ ] **Step 1: Apply Task 4 pattern to `ContextDescriptor/`** - -Mirror Task 5 substeps. For `ContextDescriptorWrapper` and `ContextWrapper`, the fixture variants need to span `class`/`struct`/`enum`/`protocol`/`extension`/`anonymous`/`module` cases — pick one per ContextDescriptorKind. - -- [ ] **Step 2: Confirm + run** - -```bash -swift run baseline-generator && git status Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ -swift test --filter "ContextDescriptor" 2>&1 | xcsift -``` - -- [ ] **Step 3: Commit** - -```bash -git add Sources/MachOTestingSupport/Baseline/Generators/ContextDescriptor*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/ContextDescriptor/ \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ContextDescriptor*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Context*.swift -git commit -m "test(MachOSwiftSection): add fixture-based Suites for ContextDescriptor/" -``` - -### Task 7: `Type/Class/` (incl. `Method/`, `Metadata/`, `Resilient/`) - -**Files (testable):** `Class.swift`, `ClassDescriptor.swift`, `ClassFlags.swift` (only public funcs/vars), `Method/MethodDescriptor.swift`, `Method/MethodOverrideDescriptor.swift`, `Method/MethodDefaultOverrideDescriptor.swift`, `Method/MethodDescriptorWrapper.swift`, `Method/VTableDescriptorHeader.swift`, `Method/OverrideTableHeader.swift`, `Method/MethodDefaultOverrideTableHeader.swift`, `Method/MethodImplementationPointer.swift`, `Metadata/AnyClassMetadata/AnyClassMetadata.swift`, `Metadata/AnyClassMetadata/AnyClassMetadataProtocol.swift`, `Metadata/AnyClassMetadataObjCInterop/AnyClassMetadataObjCInterop.swift`, `Metadata/AnyClassMetadataObjCInterop/AnyClassMetadataObjCInteropProtocol.swift`, `Metadata/Bounds/ClassMetadataBounds.swift`, `Metadata/Bounds/ClassMetadataBoundsProtocol.swift`, `Metadata/Bounds/StoredClassMetadataBounds.swift`, `Metadata/ClassMetadata/ClassMetadata.swift`, `Metadata/ClassMetadata/ClassMetadataProtocol.swift`, `Metadata/ClassMetadataObjCInterop/ClassMetadataObjCInterop.swift`, `Metadata/ClassMetadataObjCInterop/ClassMetadataObjCInteropProtocol.swift`, `Metadata/FinalClassMetadataProtocol.swift`, `Metadata/ObjCClassWrapperMetadata.swift`, `Resilient/ResilientSuperclass.swift`, `Resilient/ObjCResilientClassStubInfo.swift` - -**Fixture variants:** -- Plain class: `Classes.SimpleClassTest` (or whatever exists in `SymbolTestsCore/Classes.swift`). -- Diamond: pick from `DiamondInheritance.swift`. -- ObjC interop: pick from `Classes.swift` for an `NSObject`-derived class. -- Generic class: pick from `ClassBoundGenerics.swift`. - -- [ ] **Step 1: Apply Task 4 pattern to each file under `Models/Type/Class/`** - -Many files (~25 testable). Group sub-generators under `Sources/MachOTestingSupport/Baseline/Generators/Class/`. Suites under `Tests/MachOSwiftSectionTests/Fixtures/Type/Class/`. - -- [ ] **Step 2: Confirm + run** - -```bash -swift run baseline-generator && git status Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ -swift test --filter "Type/Class" 2>&1 | xcsift -``` - -- [ ] **Step 3: Commit** - -```bash -git add Sources/MachOTestingSupport/Baseline/Generators/Class/ \ - Tests/MachOSwiftSectionTests/Fixtures/Type/Class/ \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Class*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AnyClassMetadata*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Method*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/VTable*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Resilient*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Override*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StoredClass*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ObjCClass*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ObjCResilient*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/FinalClass*.swift -git commit -m "test(MachOSwiftSection): add fixture-based Suites for Type/Class/" -``` - -### Task 8: `Type/Enum/` - -**Files (testable):** `Enum.swift`, `EnumDescriptor.swift`, `EnumFunctions.swift` (if it has public APIs), `MultiPayloadEnumDescriptor.swift`, `Metadata/EnumMetadata.swift`, `Metadata/EnumMetadataProtocol.swift` - -**Fixture variants:** -- No-payload: from `Enums.swift`. -- Single payload: from `Enums.swift`. -- Multi-payload: from `Enums.swift` (the test types in `MetadataAccessorTests.swift` already document these — adapt names). - -- [ ] **Step 1: Apply Task 4 pattern to `Models/Type/Enum/`** - -- [ ] **Step 2: Confirm + run** - -```bash -swift run baseline-generator && git status Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ -swift test --filter "Type/Enum" 2>&1 | xcsift -``` - -- [ ] **Step 3: Commit** - -```bash -git add Sources/MachOTestingSupport/Baseline/Generators/Enum/ \ - Tests/MachOSwiftSectionTests/Fixtures/Type/Enum/ \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Enum*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MultiPayload*.swift -git commit -m "test(MachOSwiftSection): add fixture-based Suites for Type/Enum/" -``` - -### Task 9: `Type/` root files - -**Files (testable):** `TypeContextDescriptor.swift`, `TypeContextDescriptorWrapper.swift`, `TypeContextWrapper.swift`, `TypeContextDescriptorProtocol.swift`, `TypeReference.swift`, `TypeMetadataRecord.swift`, `ValueMetadata.swift`, `ValueMetadataProtocol.swift` - -**Fixture variants:** mix of `Structs.StructTest` (struct), `Classes.SimpleClassTest` (class), `Enums.SimpleEnumTest` (enum) — these wrappers/descriptors abstract over kind, so each test runs against all three to catch kind-specific reader bugs. - -- [ ] **Step 1: Apply Task 4 pattern to `Models/Type/` root files** - -- [ ] **Step 2: Confirm + run** - -```bash -swift run baseline-generator && git status Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ -swift test --filter "Type/" 2>&1 | xcsift -``` - -(Note: `Type/` filter will match `Type/Class/`, `Type/Enum/`, `Type/Struct/`, `Type/` root — confirm all green.) - -- [ ] **Step 3: Commit** - -```bash -git add Sources/MachOTestingSupport/Baseline/Generators/Type*.swift \ - Sources/MachOTestingSupport/Baseline/Generators/ValueMetadata*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/Type/Type*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/Type/Value*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Type*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ValueMetadata*.swift -git commit -m "test(MachOSwiftSection): add fixture-based Suites for Type/ root files" -``` - -### Task 10: `Protocol/` - -**Files (testable):** `Protocol.swift`, `ProtocolDescriptor.swift`, `ProtocolDescriptorProtocol.swift`, `ProtocolDescriptorRef.swift`, `ProtocolDescriptorWithObjCInterop.swift`, `ProtocolRecord.swift`, `ProtocolRequirement.swift`, `ProtocolWitnessTable.swift`, `ResilientWitness.swift`, `ResilientWitnessesHeader.swift`, `ObjC/ObjCProtocolPrefix.swift`, `ObjC/RelativeObjCProtocolPrefix.swift`, `Invertible/InvertibleProtocolSet.swift` - -**Fixture variants:** -- Plain protocol: `Protocols.ProtocolTest` (from `SymbolTestsCore/Protocols.swift`). -- Witness-table protocol: `Protocols.ProtocolWitnessTableTest`. -- Associated-type protocol: pick from `AssociatedTypeWitnessPatterns.swift`. -- ObjC protocol: pick a `@objc protocol` from `Protocols.swift` if available; otherwise add to allowlist with reason "needs fixture extension". - -- [ ] **Step 1: Apply Task 4 pattern** - -For `ResilientWitness.implementationAddress` (MachO-only debug formatter), add a `CoverageAllowlistEntry` (created in Task 16) referencing the source comment that already explains the omission. - -- [ ] **Step 2: Confirm + run** - -```bash -swift run baseline-generator && git status Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ -swift test --filter "Protocol" 2>&1 | xcsift -``` - -(Filter matches `Protocol/`, `ProtocolConformance/` — make sure both pass once Task 11 lands.) - -- [ ] **Step 3: Commit** - -```bash -git add Sources/MachOTestingSupport/Baseline/Generators/Protocol/ \ - Tests/MachOSwiftSectionTests/Fixtures/Protocol/ \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Protocol*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientWitness*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ObjCProtocol*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Invertible*.swift -git commit -m "test(MachOSwiftSection): add fixture-based Suites for Protocol/" -``` - -### Task 11: `ProtocolConformance/` - -**Files (testable):** `ProtocolConformance.swift`, `ProtocolConformanceDescriptor.swift`, `GlobalActorReference.swift` (if applicable) - -**Fixture variants:** -- Concrete struct conforming to plain protocol: `Structs.StructTest: Protocols.ProtocolTest`. -- Class conforming to multiple protocols: pick from `ConditionalConformanceVariants.swift` or `Codable.swift`. -- Conditional conformance: pick from `ConditionalConformanceVariants.swift`. -- GlobalActor: from `Actors.swift` or `Concurrency.swift`. - -- [ ] **Step 1: Apply Task 4 pattern** - -- [ ] **Step 2: Confirm + run** - -```bash -swift run baseline-generator && git status Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ -swift test --filter "ProtocolConformance" 2>&1 | xcsift -``` - -- [ ] **Step 3: Commit** - -```bash -git add Sources/MachOTestingSupport/Baseline/Generators/ProtocolConformance/ \ - Tests/MachOSwiftSectionTests/Fixtures/ProtocolConformance/ \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ProtocolConformance*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/GlobalActorReference*.swift -git commit -m "test(MachOSwiftSection): add fixture-based Suites for ProtocolConformance/" -``` - -### Task 12: `Generic/` - -**Files (testable, public methods):** `GenericContext.swift`, `GenericRequirement.swift`, `GenericRequirementDescriptor.swift`, `GenericContextDescriptorHeader.swift`, `GenericContextDescriptorHeaderProtocol.swift`, `GenericPackShapeDescriptor.swift`, `GenericPackShapeHeader.swift`, `GenericParamDescriptor.swift`, `GenericValueDescriptor.swift`, `GenericValueHeader.swift`, `GenericWitnessTable.swift`, `TypeGenericContext.swift`, `TypeGenericContextDescriptorHeader.swift`, `GenericEnvironment.swift` - -(Skip `*Flags.swift`, `*Kind.swift`, `*Type.swift` (pure data types).) - -**Fixture variants:** -- No-requirement generic struct: `GenericFieldLayout.GenericStructNonRequirement`. -- Layout-requirement: `GenericFieldLayout.GenericStructLayoutRequirement`. -- Swift-protocol-requirement: `GenericFieldLayout.GenericStructSwiftProtocolRequirement`. -- ObjC-protocol-requirement: `GenericFieldLayout.GenericStructObjCProtocolRequirement`. -- Same-type-requirement: from `SameTypeRequirements.swift`. -- Multiple variants from `GenericRequirementVariants.swift`. - -- [ ] **Step 1: Apply Task 4 pattern** - -- [ ] **Step 2: Confirm + run** - -```bash -swift run baseline-generator && git status Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ -swift test --filter "Generic" 2>&1 | xcsift -``` - -- [ ] **Step 3: Commit** - -```bash -git add Sources/MachOTestingSupport/Baseline/Generators/Generic/ \ - Tests/MachOSwiftSectionTests/Fixtures/Generic/ \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Generic*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeGeneric*.swift -git commit -m "test(MachOSwiftSection): add fixture-based Suites for Generic/" -``` - -### Task 13: `FieldDescriptor/`, `FieldRecord/`, `AssociatedType/` - -**Files (testable):** -- `FieldDescriptor/FieldDescriptor.swift` -- `FieldRecord/FieldRecord.swift` -- `AssociatedType/AssociatedType.swift`, `AssociatedTypeDescriptor.swift`, `AssociatedTypeRecord.swift` - -**Fixture variants:** -- Plain field-bearing struct: `Structs.StructTest`. -- Generic struct: `GenericFieldLayout.GenericStructNonRequirement`. -- AssociatedType: pick from `AssociatedTypeWitnessPatterns.swift`. - -- [ ] **Step 1: Apply Task 4 pattern** - -- [ ] **Step 2: Confirm + run** - -```bash -swift run baseline-generator && git status Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ -swift test --filter "FieldDescriptor|FieldRecord|AssociatedType" 2>&1 | xcsift -``` - -- [ ] **Step 3: Commit** - -```bash -git add Sources/MachOTestingSupport/Baseline/Generators/FieldDescriptor*.swift \ - Sources/MachOTestingSupport/Baseline/Generators/FieldRecord*.swift \ - Sources/MachOTestingSupport/Baseline/Generators/AssociatedType*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/FieldDescriptor/ \ - Tests/MachOSwiftSectionTests/Fixtures/FieldRecord/ \ - Tests/MachOSwiftSectionTests/Fixtures/AssociatedType/ \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Field*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AssociatedType*.swift -git commit -m "test(MachOSwiftSection): add fixture-based Suites for FieldDescriptor/FieldRecord/AssociatedType" -``` - -### Task 14: `Metadata/` - -**Files (testable):** `Metadata.swift`, `MetadataAccessorFunction.swift`, `MetadataBounds.swift`, `MetadataBoundsProtocol.swift`, `MetadataProtocol.swift`, `MetadataRequest.swift`, `MetadataResponse.swift`, `MetadataWrapper.swift`, `MetatypeMetadata.swift`, `FullMetadata.swift`, `FixedArrayTypeMetadata.swift`, `Headers/HeapMetadataHeader.swift`, `Headers/HeapMetadataHeaderProtocol.swift`, `Headers/HeapMetadataHeaderPrefix.swift`, `Headers/HeapMetadataHeaderPrefixProtocol.swift`, `Headers/TypeMetadataHeader.swift`, `Headers/TypeMetadataHeaderProtocol.swift`, `Headers/TypeMetadataHeaderBase.swift`, `Headers/TypeMetadataHeaderBaseProtocol.swift`, `Headers/TypeMetadataLayoutPrefix.swift`, `Headers/TypeMetadataLayoutPrefixProtocol.swift`, `MetadataInitialization/ForeignMetadataInitialization.swift`, `MetadataInitialization/SingletonMetadataInitialization.swift`, `CanonicalSpecialized*.swift` (if they have public methods), `HeapMetadataProtocol.swift`, `SingletonMetadataPointer.swift` - -(Skip pure layout/state/kind enums.) - -**Fixture variants:** mostly resolved via `MetadataAccessorFunction` — exercise across struct/class/enum kinds, generic vs non-generic, ObjC interop vs pure Swift. - -`metadataAccessorFunction` only resolves on `MachOImage` (not `MachOFile`); accordingly, sub-Suite tests targeting metadata wrappers must adapt the cross-reader equality block: -- For methods that read MachOImage-only state: skip MachOFile assertion, document why. -- For methods that read static descriptor state: full three-way assertion. - -- [ ] **Step 1: Apply Task 4 pattern** - -- [ ] **Step 2: Confirm + run** - -```bash -swift run baseline-generator && git status Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ -swift test --filter "Metadata" 2>&1 | xcsift -``` - -(`Metadata` filter matches Type/*/Metadata as well as Models/Metadata — confirm all pass.) - -- [ ] **Step 3: Commit** - -```bash -git add Sources/MachOTestingSupport/Baseline/Generators/Metadata/ \ - Tests/MachOSwiftSectionTests/Fixtures/Metadata/ \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Metadata*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Heap*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TypeMetadata*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Metatype*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/FullMetadata*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/FixedArray*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Foreign*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Singleton*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Canonical*.swift -git commit -m "test(MachOSwiftSection): add fixture-based Suites for Metadata/ (incl. Headers + Initialization)" -``` - -### Task 15: Misc — `ExistentialType/`, `TupleType/`, `OpaqueType/`, `BuiltinType/`, `ForeignType/`, `Function/`, `Heap/`, `Capture/`, `DispatchClass/`, `ValueWitnessTable/`, `Mangling/`, `Misc/` - -**Files (testable):** All public-method-bearing files in the listed subdirectories. Each subdirectory typically has 1-3 testable files. - -**Fixture variants per subdirectory:** -- `ExistentialType`: from `ExistentialAny.swift`, `ProtocolComposition.swift`. -- `TupleType`: from `Tuples.swift`. -- `OpaqueType`: from `OpaqueReturnTypes.swift`. -- `BuiltinType`: from `BuiltinTypeFields.swift`. -- `ForeignType`: depends — Swift CFTypes exposed via SymbolTestsCore. If none, add allowlist entries with `needs fixture extension`. -- `Function`: from `FunctionFeatures.swift`, `FunctionTypes.swift`. -- `Heap`: from `Closure.swift` if present, otherwise allowlist. -- `Capture`: from `Closure.swift` / generic functions. -- `DispatchClass`: ObjC dispatch metadata — pick from `Classes.swift` `NSObject`-derived test type. -- `ValueWitnessTable`: any concrete struct with non-trivial layout — `Structs.StructTest`. -- `Mangling`: `MangledName.swift` operates on raw bytes; pick any descriptor's mangled type name. -- `Misc/SpecialPointerAuthDiscriminators.swift`: typically constants — confirm with inventory and allowlist if no public methods worth testing. - -- [ ] **Step 1: Apply Task 4 pattern to each subdirectory** - -For each, follow Steps 1-13 of Task 4. Take care for `ForeignType` and `Heap` — add `CoverageAllowlistEntry`s (with reason `needs fixture extension`) if SymbolTestsCore doesn't have a sample that reaches those code paths. - -- [ ] **Step 2: Confirm + run** - -```bash -swift run baseline-generator && git status Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ -swift test --filter MachOSwiftSectionTests 2>&1 | xcsift -``` - -Expected: all currently-existing fixture tests pass. - -- [ ] **Step 3: Commit** - -```bash -git add Sources/MachOTestingSupport/Baseline/Generators/ \ - Tests/MachOSwiftSectionTests/Fixtures/ \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ -git commit -m "$(cat <<'EOF' -test(MachOSwiftSection): add fixture-based Suites for misc subdirectories - -Cover ExistentialType, TupleType, OpaqueType, BuiltinType, ForeignType, Function, -Heap, Capture, DispatchClass, ValueWitnessTable, Mangling, Misc. Subdirectories -without fixture coverage in SymbolTestsCore get CoverageAllowlist entries with -reason `needs fixture extension`. -EOF -)" -``` - ---- - -## Task 16: Coverage Invariant Test - -**Files:** -- Create: `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift` -- Create: `Tests/MachOSwiftSectionTests/Fixtures/MachOSwiftSectionCoverageInvariantTests.swift` -- Create: `Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AllFixtureSuites.swift` (auto-generated, but also editable manually as a fallback if generator hasn't gotten to it) - -- [ ] **Step 1: Write `CoverageAllowlistEntries`** - -`Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift`: - -```swift -import Foundation -import MachOTestingSupport - -/// Public members of MachOSwiftSection/Models/ that are intentionally not under -/// fixture-based test coverage. Each entry MUST carry a human-readable reason. -enum CoverageAllowlistEntries { - static let entries: [CoverageAllowlistEntry] = [ - // MachO-only debug formatters — no ReadingContext mirror exists by design. - .init( - typeName: "ResilientWitness", - memberName: "implementationAddress", - reason: "MachO-only debug formatter, documented in source" - ), - - // Subdirectories without SymbolTestsCore fixture coverage. Track these - // and address with a fixture extension when prioritized. - // Entries added per-task during Tasks 5-15 land here. - // Example (remove when fixture lands): - // .init( - // typeName: "ForeignClassMetadata", - // memberName: "classDescriptor", - // reason: "needs fixture extension — no foreign class in SymbolTestsCore" - // ), - ] - - static var keys: Set { Set(entries.map(\.key)) } -} -``` - -- [ ] **Step 2: Write `MachOSwiftSectionCoverageInvariantTests`** - -`Tests/MachOSwiftSectionTests/Fixtures/MachOSwiftSectionCoverageInvariantTests.swift`: - -```swift -import Foundation -import Testing -@testable import MachOTestingSupport - -@Suite -struct MachOSwiftSectionCoverageInvariantTests { - - private var modelsRoot: URL { - URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() // Fixtures/ - .deletingLastPathComponent() // MachOSwiftSectionTests/ - .deletingLastPathComponent() // Tests/ - .deletingLastPathComponent() // repo root - .appendingPathComponent("Sources/MachOSwiftSection/Models") - } - - @Test func everyPublicMemberHasATest() throws { - let scanner = PublicMemberScanner(sourceRoot: modelsRoot) - let expected = try scanner.scan(applyingAllowlist: CoverageAllowlistEntries.keys) - - let registered: Set = Set( - allFixtureSuites.flatMap { suite -> [MethodKey] in - suite.registeredTestMethodNames.map { name in - MethodKey(typeName: suite.testedTypeName, memberName: name) - } - } - ) - - let missing = expected.subtracting(registered) - let extra = registered.subtracting(expected) - - #expect( - missing.isEmpty, - """ - Missing tests for these public members of MachOSwiftSection/Models: - \(missing.sorted().map { " \($0)" }.joined(separator: "\n")) - - Tip: add the corresponding @Test func to the matching Suite, append the - name to its registeredTestMethodNames (or rerun - `swift run baseline-generator --suite `), and re-run. - """ - ) - #expect( - extra.isEmpty, - """ - Tests registered for non-existent (or refactored-away) public members: - \(extra.sorted().map { " \($0)" }.joined(separator: "\n")) - - Tip: source method was renamed or removed — sync the Suite's - registeredTestMethodNames + remove the orphan @Test. - """ - ) - } -} -``` - -- [ ] **Step 3: Generate `AllFixtureSuites.swift`** - -Either: -- Extend `BaselineGenerator.generateAll()` to emit `AllFixtureSuites.swift` listing every Suite registered so far. -- Or hand-write one (pre-populating with the Suites added in Tasks 4-15). - -For the auto-generated form, replace the `writeAllFixtureSuitesIndex` no-op stub in `BaselineGenerator.swift` (Task 4 Step 5) with an implementation that uses SwiftSyntaxBuilder: - -```swift -import SwiftSyntax -import SwiftSyntaxBuilder - -private static func writeAllFixtureSuitesIndex(outputDirectory: URL) throws { - // Hand-maintained list of every Suite type registered across Tasks 4-15. - // When a new Suite is added, update this list AND the dispatchSuite case - // (both can be done from one editor pass). - let suiteTypeNames = [ - "StructDescriptorTests", - "StructTests", - "StructMetadataTests", - "StructMetadataProtocolTests", - "AnonymousContextTests", - "AnonymousContextDescriptorTests", - // ... extend per Task 5-15 as Suites land - ].sorted() - - // `\(raw: "Foo.self")` because `\(literal:)` would treat the string as a - // String literal (i.e. emit `"Foo.self"`). - let suiteListItems = suiteTypeNames.map { "\($0).self" }.joined(separator: ",\n ") - - let header = """ - // AUTO-GENERATED — DO NOT EDIT. - // Regenerate via: swift run baseline-generator - // Generated: \(ISO8601DateFormatter().string(from: Date())) - """ - - let file: SourceFileSyntax = """ - \(raw: header) - - let allFixtureSuites: [any FixtureSuite.Type] = [ - \(raw: suiteListItems) - ] - """ - - let formatted = file.formatted().description + "\n" - let outputURL = outputDirectory.appendingPathComponent("AllFixtureSuites.swift") - try formatted.write(to: outputURL, atomically: true, encoding: .utf8) -} -``` - -(In practice, the generator could build the list from a registry populated by each sub-generator's call — but the hand-maintained list in `writeAllFixtureSuitesIndex` is simpler and the Coverage Invariant test in Step 4 below catches drift if a Suite is missing.) - -Run `swift run baseline-generator` to produce the file. - -- [ ] **Step 4: Run coverage test** - -```bash -swift test --filter MachOSwiftSectionCoverageInvariantTests 2>&1 | xcsift -``` - -Expected: passes (missing/extra are both empty). - -If `missing` is non-empty: each entry shows `.`. Either add a `@Test` and `registeredTestMethodNames` entry to the relevant Suite, or add a `CoverageAllowlistEntry` with a reason. Re-run. - -If `extra` is non-empty: a member name in `registeredTestMethodNames` doesn't match any public source member. Likely a typo or stale entry — fix and re-run. - -- [ ] **Step 5: Probe the guard works (manual verification)** - -Temporarily add to `Sources/MachOSwiftSection/Models/Type/Struct/StructDescriptor.swift`: - -```swift -extension StructDescriptor { - public func _coverageProbe() -> Int { 0 } -} -``` - -Run: `swift test --filter MachOSwiftSectionCoverageInvariantTests 2>&1 | xcsift` - -Expected: FAIL with `Missing tests for these public members ... StructDescriptor._coverageProbe`. - -Revert the probe. Re-run; expected: PASS. - -- [ ] **Step 6: Commit** - -```bash -git add Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift \ - Tests/MachOSwiftSectionTests/Fixtures/MachOSwiftSectionCoverageInvariantTests.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AllFixtureSuites.swift \ - Sources/MachOTestingSupport/Baseline/BaselineGenerator.swift -git commit -m "$(cat <<'EOF' -test(MachOSwiftSection): wire up coverage invariant guard - -Static SwiftSyntax scan of Sources/MachOSwiftSection/Models/ produces the -expected (typeName, memberName) set; reflection over allFixtureSuites produces -the registered set. missing/extra are both required to be empty. -CoverageAllowlistEntries collects intentional exclusions with reasons. -Verified by adding a probe public func and observing the test failed. -EOF -)" -``` - ---- - -## Task 17: `baseline-generator` Executable Polish - -**Files:** -- Modify: `Sources/baseline-generator/main.swift` — proper ArgumentParser CLI - -- [ ] **Step 1: Replace stub `main.swift` with proper CLI** - -```swift -// Sources/baseline-generator/main.swift -import Foundation -import ArgumentParser -import MachOTestingSupport - -@main -struct BaselineGeneratorMain: AsyncParsableCommand { - static let configuration = CommandConfiguration( - commandName: "baseline-generator", - abstract: "Regenerates ABI baselines for MachOSwiftSection fixture tests." - ) - - @Option( - name: .long, - help: "Output directory for baseline files. Defaults to Tests/MachOSwiftSectionTests/Fixtures/__Baseline__." - ) - var output: String = "Tests/MachOSwiftSectionTests/Fixtures/__Baseline__" - - @Option( - name: .long, - help: "Restrict regeneration to a specific Suite, e.g. StructDescriptor. If omitted, regenerates all baselines." - ) - var suite: String? - - func run() async throws { - let outputURL = URL(fileURLWithPath: output) - if let suite { - try await BaselineGenerator.generate(suite: suite, outputDirectory: outputURL) - } else { - try await BaselineGenerator.generateAll(outputDirectory: outputURL) - } - } -} -``` - -- [ ] **Step 2: Confirm `generate(suite:outputDirectory:)` dispatcher exists in `BaselineGenerator`** - -Task 4 Step 5 already established the dispatcher (`dispatchSuite(_:in:outputDirectory:)`) and the `generate(suite:outputDirectory:)` entry point. Tasks 5-15 should have already extended both `generateAll` and `dispatchSuite` with each new sub-generator. - -Verify by inspecting `Sources/MachOTestingSupport/Baseline/BaselineGenerator.swift`: - -```bash -rg "case \"" Sources/MachOTestingSupport/Baseline/BaselineGenerator.swift -``` - -Expected: one `case "":` line per sub-generator added across Tasks 4-15. - -If any `case` is missing for a suite that has a sub-generator file, add it (and the corresponding `try dispatchSuite("...", ...)` line in `generateAll`). Re-run `swift build`. - -- [ ] **Step 3: Test the CLI** - -```bash -swift run baseline-generator --suite StructDescriptor -git status Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructDescriptorBaseline.swift -``` - -Expected: file unchanged (idempotent regeneration of just one file). - -```bash -swift run baseline-generator --output /tmp/test-baselines -ls /tmp/test-baselines/ -``` - -Expected: full set of baseline files in `/tmp/test-baselines/`. - -- [ ] **Step 4: Test full regen idempotence** - -```bash -swift run baseline-generator -git diff Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ -``` - -Expected: empty diff. If non-empty, the generator is non-deterministic somewhere — fix. - -- [ ] **Step 5: Run full test suite** - -```bash -swift test --filter MachOSwiftSectionTests 2>&1 | xcsift -``` - -Expected: all green. - -- [ ] **Step 6: Commit** - -```bash -git add Sources/baseline-generator/ Sources/MachOTestingSupport/Baseline/BaselineGenerator.swift -git commit -m "$(cat <<'EOF' -feat(baseline-generator): polish CLI with --suite/--output flags - -Adds AsyncParsableCommand-based CLI to baseline-generator. --suite restricts -regeneration to one Suite (e.g. `swift run baseline-generator --suite StructDescriptor`), ---output overrides the default Tests/MachOSwiftSectionTests/Fixtures/__Baseline__. -EOF -)" -``` - ---- - -## Task 18: Final validation + cleanup - -**Files:** -- Modify: `CLAUDE.md` — add brief section on the new test infrastructure -- Modify: `.gitignore` if generated files leak - -- [ ] **Step 1: Validate the full Validation checklist from spec** - -```bash -swift test --filter MachOSwiftSectionTests 2>&1 | xcsift -swift test --filter MachOSwiftSectionCoverageInvariantTests 2>&1 | xcsift -swift run baseline-generator --suite StructDescriptor -git status Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructDescriptorBaseline.swift -``` - -Expected from the spec: -- All `swift test --filter MachOSwiftSectionTests` green. -- Coverage invariant green (missing/extra empty). -- `baseline-generator --suite ` is idempotent. - -- [ ] **Step 2: Probe Coverage guard with synthetic public method** - -```bash -# Temporarily add a public func -echo 'extension StructDescriptor { public func _probe() {} }' >> Sources/MachOSwiftSection/Models/Type/Struct/StructDescriptor.swift -swift test --filter MachOSwiftSectionCoverageInvariantTests 2>&1 | xcsift -# Should FAIL with "Missing tests for ... StructDescriptor._probe" -git checkout Sources/MachOSwiftSection/Models/Type/Struct/StructDescriptor.swift -swift test --filter MachOSwiftSectionCoverageInvariantTests 2>&1 | xcsift -# Should PASS -``` - -- [ ] **Step 3: Probe baseline assertion with manual edit** - -Open any `Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Baseline.swift`. Change one numeric value. Run the corresponding Suite — expect `#expect(... == ...)` failure with a clear message. Revert the change. - -- [ ] **Step 4: Update `CLAUDE.md`** - -In `CLAUDE.md`, add a new section under "Test Environment": - -```markdown -## Fixture-Based Test Coverage (MachOSwiftSection) - -`MachOSwiftSection/Models/` is exhaustively covered by `Tests/MachOSwiftSectionTests/Fixtures/`. Suites mirror the source directory and assert (a) cross-reader equality across MachOFile/MachOImage/InProcess + their ReadingContext counterparts, and (b) per-method ABI literal expected values from `__Baseline__/*Baseline.swift`. - -To add a new public method: - -1. Add the method. -2. Run `swift test --filter MachOSwiftSectionCoverageInvariantTests` to see which Suite needs updating. -3. Add a `@Test` to that Suite + append the member name to `registeredTestMethodNames`. -4. Run `swift run baseline-generator --suite ` to regenerate the baseline. -5. Re-run the affected Suite. - -To regenerate all baselines after fixture rebuild or toolchain upgrade: - -``` -xcodebuild -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj -scheme SymbolTestsCore -configuration Release build -swift run baseline-generator -git diff Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ # review drift -``` -``` - -- [ ] **Step 5: Commit** - -```bash -git add CLAUDE.md -git commit -m "docs(MachOSwiftSection): document fixture-based test coverage workflow" -``` - -- [ ] **Step 6: Final summary commit (optional)** - -```bash -git log --oneline feature/machoswift-section-fixture-tests ^feature/reading-context-api -``` - -Expected: ~18 commits showing the structured implementation. - ---- - -## Spec → Plan Coverage Check - -| Spec section | Plan task | -|---|---| -| §1 整体架构 — Fixture loading layer | Task 1 | -| §1 整体架构 — Suite layer | Tasks 4-15 | -| §1 整体架构 — Baseline generator layer | Tasks 4 (sub-generator), 17 (CLI polish) | -| §1 整体架构 — Coverage invariant layer | Task 16 | -| §2 Test Infrastructure — `MachOSwiftSectionFixtureTests` | Task 1 | -| §2.2 `MachOImageName.SymbolTestsCore` | Task 1 Step 1 | -| §2.3 `acrossAllReaders` / `acrossAllContexts` | Task 1 Step 4 | -| §3.1 文件组织 (镜像 Models/) | Tasks 4-15 | -| §3.2 Suite 模板 | Task 4 Step 8 (template), reused 5-15 | -| §3.3 fixture 主测目标 (主 + 变体) | Task 4 Step 2 + per-task variants | -| §3.4 Baseline 引用形态 | Task 4 Step 5 + per-task baselines | -| §4.1 baseline-generator executable | Task 4 Step 6 (stub), Task 17 (CLI) | -| §4.2 模块组织 | Task 4 Step 5 | -| §4.3 生成流程 | Task 4 Step 7 + per-task generator runs | -| §4.4 数值进制约定 | Task 2 (BaselineEmitter hex helper, with `\(literal:)` covering decimal/string/bool/array) | -| §4.5 重生成流程 | Task 18 Step 4 (CLAUDE.md docs) | -| §4.6 Generator 自身正确性保证 | Task 4 Step 5 (generator only uses MachOFile path) + Task 2 (emitter unit tests) | -| §5.1 数据源 (expected via SwiftSyntax + registered via reflection) | Task 3 (scanner) + Task 16 (invariant test) | -| §5.2 MethodKey | Task 3 Step 3 | -| §5.3 Scanner 实现 | Task 3 Step 6 | -| §5.4 Coverage Test | Task 16 Step 2 | -| §5.5 失败信息 | Task 16 Step 2 (#expect messages) | -| §5.6 Coverage / Generator 协作矩阵 | Tasks 16+18 (probe verification) | -| §6.1 入测范围 | Task 3 Step 6 (scanner config) | -| §6.2 显式 Exclusions | Task 16 Step 1 (CoverageAllowlistEntries) | -| §7 Risks & Mitigations | Task 1 (FixtureLoadError), Task 4 (idempotence check), Task 16 (probe) | -| Validation checklist | Task 18 Steps 1-3 | - ---- - -**Plan complete.** diff --git a/docs/superpowers/plans/2026-05-05-fixture-coverage-tightening.md b/docs/superpowers/plans/2026-05-05-fixture-coverage-tightening.md deleted file mode 100644 index bf8363af..00000000 --- a/docs/superpowers/plans/2026-05-05-fixture-coverage-tightening.md +++ /dev/null @@ -1,3437 +0,0 @@ -# Fixture-Coverage Tightening Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Eliminate the silent-sentinel coverage gap discovered in PR #85 review by tagging every sentinel-only Suite with a typed `SentinelReason`, converting ~30 runtime-only metadata Suites to InProcess single-reader real tests, and adding ~7 new SymbolTestsCore fixture types so the `needsFixtureExtension` category clears. - -**Architecture:** Three-phase migration on the existing `feature/machoswift-section-fixture-tests` branch. Phase A introduces the new schema + a SwiftSyntax-based `SuiteBehaviorScanner` and tightens `MachOSwiftSectionCoverageInvariantTests` with two new assertions (`liarSentinel`, `unmarkedSentinel`). Phase C adds an `InProcessMetadataPicker` and converts runtime-only Suites to InProcess single-reader tests. Phase B adds fixture types to `SymbolTestsCore` and converts `needsFixtureExtension` Suites to cross-reader tests. Phase D refreshes docs. - -**Tech Stack:** Swift 6.2 / Xcode 26, swift-testing (`@Test`/`#expect`/`@Suite`), SwiftSyntax for source-level scanning, swift-argument-parser for `baseline-generator`, custom SwiftPM command plugin (`regen-baselines`), `SymbolTestsCore.framework` Mach-O fixture, `MachOFile`/`MachOImage`/`InProcessContext` readers from MachOFoundation. - -**Spec:** [`docs/superpowers/specs/2026-05-05-fixture-coverage-tightening-design.md`](../specs/2026-05-05-fixture-coverage-tightening-design.md) - ---- - -## File Structure - -### Phase A — Mechanism - -| Action | Path | Responsibility | -|---|---|---| -| Modify | `Sources/MachOFixtureSupport/Coverage/CoverageAllowlist.swift` | Extend with `SentinelReason`, `AllowlistKind`, `sentinelGroup(...)` helper. Keep `legacyExempt` path. | -| Create | `Sources/MachOFixtureSupport/Coverage/SuiteBehaviorScanner.swift` | SwiftSyntax-based per-method scanner producing `[MethodKey: MethodBehavior]`. | -| Create | `Tests/MachOTestingSupportTests/Coverage/SuiteBehaviorScannerTests.swift` | Unit tests for scanner using fixture sample sources. | -| Create | `Tests/MachOTestingSupportTests/Coverage/Fixtures/SuiteSampleSource.swift.txt` | Sample suites in 3 behaviors for scanner unit test. | -| Modify | `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift` | Replace single legacy entry with sentinel-grouped entries for all 88 sentinel suites. | -| Modify | `Tests/MachOSwiftSectionTests/Fixtures/MachOSwiftSectionCoverageInvariantTests.swift` | Add `③ liarSentinel` + `④ unmarkedSentinel` assertions. | - -### Phase C — Runtime-only InProcess conversion - -| Action | Path | Responsibility | -|---|---|---| -| Create | `Sources/MachOFixtureSupport/InProcess/InProcessMetadataPicker.swift` | Static `UnsafeRawPointer` constants for stdlib + fixture-bound metadata. | -| Modify | `Sources/MachOTestingSupport/MachOSwiftSectionFixtureTests.swift` | Add `usingInProcessOnly(...)` helper. | -| Modify (~30) | `Tests/MachOSwiftSectionTests/Fixtures/**/*Tests.swift` | Replace `registrationOnly` with real `usingInProcessOnly`-based tests. | -| Modify (~30) | `Sources/MachOFixtureSupport/Baseline/Generators/**/*BaselineGenerator.swift` | Emit ABI-literal `Entry` from InProcess metadata pointer. | -| Modify (~30) | `Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/*Baseline.swift` | Regenerated via `swift package regen-baselines`. | -| Modify | `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift` | Remove converted `runtimeOnly` entries. | - -### Phase B — SymbolTestsCore fixture extension - -| Action | Path | Responsibility | -|---|---|---| -| Create | `Tests/Projects/SymbolTests/SymbolTestsCore/DefaultOverrideTable.swift` | Class with dynamic replacement to surface default-override table | -| Create | `Tests/Projects/SymbolTests/SymbolTestsCore/ResilientClasses.swift` | Resilient class + resilient superclass references | -| Create | `Tests/Projects/SymbolTests/SymbolTestsCore/ObjCClassWrappers.swift` | NSObject-inheriting Swift classes | -| Create | `Tests/Projects/SymbolTests/SymbolTestsCore/ObjCResilientStubs.swift` | Swift class inheriting resilient ObjC class | -| Create | `Tests/Projects/SymbolTests/SymbolTestsCore/CanonicalSpecializedMetadata.swift` | `@_specialize(exported: true)` generic types | -| Create | `Tests/Projects/SymbolTests/SymbolTestsCore/ForeignTypes.swift` | Foreign class import + foreign reference type | -| Create | `Tests/Projects/SymbolTests/SymbolTestsCore/GenericValueParameters.swift` | Type with `` value generic parameters | -| Modify | `Sources/MachOFixtureSupport/Baseline/BaselineFixturePicker.swift` | Add picker function per new fixture | -| Modify | various `Sources/MachOFixtureSupport/Baseline/Generators/**/*.swift` | Wire picker → generator | -| Modify | various `Tests/MachOSwiftSectionTests/Fixtures/**/*Tests.swift` | Convert `registrationOnly` → real cross-reader test | -| Rebuild | `Tests/Projects/SymbolTests/DerivedData/.../SymbolTestsCore.framework` | Via `xcodebuild ... build` | -| Modify | `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift` | Remove converted `needsFixtureExtension` entries | - -### Phase D — Docs - -| Action | Path | Responsibility | -|---|---|---| -| Modify | `CLAUDE.md` | Update fixture-coverage section with sentinel concept and `regen-baselines` plugin reference | - ---- - -## Phase A — Mechanism - -### Task A1: Introduce `SentinelReason`/`AllowlistKind` schema and `SuiteBehaviorScanner` - -**Files:** -- Modify: `Sources/MachOFixtureSupport/Coverage/CoverageAllowlist.swift` -- Create: `Sources/MachOFixtureSupport/Coverage/SuiteBehaviorScanner.swift` -- Create: `Tests/MachOTestingSupportTests/Coverage/Fixtures/SuiteSampleSource.swift.txt` -- Create: `Tests/MachOTestingSupportTests/Coverage/SuiteBehaviorScannerTests.swift` -- Modify: `Package.swift` (extend `MachOTestingSupportTests.exclude` for new fixture) - -- [ ] **Step 1: Extend `CoverageAllowlist.swift` with new schema (additive, keep current public surface working)** - -Replace the contents of `Sources/MachOFixtureSupport/Coverage/CoverageAllowlist.swift` with: - -```swift -import Foundation - -/// Why a `(typeName, memberName)` pair is allowed to skip cross-reader fixture coverage. -package enum SentinelReason: Hashable { - /// The type is allocated by the Swift runtime at type-load time and is - /// never serialized into the fixture's Mach-O image. Covered via - /// `InProcessMetadataPicker` + single-reader assertions instead. - case runtimeOnly(detail: String) - - /// SymbolTestsCore currently lacks a sample that surfaces this metadata - /// shape. Should be eliminated by extending the fixture (Phase B). - case needsFixtureExtension(detail: String) - - /// Pure raw-value enum / marker protocol / pure-data utility. Sentinel - /// status is intended to be permanent. Future follow-ups may pin - /// `rawValue` literals as a deeper assertion. - case pureDataUtility(detail: String) -} - -/// Either a legacy "scanner-saw-it-but-it-shouldn't-count" exemption (kept as-is -/// from PR #85) or a typed sentinel with a reason. -package enum AllowlistKind: Hashable { - case legacyExempt(reason: String) - case sentinel(SentinelReason) -} - -/// A single entry exempting one (typeName, memberName) pair from coverage requirements. -package struct CoverageAllowlistEntry: Hashable, CustomStringConvertible { - package let key: MethodKey - package let kind: AllowlistKind - - package init(typeName: String, memberName: String, reason: String) { - self.key = MethodKey(typeName: typeName, memberName: memberName) - self.kind = .legacyExempt(reason: reason) - } - - package init(typeName: String, memberName: String, sentinel: SentinelReason) { - self.key = MethodKey(typeName: typeName, memberName: memberName) - self.kind = .sentinel(sentinel) - } - - package var description: String { - switch kind { - case .legacyExempt(let reason): - return "\(key) // legacyExempt: \(reason)" - case .sentinel(let reason): - return "\(key) // sentinel: \(reason)" - } - } -} -``` - -- [ ] **Step 2: Verify build still works after schema extension** - -Run: -```bash -swift build 2>&1 | tail -3 -``` -Expected: -``` -Build complete! -``` - -This proves the schema extension is source-compatible — `CoverageAllowlistEntries.swift` (Tests target) still uses the old `init(typeName:memberName:reason:)` initializer, which the new schema preserves. - -- [ ] **Step 3: Create the SwiftSyntax sample-source fixture for scanner tests** - -Create `Tests/MachOTestingSupportTests/Coverage/Fixtures/SuiteSampleSource.swift.txt`: - -```swift -// Sample suites consumed by SuiteBehaviorScannerTests via on-disk reads. -// File extension intentionally `.swift.txt` so SPM ignores it during builds. - -import Testing - -@Suite -final class CrossReaderTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable { - static let testedTypeName = "CrossReaderType" - static var registeredTestMethodNames: Set { ["liveMethod"] } - - @Test func liveMethod() async throws { - let result = try acrossAllReaders( - file: { 1 }, - image: { 1 } - ) - #expect(result == 1) - } -} - -@Suite -final class InProcessOnlyTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable { - static let testedTypeName = "RuntimeOnlyType" - static var registeredTestMethodNames: Set { ["kind"] } - - @Test func kind() async throws { - let result = try usingInProcessOnly { context in - 42 - } - #expect(result == 42) - } -} - -@Suite -final class SentinelTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable { - static let testedTypeName = "RegistrationOnlyType" - static var registeredTestMethodNames: Set { ["registeredOnly"] } - - @Test func registrationOnly() async throws { - #expect(SentinelTests.registeredTestMethodNames.contains("registeredOnly")) - } -} -``` - -- [ ] **Step 4: Update `Package.swift` to exclude the new sample-source fixture** - -In `Package.swift`, find `MachOTestingSupportTests` target definition (around line 619-629). Update its `exclude` array to also include the new fixture: - -```swift -static let MachOTestingSupportTests = Target.testTarget( - name: "MachOTestingSupportTests", - dependencies: [ - .target(.MachOTestingSupport), - .target(.MachOFixtureSupport), - ], - exclude: [ - "Coverage/Fixtures/SampleSource.swift.txt", - "Coverage/Fixtures/SuiteSampleSource.swift.txt", - ], - swiftSettings: testSettings -) -``` - -- [ ] **Step 5: Write the failing scanner test** - -Create `Tests/MachOTestingSupportTests/Coverage/SuiteBehaviorScannerTests.swift`: - -```swift -import Foundation -import Testing -@testable import MachOTestingSupport -import MachOFixtureSupport - -@Suite -struct SuiteBehaviorScannerTests { - private var fixtureRoot: URL { - URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() - .appendingPathComponent("Fixtures") - } - - private func makeScanRoot() throws -> URL { - let tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) - let source = try String(contentsOf: fixtureRoot.appendingPathComponent("SuiteSampleSource.swift.txt")) - let dest = tempDir.appendingPathComponent("SuiteSampleSource.swift") - try source.write(to: dest, atomically: true, encoding: .utf8) - return tempDir - } - - @Test func detectsAcrossAllReaders() throws { - let root = try makeScanRoot() - defer { try? FileManager.default.removeItem(at: root) } - let scanner = SuiteBehaviorScanner(suiteRoot: root) - let result = try scanner.scan() - let key = MethodKey(typeName: "CrossReaderType", memberName: "liveMethod") - #expect(result[key] == .acrossAllReaders) - } - - @Test func detectsInProcessOnly() throws { - let root = try makeScanRoot() - defer { try? FileManager.default.removeItem(at: root) } - let scanner = SuiteBehaviorScanner(suiteRoot: root) - let result = try scanner.scan() - let key = MethodKey(typeName: "RuntimeOnlyType", memberName: "kind") - #expect(result[key] == .inProcessOnly) - } - - @Test func detectsSentinel() throws { - let root = try makeScanRoot() - defer { try? FileManager.default.removeItem(at: root) } - let scanner = SuiteBehaviorScanner(suiteRoot: root) - let result = try scanner.scan() - let key = MethodKey(typeName: "RegistrationOnlyType", memberName: "registrationOnly") - #expect(result[key] == .sentinel) - } -} -``` - -- [ ] **Step 6: Run scanner test, confirm it fails because `SuiteBehaviorScanner` doesn't exist** - -Run: -```bash -swift test --filter SuiteBehaviorScannerTests 2>&1 | tail -10 -``` -Expected: -``` -error: cannot find 'SuiteBehaviorScanner' in scope -``` - -- [ ] **Step 7: Implement `SuiteBehaviorScanner`** - -Create `Sources/MachOFixtureSupport/Coverage/SuiteBehaviorScanner.swift`: - -```swift -import Foundation -import SwiftSyntax -import SwiftParser - -/// Scans `*Tests.swift` Suite source files and reports per-method behavior: -/// whether each `@Test func` calls `acrossAllReaders` / `acrossAllContexts`, -/// `usingInProcessOnly` / `inProcessContext`, or neither. -/// -/// Used by `MachOSwiftSectionCoverageInvariantTests` to enforce that every -/// sentinel-only method is declared in `CoverageAllowlistEntries`. -package struct SuiteBehaviorScanner { - package enum MethodBehavior: Equatable { - case acrossAllReaders - case inProcessOnly - case sentinel - } - - package let suiteRoot: URL - - package init(suiteRoot: URL) { - self.suiteRoot = suiteRoot - } - - package func scan() throws -> [MethodKey: MethodBehavior] { - let files = try collectSwiftFiles(under: suiteRoot) - var result: [MethodKey: MethodBehavior] = [:] - for fileURL in files { - let source = try String(contentsOf: fileURL, encoding: .utf8) - let tree = Parser.parse(source: source) - let visitor = SuiteBehaviorVisitor(viewMode: .sourceAccurate) - visitor.walk(tree) - for entry in visitor.collected { - let key = MethodKey(typeName: entry.testedTypeName, memberName: entry.methodName) - result[key] = entry.behavior - } - } - return result - } - - private func collectSwiftFiles(under root: URL) throws -> [URL] { - let fileManager = FileManager.default - let enumerator = fileManager.enumerator(at: root, includingPropertiesForKeys: nil) - var files: [URL] = [] - while let url = enumerator?.nextObject() as? URL { - if url.pathExtension == "swift" { files.append(url) } - } - return files - } -} - -private final class SuiteBehaviorVisitor: SyntaxVisitor { - struct Entry { - let testedTypeName: String - let methodName: String - let behavior: SuiteBehaviorScanner.MethodBehavior - } - private(set) var collected: [Entry] = [] - private var currentTestedTypeName: String? - - override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { - currentTestedTypeName = extractTestedTypeName(from: node.memberBlock) - return .visitChildren - } - override func visitPost(_ node: ClassDeclSyntax) { - currentTestedTypeName = nil - } - - override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { - currentTestedTypeName = extractTestedTypeName(from: node.memberBlock) - return .visitChildren - } - override func visitPost(_ node: StructDeclSyntax) { - currentTestedTypeName = nil - } - - override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { - guard hasTestAttribute(node.attributes), - let testedTypeName = currentTestedTypeName, - let body = node.body else { - return .skipChildren - } - let behavior = inferBehavior(from: body) - collected.append(Entry( - testedTypeName: testedTypeName, - methodName: node.name.text, - behavior: behavior - )) - return .skipChildren - } - - private func extractTestedTypeName(from memberBlock: MemberBlockSyntax) -> String? { - for member in memberBlock.members { - guard let varDecl = member.decl.as(VariableDeclSyntax.self) else { continue } - let isStatic = varDecl.modifiers.contains(where: { $0.name.text == "static" }) - guard isStatic else { continue } - for binding in varDecl.bindings { - guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self), - pattern.identifier.text == "testedTypeName", - let initializer = binding.initializer, - let stringLit = initializer.value.as(StringLiteralExprSyntax.self) - else { continue } - let value = stringLit.segments.compactMap { - $0.as(StringSegmentSyntax.self)?.content.text - }.joined() - if !value.isEmpty { return value } - } - } - return nil - } - - private func hasTestAttribute(_ attributes: AttributeListSyntax) -> Bool { - for attribute in attributes { - if let attr = attribute.as(AttributeSyntax.self), - attr.attributeName.trimmedDescription == "Test" { - return true - } - } - return false - } - - private func inferBehavior(from body: CodeBlockSyntax) -> SuiteBehaviorScanner.MethodBehavior { - let bodyText = body.description - if bodyText.contains("acrossAllReaders") || bodyText.contains("acrossAllContexts") { - return .acrossAllReaders - } - if bodyText.contains("usingInProcessOnly") || bodyText.contains("inProcessContext") { - return .inProcessOnly - } - return .sentinel - } -} -``` - -- [ ] **Step 8: Run scanner test, confirm it passes** - -Run: -```bash -swift test --filter SuiteBehaviorScannerTests 2>&1 | tail -10 -``` -Expected: 3 passed, 0 failed. - -- [ ] **Step 9: Run full test suite to confirm nothing broke** - -Run: -```bash -swift test 2>&1 | tail -5 -``` -Expected: All previously-passing tests still pass. - -- [ ] **Step 10: Commit** - -```bash -git add Sources/MachOFixtureSupport/Coverage/CoverageAllowlist.swift \ - Sources/MachOFixtureSupport/Coverage/SuiteBehaviorScanner.swift \ - Tests/MachOTestingSupportTests/Coverage/SuiteBehaviorScannerTests.swift \ - Tests/MachOTestingSupportTests/Coverage/Fixtures/SuiteSampleSource.swift.txt \ - Package.swift -git commit -m "$(cat <<'EOF' -feat(MachOFixtureSupport): introduce SentinelReason schema + SuiteBehaviorScanner - -Phase A1 of fixture-coverage tightening (see -docs/superpowers/specs/2026-05-05-fixture-coverage-tightening-design.md). - -CoverageAllowlist.swift now exposes typed AllowlistKind with two paths: - - legacyExempt(reason): identical to the prior single-reason path, - used by the existing ProtocolDescriptorRef.init(storage:) entry. - - sentinel(SentinelReason): typed reason with three cases — - runtimeOnly, needsFixtureExtension, pureDataUtility. - -SuiteBehaviorScanner walks fixture suite source files and produces -[MethodKey: MethodBehavior] keyed on testedTypeName + method name. -Behavior is inferred from substring presence of acrossAllReaders / -acrossAllContexts / usingInProcessOnly / inProcessContext in the -@Test function body. Identifier collisions are avoided by the -project's identifier conventions. - -CoverageInvariant assertions remain unchanged in this commit; they -will be tightened in A3 once existing 88 sentinel suites are tagged -in A2. -EOF -)" -``` - ---- - -### Task A2: Seed sentinel reasons for all 88 existing sentinel Suites - -**Files:** -- Modify: `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift` -- Modify: `Sources/MachOFixtureSupport/Coverage/CoverageAllowlist.swift` (add `sentinelGroup` helper) - -This is the largest single commit in the plan. We add 88 `sentinelGroup(...)` calls covering 277 method names across three categories. Each group's reason is type-stable based on the type's nature (runtime-allocated metadata → `runtimeOnly`, fixture-extension-needed → `needsFixtureExtension`, pure raw-value enum → `pureDataUtility`). - -- [ ] **Step 1: Add `sentinelGroup` helper** - -In `Sources/MachOFixtureSupport/Coverage/CoverageAllowlist.swift`, append (after the `CoverageAllowlistEntry` struct): - -```swift -package enum CoverageAllowlistHelpers { - /// Construct flat `[CoverageAllowlistEntry]` with the same `SentinelReason` - /// applied to every member of `typeName`. Used in `CoverageAllowlistEntries.entries` - /// to avoid repeating the reason on every method. - package static func sentinelGroup( - typeName: String, - members: [String], - reason: SentinelReason - ) -> [CoverageAllowlistEntry] { - members.map { memberName in - CoverageAllowlistEntry(typeName: typeName, memberName: memberName, sentinel: reason) - } - } -} -``` - -- [ ] **Step 2: Inventory the 88 sentinel suites and their methods** - -Run: -```bash -for f in $(find Tests/MachOSwiftSectionTests/Fixtures -name '*Tests.swift' -not -name 'CoverageInvariant*' -not -name 'FixtureLoadingProbe*'); do - if ! grep -q 'acrossAllReaders\|acrossAllContexts' "$f" 2>/dev/null; then - suite=$(basename $f .swift) - baseline_file="Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/${suite%Tests}Baseline.swift" - if [ -f "$baseline_file" ]; then - tested=$(grep -E 'static let testedTypeName' $f | head -1 | sed -E 's/.*"([^"]+)".*/\1/') - methods=$(grep -E 'registeredTestMethodNames: Set' $baseline_file | head -1 | grep -oE '\["[^]]+"' | tr -d '[' | tr ',' '\n' | tr -d '"' | tr -d ' ' | sort | tr '\n' ',' | sed 's/,$//') - echo "$tested|$methods" - fi - fi -done | sort > /tmp/sentinel_inventory.txt - -wc -l /tmp/sentinel_inventory.txt -``` -Expected: ~88 lines (one per sentinel suite). Inspect `/tmp/sentinel_inventory.txt` to confirm. - -- [ ] **Step 3: Write the new `CoverageAllowlistEntries.swift` skeleton with empty sentinel arrays** - -Note: This step writes a structurally-complete file with empty entry arrays. Steps 4, 5, 6 use Edit to replace each empty array with the populated content. After step 6 the file is in its committed state. - -Replace the entire contents of `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift` with: - -```swift -import Foundation -@testable import MachOTestingSupport -import MachOFixtureSupport - -/// Public members of `Sources/MachOSwiftSection/Models/` that are intentionally -/// not under cross-reader fixture coverage. Each entry MUST carry either a -/// legacy exemption reason or a typed `SentinelReason`. The Coverage Invariant -/// Test treats listed entries as if they had been tested. -/// -/// Categories: -/// -/// - `legacyExempt`: scanner blind spots (e.g., `@MemberwiseInit` synthesized -/// init visible to `@testable` but not to the SwiftSyntax scanner). -/// -/// - `.sentinel(.runtimeOnly(...))`: type is allocated by the Swift runtime -/// at type-load time and is never serialized into the fixture's Mach-O. -/// Covered via `InProcessMetadataPicker` + single-reader assertions in -/// Phase C; suite is allowed to skip cross-reader assertions. -/// -/// - `.sentinel(.needsFixtureExtension(...))`: SymbolTestsCore lacks a -/// sample that surfaces this metadata shape. Should be eliminated by -/// Phase B; entries removed when each fixture file lands. -/// -/// - `.sentinel(.pureDataUtility(...))`: pure raw-value enum / marker -/// protocol / pure-data utility. Sentinel status is intended to be -/// permanent; future follow-ups may pin rawValue literals. -enum CoverageAllowlistEntries { - static let entries: [CoverageAllowlistEntry] = legacyEntries + sentinelEntries - - /// Pre-existing entries from PR #85 that aren't strictly sentinel-only. - private static let legacyEntries: [CoverageAllowlistEntry] = [ - CoverageAllowlistEntry( - typeName: "ProtocolDescriptorRef", - memberName: "init(storage:)", - reason: "synthesized memberwise initializer (visible via @testable)" - ), - ] - - /// All current sentinel-only suite methods (88 suites, ~277 methods). - /// Phase B and Phase C remove entries here as suites are converted to - /// real cross-reader / InProcess single-reader tests. - private static let sentinelEntries: [CoverageAllowlistEntry] = ( - runtimeOnlyEntries - + needsFixtureExtensionEntries - + pureDataUtilityEntries - ) - - // MARK: - runtimeOnly - - private static let runtimeOnlyEntries: [CoverageAllowlistEntry] = [] - - // MARK: - needsFixtureExtension - - private static let needsFixtureExtensionEntries: [CoverageAllowlistEntry] = [] - - // MARK: - pureDataUtility - - private static let pureDataUtilityEntries: [CoverageAllowlistEntry] = [] - - static var keys: Set { Set(entries.map(\.key)) } - - /// Subset of `keys` whose entry kind is `.sentinel(...)`. Used by the - /// Coverage Invariant Test for `liarSentinel` and `unmarkedSentinel` - /// assertions. - static var sentinelKeys: Set { - Set(entries.compactMap { entry in - if case .sentinel = entry.kind { return entry.key } else { return nil } - }) - } -} -``` - -This skeleton compiles but is empty in the three sentinel arrays. We populate them next. - -- [ ] **Step 4: Populate `runtimeOnlyEntries` array** - -In `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift`, replace the line: -```swift - private static let runtimeOnlyEntries: [CoverageAllowlistEntry] = [] -``` -with: - -```swift -private static let runtimeOnlyEntries: [CoverageAllowlistEntry] = [ - CoverageAllowlistHelpers.sentinelGroup( - typeName: "Metadata", - members: ["init", "kind", "valueWitnessTable"], - reason: .runtimeOnly(detail: "abstract Metadata pointer; concrete kind dispatched at runtime") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "FullMetadata", - members: ["init", "metadata", "header"], - reason: .runtimeOnly(detail: "metadata layout prefix not serialized in section data") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "MetadataProtocol", - members: ["kind", "valueWitnessTable"], - reason: .runtimeOnly(detail: "marker protocol on runtime metadata") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "MetadataWrapper", - members: ["init", "pointer", "kind"], - reason: .runtimeOnly(detail: "wraps live runtime metadata pointer") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "MetadataRequest", - members: ["init", "rawValue", "state", "isBlocking", "isNonBlocking"], - reason: .runtimeOnly(detail: "passed to runtime metadata accessor functions") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "MetadataResponse", - members: ["metadata", "state"], - reason: .runtimeOnly(detail: "returned by runtime metadata accessor functions") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "MetadataAccessorFunction", - members: ["init", "address", "invoke"], - reason: .runtimeOnly(detail: "function pointer to runtime metadata accessor") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "SingletonMetadataPointer", - members: ["init", "pointer", "metadata"], - reason: .runtimeOnly(detail: "runtime singleton metadata cache pointer") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "MetadataBounds", - members: ["init", "negativeSizeInWords", "positiveSizeInWords"], - reason: .runtimeOnly(detail: "computed by runtime, not in section data") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "MetadataBoundsProtocol", - members: ["negativeSizeInWords", "positiveSizeInWords"], - reason: .runtimeOnly(detail: "marker protocol on runtime-computed bounds") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ClassMetadataBounds", - members: ["init", "immediateMembers", "negativeSizeInWords", "positiveSizeInWords"], - reason: .runtimeOnly(detail: "computed by runtime from ClassDescriptor + parent chain") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ClassMetadataBoundsProtocol", - members: ["immediateMembers", "negativeSizeInWords", "positiveSizeInWords"], - reason: .runtimeOnly(detail: "marker protocol on runtime-computed class bounds") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "StoredClassMetadataBounds", - members: ["init", "immediateMembers", "bounds"], - reason: .runtimeOnly(detail: "filled in by runtime at class-loading time") - ), - // Type-flavored runtime metadata (B/C-eligible ones go here too; - // C will convert them when InProcessMetadataPicker provides pointers) - CoverageAllowlistHelpers.sentinelGroup( - typeName: "StructMetadata", - members: ["init", "kind", "description", "fieldOffsetVectorOffset"], - reason: .runtimeOnly(detail: "live runtime metadata pointer; covered via InProcess in Phase C") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "StructMetadataProtocol", - members: ["description", "fieldOffsetVectorOffset"], - reason: .runtimeOnly(detail: "marker protocol on StructMetadata") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "EnumMetadata", - members: ["init", "kind", "description"], - reason: .runtimeOnly(detail: "live runtime metadata; covered via InProcess in Phase C") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "EnumMetadataProtocol", - members: ["description"], - reason: .runtimeOnly(detail: "marker protocol on EnumMetadata") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ClassMetadata", - members: ["init", "kind", "superclass", "flags", "instanceAddressPoint", "instanceSize", "instanceAlignMask", "classSize", "classAddressPoint", "description", "iVarDestroyer"], - reason: .runtimeOnly(detail: "live class metadata; covered via InProcess in Phase C") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ClassMetadataObjCInterop", - members: ["init", "isaPointer", "superclass", "cacheData0", "cacheData1", "data", "flags", "instanceAddressPoint", "instanceSize", "instanceAlignMask", "classSize", "classAddressPoint", "description", "iVarDestroyer"], - reason: .runtimeOnly(detail: "live ObjC-interop class metadata; covered via InProcess in Phase C") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "AnyClassMetadata", - members: ["init", "kind", "isaPointer", "superclass"], - reason: .runtimeOnly(detail: "any-class metadata; covered via InProcess in Phase C") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "AnyClassMetadataObjCInterop", - members: ["init", "isaPointer", "superclass", "cacheData0", "cacheData1", "data"], - reason: .runtimeOnly(detail: "any-class metadata with ObjC interop; covered via InProcess in Phase C") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "AnyClassMetadataProtocol", - members: ["isaPointer", "superclass"], - reason: .runtimeOnly(detail: "marker protocol on AnyClassMetadata") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "AnyClassMetadataObjCInteropProtocol", - members: ["isaPointer", "superclass", "cacheData0", "cacheData1", "data"], - reason: .runtimeOnly(detail: "marker protocol on AnyClassMetadataObjCInterop") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "FinalClassMetadataProtocol", - members: ["isaPointer", "superclass", "flags"], - reason: .runtimeOnly(detail: "marker protocol on final class metadata") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "DispatchClassMetadata", - members: ["init", "kind", "isaPointer", "superclass", "data", "ivar1", "flags"], - reason: .runtimeOnly(detail: "Swift class with embedded ObjC metadata for dispatch; covered via InProcess") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ValueMetadata", - members: ["init", "kind", "description"], - reason: .runtimeOnly(detail: "value-type metadata (struct/enum); covered via InProcess in Phase C") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ValueMetadataProtocol", - members: ["description"], - reason: .runtimeOnly(detail: "marker protocol on ValueMetadata") - ), - // Existentials - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ExistentialTypeMetadata", - members: ["init", "kind", "flags", "numberOfWitnessTables", "numberOfProtocols", "isClassConstrained", "isErrorExistential", "superclassConstraint", "protocols"], - reason: .runtimeOnly(detail: "live existential metadata; covered via InProcess in Phase C") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ExistentialMetatypeMetadata", - members: ["init", "kind", "instanceType", "flags"], - reason: .runtimeOnly(detail: "live existential metatype; covered via InProcess in Phase C") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ExtendedExistentialTypeMetadata", - members: ["init", "kind", "shape", "genericArguments"], - reason: .runtimeOnly(detail: "Swift 5.7+ extended existential metadata; covered via InProcess") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ExtendedExistentialTypeShape", - members: ["init", "flags", "existentialType", "requirementSignatureHeader", "typeExpression", "suggestedValueWitnesses"], - reason: .runtimeOnly(detail: "Shape descriptor stored alongside extended existential metadata at runtime") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "NonUniqueExtendedExistentialTypeShape", - members: ["init", "uniqueShape", "specializedShape"], - reason: .runtimeOnly(detail: "non-uniqued shape variant computed at runtime") - ), - // Tuple/function/metatype/opaque/fixed-array/heap - CoverageAllowlistHelpers.sentinelGroup( - typeName: "TupleTypeMetadata", - members: ["init", "kind", "numberOfElements", "labels", "elements"], - reason: .runtimeOnly(detail: "tuple metadata is allocated lazily by the runtime; covered via InProcess") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "Element", - members: ["init", "type", "offset"], - reason: .runtimeOnly(detail: "TupleTypeMetadata.Element nested struct; lives in runtime tuple metadata") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "FunctionTypeMetadata", - members: ["init", "kind", "flags", "result", "parameters", "parameterFlags"], - reason: .runtimeOnly(detail: "function-type metadata is uniqued at runtime; covered via InProcess") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "MetatypeMetadata", - members: ["init", "kind", "instanceType"], - reason: .runtimeOnly(detail: "metatype metadata is per-type runtime singleton; covered via InProcess") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "OpaqueMetadata", - members: ["init", "kind", "instanceType"], - reason: .runtimeOnly(detail: "Swift Builtin opaque metadata; covered via InProcess") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "FixedArrayTypeMetadata", - members: ["init", "kind", "count", "element"], - reason: .runtimeOnly(detail: "InlineArray runtime metadata; covered via InProcess on Swift 6.2+") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "GenericBoxHeapMetadata", - members: ["init", "kind", "valueWitnessTable", "offsetOfBoxHeader", "captureOffset", "boxedType"], - reason: .runtimeOnly(detail: "swift_allocBox-allocated; not feasible to construct stably from tests") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "HeapLocalVariableMetadata", - members: ["init", "kind", "offsetToFirstCapture", "captureDescription"], - reason: .runtimeOnly(detail: "captured by closures; not feasible to construct stably from tests") - ), - // Headers (live in metadata layout prefix) - CoverageAllowlistHelpers.sentinelGroup( - typeName: "HeapMetadataHeader", - members: ["init", "destroy", "valueWitnessTable"], - reason: .runtimeOnly(detail: "metadata layout prefix; readable via InProcess + offset") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "HeapMetadataHeaderPrefix", - members: ["init", "destroy"], - reason: .runtimeOnly(detail: "metadata layout prefix") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "TypeMetadataHeader", - members: ["init", "destroy", "valueWitnessTable"], - reason: .runtimeOnly(detail: "metadata layout prefix") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "TypeMetadataHeaderBase", - members: ["destroy", "valueWitnessTable"], - reason: .runtimeOnly(detail: "marker protocol on type-metadata layout prefix") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "TypeMetadataLayoutPrefix", - members: ["destroy", "valueWitnessTable"], - reason: .runtimeOnly(detail: "marker protocol on layout prefix") - ), - // Generic / VWT / runtime layer - CoverageAllowlistHelpers.sentinelGroup( - typeName: "GenericEnvironment", - members: ["init", "flags", "genericParameters", "requirements"], - reason: .runtimeOnly(detail: "generic environment is materialized at runtime") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "GenericWitnessTable", - members: ["init", "witnessTableSizeInWords", "witnessTablePrivateSizeInWordsAndRequiresInstantiation", "instantiator", "privateData"], - reason: .runtimeOnly(detail: "generic witness table allocated lazily by runtime") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ValueWitnessTable", - members: ["init", "initializeBufferWithCopyOfBuffer", "destroy", "initializeWithCopy", "assignWithCopy", "initializeWithTake", "assignWithTake", "getEnumTagSinglePayload", "storeEnumTagSinglePayload", "size", "stride", "flags", "extraInhabitantCount"], - reason: .runtimeOnly(detail: "value witness table is computed by runtime; covered via InProcess on stdlib types") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "TypeLayout", - members: ["init", "size", "stride", "flags", "extraInhabitantCount"], - reason: .runtimeOnly(detail: "value-witness-table layout slice; covered via InProcess") - ), - // Foreign metadata initialization - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ForeignMetadataInitialization", - members: ["init", "completionFunction"], - reason: .runtimeOnly(detail: "foreign-metadata callback installed by runtime") - ), -].flatMap { $0 } -``` - -- [ ] **Step 5: Populate `needsFixtureExtensionEntries`** - -In `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift`, replace the line: -```swift - private static let needsFixtureExtensionEntries: [CoverageAllowlistEntry] = [] -``` -with: - -```swift -private static let needsFixtureExtensionEntries: [CoverageAllowlistEntry] = [ - CoverageAllowlistHelpers.sentinelGroup( - typeName: "MethodDefaultOverrideDescriptor", - members: ["originalMethodDescriptor", "replacementMethodDescriptor", "implementationSymbols", "layout", "offset"], - reason: .needsFixtureExtension(detail: "no class with default-override table in SymbolTestsCore — Phase B1") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "MethodDefaultOverrideTableHeader", - members: ["init", "numEntries"], - reason: .needsFixtureExtension(detail: "no class with default-override table in SymbolTestsCore — Phase B1") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "OverrideTableHeader", - members: ["init", "numEntries"], - reason: .needsFixtureExtension(detail: "no class triggers method-override table in SymbolTestsCore — Phase B1") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ResilientSuperclass", - members: ["init", "superclass", "layout", "offset"], - reason: .needsFixtureExtension(detail: "no resilient class with explicit superclass reference — Phase B2") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ObjCClassWrapperMetadata", - members: ["init", "kind", "objcClass"], - reason: .needsFixtureExtension(detail: "no NSObject-derived class in SymbolTestsCore — Phase B3") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ObjCResilientClassStubInfo", - members: ["init", "stub"], - reason: .needsFixtureExtension(detail: "no Swift class inheriting resilient ObjC class — Phase B4") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "RelativeObjCProtocolPrefix", - members: ["init", "isObjC", "rawValue"], - reason: .needsFixtureExtension(detail: "no ObjC-prefix protocol references in SymbolTestsCore — Phase B3") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ObjCProtocolPrefix", - members: ["init", "rawValue"], - reason: .needsFixtureExtension(detail: "no ObjC-prefix protocol references in SymbolTestsCore — Phase B3") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "CanonicalSpecializedMetadataAccessorsListEntry", - members: ["init", "accessor"], - reason: .needsFixtureExtension(detail: "no @_specialize(exported:) generic in SymbolTestsCore — Phase B5") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "CanonicalSpecializedMetadatasCachingOnceToken", - members: ["init", "token"], - reason: .needsFixtureExtension(detail: "no @_specialize(exported:) generic in SymbolTestsCore — Phase B5") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "CanonicalSpecializedMetadatasListCount", - members: ["init", "count"], - reason: .needsFixtureExtension(detail: "no @_specialize(exported:) generic in SymbolTestsCore — Phase B5") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "CanonicalSpecializedMetadatasListEntry", - members: ["init", "metadata"], - reason: .needsFixtureExtension(detail: "no @_specialize(exported:) generic in SymbolTestsCore — Phase B5") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ForeignClassMetadata", - members: ["init", "kind", "name", "superclass", "reserved"], - reason: .needsFixtureExtension(detail: "no foreign class import in SymbolTestsCore — Phase B6") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ForeignReferenceTypeMetadata", - members: ["init", "kind", "name"], - reason: .needsFixtureExtension(detail: "no foreign reference type in SymbolTestsCore — Phase B6") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "GenericValueDescriptor", - members: ["init", "type", "valueType"], - reason: .needsFixtureExtension(detail: "no value-generic type in SymbolTestsCore — Phase B7") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "GenericValueHeader", - members: ["init", "numValues"], - reason: .needsFixtureExtension(detail: "no value-generic type in SymbolTestsCore — Phase B7") - ), -].flatMap { $0 } -``` - -- [ ] **Step 6: Populate `pureDataUtilityEntries`** - -In `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift`, replace the line: -```swift - private static let pureDataUtilityEntries: [CoverageAllowlistEntry] = [] -``` -with: - -```swift -private static let pureDataUtilityEntries: [CoverageAllowlistEntry] = [ - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ContextDescriptorFlags", - members: ["init", "rawValue", "kind", "isGeneric", "isUnique", "version", "kindSpecificFlags"], - reason: .pureDataUtility(detail: "raw bitfield over context descriptor flag word") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ContextDescriptorKindSpecificFlags", - members: ["init", "rawValue"], - reason: .pureDataUtility(detail: "raw bitfield over kind-specific flag word") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "AnonymousContextDescriptorFlags", - members: ["init", "rawValue", "hasMangledName"], - reason: .pureDataUtility(detail: "raw bitfield over anonymous descriptor flags") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "TypeContextDescriptorFlags", - members: ["init", "rawValue", "metadataInitialization", "hasImportInfo", "hasCanonicalMetadataPrespecializations", "hasLayoutString"], - reason: .pureDataUtility(detail: "raw bitfield over type-context flags") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ClassFlags", - members: ["init", "rawValue", "hasResilientSuperclass", "hasOverrideTable", "hasVTable", "hasObjCResilientClassStub", "isActor", "isDefaultActor"], - reason: .pureDataUtility(detail: "raw bitfield over class metadata flags") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ExtraClassDescriptorFlags", - members: ["init", "rawValue", "hasObjCResilientClassStub"], - reason: .pureDataUtility(detail: "raw bitfield over extra class descriptor flags") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "MethodDescriptorFlags", - members: ["init", "rawValue", "isInstance", "isDynamic", "kind", "extraDiscriminator"], - reason: .pureDataUtility(detail: "raw bitfield over method descriptor flags") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "MethodDescriptorKind", - members: ["init", "rawValue"], - reason: .pureDataUtility(detail: "method descriptor kind enum") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ProtocolDescriptorFlags", - members: ["init", "rawValue", "hasClassConstraint", "isResilient", "specialProtocol", "dispatchStrategy"], - reason: .pureDataUtility(detail: "raw bitfield over protocol descriptor flags") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ProtocolContextDescriptorFlags", - members: ["init", "rawValue", "isClassConstrained", "isResilient", "specialProtocol"], - reason: .pureDataUtility(detail: "raw bitfield over protocol-context flags") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ProtocolRequirementFlags", - members: ["init", "rawValue", "kind", "isInstance", "extraDiscriminator"], - reason: .pureDataUtility(detail: "raw bitfield over protocol requirement flags") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ProtocolRequirementKind", - members: ["init", "rawValue"], - reason: .pureDataUtility(detail: "protocol requirement kind enum") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "GenericContextDescriptorFlags", - members: ["init", "rawValue", "hasTypePacks", "hasConditionalInvertedRequirements"], - reason: .pureDataUtility(detail: "raw bitfield over generic context flags") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "GenericRequirementFlags", - members: ["init", "rawValue", "hasKeyArgument", "isPackRequirement", "isValueRequirement", "kind"], - reason: .pureDataUtility(detail: "raw bitfield over generic requirement flags") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "GenericEnvironmentFlags", - members: ["init", "rawValue", "numGenericParameterLevels"], - reason: .pureDataUtility(detail: "raw bitfield over generic environment flags") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "FieldRecordFlags", - members: ["init", "rawValue", "isVar", "isArtificial"], - reason: .pureDataUtility(detail: "raw bitfield over field record flags") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ProtocolConformanceFlags", - members: ["init", "rawValue", "kind", "isRetroactive", "isSynthesizedNonUnique", "numConditionalRequirements", "numConditionalPackShapeDescriptors", "hasResilientWitnesses", "hasGenericWitnessTable", "isGlobalActorIsolated"], - reason: .pureDataUtility(detail: "raw bitfield over protocol conformance flags") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ExistentialTypeFlags", - members: ["init", "rawValue", "numProtocols", "numWitnessTables", "isClassConstraint", "isErrorExistential", "isObjCExistential"], - reason: .pureDataUtility(detail: "raw bitfield over existential type flags") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ExtendedExistentialTypeShapeFlags", - members: ["init", "rawValue", "specialKind", "hasGeneralizationSignature", "hasTypeExpression", "hasSuggestedValueWitnesses", "hasImplicitGenericParamsCount"], - reason: .pureDataUtility(detail: "raw bitfield over extended existential shape flags") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "FunctionTypeFlags", - members: ["init", "rawValue", "numParameters", "convention", "isThrowing", "isAsync", "isEscaping", "isSendable", "hasParameterFlags", "hasGlobalActor", "hasThrownError"], - reason: .pureDataUtility(detail: "raw bitfield over function type flags") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ValueWitnessFlags", - members: ["init", "rawValue", "alignmentMask", "isNonPOD", "isNonInline", "hasExtraInhabitants", "hasSpareBits", "isNonBitwiseTakable", "isIncomplete"], - reason: .pureDataUtility(detail: "raw bitfield over value witness flags") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "ContextDescriptorKind", - members: ["init", "rawValue"], - reason: .pureDataUtility(detail: "context descriptor kind enum") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "EnumFunctions", - members: ["destroy", "initializeWithCopy", "destructiveInjectEnumTag", "destructiveProjectEnumValue", "getEnumTag"], - reason: .pureDataUtility(detail: "enum-specific value witness function group; covered via VWT InProcess test") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "InvertibleProtocolSet", - members: ["init", "rawValue", "contains", "isSuppressedByDefault"], - reason: .pureDataUtility(detail: "raw bitset over invertible protocols") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "InvertibleProtocolsRequirementCount", - members: ["init", "rawValue"], - reason: .pureDataUtility(detail: "encoded count of invertible protocol requirements") - ), - CoverageAllowlistHelpers.sentinelGroup( - typeName: "TypeReference", - members: ["init", "kind", "directType", "indirectType", "objCClassName"], - reason: .pureDataUtility(detail: "discriminated union over type reference forms") - ), -].flatMap { $0 } -``` - -- [ ] **Step 7: Run build to verify entries compile** - -Run: -```bash -swift build 2>&1 | tail -3 -``` -Expected: -``` -Build complete! -``` - -If a property name was wrong (the type's actual public surface uses a different name), this fails with `value of type 'X' has no member 'Y'` from the test target — but the new schema is in `MachOFixtureSupport`, so it won't fail on missing members directly. Instead, the **CoverageInvariant** will detect mismatches at runtime in step 8. - -- [ ] **Step 8: Run CoverageInvariantTests, expect missing/extra to be empty** - -Run: -```bash -swift test --filter MachOSwiftSectionCoverageInvariantTests 2>&1 | tail -15 -``` -Expected: PASS. The new entries cover the same `MethodKey` set that the legacy single entry plus implicit "registered" set covered, so `missing` and `extra` remain empty. - -If `extra` reports keys, those are members listed in the seeded array but not actually declared in `Sources/MachOSwiftSection/Models/`. Cross-check the spelling in `Models/.swift`. Common mistakes: `init` (no parameters) vs `init(layout:offset:)` (has parameters). - -If `missing` reports keys, an existing public member was missed — add it to the appropriate sentinel group above. - -- [ ] **Step 9: Run the entire fixture suite to confirm regression-free** - -Run: -```bash -swift test --filter MachOSwiftSectionTests 2>&1 | tail -5 -``` -Expected: All previously-passing tests still pass. - -- [ ] **Step 10: Commit** - -```bash -git add Sources/MachOFixtureSupport/Coverage/CoverageAllowlist.swift \ - Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift -git commit -m "$(cat <<'EOF' -test(MachOSwiftSection): seed sentinel reasons for 88 sentinel suites - -Phase A2 of fixture-coverage tightening. Tags every existing -sentinel-only suite (88 suites, ~277 methods) with a typed -SentinelReason in CoverageAllowlistEntries, grouped via the new -sentinelGroup helper. - -Categories: - - runtimeOnly: ~50 suites — runtime-allocated metadata + headers, - layered protocols, etc. Phase C will convert most to InProcess - single-reader real tests. - - needsFixtureExtension: ~15 suites — SymbolTestsCore lacks samples. - Phase B will add fixtures and convert these. - - pureDataUtility: ~25 suites — pure raw-value enums, flag bitfields, - discriminated unions. Permanent sentinels; rawValue pinning is a - follow-up. - -CoverageInvariant assertions are not yet tightened (next commit). -EOF -)" -``` - ---- - -### Task A3: Enable `liarSentinel` and `unmarkedSentinel` invariant assertions - -**Files:** -- Modify: `Tests/MachOSwiftSectionTests/Fixtures/MachOSwiftSectionCoverageInvariantTests.swift` - -- [ ] **Step 1: Update CoverageInvariant test to include behavior scanning + new assertions** - -Replace the entire contents of `Tests/MachOSwiftSectionTests/Fixtures/MachOSwiftSectionCoverageInvariantTests.swift` with: - -```swift -import Foundation -import Testing -@testable import MachOTestingSupport -import MachOFixtureSupport - -/// Static-vs-runtime invariant guard for fixture-based test coverage. -/// -/// Compares four sets: -/// - **Expected** (source-code public members, scanned by SwiftSyntax). -/// - **Registered** (Suite-declared `registeredTestMethodNames`, reflected). -/// - **Behavior** (per-method behavior inferred from Suite source by -/// SuiteBehaviorScanner: acrossAllReaders / inProcessOnly / sentinel). -/// - **Allowlist** (`CoverageAllowlistEntries`, with typed `SentinelReason`). -/// -/// Failure modes: -/// ① missing — declared public member with no registered name and no -/// allowlist entry → add `@Test` or sentinel allowlist entry. -/// ② extra — registered name not matching any declaration → sync -/// `registeredTestMethodNames` and remove orphan `@Test`. -/// ③ liarSentinel — sentinel-tagged key whose Suite actually calls -/// `acrossAllReaders` / `inProcessContext` → tag is stale, remove -/// sentinel entry or revert test. -/// ④ unmarkedSentinel — Suite method behavior is sentinel but the key -/// isn't declared sentinel in the allowlist → either implement a real -/// test, or add a `SentinelReason` entry. -@Suite -@MainActor -struct MachOSwiftSectionCoverageInvariantTests { - - private var modelsRoot: URL { - URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() // Fixtures/ - .deletingLastPathComponent() // MachOSwiftSectionTests/ - .deletingLastPathComponent() // Tests/ - .appendingPathComponent("../Sources/MachOSwiftSection/Models") - .standardizedFileURL - } - - private var suitesRoot: URL { - URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() // Fixtures/ - .standardizedFileURL - } - - @Test func everyPublicMemberHasATest() throws { - let scanner = PublicMemberScanner(sourceRoot: modelsRoot) - let allowlistKeys = CoverageAllowlistEntries.keys - let sentinelKeys = CoverageAllowlistEntries.sentinelKeys - - let expected = try scanner.scan(applyingAllowlist: allowlistKeys) - - let registered: Set = Set( - allFixtureSuites.flatMap { suite -> [MethodKey] in - suite.registeredTestMethodNames.map { name in - MethodKey(typeName: suite.testedTypeName, memberName: name) - } - } - ).subtracting(allowlistKeys) - - let behaviorScanner = SuiteBehaviorScanner(suiteRoot: suitesRoot) - let behaviorMap = try behaviorScanner.scan() - - // ① missing - let missing = expected.subtracting(registered) - #expect( - missing.isEmpty, - """ - Missing tests for these public members of MachOSwiftSection/Models: - \(missing.sorted().map { " \($0)" }.joined(separator: "\n")) - - Tip: add the corresponding @Test func to the matching Suite, append the - name to its registeredTestMethodNames (or rerun - `swift package --allow-writing-to-package-directory regen-baselines --suite `), - and re-run. - """ - ) - - // ② extra - let extra = registered.subtracting(expected) - #expect( - extra.isEmpty, - """ - Tests registered for non-existent (or refactored-away) public members: - \(extra.sorted().map { " \($0)" }.joined(separator: "\n")) - - Tip: source method was renamed or removed — sync the Suite's - registeredTestMethodNames + remove the orphan @Test. - """ - ) - - // ③ liarSentinel — sentinel tag claims sentinel but suite actually tests - let liarSentinels = sentinelKeys.filter { key in - if let behavior = behaviorMap[key], behavior != .sentinel { - return true - } - return false - } - #expect( - liarSentinels.isEmpty, - """ - These methods are tagged sentinel in CoverageAllowlistEntries but - their Suite actually calls acrossAllReaders / inProcessContext — the - sentinel tag is stale. Remove the sentinel entry or revert the test - to registration-only: - \(liarSentinels.sorted().map { " \($0)" }.joined(separator: "\n")) - """ - ) - - // ④ unmarkedSentinel — suite behavior is sentinel but key isn't declared - let actualSentinelKeys = Set(behaviorMap.compactMap { (key, behavior) in - behavior == .sentinel ? key : nil - }) - let unmarked = actualSentinelKeys - .subtracting(sentinelKeys) - .subtracting(allowlistKeys) - .intersection(expected) // only flag if it's actually a public method - #expect( - unmarked.isEmpty, - """ - These methods are sentinel-only (the Suite never calls - acrossAllReaders / inProcessContext) but are not declared in - CoverageAllowlistEntries. Either implement a real test, or add a - SentinelReason entry explaining why this is the right level of - coverage: - \(unmarked.sorted().map { " \($0)" }.joined(separator: "\n")) - """ - ) - } -} -``` - -- [ ] **Step 2: Run CoverageInvariant test, expect all four assertions pass** - -Run: -```bash -swift test --filter MachOSwiftSectionCoverageInvariantTests 2>&1 | tail -15 -``` -Expected: PASS. - -If `liarSentinels` reports keys: a Suite labeled sentinel calls cross-reader test machinery — either the Suite was upgraded recently and the allowlist tag is stale, or the scanner detected a misleading substring. Fix the allowlist tag. - -If `unmarked` reports keys: a Suite without `acrossAllReaders`/`inProcessContext` exists but isn't tagged sentinel — verify A2 covered all ~88 sentinel suites; add the missing one to the appropriate group. - -- [ ] **Step 3: Run full test suite to confirm no regression** - -Run: -```bash -swift test 2>&1 | tail -5 -``` -Expected: All previously-passing tests still pass. - -- [ ] **Step 4: Commit** - -```bash -git add Tests/MachOSwiftSectionTests/Fixtures/MachOSwiftSectionCoverageInvariantTests.swift -git commit -m "$(cat <<'EOF' -test(MachOSwiftSection): enable liarSentinel + unmarkedSentinel invariant assertions - -Phase A3 of fixture-coverage tightening. Tightens -MachOSwiftSectionCoverageInvariantTests with two new assertions backed -by SuiteBehaviorScanner (per-method @Test behavior introspection): - - ③ liarSentinel — fails if a sentinel-tagged key's Suite actually - calls acrossAllReaders / inProcessContext. Catches stale tags - after a sentinel suite is upgraded to a real test. - - ④ unmarkedSentinel — fails if a Suite has sentinel behavior (no - acrossAllReaders / inProcessContext call) but the key isn't - declared sentinel in CoverageAllowlistEntries. Closes the - silent-sentinel loophole found in PR #85 review. - -The PR's 88 existing sentinel suites are tagged in A2; this commit -just wires the gates. Phase B and Phase C remove sentinel entries as -suites are converted to real tests. -EOF -)" -``` - -- [ ] **Step 5: Push Phase A** - -```bash -git push 2>&1 | tail -5 -``` -Expected: success on `feature/machoswift-section-fixture-tests` upstream. - ---- - -## Phase C — Runtime-only InProcess conversion - -### Task C1: Add `InProcessMetadataPicker` + `usingInProcessOnly` helper - -**Files:** -- Create: `Sources/MachOFixtureSupport/InProcess/InProcessMetadataPicker.swift` -- Modify: `Sources/MachOTestingSupport/MachOSwiftSectionFixtureTests.swift` - -- [ ] **Step 1: Create `InProcessMetadataPicker.swift`** - -Create `Sources/MachOFixtureSupport/InProcess/InProcessMetadataPicker.swift`: - -```swift -import Foundation - -/// Static `UnsafeRawPointer` constants exposing Swift runtime metadata -/// for Suites that exercise `*Metadata` types without a fixture-binary -/// section presence (runtime-allocated metadata). -/// -/// Each constant is a `unsafeBitCast(.self, to: UnsafeRawPointer.self)` -/// — this is the standard idiom for obtaining a metadata pointer from a -/// Swift type reference. The pointer is stable for the test process's -/// lifetime; the Swift runtime uniques metadata. -/// -/// Suites consume these via `MachOSwiftSectionFixtureTests.usingInProcessOnly(_:)`. -package enum InProcessMetadataPicker { - // MARK: - stdlib metatype - - /// `Int.self.self` — metatype of metatype. Exercises `MetatypeMetadata.kind` - /// + `instanceType` chain. - package static let stdlibIntMetatype: UnsafeRawPointer = { - unsafeBitCast(Int.self.self, to: UnsafeRawPointer.self) - }() - - // MARK: - stdlib tuple - - /// `(Int, String).self` — covers `TupleTypeMetadata` + `TupleTypeMetadata.Element`. - package static let stdlibTupleIntString: UnsafeRawPointer = { - unsafeBitCast((Int, String).self, to: UnsafeRawPointer.self) - }() - - // MARK: - stdlib function - - /// `((Int) -> Void).self` — covers `FunctionTypeMetadata` + `FunctionTypeFlags`. - package static let stdlibFunctionIntToVoid: UnsafeRawPointer = { - unsafeBitCast(((Int) -> Void).self, to: UnsafeRawPointer.self) - }() - - // MARK: - stdlib existential - - /// `Any.self` — covers `ExistentialTypeMetadata` for the maximally-general - /// existential. - package static let stdlibAnyExistential: UnsafeRawPointer = { - unsafeBitCast(Any.self, to: UnsafeRawPointer.self) - }() - - /// `(any Equatable).self` — covers `ExtendedExistentialTypeMetadata` (with - /// shape) and constrained existential. - package static let stdlibAnyEquatable: UnsafeRawPointer = { - unsafeBitCast((any Equatable).self, to: UnsafeRawPointer.self) - }() - - /// `(Any).Type.self` — covers `ExistentialMetatypeMetadata`. - package static let stdlibAnyMetatype: UnsafeRawPointer = { - unsafeBitCast(Any.Type.self, to: UnsafeRawPointer.self) - }() - - // MARK: - stdlib opaque - - /// `Int8.self` proxies for OpaqueMetadata; Swift runtime exposes opaque - /// metadata via Builtin types but `Builtin.Int8` isn't visible outside - /// the standard library, so use the user-visible `Int8` whose metadata - /// includes the same opaque-metadata layout. - package static let stdlibOpaqueInt8: UnsafeRawPointer = { - unsafeBitCast(Int8.self, to: UnsafeRawPointer.self) - }() - - // MARK: - stdlib fixed array (macOS 26+ only) - - #if compiler(>=6.2) - @available(macOS 26.0, *) - package static let stdlibInlineArrayInt3: UnsafeRawPointer = { - unsafeBitCast(InlineArray<3, Int>.self, to: UnsafeRawPointer.self) - }() - #endif -} -``` - -The `*MetadataHeader`, `*MetadataBounds`, and `Metadata`/`FullMetadata`/etc. layer-protocol Suites are covered using existing pointers above + `InProcessContext` offset arithmetic; they don't need separate constants. - -- [ ] **Step 2: Add `usingInProcessOnly` helper to `MachOSwiftSectionFixtureTests`** - -In `Sources/MachOTestingSupport/MachOSwiftSectionFixtureTests.swift`, append a new helper extension at the bottom of the file (after the existing `acrossAllReaders` / `acrossAllContexts` helpers): - -```swift -extension MachOSwiftSectionFixtureTests { - /// Run `body` against the in-process reader only. Used by Suites covering - /// runtime-only metadata types (MetatypeMetadata, TupleTypeMetadata, - /// FunctionTypeMetadata, etc.) — types that the Swift runtime allocates - /// at type-load time and that have no Mach-O section to read from. - /// - /// Cross-reader equality is not asserted because `MachOFile` and - /// `MachOImage` cannot reach this metadata. Single-reader assertion + - /// baseline literal pinning is the deepest coverage achievable. - package func usingInProcessOnly( - _ work: (InProcessContext) throws -> T, - sourceLocation: SourceLocation = #_sourceLocation - ) throws -> T { - try work(inProcessContext) - } -} -``` - -- [ ] **Step 3: Run build to verify** - -Run: -```bash -swift build 2>&1 | tail -3 -``` -Expected: -``` -Build complete! -``` - -- [ ] **Step 4: Run full test suite to confirm no regression** - -Run: -```bash -swift test 2>&1 | tail -5 -``` -Expected: All previously-passing tests still pass. CoverageInvariant remains green (no allowlist changes). - -- [ ] **Step 5: Commit** - -```bash -git add Sources/MachOFixtureSupport/InProcess/InProcessMetadataPicker.swift \ - Sources/MachOTestingSupport/MachOSwiftSectionFixtureTests.swift -git commit -m "$(cat <<'EOF' -feat(MachOFixtureSupport): add InProcessMetadataPicker + usingInProcessOnly - -Phase C1 of fixture-coverage tightening. Provides the infrastructure -for converting runtime-only metadata sentinel suites to real -single-reader InProcess tests: - - - InProcessMetadataPicker exposes `UnsafeRawPointer` constants for - stdlib metatype, tuple, function, existential, opaque, and fixed - array (macOS 26+) types via `unsafeBitCast(T.self, to: UnsafeRawPointer.self)`. - Each pointer is stable for the test process lifetime (Swift - runtime uniques metadata). - - - MachOSwiftSectionFixtureTests gains usingInProcessOnly(_:), the - SuiteBehaviorScanner-recognized helper that runs a closure with - only the in-process reader and skips cross-reader assertions - (other readers cannot see runtime-allocated metadata). - -C2-C5 will use these to convert ~30 runtime-only sentinel suites. -EOF -)" -``` - ---- - -### Task C2: Convert stdlib metatype/tuple/function suites (5 suites) - -**Files:** -- Modify: `Tests/MachOSwiftSectionTests/Fixtures/Metadata/MetatypeMetadataTests.swift` -- Modify: `Tests/MachOSwiftSectionTests/Fixtures/TupleType/TupleTypeMetadataTests.swift` -- Modify: `Tests/MachOSwiftSectionTests/Fixtures/TupleType/TupleTypeMetadataElementTests.swift` -- Modify: `Tests/MachOSwiftSectionTests/Fixtures/Function/FunctionTypeMetadataTests.swift` -- Modify: `Tests/MachOSwiftSectionTests/Fixtures/Function/FunctionTypeFlagsTests.swift` -- Modify (5): corresponding `Sources/MachOFixtureSupport/Baseline/Generators/.../*BaselineGenerator.swift` -- Modify: `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift` (remove 5 suite groups from `runtimeOnlyEntries`) - -This task converts the most straightforward 5 sentinel suites — those whose underlying types live in stdlib and have stable metadata pointers via `InProcessMetadataPicker`. Each follows the same pattern. - -- [ ] **Step 1: Convert `MetatypeMetadataTests.swift`** - -Replace the contents of `Tests/MachOSwiftSectionTests/Fixtures/Metadata/MetatypeMetadataTests.swift` with: - -```swift -import Foundation -import Testing -import MachOFoundation -@testable import MachOSwiftSection -@testable import MachOTestingSupport -import MachOFixtureSupport - -@Suite -final class MetatypeMetadataTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable { - static let testedTypeName = "MetatypeMetadata" - static var registeredTestMethodNames: Set { - MetatypeMetadataBaseline.registeredTestMethodNames - } - - @Test func kind() async throws { - let result = try usingInProcessOnly { context in - try MetatypeMetadata(at: InProcessMetadataPicker.stdlibIntMetatype, in: context).kind - } - #expect(result.rawValue == MetatypeMetadataBaseline.stdlibIntMetatype.kindRawValue) - } - - @Test func instanceType() async throws { - let pointer = try usingInProcessOnly { context in - try MetatypeMetadata(at: InProcessMetadataPicker.stdlibIntMetatype, in: context).instanceType - } - // `Int.self.self.instanceType == Int.self`. The pointer must equal - // `unsafeBitCast(Int.self, to: UnsafeRawPointer.self)`. - #expect(pointer == unsafeBitCast(Int.self, to: UnsafeRawPointer.self)) - } -} -``` - -- [ ] **Step 2: Update `MetatypeMetadataBaselineGenerator.swift` to emit InProcess Entry** - -Replace the contents of `Sources/MachOFixtureSupport/Baseline/Generators/Metadata/MetatypeMetadataBaselineGenerator.swift` with: - -```swift -import Foundation -import SwiftSyntax -import SwiftSyntaxBuilder -@testable import MachOSwiftSection - -package enum MetatypeMetadataBaselineGenerator { - package static func generate(outputDirectory: URL) throws { - let pointer = InProcessMetadataPicker.stdlibIntMetatype - let metatype = try MetatypeMetadata(at: pointer, in: InProcessContext()) - let kindRaw = metatype.kind.rawValue - - let registered = ["instanceType", "kind"] - - let header = """ - // AUTO-GENERATED — DO NOT EDIT. - // Regenerate via: swift package --allow-writing-to-package-directory regen-baselines - // Source: InProcess (stdlib `Int.self.self`); no Mach-O section presence. - """ - - let file: SourceFileSyntax = """ - \(raw: header) - - enum MetatypeMetadataBaseline { - static let registeredTestMethodNames: Set = \(literal: registered) - - struct Entry { - let kindRawValue: UInt - } - - static let stdlibIntMetatype = Entry( - kindRawValue: \(raw: BaselineEmitter.hex(kindRaw)) - ) - } - """ - - let formatted = file.formatted().description + "\n" - let outputURL = outputDirectory.appendingPathComponent("MetatypeMetadataBaseline.swift") - try formatted.write(to: outputURL, atomically: true, encoding: .utf8) - } -} -``` - -- [ ] **Step 3: Regenerate the baseline** - -Run: -```bash -swift package --allow-writing-to-package-directory regen-baselines --suite MetatypeMetadata 2>&1 | tail -5 -``` -Expected: success, baseline updated. Verify: -```bash -cat Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MetatypeMetadataBaseline.swift -``` -Should show non-zero `kindRawValue` (the value of `MetadataKind.metatype`). - -- [ ] **Step 4: Run the converted suite** - -Run: -```bash -swift test --filter MetatypeMetadataTests 2>&1 | tail -10 -``` -Expected: 2 tests pass (`kind`, `instanceType`). - -- [ ] **Step 5: Convert `TupleTypeMetadataTests.swift`** - -Replace contents with: - -```swift -import Foundation -import Testing -import MachOFoundation -@testable import MachOSwiftSection -@testable import MachOTestingSupport -import MachOFixtureSupport - -@Suite -final class TupleTypeMetadataTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable { - static let testedTypeName = "TupleTypeMetadata" - static var registeredTestMethodNames: Set { - TupleTypeMetadataBaseline.registeredTestMethodNames - } - - @Test func kind() async throws { - let result = try usingInProcessOnly { context in - try TupleTypeMetadata(at: InProcessMetadataPicker.stdlibTupleIntString, in: context).kind - } - #expect(result.rawValue == TupleTypeMetadataBaseline.stdlibTupleIntString.kindRawValue) - } - - @Test func numberOfElements() async throws { - let result = try usingInProcessOnly { context in - try TupleTypeMetadata(at: InProcessMetadataPicker.stdlibTupleIntString, in: context).numberOfElements - } - #expect(result == TupleTypeMetadataBaseline.stdlibTupleIntString.numberOfElements) - } - - @Test func labels() async throws { - let result = try usingInProcessOnly { context in - try TupleTypeMetadata(at: InProcessMetadataPicker.stdlibTupleIntString, in: context).labels - } - #expect(result == TupleTypeMetadataBaseline.stdlibTupleIntString.labels) - } -} -``` - -- [ ] **Step 6: Update `TupleTypeMetadataBaselineGenerator.swift`** - -Replace contents with: - -```swift -import Foundation -import SwiftSyntax -import SwiftSyntaxBuilder -@testable import MachOSwiftSection - -package enum TupleTypeMetadataBaselineGenerator { - package static func generate(outputDirectory: URL) throws { - let pointer = InProcessMetadataPicker.stdlibTupleIntString - let context = InProcessContext() - let metadata = try TupleTypeMetadata(at: pointer, in: context) - let kindRaw = metadata.kind.rawValue - let count = metadata.numberOfElements - let labels = metadata.labels - - let registered = ["kind", "labels", "numberOfElements"] - - let header = """ - // AUTO-GENERATED — DO NOT EDIT. - // Regenerate via: swift package --allow-writing-to-package-directory regen-baselines - // Source: InProcess (stdlib `(Int, String).self`); no Mach-O section presence. - """ - - let file: SourceFileSyntax = """ - \(raw: header) - - enum TupleTypeMetadataBaseline { - static let registeredTestMethodNames: Set = \(literal: registered) - - struct Entry { - let kindRawValue: UInt - let numberOfElements: Int - let labels: String - } - - static let stdlibTupleIntString = Entry( - kindRawValue: \(raw: BaselineEmitter.hex(kindRaw)), - numberOfElements: \(literal: count), - labels: \(literal: labels) - ) - } - """ - - let formatted = file.formatted().description + "\n" - let outputURL = outputDirectory.appendingPathComponent("TupleTypeMetadataBaseline.swift") - try formatted.write(to: outputURL, atomically: true, encoding: .utf8) - } -} -``` - -- [ ] **Step 7: Regenerate + run** - -```bash -swift package --allow-writing-to-package-directory regen-baselines --suite TupleTypeMetadata 2>&1 | tail -3 -swift test --filter TupleTypeMetadataTests 2>&1 | tail -10 -``` -Expected: PASS. - -- [ ] **Step 8: Convert `TupleTypeMetadataElementTests.swift`** - -Replace contents with: - -```swift -import Foundation -import Testing -import MachOFoundation -@testable import MachOSwiftSection -@testable import MachOTestingSupport -import MachOFixtureSupport - -@Suite -final class TupleTypeMetadataElementTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable { - static let testedTypeName = "Element" - static var registeredTestMethodNames: Set { - TupleTypeMetadataElementBaseline.registeredTestMethodNames - } - - @Test func type() async throws { - let result = try usingInProcessOnly { context in - let tuple = try TupleTypeMetadata(at: InProcessMetadataPicker.stdlibTupleIntString, in: context) - return try tuple.elements.first!.type - } - // First element of `(Int, String)` is `Int` — pointer must equal Int's metadata. - #expect(result == unsafeBitCast(Int.self, to: UnsafeRawPointer.self)) - } - - @Test func offset() async throws { - let result = try usingInProcessOnly { context in - let tuple = try TupleTypeMetadata(at: InProcessMetadataPicker.stdlibTupleIntString, in: context) - return try tuple.elements.first!.offset - } - #expect(result == TupleTypeMetadataElementBaseline.firstElementOfIntStringTuple.offset) - } -} -``` - -- [ ] **Step 9: Update `TupleTypeMetadataElementBaselineGenerator.swift`** - -Replace contents with: - -```swift -import Foundation -import SwiftSyntax -import SwiftSyntaxBuilder -@testable import MachOSwiftSection - -package enum TupleTypeMetadataElementBaselineGenerator { - package static func generate(outputDirectory: URL) throws { - let pointer = InProcessMetadataPicker.stdlibTupleIntString - let context = InProcessContext() - let tuple = try TupleTypeMetadata(at: pointer, in: context) - let firstElement = try tuple.elements.first! - let offset = try firstElement.offset - - let registered = ["offset", "type"] - - let header = """ - // AUTO-GENERATED — DO NOT EDIT. - // Regenerate via: swift package --allow-writing-to-package-directory regen-baselines - // Source: InProcess first element of `(Int, String)`; no Mach-O section presence. - """ - - let file: SourceFileSyntax = """ - \(raw: header) - - enum TupleTypeMetadataElementBaseline { - static let registeredTestMethodNames: Set = \(literal: registered) - - struct Entry { - let offset: Int - } - - static let firstElementOfIntStringTuple = Entry( - offset: \(literal: offset) - ) - } - """ - - let formatted = file.formatted().description + "\n" - let outputURL = outputDirectory.appendingPathComponent("TupleTypeMetadataElementBaseline.swift") - try formatted.write(to: outputURL, atomically: true, encoding: .utf8) - } -} -``` - -- [ ] **Step 10: Regenerate + run** - -```bash -swift package --allow-writing-to-package-directory regen-baselines --suite TupleTypeMetadataElement 2>&1 | tail -3 -swift test --filter TupleTypeMetadataElementTests 2>&1 | tail -10 -``` -Expected: PASS. - -- [ ] **Step 11: Convert `FunctionTypeMetadataTests.swift`** - -Replace contents with: - -```swift -import Foundation -import Testing -import MachOFoundation -@testable import MachOSwiftSection -@testable import MachOTestingSupport -import MachOFixtureSupport - -@Suite -final class FunctionTypeMetadataTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable { - static let testedTypeName = "FunctionTypeMetadata" - static var registeredTestMethodNames: Set { - FunctionTypeMetadataBaseline.registeredTestMethodNames - } - - @Test func kind() async throws { - let result = try usingInProcessOnly { context in - try FunctionTypeMetadata(at: InProcessMetadataPicker.stdlibFunctionIntToVoid, in: context).kind - } - #expect(result.rawValue == FunctionTypeMetadataBaseline.stdlibFunctionIntToVoid.kindRawValue) - } - - @Test func flags() async throws { - let result = try usingInProcessOnly { context in - try FunctionTypeMetadata(at: InProcessMetadataPicker.stdlibFunctionIntToVoid, in: context).flags.rawValue - } - #expect(result == FunctionTypeMetadataBaseline.stdlibFunctionIntToVoid.flagsRawValue) - } -} -``` - -- [ ] **Step 12: Update `FunctionTypeMetadataBaselineGenerator.swift`** - -Replace contents with: - -```swift -import Foundation -import SwiftSyntax -import SwiftSyntaxBuilder -@testable import MachOSwiftSection - -package enum FunctionTypeMetadataBaselineGenerator { - package static func generate(outputDirectory: URL) throws { - let pointer = InProcessMetadataPicker.stdlibFunctionIntToVoid - let context = InProcessContext() - let metadata = try FunctionTypeMetadata(at: pointer, in: context) - let kindRaw = metadata.kind.rawValue - let flagsRaw = metadata.flags.rawValue - - let registered = ["flags", "kind"] - - let header = """ - // AUTO-GENERATED — DO NOT EDIT. - // Regenerate via: swift package --allow-writing-to-package-directory regen-baselines - // Source: InProcess `((Int) -> Void).self`; no Mach-O section presence. - """ - - let file: SourceFileSyntax = """ - \(raw: header) - - enum FunctionTypeMetadataBaseline { - static let registeredTestMethodNames: Set = \(literal: registered) - - struct Entry { - let kindRawValue: UInt - let flagsRawValue: UInt - } - - static let stdlibFunctionIntToVoid = Entry( - kindRawValue: \(raw: BaselineEmitter.hex(kindRaw)), - flagsRawValue: \(raw: BaselineEmitter.hex(flagsRaw)) - ) - } - """ - - let formatted = file.formatted().description + "\n" - let outputURL = outputDirectory.appendingPathComponent("FunctionTypeMetadataBaseline.swift") - try formatted.write(to: outputURL, atomically: true, encoding: .utf8) - } -} -``` - -- [ ] **Step 13: Convert `FunctionTypeFlagsTests.swift`** - -Replace contents with: - -```swift -import Foundation -import Testing -import MachOFoundation -@testable import MachOSwiftSection -@testable import MachOTestingSupport -import MachOFixtureSupport - -@Suite -final class FunctionTypeFlagsTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable { - static let testedTypeName = "FunctionTypeFlags" - static var registeredTestMethodNames: Set { - FunctionTypeFlagsBaseline.registeredTestMethodNames - } - - @Test func numberOfParameters() async throws { - let result = try usingInProcessOnly { context in - try FunctionTypeMetadata(at: InProcessMetadataPicker.stdlibFunctionIntToVoid, in: context) - .flags.numParameters - } - #expect(result == FunctionTypeFlagsBaseline.stdlibFunctionIntToVoid.numParameters) - } -} -``` - -- [ ] **Step 14: Update `FunctionTypeFlagsBaselineGenerator.swift`** - -Replace contents with: - -```swift -import Foundation -import SwiftSyntax -import SwiftSyntaxBuilder -@testable import MachOSwiftSection - -package enum FunctionTypeFlagsBaselineGenerator { - package static func generate(outputDirectory: URL) throws { - let pointer = InProcessMetadataPicker.stdlibFunctionIntToVoid - let context = InProcessContext() - let metadata = try FunctionTypeMetadata(at: pointer, in: context) - let numParams = metadata.flags.numParameters - - let registered = ["numberOfParameters"] - - let header = """ - // AUTO-GENERATED — DO NOT EDIT. - // Regenerate via: swift package --allow-writing-to-package-directory regen-baselines - // Source: InProcess `((Int) -> Void).self` flags slice. - """ - - let file: SourceFileSyntax = """ - \(raw: header) - - enum FunctionTypeFlagsBaseline { - static let registeredTestMethodNames: Set = \(literal: registered) - - struct Entry { - let numParameters: Int - } - - static let stdlibFunctionIntToVoid = Entry( - numParameters: \(literal: numParams) - ) - } - """ - - let formatted = file.formatted().description + "\n" - let outputURL = outputDirectory.appendingPathComponent("FunctionTypeFlagsBaseline.swift") - try formatted.write(to: outputURL, atomically: true, encoding: .utf8) - } -} -``` - -- [ ] **Step 15: Regenerate both Function baselines + run** - -```bash -swift package --allow-writing-to-package-directory regen-baselines --suite FunctionTypeMetadata 2>&1 | tail -3 -swift package --allow-writing-to-package-directory regen-baselines --suite FunctionTypeFlags 2>&1 | tail -3 -swift test --filter "FunctionTypeMetadataTests|FunctionTypeFlagsTests" 2>&1 | tail -10 -``` -Expected: PASS. - -- [ ] **Step 16: Remove the 5 converted suites from `runtimeOnlyEntries`** - -Edit `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift`. In the `runtimeOnlyEntries` array, **delete** the 5 `sentinelGroup` calls for: -- `MetatypeMetadata` -- `TupleTypeMetadata` -- `Element` (the `TupleTypeMetadata.Element` group) -- `FunctionTypeMetadata` - -(Note: `FunctionTypeFlags` is in `pureDataUtilityEntries` not `runtimeOnlyEntries` — it's `numberOfParameters` registered above so the registered method becomes a real test, but the type stays in `pureDataUtility` allowlist for unconverted methods. Do **not** remove it from `pureDataUtilityEntries` here. The `numberOfParameters` registered name is now a real test, so it should be removed from the allowlist's `FunctionTypeFlags` group's members list.) - -For `FunctionTypeFlags` in `pureDataUtilityEntries`, change: -```swift -CoverageAllowlistHelpers.sentinelGroup( - typeName: "FunctionTypeFlags", - members: ["init", "rawValue", "numParameters", "convention", "isThrowing", "isAsync", "isEscaping", "isSendable", "hasParameterFlags", "hasGlobalActor", "hasThrownError"], - ... -) -``` -to: -```swift -CoverageAllowlistHelpers.sentinelGroup( - typeName: "FunctionTypeFlags", - members: ["init", "rawValue", "convention", "isThrowing", "isAsync", "isEscaping", "isSendable", "hasParameterFlags", "hasGlobalActor", "hasThrownError"], - ... -) -``` -(removed `numberOfParameters` — wait, the original used `numParameters` which is the `FunctionTypeFlags` property name; the `@Test` is `numberOfParameters`. The `MethodKey` is keyed on the **public method name** which is `numParameters` in the source. Inspect `Sources/MachOSwiftSection/Models/Function/FunctionTypeFlags.swift` to confirm the actual public name. If the source says `numParameters`, the suite's `@Test func numberOfParameters` is technically registering a different name than the source declares — flag this in step 18.) - -- [ ] **Step 17: Run CoverageInvariant + full test** - -```bash -swift test --filter MachOSwiftSectionCoverageInvariantTests 2>&1 | tail -10 -swift test --filter MachOSwiftSectionTests 2>&1 | tail -5 -``` -Expected: PASS. CoverageInvariant should now confirm 5 fewer sentinel-tagged keys, all replaced by real-test keys. - -- [ ] **Step 18: Reconcile property name mismatch if step 16 found one** - -If `FunctionTypeFlags` source declares `numParameters` but the suite uses `@Test func numberOfParameters`, fix the test name to match the source, regenerate baseline, re-run. (`MethodKey` matching is exact: scanner produces `(FunctionTypeFlags, numParameters)`; if test is named `numberOfParameters`, scanner won't see a match in `expected`.) - -- [ ] **Step 19: Commit** - -```bash -git add Tests/MachOSwiftSectionTests/Fixtures/Metadata/MetatypeMetadataTests.swift \ - Tests/MachOSwiftSectionTests/Fixtures/TupleType/TupleTypeMetadataTests.swift \ - Tests/MachOSwiftSectionTests/Fixtures/TupleType/TupleTypeMetadataElementTests.swift \ - Tests/MachOSwiftSectionTests/Fixtures/Function/FunctionTypeMetadataTests.swift \ - Tests/MachOSwiftSectionTests/Fixtures/Function/FunctionTypeFlagsTests.swift \ - Sources/MachOFixtureSupport/Baseline/Generators/Metadata/MetatypeMetadataBaselineGenerator.swift \ - Sources/MachOFixtureSupport/Baseline/Generators/TupleType/TupleTypeMetadataBaselineGenerator.swift \ - Sources/MachOFixtureSupport/Baseline/Generators/TupleType/TupleTypeMetadataElementBaselineGenerator.swift \ - Sources/MachOFixtureSupport/Baseline/Generators/Function/FunctionTypeMetadataBaselineGenerator.swift \ - Sources/MachOFixtureSupport/Baseline/Generators/Function/FunctionTypeFlagsBaselineGenerator.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MetatypeMetadataBaseline.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TupleTypeMetadataBaseline.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/TupleTypeMetadataElementBaseline.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/FunctionTypeMetadataBaseline.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/FunctionTypeFlagsBaseline.swift \ - Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift -git commit -m "$(cat <<'EOF' -test(MachOSwiftSection): convert metatype/tuple/function suites to InProcess real tests - -Phase C2 of fixture-coverage tightening. Converts 5 sentinel-only -suites covering stdlib runtime-allocated metadata: - - MetatypeMetadata (Int.self.self) - - TupleTypeMetadata, TupleTypeMetadataElement ((Int, String).self) - - FunctionTypeMetadata, FunctionTypeFlags (((Int) -> Void).self) - -Each suite now uses usingInProcessOnly + InProcessMetadataPicker -constants and asserts against ABI literals pinned in regenerated -baselines. Removed corresponding entries from CoverageAllowlistEntries -runtimeOnly group. -EOF -)" -``` - ---- - -### Task C3: Convert existential family suites (7 suites) - -**Files:** -- Modify (7): `Tests/MachOSwiftSectionTests/Fixtures/ExistentialType/*Tests.swift` -- Modify (7): `Sources/MachOFixtureSupport/Baseline/Generators/ExistentialType/*BaselineGenerator.swift` -- Modify: `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift` - -This task converts the existential-family suites: `ExistentialTypeMetadata`, `ExistentialMetatypeMetadata`, `ExistentialTypeFlags`, `ExtendedExistentialTypeMetadata`, `ExtendedExistentialTypeShape`, `ExtendedExistentialTypeShapeFlags`, `NonUniqueExtendedExistentialTypeShape`. - -The pattern follows C2 exactly — use `InProcessMetadataPicker.stdlibAnyExistential` for plain existentials, `stdlibAnyEquatable` for extended existentials, `stdlibAnyMetatype` for existential metatype. - -- [ ] **Step 1: Convert `ExistentialTypeMetadataTests.swift`** - -Replace with: - -```swift -import Foundation -import Testing -import MachOFoundation -@testable import MachOSwiftSection -@testable import MachOTestingSupport -import MachOFixtureSupport - -@Suite -final class ExistentialTypeMetadataTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable { - static let testedTypeName = "ExistentialTypeMetadata" - static var registeredTestMethodNames: Set { - ExistentialTypeMetadataBaseline.registeredTestMethodNames - } - - @Test func kind() async throws { - let result = try usingInProcessOnly { context in - try ExistentialTypeMetadata(at: InProcessMetadataPicker.stdlibAnyExistential, in: context).kind - } - #expect(result.rawValue == ExistentialTypeMetadataBaseline.stdlibAnyExistential.kindRawValue) - } - - @Test func numberOfProtocols() async throws { - let result = try usingInProcessOnly { context in - try ExistentialTypeMetadata(at: InProcessMetadataPicker.stdlibAnyExistential, in: context).numberOfProtocols - } - #expect(result == ExistentialTypeMetadataBaseline.stdlibAnyExistential.numberOfProtocols) - } - - @Test func numberOfWitnessTables() async throws { - let result = try usingInProcessOnly { context in - try ExistentialTypeMetadata(at: InProcessMetadataPicker.stdlibAnyExistential, in: context).numberOfWitnessTables - } - #expect(result == ExistentialTypeMetadataBaseline.stdlibAnyExistential.numberOfWitnessTables) - } - - @Test func isClassConstrained() async throws { - let result = try usingInProcessOnly { context in - try ExistentialTypeMetadata(at: InProcessMetadataPicker.stdlibAnyExistential, in: context).isClassConstrained - } - #expect(result == ExistentialTypeMetadataBaseline.stdlibAnyExistential.isClassConstrained) - } - - @Test func isErrorExistential() async throws { - let result = try usingInProcessOnly { context in - try ExistentialTypeMetadata(at: InProcessMetadataPicker.stdlibAnyExistential, in: context).isErrorExistential - } - #expect(result == ExistentialTypeMetadataBaseline.stdlibAnyExistential.isErrorExistential) - } - - @Test func flags() async throws { - let result = try usingInProcessOnly { context in - try ExistentialTypeMetadata(at: InProcessMetadataPicker.stdlibAnyExistential, in: context).flags.rawValue - } - #expect(result == ExistentialTypeMetadataBaseline.stdlibAnyExistential.flagsRawValue) - } -} -``` - -- [ ] **Step 2: Update `ExistentialTypeMetadataBaselineGenerator.swift`** - -```swift -import Foundation -import SwiftSyntax -import SwiftSyntaxBuilder -@testable import MachOSwiftSection - -package enum ExistentialTypeMetadataBaselineGenerator { - package static func generate(outputDirectory: URL) throws { - let pointer = InProcessMetadataPicker.stdlibAnyExistential - let context = InProcessContext() - let metadata = try ExistentialTypeMetadata(at: pointer, in: context) - let kindRaw = metadata.kind.rawValue - let numProtocols = metadata.numberOfProtocols - let numWitnessTables = metadata.numberOfWitnessTables - let classConstrained = metadata.isClassConstrained - let errorExistential = metadata.isErrorExistential - let flagsRaw = metadata.flags.rawValue - - let registered = ["flags", "isClassConstrained", "isErrorExistential", "kind", "numberOfProtocols", "numberOfWitnessTables"] - - let header = """ - // AUTO-GENERATED — DO NOT EDIT. - // Regenerate via: swift package --allow-writing-to-package-directory regen-baselines - // Source: InProcess `Any.self`; no Mach-O section presence. - """ - - let file: SourceFileSyntax = """ - \(raw: header) - - enum ExistentialTypeMetadataBaseline { - static let registeredTestMethodNames: Set = \(literal: registered) - - struct Entry { - let kindRawValue: UInt - let numberOfProtocols: Int - let numberOfWitnessTables: Int - let isClassConstrained: Bool - let isErrorExistential: Bool - let flagsRawValue: UInt32 - } - - static let stdlibAnyExistential = Entry( - kindRawValue: \(raw: BaselineEmitter.hex(kindRaw)), - numberOfProtocols: \(literal: numProtocols), - numberOfWitnessTables: \(literal: numWitnessTables), - isClassConstrained: \(literal: classConstrained), - isErrorExistential: \(literal: errorExistential), - flagsRawValue: \(raw: BaselineEmitter.hex(flagsRaw)) - ) - } - """ - - let formatted = file.formatted().description + "\n" - let outputURL = outputDirectory.appendingPathComponent("ExistentialTypeMetadataBaseline.swift") - try formatted.write(to: outputURL, atomically: true, encoding: .utf8) - } -} -``` - -- [ ] **Step 3: Regenerate + verify ExistentialTypeMetadata** - -```bash -swift package --allow-writing-to-package-directory regen-baselines --suite ExistentialTypeMetadata 2>&1 | tail -3 -swift test --filter ExistentialTypeMetadataTests 2>&1 | tail -10 -``` -Expected: PASS. - -- [ ] **Step 4: Apply the same pattern to remaining 6 existential suites** - -Repeat steps 1-3 (suite + generator + regenerate + test) for each of: - -| Suite | InProcess source | Methods to convert | -|---|---|---| -| `ExistentialMetatypeMetadataTests` | `stdlibAnyMetatype` | `kind`, `instanceType`, `flags` | -| `ExistentialTypeFlagsTests` | `stdlibAnyExistential.flags` slice | `numProtocols`, `numWitnessTables`, `isClassConstraint`, `isErrorExistential`, `isObjCExistential`, `rawValue` | -| `ExtendedExistentialTypeMetadataTests` | `stdlibAnyEquatable` | `kind`, `shape` | -| `ExtendedExistentialTypeShapeTests` | `(stdlibAnyEquatable as ExtendedExistentialTypeMetadata).shape` | `flags`, `existentialType`, `requirementSignatureHeader` | -| `ExtendedExistentialTypeShapeFlagsTests` | shape flags slice | `specialKind`, `hasGeneralizationSignature`, `hasTypeExpression`, `rawValue` | -| `NonUniqueExtendedExistentialTypeShapeTests` | maybe-not-applicable; if `(any Equatable).shape` is uniqued, leave as `runtimeOnly` and document | - -For the shape tests, source the shape pointer like this in the suite: -```swift -let shapePointer = try usingInProcessOnly { context in - try ExtendedExistentialTypeMetadata(at: InProcessMetadataPicker.stdlibAnyEquatable, in: context).shape -} -let result = try usingInProcessOnly { context in - try ExtendedExistentialTypeShape(at: shapePointer, in: context). -} -``` - -If `NonUniqueExtendedExistentialTypeShape` cannot be sourced from `(any Equatable).self` (most extended existentials use the unique form), update that suite's allowlist entry to keep it `runtimeOnly` with detail "non-unique form not produced by stdlib types"; do not convert. - -- [ ] **Step 5: Update `CoverageAllowlistEntries.swift`** - -In `runtimeOnlyEntries`, remove the `sentinelGroup` calls for converted types: -- `ExistentialTypeMetadata` -- `ExistentialMetatypeMetadata` -- `ExtendedExistentialTypeMetadata` -- `ExtendedExistentialTypeShape` -- (Keep `NonUniqueExtendedExistentialTypeShape` if not converted) - -In `pureDataUtilityEntries`, the `ExistentialTypeFlags` and `ExtendedExistentialTypeShapeFlags` entries' members list should drop converted method names. - -- [ ] **Step 6: Run CoverageInvariant + full** - -```bash -swift test --filter MachOSwiftSectionCoverageInvariantTests 2>&1 | tail -10 -swift test --filter "ExistentialType|ExtendedExistentialType" 2>&1 | tail -5 -``` -Expected: PASS. Liar/unmarked sentinel reports empty. - -- [ ] **Step 7: Push C2 + C3 progress (mid-Phase C push)** - -```bash -git add Tests/MachOSwiftSectionTests/Fixtures/ExistentialType/ \ - Sources/MachOFixtureSupport/Baseline/Generators/ExistentialType/ \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Existential*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ExtendedExistential*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/NonUniqueExtended*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift -git commit -m "$(cat <<'EOF' -test(MachOSwiftSection): convert existential-family suites to InProcess real tests - -Phase C3. Converts 6-7 existential-related sentinel suites to -real InProcess single-reader tests using stdlib `Any.self` and -`(any Equatable).self` as metadata sources. - -NonUniqueExtendedExistentialTypeShape may remain runtimeOnly if -stdlib produces only the unique form — documented in allowlist. -EOF -)" - -git push 2>&1 | tail -3 -``` - ---- - -### Task C4: Convert fixture-nominal-bound metadata suites (~10 suites) - -**Files:** -- Modify (~10): suites under `Tests/MachOSwiftSectionTests/Fixtures/Type/{Struct,Enum,Class}/Metadata/...` -- Modify (~10): generators -- Modify: `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift` - -These suites cover metadata for types that **do** have a fixture-binary nominal type counterpart but whose metadata is still runtime-allocated (e.g., `StructMetadata` of `StructTest`). - -- [ ] **Step 1: Add fixture-nominal pickers** - -Append to `Sources/MachOFixtureSupport/InProcess/InProcessMetadataPicker.swift`: - -```swift -extension InProcessMetadataPicker { - // MARK: - fixture nominal types - // - // These metadata pointers come from `dlopen`-loaded SymbolTestsCore - // types reached via `unsafeBitCast(.self, to: UnsafeRawPointer.self)`. - // The fixture must be loaded into the test process (handled by - // MachOSwiftSectionFixtureTests' dlopen) before these are valid. - // - // Type names follow the SymbolTestsCore convention `Structs.StructTest`, - // `Classes.ClassTest`, `Enums.EnumTest`. We resolve them via @objc lookup - // when ObjC bridge applies, otherwise via a generated symbol-resolution - // helper. For simplicity here, we hard-code unsafeBitCast on the - // public Swift type reference — but the SymbolTestsCore module is - // not imported into MachOFixtureSupport (it's a separate framework - // loaded at test time). We expose them as accessor functions taking - // a metadata pointer, sourced by the consuming Suite via dlsym. - - /// Returns a metadata pointer for SymbolTestsCore's nominal type. - /// `metatypeName` is the demangled symbol of the type metadata accessor, - /// e.g. "$s15SymbolTestsCore10StructTestVMa". - package static func fixtureMetadata(symbol: String) throws -> UnsafeRawPointer { - guard let handle = dlopen(nil, RTLD_NOW) else { - throw FixtureLoadError.imageNotFoundAfterDlopen(path: "", dlerror: nil) - } - guard let accessorAddress = dlsym(handle, symbol) else { - throw FixtureLoadError.imageNotFoundAfterDlopen( - path: symbol, - dlerror: dlerror().map { String(cString: $0) } - ) - } - // Type metadata accessor signature: `MetadataResponse(MetadataRequest)`. - // For simple non-generic types, pass MetadataRequest(0) and return - // the metadata pointer from the response. - typealias MetadataAccessor = @convention(c) (UInt) -> (UnsafeRawPointer, UInt) - let accessor = unsafeBitCast(accessorAddress, to: MetadataAccessor.self) - let response = accessor(0) - return response.0 - } -} -``` - -- [ ] **Step 2: Convert `StructMetadataTests.swift`** - -Replace contents of `Tests/MachOSwiftSectionTests/Fixtures/Type/Struct/StructMetadataTests.swift` with: - -```swift -import Foundation -import Testing -import MachOFoundation -@testable import MachOSwiftSection -@testable import MachOTestingSupport -import MachOFixtureSupport - -@Suite -final class StructMetadataTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable { - static let testedTypeName = "StructMetadata" - static var registeredTestMethodNames: Set { - StructMetadataBaseline.registeredTestMethodNames - } - - @Test func kind() async throws { - let pointer = try InProcessMetadataPicker.fixtureMetadata( - symbol: "$s15SymbolTestsCore10StructTestVMa" - ) - let result = try usingInProcessOnly { context in - try StructMetadata(at: pointer, in: context).kind - } - #expect(result.rawValue == StructMetadataBaseline.structTest.kindRawValue) - } - - @Test func description() async throws { - let pointer = try InProcessMetadataPicker.fixtureMetadata( - symbol: "$s15SymbolTestsCore10StructTestVMa" - ) - let result = try usingInProcessOnly { context in - try StructMetadata(at: pointer, in: context).description - } - // The description pointer should equal the StructDescriptor's offset - // resolved via MachOFile (already covered by StructDescriptorTests), - // so just assert it's non-zero here. - #expect(result != UnsafeRawPointer(bitPattern: 0)) - } - - @Test func fieldOffsetVectorOffset() async throws { - let pointer = try InProcessMetadataPicker.fixtureMetadata( - symbol: "$s15SymbolTestsCore10StructTestVMa" - ) - let result = try usingInProcessOnly { context in - try StructMetadata(at: pointer, in: context).fieldOffsetVectorOffset - } - #expect(result == StructMetadataBaseline.structTest.fieldOffsetVectorOffset) - } -} -``` - -- [ ] **Step 3: Update `StructMetadataBaselineGenerator.swift`** - -```swift -import Foundation -import SwiftSyntax -import SwiftSyntaxBuilder -@testable import MachOSwiftSection -import MachOFixtureSupport - -package enum StructMetadataBaselineGenerator { - package static func generate(outputDirectory: URL) throws { - let pointer = try InProcessMetadataPicker.fixtureMetadata( - symbol: "$s15SymbolTestsCore10StructTestVMa" - ) - let context = InProcessContext() - let metadata = try StructMetadata(at: pointer, in: context) - let kindRaw = metadata.kind.rawValue - let fieldOffsetVectorOffset = try metadata.fieldOffsetVectorOffset - - let registered = ["description", "fieldOffsetVectorOffset", "kind"] - - let header = """ - // AUTO-GENERATED — DO NOT EDIT. - // Regenerate via: swift package --allow-writing-to-package-directory regen-baselines - // Source: InProcess SymbolTestsCore.Structs.StructTest metadata. - """ - - let file: SourceFileSyntax = """ - \(raw: header) - - enum StructMetadataBaseline { - static let registeredTestMethodNames: Set = \(literal: registered) - - struct Entry { - let kindRawValue: UInt - let fieldOffsetVectorOffset: Int - } - - static let structTest = Entry( - kindRawValue: \(raw: BaselineEmitter.hex(kindRaw)), - fieldOffsetVectorOffset: \(literal: fieldOffsetVectorOffset) - ) - } - """ - - let formatted = file.formatted().description + "\n" - let outputURL = outputDirectory.appendingPathComponent("StructMetadataBaseline.swift") - try formatted.write(to: outputURL, atomically: true, encoding: .utf8) - } -} -``` - -The mangled symbol `$s15SymbolTestsCore10StructTestVMa` follows Swift mangling: `$s` + module-name-length + module-name + type-name-length + type-name + `V` (struct) + `Ma` (metadata accessor). If `StructTest` is nested inside `Structs.swift`'s top-level enum namespace, the mangled name becomes `$s15SymbolTestsCore7StructsO10StructTestVMa`. Run - -```bash -nm -gU Tests/Projects/SymbolTests/DerivedData/Build/Products/Release/SymbolTestsCore.framework/Versions/A/SymbolTestsCore | grep '10StructTest' | grep 'Ma$' -``` - -to confirm the exact symbol name. Replace `$s15SymbolTestsCore10StructTestVMa` with whatever `nm` reports. - -- [ ] **Step 4: Regenerate + verify** - -```bash -swift package --allow-writing-to-package-directory regen-baselines --suite StructMetadata 2>&1 | tail -3 -swift test --filter StructMetadataTests 2>&1 | tail -10 -``` -Expected: PASS. - -- [ ] **Step 5: Apply the same pattern to remaining fixture-nominal suites** - -Repeat steps 2-4 for each of: - -| Suite | Fixture type | Symbol pattern | -|---|---|---| -| `EnumMetadataTests` | `EnumTest` | `$s...10EnumTestOMa` (`O` = enum) | -| `ClassMetadataTests` | `ClassTest` | `$s...09ClassTestCMa` (`C` = class) | -| `ClassMetadataObjCInteropTests` | (skip if `ClassTest` doesn't inherit NSObject — Phase B3 will add it) | -| `AnyClassMetadataTests` | `ClassTest` | reuse `$s...09ClassTestCMa` | -| `AnyClassMetadataObjCInteropTests` | (skip; depends on B3) | -| `DispatchClassMetadataTests` | `ClassTest` | reuse `$s...09ClassTestCMa` (DispatchClassMetadata layer reads class metadata + ObjC fields, can probe with regular Swift class) | -| `ValueMetadataTests` | `StructTest` | reuse | -| `StructMetadataProtocolTests` | (protocol-extension on StructMetadata; protocol methods are not stand-alone) — most likely just maps to StructMetadata methods, may not need separate suite. Inspect `Sources/MachOSwiftSection/Models/Type/Struct/StructMetadataProtocol.swift` to see what extension methods it adds. If empty, allowlist stays `pureDataUtility`. | -| `EnumMetadataProtocolTests` | similar | -| `AnyClassMetadataProtocolTests` | similar | -| `AnyClassMetadataObjCInteropProtocolTests` | similar | -| `FinalClassMetadataProtocolTests` | similar | -| `ValueMetadataProtocolTests` | similar | -| `ClassMetadataBoundsTests` | from `ClassMetadata.bounds`; reuse `ClassTest` | -| `ClassMetadataBoundsProtocolTests` | similar | -| `StoredClassMetadataBoundsTests` | similar | - -For each suite: -1. Add 2-5 `@Test` methods using `usingInProcessOnly` + appropriate metadata pointer -2. Update generator to emit ABI-literal `Entry` -3. Regenerate baseline -4. Run suite - -- [ ] **Step 6: Update `CoverageAllowlistEntries.swift`** - -Remove from `runtimeOnlyEntries`: -- All converted suite entries - -Keep: -- `ClassMetadataObjCInterop`, `AnyClassMetadataObjCInterop` (and their protocol forms) — wait until Phase B3 adds NSObject-inheriting fixture -- All `MetadataBounds*` if they're protocol-only (no public stand-alone surface) -- All `*MetadataProtocol` if they're empty marker protocols (no extension methods) - -- [ ] **Step 7: Run CoverageInvariant + full** - -```bash -swift test --filter MachOSwiftSectionCoverageInvariantTests 2>&1 | tail -10 -swift test 2>&1 | tail -5 -``` -Expected: PASS. - -- [ ] **Step 8: Commit + push** - -```bash -git add Tests/MachOSwiftSectionTests/Fixtures/Type/ \ - Tests/MachOSwiftSectionTests/Fixtures/Metadata/ \ - Sources/MachOFixtureSupport/Baseline/Generators/Type/ \ - Sources/MachOFixtureSupport/Baseline/Generators/Metadata/ \ - Sources/MachOFixtureSupport/InProcess/InProcessMetadataPicker.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StructMetadata*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/EnumMetadata*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ClassMetadata*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AnyClassMetadata*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/DispatchClass*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ValueMetadata*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/*Bounds*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift -git commit -m "$(cat <<'EOF' -test(MachOSwiftSection): convert fixture-nominal metadata suites to InProcess - -Phase C4. Converts ~10 sentinel suites covering metadata bound to -SymbolTestsCore nominal types (StructMetadata of StructTest, etc.) -using dlsym-resolved metadata accessor functions. - -ObjC-interop variants (ClassMetadataObjCInterop, etc.) deferred to -Phase B3 once NSObject-inheriting fixture lands. -EOF -)" - -git push 2>&1 | tail -3 -``` - ---- - -### Task C5: Convert metadata-layer suites (~6 suites) - -**Files:** -- Modify (~6): `Tests/MachOSwiftSectionTests/Fixtures/Metadata/*.swift` -- Modify (~6): generators -- Modify: `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift` - -This task converts the "metadata layer" suites — types covering the metadata layout prefix (`*MetadataHeader`, `*Bounds`, `MetadataResponse`, `Metadata`, `FullMetadata`, `MetadataAccessorFunction`, `SingletonMetadataPointer`, etc.). They reuse the metadata pointers from C2/C3/C4 and read offset slices. - -- [ ] **Step 1: Convert `MetadataTests.swift`** - -```swift -import Foundation -import Testing -import MachOFoundation -@testable import MachOSwiftSection -@testable import MachOTestingSupport -import MachOFixtureSupport - -@Suite -final class MetadataTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable { - static let testedTypeName = "Metadata" - static var registeredTestMethodNames: Set { - MetadataBaseline.registeredTestMethodNames - } - - @Test func kind() async throws { - let pointer = InProcessMetadataPicker.stdlibIntMetatype - let result = try usingInProcessOnly { context in - try Metadata(at: pointer, in: context).kind - } - #expect(result.rawValue == MetadataBaseline.intMetadata.kindRawValue) - } - - @Test func valueWitnessTable() async throws { - let pointer = InProcessMetadataPicker.stdlibIntMetatype - let pointer2 = try usingInProcessOnly { context in - try Metadata(at: pointer, in: context).valueWitnessTable - } - #expect(pointer2 != UnsafeRawPointer(bitPattern: 0)) - } -} -``` - -- [ ] **Step 2: Update generator + regenerate** - -Apply pattern from C2 step 2; baseline emits `kindRawValue` for `Int.self.self` metadata layer. - -- [ ] **Step 3: Convert remaining metadata-layer suites** - -| Suite | Pointer source | Methods | -|---|---|---| -| `FullMetadataTests` | `stdlibIntMetatype` | `metadata`, `header` | -| `MetadataWrapperTests` | `stdlibIntMetatype` | `pointer`, `kind` | -| `MetadataResponseTests` | construct via `MetadataRequest(0)` accessor call | `metadata`, `state` | -| `MetadataRequestTests` | new `MetadataRequest(state: .complete)` instance | `rawValue`, `state`, `isBlocking` | -| `MetadataAccessorFunctionTests` | dlsym lookup of any accessor | `address`, `invoke` | -| `SingletonMetadataPointerTests` | from a fixture singleton accessor | `pointer`, `metadata` | -| `MetadataBoundsTests` | computed offset on class metadata | `negativeSizeInWords`, `positiveSizeInWords` | -| `HeapMetadataHeaderTests` | header offset before class metadata | `destroy`, `valueWitnessTable` | -| `HeapMetadataHeaderPrefixTests` | similar | `destroy` | -| `TypeMetadataHeaderTests` | type-metadata layout prefix | `valueWitnessTable` | - -- [ ] **Step 4: Update `CoverageAllowlistEntries.swift`** - -Remove converted suites' entries from `runtimeOnlyEntries`. Keep `MetadataProtocol`, `MetadataBoundsProtocol`, `*BaseProtocol` (marker-only protocols) and `GenericBoxHeapMetadata`, `HeapLocalVariableMetadata` (cannot construct stably). - -- [ ] **Step 5: Run CoverageInvariant + full + commit + push (Phase C complete)** - -```bash -swift test --filter MachOSwiftSectionCoverageInvariantTests 2>&1 | tail -10 -swift test 2>&1 | tail -5 - -git add Tests/MachOSwiftSectionTests/Fixtures/Metadata/ \ - Sources/MachOFixtureSupport/Baseline/Generators/Metadata/ \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Metadata*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Heap*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Type*Header*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift -git commit -m "$(cat <<'EOF' -test(MachOSwiftSection): convert metadata-layer suites to InProcess - -Phase C5 — completes Phase C. Converts ~6 metadata-layer suites: -Metadata, FullMetadata, MetadataWrapper, MetadataRequest/Response, -MetadataAccessorFunction, SingletonMetadataPointer, *MetadataHeader, -*Bounds. Each reuses pointers from C2-C4 + offset arithmetic. - -Remaining runtimeOnly sentinel: marker protocols (no public extension -methods) and GenericBoxHeapMetadata / HeapLocalVariableMetadata -(cannot construct stably from tests). -EOF -)" - -git push 2>&1 | tail -3 -``` - ---- - -## Phase B — SymbolTestsCore Fixture Extension - -### Task B0: Re-align baselines if `xcodebuild` rebuild drift detected - -**Files:** -- Possibly: all `Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/*Baseline.swift` - -This task is conditional. If during Phase A or Phase C the `SymbolTestsCore.framework` binary in `Tests/Projects/SymbolTests/DerivedData/` was modified incidentally (e.g., re-derived during Xcode auto-build), the file/image baselines may have drifted relative to the latest build. Re-run the regen and review diffs. - -- [ ] **Step 1: Detect drift** - -```bash -xcodebuild -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj \ - -scheme SymbolTestsCore -configuration Release build 2>&1 | tail -5 - -swift package --allow-writing-to-package-directory regen-baselines 2>&1 | tail -5 - -git diff --stat Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ -``` -Expected: empty diff (Phase A/C didn't introduce drift) or a small set of file/image-baseline tweaks. - -- [ ] **Step 2: If diff is non-empty, review and commit baseline alignment** - -```bash -git diff Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ # human review -git add Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ -git commit -m "$(cat <<'EOF' -test(MachOSwiftSection): realign baselines after SymbolTestsCore rebuild - -Phase B0. After xcodebuild produced a fresh SymbolTestsCore.framework, -baselines drift in offset/flag values. This commit captures the new -ABI literal values; review the diff to confirm only expected drift. -EOF -)" -``` - -If the diff is empty, **skip this task**. - -- [ ] **Step 3: Confirm tests pass** - -```bash -swift test --filter MachOSwiftSectionTests 2>&1 | tail -5 -``` -Expected: PASS. - ---- - -### Task B1: Add `DefaultOverrideTable.swift` fixture - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/DefaultOverrideTable.swift` -- Modify: `Sources/MachOFixtureSupport/Baseline/BaselineFixturePicker.swift` -- Modify: `Sources/MachOFixtureSupport/Baseline/Generators/Class/MethodDefaultOverrideDescriptorBaselineGenerator.swift` -- Modify: `Sources/MachOFixtureSupport/Baseline/Generators/Class/MethodDefaultOverrideTableHeaderBaselineGenerator.swift` -- Modify: `Sources/MachOFixtureSupport/Baseline/Generators/Class/OverrideTableHeaderBaselineGenerator.swift` -- Modify: `Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Method/MethodDefaultOverrideDescriptorTests.swift` -- Modify: `Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Method/MethodDefaultOverrideTableHeaderTests.swift` -- Modify: `Tests/MachOSwiftSectionTests/Fixtures/Type/Class/OverrideTableHeaderTests.swift` -- Modify: `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift` - -- [ ] **Step 1: Create the fixture file** - -Create `Tests/Projects/SymbolTests/SymbolTestsCore/DefaultOverrideTable.swift`: - -```swift -// Fixtures producing __swift5_types entries with method default-override tables. -// -// `dynamicReplacement(for:)` causes the compiler to emit a method -// default-override descriptor in the class context descriptor's tail. -// We declare a "primary" class with a dynamic method, then a separate -// extension that replaces it via `@_dynamicReplacement(for:)`. - -public enum DefaultOverrideTableFixtures { - /// Primary class whose dynamic method will be replaced. The presence of - /// `dynamic` triggers the class to emit a method-override stub in its - /// vtable, and the replacement adds an entry to the default-override - /// table. - open class PrimaryWithDynamic { - public init() {} - public dynamic func dynamicMethod() -> Int { 1 } - } - - /// Replacement extension. The `@_dynamicReplacement(for:)` attribute - /// causes the compiler to emit a default-override descriptor in the - /// primary class's descriptor tail. - public static func setupReplacement() {} -} - -extension DefaultOverrideTableFixtures.PrimaryWithDynamic { - @_dynamicReplacement(for: dynamicMethod()) - public func replacedDynamicMethod() -> Int { 2 } -} -``` - -- [ ] **Step 2: Rebuild SymbolTestsCore** - -```bash -xcodebuild -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj \ - -scheme SymbolTestsCore -configuration Release build 2>&1 | tail -5 -``` -Expected: -``` -** BUILD SUCCEEDED ** -``` - -- [ ] **Step 3: Verify the new descriptor surface** - -```bash -nm -gU Tests/Projects/SymbolTests/DerivedData/Build/Products/Release/SymbolTestsCore.framework/Versions/A/SymbolTestsCore \ - | grep 'PrimaryWithDynamic' -``` -Expected: visible mangled symbols for the class and its replacement, including a `Mn` (nominal type descriptor) suffix. - -- [ ] **Step 4: Add picker for the new fixture** - -In `Sources/MachOFixtureSupport/Baseline/BaselineFixturePicker.swift`, append: - -```swift -extension BaselineFixturePicker { - /// Picks the SymbolTestsCore class with default-override table - /// (`DefaultOverrideTableFixtures.PrimaryWithDynamic`). - package static func class_PrimaryWithDynamic( - in machO: some MachOSwiftSectionRepresentableWithCache - ) throws -> ClassDescriptor { - try required( - try machO.swift.typeContextDescriptors.compactMap(\.class).first(where: { descriptor in - try descriptor.name(in: machO) == "PrimaryWithDynamic" - }) - ) - } -} -``` - -- [ ] **Step 5: Convert `MethodDefaultOverrideDescriptorTests.swift` to real test** - -```swift -import Foundation -import Testing -import MachOFoundation -@testable import MachOSwiftSection -@testable import MachOTestingSupport -import MachOFixtureSupport - -@Suite -final class MethodDefaultOverrideDescriptorTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable { - static let testedTypeName = "MethodDefaultOverrideDescriptor" - static var registeredTestMethodNames: Set { - MethodDefaultOverrideDescriptorBaseline.registeredTestMethodNames - } - - @Test func offset() async throws { - let fileSubject = try BaselineFixturePicker.class_PrimaryWithDynamic(in: machOFile) - let imageSubject = try BaselineFixturePicker.class_PrimaryWithDynamic(in: machOImage) - - let result = try acrossAllReaders( - file: { try fileSubject.methodDefaultOverrideDescriptors(in: machOFile).first!.offset }, - image: { try imageSubject.methodDefaultOverrideDescriptors(in: machOImage).first!.offset } - ) - #expect(result == MethodDefaultOverrideDescriptorBaseline.primaryReplacement.offset) - } - - @Test func layout() async throws { - let fileSubject = try BaselineFixturePicker.class_PrimaryWithDynamic(in: machOFile) - let imageSubject = try BaselineFixturePicker.class_PrimaryWithDynamic(in: machOImage) - - let originalDescriptorOffset = try acrossAllReaders( - file: { try fileSubject.methodDefaultOverrideDescriptors(in: machOFile).first!.layout.originalMethodDescriptor.offset }, - image: { try imageSubject.methodDefaultOverrideDescriptors(in: machOImage).first!.layout.originalMethodDescriptor.offset } - ) - #expect(originalDescriptorOffset == MethodDefaultOverrideDescriptorBaseline.primaryReplacement.layoutOriginalMethodDescriptorOffset) - } -} -``` - -(Adjust calls to match the actual public API of `ClassDescriptor` for accessing `methodDefaultOverrideDescriptors`. Check `Sources/MachOSwiftSection/Models/Type/Class/ClassDescriptor.swift` for the exact method/property name. If absent, use the existing `extension`-style accessor and adjust the suite.) - -- [ ] **Step 6: Update `MethodDefaultOverrideDescriptorBaselineGenerator.swift`** - -```swift -import Foundation -import SwiftSyntax -import SwiftSyntaxBuilder -import MachOFoundation -@testable import MachOSwiftSection - -package enum MethodDefaultOverrideDescriptorBaselineGenerator { - package static func generate( - in machO: some MachOSwiftSectionRepresentableWithCache, - outputDirectory: URL - ) throws { - let primary = try BaselineFixturePicker.class_PrimaryWithDynamic(in: machO) - let firstDescriptor = try primary.methodDefaultOverrideDescriptors(in: machO).first! - let offset = firstDescriptor.offset - let originalOffset = firstDescriptor.layout.originalMethodDescriptor.offset - - let registered = ["implementationSymbols", "layout", "offset", "originalMethodDescriptor", "replacementMethodDescriptor"] - - let header = """ - // AUTO-GENERATED — DO NOT EDIT. - // Regenerate via: swift package --allow-writing-to-package-directory regen-baselines - // Source fixture: SymbolTestsCore.DefaultOverrideTableFixtures.PrimaryWithDynamic - """ - - let file: SourceFileSyntax = """ - \(raw: header) - - enum MethodDefaultOverrideDescriptorBaseline { - static let registeredTestMethodNames: Set = \(literal: registered) - - struct Entry { - let offset: Int - let layoutOriginalMethodDescriptorOffset: Int - } - - static let primaryReplacement = Entry( - offset: \(raw: BaselineEmitter.hex(offset)), - layoutOriginalMethodDescriptorOffset: \(raw: BaselineEmitter.hex(originalOffset)) - ) - } - """ - - let formatted = file.formatted().description + "\n" - let outputURL = outputDirectory.appendingPathComponent("MethodDefaultOverrideDescriptorBaseline.swift") - try formatted.write(to: outputURL, atomically: true, encoding: .utf8) - } -} -``` - -Update `BaselineGenerator.dispatchSuite`'s `MethodDefaultOverrideDescriptor` case to pass the `machOFile` argument (it was previously called without `in:`): - -```swift -case "MethodDefaultOverrideDescriptor": - try MethodDefaultOverrideDescriptorBaselineGenerator.generate(in: machOFile, outputDirectory: outputDirectory) -``` - -- [ ] **Step 7: Update `MethodDefaultOverrideTableHeaderTests.swift` and `OverrideTableHeaderTests.swift`** - -Apply the same pattern: source `numEntries` from `class_PrimaryWithDynamic`'s default-override table header. Both suites become `acrossAllReaders` based. - -- [ ] **Step 8: Regenerate the three baselines** - -```bash -swift package --allow-writing-to-package-directory regen-baselines --suite MethodDefaultOverrideDescriptor 2>&1 | tail -3 -swift package --allow-writing-to-package-directory regen-baselines --suite MethodDefaultOverrideTableHeader 2>&1 | tail -3 -swift package --allow-writing-to-package-directory regen-baselines --suite OverrideTableHeader 2>&1 | tail -3 -``` - -- [ ] **Step 9: Run the three converted suites** - -```bash -swift test --filter "MethodDefaultOverrideDescriptorTests|MethodDefaultOverrideTableHeaderTests|OverrideTableHeaderTests" 2>&1 | tail -10 -``` -Expected: PASS. - -- [ ] **Step 10: Remove the three suite groups from `needsFixtureExtensionEntries`** - -In `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift`'s `needsFixtureExtensionEntries`, delete the three `sentinelGroup` calls: -- `MethodDefaultOverrideDescriptor` -- `MethodDefaultOverrideTableHeader` -- `OverrideTableHeader` - -- [ ] **Step 11: Run CoverageInvariant + full** - -```bash -swift test --filter MachOSwiftSectionCoverageInvariantTests 2>&1 | tail -10 -swift test --filter MachOSwiftSectionTests 2>&1 | tail -5 -``` -Expected: PASS. - -- [ ] **Step 12: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/DefaultOverrideTable.swift \ - Tests/Projects/SymbolTests/DerivedData/ \ - Sources/MachOFixtureSupport/Baseline/BaselineFixturePicker.swift \ - Sources/MachOFixtureSupport/Baseline/Generators/Class/MethodDefaultOverrideDescriptorBaselineGenerator.swift \ - Sources/MachOFixtureSupport/Baseline/Generators/Class/MethodDefaultOverrideTableHeaderBaselineGenerator.swift \ - Sources/MachOFixtureSupport/Baseline/Generators/Class/OverrideTableHeaderBaselineGenerator.swift \ - Sources/MachOFixtureSupport/Baseline/BaselineGenerator.swift \ - Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Method/MethodDefaultOverrideDescriptorTests.swift \ - Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Method/MethodDefaultOverrideTableHeaderTests.swift \ - Tests/MachOSwiftSectionTests/Fixtures/Type/Class/OverrideTableHeaderTests.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/MethodDefaultOverride*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/OverrideTableHeader*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift -git commit -m "$(cat <<'EOF' -test(fixture): add DefaultOverrideTable fixture, convert 3 sentinel suites - -Phase B1 of fixture-coverage tightening. Adds -SymbolTestsCore.DefaultOverrideTableFixtures.PrimaryWithDynamic, a -class with a `dynamic` method replaced via `@_dynamicReplacement(for:)`. -This produces a method default-override table in the class context -descriptor's tail, surfacing: - - - MethodDefaultOverrideDescriptor (per-replacement record) - - MethodDefaultOverrideTableHeader (table header) - - OverrideTableHeader (general method-override table header) - -All three suites convert from sentinel registrationOnly to real -acrossAllReaders cross-reader assertions. Allowlist entries removed. -EOF -)" -``` - ---- - -### Task B2: Add `ResilientClasses.swift` fixture (2 suites) - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/ResilientClasses.swift` -- Modify: `Sources/MachOFixtureSupport/Baseline/BaselineFixturePicker.swift` -- Modify (2): generators -- Modify (2): suites -- Modify: `CoverageAllowlistEntries.swift` - -- [ ] **Step 1: Create fixture** - -```swift -// Fixtures producing classes with resilient superclass references and -// resilient bounds (i.e., the compiler defers metadata bounds computation -// to runtime because the parent class's layout may change). - -public enum ResilientClassFixtures { - /// Resilient class — declared `@_fixed_layout` is INTENTIONALLY OMITTED, - /// and the framework is built `-enable-library-evolution` so this class - /// gets resilient metadata bounds. - public class ResilientBase { - public init() {} - public var counter: Int = 0 - } - - /// Subclass referring to the resilient parent. Triggers a - /// ResilientSuperclass record in the class context descriptor. - public class ResilientChild: ResilientBase { - public override init() { super.init() } - public var extraField: Int = 0 - } -} -``` - -- [ ] **Step 2: Rebuild + verify** - -```bash -xcodebuild -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj \ - -scheme SymbolTestsCore -configuration Release build 2>&1 | tail -5 -nm -gU Tests/Projects/SymbolTests/DerivedData/Build/Products/Release/SymbolTestsCore.framework/Versions/A/SymbolTestsCore \ - | grep 'ResilientChild' -``` - -- [ ] **Step 3: Add `class_ResilientChild` picker, convert `ResilientSuperclassTests` and `StoredClassMetadataBoundsTests`, update generators, regenerate baselines, run, remove from `needsFixtureExtensionEntries`** - -Follow B1 pattern (steps 4-12) substituting `ResilientChild` for `PrimaryWithDynamic`. The two suites are: - -| Suite | Methods | Fixture source | -|---|---|---| -| `ResilientSuperclassTests` | `superclass`, `layout`, `offset` | `ResilientChild`'s class descriptor's resilient-superclass tail | -| `StoredClassMetadataBoundsTests` | `immediateMembers`, `bounds` | `ResilientChild` runtime-loaded class metadata's bounds slot | - -Note: `StoredClassMetadataBoundsTests` reads class metadata at runtime, so it stays InProcess-only — but it's now backed by a real fixture-bound class, so you can assert pinned literal values. - -- [ ] **Step 4: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/ResilientClasses.swift \ - Tests/Projects/SymbolTests/DerivedData/ \ - Sources/MachOFixtureSupport/Baseline/BaselineFixturePicker.swift \ - Sources/MachOFixtureSupport/Baseline/Generators/Class/ResilientSuperclassBaselineGenerator.swift \ - Sources/MachOFixtureSupport/Baseline/Generators/Class/StoredClassMetadataBoundsBaselineGenerator.swift \ - Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Resilient/ResilientSuperclassTests.swift \ - Tests/MachOSwiftSectionTests/Fixtures/Type/Class/Metadata/Bounds/StoredClassMetadataBoundsTests.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ResilientSuperclassBaseline.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/StoredClassMetadataBoundsBaseline.swift \ - Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift -git commit -m "$(cat <<'EOF' -test(fixture): add ResilientClasses fixture, convert 2 sentinel suites - -Phase B2. SymbolTestsCore.ResilientClassFixtures.ResilientChild -inherits from ResilientBase; under -enable-library-evolution the -parent's metadata is resilient, triggering: - - - ResilientSuperclass (descriptor tail record) - - StoredClassMetadataBounds (runtime-loaded class metadata bounds) - -Both suites converted from sentinel to real tests; allowlist updated. -EOF -)" -``` - ---- - -### Task B3: Add `ObjCClassWrappers.swift` fixture (4 suites) - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/ObjCClassWrappers.swift` -- Modify: same support files as B1, B2 -- Modify (4): suites + generators -- Modify: `CoverageAllowlistEntries.swift` - -- [ ] **Step 1: Create fixture** - -```swift -import Foundation - -// Fixtures producing classes with ObjC interop, surfacing -// AnyClassMetadataObjCInterop, ClassMetadataObjCInterop, -// ObjCClassWrapperMetadata, and ObjC protocol prefix metadata. - -public enum ObjCClassWrapperFixtures { - /// Swift class inheriting NSObject — gets full ObjC interop metadata. - @objc(SymbolTestsCoreObjCBridgeClass) - public class ObjCBridge: NSObject { - public override init() { super.init() } - @objc public var label: String = "objc" - } - - /// Class with ObjC-protocol conformance — surfaces RelativeObjCProtocolPrefix. - @objc public protocol ObjCProto { - @objc func ping() - } - - @objc(SymbolTestsCoreObjCBridgeWithProto) - public class ObjCBridgeWithProto: NSObject, ObjCProto { - public override init() { super.init() } - public func ping() {} - } -} -``` - -- [ ] **Step 2: Rebuild SymbolTestsCore** - -```bash -xcodebuild -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj \ - -scheme SymbolTestsCore -configuration Release build 2>&1 | tail -5 -``` - -- [ ] **Step 3: Add picker for ObjC-bridge classes** - -```swift -extension BaselineFixturePicker { - package static func class_ObjCBridge( - in machO: some MachOSwiftSectionRepresentableWithCache - ) throws -> ClassDescriptor { - try required( - try machO.swift.typeContextDescriptors.compactMap(\.class).first(where: { descriptor in - try descriptor.name(in: machO) == "ObjCBridge" - }) - ) - } -} -``` - -- [ ] **Step 4: Convert 4 suites** - -| Suite | Methods | Pattern | -|---|---|---| -| `ObjCClassWrapperMetadataTests` | `kind`, `objcClass` | InProcess `unsafeBitCast(NSObject.self, to: UnsafeRawPointer.self)` (NSObject metadata is the wrapped form) | -| `ClassMetadataObjCInteropTests` | full property set | InProcess on `ObjCBridge`'s metadata via dlsym | -| `AnyClassMetadataObjCInteropTests` | `isaPointer`, `superclass`, etc. | same | -| `RelativeObjCProtocolPrefixTests` | `isObjC`, `rawValue` | from `ObjCProto`'s relative-protocol-descriptor reference in `ObjCBridgeWithProto`'s conformance | - -- [ ] **Step 5-7: Update generators, regenerate baselines, run** - -Standard pattern per B1 steps 6-9. - -- [ ] **Step 8: Remove from `needsFixtureExtensionEntries`** - -Delete `sentinelGroup` calls for: `ObjCClassWrapperMetadata`, `RelativeObjCProtocolPrefix`, `ObjCProtocolPrefix`. Move `ClassMetadataObjCInterop` and `AnyClassMetadataObjCInterop` from `runtimeOnlyEntries` to nothing (they're converted now). - -- [ ] **Step 9: Commit** - -```bash -git add Tests/Projects/SymbolTests/SymbolTestsCore/ObjCClassWrappers.swift \ - Tests/Projects/SymbolTests/DerivedData/ \ - Sources/MachOFixtureSupport/Baseline/BaselineFixturePicker.swift \ - Sources/MachOFixtureSupport/Baseline/Generators/ \ - Tests/MachOSwiftSectionTests/Fixtures/Type/Class/ \ - Tests/MachOSwiftSectionTests/Fixtures/Protocol/ObjC/ \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ObjCClassWrapper*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ClassMetadataObjCInterop*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AnyClassMetadataObjCInterop*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/RelativeObjCProtocolPrefix*.swift \ - Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift -git commit -m "$(cat <<'EOF' -test(fixture): add ObjCClassWrappers fixture, convert 4 sentinel suites - -Phase B3. ObjCBridge (NSObject-derived) and ObjCBridgeWithProto -(conforming to @objc protocol) surface: - - ObjCClassWrapperMetadata - - ClassMetadataObjCInterop, AnyClassMetadataObjCInterop - - RelativeObjCProtocolPrefix - -All 4 suites converted to real tests via dlsym + InProcess pointer -acquisition. Allowlist entries removed. -EOF -)" - -git push 2>&1 | tail -3 # Phase B mid-push -``` - ---- - -### Task B4: Add `ObjCResilientStubs.swift` fixture (1 suite) - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/ObjCResilientStubs.swift` -- Modify: `BaselineFixturePicker.swift`, generator, suite, `CoverageAllowlistEntries.swift` - -- [ ] **Step 1: Create fixture** - -```swift -import Foundation - -// A Swift class inheriting from a resilient ObjC class. The Swift compiler -// emits an `ObjCResilientClassStubInfo` record so the runtime can fixup -// the class's superclass pointer at load time. -// -// Inherit from NSDictionary (a resilient Foundation class); the framework's -// -enable-library-evolution means the Swift compiler treats Foundation as -// resilient and emits the stub record. - -public enum ObjCResilientStubFixtures { - public class ResilientObjCSubclass: NSDictionary {} -} -``` - -- [ ] **Step 2-9: Rebuild, picker, convert `ObjCResilientClassStubInfoTests`, generator, regenerate, run, remove allowlist entry, commit** - -Standard pattern. - -```bash -git commit -m "test(fixture): add ObjCResilientStubs fixture, convert ObjCResilientClassStubInfo" -``` - ---- - -### Task B5: Add `CanonicalSpecializedMetadata.swift` fixture (4 suites, experimental) - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/CanonicalSpecializedMetadata.swift` -- Modify (4): suites + generators -- Modify: `CoverageAllowlistEntries.swift` - -- [ ] **Step 1: Attempt fixture** - -```swift -// Fixtures producing canonical pre-specialized metadata. -// `@_specialize(exported: true, where T == Int)` on a generic function or -// type causes the compiler to emit a "canonical specialized metadata -// list" entry for the specialization, complete with caching once-token -// and accessor function. -// -// Stability note: this attribute's emission rules can shift between -// Swift versions. If the resulting binary doesn't surface -// CanonicalSpecializedMetadatas* records (verify with otool / xref to -// __swift5_types tail), this fixture stays sentinel. - -public enum CanonicalSpecializedFixtures { - @_specialize(exported: true, where T == Int) - @_specialize(exported: true, where T == String) - public static func specializedFunction(_ value: T) -> T { value } - - public struct SpecializedGeneric { - public init(_ value: T) {} - } -} -``` - -- [ ] **Step 2: Rebuild + verify presence** - -```bash -xcodebuild ... build -otool -V -s __TEXT __swift5_types Tests/Projects/SymbolTests/DerivedData/.../SymbolTestsCore | head -30 -``` - -If the `__swift5_types` section gains entries with canonical-specialized-metadata tails (look for "canonical specialized" in `otool -V -s __TEXT __swift5_types`), proceed. Otherwise: - -- [ ] **Step 3: If presence not surfaced, document and skip** - -Update `CoverageAllowlistEntries.swift` `needsFixtureExtensionEntries` to relabel the 4 canonical-specialized entries as `runtimeOnly` with detail "@_specialize(exported:) on stdlib types not emitted by Swift 6.2 — needs revisit when toolchain emission changes". Move them to `runtimeOnlyEntries`. Commit: - -```bash -git commit -m "test: defer canonical-specialized-metadata fixture (Swift 6.2 toolchain doesn't emit)" -``` - -Skip remaining steps for B5. - -- [ ] **Step 4: Otherwise, convert 4 suites + commit** - -Standard pattern, similar to B1. - ---- - -### Task B6: Add `ForeignTypes.swift` fixture (2 suites) - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/ForeignTypes.swift` -- Modify (2): suites + generators -- Modify: `CoverageAllowlistEntries.swift` - -- [ ] **Step 1: Create fixture (uses CoreFoundation)** - -```swift -import CoreFoundation - -// Fixtures producing references to foreign classes. CoreFoundation types -// (CFString, CFArray) are imported as foreign classes — the Swift compiler -// emits a ForeignClassMetadata record for them. - -public enum ForeignTypeFixtures { - public static func foreignClassReference() -> CFString { - "" as CFString - } -} -``` - -- [ ] **Step 2-9: Rebuild, convert `ForeignClassMetadataTests` and `ForeignReferenceTypeMetadataTests`, etc.** - -Note: `ForeignReferenceTypeMetadata` covers C++ interop foreign-reference types; if SymbolTestsCore can't reasonably import C++, leave that one as `runtimeOnly` with detail "no C++ interop in SymbolTestsCore". - -```bash -git commit -m "test(fixture): add ForeignTypes fixture, convert ForeignClassMetadata" -``` - ---- - -### Task B7: Add `GenericValueParameters.swift` fixture (2 suites) - -**Files:** -- Create: `Tests/Projects/SymbolTests/SymbolTestsCore/GenericValueParameters.swift` -- Modify: `CoverageAllowlistEntries.swift` - -- [ ] **Step 1: Attempt fixture** - -```swift -// Generic types with value parameters (Swift 6.1+). - -@available(macOS 26.0, *) -public enum GenericValueFixtures { - public struct FixedSizeArray { - public init() {} - } -} -``` - -- [ ] **Step 2: Rebuild** - -If Swift 6.2 / Xcode 26 is the active toolchain, this should compile. If not: - -- [ ] **Step 3: If unavailable, defer** - -Move `GenericValueDescriptor` and `GenericValueHeader` from `needsFixtureExtensionEntries` to `runtimeOnlyEntries` with detail "value generics require macOS 26.0 + InProcess only — defer to follow-up PR after toolchain stabilizes". Commit: - -```bash -git commit -m "test: defer value-generic fixture pending toolchain stability" -``` - -- [ ] **Step 4: Otherwise, convert 2 suites + commit** - -Standard pattern. - -```bash -git commit -m "test(fixture): add GenericValueParameters fixture, convert GenericValueDescriptor/Header" -git push 2>&1 | tail -3 # Phase B complete push -``` - ---- - -## Phase D — Cleanup - -### Task D1: Update CLAUDE.md fixture-coverage section - -**Files:** -- Modify: `CLAUDE.md` - -- [ ] **Step 1: Update CLAUDE.md fixture-coverage section** - -In `CLAUDE.md`, find the "Fixture-Based Test Coverage (MachOSwiftSection)" section. Replace it with: - -```markdown -## Fixture-Based Test Coverage (MachOSwiftSection) - -`MachOSwiftSection/Models/` is exhaustively covered by `Tests/MachOSwiftSectionTests/Fixtures/`. Suites mirror the source directory and assert one of: - -- **Cross-reader equality** across MachOFile/MachOImage/InProcess + their ReadingContext counterparts (via `acrossAllReaders` / `acrossAllContexts` helpers), plus per-method ABI literal values from `__Baseline__/*Baseline.swift` — this is the standard depth. -- **InProcess single-reader equality** plus per-method ABI literal values (via `usingInProcessOnly` helper). Used for runtime-allocated metadata types (MetatypeMetadata, TupleTypeMetadata, etc.) that have no Mach-O section presence. -- **Sentinel allowlist** with typed `SentinelReason` (in `CoverageAllowlistEntries.swift`). Used for: - - `pureDataUtility`: pure raw-value enums / flag bitfields with no behavior to test (tests would just be tautologies) - - `runtimeOnly`: types impossible to construct stably from tests (e.g., `swift_allocBox`-allocated `GenericBoxHeapMetadata`) - -`MachOSwiftSectionCoverageInvariantTests` enforces four invariants: -1. Every public method in `Sources/MachOSwiftSection/Models/` has a registered test (or allowlist entry) -2. Every registered test name maps to an actual public method -3. Sentinel-tagged keys' Suites must actually have sentinel behavior (no acrossAllReaders / inProcessContext) -4. Sentinel-behavior Suites must be tagged in the allowlist (no silent sentinels) - -To add a new public method: - -1. Add the method. -2. Run `swift test --filter MachOSwiftSectionCoverageInvariantTests` to see which Suite needs updating. -3. Add a `@Test` to that Suite, using `acrossAllReaders` for fixture-bound types or `usingInProcessOnly` for runtime-only metadata. -4. Append the member name to `registeredTestMethodNames`. -5. Run `swift package --allow-writing-to-package-directory regen-baselines --suite ` to regenerate the baseline. -6. Re-run the affected Suite. - -To regenerate all baselines after fixture rebuild or toolchain upgrade: - -```bash -xcodebuild -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj -scheme SymbolTestsCore -configuration Release build -swift package --allow-writing-to-package-directory regen-baselines -git diff Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ # review drift -``` - -The `regen-baselines` command is provided by the `RegenerateBaselinesPlugin` -SwiftPM command plugin (`Plugins/RegenerateBaselinesPlugin/`). It builds and -invokes the `baseline-generator` executable target. From Xcode you can also -right-click the package → "Regenerate MachOSwiftSection fixture-test ABI -baselines.". -``` - -- [ ] **Step 2: Run all gates one last time** - -```bash -swift build 2>&1 | tail -3 -swift test 2>&1 | tail -10 -``` -Expected: All green. - -- [ ] **Step 3: Verify the residual sentinel set** - -```bash -grep -E '\.sentinel\(' Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift | grep -oE 'runtimeOnly|needsFixtureExtension|pureDataUtility' | sort | uniq -c -``` -Expected: only `runtimeOnly` (~3-5) and `pureDataUtility` (~25). `needsFixtureExtension` count should be 0 (or only appear with explicit "deferred" detail comments from B5/B7 if those were skipped). - -- [ ] **Step 4: Commit** - -```bash -git add CLAUDE.md -git commit -m "$(cat <<'EOF' -docs(MachOSwiftSection): update fixture-coverage workflow for sentinel concept - -Phase D of fixture-coverage tightening (closes the work). Updates -CLAUDE.md to reflect: - - acrossAllReaders / usingInProcessOnly distinction - - typed SentinelReason categorization - - 4-invariant CoverageInvariant - - regen-baselines SwiftPM plugin path -EOF -)" -``` - -- [ ] **Step 5: Push final** - -```bash -git push 2>&1 | tail -3 -``` - ---- - -## Self-Review Notes - -This plan covers every requirement from the spec: - -- ✅ Goal 1 (typed `SentinelReason`): Tasks A1, A2 -- ✅ Goal 2 (4 invariants): Task A3 -- ✅ Goal 3 (88 suites categorized): Task A2 -- ✅ Goal 4 (15 fixture types added): Tasks B1-B7 -- ✅ Goal 5 (~30 runtime-only suites converted): Tasks C2-C5 -- ✅ Goal 6 (residual ~25 pureDataUtility + ~3-5 runtimeOnly): verified in Task D1 step 3 - -Risks called out in spec section 5.4 are addressed inline: -- Scanner edge cases — addressed in A1 step 7 implementation -- xcodebuild drift — addressed in B0 conditional task -- Fixture build failures — fallback paths in B5 (canonical specialized) and B7 (value generics) explicitly documented -- macOS 26 dependency — guarded with `@available` in InProcessMetadataPicker -- swift_allocBox unavailability — `GenericBoxHeapMetadata` and `HeapLocalVariableMetadata` kept as `runtimeOnly` with documented detail diff --git a/docs/superpowers/specs/2026-03-15-ci-snapshot-testing-design.md b/docs/superpowers/specs/2026-03-15-ci-snapshot-testing-design.md deleted file mode 100644 index 00d0b102..00000000 --- a/docs/superpowers/specs/2026-03-15-ci-snapshot-testing-design.md +++ /dev/null @@ -1,446 +0,0 @@ -# CI Snapshot Testing Design - -## Overview - -Snapshot tests on CI are driven **only** by the `SymbolTestsCore` framework built from `Tests/Projects/SymbolTests/SymbolTests.xcodeproj`. Snapshots for system dyld cache images and bundled Xcode frameworks are explicitly **out of scope** because the binaries they depend on drift across macOS / Xcode updates and cannot be reproduced by a CI runner. - -Coverage requirement: every source file in `Tests/Projects/SymbolTests/SymbolTestsCore/` (one file per Swift language / ABI feature category) must have a dedicated snapshot that fails when that category's emitted metadata changes. - -## Why only SymbolTestsCore - -| Source | Reproducible on CI? | Dimensions of drift | -|---|---|---| -| System dyld cache (`.current`) | No — contents change with every macOS patch | macOS version, kernel/cache layout, framework updates | -| Xcode bundled frameworks | No — change with every Xcode release | Xcode version, swiftlang version | -| `SymbolTestsCore` (checked-in Swift sources) | **Yes** — built from pinned source with pinned Xcode | Only the pinned Xcode/swiftlang version | - -Collapsing the problem to a single deterministic source means: -- Snapshots live in the main repo under `Tests/**/Snapshots/__Snapshots__/` and are committed alongside source changes. -- No external fixtures package, no version-keyed directories, no auto-recording workflow. -- A snapshot diff is a direct signal: "the metadata this library emits for a given Swift construct just changed." - -## Architecture - -``` -MachOSwiftSection (main repo) - │ - ├── Tests/Projects/SymbolTests/ - │ ├── SymbolTests.xcodeproj (3 targets) - │ ├── SymbolTestsCore/*.swift (feature categories — the fixture source) - │ ├── SymbolTestsHelper/*.swift (support types referenced by SymbolTestsCore) - │ └── DerivedData/.../Release/ - │ └── SymbolTestsCore.framework/Versions/A/SymbolTestsCore ← the Mach-O binary - │ - └── Tests/ - ├── SwiftDumpTests/Snapshots/ - │ ├── SymbolTestsCoreDumpSnapshotTests.swift - │ └── __Snapshots__/SymbolTestsCoreDumpSnapshotTests/ - │ ├── actorsSnapshot.1.txt - │ ├── enumsSnapshot.1.txt - │ └── … (one file per category) - └── SwiftInterfaceTests/Snapshots/ - ├── SymbolTestsCoreInterfaceSnapshotTests.swift - └── __Snapshots__/SymbolTestsCoreInterfaceSnapshotTests/ - └── interfaceSnapshot.1.txt (single full-module interface) -``` - -## Building the Fixture Binary - -`SymbolTestsCore.framework` is an Xcode-project artifact, so `swift test` alone cannot produce it. CI (and any fresh developer checkout) must build it first. - -**Pre-test step added to `macOS.yml`:** - -```bash -xcodebuild \ - -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj \ - -scheme SymbolTestsCore \ - -configuration Release \ - -derivedDataPath Tests/Projects/SymbolTests/DerivedData \ - -destination 'generic/platform=macOS' \ - build -``` - -### Path anchoring — how tests locate the binary - -`MachOFileName.SymbolTestsCore` stores a **relative path**: `../../Tests/Projects/SymbolTests/DerivedData/SymbolTests/Build/Products/Release/SymbolTestsCore.framework/Versions/A/SymbolTestsCore`. - -That path is **not** resolved against the current working directory. `MachOTestingSupport/Extensions.swift::loadFromFile(named:)` resolves it against `#filePath` of that source file, i.e. `Sources/MachOTestingSupport/Extensions.swift`. `../../` therefore climbs out of `Sources/MachOTestingSupport/` and lands at the repository root, then descends into `Tests/Projects/SymbolTests/DerivedData/…`. - -Consequences: -- The `xcodebuild -derivedDataPath Tests/Projects/SymbolTests/DerivedData` argument is interpreted by `xcodebuild` relative to **its own CWD**, which must be the repo root when the workflow step runs (GitHub Actions' default working directory is the checkout root, so this lines up with the runtime lookup). -- The two paths agree **because both end up pointing at `/Tests/Projects/SymbolTests/DerivedData/…`** — one via `#filePath`-relative resolution at runtime, the other via CWD-relative resolution at build time. Any CI step that runs `xcodebuild` from a different CWD (e.g. inside `Tests/Projects/SymbolTests/`) will break the alignment. - -### Implicit dependency - -`SymbolTests.xcscheme` declares `buildImplicitDependencies = "YES"`, so building the `SymbolTestsCore` target transitively produces `SymbolTestsHelper.framework`. The runtime dynamic-link search path inside `SymbolTestsCore` already includes `@rpath`, and both frameworks end up in the same `Build/Products/Release/` directory, so no extra `DYLD_FRAMEWORK_PATH` plumbing is needed. - -### Other notes - -- The scheme list is visible in `Tests/Projects/SymbolTests/SymbolTests.xcodeproj/xcshareddata/xcschemes/` — `SymbolTestsCore.xcscheme` is already a shared scheme and therefore available to headless `xcodebuild`. -- `-destination 'generic/platform=macOS'` avoids picking a concrete simulator. `MachOFileTests` then selects `.arm64` via `preferredArchitecture`; GitHub's `macos-15` runners are Apple Silicon so the preferred slice exists. -- Cache the `DerivedData` directory in the workflow to avoid a full rebuild on every run (see `macOS.yml` outline below). -- Do **not** pipe `xcodebuild` through `xcsift` on CI — `xcsift` is a local convenience and is not installed on GitHub-hosted runners. If the raw log is too noisy, use `-quiet` instead. - -## Test Architecture - -### Namespace-filtered collectors - -49 of the 54 `SymbolTestsCore/*.swift` files open with `public enum { … }`, making the top-level enum name match the file name and therefore also the `@Test func Snapshot()` handler. Five files deviate: - -- `AsyncSequence.swift` / `Codable.swift` / `StringInterpolation.swift` — the enum name differs from the filename (`AsyncSequenceTests`, `CodableTests`, `StringInterpolations`) to avoid stdlib-type collisions (`Swift.AsyncSequence`, `Swift.Codable`, `String.StringInterpolation`). Their snapshot tests pass the real enum name as `inNamespace:` while keeping the `@Test func` named after the filename. -- `GlobalDeclarations.swift` — no `TypeContextDescriptor` emitted. Handled as an edge case; per-category dump is expected empty. -- `NeverExtensions.swift` — all declarations are `extension Never: …`; descriptors belong to `Swift.Never`, so a mangled-symbol fallback matches any `$ss5NeverO*`-stem symbol in the conforming-type reference (i.e. `_$ss5NeverO*` as it appears in the Mach-O symbol table, where the leading `_` is the C-style symbol-name prefix). - -See "Edge-case categories" below for the `GlobalDeclarations.swift` / `NeverExtensions.swift` details. - -Add to `MachOTestingSupport/SnapshotDumpableTests.swift`: - -```swift -extension SnapshotDumpableTests { - /// Walks the parent chain of a type context descriptor wrapper and returns the name of - /// the top-level enclosing type (the category namespace), or nil if the symbol lives - /// at module scope — in which case it should be considered part of the - /// "GlobalDeclarations" bucket by the caller. - package func rootNamespace( - of descriptor: TypeContextDescriptorWrapper, - in machO: MachO - ) throws -> String? - - /// Same as above but keyed on a ProtocolDescriptor (used when filtering the - /// protocols / associatedTypes sections). - package func rootNamespace( - of descriptor: ProtocolDescriptor, - in machO: MachO - ) throws -> String? - - /// Category-filtered variants of the existing collect* methods. Each first enumerates - /// the full descriptor list and keeps only entries whose rootNamespace matches `category`. - package func collectDumpTypes( - for machO: MachO, - inNamespace category: String, - options: DumpableTypeOptions = [.enum, .struct, .class] - ) async throws -> String - package func collectDumpProtocols( - for machO: MachO, - inNamespace category: String - ) async throws -> String - package func collectDumpProtocolConformances( - for machO: MachO, - inNamespace category: String - ) async throws -> String - package func collectDumpAssociatedTypes( - for machO: MachO, - inNamespace category: String - ) async throws -> String - - /// Combined per-category dump used by the snapshot tests. Concatenates the four - /// sections with `// MARK:` headers; omits sections whose filtered output is empty. - package func collectDump( - for machO: MachO, - inNamespace category: String - ) async throws -> String -} -``` - -Note the parameter types: the existing `collect*` implementations in the codebase take `TypeContextDescriptorWrapper` (an enum of `.enum`/`.struct`/`.class`) for types and a `ProtocolDescriptor` for protocols. The namespace filter therefore branches on the descriptor flavour rather than using one generic `TypeContextDescriptorProtocol`. - -The combined `collectDump(for:inNamespace:)` is the primary entry point used by snapshot tests; individual collectors remain available for targeted assertions. - -### ProtocolConformance attribution - -A `ProtocolConformanceDescriptor` binds a *conforming type* to a *protocol*. Both sides have a namespace. The filter uses the **conforming type's** root namespace: - -- `extension Extensions.ExtensionConstrainedStruct: Extensions.ExtensionProtocol` → both sides live under `Extensions`, no ambiguity. -- `extension Never: Protocols.ProtocolTest` (from `NeverExtensions.swift`) → conforming type is `Swift.Never`, root namespace is therefore **not** `NeverExtensions`. Handled by the edge-case rule below, not by the default filter. - -Rationale: attributing a conformance to the conforming type follows the same convention used when browsing the dump output — one instance of the type's metadata gathers all the conformances declared for it. - -### Edge-case categories - -| File | Why it doesn't fit | How its snapshot is produced | -|---|---|---| -| `GlobalDeclarations.swift` | Declares only `public let / var / func` — no TypeContextDescriptor is emitted. | The per-category dump is intentionally empty. Coverage for globals comes from the full-module **interface** snapshot (where globals are printed). The `@Test func globalDeclarationsSnapshot()` still exists and asserts against the empty (or near-empty) combined output — a diff would surface unexpected new TypeContextDescriptor emissions for globals. | -| `NeverExtensions.swift` | All declarations are `extension Never: …`; descriptors live under `Swift.Never`, not a `NeverExtensions` namespace. | The filter takes a fallback list: `ProtocolConformanceDescriptor`s whose conforming type is `Swift.Never` are attributed to the `NeverExtensions` bucket. Implementation: `collectDumpProtocolConformances(for:inNamespace: "NeverExtensions")` switches to the explicit Never-based predicate. This is the only category with a non-namespace attribution rule. | - -If another fixture file is ever added that doesn't use the `public enum ` pattern, it must either be rewritten to fit the pattern or added as a new edge case here (and to the coverage-invariant test). - -### Dump snapshot suite - -One suite, one `@Test` per category. The whole class is a mechanical mapping from the file list. - -```swift -@Suite(.serialized, .snapshots(record: .missing)) -final class SymbolTestsCoreDumpSnapshotTests: MachOFileTests, SnapshotDumpableTests, @unchecked Sendable { - override class var fileName: MachOFileName { .SymbolTestsCore } - - @Test func actorsSnapshot() async throws { - let output = try await collectDump(for: machOFile, inNamespace: "Actors") - assertSnapshot(of: output, as: .lines) - } - - @Test func associatedTypeWitnessPatternsSnapshot() async throws { - let output = try await collectDump(for: machOFile, inNamespace: "AssociatedTypeWitnessPatterns") - assertSnapshot(of: output, as: .lines) - } - - // … one @Test per category, 54 total -} -``` - -### Interface snapshot suite - -`SwiftInterfaceBuilder` emits the whole module at once; splitting by namespace would require new builder plumbing that isn't justified for this fixture. Keep a single snapshot per module: - -```swift -@Suite(.serialized, .snapshots(record: .missing)) -final class SymbolTestsCoreInterfaceSnapshotTests: MachOFileTests, SnapshotInterfaceTests, @unchecked Sendable { - override class var fileName: MachOFileName { .SymbolTestsCore } - - @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) - @Test func interfaceSnapshot() async throws { - let output = try await collectInterfaceString(in: machOFile) - assertSnapshot(of: output, as: .lines) - } -} -``` - -The interface snapshot acts as an end-to-end regression check across every category simultaneously. When it breaks, the corresponding per-category dump snapshot usually points at the root cause. - -## CI Workflow Changes - -### `macOS.yml` — full job outline - -```yaml -jobs: - macos_test: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [macos-15] - xcode-version: ["16.3"] - env: - MACHO_SWIFT_SECTION_SILENT_TEST: 1 - GH_TOKEN: ${{ github.token }} - steps: - - uses: actions/checkout@v4 - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: ${{ matrix.xcode-version }} - - - name: Resolve SPM dependencies - run: swift package update - - - name: Cache SymbolTests DerivedData - uses: actions/cache@v4 - with: - path: Tests/Projects/SymbolTests/DerivedData - key: symboltests-${{ matrix.xcode-version }}-${{ hashFiles('Tests/Projects/SymbolTests/**/*.swift', 'Tests/Projects/SymbolTests/**/*.pbxproj') }} - - - name: Build SymbolTestsCore fixture - working-directory: ${{ github.workspace }} - run: | - set -o pipefail - xcodebuild \ - -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj \ - -scheme SymbolTestsCore \ - -configuration Release \ - -derivedDataPath Tests/Projects/SymbolTests/DerivedData \ - -destination 'generic/platform=macOS' \ - -quiet \ - build - - - name: Upload xcodebuild logs on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: symboltests-derived-data-logs - path: Tests/Projects/SymbolTests/DerivedData/Logs - if-no-files-found: ignore - - - name: Swift test (debug) - run: swift test -c debug --build-path .build-test-debug - - - name: Swift test (release) - run: swift test -c release --build-path .build-test-release -``` - -Key points: -- `swift package update` runs before `swift test` per the project-level SPM workflow convention; otherwise stale `.build/checkouts/` can cause hard-to-diagnose resolution failures. -- `working-directory: ${{ github.workspace }}` is explicit on the fixture-build step so the `../../`-anchored path stays aligned with the `#filePath`-based runtime lookup (see "Path anchoring" above). -- `-quiet` keeps the `xcodebuild` output compact without relying on `xcsift` (which is not installed on GitHub-hosted runners). -- `set -o pipefail` ensures a failed `xcodebuild` fails the step even if something is piped later. -- The cache key hashes both the SymbolTestsCore source files and `project.pbxproj` so any change invalidates the cache. -- The upload-logs step runs only on failure and is tolerant of missing directories, which keeps happy-path runs uncluttered. - -### No recording workflow - -With deterministic snapshots, there is no `snapshot-record.yml`. When a Swift compiler change legitimately alters emitted metadata, the developer regenerates snapshots locally: - -```bash -SNAPSHOT_TESTING_RECORD=all swift test \ - --filter SymbolTestsCoreDumpSnapshotTests \ - --filter SymbolTestsCoreInterfaceSnapshotTests -``` - -`swift test --filter` takes a substring matched against the test identifier and accepts multiple occurrences. Using the two concrete class names (rather than a regex) avoids ambiguity and will not accidentally pick up unrelated suites. Updated `__Snapshots__/` files get committed in the same PR as the triggering source change. - -## Coverage Matrix - -Every row below must have (a) a dedicated `@Test func Snapshot()` in `SymbolTestsCoreDumpSnapshotTests` and (b) at least one type / protocol / conformance / associated type visible in the combined dump output (otherwise the category's source file is suspect). - -Unless a row in the "Dump snapshot" column explicitly annotates a `namespace:`, the `inNamespace:` argument passed by the corresponding `@Test` is the filename (without the `.swift` extension). The three annotated rows (`AsyncSequence.swift`, `Codable.swift`, `StringInterpolation.swift`) deviate because their top-level `public enum` uses a different identifier to avoid colliding with stdlib types — see "Namespace-filtered collectors" above. - -| # | Category source file | Dump snapshot | Appears in interface | -|---|---|---|---| -| 1 | Actors.swift | `actorsSnapshot` | yes | -| 2 | AssociatedTypeWitnessPatterns.swift | `associatedTypeWitnessPatternsSnapshot` | yes | -| 3 | AsyncSequence.swift | `asyncSequenceSnapshot` (namespace: `AsyncSequenceTests`) | yes | -| 4 | Attributes.swift | `attributesSnapshot` | yes | -| 5 | BasicTypes.swift | `basicTypesSnapshot` | yes | -| 6 | BuiltinTypeFields.swift | `builtinTypeFieldsSnapshot` | yes | -| 7 | ClassBoundGenerics.swift | `classBoundGenericsSnapshot` | yes | -| 8 | Classes.swift | `classesSnapshot` | yes | -| 9 | Codable.swift | `codableSnapshot` (namespace: `CodableTests`) | yes | -| 10 | CollectionConformances.swift | `collectionConformancesSnapshot` | yes | -| 11 | Concurrency.swift | `concurrencySnapshot` | yes | -| 12 | ConditionalConformanceVariants.swift | `conditionalConformanceVariantsSnapshot` | yes | -| 13 | CustomLiterals.swift | `customLiteralsSnapshot` | yes | -| 14 | DefaultImplementationVariants.swift | `defaultImplementationVariantsSnapshot` | yes | -| 15 | DeinitVariants.swift | `deinitVariantsSnapshot` | yes | -| 16 | DependentTypeAccess.swift | `dependentTypeAccessSnapshot` | yes | -| 17 | DiamondInheritance.swift | `diamondInheritanceSnapshot` | yes | -| 18 | DistributedActors.swift | `distributedActorsSnapshot` | yes | -| 19 | Enums.swift | `enumsSnapshot` | yes | -| 20 | ErrorTypes.swift | `errorTypesSnapshot` | yes | -| 21 | ExistentialAny.swift | `existentialAnySnapshot` | yes | -| 22 | Extensions.swift | `extensionsSnapshot` | yes | -| 23 | FieldDescriptorVariants.swift | `fieldDescriptorVariantsSnapshot` | yes | -| 24 | FunctionFeatures.swift | `functionFeaturesSnapshot` | yes | -| 25 | FunctionTypes.swift | `functionTypesSnapshot` | yes | -| 26 | GenericFieldLayout.swift | `genericFieldLayoutSnapshot` | yes | -| 27 | GenericRequirementVariants.swift | `genericRequirementVariantsSnapshot` | yes | -| 28 | Generics.swift | `genericsSnapshot` | yes | -| 29 | GlobalDeclarations.swift | `globalDeclarationsSnapshot` | yes | -| 30 | Initializers.swift | `initializersSnapshot` | yes | -| 31 | KeyPaths.swift | `keyPathsSnapshot` | yes | -| 32 | MarkerProtocols.swift | `markerProtocolsSnapshot` | yes | -| 33 | MetatypeUsage.swift | `metatypeUsageSnapshot` | yes | -| 34 | NestedFunctions.swift | `nestedFunctionsSnapshot` | yes | -| 35 | NestedGenerics.swift | `nestedGenericsSnapshot` | yes | -| 36 | NeverExtensions.swift | `neverExtensionsSnapshot` | yes | -| 37 | Noncopyable.swift | `noncopyableSnapshot` | yes | -| 38 | OpaqueReturnTypes.swift | `opaqueReturnTypesSnapshot` | yes | -| 39 | Operators.swift | `operatorsSnapshot` | yes | -| 40 | OptionSetAndRawRepresentable.swift | `optionSetAndRawRepresentableSnapshot` | yes | -| 41 | OverloadedMembers.swift | `overloadedMembersSnapshot` | yes | -| 42 | PropertyWrapperVariants.swift | `propertyWrapperVariantsSnapshot` | yes | -| 43 | ProtocolComposition.swift | `protocolCompositionSnapshot` | yes | -| 44 | Protocols.swift | `protocolsSnapshot` | yes | -| 45 | ResultBuilderDSL.swift | `resultBuilderDSLSnapshot` | yes | -| 46 | SameTypeRequirements.swift | `sameTypeRequirementsSnapshot` | yes | -| 47 | StaticMembers.swift | `staticMembersSnapshot` | yes | -| 48 | StringInterpolation.swift | `stringInterpolationSnapshot` (namespace: `StringInterpolations`) | yes | -| 49 | Structs.swift | `structsSnapshot` | yes | -| 50 | Subscripts.swift | `subscriptsSnapshot` | yes | -| 51 | Tuples.swift | `tuplesSnapshot` | yes | -| 52 | UnsafePointers.swift | `unsafePointersSnapshot` | yes | -| 53 | VTableEntryVariants.swift | `vTableEntryVariantsSnapshot` | yes | -| 54 | WeakUnownedReferences.swift | `weakUnownedReferencesSnapshot` | yes | - -Total: 54 per-category dump snapshots + 1 full-module interface snapshot = **55 files** under `__Snapshots__/`. - -### Coverage invariant - -Completeness is enforced by a dedicated Swift test (not a shell step) so it runs in the same `swift test` invocation as the snapshot tests themselves — contributors get the failure locally before pushing. Skeleton: - -```swift -// Tests/SwiftDumpTests/Snapshots/SymbolTestsCoreCoverageInvariantTests.swift -import Foundation -import Testing -@testable import MachOTestingSupport - -@Suite -struct SymbolTestsCoreCoverageInvariantTests { - /// `@Test` method names declared on SymbolTestsCoreDumpSnapshotTests that intentionally - /// have no backing category source file (edge-case shims, if any). - private static let allowlist: Set = [] - - @Test func everyCategoryHasASnapshotTest() throws { - let fixtureDir = URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() // Snapshots/ - .deletingLastPathComponent() // SwiftDumpTests/ - .deletingLastPathComponent() // Tests/ - .appendingPathComponent("Tests/Projects/SymbolTests/SymbolTestsCore") - - let categories = try FileManager.default - .contentsOfDirectory(at: fixtureDir, includingPropertiesForKeys: nil) - .filter { $0.pathExtension == "swift" } - .map { $0.deletingPathExtension().lastPathComponent } - .sorted() - - let expectedMethods = Set(categories.map { "\($0.lowercasedFirst)Snapshot" }) - let actualMethods = SymbolTestsCoreDumpSnapshotTests.registeredTestMethodNames - - let missing = expectedMethods.subtracting(actualMethods).subtracting(Self.allowlist) - #expect(missing.isEmpty, "Missing per-category snapshot tests: \(missing.sorted())") - } -} -``` - -Implementation notes: -- `SymbolTestsCoreDumpSnapshotTests.registeredTestMethodNames` is a `static let` populated at type-load time; it can be as simple as a hand-maintained array — the coverage test fails loudly if it drifts from the filesystem. -- `lowercasedFirst` lowercases the leading character so `Actors.swift` → `actorsSnapshot`, matching the `@Test func` naming in the dump suite. -- Running the check from a Swift test keeps it platform-agnostic and reusable for local development. -- The `allowlist` is there for safety: if a SymbolTestsCore file is ever renamed or split, the invariant shouldn't block an otherwise-valid PR while the migration is in flight. - -## Cleanup - -Remove the DSC- and Xcode-sourced snapshot tests and their snapshots: - -- Delete classes: - - `Tests/SwiftDumpTests/Snapshots/DyldCacheDumpSnapshotTests.swift` - - `Tests/SwiftDumpTests/Snapshots/XcodeMachOFileDumpSnapshotTests.swift` - - `Tests/SwiftInterfaceTests/Snapshots/DyldCacheInterfaceSnapshotTests.swift` - - `Tests/SwiftInterfaceTests/Snapshots/XcodeMachOFileInterfaceSnapshotTests.swift` -- Delete the corresponding `__Snapshots__/` directories of those four classes. -- Rename the existing `MachOFileDumpSnapshotTests` → `SymbolTestsCoreDumpSnapshotTests` (same with the interface counterpart); migrate the old single `typesSnapshot/protocolsSnapshot/…` snapshot files into the new per-category layout by regenerating once locally with `SNAPSHOT_TESTING_RECORD=all`. -- Leave `.gitignore` untouched — local `__Snapshots__` stay tracked, which is the desired behaviour. - -## Migration Plan - -1. **Add namespace-filtered collectors** in `MachOTestingSupport` (`SnapshotDumpableTests.swift`). No test changes yet; existing tests keep passing. -2. **Add CI build step** for `SymbolTestsCore.framework` plus the DerivedData cache. Verify `macOS.yml` is green with the current test set still in place. -3. **Introduce `SymbolTestsCoreDumpSnapshotTests`** with one `@Test` per category, run locally with `SNAPSHOT_TESTING_RECORD=all` to generate all 54 snapshot files, and commit them. -4. **Rename** `MachOFileInterfaceSnapshotTests` → `SymbolTestsCoreInterfaceSnapshotTests` (trivial; its existing single snapshot can stay). -5. **Delete** the DSC / Xcode snapshot classes and their `__Snapshots__` subdirectories. -6. **Add the coverage-invariant check** described above so future fixture additions fail CI until a matching snapshot test is registered. - -## Reproducibility Notes - -- **Swift compiler / swiftlang upgrades** may legitimately change emitted metadata. When that happens, regenerate snapshots locally per the "No recording workflow" section and commit the updated files as part of the Xcode-bump PR. -- **Pin the CI `xcode-version` explicitly** — never use `latest-stable`. Any bump is an intentional, reviewable change. -- **Section-layout order is linker-determined.** `machO.swift.typeContextDescriptors` returns descriptors in the order they appear in `__swift5_types`, and that order comes from `ld` / `ld-prime`. For a given Xcode + source combination it is stable; across Xcode updates it may shuffle. If ordering churn becomes a frequent source of snapshot diffs (i.e. linker changes dominate over metadata changes), consider sorting the output by mangled name before snapshotting — but keep the default "as-emitted" ordering until that noise shows up in practice, since it preserves more signal. -- **Architecture pinning.** `MachOFileTests.preferredArchitecture` is `.arm64`, and `macos-15` runners are Apple Silicon, so the `.arm64` slice is always what gets snapshotted. A matrix entry that added an Intel runner would produce a different binary and therefore a different snapshot — avoid doing that without separating the snapshots. -- **`MACHO_SWIFT_SECTION_SILENT_TEST=1`** stays set on CI to keep snapshot test output clean. - -## Developer Bootstrap - -For contributors running tests locally without Xcode preopened on the `SymbolTests` project: - -```bash -# One-time (or whenever SymbolTestsCore/ sources change): -xcodebuild \ - -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj \ - -scheme SymbolTestsCore \ - -configuration Release \ - -derivedDataPath Tests/Projects/SymbolTests/DerivedData \ - -destination 'generic/platform=macOS' \ - build - -# Then the normal test flow: -swift package update -swift test -``` - -Package this into `Scripts/build-test-fixtures.sh` (a sibling of the existing `build-executable-product.sh`) so new contributors have a one-command entry point. The README's "Running tests" section should reference the script and explain that skipping it causes `MachOFileTests` to throw at `init()` with a "file not found" error at `Tests/Projects/SymbolTests/DerivedData/.../SymbolTestsCore`. diff --git a/docs/superpowers/specs/2026-04-10-symboltestscore-integration-tests-design.md b/docs/superpowers/specs/2026-04-10-symboltestscore-integration-tests-design.md deleted file mode 100644 index 06c0d25b..00000000 --- a/docs/superpowers/specs/2026-04-10-symboltestscore-integration-tests-design.md +++ /dev/null @@ -1,188 +0,0 @@ -# SymbolTestsCore Integration Tests Design - -**Date:** 2026-04-10 -**Branch:** feature/vtable-offset-and-member-ordering -**Scope:** Extend SymbolTestsCore with attribute-focused types; add two-layer integration tests covering both new and existing functionality. - -## Goal - -The existing test suite for SymbolTestsCore relies heavily on "dump and print" tests without structured assertions. This design adds: - -1. New Swift types in SymbolTestsCore that exercise attribute inference features -2. Middle-layer integration tests that load the compiled binary and assert on parsed `TypeDefinition` data -3. End-to-end tests that verify `SwiftInterfaceBuilder` output strings - -## SymbolTestsCore: New Types - -Added to `Tests/Projects/SymbolTests/SymbolTestsCore/SymbolTestsCore.swift`: - -### PropertyWrapperStruct - -```swift -@propertyWrapper -public struct PropertyWrapperStruct { - public var wrappedValue: Value - public var projectedValue: ClosedRange - - public init(wrappedValue: Value, range: ClosedRange) { - self.projectedValue = range - self.wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound) - } -} -``` - -Exercises: `TypeAttributeInferrer` detecting `@propertyWrapper` via `wrappedValue` field. - -### ResultBuilderStruct - -```swift -@resultBuilder -public struct ResultBuilderStruct { - public static func buildBlock(_ components: Element...) -> [Element] { components } - public static func buildOptional(_ component: [Element]?) -> [Element] { component ?? [] } -} -``` - -Exercises: `TypeAttributeInferrer` detecting `@resultBuilder` via `static buildBlock`. - -### DynamicMemberLookupStruct - -```swift -@dynamicMemberLookup -public struct DynamicMemberLookupStruct { - public subscript(dynamicMember member: String) -> Int { 0 } -} -``` - -Exercises: `TypeAttributeInferrer` detecting `@dynamicMemberLookup` via `subscript(dynamicMember:)`. - -### DynamicCallableStruct - -```swift -@dynamicCallable -public struct DynamicCallableStruct { - public func dynamicallyCall(withArguments arguments: [Int]) -> Int { - arguments.reduce(0, +) - } - public func dynamicallyCall(withKeywordArguments arguments: KeyValuePairs) -> Int { 0 } -} -``` - -Exercises: `TypeAttributeInferrer` detecting `@dynamicCallable` via `dynamicallyCall`. - -### ObjCAttributeClass - -```swift -public class ObjCAttributeClass: NSObject { - @objc public func objcMethod() {} - @nonobjc public func nonobjcMethod() {} - @objc public dynamic func objcDynamicMethod() {} -} -``` - -Exercises: `MemberAttributeInferrer` detecting `@objc`, `@nonobjc`, and `dynamic` from real binary symbols. - -## Test File 1: SymbolTestsCoreIntegrationTests.swift - -**Location:** `Tests/SwiftInterfaceTests/SymbolTestsCoreIntegrationTests.swift` - -Loads `SymbolTestsCore` binary via `MachOFileTests` base class. Uses `SwiftInterfaceIndexer` and `TypeDefinition` APIs for assertions. - -### Type Parsing - -- Parsed type names include `StructTest`, `ClassTest`, `MultiPayloadEnumTests`, `ProtocolTest`, etc. -- Each type's kind (struct/class/enum) is correct. - -### Fields and Stored Properties - -- `GenericStructNonRequirement` fields: `field1` (Double), `field2` (A), `field3` (Int) in order. - -### Protocol Conformances - -- `StructTest` conforms to `ProtocolTest` and `ProtocolWitnessTableTest`. -- `GenericRequirementTest` conforms to `ProtocolTest`. -- `Never: @retroactive IteratorProtocol` has `isRetroactive == true`. - -### Class Hierarchy and Override - -- `SubclassTest.instanceMethod` has `isOverride == true`. -- `FinalClassTest.dynamicMethod` has `isOverride == true`. -- `ClassTest` own methods have `isOverride == false`. - -### Nested Types - -- `GenericRequirementTest` has child type `RawRepresentableNestedStruct`. -- `RawRepresentableNestedStruct` has child type `NestedStruct`. - -### Associated Types - -- `ProtocolTest` has `associatedtype Body`. - -### Type Attributes (Integration) - -- `PropertyWrapperStruct` → inferred `.propertyWrapper` -- `ResultBuilderStruct` → inferred `.resultBuilder` -- `DynamicMemberLookupStruct` → inferred `.dynamicMemberLookup` -- `DynamicCallableStruct` → inferred `.dynamicCallable` -- `StructTest` → no attribute inferred (negative test) - -### Member Attributes (Integration) - -- `ObjCAttributeClass.objcMethod` → `.objc` -- `ObjCAttributeClass.nonobjcMethod` → `.nonobjc` -- `ObjCAttributeClass.objcDynamicMethod` → `.objc`, `.dynamic` -- `ClassTest.dynamicVariable` → `.dynamic` -- `ClassTest.dynamicMethod` → `.dynamic` - -### VTable Offset and Member Ordering - -- `ClassTest` ordered members: vtable-offset members first, sorted by vtable offset ascending. -- `SubclassTest` override members retain vtable offsets. - -### PWT Offset Ordering - -- `StructTest: ProtocolWitnessTableTest` extension ordered members sorted by PWT offset. - -## Test File 2: SymbolTestsCoreE2ETests.swift - -**Location:** `Tests/SwiftInterfaceTests/SymbolTestsCoreE2ETests.swift` - -Uses `SwiftInterfaceBuilder` to generate full output string, then asserts on content. - -**Configuration:** `printVTableOffset: true`, `memberSortOrder: .byOffset`, `resilienceAwareAttributes: false`. - -### Type Attributes in Output - -- Output contains `@propertyWrapper` before `PropertyWrapperStruct` declaration. -- Output contains `@resultBuilder` before `ResultBuilderStruct` declaration. -- Output contains `@dynamicMemberLookup` before `DynamicMemberLookupStruct`. -- Output contains `@dynamicCallable` before `DynamicCallableStruct`. - -### Member Attributes in Output - -- `ObjCAttributeClass` block contains `@objc`. -- `ClassTest` block contains `dynamic`. - -### VTable Offset in Output - -- `ClassTest` members have `vtable offset` comments. -- VTable offset values are in ascending order. - -### Structure Completeness - -- Output contains all expected type declarations. -- Conditional conformances include `where` clauses. -- `@retroactive` annotation present for retroactive conformances. -- `override` keyword present for override members. - -## Build Prerequisite - -The SymbolTests Xcode project must be rebuilt in Release configuration after adding new types: - -```bash -xcodebuild -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj \ - -scheme SymbolTests \ - -configuration Release \ - -derivedDataPath Tests/Projects/SymbolTests/DerivedData \ - build -``` diff --git a/docs/superpowers/specs/2026-04-13-symboltestscore-fixture-expansion-design.md b/docs/superpowers/specs/2026-04-13-symboltestscore-fixture-expansion-design.md deleted file mode 100644 index 1010d916..00000000 --- a/docs/superpowers/specs/2026-04-13-symboltestscore-fixture-expansion-design.md +++ /dev/null @@ -1,149 +0,0 @@ -# SymbolTestsCore Fixture Expansion Design - -**Date:** 2026-04-13 -**Branch:** feature/vtable-offset-and-member-ordering -**Scope:** Expand the SymbolTestsCore test fixture with 44 new Swift source files and 4 additions to existing files, producing broader coverage of Swift mangling patterns and `__swift5_*` metadata shapes consumable by the MachOSwiftSection parser. - -## Goal - -`SymbolTestsCore` is compiled as a framework and then loaded by `SwiftInterfaceTests` as a Mach-O fixture. Each declaration in `SymbolTestsCore` exists to produce distinctive mangled symbols and Swift metadata descriptors that exercise the parsing pipeline end-to-end (MachOSwiftSection → SwiftDump → SwiftInterface). - -The current 18 files already cover a good baseline (structs, classes, enums, protocols, generics, actors, opaque return types, noncopyable types, etc.), but there are significant gaps around: - -- Swift features that produce distinctive mangling (KeyPaths, Typealiases, Default arguments, Property observers, Codable synthesis, etc.) -- Binary-layer metadata variants that MachOSwiftSection directly parses (`__swift5_fieldmd`, VTable entries, generic requirement kinds, conditional conformance, default implementations, etc.) - -This design adds 44 new Swift source files plus 4 edits to existing files. No new test cases in `Tests/SwiftInterfaceTests/` are required — existing E2E/Integration tests are value-based and tolerant of new declarations. The fixture itself is the deliverable; assertions can be layered on in follow-up PRs if desired. - -## Non-Goals - -- No additions to the `SymbolTests`, `SymbolTestsHelper`, or `Tests/SwiftInterfaceTests/` targets. -- No new integration/E2E test assertions in this scope (follow-up). -- No changes to `SwiftInterfaceBuilder`, `SwiftInterfaceIndexer`, or other parsing code. -- No macros or C++ interop (would require additional target config). - -## Constraints - -- Each file must be a `public enum` namespace (matching existing style) with only `public` declarations inside, so the binary always exposes them. -- Must compile with Swift 6.2 / Xcode 26 targets. -- `SymbolTestsCore.xcodeproj` uses `PBXFileSystemSynchronizedRootGroup` — new `.swift` files dropped into `Tests/Projects/SymbolTests/SymbolTestsCore/` are automatically picked up, **no `project.pbxproj` modification is required**. -- Files must not collide with or shadow existing type names (`StructTest`, `ClassTest`, `EnumTest`, etc.). -- Avoid private Swift compiler attributes (`@_` prefix) except where already used in existing files (`@_originallyDefinedIn`, `@_hasStorage` only if necessary). - -## File Index (44 new files) - -### Category 1 — General Swift Features (files 1–24) - -| # | File | Produced mangling / metadata | -|---|------|------------------------------| -| 1 | `KeyPaths.swift` | `KeyPath`, `WritableKeyPath`, `ReferenceWritableKeyPath`, `PartialKeyPath`, `AnyKeyPath` as stored fields; `\Type.prop` literal in stored closures | -| 2 | `Typealiases.swift` | Generic typealias, nested typealias, function-type typealias, constrained typealias | -| 3 | `Extensions.swift` | Dedicated `where`-clause conditional extensions, multi-constraint extensions, cross-protocol default implementations | -| 4 | `DefaultArguments.swift` | Default argument generator symbols (`fA_` / `fA0_`) on methods, initializers, subscripts | -| 5 | `PropertyObservers.swift` | `willSet`/`didSet` witness functions; `oldValue`/`newValue` captures | -| 6 | `Initializers.swift` | `convenience`, `required`, `init?`, `init!`, `init throws(E)`, `init() async`, `init() async throws(E)` | -| 7 | `Codable.swift` | Synthesized `init(from:)`, `encode(to:)`, nested `CodingKeys` enum; custom implementations too | -| 8 | `AccessLevels.swift` | `package`, `fileprivate`, `open`, nested access-level variation | -| 9 | `Availability.swift` | `@available` multi-platform, `deprecated`, `unavailable`, `renamed`, `message` | -| 10 | `DistributedActors.swift` | `distributed actor`, `distributed func`, `nonisolated distributed`, `ActorSystem` typealias | -| 11 | `StringInterpolation.swift` | `ExpressibleByStringInterpolation`, custom `StringInterpolationProtocol` | -| 12 | `NestedGenerics.swift` | Deeply nested generic types + conditional nested types + generic typealias inside generics | -| 13 | `Tuples.swift` | Named/unnamed/nested tuples as parameters, return types, fields | -| 14 | `FunctionTypes.swift` | Higher-order signatures, function references, curried functions, method references | -| 15 | `NestedFunctions.swift` | Function-local functions and local types (`_LXXX` local-symbol mangling) | -| 16 | `MetatypeUsage.swift` | `T.Type`, `Type.self`, `any Proto.Type`, `type(of:)` exposure | -| 17 | `ExistentialAny.swift` | `any Proto`, `any Proto & Sendable`, `[any Proto]`, `(any Proto) -> Void` | -| 18 | `SameTypeRequirements.swift` | `where A == B.Element`, `where A.Element == B.Element`, complex same-type chains | -| 19 | `OptionSetAndRawRepresentable.swift` | `OptionSet` and custom `RawRepresentable` synthesis | -| 20 | `DiamondInheritance.swift` | Protocol diamond inheritance, multi-inheritance PWT layout | -| 21 | `WeakUnownedReferences.swift` | `weak`, `unowned`, `unowned(safe)`, `unowned(unsafe)` | -| 22 | `ErrorTypes.swift` | Error enums + `LocalizedError` + `CustomNSError` + `Sendable` Error | -| 23 | `ResultBuilderDSL.swift` | Full result builder (`buildBlock`, `buildOptional`, `buildEither(first:)`, `buildEither(second:)`, `buildArray`, `buildIf`, `buildLimitedAvailability`, `buildFinalResult`, `buildExpression`) | -| 24 | `RethrowingFunctions.swift` | `rethrows`, `async rethrows`, conditional-throws combinations | - -### Category 2 — Extended Swift Features (files 25–36) - -| # | File | Produced mangling / metadata | -|---|------|------------------------------| -| 25 | `ProtocolComposition.swift` | `A & B & C`, `AnyObject & Proto`, `Sendable & Proto` composition types | -| 26 | `OverloadedMembers.swift` | Same-name different-signature methods, subscripts, initializers | -| 27 | `UnsafePointers.swift` | `UnsafePointer`, `UnsafeMutablePointer`, `OpaquePointer`, `Unmanaged`, `AutoreleasingUnsafeMutablePointer` | -| 28 | `AsyncSequence.swift` | Custom `AsyncSequence` and `AsyncIteratorProtocol` implementations | -| 29 | `PropertyWrapperVariants.swift` | Property wrapper with `projectedValue`, with `init()`, with `static subscript` | -| 30 | `CustomLiterals.swift` | `ExpressibleByIntegerLiteral`, `ByStringLiteral`, `ByArrayLiteral`, `ByDictionaryLiteral` | -| 31 | `StaticMembers.swift` | `static` vs `class` members, static subscripts, static stored/computed | -| 32 | `ClassBoundGenerics.swift` | `T: AnyObject`, `where T: AnyObject & Proto`, complete class-bound combinations | -| 33 | `MarkerProtocols.swift` | Protocols with no requirements (marker protocol style) | -| 34 | `DependentTypeAccess.swift` | `T.Element.Index`, `Self.Iterator.Element`, deeply-chained dependent type access | -| 35 | `DeinitVariants.swift` | `class`/`actor` `deinit`, `isolated deinit` (Swift 6.0+) | -| 36 | `CollectionConformances.swift` | Custom `Collection`, `Sequence`, `BidirectionalCollection` implementations | - -### Category 3 — Binary Metadata Variants (files 37–44) - -These files are specifically designed to exercise `__swift5_*` section parsing shapes consumed by MachOSwiftSection. - -| # | File | Target section / descriptor | -|---|------|------------------------------| -| 37 | `FieldDescriptorVariants.swift` | `__swift5_fieldmd` — `var`/`let`/`weak`/`unowned` fields, mangled-type-name variants, generic payload fields | -| 38 | `GenericRequirementVariants.swift` | `TargetGenericRequirementDescriptor` — full coverage of `Protocol` / `SameType` / `BaseClass` / `Layout` / `SameConformance` / `SameShape` / `InvertibleProtocol` requirement kinds (the last via `~Copyable` / `~Escapable`) | -| 39 | `VTableEntryVariants.swift` | Class `VTableDescriptorHeader` — virtual / override / final / async / throws / mutating entry flags | -| 40 | `ConditionalConformanceVariants.swift` | `__swift5_proto` with conditional requirement table — multi-constraint witness-table patterns | -| 41 | `DefaultImplementationVariants.swift` | `__swift5_protos` default-implementation extensions — constrained (`where Self:`) default implementations | -| 42 | `FrozenResilienceContrast.swift` | `TargetTypeContextDescriptorFlags` — identical field layouts as `@frozen` vs default resilient, to contrast descriptor shape | -| 43 | `AssociatedTypeWitnessPatterns.swift` | `__swift5_assocty` — 5 witness patterns: concrete type, nested type, typealias, recursive, dependent-on-another-AT | -| 44 | `BuiltinTypeFields.swift` | `__swift5_builtin` (indirect) — fields of `Int`/`Float`/`Double`/`Bool`/`UInt8`/`Int64`/tuples | - -## Edits to Existing Files (not new files) - -| File | Additions | -|------|-----------| -| `Classes.swift` | `required init`, method default arguments, `class func` | -| `Enums.swift` | Large `@frozen` enum, generic payload enum, case-as-function-reference | -| `FunctionFeatures.swift` | `@MainActor` closure parameter, function default arguments | -| `Protocols.swift` | `where Self:` constraint on requirement, multi primary-associated-type protocol | - -## File Template / Style - -All new files follow the existing namespace-enum style: - -```swift -import Foundation // only when needed - -public enum Feature { - // Types nested inside the namespace - public struct SomeTest { - public var field: Int - public init(field: Int) { self.field = field } - } -} -``` - -- **Always use** `public enum ` as the outer namespace. -- **Never** introduce top-level types outside the namespace enum (to avoid colliding with existing top-level `TestsValues` in `BasicTypes.swift`). -- **Always** use full descriptive variable names (per project coding standard). -- **Always** use `public` for every nested declaration (so descriptors emit to the binary). -- **Do not** import modules other than `Foundation` / `Distributed` / `Observation` unless required for a specific binding. -- **Do not** add initial-value logic with side effects; prefer `fatalError()` where runtime behavior is irrelevant (the fixture is never run). - -## Build Validation - -After writing all files, the fixture must be rebuilt so downstream tests observe the new symbols: - -```bash -xcodebuild \ - -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj \ - -scheme SymbolTests \ - -configuration Release \ - -derivedDataPath Tests/Projects/SymbolTests/DerivedData \ - build 2>&1 | xcsift -``` - -Success criterion: the Xcode build succeeds, and `swift test --filter SymbolTestsCoreE2ETests` still passes without regression. A single failing compile in a new file should not cascade to block the rest — each file is independent and can be fixed in isolation. - -## Risks and Mitigations - -1. **Compiler-version-specific syntax.** `distributed actor` requires `import Distributed`; `isolated deinit` requires Swift 6.0+. Mitigation: target Swift 6.2 explicitly per `CLAUDE.md`; if any syntax proves unsupported, omit that specific construct from the file and document it in a comment. -2. **Name collisions.** A new type with the same name as an existing type in another namespace could confuse indexer tests that use `hasSuffix` lookups. Mitigation: prefix new type names with their feature namespace where ambiguity is possible (e.g., `KeyPathTest` not `Test`). -3. **Symbol-free declarations.** Some Swift constructs (local type aliases inside function bodies, purely-generic phantom types) may not appear in `__swift5_*` sections. Mitigation: each new file exposes at least one top-level `public` declaration guaranteed to emit a type descriptor. -4. **Excessive file growth.** 44 new files is a large diff. Mitigation: each file is self-contained and ~20–60 lines; the total line addition is bounded at ~2000 lines. -5. **Existing assertions breakage.** Some E2E tests check that certain type names are present, but none check that the total count matches a specific number. Mitigation: verify by running the full `SwiftInterfaceTests` suite after the fixture rebuild. diff --git a/docs/superpowers/specs/2026-04-18-ci-test-filter-design.md b/docs/superpowers/specs/2026-04-18-ci-test-filter-design.md deleted file mode 100644 index 2fe8e54c..00000000 --- a/docs/superpowers/specs/2026-04-18-ci-test-filter-design.md +++ /dev/null @@ -1,162 +0,0 @@ -# CI Test Filter and Environment Upgrade - -**Date:** 2026-04-18 -**Status:** Approved, pending implementation - -## Problem - -The GitHub Actions `macOS` workflow runs `swift test` against the entire test -suite. Most test classes rely on resources that only exist on a developer -machine — installed Xcode frameworks (`/Applications/Xcode.app/...`), iOS -Simulator runtimes, the on-device dyld shared cache, runtime-loaded images, and -user applications. On a CI runner these paths do not exist, so those tests -either fail or are meaningless. Running them wastes time and masks real -signal. - -There are also tests that can run on CI but require a build artifact that is -not in git: `SymbolTestsCore.framework`. It is produced by the Xcode project -at `Tests/Projects/SymbolTests/SymbolTests.xcodeproj`, and its `DerivedData` -directory is listed in `.gitignore`. The current workflow does not build this -artifact, so even the self-contained fixture tests can't run. - -Separately, the workflow pins `macos-15` + Xcode `16.3`. Project requirements -have moved to macOS 26.2 + Xcode 26.4. - -## Goals - -- CI runs only the test classes that work without a developer-machine - environment. -- CI builds the `SymbolTestsCore.framework` fixture before running tests. -- CI and the release workflow both run on `macos-26` + Xcode `26.4`. -- No changes to test source files — the filter lives in CI configuration only. - -## Non-Goals - -- Introducing runtime flags, tags, or `.disabled(if:)` traits in test code. -- Changing which tests exist or how they're organised. -- Making the blocked tests runnable in CI (e.g. vendoring dyld caches, shipping - Xcode frameworks). They remain developer-only. -- Checking `SymbolTestsCore.framework` into git. - -## Scope: Tests that run in CI (whitelist) - -Exactly five test classes run in CI. Four of them read from -`SymbolTestsCore.framework`; the fifth (`SymbolTestsCoreCoverageInvariantTests`) -is a pure-Swift invariant that walks the `SymbolTestsCore/*.swift` source -files and confirms `SymbolTestsCoreDumpSnapshotTests` has a per-category -`@Test` method for each of them: - -| Test class | File | Category | -|---|---|---| -| `SymbolTestsCoreDumpSnapshotTests` | `Tests/SwiftDumpTests/Snapshots/SymbolTestsCoreDumpSnapshotTests.swift` | Dump snapshot | -| `SymbolTestsCoreInterfaceSnapshotTests` | `Tests/SwiftInterfaceTests/Snapshots/SymbolTestsCoreInterfaceSnapshotTests.swift` | Interface snapshot | -| `SymbolTestsCoreCoverageInvariantTests` | `Tests/SwiftDumpTests/Snapshots/SymbolTestsCoreCoverageInvariantTests.swift` | Coverage invariant | -| `STCoreE2ETests` | `Tests/SwiftInterfaceTests/SymbolTestsCoreE2ETests.swift` | Fixture E2E | -| `STCoreTests` | `Tests/SwiftInterfaceTests/SymbolTestsCoreIntegrationTests.swift` | Fixture integration | - -Every other test class — whether it is a "dump" test, a `DyldCache*` / -`Xcode*` snapshot, a `MachOSwiftSectionTests` unit test, or anything in -`MachOSymbolsTests` / `TypeIndexingTests` / `SwiftInspectionTests` — is -excluded from CI runs. Those tests remain fully functional locally. - -## Design - -### 1. Build the fixture - -The `Build SymbolTestsCore fixture` step is already present on `main` (along -with `Resolve SPM dependencies`, `Cache SymbolTests DerivedData`, and -`Upload xcodebuild logs on failure`). It uses `-derivedDataPath -Tests/Projects/SymbolTests/DerivedData`, which matches the path -`MachOFileName.SymbolTestsCore` (in -`Sources/MachOTestingSupport/MachOFileName.swift`) resolves: - -``` -../../Tests/Projects/SymbolTests/DerivedData/SymbolTests/Build/Products/Release/SymbolTestsCore.framework/Versions/A/SymbolTestsCore -``` - -No change is needed for fixture construction. - -### 2. Filter test runs - -Pass a single combined regex to `swift test --filter`: - -``` -\.(SymbolTestsCoreDumpSnapshotTests|SymbolTestsCoreInterfaceSnapshotTests|SymbolTestsCoreCoverageInvariantTests|STCoreE2ETests|STCoreTests)(/|$) -``` - -- Leading `\.` anchors against the module prefix (`SwiftDumpTests.`, - `SwiftInterfaceTests.`) so the names can't match module-less substrings. -- Trailing `(/|$)` uses a word-boundary-style anchor so `STCoreTests` does - **not** also match `STCoreE2ETests`, and `SymbolTestsCoreDumpSnapshotTests` - does not also accidentally match a hypothetical - `SymbolTestsCoreDumpSnapshotTestsExtra`. - -Both Debug and Release `swift test` invocations receive this filter. - -### 3. Environment upgrade - -Both workflows move to macOS 26 (`macos-26` runner image) + Xcode 26.4. - -`.github/workflows/macOS.yml`: - -- `matrix.os`: `macos-15` → `macos-26` -- `matrix.xcode-version`: `"16.3"` → `"26.4"` - -`.github/workflows/release.yml`: - -- `runs-on`: `macos-15` → `macos-26` -- `Setup Xcode` with xcode-version `"16.3"` → `"26.4"` - -**Note on Xcode version:** The original requirement was Xcode 26.2, but -26.2 ships Swift 6.2.3, whose compiler emits a `.swiftinterface` -containing `nonisolated(nonsending)` syntax (auto-enabled by the macOS -26 SDK's default upcoming features) that the same compiler then refuses -to verify. Xcode 26.4 / Swift 6.3 fixed this. The bump was the smallest -change that lets the fixture build on CI. - -## Final workflow shape - -The `macOS.yml` workflow keeps its existing structure (Resolve SPM, -Cache DerivedData, Build SymbolTestsCore fixture, Upload logs on failure, -Build and run tests). Only three lines change: - -- `matrix.os: macos-15` → `matrix.os: macos-26` -- `matrix.xcode-version: "16.3"` → `matrix.xcode-version: "26.4"` -- Both `swift test` invocations gain a single `--filter` argument: - `'\.(SymbolTestsCoreDumpSnapshotTests|SymbolTestsCoreInterfaceSnapshotTests|SymbolTestsCoreCoverageInvariantTests|STCoreE2ETests|STCoreTests)(/|$)'` - -`release.yml` keeps its existing shape, with only the runner and Xcode -version bumped: - -- `runs-on: macos-15` → `runs-on: macos-26` -- `xcode-version: "16.3"` → `xcode-version: "26.4"` - -## Trade-offs - -- **Whitelist maintenance.** New test classes that belong in CI must be added - to the regex. The alternative (a blacklist of environment-dependent tests) - would drift the opposite way — new tests get included by accident. The - whitelist matches the intent ("CI only runs reproducible fixture-based - tests") more directly. -- **`xcodebuild` adds wall-clock time.** Expected ~20-40 seconds for a single - Release build of the small fixture framework. Acceptable given it's a - one-time per-run cost and the alternative (checking in the binary) adds - other problems. -- **Debug and Release both run.** Preserved from the existing workflow. If - compile time on `macos-26` becomes a concern later, dropping to Debug-only - is a one-line change. -- **Regex vs. multiple `--filter` flags.** A single regex with an anchored - word boundary is more compact and avoids the `STCoreTests` ⊂ `STCoreE2ETests` - substring pitfall that separate `--filter` flags would have. - -## Verification - -After the workflow change lands, a CI run on the next PR should: - -1. Successfully complete the `Build SymbolTestsCore fixture` step and leave a - framework at - `Tests/Projects/SymbolTests/DerivedData/SymbolTests/Build/Products/Release/SymbolTestsCore.framework`. -2. Run exactly the five whitelisted test classes (visible in the test log), - one invocation per config (Debug + Release). -3. Not attempt `DyldCache*`, `Xcode*`, `MachOImage*`, or `*DumpTests` (non- - Snapshot) classes. diff --git a/docs/superpowers/specs/2026-05-02-generic-specializer-cleanup-design.md b/docs/superpowers/specs/2026-05-02-generic-specializer-cleanup-design.md deleted file mode 100644 index 03b6991d..00000000 --- a/docs/superpowers/specs/2026-05-02-generic-specializer-cleanup-design.md +++ /dev/null @@ -1,304 +0,0 @@ -# GenericSpecializer Cleanup and API Polish - -**Date:** 2026-05-02 -**Status:** Approved, pending implementation -**Branch:** `feature/generic-specializer` - -## Problem - -`GenericSpecializer`'s main path — Type generic parameters, direct protocol -constraints, and multi-level associated-type witness tables — is functional -and covered by `Tests/SwiftInterfaceTests/GenericSpecializationTests.swift`. -A diff against the Swift Runtime ABI nevertheless surfaces several lightweight -gaps that hurt API quality without affecting current passing tests: - -- `~Copyable` / `~Escapable` parameter capability declarations - (`GenericRequirementKind.invertedProtocols`) are silently dropped: - `buildRequirement` in - `Sources/SwiftInterface/GenericSpecializer/GenericSpecializer.swift:265-268` - returns `nil` for `.sameConformance`, `.sameShape`, and `.invertedProtocols` - in one combined branch, so callers cannot tell whether a parameter allows - non-Copyable types. -- `GenericContext` reads - `conditionalInvertibleProtocolsRequirements` - (`Sources/MachOSwiftSection/Models/Generic/GenericContext.swift:30`) but - `GenericSpecializer.makeRequest` only consults `genericContext.allRequirements`, - so any conditional requirement stored under that header is invisible to the - specializer. -- A private helper `convertLayoutKind` - (`GenericSpecializer.swift:272-277`) has no callers. -- `MetadataRequest` is hard-coded to `.completeAndBlocking` at - `GenericSpecializer.swift:458`, leaving callers no way to request - `.complete` or `.abstract` when needed (e.g. to avoid blocking inside - recursive specialization). -- A generic `Candidate` (e.g. `Array`, `Optional`) cannot be resolved by - `resolveCandidate` — line 516 calls - `accessorFunction(request: .completeAndBlocking)` with no arguments, which - is correct only for non-generic types. The resulting failure surfaces as a - generic "Cannot get metadata accessor function" message, not as an - actionable error directing the caller toward - `Argument.specialized(...)` or nested specialization. -- The combined `case .sameConformance, .sameShape, .invertedProtocols` - branch reads as "everything we don't support yet", but the three kinds have - very different reasons for being skipped. The single-line comment makes - future maintenance harder. - -This document scopes a self-contained cleanup that surfaces missing -information, removes dead code, and improves error and request signatures. - -## Goals - -- Surface `~Copyable` / `~Escapable` parameter capability information on - `SpecializationRequest.Parameter`. -- Include `conditionalInvertibleProtocolsRequirements` in the requirement set - consumed by the specializer. -- Split `sameConformance` / `sameShape` / `invertedProtocols` into individual - `case` arms with intent-revealing comments. -- Let callers pass a `MetadataRequest` to `specialize(...)`. -- Detect generic candidates eagerly in `resolveCandidate` and throw a typed, - actionable error. -- Remove the unused `convertLayoutKind` helper. - -## Non-Goals - -- Variadic generic parameters (`each T`, `GenericParamKind.typePack`). -- Value generic parameters (`let N: Int`, `GenericParamKind.value`). -- `isPackRequirement` / `isValueRequirement` flag handling. -- `validate(...)` substantive validation (typed errors for `.protocol`, - `.sameType`, `.baseClass`, `.layout`); tracked separately. -- PWT caching across `RuntimeFunctions.conformsToProtocol` calls; tracked - separately. -- Querying whether a candidate type itself conforms to `Copyable` / - `Escapable` (would require new indexer surface). -- Full nested-candidate specialization (would require `Candidate` to carry - sub-arguments). The fail-fast in this spec is the prerequisite for that - future work. - -## Design - -### 1. Surface inverted protocols on `Parameter` (#5) - -`SpecializationRequest.Parameter` gains one optional field: - -```swift -public struct Parameter: Sendable { - // existing fields... - public let invertibleProtocols: InvertibleProtocolSet? -} -``` - -The set carries the **bits that ARE present** in the type (e.g. a parameter -declared `` produces a set that does **not** contain -`.copyable`). `nil` means the parameter has no `invertedProtocols` -requirement at all (i.e. it is a normal parameter that retains every -invertible protocol by default). - -Population: at the end of `buildParameters`, iterate the merged requirement -list a second time and pick out `kind == .invertedProtocols`. Each such -requirement carries `genericParamIndex: UInt16` and an -`InvertibleProtocolSet`. Match the index against the parameter's flat depth/ -index pair and write the set onto the corresponding `Parameter`. If multiple -inverted requirements target the same parameter (theoretically possible -across enclosing contexts), intersect the sets. - -`Requirement` enum is **not** changed. The existing -`case .invertedProtocols` branch in `buildRequirement` keeps returning `nil` -but with a comment explaining that the information is surfaced one level up. - -### 2. Merge conditional invertible protocol requirements (#6) - -Introduce a single private helper: - -```swift -private static func mergedRequirements(from genericContext: GenericContext) - -> [GenericRequirementDescriptor] -{ - genericContext.allRequirements.flatMap { $0 } - + genericContext.conditionalInvertibleProtocolsRequirements -} -``` - -Replace the three current call sites in `GenericSpecializer.swift` that read -`genericContext.allRequirements.flatMap { $0 }`: - -- `buildParameters` (line 94) -- `buildAssociatedTypeRequirements` (line 285) -- `resolveAssociatedTypeWitnesses` (line 593, via - `genericContextInProcess.requirements`) - -The third call site lives on the `MachO == MachOImage` extension and reads -`genericContextInProcess.requirements` (a flat array, not nested). To stay -consistent we apply the same merge inline: - -```swift -let mergedDescriptors = genericContextInProcess.requirements - + genericContextInProcess.conditionalInvertibleProtocolsRequirements -let requirements = try mergedDescriptors.map { - try GenericRequirement(descriptor: $0) -} -``` - -This treats every conditional requirement as active. The current scope rules -out non-Copyable / non-Escapable candidates (#1-#4 are out of scope), so all -candidates retain Copyable / Escapable by default and the conditional -predicates always evaluate true. When future work introduces non-default -candidates, the merge can be made conditional on the candidate's invertible -set. - -### 3. Split `.sameConformance` / `.sameShape` / `.invertedProtocols` (#7) - -Replace -`GenericSpecializer.swift:265-268`: - -```swift -case .sameConformance, .sameShape, .invertedProtocols: - // These are more advanced requirements that we don't need for basic specialization - return nil -``` - -with three independent arms: - -```swift -case .sameConformance: - // Derived from SameType / BaseClass; compiler forces hasKeyArgument = false, - // so it never participates in metadata accessor key arguments. - return nil - -case .sameShape: - // Pack-shape constraint between two TypePacks. Relevant only to variadic - // generics, which are out of scope for this specializer. - return nil - -case .invertedProtocols: - // Capability declaration (~Copyable / ~Escapable) — surfaced one level up - // on Parameter.invertibleProtocols rather than as a Requirement, because - // it relaxes rather than constrains the parameter. - return nil -``` - -No behavioural change. - -### 4. Generic candidate fail-fast (#9) - -`SpecializationRequest.Candidate` gains a `Bool` field: - -```swift -public struct Candidate: Sendable, Hashable { - public let typeName: TypeName - public let source: Source - public let isGeneric: Bool -} -``` - -`findCandidates` populates `isGeneric` by reading -`typeDefinition.type.typeContextDescriptorWrapper.typeContextDescriptor.flags.isGeneric` -(provided by `ContextDescriptorFlags` at -`Sources/MachOSwiftSection/Models/ContextDescriptor/ContextDescriptorFlags.swift:65`). -The field is informational — it lets callers gray-out generic candidates in -UI before attempting to use them. - -`GenericSpecializer.SpecializerError` gains: - -```swift -case candidateRequiresNestedSpecialization( - candidate: SpecializationRequest.Candidate, - parameterCount: Int -) -``` - -`resolveCandidate` checks -`try descriptor.genericContext(in: typeDefinitionEntry.machO) != nil` before -calling `metadataAccessorFunction`. If the candidate is generic, it throws -`candidateRequiresNestedSpecialization` carrying the candidate and the count -of generic parameters from the descriptor's generic context header. The -existing fall-through to a no-argument `accessorFunction(request:)` call is -removed for generic candidates. - -`parameterCount` lets callers preallocate UI for the nested selection step. - -### 5. Configurable `MetadataRequest` on `specialize` (#10) - -`specialize` signature changes: - -```swift -public func specialize( - _ request: SpecializationRequest, - with selection: SpecializationSelection, - metadataRequest: MetadataRequest = .completeAndBlocking -) throws -> SpecializationResult -``` - -The new parameter is forwarded only to the **main** accessor invocation at -`GenericSpecializer.swift:458`. Internal calls keep their original requests: - -- `resolveCandidate`'s `accessorFunction(request: .completeAndBlocking)` - stays — candidate metadata must be complete to be used as a key argument. -- `resolveAssociatedTypeStep`'s `getAssociatedTypeWitness(request: .init(), - ...)` stays — abstract is correct for type-witness extraction. - -This matches the semantics of `swift_getGenericMetadata`'s `request` -parameter: the caller controls only the freshness state of the **returned** -metadata, not transitive runtime calls. - -### 6. Remove dead code (#12) - -Delete `convertLayoutKind` at `GenericSpecializer.swift:272-277`. No callers. - -## Testing - -All new tests live in -`Tests/SwiftInterfaceTests/GenericSpecializationTests.swift`. The other four -items (#6 merge, #7 comments, #10 default parameter, #12 dead code) are -behaviour-preserving refactors covered by the existing test suite. - -### Inverted protocols exposure (#5) - -```swift -struct TestNonCopyableStruct { let a: A } -``` - -Tests: - -- `request.parameters[0].invertibleProtocols` is non-`nil`. -- The set does **not** contain `.copyable`. -- `specialize` with `A = Int` (a Copyable type) still succeeds. - -### Generic candidate fail-fast (#9) - -Set up a request whose candidates include a generic standard-library type -(e.g. `Array` against `A: Collection`). Assertions: - -- The matching `Candidate.isGeneric == true`. -- Calling `specialize(request, with: ["A": .candidate(arrayCandidate)])` - throws `candidateRequiresNestedSpecialization`, not - `candidateResolutionFailed`. - -### Configurable `MetadataRequest` (#10) - -Run `TestGenericStruct` specialization with the default request, then -again with `metadataRequest: .complete` (non-blocking). Both runs must -produce identical `fieldOffsets() == [0, 8, 16]`. - -### Conditional invertible requirements (#6) - -If a fixture exposing `conditionalInvertibleProtocolsRequirements` can be -authored within Swift 5.9+ language constraints (e.g. -`struct S: ~Copyable where A: P { ... }`), the test asserts the -resulting `Parameter.requirements` includes the merged conditional entries. -If `~Copyable` placement constraints prevent a minimal example, -this test degrades to an end-to-end specialization that exercises the merge -path without directly inspecting the merged list. - -## Risks and Migration - -- **`Candidate` and `Parameter` shape changes are public API.** Both - structures live under `@_spi(Support)` indirectly via - `SpecializationRequest`, but the new fields are sources-compatible only - for callers that use the synthesised memberwise initialiser positionally. - Existing call sites in tests use named arguments, so the impact is minimal. -- **Conditional merge is unconditional.** As noted in §2, this is correct - for the current candidate set. The merge helper is the natural extension - point when non-default candidates are introduced. -- **No ABI-level change.** No new key arguments are passed; no metadata - accessor invocation order is altered. The main accessor still receives - `[metadatas...] + [witnessTables...]` in the same order as today. diff --git a/docs/superpowers/specs/2026-05-02-reading-context-api-design.md b/docs/superpowers/specs/2026-05-02-reading-context-api-design.md deleted file mode 100644 index 9dcfce42..00000000 --- a/docs/superpowers/specs/2026-05-02-reading-context-api-design.md +++ /dev/null @@ -1,272 +0,0 @@ -# ReadingContext API Coverage for `MachOSwiftSection/Models` - -**Date:** 2026-05-02 -**Status:** Approved, pending implementation -**Branch:** `feature/reading-context-api` (to be created from `main`) - -## Problem - -Today, types and descriptors in -`Sources/MachOSwiftSection/Models/` expose two parallel API families: - -1. **MachO-based**, parameterized over the backing file/image: - ```swift - func foo(in machO: MachO) throws -> X - ``` -2. **InProcess**, no parameters, reading directly through the descriptor's - runtime pointer (`asPointer`): - ```swift - func foo() throws -> X - ``` - -A third family — `ReadingContext`-based — has been introduced incrementally -(see `Sources/MachOReading/ReadingContext/ReadingContext.swift`) and is meant -to be the unified abstraction across the two reading modes: - -```swift -func foo(in context: Context) throws -> X -``` - -Today only ~15 of the ~60 model files that have a MachO API also expose a -ReadingContext API. The remaining ~45 files are silently incomplete: any -caller who already adopts the `ReadingContext` abstraction must drop down to -the MachO/InProcess APIs, defeating the purpose. - -This document scopes a focused completion pass: add the missing -`ReadingContext` overloads across `MachOSwiftSection/Models/`, mirroring the -pattern already established in -`Sources/MachOSwiftSection/Models/Type/TypeContextDescriptorProtocol.swift` -and friends. While doing so, introduce one small protocol extension — -`runtimePointer(at:)` — needed to express runtime-pointer-returning methods -(notably `metadataAccessorFunction`) under the unified abstraction. - -## Goals - -- Provide a `ReadingContext`-based overload for every model method that - currently has a `MachOSwiftSectionRepresentableWithCache` overload, across - `MachOSwiftSection/Models/`. -- Keep behavior identical to the existing implementations: the new overloads - are purely additive surface — no existing call sites change. -- Add a minimal extension (`ReadingContext.runtimePointer(at:)`) so that - methods returning a runtime pointer (`metadataAccessorFunction`, and any - similar metadata pointer methods uncovered during the pass) can be - expressed cleanly without per-call type dispatch. -- Land the work in reviewable, build-passing batches grouped by sub-directory. - -## Non-Goals - -- Touching modules outside `MachOSwiftSection/Models/`. The infrastructure - (`MachOReading`, `MachOPointers`, `MachOResolving`) already exposes - ReadingContext entry points — this work consumes them, it does not change - them — except for the single `runtimePointer(at:)` addition described - below. -- Touching higher-level modules (`SwiftDump`, `SwiftInspection`, - `SwiftInterface`, `swift-section`). They keep using the existing MachO/ - InProcess APIs. A follow-up branch can migrate them later. -- Refactoring the existing MachO or InProcess overloads. They stay as-is. -- Adding new unit tests. The new overloads are mechanical mirrors of - existing, tested code, and the underlying primitives - (`Resolvable.resolve(at:in:)`, `ReadingContext.read*`) are already covered - by their own tests. - -## Design - -### 1. Pattern for the common case - -For every method of the shape: - -```swift -public func foo( - in machO: MachO -) throws -> X { - try layout.field.resolve(from: offset + layout.offset(of: .field), in: machO) -} -``` - -add a sibling overload: - -```swift -public func foo( - in context: Context -) throws -> X { - let address = try context.addressFromOffset(offset + layout.offset(of: .field)) - return try layout.field.resolve(at: address, in: context) -} -``` - -Substitution rules — applied mechanically per call: - -| MachO call | ReadingContext equivalent | -|---|---| -| `machO.readElement(offset: o)` | `context.readElement(at: try context.addressFromOffset(o))` | -| `machO.readWrapperElement(offset: o)` | `context.readWrapperElement(at: try context.addressFromOffset(o))` | -| `machO.readElements(offset: o, numberOfElements: n)` | `context.readElements(at: try context.addressFromOffset(o), numberOfElements: n)` | -| `machO.readWrapperElements(offset: o, numberOfElements: n)` | `context.readWrapperElements(at: try context.addressFromOffset(o), numberOfElements: n)` | -| `machO.readString(offset: o)` | `context.readString(at: try context.addressFromOffset(o))` | -| `pointer.resolve(from: o, in: machO)` | `pointer.resolve(at: try context.addressFromOffset(o), in: context)` | -| Recursive `someMethod(in: machO)` | `someMethod(in: context)` | - -Local offset arithmetic on `Int` (`currentOffset.offset(of:)`, -`currentOffset.align(to:)`, `currentOffset += ...`) stays unchanged: the -final translation to a context-specific address happens at the read site. - -Each new overload sits in a `// MARK: - ReadingContext Support` section -inside the file. Existing MachO/InProcess code is left untouched. - -### 2. Extension to support runtime-pointer methods - -Some methods return a runtime function pointer -(`metadataAccessorFunction` is the canonical example) and only make sense -when the underlying reader is mapped into the current process. The MachO -overload special-cases `MachO is MachOImage`; the InProcess overload uses -`asPointer`. - -To express this under the unified abstraction without per-call -`as?` dispatch, add a single optional capability to the protocol: - -```swift -extension ReadingContext { - /// Converts a context-specific address to a runtime `UnsafeRawPointer`, - /// when the context is mapped into the current process. - /// - /// - `InProcessContext`: returns the address itself (already a pointer). - /// - `MachOContext`: returns `machO.ptr + address`. - /// - `MachOContext` / other readers: returns `nil`. - public func runtimePointer(at address: Address) throws -> UnsafeRawPointer? { - nil - } -} -``` - -`InProcessContext` and `MachOContext` provide concrete overrides: - -```swift -extension InProcessContext { - public func runtimePointer(at address: UnsafeRawPointer) throws -> UnsafeRawPointer? { - address - } -} - -extension MachOContext { - public func runtimePointer(at address: Int) throws -> UnsafeRawPointer? { - if let machOImage = machO as? MachOImage { - return machOImage.ptr + UnsafeRawPointer.Stride(address) - } - return nil - } -} -``` - -The runtime `as?` cast inside `MachOContext` is unavoidable because the -generic parameter `MachO` is unconstrained at the type level; specializing -the extension with `where MachO == MachOImage` would not produce a witness -for the `MachOContext: ReadingContext` conformance because the -witness is bound at the unconstrained conformance site. - -`runtimePointer(at:)` is **not** added as a `requirement` of the -`ReadingContext` protocol — adding it as an extension method with a default -implementation keeps the change non-breaking for any external conformer. - -With this in place, runtime-pointer methods translate cleanly: - -```swift -public func metadataAccessorFunction( - in context: Context -) throws -> MetadataAccessorFunction? { - let fieldAddress = try context.addressFromOffset(offset + layout.offset(of: .accessFunctionPtr)) - let relativeOffset: Int32 = try context.readElement(at: fieldAddress) - let targetAddress = context.advanceAddress(fieldAddress, by: Int(relativeOffset)) - return try context.runtimePointer(at: targetAddress).map { MetadataAccessorFunction(ptr: $0) } -} -``` - -For `MachOContext` this returns `nil`, matching today's MachO -overload. For `InProcessContext` and `MachOContext` it returns -the function pointer, matching the InProcess overload's behavior. - -The implementation pass will identify the small set of similar -runtime-pointer-returning methods (likely confined to a few metadata -descriptors) and apply the same pattern. - -### 3. Files in scope - -The 45 files needing new overloads live in these sub-directories of -`Sources/MachOSwiftSection/Models/`: - -- `Anonymous/`, `Module/`, `Extension/` -- `ContextDescriptor/` (`ContextProtocol.swift`, `ContextWrapper.swift`) -- `Type/Class/` (descriptor, methods, metadata protocols) -- `Type/Enum/`, `Type/Struct/` -- `Type/` root (`TypeContextDescriptor.swift`, `TypeContextWrapper.swift`, - `TypeReference.swift`, `ValueMetadataProtocol.swift`) -- `Protocol/`, `ProtocolConformance/` -- `Generic/` (`GenericRequirement.swift` — the descriptor already has it) -- `FieldDescriptor/`, `FieldRecord/`, `AssociatedType/` -- `Metadata/` (protocols and wrappers) -- `ExistentialType/`, `ForeignType/`, `TupleType/`, `OpaqueType/`, - `BuiltinType/` - -The complete list is the output of: - -```sh -grep -rL "ReadingContext" $(grep -rl "MachOSwiftSectionRepresentableWithCache" \ - Sources/MachOSwiftSection/Models) -``` - -at the start of the implementation. The implementation plan will pin a -verified file list per batch. - -### 4. Batch plan (commit grouping) - -Each batch is a single commit and must build cleanly (`swift build`) before -moving on. Batches are grouped to keep diffs cohesive and reviewable: - -1. **Reading-context infrastructure** — add `runtimePointer(at:)` extension - in `MachOReading` (no model file changes yet). -2. `Anonymous/`, `Module/`, `Extension/` (simple context wrappers). -3. `ContextDescriptor/` (`ContextProtocol.swift`, `ContextWrapper.swift`). -4. `Type/Class/*` (descriptor, methods, metadata protocols). -5. `Type/Enum/*`, `Type/Struct/*`. -6. `Type/` root files (descriptor, wrapper, references, metadata - protocols), including `metadataAccessorFunction` overload using - `runtimePointer(at:)`. -7. `Protocol/`, `ProtocolConformance/`. -8. `Generic/` (`GenericRequirement.swift`), - `FieldDescriptor/`, `FieldRecord/`, `AssociatedType/`. -9. `Metadata/` (protocols, wrappers). -10. `ExistentialType/`, `ForeignType/`, `TupleType/`, `OpaqueType/`, - `BuiltinType/`. - -If any batch turns out larger or smaller than expected, the plan can -re-balance — the only invariant is "one batch = one passing build". - -## Validation - -- `swift package update && swift build` after each batch. -- `swift test` once at the end of the work, to confirm the existing - `MachOSwiftSectionTests`, `SwiftDumpTests`, and `SwiftInterfaceTests` - suites still pass (they exercise the underlying MachO/InProcess paths - that the new overloads reduce to). -- Spot check: pick one or two of the new overloads (e.g. - `TypeContextDescriptorProtocol.fieldDescriptor(in:)` once added) and - confirm they delegate to the same primitives as the existing MachO - overload by reading the diff. - -## Risks and mitigations - -- **Risk:** A method's MachO overload has subtle behavior (e.g. early - return on a bind/rebase resolver, special-case for `MachOImage`) that the - mechanical translation skips. - - *Mitigation:* During each batch, read the existing overload end-to-end - before mirroring it. Anything that does not fit the substitution table - above gets a per-method note in the commit message. -- **Risk:** `runtimePointer(at:)` extension default returns `nil` and a - caller silently loses functionality for `MachOContext`. - - *Mitigation:* This matches today's behavior — the existing - `metadataAccessorFunction(in: MachO)` already returns `nil` for - `MachOFile`. Documented explicitly on the extension. -- **Risk:** A future contributor adds a new `ReadingContext` conformer and - expects `runtimePointer(at:)` to be a requirement. - - *Mitigation:* The doc comment on the extension states the contract; - making it an extension (not a requirement) is intentional to avoid a - breaking change. diff --git a/docs/superpowers/specs/2026-05-03-machoswift-section-fixture-tests-design.md b/docs/superpowers/specs/2026-05-03-machoswift-section-fixture-tests-design.md deleted file mode 100644 index 39008b9f..00000000 --- a/docs/superpowers/specs/2026-05-03-machoswift-section-fixture-tests-design.md +++ /dev/null @@ -1,550 +0,0 @@ -# MachOSwiftSection Fixture-Based Test Coverage Design - -**日期:** 2026-05-03 -**状态:** 待实施 -**分支:** `feature/machoswift-section-fixture-tests`(从 `feature/reading-context-api` 拉,因新测试要覆盖 ReadingContext API) - -## 问题 - -`Sources/MachOSwiftSection/Models/` 下有 **287 个 public func**、**781 个 public 成员**、分布于 **24 个子目录、约 60 个文件**。这些方法构成了把 Mach-O 二进制中 Swift 元数据节(`__swift5_types`、`__swift5_proto` 等)解析成 Swift 模型的全部入口。 - -现状: - -- **`Tests/MachOSwiftSectionTests/`** 只有 9 个测试文件,大多 ad-hoc 风格,基于系统 framework(SwiftUI、dyld shared cache)而非可控 fixture,且没有"哪些方法被覆盖、哪些没被覆盖"的客观标准。 -- 最近刚加完的 ReadingContext API(每个 method 多了一个 `(in: Context)` 重载)更需要回归保护——目前没有一个测试断言"三家 reader 在同一 fixture 上返回相同结果"。 -- `SwiftDumpTests` 已经建立了 fixture-based 范式(`SymbolTestsCoreDumpSnapshotTests` + `SymbolTestsCoreCoverageInvariantTests`),但守的是更高层 SwiftDump 输出,无法替代对 MachOSwiftSection reader API 直接的 ABI 级断言。 - -本设计建立一套 fixture-based 测试体系,达成: - -- 每一个 `Sources/MachOSwiftSection/Models/**` 下的 public func/var/init **被至少一个 `@Test` 覆盖**; -- 每个被覆盖方法做 **跨 reader 一致性断言**(MachOFile/MachOImage/InProcess + 三家对应 ReadingContext); -- 每个被覆盖方法做 **完整 ABI 数值层硬编码断言**(offset、size、flags、count、name 等); -- 新增 public method 不写测试 → `swift test` 红; -- baseline 数据通过 generator 一次性生成、人工 review 后冻结进 git。 - -## 目标 - -1. 为 `Sources/MachOSwiftSection/Models/` 下所有 public 入口建立 fixture-based `@Test`,镜像源码目录结构。 -2. 引入 `MachOSwiftSectionFixtureTests` 基类,持有同一份 `SymbolTestsCore.framework` 的三种视图(`MachOFile`、`MachOImage`、`InProcessContext`)。 -3. 每个 `@Test` 同时执行**跨 reader 一致性断言**(三家 reader + 三家 ReadingContext)与**ABI baseline 字面量断言**。 -4. 提供 `baseline-generator` executable,从 fixture 自动生成 baseline 期望值,产出可读 Swift 代码,commit 进 git。 -5. 提供 `MachOSwiftSectionCoverageInvariantTests` 守护测试,基于源码静态扫描确保覆盖完整。 -6. 失败信息要 actionable,能直接告诉作者要新增/修改哪个 `@Test` 或 baseline。 - -## 非目标 - -- **扩展 fixture 内容**:`SymbolTestsCore.framework`(54 个 .swift 文件)已经覆盖大多数 Swift 语法元素,本期不增加新 fixture 文件。如某 model type 在 fixture 内找不到合适样本,先入 `CoverageAllowlist` 标 `needs fixture extension`,留 future work。 -- **测试 MachOSwiftSection 之外的模块**:SwiftDump/SwiftInspection/SwiftInterface/TypeIndexing 都已有(或不在范围)各自的测试套件,本期仅聚焦 `MachOSwiftSection`。 -- **性能 benchmark**:仅做正确性,不做性能基线。 -- **fixture 自动构建**:沿用 `xcodebuild -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj -scheme SymbolTestsCore` 手动构建,DerivedData/ 已经 commit 在仓库内。 -- **重写或合并现有测试**:`LayoutTests`、`AssociatedTypeTests`、`MetadataAccessorTests` 等保留,作为补充,不冲突。 - -## 设计 - -### 1. 整体架构 - -设计由四个支柱组成: - -``` -fixture.framework (SymbolTestsCore) - │ - ├──[disk]──── MachOFile ──┐ - ├──[dlopen]── MachOImage ─┼──→ 3 个 ReadingContext (file/image/inprocess) ──→ Tests - └──[ptr]───── InProcess ──┘ - │ - ├──→ ① cross-reader equality #expect (Suite 内自动) - └──→ ② ABI baseline literal #expect (引用 baseline) - │ - └── BaselineGenerator 自动生成 - ↑ - MachOSwiftSectionCoverageInvariantTests 守护 - ──→ 静态扫描 Sources/.../Models/ 找到 expected 名单 - ──→ 反射 Suite registeredTestMethodNames 找到 registered 名单 - ──→ missing/extra 必须为空 -``` - -| 支柱 | 位置 | 职责 | -|---|---|---| -| Fixture 加载层 | `Sources/MachOTestingSupport/MachOSwiftSectionFixtureTests.swift` | 同时持有 fixture 的 MachOFile/MachOImage,以及三家 reader 与三家 ReadingContext | -| Suite 层 | `Tests/MachOSwiftSectionTests/Fixtures/`(镜像 Models/ 24 子目录) | 50 个左右 Suite 文件,每个 `@Test` 对应一个 public method,做"跨 reader 一致性 + baseline 断言" | -| Baseline Generator 层 | `Sources/MachOTestingSupport/Baseline/` + `Sources/baseline-generator/`(executable target) | 一次性运行,从 fixture 生成 `__Baseline__/Baseline.swift` | -| Coverage 守护层 | `Sources/MachOTestingSupport/Coverage/` + `Tests/MachOSwiftSectionTests/Fixtures/MachOSwiftSectionCoverageInvariantTests.swift` | 静态扫描源码 + 反射 Suite,缺漏直接红 | - -与现有测试的关系: - -- `LayoutTests.swift`(纯 Layout offset 计算)→ 保留,继续管 Layout。 -- `AssociatedTypeTests.swift` / `MetadataAccessorTests.swift` 等 ad-hoc 风格 → 保留,作为示例与补充。 -- `SwiftDumpTests/Snapshots/SymbolTestsCoreDumpSnapshotTests` → 不冲突,守的是 SwiftDump 输出,本套件守 MachOSwiftSection reader API。 - -### 2. Test Infrastructure - -#### 2.1 `MachOSwiftSectionFixtureTests` 基类 - -新文件 `Sources/MachOTestingSupport/MachOSwiftSectionFixtureTests.swift`: - -```swift -@MainActor -package class MachOSwiftSectionFixtureTests: Sendable { - package let machOFile: MachOFile - package let machOImage: MachOImage - - package let fileContext: MachOContext - package let imageContext: MachOContext - package let inProcessContext: InProcessContext - - package class var fixtureFileName: MachOFileName { .SymbolTestsCore } - package class var fixtureImageName: MachOImageName { .SymbolTestsCore } - - package init() async throws { - // 1. 磁盘加载(同 MachOFileTests) - let file = try loadFromFile(named: Self.fixtureFileName) - switch file { - case .fat(let fatFile): - self.machOFile = try required( - fatFile.machOFiles().first(where: { $0.header.cpuType == .arm64 }) - ?? fatFile.machOFiles().first - ) - case .machO(let machO): - self.machOFile = machO - @unknown default: - fatalError() - } - - // 2. dlopen 加载到当前进程 - try Self.ensureFixtureLoaded() - self.machOImage = try #require(MachOImage(named: Self.fixtureImageName)) - - // 3. 三家 context - self.fileContext = MachOContext(machO: machOFile) - self.imageContext = MachOContext(machO: machOImage) - self.inProcessContext = InProcessContext() - } - - private static let dlopenOnce: Void = { - // MachOImageName 的 raw value 是相对路径 "../../Tests/...",dlopen 需绝对路径。 - // 用 #filePath 作为 anchor 解析为绝对路径(同 SwiftDumpTests 已有的解析逻辑)。 - let path = resolveFixturePath(MachOImageName.SymbolTestsCore.rawValue) - _ = dlopen(path, RTLD_LAZY) - }() - - private static func ensureFixtureLoaded() throws { - _ = dlopenOnce - guard MachOImage(named: .SymbolTestsCore) != nil else { - throw FixtureLoadError.imageNotFoundAfterDlopen( - path: MachOImageName.SymbolTestsCore.rawValue, - dlerror: String(cString: dlerror() ?? "") - ) - } - } -} - -package enum FixtureLoadError: Error { - case imageNotFoundAfterDlopen(path: String, dlerror: String) -} -``` - -#### 2.2 `MachOImageName.SymbolTestsCore` 枚举条目 - -需要新增,镜像 `MachOFileName.SymbolTestsCore` 的相对路径: - -```swift -extension MachOImageName { - case SymbolTestsCore = "../../Tests/Projects/SymbolTests/DerivedData/SymbolTests/Build/Products/Release/SymbolTestsCore.framework/Versions/A/SymbolTestsCore" - case SymbolTests = "../../Tests/Projects/SymbolTests/DerivedData/SymbolTests/Build/Products/Release/SymbolTests.framework/Versions/A/SymbolTests" -} -``` - -#### 2.3 `acrossAllReaders` helper - -为减少 `@Test` 内重复,提供: - -```swift -extension MachOSwiftSectionFixtureTests { - /// 在 (machOFile, machOImage, inProcess) 三家上分别求值,断言相等,返回唯一值。 - package func acrossAllReaders( - file: () throws -> T, - image: () throws -> T, - inProcess: () throws -> T - ) throws -> T { ... } - - /// 在 (fileContext, imageContext, inProcessContext) 三家 ReadingContext 上分别求值,断言相等。 - package func acrossAllContexts( - file: () throws -> T, - image: () throws -> T, - inProcess: () throws -> T - ) throws -> T { ... } -} -``` - -dlopen 失败采取**抛错**而非 fatalError,让 `@Test` 显示 actionable 信息;`dlerror` 输出到错误。 - -### 3. Suite 结构 - -#### 3.1 文件组织 - -镜像 `Sources/MachOSwiftSection/Models/`: - -``` -Tests/MachOSwiftSectionTests/Fixtures/ -├── Anonymous/ -│ ├── AnonymousContextDescriptorTests.swift -│ └── AnonymousContextTests.swift -├── AssociatedType/... -├── BuiltinType/... -├── ContextDescriptor/... -├── ExistentialType/... -├── Extension/... -├── FieldDescriptor/... -├── FieldRecord/... -├── ForeignType/... -├── Generic/... -├── Metadata/... -├── Module/... -├── OpaqueType/... -├── Protocol/... -├── ProtocolConformance/... -├── TupleType/... -└── Type/ - ├── TypeContextDescriptorTests.swift - ├── TypeContextWrapperTests.swift - ├── TypeReferenceTests.swift - ├── TypeContextDescriptorProtocolTests.swift - ├── ValueMetadataProtocolTests.swift - ├── Class/ - │ ├── ClassTests.swift - │ ├── ClassDescriptorTests.swift - │ ├── AnyClassMetadataProtocolTests.swift - │ ├── ... - │ └── Method/... - ├── Enum/ - │ ├── EnumTests.swift - │ ├── EnumMetadataProtocolTests.swift - │ └── MultiPayloadEnumDescriptorTests.swift - └── Struct/ - ├── StructTests.swift - └── StructMetadataProtocolTests.swift -``` - -约 50 个 Suite 文件。 - -#### 3.2 Suite 模板 - -```swift -@Suite -final class StructDescriptorTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable { - static let testedTypeName = "StructDescriptor" - static let registeredTestMethodNames: Set = [ - "name", - "fields", - "genericContext", - "numberOfFields", - "fieldOffsetVectorOffset", - // ... generator 同步生成 - ] - - private func pickedStruct(in machO: some MachOSwiftSectionRepresentableWithCache) throws -> StructDescriptor { - try #require( - machO.swift.typeContextDescriptors.lazy - .compactMap(\.struct) - .first(where: { try $0.name(in: machO) == "Structs.StructTest" }) - ) - } - - @Test func name() async throws { - let fileSubject = try pickedStruct(in: machOFile) - let imageSubject = try pickedStruct(in: machOImage) - - let fromFile = try fileSubject.name(in: machOFile) - let fromImage = try imageSubject.name(in: machOImage) - let fromInProcess = try imageSubject.asPointerWrapper(in: machOImage).name() - let fromFileCtx = try fileSubject.name(in: fileContext) - let fromImageCtx = try imageSubject.name(in: imageContext) - - // ① cross-reader 一致性 - #expect(fromFile == fromImage) - #expect(fromFile == fromInProcess) - #expect(fromFile == fromFileCtx) - #expect(fromFile == fromImageCtx) - - // ② ABI baseline literal - #expect(fromFile == StructDescriptorBaseline.structTest.name) - } - - // ... 每个 public func/var/init 一个 @Test -} -``` - -约定: - -- `@Suite final class XxxTests: MachOSwiftSectionFixtureTests, FixtureSuite, @unchecked Sendable`,文件名 = `<被测类型>Tests.swift`。 -- 每个 `@Test func` 名 = 被测 member 名。 -- 每个 `@Test` 同时做 ① cross-reader equality(包括 fileContext/imageContext/inProcessContext)和 ② baseline literal。 -- 跨 reader 比对 wrapper 类型时,投影到"语义可比较"字段(string、numeric、array of string),避免 wrapper 内部不可比较的 offset/pointer。 -- **InProcess 重载缺失**:并非每个 method 都有 `()` 形式的 InProcess 重载(部分 model 类型未提供 `asPointerWrapper`,或 InProcess 形式与 MachO 形式不对称)。Suite 模板对没有 InProcess 重载的 method 跳过 `fromInProcess` 一致性断言;对没有 ReadingContext 重载的 method(极少数)同理跳过 context 断言。每个 `@Test` 实际验证哪些 reader 由该 method 在源码中存在的重载决定,plan 阶段会逐 method 列出。 - -#### 3.3 fixture 主测目标策略 - -每个 Suite 选 **1 主 + 2~3 个反差变体**(由 `BaselineFixturePicker` 统一规划): - -- struct:Structs.StructTest(主) + GenericFieldLayout.GenericStructNonRequirement(generic)。 -- class:Classes.ClassTest(主) + DiamondInheritance.DiamondLeaf(继承链) + Classes.ObjCDerivedTest(ObjC interop)。 -- enum:Enums.EnumTest(主) + (single payload) + (multi-payload)。 -- protocol:Protocols.ProtocolTest(主) + AssociatedTypeWitnessPatterns 选 1 关联类型 protocol。 -- 等等。 - -#### 3.4 Baseline 引用形态 - -每个 Suite 配 `Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/Baseline.swift`: - -```swift -// AUTO-GENERATED — DO NOT EDIT. -// Regenerate via: swift run baseline-generator --suite StructDescriptor -// Source fixture: SymbolTestsCore.framework -// Toolchain: Swift 6.2 (swiftlang-6.2.x) -// Generated: 2026-05-03 - -enum StructDescriptorBaseline { - static let registeredTestMethodNames: Set = [ - "name", "fields", "genericContext", - "numberOfFields", "fieldOffsetVectorOffset", - ] - - struct Entry { - let name: String - let numberOfFields: Int - let fieldNames: [String] - let fieldOffsets: [Int] - let isGeneric: Bool - let flagsRawValue: UInt32 - } - - static let structTest = Entry( - name: "SymbolTestsCore.Structs.StructTest", - numberOfFields: 1, - fieldNames: ["body"], - fieldOffsets: [0x10], - isGeneric: false, - flagsRawValue: 0x40000051 - ) - - static let genericStructNonRequirement = Entry( - name: "SymbolTestsCore.GenericFieldLayout.GenericStructNonRequirement", - numberOfFields: 3, - fieldNames: ["field1", "field2", "field3"], - fieldOffsets: [0x10, 0x18, 0x28], - isGeneric: true, - flagsRawValue: 0x40000091 - ) -} -``` - -### 4. Baseline Generator - -#### 4.1 形态 - -独立 executable target `baseline-generator`,通过 `swift run baseline-generator [--suite ] [--output ]` 触发。 -不混入 `swift test`(不属于"测试")。 - -#### 4.2 模块组织 - -``` -Sources/MachOTestingSupport/Baseline/ -├── BaselineGenerator.swift // 主入口:遍历 fixture、调度子 generator -├── BaselineEmitter.swift // 数值 → Swift 字面量(offset/flags hex,count 十进制) -├── BaselineFixturePicker.swift // 在 fixture 中找"主测目标 + 关键变体" -└── Generators/ - ├── StructDescriptorBaselineGenerator.swift - ├── ClassDescriptorBaselineGenerator.swift - └── ... (每个被测 type 一个 generator) - -Sources/baseline-generator/ -└── main.swift // ArgumentParser + 调 BaselineGenerator -``` - -#### 4.3 生成流程 - -``` -1. 加载 fixture(同 MachOSwiftSectionFixtureTests:磁盘 + dlopen) -2. 对每个被测 model type - ├── BaselineFixturePicker 选出 (主测目标 + 关键变体) - ├── 对每个挑中的 fixture entity,Generator 调用 entity 上每个 public 入口 - │ └── BaselineEmitter 序列化为 Swift 字面量 - └── 输出 Baseline.swift 到 Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/ -3. 同时输出每个 Suite 的 registeredTestMethodNames(嵌入对应 `Baseline.swift`)+ 一个汇总文件 `Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AllFixtureSuites.swift`,内含 `allFixtureSuites` 数组(供 Coverage 守护测试使用) -4. 文件头写元数据:fixture commit hash + Swift toolchain version + 生成日期 -``` - -#### 4.4 Emitter 数值进制约定 - -- **offset / size** 用 hex(`0x10`),便于和 `otool`/Hopper 对照。 -- **flags rawValue** 用 hex(`0x40000051`),与 Swift 源码内 flag 定义一致。 -- **count / index** 用十进制。 -- **name / mangled name** 用字符串字面量,转义 backslash/quote。 -- **enum 值**(如 `ContextDescriptorKind`)输出全限定名:`.class`。 - -#### 4.5 重生成流程(operator-facing) - -fixture 重编后(toolchain 升级 / 源文件改动): - -``` -1. xcodebuild -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj \ - -scheme SymbolTestsCore -configuration Release build -2. swift run baseline-generator --output Tests/MachOSwiftSectionTests/Fixtures/__Baseline__ -3. git diff Tests/MachOSwiftSectionTests/Fixtures/__Baseline__ - ── 漂移符合预期:commit - ── 漂移不符合预期:定位 reader bug -4. swift test --filter MachOSwiftSectionTests -``` - -支持 `--suite ` 局部重生,降低 review 范围。 - -#### 4.6 Generator 自身正确性保证 - -- generator 只用 **MachOFile** 路径生成 baseline(单一路径,易审)。 -- 测试 Suite 通过三家 reader 一致性独立验证 MachOImage/InProcess,**不依赖** baseline。 -- 关键 emitter(数值进制、字符串转义)有专门的 emitter unit test。 - -### 5. Coverage Invariant - -#### 5.1 数据源 - -- **expected**:SwiftSyntax 静态扫描 `Sources/MachOSwiftSection/Models/**/*.swift`,提取每个 `public func`/`public var`/`public init`。 -- **registered**:反射所有 `FixtureSuite`-conforming Suite 类型的 `static var registeredTestMethodNames` + `testedTypeName`。 - -#### 5.2 MethodKey - -```swift -struct MethodKey: Hashable, Comparable { - let typeName: String // e.g. "StructDescriptor" - let memberName: String // e.g. "fields" -} -``` - -**重载合并**:三家重载(`(in: MachO)` / `(in: Context)` / `()` InProcess)共享一个 `memberName`,在单个 `@Test` 内验证一致性。Coverage 守护按 `(typeName, memberName)` 比对,不区分重载。 - -#### 5.3 Scanner 实现 - -`Sources/MachOTestingSupport/Coverage/PublicMemberScanner.swift`: - -- 用 SwiftSyntax 解析 `Sources/MachOSwiftSection/Models/**/*.swift`。 -- 跳过 `@_spi(Internals)` 标注的方法。 -- 跳过 `internal`/`private`/`fileprivate`(必须 `public` 或 `open`)。 -- 跳过 `Layout` 内字段(已被 `LayoutTests` 覆盖)。 -- 跳过 `@MemberwiseInit` 宏生成的 `init(layout:offset:)`(识别 attribute 或签名)。 -- 接受 `Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlist.swift` 配置,允许 (a) 已知不可测方法的明确豁免,(b) 暂未支持的 fixture 场景。每个 allowlist 项必须有 reason 注释。 - -#### 5.4 Coverage Test - -```swift -@Suite -struct MachOSwiftSectionCoverageInvariantTests { - @Test func everyPublicMemberHasATest() async throws { - let scanner = PublicMemberScanner(sourceRoot: ... /* Sources/MachOSwiftSection/Models */) - let expected = try scanner.scan(applyingAllowlist: CoverageAllowlist.entries) - - let registered = Set( - allFixtureSuites.flatMap { suite -> [MethodKey] in - suite.registeredTestMethodNames.map { name in - MethodKey(typeName: suite.testedTypeName, memberName: name) - } - } - ) - - let missing = expected.subtracting(registered) - let extra = registered.subtracting(expected) - - #expect(missing.isEmpty, "Missing tests for: \(missing.sorted())") - #expect(extra.isEmpty, "Tests registered for non-existent members: \(extra.sorted())") - } -} -``` - -`allFixtureSuites` 由 generator 同步生成: - -```swift -let allFixtureSuites: [any FixtureSuite.Type] = [ - StructDescriptorTests.self, - ClassDescriptorTests.self, - ProtocolDescriptorTests.self, - // ... -] -``` - -#### 5.5 失败信息 - -``` -Missing tests for these public members of MachOSwiftSection/Models: - StructDescriptor.classGenericContext - ClassDescriptor.objCRuntimeName - ProtocolDescriptor.numAssociatedTypes - -Tip: add these names to the registeredTestMethodNames of the corresponding Suite, -add a @Test per name, and run `swift run baseline-generator --suite ` -to populate baseline expected values. -``` - -#### 5.6 Coverage / Generator 协作矩阵 - -| 触发场景 | 谁动了什么 | Coverage 守护行为 | -|---|---|---| -| 给 Models/ 加 public method | scanner expected 多一项 | 失败,提示新增 @Test | -| 给 Suite 加 @Test + registeredTestMethodNames | registered 多一项 | 通过(前提是 expected 也有) | -| 删 public method 但忘删 @Test | scanner expected 少一项 | 失败 with "extra" | -| 改 method 名 | expected/registered 都变 | 失败 with both,作者跟着改 | -| 重新生成 baseline | generator 自动同步 registered | 通过 | - -### 6. 测试范围与 Allowlist - -#### 6.1 入测范围 - -- `Sources/MachOSwiftSection/Models/**/*.swift` 中所有 `public`/`open` 标注的 `func` / `var` / `init`(open 主要出现在 class 上)。 -- 三家重载(`(in: MachO)` / `(in: Context)` / `()` InProcess)在单个 `@Test` 内一并验证;实际存在的重载由源码决定,缺失的跳过(见 §3.2)。 - -#### 6.2 显式 Exclusions(`CoverageAllowlist`) - -每项必须带 reason: - -1. `@MemberwiseInit` 宏生成的 `init(layout:offset:)` —— 自动产物,无业务逻辑。 -2. `Layout` 类型的 `static func offset(of:)` —— 已在 `LayoutTests` 覆盖。 -3. MachO-only 调试 helper(如 `ResilientWitness.implementationAddress(in: MachO) -> String`)—— 已在源码 doc comment 标注非数据读取。 -4. `@_spi(Internals)` 标注的 public —— SPI 不属于稳定 API。 -5. `Capture/` 等高度专用 wrapper(实施阶段 case-by-case 决定),fixture 不触发的暂入 allowlist with reason `needs fixture extension`。 - -### 7. Risks & Mitigations - -| 风险 | 触发 | 缓解 | -|---|---|---| -| Fixture 路径在 CI 上找不到 | DerivedData 相对路径 / CI working dir 不同 | (a) 沿用 SwiftDumpTests 已跑通的相对路径;(b) `init` 失败给 actionable 信息(指出 `xcodebuild` 命令) | -| dlopen fixture framework 失败 | framework 未编译 / iOS Simulator 路径不匹配 / sandbox | 抛 `FixtureLoadError.imageNotFoundAfterDlopen(path:dlerror:)`,所有子类的 `@Test` init 阶段就 fail | -| Fixture 重编 → ABI 漂移 | toolchain 升级 / 源文件改动 / Xcode 升级 | (a) `git diff __Baseline__` 一目了然;(b) baseline 头记录 toolchain version + 日期;(c) `--suite ` 局部重生 | -| Generator 自身 bug 把错值固化进 baseline | generator 调用 reader 错或 emitter 转义错 | (a) generator 只用 MachOFile 单一路径,易审;(b) 三家 reader 一致性独立验证 MachOImage/InProcess;(c) emitter unit test | -| 跨 reader 一致性"假阳性通过":三家都错同一个 bug | 共享底层 helper 出 bug | baseline 数值断言独立兜底 | -| `@MemberwiseInit` 签名变化 → scanner 误判 | 宏更新 | scanner 基于 `@MemberwiseInit` attribute 是否存在,而非签名形状;allowlist 兜底 | -| Coverage 守护数据漂移成本 | 改方法名同时改两处 | 失败信息明确 missing/extra;正常工作流是 `改源码 → 跑 generator → commit`,registered 自动同步 | -| InProcess 路径在 fixture 上不存在的 method | 部分 model 类型未提供 `asPointerWrapper` 桥接,InProcess 与 MachO 重载不完全对称 | Suite 模板对没有 InProcess 重载的 method 跳过 `fromInProcess` 一致性断言;每个 `@Test` 实际验证的 reader 集合由 method 在源码中存在的重载决定(详见 §3.2) | -| 测试规模膨胀拖慢 swift test | 几百 `@Test`,每个 init 加载 fixture | (a) `dlopen` 用 `static let` 只跑一次;(b) MachOFile/MachOImage 单 init 成本不高(SwiftDumpTests 已验证);(c) 必要时 future work 引入 fixture cache | - -## Validation - -实施完成的验收 checklist: - -- [ ] `swift test --filter MachOSwiftSectionTests` 全绿。 -- [ ] `swift test --filter MachOSwiftSectionCoverageInvariantTests` 绿(missing/extra 均空)。 -- [ ] `swift run baseline-generator --suite <任一>` 应该幂等(刚生成完跑不修改任何 baseline 文件)。 -- [ ] 在 `Sources/MachOSwiftSection/Models/Type/Struct/StructDescriptor.swift` 临时加空 `public func dummyForCoverageProbe() {}`,coverage test 必须报 missing 含 `StructDescriptor.dummyForCoverageProbe`。回滚后重新绿。 -- [ ] 在某个 Suite 临时改一个 baseline 数值断言,运行该 Suite 必须报 #expect 失败,信息能定位到具体 method 名。 -- [ ] CI 上无需新增配置即可通过(fixture 已编译并 commit 在 DerivedData/)。 - -## 实施分批(初步) - -详细 plan 由 writing-plans 阶段产出,初步切分参考: - -1. **基础设施**:`MachOImageName.SymbolTestsCore` + `MachOSwiftSectionFixtureTests` + `acrossAllReaders` helper + `FixtureLoadError`。 -2. **BaselineEmitter**:数值/字符串/数组/Optional/enum 字面量序列化 + emitter unit test。 -3. **PublicMemberScanner + CoverageAllowlist 框架**:scanner 实现 + 一个故意制造的 sample 验证 missing/extra 报错。 -4. **第一个 Suite(StructDescriptorTests + StructDescriptorBaselineGenerator)** 跑通端到端流程,锁定模板。 -5. 后续按 Models/ 子目录批量迁移,每批一个 commit:Anonymous/Module/Extension → ContextDescriptor → Type/Class → Type/Enum → Type/Struct → Type 根 → Protocol/ProtocolConformance → Generic → FieldDescriptor/FieldRecord/AssociatedType → Metadata → ExistentialType/TupleType/OpaqueType/BuiltinType/ForeignType → Capture(若需)。 -6. **`MachOSwiftSectionCoverageInvariantTests`** 上线,把所有上面批次串起来守护。 -7. **`baseline-generator` executable target** 收尾(整合所有 sub generator + ArgumentParser)。 - -每批一个 commit 且必须 `swift build` + `swift test --filter MachOSwiftSectionTests` 全绿。 diff --git a/docs/superpowers/specs/2026-05-05-fixture-coverage-tightening-design.md b/docs/superpowers/specs/2026-05-05-fixture-coverage-tightening-design.md deleted file mode 100644 index bef57294..00000000 --- a/docs/superpowers/specs/2026-05-05-fixture-coverage-tightening-design.md +++ /dev/null @@ -1,640 +0,0 @@ -# Fixture-Based Test Coverage 收紧 Design - -**日期:** 2026-05-05 -**状态:** 待实施 -**分支:** `feature/machoswift-section-fixture-tests` -**关联 PR:** #85 -**前置 spec:** `2026-05-03-machoswift-section-fixture-tests-design.md` (PR #85 原始设计) - -## 问题 - -PR #85 (`Fixture-based test coverage for MachOSwiftSection Models/`) 的 review 暴露出 fixture 测试覆盖系统性失真。具体度量: - -| 维度 | 数字 | -|---|---| -| Fixture suites 总数 | 157 | -| 从不调用 `acrossAllReaders`/`acrossAllContexts` 的 sentinel-only suites | 88 (56%) | -| 通过 `registeredTestMethodNames` 声明已覆盖的 public method 总数 | 687 | -| 实际只挂在 baseline 字符串集合里、从未真正跨 reader 验证的 method | 277 (40%) | - -`MachOSwiftSectionCoverageInvariantTests` 的 "missing == [] && extra == []" 断言在过半 suite 上是空挡: 它只比对"源码声明的 public 名字"和"baseline 字符串集合里登记的名字"——后者是手动维护的 `registeredTestMethodNames` 而非 `@Test` 实际执行的 behavior。一个 - -```swift -@Test func registrationOnly() { - #expect(Baseline.registeredTestMethodNames.contains("foo")) -} -``` - -形如上面的"sentinel 测试"永远 pass,跟方法 `foo` 是否被实际跨 reader 验证完全无关。 - -PR #85 的原始设计 spec 明确写过 "找不到合适样本就入 `CoverageAllowlist` 标 `needs fixture extension`,留 future work"。但实施时被偷换成 sentinel suite —— 绕过了 allowlist 必须填 `reason: String` 的强制约束,导致 fixture 缺口不可见。 - -本设计修复信任问题,沿三条路径并行: - -- **A — Sentinel 机制就位**: 让 sentinel 成为 first-class 概念,每个 sentinel method 必须显式登记类型化 reason -- **B — Fixture 扩展**: 给 `SymbolTestsCore` 加 12-15 种新 metadata 形态,把"应能但没做"的 sentinel 转成真测 -- **C — InProcess 真测**: runtime-only metadata 用 InProcess single-reader 路径 + baseline literal 真测,把"运行时分配类型"sentinel 转成真测 - -## 目标 - -1. **类型化 sentinel reason**: 引入 `SentinelReason` enum (`runtimeOnly` / `needsFixtureExtension` / `pureDataUtility`),写进 `CoverageAllowlistEntries` -2. **CoverageInvariant 新增双约束**: - - **③ liarSentinel**: 标记 sentinel 但 suite 实际调过 `acrossAllReaders`/`inProcessContext` → fail (标签不同步) - - **④ unmarkedSentinel**: suite 行为是 sentinel 但未登记 → fail (核心新约束,堵住"silent sentinel") -3. **88 个现有 sentinel suites (共 277 个 method) 一次性 categorize**: 启发式归类 + 人工补 unknown。Allowlist 是 per-method 粒度,但同 suite 内 method 共享同一 `SentinelReason` (用 `sentinelGroup(typeName:members:reason:)` helper 减少重复) -4. **~15 个 type 扩 fixture**: PR merge 时 `needsFixtureExtension` 类目清零 (覆盖约 15 个 suite,对应 ~50-70 个 method) -5. **~30 个 runtime-only type 转真测**: PR merge 时 `runtimeOnly` 类目清减至 ~3-5 个无法稳定构造的 type (heap 内部 metadata) -6. **可消化的 sentinel 全部消化**: PR merge 时残留 sentinel suite 仅: - - `pureDataUtility`: ~25 个 type (合理永久 sentinel,纯 raw-value enum / flags / kind protocol;允许后续 follow-up 做 rawValue pinning 增强) - - `runtimeOnly`: ~3-5 个 type (无法在测试进程稳定构造的 heap 内部 metadata,documented) - - _精确数字按 A2 commit 落地为准,以上为 brainstorm 阶段预估上限。_ - -## 非目标 - -- **不重构 `PublicMemberScanner` 与现有 `BaselineFixturePicker`** 已落地代码。新 picker 加在它们旁边,不动旧代码。 -- **不修改 `__Baseline__/AllFixtureSuites.swift` 索引的 hand-maintained 机制** (review 提过的双源问题留独立 follow-up) -- **不动 PR #85 已经 push 的 commit 历史** (前 30+ commits 不 amend / rebase / squash) -- **不引入 Swift runtime backdeploy hack**, 走 macOS 12 + 标准 API -- **不解决 `pureDataUtility` 的 rawValue pinning 增强** (sentinel 标签就位后是 follow-up 优化项,不在本 spec 范围) - -## 设计 - -### 1. 整体架构 - -``` -┌─ Sources/MachOSwiftSection/Models/ (源代码事实) -│ │ -│ │ PublicMemberScanner (SwiftSyntax,保持现状) -│ ▼ -│ expected: Set -│ -├─ Tests/MachOSwiftSectionTests/Fixtures/**/*Tests.swift (suite 文件事实) -│ │ -│ │ SuiteBehaviorScanner (SwiftSyntax,新增) ← A 核心 -│ ▼ -│ suiteBehavior: [MethodKey: MethodBehavior] -│ MethodBehavior = .acrossAllReaders | .inProcessOnly | .sentinel -│ -├─ Tests/MachOSwiftSectionTests/Fixtures/__Baseline__/AllFixtureSuites.swift -│ │ -│ │ 反射 (保持现状) -│ ▼ -│ registered: Set -│ -└─ Tests/MachOSwiftSectionTests/Fixtures/CoverageAllowlistEntries.swift (人工事实) - │ - ▼ - allowlist: [CoverageAllowlistEntry] - kind: AllowlistKind = .legacyExempt(reason) ← 现有 - | .sentinel(SentinelReason) ← 新增 - -CoverageInvariant 四段断言: - ① missing = expected − registered − allowlist.keys 必须为空 - ② extra = registered − expected − allowlist.keys 必须为空 - ③ liarSentinel = sentinel-tagged keys whose actual behavior is non-sentinel 必须为空 - ④ unmarkedSentinel = behavior=.sentinel keys missing from sentinel-tagged set 必须为空 -``` - -`SuiteBehaviorScanner` 在 method 粒度判定行为,聚合到 `(typeName, memberName)` key。Mixed suite (一部分 method 真测、一部分 sentinel) 自然成立 —— behavior map 是 per-key 的。 - -### 2. A — Sentinel 机制就位 - -#### 2.1 `CoverageAllowlistEntries.swift` 新 schema - -```swift -package enum SentinelReason: Hashable { - /// 类型由 Swift runtime 现场分配,不在 fixture binary 里序列化。 - /// 由 C 通过 InProcess single-reader + baseline literal pinning 覆盖。 - /// 例: MetatypeMetadata, TupleTypeMetadata, FunctionTypeMetadata, - /// OpaqueMetadata, FixedArrayTypeMetadata, *MetadataHeader, *MetadataBounds. - case runtimeOnly(detail: String) - - /// fixture 内缺合适样本,理论上能扩 SymbolTestsCore 后转真测。 - /// 由 B 通过新增 fixture 文件 + 转真测消化。 - /// 本 PR 内此类目最终应清零。 - /// 例: MethodDefaultOverrideDescriptor, ObjCClassWrapperMetadata, - /// CanonicalSpecializedMetadatas* family, ResilientSuperclass. - case needsFixtureExtension(detail: String) - - /// 纯 raw-value enum / 标记 protocol / pure-data utility, - /// 永久 sentinel 也合理。仍要求后续 follow-up 做 rawValue pinning。 - /// 例: ContextDescriptorKind, MetadataKind, ProtocolDescriptorFlags 等。 - case pureDataUtility(detail: String) -} - -package enum AllowlistKind: Hashable { - /// 现有用法: 源码扫描误判 / @MemberwiseInit 合成 init / @testable 才可见的合成 init 等。 - case legacyExempt(reason: String) - - /// 新增: 标 sentinel 类目 + reason。 - case sentinel(SentinelReason) -} - -package struct CoverageAllowlistEntry: Hashable { - package let key: MethodKey - package let kind: AllowlistKind - - /// 兼容现有 `legacyExempt` 调用点的 convenience init。 - package init(typeName: String, memberName: String, reason: String) { - self.key = MethodKey(typeName: typeName, memberName: memberName) - self.kind = .legacyExempt(reason: reason) - } - - package init(typeName: String, memberName: String, sentinel: SentinelReason) { - self.key = MethodKey(typeName: typeName, memberName: memberName) - self.kind = .sentinel(sentinel) - } -} -``` - -`CoverageAllowlistEntries.entries` 数组里现有的 1 项 (`ProtocolDescriptorRef.init(storage:)`) 走 `legacyExempt` 路径不变。新增 277 项 sentinel 走 `.sentinel(...)` 路径。 - -`CoverageAllowlistEntries.keys: Set` 保持不变,仍返回所有项的 key 集合。新增便利 accessor: - -```swift -extension CoverageAllowlistEntries { - static var sentinelKeys: Set { - Set(entries.compactMap { entry in - if case .sentinel = entry.kind { return entry.key } else { return nil } - }) - } - - static func sentinelReason(for key: MethodKey) -> SentinelReason? { - for entry in entries { - if entry.key == key, case .sentinel(let reason) = entry.kind { - return reason - } - } - return nil - } - - /// Construct a flat array of `[CoverageAllowlistEntry]` sharing the same - /// `SentinelReason` for all `members` of `typeName`. Use this in - /// `entries` initialization to avoid repeating the reason on every method: - /// - /// static let entries: [CoverageAllowlistEntry] = [ - /// .init(typeName: "ProtocolDescriptorRef", memberName: "init(storage:)", - /// reason: "synthesized memberwise init"), - /// ] + sentinelGroup( - /// typeName: "MethodDefaultOverrideDescriptor", - /// members: ["originalMethodDescriptor", "replacementMethodDescriptor", - /// "implementationSymbols", "layout", "offset"], - /// reason: .needsFixtureExtension(detail: "no class with default-override table in SymbolTestsCore") - /// ) + sentinelGroup(...) - static func sentinelGroup( - typeName: String, - members: [String], - reason: SentinelReason - ) -> [CoverageAllowlistEntry] { - members.map { memberName in - CoverageAllowlistEntry( - typeName: typeName, - memberName: memberName, - sentinel: reason - ) - } - } -} -``` - -#### 2.2 `SuiteBehaviorScanner` (新增) - -文件: `Sources/MachOFixtureSupport/Coverage/SuiteBehaviorScanner.swift` - -```swift -package struct SuiteBehaviorScanner { - package enum MethodBehavior: Equatable { - case acrossAllReaders // 调用过 acrossAllReaders / acrossAllContexts - case inProcessOnly // 只调过 usingInProcessOnly / inProcessContext (不接 acrossAllReaders) - case sentinel // 既没跨 reader 也没 InProcess single-reader - } - - package let suiteRoot: URL - - package init(suiteRoot: URL) { self.suiteRoot = suiteRoot } - - /// 扫描 suiteRoot 下所有 *Tests.swift,对每个 `@Test` 函数判定行为, - /// 聚合到 `(testedTypeName, methodName)` key。 - package func scan() throws -> [MethodKey: MethodBehavior] -} -``` - -实现: -- 用 `SwiftSyntax.Parser` 解析每个 `*Tests.swift` -- 找带 `@Test` attribute 的 `FunctionDeclSyntax` -- 函数 body 里 `IdentifierExprSyntax` / `MemberAccessExprSyntax` 含 `acrossAllReaders` 或 `acrossAllContexts` → `.acrossAllReaders` -- 否则若含 `usingInProcessOnly` 或 `inProcessContext` → `.inProcessOnly` -- 否则 → `.sentinel` -- key 用 `.testedTypeName` (从 class body 里找 `static let testedTypeName = "..."`) + 函数名 - -边界处理: -- 函数名直接取 `FunctionDeclSyntax.name.text` -- 若 suite 类不 conform `FixtureSuite` (例如 `MachOSwiftSectionCoverageInvariantTests` 自身) → 跳过 -- testedTypeName 从 `static let testedTypeName = "Foo"` 字面量提取;无法提取 → 抛错 - -#### 2.3 `MachOSwiftSectionCoverageInvariantTests` 新断言 - -```swift -@Test func everyPublicMemberHasATest() throws { - let scanner = PublicMemberScanner(sourceRoot: modelsRoot) - let allowlistAllKeys = CoverageAllowlistEntries.keys - let sentinelKeys = CoverageAllowlistEntries.sentinelKeys - - let expected = try scanner.scan(applyingAllowlist: []) - let registered: Set = Set(...) // 同现状 - let behaviorMap = try SuiteBehaviorScanner(suiteRoot: ...).scan() - - // ① + ② 同现状,允许 allowlist 兜底 - let missing = expected.subtracting(registered).subtracting(allowlistAllKeys) - let extra = registered.subtracting(expected).subtracting(allowlistAllKeys) - #expect(missing.isEmpty, ...) - #expect(extra.isEmpty, ...) - - // ③ liar sentinel - let liarSentinels = sentinelKeys.filter { key in - if let behavior = behaviorMap[key], behavior != .sentinel { - return true - } - return false - } - #expect( - liarSentinels.isEmpty, - """ - These methods are tagged sentinel in CoverageAllowlistEntries but the - Suite actually calls acrossAllReaders / inProcessContext — the sentinel - tag is stale. Either remove the sentinel entry or revert the test to - registration-only. - \(liarSentinels.sorted().map { " \($0)" }.joined(separator: "\n")) - """ - ) - - // ④ unmarked sentinel - let actualSentinelKeys = Set(behaviorMap.compactMap { $0.value == .sentinel ? $0.key : nil }) - let unmarked = actualSentinelKeys.subtracting(sentinelKeys).subtracting(allowlistAllKeys) - #expect( - unmarked.isEmpty, - """ - These methods are sentinel-only (the Suite never calls - acrossAllReaders / inProcessContext) but are not declared in - CoverageAllowlistEntries. Either implement a real test, or add a - SentinelReason entry explaining why this is the right level of coverage. - \(unmarked.sorted().map { " \($0)" }.joined(separator: "\n")) - """ - ) -} -``` - -#### 2.4 88 个现有 sentinel 的初步归类 - -预归类清单见 Appendix A。A2 commit 实施时按实际 suite 内容精调。 - -### 3. B — SymbolTestsCore Fixture 扩展 - -#### 3.1 新增 fixture 文件 - -按"一种 metadata 形态 → 一个 .swift 文件"组织,drop 进 `Tests/Projects/SymbolTests/SymbolTestsCore/`。`PBXFileSystemSynchronizedRootGroup` 自动 pick up。 - -| 文件 | 引入的 metadata 形态 | 消化的 sentinel suites | -|---|---|---| -| `DefaultOverrideTable.swift` | class with dynamic replacement → method default-override table | `MethodDefaultOverrideDescriptor`, `MethodDefaultOverrideTableHeader`, `OverrideTableHeader` | -| `ResilientClasses.swift` | resilient class + resilient superclass reference | `ResilientSuperclass`, `StoredClassMetadataBounds` | -| `ObjCClassWrappers.swift` | Swift class inheriting `NSObject` → ObjC class wrapper metadata | `ObjCClassWrapperMetadata`, `ClassMetadataObjCInterop`, `AnyClassMetadataObjCInterop`, `RelativeObjCProtocolPrefix` | -| `ObjCResilientStubs.swift` | Swift class inheriting resilient ObjC class | `ObjCResilientClassStubInfo` | -| `CanonicalSpecializedMetadata.swift` | generic types with `@_specialize(exported: true)` → canonical specialized metadata | `CanonicalSpecializedMetadataAccessorsListEntry`, `CanonicalSpecializedMetadatasCachingOnceToken`, `CanonicalSpecializedMetadatasListCount`, `CanonicalSpecializedMetadatasListEntry` | -| `ForeignTypes.swift` | foreign class import + foreign reference type | `ForeignClassMetadata`, `ForeignReferenceTypeMetadata`, `ForeignMetadataInitialization` | -| `GenericValueParameters.swift` | type with `` value generic parameters | `GenericValueDescriptor`, `GenericValueHeader` | - -预估 15 个 sentinel suites 通过 B 转真测。剩余少数 fixture 技术上做不出的 (例如 `@_specialize(exported:)` 在 framework 不触发 canonical-specialized-metadata 的情况下) 保留 `runtimeOnly` 标签或新增 `unbuildable` case 处理,在 spec 末尾登记。 - -#### 3.2 工程流程 (每个 fixture 文件一个 commit) - -1. 写新 `.swift` 文件到 `Tests/Projects/SymbolTests/SymbolTestsCore/` -2. 在 `SymbolTestsCore` Xcode 项目中 build: - ```bash - xcodebuild -project Tests/Projects/SymbolTests/SymbolTests.xcodeproj \ - -scheme SymbolTestsCore -configuration Release build - ``` -3. 在 `Sources/MachOFixtureSupport/Baseline/BaselineFixturePicker.swift` 加新 picker 函数: - ```swift - package static func class_DefaultOverrideTest( - in machO: some MachOSwiftSectionRepresentableWithCache - ) throws -> ClassDescriptor { ... } - ``` -4. 在对应 `Sources/MachOFixtureSupport/Baseline/Generators//BaselineGenerator.swift` 把 `static let registeredTestMethodNames` 改完整字面量列表,发出 `Entry` ABI literal -5. 重写对应 suite: 删 `registrationOnly` 函数,加入真 `acrossAllReaders` 测试函数 -6. `swift package --allow-writing-to-package-directory regen-baselines --suite ` -7. `swift test --filter Tests` 验证 -8. 同步移除 `CoverageAllowlistEntries` 中对应 `needsFixtureExtension` 项 - -#### 3.3 风险与缓解 - -| 风险 | 缓解 | -|---|---| -| 某 metadata 形态需要内部 `@_` attribute 才能触发,编译/链接失败 | 优先尝试不带 `@_` 的最小路径;不行则保留 `runtimeOnly`/新建 `unbuildable` case 在 spec 登记 | -| `xcodebuild` rebuild 后 `DerivedData/` 二进制变动触发整片 baseline drift | B0 阶段先做一次 baseline 全量对齐 commit;后续每个 B-commit 标 `[fixture rebuild]` 并 git diff 全量 review | -| ObjC interop fixture 需要 ObjC runtime 加载 | 现有 `dlopen(SymbolTestsCore)` 走 dyld,ObjC runtime 自动加载,无需配置 | -| `@_specialize(exported:)` 在 framework 里能否触发 canonical-specialized 不确定 | spec 标记此 fixture 为"实验",B5 commit 失败则保留 `needsFixtureExtension` | -| `` value-generic 在 Swift 6.2 仍是 experimental | `@available(...)` 守卫;旧 OS 跳过 | - -### 4. C — InProcess Runtime Metadata 真测 - -#### 4.1 来源分流 - -| 来源 | 适用 suite | 取得方式 | -|---|---|---| -| **stdlib metatype** | `MetatypeMetadata` | `unsafeBitCast(Int.self.self, to: UnsafeRawPointer.self)` | -| **stdlib tuple** | `TupleTypeMetadata`, `TupleTypeMetadataElement` | `unsafeBitCast((Int, String).self, to: UnsafeRawPointer.self)` | -| **stdlib function** | `FunctionTypeMetadata`, `FunctionTypeFlags` | `unsafeBitCast(((Int) -> Void).self, to: UnsafeRawPointer.self)` | -| **stdlib existential** | `ExistentialTypeMetadata`, `ExistentialMetatypeMetadata`, `ExistentialTypeFlags`, `ExtendedExistentialTypeMetadata`, `ExtendedExistentialTypeShape`, `ExtendedExistentialTypeShapeFlags`, `NonUniqueExtendedExistentialTypeShape` | `Any.self`, `(any Equatable).self`, `(any Equatable & Sendable).self` | -| **stdlib opaque** | `OpaqueMetadata` | `unsafeBitCast(Builtin.Int8.self, to: UnsafeRawPointer.self)` (或 `Int8.self` fallback) | -| **stdlib fixed array** | `FixedArrayTypeMetadata` | `InlineArray<3, Int>.self` (macOS 26+ guard) | -| **fixture nominal** | `StructMetadata`, `EnumMetadata`, `ClassMetadata`, `DispatchClassMetadata`, `ValueMetadata`, `AnyClassMetadata`, `AnyClassMetadataObjCInterop`, `FinalClassMetadataProtocol`, `ClassMetadataBounds`, `StoredClassMetadataBounds` | `unsafeBitCast(SymbolTestsCore..self, to: UnsafeRawPointer.self)` | -| **header offset on existing metadata** | `HeapMetadataHeader`, `HeapMetadataHeaderPrefix`, `TypeMetadataHeader`, `TypeMetadataHeaderBase`, `TypeMetadataLayoutPrefix`, `MetadataBounds`, `MetadataBoundsProtocol`, `Metadata`, `FullMetadata`, `MetadataWrapper`, `MetadataProtocol`, `MetadataResponse`, `MetadataRequest`, `MetadataAccessorFunction`, `SingletonMetadataPointer` | 复用上面 metadata pointer,从 layout prefix 偏移读取 | -| **保留 sentinel** (无法稳定构造) | `GenericBoxHeapMetadata`, `HeapLocalVariableMetadata` | 保留 `runtimeOnly` 标签,spec 解释 | - -总计预计 ~30 个 sentinel suites 通过 C 转出真测。 - -#### 4.2 新增 helper — `InProcessMetadataPicker` - -文件: `Sources/MachOFixtureSupport/InProcess/InProcessMetadataPicker.swift` - -```swift -package enum InProcessMetadataPicker { - /// stdlib `Int` 的 metatype metadata,用于 MetatypeMetadataTests。 - package static let stdlibIntMetatype: UnsafeRawPointer = { - unsafeBitCast(Int.self.self, to: UnsafeRawPointer.self) - }() - - /// `(Int, String)` 的 tuple metadata。 - package static let stdlibTupleIntString: UnsafeRawPointer = { - unsafeBitCast((Int, String).self, to: UnsafeRawPointer.self) - }() - - /// `((Int) -> Void)` 的 function metadata。 - package static let stdlibFunctionIntToVoid: UnsafeRawPointer = { - unsafeBitCast(((Int) -> Void).self, to: UnsafeRawPointer.self) - }() - - /// `Any` 的 existential metadata。 - package static let stdlibAnyExistential: UnsafeRawPointer = { - unsafeBitCast(Any.self, to: UnsafeRawPointer.self) - }() - - /// `(any Equatable)` 的 extended existential metadata (with shape)。 - package static let stdlibAnyEquatable: UnsafeRawPointer = { - unsafeBitCast((any Equatable).self, to: UnsafeRawPointer.self) - }() - - // ... 其余按 4.1 表逐一暴露 -} -``` - -#### 4.3 一致性策略调整 - -`MachOSwiftSectionFixtureTests` 加 helper: - -```swift -package func usingInProcessOnly( - _ work: (InProcessContext) throws -> T, - sourceLocation: SourceLocation = #_sourceLocation -) throws -> T { - try work(inProcessContext) -} -``` - -Suite 模板: -```swift -@Test func kind() async throws { - let metadataPointer = InProcessMetadataPicker.stdlibIntMetatype - let result = try usingInProcessOnly { context in - try MetatypeMetadata(at: metadataPointer, in: context).kind - } - #expect(result == MetatypeMetadataBaseline.stdlibIntMetatype.kind) -} -``` - -`SuiteBehaviorScanner` 把 `usingInProcessOnly` / `inProcessContext` 也认作非 sentinel。 - -#### 4.4 边界处理 - -- `Builtin.Int8` 不在普通 module 可见 → 用 `Int8.self` fallback -- `InlineArray<3, Int>` 需 macOS 26+ → `@available` 守卫,旧 OS 跳过该 suite 的 InProcess 测,baseline 标 OS-conditional -- `swift_allocBox` 等 runtime API 不在 public surface → `GenericBoxHeapMetadata` / `HeapLocalVariableMetadata` 保留 `runtimeOnly` 不消化 - -### 5. Migration / Commit / 验证 - -#### 5.1 Commit 顺序 - -``` -Phase A — 机制就位 (3 commits, ~1 day) -├── A0. docs: add fixture-coverage tightening design (本 spec 文档) -├── A1. feat(MachOFixtureSupport): introduce SuiteBehaviorScanner + AllowlistKind/SentinelReason schema -│ 新增 scanner、扩 schema、CoverageInvariant 暂保留旧断言不启用新约束 -├── A2. test: seed sentinel reasons for existing 88 suites (277 methods) -│ 一次性 categorize,allowlist 277 个 entries 填好。用 `sentinelGroup(typeName:members:reason:)` -│ helper 缩短 (88 个 suite × 平均 3-5 行 = ~300-400 行 schema 数据) -└── A3. test: enable liarSentinel + unmarkedSentinel invariant assertions - 点亮新断言 ③ ④,跑通 - -Phase C — runtime-only 转 InProcess (5-6 commits, ~2 days) -├── C1. feat(MachOFixtureSupport): add InProcessMetadataPicker + usingInProcessOnly helper + BaselineGenerator InProcess Entry 支持 -├── C2. test: convert MetatypeMetadata/TupleType*/FunctionType* (~5 suites) -├── C3. test: convert ExistentialType* family (~7 suites) -├── C4. test: convert *Metadata/*Header/*Bounds fixture-nominal (~10 suites) -├── C5. test: convert Metadata/MetadataResponse/SingletonMetadataPointer layer (~6 suites) -└── (C6 视情况合入,每 commit 同步删 allowlist 中对应 runtimeOnly 项) - -Phase B — 扩 SymbolTestsCore 消化 needsFixtureExtension (7-8 commits, ~2 days) -├── B0. test(fixture): rebuild SymbolTestsCore baseline DerivedData snapshot -│ (若 phase A/C 期间 DerivedData 漂移,先 baseline 对齐) -├── B1. test(fixture): add DefaultOverrideTable.swift, convert 3 suites -├── B2. test(fixture): add ResilientClasses.swift, convert 2 suites -├── B3. test(fixture): add ObjCClassWrappers.swift, convert 4 suites -├── B4. test(fixture): add ObjCResilientStubs.swift, convert 1 suite -├── B5. test(fixture): add CanonicalSpecializedMetadata.swift, convert 4 suites -├── B6. test(fixture): add ForeignTypes.swift, convert 3 suites -└── B7. test(fixture): add GenericValueParameters.swift, convert 2 suites - -Phase D — cleanup (1 commit) -└── D1. docs: update CLAUDE.md fixture-coverage section + PR description -``` - -总 16-18 个 commit,~5 工作日。 - -#### 5.2 每个 commit 的硬性 gate - -```bash -swift build # 编译 -swift test --filter MachOSwiftSectionTests # 该 phase fixture suites 全绿 -swift test --filter MachOSwiftSectionCoverageInvariantTests # invariant 绿 -``` - -A3 之后 invariant 是 PR tripwire。任何 commit 后若 invariant 红 → 该 commit 必须 fix-forward,**不允许 skip**。 - -#### 5.3 Push 节奏 - -不每个 commit push,按 phase 边界 push,共 6 次: -1. A 完成 (3 commits) -2. C 中段 (~3 commits) -3. C 完成 (~3 commits) -4. B 中段 (~4 commits) -5. B 完成 (~3 commits) -6. D (1 commit) - -#### 5.4 风险登记 - -| 风险 | 触发位置 | 处置 | -|---|---|---| -| `SuiteBehaviorScanner` 误判 mixed-suite 中某 method 行为 | A1-A3 | scanner 遇分歧时 fallback per-suite 粒度;allowlist 项相应放宽,spec 备注精度损失 | -| B 期间 `xcodebuild` 重建 SymbolTestsCore 触发整片 baseline ABI drift | B 任意 commit | B0 先做 baseline 全量对齐;漂移大时该 commit 标 `[fixture rebuild]`,git diff 全量人工 review | -| 某 fixture metadata 形态在当前 Swift 6.2 不触发预期 ABI | B5/B6/B7 | 该项保留 `needsFixtureExtension`,spec doc 更新解释,不 block 其他 phase | -| `InlineArray<3, Int>` 在 macOS 12 不可用 | C-fixedarray | `@available(macOS 26.0, *)` 守卫;旧 OS 跳过该 suite InProcess 测,baseline 标 OS-conditional | -| `swift_allocBox` 等 runtime API 无 public surface | C5 | `GenericBoxHeapMetadata` / `HeapLocalVariableMetadata` 保留 `runtimeOnly`,spec 标"未消化" | - -## Appendix A: 88 个现有 sentinel 的初步归类 - -基于命名规则与 Swift runtime 知识的预归类。A2 commit 实施时按实际 suite 内容精调。 - -### A.1 `runtimeOnly` (~50 项) - -由 Swift runtime 现场分配、不在 fixture binary 序列化的类型: - -- **Metadata core**: `Metadata`, `FullMetadata`, `MetadataProtocol`, `MetadataWrapper`, `MetadataRequest`, `MetadataResponse`, `MetadataAccessorFunction`, `SingletonMetadataPointer` -- **Metadata bounds**: `MetadataBounds`, `MetadataBoundsProtocol`, `ClassMetadataBounds`, `ClassMetadataBoundsProtocol`, `StoredClassMetadataBounds` -- **Metadata headers**: `HeapMetadataHeader`, `HeapMetadataHeaderPrefix`, `TypeMetadataHeader`, `TypeMetadataHeaderBase`, `TypeMetadataLayoutPrefix` -- **Type-flavored metadata**: `StructMetadata`, `StructMetadataProtocol`, `EnumMetadata`, `EnumMetadataProtocol`, `ClassMetadata`, `ClassMetadataObjCInterop`, `AnyClassMetadata`, `AnyClassMetadataObjCInterop`, `AnyClassMetadataProtocol`, `AnyClassMetadataObjCInteropProtocol`, `FinalClassMetadataProtocol`, `DispatchClassMetadata`, `ValueMetadata`, `ValueMetadataProtocol` -- **Existentials**: `ExistentialTypeMetadata`, `ExistentialMetatypeMetadata`, `ExtendedExistentialTypeMetadata`, `ExtendedExistentialTypeShape`, `NonUniqueExtendedExistentialTypeShape` -- **Tuple/function/metatype/opaque/fixed-array**: `TupleTypeMetadata`, `TupleTypeMetadataElement`, `FunctionTypeMetadata`, `MetatypeMetadata`, `OpaqueMetadata`, `FixedArrayTypeMetadata` -- **Heap (保留 sentinel)**: `GenericBoxHeapMetadata`, `HeapLocalVariableMetadata` -- **Generic*runtime layer***: `GenericEnvironment`, `GenericWitnessTable` -- **Value witness table**: `ValueWitnessTable`, `TypeLayout` -- **Foreign metadata initialization**: `ForeignMetadataInitialization` - -### A.2 `needsFixtureExtension` (~15 项) - -应能扩 fixture 后转真测: - -- `MethodDefaultOverrideDescriptor`, `MethodDefaultOverrideTableHeader`, `OverrideTableHeader` -- `ResilientSuperclass` -- `ObjCClassWrapperMetadata`, `RelativeObjCProtocolPrefix`, `ObjCProtocolPrefix` -- `ObjCResilientClassStubInfo` -- `CanonicalSpecializedMetadataAccessorsListEntry`, `CanonicalSpecializedMetadatasCachingOnceToken`, `CanonicalSpecializedMetadatasListCount`, `CanonicalSpecializedMetadatasListEntry` -- `ForeignClassMetadata`, `ForeignReferenceTypeMetadata` -- `GenericValueDescriptor`, `GenericValueHeader` - -### A.3 `pureDataUtility` (~25 项) - -纯 raw-value enum / 标记 protocol / pure-data utility,合理永久 sentinel: - -- **Flags**: `ContextDescriptorFlags`, `ContextDescriptorKindSpecificFlags`, `AnonymousContextDescriptorFlags`, `TypeContextDescriptorFlags`, `ClassFlags`, `ExtraClassDescriptorFlags`, `MethodDescriptorFlags`, `ProtocolDescriptorFlags`, `ProtocolContextDescriptorFlags`, `ProtocolRequirementFlags`, `GenericContextDescriptorFlags`, `GenericRequirementFlags`, `GenericEnvironmentFlags`, `FieldRecordFlags`, `ProtocolConformanceFlags`, `ExistentialTypeFlags`, `ExtendedExistentialTypeShapeFlags`, `FunctionTypeFlags`, `ValueWitnessFlags` -- **Kinds**: `ContextDescriptorKind`, `MethodDescriptorKind`, `ProtocolRequirementKind` -- **Other utilities**: `EnumFunctions`, `InvertibleProtocolSet`, `InvertibleProtocolsRequirementCount`, `TypeReference` - -### A.4 备注 - -- 上面三类列举的是**type 名 (即 sentinel suite 对应的 testedTypeName)**,不是 method 数。Allowlist schema 是 per-method,实际 entry 数 = 各 type 对应 suite 内的 method 总和 (约 277)。 -- 三类 type 总和 ≈ 88,具体每类精确数量 A2 commit 实施时按 suite 内容精调。 -- A.1 中 `GenericBoxHeapMetadata`, `HeapLocalVariableMetadata` 不进 C 真测,保持 `runtimeOnly` 永久 sentinel。 -- A.3 数量预估 25 type,实际可能略多 (某些 *Header / *Bounds 在 method 粒度看更接近 pure-data,需 A2 实施时确认)。 -- A2 commit 会用 `sentinelGroup` helper 把同一 type 下所有 method 共享同一 `SentinelReason`,避免重复: - ```swift - CoverageAllowlistEntries.sentinelGroup( - typeName: "MethodDefaultOverrideDescriptor", - members: ["originalMethodDescriptor", "replacementMethodDescriptor", - "implementationSymbols", "layout", "offset"], - reason: .needsFixtureExtension(detail: "no class with default-override table in SymbolTestsCore — covered after B1") - ) - ``` - -## Appendix B: SuiteBehaviorScanner 实现要点 - -```swift -import SwiftSyntax -import SwiftParser - -private final class SuiteBehaviorVisitor: SyntaxVisitor { - private(set) var collected: [(testedTypeName: String, methodName: String, behavior: SuiteBehaviorScanner.MethodBehavior)] = [] - private var currentTestedTypeName: String? - private var currentClassName: String? - - override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { - currentClassName = node.name.text - currentTestedTypeName = extractTestedTypeName(from: node) - return .visitChildren - } - override func visitPost(_ node: ClassDeclSyntax) { - currentClassName = nil - currentTestedTypeName = nil - } - - override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { - guard hasTestAttribute(node.attributes), - let testedTypeName = currentTestedTypeName, - let body = node.body else { - return .skipChildren - } - let behavior = inferBehavior(from: body) - collected.append((testedTypeName, node.name.text, behavior)) - return .skipChildren - } - - private func extractTestedTypeName(from classDecl: ClassDeclSyntax) -> String? { - // 找 `static let testedTypeName = "Foo"` 字面量 - for member in classDecl.memberBlock.members { - if let varDecl = member.decl.as(VariableDeclSyntax.self), - varDecl.modifiers.contains(where: { $0.name.text == "static" }) { - for binding in varDecl.bindings { - if let ident = binding.pattern.as(IdentifierPatternSyntax.self), - ident.identifier.text == "testedTypeName", - let initializer = binding.initializer, - let stringLit = initializer.value.as(StringLiteralExprSyntax.self) { - return stringLit.segments.compactMap { - $0.as(StringSegmentSyntax.self)?.content.text - }.joined() - } - } - } - } - return nil - } - - private func hasTestAttribute(_ attributes: AttributeListSyntax) -> Bool { - for attr in attributes { - if let attribute = attr.as(AttributeSyntax.self), - attribute.attributeName.trimmedDescription == "Test" { - return true - } - } - return false - } - - private func inferBehavior(from body: CodeBlockSyntax) -> SuiteBehaviorScanner.MethodBehavior { - let bodyText = body.description - if bodyText.contains("acrossAllReaders") || bodyText.contains("acrossAllContexts") { - return .acrossAllReaders - } - if bodyText.contains("usingInProcessOnly") || bodyText.contains("inProcessContext") { - return .inProcessOnly - } - return .sentinel - } -} -``` - -字符串 `contains` 检测足够 — `acrossAllReaders` 等 identifier 在 fixture suite 里没有 false-positive 同名变量约束 (本项目命名规则保证)。如果未来出现冲突,升级到 `MemberAccessExprSyntax` / `IdentifierExprSyntax` 走 SwiftSyntax 树。 - -## Appendix C: 决策记录 - -本 spec 在 brainstorming 阶段做出的关键决策: - -| 决策 | 选项 | 选择 | 理由 | -|---|---|---|---| -| 总体路径 | α 一次大 PR / β 分 PR / γ 先 A 增量 / δ 当前 PR 分批 | δ | PR 内闭环,review 一次看完 | -| Sentinel 检测机制 | 1 全自动 / 2 半显式 marker / 3 per-method baseline 拆分 | 1 | 现有 88 suite 不动源码;88 suite 全 sentinel 无 mixed,per-suite 粒度够用;行为事实最难撒谎 | -| Reason 存储 | a baseline 内 / b 独立文件 / c 扩 CoverageAllowlistEntries | c | 已存在的 reason 集中点,baseline 保持 100% auto-generated | -| Reason 类型 | free-text / typed enum | typed enum | B/C 各自能 iter `.needsFixtureExtension` / `.runtimeOnly` 子集 | -| 88 sentinel seed 策略 | i 全手工 / ii 启发式 + needsCategorization placeholder / iii 启发式 + 立刻补 | iii | spec 落地即完整分类,无 needsCategorization 残留 | -| B 范围 | a 全部消化 / b top-N / c 不做 | a | δ 路径目标是 PR 内闭环 | -| C 实现方式 | 1 stdlib / 2 fixture helper / 3 按 metadata 性质分流 | 3 | 不同 metadata 类型来源不同,分流是技术上更对 | -| C 一致性策略 | 仍要求 acrossAllReaders / 仅 InProcess single-reader | 仅 InProcess | runtime-allocated 在其他 reader 拿不到数据,强求 cross-reader 是另一种 sentinel |