metrale_grammar/fsm/builder/
regex_leaf.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// Leaf-FSM construction for the regex builder — turns a single
4// regex fragment (a literal run or a `[...]` character class) into
5// an FSM, and expands escape classes (`\d`, `\w`, …). Split out of
6// `regex_ir.rs` to keep each file under the 250-line cap. Port of
7// `BuildLeafFSMFromRegex` / `HandleEscapes` from `cpp/fsm_builder.cc`.
8
9use crate::fsm::fsm::Fsm;
10use crate::fsm::with_start_end::FsmWithStartEnd;
11
12/// Escape-class expansion for `\n`, `\d`, `\w`, … Returns the
13/// `(min, max)` byte ranges the escape at `regex[start]` denotes.
14pub fn handle_escapes(regex: &[u8], start: usize) -> Vec<(i32, i32)> {
15    let c = regex[start + 1];
16    match c {
17        b'n' => vec![('\n' as i32, '\n' as i32)],
18        b't' => vec![('\t' as i32, '\t' as i32)],
19        b'r' => vec![('\r' as i32, '\r' as i32)],
20        b'0' => vec![(0, 0)],
21        b's' => vec![(0, ' ' as i32)],
22        b'S' => vec![(' ' as i32 + 1, 0x00FF)],
23        b'd' => vec![('0' as i32, '9' as i32)],
24        b'D' => vec![(0, '0' as i32 - 1), ('9' as i32 + 1, 0x00FF)],
25        b'w' => vec![
26            ('0' as i32, '9' as i32),
27            ('a' as i32, 'z' as i32),
28            ('A' as i32, 'Z' as i32),
29            ('_' as i32, '_' as i32),
30        ],
31        b'W' => vec![
32            (0, '0' as i32 - 1),
33            ('9' as i32 + 1, 'A' as i32 - 1),
34            ('Z' as i32 + 1, '_' as i32 - 1),
35            ('_' as i32 + 1, 'a' as i32 - 1),
36            ('z' as i32 + 1, 0x00FF),
37        ],
38        other => vec![(other as i32, other as i32)],
39    }
40}
41
42/// Build a leaf FSM from a regex fragment that is either a plain literal
43/// (`"abx"`, possibly with escapes / `.`) or a character class `[...]`.
44/// `regex` is raw bytes — the builder is byte-wise (matches the C++).
45pub fn build_leaf_fsm(regex: &[u8]) -> FsmWithStartEnd {
46    let bytes = regex;
47    let mut result = FsmWithStartEnd::new(Fsm::with_states(0), 0, Vec::new(), true);
48
49    let is_class = !bytes.is_empty() && bytes[0] == b'[' && bytes[bytes.len() - 1] == b']';
50
51    if !is_class {
52        build_literal_leaf(&mut result, bytes);
53    } else {
54        build_class_leaf(&mut result, bytes);
55    }
56    result
57}
58
59/// Literal fragment: one state per byte, optional `\escape` / `.`.
60fn build_literal_leaf(result: &mut FsmWithStartEnd, bytes: &[u8]) {
61    result.add_state();
62    let mut i = 0;
63    while i < bytes.len() {
64        if bytes[i] != b'\\' {
65            let from = result.num_states() - 1;
66            // INTEGRATION: a UTF-8-aware leaf would decode codepoints via
67            // crate::support::encoding::decode_utf8 here; the C++ regex
68            // builder is byte-wise, so this port matches it byte-wise.
69            if bytes[i] == b'.' {
70                result.fsm_mut().add_edge(from, from + 1, 0, 0xFF);
71            } else {
72                let b = bytes[i] as i16;
73                result.fsm_mut().add_edge(from, from + 1, b, b);
74            }
75            result.add_state();
76            i += 1;
77            continue;
78        }
79        let escapes = handle_escapes(bytes, i);
80        let from = result.num_states() - 1;
81        for (lo, hi) in escapes {
82            result
83                .fsm_mut()
84                .add_edge(from, from + 1, lo as u8 as i16, hi as u8 as i16);
85        }
86        result.add_state();
87        i += 2;
88    }
89    let last = result.num_states() - 1;
90    result.add_end_state(last);
91}
92
93/// Character class `[...]`: a two-state FSM whose edges are the
94/// (possibly negated, gap-coalesced) byte ranges of the class.
95fn build_class_leaf(result: &mut FsmWithStartEnd, bytes: &[u8]) {
96    result.add_state();
97    result.add_state();
98    result.add_end_state(1);
99    let reverse = bytes.len() > 1 && bytes[1] == b'^';
100
101    let start_idx = if reverse { 2 } else { 1 };
102    let end_idx = bytes.len() - 1; // exclusive (the closing ']')
103    let mut i = start_idx;
104    while i < end_idx {
105        if bytes[i] != b'\\' {
106            let is_range = i + 2 < end_idx && bytes[i + 1] == b'-';
107            if !is_range {
108                let b = bytes[i] as i16;
109                result.fsm_mut().add_edge(0, 1, b, b);
110                i += 1;
111                continue;
112            }
113            if bytes[i + 2] != b'\\' {
114                result
115                    .fsm_mut()
116                    .add_edge(0, 1, bytes[i] as i16, bytes[i + 2] as i16);
117                i += 3;
118                continue;
119            }
120            let escaped = handle_escapes(bytes, i + 2);
121            if escaped.len() != 1 || escaped[0].0 != escaped[0].1 {
122                let b = bytes[i] as i16;
123                result.fsm_mut().add_edge(0, 1, b, b);
124                i += 1;
125                continue;
126            }
127            // C++ uses regex[0] here (a known quirk); preserved faithfully.
128            result
129                .fsm_mut()
130                .add_edge(0, 1, bytes[0] as i16, escaped[0].0 as u8 as i16);
131            i += 4;
132            continue;
133        }
134        // escape at position i
135        let escaped = handle_escapes(bytes, i);
136        i += 1;
137        if escaped.len() != 1 || escaped[0].0 != escaped[0].1 {
138            for (lo, hi) in &escaped {
139                result
140                    .fsm_mut()
141                    .add_edge(0, 1, *lo as u8 as i16, *hi as u8 as i16);
142            }
143            i += 1;
144            continue;
145        }
146        let is_range = i + 2 < end_idx && bytes[i + 1] == b'-';
147        if !is_range {
148            result
149                .fsm_mut()
150                .add_edge(0, 1, escaped[0].0 as u8 as i16, escaped[0].1 as u8 as i16);
151            i += 1;
152            continue;
153        }
154        if bytes[i + 2] != b'\\' {
155            result
156                .fsm_mut()
157                .add_edge(0, 1, escaped[0].0 as u8 as i16, bytes[i + 2] as i16);
158            i += 3;
159            continue;
160        }
161        let rhs = handle_escapes(bytes, i + 2);
162        if rhs.len() != 1 || rhs[0].0 != rhs[0].1 {
163            result
164                .fsm_mut()
165                .add_edge(0, 1, escaped[0].0 as u8 as i16, escaped[0].1 as u8 as i16);
166            i += 1;
167            continue;
168        }
169        result
170            .fsm_mut()
171            .add_edge(0, 1, escaped[0].0 as u8 as i16, rhs[0].0 as u8 as i16);
172        i += 4;
173    }
174
175    // Coalesce the bitmap of covered bytes into maximal ranges,
176    // optionally negated.
177    let mut has_edge = [false; 0x100];
178    for edge in result.fsm().edges(0) {
179        for c in edge.min..=edge.max {
180            has_edge[c as usize] = true;
181        }
182    }
183    let mut new_fsm = Fsm::with_states(2);
184    let mut last: i64 = -1;
185    let want = !reverse;
186    for c in 0..0x100usize {
187        if has_edge[c] == want {
188            if last == -1 {
189                last = c as i64;
190            }
191        } else if last != -1 {
192            new_fsm.add_edge(0, 1, last as i16, (c - 1) as i16);
193            last = -1;
194        }
195    }
196    if last != -1 {
197        new_fsm.add_edge(0, 1, last as i16, 0xFF);
198    }
199    let mut ends = vec![false; new_fsm.num_states()];
200    ends[1] = true;
201    *result = FsmWithStartEnd::new(new_fsm, 0, ends, false);
202}