From 13c06ca2d2239cf3df7854b4fa6be608a4cd59e5 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sat, 30 Apr 2022 21:16:33 -0500 Subject: [PATCH 01/20] Remove a fatalError in ConformanceDescriptor 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`. --- Sources/Echo/Runtime/ConformanceDescriptor.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Echo/Runtime/ConformanceDescriptor.swift b/Sources/Echo/Runtime/ConformanceDescriptor.swift index 33760e8..a06aad6 100644 --- a/Sources/Echo/Runtime/ConformanceDescriptor.swift +++ b/Sources/Echo/Runtime/ConformanceDescriptor.swift @@ -64,7 +64,9 @@ public struct ConformanceDescriptor: LayoutWrapper { .assumingMemoryBound(to: CChar.self) guard let anyClass = objc_lookUpClass(ptr) else { - fatalError("No Objective-C class named \(ptr.string)") + // A conformance with a nil class means the class was weak-linked + // from a newer SDK and isn't available in this version of iOS + return nil } return reflect(anyClass) as? ObjCClassWrapperMetadata From a74a39403068cdc88697492d9675691c790acb63 Mon Sep 17 00:00:00 2001 From: Ole Begemann Date: Mon, 23 May 2022 17:36:05 +0200 Subject: [PATCH 02/20] Fix release build 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) --- Sources/Echo/Runtime/ImageInspection.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Echo/Runtime/ImageInspection.swift b/Sources/Echo/Runtime/ImageInspection.swift index 3b7178e..e749fcf 100644 --- a/Sources/Echo/Runtime/ImageInspection.swift +++ b/Sources/Echo/Runtime/ImageInspection.swift @@ -56,7 +56,7 @@ let protocolLock = NSLock() var _protocols = Set() @_cdecl("registerProtocols") -func registerProtocols(section: UnsafeRawPointer, size: Int) { +public func registerProtocols(section: UnsafeRawPointer, size: Int) { for i in 0 ..< size / 4 { let start = section.offset(of: i, as: Int32.self) let ptr = start.relativeDirectAddress(as: _ProtocolDescriptor.self) @@ -75,7 +75,7 @@ let conformanceLock = NSLock() var conformances = [UnsafeRawPointer: [ConformanceDescriptor]]() @_cdecl("registerProtocolConformances") -func registerProtocolConformances(section: UnsafeRawPointer, size: Int) { +public func registerProtocolConformances(section: UnsafeRawPointer, size: Int) { for i in 0 ..< size / 4 { let start = section.offset(of: i, as: Int32.self) let ptr = start.relativeDirectAddress(as: _ConformanceDescriptor.self) @@ -137,7 +137,7 @@ let typeLock = NSLock() var _types = Set() @_cdecl("registerTypeMetadata") -func registerTypeMetadata(section: UnsafeRawPointer, size: Int) { +public func registerTypeMetadata(section: UnsafeRawPointer, size: Int) { for i in 0 ..< size / 4 { let start = section.offset(of: i, as: Int32.self) let ptr = start.relativeDirectAddress(as: _ContextDescriptor.self) @@ -161,7 +161,7 @@ typealias mach_header_platform = mach_header #endif @_cdecl("lookupSection") -func lookupSection( +public func lookupSection( _ header: UnsafePointer?, segment: UnsafePointer?, section: UnsafePointer?, From 680ab47c76cfa1a48976cf1ea36684d4adc03333 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sun, 3 Jul 2022 16:33:56 -0500 Subject: [PATCH 03/20] Make SignedPointer implicitly-unwrapped optional --- Sources/Echo/ContextDescriptor/GenericContext.swift | 2 +- Sources/Echo/Metadata/ValueWitnessTable.swift | 2 +- Sources/Echo/Utils/SignedPointer.swift | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Echo/ContextDescriptor/GenericContext.swift b/Sources/Echo/ContextDescriptor/GenericContext.swift index 64e43fa..490c9b2 100644 --- a/Sources/Echo/ContextDescriptor/GenericContext.swift +++ b/Sources/Echo/ContextDescriptor/GenericContext.swift @@ -135,7 +135,7 @@ public struct GenericRequirementDescriptor: LayoutWrapper { and: UInt8.self ) ).signed - return ProtocolDescriptor(ptr: ptr) + return ProtocolDescriptor(ptr: ptr!) } /// If this requirement is some layout (currently can only be a class), diff --git a/Sources/Echo/Metadata/ValueWitnessTable.swift b/Sources/Echo/Metadata/ValueWitnessTable.swift index 7b95760..dee3336 100644 --- a/Sources/Echo/Metadata/ValueWitnessTable.swift +++ b/Sources/Echo/Metadata/ValueWitnessTable.swift @@ -28,7 +28,7 @@ public struct ValueWitnessTable: LayoutWrapper { let ptr: UnsafeRawPointer var _vwt: _ValueWitnessTable { - layout.signed.load(as: _ValueWitnessTable.self) + layout.signed!.load(as: _ValueWitnessTable.self) } /// Given a buffer an instance of the type in the source buffer, initialize diff --git a/Sources/Echo/Utils/SignedPointer.swift b/Sources/Echo/Utils/SignedPointer.swift index 82fda54..93cb03e 100644 --- a/Sources/Echo/Utils/SignedPointer.swift +++ b/Sources/Echo/Utils/SignedPointer.swift @@ -11,9 +11,9 @@ import CEcho // A wrapper around a pointer who will return the signed version of the wrapped // pointer through the `signed` property. struct SignedPointer { - var ptr: UnsafeRawPointer + var ptr: UnsafeRawPointer! - var signed: UnsafeRawPointer { + var signed: UnsafeRawPointer! { ptr } } From b2045312e6973b1b6332b16dcf8af4a1abbc946d Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sun, 3 Jul 2022 16:34:33 -0500 Subject: [PATCH 04/20] Class descriptor is optional; safely unwrap it --- Sources/Echo/Metadata/ClassMetadata.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Sources/Echo/Metadata/ClassMetadata.swift b/Sources/Echo/Metadata/ClassMetadata.swift index e3befeb..f7e3988 100644 --- a/Sources/Echo/Metadata/ClassMetadata.swift +++ b/Sources/Echo/Metadata/ClassMetadata.swift @@ -21,9 +21,14 @@ public struct ClassMetadata: TypeMetadata, LayoutWrapper { public let ptr: UnsafeRawPointer /// The class context descriptor that describes this class. - public var descriptor: ClassDescriptor { + public var descriptor: ClassDescriptor? { precondition(isSwiftClass) - return ClassDescriptor(ptr: layout._descriptor.signed) + + if let descriptorPtr = layout._descriptor.signed { + return ClassDescriptor(ptr: descriptorPtr) + } + + return nil } /// The Objective-C ISA pointer, if it has one. From f3f815de73f37ee535472a4dceb7264a48a50976 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sun, 3 Jul 2022 16:35:12 -0500 Subject: [PATCH 05/20] Unwrap ClassMetadata.descriptor --- Sources/Echo/Metadata/ClassMetadata.swift | 6 +++++- Sources/Echo/Metadata/TypeMetadata.swift | 4 ++-- .../Context Descriptor/ClassDescriptor.swift | 11 ++++++----- .../Context Descriptor/FieldDescriptor.swift | 2 +- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Sources/Echo/Metadata/ClassMetadata.swift b/Sources/Echo/Metadata/ClassMetadata.swift index f7e3988..1f55e79 100644 --- a/Sources/Echo/Metadata/ClassMetadata.swift +++ b/Sources/Echo/Metadata/ClassMetadata.swift @@ -106,7 +106,11 @@ public struct ClassMetadata: TypeMetadata, LayoutWrapper { /// An array of field offsets for this class's stored representation. public var fieldOffsets: [Int] { - Array(unsafeUninitializedCapacity: descriptor.numFields) { + guard let descriptor = descriptor else { + return [] + } + + return Array(unsafeUninitializedCapacity: descriptor.numFields) { let start = ptr.offset(of: descriptor.fieldOffsetVectorOffset) for i in 0 ..< descriptor.numFields { diff --git a/Sources/Echo/Metadata/TypeMetadata.swift b/Sources/Echo/Metadata/TypeMetadata.swift index bb709e6..d4fa163 100644 --- a/Sources/Echo/Metadata/TypeMetadata.swift +++ b/Sources/Echo/Metadata/TypeMetadata.swift @@ -54,7 +54,7 @@ extension TypeMetadata { case let enumMetadata as EnumMetadata: return enumMetadata.descriptor case let classMetadata as ClassMetadata: - return classMetadata.descriptor + return classMetadata.descriptor! default: fatalError("Unknown TypeMetadata conformance") } @@ -83,7 +83,7 @@ extension TypeMetadata { return ptr + MemoryLayout<_EnumMetadata>.size case let classMetadata as ClassMetadata: - return ptr.offset(of: classMetadata.descriptor.genericArgumentOffset) + return ptr.offset(of: classMetadata.descriptor!.genericArgumentOffset) default: fatalError("Unknown TypeMetadata conformance") diff --git a/Tests/EchoTests/Context Descriptor/ClassDescriptor.swift b/Tests/EchoTests/Context Descriptor/ClassDescriptor.swift index 191bf2d..547d8f3 100644 --- a/Tests/EchoTests/Context Descriptor/ClassDescriptor.swift +++ b/Tests/EchoTests/Context Descriptor/ClassDescriptor.swift @@ -16,19 +16,20 @@ class Child: Super {} extension EchoTests { func testClassDescriptor() { let metadata = reflectClass(Super.self)! - let descriptor = metadata.descriptor + let descriptor = metadata.descriptor! XCTAssertEqual(descriptor.superclass.load(as: CChar.self), 0) // nullptr XCTAssertEqual(descriptor.numFields, 1) XCTAssertEqual(descriptor.numMembers, 3) // name, init, sayHello XCTAssertEqual(descriptor.fieldOffsetVectorOffset, 10) let child = reflectClass(Child.self)! - let size = getSymbolicMangledNameLength(child.descriptor.superclass) + let childDescriptor = child.descriptor! + let size = getSymbolicMangledNameLength(childDescriptor.superclass) // 5 because symbolic prefix (1), symbol (4) XCTAssertEqual(size, 5) - XCTAssertEqual(child.descriptor.numFields, 0) - XCTAssertEqual(child.descriptor.numMembers, 0) - XCTAssertEqual(child.descriptor.fieldOffsetVectorOffset, 13) + XCTAssertEqual(childDescriptor.numFields, 0) + XCTAssertEqual(childDescriptor.numMembers, 0) + XCTAssertEqual(childDescriptor.fieldOffsetVectorOffset, 13) } } diff --git a/Tests/EchoTests/Context Descriptor/FieldDescriptor.swift b/Tests/EchoTests/Context Descriptor/FieldDescriptor.swift index 6156396..379b1f6 100644 --- a/Tests/EchoTests/Context Descriptor/FieldDescriptor.swift +++ b/Tests/EchoTests/Context Descriptor/FieldDescriptor.swift @@ -16,7 +16,7 @@ enum FieldDescriptorTests { static func testClass() throws { let metadata = reflectClass(FieldTesting.self)! - let fields = metadata.descriptor.fields + let fields = metadata.descriptor!.fields XCTAssert(fields.hasMangledTypeName) XCTAssertEqual(fields.kind, .class) From b0155b5615cdded03796b330bc18b847bc3682a4 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sun, 3 Jul 2022 17:27:13 -0500 Subject: [PATCH 06/20] Don't forcibly unwrap ClassMetadata.descriptor --- Sources/Echo/Metadata/TypeMetadata.swift | 31 ++++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/Sources/Echo/Metadata/TypeMetadata.swift b/Sources/Echo/Metadata/TypeMetadata.swift index d4fa163..1c43efa 100644 --- a/Sources/Echo/Metadata/TypeMetadata.swift +++ b/Sources/Echo/Metadata/TypeMetadata.swift @@ -41,20 +41,24 @@ extension TypeMetadata { iterateSharedObjects() #endif + guard let contextDescriptorPtr = contextDescriptor?.ptr else { + return [] + } + return conformanceLock.withLock { - Echo.conformances[contextDescriptor.ptr, default: []] + Echo.conformances[contextDescriptorPtr, default: []] } } /// The base type context descriptor for this type metadata record. - public var contextDescriptor: TypeContextDescriptor { + public var contextDescriptor: TypeContextDescriptor? { switch self { case let structMetadata as StructMetadata: return structMetadata.descriptor case let enumMetadata as EnumMetadata: return enumMetadata.descriptor case let classMetadata as ClassMetadata: - return classMetadata.descriptor! + return classMetadata.descriptor default: fatalError("Unknown TypeMetadata conformance") } @@ -74,7 +78,7 @@ extension TypeMetadata { } } - var genericArgumentPtr: UnsafeRawPointer { + var genericArgumentPtr: UnsafeRawPointer? { switch self { case is StructMetadata: return ptr + MemoryLayout<_StructMetadata>.size @@ -83,7 +87,10 @@ extension TypeMetadata { return ptr + MemoryLayout<_EnumMetadata>.size case let classMetadata as ClassMetadata: - return ptr.offset(of: classMetadata.descriptor!.genericArgumentOffset) + guard let descriptor = classMetadata.descriptor else { + return nil + } + return ptr.offset(of: descriptor.genericArgumentOffset) default: fatalError("Unknown TypeMetadata conformance") @@ -93,17 +100,17 @@ extension TypeMetadata { /// An array of types that represent the generic arguments that make up this /// type. public var genericTypes: [Any.Type] { - guard contextDescriptor.flags.isGeneric else { + guard let contextDescriptor = contextDescriptor, + contextDescriptor.flags.isGeneric, + // Explicitly only call this once because class metadata could require + // computation, so only do it once if needed. + let gap = genericArgumentPtr else { return [] } let numParams = contextDescriptor.genericContext!.numParams return Array(unsafeUninitializedCapacity: numParams) { - // Explicitly only call this once because class metadata could require - // computation, so only do it once if needed. - let gap = genericArgumentPtr - for i in 0 ..< numParams { let type = gap.load( fromByteOffset: i * MemoryLayout.stride, @@ -142,6 +149,10 @@ extension TypeMetadata { return entry! } + guard let contextDescriptor = contextDescriptor else { + return nil + } + let length = getSymbolicMangledNameLength(mangledName) let name = mangledName.assumingMemoryBound(to: UInt8.self) let type = _getTypeByMangledNameInContext( From c181c26bc27726594f4ca58d2dcfce62d2b497c9 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sun, 22 Dec 2024 03:30:07 -0600 Subject: [PATCH 07/20] Update swift-atomics --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 7dce6a4..7999616 100644 --- a/Package.swift +++ b/Package.swift @@ -11,7 +11,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/apple/swift-atomics.git", from: "0.0.1") + .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.0") ], targets: [ .target( From 502d3a405366de70328351ac2501995ad7c2b8aa Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sun, 5 Jan 2025 15:09:13 -0600 Subject: [PATCH 08/20] Ignore Tuist files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 834d020..4b2cfff 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ .build/ .swiftpm/ Package.resolved +/Echo.xcodeproj +/Project.swift From 443997a5f1649b054729399b28b803e2af479c2e Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Fri, 5 Jun 2026 17:38:35 -0500 Subject: [PATCH 09/20] Safely handle null indirect type descriptor in ConformanceDescriptor 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. --- Sources/Echo/Runtime/ConformanceDescriptor.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Sources/Echo/Runtime/ConformanceDescriptor.swift b/Sources/Echo/Runtime/ConformanceDescriptor.swift index a06aad6..c10ed41 100644 --- a/Sources/Echo/Runtime/ConformanceDescriptor.swift +++ b/Sources/Echo/Runtime/ConformanceDescriptor.swift @@ -39,18 +39,22 @@ public struct ConformanceDescriptor: LayoutWrapper { /// The context descriptor of the type being conformed. public var contextDescriptor: TypeContextDescriptor? { let start = address(for: \._typeRef) + let ptr: UnsafeRawPointer? switch flags.typeReferenceKind { case .directTypeDescriptor: - let ptr = start.relativeDirectAddress(as: _ContextDescriptor.self) - return getContextDescriptor(at: ptr) as? TypeContextDescriptor + ptr = start.relativeDirectAddress(as: _ContextDescriptor.self) case .indirectTypeDescriptor: - var ptr = start.relativeDirectAddress(as: UnsafeRawPointer.self) - ptr = ptr.load(as: UnsafeRawPointer.self) - return getContextDescriptor(at: ptr) as? TypeContextDescriptor + ptr = start.relativeDirectAddress(as: UnsafeRawPointer?.self).load(as: UnsafeRawPointer?.self) default: return nil } + + if let ptr { + return getContextDescriptor(at: ptr) as? TypeContextDescriptor + } + + return nil } /// The ObjectiveC class metadata of the type being conformed. From 3d1a88494db7796fd3631718accce2ced9f5a594 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Fri, 5 Jun 2026 17:48:01 -0500 Subject: [PATCH 10/20] Decode TypeReferenceKind in __swift5_types records Each __swift5_types entry is a RelativeDirectPointerIntPair, 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. --- Sources/Echo/Runtime/ImageInspection.swift | 29 ++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/Sources/Echo/Runtime/ImageInspection.swift b/Sources/Echo/Runtime/ImageInspection.swift index e749fcf..6a68fda 100644 --- a/Sources/Echo/Runtime/ImageInspection.swift +++ b/Sources/Echo/Runtime/ImageInspection.swift @@ -140,8 +140,33 @@ var _types = Set() public func registerTypeMetadata(section: UnsafeRawPointer, size: Int) { for i in 0 ..< size / 4 { let start = section.offset(of: i, as: Int32.self) - let ptr = start.relativeDirectAddress(as: _ContextDescriptor.self) - + + // Each record is a RelativeDirectPointerIntPair: the low 2 bits of the relative offset encode the + // reference kind and must be masked off before resolving the pointer. + // Records whose kind is an indirect reference point at a GOT slot that + // holds the real descriptor, and ObjC class references never appear here. + let raw = Int(start.load(as: Int32.self)) + let pointerOffset = raw & ~0x3 + + // A zero offset is a null/padding record; skip it. + guard pointerOffset != 0 else { + continue + } + + let addr = start + pointerOffset + let ptr: UnsafeRawPointer + + switch TypeReferenceKind(rawValue: UInt16(raw & 0x3)) { + case .directTypeDescriptor: + ptr = addr + case .indirectTypeDescriptor: + ptr = addr.load(as: UnsafeRawPointer.self) + default: + // .directObjCClass / .indirectObjCClass are never emitted into this list. + continue + } + _ = typeLock.withLock { _types.insert(ptr) } From 36228faa785242614088ee674548c3782679e504 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Fri, 5 Jun 2026 17:51:18 -0500 Subject: [PATCH 11/20] Update ClassMetadata test expectations for current Swift ABI - 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. --- Tests/EchoTests/Metadata/ClassMetadata.swift | 21 ++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Tests/EchoTests/Metadata/ClassMetadata.swift b/Tests/EchoTests/Metadata/ClassMetadata.swift index fe5c46f..c0a2dab 100644 --- a/Tests/EchoTests/Metadata/ClassMetadata.swift +++ b/Tests/EchoTests/Metadata/ClassMetadata.swift @@ -36,8 +36,12 @@ enum ClassMetadataTests { let metadata = maybeMetadata! - XCTAssertEqual(metadata.classAddressPoint, 16) - XCTAssertEqual(metadata.classSize, 120) + // classAddressPoint/classSize are runtime metadata-allocation details that + // legitimately drift between Swift versions (the class metadata header grew + // by one word after the values originally baked in here). Pin them to the + // current ABI but keep the asserts so regressions in Echo's reading surface. + XCTAssertEqual(metadata.classAddressPoint, 24) + XCTAssertEqual(metadata.classSize, 128) XCTAssertEqual(metadata.instanceAddressPoint, 0) XCTAssertEqual(metadata.instanceAlignmentMask, 7) XCTAssertEqual(metadata.instanceSize, 40) @@ -74,8 +78,8 @@ enum ClassMetadataTests { let metadata = maybeMetadata! - XCTAssertEqual(metadata.classAddressPoint, 16) - XCTAssertEqual(metadata.classSize, 136) + XCTAssertEqual(metadata.classAddressPoint, 24) + XCTAssertEqual(metadata.classSize, 144) XCTAssertEqual(metadata.instanceAddressPoint, 0) XCTAssertEqual(metadata.instanceAlignmentMask, 7) XCTAssertEqual(metadata.instanceSize, 40) @@ -119,10 +123,11 @@ enum ClassMetadataTests { XCTAssertNotNil(maybeMetadata) let metadata = maybeMetadata! - - XCTAssertEqual(metadata.classAddressPoint, 32767) - XCTAssertEqual(metadata.instanceAddressPoint, 32767) - XCTAssertEqual(metadata.instanceAlignmentMask, 32767) + + // NSObject is a pure Objective-C class: the Swift-specific class metadata + // fields (address points, alignment mask) overlap unrelated Objective-C + // class bytes and carry no meaningful value, so we don't assert on them. + // The meaningful invariant is that Echo recognizes it as a non-Swift class. XCTAssertEqual(metadata.isSwiftClass, false) } #endif From eee06b846970cf0d4f0cdf9f9665e6025e32762d Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Fri, 5 Jun 2026 19:47:46 -0500 Subject: [PATCH 12/20] Fix crash decoding resilient-superclass reference kind 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: JSONEncoder, whose Foundation superclass is referenced indirectly; reading the kind trapped before. --- .../ContextDescriptor/ContextDescriptorValues.swift | 7 ++++++- Tests/EchoTests/Metadata/ClassMetadata.swift | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Sources/Echo/ContextDescriptor/ContextDescriptorValues.swift b/Sources/Echo/ContextDescriptor/ContextDescriptorValues.swift index 1e7a45f..f793797 100644 --- a/Sources/Echo/ContextDescriptor/ContextDescriptorValues.swift +++ b/Sources/Echo/ContextDescriptor/ContextDescriptorValues.swift @@ -317,7 +317,12 @@ public struct TypeContextDescriptorFlags { /// The resilient superclass type reference kind. public var resilientSuperclassRefKind: TypeReferenceKind { - TypeReferenceKind(rawValue: UInt16(bits) & 0xE00)! + // The reference kind occupies a 3-bit field starting at bit 9, so it must + // be shifted down after masking — without the shift, any non-direct kind + // (e.g. the indirect reference used for a cross-module resilient + // superclass) produces a value like 0x200 that is not a valid + // TypeReferenceKind raw value and traps the force-unwrap. + TypeReferenceKind(rawValue: UInt16(bits & (0x7 << 9)) >> 9)! } /// Whether or not this class has any immediate members negative. diff --git a/Tests/EchoTests/Metadata/ClassMetadata.swift b/Tests/EchoTests/Metadata/ClassMetadata.swift index c0a2dab..9457f5a 100644 --- a/Tests/EchoTests/Metadata/ClassMetadata.swift +++ b/Tests/EchoTests/Metadata/ClassMetadata.swift @@ -115,6 +115,17 @@ enum ClassMetadataTests { XCTAssert(typeArraysEquals(resilientMetadata.genericTypes, [String.self])) XCTAssertNotNil(resilientMetadata.superclassType) XCTAssert(resilientMetadata.superclassType! == JSONEncoder.self) + + // Regression: the resilient-superclass reference kind is a 3-bit field at + // bit 9 and must be shifted, not just masked. Boat3's superclass + // (JSONEncoder) lives in Foundation, so it is referenced indirectly; + // reading the kind used to trap on a force-unwrapped nil before the shift. + let resilientDescriptor = resilientMetadata.descriptor! + XCTAssertTrue(resilientDescriptor.typeFlags.classHasResilientSuperclass) + XCTAssertEqual( + resilientDescriptor.typeFlags.resilientSuperclassRefKind, + .indirectTypeDescriptor + ) } #if canImport(ObjectiveC) From 09de17db90303d198ab1d3884a7dcbc7ba79a8ea Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Fri, 5 Jun 2026 19:47:46 -0500 Subject: [PATCH 13/20] Fix createMetadataAccessBuffer storing the first generic arg N times 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 with distinct, witness-table-bearing arguments, forcing the buffer path and asserting argument order is preserved. --- .../Metadata/MetadataAccessFunction.swift | 2 +- .../Metadata/MetadataAccessFunction.swift | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/Sources/Echo/Metadata/MetadataAccessFunction.swift b/Sources/Echo/Metadata/MetadataAccessFunction.swift index 83c41f7..792bb14 100644 --- a/Sources/Echo/Metadata/MetadataAccessFunction.swift +++ b/Sources/Echo/Metadata/MetadataAccessFunction.swift @@ -247,7 +247,7 @@ func createMetadataAccessBuffer( // First loop is inserting the key arguments at the front of the buffer. for i in 0 ..< args.count { buffer.storeBytes( - of: args[0].0, + of: args[i].0, toByteOffset: ptrSize * i, as: Any.Type.self ) diff --git a/Tests/EchoTests/Metadata/MetadataAccessFunction.swift b/Tests/EchoTests/Metadata/MetadataAccessFunction.swift index 707cb17..ca29f45 100644 --- a/Tests/EchoTests/Metadata/MetadataAccessFunction.swift +++ b/Tests/EchoTests/Metadata/MetadataAccessFunction.swift @@ -121,11 +121,44 @@ enum MetadataAccessFunctionTests { XCTAssertEqual(dictResponse.state, .complete) XCTAssert(dictResponse.type == [Double: Double].self) } + + static func testWitnessTableDistinctArgs() throws { + // Regression: createMetadataAccessBuffer previously stored args[0] for + // every key-argument slot, so any instantiation whose generic arguments + // differ by position came out wrong. The bug only surfaces when (a) the + // arguments are distinct and (b) witness tables are present, which forces + // the buffer path instead of the fixed-arity accessors. FooBaz2 + // with two Equatable witness tables hits exactly that path. + let equatableMetadata = reflect(_typeByName("SQ")!) as! ExistentialMetadata + let equatable = equatableMetadata.protocols[0] + + func equatableWitness(for type: Any.Type) -> WitnessTable { + for conformance in reflectStruct(type)!.conformances + where conformance.protocol == equatable { + return conformance.witnessTablePattern + } + fatalError("\(type) has no Equatable conformance") + } + + let intEquatable = equatableWitness(for: Int.self) + let doubleEquatable = equatableWitness(for: Double.self) + + let metadata = reflectStruct(FooBaz2.self)! + let accessor = metadata.descriptor.accessor + let response = accessor( + .complete, + (Int.self, intEquatable), + (Double.self, doubleEquatable) + ) + XCTAssertEqual(response.state, .complete) + XCTAssert(response.type == FooBaz2.self) + } } extension EchoTests { func testMetadataAccessFunction() throws { try MetadataAccessFunctionTests.testPlain() try MetadataAccessFunctionTests.testWitnessTable() + try MetadataAccessFunctionTests.testWitnessTableDistinctArgs() } } From bcd4a654be8cd166ee19b9a41e230ad4db9404f1 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Fri, 5 Jun 2026 20:41:46 -0500 Subject: [PATCH 14/20] Gaps #6/#7/#8: complete the metadata flag structs - 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. --- .../ContextDescriptorValues.swift | 24 ++++++- Sources/Echo/Metadata/ClassMetadata.swift | 19 ++++- Sources/Echo/Metadata/MetadataValues.swift | 43 ++++++++++++ Tests/EchoTests/Metadata/MetadataFlags.swift | 70 +++++++++++++++++++ 4 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 Tests/EchoTests/Metadata/MetadataFlags.swift diff --git a/Sources/Echo/ContextDescriptor/ContextDescriptorValues.swift b/Sources/Echo/ContextDescriptor/ContextDescriptorValues.swift index f793797..c9c08ff 100644 --- a/Sources/Echo/ContextDescriptor/ContextDescriptorValues.swift +++ b/Sources/Echo/ContextDescriptor/ContextDescriptorValues.swift @@ -314,7 +314,29 @@ public struct TypeContextDescriptorFlags { public var hasImportInfo: Bool { bits & 0x4 != 0 } - + + /// Whether this type's metadata carries a layout string (compact byte-coded + /// layout description used by the runtime for value operations). + public var hasLayoutString: Bool { + bits & 0x10 != 0 + } + + /// Whether this class has a default override table (a class-only flag). + public var classHasDefaultOverrideTable: Bool { + bits & 0x40 != 0 + } + + /// Whether this class is an `actor` (a class-only flag). + public var classIsActor: Bool { + bits & 0x80 != 0 + } + + /// Whether this class is a default actor — an actor with the default, + /// runtime-provided executor rather than a custom one (a class-only flag). + public var classIsDefaultActor: Bool { + bits & 0x100 != 0 + } + /// The resilient superclass type reference kind. public var resilientSuperclassRefKind: TypeReferenceKind { // The reference kind occupies a 3-bit field starting at bit 9, so it must diff --git a/Sources/Echo/Metadata/ClassMetadata.swift b/Sources/Echo/Metadata/ClassMetadata.swift index 1f55e79..2361b4e 100644 --- a/Sources/Echo/Metadata/ClassMetadata.swift +++ b/Sources/Echo/Metadata/ClassMetadata.swift @@ -23,14 +23,27 @@ public struct ClassMetadata: TypeMetadata, LayoutWrapper { /// The class context descriptor that describes this class. public var descriptor: ClassDescriptor? { precondition(isSwiftClass) - + if let descriptorPtr = layout._descriptor.signed { return ClassDescriptor(ptr: descriptorPtr) } - + return nil } - + + /// Whether this class is an `actor`. Always `false` for non-Swift (e.g. + /// Objective-C) classes. + public var isActor: Bool { + isSwiftClass && descriptor?.typeFlags.classIsActor == true + } + + /// Whether this class is a *default* actor — an actor using the default, + /// runtime-provided executor rather than a custom one. Always `false` for + /// non-Swift classes. + public var isDefaultActor: Bool { + isSwiftClass && descriptor?.typeFlags.classIsDefaultActor == true + } + /// The Objective-C ISA pointer, if it has one. public var isaPointer: UnsafeRawPointer? { let int = ptr.load(as: Int.self) diff --git a/Sources/Echo/Metadata/MetadataValues.swift b/Sources/Echo/Metadata/MetadataValues.swift index 7a82439..ed119b2 100644 --- a/Sources/Echo/Metadata/MetadataValues.swift +++ b/Sources/Echo/Metadata/MetadataValues.swift @@ -174,6 +174,33 @@ extension FunctionMetadata { public var isEscaping: Bool { bits & 0x4000000 != 0 } + + /// Whether or not this function is `@differentiable`. + public var isDifferentiable: Bool { + bits & 0x8000000 != 0 + } + + /// Whether or not this function is isolated to a global actor. When set, the + /// global actor type is stored in the function metadata's trailing objects. + public var hasGlobalActor: Bool { + bits & 0x10000000 != 0 + } + + /// Whether or not this is an `async` function. + public var isAsync: Bool { + bits & 0x20000000 != 0 + } + + /// Whether or not this function is `@Sendable`. + public var isSendable: Bool { + bits & 0x40000000 != 0 + } + + /// Whether or not this function carries extended flags (e.g. typed throws, + /// isolation, a sending result), stored in the trailing objects. + public var hasExtendedFlags: Bool { + bits & 0x80000000 != 0 + } } } @@ -237,6 +264,8 @@ extension ValueWitnessTable { case isNonBitwiseTakable = 0x100000 case hasEnumWitnesses = 0x200000 case incomplete = 0x400000 + case isNonCopyable = 0x800000 + case isNonBitwiseBorrowable = 0x1000000 } /// Flags as represented in bits. @@ -277,5 +306,19 @@ extension ValueWitnessTable { public var isIncomplete: Bool { bits & Flags.incomplete.rawValue != 0 } + + /// Whether or not this type is copyable. Non-copyable types (`~Copyable`) + /// must not be copied — only moved/consumed — so reflection code that copies + /// values (e.g. via `initializeWithCopy`) must check this first. + public var isCopyable: Bool { + bits & Flags.isNonCopyable.rawValue == 0 + } + + /// Whether or not this type is bitwise borrowable. A type is bitwise + /// borrowable when it is bitwise takable and the runtime has not marked it + /// non-bitwise-borrowable. + public var isBitwiseBorrowable: Bool { + isBitwiseTakable && bits & Flags.isNonBitwiseBorrowable.rawValue == 0 + } } } diff --git a/Tests/EchoTests/Metadata/MetadataFlags.swift b/Tests/EchoTests/Metadata/MetadataFlags.swift new file mode 100644 index 0000000..bd9b20b --- /dev/null +++ b/Tests/EchoTests/Metadata/MetadataFlags.swift @@ -0,0 +1,70 @@ +import XCTest +import Echo + +actor FlagActor { + var counter = 0 +} + +class FlagPlainClass { + var x = 0 +} + +enum MetadataFlagsTests { + static func testValueWitnessCopyability() throws { + // Ordinary copyable types report copyable / bitwise-borrowable. + let intFlags = reflect(Int.self).vwt.flags + XCTAssertTrue(intFlags.isCopyable) + XCTAssertTrue(intFlags.isBitwiseBorrowable) + + let stringFlags = reflect(String.self).vwt.flags + XCTAssertTrue(stringFlags.isCopyable) + } + + static func testFunctionFlags() throws { + let plain = reflect((() -> Void).self) as! FunctionMetadata + XCTAssertFalse(plain.flags.isAsync) + XCTAssertFalse(plain.flags.throws) + XCTAssertFalse(plain.flags.isSendable) + XCTAssertFalse(plain.flags.hasGlobalActor) + + let asyncFn = reflect((() async -> Void).self) as! FunctionMetadata + XCTAssertTrue(asyncFn.flags.isAsync) + + let throwingFn = reflect((() throws -> Void).self) as! FunctionMetadata + XCTAssertTrue(throwingFn.flags.throws) + + let sendableFn = reflect((@Sendable () -> Void).self) as! FunctionMetadata + XCTAssertTrue(sendableFn.flags.isSendable) + + let asyncThrows = reflect((() async throws -> Void).self) as! FunctionMetadata + XCTAssertTrue(asyncThrows.flags.isAsync) + XCTAssertTrue(asyncThrows.flags.throws) + + let globalActorFn = reflect((@MainActor () -> Void).self) as! FunctionMetadata + XCTAssertTrue(globalActorFn.flags.hasGlobalActor) + } + + static func testActorFlags() throws { + let actorMetadata = reflectClass(FlagActor.self)! + XCTAssertTrue(actorMetadata.isActor) + XCTAssertTrue(actorMetadata.isDefaultActor) + + let plainMetadata = reflectClass(FlagPlainClass.self)! + XCTAssertFalse(plainMetadata.isActor) + XCTAssertFalse(plainMetadata.isDefaultActor) + + #if canImport(ObjectiveC) + // Non-Swift classes never trap and report not-an-actor. + let nsObject = reflectClass(NSObject.self)! + XCTAssertFalse(nsObject.isActor) + #endif + } +} + +extension EchoTests { + func testMetadataFlags() throws { + try MetadataFlagsTests.testValueWitnessCopyability() + try MetadataFlagsTests.testFunctionFlags() + try MetadataFlagsTests.testActorFlags() + } +} From 489000738716212b434d391d26bb63aa55803608 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Fri, 5 Jun 2026 20:43:37 -0500 Subject: [PATCH 15/20] Gap #1: general demangler + context-free type(named:) 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. --- Sources/Echo/Runtime/Demangle.swift | 55 ++++++++++++++++++++++++++ Tests/EchoTests/Runtime/Demangle.swift | 26 ++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 Sources/Echo/Runtime/Demangle.swift create mode 100644 Tests/EchoTests/Runtime/Demangle.swift diff --git a/Sources/Echo/Runtime/Demangle.swift b/Sources/Echo/Runtime/Demangle.swift new file mode 100644 index 0000000..5f7b45f --- /dev/null +++ b/Sources/Echo/Runtime/Demangle.swift @@ -0,0 +1,55 @@ +// +// Demangle.swift +// Echo +// +// Demangling and context-free type-by-name resolution. The context-aware +// resolver already lives on `TypeMetadata.type(of:)`; these round out the +// surface with human-readable demangling and a standalone name lookup. +// + +import Foundation + +@_silgen_name("swift_demangle") +private func _stdlib_swift_demangle( + _ mangledName: UnsafePointer?, + _ mangledNameLength: UInt, + _ outputBuffer: UnsafeMutablePointer?, + _ outputBufferSize: UnsafeMutablePointer?, + _ flags: UInt32 +) -> UnsafeMutablePointer? + +/// Demangles a Swift mangled symbol name into a human-readable description. +/// +/// For example, the mangled type symbol `"$sSiD"` demangles to `"Swift.Int"`. +/// The input must be a complete Swift symbol (typically prefixed with `$s`). +/// - Parameter mangledName: A Swift mangled symbol. +/// - Returns: The human-readable demangling, or `nil` if `mangledName` is not a +/// recognized Swift symbol. +public func demangle(_ mangledName: String) -> String? { + mangledName.utf8CString.withUnsafeBufferPointer { buffer -> String? in + guard let base = buffer.baseAddress else { return nil } + + // `utf8CString` includes the trailing null, which is not part of the length. + guard let demangled = _stdlib_swift_demangle( + base, UInt(buffer.count - 1), nil, nil, 0 + ) else { + return nil + } + + defer { free(demangled) } + return String(cString: demangled) + } +} + +/// Resolves a mangled type name to its metatype, with no enclosing generic +/// context. +/// +/// Use this for self-contained names (e.g. `"Si"` → `Int`). For names that +/// reference the generic parameters of an enclosing type — such as a generic +/// type's field types — use `TypeMetadata.type(of:)`, which threads the +/// correct context and generic arguments. +/// - Parameter mangledName: A mangled Swift type name. +/// - Returns: The resolved metatype, or `nil` if it could not be resolved. +public func type(named mangledName: String) -> Any.Type? { + _typeByName(mangledName) +} diff --git a/Tests/EchoTests/Runtime/Demangle.swift b/Tests/EchoTests/Runtime/Demangle.swift new file mode 100644 index 0000000..d2de402 --- /dev/null +++ b/Tests/EchoTests/Runtime/Demangle.swift @@ -0,0 +1,26 @@ +import XCTest +import Echo + +enum DemangleTests { + static func testTypeByName() throws { + XCTAssert(type(named: "Si")! == Int.self) + XCTAssert(type(named: "SS")! == String.self) + XCTAssert(type(named: "Sb")! == Bool.self) + XCTAssertNil(type(named: "this is definitely not a mangled name")) + } + + static func testDemangle() throws { + let demangled = demangle("$sSiD") + XCTAssertNotNil(demangled) + XCTAssertTrue(demangled?.contains("Int") ?? false) + + XCTAssertNil(demangle("not a swift symbol at all")) + } +} + +extension EchoTests { + func testDemangle() throws { + try DemangleTests.testTypeByName() + try DemangleTests.testDemangle() + } +} From 016b73f435a822cceb6226ceaa6fcc58b8b200b7 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Fri, 5 Jun 2026 20:46:17 -0500 Subject: [PATCH 16/20] Gap #4: ReflectionMirror child enumeration (children(of:)) 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. --- Sources/Echo/Runtime/Reflection.swift | 101 +++++++++++++++++++++++ Tests/EchoTests/Runtime/Reflection.swift | 76 +++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 Sources/Echo/Runtime/Reflection.swift create mode 100644 Tests/EchoTests/Runtime/Reflection.swift diff --git a/Sources/Echo/Runtime/Reflection.swift b/Sources/Echo/Runtime/Reflection.swift new file mode 100644 index 0000000..2a068bd --- /dev/null +++ b/Sources/Echo/Runtime/Reflection.swift @@ -0,0 +1,101 @@ +// +// Reflection.swift +// Echo +// +// A thin wrap over the standard library's reflection-mirror runtime — the same +// SPI `Swift.Mirror` is built on. This gives child/value enumeration of an +// arbitrary value (structs, classes incl. superclasses, enums with payload +// projection, tuples) that is more robust than assembling it by hand from +// field descriptors, at the cost of depending on `@_silgen_name` SPI. +// + +internal typealias _NameFreeFunc = @convention(c) (UnsafePointer?) -> Void + +@_silgen_name("swift_reflectionMirror_count") +internal func _reflectionMirror_count(_ value: T, type: Any.Type) -> Int + +@_silgen_name("swift_reflectionMirror_subscript") +internal func _reflectionMirror_subscript( + _ value: T, + type: Any.Type, + index: Int, + outName: UnsafeMutablePointer?>, + outFreeFunc: UnsafeMutablePointer<_NameFreeFunc?> +) -> Any + +@_silgen_name("swift_reflectionMirror_displayStyle") +internal func _reflectionMirror_displayStyle(_ value: T) -> CChar + +/// One child of a reflected value: a stored property, tuple element, or an +/// enum's payload. +public struct ReflectedChild { + /// The child's label — a property name, tuple element label, or enum case + /// name. `nil` for unlabeled tuple elements. + public let label: String? + + /// The child's value. + public let value: Any +} + +/// The high-level "kind" the reflection runtime reports for a value, mirroring +/// `Mirror.DisplayStyle` for the cases the runtime distinguishes. +public enum ReflectionDisplayStyle { + case `struct` + case `class` + case `enum` + case tuple + /// No special structure (e.g. a scalar, function, or opaque value). + case none +} + +/// Enumerates the children of `subject` using the standard library's reflection +/// runtime — the same machinery `Swift.Mirror` uses. +/// +/// For a struct or class this yields its *directly declared* stored properties +/// (a class's inherited properties are not included, matching `Mirror`); for a +/// tuple, its elements; for an enum, the single child is the current case's +/// payload labeled with the case name. Each child's `value` is an independent +/// copy, so it outlives `subject`. To include inherited class fields, use the +/// `ClassMetadata` field/`storedProperty` APIs, which walk the superclass chain. +/// - Parameter subject: The value to reflect. +/// - Returns: The value's children in order. +public func children(of subject: Any) -> [ReflectedChild] { + let subjectType = Swift.type(of: subject) + let count = _reflectionMirror_count(subject, type: subjectType) + + var result = [ReflectedChild]() + result.reserveCapacity(count) + + for index in 0 ..< count { + var name: UnsafePointer? = nil + var freeFunc: _NameFreeFunc? = nil + + let value = _reflectionMirror_subscript( + subject, + type: subjectType, + index: index, + outName: &name, + outFreeFunc: &freeFunc + ) + + let label = name.map { String(cString: $0) } + freeFunc?(name) + + result.append(ReflectedChild(label: label, value: value)) + } + + return result +} + +/// The structural kind the reflection runtime reports for `subject`. +/// - Parameter subject: The value to inspect. +/// - Returns: The reflected display style. +public func displayStyle(of subject: Any) -> ReflectionDisplayStyle { + switch UInt8(bitPattern: _reflectionMirror_displayStyle(subject)) { + case UInt8(ascii: "s"): return .struct + case UInt8(ascii: "c"): return .class + case UInt8(ascii: "e"): return .enum + case UInt8(ascii: "t"): return .tuple + default: return .none + } +} diff --git a/Tests/EchoTests/Runtime/Reflection.swift b/Tests/EchoTests/Runtime/Reflection.swift new file mode 100644 index 0000000..8ec61f7 --- /dev/null +++ b/Tests/EchoTests/Runtime/Reflection.swift @@ -0,0 +1,76 @@ +import XCTest +import Echo + +struct ReflStruct { + var a: Int + var b: String +} + +class ReflBase { + var base = 0 +} + +class ReflDerived: ReflBase { + var derived = "d" +} + +enum ReflEnum { + case empty + case payload(Int) +} + +enum ReflectionTests { + static func testStructChildren() throws { + let kids = children(of: ReflStruct(a: 7, b: "x")) + XCTAssertEqual(kids.count, 2) + XCTAssertEqual(kids[0].label, "a") + XCTAssertEqual(kids[0].value as? Int, 7) + XCTAssertEqual(kids[1].label, "b") + XCTAssertEqual(kids[1].value as? String, "x") + XCTAssertEqual(displayStyle(of: ReflStruct(a: 0, b: "")), .struct) + } + + static func testClassChildrenAreDirect() throws { + // Matches Mirror: only directly-declared properties, not inherited ones. + let kids = children(of: ReflDerived()) + XCTAssertEqual(kids.map(\.label), ["derived"]) + XCTAssertEqual(kids.first?.value as? String, "d") + XCTAssertEqual(displayStyle(of: ReflDerived()), .class) + } + + static func testEnumPayloadProjection() throws { + let payload = children(of: ReflEnum.payload(42)) + XCTAssertEqual(payload.count, 1) + XCTAssertEqual(payload[0].label, "payload") + XCTAssertEqual(payload[0].value as? Int, 42) + XCTAssertEqual(displayStyle(of: ReflEnum.payload(1)), .enum) + + // A case with no payload has no children. + XCTAssertEqual(children(of: ReflEnum.empty).count, 0) + } + + static func testTupleChildren() throws { + let kids = children(of: (1, "two")) + XCTAssertEqual(kids.count, 2) + XCTAssertEqual(kids[0].value as? Int, 1) + XCTAssertEqual(kids[1].value as? String, "two") + XCTAssertEqual(displayStyle(of: (1, 2)), .tuple) + } + + static func testOpaqueValueHasNoChildren() throws { + // A function value has no reflectable structure. + let function: () -> Void = {} + XCTAssertEqual(children(of: function).count, 0) + XCTAssertEqual(displayStyle(of: function), .none) + } +} + +extension EchoTests { + func testReflectionMirror() throws { + try ReflectionTests.testStructChildren() + try ReflectionTests.testClassChildrenAreDirect() + try ReflectionTests.testEnumPayloadProjection() + try ReflectionTests.testTupleChildren() + try ReflectionTests.testOpaqueValueHasNoChildren() + } +} From c325bb975072d2d22957b44cf591798d49c27647 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Fri, 5 Jun 2026 20:52:16 -0500 Subject: [PATCH 17/20] Gap #3: associated-type witness resolution 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. --- Sources/Echo/Runtime/AssociatedType.swift | 116 +++++++++++++++++++ Tests/EchoTests/Runtime/AssociatedType.swift | 54 +++++++++ 2 files changed, 170 insertions(+) create mode 100644 Sources/Echo/Runtime/AssociatedType.swift create mode 100644 Tests/EchoTests/Runtime/AssociatedType.swift diff --git a/Sources/Echo/Runtime/AssociatedType.swift b/Sources/Echo/Runtime/AssociatedType.swift new file mode 100644 index 0000000..dbcb81c --- /dev/null +++ b/Sources/Echo/Runtime/AssociatedType.swift @@ -0,0 +1,116 @@ +// +// AssociatedType.swift +// Echo +// +// Resolving associated-type witnesses. A conformance's associated types (e.g. +// `Self.Element` for a `Sequence` conformance) are not stored statically — +// they are recovered at runtime by invoking accessor witnesses in the witness +// table. Echo already surfaces the requirement descriptors and associated-type +// names; this provides the call path to actually resolve them. +// + +// swift_getAssociatedTypeWitness is SWIFT_CC(swift); @_silgen_name calls it with +// Swift's native calling convention, which matches. MetadataResponse's layout +// ({ metadata pointer, state }) mirrors the runtime's return struct. +@_silgen_name("swift_getAssociatedTypeWitness") +internal func _swift_getAssociatedTypeWitness( + _ request: Int, + _ witnessTable: UnsafeRawPointer, + _ conformingType: UnsafeRawPointer, + _ requirementBase: UnsafeRawPointer, + _ associatedTypeRequirement: UnsafeRawPointer +) -> MetadataResponse + +extension ProtocolDescriptor { + /// The names of this protocol's associated types, in declaration order. + public var associatedTypeNameList: [String] { + associatedTypeNames + .split(separator: " ") + .map(String.init) + } + + /// Resolves the metadata bound to the associated type named `name` for the + /// conformance of `conformingType` described by `witnessTable`. + /// + /// For example, for a `Sequence` conformance this can recover `Element`. + /// - Parameters: + /// - name: The associated type's name (e.g. `"Element"`). + /// - conformingType: Metadata for the conforming type. + /// - witnessTable: The witness table for `conformingType`'s conformance to + /// this protocol. + /// - Returns: The associated type's metadata, or `nil` if this protocol has no + /// such associated type. + public func associatedTypeWitness( + named name: String, + conformingType: Metadata, + witnessTable: WitnessTable + ) -> Metadata? { + let names = associatedTypeNameList + guard let nameIndex = names.firstIndex(of: name) else { + return nil + } + + let requirements = self.requirements + guard let firstRequirement = requirements.first else { + return nil + } + + // reqBase = &requirements[0] - WitnessTableFirstRequirementOffset (= 1). + let requirementSize = MemoryLayout<_ProtocolRequirement>.size + let requirementBase = firstRequirement.ptr - requirementSize + + // The associated-type access-function requirements appear in the same order + // as the associated-type names, so pick the nameIndex-th one. + var seen = 0 + var associatedTypeRequirement: UnsafeRawPointer? + for requirement in requirements + where requirement.flags.kind == .associatedTypeAccessFunction { + if seen == nameIndex { + associatedTypeRequirement = requirement.ptr + break + } + seen += 1 + } + + guard let assocReq = associatedTypeRequirement else { + return nil + } + + let response = _swift_getAssociatedTypeWitness( + MetadataRequest.complete.bits, + witnessTable.ptr, + conformingType.ptr, + requirementBase, + assocReq + ) + + return response.metadata + } +} + +extension TypeMetadata { + /// Resolves the associated type named `name` for this type's conformance to + /// `protocolDescriptor`, looking up the witness table at runtime. + /// - Parameters: + /// - name: The associated type's name (e.g. `"Element"`). + /// - protocolDescriptor: The protocol declaring the associated type. + /// - Returns: The associated type's metadata, or `nil` if this type does not + /// conform or the protocol has no such associated type. + public func associatedType( + named name: String, + conformingTo protocolDescriptor: ProtocolDescriptor + ) -> Metadata? { + guard let witnessTable = swift_conformsToProtocol( + metadata: self, + protocol: protocolDescriptor + ) else { + return nil + } + + return protocolDescriptor.associatedTypeWitness( + named: name, + conformingType: self, + witnessTable: witnessTable + ) + } +} diff --git a/Tests/EchoTests/Runtime/AssociatedType.swift b/Tests/EchoTests/Runtime/AssociatedType.swift new file mode 100644 index 0000000..5c1db47 --- /dev/null +++ b/Tests/EchoTests/Runtime/AssociatedType.swift @@ -0,0 +1,54 @@ +import XCTest +import Echo + +protocol AssocContainer { + associatedtype Item +} + +struct IntContainer: AssocContainer { + typealias Item = Int +} + +struct StringContainer: AssocContainer { + typealias Item = String +} + +enum AssociatedTypeTests { + private static func assocContainerProtocol() throws -> ProtocolDescriptor { + let metadata = reflectStruct(IntContainer.self)! + let conformance = metadata.conformances.first { + $0.protocol.name == "AssocContainer" + } + return try XCTUnwrap(conformance?.protocol) + } + + static func testResolveAssociatedType() throws { + let proto = try assocContainerProtocol() + XCTAssertEqual(proto.associatedTypeNameList, ["Item"]) + + let intItem = reflectStruct(IntContainer.self)! + .associatedType(named: "Item", conformingTo: proto) + XCTAssertNotNil(intItem) + XCTAssert(intItem!.type == Int.self) + + // A different conforming type resolves to its own associated type. + let stringItem = reflectStruct(StringContainer.self)! + .associatedType(named: "Item", conformingTo: proto) + XCTAssertNotNil(stringItem) + XCTAssert(stringItem!.type == String.self) + } + + static func testUnknownAssociatedTypeIsNil() throws { + let proto = try assocContainerProtocol() + let result = reflectStruct(IntContainer.self)! + .associatedType(named: "DoesNotExist", conformingTo: proto) + XCTAssertNil(result) + } +} + +extension EchoTests { + func testAssociatedType() throws { + try AssociatedTypeTests.testResolveAssociatedType() + try AssociatedTypeTests.testUnknownAssociatedTypeIsNil() + } +} From 84dae8d0695e030b51657510b90349ca0ab9d4f2 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sat, 6 Jun 2026 00:06:53 -0500 Subject: [PATCH 18/20] #3: associated conformance witness resolution Completes Gap #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. --- Sources/Echo/Runtime/AssociatedType.swift | 106 +++++++++++++++++++ Sources/Echo/Runtime/WitnessTable.swift | 9 ++ Tests/EchoTests/Runtime/AssociatedType.swift | 35 ++++++ 3 files changed, 150 insertions(+) diff --git a/Sources/Echo/Runtime/AssociatedType.swift b/Sources/Echo/Runtime/AssociatedType.swift index dbcb81c..f455dc4 100644 --- a/Sources/Echo/Runtime/AssociatedType.swift +++ b/Sources/Echo/Runtime/AssociatedType.swift @@ -21,6 +21,15 @@ internal func _swift_getAssociatedTypeWitness( _ associatedTypeRequirement: UnsafeRawPointer ) -> MetadataResponse +@_silgen_name("swift_getAssociatedConformanceWitness") +internal func _swift_getAssociatedConformanceWitness( + _ witnessTable: UnsafeRawPointer, + _ conformingType: UnsafeRawPointer, + _ associatedType: UnsafeRawPointer, + _ requirementBase: UnsafeRawPointer, + _ associatedConformance: UnsafeRawPointer +) -> UnsafeRawPointer? + extension ProtocolDescriptor { /// The names of this protocol's associated types, in declaration order. public var associatedTypeNameList: [String] { @@ -86,6 +95,75 @@ extension ProtocolDescriptor { return response.metadata } + + /// Resolves the witness table proving that the associated type named + /// `associatedTypeName` conforms to `targetProtocol`, for the conformance of + /// `conformingType` described by `witnessTable`. + /// + /// For example, given `protocol P { associatedtype A: Comparable }` and a + /// type `T: P` whose `A` is `Int`, this returns `Int`'s `Comparable` witness + /// table. + /// + /// - Note: This relies on the associated-conformance access-function + /// requirements corresponding one-to-one, in order, with the + /// protocol-conformance requirements in the requirement signature. That + /// holds for protocols without inherited protocols; when the counts differ + /// (e.g. a refining protocol), this conservatively returns `nil` rather + /// than risk a wrong witness. + /// - Parameters: + /// - associatedTypeName: The associated type's name (e.g. `"A"`). + /// - targetProtocol: The protocol the associated type is constrained to. + /// - conformingType: Metadata for the conforming type. + /// - witnessTable: The witness table for `conformingType`'s conformance to + /// this protocol. + /// - Returns: The associated conformance's witness table, or `nil`. + public func associatedConformanceWitness( + ofAssociatedType associatedTypeName: String, + to targetProtocol: ProtocolDescriptor, + conformingType: Metadata, + witnessTable: WitnessTable + ) -> WitnessTable? { + guard let associatedType = associatedTypeWitness( + named: associatedTypeName, + conformingType: conformingType, + witnessTable: witnessTable + ) else { + return nil + } + + let requirements = self.requirements + guard let firstRequirement = requirements.first else { + return nil + } + let requirementSize = MemoryLayout<_ProtocolRequirement>.size + let requirementBase = firstRequirement.ptr - requirementSize + + let signatureConformances = requirementSignature.filter { + $0.flags.kind == .protocol + } + let accessRequirements = requirements.filter { + $0.flags.kind == .associatedConformanceAccessFunction + } + + guard signatureConformances.count == accessRequirements.count, + let index = signatureConformances.firstIndex(where: { + $0.protocol == targetProtocol + }) else { + return nil + } + + guard let witnessPointer = _swift_getAssociatedConformanceWitness( + witnessTable.ptr, + conformingType.ptr, + associatedType.ptr, + requirementBase, + accessRequirements[index].ptr + ) else { + return nil + } + + return WitnessTable(ptr: witnessPointer) + } } extension TypeMetadata { @@ -113,4 +191,32 @@ extension TypeMetadata { witnessTable: witnessTable ) } + + /// Resolves the witness table proving this type's associated type + /// `associatedTypeName` (from its conformance to `protocolDescriptor`) + /// conforms to `targetProtocol`, looking up the witness table at runtime. + /// - Parameters: + /// - associatedTypeName: The associated type's name (e.g. `"Element"`). + /// - targetProtocol: The protocol the associated type is constrained to. + /// - protocolDescriptor: The protocol declaring the associated type. + /// - Returns: The associated conformance's witness table, or `nil`. + public func associatedConformance( + ofAssociatedType associatedTypeName: String, + to targetProtocol: ProtocolDescriptor, + conformingTo protocolDescriptor: ProtocolDescriptor + ) -> WitnessTable? { + guard let witnessTable = swift_conformsToProtocol( + metadata: self, + protocol: protocolDescriptor + ) else { + return nil + } + + return protocolDescriptor.associatedConformanceWitness( + ofAssociatedType: associatedTypeName, + to: targetProtocol, + conformingType: self, + witnessTable: witnessTable + ) + } } diff --git a/Sources/Echo/Runtime/WitnessTable.swift b/Sources/Echo/Runtime/WitnessTable.swift index 062c018..5e6abd8 100644 --- a/Sources/Echo/Runtime/WitnessTable.swift +++ b/Sources/Echo/Runtime/WitnessTable.swift @@ -36,6 +36,15 @@ public struct WitnessTable: LayoutWrapper { } } +extension WitnessTable: Equatable { + /// Two witness tables are equal when they are the same table — i.e. the same + /// conformance instantiation. Useful for confirming, for example, that a + /// resolved associated conformance is the conforming type's own conformance. + public static func == (lhs: WitnessTable, rhs: WitnessTable) -> Bool { + lhs.ptr == rhs.ptr + } +} + struct _WitnessTable { let _conformance: ConformanceDescriptor } diff --git a/Tests/EchoTests/Runtime/AssociatedType.swift b/Tests/EchoTests/Runtime/AssociatedType.swift index 5c1db47..8735c82 100644 --- a/Tests/EchoTests/Runtime/AssociatedType.swift +++ b/Tests/EchoTests/Runtime/AssociatedType.swift @@ -13,6 +13,14 @@ struct StringContainer: AssocContainer { typealias Item = String } +protocol HasComparableItem { + associatedtype Item: Comparable +} + +struct IntItemHolder: HasComparableItem { + typealias Item = Int +} + enum AssociatedTypeTests { private static func assocContainerProtocol() throws -> ProtocolDescriptor { let metadata = reflectStruct(IntContainer.self)! @@ -44,11 +52,38 @@ enum AssociatedTypeTests { .associatedType(named: "DoesNotExist", conformingTo: proto) XCTAssertNil(result) } + + static func testResolveAssociatedConformance() throws { + let metadata = reflectStruct(IntItemHolder.self)! + let proto = try XCTUnwrap( + metadata.conformances.first { $0.protocol.name == "HasComparableItem" }?.protocol + ) + + // Comparable's protocol descriptor ("SL" is the stdlib mangling). + let comparableMetadata = reflect(_typeByName("SL")!) as! ExistentialMetadata + let comparable = comparableMetadata.protocols[0] + + let witness = try XCTUnwrap( + metadata.associatedConformance( + ofAssociatedType: "Item", + to: comparable, + conformingTo: proto + ) + ) + + // The associated conformance for Item == Int must be Int's own Comparable + // witness table. + let intComparable = try XCTUnwrap( + swift_conformsToProtocol(type: Int.self, protocol: comparable) + ) + XCTAssertEqual(witness, intComparable) + } } extension EchoTests { func testAssociatedType() throws { try AssociatedTypeTests.testResolveAssociatedType() try AssociatedTypeTests.testUnknownAssociatedTypeIsNil() + try AssociatedTypeTests.testResolveAssociatedConformance() } } From 3b08081deda0a8e5f8f070ce5929359c8ccdf0f5 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sat, 6 Jun 2026 00:13:26 -0500 Subject: [PATCH 19/20] #7: decode function metadata trailing types 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. --- Sources/Echo/Metadata/FunctionMetadata.swift | 111 +++++++++++++++++++ Tests/EchoTests/Metadata/MetadataFlags.swift | 27 +++++ 2 files changed, 138 insertions(+) diff --git a/Sources/Echo/Metadata/FunctionMetadata.swift b/Sources/Echo/Metadata/FunctionMetadata.swift index 7cae89e..34d347c 100644 --- a/Sources/Echo/Metadata/FunctionMetadata.swift +++ b/Sources/Echo/Metadata/FunctionMetadata.swift @@ -78,6 +78,117 @@ public struct FunctionMetadata: Metadata, LayoutWrapper { } } +extension FunctionMetadata { + // The conditional trailing fields after the parameter array, in ABI order: + // parameters, parameter flags, differentiability, global actor, extended + // flags, thrown error. Each is laid out at its natural alignment. + private func trailingFieldOffsets() -> ( + globalActor: Int?, extendedFlags: Int?, thrownError: Int? + ) { + let pointerSize = MemoryLayout.size + let uint32Size = MemoryLayout.size + + func aligned(_ offset: Int, to alignment: Int) -> Int { + (offset + alignment - 1) & ~(alignment - 1) + } + + // After the parameter type array. + var offset = flags.numParams * pointerSize + + if flags.hasParamFlags { + offset += flags.numParams * uint32Size + } + + if flags.isDifferentiable { + offset = aligned(offset, to: pointerSize) + offset += pointerSize + } + + var globalActor: Int? + if flags.hasGlobalActor { + offset = aligned(offset, to: pointerSize) + globalActor = offset + offset += pointerSize + } + + var extendedFlags: Int? + if flags.hasExtendedFlags { + offset = aligned(offset, to: uint32Size) + extendedFlags = offset + offset += uint32Size + } + + var thrownError: Int? + if let extendedFlags, + trailing.load(fromByteOffset: extendedFlags, as: ExtendedFunctionTypeFlags.self) + .isTypedThrows { + offset = aligned(offset, to: pointerSize) + thrownError = offset + offset += pointerSize + } + + return (globalActor, extendedFlags, thrownError) + } + + /// The global actor this function type is isolated to (e.g. `MainActor`), if + /// it has one (`flags.hasGlobalActor`). + public var globalActorType: Any.Type? { + guard let offset = trailingFieldOffsets().globalActor else { + return nil + } + + return trailing.load(fromByteOffset: offset, as: Any.Type.self) + } + + /// The extended function-type flags (typed throws, isolation kind, sending + /// result), if present (`flags.hasExtendedFlags`). + public var extendedFlags: ExtendedFunctionTypeFlags? { + guard let offset = trailingFieldOffsets().extendedFlags else { + return nil + } + + return trailing.load(fromByteOffset: offset, as: ExtendedFunctionTypeFlags.self) + } + + /// The error type of a typed-throws function (e.g. `MyError` for + /// `throws(MyError)`), if it has one. + public var thrownErrorType: Any.Type? { + guard let offset = trailingFieldOffsets().thrownError else { + return nil + } + + return trailing.load(fromByteOffset: offset, as: Any.Type.self) + } +} + +/// The extended flags carried by some function types, beyond the 32-bit +/// `FunctionMetadata.Flags`. Present only when `flags.hasExtendedFlags`. +public struct ExtendedFunctionTypeFlags { + /// Flags as represented in bits. + public let bits: UInt32 + + /// Whether this function uses typed throws (`throws(E)`); if so, the thrown + /// error type is available via `FunctionMetadata.thrownErrorType`. + public var isTypedThrows: Bool { + bits & 0x1 != 0 + } + + /// Whether this function has `@isolated(any)` isolation. + public var isIsolatedAny: Bool { + bits & 0xE == 0x2 + } + + /// Whether this function is `nonisolated(nonsending)`. + public var isNonIsolatedNonsending: Bool { + bits & 0xE == 0x4 + } + + /// Whether this function has a `sending` result. + public var hasSendingResult: Bool { + bits & 0x10 != 0 + } +} + extension FunctionMetadata: Equatable {} struct _FunctionMetadata { diff --git a/Tests/EchoTests/Metadata/MetadataFlags.swift b/Tests/EchoTests/Metadata/MetadataFlags.swift index bd9b20b..9730ba7 100644 --- a/Tests/EchoTests/Metadata/MetadataFlags.swift +++ b/Tests/EchoTests/Metadata/MetadataFlags.swift @@ -9,6 +9,10 @@ class FlagPlainClass { var x = 0 } +enum FlagThrownError: Error { + case boom +} + enum MetadataFlagsTests { static func testValueWitnessCopyability() throws { // Ordinary copyable types report copyable / bitwise-borrowable. @@ -44,6 +48,28 @@ enum MetadataFlagsTests { XCTAssertTrue(globalActorFn.flags.hasGlobalActor) } + static func testFunctionTrailingTypes() throws { + // Plain function: none of the conditional trailing fields. + let plain = reflect((() -> Void).self) as! FunctionMetadata + XCTAssertNil(plain.globalActorType) + XCTAssertNil(plain.extendedFlags) + XCTAssertNil(plain.thrownErrorType) + + // Global actor isolation carries the actor type in the trailing objects. + let mainActorFn = reflect((@MainActor () -> Void).self) as! FunctionMetadata + XCTAssertTrue(mainActorFn.flags.hasGlobalActor) + XCTAssert(mainActorFn.globalActorType == MainActor.self) + + // Typed throws carries extended flags + the thrown error type. The runtime + // support for typed-throws function metadata requires macOS 15 / iOS 18. + if #available(macOS 15, iOS 18, tvOS 18, watchOS 11, *) { + let typedThrows = reflect((() throws(FlagThrownError) -> Void).self) as! FunctionMetadata + XCTAssertTrue(typedThrows.flags.hasExtendedFlags) + XCTAssertEqual(typedThrows.extendedFlags?.isTypedThrows, true) + XCTAssert(typedThrows.thrownErrorType == FlagThrownError.self) + } + } + static func testActorFlags() throws { let actorMetadata = reflectClass(FlagActor.self)! XCTAssertTrue(actorMetadata.isActor) @@ -65,6 +91,7 @@ extension EchoTests { func testMetadataFlags() throws { try MetadataFlagsTests.testValueWitnessCopyability() try MetadataFlagsTests.testFunctionFlags() + try MetadataFlagsTests.testFunctionTrailingTypes() try MetadataFlagsTests.testActorFlags() } } From fc3d46b0206b4359d191728cecec5dc2f8e8b9f4 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sat, 6 Jun 2026 00:18:09 -0500 Subject: [PATCH 20/20] #8: decode invertible-protocol set in extended function flags 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. --- Sources/Echo/Metadata/FunctionMetadata.swift | 31 ++++++++++++++++++++ Tests/EchoTests/Metadata/MetadataFlags.swift | 6 +++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Sources/Echo/Metadata/FunctionMetadata.swift b/Sources/Echo/Metadata/FunctionMetadata.swift index 34d347c..54e938b 100644 --- a/Sources/Echo/Metadata/FunctionMetadata.swift +++ b/Sources/Echo/Metadata/FunctionMetadata.swift @@ -187,6 +187,37 @@ public struct ExtendedFunctionTypeFlags { public var hasSendingResult: Bool { bits & 0x10 != 0 } + + /// The set of invertible protocols (`Copyable`, `Escapable`) whose + /// requirement is *inverted* for this function type — i.e. that the function + /// does not require of its noncopyable/nonescapable inputs. Stored in the + /// high 16 bits. + public var invertedProtocols: InvertibleProtocolSet { + InvertibleProtocolSet(bits: UInt16(truncatingIfNeeded: bits >> 16)) + } +} + +/// A set of invertible protocols (`Copyable`, `Escapable`). When a protocol is +/// present in the set, its requirement is *inverted* — i.e. the corresponding +/// `~Copyable`/`~Escapable` capability is permitted rather than required. +public struct InvertibleProtocolSet { + /// Flags as represented in bits. + public let bits: UInt16 + + /// Whether `Copyable` is inverted (i.e. `~Copyable` is permitted). + public var invertsCopyable: Bool { + bits & (1 << 0) != 0 + } + + /// Whether `Escapable` is inverted (i.e. `~Escapable` is permitted). + public var invertsEscapable: Bool { + bits & (1 << 1) != 0 + } + + /// Whether the set is empty — no invertible-protocol requirement is inverted. + public var isEmpty: Bool { + bits == 0 + } } extension FunctionMetadata: Equatable {} diff --git a/Tests/EchoTests/Metadata/MetadataFlags.swift b/Tests/EchoTests/Metadata/MetadataFlags.swift index 9730ba7..58b1e9b 100644 --- a/Tests/EchoTests/Metadata/MetadataFlags.swift +++ b/Tests/EchoTests/Metadata/MetadataFlags.swift @@ -65,8 +65,12 @@ enum MetadataFlagsTests { if #available(macOS 15, iOS 18, tvOS 18, watchOS 11, *) { let typedThrows = reflect((() throws(FlagThrownError) -> Void).self) as! FunctionMetadata XCTAssertTrue(typedThrows.flags.hasExtendedFlags) - XCTAssertEqual(typedThrows.extendedFlags?.isTypedThrows, true) + let extended = try XCTUnwrap(typedThrows.extendedFlags) + XCTAssertTrue(extended.isTypedThrows) XCTAssert(typedThrows.thrownErrorType == FlagThrownError.self) + // A copyable/escapable function inverts nothing. + XCTAssertTrue(extended.invertedProtocols.isEmpty) + XCTAssertFalse(extended.invertedProtocols.invertsCopyable) } }