diff --git a/.github/workflows/swift-wasm.yml b/.github/workflows/swift-wasm.yml index 9305c14..a3ce69f 100644 --- a/.github/workflows/swift-wasm.yml +++ b/.github/workflows/swift-wasm.yml @@ -55,6 +55,7 @@ jobs: - name: Build run: >- SWIFTPM_ENABLE_MACROS=0 + SWIFT_EMBEDDED=1 swift build -c ${{ matrix.config }} --swift-sdk swift-6.3.2-RELEASE_wasm-embedded diff --git a/Package.swift b/Package.swift index 7f27ddf..e789be4 100644 --- a/Package.swift +++ b/Package.swift @@ -81,6 +81,23 @@ package.targets[package.targets.count - 1] = .testTarget( ] ) +// Embedded Swift support (Foundation-free builds) +let enableEmbedded = environment["SWIFT_EMBEDDED"] == "1" +if enableEmbedded { + package.dependencies += [ + .package( + url: "https://github.com/PureSwift/swift-embedded-foundation.git", + from: "0.1.0" + ) + ] + package.targets[0].dependencies += [ + .product( + name: "FoundationEmbedded", + package: "swift-embedded-foundation" + ) + ] +} + // Skip (skip.dev) Fuse (native) transpilation support let enableSkipFuse = environment["SKIP_FUSE"] == "1" if enableSkipFuse { diff --git a/Sources/CoreModel/FoundationShims.swift b/Sources/CoreModel/FoundationShims.swift new file mode 100644 index 0000000..1c9bda7 --- /dev/null +++ b/Sources/CoreModel/FoundationShims.swift @@ -0,0 +1,19 @@ +// +// FoundationShims.swift +// CoreModel +// +// Re-exports Foundation value types from `FoundationEmbedded` +// (https://github.com/PureSwift/swift-embedded-foundation) for platforms +// without Foundation (e.g. Embedded Swift). +// + +#if !canImport(FoundationEssentials) && !canImport(Foundation) +import FoundationEmbedded + +public typealias Data = FoundationEmbedded.Data +public typealias UUID = FoundationEmbedded.UUID +public typealias Date = FoundationEmbedded.Date +public typealias TimeInterval = FoundationEmbedded.TimeInterval +public typealias Decimal = FoundationEmbedded.Decimal +public typealias URL = FoundationEmbedded.URL +#endif diff --git a/Sources/CoreModel/FoundationShims/Data.swift b/Sources/CoreModel/FoundationShims/Data.swift deleted file mode 100644 index 59d039f..0000000 --- a/Sources/CoreModel/FoundationShims/Data.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// Data.swift -// CoreModel -// -// Minimal Foundation-free `Data` for platforms without Foundation -// (e.g. Embedded Swift). Not API-complete — storage-layer round-tripping only. -// - -#if !canImport(FoundationEssentials) && !canImport(Foundation) - -public struct Data: Sendable { - - internal var bytes: [UInt8] - - public init() { - self.bytes = [] - } - - public init(_ elements: S) where S.Element == UInt8 { - self.bytes = Array(elements) - } - - public init(repeating byte: UInt8, count: Int) { - self.bytes = Array(repeating: byte, count: count) - } -} - -// MARK: - Collection - -extension Data: RandomAccessCollection, MutableCollection { - - public typealias Element = UInt8 - public typealias Index = Int - - public var startIndex: Int { bytes.startIndex } - public var endIndex: Int { bytes.endIndex } - - public subscript(position: Int) -> UInt8 { - get { bytes[position] } - set { bytes[position] = newValue } - } - - public func index(after i: Int) -> Int { bytes.index(after: i) } - public func index(before i: Int) -> Int { bytes.index(before: i) } -} - -extension Data { - - public mutating func append(_ byte: UInt8) { - bytes.append(byte) - } - - public mutating func append(contentsOf newElements: S) where S.Element == UInt8 { - bytes.append(contentsOf: newElements) - } -} - -// MARK: - Equatable, Hashable - -extension Data: Equatable, Hashable { - - public static func == (lhs: Data, rhs: Data) -> Bool { - lhs.bytes == rhs.bytes - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(bytes) - } -} - -// MARK: - CustomStringConvertible - -extension Data: CustomStringConvertible, CustomDebugStringConvertible { - - public var description: String { - "\(count) bytes" - } - - public var debugDescription: String { - description - } -} - -// MARK: - Codable - -#if !hasFeature(Embedded) -extension Data: Codable { - - public init(from decoder: Decoder) throws { - var container = try decoder.unkeyedContainer() - var bytes: [UInt8] = [] - if let count = container.count { - bytes.reserveCapacity(count) - } - while !container.isAtEnd { - bytes.append(try container.decode(UInt8.self)) - } - self.init(bytes) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.unkeyedContainer() - for byte in bytes { - try container.encode(byte) - } - } -} -#endif - -#endif diff --git a/Sources/CoreModel/FoundationShims/Date.swift b/Sources/CoreModel/FoundationShims/Date.swift deleted file mode 100644 index 199d63d..0000000 --- a/Sources/CoreModel/FoundationShims/Date.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// Date.swift -// CoreModel -// -// Minimal Foundation-free `Date` for platforms without Foundation -// (e.g. Embedded Swift). Not API-complete — storage-layer round-tripping only. -// - -#if !canImport(FoundationEssentials) && !canImport(Foundation) - -public typealias TimeInterval = Double - -public struct Date: Sendable { - - /// Number of seconds relative to the reference date of Jan 1, 2001, 00:00:00 UTC. - public var timeIntervalSinceReferenceDate: TimeInterval - - public init(timeIntervalSinceReferenceDate: TimeInterval) { - self.timeIntervalSinceReferenceDate = timeIntervalSinceReferenceDate - } -} - -extension Date { - - /// Seconds between the Unix epoch (Jan 1, 1970) and the reference date (Jan 1, 2001). - private static let referenceDateToUnixEpoch: TimeInterval = 978307200 - - public init(timeIntervalSince1970: TimeInterval) { - self.init(timeIntervalSinceReferenceDate: timeIntervalSince1970 - Self.referenceDateToUnixEpoch) - } - - public var timeIntervalSince1970: TimeInterval { - timeIntervalSinceReferenceDate + Self.referenceDateToUnixEpoch - } - - public func timeIntervalSince(_ other: Date) -> TimeInterval { - timeIntervalSinceReferenceDate - other.timeIntervalSinceReferenceDate - } - - public func addingTimeInterval(_ interval: TimeInterval) -> Date { - Date(timeIntervalSinceReferenceDate: timeIntervalSinceReferenceDate + interval) - } - - public mutating func addTimeInterval(_ interval: TimeInterval) { - timeIntervalSinceReferenceDate += interval - } - - public static func + (lhs: Date, rhs: TimeInterval) -> Date { - lhs.addingTimeInterval(rhs) - } - - public static func - (lhs: Date, rhs: TimeInterval) -> Date { - lhs.addingTimeInterval(-rhs) - } - - public static func - (lhs: Date, rhs: Date) -> TimeInterval { - lhs.timeIntervalSince(rhs) - } - - public static func += (lhs: inout Date, rhs: TimeInterval) { - lhs.addTimeInterval(rhs) - } - - public static func -= (lhs: inout Date, rhs: TimeInterval) { - lhs.addTimeInterval(-rhs) - } -} - -// MARK: - Equatable, Comparable, Hashable - -extension Date: Equatable, Comparable, Hashable { - - public static func == (lhs: Date, rhs: Date) -> Bool { - lhs.timeIntervalSinceReferenceDate == rhs.timeIntervalSinceReferenceDate - } - - public static func < (lhs: Date, rhs: Date) -> Bool { - lhs.timeIntervalSinceReferenceDate < rhs.timeIntervalSinceReferenceDate - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(timeIntervalSinceReferenceDate) - } -} - -// MARK: - CustomStringConvertible - -extension Date: CustomStringConvertible, CustomDebugStringConvertible { - - /// - Note: Not a Foundation-compatible ISO 8601 format — this is a - /// storage-layer replacement. Only used for debug/predicate description. - public var description: String { - timeIntervalSinceReferenceDate.description - } - - public var debugDescription: String { - description - } -} - -// MARK: - Codable - -#if !hasFeature(Embedded) -extension Date: Codable { - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - self.init(timeIntervalSinceReferenceDate: try container.decode(TimeInterval.self)) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(timeIntervalSinceReferenceDate) - } -} -#endif - -#endif diff --git a/Sources/CoreModel/FoundationShims/Decimal.swift b/Sources/CoreModel/FoundationShims/Decimal.swift deleted file mode 100644 index 4b872cb..0000000 --- a/Sources/CoreModel/FoundationShims/Decimal.swift +++ /dev/null @@ -1,138 +0,0 @@ -// -// Decimal.swift -// CoreModel -// -// Minimal Foundation-free `Decimal` for platforms without Foundation -// (e.g. Embedded Swift). Storage-layer only: normalized decimal-string -// representation, no arithmetic. Not a replacement for `Foundation.Decimal` -// in numeric contexts. -// - -#if !canImport(FoundationEssentials) && !canImport(Foundation) - -public struct Decimal: Sendable { - - /// Normalized decimal string: optional `-` sign, digits, optional `.` fraction - /// with no trailing zeros; `-0` and `-0.0` normalize to `0`. - private let normalized: String - - public init?(string: String) { - guard let normalized = Decimal.normalize(string) else { - return nil - } - self.normalized = normalized - } - - private init(normalized: String) { - self.normalized = normalized - } - - private static func normalize(_ string: String) -> String? { - let chars = Array(string.utf8) - guard chars.isEmpty == false else { return nil } - - var index = 0 - var negative = false - if chars[index] == UInt8(ascii: "-") { - negative = true - index += 1 - } - guard index < chars.count else { return nil } - - var integerDigits: [UInt8] = [] - while index < chars.count, chars[index] >= UInt8(ascii: "0"), chars[index] <= UInt8(ascii: "9") { - integerDigits.append(chars[index]) - index += 1 - } - guard integerDigits.isEmpty == false else { return nil } - - var fractionDigits: [UInt8] = [] - if index < chars.count, chars[index] == UInt8(ascii: ".") { - index += 1 - while index < chars.count, chars[index] >= UInt8(ascii: "0"), chars[index] <= UInt8(ascii: "9") { - fractionDigits.append(chars[index]) - index += 1 - } - guard fractionDigits.isEmpty == false else { return nil } - } - guard index == chars.count else { return nil } - - // Strip leading zeros from the integer part (keep at least one digit). - var integerStart = 0 - while integerStart < integerDigits.count - 1, integerDigits[integerStart] == UInt8(ascii: "0") { - integerStart += 1 - } - integerDigits = Array(integerDigits[integerStart...]) - - // Strip trailing zeros from the fraction part. - var fractionEnd = fractionDigits.count - while fractionEnd > 0, fractionDigits[fractionEnd - 1] == UInt8(ascii: "0") { - fractionEnd -= 1 - } - fractionDigits = Array(fractionDigits[.. Bool { - lhs.normalized == rhs.normalized - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(normalized) - } -} - -// MARK: - CustomStringConvertible - -extension Decimal: CustomStringConvertible, CustomDebugStringConvertible { - - public var description: String { - normalized - } - - public var debugDescription: String { - description - } -} - -// MARK: - Codable - -#if !hasFeature(Embedded) -extension Decimal: Codable { - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let string = try container.decode(String.self) - guard let value = Decimal(string: string) else { - throw DecodingError.dataCorrupted( - DecodingError.Context( - codingPath: decoder.codingPath, - debugDescription: "Attempted to decode Decimal from invalid string.")) - } - self = value - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(description) - } -} -#endif - -#endif diff --git a/Sources/CoreModel/FoundationShims/URL.swift b/Sources/CoreModel/FoundationShims/URL.swift deleted file mode 100644 index 9b48796..0000000 --- a/Sources/CoreModel/FoundationShims/URL.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// URL.swift -// CoreModel -// -// Minimal Foundation-free `URL` for platforms without Foundation -// (e.g. Embedded Swift). Not API-complete — storage-layer round-tripping only, -// no real parsing/resolution. -// - -#if !canImport(FoundationEssentials) && !canImport(Foundation) - -public struct URL: Sendable { - - public let absoluteString: String - - public init?(string: String) { - guard string.isEmpty == false else { - return nil - } - self.absoluteString = string - } -} - -// MARK: - Equatable, Hashable - -extension URL: Equatable, Hashable {} - -// MARK: - CustomStringConvertible - -extension URL: CustomStringConvertible, CustomDebugStringConvertible { - - public var description: String { - absoluteString - } - - public var debugDescription: String { - description - } -} - -// MARK: - Codable - -#if !hasFeature(Embedded) -extension URL: Codable { - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let string = try container.decode(String.self) - guard let url = URL(string: string) else { - throw DecodingError.dataCorrupted( - DecodingError.Context( - codingPath: decoder.codingPath, - debugDescription: "Attempted to decode URL from invalid string.")) - } - self = url - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(absoluteString) - } -} -#endif - -#endif diff --git a/Sources/CoreModel/FoundationShims/UUID.swift b/Sources/CoreModel/FoundationShims/UUID.swift deleted file mode 100644 index 2081cd9..0000000 --- a/Sources/CoreModel/FoundationShims/UUID.swift +++ /dev/null @@ -1,266 +0,0 @@ -// -// UUID.swift -// CoreModel -// -// Minimal Foundation-free `UUID` for platforms without Foundation -// (e.g. Embedded Swift). Modeled on PureSwift/Bluetooth's embedded UUID. -// Not API-complete — storage-layer round-tripping only. -// - -#if !canImport(FoundationEssentials) && !canImport(Foundation) - -public struct UUID: Sendable { - - public typealias ByteValue = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) - - public let uuid: ByteValue - - public init(uuid: ByteValue) { - self.uuid = uuid - } -} - -// MARK: - Random Initialization - -extension UUID { - - /// Create a new UUID with RFC 4122 version 4 random bytes. - public init() { - var uuidBytes: ByteValue = ( - .random(in: 0...255), - .random(in: 0...255), - .random(in: 0...255), - .random(in: 0...255), - .random(in: 0...255), - .random(in: 0...255), - .random(in: 0...255), - .random(in: 0...255), - .random(in: 0...255), - .random(in: 0...255), - .random(in: 0...255), - .random(in: 0...255), - .random(in: 0...255), - .random(in: 0...255), - .random(in: 0...255), - .random(in: 0...255) - ) - - // Set the version to 4 (random UUID) - uuidBytes.6 = (uuidBytes.6 & 0x0F) | 0x40 - - // Set the variant to RFC 4122 - uuidBytes.8 = (uuidBytes.8 & 0x3F) | 0x80 - - self.init(uuid: uuidBytes) - } -} - -// MARK: - String Parsing / Formatting - -extension UUID { - - /// Create a UUID from a string such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F". - /// - /// Returns nil for invalid strings. - public init?(uuidString string: String) { - guard let value = UInt128.bigEndian(uuidString: string) else { - return nil - } - self.init(uuid: value.bytes) - } - - /// Returns a string created from the UUID, such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" - public var uuidString: String { - UInt128(bytes: uuid).bigEndianUUIDString - } -} - -// MARK: - Equatable - -extension UUID: Equatable { - - public static func == (lhs: UUID, rhs: UUID) -> Bool { - Swift.withUnsafeBytes(of: lhs.uuid) { lhsPtr in - Swift.withUnsafeBytes(of: rhs.uuid) { rhsPtr in - let lhsTuple = lhsPtr.loadUnaligned(as: (UInt64, UInt64).self) - let rhsTuple = rhsPtr.loadUnaligned(as: (UInt64, UInt64).self) - return (lhsTuple.0 ^ rhsTuple.0) | (lhsTuple.1 ^ rhsTuple.1) == 0 - } - } - } -} - -// MARK: - Hashable - -extension UUID: Hashable { - - public func hash(into hasher: inout Hasher) { - Swift.withUnsafeBytes(of: uuid) { buffer in - hasher.combine(bytes: buffer) - } - } -} - -// MARK: - CustomStringConvertible - -extension UUID: CustomStringConvertible, CustomDebugStringConvertible { - - public var description: String { - uuidString - } - - public var debugDescription: String { - description - } -} - -// MARK: - Comparable - -extension UUID: Comparable { - - public static func < (lhs: UUID, rhs: UUID) -> Bool { - var leftUUID = lhs.uuid - var rightUUID = rhs.uuid - var result: Int = 0 - var diff: Int = 0 - Swift.withUnsafeBytes(of: &leftUUID) { leftPtr in - Swift.withUnsafeBytes(of: &rightUUID) { rightPtr in - for offset in (0...size).reversed() { - diff = Int(leftPtr.load(fromByteOffset: offset, as: UInt8.self)) - Int(rightPtr.load(fromByteOffset: offset, as: UInt8.self)) - result = (result & (((diff - 1) & ~diff) >> 8)) | diff - } - } - } - return result < 0 - } -} - -// MARK: - Codable - -#if !hasFeature(Embedded) -extension UUID: Codable { - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let uuidString = try container.decode(String.self) - guard let uuid = UUID(uuidString: uuidString) else { - throw DecodingError.dataCorrupted( - DecodingError.Context( - codingPath: decoder.codingPath, - debugDescription: "Attempted to decode UUID from invalid UUID string.")) - } - self = uuid - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(self.uuidString) - } -} -#endif - -// MARK: - UUID String Parsing - -fileprivate extension UInt128 { - - /// Parse a UUID string and return a value in big endian order. - static func bigEndian(uuidString string: String) -> UInt128? { - guard string.utf8.count == 36, - let separator = "-".utf8.first - else { - return nil - } - let characters = string.utf8 - guard characters[characters.index(characters.startIndex, offsetBy: 8)] == separator, - characters[characters.index(characters.startIndex, offsetBy: 13)] == separator, - characters[characters.index(characters.startIndex, offsetBy: 18)] == separator, - characters[characters.index(characters.startIndex, offsetBy: 23)] == separator, - let a = String(characters[characters.startIndex.. String { - let hexDigits: [Character] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"] - let high = Int(self >> 4) - let low = Int(self & 0x0F) - return String([hexDigits[high], hexDigits[low]]) - } -} - -#endif diff --git a/Sources/CoreModel/ObjectID.swift b/Sources/CoreModel/ObjectID.swift index 5c019cb..8e29de7 100644 --- a/Sources/CoreModel/ObjectID.swift +++ b/Sources/CoreModel/ObjectID.swift @@ -9,6 +9,8 @@ import FoundationEssentials #elseif canImport(Foundation) import Foundation +#elseif canImport(FoundationEmbedded) +import FoundationEmbedded #endif /// CoreModel Object Identifier diff --git a/Sources/CoreModel/Predicate/Expression.swift b/Sources/CoreModel/Predicate/Expression.swift index f9a7575..2e2e0a9 100644 --- a/Sources/CoreModel/Predicate/Expression.swift +++ b/Sources/CoreModel/Predicate/Expression.swift @@ -6,6 +6,14 @@ // Copyright © 2017 PureSwift. All rights reserved. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#elseif canImport(Foundation) +import Foundation +#elseif canImport(FoundationEmbedded) +import FoundationEmbedded +#endif + public extension FetchRequest.Predicate { /// Used to represent expressions in a predicate.