metrale_grammar/grammar/functor/
analyzer.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// Grammar analysis passes — port of the visitor-style functors in
4// `cpp/grammar_functor.cc`:
5//   UsedRulesAnalyzer, RuleRefGraphFinder, AllowEmptyRuleAnalyzer.
6//
7// These are read-only visitors; they collect information rather than
8// producing a new grammar, so they do not use the `GrammarMutator`
9// trait. `LookaheadAssertionAnalyzer` (which does mutate) lives in
10// `lookahead.rs`.
11
12use std::collections::{BTreeSet, HashSet, VecDeque};
13
14use crate::grammar::data::GrammarData;
15use crate::grammar::expr::GrammarExprType;
16
17/// Collect every rule reachable from the root rule (transitive closure
18/// over rule refs, repeats and tag dispatches). Used for dead-code
19/// elimination. Returns the reachable rule ids in ascending order.
20pub struct UsedRulesAnalyzer;
21
22impl UsedRulesAnalyzer {
23    /// Run the analysis.
24    pub fn apply(grammar: &GrammarData) -> Vec<i32> {
25        let mut visited: BTreeSet<i32> = BTreeSet::new();
26        let mut queue: VecDeque<i32> = VecDeque::new();
27        queue.push_back(grammar.root_rule_id());
28        while let Some(rule_id) = queue.pop_front() {
29            if !visited.insert(rule_id) {
30                continue;
31            }
32            let rule = grammar.rule(rule_id);
33            collect_refs(grammar, rule.body_expr_id, &mut queue);
34            if rule.lookahead_assertion_id != -1 {
35                collect_refs(grammar, rule.lookahead_assertion_id, &mut queue);
36            }
37        }
38        visited.into_iter().collect()
39    }
40}
41
42/// Push every rule referenced anywhere under `expr_id` onto `queue`.
43fn collect_refs(grammar: &GrammarData, expr_id: i32, queue: &mut VecDeque<i32>) {
44    let e = grammar.expr(expr_id);
45    match e.kind {
46        GrammarExprType::RuleRef | GrammarExprType::Repeat => queue.push_back(e.data[0]),
47        GrammarExprType::TagDispatch => {
48            for (_, rule_id) in grammar.tag_dispatch(expr_id).tag_rule_pairs {
49                queue.push_back(rule_id);
50            }
51        }
52        GrammarExprType::Sequence | GrammarExprType::Choices => {
53            for &child in e.data {
54                collect_refs(grammar, child, queue);
55            }
56        }
57        _ => {}
58    }
59}
60
61/// Build the *inverted* rule reference graph: `graph[referee]` is the
62/// sorted, deduplicated list of rules that reference `referee`.
63pub struct RuleRefGraphFinder;
64
65impl RuleRefGraphFinder {
66    /// Run the analysis.
67    pub fn apply(grammar: &GrammarData) -> Vec<Vec<i32>> {
68        let num_rules = grammar.num_rules() as usize;
69        let mut graph: Vec<Vec<i32>> = vec![Vec::new(); num_rules];
70        for i in 0..grammar.num_rules() {
71            let body = grammar.rule(i).body_expr_id;
72            visit_refs(grammar, body, i, &mut graph);
73        }
74        for row in &mut graph {
75            row.sort_unstable();
76            row.dedup();
77        }
78        graph
79    }
80}
81
82/// Record into `graph` every rule referenced under `expr_id`, attributed
83/// to referer `cur_rule`.
84fn visit_refs(grammar: &GrammarData, expr_id: i32, cur_rule: i32, graph: &mut [Vec<i32>]) {
85    let e = grammar.expr(expr_id);
86    match e.kind {
87        GrammarExprType::RuleRef | GrammarExprType::Repeat => {
88            graph[e.data[0] as usize].push(cur_rule);
89        }
90        GrammarExprType::TagDispatch => {
91            for (_, rule_id) in grammar.tag_dispatch(expr_id).tag_rule_pairs {
92                graph[rule_id as usize].push(cur_rule);
93            }
94        }
95        GrammarExprType::Sequence | GrammarExprType::Choices => {
96            for &child in e.data {
97                visit_refs(grammar, child, cur_rule, graph);
98            }
99        }
100        _ => {}
101    }
102}
103
104/// Analyze which rules can match the empty string. Returns the sorted
105/// list of nullable rule ids.
106pub struct AllowEmptyRuleAnalyzer;
107
108impl AllowEmptyRuleAnalyzer {
109    /// Run the analysis.
110    pub fn apply(grammar: &GrammarData) -> Vec<i32> {
111        let mut empty: HashSet<i32> = HashSet::new();
112        Self::find_explicit(grammar, &mut empty);
113        let graph = RuleRefGraphFinder::apply(grammar);
114        Self::find_indirect(grammar, &mut empty, &graph);
115        let mut result: Vec<i32> = empty.into_iter().collect();
116        result.sort_unstable();
117        result
118    }
119
120    /// Rules that explicitly allow empty: a leading EmptyStr choice, a
121    /// TagDispatch with `stop_eos`, or a sequence of only star classes.
122    fn find_explicit(grammar: &GrammarData, empty: &mut HashSet<i32>) {
123        for i in 0..grammar.num_rules() {
124            let body_id = grammar.rule(i).body_expr_id;
125            let body = grammar.expr(body_id);
126            if body.kind == GrammarExprType::TagDispatch {
127                if grammar.tag_dispatch(body_id).stop_eos {
128                    empty.insert(i);
129                }
130                continue;
131            }
132            debug_assert_eq!(body.kind, GrammarExprType::Choices);
133            if grammar.expr(body.data[0]).kind == GrammarExprType::EmptyStr {
134                empty.insert(i);
135                continue;
136            }
137            for &seq_id in body.data {
138                let seq = grammar.expr(seq_id);
139                if seq.kind == GrammarExprType::Sequence
140                    && !seq.data.is_empty()
141                    && seq
142                        .data
143                        .iter()
144                        .all(|&x| grammar.expr(x).kind == GrammarExprType::CharacterClassStar)
145                {
146                    empty.insert(i);
147                    break;
148                }
149            }
150        }
151    }
152
153    /// True if a choice/sequence expr can collapse to epsilon given the
154    /// currently-known empty rule set.
155    fn seq_is_epsilon(grammar: &GrammarData, seq_id: i32, empty: &HashSet<i32>) -> bool {
156        let seq = grammar.expr(seq_id);
157        if seq.kind == GrammarExprType::EmptyStr {
158            return true;
159        }
160        debug_assert_eq!(seq.kind, GrammarExprType::Sequence);
161        seq.data.iter().all(|&x| {
162            let elem = grammar.expr(x);
163            match elem.kind {
164                GrammarExprType::RuleRef => empty.contains(&elem.data[0]),
165                GrammarExprType::CharacterClassStar => true,
166                GrammarExprType::Repeat => empty.contains(&elem.data[0]) || elem.data[1] == 0,
167                _ => false,
168            }
169        })
170    }
171
172    /// Propagate emptiness through the reference graph (worklist).
173    fn find_indirect(grammar: &GrammarData, empty: &mut HashSet<i32>, graph: &[Vec<i32>]) {
174        let mut queue: VecDeque<i32> = empty.iter().copied().collect();
175        while let Some(rule_id) = queue.pop_front() {
176            for &referer in &graph[rule_id as usize] {
177                if empty.contains(&referer) {
178                    continue;
179                }
180                let body = grammar.expr(grammar.rule(referer).body_expr_id);
181                debug_assert_ne!(body.kind, GrammarExprType::TagDispatch);
182                let is_epsilon = body
183                    .data
184                    .iter()
185                    .any(|&seq_id| Self::seq_is_epsilon(grammar, seq_id, empty));
186                if is_epsilon {
187                    empty.insert(referer);
188                    queue.push_back(referer);
189                }
190            }
191        }
192    }
193}
194
195#[cfg(test)]
196#[path = "analyzer_tests.rs"]
197mod tests;