├── 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