Skip to content

Modern metadata flags, function signatures, associated types, demangling & Mirror children#76

Open
NSExceptional wants to merge 20 commits into
Azoy:mainfrom
NSExceptional:metadata-flags-and-protocols
Open

Modern metadata flags, function signatures, associated types, demangling & Mirror children#76
NSExceptional wants to merge 20 commits into
Azoy:mainfrom
NSExceptional:metadata-flags-and-protocols

Conversation

@NSExceptional

@NSExceptional NSExceptional commented Jun 6, 2026

Copy link
Copy Markdown

Important

Depends on #74 merging first

Summary

Brings Echo's metadata and protocol reflection up to current Swift, and fills in long-standing gaps. Covers the metadata-flag bits that gate modern introspection (concurrency, ~Copyable), full function-type signatures, associated-type/conformance resolution, demangling, and Mirror-style child enumeration.

New Features

Metadata flags — the flag structs were missing most modern bits:

  • ClassMetadata.isActor / isDefaultActor, plus the TypeContextDescriptorFlags bits (layout string, default override table).
  • FunctionMetadata.Flags: isAsync, isSendable, hasGlobalActor, isDifferentiable, hasExtendedFlags.
  • ValueWitnessTable.Flags: isCopyable (~Copyable) and isBitwiseBorrowable.

Function metadata trailing types — walks the conditional trailing objects to expose globalActorType (e.g. MainActor), extendedFlags (ExtendedFunctionTypeFlags: typed throws, isolation, sending result, inverted-protocol set), and thrownErrorType (the E in throws(E)).

Associated types & conformances (AssociatedType.swift) — TypeMetadata.associatedType(named:conformingTo:) and associatedConformance(...) resolve a conformance's associated types (e.g. Sequence.Element) and associated-conformance witnesses at runtime via swift_getAssociatedType/ConformanceWitness. Also makes WitnessTable Equatable.

Demangling (Demangle.swift) — demangle(_:) (human-readable, via swift_demangle) and context-free type(named:) over _typeByName.

Mirror-style enumeration (Reflection.swift) — children(of:) / displayStyle(of:) wrapping the stdlib reflection runtime (struct/class properties, tuple elements, enum payload projection).

Tests

