diff --git a/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs b/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs index 9044a8488..407c7fdb1 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs @@ -41,10 +41,10 @@ public IEnumerable Apply(Word input) if (_morpher.TraceManager.IsTracing) _morpher.TraceManager.BeginUnapplyTemplate(_template, input); - // Do not allow a final template to unapply if we don't need to worry about partials + // Do not allow a final template to unapply if we don't need to worry about partial rules // and a non-template was last unapplied. if ( - (!_morpher.IsPartial || _morpher.AlwaysEnforceFinalTemplates) + (!_morpher.HasPartialRules || _morpher.AlwaysEnforceFinalTemplates) && input.FinalTemplateState == FinalTemplateState.NonTemplate && _template.IsFinal ) diff --git a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs index d0675509e..2867ea6cf 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs @@ -62,7 +62,7 @@ public Morpher(ITraceManager traceManager, Language lang, int maxDegreeOfParalle RuleSelector = rule => true; _morphemes = new ReadOnlyObservableCollection(morphemes); - IsPartial = GetPartialMorphemes().Count() > 0; + HasPartialRules = GetPartialMorphemes().Any(morpheme => !(morpheme is LexEntry)); } public IEnumerable GetPartialMorphemes() @@ -98,9 +98,13 @@ public ITraceManager TraceManager public bool MergeEquivalentAnalyses { get; set; } /// - /// A Morpher is partial if any of the elements are partial. + /// True if any morpheme other than a is partial. Analysis may prune a final + /// template that follows a non-template rule only when this is false. Partial lexical entries do not + /// count: applies no template to a word whose root is partial, + /// and when it substitutes an inflectional-family relative for the root, the relative's word is + /// non-partial, so a following non-partial rule is blocked either way. /// - public bool IsPartial { get; } + public bool HasPartialRules { get; } /// /// Enforce final templates even if some of the morphemes were partial. @@ -491,8 +495,6 @@ private IEnumerable LexicalGuess(Word input) SyntacticFeatureStruct = input.SyntacticFeatureStruct, Stratum = input.Stratum, }; - // Don't update Morpher.IsPartial even if lexEntry.IsPartial is true. - // We don't mind if final templates are filtered during analysis in this case. lexEntry.Allomorphs.Add(root); // Point the root allomorph to the lexical pattern in FieldWorks. if (lexicalPattern.Morpheme != null) diff --git a/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisAffixProcessRule.cs b/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisAffixProcessRule.cs index 7e841b1fb..f7f018607 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisAffixProcessRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisAffixProcessRule.cs @@ -80,7 +80,7 @@ out FeatureStruct syntacticFS outWord.SyntacticFeatureStruct = syntacticFS; } - if (!_morpher.IsPartial || _morpher.AlwaysEnforceFinalTemplates) + if (!_morpher.HasPartialRules || _morpher.AlwaysEnforceFinalTemplates) { if (!_rule.GetIsCliticRule()) { diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/AffixTemplateTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/AffixTemplateTests.cs index 5281410a8..83498f566 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/AffixTemplateTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/AffixTemplateTests.cs @@ -473,7 +473,7 @@ public void EarlyPruningOfFinalTemplate() // Verify early pruning of final template. var morpher = new Morpher(TraceManager, Language); - Assert.That(morpher.IsPartial, Is.True); + Assert.That(morpher.GetPartialMorphemes(), Is.Not.Empty); morpher.AlwaysEnforceFinalTemplates = true; TraceManager.IsTracing = true; AssertMorphsEqual(morpher.ParseWord("sagdv", out object trace)); @@ -501,6 +501,79 @@ public void EarlyPruningOfFinalTemplate() AssertMorphsEqual(morpher.ParseWord("sagdmis"), "32 PAST 53 PL"); } + [Test] + public void PartialLexicalEntryDoesNotDisableEarlyPruningOfFinalTemplate() + { + var any = FeatureStruct.New().Symbol(HCFeatureSystem.Segment).Value; + var verb = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("V").Value; + + AffixProcessRule CreateSuffix(string name, string gloss, string suffix) + { + var rule = new AffixProcessRule + { + Name = name, + Gloss = gloss, + RequiredSyntacticFeatureStruct = verb.Clone(), + }; + rule.Allomorphs.Add( + new AffixProcessAllomorph + { + Lhs = { Pattern.New("1").Annotation(any).OneOrMore.Value }, + Rhs = { new CopyFromInput("1"), new InsertSegments(Table3, suffix) }, + } + ); + return rule; + } + + var edSuffix = CreateSuffix("ed_suffix", "PAST", "d"); + var verbTemplate = new AffixTemplate { Name = "verb", RequiredSyntacticFeatureStruct = verb.Clone() }; + verbTemplate.Slots.Add(new AffixTemplateSlot(edSuffix)); + Morphophonemic.AffixTemplates.Add(verbTemplate); + var nominalizer = CreateSuffix("nominalizer", "NOM", "v"); + Morphophonemic.MorphologicalRules.Add(nominalizer); + var partialRoot = AddEntry("partial_root", verb.Clone(), Morphophonemic, "sab"); + partialRoot.IsPartial = true; + try + { + // The test grammar already has a partial entry, so only the rules decide the prune. + var morpher = new Morpher(TraceManager, Language); + Assert.That(morpher.GetPartialMorphemes(), Is.Not.Empty); + Assert.That(morpher.HasPartialRules, Is.False); + Assert.That(morpher.AlwaysEnforceFinalTemplates, Is.False); + TraceManager.IsTracing = true; + AssertMorphsEqual(morpher.ParseWord("sagdv", out object trace)); + Assert.That( + GetFailureDepth((Trace)trace, FailureReason.NonPartialRuleProhibitedAfterFinalTemplate), + Is.EqualTo(2) + ); + TraceManager.IsTracing = false; + AssertMorphsEqual(morpher.ParseWord("sagd"), "32 PAST"); + + // No template applies to a partial root, so pruning loses nothing for it. + AssertMorphsEqual(morpher.ParseWord("sab"), "partial_root"); + AssertMorphsEqual(morpher.ParseWord("sabd")); + AssertMorphsEqual(morpher.ParseWord("sabdv")); + AssertMorphsEqual(morpher.ParseWord("sabv"), "partial_root NOM"); + + // A partial rule still turns the prune off, and what it licenses after a final template survives. + nominalizer.IsPartial = true; + morpher = new Morpher(TraceManager, Language); + Assert.That(morpher.HasPartialRules, Is.True); + TraceManager.IsTracing = true; + AssertMorphsEqual(morpher.ParseWord("sagdv", out trace), "32 PAST NOM"); + Assert.That( + GetFailureDepth((Trace)trace, FailureReason.NonPartialRuleProhibitedAfterFinalTemplate), + Is.EqualTo(0) + ); + TraceManager.IsTracing = false; + } + finally + { + Morphophonemic.Entries.Remove(partialRoot); + Entries.Remove("partial_root"); + } + } + private static int GetFailureDepth(Trace trace, FailureReason reason) { if (trace == null) diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorphologicalRules/PruneDisagreeingCopiesTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorphologicalRules/PruneDisagreeingCopiesTests.cs index ffd6fd9f8..07314040a 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorphologicalRules/PruneDisagreeingCopiesTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorphologicalRules/PruneDisagreeingCopiesTests.cs @@ -228,7 +228,11 @@ public void PartialMorphemesParseTheSameWithAndWithoutPruning(bool alwaysEnforce string[] Parse(bool pruneCopies, string word) { Morpher morpher = CreateMorpher(pruneCopies); - Assert.That(morpher.IsPartial, Is.True, "entry 54 has an empty syntactic feature structure"); + Assert.That( + morpher.GetPartialMorphemes(), + Is.Not.Empty, + "entry 54 has an empty syntactic feature structure" + ); morpher.AlwaysEnforceFinalTemplates = alwaysEnforceFinalTemplates; return ParseGlosses(morpher, word); }