pointfreeco / swift-parsing

A library for turning nebulous data into well-structured data, with a focus on composition, performance, generality, and ergonomics.
https://www.pointfree.co/collections/parsing
MIT License
864 stars 77 forks source link

Applied restriction to result builders in future swift version breaks code created with swift-parsing #278

Closed JaapWijnen closed 1 year ago

JaapWijnen commented 1 year ago

This describes and discusses the issue: https://github.com/apple/swift/issues/62978

It seems a design choice was made to make resultBuilders stricter in how they compile causing some of the behaviour swift-parsing was relying on to no longer be valid. I've made the example provided in the discussion work for the new toolchain (2023-01-09) by making the ParserBuilder generic over the accepted Input, see solution below.

I've tried but haven't succeeded in creating a fix for the library as a whole however. Would love to hear your thoughts on this cause I'm a little stuck at the moment unfortunately.

struct ParsingError: Error { }

//MARK: Parsers
@rethrows public protocol Parser<Input, Output> {
    associatedtype Input
    associatedtype Output

    func parse(input: inout Input) throws -> Output
}

extension String: Parser {
    public typealias Input = Substring

    public func parse(input: inout Substring) throws {
        guard input.starts(with: self) else {
          throw ParsingError()
        }
        input.removeFirst(self.count)
    }
}

extension Int {
    static func parser(
        of inputType: Substring.Type = Substring.self
    ) -> FromSubstringToUTF8<IntParser<Substring.UTF8View>> {
        FromSubstringToUTF8 { IntParser<Substring.UTF8View>() }
    }

    static func parser(
        of inputType: Substring.UTF8View.Type = Substring.UTF8View.self
    ) -> IntParser<Substring.UTF8View> {
        .init()
    }
}

struct FromSubstringToUTF8<P: Parser>: Parser where P.Input == Substring.UTF8View {
    typealias Input = Substring

    let parser: P

    init(_ parser: P) {
        self.parser = parser
    }

    init(@ParserBuilder<P.Input> _ build: () -> P) { self.parser = build() }

    func parse(input: inout Substring) throws -> P.Output {
        var transformedInput = input.utf8
        let result = try parser.parse(input: &transformedInput)
        input = Substring(transformedInput)
        return result
    }
}

struct IntParser<Input: Collection>: Parser where Input.SubSequence == Input, Input.Element == UTF8.CodeUnit {
    public init() { }

    func parse(input: inout Input) throws -> Int {
        // some int parsing logic
        return 4
    }
}

//MARK: ParserBuilder
struct Parse<Input, Parsers: Parser>: Parser where Input == Parsers.Input {
    let parsers: Parsers

    init(@ParserBuilder<Parsers.Input> with build: () -> Parsers) { self.parsers = build() }

    func parse(input: inout Parsers.Input) throws -> Parsers.Output {
        try parsers.parse(input: &input)
    }
}

@resultBuilder
struct ParserBuilder<Input> {
    public static func buildPartialBlock<P: Parser>(first: P) -> P where P.Input == Input { first }

    public static func buildPartialBlock<P0, P1>(accumulated: P0, next: P1) -> SkipFirst<P0, P1> where P0.Input == Input, P1.Input == Input  {
        .init(p0: accumulated, p1: next)
    }

    public static func buildExpression<P: Parser>(_ expression: P) -> P where P.Input == Input {
        expression
    }
}

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

    func parse(input: inout P0.Input) throws -> P1.Output {
        let _ = try p0.parse(input: &input)
        return try p1.parse(input: &input)
    }
}

//MARK: main
let parser = Parse {
    ","
    Int.parser()
}

var input = ",4"[...]
let result = try parser.parse(input: &input)
print(result)
stephencelis commented 1 year ago

Hey @JaapWijnen! We have intended to make the parser builder generic over its input, but haven't carved out the time to do so yet, especially since it'll likely require many more changes in the library to work, as you seem to be confirming. Let's keep this issue open as an action item, and we'll make sure we return to it before these changes make it into an official version of Swift.

dmzza commented 1 year ago

Upgrading to 0.12 and following this guidance solved issues for me with Swift 5.8. Can this issue be closed now?

stephencelis commented 1 year ago

@dmzza Yup, thanks for the nudge!