Dedicated tests for actor classes, async/throws/@Sendable/@MainActor/typed-throws function types, value-witness copyability, associated-type/conformance resolution (verified the resolved conformance is pointer-identical to the type's own witness), demangling, and child enumeration across struct/class/enum/tuple. swift test green (29 tests).

NSExceptional and others added 13 commits April 30, 2022 21:16
A conformance's class will be nil when the conformance refers to a class that is only present in a newer SDK. For example, SDKAdImpression is only available on iOS 14.5. An app that uses SDKAdImpression would wrap it in `if @available` guards. While the conformance and class name is still present in the binary when it runs on iOS < 14.5, the class will be `nil`.
Make some Swift functions public so that the linker can see them when linking CEcho.

If they're not public, release builds would fail with undefined symbols errors:

Undefined symbols for architecture arm64:
  "_lookupSection", referenced from:
      __loadImageFunc in CEcho.o
  "_registerProtocolConformances", referenced from:
      __loadImageFunc in CEcho.o
  "_registerProtocols", referenced from:
      __loadImageFunc in CEcho.o
  "_registerTypeMetadata", referenced from:
      __loadImageFunc in CEcho.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
The .indirectTypeDescriptor case loaded the GOT slot as a non-optional
UnsafeRawPointer, which traps when the slot holds null (e.g. a type that
was weak-linked from a newer SDK and isn't present at runtime). Load it
as an optional and return nil instead of crashing, matching how the
.directObjCClass case already guards weak-linked classes.
Each __swift5_types entry is a RelativeDirectPointerIntPair<ContextDescriptor,
TypeReferenceKind>, packing the reference kind into the low 2 bits of the
relative offset. registerTypeMetadata treated every entry as a plain direct
relative pointer, so indirect type-descriptor records (emitted for cross-module
type references, e.g. when Foundation is imported) produced a misaligned
pointer and crashed getContextDescriptor with "load from misaligned raw
pointer". Mask off the kind bits and dereference the GOT slot for indirect
records, matching ConformanceDescriptor's type-reference handling.
- classAddressPoint/classSize grew by one word in newer Swift's class
  metadata header; Echo reads them correctly (its _ClassMetadata layout
  matches TargetClassMetadata, and instanceSize/fieldOffsets still match),
  so update the stale constants (16->24, 120->128, 136->144).
- Drop the NSObject classAddressPoint/instanceAddressPoint/alignmentMask
  assertions: reading Swift class-metadata fields off a pure Objective-C
  class inspects unrelated bytes (the old 32767 sentinel was meaningless).
  Keep the isSwiftClass == false invariant.
TypeContextDescriptorFlags.resilientSuperclassRefKind masked the 3-bit
field at bit 9 (& 0xE00) but never shifted it down, so any non-direct
kind produced a value like 0x200 that isn't a valid TypeReferenceKind
raw value and trapped the force-unwrap. This crashes on any class with a
resilient superclass referenced indirectly -- i.e. a cross-module
resilient superclass such as a Foundation base class. Shift the masked
field down by 9 (matching the sibling decode in RuntimeValues).

Regression test reflects Boat3<String>: JSONEncoder, whose Foundation
superclass is referenced indirectly; reading the kind trapped before.
The key-argument loop stored args[0].0 for every slot instead of
args[i].0, so instantiating a generic type via the buffer path (4+
arguments, or 2-3 arguments with witness tables) wrote the first type
argument into every position -- silent metadata corruption. Existing
tests missed it because they instantiated with identical arguments
(Double, Double), where storing args[0] repeatedly is accidentally
correct.

Regression test instantiates FooBaz2<Int, Double> with distinct,
witness-table-bearing arguments, forcing the buffer path and asserting
argument order is preserved.
- ValueWitnessTable.Flags: isCopyable (~Copyable) and isBitwiseBorrowable.
- TypeContextDescriptorFlags: hasLayoutString, classHasDefaultOverrideTable,
  classIsActor, classIsDefaultActor.
- FunctionMetadata.Flags: isDifferentiable, hasGlobalActor, isAsync,
  isSendable, hasExtendedFlags.
- ClassMetadata.isActor / isDefaultActor conveniences (guarded for non-Swift
  classes so they never trip descriptor's isSwiftClass precondition).

Tests cover actor classes, async/throws/Sendable/@mainactor function types,
and value-witness copyability.
The context-aware resolver already exists (TypeMetadata.type(of:)); this
adds the rest of the surface:
- demangle(_:): human-readable demangling of a Swift symbol via
  swift_demangle (e.g. "$sSiD" -> "Swift.Int").
- type(named:): context-free mangled-name -> metatype resolution
  (e.g. "Si" -> Int) via _typeByName.

Tests cover Int/String/Bool resolution, garbage input, and demangling.
Wraps the stdlib reflection-mirror SPI (the machinery Swift.Mirror uses)
for robust value/child enumeration: children(of:) yields directly-declared
stored properties (struct/class), tuple elements, and an enum's current-case
payload labeled with the case name; displayStyle(of:) reports the structural
kind. Called with T=Any exactly as the stdlib's Mirror does. Each child
value is an independent copy.

Tests cover structs, direct (non-inherited) class properties, enum payload
projection, tuples, and opaque (function) values.
Adds the call path to resolve a conformance's associated types at runtime
(e.g. Sequence's Element), which were previously only describable by name:
- ProtocolDescriptor.associatedTypeWitness(named:conformingType:witnessTable:)
  computes the requirement base and the associated-type access-function
  requirement, then invokes swift_getAssociatedTypeWitness (called via
  @_silgen_name with Swift's calling convention, which matches the runtime's
  SWIFT_CC(swift)).
- TypeMetadata.associatedType(named:conformingTo:) looks up the witness table
  (swift_conformsToProtocol) and resolves in one step.
- ProtocolDescriptor.associatedTypeNameList.

Tests resolve Item for two different conforming types and check an unknown
name returns nil.
Completes Gap Azoy#3: resolve the witness table proving an associated type
conforms to its constraint (e.g. for 'associatedtype Item: Comparable',
recover Item's Comparable witness). Adds
ProtocolDescriptor.associatedConformanceWitness(...) and the
TypeMetadata.associatedConformance(...) convenience over
swift_getAssociatedConformanceWitness, matching the access-function
requirement to the requirement signature's conformance requirements (and
conservatively returning nil when the counts diverge, e.g. refining
protocols). Also makes WitnessTable Equatable.

Test confirms the resolved associated conformance for Item == Int is
pointer-identical to Int's own Comparable witness table.
Walks the conditional trailing objects after the parameter array (in ABI
order: param flags, differentiability, global actor, extended flags,
thrown error), each at its natural alignment, to expose:
- FunctionMetadata.globalActorType (e.g. MainActor for @mainactor fns)
- FunctionMetadata.extendedFlags (ExtendedFunctionTypeFlags: typed-throws,
  isolation kind, sending result)
- FunctionMetadata.thrownErrorType (the E in throws(E))

Tests cover @mainactor isolation and typed throws (guarded for the macOS 15
runtime requirement); plain functions report nil for all three.
Adds InvertibleProtocolSet (Copyable/Escapable, with invertsCopyable/
invertsEscapable) and ExtendedFunctionTypeFlags.invertedProtocols, decoding
the inverted-protocol bitset stored in the high 16 bits of a function type's
extended flags. Combined with the value-witness isCopyable/isBitwiseBorrowable
already landed, this rounds out ~Copyable/~Escapable reflection at the
type and function-type level. (The generic-context conditional inverted-
protocol records remain, gated by descriptorFlags.hasConditionalInvertedProtocols.)

Test verifies a copyable typed-throws function inverts nothing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants