1use std::collections::HashMap;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16pub enum TermPosition {
17 Head,
19 Body(usize),
21 Fact,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq, Hash)]
29pub enum TermType {
30 Predicate(String),
32 Constant(String),
34 Variable(String),
36 Numeric(u64),
40}
41
42impl TermType {
43 #[inline]
45 pub fn from_f64(v: f64) -> Self {
46 Self::Numeric(v.to_bits())
47 }
48
49 #[inline]
52 pub fn as_f64(&self) -> Option<f64> {
53 match self {
54 Self::Numeric(bits) => Some(f64::from_bits(*bits)),
55 _ => None,
56 }
57 }
58}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
69pub struct TermRef {
70 pub rule_id: u64,
72 pub position: TermPosition,
74 pub term_type: TermType,
76}
77
78#[derive(Clone, Debug, PartialEq)]
82pub struct IndexStats {
83 pub total_terms: usize,
85 pub total_refs: usize,
87}
88
89impl IndexStats {
90 pub fn average_posting_len(&self) -> f64 {
94 if self.total_terms == 0 {
95 0.0
96 } else {
97 self.total_refs as f64 / self.total_terms as f64
98 }
99 }
100}
101
102#[derive(Debug, Default)]
127pub struct TermIndexBuilder {
128 pub index: HashMap<TermType, Vec<TermRef>>,
130}
131
132impl TermIndexBuilder {
133 pub fn new() -> Self {
135 Self {
136 index: HashMap::new(),
137 }
138 }
139
140 pub fn index_term(&mut self, term: TermType, rule_id: u64, position: TermPosition) {
144 let entry = self.index.entry(term.clone()).or_default();
145 entry.push(TermRef {
146 rule_id,
147 position,
148 term_type: term,
149 });
150 }
151
152 pub fn lookup(&self, term: &TermType) -> &[TermRef] {
154 self.index.get(term).map(Vec::as_slice).unwrap_or_default()
155 }
156
157 pub fn lookup_predicate(&self, name: &str) -> &[TermRef] {
159 self.lookup(&TermType::Predicate(name.to_string()))
160 }
161
162 pub fn lookup_constant(&self, name: &str) -> &[TermRef] {
164 self.lookup(&TermType::Constant(name.to_string()))
165 }
166
167 pub fn rules_for_predicate(&self, name: &str) -> Vec<u64> {
170 let mut ids: Vec<u64> = self
171 .lookup_predicate(name)
172 .iter()
173 .map(|r| r.rule_id)
174 .collect();
175 ids.sort_unstable();
176 ids.dedup();
177 ids
178 }
179
180 pub fn remove_rule(&mut self, rule_id: u64) {
184 self.index.retain(|_, refs| {
185 refs.retain(|r| r.rule_id != rule_id);
186 !refs.is_empty()
187 });
188 }
189
190 pub fn stats(&self) -> IndexStats {
192 let total_terms = self.index.len();
193 let total_refs = self.index.values().map(Vec::len).sum();
194 IndexStats {
195 total_terms,
196 total_refs,
197 }
198 }
199
200 pub fn clear(&mut self) {
202 self.index.clear();
203 }
204}
205
206#[cfg(test)]
209mod tests {
210 use super::*;
211
212 fn sample_builder() -> TermIndexBuilder {
214 let mut b = TermIndexBuilder::new();
215 b.index_term(
216 TermType::Predicate("parent".to_string()),
217 1,
218 TermPosition::Head,
219 );
220 b.index_term(
221 TermType::Predicate("parent".to_string()),
222 2,
223 TermPosition::Body(0),
224 );
225 b.index_term(
226 TermType::Predicate("child".to_string()),
227 2,
228 TermPosition::Head,
229 );
230 b.index_term(
231 TermType::Constant("alice".to_string()),
232 1,
233 TermPosition::Body(0),
234 );
235 b
236 }
237
238 #[test]
241 fn test_index_term_single_entry() {
242 let mut b = TermIndexBuilder::new();
243 b.index_term(
244 TermType::Predicate("foo".to_string()),
245 10,
246 TermPosition::Head,
247 );
248 assert_eq!(b.index.len(), 1);
249 let list = b.lookup_predicate("foo");
250 assert_eq!(list.len(), 1);
251 assert_eq!(list[0].rule_id, 10);
252 assert_eq!(list[0].position, TermPosition::Head);
253 }
254
255 #[test]
256 fn test_index_term_multiple_entries_same_key() {
257 let mut b = TermIndexBuilder::new();
258 b.index_term(TermType::Predicate("p".to_string()), 1, TermPosition::Head);
259 b.index_term(TermType::Predicate("p".to_string()), 2, TermPosition::Head);
260 b.index_term(
261 TermType::Predicate("p".to_string()),
262 3,
263 TermPosition::Body(1),
264 );
265 let list = b.lookup_predicate("p");
266 assert_eq!(list.len(), 3);
267 }
268
269 #[test]
272 fn test_lookup_returns_correct_refs() {
273 let b = sample_builder();
274 let list = b.lookup(&TermType::Predicate("parent".to_string()));
275 assert_eq!(list.len(), 2);
276 assert!(list
277 .iter()
278 .any(|r| r.rule_id == 1 && r.position == TermPosition::Head));
279 assert!(list
280 .iter()
281 .any(|r| r.rule_id == 2 && r.position == TermPosition::Body(0)));
282 }
283
284 #[test]
285 fn test_lookup_missing_returns_empty() {
286 let b = sample_builder();
287 let list = b.lookup(&TermType::Predicate("nonexistent".to_string()));
288 assert!(list.is_empty());
289 }
290
291 #[test]
294 fn test_lookup_predicate_found() {
295 let b = sample_builder();
296 assert_eq!(b.lookup_predicate("child").len(), 1);
297 }
298
299 #[test]
300 fn test_lookup_predicate_not_found() {
301 let b = sample_builder();
302 assert!(b.lookup_predicate("grandparent").is_empty());
303 }
304
305 #[test]
308 fn test_lookup_constant_found() {
309 let b = sample_builder();
310 let list = b.lookup_constant("alice");
311 assert_eq!(list.len(), 1);
312 assert_eq!(list[0].rule_id, 1);
313 assert_eq!(list[0].position, TermPosition::Body(0));
314 }
315
316 #[test]
317 fn test_lookup_constant_not_found() {
318 let b = sample_builder();
319 assert!(b.lookup_constant("bob").is_empty());
320 }
321
322 #[test]
325 fn test_rules_for_predicate_deduplicates() {
326 let mut b = TermIndexBuilder::new();
327 b.index_term(
329 TermType::Predicate("ancestor".to_string()),
330 5,
331 TermPosition::Head,
332 );
333 b.index_term(
334 TermType::Predicate("ancestor".to_string()),
335 5,
336 TermPosition::Body(0),
337 );
338 b.index_term(
339 TermType::Predicate("ancestor".to_string()),
340 7,
341 TermPosition::Head,
342 );
343
344 let ids = b.rules_for_predicate("ancestor");
345 assert_eq!(ids, vec![5, 7]);
346 }
347
348 #[test]
349 fn test_rules_for_predicate_sorted() {
350 let mut b = TermIndexBuilder::new();
351 b.index_term(TermType::Predicate("r".to_string()), 10, TermPosition::Head);
352 b.index_term(
353 TermType::Predicate("r".to_string()),
354 3,
355 TermPosition::Body(0),
356 );
357 b.index_term(TermType::Predicate("r".to_string()), 7, TermPosition::Head);
358
359 let ids = b.rules_for_predicate("r");
360 assert_eq!(ids, vec![3, 7, 10]);
361 }
362
363 #[test]
364 fn test_rules_for_predicate_empty() {
365 let b = TermIndexBuilder::new();
366 assert!(b.rules_for_predicate("missing").is_empty());
367 }
368
369 #[test]
372 fn test_remove_rule_removes_refs() {
373 let mut b = sample_builder();
374 b.remove_rule(1);
376 let list = b.lookup_predicate("parent");
377 assert_eq!(list.len(), 1);
378 assert_eq!(list[0].rule_id, 2);
379 }
380
381 #[test]
382 fn test_remove_rule_drops_empty_posting_list() {
383 let mut b = sample_builder();
384 b.remove_rule(1);
386 assert!(!b
387 .index
388 .contains_key(&TermType::Constant("alice".to_string())));
389 }
390
391 #[test]
392 fn test_remove_rule_nonexistent_is_noop() {
393 let mut b = sample_builder();
394 let before = b.stats();
395 b.remove_rule(999);
396 assert_eq!(b.stats(), before);
397 }
398
399 #[test]
402 fn test_stats_counts() {
403 let b = sample_builder();
404 let s = b.stats();
405 assert_eq!(s.total_terms, 3);
407 assert_eq!(s.total_refs, 4);
409 }
410
411 #[test]
412 fn test_stats_empty() {
413 let b = TermIndexBuilder::new();
414 let s = b.stats();
415 assert_eq!(s.total_terms, 0);
416 assert_eq!(s.total_refs, 0);
417 }
418
419 #[test]
422 fn test_average_posting_len() {
423 let b = sample_builder();
424 let s = b.stats();
425 let avg = s.average_posting_len();
427 assert!((avg - (4.0 / 3.0)).abs() < 1e-10);
428 }
429
430 #[test]
431 fn test_average_posting_len_empty() {
432 let s = IndexStats {
433 total_terms: 0,
434 total_refs: 0,
435 };
436 assert_eq!(s.average_posting_len(), 0.0);
437 }
438
439 #[test]
442 fn test_clear_empties_index() {
443 let mut b = sample_builder();
444 b.clear();
445 assert!(b.index.is_empty());
446 let s = b.stats();
447 assert_eq!(s.total_terms, 0);
448 assert_eq!(s.total_refs, 0);
449 }
450
451 #[test]
454 fn test_multiple_terms_same_rule() {
455 let mut b = TermIndexBuilder::new();
456 let rule_id = 42;
457 b.index_term(
458 TermType::Predicate("path".to_string()),
459 rule_id,
460 TermPosition::Head,
461 );
462 b.index_term(
463 TermType::Predicate("edge".to_string()),
464 rule_id,
465 TermPosition::Body(0),
466 );
467 b.index_term(
468 TermType::Variable("X".to_string()),
469 rule_id,
470 TermPosition::Body(0),
471 );
472 b.index_term(
473 TermType::Variable("Y".to_string()),
474 rule_id,
475 TermPosition::Body(0),
476 );
477
478 assert_eq!(b.lookup_predicate("path").len(), 1);
479 assert_eq!(b.lookup_predicate("edge").len(), 1);
480 assert_eq!(b.lookup(&TermType::Variable("X".to_string())).len(), 1);
481 assert_eq!(b.lookup(&TermType::Variable("Y".to_string())).len(), 1);
482
483 b.remove_rule(rule_id);
485 assert!(b.index.is_empty());
486 }
487
488 #[test]
491 fn test_numeric_term_index_and_lookup() {
492 let mut b = TermIndexBuilder::new();
493 let num = TermType::from_f64(std::f64::consts::PI);
494 b.index_term(num.clone(), 99, TermPosition::Fact);
495
496 let list = b.lookup(&num);
497 assert_eq!(list.len(), 1);
498 assert_eq!(list[0].rule_id, 99);
499 assert_eq!(list[0].position, TermPosition::Fact);
500 assert!(
501 (list[0].term_type.as_f64().expect("test: should succeed") - std::f64::consts::PI)
502 .abs()
503 < 1e-10
504 );
505 }
506
507 #[test]
508 fn test_numeric_term_distinct_values() {
509 let mut b = TermIndexBuilder::new();
510 b.index_term(TermType::from_f64(1.0), 1, TermPosition::Fact);
511 b.index_term(TermType::from_f64(2.0), 2, TermPosition::Fact);
512 b.index_term(TermType::from_f64(1.0), 3, TermPosition::Fact);
513
514 assert_eq!(b.index.len(), 2);
516 assert_eq!(b.lookup(&TermType::from_f64(1.0)).len(), 2);
517 assert_eq!(b.lookup(&TermType::from_f64(2.0)).len(), 1);
518 }
519
520 #[test]
523 fn test_fact_position() {
524 let mut b = TermIndexBuilder::new();
525 b.index_term(
526 TermType::Predicate("likes".to_string()),
527 0,
528 TermPosition::Fact,
529 );
530 let list = b.lookup_predicate("likes");
531 assert_eq!(list[0].position, TermPosition::Fact);
532 }
533
534 #[test]
537 fn test_body_position_index() {
538 let mut b = TermIndexBuilder::new();
539 b.index_term(
540 TermType::Predicate("q".to_string()),
541 7,
542 TermPosition::Body(3),
543 );
544 let list = b.lookup_predicate("q");
545 assert_eq!(list[0].position, TermPosition::Body(3));
546 }
547
548 #[test]
551 fn test_term_ref_stores_term_type() {
552 let mut b = TermIndexBuilder::new();
553 b.index_term(
554 TermType::Constant("hello".to_string()),
555 5,
556 TermPosition::Head,
557 );
558 let list = b.lookup_constant("hello");
559 assert_eq!(list[0].term_type, TermType::Constant("hello".to_string()));
560 }
561
562 #[test]
565 fn test_remove_rule_partial_posting_list_retained() {
566 let mut b = TermIndexBuilder::new();
567 b.index_term(TermType::Predicate("p".to_string()), 1, TermPosition::Head);
568 b.index_term(TermType::Predicate("p".to_string()), 2, TermPosition::Head);
569 b.index_term(TermType::Predicate("p".to_string()), 3, TermPosition::Head);
570
571 b.remove_rule(2);
572
573 let ids = b.rules_for_predicate("p");
574 assert_eq!(ids, vec![1, 3]);
575 }
576
577 #[test]
580 fn test_stats_after_remove() {
581 let mut b = sample_builder();
582 b.remove_rule(1);
584 let s = b.stats();
588 assert_eq!(s.total_terms, 2); assert_eq!(s.total_refs, 2); }
591
592 #[test]
595 fn test_new_is_empty() {
596 let b = TermIndexBuilder::new();
597 assert!(b.index.is_empty());
598 let s = b.stats();
599 assert_eq!(s.total_terms, 0);
600 assert_eq!(s.total_refs, 0);
601 assert_eq!(s.average_posting_len(), 0.0);
602 }
603}