File: DiagnoseInfiniteRecursion.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (578 lines) | stat: -rw-r--r-- 19,494 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
//===--- DiagnoseInfiniteRecursion.swift -----------------------------------==//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import AST
import SIL

/// A diagnostic pass that detects infinite recursive function calls.
///
/// It detects simple forms of infinite recursions, like
///
///   func f() {
///     f()
///   }
///
/// and can also deal with invariant conditions, like availability checks
///
///   func f() {
///     if #available(macOS 10.4.4, *) {
///       f()
///     }
///   }
///
/// or invariant conditions due to forwarded arguments:
///
///   func f(_ x: Int) {
///     if x > 0 {
///       f(x)
///     }
///   }
///
let diagnoseInfiniteRecursion = FunctionPass(name: "diagnose-infinite-recursion") {
  (function: Function, context: FunctionPassContext) in

  // Don't rerun diagnostics on deserialized functions.
  if function.wasDeserializedCanonical {
    return
  }

  // Try with different sets of invariants. To catch all cases we would need to try all
  // parameter/memory permutations. But in practice, it's good enough to collect a reasonable set by
  // finding all recursive calls and see what arguments they are forwarding.
  guard let invariantsToTry = collectInvariantsToTry(in: function, context) else {
    // There are no recursive calls in the function at all. This is the case for most functions.
    return
  }

  for invariants in invariantsToTry {
    if analizeAndDiagnose(function, with: invariants, context) {
      return
    }
    // Try again, assuming that memory is invariant.
    if analizeAndDiagnose(function, with: invariants.withInvariantMemory, context) {
      return
    }
  }
}

/// Collect invariants with which we should try the analysis and return true if
/// there is at least one recursive call in the function.
private func collectInvariantsToTry(in function: Function, _ context: FunctionPassContext) -> [Invariants]? {
  var invariants = [Invariants]()

  // Try with no invariants.
  invariants.append(Invariants());

  var recursiveCallsFound = false

  // Scan the function for recursive calls.
  for inst in function.instructions {
    if let applySite = inst as? FullApplySite, applySite.isRecursiveCall(context) {
      recursiveCallsFound = true

      // See what parameters the recursive call is forwarding and use that as invariants.
      let inv = Invariants(fromForwardingArgumentsOf: applySite)
      if !invariants.contains(inv) {
        invariants.append(inv)
      }

      // Limit the size of the set to avoid quadratic complexity in corner
      // cases. Usually 4 invariants are more than enough.
      if invariants.count >= 4 {
        return invariants;
      }
    }
  }
  if !recursiveCallsFound {
    return nil
  }
  return invariants;
}


/// Performs the analysis and issues a warnings for recursive calls.
/// Returns true, if at least one recursive call is found.
private func analizeAndDiagnose(_ function: Function,
                                with invariants: Invariants,
                                _ context: FunctionPassContext) -> Bool
{
  var analysis = Analysis(function: function, with: invariants, context)
  defer { analysis.deinitialize() }

  analysis.compute()

  if analysis.isInfiniteRecursiveFunction {
    analysis.printWarningsForInfiniteRecursiveCalls()
    return true
  }
  return false
}

/// Describes what is expected to be invariant in an infinite recursion loop.
///
/// The dataflow analysis is done with a given set of `Invariants`. The correctness of the result (i.e.
/// no false infinite recursion reported) does _not_ depend on the chosen invariants. But it's a trade-off:
/// The more invariants we include, the more conditions might become invariant (which is good).
/// On the other hand, we have to ignore recursive calls which don't forward all invariant arguments.
///
/// We don't know in advance which invariants will yield the best result, i.e. let us detect an
/// infinite recursion. For example, in `f()` we can only detect the infinite recursion if we expect
/// that the parameter `x` is invariant.
/// ```
///   func f(_ x: Int) {
///     if x > 0 {   // an invariant condition!
///       f(x)       // the call is forwarding the argument
///     }
///   }
/// ```
/// But in `g()` we can only detect the infinite recursion if we _don't_ expect that the parameter
/// is invariant.
/// ```
///   func g(_ x: Int) {
///     if x > 0 {   // no invariant condition
///       g(x - 1)   // argument is not forwarded
///     } else {
///       g(x - 2)   // argument is not forwarded
///     }
///   }
/// ```
private struct Invariants: Equatable {

  // Support up to 32 arguments, which should be enough in real world code.
  // As the definition of invariants does not need to be accurate for correctness, it's fine to only support
  // the common cases.
  typealias ArgumentBits = UInt32

