Is it possible to view how the compiler desugars result builder code?

I'm having difficult fully grasping all the steps, and I think it would be quite illustrative to see exactly how it all comes together.

Right now I've been trying to reverse engineer it using print statements. For example, I modified the sample from the official docs:

ArrayBuilder instrumented to print on every step
@resultBuilder
struct ArrayBuilder {
	typealias Component = [Int]
	typealias Expression = Int
	typealias FinalResult = [Int]

	static func buildExpression(_ element: Expression) -> Component {
		let result = [element]
		print("buildExpression(\(element)) // => \([element])")
		return result
	}

	static func buildOptional(_ component: Component?) -> Component {
		let result = component ?? []
		print("buildOptional(\(component.map(String.init) ?? "nil")) // => \(result)")
		return result
	}

	static func buildEither(first component: Component) -> Component {
		let result = component
		print("buildEither(first: \(component)) // => \(result)")
		return result
	}

	static func buildEither(second component: Component) -> Component {
		let result = component
		print("buildEither(second: \(component)) // => \(result)")
		return component
		return result
	}

	static func buildArray(_ components: [Component]) -> Component {
		let result = Array(components.joined())
		print("buildArray(components: \(components)) // => \(result)")
		return result
	}

	static func buildBlock(_ components: Component...) -> Component {
		let result = Array(components.joined())
		print("buildBlock(components: \(components.map(String.init).joined(separator: ", "))) // => \(result)")
		return result
	}
	
	static func buildFinalResult(_ component: Component) -> FinalResult {
		let result = component // No-op, just here for the print-out
		print("buildFinalResult(\(component)) // => \(result)")
		return result
	}
}

Which shows that this:

@ArrayBuilder var myArray: [Int] {
	10
	20
	if true {
		30
	} else {
		999
	}
	if true {
		40
	}
}

Desugars equivalently to this code (which I wrote by hand using guess&check):

var myDesugaredArray: [Int] {
	let e1 = ArrayBuilder.buildExpression(10)
	let e2 = ArrayBuilder.buildExpression(20)
	let e3 = true
		? ArrayBuilder.buildEither(first: ArrayBuilder.buildBlock(ArrayBuilder.buildExpression(30)))
		: ArrayBuilder.buildEither(second: ArrayBuilder.buildBlock(ArrayBuilder.buildExpression(999)))
	
	let e4 = ArrayBuilder.buildOptional(ArrayBuilder.buildExpression(40))
	
	return ArrayBuilder.buildFinalResult(
		ArrayBuilder.buildBlock(e1, e2, e3, e4)
	)
}

Is there a way to generalize this, to see how any arbitrary @resultBuilder usage gets desugared?

4 Likes

FWIW, whenever I need to write out the result builder transform (e.g. to explain the type inference model), I always refer to the section on the transform from the Swift evolution proposal: https://github.com/apple/swift-evolution/blob/main/proposals/0289-result-builders.md#the-result-builder-transform

There is not currently a way to see Swift source code for the result builder transform. I believe the closest output you can get today is the type-checked AST, but unfortunately mapping the AST back to source code isn't fully implemented in the compiler. Also note that the result builder transform can express some things that manually-written Swift code today cannot (control flow statements in result builders are effectively if/switch expressions whose results get assigned to synthesized local variables).

I think your best bet for now is the documentation in the SE-0298 proposal.

1 Like

I figured this transform was don’t on the AST and doesn’t literally rewrite the source text, but I’m surprised the AST can’t be serialized back out as source, why is that? Is generating the AST a lossy process that can’t be fully reversed?

That’s a pretty good reference, thanks!

You can approximate this using an immediate evaluated closure, like:

let x: Int = {
    if predicate {
        return 123
    else {
        return 456
    }
}()

(sorry I forgot to reply a couple weeks ago!)

The type-checked AST definitely has more information than source, but the compiler should be able to translate a type-checked AST back to source that type checks in the same way. I believe the main reason why the compiler only supports printing declarations today is because expression printing hasn't been fully implemented yet in the ASTPrinter. The only place where the compiler needs to print expressions today is in inlinable code in Swift interfaces, and it's simply printed as written in source.

1 Like

From Write a DSL in Swift using result builders

the example uses enum instead of struct

Please excuse the maybe-dumb question, but what more information could the AST possibly have than the source—is "the source" not the sole input to the compiler for producing the AST?

I think she meant that the AST contains more explicitly stated information than the source, which is readily available for querying.

Not to get too epistemological, but the compiler can only do deductive reasoning: drawing conclusions from premises (your source, and the definition of the language that's encoded into its implementation). No "net new" information is obtained in that process, it's merely surfacing things that were true all along.

3 Likes

Yes, this is what I meant, thank you for clarifying!

2 Likes