swift: Partial block result builder fails to pick correct overload and generates compiler error

Description Partial block result builder fails to pick correct overload and generates a compiler error Ambiguous use of 'parser(of:)'

Steps to reproduce Paste the following code into a new project (This issue is a reduction of trying to compile an example (final 4 lines of the code below) of the swift-parsing package)

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

extension String: Parser {
    public typealias Input = Substring
}

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(@ParserBuilder _ build: () -> P) { self.parser = build() }
}

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

//MARK: ParserBuilder
struct Parse<Parsers: Parser>: Parser {
    typealias Input = Parsers.Input

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

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

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

struct SkipFirst<P0: Parser, P1: Parser>: Parser where P0.Input == P1.Input {
    typealias Input = P0.Input

    let p0: P0
    let p1: P1
}

//MARK: main
let first = ParserBuilder.buildPartialBlock(first: ",")
let second = ParserBuilder.buildPartialBlock(accumulated: first, next: Int.parser())

let parser = Parse {
    ","
    Int.parser() // Ambiguous use of 'parser(of:)'
}

Expected behavior This compiles for toolchain 2023-01-02 but breaks from 2023-01-07. Expected to compile and choose the correct overload (in this case static func parser(of inputType: Substring.Type = Substring.self))

Since the first parser passed to the resultBuilder closure is of type String therefore the Parser associated type Input is of type Substring. Therefore the compiler should be able to infer that the parser returned by Int.parser() should have an Input of Substring as well.

Calling the individual partialBuildBlock functions does compile works:

let first = ParserBuilder.buildPartialBlock(first: ",")
let second = ParserBuilder.buildPartialBlock(accumulated: first, next: Int.parser())

fails:

let parser = Parse {
    ","
    Int.parser() // Ambiguous use of 'parser(of:)'
}

Environment

  • Swift compiler version info Swift Development Snapshot 2023-01-09
  • Xcode version info Xcode 13.4.1 Build version 13F100
  • Deployment target: M1 running macOS 12.5

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 81 (81 by maintainers)

Most upvoted comments

@xedin Wow! Thanks! That actually gets the one regression in our test suite building again without needing to be explicit with \State.path and allowing abbreviation in \.path again!

https://github.com/pointfreeco/swift-composable-architecture/commit/feae99aa459f47962a01289682ee8468e2dc969a

The implementation is https://github.com/apple/swift/pull/60065 and there are a few smaller follow-up PRs. Enablement is https://github.com/apple/swift/pull/62734. I’m also working on a forums post to announce the change and describe some of the situations that were allowed before but wouldn’t be now.

@stephencelis I actually found a solution for CombineReducers problem (based on builder-updates branch):

diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/CombineReducers.swift b/Sources/ComposableArchitecture/Reducer/Reducers/CombineReducers.swift
index bb569e8dc..031a8dc51 100644
--- a/Sources/ComposableArchitecture/Reducer/Reducers/CombineReducers.swift
+++ b/Sources/ComposableArchitecture/Reducer/Reducers/CombineReducers.swift
@@ -15,7 +15,8 @@
 ///   .ifLet(\.child, action: /Action.child)
 /// }
 /// ```
-public struct CombineReducers<Reducers: ReducerProtocol>: ReducerProtocol {
+public struct CombineReducers<Action, State, Reducers: ReducerProtocol>: ReducerProtocol
+    where Action == Reducers.Action, State == Reducers.State {
   @usableFromInline
   let reducers: Reducers

@@ -23,9 +24,9 @@ public struct CombineReducers<Reducers: ReducerProtocol>: ReducerProtocol {
   ///
   /// - Parameter build: A reducer builder.
   @inlinable
-  public init<State, Action>(
+  public init(
     @ReducerBuilder<State, Action> _ build: () -> Reducers
-  ) where State == Reducers.State, Action == Reducers.Action {
+  ) {
     self.init(internal: build())
   }

Adding this indirection for State/Action generic parameter makes sure that when CombineReducers() is wrapped in buildExpression declared as:

static func buildExpression(_: some ReducerProtocol<State, Action>) -> some ReducerProtocol<State, Action> {
    return expression
  }

State Action could be infer for it based on the “builder self”: ReducerBuilder<State, Action>.builderExpression(CombineReducers()) where State and Action are struct and enum from your example.

Example you have posted previously type-checks with these changes.

This is definitely helpful! While we knew a lot of changes we needed to make based off more recent result builder projects we’ve taken on, this thread provided a lot of additional food for thought. Thanks, @xedin!

No problem! I’m going to resolve this one, feel free to reach out on forums as well if you need more help with result builders.