plotnik_lib/query/
graph_qis.rs

1//! Capture scope detection: QIS and single-capture definitions.
2//!
3//! - QIS triggers when a quantified expression has ≥2 propagating captures.
4//! - Single-capture definitions unwrap to their capture's type directly.
5//!
6//! See ADR-0009 for full specification.
7
8use crate::parser::{ast, token_src};
9
10use super::{QisTrigger, Query};
11
12impl<'a> Query<'a> {
13    /// Detect capture scopes: QIS triggers and single-capture definitions.
14    ///
15    /// - QIS triggers when quantified expression has ≥2 propagating captures
16    /// - Single-capture definitions unwrap (no Field effect, type is capture's type)
17    pub(super) fn detect_capture_scopes(&mut self) {
18        let entries: Vec<_> = self
19            .symbol_table
20            .iter()
21            .map(|(n, b)| (*n, b.clone()))
22            .collect();
23        for (name, body) in &entries {
24            // Detect single-capture and multi-capture definitions
25            let captures = self.collect_propagating_captures(body);
26            if captures.len() == 1 {
27                self.single_capture_defs.insert(*name, captures[0]);
28            } else if captures.len() >= 2 {
29                self.multi_capture_defs.insert(*name);
30            }
31            // Detect QIS within this definition
32            self.detect_qis_in_expr(body);
33        }
34    }
35
36    fn detect_qis_in_expr(&mut self, expr: &ast::Expr) {
37        match expr {
38            ast::Expr::QuantifiedExpr(q) => {
39                if let Some(inner) = q.inner() {
40                    let captures = self.collect_propagating_captures(&inner);
41                    if captures.len() >= 2 {
42                        self.qis_triggers.insert(q.clone(), QisTrigger { captures });
43                    }
44                    self.detect_qis_in_expr(&inner);
45                }
46            }
47            ast::Expr::CapturedExpr(c) => {
48                // Captures on sequences/alternations absorb inner captures,
49                // but we still recurse to find nested quantifiers.
50                // Special case: captured quantifier with ≥1 nested capture needs QIS
51                // to wrap each iteration with StartObject/EndObject for proper field scoping.
52                if let Some(inner) = c.inner() {
53                    // Check if this capture wraps a quantifier with nested captures
54                    if let ast::Expr::QuantifiedExpr(q) = &inner
55                        && let Some(quant_inner) = q.inner()
56                    {
57                        let captures = self.collect_propagating_captures(&quant_inner);
58                        // Trigger QIS if there's at least 1 capture (not already covered by ≥2 rule)
59                        if !captures.is_empty() && !self.qis_triggers.contains_key(q) {
60                            self.qis_triggers.insert(q.clone(), QisTrigger { captures });
61                        }
62                    }
63                    self.detect_qis_in_expr(&inner);
64                }
65            }
66            _ => {
67                for child in expr.children() {
68                    self.detect_qis_in_expr(&child);
69                }
70            }
71        }
72    }
73
74    /// Collect captures that propagate out of an expression (not absorbed by inner scopes).
75    pub(super) fn collect_propagating_captures(&self, expr: &ast::Expr) -> Vec<&'a str> {
76        let mut captures = Vec::new();
77        self.collect_propagating_captures_impl(expr, &mut captures);
78        captures
79    }
80
81    fn collect_propagating_captures_impl(&self, expr: &ast::Expr, out: &mut Vec<&'a str>) {
82        match expr {
83            ast::Expr::CapturedExpr(c) => {
84                if let Some(name_token) = c.name() {
85                    let name = token_src(&name_token, self.source);
86                    out.push(name);
87                }
88                // Captured sequence/alternation absorbs inner captures.
89                // Captured quantifiers with nested captures also absorb (they become QIS).
90                if let Some(inner) = c.inner()
91                    && !self.is_scope_container(&inner)
92                {
93                    self.collect_propagating_captures_impl(&inner, out);
94                }
95            }
96            ast::Expr::QuantifiedExpr(q) => {
97                // Nested quantifier: its captures propagate (with modified cardinality)
98                if let Some(inner) = q.inner() {
99                    self.collect_propagating_captures_impl(&inner, out);
100                }
101            }
102            _ => {
103                for child in expr.children() {
104                    self.collect_propagating_captures_impl(&child, out);
105                }
106            }
107        }
108    }
109
110    /// Check if an expression is a scope container that absorbs inner captures.
111    /// - Sequences and alternations always absorb
112    /// - Quantifiers absorb if they have nested captures (will become QIS)
113    fn is_scope_container(&self, expr: &ast::Expr) -> bool {
114        match expr {
115            ast::Expr::SeqExpr(_) | ast::Expr::AltExpr(_) => true,
116            ast::Expr::QuantifiedExpr(q) => {
117                if let Some(inner) = q.inner() {
118                    // Quantifier with nested captures acts as scope container
119                    // (will be treated as QIS, wrapping each element in an object)
120                    let nested_captures = self.collect_propagating_captures(&inner);
121                    if !nested_captures.is_empty() {
122                        return true;
123                    }
124                    // Otherwise check if inner is a scope container
125                    self.is_scope_container(&inner)
126                } else {
127                    false
128                }
129            }
130            _ => false,
131        }
132    }
133
134    /// Check if a quantified expression triggers QIS.
135    pub fn is_qis_trigger(&self, q: &ast::QuantifiedExpr) -> bool {
136        self.qis_triggers.contains_key(q)
137    }
138
139    /// Get QIS trigger info for a quantified expression.
140    pub fn qis_trigger(&self, q: &ast::QuantifiedExpr) -> Option<&QisTrigger<'a>> {
141        self.qis_triggers.get(q)
142    }
143
144    /// Check if this capture is the single propagating capture for its definition.
145    /// Only that specific capture should unwrap (skip Field effect).
146    pub fn is_single_capture(&self, def_name: &str, capture_name: &str) -> bool {
147        self.single_capture_defs
148            .get(def_name)
149            .map(|c| *c == capture_name)
150            .unwrap_or(false)
151    }
152
153    /// Check if definition has 2+ propagating captures (needs struct wrapping).
154    pub fn is_multi_capture_def(&self, name: &str) -> bool {
155        self.multi_capture_defs.contains(name)
156    }
157}