pointfreeco/swift-parsing

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

JaapWijnen opened this issue · 3 comments

This describes and discusses the issue: apple/swift#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)

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

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

@dmzza Yup, thanks for the nudge!