diff --git a/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/SynthesisAffixProcessRule.cs b/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/SynthesisAffixProcessRule.cs index f3191145f..fbea71375 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/SynthesisAffixProcessRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/SynthesisAffixProcessRule.cs @@ -137,8 +137,14 @@ public IEnumerable Apply(Word input) var appliedAllomorphIndices = new HashSet(); var output = new List(); + // Allomorphs after the recorded one can never yield output, so the loop ends there. + int recordedIndex = -1; + if (input.CurrentRecordedAllomorph is AffixProcessAllomorph recorded) + recordedIndex = _rule.Allomorphs.IndexOf(recorded); for (int i = 0; i < _rules.Count; i++) { + if (recordedIndex >= 0 && i > recordedIndex) + break; AffixProcessAllomorph allo = _rule.Allomorphs[i]; MprFeatureGroup group; if ( diff --git a/src/SIL.Machine.Morphology.HermitCrab/Word.cs b/src/SIL.Machine.Morphology.HermitCrab/Word.cs index a1722a18a..aefe26cc9 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/Word.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/Word.cs @@ -260,6 +260,12 @@ internal bool IsAllomorphApplicable(Allomorph allomorph) return curAllomorph == null || curAllomorph == allomorph; } + /// + /// The allomorph that analysis recorded for the rule now being applied, or null when any allomorph is + /// acceptable or no rule is pending. + /// + internal Allomorph CurrentRecordedAllomorph => _mruleAppIndex < 0 ? null : _allomorphApps[_mruleAppIndex]; + internal bool HasRemainingRulesFromStratum(Stratum stratum) { if (_mruleAppIndex < 0) diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorphologicalRules/AffixProcessRuleTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorphologicalRules/AffixProcessRuleTests.cs index 278f46155..f1e6ede38 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorphologicalRules/AffixProcessRuleTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorphologicalRules/AffixProcessRuleTests.cs @@ -2088,4 +2088,123 @@ public void IsAllomorphApplicable() Assert.That(output.Count(), Is.EqualTo(1)); Assert.That(output.First().ToString(), Is.EqualTo("sagz")); } + + private AffixProcessRule AddThreeSegmentSuffix( + string seg0, + string env0, + string seg1, + string env1, + string seg2, + string env2 + ) + { + FeatureStruct any = FeatureStruct.New().Symbol(HCFeatureSystem.Segment).Value; + var sSuffix = new AffixProcessRule + { + Name = "s_suffix", + Gloss = "3SG", + RequiredSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("V").Value, + }; + foreach ((string seg, string env) in new[] { (seg0, env0), (seg1, env1), (seg2, env2) }) + { + var allomorph = new AffixProcessAllomorph + { + Lhs = { Pattern.New("1").Annotation(any).OneOrMore.Value }, + Rhs = { new CopyFromInput("1"), new InsertSegments(Table3, seg) }, + }; + if (env.Length > 0) + { + allomorph.Environments.Add( + new AllomorphEnvironment( + ConstraintType.Require, + null, + Pattern.New().Annotation(Character(Table3, env)).Value + ) + ); + } + sSuffix.Allomorphs.Add(allomorph); + } + Morphophonemic.MorphologicalRules.Add(sSuffix); + + var edSuffix = new AffixProcessRule + { + Name = "ed_suffix", + Gloss = "PAST", + RequiredSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("V").Value, + }; + edSuffix.Allomorphs.Add( + new AffixProcessAllomorph + { + Lhs = { Pattern.New("1").Annotation(any).OneOrMore.Value }, + Rhs = { new CopyFromInput("1"), new InsertSegments(Table3, "+ɯd") }, + } + ); + Morphophonemic.MorphologicalRules.Add(edSuffix); + return sSuffix; + } + + private static void CollectSubruleApplications( + Trace trace, + IMorphologicalRule rule, + List<(int Index, int Recorded)> found + ) + { + if ( + trace.Type == TraceType.MorphologicalRuleSynthesis + && trace.Source == rule + && trace.SubruleIndex >= 0 + && trace.Input?.CurrentRecordedAllomorph is AffixProcessAllomorph recorded + ) + { + found.Add((trace.SubruleIndex, ((AffixProcessRule)rule).Allomorphs.IndexOf(recorded))); + } + foreach (Trace child in trace.Children) + CollectSubruleApplications(child, rule, found); + } + + private List<(int Index, int Recorded)> ParseTraced(AffixProcessRule rule, string word, params string[] expected) + { + TraceManager.IsTracing = true; + var morpher = new Morpher(TraceManager, Language); + AssertMorphsEqual(morpher.ParseWord(word, out object trace), expected); + TraceManager.IsTracing = false; + var found = new List<(int Index, int Recorded)>(); + CollectSubruleApplications((Trace)trace, rule, found); + return found; + } + + [Test] + public void SynthesisStopsAtRecordedAllomorph() + { + AffixProcessRule sSuffix = AddThreeSegmentSuffix("s", "ɯ", "z", "a", "z", "a"); + + var applications = ParseTraced(sSuffix, "sagsɯd", "32 3SG PAST"); + + Assert.That(applications, Is.Not.Empty); + Assert.That(applications.Any(a => a.Index == a.Recorded), Is.True); + Assert.That(applications.Any(a => a.Index > a.Recorded), Is.False); + } + + [Test] + public void SynthesisTriesEveryAllomorphUpToRecordedLast() + { + AffixProcessRule sSuffix = AddThreeSegmentSuffix("s", "a", "z", "a", "z", "ɯ"); + + var applications = ParseTraced(sSuffix, "sagzɯd", "32 3SG PAST"); + + Assert.That(applications.Any(a => a.Recorded == 2 && a.Index == 2), Is.True); + Assert.That(applications.Any(a => a.Index > a.Recorded), Is.False); + } + + [Test] + public void EarlierEnvironmentFreeAllomorphStillBlocksLaterOnes() + { + AffixProcessRule sSuffix = AddThreeSegmentSuffix("s", "", "z", "ɯ", "z", "ɯ"); + + // allomorph 0 has no environment, so the disjunctive property rejects the later z allomorphs + var applications = ParseTraced(sSuffix, "sagzɯd"); + + Assert.That(applications.Any(a => a.Index > a.Recorded), Is.False); + ParseTraced(sSuffix, "sagsɯd", "32 3SG PAST"); + } }