Skip to content

Commit 5c7b790

Browse files
committed
Detect and diagnose infinitely-recursive code
Add a new warning that detects when a function will call itself recursively on all code paths. Attempts to invoke functions like this may cause unbounded stack growth at least or undefined behavior in the worst cases. The detection code is implemented as DFS for a reachable exit path in a given SILFunction.
1 parent 68be792 commit 5c7b790

File tree

8 files changed

+300
-1
lines changed

8 files changed

+300
-1
lines changed

include/swift/AST/DiagnosticsSIL.def

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ WARNING(unreachable_case,none,
243243
WARNING(switch_on_a_constant,none,
244244
"switch condition evaluates to a constant", ())
245245
NOTE(unreachable_code_note,none, "will never be executed", ())
246+
WARNING(warn_infinite_recursive_function,none,
247+
"all paths through this function will call itself", ())
246248

247249
// 'transparent' diagnostics
248250
ERROR(circular_transparent,none,

include/swift/SILOptimizer/PassManager/Passes.def

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ PASS(DefiniteInitialization, "definite-init",
118118
"Definite Initialization for Diagnostics")
119119
PASS(Devirtualizer, "devirtualizer",
120120
"Indirect Call Devirtualization")
121+
PASS(DiagnoseInfiniteRecursion, "diagnose-infinite-recursion",
122+
"Diagnose Infinitely-Recursive Code")
121123
PASS(DiagnoseStaticExclusivity, "diagnose-static-exclusivity",
122124
"Static Enforcement of Law of Exclusivity")
123125
PASS(DiagnoseUnreachable, "diagnose-unreachable",

lib/SILOptimizer/Mandatory/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ set(MANDATORY_SOURCES
66
Mandatory/DIMemoryUseCollector.cpp
77
Mandatory/DIMemoryUseCollectorOwnership.cpp
88
Mandatory/DataflowDiagnostics.cpp
9+
Mandatory/DiagnoseInfiniteRecursion.cpp
910
Mandatory/DiagnoseStaticExclusivity.cpp
1011
Mandatory/DiagnoseUnreachable.cpp
1112
Mandatory/GuaranteedARCOpts.cpp
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//==-- DiagnoseInfiniteRecursion.cpp - Find infinitely-recursive applies --==//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
//
13+
// This file implements a diagnostic pass that detects deleterious forms of
14+
// recursive functions.
15+
//
16+
//===----------------------------------------------------------------------===//
17+
18+
#define DEBUG_TYPE "infinite-recursion"
19+
#include "swift/AST/DiagnosticsSIL.h"
20+
#include "swift/AST/Expr.h"
21+
#include "swift/Parse/Lexer.h"
22+
#include "swift/SIL/CFG.h"
23+
#include "swift/SIL/SILArgument.h"
24+
#include "swift/SIL/SILInstruction.h"
25+
#include "swift/SILOptimizer/Analysis/PostOrderAnalysis.h"
26+
#include "swift/SILOptimizer/PassManager/Passes.h"
27+
#include "swift/SILOptimizer/PassManager/Transforms.h"
28+
#include "swift/SILOptimizer/Utils/Devirtualize.h"
29+
#include "llvm/ADT/PostOrderIterator.h"
30+
#include "llvm/ADT/iterator_range.h"
31+
#include "llvm/Support/Debug.h"
32+
33+
using namespace swift;
34+
35+
template<typename...T, typename...U>
36+
static void diagnose(ASTContext &Context, SourceLoc loc, Diag<T...> diag,
37+
U &&...args) {
38+
Context.Diags.diagnose(loc,
39+
diag, std::forward<U>(args)...);
40+
}
41+
42+
static bool hasRecursiveCallInPath(SILBasicBlock &Block,
43+
SILFunction *Target,
44+
ModuleDecl *TargetModule) {
45+
// Process all instructions in the block to find applies that reference
46+
// the parent function. Also looks through vtables for statically
47+
// dispatched (witness) methods.
48+
for (auto &I : Block) {
49+
auto *AI = dyn_cast<ApplyInst>(&I);
50+
if (AI && AI->getCalleeFunction() && AI->getCalleeFunction() == Target)
51+
return true;
52+
53+
if (FullApplySite FAI = FullApplySite::isa(&I)) {
54+
// Don't touch dynamic dispatch.
55+
if (isa<ObjCMethodInst>(FAI.getCallee()))
56+
continue;
57+
58+
auto &M = FAI.getModule();
59+
if (auto *CMI = dyn_cast<ClassMethodInst>(FAI.getCallee())) {
60+
auto ClassType = CMI->getOperand()->getType();
61+
62+
// FIXME: If we're not inside the module context of the method,
63+
// we may have to deserialize vtables. If the serialized tables
64+
// are damaged, the pass will crash.
65+
//
66+
// Though, this has the added bonus of not looking into vtables
67+
// outside the current module. Because we're not doing IPA, let
68+
// alone cross-module IPA, this is all well and good.
69+
auto *BGC = ClassType.getNominalOrBoundGenericNominal();
70+
if (BGC && BGC->getModuleContext() != TargetModule) {
71+
continue;
72+
}
73+
74+
auto *F = getTargetClassMethod(M, ClassType, CMI);
75+
if (F == Target)
76+
return true;
77+
78+
continue;
79+
}
80+
81+
if (auto *WMI = dyn_cast<WitnessMethodInst>(FAI.getCallee())) {
82+
SILFunction *F;
83+
SILWitnessTable *WT;
84+
85+
std::tie(F, WT) = M.lookUpFunctionInWitnessTable(
86+
WMI->getConformance(), WMI->getMember());
87+
if (F == Target)
88+
return true;
89+
90+
continue;
91+
}
92+
}
93+
}
94+
return false;
95+
}
96+
97+
static bool hasInfinitelyRecursiveApply(SILFunction &Fn,
98+
SILFunction *TargetFn) {
99+
SmallPtrSet<SILBasicBlock *, 16> Visited;
100+
SmallVector<SILBasicBlock *, 16> WorkList;
101+
// Keep track of whether we found at least one recursive path.
102+
bool foundRecursion = false;
103+
104+
auto *TargetModule = TargetFn->getModule().getSwiftModule();
105+
auto analyzeSuccessor = [&](SILBasicBlock *Succ) -> bool {
106+
if (!Visited.insert(Succ).second)
107+
return false;
108+
109+
// If the successor block contains a recursive call, end analysis there.
110+
if (!hasRecursiveCallInPath(*Succ, TargetFn, TargetModule)) {
111+
WorkList.push_back(Succ);
112+
return false;
113+
}
114+
return true;
115+
};
116+
117+
// Seed the work list with the entry block.
118+
foundRecursion |= analyzeSuccessor(Fn.getEntryBlock());
119+
120+
while (!WorkList.empty()) {
121+
SILBasicBlock *CurBlock = WorkList.pop_back_val();
122+
// Found a path to the exit node without a recursive call.
123+
if (CurBlock->getTerminator()->isFunctionExiting())
124+
return false;
125+
126+
for (SILBasicBlock *Succ : CurBlock->getSuccessorBlocks())
127+
foundRecursion |= analyzeSuccessor(Succ);
128+
}
129+
return foundRecursion;
130+
}
131+
132+
namespace {
133+
class DiagnoseInfiniteRecursion : public SILFunctionTransform {
134+
public:
135+
DiagnoseInfiniteRecursion() {}
136+
137+
private:
138+
void run() override {
139+
SILFunction *Fn = getFunction();
140+
// Don't rerun diagnostics on deserialized functions.
141+
if (Fn->wasDeserializedCanonical())
142+
return;
143+
144+
// Ignore empty functions and straight-line thunks.
145+
if (Fn->empty() || Fn->isThunk() != IsNotThunk)
146+
return;
147+
148+
// If we can't diagnose it, there's no sense analyzing it.
149+
if (!Fn->hasLocation() || Fn->getLocation().getSourceLoc().isInvalid())
150+
return;
151+
152+
if (hasInfinitelyRecursiveApply(*Fn, Fn)) {
153+
diagnose(Fn->getModule().getASTContext(),
154+
Fn->getLocation().getSourceLoc(),
155+
diag::warn_infinite_recursive_function);
156+
}
157+
}
158+
};
159+
} // end anonymous namespace
160+
161+
SILTransform *swift::createDiagnoseInfiniteRecursion() {
162+
return new DiagnoseInfiniteRecursion();
163+
}

lib/SILOptimizer/PassManager/PassPipeline.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ static void addMandatoryOptPipeline(SILPassPipelinePlan &P,
9898
P.addDiagnosticConstantPropagation();
9999
P.addGuaranteedARCOpts();
100100
P.addDiagnoseUnreachable();
101+
P.addDiagnoseInfiniteRecursion();
101102
P.addEmitDFDiagnostics();
102103
// Canonical swift requires all non cond_br critical edges to be split.
103104
P.addSplitNonCondBrCriticalEdges();
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// RUN: %target-swift-frontend -emit-sil -primary-file %s -o /dev/null -verify
2+
3+
func a() { // expected-warning {{all paths through this function will call itself}}
4+
a()
5+
}
6+
7+
func b(_ x : Int) { // expected-warning {{all paths through this function will call itself}}
8+
if x != 0 {
9+
b(x)
10+
} else {
11+
b(x+1)
12+
}
13+
}
14+
15+
func c(_ x : Int) {
16+
if x != 0 {
17+
c(5)
18+
}
19+
}
20+
21+
func d(_ x : Int) { // expected-warning {{all paths through this function will call itself}}
22+
var x = x
23+
if x != 0 {
24+
x += 1
25+
}
26+
return d(x)
27+
}
28+
29+
// Doesn't warn on mutually recursive functions
30+
31+
func e() { f() }
32+
func f() { e() }
33+
34+
func g() { // expected-warning {{all paths through this function will call itself}}
35+
while true { // expected-note {{condition always evaluates to true}}
36+
g()
37+
}
38+
39+
g() // expected-warning {{will never be executed}}
40+
}
41+
42+
func h(_ x : Int) {
43+
while (x < 5) {
44+
h(x+1)
45+
}
46+
}
47+
48+
func i(_ x : Int) { // expected-warning {{all paths through this function will call itself}}
49+
var x = x
50+
while (x < 5) {
51+
x -= 1
52+
}
53+
i(0)
54+
}
55+
56+
func j() -> Int { // expected-warning {{all paths through this function will call itself}}
57+
return 5 + j()
58+
}
59+
60+
func k() -> Any { // expected-warning {{all paths through this function will call itself}}
61+
return type(of: k())
62+
}
63+
64+
class S {
65+
convenience init(a: Int) { // expected-warning {{all paths through this function will call itself}}
66+
self.init(a: a)
67+
}
68+
init(a: Int?) {}
69+
70+
static func a() { // expected-warning {{all paths through this function will call itself}}
71+
return a()
72+
}
73+
74+
func b() { // expected-warning {{all paths through this function will call itself}}
75+
var i = 0
76+
repeat {
77+
i += 1
78+
b()
79+
} while (i > 5)
80+
}
81+
}
82+
83+
class T: S {
84+
// No warning, calls super
85+
override func b() {
86+
var i = 0
87+
repeat {
88+
i += 1
89+
super.b()
90+
} while (i > 5)
91+
}
92+
}
93+
94+
func == (l: S?, r: S?) -> Bool { // expected-warning {{all paths through this function will call itself}}
95+
if l == nil && r == nil { return true }
96+
guard let l = l, let r = r else { return false }
97+
return l === r
98+
}
99+
100+
public func == <Element>(lhs: Array<Element>, rhs: Array<Element>) -> Bool { // expected-warning {{all paths through this function will call itself}}
101+
return lhs == rhs
102+
}
103+
104+
func factorial(_ n : UInt) -> UInt { // expected-warning {{all paths through this function will call itself}}
105+
return (n != 0) ? factorial(n - 1) * n : factorial(1)
106+
}
107+
108+
func tr(_ key: String) -> String { // expected-warning {{all paths through this function will call itself}}
109+
return tr(key) ?? key // expected-warning {{left side of nil coalescing operator '??' has non-optional type}}
110+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// RUN: %target-swift-frontend -emit-sil -primary-file %s -o /dev/null -verify
2+
3+
// REQUIRES: objc_interop
4+
5+
// A negative test that the infinite recursion pass doesn't diagnose dynamic
6+
// dispatch.
7+
8+
import Foundation
9+
10+
class MyRecursiveClass {
11+
required init() {}
12+
@objc dynamic func foo() {
13+
return type(of: self).foo(self)()
14+
}
15+
16+
@objc dynamic func foo2() {
17+
return self.foo()
18+
}
19+
}
20+

test/SILOptimizer/unreachable_code.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ class r20097963MyClass {
215215
}
216216
}
217217

218-
func die() -> Never { die() }
218+
func die() -> Never { die() } // expected-warning {{all paths through this function will call itself}}
219219

220220
func testGuard(_ a : Int) {
221221
guard case 4 = a else { } // expected-error {{'guard' body must not fall through, consider using a 'return' or 'throw'}}

0 commit comments

Comments
 (0)