1use crate::bias::HashBiasTable;
2use crate::format::{
3 BUCKET_COUNT, BUCKET_TABLE_SIZE, BucketMeta, DATA_START, ENTRY_SIZE, Entry,
4 FLAG_HAS_BIAS_TABLE, HEADER_SIZE, Header, MAGIC, VERSION,
5};
6use crate::io::{extract_unique_hashes, read_entries, write_entries};
7use crate::sketch::{SketchConfig, SketchResult};
8use bytemuck;
9use memmap2::MmapMut;
10use rayon::prelude::*;
11use std::io;
12use std::path::{Path, PathBuf};
13use std::sync::Arc;
14use std::time::Duration;
15use xorf::{BinaryFuse8, DmaSerializable};
16
17pub const FILTER_DESCRIPTOR_SIZE: usize = 20;
18
19pub fn serialize_filter(filter: &BinaryFuse8) -> Vec<u8> {
20 let fingerprints = filter.dma_fingerprints();
21 let descriptor_size = FILTER_DESCRIPTOR_SIZE as u32;
22 let fingerprints_size = fingerprints.len() as u32;
23
24 let total_size = 4 + 4 + FILTER_DESCRIPTOR_SIZE + fingerprints.len();
25 let mut out = vec![0u8; total_size];
26
27 out[0..4].copy_from_slice(&descriptor_size.to_le_bytes());
28 out[4..8].copy_from_slice(&fingerprints_size.to_le_bytes());
29 filter.dma_copy_descriptor_to(&mut out[8..8 + FILTER_DESCRIPTOR_SIZE]);
30 out[8 + FILTER_DESCRIPTOR_SIZE..].copy_from_slice(fingerprints);
31
32 out
33}
34
35fn serialize_sample_names(names: &[String]) -> Vec<u8> {
36 let mut buf = Vec::new();
37 for (idx, name) in names.iter().enumerate() {
38 let bytes = name.as_bytes();
39 let len = if bytes.len() > u16::MAX as usize {
40 eprintln!(
41 "Warning: Sample name at index {} is {} bytes, truncating to {} bytes",
42 idx,
43 bytes.len(),
44 u16::MAX
45 );
46 u16::MAX
47 } else {
48 bytes.len() as u16
49 };
50 buf.extend_from_slice(&len.to_le_bytes());
51 buf.extend_from_slice(&bytes[..len as usize]);
52 }
53 buf
54}
55
56fn serialize_sample_sizes(sizes: &[u64]) -> Vec<u8> {
57 bytemuck::cast_slice(sizes).to_vec()
58}
59
60#[derive(Clone)]
61pub struct CompactConfig {
62 pub num_threads: usize,
63}
64
65impl Default for CompactConfig {
66 fn default() -> Self {
67 Self { num_threads: 1 }
68 }
69}
70
71struct ProcessedBucket {
72 bucket_id: usize,
73 entry_count: u64,
74 unique_hash_count: u64,
75 filter_bytes: Vec<u8>,
76 sample_hash_counts: std::collections::HashMap<u32, u64>,
77}
78
79#[derive(Debug, Clone)]
80pub struct IndexStats {
81 pub total_entries: u64,
82 pub unique_hashes: u64,
83 pub sample_count: u32,
84 pub file_size: u64,
85 pub kmer_size: u8,
86 pub frac_max: u64,
87 pub bucket_entry_counts: [u64; BUCKET_COUNT],
88}
89
90#[derive(Clone)]
91pub struct BuildConfig {
92 pub kmer_size: u8,
93 pub fscale: u64,
94 pub num_threads: usize,
95 pub memory: usize,
96 pub temp_dir_base: Option<PathBuf>,
97 pub min_entropy: f64,
98 pub singleton: bool,
99 pub bias_table: Option<Arc<HashBiasTable>>,
100 pub show_progress: bool,
101}
102
103impl Default for BuildConfig {
104 fn default() -> Self {
105 Self {
106 kmer_size: 21,
107 fscale: 1000,
108 num_threads: 1,
109 memory: 4,
110 temp_dir_base: None,
111 min_entropy: 0.0,
112 singleton: false,
113 bias_table: None,
114 show_progress: false,
115 }
116 }
117}
118
119#[derive(Debug, thiserror::Error)]
120pub enum BuildError {
121 #[error("Sketch error: {0}")]
122 Sketch(#[from] crate::sketch::SketchError),
123
124 #[error("Compact error: {0}")]
125 Compact(#[from] CompactError),
126}
127
128pub fn build(
129 input_files: &[PathBuf],
130 output_path: &Path,
131 config: &BuildConfig,
132) -> Result<IndexStats, BuildError> {
133 let sketch_config = SketchConfig {
134 kmer_size: config.kmer_size,
135 fscale: config.fscale,
136 num_threads: config.num_threads,
137 memory: config.memory,
138 temp_dir_base: config.temp_dir_base.clone(),
139 min_entropy: config.min_entropy,
140 singleton: config.singleton,
141 bias_table: config.bias_table.clone(),
142 send_timeout: Duration::from_millis(1),
143 show_progress: config.show_progress,
144 };
145
146 let sketch_result = crate::sketch::run(input_files, &sketch_config)?;
147
148 let compact_config = CompactConfig {
149 num_threads: config.num_threads,
150 };
151
152 let stats = run(
153 output_path,
154 &sketch_result,
155 &compact_config,
156 config.kmer_size,
157 config.bias_table.as_deref(),
158 )?;
159
160 Ok(stats)
161}
162
163#[derive(Debug, thiserror::Error)]
164pub enum CompactError {
165 #[error("I/O error: {0}")]
166 Io(#[from] io::Error),
167
168 #[error("Filter construction failed for bucket {bucket}: {message}")]
169 FilterConstruction { bucket: usize, message: String },
170}
171
172pub fn run(
173 output_path: &Path,
174 sketch_result: &SketchResult,
175 _config: &CompactConfig,
176 kmer_size: u8,
177 bias_table: Option<&HashBiasTable>,
178) -> Result<IndexStats, CompactError> {
179 let temp_path = sketch_result.temp_dir.path();
180
181 let processed: Result<Vec<ProcessedBucket>, CompactError> = (0..BUCKET_COUNT)
182 .into_par_iter()
183 .map(|bucket_id| {
184 let bucket_path = temp_path.join(format!("bucket_{bucket_id:03}.bin"));
185
186 let mut entries = if bucket_path.exists() {
187 read_entries(&bucket_path)?
188 } else {
189 Vec::new()
190 };
191
192 entries.sort_unstable();
193 entries.dedup();
194
195 let mut sample_hash_counts: std::collections::HashMap<u32, u64> =
196 std::collections::HashMap::new();
197 for entry in &entries {
198 *sample_hash_counts.entry(entry.sample_id).or_insert(0) += 1;
199 }
200
201 let unique_hashes = extract_unique_hashes(&entries);
202 let unique_hash_count = unique_hashes.len() as u64;
203
204 let filter_bytes = if unique_hashes.is_empty() {
205 Vec::new()
206 } else {
207 let filter = BinaryFuse8::try_from(&unique_hashes[..]).map_err(|e| {
208 CompactError::FilterConstruction {
209 bucket: bucket_id,
210 message: format!("{e:?}"),
211 }
212 })?;
213 serialize_filter(&filter)
214 };
215
216 if !entries.is_empty() {
217 write_entries(&bucket_path, &entries)?;
218 }
219
220 Ok(ProcessedBucket {
221 bucket_id,
222 entry_count: entries.len() as u64,
223 unique_hash_count,
224 filter_bytes,
225 sample_hash_counts,
226 })
227 })
228 .collect();
229
230 let processed = processed?;
231
232 let mut aggregated_sample_sizes: Vec<u64> = vec![0u64; sketch_result.sample_count as usize];
233 for bucket in &processed {
234 for (&sample_id, &count) in &bucket.sample_hash_counts {
235 if (sample_id as usize) < aggregated_sample_sizes.len() {
236 aggregated_sample_sizes[sample_id as usize] += count;
237 }
238 }
239 }
240
241 use crate::format::align_to_page;
242
243 let bias_size: u64 = bias_table.map(|b| b.to_bytes().len() as u64).unwrap_or(0);
244 let sample_names_bytes = serialize_sample_names(&sketch_result.sample_names);
245 let sample_sizes_bytes = serialize_sample_sizes(&aggregated_sample_sizes);
246
247 let bucket_regions_start = align_to_page(DATA_START);
248 let mut current_offset = bucket_regions_start;
249 let mut bucket_offsets = Vec::with_capacity(BUCKET_COUNT);
250
251 for bucket in &processed {
252 bucket_offsets.push(current_offset);
253 let bucket_size = bucket.filter_bytes.len() + (bucket.entry_count as usize) * ENTRY_SIZE;
254 if bucket_size > 0 {
255 current_offset = align_to_page(current_offset + bucket_size);
256 }
257 }
258
259 let metadata_offset = current_offset;
260 let total_size =
261 metadata_offset + bias_size as usize + sample_names_bytes.len() + sample_sizes_bytes.len();
262
263 let file = std::fs::OpenOptions::new()
264 .read(true)
265 .write(true)
266 .create(true)
267 .truncate(true)
268 .open(output_path)?;
269 file.set_len(total_size as u64)?;
270
271 let mut mmap = unsafe { MmapMut::map_mut(&file)? };
272
273 let mut bucket_metas = vec![BucketMeta::default(); BUCKET_COUNT];
274 let mut total_unique_hashes = 0u64;
275 let mut entries_size = 0u64;
276 let mut filters_size = 0u64;
277
278 for bucket in &processed {
279 let bucket_offset = bucket_offsets[bucket.bucket_id];
280 let filter_size = bucket.filter_bytes.len();
281 let entries_bytes_len = (bucket.entry_count as usize) * ENTRY_SIZE;
282
283 if !bucket.filter_bytes.is_empty() {
284 mmap[bucket_offset..bucket_offset + filter_size].copy_from_slice(&bucket.filter_bytes);
285 }
286
287 let entry_offset = bucket_offset + filter_size;
288 if bucket.entry_count > 0 {
289 let bucket_path = temp_path.join(format!("bucket_{:03}.bin", bucket.bucket_id));
290 let entries = read_entries(&bucket_path)?;
291 let entry_bytes = bytemuck::cast_slice::<Entry, u8>(&entries);
292 mmap[entry_offset..entry_offset + entry_bytes.len()].copy_from_slice(entry_bytes);
293 }
294
295 bucket_metas[bucket.bucket_id] = BucketMeta {
296 filter_offset: bucket_offset as u64,
297 filter_size: filter_size as u64,
298 entry_offset: entry_offset as u64,
299 entry_count: bucket.entry_count,
300 };
301
302 total_unique_hashes += bucket.unique_hash_count;
303 entries_size += entries_bytes_len as u64;
304 filters_size += filter_size as u64;
305 }
306
307 let mut meta_offset = metadata_offset;
308
309 let (bias_table_offset, bias_table_size, flags) = if let Some(bias) = bias_table {
310 let bias_bytes = bias.to_bytes();
311 mmap[meta_offset..meta_offset + bias_bytes.len()].copy_from_slice(&bias_bytes);
312 let offset = meta_offset;
313 meta_offset += bias_bytes.len();
314 (offset as u64, bias_bytes.len() as u64, FLAG_HAS_BIAS_TABLE)
315 } else {
316 (0, 0, 0)
317 };
318
319 let sample_names_offset = meta_offset;
320 mmap[sample_names_offset..sample_names_offset + sample_names_bytes.len()]
321 .copy_from_slice(&sample_names_bytes);
322 meta_offset += sample_names_bytes.len();
323
324 let sample_sizes_offset = meta_offset;
325 mmap[sample_sizes_offset..sample_sizes_offset + sample_sizes_bytes.len()]
326 .copy_from_slice(&sample_sizes_bytes);
327
328 let table_bytes = bytemuck::cast_slice::<BucketMeta, u8>(&bucket_metas);
329 mmap[HEADER_SIZE..HEADER_SIZE + BUCKET_TABLE_SIZE].copy_from_slice(table_bytes);
330
331 let total_entries: u64 = processed.iter().map(|b| b.entry_count).sum();
332
333 let header = Header {
334 magic: MAGIC,
335 version: VERSION,
336 flags,
337 entry_count: total_entries,
338 unique_hash_count: total_unique_hashes,
339 sample_count: sketch_result.sample_count,
340 bucket_count: BUCKET_COUNT as u16,
341 bucket_bits: 8,
342 entry_size: ENTRY_SIZE as u8,
343 hash_threshold: sketch_result.frac_max,
344 kmer_size,
345 _param_reserved: [0; 7],
346 bucket_table_offset: HEADER_SIZE as u64,
347 entries_offset: bucket_regions_start as u64,
348 filters_offset: bucket_regions_start as u64,
349 bias_table_offset,
350 entries_size,
351 filters_size,
352 bias_table_size,
353 sample_names_offset: sample_names_offset as u64,
354 sample_names_size: sample_names_bytes.len() as u64,
355 sample_sizes_offset: sample_sizes_offset as u64,
356 sample_sizes_size: sample_sizes_bytes.len() as u64,
357 _padding: [0; 16],
358 };
359
360 let header_bytes = bytemuck::bytes_of(&header);
361 mmap[..HEADER_SIZE].copy_from_slice(header_bytes);
362
363 mmap.flush()?;
364 drop(mmap);
365
366 let mut bucket_entry_counts = [0u64; BUCKET_COUNT];
367 for bucket in &processed {
368 bucket_entry_counts[bucket.bucket_id] = bucket.entry_count;
369 }
370
371 Ok(IndexStats {
372 total_entries,
373 unique_hashes: total_unique_hashes,
374 sample_count: sketch_result.sample_count,
375 file_size: total_size as u64,
376 kmer_size,
377 frac_max: sketch_result.frac_max,
378 bucket_entry_counts,
379 })
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use crate::format::bucket_id;
386 use crate::sketch::{SketchConfig, run as sketch_run};
387 use std::fs::File;
388 use std::io::Write;
389 use tempfile::NamedTempFile;
390
391 fn make_fasta(seqs: &[(&str, &str)]) -> NamedTempFile {
392 let mut f = NamedTempFile::with_suffix(".fa").unwrap();
393 for (name, seq) in seqs {
394 writeln!(f, ">{name}").unwrap();
395 writeln!(f, "{seq}").unwrap();
396 }
397 f
398 }
399
400 #[test]
401 fn test_compact_basic() {
402 let input = make_fasta(&[("seq1", "ATCGATCGATCGATCGATCGATCGATCGATCG")]);
403 let sketch_config = SketchConfig {
404 kmer_size: 11,
405 fscale: 1,
406 num_threads: 2,
407 memory: 1,
408 ..Default::default()
409 };
410
411 let sketch_result = sketch_run(&[input.path().to_path_buf()], &sketch_config).unwrap();
412
413 let output_dir = tempfile::tempdir().unwrap();
414 let output_path = output_dir.path().join("test.jam");
415
416 let compact_config = CompactConfig::default();
417 let stats = run(&output_path, &sketch_result, &compact_config, 11, None).unwrap();
418
419 assert!(stats.total_entries > 0);
420 assert_eq!(stats.sample_count, 1);
421 assert_eq!(stats.kmer_size, 11);
422 assert!(output_path.exists());
423
424 let metadata = std::fs::metadata(&output_path).unwrap();
425 assert_eq!(metadata.len(), stats.file_size);
426 }
427
428 #[test]
429 fn test_compact_empty_buckets() {
430 let input = make_fasta(&[("seq1", "ATCGATCGATCGATCGATCG")]);
431 let sketch_config = SketchConfig {
432 kmer_size: 11,
433 fscale: 1_000_000, num_threads: 1,
435 memory: 1,
436 ..Default::default()
437 };
438
439 let sketch_result = sketch_run(&[input.path().to_path_buf()], &sketch_config).unwrap();
440
441 let output_dir = tempfile::tempdir().unwrap();
442 let output_path = output_dir.path().join("test.jam");
443
444 let compact_config = CompactConfig::default();
445 let result = run(&output_path, &sketch_result, &compact_config, 11, None);
446
447 assert!(result.is_ok());
448 }
449
450 #[test]
451 fn test_compact_header_validation() {
452 let input = make_fasta(&[("seq1", "ATCGATCGATCGATCGATCGATCGATCGATCG")]);
453 let sketch_config = SketchConfig {
454 kmer_size: 21,
455 fscale: 10,
456 num_threads: 1,
457 memory: 1,
458 ..Default::default()
459 };
460
461 let sketch_result = sketch_run(&[input.path().to_path_buf()], &sketch_config).unwrap();
462
463 let output_dir = tempfile::tempdir().unwrap();
464 let output_path = output_dir.path().join("test.jam");
465
466 let compact_config = CompactConfig::default();
467 run(&output_path, &sketch_result, &compact_config, 21, None).unwrap();
468
469 let file = File::open(&output_path).unwrap();
470 let mmap = unsafe { memmap2::Mmap::map(&file).unwrap() };
471
472 let header: &Header = bytemuck::from_bytes(&mmap[..HEADER_SIZE]);
473 assert!(header.validate().is_ok());
474 assert_eq!(header.kmer_size, 21);
475 assert_eq!(header.sample_count, 1);
476 }
477
478 #[test]
479 fn test_compact_entry_sorting() {
480 let input = make_fasta(&[
481 ("seq1", "ATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCG"),
482 ("seq2", "GCTAGCTAGCTAGCTAGCTAGCTAGCTAGCTAGCTAGCTAGCTAGCTA"),
483 ]);
484 let sketch_config = SketchConfig {
485 kmer_size: 11,
486 fscale: 1, num_threads: 2,
488 memory: 1,
489 ..Default::default()
490 };
491
492 let sketch_result = sketch_run(&[input.path().to_path_buf()], &sketch_config).unwrap();
493
494 let output_dir = tempfile::tempdir().unwrap();
495 let output_path = output_dir.path().join("test.jam");
496
497 let compact_config = CompactConfig::default();
498 run(&output_path, &sketch_result, &compact_config, 11, None).unwrap();
499
500 let file = File::open(&output_path).unwrap();
501 let mmap = unsafe { memmap2::Mmap::map(&file).unwrap() };
502
503 let bucket_table: &[BucketMeta] =
504 bytemuck::cast_slice(&mmap[HEADER_SIZE..HEADER_SIZE + BUCKET_TABLE_SIZE]);
505
506 for (i, meta) in bucket_table.iter().enumerate() {
507 if meta.entry_count == 0 {
508 continue;
509 }
510
511 let start = meta.entry_offset as usize;
512 let end = start + (meta.entry_count as usize) * ENTRY_SIZE;
513 let entries: &[Entry] = bytemuck::cast_slice(&mmap[start..end]);
514
515 for entry in entries {
516 assert_eq!(bucket_id(entry.hash), i, "Entry in wrong bucket");
517 }
518
519 for window in entries.windows(2) {
520 assert!(
521 window[0] <= window[1],
522 "Entries not sorted in bucket {i}: {:?} > {:?}",
523 window[0],
524 window[1]
525 );
526 }
527 }
528 }
529
530 #[test]
531 fn test_build_basic() {
532 let input = make_fasta(&[("seq1", "ATCGATCGATCGATCGATCGATCGATCGATCG")]);
533 let output_dir = tempfile::tempdir().unwrap();
534 let output_path = output_dir.path().join("test.jam");
535
536 let config = BuildConfig {
537 kmer_size: 11,
538 fscale: 1,
539 num_threads: 2,
540 memory: 1,
541 ..Default::default()
542 };
543
544 let stats = build(&[input.path().to_path_buf()], &output_path, &config).unwrap();
545
546 assert!(stats.total_entries > 0);
547 assert_eq!(stats.sample_count, 1);
548 assert_eq!(stats.kmer_size, 11);
549 assert!(output_path.exists());
550 }
551
552 #[test]
553 fn test_build_singleton_mode() {
554 let input = make_fasta(&[
555 ("seq1", "ATCGATCGATCGATCGATCGATCGATCGATCG"),
556 ("seq2", "GCTAGCTAGCTAGCTAGCTAGCTAGCTAGCTA"),
557 ]);
558 let output_dir = tempfile::tempdir().unwrap();
559 let output_path = output_dir.path().join("test.jam");
560
561 let config = BuildConfig {
562 kmer_size: 11,
563 fscale: 1,
564 singleton: true,
565 num_threads: 2,
566 memory: 1,
567 ..Default::default()
568 };
569
570 let stats = build(&[input.path().to_path_buf()], &output_path, &config).unwrap();
571
572 assert_eq!(
573 stats.sample_count, 2,
574 "Singleton mode should create one sample per sequence"
575 );
576 }
577
578 #[test]
579 fn test_build_multiple_files() {
580 let input1 = make_fasta(&[("seq1", "ATCGATCGATCGATCGATCGATCGATCGATCG")]);
581 let input2 = make_fasta(&[("seq2", "GCTAGCTAGCTAGCTAGCTAGCTAGCTAGCTA")]);
582 let output_dir = tempfile::tempdir().unwrap();
583 let output_path = output_dir.path().join("test.jam");
584
585 let config = BuildConfig {
586 kmer_size: 11,
587 fscale: 1,
588 num_threads: 2,
589 memory: 1,
590 ..Default::default()
591 };
592
593 let stats = build(
594 &[input1.path().to_path_buf(), input2.path().to_path_buf()],
595 &output_path,
596 &config,
597 )
598 .unwrap();
599
600 assert_eq!(stats.sample_count, 2);
601 assert!(stats.total_entries > 0);
602 }
603
604 #[test]
605 fn test_build_with_entropy_filter() {
606 let input = make_fasta(&[
607 ("low_complexity", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
608 ("high_complexity", "ATCGATCGATCGATCGATCGATCGATCGATCG"),
609 ]);
610 let output_dir = tempfile::tempdir().unwrap();
611 let output_path = output_dir.path().join("test.jam");
612
613 let config = BuildConfig {
614 kmer_size: 11,
615 fscale: 1,
616 min_entropy: 1.5,
617 num_threads: 1,
618 memory: 1,
619 ..Default::default()
620 };
621
622 let stats = build(&[input.path().to_path_buf()], &output_path, &config).unwrap();
623
624 assert!(stats.total_entries > 0);
625 }
626
627 #[test]
628 fn test_build_with_bias_table() {
629 use crate::bias::{CMSConfig, HashBiasTable, RawHashCounts};
630
631 let pos_fasta = make_fasta(&[("pos", "ATATATATATATATATATATATATATATATAT")]);
632 let neg_fasta = make_fasta(&[("neg", "GCGCGCGCGCGCGCGCGCGCGCGCGCGCGCGC")]);
633
634 let config = CMSConfig {
635 width: 1024,
636 depth: 3,
637 k: 11,
638 fscale: 1,
639 };
640
641 let rc = std::sync::atomic::AtomicU64::new(0);
642 let hc = std::sync::atomic::AtomicU64::new(0);
643 let pos_raw = RawHashCounts::build(&[pos_fasta.path()], config.clone(), &rc, &hc).unwrap();
644 let neg_raw = RawHashCounts::build(&[neg_fasta.path()], config, &rc, &hc).unwrap();
645 let bias_table = HashBiasTable::build(&pos_raw, &neg_raw, 1.0, Some(2.0)).unwrap();
646
647 let input = make_fasta(&[("seq1", "ATATATATATATATATATATATATATATATATATAT")]);
648 let output_dir = tempfile::tempdir().unwrap();
649 let output_path = output_dir.path().join("test.jam");
650
651 let config = BuildConfig {
652 kmer_size: 11,
653 fscale: 1,
654 num_threads: 1,
655 memory: 1,
656 bias_table: Some(std::sync::Arc::new(bias_table.clone())),
657 ..Default::default()
658 };
659
660 build(&[input.path().to_path_buf()], &output_path, &config).unwrap();
661
662 let reader = crate::reader::JamReader::open(&output_path).unwrap();
663 assert!(reader.has_bias_table());
664
665 let embedded_bias = reader.bias_table().unwrap();
666 assert_eq!(*embedded_bias, bias_table);
667 }
668
669 #[test]
670 fn test_build_without_bias_table() {
671 let input = make_fasta(&[("seq1", "ATCGATCGATCGATCGATCGATCGATCGATCG")]);
672 let output_dir = tempfile::tempdir().unwrap();
673 let output_path = output_dir.path().join("test.jam");
674
675 let config = BuildConfig {
676 kmer_size: 11,
677 fscale: 1,
678 num_threads: 1,
679 memory: 1,
680 bias_table: None,
681 ..Default::default()
682 };
683
684 build(&[input.path().to_path_buf()], &output_path, &config).unwrap();
685
686 let reader = crate::reader::JamReader::open(&output_path).unwrap();
687 assert!(!reader.has_bias_table());
688 assert!(reader.bias_table().is_none());
689 }
690}