Improved Result Builder Implementation in Swift 5.8

I am happy to announce that the result builder implementation has been re-worked in Swift 5.8 to greatly improve compile-time performance, code completion results, and diagnostics. The new implementation is now enabled by default on main and release/5.8. The Swift 5.8 result builder implementation enforces stricter type inference that matches the semantics in SE-0289: Result Builders, which has an impact on some existing code that relied on invalid type inference. This post outlines the motivation and the impact of the new result builder implementation.

The original result builder transform implementation has some significant problems - type-checking becomes slower as the number of elements in the transformed body grows, code completion is not always helpful or fast, and invalid result builders often produce poor diagnostics, especially the unhelpful “failed to produce diagnostic” fallback message. All of these problems stem from a bespoke type inference mechanism for result builders that did not match the simple source-level transformation in the result builder design.

The new implementation takes advantage of the extended multi-statement closure inference introduced in Swift 5.7 and applies the result builder transformation exactly as specified by the result builder proposal - a source-level transformation which is type-checked like a multi-statement closure. Doing so enables the compiler to take advantage of all the benefits of the improved closure inference for result builder transformed code, including optimized type-checking performance (especially in invalid code) and improved error messages. Under this model, each element in a result builder body is type-checked in isolation, allowing code completion to skip unrelated elements to yield improvements in its accuracy and performance.

In the vast majority of cases, there is no visible difference between the original and new result builder implementation. However, some result builder uses that compile in Swift 5.7 relied on invalid type inference that was not part of the intended semantics described by the result builder proposal. Such result builder uses were also the most prone to poor type checking performance, diagnostics, and code completion results. While this invalid type inference is now rejected in Swift 5.8, there are several strategies that can be used to achieve the same effect in result builders that forward-propagate type information or provide additional type context for its components, which are covered later in this post.

Improvements

Let’s take a look at couple of examples improved in Swift 5.8:

  • Improved diagnostics when contextual base type couldn’t be resolved:
import SwiftUI

struct ContentView: View {

    enum Destination {
        case one
        case two
    }

    var body: some View {
        List {
            NavigationLink(value: .one) {
                Text("one")
            }
            NavigationLink(value: .two) {
                Text("two")
            }
        }
        .navigationDestination(for: Destination.self) { $0.view }
    }
}

Swift 5.7 produced a misleading error:

error: value of type 'ContentView.Destination' has no member 'view'
        .navigationDestination(for: Destination.self) { $0.view }
                                                        ~~ ^~~~

Swift 5.8 produces the following error that precisely points out the mistake:

