From ab8471dc529f577b582e57a513127b725b689570 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Fri, 10 Jul 2026 15:53:05 -0400 Subject: [PATCH 1/4] LT-22613: regression tests for analysis-length gate over-pruning epenthesis grammars Two failing-first tests pinning the bug where Morpher.ParseWord's default (non-tracing) path returns 0 parses while the traced path returns the correct 1 parse: RewriteRuleTests.EpenthesisRuleWithMinimalLexicon (end-to-end: root 19 "b+ubu" alone in the lexicon, Simultaneous and Iterative epenthesis, so surface "buibui"/"biubiu" outgrow the auto-derived MaxAnalysisLength bound) and MorpherTests.ComputeMaxAnalysisLength_ReturnsNull_ForInsertionRewriteRule (the bound must be inadmissible when an insertion-type rewrite subrule exists). Isolated mechanism (not the nogood/memo cache -- verified by re-parsing with the memo active and only MaxAnalysisLength nulled): epenthesis unapplication marks inserted segments Optional without removing them, so Shape.SegmentCount at AnalysisStratumRule's Phase 4 Gate B still counts them and the gate prunes a candidate whose obligatory segments fit the lexicon fine. Co-Authored-By: Claude Sonnet 5 --- .../MorpherTests.cs | 35 ++++++ .../PhonologicalRules/RewriteRuleTests.cs | 105 ++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs index 1f5b6c24..2e3125c8 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs @@ -852,4 +852,39 @@ public void IsEdgeStripperQualified_ReturnsFalse_ForInfixation() Allophonic.MorphologicalRules.Remove(infix); } } + + [Test] + public void ComputeMaxAnalysisLength_ReturnsNull_ForInsertionRewriteRule() + { + // LT-22613: an insertion-type rewrite subrule (Rhs longer than Lhs -- epenthesis when Lhs is + // empty) makes the analysis length bound inadmissible: its unapplication marks the inserted + // surface segments OPTIONAL rather than removing them (EpenthesisAnalysisRewriteRuleSpec/ + // NarrowAnalysisRewriteRuleSpec), so a candidate's segment count at AnalysisStratumRule's gate + // no longer bounds the underlying length the candidate could still justify. Per this class's + // own ground rule, that means "no admissible bound" (null, gate off), not a guessed bound. + var highVowel = FeatureStruct + .New(Language.PhonologicalFeatureSystem) + .Symbol(HCFeatureSystem.Segment) + .Symbol("cons-") + .Symbol("voc+") + .Symbol("high+") + .Value; + var epenthesis = new RewriteRule { Name = "epenthesis", ApplicationMode = RewriteApplicationMode.Simultaneous }; + epenthesis.Subrules.Add( + new RewriteSubrule + { + Rhs = Pattern.New().Annotation(highVowel).Value, + LeftEnvironment = Pattern.New().Annotation(highVowel).Value, + } + ); + Allophonic.PhonologicalRules.Add(epenthesis); + try + { + Assert.That(GrammarAnalyzer.ComputeMaxAnalysisLength(Language, 0), Is.Null); + } + finally + { + Allophonic.PhonologicalRules.Remove(epenthesis); + } + } } diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/PhonologicalRules/RewriteRuleTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PhonologicalRules/RewriteRuleTests.cs index 68913bba..6495ca4a 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/PhonologicalRules/RewriteRuleTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PhonologicalRules/RewriteRuleTests.cs @@ -1341,6 +1341,111 @@ public void EpenthesisRules() AssertMorphsEqual(morpher.ParseWord("butubu"), "25"); } + [Test] + public void EpenthesisRuleWithMinimalLexicon() + { + // LT-22613 regression: the default (non-tracing) parse path must give the same answer as the + // traced path. EpenthesisRules above only passes because this fixture's large lexicon makes + // GrammarAnalyzer.ComputeMaxAnalysisLength's auto-derived bound big enough to hide the bug: an + // epenthesis rule's unapplication marks the inserted segments OPTIONAL (it never removes them, + // see EpenthesisAnalysisRewriteRuleSpec.Unapply), so a surface form that outgrew the longest + // root via epenthesis still counts all its segments at AnalysisStratumRule's length gate and + // was pruned as "unreachable" -- 0 parses non-traced, 1 parse traced. This grammar is + // EpenthesisRules sub-case (1) with the lexicon cut down to root 19 alone ("b+ubu", 4 + // segments), so surface "buibui" (6 segments) exceeds any bound derived from the lexicon. + var highVowel = FeatureStruct + .New(Language.PhonologicalFeatureSystem) + .Symbol(HCFeatureSystem.Segment) + .Symbol("cons-") + .Symbol("voc+") + .Symbol("high+") + .Value; + var highFrontUnrndVowel = FeatureStruct + .New(Language.PhonologicalFeatureSystem) + .Symbol(HCFeatureSystem.Segment) + .Symbol("cons-") + .Symbol("voc+") + .Symbol("high+") + .Symbol("back-") + .Symbol("round-") + .Value; + + var morphophonemic = new Stratum(Table3) + { + Name = "Morphophonemic", + MorphologicalRuleOrder = MorphologicalRuleOrder.Unordered, + }; + var allophonic = new Stratum(Table1) + { + Name = "Allophonic", + MorphologicalRuleOrder = MorphologicalRuleOrder.Unordered, + }; + var surface = new Stratum(Table1) { Name = "Surface", MorphologicalRuleOrder = MorphologicalRuleOrder.Unordered }; + + var entry = new LexEntry + { + Id = "19", + SyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("N").Value, + Gloss = "19", + }; + entry.Allomorphs.Add(new RootAllomorph(new Segments(Table3, "b+ubu", true))); + morphophonemic.Entries.Add(entry); + + var rule4 = new RewriteRule { Name = "rule4", ApplicationMode = RewriteApplicationMode.Simultaneous }; + allophonic.PhonologicalRules.Add(rule4); + rule4.Subrules.Add( + new RewriteSubrule + { + Rhs = Pattern.New().Annotation(highFrontUnrndVowel).Value, + LeftEnvironment = Pattern.New().Annotation(highVowel).Value, + } + ); + + var lang = new Language + { + Name = "EpenthesisMinimal", + PhonologicalFeatureSystem = Language.PhonologicalFeatureSystem, + SyntacticFeatureSystem = Language.SyntacticFeatureSystem, + Strata = { morphophonemic, allophonic, surface }, + }; + + var morpher = new Morpher(TraceManager, lang); + AssertMorphsEqual(morpher.ParseWord("buibui"), "19"); + + // The same over-prune is not Simultaneous-specific -- any insertion-type subrule's + // unapplication leaves the inserted segments in the shape as optional nodes. This is + // EpenthesisRules's Iterative cons_highBackRndVowel sub-case, same minimal lexicon. + var highBackRndVowel = FeatureStruct + .New(Language.PhonologicalFeatureSystem) + .Symbol(HCFeatureSystem.Segment) + .Symbol("cons-") + .Symbol("voc+") + .Symbol("high+") + .Symbol("back+") + .Symbol("round+") + .Value; + var cons = FeatureStruct + .New(Language.PhonologicalFeatureSystem) + .Symbol(HCFeatureSystem.Segment) + .Symbol("cons+") + .Symbol("voc-") + .Value; + + rule4.ApplicationMode = RewriteApplicationMode.Iterative; + rule4.Subrules.Clear(); + rule4.Subrules.Add( + new RewriteSubrule + { + Rhs = Pattern.New().Annotation(highFrontUnrndVowel).Value, + LeftEnvironment = Pattern.New().Annotation(cons).Value, + RightEnvironment = Pattern.New().Annotation(highBackRndVowel).Value, + } + ); + + morpher = new Morpher(TraceManager, lang); + AssertMorphsEqual(morpher.ParseWord("biubiu"), "19"); + } + [Test] public void DeletionRules() { From 9a43d25175bd724723c645e4b0fbca16da4f17cc Mon Sep 17 00:00:00 2001 From: John Lambert Date: Fri, 10 Jul 2026 17:21:02 -0400 Subject: [PATCH 2/4] LT-22613: no analysis-length bound for grammars with insertion rewrite subrules Fixes the non-tracing/tracing divergence isolated in the previous commit: the Phase 4 Gate B bound (GrammarAnalyzer.ComputeMaxAnalysisLength) treated insertion-type rewrite subrules (Rhs longer than Lhs; epenthesis when Lhs is empty) as contributing nothing, but their ANALYSIS-side unapplication marks the possibly-rule-inserted surface segments Optional instead of removing them (EpenthesisAnalysisRewriteRuleSpec / NarrowAnalysisRewriteRuleSpec), so Shape.SegmentCount at AnalysisStratumRule's gate keeps counting them. A surface form that legally outgrew the longest root via epenthesis (fixture: root "b+ubu", surface "buibui", auto-derived bound 4 < 6 segments) was pruned as unreachable on the default path while the traced path -- which bypasses the gate -- parsed it correctly. Mode-independent: Iterative epenthesis over-prunes identically (verified), so the fix keys on subrule shape, not Simultaneous mode. Per GrammarAnalyzer's own ground rule (never guess an admissible bound), an insertion subrule now makes the bound null (gate off), same as compounding or unmeasurable patterns. The nogood/memo cache (AnalysisScope) was ruled out: with the memo fully active and only MaxAnalysisLength nulled, the parse is correct. Co-Authored-By: Claude Sonnet 5 --- .../GrammarAnalyzer.cs | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/SIL.Machine.Morphology.HermitCrab/GrammarAnalyzer.cs b/src/SIL.Machine.Morphology.HermitCrab/GrammarAnalyzer.cs index f6428230..2e906623 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/GrammarAnalyzer.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/GrammarAnalyzer.cs @@ -17,10 +17,12 @@ namespace SIL.Machine.Morphology.HermitCrab /// when NO combination of rules in the grammar could ever produce something that long, regardless of /// which specific root or derivation path is under consideration. Returns null (meaning "no admissible /// bound, gate off") the moment any rule's shape falls outside what this class knows how to measure - /// exactly (quantifiers/groups/alternations in a phonological Lhs/Rhs, or a compounding rule present - /// at all, since compounding combines multiple full root lengths rather than adding a bounded affix) - /// -- per the plan's own rule: skipping only costs pruning opportunity, an admissible bound must never - /// be guessed. + /// exactly (quantifiers/groups/alternations in a phonological Lhs/Rhs, an insertion-type rewrite + /// subrule -- epenthesis/expansion, whose unapplication marks surface segments optional instead of + /// removing them, so the running shape length stops bounding the underlying length; see LT-22613 -- + /// or a compounding rule present at all, since compounding combines multiple full root lengths rather + /// than adding a bounded affix) -- per the plan's own rule: skipping only costs pruning opportunity, + /// an admissible bound must never be guessed. /// public static class GrammarAnalyzer { @@ -73,6 +75,20 @@ RealizationalAffixProcessRule rule in stratum.MorphologicalRules.OfType rhsCount) phonoGrowthRate += lhsCount - rhsCount; } From 93e52dc9da0a6e50122c22134d2cd42d8c6de503 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Fri, 10 Jul 2026 17:50:49 -0400 Subject: [PATCH 3/4] LT-22613: broaden analysis-length bail-out to any Lhs/Rhs count mismatch Independent adversarial review of the insertion-only fix (9a43d251) found the same over-prune survives for deletion/coalescence subrules (Lhs longer than Rhs): NarrowAnalysisRewriteRuleSpec.Unapply always adds Lhs.Count new (Optional, not Deleted) nodes per match site regardless of Rhs.Count, while leaving the matched Rhs-shaped region in place (also not Deleted) -- so real per-site growth is always Lhs.Count, never the Lhs.Count - Rhs.Count this method budgeted. That undercounts by Rhs.Count whenever Rhs.Count > 0, independent of which direction the count difference runs. Broadens the bail-out from "Rhs longer than Lhs" to "counts differ at all", which makes the now-unreachable deletion-reapplication compounding loop (and the DeletionReapplications parameter it depended on) dead code; removed both, along with the doc-comment claim that insertion was uniquely unsound. Co-Authored-By: Claude Sonnet 5 --- .../GrammarAnalyzer.cs | 77 ++++++++----------- .../Morpher.cs | 10 +-- .../MorpherTests.cs | 35 ++++++++- 3 files changed, 71 insertions(+), 51 deletions(-) diff --git a/src/SIL.Machine.Morphology.HermitCrab/GrammarAnalyzer.cs b/src/SIL.Machine.Morphology.HermitCrab/GrammarAnalyzer.cs index 2e906623..6d4c38d3 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/GrammarAnalyzer.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/GrammarAnalyzer.cs @@ -12,17 +12,16 @@ namespace SIL.Machine.Morphology.HermitCrab /// "Gate B" -- Gate A, a mirror-image synthesis-side bound, was attempted and reverted; see the note /// in ). The bound is a deliberately loose over-approximation /// -- summed across every rule's own already-declared reapplication limit - /// (, - /// ), never estimated -- so it can prune a candidate only - /// when NO combination of rules in the grammar could ever produce something that long, regardless of - /// which specific root or derivation path is under consideration. Returns null (meaning "no admissible - /// bound, gate off") the moment any rule's shape falls outside what this class knows how to measure - /// exactly (quantifiers/groups/alternations in a phonological Lhs/Rhs, an insertion-type rewrite - /// subrule -- epenthesis/expansion, whose unapplication marks surface segments optional instead of - /// removing them, so the running shape length stops bounding the underlying length; see LT-22613 -- - /// or a compounding rule present at all, since compounding combines multiple full root lengths rather - /// than adding a bounded affix) -- per the plan's own rule: skipping only costs pruning opportunity, - /// an admissible bound must never be guessed. + /// (), + /// never estimated -- so it can prune a candidate only when NO combination of rules in the grammar + /// could ever produce something that long, regardless of which specific root or derivation path is + /// under consideration. Returns null (meaning "no admissible bound, gate off") the moment any rule's + /// shape falls outside what this class knows how to measure exactly (quantifiers/groups/alternations + /// in a phonological Lhs/Rhs, any phonological rewrite subrule whose Lhs and Rhs segment counts differ + /// -- see LT-22613, and the remarks on 's caller below -- or a + /// compounding rule present at all, since compounding combines multiple full root lengths rather than + /// adding a bounded affix) -- per the plan's own rule: skipping only costs pruning opportunity, an + /// admissible bound must never be guessed. /// public static class GrammarAnalyzer { @@ -31,23 +30,25 @@ public static class GrammarAnalyzer /// represent: the longest root allomorph in the lexicon, plus every affix/realizational rule's own /// maximum possible net insertion (its allomorphs' / /// actions, summed and multiplied by - /// ), - /// plus every phonological deletion-type subrule's maximum possible net restoration. Null if any - /// rule in the grammar can't be measured this way (see class remarks). + /// ). + /// Null if any rule in the grammar can't be measured this way (see class remarks) -- in + /// particular, every phonological rewrite subrule in the grammar must leave Lhs and Rhs segment + /// counts equal (a pure feature-changing rule, handled analysis-side by + /// FeatureAnalysisRewriteRuleSpec, which mutates matched segments in place and never + /// changes the shape's length). Any subrule where they differ -- epenthesis/expansion (Rhs longer) + /// or deletion/coalescence (Lhs longer) alike -- is unapplied analysis-side by + /// EpenthesisAnalysisRewriteRuleSpec/NarrowAnalysisRewriteRuleSpec, both of which + /// grow the candidate shape by inserting Lhs.Count new (Optional, not Deleted) nodes per + /// match site regardless of Rhs.Count, while leaving the matched Rhs-shaped region in place + /// (also not Deleted). counts every non-Deleted + /// segment, so real per-site growth is always exactly Lhs.Count -- never the naively + /// expected Lhs.Count - Rhs.Count, which undercounts by Rhs.Count whenever + /// Rhs.Count > 0 (LT-22613: first found via epenthesis pruning a valid analysis as + /// "too long" on the default non-tracing path while the tracing path, which bypasses this gate, + /// parsed it correctly; the same undercount also applies to ordinary deletion/coalescence rules, + /// which is why the bail-out is on any count mismatch, not just Rhs longer than Lhs). /// - /// - /// The phonological term is compounding, not additive: AnalysisRewriteRule's Deletion - /// reapply loop runs + 1 passes, and each pass is a - /// SimultaneousPhonologicalPatternRule sweep that can restore EVERY non-overlapping match - /// site in the current shape at once, not just one -- a real case (RewriteRuleTests - /// .MultipleDeletionRules: an 8-segment root deletes two independent "ii" clusters down to a - /// 4-segment surface form in one pass) needs more than "count of subrules" restored segments per - /// pass. Bounding the number of sites by the current running length (itself already an - /// over-approximation of the true pre-phonology length at this point) keeps this sound: real growth - /// can never exceed runningLength * subruleDelta per pass, since a simultaneous sweep cannot - /// match more sites than there are segments to match against. - /// - public static int? ComputeMaxAnalysisLength(Language language, int deletionReapplications) + public static int? ComputeMaxAnalysisLength(Language language) { int bound = 0; foreach (Stratum stratum in language.Strata) @@ -66,7 +67,6 @@ RealizationalAffixProcessRule rule in stratum.MorphologicalRules.OfType()) { if (!TryGetFlatSegmentCount(rule.Lhs, out int lhsCount)) @@ -75,26 +75,13 @@ RealizationalAffixProcessRule rule in stratum.MorphologicalRules.OfType rhsCount) - phonoGrowthRate += lhsCount - rhsCount; } } - for (int pass = 0; pass < deletionReapplications + 1 && phonoGrowthRate > 0; pass++) - bound += bound * phonoGrowthRate; } return bound; } diff --git a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs index 22295305..86189ba0 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs @@ -130,10 +130,10 @@ public ITraceManager TraceManager /// "Gate B") -- auto-derived from the grammar (: /// the longest lexicon root plus every rule's own maximum possible insertion) unless explicitly set. /// Setting this (including to null, which disables the gate entirely) overrides the - /// auto-derived value; re-derived fresh from the current grammar and - /// on every read otherwise, so it never goes stale if either changes after construction. Auto-derives - /// to null (gate off) when the grammar contains a compounding rule or a phonological rule shape - /// this analysis can't measure exactly -- see 's remarks. + /// auto-derived value; re-derived fresh from the current grammar on every read otherwise, so it + /// never goes stale if the grammar changes after construction. Auto-derives to null (gate + /// off) when the grammar contains a compounding rule or a phonological rule shape this analysis + /// can't measure exactly -- see 's remarks. /// public int? MaxAnalysisLength { @@ -141,7 +141,7 @@ public int? MaxAnalysisLength { return _maxAnalysisLengthOverrideSet ? _maxAnalysisLengthOverride - : GrammarAnalyzer.ComputeMaxAnalysisLength(_lang, DeletionReapplications); + : GrammarAnalyzer.ComputeMaxAnalysisLength(_lang); } set { diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs index 2e3125c8..0b28d4f6 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs @@ -880,11 +880,44 @@ public void ComputeMaxAnalysisLength_ReturnsNull_ForInsertionRewriteRule() Allophonic.PhonologicalRules.Add(epenthesis); try { - Assert.That(GrammarAnalyzer.ComputeMaxAnalysisLength(Language, 0), Is.Null); + Assert.That(GrammarAnalyzer.ComputeMaxAnalysisLength(Language), Is.Null); } finally { Allophonic.PhonologicalRules.Remove(epenthesis); } } + + [Test] + public void ComputeMaxAnalysisLength_ReturnsNull_ForDeletionRewriteRule() + { + // LT-22613 follow-up: a deletion/coalescence-type rewrite subrule (Lhs longer than Rhs) is + // unapplied by the same NarrowAnalysisRewriteRuleSpec as expansion, which always inserts + // Lhs.Count new (Optional, not Deleted) nodes per match site and leaves the matched Rhs-shaped + // region in place (also not Deleted) -- so real per-site growth is Lhs.Count, not the + // Lhs.Count - Rhs.Count this method used to budget. That undercounts by Rhs.Count whenever + // Rhs.Count > 0, making the bound inadmissible here too, not just for insertion. + var vowel = FeatureStruct + .New(Language.PhonologicalFeatureSystem) + .Symbol(HCFeatureSystem.Segment) + .Symbol("cons-") + .Symbol("voc+") + .Value; + var coalescence = new RewriteRule + { + Name = "coalescence", + ApplicationMode = RewriteApplicationMode.Simultaneous, + Lhs = Pattern.New().Annotation(vowel).Annotation(vowel).Value, + }; + coalescence.Subrules.Add(new RewriteSubrule { Rhs = Pattern.New().Annotation(vowel).Value }); + Allophonic.PhonologicalRules.Add(coalescence); + try + { + Assert.That(GrammarAnalyzer.ComputeMaxAnalysisLength(Language), Is.Null); + } + finally + { + Allophonic.PhonologicalRules.Remove(coalescence); + } + } } From b8dbde482eb60045d012de5d362ca66c947449c3 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Fri, 10 Jul 2026 18:16:54 -0400 Subject: [PATCH 4/4] Fix CSharpier formatting violations from recent HermitCrab changes CI's "Check formatting" step was failing because several files from the recent parse-optimization commits weren't run through `dotnet csharpier format`. No logic changes, just re-wrapping to match the enforced style. Co-Authored-By: Claude Sonnet 5 --- src/SIL.Machine.Morphology.HermitCrab/AnalysisStateKey.cs | 5 +---- src/SIL.Machine.Morphology.HermitCrab/GrammarAnalyzer.cs | 6 +++++- .../MemoizedCombinationRuleCascade.cs | 6 +++++- .../SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs | 4 +--- .../PhonologicalRules/RewriteRuleTests.cs | 6 +++++- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/SIL.Machine.Morphology.HermitCrab/AnalysisStateKey.cs b/src/SIL.Machine.Morphology.HermitCrab/AnalysisStateKey.cs index fe48f977..535593cd 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/AnalysisStateKey.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/AnalysisStateKey.cs @@ -87,10 +87,7 @@ public bool Equals(AnalysisStateKey other) return false; if (!_shape.ValueEquals(other._shape)) return false; - if ( - !_syntacticFS.ValueEquals(other._syntacticFS) - || !_realizationalFS.ValueEquals(other._realizationalFS) - ) + if (!_syntacticFS.ValueEquals(other._syntacticFS) || !_realizationalFS.ValueEquals(other._realizationalFS)) return false; return RuleCountsEqual(_ruleCounts, other._ruleCounts); } diff --git a/src/SIL.Machine.Morphology.HermitCrab/GrammarAnalyzer.cs b/src/SIL.Machine.Morphology.HermitCrab/GrammarAnalyzer.cs index 6d4c38d3..d1960788 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/GrammarAnalyzer.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/GrammarAnalyzer.cs @@ -56,7 +56,11 @@ public static class GrammarAnalyzer if (stratum.MorphologicalRules.OfType().Any()) return null; - int longestRoot = stratum.Entries.SelectMany(e => e.Allomorphs).Select(SegmentCount).DefaultIfEmpty(0).Max(); + int longestRoot = stratum + .Entries.SelectMany(e => e.Allomorphs) + .Select(SegmentCount) + .DefaultIfEmpty(0) + .Max(); bound += longestRoot; foreach (AffixProcessRule rule in stratum.MorphologicalRules.OfType()) diff --git a/src/SIL.Machine.Morphology.HermitCrab/MemoizedCombinationRuleCascade.cs b/src/SIL.Machine.Morphology.HermitCrab/MemoizedCombinationRuleCascade.cs index 7dc392b7..e3b18f57 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/MemoizedCombinationRuleCascade.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/MemoizedCombinationRuleCascade.cs @@ -75,7 +75,11 @@ private List ApplyRules(Word input, HashSet output) var replayed = new List(entry.Results.Count); foreach (Word storedResult in entry.Results) { - Word replay = storedResult.ReplayOnto(input, entry.MruleTrailPrefixLength, entry.NonHeadPrefixLength); + Word replay = storedResult.ReplayOnto( + input, + entry.MruleTrailPrefixLength, + entry.NonHeadPrefixLength + ); output.Add(replay); replayed.Add(replay); } diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs index 0b28d4f6..7d9f848b 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/MorpherTests.cs @@ -775,9 +775,7 @@ public void EnableLexicalGating_MatchesDisabled_SimpleAffixGrammar() List onResult = gateOn.ParseWord(word).ToList(); Assert.That( onResult.Select(WordResultSignature).OrderBy(s => s, System.StringComparer.Ordinal), - Is.EqualTo( - offResult.Select(WordResultSignature).OrderBy(s => s, System.StringComparer.Ordinal) - ), + Is.EqualTo(offResult.Select(WordResultSignature).OrderBy(s => s, System.StringComparer.Ordinal)), $"lexical-gate-on parse of '{word}' must match gate-off parse" ); } diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/PhonologicalRules/RewriteRuleTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PhonologicalRules/RewriteRuleTests.cs index 6495ca4a..b7dae6d2 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/PhonologicalRules/RewriteRuleTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PhonologicalRules/RewriteRuleTests.cs @@ -1380,7 +1380,11 @@ public void EpenthesisRuleWithMinimalLexicon() Name = "Allophonic", MorphologicalRuleOrder = MorphologicalRuleOrder.Unordered, }; - var surface = new Stratum(Table1) { Name = "Surface", MorphologicalRuleOrder = MorphologicalRuleOrder.Unordered }; + var surface = new Stratum(Table1) + { + Name = "Surface", + MorphologicalRuleOrder = MorphologicalRuleOrder.Unordered, + }; var entry = new LexEntry {