diff --git a/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/SynthesisRealizationalAffixProcessRule.cs b/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/SynthesisRealizationalAffixProcessRule.cs index 1ea640909..061ae9065 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/SynthesisRealizationalAffixProcessRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/SynthesisRealizationalAffixProcessRule.cs @@ -43,6 +43,24 @@ public IEnumerable Apply(Word input) if (!_morpher.RuleSelector(_rule)) return Enumerable.Empty(); + // RealizationalRule has no multipleApplication attribute, so it applies at most once per + // word; otherwise a rule cascade that retries a matching rule against its own output would + // never terminate. + if (input.GetApplicationCount(_rule) >= 1) + { + if (_morpher.TraceManager.IsTracing) + { + _morpher.TraceManager.MorphologicalRuleNotApplied( + _rule, + -1, + input, + FailureReason.MaxApplicationCount, + 1 + ); + } + return Enumerable.Empty(); + } + if (!_rule.RealizationalFeatureStruct.Subsumes(input.RealizationalFeatureStruct)) return Enumerable.Empty(); diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs index ab5a3f483..3c92898e3 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs @@ -141,6 +141,44 @@ public void AnalyzeWord_CanAnalyzeLinear_ReturnsCorrectAnalysis() ); } + [Test] + public void ParseWord_UnblockedRealizationalRuleMatchesOwnOutput_DoesNotHang() + { + var any = FeatureStruct.New().Symbol(HCFeatureSystem.Segment).Value; + + LexEntry entry = AddEntry( + "realtest", + FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("V").Value, + Morphophonemic, + "zag" + ); + entry.MprFeatures.Add(Latinate); + + var realRule = new RealizationalAffixProcessRule { Name = "real_rule", Gloss = "REAL" }; + realRule.Allomorphs.Add( + new AffixProcessAllomorph + { + Lhs = { Pattern.New("1").Annotation(any).OneOrMore.Value }, + Rhs = { new CopyFromInput("1"), new InsertSegments(Table3, "d") }, + } + ); + realRule.Allomorphs[0].RequiredMprFeatures.Add(Latinate); + Morphophonemic.MorphologicalRules.Add(realRule); + + SetRuleOrder(MorphologicalRuleOrder.Linear); + var morpher = new Morpher(TraceManager, Language); + + Word[]? output = null; + bool completed = Task.Run(() => output = morpher.ParseWord("zag").ToArray()).Wait(TimeSpan.FromSeconds(10)); + + Assert.That( + completed, + Is.True, + "ParseWord did not return: the realizational rule reapplied to its own output without bound." + ); + AssertMorphsEqual(output!, "realtest"); + } + [Test] public void AnalyzeWord_CannotAnalyze_ReturnsEmptyEnumerable() {