Skip to main content

patch_prolog_core/
index.rs

1use crate::term::{AtomId, Clause, FirstArgKey, Term};
2use fnv::FnvHashMap;
3use serde::{Deserialize, Serialize};
4
5/// Entry for a single predicate (functor/arity).
6/// Supports two-tier indexing: functor/arity lookup, then first-argument hash.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct PredicateEntry {
9    /// First-argument index: ground first arg -> clause indices
10    pub arg_index: FnvHashMap<FirstArgKey, Vec<usize>>,
11    /// Clause indices where the first arg is a variable (must always be included)
12    pub var_clause_indices: Vec<usize>,
13    /// All clause indices in source order (fallback)
14    pub all_clause_indices: Vec<usize>,
15}
16
17/// Two-tier predicate index: (functor AtomId, arity) -> PredicateEntry
18pub type PredicateIndex = FnvHashMap<(AtomId, usize), PredicateEntry>;
19
20/// Build the predicate index from a list of clauses.
21pub fn build_index(clauses: &[Clause]) -> PredicateIndex {
22    let mut index: PredicateIndex = FnvHashMap::default();
23
24    for (clause_idx, clause) in clauses.iter().enumerate() {
25        let (functor, arity) = match clause.head.functor_arity() {
26            Some(fa) => fa,
27            None => continue, // Skip malformed clauses
28        };
29
30        let entry = index
31            .entry((functor, arity))
32            .or_insert_with(|| PredicateEntry {
33                arg_index: FnvHashMap::default(),
34                var_clause_indices: Vec::new(),
35                all_clause_indices: Vec::new(),
36            });
37
38        entry.all_clause_indices.push(clause_idx);
39
40        match clause.head.first_arg_key() {
41            Some(key) => {
42                entry
43                    .arg_index
44                    .entry(key)
45                    .or_insert_with(Vec::new)
46                    .push(clause_idx);
47            }
48            None => {
49                // Variable or no first arg — goes to var_clauses
50                if let Term::Compound { args, .. } = &clause.head {
51                    if !args.is_empty() && args[0].is_var() {
52                        entry.var_clause_indices.push(clause_idx);
53                    }
54                }
55                // Atom head (arity 0) has no first arg — no special indexing needed
56            }
57        }
58    }
59
60    index
61}
62
63/// Look up candidate clause indices for a goal.
64/// Returns indices into the clauses Vec.
65pub fn lookup_clauses(index: &PredicateIndex, goal: &Term, clauses: &[Clause]) -> Vec<usize> {
66    let _ = clauses; // used for type reference only
67    let (functor, arity) = match goal.functor_arity() {
68        Some(fa) => fa,
69        None => return vec![],
70    };
71
72    let entry = match index.get(&(functor, arity)) {
73        Some(e) => e,
74        None => return vec![],
75    };
76
77    // If any clause has a variable first arg, we must use all_clauses to preserve ordering
78    if !entry.var_clause_indices.is_empty() {
79        return entry.all_clause_indices.clone();
80    }
81
82    // Try to use first-arg indexing if the goal's first arg is ground
83    let first_arg_key = match goal {
84        Term::Compound { args, .. } if !args.is_empty() => {
85            match &args[0] {
86                Term::Atom(id) => Some(FirstArgKey::Atom(*id)),
87                Term::Integer(n) => Some(FirstArgKey::Integer(*n)),
88                Term::Compound { functor, args } => {
89                    Some(FirstArgKey::Functor(*functor, args.len()))
90                }
91                _ => None, // Variable first arg in query -> use all_clauses
92            }
93        }
94        _ => None,
95    };
96
97    match first_arg_key {
98        Some(key) => {
99            match entry.arg_index.get(&key) {
100                Some(indices) => indices.clone(),
101                None => vec![], // ground arg with no matching clause — no candidates
102            }
103        }
104        None => entry.all_clause_indices.clone(),
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::term::{Clause, Term};
112
113    fn make_fact(functor: AtomId, args: Vec<Term>) -> Clause {
114        Clause {
115            head: Term::Compound { functor, args },
116            body: vec![],
117        }
118    }
119
120    #[test]
121    fn test_build_index_basic() {
122        let clauses = vec![
123            make_fact(0, vec![Term::Atom(1)]),
124            make_fact(0, vec![Term::Atom(2)]),
125            make_fact(0, vec![Term::Atom(3)]),
126        ];
127        let index = build_index(&clauses);
128        let entry = index.get(&(0, 1)).unwrap();
129        assert_eq!(entry.all_clause_indices, vec![0, 1, 2]);
130        assert!(entry.var_clause_indices.is_empty());
131        assert_eq!(entry.arg_index.len(), 3);
132    }
133
134    #[test]
135    fn test_lookup_with_ground_arg() {
136        let clauses = vec![
137            make_fact(0, vec![Term::Atom(1)]),
138            make_fact(0, vec![Term::Atom(2)]),
139            make_fact(0, vec![Term::Atom(3)]),
140        ];
141        let index = build_index(&clauses);
142
143        // Lookup with specific first arg
144        let goal = Term::Compound {
145            functor: 0,
146            args: vec![Term::Atom(2)],
147        };
148        let result = lookup_clauses(&index, &goal, &clauses);
149        assert_eq!(result, vec![1]); // only the matching clause
150    }
151
152    #[test]
153    fn test_lookup_with_variable_arg() {
154        let clauses = vec![
155            make_fact(0, vec![Term::Atom(1)]),
156            make_fact(0, vec![Term::Atom(2)]),
157            make_fact(0, vec![Term::Atom(3)]),
158        ];
159        let index = build_index(&clauses);
160
161        // Lookup with variable first arg -> all clauses
162        let goal = Term::Compound {
163            functor: 0,
164            args: vec![Term::Var(0)],
165        };
166        let result = lookup_clauses(&index, &goal, &clauses);
167        assert_eq!(result, vec![0, 1, 2]);
168    }
169
170    #[test]
171    fn test_lookup_unknown_predicate() {
172        let clauses = vec![make_fact(0, vec![Term::Atom(1)])];
173        let index = build_index(&clauses);
174
175        let goal = Term::Compound {
176            functor: 99,
177            args: vec![Term::Atom(1)],
178        };
179        let result = lookup_clauses(&index, &goal, &clauses);
180        assert!(result.is_empty());
181    }
182
183    #[test]
184    fn test_mixed_ground_and_var_clauses() {
185        // When some clauses have variable first arg, all lookups use all_clauses
186        let clauses = vec![
187            make_fact(0, vec![Term::Atom(1)]),
188            make_fact(0, vec![Term::Atom(2)]),
189            Clause {
190                head: Term::Compound {
191                    functor: 0,
192                    args: vec![Term::Var(0)],
193                },
194                body: vec![],
195            },
196        ];
197        let index = build_index(&clauses);
198
199        let goal = Term::Compound {
200            functor: 0,
201            args: vec![Term::Atom(1)],
202        };
203        let result = lookup_clauses(&index, &goal, &clauses);
204        // Must return all clauses because there's a variable-headed clause
205        assert_eq!(result, vec![0, 1, 2]);
206    }
207
208    #[test]
209    fn test_multiple_predicates() {
210        let clauses = vec![
211            make_fact(0, vec![Term::Atom(10)]), // color(red)
212            make_fact(0, vec![Term::Atom(11)]), // color(blue)
213            make_fact(1, vec![Term::Atom(20)]), // shape(circle)
214        ];
215        let index = build_index(&clauses);
216
217        // color lookup
218        let goal_color = Term::Compound {
219            functor: 0,
220            args: vec![Term::Var(0)],
221        };
222        assert_eq!(lookup_clauses(&index, &goal_color, &clauses), vec![0, 1]);
223
224        // shape lookup
225        let goal_shape = Term::Compound {
226            functor: 1,
227            args: vec![Term::Var(0)],
228        };
229        assert_eq!(lookup_clauses(&index, &goal_shape, &clauses), vec![2]);
230    }
231
232    #[test]
233    fn test_no_match_returns_empty() {
234        let clauses = vec![
235            make_fact(0, vec![Term::Atom(1)]),
236            make_fact(0, vec![Term::Atom(2)]),
237        ];
238        let index = build_index(&clauses);
239
240        // Query with an atom that doesn't match any first arg — no candidates
241        let goal = Term::Compound {
242            functor: 0,
243            args: vec![Term::Atom(99)],
244        };
245        let result = lookup_clauses(&index, &goal, &clauses);
246        assert_eq!(result, vec![]);
247    }
248}