error: cannot infer contextual base in reference to member 'one'
            NavigationLink(value: .one) {
                                  ~^~~
  • Time to type-check this relatively simple example of Table API use dropped from 4s. to 0.6s.
import SwiftUI
import Foundation

struct Data: Identifiable {
  var value: Int
  let id: UUID = UUID()
  init(value: Int) {
    self.value = value
  }
}

struct MyTest : View {
  var body: some View {
    var comparators = [KeyPathComparator(\Data.value)]

    Table([Data(value: 0)], sortOrder: .constant(comparators)) {
      TableColumn("String 1", value: \.id.uuidString)
      TableColumn("String 2", value: \Data.id.uuidString)
      TableColumn("String 3", value: \Data.value) {
        Text("\($0.value)")
      }

      Group {
        TableColumn("String 3", value: \Data.id.uuidString)
        TableColumn("View") {
          Text("\($0.value)")
        }
      }
    }
  }
}
  • Invalid code has even bigger performance gains:
import SwiftUI
import Foundation

struct Place: Identifiable, Hashable {
    let id: UUID = UUID()
    let name: String
    let comfortLevel: String
    let noiseLevel: Int
}

struct PlacesView : View {
    @State var places: [Place]

    var body: some View {
        NavigationStack {
            Table(places) {
                TableColumn("Name", value: \.name)
                TableColumn("Comfort Level", value: \.comfortLevel).width(200)
                TableColumn("Noise", value: \.noiseLevel)
            }
            .navigationTitle("All Places")
        }
    }
}

This example would produce error: the compiler is unable to type-check this expression in reasonable time error when compiled with Swift 5.7. Swift 5.8 is capable of quickly identifying a mismatch in the last TableColumn argument:

error: key path value type 'Int' cannot be converted to contextual type 'String'
                TableColumn("Noise", value: \.noiseLevel)
                                             ^
  • Structural diagnostics:
import SwiftUI

struct Item : Hashable {
  var question: String
  var answer: Int
}

struct MyView : View {
  var items: [Item] = []
  
  var body: some View {
    ZStack {
      ForEach(items, id: \.self) { item in
        if let question = item.question { 🔴 initializer for conditional binding must have Optional type, not 'String'
          ...
        }
      }
    }
  }
}

Strategies for propagating type information through result builders

Type inference failures can always be resolved with explicit type annotations, but there are a few techniques that result builders can use to achieve better call-site ergonomics when the result builder can provide the necessary context. Result builders have the ability to specify additional type context for components at the use site using buildExpression, and generic result builders can be used for forward propagation of type information. Let’s take a look at a few examples.

  • Result builders that rely on build{Partial}Block to provide context to the elements in the body:
@resultBuilder
struct TupleBuilder {
   ...
   static func buildBlock(_ v1: String, 
                          _ v2: String) -> (String, String) {
     (v1, v2)
   }
}

func ambiguous() -> Int { 42 }
func ambiguous() -> String { "World" }

func test<T>(@TupleBuilder _ fn: () -> T) {}

test {
  "Hello"
  ambiguous()
}

To better understand why this is a problem, let’s take a look at the transformed body:

test {
  var v1 = "Hello"
  var v2 = ambiguous()
  
  return TupleBuilder.buildBlock(v1, v2)
}

The result builder proposal states that initialization expressions for v1, v2 are type-checked independently. Not allowing type information to back-propagate or side-propagate between statements is deliberate in the result builder design, because the underlying type inference model follows that of regular function bodies, and it prevents new avenues for exponential type-checking complexity. However, in the above example, independent type checking of components makes it impossible for the compiler to determine which overload of the function ambiguous to choose.

In order to make this work TupleBuilder needs to implement buildExpression that would provide the necessary context:

extension TupleBuilder {
  static func buildExpression(_ v: String) -> String { v }
}

Because buildExpression wraps every initialization expression, it’s now possible to filter out unrelated overload of ambiguous :

test {
  var v1 = TupleBuilder.buildExpression("Hello")
  var v2 = TupleBuilder.buildExpression(ambiguous())
  
  return TupleBuilder.buildBlock(v1, v2)
}

This is a very simple example, but this technique applies to more complex cases as well - i.e. buildExpression could be used to make sure that all statements conform to a certain protocol or a set thereof and share other common properties that could be expressed via generic requirements. buildExpression can also be overloaded to provide a set of possible type contexts that will be resolved based on the argument during overload resolution.

  • Bi-directional inference between result builder components:

In certain cases the original implementation behaved as-if all the elements in the body have been type-checked together:

protocol CaseValue {
  associatedtype B
  associatedtype V
}

protocol ContextualValue {
  associatedtype Context
  associatedtype V
}

struct Case<Base, Value> : CaseValue {
   typealias B = Base
   typealias V = Value
   
   init<T>(value: Value,
           @ValueBuilder<Base> _ value: () -> T) {}
}

@resultBuilder
struct Switch<Base, Value> {
  init(_ keyPath: KeyPath<Base, Value>,
       @CaseBuilder<Base, Value> _ cases: () -> [Case]) {}
}

@resultBuilder
struct CaseBuilder<Base, Value> {
   static func buildExpression<T: CaseValue>(_ v: T) -> T 
           **where** **T****.****B** **==** **** **Base****,** **T****.****V** **==** **** **Value** { v }
   ...
}

@resultBuilder
struct ValueBuilder<Context> {
   ...
   static func buildBlock<T: ContextualValue>(_ v: T) -> T.V
           **where T.Context == Context** { ... }
}

struct Test {
  var x: Int
  
  struct Value<T> : ContextualValue {
    typealias Context = Test
    typealias V = T
    
    init(_: T) {}
  }

  func test() {
     Switch(\.x) {
        Case(.zero) {
           Value("0")
        }
     }
  }
}

This is a convoluted example but there is no way to illustrate it fully without using multiple builders. Let’s take a look at the transformed version of the body of test():

func test() {
     Switch(\.x) {
        var v0 = CaseBuilder.buildExpression(Case(.zero) {
           var v1 = Value("0")
           return ValueBuilder.buildBlock(v1)
        })
        return CaseBuilder.buildBlock(v0)
     }
  }

To fully resolve \.x passed to initializer of Switch , the compiler has to fully type-check v0 which is in turn impossible because Value associated with CaseBuilder could only be resolved after \.x is type-checked.

  • Result builder types with generic parameters

Because result builders are designed prohibit side-propagation of type information between their components, generic result builders must be fully resolved at the first call to a result builder method such as buildExpression or buildBlock. However, under some circumstances, the original result builder implementation allowed such generic parameters to be inferred as-if all of the statements were type-checked together, leading to worst-case type checking performance in the compiler. The new implementation enforces forward-propagation and allows generic parameters associated with a result builder type to be inferred only from the enclosing context and/or from the first buildExpression in the body:

protocol MyProtocol {
  associatedtype Value
}

struct Data<Value> : MyProtocol {
   init(_ v: Value) {}
}

@resultBuilder
struct GenericBuilder<T> {
  static func buildExpression<U: MyProtocol>(_ v: U) -> U 
          where U.Value == T { v }
}

func test<T>(@GenericBuilder<T> _ fn: () -> T) {}

test {
  Data(0)
  Data(1)
  Data(2)
}

The transformed body looks like this:

test {
  var v1 = GenericBuilder.buildExpression(Data(0))
  var v2 = GenericBuilder.buildExpression(Data(1))
  var v3 = GenericBuilder.buildExpression(Data(2))
  
  return GenericBuilder.buildBlock(v1, v2, v3)
}

buildExpression connects its generic parameter U to the contextual generic parameter T which allows to infer it from the first statement in the body and make sure that all of the other statements have the same type.

  • Partially resolved closure parameters used in the result builder body
@resultBuilder
struct MyBuilder { ... }

struct Test<Content> {
  enum State {
     case stateA
     case stateB
  }
  
  init(@MyBuilder fn: (State) -> Content) { ... }
}

Test { state in
  if state == .stateA {
     ...
  }
}

This is an attempt to reference a member on a partially resolved type Test<Content> , the reference is ill-formed because Content is inferred from the buildBlock that represents the return of the closure body, which means expression state == .stateA cannot produce a complete solution and would be rejected by the compiler.

The workaround here could be to lift State from Test to top-level scope because it doesn’t rely on the Content , in more complex situations it might be possible to connect Content of Test with MyBuilder by making it generic and use technic outlined in the first bullet point to infer it early.

76 Likes

Awesome! Can't wait to use this!

3 Likes

I'm seeing some regressions that, presumably, are due to the previous incorrect inference. I can fix them by forcing a builder closure to have an explicit return type. Do you have any guidance on how to narrow down where the incorrect inference was being performed?

For example, simple usage from the Swift Composable Architecture's ReducerBuilder and TCACoordinators' forEachRoute leads to a rather simple inference failure:

Reduce { state, action in
    switch action {
    // Many actions elided.
    }
    return .none
}
.forEachRoute {
    NewExpenseFlow()
}

This now fails with an error on the Reduce line: Generic parameter 'ScreenReducer' could not be inferred. Which is misleading, because it's the forEachRoute function that introduces the ScreenReducer generic type. I can fix it by simply adding a return type to the builder closure:

.forEachRoute { () -> NewExpenseFlow in
    NewExpenseFlow()
}

Obviously this seems like a really simple issue where the inference should work, but there's an intermediate layer of an extension on ReducerProtocol, so I'm not sure where the issue may lie.

On another note, is this why there were several new SwiftUI APIs that added explicit generics, like Table? The previous versions were relying on the invalid inference rules? Or perhaps these changes were for compilation performance?

3 Likes

Folks from Swift Composible Architecture reached out to us about some issues with ReducerBuilder and we have provided solution for that that shouldn't require any new type annotations.

Declarations like @ReducerBuilder<ScreenReducer.State, ScreenReducer.Action> screenReducer: () -> ScreenReducer are the problem because they assume that the whole body of the result builder transformed expression is type-checked as-if it's a single expression, which it is not, generic parameters for ReducerBuilder should be inferred from the first use of buildExpression.

2 Likes

Yes, for both reasons.

Several of SwiftUI's Table initializers relied on shared type inference across multiple builders. These initializers were recently deprecated and replaced with new versions that require the table's Value type to be specified explicitly, which also has the effect of improving compilation performance.

For example, the table below needs to infer its Value type (Place) from its rows and columns closures, and ensure that each closure uses the same type. In this case, it can only infer the type from the rows closure, and then use that type for its columns, since columns on its own is ambiguous:

extension Table {
    // `Value` is a generic parameter of `Table`, and 
    // used by both builders.
    public init(
        @TableColumnBuilder<Value, Never> columns: () -> Columns,
        @TableRowBuilder<Value> rows: () -> Rows
    )
    ...
}
let (place1, place2, place3): (Place, Place, Place) = ...

Table {
    TableColumn("Name", value: \.name)
    TableColumn("Comfort Level", value: \.comfortLevel)
    TableColumn("Noise", value: \.noiseLevel)
} rows: {
    TableRow(place1)
    TableRow(place2)
    TableRow(place3)
}

This kind of type inference required that the entire initializer was type-checked as a single expression, which will no longer be allowed with the new compiler. But it was also inefficient, increasing compile times.

To resolve this, SwiftUI will require that developers explicitly identify the Value type using a new of: parameter, which guarantees that the Value type is known prior to type-checking each closure:

Table(of: Place.self) {
    TableColumn("Name", value: \.name)
    TableColumn("Comfort Level", value: \.comfortLevel)
    TableColumn("Noise", value: \.noiseLevel)
} rows: {
    TableRow(place1)
    TableRow(place2)
    TableRow(place3)
}

Note that in many cases existing code will not require any changes: this new of: type parameter is not required for all Table initializers, only those that don’t identify the Value type via other initializer parameters. For example, tables that directly take a collection (e.g. Table(places)) or sort order (e.g. Table(sortOrder:)) can infer the Value type from those parameters, in which case the of: parameter is not required.

5 Likes

To elaborate further here this is point Result builder types with generic parameters from the post.

Tranformed body looks something like this:

.forEachRoute {
    var $__builder0 = ReducerBuilder.builderExpression(NewExpenseFlow())
    return ReducerBuilder.buildBlock($__builder0)
}

Generic parameter ScreenReducer is only inferred from the final return buildBlock(...) in the body of the transformed closure. It should be possible to change the declaration of forEachRoute to something like:

func forEachRoute<SRState, SRAction: Identifable, ScreenReducer: ReducerProtocol, CoordinatorID: Hashable>(
   <#parameters#>,
   @ReducerBuilder<SRState, SRAction> screenReducer: () -> ScreenReducer
  ) -> some ReducerProtocol<State, Action> where
          SRState == ScreenReducer.State,
          SRAction == ScreenReducer.Action,
          ...  {
    ...
}

This would make it possible for the builder to infer State/Action from buildExpression and then later check that against ScreenReducer once its type is inferred.

1 Like

As you have seen in my transformed example the error is actually correct ReducerBuilder<$T_ScreenReducer.State, $T_ScreenReducer.Action>.buildExpression(NewExpenseFlow()) where $T_ScreenReducer is a type variable for ScreenReducer generic parameter, this expression requires that ScreenReducer be resolved before the solver can deduce associated type State and Action from it and that is not possible because $T_ScreenReducer could only be inferred from return ReducerBuilder.buildBlock(...)

1 Like

My issue was that the error didn't seem to be correctly placed in the source, not that it was generally incorrect. I really have no way of evaluating that.

Also, how does the inference from ReducerBuilder.buildBlock(NewExpenseFlow()) fail? I understand the issue with multiple statements, but given this is only a single statement, I would've thought the inference would work. That's why I was also confused about the error and fix, given we're just providing a type that is also provided right there on the next line. I don't think most users would understand that, which is why I brought it up.

I've applied your fix and it works great, thanks!

The post was trying to outline that result builder transformed closures, although they might appear as a single-expression closures in the source, are in fact not that. They consist of multiple synthesized statements and variable declarations. That said, I have a few ideas how to improve diagnostics when it comes to implicit code like that (i.e. “self” of the result builder), so stay tuned!

4 Likes

Love this change, especially the improved type checking with meaningful errors! @xedin, you've added an example on how to enforce typing on build{Partial}Block and I've been using it for some time in the past. Tho now I have troubles with coming up with something similar for buildFinalResult. Context: I have a result builder that that either construct some type conforming to a protocol or it can construct itself (source). Before swift 5.8 compiler was able to correctly infer the result type from usage (example), but it does not look it's the case anymore. Do you have any tips (besides splitting the the result builder in two different entities, that's the last resort refactoring I have in mind)?

