From 558a69ed986b653c28aaad0926bbe7f4c612da35 Mon Sep 17 00:00:00 2001 From: Tim Morgan Date: Fri, 26 Jun 2026 23:46:32 -0700 Subject: [PATCH 1/2] Adopt Approachable Concurrency and migrate to modern structured concurrency Enable the Approachable Concurrency upcoming features on every target (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 reader's next() is annotated @concurrent so file and byte iteration keep running off the caller's executor. SwiftCIFP was already close to modern structured concurrency (actor-based data model, async inits, hand-rolled AsyncSequence readers, async @Sendable resolver closures, zero GCD/threads/locks), so the migration focused on removing the remaining escape hatches: - Remove both nonisolated(unsafe) static-regex escape hatches: the two header-parsing regexes now live as instance stored properties on CIFPBuilder, compiled once per parse rather than as shared unsafe statics (no perf regression). - Drop a vestigial @preconcurrency from import RegexBuilder; the module is fully Sendable-audited under Swift 6 and builds clean. The remaining @unchecked Sendable conformances (UnitSlope and UnitConverterDegrees) are unavoidable Foundation Dimension/UnitConverter patterns and are kept by design. No public signatures changed. Co-Authored-By: Claude Opus 4.8 (1M context) --- Package.swift | 14 +++++++-- Sources/SwiftCIFP/CIFP.swift | 30 ++++++++++--------- Sources/SwiftCIFP/Parser/CIFPLineReader.swift | 1 + 3 files changed, 28 insertions(+), 17 deletions(-) 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) From 6b11542cf4cdb4bcf55ae198e256122085c679d9 Mon Sep 17 00:00:00 2001 From: Tim Morgan Date: Fri, 26 Jun 2026 23:47:22 -0700 Subject: [PATCH 2/2] Bump version to 1.1.0 Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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.