comparison rust/hg-core/src/matchers.rs @ 51571:74230abb2504 stable

match: strengthen visit_children_set invariant, Recursive means "all files" My previous interpretation of "Recursive" was too relaxed: I thought it instructed the caller to do something like this: > you can stop calling `visit_children_set` because you'll need to descend into > every directory recursively, but you should still check every file if it > matches or not Whereas the real instruction seems to be: > I guarantee that everything in this subtree matches, you can stop > querying the matcher for all files and dirs altogether. The evidence to support this: - the test actually passes with the stronger invariant, revealing no exceptions from this rule - the implementation of `visit_children_set` for `DifferenceMatcher` clearly relies on this requirement, so it must hold for that not to lead to bugs.
author Arseniy Alekseyev <aalekseyev@janestreet.com>
date Mon, 15 Apr 2024 16:33:37 +0100
parents b39057b713b1
children
comparison
equal deleted inserted replaced
51570:b39057b713b1 51571:74230abb2504
2144 path: &'a HgPath, 2144 path: &'a HgPath,
2145 matching: &'a Tree, 2145 matching: &'a Tree,
2146 visit_children_set: &'a VisitChildrenSet, 2146 visit_children_set: &'a VisitChildrenSet,
2147 } 2147 }
2148 2148
2149 fn holds(matching: &Tree, vcs: &VisitChildrenSet) -> bool { 2149 fn holds(
2150 matching: &Tree,
2151 not_matching: &Tree,
2152 vcs: &VisitChildrenSet,
2153 ) -> bool {
2150 match vcs { 2154 match vcs {
2151 VisitChildrenSet::Empty => matching.is_empty(), 2155 VisitChildrenSet::Empty => matching.is_empty(),
2152 VisitChildrenSet::This => { 2156 VisitChildrenSet::This => {
2153 // `This` does not come with any obligations. 2157 // `This` does not come with any obligations.
2154 true 2158 true
2155 } 2159 }
2156 VisitChildrenSet::Recursive => { 2160 VisitChildrenSet::Recursive => {
2157 // `Recursive` does not come with any correctness 2161 // `Recursive` requires that *everything* in the
2158 // obligations. 2162 // subtree matches. This
2159 // It instructs the caller to stop calling 2163 // requirement is relied on for example in
2160 // `visit_children_set` for all 2164 // DifferenceMatcher implementation.
2161 // descendants, so may have negative performance 2165 not_matching.is_empty()
2162 // implications, but we're not testing against that
2163 // here.
2164 true
2165 } 2166 }
2166 VisitChildrenSet::Set(allowed_children) => { 2167 VisitChildrenSet::Set(allowed_children) => {
2167 // `allowed_children` does not distinguish between 2168 // `allowed_children` does not distinguish between
2168 // files and directories: if it's not included, it 2169 // files and directories: if it's not included, it
2169 // must not be matched. 2170 // must not be matched.
2184 2185
2185 pub fn check<M: Matcher + std::fmt::Debug>( 2186 pub fn check<M: Matcher + std::fmt::Debug>(
2186 matcher: &M, 2187 matcher: &M,
2187 path: &HgPath, 2188 path: &HgPath,
2188 matching: &Tree, 2189 matching: &Tree,
2190 not_matching: &Tree,
2189 visit_children_set: &VisitChildrenSet, 2191 visit_children_set: &VisitChildrenSet,
2190 ) { 2192 ) {
2191 if !holds(matching, visit_children_set) { 2193 if !holds(matching, not_matching, visit_children_set) {
2192 panic!( 2194 panic!(
2193 "{:#?}", 2195 "{:#?}",
2194 Error { 2196 Error {
2195 matcher, 2197 matcher,
2196 path, 2198 path,
2221 2223
2222 fn is_empty(&self) -> bool { 2224 fn is_empty(&self) -> bool {
2223 self.files.is_empty() && self.dirs.is_empty() 2225 self.files.is_empty() && self.dirs.is_empty()
2224 } 2226 }
2225 2227
2228 fn make(
2229 files: BTreeSet<HgPathBuf>,
2230 dirs: BTreeMap<HgPathBuf, Tree>,
2231 ) -> Self {
2232 Self {
2233 files,
2234 dirs: dirs
2235 .into_iter()
2236 .filter(|(_k, v)| (!(v.is_empty())))
2237 .collect(),
2238 }
2239 }
2240
2226 fn filter_and_check<M: Matcher + Debug>( 2241 fn filter_and_check<M: Matcher + Debug>(
2227 &self, 2242 &self,
2228 m: &M, 2243 m: &M,
2229 path: &HgPath, 2244 path: &HgPath,
2230 ) -> Self { 2245 ) -> (Self, Self) {
2231 let files: BTreeSet<HgPathBuf> = self 2246 let (files1, files2): (BTreeSet<HgPathBuf>, BTreeSet<HgPathBuf>) =
2232 .files 2247 self.files
2233 .iter() 2248 .iter()
2234 .filter(|v| m.matches(&path.join(v))) 2249 .map(|v| v.to_owned())
2235 .map(|f| f.to_owned()) 2250 .partition(|v| m.matches(&path.join(v)));
2236 .collect(); 2251 let (dirs1, dirs2): (
2237 let dirs: BTreeMap<HgPathBuf, Tree> = self 2252 BTreeMap<HgPathBuf, Tree>,
2253 BTreeMap<HgPathBuf, Tree>,
2254 ) = self
2238 .dirs 2255 .dirs
2239 .iter() 2256 .iter()
2240 .filter_map(|(k, v)| { 2257 .map(|(k, v)| {
2241 let path = path.join(k); 2258 let path = path.join(k);
2242 let v = v.filter_and_check(m, &path); 2259 let (t1, t2) = v.filter_and_check(m, &path);
2243 if v.is_empty() { 2260 ((k.clone(), t1), (k.clone(), t2))
2244 None
2245 } else {
2246 Some((k.to_owned(), v))
2247 }
2248 }) 2261 })
2249 .collect(); 2262 .unzip();
2250 let matching = Self { files, dirs }; 2263 let matching = Self::make(files1, dirs1);
2264 let not_matching = Self::make(files2, dirs2);
2251 let vcs = m.visit_children_set(path); 2265 let vcs = m.visit_children_set(path);
2252 invariants::visit_children_set::check(m, path, &matching, &vcs); 2266 invariants::visit_children_set::check(
2253 matching 2267 m,
2268 path,
2269 &matching,
2270 &not_matching,
2271 &vcs,
2272 );
2273 (matching, not_matching)
2254 } 2274 }
2255 2275
2256 fn check_matcher<M: Matcher + Debug>( 2276 fn check_matcher<M: Matcher + Debug>(
2257 &self, 2277 &self,
2258 m: &M, 2278 m: &M,
2259 expect_count: usize, 2279 expect_count: usize,
2260 ) { 2280 ) {
2261 let res = self.filter_and_check(m, &HgPathBuf::new()); 2281 let res = self.filter_and_check(m, &HgPathBuf::new());
2262 if expect_count != res.len() { 2282 if expect_count != res.0.len() {
2263 eprintln!( 2283 eprintln!(
2264 "warning: expected {} matches, got {} for {:#?}", 2284 "warning: expected {} matches, got {} for {:#?}",
2265 expect_count, 2285 expect_count,
2266 res.len(), 2286 res.0.len(),
2267 m 2287 m
2268 ); 2288 );
2269 } 2289 }
2270 } 2290 }
2271 } 2291 }