I'm not exactly sure what's the problem with buildFinalResult, you'd have to elaborate. After looking at the builder itself it looks like both buildExpression and buildPartialBlock methods produce some LVPath<Struct> which means that there is only one possible overload of buildFinalBlock that could get used in this case - one that takes some LVPath<Struct> as an argument.

1 Like

Consider this code:

        try LavaBuilder<VkPresentInfoKHR> {
            (\.waitSemaphoreCount, \.pWaitSemaphores) <- waitSemaphores
            (\.swapchainCount, \.pSwapchains) <- swapchains
            \.pImageIndices <- imageIndices
            \.pResults <- nil
        }
        .withUnsafeMutableResultPointer { info in
            try lock.synchronized {
                try vulkanInvoke {
                    device.vkQueuePresentKHR(pointer, info)
                }
            }
        }

Method withUnsafeMutableResultPointer exists only on LavaBuilder, protocol LVPath does not define it. Prior to version 5.8 compiler was able to understand which init method of LavaBuilder to use to produce instance of LavaBuilder. I assume this was costly operation since it requires some heuristics. With 5.8 compiler throws error: ambiguous use of 'buildFinalResult' with reference to two candidates which consume some LVPath<Struct>. i.e. compiler can not infer that the result type should be LavaBuilder. And it is indeed ambiguous which confused me for a very long time (like, how did it work? my brain says it never should have worked like that)
Full error:

