From a23f4f696e4bb1b918501ab3d632251fc1e07412 Mon Sep 17 00:00:00 2001 From: John Maxwell Date: Tue, 7 Jul 2026 10:39:03 -0700 Subject: [PATCH] Support LT-22605: Add ability to limit HermitCrab parses --- .../AnalysisLanguageRule.cs | 7 ++--- .../AnalysisStratumRule.cs | 21 +++++++++++++-- .../Morpher.cs | 4 +-- .../Rules/ParallelCombinationRuleCascade.cs | 11 ++++++++ .../MorpherTests.cs | 27 +++++++++++++++++++ 5 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/SIL.Machine.Morphology.HermitCrab/AnalysisLanguageRule.cs b/src/SIL.Machine.Morphology.HermitCrab/AnalysisLanguageRule.cs index b4673ca55..ef1617924 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/AnalysisLanguageRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/AnalysisLanguageRule.cs @@ -10,13 +10,13 @@ internal class AnalysisLanguageRule : IRule { private readonly Morpher _morpher; private readonly List _strata; - private readonly List> _rules; + private readonly List _rules; public AnalysisLanguageRule(Morpher morpher, Language language) { _morpher = morpher; _strata = language.Strata.Reverse().ToList(); - _rules = _strata.Select(stratum => stratum.CompileAnalysisRule(morpher)).ToList(); + _rules = _strata.Select(stratum => new AnalysisStratumRule(morpher, stratum)).ToList(); } public IEnumerable Apply(Word input) @@ -31,10 +31,11 @@ public IEnumerable Apply(Word input) HashSet outputSet = tempSet; outputSet.Clear(); + int alternativeCount = 0; foreach (Word inData in inputSet) { - foreach (Word outData in _rules[i].Apply(inData)) + foreach (Word outData in _rules[i].CappedApply(inData, ref alternativeCount)) { outputSet.Add(outData); results.Add(outData); diff --git a/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs b/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs index 36d9557ad..20b3bfea1 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs @@ -100,6 +100,12 @@ private IRule CompilePhonologicalRule(IPhonologicalRule prule, } public IEnumerable Apply(Word input) + { + int alternativeCount = 0; + return CappedApply(input, ref alternativeCount); + } + + internal IEnumerable CappedApply(Word input, ref int alternativeCount) { if (_morpher.TraceManager.IsTracing) _morpher.TraceManager.BeginUnapplyStratum(_stratum, input); @@ -108,6 +114,11 @@ public IEnumerable Apply(Word input) input = input.Clone(); input.Stratum = _stratum; + if (_mrulesRule is ParallelCombinationRuleCascade parallelMRulesRule) + { + parallelMRulesRule.SetMaxAlternatives(_morpher.MaxAlternatives); + } + _prulesRule.Apply(input); input.Freeze(); IDictionary shapeWord = null; @@ -125,6 +136,14 @@ public IEnumerable Apply(Word input) _morpher.TraceManager.EndUnapplyStratum(_stratum, input); foreach (Word mruleOutWord in mruleOutWords) { + alternativeCount++; + if (_morpher.MaxAlternatives > 0 && alternativeCount >= _morpher.MaxAlternatives) + { + // Not literally a timeout, but serves the same purpose. + // (A literal timeout would produce different results on different machines.) + // Stops before full enumeration because ApplyTemplates and ApplyMorphologicalRules use yield return. + throw new TimeoutException("MaxAlternatives exceeded"); + } // Skip intermediate sources from phonological rules, templates, and morphological rules. mruleOutWord.Source = origInput; if (mergeEquivalentAnalyses) @@ -141,8 +160,6 @@ public IEnumerable Apply(Word input) output.Add(mruleOutWord); if (_morpher.TraceManager.IsTracing) _morpher.TraceManager.EndUnapplyStratum(_stratum, mruleOutWord); - if (_morpher.MaxUnapplications > 0 && output.Count >= _morpher.MaxUnapplications) - break; } return output; } diff --git a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs index e4fd1879d..ff8d7ad16 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs @@ -54,7 +54,7 @@ public Morpher(ITraceManager traceManager, Language lang) _analysisRule = lang.CompileAnalysisRule(this); _synthesisRule = lang.CompileSynthesisRule(this); MaxStemCount = 2; - MaxUnapplications = 0; + MaxAlternatives = 0; MergeEquivalentAnalyses = true; LexEntrySelector = entry => true; RuleSelector = rule => true; @@ -76,7 +76,7 @@ public ITraceManager TraceManager /// to make it possible to debug words that take 30 minutes to parse /// because there are too many unapplications. /// - public int MaxUnapplications { get; set; } + public int MaxAlternatives { get; set; } /// /// Merge analyses that have equivalent shapes. diff --git a/src/SIL.Machine/Rules/ParallelCombinationRuleCascade.cs b/src/SIL.Machine/Rules/ParallelCombinationRuleCascade.cs index 9a68ea244..76fdecd96 100644 --- a/src/SIL.Machine/Rules/ParallelCombinationRuleCascade.cs +++ b/src/SIL.Machine/Rules/ParallelCombinationRuleCascade.cs @@ -29,6 +29,13 @@ IEqualityComparer comparer ) : base(rules, multiApp, comparer) { } + private int _maxAlternatives = 0; + + public void SetMaxAlternatives(int maxAlternatives) + { + _maxAlternatives = maxAlternatives; + } + public override IEnumerable Apply(TData input) { var output = new ConcurrentStack(); @@ -50,6 +57,10 @@ public override IEnumerable Apply(TData input) if (results.Length > 0) { output.PushRange(results); + if (_maxAlternatives > 0 && output.Count > _maxAlternatives) + { + throw new TimeoutException("MaxAlternatives exceeded"); + } Tuple>[] workItems = results .Where(res => !Comparer.Equals(work.Item1, res)) diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs index eb8944ad0..905663e0a 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs @@ -39,6 +39,33 @@ public void AnalyzeWord_CanAnalyze_ReturnsCorrectAnalysis() ); } + [Test] + public void AnalyzeWord_MaxAlternatives() + { + var any = FeatureStruct.New().Symbol(HCFeatureSystem.Segment).Value; + + var edSuffix = new AffixProcessRule + { + Id = "PAST", + 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); + + var morpher = new Morpher(TraceManager, Language); + morpher.MaxAlternatives = 1; + + Assert.Throws(() => morpher.AnalyzeWord("sagd")); + } + [Test] public void AnalyzeWord_CanAnalyzeLinear_ReturnsCorrectAnalysis() {