metrale_grammar/fsm/builder/
trie.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// TrieFsmBuilder — builds a trie / Aho-Corasick FSM from a pattern list.
4// Port of `TrieFSMBuilder` / `TrieFSMBuilderImpl` from `cpp/fsm_builder.cc`.
5
6use ahash::AHashSet;
7use std::collections::BTreeSet;
8
9use crate::fsm::edge::{FsmEdge, cmp_edge_range, edge_type};
10use crate::fsm::fsm::{Fsm, NO_NEXT_STATE};
11use crate::fsm::with_start_end::FsmWithStartEnd;
12
13/// Result of a trie build — the FSM plus the per-pattern terminal states.
14pub struct TrieBuildResult {
15    /// The constructed FSM with start/end states.
16    pub fsm: FsmWithStartEnd,
17    /// Terminal state of each input pattern, in input order.
18    pub end_states: Vec<i32>,
19}
20
21/// Build a trie-based FSM.
22///
23/// `patterns` are inserted as a character trie. `excluded_patterns` are
24/// only honored when `add_back_edges` is true (the trie becomes an
25/// Aho-Corasick automaton with the excluded terminals pruned).
26///
27/// Returns `None` when `allow_overlap` is false and a pattern is empty
28/// or is a prefix of / shares a terminal with another pattern.
29pub fn build_trie(
30    patterns: &[&[u8]],
31    excluded_patterns: &[&[u8]],
32    allow_overlap: bool,
33    add_back_edges: bool,
34) -> Option<TrieBuildResult> {
35    let mut fsm = Fsm::with_states(1);
36    let start = 0usize;
37    let mut ends: AHashSet<i32> = AHashSet::new();
38    let mut end_states: Vec<i32> = Vec::new();
39
40    for pattern in patterns {
41        if !allow_overlap && pattern.is_empty() {
42            return None;
43        }
44        let mut current = 0i32;
45        for &ch in *pattern {
46            let ch16 = ch as i16;
47            let mut next = fsm.next_state(current as usize, ch16 as i32, edge_type::CHAR_RANGE);
48            if next == NO_NEXT_STATE {
49                next = fsm.add_state() as i32;
50                fsm.add_edge(current as usize, next as usize, ch16, ch16);
51            }
52            current = next;
53            if !allow_overlap && ends.contains(&current) {
54                return None;
55            }
56        }
57        if !allow_overlap && !fsm.edges(current as usize).is_empty() {
58            return None;
59        }
60        ends.insert(current);
61        end_states.push(current);
62    }
63
64    let mut dead_states: AHashSet<i32> = AHashSet::new();
65
66    if add_back_edges {
67        for excluded in excluded_patterns {
68            if !allow_overlap && excluded.is_empty() {
69                return None;
70            }
71            let mut current = 0i32;
72            for &ch in *excluded {
73                let ch16 = ch as i16;
74                let mut next = fsm.next_state(current as usize, ch16 as i32, edge_type::CHAR_RANGE);
75                if next == NO_NEXT_STATE {
76                    next = fsm.add_state() as i32;
77                    fsm.add_edge(current as usize, next as usize, ch16, ch16);
78                }
79                current = next;
80                if !allow_overlap && ends.contains(&current) {
81                    return None;
82                }
83            }
84            if !allow_overlap && !fsm.edges(current as usize).is_empty() {
85                return None;
86            }
87            ends.insert(current);
88            dead_states.insert(current);
89        }
90
91        add_back_edges_to_fsm(&mut fsm, start, &ends);
92
93        // Drop edges pointing at excluded (dead) terminal states.
94        if !dead_states.is_empty() {
95            for state in 0..fsm.num_states() {
96                let kept: Vec<FsmEdge> = fsm
97                    .edges(state)
98                    .iter()
99                    .copied()
100                    .filter(|e| !dead_states.contains(&e.target))
101                    .collect();
102                *fsm.edges_mut(state) = kept;
103            }
104        }
105    }
106
107    let mut is_end_state = vec![false; fsm.num_states()];
108    for &end in &ends {
109        is_end_state[end as usize] = true;
110    }
111    Some(TrieBuildResult {
112        fsm: FsmWithStartEnd::new(fsm, start, is_end_state, false),
113        end_states,
114    })
115}
116
117/// Insert Aho-Corasick back edges so a failed match restarts the search.
118fn add_back_edges_to_fsm(fsm: &mut Fsm, start: usize, ends: &AHashSet<i32>) {
119    for i in 0..fsm.num_states() {
120        if i == start || ends.contains(&(i as i32)) {
121            continue;
122        }
123        let mut edge_set: BTreeSet<OrdRange> = fsm.edges(i).iter().map(|e| OrdRange(*e)).collect();
124
125        // Step 1: inherit the start state's edges (for chars not present).
126        for root_edge in fsm.edges(start) {
127            edge_set.insert(OrdRange(*root_edge));
128        }
129        // Step 2: route every uncovered char back to `start`.
130        fill_range_gaps(&mut edge_set, start);
131
132        let new_edges: Vec<FsmEdge> = edge_set.iter().map(|o| o.0).collect();
133        *fsm.edges_mut(i) = new_edges;
134    }
135
136    // Finally, complete the start state itself with range edges.
137    let mut start_set: BTreeSet<OrdRange> = fsm.edges(start).iter().map(|e| OrdRange(*e)).collect();
138    fill_range_gaps(&mut start_set, start);
139    *fsm.edges_mut(start) = start_set.iter().map(|o| o.0).collect();
140}
141
142/// Insert edges covering every `[0,255]` char not already covered,
143/// targeting `start`. Mirrors C++ `f_add_range_edges`.
144fn fill_range_gaps(edge_set: &mut BTreeSet<OrdRange>, start: usize) {
145    // Sentinels at -1 and 256 bracket the range so gap detection is uniform.
146    edge_set.insert(OrdRange(FsmEdge::new(-1, -1, 0)));
147    edge_set.insert(OrdRange(FsmEdge::new(256, 256, 0)));
148
149    let ordered: Vec<FsmEdge> = edge_set.iter().map(|o| o.0).collect();
150    let mut to_insert: Vec<FsmEdge> = Vec::new();
151    for w in ordered.windows(2) {
152        let prev = w[0];
153        let cur = w[1];
154        if prev.max + 1 != cur.min {
155            to_insert.push(FsmEdge::new(prev.max + 1, cur.min - 1, start as i32));
156        }
157    }
158    for e in to_insert {
159        edge_set.insert(OrdRange(e));
160    }
161    edge_set.remove(&OrdRange(FsmEdge::new(-1, -1, 0)));
162    edge_set.remove(&OrdRange(FsmEdge::new(256, 256, 0)));
163}
164
165/// Wrapper that orders [`FsmEdge`] by `(min, max)` only, for the
166/// `BTreeSet` used during back-edge construction (the C++ uses a
167/// `std::set<FSMEdge, FSMEdgeRangeComparator>`).
168#[derive(Clone, Copy)]
169struct OrdRange(FsmEdge);
170impl PartialEq for OrdRange {
171    fn eq(&self, other: &Self) -> bool {
172        cmp_edge_range(&self.0, &other.0) == std::cmp::Ordering::Equal
173    }
174}
175impl Eq for OrdRange {}
176impl PartialOrd for OrdRange {
177    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
178        Some(self.cmp(other))
179    }
180}
181impl Ord for OrdRange {
182    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
183        cmp_edge_range(&self.0, &other.0)
184    }
185}
186
187#[cfg(test)]
188#[path = "trie_tests.rs"]
189mod tests;