diff --git a/AGENTS.md b/AGENTS.md index 1d458373f2..3f24db55c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -139,7 +139,7 @@ This is the entry point for AI guidance in Apache Fory. Read this file first, th or map entry may instead advance because of ref, null, or type envelopes; name those derived facts `fieldReadAlwaysAdvances`, `elementReadAlwaysAdvances`, or `entryReadAlwaysAdvances` rather than conflating them with `readData`. -- For remote TypeDef/TypeMeta reads, the checked metadata cache is the only owner of remote "already validated" state. Cache hit means the header was previously parsed, body/hash-validated, policy-checked, and published by that cache, so the hot path must skip the body and use cached metadata without extra validation, hashing, limit checks, exact-local checks, allocation, or policy work. A known expected local TypeDef/TypeMeta header/hash match is a local-schema hit, not a remote cache miss: it may skip the body and use the local TypeInfo/TypeMeta without schema-version counting or cache publish. Cache miss is the only path that parses and validates non-local metadata, enforces limits, performs exact-local byte comparison when needed, and publishes remote metadata to the cache. Do not add nullable accepted-header fields, sentinel headers, per-TypeInfo markers, pending metadata state, parallel header-low/header-high slots, or parallel acceptance state for this decision. If a runtime needs a metadata hit hint, cache the concrete checked metadata owner object, such as the TypeInfo, TypeDef, or TypeMeta used by that runtime, and compare its validated header identity directly. +- For remote TypeDef/TypeMeta reads, the checked metadata cache is the only owner of remote "already validated" state. Cache hit means the header was previously parsed, body/hash-validated, policy-checked, and published by that cache, so the hot path must skip the body and use cached metadata without extra validation, hashing, limit checks, exact-local checks, allocation, or policy work. The protocol-defined 52-bit TypeDef/TypeMeta header hash is the unique schema identity, so a known expected local header/hash match is a local-schema hit and must not recompare field arrays or metadata bodies. It may skip the body and use the local TypeInfo/TypeMeta without schema-version counting or cache publish. Cache miss is the only path that parses and validates non-local metadata, enforces limits, and publishes remote metadata to the cache. Do not add nullable accepted-header fields, sentinel headers, per-TypeInfo markers, pending metadata state, parallel header-low/header-high slots, or parallel acceptance state for this decision. If a runtime needs a metadata hit hint, cache the concrete checked metadata owner object, such as the TypeInfo, TypeDef, or TypeMeta used by that runtime, and compare its validated header identity directly. - When a user corrects a non-obvious invariant, encode it in the nearest source comment before continuing, and also update `AGENTS.md`, `.agents/**`, docs, or specs when the rule is reusable beyond one file. Do not rely only on chat history, task notes, commit messages, or benchmark logs for corrections that protect security, protocol behavior, ownership, naming, or hot-path performance. - Reject semantic hacks. Do not bypass broken semantics by deleting cases, simplifying callers, adding coercion hooks, or using workaround fallbacks; fix the underlying bug and prove it with focused tests. - Protect hot paths. Avoid per-call allocations, callback objects, result tuples or records, unnecessary runtime branches, and wrapper-class substitutions in hot codec/runtime paths; prefer conditional imports and allocation-free concrete implementations where they fit the language. diff --git a/benchmarks/swift/README.md b/benchmarks/swift/README.md index a5b4e1101f..c1b8a5b747 100644 --- a/benchmarks/swift/README.md +++ b/benchmarks/swift/README.md @@ -119,6 +119,6 @@ protoc \ ``` - The benchmark intentionally includes plain-model conversion for protobuf to mirror real-world usage. -- `swift-benchmark` and `swift-external-benchmark` are separate products. Building the ordinary - product does not compile external-type models or serializer specializations. +- `swift-benchmark` and `swift-external-benchmark` are separate executables. Building the ordinary + benchmark does not compile external-type models or serializer specializations. - Results vary across machines and runtime environments. diff --git a/benchmarks/swift/benchmark_report.py b/benchmarks/swift/benchmark_report.py index cf0e717e1b..1ea4c403e7 100755 --- a/benchmarks/swift/benchmark_report.py +++ b/benchmarks/swift/benchmark_report.py @@ -265,13 +265,12 @@ def write_report( "Apache Fory, Protocol Buffers, and JSON in Swift." ) lines.append("") - lines.append("## Benchmark Products") + lines.append("## Benchmark Scope") lines.append("") lines.append( - "The ordinary/xlang cases are built by `swift-benchmark`. External-type " - "and carrier comparisons are built by the separate " - "`swift-external-benchmark` product, so building the ordinary product " - "does not compile those models or serializer specializations." + "The results below cover the ordinary/xlang cases run by `swift-benchmark`. " + "External-type and carrier comparisons use the separate " + "`swift-external-benchmark` executable and are included only when requested." ) lines.append("") lines.append("## Throughput Plot") @@ -344,7 +343,7 @@ def write_report( lines.append("## External-Type Serialization") lines.append("") lines.append( - "These cases are built in the isolated `swift-external-benchmark` product." + "These cases run in the isolated `swift-external-benchmark` executable." ) lines.append("") lines.append("| Case | Operation | TPS | ns/op | Bytes |") diff --git a/docs/benchmarks/object-serialization/xlang/swift/README.md b/docs/benchmarks/object-serialization/xlang/swift/README.md index 91d4568221..5da6888396 100644 --- a/docs/benchmarks/object-serialization/xlang/swift/README.md +++ b/docs/benchmarks/object-serialization/xlang/swift/README.md @@ -2,12 +2,9 @@ This benchmark compares serialization and deserialization throughput for Apache Fory, Protocol Buffers, and JSON in Swift. -## Benchmark Executables +## Benchmark Scope -The ordinary/xlang cases are built by `swift-benchmark`. External-type and -carrier comparisons are built by the separate `swift-external-benchmark` -executable, so building the ordinary executable does not compile those models or -serializer specializations. +The results below cover the ordinary/xlang cases run by `swift-benchmark`. External-type and carrier comparisons use the separate `swift-external-benchmark` executable and are included only when requested. ## Throughput Plot @@ -17,9 +14,9 @@ serializer specializations. | Key | Value | | --------------------- | ----------------------------- | -| Timestamp | 2026-05-08T09:05:32Z | +| Timestamp | 2026-08-04T09:52:12Z | | OS | Version 15.7.2 (Build 24G325) | -| Host | macbook-pro.local | +| Host | MacBook-Pro.local | | CPU Cores (Logical) | 12 | | Memory (GB) | 48.00 | | Duration per case (s) | 3 | @@ -28,18 +25,18 @@ serializer specializations. | Datatype | Operation | Fory TPS | Protobuf TPS | JSON TPS | Fastest | | ----------------- | ----------- | ---------: | -----------: | -------: | ------------ | -| NumericStruct | Serialize | 9,435,623 | 6,175,939 | 408,960 | fory (1.53x) | -| NumericStruct | Deserialize | 11,037,225 | 6,842,676 | 328,302 | fory (1.61x) | -| Sample | Serialize | 3,596,835 | 1,257,100 | 79,781 | fory (2.86x) | -| Sample | Deserialize | 982,255 | 733,588 | 41,274 | fory (1.34x) | -| MediaContent | Serialize | 1,561,376 | 609,896 | 98,677 | fory (2.56x) | -| MediaContent | Deserialize | 523,836 | 395,202 | 70,528 | fory (1.33x) | -| NumericStructList | Serialize | 2,910,846 | 918,363 | 82,965 | fory (3.17x) | -| NumericStructList | Deserialize | 2,436,636 | 701,656 | 69,353 | fory (3.47x) | -| SampleList | Serialize | 694,557 | 202,040 | 16,679 | fory (3.44x) | -| SampleList | Deserialize | 187,109 | 131,947 | 8,236 | fory (1.42x) | -| MediaContentList | Serialize | 348,238 | 98,007 | 18,698 | fory (3.55x) | -| MediaContentList | Deserialize | 104,990 | 74,422 | 16,298 | fory (1.41x) | +| NumericStruct | Serialize | 14,759,606 | 7,572,190 | 481,278 | fory (1.95x) | +| NumericStruct | Deserialize | 18,155,795 | 7,317,942 | 377,802 | fory (2.48x) | +| Sample | Serialize | 5,633,231 | 1,380,130 | 102,372 | fory (4.08x) | +| Sample | Deserialize | 1,509,740 | 916,626 | 52,897 | fory (1.65x) | +| MediaContent | Serialize | 2,987,821 | 784,863 | 111,200 | fory (3.81x) | +| MediaContent | Deserialize | 1,072,345 | 549,726 | 97,799 | fory (1.95x) | +| NumericStructList | Serialize | 5,515,769 | 1,093,924 | 93,635 | fory (5.04x) | +| NumericStructList | Deserialize | 2,598,609 | 746,527 | 76,161 | fory (3.48x) | +| SampleList | Serialize | 1,201,868 | 220,772 | 20,772 | fory (5.44x) | +| SampleList | Deserialize | 286,949 | 161,192 | 10,777 | fory (1.78x) | +| MediaContentList | Serialize | 657,276 | 118,004 | 22,744 | fory (5.57x) | +| MediaContentList | Deserialize | 215,726 | 99,445 | 19,521 | fory (2.17x) | ## Serialized Size (bytes) diff --git a/docs/benchmarks/object-serialization/xlang/swift/throughput.png b/docs/benchmarks/object-serialization/xlang/swift/throughput.png index c8d7f75e35..3e1ee73540 100644 Binary files a/docs/benchmarks/object-serialization/xlang/swift/throughput.png and b/docs/benchmarks/object-serialization/xlang/swift/throughput.png differ diff --git a/swift/Sources/Fory/ByteBuffer.swift b/swift/Sources/Fory/ByteBuffer.swift index c4c7fd8128..582e7d7488 100644 --- a/swift/Sources/Fory/ByteBuffer.swift +++ b/swift/Sources/Fory/ByteBuffer.swift @@ -18,12 +18,17 @@ import Foundation public final class ByteBuffer { + // Buffer state access is synchronous and never re-entered while borrowed. Overlapping access is + // invalid because unchecked exclusivity removes Swift's runtime enforcement on these hot paths. @usableFromInline + @exclusivity(unchecked) internal var storage: [UInt8] @usableFromInline + @exclusivity(unchecked) internal var cursor: Int + @exclusivity(unchecked) private var dataBridge = Data() @inlinable @@ -180,26 +185,6 @@ public final class ByteBuffer { return dataBridge } - @usableFromInline - @inline(__always) - internal func materializeData( - byteCount: Int, - _ body: (UnsafeMutablePointer) -> Void - ) -> Data { - if dataBridge.count != byteCount { - dataBridge.count = byteCount - } - if byteCount > 0 { - dataBridge.withUnsafeMutableBytes { destination in - guard let base = destination.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return - } - body(base) - } - } - return dataBridge - } - @inlinable public func getCursor() -> Int { cursor @@ -837,7 +822,36 @@ public final class ByteBuffer { guard let base = buffer.baseAddress else { return nil } - let utf8Bytes = UnsafeBufferPointer(start: base.advanced(by: start), count: count) + let utf8Base = base.advanced(by: start) + let utf8Bytes = UnsafeBufferPointer(start: utf8Base, count: count) + var index = 0 + var isASCII = true + // An ASCII byte has a clear high bit, so this mask checks eight bytes at once + // regardless of native byte order. The bounded loop makes the unaligned load safe. + while index <= count - MemoryLayout.size { + let word = UnsafeRawPointer(utf8Base.advanced(by: index)) + .loadUnaligned(as: UInt64.self) + if word & 0x8080_8080_8080_8080 != 0 { + isASCII = false + break + } + index += MemoryLayout.size + } + if isASCII { + while index < count { + if utf8Bytes[index] >= 0x80 { + isASCII = false + break + } + index += 1 + } + } + if isASCII { + return String(decoding: utf8Bytes, as: UTF8.self) + } + if #available(macOS 15.0, iOS 18.0, *) { + return String(validating: utf8Bytes, as: UTF8.self) + } return String(bytes: utf8Bytes, encoding: .utf8) } guard let decoded else { diff --git a/swift/Sources/Fory/CollectionSerializers.swift b/swift/Sources/Fory/CollectionSerializers.swift index fde1c2356b..0d351cceda 100644 --- a/swift/Sources/Fory/CollectionSerializers.swift +++ b/swift/Sources/Fory/CollectionSerializers.swift @@ -367,23 +367,211 @@ private func preparePrimitiveArray( type: Element.Type, count: Int, label: String +) throws { + try preparePrimitiveArrayStorage( + context, + reserveGraphStorage: reserveGraphStorage, + ownerBytes: storedOwnerBytes([Element].self), + count: count, + elementBytes: storedElementBytes(type), + label: label + ) +} + +@inline(__always) +private func preparePrimitiveArrayStorage( + _ context: ReadContext, + reserveGraphStorage: Bool, + ownerBytes: Int, + count: Int, + elementBytes: Int, + label: String ) throws { try context.ensureCollectionLength(count, label: label) if reserveGraphStorage { - try reserveGraphArrayMemory( - context, type, ownerBytes: storedOwnerBytes([Element].self), count: count) + try reserveGraphElements( + context, ownerBytes: ownerBytes, count: count, elementBytes: elementBytes) + } +} + +@inline(__always) +private func readPrimitiveArrayByteSize(_ context: ReadContext) throws -> Int { + let byteSize = Int(try context.buffer.readVarUInt32()) + try context.ensureRemainingBytes(byteSize, label: "primitive_array_bytes") + return byteSize +} + +@inline(__always) +func readBoolPrimitiveArray( + _ context: ReadContext, + reserveGraphStorage: Bool = false +) throws -> [Bool] { + let byteSize = try readPrimitiveArrayByteSize(context) + try preparePrimitiveArrayStorage( + context, + reserveGraphStorage: reserveGraphStorage, + ownerBytes: MemoryLayout<[Bool]>.stride, + count: byteSize, + elementBytes: MemoryLayout.stride, + label: "bool_array" + ) + return try readArrayUninitialized(count: byteSize) { destination in + for index in 0.. [Int32] { + let byteSize = try readPrimitiveArrayByteSize(context) + if byteSize % 4 != 0 { throw primitiveArraySizeMismatch("int32") } + let count = byteSize / 4 + try preparePrimitiveArrayStorage( + context, + reserveGraphStorage: reserveGraphStorage, + ownerBytes: MemoryLayout<[Int32]>.stride, + count: count, + elementBytes: MemoryLayout.stride, + label: "int32_array" + ) + if hostIsLittleEndian { + return try readArrayUninitialized(count: count) { destination in + try context.buffer.readBytes( + into: UnsafeMutableRawBufferPointer(start: destination, count: byteSize)) + } + } + return try readArrayUninitialized(count: count) { destination in + for index in 0.. [Int64] { + let byteSize = try readPrimitiveArrayByteSize(context) + if byteSize % 8 != 0 { throw primitiveArraySizeMismatch("int64") } + let count = byteSize / 8 + try preparePrimitiveArrayStorage( + context, + reserveGraphStorage: reserveGraphStorage, + ownerBytes: MemoryLayout<[Int64]>.stride, + count: count, + elementBytes: MemoryLayout.stride, + label: "int64_array" + ) + if hostIsLittleEndian { + return try readArrayUninitialized(count: count) { destination in + try context.buffer.readBytes( + into: UnsafeMutableRawBufferPointer(start: destination, count: byteSize)) + } + } + return try readArrayUninitialized(count: count) { destination in + for index in 0.. [Float] { + let byteSize = try readPrimitiveArrayByteSize(context) + if byteSize % 4 != 0 { throw primitiveArraySizeMismatch("float32") } + let count = byteSize / 4 + try preparePrimitiveArrayStorage( + context, + reserveGraphStorage: reserveGraphStorage, + ownerBytes: MemoryLayout<[Float]>.stride, + count: count, + elementBytes: MemoryLayout.stride, + label: "float32_array" + ) + if hostIsLittleEndian { + return try readArrayUninitialized(count: count) { destination in + try context.buffer.readBytes( + into: UnsafeMutableRawBufferPointer(start: destination, count: byteSize)) + } + } + return try readArrayUninitialized(count: count) { destination in + for index in 0.. [Double] { + let byteSize = try readPrimitiveArrayByteSize(context) + if byteSize % 8 != 0 { throw primitiveArraySizeMismatch("float64") } + let count = byteSize / 8 + try preparePrimitiveArrayStorage( + context, + reserveGraphStorage: reserveGraphStorage, + ownerBytes: MemoryLayout<[Double]>.stride, + count: count, + elementBytes: MemoryLayout.stride, + label: "float64_array" + ) + if hostIsLittleEndian { + return try readArrayUninitialized(count: count) { destination in + try context.buffer.readBytes( + into: UnsafeMutableRawBufferPointer(start: destination, count: byteSize)) + } + } + return try readArrayUninitialized(count: count) { destination in + for index in 0..( _ context: ReadContext, reserveGraphStorage: Bool = false ) throws -> [Element] { - let byteSize = Int(try context.buffer.readVarUInt32()) - try context.ensureRemainingBytes(byteSize, label: "primitive_array_bytes") + if Element.self == Bool.self { + return uncheckedArrayCast( + try readBoolPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage), + to: Element.self) + } + if Element.self == Int32.self { + return uncheckedArrayCast( + try readInt32PrimitiveArray(context, reserveGraphStorage: reserveGraphStorage), + to: Element.self) + } + if Element.self == Int64.self { + return uncheckedArrayCast( + try readInt64PrimitiveArray(context, reserveGraphStorage: reserveGraphStorage), + to: Element.self) + } + if Element.self == Float.self { + return uncheckedArrayCast( + try readFloatPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage), + to: Element.self) + } + if Element.self == Double.self { + return uncheckedArrayCast( + try readDoublePrimitiveArray(context, reserveGraphStorage: reserveGraphStorage), + to: Element.self) + } + + let byteSize = try readPrimitiveArrayByteSize(context) if Element.self == UInt8.self { try preparePrimitiveArray( @@ -393,18 +581,6 @@ func readPrimitiveArray( return uncheckedArrayCast(bytes, to: Element.self) } - if Element.self == Bool.self { - try preparePrimitiveArray( - context, reserveGraphStorage: reserveGraphStorage, type: Element.self, count: byteSize, - label: "bool_array") - let out = try readArrayUninitialized(count: byteSize) { destination in - for index in 0..( return uncheckedArrayCast(out, to: Element.self) } - if Element.self == Int32.self { - if byteSize % 4 != 0 { throw primitiveArraySizeMismatch("int32") } - let count = byteSize / 4 - try preparePrimitiveArray( - context, reserveGraphStorage: reserveGraphStorage, type: Element.self, count: count, - label: "int32_array") - if hostIsLittleEndian { - var out = Array(repeating: Int32(0), count: count) - try out.withUnsafeMutableBytes { rawBytes in - try context.buffer.readBytes(into: rawBytes) - } - return uncheckedArrayCast(out, to: Element.self) - } - let out = try readArrayUninitialized(count: count) { destination in - for index in 0..( return uncheckedArrayCast(out, to: Element.self) } - if Element.self == Int64.self { - if byteSize % 8 != 0 { throw primitiveArraySizeMismatch("int64") } - let count = byteSize / 8 - try preparePrimitiveArray( - context, reserveGraphStorage: reserveGraphStorage, type: Element.self, count: count, - label: "int64_array") - if hostIsLittleEndian { - var out = Array(repeating: Int64(0), count: count) - try out.withUnsafeMutableBytes { rawBytes in - try context.buffer.readBytes(into: rawBytes) - } - return uncheckedArrayCast(out, to: Element.self) - } - let out = try readArrayUninitialized(count: count) { destination in - for index in 0..( return uncheckedArrayCast(values, to: Element.self) } - if Element.self == Float.self { - if byteSize % 4 != 0 { throw primitiveArraySizeMismatch("float32") } - let count = byteSize / 4 - try preparePrimitiveArray( - context, reserveGraphStorage: reserveGraphStorage, type: Element.self, count: count, - label: "float32_array") - if hostIsLittleEndian { - var out = Array(repeating: Float(0), count: count) - try out.withUnsafeMutableBytes { rawBytes in - try context.buffer.readBytes(into: rawBytes) - } - return uncheckedArrayCast(out, to: Element.self) - } - let out = try readArrayUninitialized(count: count) { destination in - for index in 0..: Serializer { itemReadAlwaysAdvances: elementReadAlwaysAdvances, label: "array" ) + if !trackRef && !hasNull && elementReadAlwaysAdvances { + if let elementTypeInfo { + return try Codec.withFieldTypeInfo(elementTypeInfo, context) { + try readNonNullElements(context, codec: Codec.self, count: length) + } + } + return try readNonNullElements(context, codec: Codec.self, count: length) + } return try Codec.withFieldTypeInfo(elementTypeInfo, context) { if trackRef { return try readArrayTrackingInitialization( @@ -845,19 +949,6 @@ public enum ArraySerializer: Serializer { } } - if elementReadAlwaysAdvances { - return try readArrayTrackingInitialization( - count: length - ) { destination, initializedCount in - for index in 0..: Serializer { } } } + + @inlinable + @inline(__always) + internal static func readNonNullElements( + _ context: ReadContext, + codec _: Codec.Type, + count: Int + ) throws -> [Codec.Target] where Codec.Target == Element.Target { + try [Codec.Target](unsafeUninitializedCapacity: count) { destination, initializedCount in + let baseAddress = destination.baseAddress! + for index in 0..( ) throws -> [ElementCodec.Target]? { if ElementCodec.self == BoolCodec.self { return uncheckedPackedArrayCast( - try readPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage) as [Bool], + try readBoolPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage), to: ElementCodec.Target.self) } if ElementCodec.self == Int8Codec.self { @@ -1417,12 +1417,12 @@ private func readPackedArrayPayload( } if ElementCodec.self == Int32FixedCodec.self { return uncheckedPackedArrayCast( - try readPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage) as [Int32], + try readInt32PrimitiveArray(context, reserveGraphStorage: reserveGraphStorage), to: ElementCodec.Target.self) } if ElementCodec.self == Int64FixedCodec.self { return uncheckedPackedArrayCast( - try readPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage) as [Int64], + try readInt64PrimitiveArray(context, reserveGraphStorage: reserveGraphStorage), to: ElementCodec.Target.self) } if ElementCodec.self == IntFixedCodec.self { @@ -1467,12 +1467,12 @@ private func readPackedArrayPayload( } if ElementCodec.self == FloatCodec.self { return uncheckedPackedArrayCast( - try readPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage) as [Float], + try readFloatPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage), to: ElementCodec.Target.self) } if ElementCodec.self == DoubleCodec.self { return uncheckedPackedArrayCast( - try readPrimitiveArray(context, reserveGraphStorage: reserveGraphStorage) as [Double], + try readDoublePrimitiveArray(context, reserveGraphStorage: reserveGraphStorage), to: ElementCodec.Target.self) } return nil diff --git a/swift/Sources/Fory/TypeMeta.swift b/swift/Sources/Fory/TypeMeta.swift index 29e9efde03..da093c1c25 100644 --- a/swift/Sources/Fory/TypeMeta.swift +++ b/swift/Sources/Fory/TypeMeta.swift @@ -314,6 +314,8 @@ public final class TypeMeta: Equatable, @unchecked Sendable { public let registerByName: Bool public let fields: [FieldInfo] public let compressed: Bool + /// The protocol-defined 52-bit schema identity. Equal values identify the same schema, + /// so readers do not need to compare the field array again. public let headerHash: UInt64 internal var readDataAlwaysAdvances: Bool { diff --git a/swift/Sources/ForyMacro/ForyObjectMacroReadGeneration.swift b/swift/Sources/ForyMacro/ForyObjectMacroReadGeneration.swift index f533ae2572..55ce7e47d8 100644 --- a/swift/Sources/ForyMacro/ForyObjectMacroReadGeneration.swift +++ b/swift/Sources/ForyMacro/ForyObjectMacroReadGeneration.swift @@ -345,10 +345,8 @@ private func buildClassReadCompatibleDataDecl( if let reservedRefID { context.refReader.storeRef(value, at: reservedRefID) } - if let localTypeMeta = remoteTypeInfo.typeMeta, - let localHeaderHash = remoteTypeInfo.typeDefHeaderHash, - typeMeta.headerHash == localHeaderHash, - typeMeta.fields == localTypeMeta.fields { + if let localHeaderHash = remoteTypeInfo.typeDefHeaderHash, + typeMeta.headerHash == localHeaderHash { if !remoteTypeInfo.typeDefHasUserTypeFields { \(schemaAssignBody) return value @@ -382,10 +380,8 @@ private func buildEmptyStructReadCompatibleDataDecl(accessPrefix: String) -> Str guard let typeMeta = typeInfo.compatibleTypeMeta else { throw ForyError.invalidData("compatible type metadata is required") } - if let localTypeMeta = typeInfo.typeMeta, - let localHeaderHash = typeInfo.typeDefHeaderHash, - typeMeta.headerHash == localHeaderHash, - typeMeta.fields == localTypeMeta.fields { + if let localHeaderHash = typeInfo.typeDefHeaderHash, + typeMeta.headerHash == localHeaderHash { return Target() } for remoteField in typeMeta.fields { @@ -436,10 +432,8 @@ private func buildStructReadCompatibleDataDecl( \(bufferBinding)guard let typeMeta = typeInfo.compatibleTypeMeta else { throw ForyError.invalidData("compatible type metadata is required") } - if let localTypeMeta = typeInfo.typeMeta, - let localHeaderHash = typeInfo.typeDefHeaderHash, - typeMeta.headerHash == localHeaderHash, - typeMeta.fields == localTypeMeta.fields { + if let localHeaderHash = typeInfo.typeDefHeaderHash, + typeMeta.headerHash == localHeaderHash { if !typeInfo.typeDefHasUserTypeFields { \(schemaReadBody) return Target(