From a23f4f696e4bb1b918501ab3d632251fc1e07412 Mon Sep 17 00:00:00 2001 From: John Maxwell Date: Tue, 7 Jul 2026 10:39:03 -0700 Subject: [PATCH 1/5] 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() { From 54c640af888f8fbbaac3f3b3a906ecb82bd7daa1 Mon Sep 17 00:00:00 2001 From: John Maxwell Date: Wed, 8 Jul 2026 09:01:21 -0700 Subject: [PATCH 2/5] Change RuleBatch and RuleCascade to check max alternatives --- .../AnalysisLanguageRule.cs | 5 +++- .../AnalysisStratumRule.cs | 23 +++++++++++-------- .../Rules/CombinationRuleCascade.cs | 1 + src/SIL.Machine/Rules/LinearRuleCascade.cs | 1 + .../Rules/ParallelCombinationRuleCascade.cs | 12 +--------- .../Rules/ParallelPermutationRuleCascade.cs | 1 + .../Rules/PermutationRuleCascade.cs | 1 + src/SIL.Machine/Rules/PipelineRuleCascade.cs | 3 +++ src/SIL.Machine/Rules/RuleBatch.cs | 11 ++++++++- src/SIL.Machine/Rules/RuleCascade.cs | 15 +++++++++++- 10 files changed, 49 insertions(+), 24 deletions(-) diff --git a/src/SIL.Machine.Morphology.HermitCrab/AnalysisLanguageRule.cs b/src/SIL.Machine.Morphology.HermitCrab/AnalysisLanguageRule.cs index ef1617924..9a0abc7fc 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/AnalysisLanguageRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/AnalysisLanguageRule.cs @@ -31,11 +31,14 @@ public IEnumerable Apply(Word input) HashSet outputSet = tempSet; outputSet.Clear(); + + // Limit alternatives accross all invocations of _rules[i].Apply. int alternativeCount = 0; + _rules[i].SetMaxAlternatives(_morpher.MaxAlternatives); foreach (Word inData in inputSet) { - foreach (Word outData in _rules[i].CappedApply(inData, ref alternativeCount)) + foreach (Word outData in _rules[i].Apply(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 20b3bfea1..66e50d0ef 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs @@ -10,11 +10,12 @@ namespace SIL.Machine.Morphology.HermitCrab { internal class AnalysisStratumRule : IRule { - private readonly IRule _mrulesRule; + private readonly RuleCascade _mrulesRule; private readonly IRule _prulesRule; - private readonly IRule _templatesRule; + private readonly RuleBatch _templatesRule; private readonly Stratum _stratum; private readonly Morpher _morpher; + private int _maxAlternatives; public AnalysisStratumRule(Morpher morpher, Stratum stratum) { @@ -99,13 +100,20 @@ private IRule CompilePhonologicalRule(IPhonologicalRule prule, } } + public void SetMaxAlternatives(int maxAlternatives) + { + _maxAlternatives = maxAlternatives; + _mrulesRule.SetMaxAlternatives(maxAlternatives); + _templatesRule.SetMaxAlternatives(maxAlternatives); + } + public IEnumerable Apply(Word input) { int alternativeCount = 0; - return CappedApply(input, ref alternativeCount); + return Apply(input, ref alternativeCount); } - internal IEnumerable CappedApply(Word input, ref int alternativeCount) + internal IEnumerable Apply(Word input, ref int alternativeCount) { if (_morpher.TraceManager.IsTracing) _morpher.TraceManager.BeginUnapplyStratum(_stratum, input); @@ -114,11 +122,6 @@ internal IEnumerable CappedApply(Word input, ref int alternativeCount) input = input.Clone(); input.Stratum = _stratum; - if (_mrulesRule is ParallelCombinationRuleCascade parallelMRulesRule) - { - parallelMRulesRule.SetMaxAlternatives(_morpher.MaxAlternatives); - } - _prulesRule.Apply(input); input.Freeze(); IDictionary shapeWord = null; @@ -137,7 +140,7 @@ internal IEnumerable CappedApply(Word input, ref int alternativeCount) foreach (Word mruleOutWord in mruleOutWords) { alternativeCount++; - if (_morpher.MaxAlternatives > 0 && alternativeCount >= _morpher.MaxAlternatives) + if (_morpher.MaxAlternatives > 0 && alternativeCount >= _maxAlternatives) { // Not literally a timeout, but serves the same purpose. // (A literal timeout would produce different results on different machines.) diff --git a/src/SIL.Machine/Rules/CombinationRuleCascade.cs b/src/SIL.Machine/Rules/CombinationRuleCascade.cs index 25fdfb8c6..7805c47e2 100644 --- a/src/SIL.Machine/Rules/CombinationRuleCascade.cs +++ b/src/SIL.Machine/Rules/CombinationRuleCascade.cs @@ -48,6 +48,7 @@ private void ApplyRules(TData input, HashSet rulesApplied, HashSet o } output.Add(result); + CheckMaxAlternatives(output); } } } diff --git a/src/SIL.Machine/Rules/LinearRuleCascade.cs b/src/SIL.Machine/Rules/LinearRuleCascade.cs index b4e985a33..8d05ddd2f 100644 --- a/src/SIL.Machine/Rules/LinearRuleCascade.cs +++ b/src/SIL.Machine/Rules/LinearRuleCascade.cs @@ -46,6 +46,7 @@ private bool ApplyRules(TData input, int ruleIndex, HashSet output) { output.Add(result); } + CheckMaxAlternatives(output); applied = true; } diff --git a/src/SIL.Machine/Rules/ParallelCombinationRuleCascade.cs b/src/SIL.Machine/Rules/ParallelCombinationRuleCascade.cs index 76fdecd96..871c21062 100644 --- a/src/SIL.Machine/Rules/ParallelCombinationRuleCascade.cs +++ b/src/SIL.Machine/Rules/ParallelCombinationRuleCascade.cs @@ -29,13 +29,6 @@ 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(); @@ -57,10 +50,7 @@ public override IEnumerable Apply(TData input) if (results.Length > 0) { output.PushRange(results); - if (_maxAlternatives > 0 && output.Count > _maxAlternatives) - { - throw new TimeoutException("MaxAlternatives exceeded"); - } + CheckMaxAlternatives(output); Tuple>[] workItems = results .Where(res => !Comparer.Equals(work.Item1, res)) diff --git a/src/SIL.Machine/Rules/ParallelPermutationRuleCascade.cs b/src/SIL.Machine/Rules/ParallelPermutationRuleCascade.cs index 34961739a..0f00b3b0b 100644 --- a/src/SIL.Machine/Rules/ParallelPermutationRuleCascade.cs +++ b/src/SIL.Machine/Rules/ParallelPermutationRuleCascade.cs @@ -48,6 +48,7 @@ public override IEnumerable Apply(TData input) if (results.Length > 0) { output.PushRange(results); + CheckMaxAlternatives(output); Tuple[] workItems = results .Where(res => !MultipleApplication || !Comparer.Equals(work.Item1, res)) diff --git a/src/SIL.Machine/Rules/PermutationRuleCascade.cs b/src/SIL.Machine/Rules/PermutationRuleCascade.cs index b16671f44..f2f69abc2 100644 --- a/src/SIL.Machine/Rules/PermutationRuleCascade.cs +++ b/src/SIL.Machine/Rules/PermutationRuleCascade.cs @@ -39,6 +39,7 @@ private void ApplyRules(TData input, int ruleIndex, HashSet output) if (!MultipleApplication || !Comparer.Equals(input, result)) ApplyRules(result, MultipleApplication ? i : i + 1, output); output.Add(result); + CheckMaxAlternatives(output); } } } diff --git a/src/SIL.Machine/Rules/PipelineRuleCascade.cs b/src/SIL.Machine/Rules/PipelineRuleCascade.cs index 524300883..73bde918a 100644 --- a/src/SIL.Machine/Rules/PipelineRuleCascade.cs +++ b/src/SIL.Machine/Rules/PipelineRuleCascade.cs @@ -23,7 +23,10 @@ public override IEnumerable Apply(TData input) outputSet.Clear(); foreach (TData inData in inputSet) + { outputSet.UnionWith(ApplyRule(Rules[i], i, inData)); + CheckMaxAlternatives(outputSet); + } tempSet = inputSet; inputSet = outputSet; diff --git a/src/SIL.Machine/Rules/RuleBatch.cs b/src/SIL.Machine/Rules/RuleBatch.cs index 61249068b..b58a817ed 100644 --- a/src/SIL.Machine/Rules/RuleBatch.cs +++ b/src/SIL.Machine/Rules/RuleBatch.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using SIL.Extensions; using SIL.Machine.Annotations; @@ -10,6 +11,7 @@ public class RuleBatch : IRule private readonly List> _rules; private readonly bool _disjunctive; private readonly IEqualityComparer _comparer; + private int _maxAlternatives; public RuleBatch(IEnumerable> rules) : this(rules, EqualityComparer.Default) { } @@ -42,12 +44,19 @@ public bool IsDisjunctive get { return _disjunctive; } } + public void SetMaxAlternatives(int maxAlternatives) + { + _maxAlternatives = maxAlternatives; + } + public virtual IEnumerable Apply(TData input) { var output = new HashSet(_comparer); foreach (IRule rule in _rules) { output.UnionWith(rule.Apply(input)); + if (_maxAlternatives > 0 && output.Count > _maxAlternatives) + throw new TimeoutException("MaxAlternatives exceeded"); if (_disjunctive && output.Count > 0) return output; } diff --git a/src/SIL.Machine/Rules/RuleCascade.cs b/src/SIL.Machine/Rules/RuleCascade.cs index a139e8ced..4df291687 100644 --- a/src/SIL.Machine/Rules/RuleCascade.cs +++ b/src/SIL.Machine/Rules/RuleCascade.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using SIL.Machine.Annotations; using SIL.ObjectModel; @@ -11,6 +12,7 @@ public abstract class RuleCascade : IRule private readonly ReadOnlyList> _rules; private readonly bool _multiApp; private readonly IEqualityComparer _comparer; + protected int _maxAlternatives; protected RuleCascade(IEnumerable> rules) : this(rules, false) { } @@ -47,6 +49,17 @@ public IReadOnlyList> Rules get { return _rules; } } + public void SetMaxAlternatives(int maxApplications) + { + _maxAlternatives = maxApplications; + } + + public void CheckMaxAlternatives(IEnumerable output) + { + if (_maxAlternatives > 0 && output.Count() > _maxAlternatives) + throw new TimeoutException("MaxAlternatives exceeded"); + } + public abstract IEnumerable Apply(TData input); protected virtual IEnumerable ApplyRule(IRule rule, int index, TData input) From a13eef9dcb7afc263870b8bb784388ce8bab7596 Mon Sep 17 00:00:00 2001 From: John Maxwell Date: Thu, 9 Jul 2026 08:12:51 -0700 Subject: [PATCH 3/5] Respond to Devin's code review --- .../AnalysisStratumRule.cs | 2 +- .../Rules/CombinationRuleCascade.cs | 2 +- src/SIL.Machine/Rules/LinearRuleCascade.cs | 2 +- .../Rules/ParallelCombinationRuleCascade.cs | 48 ++++++++++++------- .../Rules/ParallelPermutationRuleCascade.cs | 38 ++++++++++----- .../Rules/PermutationRuleCascade.cs | 2 +- src/SIL.Machine/Rules/PipelineRuleCascade.cs | 2 +- src/SIL.Machine/Rules/RuleCascade.cs | 4 +- 8 files changed, 64 insertions(+), 36 deletions(-) diff --git a/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs b/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs index 66e50d0ef..4b365ddd1 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs @@ -140,7 +140,7 @@ internal IEnumerable Apply(Word input, ref int alternativeCount) foreach (Word mruleOutWord in mruleOutWords) { alternativeCount++; - if (_morpher.MaxAlternatives > 0 && alternativeCount >= _maxAlternatives) + if (_maxAlternatives > 0 && alternativeCount > _maxAlternatives) { // Not literally a timeout, but serves the same purpose. // (A literal timeout would produce different results on different machines.) diff --git a/src/SIL.Machine/Rules/CombinationRuleCascade.cs b/src/SIL.Machine/Rules/CombinationRuleCascade.cs index 7805c47e2..1b8909b49 100644 --- a/src/SIL.Machine/Rules/CombinationRuleCascade.cs +++ b/src/SIL.Machine/Rules/CombinationRuleCascade.cs @@ -48,7 +48,7 @@ private void ApplyRules(TData input, HashSet rulesApplied, HashSet o } output.Add(result); - CheckMaxAlternatives(output); + CheckMaxAlternatives(output.Count); } } } diff --git a/src/SIL.Machine/Rules/LinearRuleCascade.cs b/src/SIL.Machine/Rules/LinearRuleCascade.cs index 8d05ddd2f..295450514 100644 --- a/src/SIL.Machine/Rules/LinearRuleCascade.cs +++ b/src/SIL.Machine/Rules/LinearRuleCascade.cs @@ -46,7 +46,7 @@ private bool ApplyRules(TData input, int ruleIndex, HashSet output) { output.Add(result); } - CheckMaxAlternatives(output); + CheckMaxAlternatives(output.Count); applied = true; } diff --git a/src/SIL.Machine/Rules/ParallelCombinationRuleCascade.cs b/src/SIL.Machine/Rules/ParallelCombinationRuleCascade.cs index 871c21062..981660918 100644 --- a/src/SIL.Machine/Rules/ParallelCombinationRuleCascade.cs +++ b/src/SIL.Machine/Rules/ParallelCombinationRuleCascade.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using SIL.Machine.Annotations; @@ -35,39 +36,52 @@ public override IEnumerable Apply(TData input) var from = new ConcurrentStack>>(); from.Push(Tuple.Create(input, !MultipleApplication ? new HashSet() : null)); var to = new ConcurrentStack>>(); + Exception exception = null; + int alternativeCount = 0; while (!from.IsEmpty) { to.Clear(); Parallel.ForEach( from, - work => + (work, state) => { - for (int i = 0; i < Rules.Count; i++) + try { - if ((work.Item2 == null || !work.Item2.Contains(i))) + for (int i = 0; i < Rules.Count; i++) { - TData[] results = ApplyRule(Rules[i], i, work.Item1).ToArray(); - if (results.Length > 0) + if ((work.Item2 == null || !work.Item2.Contains(i))) { - output.PushRange(results); - CheckMaxAlternatives(output); + TData[] results = ApplyRule(Rules[i], i, work.Item1).ToArray(); + if (results.Length > 0) + { + output.PushRange(results); + Interlocked.Add(ref alternativeCount, results.Length); + CheckMaxAlternatives(alternativeCount); - Tuple>[] workItems = results - .Where(res => !Comparer.Equals(work.Item1, res)) - .Select(res => - Tuple.Create( - res, - work.Item2 == null ? null : new HashSet(work.Item2) { i } + Tuple>[] workItems = results + .Where(res => !Comparer.Equals(work.Item1, res)) + .Select(res => + Tuple.Create( + res, + work.Item2 == null ? null : new HashSet(work.Item2) { i } + ) ) - ) - .ToArray(); - if (workItems.Length > 0) - to.PushRange(workItems); + .ToArray(); + if (workItems.Length > 0) + to.PushRange(workItems); + } } } } + catch (Exception ex) + { + state.Stop(); + exception = ex; + } } ); + if (exception != null) + throw exception; ConcurrentStack>> temp = from; from = to; to = temp; diff --git a/src/SIL.Machine/Rules/ParallelPermutationRuleCascade.cs b/src/SIL.Machine/Rules/ParallelPermutationRuleCascade.cs index 0f00b3b0b..bbd39fca3 100644 --- a/src/SIL.Machine/Rules/ParallelPermutationRuleCascade.cs +++ b/src/SIL.Machine/Rules/ParallelPermutationRuleCascade.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using SIL.Machine.Annotations; @@ -35,31 +36,44 @@ public override IEnumerable Apply(TData input) var from = new ConcurrentStack>(); from.Push(Tuple.Create(input, 0)); var to = new ConcurrentStack>(); + Exception exception = null; + int alternativeCount = 0; while (!from.IsEmpty) { to.Clear(); Parallel.ForEach( from, - work => + (work, state) => { - for (int i = work.Item2; i < Rules.Count; i++) + try { - TData[] results = ApplyRule(Rules[i], i, work.Item1).ToArray(); - if (results.Length > 0) + for (int i = work.Item2; i < Rules.Count; i++) { - output.PushRange(results); - CheckMaxAlternatives(output); + TData[] results = ApplyRule(Rules[i], i, work.Item1).ToArray(); + if (results.Length > 0) + { + output.PushRange(results); + Interlocked.Add(ref alternativeCount, results.Length); + CheckMaxAlternatives(alternativeCount); - Tuple[] workItems = results - .Where(res => !MultipleApplication || !Comparer.Equals(work.Item1, res)) - .Select(res => Tuple.Create(res, MultipleApplication ? i : i + 1)) - .ToArray(); - if (workItems.Length > 0) - to.PushRange(workItems); + Tuple[] workItems = results + .Where(res => !MultipleApplication || !Comparer.Equals(work.Item1, res)) + .Select(res => Tuple.Create(res, MultipleApplication ? i : i + 1)) + .ToArray(); + if (workItems.Length > 0) + to.PushRange(workItems); + } } } + catch (Exception ex) + { + state.Stop(); + exception = ex; + } } ); + if (exception != null) + throw exception; ConcurrentStack> temp = from; from = to; to = temp; diff --git a/src/SIL.Machine/Rules/PermutationRuleCascade.cs b/src/SIL.Machine/Rules/PermutationRuleCascade.cs index f2f69abc2..2c7768308 100644 --- a/src/SIL.Machine/Rules/PermutationRuleCascade.cs +++ b/src/SIL.Machine/Rules/PermutationRuleCascade.cs @@ -39,7 +39,7 @@ private void ApplyRules(TData input, int ruleIndex, HashSet output) if (!MultipleApplication || !Comparer.Equals(input, result)) ApplyRules(result, MultipleApplication ? i : i + 1, output); output.Add(result); - CheckMaxAlternatives(output); + CheckMaxAlternatives(output.Count); } } } diff --git a/src/SIL.Machine/Rules/PipelineRuleCascade.cs b/src/SIL.Machine/Rules/PipelineRuleCascade.cs index 73bde918a..899dd34f8 100644 --- a/src/SIL.Machine/Rules/PipelineRuleCascade.cs +++ b/src/SIL.Machine/Rules/PipelineRuleCascade.cs @@ -25,7 +25,7 @@ public override IEnumerable Apply(TData input) foreach (TData inData in inputSet) { outputSet.UnionWith(ApplyRule(Rules[i], i, inData)); - CheckMaxAlternatives(outputSet); + CheckMaxAlternatives(outputSet.Count); } tempSet = inputSet; diff --git a/src/SIL.Machine/Rules/RuleCascade.cs b/src/SIL.Machine/Rules/RuleCascade.cs index 4df291687..7799381f5 100644 --- a/src/SIL.Machine/Rules/RuleCascade.cs +++ b/src/SIL.Machine/Rules/RuleCascade.cs @@ -54,9 +54,9 @@ public void SetMaxAlternatives(int maxApplications) _maxAlternatives = maxApplications; } - public void CheckMaxAlternatives(IEnumerable output) + public void CheckMaxAlternatives(int alternativeCount) { - if (_maxAlternatives > 0 && output.Count() > _maxAlternatives) + if (_maxAlternatives > 0 && alternativeCount > _maxAlternatives) throw new TimeoutException("MaxAlternatives exceeded"); } From 4cb81ce036f19cd6a1c28a7306856ef4af353139 Mon Sep 17 00:00:00 2001 From: John Maxwell Date: Thu, 9 Jul 2026 08:20:22 -0700 Subject: [PATCH 4/5] Restore unit test --- src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs b/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs index 4b365ddd1..c7176ec1f 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs @@ -140,7 +140,7 @@ internal IEnumerable Apply(Word input, ref int alternativeCount) foreach (Word mruleOutWord in mruleOutWords) { alternativeCount++; - if (_maxAlternatives > 0 && alternativeCount > _maxAlternatives) + if (_maxAlternatives > 0 && alternativeCount >= _maxAlternatives) { // Not literally a timeout, but serves the same purpose. // (A literal timeout would produce different results on different machines.) From ae9bd9a4f41000063f15e280d84f5eb8a6bbfcf4 Mon Sep 17 00:00:00 2001 From: John Maxwell Date: Fri, 10 Jul 2026 09:19:34 -0700 Subject: [PATCH 5/5] Adds check to MaxAlternatives during synthesis --- src/SIL.Machine.Morphology.HermitCrab/Morpher.cs | 4 ++++ src/SIL.Machine/Rules/RuleBatch.cs | 2 +- src/SIL.Machine/Rules/RuleCascade.cs | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs index ff8d7ad16..c452c460a 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs @@ -303,6 +303,7 @@ private IEnumerable Synthesize(string word, IEnumerable analyses) private IEnumerable Synthesize(string word, ConcurrentQueue analyses) { var matches = new ConcurrentBag(); + int alternativeCount = 0; Exception exception = null; Parallel.ForEach( Partitioner.Create(0, analyses.Count), @@ -318,6 +319,9 @@ private IEnumerable Synthesize(string word, ConcurrentQueue analyses { foreach (Word alternative in synthesisWord.ExpandAlternatives()) { + alternativeCount++; + if (MaxAlternatives > 0 && alternativeCount >= MaxAlternatives) + throw new TimeoutException("MaxAlternatives exceeded"); foreach (Word validWord in _synthesisRule.Apply(alternative).Where(IsWordValid)) { if (IsMatch(word, validWord)) diff --git a/src/SIL.Machine/Rules/RuleBatch.cs b/src/SIL.Machine/Rules/RuleBatch.cs index b58a817ed..36a264dce 100644 --- a/src/SIL.Machine/Rules/RuleBatch.cs +++ b/src/SIL.Machine/Rules/RuleBatch.cs @@ -55,7 +55,7 @@ public virtual IEnumerable Apply(TData input) foreach (IRule rule in _rules) { output.UnionWith(rule.Apply(input)); - if (_maxAlternatives > 0 && output.Count > _maxAlternatives) + if (_maxAlternatives > 0 && output.Count >= _maxAlternatives) throw new TimeoutException("MaxAlternatives exceeded"); if (_disjunctive && output.Count > 0) return output; diff --git a/src/SIL.Machine/Rules/RuleCascade.cs b/src/SIL.Machine/Rules/RuleCascade.cs index 7799381f5..e1eed5965 100644 --- a/src/SIL.Machine/Rules/RuleCascade.cs +++ b/src/SIL.Machine/Rules/RuleCascade.cs @@ -56,7 +56,7 @@ public void SetMaxAlternatives(int maxApplications) public void CheckMaxAlternatives(int alternativeCount) { - if (_maxAlternatives > 0 && alternativeCount > _maxAlternatives) + if (_maxAlternatives > 0 && alternativeCount >= _maxAlternatives) throw new TimeoutException("MaxAlternatives exceeded"); }