Why can't Swift infer the contextual base type in a result builder?

Viewed 148

Background: I'm writing a compiler for a toy language in Swift, and I want an elegant way to create ASTs for my language. The AST type for a statement in my toy language looks like this:

indirect enum Statement {
    case assignment(variable: String, expression: Expression)
    case conditional(`if`: Expression, then: Statement, `else`: Statement)
    case loop(`while`: Expression, `do`: Statement)
    case sequence(Statement, Statement)
    case noop
    case halt
}

Right now, if I want to write the AST for a series of statements, I would have to write:

// let's say I want to represent a series of 4 no-ops:
.sequence(.noop, 
    .sequence(.noop, 
        .sequence(.noop, 
                      .noop)))

That looks very verbose. I thought it would be nice if I could use the @resultBuilder feature, so that I can write:

Statement {
    .noop
    .noop
    .noop
    .noop
}

This is my attempt:

@resultBuilder
struct StatementBuilder {
    static func buildBlock(_ components: Statement...) -> Statement {
        if components.isEmpty {
            return .noop
        } else {
            return components.dropFirst().reduce(components.first!) { x, y in .sequence(x, y) }
        }
    }
}

extension Statement {
    init(@StatementBuilder block: () -> Statement) {
        self = block()
    }
}

However, this gives me the error:

Cannot infer contextual base in reference to member 'noop'

in the Statement { ... } block.

What is unclear about the contextual base? What type can it be, other than Statement? I could fix this by prefixing everything with Statement., but that's too verbose. What else can I do?


Note that I also plan on overloading operators so that I can easily create assignments and expressions, conforming the syntax tree types to ExpressibleXXXLiteral, and adding If and While functions taking StatementBuilders, which create .conditional and .loop statements. So the result builder will be far more useful than just for creating no-ops and halts.

1 Answers

It appears ResultBuilder expressions use something called one-way constraints

Holly Borla explains here:

Yeah, this is expected behavior. The type inferred for an expression in a function builder does not affect the types inferred for the other expressions.

Unlike most expressions in Swift, function builders use something called one-way constraints for type inference (this is why a regular function call to buildBlock behaves differently wrt type inference). A one-way constraint means that type information can flow only in one direction, instead of the usual bi-directional type inference. So, in this case, the type information Conv2D will affect the overall type inferred for model, but not for the other expressions inside Sequential { ....

@Douglas_Gregor wrote up an explanation of one-way constraints and why they're used in his PR that added them: https://github.com/apple/swift/pull/25983

Related