.../Queue.swift:75:43: error: ambiguous use of 'buildFinalResult'
        try LavaBuilder<VkPresentInfoKHR> {
                                          ^
.../LavaBuilder.swift:71:24: note: found this candidate
    public static func buildFinalResult(_ component: some LVPath<Struct>) -> some LVPath<Struct> {
                       ^
.../LavaBuilder.swift:76:24: note: found this candidate
    public static func buildFinalResult(_ component: some LVPath<Struct>) -> LavaBuilder<Struct> {
                       ^

Method withUnsafeMutableResultPointer exists only on LavaBuilder, protocol LVPath does not define it.

This is how the transformed code looks like:

try LavaBuilder<VkPresentInfoKHR>.init({
  let $__builder0 = LavaBuilder<VkPresentInfoKHR>.buildExpression((\.waitSemaphoreCount, \.pWaitSemaphores) <- waitSemaphores)
  let $__buider1 = LavaBuilder<VkPresentInfoKHR>.buildExpression((\.swapchainCount, \.pSwapchains) <- swapchains)
  let $__builder2 = LavaBuilder<VkPresentInfoKHR>.buildExpression(\.pImageIndices <- imageIndices)
  let $__builder3 = LavaBuilder<VkPresentInfoKHR>.buildExpression(\.pResults <- nil)

  let $__buildBlock0 = LavaBuilder<VkPresentInfoKHR>.buildPartialBlock(first: $__builder0)
  let $__buildBlock1 = LavaBuilder<VkPresentInfoKHR>.buildPartialBlock(accumulated: $__buildBlock0, next: $__builder1)
  let $__buildBlock2 = LavaBuilder<VkPresentInfoKHR>.buildPartialBlock(accumulated: $__buildBlock1, next: $__builder2)
  let $__buildBlock3 = LavaBuilder<VkPresentInfoKHR>.buildPartialBlock(accumulated: $__buildBlock2, next: $__builder3)

  return LavaBuilder<VkPresentInfoKHR>.buildFinalResult($__buildBlock3)
})
.withUnsafeMutableResultPointer { info in
  try lock.synchronized {
    try vulkanInvoke {
      device.vkQueuePresentKHR(pointer, info)
    }
  }
}

I added .init to explicitly delimit where initializer reference begins and ends to show that .withUnsafeMutableResultPointer is always chained on it and therefore its base type is LavaBuilder<VkPresentInfoKHR> always.

The actual issue here is that return buildFinalResult($__buildBlock3) is a result of the @LavaBuilder<Struct> _ content: () throws -> (Path) closure which means that it's type-checked separately from parent context and the type-checker currently doesn't propagate any generic requirements into the body of the multi-statement closure (return doesn't know that result type should conform to LVPath protocol) which makes that bit of code ambiguous.

Luckily, all overloads buildFinalResult overloads besides:

    @inlinable @_transparent
    public static func buildFinalResult(_ component: some LVPath<Struct>) -> some LVPath<Struct> {
        component
    }

are redundant because buildPartialBlock(...) would only be able to produce one type - some LVPath<Struct>, so you can safely delete them and it should fix this situation.

3 Likes

Awesome, thanks for detailed analysis! I was not sure how migrating to buildPartialBlock would affect existing code, so I kept the original buildFinalResult overloads. Now I can safely remove those. Tho this will remove quality of life feature to be able to save and pass around instance to LavaBuilder. Welp, changing how it works was long overdue :)

1 Like

The following code works fine before Swift 5.8:

protocol View { }

struct ArrayContent<Content: View>: View {
    let array: [Content]
}

@resultBuilder
struct ViewBuilder {
    static func buildBlock<Content: View>(_ content: Content) -> Content {
        content
    }
    
    static func buildArray<Content: View>(_ components: [Content]) -> ArrayContent<Content> {
        ArrayContent(array: components)
    }
}

struct MyView: View { }

func createView() -> some View {
    MyView()
}

@ViewBuilder
func test() -> some View {
    for _ in 0...10 {
        createView()
    }
}

print(test())

But with Swift 5.8, the compiler throws error:

@ViewBuilder
func test() -> some View {
    for _ in 0...10 {
        createView()
    } // <- Generic parameter 'τ_0_0' could not be inferred
}

It works if I change the return type of createView to a specific type (such as MyView in this sample). Is there something else I missed to get it to work with some View?

2 Likes

I'm surprised that it worked before and I don't know that type did the transform use, it looks like one of the examples where the compiler allowed something it shouldn't have. As written your example would synthesize an array variable that infers its element type from the body of for-in loop, the value that gets passed to synthesized .append(...) call would have type some View which we cannot use as an element type for the array variable, transform looks something like this:

func test() -> some View {
    let __builder0: ArrayContent<some View>
    var __array: [some View] = []
    for _ in 0...10 {
        let __builder1 = createView()
        let __builder2 = ViewBuilder.buildBlock(__builder1)
        __array.append(__builder2)
    }

    __builder0 = ViewBuilder.buildArray(__array)
    return ViewBuilder.buildBlock(__builder0)
}
1 Like

It seems that I need to use a specific type for the body of the for-in loop, or replace the loop with a ForEach type like SwiftUI. Thank you for your help!

Yes, if you are using SwiftUI then ForEach is the way to go, for custom builders type erasure might also work.

1 Like

I hate to complain because Rule Builders are so magical already, but what if one doesn't want to use a specific type or write a custom ForEach/Repeater type that conforms to the protocol? Is there not a way?

I asked because I had a problem that appeared to be related when I went to add for-loops to a protocol based result builder.

I fixed my specific problem, but I'd like to be able to do something like:

let multiType = Assembly {
    Square()
    for _ in 0...3 {
         //in the "real world" the index would be used to determine characteristics
        Circle()
        Square()
    }
    Circle()
}

At this point I don't even care how ugly the underlying data is.

I wrote a Repeating(count:Int) { LayerBuilder } example (in the linked to topic), but I'm hesitant to reimplement for loops when they already exist and are familiar.

You can tell me to just go write javascript if I'm being unreasonable :laughing:. I'll take the hint.

For clarity, my biggest discomfort is that this works easily and so nicely with a buildOptional implemented:

let maybe = Assembly {
    Square()
    if showCircle {
        Circle()
        Circle()
        Square()
    }
    Square()
}

But

let multiType = Assembly {
    Square()
    for _ in numbers {
        Circle()
        Circle()
        Square()
    }
    Square()
}

Needs a lot of hoop jumping, a custom implementation actually, to mimic the functionality. The buildArray function only lets you pass one and only one item to the for loop from what I can tell?

Is there a trick I'm not seeing?

What is the difference?

Would it be possible to change @resultBuilder to synthesize .map instead?

func test() -> some View {
    let __array = (0...10).map { _ in
        let __builder1 = createView()
        let __builder2 = ViewBuilder.buildBlock(__builder1)
        return __builder2
    }

    let __builder0 = ViewBuilder.buildArray(__array)
    return ViewBuilder.buildBlock(__builder0)
}

Is it too late?