  /// A bit mask of indices of arguments which are expected to be invariant.
  /// An argument is invariant if a recursive call forwards the incoming argument.
  /// For example:
  /// ```
  ///     func f(_ x: Int, _ y: Int) {
  ///       f(x, y - 1) // The first argument is invariant, the second is not
  ///     }
  /// ```
  let arguments: ArgumentBits

  /// True, if all type arguments are invariant.
  /// In contrast to `arguments` we don't distinguish between individual type arguments but have a single
  /// flag for all type arguments.
  /// For example:
  /// ```
  ///     func f<T: P>(_ t: T.Type) {
  ///       f(T.self)   // The type argument is invariant
  ///       f(T.V.self) // The type argument is not invariant
  ///     }
  /// ```
  let typeArguments: Bool

  /// True if memory content is invariant.
  /// Like `typeArguments`, it's all or nothing. Either all memory is expected to be invariant (= never
  /// written) or not. We could use AliasAnalysis to do a more fine-grained analysis, but in mandatory
  /// optimizations we want to keep things simple.
  let memory: Bool

  // Nothing is invariant.
  init() {
    self.memory = false
    self.typeArguments = false
    self.arguments = 0
  }

  init(fromForwardingArgumentsOf recursiveApply: FullApplySite) {
    let function = recursiveApply.parentFunction

    // Check which parameters are exactly passed 1:1 to the recursive call.
    var argMask: ArgumentBits = 0
    for (argIndex, arg) in recursiveApply.arguments.enumerated() {
      if argIndex >= MemoryLayout<ArgumentBits>.size * 8 {
        break
      }
      if arg.rootValue == function.arguments[argIndex] {
        argMask |= 1 << argIndex
      }
    }
    self.arguments = argMask

    // Check if the generic type parameters are exactly passed 1:1 to the recursive call.
    self.typeArguments = recursiveApply.substitutionMap == function.forwardingSubstitutionMap

    // Assume memory is not invariant
    self.memory = false
  }

  private init(arguments: ArgumentBits, genericArguments: Bool, memory: Bool) {
    self.arguments = arguments
    self.typeArguments = genericArguments
    self.memory = memory
  }

  var withInvariantMemory: Invariants {
    Invariants(arguments: arguments, genericArguments: typeArguments, memory: true)
  }

  func isArgumentInvariant(at index: Int) -> Bool {
    if index >= MemoryLayout<ArgumentBits>.size * 8 {
      return false
    }
    return (arguments & (1 << index)) != 0
  }
}

/// Performs the analysis to detect infinite recursion loops.
///
/// The basic idea is to see if there is a path from the entry block to a function return without
/// going through an infinite recursive call.
///
private struct Analysis {

  /// All blocks which contain a recursive call.
  var haveRecursiveCall: BasicBlockSet

  /// All blocks which have a terminator with an invariant condition.
  ///
  /// Note: "invariant" means: invariant with respect to the expected invariants,
  ///       which are passed to the initializer.
  var haveInvariantCondition: BasicBlockSet

  /// All blocks from which there is a path to a function exit, without going through a recursive call.
  ///
  /// Note that if memory is expected to be invariant, all memory-writing instructions are also
  /// considered as a "function exit".
  var reachingFunctionExit: BasicBlockSet

  /// All blocks from which there is a path to a recursive call.
  var reachingRecursiveCall: BasicBlockSet

  private let function: Function
  private let invariants: Invariants
  private let context: FunctionPassContext

  init(function: Function, with invariants: Invariants, _ context: FunctionPassContext) {
    self.haveRecursiveCall = BasicBlockSet(context)
    self.haveInvariantCondition = BasicBlockSet(context)
    self.reachingFunctionExit = BasicBlockSet(context)
    self.reachingRecursiveCall = BasicBlockSet(context)
    self.function = function
    self.context = context
    self.invariants = invariants
  }

  mutating func compute() {
    computeInitialSets()
    propagateReachingRecursiveCall()
    propagateReachingFunctionExit()
  }

  mutating func deinitialize() {
    haveRecursiveCall.deinitialize()
    haveInvariantCondition.deinitialize()
    reachingFunctionExit.deinitialize()
    reachingRecursiveCall.deinitialize()
  }

  var isInfiniteRecursiveFunction: Bool { isInInfiniteRecursionLoop(function.entryBlock) }

