diff --git a/CHANGELOG.md b/CHANGELOG.md index 91c5da4..c6cabe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Change Log +## [1.1.0] - 2026-06-26 + +### Changed + +- Adopted Swift's Approachable Concurrency upcoming features + (`NonisolatedNonsendingByDefault` and `InferIsolatedConformances`). The public + `async` entry points (`CIFP(url:)`, `CIFP(bytes:)`, and `linked()`) now run on + the caller's executor by default, and the streaming line readers are annotated + `@concurrent` so file and byte iteration keep running off the caller's + executor. No public signatures changed. + +### Internal + +- Removed the remaining `nonisolated(unsafe)` escape hatches: the two + header-parsing regexes are now compiled once per parse on the builder instead + of stored as shared unsafe statics. +- Dropped a vestigial `@preconcurrency` from `import RegexBuilder`; the module is + fully `Sendable`-audited under Swift 6. + ## [1.0.0] - 2026-01-17 Initial release. diff --git a/Package.swift b/Package.swift index bf81a41..e4cee33 100644 --- a/Package.swift +++ b/Package.swift @@ -3,6 +3,11 @@ import PackageDescription +let approachableConcurrency: [SwiftSetting] = [ + .enableUpcomingFeature("NonisolatedNonsendingByDefault"), + .enableUpcomingFeature("InferIsolatedConformances") +] + let package = Package( name: "SwiftCIFP", defaultLocalization: "en", @@ -22,11 +27,13 @@ let package = Package( targets: [ .target( name: "SwiftCIFP", - resources: [.process("Resources")] + resources: [.process("Resources")], + swiftSettings: approachableConcurrency ), .testTarget( name: "SwiftCIFPTests", - dependencies: ["SwiftCIFP"] + dependencies: ["SwiftCIFP"], + swiftSettings: approachableConcurrency ) ], swiftLanguageModes: [.v5, .v6] @@ -41,7 +48,8 @@ let package = Package( .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "ZIPFoundation", package: "ZIPFoundation"), .product(name: "Progress", package: "Progress.swift") - ] + ], + swiftSettings: approachableConcurrency ) ) #endif diff --git a/Sources/SwiftCIFP/CIFP.swift b/Sources/SwiftCIFP/CIFP.swift index b9ae127..f032d83 100644 --- a/Sources/SwiftCIFP/CIFP.swift +++ b/Sources/SwiftCIFP/CIFP.swift @@ -3,7 +3,7 @@ import Foundation #if canImport(CoreLocation) import CoreLocation #endif -@preconcurrency import RegexBuilder +import RegexBuilder /// Container for CIFP (Coded Instrument Flight Procedures) data. /// @@ -361,17 +361,27 @@ func expandRunwayTransitionId(_ transitionId: String) -> [String] { /// Builder for aggregating parsed records into CIFP. private struct CIFPBuilder { - // MARK: - Static Regex Patterns + // MARK: - Date Parsing + + /// Parses the creation date from HDR01. + private static let creationDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "dd-MMM-yyyy" + formatter.locale = Locale(identifier: "en_US_POSIX") + return formatter + }() + + // MARK: - Header-Parsing Patterns /// Matches "VOLUME" followed by whitespace and a 4-digit cycle number. - nonisolated(unsafe) private static let volumeCycleRegex = Regex { + private let volumeCycleRegex = Regex { "VOLUME" OneOrMore(.whitespace) Capture { Repeat(.digit, count: 4) } } /// Matches a date in DD-MMM-YYYY format (e.g., "15-JAN-2024"). - nonisolated(unsafe) private static let creationDateRegex = Regex { + private let creationDateRegex = Regex { Repeat(.digit, count: 2) "-" Repeat("A"..."Z", count: 3) @@ -379,14 +389,6 @@ private struct CIFPBuilder { Repeat(.digit, count: 4) } - /// Parses the creation date from HDR01. - private static let creationDateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "dd-MMM-yyyy" - formatter.locale = Locale(identifier: "en_US_POSIX") - return formatter - }() - var headerRecords: [HeaderRecord] = [] var gridMORAs: [GridMORA] = [] var vhfNavaids: [String: VHFNavaid] = [:] @@ -569,7 +571,7 @@ private struct CIFPBuilder { } // Fall back to looking for "VOLUME XXXX" pattern for text in headerText { - guard let match = text.firstMatch(of: Self.volumeCycleRegex) else { continue } + guard let match = text.firstMatch(of: volumeCycleRegex) else { continue } let cycleStr = String(match.1) guard let c = Cycle(yymm: cycleStr) else { continue } return c @@ -1080,7 +1082,7 @@ private struct CIFPBuilder { private func parseCreationDate(from hdr01: String) -> DateComponents? { // Look for DD-MMM-YYYY pattern - guard let match = hdr01.firstMatch(of: Self.creationDateRegex) else { + guard let match = hdr01.firstMatch(of: creationDateRegex) else { return nil } let dateStr = String(match.0) diff --git a/Sources/SwiftCIFP/Parser/CIFPLineReader.swift b/Sources/SwiftCIFP/Parser/CIFPLineReader.swift index 427bcc1..1205f88 100644 --- a/Sources/SwiftCIFP/Parser/CIFPLineReader.swift +++ b/Sources/SwiftCIFP/Parser/CIFPLineReader.swift @@ -171,6 +171,7 @@ where Source.Element == UInt8, Source: Sendable { lineBuffer.reserveCapacity(lineBufferCapacity) } + @concurrent mutating func next() async throws -> [UInt8]? { lineBuffer.removeAll(keepingCapacity: true)