Hey folks, I'd like to propose a fix for the checking of forward references to local variables in the type-checker. Today we forbid forward referencing local variables, e.g the following is invalid:
func foo() {
let y = x // error: use of local variable 'x' before its declaration
let x: Int = x // error: use of local variable 'x' before its declaration
}
However we do not currently handle these cases in the type-checker when the forward reference is nested in a closure, e.g:
func foo() {
let y = { x }
let x = 0
}
In practice most of these cases still end up being rejected, they are instead caught by SILGen when we go to form the closure, e.g the above emits the error closure captures 'x' before it is declared. However there are a couple of cases that SILGen permits, which I'd like to consistently reject in the type-checker.
These are:
-
lazylocal variables with initializers that forward reference a local variable from a closure,
and local variables with attached macros that similarly relocate the initializer into an accessor, e.g:func foo() { lazy var x = { y } let y = 0 }func foo() { @LazyLikeMacro var x = { y } let y = 0 } -
Forward references to local computed variables from a closure, e.g:
func foo() { var x = { y } var y: Int { 0 } }
Both of these cases are already rejected today when closures aren't involved. I'd like to reject them consistently. In general we cannot fully support forward references to local variables in the type-checker (at least not without a significant overhaul) since it violates the method by which we type-check closure bodies, where each element is solved individually in source order.
In principle we could always allow forward references to local computed variables, but I think it's better to have a consistent rule for all local variables, regardless of whether they are stored or computed. Lifting the restriction for local computed variables would also potentially be source breaking in the other direction for cases where a reference is not within a closure and is currently relying on shadowing behavior, e.g:
struct S {
var x: Int
func foo() {
let y = x // Currently refers to `self.x`, would change to the local variable
var x: Int { 0 }
}
}
Enforcing a consistent rule ensures we get consistent shadowing behavior, avoiding unexpected surprises. For example the following case now has consistent behavior both inside and outside the closure:
struct S {
var x: Int
func foo(_ xs: [Int]) {
print(x) // Refers to `self.x`
// Currently invalid since it refers to the local variable below, will
// now refer to `self.x` and be valid.
let xs = xs.filter { $0 > x }
// ...
let x = 0
}
}
I have opened a PR that implements the proposed fix: [Sema] Catch use-before-declarations in nested closures by hamishknight · Pull Request #85535 · swiftlang/swift · GitHub