From f369429b4d3fa09528901ddbc34d1fc8595105cb Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Fri, 5 Jun 2026 20:48:01 -0500 Subject: [PATCH 1/4] Gap #10: decode variadic/value generic param & requirement kinds GenericParameterKind only had .type, and GenericRequirementKind lacked .sameShape/.invertedProtocols, so the force-unwrapped rawValue decode crashed on any variadic-generic (each T), integer-generic (let N: Int), or noncopyable-generic type. Add: - GenericParameterKind: .typePack, .value - GenericRequirementKind: .sameShape, .invertedProtocols Test reflects a variadic-generic type and confirms its pack parameter decodes as .typePack without trapping. --- .../ContextDescriptorValues.swift | 16 ++++++++ .../Context Descriptor/GenericKinds.swift | 37 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 Tests/EchoTests/Context Descriptor/GenericKinds.swift diff --git a/Sources/Echo/ContextDescriptor/ContextDescriptorValues.swift b/Sources/Echo/ContextDescriptor/ContextDescriptorValues.swift index 1e7a45f..e10e83a 100644 --- a/Sources/Echo/ContextDescriptor/ContextDescriptorValues.swift +++ b/Sources/Echo/ContextDescriptor/ContextDescriptorValues.swift @@ -125,7 +125,14 @@ extension GenericMetadataPattern { /// A discriminator to determine what type of parameter a generic parameter is. public enum GenericParameterKind: UInt8 { + /// An ordinary type parameter, e.g. `T`. case type = 0x0 + + /// A type parameter pack, e.g. `each T` (variadic generics). + case typePack = 0x1 + + /// A value type parameter, e.g. `let N: Int` (integer generic parameters). + case value = 0x2 } /// The flags that describe a generic parameter. @@ -157,6 +164,15 @@ public enum GenericRequirementKind: UInt8 { case sameType = 0x1 case baseClass = 0x2 case sameConformance = 0x3 + + /// A same-shape requirement between two generic parameter packs (variadic + /// generics). + case sameShape = 0x4 + + /// A requirement listing the invertible-protocol checks (e.g. `Copyable`, + /// `Escapable`) that are *inverted* for a parameter — an "anti-requirement". + case invertedProtocols = 0x5 + case layout = 0x1F } diff --git a/Tests/EchoTests/Context Descriptor/GenericKinds.swift b/Tests/EchoTests/Context Descriptor/GenericKinds.swift new file mode 100644 index 0000000..bdd3113 --- /dev/null +++ b/Tests/EchoTests/Context Descriptor/GenericKinds.swift @@ -0,0 +1,37 @@ +import XCTest +import Echo + +struct PlainGeneric { + var t: T + var u: U +} + +// Variadic generics (Swift 5.9+): a type parameter pack. +struct PackGeneric {} + +enum GenericKindsTests { + static func testOrdinaryParameterKinds() throws { + let metadata = reflectStruct(PlainGeneric.self)! + let context = try XCTUnwrap(metadata.descriptor.genericContext) + XCTAssertEqual(context.parameters.count, 2) + for parameter in context.parameters { + XCTAssertEqual(parameter.kind, .type) + } + } + + static func testParameterPackKind() throws { + // Reflecting a variadic-generic type must not crash decoding the pack + // parameter's kind (previously force-unwrapped against a .type-only enum). + let metadata = reflectStruct(PackGeneric.self)! + let context = try XCTUnwrap(metadata.descriptor.genericContext) + let kinds = context.parameters.map(\.kind) + XCTAssertTrue(kinds.contains(.typePack)) + } +} + +extension EchoTests { + func testGenericKinds() throws { + try GenericKindsTests.testOrdinaryParameterKinds() + try GenericKindsTests.testParameterPackKind() + } +} From 718b6fe83b8172dae8b8a471eccaac8b9e6b52b6 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sat, 6 Jun 2026 00:10:33 -0500 Subject: [PATCH 2/4] #12: opaque type underlying-type realization OpaqueDescriptor.underlyingType(at:genericArguments:) resolves a 'some P' opaque type's mangled underlying-type name into a live metatype via the in-context resolver, threading the opaque descriptor's generic context. Out-of-range indices return nil. Test enumerates the image's opaque descriptors and confirms two opaque-returning functions' underlying types realize to Int and String. --- .../ContextDescriptor/OpaqueDescriptor.swift | 33 ++++++++++++++++ .../Context Descriptor/OpaqueDescriptor.swift | 38 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 Tests/EchoTests/Context Descriptor/OpaqueDescriptor.swift diff --git a/Sources/Echo/ContextDescriptor/OpaqueDescriptor.swift b/Sources/Echo/ContextDescriptor/OpaqueDescriptor.swift index c8d9fae..b16507e 100644 --- a/Sources/Echo/ContextDescriptor/OpaqueDescriptor.swift +++ b/Sources/Echo/ContextDescriptor/OpaqueDescriptor.swift @@ -43,6 +43,39 @@ public struct OpaqueDescriptor: ContextDescriptor, LayoutWrapper { $1 = numUnderlyingTypes } } + + /// Realizes the concrete underlying type for the underlying-type entry at + /// `index`, resolving its mangled name in this opaque descriptor's context. + /// + /// An opaque type (`some P`) stores its underlying type only as a mangled + /// name; this turns it into a live metatype. `genericArguments` supplies the + /// type arguments when the opaque type is nested in a generic context — pass + /// `nil` (the default) for a self-contained underlying type, e.g. a + /// non-generic `func f() -> some P` returning a concrete type. + /// - Parameters: + /// - index: Which underlying type (`0 ..< numUnderlyingTypes`). + /// - genericArguments: Pointer to the generic arguments, if any. + /// - Returns: The underlying type's metatype, or `nil` if out of range or + /// unresolvable. + public func underlyingType( + at index: Int, + genericArguments: UnsafeRawPointer? = nil + ) -> Any.Type? { + let names = underlyingTypeMangledNames + guard names.indices.contains(index) else { + return nil + } + + let mangledName = names[index] + let length = getSymbolicMangledNameLength(mangledName) + let name = mangledName.assumingMemoryBound(to: UInt8.self) + return _getTypeByMangledNameInContext( + name, + UInt(length), + genericContext: flags.isGeneric ? genericContext!.ptr : nil, + genericArguments: genericArguments + ) + } } extension OpaqueDescriptor: Equatable {} diff --git a/Tests/EchoTests/Context Descriptor/OpaqueDescriptor.swift b/Tests/EchoTests/Context Descriptor/OpaqueDescriptor.swift new file mode 100644 index 0000000..91285fb --- /dev/null +++ b/Tests/EchoTests/Context Descriptor/OpaqueDescriptor.swift @@ -0,0 +1,38 @@ +import XCTest +import Echo + +// Opaque-returning functions emit opaque type descriptors into the image. +// `@inline(never)` keeps them from being optimized away. +@inline(never) +func opaqueReturningInt() -> some Equatable { 42 } + +@inline(never) +func opaqueReturningString() -> some Equatable { "an opaque string" } + +enum OpaqueDescriptorTests { + static func testUnderlyingTypeRealization() throws { + // Reference the functions so their opaque descriptors are emitted. + _ = opaqueReturningInt() + _ = opaqueReturningString() + + var underlyingTypeNames = Set() + for type in Echo.types { + guard let opaque = type as? OpaqueDescriptor else { continue } + if let underlying = opaque.underlyingType(at: 0) { + underlyingTypeNames.insert(String(describing: underlying)) + } + // Out-of-range indices resolve to nil rather than crashing. + XCTAssertNil(opaque.underlyingType(at: opaque.numUnderlyingTypes)) + } + + // The two functions above contribute `Int` and `String` underlying types. + XCTAssertTrue(underlyingTypeNames.contains("Int")) + XCTAssertTrue(underlyingTypeNames.contains("String")) + } +} + +extension EchoTests { + func testOpaqueDescriptor() throws { + try OpaqueDescriptorTests.testUnderlyingTypeRealization() + } +} From 19d707b0019066ce8d8b3785010b7c6408630ccf Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sat, 6 Jun 2026 00:16:30 -0500 Subject: [PATCH 3/4] #11: decode variadic-generic pack shapes (+ #10 sameShape payload) Adds GenericContext.descriptorFlags (hasTypePacks / hasConditionalInverted Protocols / hasValues), packShapeHeader, and packShapeDescriptors, decoding the GenericPackShapeHeader + GenericPackShapeDescriptor records that trail a variadic-generic context's parameters and requirements. Each descriptor reports its pack kind (metadata / witness-table), generic-argument index, and same-shape equivalence class. Also relaxes GenericRequirementDescriptor. mangledTypeName to accept .sameShape requirements. Tests confirm PackGeneric exposes a metadata pack shape and a non-pack generic exposes none. --- .../ContextDescriptor/GenericContext.swift | 114 +++++++++++++++++- .../Context Descriptor/GenericKinds.swift | 23 ++++ 2 files changed, 134 insertions(+), 3 deletions(-) diff --git a/Sources/Echo/ContextDescriptor/GenericContext.swift b/Sources/Echo/ContextDescriptor/GenericContext.swift index 64e43fa..6cabf5c 100644 --- a/Sources/Echo/ContextDescriptor/GenericContext.swift +++ b/Sources/Echo/ContextDescriptor/GenericContext.swift @@ -89,6 +89,112 @@ public struct GenericContext: LayoutWrapper { let base = MemoryLayout<_GenericContextDescriptorHeader>.size return base + parameterSize + requirementSize } + + /// Flags describing what trailing records this generic context carries (type + /// packs, conditional inverted protocols, value parameters). + public var descriptorFlags: GenericContextDescriptorFlags { + // The header's 4th word was originally "NumExtraArguments" (always 0 in + // pre-5.8 runtimes) and is now repurposed as the flags word. + GenericContextDescriptorFlags(bits: layout._numExtraArguments) + } + + /// The pack-shape header, present when this context has type-pack parameters + /// (variadic generics). Describes how many packs and same-shape equivalence + /// classes the context has. + public var packShapeHeader: GenericPackShapeHeader? { + guard descriptorFlags.hasTypePacks else { + return nil + } + + // Trails the parameter and requirement arrays. + let offset = parameterSize + requirementSize + return (trailing + offset).load(as: GenericPackShapeHeader.self) + } + + /// The pack-shape descriptors, one per pack metadata/witness-table argument, + /// each tagged with its same-shape equivalence class. Empty for contexts + /// without type packs. + public var packShapeDescriptors: [GenericPackShapeDescriptor] { + guard let header = packShapeHeader else { + return [] + } + + let base = trailing + parameterSize + requirementSize + + MemoryLayout.size + + return Array(unsafeUninitializedCapacity: Int(header.numShapeClasses)) { + for i in 0 ..< Int(header.numShapeClasses) { + $0[i] = base.load( + fromByteOffset: i * MemoryLayout.stride, + as: GenericPackShapeDescriptor.self + ) + } + + $1 = Int(header.numShapeClasses) + } + } +} + +/// Flags describing the trailing records a generic context carries. +public struct GenericContextDescriptorFlags { + /// Flags as represented in bits. + public let bits: UInt16 + + /// Whether at least one generic parameter is a type pack (`each T`), in which + /// case the context carries a trailing `GenericPackShapeHeader`. + public var hasTypePacks: Bool { + bits & 0x1 != 0 + } + + /// Whether the context has conditional conformances to inverted protocols + /// (e.g. conditional `~Copyable`), with trailing inverted-protocol records. + public var hasConditionalInvertedProtocols: Bool { + bits & 0x2 != 0 + } + + /// Whether the context has at least one value generic parameter + /// (`let N: Int`), with a trailing value header. + public var hasValues: Bool { + bits & 0x4 != 0 + } +} + +/// The header preceding a generic context's pack-shape descriptors. +public struct GenericPackShapeHeader { + /// The number of generic parameters and conformance requirements that are + /// packs. + public let numPacks: UInt16 + + /// The number of equivalence classes in the same-shape relation. + public let numShapeClasses: UInt16 +} + +/// What a pack-shape descriptor describes — a metadata pack or a witness-table +/// pack. +public enum GenericPackKind: UInt16 { + case metadata = 0 + case witnessTable = 1 +} + +/// Describes a single generic parameter/requirement pack and its same-shape +/// equivalence class. +public struct GenericPackShapeDescriptor { + let _kind: UInt16 + + /// The index of this metadata pack or witness-table pack in the generic + /// arguments array. + public let index: UInt16 + + /// The equivalence class of this pack under the same-shape relation + /// (`< GenericPackShapeHeader.numShapeClasses`). + public let shapeClass: UInt16 + + let _unused: UInt16 + + /// Whether this describes a metadata pack or a witness-table pack. + public var kind: GenericPackKind { + GenericPackKind(rawValue: _kind) ?? .metadata + } } /// This descriptor describes any generic requirement in either a generic @@ -116,10 +222,12 @@ public struct GenericRequirementDescriptor: LayoutWrapper { address(for: \._param) } - /// If this requirement is a sameType or baseClass, this is the mangled name - /// for the type that's being constrained. + /// If this requirement is a `sameType`, `baseClass`, or `sameShape`, this is + /// the mangled name for the type (or, for `sameShape`, the parameter pack) + /// that's being constrained against. public var mangledTypeName: UnsafeRawPointer { - assert(flags.kind == .sameType || flags.kind == .baseClass) + assert(flags.kind == .sameType || flags.kind == .baseClass + || flags.kind == .sameShape) let addr = address(for: \._requirement) return addr.relativeDirectAddress(as: CChar.self) } diff --git a/Tests/EchoTests/Context Descriptor/GenericKinds.swift b/Tests/EchoTests/Context Descriptor/GenericKinds.swift index bdd3113..b661f6b 100644 --- a/Tests/EchoTests/Context Descriptor/GenericKinds.swift +++ b/Tests/EchoTests/Context Descriptor/GenericKinds.swift @@ -27,11 +27,34 @@ enum GenericKindsTests { let kinds = context.parameters.map(\.kind) XCTAssertTrue(kinds.contains(.typePack)) } + + static func testPackShapeDescriptors() throws { + let metadata = reflectStruct(PackGeneric.self)! + let context = try XCTUnwrap(metadata.descriptor.genericContext) + + XCTAssertTrue(context.descriptorFlags.hasTypePacks) + let header = try XCTUnwrap(context.packShapeHeader) + XCTAssertGreaterThanOrEqual(Int(header.numShapeClasses), 1) + XCTAssertEqual(context.packShapeDescriptors.count, Int(header.numShapeClasses)) + // The single `each Element` parameter is a metadata pack. + XCTAssertTrue(context.packShapeDescriptors.contains { $0.kind == .metadata }) + } + + static func testNonPackHasNoShapes() throws { + let metadata = reflectStruct(PlainGeneric.self)! + let context = try XCTUnwrap(metadata.descriptor.genericContext) + + XCTAssertFalse(context.descriptorFlags.hasTypePacks) + XCTAssertNil(context.packShapeHeader) + XCTAssertTrue(context.packShapeDescriptors.isEmpty) + } } extension EchoTests { func testGenericKinds() throws { try GenericKindsTests.testOrdinaryParameterKinds() try GenericKindsTests.testParameterPackKind() + try GenericKindsTests.testPackShapeDescriptors() + try GenericKindsTests.testNonPackHasNoShapes() } } From c13ef33edffd02b3ed420943654b548231d18274 Mon Sep 17 00:00:00 2001 From: Tanner Bennett Date: Sat, 6 Jun 2026 00:24:13 -0500 Subject: [PATCH 4/4] #16: extended (generalized) existential metadata + crash fix Reflecting a parameterized-protocol existential like 'any Collection' crashed Echo with 'unknown kind 775' (ExtendedExistential = 0x307). Adds the .extendedExistential MetadataKind, an ExtendedExistentialMetadata type exposing the existential shape, and ExtendedExistentialTypeShape with flags (specialKind: opaque/class/metatype/explicit-layout; hasGeneralization Signature / hasTypeExpression / hasSuggestedValueWitnesses), wired into getMetadata. Test reflects 'any Collection': kind .extendedExistential, the shape reports a generalization signature and opaque special kind, and the metadata round-trips; a plain 'any Equatable' still uses the classic layout. --- .../ExtendedExistentialMetadata.swift | 104 ++++++++++++++++++ Sources/Echo/Metadata/Metadata.swift | 2 + Sources/Echo/Metadata/MetadataValues.swift | 6 +- .../ExtendedExistentialMetadata.swift | 36 ++++++ 4 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 Sources/Echo/Metadata/ExtendedExistentialMetadata.swift create mode 100644 Tests/EchoTests/Metadata/ExtendedExistentialMetadata.swift diff --git a/Sources/Echo/Metadata/ExtendedExistentialMetadata.swift b/Sources/Echo/Metadata/ExtendedExistentialMetadata.swift new file mode 100644 index 0000000..21548cc --- /dev/null +++ b/Sources/Echo/Metadata/ExtendedExistentialMetadata.swift @@ -0,0 +1,104 @@ +// +// ExtendedExistentialMetadata.swift +// Echo +// +// Metadata for a generalized / constrained existential — e.g. a +// parameterized-protocol existential like `any Collection`. These carry +// a *shape* describing the existential's requirement and generalization +// signatures, distinct from the classic protocol-composition layout modeled by +// `ExistentialMetadata`. Reflecting one previously crashed Echo with an +// "unknown kind 775". +// + +/// The metadata structure that represents a generalized existential type, such +/// as `any Collection` or other constrained / parameterized-protocol +/// existentials. +/// +/// ABI Stability: Unstable across all platforms +public struct ExtendedExistentialMetadata: Metadata, LayoutWrapper { + typealias Layout = _ExtendedExistentialMetadata + + /// Backing extended existential metadata pointer. + public let ptr: UnsafeRawPointer + + /// The existential shape describing this existential's constraints and + /// generalization signature. + public var shape: ExtendedExistentialTypeShape { + ExtendedExistentialTypeShape(ptr: layout._shape.signed) + } +} + +extension ExtendedExistentialMetadata: Equatable {} + +struct _ExtendedExistentialMetadata { + let _kind: Int + let _shape: SignedPointer<_ExtendedExistentialTypeShape> +} + +/// Describes the shape of a generalized existential: how its value is +/// represented and what optional records (generalization signature, type +/// expression, suggested value witnesses) it carries. +/// +/// ABI Stability: Unstable across all platforms +public struct ExtendedExistentialTypeShape: LayoutWrapper { + typealias Layout = _ExtendedExistentialTypeShape + + /// Backing shape pointer. + let ptr: UnsafeRawPointer + + /// Flags describing this existential shape. + public var flags: Flags { + layout._flags + } +} + +extension ExtendedExistentialTypeShape { + /// Flags for an extended existential type shape. + public struct Flags { + /// Flags as represented in bits. + public let bits: UInt32 + + /// How the existential value is represented. + public var specialKind: SpecialKind { + SpecialKind(rawValue: UInt8(bits & 0xFF)) ?? .none + } + + /// Whether the shape has a generalization signature — the substituted + /// generic arguments of the existential (e.g. the `Int` in + /// `any Collection`). + public var hasGeneralizationSignature: Bool { + bits & 0x100 != 0 + } + + /// Whether the shape carries a mangled type expression. + public var hasTypeExpression: Bool { + bits & 0x200 != 0 + } + + /// Whether the shape carries suggested value witnesses. + public var hasSuggestedValueWitnesses: Bool { + bits & 0x400 != 0 + } + } + + /// How a generalized existential's value is represented. + public enum SpecialKind: UInt8 { + /// An opaque value existential (the general case). + case none = 0 + + /// A class existential — a single retainable pointer. + case `class` = 1 + + /// A metatype existential. + case metatype = 2 + + /// An existential with an explicitly-described layout. + case explicitLayout = 3 + } +} + +extension ExtendedExistentialTypeShape: Equatable {} + +struct _ExtendedExistentialTypeShape { + let _flags: ExtendedExistentialTypeShape.Flags +} diff --git a/Sources/Echo/Metadata/Metadata.swift b/Sources/Echo/Metadata/Metadata.swift index f87bc5e..66e66c1 100644 --- a/Sources/Echo/Metadata/Metadata.swift +++ b/Sources/Echo/Metadata/Metadata.swift @@ -105,6 +105,8 @@ func getMetadata(at ptr: UnsafeRawPointer) -> Metadata { return ObjCClassWrapperMetadata(ptr: ptr) case .existentialMetatype: return ExistentialMetatypeMetadata(ptr: ptr) + case .extendedExistential: + return ExtendedExistentialMetadata(ptr: ptr) case .heapLocalVariable: return HeapLocalVariableMetadata(ptr: ptr) case .heapGenericLocalVariable: diff --git a/Sources/Echo/Metadata/MetadataValues.swift b/Sources/Echo/Metadata/MetadataValues.swift index 7a82439..0d1d955 100644 --- a/Sources/Echo/Metadata/MetadataValues.swift +++ b/Sources/Echo/Metadata/MetadataValues.swift @@ -42,7 +42,11 @@ public enum MetadataKind: Int { // (6 | Flags.isRuntimePrivate | Flags.isNonHeap) case existentialMetatype = 774 - + + // (7 | Flags.isRuntimePrivate | Flags.isNonHeap) + // A generalized/constrained existential, e.g. `any Collection`. + case extendedExistential = 775 + // (0 | Flags.isNonType) case heapLocalVariable = 1024 diff --git a/Tests/EchoTests/Metadata/ExtendedExistentialMetadata.swift b/Tests/EchoTests/Metadata/ExtendedExistentialMetadata.swift new file mode 100644 index 0000000..acbde89 --- /dev/null +++ b/Tests/EchoTests/Metadata/ExtendedExistentialMetadata.swift @@ -0,0 +1,36 @@ +import XCTest +import Echo + +enum ExtendedExistentialMetadataTests { + static func testParameterizedExistential() throws { + // `any Collection` is a parameterized-protocol existential, which uses + // the extended-existential metadata (kind 775). Reflecting it used to crash + // Echo with "unknown kind 775". + let metadata = reflect((any Collection).self) + XCTAssertEqual(metadata.kind, .extendedExistential) + + let extended = try XCTUnwrap(metadata as? ExtendedExistentialMetadata) + // Collection carries Int as a generalization argument. + XCTAssertTrue(extended.shape.flags.hasGeneralizationSignature) + // It is an opaque value existential, not a class/metatype one. + XCTAssertEqual(extended.shape.flags.specialKind, .none) + + // The metadata round-trips back to the same type. + XCTAssert(metadata.type == (any Collection).self) + } + + static func testPlainExistentialIsClassic() throws { + // A non-parameterized existential keeps the classic protocol-composition + // metadata, not the extended one. + let metadata = reflect((any Equatable).self) + XCTAssertEqual(metadata.kind, .existential) + XCTAssertTrue(metadata is ExistentialMetadata) + } +} + +extension EchoTests { + func testExtendedExistentialMetadata() throws { + try ExtendedExistentialMetadataTests.testParameterizedExistential() + try ExtendedExistentialMetadataTests.testPlainExistentialIsClassic() + } +}