AlexanderM
(Alexander Momchilov)
1
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
hborla
(Holly Borla)
2
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
AlexanderM
(Alexander Momchilov)
3
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
}
}()
hborla
(Holly Borla)
4
(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
young
(rtSwift)
5
From Write a DSL in Swift using result builders
the example uses enum instead of struct
Jumhyn
(Frederick Kellison-Linn)
6
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?
AlexanderM
(Alexander Momchilov)
7
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
hborla
(Holly Borla)
8
Yes, this is what I meant, thank you for clarifying!
2 Likes