Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Swift 5.7 Improvements
Browse files Browse the repository at this point in the history
While we have some ideas to take Swift 5.7 even further, we need more
time to let the details bake. Till then, we can still offer a
quality-of-life improvement for folks using Swift 5.7, especially those
working on Advent of Code 2022!

Improving Parsing's builder limits
----------------------------------

This is perhaps the most notable improvement for now.

Currently, Parsing's builders limits the number of parsers allowed in a
block because of how many overloads need to be maintained and generated.

Using Swift 5.7's new [`buildPartialBlock`][se-0348], we can greatly
improve upon this limit:

| Builder          | Block limit `swift(<5.7)` | Block limit `swift(>=5.7)` |
| ---------------- | ------------------------- | -------------------------- |
| `OneOfBuilder`   | 10                        | ∞                          |
| `ParserBuilder`  | 6                         | 10–∞*                      |

\* Up to 10 non-`Void` captures in a block, plus unlimited `Void`
captures.

This should make working with larger builders much easier. The limit of
10 captures is arbitrary and could be expanded. If you hit it, please
[let us know][discussions]!

[se-0348]: https://github.com/apple/swift-evolution/blob/main/proposals/0348-buildpartialblock.md
[discussions]: https://github.com/pointfreeco/swift-parsing/discussions

Adding primary associated types
-------------------------------

We've added primary associated types to a number of protocols:

  * `Parser<Input, Output>`
  * `ParserPrinter<Intput, Output>`
  * `Conversion<Input, Output>`
  * `PrependableCollection<Element>`

This will allow you to express and constrain these protocols in a more
lightweight, natural manner.

Formatter parser-printer support
--------------------------------

We've added support for using formatters directly in your parser
printers with the `Formatted` parser-printer:

```swift
let total = ParsePrint {
  "TOTAL: "
  Formatted(.currency(code: "USD"))
}

try total.parse("TOTAL: $42.42")  // 42.42
try total.print(99.95)            // "TOTAL: $99.95"
```

`Formatted` takes any of the many formatters shipping in iOS 15 and more
recently.

---

We have more plans for Parsing in the coming months to take even greater
advantage of modern Swift features, and we hope to explore them soon!
stephencelis committed Dec 8, 2022
1 parent 4094025 commit 9ed8959
Showing 12 changed files with 9,857 additions and 9,182 deletions.
51 changes: 51 additions & 0 deletions Sources/Parsing/Builders/OneOfBuilder.swift
Original file line number Diff line number Diff line change
@@ -110,6 +110,40 @@ public enum OneOfBuilder {
.init(wrapped: parser)
}

@inlinable
public static func buildPartialBlock<P0: Parser>(first: P0) -> P0 {
first
}

@inlinable
public static func buildPartialBlock<P0, P1>(accumulated: P0, next: P1) -> OneOf2<P0, P1> {
.init(accumulated, next)
}

public struct OneOf2<P0: Parser, P1: Parser>: Parser
where P0.Input == P1.Input, P0.Output == P1.Output {
public let p0: P0, p1: P1

@inlinable public init(_ p0: P0, _ p1: P1) {
self.p0 = p0
self.p1 = p1
}

@inlinable public func parse(_ input: inout P0.Input) rethrows -> P0.Output {
let original = input
do { return try self.p0.parse(&input) } catch let e0 {
do {
input = original
return try self.p1.parse(&input)
} catch let e1 {
throw ParsingError.manyFailed(
[e0, e1], at: input
)
}
}
}
}

/// A parser that parses output from an optional parser.
///
/// You won't typically construct this parser directly, but instead will use standard `if`
@@ -145,6 +179,23 @@ public enum OneOfBuilder {
}
}

extension OneOfBuilder.OneOf2: ParserPrinter where P0: ParserPrinter, P1: ParserPrinter {
@inlinable
public func print(_ output: P0.Output, into input: inout P0.Input) rethrows {
let original = input
do { try self.p1.print(output, into: &input) } catch let e1 {
do {
input = original
try self.p0.print(output, into: &input)
} catch let e0 {
throw PrintingError.manyFailed(
[e1, e0], at: input
)
}
}
}
}

