Open stevenkellner opened 1 year ago
I experience the same problem with my custom result builder. Is there any way to get a meaningful diagnostic from the compiler?
The issue here is that buildEither(first:)
and buildEither(second:)
return different types, and rootModifiers
must return a single consistent type (some ViewModifier
is opaque but has to be a particular ViewModifier).
Replacing the opaque return types in the result builder methods themselves:
static func buildPartialBlock<T: ViewModifier>(first: T) -> T {
return first
}
static func buildEither<T: ViewModifier>(first modifier: T) -> ConditionalModifier<T,EmptyModifier> {
return ConditionalModifier(alwaysTrue: modifier)
}
static func buildEither<T: ViewModifier>(second modifier: T) -> ConditionalModifier<EmptyModifier,T> {
return ConditionalModifier(alwaysFalse: modifier)
}
... it becomes more clear that the issue is that ConditionalModifier<MyViewModifier,EmptyModifier>
!= ConditionalModifier<EmptyModifier,MyOtherViewModifier>
. If you make this change, the reported errors are still poor, but at least they exist:
rb.swift:123:24: error: cannot convert value of type 'MyViewModifier' to expected argument type 'EmptyModifier'
if myCondition {
^
rb.swift:125:16: error: cannot convert value of type 'MyOtherViewModifier' to expected argument type 'EmptyModifier'
} else {
^
These errors are reported in the implicit calls to buildPartialBlock
and are reported this way because using EmptyModifier()
to make both the then and else results into ConditionalModifier<EmptyModifier,EmptyModifier>
is the only way to make the constraints work out.
The easy (but possibly wrong?) way to fix this is to check in ResultBuilderTransform::isBuildableIfChain
that the buildEither(first:)
and buildEither(second:)
declarations that we lookup match each others return types. That could be a very focused diagnosis. But are there possible (useful) result builder implementations where these return types structurally differ but match up in practice? (Like the trivial ConditionalModifier<EmptyModifier,EmptyModifier>
in this example.) If so, does that mean we're closing off useful possibilities?
The more complicated but maybe more correct fix is in two parts:
(1) With the modified (removed opaque types) ModifierBuilder, we get a solution with fixes and could examine the constraint system's overload choices after solving has failed in TypeChecker:applyResultBuilderBodyTransform
and check the actual resolved types for the choices made for buildEither(first:)
and buildEither(second:)
and diagnose there.
(2) But also, in the original code with opaque return types, there are no fixes and thus no solution and no overload choices left in the constraint system. The solution is probably a new kind of Fix where we allow two similar looking opaque return types to match each other and additional diagnoses for that.
@xedin What do you think?
As I'm thinking about it more, it doesn't feel like restricting the result types to be structurally identical is a big problem. That seems to be the intention of the design, and there is flexibility with buildExpression
and buildFinalResult
to convert from and to other intermediate types.
Description The following is taken from a larger piece of code and condensed to show a compiler problem with a custom result builder and an if-else statement. This bug only occurred with this custom result builder and not with e.g. the ViewBuilder result builder.
A single if statement compiles successfully, but with an else block it produced this error:
Failed to produce diagnostic for expression; please submit a bug report (https://swift.org/contributing/#reporting-bugs)
.The
ModifierBuilder
is just a result builder to build view modifiers. It implements bothbuildEither
methods so it should be no problem to write an if-else statement.Steps to reproduce
Compiling the following code in playground will generate the error
Failed to produce diagnostic for expression; please submit a bug report (https://swift.org/contributing/#reporting-bugs)
.Show code to reproduce
```swift import SwiftUI @resultBuilder struct ModifierBuilder { static func buildPartialBlock(first: some ViewModifier) -> some ViewModifier { return first } static func buildPartialBlock(accumulated: some ViewModifier, next: some ViewModifier) -> some ViewModifier { return PairModifier(first: accumulated, second: next) } static func buildEither(first modifier: some ViewModifier) -> some ViewModifier { return ConditionalModifier(alwaysTrue: modifier) } static func buildEither(second modifier: some ViewModifier) -> some ViewModifier { return ConditionalModifier(alwaysFalse: modifier) } static func buildOptional(_ modifier: (some ViewModifier)?) -> some ViewModifier { return OptionalModifier(modifier: modifier) } static func buildLimitedAvailability(_ modifier: some ViewModifier) -> some ViewModifier { return LimitedAvailabilityModifier(modifier: modifier) } } struct PairModifierThe error will show at the line
@ModifierBuilder private var rootModifiers: some ViewModifier {
Expected behavior
The if-else statement compiles successfully without an error.
Environment
Swift compiler version info: Apple Swift version 5.9 (swiftlang-5.9.0.114.10 clang-1500.0.29.1) Target: x86_64-apple-darwin22.6.0
Xcode version info: Xcode 15.0 Build version 15A5209g
Deployment target: iOS 16.4