From 6fa1cfff9ad1e1318c599580628f4f1df9e1fecc Mon Sep 17 00:00:00 2001 From: David Peterson Date: Thu, 21 Mar 2024 22:48:37 +1000 Subject: [PATCH 1/2] Initial implementations of Take and TakeUpTo parsers Along with some test cases. --- Sources/Parsing/ParserPrinters/Take.swift | 80 ++++++++++++++++ Sources/Parsing/ParserPrinters/TakeUpTo.swift | 66 +++++++++++++ Tests/ParsingTests/TakeTests.swift | 94 +++++++++++++++++++ Tests/ParsingTests/TakeUpToTests.swift | 54 +++++++++++ 4 files changed, 294 insertions(+) create mode 100644 Sources/Parsing/ParserPrinters/Take.swift create mode 100644 Sources/Parsing/ParserPrinters/TakeUpTo.swift create mode 100644 Tests/ParsingTests/TakeTests.swift create mode 100644 Tests/ParsingTests/TakeUpToTests.swift diff --git a/Sources/Parsing/ParserPrinters/Take.swift b/Sources/Parsing/ParserPrinters/Take.swift new file mode 100644 index 0000000000..1f709539ac --- /dev/null +++ b/Sources/Parsing/ParserPrinters/Take.swift @@ -0,0 +1,80 @@ +/// A parser that returns input from the until the `terminator` `Parser` +/// matches. This provides a method of "lazy" (as opposed to "greedy") consumption of the input. +/// +/// ```swift +/// enum ParkType { +/// case park +/// case world +/// } +/// +/// let lineParser: some Parser = Take { +/// Prefix(0...).map(.string) +/// } upTo: { +/// OneOf { +/// "Park".map { ParkType.park } +/// "World".map { ParkType.world } +/// } +/// } +/// +/// var input = "Jurrasic World"[...] +/// let parsed = try line.parse(&input) // ("Jurrasic ", .world) +/// ``` +public struct Take: Parser + where Input.SubSequence == Input, Terminator.Input == Input, Taken.Input == Input { + public let taken: Taken + public let terminator: Terminator + + @inlinable + public init( + @ParserBuilder _ taken: () -> Taken, + @ParserBuilder upTo terminator: () -> Terminator + ) { + self.taken = taken() + self.terminator = terminator() + } + + @inlinable + @inline(__always) + public func parse(_ input: inout Input) throws -> (Taken.Output, Terminator.Output) { + let original = input + + var currentIndex = input.startIndex + while currentIndex <= input.endIndex { + let terminatorOutput: Terminator.Output + var takenInput: Input + + do { + var test = input[currentIndex...] + terminatorOutput = try terminator.parse(&test) + input = test + takenInput = original[..: Parser +where Input.SubSequence == Input, Upstream.Input == Input +{ + public let terminator: Upstream + + @inlinable + public init( + _ terminator: Upstream + ) { + self.terminator = terminator + } + + @inlinable + public init( + @ParserBuilder _ terminator: () -> Upstream + ) { + self.terminator = terminator() + } + + @inlinable + @inline(__always) + public func parse(_ input: inout Input) throws -> (Input, Upstream.Output) { + let original = input + + var currentIndex = input.startIndex + while currentIndex <= input.endIndex { + do { + var test = input[currentIndex...] + let result = try terminator.parse(&test) + input = test + return (original[.. = Take { + Int.parser() + } upTo: { + "." + } + + var input = "123.456"[...] + XCTAssertEqual(123, try parser.parse(&input).0) + } + + func testUnterminated() throws { + let parser: some Parser = Take { + Int.parser() + } upTo: { + "." + } + + var input = "123456"[...] + + XCTAssertThrowsError(try parser.parse(&input)) + } + + func testTakeStringAndInt() throws { + let parser: some Parser = Take { + Prefix(0...).map(.string) + } upTo: { + "456".map { 456 } + } + + var input = "123456"[...] + let output = try parser.parse(&input) + XCTAssertEqual("123", output.0) + XCTAssertEqual(456, output.1) + } + + func testTakeUpToEnd() throws { + let parser: some Parser = Take { + Prefix(0...) + } upTo: { + End() + } + + var input = "123"[...] + XCTAssertEqual("123", try parser.parse(&input).0) + XCTAssertEqual("", input) + } + + func testTakeCantParseUpTo() throws { + let parser: some Parser = Take { + Int.parser() + } upTo: { + End() + } + + var input = "123abc"[...] + XCTAssertThrowsError(try parser.parse(&input)) + } + + func testComplextInitMap() throws { + enum Example: Equatable { + case a + case b + } + + struct ComplexType: Equatable { + let string: String + let number: Int + let example: Example? + } + + let parser: some Parser = Parse(ComplexType.init(string:number:example:)) { + Take { + Prefix(0...).map(.string) + } upTo: { + Int.parser() + } + Optionally { + OneOf { + "a".map { Example.a } + "b".map { Example.b } + } + } + } + + var input = "Hello1b"[...] + XCTAssertEqual(ComplexType(string: "Hello", number: 1, example: .b), try parser.parse(&input)) + XCTAssertTrue(input.isEmpty) + } +} diff --git a/Tests/ParsingTests/TakeUpToTests.swift b/Tests/ParsingTests/TakeUpToTests.swift new file mode 100644 index 0000000000..7f316bdb12 --- /dev/null +++ b/Tests/ParsingTests/TakeUpToTests.swift @@ -0,0 +1,54 @@ +import Parsing +import XCTest + +final class TakeUpToTests: XCTestCase { + func testSimple() throws { + let parser = TakeUpTo { + "." + } + + var input = "123.456"[...] + XCTAssertEqual("123", try parser.parse(&input).0) + } + + func testUnterminated() throws { + let parser = TakeUpTo { + "." + } + + var input = "123456"[...] + + XCTAssertThrowsError(try parser.parse(&input)) + } + + func testTakeUpToString() throws { + let parser = TakeUpTo { + "456".map { 456 } + } + var input = "123456"[...] + let output = try parser.parse(&input) + XCTAssertEqual("123", output.0) + XCTAssertEqual(456, output.1) + } + + func testTakeSubstring() throws { + let parser = TakeUpTo { + "456".map { 456 } + } + var input = "123456"[...] + let output = try parser.parse(&input) + XCTAssertEqual("123", output.0) + XCTAssertEqual(456, output.1) + XCTAssertEqual("", input) + } + + func testTakeUpToEnd() throws { + let parser = TakeUpTo { + End() + } + + var input = "123"[...] + XCTAssertEqual("123", try parser.parse(&input).0) + XCTAssertEqual("", input) + } +} From d8567374347c0ffd5332f16e5170d5d96886d828 Mon Sep 17 00:00:00 2001 From: David Peterson Date: Thu, 21 Mar 2024 23:31:09 +1000 Subject: [PATCH 2/2] Fixes for TakeUpTo --- Sources/Parsing/ParserPrinters/TakeUpTo.swift | 17 ++++------------- Tests/ParsingTests/TakeUpToTests.swift | 16 ++++++++-------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/Sources/Parsing/ParserPrinters/TakeUpTo.swift b/Sources/Parsing/ParserPrinters/TakeUpTo.swift index 9b664c779c..df41b4a5dc 100644 --- a/Sources/Parsing/ParserPrinters/TakeUpTo.swift +++ b/Sources/Parsing/ParserPrinters/TakeUpTo.swift @@ -30,16 +30,16 @@ where Input.SubSequence == Input, Upstream.Input == Input @inlinable @inline(__always) - public func parse(_ input: inout Input) throws -> (Input, Upstream.Output) { + public func parse(_ input: inout Input) throws -> Input { let original = input var currentIndex = input.startIndex while currentIndex <= input.endIndex { do { var test = input[currentIndex...] - let result = try terminator.parse(&test) - input = test - return (original[..