extension OneOfBuilder.OptionalOneOf: ParserPrinter where Wrapped: ParserPrinter {
@inlinable
public func print(_ output: Wrapped.Output, into input: inout Wrapped.Input) throws {
401 changes: 400 additions & 1 deletion Sources/Parsing/Builders/ParserBuilder.swift

Large diffs are not rendered by default.

17,975 changes: 8,960 additions & 9,015 deletions Sources/Parsing/Builders/Variadics.swift

Large diffs are not rendered by default.

79 changes: 55 additions & 24 deletions Sources/Parsing/Conversion.swift
Original file line number Diff line number Diff line change
@@ -1,28 +1,59 @@
/// Declares a type that can transform an `Input` value into an `Output` value *and* transform an
/// `Output` value back into an `Input` value.
///
/// Useful in transforming the output of a parser-printer into some new type while preserving
/// printability via ``Parser/map(_:)-18m9d``.
@rethrows public protocol Conversion {
// The type of values this conversion converts from.
associatedtype Input
#if swift(>=5.7)
/// Declares a type that can transform an `Input` value into an `Output` value *and* transform an
/// `Output` value back into an `Input` value.
///
/// Useful in transforming the output of a parser-printer into some new type while preserving
/// printability via ``Parser/map(_:)-18m9d``.
@rethrows public protocol Conversion<Input, Output> {
// The type of values this conversion converts from.
associatedtype Input

// The type of values this conversion converts to.
associatedtype Output
// The type of values this conversion converts to.
associatedtype Output

/// Attempts to transform an input into an output.
///
/// See ``Conversion/apply(_:)`` for the reverse process.
///
/// - Parameter input: An input value.
/// - Returns: A transformed output value.
func apply(_ input: Input) throws -> Output
/// Attempts to transform an input into an output.
///
/// See ``Conversion/apply(_:)`` for the reverse process.
///
/// - Parameter input: An input value.
/// - Returns: A transformed output value.
func apply(_ input: Input) throws -> Output

/// Attempts to transform an output back into an input.
///
/// The reverse process of ``Conversion/apply(_:)``.
/// Attempts to transform an output back into an input.
///
/// The reverse process of ``Conversion/apply(_:)``.
///
/// - Parameter output: An output value.
/// - Returns: An "un"-transformed input value.
func unapply(_ output: Output) throws -> Input
}
#else
/// Declares a type that can transform an `Input` value into an `Output` value *and* transform an
/// `Output` value back into an `Input` value.
///
/// - Parameter output: An output value.
/// - Returns: An "un"-transformed input value.
func unapply(_ output: Output) throws -> Input
}
/// Useful in transforming the output of a parser-printer into some new type while preserving
/// printability via ``Parser/map(_:)-18m9d``.
@rethrows public protocol Conversion {
// The type of values this conversion converts from.
associatedtype Input

// The type of values this conversion converts to.
associatedtype Output

/// Attempts to transform an input into an output.
///
/// See ``Conversion/apply(_:)`` for the reverse process.
///
/// - Parameter input: An input value.
/// - Returns: A transformed output value.
func apply(_ input: Input) throws -> Output

/// Attempts to transform an output back into an input.
///
/// The reverse process of ``Conversion/apply(_:)``.
///
/// - Parameter output: An output value.
/// - Returns: An "un"-transformed input value.
func unapply(_ output: Output) throws -> Input
}
#endif
135 changes: 98 additions & 37 deletions Sources/Parsing/Parser.swift
Original file line number Diff line number Diff line change
@@ -1,43 +1,104 @@
/// Declares a type that can incrementally parse an `Output` value from an `Input` value.
///
/// A parser attempts to parse a nebulous piece of data, represented by the `Input` associated type,
/// into something more well-structured, represented by the `Output` associated type. The parser
/// implements the ``parse(_:)-76tcw`` method, which is handed an `inout Input`, and its job is to
/// turn this into an `Output` if possible, or throw an error if it cannot.
///
/// The argument of the ``parse(_:)-76tcw`` function is `inout` because a parser will usually
/// consume some of the input in order to produce an output. For example, we can use an
/// `Int.parser()` parser to extract an integer from the beginning of a substring and consume that
/// portion of the string:
///
/// ```swift
/// var input: Substring = "123 Hello world"
///
/// try Int.parser().parse(&input) // 123
/// input // " Hello world"
/// ```
///
/// Note that this parser works on `Substring` rather than `String` because substrings expose
/// efficient ways of removing characters from its beginning. Substrings are "views" into a string,
/// specified by start and end indices. Operations like `removeFirst`, `removeLast` and others can
/// be implemented efficiently on substrings because they simply move the start and end indices,
/// whereas their implementation on strings must make a copy of the string with the characters
/// removed.
@rethrows public protocol Parser {
/// The type of values this parser parses from.
associatedtype Input
#if swift(>=5.7)
/// Declares a type that can incrementally parse an `Output` value from an `Input` value.
///
/// A parser attempts to parse a nebulous piece of data, represented by the `Input` associated
/// type, into something more well-structured, represented by the `Output` associated type. The
/// parser implements the ``parse(_:)-76tcw`` method, which is handed an `inout Input`, and its
/// job is to turn this into an `Output` if possible, or throw an error if it cannot.
///
/// The argument of the ``parse(_:)-76tcw`` function is `inout` because a parser will usually
/// consume some of the input in order to produce an output. For example, we can use an
/// `Int.parser()` parser to extract an integer from the beginning of a substring and consume that
/// portion of the string:
///
/// ```swift
/// var input: Substring = "123 Hello world"
///
/// try Int.parser().parse(&input) // 123
/// input // " Hello world"
/// ```
///
/// Note that this parser works on `Substring` rather than `String` because substrings expose
/// efficient ways of removing characters from its beginning. Substrings are "views" into a
/// string, specified by start and end indices. Operations like `removeFirst`, `removeLast` and
/// others can be implemented efficiently on substrings because they simply move the start and end
/// indices, whereas their implementation on strings must make a copy of the string with the
/// characters removed.
@rethrows public protocol Parser<Input, Output> {
/// The type of values this parser parses from.
associatedtype Input

/// The type of values parsed by this parser.
associatedtype Output
/// The type of values parsed by this parser.
associatedtype Output

associatedtype Body: Parser<Input, Output> = Self

/// Attempts to parse a nebulous piece of data into something more well-structured. Typically
/// you only call this from other `Parser` conformances, not when you want to parse a concrete
/// input.
///
/// - Parameter input: A nebulous, mutable piece of data to be incrementally parsed.
/// - Returns: A more well-structured value parsed from the given input.
func parse(_ input: inout Input) throws -> Output

@ParserBuilder var body: Body { get }
}

extension Parser {
@inlinable
public func parse(_ input: inout Body.Input) rethrows -> Body.Output {
try self.body.parse(&input)
}
}

/// Attempts to parse a nebulous piece of data into something more well-structured. Typically
/// you only call this from other `Parser` conformances, not when you want to parse a concrete
/// input.
extension Parser where Body == Self {
@inlinable
public var body: Body {
self
}
}
#else
/// Declares a type that can incrementally parse an `Output` value from an `Input` value.
///
/// - Parameter input: A nebulous, mutable piece of data to be incrementally parsed.
/// - Returns: A more well-structured value parsed from the given input.
func parse(_ input: inout Input) throws -> Output
}
/// A parser attempts to parse a nebulous piece of data, represented by the `Input` associated
/// type, into something more well-structured, represented by the `Output` associated type. The
/// parser implements the ``parse(_:)-76tcw`` method, which is handed an `inout Input`, and its
/// job is to turn this into an `Output` if possible, or throw an error if it cannot.
///
/// The argument of the ``parse(_:)-76tcw`` function is `inout` because a parser will usually
/// consume some of the input in order to produce an output. For example, we can use an
/// `Int.parser()` parser to extract an integer from the beginning of a substring and consume that
/// portion of the string:
///
/// ```swift
/// var input: Substring = "123 Hello world"
///
/// try Int.parser().parse(&input) // 123
/// input // " Hello world"
/// ```
///
/// Note that this parser works on `Substring` rather than `String` because substrings expose
/// efficient ways of removing characters from its beginning. Substrings are "views" into a
/// string, specified by start and end indices. Operations like `removeFirst`, `removeLast` and
/// others can be implemented efficiently on substrings because they simply move the start and end
/// indices, whereas their implementation on strings must make a copy of the string with the
/// characters removed.
@rethrows public protocol Parser {
/// The type of values this parser parses from.
associatedtype Input

/// The type of values parsed by this parser.
associatedtype Output

/// Attempts to parse a nebulous piece of data into something more well-structured. Typically
/// you only call this from other `Parser` conformances, not when you want to parse a concrete
/// input.
///
/// - Parameter input: A nebulous, mutable piece of data to be incrementally parsed.
/// - Returns: A more well-structured value parsed from the given input.
func parse(_ input: inout Input) throws -> Output
}
#endif

extension Parser {
/// Parse an input value into an output. This method is more ergonomic to use than
55 changes: 42 additions & 13 deletions Sources/Parsing/ParserPrinter.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,46 @@
/// A ``Parser`` that can incrementally "print" an output value back into an input.
///
/// > Note: Printing is the reverse operation of parsing, so the `Input` is essentially built up in
/// > reverse. As such, new values should be prepended to the front of the input. This allows
/// > parsers to check that the already-printed values match what is expected for any given
/// > ``Parser``.
@rethrows public protocol ParserPrinter: Parser {
/// Attempts to print a well-structured piece of data into something more nebulous.
#if swift(>=5.7)
/// A ``Parser`` that can incrementally "print" an output value back into an input.
///
/// - Parameters
/// - output: A well-structured value to be printed to the given input.
/// - input: A nebulous, mutable piece of data to be incrementally printed into.
func print(_ output: Output, into input: inout Input) throws
}
/// > Note: Printing is the reverse operation of parsing, so the `Input` is essentially built up
/// > in reverse. As such, new values should be prepended to the front of the input. This allows
/// > parsers to check that the already-printed values match what is expected for any given
/// > ``Parser``.
@rethrows public protocol ParserPrinter<Input, Output>: Parser {

associatedtype Body: ParserPrinter = Self

var body: Body { get }

/// Attempts to print a well-structured piece of data into something more nebulous.
///
/// - Parameters
/// - output: A well-structured value to be printed to the given input.
/// - input: A nebulous, mutable piece of data to be incrementally printed into.
func print(_ output: Output, into input: inout Input) throws
}

extension ParserPrinter {
@inlinable
public func print(_ output: Body.Output, into input: inout Body.Input) rethrows {
try self.body.print(output, into: &input)
}
}
#else
/// A ``Parser`` that can incrementally "print" an output value back into an input.
///
/// > Note: Printing is the reverse operation of parsing, so the `Input` is essentially built up
/// > in reverse. As such, new values should be prepended to the front of the input. This allows
/// > parsers to check that the already-printed values match what is expected for any given
/// > ``Parser``.
@rethrows public protocol ParserPrinter: Parser {
/// Attempts to print a well-structured piece of data into something more nebulous.
///
/// - Parameters
/// - output: A well-structured value to be printed to the given input.
/// - input: A nebulous, mutable piece of data to be incrementally printed into.
func print(_ output: Output, into input: inout Input) throws
}
#endif

extension ParserPrinter where Input: _EmptyInitializable {
/// Attempts to print a well-structured piece of data to something more nebulous.
36 changes: 36 additions & 0 deletions Sources/Parsing/ParserPrinters/ParseableFormatStyle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#if swift(>=5.7) && (os(iOS) || os(macOS) || os(tvOS) || os(watchOS))
import Foundation

@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
public struct Formatted<Style: ParseableFormatStyle & RegexComponent>: ParserPrinter
where
Style.Strategy.ParseInput == String,
Style.Strategy.ParseOutput == Style.RegexOutput
{
@usableFromInline
let style: Style

@inlinable
public init(_ style: Style) {
self.style = style
}

@inlinable
public func parse(_ input: inout Substring) throws -> Style.Strategy.ParseOutput {
guard let match = input.prefixMatch(of: self.style.regex)
else {
throw ParsingError.failed(
summary: "failed to process \"\(Output.self)\"",
at: input
)
}
input.removeFirst(input.distance(from: input.startIndex, to: input.endIndex))
return match.output
}

@inlinable
public func print(_ output: Style.FormatInput, into input: inout Substring) {
input.prepend(contentsOf: self.style.format(output))
}
}
#endif
8 changes: 3 additions & 5 deletions Sources/Parsing/ParserPrinters/Pipe.swift
Original file line number Diff line number Diff line change
@@ -40,13 +40,11 @@ extension Parser {
extension Parser where Input: Collection {
public func pipe<Downstream>(
@ParserBuilder _ build: () -> Downstream
) -> Parsers.Pipe<Self, Parse<ParserBuilder.ZipOV<Downstream, Parsers.PipeEnd<Self.Input>>>> {
) -> Parsers.Pipe<Self, ParserBuilder.SkipSecond<Downstream, Parsers.PipeEnd<Self.Input>>> {
.init(
upstream: self,
downstream: Parse {
build()
Parsers.PipeEnd<Input>()
})
downstream: .init(build(), Parsers.PipeEnd<Input>())
)
}
}

255 changes: 171 additions & 84 deletions Sources/Parsing/PrependableCollection.swift
Original file line number Diff line number Diff line change
@@ -1,89 +1,176 @@
import Foundation

/// A collection that supports empty initialization and the ability to prepend a sequence of
/// elements of elements to itself.
///
/// `PrependableCollection` is a specialized subset of `RangeReplaceableCollection` that is tuned to
/// incremental printing.
///
/// In fact, any `RangeReplaceableCollection` can get a conformance for free:
///
/// ```swift
/// extension MyRangeReplaceableCollection: PrependableCollection {}
/// ```
///
/// Because it is also less strict than `RangeReplaceableCollection`, it is an appropriate protocol
/// to conform to for types that cannot and should not conform to `RangeReplaceableCollection`
/// directly.
///
/// For example, `Substring.UTF8View` is a common input for string parsers to parse from, but it
/// does not conform to `RangeReplaceableCollection`. It does, however, conform to
/// `PrependableCollection` by validating and prepending the given UTF-8 bytes to its underlying
/// substring. So in order to write a parser against generic sequences of UTF-8 bytes, you would
/// constrain its input against `PrependableCollection`.
///
/// For example the following `Digits` parser is generic over an `Collection` of bytes, and its
/// printer conformance further constraints its input to be prependable.
///
/// ```swift
/// struct Digits<Input: Collection>: Parser
/// where
/// Input.Element == UTF8.CodeUnit, // Required for working with a collection of bytes (`UInt8`)
/// Input.SubSequence == Input // Required for the parser to consume from input
/// {
/// func parse(_ input: inout Input) throws -> Int {
/// // Collect all bytes between ASCII "0" and "9"
/// let prefix = input.prefix(while: { $0 >= .init(ascii: "0") && $0 <= .init(ascii: "9") })
///
/// // Attempt to convert to an `Int`
/// guard let int = Int(prefix) else {
/// struct ParseError: Error {}
/// throw ParseError()
/// }
///
/// // Incrementally consume bytes from input
/// input.removeFirst(prefix.count)
///
/// return int
/// }
/// }
///
/// extension Digits: ParserPrinter where Input: PrependableCollection {
/// func print(_ output: Int, into input: inout Input) {
/// // Convert `Int` to string's underlying bytes
/// let bytes = String(output).utf8
///
/// // Prepend bytes using `PrependableCollection` conformance.
/// input.prepend(contentsOf: bytes)
/// }
/// }
/// ```
///
/// The `Digits` parser-printer now works on any collection of UTF-8 code units, including
/// `UTF8View` and `ArraySlice<UInt8>`:
///
/// ```swift
/// var input = "123"[...].utf8
/// try Digits().parse(&input) // 123
/// try Digits().print(123, into: &input)
/// Substring(input) // "123"
/// ```
///
/// ```swift
/// var input = ArraySlice("123"[...].utf8)
/// try Digits().parse(&input) // 123
/// try Digits().print(123, into: &input)
/// Substring(decoding: input, as: UTF8.self) // "123"
/// ```
public protocol PrependableCollection: Collection, _EmptyInitializable {
/// Inserts the elements of a sequence or collection to the beginning of this collection.
///
/// The collection being prepended to allocates any additional necessary storage to hold the new
/// elements.
///
/// - Parameter newElements: The elements to append to the collection.
mutating func prepend<S: Sequence>(contentsOf newElements: S) where S.Element == Element
}
#if swift(>=5.7)
/// A collection that supports empty initialization and the ability to prepend a sequence of
/// elements of elements to itself.
///
/// `PrependableCollection` is a specialized subset of `RangeReplaceableCollection` that is tuned
/// to incremental printing.
///
/// In fact, any `RangeReplaceableCollection` can get a conformance for free:
///
/// ```swift
/// extension MyRangeReplaceableCollection: PrependableCollection {}
/// ```
///
/// Because it is also less strict than `RangeReplaceableCollection`, it is an appropriate
/// protocol to conform to for types that cannot and should not conform to
/// `RangeReplaceableCollection` directly.
///
/// For example, `Substring.UTF8View` is a common input for string parsers to parse from, but it
/// does not conform to `RangeReplaceableCollection`. It does, however, conform to
/// `PrependableCollection` by validating and prepending the given UTF-8 bytes to its underlying
/// substring. So in order to write a parser against generic sequences of UTF-8 bytes, you would
/// constrain its input against `PrependableCollection`.
///
/// For example the following `Digits` parser is generic over an `Collection` of bytes, and its
/// printer conformance further constraints its input to be prependable.
///
/// ```swift
/// struct Digits<Input: Collection>: Parser
/// where
/// Input.Element == UTF8.CodeUnit, // Required for working with a collection of bytes
/// Input.SubSequence == Input // Required for the parser to consume from input
/// {
/// func parse(_ input: inout Input) throws -> Int {
/// // Collect all bytes between ASCII "0" and "9"
/// let prefix = input.prefix(while: { $0 >= .init(ascii: "0") && $0 <= .init(ascii: "9") })
///
/// // Attempt to convert to an `Int`
/// guard let int = Int(prefix) else {
/// struct ParseError: Error {}
/// throw ParseError()
/// }
///
/// // Incrementally consume bytes from input
/// input.removeFirst(prefix.count)
///
/// return int
/// }
/// }
///
/// extension Digits: ParserPrinter where Input: PrependableCollection {
/// func print(_ output: Int, into input: inout Input) {
/// // Convert `Int` to string's underlying bytes
/// let bytes = String(output).utf8
///
/// // Prepend bytes using `PrependableCollection` conformance.
/// input.prepend(contentsOf: bytes)
/// }
/// }
/// ```
///
/// The `Digits` parser-printer now works on any collection of UTF-8 code units, including
/// `UTF8View` and `ArraySlice<UInt8>`:
///
/// ```swift
/// var input = "123"[...].utf8
/// try Digits().parse(&input) // 123
/// try Digits().print(123, into: &input)
/// Substring(input) // "123"
/// ```
///
/// ```swift
/// var input = ArraySlice("123"[...].utf8)
/// try Digits().parse(&input) // 123
/// try Digits().print(123, into: &input)
/// Substring(decoding: input, as: UTF8.self) // "123"
/// ```
public protocol PrependableCollection<Element>: Collection, _EmptyInitializable {
/// Inserts the elements of a sequence or collection to the beginning of this collection.
///
/// The collection being prepended to allocates any additional necessary storage to hold the new
/// elements.
///
/// - Parameter newElements: The elements to append to the collection.
mutating func prepend<S: Sequence>(contentsOf newElements: S) where S.Element == Element
}
#else
/// A collection that supports empty initialization and the ability to prepend a sequence of
/// elements of elements to itself.
///
/// `PrependableCollection` is a specialized subset of `RangeReplaceableCollection` that is tuned
/// to incremental printing.
///
/// In fact, any `RangeReplaceableCollection` can get a conformance for free:
///
/// ```swift
/// extension MyRangeReplaceableCollection: PrependableCollection {}
/// ```
///
/// Because it is also less strict than `RangeReplaceableCollection`, it is an appropriate
/// protocol to conform to for types that cannot and should not conform to
/// `RangeReplaceableCollection` directly.
///
/// For example, `Substring.UTF8View` is a common input for string parsers to parse from, but it
/// does not conform to `RangeReplaceableCollection`. It does, however, conform to
/// `PrependableCollection` by validating and prepending the given UTF-8 bytes to its underlying
/// substring. So in order to write a parser against generic sequences of UTF-8 bytes, you would
/// constrain its input against `PrependableCollection`.
///
/// For example the following `Digits` parser is generic over an `Collection` of bytes, and its
/// printer conformance further constraints its input to be prependable.
///
/// ```swift
/// struct Digits<Input: Collection>: Parser
/// where
/// Input.Element == UTF8.CodeUnit, // Required for working with a collection of bytes
/// Input.SubSequence == Input // Required for the parser to consume from input
/// {
/// func parse(_ input: inout Input) throws -> Int {
/// // Collect all bytes between ASCII "0" and "9"
/// let prefix = input.prefix(while: { $0 >= .init(ascii: "0") && $0 <= .init(ascii: "9") })
///
/// // Attempt to convert to an `Int`
/// guard let int = Int(prefix) else {
/// struct ParseError: Error {}
/// throw ParseError()
/// }
///
/// // Incrementally consume bytes from input
/// input.removeFirst(prefix.count)
///
/// return int
/// }
/// }
///
/// extension Digits: ParserPrinter where Input: PrependableCollection {
/// func print(_ output: Int, into input: inout Input) {
/// // Convert `Int` to string's underlying bytes
/// let bytes = String(output).utf8
///
/// // Prepend bytes using `PrependableCollection` conformance.
/// input.prepend(contentsOf: bytes)
/// }
/// }
/// ```
///
/// The `Digits` parser-printer now works on any collection of UTF-8 code units, including
/// `UTF8View` and `ArraySlice<UInt8>`:
///
/// ```swift
/// var input = "123"[...].utf8
/// try Digits().parse(&input) // 123
/// try Digits().print(123, into: &input)
/// Substring(input) // "123"
/// ```
///
/// ```swift
/// var input = ArraySlice("123"[...].utf8)
/// try Digits().parse(&input) // 123
/// try Digits().print(123, into: &input)
/// Substring(decoding: input, as: UTF8.self) // "123"
/// ```
public protocol PrependableCollection: Collection, _EmptyInitializable {
/// Inserts the elements of a sequence or collection to the beginning of this collection.
///
/// The collection being prepended to allocates any additional necessary storage to hold the new
/// elements.
///
/// - Parameter newElements: The elements to append to the collection.
mutating func prepend<S: Sequence>(contentsOf newElements: S) where S.Element == Element
}
#endif

extension PrependableCollection {
/// Creates a new instance of a collection containing the elements of a sequence.
21 changes: 18 additions & 3 deletions Sources/variadics-generator/VariadicsGenerator.swift
Original file line number Diff line number Diff line change
@@ -90,7 +90,15 @@ struct VariadicsGenerator: ParsableCommand {
var generatePathZips = false

func run() throws {
output("// BEGIN AUTO-GENERATED CONTENT\n\n")
output(
"""
// BEGIN AUTO-GENERATED CONTENT
#if swift(<5.7)
"""
)

if self.generateZips {
for arity in 2...6 {
@@ -99,7 +107,7 @@ struct VariadicsGenerator: ParsableCommand {
}

if self.generateOneOfs {
for arity in 2...10 {
for arity in 3...10 {
emitOneOfDeclaration(arity: arity)
}
}
@@ -110,7 +118,14 @@ struct VariadicsGenerator: ParsableCommand {
}
}

output("// END AUTO-GENERATED CONTENT\n")
output(
"""
#endif
// END AUTO-GENERATED CONTENT
"""
)
}

func emitZipDeclarations(arity: Int) {
23 changes: 23 additions & 0 deletions Tests/ParsingTests/ParseableFormatTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#if swift(>=5.7) && (os(iOS) || os(macOS) || os(tvOS) || os(watchOS))
import Parsing
import XCTest

@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
final class ParseableFormatStyleTests: XCTestCase {
func testFormatted() {
let p = ParsePrint {
"TOTAL: "
Formatted(.currency(code: "USD"))
}

XCTAssertEqual(
try p.parse("TOTAL: $42.42"),
42.42
)
XCTAssertEqual(
try p.print(42.42),
"TOTAL: $42.42"
)
}
}
#endif

0 comments on commit 9ed8959

Please sign in to comment.