├── Sources
├── Parsing
│ ├── ParserPrinters
│ │ ├── Newline.swift
│ │ ├── Skip.swift
│ │ ├── Consumed.swift
│ │ ├── Backtracking.swift
│ │ ├── ParseableFormatStyle.swift
│ │ ├── First.swift
│ │ ├── Optional.swift
│ │ ├── From.swift
│ │ ├── Literal.swift
│ │ ├── End.swift
│ │ ├── Not.swift
│ │ ├── CharacterSet.swift
│ │ ├── Bool.swift
│ │ ├── Optionally.swift
│ │ ├── Conditional.swift
│ │ ├── OneOfMany.swift
│ │ ├── Lazy.swift
│ │ ├── Peek.swift
│ │ ├── Fail.swift
│ │ ├── Rest.swift
│ │ ├── Always.swift
│ │ ├── Pullback.swift
│ │ ├── ReplaceError.swift
│ │ ├── AnyParserPrinter.swift
│ │ ├── Filter.swift
│ │ └── PrefixThrough.swift
│ ├── EmptyInitializable.swift
│ ├── Documentation.docc
│ │ ├── Extensions
│ │ │ ├── OneOf.md
│ │ │ ├── Parse.md
│ │ │ ├── Conversions.md
│ │ │ └── Parser.md
│ │ ├── Articles
│ │ │ ├── Parsers
│ │ │ │ ├── UUID.md
│ │ │ │ ├── CharacterSet.md
│ │ │ │ ├── String.md
│ │ │ │ ├── Bool.md
│ │ │ │ ├── Float.md
│ │ │ │ ├── CaseIterable.md
│ │ │ │ └── Int.md
│ │ │ └── Roundtripping.md
│ │ └── Parsing.md
│ ├── ConvertingError.swift
│ ├── Parsers
│ │ ├── Parsers.swift
│ │ ├── Stream.swift
│ │ ├── AnyParser.swift
│ │ ├── FlatMap.swift
│ │ └── CompactMap.swift
│ ├── Conversions
│ │ ├── Conversions.swift
│ │ ├── Identity.swift
│ │ ├── UTF8View.swift
│ │ ├── FixedWidthInteger.swift
│ │ ├── Enum.swift
│ │ ├── Data.swift
│ │ ├── BinaryFloatingPoint.swift
│ │ ├── LosslessStringConvertible.swift
│ │ ├── ConversionMap.swift
│ │ ├── ParseableFormatStyleConversion.swift
│ │ ├── JSON.swift
│ │ ├── String.swift
│ │ └── Substring.swift
│ ├── Internal
│ │ └── AnyEquatable.swift
│ ├── CountingRange.swift
│ ├── Conversion.swift
│ ├── ParserPrinter.swift
│ └── PrintingError.swift
└── swift-parsing-benchmark
│ ├── Common
│ ├── ParsingError.swift
│ └── Benchmarking.swift
│ ├── main.swift
│ ├── UUID.swift
│ ├── PrefixUpTo.swift
│ ├── Bool.swift
│ ├── Color.swift
│ ├── StringAbstractions.swift
│ ├── CSV.swift
│ └── Arithmetic.swift
├── .gitignore
├── Parsing.playground
├── contents.xcplayground
└── Contents.swift
├── .editorconfig
├── Parsing.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ ├── IDEWorkspaceChecks.plist
│ ├── swiftpm
│ └── Package.resolved
│ └── xcschemes
│ ├── Parsing.xcscheme
│ ├── variadics-generator.xcscheme
│ └── swift-parsing-benchmark.xcscheme
├── Tests
└── ParsingTests
│ ├── StartsWithTests.swift
│ ├── RegressionTests.swift
│ ├── ReplaceErrorTests.swift
│ ├── AlwaysTests.swift
│ ├── ParserTests.swift
│ ├── LazyTests.swift
│ ├── StreamTests.swift
│ ├── ConsumedTests.swift
│ ├── FirstTests.swift
│ ├── ParseableFormatTests.swift
│ ├── BacktrackTests.swift
│ ├── WhitespaceTests.swift
│ ├── SkipTests.swift
│ ├── ConditionalTests.swift
│ ├── UUIDTests.swift
│ ├── BoolTests.swift
│ ├── VariadicTests.swift
│ ├── CaseIterableTests.swift
│ ├── MapTests.swift
│ ├── CompactMapTests.swift
│ ├── FilterTests.swift
│ ├── FailTests.swift
│ ├── FlatMapTests.swift
│ ├── PipeEndTests.swift
│ ├── PrefixUpToTests.swift
│ ├── PrefixThroughTests.swift
│ ├── PipeTests.swift
│ ├── FromSubstringTests.swift
│ ├── RestTests.swift
│ ├── OptionallyTests.swift
│ ├── DigitsTests.swift
│ ├── EndTests.swift
│ ├── OneOfBuilderTests.swift
│ ├── UTF8Tests.swift
│ ├── NotTests.swift
│ ├── AnyParserTests.swift
│ ├── IntTests.swift
│ └── CharacterSetTests.swift
├── .github
└── workflows
│ ├── format.yml
│ ├── ci.yml
│ └── release.yml
├── LICENSE
├── Package.swift
├── Makefile
└── Package.resolved
/Sources/Parsing/ParserPrinters/Newline.swift:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Sources/swift-parsing-benchmark/Common/ParsingError.swift:
--------------------------------------------------------------------------------
1 | struct ParsingError: Error {}
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /.swiftpm
4 | /Packages
5 | /*.xcodeproj
6 | xcuserdata/
7 |
--------------------------------------------------------------------------------
/Sources/Parsing/EmptyInitializable.swift:
--------------------------------------------------------------------------------
1 | public protocol _EmptyInitializable {
2 | init()
3 | }
4 |
--------------------------------------------------------------------------------
/Sources/Parsing/Documentation.docc/Extensions/OneOf.md:
--------------------------------------------------------------------------------
1 | # ``Parsing/OneOf``
2 |
3 | ## Topics
4 |
5 | ### Builder
6 |
7 | - ``OneOfBuilder``
8 |
--------------------------------------------------------------------------------
/Sources/Parsing/Documentation.docc/Extensions/Parse.md:
--------------------------------------------------------------------------------
1 | # ``Parsing/Parse``
2 |
3 | ## Topics
4 |
5 | ### Builder
6 |
7 | - ``ParserBuilder``
8 |
--------------------------------------------------------------------------------
/Parsing.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 2
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/Parsing.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Sources/Parsing/ConvertingError.swift:
--------------------------------------------------------------------------------
1 | @usableFromInline
2 | struct ConvertingError: Error {
3 | @usableFromInline
4 | let message: String
5 |
6 | @usableFromInline
7 | init(_ message: String = "") {
8 | self.message = message
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Parsing.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/StartsWithTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class StartsWithTests: XCTestCase {
5 | func testStartsWith() {
6 | var str = "Hello, world!"[...].utf8
7 | XCTAssertNoThrow(try StartsWith("Hello".utf8).parse(&str))
8 | XCTAssertEqual(", world!", Substring(str))
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/Parsing/Parsers/Parsers.swift:
--------------------------------------------------------------------------------
1 | /// A namespace for types that serve as parsers.
2 | ///
3 | /// The various operators defined as extensions on ``Parser`` implement their functionality as
4 | /// classes or structures that extend this enumeration. For example, the ``Parser/map(_:)-4hsj5``
5 | /// operator returns a ``Parsers/Map`` parser.
6 | public enum Parsers {}
7 |
--------------------------------------------------------------------------------
/Sources/Parsing/Conversions/Conversions.swift:
--------------------------------------------------------------------------------
1 | /// A namespace for types that serve as conversions.
2 | ///
3 | /// The various operators defined as extensions on ``Conversion`` implement their functionality as
4 | /// classes or structures that extend this enumeration. For example, the ``Conversion/map(_:)``
5 | /// operator returns a ``Map`` conversion.
6 | public enum Conversions {}
7 |
--------------------------------------------------------------------------------
/Sources/Parsing/Documentation.docc/Articles/Parsers/UUID.md:
--------------------------------------------------------------------------------
1 | # UUID
2 |
3 | A parser that consumes a `UUID` value from the beginning of a string.
4 |
5 | For example:
6 |
7 | ```swift
8 | try Parse {
9 | UUID.parser()
10 | ","
11 | Bool.parser()
12 | }
13 | .parse("deadbeef-dead-beef-dead-beefdeadbeef,true")
14 | // (DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF, true)
15 | ```
16 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/RegressionTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 |
3 | // https://github.com/pointfreeco/swift-parsing/discussions/290#discussioncomment-5439338
4 | enum ValueOrEmpty {
5 | case value(Double)
6 | case empty
7 |
8 | static func parser() -> some Parser {
9 | OneOf {
10 | Double.parser().map(Self.value)
11 | "".map { .empty }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/ReplaceErrorTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class ReplaceErrorTests: XCTestCase {
5 | func testBacktracks() {
6 | var input = "123"[...]
7 | XCTAssertEqual(
8 | 0,
9 | Parse(input: Substring.self) {
10 | Int.parser()
11 | "!"
12 | }.replaceError(with: 0).parse(&input))
13 | XCTAssertEqual("123", input)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/Parsing/Documentation.docc/Articles/Parsers/CharacterSet.md:
--------------------------------------------------------------------------------
1 | # CharacterSet
2 |
3 | A parser that consumes the characters contained in a `CharacterSet` from the beginning of a string.
4 |
5 | For example:
6 |
7 | ```swift
8 | Parse {
9 | CharacterSet.alphanumerics
10 | CharacterSet.punctuationCharacters
11 | CharacterSet.alphanumerics
12 | }
13 | .parse("Hello...World") // ("Hello", "...", "World")
14 | ```
15 |
--------------------------------------------------------------------------------
/Sources/Parsing/Conversions/Identity.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Conversions {
4 | public struct Identity: Conversion {
5 | @inlinable
6 | public init() {}
7 |
8 | @inlinable
9 | @inline(__always)
10 | public func apply(_ input: Value) -> Value {
11 | input
12 | }
13 |
14 | @inlinable
15 | @inline(__always)
16 | public func unapply(_ output: Value) -> Value {
17 | output
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/AlwaysTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class AlwaysTests: XCTestCase {
5 | func testAlways() {
6 | var input = "Hello, world!"[...]
7 | XCTAssertEqual(42, Always(42).parse(&input))
8 | XCTAssertEqual("Hello, world!", input)
9 | }
10 |
11 | func testMap() {
12 | var input = "Hello, world!"[...]
13 | XCTAssertEqual(43, Always(42).map { $0 + 1 }.parse(&input))
14 | XCTAssertEqual("Hello, world!", input)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/swift-parsing-benchmark/main.swift:
--------------------------------------------------------------------------------
1 | import Benchmark
2 | import Parsing
3 |
4 | Benchmark.main(
5 | [
6 | defaultBenchmarkSuite,
7 | arithmeticSuite,
8 | binaryDataSuite,
9 | boolSuite,
10 | colorSuite,
11 | csvSuite,
12 | dateSuite,
13 | httpSuite,
14 | jsonSuite,
15 | numericsSuite,
16 | prefixUpToSuite,
17 | raceSuite,
18 | readmeExampleSuite,
19 | stringAbstractionsSuite,
20 | uuidSuite,
21 | xcodeLogsSuite,
22 | ]
23 | )
24 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/ParserTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class ParserTests: XCTestCase {
5 | func testNonIncrementalParsingValidatesEnd() {
6 | XCTAssertThrowsError(try Int.parser().parse("123 Hello")) { error in
7 | XCTAssertEqual(
8 | """
9 | error: unexpected input
10 | --> input:1:4
11 | 1 | 123 Hello
12 | | ^ expected end of input
13 | """,
14 | "\(error)"
15 | )
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/Parsing/Documentation.docc/Articles/Roundtripping.md:
--------------------------------------------------------------------------------
1 | # Round-tripping
2 |
3 | An important property that parser-printers must satisfy in order to guarantee that large, complex
4 | parser-printers build from simpler ones will behave as you expect.
5 |
6 | A parser printer `p` is said to be round-tripping if:
7 |
8 | For every `input` for which `p.parse(input)` does not throw, `p.print(p.parse(input))` is equal to
9 | `input`.
10 |
11 | And:
12 |
13 | For every `output` for which `p.print(output)` does not throw, `p.parse(p.print(output))` is equal
14 | to `output`.
15 |
--------------------------------------------------------------------------------
/Sources/Parsing/Internal/AnyEquatable.swift:
--------------------------------------------------------------------------------
1 | @usableFromInline
2 | func isEqual(_ lhs: Any, _ rhs: Any) -> Bool {
3 | func open(_: LHS.Type) -> Bool? {
4 | (Box.self as? AnyEquatable.Type)?.isEqual(lhs, rhs)
5 | }
6 | return _openExistential(type(of: lhs), do: open) ?? false
7 | }
8 |
9 | private enum Box {}
10 |
11 | private protocol AnyEquatable {
12 | static func isEqual(_ lhs: Any, _ rhs: Any) -> Bool
13 | }
14 |
15 | extension Box: AnyEquatable where T: Equatable {
16 | fileprivate static func isEqual(_ lhs: Any, _ rhs: Any) -> Bool {
17 | lhs as? T == rhs as? T
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/LazyTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class LazyTests: XCTestCase {
5 | func testBasics() {
6 | var input = "123 Hello"[...]
7 |
8 | var evaluated = 0
9 | let parser = Lazy> {
10 | evaluated += 1
11 | return Always(())
12 | }
13 |
14 | XCTAssertEqual(0, evaluated, "has not evaluated")
15 | XCTAssertNotNil(parser.parse(&input))
16 | XCTAssertEqual(1, evaluated, "evaluated")
17 | XCTAssertNotNil(parser.parse(&input))
18 | XCTAssertEqual(1, evaluated, "did not re-evaluate")
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/StreamTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class StreamTests: XCTestCase {
5 | func testBasics() throws {
6 | var stream = AnyIterator(
7 | sequence(state: 0) { state -> ArraySlice? in
8 | state += 1
9 | return state <= 20 ? ArraySlice("\(state)\n".utf8) : nil
10 | }
11 | )
12 |
13 | let output = try XCTUnwrap(
14 | Stream {
15 | Int.parser(of: ArraySlice.self)
16 | Array("\n".utf8)
17 | }
18 | .parse(&stream)
19 | )
20 |
21 | XCTAssertEqual(Array(output), Array(1...20))
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Parsing/Documentation.docc/Extensions/Conversions.md:
--------------------------------------------------------------------------------
1 | # ``Parsing/Conversions``
2 |
3 | ## Topics
4 |
5 | ### Common conversions
6 |
7 | * ``Conversion/memberwise(_:)``
8 | * ``Conversion/case(_:)-4j2n7``
9 | * ``Conversion/string-swift.type.property-3u2b5``
10 | * ``Conversion/string-swift.type.property-9owth``
11 | * ``Conversion/int-swift.type.property``
12 | * ``Conversion/double-swift.type.property``
13 | * ``Conversion/json(_:decoder:encoder:)-swift.type.method``
14 | * ``Conversion/formatted(_:)-swift.type.method``
15 | * ``Conversion/representing(_:)-swift.type.method``
16 | * ``Conversion/lossless(_:)-swift.type.method``
17 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/ConsumedTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class ConsumedTests: XCTestCase {
5 | func testConsumed() throws {
6 | struct ConsumedWhitespace: Parser {
7 | var body: some Parser {
8 | Consumed { Whitespace() }
9 | }
10 | }
11 |
12 | var input = " \r \t\t \r\n \n\r Hello, world!"[...].utf8
13 | XCTAssertEqual(
14 | " \r \t\t \r\n \n\r ",
15 | try Substring(ConsumedWhitespace().parse(&input))
16 | )
17 | XCTAssertEqual("Hello, world!", Substring(input))
18 |
19 | input = ""[...].utf8
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/FirstTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class FirstTests: XCTestCase {
5 | func testSuccess() {
6 | var input = "Hello, world!"[...]
7 | XCTAssertEqual("H", try First().parse(&input))
8 | XCTAssertEqual("ello, world!", input)
9 | }
10 |
11 | func testFailure() {
12 | var input = ""[...]
13 | XCTAssertThrowsError(try First().parse(&input)) { error in
14 | XCTAssertEqual(
15 | """
16 | error: unexpected input
17 | --> input:1:1
18 | 1 |
19 | | ^ expected element
20 | """,
21 | "\(error)"
22 | )
23 | }
24 | XCTAssertEqual("", input)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Parsing/CountingRange.swift:
--------------------------------------------------------------------------------
1 | public protocol CountingRange {
2 | var minimum: Int { get }
3 | var maximum: Int? { get }
4 | }
5 |
6 | extension Int: CountingRange {
7 | public var minimum: Int { self }
8 | public var maximum: Int? { self }
9 | }
10 |
11 | extension ClosedRange: CountingRange where Bound == Int {
12 | public var minimum: Int { self.lowerBound }
13 | public var maximum: Int? { self.upperBound }
14 | }
15 |
16 | extension PartialRangeFrom: CountingRange where Bound == Int {
17 | public var minimum: Int { self.lowerBound }
18 | public var maximum: Int? { nil }
19 | }
20 |
21 | extension PartialRangeThrough: CountingRange where Bound == Int {
22 | public var minimum: Int { 0 }
23 | public var maximum: Int? { self.upperBound }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/swift-parsing-benchmark/UUID.swift:
--------------------------------------------------------------------------------
1 | import Benchmark
2 | import Foundation
3 | import Parsing
4 |
5 | /// This benchmark demonstrates how the UUID parser compares to `UUID`'s initializer.
6 | let uuidSuite = BenchmarkSuite(name: "UUID") { suite in
7 | let input = "deadbeef-dead-beef-dead-beefdeadbeef"
8 | let expected = UUID(uuidString: "deadbeef-dead-beef-dead-beefdeadbeef")!
9 | var output: UUID!
10 |
11 | suite.benchmark("UUID.init") {
12 | output = UUID(uuidString: input)
13 | } tearDown: {
14 | precondition(output == expected)
15 | }
16 |
17 | suite.benchmark("UUID.parser") {
18 | var input = input[...].utf8
19 | output = try UUID.parser().parse(&input)
20 | } tearDown: {
21 | precondition(output == expected)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.github/workflows/format.yml:
--------------------------------------------------------------------------------
1 | name: Format
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | workflow_dispatch:
8 |
9 | concurrency:
10 | group: format-${{ github.ref }}
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | swift_format:
15 | name: swift-format
16 | runs-on: macos-14
17 | steps:
18 | - uses: actions/checkout@v4
19 | - name: Xcode Select
20 | run: sudo xcode-select -s /Applications/Xcode_15.4.app
21 | - name: Install
22 | run: brew install swift-format
23 | - name: Format
24 | run: make format
25 | - uses: stefanzweifel/git-auto-commit-action@v4
26 | with:
27 | commit_message: Run swift-format
28 | branch: 'main'
29 | env:
30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/Skip.swift:
--------------------------------------------------------------------------------
1 | /// A parser that discards the output of another parser.
2 | public struct Skip: Parser where Parsers.Input == Input {
3 | /// The parser from which this parser receives output.
4 | public let parsers: Parsers
5 |
6 | @inlinable
7 | public init(@ParserBuilder _ build: () -> Parsers) {
8 | self.parsers = build()
9 | }
10 |
11 | @inlinable
12 | public func parse(_ input: inout Parsers.Input) rethrows {
13 | _ = try self.parsers.parse(&input)
14 | }
15 | }
16 |
17 | extension Skip: ParserPrinter where Parsers: ParserPrinter, Parsers.Output == Void {
18 | @inlinable
19 | public func print(_ output: (), into input: inout Parsers.Input) rethrows {
20 | try self.parsers.print(into: &input)
21 | }
22 | }
23 |
24 | extension Parsers {
25 | public typealias Skip = Parsing.Skip // NB: Convenience type alias for discovery
26 | }
27 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/ParseableFormatTests.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS) || os(tvOS) || os(watchOS)
2 | import Parsing
3 | import XCTest
4 |
5 | @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
6 | final class ParseableFormatStyleTests: XCTestCase {
7 | func testFormatted() {
8 | let p = ParsePrint {
9 | "TOTAL: "
10 | Formatted(.currency(code: "USD"))
11 | }
12 |
13 | XCTAssertEqual(
14 | try p.parse("TOTAL: $42.42"),
15 | 42.42
16 | )
17 | XCTAssertEqual(
18 | try p.print(42.42),
19 | "TOTAL: $42.42"
20 | )
21 | }
22 |
23 | func testFormatted_PartiallyConsumes() throws {
24 | var input = "COORD: 12.34°N"[...]
25 | let p = Parse {
26 | "COORD: "
27 | Formatted(.number)
28 | }
29 | XCTAssertEqual(12.34, try p.parse(&input))
30 | XCTAssertEqual(String(input), "°N")
31 | }
32 | }
33 | #endif
34 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/BacktrackTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class BacktrackTests: XCTestCase {
5 | func testFailure() {
6 | let parser: some Parser = Backtracking {
7 | Prefix(2) { $0 == "A" }
8 | }
9 |
10 | var input = "AB"[...]
11 | XCTAssertThrowsError(try parser.parse(&input))
12 | XCTAssertEqual("AB", Substring(input))
13 | }
14 |
15 | func testSuccess() throws {
16 | let parser: some Parser = Backtracking {
17 | Prefix(1) { $0 == "A" }
18 | }
19 | var input = "AB"[...]
20 | let output = try parser.parse(&input)
21 | XCTAssertEqual("B", Substring(input))
22 | XCTAssertEqual("A", output)
23 | }
24 |
25 | func testPrint() throws {
26 | let parser: some ParserPrinter = Backtracking {
27 | Prefix(2) { $0 == "A" }
28 | }
29 |
30 | let input = try parser.print("AA"[...])
31 | XCTAssertEqual(input, "AA")
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/WhitespaceTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class WhitespaceTests: XCTestCase {
5 | func testTrimsAllWhitespace() {
6 | var input = " \r \t\t \r\n \n\r Hello, world!"[...].utf8
7 | XCTAssertNotNil(try Whitespace().parse(&input))
8 | XCTAssertEqual("Hello, world!", Substring(input))
9 | }
10 |
11 | func testAlwaysSucceeds() {
12 | var input = "Hello, world!"[...].utf8
13 | XCTAssertNotNil(try Whitespace().parse(&input))
14 | XCTAssertEqual("Hello, world!", Substring(input))
15 | }
16 |
17 | func testTrimsHorizontalWhitespace() {
18 | var input = " \r \t\t \r\n \n\r Hello, world!"[...].utf8
19 | XCTAssertNotNil(try Whitespace(.horizontal).parse(&input))
20 | XCTAssertEqual("\r \t\t \r\n \n\r Hello, world!", Substring(input))
21 | }
22 |
23 | func testTrimsVerticalWhitespace() {
24 | var input = "\r\n\r\n \n\r Hello, world!"[...].utf8
25 | XCTAssertNotNil(try Whitespace(.vertical).parse(&input))
26 | XCTAssertEqual(" \n\r Hello, world!", Substring(input))
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/Consumed.swift:
--------------------------------------------------------------------------------
1 | /// A parser that returns the subsequence of input consumed by another parser.
2 | public struct Consumed: Parser
3 | where
4 | Upstream.Input == Input,
5 | Upstream.Input: Collection,
6 | Input.SubSequence == Input
7 | {
8 | public let upstream: Upstream
9 |
10 | @inlinable
11 | public init(@ParserBuilder _ build: () -> Upstream) {
12 | self.upstream = build()
13 | }
14 |
15 | @inlinable
16 | public func parse(_ input: inout Upstream.Input) rethrows -> Upstream.Input {
17 | let original = input
18 | _ = try self.upstream.parse(&input)
19 | return original[.. {
7 | // The type of values this conversion converts from.
8 | associatedtype Input
9 |
10 | // The type of values this conversion converts to.
11 | associatedtype Output
12 |
13 | /// Attempts to transform an input into an output.
14 | ///
15 | /// See ``Conversion/apply(_:)`` for the reverse process.
16 | ///
17 | /// - Parameter input: An input value.
18 | /// - Returns: A transformed output value.
19 | func apply(_ input: Input) throws -> Output
20 |
21 | /// Attempts to transform an output back into an input.
22 | ///
23 | /// The reverse process of ``Conversion/apply(_:)``.
24 | ///
25 | /// - Parameter output: An output value.
26 | /// - Returns: An "un"-transformed input value.
27 | func unapply(_ output: Output) throws -> Input
28 | }
29 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.9
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "swift-parsing",
7 | platforms: [
8 | .iOS(.v13),
9 | .macOS(.v10_15),
10 | .tvOS(.v13),
11 | .watchOS(.v6),
12 | ],
13 | products: [
14 | .library(
15 | name: "Parsing",
16 | targets: ["Parsing"]
17 | )
18 | ],
19 | dependencies: [
20 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
21 | .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.0.0"),
22 | .package(url: "https://github.com/google/swift-benchmark", from: "0.1.1"),
23 | ],
24 | targets: [
25 | .target(
26 | name: "Parsing",
27 | dependencies: [.product(name: "CasePaths", package: "swift-case-paths")]
28 | ),
29 | .testTarget(
30 | name: "ParsingTests",
31 | dependencies: [
32 | "Parsing"
33 | ]
34 | ),
35 | .executableTarget(
36 | name: "swift-parsing-benchmark",
37 | dependencies: [
38 | "Parsing",
39 | .product(name: "Benchmark", package: "swift-benchmark"),
40 | ]
41 | ),
42 | ]
43 | )
44 |
--------------------------------------------------------------------------------
/Sources/swift-parsing-benchmark/Common/Benchmarking.swift:
--------------------------------------------------------------------------------
1 | import Benchmark
2 |
3 | extension BenchmarkSuite {
4 | func benchmark(
5 | _ name: String,
6 | run: @escaping () throws -> Void,
7 | setUp: @escaping () -> Void = {},
8 | tearDown: @escaping () -> Void
9 | ) {
10 | self.register(
11 | benchmark: Benchmarking(name: name, run: run, setUp: setUp, tearDown: tearDown)
12 | )
13 | }
14 | }
15 |
16 | struct Benchmarking: AnyBenchmark {
17 | let name: String
18 | let settings: [BenchmarkSetting] = []
19 | private let _run: () throws -> Void
20 | private let _setUp: () -> Void
21 | private let _tearDown: () -> Void
22 |
23 | init(
24 | name: String,
25 | run: @escaping () throws -> Void,
26 | setUp: @escaping () -> Void = {},
27 | tearDown: @escaping () -> Void = {}
28 | ) {
29 | self.name = name
30 | self._run = run
31 | self._setUp = setUp
32 | self._tearDown = tearDown
33 | }
34 |
35 | func setUp() {
36 | self._setUp()
37 | }
38 |
39 | func run(_ state: inout BenchmarkState) throws {
40 | try self._run()
41 | }
42 |
43 | func tearDown() {
44 | self._tearDown()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/SkipTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class SkipTests: XCTestCase {
5 | func testSkipSuccess() {
6 | var input = "42 Hello, world!"[...].utf8
7 | XCTAssert(try () == XCTUnwrap(Skip { Int.parser(of: Substring.UTF8View.self) }.parse(&input)))
8 | XCTAssertEqual(" Hello, world!", Substring(input))
9 | }
10 |
11 | func testSkipFailure() {
12 | var input = "Hello, world!"[...].utf8
13 | XCTAssertThrowsError(
14 | try Skip { Int.parser(of: Substring.UTF8View.self) }.parse(&input)
15 | ) { error in
16 | XCTAssertEqual(
17 | """
18 | error: unexpected input
19 | --> input:1:1
20 | 1 | Hello, world!
21 | | ^ expected integer
22 | """,
23 | "\(error)"
24 | )
25 | }
26 | XCTAssertEqual("Hello, world!", Substring(input))
27 | }
28 |
29 | func testPrintParserSkipSuccess() {
30 | var input = "!"[...]
31 | let parser = Parse {
32 | Skip { "Hello, " }
33 | Prefix { $0 != "!" }
34 | }
35 |
36 | XCTAssertNoThrow(try parser.print("world"[...], into: &input))
37 | XCTAssertEqual(input, "Hello, world!")
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/ConditionalTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class ConditionalTests: XCTestCase {
5 | struct MultipleOf2: Parser {
6 | var body: some Parser {
7 | Int.parser()
8 | .flatMap { n in
9 | if n.isMultiple(of: 2) {
10 | Always(true)
11 | } else {
12 | Fail(throwing: OddNumberError())
13 | }
14 | }
15 | }
16 | }
17 |
18 | func testFirst() {
19 | var input = "42 Hello, world!"[...]
20 | XCTAssertEqual(true, try MultipleOf2().parse(&input.utf8))
21 | XCTAssertEqual(" Hello, world!", input)
22 | }
23 |
24 | func testSecond() {
25 | var input = "43 Hello, world!"[...]
26 | XCTAssertThrowsError(try MultipleOf2().parse(&input.utf8)) { error in
27 | XCTAssertEqual(
28 | """
29 | error: OddNumberError()
30 | --> input:1:1-2
31 | 1 | 43 Hello, world!
32 | | ^^
33 | """,
34 | "\(error)"
35 | )
36 | }
37 | XCTAssertEqual(" Hello, world!", input)
38 | }
39 | }
40 |
41 | private struct OddNumberError: Error {}
42 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/Backtracking.swift:
--------------------------------------------------------------------------------
1 | /// A parser that attempts to run another parser and if it fails the input will be restored to its
2 | /// original value.
3 | ///
4 | /// Use this parser if you want to manually manage the backtracking behavior of your parsers.
5 | /// Another tool for managing backtracking is the ``OneOf`` parser. Also see the
6 | /// article for more information on backtracking.
7 | public struct Backtracking: Parser where Upstream.Input == Input {
8 | public let upstream: Upstream
9 |
10 | public init(
11 | @ParserBuilder upstream: () -> Upstream
12 | ) {
13 | self.upstream = upstream()
14 | }
15 |
16 | public func parse(_ input: inout Upstream.Input) throws -> Upstream.Output {
17 | let original = input
18 | do {
19 | return try self.upstream.parse(&input)
20 | } catch {
21 | input = original
22 | throw error
23 | }
24 | }
25 | }
26 |
27 | extension Backtracking: ParserPrinter where Upstream: ParserPrinter {
28 | public func print(_ output: Upstream.Output, into input: inout Upstream.Input) throws {
29 | try self.upstream.print(output, into: &input)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/UUIDTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class UUIDTests: XCTestCase {
5 | func testUUID() {
6 | let parser = UUID.parser(of: Substring.UTF8View.self)
7 |
8 | var input = "deadbeef-dead-beef-dead-beefdeadbeef Hello"[...].utf8
9 | XCTAssertEqual(
10 | UUID(uuidString: "deadbeef-dead-beef-dead-beefdeadbeef"),
11 | try parser.parse(&input)
12 | )
13 | XCTAssertEqual(" Hello", String(input))
14 |
15 | input = "DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF Hello"[...].utf8
16 | XCTAssertEqual(
17 | UUID(uuidString: "deadbeef-dead-beef-dead-beefdeadbeef"), try parser.parse(&input)
18 | )
19 | XCTAssertEqual(" Hello", String(input))
20 |
21 | input = "DADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF Hello"[...].utf8
22 | XCTAssertThrowsError(try parser.parse(&input)) { error in
23 | XCTAssertEqual(
24 | """
25 | error: unexpected input
26 | --> input:1:1
27 | 1 | DADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF Hello
28 | | ^ expected UUID
29 | """,
30 | "\(error)"
31 | )
32 | }
33 | XCTAssertEqual("DADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF Hello", String(input))
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/ParseableFormatStyle.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
2 | import Foundation
3 |
4 | @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
5 | public struct Formatted: ParserPrinter
6 | where
7 | Style.Strategy.ParseInput == String,
8 | Style.Strategy.ParseOutput == Style.RegexOutput
9 | {
10 | @usableFromInline
11 | let style: Style
12 |
13 | @inlinable
14 | public init(_ style: Style) {
15 | self.style = style
16 | }
17 |
18 | @inlinable
19 | public func parse(_ input: inout Substring) throws -> Style.Strategy.ParseOutput {
20 | guard let match = input.prefixMatch(of: self.style.regex)
21 | else {
22 | throw ParsingError.failed(
23 | summary: "failed to process \"\(Output.self)\"",
24 | at: input
25 | )
26 | }
27 | input.removeFirst(input.distance(from: match.range.lowerBound, to: match.range.upperBound))
28 | return match.output
29 | }
30 |
31 | @inlinable
32 | public func print(_ output: Style.FormatInput, into input: inout Substring) {
33 | input.prepend(contentsOf: self.style.format(output))
34 | }
35 | }
36 | #endif
37 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PLATFORM_IOS = iOS Simulator,name=iPhone 11 Pro
2 | PLATFORM_MACOS = macOS
3 | PLATFORM_TVOS = tvOS Simulator,name=Apple TV
4 |
5 | default: test
6 |
7 | benchmarks:
8 | swift run -c release swift-parsing-benchmark
9 |
10 | test:
11 | xcodebuild test \
12 | -workspace Parsing.xcworkspace \
13 | -scheme Parsing \
14 | -destination platform="$(PLATFORM_IOS)"
15 | xcodebuild test \
16 | -workspace Parsing.xcworkspace \
17 | -scheme Parsing \
18 | -destination platform="$(PLATFORM_MACOS)"
19 | xcodebuild test \
20 | -workspace Parsing.xcworkspace \
21 | -scheme Parsing \
22 | -destination platform="$(PLATFORM_TVOS)"
23 |
24 | test-linux:
25 | docker run \
26 | --rm \
27 | -v "$(PWD):$(PWD)" \
28 | -w "$(PWD)" \
29 | swift:5.3 \
30 | bash -c 'make test-swift'
31 |
32 | test-swift:
33 | swift test \
34 | --enable-test-discovery \
35 | --parallel
36 |
37 | format:
38 | swift format --in-place --recursive \
39 | ./Package.swift ./Sources ./Tests
40 | find . -type f -name '*.md' -print0 | xargs -0 perl -pi -e 's/ +$$//'
41 |
42 | generate-variadics:
43 | swift run variadics-generator \
44 | --generate-zips \
45 | --generate-one-ofs \
46 | > Sources/Parsing/Builders/Variadics.swift
47 |
48 | .PHONY: benchmarks format generate-variadics test
49 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/BoolTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class BoolTests: XCTestCase {
5 | func testParsesTrue() {
6 | var input = "true Hello, world!"[...].utf8
7 | XCTAssertEqual(true, try Bool.parser().parse(&input))
8 | XCTAssertEqual(" Hello, world!", Substring(input))
9 | }
10 |
11 | func testParsesFalse() {
12 | var input = "false Hello, world!"[...].utf8
13 | XCTAssertEqual(false, try Bool.parser().parse(&input))
14 | XCTAssertEqual(" Hello, world!", Substring(input))
15 | }
16 |
17 | func testParseFailure() {
18 | var input = "Hello, world!"[...].utf8
19 | XCTAssertThrowsError(try Bool.parser().parse(&input)) { error in
20 | XCTAssertEqual(
21 | """
22 | error: unexpected input
23 | --> input:1:1
24 | 1 | Hello, world!
25 | | ^ expected "true" or "false"
26 | """,
27 | "\(error)"
28 | )
29 | }
30 | XCTAssertEqual("Hello, world!", Substring(input))
31 | }
32 |
33 | func testPrintsTrue() {
34 | var input = "!"[...]
35 | XCTAssertNoThrow(Bool.parser().print(true, into: &input.utf8))
36 | XCTAssertEqual(input, "true!"[...])
37 | }
38 |
39 | func testPrintsFalse() {
40 | var input = "!"[...]
41 | XCTAssertNoThrow(Bool.parser().print(false, into: &input.utf8))
42 | XCTAssertEqual(input, "false!"[...])
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/Parsing/Conversions/UTF8View.swift:
--------------------------------------------------------------------------------
1 | extension Conversion where Self == Conversions.SubstringToUTF8View {
2 | /// A conversion from `Substring` to `Substring.UTF8View`.
3 | ///
4 | /// Useful when used with the ``From`` parser-printer to integrate a UTF-8 parser into a substring
5 | /// parser.
6 | @inlinable
7 | public static var utf8: Self { .init() }
8 | }
9 |
10 | extension Conversion where Output == Substring {
11 | /// Transforms this conversion to `Substring` into a conversion to `Substring.UTF8View`.
12 | ///
13 | /// A fluent version of ``Conversion/utf8-swift.type.property``.
14 | @inlinable
15 | public var utf8: Conversions.Map { self.map(.utf8) }
16 | }
17 |
18 | extension Conversions {
19 | /// A conversion from a substring to its UTF-8 view.
20 | ///
21 | /// You will not typically need to interact with this type directly. Instead you will usually use
22 | /// the ``Conversion/utf8-swift.type.property`` operation, which constructs this type.
23 | public struct SubstringToUTF8View: Conversion {
24 | @inlinable
25 | public init() {}
26 |
27 | @inlinable
28 | public func apply(_ input: Substring) -> Substring.UTF8View {
29 | input.utf8
30 | }
31 |
32 | @inlinable
33 | public func unapply(_ output: Substring.UTF8View) -> Substring {
34 | Substring(output)
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/VariadicTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class Over11Tests: XCTestCase {
5 | func testOver11() throws {
6 | struct ParserTest: Parser {
7 | var body: some Parser
8 | {
9 | Digits()
10 | "."
11 | Digits()
12 | "."
13 | Digits()
14 | "."
15 | Digits()
16 | "."
17 | Digits()
18 | "."
19 | Digits()
20 | "."
21 | Digits()
22 | "."
23 | Digits()
24 | "."
25 | Digits()
26 | "."
27 | Digits()
28 | "."
29 | Digits()
30 | "."
31 | Digits()
32 | }
33 | }
34 |
35 | var input = "1.2.3.4.5.6.7.8.9.10.11.12"[...]
36 | let output: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) = try ParserTest()
37 | .parse(&input)
38 | XCTAssertEqual(output.0, 1)
39 | XCTAssertEqual(output.1, 2)
40 | XCTAssertEqual(output.2, 3)
41 | XCTAssertEqual(output.3, 4)
42 | XCTAssertEqual(output.4, 5)
43 | XCTAssertEqual(output.5, 6)
44 | XCTAssertEqual(output.6, 7)
45 | XCTAssertEqual(output.7, 8)
46 | XCTAssertEqual(output.8, 9)
47 | XCTAssertEqual(output.9, 10)
48 | XCTAssertEqual(output.10, 11)
49 | XCTAssertEqual(output.11, 12)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/CaseIterableTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class CaseIterableTests: XCTestCase {
5 | func testCaseIterableParser() throws {
6 | enum Person: String, CaseIterable {
7 | case blob = "Blob"
8 | case blobJr = "Blob Jr"
9 | }
10 |
11 | struct People: Parser {
12 | var body: some Parser {
13 | Many {
14 | Person.parser()
15 | } separator: {
16 | ",".utf8
17 | } terminator: {
18 | End()
19 | }
20 | }
21 | }
22 |
23 | var input = "Blob,Blob Jr"[...].utf8
24 | XCTAssertEqual(try People().parse(&input), [.blob, .blobJr])
25 |
26 | input = "Blob Jr,Blob"[...].utf8
27 | XCTAssertEqual(try People().parse(&input), [.blobJr, .blob])
28 |
29 | input = "Blob,Mr Blob"[...].utf8
30 | XCTAssertThrowsError(try People().parse(&input)) { error in
31 | XCTAssertEqual(
32 | """
33 | error: multiple failures occurred
34 |
35 | error: unexpected input
36 | --> input:1:6
37 | 1 | Blob,Mr Blob
38 | | ^ expected "Blob Jr"
39 | | ^ expected "Blob"
40 |
41 | error: unexpected input
42 | --> input:1:5
43 | 1 | Blob,Mr Blob
44 | | ^ expected end of input
45 | """,
46 | "\(error)"
47 | )
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/Parsing/Documentation.docc/Articles/Parsers/String.md:
--------------------------------------------------------------------------------
1 | # String
2 |
3 | A parser that consumes a string literal from the beginning of a string.
4 |
5 | Many of Swift's string types conform to the ``Parser`` protocol, which allows you to use string types
6 | directly in a parser. For example, to parse two integers separated by a comma we can do:
7 |
8 | ```swift
9 | try Parse {
10 | Int.parser()
11 | ","
12 | Int.parser()
13 | }
14 | .parse("123,456") // (123, 456)
15 | ```
16 |
17 | The string `","` acts as a parser that consumes a comma from the beginning of an input and fails
18 | if the input does not start with a comma.
19 |
20 | Swift's other string representations also conform to ``Parser``, such as `UnicodeScalarView`
21 | and `UTF8View`. This allows you to consume strings from the beginning of an input in a more
22 | efficient manner than is possible with `Substring` (see for more info).
23 |
24 | For example, we can conver the above parser to work on the level of `UTF8View`s, which is a
25 | collection of UTF-8 code units:
26 |
27 | ```swift
28 | try Parse {
29 | Int.parser()
30 | ",".utf8
31 | Int.parser()
32 | }
33 | .parse("123,456") // (123, 456)
34 | ```
35 |
36 | Here `",".utf8` is a `String.UTF8View`, which conforms to the ``Parser`` protocol. Also, by type
37 | inference, Swift is choosing the overload of `Int.parser()` that now works on `UTF8View`s rather
38 | than `Substring`s. See for more info.
39 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/MapTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class MapTests: XCTestCase {
5 | func testSuccess() {
6 | var input = "42 Hello, world!"[...].utf8
7 | XCTAssertEqual("42", try Int.parser().map(String.init).parse(&input))
8 | XCTAssertEqual(" Hello, world!", Substring(input))
9 | }
10 |
11 | func testOverloadArray() {
12 | let array = [1].map { "\($0)" }
13 | XCTAssert(type(of: array) == Array.self)
14 | }
15 |
16 | func testOverloadString() {
17 | let array = "abc".map { "\($0)" }
18 | XCTAssert(type(of: array) == Array.self)
19 | }
20 |
21 | func testOverloadUnicodeScalars() {
22 | let array = "abc".unicodeScalars.map { "\($0)" }
23 | XCTAssert(type(of: array) == Array.self)
24 | }
25 |
26 | func testOverloadUTF8View() {
27 | let array = "abc".utf8.map { "\($0)" }
28 | XCTAssert(type(of: array) == Array.self)
29 | }
30 |
31 | func testConstantPrinting() {
32 | enum Person { case blob, blobJr, blobSr }
33 | XCTAssertThrowsError(
34 | try OneOf {
35 | "Blob Sr".map { Person.blobSr }
36 | "Blob".map { Person.blob }
37 | }
38 | .print(.blobJr)
39 | ) { error in
40 | XCTAssertEqual(
41 | """
42 | error: multiple failures occurred
43 |
44 | error: expected blob
45 |
46 | error: expected blobSr
47 | """,
48 | "\(error)"
49 | )
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/CompactMapTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class CompactMapTests: XCTestCase {
5 | func testSuccess() {
6 | var input = "FF0000"[...]
7 | XCTAssertEqual(
8 | 0xFF,
9 | try Prefix(2).compactMap { Int(String($0), radix: 16) }.parse(&input)
10 | )
11 | XCTAssertEqual("0000", Substring(input))
12 | }
13 |
14 | func testFailure() {
15 | var input = "GG0000"[...]
16 | XCTAssertThrowsError(
17 | try Prefix(2).compactMap { Int(String($0), radix: 16) }.parse(&input)
18 | ) { error in
19 | XCTAssertEqual(
20 | """
21 | error: failed to process "Int" from "GG"
22 | --> input:1:1-2
23 | 1 | GG0000
24 | | ^^
25 | """,
26 | "\(error)"
27 | )
28 | }
29 | XCTAssertEqual("0000", Substring(input))
30 | }
31 |
32 | func testOverloadArray() {
33 | let array = [1].compactMap { "\($0)" }
34 | XCTAssert(type(of: array) == Array.self)
35 | }
36 |
37 | func testOverloadString() {
38 | let array = "abc".compactMap { "\($0)" }
39 | XCTAssert(type(of: array) == Array.self)
40 | }
41 |
42 | func testOverloadUnicodeScalars() {
43 | let array = "abc".unicodeScalars.compactMap { "\($0)" }
44 | XCTAssert(type(of: array) == Array.self)
45 | }
46 |
47 | func testOverloadUTF8View() {
48 | let array = "abc".utf8.compactMap { "\($0)" }
49 | XCTAssert(type(of: array) == Array.self)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/Parsing/Documentation.docc/Extensions/Parser.md:
--------------------------------------------------------------------------------
1 | # ``Parsing/Parser``
2 |
3 | ## Topics
4 |
5 | ### Diving deeper
6 |
7 | *
8 | *
9 | *
10 | *
11 | *
12 |
13 | ### Running a parser
14 |
15 | All of the ways to run a parser on an input.
16 |
17 | - ``parse(_:)-717qw``
18 | - ``parse(_:)-6h1d0``
19 | - ``parse(_:)-2wzcq``
20 |
21 | ### Common parsers
22 |
23 | Some of the most commonly used parsers in the library. Use these parsers with operators in order
24 | to build complex parsers from simpler pieces.
25 |
26 | -
27 | -
28 | -
29 | -
30 | -
31 | -
32 | -
33 | -
34 | - ``Parse``
35 | - ``OneOf``
36 | - ``Many``
37 | - ``Prefix``
38 | - ``PrefixThrough``
39 | - ``PrefixUpTo``
40 | - ``Optionally``
41 | - ``From``
42 | - ``Consumed``
43 | - ``Always``
44 | - ``End``
45 | - ``Rest``
46 | - ``Fail``
47 | - ``FromSubstring``
48 | - ``FromUTF8View``
49 | - ``FromUnicodeScalarView``
50 | - ``First``
51 | - ``Skip``
52 | - ``Lazy``
53 | - ``Newline``
54 | - ``Whitespace``
55 | - ``AnyParser``
56 | - ``Peek``
57 | - ``Not``
58 | - ``StartsWith``
59 | - ``Stream``
60 |
61 | ### Parser operators
62 |
63 | - ``Parser/map(_:)-4hsj5``
64 | - ``flatMap(_:)``
65 | - ``compactMap(_:)``
66 | - ``filter(_:)``
67 | - ``pullback(_:)``
68 | - ``replaceError(with:)``
69 | - ``pipe(_:)-2zck4``
70 | - ``eraseToAnyParser()``
71 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/FilterTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class FilterTests: XCTestCase {
5 | func testSuccess() {
6 | var input = "42 Hello, world!"[...].utf8
7 | XCTAssertEqual(42, try Int.parser().filter { $0.isMultiple(of: 2) }.parse(&input))
8 | XCTAssertEqual(" Hello, world!", Substring(input))
9 | }
10 |
11 | func testFailure() {
12 | var input = "43 Hello, world!"[...].utf8
13 | XCTAssertThrowsError(try Int.parser().filter { $0.isMultiple(of: 2) }.parse(&input)) { error in
14 | XCTAssertEqual(
15 | """
16 | error: processed value 43 failed to satisfy predicate
17 | --> input:1:1-2
18 | 1 | 43 Hello, world!
19 | | ^^ processed input
20 | """,
21 | "\(error)"
22 | )
23 | }
24 | XCTAssertEqual(" Hello, world!", Substring(input))
25 | }
26 |
27 | func testOverloadArray() {
28 | let array = [1].filter { _ in true }
29 | XCTAssert(type(of: array) == Array.self)
30 | }
31 |
32 | func testOverloadString() {
33 | let array = "abc".filter { _ in true }
34 | XCTAssert(type(of: array) == String.self)
35 | }
36 |
37 | func testOverloadUnicodeScalars() {
38 | let array = "abc".unicodeScalars.filter { _ in true }
39 | XCTAssert(type(of: array) == String.UnicodeScalarView.self)
40 | }
41 |
42 | func testOverloadUTF8View() {
43 | let array = "abc".utf8.filter { _ in true }
44 | XCTAssert(type(of: array) == [String.UTF8View.Element].self)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/FailTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class FailTests: XCTestCase {
5 | func testFailure() {
6 | var input = "Hello, world!"[...]
7 | XCTAssertThrowsError(try Fail().parse(&input)) { error in
8 | XCTAssertEqual(
9 | """
10 | error: failed
11 | --> input:1:1
12 | 1 | Hello, world!
13 | | ^
14 | """,
15 | "\(error)"
16 | )
17 | }
18 | XCTAssertEqual("Hello, world!", input)
19 |
20 | XCTAssertThrowsError(try Fail().print(42)) { error in
21 | XCTAssertEqual(
22 | """
23 | error: failed
24 |
25 | A failing parser-printer attempted to print:
26 |
27 | 42
28 | """,
29 | "\(error)"
30 | )
31 | }
32 | }
33 |
34 | func testCustomError() {
35 | struct MyError: Error {}
36 |
37 | XCTAssertThrowsError(try Fail(throwing: MyError()).parse("Hello")) { error in
38 | XCTAssertEqual(
39 | """
40 | error: MyError()
41 | --> input:1:1
42 | 1 | Hello
43 | | ^
44 | """,
45 | "\(error)"
46 | )
47 | }
48 |
49 | XCTAssertThrowsError(try Fail(throwing: MyError()).print(42)) { error in
50 | XCTAssertEqual(
51 | """
52 | error: MyError()
53 |
54 | A failing parser-printer attempted to print:
55 |
56 | 42
57 | """,
58 | "\(error)"
59 | )
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/swift-parsing-benchmark/PrefixUpTo.swift:
--------------------------------------------------------------------------------
1 | import Benchmark
2 | import Foundation
3 | import Parsing
4 |
5 | /// This benchmarks the performance of `PrefixUpTo` against Apple's tools.
6 | let prefixUpToSuite = BenchmarkSuite(name: "PrefixUpTo") { suite in
7 | let input = String(repeating: ".", count: 10_000) + "Hello, world!"
8 |
9 | do {
10 | var output: Substring!
11 | suite.benchmark("Parser: Substring") {
12 | var input = input[...]
13 | output = try PrefixUpTo("Hello").parse(&input)
14 | } tearDown: {
15 | precondition(output.count == 10_000)
16 | }
17 | }
18 |
19 | do {
20 | var output: Substring.UTF8View!
21 | suite.benchmark("Parser: UTF8") {
22 | var input = input[...].utf8
23 | output = try PrefixUpTo("Hello".utf8).parse(&input)
24 | } tearDown: {
25 | precondition(output.count == 10_000)
26 | }
27 | }
28 |
29 | do {
30 | var output: Substring!
31 | suite.benchmark("String.range(of:)") {
32 | output = input.range(of: "Hello").map { input.prefix(upTo: $0.lowerBound) }
33 | } tearDown: {
34 | precondition(output.count == 10_000)
35 | }
36 | }
37 |
38 | if #available(macOS 10.15, *) {
39 | var output: String!
40 | let scanner = Scanner(string: input)
41 | suite.benchmark("Scanner.scanUpToString") {
42 | output = scanner.scanUpToString("Hello")
43 | } setUp: {
44 | scanner.currentIndex = input.startIndex
45 | } tearDown: {
46 | precondition(output.count == 10_000)
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/FlatMapTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class FlatMapTests: XCTestCase {
5 | func testSuccess() {
6 | var input = "42 Hello, world!"[...].utf8
7 | XCTAssertEqual(43, try Int.parser().flatMap { Always($0 + 1) }.parse(&input))
8 | XCTAssertEqual(" Hello, world!", Substring(input))
9 |
10 | input = "42 Hello, world!"[...].utf8
11 | XCTAssertEqual(43, try Int.parser().flatMap { return Always($0 + 1) }.parse(&input))
12 | XCTAssertEqual(" Hello, world!", Substring(input))
13 | }
14 |
15 | func testUpstreamFailure() {
16 | var input = "Hello, world!"[...].utf8
17 | XCTAssertThrowsError(try Int.parser().flatMap { Always($0 + 1) }.parse(&input)) { error in
18 | XCTAssertEqual(
19 | """
20 | error: unexpected input
21 | --> input:1:1
22 | 1 | Hello, world!
23 | | ^ expected integer
24 | """,
25 | "\(error)"
26 | )
27 | }
28 | XCTAssertEqual("Hello, world!", Substring(input))
29 | }
30 |
31 | func testDownstreamFailure() {
32 | var input = "Hello, world!"[...].utf8
33 | XCTAssertThrowsError(
34 | try Prefix(2).flatMap { _ in Int.parser() }.parse(&input)
35 | ) { error in
36 | XCTAssertEqual(
37 | """
38 | error: unexpected input
39 | --> input:1:1-2
40 | 1 | Hello, world!
41 | | ^^ expected integer
42 | """,
43 | "\(error)"
44 | )
45 | }
46 | XCTAssertEqual("llo, world!", Substring(input))
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/First.swift:
--------------------------------------------------------------------------------
1 | /// A parser that consumes the first element from a collection.
2 | ///
3 | /// This parser is named after `Sequence.first`, and attempts to parse the first element from a
4 | /// collection of input by calling this property under the hood.
5 | ///
6 | /// For example, it can parse the leading character off a substring:
7 | ///
8 | /// ```swift
9 | /// var input = "Hello"[...]
10 | /// try First().parse(&input) // "H"
11 | /// input // "ello"
12 | /// ```
13 | ///
14 | /// This parser fails if the input collection is empty:
15 | ///
16 | /// ```swift
17 | /// input = ""
18 | /// try First().parse(&input)
19 | /// // error: unexpected input
20 | /// // --> input:1:1
21 | /// // 1 |
22 | /// // | ^ expected element
23 | /// ```
24 | public struct First: Parser where Input.SubSequence == Input {
25 | @inlinable
26 | public init() {}
27 |
28 | @inlinable
29 | public func parse(_ input: inout Input) throws -> Input.Element {
30 | guard let first = input.first else {
31 | throw ParsingError.expectedInput("element", at: input)
32 | }
33 | input.removeFirst()
34 | return first
35 | }
36 | }
37 |
38 | extension First: ParserPrinter where Input: PrependableCollection {
39 | @inlinable
40 | public func print(_ output: Input.Element, into input: inout Input) {
41 | input.prepend(contentsOf: CollectionOfOne(output))
42 | }
43 | }
44 |
45 | extension Parsers {
46 | public typealias First = Parsing.First // NB: Convenience type alias for discovery
47 | }
48 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/PipeEndTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import Parsing
4 |
5 | final class PipeEndTests: XCTestCase {
6 | func testSuccess() {
7 | var input = ""[...]
8 | XCTAssertNoThrow(try Parsers.PipeEnd().parse(&input))
9 | XCTAssertEqual("", input)
10 | }
11 |
12 | func testFailure() {
13 | var input = "Hello, world!"[...]
14 | XCTAssertThrowsError(try Parsers.PipeEnd().parse(&input)) { error in
15 | XCTAssertEqual(
16 | """
17 | error: unexpected input
18 | --> input:1:1-13
19 | 1 | Hello, world!
20 | | ^^^^^^^^^^^^^ expected end of pipe
21 | """,
22 | "\(error)"
23 | )
24 | }
25 | XCTAssertEqual("Hello, world!", input)
26 | }
27 |
28 | func testPrintSuccess() {
29 | var input = ""[...]
30 | XCTAssertNoThrow(try Parsers.PipeEnd().print(into: &input))
31 | XCTAssertEqual(input, ""[...])
32 | }
33 |
34 | func testPrintFailure() {
35 | var input = "Hello, world!"[...]
36 | XCTAssertThrowsError(try Parsers.PipeEnd().print(into: &input)) { error in
37 | XCTAssertEqual(
38 | """
39 | error: round-trip expectation failed
40 |
41 | A piped parser-printer expected no more input, but more was printed.
42 |
43 | "Hello, world!"
44 |
45 | During a round-trip, the piped parser-printer would have failed to parse at this \
46 | remaining input.
47 | """,
48 | "\(error)"
49 | )
50 | }
51 | XCTAssertEqual(input, "Hello, world!"[...])
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/swift-parsing-benchmark/Bool.swift:
--------------------------------------------------------------------------------
1 | import Benchmark
2 | import Foundation
3 | import Parsing
4 |
5 | /// This benchmark demonstrates how to parse a boolean from the front of an input and compares its
6 | /// performance against `Bool.init`, which does not incrementally parse, and Foundation's `Scanner`
7 | /// type. `Scanner` does not have a `scanBool` method, but we can emulate this functionality by calling
8 | /// `scanString` twice and mapping each result to a boolean.
9 | let boolSuite = BenchmarkSuite(name: "Bool") { suite in
10 | var input = "true"
11 | var expected = true
12 | var output: Bool!
13 |
14 | suite.benchmark("Bool.init") {
15 | output = Bool(input)
16 | } tearDown: {
17 | tearDown()
18 | }
19 |
20 | suite.benchmark("Bool.parser") {
21 | var input = input[...].utf8
22 | output = try Bool.parser().parse(&input)
23 | } tearDown: {
24 | tearDown()
25 | }
26 |
27 | if #available(macOS 10.15, *) {
28 | var scanner: Scanner!
29 | suite.benchmark("Scanner.scanBool") {
30 | output = scanner.scanBool()
31 | } setUp: {
32 | scanner = Scanner(string: input)
33 | } tearDown: {
34 | tearDown()
35 | }
36 | }
37 |
38 | func tearDown() {
39 | precondition(output == expected)
40 | (input, expected) = expected ? ("false", false) : ("true", true)
41 | }
42 | }
43 |
44 | extension Scanner {
45 | @available(macOS 10.15, *)
46 | func scanBool() -> Bool? {
47 | self.scanString("true").map { _ in true }
48 | ?? self.scanString("false").map { _ in false }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Parsing.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 |
3 | /*
4 | This playground builds a parser for [day 13][AoC13] of the Advent of Code 2021 challenge.
5 |
6 | [AoC13]: https://adventofcode.com/2021/day/13
7 | */
8 |
9 | /// Sample input to be parsed.
10 | let input = """
11 | 6,10
12 | 0,14
13 | 9,10
14 | 0,3
15 | 10,4
16 | 4,11
17 | 6,0
18 | 6,12
19 | 4,1
20 | 0,13
21 | 10,12
22 | 3,4
23 | 3,0
24 | 8,4
25 | 1,10
26 | 2,14
27 | 8,10
28 | 9,0
29 |
30 | fold along y=7
31 | fold along x=5
32 | """
33 |
34 | // MARK: - Models
35 |
36 | struct Dot {
37 | let x, y: Int
38 | }
39 |
40 | enum Direction: String, CaseIterable {
41 | case x, y
42 | }
43 |
44 | struct Fold {
45 | let direction: Direction
46 | let position: Int
47 | }
48 |
49 | struct Instructions {
50 | let dots: [Dot]
51 | let folds: [Fold]
52 | }
53 |
54 | // MARK: - Parsers
55 |
56 | let dot = ParsePrint(input: Substring.self, .memberwise(Dot.init)) {
57 | Digits()
58 | ","
59 | Digits()
60 | }
61 |
62 | let fold = ParsePrint(input: Substring.self, .memberwise(Fold.init)) {
63 | "fold along "
64 | Direction.parser()
65 | "="
66 | Digits()
67 | }
68 |
69 | let instructions = ParsePrint(input: Substring.self, .memberwise(Instructions.init)) {
70 | Many {
71 | dot
72 | } separator: {
73 | "\n"
74 | } terminator: {
75 | "\n\n"
76 | }
77 | Many {
78 | fold
79 | } separator: {
80 | "\n"
81 | }
82 | }
83 |
84 | // MARK: Round-trip
85 |
86 | let parsed = try instructions.parse(input)
87 | let printed = try instructions.print(parsed)
88 |
89 | input == printed
90 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - '*'
10 | workflow_dispatch:
11 |
12 | jobs:
13 | macos15:
14 | name: macOS (Swift 6)
15 | runs-on: macos-15
16 | strategy:
17 | matrix:
18 | xcode:
19 | - '16.2'
20 | steps:
21 | - uses: actions/checkout@v4
22 | - name: Select Xcode ${{ matrix.xcode }}
23 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
24 | - name: System
25 | run: system_profiler SPHardwareDataType
26 | - name: Build
27 | run: swift build
28 | - name: Run tests
29 | run: swift test
30 |
31 | macos14:
32 | name: macOS (Swift 5.9)
33 | runs-on: macos-14
34 | strategy:
35 | matrix:
36 | xcode:
37 | - '15.2'
38 | steps:
39 | - uses: actions/checkout@v4
40 | - name: Select Xcode ${{ matrix.xcode }}
41 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
42 | - name: System
43 | run: system_profiler SPHardwareDataType
44 | - name: Build
45 | run: swift build
46 | - name: Run tests
47 | run: swift test
48 |
49 | linux:
50 | name: Linux
51 | strategy:
52 | matrix:
53 | os: [ubuntu-latest]
54 | swift:
55 | - '6.0'
56 | runs-on: ${{ matrix.os }}
57 | container: swift:${{ matrix.swift }}
58 | steps:
59 | - uses: actions/checkout@v4
60 | - name: Build
61 | run: swift build
62 | - name: Run tests
63 | run: swift test
64 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-argument-parser",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/apple/swift-argument-parser",
7 | "state" : {
8 | "revision" : "6b2aa2748a7881eebb9f84fb10c01293e15b52ca",
9 | "version" : "0.5.0"
10 | }
11 | },
12 | {
13 | "identity" : "swift-benchmark",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/google/swift-benchmark",
16 | "state" : {
17 | "revision" : "a0564bf88df5f94eec81348a2f089494c6b28d80",
18 | "version" : "0.1.1"
19 | }
20 | },
21 | {
22 | "identity" : "swift-case-paths",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/pointfreeco/swift-case-paths",
25 | "state" : {
26 | "revision" : "5da6989aae464f324eef5c5b52bdb7974725ab81",
27 | "version" : "1.0.0"
28 | }
29 | },
30 | {
31 | "identity" : "swift-docc-plugin",
32 | "kind" : "remoteSourceControl",
33 | "location" : "https://github.com/apple/swift-docc-plugin",
34 | "state" : {
35 | "revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6",
36 | "version" : "1.0.0"
37 | }
38 | },
39 | {
40 | "identity" : "xctest-dynamic-overlay",
41 | "kind" : "remoteSourceControl",
42 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
43 | "state" : {
44 | "revision" : "302891700c7fa3b92ebde9fe7b42933f8349f3c7",
45 | "version" : "1.0.0"
46 | }
47 | }
48 | ],
49 | "version" : 2
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/Optional.swift:
--------------------------------------------------------------------------------
1 | extension Optional: Parser where Wrapped: Parser {
2 | public func parse(_ input: inout Wrapped.Input) rethrows -> Wrapped.Output? {
3 | guard let self = self
4 | else { return nil }
5 | return try self.parse(&input)
6 | }
7 | }
8 |
9 | extension Optional: ParserPrinter where Wrapped: ParserPrinter {
10 | public func print(_ output: Wrapped.Output?, into input: inout Wrapped.Input) rethrows {
11 | guard let output = output else { return }
12 | try self?.print(output, into: &input)
13 | }
14 | }
15 |
16 | extension Parsers {
17 | /// A parser that attempts to run a given void parser, succeeding with void.
18 | ///
19 | /// You will not typically need to interact with this type directly. Instead you will usually use
20 | /// `if` statements in parser builder blocks:
21 | ///
22 | /// ```swift
23 | /// Parse {
24 | /// "Hello"
25 | /// if useComma {
26 | /// ","
27 | /// }
28 | /// " "
29 | /// Rest()
30 | /// }
31 | /// ```
32 | public struct OptionalVoid: Parser where Wrapped.Output == Void {
33 | let wrapped: Wrapped?
34 |
35 | public init(wrapped: Wrapped?) {
36 | self.wrapped = wrapped
37 | }
38 |
39 | public func parse(_ input: inout Wrapped.Input) rethrows {
40 | guard let wrapped = self.wrapped
41 | else { return }
42 |
43 | try wrapped.parse(&input)
44 | }
45 | }
46 | }
47 |
48 | extension Parsers.OptionalVoid: ParserPrinter where Wrapped: ParserPrinter {
49 | public func print(_ output: (), into input: inout Wrapped.Input) rethrows {
50 | try self.wrapped?.print(into: &input)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/swift-parsing-benchmark/Color.swift:
--------------------------------------------------------------------------------
1 | import Benchmark
2 | import Parsing
3 |
4 | /// This benchmark demonstrates how to parse a hexadecimal color.
5 | ///
6 | /// Compare to the Rust [example using nom](https://github.com/Geal/nom#example).
7 | let colorSuite = BenchmarkSuite(name: "Color") { suite in
8 | struct Color: Equatable {
9 | let red, green, blue: UInt8
10 | }
11 |
12 | struct HexByte: ParserPrinter {
13 | func parse(_ input: inout Substring.UTF8View) throws -> UInt8 {
14 | let prefix = input.prefix(2)
15 | guard
16 | prefix.count == 2,
17 | let byte = UInt8(String(decoding: prefix, as: UTF8.self), radix: 16)
18 | else { throw ParsingError() }
19 | input.removeFirst(2)
20 | return byte
21 | }
22 |
23 | func print(_ output: UInt8, into input: inout Substring.UTF8View) {
24 | let byte = String(output, radix: 16)
25 | input.prepend(contentsOf: byte.count == 1 ? "0\(byte)".utf8 : "\(byte)".utf8)
26 | }
27 | }
28 |
29 | struct HexColor: ParserPrinter {
30 | var body: some ParserPrinter {
31 | ParsePrint(.memberwise(Color.init(red:green:blue:))) {
32 | "#".utf8
33 | HexByte()
34 | HexByte()
35 | HexByte()
36 | }
37 | }
38 | }
39 |
40 | let input = "#FF0000"
41 | let expected = Color(red: 0xFF, green: 0x00, blue: 0x00)
42 | var output: Color!
43 |
44 | suite.benchmark("Parser") {
45 | var input = input[...].utf8
46 | output = try HexColor().parse(&input)
47 | } tearDown: {
48 | precondition(output == expected)
49 | precondition(try! HexColor().print(output).elementsEqual("#ff0000".utf8) == true)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/Parsing/Parsers/Stream.swift:
--------------------------------------------------------------------------------
1 | /// A parser that can parse streams of input.
2 | ///
3 | /// For example, the following parser can parse an integer followed by a newline from a collection
4 | /// of UTF8 bytes:
5 | ///
6 | /// ```swift
7 | /// Parse {
8 | /// Int.parser(of: ArraySlice.self)
9 | /// StartsWith("\n".utf8)
10 | /// }
11 | /// ```
12 | ///
13 | /// This parser can be transformed into one that processes an incoming stream of UTF8 bytes:
14 | ///
15 | /// ```swift
16 | /// Stream {
17 | /// Parse {
18 | /// Int.parser(of: ArraySlice.self)
19 | /// StartsWith("\n".utf8)
20 | /// }
21 | /// }
22 | /// ```
23 | ///
24 | /// And then it can be used on a stream, such as values coming from standard in:
25 | ///
26 | /// ```swift
27 | /// var stdin = AnyIterator {
28 | /// readLine().map { ArraySlice($0.utf8) }
29 | /// }
30 | ///
31 | /// try newlineSeparatedIntegers.parse(&stdin)
32 | /// ```
33 | public struct Stream: Parser
34 | where Parsers.Input == Input {
35 | public let parsers: Parsers
36 |
37 | @inlinable
38 | public init(@ParserBuilder build: () -> Parsers) {
39 | self.parsers = build()
40 | }
41 |
42 | @inlinable
43 | public func parse(_ input: inout AnyIterator) rethrows -> [Parsers.Output] {
44 | var buffer = Parsers.Input()
45 | var outputs: Output = []
46 | while let chunk = input.next() {
47 | buffer.append(contentsOf: chunk)
48 | outputs.append(try self.parsers.parse(&buffer))
49 | }
50 | return outputs
51 | }
52 | }
53 |
54 | extension Parsers {
55 | public typealias Stream = Parsing.Stream // NB: Convenience type alias for discovery
56 | }
57 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/PrefixUpToTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class PrefixUpToTests: XCTestCase {
5 | func testSuccess() {
6 | var input = "Hello,world, 42!"[...]
7 | XCTAssertEqual("Hello,world", try PrefixUpTo(", ").parse(&input))
8 | XCTAssertEqual(", 42!", input)
9 | }
10 |
11 | func testSuccessIsEmpty() {
12 | var input = "Hello, world!"[...]
13 | XCTAssertEqual("", try PrefixUpTo("").parse(&input))
14 | XCTAssertEqual("Hello, world!", input)
15 | }
16 |
17 | func testFailureIsEmpty() {
18 | var input = ""[...]
19 | XCTAssertThrowsError(try PrefixUpTo(", ").parse(&input)) { error in
20 | XCTAssertEqual(
21 | """
22 | error: unexpected input
23 | --> input:1:1
24 | 1 |
25 | | ^ expected prefix up to ", "
26 | """,
27 | "\(error)"
28 | )
29 | }
30 | XCTAssertEqual("", input)
31 | }
32 |
33 | func testFailureNoMatch() {
34 | var input = "Hello world!"[...]
35 | XCTAssertThrowsError(try PrefixUpTo(", ").parse(&input)) { error in
36 | XCTAssertEqual(
37 | """
38 | error: unexpected input
39 | --> input:1:1
40 | 1 | Hello world!
41 | | ^ expected prefix up to ", "
42 | """,
43 | "\(error)"
44 | )
45 | }
46 | XCTAssertEqual("Hello world!", input)
47 | }
48 |
49 | func testUTF8() {
50 | var input = "Hello,world, 42!"[...].utf8
51 | XCTAssertEqual("Hello,world", Substring(try PrefixUpTo(", ".utf8).parse(&input)))
52 | XCTAssertEqual(", 42!", Substring(input))
53 | }
54 |
55 | func testPrint() throws {
56 | var input = ","[...]
57 | try PrefixUpTo(",").print("Hello", into: &input)
58 | XCTAssertEqual("Hello,", input)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/PrefixThroughTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class PrefixThroughTests: XCTestCase {
5 | func testSuccess() {
6 | var input = "Hello,world, 42!"[...]
7 | XCTAssertEqual("Hello,world, ", try PrefixThrough(", ").parse(&input))
8 | XCTAssertEqual("42!", input)
9 | }
10 |
11 | func testSuccessIsEmpty() {
12 | var input = "Hello, world!"[...]
13 | XCTAssertEqual("", try PrefixThrough("").parse(&input))
14 | XCTAssertEqual("Hello, world!", input)
15 | }
16 |
17 | func testFailureIsEmpty() {
18 | var input = ""[...]
19 | XCTAssertThrowsError(try PrefixThrough(", ").parse(&input)) { error in
20 | XCTAssertEqual(
21 | """
22 | error: unexpected input
23 | --> input:1:1
24 | 1 |
25 | | ^ expected prefix through ", "
26 | """,
27 | "\(error)"
28 | )
29 | }
30 | XCTAssertEqual("", input)
31 | }
32 |
33 | func testFailureNoMatch() {
34 | var input = "Hello world!"[...]
35 | XCTAssertThrowsError(try PrefixThrough(", ").parse(&input)) { error in
36 | XCTAssertEqual(
37 | """
38 | error: unexpected input
39 | --> input:1:1
40 | 1 | Hello world!
41 | | ^ expected prefix through ", "
42 | """,
43 | "\(error)"
44 | )
45 | }
46 | XCTAssertEqual("Hello world!", input)
47 | }
48 |
49 | func testUTF8() {
50 | var input = "Hello,world, 42!"[...].utf8
51 | XCTAssertEqual("Hello,world, ", Substring(try PrefixThrough(", ".utf8).parse(&input)))
52 | XCTAssertEqual("42!", Substring(input))
53 | }
54 |
55 | func testPrint() throws {
56 | var input = ""[...]
57 | try PrefixThrough(",").print("Hello,", into: &input)
58 | XCTAssertEqual("Hello,", input)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/PipeTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class PipeTests: XCTestCase {
5 | func testSuccess() {
6 | var input = "true Hello, world!"[...].utf8
7 | XCTAssertEqual(true, try Prefix(4).pipe { Bool.parser() }.parse(&input))
8 | XCTAssertEqual(" Hello, world!", Substring(input))
9 | }
10 |
11 | func testFailureOutput() {
12 | var input = "true Hello, world!"[...].utf8
13 | XCTAssertThrowsError(
14 | try Prefix(10).pipe {
15 | Bool.parser()
16 | }
17 | .parse(&input)
18 | ) { error in
19 | XCTAssertEqual(
20 | """
21 | error: unexpected input
22 | --> input:1:5-10
23 | 1 | true Hello, world!
24 | | ^^^^^^ expected end of pipe
25 | """,
26 | "\(error)"
27 | )
28 | }
29 | XCTAssertEqual(", world!", Substring(input))
30 | }
31 |
32 | func testFailureInput() {
33 | var input = "true"[...].utf8
34 | XCTAssertThrowsError(
35 | try PrefixUpTo("\n".utf8).pipe {
36 | Bool.parser()
37 | }
38 | .parse(&input)
39 | ) { error in
40 | XCTAssertEqual(
41 | #"""
42 | error: unexpected input
43 | --> input:1:1
44 | 1 | true
45 | | ^ expected prefix up to "\n"
46 | """#,
47 | "\(error)"
48 | )
49 | }
50 | XCTAssertEqual("true", Substring(input))
51 | }
52 |
53 | func testUsingDownstreamInput() {
54 | let utf8InputParser = Parse(input: Substring.UTF8View.self) {
55 | Always("Hello world"[...])
56 | }
57 | let pipedParser = utf8InputParser.pipe {
58 | Parse(input: Substring.self) {
59 | Always("123")
60 | }
61 | }
62 | var input = ""[...].utf8
63 | let output = pipedParser.parse(&input)
64 | XCTAssertEqual("123", output)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/Parsing/Conversions/FixedWidthInteger.swift:
--------------------------------------------------------------------------------
1 | extension Conversion where Self == Conversions.BinaryFloatingPointToFixedWidthInteger {
2 | /// A conversion from a `Double` to an `Int`.
3 | ///
4 | /// This conversion can be used to transform a ``ParserPrinter``'s double output into an integer
5 | /// output, rounding toward zero.
6 | ///
7 | /// ```swift
8 | /// Double.parser().map(.int).parse("123.45") // 123
9 | /// ```
10 | @inlinable
11 | public static var int: Self { .init() }
12 | }
13 |
14 | extension Conversion where Output == Double {
15 | /// Transforms this conversion to `Double` into a conversion to `Int`.
16 | ///
17 | /// A fluent version of ``Conversion/int-swift.type.property``. Equivalent to calling ``map(_:)``
18 | /// with ``Conversion/int-swift.type.property``:
19 | ///
20 | /// ```swift
21 | /// doubleConversion.int
22 | /// // =
23 | /// doubleConversion.map(.int)
24 | /// ```
25 | @inlinable
26 | public var int:
27 | Conversions.Map<
28 | Self, Conversions.BinaryFloatingPointToFixedWidthInteger
29 | >
30 | { self.map(.int) }
31 | }
32 |
33 | extension Conversions {
34 | /// A conversion from a `Double` to an `Int`.
35 | ///
36 | /// You will not typically need to interact with this type directly. Instead you will usually use
37 | /// the ``Conversion/int-swift.type.property`` operation, which constructs this type.
38 | public struct BinaryFloatingPointToFixedWidthInteger<
39 | Input: BinaryFloatingPoint, Output: FixedWidthInteger
40 | >: Conversion {
41 | @usableFromInline
42 | init() {}
43 |
44 | @inlinable
45 | public func apply(_ input: Input) -> Output {
46 | .init(input)
47 | }
48 |
49 | @inlinable
50 | public func unapply(_ output: Output) -> Input {
51 | .init(output)
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/FromSubstringTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | @available(*, deprecated)
5 | final class FromSubstringTests: XCTestCase {
6 | func testUTF8View() {
7 | let p = Parse(input: Substring.UTF8View.self) {
8 | "caf".utf8
9 | From(.substring) { "é" }
10 | }
11 |
12 | var input = "caf\u{00E9}"[...].utf8
13 | XCTAssertNoThrow(try p.parse(&input))
14 | XCTAssert(input.isEmpty)
15 |
16 | input = "cafe\u{0301}"[...].utf8
17 | XCTAssertNoThrow(try p.parse(&input))
18 | XCTAssert(input.isEmpty)
19 | }
20 |
21 | func testUnicodeScalarView() {
22 | let p = Parse {
23 | "caf".unicodeScalars
24 | From(.substring) { "é" }
25 | }
26 |
27 | var input = "caf\u{00E9}"[...].unicodeScalars
28 | XCTAssertNoThrow(try p.parse(&input))
29 | XCTAssert(input.isEmpty)
30 |
31 | input = "cafe\u{0301}"[...].unicodeScalars
32 | XCTAssertNoThrow(try p.parse(&input))
33 | XCTAssert(input.isEmpty)
34 | }
35 |
36 | func testDeprecatedUTF8View() {
37 | let p = Parse(input: Substring.UTF8View.self) {
38 | "caf".utf8
39 | FromSubstring { "é" }
40 | }
41 |
42 | var input = "caf\u{00E9}"[...].utf8
43 | XCTAssertNoThrow(try p.parse(&input))
44 | XCTAssert(input.isEmpty)
45 |
46 | input = "cafe\u{0301}"[...].utf8
47 | XCTAssertNoThrow(try p.parse(&input))
48 | XCTAssert(input.isEmpty)
49 | }
50 |
51 | func testDeprecatedUnicodeScalarView() {
52 | let p = Parse {
53 | "caf".unicodeScalars
54 | FromSubstring { "é" }
55 | }
56 |
57 | var input = "caf\u{00E9}"[...].unicodeScalars
58 | XCTAssertNoThrow(try p.parse(&input))
59 | XCTAssert(input.isEmpty)
60 |
61 | input = "cafe\u{0301}"[...].unicodeScalars
62 | XCTAssertNoThrow(try p.parse(&input))
63 | XCTAssert(input.isEmpty)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/RestTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class RestTests: XCTestCase {
5 | func testParseRest() {
6 | var input = "Hello, world!"[...]
7 | XCTAssertEqual("Hello, world!", try Rest().parse(&input))
8 | XCTAssertEqual("", input)
9 | }
10 |
11 | func testParseRestFailsOnEmpty() {
12 | var input = ""[...]
13 | XCTAssertThrowsError(try Rest().parse(&input)) { error in
14 | XCTAssertEqual(
15 | """
16 | error: unexpected input
17 | --> input:1:1
18 | 1 |
19 | | ^ expected a non-empty input
20 | """,
21 | "\(error)"
22 | )
23 | }
24 | XCTAssertEqual("", input)
25 | }
26 |
27 | func testPrintRest() {
28 | XCTAssertEqual(try Rest().print("Hello"[...]), "Hello")
29 | }
30 |
31 | func testPrintRestFailsOnUpcoming() throws {
32 | var input = ", World!"[...]
33 | XCTAssertThrowsError(try Rest().print("Hello", into: &input)) { error in
34 | XCTAssertEqual(
35 | """
36 | error: round-trip expectation failed
37 |
38 | A "Rest" parser-printer expected to have printed all remaining input, but more was printed.
39 |
40 | ", World!"
41 |
42 | During a round-trip, the "Rest" parser-printer would have parsed this remaining input.
43 | """,
44 | "\(error)"
45 | )
46 | }
47 | }
48 |
49 | func testPrintRestFailsOnEmpty() throws {
50 | XCTAssertThrowsError(try Rest().print(""[...])) { error in
51 | XCTAssertEqual(
52 | """
53 | error: round-trip expectation failed
54 |
55 | A "Rest" parser-printer attempted to print an empty Substring.
56 |
57 | During a round-trip, the "Rest" parser-printer would have failed to parse an empty input.
58 | """,
59 | "\(error)"
60 | )
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/From.swift:
--------------------------------------------------------------------------------
1 | public struct From: Parser
2 | where Upstream.Output == DownstreamInput, Downstream.Input == DownstreamInput {
3 | @usableFromInline
4 | let conversion: Upstream
5 |
6 | @usableFromInline
7 | let parser: Downstream
8 |
9 | @inlinable
10 | public init(_ conversion: Upstream, @ParserBuilder _ parser: () -> Downstream) {
11 | self.conversion = conversion
12 | self.parser = parser()
13 | }
14 |
15 | @inlinable
16 | public func parse(_ input: inout Upstream.Input) rethrows -> Downstream.Output {
17 | var parserInput = try self.conversion.apply(input)
18 | let output = try self.parser.parse(&parserInput)
19 | input = try self.conversion.unapply(parserInput)
20 | return output
21 | }
22 | }
23 |
24 | extension From: ParserPrinter where Downstream: ParserPrinter {
25 | @inlinable
26 | public func print(_ output: Downstream.Output, into input: inout Upstream.Input) rethrows {
27 | var parserInput = try self.conversion.apply(input)
28 | try self.parser.print(output, into: &parserInput)
29 | input = try self.conversion.unapply(parserInput)
30 | }
31 | }
32 |
33 | // TODO: Do we want to ship this?
34 | extension Parsers {
35 | public struct Identity: ParserPrinter {
36 | @usableFromInline
37 | init() {}
38 |
39 | @inlinable
40 | public func parse(_ input: inout InputOutput) -> InputOutput {
41 | input
42 | }
43 |
44 | @inlinable
45 | public func print(_ output: InputOutput, into input: inout InputOutput) {
46 | input = output
47 | }
48 | }
49 | }
50 |
51 | extension From {
52 | @inlinable
53 | public init(_ conversion: Upstream) where Downstream == Parsers.Identity {
54 | self.conversion = conversion
55 | self.parser = .init()
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/Literal.swift:
--------------------------------------------------------------------------------
1 | extension Array: Parser, ParserPrinter where Element: Equatable {
2 | @inlinable
3 | public func parse(_ input: inout ArraySlice) throws {
4 | guard input.starts(with: self) else {
5 | throw ParsingError.expectedInput(self.debugDescription, at: input)
6 | }
7 | input.removeFirst(self.count)
8 | }
9 |
10 | @inlinable
11 | public func print(_ output: (), into input: inout SubSequence) {
12 | input.prepend(contentsOf: self)
13 | }
14 | }
15 |
16 | extension String: ParserPrinter {
17 | @inlinable
18 | public func parse(_ input: inout Substring) throws {
19 | guard input.starts(with: self) else {
20 | throw ParsingError.expectedInput(self.debugDescription, at: input)
21 | }
22 | input.removeFirst(self.count)
23 | }
24 |
25 | @inlinable
26 | public func print(_ output: (), into input: inout SubSequence) {
27 | input.prepend(contentsOf: self)
28 | }
29 | }
30 |
31 | extension String.UnicodeScalarView: ParserPrinter {
32 | @inlinable
33 | public func parse(_ input: inout Substring.UnicodeScalarView) throws {
34 | guard input.starts(with: self) else {
35 | throw ParsingError.expectedInput(String(self).debugDescription, at: input)
36 | }
37 | input.removeFirst(self.count)
38 | }
39 |
40 | @inlinable
41 | public func print(_ output: (), into input: inout SubSequence) {
42 | input.prepend(contentsOf: self)
43 | }
44 | }
45 |
46 | extension String.UTF8View: ParserPrinter {
47 | @inlinable
48 | public func parse(_ input: inout Substring.UTF8View) throws {
49 | guard input.starts(with: self) else {
50 | throw ParsingError.expectedInput(String(self).debugDescription, at: input)
51 | }
52 | input.removeFirst(self.count)
53 | }
54 |
55 | @inlinable
56 | public func print(_ output: (), into input: inout SubSequence) {
57 | input.prepend(contentsOf: self)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/OptionallyTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class OptionalTests: XCTestCase {
5 | struct OptionalBool: ParserPrinter {
6 | var body: some ParserPrinter {
7 | Optionally { Bool.parser() }
8 | }
9 | }
10 |
11 | func testParseWrappedSuccess() {
12 | var input = "true Hello, world!"[...].utf8
13 | XCTAssertEqual(.some(true), try OptionalBool().parse(&input))
14 | XCTAssertEqual(" Hello, world!", Substring(input))
15 | }
16 |
17 | func testParseWrappedFailure() {
18 | var input = "Hello, world!"[...].utf8
19 | XCTAssertEqual(.none, try OptionalBool().parse(&input))
20 | XCTAssertEqual("Hello, world!", Substring(input))
21 | }
22 |
23 | func testParseBacktracking() {
24 | let parser = Parse {
25 | "Hello,"
26 | Optionally {
27 | " "
28 | Bool.parser()
29 | }
30 | " world!"
31 | }
32 |
33 | XCTAssertEqual(.some(true), try parser.parse("Hello, true world!"))
34 | XCTAssertEqual(.none, try parser.parse("Hello, world!"))
35 | }
36 |
37 | func testPrintNilSuccess() {
38 | var input = "!"[...]
39 |
40 | XCTAssertNoThrow(try OptionalBool().print(.none, into: &input.utf8))
41 | XCTAssertEqual("!"[...], input)
42 | }
43 |
44 | func testPrintValueSuccess() {
45 | var input = "!"[...]
46 |
47 | XCTAssertNoThrow(try OptionalBool().print(.some(true), into: &input.utf8))
48 | XCTAssertEqual("true!"[...], input)
49 | }
50 |
51 | func testPrintBadOptionalValue() {
52 | struct OptionalPrefix: ParserPrinter {
53 | var body: some ParserPrinter {
54 | Optionally { Prefix { !$0.isWhitespace } }
55 | }
56 | }
57 |
58 | var input = "!"[...]
59 |
60 | XCTAssertThrowsError(
61 | try OptionalPrefix().print("foo bar", into: &input))
62 | XCTAssertEqual("!"[...], input)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/End.swift:
--------------------------------------------------------------------------------
1 | /// A parser that succeeds if the input is empty, and fails otherwise.
2 | ///
3 | /// Useful as a final parser in a long sequence of parsers to guarantee that all input has been
4 | /// consumed, especially as ``Many``'s terminal parser.
5 | ///
6 | /// This parser will fail if there are input elements that have not been consumed:
7 | ///
8 | /// ```swift
9 | /// input = "Hello, Blob!!"
10 | /// try parser.parse(&input)
11 | /// // error: unexpected input
12 | /// // --> input:1:13
13 | /// // 1 | Hello, Blob!!
14 | /// // | ^ expected end of input
15 | /// ```
16 | ///
17 | /// > Note: This parser is automatically inserted when you invoke the non-incremental
18 | /// > ``Parser/parse(_:)-6h1d0`` and ``Parser/parse(_:)-2wzcq`` methods.
19 | public struct End: ParserPrinter {
20 | @inlinable
21 | public init() {}
22 |
23 | @inlinable
24 | public func parse(_ input: inout Input) throws {
25 | var iterator = input.makeIterator()
26 | guard iterator.next() == nil else {
27 | throw ParsingError.expectedInput("end of input", at: input)
28 | }
29 | }
30 |
31 | @inlinable
32 | public func print(_ output: (), into input: inout Input) throws {
33 | var iterator = input.makeIterator()
34 | guard iterator.next() == nil else {
35 | let description = describe(input).map { "\n\n\($0.debugDescription)" } ?? ""
36 | throw PrintingError.failed(
37 | summary: """
38 | round-trip expectation failed
39 |
40 | An "End" parser-printer expected no more input, but more was printed.\(description)
41 |
42 | During a round-trip, the "End" parser-printer would have failed to parse at this \
43 | remaining input.
44 | """,
45 | input: input
46 | )
47 | }
48 | }
49 | }
50 |
51 | extension Parsers {
52 | public typealias End = Parsing.End // NB: Convenience type alias for discovery
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/Not.swift:
--------------------------------------------------------------------------------
1 | /// A parser that succeeds if the given parser fails, and does not consume any input.
2 | ///
3 | /// For example, to parse a line from an input that does not start with "//" one can do:
4 | ///
5 | /// ```swift
6 | /// let uncommentedLine = Parse {
7 | /// Not { "//" }
8 | /// PrefixUpTo("\n")
9 | /// }
10 | ///
11 | /// try uncommentedLine.parse("let x = 1") // ✅ "let x = 1"
12 | ///
13 | /// try uncommentedLine.parse("// let x = 1") // ❌
14 | /// // error: unexpected input
15 | /// // --> input:1:1-2
16 | /// // 1 | // let x = 1
17 | /// // | ^^ expected not to be processed
18 | /// ```
19 | public struct Not: ParserPrinter where Upstream.Input == Input {
20 | public let upstream: Upstream
21 |
22 | /// Creates a parser that succeeds if the given parser fails, and does not consume any input.
23 | ///
24 | /// - Parameter build: A parser that causes this parser to fail if it succeeds, or succeed if it
25 | /// fails.
26 | @inlinable
27 | public init(@ParserBuilder _ build: () -> Upstream) {
28 | self.upstream = build()
29 | }
30 |
31 | @inlinable
32 | public func parse(_ input: inout Upstream.Input) throws {
33 | let original = input
34 | do {
35 | _ = try self.upstream.parse(&input)
36 | } catch {
37 | input = original
38 | return
39 | }
40 | throw ParsingError.expectedInput("not to be processed", from: original, to: input)
41 | }
42 |
43 | @inlinable
44 | public func print(_ output: (), into input: inout Upstream.Input) throws {
45 | do {
46 | _ = try self.upstream.parse(&input)
47 | } catch {
48 | return
49 | }
50 | throw PrintingError.failed(
51 | summary: """
52 | round-trip expectation failed
53 |
54 | A "Not" parser-printer was handed a value to print that it would have failed to parse.
55 | """,
56 | input: input
57 | )
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Parsing.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-argument-parser",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/apple/swift-argument-parser",
7 | "state" : {
8 | "revision" : "6b2aa2748a7881eebb9f84fb10c01293e15b52ca",
9 | "version" : "0.5.0"
10 | }
11 | },
12 | {
13 | "identity" : "swift-benchmark",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/google/swift-benchmark",
16 | "state" : {
17 | "revision" : "a0564bf88df5f94eec81348a2f089494c6b28d80",
18 | "version" : "0.1.1"
19 | }
20 | },
21 | {
22 | "identity" : "swift-case-paths",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/pointfreeco/swift-case-paths",
25 | "state" : {
26 | "revision" : "642e6aab8e03e5f992d9c83e38c5be98cfad5078",
27 | "version" : "1.5.5"
28 | }
29 | },
30 | {
31 | "identity" : "swift-docc-plugin",
32 | "kind" : "remoteSourceControl",
33 | "location" : "https://github.com/apple/swift-docc-plugin",
34 | "state" : {
35 | "revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6",
36 | "version" : "1.0.0"
37 | }
38 | },
39 | {
40 | "identity" : "swift-syntax",
41 | "kind" : "remoteSourceControl",
42 | "location" : "https://github.com/swiftlang/swift-syntax",
43 | "state" : {
44 | "revision" : "cb53fa1bd3219b0b23ded7dfdd3b2baff266fd25",
45 | "version" : "600.0.0"
46 | }
47 | },
48 | {
49 | "identity" : "xctest-dynamic-overlay",
50 | "kind" : "remoteSourceControl",
51 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
52 | "state" : {
53 | "revision" : "bc2a151366f2cd0e347274544933bc2acb00c9fe",
54 | "version" : "1.4.0"
55 | }
56 | }
57 | ],
58 | "version" : 2
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/Parsing/Conversions/Enum.swift:
--------------------------------------------------------------------------------
1 | import CasePaths
2 |
3 | extension Conversion {
4 | /// Converts the associated values of an enum case into the case, and an enum case into its
5 | /// associated values.
6 | ///
7 | /// Useful for transforming the output of a ``ParserPrinter`` into an enum:
8 | ///
9 | /// ```swift
10 | /// enum Expression {
11 | /// case add(Int, Int)
12 | /// ...
13 | /// }
14 | ///
15 | /// let add = ParsePrint(.case(Expression.add)) {
16 | /// Int.parser()
17 | /// "+"
18 | /// Int.parser()
19 | /// }
20 | /// try add.parse("1+2") // Expression.add(1, 2)
21 | /// ```
22 | ///
23 | /// To transform the output of a ``ParserPrinter`` into a struct, see ``memberwise(_:)``.
24 | ///
25 | /// - Parameter embed: An embed function where `Values` directly maps to the memory
26 | /// layout of `Enum`, for example the internal, default initializer that is automatically
27 | /// synthesized for structs.
28 | /// - Returns: A conversion that can embed the associated values of an enum case into the case,
29 | /// and extract the associated values from the case.
30 | @inlinable
31 | public static func `case`(
32 | _ initializer: @escaping (Values) -> Enum
33 | ) -> Self where Self == CasePath {
34 | /initializer
35 | }
36 |
37 | @inlinable
38 | public static func `case`(
39 | _ initializer: Enum
40 | ) -> Self where Self == CasePath {
41 | /initializer
42 | }
43 | }
44 |
45 | extension CasePath: Conversion {
46 | @inlinable
47 | public func apply(_ input: Value) -> Root {
48 | self.embed(input)
49 | }
50 |
51 | @inlinable
52 | public func unapply(_ output: Root) throws -> Value {
53 | guard let value = self.extract(from: output)
54 | else {
55 | throw ConvertingError(
56 | """
57 | case: Failed to extract \(Value.self) from \(output).
58 | """
59 | )
60 | }
61 | return value
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/Parsing/Conversions/Data.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Conversion where Self == Conversions.BytesToData {
4 | /// A conversion from `Substring.UTF8View` to `Data`.
5 | @inlinable
6 | public static var data: Self { .init() }
7 | }
8 |
9 | extension Conversion where Output == Substring.UTF8View {
10 | /// Transforms this conversion to `Substring.UTF8View` into a conversion to `Data`.
11 | ///
12 | /// A fluent version of ``Conversion/data-swift.type.property-8z7qz``.
13 | @inlinable
14 | public var data: Conversions.Map> {
15 | self.map(.data)
16 | }
17 | }
18 |
19 | extension Conversion where Self == Conversions.BytesToData> {
20 | /// A conversion from `ArraySlice` to `Data`.
21 | @inlinable
22 | public static var data: Self { .init() }
23 | }
24 |
25 | extension Conversion where Output == ArraySlice {
26 | /// Transforms this conversion to `ArraySlice` into a conversion to `Data`.
27 | ///
28 | /// A fluent version of ``Conversion/data-swift.type.property-7g9sj``.
29 | @inlinable
30 | public var data: Conversions.Map> {
31 | self.map(.data)
32 | }
33 | }
34 |
35 | extension Conversions {
36 | /// A conversion from a ``PrependableCollection`` of UTF-8 bytes to `Data`.
37 | ///
38 | /// You will not typically need to interact with this type directly. Instead you will usually use
39 | /// the ``Conversion/data-swift.type.property-8z7qz`` and
40 | /// ``Conversion/data-swift.type.property-7g9sj`` operations, which constructs this type.
41 | public struct BytesToData: Conversion where Input.Element == UInt8 {
42 | @usableFromInline
43 | init() {}
44 |
45 | @inlinable
46 | public func apply(_ input: Input) -> Data {
47 | .init(input)
48 | }
49 |
50 | @inlinable
51 | public func unapply(_ output: Data) -> Input {
52 | .init(output)
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/swift-parsing-benchmark/StringAbstractions.swift:
--------------------------------------------------------------------------------
1 | import Benchmark
2 | import Parsing
3 |
4 | /// This benchmark demonstrates how to parse on multiple string abstractions at once, and the costs
5 | /// of doing so. The parsers benchmarked parse a list of integers that are separated by a
6 | /// UTF8 character with multiple equivalent representations: "LATIN SMALL LETTER E WITH ACUTE" and
7 | /// "E + COMBINING ACUTE ACCENT".
8 | ///
9 | /// In the "Substring" suite we parse integers on the UTF8View abstraction and parse the separator
10 | /// on the Substring abstraction in order to take advantage of its UTF8 normalization logic.
11 | ///
12 | /// In the "UTF8" suite we parse both the integers and the separators on the UTF8View abstraction,
13 | /// but this means we are responsible for handling UTF8 normalization, so we have to explicitly
14 | /// handle both the "LATIN SMALL LETTER E WITH ACUTE" and "E + COMBINING ACUTE ACCENT" characters.
15 | let stringAbstractionsSuite = BenchmarkSuite(name: "String Abstractions") { suite in
16 | let count = 1_000
17 | let input = (1...count)
18 | .reduce(into: "") { accum, int in
19 | accum += "\(int)" + (int.isMultiple(of: 2) ? "\u{00E9}" : "e\u{0301}")
20 | }
21 | .dropLast()
22 |
23 | var output: [Int]!
24 | suite.benchmark("Substring") {
25 | let parser: some Parser = Many {
26 | Int.parser()
27 | } separator: {
28 | From(.substring) { "\u{00E9}" }
29 | }
30 |
31 | var input = input[...].utf8
32 | output = try parser.parse(&input)
33 | } tearDown: {
34 | precondition(output.count == count)
35 | }
36 |
37 | suite.benchmark("UTF8") {
38 | let parser: some Parser = Many {
39 | Int.parser()
40 | } separator: {
41 | OneOf {
42 | "\u{00E9}".utf8
43 | "e\u{0301}".utf8
44 | }
45 | }
46 |
47 | var input = input[...].utf8
48 | output = try parser.parse(&input)
49 | } tearDown: {
50 | precondition(output.count == count)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/CharacterSet.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension CharacterSet: ParserPrinter {
4 | @inlinable
5 | public func parse(_ input: inout Substring) -> Substring {
6 | let output = input.unicodeScalars.prefix(while: self.contains)
7 | input.unicodeScalars.removeFirst(output.count)
8 | return Substring(output)
9 | }
10 |
11 | @inlinable
12 | public func print(_ output: Substring, into input: inout Substring) throws {
13 | let prefix = output.unicodeScalars.prefix(while: self.contains)
14 | guard prefix.endIndex == output.unicodeScalars.endIndex
15 | else {
16 | let indent = String(repeating: " ", count: Substring(prefix).debugDescription.count - 1)
17 | throw PrintingError.failed(
18 | summary: """
19 | round-trip expectation failed
20 |
21 | A character set does not contain a character to be printed.
22 |
23 | \(output.debugDescription)
24 | \(indent)^ not found in set
25 |
26 | \(self)
27 |
28 | During a round-trip, the character set would have failed to parse this character, which \
29 | means its data is in an invalid state.
30 | """,
31 | input: input
32 | )
33 | }
34 |
35 | guard input.first?.unicodeScalars.allSatisfy(self.contains) != true
36 | else {
37 | var prefix = input.prefix(70)
38 | if prefix.endIndex != input.endIndex { prefix.append("…") }
39 | throw PrintingError.failed(
40 | summary: """
41 | round-trip expectation failed
42 |
43 | A character set contains a character that is printed by the next printer.
44 |
45 | \(prefix.debugDescription)
46 | ^ found in set
47 |
48 | \(self)
49 |
50 | During a round-trip, the character set would have parsed this character, which means the \
51 | data handed to the next printer is in an invalid state.
52 | """,
53 | input: input
54 | )
55 | }
56 |
57 | input.prepend(contentsOf: output)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/Parsing/Conversions/BinaryFloatingPoint.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Conversion where Self == Conversions.FixedWidthIntegerToBinaryFloatingPoint {
4 | /// A conversion from an `Int` to a `Double`.
5 | ///
6 | /// This conversion can be used to transform a ``ParserPrinter``'s integer output into a double
7 | /// output:
8 | ///
9 | /// ```swift
10 | /// Digits().map(.double).parse("123") // ✅ 123.0
11 | ///
12 | /// Digits().map(.double).parse("123.0") // ❌
13 | /// // error: unexpected input
14 | /// // --> input:1:4
15 | /// // 1 | 123.0
16 | /// // | ^ expected end of input
17 | /// ```
18 | @inlinable
19 | public static var double: Self { .init() }
20 | }
21 |
22 | extension Conversion where Output == Int {
23 | /// Transforms this conversion to `Int` into a conversion to `Double`.
24 | ///
25 | /// A fluent version of ``Conversion/double-swift.type.property``. Equivalent to calling
26 | /// ``map(_:)`` with ``Conversion/double-swift.type.property``:
27 | ///
28 | /// ```swift
29 | /// intConversion.double
30 | /// // =
31 | /// intConversion.map(.double)
32 | /// ```
33 | @inlinable
34 | public var double:
35 | Conversions.Map<
36 | Self, Conversions.FixedWidthIntegerToBinaryFloatingPoint
37 | >
38 | { self.map(.double) }
39 | }
40 |
41 | extension Conversions {
42 | /// A conversion from an `Int` to a `Double`.
43 | ///
44 | /// You will not typically need to interact with this type directly. Instead you will usually use
45 | /// the ``Conversion/double-swift.type.property`` operation, which constructs this type.
46 | public struct FixedWidthIntegerToBinaryFloatingPoint<
47 | Input: FixedWidthInteger, Output: BinaryFloatingPoint
48 | >: Conversion {
49 | @usableFromInline
50 | init() {}
51 |
52 | @inlinable
53 | public func apply(_ input: Input) -> Output {
54 | .init(input)
55 | }
56 |
57 | @inlinable
58 | public func unapply(_ output: Output) -> Input {
59 | .init(output)
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/DigitsTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class DigitsTests: XCTestCase {
5 | func testDigits() throws {
6 | var input = "201801"[...].utf8
7 | XCTAssertEqual(2018, try Digits(4).parse(&input))
8 | XCTAssertEqual("01", String(input))
9 |
10 | XCTAssertThrowsError(try Digits(4).parse(&input)) { error in
11 | XCTAssertEqual(
12 | """
13 | error: unexpected input
14 | --> input:1:5
15 | 1 | 201801
16 | | ^ expected 4 digits
17 | """,
18 | "\(error)"
19 | )
20 | }
21 |
22 | XCTAssertThrowsError(try Digits(4...).parse(&input)) { error in
23 | XCTAssertEqual(
24 | """
25 | error: unexpected input
26 | --> input:1:5
27 | 1 | 201801
28 | | ^ expected at least 4 digits
29 | """,
30 | "\(error)"
31 | )
32 | }
33 |
34 | XCTAssertEqual("0", try Digits().print(0))
35 | XCTAssertEqual("00", try Digits(2).print(0))
36 | XCTAssertEqual("01", try Digits(2).print(1))
37 |
38 | XCTAssertThrowsError(try Digits(2).print(255) as Substring) { error in
39 | XCTAssertEqual(
40 | """
41 | error: round-trip expectation failed
42 |
43 | A "Digits" parser configured to parse at most 2 digits tried to print 255 (3 digits).
44 | """,
45 | "\(error)"
46 | )
47 | }
48 | }
49 |
50 | func testZeroMinimum() {
51 | struct Rational: Equatable { var numerator, denominator: Int }
52 |
53 | struct RationalParser: ParserPrinter {
54 | var body: some ParserPrinter {
55 | Parse(.memberwise(Rational.init)) {
56 | Digits(0...)
57 | "."
58 | Digits(1...)
59 | }
60 | }
61 | }
62 |
63 | XCTAssertEqual(
64 | try RationalParser().parse(".123"),
65 | .init(numerator: 0, denominator: 123)
66 | )
67 | XCTAssertEqual(
68 | try RationalParser().print(Rational(numerator: 0, denominator: 123)),
69 | ".123"
70 | )
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/Bool.swift:
--------------------------------------------------------------------------------
1 | extension Bool {
2 | /// A parser that consumes a Boolean value from the beginning of a collection of UTF-8 code units.
3 | ///
4 | /// See for more information about this parser.
5 | ///
6 | /// - Parameter inputType: The collection type of UTF-8 code units to parse.
7 | /// - Returns: A parser that consumes a Boolean value from the beginning of a collection of UTF-8
8 | /// code units.
9 | @inlinable
10 | public static func parser(
11 | of inputType: Input.Type = Input.self
12 | ) -> Parsers.BoolParser {
13 | .init()
14 | }
15 | }
16 |
17 | extension Parsers {
18 | /// A parser that consumes a Boolean value from the beginning of a collection of UTF-8 code units.
19 | ///
20 | /// You will not typically need to interact with this type directly. Instead you will usually use
21 | /// `Bool.parser()`, which constructs this type.
22 | ///
23 | /// See for more information about this parser.
24 | public struct BoolParser: Parser
25 | where
26 | Input.SubSequence == Input,
27 | Input.Element == UTF8.CodeUnit
28 | {
29 | @inlinable
30 | public init() {}
31 |
32 | @inlinable
33 | public func parse(_ input: inout Input) throws -> Bool {
34 | if input.starts(with: [116, 114, 117, 101] /*"true".utf8*/) {
35 | input.removeFirst(4)
36 | return true
37 | } else if input.starts(with: [102, 97, 108, 115, 101] /*"false".utf8*/) {
38 | input.removeFirst(5)
39 | return false
40 | }
41 | throw ParsingError.expectedInput("\"true\" or \"false\"", at: input)
42 | }
43 | }
44 | }
45 |
46 | extension Parsers.BoolParser: ParserPrinter where Input: PrependableCollection {
47 | @inlinable
48 | public func print(_ output: Bool, into input: inout Input) {
49 | switch output {
50 | case true:
51 | input.prepend(contentsOf: [116, 114, 117, 101] /*"true".utf8*/)
52 | case false:
53 | input.prepend(contentsOf: [102, 97, 108, 115, 101] /*"false".utf8*/)
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/Optionally.swift:
--------------------------------------------------------------------------------
1 | /// A parser that runs the given parser and succeeds with `nil` if it fails.
2 | ///
3 | /// Use this parser when you are parsing into an output data model that contains `nil`.
4 | ///
5 | /// When the wrapped parser fails ``Optionally`` will backtrack any consumption of the input so
6 | /// that later parsers can attempt to parser the input:
7 | ///
8 | /// ```swift
9 | /// let parser = Parse {
10 | /// "Hello,"
11 | /// Optionally { " "; Bool.parser() }
12 | /// " world!"
13 | /// }
14 | ///
15 | /// try parser.parse("Hello, world!") // nil 1️⃣
16 | /// try parser.parse("Hello, true world!") // true
17 | /// ```
18 | ///
19 | /// If ``Optionally`` did not backtrack then 1️⃣ would fail because it would consume a space,
20 | /// causing the `" world!"` parser to fail since there is no longer any space to consume.
21 | /// Read the article to learn more about how backtracking works.
22 | ///
23 | /// If you are optionally parsing input that should coalesce into some default, you can skip the
24 | /// optionality and instead use ``replaceError(with:)`` with a default:
25 | ///
26 | /// ```swift
27 | /// Optionally { Int.parser() }
28 | /// .map { $0 ?? 0 }
29 | ///
30 | /// // vs.
31 | ///
32 | /// Int.parser()
33 | /// .replaceError(with: 0)
34 | /// ```
35 | public struct Optionally: Parser where Wrapped.Input == Input {
36 | public let wrapped: Wrapped
37 |
38 | @inlinable
39 | public init(@ParserBuilder _ build: () -> Wrapped) {
40 | self.wrapped = build()
41 | }
42 |
43 | @inlinable
44 | public func parse(_ input: inout Wrapped.Input) -> Wrapped.Output? {
45 | let original = input
46 | do {
47 | return try self.wrapped.parse(&input)
48 | } catch {
49 | input = original
50 | return nil
51 | }
52 | }
53 | }
54 |
55 | extension Optionally: ParserPrinter where Wrapped: ParserPrinter {
56 | @inlinable
57 | public func print(_ output: Wrapped.Output?, into input: inout Wrapped.Input) rethrows {
58 | guard let output = output else { return }
59 | try self.wrapped.print(output, into: &input)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/Conditional.swift:
--------------------------------------------------------------------------------
1 | extension Parsers {
2 | /// A parser that can parse output from two types of parsers.
3 | ///
4 | /// This parser is useful for situations where you want to run one of two different parsers based on
5 | /// a condition, which typically would force you to perform ``Parser/eraseToAnyParser()`` and incur
6 | /// a performance penalty.
7 | ///
8 | /// For example, you can use this parser in a ``Parser/flatMap(_:)`` operation to use the parsed
9 | /// output to determine what parser to run next:
10 | ///
11 | /// ```swift
12 | /// versionParser.flatMap { version in
13 | /// version == "2.0"
14 | /// ? Conditional.first(V2Parser())
15 | /// : Conditional.second(LegacyParser())
16 | /// }
17 | /// ```
18 | ///
19 | /// You won't typically construct this parser directly, but instead will use standard `if`-`else`
20 | /// statements in a parser builder to automatically build conditional parsers:
21 | ///
22 | /// ```swift
23 | /// versionParser.flatMap { version in
24 | /// if version == "2.0" {
25 | /// V2Parser()
26 | /// } else {
27 | /// LegacyParser()
28 | /// }
29 | /// }
30 | /// ```
31 | public enum Conditional: Parser
32 | where
33 | First.Input == Second.Input,
34 | First.Output == Second.Output
35 | {
36 | case first(First)
37 | case second(Second)
38 |
39 | @inlinable
40 | public func parse(_ input: inout First.Input) rethrows -> First.Output {
41 | switch self {
42 | case let .first(first):
43 | return try first.parse(&input)
44 | case let .second(second):
45 | return try second.parse(&input)
46 | }
47 | }
48 | }
49 | }
50 |
51 | extension Parsers.Conditional: ParserPrinter
52 | where
53 | First: ParserPrinter,
54 | Second: ParserPrinter
55 | {
56 | @inlinable
57 | public func print(_ output: First.Output, into input: inout First.Input) rethrows {
58 | switch self {
59 | case let .first(first):
60 | try first.print(output, into: &input)
61 | case let .second(second):
62 | try second.print(output, into: &input)
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/OneOfMany.swift:
--------------------------------------------------------------------------------
1 | extension Parsers {
2 | /// A parser that attempts to run a number of parsers till one succeeds.
3 | ///
4 | /// You will not typically need to interact with this type directly. Instead you will usually loop
5 | /// over each parser in a builder block:
6 | ///
7 | /// ```swift
8 | /// enum Role: String, CaseIterable {
9 | /// case admin
10 | /// case guest
11 | /// case member
12 | /// }
13 | ///
14 | /// let roleParser = OneOf {
15 | /// for role in Role.allCases {
16 | /// role.rawValue.map { role }
17 | /// }
18 | /// }
19 | /// ```
20 | public struct OneOfMany: Parser {
21 | public let parsers: [Parsers]
22 |
23 | @inlinable
24 | public init(_ parsers: [Parsers]) {
25 | self.parsers = parsers
26 | }
27 |
28 | @inlinable
29 | @inline(__always)
30 | public func parse(_ input: inout Parsers.Input) throws -> Parsers.Output {
31 | let original = input
32 | var count = self.parsers.count
33 | var errors: [Error] = []
34 | errors.reserveCapacity(count)
35 | for parser in self.parsers {
36 | do {
37 | return try parser.parse(&input)
38 | } catch {
39 | count -= 1
40 | if count > 0 { input = original }
41 | errors.append(error)
42 | }
43 | }
44 | throw ParsingError.manyFailed(errors, at: original)
45 | }
46 | }
47 | }
48 |
49 | extension Parsers.OneOfMany: ParserPrinter where Parsers: ParserPrinter {
50 | @inlinable
51 | public func print(_ output: Parsers.Output, into input: inout Parsers.Input) throws {
52 | let original = input
53 | var count = self.parsers.count
54 | var errors: [Error] = []
55 | errors.reserveCapacity(count)
56 | for parser in self.parsers.reversed() {
57 | do {
58 | try parser.print(output, into: &input)
59 | return
60 | } catch {
61 | count -= 1
62 | if count > 0 { input = original }
63 | errors.insert(error, at: errors.startIndex) // TODO: Should this be `append`?
64 | }
65 | }
66 | throw PrintingError.manyFailed(errors, at: input)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/EndTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class EndTests: XCTestCase {
5 | func testSuccess() {
6 | var input = ""[...]
7 | XCTAssertNoThrow(try End().parse(&input))
8 | XCTAssertEqual("", input)
9 | }
10 |
11 | func testFailure() {
12 | var input = "Hello, world!"[...]
13 | XCTAssertThrowsError(try End().parse(&input)) { error in
14 | XCTAssertEqual(
15 | """
16 | error: unexpected input
17 | --> input:1:1
18 | 1 | Hello, world!
19 | | ^ expected end of input
20 | """,
21 | "\(error)"
22 | )
23 | }
24 | XCTAssertEqual("Hello, world!", input)
25 | }
26 |
27 | func testPrintSuccess() {
28 | var input = ""[...]
29 | XCTAssertNoThrow(try End().print(into: &input))
30 | XCTAssertEqual(input, ""[...])
31 | }
32 |
33 | func testPrintFailure() {
34 | var input = "Hello, world!"[...]
35 | XCTAssertThrowsError(try End().print(into: &input)) { error in
36 | XCTAssertEqual(
37 | """
38 | error: round-trip expectation failed
39 |
40 | An "End" parser-printer expected no more input, but more was printed.
41 |
42 | "Hello, world!"
43 |
44 | During a round-trip, the "End" parser-printer would have failed to parse at this \
45 | remaining input.
46 | """,
47 | "\(error)"
48 | )
49 | }
50 | XCTAssertEqual(input, "Hello, world!"[...])
51 | }
52 |
53 | func testTrailingWhitespace() {
54 | XCTAssertThrowsError(try Int.parser().parse("123 ")) { error in
55 | XCTAssertEqual(
56 | """
57 | error: unexpected input
58 | --> input:1:4
59 | 1 | 123␣␣␣
60 | | ^ expected end of input
61 | """,
62 | "\(error)"
63 | )
64 | }
65 | }
66 |
67 | // func testFoo() throws {
68 | // let quotedField = Parse {
69 | // "\""
70 | // Prefix { $0 != "\"" }
71 | // "\""
72 | // }
73 | // let field = OneOf {
74 | // quotedField
75 | // Prefix { $0 != "," }
76 | // }
77 | // .map(String.init)
78 | //
79 | // try field.print("Blob, Esq.")
80 | // }
81 | }
82 |
--------------------------------------------------------------------------------
/Sources/Parsing/Documentation.docc/Articles/Parsers/Bool.md:
--------------------------------------------------------------------------------
1 | # Bool
2 |
3 | A parser that consumes a Boolean value from the beginning of a string.
4 |
5 | This parser only recognizes the literal `"true"` and `"false"` sequence of characters:
6 |
7 | ```swift
8 | // Parses "true":
9 | var input = "true Hello"[...]
10 | try Bool.parser().parse(&input) // true
11 | input // " Hello"
12 |
13 | // Parses "false":
14 | input = "false Hello"[...]
15 | try Bool.parser().parse(&input) // false
16 | input // " Hello"
17 |
18 | // Otherwise fails:
19 | input = "1 Hello"[...]
20 | try Bool.parser().parse(&input)
21 |
22 | // error: unexpected input
23 | // --> input:1:1
24 | // 1 | 1 Hello
25 | // ^ expected "true" or "false"
26 | ```
27 |
28 | The `Bool.parser()` method is overloaded to work on a variety of string representations in order
29 | to be as efficient as possible, including `Substring`, `UTF8View`, and more general collections of
30 | UTF-8 code units (see for more info).
31 |
32 | Typically Swift can choose the correct overload by using type inference based on what other parsers
33 | you are combining `Bool.parser()` with. For example, if you use `Bool.parser()` with a
34 | `Substring` parser, say the literal `","` parser (see for more information), Swift
35 | will choose the overload that works on substrings:
36 |
37 | ```swift
38 | let parser = Parse {
39 | Bool.parser()
40 | ","
41 | Bool.parser()
42 | }
43 |
44 | try parser.parse("true,false") // (true, false)
45 | ```
46 |
47 | On the other hand, if `Bool.parser()` is used in a context where the input type cannot be inferred,
48 | then you will get an compiler error:
49 |
50 | ```swift
51 | let parser = Parse {
52 | Bool.parser()
53 | Bool.parser() // 🛑 Ambiguous use of 'parser(of:)'
54 | }
55 |
56 | try parser.parse("truefalse")
57 | ```
58 |
59 | To fix this you can force one of the boolean parsers to be the `Substring` parser, and then the
60 | other will figure it out via type inference:
61 |
62 | ```swift
63 | let parser = Parse {
64 | Bool.parser(of: Substring.self)
65 | Bool.parser() // ✅
66 | }
67 |
68 | try parser.parse("truefalse")
69 | ```
70 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/Lazy.swift:
--------------------------------------------------------------------------------
1 | /// A parser that waits for a call to its ``parse(_:)`` method before running the given closure to
2 | /// create a parser for the given input.
3 | @available(
4 | iOS,
5 | deprecated: 9999,
6 | message: """
7 | Lazily evaluate a parser by specifying it in a computed 'Parser.body' property, instead.
8 | """
9 | )
10 | @available(
11 | macOS,
12 | deprecated: 9999,
13 | message: """
14 | Lazily evaluate a parser by specifying it in a computed 'Parser.body' property, instead.
15 | """
16 | )
17 | @available(
18 | tvOS,
19 | deprecated: 9999,
20 | message: """
21 | Lazily evaluate a parser by specifying it in a computed 'Parser.body' property, instead.
22 | """
23 | )
24 | @available(
25 | watchOS,
26 | deprecated: 9999,
27 | message: """
28 | Lazily evaluate a parser by specifying it in a computed 'Parser.body' property, instead.
29 | """
30 | )
31 | public final class Lazy: Parser where Input == LazyParser.Input {
32 | @usableFromInline
33 | internal var lazyParser: LazyParser?
34 |
35 | public let createParser: () -> LazyParser
36 |
37 | @inlinable
38 | public init(@ParserBuilder createParser: @escaping () -> LazyParser) {
39 | self.createParser = createParser
40 | }
41 |
42 | @inlinable
43 | public func parse(_ input: inout LazyParser.Input) rethrows -> LazyParser.Output {
44 | guard let parser = self.lazyParser else {
45 | let parser = self.createParser()
46 | self.lazyParser = parser
47 | return try parser.parse(&input)
48 | }
49 | return try parser.parse(&input)
50 | }
51 | }
52 |
53 | extension Lazy: ParserPrinter where LazyParser: ParserPrinter {
54 | @inlinable
55 | public func print(_ output: LazyParser.Output, into input: inout LazyParser.Input) rethrows {
56 | guard let parser = self.lazyParser else {
57 | let parser = self.createParser()
58 | self.lazyParser = parser
59 | try parser.print(output, into: &input)
60 | return
61 | }
62 | try parser.print(output, into: &input)
63 | }
64 | }
65 |
66 | extension Parsers {
67 | public typealias Lazy = Parsing.Lazy // NB: Convenience type alias for discovery
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/Parsing/Documentation.docc/Articles/Parsers/Float.md:
--------------------------------------------------------------------------------
1 | # Float
2 |
3 | A parser that consumes a floating-point number from the beginning of a string.
4 |
5 | Supports any type that conforms to `BinaryFloatingPoint` and `LosslessStringConvertible`. This
6 | includes `Double`, `Float`, `Float16`, and `Float80`.
7 |
8 | Parses the same format parsed by `LosslessStringConvertible.init(_:)` on `BinaryFloatingPoint`.
9 |
10 | ```swift
11 | var input = "123.45 Hello world"[...]
12 | try Double.parser().parse(&input) // 123.45
13 | input // " Hello world"
14 |
15 | input = "-123. Hello world"[...]
16 | try Double.parser().parse(&input) // -123.0
17 | input // " Hello world"
18 |
19 |
20 | input = "123.123E+2 Hello world"[...]
21 | try Double.parser().parse(&input) // 12312.3
22 | input // " Hello world"
23 | ```
24 |
25 | The `parser()` static method is overloaded to work on a variety of string representations in order
26 | to be as efficient as possible, including `Substring`, `UTF8View`, and generally collections of
27 | UTF-8 code units (see for more info).
28 |
29 | Typically Swift can choose the correct overload by using type inference based on what other parsers
30 | you are combining `parser()` with. For example, if you use `Double.parser()` with a `Substring`
31 | parser, say the literal `","` parser (see for more information), Swift will choose the
32 | overload that works on substrings:
33 |
34 | ```swift
35 | let parser = Parse {
36 | Double.parser()
37 | ","
38 | Double.parser()
39 | }
40 |
41 | try parser.parse("1,-2") // (1.0, -2.0)
42 | ```
43 |
44 | On the other hand, if `Double.parser()` is used in a context where the input type cannot be
45 | inferred, then you will get an compiler error:
46 |
47 | ```swift
48 | let parser = Parse {
49 | Double.parser()
50 | Double.parser() // 🛑 Ambiguous use of 'parser(of:)'
51 | }
52 |
53 | try parser.parse(".1.2")
54 | ```
55 |
56 | To fix this you can force one of the double parsers to be the `Substring` parser, and then the
57 | other will figure it out via type inference:
58 |
59 | ```swift
60 | let parser = Parse {
61 | Double.parser(of: Substring.self)
62 | Double.parser() // ✅
63 | }
64 |
65 | try parser.parse(".1.2") // (0.1, 0.2)
66 | ```
67 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/Peek.swift:
--------------------------------------------------------------------------------
1 | /// A parser that runs the given parser, but does not consume any input.
2 | ///
3 | /// It lets the upstream parser "peek" into the input without consuming it.
4 | ///
5 | /// For example, identifiers (variables, functions, etc.) in Swift allow the first character to be a
6 | /// letter or underscore, but not a digit, but subsequent characters can be digits. _E.g._, `foo123`
7 | /// is a valid identifier, but `123foo` is not. We can create an identifier parser by using `Peek`
8 | /// to first check if the input starts with a letter or underscore, and if it does, return the
9 | /// remainder of the input up to the first character that is not a letter, a digit, or an
10 | /// underscore.
11 | ///
12 | /// ```swift
13 | /// let identifier = Parse {
14 | /// Peek { Prefix(1) { $0.isLetter || $0 == "_" } }
15 | /// Prefix { $0.isNumber || $0.isLetter || $0 == "_" }
16 | /// }
17 | ///
18 | /// try identifier.parse("foo123") // ✅ "foo123"
19 | /// try identifier.parse("_foo123") // ✅ "_foo123"
20 | /// try identifier.parse("1_foo123") // ❌
21 | /// // error: unexpected input
22 | /// // --> input:1:1
23 | /// // 1 | 1_foo123
24 | /// // | ^ expected 1 element satisfying predicate
25 | /// ```
26 | public struct Peek: ParserPrinter where Upstream.Input == Input {
27 | public let upstream: Upstream
28 |
29 | /// Construct a parser that runs the given parser, but does not consume any input.
30 | ///
31 | /// - Parameter build: A parser this parser wants to inspect.
32 | @inlinable
33 | public init(@ParserBuilder _ build: () -> Upstream) {
34 | self.upstream = build()
35 | }
36 |
37 | @inlinable
38 | public func parse(_ input: inout Upstream.Input) rethrows {
39 | let original = input
40 | _ = try self.upstream.parse(&input)
41 | input = original
42 | }
43 |
44 | @inlinable
45 | public func print(_ output: (), into input: inout Upstream.Input) throws {
46 | do {
47 | var i = input
48 | _ = try self.upstream.parse(&i)
49 | } catch {
50 | throw PrintingError.failed(
51 | summary: """
52 | round-trip expectation failed
53 |
54 | A "Peek" parser-printer was handed a value to print that it would have failed to parse.
55 | """,
56 | input: input
57 | )
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinter.swift:
--------------------------------------------------------------------------------
1 | /// A ``Parser`` that can incrementally "print" an output value back into an input.
2 | ///
3 | /// > Note: Printing is the reverse operation of parsing, so the `Input` is essentially built up
4 | /// > in reverse. As such, new values should be prepended to the front of the input. This allows
5 | /// > parsers to check that the already-printed values match what is expected for any given
6 | /// > ``Parser``.
7 | @rethrows public protocol ParserPrinter: Parser {
8 | /// Attempts to print a well-structured piece of data into something more nebulous.
9 | ///
10 | /// - Parameters
11 | /// - output: A well-structured value to be printed to the given input.
12 | /// - input: A nebulous, mutable piece of data to be incrementally printed into.
13 | func print(_ output: Output, into input: inout Input) throws
14 | }
15 |
16 | extension ParserPrinter where Body: ParserPrinter, Body.Input == Input, Body.Output == Output {
17 | // NB: This can't be `rethrows` do to a bug that swallows `throws` even when it's needed.
18 | @inlinable
19 | public func print(_ output: Output, into input: inout Input) throws {
20 | try self.body.print(output, into: &input)
21 | }
22 | }
23 |
24 | extension ParserPrinter where Input: _EmptyInitializable {
25 | /// Attempts to print a well-structured piece of data to something more nebulous.
26 | ///
27 | /// - Parameter output: A well-structured piece of data to be printed.
28 | /// - Returns: A more nebulous value printed from the given output.
29 | @inlinable
30 | public func print(_ output: Output) rethrows -> Input {
31 | var input = Input()
32 | try self.print(output, into: &input)
33 | return input
34 | }
35 | }
36 |
37 | extension ParserPrinter where Output == Void {
38 | /// Attempts to print into a nebulous piece of data.
39 | ///
40 | /// - Parameter input: A nebulous, mutable piece of data to be incrementally printed into.
41 | @inlinable
42 | public func print(into input: inout Input) rethrows {
43 | try self.print((), into: &input)
44 | }
45 | }
46 |
47 | extension ParserPrinter where Input: _EmptyInitializable, Output == Void {
48 | /// Attempts to print a nebulous piece of data.
49 | ///
50 | /// - Returns: A nebulous value.
51 | @inlinable
52 | public func print() rethrows -> Input {
53 | try self.print(())
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/OneOfBuilderTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class OneOfBuilderTests: XCTestCase {
5 | func testBuildArray() {
6 | enum Role: String, CaseIterable, Equatable {
7 | case admin
8 | case guest
9 | case member
10 | }
11 |
12 | let parser = OneOf {
13 | for role in Role.allCases {
14 | role.rawValue.map { role }
15 | }
16 | }
17 |
18 | for role in Role.allCases {
19 | XCTAssertEqual(role, try parser.parse(role.rawValue))
20 | }
21 | XCTAssertThrowsError(try parser.parse("president")) { error in
22 | XCTAssertEqual(
23 | """
24 | error: unexpected input
25 | --> input:1:1
26 | 1 | president
27 | | ^ expected "admin"
28 | | ^ expected "guest"
29 | | ^ expected "member"
30 | """,
31 | "\(error)"
32 | )
33 | }
34 | }
35 |
36 | func testBuildIf() {
37 | enum Role {
38 | case admin
39 | case guest
40 | case member
41 | }
42 |
43 | var parseAdmins = false
44 | var parser = OneOf {
45 | if parseAdmins {
46 | "admin".map { Role.admin }
47 | }
48 |
49 | "guest".map { Role.guest }
50 | "member".map { Role.member }
51 | }
52 |
53 | XCTAssertEqual(.guest, try parser.parse("guest"))
54 | XCTAssertThrowsError(try parser.parse("admin")) { error in
55 | XCTAssertEqual(
56 | """
57 | error: unexpected input
58 | --> input:1:1
59 | 1 | admin
60 | | ^ expected "guest"
61 | | ^ expected "member"
62 | """,
63 | "\(error)"
64 | )
65 | }
66 |
67 | parseAdmins = true
68 | parser = OneOf {
69 | if parseAdmins {
70 | "admin".map { Role.admin }
71 | }
72 |
73 | "guest".map { Role.guest }
74 | "member".map { Role.member }
75 | }
76 | XCTAssertEqual(.guest, try parser.parse("guest"))
77 | XCTAssertEqual(.admin, try parser.parse("admin"))
78 | XCTAssertThrowsError(try parser.parse("president")) { error in
79 | XCTAssertEqual(
80 | """
81 | error: unexpected input
82 | --> input:1:1
83 | 1 | president
84 | | ^ expected "admin"
85 | | ^ expected "guest"
86 | | ^ expected "member"
87 | """,
88 | "\(error)"
89 | )
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/UTF8Tests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class UTF8Tests: XCTestCase {
5 | func testSubstringNormalization() {
6 | var input = "\u{00E9}e\u{0301}e\u{0341} Hello, world"[...].utf8
7 | let parser = From(.substring) { "é" }
8 | XCTAssertNoThrow(try parser.parse(&input))
9 | XCTAssertEqual("e\u{0301}e\u{0341} Hello, world", Substring(input))
10 | XCTAssertNoThrow(try parser.parse(&input))
11 | XCTAssertEqual("e\u{0341} Hello, world", Substring(input))
12 | XCTAssertNoThrow(try parser.parse(&input))
13 | XCTAssertEqual(" Hello, world", Substring(input))
14 | XCTAssertThrowsError(try parser.parse(&input)) { error in
15 | XCTAssertEqual(
16 | """
17 | error: unexpected input
18 | --> input:1:4
19 | 1 | ééé Hello, world
20 | | ^ expected "é"
21 | """,
22 | "\(error)"
23 | )
24 | }
25 | XCTAssertEqual(" Hello, world", Substring(input))
26 | }
27 |
28 | func testUnicodeScalars() {
29 | var input = "🇺🇸 Hello, world"[...].unicodeScalars
30 | let parser = "🇺".unicodeScalars
31 | XCTAssertNoThrow(try parser.parse(&input))
32 | XCTAssertEqual("🇸 Hello, world", Substring(input))
33 | XCTAssertThrowsError(try parser.parse(&input)) { error in
34 | XCTAssertEqual(
35 | """
36 | error: unexpected input
37 | --> input:1:1
38 | 1 | 🇺🇸 Hello, world
39 | | ^ expected "🇺"
40 | """,
41 | "\(error)"
42 | )
43 | }
44 | XCTAssertEqual("🇸 Hello, world", Substring(input))
45 | }
46 |
47 | func testUTF8Normalization() {
48 | enum City {
49 | case losAngeles
50 | case newYork
51 | case sanJose
52 | }
53 |
54 | let city = OneOf {
55 | "Los Angeles".utf8.map { City.losAngeles }
56 | "New York".utf8.map { City.newYork }
57 | "San Jos\u{00E9}".utf8.map { City.sanJose }
58 | }
59 |
60 | XCTAssertThrowsError(try city.parse("San Jose\u{0301}")) { error in
61 | XCTAssertEqual(
62 | """
63 | error: unexpected input
64 | --> input:1:1
65 | 1 | San José
66 | | ^ expected "Los Angeles"
67 | | ^ expected "New York"
68 | | ^ expected "San José"
69 | """,
70 | "\(error)"
71 | )
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Sources/Parsing/Conversions/LosslessStringConvertible.swift:
--------------------------------------------------------------------------------
1 | extension Conversion {
2 | /// A conversion from a string to a lossless string-convertible type.
3 | ///
4 | /// See ``lossless(_:)-swift.method`` for a fluent version of this interface that transforms an
5 | /// existing conversion.
6 | ///
7 | /// - Parameter type: A type that conforms to `LosslessStringConvertible`.
8 | /// - Returns: A conversion from a string to the given type.
9 | @inlinable
10 | public static func lossless(
11 | _ type: NewOutput.Type
12 | ) -> Self where Self == Conversions.FromLosslessString {
13 | .init()
14 | }
15 |
16 | /// Transforms this conversion to a string into a conversion to the given lossless
17 | /// string-convertible type.
18 | ///
19 | /// A fluent version of ``Conversion/lossless(_:)-swift.type.method``. Equivalent to calling
20 | /// ``map(_:)`` with ``Conversion/lossless(_:)-swift.type.method``:
21 | ///
22 | /// ```swift
23 | /// stringConversion.lossless(NewOutput.self)
24 | /// // =
25 | /// stringConversion.map(.lossless(NewOutput.self))
26 | /// ```
27 | ///
28 | /// - Parameter type: A type that conforms to `LosslessStringConvertible`.
29 | /// - Returns: A conversion from a string to the given type.
30 | @inlinable
31 | public func lossless(
32 | _ type: NewOutput.Type
33 | ) -> Conversions.Map> {
34 | self.map(.lossless(NewOutput.self))
35 | }
36 | }
37 |
38 | extension Conversions {
39 | /// A conversion from a string to a lossless string-convertible type.
40 | ///
41 | /// You will not typically need to interact with this type directly. Instead you will usually use
42 | /// the ``Conversion/lossless(_:)-swift.type.method`` operation, which constructs this type.
43 | public struct FromLosslessString: Conversion {
44 | @usableFromInline
45 | init() {}
46 |
47 | @inlinable
48 | public func apply(_ input: String) throws -> Output {
49 | guard let output = Output(input)
50 | else {
51 | throw ConvertingError(
52 | """
53 | lossless: Failed to convert \(input.debugDescription) to \(Output.self).
54 | """
55 | )
56 | }
57 |
58 | return output
59 | }
60 |
61 | @inlinable
62 | public func unapply(_ output: Output) -> String {
63 | output.description
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/Fail.swift:
--------------------------------------------------------------------------------
1 | /// A parser that always fails, no matter the input.
2 | ///
3 | /// While not very useful on its own, this parser can be helpful when combined with other parsers or
4 | /// operators.
5 | ///
6 | /// For example, it can be used to conditionally causing parsing to fail when used with
7 | /// ``Parser/flatMap(_:)``:
8 | ///
9 | /// ```swift
10 | /// struct OddFailure: Error {}
11 | ///
12 | /// let evens = Int.parser().flatMap {
13 | /// if $0.isMultiple(of: 2) {
14 | /// Always($0)
15 | /// } else {
16 | /// Fail(throwing: OddFailure())
17 | /// }
18 | /// }
19 | ///
20 | /// try evens.parse("42") // 42
21 | ///
22 | /// try evens.parse("123")
23 | /// // error: OddFailure()
24 | /// // --> input:1:1-3
25 | /// // 1 | 123
26 | /// // | ^^^
27 | /// ```
28 | public struct Fail: ParserPrinter {
29 | @usableFromInline
30 | let error: Error
31 |
32 | /// Creates a parser that throws an error when it runs.
33 | ///
34 | /// - Parameter error: An error to throw when the parser is run.
35 | @inlinable
36 | public init(throwing error: Error) {
37 | self.error = error
38 | }
39 |
40 | @inlinable
41 | public func parse(_ input: inout Input) throws -> Output {
42 | switch self.error {
43 | case is ParsingError:
44 | throw self.error
45 | default:
46 | throw ParsingError.wrap(self.error, at: input)
47 | }
48 | }
49 |
50 | @inlinable
51 | public func print(_ output: Output, into input: inout Input) throws {
52 | switch self.error {
53 | case is PrintingError:
54 | throw self.error
55 | default:
56 | throw PrintingError.failed(
57 | summary: """
58 | \(self.error)
59 |
60 | A failing parser-printer attempted to print:
61 |
62 | \(output)
63 | """,
64 | input: input
65 | )
66 | }
67 | }
68 | }
69 |
70 | extension Fail {
71 | /// Creates a parser that throws an error when it runs.
72 | @inlinable
73 | public init() {
74 | self.init(throwing: DefaultError())
75 | }
76 |
77 | @usableFromInline
78 | struct DefaultError: Error, CustomDebugStringConvertible {
79 | @usableFromInline
80 | init() {}
81 |
82 | @usableFromInline
83 | var debugDescription: String {
84 | "failed"
85 | }
86 | }
87 | }
88 |
89 | extension Parsers {
90 | public typealias Fail = Parsing.Fail // NB: Convenience type alias for discovery
91 | }
92 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/NotTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | class NotTests: XCTestCase {
5 | func testNotUpstreamFails() {
6 | var input = """
7 | let foo = true
8 | let bar = false
9 | """[...]
10 |
11 | let uncommentedLine = Parse {
12 | Not { "//" }
13 | Prefix { $0 != "\n" }
14 | OneOf {
15 | "\n"
16 | End()
17 | }
18 | }
19 |
20 | XCTAssertEqual(try uncommentedLine.parse(&input), "let foo = true")
21 | XCTAssertEqual(input, "let bar = false")
22 | }
23 |
24 | func testNotUpstreamParses() {
25 | var input = """
26 | // a comment
27 | let foo = true
28 | """[...]
29 |
30 | let uncommentedLine = Parse {
31 | Not { "//" }
32 | Prefix { $0 != "\n" }
33 | }
34 |
35 | XCTAssertThrowsError(try uncommentedLine.parse(&input)) { error in
36 | XCTAssertEqual(
37 | """
38 | error: unexpected input
39 | --> input:1:1-2
40 | 1 | // a comment
41 | | ^^ expected not to be processed
42 | """,
43 | "\(error)"
44 | )
45 | }
46 | XCTAssertEqual(
47 | input,
48 | """
49 | a comment
50 | let foo = true
51 | """
52 | )
53 | }
54 |
55 | func testPrintUpstreamFails() {
56 | var input = "not a comment"[...]
57 | let parser = Not { "//" }
58 | XCTAssertNoThrow(try parser.print((), into: &input))
59 | XCTAssertEqual(input, "not a comment"[...])
60 | }
61 |
62 | func testPrintUpstreamParses() {
63 | var input = "// a comment"[...]
64 | let parser = Not { "//" }
65 | XCTAssertThrowsError(try parser.print((), into: &input))
66 | XCTAssertEqual(input, " a comment"[...])
67 | }
68 |
69 | func testPrintComplexParserSucceeds() {
70 | var input = ""[...]
71 |
72 | let uncommentedLine = Parse {
73 | Not { "//" }
74 | Rest()
75 | }
76 |
77 | XCTAssertNoThrow(try uncommentedLine.print("uncommented line"[...], into: &input))
78 | XCTAssertEqual(
79 | input,
80 | "uncommented line"
81 | )
82 | }
83 |
84 | func testPrintComplexParserFails() {
85 | var input = ""[...]
86 |
87 | let uncommentedLine = Parse {
88 | Not { "//" }
89 | Rest()
90 | }
91 |
92 | XCTAssertThrowsError(try uncommentedLine.print("// commented line"[...], into: &input))
93 | XCTAssertEqual(
94 | input,
95 | " commented line"
96 | )
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Sources/Parsing/Parsers/AnyParser.swift:
--------------------------------------------------------------------------------
1 | extension Parser {
2 | /// Wraps this parser with a type eraser.
3 | ///
4 | /// This form of _type erasure_ preserves abstraction across API boundaries, such as different
5 | /// modules.
6 | ///
7 | /// When you expose your composed parsers as the ``AnyParser`` type, you can change the underlying
8 | /// implementation over time without affecting existing clients.
9 | ///
10 | /// Equivalent to passing `self` to `AnyParser.init`.
11 | ///
12 | /// - Returns: An ``AnyParser`` wrapping this parser.
13 | @inlinable
14 | public func eraseToAnyParser() -> AnyParser {
15 | .init(self)
16 | }
17 | }
18 |
19 | /// A type-erased parser of `Output` from `Input`.
20 | ///
21 | /// This parser forwards its ``parse(_:)`` method to an arbitrary underlying parser having the same
22 | /// `Input` and `Output` types, hiding the specifics of the underlying ``Parser``.
23 | ///
24 | /// Use ``AnyParser`` to wrap a parser whose type has details you don't want to expose across API
25 | /// boundaries, such as different modules. When you use type erasure this way, you can change the
26 | /// underlying parser over time without affecting existing clients.
27 | public struct AnyParser: Parser {
28 | @usableFromInline
29 | let parser: (inout Input) throws -> Output
30 |
31 | /// Creates a type-erasing parser to wrap the given parser.
32 | ///
33 | /// Equivalent to calling ``Parser/eraseToAnyParser()`` on the parser.
34 | ///
35 | /// - Parameter parser: A parser to wrap with a type eraser.
36 | @inlinable
37 | public init(_ parser: P) where P.Input == Input, P.Output == Output {
38 | self.init(parser.parse)
39 | }
40 |
41 | /// Creates a parser that wraps the given closure in its ``parse(_:)`` method.
42 | ///
43 | /// - Parameter parse: A closure that attempts to parse an output from an input. `parse` is
44 | /// executed each time the ``parse(_:)`` method is called on the resulting parser.
45 | @inlinable
46 | public init(_ parse: @escaping (inout Input) throws -> Output) {
47 | self.parser = parse
48 | }
49 |
50 | @inlinable
51 | public func parse(_ input: inout Input) throws -> Output {
52 | try self.parser(&input)
53 | }
54 |
55 | @inlinable
56 | public func eraseToAnyParser() -> Self {
57 | self
58 | }
59 | }
60 |
61 | extension Parsers {
62 | public typealias AnyParser = Parsing.AnyParser // NB: Convenience type alias for discovery
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/Parsing/Documentation.docc/Articles/Parsers/CaseIterable.md:
--------------------------------------------------------------------------------
1 | # CaseIterable
2 |
3 | A parser-printer that consumes a case-iterable, raw representable value from the beginning of a
4 | string.
5 |
6 | Given a type that conforms to `CaseIterable` and `RawRepresentable` with a `RawValue` of `String`
7 | or `Int`, we can incrementally parse a value of it.
8 |
9 | Notably, raw enumerations that conform to `CaseIterable` meet this criteria, so cases of the
10 | following type can be parsed with no extra work:
11 |
12 | ```swift
13 | enum Role: String, CaseIterable {
14 | case admin
15 | case guest
16 | case member
17 | }
18 |
19 | let parser = ParsePrint {
20 | Int.parser()
21 | ","
22 | Role.parser()
23 | }
24 | try parser.parse("123,member") // (123, .member)
25 | try parser.print((123, .member)) // "123,member"
26 | ```
27 |
28 | This also works with raw enumerations that are backed by integers:
29 |
30 | ```swift
31 | enum Role: Int, CaseIterable {
32 | case admin = 1
33 | case guest = 2
34 | case member = 3
35 | }
36 |
37 | let parser = ParsePrint {
38 | Int.parser()
39 | ","
40 | Role.parser()
41 | }
42 | try parser.print((123, .member)) // "123,member"
43 | try parser.parse("123,1") // (123, .admin)
44 | ```
45 |
46 | The `parser()` method on `CaseIterable` is overloaded to work on a variety of string representations
47 | in order to be as efficient as possible, including `Substring`, `UTF8View`, and more general
48 | collections of UTF-8 code units (see for more info).
49 |
50 | Typically Swift can choose the correct overload by using type inference based on what other parsers
51 | you are combining `parser()` with. For example, if you use `Role.parser()` with a
52 | `Substring` parser, like the literal "," parser in the above examples, Swift
53 | will choose the overload that works on substrings.
54 |
55 | On the other hand, if `Role.parser()` is used in a context where the input type cannot be inferred,
56 | then you will get an compiler error:
57 |
58 | ```swift
59 | let parser = Parse {
60 | Int.parser()
61 | Role.parser() // 🛑 Ambiguous use of 'parser(of:)'
62 | }
63 |
64 | try parser.parse("123member")
65 | ```
66 |
67 | To fix this you can force one of the parsers to be the `Substring` parser, and then the
68 | other will figure it out via type inference:
69 |
70 | ```swift
71 | let parser = Parse {
72 | Int.parser(of: Substring.self)
73 | Role.parser()
74 | }
75 |
76 | try parser.parse("123member") // (123, .member)
77 | ```
78 |
--------------------------------------------------------------------------------
/Sources/Parsing/Conversions/ConversionMap.swift:
--------------------------------------------------------------------------------
1 | extension Conversion {
2 | /// Returns a conversion that transforms the output of this conversion with a given downstream
3 | /// conversion.
4 | ///
5 | /// When provided with a conversion from this conversion's output type to some new output type,
6 | /// this method can return a new conversion from this conversion's input type to the given
7 | /// conversion's output type by calling their ``apply(_:)`` functions and ``unapply(_:)``
8 | /// functions one after the other.
9 | ///
10 | /// This method is similar to `Sequence.map`, `Optional.map`, and `Result.map` in the Swift
11 | /// standard library, as well as `Publisher.map` in the Combine framework. This method is also
12 | /// similar to the `map` functions on ``Parser`` and ``ParserPrinter``, especially
13 | /// ``Parser/map(_:)-18m9d``, which takes a conversion.
14 | ///
15 | /// - Parameter downstream: A conversion that transforms the output of this conversion into some
16 | /// new output.
17 | /// - Returns: A conversion that transforms the input of this conversion into the output of the
18 | /// given conversion.
19 | @inlinable
20 | public func map(_ downstream: C) -> Conversions.Map {
21 | .init(upstream: self, downstream: downstream)
22 | }
23 | }
24 |
25 | extension Conversions {
26 | /// A conversion that composes two conversions together by composing their
27 | /// ``Conversion/apply(_:)`` functions and ``Conversion/unapply(_:)`` functions together.
28 | ///
29 | /// You will not typically need to interact with this type directly. Instead you will usually use
30 | /// the ``Conversion/map(_:)`` operation, which constructs this type.
31 | public struct Map: Conversion
32 | where Upstream.Output == Downstream.Input {
33 | public let upstream: Upstream
34 | public let downstream: Downstream
35 |
36 | @usableFromInline
37 | init(upstream: Upstream, downstream: Downstream) {
38 | self.upstream = upstream
39 | self.downstream = downstream
40 | }
41 |
42 | @inlinable
43 | @inline(__always)
44 | public func apply(_ input: Upstream.Input) rethrows -> Downstream.Output {
45 | try self.downstream.apply(self.upstream.apply(input))
46 | }
47 |
48 | @inlinable
49 | @inline(__always)
50 | public func unapply(_ output: Downstream.Output) rethrows -> Upstream.Input {
51 | try self.upstream.unapply(self.downstream.unapply(output))
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/AnyParserTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class AnyParserTests: XCTestCase {
5 | func testClosureInitializer() {
6 | struct CustomError: Equatable, Error {}
7 |
8 | let parser = AnyParser { input in
9 | guard input.starts(with: "Hello") else {
10 | throw CustomError()
11 | }
12 | input.removeFirst(5)
13 | }
14 |
15 | var input = "Hello, world!"[...]
16 | XCTAssertNoThrow(try parser.parse(&input))
17 | XCTAssertEqual(", world!", input)
18 |
19 | XCTAssertThrowsError(try parser.parse(&input)) { error in
20 | XCTAssertEqual(error as? CustomError, CustomError())
21 | }
22 | XCTAssertEqual(", world!", input)
23 | }
24 |
25 | func testParserInitializer() {
26 | let parser = AnyParser("Hello")
27 |
28 | var input = "Hello, world!"[...]
29 | XCTAssertNoThrow(try parser.parse(&input))
30 | XCTAssertEqual(", world!", input)
31 |
32 | XCTAssertThrowsError(try parser.parse(&input)) { error in
33 | XCTAssertEqual(
34 | """
35 | error: unexpected input
36 | --> input:1:6
37 | 1 | Hello, world!
38 | | ^ expected "Hello"
39 | """,
40 | "\(error)"
41 | )
42 | }
43 | XCTAssertEqual(", world!", input)
44 | }
45 |
46 | func testParserEraseToAnyParser() {
47 | let parser = "Hello".eraseToAnyParser()
48 |
49 | var input = "Hello, world!"[...]
50 | XCTAssertNoThrow(try parser.parse(&input))
51 | XCTAssertEqual(", world!", input)
52 |
53 | XCTAssertThrowsError(try parser.parse(&input)) { error in
54 | XCTAssertEqual(
55 | """
56 | error: unexpected input
57 | --> input:1:6
58 | 1 | Hello, world!
59 | | ^ expected "Hello"
60 | """,
61 | "\(error)"
62 | )
63 | }
64 | XCTAssertEqual(", world!", input)
65 | }
66 |
67 | func testAnyParserEraseToAnyParser() {
68 | let parser = "Hello".eraseToAnyParser().eraseToAnyParser()
69 |
70 | var input = "Hello, world!"[...]
71 | XCTAssertNoThrow(try parser.parse(&input))
72 | XCTAssertEqual(", world!", input)
73 |
74 | XCTAssertThrowsError(try parser.parse(&input)) { error in
75 | XCTAssertEqual(
76 | """
77 | error: unexpected input
78 | --> input:1:6
79 | 1 | Hello, world!
80 | | ^ expected "Hello"
81 | """,
82 | "\(error)"
83 | )
84 | }
85 | XCTAssertEqual(", world!", input)
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/Rest.swift:
--------------------------------------------------------------------------------
1 | /// A parser that consumes everything to the end of the collection and returns this subsequence as
2 | /// its output.
3 | ///
4 | /// ```swift
5 | /// var input = "Hello"[...]
6 | /// Rest().parse(&input) // "Hello"
7 | /// input // ""
8 | /// ```
9 | ///
10 | /// This parser fails if there is no input to consume:
11 | ///
12 | /// ```swift
13 | /// try Rest().parse("")
14 | ///
15 | /// /// error: unexpected input
16 | /// /// --> input:1:1
17 | /// /// 1 |
18 | /// /// | ^ expected a non-empty input
19 | /// ```
20 | ///
21 | /// If you want to allow for the possibility of an empty remaining input you can use the
22 | /// ``Optionally`` parser to parse an optional output value, or the ``replaceError(with:)`` method
23 | /// to coalesce the error into a default output value.
24 | public struct Rest: Parser where Input.SubSequence == Input {
25 | @inlinable
26 | public init() {}
27 |
28 | @inlinable
29 | public func parse(_ input: inout Input) throws -> Input {
30 | guard !input.isEmpty
31 | else { throw ParsingError.expectedInput("a non-empty input", at: input) }
32 | let output = input
33 | input.removeFirst(input.count)
34 | return output
35 | }
36 | }
37 |
38 | extension Rest: ParserPrinter where Input: PrependableCollection {
39 | @inlinable
40 | public func print(_ output: Input, into input: inout Input) throws {
41 | guard input.isEmpty
42 | else {
43 | let description = describe(input).map { "\n\n\($0.debugDescription)" } ?? ""
44 | throw PrintingError.failed(
45 | summary: """
46 | round-trip expectation failed
47 |
48 | A "Rest" parser-printer expected to have printed all remaining input, but more was \
49 | printed.\(description)
50 |
51 | During a round-trip, the "Rest" parser-printer would have parsed this remaining input.
52 | """,
53 | input: input
54 | )
55 | }
56 |
57 | guard !output.isEmpty
58 | else {
59 | throw PrintingError.failed(
60 | summary: """
61 | round-trip expectation failed
62 |
63 | A "Rest" parser-printer attempted to print an empty \(Input.self).
64 |
65 | During a round-trip, the "Rest" parser-printer would have failed to parse an empty input.
66 | """,
67 | input: input
68 | )
69 | }
70 | input.prepend(contentsOf: output)
71 | }
72 | }
73 |
74 | extension Parsers {
75 | public typealias Rest = Parsing.Rest // NB: Convenience type alias for discovery
76 | }
77 |
--------------------------------------------------------------------------------
/Tests/ParsingTests/IntTests.swift:
--------------------------------------------------------------------------------
1 | import Parsing
2 | import XCTest
3 |
4 | final class IntTests: XCTestCase {
5 | func testBasics() {
6 | let parser = Int.parser(of: Substring.UTF8View.self)
7 |
8 | var input = "123 Hello"[...].utf8
9 | XCTAssertEqual(123, try parser.parse(&input))
10 | XCTAssertEqual(" Hello", String(input))
11 |
12 | input = "-123 Hello"[...].utf8
13 | XCTAssertEqual(-123, try parser.parse(&input))
14 | XCTAssertEqual(" Hello", String(input))
15 |
16 | input = "+123 Hello"[...].utf8
17 | XCTAssertEqual(123, try parser.parse(&input))
18 | XCTAssertEqual(" Hello", String(input))
19 |
20 | input = "\(Int.max) Hello"[...].utf8
21 | XCTAssertEqual(Int.max, try parser.parse(&input))
22 | XCTAssertEqual(" Hello", String(input))
23 |
24 | input = "\(Int.min) Hello"[...].utf8
25 | XCTAssertEqual(Int.min, try parser.parse(&input))
26 | XCTAssertEqual(" Hello", String(input))
27 |
28 | input = "Hello"[...].utf8
29 | XCTAssertThrowsError(try parser.parse(&input)) { error in
30 | XCTAssertEqual(
31 | """
32 | error: unexpected input
33 | --> input:1:1
34 | 1 | Hello
35 | | ^ expected integer
36 | """,
37 | "\(error)"
38 | )
39 | }
40 | XCTAssertEqual("Hello", String(input))
41 |
42 | input = "- Hello"[...].utf8
43 | XCTAssertThrowsError(try parser.parse(&input)) { error in
44 | XCTAssertEqual(
45 | """
46 | error: unexpected input
47 | --> input:1:2
48 | 1 | - Hello
49 | | ^ expected integer
50 | """,
51 | "\(error)"
52 | )
53 | }
54 | XCTAssertEqual(" Hello", String(input))
55 |
56 | input = "+ Hello"[...].utf8
57 | XCTAssertThrowsError(try parser.parse(&input)) { error in
58 | XCTAssertEqual(
59 | """
60 | error: unexpected input
61 | --> input:1:2
62 | 1 | + Hello
63 | | ^ expected integer
64 | """,
65 | "\(error)"
66 | )
67 | }
68 | XCTAssertEqual(" Hello", String(input))
69 | }
70 |
71 | func testOverflow() {
72 | var input = "1234 Hello"[...].utf8
73 | XCTAssertThrowsError(try UInt8.parser(of: Substring.UTF8View.self).parse(&input)) { error in
74 | XCTAssertEqual(
75 | """
76 | error: failed to process "UInt8"
77 | --> input:1:1-4
78 | 1 | 1234 Hello
79 | | ^^^^ overflowed 255
80 | """,
81 | "\(error)"
82 | )
83 | }
84 | XCTAssertEqual(" Hello", String(input))
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Sources/Parsing/Parsers/FlatMap.swift:
--------------------------------------------------------------------------------
1 | extension Parser {
2 | /// Returns a parser that transforms the output of this parser into a new parser.
3 | ///
4 | /// This method is similar to `Sequence.flatMap`, `Optional.flatMap`, and `Result.flatMap` in the
5 | /// Swift standard library, as well as `Publisher.flatMap` in the Combine framework.
6 | ///
7 | /// ## Printability
8 | ///
9 | /// `Parser.flatMap` is _not_ printable, as the logic contained inside its transform operation to
10 | /// some new parser is not reversible.
11 | ///
12 | /// If you are building a parser-printer, avoid uses of `flatMap` and instead prefer the use of
13 | /// ``Parser/map(_:)-4hsj5`` with conversions that preserve printability.
14 | ///
15 | /// - Parameter transform: A closure that transforms values of this parser's output and returns a
16 | /// new parser.
17 | /// - Returns: A parser that transforms output from an upstream parser into a new parser.
18 | @inlinable
19 | public func flatMap(
20 | @ParserBuilder _ transform: @escaping (Output) -> NewParser
21 | ) -> Parsers.FlatMap where Self.Input == Input {
22 | .init(upstream: self, transform: transform)
23 | }
24 | }
25 |
26 | extension Parsers {
27 | /// A parser that transforms the output of another parser into a new parser.
28 | ///
29 | /// You will not typically need to interact with this type directly. Instead you will usually use
30 | /// the ``Parser/flatMap(_:)`` operation, which constructs this type.
31 | public struct FlatMap: Parser
32 | where NewParser.Input == Upstream.Input {
33 | public let upstream: Upstream
34 | public let transform: (Upstream.Output) -> NewParser
35 |
36 | @inlinable
37 | public init(upstream: Upstream, transform: @escaping (Upstream.Output) -> NewParser) {
38 | self.upstream = upstream
39 | self.transform = transform
40 | }
41 |
42 | @inlinable
43 | public func parse(_ input: inout Upstream.Input) rethrows -> NewParser.Output {
44 | let original = input
45 | do {
46 | return try self.transform(self.upstream.parse(&input)).parse(&input)
47 | } catch let ParsingError.failed(reason, context) {
48 | throw ParsingError.failed(
49 | reason,
50 | .init(
51 | originalInput: original,
52 | remainingInput: input,
53 | debugDescription: context.debugDescription,
54 | underlyingError: ParsingError.failed(reason, context)
55 | )
56 | )
57 | }
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/Parsing/ParserPrinters/Always.swift:
--------------------------------------------------------------------------------
1 | /// A parser that always succeeds with the given value, and does not consume any input.
2 | ///
3 | /// While not very useful on its own, the `Always` parser can be helpful when combined with other
4 | /// parsers or operators.
5 | ///
6 | /// When its `Output` is `Void`, it can be used as a "no-op" parser of sorts and be plugged into
7 | /// other parser operations. For example, the ``Many`` parser can be configured with separator and
8 | /// terminator parsers:
9 | ///
10 | /// ```swift
11 | /// Many {
12 | /// Int.parser()
13 | /// } separator: {
14 | /// ","
15 | /// } terminator: {
16 | /// End()
17 | /// }
18 | /// ```
19 | ///
20 | /// But also exposes initializers that omit these parsers when there is no separator or terminator
21 | /// to be parsed:
22 | ///
23 | /// ```swift
24 | /// Many {
25 | /// Prefix { $0 != "\n" }
26 | /// "\n"
27 | /// }
28 | /// ```
29 | ///
30 | /// To support this, `Many` plugs `Always` into each omitted parser. As a simplified
31 | /// example:
32 | ///
33 | /// ```swift
34 | /// struct Many: Parser
35 | /// where Separator.Input == Element.Input, Terminator.Input == Element.Input {
36 | /// ...
37 | /// }
38 | ///
39 | /// extension Many where Separator == Always, Terminator == Always {
40 | /// init(@ParserBuilder element: () -> Element) {
41 | /// self.element = element()
42 | /// self.separator = Always(())
43 | /// self.terminator = Always(())
44 | /// }
45 | /// }
46 | /// ```
47 | ///
48 | /// This means the previous example is equivalent to:
49 | ///
50 | /// ```swift
51 | /// Many {
52 | /// Prefix { $0 != "\n" }
53 | /// "\n"
54 | /// } separator: {
55 | /// Always(())
56 | /// } terminator: {
57 | /// Always(())
58 | /// }
59 | /// ```
60 | ///
61 | /// > Note: While `Always` can be used as the last alternative of a ``OneOf`` to specify a default
62 | /// > output, the resulting parser will be throwing. Instead, prefer ``Parser/replaceError(with:)``,
63 | /// > which returns a non-throwing parser.
64 | public struct Always: ParserPrinter {
65 | public let output: Output
66 |
67 | @inlinable
68 | public init(_ output: Output) {
69 | self.output = output
70 | }
71 |
72 | @inlinable
73 | public func parse(_ input: inout Input) -> Output {
74 | self.output
75 | }
76 |
77 | @inlinable
78 | public func print(_ output: Output, into input: inout Input) {}
79 | }
80 |
81 | extension Parsers {
82 | public typealias Always = Parsing.Always // NB: Convenience type alias for discovery
83 | }
84 |
--------------------------------------------------------------------------------
/Sources/Parsing/Documentation.docc/Articles/Parsers/Int.md:
--------------------------------------------------------------------------------
1 | # Int
2 |
3 | A parser that consumes an integer from the beginning of a string.
4 |
5 | Supports any type that conforms to `FixedWidthInteger`. This includes `Int`, `UInt`, `UInt8`, and
6 | many more.
7 |
8 | Parses the same format parsed by `FixedWidthInteger.init(_:radix:)`.
9 |
10 | ```swift
11 | var input = "123 Hello world"[...]
12 | try Int.parser().parse(&input) // 123
13 | input // " Hello world"
14 |
15 | input = "-123 Hello world"
16 | try Int.parser().parse(&input) // -123
17 | input // " Hello world"
18 | ```
19 |
20 | This parser fails when it does not find an integer at the beginning of the collection:
21 |
22 | ```swift
23 | var input = "+Hello"[...]
24 | let number = try Int.parser().parse(&input)
25 | // error: unexpected input
26 | // --> input:1:2
27 | // 1 | +Hello
28 | // | ^ expected integer
29 | ```
30 |
31 | And it fails when the digits extracted from the beginning of the collection would cause the
32 | integer type to overflow:
33 |
34 | ```swift
35 | var input = "9999999999999999999 Hello"[...]
36 | let number = try Int.parser().parse(&input)
37 | // error: failed to process "Int"
38 | // --> input:1:1-19
39 | // 1 | 9999999999999999999 Hello
40 | // | ^^^^^^^^^^^^^^^^^^^ overflowed 9223372036854775807
41 | ```
42 |
43 | The static `parser()` method is overloaded to work on a variety of string representations in order
44 | to be as efficient as possible, including `Substring`, `UTF8View`, and generally collections of
45 | UTF-8 code units (see for more info).
46 |
47 | Typically Swift can choose the correct overload by using type inference based on what other parsers
48 | you are combining `parser()` with. For example, if you use `Int.parser()` with a `Substring` parser,
49 | say the literal `","` parser (see for more information), Swift will choose the overload
50 | that works on substrings:
51 |
52 | ```swift
53 | let parser = Parse {
54 | Int.parser()
55 | ","
56 | Int.parser()
57 | }
58 |
59 | try parser.parse("123,456") // (123, 456)
60 | ```
61 |
62 | On the other hand, if `Int.parser()` is used in a context where the input type cannot be inferred,
63 | then you will get an compiler error:
64 |
65 | ```swift
66 | let parser = Parse {
67 | Int.parser() // 🛑 Ambiguous use of 'parser(of:radix:)'
68 | Bool.parser()
69 | }
70 |
71 | try parser.parse("123true")
72 | ```
73 |
74 | To fix this you can force one of the parsers to be the `Substring` parser, and then the
75 | other will figure it out via type inference:
76 |
77 | ```swift
78 | let parser = Parse {
79 | Int.parser() // ✅
80 | Bool.parser(of: Substring.self)
81 | }
82 |
83 | try parser.parse("123true") // (123, true)
84 | ```
85 |
--------------------------------------------------------------------------------
/Sources/Parsing/Conversions/ParseableFormatStyleConversion.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
2 | import Foundation
3 |
4 | @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *)
5 | extension Conversion {
6 | /// A conversion that wraps a parseable format style.
7 | ///
8 | /// This conversion forwards its ``apply(_:)`` and ``unapply(_:)`` methods to the underlying
9 | /// `ParseableFormatStyle` by invoking its parse strategy's `parse` method and its `format`
10 | /// method.
11 | ///
12 | /// See ``formatted(_:)-swift.method`` for a fluent version of this interface that transforms an
13 | /// existing conversion.
14 | ///
15 | /// - Parameter style: A parseable format style.
16 | /// - Returns: A conversion from a string to the given type.
17 | public static func formatted