diff --git a/crates/engine/src/spell.rs b/crates/engine/src/spell.rs index 0eeaa6757..e439ac366 100644 --- a/crates/engine/src/spell.rs +++ b/crates/engine/src/spell.rs @@ -304,7 +304,12 @@ impl Dictionary { /// `w` made of dictionary parts allowed in compounds (`COMPOUNDFLAG`, or the begin, middle and /// end flags), each at least `COMPOUNDMIN` characters long. - fn compounded(&self, w: &[char], index: usize) -> bool { + fn compounded(&self, w: &[char], index: usize, failed: &mut HashSet<(usize, usize)>) -> bool { + // `w` is always a tail of the checked word, so its length names it: a (tail, part index) + // that failed once fails again however the earlier parts were split. + if failed.contains(&(w.len(), index)) { + return false; + } if self.compound.is_none() && self.compound_end.is_none() { return false; } @@ -322,10 +327,11 @@ impl Dictionary { if [self.compound, self.compound_end].into_iter().flatten().any(|f| self.compound_part(rest, f, true)) { return true; } - if self.compounded(rest, index + 1) { + if self.compounded(rest, index + 1, failed) { return true; } } + failed.insert((w.len(), index)); false } @@ -352,7 +358,7 @@ impl Dictionary { return true; } } - self.compounded(&w, 0) + self.compounded(&w, 0, &mut HashSet::new()) } /// Is `word` spelt correctly? Capitalised and upper-case forms of a lower-case word pass. @@ -931,6 +937,15 @@ SFX D Y 1\nSFX D 0 ed/S .\n"; } } + #[test] + fn overlapping_compound_roots_with_a_failing_ending_stay_cheap() { + // Every split of "aaa…ab" reaches the same (tail, part) states; unmemoized this is + // ~C(99, 7) searches. + let d = Dictionary::parse(b"COMPOUNDFLAG X\nCOMPOUNDMIN 1\n".as_slice(), b"1\na/X\n".as_slice()); + assert!(!d.check(&format!("{}b", "a".repeat(99)))); + assert!(d.check(&"a".repeat(5))); + } + #[test] fn runs_give_words_their_language() { use deckcraft_model::{Paragraph, Run, RunProps};