@@ -94,6 +94,21 @@ impl Queue {
9494/// emits the minimum number of grams that cover the input stream. Its state space is fully
9595/// represented by the content buffer and can be shrunk on demand when only part of the stream
9696/// needs to be retained.
97+ ///
98+ /// # Consumer callback
99+ ///
100+ /// Every emitting method ([`append_char`](Self::append_char), [`append_byte`](Self::append_byte),
101+ /// [`flush`](Self::flush), [`consume_first`](Self::consume_first)) takes a consumer of shape
102+ /// `FnMut(NGram, u32, Option<u8>, &[u8])`, called once per emitted gram with:
103+ ///
104+ /// * `gram` — the compact [`NGram`] identifier to look up in an index.
105+ /// * `end` — the position of the character just after the gram in the fed stream.
106+ /// * `follow` — that following (index-folded) byte, when it has already been fed; `None` at the
107+ /// current stream end.
108+ /// * `bytes` — the gram's index-folded bytes, in reading order. They are borrowed from a stack
109+ /// buffer that is only valid for the duration of the call, so a consumer that needs to keep them
110+ /// must copy them. Nothing is allocated per gram, and a consumer that ignores the argument
111+ /// optimizes back to code identical to not reporting the bytes at all.
97112#[ derive( Clone ) ]
98113pub struct QueryGrams {
99114 /// Queue of candidate boundaries (strictly increasing indices and nondecreasing priorities).
@@ -202,26 +217,41 @@ impl QueryGrams {
202217
203218 fn extract_gram < F > ( & mut self , begin_index : u32 , end_index : u32 , consumer : & mut F )
204219 where
205- F : FnMut ( NGram , u32 , Option < u8 > ) ,
220+ F : FnMut ( NGram , u32 , Option < u8 > , & [ u8 ] ) ,
206221 {
207222 debug_assert ! ( end_index >= begin_index) ;
208223 debug_assert ! ( end_index <= self . content_end_idx) ;
209224
210225 let len = ( end_index - begin_index + 2 ) as usize ;
211226 let dist = self . content_end_idx - end_index;
212227 let shifted = self . content >> ( dist * 8 ) ;
228+ // The gram is the low `len` bytes of `shifted`; shifting them up into the most-significant
229+ // bytes gives both the big-endian layout `NGram::from_window` consumes and, via
230+ // `to_be_bytes`, the gram's bytes in reading order in the first `len` slots.
231+ let aligned = shifted << ( ( MAX_SPARSE_GRAM_SIZE - len) * 8 ) ;
232+ let bytes = aligned. to_be_bytes ( ) ;
213233 // Report the position of the character just after the emitted ngram, along with that
214234 // character itself when it has already been fed (see `follow_byte`).
215235 let follow = self . follow_byte ( end_index) ;
216- consumer ( NGram :: from_window_masked ( shifted, len) , end_index, follow) ;
236+ // `len` is always in `2..=MAX_SPARSE_GRAM_SIZE` (`from_window` debug-asserts it), so the
237+ // clamp never changes the slice. It is what makes the bound *statically* provable: without
238+ // it the slicing keeps a bounds-check panic path alive, and that observable side effect
239+ // stops the optimizer from eliminating the byte buffer for consumers that ignore it.
240+ consumer (
241+ NGram :: from_window ( aligned, len) ,
242+ end_index,
243+ follow,
244+ & bytes[ ..len. min ( MAX_SPARSE_GRAM_SIZE ) ] ,
245+ ) ;
217246 }
218247
219248 /// Appends a single character to the n-gram state.
220249 ///
221250 /// The character is index-folded using the `casefold` crate and may trigger one or more grams.
251+ /// See the [type-level docs](Self#consumer-callback) for the consumer arguments.
222252 pub fn append_char < F > ( & mut self , c : char , consumer : F )
223253 where
224- F : FnMut ( NGram , u32 , Option < u8 > ) ,
254+ F : FnMut ( NGram , u32 , Option < u8 > , & [ u8 ] ) ,
225255 {
226256 self . append_byte ( index_fold_char ( c) , consumer) ;
227257 }
@@ -233,7 +263,7 @@ impl QueryGrams {
233263 /// applied. May trigger one or more grams.
234264 pub fn append_byte < F > ( & mut self , right : u8 , mut consumer : F )
235265 where
236- F : FnMut ( NGram , u32 , Option < u8 > ) ,
266+ F : FnMut ( NGram , u32 , Option < u8 > , & [ u8 ] ) ,
237267 {
238268 let left = ( self . content & 0xFF ) as u8 ;
239269 self . content_end_idx += 1 ;
@@ -276,7 +306,7 @@ impl QueryGrams {
276306 /// Flushes all buffered characters and emits remaining grams.
277307 pub fn flush < F > ( mut self , mut consumer : F )
278308 where
279- F : FnMut ( NGram , u32 , Option < u8 > ) ,
309+ F : FnMut ( NGram , u32 , Option < u8 > , & [ u8 ] ) ,
280310 {
281311 if self . content_end_idx == 2 {
282312 self . extract_gram ( 2 , 2 , & mut consumer) ;
@@ -292,7 +322,7 @@ impl QueryGrams {
292322 /// Consumes and emits at most one queued gram (if available), shrinking state.
293323 pub fn consume_first < F > ( & mut self , mut consumer : F )
294324 where
295- F : FnMut ( NGram , u32 , Option < u8 > ) ,
325+ F : FnMut ( NGram , u32 , Option < u8 > , & [ u8 ] ) ,
296326 {
297327 if self . queue . len > 1 {
298328 // Emit the gram spanning the first boundary to the next one, mirroring `flush`.
@@ -343,12 +373,12 @@ mod tests {
343373 let mut q = QueryGrams :: default ( ) ;
344374 let mut out = Vec :: new ( ) ;
345375 for c in input. chars ( ) {
346- q. append_char ( c, |gram, end, _follow| {
376+ q. append_char ( c, |gram, end, _follow, _bytes | {
347377 let begin = end + 1 - gram. len ( ) as u32 ;
348378 out. push ( begin..end - 1 ) ;
349379 } ) ;
350380 }
351- q. flush ( |gram, end, _follow| {
381+ q. flush ( |gram, end, _follow, _bytes | {
352382 let begin = end + 1 - gram. len ( ) as u32 ;
353383 out. push ( begin..end - 1 ) ;
354384 } ) ;
@@ -428,10 +458,10 @@ mod tests {
428458 fn append_and_flush_emit_grams ( ) {
429459 let mut q = QueryGrams :: default ( ) ;
430460 for c in "hello world" . chars ( ) {
431- q. append_char ( c, |_gram, _idx, _follow| { } ) ;
461+ q. append_char ( c, |_gram, _idx, _follow, _bytes | { } ) ;
432462 }
433463 let mut out = Vec :: new ( ) ;
434- q. flush ( |gram, idx, _follow| out. push ( ( gram, idx) ) ) ;
464+ q. flush ( |gram, idx, _follow, _bytes | out. push ( ( gram, idx) ) ) ;
435465 assert ! ( !out. is_empty( ) ) ;
436466 assert ! (
437467 out. iter( )
@@ -446,17 +476,18 @@ mod tests {
446476 let bytes: Vec < u8 > = "ab" . chars ( ) . map ( crate :: index_fold_char) . collect ( ) ;
447477 let mut q = QueryGrams :: default ( ) ;
448478 for & b in & bytes {
449- q. append_byte ( b, |_gram, _end, _follow| { } ) ;
479+ q. append_byte ( b, |_gram, _end, _follow, _bytes | { } ) ;
450480 }
451481 let mut emitted = Vec :: new ( ) ;
452- q. flush ( |gram, end, follow| emitted. push ( ( gram. len ( ) , end, follow) ) ) ;
482+ q. flush ( |gram, end, follow, _bytes | emitted. push ( ( gram. len ( ) , end, follow) ) ) ;
453483 assert_eq ! ( emitted, vec![ ( 2usize , 2u32 , None ) ] ) ;
454484 }
455485
456486 #[ test]
457- fn emitted_follow_is_the_actual_next_char ( ) {
487+ fn emitted_follow_and_bytes_match_the_input ( ) {
458488 // Whenever a gram is emitted with a follow character, it must be the actual next byte of the
459489 // input at the reported boundary (and a gram ending at the newest fed byte reports `None`).
490+ // The emitted byte slice must likewise be the input slice the gram was built from.
460491 for input in [
461492 "ab" ,
462493 "abc" ,
@@ -469,7 +500,17 @@ mod tests {
469500 let bytes: Vec < u8 > = input. chars ( ) . map ( crate :: index_fold_char) . collect ( ) ;
470501 let n = bytes. len ( ) as u32 ;
471502 let mut q = QueryGrams :: default ( ) ;
472- let mut check = |_gram : NGram , end : u32 , follow : Option < u8 > | {
503+ let mut check = |gram : NGram , end : u32 , follow : Option < u8 > , gram_bytes : & [ u8 ] | {
504+ assert_eq ! (
505+ gram_bytes,
506+ & bytes[ end as usize - gram. len( ) ..end as usize ] ,
507+ "wrong gram bytes for {input:?} at end={end}"
508+ ) ;
509+ assert_eq ! (
510+ gram,
511+ NGram :: from_bytes( gram_bytes) ,
512+ "gram bytes disagree with the emitted key for {input:?} at end={end}"
513+ ) ;
473514 if let Some ( b) = follow {
474515 assert ! (
475516 end < n,
@@ -516,7 +557,7 @@ mod tests {
516557 "state moved without an append before byte {i}"
517558 ) ;
518559
519- q. append_byte ( byte, |_, _, _| { } ) ;
560+ q. append_byte ( byte, |_, _, _, _ | { } ) ;
520561
521562 let len = expected_len[ i] ;
522563 let window = & input[ i + 1 - len as usize ..=i] ;
@@ -537,12 +578,12 @@ mod tests {
537578 let mut b = QueryGrams :: default ( ) ;
538579
539580 for c in "abc" . chars ( ) {
540- a. append_char ( c, |_gram, _idx, _follow| { } ) ;
581+ a. append_char ( c, |_gram, _idx, _follow, _bytes | { } ) ;
541582 }
542583 for c in "zabc" . chars ( ) {
543- b. append_char ( c, |_gram, _idx, _follow| { } ) ;
584+ b. append_char ( c, |_gram, _idx, _follow, _bytes | { } ) ;
544585 }
545- b. consume_first ( |_gram, _idx, _follow| { } ) ;
586+ b. consume_first ( |_gram, _idx, _follow, _bytes | { } ) ;
546587
547588 // Only assert hash consistency when Eq says they are equivalent.
548589 if a == b {
@@ -724,14 +765,14 @@ mod tests {
724765 // plus grams drained via `consume_first`.
725766 let mut first_half = Vec :: new ( ) ;
726767 for c in prefix. chars ( ) {
727- q. append_char ( c, |gram, end, _follow| {
768+ q. append_char ( c, |gram, end, _follow, _bytes | {
728769 let begin = end + 1 - gram. len ( ) as u32 ;
729770 first_half. push ( begin..end - 1 ) ;
730771 } ) ;
731772 }
732773
733774 while !q. queue . is_empty ( ) {
734- q. consume_first ( |gram, end, _follow| {
775+ q. consume_first ( |gram, end, _follow, _bytes | {
735776 let begin = end + 1 - gram. len ( ) as u32 ;
736777 first_half. push ( begin..end - 1 ) ;
737778 } ) ;
@@ -770,12 +811,12 @@ mod tests {
770811
771812 let mut remaining = Vec :: new ( ) ;
772813 for c in suffix. chars ( ) {
773- q. append_char ( c, |gram, end, _follow| {
814+ q. append_char ( c, |gram, end, _follow, _bytes | {
774815 let begin = end + 1 - gram. len ( ) as u32 ;
775816 remaining. push ( begin..end - 1 ) ;
776817 } ) ;
777818 }
778- q. flush ( |gram, end, _follow| {
819+ q. flush ( |gram, end, _follow, _bytes | {
779820 let begin = end + 1 - gram. len ( ) as u32 ;
780821 remaining. push ( begin..end - 1 ) ;
781822 } ) ;
@@ -825,12 +866,12 @@ mod tests {
825866 for input in [ "abc" , "abcdef" , "hello world" , "ababababab" , "mississippi" ] {
826867 let mut q = QueryGrams :: default ( ) ;
827868 for c in input. chars ( ) {
828- q. append_char ( c, |_gram, _idx, _follow| { } ) ;
869+ q. append_char ( c, |_gram, _idx, _follow, _bytes | { } ) ;
829870 }
830871 // Far more calls than there are bytes: the state must be at the default fixpoint well
831872 // before this loop ends, and further calls must keep it there.
832873 for _ in 0 ..( input. len ( ) + 8 ) {
833- q. consume_first ( |_gram, _idx, _follow| { } ) ;
874+ q. consume_first ( |_gram, _idx, _follow, _bytes | { } ) ;
834875 }
835876 assert_eq ! (
836877 q. state( ) ,
0 commit comments