1use crate::nary::{BodyAtomPattern, NaryRulePattern, PatternVar};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct HostRelation {
21 pub rows: Vec<Vec<u64>>,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct ReferenceCoverage {
27 pub positives_covered: u32,
28 pub negatives_covered: u32,
29}
30
31pub fn score_pattern_reference(
39 pattern: &NaryRulePattern,
40 candidates: &[HostRelation],
41 positives: &[Vec<u64>],
42 negatives: &[Vec<u64>],
43) -> ReferenceCoverage {
44 let count = |examples: &[Vec<u64>]| -> u32 {
45 examples
46 .iter()
47 .filter(|example| covers(pattern, candidates, example))
48 .count() as u32
49 };
50 ReferenceCoverage {
51 positives_covered: count(positives),
52 negatives_covered: count(negatives),
53 }
54}
55
56fn covers(pattern: &NaryRulePattern, candidates: &[HostRelation], example: &[u64]) -> bool {
58 let join_count = join_variable_count(pattern);
59 let mut joins: Vec<Option<u64>> = vec![None; join_count];
60 satisfy(&pattern.body, candidates, example, &mut joins)
61}
62
63fn satisfy(
65 body: &[BodyAtomPattern],
66 candidates: &[HostRelation],
67 example: &[u64],
68 joins: &mut Vec<Option<u64>>,
69) -> bool {
70 let Some((atom, rest)) = body.split_first() else {
71 return true;
72 };
73 let relation = &candidates[atom.candidate_slot as usize];
74 'rows: for row in &relation.rows {
75 assert_eq!(
80 row.len(),
81 atom.bindings.len(),
82 "candidate relation row width does not match the atom's bindings",
83 );
84 let mut newly_bound: Vec<u8> = Vec::new();
87 for (value, binding) in row.iter().zip(&atom.bindings) {
88 match *binding {
89 PatternVar::Head(i) => {
90 if example[i as usize] != *value {
91 undo(joins, &newly_bound);
92 continue 'rows;
93 }
94 }
95 PatternVar::Join(j) => match joins[j as usize] {
96 Some(bound) => {
97 if bound != *value {
98 undo(joins, &newly_bound);
99 continue 'rows;
100 }
101 }
102 None => {
103 joins[j as usize] = Some(*value);
104 newly_bound.push(j);
105 }
106 },
107 }
108 }
109 if satisfy(rest, candidates, example, joins) {
110 return true;
111 }
112 undo(joins, &newly_bound);
113 }
114 false
115}
116
117fn undo(joins: &mut [Option<u64>], newly_bound: &[u8]) {
118 for j in newly_bound {
119 joins[*j as usize] = None;
120 }
121}
122
123fn join_variable_count(pattern: &NaryRulePattern) -> usize {
124 let mut count = 0usize;
125 for atom in &pattern.body {
126 for binding in &atom.bindings {
127 if let PatternVar::Join(j) = *binding {
128 count = count.max(j as usize + 1);
129 }
130 }
131 }
132 count
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::nary::canonical_binary_pattern;
139 use crate::types::Topology;
140
141 fn pairs(rows: &[(u64, u64)]) -> HostRelation {
142 HostRelation {
143 rows: rows.iter().map(|(a, b)| vec![*a, *b]).collect(),
144 }
145 }
146
147 #[test]
154 fn reference_matches_shipped_binary_kernel_fixture() {
155 let p_b = pairs(&[(1, 2), (2, 3)]);
156 let p_c = pairs(&[(2, 4), (3, 5), (4, 6)]);
157 let candidates = vec![p_b, p_c];
158 let positives = vec![vec![1u64, 4u64], vec![2, 5]];
159 let negatives = vec![vec![7u64, 8u64]];
160
161 let mut nonzero = Vec::new();
162 for topology in Topology::ALL {
163 for left in 0..2u32 {
164 for right in 0..2u32 {
165 let pattern = canonical_binary_pattern(topology, left, right);
166 let coverage =
167 score_pattern_reference(&pattern, &candidates, &positives, &negatives);
168 assert_eq!(
169 coverage.negatives_covered, 0,
170 "{topology:?}({left},{right}) covered a negative"
171 );
172 if coverage.positives_covered > 0 {
173 nonzero.push((topology, left, right, coverage.positives_covered));
174 }
175 }
176 }
177 }
178 assert_eq!(
179 nonzero,
180 vec![(Topology::Chain, 0, 1, 2)],
181 "coverage disagrees with the shipped kernel fixture"
182 );
183 }
184
185 #[test]
191 fn ternary_head_join_consistency_is_enforced() {
192 use crate::nary::{BodyAtomPattern, NaryRulePattern};
193 use PatternVar::{Head, Join};
194 let pattern = NaryRulePattern {
195 head_arity: 3,
196 body: vec![
197 BodyAtomPattern {
198 candidate_slot: 0,
199 bindings: vec![Head(0), Head(1), Join(0)],
200 },
201 BodyAtomPattern {
202 candidate_slot: 1,
203 bindings: vec![Join(0), Head(2)],
204 },
205 ],
206 };
207 let ternary = HostRelation {
208 rows: vec![vec![1, 2, 9], vec![4, 5, 8]],
209 };
210 let binary = pairs(&[(9, 3), (8, 7)]);
211 let candidates = vec![ternary, binary];
212 let positives = vec![vec![1u64, 2, 3], vec![4, 5, 6], vec![1, 2, 7]];
213 let coverage = score_pattern_reference(&pattern, &candidates, &positives, &[]);
214 assert_eq!(coverage.positives_covered, 1);
215
216 let per_example: Vec<u32> = positives
221 .iter()
222 .map(|example| {
223 score_pattern_reference(&pattern, &candidates, std::slice::from_ref(example), &[])
224 .positives_covered
225 })
226 .collect();
227 assert_eq!(
228 per_example,
229 vec![1, 0, 0],
230 "(1,2,3) must be the covered example; (4,5,6) and (1,2,7) must not be",
231 );
232 }
233
234 #[test]
237 fn single_occurrence_join_is_a_dont_care_position() {
238 let pattern = canonical_binary_pattern(Topology::Fanout, 0, 1);
239 let l = pairs(&[(1, 999)]);
241 let r = pairs(&[(1, 5)]);
242 let coverage = score_pattern_reference(&pattern, &[l, r], &[vec![1u64, 5u64]], &[]);
243 assert_eq!(coverage.positives_covered, 1);
244 }
245
246 #[test]
250 fn backtracking_revisits_earlier_atom_rows() {
251 use crate::nary::{BodyAtomPattern, NaryRulePattern};
252 use PatternVar::{Head, Join};
253 let pattern = NaryRulePattern {
254 head_arity: 2,
255 body: vec![
256 BodyAtomPattern {
257 candidate_slot: 0,
258 bindings: vec![Head(0), Join(0)],
259 },
260 BodyAtomPattern {
261 candidate_slot: 1,
262 bindings: vec![Join(0), Head(1)],
263 },
264 ],
265 };
266 let t = pairs(&[(1, 8), (1, 9)]);
267 let p = pairs(&[(9, 2)]);
268 let coverage = score_pattern_reference(&pattern, &[t, p], &[vec![1u64, 2u64]], &[]);
269 assert_eq!(coverage.positives_covered, 1);
270 }
271}