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/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/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/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/Context Descriptor/GenericKinds.swift b/Tests/EchoTests/Context Descriptor/GenericKinds.swift new file mode 100644 index 0000000..b661f6b --- /dev/null +++ b/Tests/EchoTests/Context Descriptor/GenericKinds.swift @@ -0,0 +1,60 @@ +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)) + } + + 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() + } +} 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() + } +} 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() + } +}