Skip to content

ResultBuilders do not produce efficient parameter pack types #85837

@ultramiraculous

Description

@ultramiraculous

Description

As described here, it seems like ResultBuilders have strange behavior when dealing with parameter packs.

This makes something like a simple tuple builder require an intermediate type to work.

The type checker for result builders seems to prefer nested types when parameter packs are involved, resulting in types like:

(((String, Int), String) Int)

where you might otherwise expect:

(String, Int, String Int)

And not only are is the former, a more "complex" option chosen, it is the only type option available, as detailed below.

Reproduction

@resultBuilder public enum TupleBuilder {
    public static func buildPartialBlock<T>(first: T) -> (T) {
        return first
    }

    public static func buildPartialBlock<each A, B>(accumulated: (repeat each A), next: B) -> (repeat each A, B) {
        return (repeat each accumulated, next)
    }

}

func builder<each A>(@TupleBuilder content: ()->(repeat each A)) -> (repeat each A) {
    return content()
}

let manual: (String, Int, String) = {
    let a = TupleBuilder.buildPartialBlock(first: "a")
    let b = TupleBuilder.buildPartialBlock(accumulated: a, next: 2)
    return TupleBuilder.buildPartialBlock(accumulated: b, next: "c")
}()

// Cannot convert return expression of type '((String, Int), String)' to return type '(String, Int, String)'
let built: (String, Int, String) = {
    return builder {
        "a"
        2
        "c"
    }
}()

// Works
let built2: ((String, Int), String) = {
    return builder {
        "a"
        2
        "c"
    }
}()

// Interestingly *doesn't* work? ...but in the opposite way as `built`?
//
// Cannot convert value of type '(String, Int, String)' to closure result type '((String, Int), String)'
let manual2: ((String, Int), String) = {
    let a = TupleBuilder.buildPartialBlock(first: "a")
    let b = TupleBuilder.buildPartialBlock(accumulated: a, next: 2)
    return TupleBuilder.buildPartialBlock(accumulated: b, next: "c")
}()

Expected behavior

I would expect both of these to work/type check:

let builtA: (String, Int, String) = {
    return builder {
        "a"
        2
        "c"
    }
}()

let builtB: ((String, Int), String) = {
    return builder {
        "a"
        2
        "c"
    }
}()

For example a simple tuple merge function allows for both groupings:

private func merge<each A, B>(_ a: (repeat each A), _ b: B) -> (repeat each A, B) {
    return (repeat each a, b)
}

let t: (String) = "1"
let tt = merge(t, 2)
let ttt1: (String, Int, String) = merge(tt, "C") // Works
let ttt2: ((String, Int), String) = merge(tt, "C") // Works

At the very least it would be more intuitive for the compiler to prefer the type of builtA above because the (String, Int, String) tuple is less "complex"/nested (imo) compared to ((String, Int), String).

This packing complexity also compounds at each step, resulting in:

let builtC: (((String, Int), String), Int) = {
    return builder {
        "a"
        2
        "c"
        4
    }
}()

Environment

swift-driver version: 1.127.14.1 Apple Swift version 6.2.1 (swiftlang-6.2.1.4.8 clang-1700.4.4.1)
Target: arm64-apple-macosx26.0

Additional information

Discussion about parameter pack builders: Here

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugA deviation from expected or documented behavior. Also: expected but undesirable behavior.triage neededThis issue needs more specific labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions