Repository navigation
Infer generic arguments of result builder attributes - #3062
Conversation
02f1b16 to
d8caf3e
Compare
d8caf3e to
b4ca275
Compare
b4ca275 to
3ec7b0d
Compare
allevato
left a comment
There was a problem hiding this comment.
Hey Cal! I'll be review managing this proposal. A couple comments before we can get it started.
| 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, andbuildFinalResultuse 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!
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!!
There was a problem hiding this comment.
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:
- We select the eligible
buildBlock/buildFinalResult/buildPartialBlockpath, following standard result builder priority:buildFinalResultif present / valid,buildPartialBlockif present / valid, and thenbuildBlock. - We type check the expression
<UnresolvedResultBuilderType>.buildFinalResultagainst the type(_) -> <Result>, whereResultis 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 :)
…ad, use the type as-written in the result builder transform, and rely on type inference within the transformed result.
ac7d41d to
98e0efe
Compare
|
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 😄 |
3227e38 to
688abd7
Compare
688abd7 to
54dd54a
Compare
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.