1 // This is the backtracking matching engine. It has the same exact capability
2 // as the full NFA simulation, except it is artificially restricted to small
3 // regexes on small inputs because of its memory requirements.
4 //
5 // In particular, this is a *bounded* backtracking engine. It retains worst
6 // case linear time by keeping track of the states that it has visited (using a
7 // bitmap). Namely, once a state is visited, it is never visited again. Since a
8 // state is keyed by `(instruction index, input index)`, we have that its time
9 // complexity is `O(mn)` (i.e., linear in the size of the search text).
10 //
11 // The backtracking engine can beat out the NFA simulation on small
12 // regexes/inputs because it doesn't have to keep track of multiple copies of
13 // the capture groups. In benchmarks, the backtracking engine is roughly twice
14 // as fast as the full NFA simulation. Note though that its performance doesn't
15 // scale, even if you're willing to live with the memory requirements. Namely,
16 // the bitset has to be zeroed on each execution, which becomes quite expensive
17 // on large bitsets.
18
19 use exec::ProgramCache;
20 use input::{Input, InputAt};
21 use prog::{InstPtr, Program};
22 use re_trait::Slot;
23
24 type Bits = u32;
25
26 const BIT_SIZE: usize = 32;
27 const MAX_SIZE_BYTES: usize = 256 * (1 << 10); // 256 KB
28
29 /// Returns true iff the given regex and input should be executed by this
30 /// engine with reasonable memory usage.
should_exec(num_insts: usize, text_len: usize) -> bool31 pub fn should_exec(num_insts: usize, text_len: usize) -> bool {
32 // Total memory usage in bytes is determined by:
33 //
34 // ((len(insts) * (len(input) + 1) + bits - 1) / bits) * (size_of(u32))
35 //
36 // The actual limit picked is pretty much a heuristic.
37 // See: https://github.com/rust-lang/regex/issues/215
38 let size = ((num_insts * (text_len + 1) + BIT_SIZE - 1) / BIT_SIZE) * 4;
39 size <= MAX_SIZE_BYTES
40 }
41
42 /// A backtracking matching engine.
43 #[derive(Debug)]
44 pub struct Bounded<'a, 'm, 'r, 's, I> {
45 prog: &'r Program,
46 input: I,
47 matches: &'m mut [bool],
48 slots: &'s mut [Slot],
49 m: &'a mut Cache,
50 }
51
52 /// Shared cached state between multiple invocations of a backtracking engine
53 /// in the same thread.
54 #[derive(Clone, Debug)]
55 pub struct Cache {
56 jobs: Vec<Job>,
57 visited: Vec<Bits>,
58 }
59
60 impl Cache {
61 /// Create new empty cache for the backtracking engine.
new(_prog: &Program) -> Self62 pub fn new(_prog: &Program) -> Self {
63 Cache { jobs: vec![], visited: vec![] }
64 }
65 }
66
67 /// A job is an explicit unit of stack space in the backtracking engine.
68 ///
69 /// The "normal" representation is a single state transition, which corresponds
70 /// to an NFA state and a character in the input. However, the backtracking
71 /// engine must keep track of old capture group values. We use the explicit
72 /// stack to do it.
73 #[derive(Clone, Copy, Debug)]
74 enum Job {
75 Inst { ip: InstPtr, at: InputAt },
76 SaveRestore { slot: usize, old_pos: Option<usize> },
77 }
78
79 impl<'a, 'm, 'r, 's, I: Input> Bounded<'a, 'm, 'r, 's, I> {
80 /// Execute the backtracking matching engine.
81 ///
82 /// If there's a match, `exec` returns `true` and populates the given
83 /// captures accordingly.
exec( prog: &'r Program, cache: &ProgramCache, matches: &'m mut [bool], slots: &'s mut [Slot], input: I, start: usize, end: usize, ) -> bool84 pub fn exec(
85 prog: &'r Program,
86 cache: &ProgramCache,
87 matches: &'m mut [bool],
88 slots: &'s mut [Slot],
89 input: I,
90 start: usize,
91 end: usize,
92 ) -> bool {
93 let mut cache = cache.borrow_mut();
94 let cache = &mut cache.backtrack;
95 let start = input.at(start);
96 let mut b = Bounded {
97 prog: prog,
98 input: input,
99 matches: matches,
100 slots: slots,
101 m: cache,
102 };
103 b.exec_(start, end)
104 }
105
106 /// Clears the cache such that the backtracking engine can be executed
107 /// on some input of fixed length.
clear(&mut self)108 fn clear(&mut self) {
109 // Reset the job memory so that we start fresh.
110 self.m.jobs.clear();
111
112 // Now we need to clear the bit state set.
113 // We do this by figuring out how much space we need to keep track
114 // of the states we've visited.
115 // Then we reset all existing allocated space to 0.
116 // Finally, we request more space if we need it.
117 //
118 // This is all a little circuitous, but doing this using unchecked
119 // operations doesn't seem to have a measurable impact on performance.
120 // (Probably because backtracking is limited to such small
121 // inputs/regexes in the first place.)
122 let visited_len =
123 (self.prog.len() * (self.input.len() + 1) + BIT_SIZE - 1)
124 / BIT_SIZE;
125 self.m.visited.truncate(visited_len);
126 for v in &mut self.m.visited {
127 *v = 0;
128 }
129 if visited_len > self.m.visited.len() {
130 let len = self.m.visited.len();
131 self.m.visited.reserve_exact(visited_len - len);
132 for _ in 0..(visited_len - len) {
133 self.m.visited.push(0);
134 }
135 }
136 }
137
138 /// Start backtracking at the given position in the input, but also look
139 /// for literal prefixes.
exec_(&mut self, mut at: InputAt, end: usize) -> bool140 fn exec_(&mut self, mut at: InputAt, end: usize) -> bool {
141 self.clear();
142 // If this is an anchored regex at the beginning of the input, then
143 // we're either already done or we only need to try backtracking once.
144 if self.prog.is_anchored_start {
145 return if !at.is_start() { false } else { self.backtrack(at) };
146 }
147 let mut matched = false;
148 loop {
149 if !self.prog.prefixes.is_empty() {
150 at = match self.input.prefix_at(&self.prog.prefixes, at) {
151 None => break,
152 Some(at) => at,
153 };
154 }
155 matched = self.backtrack(at) || matched;
156 if matched && self.prog.matches.len() == 1 {
157 return true;
158 }
159 if at.pos() >= end {
160 break;
161 }
162 at = self.input.at(at.next_pos());
163 }
164 matched
165 }
166
167 /// The main backtracking loop starting at the given input position.
backtrack(&mut self, start: InputAt) -> bool168 fn backtrack(&mut self, start: InputAt) -> bool {
169 // N.B. We use an explicit stack to avoid recursion.
170 // To avoid excessive pushing and popping, most transitions are handled
171 // in the `step` helper function, which only pushes to the stack when
172 // there's a capture or a branch.
173 let mut matched = false;
174 self.m.jobs.push(Job::Inst { ip: 0, at: start });
175 while let Some(job) = self.m.jobs.pop() {
176 match job {
177 Job::Inst { ip, at } => {
178 if self.step(ip, at) {
179 // Only quit if we're matching one regex.
180 // If we're matching a regex set, then mush on and
181 // try to find other matches (if we want them).
182 if self.prog.matches.len() == 1 {
183 return true;
184 }
185 matched = true;
186 }
187 }
188 Job::SaveRestore { slot, old_pos } => {
189 if slot < self.slots.len() {
190 self.slots[slot] = old_pos;
191 }
192 }
193 }
194 }
195 matched
196 }
197
step(&mut self, mut ip: InstPtr, mut at: InputAt) -> bool198 fn step(&mut self, mut ip: InstPtr, mut at: InputAt) -> bool {
199 use prog::Inst::*;
200 loop {
201 // This loop is an optimization to avoid constantly pushing/popping
202 // from the stack. Namely, if we're pushing a job only to run it
203 // next, avoid the push and just mutate `ip` (and possibly `at`)
204 // in place.
205 if self.has_visited(ip, at) {
206 return false;
207 }
208 match self.prog[ip] {
209 Match(slot) => {
210 if slot < self.matches.len() {
211 self.matches[slot] = true;
212 }
213 return true;
214 }
215 Save(ref inst) => {
216 if let Some(&old_pos) = self.slots.get(inst.slot) {
217 // If this path doesn't work out, then we save the old
218 // capture index (if one exists) in an alternate
219 // job. If the next path fails, then the alternate
220 // job is popped and the old capture index is restored.
221 self.m.jobs.push(Job::SaveRestore {
222 slot: inst.slot,
223 old_pos: old_pos,
224 });
225 self.slots[inst.slot] = Some(at.pos());
226 }
227 ip = inst.goto;
228 }
229 Split(ref inst) => {
230 self.m.jobs.push(Job::Inst { ip: inst.goto2, at: at });
231 ip = inst.goto1;
232 }
233 EmptyLook(ref inst) => {
234 if self.input.is_empty_match(at, inst) {
235 ip = inst.goto;
236 } else {
237 return false;
238 }
239 }
240 Char(ref inst) => {
241 if inst.c == at.char() {
242 ip = inst.goto;
243 at = self.input.at(at.next_pos());
244 } else {
245 return false;
246 }
247 }
248 Ranges(ref inst) => {
249 if inst.matches(at.char()) {
250 ip = inst.goto;
251 at = self.input.at(at.next_pos());
252 } else {
253 return false;
254 }
255 }
256 Bytes(ref inst) => {
257 if let Some(b) = at.byte() {
258 if inst.matches(b) {
259 ip = inst.goto;
260 at = self.input.at(at.next_pos());
261 continue;
262 }
263 }
264 return false;
265 }
266 }
267 }
268 }
269
has_visited(&mut self, ip: InstPtr, at: InputAt) -> bool270 fn has_visited(&mut self, ip: InstPtr, at: InputAt) -> bool {
271 let k = ip * (self.input.len() + 1) + at.pos();
272 let k1 = k / BIT_SIZE;
273 let k2 = usize_to_u32(1 << (k & (BIT_SIZE - 1)));
274 if self.m.visited[k1] & k2 == 0 {
275 self.m.visited[k1] |= k2;
276 false
277 } else {
278 true
279 }
280 }
281 }
282
usize_to_u32(n: usize) -> u32283 fn usize_to_u32(n: usize) -> u32 {
284 if (n as u64) > (::std::u32::MAX as u64) {
285 panic!("BUG: {} is too big to fit into u32", n)
286 }
287 n as u32
288 }
289