From f0528ebd18e47a3a3f516079f14362bd519f9ef7 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Jul 2026 11:38:26 -0400 Subject: [PATCH 1/4] Add ModelDataDecodingError typealias for Embedded typed throws Resolves to the concrete CoreModelError under Embedded Swift (where any Error existentials are disallowed) and to any Swift.Error otherwise, so callers can adopt throws(ModelDataDecodingError) with no behavior change off-Embedded. --- Sources/CoreModel/Error.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Sources/CoreModel/Error.swift b/Sources/CoreModel/Error.swift index 47e1a1e..2115f72 100644 --- a/Sources/CoreModel/Error.swift +++ b/Sources/CoreModel/Error.swift @@ -21,6 +21,18 @@ public enum CoreModelError: Error { // MARK: - Decoding Error Factories +#if hasFeature(Embedded) +/// The concrete error type thrown by `ModelData.decode`/`decodeRelationship` +/// and by hand-written `Entity.init(from:)` witnesses under Embedded Swift, +/// where `any Error` existentials are disallowed. Resolves to `any Swift.Error` +/// on non-Embedded platforms, where the existing untyped-`throws` behavior is +/// unchanged. +public typealias ModelDataDecodingError = CoreModelError +#else +/// See the Embedded branch of this typealias above. +public typealias ModelDataDecodingError = any Swift.Error +#endif + #if hasFeature(Embedded) // - Note: Embedded Swift disallows `any Error` as a value/return type (existential From 2c9f598067d794a47dcda8a67e03ead5da96479c Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Jul 2026 11:38:26 -0400 Subject: [PATCH 2/4] Type ModelData decode helpers with throws(ModelDataDecodingError) The generic decode/decodeRelationship helpers threw untyped (any Error), forcing a boxing thunk the compiler rejects under Embedded Swift once specialized for a concrete type. Adopt the typed throws clause and replace the throwing objectIDs.map with an explicit loop, since the rethrows overload erases the thrown type back to any Error. --- Sources/CoreModel/Decodable.swift | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/Sources/CoreModel/Decodable.swift b/Sources/CoreModel/Decodable.swift index 086f524..f90d20c 100644 --- a/Sources/CoreModel/Decodable.swift +++ b/Sources/CoreModel/Decodable.swift @@ -15,7 +15,7 @@ import Foundation public extension ModelData { - func decode(_ type: T.Type, forKey key: K) throws -> T where T: AttributeDecodable, K: CodingKey { + func decode(_ type: T.Type, forKey key: K) throws(ModelDataDecodingError) -> T where T: AttributeDecodable, K: CodingKey { let property = PropertyKey(key) guard let attribute = self.attributes[property] else { @@ -32,7 +32,7 @@ public extension ModelData { return decodable } - func decodeRelationship(_ type: T.Type, forKey key: K) throws -> T where T: ObjectIDConvertible, K: CodingKey { + func decodeRelationship(_ type: T.Type, forKey key: K) throws(ModelDataDecodingError) -> T where T: ObjectIDConvertible, K: CodingKey { let property = PropertyKey(key) guard let relationship = self.relationships[property] else { @@ -51,7 +51,7 @@ public extension ModelData { } } - func decodeRelationship(_ type: T?.Type, forKey key: K) throws -> T? where T: ObjectIDConvertible, K: CodingKey { + func decodeRelationship(_ type: T?.Type, forKey key: K) throws(ModelDataDecodingError) -> T? where T: ObjectIDConvertible, K: CodingKey { let property = PropertyKey(key) guard let relationship = self.relationships[property] else { @@ -70,7 +70,7 @@ public extension ModelData { } } - func decodeRelationship(_ type: [T].Type, forKey key: K) throws -> [T] where T: ObjectIDConvertible, K: CodingKey { + func decodeRelationship(_ type: [T].Type, forKey key: K) throws(ModelDataDecodingError) -> [T] where T: ObjectIDConvertible, K: CodingKey { let property = PropertyKey(key) guard let relationship = self.relationships[property] else { @@ -82,12 +82,19 @@ public extension ModelData { case .toOne: throw coreModelTypeMismatchError(type, forKey: key, from: relationship) case let .toMany(objectIDs): - return try objectIDs.map { - guard let id = T.init(objectID: $0) else { - throw coreModelInvalidIdentifierError($0) + // - Note: An explicit loop rather than `objectIDs.map`, because the + // `rethrows` closure overload erases the thrown error back to + // `any Error`, which cannot convert to the typed `throws` clause + // under Embedded Swift. + var ids = [T]() + ids.reserveCapacity(objectIDs.count) + for objectID in objectIDs { + guard let id = T.init(objectID: objectID) else { + throw coreModelInvalidIdentifierError(objectID) } - return id + ids.append(id) } + return ids } } } From 27d2955f0a0f14eb507e58fe319d3ec7fc1804cb Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Jul 2026 11:38:26 -0400 Subject: [PATCH 3/4] Type Entity.init(from:) requirement with throws(ModelDataDecodingError) The untyped throws requirement forced every throwing witness to synthesize an any Error boxing thunk in the witness table, which Embedded Swift rejects. encode() is left untyped since no witness throws. --- Sources/CoreModel/Entity.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CoreModel/Entity.swift b/Sources/CoreModel/Entity.swift index 9637b75..225ac8a 100644 --- a/Sources/CoreModel/Entity.swift +++ b/Sources/CoreModel/Entity.swift @@ -17,7 +17,7 @@ public protocol Entity: Identifiable, Sendable where CodingKeys: Hashable, Self. associatedtype CodingKeys: CodingKey - init(from model: ModelData) throws + init(from model: ModelData) throws(ModelDataDecodingError) func encode() throws -> ModelData } From ea27f47e92bd2f5efdd215a72f1f1301211d5936 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Jul 2026 11:38:26 -0400 Subject: [PATCH 4/4] Add Embedded-only Entity fixture to force decode-path specialization An always-built (Embedded-gated) concrete Entity conformance that specializes the generic init(from:)/decode path the wasm-embedded CI job never reaches through the test target, guarding against error-boxing regressions. --- Sources/CoreModel/EmbeddedFixture.swift | 48 +++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 Sources/CoreModel/EmbeddedFixture.swift diff --git a/Sources/CoreModel/EmbeddedFixture.swift b/Sources/CoreModel/EmbeddedFixture.swift new file mode 100644 index 0000000..8d7901c --- /dev/null +++ b/Sources/CoreModel/EmbeddedFixture.swift @@ -0,0 +1,48 @@ +// +// EmbeddedFixture.swift +// CoreModel +// +// Created by Alsey Coleman Miller on 7/18/26. +// + +// - Note: A concrete `Entity` conformance that only exists under Embedded Swift. +// It forces the compiler to specialize the generic `Entity.init(from:)` / +// `ModelData.decode(...)` / `decodeRelationship(...)` decode path for a real +// conforming type — the exact instantiation the wasm-embedded CI job never +// exercises through the test target. Without it, error-boxing regressions in +// that path (`#EmbeddedRestrictions`) can silently reappear. Dead weight on +// non-Embedded builds, so it is gated out entirely there. + +#if hasFeature(Embedded) + +#if canImport(FoundationEssentials) +import FoundationEssentials +#elseif canImport(Foundation) +import Foundation +#endif + +private struct EmbeddedEntityFixture: Entity { + + let id: UUID + + static var entityName: EntityName { "EmbeddedEntityFixture" } + + static var attributes: [CodingKeys: AttributeType] { [:] } + + static var relationships: [CodingKeys: Relationship] { [:] } + + enum CodingKeys: String, CodingKey { case id } + + init(from model: ModelData) throws(ModelDataDecodingError) { + guard let id = Self.ID(objectID: model.id) else { + throw .invalidIdentifier(model.id) + } + self.id = id + } + + func encode() -> ModelData { + ModelData(entity: Self.entityName, id: ObjectID(id)) + } +} + +#endif