Skip to content

Revise the typed throws design #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 88 additions & 79 deletions Sources/BinaryParsing/Parser Types/ParserSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ public protocol ExpressibleByParsing {
}

extension ExpressibleByParsing {
@_alwaysEmitIntoClient
public init(
parsing data: some ParserSpanProvider
) throws(ThrownParsingError) {
self = try data.withParserSpan(Self.init(parsing:))
}

@_alwaysEmitIntoClient
@_disfavoredOverload
public init(parsing data: some RandomAccessCollection<UInt8>)
throws(ThrownParsingError)
{
Expand All @@ -41,112 +50,112 @@ extension RandomAccessCollection<UInt8> {
) throws(ThrownParsingError) -> T? {
#if canImport(Foundation)
if let data = self as? Foundation.Data {
do {
return try data.withUnsafeBytes { buffer -> T in
var span = ParserSpan(_unsafeBytes: buffer)
return try body(&span)
}
} catch {
// Workaround for lack of typed-throwing API on Data
// swift-format-ignore: NeverForceUnwrap
throw error as! ThrownParsingError
let result = data.withUnsafeBytes { buffer in
var span = ParserSpan(_unsafeBytes: buffer)
return Result<T, ThrownParsingError> { try body(&span) }
}
switch result {
case .success(let t): return t
case .failure(let e): throw e
}
}
#endif
do {
return try self.withContiguousStorageIfAvailable { buffer in
let rawBuffer = UnsafeRawBufferPointer(buffer)
var span = ParserSpan(_unsafeBytes: rawBuffer)
return try body(&span)
}
} catch {
// Workaround for lack of typed-throwing API on Collection
// swift-format-ignore: NeverForceUnwrap
throw error as! ThrownParsingError

let result = self.withContiguousStorageIfAvailable { buffer in
let rawBuffer = UnsafeRawBufferPointer(buffer)
var span = ParserSpan(_unsafeBytes: rawBuffer)
return Result<T, ThrownParsingError> { try body(&span) }
}
switch result {
case .success(let t): return t
case .failure(let e): throw e
case nil: return nil
}
}
}

// MARK: ParserSpanProvider

public protocol ParserSpanProvider {
func withParserSpan<T>(
_ body: (inout ParserSpan) throws(ThrownParsingError) -> T
) throws(ThrownParsingError) -> T
func withParserSpan<T, E>(
_ body: (inout ParserSpan) throws(E) -> T
) throws(E) -> T
}

#if canImport(Foundation)
extension Data: ParserSpanProvider {
extension ParserSpanProvider {
#if !$Embedded
@_alwaysEmitIntoClient
@inlinable
public func withParserSpan<T>(
_ body: (inout ParserSpan) throws(ThrownParsingError) -> T
) throws(ThrownParsingError) -> T {
do {
return try withUnsafeBytes { buffer -> T in
// FIXME: RawSpan getter
// var span = ParserSpan(buffer.bytes)
var span = ParserSpan(_unsafeBytes: buffer)
return try body(&span)
}
} catch {
// Workaround for lack of typed-throwing API on Data
// swift-format-ignore: NeverForceUnwrap
throw error as! ThrownParsingError
usingRange range: inout ParserRange,
_ body: (inout ParserSpan) throws -> T
) throws -> T {
try withParserSpan { span in
var subspan = try span.seeking(toRange: range)
defer { range = subspan.parserRange }
return try body(&subspan)
}
}
#endif

@_alwaysEmitIntoClient
@inlinable
public func withParserSpan<T>(
usingRange range: inout ParserRange,
_ body: (inout ParserSpan) throws(ThrownParsingError) -> T
) throws(ThrownParsingError) -> T {
do {
return try withUnsafeBytes { (buffer) throws(ThrownParsingError) -> T in
// FIXME: RawSpan getter
// var span = try ParserSpan(buffer.bytes)
var span = try ParserSpan(_unsafeBytes: buffer)
.seeking(toRange: range)
defer {
range = span.parserRange
}
return try body(&span)
}
} catch {
// Workaround for lack of typed-throwing API on Data
// swift-format-ignore: NeverForceUnwrap
throw error as! ThrownParsingError
_ body: (inout ParserSpan) throws(ParsingError) -> T
) throws(ParsingError) -> T {
try withParserSpan { (span) throws(ParsingError) in
var subspan = try span.seeking(toRange: range)
defer { range = subspan.parserRange }
return try body(&subspan)
}
}
}
#endif

extension ParserSpanProvider where Self: RandomAccessCollection<UInt8> {
@discardableResult
#if canImport(Foundation)
extension Data: ParserSpanProvider {
@inlinable
public func withParserSpan<T>(
_ body: (inout ParserSpan) throws(ThrownParsingError) -> T
) throws(ThrownParsingError) -> T {
do {
guard
let result = try self.withContiguousStorageIfAvailable({ buffer in
// FIXME: RawSpan getter
// var span = ParserSpan(UnsafeRawBufferPointer(buffer).bytes)
let rawBuffer = UnsafeRawBufferPointer(buffer)
var span = ParserSpan(_unsafeBytes: rawBuffer)
return try body(&span)
})
else {
throw ParsingError(status: .userError, location: 0)
}
return result
} catch {
// Workaround for lack of typed-throwing API on Collection
// swift-format-ignore: NeverForceUnwrap
throw error as! ThrownParsingError
public func withParserSpan<T, E>(
_ body: (inout ParserSpan) throws(E) -> T
) throws(E) -> T {
let result = withUnsafeBytes { buffer in
var span = ParserSpan(_unsafeBytes: buffer)
return Result<T, E> { () throws(E) in try body(&span) }
}
switch result {
case .success(let t): return t
case .failure(let e): throw e
}
}
}
#endif

extension [UInt8]: ParserSpanProvider {
public func withParserSpan<T, E>(
_ body: (inout ParserSpan) throws(E) -> T
) throws(E) -> T {
let result = self.withUnsafeBytes { rawBuffer in
var span = ParserSpan(_unsafeBytes: rawBuffer)
return Result<T, E> { () throws(E) in try body(&span) }
}
switch result {
case .success(let t): return t
case .failure(let e): throw e
}
}
}

extension [UInt8]: ParserSpanProvider {}
extension ArraySlice<UInt8>: ParserSpanProvider {}
extension ArraySlice<UInt8>: ParserSpanProvider {
public func withParserSpan<T, E>(
_ body: (inout ParserSpan) throws(E) -> T
) throws(E) -> T {
let result = self.withUnsafeBytes { rawBuffer in
var span = ParserSpan(_unsafeBytes: rawBuffer)
return Result<T, E> { () throws(E) in try body(&span) }
}
switch result {
case .success(let t): return t
case .failure(let e): throw e
}
}
}
2 changes: 1 addition & 1 deletion Sources/BinaryParsing/Parser Types/ParsingError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ extension ParsingError.Status: CustomStringConvertible {
/// In a build for embedded Swift, `ThrownParsingError` instead aliases the
/// specific `ParsingError` type. Because embedded Swift supports only
/// fully-typed throws, and not the existential `any Error`, this allows you
/// to still take use error-throwing APIs in an embedded context.
/// to still use error-throwing APIs in an embedded context.
public typealias ThrownParsingError = any Error
#else
// Documentation is built using the non-embedded build.
Expand Down
28 changes: 23 additions & 5 deletions Sources/BinaryParsing/Parsers/Array.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ extension Array where Element == UInt8 {
}

extension Array {
#if !$Embedded
@inlinable
@lifetime(&input)
public init(
parsing input: inout ParserSpan,
count: some FixedWidthInteger,
parser: (inout ParserSpan) throws(ThrownParsingError) -> Element
) throws(ThrownParsingError) {
parser: (inout ParserSpan) throws -> Element
) throws {
let count = try Int(throwingOnOverflow: count)
self = []
self.reserveCapacity(count)
Expand All @@ -50,13 +51,30 @@ extension Array {
try self.append(parser(&input))
}
}
#endif

@inlinable
@lifetime(&input)
public init(
public init<E>(
parsing input: inout ParserSpan,
count: Int,
parser: (inout ParserSpan) throws(E) -> Element
) throws(E) {
self = []
self.reserveCapacity(count)
// This doesn't throw (e.g. on empty) because `parser` can produce valid
// values no matter the state of `input`.
for _ in 0..<count {
try self.append(parser(&input))
}
}

@inlinable
@lifetime(&input)
public init<E>(
parsingAll input: inout ParserSpan,
parser: (inout ParserSpan) throws(ThrownParsingError) -> Element
) throws(ThrownParsingError) {
parser: (inout ParserSpan) throws(E) -> Element
) throws(E) {
self = []
while !input.isEmpty {
try self.append(parser(&input))
Expand Down
Loading