Skip to content

Commit fa4f743

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. rdar://74430478
1 parent deba793 commit fa4f743

16 files changed

+315
-187
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 allow falling back to an outer result if
416+
// we have a forward reference. This preserves the behavior prior to
417+
// 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
}
@@ -587,19 +592,20 @@ static Expr *resolveDeclRefExpr(UnresolvedDeclRefExpr *UDRE, DeclContext *DC,
587592
Lookup = TypeChecker::lookupUnqualified(DC, LookupName, Loc, lookupOptions);
588593

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

597603
// If local declaration after use is found, check outer results for
598604
// better matching candidates.
599605
if (ResultValues.empty() && localDeclAfterUse) {
600606
auto innerDecl = localDeclAfterUse;
601607
while (localDeclAfterUse) {
602-
if (Lookup.outerResults().empty()) {
608+
if (!shouldUseOuterResults || Lookup.outerResults().empty()) {
603609
Context.Diags.diagnose(Loc, diag::use_local_before_declaration, Name);
604610
Context.Diags.diagnose(innerDecl, diag::decl_declared_here,
605611
localDeclAfterUse);
@@ -609,12 +615,12 @@ static Expr *resolveDeclRefExpr(UnresolvedDeclRefExpr *UDRE, DeclContext *DC,
609615
Lookup.shiftDownResults();
610616
ResultValues.clear();
611617
localDeclAfterUse = nullptr;
612-
AllDeclRefs =
613-
findNonMembers(Lookup.innerResults(), UDRE->getRefKind(),
614-
/*breakOnMember=*/true, ResultValues,
615-
[&](ValueDecl *D) {
616-
return isValidForwardReference(D, DC, &localDeclAfterUse);
617-
});
618+
AllDeclRefs = findNonMembers(
619+
Lookup.innerResults(), UDRE->getRefKind(),
620+
/*breakOnMember=*/true, ResultValues, [&](ValueDecl *D) {
621+
return isValidForwardReference(D, DC, localDeclAfterUse,
622+
shouldUseOuterResults);
623+
});
618624
}
619625
}
620626
}

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: 10 additions & 131 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
@@ -183,16 +173,6 @@ func f_57097() {
183173
// expected-warning@-1 {{variable 'r' was never mutated; consider changing to 'let' constant}}
184174
}
185175

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-
}
195-
196176
// MARK: - Forward Declared Lets
197177

198178
// https://github.com/swiftlang/swift/issues/84909
@@ -208,110 +188,6 @@ func global_fwd(_ a: () -> Any) -> Any { a() }
208188
func global_gen_fwd<T>(_ g: () -> T) -> T { g() }
209189
func global_fwd_p(_ p: () -> any P) -> any P { p() }
210190

211-
func forward_declared_let_captures() {
212-
do {
213-
let bad: Any = { bad }()
214-
// expected-error@-1 {{closure captures 'bad' before it is declared}}
215-
// expected-note@-2 {{captured here}}
216-
// expected-note@-3 {{captured value declared here}}
217-
}
218-
219-
do {
220-
func fwd(_ i: () -> Any) -> Any { i() }
221-
let bad = fwd { bad }
222-
// expected-error@-1 {{closure captures 'bad' before it is declared}}
223-
// expected-note@-2 {{captured here}}
224-
// expected-note@-3 {{captured value declared here}}
225-
}
226-
227-
do {
228-
let bad = global_fwd { bad }
229-
// expected-error@-1 {{closure captures 'bad' before it is declared}}
230-
// expected-note@-2 {{captured here}}
231-
// expected-note@-3 {{captured value declared here}}
232-
}
233-
234-
do {
235-
let bad: Any = global_gen_fwd { bad }
236-
// expected-error@-1 {{closure captures 'bad' before it is declared}}
237-
// expected-note@-2 {{captured here}}
238-
// expected-note@-3 {{captured value declared here}}
239-
}
240-
241-
do {
242-
let bad: Any = E.static_gen_fwd { bad }
243-
// expected-error@-1 {{closure captures 'bad' before it is declared}}
244-
// expected-note@-2 {{captured here}}
245-
// expected-note@-3 {{captured value declared here}}
246-
}
247-
248-
do {
249-
let badNested: Any = global_fwd { { [badNested] in badNested }() }
250-
// expected-error@-1 {{closure captures 'badNested' before it is declared}}
251-
// expected-note@-2 {{captured here}}
252-
// expected-note@-3 {{captured value declared here}}
253-
}
254-
255-
do {
256-
let badOpt: Any? = { () -> Any? in badOpt }()
257-
// expected-error@-1 {{closure captures 'badOpt' before it is declared}}
258-
// expected-note@-2 {{captured here}}
259-
// expected-note@-3 {{captured value declared here}}
260-
}
261-
262-
do {
263-
let badTup: (Any, Any) = { (badTup.0, badTup.1) }()
264-
// expected-error@-1 {{closure captures 'badTup' before it is declared}}
265-
// expected-note@-2 {{captured here}}
266-
// expected-note@-3 {{captured value declared here}}
267-
}
268-
269-
do {
270-
let badTup: (Int, Any) = { (badTup.0, badTup.1) }()
271-
// expected-error@-1 {{closure captures 'badTup' before it is declared}}
272-
// expected-note@-2 {{captured here}}
273-
// expected-note@-3 {{captured value declared here}}
274-
}
275-
276-
do {
277-
let (badTup3, badTup4): (Any, Any) = { (badTup4, badTup3) }()
278-
// expected-error@-1 {{closure captures 'badTup4' before it is declared}}
279-
// expected-note@-2 {{captured here}}
280-
// expected-note@-3 {{captured value declared here}}
281-
// expected-error@-4 {{closure captures 'badTup3' before it is declared}}
282-
// expected-note@-5 {{captured here}}
283-
// expected-note@-6 {{captured value declared here}}
284-
}
285-
286-
do {
287-
struct S { var p: Any }
288-
let badStruct: S = { S(p: badStruct.p) }()
289-
// expected-error@-1 {{closure captures 'badStruct' before it is declared}}
290-
// expected-note@-2 {{captured here}}
291-
// expected-note@-3 {{captured value declared here}}
292-
}
293-
294-
do {
295-
enum EE {
296-
case boring
297-
case weird(Any)
298-
case strange(Any)
299-
}
300-
301-
let badEnum: EE = { .weird(EE.strange(badEnum)) }()
302-
// expected-error@-1 {{closure captures 'badEnum' before it is declared}}
303-
// expected-note@-2 {{captured here}}
304-
// expected-note@-3 {{captured value declared here}}
305-
}
306-
307-
do {
308-
let badproto: any P = global_fwd_p { badproto }
309-
// expected-error@-1 {{closure captures 'badproto' before it is declared}}
310-
// expected-note@-2 {{captured here}}
311-
// expected-note@-3 {{captured value declared here}}
312-
}
313-
}
314-
315191
func forward_declared_let_captures_local_fn() {
316192
do {
317193
func bad_local_f() -> Any { bad }
@@ -441,10 +317,13 @@ func forward_declared_let_captures_local_fn() {
441317
}
442318
}
443319

444-
func forward_declared_local_lazy_captures() {
445-
// runtime stack overflow
446-
lazy var infiniteRecurse: Any = { infiniteRecurse }()
447-
448-
// function that returns itself
449-
lazy var hmm: () -> Any = { hmm }
450-
}
320+
// FIXME: Currently they crash SILGen (TypeConverter-setCaptureTypeExpansionContext-e72208.swift)
321+
//func forward_declared_local_lazy_captures() {
322+
// // runtime stack overflow
323+
// var _infiniteRecurse: Any { infiniteRecurse }
324+
// lazy var infiniteRecurse = _infiniteRecurse
325+
//
326+
// // function that returns itself
327+
// func _hmm() -> Any { hmm }
328+
// lazy var hmm = _hmm
329+
//}
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+
}

0 commit comments

Comments
 (0)