plsql_ir/recursion_guard.rs
1//! Bounded-depth guard for the re-lowering walks.
2//!
3//! Both [`crate::extract_call_sites`] and
4//! [`crate::extract_table_accesses`] descend through control-flow
5//! statements (`IF` / `LOOP` / nested block) by *re-lowering* the
6//! captured raw body text and recursing into the result. This is
7//! sound only while the captured slice **strictly shrinks** on each
8//! pass. On a malformed / parser-recovered unit (e.g. an `IF` whose
9//! `END IF` is missing, so the text-scanner's `rfind("END IF")`
10//! falls back to `text.len()` and the arm body re-captures almost
11//! the whole input) the slice fails to shrink and the mutual
12//! recursion is unbounded — a real stack-overflow / SIGABRT seen on
13//! the bundled public fixture `corpus/synthetic/l1`.
14//!
15//! The guard caps recursion depth. Hitting the cap is **not**
16//! silently swallowed: the walk records that it degraded a nested
17//! body, the caller surfaces a typed
18//! [`plsql_core::UnknownReason::AnalysisRecursionLimit`] +
19//! `Diagnostic` with provenance, and the rest of the analysis
20//! continues (R13: honest degradation, never crash, never hide
21//! uncertainty — the anti-pattern is *not* to cap
22//! silently).
23
24/// Maximum re-lowering recursion depth. Real well-formed PL/SQL
25/// nests far below this (the deepest private-estate control-flow body
26/// re-lowered is < 30 levels); the cap exists only to make a
27/// non-shrinking malformed slice terminate. Chosen high enough
28/// that it never clips genuine extraction on well-formed input
29/// and low enough that 128 stack frames of the walk cannot
30/// overflow the default 8 MiB main-thread stack.
31pub const MAX_RELOWER_DEPTH: usize = 128;
32
33/// Outcome of a bounded re-lowering walk: whether the depth cap
34/// was hit (so the caller can emit a typed degradation) and how
35/// many distinct nested bodies were truncated.
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
37pub struct RecursionOutcome {
38 /// `true` iff at least one nested body was abandoned because
39 /// the depth cap was reached before it provably shrank.
40 pub limit_hit: bool,
41 /// Count of nested bodies degraded at the cap. Useful for the
42 /// completeness report / diagnostic wording.
43 pub truncated_bodies: usize,
44}
45
46impl RecursionOutcome {
47 /// Fold a child walk's outcome into this one.
48 pub fn absorb(&mut self, other: RecursionOutcome) {
49 self.limit_hit |= other.limit_hit;
50 self.truncated_bodies += other.truncated_bodies;
51 }
52
53 /// Record that a single nested body was abandoned at the cap.
54 pub fn note_truncated(&mut self) {
55 self.limit_hit = true;
56 self.truncated_bodies += 1;
57 }
58}