ik_llama_cpp_2/sampling.rs
1//! Sampling — a chain-object [`LlamaSampler`] matching `llama-cpp-2`'s API,
2//! emulated over ik_llama.cpp's **legacy** `llama_sample_*` array functions.
3//!
4//! ik has no `llama_sampler_chain_*` and only `grammar`/`dry` sampler-init
5//! functions, so this reconstructs the anchor's chain model in Rust: each
6//! constructor builds a one-stage sampler, [`LlamaSampler::chain_simple`]
7//! concatenates them, and [`apply`](LlamaSampler::apply) runs the stages over a
8//! [`LlamaTokenDataArray`] in order. The legacy transforms accept a null
9//! `llama_context` (ik guards it internally), so no context is needed until the
10//! final draw; the `dist` selector draws in Rust from a seeded RNG.
11
12use std::ffi::CString;
13use std::os::raw::c_char;
14use std::ptr::NonNull;
15
16use ik_llama_cpp_sys as sys;
17
18use crate::context::LlamaContext;
19use crate::grammar::GrammarInitError;
20use crate::model::LlamaModel;
21use crate::token::LlamaToken;
22
23// The candidate array lives in `token::data_array` (matching the anchor);
24// re-exported for ergonomics / back-compat.
25pub use crate::token::data_array::LlamaTokenDataArray;
26
27/// A small deterministic RNG for the `dist` selector, so a stochastic sampler
28/// is reproducible from its seed without pulling in a `rand` dependency
29/// (xorshift64\* seeded via SplitMix64).
30#[derive(Debug, Clone)]
31struct Rng {
32 state: u64,
33}
34
35impl Rng {
36 fn new(seed: u32) -> Self {
37 let mut z = u64::from(seed).wrapping_add(0x9E37_79B9_7F4A_7C15);
38 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
39 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
40 Self {
41 state: (z ^ (z >> 31)) | 1,
42 }
43 }
44
45 fn next_u64(&mut self) -> u64 {
46 let mut x = self.state;
47 x ^= x >> 12;
48 x ^= x << 25;
49 x ^= x >> 27;
50 self.state = x;
51 x.wrapping_mul(0x2545_F491_4F6C_DD1D)
52 }
53
54 /// Uniform `f32` in `[0, 1)` (top 24 bits / 2^24).
55 fn next_f32(&mut self) -> f32 {
56 let bits = (self.next_u64() >> 40) as u32; // 24 bits
57 (bits as f32) / (1u32 << 24) as f32
58 }
59}
60
61#[derive(Debug)]
62enum Stage {
63 TopK(i32),
64 TopP {
65 p: f32,
66 min_keep: usize,
67 },
68 MinP {
69 p: f32,
70 min_keep: usize,
71 },
72 Typical {
73 p: f32,
74 min_keep: usize,
75 },
76 TopNSigma(f32),
77 TailFree {
78 z: f32,
79 min_keep: usize,
80 },
81 Temp(f32),
82 TempExt {
83 t: f32,
84 delta: f32,
85 exponent: f32,
86 },
87 Penalties {
88 last_n: i32,
89 repeat: f32,
90 freq: f32,
91 presence: f32,
92 },
93 /// Stochastic selector: softmax + a seeded weighted draw.
94 Dist(Rng),
95 /// Deterministic selector: argmax over logits.
96 Greedy,
97 /// GBNF grammar constraint (masks disallowed tokens). Owns the C grammar;
98 /// `vocab` is stashed from the model so apply/accept need no context.
99 Grammar {
100 grammar: NonNull<sys::llama_grammar>,
101 vocab: *const sys::llama_vocab,
102 },
103}
104
105impl Drop for Stage {
106 fn drop(&mut self) {
107 if let Stage::Grammar { grammar, .. } = self {
108 // SAFETY: `grammar` was produced by `llama_sampler_init_grammar[_lazy]`
109 // and is owned solely by this stage (Stage is not Clone).
110 unsafe { sys::llama_grammar_free(grammar.as_ptr()) };
111 }
112 }
113}
114
115impl Stage {
116 fn run(&mut self, arr: &mut LlamaTokenDataArray, hist: &[sys::llama_token]) {
117 let null = std::ptr::null_mut();
118 match self {
119 Stage::TopK(k) => {
120 // SAFETY: null ctx is guarded by ik; `c` describes `arr.data`.
121 unsafe {
122 arr.modify_as_c_llama_token_data_array(|c| {
123 sys::llama_sample_top_k(null, c, *k, 1)
124 });
125 }
126 }
127 Stage::TopP { p, min_keep } => unsafe {
128 arr.modify_as_c_llama_token_data_array(|c| {
129 sys::llama_sample_top_p(null, c, *p, *min_keep)
130 });
131 },
132 Stage::MinP { p, min_keep } => unsafe {
133 arr.modify_as_c_llama_token_data_array(|c| {
134 sys::llama_sample_min_p(null, c, *p, *min_keep)
135 });
136 },
137 Stage::Typical { p, min_keep } => unsafe {
138 arr.modify_as_c_llama_token_data_array(|c| {
139 sys::llama_sample_typical(null, c, *p, *min_keep)
140 });
141 },
142 Stage::TopNSigma(n) => unsafe {
143 arr.modify_as_c_llama_token_data_array(|c| {
144 sys::llama_sample_top_n_sigma(null, c, *n)
145 });
146 },
147 Stage::TailFree { z, min_keep } => unsafe {
148 arr.modify_as_c_llama_token_data_array(|c| {
149 sys::llama_sample_tail_free(null, c, *z, *min_keep)
150 });
151 },
152 Stage::Temp(t) => unsafe {
153 arr.modify_as_c_llama_token_data_array(|c| sys::llama_sample_temp(null, c, *t));
154 },
155 Stage::TempExt { t, delta, exponent } => unsafe {
156 arr.modify_as_c_llama_token_data_array(|c| {
157 sys::llama_sample_entropy(null, c, *t - *delta, *t + *delta, *exponent);
158 });
159 },
160 Stage::Penalties {
161 last_n,
162 repeat,
163 freq,
164 presence,
165 } => {
166 let use_n = (*last_n).max(0) as usize;
167 let use_n = use_n.min(hist.len());
168 let start = hist.len() - use_n;
169 let window = &hist[start..];
170 // SAFETY: null ctx guarded by ik; `window` lives for the call.
171 unsafe {
172 arr.modify_as_c_llama_token_data_array(|c| {
173 sys::llama_sample_repetition_penalties(
174 null,
175 c,
176 window.as_ptr(),
177 window.len(),
178 *repeat,
179 *freq,
180 *presence,
181 );
182 });
183 }
184 }
185 Stage::Dist(rng) => {
186 let r = rng.next_f32();
187 // SAFETY: null ctx guarded; softmax fills `p`; we read the C
188 // array within `[0, size)` and set `selected`.
189 unsafe {
190 arr.modify_as_c_llama_token_data_array(|c| {
191 // Guard BEFORE softmax: llama_sample_softmax_impl does
192 // GGML_ASSERT(size > 0) and would abort on an empty set.
193 if c.size == 0 {
194 c.selected = -1;
195 return;
196 }
197 sys::llama_sample_softmax(null, c);
198 let data = std::slice::from_raw_parts(c.data, c.size);
199 let mut cum = 0.0_f32;
200 let mut chosen = (c.size - 1) as i64;
201 for (i, d) in data.iter().enumerate() {
202 cum += d.p;
203 if r < cum {
204 chosen = i as i64;
205 break;
206 }
207 }
208 c.selected = chosen;
209 });
210 }
211 }
212 Stage::Greedy => {
213 // SAFETY: we only read `[0, size)` and set `selected`.
214 unsafe {
215 arr.modify_as_c_llama_token_data_array(|c| {
216 if c.size == 0 {
217 c.selected = -1;
218 return;
219 }
220 let data = std::slice::from_raw_parts(c.data, c.size);
221 let mut best = 0i64;
222 let mut best_logit = f32::NEG_INFINITY;
223 for (i, d) in data.iter().enumerate() {
224 if d.logit > best_logit {
225 best_logit = d.logit;
226 best = i as i64;
227 }
228 }
229 c.selected = best;
230 });
231 }
232 }
233 Stage::Grammar { grammar, vocab } => {
234 let g = grammar.as_ptr();
235 let v = *vocab;
236 // SAFETY: `g`/`v` valid; the glue applies grammar constraints to
237 // `c` in place (masks disallowed tokens), null-`smpl` internally.
238 unsafe {
239 arr.modify_as_c_llama_token_data_array(|c| {
240 sys::ik_llama_rs_grammar_apply(g, v, c);
241 });
242 }
243 }
244 }
245 }
246}
247
248/// Build the single lazy-grammar trigger *pattern* from `trigger_words`,
249/// reproducing byte-for-byte what ik's C++ `llama_sampler_init_grammar_lazy`
250/// assembles internally: `[\s\S]*?(w1|w2|…)[\s\S]*`, with each word's regex
251/// metacharacters backslash-escaped. Returns `None` when there are no words (so
252/// the caller passes no pattern — a token-only lazy trigger), matching the C++
253/// which leaves `trigger_patterns` empty in that case.
254fn build_lazy_trigger_pattern(
255 trigger_words: impl IntoIterator<Item = impl AsRef<[u8]>>,
256) -> Option<Vec<u8>> {
257 let mut words = trigger_words.into_iter();
258 let first = words.next()?;
259 let mut pattern: Vec<u8> = b"[\\s\\S]*?(".to_vec();
260 push_regex_escaped(&mut pattern, first.as_ref());
261 for word in words {
262 pattern.push(b'|');
263 push_regex_escaped(&mut pattern, word.as_ref());
264 }
265 pattern.extend_from_slice(b")[\\s\\S]*");
266 Some(pattern)
267}
268
269/// Append `word` to `out`, backslash-escaping the regex metacharacters ik's C++
270/// escapes (`std::regex special_chars = "[.^$|()*+?\\[\\]{}\\\\]"`). All are
271/// ASCII, so escaping byte-wise matches the C++ (which runs `std::regex_replace`
272/// over the raw bytes) even for multi-byte UTF-8 words.
273fn push_regex_escaped(out: &mut Vec<u8>, word: &[u8]) {
274 for &b in word {
275 if matches!(
276 b,
277 b'.' | b'^'
278 | b'$'
279 | b'|'
280 | b'('
281 | b')'
282 | b'*'
283 | b'+'
284 | b'?'
285 | b'['
286 | b']'
287 | b'{'
288 | b'}'
289 | b'\\'
290 ) {
291 out.push(b'\\');
292 }
293 out.push(b);
294 }
295}
296
297/// A sampler: an ordered chain of transform stages ending in a selector,
298/// matching `llama-cpp-2`'s `LlamaSampler`.
299///
300/// Build single stages with the constructors ([`greedy`](Self::greedy),
301/// [`temp`](Self::temp), [`top_k`](Self::top_k), …) and compose them with
302/// [`chain_simple`](Self::chain_simple). During generation, either call
303/// [`sample`](Self::sample) (which reads logits from the context) or
304/// [`apply`](Self::apply) on a candidate array you already built, then read
305/// [`LlamaTokenDataArray::selected_token`]. Call [`accept`](Self::accept) with
306/// each drawn token so stateful stages (penalties, grammar) stay in sync.
307///
308/// Not `Clone` (a grammar stage owns a C grammar object freed on drop).
309#[derive(Debug)]
310pub struct LlamaSampler {
311 stages: Vec<Stage>,
312 history: Vec<LlamaToken>,
313}
314
315impl LlamaSampler {
316 fn single(stage: Stage) -> Self {
317 Self {
318 stages: vec![stage],
319 history: Vec::new(),
320 }
321 }
322
323 /// Compose several samplers into one chain, applied left to right.
324 #[must_use]
325 pub fn chain_simple(samplers: impl IntoIterator<Item = LlamaSampler>) -> Self {
326 let mut stages = Vec::new();
327 for s in samplers {
328 stages.extend(s.stages);
329 }
330 Self {
331 stages,
332 history: Vec::new(),
333 }
334 }
335
336 /// Greedy (argmax) selector.
337 #[must_use]
338 pub fn greedy() -> Self {
339 Self::single(Stage::Greedy)
340 }
341
342 /// Stochastic distribution selector seeded by `seed` (softmax + draw).
343 #[must_use]
344 pub fn dist(seed: u32) -> Self {
345 Self::single(Stage::Dist(Rng::new(seed)))
346 }
347
348 /// Temperature scaling.
349 #[must_use]
350 pub fn temp(t: f32) -> Self {
351 Self::single(Stage::Temp(t))
352 }
353
354 /// Dynamic-temperature ("entropy") scaling over `[t-delta, t+delta]`.
355 #[must_use]
356 pub fn temp_ext(t: f32, delta: f32, exponent: f32) -> Self {
357 Self::single(Stage::TempExt { t, delta, exponent })
358 }
359
360 /// Top-k filtering.
361 #[must_use]
362 pub fn top_k(k: i32) -> Self {
363 Self::single(Stage::TopK(k))
364 }
365
366 /// Top-p (nucleus) filtering.
367 #[must_use]
368 pub fn top_p(p: f32, min_keep: usize) -> Self {
369 Self::single(Stage::TopP { p, min_keep })
370 }
371
372 /// Min-p filtering.
373 #[must_use]
374 pub fn min_p(p: f32, min_keep: usize) -> Self {
375 Self::single(Stage::MinP { p, min_keep })
376 }
377
378 /// Locally-typical filtering.
379 #[must_use]
380 pub fn typical(p: f32, min_keep: usize) -> Self {
381 Self::single(Stage::Typical { p, min_keep })
382 }
383
384 /// Top-nσ filtering.
385 #[must_use]
386 pub fn top_n_sigma(n: f32) -> Self {
387 Self::single(Stage::TopNSigma(n))
388 }
389
390 /// Tail-free filtering.
391 #[must_use]
392 pub fn tail_free(z: f32, min_keep: usize) -> Self {
393 Self::single(Stage::TailFree { z, min_keep })
394 }
395
396 /// A GBNF grammar constraint stage.
397 ///
398 /// `grammar_str` is GBNF source, `root` the start symbol (usually `"root"`).
399 /// The stage masks tokens the grammar cannot currently accept; call
400 /// [`accept`](Self::accept) with each drawn token to advance grammar state.
401 ///
402 /// # Safety contract
403 ///
404 /// The returned sampler stashes a pointer into `model`'s vocab, so **`model`
405 /// must outlive this sampler** — using it after the model is dropped is
406 /// undefined behavior. This is not encoded in the type (the sampler stays
407 /// `'static`) so that it drops in for `llama-cpp-2`, which makes the same
408 /// choice; keep the model alive at least as long as any sampler built from
409 /// it (a `'static` model trivially satisfies this). Prefer
410 /// [`crate::grammar::LlamaGrammar`] if you want the model lifetime enforced.
411 ///
412 /// # Errors
413 ///
414 /// [`GrammarInitError::Nul`] on an interior NUL byte; [`GrammarInitError::Parse`]
415 /// if the GBNF fails to parse.
416 pub fn grammar(
417 model: &LlamaModel,
418 grammar_str: &str,
419 root: &str,
420 ) -> Result<Self, GrammarInitError> {
421 let c_gbnf = CString::new(grammar_str)?;
422 let c_root = CString::new(root)?;
423 // SAFETY: model is valid; vocab is const and lives with the model.
424 let vocab = unsafe { sys::llama_model_get_vocab(model.model.as_ptr()) };
425 // SAFETY: valid vocab + two valid C strings (copied C-side). Null = parse fail.
426 let raw =
427 unsafe { sys::llama_sampler_init_grammar(vocab, c_gbnf.as_ptr(), c_root.as_ptr()) };
428 let grammar = NonNull::new(raw).ok_or(GrammarInitError::Parse)?;
429 Ok(Self::single(Stage::Grammar { grammar, vocab }))
430 }
431
432 /// A lazy GBNF grammar constraint: it stays inert until one of
433 /// `trigger_words` / `trigger_tokens` appears, then constrains generation.
434 ///
435 /// # Errors
436 ///
437 /// [`GrammarInitError::Nul`] on an interior NUL byte in the grammar, root, or
438 /// a trigger word; [`GrammarInitError::Parse`] if the GBNF fails to parse.
439 pub fn grammar_lazy(
440 model: &LlamaModel,
441 grammar_str: &str,
442 root: &str,
443 trigger_words: impl IntoIterator<Item = impl AsRef<[u8]>>,
444 trigger_tokens: &[LlamaToken],
445 ) -> Result<Self, GrammarInitError> {
446 let c_gbnf = CString::new(grammar_str)?;
447 let c_root = CString::new(root)?;
448 // Build the trigger *pattern* in Rust and call `..._lazy_patterns`, rather
449 // than passing `trigger_words` to the deprecated `..._lazy`. The words path
450 // has a use-after-scope bug in ik's C++: it assembles the pattern in a local
451 // `std::string`, stashes a pointer to that local, then reads the pointer
452 // *after* the string has dropped (llama-sampling.cpp:1372-1387). The
453 // `_patterns` entry point copies each pattern into an owned `std::string`
454 // immediately (llama-grammar.cpp:1318), so a live-for-the-call pattern is
455 // safe. We reproduce the exact pattern the C++ would have built.
456 let c_pattern = match build_lazy_trigger_pattern(trigger_words) {
457 Some(pattern) => Some(CString::new(pattern)?),
458 None => None,
459 };
460 // 0 or 1 pattern pointers, borrowing `c_pattern` (alive across the call).
461 let mut pattern_ptrs: Vec<*const c_char> = c_pattern.iter().map(|c| c.as_ptr()).collect();
462 // Match the original semantics when there are no words: (null, 0).
463 let (pat_ptr, pat_len) = if pattern_ptrs.is_empty() {
464 (std::ptr::null_mut::<*const c_char>(), 0usize)
465 } else {
466 (pattern_ptrs.as_mut_ptr(), pattern_ptrs.len())
467 };
468 let tokens: Vec<sys::llama_token> = trigger_tokens.iter().map(|t| t.0).collect();
469 // SAFETY: model valid → const vocab.
470 let vocab = unsafe { sys::llama_model_get_vocab(model.model.as_ptr()) };
471 // SAFETY: valid vocab + C strings + trigger arrays live for the call
472 // (ik copies what it needs). Null = parse failure.
473 let raw = unsafe {
474 sys::llama_sampler_init_grammar_lazy_patterns(
475 vocab,
476 c_gbnf.as_ptr(),
477 c_root.as_ptr(),
478 pat_ptr,
479 pat_len,
480 tokens.as_ptr(),
481 tokens.len(),
482 )
483 };
484 let grammar = NonNull::new(raw).ok_or(GrammarInitError::Parse)?;
485 Ok(Self::single(Stage::Grammar { grammar, vocab }))
486 }
487
488 /// Repetition / frequency / presence penalties over the accepted history.
489 #[must_use]
490 pub fn penalties(
491 penalty_last_n: i32,
492 penalty_repeat: f32,
493 penalty_freq: f32,
494 penalty_present: f32,
495 ) -> Self {
496 Self::single(Stage::Penalties {
497 last_n: penalty_last_n,
498 repeat: penalty_repeat,
499 freq: penalty_freq,
500 presence: penalty_present,
501 })
502 }
503
504 /// Run every stage over `arr` in order (transforms + selector). Read the
505 /// result with [`LlamaTokenDataArray::selected_token`].
506 ///
507 /// A no-op on an empty candidate array (leaving `selected = None`): ik's
508 /// legacy samplers `GGML_ASSERT(size > 0)` and would abort the process, so
509 /// this safe wrapper must not forward an empty set into them.
510 pub fn apply(&mut self, arr: &mut LlamaTokenDataArray) {
511 if arr.data.is_empty() {
512 return;
513 }
514 let hist: Vec<sys::llama_token> = self.history.iter().map(|t| t.0).collect();
515 for stage in &mut self.stages {
516 stage.run(arr, &hist);
517 }
518 }
519
520 /// Record `token`: appends to the history (penalties stage) and advances any
521 /// grammar stage.
522 ///
523 /// If a grammar stage is present, only feed tokens the grammar permits at the
524 /// current position (i.e. tokens surviving [`apply`](Self::apply)). Accepting
525 /// a grammar-disallowed end-of-generation token aborts the process (ik's
526 /// `llama_grammar_accept_impl` treats it as fatal).
527 pub fn accept(&mut self, token: LlamaToken) {
528 self.history.push(token);
529 for stage in &mut self.stages {
530 if let Stage::Grammar { grammar, vocab } = stage {
531 // SAFETY: `grammar`/`vocab` valid; the glue advances grammar
532 // state with null `smpl` internally.
533 unsafe { sys::ik_llama_rs_grammar_accept(grammar.as_ptr(), *vocab, token.0) };
534 }
535 }
536 }
537
538 /// Clear the accepted-token history.
539 pub fn reset(&mut self) {
540 self.history.clear();
541 }
542
543 /// Build the candidate array from the context's logits at batch index `idx`,
544 /// run the chain, and return the selected token (argmax fallback if no
545 /// selector ran).
546 pub fn sample(&mut self, ctx: &LlamaContext, idx: i32) -> LlamaToken {
547 let mut arr = ctx.token_data_array_ith(idx);
548 self.apply(&mut arr);
549 arr.selected_token().unwrap_or_else(|| {
550 arr.data
551 .iter()
552 .max_by(|a, b| {
553 a.logit()
554 .partial_cmp(&b.logit())
555 .unwrap_or(std::cmp::Ordering::Equal)
556 })
557 .map_or(LlamaToken(0), crate::token::data::LlamaTokenData::id)
558 })
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565 use crate::token::data::LlamaTokenData;
566
567 #[test]
568 fn greedy_selects_argmax() {
569 let mut arr = LlamaTokenDataArray::from_logits(&[0.1, 2.5, -1.0, 2.4, 0.0]);
570 let mut s = LlamaSampler::greedy();
571 s.apply(&mut arr);
572 assert_eq!(s.stages.len(), 1);
573 assert_eq!(arr.selected_token(), Some(LlamaToken(1)));
574 }
575
576 #[test]
577 fn dist_is_deterministic_for_a_seed() {
578 let logits = [0.2_f32, 1.0, 0.5, 3.0, -0.5];
579 let mut a = LlamaSampler::dist(1234);
580 let mut b = LlamaSampler::dist(1234);
581 let mut arr_a = LlamaTokenDataArray::from_logits(&logits);
582 let mut arr_b = LlamaTokenDataArray::from_logits(&logits);
583 a.apply(&mut arr_a);
584 b.apply(&mut arr_b);
585 assert!(arr_a.selected_token().is_some());
586 assert_eq!(arr_a.selected_token(), arr_b.selected_token());
587 }
588
589 #[test]
590 fn apply_on_empty_array_is_noop_not_abort() {
591 // ik's legacy samplers GGML_ASSERT(size > 0); apply must short-circuit
592 // an empty candidate set rather than forward it (which would SIGABRT).
593 let mut arr = LlamaTokenDataArray::new(Vec::new(), false);
594 let mut s = LlamaSampler::chain_simple([
595 LlamaSampler::top_k(3),
596 LlamaSampler::top_p(0.9, 1),
597 LlamaSampler::temp(0.8),
598 LlamaSampler::dist(1),
599 ]);
600 s.apply(&mut arr);
601 assert_eq!(arr.selected_token(), None);
602 }
603
604 #[test]
605 fn chain_composes_stages() {
606 let s = LlamaSampler::chain_simple([
607 LlamaSampler::top_k(3),
608 LlamaSampler::temp(0.8),
609 LlamaSampler::greedy(),
610 ]);
611 assert_eq!(s.stages.len(), 3);
612 // history + accept
613 let mut s = s;
614 s.accept(LlamaTokenData::new(LlamaToken(5), 0.0, 0.0).id());
615 assert_eq!(s.history, vec![LlamaToken(5)]);
616 }
617
618 #[test]
619 fn lazy_trigger_pattern_none_when_no_words() {
620 let empty: [&[u8]; 0] = [];
621 assert_eq!(build_lazy_trigger_pattern(empty), None);
622 }
623
624 #[test]
625 fn lazy_trigger_pattern_wraps_and_joins() {
626 // Two plain words -> `[\s\S]*?(<tool>|call)[\s\S]*` (byte-for-byte the
627 // string ik's C++ would have assembled from these trigger words).
628 let pat = build_lazy_trigger_pattern([b"<tool>".as_slice(), b"call".as_slice()])
629 .expect("some pattern");
630 assert_eq!(pat, br"[\s\S]*?(<tool>|call)[\s\S]*".to_vec());
631 }
632
633 #[test]
634 fn lazy_trigger_pattern_escapes_metacharacters() {
635 // Every regex metacharacter ik escapes must be backslash-prefixed so the
636 // trigger matches the literal word, not a regex.
637 let pat =
638 build_lazy_trigger_pattern([br".^$|()*+?[]{}\".as_slice()]).expect("some pattern");
639 assert_eq!(
640 pat,
641 br"[\s\S]*?(\.\^\$\|\(\)\*\+\?\[\]\{\}\\)[\s\S]*".to_vec()
642 );
643 }
644
645 #[test]
646 fn lazy_trigger_pattern_preserves_non_meta_utf8() {
647 // Multi-byte UTF-8 passes through unescaped (its bytes are all >= 0x80,
648 // none collide with the ASCII metacharacters).
649 let pat = build_lazy_trigger_pattern(["café".as_bytes()]).expect("some pattern");
650 let mut expected = br"[\s\S]*?(".to_vec();
651 expected.extend_from_slice("café".as_bytes());
652 expected.extend_from_slice(br")[\s\S]*");
653 assert_eq!(pat, expected);
654 }
655}