  func printWarningsForInfiniteRecursiveCalls() {
    var worklist = BasicBlockWorklist(context)
    defer { worklist.deinitialize() }

    // Print warnings for the first recursive call(s) we reach from the entry block.
    worklist.pushIfNotVisited(function.entryBlock)
    while let block = worklist.pop() {
      if case .recursive(let apply) = block.getKind(for: invariants, context) {
        context.diagnosticEngine.diagnose(apply.location.sourceLoc, .warn_infinite_recursive_call)
      } else {
        for succ in block.successors where isInInfiniteRecursionLoop(succ) {
          worklist.pushIfNotVisited(succ)
        }
      }
    }
  }

  private mutating func computeInitialSets() {
    for block in function.blocks {
      switch block.getKind(for: invariants, context) {
      case .transparent:
        break
      case .functionExiting:
        reachingFunctionExit.insert(block)
      case .recursive:
        haveRecursiveCall.insert(block)
        reachingRecursiveCall.insert(block)
      case .invariantCondition:
        haveInvariantCondition.insert(block)
      }
    }
  }

  private mutating func propagateReachingRecursiveCall() {
    var worklist = Stack<BasicBlock>(context)
    defer { worklist.deinitialize() }

    worklist.append(contentsOf: function.blocks.filter { reachingRecursiveCall.contains($0) })

    while let block = worklist.pop() {
      for pred in block.predecessors {
        if reachingRecursiveCall.insert(pred) {
          worklist.push(pred)
        }
      }
    }
  }

  private mutating func propagateReachingFunctionExit() {
    var worklist = Stack<BasicBlock>(context)
    defer { worklist.deinitialize() }

    worklist.append(contentsOf: function.blocks.filter { reachingFunctionExit.contains($0) })

    while let block = worklist.pop() {
      for pred in block.predecessors where !reachingFunctionExit.contains(pred) {
        // Recursive calls block the propagation.
        if  haveRecursiveCall.contains(pred) {
          continue
        }

        // This is the trick for handling invariant conditions: usually `reachingFunctionExit` is propagated
        // if _any_ of the successors reach a function exit.
        // For invariant conditions, it's only propagated if _all_ successors reach a function exit.
        // If at least one of the successors is in an infinite recursion loop and this successor is
        // taken once, it will be taken forever (because the condition is invariant).
        if haveInvariantCondition.contains(pred),
           pred.successors.contains(where: isInInfiniteRecursionLoop)
        {
          continue
        }

        reachingFunctionExit.insert(pred)
        worklist.push(pred)
      }
    }
  }

  private func isInInfiniteRecursionLoop(_ block: BasicBlock) -> Bool {
    return reachingRecursiveCall.contains(block) && !reachingFunctionExit.contains(block)
  }
}

private enum BlockKind {
  case functionExiting          // the block is exiting the function (e.g. via a `return`)
  case recursive(FullApplySite) // the block contains a recursive call
  case invariantCondition       // the block's terminator has an invariant condition
  case transparent              // all other blocks
}

private extension BasicBlock {
  func getKind(for invariants: Invariants, _ context: FunctionPassContext) -> BlockKind {
    for inst in instructions {
      if let apply = inst as? FullApplySite {
        // Ignore blocks which call a @_semantics("programtermination_point").
        // This is an assert-like program termination and we explicitly don't
        // want this call to disqualify the warning for infinite recursion,
        // because they're reserved for exceptional circumstances.
        if let callee = apply.referencedFunction, callee.hasSemanticsAttribute("programtermination_point") {
          return .transparent
        }

        if apply.isRecursiveCall(context), apply.hasInvariantArguments(with: invariants) {
          return .recursive(apply)
        }
      }
      if invariants.memory, inst.mayReallyWriteToMemory {
        // If we are assuming that all memory is invariant, a memory-writing
        // instruction potentially breaks the infinite recursion loop. For the
        // sake of the analysis, it's like a function exit.
        return .functionExiting
      }
    }
    if terminator.isFunctionExiting ||
       // Also treat non-assert-like unreachables as returns, like "exit()".
        terminator is UnreachableInst
    {
      return .functionExiting
    }
    if terminator.isInvariant(accordingTo: invariants, context) {
      return .invariantCondition
    }
    return .transparent
  }
}

