Skip to content

Commit 7caf3eb

Browse files
committed
[Sema] Catch use-before-declarations in nested closures
Previously we would allow these in Sema and diagnose them in SILGen, but allowing them in Sema is unsound because it means the constraint system ends up kicking interface type requests for declarations that should be type-checked as part of the closure itself. Adjust the name lookup logic to look through parent closures when detecting invalid forward references. For now we don't fallback to an outer result on encountering a use-before-declaration to preserve the current behavior. I'm planning on changing that in the next commit though.
1 parent 55ebd07 commit 7caf3eb

15 files changed

+176
-76
lines changed

lib/Sema/PreCheckTarget.cpp

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -393,29 +393,34 @@ static bool isMemberChainTail(Expr *expr, Expr *parent, MemberChainKind kind) {
393393
}
394394

395395
static bool isValidForwardReference(ValueDecl *D, DeclContext *DC,
396-
ValueDecl **localDeclAfterUse) {
397-
*localDeclAfterUse = nullptr;
398-
399-
// References to variables injected by lldb are always valid.
400-
if (isa<VarDecl>(D) && cast<VarDecl>(D)->isDebuggerVar())
396+
ValueDecl *&localDeclAfterUse,
397+
bool &shouldUseOuterResults) {
398+
// Only VarDecls require declaration before use.
399+
auto *VD = dyn_cast<VarDecl>(D);
400+
if (!VD)
401401
return true;
402402

403-
// If we find something in the current context, it must be a forward
404-
// reference, because otherwise if it was in scope, it would have
405-
// been returned by the call to ASTScope::lookupLocalDecls() above.
406-
if (D->getDeclContext()->isLocalContext()) {
407-
do {
408-
if (D->getDeclContext() == DC) {
409-
*localDeclAfterUse = D;
410-
return false;
411-
}
403+
// Non-local and variables injected by lldb are always valid.
404+
auto *varDC = VD->getDeclContext();
405+
if (!varDC->isLocalContext() || VD->isDebuggerVar())
406+
return true;
412407

413-
// If we're inside of a 'defer' context, walk up to the parent
414-
// and check again. We don't want 'defer' bodies to forward
415-
// reference bindings in the immediate outer scope.
416-
} while (isa<FuncDecl>(DC) &&
417-
cast<FuncDecl>(DC)->isDeferBody() &&
418-
(DC = DC->getParent()));
408+
while (true) {
409+
if (varDC == DC) {
410+
localDeclAfterUse = VD;
411+
return false;
412+
}
413+
if (isa<AbstractClosureExpr>(DC) ||
414+
(isa<FuncDecl>(DC) && cast<FuncDecl>(DC)->isDeferBody())) {
415+
// If we cross a closure, don't context, don't allow falling back to
416+
// an outer result if we have a forward reference. This preserves the
417+
// behavior prior to diagnosing this in Sema.
418+
if (isa<AbstractClosureExpr>(DC))
419+
shouldUseOuterResults = false;
420+
DC = DC->getParent();
421+
continue;
422+
}
423+
break;
419424
}
420425
return true;
421426
}
@@ -589,19 +594,20 @@ static Expr *resolveDeclRefExpr(UnresolvedDeclRefExpr *UDRE, DeclContext *DC,
589594
Lookup = TypeChecker::lookupUnqualified(DC, LookupName, Loc, lookupOptions);
590595

591596
ValueDecl *localDeclAfterUse = nullptr;
592-
AllDeclRefs =
593-
findNonMembers(Lookup.innerResults(), UDRE->getRefKind(),
594-
/*breakOnMember=*/true, ResultValues,
595-
[&](ValueDecl *D) {
596-
return isValidForwardReference(D, DC, &localDeclAfterUse);
597-
});
597+
bool shouldUseOuterResults = true;
598+
AllDeclRefs = findNonMembers(
599+
Lookup.innerResults(), UDRE->getRefKind(),
600+
/*breakOnMember=*/true, ResultValues, [&](ValueDecl *D) {
601+
return isValidForwardReference(D, DC, localDeclAfterUse,
602+
shouldUseOuterResults);
603+
});
598604

599605
// If local declaration after use is found, check outer results for
600606
// better matching candidates.
601607
if (ResultValues.empty() && localDeclAfterUse) {
602608
auto innerDecl = localDeclAfterUse;
603609
while (localDeclAfterUse) {
604-
if (Lookup.outerResults().empty()) {
610+
if (!shouldUseOuterResults || Lookup.outerResults().empty()) {
605611
Context.Diags.diagnose(Loc, diag::use_local_before_declaration, Name);
606612
Context.Diags.diagnose(innerDecl, diag::decl_declared_here,
607613
localDeclAfterUse);
@@ -611,12 +617,12 @@ static Expr *resolveDeclRefExpr(UnresolvedDeclRefExpr *UDRE, DeclContext *DC,
611617
Lookup.shiftDownResults();
612618
ResultValues.clear();
613619
localDeclAfterUse = nullptr;
614-
AllDeclRefs =
615-
findNonMembers(Lookup.innerResults(), UDRE->getRefKind(),
616-
/*breakOnMember=*/true, ResultValues,
617-
[&](ValueDecl *D) {
618-
return isValidForwardReference(D, DC, &localDeclAfterUse);
619-
});
620+
AllDeclRefs = findNonMembers(
621+
Lookup.innerResults(), UDRE->getRefKind(),
622+
/*breakOnMember=*/true, ResultValues, [&](ValueDecl *D) {
623+
return isValidForwardReference(D, DC, localDeclAfterUse,
624+
shouldUseOuterResults);
625+
});
620626
}
621627
}
622628
}

test/NameLookup/name_lookup.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -643,8 +643,8 @@ struct PatternBindingWithTwoVars3 { var x = y, y = x }
643643

644644
// https://github.com/apple/swift/issues/51518
645645
do {
646-
let closure1 = { closure2() } // expected-error {{circular reference}} expected-note {{through reference here}}
647-
let closure2 = { closure1() } // expected-note {{through reference here}} expected-note {{through reference here}}
646+
let closure1 = { closure2() } // expected-error {{use of local variable 'closure2' before its declaration}}
647+
let closure2 = { closure1() } // expected-note {{'closure2' declared here}}
648648
}
649649

650650
func color(with value: Int) -> Int {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// RUN: %target-typecheck-verify-swift
2+
// RUN: %target-typecheck-verify-swift -parse-as-library
3+
4+
func testLocal() {
5+
// The first `y` here is considered the inner result.
6+
do {
7+
let y = ""
8+
do {
9+
let _: String = y
10+
let y = 0
11+
_ = y
12+
}
13+
}
14+
do {
15+
let y = ""
16+
do {
17+
_ = {
18+
let _: String = y
19+
}
20+
let y = 0
21+
_ = y
22+
}
23+
}
24+
do {
25+
let y = ""
26+
_ = {
27+
_ = {
28+
let _: String = y
29+
}
30+
let y = 0
31+
_ = y
32+
}
33+
}
34+
do {
35+
let y = ""
36+
func bar() {
37+
_ = {
38+
let _: String = y
39+
}
40+
let y = 0
41+
_ = y
42+
}
43+
}
44+
}
45+
46+
let topLevelString = ""
47+
48+
func testTopLevel() {
49+
// Here 'topLevelString' is now an outer result.
50+
do {
51+
let _: String = topLevelString
52+
let topLevelString = 0
53+
_ = topLevelString
54+
}
55+
do {
56+
_ = {
57+
let _: String = topLevelString // expected-error {{use of local variable 'topLevelString' before its declaration}}
58+
}
59+
let topLevelString = 0 // expected-note {{'topLevelString' declared here}}
60+
}
61+
_ = {
62+
_ = {
63+
let _: String = topLevelString // expected-error {{use of local variable 'topLevelString' before its declaration}}
64+
}
65+
let topLevelString = 0 // expected-note {{'topLevelString' declared here}}
66+
}
67+
func bar() {
68+
_ = {
69+
let _: String = topLevelString // expected-error {{use of local variable 'topLevelString' before its declaration}}
70+
}
71+
let topLevelString = 0 // expected-note {{'topLevelString' declared here}}
72+
}
73+
}

test/SILGen/capture_order.swift

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,6 @@ func transitiveForwardCapture3() {
109109
}
110110
}
111111

112-
func captureInClosure() {
113-
let x = { (i: Int) in // expected-error {{closure captures 'currentTotal' before it is declared}}
114-
currentTotal += i // expected-note {{captured here}}
115-
}
116-
117-
var currentTotal = 0 // expected-note {{captured value declared here}}
118-
119-
_ = x
120-
}
121-
122112
/// Regression tests
123113

124114
// https://github.com/apple/swift/issues/47389
@@ -182,13 +172,3 @@ func f_57097() {
182172
var r = g() // expected-note {{captured value declared here}}
183173
// expected-warning@-1 {{variable 'r' was never mutated; consider changing to 'let' constant}}
184174
}
185-
186-
class class77933460 {}
187-
188-
func func77933460() {
189-
var obj: class77933460 = { obj }()
190-
// expected-error@-1 {{closure captures 'obj' before it is declared}}
191-
// expected-note@-2 {{captured here}}
192-
// expected-note@-3 {{captured value declared here}}
193-
// expected-warning@-4 {{variable 'obj' was never mutated; consider changing to 'let' constant}}
194-
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
// RUN: not %target-swift-frontend %s -sil-verify-all -c 2>&1 | %FileCheck %s
1+
// RUN: %target-typecheck-verify-swift
22

33
// Report the error but don't crash.
4-
// CHECK: error: closure captures 'stringList' before it is declared
54

65
class TestUndefined {
76
private var stringList: [String]!
87

98
func dontCrash(strings: [String]) {
109
assert(stringList.allSatisfy({ $0 == stringList.first!}))
10+
// expected-error@-1 {{use of local variable 'stringList' before its declaration}}
1111
let stringList = strings.filter({ $0 == "a" })
12+
// expected-note@-1 {{'stringList' declared here}}
1213
}
1314
}

test/SILOptimizer/definite_init_closures_fail.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,7 @@ struct Generic<T : P> {
6565
} // expected-error {{return from initializer without initializing all stored properties}}
6666
}
6767

68+
func captureUninitialized() {
69+
let fn: () -> Void // expected-note {{constant defined here}}
70+
fn = { fn() } // expected-error {{constant 'fn' captured by a closure before being initialized}}
71+
}

test/Sema/diag_use_before_declaration.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,24 @@ func nested_scope_3() {
8080
}
8181
}
8282

83+
func captureInClosure() {
84+
let x = { (i: Int) in
85+
currentTotal += i // expected-error {{use of local variable 'currentTotal' before its declaration}}
86+
}
87+
88+
var currentTotal = 0 // expected-note {{'currentTotal' declared here}}
89+
90+
_ = x
91+
}
92+
93+
class class77933460 {}
94+
95+
func func77933460() {
96+
var obj: class77933460 = { obj }()
97+
// expected-error@-1 {{use of local variable 'obj' before its declaration}}
98+
// expected-note@-2 {{'obj' declared here}}
99+
}
100+
83101
//===----------------------------------------------------------------------===//
84102
// Type scope
85103
//===----------------------------------------------------------------------===//

test/expr/closure/multi_statement.swift

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,8 @@ func test_no_crash_with_circular_ref_due_to_error() {
358358
// expected-error@-1 {{consecutive statements on a line must be separated by ';'}}
359359
// expected-error@-2 {{'let' cannot appear nested inside another 'var' or 'let' pattern}}
360360
// expected-error@-3 {{cannot call value of non-function type 'Int?'}}
361-
print(next)
361+
// expected-note@-4 {{'next' declared here}}
362+
print(next) // expected-error {{use of local variable 'next' before its declaration}}
362363
return x
363364
}
364365
return 0
@@ -676,20 +677,26 @@ func test_recursive_var_reference_in_multistatement_closure() {
676677

677678
func test(optionalInt: Int?, themes: MyStruct?) {
678679
takeClosure {
679-
let int = optionalInt { // expected-error {{cannot call value of non-function type 'Int?'}}
680-
print(int)
680+
let int = optionalInt {
681+
// expected-error@-1 {{cannot call value of non-function type 'Int?'}}
682+
// expected-note@-2 {{'int' declared here}}
683+
print(int) // expected-error {{use of local variable 'int' before its declaration}}
681684
}
682685
}
683686

684687
takeClosure {
685-
let theme = themes?.someMethod() { // expected-error {{extra trailing closure passed in call}}
686-
_ = theme
688+
let theme = themes?.someMethod() {
689+
// expected-error@-1 {{extra trailing closure passed in call}}
690+
// expected-note@-2 {{'theme' declared here}}
691+
_ = theme // expected-error {{use of local variable 'theme' before its declaration}}
687692
}
688693
}
689694

690695
takeClosure {
691-
let theme = themes?.filter({ $0 }) { // expected-error {{value of type 'MyStruct' has no member 'filter'}}
692-
_ = theme
696+
let theme = themes?.filter({ $0 }) {
697+
// expected-error@-1 {{value of type 'MyStruct' has no member 'filter'}}
698+
// expected-note@-2 {{'theme' declared here}}
699+
_ = theme // expected-error {{use of local variable 'theme' before its declaration}}
693700
}
694701
}
695702
}

test/expr/expressions.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -244,23 +244,24 @@ func test_as_2() {
244244
x as [] // expected-error {{expected element type}} {{9-9= <#type#>}}
245245
}
246246

247-
func test_lambda() {
247+
func test_lambda1() {
248248
// A simple closure.
249249
var a = { (value: Int) -> () in markUsed(value+1) }
250250
// expected-warning@-1 {{initialization of variable 'a' was never used; consider replacing with assignment to '_' or removing it}}
251+
}
251252

253+
func test_lambda2() {
252254
// A recursive lambda.
253-
var fib = { (n: Int) -> Int in
254-
// expected-warning@-1 {{variable 'fib' was never mutated; consider changing to 'let' constant}}
255+
var fib = { (n: Int) -> Int in // expected-note 2{{'fib' declared here}}
255256
if (n < 2) {
256257
return n
257258
}
258259

259-
return fib(n-1)+fib(n-2)
260+
return fib(n-1)+fib(n-2) // expected-error 2{{use of local variable 'fib' before its declaration}}
260261
}
261262
}
262263

263-
func test_lambda2() {
264+
func test_lambda3() {
264265
{ () -> protocol<Int> in
265266
// expected-error @-1 {{'protocol<...>' composition syntax has been removed and is not needed here}} {{11-24=Int}}
266267
// expected-error @-2 {{non-protocol, non-class type 'Int' cannot be used within a protocol-constrained type}}

test/expr/unary/if_expr.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,3 +1647,13 @@ func testCaptureList() {
16471647
let _ = { [x = (if .random() { 0 } else { 1 })] in x }
16481648
// expected-error@-1 {{'if' may only be used as expression in return, throw, or as the source of an assignment}}
16491649
}
1650+
1651+
func testUseBeforeDecl() throws {
1652+
let x = if .random() {
1653+
print(y) // expected-error {{use of local variable 'y' before its declaration}}
1654+
let y = 0 // expected-note {{'y' declared here}}
1655+
throw Err()
1656+
} else {
1657+
0
1658+
}
1659+
}

0 commit comments

Comments
 (0)