metrale_grammar/grammar/functor/
lookahead.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// LookaheadAssertionAnalyzer — port of `LookaheadAssertionAnalyzerImpl`
4// from `cpp/grammar_functor.cc`.
5//
6// Detects an exact lookahead assertion for each non-root rule: when a
7// rule is referenced in exactly one mid-sequence position across the
8// whole grammar, the suffix following that reference becomes the rule's
9// lookahead assertion (and is marked exact).
10//
11// Performance: per-rule lookahead facts are pre-computed in a single
12// O(N) pass (`build_rule_lookahead_info`) instead of re-scanning every
13// rule body for every rule (which is O(N^2)). Port of upstream
14// xgrammar commit 96ae88b. Behavior is unchanged: the per-rule
15// predicates and the derived suffix sequence are identical to the old
16// per-rule scan.
17
18use crate::grammar::data::GrammarData;
19use crate::grammar::expr::GrammarExprType;
20
21/// Lookahead-assertion detection pass. Operates on a *normalized*
22/// grammar (rule bodies are choices-of-sequences or TagDispatch).
23pub struct LookaheadAssertionAnalyzer;
24
25/// Pre-computed per-rule lookahead facts. Port of upstream
26/// `RuleLookaheadInfo`.
27#[derive(Debug, Clone, Default)]
28struct RuleLookaheadInfo {
29    /// The rule is the target of a tag-dispatch trigger.
30    is_triggered_by_dispatch: bool,
31    /// The rule appears as the last element of some *other* rule's
32    /// sequence (a tail reference).
33    appears_as_last_in_other_rule: bool,
34    /// How many times the rule is referenced in a non-last (mid-)
35    /// position across all sequences.
36    non_last_occurrence_count: i32,
37    /// The element ids following the *first* mid-position reference.
38    suffix_after_first_occurrence: Vec<i32>,
39}
40
41impl LookaheadAssertionAnalyzer {
42    /// Run the pass, returning the updated grammar.
43    pub fn apply(grammar: GrammarData) -> GrammarData {
44        let root = grammar.root_rule();
45        if grammar.expr(root.body_expr_id).kind == GrammarExprType::TagDispatch {
46            return grammar;
47        }
48        let mut g = grammar;
49        let root_id = g.root_rule_id();
50        let infos = build_rule_lookahead_info(&g);
51        for i in 0..g.num_rules() {
52            if i == root_id {
53                continue;
54            }
55            if g.rule(i).lookahead_assertion_id != -1 {
56                // `is_exact_lookahead` == `can_use_derived_lookahead`.
57                g.rule_mut(i).is_exact_lookahead = can_use_derived_lookahead(&infos, i);
58                continue;
59            }
60            if can_use_derived_lookahead(&infos, i) {
61                let seq = infos[i as usize].suffix_after_first_occurrence.clone();
62                let seq_id = g.append_expr(GrammarExprType::Sequence, &seq);
63                g.rule_mut(i).lookahead_assertion_id = seq_id;
64                g.rule_mut(i).is_exact_lookahead = true;
65            }
66        }
67        g
68    }
69}
70
71/// A rule can use a derived lookahead assertion iff it is not triggered
72/// by a dispatch, never appears as a tail reference, and is referenced
73/// in exactly one mid-sequence position. Port of upstream
74/// `CanUseDerivedLookahead`.
75fn can_use_derived_lookahead(infos: &[RuleLookaheadInfo], rule_id: i32) -> bool {
76    let info = &infos[rule_id as usize];
77    !info.is_triggered_by_dispatch
78        && !info.appears_as_last_in_other_rule
79        && info.non_last_occurrence_count == 1
80}
81
82/// Single O(N) pass over all rule bodies computing the lookahead facts
83/// for every rule. Port of upstream `BuildRuleLookaheadInfo`.
84fn build_rule_lookahead_info(grammar: &GrammarData) -> Vec<RuleLookaheadInfo> {
85    let mut infos: Vec<RuleLookaheadInfo> =
86        vec![RuleLookaheadInfo::default(); grammar.num_rules() as usize];
87    for i in 0..grammar.num_rules() {
88        let body_id = grammar.rule(i).body_expr_id;
89        let body = grammar.expr(body_id);
90        if body.kind == GrammarExprType::TagDispatch {
91            for (_, rid) in grammar.tag_dispatch(body_id).tag_rule_pairs {
92                infos[rid as usize].is_triggered_by_dispatch = true;
93            }
94            continue;
95        }
96        debug_assert_eq!(body.kind, GrammarExprType::Choices);
97        let choices: Vec<i32> = body.data.to_vec();
98        for seq_id in choices {
99            let seq = grammar.expr(seq_id);
100            if seq.kind != GrammarExprType::Sequence || seq.data.is_empty() {
101                continue;
102            }
103            let seq_data: Vec<i32> = seq.data.to_vec();
104            // Tail reference: last element is a RuleRef in a *different*
105            // rule's sequence.
106            if let Some(&last) = seq_data.last() {
107                let last_e = grammar.expr(last);
108                if last_e.kind == GrammarExprType::RuleRef && i != last_e.data[0] {
109                    infos[last_e.data[0] as usize].appears_as_last_in_other_rule = true;
110                }
111            }
112            // Mid-position references: every RuleRef before the last
113            // element counts toward its target's occurrence count, and
114            // the first such reference records the suffix after it.
115            for j in 0..seq_data.len().saturating_sub(1) {
116                let elem = grammar.expr(seq_data[j]);
117                if elem.kind != GrammarExprType::RuleRef {
118                    continue;
119                }
120                let target = elem.data[0] as usize;
121                let info = &mut infos[target];
122                if info.non_last_occurrence_count == 0 {
123                    info.suffix_after_first_occurrence = seq_data[j + 1..].to_vec();
124                }
125                info.non_last_occurrence_count += 1;
126            }
127        }
128    }
129    infos
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::grammar::functor::normalizer::GrammarNormalizer;
136    use crate::grammar::parse_ebnf_default;
137
138    fn normed(ebnf: &str) -> GrammarData {
139        GrammarNormalizer::apply(parse_ebnf_default(ebnf).expect("parse"))
140    }
141
142    #[test]
143    fn detects_suffix_lookahead() {
144        // `sub` is referenced mid-sequence in root, followed by "z".
145        let g = normed("root ::= sub \"z\"\nsub ::= \"a\"\n");
146        let analyzed = LookaheadAssertionAnalyzer::apply(g);
147        let sub_id = (0..analyzed.num_rules())
148            .find(|&i| analyzed.rule(i).name == "sub")
149            .unwrap();
150        assert_ne!(analyzed.rule(sub_id).lookahead_assertion_id, -1);
151        assert!(analyzed.rule(sub_id).is_exact_lookahead);
152    }
153
154    #[test]
155    fn tail_reference_no_lookahead() {
156        // `sub` is only at the tail of root — no lookahead.
157        let g = normed("root ::= \"z\" sub\nsub ::= \"a\"\n");
158        let analyzed = LookaheadAssertionAnalyzer::apply(g);
159        let sub_id = (0..analyzed.num_rules())
160            .find(|&i| analyzed.rule(i).name == "sub")
161            .unwrap();
162        assert_eq!(analyzed.rule(sub_id).lookahead_assertion_id, -1);
163    }
164
165    #[test]
166    fn tag_dispatch_root_passthrough() {
167        let g = normed("root ::= TagDispatch((\"a\", sub))\nsub ::= \"x\"\n");
168        let analyzed = LookaheadAssertionAnalyzer::apply(g.clone());
169        assert_eq!(analyzed.num_rules(), g.num_rules());
170    }
171
172    #[test]
173    fn multiple_references_no_lookahead() {
174        let g = normed("root ::= sub \"y\" | sub \"z\"\nsub ::= \"a\"\n");
175        let analyzed = LookaheadAssertionAnalyzer::apply(g);
176        let sub_id = (0..analyzed.num_rules())
177            .find(|&i| analyzed.rule(i).name == "sub")
178            .unwrap();
179        assert_eq!(analyzed.rule(sub_id).lookahead_assertion_id, -1);
180    }
181
182    #[test]
183    fn precompute_pass_matches_predicates() {
184        // One mid-position reference of `sub` → derivable lookahead.
185        let g = normed("root ::= sub \"z\"\nsub ::= \"a\"\n");
186        let infos = build_rule_lookahead_info(&g);
187        let sub_id = (0..g.num_rules())
188            .find(|&i| g.rule(i).name == "sub")
189            .unwrap();
190        assert!(can_use_derived_lookahead(&infos, sub_id));
191        assert_eq!(infos[sub_id as usize].non_last_occurrence_count, 1);
192        assert!(!infos[sub_id as usize].appears_as_last_in_other_rule);
193        assert!(!infos[sub_id as usize].is_triggered_by_dispatch);
194        assert!(
195            !infos[sub_id as usize]
196                .suffix_after_first_occurrence
197                .is_empty()
198        );
199    }
200
201    #[test]
202    fn precompute_pass_counts_dispatch_trigger() {
203        let g = normed("root ::= TagDispatch((\"a\", sub))\nsub ::= \"x\"\n");
204        let infos = build_rule_lookahead_info(&g);
205        let sub_id = (0..g.num_rules())
206            .find(|&i| g.rule(i).name == "sub")
207            .unwrap();
208        assert!(infos[sub_id as usize].is_triggered_by_dispatch);
209        assert!(!can_use_derived_lookahead(&infos, sub_id));
210    }
211}