private extension FullApplySite {
  /// True if this apply calls its parent function.
  func isRecursiveCall(_ context: FunctionPassContext) -> Bool {
    if let calledFn = referencedFunction {
      return calledFn == parentFunction
    }

    switch callee {
    case let cmi as ClassMethodInst:
      let classType = cmi.operand.value.type.canonicalType.lookThroughMetatype
      guard let nominal = classType.nominal,
            let classDecl = nominal as? ClassDecl,
            // It's sufficient to handle class methods in the module where they are defined.
            // This aovids the need to de-serialized vtables from other modules.
           classDecl.parentModule == context.currentModuleContext,
           let vtable = context.lookupVTable(for: classDecl),
           let entry = vtable.lookup(method: cmi.member),
           entry.implementation == parentFunction
      else {
        return false
      }

      if cmi.member.calleesAreStaticallyKnowable(context),
          // The "statically knowable" check just means that we have all the
          // callee candidates available for analysis. We still need to check
          // if the current function has a known override point.
          !(cmi.member.decl as! AbstractFunctionDecl).isOverridden
      {
        return true
      }

      // Even if the method is (or could be) overridden, it's a recursive call if
      // it's called on the self argument:
      // ```
      // class X {
      //   // Even if foo() is overridden in a derived class, it'll end up in an
      //   // infinite recursion if initially called on an instance of `X`.
      //   func foo() { foo() }
      // }
      // ```
      if let selfArgument = parentFunction.selfArgument, cmi.operand.value == selfArgument {
        return true
      }
      return false

    case let wmi as WitnessMethodInst:
      if wmi.conformance.isConcrete,
         let wTable = context.lookupWitnessTable(for: wmi.conformance.rootConformance),
         let method = wTable.lookup(method: wmi.member),
         method == parentFunction
      {
        return true
      }
      return false

    default:
      return false
    }
  }

  func hasInvariantArguments(with invariants: Invariants) -> Bool {
    return arguments.enumerated().allSatisfy { (argIndex, arg) in
      !invariants.isArgumentInvariant(at: argIndex) ||
      arg.rootValue == parentFunction.arguments[argIndex]
    }
  }
}

private extension CanonicalType {
  var lookThroughMetatype: CanonicalType {
    if self.isMetatype {
      return self.instanceTypeOfMetatype
    }
    return self
  }
}

private extension Value {
  /// Recursively walks the use-def chain starting at this value and returns
  /// true if all visited values are invariant.
  func isInvariant(accordingTo invariants: Invariants, visited: inout InstructionSet) -> Bool {
    if let inst = definingInstruction {
      // Avoid exponential complexity in case a value is used by multiple
      // operands.
      if !visited.insert(inst) {
        return true
      }

      if !invariants.memory, inst.mayReadFromMemory {
        return false
      }

      if !invariants.typeArguments, inst.mayDependOnTypeParameters {
        return false
      }

      for op in inst.operands {
        if !op.value.isInvariant(accordingTo: invariants, visited: &visited) {
          return false
        }
      }
      return true
    }

    if let funcArg = self as? FunctionArgument {
      return invariants.isArgumentInvariant(at: funcArg.index)
    }
    return false
  }

  var rootValue: Value {
    switch self {
    case let ba as BeginAccessInst:
      return ba.operand.value.rootValue
    default:
      return self
    }
  }
}

private extension Instruction {
  var mayReallyWriteToMemory: Bool {
    switch self {
    case is BeginAccessInst, is EndAccessInst,
         // A `load` is defined to write memory or have side effects in two cases:
         // * We don't care about retain instructions of a `load [copy]`.
         // * We don't care about a `load [take]` because it cannot occur in an
         //   infinite recursion loop without another write (which re-initializes
         //   the memory).
         is LoadInst:
      return false
    default:
      return mayWriteToMemory
    }
  }

  var mayDependOnTypeParameters: Bool {
    switch self {
    case let bi as BuiltinInst:
      return bi.substitutionMap.replacementTypes.contains { $0.hasArchetype }
    case let mt as MetatypeInst:
      return mt.type.hasArchetype
    default:
      return false
    }
  }
}

private extension TermInst {
  func isInvariant(accordingTo invariants: Invariants, _ context: FunctionPassContext) -> Bool {
    switch self {
    case is SwitchEnumAddrInst,
         is CheckedCastAddrBranchInst:
      if !invariants.memory {
        return false
      }
      fallthrough
    case is CondBranchInst,
         is SwitchValueInst,
         is SwitchEnumInst,
         is CheckedCastBranchInst:
      var visited = InstructionSet(context)
      defer { visited.deinitialize() }
      return operands[0].value.isInvariant(accordingTo: invariants, visited: &visited)
    default:
      return false
    }
  }
}