From e47f5c97d7a961620015f73fab71e5cd63047534 Mon Sep 17 00:00:00 2001 From: Hamish Knight Date: Mon, 17 Nov 2025 17:33:46 +0000 Subject: [PATCH 1/3] [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 --- lib/Sema/PreCheckTarget.cpp | 72 +++++---- test/NameLookup/name_lookup.swift | 4 +- .../use_before_declaration_shadowing.swift | 73 +++++++++ test/SILGen/capture_order.swift | 141 ++---------------- .../closure_lifetime_fixup_undef.swift | 5 +- .../definite_init_closures_fail.swift | 4 + test/Sema/diag_use_before_declaration.swift | 135 +++++++++++++++++ test/expr/closure/multi_statement.swift | 21 ++- test/expr/expressions.swift | 11 +- test/expr/unary/if_expr.swift | 11 ++ ...tSystem-getTypeOfReferencePre-1a97a7.swift | 2 +- .../rdar100753270.swift | 4 +- .../rdar141012049.swift | 4 +- .../KeyPathExpr-getKeyPathType-c80788.swift | 2 +- ...typeCheckStmtConditionElement-08b850.swift | 11 ++ ...Verifier-dispatchVisitPreExpr-9ebf7c.swift | 2 +- 16 files changed, 315 insertions(+), 187 deletions(-) create mode 100644 test/NameLookup/use_before_declaration_shadowing.swift rename validation-test/IDE/{crashers => crashers_fixed}/ConstraintSystem-getTypeOfReferencePre-1a97a7.swift (76%) rename validation-test/{compiler_crashers => compiler_crashers_fixed}/KeyPathExpr-getKeyPathType-c80788.swift (81%) create mode 100644 validation-test/compiler_crashers_fixed/TypeChecker-typeCheckStmtConditionElement-08b850.swift rename validation-test/{compiler_crashers => compiler_crashers_fixed}/Verifier-dispatchVisitPreExpr-9ebf7c.swift (89%) diff --git a/lib/Sema/PreCheckTarget.cpp b/lib/Sema/PreCheckTarget.cpp index 4ee4290dd81b4..b6d3fb6964183 100644 --- a/lib/Sema/PreCheckTarget.cpp +++ b/lib/Sema/PreCheckTarget.cpp @@ -393,29 +393,34 @@ static bool isMemberChainTail(Expr *expr, Expr *parent, MemberChainKind kind) { } static bool isValidForwardReference(ValueDecl *D, DeclContext *DC, - ValueDecl **localDeclAfterUse) { - *localDeclAfterUse = nullptr; - - // References to variables injected by lldb are always valid. - if (isa(D) && cast(D)->isDebuggerVar()) + ValueDecl *&localDeclAfterUse, + bool &shouldUseOuterResults) { + // Only VarDecls require declaration before use. + auto *VD = dyn_cast(D); + if (!VD) return true; - // If we find something in the current context, it must be a forward - // reference, because otherwise if it was in scope, it would have - // been returned by the call to ASTScope::lookupLocalDecls() above. - if (D->getDeclContext()->isLocalContext()) { - do { - if (D->getDeclContext() == DC) { - *localDeclAfterUse = D; - return false; - } + // Non-local and variables injected by lldb are always valid. + auto *varDC = VD->getDeclContext(); + if (!varDC->isLocalContext() || VD->isDebuggerVar()) + return true; - // If we're inside of a 'defer' context, walk up to the parent - // and check again. We don't want 'defer' bodies to forward - // reference bindings in the immediate outer scope. - } while (isa(DC) && - cast(DC)->isDeferBody() && - (DC = DC->getParent())); + while (true) { + if (varDC == DC) { + localDeclAfterUse = VD; + return false; + } + if (isa(DC) || + (isa(DC) && cast(DC)->isDeferBody())) { + // If we cross a closure, don't allow falling back to an outer result if + // we have a forward reference. This preserves the behavior prior to + // diagnosing this in Sema. + if (isa(DC)) + shouldUseOuterResults = false; + DC = DC->getParent(); + continue; + } + break; } return true; } @@ -587,19 +592,20 @@ static Expr *resolveDeclRefExpr(UnresolvedDeclRefExpr *UDRE, DeclContext *DC, Lookup = TypeChecker::lookupUnqualified(DC, LookupName, Loc, lookupOptions); ValueDecl *localDeclAfterUse = nullptr; - AllDeclRefs = - findNonMembers(Lookup.innerResults(), UDRE->getRefKind(), - /*breakOnMember=*/true, ResultValues, - [&](ValueDecl *D) { - return isValidForwardReference(D, DC, &localDeclAfterUse); - }); + bool shouldUseOuterResults = true; + AllDeclRefs = findNonMembers( + Lookup.innerResults(), UDRE->getRefKind(), + /*breakOnMember=*/true, ResultValues, [&](ValueDecl *D) { + return isValidForwardReference(D, DC, localDeclAfterUse, + shouldUseOuterResults); + }); // If local declaration after use is found, check outer results for // better matching candidates. if (ResultValues.empty() && localDeclAfterUse) { auto innerDecl = localDeclAfterUse; while (localDeclAfterUse) { - if (Lookup.outerResults().empty()) { + if (!shouldUseOuterResults || Lookup.outerResults().empty()) { Context.Diags.diagnose(Loc, diag::use_local_before_declaration, Name); Context.Diags.diagnose(innerDecl, diag::decl_declared_here, localDeclAfterUse); @@ -609,12 +615,12 @@ static Expr *resolveDeclRefExpr(UnresolvedDeclRefExpr *UDRE, DeclContext *DC, Lookup.shiftDownResults(); ResultValues.clear(); localDeclAfterUse = nullptr; - AllDeclRefs = - findNonMembers(Lookup.innerResults(), UDRE->getRefKind(), - /*breakOnMember=*/true, ResultValues, - [&](ValueDecl *D) { - return isValidForwardReference(D, DC, &localDeclAfterUse); - }); + AllDeclRefs = findNonMembers( + Lookup.innerResults(), UDRE->getRefKind(), + /*breakOnMember=*/true, ResultValues, [&](ValueDecl *D) { + return isValidForwardReference(D, DC, localDeclAfterUse, + shouldUseOuterResults); + }); } } } diff --git a/test/NameLookup/name_lookup.swift b/test/NameLookup/name_lookup.swift index 0b969aab367a0..4a1cfb3d17fd2 100644 --- a/test/NameLookup/name_lookup.swift +++ b/test/NameLookup/name_lookup.swift @@ -643,8 +643,8 @@ struct PatternBindingWithTwoVars3 { var x = y, y = x } // https://github.com/apple/swift/issues/51518 do { - let closure1 = { closure2() } // expected-error {{circular reference}} expected-note {{through reference here}} - let closure2 = { closure1() } // expected-note {{through reference here}} expected-note {{through reference here}} + let closure1 = { closure2() } // expected-error {{use of local variable 'closure2' before its declaration}} + let closure2 = { closure1() } // expected-note {{'closure2' declared here}} } func color(with value: Int) -> Int { diff --git a/test/NameLookup/use_before_declaration_shadowing.swift b/test/NameLookup/use_before_declaration_shadowing.swift new file mode 100644 index 0000000000000..75c0f64c2fba2 --- /dev/null +++ b/test/NameLookup/use_before_declaration_shadowing.swift @@ -0,0 +1,73 @@ +// RUN: %target-typecheck-verify-swift +// RUN: %target-typecheck-verify-swift -parse-as-library + +func testLocal() { + // The first `y` here is considered the inner result. + do { + let y = "" + do { + let _: String = y + let y = 0 + _ = y + } + } + do { + let y = "" + do { + _ = { + let _: String = y + } + let y = 0 + _ = y + } + } + do { + let y = "" + _ = { + _ = { + let _: String = y + } + let y = 0 + _ = y + } + } + do { + let y = "" + func bar() { + _ = { + let _: String = y + } + let y = 0 + _ = y + } + } +} + +let topLevelString = "" + +func testTopLevel() { + // Here 'topLevelString' is now an outer result. + do { + let _: String = topLevelString + let topLevelString = 0 + _ = topLevelString + } + do { + _ = { + let _: String = topLevelString // expected-error {{use of local variable 'topLevelString' before its declaration}} + } + let topLevelString = 0 // expected-note {{'topLevelString' declared here}} + } + _ = { + _ = { + let _: String = topLevelString // expected-error {{use of local variable 'topLevelString' before its declaration}} + } + let topLevelString = 0 // expected-note {{'topLevelString' declared here}} + } + func bar() { + _ = { + let _: String = topLevelString // expected-error {{use of local variable 'topLevelString' before its declaration}} + } + let topLevelString = 0 // expected-note {{'topLevelString' declared here}} + } +} diff --git a/test/SILGen/capture_order.swift b/test/SILGen/capture_order.swift index 564224e5dbfd0..50d8e452cdad4 100644 --- a/test/SILGen/capture_order.swift +++ b/test/SILGen/capture_order.swift @@ -109,16 +109,6 @@ func transitiveForwardCapture3() { } } -func captureInClosure() { - let x = { (i: Int) in // expected-error {{closure captures 'currentTotal' before it is declared}} - currentTotal += i // expected-note {{captured here}} - } - - var currentTotal = 0 // expected-note {{captured value declared here}} - - _ = x -} - /// Regression tests // https://github.com/apple/swift/issues/47389 @@ -183,16 +173,6 @@ func f_57097() { // expected-warning@-1 {{variable 'r' was never mutated; consider changing to 'let' constant}} } -class class77933460 {} - -func func77933460() { - var obj: class77933460 = { obj }() - // expected-error@-1 {{closure captures 'obj' before it is declared}} - // expected-note@-2 {{captured here}} - // expected-note@-3 {{captured value declared here}} - // expected-warning@-4 {{variable 'obj' was never mutated; consider changing to 'let' constant}} -} - // MARK: - Forward Declared Lets // https://github.com/swiftlang/swift/issues/84909 @@ -208,110 +188,6 @@ func global_fwd(_ a: () -> Any) -> Any { a() } func global_gen_fwd(_ g: () -> T) -> T { g() } func global_fwd_p(_ p: () -> any P) -> any P { p() } -func forward_declared_let_captures() { - do { - let bad: Any = { bad }() - // expected-error@-1 {{closure captures 'bad' before it is declared}} - // expected-note@-2 {{captured here}} - // expected-note@-3 {{captured value declared here}} - } - - do { - func fwd(_ i: () -> Any) -> Any { i() } - let bad = fwd { bad } - // expected-error@-1 {{closure captures 'bad' before it is declared}} - // expected-note@-2 {{captured here}} - // expected-note@-3 {{captured value declared here}} - } - - do { - let bad = global_fwd { bad } - // expected-error@-1 {{closure captures 'bad' before it is declared}} - // expected-note@-2 {{captured here}} - // expected-note@-3 {{captured value declared here}} - } - - do { - let bad: Any = global_gen_fwd { bad } - // expected-error@-1 {{closure captures 'bad' before it is declared}} - // expected-note@-2 {{captured here}} - // expected-note@-3 {{captured value declared here}} - } - - do { - let bad: Any = E.static_gen_fwd { bad } - // expected-error@-1 {{closure captures 'bad' before it is declared}} - // expected-note@-2 {{captured here}} - // expected-note@-3 {{captured value declared here}} - } - - do { - let badNested: Any = global_fwd { { [badNested] in badNested }() } - // expected-error@-1 {{closure captures 'badNested' before it is declared}} - // expected-note@-2 {{captured here}} - // expected-note@-3 {{captured value declared here}} - } - - do { - let badOpt: Any? = { () -> Any? in badOpt }() - // expected-error@-1 {{closure captures 'badOpt' before it is declared}} - // expected-note@-2 {{captured here}} - // expected-note@-3 {{captured value declared here}} - } - - do { - let badTup: (Any, Any) = { (badTup.0, badTup.1) }() - // expected-error@-1 {{closure captures 'badTup' before it is declared}} - // expected-note@-2 {{captured here}} - // expected-note@-3 {{captured value declared here}} - } - - do { - let badTup: (Int, Any) = { (badTup.0, badTup.1) }() - // expected-error@-1 {{closure captures 'badTup' before it is declared}} - // expected-note@-2 {{captured here}} - // expected-note@-3 {{captured value declared here}} - } - - do { - let (badTup3, badTup4): (Any, Any) = { (badTup4, badTup3) }() - // expected-error@-1 {{closure captures 'badTup4' before it is declared}} - // expected-note@-2 {{captured here}} - // expected-note@-3 {{captured value declared here}} - // expected-error@-4 {{closure captures 'badTup3' before it is declared}} - // expected-note@-5 {{captured here}} - // expected-note@-6 {{captured value declared here}} - } - - do { - struct S { var p: Any } - let badStruct: S = { S(p: badStruct.p) }() - // expected-error@-1 {{closure captures 'badStruct' before it is declared}} - // expected-note@-2 {{captured here}} - // expected-note@-3 {{captured value declared here}} - } - - do { - enum EE { - case boring - case weird(Any) - case strange(Any) - } - - let badEnum: EE = { .weird(EE.strange(badEnum)) }() - // expected-error@-1 {{closure captures 'badEnum' before it is declared}} - // expected-note@-2 {{captured here}} - // expected-note@-3 {{captured value declared here}} - } - - do { - let badproto: any P = global_fwd_p { badproto } - // expected-error@-1 {{closure captures 'badproto' before it is declared}} - // expected-note@-2 {{captured here}} - // expected-note@-3 {{captured value declared here}} - } -} - func forward_declared_let_captures_local_fn() { do { func bad_local_f() -> Any { bad } @@ -441,10 +317,13 @@ func forward_declared_let_captures_local_fn() { } } -func forward_declared_local_lazy_captures() { - // runtime stack overflow - lazy var infiniteRecurse: Any = { infiniteRecurse }() - - // function that returns itself - lazy var hmm: () -> Any = { hmm } -} +// FIXME: Currently they crash SILGen (TypeConverter-setCaptureTypeExpansionContext-e72208.swift) +//func forward_declared_local_lazy_captures() { +// // runtime stack overflow +// var _infiniteRecurse: Any { infiniteRecurse } +// lazy var infiniteRecurse = _infiniteRecurse +// +// // function that returns itself +// func _hmm() -> Any { hmm } +// lazy var hmm = _hmm +//} diff --git a/test/SILOptimizer/closure_lifetime_fixup_undef.swift b/test/SILOptimizer/closure_lifetime_fixup_undef.swift index cf27396999f52..05f1be95a5e21 100644 --- a/test/SILOptimizer/closure_lifetime_fixup_undef.swift +++ b/test/SILOptimizer/closure_lifetime_fixup_undef.swift @@ -1,13 +1,14 @@ -// RUN: not %target-swift-frontend %s -sil-verify-all -c 2>&1 | %FileCheck %s +// RUN: %target-typecheck-verify-swift // Report the error but don't crash. -// CHECK: error: closure captures 'stringList' before it is declared class TestUndefined { private var stringList: [String]! func dontCrash(strings: [String]) { assert(stringList.allSatisfy({ $0 == stringList.first!})) + // expected-error@-1 {{use of local variable 'stringList' before its declaration}} let stringList = strings.filter({ $0 == "a" }) + // expected-note@-1 {{'stringList' declared here}} } } diff --git a/test/SILOptimizer/definite_init_closures_fail.swift b/test/SILOptimizer/definite_init_closures_fail.swift index 64d05e31c6e5d..af616b935ab08 100644 --- a/test/SILOptimizer/definite_init_closures_fail.swift +++ b/test/SILOptimizer/definite_init_closures_fail.swift @@ -65,3 +65,7 @@ struct Generic { } // expected-error {{return from initializer without initializing all stored properties}} } +func captureUninitialized() { + let fn: () -> Void // expected-note {{constant defined here}} + fn = { fn() } // expected-error {{constant 'fn' captured by a closure before being initialized}} +} diff --git a/test/Sema/diag_use_before_declaration.swift b/test/Sema/diag_use_before_declaration.swift index 216490315daf9..d07bc05d5e7f1 100644 --- a/test/Sema/diag_use_before_declaration.swift +++ b/test/Sema/diag_use_before_declaration.swift @@ -80,6 +80,141 @@ func nested_scope_3() { } } +func captureInClosure() { + let x = { (i: Int) in + currentTotal += i // expected-error {{use of local variable 'currentTotal' before its declaration}} + } + + var currentTotal = 0 // expected-note {{'currentTotal' declared here}} + + _ = x +} + +class class77933460 {} + +func func77933460() { + var obj: class77933460 = { obj }() + // expected-error@-1 {{use of local variable 'obj' before its declaration}} + // expected-note@-2 {{'obj' declared here}} +} + +protocol P {} + +enum E { + static func static_gen_fwd(_ g: () -> T) -> T { g() } +} + +func global_fwd(_ a: () -> Any) -> Any { a() } +func global_gen_fwd(_ g: () -> T) -> T { g() } +func global_fwd_p(_ p: () -> any P) -> any P { p() } + +func forward_declared_let_captures() { + do { + let bad: Any = { bad }() + // expected-error@-1 {{use of local variable 'bad' before its declaration}} + // expected-note@-2 {{'bad' declared here}} + } + + do { + func fwd(_ i: () -> Any) -> Any { i() } + let bad = fwd { bad } + // expected-error@-1 {{use of local variable 'bad' before its declaration}} + // expected-note@-2 {{'bad' declared here}} + } + + do { + let bad = global_fwd { bad } + // expected-error@-1 {{use of local variable 'bad' before its declaration}} + // expected-note@-2 {{'bad' declared here}} + } + + do { + let bad: Any = global_gen_fwd { bad } + // expected-error@-1 {{use of local variable 'bad' before its declaration}} + // expected-note@-2 {{'bad' declared here}} + } + + do { + let bad: Any = E.static_gen_fwd { bad } + // expected-error@-1 {{use of local variable 'bad' before its declaration}} + // expected-note@-2 {{'bad' declared here}} + } + + do { + let badNested: Any = global_fwd { { [badNested] in badNested }() } + // expected-error@-1 {{use of local variable 'badNested' before its declaration}} + // expected-note@-2 {{'badNested' declared here}} + } + + do { + let badOpt: Any? = { () -> Any? in badOpt }() + // expected-error@-1 {{use of local variable 'badOpt' before its declaration}} + // expected-note@-2 {{'badOpt' declared here}} + } + + do { + let badTup: (Any, Any) = { (badTup.0, badTup.1) }() + // expected-error@-1 2{{use of local variable 'badTup' before its declaration}} + // expected-note@-2 2{{'badTup' declared here}} + } + + do { + let badTup: (Int, Any) = { (badTup.0, badTup.1) }() + // expected-error@-1 2{{use of local variable 'badTup' before its declaration}} + // expected-note@-2 2{{'badTup' declared here}} + } + + do { + let (badTup3, badTup4): (Any, Any) = { (badTup4, badTup3) }() + // expected-error@-1 {{use of local variable 'badTup3' before its declaration}} + // expected-note@-2 {{'badTup3' declared here}} + // expected-error@-3 {{use of local variable 'badTup4' before its declaration}} + // expected-note@-4 {{'badTup4' declared here}} + } + + do { + struct S { var p: Any } + let badStruct: S = { S(p: badStruct.p) }() + // expected-error@-1 {{use of local variable 'badStruct' before its declaration}} + // expected-note@-2 {{'badStruct' declared here}} + } + + do { + enum EE { + case boring + case weird(Any) + case strange(Any) + } + + let badEnum: EE = { .weird(EE.strange(badEnum)) }() + // expected-error@-1 {{use of local variable 'badEnum' before its declaration}} + // expected-note@-2 {{'badEnum' declared here}} + } + + do { + let badproto: any P = global_fwd_p { badproto } + // expected-error@-1 {{use of local variable 'badproto' before its declaration}} + // expected-note@-2 {{'badproto' declared here}} + } +} + +func forward_declared_local_lazy_captures() { + lazy var infiniteRecurse: Any = { infiniteRecurse }() + // expected-error@-1 {{use of local variable 'infiniteRecurse' before its declaration}} + // expected-note@-2 {{'infiniteRecurse' declared here}} + + lazy var hmm: () -> Any = { hmm } + // expected-error@-1 {{use of local variable 'hmm' before its declaration}} + // expected-note@-2 {{'hmm' declared here}} +} + +func forward_declared_computed_locals() { + // In principle we could allow these, but it's simpler to just reject them. + let x = z // expected-error {{use of local variable 'z' before its declaration}} + let y = { z } // expected-error {{use of local variable 'z' before its declaration}} + var z: Int { 0 } // expected-note 2{{'z' declared here}} +} + //===----------------------------------------------------------------------===// // Type scope //===----------------------------------------------------------------------===// diff --git a/test/expr/closure/multi_statement.swift b/test/expr/closure/multi_statement.swift index c37d1ecb3f2f4..1a28a55ab758d 100644 --- a/test/expr/closure/multi_statement.swift +++ b/test/expr/closure/multi_statement.swift @@ -358,7 +358,8 @@ func test_no_crash_with_circular_ref_due_to_error() { // expected-error@-1 {{consecutive statements on a line must be separated by ';'}} // expected-error@-2 {{'let' cannot appear nested inside another 'var' or 'let' pattern}} // expected-error@-3 {{cannot call value of non-function type 'Int?'}} - print(next) + // expected-note@-4 {{'next' declared here}} + print(next) // expected-error {{use of local variable 'next' before its declaration}} return x } return 0 @@ -676,20 +677,26 @@ func test_recursive_var_reference_in_multistatement_closure() { func test(optionalInt: Int?, themes: MyStruct?) { takeClosure { - let int = optionalInt { // expected-error {{cannot call value of non-function type 'Int?'}} - print(int) + let int = optionalInt { + // expected-error@-1 {{cannot call value of non-function type 'Int?'}} + // expected-note@-2 {{'int' declared here}} + print(int) // expected-error {{use of local variable 'int' before its declaration}} } } takeClosure { - let theme = themes?.someMethod() { // expected-error {{extra trailing closure passed in call}} - _ = theme + let theme = themes?.someMethod() { + // expected-error@-1 {{extra trailing closure passed in call}} + // expected-note@-2 {{'theme' declared here}} + _ = theme // expected-error {{use of local variable 'theme' before its declaration}} } } takeClosure { - let theme = themes?.filter({ $0 }) { // expected-error {{value of type 'MyStruct' has no member 'filter'}} - _ = theme + let theme = themes?.filter({ $0 }) { + // expected-error@-1 {{value of type 'MyStruct' has no member 'filter'}} + // expected-note@-2 {{'theme' declared here}} + _ = theme // expected-error {{use of local variable 'theme' before its declaration}} } } } diff --git a/test/expr/expressions.swift b/test/expr/expressions.swift index b4416db19c5bd..0dfde1f9f5263 100644 --- a/test/expr/expressions.swift +++ b/test/expr/expressions.swift @@ -244,23 +244,24 @@ func test_as_2() { x as [] // expected-error {{expected element type}} {{9-9= <#type#>}} } -func test_lambda() { +func test_lambda1() { // A simple closure. var a = { (value: Int) -> () in markUsed(value+1) } // expected-warning@-1 {{initialization of variable 'a' was never used; consider replacing with assignment to '_' or removing it}} +} +func test_lambda2() { // A recursive lambda. - var fib = { (n: Int) -> Int in - // expected-warning@-1 {{variable 'fib' was never mutated; consider changing to 'let' constant}} + var fib = { (n: Int) -> Int in // expected-note 2{{'fib' declared here}} if (n < 2) { return n } - return fib(n-1)+fib(n-2) + return fib(n-1)+fib(n-2) // expected-error 2{{use of local variable 'fib' before its declaration}} } } -func test_lambda2() { +func test_lambda3() { { () -> protocol in // expected-error @-1 {{'protocol<...>' composition syntax has been removed and is not needed here}} {{11-24=Int}} // expected-error @-2 {{non-protocol, non-class type 'Int' cannot be used within a protocol-constrained type}} diff --git a/test/expr/unary/if_expr.swift b/test/expr/unary/if_expr.swift index f7775d99ff97c..761abb5a9f5df 100644 --- a/test/expr/unary/if_expr.swift +++ b/test/expr/unary/if_expr.swift @@ -1647,3 +1647,14 @@ func testCaptureList() { let _ = { [x = (if .random() { 0 } else { 1 })] in x } // expected-error@-1 {{'if' may only be used as expression in return, throw, or as the source of an assignment}} } + +func testUseBeforeDecl() throws { + let x = if .random() { // expected-note {{'x' declared here}} + print(y) // expected-error {{use of local variable 'y' before its declaration}} + let y = 0 // expected-note {{'y' declared here}} + print(x) // expected-error {{use of local variable 'x' before its declaration}} + throw Err() + } else { + 0 + } +} diff --git a/validation-test/IDE/crashers/ConstraintSystem-getTypeOfReferencePre-1a97a7.swift b/validation-test/IDE/crashers_fixed/ConstraintSystem-getTypeOfReferencePre-1a97a7.swift similarity index 76% rename from validation-test/IDE/crashers/ConstraintSystem-getTypeOfReferencePre-1a97a7.swift rename to validation-test/IDE/crashers_fixed/ConstraintSystem-getTypeOfReferencePre-1a97a7.swift index a8b10f008bb19..94fea2ca1eb63 100644 --- a/validation-test/IDE/crashers/ConstraintSystem-getTypeOfReferencePre-1a97a7.swift +++ b/validation-test/IDE/crashers_fixed/ConstraintSystem-getTypeOfReferencePre-1a97a7.swift @@ -1,5 +1,5 @@ // {"kind":"complete","original":"2d0bf5d7","signature":"swift::constraints::ConstraintSystem::getTypeOfReferencePre(swift::constraints::OverloadChoice, swift::DeclContext*, swift::constraints::ConstraintLocatorBuilder, swift::constraints::PreparedOverloadBuilder*)","signatureAssert":"Assertion failed: (!valueType->hasUnboundGenericType() && !valueType->hasTypeParameter()), function getTypeOfReferencePre"} -// RUN: not --crash %target-swift-ide-test -code-completion -batch-code-completion -skip-filecheck -code-completion-diagnostics -source-filename %s +// RUN: %target-swift-ide-test -code-completion -batch-code-completion -skip-filecheck -code-completion-diagnostics -source-filename %s { (a: Dictionary) in #^^# let b = a { diff --git a/validation-test/Sema/type_checker_crashers_fixed/rdar100753270.swift b/validation-test/Sema/type_checker_crashers_fixed/rdar100753270.swift index bdac4d0d580bc..56282f4859dad 100644 --- a/validation-test/Sema/type_checker_crashers_fixed/rdar100753270.swift +++ b/validation-test/Sema/type_checker_crashers_fixed/rdar100753270.swift @@ -20,7 +20,7 @@ struct Test { // expected-note {{to match this opening '{'}} var infos = [Info]() for (index, info) in infos.enumerated() { - let dataPerHost = Dictionary(grouping: info.data) { data in + let dataPerHost = Dictionary(grouping: info.data) { data in // expected-note {{'dataPerHost' declared here}} let location = data.location() guard let host = location.host else { return 0 @@ -30,7 +30,7 @@ struct Test { // expected-note {{to match this opening '{'}} // Missing paren! } - for _ in dataPerHost { // `dataPerHost` is inside of the closure! + for _ in dataPerHost { // expected-error {{use of local variable 'dataPerHost' before its declaration}} } } } diff --git a/validation-test/Sema/type_checker_crashers_fixed/rdar141012049.swift b/validation-test/Sema/type_checker_crashers_fixed/rdar141012049.swift index cd132c2afa4cb..60b28806acd79 100644 --- a/validation-test/Sema/type_checker_crashers_fixed/rdar141012049.swift +++ b/validation-test/Sema/type_checker_crashers_fixed/rdar141012049.swift @@ -1,12 +1,12 @@ // RUN: %target-typecheck-verify-swift -verify-ignore-unrelated func test(_ v: [Int]) { - let result = v.filter { }.flatMap(\.wrong) { + let result = v.filter { }.flatMap(\.wrong) { // expected-note {{'result' declared here}} // expected-error@-1 {{type for closure argument list expects 1 argument, which cannot be implicitly ignored}} // expected-error@-2 {{cannot convert value of type '()' to closure result type 'Bool'}} // expected-error@-3 {{value of type 'Int' has no member 'wrong'}} // expected-error@-4 {{extra trailing closure passed in call}} - print(result) + print(result) // expected-error {{use of local variable 'result' before its declaration}} } let otherResult = v.filter { _ in false }.flatMap(\.wrong, { $0 }, 42) diff --git a/validation-test/compiler_crashers/KeyPathExpr-getKeyPathType-c80788.swift b/validation-test/compiler_crashers_fixed/KeyPathExpr-getKeyPathType-c80788.swift similarity index 81% rename from validation-test/compiler_crashers/KeyPathExpr-getKeyPathType-c80788.swift rename to validation-test/compiler_crashers_fixed/KeyPathExpr-getKeyPathType-c80788.swift index 90c97a3ff0cba..0565a77fa631b 100644 --- a/validation-test/compiler_crashers/KeyPathExpr-getKeyPathType-c80788.swift +++ b/validation-test/compiler_crashers_fixed/KeyPathExpr-getKeyPathType-c80788.swift @@ -1,3 +1,3 @@ // {"kind":"typecheck","original":"6bb0b020","signature":"swift::KeyPathExpr::getKeyPathType() const","signatureAssert":"Assertion failed: (isa(Val) && \"cast() argument of incompatible type!\"), function cast"} -// RUN: not --crash %target-swift-frontend -typecheck %s +// RUN: not %target-swift-frontend -typecheck %s { let a = \ .b { c} (let c = a diff --git a/validation-test/compiler_crashers_fixed/TypeChecker-typeCheckStmtConditionElement-08b850.swift b/validation-test/compiler_crashers_fixed/TypeChecker-typeCheckStmtConditionElement-08b850.swift new file mode 100644 index 0000000000000..e8a6229bdf79a --- /dev/null +++ b/validation-test/compiler_crashers_fixed/TypeChecker-typeCheckStmtConditionElement-08b850.swift @@ -0,0 +1,11 @@ +// {"kind":"typecheck","signature":"swift::TypeChecker::typeCheckStmtConditionElement(swift::StmtConditionElement&, bool&, swift::DeclContext*)","signatureAssert":"Assertion failed: (!elt.getPattern()->hasType() && \"the pattern binding condition is already type checked\"), function typeCheckPatternBindingStmtConditionElement"} +// RUN: not %target-swift-frontend -typecheck %s +struct a { + func b() {} +} +func foo() { + _ = { c } + var d: a? + guard let d else {} + let c = d.b() +} diff --git a/validation-test/compiler_crashers/Verifier-dispatchVisitPreExpr-9ebf7c.swift b/validation-test/compiler_crashers_fixed/Verifier-dispatchVisitPreExpr-9ebf7c.swift similarity index 89% rename from validation-test/compiler_crashers/Verifier-dispatchVisitPreExpr-9ebf7c.swift rename to validation-test/compiler_crashers_fixed/Verifier-dispatchVisitPreExpr-9ebf7c.swift index c397b5c884d9d..3b9104f879181 100644 --- a/validation-test/compiler_crashers/Verifier-dispatchVisitPreExpr-9ebf7c.swift +++ b/validation-test/compiler_crashers_fixed/Verifier-dispatchVisitPreExpr-9ebf7c.swift @@ -1,5 +1,5 @@ // {"kind":"typecheck","original":"5b785ef0","signature":"swift::ASTWalker::PreWalkResult (anonymous namespace)::Verifier::dispatchVisitPreExpr(swift::OpenExistentialExpr*)","signatureAssert":"Assertion failed: (isa(Val) && \"cast() argument of incompatible type!\"), function cast"} -// RUN: not --crash %target-swift-frontend -typecheck %s +// RUN: not %target-swift-frontend -typecheck %s extension Dictionary { a(b: Sequence) { From 9336e3ee5985ccc9a434e4067d31f9e3d02c9fde Mon Sep 17 00:00:00 2001 From: Hamish Knight Date: Mon, 17 Nov 2025 17:33:46 +0000 Subject: [PATCH 2/3] [Sema] Allow falling back to outer results from closure If we encounter a variable declared after its use within a closure, we can fallback to using an outer result if present. This matches the behavior outside of a closure and generally seems more consistent with the behavior we have if we also find an inner result. rdar://163656720 --- lib/Sema/PreCheckTarget.cpp | 17 +++------- .../use_before_declaration_shadowing.swift | 33 +++++++++++++++---- .../closure_lifetime_fixup_undef.swift | 6 ++-- test/expr/closure/closures.swift | 12 +++++++ test/expr/expressions.swift | 13 ++++++-- 5 files changed, 56 insertions(+), 25 deletions(-) diff --git a/lib/Sema/PreCheckTarget.cpp b/lib/Sema/PreCheckTarget.cpp index b6d3fb6964183..2d8b894788a15 100644 --- a/lib/Sema/PreCheckTarget.cpp +++ b/lib/Sema/PreCheckTarget.cpp @@ -393,8 +393,7 @@ static bool isMemberChainTail(Expr *expr, Expr *parent, MemberChainKind kind) { } static bool isValidForwardReference(ValueDecl *D, DeclContext *DC, - ValueDecl *&localDeclAfterUse, - bool &shouldUseOuterResults) { + ValueDecl *&localDeclAfterUse) { // Only VarDecls require declaration before use. auto *VD = dyn_cast(D); if (!VD) @@ -412,11 +411,6 @@ static bool isValidForwardReference(ValueDecl *D, DeclContext *DC, } if (isa(DC) || (isa(DC) && cast(DC)->isDeferBody())) { - // If we cross a closure, don't allow falling back to an outer result if - // we have a forward reference. This preserves the behavior prior to - // diagnosing this in Sema. - if (isa(DC)) - shouldUseOuterResults = false; DC = DC->getParent(); continue; } @@ -592,12 +586,10 @@ static Expr *resolveDeclRefExpr(UnresolvedDeclRefExpr *UDRE, DeclContext *DC, Lookup = TypeChecker::lookupUnqualified(DC, LookupName, Loc, lookupOptions); ValueDecl *localDeclAfterUse = nullptr; - bool shouldUseOuterResults = true; AllDeclRefs = findNonMembers( Lookup.innerResults(), UDRE->getRefKind(), /*breakOnMember=*/true, ResultValues, [&](ValueDecl *D) { - return isValidForwardReference(D, DC, localDeclAfterUse, - shouldUseOuterResults); + return isValidForwardReference(D, DC, localDeclAfterUse); }); // If local declaration after use is found, check outer results for @@ -605,7 +597,7 @@ static Expr *resolveDeclRefExpr(UnresolvedDeclRefExpr *UDRE, DeclContext *DC, if (ResultValues.empty() && localDeclAfterUse) { auto innerDecl = localDeclAfterUse; while (localDeclAfterUse) { - if (!shouldUseOuterResults || Lookup.outerResults().empty()) { + if (Lookup.outerResults().empty()) { Context.Diags.diagnose(Loc, diag::use_local_before_declaration, Name); Context.Diags.diagnose(innerDecl, diag::decl_declared_here, localDeclAfterUse); @@ -618,8 +610,7 @@ static Expr *resolveDeclRefExpr(UnresolvedDeclRefExpr *UDRE, DeclContext *DC, AllDeclRefs = findNonMembers( Lookup.innerResults(), UDRE->getRefKind(), /*breakOnMember=*/true, ResultValues, [&](ValueDecl *D) { - return isValidForwardReference(D, DC, localDeclAfterUse, - shouldUseOuterResults); + return isValidForwardReference(D, DC, localDeclAfterUse); }); } } diff --git a/test/NameLookup/use_before_declaration_shadowing.swift b/test/NameLookup/use_before_declaration_shadowing.swift index 75c0f64c2fba2..79bef0ad5521d 100644 --- a/test/NameLookup/use_before_declaration_shadowing.swift +++ b/test/NameLookup/use_before_declaration_shadowing.swift @@ -54,20 +54,41 @@ func testTopLevel() { } do { _ = { - let _: String = topLevelString // expected-error {{use of local variable 'topLevelString' before its declaration}} + let _: String = topLevelString } - let topLevelString = 0 // expected-note {{'topLevelString' declared here}} + let topLevelString = 0 + _ = topLevelString } _ = { _ = { - let _: String = topLevelString // expected-error {{use of local variable 'topLevelString' before its declaration}} + let _: String = topLevelString } - let topLevelString = 0 // expected-note {{'topLevelString' declared here}} + let topLevelString = 0 + _ = topLevelString } func bar() { _ = { - let _: String = topLevelString // expected-error {{use of local variable 'topLevelString' before its declaration}} + let _: String = topLevelString } - let topLevelString = 0 // expected-note {{'topLevelString' declared here}} + let topLevelString = 0 + _ = topLevelString + } +} + +struct TestLocalPropertyShadowing { + var str: String + + func foo() { + { _ = str }() + let str = str + _ = str + } + func bar() { + let str = { str } + _ = str + } + func baz() { + let str = str + _ = str } } diff --git a/test/SILOptimizer/closure_lifetime_fixup_undef.swift b/test/SILOptimizer/closure_lifetime_fixup_undef.swift index 05f1be95a5e21..88cd500183762 100644 --- a/test/SILOptimizer/closure_lifetime_fixup_undef.swift +++ b/test/SILOptimizer/closure_lifetime_fixup_undef.swift @@ -1,14 +1,12 @@ -// RUN: %target-typecheck-verify-swift +// RUN: %target-swift-frontend %s -sil-verify-all -c -// Report the error but don't crash. +// Make sure we don't crash. class TestUndefined { private var stringList: [String]! func dontCrash(strings: [String]) { assert(stringList.allSatisfy({ $0 == stringList.first!})) - // expected-error@-1 {{use of local variable 'stringList' before its declaration}} let stringList = strings.filter({ $0 == "a" }) - // expected-note@-1 {{'stringList' declared here}} } } diff --git a/test/expr/closure/closures.swift b/test/expr/closure/closures.swift index b8af7e06c653c..572153f6bd1ab 100644 --- a/test/expr/closure/closures.swift +++ b/test/expr/closure/closures.swift @@ -1852,6 +1852,18 @@ class TestLazyLocal { } } +class TestLocalPropertyShadowing { + var str: String = "" + + func foo() { + let str = { str } + // expected-error@-1 {{reference to property 'str' in closure requires explicit use of 'self' to make capture semantics explicit}} + // expected-note@-2 {{reference 'self.' explicitly}} + // expected-note@-3 {{capture 'self' explicitly to enable implicit 'self' in this closure}} + _ = str + } +} + class TestExtensionOnOptionalSelf { init() {} } diff --git a/test/expr/expressions.swift b/test/expr/expressions.swift index 0dfde1f9f5263..3a0710b8f4485 100644 --- a/test/expr/expressions.swift +++ b/test/expr/expressions.swift @@ -252,12 +252,21 @@ func test_lambda1() { func test_lambda2() { // A recursive lambda. - var fib = { (n: Int) -> Int in // expected-note 2{{'fib' declared here}} + var fibLocal = { (n: Int) -> Int in // expected-note 2{{'fibLocal' declared here}} if (n < 2) { return n } - return fib(n-1)+fib(n-2) // expected-error 2{{use of local variable 'fib' before its declaration}} + return fibLocal(n-1)+fibLocal(n-2) // expected-error 2{{use of local variable 'fibLocal' before its declaration}} + } + + var fib = { (n: Int) -> Int in + if (n < 2) { + return n + } + + // These resolve to the top-level function. + return fib(n-1)+fib(n-2) } } From 2f0dee491c647ef735fce5c70aecd758770002b0 Mon Sep 17 00:00:00 2001 From: Hamish Knight Date: Mon, 17 Nov 2025 17:33:46 +0000 Subject: [PATCH 3/3] [Changelog] Add changelog entry for forward declaration rule change --- CHANGELOG.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83edf56791968..b4a845a54e6e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,69 @@ > [!NOTE] > This is in reverse chronological order, so newer entries are added to the top. -## Swift (next) +## Swift 6.3 + +* The checking for illegal forward references to local variables is now consistent regardless of + whether the reference appears in a closure. Previously the type-checker could incorrectly permit + forward references within a closure that it would reject outside of the closure, however this + is not something that can be supported in general in the type-checker. In most cases this should + have no impact since such invalid references would have already been rejected by later diagnostic + passes in the compiler. However there are a couple of cases that were previously legal that are + now illegal. + + These include: + + - `lazy` local variables with initializers that forward reference a local variable in a closure, + or local variables with attached macros that relocate the initializer into an accessor, e.g: + + ```swift + func foo() { + lazy var x = { y } // error: use of local variable 'y' before its declaration + let y = 0 + } + ``` + + ```swift + func foo() { + @LazyLikeMacro var x = { y } // error: use of local variable 'y' before its declaration + let y = 0 + } + ``` + + - Forward references to local computed variables from a closure, e.g: + + ```swift + func foo() { + var x = { y } // error: use of local variable 'y' before its declaration + var y: Int { 0 } + } + ``` + + Both cases were already invalid if there was no closure involved. These are now consistently + rejected. + + This then allows for consistent shadowing behavior inside and outside of closures, allowing the + following previously illegal case to become legal: + + ```swift + struct S { + var x: Int + func foo() { + // Already legal, both refer to `self.x` + let y = x + let x = x + + let z = x // Refers to the local var. + } + func bar() { + // Both previously illegal, now both refer to `self.x`. + let y = { x }() + let x: Int = { x }() + + let z = x // Still refers to the local var. + } + } + ``` * [SE-0491][]: You can now use a module selector to specify which module Swift should look inside to find a named declaration. A