mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-09 03:51:47 +00:00
Hunspell: forms() chain loop follows NEEDAFFIX, CIRCUMFIX and FULLSTRIP
The first-level suffix loop of forms() skipped suffix rules that need another affix or are circumfix ones, and rules that would replace a whole stem without FULLSTRIP. The chain loop did not, so forms() could list a chained form the checker rejects (work/A, SFX A able/B, SFX B s/N with NEEDAFFIX N listed workables). The chain loop now skips an empty base without FULLSTRIP and does not list a form whose last suffix needs another affix or is a circumfix one; such a form is still queued, so it can carry further suffixes. The new test checks that every listed form is accepted for the three cases. Ported from storytold/wordcraft#5012 (2f3a83f6e) onto craft-proof. Adapted to flag ids (cont_holds). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
This commit is contained in:
1 parent
a4edc0dce7
commit
208de70f02
1 file changed
+38
-1
@@ -1585,11 +1585,18 @@ impl AffixDict {
|
||||
continue;
|
||||
}
|
||||
let Some(base) = form.strip_suffix(&*r.strip) else { continue };
|
||||
if base.is_empty() && !self.full_strip {
|
||||
continue;
|
||||
}
|
||||
let next = format!("{base}{}", r.add);
|
||||
if r.cont != 0 && queued.insert((next.clone(), r.cont)) {
|
||||
chain.push((next.clone(), r.cont));
|
||||
}
|
||||
out.push(next);
|
||||
// A circumfix suffix or one that needs another affix is no word on its own,
|
||||
// but a form it ends can still carry further suffixes.
|
||||
if !self.cont_holds(r, self.circumfix) && !self.cont_holds(r, self.need_affix) {
|
||||
out.push(next);
|
||||
}
|
||||
if out.len() >= MAX_FORMS {
|
||||
return out;
|
||||
}
|
||||
@@ -3072,4 +3079,34 @@ x/12
|
||||
let back = AffixDict::from_bytes(&d.to_bytes().unwrap()).unwrap();
|
||||
assert!(back.contains("nestromy") && !back.contains("stromy") && !back.contains("nestrom"));
|
||||
}
|
||||
|
||||
/// A chained suffix form is listed only when the checker accepts it: not when its last suffix
|
||||
/// needs another affix or is a circumfix one, and not when it would replace a whole stem
|
||||
/// without `FULLSTRIP`.
|
||||
#[test]
|
||||
fn chained_forms_follow_needaffix_circumfix_and_fullstrip() {
|
||||
let chain = |aff: &str, dic: &str, stem: &str| {
|
||||
let d = AffixDict::from_hunspell(aff, dic, &[]).unwrap();
|
||||
let mut forms = d.forms(stem);
|
||||
forms.sort();
|
||||
forms.dedup();
|
||||
for f in &forms {
|
||||
assert!(d.contains(f), "{stem} → {f}");
|
||||
}
|
||||
(d, forms)
|
||||
};
|
||||
let (d, forms) = chain("SET UTF-8\nNEEDAFFIX N\nSFX A Y 1\nSFX A 0 able/B .\nSFX B Y 1\nSFX B 0 s/N .\n", "1\nwork/A\n", "work");
|
||||
assert_eq!(forms, ["work", "workable"]);
|
||||
assert!(!d.contains("workables"), "its last suffix needs another affix");
|
||||
let (d, forms) = chain("SET UTF-8\nCIRCUMFIX c\nSFX A Y 1\nSFX A 0 able/B .\nSFX B Y 1\nSFX B 0 s/c .\n", "1\nwork/A\n", "work");
|
||||
assert_eq!(forms, ["work", "workable"]);
|
||||
assert!(!d.contains("workables"), "its last suffix is a circumfix one");
|
||||
let aff = "SFX A Y 1\nSFX A 0 y/B .\nSFX B Y 1\nSFX B xy z .\n";
|
||||
let (d, forms) = chain(&format!("SET UTF-8\n{aff}"), "1\nx/A\n", "x");
|
||||
assert_eq!(forms, ["x", "xy"]);
|
||||
assert!(!d.contains("z"), "a whole stem is replaced only with FULLSTRIP");
|
||||
let (d, forms) = chain(&format!("SET UTF-8\nFULLSTRIP\n{aff}"), "1\nx/A\n", "x");
|
||||
assert_eq!(forms, ["x", "xy", "z"]);
|
||||
assert!(d.contains("z"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user