Skip to content

Infer generic arguments of result builder attributes - #3062

Merged
allevato merged 8 commits into
swiftlang:mainfrom
calda:cal--infer-result-builder-generics
Nov 12, 2026
Merged

allevato merged 8 commits into
swiftlang:mainfrom
calda:cal--infer-result-builder-generics

Conversation

@calda

@calda calda commented Jan 3, 2026 •

Copy link
Copy Markdown
Contributor

This PR adds a proposal to support inferring generic arguments of result builder attributes.

For example, this sample code is currently rejected with the error "reference to generic type 'ArrayBuilder' requires arguments in <...>", but would now compile successfully.

@resultBuilder
struct ArrayBuilder<Element> {
  static func buildBlock(_ elements: Element...) -> [Element] {
    elements
  }
}

@ArrayBuilder
var stringArray: [String] {
  "foo"
  "bar"
}

@calda
calda force-pushed the cal--infer-result-builder-generics branch from 02f1b16 to d8caf3e Compare January 3, 2026 21:22
@calda calda changed the title Add proposal to support inferring generic arguments of result builder attributes Infer generic arguments of result builder attributes Jan 3, 2026
@calda
calda force-pushed the cal--infer-result-builder-generics branch from d8caf3e to b4ca275 Compare January 3, 2026 22:23
@calda
calda force-pushed the cal--infer-result-builder-generics branch from b4ca275 to 3ec7b0d Compare January 5, 2026 00:08
@rjmccall rjmccall added LSG Contains topics under the domain of the Language Steering Group new proposal Adds a new proposal document labels Jan 5, 2026
@allevato allevato self-assigned this Nov 11, 2026

@allevato allevato left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Cal! I'll be review managing this proposal. A couple comments before we can get it started.

Comment on lines +97 to +99
When not specified explicitly, the generic arguments for a generic result builder attribute will be inferred from the return type of the attached declaration.

We can infer that the return type of the attached declaration should be equal to one of the potential result types of the result builder. The potential result types of the result builder are defined by the types returned from the `buildFinalResult`, `buildPartialBlock`, and `buildBlock` methods.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you go into a little more detail about how this is resolved? We don't need fine-grained implementation details, but elaborating a bit on the steps the algorithm takes would be helpful. For example,

  • Does it look at each of the named functions in sequence, picking the first one it comes across? Or does it rank them in some way?
  • If there are multiple overloads (as you have below), how is the correct one picked? Does it create and solve a disjunction in the constraint system?

If any of the decisions above are derived from other existing result builder logic in the compiler, it would be helpful to call that precedent out. Likewise, if you're making any decisions that aren't directly related to existing behavior, call that out too.

@calda calda Nov 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very helpful call-out. The original implementation was not very robust: it tried each buildFinalResult, buildPartialBlock etc in order until it found one that matched, and did not attempt to do overload resolution if there were multiple valid options.

I thought about it and realized that we don't actually need to concretely resolve the generic arguments for the result builder attribute. Instead, we can simply allow it to be and stay non-specialized, and use the type "as written" in the result builder transform output. That way, all of the type inference and overload resolution is handled within the existing result builder transform, which is most aligned with the design direction of result builders.

Here's the new discussion and examples in the proposal body:

Result builder attributes will be allowed to omit some or all of a generic result builder's generic arguments, either by leaving them out entirely (@ArrayBuilder) or by using placeholder types (e.g. @DictionaryBuilder<String, _>). The missing generic arguments are then inferred by the existing result builder transform, as part of type-checking the transformed body.

Take this result builder call:

@ArrayBuilder<String>
var arguments: [String] {
    "format"
    "--in-place"
}

Today, its result builder transform approximately expands to:

var arguments: [String] {
    let v0 = "format"
    let v1 = "--in-place"
    return ArrayBuilder<String>.buildBlock(v0, v1)
}

When omitting the generic type arguments:

@ArrayBuilder
var arguments: [String] {
    "format"
    "--in-place"
}

the result builder transform simply uses the type as written in the result builder attribute:

var arguments: [String] {
    let v0 = "format"
    let v1 = "--in-place"
    return ArrayBuilder.buildBlock(v0, v1)
}

The compiler will then type-check the resulting transformed code as usual, and will resolve the implicit generic arguments following standard type inference. This simple approach naturally scales to all result builder capabilities, including buildBlock, buildPartialBlock, and buildFinalResult use cases, as well as complex scenarios that require overload resolution.

This dramatically simplified the implementation, which is a good sign it's on the right track :)

Let me know what you think!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds reasonable to me, though it's near the edges of my understanding of the type checker. 🙂 I agree that if it simplifies the implementation then it's probably the right track though.

I see you've already committed that change; is this ready to go now? I'm ready to start the review whenever you are.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Ready now :)

I added one extra note on type checking performance to the proposal body. The implementation PR is updated as well and the full test suite passes.

Ready to run the review and available to participate. Thanks @allevato!!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found two cases where the "new, simplified implementation" does not work well, and shifted gears back to an improved version of the initial version.

First, by not resolving the generic arguments of the result builder attribute at the original declaration site (and instead only doing it in the result builder transform), it became possible to write a declaration that was impossible to actually use:

struct Invocation {
  @ArrayBuilder let arguments: Set<String> // No error here, even though `ArrayBuilder` doesn't support `Set<String>`
}

// Always an error: cannot convert return expression of type '[String]' to return type 'Set<String>'
let value = Invocation(arguments: {
  "format"
  "--in-place"
})

Second, I found that in subtype situations the "type inference within the result builder transform" produced unintuitive results:

// Most intuitive inference result would be `ArrayBuilder<any StringProtocol>`.
// However, inferred as `ArrayBuilder<String>` by the result builder transform's type inference.
@ArrayBuilder
var inferredValues: [any StringProtocol] {
  "format"
  "--in-place".dropFirst(2) // error: cannot convert value of type 'String.SubSequence' (aka 'Substring') to expected argument type 'String'
}

So, ultimately I do think we need to go with the original approach of resolving the generic arguments of result builder attribute in the context of the original declaration.

The good news is that I was able to simplify the original implementation to address this concern: #3062 (comment)

The algorithm now handles overload resolution very naturally:

  1. We select the eligible buildBlock / buildFinalResult / buildPartialBlock path, following standard result builder priority: buildFinalResult if present / valid, buildPartialBlock if present / valid, and then buildBlock.
  2. We type check the expression <UnresolvedResultBuilderType>.buildFinalResult against the type (_) -> <Result>, where Result is the return type of the declaration the result builder is attached to.

By using the standard ConstraintSystem::solve path, we get all of the standard overload resolution and ranking that you would expect 😄

I will update the proposal text to reflect this and send a PR :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you go: #3590

@calda
calda force-pushed the cal--infer-result-builder-generics branch from ac7d41d to 98e0efe Compare November 12, 2026 02:55
@calda

calda commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

Awesome, thank you @allevato!

I updated the proposal and implementation based on your feedback. Let me know what you think, and looking forward to the review 😄

@calda
calda force-pushed the cal--infer-result-builder-generics branch from 3227e38 to 688abd7 Compare November 12, 2026 16:36
@calda
calda force-pushed the cal--infer-result-builder-generics branch from 688abd7 to 54dd54a Compare November 12, 2026 16:37
@allevato
allevato merged commit 6ee9c3a into swiftlang:main Nov 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

LSG Contains topics under the domain of the Language Steering Group new proposal Adds a new proposal document

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants